From 865d8d0d66910a98b90384ec0bb337bddb6f9a02 Mon Sep 17 00:00:00 2001 From: Sam Rolfe Date: Wed, 6 May 2026 19:19:17 +1000 Subject: [PATCH] flat: convert steel-browser from submodule to regular folder --- extensions/steel-browser | 1 - extensions/steel-browser/LICENSE | 21 + extensions/steel-browser/README.md | 178 + extensions/steel-browser/TESTING.md | 123 + extensions/steel-browser/docs/pi-agent.md | 618 +++ extensions/steel-browser/docs/pi-packages.md | 218 + extensions/steel-browser/package-lock.json | 4565 +++++++++++++++++ extensions/steel-browser/package.json | 59 + extensions/steel-browser/src/index.ts | 103 + extensions/steel-browser/src/session-mode.ts | 18 + extensions/steel-browser/src/steel-client.ts | 686 +++ .../steel-browser/src/tools/captcha-guard.ts | 321 ++ extensions/steel-browser/src/tools/click.ts | 340 ++ .../steel-browser/src/tools/computer.ts | 456 ++ extensions/steel-browser/src/tools/extract.ts | 621 +++ .../steel-browser/src/tools/fill-form.ts | 304 ++ .../steel-browser/src/tools/find-elements.ts | 316 ++ .../steel-browser/src/tools/navigate.ts | 335 ++ .../steel-browser/src/tools/navigation.ts | 193 + extensions/steel-browser/src/tools/pdf.ts | 237 + extensions/steel-browser/src/tools/scrape.ts | 408 ++ .../steel-browser/src/tools/screenshot.ts | 373 ++ extensions/steel-browser/src/tools/scroll.ts | 311 ++ .../src/tools/session-control.ts | 108 + .../steel-browser/src/tools/session-state.ts | 64 + .../steel-browser/src/tools/tool-runtime.ts | 246 + .../steel-browser/src/tools/tool-settings.ts | 56 + extensions/steel-browser/src/tools/type.ts | 269 + extensions/steel-browser/src/tools/wait.ts | 236 + .../steel-browser/tests/steel-client.test.ts | 165 + extensions/steel-browser/tests/tools.test.ts | 1190 +++++ extensions/steel-browser/tsconfig.json | 22 + 32 files changed, 13160 insertions(+), 1 deletion(-) delete mode 160000 extensions/steel-browser create mode 100644 extensions/steel-browser/LICENSE create mode 100644 extensions/steel-browser/README.md create mode 100644 extensions/steel-browser/TESTING.md create mode 100644 extensions/steel-browser/docs/pi-agent.md create mode 100644 extensions/steel-browser/docs/pi-packages.md create mode 100644 extensions/steel-browser/package-lock.json create mode 100644 extensions/steel-browser/package.json create mode 100644 extensions/steel-browser/src/index.ts create mode 100644 extensions/steel-browser/src/session-mode.ts create mode 100644 extensions/steel-browser/src/steel-client.ts create mode 100644 extensions/steel-browser/src/tools/captcha-guard.ts create mode 100644 extensions/steel-browser/src/tools/click.ts create mode 100644 extensions/steel-browser/src/tools/computer.ts create mode 100644 extensions/steel-browser/src/tools/extract.ts create mode 100644 extensions/steel-browser/src/tools/fill-form.ts create mode 100644 extensions/steel-browser/src/tools/find-elements.ts create mode 100644 extensions/steel-browser/src/tools/navigate.ts create mode 100644 extensions/steel-browser/src/tools/navigation.ts create mode 100644 extensions/steel-browser/src/tools/pdf.ts create mode 100644 extensions/steel-browser/src/tools/scrape.ts create mode 100644 extensions/steel-browser/src/tools/screenshot.ts create mode 100644 extensions/steel-browser/src/tools/scroll.ts create mode 100644 extensions/steel-browser/src/tools/session-control.ts create mode 100644 extensions/steel-browser/src/tools/session-state.ts create mode 100644 extensions/steel-browser/src/tools/tool-runtime.ts create mode 100644 extensions/steel-browser/src/tools/tool-settings.ts create mode 100644 extensions/steel-browser/src/tools/type.ts create mode 100644 extensions/steel-browser/src/tools/wait.ts create mode 100644 extensions/steel-browser/tests/steel-client.test.ts create mode 100644 extensions/steel-browser/tests/tools.test.ts create mode 100644 extensions/steel-browser/tsconfig.json diff --git a/extensions/steel-browser b/extensions/steel-browser deleted file mode 160000 index c2f5fd5..0000000 --- a/extensions/steel-browser +++ /dev/null @@ -1 +0,0 @@ -Subproject commit c2f5fd5dc1ef7a4c4a0f8f9169176d19f455917b diff --git a/extensions/steel-browser/LICENSE b/extensions/steel-browser/LICENSE new file mode 100644 index 0000000..30c7f31 --- /dev/null +++ b/extensions/steel-browser/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Steel + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/extensions/steel-browser/README.md b/extensions/steel-browser/README.md new file mode 100644 index 0000000..4fa7208 --- /dev/null +++ b/extensions/steel-browser/README.md @@ -0,0 +1,178 @@ +# @steel-experiments/pi-steel + +> **Steel Experiments** — This is where we ship early, break things, and explore what's next for browser agents. Experiments, prototypes, bleeding-edge demos, and community contributions that push the boundaries of what's possible with web automation. Not production-ready. Definitely interesting. + +[Steel](https://steel.dev) browser automation tools for the [Pi](https://github.com/badlogic/pi-mono) coding agent. + +This package publishes the Steel extension as a reusable Pi package so it can be installed directly into Pi or consumed by other runtimes such as Takopi-based wrappers. + +## Quick start + +```bash +pi install npm:@steel-experiments/pi-steel +``` + +Then just ask Pi to browse: + +``` +> Go to hacker news and find the top story +``` + +Pi will use `steel_navigate` to open the page, `steel_scrape` to read the content, and return what it finds. All session management happens automatically. + +## Tools + +### Navigation + +| Tool | Description | +|------|-------------| +| `steel_navigate` | Open a URL with automatic scheme normalization and retry logic | +| `steel_go_back` | Navigate back in browser history | +| `steel_get_url` | Read the current page URL | +| `steel_get_title` | Read the current page title | + +### Content extraction + +| Tool | Description | +|------|-------------| +| `steel_scrape` | Extract page content as text, markdown, or html | +| `steel_screenshot` | Capture a screenshot artifact | +| `steel_pdf` | Generate a PDF artifact | +| `steel_extract` | Extract structured data using a JSON schema | + +### Interaction + +| Tool | Description | +|------|-------------| +| `steel_click` | Click an element with captcha recovery | +| `steel_type` | Type text into a field | +| `steel_fill_form` | Fill multiple form fields at once | +| `steel_scroll` | Scroll the page or a nested container | +| `steel_find_elements` | Find interactive elements by selector | +| `steel_wait` | Wait for an element to appear | +| `steel_computer` | Low-level computer action with screenshot | + +### Session management + +| Tool | Description | +|------|-------------| +| `steel_pin_session` | Keep the browser session alive across prompts | +| `steel_release_session` | Close the browser and reset to default session mode | + +`steel_scrape` defaults to `text`. Ask for `markdown` when headings, lists, and links matter. Ask for `html` only when raw DOM markup is actually needed. + +`steel_scroll` can scroll the page or a nested container. For apps like Google Maps, pass a selector for the results pane instead of relying on window scrolling. + +## Install + +Install into Pi as a package: + +```bash +pi install npm:@steel-experiments/pi-steel +``` + +Or load it for a single run: + +```bash +pi -e npm:@steel-experiments/pi-steel +``` + +For local development from this repo: + +```bash +pi -e . +``` + +## Session modes + +Steel sessions have a lifecycle tied to how Pi uses them. The default works for most cases, but you can tune it: + +| Mode | Behavior | +|------|----------| +| `agent` (default) | One session per Pi prompt, closed after `agent_end` | +| `session` | Session stays alive until Pi switches or shuts down | +| `turn` | Session closed after each Pi turn — aggressive, can break multi-step workflows | + +Set the mode via environment variable: + +```bash +STEEL_SESSION_MODE=session pi -e npm:@steel-experiments/pi-steel +``` + +You can also change session persistence at runtime with `steel_pin_session` and `steel_release_session`. + +## Configuration + +### Required + +- Node.js 20+ +- A Pi runtime that supports extensions +- Steel authentication via either: + - `STEEL_API_KEY`, or + - `steel login` config in `~/.config/steel/config.json` + +### Environment variables + +**Connection** + +| Variable | Purpose | +|----------|---------| +| `STEEL_BASE_URL` | Steel API base URL | +| `STEEL_BROWSER_API_URL` | Browser API endpoint | +| `STEEL_LOCAL_API_URL` | Local Steel instance URL | +| `STEEL_API_URL` | Alternative API URL | +| `STEEL_CONFIG_DIR` | Custom config directory | + +**Session** + +| Variable | Purpose | +|----------|---------| +| `STEEL_SESSION_MODE` | Lifecycle mode: `agent`, `session`, or `turn` | +| `STEEL_SESSION_TIMEOUT_MS` | Session timeout | +| `STEEL_SESSION_HEADLESS` | Run browser headless | +| `STEEL_SESSION_REGION` | Browser region | +| `STEEL_SESSION_PROFILE_ID` | Persistent browser profile | +| `STEEL_SESSION_PERSIST_PROFILE` | Save profile changes | +| `STEEL_SESSION_CREDENTIALS` | Session credentials | +| `STEEL_SESSION_NAMESPACE` | Session namespace | + +**Proxy** + +| Variable | Purpose | +|----------|---------| +| `STEEL_USE_PROXY` | Enable proxy | +| `STEEL_PROXY_URL` | Proxy URL | + +**Captcha** + +| Variable | Purpose | +|----------|---------| +| `STEEL_SOLVE_CAPTCHA` | Enable captcha solving | +| `STEEL_CAPTCHA_MAX_RETRIES` | Max captcha retry attempts | +| `STEEL_CAPTCHA_WAIT_MS` | Captcha solve wait time | +| `STEEL_CAPTCHA_POLL_INTERVAL_MS` | Captcha poll interval | + +**Tools** + +| Variable | Purpose | +|----------|---------| +| `STEEL_TOOL_TIMEOUT_MS` | Default tool timeout | +| `STEEL_NAVIGATE_RETRY_COUNT` | Navigation retry attempts | + +`pi-steel` reads Steel CLI config for auth and local API resolution, and it normalizes CLI-style API URLs such as `http://localhost:3000/v1` to the SDK-compatible base URL form. + +## Development + +```bash +npm install +npm run build +npm test +``` + +Publish preflight: + +```bash +npm pack --dry-run +``` + +The package manifest in `package.json` exposes the compiled extension entrypoint via `pi.extensions`, which lets Pi load the package root directly after install. diff --git a/extensions/steel-browser/TESTING.md b/extensions/steel-browser/TESTING.md new file mode 100644 index 0000000..ad2d24c --- /dev/null +++ b/extensions/steel-browser/TESTING.md @@ -0,0 +1,123 @@ +# Testing `pi-steel` + +Prompts for proving the extension works end to end inside `pi`. One prompt per line. Phrased the way a person would actually ask. Run each at least three times — web agents are noisy. + +Load the extension: + +```bash +pi -e /Users/nikola/dev/steel/steel-pi/dist/index.js +``` + +Or from this repo: + +```bash +pi -e . +``` + +Unit tests: + +```bash +npm test +``` + +## Navigation and page identity + +Open https://example.com and tell me the page title and the final URL. +Open https://example.com, then go back, and tell me where you ended up. +Open https://example.com, then open https://news.ycombinator.com, then go back, and confirm you are on example.com again. +Open https://httpstat.us/404 and tell me exactly what you see and what the URL resolved to. +Try to open http://this-domain-should-not-exist-123.invalid and report the exact error without guessing. + +## Screenshots and PDFs + +Open https://example.com and save a full-page screenshot. Give me the artifact path. +Open https://example.com and save both a screenshot and a PDF. Confirm the two files are distinct and tell me their paths. +Open https://news.ycombinator.com and take a screenshot of just the top navigation bar. Tell me which selector you used. +Open https://example.com and try to screenshot a selector that does not exist. When that fails, recover with a full-page screenshot and report both attempts. + +## Scraping and extracting + +Open https://example.com, scrape the page as markdown, and quote the main heading back to me. +Open https://news.ycombinator.com and give me the first five story titles with their links as structured data. +Open https://news.ycombinator.com, extract the first five story titles, then scrape the page as markdown, and confirm each extracted title actually appears in the scrape. +Open https://httpbin.org/forms/post and list every visible form field with its label and type. +Open https://example.com and tell me the visible text content in under 200 characters. + +## Finding and clicking + +Open https://news.ycombinator.com and find the login link. Give me the top selector candidates and why you chose each. +Open https://news.ycombinator.com, click the login link, and tell me the new page title and URL. +Open https://news.ycombinator.com, click the login link, then go back, and prove you are on the front page again. +Open https://news.ycombinator.com and click a selector that definitely does not exist. Return the raw error and whether the URL changed. + +## Forms and typing + +Open https://httpbin.org/forms/post, fill in the customer name and telephone fields only, and return both the intended values and what the page actually shows in those fields. +Open https://duckduckgo.com, type "steel browser" into the search box, submit, and give me the first three result titles. +Open https://httpbin.org/forms/post, try to fill a field that does not exist, and report the exact failure instead of pretending it worked. + +## Scrolling and waiting + +Open https://news.ycombinator.com, scroll to the bottom, and tell me the last visible story title. +Open https://news.ycombinator.com, scroll down two viewports, extract five currently visible story titles, and confirm they appear in the scraped markdown after scrolling. +Open https://www.google.com/maps/search/beauty+salons+in+seattle+wa, then use steel_scroll with selector `div[role="feed"]` to move the results pane down and confirm the visible listings changed. +Open https://news.ycombinator.com, then use steel_scrape with format `markdown` and quote the first two story links. +Open https://news.ycombinator.com, then use steel_scrape with the default format and confirm it returns readable text rather than raw HTML. +Open https://example.com and wait for `h1` to appear before reading the page title. +Open https://example.com and wait for a selector that will never appear with a 3 second timeout. Report the timeout cleanly. + +## Session reuse + +Pin a session, open https://example.com, then in the same session open https://news.ycombinator.com, and confirm both pages were handled by the same browser instance. +Pin a session, open https://news.ycombinator.com, click the login link, then release the session and tell me what state you left it in. +Run two navigations back to back without pinning, and tell me whether a new session was created for each or the session was reused. + +## Truthfulness + +Open https://example.com and tell me the color of every visible button. If there are no visible buttons, say so explicitly instead of inventing any. +Open https://news.ycombinator.com and tell me whether there is a "Buy now" button. Do not claim it exists unless you can point to tool evidence. +Open https://example.com and list every image on the page with its alt text. If there are no images, say that. + +## Recovery + +Open https://news.ycombinator.com, try to click "Sign out", and when it fails, fall back to clicking "login" and report both attempts. +Open https://example.com, try to extract a "pricing table", and when there is none, say so and offer what is actually on the page instead. +Open https://httpbin.org/delay/5 with a 2 second timeout, let it fail, then retry with a longer timeout and report both runs. + +## End-to-end journeys + +Open https://news.ycombinator.com, capture the first five story titles, take a screenshot, click through to the first story's comments page, and give me the story title, the comments URL, and both artifact paths. +Open https://example.com, save a screenshot and a PDF, then navigate to https://news.ycombinator.com, save another screenshot, and return all three artifact paths with the URL each came from. +Open https://duckduckgo.com, search for "hacker news", click the first organic result, confirm the final URL is news.ycombinator.com, and return a screenshot of the landing page. + +## WebVoyager tasks + +Borrowed verbatim from the WebVoyager benchmark (https://github.com/MinorJerry/WebVoyager). Real sites, one clear goal, one checkable answer. Good for comparing our agent to published numbers. + +### Friendly sites (no login, no heavy bot walls) + +Find a recipe for a vegetarian lasagna that has at least a four-star rating and uses zucchini on https://www.allrecipes.com. +Find a five-star rated chocolate chip cookie recipe that takes less than 1 hour to make on https://www.allrecipes.com and tell me how many reviews it has. +Compare the prices of the latest models of MacBook Air available on https://www.apple.com. +Search https://arxiv.org for the latest preprints about "quantum computing" and give me the top three titles with authors. +Read the latest health-related news article published on https://www.bbc.com/news and summarize the key points. +Find the pronunciation, definition, and a sample sentence for the word "serendipity" on https://dictionary.cambridge.org. +Search https://www.coursera.org for a beginner-level course on Python programming suitable for someone with no programming experience, and give me the top result. +Look up the current standings for the NBA Eastern Conference on https://www.espn.com. +Search https://github.com for an open-source project related to "climate change data visualization" and report the project with the most stars. +Find a pre-trained sentiment analysis model on https://huggingface.co and return its name, downloads, and last update date. +Ask https://www.wolframalpha.com for the derivative of x^2 at x = 5.6 and report the answer it returns. +Use https://www.google.com to find the initial release date of "Guardians of the Galaxy Vol. 3" and return the date plus the source snippet. + +### Hard sites (bot walls, captchas, heavy JS) + +Search https://www.amazon.com for an Xbox Wireless controller in green color rated above 4 stars and return the top result with price and rating. +Find the cheapest available hotel room on https://www.booking.com for a three night stay starting 1 January in Jakarta for 2 adults, and return the hotel name and price. +On https://www.google.com/travel/flights, show me one-way flights from Chicago to Paris for next Saturday and return the three cheapest options. +Find 5 beauty salons with ratings greater than 4.8 in Seattle, WA on https://www.google.com/maps and return names, ratings, and addresses. + +## Output contract + +For anything above where you care about grading, append: + +> Return JSON only with: task, status (success | partial | failure), tools_used, observed (raw facts from tool output), artifacts, errors, notes (your conclusions). Do not claim success without tool evidence. diff --git a/extensions/steel-browser/docs/pi-agent.md b/extensions/steel-browser/docs/pi-agent.md new file mode 100644 index 0000000..5b8dedf --- /dev/null +++ b/extensions/steel-browser/docs/pi-agent.md @@ -0,0 +1,618 @@ + +# 🏖️ OSS Weekend + +**Issue tracker reopens Monday, April 13, 2026.** + +OSS weekend runs Thursday, April 2, 2026 through Monday, April 13, 2026. New issues and PRs from unapproved contributors are auto-closed during this time. Approved contributors can still open issues and PRs if something is genuinely urgent, but please keep that to pressing matters only. For support, join [Discord](https://discord.com/invite/3cU7Bz4UPx). + +> _Current focus: at the moment i'm deep in refactoring internals, and need to focus._ + + +--- + +

+ + pi logo + +

+

+ Discord + npm + Build status +

+

+ pi.dev domain graciously donated by +

+ Exy mascot
exe.dev
+

