commit 47878d9962826c3abb6634e8274ca28173678bb4 Author: Ivan Pereira <183991+ivanrvpereira@users.noreply.github.com> Date: Mon Feb 16 18:42:03 2026 +0000 Initial commit diff --git a/.mise.toml b/.mise.toml new file mode 100644 index 0000000..686f6ec --- /dev/null +++ b/.mise.toml @@ -0,0 +1,3 @@ +[env] +_.file = '.env' + diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..ae4eec5 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,170 @@ +# AGENTS.md — pi-perplexity + +## What This Is + +A **pi extension** (plugin for `@mariozechner/pi-coding-agent`) that provides web search via a Perplexity Pro/Max subscription. Uses OAuth JWT authentication against Perplexity's internal SSE endpoint — no API credits consumed, only the subscription. + +## Build / Test / Lint + +```bash +# Type check +bunx tsc --noEmit + +# Run tests +bun test + +# Quick smoke test — factory returns valid tool shape +bun run --bun src/index.ts +``` + +No build step. Extensions are loaded via jiti — TypeScript runs directly. + +## Project Structure + +``` +pi-perplexity/ + package.json # pi extension manifest (see "omp"/"pi" field) + tsconfig.json + AGENTS.md # You are here + architecture.md # Full protocol spec — SSE format, auth flows, event schemas + plan.md # Implementation phases and acceptance criteria + docs/ + pi_docs_extension.md # Official pi extension system documentation + pi_platform_reference.md # Pi platform reference (SDK, RPC, sessions, settings, packages) + src/ + index.ts # CustomToolFactory entry — default export + auth/ + jwt.ts # JWT base64url decode, expiry extraction + login.ts # macOS app extraction + email OTP flow + storage.ts # Token persistence (~/.config/pi-perplexity/auth.json) + search/ + types.ts # All type definitions (StreamEvent, SearchResult, etc.) + client.ts # HTTP POST to SSE endpoint, orchestrates stream + merge + stream.ts # SSE line parser + incremental event merging + format.ts # SearchResult → LLM-readable text output + render/ + call.ts # TUI renderCall component + result.ts # TUI renderResult component +``` + +## Critical Constraints + +### Zero Runtime Dependencies +This plugin has **zero npm dependencies**. All HTTP, SSE parsing, JWT decoding, and UUID generation use platform globals: +- `fetch` — global (Bun/Node 18+) +- `crypto.randomUUID()` — global +- `atob` / `Buffer.from(payload, "base64url")` — global +- `Intl.DateTimeFormat` — global + +Do NOT add dependencies to `dependencies` in package.json. + +### Peer Dependencies Only +These are bundled by pi and must go in `peerDependencies` with `"*"` range: +- `@sinclair/typebox` — schema definitions (injected at runtime via `api.typebox`) +- `@mariozechner/pi-tui` — TUI Component types for renderers + +### Reverse-Engineered API +The Perplexity SSE endpoint is **not a public API**. It can break without notice. +- Keep types loose — all fields optional +- Keep the client thin — minimal assumptions about response shape +- Specific User-Agent and headers are required (see architecture.md § Request) +- `is_incognito: true` always — don't pollute user's Perplexity history + +## Coding Conventions + +### TypeScript +- `strict: true`, `noEmit: true` +- Target: ESNext, module: ESNext, moduleResolution: bundler +- Use `interface` for data shapes, `type` for unions/aliases +- All stream event fields are optional — the API is unstable + +### Extension API +- Entry point: `src/index.ts` exports `default` function or factory +- Import types from `@mariozechner/pi-coding-agent` +- Use `StringEnum` from `@mariozechner/pi-ai` for string enum params — `Type.Union`/`Type.Literal` breaks Google's API +- Tool execute signature: `execute(toolCallId, params, signal, onUpdate, ctx)` +- Return shape: `{ content: [{ type: "text", text }], details: { ... } }` + +### Naming +- Tool name: `perplexity_search` (snake_case, matches pi convention) +- File names: lowercase, descriptive (`stream.ts`, `client.ts`, `jwt.ts`) +- Types: PascalCase (`StreamEvent`, `SearchResult`, `StoredToken`) +- Constants: UPPER_SNAKE or camelCase for compound values + +### Error Handling +- Never throw raw errors from tool execute — always return error text in `content` +- Use a `SearchError` class for typed errors with code and message +- HTTP 401/403: clear stored token, return auth error to agent +- HTTP 429: return rate limit message with retry suggestion +- Network failures: return error text, don't crash +- Empty responses: return "No results found" +- JWT errors: fallback expiry of 1 hour if decode fails + +## Auth Flow (two paths, tried in order) + +1. **macOS Desktop App** (zero-interaction): `defaults read ai.perplexity.mac authToken` + - Skip if `PI_AUTH_NO_BORROW=1` is set + - Returns null on non-macOS or if app not installed +2. **Email OTP** (interactive fallback): CSRF → send OTP → verify OTP + - Uses `ctx.ui.input()` for OTP prompt + +Token stored at `~/.config/pi-perplexity/auth.json` with `0600` permissions. +JWT expiry: `decoded.exp * 1000 - 5min` buffer. No auto-refresh — re-login on expiry. + +## SSE Stream Protocol + +Endpoint: `POST https://www.perplexity.ai/rest/sse/perplexity_ask` + +Events are **incremental snapshots** that must be merged: +- Top-level fields: shallow merge +- Blocks: keyed by `intended_usage` — merge, don't replace array +- Markdown chunks: respect `chunk_starting_offset` for splice +- Sources: accumulate, never replace; preserve from earlier events + +Stream terminates when `event.final === true` or `event.status === "COMPLETED"`. + +Answer extraction priority: markdown blocks → ask_text blocks → event.text fallback. +Source extraction priority: web_results block → sources_list fallback. Deduplicate by URL. + +Full protocol details: `architecture.md` § Search Protocol, § Response: SSE Event Stream. + +## Tool Output Format + +``` +## Answer + + +## Sources +N sources +[1] Title (2d ago) + https://url + snippet preview... + +## Meta +Provider: perplexity (oauth) +Model: +``` + +- Age: human-readable relative time ("2d ago", "3h ago", "just now") +- Snippets: truncated to 240 chars +- Source count: respect `limit` parameter + +## Key References + +| What | Where | +|------|-------| +| Full protocol spec (headers, body, SSE events) | `architecture.md` | +| Implementation phases and acceptance criteria | `plan.md` | +| Pi extension system overview | `docs/pi_docs_extension.md` | +| Pi platform reference (SDK, RPC, sessions, settings, packages) | `docs/pi_platform_reference.md` | + +## Common Gotchas + +- `Type.Union([Type.Literal("a"), ...])` does NOT work for Google models — use `StringEnum` from `@mariozechner/pi-ai` +- Tool `execute` param order is `(toolCallId, params, signal, onUpdate, ctx)` — signal before onUpdate +- The SSE stream is NOT standard Server-Sent Events — it uses `data:` lines with JSON but requires custom parsing for multi-line data fields and `[DONE]` marker +- Perplexity SSE events are incremental snapshots, not deltas — each event contains the full state up to that point, but blocks must still be merged by `intended_usage` key +- macOS `defaults read` via Bun shell (`Bun.$`) — handle non-zero exit code (app not installed) gracefully +- JWT `exp` claim is in seconds, not milliseconds — multiply by 1000 +- Token file must be written with `0600` permissions — use `Bun.write()` and set mode +- Always pass `AbortSignal` through to `fetch` for cancellation support diff --git a/architecture.md b/architecture.md new file mode 100644 index 0000000..e6e9f65 --- /dev/null +++ b/architecture.md @@ -0,0 +1,320 @@ +# pi-perplexity Architecture + +## Overview + +An oh-my-pi plugin that provides web search via a Perplexity Pro/Max subscription. Uses OAuth JWT authentication against Perplexity's internal SSE endpoint — no API credits consumed, only the subscription. + +## System Context + +``` +oh-my-pi (coding-agent) + | + +-- plugin loader (discovers pi-perplexity via package.json "omp" manifest) + | + +-- pi-perplexity (CustomToolFactory) + | + +-- perplexity_search tool (CustomTool) + | | + | +-- Auth: JWT from macOS app or email OTP + | +-- Search: POST SSE to www.perplexity.ai + | +-- Parse: incremental event merging + | +-- Render: TUI components for call/result + | + +-- Token storage (SQLite via omp's AgentStorage, or standalone file) +``` + +## Authentication + +### JWT Acquisition (two paths, tried in order) + +**Path 1 — macOS Desktop App Extraction (zero-interaction)** + +The Perplexity macOS Catalyst app (`ai.perplexity.mac`) stores its auth JWT in NSUserDefaults, readable by any same-UID process: + +```bash +defaults read ai.perplexity.mac authToken +``` + +If the app is installed and logged in, this returns the JWT immediately. No browser, no user interaction. + +Skip this path if `PI_AUTH_NO_BORROW=1` is set. + +**Path 2 — Email OTP (interactive, fallback)** + +``` +GET https://www.perplexity.ai/api/auth/csrf + -> { csrfToken: string } + +POST https://www.perplexity.ai/api/auth/signin-email + body: { email, csrfToken } + -> Sends OTP code to email + +(user enters OTP) + +POST https://www.perplexity.ai/api/auth/signin-otp + body: { email, otp, csrfToken } + -> { token: "" } +``` + +All auth requests use these headers: +``` +User-Agent: Perplexity/641 CFNetwork/1568 Darwin/25.2.0 +X-App-ApiVersion: 2.18 +``` + +### JWT Handling + +- Expiry extracted from JWT payload `exp` claim: `decoded.exp * 1000 - 5min` +- Fallback expiry: 1 hour from acquisition if decode fails +- No automated refresh — Perplexity JWTs are long-lived; re-login on expiry +- Storage: persisted to disk so login survives restarts + +### Token Storage + +The JWT is stored as: +```typescript +{ + type: "oauth", + access: "", + expires: , + email?: "" +} +``` + +Two storage strategies (choose during implementation): + +1. **Integrate with omp's AgentStorage** — query `listAuthCredentials("perplexity")` from the agent.db SQLite. Requires access to the db path via `getAgentDbPath()`. +2. **Standalone JSON file** — `~/.config/pi-perplexity/auth.json`. Simpler, no dependency on omp internals, portable. + +On search, check stored JWT expiry (with 5-minute buffer). If expired, prompt re-login. + +## Search Protocol + +### Endpoint + +``` +POST https://www.perplexity.ai/rest/sse/perplexity_ask +``` + +### Request + +**Headers:** +``` +Authorization: Bearer +Content-Type: application/json +Accept: text/event-stream +Origin: https://www.perplexity.ai +Referer: https://www.perplexity.ai/ +User-Agent: Perplexity/641 CFNetwork/1568 Darwin/25.2.0 +X-App-ApiClient: default +X-App-ApiVersion: 2.18 +X-Perplexity-Request-Reason: submit +X-Request-ID: +``` + +**Body:** +```json +{ + "query_str": "", + "params": { + "query_str": "", + "search_focus": "internet", + "mode": "copilot", + "model_preference": "pplx_pro_upgraded", + "sources": ["web"], + "attachments": [], + "frontend_uuid": "", + "frontend_context_uuid": "", + "version": "2.18", + "language": "en-US", + "timezone": "", + "search_recency_filter": null | "hour" | "day" | "week" | "month" | "year", + "is_incognito": true, + "use_schematized_api": true, + "skip_search_enabled": true + } +} +``` + +Key parameters: +- `model_preference: "pplx_pro_upgraded"` — Pro subscription model +- `mode: "copilot"` — multi-step reasoning (Pro feature) +- `is_incognito: true` — does not save to Perplexity history +- `effective_query` — system prompt prepended to user query with `\n\n` separator (no separate system message in this API) + +### Response: SSE Event Stream + +Each `data:` line contains a JSON event. Events are **incremental snapshots** that must be merged. + +#### Event Shape + +```typescript +interface StreamEvent { + status?: string; // "COMPLETED" on final + final?: boolean; // true on last event + text?: string; // plain text answer (fallback) + blocks?: StreamBlock[]; // structured content blocks + sources_list?: StreamSource[];// source references (alternative format) + display_model?: string; // model name used + uuid?: string; // request ID + error_code?: string; + error_message?: string; +} + +interface StreamBlock { + intended_usage?: string; // "markdown_block" | "ask_text" | "web_results" + markdown_block?: { + answer?: string; + chunks?: string[]; // incremental text chunks + chunk_starting_offset?: number; + }; + web_result_block?: { + web_results?: WebResult[]; + }; +} + +interface WebResult { + name?: string; + url?: string; + snippet?: string; + timestamp?: string; +} + +interface StreamSource { + title?: string; + url?: string; + snippet?: string; + date?: string; +} +``` + +#### Event Merging Strategy + +Events are incremental — each new event is merged into a running snapshot: + +1. **Top-level fields**: shallow merge (`{ ...existing, ...incoming }`) +2. **Blocks**: keyed by `intended_usage` — new blocks with same key replace/merge with existing +3. **Markdown chunks**: if `chunk_starting_offset` is 0, replace all chunks; otherwise splice at offset +4. **Sources**: accumulated, not replaced; `sources_list` preserved from earlier events if absent in later ones + +Stream terminates when `event.final === true` or `event.status === "COMPLETED"`. + +#### Answer Extraction (priority order) + +1. `blocks` where `intended_usage` contains `"markdown"` -> join `chunks[]` or use `answer` field +2. `blocks` where `intended_usage === "ask_text"` -> same logic +3. `event.text` fallback + +#### Source Extraction (priority order) + +1. `blocks` where `intended_usage === "web_results"` -> `web_result_block.web_results[]` +2. `event.sources_list[]` fallback +3. Deduplicate by URL + +## Plugin Interface + +### oh-my-pi CustomTool Contract + +The plugin exports a `CustomToolFactory`: + +```typescript +type CustomToolFactory = (api: CustomToolAPI) => + CustomTool | CustomTool[] | Promise; +``` + +Each `CustomTool` implements: +```typescript +interface CustomTool { + name: string; // "perplexity_search" + label: string; // "Perplexity Search" + description: string; // tool description for LLM + parameters: TSchema; // TypeBox schema + execute(toolCallId, params, onUpdate, ctx, signal): Promise; + renderCall?(args, theme): Component; // TUI call display + renderResult?(result, options, theme): Component; // TUI result display +} +``` + +### Tool Parameters + +```typescript +{ + query: string; // required + recency?: "hour" | "day" | "week" | "month" | "year"; + limit?: number; // max sources to return +} +``` + +### Tool Output + +Text block formatted for LLM consumption: +``` +## Answer + + +## Sources +N sources +[1] Title (age) + https://url + snippet... + +## Meta +Provider: perplexity (oauth) +Model: +``` + +## Package Structure + +``` +pi-perplexity/ + package.json # omp plugin manifest + tsconfig.json + src/ + index.ts # CustomToolFactory entry point + auth/ + jwt.ts # JWT decode, expiry extraction + login.ts # macOS app extraction + email OTP flow + storage.ts # Token persistence (read/write/check expiry) + search/ + client.ts # HTTP request to SSE endpoint + stream.ts # SSE parsing + event merging + types.ts # All type definitions + format.ts # Response formatting for LLM output + render/ + call.ts # TUI renderCall component + result.ts # TUI renderResult component +``` + +## Dependencies + +### Required +- `@sinclair/typebox` — injected by omp via `CustomToolAPI.typebox`, no direct dependency needed +- `@oh-my-pi/pi-tui` — for TUI `Component` type in renderers (peer dependency) + +### None (zero runtime dependencies) +- `fetch` — global (Bun/Node 18+) +- `crypto.randomUUID()` — global +- `atob` / `Buffer` — global +- `Bun.$` — for `defaults read` on macOS +- `Intl.DateTimeFormat` — global + +The plugin should have **zero npm dependencies**. All HTTP, SSE parsing, and JWT decoding use platform APIs. + +## Error Handling + +| Error | Behavior | +|---|---| +| No JWT found (not logged in) | Return error text to agent: "Not authenticated. Run `omp login perplexity`." | +| JWT expired | Attempt re-login via macOS app extraction; if fails, return error prompting manual login | +| HTTP 401/403 | JWT revoked or expired; clear stored token, return auth error | +| HTTP 429 | Rate limited; return error with retry suggestion | +| SSE `error_code` in stream | Extract `error_message`, throw SearchError | +| Network failure | Return error text to agent | +| Empty response (no answer, no sources) | Return "No results found" | + +## Security Considerations + +- JWT stored on disk with user-only permissions (0600) +- `is_incognito: true` prevents queries from appearing in Perplexity history +- No API key needed — subscription auth only +- Token never logged; only expiry metadata logged at debug level diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..450b323 --- /dev/null +++ b/bun.lock @@ -0,0 +1,585 @@ +{ + "lockfileVersion": 1, + "configVersion": 1, + "workspaces": { + "": { + "name": "pi-perplexity", + "devDependencies": { + "@mariozechner/pi-ai": "*", + "@mariozechner/pi-coding-agent": "*", + "@mariozechner/pi-tui": "*", + "@sinclair/typebox": "*", + "bun-types": "*", + "typescript": "*", + }, + "peerDependencies": { + "@mariozechner/pi-ai": "*", + "@mariozechner/pi-tui": "*", + "@sinclair/typebox": "*", + }, + }, + }, + "packages": { + "@anthropic-ai/sdk": ["@anthropic-ai/sdk@0.73.0", "", { "dependencies": { "json-schema-to-ts": "^3.1.1" }, "peerDependencies": { "zod": "^3.25.0 || ^4.0.0" }, "optionalPeers": ["zod"], "bin": { "anthropic-ai-sdk": "bin/cli" } }, "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw=="], + + "@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="], + + "@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="], + + "@aws-crypto/sha256-js": ["@aws-crypto/sha256-js@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA=="], + + "@aws-crypto/supports-web-crypto": ["@aws-crypto/supports-web-crypto@5.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg=="], + + "@aws-crypto/util": ["@aws-crypto/util@5.2.0", "", { "dependencies": { "@aws-sdk/types": "^3.222.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ=="], + + "@aws-sdk/client-bedrock-runtime": ["@aws-sdk/client-bedrock-runtime@3.990.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.10", "@aws-sdk/credential-provider-node": "^3.972.9", "@aws-sdk/eventstream-handler-node": "^3.972.5", "@aws-sdk/middleware-eventstream": "^3.972.3", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.10", "@aws-sdk/middleware-websocket": "^3.972.6", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/token-providers": "3.990.0", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.990.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.8", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.0", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/eventstream-serde-config-resolver": "^4.3.8", "@smithy/eventstream-serde-node": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-retry": "^4.4.31", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.30", "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-8TtV9c0DWGxwYvlcED/NlhTM0aDHM9yb0Y3Q0b0NQwiyrahX+qlck/Wo8fJQ7GHAkFn8MtczvAQzbLszyo+w0Q=="], + + "@aws-sdk/client-sso": ["@aws-sdk/client-sso@3.990.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.10", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.990.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.8", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-retry": "^4.4.31", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.30", "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-xTEaPjZwOqVjGbLOP7qzwbdOWJOo1ne2mUhTZwEBBkPvNk4aXB/vcYwWwrjoSWUqtit4+GDbO75ePc/S6TUJYQ=="], + + "@aws-sdk/core": ["@aws-sdk/core@3.973.10", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/xml-builder": "^3.972.4", "@smithy/core": "^3.23.0", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-4u/FbyyT3JqzfsESI70iFg6e2yp87MB5kS2qcxIA66m52VSTN1fvuvbCY1h/LKq1LvuxIrlJ1ItcyjvcKoaPLg=="], + + "@aws-sdk/credential-provider-env": ["@aws-sdk/credential-provider-env@3.972.8", "", { "dependencies": { "@aws-sdk/core": "^3.973.10", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-r91OOPAcHnLCSxaeu/lzZAVRCZ/CtTNuwmJkUwpwSDshUrP7bkX1OmFn2nUMWd9kN53Q4cEo8b7226G4olt2Mg=="], + + "@aws-sdk/credential-provider-http": ["@aws-sdk/credential-provider-http@3.972.10", "", { "dependencies": { "@aws-sdk/core": "^3.973.10", "@aws-sdk/types": "^3.973.1", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-DTtuyXSWB+KetzLcWaSahLJCtTUe/3SXtlGp4ik9PCe9xD6swHEkG8n8/BNsQ9dsihb9nhFvuUB4DpdBGDcvVg=="], + + "@aws-sdk/credential-provider-ini": ["@aws-sdk/credential-provider-ini@3.972.8", "", { "dependencies": { "@aws-sdk/core": "^3.973.10", "@aws-sdk/credential-provider-env": "^3.972.8", "@aws-sdk/credential-provider-http": "^3.972.10", "@aws-sdk/credential-provider-login": "^3.972.8", "@aws-sdk/credential-provider-process": "^3.972.8", "@aws-sdk/credential-provider-sso": "^3.972.8", "@aws-sdk/credential-provider-web-identity": "^3.972.8", "@aws-sdk/nested-clients": "3.990.0", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-n2dMn21gvbBIEh00E8Nb+j01U/9rSqFIamWRdGm/mE5e+vHQ9g0cBNdrYFlM6AAiryKVHZmShWT9D1JAWJ3ISw=="], + + "@aws-sdk/credential-provider-login": ["@aws-sdk/credential-provider-login@3.972.8", "", { "dependencies": { "@aws-sdk/core": "^3.973.10", "@aws-sdk/nested-clients": "3.990.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-rMFuVids8ICge/X9DF5pRdGMIvkVhDV9IQFQ8aTYk6iF0rl9jOUa1C3kjepxiXUlpgJQT++sLZkT9n0TMLHhQw=="], + + "@aws-sdk/credential-provider-node": ["@aws-sdk/credential-provider-node@3.972.9", "", { "dependencies": { "@aws-sdk/credential-provider-env": "^3.972.8", "@aws-sdk/credential-provider-http": "^3.972.10", "@aws-sdk/credential-provider-ini": "^3.972.8", "@aws-sdk/credential-provider-process": "^3.972.8", "@aws-sdk/credential-provider-sso": "^3.972.8", "@aws-sdk/credential-provider-web-identity": "^3.972.8", "@aws-sdk/types": "^3.973.1", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LfJfO0ClRAq2WsSnA9JuUsNyIicD2eyputxSlSL0EiMrtxOxELLRG6ZVYDf/a1HCepaYPXeakH4y8D5OLCauag=="], + + "@aws-sdk/credential-provider-process": ["@aws-sdk/credential-provider-process@3.972.8", "", { "dependencies": { "@aws-sdk/core": "^3.973.10", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-6cg26ffFltxM51OOS8NH7oE41EccaYiNlbd5VgUYwhiGCySLfHoGuGrLm2rMB4zhy+IO5nWIIG0HiodX8zdvHA=="], + + "@aws-sdk/credential-provider-sso": ["@aws-sdk/credential-provider-sso@3.972.8", "", { "dependencies": { "@aws-sdk/client-sso": "3.990.0", "@aws-sdk/core": "^3.973.10", "@aws-sdk/token-providers": "3.990.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-35kqmFOVU1n26SNv+U37sM8b2TzG8LyqAcd6iM9gprqxyHEh/8IM3gzN4Jzufs3qM6IrH8e43ryZWYdvfVzzKQ=="], + + "@aws-sdk/credential-provider-web-identity": ["@aws-sdk/credential-provider-web-identity@3.972.8", "", { "dependencies": { "@aws-sdk/core": "^3.973.10", "@aws-sdk/nested-clients": "3.990.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CZhN1bOc1J3ubQPqbmr5b4KaMJBgdDvYsmEIZuX++wFlzmZsKj1bwkaiTEb5U2V7kXuzLlpF5HJSOM9eY/6nGA=="], + + "@aws-sdk/eventstream-handler-node": ["@aws-sdk/eventstream-handler-node@3.972.5", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/eventstream-codec": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-xEmd3dnyn83K6t4AJxBJA63wpEoCD45ERFG0XMTViD2E/Ohls9TLxjOWPb1PAxR9/46cKy/TImez1GoqP6xVNQ=="], + + "@aws-sdk/middleware-eventstream": ["@aws-sdk/middleware-eventstream@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-pbvZ6Ye/Ks6BAZPa3RhsNjHrvxU9li25PMhSdDpbX0jzdpKpAkIR65gXSNKmA/REnSdEMWSD4vKUW+5eMFzB6w=="], + + "@aws-sdk/middleware-host-header": ["@aws-sdk/middleware-host-header@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aknPTb2M+G3s+0qLCx4Li/qGZH8IIYjugHMv15JTYMe6mgZO8VBpYgeGYsNMGCqCZOcWzuf900jFBG5bopfzmA=="], + + "@aws-sdk/middleware-logger": ["@aws-sdk/middleware-logger@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-Ftg09xNNRqaz9QNzlfdQWfpqMCJbsQdnZVJP55jfhbKi1+FTWxGuvfPoBhDHIovqWKjqbuiew3HuhxbJ0+OjgA=="], + + "@aws-sdk/middleware-recursion-detection": ["@aws-sdk/middleware-recursion-detection@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws/lambda-invoke-store": "^0.2.2", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PY57QhzNuXHnwbJgbWYTrqIDHYSeOlhfYERTAuc16LKZpTZRJUjzBFokp9hF7u1fuGeE3D70ERXzdbMBOqQz7Q=="], + + "@aws-sdk/middleware-user-agent": ["@aws-sdk/middleware-user-agent@3.972.10", "", { "dependencies": { "@aws-sdk/core": "^3.973.10", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.990.0", "@smithy/core": "^3.23.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-bBEL8CAqPQkI91ZM5a9xnFAzedpzH6NYCOtNyLarRAzTUTFN2DKqaC60ugBa7pnU1jSi4mA7WAXBsrod7nJltg=="], + + "@aws-sdk/middleware-websocket": ["@aws-sdk/middleware-websocket@3.972.6", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-format-url": "^3.972.3", "@smithy/eventstream-codec": "^4.2.8", "@smithy/eventstream-serde-browser": "^4.2.8", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/protocol-http": "^5.3.8", "@smithy/signature-v4": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-1DedO6N3m8zQ/vG6twNiHtsdwBgk773VdavLEbB3NXeKZDlzSK1BTviqWwvJdKx5UnIy4kGGP6WWpCEFEt/bhQ=="], + + "@aws-sdk/nested-clients": ["@aws-sdk/nested-clients@3.990.0", "", { "dependencies": { "@aws-crypto/sha256-browser": "5.2.0", "@aws-crypto/sha256-js": "5.2.0", "@aws-sdk/core": "^3.973.10", "@aws-sdk/middleware-host-header": "^3.972.3", "@aws-sdk/middleware-logger": "^3.972.3", "@aws-sdk/middleware-recursion-detection": "^3.972.3", "@aws-sdk/middleware-user-agent": "^3.972.10", "@aws-sdk/region-config-resolver": "^3.972.3", "@aws-sdk/types": "^3.973.1", "@aws-sdk/util-endpoints": "3.990.0", "@aws-sdk/util-user-agent-browser": "^3.972.3", "@aws-sdk/util-user-agent-node": "^3.972.8", "@smithy/config-resolver": "^4.4.6", "@smithy/core": "^3.23.0", "@smithy/fetch-http-handler": "^5.3.9", "@smithy/hash-node": "^4.2.8", "@smithy/invalid-dependency": "^4.2.8", "@smithy/middleware-content-length": "^4.2.8", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-retry": "^4.4.31", "@smithy/middleware-serde": "^4.2.9", "@smithy/middleware-stack": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/node-http-handler": "^4.4.10", "@smithy/protocol-http": "^5.3.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-body-length-node": "^4.2.1", "@smithy/util-defaults-mode-browser": "^4.3.30", "@smithy/util-defaults-mode-node": "^4.2.33", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-3NA0s66vsy8g7hPh36ZsUgO4SiMyrhwcYvuuNK1PezO52vX3hXDW4pQrC6OQLGKGJV0o6tbEyQtXb/mPs8zg8w=="], + + "@aws-sdk/region-config-resolver": ["@aws-sdk/region-config-resolver@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/config-resolver": "^4.4.6", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-v4J8qYAWfOMcZ4MJUyatntOicTzEMaU7j3OpkRCGGFSL2NgXQ5VbxauIyORA+pxdKZ0qQG2tCQjQjZDlXEC3Ow=="], + + "@aws-sdk/token-providers": ["@aws-sdk/token-providers@3.990.0", "", { "dependencies": { "@aws-sdk/core": "^3.973.10", "@aws-sdk/nested-clients": "3.990.0", "@aws-sdk/types": "^3.973.1", "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-L3BtUb2v9XmYgQdfGBzbBtKMXaP5fV973y3Qdxeevs6oUTVXFmi/mV1+LnScA/1wVPJC9/hlK+1o5vbt7cG7EQ=="], + + "@aws-sdk/types": ["@aws-sdk/types@3.973.1", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DwHBiMNOB468JiX6+i34c+THsKHErYUdNQ3HexeXZvVn4zouLjgaS4FejiGSi2HyBuzuyHg7SuOPmjSvoU9NRg=="], + + "@aws-sdk/util-endpoints": ["@aws-sdk/util-endpoints@3.990.0", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-endpoints": "^3.2.8", "tslib": "^2.6.2" } }, "sha512-kVwtDc9LNI3tQZHEMNbkLIOpeDK8sRSTuT8eMnzGY+O+JImPisfSTjdh+jw9OTznu+MYZjQsv0258sazVKunYg=="], + + "@aws-sdk/util-format-url": ["@aws-sdk/util-format-url@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-n7F2ycckcKFXa01vAsT/SJdjFHfKH9s96QHcs5gn8AaaigASICeME8WdUL9uBp8XV/OVwEt8+6gzn6KFUgQa8g=="], + + "@aws-sdk/util-locate-window": ["@aws-sdk/util-locate-window@3.965.4", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-H1onv5SkgPBK2P6JR2MjGgbOnttoNzSPIRoeZTNPZYyaplwGg50zS3amXvXqF0/qfXpWEC9rLWU564QTB9bSog=="], + + "@aws-sdk/util-user-agent-browser": ["@aws-sdk/util-user-agent-browser@3.972.3", "", { "dependencies": { "@aws-sdk/types": "^3.973.1", "@smithy/types": "^4.12.0", "bowser": "^2.11.0", "tslib": "^2.6.2" } }, "sha512-JurOwkRUcXD/5MTDBcqdyQ9eVedtAsZgw5rBwktsPTN7QtPiS2Ld1jkJepNgYoCufz1Wcut9iup7GJDoIHp8Fw=="], + + "@aws-sdk/util-user-agent-node": ["@aws-sdk/util-user-agent-node@3.972.8", "", { "dependencies": { "@aws-sdk/middleware-user-agent": "^3.972.10", "@aws-sdk/types": "^3.973.1", "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" }, "peerDependencies": { "aws-crt": ">=1.0.0" }, "optionalPeers": ["aws-crt"] }, "sha512-XJZuT0LWsFCW1C8dEpPAXSa7h6Pb3krr2y//1X0Zidpcl0vmgY5nL/X0JuBZlntpBzaN3+U4hvKjuijyiiR8zw=="], + + "@aws-sdk/xml-builder": ["@aws-sdk/xml-builder@3.972.4", "", { "dependencies": { "@smithy/types": "^4.12.0", "fast-xml-parser": "5.3.4", "tslib": "^2.6.2" } }, "sha512-0zJ05ANfYqI6+rGqj8samZBFod0dPPousBjLEqg8WdxSgbMAkRgLyn81lP215Do0rFJ/17LIXwr7q0yK24mP6Q=="], + + "@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.2.3", "", {}, "sha512-oLvsaPMTBejkkmHhjf09xTgk71mOqyr/409NKhRIL08If7AhVfUsJhVsx386uJaqNd42v9kWamQ9lFbkoC2dYw=="], + + "@babel/runtime": ["@babel/runtime@7.28.6", "", {}, "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA=="], + + "@borewit/text-codec": ["@borewit/text-codec@0.2.1", "", {}, "sha512-k7vvKPbf7J2fZ5klGRD9AeKfUvojuZIQ3BT5u7Jfv+puwXkUBUT5PVyMDfJZpy30CBDXGMgw7fguK/lpOMBvgw=="], + + "@google/genai": ["@google/genai@1.41.0", "", { "dependencies": { "google-auth-library": "^10.3.0", "p-retry": "^7.1.1", "protobufjs": "^7.5.4", "ws": "^8.18.0" }, "peerDependencies": { "@modelcontextprotocol/sdk": "^1.25.2" }, "optionalPeers": ["@modelcontextprotocol/sdk"] }, "sha512-S4WGil+PG0NBQRAx+0yrQuM/TWOLn2gGEy5wn4IsoOI6ouHad0P61p3OWdhJ3aqr9kfj8o904i/jevfaGoGuIQ=="], + + "@isaacs/cliui": ["@isaacs/cliui@9.0.0", "", {}, "sha512-AokJm4tuBHillT+FpMtxQ60n8ObyXBatq7jD2/JA9dxbDDokKQm8KMht5ibGzLVU9IJDIKK4TPKgMHEYMn3lMg=="], + + "@mariozechner/clipboard": ["@mariozechner/clipboard@0.3.2", "", { "optionalDependencies": { "@mariozechner/clipboard-darwin-arm64": "0.3.2", "@mariozechner/clipboard-darwin-universal": "0.3.2", "@mariozechner/clipboard-darwin-x64": "0.3.2", "@mariozechner/clipboard-linux-arm64-gnu": "0.3.2", "@mariozechner/clipboard-linux-arm64-musl": "0.3.2", "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2", "@mariozechner/clipboard-linux-x64-gnu": "0.3.2", "@mariozechner/clipboard-linux-x64-musl": "0.3.2", "@mariozechner/clipboard-win32-arm64-msvc": "0.3.2", "@mariozechner/clipboard-win32-x64-msvc": "0.3.2" } }, "sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA=="], + + "@mariozechner/clipboard-darwin-arm64": ["@mariozechner/clipboard-darwin-arm64@0.3.2", "", { "os": "darwin", "cpu": "arm64" }, "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw=="], + + "@mariozechner/clipboard-darwin-universal": ["@mariozechner/clipboard-darwin-universal@0.3.2", "", { "os": "darwin" }, "sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg=="], + + "@mariozechner/clipboard-darwin-x64": ["@mariozechner/clipboard-darwin-x64@0.3.2", "", { "os": "darwin", "cpu": "x64" }, "sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg=="], + + "@mariozechner/clipboard-linux-arm64-gnu": ["@mariozechner/clipboard-linux-arm64-gnu@0.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ=="], + + "@mariozechner/clipboard-linux-arm64-musl": ["@mariozechner/clipboard-linux-arm64-musl@0.3.2", "", { "os": "linux", "cpu": "arm64" }, "sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw=="], + + "@mariozechner/clipboard-linux-riscv64-gnu": ["@mariozechner/clipboard-linux-riscv64-gnu@0.3.2", "", { "os": "linux", "cpu": "none" }, "sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA=="], + + "@mariozechner/clipboard-linux-x64-gnu": ["@mariozechner/clipboard-linux-x64-gnu@0.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A=="], + + "@mariozechner/clipboard-linux-x64-musl": ["@mariozechner/clipboard-linux-x64-musl@0.3.2", "", { "os": "linux", "cpu": "x64" }, "sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw=="], + + "@mariozechner/clipboard-win32-arm64-msvc": ["@mariozechner/clipboard-win32-arm64-msvc@0.3.2", "", { "os": "win32", "cpu": "arm64" }, "sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg=="], + + "@mariozechner/clipboard-win32-x64-msvc": ["@mariozechner/clipboard-win32-x64-msvc@0.3.2", "", { "os": "win32", "cpu": "x64" }, "sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA=="], + + "@mariozechner/jiti": ["@mariozechner/jiti@2.6.5", "", { "dependencies": { "std-env": "^3.10.0", "yoctocolors": "^2.1.2" }, "bin": { "jiti": "lib/jiti-cli.mjs" } }, "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw=="], + + "@mariozechner/pi-agent-core": ["@mariozechner/pi-agent-core@0.52.12", "", { "dependencies": { "@mariozechner/pi-ai": "^0.52.12" } }, "sha512-fBQdwLMvTteHUP9nJxMjtMpEHH4I8tdGnkerOoCFnS9y03AHdqy96IhtL+zZjw9N3dmVCOVqh8gwGjAGLZT31Q=="], + + "@mariozechner/pi-ai": ["@mariozechner/pi-ai@0.52.12", "", { "dependencies": { "@anthropic-ai/sdk": "^0.73.0", "@aws-sdk/client-bedrock-runtime": "^3.983.0", "@google/genai": "^1.40.0", "@mistralai/mistralai": "1.10.0", "@sinclair/typebox": "^0.34.41", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "chalk": "^5.6.2", "openai": "6.10.0", "partial-json": "^0.1.7", "proxy-agent": "^6.5.0", "undici": "^7.19.1", "zod-to-json-schema": "^3.24.6" }, "bin": { "pi-ai": "dist/cli.js" } }, "sha512-oF7OMJu1aUx7MXJeJoJ/3JDXzD2a5SqK9nHVK3mCA8DRQaykv9g+wcFZaANcCl0vAR2QSDr5KN3ZMARlFNWiVg=="], + + "@mariozechner/pi-coding-agent": ["@mariozechner/pi-coding-agent@0.52.12", "", { "dependencies": { "@mariozechner/jiti": "^2.6.2", "@mariozechner/pi-agent-core": "^0.52.12", "@mariozechner/pi-ai": "^0.52.12", "@mariozechner/pi-tui": "^0.52.12", "@silvia-odwyer/photon-node": "^0.3.4", "chalk": "^5.5.0", "cli-highlight": "^2.1.11", "diff": "^8.0.2", "file-type": "^21.1.1", "glob": "^13.0.1", "hosted-git-info": "^9.0.2", "ignore": "^7.0.5", "marked": "^15.0.12", "minimatch": "^10.1.1", "proper-lockfile": "^4.1.2", "yaml": "^2.8.2" }, "optionalDependencies": { "@mariozechner/clipboard": "^0.3.2" }, "bin": { "pi": "dist/cli.js" } }, "sha512-6Zmh57vUoRiN+rfRJxWErII/CNC5/3yX5nCU7tK+Eud2Ko+RcVZoBccwjdIUzsJib3Liw/yv9T1EWvz6ZdGbhw=="], + + "@mariozechner/pi-tui": ["@mariozechner/pi-tui@0.52.12", "", { "dependencies": { "@types/mime-types": "^2.1.4", "chalk": "^5.5.0", "get-east-asian-width": "^1.3.0", "marked": "^15.0.12", "mime-types": "^3.0.1" } }, "sha512-QQ4LUlAYKN2BvT3EMU63+kYLlIkyr706+rUFBGWvkiT8ZyMy5if3oaVJpO5qAndsMB+MaUnttIBPh3iHiaJ01g=="], + + "@mistralai/mistralai": ["@mistralai/mistralai@1.10.0", "", { "dependencies": { "zod": "^3.20.0", "zod-to-json-schema": "^3.24.1" } }, "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg=="], + + "@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="], + + "@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="], + + "@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="], + + "@protobufjs/codegen": ["@protobufjs/codegen@2.0.4", "", {}, "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg=="], + + "@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.0", "", {}, "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q=="], + + "@protobufjs/fetch": ["@protobufjs/fetch@1.1.0", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1", "@protobufjs/inquire": "^1.1.0" } }, "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ=="], + + "@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="], + + "@protobufjs/inquire": ["@protobufjs/inquire@1.1.0", "", {}, "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q=="], + + "@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="], + + "@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="], + + "@protobufjs/utf8": ["@protobufjs/utf8@1.1.0", "", {}, "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw=="], + + "@silvia-odwyer/photon-node": ["@silvia-odwyer/photon-node@0.3.4", "", {}, "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA=="], + + "@sinclair/typebox": ["@sinclair/typebox@0.34.48", "", {}, "sha512-kKJTNuK3AQOrgjjotVxMrCn1sUJwM76wMszfq1kdU4uYVJjvEWuFQ6HgvLt4Xz3fSmZlTOxJ/Ie13KnIcWQXFA=="], + + "@smithy/abort-controller": ["@smithy/abort-controller@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-peuVfkYHAmS5ybKxWcfraK7WBBP0J+rkfUcbHJJKQ4ir3UAUNQI+Y4Vt/PqSzGqgloJ5O1dk7+WzNL8wcCSXbw=="], + + "@smithy/config-resolver": ["@smithy/config-resolver@4.4.6", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "@smithy/util-config-provider": "^4.2.0", "@smithy/util-endpoints": "^3.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-qJpzYC64kaj3S0fueiu3kXm8xPrR3PcXDPEgnaNMRn0EjNSZFoFjvbUp0YUDsRhN1CB90EnHJtbxWKevnH99UQ=="], + + "@smithy/core": ["@smithy/core@3.23.0", "", { "dependencies": { "@smithy/middleware-serde": "^4.2.9", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-body-length-browser": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-stream": "^4.5.12", "@smithy/util-utf8": "^4.2.0", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-Yq4UPVoQICM9zHnByLmG8632t2M0+yap4T7ANVw482J0W7HW0pOuxwVmeOwzJqX2Q89fkXz0Vybz55Wj2Xzrsg=="], + + "@smithy/credential-provider-imds": ["@smithy/credential-provider-imds@4.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FNT0xHS1c/CPN8upqbMFP83+ul5YgdisfCfkZ86Jh2NSmnqw/AJ6x5pEogVCTVvSm7j9MopRU89bmDelxuDMYw=="], + + "@smithy/eventstream-codec": ["@smithy/eventstream-codec@4.2.8", "", { "dependencies": { "@aws-crypto/crc32": "5.2.0", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-jS/O5Q14UsufqoGhov7dHLOPCzkYJl9QDzusI2Psh4wyYx/izhzvX9P4D69aTxcdfVhEPhjK+wYyn/PzLjKbbw=="], + + "@smithy/eventstream-serde-browser": ["@smithy/eventstream-serde-browser@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-MTfQT/CRQz5g24ayXdjg53V0mhucZth4PESoA5IhvaWVDTOQLfo8qI9vzqHcPsdd2v6sqfTYqF5L/l+pea5Uyw=="], + + "@smithy/eventstream-serde-config-resolver": ["@smithy/eventstream-serde-config-resolver@4.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-ah12+luBiDGzBruhu3efNy1IlbwSEdNiw8fOZksoKoWW1ZHvO/04MQsdnws/9Aj+5b0YXSSN2JXKy/ClIsW8MQ=="], + + "@smithy/eventstream-serde-node": ["@smithy/eventstream-serde-node@4.2.8", "", { "dependencies": { "@smithy/eventstream-serde-universal": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cYpCpp29z6EJHa5T9WL0KAlq3SOKUQkcgSoeRfRVwjGgSFl7Uh32eYGt7IDYCX20skiEdRffyDpvF2efEZPC0A=="], + + "@smithy/eventstream-serde-universal": ["@smithy/eventstream-serde-universal@4.2.8", "", { "dependencies": { "@smithy/eventstream-codec": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-iJ6YNJd0bntJYnX6s52NC4WFYcZeKrPUr1Kmmr5AwZcwCSzVpS7oavAmxMR7pMq7V+D1G4s9F5NJK0xwOsKAlQ=="], + + "@smithy/fetch-http-handler": ["@smithy/fetch-http-handler@5.3.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "tslib": "^2.6.2" } }, "sha512-I4UhmcTYXBrct03rwzQX1Y/iqQlzVQaPxWjCjula++5EmWq9YGBrx6bbGqluGc1f0XEfhSkiY4jhLgbsJUMKRA=="], + + "@smithy/hash-node": ["@smithy/hash-node@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-7ZIlPbmaDGxVoxErDZnuFG18WekhbA/g2/i97wGj+wUBeS6pcUeAym8u4BXh/75RXWhgIJhyC11hBzig6MljwA=="], + + "@smithy/invalid-dependency": ["@smithy/invalid-dependency@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-N9iozRybwAQ2dn9Fot9kI6/w9vos2oTXLhtK7ovGqwZjlOcxu6XhPlpLpC+INsxktqHinn5gS2DXDjDF2kG5sQ=="], + + "@smithy/is-array-buffer": ["@smithy/is-array-buffer@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-DZZZBvC7sjcYh4MazJSGiWMI2L7E0oCiRHREDzIxi/M2LY79/21iXt6aPLHge82wi5LsuRF5A06Ds3+0mlh6CQ=="], + + "@smithy/middleware-content-length": ["@smithy/middleware-content-length@4.2.8", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-RO0jeoaYAB1qBRhfVyq0pMgBoUK34YEJxVxyjOWYZiOKOq2yMZ4MnVXMZCUDenpozHue207+9P5ilTV1zeda0A=="], + + "@smithy/middleware-endpoint": ["@smithy/middleware-endpoint@4.4.14", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-serde": "^4.2.9", "@smithy/node-config-provider": "^4.3.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "@smithy/url-parser": "^4.2.8", "@smithy/util-middleware": "^4.2.8", "tslib": "^2.6.2" } }, "sha512-FUFNE5KVeaY6U/GL0nzAAHkaCHzXLZcY1EhtQnsAqhD8Du13oPKtMB9/0WK4/LK6a/T5OZ24wPoSShff5iI6Ag=="], + + "@smithy/middleware-retry": ["@smithy/middleware-retry@4.4.31", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/protocol-http": "^5.3.8", "@smithy/service-error-classification": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-retry": "^4.2.8", "@smithy/uuid": "^1.1.0", "tslib": "^2.6.2" } }, "sha512-RXBzLpMkIrxBPe4C8OmEOHvS8aH9RUuCOH++Acb5jZDEblxDjyg6un72X9IcbrGTJoiUwmI7hLypNfuDACypbg=="], + + "@smithy/middleware-serde": ["@smithy/middleware-serde@4.2.9", "", { "dependencies": { "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-eMNiej0u/snzDvlqRGSN3Vl0ESn3838+nKyVfF2FKNXFbi4SERYT6PR392D39iczngbqqGG0Jl1DlCnp7tBbXQ=="], + + "@smithy/middleware-stack": ["@smithy/middleware-stack@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-w6LCfOviTYQjBctOKSwy6A8FIkQy7ICvglrZFl6Bw4FmcQ1Z420fUtIhxaUZZshRe0VCq4kvDiPiXrPZAe8oRA=="], + + "@smithy/node-config-provider": ["@smithy/node-config-provider@4.3.8", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/shared-ini-file-loader": "^4.4.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-aFP1ai4lrbVlWjfpAfRSL8KFcnJQYfTl5QxLJXY32vghJrDuFyPZ6LtUL+JEGYiFRG1PfPLHLoxj107ulncLIg=="], + + "@smithy/node-http-handler": ["@smithy/node-http-handler@4.4.10", "", { "dependencies": { "@smithy/abort-controller": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/querystring-builder": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-u4YeUwOWRZaHbWaebvrs3UhwQwj+2VNmcVCwXcYTvPIuVyM7Ex1ftAj+fdbG/P4AkBwLq/+SKn+ydOI4ZJE9PA=="], + + "@smithy/property-provider": ["@smithy/property-provider@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-EtCTbyIveCKeOXDSWSdze3k612yCPq1YbXsbqX3UHhkOSW8zKsM9NOJG5gTIya0vbY2DIaieG8pKo1rITHYL0w=="], + + "@smithy/protocol-http": ["@smithy/protocol-http@5.3.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-QNINVDhxpZ5QnP3aviNHQFlRogQZDfYlCkQT+7tJnErPQbDhysondEjhikuANxgMsZrkGeiAxXy4jguEGsDrWQ=="], + + "@smithy/querystring-builder": ["@smithy/querystring-builder@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "@smithy/util-uri-escape": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-Xr83r31+DrE8CP3MqPgMJl+pQlLLmOfiEUnoyAlGzzJIrEsbKsPy1hqH0qySaQm4oWrCBlUqRt+idEgunKB+iw=="], + + "@smithy/querystring-parser": ["@smithy/querystring-parser@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-vUurovluVy50CUlazOiXkPq40KGvGWSdmusa3130MwrR1UNnNgKAlj58wlOe61XSHRpUfIIh6cE0zZ8mzKaDPA=="], + + "@smithy/service-error-classification": ["@smithy/service-error-classification@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0" } }, "sha512-mZ5xddodpJhEt3RkCjbmUQuXUOaPNTkbMGR0bcS8FE0bJDLMZlhmpgrvPNCYglVw5rsYTpSnv19womw9WWXKQQ=="], + + "@smithy/shared-ini-file-loader": ["@smithy/shared-ini-file-loader@4.4.3", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-DfQjxXQnzC5UbCUPeC3Ie8u+rIWZTvuDPAGU/BxzrOGhRvgUanaP68kDZA+jaT3ZI+djOf+4dERGlm9mWfFDrg=="], + + "@smithy/signature-v4": ["@smithy/signature-v4@5.3.8", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-middleware": "^4.2.8", "@smithy/util-uri-escape": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-6A4vdGj7qKNRF16UIcO8HhHjKW27thsxYci+5r/uVRkdcBEkOEiY8OMPuydLX4QHSrJqGHPJzPRwwVTqbLZJhg=="], + + "@smithy/smithy-client": ["@smithy/smithy-client@4.11.3", "", { "dependencies": { "@smithy/core": "^3.23.0", "@smithy/middleware-endpoint": "^4.4.14", "@smithy/middleware-stack": "^4.2.8", "@smithy/protocol-http": "^5.3.8", "@smithy/types": "^4.12.0", "@smithy/util-stream": "^4.5.12", "tslib": "^2.6.2" } }, "sha512-Q7kY5sDau8OoE6Y9zJoRGgje8P4/UY0WzH8R2ok0PDh+iJ+ZnEKowhjEqYafVcubkbYxQVaqwm3iufktzhprGg=="], + + "@smithy/types": ["@smithy/types@4.12.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-9YcuJVTOBDjg9LWo23Qp0lTQ3D7fQsQtwle0jVfpbUHy9qBwCEgKuVH4FqFB3VYu0nwdHKiEMA+oXz7oV8X1kw=="], + + "@smithy/url-parser": ["@smithy/url-parser@4.2.8", "", { "dependencies": { "@smithy/querystring-parser": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-NQho9U68TGMEU639YkXnVMV3GEFFULmmaWdlu1E9qzyIePOHsoSnagTGSDv1Zi8DCNN6btxOSdgmy5E/hsZwhA=="], + + "@smithy/util-base64": ["@smithy/util-base64@4.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-GkXZ59JfyxsIwNTWFnjmFEI8kZpRNIBfxKjv09+nkAWPt/4aGaEWMM04m4sxgNVWkbt2MdSvE3KF/PfX4nFedQ=="], + + "@smithy/util-body-length-browser": ["@smithy/util-body-length-browser@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-Fkoh/I76szMKJnBXWPdFkQJl2r9SjPt3cMzLdOB6eJ4Pnpas8hVoWPYemX/peO0yrrvldgCUVJqOAjUrOLjbxg=="], + + "@smithy/util-body-length-node": ["@smithy/util-body-length-node@4.2.1", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-h53dz/pISVrVrfxV1iqXlx5pRg3V2YWFcSQyPyXZRrZoZj4R4DeWRDo1a7dd3CPTcFi3kE+98tuNyD2axyZReA=="], + + "@smithy/util-buffer-from": ["@smithy/util-buffer-from@4.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-kAY9hTKulTNevM2nlRtxAG2FQ3B2OR6QIrPY3zE5LqJy1oxzmgBGsHLWTcNhWXKchgA0WHW+mZkQrng/pgcCew=="], + + "@smithy/util-config-provider": ["@smithy/util-config-provider@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-YEjpl6XJ36FTKmD+kRJJWYvrHeUvm5ykaUS5xK+6oXffQPHeEM4/nXlZPe+Wu0lsgRUcNZiliYNh/y7q9c2y6Q=="], + + "@smithy/util-defaults-mode-browser": ["@smithy/util-defaults-mode-browser@4.3.30", "", { "dependencies": { "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-cMni0uVU27zxOiU8TuC8pQLC1pYeZ/xEMxvchSK/ILwleRd1ugobOcIRr5vXtcRqKd4aBLWlpeBoDPJJ91LQng=="], + + "@smithy/util-defaults-mode-node": ["@smithy/util-defaults-mode-node@4.2.33", "", { "dependencies": { "@smithy/config-resolver": "^4.4.6", "@smithy/credential-provider-imds": "^4.2.8", "@smithy/node-config-provider": "^4.3.8", "@smithy/property-provider": "^4.2.8", "@smithy/smithy-client": "^4.11.3", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-LEb2aq5F4oZUSzWBG7S53d4UytZSkOEJPXcBq/xbG2/TmK9EW5naUZ8lKu1BEyWMzdHIzEVN16M3k8oxDq+DJA=="], + + "@smithy/util-endpoints": ["@smithy/util-endpoints@3.2.8", "", { "dependencies": { "@smithy/node-config-provider": "^4.3.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-8JaVTn3pBDkhZgHQ8R0epwWt+BqPSLCjdjXXusK1onwJlRuN69fbvSK66aIKKO7SwVFM6x2J2ox5X8pOaWcUEw=="], + + "@smithy/util-hex-encoding": ["@smithy/util-hex-encoding@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-CCQBwJIvXMLKxVbO88IukazJD9a4kQ9ZN7/UMGBjBcJYvatpWk+9g870El4cB8/EJxfe+k+y0GmR9CAzkF+Nbw=="], + + "@smithy/util-middleware": ["@smithy/util-middleware@4.2.8", "", { "dependencies": { "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-PMqfeJxLcNPMDgvPbbLl/2Vpin+luxqTGPpW3NAQVLbRrFRzTa4rNAASYeIGjRV9Ytuhzny39SpyU04EQreF+A=="], + + "@smithy/util-retry": ["@smithy/util-retry@4.2.8", "", { "dependencies": { "@smithy/service-error-classification": "^4.2.8", "@smithy/types": "^4.12.0", "tslib": "^2.6.2" } }, "sha512-CfJqwvoRY0kTGe5AkQokpURNCT1u/MkRzMTASWMPPo2hNSnKtF1D45dQl3DE2LKLr4m+PW9mCeBMJr5mCAVThg=="], + + "@smithy/util-stream": ["@smithy/util-stream@4.5.12", "", { "dependencies": { "@smithy/fetch-http-handler": "^5.3.9", "@smithy/node-http-handler": "^4.4.10", "@smithy/types": "^4.12.0", "@smithy/util-base64": "^4.3.0", "@smithy/util-buffer-from": "^4.2.0", "@smithy/util-hex-encoding": "^4.2.0", "@smithy/util-utf8": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-D8tgkrmhAX/UNeCZbqbEO3uqyghUnEmmoO9YEvRuwxjlkKKUE7FOgCJnqpTlQPe9MApdWPky58mNQQHbnCzoNg=="], + + "@smithy/util-uri-escape": ["@smithy/util-uri-escape@4.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-igZpCKV9+E/Mzrpq6YacdTQ0qTiLm85gD6N/IrmyDvQFA4UnU3d5g3m8tMT/6zG/vVkWSU+VxeUyGonL62DuxA=="], + + "@smithy/util-utf8": ["@smithy/util-utf8@4.2.0", "", { "dependencies": { "@smithy/util-buffer-from": "^4.2.0", "tslib": "^2.6.2" } }, "sha512-zBPfuzoI8xyBtR2P6WQj63Rz8i3AmfAaJLuNG8dWsfvPe8lO4aCPYLn879mEgHndZH1zQ2oXmG8O1GGzzaoZiw=="], + + "@smithy/uuid": ["@smithy/uuid@1.1.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-4aUIteuyxtBUhVdiQqcDhKFitwfd9hqoSDYY2KRXiWtgoWJ9Bmise+KfEPDiVHWeJepvF8xJO9/9+WDIciMFFw=="], + + "@tokenizer/inflate": ["@tokenizer/inflate@0.4.1", "", { "dependencies": { "debug": "^4.4.3", "token-types": "^6.1.1" } }, "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA=="], + + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + + "@tootallnate/quickjs-emscripten": ["@tootallnate/quickjs-emscripten@0.23.0", "", {}, "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA=="], + + "@types/mime-types": ["@types/mime-types@2.1.4", "", {}, "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w=="], + + "@types/node": ["@types/node@25.2.3", "", { "dependencies": { "undici-types": "~7.16.0" } }, "sha512-m0jEgYlYz+mDJZ2+F4v8D1AyQb+QzsNqRuI7xg1VQX/KlKS0qT9r1Mo16yo5F/MtifXFgaofIFsdFMox2SxIbQ=="], + + "agent-base": ["agent-base@7.1.4", "", {}, "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ=="], + + "ajv": ["ajv@8.18.0", "", { "dependencies": { "fast-deep-equal": "^3.1.3", "fast-uri": "^3.0.1", "json-schema-traverse": "^1.0.0", "require-from-string": "^2.0.2" } }, "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A=="], + + "ajv-formats": ["ajv-formats@3.0.1", "", { "dependencies": { "ajv": "^8.0.0" } }, "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ=="], + + "ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="], + + "ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="], + + "any-promise": ["any-promise@1.3.0", "", {}, "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A=="], + + "ast-types": ["ast-types@0.13.4", "", { "dependencies": { "tslib": "^2.0.1" } }, "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w=="], + + "balanced-match": ["balanced-match@4.0.2", "", { "dependencies": { "jackspeak": "^4.2.3" } }, "sha512-x0K50QvKQ97fdEz2kPehIerj+YTeptKF9hyYkKf6egnwmMWAkADiO0QCzSp0R5xN8FTZgYaBfSaue46Ej62nMg=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "basic-ftp": ["basic-ftp@5.1.0", "", {}, "sha512-RkaJzeJKDbaDWTIPiJwubyljaEPwpVWkm9Rt5h9Nd6h7tEXTJ3VB4qxdZBioV7JO5yLUaOKwz7vDOzlncUsegw=="], + + "bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="], + + "bowser": ["bowser@2.14.1", "", {}, "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg=="], + + "brace-expansion": ["brace-expansion@5.0.2", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-Pdk8c9poy+YhOgVWw1JNN22/HcivgKWwpxKq04M/jTmHyCZn12WPJebZxdjSa5TmBqISrUSgNYU3eRORljfCCw=="], + + "buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="], + + "bun-types": ["bun-types@1.3.9", "", { "dependencies": { "@types/node": "*" } }, "sha512-+UBWWOakIP4Tswh0Bt0QD0alpTY8cb5hvgiYeWCMet9YukHbzuruIEeXC2D7nMJPB12kbh8C7XJykSexEqGKJg=="], + + "chalk": ["chalk@5.6.2", "", {}, "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA=="], + + "cli-highlight": ["cli-highlight@2.1.11", "", { "dependencies": { "chalk": "^4.0.0", "highlight.js": "^10.7.1", "mz": "^2.4.0", "parse5": "^5.1.1", "parse5-htmlparser2-tree-adapter": "^6.0.0", "yargs": "^16.0.0" }, "bin": { "highlight": "bin/highlight" } }, "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg=="], + + "cliui": ["cliui@7.0.4", "", { "dependencies": { "string-width": "^4.2.0", "strip-ansi": "^6.0.0", "wrap-ansi": "^7.0.0" } }, "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ=="], + + "color-convert": ["color-convert@2.0.1", "", { "dependencies": { "color-name": "~1.1.4" } }, "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ=="], + + "color-name": ["color-name@1.1.4", "", {}, "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="], + + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + + "data-uri-to-buffer": ["data-uri-to-buffer@6.0.2", "", {}, "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + + "degenerator": ["degenerator@5.0.1", "", { "dependencies": { "ast-types": "^0.13.4", "escodegen": "^2.1.0", "esprima": "^4.0.1" } }, "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ=="], + + "diff": ["diff@8.0.3", "", {}, "sha512-qejHi7bcSD4hQAZE0tNAawRK1ZtafHDmMTMkrrIGgSLl7hTnQHmKCeB45xAcbfTqK2zowkM3j3bHt/4b/ARbYQ=="], + + "eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="], + + "ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="], + + "emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "escodegen": ["escodegen@2.1.0", "", { "dependencies": { "esprima": "^4.0.1", "estraverse": "^5.2.0", "esutils": "^2.0.2" }, "optionalDependencies": { "source-map": "~0.6.1" }, "bin": { "esgenerate": "bin/esgenerate.js", "escodegen": "bin/escodegen.js" } }, "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w=="], + + "esprima": ["esprima@4.0.1", "", { "bin": { "esparse": "./bin/esparse.js", "esvalidate": "./bin/esvalidate.js" } }, "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A=="], + + "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], + + "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], + + "extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="], + + "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], + + "fast-uri": ["fast-uri@3.1.0", "", {}, "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA=="], + + "fast-xml-parser": ["fast-xml-parser@5.3.4", "", { "dependencies": { "strnum": "^2.1.0" }, "bin": { "fxparser": "src/cli/cli.js" } }, "sha512-EFd6afGmXlCx8H8WTZHhAoDaWaGyuIBoZJ2mknrNxug+aZKjkp0a0dlars9Izl+jF+7Gu1/5f/2h68cQpe0IiA=="], + + "fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="], + + "file-type": ["file-type@21.3.0", "", { "dependencies": { "@tokenizer/inflate": "^0.4.1", "strtok3": "^10.3.4", "token-types": "^6.1.1", "uint8array-extras": "^1.4.0" } }, "sha512-8kPJMIGz1Yt/aPEwOsrR97ZyZaD1Iqm8PClb1nYFclUCkBi0Ma5IsYNQzvSFS9ib51lWyIw5mIT9rWzI/xjpzA=="], + + "foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="], + + "formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="], + + "gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="], + + "gcp-metadata": ["gcp-metadata@8.1.2", "", { "dependencies": { "gaxios": "^7.0.0", "google-logging-utils": "^1.0.0", "json-bigint": "^1.0.0" } }, "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg=="], + + "get-caller-file": ["get-caller-file@2.0.5", "", {}, "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg=="], + + "get-east-asian-width": ["get-east-asian-width@1.4.0", "", {}, "sha512-QZjmEOC+IT1uk6Rx0sX22V6uHWVwbdbxf1faPqJ1QhLdGgsRGCZoyaQBm/piRdJy/D2um6hM1UP7ZEeQ4EkP+Q=="], + + "get-uri": ["get-uri@6.0.5", "", { "dependencies": { "basic-ftp": "^5.0.2", "data-uri-to-buffer": "^6.0.2", "debug": "^4.3.4" } }, "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg=="], + + "glob": ["glob@13.0.3", "", { "dependencies": { "minimatch": "^10.2.0", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-/g3B0mC+4x724v1TgtBlBtt2hPi/EWptsIAmXUx9Z2rvBYleQcsrmaOzd5LyL50jf/Soi83ZDJmw2+XqvH/EeA=="], + + "google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="], + + "google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="], + + "graceful-fs": ["graceful-fs@4.2.11", "", {}, "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ=="], + + "gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="], + + "has-flag": ["has-flag@4.0.0", "", {}, "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ=="], + + "highlight.js": ["highlight.js@10.7.3", "", {}, "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="], + + "hosted-git-info": ["hosted-git-info@9.0.2", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg=="], + + "http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="], + + "https-proxy-agent": ["https-proxy-agent@7.0.6", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "4" } }, "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "ignore": ["ignore@7.0.5", "", {}, "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg=="], + + "ip-address": ["ip-address@10.1.0", "", {}, "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q=="], + + "is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + + "is-network-error": ["is-network-error@1.3.0", "", {}, "sha512-6oIwpsgRfnDiyEDLMay/GqCl3HoAtH5+RUKW29gYkL0QA+ipzpDLA16yQs7/RHCSu+BwgbJaOUqa4A99qNVQVw=="], + + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + + "jackspeak": ["jackspeak@4.2.3", "", { "dependencies": { "@isaacs/cliui": "^9.0.0" } }, "sha512-ykkVRwrYvFm1nb2AJfKKYPr0emF6IiXDYUaFx4Zn9ZuIH7MrzEZ3sD5RlqGXNRpHtvUHJyOnCEFxOlNDtGo7wg=="], + + "json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="], + + "json-schema-to-ts": ["json-schema-to-ts@3.1.1", "", { "dependencies": { "@babel/runtime": "^7.18.3", "ts-algebra": "^2.0.0" } }, "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g=="], + + "json-schema-traverse": ["json-schema-traverse@1.0.0", "", {}, "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug=="], + + "jwa": ["jwa@2.0.1", "", { "dependencies": { "buffer-equal-constant-time": "^1.0.1", "ecdsa-sig-formatter": "1.0.11", "safe-buffer": "^5.0.1" } }, "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg=="], + + "jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="], + + "long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="], + + "lru-cache": ["lru-cache@7.18.3", "", {}, "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA=="], + + "marked": ["marked@15.0.12", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA=="], + + "mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="], + + "mime-types": ["mime-types@3.0.2", "", { "dependencies": { "mime-db": "^1.54.0" } }, "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A=="], + + "minimatch": ["minimatch@10.2.0", "", { "dependencies": { "brace-expansion": "^5.0.2" } }, "sha512-ugkC31VaVg9cF0DFVoADH12k6061zNZkZON+aX8AWsR9GhPcErkcMBceb6znR8wLERM2AkkOxy2nWRLpT9Jq5w=="], + + "minipass": ["minipass@7.1.2", "", {}, "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + + "mz": ["mz@2.7.0", "", { "dependencies": { "any-promise": "^1.0.0", "object-assign": "^4.0.1", "thenify-all": "^1.0.0" } }, "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q=="], + + "netmask": ["netmask@2.0.2", "", {}, "sha512-dBpDMdxv9Irdq66304OLfEmQ9tbNRFnFTuZiLo+bD+r332bBmMJ8GBLXklIXXgxd3+v9+KUnZaUR5PJMa75Gsg=="], + + "node-domexception": ["node-domexception@1.0.0", "", {}, "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ=="], + + "node-fetch": ["node-fetch@3.3.2", "", { "dependencies": { "data-uri-to-buffer": "^4.0.0", "fetch-blob": "^3.1.4", "formdata-polyfill": "^4.0.10" } }, "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA=="], + + "object-assign": ["object-assign@4.1.1", "", {}, "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg=="], + + "openai": ["openai@6.10.0", "", { "peerDependencies": { "ws": "^8.18.0", "zod": "^3.25 || ^4.0" }, "optionalPeers": ["ws", "zod"], "bin": { "openai": "bin/cli" } }, "sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A=="], + + "p-retry": ["p-retry@7.1.1", "", { "dependencies": { "is-network-error": "^1.1.0" } }, "sha512-J5ApzjyRkkf601HpEeykoiCvzHQjWxPAHhyjFcEUP2SWq0+35NKh8TLhpLw+Dkq5TZBFvUM6UigdE9hIVYTl5w=="], + + "pac-proxy-agent": ["pac-proxy-agent@7.2.0", "", { "dependencies": { "@tootallnate/quickjs-emscripten": "^0.23.0", "agent-base": "^7.1.2", "debug": "^4.3.4", "get-uri": "^6.0.1", "http-proxy-agent": "^7.0.0", "https-proxy-agent": "^7.0.6", "pac-resolver": "^7.0.1", "socks-proxy-agent": "^8.0.5" } }, "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA=="], + + "pac-resolver": ["pac-resolver@7.0.1", "", { "dependencies": { "degenerator": "^5.0.0", "netmask": "^2.0.2" } }, "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg=="], + + "package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="], + + "parse5": ["parse5@5.1.1", "", {}, "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug=="], + + "parse5-htmlparser2-tree-adapter": ["parse5-htmlparser2-tree-adapter@6.0.1", "", { "dependencies": { "parse5": "^6.0.1" } }, "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA=="], + + "partial-json": ["partial-json@0.1.7", "", {}, "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA=="], + + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + + "path-scurry": ["path-scurry@2.0.1", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-oWyT4gICAu+kaA7QWk/jvCHWarMKNs6pXOGWKDTr7cw4IGcUbW+PeTfbaQiLGheFRpjo6O9J0PmyMfQPjH71oA=="], + + "proper-lockfile": ["proper-lockfile@4.1.2", "", { "dependencies": { "graceful-fs": "^4.2.4", "retry": "^0.12.0", "signal-exit": "^3.0.2" } }, "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA=="], + + "protobufjs": ["protobufjs@7.5.4", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.4", "@protobufjs/eventemitter": "^1.1.0", "@protobufjs/fetch": "^1.1.0", "@protobufjs/float": "^1.0.2", "@protobufjs/inquire": "^1.1.0", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.0", "@types/node": ">=13.7.0", "long": "^5.0.0" } }, "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg=="], + + "proxy-agent": ["proxy-agent@6.5.0", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "http-proxy-agent": "^7.0.1", "https-proxy-agent": "^7.0.6", "lru-cache": "^7.14.1", "pac-proxy-agent": "^7.1.0", "proxy-from-env": "^1.1.0", "socks-proxy-agent": "^8.0.5" } }, "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A=="], + + "proxy-from-env": ["proxy-from-env@1.1.0", "", {}, "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg=="], + + "require-directory": ["require-directory@2.1.1", "", {}, "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q=="], + + "require-from-string": ["require-from-string@2.0.2", "", {}, "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw=="], + + "retry": ["retry@0.12.0", "", {}, "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow=="], + + "rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], + + "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + + "signal-exit": ["signal-exit@3.0.7", "", {}, "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ=="], + + "smart-buffer": ["smart-buffer@4.2.0", "", {}, "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg=="], + + "socks": ["socks@2.8.7", "", { "dependencies": { "ip-address": "^10.0.1", "smart-buffer": "^4.2.0" } }, "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A=="], + + "socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="], + + "source-map": ["source-map@0.6.1", "", {}, "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g=="], + + "std-env": ["std-env@3.10.0", "", {}, "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg=="], + + "string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="], + + "strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="], + + "strnum": ["strnum@2.1.2", "", {}, "sha512-l63NF9y/cLROq/yqKXSLtcMeeyOfnSQlfMSlzFt/K73oIaD8DGaQWd7Z34X9GPiKqP5rbSh84Hl4bOlLcjiSrQ=="], + + "strtok3": ["strtok3@10.3.4", "", { "dependencies": { "@tokenizer/token": "^0.3.0" } }, "sha512-KIy5nylvC5le1OdaaoCJ07L+8iQzJHGH6pWDuzS+d07Cu7n1MZ2x26P8ZKIWfbK02+XIL8Mp4RkWeqdUCrDMfg=="], + + "supports-color": ["supports-color@7.2.0", "", { "dependencies": { "has-flag": "^4.0.0" } }, "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw=="], + + "thenify": ["thenify@3.3.1", "", { "dependencies": { "any-promise": "^1.0.0" } }, "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw=="], + + "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], + + "token-types": ["token-types@6.1.2", "", { "dependencies": { "@borewit/text-codec": "^0.2.1", "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww=="], + + "ts-algebra": ["ts-algebra@2.0.0", "", {}, "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw=="], + + "tslib": ["tslib@2.8.1", "", {}, "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w=="], + + "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], + + "uint8array-extras": ["uint8array-extras@1.5.0", "", {}, "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A=="], + + "undici": ["undici@7.22.0", "", {}, "sha512-RqslV2Us5BrllB+JeiZnK4peryVTndy9Dnqq62S3yYRRTj0tFQCwEniUy2167skdGOy3vqRzEvl1Dm4sV2ReDg=="], + + "undici-types": ["undici-types@7.16.0", "", {}, "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw=="], + + "web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="], + + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + + "wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "wrap-ansi-cjs": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + + "ws": ["ws@8.19.0", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg=="], + + "y18n": ["y18n@5.0.8", "", {}, "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA=="], + + "yaml": ["yaml@2.8.2", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-mplynKqc1C2hTVYxd0PU2xQAc22TI1vShAYGksCCfxbn/dFwnHTNi1bvYsBTkhdUNtGIf5xNOg938rrSSYvS9A=="], + + "yargs": ["yargs@16.2.0", "", { "dependencies": { "cliui": "^7.0.2", "escalade": "^3.1.1", "get-caller-file": "^2.0.5", "require-directory": "^2.1.1", "string-width": "^4.2.0", "y18n": "^5.0.5", "yargs-parser": "^20.2.2" } }, "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw=="], + + "yargs-parser": ["yargs-parser@20.2.9", "", {}, "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w=="], + + "yoctocolors": ["yoctocolors@2.1.2", "", {}, "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug=="], + + "zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "zod-to-json-schema": ["zod-to-json-schema@3.25.1", "", { "peerDependencies": { "zod": "^3.25 || ^4" } }, "sha512-pM/SU9d3YAggzi6MtR4h7ruuQlqKtad8e9S0fmxcMi+ueAK5Korys/aWcV9LIIHTVbj01NdzxcnXSN+O74ZIVA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="], + + "cli-highlight/chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], + + "foreground-child/signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], + + "hosted-git-info/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + + "node-fetch/data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="], + + "parse5-htmlparser2-tree-adapter/parse5": ["parse5@6.0.1", "", {}, "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw=="], + + "path-scurry/lru-cache": ["lru-cache@11.2.6", "", {}, "sha512-ESL2CrkS/2wTPfuend7Zhkzo2u0daGJ/A2VucJOgQ/C48S/zB8MMeMHSGKYpXhIjbPxfuezITkaBH1wqv00DDQ=="], + + "rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="], + + "rimraf/glob/jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="], + + "rimraf/glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], + + "rimraf/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "@aws-crypto/sha256-browser/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from/@smithy/is-array-buffer": ["@smithy/is-array-buffer@2.2.0", "", { "dependencies": { "tslib": "^2.6.2" } }, "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA=="], + + "rimraf/glob/jackspeak/@isaacs/cliui": ["@isaacs/cliui@8.0.2", "", { "dependencies": { "string-width": "^5.1.2", "string-width-cjs": "npm:string-width@^4.2.0", "strip-ansi": "^7.0.1", "strip-ansi-cjs": "npm:strip-ansi@^6.0.1", "wrap-ansi": "^8.1.0", "wrap-ansi-cjs": "npm:wrap-ansi@^7.0.0" } }, "sha512-O8jcjabXaleOG9DQ0+ARXWZBTfnP4WNAqzuiJK7ll44AmxGKv/J2M4TPjxjY3znBCfvBXFzucm1twdyFybFqEA=="], + + "rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.0.2", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ=="], + + "rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="], + + "rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/strip-ansi/ansi-regex": ["ansi-regex@6.2.2", "", {}, "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg=="], + + "rimraf/glob/jackspeak/@isaacs/cliui/wrap-ansi/ansi-styles": ["ansi-styles@6.2.3", "", {}, "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..92b91b3 --- /dev/null +++ b/package.json @@ -0,0 +1,26 @@ +{ + "name": "pi-perplexity", + "version": "0.1.0", + "type": "module", + "main": "src/index.ts", + "pi": { + "extensions": ["./src/index.ts"] + }, + "peerDependencies": { + "@mariozechner/pi-tui": "*", + "@mariozechner/pi-ai": "*", + "@sinclair/typebox": "*" + }, + "devDependencies": { + "@mariozechner/pi-coding-agent": "*", + "@mariozechner/pi-tui": "*", + "@mariozechner/pi-ai": "*", + "@sinclair/typebox": "*", + "bun-types": "*", + "typescript": "*" + }, + "scripts": { + "typecheck": "tsc --noEmit", + "test": "bun test" + } +} diff --git a/plan.md b/plan.md new file mode 100644 index 0000000..a77f91e --- /dev/null +++ b/plan.md @@ -0,0 +1,385 @@ +# pi-perplexity Implementation Plan + +## Prerequisites + +- Bun installed +- oh-my-pi installed (for testing the plugin) +- Perplexity macOS app installed and logged in (for Path 1 auth), OR a Perplexity account email (for Path 2 OTP auth) + +--- + +## Phase 1: Project Scaffold + +### 1.1 Initialize package + +``` +pi-perplexity/ + package.json + tsconfig.json + src/ + index.ts +``` + +**package.json** — must include `omp` (or `pi`) manifest field: +```json +{ + "name": "pi-perplexity", + "version": "0.1.0", + "type": "module", + "main": "src/index.ts", + "omp": { + "name": "Perplexity Search", + "description": "Web search via Perplexity Pro/Max subscription (OAuth)", + "tools": "src/index.ts", + "settings": { + "email": { + "type": "string", + "description": "Perplexity account email (for OTP login)", + "env": "PERPLEXITY_EMAIL" + } + } + }, + "peerDependencies": { + "@oh-my-pi/pi-tui": "*" + }, + "devDependencies": { + "@oh-my-pi/pi-tui": "workspace:*", + "@oh-my-pi/pi-agent-core": "workspace:*", + "@sinclair/typebox": "*", + "typescript": "*" + } +} +``` + +**tsconfig.json** — extend from oh-my-pi root or standalone: +```json +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "declaration": true, + "outDir": "dist", + "rootDir": "src" + }, + "include": ["src"] +} +``` + +**src/index.ts** — skeleton factory: +```typescript +import type { CustomToolFactory } from "@oh-my-pi/pi-coding-agent/extensibility"; + +const factory: CustomToolFactory = (api) => { + return { + name: "perplexity_search", + label: "Perplexity Search", + description: "...", + parameters: api.typebox.Type.Object({ + query: api.typebox.Type.String({ description: "Search query" }), + }), + async execute(toolCallId, params, onUpdate, ctx, signal) { + return { content: [{ type: "text", text: "TODO" }] }; + }, + }; +}; + +export default factory; +``` + +### Acceptance +- `bun run --bun src/index.ts` doesn't crash +- Factory returns a valid CustomTool shape + +--- + +## Phase 2: Auth — JWT Acquisition and Storage + +### 2.1 JWT utilities (`src/auth/jwt.ts`) + +Implement: +- `decodeJwtExpiry(token: string): number` — base64url decode payload, extract `exp` claim, return ms with 5-min margin. Fallback: now + 1 hour. +- `isJwtExpired(token: string, bufferMs?: number): boolean` + +No dependencies. Use `atob` or `Buffer.from(payload, "base64url")`. + +### 2.2 Token storage (`src/auth/storage.ts`) + +Implement: +- `loadToken(): Promise` — read from `~/.config/pi-perplexity/auth.json` +- `saveToken(token: StoredToken): Promise` — write with `0600` permissions +- `clearToken(): Promise` — delete file + +```typescript +interface StoredToken { + jwt: string; + expires: number; + email?: string; + acquiredAt: number; +} +``` + +Use `Bun.write()` and `Bun.file()`. Handle ENOENT on read (no stored token). + +### 2.3 Login flow (`src/auth/login.ts`) + +Implement: +- `extractFromDesktopApp(): Promise` — `defaults read ai.perplexity.mac authToken` via `Bun.$`. macOS only, returns null on other platforms or if app not installed. +- `loginViaEmailOtp(promptFn): Promise` — three-step HTTP flow (CSRF -> send OTP -> verify OTP). Uses `fetch` with the required headers. +- `authenticate(promptFn): Promise` — tries desktop extraction, falls back to email OTP, saves result. + +Constants: +```typescript +const USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0"; +const API_VERSION = "2.18"; +``` + +### Acceptance +- Can extract JWT from macOS app (if installed) +- Can complete email OTP flow (manual test) +- Token persists across process restarts +- Expired tokens detected correctly + +--- + +## Phase 3: SSE Client and Event Merging + +### 3.1 Type definitions (`src/search/types.ts`) + +Define all types from architecture.md: +- `StreamEvent`, `StreamBlock`, `MarkdownBlock`, `WebResult`, `StreamSource` +- `SearchResult` (unified output: answer, sources, model, requestId) +- `SearchSource` (title, url, snippet, publishedDate, ageSeconds) +- `SearchParams` (query, recency, limit, signal) +- `SearchError` class + +### 3.2 SSE stream parser (`src/search/stream.ts`) + +Implement: +- `async function* readSseEvents(body: ReadableStream, signal?: AbortSignal): AsyncGenerator` — reads SSE `data:` lines, parses JSON, yields typed events. Handles `[DONE]` marker. Abort-aware via signal. + +This is the core utility. Parse byte stream line by line, detect `data:` prefix, accumulate multi-line data fields, parse JSON. + +### 3.3 Event merging (`src/search/stream.ts`) + +Implement: +- `mergeMarkdownBlock(existing, incoming): MarkdownBlock` — handle `chunk_starting_offset` splice logic +- `mergeBlocks(existing, incoming): StreamBlock[]` — key by `intended_usage`, merge markdown blocks +- `mergeEvent(existing, incoming): StreamEvent` — shallow merge top-level, delegate blocks, preserve sources + +### 3.4 Search client (`src/search/client.ts`) + +Implement: +- `searchPerplexity(params: SearchParams, jwt: string): Promise` + +Steps: +1. Build request body (query, params object with all required fields) +2. `fetch` POST to `https://www.perplexity.ai/rest/sse/perplexity_ask` with required headers +3. Iterate SSE events via `readSseEvents()`, merge incrementally +4. On stream end, extract answer (markdown blocks -> ask_text -> text fallback) +5. Extract sources (web_results block -> sources_list fallback), deduplicate by URL +6. Return `SearchResult` + +### Acceptance +- Given a valid JWT, returns answer + sources for a test query +- Handles stream errors (error_code in event) +- Handles abort signal +- Empty results return gracefully + +--- + +## Phase 4: Response Formatting + +### 4.1 LLM output formatter (`src/search/format.ts`) + +Implement: +- `formatForLLM(result: SearchResult): string` + +Output format: +``` +## Answer + + +## Sources +N sources +[1] Title (2d ago) + https://url + snippet preview... +... + +## Meta +Provider: perplexity (oauth) +Model: +Request: +``` + +- Age calculation: `(Date.now() - new Date(dateStr).getTime()) / 1000` -> human-readable ("2d ago", "3h ago", "just now") +- Truncate snippets to 240 chars +- Respect `limit` param for source count + +### Acceptance +- Output is clean, readable, parseable by LLM +- Sources numbered and linked +- Age formatting works for various date formats + +--- + +## Phase 5: Plugin Integration + +### 5.1 Tool definition (`src/index.ts`) + +Wire everything together: + +```typescript +const factory: CustomToolFactory = (api) => { + const { Type } = api.typebox; + + return { + name: "perplexity_search", + label: "Perplexity Search", + description: "...", // from a .md file or inline + parameters: Type.Object({ + query: Type.String({ description: "Search query" }), + recency: Type.Optional( + Type.Union([ + Type.Literal("hour"), + Type.Literal("day"), + Type.Literal("week"), + Type.Literal("month"), + Type.Literal("year"), + ], { description: "Filter results by recency" }) + ), + limit: Type.Optional( + Type.Number({ description: "Max sources to return", minimum: 1, maximum: 50 }) + ), + }), + + async execute(toolCallId, params, onUpdate, ctx, signal) { + // 1. Get or refresh JWT + // 2. Call searchPerplexity() + // 3. Format result + // 4. Return { content: [{ type: "text", text }], details } + }, + }; +}; +``` + +### 5.2 Tool description prompt + +Create `src/prompts/tool-description.md`: +```markdown +# Perplexity Search + +Search the web using Perplexity Pro with multi-step reasoning and source citations. + + +- Use for questions requiring up-to-date web information +- Prefer primary sources; corroborate claims across multiple results +- Include source links in your response + + + +Returns synthesized answer with numbered source citations, URLs, and snippets. + + + +- query: Search query (required) +- recency: Filter by time — hour, day, week, month, year (optional) +- limit: Maximum number of sources to return (optional) + +``` + +### 5.3 TUI rendering (optional, can defer) + +If TUI rendering is desired: +- `renderCall(args, theme)` — show query text and recency filter +- `renderResult(result, options, theme)` — show answer preview, source count, expandable source list + +This requires importing `@oh-my-pi/pi-tui` Component types. Can be added later; the tool works without it (omp shows raw text). + +### Acceptance +- Plugin loads when registered with omp (`omp plugin install ./pi-perplexity`) +- Agent sees `perplexity_search` tool +- Agent can invoke tool and gets results +- Results display in TUI + +--- + +## Phase 6: Login Command (optional) + +### 6.1 Custom command for `omp login perplexity` + +If the plugin should expose a login command: + +Create `src/commands/login.ts`: +```typescript +// CustomCommand that triggers the auth flow interactively +``` + +Register in package.json manifest: +```json +"omp": { + "commands": ["src/commands/login.ts"] +} +``` + +This allows `omp perplexity login` or similar. If not needed, the tool's `execute` can trigger login on first use. + +--- + +## Phase 7: Testing + +### 7.1 Unit tests + +- `auth/jwt.test.ts` — JWT decode, expiry extraction, edge cases (malformed, missing exp) +- `search/stream.test.ts` — SSE parsing with fixture data, event merging logic +- `search/format.test.ts` — LLM output formatting, age calculation, truncation + +### 7.2 Integration tests (manual) + +- End-to-end: authenticate -> search -> verify answer and sources returned +- Expired JWT handling: mock expired token, verify re-auth prompt +- Abort: cancel mid-stream, verify clean exit +- Error responses: 401, 429, stream errors + +### 7.3 Fixture data + +Capture real SSE responses from Perplexity for test fixtures: +```typescript +// test/fixtures/sse-response.txt — raw SSE stream +// test/fixtures/merged-event.json — expected merged result +``` + +--- + +## Implementation Order + +``` +Phase 1: Scaffold (~30 min) — package.json, tsconfig, skeleton factory +Phase 2: Auth (~2 hours) — JWT decode, storage, login flow +Phase 3: SSE Client (~3 hours) — stream parser, event merging, search client +Phase 4: Formatting (~1 hour) — LLM output formatting +Phase 5: Plugin Wiring (~1 hour) — connect auth + search + format in factory +Phase 6: Login Command (~30 min) — optional interactive login command +Phase 7: Testing (~2 hours) — unit tests + manual integration +``` + +Total estimate: ~10 hours + +### Critical path + +Phase 3 (SSE parsing + event merging) is the most complex and error-prone component. The incremental merge logic for markdown chunks with offset splicing needs careful testing with real Perplexity SSE data. Capture fixtures early. + +### Risk areas + +1. **Perplexity API instability** — this is a reverse-engineered internal API, not a public contract. Headers, body format, or SSE event schema could change without notice. Mitigate by keeping the client thin and the types loose (optional fields everywhere). + +2. **JWT expiry** — Perplexity JWTs from the desktop app may have varying lifetimes. The 5-minute buffer should handle most cases, but monitor for short-lived tokens. + +3. **macOS-only desktop extraction** — Path 1 only works on macOS. Linux/Windows users must use email OTP. This is acceptable for the initial version. + +4. **Cloudflare challenges** — Perplexity uses Cloudflare. The specific User-Agent and headers bypass managed challenges (reverse-engineered from the macOS app). If Cloudflare changes rules, this may break. diff --git a/src/auth/jwt.ts b/src/auth/jwt.ts new file mode 100644 index 0000000..2ab43c5 --- /dev/null +++ b/src/auth/jwt.ts @@ -0,0 +1,37 @@ +const FIVE_MINUTES_MS = 5 * 60 * 1000; +const ONE_HOUR_MS = 60 * 60 * 1000; + +function decodeBase64Url(input: string): string { + const normalized = input.replace(/-/g, "+").replace(/_/g, "/"); + const padded = normalized + "=".repeat((4 - (normalized.length % 4)) % 4); + return Buffer.from(padded, "base64").toString("utf8"); +} + +/** Decode JWT payload and extract expiry as epoch ms (with 5min safety margin). Returns fallback of now+1h on decode failure. */ +export function decodeJwtExpiry(token: string): number { + const fallback = Date.now() + ONE_HOUR_MS; + + try { + const payload = token.split(".")[1]; + if (!payload) { + return fallback; + } + + const decodedPayload = decodeBase64Url(payload); + const parsed = JSON.parse(decodedPayload) as { exp?: unknown }; + + if (typeof parsed.exp !== "number" || !Number.isFinite(parsed.exp)) { + return fallback; + } + + const expiryMs = parsed.exp * 1000 - FIVE_MINUTES_MS; + return Number.isFinite(expiryMs) ? expiryMs : fallback; + } catch { + return fallback; + } +} + +/** Returns true if the token is expired (with optional buffer). */ +export function isJwtExpired(token: string, bufferMs = 0): boolean { + return decodeJwtExpiry(token) <= Date.now() + bufferMs; +} diff --git a/src/auth/login.ts b/src/auth/login.ts new file mode 100644 index 0000000..e76b182 --- /dev/null +++ b/src/auth/login.ts @@ -0,0 +1,86 @@ +import { AuthError } from "../search/types.js"; +import { decodeJwtExpiry, isJwtExpired } from "./jwt.js"; +import { clearToken, loadToken, saveToken } from "./storage.js"; +import { execFile } from "node:child_process"; +import { promisify } from "node:util"; + +const DESKTOP_AUTH_HELP = + "Install the Perplexity desktop app and sign in, or set PI_AUTH_NO_BORROW=1 to skip desktop token borrowing."; +const execFileAsync = promisify(execFile); + +/** Extract JWT from macOS Perplexity desktop app via `defaults read`. Returns null if app not installed or not logged in. */ +export async function extractFromDesktopApp(): Promise { + if (process.platform !== "darwin") { + return null; + } + + try { + const { stdout } = await execFileAsync("defaults", ["read", "ai.perplexity.mac", "authToken"]); + const token = stdout.trim(); + if (!token || token.split(".").length !== 3) { + return null; + } + + return token; + } catch { + return null; + } +} + +/** Run MVP auth strategy: load cached → try desktop extraction → save → throw AuthError if all fail. */ +export async function authenticate(): Promise { + const cached = await loadToken(); + if (cached) { + if (!isJwtExpired(cached.access)) { + return cached.access; + } + + await clearToken(); + + if (process.env.PI_AUTH_NO_BORROW === "1") { + throw new AuthError( + "EXPIRED", + `Cached token is expired. Re-authenticate in Perplexity desktop app, then retry. ${DESKTOP_AUTH_HELP}`, + ); + } + } + + if (process.env.PI_AUTH_NO_BORROW === "1") { + throw new AuthError( + "NO_TOKEN", + `No valid cached token found and desktop token borrowing is disabled. ${DESKTOP_AUTH_HELP}`, + ); + } + + let desktopToken: string | null; + try { + desktopToken = await extractFromDesktopApp(); + } catch { + throw new AuthError( + "EXTRACTION_FAILED", + `Failed to read token from the Perplexity desktop app. Ensure the app is installed and signed in. ${DESKTOP_AUTH_HELP}`, + ); + } + + if (!desktopToken) { + throw new AuthError( + "NO_TOKEN", + `Could not find a desktop token. Ensure Perplexity desktop app is installed and signed in. ${DESKTOP_AUTH_HELP}`, + ); + } + + if (isJwtExpired(desktopToken)) { + throw new AuthError( + "EXPIRED", + `Desktop token is expired. Open Perplexity desktop app and sign in again, then retry. ${DESKTOP_AUTH_HELP}`, + ); + } + + await saveToken({ + type: "oauth", + access: desktopToken, + expires: decodeJwtExpiry(desktopToken), + }); + + return desktopToken; +} diff --git a/src/auth/storage.ts b/src/auth/storage.ts new file mode 100644 index 0000000..7da1f51 --- /dev/null +++ b/src/auth/storage.ts @@ -0,0 +1,49 @@ +import { chmod, mkdir, readFile, rm, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { dirname, join } from "node:path"; + +import type { StoredToken } from "../search/types.js"; + +const TOKEN_PATH = join(homedir(), ".config", "pi-perplexity", "auth.json"); + +function isStoredToken(value: unknown): value is StoredToken { + if (!value || typeof value !== "object") { + return false; + } + + const candidate = value as Partial; + return ( + candidate.type === "oauth" && + typeof candidate.access === "string" && + candidate.access.length > 0 && + typeof candidate.expires === "number" && + Number.isFinite(candidate.expires) + ); +} + +/** Load persisted token from ~/.config/pi-perplexity/auth.json. Returns null if missing or unreadable. */ +export async function loadToken(): Promise { + try { + const raw = await readFile(TOKEN_PATH, "utf8"); + const parsed = JSON.parse(raw) as unknown; + if (!isStoredToken(parsed)) { + return null; + } + + return parsed; + } catch { + return null; + } +} + +/** Save token to disk with 0600 permissions. Creates directory if needed. */ +export async function saveToken(token: StoredToken): Promise { + await mkdir(dirname(TOKEN_PATH), { recursive: true }); + await writeFile(TOKEN_PATH, `${JSON.stringify(token, null, 2)}\n`, "utf8"); + await chmod(TOKEN_PATH, 0o600); +} + +/** Delete the stored token file. No-op if missing. */ +export async function clearToken(): Promise { + await rm(TOKEN_PATH, { force: true }); +} diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 0000000..111ac4e --- /dev/null +++ b/src/index.ts @@ -0,0 +1,109 @@ +import { StringEnum } from "@mariozechner/pi-ai"; +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; + +import { authenticate } from "./auth/login.js"; +import { clearToken } from "./auth/storage.js"; +import { formatForLLM } from "./search/format.js"; +import { searchPerplexity } from "./search/client.js"; +import { AuthError, SearchError } from "./search/types.js"; + +export default function (pi: ExtensionAPI) { + pi.registerTool({ + name: "perplexity_search", + label: "Perplexity Search", + description: "Search the web with your Perplexity subscription.", + parameters: Type.Object({ + query: Type.String({ description: "Search query" }), + recency: Type.Optional( + StringEnum(["hour", "day", "week", "month", "year"] as const, { + description: "Filter results by recency", + }), + ), + limit: Type.Optional( + Type.Number({ description: "Max sources to return", minimum: 1, maximum: 50 }), + ), + }), + async execute(toolCallId, params, signal, onUpdate, ctx) { + const start = Date.now(); + let sourceCount = 0; + + try { + onUpdate?.({ + content: [{ type: "text", text: "Authenticating with Perplexity..." }], + details: { toolCallId }, + }); + + const jwt = await authenticate(); + + if (signal?.aborted) { + return { + content: [{ type: "text", text: "Perplexity search was cancelled." }], + details: { sourceCount: 0, queryMs: Date.now() - start }, + }; + } + + onUpdate?.({ + content: [{ type: "text", text: "Querying Perplexity..." }], + details: { toolCallId }, + }); + + const result = await searchPerplexity( + { + query: params.query, + recency: params.recency, + limit: params.limit, + }, + jwt, + signal, + ); + + const formatted = formatForLLM(result, params.limit); + sourceCount = + typeof params.limit === "number" + ? Math.min(params.limit, result.sources.length) + : result.sources.length; + + return { + content: [{ type: "text", text: formatted }], + details: { + model: result.displayModel, + sourceCount, + queryMs: Date.now() - start, + uuid: result.uuid, + }, + }; + } catch (error) { + const queryMs = Date.now() - start; + + if (error instanceof AuthError) { + return { + content: [{ type: "text", text: `Authentication failed: ${error.message}` }], + details: { sourceCount, queryMs }, + }; + } + + if (error instanceof SearchError) { + if (error.code === "AUTH") { + await clearToken().catch(() => undefined); + } + + return { + content: [{ type: "text", text: `Perplexity search failed: ${error.message}` }], + details: { sourceCount, queryMs }, + }; + } + + return { + content: [ + { + type: "text", + text: `Perplexity search failed: ${(error as Error).message || "Unknown error"}`, + }, + ], + details: { sourceCount, queryMs }, + }; + } + }, + }); +} diff --git a/src/search/client.ts b/src/search/client.ts new file mode 100644 index 0000000..005d794 --- /dev/null +++ b/src/search/client.ts @@ -0,0 +1,244 @@ +import { mergeEvent, readSseEvents } from "./stream.js"; +import type { SearchResult, StreamEvent, WebResult } from "./types.js"; +import { SearchError } from "./types.js"; + +const PERPLEXITY_ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask"; +const PERPLEXITY_USER_AGENT = "Perplexity/641 CFNetwork/1568 Darwin/25.2.0"; + +export interface SearchParams { + query: string; + recency?: "hour" | "day" | "week" | "month" | "year"; + limit?: number; +} + +function normalizeUrl(url: string): string { + return url.trim().replace(/\/$/, "").toLowerCase(); +} + +function dedupeSourcesByUrl(sources: WebResult[]): WebResult[] { + const seen = new Set(); + const deduped: WebResult[] = []; + + for (const source of sources) { + const url = source.url?.trim(); + if (!url) { + deduped.push(source); + continue; + } + + const key = normalizeUrl(url); + if (seen.has(key)) { + continue; + } + + seen.add(key); + deduped.push(source); + } + + return deduped; +} + +function extractTextFromBlock(event: StreamEvent, match: (usage: string) => boolean): string | null { + const blocks = event.blocks ?? []; + + for (const block of blocks) { + const usage = block.intended_usage ?? ""; + if (!match(usage)) { + continue; + } + + const markdown = block.markdown_block; + if (!markdown) { + continue; + } + + if (typeof markdown.answer === "string" && markdown.answer.trim().length > 0) { + return markdown.answer.trim(); + } + + if (markdown.chunks && markdown.chunks.length > 0) { + const chunkText = markdown.chunks.join("").trim(); + if (chunkText.length > 0) { + return chunkText; + } + } + } + + return null; +} + +function extractAnswer(event: StreamEvent): string { + const markdownAnswer = extractTextFromBlock(event, (usage) => usage.includes("markdown")); + if (markdownAnswer) { + return markdownAnswer; + } + + const askTextAnswer = extractTextFromBlock(event, (usage) => usage === "ask_text"); + if (askTextAnswer) { + return askTextAnswer; + } + + return event.text?.trim() ?? ""; +} + +function extractSources(event: StreamEvent): WebResult[] { + const webResultsBlock = (event.blocks ?? []).find( + (block) => block.intended_usage === "web_results", + ); + + const blockSources = webResultsBlock?.web_result_block?.web_results ?? []; + if (blockSources.length > 0) { + return dedupeSourcesByUrl(blockSources); + } + + const fallbackSources: WebResult[] = (event.sources_list ?? []).map((source) => ({ + name: source.title, + url: source.url, + snippet: source.snippet, + timestamp: source.date, + })); + + return dedupeSourcesByUrl(fallbackSources); +} + +function buildRequestBody(params: SearchParams): Record { + const query = params.query; + const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone ?? "UTC"; + + return { + query_str: query, + params: { + query_str: query, + search_focus: "internet", + mode: "copilot", + model_preference: "pplx_pro_upgraded", + sources: ["web"], + attachments: [], + frontend_uuid: crypto.randomUUID(), + frontend_context_uuid: crypto.randomUUID(), + version: "2.18", + language: "en-US", + timezone, + search_recency_filter: params.recency ?? null, + is_incognito: true, + use_schematized_api: true, + skip_search_enabled: true, + }, + }; +} + +function mapHttpError(status: number): SearchError { + if (status === 401 || status === 403) { + return new SearchError( + "AUTH", + "Perplexity rejected authentication (401/403). Sign in to Perplexity desktop app and retry.", + ); + } + + if (status === 429) { + return new SearchError( + "RATE_LIMIT", + "Perplexity rate limited this request (429). Wait a bit, then retry.", + ); + } + + return new SearchError( + "NETWORK", + `Perplexity request failed with HTTP ${status}. Check connectivity and retry.`, + ); +} + +/** Execute a Perplexity search: POST SSE, stream/merge events, extract answer + sources. Throws SearchError on failure. */ +export async function searchPerplexity( + params: SearchParams, + jwt: string, + signal?: AbortSignal, +): Promise { + const requestId = crypto.randomUUID(); + + let response: Response; + try { + response = await fetch(PERPLEXITY_ENDPOINT, { + method: "POST", + headers: { + Authorization: `Bearer ${jwt}`, + "Content-Type": "application/json", + Accept: "text/event-stream", + Origin: "https://www.perplexity.ai", + Referer: "https://www.perplexity.ai/", + "User-Agent": PERPLEXITY_USER_AGENT, + "X-App-ApiClient": "default", + "X-App-ApiVersion": "2.18", + "X-Perplexity-Request-Reason": "submit", + "X-Request-ID": requestId, + }, + body: JSON.stringify(buildRequestBody(params)), + signal, + }); + } catch (error) { + if (signal?.aborted) { + throw new SearchError("NETWORK", "Perplexity request was cancelled."); + } + + throw new SearchError( + "NETWORK", + `Could not connect to Perplexity. ${(error as Error).message || "Network failure."}`, + ); + } + + if (!response.ok) { + throw mapHttpError(response.status); + } + + if (!response.body) { + throw new SearchError("STREAM", "Perplexity returned an empty stream body."); + } + + let snapshot: StreamEvent = {}; + + try { + for await (const event of readSseEvents(response.body, signal)) { + snapshot = mergeEvent(snapshot, event); + if (event.final || event.status === "COMPLETED") { + break; + } + } + } catch (error) { + if (error instanceof SearchError) { + throw error; + } + + if (signal?.aborted) { + throw new SearchError("NETWORK", "Perplexity request was cancelled."); + } + + throw new SearchError( + "STREAM", + `Failed to parse Perplexity stream: ${(error as Error).message || "unknown error"}`, + ); + } + + if (snapshot.error_code || snapshot.error_message) { + throw new SearchError( + "STREAM", + snapshot.error_message || `Perplexity stream error: ${snapshot.error_code}`, + ); + } + + const answer = extractAnswer(snapshot); + const sources = extractSources(snapshot); + + if (!answer && sources.length === 0) { + throw new SearchError( + "EMPTY", + "Perplexity returned no answer and no sources for this query.", + ); + } + + return { + answer: answer || "No answer text returned by Perplexity.", + sources, + displayModel: snapshot.display_model, + uuid: snapshot.uuid, + }; +} diff --git a/src/search/format.ts b/src/search/format.ts new file mode 100644 index 0000000..d0a2640 --- /dev/null +++ b/src/search/format.ts @@ -0,0 +1,96 @@ +import type { SearchResult, WebResult } from "./types.js"; + +const MAX_SNIPPET_LENGTH = 240; + +function truncateSnippet(snippet: string): string { + const normalized = snippet.replace(/\s+/g, " ").trim(); + if (normalized.length <= MAX_SNIPPET_LENGTH) { + return normalized; + } + + return `${normalized.slice(0, MAX_SNIPPET_LENGTH - 3)}...`; +} + +function humanizeAge(timestamp?: string): string { + if (!timestamp) { + return "unknown"; + } + + const parsed = Date.parse(timestamp); + if (Number.isNaN(parsed)) { + return "unknown"; + } + + const diffMs = Math.max(0, Date.now() - parsed); + const diffSeconds = Math.floor(diffMs / 1000); + + if (diffSeconds < 60) { + return "just now"; + } + + const diffMinutes = Math.floor(diffSeconds / 60); + if (diffMinutes < 60) { + return `${diffMinutes}m ago`; + } + + const diffHours = Math.floor(diffMinutes / 60); + if (diffHours < 24) { + return `${diffHours}h ago`; + } + + const diffDays = Math.floor(diffHours / 24); + return `${diffDays}d ago`; +} + +function formatSource(source: WebResult, index: number): string { + const title = source.name?.trim() || "Untitled source"; + const age = humanizeAge(source.timestamp); + const lines: string[] = [`[${index + 1}] ${title} (${age})`]; + + if (source.url?.trim()) { + lines.push(` ${source.url.trim()}`); + } + + if (source.snippet?.trim()) { + lines.push(` ${truncateSnippet(source.snippet)}`); + } + + return lines.join("\n"); +} + +/** Format a SearchResult into LLM-friendly text with ## Answer, ## Sources, ## Meta sections. */ +export function formatForLLM(result: SearchResult, limit?: number): string { + const sourceLimit = + typeof limit === "number" && Number.isFinite(limit) + ? Math.max(0, Math.floor(limit)) + : result.sources.length; + + const limitedSources = result.sources.slice(0, sourceLimit); + + const sourceSection = + limitedSources.length === 0 + ? "0 sources\n(no sources returned)" + : `${limitedSources.length} sources\n${limitedSources + .map((source, index) => formatSource(source, index)) + .join("\n\n")}`; + + const metaLines = [ + "Provider: perplexity (oauth)", + `Model: ${result.displayModel ?? "unknown"}`, + ]; + + if (result.uuid) { + metaLines.push(`Request ID: ${result.uuid}`); + } + + return [ + "## Answer", + result.answer.trim() || "No answer returned.", + "", + "## Sources", + sourceSection, + "", + "## Meta", + ...metaLines, + ].join("\n"); +} diff --git a/src/search/stream.ts b/src/search/stream.ts new file mode 100644 index 0000000..53c8e64 --- /dev/null +++ b/src/search/stream.ts @@ -0,0 +1,210 @@ +import type { StreamBlock, StreamEvent } from "./types.js"; + +function parseEventPayload(payload: string): StreamEvent | null { + const trimmed = payload.trim(); + if (!trimmed || trimmed === "[DONE]") { + return null; + } + + try { + const parsed = JSON.parse(trimmed) as unknown; + if (!parsed || typeof parsed !== "object") { + return null; + } + return parsed as StreamEvent; + } catch { + return null; + } +} + +/** Parse SSE `data:` lines from a ReadableStream, yielding parsed StreamEvent objects. Handles multi-line payloads, `[DONE]` marker, and abort signal. */ +export async function* readSseEvents( + body: ReadableStream, + signal?: AbortSignal, +): AsyncGenerator { + const reader = body.getReader(); + const decoder = new TextDecoder(); + + let bufferedText = ""; + let dataLines: string[] = []; + + const flushEvent = (): StreamEvent | null | "done" => { + if (dataLines.length === 0) { + return null; + } + + const payload = dataLines.join("\n"); + dataLines = []; + + if (payload.trim() === "[DONE]") { + return "done"; + } + + return parseEventPayload(payload); + }; + + try { + while (true) { + if (signal?.aborted) { + return; + } + + const { value, done } = await reader.read(); + if (done) { + break; + } + + bufferedText += decoder.decode(value, { stream: true }); + + while (true) { + const newlineIndex = bufferedText.indexOf("\n"); + if (newlineIndex < 0) { + break; + } + + const rawLine = bufferedText.slice(0, newlineIndex); + bufferedText = bufferedText.slice(newlineIndex + 1); + const line = rawLine.endsWith("\r") ? rawLine.slice(0, -1) : rawLine; + + if (line === "") { + const parsed = flushEvent(); + if (parsed === "done") { + return; + } + if (parsed) { + yield parsed; + } + continue; + } + + if (line.startsWith("data:")) { + dataLines.push(line.slice(5).trimStart()); + } + } + } + + bufferedText += decoder.decode(); + const tail = bufferedText.trim(); + if (tail.startsWith("data:")) { + dataLines.push(tail.slice(5).trimStart()); + } + + const parsed = flushEvent(); + if (parsed && parsed !== "done") { + yield parsed; + } + } finally { + reader.releaseLock(); + } +} + +/** Merge a markdown block's chunks, respecting chunk_starting_offset for splice. */ +export function mergeMarkdownBlock( + existing: { answer?: string; chunks?: string[]; chunk_starting_offset?: number }, + incoming: { answer?: string; chunks?: string[]; chunk_starting_offset?: number }, +): { answer?: string; chunks?: string[]; chunk_starting_offset?: number } { + const currentChunks = existing.chunks ?? []; + let mergedChunks = currentChunks; + + if (incoming.chunks) { + if (typeof incoming.chunk_starting_offset === "number") { + if (incoming.chunk_starting_offset <= 0) { + mergedChunks = [...incoming.chunks]; + } else { + mergedChunks = [ + ...currentChunks.slice(0, incoming.chunk_starting_offset), + ...incoming.chunks, + ]; + } + } else { + mergedChunks = [...incoming.chunks]; + } + } + + const mergedAnswer = + incoming.answer ?? + (mergedChunks.length > 0 ? mergedChunks.join("") : undefined) ?? + existing.answer; + + return { + ...existing, + ...incoming, + answer: mergedAnswer, + chunks: mergedChunks, + }; +} + +function mergeSingleBlock(existing: StreamBlock, incoming: StreamBlock): StreamBlock { + const merged: StreamBlock = { + ...existing, + ...incoming, + }; + + if (existing.markdown_block || incoming.markdown_block) { + merged.markdown_block = mergeMarkdownBlock( + existing.markdown_block ?? {}, + incoming.markdown_block ?? {}, + ); + } + + if (existing.web_result_block || incoming.web_result_block) { + merged.web_result_block = { + web_results: + incoming.web_result_block?.web_results ?? existing.web_result_block?.web_results ?? [], + }; + } + + return merged; +} + +/** Merge block arrays keyed by intended_usage. */ +export function mergeBlocks( + existing: StreamBlock[], + incoming: StreamBlock[], +): StreamBlock[] { + const result = existing.map((block) => ({ ...block })); + + for (const incomingBlock of incoming) { + if (!incomingBlock.intended_usage) { + result.push({ ...incomingBlock }); + continue; + } + + const index = result.findIndex( + (existingBlock) => existingBlock.intended_usage === incomingBlock.intended_usage, + ); + + if (index < 0) { + result.push({ ...incomingBlock }); + continue; + } + + result[index] = mergeSingleBlock(result[index], incomingBlock); + } + + return result; +} + +/** Merge two StreamEvents into a single accumulated snapshot. Sources are accumulated, never replaced. */ +export function mergeEvent( + existing: StreamEvent, + incoming: StreamEvent, +): StreamEvent { + const merged: StreamEvent = { + ...existing, + ...incoming, + }; + + if (existing.blocks || incoming.blocks) { + merged.blocks = mergeBlocks(existing.blocks ?? [], incoming.blocks ?? []); + } + + const existingSources = existing.sources_list ?? []; + const incomingSources = incoming.sources_list ?? []; + + if (existingSources.length > 0 || incomingSources.length > 0) { + merged.sources_list = [...existingSources, ...incomingSources]; + } + + return merged; +} diff --git a/src/search/types.ts b/src/search/types.ts new file mode 100644 index 0000000..f5770eb --- /dev/null +++ b/src/search/types.ts @@ -0,0 +1,83 @@ +// --- SSE event types (all fields optional per AGENTS.md: API is unstable) --- + +export interface StreamEvent { + status?: string; + final?: boolean; + text?: string; + blocks?: StreamBlock[]; + sources_list?: StreamSource[]; + display_model?: string; + uuid?: string; + error_code?: string; + error_message?: string; +} + +export interface StreamBlock { + intended_usage?: string; + markdown_block?: { + answer?: string; + chunks?: string[]; + chunk_starting_offset?: number; + }; + web_result_block?: { + web_results?: WebResult[]; + }; +} + +export interface WebResult { + name?: string; + url?: string; + snippet?: string; + timestamp?: string; +} + +export interface StreamSource { + title?: string; + url?: string; + snippet?: string; + date?: string; +} + +// --- Auth types --- + +export interface StoredToken { + type: "oauth"; + access: string; + expires: number; + email?: string; +} + +// --- Search result (output of client, input to formatter) --- + +export interface SearchResult { + answer: string; + sources: WebResult[]; + displayModel?: string; + uuid?: string; +} + +// --- Error types --- + +export type SearchErrorCode = "AUTH" | "RATE_LIMIT" | "NETWORK" | "STREAM" | "EMPTY"; + +export class SearchError extends Error { + constructor( + public readonly code: SearchErrorCode, + message: string, + ) { + super(message); + this.name = "SearchError"; + } +} + +export type AuthErrorCode = "NO_TOKEN" | "EXPIRED" | "EXTRACTION_FAILED"; + +export class AuthError extends Error { + constructor( + public readonly code: AuthErrorCode, + message: string, + ) { + super(message); + this.name = "AuthError"; + } +} diff --git a/test/auth/jwt.test.ts b/test/auth/jwt.test.ts new file mode 100644 index 0000000..b5b1c83 --- /dev/null +++ b/test/auth/jwt.test.ts @@ -0,0 +1,42 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; + +import { decodeJwtExpiry, isJwtExpired } from "../../src/auth/jwt.js"; + +const FIXED_NOW = Date.UTC(2026, 1, 16, 12, 0, 0); + +function createJwt(expSeconds: number): string { + const header = Buffer.from(JSON.stringify({ alg: "HS256", typ: "JWT" })).toString("base64url"); + const payload = Buffer.from(JSON.stringify({ exp: expSeconds })).toString("base64url"); + return `${header}.${payload}.signature`; +} + +describe("jwt helpers", () => { + const originalNow = Date.now; + + beforeEach(() => { + Date.now = () => FIXED_NOW; + }); + + afterEach(() => { + Date.now = originalNow; + }); + + test("decodeJwtExpiry returns expiry in ms with 5 minute safety margin", () => { + const expSeconds = Math.floor((FIXED_NOW + 2 * 60 * 60 * 1000) / 1000); + const token = createJwt(expSeconds); + + expect(decodeJwtExpiry(token)).toBe(expSeconds * 1000 - 5 * 60 * 1000); + }); + + test("decodeJwtExpiry falls back to now + 1h when token is malformed", () => { + expect(decodeJwtExpiry("not-a-jwt")).toBe(FIXED_NOW + 60 * 60 * 1000); + }); + + test("isJwtExpired honors additional caller-provided buffer", () => { + const expSeconds = Math.floor((FIXED_NOW + 20 * 60 * 1000) / 1000); + const token = createJwt(expSeconds); + + expect(isJwtExpired(token)).toBe(false); + expect(isJwtExpired(token, 16 * 60 * 1000)).toBe(true); + }); +}); diff --git a/test/fixtures/sse-basic.txt b/test/fixtures/sse-basic.txt new file mode 100644 index 0000000..8b1b5c6 --- /dev/null +++ b/test/fixtures/sse-basic.txt @@ -0,0 +1,6 @@ +data: {"status":"IN_PROGRESS", +data: "text":"partial"} + +data: {"status":"COMPLETED","final":true,"text":"final answer","display_model":"pplx_pro_upgraded","uuid":"req-basic","sources_list":[{"title":"Example","url":"https://example.com","snippet":"Example source","date":"2026-02-15T12:00:00.000Z"}]} + +data: [DONE] diff --git a/test/fixtures/sse-incremental.txt b/test/fixtures/sse-incremental.txt new file mode 100644 index 0000000..8970a3e --- /dev/null +++ b/test/fixtures/sse-incremental.txt @@ -0,0 +1,7 @@ +data: {"status":"IN_PROGRESS","blocks":[{"intended_usage":"markdown_block","markdown_block":{"chunks":["Hello ","wor"],"chunk_starting_offset":0}}]} + +data: {"status":"IN_PROGRESS","blocks":[{"intended_usage":"markdown_block","markdown_block":{"chunks":["world"],"chunk_starting_offset":1}}],"sources_list":[{"title":"Source A","url":"https://a.example","snippet":"A","date":"2026-02-15T10:00:00.000Z"}]} + +data: {"status":"COMPLETED","final":true,"blocks":[{"intended_usage":"web_results","web_result_block":{"web_results":[{"name":"Source B","url":"https://b.example","snippet":"B","timestamp":"2026-02-14T10:00:00.000Z"}]}}],"display_model":"pplx_pro_upgraded","uuid":"req-incremental"} + +data: [DONE] diff --git a/test/search/client.test.ts b/test/search/client.test.ts new file mode 100644 index 0000000..cfa1e30 --- /dev/null +++ b/test/search/client.test.ts @@ -0,0 +1,241 @@ +import { afterEach, describe, expect, test } from "bun:test"; + +import { searchPerplexity } from "../../src/search/client.js"; +import { SearchError } from "../../src/search/types.js"; + +const ENDPOINT = "https://www.perplexity.ai/rest/sse/perplexity_ask"; + +function createSseResponse(events: Array>, status = 200): Response { + const streamText = [ + ...events.map((event) => `data: ${JSON.stringify(event)}\n\n`), + "data: [DONE]\n\n", + ].join(""); + + return new Response(streamText, { + status, + headers: { + "content-type": "text/event-stream", + }, + }); +} + +describe("searchPerplexity", () => { + const originalFetch = globalThis.fetch; + + afterEach(() => { + globalThis.fetch = originalFetch; + }); + + test("builds request body and headers according to protocol", async () => { + let capturedUrl: RequestInfo | URL | undefined; + let capturedInit: RequestInit | undefined; + + globalThis.fetch = (async (url: RequestInfo | URL, init?: RequestInit) => { + capturedUrl = url; + capturedInit = init; + + return createSseResponse([ + { + status: "COMPLETED", + final: true, + blocks: [ + { + intended_usage: "markdown_block", + markdown_block: { + answer: "answer text", + }, + }, + { + intended_usage: "web_results", + web_result_block: { + web_results: [ + { + name: "Source", + url: "https://example.com", + snippet: "snippet", + timestamp: "2026-02-16T10:00:00.000Z", + }, + ], + }, + }, + ], + }, + ]); + }) as unknown as typeof fetch; + + const controller = new AbortController(); + const result = await searchPerplexity( + { query: "latest bun release notes", recency: "week" }, + "jwt-token", + controller.signal, + ); + + expect(String(capturedUrl)).toBe(ENDPOINT); + expect(capturedInit?.method).toBe("POST"); + expect(capturedInit?.signal).toBe(controller.signal); + + const headers = new Headers(capturedInit?.headers); + expect(headers.get("Authorization")).toBe("Bearer jwt-token"); + expect(headers.get("Accept")).toBe("text/event-stream"); + expect(headers.get("X-App-ApiVersion")).toBe("2.18"); + expect(headers.get("X-Request-ID")).toBeTruthy(); + + const body = JSON.parse(String(capturedInit?.body)) as { + query_str: string; + params: { + query_str: string; + mode: string; + model_preference: string; + is_incognito: boolean; + search_recency_filter: string | null; + frontend_uuid: string; + frontend_context_uuid: string; + }; + }; + + expect(body.query_str).toBe("latest bun release notes"); + expect(body.params.query_str).toBe("latest bun release notes"); + expect(body.params.mode).toBe("copilot"); + expect(body.params.model_preference).toBe("pplx_pro_upgraded"); + expect(body.params.is_incognito).toBe(true); + expect(body.params.search_recency_filter).toBe("week"); + expect(body.params.frontend_uuid).toBeTruthy(); + expect(body.params.frontend_context_uuid).toBeTruthy(); + + expect(result.answer).toBe("answer text"); + expect(result.sources).toHaveLength(1); + }); + + test("maps 401 and 403 responses to AUTH error", async () => { + for (const status of [401, 403]) { + globalThis.fetch = (async () => new Response("auth fail", { status })) as unknown as typeof fetch; + + await expect(searchPerplexity({ query: "q" }, "jwt")).rejects.toMatchObject({ + name: "SearchError", + code: "AUTH", + }); + } + }); + + test("maps 429 responses to RATE_LIMIT error", async () => { + globalThis.fetch = (async () => new Response("rate limited", { status: 429 })) as unknown as typeof fetch; + + await expect(searchPerplexity({ query: "q" }, "jwt")).rejects.toMatchObject({ + name: "SearchError", + code: "RATE_LIMIT", + }); + }); + + test("deduplicates sources by normalized URL", async () => { + globalThis.fetch = (async () => + createSseResponse([ + { + status: "COMPLETED", + final: true, + blocks: [ + { + intended_usage: "markdown_block", + markdown_block: { + answer: "answer text", + }, + }, + { + intended_usage: "web_results", + web_result_block: { + web_results: [ + { name: "A", url: "https://example.com/path" }, + { name: "A duplicate", url: "https://example.com/path/" }, + { name: "B", url: "https://another.example/path" }, + ], + }, + }, + ], + }, + ])) as unknown as typeof fetch; + + const result = await searchPerplexity({ query: "q" }, "jwt"); + + expect(result.sources).toHaveLength(2); + expect(result.sources[0].url).toBe("https://example.com/path"); + expect(result.sources[1].url).toBe("https://another.example/path"); + }); + + test("answer extraction prioritizes markdown_block over ask_text and text", async () => { + globalThis.fetch = (async () => + createSseResponse([ + { + status: "COMPLETED", + final: true, + text: "fallback text", + blocks: [ + { + intended_usage: "ask_text", + markdown_block: { answer: "ask text" }, + }, + { + intended_usage: "markdown_block", + markdown_block: { answer: "markdown answer" }, + }, + ], + sources_list: [{ title: "S", url: "https://example.com" }], + }, + ])) as unknown as typeof fetch; + + const result = await searchPerplexity({ query: "q" }, "jwt"); + expect(result.answer).toBe("markdown answer"); + }); + + test("answer extraction falls back to ask_text then text", async () => { + globalThis.fetch = (async () => + createSseResponse([ + { + status: "COMPLETED", + final: true, + text: "fallback text", + blocks: [ + { + intended_usage: "ask_text", + markdown_block: { answer: "ask answer" }, + }, + ], + sources_list: [{ title: "S", url: "https://example.com" }], + }, + ])) as unknown as typeof fetch; + + const askTextResult = await searchPerplexity({ query: "q" }, "jwt"); + expect(askTextResult.answer).toBe("ask answer"); + + globalThis.fetch = (async () => + createSseResponse([ + { + status: "COMPLETED", + final: true, + text: "text fallback", + sources_list: [{ title: "S", url: "https://example.com" }], + }, + ])) as unknown as typeof fetch; + + const textResult = await searchPerplexity({ query: "q" }, "jwt"); + expect(textResult.answer).toBe("text fallback"); + }); + + test("returns EMPTY error when response has no answer and no sources", async () => { + globalThis.fetch = (async () => + createSseResponse([ + { + status: "COMPLETED", + final: true, + }, + ])) as unknown as typeof fetch; + + let thrown: unknown; + try { + await searchPerplexity({ query: "q" }, "jwt"); + } catch (error) { + thrown = error; + } + + expect(thrown).toBeInstanceOf(SearchError); + expect((thrown as SearchError).code).toBe("EMPTY"); + }); +}); diff --git a/test/search/format.test.ts b/test/search/format.test.ts new file mode 100644 index 0000000..e7804d7 --- /dev/null +++ b/test/search/format.test.ts @@ -0,0 +1,116 @@ +import { afterEach, beforeEach, describe, expect, test } from "bun:test"; + +import { formatForLLM } from "../../src/search/format.js"; + +const NOW = Date.UTC(2026, 1, 16, 12, 0, 0); + +describe("formatForLLM", () => { + const originalNow = Date.now; + + beforeEach(() => { + Date.now = () => NOW; + }); + + afterEach(() => { + Date.now = originalNow; + }); + + test("renders required sections and deterministic source ordering", () => { + const output = formatForLLM({ + answer: "Answer body", + sources: [ + { + name: "Source 1", + url: "https://example.com/1", + snippet: "Snippet 1", + timestamp: new Date(NOW - 3 * 60 * 60 * 1000).toISOString(), + }, + { + name: "Source 2", + url: "https://example.com/2", + snippet: "Snippet 2", + timestamp: new Date(NOW - 2 * 24 * 60 * 60 * 1000).toISOString(), + }, + ], + displayModel: "pplx_pro_upgraded", + uuid: "req-123", + }); + + expect(output).toContain("## Answer"); + expect(output).toContain("## Sources"); + expect(output).toContain("## Meta"); + expect(output.indexOf("[1] Source 1")).toBeLessThan(output.indexOf("[2] Source 2")); + expect(output).toContain("Provider: perplexity (oauth)"); + expect(output).toContain("Model: pplx_pro_upgraded"); + expect(output).toContain("Request ID: req-123"); + }); + + test("humanizes source ages", () => { + const output = formatForLLM({ + answer: "Age test", + sources: [ + { + name: "Recent", + url: "https://example.com/recent", + snippet: "recent snippet", + timestamp: new Date(NOW - 30 * 1000).toISOString(), + }, + { + name: "Minutes", + url: "https://example.com/minutes", + snippet: "minutes snippet", + timestamp: new Date(NOW - 12 * 60 * 1000).toISOString(), + }, + { + name: "Hours", + url: "https://example.com/hours", + snippet: "hours snippet", + timestamp: new Date(NOW - 5 * 60 * 60 * 1000).toISOString(), + }, + { + name: "Days", + url: "https://example.com/days", + snippet: "days snippet", + timestamp: new Date(NOW - 3 * 24 * 60 * 60 * 1000).toISOString(), + }, + ], + }); + + expect(output).toContain("Recent (just now)"); + expect(output).toContain("Minutes (12m ago)"); + expect(output).toContain("Hours (5h ago)"); + expect(output).toContain("Days (3d ago)"); + }); + + test("truncates snippets to 240 chars", () => { + const longSnippet = "x".repeat(300); + + const output = formatForLLM({ + answer: "Snippet test", + sources: [ + { + name: "Long snippet", + url: "https://example.com/long", + snippet: longSnippet, + }, + ], + }); + + const snippetLine = output + .split("\n") + .find((line) => line.startsWith(" ") && line.includes("...")); + + expect(snippetLine).toBeDefined(); + expect(snippetLine!.trim().length).toBe(240); + }); + + test("handles empty source list", () => { + const output = formatForLLM({ + answer: "No sources", + sources: [], + }); + + expect(output).toContain("0 sources"); + expect(output).toContain("(no sources returned)"); + }); +}); diff --git a/test/search/stream.test.ts b/test/search/stream.test.ts new file mode 100644 index 0000000..386ab53 --- /dev/null +++ b/test/search/stream.test.ts @@ -0,0 +1,107 @@ +import { describe, expect, test } from "bun:test"; + +import { mergeEvent, mergeMarkdownBlock, readSseEvents } from "../../src/search/stream.js"; +import type { StreamEvent } from "../../src/search/types.js"; + +function streamFromString(input: string, chunkSize = 8): ReadableStream { + const encoded = new TextEncoder().encode(input); + + return new ReadableStream({ + start(controller) { + for (let index = 0; index < encoded.length; index += chunkSize) { + controller.enqueue(encoded.slice(index, index + chunkSize)); + } + controller.close(); + }, + }); +} + +async function collectEvents(stream: ReadableStream): Promise { + const events: StreamEvent[] = []; + for await (const event of readSseEvents(stream)) { + events.push(event); + } + return events; +} + +describe("SSE stream parsing", () => { + test("parses multiline data payloads and stops at [DONE]", async () => { + const fixture = await Bun.file("test/fixtures/sse-basic.txt").text(); + const events = await collectEvents(streamFromString(fixture, 5)); + + expect(events).toHaveLength(2); + expect(events[0].status).toBe("IN_PROGRESS"); + expect(events[0].text).toBe("partial"); + expect(events[1].status).toBe("COMPLETED"); + expect(events[1].final).toBe(true); + }); + + test("skips invalid JSON payloads and continues parsing", async () => { + const payload = [ + "data: {invalid-json}", + "", + 'data: {"status":"COMPLETED","text":"ok"}', + "", + "data: [DONE]", + "", + ].join("\n"); + + const events = await collectEvents(streamFromString(payload)); + expect(events).toHaveLength(1); + expect(events[0].text).toBe("ok"); + }); +}); + +describe("event merging", () => { + test("mergeMarkdownBlock splices chunks at chunk_starting_offset", () => { + const merged = mergeMarkdownBlock( + { + chunks: ["Hello ", "wor"], + chunk_starting_offset: 0, + }, + { + chunks: ["world"], + chunk_starting_offset: 1, + }, + ); + + expect(merged.chunks).toEqual(["Hello ", "world"]); + expect(merged.answer).toBe("Hello world"); + }); + + test("mergeEvent preserves and accumulates sources_list", () => { + const first = mergeEvent( + { sources_list: [{ title: "A", url: "https://a.example" }] }, + { text: "step 1" }, + ); + + expect(first.sources_list).toEqual([{ title: "A", url: "https://a.example" }]); + + const second = mergeEvent(first, { + sources_list: [{ title: "B", url: "https://b.example" }], + status: "COMPLETED", + }); + + expect(second.sources_list).toEqual([ + { title: "A", url: "https://a.example" }, + { title: "B", url: "https://b.example" }, + ]); + }); + + test("incremental fixture merges markdown and metadata", async () => { + const fixture = await Bun.file("test/fixtures/sse-incremental.txt").text(); + let snapshot: StreamEvent = {}; + + for await (const event of readSseEvents(streamFromString(fixture, 11))) { + snapshot = mergeEvent(snapshot, event); + } + + const markdown = snapshot.blocks?.find((block) => block.intended_usage === "markdown_block") + ?.markdown_block; + + expect(markdown?.chunks).toEqual(["Hello ", "world"]); + expect(markdown?.answer).toBe("Hello world"); + expect(snapshot.display_model).toBe("pplx_pro_upgraded"); + expect(snapshot.uuid).toBe("req-incremental"); + }); +}); diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bbdbb89 --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,13 @@ +{ + "compilerOptions": { + "strict": true, + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "noEmit": true, + "skipLibCheck": true, + "esModuleInterop": true, + "types": ["bun-types"] + }, + "include": ["src", "test"] +}