diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5e9eee0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,3 @@ +node_modules/ +dist/ +*.js.map diff --git a/README.md b/README.md index ec49b17..9eac13c 100644 --- a/README.md +++ b/README.md @@ -1,305 +1,141 @@ # @linimin/pi-headroom -A pi extension that routes supported pi providers through local Headroom proxies. +A pi extension that manages **one Headroom process per Pi process** with auto-start, health polling, and Windows lifecycle management. -## Quickstart +## One Headroom process per Pi process -```bash -# 1) Install Headroom first -curl -fsSL https://raw.githubusercontent.com/chopratejas/headroom/main/scripts/install.sh | bash +Each launched Pi process automatically spawns exactly one child Headroom process. This child process: -# 2) Install this pi package -pi install npm:@linimin/pi-headroom -``` +- starts from the current Pi working directory +- serves the current Pi session +- uses the Code Graph of the current working directory +- supports `openai-codex` +- supports `opencode-go` (with both `anthropic-messages` and `openai-completions` APIs) +- provides CCR retrieval via the native Pi tool `headroom_retrieve` +- starts automatically on session start +- shuts down when Pi shuts down +- works on Windows +- listens on a random localhost port on `127.0.0.1` +- does not use Unix sockets +- does not use a global shared Headroom +- does not spawn separate processes per provider -Then open pi and run: +### Architecture ```text -/reload -/model -/headroom-status +PowerShell #1 +C:\src\project-a +└── Pi process + └── Headroom process + ├── Code Graph: C:\src\project-a + ├── openai-codex + └── opencode-go + +PowerShell #2 +C:\src\project-b-worktree +└── Pi process + └── Headroom process + ├── Code Graph: C:\src\project-b-worktree + ├── openai-codex + └── opencode-go ``` -## What this extension does - -When you choose a supported provider in pi, the extension: - -- overrides that provider's `baseUrl` to a local Headroom proxy, -- expects one canonical local port per provider, -- does **not** automatically start, stop, or restart proxies for you, -- does **not** attach to arbitrary fallback ports, -- shows proxy status in the pi footer, -- shows expected ports, URLs, and routing details in `/headroom-status`. - -The goal is simple: you manage Headroom yourself, and this package cleanly attaches pi to the expected local proxy. - -## Lifecycle model - -`pi-headroom` is **attach-only**. - -That means: - -- you decide whether a proxy should be running, -- you can start Headroom yourself or use the explicit helper command `/headroom-start`, -- you stop Headroom yourself, -- this extension only checks whether the expected proxy is available and routes pi to it. - -If the proxy is not running, the extension tells you which port and URL are expected. - -## Prerequisite - -You must install **Headroom** first. This package does **not** install Headroom for you. It expects the `headroom` CLI to be available on `PATH` if you want to start proxies with the CLI yourself. - -If Headroom is missing or your proxy is not running, the extension will: - -- warn clearly, -- leave pi otherwise usable, -- tell you which provider port should be started. - -## Install - -### 1. Install Headroom - -See the official Headroom install docs for your platform. Typical examples: - -#### macOS - -```bash -curl -fsSL https://raw.githubusercontent.com/chopratejas/headroom/main/scripts/install.sh | "$(brew --prefix bash)/bin/bash" -``` - -#### Linux +## Quickstart ```bash -curl -fsSL https://raw.githubusercontent.com/chopratejas/headroom/main/scripts/install.sh | bash -``` - -#### Windows PowerShell - -```powershell +# 1) Install Headroom +# Windows: irm https://raw.githubusercontent.com/chopratejas/headroom/main/scripts/install.ps1 | iex -``` -### 2. Install the pi package +# macOS/Linux: +curl -fsSL https://raw.githubusercontent.com/chopratejas/headroom/main/scripts/install.sh | bash -```bash +# 2) Install this pi package pi install npm:@linimin/pi-headroom ``` -If pi is already running, reload extensions: +Then open pi and run: ```text /reload -``` - -A sample `docker-compose.yml` is included in this package root if you want to run Headroom proxies with Docker. A matching `.env.example` is also included for upstream override values. - -### 3. Optional: configure Docker env overrides - -```bash -cp .env.example .env -``` - -Then edit `.env` if you want to point a provider at a different upstream. - -For `github-copilot` in Docker, prefer setting an explicit `GITHUB_COPILOT_TOKEN` or `GITHUB_COPILOT_GITHUB_TOKEN` in `.env`. Docker containers generally cannot reuse your host keychain/session auth the same way local host processes can. - -For `github-copilot` on Business seats, prefer starting the proxy through `/headroom-start github-copilot` or otherwise ensure the proxy process has a current `GITHUB_COPILOT_API_TOKEN`. Reusing a host OAuth token through Headroom token exchange can downgrade the available model set for the proxy process. - -## Enable / use - -1. Open pi. -2. Select a supported provider/model with `/model`. -3. If you want traffic to go through Headroom, make sure the matching proxy is running on its expected local port. -4. Start using the model normally. - -If the proxy is not running, pi-headroom falls back to the provider default and tells you what to start if you want Headroom attached. - -## Quick verification - -Inside pi: - -```text +/model /headroom-status ``` -You should see: - -- the current managed provider, -- the expected local proxy root URL, -- the expected routed base URL, -- the dashboard URL, -- whether the proxy is currently reachable, -- whether pi is currently attached to Headroom or falling back to the provider default. - -## Supported providers - -### OpenAI-style, routed through `/v1` - -- `xai` -- `github-copilot` -- `openai` -- `openrouter` -- `deepseek` -- `groq` -- `together` -- `cerebras` -- `nvidia` -- `huggingface` -- `ant-ling` -- `moonshotai` -- `moonshotai-cn` -- `xiaomi` -- `xiaomi-token-plan-cn` -- `xiaomi-token-plan-ams` -- `xiaomi-token-plan-sgp` - -### Anthropic-style, routed through the root URL - -- `anthropic` -- `fireworks` -- `minimax` -- `minimax-cn` -- `kimi-coding` -- `mistral` - -### Gemini / Vertex - -- `google` -- `google-vertex` - -## Default local ports - -Each provider has one canonical expected local port. If you override a provider port with environment variables, pi-headroom will expect that overridden port instead. - -Default ports: - -- `xai` → `8787` -- `github-copilot` → `8788` -- `openai` → `8789` -- `anthropic` → `8790` -- `openrouter` → `8791` -- `deepseek` → `8792` -- `groq` → `8793` -- `together` → `8794` -- `mistral` → `8795` -- `fireworks` → `8796` -- `google` → `8797` -- `google-vertex` → `8798` -- `cerebras` → `8799` -- `nvidia` → `8800` -- `huggingface` → `8801` -- `ant-ling` → `8802` -- `moonshotai` → `8803` -- `moonshotai-cn` → `8804` -- `minimax` → `8805` -- `minimax-cn` → `8806` -- `xiaomi` → `8807` -- `xiaomi-token-plan-cn` → `8808` -- `xiaomi-token-plan-ams` → `8809` -- `xiaomi-token-plan-sgp` → `8810` -- `kimi-coding` → `8811` - -## Commands - -### `/headroom-status [provider|all]` - -If no provider is passed, it defaults to the provider of the currently selected model. - -Shows: - -- the current provider, -- the current model base URL, -- the expected local proxy root URL, -- the expected routed base URL, -- the dashboard URL, -- observed running/unavailable status. - -This is the main diagnostic command in attach-only mode. +## What this extension does -### `/headroom-start [provider|all]` +When you launch Pi: -Start Headroom on the canonical configured port for that provider. +1. The extension starts a Headroom child process from your current working directory +2. Headroom builds a Code Graph for that directory +3. The first request waits for Headroom to be ready (may take ~15 seconds for Code Graph) +4. Requests are routed through the local Headroom proxy +5. When Pi closes, Headroom is terminated automatically -If no provider is passed, it defaults to the provider of the currently selected model. +## Provider support -This command: +### OpenAI Codex -- only uses the provider's configured port, -- does not choose fallback ports, -- reports an error if the canonical port is occupied, -- leaves stop/restart decisions to you. +- Routes through `http://127.0.0.1:/backend-api` +- Uses Pi's standard Codex OAuth flow +- Streaming works out of the box -## Footer status +### OpenCode Go -The extension shows a lightweight status line in pi, for example: +- Anthropic models: routes through `http://127.0.0.1:` +- OpenAI models: routes through `http://127.0.0.1:/v1` +- Both use a single Headroom process (no restart on provider switch) -```text -Headroom:openai running | hist saved 33.0M | $83.3 | 36% -``` +### Other providers -If the expected proxy is not running, you will see something like: +All existing providers (OpenAI, Anthropic, Google, xAI, etc.) continue to work through the Headroom proxy. -```text -Headroom:github-copilot unavailable | /headroom-start github-copilot -``` - -## Important behavior +## Commands -- **Supported providers** are routed through Headroom. -- **Unsupported providers** fall back to normal pi behavior. -- The extension does not automatically manage proxy lifecycle. -- There is no automatic stop, restart, recovery, or fallback-port migration. -- `/headroom-start` is an explicit user-triggered helper, not background lifecycle management. -- One provider maps to one expected local proxy port. -- If the expected proxy is unavailable, the extension falls back to the provider default and tells you what should be running and where. -- Footer perf numbers prefer Headroom Historical Proxy Compression data (`/stats-history` lifetime stats, with `/stats` fallback). -- GitHub Copilot keeps pi's built-in OAuth flow and re-routes the final base URL through Headroom. +| Command | Description | +|---------|-------------| +| `/headroom-status` | Show Headroom process status, port, PID, and log path | +| `/headroom-restart` | Stop and restart Headroom for the current workspace | +| `/headroom-stop` | Stop the current Headroom process (auto-restarts on next request) | +| `/headroom-log` | Show the path to the Headroom log file | -## Useful environment +## Environment variables -Common ones: +| Variable | Description | Default | +|----------|-------------|---------| +| `PI_HEADROOM_COMMAND` | Path to Headroom executable | `headroom` (from PATH) | +| `PI_HEADROOM_FAIL_OPEN` | If `1`, allow requests without Headroom | `false` | +| `PI_HEADROOM_USER_ID` | Override user ID sent to Headroom | `USERNAME` env | +| `PI_HEADROOM_PROJECT_ID` | Optional project ID for Code Graph | (none) | +| `PI_HEADROOM_OPENCODE_GO_UPSTREAM` | OpenCode Go upstream URL | `https://opencode.ai/zen/go` | -- `PI_HEADROOM_BIN` -- `PI_HEADROOM_HOST` -- `PI_HEADROOM_VERBOSE` -- `PI_HEADROOM_PROBE_TIMEOUT_MS` -- `PI_HEADROOM_HEALTH_TTL_MS` +## Windows notes -Each provider also has optional port/upstream overrides such as: +- Supports `.exe` and `.cmd` launchers +- Supports paths with spaces and Cyrillic characters +- Process tree is terminated via `taskkill /T /F` +- Orphaned Headroom processes are cleaned up on startup -- `PI_HEADROOM_OPENAI_PORT` -- `PI_HEADROOM_OPENAI_UPSTREAM` -- `PI_HEADROOM_ANTHROPIC_PORT` -- `PI_HEADROOM_ANTHROPIC_UPSTREAM` -- `PI_HEADROOM_OPENROUTER_PORT` -- `PI_HEADROOM_OPENROUTER_UPSTREAM` -- etc. +## How startup works -## Not covered yet +1. On `session_start`, Headroom starts warming up (non-blocking) +2. On `before_agent_start`, the extension waits for Headroom health readiness +3. Once ready, provider routing is applied +4. The request is sent through the Headroom proxy -These pi providers still need more provider-specific handling and are not yet managed by this extension: +If Headroom takes more than a second to start, the user sees a notification. -- `amazon-bedrock` -- `azure-openai-responses` -- `cloudflare-ai-gateway` -- `cloudflare-workers-ai` -- `vercel-ai-gateway` -- `openai-codex` -- `opencode` -- `opencode-go` -- `zai` -- `zai-coding-cn` +## Two Pi sessions in one folder -## Package development +This is supported. Each Pi gets its own: -Local source layout: +- Runtime ID +- Port +- Headroom PID +- Process lifecycle -- `index.ts` — main attach-only supervisor logic -- `provider-registry.ts` — provider registry and routing specs +They do not interfere with each other. -Install locally during development: +## License -```bash -pi install /absolute/path/to/pi-headroom -``` +MIT diff --git a/headroom-commands.ts b/headroom-commands.ts new file mode 100644 index 0000000..fe68afd --- /dev/null +++ b/headroom-commands.ts @@ -0,0 +1,125 @@ +/** + * Headroom extension commands. + * + * /headroom-status — show current Headroom state + * /headroom-restart — stop and restart Headroom + * /headroom-stop — stop the current Headroom + * /headroom-log — show path to Headroom log file + */ + +import type { ExtensionAPI, ExtensionCtx } from "./types.js"; +import type { HeadroomSessionRuntime } from "./session-runtime.js"; + +type UiLevel = "info" | "warn" | "error" | "success"; + +function notifyUi( + ctx: ExtensionCtx, + message: string, + level: UiLevel, +): void { + try { + ctx.ui.notify(message, level); + } catch { + // ignore stale ctx errors + } +} + +/** + * Register all Headroom commands. + */ +export function registerCommands( + pi: ExtensionAPI, + getRuntime: () => HeadroomSessionRuntime, + applyProviderRouting: (ctx: ExtensionCtx) => Promise, +): void { + // ── /headroom-status ────────────────────────────────────── + pi.registerCommand("headroom-status", { + description: "Show Headroom process status and configuration.", + handler: async (_args: string, ctx: ExtensionCtx) => { + const runtime = getRuntime(); + const state = runtime.getState(); + const info = runtime.getProcessInfo(); + + const lines: string[] = [`State: ${state}`]; + + if (state === "starting") { + lines.push(`Elapsed: starting...`); + lines.push(`CWD: ${ctx.cwd ?? process.cwd()}`); + } else if (state === "ready" && info) { + lines.push(`Pi PID: ${runtime.ownerPid}`); + lines.push(`Headroom PID: ${info.pid}`); + lines.push(`Port: ${info.port}`); + lines.push(`CWD: ${info.cwd}`); + lines.push(`Runtime ID: ${runtime.runtimeId}`); + lines.push(`Code Graph: enabled`); + lines.push(`Log: ${info.logFile}`); + } else if (state === "failed") { + lines.push(`CWD: ${ctx.cwd ?? process.cwd()}`); + if (info?.logFile) { + lines.push(`Log: ${info.logFile}`); + } + lines.push(`Try /headroom-restart to retry.`); + } else { + lines.push(`CWD: ${ctx.cwd ?? process.cwd()}`); + } + + notifyUi(ctx, lines.join("\n"), state === "failed" ? "error" : "info"); + }, + }); + + // ── /headroom-restart ───────────────────────────────────── + pi.registerCommand("headroom-restart", { + description: "Stop and restart Headroom for the current workspace.", + handler: async (_args: string, ctx: ExtensionCtx) => { + const runtime = getRuntime(); + const cwd = ctx.cwd ?? process.cwd(); + + notifyUi(ctx, "Restarting Headroom...", "info"); + + await runtime.restart(cwd, "manual-restart"); + await applyProviderRouting(ctx); + + const info = runtime.getProcessInfo(); + if (info) { + notifyUi( + ctx, + `Headroom ready. Port: ${info.port}, PID: ${info.pid}`, + "success", + ); + } else { + notifyUi(ctx, "Headroom restart failed.", "error"); + } + }, + }); + + // ── /headroom-stop ──────────────────────────────────────── + pi.registerCommand("headroom-stop", { + description: "Stop the current Headroom process.", + handler: async (_args: string, ctx: ExtensionCtx) => { + const runtime = getRuntime(); + + if (runtime.getState() === "idle" || runtime.getState() === "stopped") { + notifyUi(ctx, "Headroom is not running.", "info"); + return; + } + + await runtime.stop("manual-stop"); + notifyUi(ctx, "Headroom stopped. It will restart on the next request.", "info"); + }, + }); + + // ── /headroom-log ───────────────────────────────────────── + pi.registerCommand("headroom-log", { + description: "Show the path to the Headroom log file.", + handler: async (_args: string, ctx: ExtensionCtx) => { + const runtime = getRuntime(); + const info = runtime.getProcessInfo(); + + if (info?.logFile) { + notifyUi(ctx, `Log file: ${info.logFile}`, "info"); + } else { + notifyUi(ctx, "No log file available. Headroom is not running.", "info"); + } + }, + }); +} diff --git a/headroom-process.ts b/headroom-process.ts new file mode 100644 index 0000000..d56ed98 --- /dev/null +++ b/headroom-process.ts @@ -0,0 +1,122 @@ +/** + * Headroom process spawning and health checking. + * + * Spawns a single Headroom child process and polls its health endpoint + * until it becomes ready or times out. + */ + +import type { ChildProcess } from "node:child_process"; +import { openSync } from "node:fs"; +import crossSpawn from "cross-spawn"; +import type { + SpawnedHeadroomProcess, + SpawnHeadroomOptions, +} from "./types.js"; + +/** + * Spawn a Headroom child process. + * + * Uses cross-spawn for Windows .cmd/.exe compatibility. + * The child is NOT detached — Pi remains the owner of the process tree. + */ +export function spawnHeadroomProcess( + options: SpawnHeadroomOptions, +): SpawnedHeadroomProcess { + const { command, cwd, port, logFile, env, args } = options; + + // Open log file for writing + const logFd = openSync(logFile, "a"); + + const child = crossSpawn(command, args, { + cwd, + env, + windowsHide: true, + detached: false, + stdio: ["ignore", logFd, logFd], + }); + + if (!child.pid) { + throw new Error( + `Failed to spawn Headroom: ${command} ${args.join(" ")}`, + ); + } + + return { + pid: child.pid, + child, + port, + }; +} + +/** + * Poll Headroom health endpoint until ready or timeout. + * + * Algorithm: + * 1. Start polling after initialDelayMs. + * 2. Increase interval gradually up to maxIntervalMs. + * 3. On each step: check child alive + health request. + * 4. If healthy, return. + * 5. If timeout, throw error with log path. + */ +export async function waitForHeadroomReady( + port: number, + child: ChildProcess, + timeoutMs: number, + initialDelayMs: number, + maxIntervalMs: number, + logFile: string, +): Promise { + const startedAt = Date.now(); + let interval = initialDelayMs; + + // Initial delay + await sleep(initialDelayMs); + + while (Date.now() - startedAt < timeoutMs) { + // Check if child has exited + if (child.exitCode !== null || child.killed) { + throw new Error( + `Headroom process exited unexpectedly (code: ${child.exitCode}). Log: ${logFile}`, + ); + } + + // Health check + const healthy = await probeHealth(port); + if (healthy) return; + + // Wait and increase interval + await sleep(interval); + interval = Math.min(interval * 1.5, maxIntervalMs); + } + + throw new Error( + `Headroom did not become ready within ${timeoutMs}ms. Log: ${logFile}`, + ); +} + +/** + * Probe Headroom health endpoint. + */ +async function probeHealth(port: number): Promise { + const controller = new AbortController(); + const timer = setTimeout(() => controller.abort(), 2_000); + + try { + for (const path of ["/livez", "/readyz", "/health"]) { + const response = await fetch(`http://127.0.0.1:${port}${path}`, { + method: "GET", + signal: controller.signal, + }); + if (response.ok) return true; + } + return false; + } catch { + return false; + } finally { + clearTimeout(timer); + } +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} diff --git a/headroom-provider-supervisor.README.md b/headroom-provider-supervisor.README.md deleted file mode 100644 index 98e24b4..0000000 --- a/headroom-provider-supervisor.README.md +++ /dev/null @@ -1,5 +0,0 @@ -# headroom-provider-supervisor - -This project was moved out of `~/.pi/agent/extensions/` into a standalone local project at `~/dev/pi-headroom`. - -See [README.md](./README.md) for the current behavior and usage. diff --git a/headroom-provider-supervisor.ts b/headroom-provider-supervisor.ts deleted file mode 100644 index f9fcc8f..0000000 --- a/headroom-provider-supervisor.ts +++ /dev/null @@ -1 +0,0 @@ -export { default } from "./index.ts"; diff --git a/headroom-retrieve.ts b/headroom-retrieve.ts new file mode 100644 index 0000000..5227163 --- /dev/null +++ b/headroom-retrieve.ts @@ -0,0 +1,144 @@ +/** + * Headroom CCR retrieval tool. + * + * Registers the `headroom_retrieve` Pi tool that allows models to + * retrieve compressed code context from Headroom's Code Graph. + * + * Note: The actual Pi tool registration API may differ. This module + * provides the tool logic. The exact integration depends on the Pi + * extension API surface, which should be verified against the real + * @earendil-works/pi-coding-agent types. + */ + +import type { ExtensionAPI, ExtensionCtx } from "./types.js"; +import type { HeadroomSessionRuntime } from "./session-runtime.js"; +import { canonicalizeWorkspacePath } from "./session-runtime.js"; + +/** + * Build the headroom_retrieve tool configuration. + * + * The execute function receives: + * - args: the tool parameters (hash, optional query) + * - ctx: the Pi extension context with cwd, ui, model, etc. + */ +export function buildHeadroomRetrieveTool( + getRuntime: () => HeadroomSessionRuntime, +) { + return { + description: + "Используй этот tool, когда в контексте присутствует Headroom CCR marker с hash и требуется восстановить исходное содержимое.", + parameters: { + hash: { + type: "string", + description: "Hash из Headroom CCR marker", + }, + query: { + type: "string", + description: + "Необязательный запрос для выбора релевантной части", + optional: true, + }, + }, + execute: async ( + args: Record, + ctx: unknown, + ): Promise => { + // Cast ctx to ExtensionCtx — Pi passes the real context here + const extensionCtx = ctx as ExtensionCtx; + const hash = args.hash as string | undefined; + const query = args.query as string | undefined; + + if (!hash) { + return "Missing required parameter: hash"; + } + + const runtime = getRuntime(); + + // Use ctx.cwd if available, fallback to process.cwd() + // ctx.cwd is the actual Pi working directory + const cwd = extensionCtx.cwd ?? process.cwd(); + const canonicalCwd = canonicalizeWorkspacePath(cwd); + + // Ensure Headroom is ready for this cwd + await runtime.ensureReady(canonicalCwd); + + const port = runtime.getPort(); + if (!port) { + return "Headroom is not running. Cannot retrieve CCR content."; + } + + // Build request body + const body: Record = { hash }; + if (query) { + body.query = query; + } + + // Make HTTP request to Headroom proxy + const controller = new AbortController(); + const timeout = setTimeout(() => controller.abort(), 30_000); + + try { + const response = await fetch( + `http://127.0.0.1:${port}/v1/retrieve`, + { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(body), + signal: controller.signal, + }, + ); + + if (response.status === 404) { + return `Hash ${hash} not found in current Headroom session (cwd: ${canonicalCwd}). The compressed content may not be available in this Code Graph.`; + } + + if (!response.ok) { + const text = await response.text().catch(() => ""); + return `Headroom retrieve failed (HTTP ${response.status}): ${text}`; + } + + // Parse response — try multiple field names + const result = (await response.json()) as Record; + + // Check common response formats + const content = + result.content ?? + result.text ?? + result.original_content ?? + result.results; + + if (typeof content === "string") { + return content; + } + if (Array.isArray(content) && content.length > 0) { + // If results is an array, join them + return content + .map((item: unknown) => + typeof item === "string" ? item : JSON.stringify(item), + ) + .join("\n---\n"); + } + + return JSON.stringify(result); + } catch (error) { + if (error instanceof Error && error.name === "AbortError") { + return "Headroom retrieve timed out after 30 seconds."; + } + return `Headroom retrieve error: ${error instanceof Error ? error.message : String(error)}`; + } finally { + clearTimeout(timeout); + } + }, + }; +} + +/** + * Register the headroom_retrieve tool with Pi. + */ +export function registerHeadroomRetrieveTool( + pi: ExtensionAPI, + getRuntime: () => HeadroomSessionRuntime, +): void { + const tool = buildHeadroomRetrieveTool(getRuntime); + pi.registerTool("headroom_retrieve", tool); +} diff --git a/index.ts b/index.ts index 9d35b0f..6e4c2e5 100644 --- a/index.ts +++ b/index.ts @@ -1,733 +1,195 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { spawn } from "node:child_process"; -import { createServer } from "node:net"; +/** + * pi-headroom — One Headroom process per Pi process. + * + * Architecture: + * Pi process → Headroom child process + * ├── Code Graph (cwd-specific) + * ├── openai-codex support + * └── opencode-go support + * + * Each Pi gets exactly one Headroom, started eagerly on session_start. + * The first request waits for health readiness before sending. + * + * In this mode, only openai-codex and opencode-go are managed. + * Other providers (openai, anthropic, etc.) are not routed through Headroom. + */ + +import type { ExtensionAPI, ExtensionCtx, ProviderModel } from "./types.js"; +import { + HeadroomSessionRuntime, + canonicalizeWorkspacePath, + workspaceComparisonKey, + cleanupOrphanedSessions, +} from "./session-runtime.js"; +import { getOrCreateSessionRuntime } from "./runtime-state.js"; import { - buildManagedProxyConfig, managedProviderFor, - preferredPortFor, providerIds, - registerManagedProvider, - type ManagedProvider, - type ManagedProxyConfig, - type ProviderModel, -} from "./provider-registry.ts"; - -type StatusLevel = "idle" | "running" | "unavailable"; -type UiLevel = "info" | "warn" | "error" | "success"; - -type ExtensionCtx = { - model?: ProviderModel; - ui: { - notify(message: string, level?: UiLevel): void; - setStatus?: (id: string, text: string) => void; - }; -}; - -type ModelSelectEvent = { model?: ProviderModel }; - -interface ManagedProxyState { - provider: ManagedProvider; - port: number; - rootUrl: string; - routedBaseUrl: string; - status: StatusLevel; - lastHealthyAt?: number; - lastError?: string; -} - -interface PerfSummary { - requestCount: number; - tokensSaved: number; - savingsUsd?: number; - savingsPercent: number; - basis: "history" | "runtime"; -} - -interface SupervisorState { - proxies: Map; - perf: Map; -} + registerProviderWithBaseUrl, + routedBaseUrlForModel, + isManagedModel, +} from "./provider-registry.js"; +import { buildHeadroomHeaders, resolveUserId } from "./request-identity.js"; +import { registerHeadroomRetrieveTool } from "./headroom-retrieve.js"; +import { registerCommands } from "./headroom-commands.js"; const STATUS_SLOT = "pi-headroom"; -const DEFAULT_HOST = process.env.PI_HEADROOM_HOST?.trim() || "127.0.0.1"; -const HEADROOM_BIN = process.env.PI_HEADROOM_BIN?.trim() || "headroom"; -const STATUS_COMMAND = - process.env.PI_HEADROOM_STATUS_COMMAND?.trim() || "headroom-status"; -const START_COMMAND = - process.env.PI_HEADROOM_START_COMMAND?.trim() || "headroom-start"; -const VERBOSE = /^(1|true|yes|on)$/i.test(process.env.PI_HEADROOM_VERBOSE ?? ""); -const PROBE_TIMEOUT_MS = parsePositiveInt( - process.env.PI_HEADROOM_PROBE_TIMEOUT_MS, - 1500, -); -const START_TIMEOUT_MS = parsePositiveInt( - process.env.PI_HEADROOM_START_TIMEOUT_MS, - 30000, -); -const PROBE_INTERVAL_MS = parsePositiveInt( - process.env.PI_HEADROOM_PROBE_INTERVAL_MS, - 500, -); -const HEALTH_TTL_MS = parsePositiveInt( - process.env.PI_HEADROOM_HEALTH_TTL_MS, - 5000, -); - -const resolvedConfigs = Object.fromEntries( - providerIds().map((provider) => [ - provider, - buildManagedProxyConfig(provider, preferredPortFor(provider), DEFAULT_HOST), - ]), -) as Record; - -function parsePositiveInt(raw: string | undefined, fallback: number): number { - if (!raw?.trim()) return fallback; - const value = Number.parseInt(raw.trim(), 10); - return Number.isFinite(value) && value > 0 ? value : fallback; -} - -function globalState(): SupervisorState { - const g = globalThis as typeof globalThis & { - __piHeadroomSupervisor__?: Partial; - }; - if (!g.__piHeadroomSupervisor__) g.__piHeadroomSupervisor__ = {}; - const state = g.__piHeadroomSupervisor__; - if (!(state.proxies instanceof Map)) state.proxies = new Map(); - if (!(state.perf instanceof Map)) state.perf = new Map(); - return state as SupervisorState; -} - -function getConfig(provider: ManagedProvider): ManagedProxyConfig { - return resolvedConfigs[provider]; -} - -function dashboardUrl(config: ManagedProxyConfig): string { - return `${config.rootUrl}/dashboard`; -} - -function buildBaseState(config: ManagedProxyConfig): ManagedProxyState { - return { - provider: config.provider, - port: config.port, - rootUrl: config.rootUrl, - routedBaseUrl: config.routedBaseUrl, - status: "idle", - }; -} -function formatCompactMetric(value: number): string { - const abs = Math.abs(value); - if (abs >= 1_000_000_000) return `${(value / 1_000_000_000).toFixed(1)}B`; - if (abs >= 1_000_000) return `${(value / 1_000_000).toFixed(1)}M`; - if (abs >= 1_000) return `${(value / 1_000).toFixed(1)}k`; - return String(Math.round(value)); -} +// ── Identity state ─────────────────────────────────────────────── -function formatSavingsPercent(value: number): string { - return `${Math.round(value)}%`; -} +let currentSessionId = ""; -function formatCompactUsd(value: number): string { - const abs = Math.abs(value); - if (abs >= 1_000_000_000) return `$${(value / 1_000_000_000).toFixed(1)}B`; - if (abs >= 1_000_000) return `$${(value / 1_000_000).toFixed(1)}M`; - if (abs >= 1_000) return `$${(value / 1_000).toFixed(1)}k`; - return `$${value.toFixed(1)}`; -} +/** + * Build a session-scoped ID. + * + * Uses runtime ID + a monotonic counter so that session switches + * within the same Pi process get distinct IDs. + */ +let sessionCounter = 0; -function deriveSavingsPercent(tokensSaved: number, totalInputTokens: number): number { - const totalBefore = Math.max(0, totalInputTokens) + Math.max(0, tokensSaved); - return totalBefore > 0 ? (tokensSaved / totalBefore) * 100 : 0; +function updateSessionId(ctx: ExtensionCtx): void { + const cwd = ctx.cwd ?? process.cwd(); + const canonical = canonicalizeWorkspacePath(cwd); + const cwdKey = workspaceComparisonKey(canonical); + sessionCounter++; + currentSessionId = `pi-${process.pid}-${sessionCounter}-${cwdKey}`; } -function parsePerfSummary(payload: unknown): PerfSummary | undefined { - if (!payload || typeof payload !== "object") return undefined; - - const data = payload as { - lifetime?: { - requests?: unknown; - tokens_saved?: unknown; - compression_savings_usd?: unknown; - total_input_tokens?: unknown; - }; - persistent_savings?: { - lifetime?: { - requests?: unknown; - tokens_saved?: unknown; - compression_savings_usd?: unknown; - total_input_tokens?: unknown; - }; - }; - requests?: { total?: unknown }; - tokens?: { saved?: unknown; savings_percent?: unknown }; - lifetime_stats?: { - total_requests?: unknown; - total_tokens_saved?: unknown; - total_input_tokens?: unknown; - total_estimated_savings_usd?: unknown; - }; - total_requests?: unknown; - total_tokens_saved?: unknown; - total_input_tokens?: unknown; - total_estimated_savings_usd?: unknown; - }; - - const lifetime = - data.lifetime ?? data.persistent_savings?.lifetime ?? data.lifetime_stats; - - if (lifetime && typeof lifetime === "object") { - const requestCount = numberOrZero( - "requests" in lifetime ? lifetime.requests : lifetime.total_requests, - ); - const tokensSaved = numberOrZero( - "tokens_saved" in lifetime - ? lifetime.tokens_saved - : lifetime.total_tokens_saved, - ); - const totalInputTokens = numberOrZero( - "total_input_tokens" in lifetime ? lifetime.total_input_tokens : undefined, - ); - const savingsUsd = numberOrUndefined( - "compression_savings_usd" in lifetime - ? lifetime.compression_savings_usd - : lifetime.total_estimated_savings_usd, - ); - - if (requestCount > 0 || tokensSaved > 0 || typeof savingsUsd === "number") { - return { - requestCount, - tokensSaved, - savingsUsd, - savingsPercent: deriveSavingsPercent(tokensSaved, totalInputTokens), - basis: "history", - }; - } - } - - const requestCount = numberOrZero( - data.requests?.total ?? data.total_requests, - ); - const tokensSaved = numberOrZero( - data.tokens?.saved ?? data.total_tokens_saved, - ); - const explicitSavingsPercent = numberOrUndefined(data.tokens?.savings_percent); - const totalInputTokens = numberOrZero(data.total_input_tokens); - const savingsUsd = numberOrUndefined(data.total_estimated_savings_usd); - - if ( - requestCount === 0 && - tokensSaved === 0 && - explicitSavingsPercent === undefined && - savingsUsd === undefined - ) { - return undefined; - } - - return { - requestCount, - tokensSaved, - savingsUsd, - savingsPercent: - explicitSavingsPercent ?? - deriveSavingsPercent(tokensSaved, totalInputTokens), - basis: "runtime", - }; -} +// ── Provider routing ───────────────────────────────────────────── -function numberOrZero(value: unknown): number { - return typeof value === "number" && Number.isFinite(value) ? value : 0; -} +async function applyCurrentProviderRouting( + pi: ExtensionAPI, + ctx: ExtensionCtx, + runtime: HeadroomSessionRuntime, +): Promise { + const model = ctx.model; + const provider = managedProviderFor(model); -function numberOrUndefined(value: unknown): number | undefined { - return typeof value === "number" && Number.isFinite(value) ? value : undefined; -} + if (!provider) return; -async function probeUrl(url: string): Promise { - const controller = new AbortController(); - const timer = setTimeout(() => controller.abort(), PROBE_TIMEOUT_MS); - try { - const response = await fetch(url, { method: "GET", signal: controller.signal }); - return response.ok; - } catch { - return false; - } finally { - clearTimeout(timer); - } -} + await runtime.ensureReady(ctx.cwd ?? process.cwd()); -async function probeLiveness(rootUrl: string): Promise { - for (const path of ["/livez", "/readyz", "/health"]) { - if (await probeUrl(`${rootUrl}${path}`)) return true; - } - return false; -} + const port = runtime.getPort(); + if (!port) return; -function probePortBindable(port: number): Promise { - return new Promise((resolve) => { - const server = createServer(); - let settled = false; + const rootUrl = `http://127.0.0.1:${port}`; - server.once("error", () => { - if (settled) return; - settled = true; - resolve(false); - }); + // Compute the provider-specific base URL (with /backend-api or /v1) + const baseUrl = routedBaseUrlForModel(provider, rootUrl, model); - server.once("listening", () => { - server.close(() => { - if (settled) return; - settled = true; - resolve(true); - }); - }); + const userId = resolveUserId(); - server.listen(port, DEFAULT_HOST); + const headers = buildHeadroomHeaders({ + cwd: runtime.getCwd() ?? ctx.cwd ?? process.cwd(), + userId, + sessionId: currentSessionId, }); -} - -function sleep(ms: number): Promise { - return new Promise((resolve) => setTimeout(resolve, ms)); -} -async function waitForProxyReady(rootUrl: string): Promise { - const startedAt = Date.now(); - while (Date.now() - startedAt < START_TIMEOUT_MS) { - if (await probeLiveness(rootUrl)) return true; - await sleep(PROBE_INTERVAL_MS); - } - return false; -} - -async function refreshProxyState(provider: ManagedProvider): Promise { - const config = getConfig(provider); - const state = globalState(); - const current = state.proxies.get(provider) ?? buildBaseState(config); - const healthy = await probeLiveness(config.rootUrl); - current.port = config.port; - current.rootUrl = config.rootUrl; - current.routedBaseUrl = config.routedBaseUrl; - current.status = healthy ? "running" : "unavailable"; - current.lastHealthyAt = healthy ? Date.now() : undefined; - current.lastError = healthy ? undefined : buildUnavailableMessage(provider); - state.proxies.set(provider, current); - return current; + // Register with the correctly computed baseUrl + registerProviderWithBaseUrl(pi, provider, baseUrl, headers); } -function isFreshHealthy(proxy: ManagedProxyState | undefined): boolean { - return !!proxy?.lastHealthyAt && Date.now() - proxy.lastHealthyAt < HEALTH_TTL_MS; -} +// ── UI helpers ─────────────────────────────────────────────────── -async function ensureObservedState(provider: ManagedProvider): Promise { - const current = globalState().proxies.get(provider); - if (isFreshHealthy(current)) return current as ManagedProxyState; - return refreshProxyState(provider); -} - -async function refreshPerfSummary( - provider: ManagedProvider, - options: { fresh?: boolean } = {}, -): Promise { - const config = getConfig(provider); - if (!(await probeLiveness(config.rootUrl))) return; - - const urls = options.fresh - ? [`${config.rootUrl}/stats-history`, `${config.rootUrl}/stats`] - : [`${config.rootUrl}/stats?cached=1`, `${config.rootUrl}/stats-history`]; - - for (const url of urls) { - try { - const response = await fetch(url); - if (!response.ok) continue; - const payload = (await response.json()) as unknown; - const summary = parsePerfSummary(payload); - if (!summary) continue; - globalState().perf.set(provider, summary); - return; - } catch { - // best-effort UI metric refresh only +function updateUiStatus(ctx: ExtensionCtx, runtime: HeadroomSessionRuntime): void { + try { + const state = runtime.getState(); + const info = runtime.getProcessInfo(); + + if (state === "ready" && info) { + ctx.ui.setStatus?.(STATUS_SLOT, `Headroom ready @ :${info.port}`); + } else if (state === "starting") { + ctx.ui.setStatus?.(STATUS_SLOT, "Headroom starting..."); + } else if (state === "failed") { + ctx.ui.setStatus?.(STATUS_SLOT, "Headroom failed"); + } else { + ctx.ui.setStatus?.(STATUS_SLOT, "Headroom idle"); } + } catch { + // Ignore stale ctx errors } } -function shellEscape(value: string): string { - if (/^[A-Za-z0-9_\-./:=]+$/.test(value)) return value; - return `'${value.replace(/'/g, `'"'"'`)}'`; -} - -function buildStartCommand(provider: ManagedProvider): string { - const config = getConfig(provider); - const envAssignments = Object.entries(config.env) - .sort(([a], [b]) => a.localeCompare(b)) - .map(([key, value]) => `${key}=${shellEscape(value)}`); - return [ - ...envAssignments, - shellEscape(HEADROOM_BIN), - "proxy", - "--host", - shellEscape(DEFAULT_HOST), - "--port", - String(config.port), - ].join(" "); -} +// ── Extension entry point ──────────────────────────────────────── -function buildUnavailableMessage(provider: ManagedProvider): string { - const config = getConfig(provider); - return [ - `Headroom proxy for ${provider} is not running.`, - `Expected port: ${config.port}`, - `Expected root URL: ${config.rootUrl}`, - `Expected routed base URL: ${config.routedBaseUrl}`, - `Start command: ${buildStartCommand(provider)}`, - ].join(" "); -} - -function activeStatusLine(model: ProviderModel | undefined): string { - const provider = managedProviderFor(model); - if (!provider) return "Headroom: off"; - - const perf = globalState().perf.get(provider); - const status = globalState().proxies.get(provider)?.status ?? "idle"; - const perfScope = perf?.basis === "history" ? "hist " : ""; - const usdSuffix = - typeof perf?.savingsUsd === "number" - ? ` | ${formatCompactUsd(perf.savingsUsd)}` - : ""; - const perfSuffix = perf - ? ` | ${perfScope}saved ${formatCompactMetric(perf.tokensSaved)}${usdSuffix} | ${formatSavingsPercent(perf.savingsPercent)}` - : ""; - - if (status === "running") { - return `Headroom:${provider} running${perfSuffix}`; - } - if (status === "unavailable") { - return `Headroom:${provider} unavailable | /${START_COMMAND} ${provider}`; - } - return `Headroom:${provider} idle | /${START_COMMAND} ${provider}`; -} +export default async function headroomExtension( + pi: ExtensionAPI, +): Promise { + // Clean up orphaned sessions from previous crashes + await cleanupOrphanedSessions().catch(() => {}); -function isStaleCtxError(error: unknown): boolean { - return ( - error instanceof Error && - error.message.includes( - "This extension ctx is stale after session replacement or reload", - ) + // Get or create the singleton runtime for this Pi process + const runtime = getOrCreateSessionRuntime( + () => new HeadroomSessionRuntime(), ); -} - -function currentModelOrUndefined(ctx: ExtensionCtx): ProviderModel | undefined { - try { - return ctx.model; - } catch (error) { - if (isStaleCtxError(error)) return undefined; - throw error; - } -} - -function notifyUi( - ctx: ExtensionCtx, - message: string, - level: "info" | "warn" | "error", -): void { - try { - ctx.ui.notify(message, level); - } catch (error) { - if (isStaleCtxError(error)) return; - throw error; - } -} - -function updateUiStatus(ctx: ExtensionCtx, model?: ProviderModel): void { - try { - const resolvedModel = model ?? currentModelOrUndefined(ctx); - ctx.ui.setStatus?.(STATUS_SLOT, activeStatusLine(resolvedModel)); - } catch (error) { - if (isStaleCtxError(error)) return; - throw error; - } -} -function statusLines(model: ProviderModel | undefined): string[] { - const activeProvider = managedProviderFor(model); - const state = globalState(); - const relevantProviders = activeProvider - ? [activeProvider, ...providerIds().filter((provider) => provider !== activeProvider)] - : providerIds(); - - const proxyLines = relevantProviders.flatMap((provider) => { - const config = getConfig(provider); - const proxy = state.proxies.get(provider); - const status = proxy?.status ?? "idle"; - if (provider !== activeProvider && status !== "running") return []; - - const detail = - status === "running" - ? `running @ ${config.rootUrl} (pi routed through Headroom)` - : `unavailable; expected ${config.rootUrl} (pi falls back to default provider)`; - return [ - `- ${provider}: ${detail}`, - ` routed base: ${config.routedBaseUrl}`, - ` dashboard: ${dashboardUrl(config)}`, - ]; + // Register commands + registerCommands(pi, () => runtime, async (ctx) => { + await applyCurrentProviderRouting(pi, ctx, runtime); }); - const activeConfig = activeProvider ? getConfig(activeProvider) : undefined; - return [ - `Managed provider: ${activeProvider ?? "(current model is unmanaged)"}`, - `Current model: ${model ? `${model.provider}/${model.id ?? "(unknown)"}` : "(none)"}`, - `Current model base URL: ${model?.baseUrl ?? "(none)"}`, - `Expected proxy root: ${activeConfig?.rootUrl ?? "(none)"}`, - `Expected routed base URL: ${activeConfig?.routedBaseUrl ?? "(none)"}`, - `Active dashboard: ${activeConfig ? dashboardUrl(activeConfig) : "(none)"}`, - `Footer status: ${activeStatusLine(model)}`, - "Lifecycle: manual proxy management; this extension only attaches.", - activeConfig - ? `If unavailable, pi falls back to the provider default. Start Headroom manually on port ${activeConfig.port} to attach it.` - : "Select a managed provider to see expected Headroom port.", - "Proxy map:", - ...(proxyLines.length ? proxyLines : ["(no running managed provider proxies observed)"]), - ]; -} - -async function reconcileProviderRouting( - pi: ExtensionAPI, - provider: ManagedProvider, -): Promise { - const proxy = await ensureObservedState(provider); - if (proxy.status === "running") { - registerManagedProvider(pi, provider, getConfig(provider)); - await refreshPerfSummary(provider); - } else { - pi.unregisterProvider(provider); - globalState().perf.delete(provider); - } - return proxy; -} + // Register CCR retrieval tool + registerHeadroomRetrieveTool(pi, () => runtime); -async function refreshForModel( - pi: ExtensionAPI, - model: ProviderModel | undefined, -): Promise { - const provider = managedProviderFor(model); - if (!provider) return; - await reconcileProviderRouting(pi, provider); -} + // ── Lifecycle events ───────────────────────────────────── -function parseProviderArg(raw: string | undefined): ManagedProvider | "all" | undefined { - const value = raw?.trim().toLowerCase(); - if (!value) return undefined; - if (value === "all") return "all"; - return providerIds().includes(value as ManagedProvider) - ? (value as ManagedProvider) - : undefined; -} + // session_start: eager warmup (non-blocking) + pi.on("session_start", async (_event: unknown, ctx: ExtensionCtx) => { + updateSessionId(ctx); + const cwd = ctx.cwd ?? process.cwd(); -function registerCommands(pi: ExtensionAPI): void { - pi.registerCommand(STATUS_COMMAND, { - description: - "Show Headroom routing, expected proxy ports, and observed proxy status.", - handler: async (args: string, ctx: ExtensionCtx) => { - const selected = parseProviderArg(args); - if (args.trim() && !selected) { - const known = [...providerIds(), "all"].join(", "); - notifyUi( - ctx, - `Unknown provider ${JSON.stringify(args.trim())}. Use ${known}.`, - "error", - ); - return; - } + // Non-blocking warmup + void runtime.warmup(cwd).catch((error) => { + runtime.recordWarmupError(error); + }); - const currentProvider = managedProviderFor(currentModelOrUndefined(ctx)); - const providers = selected - ? selected !== "all" - ? [selected] - : providerIds() - : currentProvider - ? [currentProvider] - : []; - if (!providers.length) { - notifyUi( - ctx, - "No managed provider selected. Choose a managed model first or pass /headroom-status [provider|all].", - "error", - ); - return; - } - await Promise.all( - providers.map((provider) => reconcileProviderRouting(pi, provider)), - ); - - const model = currentModelOrUndefined(ctx); - const activeProvider = managedProviderFor(model); - if (activeProvider && providers.includes(activeProvider)) { - await refreshPerfSummary(activeProvider, { fresh: true }); - } - updateUiStatus(ctx, model); - notifyUi(ctx, statusLines(model).join("\n"), "info"); - }, + updateUiStatus(ctx, runtime); }); - pi.registerCommand(START_COMMAND, { - description: - "Start Headroom on the canonical configured port. Usage: /headroom-start [provider|all]", - handler: async (args: string, ctx: ExtensionCtx) => { - const selected = parseProviderArg(args); - if (args.trim() && !selected) { - const known = [...providerIds(), "all"].join(", "); - notifyUi( - ctx, - `Unknown provider ${JSON.stringify(args.trim())}. Use ${known}.`, - "error", - ); - return; - } + // before_agent_start: readiness barrier + provider routing + pi.on("before_agent_start", async (_event: unknown, ctx: ExtensionCtx) => { + const cwd = ctx.cwd ?? process.cwd(); - const currentProvider = managedProviderFor(currentModelOrUndefined(ctx)); - const providers = selected - ? selected !== "all" - ? [selected] - : providerIds() - : currentProvider - ? [currentProvider] - : []; - if (!providers.length) { - notifyUi( - ctx, - "No managed provider selected. Choose a managed model first or pass /headroom-start [provider|all].", - "error", - ); - return; - } + // Wait for Headroom readiness + await runtime.ensureReady(cwd); - const started: ManagedProvider[] = []; - const alreadyRunning: ManagedProvider[] = []; - const failures: string[] = []; + // Apply provider routing with the model from ctx + await applyCurrentProviderRouting(pi, ctx, runtime); - for (const provider of providers) { - const config = getConfig(provider); - if (await probeLiveness(config.rootUrl)) { - await reconcileProviderRouting(pi, provider); - alreadyRunning.push(provider); - continue; - } + updateUiStatus(ctx, runtime); + }); - if (!(await probePortBindable(config.port))) { - failures.push( - `${provider}: port ${config.port} is occupied. Expected command: ${buildStartCommand(provider)}`, - ); - continue; - } + // model_select: update routing for the new model + pi.on("model_select", async (event: unknown, ctx: ExtensionCtx) => { + const modelEvent = event as { model?: ProviderModel }; - try { - const child = spawn( - HEADROOM_BIN, - ["proxy", "--host", DEFAULT_HOST, "--port", String(config.port)], - { - env: { ...process.env, ...config.env }, - detached: true, - stdio: "ignore", - }, - ); - child.unref(); - } catch (error) { - failures.push( - `${provider}: failed to start Headroom via ${JSON.stringify(HEADROOM_BIN)}. ${error instanceof Error ? error.message : String(error)}`, - ); - continue; - } + // If runtime is already ready, apply routing for the new model + // Use the model from the event, not from ctx (which may be stale) + if (runtime.getState() === "ready") { + const model = modelEvent.model ?? ctx.model; + const provider = managedProviderFor(model); - const ready = await waitForProxyReady(config.rootUrl); - if (!ready) { - failures.push( - `${provider}: timed out waiting for proxy on ${config.rootUrl}. Expected command: ${buildStartCommand(provider)}`, - ); - continue; + if (provider) { + const port = runtime.getPort(); + if (port) { + const rootUrl = `http://127.0.0.1:${port}`; + const baseUrl = routedBaseUrlForModel(provider, rootUrl, model); + const userId = resolveUserId(); + const headers = buildHeadroomHeaders({ + cwd: runtime.getCwd() ?? ctx.cwd ?? process.cwd(), + userId, + sessionId: currentSessionId, + }); + registerProviderWithBaseUrl(pi, provider, baseUrl, headers); } - - await reconcileProviderRouting(pi, provider); - started.push(provider); } - - const model = currentModelOrUndefined(ctx); - updateUiStatus(ctx, model); - - const notices: string[] = []; - if (started.length) notices.push(`Started Headroom for ${started.join(", ")}.`); - if (alreadyRunning.length) notices.push(`Already running: ${alreadyRunning.join(", ")}.`); - if (failures.length) notices.push(...failures); - - notifyUi( - ctx, - notices.length ? notices.join("\n") : "No managed providers selected.", - failures.length ? "warn" : "info", - ); - }, - }); -} - -export default async function headroomProviderSupervisor( - pi: ExtensionAPI, -): Promise { - for (const provider of providerIds()) { - pi.unregisterProvider(provider); - } - registerCommands(pi); - - pi.on("session_start", async (_event: unknown, ctx: ExtensionCtx) => { - const model = currentModelOrUndefined(ctx); - await refreshForModel(pi, model); - updateUiStatus(ctx, model); - }); - - pi.on("model_select", async (event: ModelSelectEvent, ctx: ExtensionCtx) => { - await refreshForModel(pi, event.model); - updateUiStatus(ctx, event.model); - }); - - pi.on("before_agent_start", async (_event: unknown, ctx: ExtensionCtx) => { - const model = currentModelOrUndefined(ctx); - const provider = managedProviderFor(model); - if (!provider) { - updateUiStatus(ctx, model); - return; } - const proxy = await reconcileProviderRouting(pi, provider); - updateUiStatus(ctx, model); - if (proxy.status !== "running") { - notifyUi( - ctx, - `${buildUnavailableMessage(provider)} Falling back to the provider default.`, - "warn", - ); - } + updateUiStatus(ctx, runtime); }); - pi.on("agent_end", async (_event: unknown, ctx: ExtensionCtx) => { - const model = currentModelOrUndefined(ctx); - const provider = managedProviderFor(model); - if (!provider) { - updateUiStatus(ctx, model); - return; - } - await reconcileProviderRouting(pi, provider); - await refreshPerfSummary(provider, { fresh: true }); - updateUiStatus(ctx, model); + // session_shutdown: stop Headroom + pi.on("session_shutdown", async (_event: unknown, _ctx: ExtensionCtx) => { + await runtime.stop("pi-session-shutdown"); }); - - if (VERBOSE) { - pi.on("session_start", async (_event: unknown, ctx: ExtensionCtx) => { - const model = currentModelOrUndefined(ctx); - const provider = managedProviderFor(model); - if (!provider) return; - const proxy = await ensureObservedState(provider); - if (proxy.status !== "running") { - notifyUi( - ctx, - `${buildUnavailableMessage(provider)} Using the provider default until Headroom is available.`, - "warn", - ); - } - }); - } } diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..a9bc214 --- /dev/null +++ b/package-lock.json @@ -0,0 +1,4981 @@ +{ + "name": "@linimin/pi-headroom", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@linimin/pi-headroom", + "version": "0.2.0", + "license": "MIT", + "dependencies": { + "cross-spawn": "^7.0.6" + }, + "devDependencies": { + "@types/cross-spawn": "6.0.6", + "@types/node": "^22.15.3", + "typescript": "^5.8.3", + "vitest": "^3.1.1" + }, + "peerDependencies": { + "@earendil-works/pi-ai": "*", + "@earendil-works/pi-coding-agent": "*" + } + }, + "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", + "peer": true, + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.23.tgz", + "integrity": "sha512-MiWR/uWjxjFXGzrE0Ghc5lWxUxzHsUWFhV+OX7M4cR9SrmrnZs6TXavnCWnzzdwJeFri34xQo81rvGNzK3c4BQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@aws-sdk/xml-builder": "^3.972.31", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.49.tgz", + "integrity": "sha512-liB3yQNHCM9k/gu/w36XHMKPluT7HTlnGUhRbBGSISDQkcr/Sy1zsZabiuvQj8WG5yW573u9RehrBvvnIQ9OEQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.51", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.51.tgz", + "integrity": "sha512-XET0H2oofciJ5lMRWNIvRjAP7Q3wv2XT+JtJJEdhPWUMwe3TvQ9qcxonpu7vXmNngncvFpi4E2It+Tamas/naA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.2.tgz", + "integrity": "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.56", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.56.tgz", + "integrity": "sha512-IAmc61hbgQiHht9U3x0tnRwz0lzdwOwD/i9voRgdJrKamF+JtmrBOsW9GwB7mfFonNWOWL4qARWYrF8veEMe3w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/credential-provider-env": "^3.972.49", + "@aws-sdk/credential-provider-http": "^3.972.51", + "@aws-sdk/credential-provider-login": "^3.972.55", + "@aws-sdk/credential-provider-process": "^3.972.49", + "@aws-sdk/credential-provider-sso": "^3.972.55", + "@aws-sdk/credential-provider-web-identity": "^3.972.55", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.55.tgz", + "integrity": "sha512-hBBkANo3cDn+h2qxxzER4a+J8JCO9o9Z/YYmU7iky6AcaarX5RRdRcHNC6SLdwY0vAXQygn6soUbDqPn3GghaA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.58", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.58.tgz", + "integrity": "sha512-OyCLVmSI7pZO8hxwNVX6pXhTVlJqRBTp+ijdEfJSUj0RyjHnF602OfAarOzGq6wkGodeFkYBt8MmJ6A6ycRgWw==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.49", + "@aws-sdk/credential-provider-http": "^3.972.51", + "@aws-sdk/credential-provider-ini": "^3.972.56", + "@aws-sdk/credential-provider-process": "^3.972.49", + "@aws-sdk/credential-provider-sso": "^3.972.55", + "@aws-sdk/credential-provider-web-identity": "^3.972.55", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/credential-provider-imds": "^4.3.7", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.49", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.49.tgz", + "integrity": "sha512-C8h36lBuC/RnBSsjlO+dn6xZm3KbAl5vpJaVPAfQnMmz2/OISmKOc8XZcqMQgO2ADwBYNRMM6Kf3vz9G/TulMQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.55.tgz", + "integrity": "sha512-1FkOz74Ea5QGS9jtIoXp55T/IkSS3spv+nLTT07fRY/+T5xmEOqaYBVIaEmX4zTNvbV6g2lrtlaVKWEoNyJt3w==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/token-providers": "3.1074.0", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1074.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1074.0.tgz", + "integrity": "sha512-pv80IzgGW4RnXWtft692chZOM9i6PhebVsLCcnaM4dBEPZva2fE6FXAHs76G7Rc7s3yGyX/68G0nZMrUy+Vmpg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.55", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.55.tgz", + "integrity": "sha512-g2BoECD1q01kTPByi56+VLVvdWDzMkKIcr77qixpqH0okw2t0U5CoPv+6S8v/D1Y2Wa6QKKtn6XAtDzP+Kfpvg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/nested-clients": "^3.997.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.22", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.22.tgz", + "integrity": "sha512-tqPJv0dz4+O0hWGm1a6YekcMZyPhDFs/zH73Von7icaVT5n0Jqvm86typ3jRrG+qoUdPhALOnboRLTmnWQTlYQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.18", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.18.tgz", + "integrity": "sha512-OHpk8YoZi3yexPq8aFt1vN1IxA2zLKvsIR5GpWYylX/ve6kQmY7wxHNSFy/D3t2apMZ16rs76Co4dJWcDyIk3A==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.31.tgz", + "integrity": "sha512-ps1rumU1LybSFHaW9dTDgkhCMJLVaedEY78kKSzUDDY+b9974/g6aiaYYA0U9WV0oL4CJCJrVWG+EZ/qr4or7g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.23", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.23.tgz", + "integrity": "sha512-gO93ZPsI2bxeFZD42f1/qjDw6FAZkNZcKRO94LIiT03fzOmcJ9e/tunxjVjA1Rl69ClmVJzz8H3G9CdKef10PA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.974.23", + "@aws-sdk/signature-v4-multi-region": "^3.996.35", + "@aws-sdk/types": "^3.973.13", + "@smithy/core": "^3.24.6", + "@smithy/fetch-http-handler": "^5.4.6", + "@smithy/node-http-handler": "^4.7.6", + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.8.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.8.2.tgz", + "integrity": "sha512-wfl1uwrAqMH9/pi4kqBo5LBcFwrJLxuDLqL7p7qNcJIFcyZDUc6pzhYk4CYv+DP7fIUpQCZumwNnkhPKS52osQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.35", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.35.tgz", + "integrity": "sha512-6L/VWs+Wch2stHemCGTmUNqKLMzURxQDK5boNG3Jn3kAOp71meDUuS5sbObpEvFxHDq0uWeSLFDNSYsjNt+Dlg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.973.13", + "@smithy/signature-v4": "^5.4.6", + "@smithy/types": "^4.14.3", + "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", + "peer": true, + "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.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.13.tgz", + "integrity": "sha512-pEHZqRkAlHfnfAU9tK+WpKv/gBNjGJrHMgA3A0iYRGyswBS2t0pfez+lWlwktb3Bqa0ovh7w/QJTFwp3fDxLNg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.14.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", + "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.31.tgz", + "integrity": "sha512-SzE4Pgyl+hDF+BuyuzxUSpwnuUu9lJuO1YGgteG89/4Qv0+2IQiVQqdbPV32IozLvXWQChPQcdkk/sKvb1QHiQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/types": "^4.14.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", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", + "integrity": "sha512-9jR23tOl0BIUdQMn70Gr72xYBpM7Xgl9Lyv7gAnU1USfkNRuYG/f/edLl+n/Dp/RafDW3JI4DF7y/GhgkORuew==", + "license": "MIT", + "peer": true, + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "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-coding-agent": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.79.10.tgz", + "integrity": "sha512-YxaRhmgyDTvLDdGVbe7YzTHV80oL5mX5odg6EhGHz3w5Wu1Ix8DCw7bhtiOBLGQNFRcknia0zPmVWIj30XP1EA==", + "hasShrinkwrap": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@earendil-works/pi-agent-core": "^0.79.10", + "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-tui": "^0.79.10", + "@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", + "semver": "7.8.0", + "typebox": "1.1.38", + "undici": "8.5.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.10.tgz", + "license": "MIT", + "peer": true, + "dependencies": { + "@earendil-works/pi-ai": "^0.79.10", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", + "license": "MIT", + "peer": true, + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "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-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.10.tgz", + "license": "MIT", + "peer": true, + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", + "cpu": [ + "arm64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", + "cpu": [ + "arm64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", + "cpu": [ + "riscv64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", + "cpu": [ + "x64" + ], + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", + "cpu": [ + "x64" + ], + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "peer": true, + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/@earendil-works/pi-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", + "peer": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">= 12" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "peer": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true, + "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" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "license": "ISC", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "license": "MIT", + "peer": true, + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "license": "MIT", + "peer": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "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/@earendil-works/pi-coding-agent/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", + "peer": true, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-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", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "license": "ISC", + "peer": true, + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@earendil-works/pi-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", + "peer": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-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", + "peer": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-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", + "peer": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@earendil-works/pi-coding-agent/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", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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/@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", + "peer": true, + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "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/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "license": "Apache-2.0", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "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", + "peer": true + }, + "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", + "peer": true + }, + "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", + "peer": true + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "license": "BSD-3-Clause", + "peer": true + }, + "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", + "peer": true, + "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", + "peer": true + }, + "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", + "peer": true + }, + "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", + "peer": true + }, + "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", + "peer": true + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.62.2.tgz", + "integrity": "sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.62.2.tgz", + "integrity": "sha512-BaH7BllCACHoH1LguOU56UItGfUWjujlO65kS9LAodViaN4bwIKd7oeW/ZHJ/4ljr/7MIiENnNy3HJ0zXv8Zkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.62.2.tgz", + "integrity": "sha512-v39RCCvj4He82I9sFmk+M1VZ0PLM9sfsLVikjfx2hYBNALhrrOR2D3JjQA6AhlaSOgcR+RzrKY7e1+bT6SUO/A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.62.2.tgz", + "integrity": "sha512-yl0y2vq3S3lHeuXhEdss6TWfKW8vkujImO12tn4ZkG/4oghr09LvdYm2RElVjokTQiUvDUGXLGsYeLqUMCKpGA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.62.2.tgz", + "integrity": "sha512-tT4pvt4qXD+vEoezupCWi+a1F0vvDiksiHc+PxRlYTOH1I6/X4id9jPxTP+Fg+545euaFT1jJVs4CEdHZAU1vw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.62.2.tgz", + "integrity": "sha512-6nU5F2wCW+qvCBhTn1pdIU3bzsIoF7EUwsCDRxilWGprQR6yd508YnH9+OKFCwpfS8pjZqDUmnCAr7exax0XCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.62.2.tgz", + "integrity": "sha512-n1GJHPOvpIfhi3TmrCeh6S6URt9BFCt0KQE3qvexyGCTAKpR4Lg+eWvNZEqu7epxwus/8ElT3hacYEucm49SZg==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.62.2.tgz", + "integrity": "sha512-JqgflS8wEB+UXV/vS1RpRbifGBeN4D5lz8D8oOFbFZw4vedvdOgCFAjfBmIMdW3yL10XpQQ0Ambepw6MXrhOnA==", + "cpu": [ + "arm" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.62.2.tgz", + "integrity": "sha512-wnFJkogWvN4jm/hQRF2UBaeUmk20j5+DmHvoyWii2b8HJDyvz1MF2OU/6ynXt2KR63rbZLWkFpoytpdc/yBuSA==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.62.2.tgz", + "integrity": "sha512-HVu2bp0zhvJ8xHEV9+UUs7S90VadmBSY3LcIMvozbPo4AuMGDWlz3ymHLHZPX4hR67TKTt8Qp5PJ5RBg/i+RMQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.62.2.tgz", + "integrity": "sha512-mQqqAV8QaoSgr9I2fKDLY2BAVvmKjWoGiu/cSYQonsLvtqwEn1E4QYfnCOcp5zoEqNhsDYin1s6jx/VJmrxlZg==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.62.2.tgz", + "integrity": "sha512-IxKLoxCQ2IWi6bT2akyDUBGsOImDKB+sPp4EsTmwFQ/fMwpCKm8uLSSgP/Kx/QYUgKis6SEZ5/Nlhup0DIA0PQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.62.2.tgz", + "integrity": "sha512-Mk5ha2RQSgyFfmYYLkBpPnUk8D8FriBxesO1u9O75X0mHgXL1UQcH5Itl2lurWL2tj0RxV9b9tJgipac0hRY9A==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.62.2.tgz", + "integrity": "sha512-CjvEnqJL/0/TQ3TXX3OPIJ/kmBellrWd4heXUmHeJlTnmwjKpSJzoehLaL6Xk0ZnMHBu9dZuFADNOrtjF4v+2w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.62.2.tgz", + "integrity": "sha512-1SiZbzwdkaDURsew/tSOrooKiYy7EQGT6m8ufavAi9NEyQb/6VuIxFXAL1fqa4iZe3g4NbNk4P7J32z2tw5Mgg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.62.2.tgz", + "integrity": "sha512-nQts12zJ3NQRoE6uYljOH89v7szzLDvG2JD/vsX+vGXU8w/At1GowTZ5/7qeFQ8m7L55rpR8Okugnuo5bgjy2Q==", + "cpu": [ + "riscv64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.62.2.tgz", + "integrity": "sha512-E9/ll019jhPIJgpzfZoIkBGhcz+kKNgVWYRY0zr9srBdPPFVpvOKW8VaJKUbeK+eZXyQF9ltME+Kk6affeaPgg==", + "cpu": [ + "s390x" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.62.2.tgz", + "integrity": "sha512-5BqxR/pshjey51iliyzTD5Xi3EN0aLmQ2lZ3lvefVV9c82BvrLo2/6OT55iifpWBufs6kdwWbuOKS841DrmK9A==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "glibc" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.62.2.tgz", + "integrity": "sha512-uNN83XxQrRAh/w0/pmAfibcwyb6YWt4gP+dpnQKPVJshAloQ785ii8CT8ZCIxkGg9opVsvAlGhFitSm6D1Jjpg==", + "cpu": [ + "x64" + ], + "dev": true, + "libc": [ + "musl" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.62.2.tgz", + "integrity": "sha512-srjEIxSH3LRnJN6THczDHWQplqEMFiAJrTab0msUryh9kwNpkICf3Ea6q6MN/2cZwRFUNx5w+h6Hpi4QuHS6Zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.62.2.tgz", + "integrity": "sha512-8hOJnxgbyObnCm5AlRA3A931xX19xq80RjVTKgJOvEKWqJruP/Uf12IbAOaDjjEXYRewwHLfmF0YRIdK3OwKWA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.62.2.tgz", + "integrity": "sha512-mmF4AY1i0hG/bLWUctUq59gtmgaSIRa3cu/A3JFRp/sCNEme2bgDEiDS22P9FbnJB8NJNF4jPJiSP5RHQpUTDg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.62.2.tgz", + "integrity": "sha512-DZgkknc6jhHrk46V25vbAM0zZkyP0nSDkJB8/dRkLTxv470dOmWDqGoEJl/9A0dFfS7yE3REOwNDxpHwSLSt0Q==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.62.2.tgz", + "integrity": "sha512-T6xr6ucWSFto+VGajA8YH26LdpHRuP4YLHEKAtCWvJDOlnmWcDZVCI2Jmjr+IFHDlt2zRaTAKE4tfjTaWLgJBg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.62.2.tgz", + "integrity": "sha512-BfzEnDJOt9T8M989/lA37EcJgat01wLRnoi5dQf3QzOH7jzpqTAzdDbVfRljVr5r+jzKqpbHeyOfAaXxAd0PAA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@smithy/core": { + "version": "3.26.0", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.26.0.tgz", + "integrity": "sha512-mLUktFAn+Pa2agl1J7VgtYNFWCX8/b4GMJSK1hCu4YCvtBfM6F8Os3EP4ry+DFFlXOf3wyvlgXhuUdFoy52D3g==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.2.tgz", + "integrity": "sha512-18UMDMyrAbDcpmL1gLUA7ww0fRTcdCrSjSJOi2Sbld+tVjwD/pW+OAwjlScFLR7vvBnhZrIPQ7kVuTf1mnJLug==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.5.2", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.5.2.tgz", + "integrity": "sha512-Ei/UK/QMhq0rKaMqGPlOAkE2yS9DZeYmZdk1RAKc3vp3zxgleZHZyBLlZv8yLsxljX4svCRuMTD6u3LLIcU4Bg==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "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", + "peer": true, + "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", + "peer": true, + "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.5.2", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.5.2.tgz", + "integrity": "sha512-7xHpmPY4rt0IOmeAA8EfjgEH8isT+587TCdy9H6a7d4OMi5CQ0oEHhWllunvPu4j4Cq0vTFwdxXN/kABWPjdyA==", + "license": "Apache-2.0", + "peer": true, + "dependencies": { + "@smithy/core": "^3.26.0", + "@smithy/types": "^4.15.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.15.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.15.0.tgz", + "integrity": "sha512-Z5TAOxygoFvybJV3igo5SloFflSokHx2hu1eFA+DxDTcn+FtKxUSui+rbTRG1pAafMA888Z3MVvCWUuvCrTXjg==", + "license": "Apache-2.0", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "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": { + "@types/deep-eql": "*", + "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", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "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/node": { + "version": "22.20.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.0.tgz", + "integrity": "sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==", + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "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", + "peer": true + }, + "node_modules/@vitest/expect": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-3.2.6.tgz", + "integrity": "sha512-1+7q9BtaKzEmO+fmNT3kYvoNn5Y71XWAx2Q5HRim4tTVRQVRv4uJFAQ5FbK0OPUeNP/WmVCpxYxoJdvuHVjzBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-3.2.6.tgz", + "integrity": "sha512-EZOrpDbkKotFAP7wPAQV1UIyoGOk4oX7ynWhBhLB7v+meMHbQhU16oPpIYGTTe4oFlhpryGpgpcZP/sin3hYuw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "3.2.6", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.17" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-3.2.6.tgz", + "integrity": "sha512-lb7XXXzmm2h2ASzFnRvQpDo6onT1NmMJA3tkGTWiBFtRJ9lxGY3d3mm/Apt36gej2bkkOVLL/yTOtufDaFa/jA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-3.2.6.tgz", + "integrity": "sha512-HYcoSj1w5tcgUnzoF0HcyaAQjpA1gj9ftUJ7iSJSuipc02jW9gKkigwZbjFldAfYHA1fa8UZVRftdMY5msWM9Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "3.2.6", + "pathe": "^2.0.3", + "strip-literal": "^3.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-3.2.6.tgz", + "integrity": "sha512-H+ZjNTWGpObenh0YnlBctAPnJSI20P81PL8BPzWpx54YXLLTm8hEsWawtcYLMrwvpK48hGxLLbCS+1KRXhsKhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "magic-string": "^0.30.17", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-3.2.6.tgz", + "integrity": "sha512-oq6BbH68WzcWmwtBrU9nqLeaXTR4XwJF7FSLkKEZo4i6eoXcrxjcwSuTvWBIRUTC6VC72nXYunzqgZA+IKdtxg==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyspy": "^4.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-3.2.6.tgz", + "integrity": "sha512-lI23nIs4bnT3T8NIoh+vFaz5s2/DdP0Jgt2jxwgWljvwn82cLJtyi/If+fjFyoLMGIOz0U/fKvWE0d4jsNQEfg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "3.2.6", + "loupe": "^3.1.4", + "tinyrainbow": "^2.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "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", + "peer": true, + "engines": { + "node": ">= 14" + } + }, + "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": { + "node": ">=12" + } + }, + "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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "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", + "peer": true, + "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", + "peer": true + }, + "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", + "peer": true + }, + "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": { + "node": ">=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": { + "assertion-error": "^2.0.1", + "check-error": "^2.1.1", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "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": { + "node": ">= 16" + } + }, + "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", + "peer": true, + "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" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "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": { + "node": ">=6" + } + }, + "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", + "peer": true, + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "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", + "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/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": { + "@types/estree": "^1.0.0" + } + }, + "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": { + "node": ">=12.0.0" + } + }, + "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", + "peer": true + }, + "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": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "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", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "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", + "peer": true, + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "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, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/gaxios": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.5.tgz", + "integrity": "sha512-5FZy72Rh8LhtjmvDrKkI+lVhrsQrVKVsItxMoDm5mNQE+xR0WVIIs+jzPSJgBvKVsLi24fZhXJIsNI0bihDzFg==", + "license": "Apache-2.0", + "peer": true, + "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", + "peer": true, + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-auth-library": { + "version": "10.7.0", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.7.0.tgz", + "integrity": "sha512-QpTAbNJ36TliZLx3TTtahR8HG0hN9RllL1e3FymOvQSIKK8JmgV58H924ub2wa2DsS3ANjjP1Aw1N+Ramc8hqQ==", + "license": "Apache-2.0", + "peer": true, + "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", + "peer": true, + "engines": { + "node": ">=14" + } + }, + "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", + "peer": true, + "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", + "peer": true, + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "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/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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "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", + "peer": true, + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "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", + "peer": true + }, + "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/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": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "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.15", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.15.tgz", + "integrity": "sha512-y7Wygv/7mEOvxTuEQDB8StXdMRBWf1kR/tlhAzBRUFkB2jfcLOAxO/SHmOO2zgz1pVgK29/kyupn059/bCHdjA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "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==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.5.0" + } + }, + "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", + "peer": true, + "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", + "peer": true, + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "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", + "peer": true, + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "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", + "peer": true + }, + "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/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": { + "node": ">= 14.16" + } + }, + "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": "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/postcss": { + "version": "8.5.15", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.15.tgz", + "integrity": "sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.12", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "hasInstallScript": true, + "license": "BSD-3-Clause", + "peer": true, + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">= 4" + } + }, + "node_modules/rollup": { + "version": "4.62.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.62.2.tgz", + "integrity": "sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.9" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.62.2", + "@rollup/rollup-android-arm64": "4.62.2", + "@rollup/rollup-darwin-arm64": "4.62.2", + "@rollup/rollup-darwin-x64": "4.62.2", + "@rollup/rollup-freebsd-arm64": "4.62.2", + "@rollup/rollup-freebsd-x64": "4.62.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.62.2", + "@rollup/rollup-linux-arm-musleabihf": "4.62.2", + "@rollup/rollup-linux-arm64-gnu": "4.62.2", + "@rollup/rollup-linux-arm64-musl": "4.62.2", + "@rollup/rollup-linux-loong64-gnu": "4.62.2", + "@rollup/rollup-linux-loong64-musl": "4.62.2", + "@rollup/rollup-linux-ppc64-gnu": "4.62.2", + "@rollup/rollup-linux-ppc64-musl": "4.62.2", + "@rollup/rollup-linux-riscv64-gnu": "4.62.2", + "@rollup/rollup-linux-riscv64-musl": "4.62.2", + "@rollup/rollup-linux-s390x-gnu": "4.62.2", + "@rollup/rollup-linux-x64-gnu": "4.62.2", + "@rollup/rollup-linux-x64-musl": "4.62.2", + "@rollup/rollup-openbsd-x64": "4.62.2", + "@rollup/rollup-openharmony-arm64": "4.62.2", + "@rollup/rollup-win32-arm64-msvc": "4.62.2", + "@rollup/rollup-win32-ia32-msvc": "4.62.2", + "@rollup/rollup-win32-x64-gnu": "4.62.2", + "@rollup/rollup-win32-x64-msvc": "4.62.2", + "fsevents": "~2.3.2" + } + }, + "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", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "peer": true + }, + "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/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/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": { + "node": ">=0.10.0" + } + }, + "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/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": { + "js-tokens": "^9.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/antfu" + } + }, + "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.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "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": { + "node": "^18.0.0 || >=20.0.0" + } + }, + "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": { + "node": ">=14.0.0" + } + }, + "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": { + "node": ">=14.0.0" + } + }, + "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", + "peer": true + }, + "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", + "peer": true + }, + "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", + "peer": true + }, + "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": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "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/vite": { + "version": "7.3.5", + "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.5.tgz", + "integrity": "sha512-KuOaNhcnGFN2zIPGA7wRmzF+lJA1sea7rHq17aiJ++9lzY1WWG6Jpwqwe1KNbRVPIqHmr8GLYx7jbrQcN/7/ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "^0.27.0", + "fdir": "^6.5.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.6", + "rollup": "^4.43.0", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "lightningcss": "^1.21.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "lightningcss": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "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": { + "cac": "^6.7.14", + "debug": "^4.4.1", + "es-module-lexer": "^1.7.0", + "pathe": "^2.0.3", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0" + }, + "bin": { + "vite-node": "vite-node.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/vitest": { + "version": "3.2.6", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-3.2.6.tgz", + "integrity": "sha512-xejya+bT/j/+R/AGa1XOfRxLmNUlLtlwjRsFUILF+xHfzElmGcmFydy2gqqIrd62ptIEfwVMofd19uNWD9L7Nw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/chai": "^5.2.2", + "@vitest/expect": "3.2.6", + "@vitest/mocker": "3.2.6", + "@vitest/pretty-format": "^3.2.6", + "@vitest/runner": "3.2.6", + "@vitest/snapshot": "3.2.6", + "@vitest/spy": "3.2.6", + "@vitest/utils": "3.2.6", + "chai": "^5.2.0", + "debug": "^4.4.1", + "expect-type": "^1.2.1", + "magic-string": "^0.30.17", + "pathe": "^2.0.3", + "picomatch": "^4.0.2", + "std-env": "^3.9.0", + "tinybench": "^2.9.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.14", + "tinypool": "^1.1.1", + "tinyrainbow": "^2.0.0", + "vite": "^5.0.0 || ^6.0.0 || ^7.0.0-0", + "vite-node": "3.2.4", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^18.0.0 || ^20.0.0 || >=22.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@types/debug": "^4.1.12", + "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", + "@vitest/browser": "3.2.6", + "@vitest/ui": "3.2.6", + "happy-dom": "*", + "jsdom": "*" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@types/debug": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + } + } + }, + "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", + "peer": true, + "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/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": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "license": "MIT", + "peer": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "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", + "peer": true, + "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", + "peer": true, + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/package.json b/package.json index 6e48d15..6173210 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "@linimin/pi-headroom", - "version": "0.1.0", - "description": "Pi package that routes selected providers through local Headroom proxies with auto-start and status UI.", + "version": "0.2.0", + "description": "Pi extension that manages one Headroom proxy per Pi process with auto-start, health polling, and Windows lifecycle.", "type": "module", "license": "MIT", "author": "linimin", @@ -10,11 +10,19 @@ }, "files": [ "index.ts", + "types.ts", + "runtime-state.ts", + "session-runtime.ts", + "headroom-process.ts", + "windows-process.ts", + "port-allocation.ts", + "request-identity.ts", + "headroom-retrieve.ts", + "headroom-commands.ts", + "proxy-options.ts", "provider-registry.ts", "README.md", "LICENSE", - "headroom-provider-supervisor.ts", - "headroom-provider-supervisor.README.md", "package.json" ], "keywords": [ @@ -24,6 +32,20 @@ "pi", "llm-proxy" ], + "scripts": { + "typecheck": "tsc --noEmit", + "test": "vitest run", + "test:watch": "vitest" + }, + "dependencies": { + "cross-spawn": "^7.0.6" + }, + "devDependencies": { + "@types/cross-spawn": "6.0.6", + "@types/node": "^22.15.3", + "typescript": "^5.8.3", + "vitest": "^3.1.1" + }, "peerDependencies": { "@earendil-works/pi-ai": "*", "@earendil-works/pi-coding-agent": "*" diff --git a/port-allocation.ts b/port-allocation.ts new file mode 100644 index 0000000..f0a6d5d --- /dev/null +++ b/port-allocation.ts @@ -0,0 +1,49 @@ +/** + * Dynamic loopback port allocation. + * + * Allocates a random free port on 127.0.0.1 by briefly opening and closing + * a TCP server. The port is released immediately so that Headroom can bind it. + * + * Race condition mitigation: spawnHeadroomProcess retries up to + * maximumPortAttempts times if the port is taken by the time Headroom starts. + */ + +import { createServer } from "node:net"; + +/** + * Allocate a random free loopback port. + * + * The returned port is guaranteed to be on 127.0.0.1 and available at the + * moment the server is closed. There is a small window between release and + * Headroom binding — this is handled by the retry logic in spawnHeadroomProcess. + */ +export async function allocateLoopbackPort(): Promise { + return new Promise((resolve, reject) => { + const server = createServer(); + let settled = false; + + server.once("error", (err) => { + if (settled) return; + settled = true; + reject(new Error(`Failed to allocate loopback port: ${err.message}`)); + }); + + server.listen(0, "127.0.0.1", () => { + if (settled) return; + const addr = server.address(); + if (!addr || typeof addr === "string") { + settled = true; + server.close(() => { + reject(new Error("Failed to get allocated port from server address")); + }); + return; + } + const port = addr.port; + server.close(() => { + if (settled) return; + settled = true; + resolve(port); + }); + }); + }); +} diff --git a/provider-registry.ts b/provider-registry.ts index cb274aa..b59e7ef 100644 --- a/provider-registry.ts +++ b/provider-registry.ts @@ -1,109 +1,31 @@ -import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; -import { githubCopilotOAuthProvider } from "@earendil-works/pi-ai/oauth"; -import { readFileSync } from "node:fs"; -import { homedir } from "node:os"; -import { join } from "node:path"; +/** + * Provider registry for pi-headroom. + * + * Manages provider-specific routing through the local Headroom proxy. + * In this per-Pi mode, only openai-codex and opencode-go are managed. + */ -const MANAGED_PROVIDERS = [ - "xai", - "github-copilot", - "openai", - "anthropic", - "openrouter", - "deepseek", - "groq", - "together", - "mistral", - "fireworks", - "google", - "google-vertex", - "cerebras", - "nvidia", - "huggingface", - "ant-ling", - "moonshotai", - "moonshotai-cn", - "minimax", - "minimax-cn", - "xiaomi", - "xiaomi-token-plan-cn", - "xiaomi-token-plan-ams", - "xiaomi-token-plan-sgp", - "kimi-coding", -] as const; +import type { ExtensionAPI, ProviderModel } from "./types.js"; -const MANAGED_PROVIDER_SET = new Set(MANAGED_PROVIDERS); - -type ProviderFamily = "openai" | "anthropic" | "gemini" | "custom"; -type RoutedBaseStyle = "openai" | "root" | "google-v1beta"; - -type CopilotOAuthProvider = { - login?: (...args: unknown[]) => unknown; - refreshToken?: (...args: unknown[]) => unknown; - getApiKey?: (...args: unknown[]) => unknown; - modifyModels?: ( - models: ProviderModel[], - credentials: unknown, - ) => ProviderModel[]; -}; +// ── Managed providers in this mode ─────────────────────────────── -export type ManagedProvider = (typeof MANAGED_PROVIDERS)[number]; +type ManagedProvider = "openai-codex" | "opencode-go"; -export type ProviderModel = { - provider: string; - id?: string; - baseUrl?: string; - [key: string]: unknown; -}; +const MANAGED_PROVIDERS: ManagedProvider[] = ["openai-codex", "opencode-go"]; +const MANAGED_PROVIDER_SET = new Set(MANAGED_PROVIDERS); -export interface ManagedProxyConfig { - provider: ManagedProvider; - preferredPort: number; - port: number; - rootUrl: string; - routedBaseUrl: string; - env: Record; -} +// ── Provider spec ──────────────────────────────────────────────── -export interface ManagedProviderSpec { +interface ManagedProviderSpec { provider: ManagedProvider; displayName: string; - family: ProviderFamily; preferredPortEnv: string; defaultPreferredPort: number; buildProxyEnv(): Record; - buildRoutedBaseUrl(rootUrl: string): string; - register(pi: ExtensionAPI, config: ManagedProxyConfig): void; + buildRoutedBaseUrl(rootUrl: string, model?: ProviderModel): string; } -interface SimpleProviderSpecOptions { - provider: ManagedProvider; - displayName: string; - family: ProviderFamily; - preferredPortEnv: string; - defaultPreferredPort: number; - upstreamEnvKey: string; - defaultUpstream: string; - routedBaseStyle: RoutedBaseStyle; - extraEnv?: Record; -} - -function readPiCopilotAccessToken(): string | undefined { - const configDir = - process.env.PI_CODING_AGENT_DIR?.trim() || join(homedir(), ".pi", "agent"); - - try { - const payload = JSON.parse( - readFileSync(join(configDir, "auth.json"), "utf8"), - ) as { - [provider: string]: { access?: unknown } | undefined; - }; - const token = payload["github-copilot"]?.access; - return typeof token === "string" && token.trim() ? token.trim() : undefined; - } catch { - return undefined; - } -} +// ── Helper functions ───────────────────────────────────────────── function parsePositiveInt(raw: string | undefined, fallback: number): number { if (!raw?.trim()) return fallback; @@ -111,451 +33,140 @@ function parsePositiveInt(raw: string | undefined, fallback: number): number { return Number.isFinite(value) && value > 0 ? value : fallback; } -function normalizeUrl(raw: string): string { - const url = new URL(raw); - if (url.protocol !== "http:" && url.protocol !== "https:") { - throw new Error( - `Unsupported URL protocol ${JSON.stringify(url.protocol)} for ${JSON.stringify(raw)}.`, - ); - } - return url.toString().replace(/\/$/, ""); -} - -function openaiStyleBaseUrl(rootUrl: string): string { - return `${rootUrl}/v1`; -} - -function rootBaseUrl(rootUrl: string): string { - return rootUrl; -} +// ── Provider specs ─────────────────────────────────────────────── -function googleV1betaBaseUrl(rootUrl: string): string { - return `${rootUrl}/v1beta`; -} - -function routedBaseUrlFor(style: RoutedBaseStyle, rootUrl: string): string { - switch (style) { - case "openai": - return openaiStyleBaseUrl(rootUrl); - case "google-v1beta": - return googleV1betaBaseUrl(rootUrl); - case "root": - default: - return rootBaseUrl(rootUrl); - } -} - -function registerBaseUrlOverride( - provider: ManagedProvider, - pi: ExtensionAPI, - config: ManagedProxyConfig, -): void { - pi.registerProvider(provider, { - baseUrl: config.routedBaseUrl, - }); -} - -function makeSimpleProviderSpec( - options: SimpleProviderSpecOptions, -): ManagedProviderSpec { - return { - provider: options.provider, - displayName: options.displayName, - family: options.family, - preferredPortEnv: options.preferredPortEnv, - defaultPreferredPort: options.defaultPreferredPort, +const PROVIDER_SPECS: Record = { + "openai-codex": { + provider: "openai-codex", + displayName: "OpenAI Codex", + preferredPortEnv: "PI_HEADROOM_CODEX_PORT", + defaultPreferredPort: 8820, buildProxyEnv() { return { - [options.upstreamEnvKey]: - process.env[ - options.preferredPortEnv.replace(/_PORT$/, "_UPSTREAM") - ]?.trim() || options.defaultUpstream, - ...(options.extraEnv ?? {}), + HEADROOM_BACKEND: "openai", + LITELLM_SUPPRESS_DEBUG_INFO: "True", }; }, buildRoutedBaseUrl(rootUrl: string) { - return routedBaseUrlFor(options.routedBaseStyle, rootUrl); - }, - register(pi, config) { - registerBaseUrlOverride(options.provider, pi, config); + return `${rootUrl}/backend-api`; }, - }; -} - -const OPENAI_FAMILY_ENV = { - HEADROOM_BACKEND: "openai", - LITELLM_SUPPRESS_DEBUG_INFO: "True", -}; + }, -const PROVIDER_SPECS: Record = { - xai: makeSimpleProviderSpec({ - provider: "xai", - displayName: "xAI", - family: "openai", - preferredPortEnv: "PI_HEADROOM_XAI_PORT", - defaultPreferredPort: 8787, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.x.ai", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - "github-copilot": { - provider: "github-copilot", - displayName: "GitHub Copilot", - family: "openai", - preferredPortEnv: "PI_HEADROOM_COPILOT_PORT", - defaultPreferredPort: 8788, + "opencode-go": { + provider: "opencode-go", + displayName: "OpenCode Go", + preferredPortEnv: "PI_HEADROOM_OPENCODE_PORT", + defaultPreferredPort: 8821, buildProxyEnv() { - const accessToken = - process.env.GITHUB_COPILOT_API_TOKEN?.trim() || readPiCopilotAccessToken(); - + const upstream = + process.env.PI_HEADROOM_OPENCODE_GO_UPSTREAM?.trim() ?? + "https://opencode.ai/zen/go"; return { - HEADROOM_BACKEND: "openai", - OPENAI_TARGET_API_URL: - process.env.PI_HEADROOM_COPILOT_UPSTREAM?.trim() || - "https://api.githubcopilot.com", - GITHUB_COPILOT_USE_TOKEN_EXCHANGE: - process.env.PI_HEADROOM_COPILOT_USE_TOKEN_EXCHANGE?.trim() || "0", - ...(accessToken ? { GITHUB_COPILOT_API_TOKEN: accessToken } : {}), + OPENAI_TARGET_API_URL: upstream, + ANTHROPIC_TARGET_API_URL: upstream, + HEADROOM_BACKEND: "anthropic", LITELLM_SUPPRESS_DEBUG_INFO: "True", }; }, - buildRoutedBaseUrl(rootUrl: string) { - return openaiStyleBaseUrl(rootUrl); - }, - register(pi, config) { - const oauthProvider = githubCopilotOAuthProvider as CopilotOAuthProvider; - pi.registerProvider("github-copilot", { - baseUrl: config.routedBaseUrl, - oauth: { - name: "GitHub Copilot via Headroom", - login: oauthProvider.login, - refreshToken: oauthProvider.refreshToken, - getApiKey: oauthProvider.getApiKey, - modifyModels(models: ProviderModel[], credentials: unknown) { - const oauthAdjusted = oauthProvider.modifyModels - ? oauthProvider.modifyModels(models, credentials) - : models; - return oauthAdjusted.map((model) => - model.provider === "github-copilot" - ? { ...model, baseUrl: config.routedBaseUrl } - : model, - ); - }, - }, - }); + buildRoutedBaseUrl(rootUrl: string, model?: ProviderModel) { + if (model?.api === "anthropic-messages") { + return rootUrl; + } + if (model?.api === "openai-completions") { + return `${rootUrl}/v1`; + } + throw new Error( + `Unsupported opencode-go model API: ${String(model?.api ?? "(undefined)")}. ` + + `Expected "anthropic-messages" or "openai-completions".`, + ); }, }, - openai: makeSimpleProviderSpec({ - provider: "openai", - displayName: "OpenAI", - family: "openai", - preferredPortEnv: "PI_HEADROOM_OPENAI_PORT", - defaultPreferredPort: 8789, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.openai.com", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - anthropic: makeSimpleProviderSpec({ - provider: "anthropic", - displayName: "Anthropic", - family: "anthropic", - preferredPortEnv: "PI_HEADROOM_ANTHROPIC_PORT", - defaultPreferredPort: 8790, - upstreamEnvKey: "ANTHROPIC_TARGET_API_URL", - defaultUpstream: "https://api.anthropic.com", - routedBaseStyle: "root", - }), - openrouter: makeSimpleProviderSpec({ - provider: "openrouter", - displayName: "OpenRouter", - family: "openai", - preferredPortEnv: "PI_HEADROOM_OPENROUTER_PORT", - defaultPreferredPort: 8791, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://openrouter.ai/api", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - deepseek: makeSimpleProviderSpec({ - provider: "deepseek", - displayName: "DeepSeek", - family: "openai", - preferredPortEnv: "PI_HEADROOM_DEEPSEEK_PORT", - defaultPreferredPort: 8792, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.deepseek.com", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - groq: makeSimpleProviderSpec({ - provider: "groq", - displayName: "Groq", - family: "openai", - preferredPortEnv: "PI_HEADROOM_GROQ_PORT", - defaultPreferredPort: 8793, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.groq.com/openai", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - together: makeSimpleProviderSpec({ - provider: "together", - displayName: "Together", - family: "openai", - preferredPortEnv: "PI_HEADROOM_TOGETHER_PORT", - defaultPreferredPort: 8794, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.together.ai", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - mistral: makeSimpleProviderSpec({ - provider: "mistral", - displayName: "Mistral", - family: "openai", - preferredPortEnv: "PI_HEADROOM_MISTRAL_PORT", - defaultPreferredPort: 8795, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.mistral.ai", - routedBaseStyle: "root", - extraEnv: OPENAI_FAMILY_ENV, - }), - fireworks: makeSimpleProviderSpec({ - provider: "fireworks", - displayName: "Fireworks", - family: "anthropic", - preferredPortEnv: "PI_HEADROOM_FIREWORKS_PORT", - defaultPreferredPort: 8796, - upstreamEnvKey: "ANTHROPIC_TARGET_API_URL", - defaultUpstream: "https://api.fireworks.ai/inference", - routedBaseStyle: "root", - }), - google: makeSimpleProviderSpec({ - provider: "google", - displayName: "Google Gemini", - family: "gemini", - preferredPortEnv: "PI_HEADROOM_GEMINI_PORT", - defaultPreferredPort: 8797, - upstreamEnvKey: "GEMINI_TARGET_API_URL", - defaultUpstream: "https://generativelanguage.googleapis.com", - routedBaseStyle: "google-v1beta", - }), - "google-vertex": makeSimpleProviderSpec({ - provider: "google-vertex", - displayName: "Google Vertex", - family: "gemini", - preferredPortEnv: "PI_HEADROOM_VERTEX_PORT", - defaultPreferredPort: 8798, - upstreamEnvKey: "VERTEX_TARGET_API_URL", - defaultUpstream: "https://us-central1-aiplatform.googleapis.com", - routedBaseStyle: "root", - }), - cerebras: makeSimpleProviderSpec({ - provider: "cerebras", - displayName: "Cerebras", - family: "openai", - preferredPortEnv: "PI_HEADROOM_CEREBRAS_PORT", - defaultPreferredPort: 8799, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.cerebras.ai", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - nvidia: makeSimpleProviderSpec({ - provider: "nvidia", - displayName: "NVIDIA NIM", - family: "openai", - preferredPortEnv: "PI_HEADROOM_NVIDIA_PORT", - defaultPreferredPort: 8800, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://integrate.api.nvidia.com", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - huggingface: makeSimpleProviderSpec({ - provider: "huggingface", - displayName: "Hugging Face", - family: "openai", - preferredPortEnv: "PI_HEADROOM_HUGGINGFACE_PORT", - defaultPreferredPort: 8801, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://router.huggingface.co", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - "ant-ling": makeSimpleProviderSpec({ - provider: "ant-ling", - displayName: "Ant Ling", - family: "openai", - preferredPortEnv: "PI_HEADROOM_ANT_LING_PORT", - defaultPreferredPort: 8802, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.ant-ling.com", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - moonshotai: makeSimpleProviderSpec({ - provider: "moonshotai", - displayName: "Moonshot AI", - family: "openai", - preferredPortEnv: "PI_HEADROOM_MOONSHOTAI_PORT", - defaultPreferredPort: 8803, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.moonshot.ai", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - "moonshotai-cn": makeSimpleProviderSpec({ - provider: "moonshotai-cn", - displayName: "Moonshot AI CN", - family: "openai", - preferredPortEnv: "PI_HEADROOM_MOONSHOTAI_CN_PORT", - defaultPreferredPort: 8804, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.moonshot.cn", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - minimax: makeSimpleProviderSpec({ - provider: "minimax", - displayName: "MiniMax", - family: "anthropic", - preferredPortEnv: "PI_HEADROOM_MINIMAX_PORT", - defaultPreferredPort: 8805, - upstreamEnvKey: "ANTHROPIC_TARGET_API_URL", - defaultUpstream: "https://api.minimax.io/anthropic", - routedBaseStyle: "root", - }), - "minimax-cn": makeSimpleProviderSpec({ - provider: "minimax-cn", - displayName: "MiniMax CN", - family: "anthropic", - preferredPortEnv: "PI_HEADROOM_MINIMAX_CN_PORT", - defaultPreferredPort: 8806, - upstreamEnvKey: "ANTHROPIC_TARGET_API_URL", - defaultUpstream: "https://api.minimaxi.com/anthropic", - routedBaseStyle: "root", - }), - xiaomi: makeSimpleProviderSpec({ - provider: "xiaomi", - displayName: "Xiaomi MiMo", - family: "openai", - preferredPortEnv: "PI_HEADROOM_XIAOMI_PORT", - defaultPreferredPort: 8807, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://api.xiaomimimo.com", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - "xiaomi-token-plan-cn": makeSimpleProviderSpec({ - provider: "xiaomi-token-plan-cn", - displayName: "Xiaomi Token Plan CN", - family: "openai", - preferredPortEnv: "PI_HEADROOM_XIAOMI_TOKEN_CN_PORT", - defaultPreferredPort: 8808, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://token-plan-cn.xiaomimimo.com", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - "xiaomi-token-plan-ams": makeSimpleProviderSpec({ - provider: "xiaomi-token-plan-ams", - displayName: "Xiaomi Token Plan AMS", - family: "openai", - preferredPortEnv: "PI_HEADROOM_XIAOMI_TOKEN_AMS_PORT", - defaultPreferredPort: 8809, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://token-plan-ams.xiaomimimo.com", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - "xiaomi-token-plan-sgp": makeSimpleProviderSpec({ - provider: "xiaomi-token-plan-sgp", - displayName: "Xiaomi Token Plan SGP", - family: "openai", - preferredPortEnv: "PI_HEADROOM_XIAOMI_TOKEN_SGP_PORT", - defaultPreferredPort: 8810, - upstreamEnvKey: "OPENAI_TARGET_API_URL", - defaultUpstream: "https://token-plan-sgp.xiaomimimo.com", - routedBaseStyle: "openai", - extraEnv: OPENAI_FAMILY_ENV, - }), - "kimi-coding": makeSimpleProviderSpec({ - provider: "kimi-coding", - displayName: "Kimi Coding", - family: "anthropic", - preferredPortEnv: "PI_HEADROOM_KIMI_CODING_PORT", - defaultPreferredPort: 8811, - upstreamEnvKey: "ANTHROPIC_TARGET_API_URL", - defaultUpstream: "https://api.kimi.com/coding", - routedBaseStyle: "root", - }), }; +// ── Public API ─────────────────────────────────────────────────── + +/** + * All managed provider IDs in this per-Pi mode. + */ export function providerIds(): ManagedProvider[] { return [...MANAGED_PROVIDERS]; } -export function isManagedProvider( - provider: string, -): provider is ManagedProvider { +/** + * Check if a provider string is a managed provider. + */ +export function isManagedProvider(provider: string): provider is ManagedProvider { return MANAGED_PROVIDER_SET.has(provider); } +/** + * Get the managed provider for a model, if any. + */ export function managedProviderFor( model: { provider: string } | undefined, ): ManagedProvider | undefined { if (!model) return undefined; - return isManagedProvider(model.provider) ? model.provider : undefined; -} - -export function getManagedProviderSpec( - provider: ManagedProvider, -): ManagedProviderSpec { - return PROVIDER_SPECS[provider]; + return isManagedProvider(model.provider) ? (model.provider as ManagedProvider) : undefined; } +/** + * Get the preferred port for a provider. + */ export function preferredPortFor(provider: ManagedProvider): number { - const spec = getManagedProviderSpec(provider); + const spec = PROVIDER_SPECS[provider]; return parsePositiveInt( process.env[spec.preferredPortEnv], spec.defaultPreferredPort, ); } -export function buildManagedProxyConfig( +/** + * Build the provider-specific base URL for routing. + * This returns the correct URL including /backend-api or /v1 suffixes. + */ +export function routedBaseUrlForModel( provider: ManagedProvider, - port: number, - host: string, -): ManagedProxyConfig { - const spec = getManagedProviderSpec(provider); - const preferredPort = preferredPortFor(provider); - const rootUrl = normalizeUrl(`http://${host}:${port}`); - return { - provider, - preferredPort, - port, - rootUrl, - routedBaseUrl: spec.buildRoutedBaseUrl(rootUrl), - env: spec.buildProxyEnv(), - }; + proxyRootUrl: string, + model?: ProviderModel, +): string { + const spec = PROVIDER_SPECS[provider]; + if (!spec) { + throw new Error(`Unknown provider: ${provider}`); + } + return spec.buildRoutedBaseUrl(proxyRootUrl, model); } -export function registerManagedProvider( +/** + * Build the provider environment for the Headroom process. + */ +export function buildProviderEnvironment(provider: ManagedProvider): Record { + return PROVIDER_SPECS[provider].buildProxyEnv(); +} + +/** + * Register a provider with Pi using the already-computed baseUrl. + * + * Important: The caller must compute the correct baseUrl via + * routedBaseUrlForModel() and pass it in. This function does NOT + * recompute it, which fixes the routing bug. + */ +export function registerProviderWithBaseUrl( pi: ExtensionAPI, provider: ManagedProvider, - config: ManagedProxyConfig, + baseUrl: string, + headers: Record, ): void { - getManagedProviderSpec(provider).register(pi, config); + pi.registerProvider(provider, { + baseUrl, + headers, + }); } -export function registerManagedProviders( - pi: ExtensionAPI, - getConfig: (provider: ManagedProvider) => ManagedProxyConfig, -): void { - for (const provider of providerIds()) { - registerManagedProvider(pi, provider, getConfig(provider)); - } +/** + * Check if the model's provider is managed by this extension. + */ +export function isManagedModel( + model: { provider: string } | undefined, +): boolean { + return managedProviderFor(model) !== undefined; } diff --git a/proxy-options.ts b/proxy-options.ts new file mode 100644 index 0000000..8802e13 --- /dev/null +++ b/proxy-options.ts @@ -0,0 +1,81 @@ +/** + * Headroom CLI options and environment configuration. + * + * Builds the command-line arguments and environment variables + * for spawning a Headroom process. + * + * The environment supports both openai-codex and opencode-go + * simultaneously, since Headroom can handle both backends. + */ + +import { createHash } from "node:crypto"; + +/** + * Build the Headroom CLI arguments. + * + * Args: + * proxy --host 127.0.0.1 --port + * --mode token + * --code-aware + * --code-graph + * --memory --memory-storage project + * --no-ccr-inject-tool (if supported) + */ +export function buildHeadroomArgs(port: number): string[] { + return [ + "proxy", + "--host", + "127.0.0.1", + "--port", + String(port), + "--mode", + "token", + "--code-aware", + "--code-graph", + "--memory", + "--memory-storage", + "project", + ]; +} + +/** + * Build the Headroom environment. + * + * Configures environment for both openai-codex and opencode-go: + * - OPENAI_TARGET_API_URL: OpenCode Go upstream + * - ANTHROPIC_TARGET_API_URL: OpenCode Go upstream + * - HEADROOM_BACKEND: "anthropic" (for opencode-go) + * + * The actual API keys are inherited from the parent Pi process environment. + * + * Note: openai-codex uses its own OAuth flow through Pi, so we don't + * need to set specific env vars for it. The HEADROOM_BACKEND="openai" + * for codex is set by Headroom itself when it detects codex traffic. + */ +export function buildHeadroomEnvironment( + baseEnv: NodeJS.ProcessEnv, +): NodeJS.ProcessEnv { + const env = { ...baseEnv }; + + // OpenCode Go upstream (used for both OPENAI and ANTHROPIC targets) + const opencodeUpstream = + process.env.PI_HEADROOM_OPENCODE_GO_UPSTREAM?.trim() ?? + "https://opencode.ai/zen/go"; + + env.OPENAI_TARGET_API_URL = opencodeUpstream; + env.ANTHROPIC_TARGET_API_URL = opencodeUpstream; + env.HEADROOM_BACKEND = "anthropic"; + + return env; +} + +/** + * Generate a runtime ID from process PID, timestamp, and random UUID. + * + * Uses SHA-256 truncated to 24 characters. + */ +export function createRuntimeId(): string { + const hash = createHash("sha256"); + hash.update(`${process.pid}:${Date.now()}:${crypto.randomUUID()}`); + return hash.digest("hex").slice(0, 24); +} diff --git a/request-identity.ts b/request-identity.ts new file mode 100644 index 0000000..2c3e21c --- /dev/null +++ b/request-identity.ts @@ -0,0 +1,61 @@ +/** + * Request identity headers for Headroom. + * + * Each request to Headroom carries headers that identify the current + * Pi session, user, and working directory. + */ + +import { userInfo } from "node:os"; +import type { PiRequestIdentity } from "./types.js"; + +/** + * Build Headroom request identity headers. + * + * Headers: + * - x-headroom-cwd: canonical Windows path of the current working directory + * - x-headroom-user-id: username + * - x-headroom-session-id: current Pi session identifier + * - x-headroom-project-id: optional, only if PI_HEADROOM_PROJECT_ID is set + */ +export function buildHeadroomHeaders( + identity: PiRequestIdentity, +): Record { + const headers: Record = { + "x-headroom-cwd": identity.cwd, + "x-headroom-user-id": identity.userId, + "x-headroom-session-id": identity.sessionId, + }; + + const projectId = process.env.PI_HEADROOM_PROJECT_ID?.trim(); + if (projectId) { + headers["x-headroom-project-id"] = projectId; + } + + return headers; +} + +/** + * Resolve the current user ID. + * + * Priority: + * 1. PI_HEADROOM_USER_ID env + * 2. USERNAME env + * 3. os.userInfo().username + * 4. "unknown-user" + */ +export function resolveUserId(): string { + const envUserId = process.env.PI_HEADROOM_USER_ID?.trim(); + if (envUserId) return envUserId; + + const envUsername = process.env.USERNAME?.trim(); + if (envUsername) return envUsername; + + try { + const info = userInfo(); + if (info.username) return info.username; + } catch { + // userInfo can fail on some systems + } + + return "unknown-user"; +} diff --git a/runtime-state.ts b/runtime-state.ts new file mode 100644 index 0000000..abb6ab6 --- /dev/null +++ b/runtime-state.ts @@ -0,0 +1,50 @@ +/** + * Runtime singleton management via globalThis. + * + * One runtime corresponds to one Pi process. The singleton survives + * /reload because it is stored in globalThis keyed by a well-known symbol. + */ + +import type { HeadroomSessionRuntime } from "./session-runtime.js"; + +const RUNTIME_SYMBOL = Symbol.for("pi-headroom.session-runtime"); + +interface GlobalWithRuntime { + [RUNTIME_SYMBOL]?: HeadroomSessionRuntime; +} + +/** + * Get the existing runtime or create a new one using the provided factory. + * + * Rules: + * - If a runtime exists and its ownerPid matches process.pid, return it. + * - If no runtime exists, call the factory and store the result. + * - Never create a second runtime during /reload. + */ +export function getOrCreateSessionRuntime( + factory: () => HeadroomSessionRuntime, +): HeadroomSessionRuntime { + const g = globalThis as GlobalWithRuntime; + const existing = g[RUNTIME_SYMBOL]; + + if (existing && existing.ownerPid === process.pid) { + return existing; + } + + const runtime = factory(); + g[RUNTIME_SYMBOL] = runtime; + return runtime; +} + +/** + * Remove the runtime from globalThis after it has been fully stopped. + * Only removes if the stored runtime matches the one passed in. + */ +export function removeSessionRuntime( + runtime: HeadroomSessionRuntime, +): void { + const g = globalThis as GlobalWithRuntime; + if (g[RUNTIME_SYMBOL] === runtime) { + g[RUNTIME_SYMBOL] = undefined; + } +} diff --git a/session-runtime.ts b/session-runtime.ts new file mode 100644 index 0000000..00d015e --- /dev/null +++ b/session-runtime.ts @@ -0,0 +1,527 @@ +/** + * HeadroomSessionRuntime — one Headroom process per Pi process. + * + * State machine: + * idle → starting → ready + * ready → stopping → stopped + * starting → failed + * ready → failed + * failed → starting (restart) + * + * Invariants: + * - At most one child process at any time. + * - Single-flight: concurrent ensureReady() calls do not spawn multiple processes. + * - Same cwd → no restart. Different cwd → stop + restart. + */ + +import type { ChildProcess } from "node:child_process"; +import { mkdirSync, writeFileSync, rmSync, renameSync, realpathSync } from "node:fs"; +import { join, resolve, normalize } from "node:path"; +import { homedir } from "node:os"; +import type { + HeadroomProcessInfo, + HeadroomRuntimeOptions, + HeadroomRuntimeState, + SpawnedHeadroomProcess, +} from "./types.js"; +import { createRuntimeId } from "./proxy-options.js"; +import { allocateLoopbackPort } from "./port-allocation.js"; +import { buildHeadroomArgs, buildHeadroomEnvironment } from "./proxy-options.js"; +import { spawnHeadroomProcess, waitForHeadroomReady } from "./headroom-process.js"; +import { terminateProcessTreeWindows } from "./windows-process.js"; +import { DEFAULT_RUNTIME_OPTIONS } from "./types.js"; + +// ── State directory ────────────────────────────────────────────── + +function getStateRoot(): string { + const localAppData = + process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local"); + return join(localAppData, "pi-headroom", "sessions"); +} + +function getSessionDir(runtimeId: string): string { + return join(getStateRoot(), runtimeId); +} + +function getSessionFile(runtimeId: string): string { + return join(getSessionDir(runtimeId), "session.json"); +} + +function getLogFile(runtimeId: string): string { + return join(getSessionDir(runtimeId), "headroom.log"); +} + +function ensureSessionDir(runtimeId: string): void { + const dir = getSessionDir(runtimeId); + mkdirSync(dir, { recursive: true }); +} + +function writeSessionMetadata( + runtimeId: string, + data: Record, +): void { + ensureSessionDir(runtimeId); + const filePath = getSessionFile(runtimeId); + const tmpPath = `${filePath}.tmp`; + writeFileSync(tmpPath, JSON.stringify(data, null, 2), "utf8"); + // Atomic rename + renameSync(tmpPath, filePath); +} + +// ── Headroom command resolution ────────────────────────────────── + +async function resolveHeadroomCommand(): Promise { + const envCommand = process.env.PI_HEADROOM_COMMAND?.trim(); + if (envCommand) return envCommand; + + // Try where.exe headroom + const { execFile } = await import("node:child_process"); + const { promisify } = await import("node:util"); + const execFileAsync = promisify(execFile); + + try { + const { stdout } = await execFileAsync("where.exe", ["headroom"]); + const firstLine = stdout.trim().split("\n")[0]?.trim(); + if (firstLine) return firstLine; + } catch { + // headroom not on PATH + } + + throw new Error( + "Headroom command not found. Set PI_HEADROOM_COMMAND or install Headroom.", + ); +} + +// ── Canonical path ─────────────────────────────────────────────── + +function canonicalizeWorkspacePath(inputPath: string): string { + if (!inputPath) throw new Error("Workspace path must not be empty"); + + let resolved = resolve(inputPath); + + try { + resolved = realpathSync(resolved); + } catch { + // If realpath fails (e.g. path doesn't exist yet), use normalized path + } + + return normalize(resolved); +} + +function workspaceComparisonKey(canonicalPath: string): string { + if (process.platform === "win32") { + return canonicalPath.toLowerCase(); + } + return canonicalPath; +} + +// ── SessionRuntime class ───────────────────────────────────────── + +export class HeadroomSessionRuntime { + readonly ownerPid = process.pid; + readonly runtimeId: string; + + private state: HeadroomRuntimeState = "idle"; + private child: ChildProcess | null = null; + private processInfo: HeadroomProcessInfo | null = null; + + private readyPromise: Promise | null = null; + private stopPromise: Promise | null = null; + + private currentCwd: string | null = null; + private currentCwdKey: string | null = null; + private automaticRestartTimes: number[] = []; + private expectedExit = false; + + private options: HeadroomRuntimeOptions; + private command: string | null = null; + + constructor(options?: Partial) { + this.runtimeId = createRuntimeId(); + this.options = { ...DEFAULT_RUNTIME_OPTIONS, ...options }; + } + + // ── Public API ───────────────────────────────────────────── + + getState(): HeadroomRuntimeState { + return this.state; + } + + getProcessInfo(): HeadroomProcessInfo | null { + return this.processInfo; + } + + getPort(): number | null { + return this.processInfo?.port ?? null; + } + + getCwd(): string | null { + return this.currentCwd; + } + + /** + * Start Headroom without blocking. + * Used on session_start for eager warmup. + */ + async warmup(cwd: string): Promise { + const canonical = canonicalizeWorkspacePath(cwd); + const canonicalKey = workspaceComparisonKey(canonical); + + // Already ready with same cwd + if (this.state === "ready" && this.currentCwdKey === canonicalKey) { + return; + } + + // Already starting — return existing promise (single-flight) + if (this.state === "starting" && this.readyPromise) { + return this.readyPromise; + } + + // If stopping, wait for stop to complete + if (this.state === "stopping" && this.stopPromise) { + await this.stopPromise; + } + + this.readyPromise = this.startInternal(canonical, canonicalKey); + + try { + await this.readyPromise; + } finally { + if (this.state !== "starting") { + this.readyPromise = null; + } + } + } + + /** + * Ensure Headroom is ready. Blocks until ready. + * Main entry point called by before_agent_start. + */ + async ensureReady(cwd: string): Promise { + return this.warmup(cwd); + } + + /** + * Restart Headroom with a new cwd. + */ + async restart(cwd: string, reason: string): Promise { + await this.stop(reason); + this.automaticRestartTimes = []; // Clear restart limiter + await this.warmup(cwd); + } + + /** + * Stop the current Headroom process. + */ + async stop(reason: string): Promise { + if (this.state === "idle" || this.state === "stopped") { + return; + } + + // Single-flight stop + if (this.stopPromise) { + return this.stopPromise; + } + + this.stopPromise = this.stopInternal(reason); + + try { + await this.stopPromise; + } finally { + this.stopPromise = null; + } + } + + /** + * Record a warmup error for logging. + */ + recordWarmupError(error: unknown): void { + const message = error instanceof Error ? error.message : String(error); + console.error( + `[pi-headroom] warmup failed: ${message} (runtime=${this.runtimeId})`, + ); + } + + // ── Internal ─────────────────────────────────────────────── + + private async startInternal( + canonical: string, + canonicalKey: string, + ): Promise { + this.state = "starting"; + + // Check restart limiter + if (!this.canAutomaticallyRestart()) { + this.state = "failed"; + throw new Error( + `Headroom restart limit reached (${this.options.maximumAutomaticRestarts} in ` + + `${this.options.restartWindowMs / 60_000} minutes). Use /headroom-restart manually.`, + ); + } + + // Resolve command if not cached + if (!this.command) { + this.command = await resolveHeadroomCommand(); + } + + // Stop existing process if cwd changed + if (this.child && this.currentCwdKey !== canonicalKey) { + await this.stopInternal("cwd-changed"); + } + + // Allocate port with retry + let lastError: Error | null = null; + for (let attempt = 0; attempt < this.options.maximumPortAttempts; attempt++) { + try { + const port = await allocateLoopbackPort(); + await this.spawnAndWait(port, canonical, canonicalKey); + this.recordAutomaticRestart(); + return; // Success + } catch (error) { + lastError = error instanceof Error ? error : new Error(String(error)); + // If port conflict, try again + if (lastError.message.includes("EADDRINUSE")) { + continue; + } + break; + } + } + + this.state = "failed"; + throw lastError ?? new Error("Failed to start Headroom after maximum attempts"); + } + + private async spawnAndWait( + port: number, + canonical: string, + canonicalKey: string, + ): Promise { + ensureSessionDir(this.runtimeId); + const logFile = getLogFile(this.runtimeId); + + const env = buildHeadroomEnvironment({ ...process.env }); + const args = buildHeadroomArgs(port); + + const spawned = spawnHeadroomProcess({ + command: this.command!, + cwd: canonical, + port, + logFile, + env, + args, + }); + + this.child = spawned.child; + this.currentCwd = canonical; + this.currentCwdKey = canonicalKey; + + // Write metadata immediately after spawn (before health check) + // so orphan scanner can find and kill this process if startup fails + const startedAt = new Date().toISOString(); + writeSessionMetadata(this.runtimeId, { + runtimeId: this.runtimeId, + piPid: this.ownerPid, + headroomPid: spawned.pid, + cwd: canonical, + port, + startedAt, + state: "starting", + }); + + // Set up exit handler + this.child.on("exit", (code, signal) => { + if (this.expectedExit) return; + console.error( + `[pi-headroom] Headroom exited unexpectedly (code=${code}, signal=${signal}) runtime=${this.runtimeId}`, + ); + this.state = "failed"; + this.processInfo = null; + this.child = null; + }); + + this.child.on("error", (error) => { + if (this.expectedExit) return; + console.error( + `[pi-headroom] Headroom error: ${error.message} runtime=${this.runtimeId}`, + ); + this.state = "failed"; + this.processInfo = null; + this.child = null; + }); + + // Wait for health — if this fails, kill the child process + try { + await waitForHeadroomReady( + port, + this.child, + this.options.startupTimeoutMs, + this.options.healthPollInitialDelayMs, + this.options.healthPollMaximumDelayMs, + logFile, + ); + } catch (healthError) { + // Kill the child process that failed to become healthy + const childPid = spawned.pid; + this.child = null; + this.processInfo = null; + this.currentCwd = null; + this.currentCwdKey = null; + this.state = "failed"; + + await terminateProcessTreeWindows( + childPid, + this.options.gracefulShutdownTimeoutMs, + ).catch(() => {}); + + // Update metadata to reflect failure + try { + writeSessionMetadata(this.runtimeId, { + runtimeId: this.runtimeId, + piPid: this.ownerPid, + headroomPid: childPid, + cwd: canonical, + port, + startedAt, + state: "failed", + error: healthError instanceof Error ? healthError.message : String(healthError), + }); + } catch { + // Best-effort + } + + throw healthError; + } + + this.processInfo = { + pid: spawned.pid, + port, + cwd: canonical, + startedAt, + logFile, + }; + + this.state = "ready"; + + // Update metadata to ready state + writeSessionMetadata(this.runtimeId, { + runtimeId: this.runtimeId, + piPid: this.ownerPid, + headroomPid: spawned.pid, + cwd: canonical, + port, + startedAt, + state: "ready", + }); + } + + private async stopInternal(reason: string): Promise { + if (this.state === "idle" || this.state === "stopped") { + return; + } + + this.state = "stopping"; + this.expectedExit = true; + + const pid = this.child?.pid; + + if (pid) { + await terminateProcessTreeWindows( + pid, + this.options.gracefulShutdownTimeoutMs, + ); + } + + this.child = null; + this.processInfo = null; + this.currentCwd = null; + this.currentCwdKey = null; + this.state = "stopped"; + this.expectedExit = false; + + // Write updated metadata + try { + writeSessionMetadata(this.runtimeId, { + runtimeId: this.runtimeId, + piPid: this.ownerPid, + state: "stopped", + stoppedAt: new Date().toISOString(), + reason, + }); + } catch { + // Best-effort metadata update + } + } + + /** + * Check if automatic restart is allowed within the restart window. + */ + canAutomaticallyRestart(): boolean { + const now = Date.now(); + this.automaticRestartTimes = this.automaticRestartTimes.filter( + (time) => now - time < this.options.restartWindowMs, + ); + return this.automaticRestartTimes.length < this.options.maximumAutomaticRestarts; + } + + recordAutomaticRestart(): void { + this.automaticRestartTimes.push(Date.now()); + } + + clearRestartLimiter(): void { + this.automaticRestartTimes = []; + } +} + +// ── Path utilities (exported for testing) ──────────────────────── + +export { canonicalizeWorkspacePath, workspaceComparisonKey }; + +// ── Orphan cleanup ─────────────────────────────────────────────── + +/** + * Scan the sessions directory and clean up orphaned Headroom processes. + * An orphan is a session whose owner Pi process no longer exists. + */ +import { readdirSync, readFileSync } from "node:fs"; +import { isProcessAlive } from "./windows-process.js"; + +export async function cleanupOrphanedSessions(): Promise { + const sessionsRoot = getStateRoot(); + let entries: string[]; + try { + entries = readdirSync(sessionsRoot); + } catch { + return; // Directory doesn't exist yet + } + + for (const entry of entries) { + const sessionFile = join(sessionsRoot, entry, "session.json"); + let metadata: Record; + try { + metadata = JSON.parse(readFileSync(sessionFile, "utf8")); + } catch { + continue; // Can't read metadata, skip + } + + const piPid = metadata.piPid as number | undefined; + const headroomPid = metadata.headroomPid as number | undefined; + + // If owner Pi process doesn't exist, clean up + if (piPid && !isProcessAlive(piPid)) { + console.log( + `[pi-headroom] cleaning up orphan session ${entry} (piPid=${piPid})`, + ); + + if (headroomPid) { + await terminateProcessTreeWindows(headroomPid, 2_000).catch(() => {}); + } + + // Remove session directory + try { + rmSync(join(sessionsRoot, entry), { recursive: true, force: true }); + } catch { + // Best-effort cleanup + } + } + } +} diff --git a/test/headroom-retrieve.test.ts b/test/headroom-retrieve.test.ts new file mode 100644 index 0000000..af11428 --- /dev/null +++ b/test/headroom-retrieve.test.ts @@ -0,0 +1,124 @@ +import { describe, it, expect, beforeAll, afterAll } from "vitest"; +import { createServer, type Server } from "node:http"; +import { buildHeadroomRetrieveTool } from "../headroom-retrieve.js"; +import { HeadroomSessionRuntime } from "../session-runtime.js"; +import type { ExtensionCtx } from "../types.js"; + +describe("headroom_retrieve", () => { + let server: Server; + let port: number; + + beforeAll(async () => { + // Create a fake Headroom server + server = createServer((req, res) => { + let body = ""; + req.on("data", (chunk) => (body += chunk)); + req.on("end", () => { + if (req.url === "/v1/retrieve") { + const data = JSON.parse(body); + if (data.hash === "not-found") { + res.writeHead(404); + res.end(JSON.stringify({ error: "Hash not found" })); + } else if (data.hash === "error") { + res.writeHead(500); + res.end("Internal server error"); + } else { + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + content: `Retrieved content for ${data.hash}` + (data.query ? ` with query: ${data.query}` : ""), + }), + ); + } + } else { + // Health check + res.writeHead(200); + res.end("ok"); + } + }); + }); + + await new Promise((resolve) => { + server.listen(0, "127.0.0.1", () => { + const addr = server.address(); + port = typeof addr === "object" && addr ? addr.port : 0; + resolve(); + }); + }); + }); + + afterAll(async () => { + await new Promise((resolve) => server.close(() => resolve())); + }); + + function makeMockCtx(cwd?: string): ExtensionCtx { + return { + cwd: cwd ?? "C:\\test", + model: undefined, + ui: { + notify: () => {}, + setStatus: () => {}, + }, + }; + } + + it("retrieves content successfully", async () => { + const runtime = { + ensureReady: async () => {}, + getPort: () => port, + getCwd: () => "C:\\test", + } as unknown as HeadroomSessionRuntime; + + const tool = buildHeadroomRetrieveTool(() => runtime); + const result = await tool.execute({ hash: "abc123" }, makeMockCtx()); + expect(result).toBe("Retrieved content for abc123"); + }); + + it("retrieves content with query", async () => { + const runtime = { + ensureReady: async () => {}, + getPort: () => port, + getCwd: () => "C:\\test", + } as unknown as HeadroomSessionRuntime; + + const tool = buildHeadroomRetrieveTool(() => runtime); + const result = await tool.execute({ hash: "abc123", query: "test query" }, makeMockCtx()); + expect(result).toBe("Retrieved content for abc123 with query: test query"); + }); + + it("handles 404 gracefully", async () => { + const runtime = { + ensureReady: async () => {}, + getPort: () => port, + getCwd: () => "C:\\test", + } as unknown as HeadroomSessionRuntime; + + const tool = buildHeadroomRetrieveTool(() => runtime); + const result = await tool.execute({ hash: "not-found" }, makeMockCtx()); + expect(result).toContain("not found"); + }); + + it("handles server errors", async () => { + const runtime = { + ensureReady: async () => {}, + getPort: () => port, + getCwd: () => "C:\\test", + } as unknown as HeadroomSessionRuntime; + + const tool = buildHeadroomRetrieveTool(() => runtime); + const result = await tool.execute({ hash: "error" }, makeMockCtx()); + expect(result).toContain("failed"); + }); + + it("handles missing hash", async () => { + const runtime = { + ensureReady: async () => {}, + getPort: () => port, + getCwd: () => "C:\\test", + } as unknown as HeadroomSessionRuntime; + + const tool = buildHeadroomRetrieveTool(() => runtime); + const result = await tool.execute({}, makeMockCtx()); + expect(result).toContain("Missing required parameter"); + }); +}); diff --git a/test/lifecycle.test.ts b/test/lifecycle.test.ts new file mode 100644 index 0000000..8dc01e0 --- /dev/null +++ b/test/lifecycle.test.ts @@ -0,0 +1,68 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { HeadroomSessionRuntime } from "../session-runtime.js"; +import { isProcessAlive } from "../windows-process.js"; + +describe("lifecycle", () => { + describe("state transitions", () => { + it("starts in idle state", () => { + const runtime = new HeadroomSessionRuntime(); + expect(runtime.getState()).toBe("idle"); + }); + + it("stop on idle does nothing", async () => { + const runtime = new HeadroomSessionRuntime(); + await runtime.stop("test"); + expect(runtime.getState()).toBe("idle"); + }); + }); + + describe("process utilities", () => { + it("isProcessAlive returns false for PID 0", () => { + expect(isProcessAlive(0)).toBe(false); + }); + + it("isProcessAlive returns false for negative PID", () => { + expect(isProcessAlive(-1)).toBe(false); + }); + + it("isProcessAlive returns true for current process", () => { + expect(isProcessAlive(process.pid)).toBe(true); + }); + + it("isProcessAlive returns false for non-existent PID", () => { + // Use a very high PID that's unlikely to exist + expect(isProcessAlive(999999999)).toBe(false); + }); + }); + + describe("restart limiter", () => { + it("allows restarts within window", () => { + const runtime = new HeadroomSessionRuntime({ + maximumAutomaticRestarts: 3, + restartWindowMs: 60_000, + }); + + expect(runtime.canAutomaticallyRestart()).toBe(true); + runtime.recordAutomaticRestart(); + expect(runtime.canAutomaticallyRestart()).toBe(true); + runtime.recordAutomaticRestart(); + expect(runtime.canAutomaticallyRestart()).toBe(true); + runtime.recordAutomaticRestart(); + expect(runtime.canAutomaticallyRestart()).toBe(false); + }); + + it("clears restart limiter", () => { + const runtime = new HeadroomSessionRuntime({ + maximumAutomaticRestarts: 2, + restartWindowMs: 60_000, + }); + + runtime.recordAutomaticRestart(); + runtime.recordAutomaticRestart(); + expect(runtime.canAutomaticallyRestart()).toBe(false); + + runtime.clearRestartLimiter(); + expect(runtime.canAutomaticallyRestart()).toBe(true); + }); + }); +}); diff --git a/test/port-allocation.test.ts b/test/port-allocation.test.ts new file mode 100644 index 0000000..6b1a523 --- /dev/null +++ b/test/port-allocation.test.ts @@ -0,0 +1,31 @@ +import { describe, it, expect } from "vitest"; +import { allocateLoopbackPort } from "../port-allocation.js"; + +describe("allocateLoopbackPort", () => { + it("allocates a valid port number", async () => { + const port = await allocateLoopbackPort(); + expect(port).toBeGreaterThan(0); + expect(port).toBeLessThan(65536); + }); + + it("allocates different ports on successive calls", async () => { + const port1 = await allocateLoopbackPort(); + const port2 = await allocateLoopbackPort(); + // Not guaranteed to be different, but very likely + expect(typeof port1).toBe("number"); + expect(typeof port2).toBe("number"); + }); + + it("allocated port is on 127.0.0.1", async () => { + const port = await allocateLoopbackPort(); + // Verify by trying to connect + const net = await import("node:net"); + const server = net.createServer(); + await new Promise((resolve, reject) => { + server.once("error", reject); + server.listen(port, "127.0.0.1", () => { + server.close(() => resolve()); + }); + }); + }); +}); diff --git a/test/provider-routing.test.ts b/test/provider-routing.test.ts new file mode 100644 index 0000000..fd34470 --- /dev/null +++ b/test/provider-routing.test.ts @@ -0,0 +1,80 @@ +import { describe, it, expect } from "vitest"; +import { routedBaseUrlForModel, providerIds, isManagedProvider } from "../provider-registry.js"; +import type { ProviderModel } from "../types.js"; + +describe("routedBaseUrlForModel", () => { + const rootUrl = "http://127.0.0.1:19000"; + + describe("openai-codex", () => { + it("routes to /backend-api", () => { + const result = routedBaseUrlForModel("openai-codex", rootUrl); + expect(result).toBe("http://127.0.0.1:19000/backend-api"); + }); + }); + + describe("opencode-go", () => { + it("routes to root for anthropic-messages", () => { + const model: ProviderModel = { + provider: "opencode-go", + api: "anthropic-messages", + }; + const result = routedBaseUrlForModel("opencode-go", rootUrl, model); + expect(result).toBe("http://127.0.0.1:19000"); + }); + + it("routes to /v1 for openai-completions", () => { + const model: ProviderModel = { + provider: "opencode-go", + api: "openai-completions", + }; + const result = routedBaseUrlForModel("opencode-go", rootUrl, model); + expect(result).toBe("http://127.0.0.1:19000/v1"); + }); + + it("throws for unknown api", () => { + const model: ProviderModel = { + provider: "opencode-go", + api: "unknown-api", + }; + expect(() => routedBaseUrlForModel("opencode-go", rootUrl, model)).toThrow( + "Unsupported opencode-go model API: unknown-api", + ); + }); + + it("throws when api is undefined", () => { + const model: ProviderModel = { + provider: "opencode-go", + }; + expect(() => routedBaseUrlForModel("opencode-go", rootUrl, model)).toThrow( + "Unsupported opencode-go model API: (undefined)", + ); + }); + }); +}); + +describe("providerIds", () => { + it("includes openai-codex and opencode-go", () => { + const ids = providerIds(); + expect(ids).toContain("openai-codex"); + expect(ids).toContain("opencode-go"); + }); + + it("only includes openai-codex and opencode-go", () => { + const ids = providerIds(); + expect(ids).toHaveLength(2); + }); +}); + +describe("isManagedProvider", () => { + it("returns true for openai-codex and opencode-go", () => { + expect(isManagedProvider("openai-codex")).toBe(true); + expect(isManagedProvider("opencode-go")).toBe(true); + }); + + it("returns false for old providers", () => { + expect(isManagedProvider("openai")).toBe(false); + expect(isManagedProvider("anthropic")).toBe(false); + expect(isManagedProvider("google")).toBe(false); + expect(isManagedProvider("unknown-provider")).toBe(false); + }); +}); diff --git a/test/request-identity.test.ts b/test/request-identity.test.ts new file mode 100644 index 0000000..17c92db --- /dev/null +++ b/test/request-identity.test.ts @@ -0,0 +1,65 @@ +import { describe, it, expect } from "vitest"; +import { buildHeadroomHeaders, resolveUserId } from "../request-identity.js"; + +describe("buildHeadroomHeaders", () => { + it("builds correct headers", () => { + const headers = buildHeadroomHeaders({ + cwd: "C:\\src\\project", + userId: "testuser", + sessionId: "pi-12345-test", + }); + + expect(headers["x-headroom-cwd"]).toBe("C:\\src\\project"); + expect(headers["x-headroom-user-id"]).toBe("testuser"); + expect(headers["x-headroom-session-id"]).toBe("pi-12345-test"); + }); + + it("includes project ID when set", () => { + process.env.PI_HEADROOM_PROJECT_ID = "my-project"; + try { + const headers = buildHeadroomHeaders({ + cwd: "C:\\src\\project", + userId: "testuser", + sessionId: "pi-12345-test", + }); + expect(headers["x-headroom-project-id"]).toBe("my-project"); + } finally { + delete process.env.PI_HEADROOM_PROJECT_ID; + } + }); + + it("does not include project ID when not set", () => { + delete process.env.PI_HEADROOM_PROJECT_ID; + const headers = buildHeadroomHeaders({ + cwd: "C:\\src\\project", + userId: "testuser", + sessionId: "pi-12345-test", + }); + expect(headers["x-headroom-project-id"]).toBeUndefined(); + }); +}); + +describe("resolveUserId", () => { + it("uses PI_HEADROOM_USER_ID when set", () => { + process.env.PI_HEADROOM_USER_ID = "custom-user"; + try { + expect(resolveUserId()).toBe("custom-user"); + } finally { + delete process.env.PI_HEADROOM_USER_ID; + } + }); + + it("uses USERNAME env when PI_HEADROOM_USER_ID not set", () => { + delete process.env.PI_HEADROOM_USER_ID; + const username = process.env.USERNAME; + if (username) { + expect(resolveUserId()).toBe(username); + } + }); + + it("returns a string", () => { + const userId = resolveUserId(); + expect(typeof userId).toBe("string"); + expect(userId.length).toBeGreaterThan(0); + }); +}); diff --git a/test/session-runtime.test.ts b/test/session-runtime.test.ts new file mode 100644 index 0000000..c36d32a --- /dev/null +++ b/test/session-runtime.test.ts @@ -0,0 +1,75 @@ +import { describe, it, expect, beforeEach } from "vitest"; +import { HeadroomSessionRuntime } from "../session-runtime.js"; +import { removeSessionRuntime, getOrCreateSessionRuntime } from "../runtime-state.js"; + +describe("HeadroomSessionRuntime", () => { + let runtime: HeadroomSessionRuntime; + + beforeEach(() => { + runtime = new HeadroomSessionRuntime({ + startupTimeoutMs: 5_000, + healthPollInitialDelayMs: 100, + healthPollMaximumDelayMs: 200, + gracefulShutdownTimeoutMs: 500, + }); + }); + + it("starts in idle state", () => { + expect(runtime.getState()).toBe("idle"); + }); + + it("has a unique runtime ID", () => { + expect(runtime.runtimeId).toBeTruthy(); + expect(runtime.runtimeId.length).toBe(24); + }); + + it("has correct owner PID", () => { + expect(runtime.ownerPid).toBe(process.pid); + }); + + it("no process info when idle", () => { + expect(runtime.getProcessInfo()).toBeNull(); + expect(runtime.getPort()).toBeNull(); + expect(runtime.getCwd()).toBeNull(); + }); + + it("can check restart eligibility", () => { + // Initially should be able to restart + expect(runtime.canAutomaticallyRestart()).toBe(true); + }); + + it("can clear restart limiter", () => { + runtime.recordAutomaticRestart(); + runtime.recordAutomaticRestart(); + runtime.recordAutomaticRestart(); + // After 3, should be at limit + expect(runtime.canAutomaticallyRestart()).toBe(false); + + // After clearing, should be able to restart again + runtime.clearRestartLimiter(); + expect(runtime.canAutomaticallyRestart()).toBe(true); + }); +}); + +describe("getOrCreateSessionRuntime", () => { + let runtime: HeadroomSessionRuntime; + + beforeEach(() => { + runtime = new HeadroomSessionRuntime(); + removeSessionRuntime(runtime); + }); + + it("creates new runtime on first call", () => { + const r = getOrCreateSessionRuntime(() => new HeadroomSessionRuntime()); + expect(r).toBeTruthy(); + expect(r.ownerPid).toBe(process.pid); + removeSessionRuntime(r); + }); + + it("returns same runtime on subsequent calls", () => { + const r1 = getOrCreateSessionRuntime(() => new HeadroomSessionRuntime()); + const r2 = getOrCreateSessionRuntime(() => new HeadroomSessionRuntime()); + expect(r1).toBe(r2); + removeSessionRuntime(r1); + }); +}); diff --git a/test/windows-paths.test.ts b/test/windows-paths.test.ts new file mode 100644 index 0000000..12c7338 --- /dev/null +++ b/test/windows-paths.test.ts @@ -0,0 +1,43 @@ +import { describe, it, expect } from "vitest"; +import { + canonicalizeWorkspacePath, + workspaceComparisonKey, +} from "../session-runtime.js"; + +describe("canonicalizeWorkspacePath", () => { + it("resolves relative to absolute", () => { + const result = canonicalizeWorkspacePath("."); + expect(result).toMatch(/^[A-Z]:\\/); // Windows absolute path + }); + + it("normalizes path separators", () => { + // Just verify it doesn't throw + const result = canonicalizeWorkspacePath("C:\\Repo\\..\\Repo"); + expect(typeof result).toBe("string"); + expect(result.length).toBeGreaterThan(0); + }); + + it("handles paths with spaces", () => { + const result = canonicalizeWorkspacePath("C:\\My Project"); + expect(typeof result).toBe("string"); + }); + + it("handles paths with cyrillic", () => { + const result = canonicalizeWorkspacePath("C:\\Users\\Владимир\\repo"); + expect(typeof result).toBe("string"); + }); +}); + +describe("workspaceComparisonKey", () => { + it("lowercases on Windows", () => { + if (process.platform === "win32") { + expect(workspaceComparisonKey("C:\\Repo")).toBe("c:\\repo"); + } + }); + + it("preserves case on non-Windows", () => { + if (process.platform !== "win32") { + expect(workspaceComparisonKey("/home/User/Repo")).toBe("/home/User/Repo"); + } + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..60185b1 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "Node16", + "moduleResolution": "Node16", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true, + "declaration": false, + "outDir": "./dist", + "rootDir": ".", + "types": ["node"] + }, + "include": ["*.ts", "test/**/*.ts"], + "exclude": ["node_modules", "dist"] +} diff --git a/types.ts b/types.ts new file mode 100644 index 0000000..351692c --- /dev/null +++ b/types.ts @@ -0,0 +1,131 @@ +/** + * Core types for pi-headroom per-Pi session runtime. + */ + +// ── Runtime state ──────────────────────────────────────────────── + +export type HeadroomRuntimeState = + | "idle" + | "starting" + | "ready" + | "stopping" + | "stopped" + | "failed"; + +// ── Process info ───────────────────────────────────────────────── + +export interface HeadroomProcessInfo { + pid: number; + port: number; + cwd: string; + startedAt: string; + logFile: string; +} + +// ── Runtime options ────────────────────────────────────────────── + +export interface HeadroomRuntimeOptions { + startupTimeoutMs: number; + healthPollInitialDelayMs: number; + healthPollMaximumDelayMs: number; + gracefulShutdownTimeoutMs: number; + maximumPortAttempts: number; + maximumAutomaticRestarts: number; + restartWindowMs: number; + failOpen: boolean; +} + +export const DEFAULT_RUNTIME_OPTIONS: HeadroomRuntimeOptions = { + startupTimeoutMs: 60_000, + healthPollInitialDelayMs: 250, + healthPollMaximumDelayMs: 1_000, + gracefulShutdownTimeoutMs: 3_000, + maximumPortAttempts: 3, + maximumAutomaticRestarts: 3, + restartWindowMs: 10 * 60_000, + failOpen: false, +}; + +// ── Provider model ─────────────────────────────────────────────── + +export interface ProviderModel { + id?: string; + provider: string; + baseUrl?: string; + api?: string; + [key: string]: unknown; +} + +// ── Request identity ───────────────────────────────────────────── + +export interface PiRequestIdentity { + cwd: string; + userId: string; + sessionId: string; +} + +// ── Spawn options ──────────────────────────────────────────────── + +export interface SpawnHeadroomOptions { + command: string; + cwd: string; + port: number; + logFile: string; + env: NodeJS.ProcessEnv; + args: string[]; +} + +export interface SpawnedHeadroomProcess { + pid: number; + child: import("node:child_process").ChildProcess; + port: number; +} + +// ── Provider types ─────────────────────────────────────────────── + +export type ManagedProvider = "openai-codex" | "opencode-go"; + +// ── Extension context (minimal interface) ──────────────────────── + +export type UiLevel = "info" | "warn" | "error" | "success"; + +export interface ExtensionUI { + notify(message: string, level?: UiLevel): void; + setStatus?: (id: string, text: string) => void; +} + +export interface ExtensionCtx { + model?: ProviderModel; + cwd?: string; + ui: ExtensionUI; +} + +// ── Extension API (minimal interface) ──────────────────────────── + +export interface ExtensionAPI { + registerProvider( + provider: string, + config: { + baseUrl?: string; + headers?: Record; + oauth?: Record; + }, + ): void; + unregisterProvider(provider: string): void; + registerTool( + name: string, + config: { + description: string; + parameters: Record; + execute: (args: Record, ctx: unknown) => Promise; + }, + ): void; + registerCommand( + name: string, + config: { + description: string; + handler: (args: string, ctx: ExtensionCtx) => Promise; + }, + ): void; + on(event: string, handler: (event: unknown, ctx: ExtensionCtx) => Promise): void; +} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 0000000..5fab239 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "vitest/config"; + +export default defineConfig({ + test: { + include: ["test/**/*.test.ts"], + testTimeout: 30_000, + }, +}); diff --git a/windows-process.ts b/windows-process.ts new file mode 100644 index 0000000..c8fe741 --- /dev/null +++ b/windows-process.ts @@ -0,0 +1,78 @@ +/** + * Windows process tree termination utilities. + * + * Uses child.kill() for graceful shutdown, then falls back to + * taskkill.exe /T /F for forceful tree termination. + */ + +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const execFileAsync = promisify(execFile); + +/** + * Terminate a process tree on Windows. + * + * Sequence: + * 1. Attempt graceful child.kill(). + * 2. Wait up to gracefulTimeoutMs. + * 3. If the process still exists, run taskkill /PID /T /F. + * 4. "process not found" errors are treated as success. + */ +export async function terminateProcessTreeWindows( + pid: number, + gracefulTimeoutMs: number, +): Promise { + if (pid <= 0) return; + + // Check if process exists first + if (!isProcessAlive(pid)) return; + + // Graceful kill attempt + try { + process.kill(pid, "SIGTERM"); + } catch { + // Process may have already exited + if (!isProcessAlive(pid)) return; + } + + // Wait for graceful shutdown + await new Promise((resolve) => setTimeout(resolve, gracefulTimeoutMs)); + + if (!isProcessAlive(pid)) return; + + // Force kill with taskkill + try { + await execFileAsync("taskkill.exe", [ + "/PID", + String(pid), + "/T", + "/F", + ]); + } catch (err: unknown) { + // "process not found" (exit code 128) is treated as success + const stderr = (err as { stderr?: string }).stderr ?? ""; + if (stderr.includes("not found") || stderr.includes("NOTFOUND")) { + return; + } + // Also treat exit code 128 as success + const exitCode = (err as { code?: number }).code; + if (exitCode === 128) return; + // Re-throw unexpected errors + throw err; + } +} + +/** + * Check if a process with the given PID is alive. + * On Windows, process.kill(pid, 0) works as an existence check. + */ +export function isProcessAlive(pid: number): boolean { + if (pid <= 0) return false; + try { + process.kill(pid, 0); + return true; + } catch { + return false; + } +}