+ +Pi is a minimal terminal coding harness. Adapt pi to your workflows, not the other way around, without having to fork and modify pi internals. Extend it with TypeScript [Extensions](#extensions), [Skills](#skills), [Prompt Templates](#prompt-templates), and [Themes](#themes). Put your extensions, skills, prompt templates, and themes in [Pi Packages](#pi-packages) and share them with others via npm or git. + +Pi ships with powerful defaults but skips features like sub agents and plan mode. Instead, you can ask pi to build what you want or install a third party pi package that matches your workflow. + +Pi runs in four modes: interactive, print or JSON, RPC for process integration, and an SDK for embedding in your own apps. See [openclaw/openclaw](https://github.com/openclaw/openclaw) for a real-world SDK integration. + +## Share your OSS coding agent sessions + +If you use pi for open source work, please share your coding agent sessions. + +Public OSS session data helps improve models, prompts, tools, and evaluations using real development workflows. + +For the full explanation, see [this post on X](https://x.com/badlogicgames/status/2037811643774652911). + +To publish sessions, use [`badlogic/pi-share-hf`](https://github.com/badlogic/pi-share-hf). Read its README.md for setup instructions. All you need is a Hugging Face account, the Hugging Face CLI, and `pi-share-hf`. + +You can also watch [this video](https://x.com/badlogicgames/status/2041151967695634619), where I show how I publish my `pi-mono` sessions. + +I regularly publish my own `pi-mono` work sessions here: + +- [badlogicgames/pi-mono on Hugging Face](https://huggingface.co/datasets/badlogicgames/pi-mono) + +## Table of Contents + +- [Quick Start](#quick-start) +- [Providers & Models](#providers--models) +- [Interactive Mode](#interactive-mode) + - [Editor](#editor) + - [Commands](#commands) + - [Keyboard Shortcuts](#keyboard-shortcuts) + - [Message Queue](#message-queue) +- [Sessions](#sessions) + - [Branching](#branching) + - [Compaction](#compaction) +- [Settings](#settings) +- [Context Files](#context-files) +- [Customization](#customization) + - [Prompt Templates](#prompt-templates) + - [Skills](#skills) + - [Extensions](#extensions) + - [Themes](#themes) + - [Pi Packages](#pi-packages) +- [Programmatic Usage](#programmatic-usage) +- [Philosophy](#philosophy) +- [CLI Reference](#cli-reference) + +--- + +## Quick Start + +```bash +npm install -g @mariozechner/pi-coding-agent +``` + +Authenticate with an API key: + +```bash +export ANTHROPIC_API_KEY=sk-ant-... +pi +``` + +Or use your existing subscription: + +```bash +pi +/login # Then select provider +``` + +Then just talk to pi. By default, pi gives the model four tools: `read`, `write`, `edit`, and `bash`. The model uses these to fulfill your requests. Add capabilities via [skills](#skills), [prompt templates](#prompt-templates), [extensions](#extensions), or [pi packages](#pi-packages). + +**Platform notes:** [Windows](docs/windows.md) | [Termux (Android)](docs/termux.md) | [tmux](docs/tmux.md) | [Terminal setup](docs/terminal-setup.md) | [Shell aliases](docs/shell-aliases.md) + +--- + +## Providers & Models + +For each built-in provider, pi maintains a list of tool-capable models, updated with every release. Authenticate via subscription (`/login`) or API key, then select any model from that provider via `/model` (or Ctrl+L). + +**Subscriptions:** +- Anthropic Claude Pro/Max +- OpenAI ChatGPT Plus/Pro (Codex) +- GitHub Copilot +- Google Gemini CLI +- Google Antigravity + +**API keys:** +- Anthropic +- OpenAI +- Azure OpenAI +- Google Gemini +- Google Vertex +- Amazon Bedrock +- Mistral +- Groq +- Cerebras +- xAI +- OpenRouter +- Vercel AI Gateway +- ZAI +- OpenCode Zen +- OpenCode Go +- Hugging Face +- Kimi For Coding +- MiniMax + +See [docs/providers.md](docs/providers.md) for detailed setup instructions. + +**Custom providers & models:** Add providers via `~/.pi/agent/models.json` if they speak a supported API (OpenAI, Anthropic, Google). For custom APIs or OAuth, use extensions. See [docs/models.md](docs/models.md) and [docs/custom-provider.md](docs/custom-provider.md). + +--- + +## Interactive Mode + +

Interactive Mode

+ +The interface from top to bottom: + +- **Startup header** - Shows shortcuts (`/hotkeys` for all), loaded AGENTS.md files, prompt templates, skills, and extensions +- **Messages** - Your messages, assistant responses, tool calls and results, notifications, errors, and extension UI +- **Editor** - Where you type; border color indicates thinking level +- **Footer** - Working directory, session name, total token/cache usage, cost, context usage, current model + +The editor can be temporarily replaced by other UI, like built-in `/settings` or custom UI from extensions (e.g., a Q&A tool that lets the user answer model questions in a structured format). [Extensions](#extensions) can also replace the editor, add widgets above/below it, a status line, custom footer, or overlays. + +### Editor + +| Feature | How | +|---------|-----| +| File reference | Type `@` to fuzzy-search project files | +| Path completion | Tab to complete paths | +| Multi-line | Shift+Enter (or Ctrl+Enter on Windows Terminal) | +| Images | Ctrl+V to paste (Alt+V on Windows), or drag onto terminal | +| Bash commands | `!command` runs and sends output to LLM, `!!command` runs without sending | + +Standard editing keybindings for delete word, undo, etc. See [docs/keybindings.md](docs/keybindings.md). + +### Commands + +Type `/` in the editor to trigger commands. [Extensions](#extensions) can register custom commands, [skills](#skills) are available as `/skill:name`, and [prompt templates](#prompt-templates) expand via `/templatename`. + +| Command | Description | +|---------|-------------| +| `/login`, `/logout` | OAuth authentication | +| `/model` | Switch models | +| `/scoped-models` | Enable/disable models for Ctrl+P cycling | +| `/settings` | Thinking level, theme, message delivery, transport | +| `/resume` | Pick from previous sessions | +| `/new` | Start a new session | +| `/name ` | Set session display name | +| `/session` | Show session info (path, tokens, cost) | +| `/tree` | Jump to any point in the session and continue from there | +| `/fork` | Create a new session from the current branch | +| `/compact [prompt]` | Manually compact context, optional custom instructions | +| `/copy` | Copy last assistant message to clipboard | +| `/export [file]` | Export session to HTML file | +| `/share` | Upload as private GitHub gist with shareable HTML link | +| `/reload` | Reload keybindings, extensions, skills, prompts, and context files (themes hot-reload automatically) | +| `/hotkeys` | Show all keyboard shortcuts | +| `/changelog` | Display version history | +| `/quit` | Quit pi | + +### Keyboard Shortcuts + +See `/hotkeys` for the full list. Customize via `~/.pi/agent/keybindings.json`. See [docs/keybindings.md](docs/keybindings.md). + +**Commonly used:** + +| Key | Action | +|-----|--------| +| Ctrl+C | Clear editor | +| Ctrl+C twice | Quit | +| Escape | Cancel/abort | +| Escape twice | Open `/tree` | +| Ctrl+L | Open model selector | +| Ctrl+P / Shift+Ctrl+P | Cycle scoped models forward/backward | +| Shift+Tab | Cycle thinking level | +| Ctrl+O | Collapse/expand tool output | +| Ctrl+T | Collapse/expand thinking blocks | + +### Message Queue + +Submit messages while the agent is working: + +- **Enter** queues a *steering* message, delivered after the current assistant turn finishes executing its tool calls +- **Alt+Enter** queues a *follow-up* message, delivered only after the agent finishes all work +- **Escape** aborts and restores queued messages to editor +- **Alt+Up** retrieves queued messages back to editor + +On Windows Terminal, `Alt+Enter` is fullscreen by default. Remap it in [docs/terminal-setup.md](docs/terminal-setup.md) so pi can receive the follow-up shortcut. + +Configure delivery in [settings](docs/settings.md): `steeringMode` and `followUpMode` can be `"one-at-a-time"` (default, waits for response) or `"all"` (delivers all queued at once). `transport` selects provider transport preference (`"sse"`, `"websocket"`, or `"auto"`) for providers that support multiple transports. + +--- + +## Sessions + +Sessions are stored as JSONL files with a tree structure. Each entry has an `id` and `parentId`, enabling in-place branching without creating new files. See [docs/session.md](docs/session.md) for file format. + +### Management + +Sessions auto-save to `~/.pi/agent/sessions/` organized by working directory. + +```bash +pi -c # Continue most recent session +pi -r # Browse and select from past sessions +pi --no-session # Ephemeral mode (don't save) +pi --session # Use specific session file or ID +pi --fork # Fork specific session file or ID into a new session +``` + +### Branching + +**`/tree`** - Navigate the session tree in-place. Select any previous point, continue from there, and switch between branches. All history preserved in a single file. + +

Tree View

+ +- Search by typing, fold/unfold and jump between branches with Ctrl+←/Ctrl+→ or Alt+←/Alt+→, page with ←/→ +- Filter modes (Ctrl+O): default → no-tools → user-only → labeled-only → all +- Press Shift+L to label entries as bookmarks and Shift+T to toggle label timestamps + +**`/fork`** - Create a new session file from the current branch. Opens a selector, copies history up to the selected point, and places that message in the editor for modification. + +**`--fork `** - Fork an existing session file or partial session UUID directly from the CLI. This copies the full source session into a new session file in the current project. + +### Compaction + +Long sessions can exhaust context windows. Compaction summarizes older messages while keeping recent ones. + +**Manual:** `/compact` or `/compact ` + +**Automatic:** Enabled by default. Triggers on context overflow (recovers and retries) or when approaching the limit (proactive). Configure via `/settings` or `settings.json`. + +Compaction is lossy. The full history remains in the JSONL file; use `/tree` to revisit. Customize compaction behavior via [extensions](#extensions). See [docs/compaction.md](docs/compaction.md) for internals. + +--- + +## Settings + +Use `/settings` to modify common options, or edit JSON files directly: + +| Location | Scope | +|----------|-------| +| `~/.pi/agent/settings.json` | Global (all projects) | +| `.pi/settings.json` | Project (overrides global) | + +See [docs/settings.md](docs/settings.md) for all options. + +--- + +## Context Files + +Pi loads `AGENTS.md` (or `CLAUDE.md`) at startup from: +- `~/.pi/agent/AGENTS.md` (global) +- Parent directories (walking up from cwd) +- Current directory + +Use for project instructions, conventions, common commands. All matching files are concatenated. + +### System Prompt + +Replace the default system prompt with `.pi/SYSTEM.md` (project) or `~/.pi/agent/SYSTEM.md` (global). Append without replacing via `APPEND_SYSTEM.md`. + +--- + +## Customization + +### Prompt Templates + +Reusable prompts as Markdown files. Type `/name` to expand. + +```markdown + +Review this code for bugs, security issues, and performance problems. +Focus on: {{focus}} +``` + +Place in `~/.pi/agent/prompts/`, `.pi/prompts/`, or a [pi package](#pi-packages) to share with others. See [docs/prompt-templates.md](docs/prompt-templates.md). + +### Skills + +On-demand capability packages following the [Agent Skills standard](https://agentskills.io). Invoke via `/skill:name` or let the agent load them automatically. + +```markdown + +# My Skill +Use this skill when the user asks about X. + +## Steps +1. Do this +2. Then that +``` + +Place in `~/.pi/agent/skills/`, `~/.agents/skills/`, `.pi/skills/`, or `.agents/skills/` (from `cwd` up through parent directories) or a [pi package](#pi-packages) to share with others. See [docs/skills.md](docs/skills.md). + +### Extensions + +

Doom Extension

+ +TypeScript modules that extend pi with custom tools, commands, keyboard shortcuts, event handlers, and UI components. + +```typescript +export default function (pi: ExtensionAPI) { + pi.registerTool({ name: "deploy", ... }); + pi.registerCommand("stats", { ... }); + pi.on("tool_call", async (event, ctx) => { ... }); +} +``` + +**What's possible:** +- Custom tools (or replace built-in tools entirely) +- Sub-agents and plan mode +- Custom compaction and summarization +- Permission gates and path protection +- Custom editors and UI components +- Status lines, headers, footers +- Git checkpointing and auto-commit +- SSH and sandbox execution +- MCP server integration +- Make pi look like Claude Code +- Games while waiting (yes, Doom runs) +- ...anything you can dream up + +Place in `~/.pi/agent/extensions/`, `.pi/extensions/`, or a [pi package](#pi-packages) to share with others. See [docs/extensions.md](docs/extensions.md) and [examples/extensions/](examples/extensions/). + +### Themes + +Built-in: `dark`, `light`. Themes hot-reload: modify the active theme file and pi immediately applies changes. + +Place in `~/.pi/agent/themes/`, `.pi/themes/`, or a [pi package](#pi-packages) to share with others. See [docs/themes.md](docs/themes.md). + +### Pi Packages + +Bundle and share extensions, skills, prompts, and themes via npm or git. Find packages on [npmjs.com](https://www.npmjs.com/search?q=keywords%3Api-package) or [Discord](https://discord.com/channels/1456806362351669492/1457744485428629628). + +> **Security:** Pi packages run with full system access. Extensions execute arbitrary code, and skills can instruct the model to perform any action including running executables. Review source code before installing third-party packages. + +```bash +pi install npm:@foo/pi-tools +pi install npm:@foo/pi-tools@1.2.3 # pinned version +pi install git:github.com/user/repo +pi install git:github.com/user/repo@v1 # tag or commit +pi install git:git@github.com:user/repo +pi install git:git@github.com:user/repo@v1 # tag or commit +pi install https://github.com/user/repo +pi install https://github.com/user/repo@v1 # tag or commit +pi install ssh://git@github.com/user/repo +pi install ssh://git@github.com/user/repo@v1 # tag or commit +pi remove npm:@foo/pi-tools +pi uninstall npm:@foo/pi-tools # alias for remove +pi list +pi update # skips pinned packages +pi config # enable/disable extensions, skills, prompts, themes +``` + +Packages install to `~/.pi/agent/git/` (git) or global npm. Use `-l` for project-local installs (`.pi/git/`, `.pi/npm/`). If you use a Node version manager and want package installs to reuse a stable npm context, set `npmCommand` in `settings.json`, for example `["mise", "exec", "node@20", "--", "npm"]`. + +Create a package by adding a `pi` key to `package.json`: + +```json +{ + "name": "my-pi-package", + "keywords": ["pi-package"], + "pi": { + "extensions": ["./extensions"], + "skills": ["./skills"], + "prompts": ["./prompts"], + "themes": ["./themes"] + } +} +``` + +Without a `pi` manifest, pi auto-discovers from conventional directories (`extensions/`, `skills/`, `prompts/`, `themes/`). + +See [docs/packages.md](docs/packages.md). + +--- + +## Programmatic Usage + +### SDK + +```typescript +import { AuthStorage, createAgentSession, ModelRegistry, SessionManager } from "@mariozechner/pi-coding-agent"; + +const authStorage = AuthStorage.create(); +const modelRegistry = ModelRegistry.create(authStorage); +const { session } = await createAgentSession({ + sessionManager: SessionManager.inMemory(), + authStorage, + modelRegistry, +}); + +await session.prompt("What files are in the current directory?"); +``` + +For advanced multi-session runtime replacement, use `createAgentSessionRuntime()` and `AgentSessionRuntime`. + +See [docs/sdk.md](docs/sdk.md) and [examples/sdk/](examples/sdk/). + +### RPC Mode + +For non-Node.js integrations, use RPC mode over stdin/stdout: + +```bash +pi --mode rpc +``` + +RPC mode uses strict LF-delimited JSONL framing. Clients must split records on `\n` only. Do not use generic line readers like Node `readline`, which also split on Unicode separators inside JSON payloads. + +See [docs/rpc.md](docs/rpc.md) for the protocol. + +--- + +## Philosophy + +Pi is aggressively extensible so it doesn't have to dictate your workflow. Features that other tools bake in can be built with [extensions](#extensions), [skills](#skills), or installed from third-party [pi packages](#pi-packages). This keeps the core minimal while letting you shape pi to fit how you work. + +**No MCP.** Build CLI tools with READMEs (see [Skills](#skills)), or build an extension that adds MCP support. [Why?](https://mariozechner.at/posts/2025-11-02-what-if-you-dont-need-mcp/) + +**No sub-agents.** There's many ways to do this. Spawn pi instances via tmux, or build your own with [extensions](#extensions), or install a package that does it your way. + +**No permission popups.** Run in a container, or build your own confirmation flow with [extensions](#extensions) inline with your environment and security requirements. + +**No plan mode.** Write plans to files, or build it with [extensions](#extensions), or install a package. + +**No built-in to-dos.** They confuse models. Use a TODO.md file, or build your own with [extensions](#extensions). + +**No background bash.** Use tmux. Full observability, direct interaction. + +Read the [blog post](https://mariozechner.at/posts/2025-11-30-pi-coding-agent/) for the full rationale. + +--- + +## CLI Reference + +```bash +pi [options] [@files...] [messages...] +``` + +### Package Commands + +```bash +pi install [-l] # Install package, -l for project-local +pi remove [-l] # Remove package +pi uninstall [-l] # Alias for remove +pi update [source] # Update packages (skips pinned) +pi list # List installed packages +pi config # Enable/disable package resources +``` + +### Modes + +| Flag | Description | +|------|-------------| +| (default) | Interactive mode | +| `-p`, `--print` | Print response and exit | +| `--mode json` | Output all events as JSON lines (see [docs/json.md](docs/json.md)) | +| `--mode rpc` | RPC mode for process integration (see [docs/rpc.md](docs/rpc.md)) | +| `--export [out]` | Export session to HTML | + +In print mode, pi also reads piped stdin and merges it into the initial prompt: + +```bash +cat README.md | pi -p "Summarize this text" +``` + +### Model Options + +| Option | Description | +|--------|-------------| +| `--provider ` | Provider (anthropic, openai, google, etc.) | +| `--model ` | Model pattern or ID (supports `provider/id` and optional `:`) | +| `--api-key ` | API key (overrides env vars) | +| `--thinking ` | `off`, `minimal`, `low`, `medium`, `high`, `xhigh` | +| `--models ` | Comma-separated patterns for Ctrl+P cycling | +| `--list-models [search]` | List available models | + +### Session Options + +| Option | Description | +|--------|-------------| +| `-c`, `--continue` | Continue most recent session | +| `-r`, `--resume` | Browse and select session | +| `--session ` | Use specific session file or partial UUID | +| `--fork ` | Fork specific session file or partial UUID into a new session | +| `--session-dir ` | Custom session storage directory | +| `--no-session` | Ephemeral mode (don't save) | + +### Tool Options + +| Option | Description | +|--------|-------------| +| `--tools ` | Enable specific built-in tools (default: `read,bash,edit,write`) | +| `--no-tools` | Disable all built-in tools (extension tools still work) | + +Available built-in tools: `read`, `bash`, `edit`, `write`, `grep`, `find`, `ls` + +### Resource Options + +| Option | Description | +|--------|-------------| +| `-e`, `--extension ` | Load extension from path, npm, or git (repeatable) | +| `--no-extensions` | Disable extension discovery | +| `--skill ` | Load skill (repeatable) | +| `--no-skills` | Disable skill discovery | +| `--prompt-template ` | Load prompt template (repeatable) | +| `--no-prompt-templates` | Disable prompt template discovery | +| `--theme ` | Load theme (repeatable) | +| `--no-themes` | Disable theme discovery | + +Combine `--no-*` with explicit flags to load exactly what you need, ignoring settings.json (e.g., `--no-extensions -e ./my-ext.ts`). + +### Other Options + +| Option | Description | +|--------|-------------| +| `--system-prompt ` | Replace default prompt (context files and skills still appended) | +| `--append-system-prompt ` | Append to system prompt | +| `--verbose` | Force verbose startup | +| `-h`, `--help` | Show help | +| `-v`, `--version` | Show version | + +### File Arguments + +Prefix files with `@` to include in the message: + +```bash +pi @prompt.md "Answer this" +pi -p @screenshot.png "What's in this image?" +pi @code.ts @test.ts "Review these files" +``` + +### Examples + +```bash +# Interactive with initial prompt +pi "List all .ts files in src/" + +# Non-interactive +pi -p "Summarize this codebase" + +# Non-interactive with piped stdin +cat README.md | pi -p "Summarize this text" + +# Different model +pi --provider openai --model gpt-4o "Help me refactor" + +# Model with provider prefix (no --provider needed) +pi --model openai/gpt-4o "Help me refactor" + +# Model with thinking level shorthand +pi --model sonnet:high "Solve this complex problem" + +# Limit model cycling +pi --models "claude-*,gpt-4o" + +# Read-only mode +pi --tools read,grep,find,ls -p "Review the code" + +# High thinking level +pi --thinking high "Solve this complex problem" +``` + +### Environment Variables + +| Variable | Description | +|----------|-------------| +| `PI_CODING_AGENT_DIR` | Override config directory (default: `~/.pi/agent`) | +| `PI_PACKAGE_DIR` | Override package directory (useful for Nix/Guix where store paths tokenize poorly) | +| `PI_SKIP_VERSION_CHECK` | Skip version check at startup | +| `PI_CACHE_RETENTION` | Set to `long` for extended prompt cache (Anthropic: 1h, OpenAI: 24h) | +| `VISUAL`, `EDITOR` | External editor for Ctrl+G | + +--- + +## Contributing & Development + +See [CONTRIBUTING.md](../../CONTRIBUTING.md) for guidelines and [docs/development.md](docs/development.md) for setup, forking, and debugging. + +--- + +## License + +MIT + +## See Also + +- [@mariozechner/pi-ai](https://www.npmjs.com/package/@mariozechner/pi-ai): Core LLM toolkit +- [@mariozechner/pi-agent](https://www.npmjs.com/package/@mariozechner/pi-agent): Agent framework +- [@mariozechner/pi-tui](https://www.npmjs.com/package/@mariozechner/pi-tui): Terminal UI components diff --git a/extensions/steel-browser/docs/pi-packages.md b/extensions/steel-browser/docs/pi-packages.md new file mode 100644 index 0000000..7dee638 --- /dev/null +++ b/extensions/steel-browser/docs/pi-packages.md @@ -0,0 +1,218 @@ +> pi can help you create pi packages. Ask it to bundle your extensions, skills, prompt templates, or themes. + +# Pi Packages + +Pi packages bundle extensions, skills, prompt templates, and themes so you can share them through npm or git. A package can declare resources in `package.json` under the `pi` key, or use conventional directories. + +## Table of Contents + +- [Install and Manage](#install-and-manage) +- [Package Sources](#package-sources) +- [Creating a Pi Package](#creating-a-pi-package) +- [Package Structure](#package-structure) +- [Dependencies](#dependencies) +- [Package Filtering](#package-filtering) +- [Enable and Disable Resources](#enable-and-disable-resources) +- [Scope and Deduplication](#scope-and-deduplication) + +## Install and Manage + +> **Security:** Pi packages run with full system access. Extensions execute arbitrary code, and skills can instruct the model to perform any action including running executables. Review source code before installing third-party packages. + +```bash +pi install npm:@foo/bar@1.0.0 +pi install git:github.com/user/repo@v1 +pi install https://github.com/user/repo # raw URLs work too +pi install /absolute/path/to/package +pi install ./relative/path/to/package + +pi remove npm:@foo/bar +pi list # show installed packages from settings +pi update # update all non-pinned packages +``` + +By default, `install` and `remove` write to global settings (`~/.pi/agent/settings.json`). Use `-l` to write to project settings (`.pi/settings.json`) instead. Project settings can be shared with your team, and pi installs any missing packages automatically on startup. + +To try a package without installing it, use `--extension` or `-e`. This installs to a temporary directory for the current run only: + +```bash +pi -e npm:@foo/bar +pi -e git:github.com/user/repo +``` + +## Package Sources + +Pi accepts three source types in settings and `pi install`. + +### npm + +``` +npm:@scope/pkg@1.2.3 +npm:pkg +``` + +- Versioned specs are pinned and skipped by `pi update`. +- Global installs use `npm install -g`. +- Project installs go under `.pi/npm/`. +- Set `npmCommand` in `settings.json` to pin npm package lookup and install operations to a specific wrapper command such as `mise` or `asdf`. + +Example: + +```json +{ + "npmCommand": ["mise", "exec", "node@20", "--", "npm"] +} +``` + +### git + +``` +git:github.com/user/repo@v1 +git:git@github.com:user/repo@v1 +https://github.com/user/repo@v1 +ssh://git@github.com/user/repo@v1 +``` + +- Without `git:` prefix, only protocol URLs are accepted (`https://`, `http://`, `ssh://`, `git://`). +- With `git:` prefix, shorthand formats are accepted, including `github.com/user/repo` and `git@github.com:user/repo`. +- HTTPS and SSH URLs are both supported. +- SSH URLs use your configured SSH keys automatically (respects `~/.ssh/config`). +- For non-interactive runs (for example CI), you can set `GIT_TERMINAL_PROMPT=0` to disable credential prompts and set `GIT_SSH_COMMAND` (for example `ssh -o BatchMode=yes -o ConnectTimeout=5`) to fail fast. +- Refs pin the package and skip `pi update`. +- Cloned to `~/.pi/agent/git//` (global) or `.pi/git//` (project). +- Runs `npm install` after clone or pull if `package.json` exists. + +**SSH examples:** +```bash +# git@host:path shorthand (requires git: prefix) +pi install git:git@github.com:user/repo + +# ssh:// protocol format +pi install ssh://git@github.com/user/repo + +# With version ref +pi install git:git@github.com:user/repo@v1.0.0 +``` + +### Local Paths + +``` +/absolute/path/to/package +./relative/path/to/package +``` + +Local paths point to files or directories on disk and are added to settings without copying. Relative paths are resolved against the settings file they appear in. If the path is a file, it loads as a single extension. If it is a directory, pi loads resources using package rules. + +## Creating a Pi Package + +Add a `pi` manifest to `package.json` or use conventional directories. Include the `pi-package` keyword for discoverability. + +```json +{ + "name": "my-package", + "keywords": ["pi-package"], + "pi": { + "extensions": ["./extensions"], + "skills": ["./skills"], + "prompts": ["./prompts"], + "themes": ["./themes"] + } +} +``` + +Paths are relative to the package root. Arrays support glob patterns and `!exclusions`. + +### Gallery Metadata + +The [package gallery](https://shittycodingagent.ai/packages) displays packages tagged with `pi-package`. Add `video` or `image` fields to show a preview: + +```json +{ + "name": "my-package", + "keywords": ["pi-package"], + "pi": { + "extensions": ["./extensions"], + "video": "https://example.com/demo.mp4", + "image": "https://example.com/screenshot.png" + } +} +``` + +- **video**: MP4 only. On desktop, autoplays on hover. Clicking opens a fullscreen player. +- **image**: PNG, JPEG, GIF, or WebP. Displayed as a static preview. + +If both are set, video takes precedence. + +## Package Structure + +### Convention Directories + +If no `pi` manifest is present, pi auto-discovers resources from these directories: + +- `extensions/` loads `.ts` and `.js` files +- `skills/` recursively finds `SKILL.md` folders and loads top-level `.md` files as skills +- `prompts/` loads `.md` files +- `themes/` loads `.json` files + +## Dependencies + +Third party runtime dependencies belong in `dependencies` in `package.json`. Dependencies that do not register extensions, skills, prompt templates, or themes also belong in `dependencies`. When pi installs a package from npm or git, it runs `npm install`, so those dependencies are installed automatically. + +Pi bundles core packages for extensions and skills. If you import any of these, list them in `peerDependencies` with a `"*"` range and do not bundle them: `@mariozechner/pi-ai`, `@mariozechner/pi-agent-core`, `@mariozechner/pi-coding-agent`, `@mariozechner/pi-tui`, `@sinclair/typebox`. + +Other pi packages must be bundled in your tarball. Add them to `dependencies` and `bundledDependencies`, then reference their resources through `node_modules/` paths. Pi loads packages with separate module roots, so separate installs do not collide or share modules. + +Example: + +```json +{ + "dependencies": { + "shitty-extensions": "^1.0.1" + }, + "bundledDependencies": ["shitty-extensions"], + "pi": { + "extensions": ["extensions", "node_modules/shitty-extensions/extensions"], + "skills": ["skills", "node_modules/shitty-extensions/skills"] + } +} +``` + +## Package Filtering + +Filter what a package loads using the object form in settings: + +```json +{ + "packages": [ + "npm:simple-pkg", + { + "source": "npm:my-package", + "extensions": ["extensions/*.ts", "!extensions/legacy.ts"], + "skills": [], + "prompts": ["prompts/review.md"], + "themes": ["+themes/legacy.json"] + } + ] +} +``` + +`+path` and `-path` are exact paths relative to the package root. + +- Omit a key to load all of that type. +- Use `[]` to load none of that type. +- `!pattern` excludes matches. +- `+path` force-includes an exact path. +- `-path` force-excludes an exact path. +- Filters layer on top of the manifest. They narrow down what is already allowed. + +## Enable and Disable Resources + +Use `pi config` to enable or disable extensions, skills, prompt templates, and themes from installed packages and local directories. Works for both global (`~/.pi/agent`) and project (`.pi/`) scopes. + +## Scope and Deduplication + +Packages can appear in both global and project settings. If the same package appears in both, the project entry wins. Identity is determined by: + +- npm: package name +- git: repository URL without ref +- local: resolved absolute path diff --git a/extensions/steel-browser/package-lock.json b/extensions/steel-browser/package-lock.json new file mode 100644 index 0000000..09f1073 --- /dev/null +++ b/extensions/steel-browser/package-lock.json @@ -0,0 +1,4565 @@ +{ + "name": "@steel-experiments/pi-steel", + "version": "0.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@steel-experiments/pi-steel", + "version": "0.1.1", + "license": "MIT", + "dependencies": { + "playwright-core": "^1.58.2", + "steel-sdk": "^0.17.0" + }, + "devDependencies": { + "@mariozechner/pi-coding-agent": "^0.54.2", + "@sinclair/typebox": "^0.34.48", + "@types/node": "^25.3.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@mariozechner/pi-coding-agent": "*", + "@sinclair/typebox": "*" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.73.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.73.0.tgz", + "integrity": "sha512-URURVzhxXGJDGUGFunIOtBlSl7KWvZiAAKY/ttTkZAkXT9bTPqdk2eK0b8qqSxXpikh3QKPnPYpiyX98zf5ebw==", + "dev": true, + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-js": "^5.2.0", + "@aws-crypto/supports-web-crypto": "^5.2.0", + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "@aws-sdk/util-locate-window": "^3.0.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-browser/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/util": "^5.2.0", + "@aws-sdk/types": "^3.222.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.222.0", + "@smithy/util-utf8": "^2.0.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-crypto/util/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^2.2.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1029.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1029.0.tgz", + "integrity": "sha512-LFmNV+rLPXS87vdQBfNOmhlo+3T+t07tvyEmHeGec8jUAbOFckKbU7TTy7ePe9xVYOXQYcLw+pwslJ/VZvxDkw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-node": "^3.972.30", + "@aws-sdk/eventstream-handler-node": "^3.972.13", + "@aws-sdk/middleware-eventstream": "^3.972.9", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/middleware-websocket": "^3.972.15", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/token-providers": "3.1029.0", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/eventstream-serde-browser": "^4.2.13", + "@smithy/eventstream-serde-config-resolver": "^4.3.13", + "@smithy/eventstream-serde-node": "^4.2.13", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.973.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.973.27.tgz", + "integrity": "sha512-CUZ5m8hwMCH6OYI4Li/WgMfIEx10Q2PLI9Y3XOUTPGZJ53aZ0007jCv+X/ywsaERyKPdw5MRZWk877roQksQ4A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/xml-builder": "^3.972.17", + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.25.tgz", + "integrity": "sha512-6QfI0wv4jpG5CrdO/AO0JfZ2ux+tKwJPrUwmvxXF50vI5KIypKVGNF6b4vlkYEnKumDTI1NX2zUBi8JoU5QU3A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.27.tgz", + "integrity": "sha512-3V3Usj9Gs93h865DqN4M2NWJhC5kXU9BvZskfN3+69omuYlE3TZxOEcVQtBGLOloJB7BVfJKXVLqeNhOzHqSlQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.29.tgz", + "integrity": "sha512-SiBuAnXecCbT/OpAf3vqyI/AVE3mTaYr9ShXLybxZiPLBiPCCOIWSGAtYYGQWMRvobBTiqOewaB+wcgMMZI2Aw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-login": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.29.tgz", + "integrity": "sha512-OGOslTbOlxXexKMqhxCEbBQbUIfuhGxU5UXw3Fm56ypXHvrXH4aTt/xb5Y884LOoteP1QST1lVZzHfcTnWhiPQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.30", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.30.tgz", + "integrity": "sha512-FMnAnWxc8PG+ZrZ2OBKzY4luCUJhe9CG0B9YwYr4pzrYGLXBS2rl+UoUvjGbAwiptxRL6hyA3lFn03Bv1TLqTw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.25", + "@aws-sdk/credential-provider-http": "^3.972.27", + "@aws-sdk/credential-provider-ini": "^3.972.29", + "@aws-sdk/credential-provider-process": "^3.972.25", + "@aws-sdk/credential-provider-sso": "^3.972.29", + "@aws-sdk/credential-provider-web-identity": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.25", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.25.tgz", + "integrity": "sha512-HR7ynNRdNhNsdVCOCegy1HsfsRzozCOPtD3RzzT1JouuaHobWyRfJzCBue/3jP7gECHt+kQyZUvwg/cYLWurNQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.29.tgz", + "integrity": "sha512-HWv4SEq3jZDYPlwryZVef97+U8CxxRos5mK8sgGO1dQaFZpV5giZLzqGE5hkDmh2csYcBO2uf5XHjPTpZcJlig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/token-providers": "3.1026.0", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1026.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1026.0.tgz", + "integrity": "sha512-Ieq/HiRrbEtrYP387Nes0XlR7H1pJiJOZKv+QyQzMYpvTiDs0VKy2ZB3E2Zf+aFovWmeE7lRE4lXyF7dYM6GgA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.29.tgz", + "integrity": "sha512-PdMBza1WEKEUPFEmMGCfnU2RYCz9MskU2e8JxjyUOsMKku7j9YaDKvbDi2dzC0ihFoM6ods2SbhfAAro+Gwlew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.13", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.13.tgz", + "integrity": "sha512-2Pi1kD0MDkMAxDHqvpi/hKMs9hXUYbj2GLEjCwy+0jzfLChAsF50SUYnOeTI+RztA+Ic4pnLAdB03f1e8nggxQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/eventstream-codec": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.9.tgz", + "integrity": "sha512-ypgOvpWxQTCnQyDHGxnTviqqANE7FIIzII7VczJnTPCJcJlu17hMQXnvE47aKSKsawVJAaaRsyOEbHQuLJF9ng==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-host-header": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-host-header/-/middleware-host-header-3.972.9.tgz", + "integrity": "sha512-je5vRdNw4SkuTnmRbFZLdye4sQ0faLt8kwka5wnnSU30q1mHO4X+idGEJOOE+Tn1ME7Oryn05xxkDvIb3UaLaQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-logger": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-logger/-/middleware-logger-3.972.9.tgz", + "integrity": "sha512-HsVgDrruhqI28RkaXALm8grJ7Agc1wF6Et0xh6pom8NdO2VdO/SD9U/tPwUjewwK/pVoka+EShBxyCvgsPCtog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-recursion-detection": { + "version": "3.972.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-recursion-detection/-/middleware-recursion-detection-3.972.10.tgz", + "integrity": "sha512-RVQQbq5orQ/GHUnXvqEOj2HHPBJm+mM+ySwZKS5UaLBwra5ugRtiH09PLUoOZRl7a1YzaOzXSuGbn9iD5j60WQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-user-agent": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-user-agent/-/middleware-user-agent-3.972.29.tgz", + "integrity": "sha512-f/sIRzuTfEjg6NsbMYvye2VsmnQoNgntntleQyx5uGacUYzszbfIlO3GcI6G6daWUmTm0IDZc11qMHWwF0o0mQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-retry": "^4.3.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.15.tgz", + "integrity": "sha512-hsZ35FORQsN5hwNdMD6zWmHCphbXkDxO6j+xwCUiuMb0O6gzS/PWgttQNl1OAn7h/uqZAMUG4yOS0wY/yhAieg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-format-url": "^3.972.9", + "@smithy/eventstream-codec": "^4.2.13", + "@smithy/eventstream-serde-browser": "^4.2.13", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/protocol-http": "^5.3.13", + "@smithy/signature-v4": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.996.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.996.19.tgz", + "integrity": "sha512-uFkmCDXvmQYLanlYdOFS0+MQWkrj9wPMt/ZCc/0J0fjPim6F5jBVBmEomvGY/j77ILW6GTPwN22Jc174Mhkw6Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/sha256-browser": "5.2.0", + "@aws-crypto/sha256-js": "5.2.0", + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/middleware-host-header": "^3.972.9", + "@aws-sdk/middleware-logger": "^3.972.9", + "@aws-sdk/middleware-recursion-detection": "^3.972.10", + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/region-config-resolver": "^3.972.11", + "@aws-sdk/types": "^3.973.7", + "@aws-sdk/util-endpoints": "^3.996.6", + "@aws-sdk/util-user-agent-browser": "^3.972.9", + "@aws-sdk/util-user-agent-node": "^3.973.15", + "@smithy/config-resolver": "^4.4.14", + "@smithy/core": "^3.23.14", + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/hash-node": "^4.2.13", + "@smithy/invalid-dependency": "^4.2.13", + "@smithy/middleware-content-length": "^4.2.13", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-retry": "^4.5.0", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-body-length-node": "^4.2.3", + "@smithy/util-defaults-mode-browser": "^4.3.45", + "@smithy/util-defaults-mode-node": "^4.2.49", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.0", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/region-config-resolver": { + "version": "3.972.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/region-config-resolver/-/region-config-resolver-3.972.11.tgz", + "integrity": "sha512-6Q8B1dcx6BBqUTY1Mc/eROKA0FImEEY5VPSd6AGPEUf0ErjExz4snVqa9kNJSoVDV1rKaNf3qrWojgcKW+SdDg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/config-resolver": "^4.4.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1029.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1029.0.tgz", + "integrity": "sha512-oU3a9wEBUYHuWsoMpahiRIIQMUy2RSRb9NhlJ9DtKTwYWV2OXZ0hEM+RTjIC8T8I8v/C83OqbZrj7NBg1ATAhw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.973.27", + "@aws-sdk/nested-clients": "^3.996.19", + "@aws-sdk/types": "^3.973.7", + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.973.7", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.7.tgz", + "integrity": "sha512-reXRwoJ6CfChoqAsBszUYajAF8Z2LRE+CRcKocvFSMpIiLOtYU3aJ9trmn6VVPAzbbY5LXF+FfmUslbXk1SYFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-endpoints": { + "version": "3.996.6", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-endpoints/-/util-endpoints-3.996.6.tgz", + "integrity": "sha512-2nUQ+2ih7CShuKHpGSIYvvAIOHy52dOZguYG36zptBukhw6iFwcvGfG0tes0oZFWQqEWvgZe9HLWaNlvXGdOrg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-endpoints": "^3.3.4", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-format-url": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-format-url/-/util-format-url-3.972.9.tgz", + "integrity": "sha512-fNJXHrs0ZT7Wx0KGIqKv7zLxlDXt2vqjx9z6oKUQFmpE5o4xxnSryvVHfHpIifYHWKz94hFccIldJ0YSZjlCBw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-user-agent-browser": { + "version": "3.972.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-browser/-/util-user-agent-browser-3.972.9.tgz", + "integrity": "sha512-sn/LMzTbGjYqCCF24390WxPd6hkpoSptiUn5DzVp4cD71yqw+yGEGm1YCxyEoPXyc8qciM8UzLJcZBFslxo5Uw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.7", + "@smithy/types": "^4.14.0", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + } + }, + "node_modules/@aws-sdk/util-user-agent-node": { + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-user-agent-node/-/util-user-agent-node-3.973.15.tgz", + "integrity": "sha512-fYn3s9PtKdgQkczGZCFMgkNEe8aq1JCVbnRqjqN9RSVW43xn2RV9xdcZ3z01a48Jpkuh/xCmBKJxdLOo4Ozg7w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/middleware-user-agent": "^3.972.29", + "@aws-sdk/types": "^3.973.7", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "aws-crt": ">=1.0.0" + }, + "peerDependenciesMeta": { + "aws-crt": { + "optional": true + } + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.17", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.17.tgz", + "integrity": "sha512-Ra7hjqAZf1OXRRMueB13qex7mFJRDK/pgCvdSFemXBT8KCGnQDPoKzHY1SjN+TjJVmnpSF14W5tJ1vDamFu+Gg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "fast-xml-parser": "5.5.8", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@borewit/text-codec": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/@borewit/text-codec/-/text-codec-0.2.2.tgz", + "integrity": "sha512-DDaRehssg1aNrH4+2hnj1B7vnUGEjU6OIlyRdkMd0aUdIUvKXrJfXsy8LVtXAy7DRvYVluWbMspsRhz2lcW0mQ==", + "dev": true, + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.7.tgz", + "integrity": "sha512-EKX3Qwmhz1eMdEJokhALr0YiD0lhQNwDqkPYyPhiSwKrh7/4KRjQc04sZ8db+5DVVnZ1LmbNDI1uAMPEUBnQPg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.7.tgz", + "integrity": "sha512-jbPXvB4Yj2yBV7HUfE2KHe4GJX51QplCN1pGbYjvsyCZbQmies29EoJbkEc+vYuU5o45AfQn37vZlyXy4YJ8RQ==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.7.tgz", + "integrity": "sha512-62dPZHpIXzvChfvfLJow3q5dDtiNMkwiRzPylSCfriLvZeq0a1bWChrGx/BbUbPwOrsWKMn8idSllklzBy+dgQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.7.tgz", + "integrity": "sha512-x5VpMODneVDb70PYV2VQOmIUUiBtY3D3mPBG8NxVk5CogneYhkR7MmM3yR/uMdITLrC1ml/NV1rj4bMJuy9MCg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.7.tgz", + "integrity": "sha512-5lckdqeuBPlKUwvoCXIgI2D9/ABmPq3Rdp7IfL70393YgaASt7tbju3Ac+ePVi3KDH6N2RqePfHnXkaDtY9fkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.7.tgz", + "integrity": "sha512-rYnXrKcXuT7Z+WL5K980jVFdvVKhCHhUwid+dDYQpH+qu+TefcomiMAJpIiC2EM3Rjtq0sO3StMV/+3w3MyyqQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.7.tgz", + "integrity": "sha512-B48PqeCsEgOtzME2GbNM2roU29AMTuOIN91dsMO30t+Ydis3z/3Ngoj5hhnsOSSwNzS+6JppqWsuhTp6E82l2w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.7.tgz", + "integrity": "sha512-jOBDK5XEjA4m5IJK3bpAQF9/Lelu/Z9ZcdhTRLf4cajlB+8VEhFFRjWgfy3M1O4rO2GQ/b2dLwCUGpiF/eATNQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.7.tgz", + "integrity": "sha512-RkT/YXYBTSULo3+af8Ib0ykH8u2MBh57o7q/DAs3lTJlyVQkgQvlrPTnjIzzRPQyavxtPtfg0EopvDyIt0j1rA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.7.tgz", + "integrity": "sha512-RZPHBoxXuNnPQO9rvjh5jdkRmVizktkT7TCDkDmQ0W2SwHInKCAV95GRuvdSvA7w4VMwfCjUiPwDi0ZO6Nfe9A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.7.tgz", + "integrity": "sha512-GA48aKNkyQDbd3KtkplYWT102C5sn/EZTY4XROkxONgruHPU72l+gW+FfF8tf2cFjeHaRbWpOYa/uRBz/Xq1Pg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.7.tgz", + "integrity": "sha512-a4POruNM2oWsD4WKvBSEKGIiWQF8fZOAsycHOt6JBpZ+JN2n2JH9WAv56SOyu9X5IqAjqSIPTaJkqN8F7XOQ5Q==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.7.tgz", + "integrity": "sha512-KabT5I6StirGfIz0FMgl1I+R1H73Gp0ofL9A3nG3i/cYFJzKHhouBV5VWK1CSgKvVaG4q1RNpCTR2LuTVB3fIw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.7.tgz", + "integrity": "sha512-gRsL4x6wsGHGRqhtI+ifpN/vpOFTQtnbsupUF5R5YTAg+y/lKelYR1hXbnBdzDjGbMYjVJLJTd2OFmMewAgwlQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.7.tgz", + "integrity": "sha512-hL25LbxO1QOngGzu2U5xeXtxXcW+/GvMN3ejANqXkxZ/opySAZMrc+9LY/WyjAan41unrR3YrmtTsUpwT66InQ==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.7.tgz", + "integrity": "sha512-2k8go8Ycu1Kb46vEelhu1vqEP+UeRVj2zY1pSuPdgvbd5ykAw82Lrro28vXUrRmzEsUV0NzCf54yARIK8r0fdw==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.7.tgz", + "integrity": "sha512-hzznmADPt+OmsYzw1EE33ccA+HPdIqiCRq7cQeL1Jlq2gb1+OyWBkMCrYGBJ+sxVzve2ZJEVeePbLM2iEIZSxA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.7.tgz", + "integrity": "sha512-b6pqtrQdigZBwZxAn1UpazEisvwaIDvdbMbmrly7cDTMFnw/+3lVxxCTGOrkPVnsYIosJJXAsILG9XcQS+Yu6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.7.tgz", + "integrity": "sha512-OfatkLojr6U+WN5EDYuoQhtM+1xco+/6FSzJJnuWiUw5eVcicbyK3dq5EeV/QHT1uy6GoDhGbFpprUiHUYggrw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.7.tgz", + "integrity": "sha512-AFuojMQTxAz75Fo8idVcqoQWEHIXFRbOc1TrVcFSgCZtQfSdc1RXgB3tjOn/krRHENUB4j00bfGjyl2mJrU37A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.7.tgz", + "integrity": "sha512-+A1NJmfM8WNDv5CLVQYJ5PshuRm/4cI6WMZRg1by1GwPIQPCTs1GLEUHwiiQGT5zDdyLiRM/l1G0Pv54gvtKIg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.7.tgz", + "integrity": "sha512-+KrvYb/C8zA9CU/g0sR6w2RBw7IGc5J2BPnc3dYc5VJxHCSF1yNMxTV5LQ7GuKteQXZtspjFbiuW5/dOj7H4Yw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.7.tgz", + "integrity": "sha512-ikktIhFBzQNt/QDyOL580ti9+5mL/YZeUPKU2ivGtGjdTYoqz6jObj6nOMfhASpS4GU4Q/Clh1QtxWAvcYKamA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.7.tgz", + "integrity": "sha512-7yRhbHvPqSpRUV7Q20VuDwbjW5kIMwTHpptuUzV+AA46kiPze5Z7qgt6CLCK3pWFrHeNfDd1VKgyP4O+ng17CA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.7.tgz", + "integrity": "sha512-SmwKXe6VHIyZYbBLJrhOoCJRB/Z1tckzmgTLfFYOfpMAx63BJEaL9ExI8x7v0oAO3Zh6D/Oi1gVxEYr5oUCFhw==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.7.tgz", + "integrity": "sha512-56hiAJPhwQ1R4i+21FVF7V8kSD5zZTdHcVuRFMW0hn753vVfQN8xlx4uOPT4xoGH0Z/oVATuR82AiqSTDIpaHg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.49.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.49.0.tgz", + "integrity": "sha512-hO69Zl0H3x+L0KL4stl1pLYgnqnwHoLqtKy6MRlNnW8TAxjqMdOUVafomKd4z1BePkzoxJWbYILny9a2Zk43VQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "google-auth-library": "^10.3.0", + "p-retry": "^4.6.2", + "protobufjs": "^7.5.4", + "ws": "^8.18.0" + }, + "engines": { + "node": ">=20.0.0" + }, + "peerDependencies": { + "@modelcontextprotocol/sdk": "^1.25.2" + }, + "peerDependenciesMeta": { + "@modelcontextprotocol/sdk": { + "optional": true + } + } + }, + "node_modules/@mariozechner/clipboard": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.2.tgz", + "integrity": "sha512-IHQpksNjo7EAtGuHFU+tbWDp5LarH3HU/8WiB9O70ZEoBPHOg0/6afwSLK0QyNMMmx4Bpi/zl6+DcBXe95nWYA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.2", + "@mariozechner/clipboard-darwin-universal": "0.3.2", + "@mariozechner/clipboard-darwin-x64": "0.3.2", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.2", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.2", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.2", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.2", + "@mariozechner/clipboard-linux-x64-musl": "0.3.2", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.2", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.2" + } + }, + "node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.2.tgz", + "integrity": "sha512-uBf6K7Je1ihsgvmWxA8UCGCeI+nbRVRXoarZdLjl6slz94Zs1tNKFZqx7aCI5O1i3e0B6ja82zZ06BWrl0MCVw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.2.tgz", + "integrity": "sha512-mxSheKTW2U9LsBdXy0SdmdCAE5HqNS9QUmpNHLnfJ+SsbFKALjEZc5oRrVMXxGQSirDvYf5bjmRyT0QYYonnlg==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.2.tgz", + "integrity": "sha512-U1BcVEoidvwIp95+HJswSW+xr28EQiHR7rZjH6pn8Sja5yO4Yoe3yCN0Zm8Lo72BbSOK/fTSq0je7CJpaPCspg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.2.tgz", + "integrity": "sha512-BsinwG3yWTIjdgNCxsFlip7LkfwPk+ruw/aFCXHUg/fb5XC/Ksp+YMQ7u0LUtiKzIv/7LMXgZInJQH6gxbAaqQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.2.tgz", + "integrity": "sha512-0/Gi5Xq2V6goXBop19ePoHvXsmJD9SzFlO3S+d6+T2b+BlPcpOu3Oa0wTjl+cZrLAAEzA86aPNBI+VVAFDFPKw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.2.tgz", + "integrity": "sha512-2AFFiXB24qf0zOZsxI1GJGb9wQGlOJyN6UwoXqmKS3dpQi/l6ix30IzDDA4c4ZcCcx4D+9HLYXhC1w7Sov8pXA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.2.tgz", + "integrity": "sha512-v6fVnsn7WMGg73Dab8QMwyFce7tzGfgEixKgzLP8f1GJqkJZi5zO4k4FOHzSgUufgLil63gnxvMpjWkgfeQN7A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.2.tgz", + "integrity": "sha512-xVUtnoMQ8v2JVyfJLKKXACA6avdnchdbBkTsZs8BgJQo29qwCp5NIHAUO8gbJ40iaEGToW5RlmVk2M9V0HsHEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.2.tgz", + "integrity": "sha512-AEgg95TNi8TGgak2wSXZkXKCvAUTjWoU1Pqb0ON7JHrX78p616XUFNTJohtIon3e0w6k0pYPZeCuqRCza/Tqeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.2.tgz", + "integrity": "sha512-tGRuYpZwDOD7HBrCpyRuhGnHHSCknELvqwKKUG4JSfSB7JIU7LKRh6zx6fMUOQd8uISK35TjFg5UcNih+vJhFA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@mariozechner/jiti": { + "version": "2.6.5", + "resolved": "https://registry.npmjs.org/@mariozechner/jiti/-/jiti-2.6.5.tgz", + "integrity": "sha512-faGUlTcXka5l7rv0lP3K3vGW/ejRuOS24RR2aSFWREUQqzjgdsuWNo/IiPqL3kWRGt6Ahl2+qcDAwtdeWeuGUw==", + "dev": true, + "license": "MIT", + "dependencies": { + "std-env": "^3.10.0", + "yoctocolors": "^2.1.2" + }, + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@mariozechner/pi-agent-core": { + "version": "0.54.2", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-agent-core/-/pi-agent-core-0.54.2.tgz", + "integrity": "sha512-dSg5tl3SGdrTFeTxfifvHjZ39M8k4jQ0ogTDtVeP7Pi3A3Whw4W1o6pYzTjX285CLxxqKiJS6MkwoElQHFwH1A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mariozechner/pi-ai": "^0.54.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@mariozechner/pi-ai": { + "version": "0.54.2", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-ai/-/pi-ai-0.54.2.tgz", + "integrity": "sha512-QKQV8iT7afwdaOiLDPTPyQcsGw4ulxBjAI0GvgvowAuqy9UbDeKFSdQYLmjVt7CtnJD1Z8zMjQQ4SLigdZ6dRQ==", + "dev": true, + "license": "MIT", + "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" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@mariozechner/pi-coding-agent": { + "version": "0.54.2", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-coding-agent/-/pi-coding-agent-0.54.2.tgz", + "integrity": "sha512-m4xozDZ7GdB9iXy8/ykaLZ7aCcINxOPhA9WYY+zd6Mb8DWoeo7YmXNBAfemACGlAAGsUW2cSKI66tboczA2Uag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@mariozechner/jiti": "^2.6.2", + "@mariozechner/pi-agent-core": "^0.54.2", + "@mariozechner/pi-ai": "^0.54.2", + "@mariozechner/pi-tui": "^0.54.2", + "@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" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=20.0.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "^0.3.2" + } + }, + "node_modules/@mariozechner/pi-tui": { + "version": "0.54.2", + "resolved": "https://registry.npmjs.org/@mariozechner/pi-tui/-/pi-tui-0.54.2.tgz", + "integrity": "sha512-ekk7x69qvESE7NMRO+VIduVV1T/kH+Ghroh8yqZ1y0IF4U85nzZ4jwV8JECuiGUvtFE0MMwWfJc9KdEur4xF6A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/mime-types": "^2.1.4", + "chalk": "^5.5.0", + "get-east-asian-width": "^1.3.0", + "koffi": "^2.9.0", + "marked": "^15.0.12", + "mime-types": "^3.0.1" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@mistralai/mistralai": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-1.10.0.tgz", + "integrity": "sha512-tdIgWs4Le8vpvPiUEWne6tK0qbVc+jMenujnvTqOjogrJUsCSQhus0tHTU1avDDh5//Rq2dFgP9mWRAdIEoBqg==", + "dev": true, + "dependencies": { + "zod": "^3.20.0", + "zod-to-json-schema": "^3.24.1" + } + }, + "node_modules/@mistralai/mistralai/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/codegen": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.4.tgz", + "integrity": "sha512-YyFaikqM5sH0ziFZCN3xDC7zeGaB/d0IUb9CATugHWbd1FRFwWwt4ld4OYMPWu5a3Xe01mGAULCdqhMlPl29Jg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.0.tgz", + "integrity": "sha512-j9ednRT81vYJ9OfVuXG6ERSTdEL1xVsNgqpkxMsbIabzSo3goCjDIveeGv5d03om39ML71RdmrGNjG5SReBP/Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.0.tgz", + "integrity": "sha512-lljVXpqXebpsijW71PZaCYeIcE5on1w5DlQy5WH6GLbFryLUrBD4932W/E2BSpfRJWseIL4v/KPgBFxDOIdKpQ==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1", + "@protobufjs/inquire": "^1.1.0" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/inquire": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/inquire/-/inquire-1.1.0.tgz", + "integrity": "sha512-kdSefcPdruJiFMVSbn801t4vFK7KB/5gd2fYvrxhuJYg8ILrmn9SKSX2tZdV6V+ksulWqS7aXjBcRXl3wHoD9Q==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/utf8": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.0.tgz", + "integrity": "sha512-Vvn3zZrhQZkkBE8LSuW3em98c0FwgO4nxzv6OdSxPKJIEKY2bGbHn+mhGIPerzI4twdxaP8/0+06HBpwf345Lw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@sinclair/typebox": { + "version": "0.34.49", + "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.34.49.tgz", + "integrity": "sha512-brySQQs7Jtn0joV8Xh9ZV/hZb9Ozb0pmazDIASBkYKCjXrXU3mpcFahmK/z4YDhGkQvP9mWJbVyahdtU5wQA+A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@smithy/config-resolver": { + "version": "4.4.14", + "resolved": "https://registry.npmjs.org/@smithy/config-resolver/-/config-resolver-4.4.14.tgz", + "integrity": "sha512-N55f8mPEccpzKetUagdvmAy8oohf0J5cuj9jLI1TaSceRlq0pJsIZepY3kmAXAhyxqXPV6hDerDQhqQPKWgAoQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-config-provider": "^4.2.2", + "@smithy/util-endpoints": "^3.3.4", + "@smithy/util-middleware": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/core": { + "version": "3.23.14", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.23.14.tgz", + "integrity": "sha512-vJ0IhpZxZAkFYOegMKSrxw7ujhhT2pass/1UEcZ4kfl5srTAqtPU5I7MdYQoreVas3204ykCiNhY1o7Xlz6Yyg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-body-length-browser": "^4.2.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-stream": "^4.5.22", + "@smithy/util-utf8": "^4.2.2", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.2.13.tgz", + "integrity": "sha512-wboCPijzf6RJKLOvnjDAiBxGSmSnGXj35o5ZAWKDaHa/cvQ5U3ZJ13D4tMCE8JG4dxVAZFy/P0x/V9CwwdfULQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-codec": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-codec/-/eventstream-codec-4.2.13.tgz", + "integrity": "sha512-vYahwBAtRaAcFbOmE9aLr12z7RiHYDSLcnogSdxfm7kKfsNa3wH+NU5r7vTeB5rKvLsWyPjVX8iH94brP7umiQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.0", + "@smithy/util-hex-encoding": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-browser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-browser/-/eventstream-serde-browser-4.2.13.tgz", + "integrity": "sha512-wwybfcOX0tLqCcBP378TIU9IqrDuZq/tDV48LlZNydMpCnqnYr+hWBAYbRE+rFFf/p7IkDJySM3bgiMKP2ihPg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-config-resolver": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-config-resolver/-/eventstream-serde-config-resolver-4.3.13.tgz", + "integrity": "sha512-ied1lO559PtAsMJzg2TKRlctLnEi1PfkNeMMpdwXDImk1zV9uvS/Oxoy/vcy9uv1GKZAjDAB5xT6ziE9fzm5wA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-node": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-node/-/eventstream-serde-node-4.2.13.tgz", + "integrity": "sha512-hFyK+ORJrxAN3RYoaD6+gsGDQjeix8HOEkosoajvXYZ4VeqonM3G4jd9IIRm/sWGXUKmudkY9KdYjzosUqdM8A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-serde-universal": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/eventstream-serde-universal": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/eventstream-serde-universal/-/eventstream-serde-universal-4.2.13.tgz", + "integrity": "sha512-kRrq4EKLGeOxhC2CBEhRNcu1KSzNJzYY7RK3S7CxMPgB5dRrv55WqQOtRwQxQLC04xqORFLUgnDlc6xrNUULaA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/eventstream-codec": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.3.16", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.3.16.tgz", + "integrity": "sha512-nYDRUIvNd4mFmuXraRWt6w5UsZTNqtj4hXJA/iiOD4tuseIdLP9Lq38teH/SZTcIFCa2f+27o7hYpIsWktJKEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/hash-node": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/hash-node/-/hash-node-4.2.13.tgz", + "integrity": "sha512-4/oy9h0jjmY80a2gOIo75iLl8TOPhmtx4E2Hz+PfMjvx/vLtGY4TMU/35WRyH2JHPfT5CVB38u4JRow7gnmzJA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/invalid-dependency": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/invalid-dependency/-/invalid-dependency-4.2.13.tgz", + "integrity": "sha512-jvC0RB/8BLj2SMIkY0Npl425IdnxZJxInpZJbu563zIRnVjpDMXevU3VMCRSabaLB0kf/eFIOusdGstrLJ8IDg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-4.2.2.tgz", + "integrity": "sha512-n6rQ4N8Jj4YTQO3YFrlgZuwKodf4zUFs7EJIWH86pSCWBaAtAGBFfCM7Wx6D2bBJ2xqFNxGBSrUWswT3M0VJow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-content-length": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-content-length/-/middleware-content-length-4.2.13.tgz", + "integrity": "sha512-IPMLm/LE4AZwu6qiE8Rr8vJsWhs9AtOdySRXrOM7xnvclp77Tyh7hMs/FRrMf26kgIe67vFJXXOSmVxS7oKeig==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-endpoint": { + "version": "4.4.29", + "resolved": "https://registry.npmjs.org/@smithy/middleware-endpoint/-/middleware-endpoint-4.4.29.tgz", + "integrity": "sha512-R9Q/58U+qBiSARGWbAbFLczECg/RmysRksX6Q8BaQEpt75I7LI6WGDZnjuC9GXSGKljEbA7N118LhGaMbfrTXw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/middleware-serde": "^4.2.17", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "@smithy/url-parser": "^4.2.13", + "@smithy/util-middleware": "^4.2.13", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-retry": { + "version": "4.5.1", + "resolved": "https://registry.npmjs.org/@smithy/middleware-retry/-/middleware-retry-4.5.1.tgz", + "integrity": "sha512-/zY+Gp7Qj2D2hVm3irkCyONER7E9MiX3cUUm/k2ZmhkzZkrPgwVS4aJ5NriZUEN/M0D1hhjrgjUmX04HhRwdWA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/service-error-classification": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-retry": "^4.3.1", + "@smithy/uuid": "^1.1.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-serde": { + "version": "4.2.17", + "resolved": "https://registry.npmjs.org/@smithy/middleware-serde/-/middleware-serde-4.2.17.tgz", + "integrity": "sha512-0T2mcaM6v9W1xku86Dk0bEW7aEseG6KenFkPK98XNw0ZhOqOiD1MrMsdnQw9QsL3/Oa85T53iSMlm0SZdSuIEQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/middleware-stack": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/middleware-stack/-/middleware-stack-4.2.13.tgz", + "integrity": "sha512-g72jN/sGDLyTanrCLH9fhg3oysO3f7tQa6eWWsMyn2BiYNCgjF24n4/I9wff/5XidFvjj9ilipAoQrurTUrLvw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-config-provider": { + "version": "4.3.13", + "resolved": "https://registry.npmjs.org/@smithy/node-config-provider/-/node-config-provider-4.3.13.tgz", + "integrity": "sha512-iGxQ04DsKXLckbgnX4ipElrOTk+IHgTyu0q0WssZfYhDm9CQWHmu6cOeI5wmWRxpXbBDhIIfXMWz5tPEtcVqbw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.13", + "@smithy/shared-ini-file-loader": "^4.4.8", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.5.2.tgz", + "integrity": "sha512-/oD7u8M0oj2ZTFw7GkuuHWpIxtWdLlnyNkbrWcyVYhd5RJNDuczdkb0wfnQICyNFrVPlr8YHOhamjNy3zidhmA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/protocol-http": "^5.3.13", + "@smithy/querystring-builder": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/property-provider": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/property-provider/-/property-provider-4.2.13.tgz", + "integrity": "sha512-bGzUCthxRmezuxkbu9wD33wWg9KX3hJpCXpQ93vVkPrHn9ZW6KNNdY5xAUWNuRCwQ+VyboFuWirG1lZhhkcyRQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/protocol-http": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/protocol-http/-/protocol-http-5.3.13.tgz", + "integrity": "sha512-+HsmuJUF4u8POo6s8/a2Yb/AQ5t/YgLovCuHF9oxbocqv+SZ6gd8lC2duBFiCA/vFHoHQhoq7QjqJqZC6xOxxg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-builder": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-builder/-/querystring-builder-4.2.13.tgz", + "integrity": "sha512-tG4aOYFCZdPMjbgfhnIQ322H//ojujldp1SrHPHpBSb3NqgUp3dwiUGRJzie87hS1DYwWGqDuPaowoDF+rYCbQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "@smithy/util-uri-escape": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/querystring-parser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/querystring-parser/-/querystring-parser-4.2.13.tgz", + "integrity": "sha512-hqW3Q4P+CDzUyQ87GrboGMeD7XYNMOF+CuTwu936UQRB/zeYn3jys8C3w+wMkDfY7CyyyVwZQ5cNFoG0x1pYmA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/service-error-classification": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/service-error-classification/-/service-error-classification-4.2.13.tgz", + "integrity": "sha512-a0s8XZMfOC/qpqq7RCPvJlk93rWFrElH6O++8WJKz0FqnA4Y7fkNi/0mnGgSH1C4x6MFsuBA8VKu4zxFrMe5Vw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/shared-ini-file-loader": { + "version": "4.4.8", + "resolved": "https://registry.npmjs.org/@smithy/shared-ini-file-loader/-/shared-ini-file-loader-4.4.8.tgz", + "integrity": "sha512-VZCZx2bZasxdqxVgEAhREvDSlkatTPnkdWy1+Kiy8w7kYPBosW0V5IeDwzDUMvWBt56zpK658rx1cOBFOYaPaw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.3.13", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.3.13.tgz", + "integrity": "sha512-YpYSyM0vMDwKbHD/JA7bVOF6kToVRpa+FM5ateEVRpsTNu564g1muBlkTubXhSKKYXInhpADF46FPyrZcTLpXg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-middleware": "^4.2.13", + "@smithy/util-uri-escape": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/smithy-client": { + "version": "4.12.9", + "resolved": "https://registry.npmjs.org/@smithy/smithy-client/-/smithy-client-4.12.9.tgz", + "integrity": "sha512-ovaLEcTU5olSeHcRXcxV6viaKtpkHZumn6Ps0yn7dRf2rRSfy794vpjOtrWDO0d1auDSvAqxO+lyhERSXQ03EQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.23.14", + "@smithy/middleware-endpoint": "^4.4.29", + "@smithy/middleware-stack": "^4.2.13", + "@smithy/protocol-http": "^5.3.13", + "@smithy/types": "^4.14.0", + "@smithy/util-stream": "^4.5.22", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.14.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.0.tgz", + "integrity": "sha512-OWgntFLW88kx2qvf/c/67Vno1yuXm/f9M7QFAtVkkO29IJXGBIg0ycEaBTH0kvCtwmvZxRujrgP5a86RvsXJAQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/url-parser": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/url-parser/-/url-parser-4.2.13.tgz", + "integrity": "sha512-2G03yoboIRZlZze2+PT4GZEjgwQsJjUgn6iTsvxA02bVceHR6vp4Cuk7TUnPFWKF+ffNUk3kj4COwkENS2K3vw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/querystring-parser": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-base64": { + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/@smithy/util-base64/-/util-base64-4.3.2.tgz", + "integrity": "sha512-XRH6b0H/5A3SgblmMa5ErXQ2XKhfbQB+Fm/oyLZ2O2kCUrwgg55bU0RekmzAhuwOjA9qdN5VU2BprOvGGUkOOQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-browser": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-browser/-/util-body-length-browser-4.2.2.tgz", + "integrity": "sha512-JKCrLNOup3OOgmzeaKQwi4ZCTWlYR5H4Gm1r2uTMVBXoemo1UEghk5vtMi1xSu2ymgKVGW631e2fp9/R610ZjQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-body-length-node": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/@smithy/util-body-length-node/-/util-body-length-node-4.2.3.tgz", + "integrity": "sha512-ZkJGvqBzMHVHE7r/hcuCxlTY8pQr1kMtdsVPs7ex4mMU+EAbcXppfo5NmyxMYi2XU49eqaz56j2gsk4dHHPG/g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-4.2.2.tgz", + "integrity": "sha512-FDXD7cvUoFWwN6vtQfEta540Y/YBe5JneK3SoZg9bThSoOAC/eGeYEua6RkBgKjGa/sz6Y+DuBZj3+YEY21y4Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/is-array-buffer": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-config-provider": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-config-provider/-/util-config-provider-4.2.2.tgz", + "integrity": "sha512-dWU03V3XUprJwaUIFVv4iOnS1FC9HnMHDfUrlNDSh4315v0cWyaIErP8KiqGVbf5z+JupoVpNM7ZB3jFiTejvQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-browser": { + "version": "4.3.45", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-browser/-/util-defaults-mode-browser-4.3.45.tgz", + "integrity": "sha512-ag9sWc6/nWZAuK3Wm9KlFJUnRkXLrXn33RFjIAmCTFThqLHY+7wCst10BGq56FxslsDrjhSie46c8OULS+BiIw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-defaults-mode-node": { + "version": "4.2.49", + "resolved": "https://registry.npmjs.org/@smithy/util-defaults-mode-node/-/util-defaults-mode-node-4.2.49.tgz", + "integrity": "sha512-jlN6vHwE8gY5AfiFBavtD3QtCX2f7lM3BKkz7nFKSNfFR5nXLXLg6sqXTJEEyDwtxbztIDBQCfjsGVXlIru2lQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/config-resolver": "^4.4.14", + "@smithy/credential-provider-imds": "^4.2.13", + "@smithy/node-config-provider": "^4.3.13", + "@smithy/property-provider": "^4.2.13", + "@smithy/smithy-client": "^4.12.9", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-endpoints": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/@smithy/util-endpoints/-/util-endpoints-3.3.4.tgz", + "integrity": "sha512-BKoR/ubPp9KNKFxPpg1J28N1+bgu8NGAtJblBP7yHy8yQPBWhIAv9+l92SlQLpolGm71CVO+btB60gTgzT0wog==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/node-config-provider": "^4.3.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-hex-encoding": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-hex-encoding/-/util-hex-encoding-4.2.2.tgz", + "integrity": "sha512-Qcz3W5vuHK4sLQdyT93k/rfrUwdJ8/HZ+nMUOyGdpeGA1Wxt65zYwi3oEl9kOM+RswvYq90fzkNDahPS8K0OIg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-middleware": { + "version": "4.2.13", + "resolved": "https://registry.npmjs.org/@smithy/util-middleware/-/util-middleware-4.2.13.tgz", + "integrity": "sha512-GTooyrlmRTqvUen4eK7/K1p6kryF7bnDfq6XsAbIsf2mo51B/utaH+XThY6dKgNCWzMAaH/+OLmqaBuLhLWRow==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-retry": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/@smithy/util-retry/-/util-retry-4.3.1.tgz", + "integrity": "sha512-FwmicpgWOkP5kZUjN3y+3JIom8NLGqSAJBeoIgK0rIToI817TEBHCrd0A2qGeKQlgDeP+Jzn4i0H/NLAXGy9uQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/service-error-classification": "^4.2.13", + "@smithy/types": "^4.14.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-stream": { + "version": "4.5.22", + "resolved": "https://registry.npmjs.org/@smithy/util-stream/-/util-stream-4.5.22.tgz", + "integrity": "sha512-3H8iq/0BfQjUs2/4fbHZ9aG9yNzcuZs24LPkcX1Q7Z+qpqaGM8+qbGmE8zo9m2nCRgamyvS98cHdcWvR6YUsew==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/fetch-http-handler": "^5.3.16", + "@smithy/node-http-handler": "^4.5.2", + "@smithy/types": "^4.14.0", + "@smithy/util-base64": "^4.3.2", + "@smithy/util-buffer-from": "^4.2.2", + "@smithy/util-hex-encoding": "^4.2.2", + "@smithy/util-utf8": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-uri-escape": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-uri-escape/-/util-uri-escape-4.2.2.tgz", + "integrity": "sha512-2kAStBlvq+lTXHyAZYfJRb/DfS3rsinLiwb+69SstC9Vb0s9vNWkRwpnj918Pfi85mzi42sOqdV72OLxWAISnw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-utf8": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-4.2.2.tgz", + "integrity": "sha512-75MeYpjdWRe8M5E3AW0O4Cx3UadweS+cwdXjwYGBW5h/gxxnbeZ877sLPX/ZJA9GVTlL/qG0dXP29JWFCD1Ayw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/util-buffer-from": "^4.2.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/uuid": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@smithy/uuid/-/uuid-1.1.2.tgz", + "integrity": "sha512-O/IEdcCUKkubz60tFbGA7ceITTAJsty+lBjNoorP4Z6XRqaFb/OjQjZODophEcuq68nKm6/0r+6/lLQ+XVpk8g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@tokenizer/inflate": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@tokenizer/inflate/-/inflate-0.4.1.tgz", + "integrity": "sha512-2mAv+8pkG6GIZiF1kNg1jAjh27IDxEPKwdGul3snfztFerfPGI1LjDezZp3i7BElXompqEtPmoPx6c2wgtWsOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "debug": "^4.4.3", + "token-types": "^6.1.1" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/@tokenizer/token": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@tokenizer/token/-/token-0.3.0.tgz", + "integrity": "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tootallnate/quickjs-emscripten": { + "version": "0.23.0", + "resolved": "https://registry.npmjs.org/@tootallnate/quickjs-emscripten/-/quickjs-emscripten-0.23.0.tgz", + "integrity": "sha512-C5Mc6rdnsaJDjO3UpGW/CQTHtCKaYlScZTly4JIu97Jxo/odCiH0ITnDXSJPTOrEKk/ycSZ0AOgTmkDtkOsvIA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/mime-types": { + "version": "2.1.4", + "resolved": "https://registry.npmjs.org/@types/mime-types/-/mime-types-2.1.4.tgz", + "integrity": "sha512-lfU4b34HOri+kAY5UheuFMWPDOI+OPceBSHZKp69gEyTL/mmJ4cnU6Y/rlme3UL3GyOn6Y42hyIEw0/q8sWx5w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.6.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.6.0.tgz", + "integrity": "sha512-+qIYRKdNYJwY3vRCZMdJbPLJAtGjQBudzZzdzwQYkEPQd+PJGixUL5QfvCLDaULoLv+RhT3LDkwEfKaAkgSmNQ==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.19.0" + } + }, + "node_modules/@types/node-fetch": { + "version": "2.6.13", + "resolved": "https://registry.npmjs.org/@types/node-fetch/-/node-fetch-2.6.13.tgz", + "integrity": "sha512-QGpRVpzSaUs30JBSGPjOg4Uveu384erbHBoT1zeONvyCfwQxIkUshLAOqN/k9EjGviPRmWTTe6aH2qySWKTVSw==", + "license": "MIT", + "dependencies": { + "@types/node": "*", + "form-data": "^4.0.4" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/abort-controller": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", + "integrity": "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg==", + "license": "MIT", + "dependencies": { + "event-target-shim": "^5.0.0" + }, + "engines": { + "node": ">=6.5" + } + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/agentkeepalive": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/agentkeepalive/-/agentkeepalive-4.6.0.tgz", + "integrity": "sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==", + "license": "MIT", + "dependencies": { + "humanize-ms": "^1.2.1" + }, + "engines": { + "node": ">= 8.0.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-3.0.1.tgz", + "integrity": "sha512-8iUql50EUR+uUcdRQ3HDqa6EVyo3docL8g5WJ3FNcWmu62IbkGUue/pEyLBW8VGKKucTPgqeks4fIU1DA4yowQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/ast-types": { + "version": "0.13.4", + "resolved": "https://registry.npmjs.org/ast-types/-/ast-types-0.13.4.tgz", + "integrity": "sha512-x1FCFnFifvYDDzTaLII71vG5uvDwgtmDTEVWAxrgeiR8VjMONcCXJx7E+USjDtHlwFmt9MysbqgF9b9Vjr6w+w==", + "dev": true, + "license": "MIT", + "dependencies": { + "tslib": "^2.0.1" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/asynckit": { + "version": "0.4.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz", + "integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==", + "license": "MIT" + }, + "node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/basic-ftp": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/basic-ftp/-/basic-ftp-5.2.2.tgz", + "integrity": "sha512-1tDrzKsdCg70WGvbFss/ulVAxupNauGnOlgpyjKzeQxzyllBLS0CGLV7tjIXTK3ZQA9/FBEm9qyFFN1bciA6pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/brace-expansion": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.5.tgz", + "integrity": "sha512-VZznLgtwhn+Mact9tfiwx64fA9erHH/MCXEUfB/0bX/6Fz6ny5EGTXYltMocqg4xFAQZtnO3DHWWXi8RiuN7cQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.17.0 || ^14.13 || >=16.0.0" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cli-highlight": { + "version": "2.1.11", + "resolved": "https://registry.npmjs.org/cli-highlight/-/cli-highlight-2.1.11.tgz", + "integrity": "sha512-9KDcoEVwyUXrjcJNvHD0NFc/hiwe/WPVYIleQh2O1N2Zro5gWJZ/K+3DGn8w8P/F6FxOgzyC5bxDyHIgCSPhGg==", + "dev": true, + "license": "ISC", + "dependencies": { + "chalk": "^4.0.0", + "highlight.js": "^10.7.1", + "mz": "^2.4.0", + "parse5": "^5.1.1", + "parse5-htmlparser2-tree-adapter": "^6.0.0", + "yargs": "^16.0.0" + }, + "bin": { + "highlight": "bin/highlight" + }, + "engines": { + "node": ">=8.0.0", + "npm": ">=5.0.0" + } + }, + "node_modules/cli-highlight/node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/combined-stream": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz", + "integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==", + "license": "MIT", + "dependencies": { + "delayed-stream": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/degenerator": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/degenerator/-/degenerator-5.0.1.tgz", + "integrity": "sha512-TllpMR/t0M5sqCXfj85i4XaAzxmS5tVA16dqvdkMwGmzI+dXLXnw3J+3Vdv7VKw+ThlTMboK6i9rnZ6Nntj5CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "ast-types": "^0.13.4", + "escodegen": "^2.1.0", + "esprima": "^4.0.1" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/delayed-stream": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz", + "integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==", + "license": "MIT", + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true, + "license": "MIT" + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-set-tostringtag": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/es-set-tostringtag/-/es-set-tostringtag-2.1.0.tgz", + "integrity": "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA==", + "license": "MIT", + "dependencies": { + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.6", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/esbuild": { + "version": "0.27.7", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.7.tgz", + "integrity": "sha512-IxpibTjyVnmrIQo5aqNpCgoACA/dTKLTlhMHihVHhdkxKyPO1uBBthumT0rdHmcsk9uMonIWS0m4FljWzILh3w==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.7", + "@esbuild/android-arm": "0.27.7", + "@esbuild/android-arm64": "0.27.7", + "@esbuild/android-x64": "0.27.7", + "@esbuild/darwin-arm64": "0.27.7", + "@esbuild/darwin-x64": "0.27.7", + "@esbuild/freebsd-arm64": "0.27.7", + "@esbuild/freebsd-x64": "0.27.7", + "@esbuild/linux-arm": "0.27.7", + "@esbuild/linux-arm64": "0.27.7", + "@esbuild/linux-ia32": "0.27.7", + "@esbuild/linux-loong64": "0.27.7", + "@esbuild/linux-mips64el": "0.27.7", + "@esbuild/linux-ppc64": "0.27.7", + "@esbuild/linux-riscv64": "0.27.7", + "@esbuild/linux-s390x": "0.27.7", + "@esbuild/linux-x64": "0.27.7", + "@esbuild/netbsd-arm64": "0.27.7", + "@esbuild/netbsd-x64": "0.27.7", + "@esbuild/openbsd-arm64": "0.27.7", + "@esbuild/openbsd-x64": "0.27.7", + "@esbuild/openharmony-arm64": "0.27.7", + "@esbuild/sunos-x64": "0.27.7", + "@esbuild/win32-arm64": "0.27.7", + "@esbuild/win32-ia32": "0.27.7", + "@esbuild/win32-x64": "0.27.7" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/escodegen": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/escodegen/-/escodegen-2.1.0.tgz", + "integrity": "sha512-2NlIDTwUWJN0mRPQOdtQBzbUHvdGY2P1VXSyU83Q3xKxM7WHX2Ql8dKq782Q9TgQUNOLEzEYu9bzLNj1q88I5w==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esprima": "^4.0.1", + "estraverse": "^5.2.0", + "esutils": "^2.0.2" + }, + "bin": { + "escodegen": "bin/escodegen.js", + "esgenerate": "bin/esgenerate.js" + }, + "engines": { + "node": ">=6.0" + }, + "optionalDependencies": { + "source-map": "~0.6.1" + } + }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "dev": true, + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esutils": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", + "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/event-target-shim": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/event-target-shim/-/event-target-shim-5.0.1.tgz", + "integrity": "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fast-xml-builder": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.1.4.tgz", + "integrity": "sha512-f2jhpN4Eccy0/Uz9csxh3Nu6q4ErKxf0XIsasomfOihuSUa3/xw6w8dnOtCDgEItQFJG8KyXPzQXzcODDrrbOg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.1.3" + } + }, + "node_modules/fast-xml-parser": { + "version": "5.5.8", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.5.8.tgz", + "integrity": "sha512-Z7Fh2nVQSb2d+poDViM063ix2ZGt9jmY1nWhPfHBOK2Hgnb/OW3P4Et3P/81SEej0J7QbWtJqxO05h8QYfK7LQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "fast-xml-builder": "^1.1.4", + "path-expression-matcher": "^1.2.0", + "strnum": "^2.2.0" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "paypal", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "dependencies": { + "node-domexception": "^1.0.0", + "web-streams-polyfill": "^3.0.3" + }, + "engines": { + "node": "^12.20 || >= 14.13" + } + }, + "node_modules/file-type": { + "version": "21.3.4", + "resolved": "https://registry.npmjs.org/file-type/-/file-type-21.3.4.tgz", + "integrity": "sha512-Ievi/yy8DS3ygGvT47PjSfdFoX+2isQueoYP1cntFW1JLYAuS4GD7NUPGg4zv2iZfV52uDyk5w5Z0TdpRS6Q1g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/inflate": "^0.4.1", + "strtok3": "^10.3.4", + "token-types": "^6.1.1", + "uint8array-extras": "^1.4.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sindresorhus/file-type?sponsor=1" + } + }, + "node_modules/form-data": { + "version": "4.0.5", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.5.tgz", + "integrity": "sha512-8RipRLol37bNs2bhoV67fiTEvdTrbMUYcFTiy3+wuuOnUog2QBHCZWXDRijWQfAkhBj2Uf5UnVaiWwA5vdd82w==", + "license": "MIT", + "dependencies": { + "asynckit": "^0.4.0", + "combined-stream": "^1.0.8", + "es-set-tostringtag": "^2.1.0", + "hasown": "^2.0.2", + "mime-types": "^2.1.12" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/form-data-encoder": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-1.7.2.tgz", + "integrity": "sha512-qfqtYan3rxrnCk1VYaA4H+Ms9xdpPqvLZa6xmMgFvhO32x7/3J/ExcTd6qpxM0vH2GdMI+poehyBZvqfMTto8A==", + "license": "MIT" + }, + "node_modules/form-data/node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/form-data/node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/formdata-node": { + "version": "4.4.1", + "resolved": "https://registry.npmjs.org/formdata-node/-/formdata-node-4.4.1.tgz", + "integrity": "sha512-0iirZp3uVDjVGt9p49aTaqjk84TrglENEDuqfdlZQ1roC9CWlPk6Avf8EEnZNcAqPonwkG35x4n3ww/1THYAeQ==", + "license": "MIT", + "dependencies": { + "node-domexception": "1.0.0", + "web-streams-polyfill": "4.0.0-beta.3" + }, + "engines": { + "node": ">= 12.20" + } + }, + "node_modules/formdata-node/node_modules/web-streams-polyfill": { + "version": "4.0.0-beta.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-4.0.0-beta.3.tgz", + "integrity": "sha512-QW95TCTaHmsYfHDybGMwO5IJIM93I/6vTRk+daHTWFPhwh+C8Cg7j7XyKrwrj8Ib6vYXe0ocYNrmzY4xAAN6ug==", + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "extend": "^3.0.2", + "https-proxy-agent": "^7.0.1", + "node-fetch": "^3.3.2" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "gaxios": "^7.0.0", + "google-logging-utils": "^1.0.0", + "json-bigint": "^1.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "license": "ISC", + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.5.0.tgz", + "integrity": "sha512-CQ+bEO+Tva/qlmw24dCejulK5pMzVnUOFOijVogd3KQs07HnRIgp8TGipvCCRT06xeYEbpbgwaCxglFyiuIcmA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "license": "MIT", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "license": "MIT", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-tsconfig": { + "version": "4.13.7", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-4.13.7.tgz", + "integrity": "sha512-7tN6rFgBlMgpBML5j8typ92BKFi2sFQvIdpAqLA2beia5avZDrMs0FLZiM5etShWq5irVyGcGMEA1jcDaK7A/Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, + "node_modules/get-uri": { + "version": "6.0.5", + "resolved": "https://registry.npmjs.org/get-uri/-/get-uri-6.0.5.tgz", + "integrity": "sha512-b1O07XYq8eRuVzBNgJLstU6FYc1tS6wnMtF1I1D9lE8LxZSOGZ7LhxN54yPP6mGw5f2CkXY2BQUL9Fx41qvcIg==", + "dev": true, + "license": "MIT", + "dependencies": { + "basic-ftp": "^5.0.2", + "data-uri-to-buffer": "^6.0.2", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/get-uri/node_modules/data-uri-to-buffer": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-6.0.2.tgz", + "integrity": "sha512-7hvf7/GW8e86rW0ptuwS3OcBGDjIi6SZva7hCyWC0yYry2cOPmLIjXAUHI6DK2HsnwJd9ifmt57i8eV2n4YNpw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "minimatch": "^10.2.2", + "minipass": "^7.1.3", + "path-scurry": "^2.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "base64-js": "^1.3.0", + "ecdsa-sig-formatter": "^1.0.11", + "gaxios": "^7.1.4", + "gcp-metadata": "8.1.2", + "google-logging-utils": "1.1.3", + "jws": "^4.0.0" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "license": "MIT", + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/hosted-git-info": { + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.2.tgz", + "integrity": "sha512-M422h7o/BR3rmCQ8UHi7cyyMqKltdP9Uo+J2fXK+RSAY+wTcKOIRyhTuKv4qn+DJf3g+PL890AzId5KZpX+CBg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/humanize-ms": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/humanize-ms/-/humanize-ms-1.2.1.tgz", + "integrity": "sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==", + "license": "MIT", + "dependencies": { + "ms": "^2.0.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/ip-address": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.1.0.tgz", + "integrity": "sha512-XXADHxXmvT9+CRxhXg56LJovE+bmWnEWB78LB83VZTprKTmaC5QfruXocxzTZ2Kl0DNwKuBdlIhjL8LeY8Sf8Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-equal-constant-time": "^1.0.1", + "ecdsa-sig-formatter": "1.0.11", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/koffi": { + "version": "2.15.6", + "resolved": "https://registry.npmjs.org/koffi/-/koffi-2.15.6.tgz", + "integrity": "sha512-WQBpM5uo74UQ17UpsFN+PUOrQQg4/nYdey4SGVluQun2drYYfePziLLWdSmFb4wSdWlJC1aimXQnjhPCheRKuw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "funding": { + "url": "https://liberapay.com/Koromix" + } + }, + "node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/lru-cache": { + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.3.5.tgz", + "integrity": "sha512-NxVFwLAnrd9i7KUBxC4DrUhmgjzOs+1Qm50D3oF1/oL+r1NpZ4gA7xvG0/zJ8evR7zIKn4vLf7qTNduWFtCrRw==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/marked": { + "version": "15.0.12", + "resolved": "https://registry.npmjs.org/marked/-/marked-15.0.12.tgz", + "integrity": "sha512-8dD6FusOQSrpv9Z1rdNMdlSgQOIP880DHqnohobOmYLElGEqAL/JvxvuxZO16r4HtjTlfPRDC1hbvxC9dPN2nA==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 18" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "license": "MIT", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/mime-db": { + "version": "1.54.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", + "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-3.0.2.tgz", + "integrity": "sha512-Lbgzdk0h4juoQ9fCKXW4by0UJqj+nOOrI9MJ1sSj4nI8aI2eo1qmvQEie4VD1glsS250n15LsWsYtCugiStS5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "^1.54.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" + } + }, + "node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "brace-expansion": "^5.0.5" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/netmask": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/netmask/-/netmask-2.1.1.tgz", + "integrity": "sha512-eonl3sLUha+S1GzTPxychyhnUzKyeQkZ7jLjKrBagJgPla13F+uQ71HgpFefyHgqrjEbCPkDArxYsjY8/+gLKA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/jimmywarting" + }, + { + "type": "github", + "url": "https://paypal.me/jimmywarting" + } + ], + "license": "MIT", + "engines": { + "node": ">=10.5.0" + } + }, + "node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "dev": true, + "license": "MIT", + "dependencies": { + "data-uri-to-buffer": "^4.0.0", + "fetch-blob": "^3.1.4", + "formdata-polyfill": "^4.0.10" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/node-fetch" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/openai": { + "version": "6.10.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.10.0.tgz", + "integrity": "sha512-ITxOGo7rO3XRMiKA5l7tQ43iNNu+iXGFAcf2t+aWVzzqRaS0i7m1K2BhxNdaveB+5eENhO0VY1FkiZzhBk4v3A==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "openai": "bin/cli" + }, + "peerDependencies": { + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, + "node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pac-proxy-agent": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", + "integrity": "sha512-TEB8ESquiLMc0lV8vcd5Ql/JAKAoyzHFXaStwjkzpOpC5Yv+pIzLfHvjTSdf3vpa2bMiUQrg9i6276yn8666aA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tootallnate/quickjs-emscripten": "^0.23.0", + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "get-uri": "^6.0.1", + "http-proxy-agent": "^7.0.0", + "https-proxy-agent": "^7.0.6", + "pac-resolver": "^7.0.1", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/pac-resolver": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/pac-resolver/-/pac-resolver-7.0.1.tgz", + "integrity": "sha512-5NPgf87AT2STgwa2ntRMr45jTKrYBGkVU36yT0ig/n/GMAa3oPqhZfIQ2kMEimReg0+t9kZViDVZ83qfVUlckg==", + "dev": true, + "license": "MIT", + "dependencies": { + "degenerator": "^5.0.0", + "netmask": "^2.0.2" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/parse5": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-5.1.1.tgz", + "integrity": "sha512-ugq4DFI0Ptb+WWjAdOK16+u/nHfiIrcE+sh8kZMaM0WllQKLI9rOUq6c2b7cwPkXdzfQESqvoqK6ug7U/Yyzug==", + "dev": true, + "license": "MIT" + }, + "node_modules/parse5-htmlparser2-tree-adapter": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-6.0.1.tgz", + "integrity": "sha512-qPuWvbLgvDGilKc5BoicRovlT4MtYT6JfJyBOMDsKoiT+GiuP5qyrPCnR9HcPECIJJmZh5jRndyNThnhhb/vlA==", + "dev": true, + "license": "MIT", + "dependencies": { + "parse5": "^6.0.1" + } + }, + "node_modules/parse5-htmlparser2-tree-adapter/node_modules/parse5": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-6.0.1.tgz", + "integrity": "sha512-Ofn/CTFzRGTTxwpNEs9PP93gXShHcTq255nzRYSKe8AkVpZY7e1fpmTfOyoIvjP5HG7Z2ZM7VS9PPhQGW2pOpw==", + "dev": true, + "license": "MIT" + }, + "node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "dev": true, + "license": "MIT" + }, + "node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "18 || 20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/playwright-core": { + "version": "1.59.1", + "resolved": "https://registry.npmjs.org/playwright-core/-/playwright-core-1.59.1.tgz", + "integrity": "sha512-HBV/RJg81z5BiiZ9yPzIiClYV/QMsDCKUyogwH9p3MCP6IYjUFu/MActgYAvK0oWyV9NlwM3GLBjADyWgydVyg==", + "license": "Apache-2.0", + "bin": { + "playwright-core": "cli.js" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/protobufjs": { + "version": "7.5.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.5.4.tgz", + "integrity": "sha512-CvexbZtbov6jW2eXAvLukXjXUW1TzFaivC46BpWc/3BpcCysb5Vffu+B3XHMm8lVEuy2Mm4XGex8hBSg1yapPg==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "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" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/proxy-agent": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/proxy-agent/-/proxy-agent-6.5.0.tgz", + "integrity": "sha512-TmatMXdr2KlRiA2CyDu8GqR8EjahTG3aY3nXjdzFyoZbmB8hrBsTyMezhULIXKnC0jpfjlmiZ3+EaCzoInSu/A==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "http-proxy-agent": "^7.0.1", + "https-proxy-agent": "^7.0.6", + "lru-cache": "^7.14.1", + "pac-proxy-agent": "^7.1.0", + "proxy-from-env": "^1.1.0", + "socks-proxy-agent": "^8.0.5" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/proxy-agent/node_modules/lru-cache": { + "version": "7.18.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz", + "integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=12" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true, + "license": "MIT" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT" + }, + "node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/smart-buffer": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/smart-buffer/-/smart-buffer-4.2.0.tgz", + "integrity": "sha512-94hK0Hh8rPqQl2xXc3HsaBoOXKV20MToPkcXvwbISWLEs+64sBq5kFgn2kJDHb1Pry9yrP0dxrCI9RRci7RXKg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks": { + "version": "2.8.7", + "resolved": "https://registry.npmjs.org/socks/-/socks-2.8.7.tgz", + "integrity": "sha512-HLpt+uLy/pxB+bum/9DzAgiKS8CX1EvbWxI4zlmgGCExImLdiad2iCwXT5Z4c9c3Eq8rP2318mPW2c+QbtjK8A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ip-address": "^10.0.1", + "smart-buffer": "^4.2.0" + }, + "engines": { + "node": ">= 10.0.0", + "npm": ">= 3.0.0" + } + }, + "node_modules/socks-proxy-agent": { + "version": "8.0.5", + "resolved": "https://registry.npmjs.org/socks-proxy-agent/-/socks-proxy-agent-8.0.5.tgz", + "integrity": "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "^4.3.4", + "socks": "^2.8.3" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "optional": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/std-env": { + "version": "3.10.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.10.0.tgz", + "integrity": "sha512-5GS12FdOZNliM5mAOxFRg7Ir0pWz8MdpYm6AY6VPkGpbA7ZzmbzNcBJQ0GPvvyWgcY7QAhCgf9Uy89I03faLkg==", + "dev": true, + "license": "MIT" + }, + "node_modules/steel-sdk": { + "version": "0.17.0", + "resolved": "https://registry.npmjs.org/steel-sdk/-/steel-sdk-0.17.0.tgz", + "integrity": "sha512-Uu+kxcMHYWsPR2qj18tHcs7G0UuNIH9S+iHS3iK/viT4sqnIj3oaSSfL2xxAkMzMdft+YTMCRRxOarRQ82P4Bw==", + "license": "Apache-2.0", + "dependencies": { + "@types/node": "^18.11.18", + "@types/node-fetch": "^2.6.4", + "abort-controller": "^3.0.0", + "agentkeepalive": "^4.2.1", + "form-data-encoder": "1.7.2", + "formdata-node": "^4.3.2", + "node-fetch": "^2.6.7" + } + }, + "node_modules/steel-sdk/node_modules/@types/node": { + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", + "license": "MIT", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/steel-sdk/node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/steel-sdk/node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", + "license": "MIT" + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "license": "MIT", + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strnum": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.2.3.tgz", + "integrity": "sha512-oKx6RUCuHfT3oyVjtnrmn19H1SiCqgJSg+54XqURKp5aCMbrXrhLjRN9TjuwMjiYstZ0MzDrHqkGZ5dFTKd+zg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/strtok3": { + "version": "10.3.5", + "resolved": "https://registry.npmjs.org/strtok3/-/strtok3-10.3.5.tgz", + "integrity": "sha512-ki4hZQfh5rX0QDLLkOCj+h+CVNkqmp/CMf8v8kZpkNVK6jGQooMytqzLZYUVYIZcFZ6yDB70EfD8POcFXiF5oA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@tokenizer/token": "^0.3.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/token-types": { + "version": "6.1.2", + "resolved": "https://registry.npmjs.org/token-types/-/token-types-6.1.2.tgz", + "integrity": "sha512-dRXchy+C0IgK8WPC6xvCHFRIWYUbqqdEIKPaKo/AcTUNzwLTK6AH7RjdLWsEZcAN/TBdtfUw3PYEgPr5VPr6ww==", + "dev": true, + "license": "MIT", + "dependencies": { + "@borewit/text-codec": "^0.2.1", + "@tokenizer/token": "^0.3.0", + "ieee754": "^1.2.1" + }, + "engines": { + "node": ">=14.16" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Borewit" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT" + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/tsx": { + "version": "4.21.0", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.21.0.tgz", + "integrity": "sha512-5C1sg4USs1lfG0GFb2RLXsdpXqBSEhAaA/0kPL01wxzpMqLILNxIxIOKiILz+cdg/pLnOUxFYOR5yhHU666wbw==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.27.0", + "get-tsconfig": "^4.7.5" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/uint8array-extras": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/uint8array-extras/-/uint8array-extras-1.5.0.tgz", + "integrity": "sha512-rvKSBiC5zqCCiDZ9kAOszZcDvdAHwwIKJG33Ykj43OKcWsnmcBRL09YTU4nOeHZ8Y2a7l1MgTd08SBe9A8Qj6A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/undici": { + "version": "7.24.8", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.24.8.tgz", + "integrity": "sha512-6KQ/+QxK49Z/p3HO6E5ZCZWNnCasyZLa5ExaVYyvPxUwKtbCPMKELJOqh7EqOle0t9cH/7d2TaaTRRa6Nhs4YQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=20.18.1" + } + }, + "node_modules/undici-types": { + "version": "7.19.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.19.2.tgz", + "integrity": "sha512-qYVnV5OEm2AW8cJMCpdV20CDyaN3g0AjDlOGf1OW4iaDEx8MwdtChUp4zu4H0VP3nDRF/8RKWH+IPp9uW0YGZg==", + "license": "MIT" + }, + "node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause" + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/ws": { + "version": "8.20.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.20.0.tgz", + "integrity": "sha512-sAt8BhgNbzCtgGbt2OxmpuryO63ZoDk/sqaB/znQm94T4fCEsy/yV+7CdC1kJhOU9lboAEU7R3kquuycDoibVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yaml": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.8.3.tgz", + "integrity": "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "license": "MIT", + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "license": "ISC", + "engines": { + "node": ">=10" + } + }, + "node_modules/yoctocolors": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/yoctocolors/-/yoctocolors-2.1.2.tgz", + "integrity": "sha512-CzhO+pFNo8ajLM2d2IW/R93ipy99LWjtwblvC1RsoSUMZgyLbYFr221TnSNT7GjGdYui6P459mw9JH/g/zW2ug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/zod": { + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", + "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "dev": true, + "license": "MIT", + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/extensions/steel-browser/package.json b/extensions/steel-browser/package.json new file mode 100644 index 0000000..e45c296 --- /dev/null +++ b/extensions/steel-browser/package.json @@ -0,0 +1,59 @@ +{ + "name": "@steel-experiments/pi-steel", + "version": "0.1.1", + "description": "Steel browser automation extension package for Pi", + "type": "module", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "files": [ + "dist", + "README.md", + "LICENSE" + ], + "exports": { + ".": { + "types": "./dist/index.d.ts", + "default": "./dist/index.js" + } + }, + "keywords": [ + "pi-package", + "pi-extension", + "steel", + "browser-automation" + ], + "license": "MIT", + "engines": { + "node": ">=20.0.0" + }, + "pi": { + "extensions": [ + "./dist/index.js" + ] + }, + "scripts": { + "build": "tsc -p tsconfig.json", + "typecheck": "tsc -p tsconfig.json --noEmit", + "test:tools": "node --import tsx --test tests/*.test.ts", + "test": "npm run typecheck && npm run build && npm run test:tools", + "prepublishOnly": "npm test && npm pack --dry-run" + }, + "dependencies": { + "playwright-core": "^1.58.2", + "steel-sdk": "^0.17.0" + }, + "peerDependencies": { + "@mariozechner/pi-coding-agent": "*", + "@sinclair/typebox": "*" + }, + "devDependencies": { + "@mariozechner/pi-coding-agent": "^0.54.2", + "@sinclair/typebox": "^0.34.48", + "@types/node": "^25.3.0", + "tsx": "^4.21.0", + "typescript": "^5.9.3" + }, + "overrides": { + "rimraf": "^6.0.1" + } +} diff --git a/extensions/steel-browser/src/index.ts b/extensions/steel-browser/src/index.ts new file mode 100644 index 0000000..42ce7f8 --- /dev/null +++ b/extensions/steel-browser/src/index.ts @@ -0,0 +1,103 @@ +import type { ExtensionAPI } from "@mariozechner/pi-coding-agent"; + +import { resolveSessionMode, type SteelSessionMode } from "./session-mode.js"; +import { SteelClient } from "./steel-client.js"; +import { clickTool } from "./tools/click.js"; +import { computerTool } from "./tools/computer.js"; +import { extractTool } from "./tools/extract.js"; +import { findElementsTool } from "./tools/find-elements.js"; +import { fillFormTool } from "./tools/fill-form.js"; +import { getTitleTool, getUrlTool, goBackTool } from "./tools/navigation.js"; +import { navigateTool } from "./tools/navigate.js"; +import { pdfTool } from "./tools/pdf.js"; +import { scrapeTool } from "./tools/scrape.js"; +import { screenshotTool } from "./tools/screenshot.js"; +import { scrollTool } from "./tools/scroll.js"; +import { pinSessionTool, releaseSessionTool } from "./tools/session-control.js"; +import { typeTool } from "./tools/type.js"; +import { waitTool } from "./tools/wait.js"; + +export default function steelExtension(pi: ExtensionAPI): void { + const steelClient = new SteelClient(); + const defaultSessionMode = resolveSessionMode(); + let sessionMode = defaultSessionMode; + let closingSessions: Promise | null = null; + + const closeSessions = async (reason: string) => { + if (!closingSessions) { + closingSessions = (async () => { + try { + await steelClient.closeAllSessions(); + } catch (error: unknown) { + // Cleanup failures should not break the main agent response path. + console.warn(`[steel] session cleanup failed (${reason})`, error); + } finally { + closingSessions = null; + } + })(); + } + + await closingSessions; + }; + + const sessionController = { + getDefaultSessionMode: () => defaultSessionMode, + getSessionMode: () => sessionMode, + setSessionMode: (mode: SteelSessionMode) => { + sessionMode = mode; + }, + closeSessions, + }; + + const tools = [ + navigateTool(steelClient), + scrapeTool(steelClient), + screenshotTool(steelClient), + pdfTool(steelClient), + clickTool(steelClient), + computerTool(steelClient), + findElementsTool(steelClient), + typeTool(steelClient), + fillFormTool(steelClient), + waitTool(steelClient), + extractTool(steelClient), + scrollTool(steelClient), + goBackTool(steelClient), + getUrlTool(steelClient), + getTitleTool(steelClient), + pinSessionTool(steelClient, sessionController), + releaseSessionTool(steelClient, sessionController), + ]; + + for (const tool of tools) { + pi.registerTool(tool); + } + + pi.on("turn_end", async () => { + if (sessionMode === "turn") { + await closeSessions("turn_end"); + } + }); + + pi.on("agent_end", async () => { + if (sessionMode === "agent") { + await closeSessions("agent_end"); + } + }); + + // Defensive cleanup for interactive session switches/forks. + pi.on("session_before_switch", async () => { + await closeSessions("session_before_switch"); + }); + + pi.on("session_shutdown", async () => { + await closeSessions("session_shutdown"); + }); + + const shutdownApi = pi as ExtensionAPI & { + onShutdown?: (handler: () => Promise | void) => void; + }; + shutdownApi.onShutdown?.(async () => { + await closeSessions("onShutdown"); + }); +} diff --git a/extensions/steel-browser/src/session-mode.ts b/extensions/steel-browser/src/session-mode.ts new file mode 100644 index 0000000..0b0c973 --- /dev/null +++ b/extensions/steel-browser/src/session-mode.ts @@ -0,0 +1,18 @@ +export type SteelSessionMode = "turn" | "agent" | "session"; + +export function resolveSessionMode(): SteelSessionMode { + const rawValue = process.env.STEEL_SESSION_MODE?.trim().toLowerCase(); + if (!rawValue) { + return "agent"; + } + + if (rawValue === "turn" || rawValue === "agent" || rawValue === "session") { + return rawValue; + } + + console.warn( + `[steel] unsupported STEEL_SESSION_MODE="${rawValue}", falling back to "agent"` + ); + return "agent"; +} + diff --git a/extensions/steel-browser/src/steel-client.ts b/extensions/steel-browser/src/steel-client.ts new file mode 100644 index 0000000..567f41b --- /dev/null +++ b/extensions/steel-browser/src/steel-client.ts @@ -0,0 +1,686 @@ +import fs from "node:fs"; +import os from "node:os"; +import path from "node:path"; +import Steel from "steel-sdk"; +import type { + CaptchaSolveResponse, + CaptchaStatusResponse, +} from "steel-sdk/resources/sessions"; +import { chromium, type Browser, type BrowserContext, type Page } from "playwright-core"; +import { toolError } from "./tools/tool-runtime.js"; + +type SessionCreateOptions = Steel.SessionCreateParams; +type SessionMetadata = Awaited>; + +type SessionGotoOptions = Parameters[1]; +type SessionWaitForSelectorOptions = Parameters[1]; +type SessionClickOptions = Parameters[1]; +type SessionTypeOptions = Parameters[2]; +type SessionScreenshotOptions = Parameters[0]; +type SessionPdfOptions = Parameters[0]; +type SessionComputerParams = Steel.SessionComputerParams; +type SessionComputerResponse = Steel.SessionComputerResponse; + +type SteelConfigFile = { + apiKey?: unknown; + browser?: { + apiUrl?: unknown; + } | null; +} | null; + +type ResolvedSteelRuntimeConfig = { + apiKey: string | null; + baseURL?: string; + baseURLOverridden: boolean; + viewerBaseURL?: string; +}; + +export interface LiveSteelSession { + id: string; + sessionViewerUrl: string; + debugUrl: string; + page: Page; + goto: (url: string, options?: SessionGotoOptions) => Promise; + goBack: (options?: Parameters[0]) => Promise; + back: (options?: Parameters[0]) => Promise; + url: () => string; + title: () => Promise; + waitForSelector: ( + selector: string, + options?: SessionWaitForSelectorOptions + ) => Promise; + click: (selector: string, options?: SessionClickOptions) => Promise; + fill: (selector: string, text: string) => Promise; + type: ( + selector: string, + text: string, + options?: SessionTypeOptions + ) => Promise; + evaluate: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + locator: (selector: string) => ReturnType; + content: () => Promise; + screenshot: (options?: SessionScreenshotOptions) => Promise; + pdf: (options?: SessionPdfOptions) => Promise; + computer: (body: SessionComputerParams) => Promise; + captchasStatus: () => Promise; + captchasSolve: () => Promise; +} + +type TrackedSession = { + metadata: SessionMetadata; + browser: Browser; + context: BrowserContext; + page: Page; + liveSession: LiveSteelSession; +}; + +export interface SteelClientOptions { + apiKey?: string | null; + baseURL?: string; + sessionTimeoutMs?: number; + sessionCreateOptions?: Partial; +} + +export interface SessionRefreshOptions { + useProxy?: boolean; + proxyUrl?: string | null; +} + +const TRUE_ENV_VALUES = new Set(["1", "true", "yes", "on"]); +const FALSE_ENV_VALUES = new Set(["0", "false", "no", "off"]); +const DEFAULT_STEEL_BASE_URL = "https://api.steel.dev"; +const DEFAULT_STEEL_APP_URL = "https://app.steel.dev"; + +function normalizeConfigDir(input: string | undefined): string { + const trimmed = input?.trim(); + if (trimmed) { + return trimmed; + } + + return path.join(os.homedir(), ".config", "steel"); +} + +function readSteelConfigFile(): SteelConfigFile { + const configPath = path.join( + normalizeConfigDir(process.env.STEEL_CONFIG_DIR), + "config.json" + ); + + try { + const contents = fs.readFileSync(configPath, "utf-8"); + const parsed = JSON.parse(contents) as SteelConfigFile; + if (!parsed || typeof parsed !== "object") { + return null; + } + return parsed; + } catch { + return null; + } +} + +function normalizeOptionalString(value: unknown): string | undefined { + if (typeof value !== "string") { + return undefined; + } + + const trimmed = value.trim(); + return trimmed || undefined; +} + +function normalizeSdkBaseURL(rawUrl: string): string { + const trimmed = rawUrl.trim().replace(/\/+$/, ""); + if (!trimmed) { + throw new Error("base URL must not be empty."); + } + + let parsed: URL; + try { + parsed = new URL(trimmed); + } catch (error: unknown) { + throw toolError( + "SteelClient initialization", + `Invalid Steel base URL: ${error instanceof Error ? error.message : "invalid URL"}` + ); + } + + if (!["http:", "https:"].includes(parsed.protocol)) { + throw toolError( + "SteelClient initialization", + "Steel base URL must use http or https." + ); + } + + const pathname = parsed.pathname.replace(/\/+$/, ""); + if (pathname === "/v1") { + parsed.pathname = ""; + } + + return parsed.toString().replace(/\/+$/, ""); +} + +function resolveViewerBaseURL(baseURL: string | undefined, overridden: boolean): string | undefined { + if (!overridden || !baseURL) { + return DEFAULT_STEEL_APP_URL; + } + + try { + const parsed = new URL(baseURL); + const host = parsed.hostname.toLowerCase(); + if ( + host === "api.steel.dev" || + host.endsWith(".steel.dev") + ) { + return DEFAULT_STEEL_APP_URL; + } + } catch { + return undefined; + } + + return undefined; +} + +function resolveSteelRuntimeConfig( + apiKeyOverride?: string | null, + baseURLOverride?: string +): ResolvedSteelRuntimeConfig { + const config = readSteelConfigFile(); + + const configApiKey = normalizeOptionalString(config?.apiKey); + const configBrowserApiUrl = normalizeOptionalString(config?.browser?.apiUrl); + + const explicitApiKey = normalizeOptionalString(apiKeyOverride ?? undefined); + const envApiKey = normalizeOptionalString(process.env.STEEL_API_KEY); + const resolvedApiKey = explicitApiKey ?? envApiKey ?? configApiKey ?? null; + + const explicitBaseURL = normalizeOptionalString(baseURLOverride); + const envBaseURL = normalizeOptionalString(process.env.STEEL_BASE_URL); + const envBrowserApiURL = normalizeOptionalString(process.env.STEEL_BROWSER_API_URL); + const envLocalApiURL = normalizeOptionalString(process.env.STEEL_LOCAL_API_URL); + const envApiURL = normalizeOptionalString(process.env.STEEL_API_URL); + + const rawBaseURL = + explicitBaseURL ?? + envBaseURL ?? + envBrowserApiURL ?? + envLocalApiURL ?? + configBrowserApiUrl ?? + envApiURL; + + const normalizedBaseURL = rawBaseURL + ? normalizeSdkBaseURL(rawBaseURL) + : undefined; + const baseURLOverridden = normalizedBaseURL !== undefined; + + if (!resolvedApiKey && !baseURLOverridden) { + throw toolError( + "SteelClient initialization", + "STEEL_API_KEY is required. Set it in the environment, run `steel login`, or configure a custom Steel base URL for self-hosted usage." + ); + } + + return { + apiKey: resolvedApiKey, + baseURL: normalizedBaseURL, + baseURLOverridden, + viewerBaseURL: resolveViewerBaseURL(normalizedBaseURL, baseURLOverridden), + }; +} + +function getSessionFieldString( + session: Record, + keys: readonly string[] +): string | undefined { + for (const key of keys) { + const value = session[key]; + if (typeof value === "string" && value.trim()) { + return value.trim(); + } + } + + return undefined; +} + +export function resolveSessionId(session: Record): string | undefined { + return getSessionFieldString(session, ["id", "sessionId"]); +} + +export function resolveSessionConnectURL(session: Record): string | undefined { + return getSessionFieldString(session, [ + "websocketUrl", + "wsUrl", + "connectUrl", + "cdpUrl", + "browserWSEndpoint", + "wsEndpoint", + ]); +} + +export function buildSessionConnectURL( + session: Record, + apiKey?: string | null +): string | undefined { + const rawConnectURL = resolveSessionConnectURL(session); + const sessionId = resolveSessionId(session); + + if (!rawConnectURL) { + if (!sessionId || !apiKey) { + return undefined; + } + return `wss://connect.steel.dev?apiKey=${encodeURIComponent(apiKey)}&sessionId=${encodeURIComponent(sessionId)}`; + } + + try { + const parsed = new URL(rawConnectURL); + if (apiKey && !parsed.searchParams.get("apiKey")) { + parsed.searchParams.set("apiKey", apiKey); + } + if (sessionId && !parsed.searchParams.get("sessionId")) { + parsed.searchParams.set("sessionId", sessionId); + } + return parsed.toString(); + } catch { + const params = new URLSearchParams(); + if (apiKey && !/(?:[?&])apiKey=/.test(rawConnectURL)) { + params.set("apiKey", apiKey); + } + if (sessionId && !/(?:[?&])sessionId=/.test(rawConnectURL)) { + params.set("sessionId", sessionId); + } + const query = params.toString(); + if (!query) { + return rawConnectURL; + } + const separator = rawConnectURL.includes("?") ? "&" : "?"; + return `${rawConnectURL}${separator}${query}`; + } +} + +export function resolveSessionViewerURL( + session: Record, + viewerBaseURL?: string +): string | undefined { + const explicit = getSessionFieldString(session, [ + "sessionViewerUrl", + "viewerUrl", + "liveViewUrl", + "debugUrl", + ]); + if (explicit) { + return explicit; + } + + const sessionId = resolveSessionId(session); + if (!sessionId || !viewerBaseURL) { + return undefined; + } + + return `${viewerBaseURL.replace(/\/+$/, "")}/sessions/${sessionId}`; +} + +export function sessionDetails(session: { + id: string; + sessionViewerUrl?: string | null; +}) { + return { + sessionId: session.id, + sessionViewerUrl: + typeof session.sessionViewerUrl === "string" + ? session.sessionViewerUrl + : "", + }; +} + +function parseBooleanEnv(name: string): boolean | undefined { + const raw = process.env[name]; + if (raw === undefined) { + return undefined; + } + + const normalized = raw.trim().toLowerCase(); + if (!normalized) { + return undefined; + } + if (TRUE_ENV_VALUES.has(normalized)) { + return true; + } + if (FALSE_ENV_VALUES.has(normalized)) { + return false; + } + + throw toolError( + "SteelClient initialization", + `${name} must be a boolean value (one of: ${[...TRUE_ENV_VALUES, ...FALSE_ENV_VALUES].join(", ")}).` + ); +} + +function parseProxyUrlEnv(name: string): string | undefined { + const raw = process.env[name]; + if (raw === undefined) { + return undefined; + } + + const trimmed = raw.trim(); + if (!trimmed) { + return undefined; + } + + try { + const parsed = new URL(trimmed); + if (!["http:", "https:"].includes(parsed.protocol)) { + throw new Error("proxy URL protocol must be http or https"); + } + return parsed.toString(); + } catch (error: unknown) { + throw toolError( + "SteelClient initialization", + `${name} is invalid: ${error instanceof Error ? error.message : "invalid URL"}` + ); + } +} + +function parseStringEnv(name: string): string | undefined { + const raw = process.env[name]; + if (raw === undefined) { + return undefined; + } + + const trimmed = raw.trim(); + return trimmed || undefined; +} + +function resolveSessionCreateOptionsFromEnv(): Partial { + const resolved: Partial = {}; + const solveCaptcha = parseBooleanEnv("STEEL_SOLVE_CAPTCHA"); + const useProxy = parseBooleanEnv("STEEL_USE_PROXY"); + const proxyUrl = parseProxyUrlEnv("STEEL_PROXY_URL"); + const headless = parseBooleanEnv("STEEL_SESSION_HEADLESS"); + const persistProfile = parseBooleanEnv("STEEL_SESSION_PERSIST_PROFILE"); + const useCredentials = parseBooleanEnv("STEEL_SESSION_CREDENTIALS"); + const region = parseStringEnv("STEEL_SESSION_REGION"); + const profileId = parseStringEnv("STEEL_SESSION_PROFILE_ID"); + const namespace = parseStringEnv("STEEL_SESSION_NAMESPACE"); + + if (solveCaptcha !== undefined) { + resolved.solveCaptcha = solveCaptcha; + } + if (useProxy !== undefined) { + resolved.useProxy = useProxy; + } + if (proxyUrl !== undefined) { + resolved.proxyUrl = proxyUrl; + } + if (headless !== undefined) { + resolved.headless = headless; + } + if (persistProfile !== undefined) { + resolved.persistProfile = persistProfile; + } + if (useCredentials) { + resolved.credentials = {}; + } + if (region !== undefined) { + resolved.region = region; + } + if (profileId !== undefined) { + resolved.profileId = profileId; + } + if (namespace !== undefined) { + resolved.namespace = namespace; + } + + return resolved; +} + +export class SteelClient { + private static readonly DEFAULT_SESSION_TIMEOUT_MS = 30 * 60 * 1000; + + private readonly client: Steel; + private readonly apiKey: string | null; + private readonly sessionTimeoutMs: number; + private readonly sessionCreateOptions: Partial; + private readonly viewerBaseURL?: string; + private currentSession: TrackedSession | null = null; + private readonly sessions = new Map(); + private creatingSession: Promise | null = null; + + constructor(apiKey?: string, options: SteelClientOptions = {}) { + const runtimeConfig = resolveSteelRuntimeConfig( + options.apiKey ?? apiKey, + options.baseURL + ); + const configuredTimeout = + options.sessionTimeoutMs === undefined + ? undefined + : Number(options.sessionTimeoutMs); + + const fallbackTimeout = Number.parseInt( + process.env.STEEL_SESSION_TIMEOUT_MS || "", + 10 + ); + + const normalizedConfiguredTimeout = + typeof configuredTimeout === "number" && + Number.isFinite(configuredTimeout) && + configuredTimeout > 0 + ? configuredTimeout + : undefined; + const normalizedFallbackTimeout = + Number.isFinite(fallbackTimeout) && fallbackTimeout > 0 + ? fallbackTimeout + : undefined; + const resolvedTimeout = + normalizedConfiguredTimeout ?? + normalizedFallbackTimeout ?? + SteelClient.DEFAULT_SESSION_TIMEOUT_MS; + + this.client = new Steel({ + steelAPIKey: runtimeConfig.apiKey, + baseURL: runtimeConfig.baseURL, + }); + this.apiKey = runtimeConfig.apiKey; + this.viewerBaseURL = runtimeConfig.viewerBaseURL; + this.sessionTimeoutMs = resolvedTimeout; + this.sessionCreateOptions = { + ...resolveSessionCreateOptionsFromEnv(), + ...(options.sessionCreateOptions ?? {}), + }; + } + + async getOrCreateSession(): Promise { + if (this.currentSession) { + return this.currentSession.liveSession; + } + + if (!this.creatingSession) { + this.creatingSession = this.createSession(); + } + + const tracked = await this.creatingSession; + return tracked.liveSession; + } + + getCurrentSessionId(): string | null { + return this.currentSession?.metadata.id ?? null; + } + + hasActiveSession(): boolean { + return this.currentSession !== null; + } + + isProxyConfigured(): boolean { + const { useProxy, proxyUrl } = this.sessionCreateOptions; + if (typeof proxyUrl === "string" && proxyUrl.trim().length > 0) { + return true; + } + if (typeof useProxy === "boolean") { + return useProxy; + } + return useProxy !== undefined; + } + + async refreshSession(options: SessionRefreshOptions = {}): Promise { + const currentSessionId = this.currentSession?.metadata.id; + if (currentSessionId) { + await this.closeSession(currentSessionId); + } + + this.creatingSession = this.createSession( + this.resolveSessionCreateOptions(options) + ); + const tracked = await this.creatingSession; + return tracked.liveSession; + } + + async closeSession(sessionId?: string): Promise { + const targetSessionId = sessionId ?? this.currentSession?.metadata.id; + if (!targetSessionId) { + return; + } + + const tracked = this.sessions.get(targetSessionId); + this.sessions.delete(targetSessionId); + + if (this.currentSession?.metadata.id === targetSessionId) { + this.currentSession = null; + } + + if (!tracked) { + return; + } + + await Promise.allSettled([ + tracked.browser.close(), + this.client.sessions.release(targetSessionId), + ]); + } + + async closeAllSessions(): Promise { + const trackedSessions = [...this.sessions.values()]; + const sessionIds = trackedSessions.map((tracked) => tracked.metadata.id); + this.sessions.clear(); + this.currentSession = null; + this.creatingSession = null; + + if (sessionIds.length === 0) { + return; + } + + await Promise.allSettled( + trackedSessions.map((tracked) => tracked.browser.close()) + ); + + const releaseResult = await Promise.allSettled( + sessionIds.map((sessionId) => this.client.sessions.release(sessionId)) + ); + + const allRejected = releaseResult.every((entry) => entry.status === "rejected"); + if (allRejected) { + await this.client.sessions.releaseAll(); + } + } + + private resolveSessionCreateOptions( + options: SessionRefreshOptions = {} + ): Partial { + const merged: Partial = { + ...this.sessionCreateOptions, + }; + + if (options.useProxy !== undefined) { + merged.useProxy = options.useProxy; + if (options.useProxy === false && options.proxyUrl === undefined) { + delete merged.proxyUrl; + } + } + + if (options.proxyUrl === null) { + delete merged.proxyUrl; + } else if (typeof options.proxyUrl === "string" && options.proxyUrl.trim()) { + merged.proxyUrl = options.proxyUrl.trim(); + } + + return merged; + } + + private async createSession( + createOptions: Partial = this.sessionCreateOptions + ): Promise { + try { + const session = await this.client.sessions.create({ + ...createOptions, + timeout: this.sessionTimeoutMs, + blockAds: true, + }); + + const websocketUrl = buildSessionConnectURL( + session as unknown as Record, + this.apiKey + ); + if (!websocketUrl) { + throw new Error("Steel session did not include a connect URL."); + } + + const browser = await chromium.connectOverCDP(websocketUrl); + const context = browser.contexts()[0] ?? (await browser.newContext()); + const page = context.pages()[0] ?? (await context.newPage()); + const liveSession = this.buildLiveSession(session, page); + + const tracked: TrackedSession = { + metadata: session, + browser, + context, + page, + liveSession, + }; + + this.sessions.set(session.id, tracked); + this.currentSession = tracked; + return tracked; + } catch (error: unknown) { + throw toolError("SteelClient session creation", error); + } finally { + this.creatingSession = null; + } + } + + private buildLiveSession( + session: SessionMetadata, + page: Page + ): LiveSteelSession { + const sessionId = + resolveSessionId(session as unknown as Record) ?? session.id; + + return { + id: sessionId, + sessionViewerUrl: + resolveSessionViewerURL( + session as unknown as Record, + this.viewerBaseURL + ) ?? "", + debugUrl: session.debugUrl || "", + page, + goto: (url, options) => page.goto(url, options), + goBack: (options) => page.goBack(options), + back: (options) => page.goBack(options), + url: () => page.url(), + title: () => page.title(), + waitForSelector: (selector, options) => + options + ? page.waitForSelector(selector, options) + : page.waitForSelector(selector), + click: (selector, options) => page.click(selector, options), + fill: (selector, text) => page.fill(selector, text), + type: (selector, text, options) => page.type(selector, text, options), + evaluate: (fn: (...args: any[]) => T, ...args: any[]) => + page.evaluate(fn, ...args), + locator: (selector: string) => page.locator(selector), + content: () => page.content(), + screenshot: (options) => page.screenshot(options), + pdf: (options) => page.pdf(options), + computer: (body) => this.client.sessions.computer(sessionId, body), + captchasStatus: () => this.client.sessions.captchas.status(sessionId), + captchasSolve: () => this.client.sessions.captchas.solve(sessionId), + }; + } + +} diff --git a/extensions/steel-browser/src/tools/captcha-guard.ts b/extensions/steel-browser/src/tools/captcha-guard.ts new file mode 100644 index 0000000..535cee8 --- /dev/null +++ b/extensions/steel-browser/src/tools/captcha-guard.ts @@ -0,0 +1,321 @@ +import { + emitProgress, + isAbortError, + sleepWithSignal, + throwIfAborted, + type ToolProgressUpdater, +} from "./tool-runtime.js"; + +const CAPTCHA_WAIT_MS_ENV = "STEEL_CAPTCHA_WAIT_MS"; +const CAPTCHA_MAX_RETRIES_ENV = "STEEL_CAPTCHA_MAX_RETRIES"; +const CAPTCHA_POLL_INTERVAL_MS_ENV = "STEEL_CAPTCHA_POLL_INTERVAL_MS"; + +const DEFAULT_CAPTCHA_WAIT_MS = 45_000; +const DEFAULT_CAPTCHA_MAX_RETRIES = 1; +const DEFAULT_CAPTCHA_POLL_INTERVAL_MS = 1_500; + +const MIN_CAPTCHA_WAIT_MS = 1_000; +const MAX_CAPTCHA_WAIT_MS = 180_000; +const MIN_CAPTCHA_POLL_INTERVAL_MS = 250; +const MAX_CAPTCHA_POLL_INTERVAL_MS = 10_000; +const MAX_CAPTCHA_RETRIES = 3; + +type CaptchaStatusEntry = { + isSolvingCaptcha?: boolean; + tasks?: unknown; +}; + +export type CaptchaAwareSession = { + id: string; + captchasStatus?: () => Promise; + captchasSolve?: () => Promise; +}; + +export type CaptchaRecoverySummary = { + triggered: boolean; + retries: number; + solveAttempts: number; + statusChecks: number; + waitTimedOut: boolean; +}; + +type CaptchaRecoveryOptions = { + session: CaptchaAwareSession; + context: string; + actionLabel: string; + onUpdate: ToolProgressUpdater; + operation: () => Promise; + signal?: AbortSignal; + shouldRetry?: (error: unknown) => boolean; +}; + +function parsePositiveInt(raw: string | undefined): number | null { + if (raw === undefined) { + return null; + } + + const value = raw.trim(); + if (!value) { + return null; + } + + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return null; + } + + return parsed; +} + +function resolveCaptchaWaitMs(): number { + const parsed = parsePositiveInt(process.env[CAPTCHA_WAIT_MS_ENV]); + if (parsed === null) { + return DEFAULT_CAPTCHA_WAIT_MS; + } + return Math.max(MIN_CAPTCHA_WAIT_MS, Math.min(parsed, MAX_CAPTCHA_WAIT_MS)); +} + +function resolveCaptchaMaxRetries(): number { + const parsed = parsePositiveInt(process.env[CAPTCHA_MAX_RETRIES_ENV]); + if (parsed === null) { + return DEFAULT_CAPTCHA_MAX_RETRIES; + } + return Math.max(0, Math.min(parsed, MAX_CAPTCHA_RETRIES)); +} + +function resolveCaptchaPollIntervalMs(): number { + const parsed = parsePositiveInt(process.env[CAPTCHA_POLL_INTERVAL_MS_ENV]); + if (parsed === null) { + return DEFAULT_CAPTCHA_POLL_INTERVAL_MS; + } + return Math.max( + MIN_CAPTCHA_POLL_INTERVAL_MS, + Math.min(parsed, MAX_CAPTCHA_POLL_INTERVAL_MS) + ); +} + +function normalizeErrorText(error: unknown): string { + if (error instanceof Error) { + return error.message.toLowerCase(); + } + if (typeof error === "string") { + return error.toLowerCase(); + } + return String(error ?? "").toLowerCase(); +} + +export function isCaptchaInterferenceError(error: unknown): boolean { + const message = normalizeErrorText(error); + return ( + message.includes("captcha") || + message.includes("hcaptcha") || + message.includes("recaptcha") || + message.includes("intercepts pointer events") + ); +} + +function normalizeCaptchaStatusEntries(value: unknown): CaptchaStatusEntry[] { + if (!Array.isArray(value)) { + return []; + } + + return value.filter( + (entry): entry is CaptchaStatusEntry => + typeof entry === "object" && entry !== null + ); +} + +function hasActiveCaptcha(entries: CaptchaStatusEntry[]): boolean { + for (const entry of entries) { + if (entry.isSolvingCaptcha) { + return true; + } + if (Array.isArray(entry.tasks) && entry.tasks.length > 0) { + return true; + } + } + return false; +} + +async function tryReadCaptchaStatus( + session: CaptchaAwareSession, + summary: CaptchaRecoverySummary, + signal: AbortSignal | undefined +): Promise { + throwIfAborted(signal); + if (typeof session.captchasStatus !== "function") { + return []; + } + const status = await session.captchasStatus(); + summary.statusChecks += 1; + return normalizeCaptchaStatusEntries(status); +} + +async function runCaptchaRecoveryStep( + session: CaptchaAwareSession, + context: string, + actionLabel: string, + onUpdate: ToolProgressUpdater, + summary: CaptchaRecoverySummary, + signal: AbortSignal | undefined +): Promise { + throwIfAborted(signal); + const waitMs = resolveCaptchaWaitMs(); + const pollIntervalMs = resolveCaptchaPollIntervalMs(); + const deadline = Date.now() + waitMs; + + let statusEntries: CaptchaStatusEntry[] = []; + try { + statusEntries = await tryReadCaptchaStatus(session, summary, signal); + } catch (error: unknown) { + if (isAbortError(error)) { + throw error; + } + await emitProgress( + onUpdate, + context, + `Captcha status check failed: ${ + error instanceof Error ? error.message : "unknown error" + }` + ); + } + + if (statusEntries.length > 0) { + await emitProgress( + onUpdate, + context, + `Captcha status detected for ${statusEntries.length} page(s)` + ); + } else { + await emitProgress( + onUpdate, + context, + "No explicit captcha status returned; attempting solve anyway" + ); + } + + if (typeof session.captchasSolve === "function") { + throwIfAborted(signal); + summary.solveAttempts += 1; + try { + const solveResult = await session.captchasSolve(); + const message = + typeof solveResult === "object" && + solveResult !== null && + "message" in solveResult && + typeof (solveResult as { message?: unknown }).message === "string" + ? (solveResult as { message: string }).message + : "captcha solve requested"; + await emitProgress(onUpdate, context, `Captcha solve call: ${message}`); + } catch (error: unknown) { + if (isAbortError(error)) { + throw error; + } + await emitProgress( + onUpdate, + context, + `Captcha solve call failed: ${ + error instanceof Error ? error.message : "unknown error" + }` + ); + } + } else { + await emitProgress( + onUpdate, + context, + "Session does not expose captchas.solve; proceeding with retry" + ); + } + + while (Date.now() < deadline && typeof session.captchasStatus === "function") { + throwIfAborted(signal); + await sleepWithSignal(pollIntervalMs, signal); + try { + statusEntries = await tryReadCaptchaStatus(session, summary, signal); + } catch (error: unknown) { + if (isAbortError(error)) { + throw error; + } + await emitProgress( + onUpdate, + context, + `Captcha status polling failed: ${ + error instanceof Error ? error.message : "unknown error" + }` + ); + break; + } + + if (!hasActiveCaptcha(statusEntries)) { + await emitProgress(onUpdate, context, "Captcha state cleared; retrying action"); + return; + } + } + + if (typeof session.captchasStatus === "function") { + summary.waitTimedOut = true; + await emitProgress( + onUpdate, + context, + `Captcha wait reached ${waitMs}ms; retrying ${actionLabel}` + ); + } +} + +export async function runWithCaptchaRecovery( + options: CaptchaRecoveryOptions +): Promise { + const { + session, + context, + actionLabel, + onUpdate, + operation, + signal, + shouldRetry = isCaptchaInterferenceError, + } = options; + + const maxRetries = resolveCaptchaMaxRetries(); + const summary: CaptchaRecoverySummary = { + triggered: false, + retries: 0, + solveAttempts: 0, + statusChecks: 0, + waitTimedOut: false, + }; + + let attempt = 0; + while (true) { + throwIfAborted(signal); + try { + await operation(); + return summary; + } catch (error: unknown) { + if (isAbortError(error)) { + throw error; + } + throwIfAborted(signal); + const retriable = shouldRetry(error); + if (!retriable || attempt >= maxRetries) { + throw error; + } + + summary.triggered = true; + summary.retries += 1; + await emitProgress( + onUpdate, + context, + `Captcha-related blocker detected while trying to ${actionLabel}` + ); + await runCaptchaRecoveryStep( + session, + context, + actionLabel, + onUpdate, + summary, + signal + ); + attempt += 1; + } + } +} diff --git a/extensions/steel-browser/src/tools/click.ts b/extensions/steel-browser/src/tools/click.ts new file mode 100644 index 0000000..ab2771d --- /dev/null +++ b/extensions/steel-browser/src/tools/click.ts @@ -0,0 +1,340 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails, type SteelClient } from "../steel-client.js"; +import { runWithCaptchaRecovery, type CaptchaRecoverySummary } from "./captcha-guard.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; +import { + MAX_TOOL_TIMEOUT_MS, + resolveToolTimeoutMs, +} from "./tool-settings.js"; + +type WaitState = "attached" | "visible"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + captchasStatus?: () => Promise; + captchasSolve?: () => Promise; + waitForSelector?: ( + selector: string, + options?: { state?: WaitState; timeout?: number } + ) => Promise; + click?: (selector: string, options?: { timeout?: number }) => Promise; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + locator?: (selector: string) => { + waitFor?: (options?: { state?: WaitState; timeout?: number }) => Promise; + isVisible?: () => Promise; + isEnabled?: () => Promise; + click?: (options?: { timeout?: number }) => Promise; + }; + page?: { + waitForSelector?: ( + selector: string, + options?: { state?: WaitState; timeout?: number } + ) => Promise; + click?: (selector: string, options?: { timeout?: number }) => Promise; + locator?: (selector: string) => { + waitFor?: (options?: { state?: WaitState; timeout?: number }) => Promise; + isVisible?: () => Promise; + isEnabled?: () => Promise; + click?: (options?: { timeout?: number }) => Promise; + }; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + }; +}; + +function compactCaptchaRecovery(summary: CaptchaRecoverySummary) { + return { + triggered: summary.triggered, + retries: summary.retries, + solveAttempts: summary.solveAttempts, + statusChecks: summary.statusChecks, + waitTimedOut: summary.waitTimedOut, + }; +} + +function normalizeSelector(selector: string): string { + const trimmed = selector.trim(); + if (!trimmed) { + throw new Error("Selector cannot be empty."); + } + return trimmed; +} + +function normalizeTimeout(timeoutMs?: number): number { + return resolveToolTimeoutMs(timeoutMs); +} + +function getLocator( + session: SessionLike, + selector: string +): + | { + waitFor?: (options?: { state?: WaitState; timeout?: number }) => Promise; + isVisible?: () => Promise; + isEnabled?: () => Promise; + click?: (options?: { timeout?: number }) => Promise; + } + | undefined { + if (typeof session.locator === "function") { + return session.locator(selector); + } + + if (typeof session.page?.locator === "function") { + return session.page.locator(selector); + } + + return undefined; +} + +function supportsCssSelectorFallback(selector: string): boolean { + const normalized = selector.trim(); + if (!normalized) { + return false; + } + if ( + normalized.includes(">>") || + normalized.includes("text=") || + normalized.includes("xpath=") || + normalized.includes("nth=") || + normalized.includes(":has-text(") || + normalized.includes(":text(") || + normalized.includes(":contains(") + ) { + return false; + } + return true; +} + +async function waitForTarget( + session: SessionLike, + selector: string, + timeoutMs: number, + signal: AbortSignal | undefined +): Promise { + throwIfAborted(signal); + const locator = getLocator(session, selector); + if (locator?.waitFor) { + await withAbortSignal( + locator.waitFor({ state: "visible", timeout: timeoutMs }), + signal + ); + return; + } + + if (typeof session.waitForSelector === "function") { + await withAbortSignal( + session.waitForSelector(selector, { state: "visible", timeout: timeoutMs }), + signal + ); + return; + } + + if (typeof session.page?.waitForSelector === "function") { + await withAbortSignal( + session.page.waitForSelector(selector, { state: "visible", timeout: timeoutMs }), + signal + ); + } +} + +async function ensureClickable( + session: SessionLike, + selector: string, + signal: AbortSignal | undefined +): Promise { + throwIfAborted(signal); + const locator = getLocator(session, selector); + if (locator) { + if (typeof locator.isVisible === "function") { + const visible = await withAbortSignal(locator.isVisible(), signal); + if (!visible) { + throw new Error(`Element is not visible: ${selector}`); + } + } + if (typeof locator.isEnabled === "function") { + const enabled = await withAbortSignal(locator.isEnabled(), signal); + if (!enabled) { + throw new Error(`Element is disabled and cannot be clicked: ${selector}`); + } + } + return; + } + + if (!supportsCssSelectorFallback(selector)) { + return; + } + + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + return; + } + + const result = await withAbortSignal( + evaluate( + (input: { selector: string }) => { + const element = document.querySelector(input.selector) as HTMLElement | null; + if (!element) { + return { found: false, clickable: false, disabled: false }; + } + const style = getComputedStyle(element); + const rect = element.getBoundingClientRect(); + const visible = + rect.width > 0 && + rect.height > 0 && + style.display !== "none" && + style.visibility !== "hidden" && + Number.parseFloat(style.opacity) > 0; + const disabled = + (element as HTMLInputElement).disabled === true || + element.getAttribute("aria-disabled") === "true"; + const clickable = visible && !disabled && style.pointerEvents !== "none"; + return { found: true, clickable, disabled }; + }, + { selector } + ), + signal + ); + + if (!result || typeof result !== "object") { + return; + } + + const found = Boolean((result as Record).found); + const clickable = Boolean((result as Record).clickable); + const disabled = Boolean((result as Record).disabled); + if (!found) { + throw new Error(`No element matched selector: ${selector}`); + } + if (disabled) { + throw new Error(`Element is disabled and cannot be clicked: ${selector}`); + } + if (!clickable) { + throw new Error(`Element is not clickable: ${selector}`); + } +} + +async function invokeClick( + session: SessionLike, + selector: string, + timeoutMs: number, + signal: AbortSignal | undefined +): Promise { + throwIfAborted(signal); + const locator = getLocator(session, selector); + if (locator?.click) { + await withAbortSignal(locator.click({ timeout: timeoutMs }), signal); + return; + } + + if (typeof session.click === "function") { + await withAbortSignal(session.click(selector, { timeout: timeoutMs }), signal); + return; + } + + if (typeof session.page?.click === "function") { + await withAbortSignal( + session.page.click(selector, { timeout: timeoutMs }), + signal + ); + return; + } + + const pageEvaluate = session.evaluate ?? session.page?.evaluate; + if (typeof pageEvaluate === "function" && supportsCssSelectorFallback(selector)) { + const clicked = await withAbortSignal( + pageEvaluate( + (input: { selector: string }) => { + const element = document.querySelector(input.selector) as HTMLElement | null; + if (!element) { + return false; + } + element.click(); + return true; + }, + { selector } + ), + signal + ); + + if (clicked) { + return; + } + } + + throw new Error("Session does not support click operations."); +} + +export function clickTool(client: SteelClient): ToolDefinition { + return { + name: "steel_click", + label: "Click", + description: "Click an element in the page", + parameters: Type.Object( + { + selector: Type.String({ description: "CSS selector of the element to click" }), + timeout: Type.Optional( + Type.Integer({ + minimum: 100, + maximum: MAX_TOOL_TIMEOUT_MS, + description: "Maximum milliseconds to wait for the element", + }) + ), + } + ), + + async execute( + _toolCallId: string, + params: { selector: string; timeout?: number }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_click", async () => { + throwIfAborted(signal); + const selector = normalizeSelector(params.selector); + const timeoutMs = normalizeTimeout(params.timeout); + + await emitProgress(onUpdate, "steel_click", `Preparing click for ${selector}`); + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + await emitProgress(onUpdate, "steel_click", "Running click sequence"); + const captchaRecovery = await runWithCaptchaRecovery({ + session, + context: "steel_click", + actionLabel: `click ${selector}`, + onUpdate, + signal, + operation: async () => { + throwIfAborted(signal); + await waitForTarget(session, selector, timeoutMs, signal); + throwIfAborted(signal); + await ensureClickable(session, selector, signal); + throwIfAborted(signal); + await invokeClick(session, selector, timeoutMs, signal); + }, + }); + await emitProgress(onUpdate, "steel_click", "Click succeeded"); + + return { + content: [{ type: "text", text: `Clicked element ${selector}` }], + details: { + ...sessionDetails(session), + selector, + timeoutMs, + clicked: true, + captchaRecovery: compactCaptchaRecovery(captchaRecovery), + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/computer.ts b/extensions/steel-browser/src/tools/computer.ts new file mode 100644 index 0000000..213c286 --- /dev/null +++ b/extensions/steel-browser/src/tools/computer.ts @@ -0,0 +1,456 @@ +import { randomUUID } from "node:crypto"; +import { promises as fs } from "node:fs"; +import path from "node:path"; +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import type Steel from "steel-sdk"; +import { sessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; + +type SessionComputerParams = Steel.SessionComputerParams; +type SessionComputerResponse = Steel.SessionComputerResponse; +type ComputerAction = SessionComputerParams["action"]; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + computer?: (body: SessionComputerParams) => Promise; +}; + +type ComputerToolParams = { + action: ComputerAction; + screenshot?: boolean; + hold_keys?: string[]; + coordinates?: number[]; + button?: "left" | "right" | "middle" | "back" | "forward"; + click_type?: "down" | "up" | "click"; + num_clicks?: number; + path?: number[][]; + delta_x?: number; + delta_y?: number; + keys?: string[]; + duration?: number; + text?: string; +}; + +const RELATIVE_SCREENSHOT_DIR = path.join(".artifacts", "screenshots"); +const SUPPORTED_ACTIONS: readonly ComputerAction[] = [ + "move_mouse", + "click_mouse", + "drag_mouse", + "scroll", + "press_key", + "type_text", + "wait", + "take_screenshot", + "get_cursor_position", +]; + +function isFiniteNumber(value: unknown): value is number { + return typeof value === "number" && Number.isFinite(value); +} + +function normalizeCoordinatePair( + raw: number[] | undefined, + fieldName: string +): [number, number] { + if (!Array.isArray(raw) || raw.length !== 2) { + throw new Error(`${fieldName} must be [x, y].`); + } + + const [x, y] = raw; + if (!isFiniteNumber(x) || !isFiniteNumber(y)) { + throw new Error(`${fieldName} must contain finite numbers.`); + } + + return [x, y]; +} + +function normalizeKeyList(raw: string[] | undefined, fieldName: string): string[] { + if (!Array.isArray(raw) || raw.length === 0) { + throw new Error(`${fieldName} must contain at least one key.`); + } + + const keys = raw + .map((item) => item.trim()) + .filter((item) => item.length > 0); + if (keys.length === 0) { + throw new Error(`${fieldName} must contain at least one non-empty key.`); + } + return keys; +} + +function normalizeOptionalHoldKeys(raw: string[] | undefined): string[] | undefined { + if (raw === undefined) { + return undefined; + } + if (!Array.isArray(raw)) { + throw new Error("hold_keys must be an array of key names."); + } + const keys = raw + .map((item) => item.trim()) + .filter((item) => item.length > 0); + return keys.length > 0 ? keys : undefined; +} + +function normalizeDuration( + raw: number | undefined, + fieldName: string +): number | undefined { + if (raw === undefined) { + return undefined; + } + if (!isFiniteNumber(raw) || raw <= 0) { + throw new Error(`${fieldName} must be a positive number.`); + } + return raw; +} + +function normalizeAction(action: string): ComputerAction { + const trimmed = action.trim() as ComputerAction; + if (!SUPPORTED_ACTIONS.includes(trimmed)) { + throw new Error( + `Unsupported action "${action}". Supported actions: ${SUPPORTED_ACTIONS.join(", ")}.` + ); + } + return trimmed; +} + +function buildActionRequest(params: ComputerToolParams): SessionComputerParams { + const action = normalizeAction(params.action); + const screenshot = params.screenshot; + const holdKeys = normalizeOptionalHoldKeys(params.hold_keys); + + switch (action) { + case "move_mouse": { + return { + action, + coordinates: normalizeCoordinatePair(params.coordinates, "coordinates"), + ...(screenshot === undefined ? {} : { screenshot }), + ...(holdKeys ? { hold_keys: holdKeys } : {}), + }; + } + case "click_mouse": { + const button = params.button; + if (!button) { + throw new Error("button is required for click_mouse."); + } + const body: Extract = { + action, + button, + ...(screenshot === undefined ? {} : { screenshot }), + ...(holdKeys ? { hold_keys: holdKeys } : {}), + }; + if (params.coordinates !== undefined) { + body.coordinates = normalizeCoordinatePair(params.coordinates, "coordinates"); + } + if (params.click_type !== undefined) { + body.click_type = params.click_type; + } + if (params.num_clicks !== undefined) { + if (!Number.isInteger(params.num_clicks) || params.num_clicks <= 0) { + throw new Error("num_clicks must be a positive integer."); + } + body.num_clicks = params.num_clicks; + } + return body; + } + case "drag_mouse": { + if (!Array.isArray(params.path) || params.path.length < 2) { + throw new Error("path must contain at least two [x, y] coordinates."); + } + const pathPairs = params.path.map((entry, index) => + normalizeCoordinatePair(entry, `path[${index}]`) + ); + return { + action, + path: pathPairs, + ...(screenshot === undefined ? {} : { screenshot }), + ...(holdKeys ? { hold_keys: holdKeys } : {}), + }; + } + case "scroll": { + const hasDeltaX = params.delta_x !== undefined; + const hasDeltaY = params.delta_y !== undefined; + if (!hasDeltaX && !hasDeltaY) { + throw new Error("scroll requires delta_x, delta_y, or both."); + } + if (hasDeltaX && !isFiniteNumber(params.delta_x)) { + throw new Error("delta_x must be a finite number."); + } + if (hasDeltaY && !isFiniteNumber(params.delta_y)) { + throw new Error("delta_y must be a finite number."); + } + const body: Extract = { + action, + ...(screenshot === undefined ? {} : { screenshot }), + ...(holdKeys ? { hold_keys: holdKeys } : {}), + }; + if (params.coordinates !== undefined) { + body.coordinates = normalizeCoordinatePair(params.coordinates, "coordinates"); + } + if (hasDeltaX) { + body.delta_x = params.delta_x; + } + if (hasDeltaY) { + body.delta_y = params.delta_y; + } + return body; + } + case "press_key": { + const duration = normalizeDuration(params.duration, "duration"); + return { + action, + keys: normalizeKeyList(params.keys, "keys"), + ...(duration === undefined ? {} : { duration }), + ...(screenshot === undefined ? {} : { screenshot }), + }; + } + case "type_text": { + if (typeof params.text !== "string") { + throw new Error("text is required for type_text."); + } + return { + action, + text: params.text, + ...(screenshot === undefined ? {} : { screenshot }), + ...(holdKeys ? { hold_keys: holdKeys } : {}), + }; + } + case "wait": { + const duration = normalizeDuration(params.duration, "duration"); + if (duration === undefined) { + throw new Error("duration is required for wait."); + } + return { + action, + duration, + ...(screenshot === undefined ? {} : { screenshot }), + }; + } + case "take_screenshot": + return { action }; + case "get_cursor_position": + return { action }; + default: + throw new Error(`Unsupported action "${action}".`); + } +} + +function screenshotDirectory(): string { + return path.resolve(process.cwd(), RELATIVE_SCREENSHOT_DIR); +} + +function toArtifactDisplayPath(filePath: string): string { + const relativePath = path.relative(process.cwd(), filePath); + if (!relativePath || relativePath.startsWith("..")) { + return path.basename(filePath); + } + return relativePath; +} + +async function createScreenshotPath(): Promise { + const dir = screenshotDirectory(); + await fs.mkdir(dir, { recursive: true }); + const safeId = randomUUID().slice(0, 8); + return path.join(dir, `steel-computer-${Date.now()}-${safeId}.png`); +} + +function decodeBase64Png(raw: string): Buffer { + const text = raw.trim(); + if (!text) { + throw new Error("empty base64_image payload."); + } + + const payload = text.startsWith("data:") + ? text.slice(text.indexOf(",") + 1) + : text; + const decoded = Buffer.from(payload, "base64"); + if (decoded.length === 0) { + throw new Error("invalid base64_image payload."); + } + return decoded; +} + +async function persistScreenshotArtifact(base64Image: string) { + const buffer = decodeBase64Png(base64Image); + const targetPath = await createScreenshotPath(); + await fs.writeFile(targetPath, buffer); + const displayPath = toArtifactDisplayPath(targetPath); + + return { + path: displayPath, + fileName: path.basename(displayPath), + mimeType: "image/png", + sizeBytes: buffer.length, + type: "image", + }; +} + +export function computerTool(client: SteelClient): ToolDefinition { + return { + name: "steel_computer", + label: "Computer Action", + description: "Execute low-level Steel computer actions (mouse, keyboard, scroll, screenshot)", + parameters: Type.Object({ + action: Type.Union( + SUPPORTED_ACTIONS.map((value) => Type.Literal(value)), + { description: "Computer action type to execute" } + ), + screenshot: Type.Optional( + Type.Boolean({ + description: "Request screenshot output after the action (supported by most actions)", + }) + ), + hold_keys: Type.Optional( + Type.Array(Type.String(), { + description: "Modifier keys to hold while performing supported actions", + }) + ), + coordinates: Type.Optional( + Type.Array(Type.Number(), { + minItems: 2, + maxItems: 2, + description: "Target coordinates as [x, y]", + }) + ), + button: Type.Optional( + Type.Union( + [ + Type.Literal("left"), + Type.Literal("right"), + Type.Literal("middle"), + Type.Literal("back"), + Type.Literal("forward"), + ], + { description: "Mouse button for click_mouse" } + ) + ), + click_type: Type.Optional( + Type.Union( + [Type.Literal("click"), Type.Literal("down"), Type.Literal("up")], + { description: "Click type for click_mouse" } + ) + ), + num_clicks: Type.Optional( + Type.Integer({ + minimum: 1, + description: "Number of clicks for click_mouse", + }) + ), + path: Type.Optional( + Type.Array( + Type.Array(Type.Number(), { minItems: 2, maxItems: 2 }), + { + minItems: 2, + description: "Drag path as array of [x, y] points for drag_mouse", + } + ) + ), + delta_x: Type.Optional( + Type.Number({ description: "Horizontal scroll amount for scroll" }) + ), + delta_y: Type.Optional( + Type.Number({ description: "Vertical scroll amount for scroll" }) + ), + keys: Type.Optional( + Type.Array(Type.String(), { + minItems: 1, + description: "Keys for press_key", + }) + ), + duration: Type.Optional( + Type.Number({ + exclusiveMinimum: 0, + description: "Duration in seconds for wait/press_key", + }) + ), + text: Type.Optional( + Type.String({ description: "Text for type_text action" }) + ), + }), + + async execute( + _toolCallId: string, + params: ComputerToolParams, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_computer", async () => { + throwIfAborted(signal); + await emitProgress(onUpdate, "steel_computer", `Preparing action ${params.action}`); + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + if (typeof session.computer !== "function") { + throw new Error( + "Current Steel client does not expose sessions.computer(). Upgrade steel-sdk to a newer version." + ); + } + + const requestBody = buildActionRequest(params); + await emitProgress(onUpdate, "steel_computer", `Dispatching ${requestBody.action}`); + const response = await withAbortSignal( + session.computer(requestBody), + signal + ); + + if (response.error) { + throw new Error(response.error); + } + + let artifact: + | { + path: string; + fileName: string; + mimeType: string; + sizeBytes: number; + type: string; + } + | undefined; + + if (typeof response.base64_image === "string" && response.base64_image.trim()) { + await emitProgress(onUpdate, "steel_computer", "Persisting screenshot artifact"); + artifact = await persistScreenshotArtifact(response.base64_image); + } + + const outputParts = [response.output, response.system] + .filter((item): item is string => typeof item === "string" && item.trim().length > 0) + .map((item) => item.trim()); + const outputSuffix = outputParts.length > 0 ? ` ${outputParts.join(" ")}` : ""; + + return { + content: [ + { + type: "text", + text: `Computer action ${requestBody.action} completed.${outputSuffix}`, + }, + ], + details: { + ...sessionDetails(session), + action: requestBody.action, + request: requestBody, + output: response.output ?? null, + system: response.system ?? null, + hasScreenshot: Boolean(artifact), + ...(artifact + ? { + filePath: artifact.path, + fileName: artifact.fileName, + artifact, + } + : {}), + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/extract.ts b/extensions/steel-browser/src/tools/extract.ts new file mode 100644 index 0000000..f41d0ed --- /dev/null +++ b/extensions/steel-browser/src/tools/extract.ts @@ -0,0 +1,621 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails as baseSessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + url?: (() => Promise | string) | string; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + page?: { + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + }; +}; + +type SchemaType = "object" | "array" | "string" | "number" | "integer" | "boolean" | "null"; +type PrimitiveSchemaType = Exclude; + +type ExtractionSchema = { + type: SchemaType; + properties: Record; + required: string[]; + items?: ExtractionSchema; + selector?: string; + attribute?: string; + additionalProperties: boolean; +}; + +const ALLOWED_TYPES = new Set([ + "object", + "array", + "string", + "number", + "integer", + "boolean", + "null", +]); + +function asPlainObject(input: unknown, path: string): Record { + if (!input || typeof input !== "object" || Array.isArray(input)) { + throw new Error(`Schema at ${path} must be an object.`); + } + + return input as Record; +} + +function normalizeBoolean(value: unknown, path: string): boolean { + if (typeof value === "boolean") { + return value; + } + + throw new Error(`Schema at ${path} must define a boolean value.`); +} + +function normalizeString(value: unknown, path: string): string | undefined { + if (value === undefined) { + return undefined; + } + if (typeof value !== "string") { + throw new Error(`Schema at ${path} must define a string value.`); + } + + const normalized = value.trim(); + if (!normalized) { + throw new Error(`Schema at ${path} must not be empty.`); + } + return normalized; +} + +function normalizeRequired( + value: unknown, + properties: Record, + path: string +): string[] { + if (value === undefined) { + return []; + } + if (!Array.isArray(value) || value.length !== value.filter((entry) => typeof entry === "string").length) { + throw new Error(`Schema at ${path} must use an array of strings for required fields.`); + } + + return value.filter((entry): entry is string => true); +} + +function normalizeSchemaType( + rawType: unknown, + rawSchema: Record, + path: string +): SchemaType { + const hasProperties = + Object.prototype.hasOwnProperty.call(rawSchema, "properties"); + const hasItems = Object.prototype.hasOwnProperty.call(rawSchema, "items"); + + if (rawType === undefined) { + if (hasProperties) { + return "object"; + } + if (hasItems) { + return "array"; + } + + throw new Error( + `Schema at ${path} must define a type or include "properties"/"items" to infer object/array shape.` + ); + } + + if (typeof rawType !== "string" || !ALLOWED_TYPES.has(rawType as SchemaType)) { + throw new Error(`Schema at ${path} has unsupported type "${String(rawType)}".`); + } + + return rawType as SchemaType; +} + +function normalizeProperties(rawValue: unknown, path: string): Record { + if (rawValue === undefined) { + return {}; + } + if (!rawValue || typeof rawValue !== "object" || Array.isArray(rawValue)) { + throw new Error(`Schema at ${path} must use an object for properties.`); + } + + const properties = rawValue as Record; + const normalized: Record = {}; + + for (const [name, propertySchema] of Object.entries(properties)) { + normalized[name] = normalizeSchema(propertySchema, `${path}.${name}`); + } + + return normalized; +} + +function normalizeSchema(rawSchema: unknown, path: string): ExtractionSchema { + const schemaObject = asPlainObject(rawSchema, path); + const type = normalizeSchemaType(schemaObject.type, schemaObject, path); + + const schema: ExtractionSchema = { + type, + properties: {}, + required: [], + additionalProperties: true, + }; + + if (type === "object") { + const properties = normalizeProperties(schemaObject.properties, `${path}.properties`); + schema.properties = properties; + schema.required = normalizeRequired( + schemaObject.required, + properties, + `${path}.required` + ); + schema.additionalProperties = normalizeBoolean( + schemaObject.additionalProperties ?? true, + `${path}.additionalProperties` + ); + return schema; + } + + if (type === "array") { + schema.items = normalizeSchema( + schemaObject.items, + `${path}.items` + ); + schema.additionalProperties = normalizeBoolean( + schemaObject.additionalProperties ?? true, + `${path}.additionalProperties` + ); + return schema; + } + + schema.selector = normalizeString( + schemaObject.selector, + `${path}.selector` + ); + schema.attribute = normalizeString( + schemaObject.attribute, + `${path}.attribute` + ); + schema.additionalProperties = normalizeBoolean( + schemaObject.additionalProperties ?? true, + `${path}.additionalProperties` + ); + return schema; +} + +function enforceStrictMode(schema: ExtractionSchema): ExtractionSchema { + if (schema.type === "object") { + const properties: Record = {}; + for (const [key, propertySchema] of Object.entries(schema.properties)) { + properties[key] = enforceStrictMode(propertySchema); + } + return { + ...schema, + additionalProperties: false, + properties, + }; + } + + if (schema.type === "array") { + return { + ...schema, + items: schema.items ? enforceStrictMode(schema.items) : undefined, + }; + } + + return { ...schema }; +} + +function readSessionUrl(session: SessionLike): Promise { + const direct = session.url; + if (typeof direct === "string" && direct.trim()) { + return Promise.resolve(direct); + } + + if (typeof direct === "function") { + return Promise.resolve(direct.call(session)).then((value) => { + if (typeof value === "string" && value.trim()) { + return value; + } + return "unknown"; + }); + } + + const getter = (session as { getCurrentUrl?: () => Promise | string }).getCurrentUrl; + if (typeof getter === "function") { + return Promise.resolve(getter.call(session)).then((value) => { + if (typeof value === "string" && value.trim()) { + return value; + } + return "unknown"; + }); + } + + return Promise.resolve("unknown"); +} + +function sessionDetails(session: SessionLike, url: string, scopeSelector: string | null) { + return { + ...baseSessionDetails(session), + url, + scopeSelector, + }; +} + +function buildPrompt(summary: string, instructions: string | undefined): string { + const instructionLine = instructions ? `\nInstructions: ${instructions}` : ""; + return `Extract structured JSON from the page following this schema contract.${instructionLine}\n${summary}`; +} + +function summarizeSchema(schema: ExtractionSchema, path: string): string[] { + const lines: string[] = []; + const children = []; + const requiredSet = new Set(schema.required); + + if (schema.type === "object") { + lines.push(`${path}: object`); + for (const [key, propertySchema] of Object.entries(schema.properties)) { + const childPath = `${path}.${key}`; + children.push(...summarizeSchema( + propertySchema, + `${childPath}${requiredSet.has(key) ? " (required)" : ""}` + )); + } + } else if (schema.type === "array") { + lines.push(`${path}: array`); + if (schema.items) { + lines.push(...summarizeSchema(schema.items, `${path}[]`)); + } + } else { + const selectorPart = schema.selector ? ` selector=${schema.selector}` : ""; + const attributePart = schema.attribute ? ` attr=${schema.attribute}` : ""; + lines.push(`${path}: ${schema.type}${selectorPart}${attributePart}`); + } + + return [...lines, ...children]; +} + +function toPathPart(name: string): string { + return name.includes(".") ? `["${name}"]` : `.${name}`; +} + +function pushError(errors: string[], path: string, message: string): void { + errors.push(`${path}: ${message}`); +} + +function validateExtraction(value: unknown, schema: ExtractionSchema, path: string, errors: string[]): void { + if (schema.type === "object") { + if (!value || typeof value !== "object" || Array.isArray(value)) { + pushError(errors, path, "expected object"); + return; + } + + const record = value as Record; + const valueKeys = Object.keys(record); + + if (!schema.additionalProperties) { + for (const key of valueKeys) { + if (!Object.prototype.hasOwnProperty.call(schema.properties, key)) { + pushError(errors, `${path}${toPathPart(key)}`, "unexpected property"); + } + } + } + + for (const required of schema.required) { + if (!Object.prototype.hasOwnProperty.call(record, required)) { + pushError(errors, `${path}${toPathPart(required)}`, "missing required value"); + } + } + + for (const [key, childSchema] of Object.entries(schema.properties)) { + if (!Object.prototype.hasOwnProperty.call(record, key)) { + continue; + } + validateExtraction(record[key], childSchema, `${path}${toPathPart(key)}`, errors); + } + + return; + } + + if (schema.type === "array") { + if (!Array.isArray(value)) { + pushError(errors, path, "expected array"); + return; + } + if (!schema.items) { + return; + } + for (let i = 0; i < value.length; i++) { + validateExtraction(value[i], schema.items, `${path}[${i}]`, errors); + } + return; + } + + if (schema.type === "string") { + if (typeof value !== "string") { + pushError(errors, path, "expected string"); + } + return; + } + + if (schema.type === "number" || schema.type === "integer") { + if (typeof value !== "number" || !Number.isFinite(value)) { + pushError(errors, path, "expected finite number"); + return; + } + if (schema.type === "integer" && !Number.isInteger(value)) { + pushError(errors, path, "expected integer"); + } + return; + } + + if (schema.type === "boolean") { + if (typeof value !== "boolean") { + pushError(errors, path, "expected boolean"); + } + return; + } + + if (value !== null) { + pushError(errors, path, "expected null"); + } +} + +function trimAndNormalizeText(raw: string | null | undefined): string { + if (typeof raw !== "string") { + return ""; + } + return raw.replace(/\u00a0/g, " ").trim(); +} + +async function extractWithBrowser( + session: SessionLike, + schema: ExtractionSchema, + scopeSelector: string | null +): Promise { + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + throw new Error("Session does not support DOM-based extraction."); + } + + return evaluate( + (input: { schema: ExtractionSchema; scopeSelector: string | null }): unknown => { + const cleanText = (value: string | null): string => { + if (typeof value !== "string") { + return ""; + } + return value.replace(/\u00a0/g, " ").trim(); + }; + + const resolveScope = (scope: string | null): ParentNode => { + if (!scope) { + return document; + } + const root = document.querySelector(scope); + if (!root) { + return document; + } + return root; + }; + + const coercePrimitive = (source: string, schemaType: "string" | "number" | "integer" | "boolean" | "null"): string | number | boolean | null => { + const normalized = cleanText(source); + + if (schemaType === "string") { + return normalized; + } + + if (schemaType === "boolean") { + const value = normalized.toLowerCase(); + if (["true", "1", "yes", "on"].includes(value)) { + return true; + } + if (["false", "0", "no", "off"].includes(value)) { + return false; + } + return Boolean(normalized); + } + + if (schemaType === "number" || schemaType === "integer") { + const sanitized = normalized.replace(/[^0-9.-]/g, ""); + const parsed = Number.parseFloat(sanitized); + if (!Number.isFinite(parsed)) { + return NaN as unknown as boolean; + } + if (schemaType === "integer") { + return Number.isInteger(parsed) ? parsed : NaN as unknown as boolean; + } + return parsed; + } + + return null; + }; + + const findBySelector = (ctx: ParentNode, selector: string | undefined): ParentNode[] => { + if (!selector) { + return [ctx]; + } + if (!("querySelectorAll" in ctx)) { + return []; + } + + return Array.from(ctx.querySelectorAll(selector)) as ParentNode[]; + }; + + const readPrimitiveValue = ( + ctx: ParentNode, + targetSchema: ExtractionSchema + ): string | number | boolean | null | undefined => { + const selector = targetSchema.selector; + const attr = targetSchema.attribute; + const candidates = findBySelector(ctx, selector); + if (!candidates[0] || !(candidates[0] instanceof Element)) { + return undefined; + } + + const element = candidates[0] as Element & { value?: unknown }; + if (attr) { + const attributeValue = element.getAttribute(attr); + if (attributeValue === null) { + return undefined; + } + const casted = coercePrimitive(attributeValue, targetSchema.type as PrimitiveSchemaType); + return typeof casted === "number" && !Number.isFinite(casted) + ? undefined + : casted; + } + + if (element instanceof HTMLInputElement || element instanceof HTMLTextAreaElement) { + const casted = coercePrimitive( + String((element as HTMLInputElement).value ?? ""), + targetSchema.type as PrimitiveSchemaType + ); + return typeof casted === "number" && !Number.isFinite(casted) + ? undefined + : casted; + } + + const casted = coercePrimitive( + element.textContent ?? "", + targetSchema.type as PrimitiveSchemaType + ); + return typeof casted === "number" && !Number.isFinite(casted) + ? undefined + : casted; + }; + + const extract = (ctx: ParentNode, currentSchema: ExtractionSchema): unknown => { + if (currentSchema.type === "object") { + const base = currentSchema.selector ? findBySelector(ctx, currentSchema.selector)[0] : ctx; + if (!base || !(base instanceof Element) && base !== document) { + return undefined; + } + + const result: Record = {}; + for (const [key, childSchema] of Object.entries(currentSchema.properties)) { + const childValue = extract(base, childSchema); + if (childValue !== undefined) { + result[key] = childValue; + } + } + return result; + } + + if (currentSchema.type === "array") { + if (!currentSchema.items) { + return []; + } + + const nodes = currentSchema.selector ? findBySelector(ctx, currentSchema.selector) : []; + if (nodes.length === 0) { + return []; + } + + const extracted = []; + for (const node of nodes) { + if (node instanceof Element) { + const value = extract(node, currentSchema.items); + extracted.push(value); + } + } + return extracted; + } + + const value = readPrimitiveValue(ctx, currentSchema); + return value; + }; + + const root = resolveScope(input.scopeSelector); + return extract(root, input.schema); + }, + { schema, scopeSelector } + ); +} + +export function extractTool(client: SteelClient): ToolDefinition { + return { + name: "steel_extract", + label: "Extract", + description: "Extract structured values from page content using a JSON Schema contract", + parameters: Type.Object({ + schema: Type.Object({}, { additionalProperties: true, description: "JSON-Schema-like extraction contract." }), + instructions: Type.Optional( + Type.String({ description: "Optional extraction guidance used to disambiguate field selection." }) + ), + scopeSelector: Type.Optional( + Type.String({ description: "Optional CSS selector that scopes extraction to a container." }) + ), + strict: Type.Optional( + Type.Boolean({ description: "Reject properties not defined in schema (default true)." }) + ), + }), + + async execute( + _toolCallId: string, + params: { + schema: Record; + instructions?: string; + scopeSelector?: string; + strict?: boolean; + }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_extract", async () => { + throwIfAborted(signal); + const scopeSelector = normalizeString(params.scopeSelector, "scopeSelector") ?? null; + const strict = params.strict ?? true; + await emitProgress(onUpdate, "steel_extract", "Preparing structured extraction"); + + const normalizedSchema = normalizeSchema(params.schema, "schema"); + const enforcedSchema = strict ? enforceStrictMode(normalizedSchema) : normalizedSchema; + const prompt = buildPrompt( + summarizeSchema(enforcedSchema, "result").join("\n"), + params.instructions + ); + + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + throwIfAborted(signal); + const url = await readSessionUrl(session); + + await emitProgress(onUpdate, "steel_extract", `Preparing prompt with ${prompt.split("\n").length} lines`); + const extracted = await withAbortSignal( + extractWithBrowser(session, enforcedSchema, scopeSelector), + signal + ); + + const validationErrors: string[] = []; + validateExtraction(extracted, enforcedSchema, "result", validationErrors); + if (validationErrors.length > 0) { + throw new Error( + `Extraction result does not match requested schema:\n${validationErrors + .map((error) => `- ${error}`) + .join("\n")}` + ); + } + + await emitProgress(onUpdate, "steel_extract", "Extraction validated"); + return { + content: [{ + type: "text", + text: JSON.stringify(extracted, null, 2), + }], + details: { + ...sessionDetails(session, url, scopeSelector), + schemaEnforced: strict, + prompt, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/fill-form.ts b/extensions/steel-browser/src/tools/fill-form.ts new file mode 100644 index 0000000..c75704e --- /dev/null +++ b/extensions/steel-browser/src/tools/fill-form.ts @@ -0,0 +1,304 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails, type SteelClient } from "../steel-client.js"; +import { runWithCaptchaRecovery, type CaptchaRecoverySummary } from "./captcha-guard.js"; +import { + emitProgress, + isAbortError, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; +import { + MAX_TOOL_TIMEOUT_MS, + resolveToolTimeoutMs, +} from "./tool-settings.js"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + captchasStatus?: () => Promise; + captchasSolve?: () => Promise; + waitForSelector?: ( + selector: string, + options?: { state?: "attached" | "visible"; timeout?: number } + ) => Promise; + fill?: (selector: string, text: string) => Promise; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + locator?: (selector: string) => { + fill?: (text: string) => Promise; + waitFor?: (options?: { state?: "attached" | "visible"; timeout?: number }) => Promise; + }; + page?: { + waitForSelector?: ( + selector: string, + options?: { state?: "attached" | "visible"; timeout?: number } + ) => Promise; + fill?: (selector: string, text: string) => Promise; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + locator?: (selector: string) => { + fill?: (text: string) => Promise; + waitFor?: (options?: { state?: "attached" | "visible"; timeout?: number }) => Promise; + }; + }; +}; + +type FieldInput = { + selector: string; + value: string; +}; + +type FieldResult = { + selector: string; + status: "success" | "error"; + reason?: string; + valueLength: number; + captchaRecovery?: { + triggered: boolean; + retries: number; + solveAttempts: number; + statusChecks: number; + waitTimedOut: boolean; + }; +}; + +function compactCaptchaRecovery(summary: CaptchaRecoverySummary) { + return { + triggered: summary.triggered, + retries: summary.retries, + solveAttempts: summary.solveAttempts, + statusChecks: summary.statusChecks, + waitTimedOut: summary.waitTimedOut, + }; +} + +function normalizeSelector(selector: string): string { + const trimmed = selector.trim(); + if (!trimmed) { + throw new Error("Selector cannot be empty."); + } + return trimmed; +} + +function normalizeTimeout(timeoutMs?: number): number { + return resolveToolTimeoutMs(timeoutMs); +} + +function normalizeValue(raw: string): string { + return raw; +} + +function asArray(input: unknown): FieldInput[] { + if (!Array.isArray(input)) { + return []; + } + + return input + .map((entry): FieldInput | null => { + if (typeof entry !== "object" || entry === null) { + return null; + } + + const record = entry as Partial; + if (typeof record.selector !== "string" || typeof record.value !== "string") { + return null; + } + + return { + selector: normalizeSelector(record.selector), + value: normalizeValue(record.value), + }; + }) + .filter((entry): entry is FieldInput => Boolean(entry)); +} + +async function ensureField(session: SessionLike, selector: string, timeoutMs: number): Promise { + if (typeof session.waitForSelector === "function") { + await session.waitForSelector(selector, { state: "visible", timeout: timeoutMs }); + return; + } + + if (typeof session.page?.waitForSelector === "function") { + await session.page.waitForSelector(selector, { state: "visible", timeout: timeoutMs }); + return; + } + + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + return; + } + + const valid = await evaluate((rawSelector: string) => { + const element = document.querySelector(rawSelector); + return Boolean(element); + }, selector); + + if (!valid) { + throw new Error(`No element matched selector: ${selector}`); + } +} + +async function fill(session: SessionLike, selector: string, value: string): Promise { + if (typeof session.fill === "function") { + await session.fill(selector, value); + return; + } + + if (typeof session.page?.fill === "function") { + await session.page.fill(selector, value); + return; + } + + const locator = + typeof session.locator === "function" + ? session.locator(selector) + : session.page?.locator?.(selector); + + const locatorFill = locator?.fill; + if (typeof locatorFill === "function") { + await locatorFill.call(locator, value); + return; + } + + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + throw new Error("Session does not support setting input values."); + } + + const ok = await evaluate( + (input: { selector: string; value: string }) => { + const element = document.querySelector(input.selector) as HTMLInputElement | HTMLTextAreaElement | null; + if (!element) { + return false; + } + + element.value = input.value; + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); + return true; + }, + { selector, value } + ); + + if (!ok) { + throw new Error(`Could not set value for selector: ${selector}`); + } +} + +export function fillFormTool(client: SteelClient): ToolDefinition { + return { + name: "steel_fill_form", + label: "Fill Form", + description: "Fill multiple input fields in a single tool call", + parameters: Type.Object({ + fields: Type.Array( + Type.Object({ + selector: Type.String({ description: "CSS selector for the field" }), + value: Type.String({ description: "Value for the field" }), + }) + ), + timeout: Type.Optional( + Type.Integer({ + minimum: 100, + maximum: MAX_TOOL_TIMEOUT_MS, + description: "Maximum milliseconds to wait for each field", + }) + ), + }), + + async execute( + _toolCallId: string, + params: { fields: unknown; timeout?: number }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_fill_form", async () => { + throwIfAborted(signal); + const fields = asArray(params.fields); + if (!fields.length) { + throw new Error("At least one field with selector and value is required."); + } + + const timeoutMs = normalizeTimeout(params.timeout); + await emitProgress(onUpdate, "steel_fill_form", `Preparing ${fields.length} field(s)`); + + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + + const results: FieldResult[] = []; + let successCount = 0; + + for (let index = 0; index < fields.length; index += 1) { + throwIfAborted(signal); + const entry = fields[index]; + const result: FieldResult = { + selector: entry.selector, + status: "error", + valueLength: entry.value.length, + }; + + await emitProgress(onUpdate, "steel_fill_form", `Processing ${index + 1}/${fields.length}: ${entry.selector}`); + try { + const captchaRecovery = await runWithCaptchaRecovery({ + session, + context: "steel_fill_form", + actionLabel: `fill ${entry.selector}`, + onUpdate, + signal, + operation: async () => { + throwIfAborted(signal); + await withAbortSignal( + ensureField(session, entry.selector, timeoutMs), + signal + ); + throwIfAborted(signal); + await withAbortSignal(fill(session, entry.selector, entry.value), signal); + }, + }); + + result.status = "success"; + result.captchaRecovery = compactCaptchaRecovery(captchaRecovery); + successCount += 1; + await emitProgress(onUpdate, "steel_fill_form", `Filled ${entry.selector}`); + } catch (error) { + if (isAbortError(error)) { + throw error; + } + result.reason = error instanceof Error ? error.message : "Unknown error"; + } + + results.push(result); + } + + if (successCount === 0) { + throw new Error("No form fields were filled successfully."); + } + + await emitProgress(onUpdate, "steel_fill_form", `Filled ${successCount}/${fields.length} field(s).`); + + return { + content: [ + { + type: "text", + text: + successCount === fields.length + ? `Filled ${fields.length} form field(s).` + : `Filled ${successCount}/${fields.length} form fields. Some fields failed.`, + }, + ], + details: { + ...sessionDetails(session), + timeoutMs, + total: fields.length, + successCount, + results, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/find-elements.ts b/extensions/steel-browser/src/tools/find-elements.ts new file mode 100644 index 0000000..04801e7 --- /dev/null +++ b/extensions/steel-browser/src/tools/find-elements.ts @@ -0,0 +1,316 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails as baseSessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; +import { + blankPageError, + isBlankPageUrl, + readSessionUrl, +} from "./session-state.js"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + page?: { + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + }; + url?: (() => Promise | string) | string; + getCurrentUrl?: () => Promise | string; +}; + +type Candidate = { + selector: string; + text: string; + tag: string; + role: string | null; + clickable: boolean; + visible: boolean; +}; + +const MAX_RESULT_LIMIT = 25; + +function normalizeLimit(rawLimit?: number): number { + if (rawLimit === undefined) { + return 10; + } + const parsed = Number(rawLimit); + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error("limit must be a positive integer."); + } + return Math.min(MAX_RESULT_LIMIT, Math.trunc(parsed)); +} + +function normalizeOptionalString(value?: string): string | null { + if (value === undefined) { + return null; + } + const normalized = value.trim(); + if (!normalized) { + return null; + } + return normalized; +} + +function sessionDetails(session: SessionLike, url: string) { + return { + ...baseSessionDetails(session), + url, + }; +} + +async function discoverElements( + session: SessionLike, + input: { + query: string | null; + tag: string | null; + role: string | null; + limit: number; + clickableOnly: boolean; + } +): Promise { + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + throw new Error("Session does not support element discovery."); + } + + const results = await evaluate((params: { + query: string | null; + tag: string | null; + role: string | null; + limit: number; + clickableOnly: boolean; + }) => { + const toLower = (value: string | null | undefined): string => + String(value || "").toLowerCase(); + + const normalize = (value: string | null | undefined): string => + String(value || "").replace(/\s+/g, " ").trim(); + + const cssEscape = (value: string): string => { + if ((window as unknown as { CSS?: { escape?: (v: string) => string } }).CSS?.escape) { + return (window as unknown as { CSS: { escape: (v: string) => string } }).CSS.escape(value); + } + return value.replace(/\\/g, "\\\\").replace(/"/g, '\\"'); + }; + + const isVisible = (element: Element): boolean => { + const style = window.getComputedStyle(element); + const rect = element.getBoundingClientRect(); + return ( + rect.width > 0 && + rect.height > 0 && + style.visibility !== "hidden" && + style.display !== "none" && + Number.parseFloat(style.opacity) > 0 + ); + }; + + const isClickable = (element: Element): boolean => { + const tag = element.tagName.toLowerCase(); + const role = element.getAttribute("role"); + if (["a", "button", "summary", "select"].includes(tag)) { + return true; + } + if (tag === "input") { + const input = element as HTMLInputElement; + return input.type !== "hidden"; + } + if (role === "button" || role === "link" || role === "menuitem") { + return true; + } + if ((element as HTMLElement).onclick) { + return true; + } + if (element.getAttribute("tabindex") !== null) { + return true; + } + return false; + }; + + const buildSelector = (element: Element): string => { + const tag = element.tagName.toLowerCase(); + const id = element.getAttribute("id"); + if (id && document.querySelectorAll(`#${cssEscape(id)}`).length === 1) { + return `#${cssEscape(id)}`; + } + + const testId = element.getAttribute("data-testid"); + if (testId) { + return `${tag}[data-testid="${cssEscape(testId)}"]`; + } + + const name = element.getAttribute("name"); + if (name) { + return `${tag}[name="${cssEscape(name)}"]`; + } + + const ariaLabel = element.getAttribute("aria-label"); + if (ariaLabel) { + return `${tag}[aria-label="${cssEscape(ariaLabel)}"]`; + } + + if (tag === "a") { + const href = element.getAttribute("href"); + if (href) { + return `a[href="${cssEscape(href)}"]`; + } + } + + const text = normalize(element.textContent); + if (text) { + return `text=${text.slice(0, 80)}`; + } + + return tag; + }; + + const queryLower = toLower(params.query); + const tagLower = toLower(params.tag); + const roleLower = toLower(params.role); + + const source = Array.from(document.querySelectorAll("*")); + const candidates = source + .map((element) => { + const tag = element.tagName.toLowerCase(); + const role = element.getAttribute("role"); + const text = normalize(element.textContent); + const clickable = isClickable(element); + const visible = isVisible(element); + const searchBlob = toLower( + `${text} ${element.getAttribute("aria-label") || ""} ${element.getAttribute("title") || ""}` + ); + + if (tagLower && tag !== tagLower) { + return null; + } + if (roleLower && toLower(role) !== roleLower) { + return null; + } + if (queryLower && !searchBlob.includes(queryLower)) { + return null; + } + if (params.clickableOnly && !clickable) { + return null; + } + if (!visible) { + return null; + } + + return { + selector: buildSelector(element), + text: text.slice(0, 200), + tag, + role, + clickable, + visible, + }; + }) + .filter((item) => Boolean(item)) as Candidate[]; + + return candidates.slice(0, params.limit); + }, input); + + if (!Array.isArray(results)) { + return []; + } + return results as Candidate[]; +} + +export function findElementsTool(client: SteelClient): ToolDefinition { + return { + name: "steel_find_elements", + label: "Find Elements", + description: "Discover likely interactive elements and selector candidates", + parameters: Type.Object({ + query: Type.Optional( + Type.String({ description: "Optional text query to filter by visible label/text" }) + ), + tag: Type.Optional( + Type.String({ description: "Optional exact tag name filter (e.g. button, a, input)" }) + ), + role: Type.Optional( + Type.String({ description: "Optional exact ARIA role filter (e.g. button, link)" }) + ), + limit: Type.Optional( + Type.Integer({ + minimum: 1, + maximum: MAX_RESULT_LIMIT, + description: "Max number of candidates to return", + }) + ), + clickableOnly: Type.Optional( + Type.Boolean({ description: "When true, include only likely interactive elements" }) + ), + }), + + async execute( + _toolCallId: string, + params: { + query?: string; + tag?: string; + role?: string; + limit?: number; + clickableOnly?: boolean; + }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_find_elements", async () => { + throwIfAborted(signal); + const query = normalizeOptionalString(params.query); + const tag = normalizeOptionalString(params.tag); + const role = normalizeOptionalString(params.role); + const limit = normalizeLimit(params.limit); + const clickableOnly = params.clickableOnly ?? true; + + await emitProgress(onUpdate, "steel_find_elements", "Discovering page elements"); + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + throwIfAborted(signal); + const url = await readSessionUrl(session); + if (isBlankPageUrl(url)) { + throw blankPageError("discover page elements"); + } + const candidates = await withAbortSignal( + discoverElements(session, { + query, + tag, + role, + limit, + clickableOnly, + }), + signal + ); + + await emitProgress( + onUpdate, + "steel_find_elements", + `Found ${candidates.length} candidate element(s)` + ); + + return { + content: [{ type: "text", text: JSON.stringify(candidates, null, 2) }], + details: { + ...sessionDetails(session, url), + query, + tag, + role, + limit, + clickableOnly, + count: candidates.length, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/navigate.ts b/extensions/steel-browser/src/tools/navigate.ts new file mode 100644 index 0000000..2837c04 --- /dev/null +++ b/extensions/steel-browser/src/tools/navigate.ts @@ -0,0 +1,335 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; + +type WaitUntil = "load" | "domcontentloaded" | "networkidle"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + goto?: ( + url: string, + options?: { waitUntil?: WaitUntil } + ) => Promise | unknown; +}; + +const ALLOWED_WAIT_UNTIL: readonly WaitUntil[] = ["load", "domcontentloaded", "networkidle"]; +const DEFAULT_WAIT_UNTIL: WaitUntil = "networkidle"; +const FALLBACK_WAIT_UNTILS: readonly WaitUntil[] = ["domcontentloaded", "load"]; +const DEFAULT_NAVIGATION_RETRIES = 1; +const NAVIGATE_CONTEXT = "steel_navigate"; + +type SessionRefreshOptions = { + useProxy?: boolean; + proxyUrl?: string | null; +}; + +type SessionRefreshClient = { + refreshSession?: (options?: SessionRefreshOptions) => Promise; + isProxyConfigured?: () => boolean; +}; + +function resolveWaitUntil(waitUntil?: string): WaitUntil { + if (waitUntil !== undefined && ALLOWED_WAIT_UNTIL.includes(waitUntil as WaitUntil)) { + return waitUntil as WaitUntil; + } + return DEFAULT_WAIT_UNTIL; +} + +function normalizeUrl(rawUrl: string): string { + const trimmed = rawUrl.trim(); + if (!trimmed) { + throw new Error("URL cannot be empty."); + } + + const hasSchemeWithAuthority = /^[a-zA-Z][a-zA-Z\d+\-.]*:\/\//.test(trimmed); + const hasSchemeWithoutAuthority = /^[a-zA-Z][a-zA-Z\d+\-.]*:/.test(trimmed); + const looksLikeHostWithPort = /^[^/\s:]+:\d+(?:[/?#]|$)/.test(trimmed); + const normalized = trimmed.startsWith("//") + ? `https:${trimmed}` + : hasSchemeWithAuthority || (hasSchemeWithoutAuthority && !looksLikeHostWithPort) + ? trimmed + : `https://${trimmed}`; + + try { + const parsed = new URL(normalized); + if (parsed.protocol !== "http:" && parsed.protocol !== "https:") { + throw new Error("Only http and https URLs are supported."); + } + return parsed.toString(); + } catch (error) { + throw new Error(`Invalid URL: ${String(error instanceof Error ? error.message : "invalid URL")}`); + } +} + +function normalizeRetryCount(raw: string | undefined): number { + if (raw === undefined) { + return DEFAULT_NAVIGATION_RETRIES; + } + + const value = raw.trim(); + if (!value) { + return DEFAULT_NAVIGATION_RETRIES; + } + + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed < 0) { + return DEFAULT_NAVIGATION_RETRIES; + } + + return Math.min(parsed, 3); +} + +function isTimeoutError(error: unknown): boolean { + const message = String(error instanceof Error ? error.message : error || ""); + return /timed? ?out|timeout/i.test(message); +} + +function isNetworkError(error: unknown): boolean { + const message = String(error instanceof Error ? error.message : error || ""); + return /ERR_|ECONN|ENOTFOUND|EAI_AGAIN|DNS|network/i.test(message); +} + +function isTunnelConnectionError(error: unknown): boolean { + const message = String(error instanceof Error ? error.message : error || ""); + return /ERR_TUNNEL_CONNECTION_FAILED|TUNNEL_CONNECTION_FAILED/i.test(message); +} + +function buildWaitStrategy(preferred: WaitUntil): WaitUntil[] { + const ordered = [preferred, ...FALLBACK_WAIT_UNTILS]; + const deduped: WaitUntil[] = []; + for (const value of ordered) { + if (!deduped.includes(value)) { + deduped.push(value); + } + } + return deduped; +} + +async function navigateWithRecovery( + session: SessionLike, + options: { + targetUrl: string; + waitUntil: WaitUntil; + onUpdate: ToolProgressUpdater; + signal: AbortSignal | undefined; + } +): Promise { + const { targetUrl, waitUntil, onUpdate, signal } = options; + throwIfAborted(signal); + if (!session.goto) { + throw new Error("Session does not support navigation."); + } + + const retryCount = normalizeRetryCount(process.env.STEEL_NAVIGATE_RETRY_COUNT); + const waitStrategy = buildWaitStrategy(waitUntil); + let lastError: unknown = null; + + for (let waitIndex = 0; waitIndex < waitStrategy.length; waitIndex += 1) { + throwIfAborted(signal); + const waitMode = waitStrategy[waitIndex]; + for (let attempt = 0; attempt <= retryCount; attempt += 1) { + throwIfAborted(signal); + try { + await emitProgress( + onUpdate, + NAVIGATE_CONTEXT, + `Navigating with ${waitMode} (attempt ${attempt + 1}/${retryCount + 1})` + ); + await withAbortSignal( + Promise.resolve(session.goto(targetUrl, { waitUntil: waitMode })), + signal + ); + return waitMode; + } catch (error: unknown) { + throwIfAborted(signal); + lastError = error; + const canRetryNetwork = attempt < retryCount && isNetworkError(error); + if (canRetryNetwork) { + await emitProgress( + onUpdate, + NAVIGATE_CONTEXT, + `Network issue detected; retrying ${waitMode}` + ); + continue; + } + if ( + waitIndex < waitStrategy.length - 1 && + isTimeoutError(error) + ) { + await emitProgress( + onUpdate, + NAVIGATE_CONTEXT, + `Timeout on ${waitMode}; falling back to ${waitStrategy[waitIndex + 1]}` + ); + } + break; + } + } + } + + throw lastError instanceof Error + ? lastError + : new Error("Navigation failed"); +} + +async function refreshNavigationSession( + client: SteelClient, + options?: SessionRefreshOptions +): Promise { + const refresh = (client as unknown as SessionRefreshClient).refreshSession; + if (typeof refresh !== "function") { + return null; + } + return refresh(options); +} + +function shouldTryNoProxyFallback(client: SteelClient): boolean { + const isProxyConfigured = (client as unknown as SessionRefreshClient) + .isProxyConfigured; + if (typeof isProxyConfigured !== "function") { + return false; + } + return isProxyConfigured(); +} + +export function navigateTool(client: SteelClient): ToolDefinition { + return { + name: "steel_navigate", + label: "Navigate", + description: "Navigate to a URL in the browser", + parameters: Type.Object({ + url: Type.String({ description: "The URL to navigate to" }), + waitUntil: Type.Optional( + Type.Union([ + Type.Literal("load"), + Type.Literal("domcontentloaded"), + Type.Literal("networkidle"), + ], { description: "When to consider navigation complete" }) + ), + }), + + async execute( + _toolCallId: string, + params: { url: string; waitUntil?: WaitUntil }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_navigate", async () => { + throwIfAborted(signal); + const targetUrl = normalizeUrl(params.url); + const waitUntil = resolveWaitUntil(params.waitUntil); + + await emitProgress(onUpdate, NAVIGATE_CONTEXT, `Preparing navigation to ${targetUrl}`); + await emitProgress(onUpdate, NAVIGATE_CONTEXT, `Waiting for browser session`); + + let session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + let usedWaitUntil: WaitUntil; + let recoveryMode: "none" | "fresh_session" | "no_proxy" = "none"; + + try { + usedWaitUntil = await navigateWithRecovery(session, { + targetUrl, + waitUntil, + onUpdate, + signal, + }); + } catch (error: unknown) { + throwIfAborted(signal); + if (!isTunnelConnectionError(error)) { + throw error; + } + + await emitProgress( + onUpdate, + NAVIGATE_CONTEXT, + "Tunnel connection failed; recreating browser session and retrying" + ); + const freshSession = await withAbortSignal( + refreshNavigationSession(client), + signal + ); + if (!freshSession) { + throw error; + } + session = freshSession; + + try { + usedWaitUntil = await navigateWithRecovery(session, { + targetUrl, + waitUntil, + onUpdate, + signal, + }); + recoveryMode = "fresh_session"; + } catch (freshError: unknown) { + throwIfAborted(signal); + if ( + !isTunnelConnectionError(freshError) || + !shouldTryNoProxyFallback(client) + ) { + throw freshError; + } + + await emitProgress( + onUpdate, + NAVIGATE_CONTEXT, + "Tunnel failure persisted; retrying once with proxy disabled" + ); + const noProxySession = await withAbortSignal( + refreshNavigationSession(client, { + useProxy: false, + proxyUrl: null, + }), + signal + ); + if (!noProxySession) { + throw freshError; + } + session = noProxySession; + usedWaitUntil = await navigateWithRecovery(session, { + targetUrl, + waitUntil, + onUpdate, + signal, + }); + recoveryMode = "no_proxy"; + } + } + await emitProgress(onUpdate, NAVIGATE_CONTEXT, `Navigation complete to ${targetUrl}`); + + return { + content: [{ + type: "text", + text: `Successfully navigated to ${targetUrl}`, + }], + details: { + ...sessionDetails(session), + requestedUrl: params.url, + url: targetUrl, + waitUntil: usedWaitUntil, + requestedWaitUntil: waitUntil, + tunnelRecovery: + recoveryMode === "none" + ? null + : { + attempted: true, + mode: recoveryMode, + }, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/navigation.ts b/extensions/steel-browser/src/tools/navigation.ts new file mode 100644 index 0000000..50d8d36 --- /dev/null +++ b/extensions/steel-browser/src/tools/navigation.ts @@ -0,0 +1,193 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; +import { + blankPageError, + describeBlankPage, + isBlankPageUrl, + readSessionTitle, + readSessionUrl, +} from "./session-state.js"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + goBack?: (options?: { waitUntil?: "load" | "domcontentloaded" | "networkidle"; timeout?: number }) => Promise | unknown; + back?: (options?: { waitUntil?: "load" | "domcontentloaded" | "networkidle"; timeout?: number }) => Promise | unknown; + url?: (() => Promise | string) | string; + title?: (() => Promise | string) | string; + getCurrentUrl?: () => Promise | string; +}; + +const GO_BACK_TIMEOUT_MS = 10_000; + +function isTimeoutError(error: unknown): boolean { + const message = String(error instanceof Error ? error.message : error || ""); + return /timed? ?out|timeout/i.test(message); +} + +export function goBackTool(client: SteelClient): ToolDefinition { + return { + name: "steel_go_back", + label: "Go Back", + description: "Navigate back in browser history", + parameters: Type.Object({}), + + async execute( + _toolCallId: string, + _params: {}, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_go_back", async () => { + throwIfAborted(signal); + await emitProgress(onUpdate, "steel_go_back", "Preparing history navigation"); + + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + const previousUrl = await readSessionUrl(session); + const goBack = session.goBack ?? session.back; + + if (typeof goBack !== "function") { + throw new Error("Session does not support browser history navigation."); + } + + await emitProgress(onUpdate, "steel_go_back", "Returning to previous page"); + let timeoutRecovered = false; + + try { + await withAbortSignal( + Promise.resolve( + goBack.call(session, { + waitUntil: "domcontentloaded", + timeout: GO_BACK_TIMEOUT_MS, + }) + ), + signal + ); + } catch (error: unknown) { + const currentUrlAfterFailure = await readSessionUrl(session); + if ( + isTimeoutError(error) && + currentUrlAfterFailure !== "unknown" && + currentUrlAfterFailure !== previousUrl && + !isBlankPageUrl(currentUrlAfterFailure) + ) { + timeoutRecovered = true; + await emitProgress( + onUpdate, + "steel_go_back", + `History navigation completed after timeout; now at ${currentUrlAfterFailure}` + ); + } else { + throw error; + } + } + + const currentUrl = await readSessionUrl(session); + await emitProgress(onUpdate, "steel_go_back", `Returned to ${currentUrl}`); + + return { + content: [{ + type: "text", + text: `Navigated back to ${currentUrl}`, + }], + details: { + ...sessionDetails(session), + previousUrl, + url: currentUrl, + timeoutRecovered, + }, + }; + }, signal); + }, + }; +} + +export function getUrlTool(client: SteelClient): ToolDefinition { + return { + name: "steel_get_url", + label: "Get URL", + description: "Get current page URL", + parameters: Type.Object({}), + + async execute( + _toolCallId: string, + _params: {}, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_get_url", async () => { + throwIfAborted(signal); + await emitProgress(onUpdate, "steel_get_url", "Reading current URL"); + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + const url = await readSessionUrl(session); + const isFreshSession = isBlankPageUrl(url); + const text = isFreshSession ? describeBlankPage(url) : `Current URL: ${url}`; + + return { + content: [{ type: "text", text }], + details: { + ...sessionDetails(session), + url, + isFreshSession, + }, + }; + }, signal); + }, + }; +} + +export function getTitleTool(client: SteelClient): ToolDefinition { + return { + name: "steel_get_title", + label: "Get Title", + description: "Get current page title", + parameters: Type.Object({}), + + async execute( + _toolCallId: string, + _params: {}, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_get_title", async () => { + throwIfAborted(signal); + await emitProgress(onUpdate, "steel_get_title", "Reading current page title"); + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + const url = await readSessionUrl(session); + if (isBlankPageUrl(url)) { + throw blankPageError("read the page title"); + } + const title = await readSessionTitle(session); + + return { + content: [{ type: "text", text: `Current title: ${title}` }], + details: { + ...sessionDetails(session), + url, + title, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/pdf.ts b/extensions/steel-browser/src/tools/pdf.ts new file mode 100644 index 0000000..aeae6d7 --- /dev/null +++ b/extensions/steel-browser/src/tools/pdf.ts @@ -0,0 +1,237 @@ +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails as baseSessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + pdf?: (options?: { + path?: string; + printBackground?: boolean; + preferCSSPageSize?: boolean; + }) => Promise; + page?: { + pdf?: (options?: { + path?: string; + printBackground?: boolean; + preferCSSPageSize?: boolean; + }) => Promise; + }; + url?: (() => Promise | string) | string; +}; + +const RELATIVE_PDF_DIR = path.join(".artifacts", "pdfs"); +const DEFAULT_PDF_OPTIONS = { + printBackground: true, + preferCSSPageSize: true, +}; + +function sessionDetails(session: SessionLike, url: string) { + return { + ...baseSessionDetails(session), + url, + }; +} + +function artifactDirectory(): string { + return path.resolve(process.cwd(), RELATIVE_PDF_DIR); +} + +function toArtifactDisplayPath(filePath: string): string { + const relativePath = path.relative(process.cwd(), filePath); + if (!relativePath || relativePath.startsWith("..")) { + return path.basename(filePath); + } + return relativePath; +} + +async function makeArtifactPath(): Promise { + const dir = artifactDirectory(); + await fs.mkdir(dir, { recursive: true }); + const safeId = randomUUID().slice(0, 8); + return path.join(dir, `steel-pdf-${Date.now()}-${safeId}.pdf`); +} + +async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +function isBinaryLike(value: unknown): Buffer | Uint8Array | null { + if (value instanceof Uint8Array) { + return value; + } + + if (value instanceof Buffer) { + return value; + } + + return null; +} + +async function writeBinaryArtifact(filePath: string, payload: unknown): Promise { + const binary = isBinaryLike(payload); + if (!binary) { + return; + } + + await fs.writeFile(filePath, Buffer.from(binary)); +} + +async function readSessionUrl(session: SessionLike): Promise { + const direct = session.url; + if (typeof direct === "string" && direct.trim()) { + return direct; + } + + if (typeof direct === "function") { + const value = await direct.call(session); + if (typeof value === "string" && value.trim()) { + return value; + } + } + + const getter = (session as { getCurrentUrl?: () => Promise | string }).getCurrentUrl; + if (typeof getter === "function") { + const value = await getter.call(session); + if (typeof value === "string" && value.trim()) { + return value; + } + } + + return "unknown"; +} + +async function generatePdf(session: SessionLike, filePath: string): Promise { + const pdfCall = session.pdf ?? session.page?.pdf; + if (typeof pdfCall !== "function") { + throw new Error("Session does not support PDF generation."); + } + + const options = { path: filePath, ...DEFAULT_PDF_OPTIONS }; + + if (pdfCall === session.pdf) { + return session.pdf?.(options); + } + + return session.page?.pdf?.(options); +} + +export function pdfTool(client: SteelClient): ToolDefinition { + return { + name: "steel_pdf", + label: "PDF", + description: "Capture the current page as a PDF artifact", + parameters: Type.Object({ + printBackground: Type.Optional( + Type.Boolean({ + description: "Whether to include page background graphics in the PDF", + }) + ), + preferCSSPageSize: Type.Optional( + Type.Boolean({ + description: "Whether to use page-defined CSS size when available", + }) + ), + }), + + async execute( + _toolCallId: string, + params: { + printBackground?: boolean; + preferCSSPageSize?: boolean; + }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_pdf", async () => { + throwIfAborted(signal); + await emitProgress(onUpdate, "steel_pdf", "Preparing PDF artifact path"); + + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + throwIfAborted(signal); + const url = await readSessionUrl(session); + const targetPath = await makeArtifactPath(); + const options = { + printBackground: + params.printBackground !== undefined + ? params.printBackground + : DEFAULT_PDF_OPTIONS.printBackground, + preferCSSPageSize: + params.preferCSSPageSize !== undefined + ? params.preferCSSPageSize + : DEFAULT_PDF_OPTIONS.preferCSSPageSize, + }; + + const pdfOptions = { + ...options, + path: targetPath, + }; + + await emitProgress(onUpdate, "steel_pdf", "Generating PDF now"); + const pdfResult = await (async () => { + const pdfCall = session.pdf ?? session.page?.pdf; + if (typeof pdfCall !== "function") { + throw new Error("Session does not support PDF generation."); + } + + if (pdfCall === session.pdf) { + return session.pdf?.(pdfOptions); + } + + return session.page?.pdf?.(pdfOptions); + })(); + + await emitProgress(onUpdate, "steel_pdf", `Writing PDF to ${targetPath}`); + await writeBinaryArtifact(targetPath, pdfResult); + + if (!(await fileExists(targetPath))) { + throw new Error("PDF artifact was not written to disk."); + } + + const stats = await fs.stat(targetPath); + const fileName = path.basename(targetPath); + const displayPath = toArtifactDisplayPath(targetPath); + + return { + content: [{ + type: "text", + text: `PDF saved: ${displayPath}`, + }], + details: { + ...sessionDetails(session, url), + filePath: displayPath, + absoluteFilePath: targetPath, + artifact: { + type: "pdf", + mimeType: "application/pdf", + path: displayPath, + fileName, + sizeBytes: stats.size, + createdAt: new Date().toISOString(), + }, + options, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/scrape.ts b/extensions/steel-browser/src/tools/scrape.ts new file mode 100644 index 0000000..b0d3f07 --- /dev/null +++ b/extensions/steel-browser/src/tools/scrape.ts @@ -0,0 +1,408 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails as baseSessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; +import { + blankPageError, + isBlankPageUrl, + readSessionUrl, +} from "./session-state.js"; + +type ScrapeFormat = "html" | "markdown" | "text"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + content?: () => Promise; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + page?: { + content?: () => Promise; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + }; + url?: (() => Promise | string) | string; + getCurrentUrl?: () => Promise | string; +}; + +const ALLOWED_FORMATS: readonly ScrapeFormat[] = ["html", "markdown", "text"]; +const DEFAULT_FORMAT: ScrapeFormat = "text"; +const DEFAULT_MAX_CHARS = 12_000; +const MIN_MAX_CHARS = 1; +const MAX_MAX_CHARS = 200_000; + +function resolveFormat(rawFormat?: string): ScrapeFormat { + if (typeof rawFormat === "string" && ALLOWED_FORMATS.includes(rawFormat as ScrapeFormat)) { + return rawFormat as ScrapeFormat; + } + + return DEFAULT_FORMAT; +} + +function readMaxCharsFromEnv(): number | null { + const raw = process.env.STEEL_SCRAPE_MAX_CHARS; + if (!raw) { + return null; + } + const parsed = Number(raw); + if (!Number.isFinite(parsed) || parsed <= 0) { + return null; + } + return Math.min(MAX_MAX_CHARS, Math.trunc(parsed)); +} + +function resolveMaxChars(rawMaxChars?: number): number { + if (rawMaxChars === undefined) { + return readMaxCharsFromEnv() ?? DEFAULT_MAX_CHARS; + } + + const parsed = Number(rawMaxChars); + if (!Number.isFinite(parsed) || parsed < MIN_MAX_CHARS) { + throw new Error(`maxChars must be an integer >= ${MIN_MAX_CHARS}.`); + } + return Math.min(MAX_MAX_CHARS, Math.trunc(parsed)); +} + +function normalizeSelector(selector?: string): string | undefined { + if (selector === undefined) { + return undefined; + } + + const trimmed = selector.trim(); + if (!trimmed) { + throw new Error("selector cannot be empty."); + } + + return trimmed; +} + +function sessionDetails(session: SessionLike, url: string, format: ScrapeFormat, selector: string | undefined) { + return { + ...baseSessionDetails(session), + url, + format, + selector: selector ?? null, + }; +} + +function extractFallbackText(rawHtml: string): string { + return rawHtml + .replace(/)<[^<]*)*<\/script>/gi, "") + .replace(/)<[^<]*)*<\/style>/gi, "") + .replace(/<[^>]*>/g, "\n") + .replace(/\u00a0/g, " ") + .replace(/\s+\n/g, "\n") + .replace(/\n{3,}/g, "\n\n") + .trim(); +} + +function cleanInnerText(raw: string): string { + return raw + .replace(/\u00a0/g, " ") + .replace(/\r?\n{3,}/g, "\n\n") + .trim(); +} + +function truncateContent(raw: string, maxChars: number): { + text: string; + truncated: boolean; + originalLength: number; +} { + const originalLength = raw.length; + if (originalLength <= maxChars) { + return { + text: raw, + truncated: false, + originalLength, + }; + } + + const omitted = originalLength - maxChars; + const marker = `\n\n[truncated ${omitted} chars]`; + const headLength = Math.max(0, maxChars - marker.length); + return { + text: `${raw.slice(0, headLength)}${marker}`, + truncated: true, + originalLength, + }; +} + +async function extractWithBrowserEvaluate( + session: SessionLike, + format: ScrapeFormat, + selector: string | undefined +): Promise { + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + throw new Error("Session does not support DOM extraction."); + } + + const payload = await evaluate((input: { selector: string | null; format: ScrapeFormat }) => { + const getRoot = () => { + if (!input.selector) { + return document.documentElement; + } + + return document.querySelector(input.selector); + }; + + const root = getRoot(); + if (!root) { + return null as unknown as string; + } + + const baseText = (): string => { + const text = (root as HTMLElement).innerText || root.textContent || ""; + return text.replace(/\u00a0/g, " ").replace(/\r?\n{3,}/g, "\n\n").trim(); + }; + + const markdownFromNode = (node: Node, depth = 0): string => { + if (node.nodeType === Node.TEXT_NODE) { + return (node.textContent || "").replace(/\u00a0/g, " "); + } + + if (node.nodeType !== Node.ELEMENT_NODE) { + return ""; + } + + const element = node as Element; + const tag = element.tagName.toLowerCase(); + const pad = " ".repeat(depth); + + const childText = Array.from(element.childNodes) + .map((child) => markdownFromNode(child, depth + 1)) + .join(""); + + switch (tag) { + case "h1": + return `\n# ${clean(childText)}\n\n`; + case "h2": + return `\n## ${clean(childText)}\n\n`; + case "h3": + return `\n### ${clean(childText)}\n\n`; + case "h4": + return `\n#### ${clean(childText)}\n\n`; + case "h5": + return `\n##### ${clean(childText)}\n\n`; + case "h6": + return `\n###### ${clean(childText)}\n\n`; + case "p": + case "article": + case "section": + return `${clean(childText)}\n\n`; + case "blockquote": + return `\n${clean(childText).replace(/\n/g, "\n> ")}\n\n`; + case "pre": + return `\n\`\`\`\n${(element.textContent || "").replace(/\n+$/, "")}\n\`\`\`\n\n`; + case "code": + return `\`${clean(childText)}\``; + case "strong": + case "b": + return `**${clean(childText)}**`; + case "em": + case "i": + return `*${clean(childText)}*`; + case "a": { + const href = (element as HTMLAnchorElement).getAttribute("href") || ""; + return `[${clean(childText)}](${href})`; + } + case "img": { + const src = (element as HTMLImageElement).getAttribute("src") || ""; + const alt = (element as HTMLImageElement).getAttribute("alt") || ""; + return `![${alt}](${src})`; + } + case "ul": + return ( + Array.from(element.children) + .filter((item) => item.tagName.toLowerCase() === "li") + .map((item) => `${pad}- ${clean(markdownFromNode(item).trim())}`) + .join("\n") + "\n\n" + ); + case "ol": + return ( + Array.from(element.children) + .filter((item) => item.tagName.toLowerCase() === "li") + .map((item, index) => `${pad}${index + 1}. ${clean(markdownFromNode(item).trim())}`) + .join("\n") + "\n\n" + ); + case "li": + return childText.trim(); + case "div": + case "main": + case "header": + case "footer": + case "nav": + case "aside": + return `${clean(childText)}\n`; + case "br": + return "\n"; + default: + return childText; + } + }; + + const clean = (value: string): string => + value + .replace(/\n{3,}/g, "\n\n") + .replace(/\s+\n/g, "\n") + .trim(); + + if (input.format === "html") { + return (root as HTMLElement).outerHTML; + } + + if (input.format === "text") { + return baseText(); + } + + if (input.format === "markdown") { + return clean(markdownFromNode(root).trim()); + } + + return clean(root.textContent || ""); + }, { selector: selector ?? null, format }); + + if (payload === null) { + throw new Error(selector + ? `No element matched selector: ${selector}` + : "Could not extract page HTML from the browser."); + } + + if (typeof payload !== "string") { + throw new Error("Scrape operation returned an unexpected payload."); + } + + return payload; +} + +async function scrapeContent( + session: SessionLike, + format: ScrapeFormat, + selector: string | undefined +): Promise { + if (!selector && format === "html" && typeof session.content === "function") { + const pageHtml = await session.content(); + if (typeof pageHtml === "string") { + return pageHtml; + } + } + + if (!selector && format === "html" && typeof session.page?.content === "function") { + const pageHtml = await session.page.content(); + if (typeof pageHtml === "string") { + return pageHtml; + } + } + + try { + const value = await extractWithBrowserEvaluate(session, format, selector); + if (typeof value === "string") { + return value; + } + } catch (error) { + if (format !== "text") { + throw error; + } + } + + const maybeHtml = await (() => { + if (typeof session.content === "function") { + return session.content(); + } + + if (typeof session.page?.content === "function") { + return session.page.content(); + } + + return Promise.resolve(undefined); + })(); + + if (typeof maybeHtml === "string") { + return extractFallbackText(maybeHtml); + } + + throw new Error("Session does not support scrape content extraction."); +} + +export function scrapeTool(client: SteelClient): ToolDefinition { + return { + name: "steel_scrape", + label: "Scrape", + description: "Extract readable current page content. Use text by default for answering questions, markdown when structure matters, and html only for DOM/debugging cases.", + parameters: Type.Object({ + format: Type.Optional( + Type.Union( + [Type.Literal("html"), Type.Literal("markdown"), Type.Literal("text")], + { description: "Output format. Prefer text for concise reading, markdown to preserve headings/lists/links, and html only when raw DOM markup is specifically needed." } + ) + ), + selector: Type.Optional( + Type.String({ description: "Optional CSS selector to scope extraction to a specific element before converting to the requested output format" }) + ), + maxChars: Type.Optional( + Type.Integer({ + minimum: MIN_MAX_CHARS, + maximum: MAX_MAX_CHARS, + description: `Maximum characters to return after conversion to text/markdown/html (default: ${DEFAULT_MAX_CHARS}, env override: STEEL_SCRAPE_MAX_CHARS)`, + }) + ), + }), + + async execute( + _toolCallId: string, + params: { format?: ScrapeFormat; selector?: string; maxChars?: number }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_scrape", async () => { + throwIfAborted(signal); + const format = resolveFormat(params.format); + const selector = normalizeSelector(params.selector); + const maxChars = resolveMaxChars(params.maxChars); + const target = selector ? ` (selector ${selector})` : " (full page)"; + + await emitProgress(onUpdate, "steel_scrape", `Preparing ${format} scrape for${target}`); + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + throwIfAborted(signal); + const url = await readSessionUrl(session); + if (isBlankPageUrl(url)) { + throw blankPageError("scrape page content"); + } + await emitProgress(onUpdate, "steel_scrape", "Running extraction"); + const result = await withAbortSignal( + scrapeContent(session, format, selector), + signal + ); + const cleanedResult = format === "text" ? cleanInnerText(result) : result; + const limitedResult = truncateContent(cleanedResult, maxChars); + if (limitedResult.truncated) { + await emitProgress( + onUpdate, + "steel_scrape", + `Scrape output truncated to ${maxChars} chars` + ); + } + await emitProgress(onUpdate, "steel_scrape", "Scrape complete"); + + return { + content: [{ type: "text", text: limitedResult.text }], + details: { + ...sessionDetails(session, url, format, selector), + maxChars, + contentLength: limitedResult.text.length, + originalContentLength: limitedResult.originalLength, + truncated: limitedResult.truncated, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/screenshot.ts b/extensions/steel-browser/src/tools/screenshot.ts new file mode 100644 index 0000000..b8b0050 --- /dev/null +++ b/extensions/steel-browser/src/tools/screenshot.ts @@ -0,0 +1,373 @@ +import { promises as fs } from "node:fs"; +import path from "node:path"; +import { randomUUID } from "node:crypto"; +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails as baseSessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; +import { + MAX_TOOL_TIMEOUT_MS, + resolveToolTimeoutMs, +} from "./tool-settings.js"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + waitForSelector?: ( + selector: string, + options?: { state?: "attached" | "visible"; timeout?: number } + ) => Promise; + screenshot?: (options?: Record) => Promise; + locator?: (selector: string) => { + screenshot?: (options?: Record) => Promise; + }; + page?: { + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + waitForSelector?: ( + selector: string, + options?: { state?: "attached" | "visible"; timeout?: number } + ) => Promise; + screenshot?: (options?: Record) => Promise; + locator?: (selector: string) => { + screenshot?: (options?: Record) => Promise; + }; + }; + url?: (() => Promise | string) | string; +}; + +type ClipRect = { + x: number; + y: number; + width: number; + height: number; +}; + +const DEFAULT_FULL_PAGE = false; +const RELATIVE_SCREENSHOT_DIR = path.join(".artifacts", "screenshots"); + +function sessionDetails(session: SessionLike, url: string, selector: string | undefined, fullPage: boolean) { + return { + ...baseSessionDetails(session), + url, + selector: selector ?? null, + fullPage, + }; +} + +function normalizeSelector(selector?: string): string | undefined { + if (selector === undefined) { + return undefined; + } + + const trimmed = selector.trim(); + if (!trimmed) { + throw new Error("selector cannot be empty."); + } + + return trimmed; +} + +function resolveTimeoutMs(rawTimeout?: number): number { + return resolveToolTimeoutMs(rawTimeout); +} + +function normalizeFullPage(fullPage?: boolean): boolean { + return fullPage === true; +} + +async function readSessionUrl(session: SessionLike): Promise { + const direct = session.url; + if (typeof direct === "string" && direct.trim()) { + return direct; + } + + if (typeof direct === "function") { + const value = await direct.call(session); + if (typeof value === "string" && value.trim()) { + return value; + } + } + + const getter = (session as { getCurrentUrl?: () => Promise | string }).getCurrentUrl; + if (typeof getter === "function") { + const value = await getter.call(session); + if (typeof value === "string" && value.trim()) { + return value; + } + } + + return "unknown"; +} + +async function fileExists(filePath: string): Promise { + try { + await fs.access(filePath); + return true; + } catch { + return false; + } +} + +function artifactDirectory(): string { + return path.resolve(process.cwd(), RELATIVE_SCREENSHOT_DIR); +} + +function toArtifactDisplayPath(filePath: string): string { + const relativePath = path.relative(process.cwd(), filePath); + if (!relativePath || relativePath.startsWith("..")) { + return path.basename(filePath); + } + return relativePath; +} + +async function makeArtifactPath(): Promise { + const dir = artifactDirectory(); + await fs.mkdir(dir, { recursive: true }); + const safeId = randomUUID().slice(0, 8); + return path.join(dir, `steel-screenshot-${Date.now()}-${safeId}.png`); +} + +async function getWaitForSelector(session: SessionLike): Promise< + (selector: string, timeoutMs: number) => Promise +> { + if (typeof session.waitForSelector === "function") { + return async (selector, timeoutMs) => { + await session.waitForSelector?.(selector, { state: "visible", timeout: timeoutMs }); + }; + } + + if (typeof session.page?.waitForSelector === "function") { + return async (selector, timeoutMs) => { + await session.page?.waitForSelector?.(selector, { state: "visible", timeout: timeoutMs }); + }; + } + + return async () => { + return; + }; +} + +function getSessionScreenshot( + session: SessionLike +): ((options: Record) => Promise) | undefined { + if (typeof session.screenshot === "function") { + return async (options: Record) => { + return session.screenshot?.(options); + }; + } + + if (typeof session.page?.screenshot === "function") { + return async (options: Record) => { + return session.page?.screenshot?.(options); + }; + } + + return undefined; +} + +function getSessionLocator( + session: SessionLike, + selector: string +): { screenshot?: (options: Record) => Promise } | undefined { + if (typeof session.locator === "function") { + return session.locator(selector); + } + + if (typeof session.page?.locator === "function") { + return session.page.locator(selector); + } + + return undefined; +} + +async function captureWithSelector( + session: SessionLike, + selector: string, + targetPath: string, + timeoutMs: number +): Promise { + const waitForSelector = await getWaitForSelector(session); + await waitForSelector(selector, timeoutMs); + + const locator = getSessionLocator(session, selector); + if (locator?.screenshot) { + return locator.screenshot({ path: targetPath }); + } + + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + return false; + } + + const clip = await evaluate((rawSelector: string): ClipRect | null => { + const element = document.querySelector(rawSelector) as HTMLElement | null; + if (!element) { + return null; + } + + const bounds = element.getBoundingClientRect(); + if (!bounds.width || !bounds.height) { + return null; + } + + return { + x: Math.max(0, Math.floor(bounds.left)), + y: Math.max(0, Math.floor(bounds.top)), + width: Math.max(1, Math.ceil(bounds.width)), + height: Math.max(1, Math.ceil(bounds.height)), + }; + }, selector); + + if (!clip) { + throw new Error(`No element matched selector: ${selector}`); + } + + const screenshot = getSessionScreenshot(session); + if (!screenshot) { + return undefined; + } + + return screenshot({ + path: targetPath, + clip, + }); +} + +async function captureFullPage( + session: SessionLike, + targetPath: string, + fullPage: boolean +): Promise { + const screenshot = getSessionScreenshot(session); + if (!screenshot) { + throw new Error("Session does not support screenshot capture."); + } + + return screenshot({ + path: targetPath, + fullPage, + }); +} + +function isBinaryLike(value: unknown): Buffer | Uint8Array | null { + if (value instanceof Uint8Array) { + return value; + } + + if (value instanceof Buffer) { + return value; + } + + return null; +} + +async function persistScreenshotBuffer( + targetPath: string, + value: unknown +): Promise { + const buffer = isBinaryLike(value); + if (!buffer) { + return; + } + + await fs.writeFile(targetPath, Buffer.from(buffer)); +} + +async function writeArtifact(targetPath: string, sessionResult: unknown): Promise { + if (await fileExists(targetPath)) { + return; + } + + await persistScreenshotBuffer(targetPath, sessionResult); + + if (!(await fileExists(targetPath))) { + throw new Error(`Screenshot not written to expected path: ${targetPath}`); + } +} + +export function screenshotTool(client: SteelClient): ToolDefinition { + return { + name: "steel_screenshot", + label: "Screenshot", + description: "Capture a screenshot of the current page", + parameters: Type.Object({ + fullPage: Type.Optional( + Type.Boolean({ description: "Capture full page screenshot instead of viewport-only" }) + ), + selector: Type.Optional( + Type.String({ + description: "Optional CSS selector to capture a single element instead of full page", + }) + ), + timeout: Type.Optional( + Type.Integer({ + minimum: 100, + maximum: MAX_TOOL_TIMEOUT_MS, + description: "Timeout for waiting on selector when selector mode is used", + }) + ), + }), + + async execute( + _toolCallId: string, + params: { fullPage?: boolean; selector?: string; timeout?: number }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_screenshot", async () => { + throwIfAborted(signal); + const selector = normalizeSelector(params.selector); + const fullPage = normalizeFullPage(params.fullPage); + const timeoutMs = resolveTimeoutMs(params.timeout); + const target = selector ? ` element ${selector}` : " visible page"; + + await emitProgress(onUpdate, "steel_screenshot", `Preparing capture for${target}`); + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + throwIfAborted(signal); + const url = await readSessionUrl(session); + const targetPath = await makeArtifactPath(); + let screenshotResult: unknown; + + if (selector) { + await emitProgress(onUpdate, "steel_screenshot", `Capturing element ${selector}`); + screenshotResult = await captureWithSelector(session, selector, targetPath, timeoutMs); + if (!screenshotResult && !(await fileExists(targetPath))) { + throw new Error("Session does not support selector-based screenshot capture."); + } + } else { + await emitProgress(onUpdate, "steel_screenshot", fullPage ? "Capturing full-page screenshot" : "Capturing viewport screenshot"); + screenshotResult = await captureFullPage(session, targetPath, fullPage); + } + + await emitProgress(onUpdate, "steel_screenshot", `Persisting image to ${targetPath}`); + await writeArtifact(targetPath, screenshotResult); + const displayPath = toArtifactDisplayPath(targetPath); + const contentText = selector + ? `Captured screenshot of ${selector}` + : fullPage + ? "Captured full-page screenshot" + : "Captured viewport screenshot"; + + return { + content: [{ type: "text", text: contentText }], + details: { + ...sessionDetails(session, url, selector, fullPage), + filePath: displayPath, + timeoutMs, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/scroll.ts b/extensions/steel-browser/src/tools/scroll.ts new file mode 100644 index 0000000..a138c0e --- /dev/null +++ b/extensions/steel-browser/src/tools/scroll.ts @@ -0,0 +1,311 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; + +type ScrollDirection = "up" | "down"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + page?: { + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + }; +}; + +type ScrollResult = { + before: number; + after: number; + maxScrollY: number; + effectiveAmount: number; + viewportHeight: number; + contentHeight: number; + targetType: "page" | "container"; + targetSelector: string | null; +}; + +const DEFAULT_SCROLL_AMOUNT = 800; +const MIN_SCROLL_AMOUNT = 50; +const MAX_SCROLL_AMOUNT = 5000; + +function resolveDirection(rawDirection?: string): ScrollDirection { + if (rawDirection === "up") { + return "up"; + } + if (rawDirection === "down") { + return "down"; + } + return "down"; +} + +function normalizeAmount(rawAmount?: number): number { + if (rawAmount === undefined) { + return DEFAULT_SCROLL_AMOUNT; + } + + const parsed = Number(rawAmount); + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error("amount must be a positive number of pixels."); + } + + const rounded = Math.trunc(parsed); + return Math.max(MIN_SCROLL_AMOUNT, Math.min(rounded, MAX_SCROLL_AMOUNT)); +} + +function getSessionEvaluate(session: SessionLike): ((fn: (...args: any[]) => unknown, ...args: any[]) => Promise) { + if (typeof session.evaluate === "function") { + return async (fn, ...args) => { + return session.evaluate?.(fn, ...args); + }; + } + + if (typeof session.page?.evaluate === "function") { + return async (fn, ...args) => { + return session.page?.evaluate?.(fn, ...args); + }; + } + + throw new Error("Session does not support DOM evaluation."); +} + +async function performScroll( + session: SessionLike, + direction: ScrollDirection, + amount: number, + selector?: string +): Promise { + const evaluate = getSessionEvaluate(session); + + return evaluate( + (input: { + amount: number; + direction: ScrollDirection; + selector: string | null; + }) => { + const toSelector = (element: Element): string | null => { + const tag = element.tagName.toLowerCase(); + const id = element.getAttribute("id"); + if (id) { + return `#${id}`; + } + const testId = element.getAttribute("data-testid"); + if (testId) { + return `${tag}[data-testid="${testId}"]`; + } + const name = element.getAttribute("name"); + if (name) { + return `${tag}[name="${name}"]`; + } + const role = element.getAttribute("role"); + if (role) { + return `${tag}[role="${role}"]`; + } + return tag; + }; + + const isScrollable = (element: Element): boolean => { + const htmlElement = element as HTMLElement; + const style = window.getComputedStyle(htmlElement); + const overflowY = style.overflowY; + const canOverflow = overflowY === "auto" || overflowY === "scroll" || overflowY === "overlay"; + return canOverflow && htmlElement.scrollHeight > htmlElement.clientHeight + 4; + }; + + const isVisible = (element: Element): boolean => { + const htmlElement = element as HTMLElement; + const style = window.getComputedStyle(htmlElement); + const rect = htmlElement.getBoundingClientRect(); + return ( + rect.width > 0 && + rect.height > 0 && + style.visibility !== "hidden" && + style.display !== "none" && + Number.parseFloat(style.opacity) > 0 + ); + }; + + const findScrollableAncestor = (element: Element | null): Element | null => { + let current = element; + while (current) { + if (isScrollable(current) && isVisible(current)) { + return current; + } + current = current.parentElement; + } + return null; + }; + + const findBestScrollableContainer = (): Element | null => { + const elements = Array.from(document.querySelectorAll("*")); + let best: Element | null = null; + let bestScore = -1; + for (const element of elements) { + if (!isScrollable(element) || !isVisible(element)) { + continue; + } + const htmlElement = element as HTMLElement; + const score = (htmlElement.scrollHeight - htmlElement.clientHeight) * Math.max(1, htmlElement.clientHeight); + if (score > bestScore) { + best = element; + bestScore = score; + } + } + return best; + }; + + const signedAmount = input.direction === "down" ? input.amount : -input.amount; + + const scrollElement = (element: HTMLElement, targetSelector: string | null): ScrollResult => { + const before = Number(element.scrollTop || 0); + const viewportHeight = Math.max(0, element.clientHeight); + const contentHeight = Math.max(0, element.scrollHeight); + const maxScrollY = Math.max(0, contentHeight - viewportHeight); + const target = Math.max(0, Math.min(maxScrollY, before + signedAmount)); + element.scrollTo({ top: target, left: element.scrollLeft || 0 }); + return { + before, + after: Number(element.scrollTop || 0), + maxScrollY, + effectiveAmount: target - before, + viewportHeight, + contentHeight, + targetType: "container", + targetSelector, + }; + }; + + const explicitTarget = input.selector + ? findScrollableAncestor(document.querySelector(input.selector)) + : null; + if (explicitTarget) { + return scrollElement(explicitTarget as HTMLElement, toSelector(explicitTarget)); + } + + const bodyHeight = document.body?.scrollHeight ?? 0; + const docHeight = document.documentElement?.scrollHeight ?? 0; + const contentHeight = Math.max(bodyHeight, docHeight, document.body?.offsetHeight ?? 0, document.documentElement?.offsetHeight ?? 0); + const viewportHeight = Math.max(window.innerHeight, document.documentElement?.clientHeight ?? 0); + const maxScrollY = Math.max(0, contentHeight - viewportHeight); + const before = Number(window.scrollY || window.pageYOffset || 0); + const target = Math.max(0, Math.min(maxScrollY, before + signedAmount)); + window.scrollTo({ top: target, left: window.pageXOffset || window.scrollX || 0 }); + const pageResult = { + before, + after: Number(window.scrollY || window.pageYOffset || 0), + maxScrollY, + effectiveAmount: target - before, + viewportHeight, + contentHeight, + targetType: "page" as const, + targetSelector: null, + }; + + if (pageResult.before !== pageResult.after || pageResult.contentHeight > pageResult.viewportHeight) { + return pageResult; + } + + const fallbackTarget = findBestScrollableContainer(); + if (fallbackTarget) { + return scrollElement(fallbackTarget as HTMLElement, toSelector(fallbackTarget)); + } + + return pageResult; + }, + { amount, direction, selector: selector ?? null } + ) as Promise; +} + +export function scrollTool(client: SteelClient): ToolDefinition { + return { + name: "steel_scroll", + label: "Scroll", + description: "Scroll the current page or a visible scroll container up or down", + parameters: Type.Object({ + direction: Type.Optional( + Type.Union([Type.Literal("up"), Type.Literal("down")], { + description: "Direction to scroll", + }) + ), + amount: Type.Optional( + Type.Integer({ + minimum: MIN_SCROLL_AMOUNT, + maximum: MAX_SCROLL_AMOUNT, + description: "Pixel amount for one scroll action", + }) + ), + selector: Type.Optional( + Type.String({ + description: "Optional selector for an element inside the scroll target; useful for nested panes like lists, sidebars, or map results", + }) + ), + }), + + async execute( + _toolCallId: string, + params: { direction?: ScrollDirection; amount?: number; selector?: string }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_scroll", async () => { + throwIfAborted(signal); + const direction = resolveDirection(params.direction); + const amount = normalizeAmount(params.amount); + const selector = typeof params.selector === "string" && params.selector.trim() + ? params.selector.trim() + : undefined; + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + + const targetLabel = selector ? ` near ${selector}` : ""; + await emitProgress(onUpdate, "steel_scroll", `Preparing scroll ${direction} by ${amount}px${targetLabel}`); + const result = await withAbortSignal( + performScroll(session, direction, amount, selector), + signal + ); + + if (result.contentHeight <= result.viewportHeight) { + throw new Error("Page is not scrollable: content fits within viewport."); + } + + if (result.before === result.after) { + const edge = direction === "down" ? "bottom" : "top"; + throw new Error(`No scroll movement occurred; already at ${edge}.`); + } + + await emitProgress(onUpdate, "steel_scroll", `Scroll movement: ${Math.abs(result.effectiveAmount)}px`); + return { + content: [{ + type: "text", + text: `Scrolled ${direction} by ${Math.abs(result.effectiveAmount)}px.`, + }], + details: { + ...sessionDetails(session), + direction, + requestedAmount: amount, + requestedSelector: selector ?? null, + effectiveAmount: Math.abs(result.effectiveAmount), + before: result.before, + after: result.after, + maxScrollY: result.maxScrollY, + targetType: result.targetType, + targetSelector: result.targetSelector, + bounds: { + atTop: result.after <= 0, + atBottom: result.after >= result.maxScrollY, + }, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/session-control.ts b/extensions/steel-browser/src/tools/session-control.ts new file mode 100644 index 0000000..55b0a0d --- /dev/null +++ b/extensions/steel-browser/src/tools/session-control.ts @@ -0,0 +1,108 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import type { SteelSessionMode } from "../session-mode.js"; +import type { SteelClient } from "../steel-client.js"; +import { withToolError, type ToolProgressUpdater } from "./tool-runtime.js"; + +export type SteelSessionController = { + getDefaultSessionMode: () => SteelSessionMode; + getSessionMode: () => SteelSessionMode; + setSessionMode: (mode: SteelSessionMode) => void; + closeSessions: (reason: string) => Promise; +}; + +function buildPinMessage(sessionId: string | null): string { + if (sessionId) { + return `Enabled Steel session persistence for this Pi session. Current session: ${sessionId}.`; + } + + return "Enabled Steel session persistence for this Pi session."; +} + +function buildReleaseMessage( + sessionId: string | null, + nextMode: SteelSessionMode +): string { + if (sessionId) { + return `Released Steel session ${sessionId}. Runtime session mode reset to ${nextMode}.`; + } + + return `No active Steel session to release. Runtime session mode reset to ${nextMode}.`; +} + +export function pinSessionTool( + client: SteelClient, + controller: SteelSessionController +): ToolDefinition { + return { + name: "steel_pin_session", + label: "Pin Session", + description: "Keep the current Steel browser session alive across prompts until explicitly released", + parameters: Type.Object({}), + + async execute( + _toolCallId: string, + _params: {}, + _signal: AbortSignal | undefined, + _onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_pin_session", async () => { + const previousMode = controller.getSessionMode(); + controller.setSessionMode("session"); + const sessionId = client.getCurrentSessionId(); + + return { + content: [{ type: "text", text: buildPinMessage(sessionId) }], + details: { + previousMode, + mode: "session", + defaultMode: controller.getDefaultSessionMode(), + sessionId, + hasActiveSession: client.hasActiveSession(), + }, + }; + }); + }, + }; +} + +export function releaseSessionTool( + client: SteelClient, + controller: SteelSessionController +): ToolDefinition { + return { + name: "steel_release_session", + label: "Release Session", + description: "Close the current Steel browser session immediately and restore the default runtime session mode", + parameters: Type.Object({}), + + async execute( + _toolCallId: string, + _params: {}, + _signal: AbortSignal | undefined, + _onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_release_session", async () => { + const previousMode = controller.getSessionMode(); + const defaultMode = controller.getDefaultSessionMode(); + const sessionId = client.getCurrentSessionId(); + + await controller.closeSessions("steel_release_session"); + controller.setSessionMode(defaultMode); + + return { + content: [{ type: "text", text: buildReleaseMessage(sessionId, defaultMode) }], + details: { + previousMode, + mode: defaultMode, + defaultMode, + releasedSessionId: sessionId, + hadActiveSession: Boolean(sessionId), + }, + }; + }); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/session-state.ts b/extensions/steel-browser/src/tools/session-state.ts new file mode 100644 index 0000000..1ff94b8 --- /dev/null +++ b/extensions/steel-browser/src/tools/session-state.ts @@ -0,0 +1,64 @@ +type SessionGetter = (() => Promise | string) | string; + +export type SessionStateLike = { + url?: SessionGetter; + title?: SessionGetter; + getCurrentUrl?: () => Promise | string; +}; + +export async function readSessionUrl(session: SessionStateLike): Promise { + const direct = session.url; + if (typeof direct === "string" && direct.trim()) { + return direct; + } + + if (typeof direct === "function") { + const value = await direct.call(session); + if (typeof value === "string" && value.trim()) { + return value; + } + } + + if (typeof session.getCurrentUrl === "function") { + const value = await session.getCurrentUrl.call(session); + if (typeof value === "string" && value.trim()) { + return value; + } + } + + return "unknown"; +} + +export async function readSessionTitle(session: SessionStateLike): Promise { + const direct = session.title; + if (typeof direct === "string" && direct.trim()) { + return direct; + } + + if (typeof direct === "function") { + const value = await direct.call(session); + if (typeof value === "string" && value.trim()) { + return value; + } + } + + return "unknown"; +} + +export function isBlankPageUrl(url: string): boolean { + const normalized = url.trim().toLowerCase(); + return normalized === "about:blank" || normalized === "about:srcdoc"; +} + +export function freshSessionHint(): string { + return "This usually means Pi started a fresh Steel session. Navigate to a page first, or run Pi with STEEL_SESSION_MODE=session to keep the same browser across prompts."; +} + +export function blankPageError(action: string): Error { + return new Error(`Cannot ${action} because the current page is about:blank. ${freshSessionHint()}`); +} + +export function describeBlankPage(url: string): string { + return `Current URL: ${url} (fresh Steel session; navigate first or use STEEL_SESSION_MODE=session for cross-prompt continuity)`; +} + diff --git a/extensions/steel-browser/src/tools/tool-runtime.ts b/extensions/steel-browser/src/tools/tool-runtime.ts new file mode 100644 index 0000000..49dc697 --- /dev/null +++ b/extensions/steel-browser/src/tools/tool-runtime.ts @@ -0,0 +1,246 @@ +import type { AgentToolUpdateCallback } from "@mariozechner/pi-coding-agent"; + +export type ToolErrorCategory = + | "validation" + | "timeout" + | "network" + | "tool_execution" + | "unknown"; + +export type ToolProgressUpdater = AgentToolUpdateCallback<{ + context: string; + kind: "progress"; + message: string; +}> | undefined; + +const ABORT_ERROR_NAME = "AbortError"; +const ABORT_ERROR_MESSAGE = "Tool execution cancelled."; + +const TOOL_ERROR_PATTERNS: Record = { + validation: [ + "bad request", + "invalid", + "missing", + "required", + "schema", + "format", + "validation", + "unsupported value", + "not allowed", + ], + timeout: [ + "timed out", + "timeout", + "timed-out", + "deadline", + "time out", + ], + network: [ + "network", + "connection", + "econn", + "enotfound", + "dns", + "econnreset", + "econnrefused", + "proxy", + "ssl", + "certificate", + ], + tool_execution: [ + "selector", + "tool", + "navigation", + "screenshot", + "pdf", + "session", + "click", + "extract", + "not supported", + "page", + ], + unknown: [], +}; + +const TOOL_ERROR_LABELS: Record = { + validation: "Validation failed", + timeout: "Timed out", + network: "Network issue", + tool_execution: "Tool execution failed", + unknown: "Tool error", +}; + +const TOOL_ERROR_GUIDANCE: Record = { + validation: "Check required inputs and retry with corrected values.", + timeout: + "Retry with narrower scope or longer timeout values.", + network: + "Retry once connectivity is stable.", + tool_execution: + "Retrying usually succeeds; if selector-based operations fail, refresh page state and try again.", + unknown: "Retry the action and, if it repeats, rerun with simplified inputs.", +}; + +function normalizeErrorMessage(error: unknown): string { + if (error instanceof Error) { + return error.message?.trim() || "Unknown error"; + } + + if (typeof error === "string") { + return error.trim() || "Unknown error"; + } + + if (error === undefined || error === null) { + return "Unknown error"; + } + + try { + return JSON.stringify(error); + } catch { + return String(error); + } +} + +function classifyError(message: string): ToolErrorCategory { + const normalized = message.toLowerCase(); + + for (const [category, markers] of Object.entries( + TOOL_ERROR_PATTERNS + ) as [ToolErrorCategory, readonly string[]][]) { + if (markers.some((marker) => normalized.includes(marker))) { + return category; + } + } + return "unknown"; +} + +export function toolErrorMessage(context: string, error: unknown): string { + const message = normalizeErrorMessage(error); + const category = classifyError(message); + const label = TOOL_ERROR_LABELS[category]; + const guidance = TOOL_ERROR_GUIDANCE[category]; + return `${context}: ${label}. ${message}. Retry guidance: ${guidance}`; +} + +export function toolError(context: string, error: unknown): Error { + return new Error(toolErrorMessage(context, error)); +} + +function abortError(message = ABORT_ERROR_MESSAGE): Error { + const error = new Error(message); + error.name = ABORT_ERROR_NAME; + return error; +} + +export function isAbortError(error: unknown): boolean { + if (!(error instanceof Error)) { + return false; + } + if (error.name === ABORT_ERROR_NAME) { + return true; + } + + const message = error.message.toLowerCase(); + return message.includes("cancelled") || message.includes("canceled"); +} + +export function throwIfAborted(signal: AbortSignal | undefined): void { + if (signal?.aborted) { + throw abortError(); + } +} + +export function sleepWithSignal( + ms: number, + signal: AbortSignal | undefined +): Promise { + if (!signal) { + return new Promise((resolve) => setTimeout(resolve, ms)); + } + + throwIfAborted(signal); + + return new Promise((resolve, reject) => { + const timer = setTimeout(() => { + signal.removeEventListener("abort", onAbort); + resolve(); + }, ms); + const onAbort = () => { + clearTimeout(timer); + signal.removeEventListener("abort", onAbort); + reject(abortError()); + }; + signal.addEventListener("abort", onAbort, { once: true }); + }); +} + +export function withAbortSignal( + promise: Promise, + signal: AbortSignal | undefined +): Promise { + if (!signal) { + return promise; + } + + throwIfAborted(signal); + + return new Promise((resolve, reject) => { + const onAbort = () => { + signal.removeEventListener("abort", onAbort); + reject(abortError()); + }; + + signal.addEventListener("abort", onAbort, { once: true }); + promise.then( + (value) => { + signal.removeEventListener("abort", onAbort); + resolve(value); + }, + (error: unknown) => { + signal.removeEventListener("abort", onAbort); + reject(error); + } + ); + }); +} + +export function withToolError( + context: string, + operation: () => Promise, + signal?: AbortSignal +): Promise { + try { + throwIfAborted(signal); + return operation().catch((error: unknown) => { + if (isAbortError(error) || signal?.aborted) { + throw abortError(`${context}: ${ABORT_ERROR_MESSAGE}`); + } + throw toolError(context, error); + }); + } catch (error: unknown) { + if (isAbortError(error) || signal?.aborted) { + throw abortError(`${context}: ${ABORT_ERROR_MESSAGE}`); + } + throw toolError(context, error); + } +} + +export function emitProgress( + onUpdate: ToolProgressUpdater, + context: string, + message: string +): void { + if (!onUpdate) { + return; + } + + const trimmed = message.trim(); + onUpdate({ + content: [{ type: "text", text: `${context}: ${trimmed}` }], + details: { + context, + kind: "progress", + message: trimmed, + }, + }); +} diff --git a/extensions/steel-browser/src/tools/tool-settings.ts b/extensions/steel-browser/src/tools/tool-settings.ts new file mode 100644 index 0000000..76af4c3 --- /dev/null +++ b/extensions/steel-browser/src/tools/tool-settings.ts @@ -0,0 +1,56 @@ +const DEFAULT_TOOL_TIMEOUT_MS = 30_000; +const TOOL_TIMEOUT_ENV = "STEEL_TOOL_TIMEOUT_MS"; +export const MIN_TOOL_TIMEOUT_MS = 100; +export const MAX_TOOL_TIMEOUT_MS = 120_000; + +let cachedDefaultToolTimeoutMs: number | null = null; + +function parsePositiveInt(raw: string | undefined): number | null { + if (raw === undefined) { + return null; + } + + const value = raw.trim(); + if (!value) { + return null; + } + + const parsed = Number.parseInt(value, 10); + if (!Number.isFinite(parsed) || parsed <= 0) { + return null; + } + + return parsed; +} + +export function getDefaultToolTimeoutMs(): number { + if (cachedDefaultToolTimeoutMs !== null) { + return cachedDefaultToolTimeoutMs; + } + + const parsed = parsePositiveInt(process.env[TOOL_TIMEOUT_ENV]); + if (parsed === null) { + cachedDefaultToolTimeoutMs = DEFAULT_TOOL_TIMEOUT_MS; + return cachedDefaultToolTimeoutMs; + } + + cachedDefaultToolTimeoutMs = Math.max( + MIN_TOOL_TIMEOUT_MS, + Math.min(parsed, MAX_TOOL_TIMEOUT_MS) + ); + return cachedDefaultToolTimeoutMs; +} + +export function resolveToolTimeoutMs(rawTimeout: number | undefined): number { + if (rawTimeout === undefined) { + return getDefaultToolTimeoutMs(); + } + + const parsed = Number(rawTimeout); + if (!Number.isFinite(parsed) || parsed <= 0) { + throw new Error("timeout must be a positive number in milliseconds."); + } + + const rounded = Math.max(MIN_TOOL_TIMEOUT_MS, Math.trunc(parsed)); + return Math.min(rounded, MAX_TOOL_TIMEOUT_MS); +} diff --git a/extensions/steel-browser/src/tools/type.ts b/extensions/steel-browser/src/tools/type.ts new file mode 100644 index 0000000..cb33882 --- /dev/null +++ b/extensions/steel-browser/src/tools/type.ts @@ -0,0 +1,269 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails, type SteelClient } from "../steel-client.js"; +import { runWithCaptchaRecovery, type CaptchaRecoverySummary } from "./captcha-guard.js"; +import { + emitProgress, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; +import { + MAX_TOOL_TIMEOUT_MS, + resolveToolTimeoutMs, +} from "./tool-settings.js"; + +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + captchasStatus?: () => Promise; + captchasSolve?: () => Promise; + waitForSelector?: ( + selector: string, + options?: { state?: "attached" | "visible"; timeout?: number } + ) => Promise; + fill?: (selector: string, text: string) => Promise; + type?: (selector: string, text: string, options?: { delay?: number }) => Promise; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + locator?: (selector: string) => { + fill?: (text: string) => Promise; + type?: (text: string, options?: { delay?: number }) => Promise; + waitFor?: (options?: { state?: "attached" | "visible"; timeout?: number }) => Promise; + }; + page?: { + waitForSelector?: ( + selector: string, + options?: { state?: "attached" | "visible"; timeout?: number } + ) => Promise; + fill?: (selector: string, text: string) => Promise; + type?: (selector: string, text: string, options?: { delay?: number }) => Promise; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + locator?: (selector: string) => { + fill?: (text: string) => Promise; + type?: (text: string, options?: { delay?: number }) => Promise; + waitFor?: (options?: { state?: "attached" | "visible"; timeout?: number }) => Promise; + }; + }; +}; + +type FieldActionState = { + found: boolean; + editable: boolean; +}; + +function compactCaptchaRecovery(summary: CaptchaRecoverySummary) { + return { + triggered: summary.triggered, + retries: summary.retries, + solveAttempts: summary.solveAttempts, + statusChecks: summary.statusChecks, + waitTimedOut: summary.waitTimedOut, + }; +} + +function normalizeSelector(selector: string): string { + const trimmed = selector.trim(); + if (!trimmed) { + throw new Error("Selector cannot be empty."); + } + return trimmed; +} + +function normalizeTimeout(timeoutMs?: number): number { + return resolveToolTimeoutMs(timeoutMs); +} + +async function ensureField(session: SessionLike, selector: string, timeoutMs: number): Promise { + if (typeof session.waitForSelector === "function") { + await session.waitForSelector(selector, { state: "visible", timeout: timeoutMs }); + } else if (typeof session.page?.waitForSelector === "function") { + await session.page.waitForSelector(selector, { state: "visible", timeout: timeoutMs }); + } + + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + return { found: true, editable: true }; + } + + return evaluate((rawSelector: string) => { + const element = document.querySelector(rawSelector) as HTMLElement | null; + if (!element) { + return { found: false, editable: false }; + } + + const tag = element.tagName.toLowerCase(); + const isInputLike = + tag === "input" || + tag === "textarea" || + element.isContentEditable; + + const htmlInput = element as HTMLInputElement; + const editable = isInputLike && htmlInput.readOnly !== true; + const disabled = + (element as HTMLInputElement).disabled === true || + element.getAttribute("aria-disabled") === "true"; + + return { found: true, editable: editable && !disabled }; + }, selector); +} + +async function setValue(session: SessionLike, selector: string, text: string): Promise { + if (typeof session.fill === "function") { + await session.fill(selector, text); + return; + } + + if (typeof session.page?.fill === "function") { + await session.page.fill(selector, text); + return; + } + + const locator = + typeof session.locator === "function" + ? session.locator(selector) + : session.page?.locator?.(selector); + + const locatorFill = locator?.fill; + if (typeof locatorFill === "function") { + await locatorFill.call(locator, text); + return; + } + + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + throw new Error("Session does not support setting input values."); + } + + const result = await evaluate((input: { selector: string; value: string }) => { + const element = document.querySelector(input.selector) as HTMLInputElement | HTMLTextAreaElement | null; + if (!element) { + return false; + } + + element.focus(); + element.value = input.value; + element.dispatchEvent(new Event("input", { bubbles: true })); + element.dispatchEvent(new Event("change", { bubbles: true })); + return true; + }, { selector, value: text }); + + if (!result) { + throw new Error(`Element not found: ${selector}`); + } +} + +async function typeValue(session: SessionLike, selector: string, text: string): Promise { + if (typeof session.type === "function") { + await session.type(selector, text); + return; + } + + if (typeof session.page?.type === "function") { + await session.page.type(selector, text); + return; + } + + const locator = + typeof session.locator === "function" + ? session.locator(selector) + : session.page?.locator?.(selector); + + const locatorType = locator?.type; + if (typeof locatorType === "function") { + await locatorType.call(locator, text); + return; + } + + await setValue(session, selector, text); +} + +export function typeTool(client: SteelClient): ToolDefinition { + return { + name: "steel_type", + label: "Type", + description: "Type text into an input element", + parameters: Type.Object( + { + selector: Type.String({ description: "CSS selector for the input field" }), + text: Type.String({ description: "Text to type into the field" }), + clear: Type.Optional(Type.Boolean({ description: "Whether to clear the field before typing" })), + timeout: Type.Optional( + Type.Integer({ + minimum: 100, + maximum: MAX_TOOL_TIMEOUT_MS, + description: "Maximum milliseconds to wait for the input", + }) + ), + } + ), + + async execute( + _toolCallId: string, + params: { selector: string; text: string; clear?: boolean; timeout?: number }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_type", async () => { + throwIfAborted(signal); + const selector = normalizeSelector(params.selector); + const timeoutMs = normalizeTimeout(params.timeout); + const text = params.text; + const shouldClear = params.clear ?? true; + + await emitProgress(onUpdate, "steel_type", `Preparing input for ${selector}`); + const session = (await withAbortSignal( + client.getOrCreateSession(), + signal + )) as SessionLike; + await emitProgress(onUpdate, "steel_type", "Running field input sequence"); + const captchaRecovery = await runWithCaptchaRecovery({ + session, + context: "steel_type", + actionLabel: `type into ${selector}`, + onUpdate, + signal, + operation: async () => { + throwIfAborted(signal); + const fieldState = await withAbortSignal( + ensureField(session, selector, timeoutMs), + signal + ); + if (!fieldState.found) { + throw new Error(`No element matched selector: ${selector}`); + } + + if (!fieldState.editable) { + throw new Error(`Element is not editable: ${selector}`); + } + + await emitProgress( + onUpdate, + "steel_type", + shouldClear ? "Clearing existing value" : "Typing into field" + ); + if (shouldClear) { + await withAbortSignal(setValue(session, selector, text), signal); + } else { + await withAbortSignal(typeValue(session, selector, text), signal); + } + }, + }); + await emitProgress(onUpdate, "steel_type", `Input applied to ${selector}`); + + return { + content: [{ type: "text", text: `Typed into ${selector}` }], + details: { + ...sessionDetails(session), + selector, + timeoutMs, + clear: shouldClear, + textLength: text.length, + captchaRecovery: compactCaptchaRecovery(captchaRecovery), + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/src/tools/wait.ts b/extensions/steel-browser/src/tools/wait.ts new file mode 100644 index 0000000..1cc096a --- /dev/null +++ b/extensions/steel-browser/src/tools/wait.ts @@ -0,0 +1,236 @@ +import type { ExtensionContext, ToolDefinition } from "@mariozechner/pi-coding-agent"; +import { Type } from "@sinclair/typebox"; +import { sessionDetails as baseSessionDetails, type SteelClient } from "../steel-client.js"; +import { + emitProgress, + sleepWithSignal, + throwIfAborted, + withAbortSignal, + withToolError, + type ToolProgressUpdater, +} from "./tool-runtime.js"; +import { + MAX_TOOL_TIMEOUT_MS, + MIN_TOOL_TIMEOUT_MS, + resolveToolTimeoutMs, +} from "./tool-settings.js"; + +type WaitState = "attached" | "visible"; +type SessionLike = { + id: string; + sessionViewerUrl?: string | null; + waitForSelector?: ( + selector: string, + options?: { state?: WaitState; timeout?: number } + ) => Promise; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + page?: { + waitForSelector?: ( + selector: string, + options?: { state?: WaitState; timeout?: number } + ) => Promise; + evaluate?: (fn: (...args: any[]) => T, ...args: any[]) => Promise; + }; + url?: (() => Promise | string) | string; +}; + +const POLL_DELAY_MS = 100; + +function sessionDetails(session: SessionLike, url: string) { + return { + ...baseSessionDetails(session), + url, + }; +} + +function normalizeSelector(rawSelector?: string): string { + if (typeof rawSelector !== "string") { + throw new Error("selector is required and must be a string."); + } + + const trimmed = rawSelector.trim(); + if (!trimmed) { + throw new Error("selector cannot be empty."); + } + + return trimmed; +} + +function resolveTimeout(rawTimeout?: number): number { + return resolveToolTimeoutMs(rawTimeout); +} + +function resolveState(rawState?: string): WaitState { + if (rawState === "attached") { + return "attached"; + } + return "visible"; +} + +function getWaitFunction(session: SessionLike): ((selector: string, state: WaitState, timeoutMs: number, signal: AbortSignal | undefined) => Promise) { + if (typeof session.waitForSelector === "function") { + return async (selector, state, timeoutMs, signal) => { + throwIfAborted(signal); + await withAbortSignal( + session.waitForSelector?.(selector, { state, timeout: timeoutMs }) as Promise, + signal + ); + }; + } + + if (typeof session.page?.waitForSelector === "function") { + return async (selector, state, timeoutMs, signal) => { + throwIfAborted(signal); + await withAbortSignal( + session.page?.waitForSelector?.(selector, { state, timeout: timeoutMs }) as Promise, + signal + ); + }; + } + + const evaluate = session.evaluate ?? session.page?.evaluate; + if (typeof evaluate !== "function") { + throw new Error("Session does not support selector waiting."); + } + + return async (selector, state, timeoutMs, signal) => { + const deadline = Date.now() + timeoutMs; + + while (true) { + throwIfAborted(signal); + const isMatched = await withAbortSignal( + evaluate( + (input: { selector: string; state: WaitState }) => { + const element = document.querySelector(input.selector); + if (!element) { + return false; + } + + if (input.state === "attached") { + return true; + } + + const rect = element.getBoundingClientRect(); + const style = getComputedStyle(element); + const isVisible = + rect.width > 0 && + rect.height > 0 && + style.opacity !== "0" && + style.visibility !== "hidden" && + style.display !== "none" && + Number.parseFloat(style.opacity) > 0; + + return isVisible; + }, + { selector, state } + ) as Promise, signal); + + if (isMatched) { + return; + } + + if (Date.now() > deadline) { + throw new Error("selector wait timed out"); + } + + await sleepWithSignal(Math.min(POLL_DELAY_MS, Math.max(10, deadline - Date.now())), signal); + } + }; +} + +async function readSessionUrl(session: SessionLike): Promise { + const direct = session.url; + if (typeof direct === "string" && direct.trim()) { + return direct; + } + + if (typeof direct === "function") { + const value = await direct.call(session); + if (typeof value === "string" && value.trim()) { + return value; + } + } + + const getter = (session as { getCurrentUrl?: () => Promise | string }).getCurrentUrl; + if (typeof getter === "function") { + const value = await getter.call(session); + if (typeof value === "string" && value.trim()) { + return value; + } + } + + return "unknown"; +} + +export function waitTool(client: SteelClient): ToolDefinition { + return { + name: "steel_wait", + label: "Wait", + description: "Wait for an element state with timeout", + parameters: Type.Object({ + selector: Type.String({ description: "CSS selector to wait for" }), + timeout: Type.Optional( + Type.Integer({ + minimum: MIN_TOOL_TIMEOUT_MS, + maximum: MAX_TOOL_TIMEOUT_MS, + description: "Maximum milliseconds to wait for selector state", + }) + ), + state: Type.Optional( + Type.Union([Type.Literal("attached"), Type.Literal("visible")], { + description: "Selector state to wait for", + }) + ), + }), + + async execute( + _toolCallId: string, + params: { selector?: string; timeout?: number; state?: WaitState }, + signal: AbortSignal | undefined, + onUpdate: ToolProgressUpdater, + _ctx: ExtensionContext + ): Promise<{ content: Array<{ type: "text"; text: string }>; details: object }> { + return withToolError("steel_wait", async () => { + throwIfAborted(signal); + const selector = normalizeSelector(params.selector); + const timeoutMs = resolveTimeout(params.timeout); + const state = resolveState(params.state); + const session = (await withAbortSignal(client.getOrCreateSession(), signal)) as SessionLike; + throwIfAborted(signal); + const url = await readSessionUrl(session); + + await emitProgress(onUpdate, "steel_wait", `Waiting for ${selector} with state ${state}`); + + try { + const waitForSelector = getWaitFunction(session); + await waitForSelector(selector, state, timeoutMs, signal); + } catch (error) { + const message = String(error instanceof Error ? error.message : ""); + if (/timed? ?out|timeout/i.test(message)) { + throw new Error(`Timed out waiting for selector "${selector}" after ${timeoutMs}ms.`); + } + + throw error instanceof Error + ? error + : new Error(`Failed to wait for selector "${selector}"`); + } + + await emitProgress(onUpdate, "steel_wait", `Matched ${selector}`); + + return { + content: [{ + type: "text", + text: `Selector matched: ${selector}`, + }], + details: { + ...sessionDetails(session, url), + selector, + state, + timeoutMs, + success: true, + }, + }; + }, signal); + }, + }; +} diff --git a/extensions/steel-browser/tests/steel-client.test.ts b/extensions/steel-browser/tests/steel-client.test.ts new file mode 100644 index 0000000..07be652 --- /dev/null +++ b/extensions/steel-browser/tests/steel-client.test.ts @@ -0,0 +1,165 @@ +import { strict as assert } from "node:assert/strict"; +import { mkdtemp, rm, writeFile } from "node:fs/promises"; +import os from "node:os"; +import path from "node:path"; +import { afterEach, describe, it } from "node:test"; + +import { + buildSessionConnectURL, + SteelClient, + resolveSessionConnectURL, + resolveSessionId, + resolveSessionViewerURL, + sessionDetails, +} from "../dist/steel-client.js"; + +const ENV_KEYS = [ + "STEEL_API_KEY", + "STEEL_CONFIG_DIR", + "STEEL_BASE_URL", + "STEEL_BROWSER_API_URL", + "STEEL_LOCAL_API_URL", + "STEEL_API_URL", + "STEEL_SOLVE_CAPTCHA", + "STEEL_USE_PROXY", + "STEEL_PROXY_URL", + "STEEL_SESSION_HEADLESS", + "STEEL_SESSION_PERSIST_PROFILE", + "STEEL_SESSION_CREDENTIALS", + "STEEL_SESSION_REGION", + "STEEL_SESSION_PROFILE_ID", + "STEEL_SESSION_NAMESPACE", + "STEEL_SESSION_TIMEOUT_MS", +] as const; + +const ORIGINAL_ENV = new Map( + ENV_KEYS.map((key) => [key, process.env[key]]) +); + +afterEach(async () => { + for (const key of ENV_KEYS) { + const value = ORIGINAL_ENV.get(key); + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + } +}); + +describe("SteelClient runtime resolution", () => { + it("reads API key from Steel config when env is unset", async () => { + const configDir = await mkdtemp(path.join(os.tmpdir(), "pi-steel-config-")); + try { + await writeFile( + path.join(configDir, "config.json"), + JSON.stringify({ apiKey: "config-key" }), + "utf-8" + ); + delete process.env.STEEL_API_KEY; + process.env.STEEL_CONFIG_DIR = configDir; + + const client = new SteelClient(); + + assert.equal((client as unknown as { apiKey: string | null }).apiKey, "config-key"); + assert.equal( + ((client as unknown as { client: { steelAPIKey: string | null } }).client.steelAPIKey), + "config-key" + ); + } finally { + await rm(configDir, { recursive: true, force: true }); + } + }); + + it("accepts local browser api url from Steel config and strips trailing /v1", async () => { + const configDir = await mkdtemp(path.join(os.tmpdir(), "pi-steel-config-")); + try { + await writeFile( + path.join(configDir, "config.json"), + JSON.stringify({ browser: { apiUrl: "http://127.0.0.1:3000/v1" } }), + "utf-8" + ); + delete process.env.STEEL_API_KEY; + process.env.STEEL_CONFIG_DIR = configDir; + + const client = new SteelClient(); + const internal = (client as unknown as { client: { baseURL: string }; apiKey: string | null }); + + assert.equal(internal.apiKey, null); + assert.equal(internal.client.baseURL, "http://127.0.0.1:3000"); + } finally { + await rm(configDir, { recursive: true, force: true }); + } + }); + + it("maps session defaults from env into session create options", () => { + process.env.STEEL_API_KEY = "env-key"; + process.env.STEEL_SESSION_HEADLESS = "true"; + process.env.STEEL_SESSION_PERSIST_PROFILE = "true"; + process.env.STEEL_SESSION_CREDENTIALS = "true"; + process.env.STEEL_SESSION_REGION = "iad"; + process.env.STEEL_SESSION_PROFILE_ID = "profile-123"; + process.env.STEEL_SESSION_NAMESPACE = "ops"; + + const client = new SteelClient(); + const options = (client as unknown as { + sessionCreateOptions: Record; + }).sessionCreateOptions; + + assert.equal(options.headless, true); + assert.equal(options.persistProfile, true); + assert.deepEqual(options.credentials, {}); + assert.equal(options.region, "iad"); + assert.equal(options.profileId, "profile-123"); + assert.equal(options.namespace, "ops"); + }); +}); + +describe("session normalization helpers", () => { + it("resolves flexible session id and connect url keys", () => { + assert.equal(resolveSessionId({ sessionId: "sess-1" }), "sess-1"); + assert.equal(resolveSessionConnectURL({ cdpUrl: "wss://connect.example/ws" }), "wss://connect.example/ws"); + }); + + it("injects missing apiKey and sessionId into connect URLs", () => { + assert.equal( + buildSessionConnectURL( + { id: "sess-1", websocketUrl: "wss://connect.steel.dev/" }, + "test-key" + ), + "wss://connect.steel.dev/?apiKey=test-key&sessionId=sess-1" + ); + assert.equal( + buildSessionConnectURL( + { id: "sess-1" }, + "test-key" + ), + "wss://connect.steel.dev?apiKey=test-key&sessionId=sess-1" + ); + }); + + it("prefers explicit viewer url and falls back to viewer base when needed", () => { + assert.equal( + resolveSessionViewerURL({ viewerUrl: "https://viewer.example/session/1" }, "https://app.steel.dev"), + "https://viewer.example/session/1" + ); + assert.equal( + resolveSessionViewerURL({ debugUrl: "https://debug.example/session/1" }, "https://app.steel.dev"), + "https://debug.example/session/1" + ); + assert.equal( + resolveSessionViewerURL({ id: "sess-1" }, "https://app.steel.dev"), + "https://app.steel.dev/sessions/sess-1" + ); + }); + + it("preserves normalized viewer data in session details", () => { + assert.deepEqual( + sessionDetails({ id: "sess-1", sessionViewerUrl: "https://viewer.example/session/1" }), + { + sessionId: "sess-1", + sessionViewerUrl: "https://viewer.example/session/1", + } + ); + }); +}); diff --git a/extensions/steel-browser/tests/tools.test.ts b/extensions/steel-browser/tests/tools.test.ts new file mode 100644 index 0000000..cb5b384 --- /dev/null +++ b/extensions/steel-browser/tests/tools.test.ts @@ -0,0 +1,1190 @@ +import { strict as assert } from "node:assert/strict"; +import { rm } from "node:fs/promises"; +import path from "node:path"; +import { describe, it } from "node:test"; + +import steelExtension from "../dist/index.js"; +import { navigateTool } from "../dist/tools/navigate.js"; +import { scrapeTool } from "../dist/tools/scrape.js"; +import { screenshotTool } from "../dist/tools/screenshot.js"; +import { pdfTool } from "../dist/tools/pdf.js"; +import { clickTool } from "../dist/tools/click.js"; +import { computerTool } from "../dist/tools/computer.js"; +import { typeTool } from "../dist/tools/type.js"; +import { fillFormTool } from "../dist/tools/fill-form.js"; +import { waitTool } from "../dist/tools/wait.js"; +import { extractTool } from "../dist/tools/extract.js"; +import { findElementsTool } from "../dist/tools/find-elements.js"; +import { scrollTool } from "../dist/tools/scroll.js"; +import { pinSessionTool, releaseSessionTool } from "../dist/tools/session-control.js"; +import { goBackTool, getUrlTool, getTitleTool } from "../dist/tools/navigation.js"; +import type { SteelSessionMode } from "../dist/session-mode.js"; + +type MockToolResult = { + content: Array<{ type: "text"; text: string }>; + details?: Record; +}; + +type MockTool = { + name: string; + parameters?: { + type?: string; + properties?: Record; + additionalProperties?: boolean; + }; + execute: ( + _toolCallId: string, + _params: Record, + _signal: AbortSignal, + onUpdate: (update: string) => Promise, + _ctx: unknown + ) => Promise; +}; + +type MockPiApi = { + registerTool: (tool: MockTool) => void; + on: (eventName: string, _handler: (...args: unknown[]) => unknown) => void; + onShutdown: (handler: () => Promise | void) => Promise; +}; + +type MockSession = { + id: string; + [key: string]: unknown; +}; + +type MockClient = { + getOrCreateSession: () => Promise; + getCurrentSessionId?: () => string | null; + hasActiveSession?: () => boolean; + refreshSession?: ( + options?: { useProxy?: boolean; proxyUrl?: string | null } + ) => Promise; + isProxyConfigured?: () => boolean; + closeAllSessions?: () => Promise; +}; + +function createMockClient(session: MockSession): MockClient { + return { + getOrCreateSession: async () => session, + getCurrentSessionId: () => session.id, + hasActiveSession: () => true, + }; +} + +function assertTextResult(result: MockToolResult): void { + assert.ok(Array.isArray(result.content), "tool should return content array"); + assert.equal(result.content.length, 1); + assert.equal(result.content[0].type, "text"); + assert.equal(typeof result.content[0].text, "string"); + assert.ok(result.content[0].text.length > 0); + assert.equal(typeof result.details, "object"); + assert.ok(result.details?.sessionId); + assert.equal(typeof result.details?.sessionViewerUrl, "string"); +} + +function createUpdatesCollector() { + const updates: string[] = []; + const onUpdate = async (update: string) => { + updates.push(update); + }; + return { updates, onUpdate }; +} + +function withEnv(key: string, value: string | undefined, fn: () => T): T { + const original = process.env[key]; + if (value === undefined) { + delete process.env[key]; + } else { + process.env[key] = value; + } + + try { + return fn(); + } finally { + if (original === undefined) { + delete process.env[key]; + } else { + process.env[key] = original; + } + } +} + +async function executeTool(tool: MockTool, params: Record, session: MockSession): Promise<{ + result: MockToolResult; + updates: string[]; +}> { + const client = createMockClient(session); + const toolWithClient = (tool as unknown) as MockTool; + const actual = { + navigate: navigateTool, + scrape: scrapeTool, + screenshot: screenshotTool, + pdf: pdfTool, + click: clickTool, + computer: computerTool, + type: typeTool, + fillForm: fillFormTool, + wait: waitTool, + extract: extractTool, + findElements: findElementsTool, + scroll: scrollTool, + goBack: goBackTool, + getUrl: getUrlTool, + getTitle: getTitleTool, + } as Record; + + const boundTool = + toolWithClient === actual.navigate + ? navigateTool(client as unknown as never) + : toolWithClient === actual.scrape + ? scrapeTool(client as unknown as never) + : toolWithClient === actual.screenshot + ? screenshotTool(client as unknown as never) + : toolWithClient === actual.pdf + ? pdfTool(client as unknown as never) + : toolWithClient === actual.click + ? clickTool(client as unknown as never) + : toolWithClient === actual.computer + ? computerTool(client as unknown as never) + : toolWithClient === actual.type + ? typeTool(client as unknown as never) + : toolWithClient === actual.fillForm + ? fillFormTool(client as unknown as never) + : toolWithClient === actual.wait + ? waitTool(client as unknown as never) + : toolWithClient === actual.extract + ? extractTool(client as unknown as never) + : toolWithClient === actual.findElements + ? findElementsTool(client as unknown as never) + : toolWithClient === actual.scroll + ? scrollTool(client as unknown as never) + : toolWithClient === actual.goBack + ? goBackTool(client as unknown as never) + : toolWithClient === actual.getUrl + ? getUrlTool(client as unknown as never) + : toolWithClient === actual.getTitle + ? getTitleTool(client as unknown as never) + : undefined; + + assert.ok(boundTool, `Unable to bind mock client for tool ${toolWithClient.name}`); + + const { updates, onUpdate } = createUpdatesCollector(); + const result = await boundTool!.execute("call-001", params, new AbortController().signal, onUpdate, null); + return { result, updates }; +} + +describe("Tool registration contracts", () => { + const expectedTools = [ + "steel_navigate", + "steel_scrape", + "steel_screenshot", + "steel_pdf", + "steel_click", + "steel_computer", + "steel_find_elements", + "steel_type", + "steel_fill_form", + "steel_wait", + "steel_extract", + "steel_scroll", + "steel_go_back", + "steel_get_url", + "steel_get_title", + "steel_pin_session", + "steel_release_session", + ]; + + const requiredTopLevelParams: Record = { + steel_navigate: ["url"], + steel_scrape: [], + steel_screenshot: [], + steel_pdf: [], + steel_click: ["selector"], + steel_computer: ["action"], + steel_find_elements: [], + steel_type: ["selector", "text"], + steel_fill_form: ["fields"], + steel_wait: ["selector"], + steel_extract: ["schema"], + steel_scroll: [], + steel_go_back: [], + steel_get_url: [], + steel_get_title: [], + steel_pin_session: [], + steel_release_session: [], + }; + + it("registers all tools in expected order", async () => { + const tools = withEnv("STEEL_API_KEY", "test-key", () => { + const registeredTools: MockTool[] = []; + steelExtension({ + registerTool: (tool: MockTool) => { + registeredTools.push(tool); + }, + on: () => { + return; + }, + onShutdown: async () => { + return; + }, + } as never); + return registeredTools; + }); + + assert.deepEqual( + tools.map((tool) => tool.name), + expectedTools, + "tool registration order changed" + ); + + for (const tool of tools) { + const expectedFields = requiredTopLevelParams[tool.name]; + assert.equal(tool.name in requiredTopLevelParams, true); + assert.equal(tool.parameters?.type, "object"); + const properties = tool.parameters?.properties ?? {}; + for (const key of expectedFields) { + assert.ok(Object.prototype.hasOwnProperty.call(properties, key), `${tool.name} missing required schema field ${key}`); + } + assert.ok(tool.execute instanceof Function); + } + }); + + it("registers runtime cleanup hooks for turn, agent, and session boundaries", () => { + const registeredEvents = withEnv("STEEL_API_KEY", "test-key", () => + withEnv("STEEL_SESSION_MODE", undefined, () => { + const eventNames: string[] = []; + steelExtension({ + registerTool: () => { + return; + }, + on: (eventName: string) => { + eventNames.push(eventName); + }, + onShutdown: async () => { + return; + }, + } as MockPiApi as never); + return eventNames; + }) + ); + + assert.deepEqual(registeredEvents, [ + "turn_end", + "agent_end", + "session_before_switch", + "session_shutdown", + ]); + }); + + it("registers explicit session control tools", async () => { + let mode: SteelSessionMode = "agent"; + let closeCalls = 0; + const client: MockClient = { + getOrCreateSession: async () => ({ id: "session-1" }), + getCurrentSessionId: () => "session-1", + hasActiveSession: () => true, + closeAllSessions: async () => { + closeCalls += 1; + }, + }; + + const controller = { + getDefaultSessionMode: () => "agent" as const, + getSessionMode: () => mode, + setSessionMode: (nextMode: SteelSessionMode) => { + mode = nextMode; + }, + closeSessions: async () => { + closeCalls += 1; + }, + }; + + const pin = pinSessionTool(client as never, controller); + const release = releaseSessionTool(client as never, controller); + + const pinResult = await pin.execute( + "call-001", + {}, + new AbortController().signal, + async () => {}, + null + ); + + assert.equal(mode, "session"); + assert.match(pinResult.content[0].text, /Enabled Steel session persistence/i); + assert.match(pinResult.content[0].text, /Current session: session-1/i); + assert.equal(pinResult.details?.mode, "session"); + + const releaseResult = await release.execute( + "call-002", + {}, + new AbortController().signal, + async () => {}, + null + ); + + assert.equal(mode, "agent"); + assert.equal(closeCalls, 1); + assert.match(releaseResult.content[0].text, /Released Steel session session-1/i); + assert.equal(releaseResult.details?.mode, "agent"); + }); + + it("executes navigation tool with normalized URL and response contract", async () => { + const session: MockSession = { + id: "session-1", + goto: async () => {}, + }; + const { result } = await executeTool(navigateTool as unknown as MockTool, { url: "example.com" }, session); + + assertTextResult(result); + assert.equal(result.details?.url, "https://example.com/"); + assert.equal(result.details?.waitUntil, "networkidle"); + }); + + it("accepts uppercase HTTP scheme without corrupting the target URL", async () => { + const calls: string[] = []; + const session: MockSession = { + id: "session-1", + goto: async (url: string) => { + calls.push(url); + }, + }; + + const { result } = await executeTool( + navigateTool as unknown as MockTool, + { url: "HTTP://example.com/path" }, + session + ); + + assertTextResult(result); + assert.equal(calls[0], "http://example.com/path"); + assert.equal(result.details?.url, "http://example.com/path"); + }); + + it("accepts host:port input and normalizes to https", async () => { + const calls: string[] = []; + const session: MockSession = { + id: "session-1", + goto: async (url: string) => { + calls.push(url); + }, + }; + + const { result } = await executeTool( + navigateTool as unknown as MockTool, + { url: "localhost:3000/login" }, + session + ); + + assertTextResult(result); + assert.equal(calls[0], "https://localhost:3000/login"); + assert.equal(result.details?.url, "https://localhost:3000/login"); + }); + + it("rejects non-http URL schemes", async () => { + const client = createMockClient({ + id: "session-1", + goto: async () => {}, + }); + const tool = navigateTool(client as never); + + await assert.rejects( + () => + tool.execute( + "call-001", + { url: "ftp://example.com" }, + new AbortController().signal, + async () => {}, + null + ), + /Only http and https URLs are supported/, + "expected non-http scheme to be rejected" + ); + }); + + it("retries tunnel failures with a fresh session before succeeding", async () => { + const previousRetries = process.env.STEEL_NAVIGATE_RETRY_COUNT; + process.env.STEEL_NAVIGATE_RETRY_COUNT = "0"; + try { + const sessions: MockSession[] = [ + { + id: "session-1", + goto: async () => { + throw new Error("page.goto: net::ERR_TUNNEL_CONNECTION_FAILED at https://example.com"); + }, + }, + { + id: "session-2", + goto: async () => {}, + }, + ]; + + let refreshCalls = 0; + const client: MockClient = { + getOrCreateSession: async () => sessions[0], + refreshSession: async () => { + refreshCalls += 1; + return sessions[1]; + }, + isProxyConfigured: () => true, + }; + + const tool = navigateTool(client as never); + const result = await tool.execute( + "call-001", + { url: "https://example.com" }, + new AbortController().signal, + async () => {}, + null + ); + + assertTextResult(result as unknown as MockToolResult); + assert.equal((result.details as Record).sessionId, "session-2"); + const recovery = (result.details as Record).tunnelRecovery as + | Record + | null; + assert.equal(recovery?.mode, "fresh_session"); + assert.equal(refreshCalls, 1); + } finally { + process.env.STEEL_NAVIGATE_RETRY_COUNT = previousRetries; + } + }); + + it("falls back to no-proxy session after repeated tunnel failures", async () => { + const previousRetries = process.env.STEEL_NAVIGATE_RETRY_COUNT; + process.env.STEEL_NAVIGATE_RETRY_COUNT = "0"; + try { + const sessions: MockSession[] = [ + { + id: "session-1", + goto: async () => { + throw new Error("page.goto: net::ERR_TUNNEL_CONNECTION_FAILED at https://example.com"); + }, + }, + { + id: "session-2", + goto: async () => { + throw new Error("page.goto: net::ERR_TUNNEL_CONNECTION_FAILED at https://example.com"); + }, + }, + { + id: "session-3", + goto: async () => {}, + }, + ]; + + const refreshOptions: Array<{ useProxy?: boolean; proxyUrl?: string | null } | undefined> = []; + let refreshIndex = 0; + const client: MockClient = { + getOrCreateSession: async () => sessions[0], + refreshSession: async (options) => { + refreshOptions.push(options); + refreshIndex += 1; + return sessions[refreshIndex]; + }, + isProxyConfigured: () => true, + }; + + const tool = navigateTool(client as never); + const result = await tool.execute( + "call-001", + { url: "https://example.com" }, + new AbortController().signal, + async () => {}, + null + ); + + assertTextResult(result as unknown as MockToolResult); + assert.equal((result.details as Record).sessionId, "session-3"); + const recovery = (result.details as Record).tunnelRecovery as + | Record + | null; + assert.equal(recovery?.mode, "no_proxy"); + assert.equal(refreshOptions.length, 2); + assert.equal(refreshOptions[1]?.useProxy, false); + assert.equal(refreshOptions[1]?.proxyUrl, null); + } finally { + process.env.STEEL_NAVIGATE_RETRY_COUNT = previousRetries; + } + }); + + it("executes scrape tool and returns extracted text", async () => { + const session: MockSession = { + id: "session-1", + content: async () => "

Title

", + evaluate: async (_fn: unknown, input: unknown) => { + assert.equal(typeof input, "object"); + return "Title"; + }, + }; + const { result } = await executeTool(scrapeTool as unknown as MockTool, { format: "text" }, session); + + assertTextResult(result); + assert.equal(result.content[0].text, "Title"); + assert.equal(result.details?.format, "text"); + }); + + it("truncates scrape output when maxChars is exceeded", async () => { + const longText = "A".repeat(400); + const session: MockSession = { + id: "session-1", + content: async () => "ignored", + evaluate: async (_fn: unknown, input: unknown) => { + assert.equal(typeof input, "object"); + return longText; + }, + }; + + const { result } = await executeTool( + scrapeTool as unknown as MockTool, + { format: "text", maxChars: 200 }, + session + ); + + assertTextResult(result); + assert.equal(result.details?.truncated, true); + assert.equal(result.details?.originalContentLength, longText.length); + assert.equal(result.details?.maxChars, 200); + assert.ok((result.content[0].text ?? "").includes("[truncated ")); + assert.ok((result.content[0].text ?? "").length <= 200); + }); + + it("supports short scrape excerpts below 200 characters", async () => { + const longText = "B".repeat(400); + const session: MockSession = { + id: "session-1", + content: async () => "ignored", + evaluate: async (_fn: unknown, input: unknown) => { + assert.equal(typeof input, "object"); + return longText; + }, + }; + + const { result } = await executeTool( + scrapeTool as unknown as MockTool, + { format: "text", maxChars: 150 }, + session + ); + + assertTextResult(result); + assert.equal(result.details?.maxChars, 150); + assert.equal(result.details?.truncated, true); + assert.ok((result.content[0].text ?? "").length <= 150); + }); + + it("captures screenshot artifact and returns artifact path", async () => { + const session: MockSession = { + id: "session-1", + url: "https://page.example/", + screenshot: async () => Buffer.from("png-bytes"), + }; + + const { result } = await executeTool(screenshotTool as unknown as MockTool, { fullPage: true }, session); + assertTextResult(result); + + const filePath = result.details?.filePath; + assert.equal(typeof filePath, "string"); + assert.ok(path.basename(filePath as string).startsWith("steel-screenshot-")); + assert.equal(path.extname(filePath as string), ".png"); + await rm(filePath as string); + }); + + it("generates PDF artifact and returns artifact metadata", async () => { + const session: MockSession = { + id: "session-1", + url: "https://page.example/", + pdf: async () => Buffer.from("pdf-bytes"), + }; + + const { result } = await executeTool(pdfTool as unknown as MockTool, {}, session); + assertTextResult(result); + assert.match(result.content[0].text, /^PDF saved: \.artifacts\/pdfs\/steel-pdf-/); + + const filePath = result.details?.filePath; + assert.equal(typeof filePath, "string"); + assert.ok(path.basename(filePath as string).startsWith("steel-pdf-")); + assert.equal(path.extname(filePath as string), ".pdf"); + + const absoluteFilePath = result.details?.absoluteFilePath; + assert.equal(typeof absoluteFilePath, "string"); + assert.ok(path.isAbsolute(absoluteFilePath as string)); + + const artifact = result.details?.artifact as Record | undefined; + assert.ok(artifact); + assert.equal(artifact?.type, "pdf"); + assert.equal(artifact?.mimeType, "application/pdf"); + const artifactPath = artifact?.path; + assert.equal(typeof artifactPath, "string"); + assert.equal(artifactPath, filePath); + await rm(artifactPath as string); + }); + + it("executes click tool when element is clickable", async () => { + const calls: string[] = []; + const session: MockSession = { + id: "session-1", + waitForSelector: async (selector) => { + calls.push(`wait:${selector}`); + }, + evaluate: async () => ({ found: true, visible: true, clickable: true, disabled: false }), + click: async (selector) => { + calls.push(`click:${selector}`); + }, + }; + + const { result } = await executeTool(clickTool as unknown as MockTool, { selector: "#btn" }, session); + assertTextResult(result); + assert.equal(calls[0], "wait:#btn"); + assert.equal(calls[1], "click:#btn"); + assert.equal(result.details?.selector, "#btn"); + }); + + it("executes click tool with Playwright text selectors via locator", async () => { + const calls: string[] = []; + const session: MockSession = { + id: "session-1", + locator: (selector: string) => ({ + waitFor: async () => { + calls.push(`wait:${selector}`); + }, + isVisible: async () => true, + isEnabled: async () => true, + click: async () => { + calls.push(`click:${selector}`); + }, + }), + }; + + const { result } = await executeTool( + clickTool as unknown as MockTool, + { selector: "text=Signup" }, + session + ); + + assertTextResult(result); + assert.equal(calls[0], "wait:text=Signup"); + assert.equal(calls[1], "click:text=Signup"); + }); + + it("retries click via captcha recovery when overlay blocks pointer events", async () => { + const previousWait = process.env.STEEL_CAPTCHA_WAIT_MS; + const previousPoll = process.env.STEEL_CAPTCHA_POLL_INTERVAL_MS; + const previousRetries = process.env.STEEL_CAPTCHA_MAX_RETRIES; + process.env.STEEL_CAPTCHA_WAIT_MS = "1000"; + process.env.STEEL_CAPTCHA_POLL_INTERVAL_MS = "250"; + process.env.STEEL_CAPTCHA_MAX_RETRIES = "1"; + + try { + let clickAttempts = 0; + let statusChecks = 0; + const session: MockSession = { + id: "session-1", + waitForSelector: async () => {}, + evaluate: async () => ({ + found: true, + visible: true, + clickable: true, + disabled: false, + }), + click: async () => { + clickAttempts += 1; + if (clickAttempts === 1) { + throw new Error("subtree intercepts pointer events"); + } + }, + captchasStatus: async () => { + statusChecks += 1; + if (statusChecks === 1) { + return [{ isSolvingCaptcha: false, tasks: [{}] }]; + } + return [{ isSolvingCaptcha: false, tasks: [] }]; + }, + captchasSolve: async () => ({ success: true, message: "captcha solve requested" }), + }; + + const { result } = await executeTool( + clickTool as unknown as MockTool, + { selector: "#btn" }, + session + ); + + assertTextResult(result); + assert.equal(clickAttempts, 2); + const captchaRecovery = result.details?.captchaRecovery as Record; + assert.equal(captchaRecovery?.triggered, true); + assert.equal(captchaRecovery?.retries, 1); + assert.equal(captchaRecovery?.solveAttempts, 1); + assert.ok(Number(captchaRecovery?.statusChecks) >= 1); + } finally { + process.env.STEEL_CAPTCHA_WAIT_MS = previousWait; + process.env.STEEL_CAPTCHA_POLL_INTERVAL_MS = previousPoll; + process.env.STEEL_CAPTCHA_MAX_RETRIES = previousRetries; + } + }); + + it("executes computer action and persists screenshot artifact", async () => { + const session: MockSession = { + id: "session-1", + computer: async () => ({ + base64_image: Buffer.from("png-bytes").toString("base64"), + output: "clicked", + }), + }; + + const { result } = await executeTool( + computerTool as unknown as MockTool, + { + action: "click_mouse", + button: "left", + coordinates: [100, 220], + screenshot: true, + }, + session + ); + + assertTextResult(result); + assert.equal(result.details?.action, "click_mouse"); + const filePath = result.details?.filePath; + assert.equal(typeof filePath, "string"); + assert.ok(path.basename(filePath as string).startsWith("steel-computer-")); + assert.equal(path.extname(filePath as string), ".png"); + await rm(filePath as string); + }); + + it("types text into field after clearing and returns field metadata", async () => { + const session: MockSession = { + id: "session-1", + waitForSelector: async () => {}, + evaluate: async () => ({ found: true, editable: true }), + fill: async () => {}, + }; + + const { result } = await executeTool(typeTool as unknown as MockTool, { selector: "input[name=user]", text: "Alice" }, session); + assertTextResult(result); + assert.equal(result.details?.selector, "input[name=user]"); + assert.equal(result.details?.clear, true); + assert.equal(result.details?.textLength, 5); + }); + + it("preserves literal escape characters in steel_type input text", async () => { + let filledValue = ""; + const session: MockSession = { + id: "session-1", + waitForSelector: async () => {}, + evaluate: async () => ({ found: true, editable: true }), + fill: async (_selector, text) => { + filledValue = text; + }, + }; + + const { result } = await executeTool( + typeTool as unknown as MockTool, + { selector: "input[name=user]", text: "C\\new" }, + session + ); + + assertTextResult(result); + assert.equal(filledValue, "C\\new"); + assert.equal(result.details?.textLength, 5); + }); + + it("fills multiple form fields with partial success details", async () => { + const filled: string[] = []; + const session: MockSession = { + id: "session-1", + waitForSelector: async (selector) => { + if (selector === ".missing") { + throw new Error("No element matched selector: .missing"); + } + }, + evaluate: async (_selector: string) => ({ found: true, editable: true }), + fill: async (selector) => { + filled.push(selector); + }, + }; + + const { result } = await executeTool( + fillFormTool as unknown as MockTool, + { + fields: [ + { selector: "#a", value: "1" }, + { selector: ".missing", value: "2" }, + { selector: "#b", value: "3" }, + ], + }, + session + ); + + assertTextResult(result); + assert.equal(filled[0], "#a"); + assert.equal(filled[1], "#b"); + assert.equal(result.details?.successCount, 2); + assert.equal(result.details?.total, 3); + }); + + it("preserves literal escape characters in steel_fill_form values", async () => { + const values: string[] = []; + const session: MockSession = { + id: "session-1", + waitForSelector: async () => {}, + evaluate: async () => true, + fill: async (_selector, value) => { + values.push(value); + }, + }; + + const { result } = await executeTool( + fillFormTool as unknown as MockTool, + { + fields: [{ selector: "#a", value: "A\\tB" }], + }, + session + ); + + assertTextResult(result); + assert.equal(values[0], "A\\tB"); + }); + + it("waits for selector with state and timeout contract", async () => { + const session: MockSession = { + id: "session-1", + waitForSelector: async () => {}, + url: "https://waiting.example/", + }; + + const { result } = await executeTool(waitTool as unknown as MockTool, { selector: "#ready", timeout: 1000 }, session); + assertTextResult(result); + assert.equal(result.details?.selector, "#ready"); + assert.equal(result.details?.timeoutMs, 1000); + }); + + it("extracts structured data and validates contract", async () => { + const session: MockSession = { + id: "session-1", + url: "https://extract.example/", + evaluate: async () => ({ title: "Hello", version: 1 }), + }; + + const schema = { + type: "object" as const, + properties: { + title: { type: "string" }, + version: { type: "number" }, + }, + required: ["title", "version"], + additionalProperties: false, + }; + + const { result } = await executeTool( + extractTool as unknown as MockTool, + { + schema, + instructions: "extract title and version", + strict: true, + }, + session + ); + + assertTextResult(result); + const parsed = JSON.parse(result.content[0].text); + assert.equal(parsed.title, "Hello"); + assert.equal(parsed.version, 1); + assert.equal(result.details?.schemaEnforced, true); + }); + + it("finds candidate selectors for interactive elements", async () => { + const session: MockSession = { + id: "session-1", + url: "https://find.example/", + evaluate: async () => [ + { + selector: "a[href='/signup']", + text: "Sign up", + tag: "a", + role: null, + clickable: true, + visible: true, + }, + ], + }; + + const { result } = await executeTool( + findElementsTool as unknown as MockTool, + { query: "sign up", limit: 5 }, + session + ); + + assertTextResult(result); + const parsed = JSON.parse(result.content[0].text); + assert.equal(Array.isArray(parsed), true); + assert.equal(parsed[0].selector, "a[href='/signup']"); + assert.equal(result.details?.count, 1); + }); + + it("scrolls page and reports movement bounds", async () => { + const session: MockSession = { + id: "session-1", + evaluate: async () => ({ + before: 0, + after: 700, + maxScrollY: 1200, + effectiveAmount: 700, + viewportHeight: 500, + contentHeight: 1400, + targetType: "page", + targetSelector: null, + }), + }; + + const { result } = await executeTool(scrollTool as unknown as MockTool, { direction: "down", amount: 700 }, session); + assertTextResult(result); + assert.equal(result.details?.effectiveAmount, 700); + assert.equal(result.details?.direction, "down"); + assert.equal(result.details?.targetType, "page"); + }); + + it("scrolls a nested container when selector is provided", async () => { + const session: MockSession = { + id: "session-1", + evaluate: async () => ({ + before: 120, + after: 720, + maxScrollY: 2400, + effectiveAmount: 600, + viewportHeight: 640, + contentHeight: 3040, + targetType: "container", + targetSelector: 'div[role="feed"]', + }), + }; + + const { result } = await executeTool( + scrollTool as unknown as MockTool, + { direction: "down", amount: 600, selector: 'div[role="feed"]' }, + session + ); + + assertTextResult(result); + assert.equal(result.details?.requestedSelector, 'div[role="feed"]'); + assert.equal(result.details?.targetType, "container"); + assert.equal(result.details?.targetSelector, 'div[role="feed"]'); + assert.equal(result.details?.effectiveAmount, 600); + }); + + it("reads page history and title/url details", async () => { + const historySession: MockSession = { + id: "session-1", + url: "https://history.example/", + goBack: async () => {}, + }; + + const { result: goBackResult } = await executeTool(goBackTool as unknown as MockTool, {}, historySession); + assertTextResult(goBackResult); + assert.equal(goBackResult.details?.url, "https://history.example/"); + + const urlSession: MockSession = { + id: "session-1", + url: async () => "https://current.example/", + }; + + const { result: urlResult } = await executeTool(getUrlTool as unknown as MockTool, {}, urlSession); + assertTextResult(urlResult); + assert.equal(urlResult.content[0].text, "Current URL: https://current.example/"); + + const titleSession: MockSession = { + id: "session-1", + title: () => "Current Title", + }; + + const { result: titleResult } = await executeTool(getTitleTool as unknown as MockTool, {}, titleSession); + assertTextResult(titleResult); + assert.equal(titleResult.content[0].text, "Current title: Current Title"); + }); + + it("recovers go_back when history navigation completes after a timeout", async () => { + let currentUrl = "https://news.ycombinator.com/"; + const session: MockSession = { + id: "session-1", + url: async () => currentUrl, + goBack: async () => { + currentUrl = "https://example.com/"; + throw new Error('page.goBack: Timeout 30000ms exceeded. Call log: waiting for navigation until "load"'); + }, + }; + + const { result } = await executeTool(goBackTool as unknown as MockTool, {}, session); + assertTextResult(result); + assert.equal(result.content[0].text, "Navigated back to https://example.com/"); + assert.equal(result.details?.previousUrl, "https://news.ycombinator.com/"); + assert.equal(result.details?.url, "https://example.com/"); + assert.equal(result.details?.timeoutRecovered, true); + }); + + it("reports about:blank as a fresh session in get_url", async () => { + const session: MockSession = { + id: "session-1", + url: "about:blank", + }; + + const { result } = await executeTool(getUrlTool as unknown as MockTool, {}, session); + assertTextResult(result); + assert.match(result.content[0].text, /fresh Steel session/i); + assert.equal(result.details?.url, "about:blank"); + assert.equal(result.details?.isFreshSession, true); + }); + + it("fails get_title on about:blank with continuity guidance", async () => { + const client = createMockClient({ + id: "session-1", + url: "about:blank", + title: async () => "", + }); + const tool = getTitleTool(client as never); + + await assert.rejects( + () => + tool.execute( + "call-001", + {}, + new AbortController().signal, + async () => {}, + null + ), + /about:blank.*STEEL_SESSION_MODE=session/i + ); + }); + + it("fails scrape on about:blank with continuity guidance", async () => { + const client = createMockClient({ + id: "session-1", + url: "about:blank", + content: async () => "", + }); + const tool = scrapeTool(client as never); + + await assert.rejects( + () => + tool.execute( + "call-001", + { format: "text" }, + new AbortController().signal, + async () => {}, + null + ), + /about:blank.*STEEL_SESSION_MODE=session/i + ); + }); + + it("fails find_elements on about:blank with continuity guidance", async () => { + const client = createMockClient({ + id: "session-1", + url: "about:blank", + evaluate: async () => [], + }); + const tool = findElementsTool(client as never); + + await assert.rejects( + () => + tool.execute( + "call-001", + {}, + new AbortController().signal, + async () => {}, + null + ), + /about:blank.*STEEL_SESSION_MODE=session/i + ); + }); + + it("fails on selector validation errors", async () => { + const client = createMockClient({ id: "session-1" }); + const tool = clickTool(client as never); + + await assert.rejects( + () => + tool.execute( + "call-001", + { selector: "" }, + new AbortController().signal, + async () => {}, + null + ), + /Selector cannot be empty/, + "expected selector validation failure" + ); + }); + + it("fails on timeout validation errors", async () => { + const client = createMockClient({ id: "session-1" }); + const tool = waitTool(client as never); + + await assert.rejects( + () => + tool.execute( + "call-001", + { selector: "#item", timeout: 0 }, + new AbortController().signal, + async () => {}, + null + ), + /timeout must be a positive number/, + "expected timeout validation failure" + ); + }); + + it("fails extraction when schema validation rejects result", async () => { + const client = createMockClient({ + id: "session-1", + evaluate: async () => ({ version: 1 }), + }); + const tool = extractTool(client as never); + + const schema = { + type: "object" as const, + properties: { + title: { type: "string" }, + }, + required: ["title"], + additionalProperties: false, + }; + + await assert.rejects( + () => + tool.execute( + "call-001", + { schema }, + new AbortController().signal, + async () => {}, + null + ), + /Extraction result does not match requested schema/, + "expected extraction validation failure" + ); + }); + + it("cancels wait tool when abort signal fires", async () => { + const client = createMockClient({ + id: "session-1", + waitForSelector: async () => { + await new Promise(() => {}); + }, + }); + const tool = waitTool(client as never); + const controller = new AbortController(); + + const pending = tool.execute( + "call-001", + { selector: "#slow", timeout: 60_000 }, + controller.signal, + async () => {}, + null + ); + setTimeout(() => controller.abort(), 10); + + await assert.rejects( + () => pending, + /cancelled/i, + "expected cancellation to abort wait tool execution" + ); + }); +}); diff --git a/extensions/steel-browser/tsconfig.json b/extensions/steel-browser/tsconfig.json new file mode 100644 index 0000000..945781c --- /dev/null +++ b/extensions/steel-browser/tsconfig.json @@ -0,0 +1,22 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "NodeNext", + "moduleResolution": "NodeNext", + "lib": ["ES2022", "DOM"], + "types": ["node"], + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "isolatedModules": true, + "declaration": true, + "sourceMap": true, + "outDir": "dist", + "rootDir": "src", + "resolveJsonModule": true, + "noEmitOnError": true + }, + "include": ["src/**/*.ts"], + "exclude": ["dist", "node_modules", "**/*.test.ts", "**/*.spec.ts"] +}