diff --git a/.claude-plugin/marketplace.json b/.claude-plugin/marketplace.json index 99ba191..75a6c2e 100644 --- a/.claude-plugin/marketplace.json +++ b/.claude-plugin/marketplace.json @@ -4,8 +4,8 @@ "owner": { "name": "NodeOps", "url": "https://createos.sh" }, "plugins": [ { - "name": "createos-sandbox", - "source": "./createos-sandbox", + "name": "claude-code-plugin", + "source": "./packages/claude-code-plugin", "description": "Run ad-hoc/heavy/untrusted code in disposable CreateOS Sandboxes; offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public expose, network clusters, S3 disks, WireGuard VPN, fork, pause/resume, and custom images." } ] diff --git a/CLAUDE.md b/CLAUDE.md index 1f9fb3c..f43954a 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -12,8 +12,8 @@ cross-repo mesh guide. Architectural decisions live in `docs/adr/`. Read the relevant one before reworking the thing it covers. -| ADR | Decision | Status | -|---|---|---| +| ADR | Decision | Status | +| ------------------------------------------ | ---------------------------------------------------------------------------------------- | -------------------------- | | [0001](./docs/adr/0001-cos-bash-driver.md) | `cos`, a bash driver, as the plugin's execution engine โ€” not the `createos` CLI directly | accepted, **under review** | ADR-0001 is load-bearing for anyone touching `scripts/cos`, the skill, or the @@ -52,13 +52,13 @@ claim aligned with `fc`. This repo is one of five in the product mesh. ### Repo map -| repo | path | role | public? | changes that ripple across the mesh | -|---|---|---|---|---| -| **fc** | `../fc` | control-plane โ€” **source of truth** | ๐Ÿ”’ private | HTTP API, wire/JSON fields, error shapes, lifecycle/state, limits/quotas, behavior | -| **fc-sdk** | `../fc-sdk` | TypeScript SDK **+ `examples/`** | ๐ŸŒ public | public SDK methods, wire types, example apps | -| **createos-cli** | `../createos-cli` | Go CLI | ๐ŸŒ public | commands, flags, help/UX text | -| **website-04** | `../website-04` (`content/docs/Sandbox`) | public docs | ๐ŸŒ public | REST / SDK / CLI reference + concept pages | -| **createos** | `../createos-claude-plugins` | Claude Code plugin marketplace; `createos-sandbox` plugin over the `createos` CLI | ๐ŸŒ public | skills, slash commands, hooks | +| repo | path | role | public? | changes that ripple across the mesh | +| ---------------- | ---------------------------------------- | --------------------------------------------------------------------------------- | ---------- | ---------------------------------------------------------------------------------- | +| **fc** | `../fc` | control-plane โ€” **source of truth** | ๐Ÿ”’ private | HTTP API, wire/JSON fields, error shapes, lifecycle/state, limits/quotas, behavior | +| **fc-sdk** | `../fc-sdk` | TypeScript SDK **+ `examples/`** | ๐ŸŒ public | public SDK methods, wire types, example apps | +| **createos-cli** | `../createos-cli` | Go CLI | ๐ŸŒ public | commands, flags, help/UX text | +| **website-04** | `../website-04` (`content/docs/Sandbox`) | public docs | ๐ŸŒ public | REST / SDK / CLI reference + concept pages | +| **createos** | `../createos-claude-plugins` | Claude Code plugin marketplace; `createos-sandbox` plugin over the `createos` CLI | ๐ŸŒ public | skills, slash commands, hooks | ### What counts as a shared surface diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md new file mode 100644 index 0000000..c741bf3 --- /dev/null +++ b/CONTRIBUTING.md @@ -0,0 +1,35 @@ +# Contributing to CreateOS Claude Plugins + +Contributions are welcome! This repository is a monorepo of integrations and plugins for +[CreateOS](https://nodeops.network/createos). Each package in `packages/` is self-contained; +apps in `apps/` are standalone projects. + +## What you can contribute + +- Bug fixes and new features in existing packages or apps. +- New integrations or plugins (please open an issue first to discuss the fit). +- Documentation improvements. + +## Submitting a pull request + +1. Fork the repository and create a branch for your change. +2. Keep each PR scoped to one package or app. +3. Use [Conventional Commit](https://www.conventionalcommits.org/) style for your PR title + (e.g. `feat(pi-extension): add X`, `fix(adk-plugin): handle Y`). +4. Run lint, build, and tests for the package you changed before opening the PR. +5. Open a pull request. A maintainer will review it and, once approved, merge it into `main`. + +## Code style + +- Follow the conventions already established in the package you are modifying. +- Use TypeScript where applicable. +- Keep changes focused and minimal. + +## Reporting issues + +Found a bug or have a feature request? Open an issue, but please check for duplicates first. + +## License + +By contributing, you agree that your contributions will be licensed under the same license as +the project (see [LICENSE](./LICENSE)). diff --git a/README.md b/README.md index 0fa02f4..d869e3e 100644 --- a/README.md +++ b/README.md @@ -1,12 +1,13 @@
-# CreateOS Claude Plugin Marketplace +# CreateOS Integrations -**A [Claude Code](https://docs.claude.com/en/docs/claude-code) plugin marketplace for disposable sandbox compute.** +**[Claude Code](https://docs.claude.com/en/docs/claude-code) plugin & [Pi](https://github.com/anthropics/pi) extension for disposable sandbox compute.** -Run ad-hoc, heavy, or untrusted code **off your machine** in disposable [CreateOS](https://createos.sh) Sandboxes โ€” straight from your Claude Code session. +Run code **off your machine** in disposable [CreateOS](https://createos.sh) Sandboxes โ€” from Claude Code or Pi. [![Claude Code](https://img.shields.io/badge/Claude%20Code-plugin-6E56CF)](https://docs.claude.com/en/docs/claude-code) +[![Pi](https://img.shields.io/badge/Pi-extension-F97316)](https://github.com/anthropics/pi) [![CreateOS](https://img.shields.io/badge/CreateOS-Sandboxes-0EA5E9)](https://createos.sh) [![Spawn](https://img.shields.io/badge/create%20to%20first%20command-~200ms-22C55E)](https://createos.sh) [![PRs welcome](https://img.shields.io/badge/PRs-welcome-brightgreen)](#contributing) @@ -17,7 +18,7 @@ Run ad-hoc, heavy, or untrusted code **off your machine** in disposable [CreateO ## Why -Heavy builds, flaky test suites, and untrusted code don't belong on your laptop. `createos-sandbox` gives Claude a skill + slash commands that offload them to throwaway CreateOS Sandboxes โ€” created and running your first command in roughly 200 ms, self-destructing when done โ€” so your machine stays free, your deps stay isolated, and untrusted code never touches local state. +Heavy builds, flaky test suites, and untrusted code don't belong on your laptop. `claude-code-plugin` gives Claude a skill + slash commands that offload them to throwaway CreateOS Sandboxes โ€” created and running your first command in roughly 200 ms, self-destructing when done โ€” so your machine stays free, your deps stay isolated, and untrusted code never touches local state. - ๐Ÿงจ **Disposable** โ€” one-shot offload stages a dir, runs, pulls artifacts, then auto-destroys. Box-side changes never touch local unless you ask. - โšก **Fast** โ€” ~200 ms from create to first command; parallel fanout across N boxes for matrix builds and split test suites. @@ -27,63 +28,105 @@ Heavy builds, flaky test suites, and untrusted code don't belong on your laptop. ## Quick start +**Claude Code:** + ```bash # 1. Add the marketplace + install the plugin /plugin marketplace add NodeOps-app/createos-claude-plugins -/plugin install createos-sandbox@createos +/plugin install claude-code-plugin@createos # 2. Offload a heavy test run to a throwaway box (auto-destroys) /createos-sandbox:offload . "npm ci && npm test" ``` -The `createos` CLI **auto-installs** on first use. Sign in once with `createos login` (browser OAuth, run it in your own terminal) or `export CREATEOS_API_KEY=`; check with `cos auth`. Prefer a local checkout? See [Install](#install). - -## Plugins +**Pi:** -| Plugin | What it does | -|---|---| -| [**createos-sandbox**](./createos-sandbox) | Offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public HTTPS expose, private-network clusters, BYO-S3 disk mounts, WireGuard VPN, and snapshot/fork โ€” all driving the authed `createos` CLI. | - -> The marketplace currently ships one plugin; more CreateOS plugins land here as siblings. +```bash +# 1. Install the extension +pi install npm:@createos/pi -## Commands at a glance +# 2. Start a session with all tools routed to a remote sandbox +pi --createos +``` -| Command | What | -|---|---| -| `/createos-sandbox:offload ` | one-shot: stage โ†’ run โ†’ pull artifacts โ†’ destroy | -| `/createos-sandbox:fanout [cmd2 โ€ฆ]` | run each command in its own throwaway box, in parallel | -| `/createos-sandbox:shell` | instant throwaway interactive Linux (destroyed on exit) | -| `/createos-sandbox:up` ยท `run` ยท `sync` ยท `down` | reusable per-repo box + file sync for live dev loops | -| `/createos-sandbox:tunnel ` | forward a box port to `127.0.0.1` (private) | -| `/createos-sandbox:expose ` | public HTTPS URL for a box port | -| `/createos-sandbox:cluster โ€ฆ` | N boxes on one private network, name-addressable | -| `/createos-sandbox:disk โ€ฆ` | mount your own S3 bucket into the project box | -| `/createos-sandbox:vpn โ€ฆ` | WireGuard L3 into your private networks | -| `/createos-sandbox:fork` | snapshot the project box โ†’ independent clone | -| `/createos-sandbox:pause` ยท `resume` | park the warm box at zero compute cost, then restore it exactly | -| `/createos-sandbox:template โ€ฆ` | build a custom image so boxes boot with the toolchain already installed | -| `/createos-sandbox:status` | show active box + sync + tunnels + cluster | +The `createos` CLI **auto-installs** on first use. Sign in once with `createos login` (browser OAuth, run it in your own terminal) or `export CREATEOS_API_KEY=`; check with `cos auth`. Prefer a local checkout? See [Install](#install). -Full flags, networking guide, and heavy-build tips live in the [**plugin README**](./createos-sandbox/README.md). +## Packages + +| Package | What it does | +| ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| [**claude-code-plugin**](./packages/claude-code-plugin) | Hooks-based Claude Code plugin โ€” offload, parallel fanout, scratch shell, reusable box with sync, port tunnel, public HTTPS expose, private-network clusters, BYO-S3 disk mounts, WireGuard VPN, and snapshot/fork โ€” all driving the authed `createos` CLI. | +| [**pi-extension**](./packages/pi-extension) | Pi coding agent extension that transparently routes all built-in commands (bash, read, write, edit, ls, find, grep) to a remote CreateOS Sandbox, plus 26 additional tools for sandbox lifecycle, configuration, port tunnels, file sync, private networks, persistent disks, and device VPN โ€” 33 tools total. | + +## Claude Code โ€” commands at a glance + +| Command | What | +| ------------------------------------------------ | ----------------------------------------------------------------------- | +| `/createos-sandbox:offload ` | one-shot: stage โ†’ run โ†’ pull artifacts โ†’ destroy | +| `/createos-sandbox:fanout [cmd2 โ€ฆ]` | run each command in its own throwaway box, in parallel | +| `/createos-sandbox:shell` | instant throwaway interactive Linux (destroyed on exit) | +| `/createos-sandbox:up` ยท `run` ยท `sync` ยท `down` | reusable per-repo box + file sync for live dev loops | +| `/createos-sandbox:tunnel ` | forward a box port to `127.0.0.1` (private) | +| `/createos-sandbox:expose ` | public HTTPS URL for a box port | +| `/createos-sandbox:cluster โ€ฆ` | N boxes on one private network, name-addressable | +| `/createos-sandbox:disk โ€ฆ` | mount your own S3 bucket into the project box | +| `/createos-sandbox:vpn โ€ฆ` | WireGuard L3 into your private networks | +| `/createos-sandbox:fork` | snapshot the project box โ†’ independent clone | +| `/createos-sandbox:pause` ยท `resume` | park the warm box at zero compute cost, then restore it exactly | +| `/createos-sandbox:template โ€ฆ` | build a custom image so boxes boot with the toolchain already installed | +| `/createos-sandbox:status` | show active box + sync + tunnels + cluster | + +Full flags, networking guide, and heavy-build tips live in the [**Claude Code Plugin README**](./packages/claude-code-plugin/README.md). + +## Pi โ€” commands at a glance + +All built-in tools (bash, read, write, edit, ls, find, grep) transparently route to the sandbox โ€” plus 26 additional tools for lifecycle, networking, disks, and device VPN (33 tools total). + +| Command | What | +| -------------------------- | ------------------------------------- | +| `/sandbox` | Show sandbox status | +| `/network create ` | Create a private network | +| `/network ls` | List your networks | +| `/network show ` | Show network members + IPs | +| `/network attach ` | Join this sandbox to a network | +| `/network detach ` | Leave a network | +| `/network rm ` | Delete a network | +| `/device status` | Show registered devices | +| `/device attach ` | Give your machine access to a network | +| `/device detach ` | Remove access | + +### Flags + +| Flag | Purpose | +| ------------------ | ------------------------------------- | +| `--createos` | Activate the extension | +| `--shape ` | Sandbox size (default: `s-2vcpu-2gb`) | +| `--rootfs ` | Base image or template | +| `--network ` | Network(s) to join at creation | + +Full tool inventory lives in the [**Pi Extension README**](./packages/pi-extension/README.md). ## Install **From GitHub (recommended):** + ``` /plugin marketplace add NodeOps-app/createos-claude-plugins -/plugin install createos-sandbox@createos +/plugin install claude-code-plugin@createos ``` **From a local checkout:** + ``` git clone https://github.com/NodeOps-app/createos-claude-plugins /plugin marketplace add /path/to/createos-claude-plugins -/plugin install createos-sandbox@createos +/plugin install claude-code-plugin@createos ``` **Dev (instant, no install):** + ```bash -claude --plugin-dir /path/to/createos-claude-plugins/createos-sandbox +claude --plugin-dir /path/to/createos-claude-plugins/packages/claude-code-plugin /reload-plugins # after editing plugin files ``` @@ -103,24 +146,34 @@ claude --plugin-dir /path/to/createos-claude-plugins/createos-sandbox ## Repository layout ``` -createos/ # marketplace root +createos-claude-plugins/ # marketplace root โ”œโ”€ .claude-plugin/ -โ”‚ โ””โ”€ marketplace.json # marketplace manifest -โ””โ”€ createos-sandbox/ # the plugin - โ”œโ”€ .claude-plugin/plugin.json - โ”œโ”€ commands/ # slash commands - โ”œโ”€ skills/ # the using-createos-sandbox skill + references/ - โ”œโ”€ hooks/ # SessionStart driver-path + PreToolUse offload-hint - โ”œโ”€ scripts/cos # the CLI driver - โ””โ”€ README.md +โ”‚ โ””โ”€ marketplace.json # marketplace manifest +โ”œโ”€ packages/ +โ”‚ โ”œโ”€ claude-code-plugin/ # hooks-based Claude plugin +โ”‚ โ”‚ โ”œโ”€ .claude-plugin/plugin.json +โ”‚ โ”‚ โ”œโ”€ commands/ # slash commands +โ”‚ โ”‚ โ”œโ”€ skills/ # the using-createos-sandbox skill + references/ +โ”‚ โ”‚ โ”œโ”€ hooks/ # SessionStart driver-path + PreToolUse offload-hint +โ”‚ โ”‚ โ”œโ”€ scripts/cos # the CLI driver +โ”‚ โ”‚ โ””โ”€ README.md +โ”‚ โ””โ”€ pi-extension/ # Pi extension (TypeScript) +โ”‚ โ”œโ”€ index.ts # extension entry point +โ”‚ โ”œโ”€ src/ # tools, CLI wrappers, ops +โ”‚ โ””โ”€ README.md +โ”œโ”€ apps/ # (future starter templates) +โ”œโ”€ docs/ +โ”‚ โ””โ”€ adr/ # architecture decision records +โ””โ”€ README.md ``` ## Contributing -Issues and PRs welcome. The plugin is a thin Claude Code surface over the [`createos`](https://createos.sh) CLI โ€” most command logic lives in [`createos-sandbox/scripts/cos`](./createos-sandbox/scripts/cos). Keep the slash-command, skill, and CLI surfaces aligned. +Issues and PRs welcome. The plugin is a thin Claude Code surface over the [`createos`](https://createos.sh) CLI โ€” most command logic lives in [`claude-code-plugin/scripts/cos`](./packages/claude-code-plugin/scripts/cos). Keep the slash-command, skill, and CLI surfaces aligned. ## Links - ๐ŸŒ [createos.sh](https://createos.sh) โ€” CreateOS platform - ๐Ÿ“– [Claude Code plugins](https://docs.claude.com/en/docs/claude-code) โ€” how plugins & marketplaces work -- ๐Ÿ“ฆ [Plugin README](./createos-sandbox/README.md) โ€” full command & flag reference +- ๐Ÿ“ฆ [Claude Code Plugin README](./packages/claude-code-plugin/README.md) โ€” full command & flag reference +- ๐Ÿ”ง [Pi Extension README](./packages/pi-extension/README.md) โ€” Pi extension setup & tool inventory diff --git a/apps/.gitkeep b/apps/.gitkeep new file mode 100644 index 0000000..e69de29 diff --git a/docs/adr/0001-cos-bash-driver.md b/docs/adr/0001-cos-bash-driver.md index b83dfc2..0c21161 100644 --- a/docs/adr/0001-cos-bash-driver.md +++ b/docs/adr/0001-cos-bash-driver.md @@ -6,24 +6,24 @@ The `createos-sandbox` plugin does not call the `createos` CLI directly. It ship ## Context -`createos sandbox` already exposes the primitives: `create`, `exec`, `push`, `pull`, `rm`, `sync`, `tunnel`, `fork`, `disk`, `network`, `firewall`, `shell`. It does not expose the *compositions* an agent needs. `cos` supplies exactly six things on top: +`createos sandbox` already exposes the primitives: `create`, `exec`, `push`, `pull`, `rm`, `sync`, `tunnel`, `fork`, `disk`, `network`, `firewall`, `shell`. It does not expose the _compositions_ an agent needs. `cos` supplies exactly six things on top: -| `cos` adds | Why the primitives aren't enough | -|---|---| -| `offload` | stage โ†’ exec โ†’ pull โ†’ **auto-destroy** as one atomic verb; the safe default | -| `fanout` | N throwaway boxes in parallel, per-job logs + exit codes, concurrency cap | -| `run_keepalive` | `createos sandbox exec` drops its stream on long/quiet builds; this detaches, heartbeats, and re-attaches | -| staging + excludes | `push` has no ignore semantics, so `.git`/`node_modules`/`target` get uploaded by hand | -| project-box state | a reusable per-git-root box (`cos-proj-<12hex>`), so `up`/`run`/`sync`/`down` address "this repo's box" | -| egress presets | `firewall` is a raw primitive; `-p python-uv\|rust-cargo\|npm\|github` are the allowlists people actually want | +| `cos` adds | Why the primitives aren't enough | +| ------------------ | -------------------------------------------------------------------------------------------------------------- | +| `offload` | stage โ†’ exec โ†’ pull โ†’ **auto-destroy** as one atomic verb; the safe default | +| `fanout` | N throwaway boxes in parallel, per-job logs + exit codes, concurrency cap | +| `run_keepalive` | `createos sandbox exec` drops its stream on long/quiet builds; this detaches, heartbeats, and re-attaches | +| staging + excludes | `push` has no ignore semantics, so `.git`/`node_modules`/`target` get uploaded by hand | +| project-box state | a reusable per-git-root box (`cos-proj-<12hex>`), so `up`/`run`/`sync`/`down` address "this repo's box" | +| egress presets | `firewall` is a raw primitive; `-p python-uv\|rust-cargo\|npm\|github` are the allowlists people actually want | Everything else `cos` does is passthrough. The genuinely additive surface is small โ€” and, notably, none of it is Claude-specific. `run_keepalive` is a correctness fix for `exec`; staging excludes are a correctness fix for `push`. Both currently exist only for plugin users. ## Consequences -**The driver must be located before it can be run, and that fails silently.** Slash commands invoke `"${CLAUDE_PLUGIN_ROOT}/scripts/cos"`, which Claude Code expands at command-load time โ€” that path works. But `CLAUDE_PLUGIN_ROOT` is **unset in the Bash tool's environment**, so when the *skill* (rather than a slash command) tells Claude to run `${CLAUDE_PLUGIN_ROOT}/scripts/cos`, it expands to `/scripts/cos` and dies with exit 127. Autonomous use โ€” the skill deciding on its own to offload, which is the plugin's entire reason to exist โ€” hits this on every invocation. +**The driver must be located before it can be run, and that fails silently.** Slash commands invoke `"${CLAUDE_PLUGIN_ROOT}/scripts/cos"`, which Claude Code expands at command-load time โ€” that path works. But `CLAUDE_PLUGIN_ROOT` is **unset in the Bash tool's environment**, so when the _skill_ (rather than a slash command) tells Claude to run `${CLAUDE_PLUGIN_ROOT}/scripts/cos`, it expands to `/scripts/cos` and dies with exit 127. Autonomous use โ€” the skill deciding on its own to offload, which is the plugin's entire reason to exist โ€” hits this on every invocation. -**And it fails open, not closed.** Verified end-to-end on 2026-07-09 by running Claude Code against this plugin in tmux: on `/scripts/cos: no such file`, the agent concluded "cos not on PATH โ€ฆ Skill docs assume cos. Using createos directly", then hand-rolled the offload out of `create`/`push`/`exec`. In its own words: *"no egress restriction was applied โ€ฆ don't read this run as evidence the lockdown path works."* It left the box running and billing. Losing the wrapper means silently losing keepalive, egress restriction, auto-destroy, and the `ensure_auth` preflight โ€” every safety property the wrapper exists to enforce. A missing driver should hard-stop, not degrade into a less safe path. +**And it fails open, not closed.** Verified end-to-end on 2026-07-09 by running Claude Code against this plugin in tmux: on `/scripts/cos: no such file`, the agent concluded "cos not on PATH โ€ฆ Skill docs assume cos. Using createos directly", then hand-rolled the offload out of `create`/`push`/`exec`. In its own words: _"no egress restriction was applied โ€ฆ don't read this run as evidence the lockdown path works."_ It left the box running and billing. Losing the wrapper means silently losing keepalive, egress restriction, auto-destroy, and the `ensure_auth` preflight โ€” every safety property the wrapper exists to enforce. A missing driver should hard-stop, not degrade into a less safe path. **A contributing footgun lives upstream.** `createos` exits **0** on an unknown subcommand and prints help. That masked the adapter failure for several turns, both for the agent under test and for the human reading over its shoulder. Worth fixing in `createos-cli` independent of anything here. @@ -36,9 +36,9 @@ Everything else `cos` does is passthrough. The genuinely additive surface is sma The recommendation on record is to **fold the engine into `createos-cli` and keep the plugin thin**: - **Move to `createos-cli` (Go):** `offload`, `fanout`, keepalive/re-attach, staging + excludes, project-box lifecycle (`up`/`run`/`down`/`status`, on top of the existing `.createos.json` + `FindProjectConfig` machinery), egress presets. -- **Keep in the plugin:** the skill (*when* to offload, what never to offload), the slash commands, the `offload-hint` hook, the agent-facing safety prose. +- **Keep in the plugin:** the skill (_when_ to offload, what never to offload), the slash commands, the `offload-hint` hook, the agent-facing safety prose. -`cos` then shrinks to nothing and is deleted. The `CLAUDE_PLUGIN_ROOT` bootstrap problem disappears with it โ€” the driver becomes `createos`, already on `PATH` โ€” as does `ensure_auth`, since the CLI owns auth. Making `offload` a first-class CLI verb also fixes the deeper problem the test exposed: *any* capable agent handed only the primitives will compose them and get the safety properties wrong. The safe path has to be the obvious one. +`cos` then shrinks to nothing and is deleted. The `CLAUDE_PLUGIN_ROOT` bootstrap problem disappears with it โ€” the driver becomes `createos`, already on `PATH` โ€” as does `ensure_auth`, since the CLI owns auth. Making `offload` a first-class CLI verb also fixes the deeper problem the test exposed: _any_ capable agent handed only the primitives will compose them and get the safety properties wrong. The safe path has to be the obvious one. Not yet decided, and deliberately not decided here: @@ -48,6 +48,6 @@ Not yet decided, and deliberately not decided here: ### Shipped since -**Interim fix for `CLAUDE_PLUGIN_ROOT` โ€” implemented** in `scripts/session-start.sh` (`SessionStart` hook). Hooks *do* receive `CLAUDE_PLUGIN_ROOT`, so the hook resolves it and states the driver's absolute path in context, along with an explicit instruction that a missing driver is a hard stop rather than a licence to hand-roll the offload from raw CLI primitives. No filesystem mutation; deleted alongside `cos` when the engine moves. Alternatives rejected at the time and still rejected: having the hook run `cos install` (writes to `~/.local/bin` unasked), and teaching the skill to glob `~/.claude/plugins/cache/*/createos-sandbox/*/scripts/cos` (brittle โ€” an internal Claude Code layout, and wrong for `--plugin-dir` dev installs). +**Interim fix for `CLAUDE_PLUGIN_ROOT` โ€” implemented** in `scripts/session-start.sh` (`SessionStart` hook). Hooks _do_ receive `CLAUDE_PLUGIN_ROOT`, so the hook resolves it and states the driver's absolute path in context, along with an explicit instruction that a missing driver is a hard stop rather than a licence to hand-roll the offload from raw CLI primitives. No filesystem mutation; deleted alongside `cos` when the engine moves. Alternatives rejected at the time and still rejected: having the hook run `cos install` (writes to `~/.local/bin` unasked), and teaching the skill to glob `~/.claude/plugins/cache/*/createos-sandbox/*/scripts/cos` (brittle โ€” an internal Claude Code layout, and wrong for `--plugin-dir` dev installs). -This closes the *reliability* half of the fail-open problem. It does not close the *design* half: the safe path is still a wrapper the agent has to find, rather than the obvious one. That argument for folding the engine into `createos-cli` stands unchanged. +This closes the _reliability_ half of the fail-open problem. It does not close the _design_ half: the safe path is still a wrapper the agent has to find, rather than the obvious one. That argument for folding the engine into `createos-cli` stands unchanged. diff --git a/createos-sandbox/.claude-plugin/plugin.json b/packages/claude-code-plugin/.claude-plugin/plugin.json similarity index 96% rename from createos-sandbox/.claude-plugin/plugin.json rename to packages/claude-code-plugin/.claude-plugin/plugin.json index 2cafb44..da42a8c 100644 --- a/createos-sandbox/.claude-plugin/plugin.json +++ b/packages/claude-code-plugin/.claude-plugin/plugin.json @@ -1,6 +1,6 @@ { "$schema": "https://json.schemastore.org/claude-code-plugin-manifest.json", - "name": "createos-sandbox", + "name": "claude-code-plugin", "displayName": "CreateOS Sandbox", "version": "0.6.0", "description": "Run ad-hoc, heavy, or untrusted code OFF your machine in disposable CreateOS Sandboxes. One-shot offload, parallel fanout across N boxes, instant scratch shell, a reusable box with file sync for live dev loops, port tunnel to localhost, public HTTPS expose, multi-box private-network clusters, BYO-S3 disk mounts, WireGuard VPN, snapshot/fork, pause/resume to park a warm box at zero compute cost, and custom Dockerfile-built images so boxes boot pre-provisioned. Big dirs auto-excluded from uploads; auto-installs the createos CLI if missing.", diff --git a/createos-sandbox/README.md b/packages/claude-code-plugin/README.md similarity index 66% rename from createos-sandbox/README.md rename to packages/claude-code-plugin/README.md index bf02d69..3af0685 100644 --- a/createos-sandbox/README.md +++ b/packages/claude-code-plugin/README.md @@ -67,14 +67,14 @@ CreateOS Sandbox โ† your code runs here, then the box is destroye The plugin is a **thin Claude-facing surface** over the `createos` CLI. It ships four things: -| Piece | Path | Role | -|---|---|---| -| **Slash commands** | `commands/*.md` | 18 commands (`offload`, `fanout`, `shell`, โ€ฆ), each a thin wrapper that calls `scripts/cos` | -| **Skill** | `skills/using-createos-sandbox/SKILL.md` + `references/` | teaches Claude *when* to reach for the sandbox on its own, with depth loaded on demand | -| **Hooks** | `hooks/hooks.json` + `scripts/` | `SessionStart` publishes the driver's absolute path; `PreToolUse(Bash)` nudges on heavy build/test commands | -| **Driver** | `scripts/cos` | the actual logic โ€” staging, egress, keepalive, sync, networking, lifecycle, state | +| Piece | Path | Role | +| ------------------ | -------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------- | +| **Slash commands** | `commands/*.md` | 18 commands (`offload`, `fanout`, `shell`, โ€ฆ), each a thin wrapper that calls `scripts/cos` | +| **Skill** | `skills/using-createos-sandbox/SKILL.md` + `references/` | teaches Claude _when_ to reach for the sandbox on its own, with depth loaded on demand | +| **Hooks** | `hooks/hooks.json` + `scripts/` | `SessionStart` publishes the driver's absolute path; `PreToolUse(Bash)` nudges on heavy build/test commands | +| **Driver** | `scripts/cos` | the actual logic โ€” staging, egress, keepalive, sync, networking, lifecycle, state | -Slash commands invoke `${CLAUDE_PLUGIN_ROOT}/scripts/cos`, which Claude Code expands when the command is loaded. **The Bash tool's own environment does not carry that variable**, so when the *skill* decides to offload on its own it needs a real path โ€” that is what the `SessionStart` hook supplies. Without it the path collapses to `/scripts/cos`, and the observed failure mode is Claude quietly rebuilding the offload out of raw CLI primitives, losing egress restriction, keepalive, auto-destroy, and the auth preflight along the way. See [ADR-0001](../docs/adr/0001-cos-bash-driver.md). +Slash commands invoke `${CLAUDE_PLUGIN_ROOT}/scripts/cos`, which Claude Code expands when the command is loaded. **The Bash tool's own environment does not carry that variable**, so when the _skill_ decides to offload on its own it needs a real path โ€” that is what the `SessionStart` hook supplies. Without it the path collapses to `/scripts/cos`, and the observed failure mode is Claude quietly rebuilding the offload out of raw CLI primitives, losing egress restriction, keepalive, auto-destroy, and the auth preflight along the way. See [ADR-0001](../docs/adr/0001-cos-bash-driver.md). ## Two patterns @@ -87,12 +87,14 @@ A reusable per-repo box + one-way file sync (default; `-2` for two-way). A dev s ## Install **From the marketplace (recommended):** + ``` /plugin marketplace add NodeOps-app/createos-claude-plugins /plugin install createos-sandbox@createos ``` **Dev (instant, no install):** + ```bash claude --plugin-dir /path/to/createos-claude-plugins/createos-sandbox /reload-plugins # after editing plugin files @@ -119,25 +121,25 @@ claude --plugin-dir /path/to/createos-claude-plugins/createos-sandbox ## Command reference -| Command | Summary | -|---|---| -| [`offload`](#offload--one-shot) `[flags] ` | one-shot: stage โ†’ run (keepalive) โ†’ pull โ†’ destroy | -| [`fanout`](#fanout--parallel-boxes) `[-j N] [flags] [cmd2] โ€ฆ` | run each command in its own throwaway box, in parallel | -| [`shell`](#shell--throwaway-linux) `[-s] [-r] [-e\|-p\|-E]` | instant throwaway interactive Linux (destroyed on exit) | -| [`up`](#project-box--live-sessions) `[-s] [-r] [-n] [-e\|-p\|-E]` | create/reuse the per-repo project box | -| [`run`](#project-box--live-sessions) `` | exec in the project box (streamed, state persists) | -| [`sync`](#project-box--live-sessions) `[-2\|-M] [-x glob] [remote-dir]` | file sync into the project box (background) | -| [`tunnel`](#networking) ` [local]` | forward a box port to `127.0.0.1` (private) | -| [`expose`](#networking) `` | public HTTPS URL for a box port | -| [`unexpose`](#networking) | revoke the public URL / disable ingress | -| [`cluster`](#networking) `up \| run [โ€ฆ] \| ls \| down` | N boxes on one private network, name-addressable | -| [`disk`](#disks--byo-s3) `create \| ls \| show \| attach \| detach \| rm` | BYO S3 bucket mounts on the project box | -| [`vpn`](#networking) `[register \| up]` | WireGuard L3 into your private networks | -| [`fork`](#networking) | snapshot the project box โ†’ independent clone | -| [`pause`](#pause-resume-and-custom-images) ยท [`resume`](#pause-resume-and-custom-images) | park the warm project box at zero compute cost / restore it exactly | -| [`template`](#pause-resume-and-custom-images) `submit [-f Dockerfile] \| ls \| show \| logs \| rm` | build a custom rootfs so boxes boot pre-provisioned | -| [`down`](#project-box--live-sessions) | stop sync/tunnels + destroy the project box (+ cluster) | -| [`status`](#project-box--live-sessions) | show active box + sync + tunnels + cluster | +| Command | Summary | +| --------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------- | +| [`offload`](#offload--one-shot) `[flags] ` | one-shot: stage โ†’ run (keepalive) โ†’ pull โ†’ destroy | +| [`fanout`](#fanout--parallel-boxes) `[-j N] [flags] [cmd2] โ€ฆ` | run each command in its own throwaway box, in parallel | +| [`shell`](#shell--throwaway-linux) `[-s] [-r] [-e\|-p\|-E]` | instant throwaway interactive Linux (destroyed on exit) | +| [`up`](#project-box--live-sessions) `[-s] [-r] [-n] [-e\|-p\|-E]` | create/reuse the per-repo project box | +| [`run`](#project-box--live-sessions) `` | exec in the project box (streamed, state persists) | +| [`sync`](#project-box--live-sessions) `[-2\|-M] [-x glob] [remote-dir]` | file sync into the project box (background) | +| [`tunnel`](#networking) ` [local]` | forward a box port to `127.0.0.1` (private) | +| [`expose`](#networking) `` | public HTTPS URL for a box port | +| [`unexpose`](#networking) | revoke the public URL / disable ingress | +| [`cluster`](#networking) `up \| run [โ€ฆ] \| ls \| down` | N boxes on one private network, name-addressable | +| [`disk`](#disks--byo-s3) `create \| ls \| show \| attach \| detach \| rm` | BYO S3 bucket mounts on the project box | +| [`vpn`](#networking) `[register \| up]` | WireGuard L3 into your private networks | +| [`fork`](#networking) | snapshot the project box โ†’ independent clone | +| [`pause`](#pause-resume-and-custom-images) ยท [`resume`](#pause-resume-and-custom-images) | park the warm project box at zero compute cost / restore it exactly | +| [`template`](#pause-resume-and-custom-images) `submit [-f Dockerfile] \| ls \| show \| logs \| rm` | build a custom rootfs so boxes boot pre-provisioned | +| [`down`](#project-box--live-sessions) | stop sync/tunnels + destroy the project box (+ cluster) | +| [`status`](#project-box--live-sessions) | show active box + sync + tunnels + cluster | > **Flag order:** flags (`-s/-r/-e/-p/-E/-o/-x/-w/-K`) come **before** the positional ` `. @@ -149,17 +151,17 @@ The core command. Stages a directory into a fresh box, runs a command, optionall /createos-sandbox:offload [-p preset] [-e dom] [-E] [-x glob] [-o out] [-w GB] [-K] [-s shape] [-r rootfs] ``` -| Flag | Meaning | -|---|---| -| `-s ` | box size (default `s-1vcpu-1gb`; see [Shapes](#shapes)) | -| `-r ` | root filesystem image (default `devbox:1`) | -| `-o ` | tar a box-side dir and pull it back to local | -| `-w ` | attempt swap (see caveat under [Heavy builds](#heavy-builds)) | -| `-K` | keep the box on a real failure so you can inspect it | -| `-e ` | allow one egress domain (repeatable) | +| Flag | Meaning | +| ------------- | ------------------------------------------------------------------------------------------- | +| `-s ` | box size (default `s-1vcpu-1gb`; see [Shapes](#shapes)) | +| `-r ` | root filesystem image (default `devbox:1`) | +| `-o ` | tar a box-side dir and pull it back to local | +| `-w ` | attempt swap (see caveat under [Heavy builds](#heavy-builds)) | +| `-K` | keep the box on a real failure so you can inspect it | +| `-e ` | allow one egress domain (repeatable) | | `-p ` | egress preset โ€” `python-uv \| rust-cargo \| npm \| github` (repeatable, composes with `-e`) | -| `-E` | unrestricted egress (trusted offload) | -| `-x ` | extra upload exclude (repeatable) | +| `-E` | unrestricted egress (trusted offload) | +| `-x ` | extra upload exclude (repeatable) | ```bash # Run a test suite, pull nothing, box auto-destroys @@ -210,7 +212,7 @@ A **reusable, per-repo** box addressed by your working directory. `up` creates i - **`up`** is idempotent โ€” run it again on the same repo and it reuses the same box instead of creating a new one. - **`run`** streams output and keeps box-side state between calls (installed deps, running processes). -- **`sync`** is **one-way by default** (laptop โ†’ box). `-2` = two-way (box writes flow back), `-M` = mirror (deletes box-side extras), `-x ` = exclude. First run downloads Mutagen (~60โ€“90 s before edits propagate). **Install deps *inside* the box** (`cos run 'npm ci'`) rather than syncing `node_modules` up โ€” big/regenerable dirs are excluded by default. +- **`sync`** is **one-way by default** (laptop โ†’ box). `-2` = two-way (box writes flow back), `-M` = mirror (deletes box-side extras), `-x ` = exclude. First run downloads Mutagen (~60โ€“90 s before edits propagate). **Install deps _inside_ the box** (`cos run 'npm ci'`) rather than syncing `node_modules` up โ€” big/regenerable dirs are excluded by default. - **`pause`/`resume`** park the box at zero compute cost and bring it back exactly โ€” see [Pause, resume, and custom images](#pause-resume-and-custom-images). Prefer `pause` over `down` when the box has a warm toolchain you'll want again. - **`down`** stops sync + tunnels and **destroys** the project box (and any cluster). The next session starts from scratch. - **`status`** shows the active box + sync + tunnels + cluster. @@ -225,14 +227,14 @@ A **reusable, per-repo** box addressed by your working directory. `up` creates i ### Networking -| Command | What it gives you | -|---|---| -| **`tunnel [local]`** | Reach a box-side service on your laptop. Run a dev server in the box, then `tunnel 3000` โ†’ `http://127.0.0.1:3000`. Private, background, no public URL. Stopped by `down`. | -| **`expose `** | A **public HTTPS** link for a port โ€” `-.app.sb.createos.sh`, stable for the box's lifetime. The service must bind `0.0.0.0`. **Anyone with the link can reach it.** Revoke with `unexpose`. | -| **`unexpose`** | Revoke the public URL / disable ingress on the active box. | -| **`cluster up `** | N boxes on one private network, reaching each other by **fully-qualified** name (`curl http://cos-cl--2.fc.local:8080` โ€” the bare short name is NXDOMAIN). `cluster run -a ''` fans a command across all; `cluster run ''` targets one. `cluster ls` / `cluster down` manage them. For distributed-system / DB-replication / p2p / load-test repros. **Counts against quota โ€” keep N small.** | -| **`vpn register `** then **`vpn up`** | Join your laptop to the whole private network over WireGuard (reach every sandbox by name/IP). `vpn up` needs `wg-quick` + `sudo` and **blocks until Ctrl-C** โ€” run it in your own terminal (`!cos vpn up`). | -| **`fork`** | Snapshot the warm project box โ†’ an independent clone for matrix/parallel experiments. The fork is self-managed. | +| Command | What it gives you | +| ------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | +| **`tunnel [local]`** | Reach a box-side service on your laptop. Run a dev server in the box, then `tunnel 3000` โ†’ `http://127.0.0.1:3000`. Private, background, no public URL. Stopped by `down`. | +| **`expose `** | A **public HTTPS** link for a port โ€” `-.app.sb.createos.sh`, stable for the box's lifetime. The service must bind `0.0.0.0`. **Anyone with the link can reach it.** Revoke with `unexpose`. | +| **`unexpose`** | Revoke the public URL / disable ingress on the active box. | +| **`cluster up `** | N boxes on one private network, reaching each other by **fully-qualified** name (`curl http://cos-cl--2.fc.local:8080` โ€” the bare short name is NXDOMAIN). `cluster run -a ''` fans a command across all; `cluster run ''` targets one. `cluster ls` / `cluster down` manage them. For distributed-system / DB-replication / p2p / load-test repros. **Counts against quota โ€” keep N small.** | +| **`vpn register `** then **`vpn up`** | Join your laptop to the whole private network over WireGuard (reach every sandbox by name/IP). `vpn up` needs `wg-quick` + `sudo` and **blocks until Ctrl-C** โ€” run it in your own terminal (`!cos vpn up`). | +| **`fork`** | Snapshot the warm project box โ†’ an independent clone for matrix/parallel experiments. The fork is self-managed. | ### Pause, resume, and custom images @@ -281,14 +283,15 @@ Mount **your own** S3 bucket into the project box. - `-p ` โ€” a curated bundle of the registries + CDNs a build actually reaches (repeatable, composes with `-e`). - `-E` โ€” force explicitly unrestricted (silences the warning for trusted offloads). -| Preset | Hosts allowed | -|---|---| -| `python-uv` | `astral.sh`, `releases.astral.sh`, `pypi.org`, `files.pythonhosted.org` | -| `rust-cargo` | `crates.io`, `static.crates.io`, `index.crates.io`, `static.rust-lang.org`, `cdn.pyke.io` | -| `npm` | `registry.npmjs.org` | -| `github` | `github.com`, `objects.githubusercontent.com`, `raw.githubusercontent.com`, `codeload.github.com` | +| Preset | Hosts allowed | +| ------------ | ------------------------------------------------------------------------------------------------- | +| `python-uv` | `astral.sh`, `releases.astral.sh`, `pypi.org`, `files.pythonhosted.org` | +| `rust-cargo` | `crates.io`, `static.crates.io`, `index.crates.io`, `static.rust-lang.org`, `cdn.pyke.io` | +| `npm` | `registry.npmjs.org` | +| `github` | `github.com`, `objects.githubusercontent.com`, `raw.githubusercontent.com`, `codeload.github.com` | > `cdn.pyke.io` is included in `rust-cargo` because `ort-sys` (ONNX Runtime) fetches from it. Compose presets and add stragglers with `-e `: +> > ```bash > /createos-sandbox:offload -p python-uv -p rust-cargo -e cdn.example.com . "โ€ฆ" > ``` @@ -305,7 +308,7 @@ So a domain allowlist is the right control for "this build should only reach pyp - **Egress:** default unrestricted; use `-p`/`-e` to lock it down (see above). - **Keepalive:** long/quiet compiles run detached with a heartbeat and survive stream drops. `-K` keeps the box on a real failure for inspection. - **Excludes:** `.git`/`target`/`node_modules`/`__pycache__`/`.venv`/media are excluded from the upload by default; `-x ` adds more. -- **Swap caveat:** `-w ` *attempts* swap, but `devbox:1` can't `swapon` today โ€” so a torch/maturin build on a small box may OOM/ENOSPC. Install only the extra/group you need (e.g. `uv sync --group dev`) rather than `--all-extras`. +- **Swap caveat:** `-w ` _attempts_ swap, but `devbox:1` can't `swapon` today โ€” so a torch/maturin build on a small box may OOM/ENOSPC. Install only the extra/group you need (e.g. `uv sync --group dev`) rather than `--all-extras`. - **Shape rejection:** a shape your account can't use fails with a clean `Allowed: [...]` list โ€” pick from it. - **Bandwidth:** each box starts with a fixed allowance for traffic it initiates (5 GiB by default). A large model download or `docker pull` can exhaust it, after which outbound traffic stops. Top it up additively via `createos sandbox edit `; the exec, file-transfer, and tunnel channels stay reachable regardless. @@ -328,14 +331,14 @@ Add more with `-x ` (repeatable). Install dependencies **inside** the box ## The skill -`using-createos-sandbox` teaches Claude *when* to offload โ€” untrusted code, heavy builds/tests, parallel/matrix work, clean-room repros, live dev loops โ€” so it reaches for the sandbox on its own instead of grinding on your laptop. It drives the same `scripts/cos` helper the slash commands use. +`using-createos-sandbox` teaches Claude _when_ to offload โ€” untrusted code, heavy builds/tests, parallel/matrix work, clean-room repros, live dev loops โ€” so it reaches for the sandbox on its own instead of grinding on your laptop. It drives the same `scripts/cos` helper the slash commands use. `SKILL.md` stays a decision surface; the depth lives in `skills/using-createos-sandbox/references/` and is loaded only when a task needs it: -| Reference | Covers | -|---|---| -| `offload-and-egress.md` | offload flag table, egress presets and enforcement caveats, fanout, upload excludes, heavy-build OOM/disk/bandwidth traps | -| `networking.md` | choosing between tunnel/expose/cluster/vpn, cluster DNS names, expose gotchas, WireGuard setup | +| Reference | Covers | +| ------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `offload-and-egress.md` | offload flag table, egress presets and enforcement caveats, fanout, upload excludes, heavy-build OOM/disk/bandwidth traps | +| `networking.md` | choosing between tunnel/expose/cluster/vpn, cluster DNS names, expose gotchas, WireGuard setup | | `lifecycle-and-images.md` | pause/resume, auto-pause tuning, fork caveats, built-in rootfs vs custom templates, env vars, remote editor, self-terminating jobs, single-file transfer, measured timings | ## Hooks @@ -371,19 +374,19 @@ cos down # stops sync/tunnels, destr - **One-way by default.** `sync` and offload uploads are laptop โ†’ box; box-side writes never touch local unless you opt into `-2` (two-way) or pull with `offload -o`. Use `-2` deliberately โ€” never casually on a repo root. `-M` (mirror) additionally **deletes** box-side extras. - **Scoped statefile.** `cos` only ever touches boxes it created (`cos-*`) or the project box recorded in its statefile. Your other sandboxes are never touched. State lives at `${COS_STATE_DIR:-${XDG_CACHE_HOME:-~/.cache}/createos-sandbox}/.json` (plus a `.tunnels` sidecar). - **Quota.** External keys have been observed to allow 2 boxes running at once, with a daily creation cap. Neither is published policy โ€” treat them as observed behaviour, budget `cluster`/`fanout` against them, and expect excess jobs to queue rather than fail. -- **Ending a session.** `pause` keeps the box (and its warm state) at zero compute cost; `down` destroys it. Either is fine โ€” leaving a box *running* is not. +- **Ending a session.** `pause` keeps the box (and its warm state) at zero compute cost; `down` destroys it. Either is fine โ€” leaving a box _running_ is not. - **Untrusted code.** Restrict egress (`-p`/`-e`) so a malicious dependency can't exfiltrate or phone home. ## Environment variables -| Variable | Effect | -|---|---| -| `CREATEOS_API_KEY` | sign in without browser login (CI / headless / devcontainers); verify with `cos auth` | -| `COS_NO_AUTOINSTALL=1` | don't auto-install the `createos` CLI | -| `COS_CLI_INSTALL_URL` | override the CLI install-script source | -| `COS_CLI` | use a specific `createos` binary instead of the one on `PATH` | -| `COS_STATE_DIR` | override the statefile directory (default `${XDG_CACHE_HOME:-~/.cache}/createos-sandbox`) | -| `COS_NO_HINT=1` | silence the auto-suggest hook | +| Variable | Effect | +| ---------------------- | ----------------------------------------------------------------------------------------- | +| `CREATEOS_API_KEY` | sign in without browser login (CI / headless / devcontainers); verify with `cos auth` | +| `COS_NO_AUTOINSTALL=1` | don't auto-install the `createos` CLI | +| `COS_CLI_INSTALL_URL` | override the CLI install-script source | +| `COS_CLI` | use a specific `createos` binary instead of the one on `PATH` | +| `COS_STATE_DIR` | override the statefile directory (default `${XDG_CACHE_HOME:-~/.cache}/createos-sandbox`) | +| `COS_NO_HINT=1` | silence the auto-suggest hook | ## Troubleshooting diff --git a/createos-sandbox/commands/cluster.md b/packages/claude-code-plugin/commands/cluster.md similarity index 100% rename from createos-sandbox/commands/cluster.md rename to packages/claude-code-plugin/commands/cluster.md diff --git a/createos-sandbox/commands/disk.md b/packages/claude-code-plugin/commands/disk.md similarity index 100% rename from createos-sandbox/commands/disk.md rename to packages/claude-code-plugin/commands/disk.md diff --git a/createos-sandbox/commands/down.md b/packages/claude-code-plugin/commands/down.md similarity index 100% rename from createos-sandbox/commands/down.md rename to packages/claude-code-plugin/commands/down.md diff --git a/createos-sandbox/commands/expose.md b/packages/claude-code-plugin/commands/expose.md similarity index 100% rename from createos-sandbox/commands/expose.md rename to packages/claude-code-plugin/commands/expose.md diff --git a/createos-sandbox/commands/fanout.md b/packages/claude-code-plugin/commands/fanout.md similarity index 100% rename from createos-sandbox/commands/fanout.md rename to packages/claude-code-plugin/commands/fanout.md diff --git a/createos-sandbox/commands/fork.md b/packages/claude-code-plugin/commands/fork.md similarity index 100% rename from createos-sandbox/commands/fork.md rename to packages/claude-code-plugin/commands/fork.md diff --git a/createos-sandbox/commands/offload.md b/packages/claude-code-plugin/commands/offload.md similarity index 100% rename from createos-sandbox/commands/offload.md rename to packages/claude-code-plugin/commands/offload.md diff --git a/createos-sandbox/commands/pause.md b/packages/claude-code-plugin/commands/pause.md similarity index 100% rename from createos-sandbox/commands/pause.md rename to packages/claude-code-plugin/commands/pause.md diff --git a/createos-sandbox/commands/resume.md b/packages/claude-code-plugin/commands/resume.md similarity index 100% rename from createos-sandbox/commands/resume.md rename to packages/claude-code-plugin/commands/resume.md diff --git a/createos-sandbox/commands/run.md b/packages/claude-code-plugin/commands/run.md similarity index 100% rename from createos-sandbox/commands/run.md rename to packages/claude-code-plugin/commands/run.md diff --git a/createos-sandbox/commands/shell.md b/packages/claude-code-plugin/commands/shell.md similarity index 100% rename from createos-sandbox/commands/shell.md rename to packages/claude-code-plugin/commands/shell.md diff --git a/createos-sandbox/commands/status.md b/packages/claude-code-plugin/commands/status.md similarity index 100% rename from createos-sandbox/commands/status.md rename to packages/claude-code-plugin/commands/status.md diff --git a/createos-sandbox/commands/sync.md b/packages/claude-code-plugin/commands/sync.md similarity index 100% rename from createos-sandbox/commands/sync.md rename to packages/claude-code-plugin/commands/sync.md diff --git a/createos-sandbox/commands/template.md b/packages/claude-code-plugin/commands/template.md similarity index 100% rename from createos-sandbox/commands/template.md rename to packages/claude-code-plugin/commands/template.md diff --git a/createos-sandbox/commands/tunnel.md b/packages/claude-code-plugin/commands/tunnel.md similarity index 91% rename from createos-sandbox/commands/tunnel.md rename to packages/claude-code-plugin/commands/tunnel.md index 61dcc94..e93184d 100644 --- a/createos-sandbox/commands/tunnel.md +++ b/packages/claude-code-plugin/commands/tunnel.md @@ -4,7 +4,7 @@ argument-hint: " [local-port]" allowed-tools: Bash --- -Forward a box port to `localhost` in the background. Start the service in the box first (e.g. `/createos-sandbox:run 'npm run dev &'`), then tunnel its port. Local port defaults to the remote port. For a *public* shareable URL instead, use `/createos-sandbox:expose`. +Forward a box port to `localhost` in the background. Start the service in the box first (e.g. `/createos-sandbox:run 'npm run dev &'`), then tunnel its port. Local port defaults to the remote port. For a _public_ shareable URL instead, use `/createos-sandbox:expose`. !`"${CLAUDE_PLUGIN_ROOT}/scripts/cos" tunnel $ARGUMENTS` diff --git a/createos-sandbox/commands/unexpose.md b/packages/claude-code-plugin/commands/unexpose.md similarity index 100% rename from createos-sandbox/commands/unexpose.md rename to packages/claude-code-plugin/commands/unexpose.md diff --git a/createos-sandbox/commands/up.md b/packages/claude-code-plugin/commands/up.md similarity index 100% rename from createos-sandbox/commands/up.md rename to packages/claude-code-plugin/commands/up.md diff --git a/createos-sandbox/commands/vpn.md b/packages/claude-code-plugin/commands/vpn.md similarity index 100% rename from createos-sandbox/commands/vpn.md rename to packages/claude-code-plugin/commands/vpn.md diff --git a/createos-sandbox/hooks/hooks.json b/packages/claude-code-plugin/hooks/hooks.json similarity index 100% rename from createos-sandbox/hooks/hooks.json rename to packages/claude-code-plugin/hooks/hooks.json diff --git a/createos-sandbox/scripts/cos b/packages/claude-code-plugin/scripts/cos similarity index 100% rename from createos-sandbox/scripts/cos rename to packages/claude-code-plugin/scripts/cos diff --git a/createos-sandbox/scripts/offload-hint.sh b/packages/claude-code-plugin/scripts/offload-hint.sh similarity index 100% rename from createos-sandbox/scripts/offload-hint.sh rename to packages/claude-code-plugin/scripts/offload-hint.sh diff --git a/createos-sandbox/scripts/session-start.sh b/packages/claude-code-plugin/scripts/session-start.sh similarity index 100% rename from createos-sandbox/scripts/session-start.sh rename to packages/claude-code-plugin/scripts/session-start.sh diff --git a/createos-sandbox/skills/using-createos-sandbox/SKILL.md b/packages/claude-code-plugin/skills/using-createos-sandbox/SKILL.md similarity index 78% rename from createos-sandbox/skills/using-createos-sandbox/SKILL.md rename to packages/claude-code-plugin/skills/using-createos-sandbox/SKILL.md index a7d8083..d4762f4 100644 --- a/createos-sandbox/skills/using-createos-sandbox/SKILL.md +++ b/packages/claude-code-plugin/skills/using-createos-sandbox/SKILL.md @@ -36,20 +36,20 @@ Every `cos` command except `install` and `auth` runs this check first, so an una ## When to reach for it -| Situation | Why offload | -|---|---| -| **Untrusted / unknown code** โ€” a snippet, a fresh npm/pip package, scraped code, a PoC exploit | Isolation. The blast radius is one disposable box, not the laptop. | -| **Heavy build or test suite** โ€” big `make`, full test run, compile, benchmark | Keeps the laptop free; runs on a box sized for it. | -| **Parallel/matrix work** โ€” same job across N configs, test shards, batch | `fanout` โ€” each command in its own throwaway box, concurrently, results collected. | -| **Quick scratch Linux** โ€” try a CLI/tool/snippet on a clean box | `shell` โ€” instant keyless box, destroyed on exit (interactive; the user runs it). | -| **Clean-room repro** โ€” "works on my machine" bugs, dependency conflicts | Fresh rootfs every time, no host state. | -| **Live dev loop** โ€” dev server / test watcher / REPL that reacts to edits | Project box + `sync`; Claude edits locally, the box reacts. | -| **Reach a box-side service** โ€” dev server, DB, API | `tunnel` (private, to `127.0.0.1`) or `expose` (public HTTPS link to share). | -| **Multi-machine** โ€” distributed system, DB replication, p2p mesh, load test | `cluster up N` โ€” boxes share one private net, reach each other by name. | -| **Same setup, many variants** โ€” try N branches from one prepared box | `fork` the project box into independent clones. | -| **Repeated identical setup** โ€” every offload starts with the same install prelude | `template` โ€” bake the toolchain into an image once. | -| **Done for now, back tomorrow** โ€” warm box you don't want to rebuild | `pause` โ€” snapshot at zero compute cost, `resume` restores it exactly. | -| **Big data / weights / shared cache** | `disk` โ€” BYO S3 bucket mounted into the box, survives box death. | +| Situation | Why offload | +| ---------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | +| **Untrusted / unknown code** โ€” a snippet, a fresh npm/pip package, scraped code, a PoC exploit | Isolation. The blast radius is one disposable box, not the laptop. | +| **Heavy build or test suite** โ€” big `make`, full test run, compile, benchmark | Keeps the laptop free; runs on a box sized for it. | +| **Parallel/matrix work** โ€” same job across N configs, test shards, batch | `fanout` โ€” each command in its own throwaway box, concurrently, results collected. | +| **Quick scratch Linux** โ€” try a CLI/tool/snippet on a clean box | `shell` โ€” instant keyless box, destroyed on exit (interactive; the user runs it). | +| **Clean-room repro** โ€” "works on my machine" bugs, dependency conflicts | Fresh rootfs every time, no host state. | +| **Live dev loop** โ€” dev server / test watcher / REPL that reacts to edits | Project box + `sync`; Claude edits locally, the box reacts. | +| **Reach a box-side service** โ€” dev server, DB, API | `tunnel` (private, to `127.0.0.1`) or `expose` (public HTTPS link to share). | +| **Multi-machine** โ€” distributed system, DB replication, p2p mesh, load test | `cluster up N` โ€” boxes share one private net, reach each other by name. | +| **Same setup, many variants** โ€” try N branches from one prepared box | `fork` the project box into independent clones. | +| **Repeated identical setup** โ€” every offload starts with the same install prelude | `template` โ€” bake the toolchain into an image once. | +| **Done for now, back tomorrow** โ€” warm box you don't want to rebuild | `pause` โ€” snapshot at zero compute cost, `resume` restores it exactly. | +| **Big data / weights / shared cache** | `disk` โ€” BYO S3 bucket mounted into the box, survives box death. | Do NOT offload trivial commands, anything needing the user's local secrets/SSH/cloud creds, or work that must touch real local filesystem state. @@ -89,7 +89,7 @@ cos offload -e pypi.org -e files.pythonhosted.org ./suspect 'python3 main.py' Two things about this that are easy to get wrong: - **Egress is unrestricted by default.** A fresh box can reach anything; `cos` prints a one-line notice. Restricting is opt-in with `-p ` or `-e `. So "run this untrusted thing in a sandbox" is only half done until you pass one of those. -- **Uploads are one-way.** Box-side changes never touch the local tree unless you ask with `-o `. `.git`, `node_modules`, `target`, `.venv` and friends are excluded from the upload by default โ€” dependencies are meant to be built *inside* the box. +- **Uploads are one-way.** Box-side changes never touch the local tree unless you ask with `-o `. `.git`, `node_modules`, `target`, `.venv` and friends are excluded from the upload by default โ€” dependencies are meant to be built _inside_ the box. Long, quiet builds survive a dropped connection: the command runs detached with a heartbeat watcher that re-attaches if the stream dies. The real exit code is preserved. @@ -118,9 +118,9 @@ cos resume # bring it back exactly as it was cos down # stop sync + destroy the box ``` -**`up` is for a box you intend to reuse and then tear down.** A bare "run this in a sandbox" is *not* Pattern B โ€” use `cos offload` (one-shot, auto-destroys) or `cos shell`. Reaching for `up` to satisfy "create a sandbox" makes the box outlive the task, and a later `cos down` destroys it along with anything else sharing that statefile. +**`up` is for a box you intend to reuse and then tear down.** A bare "run this in a sandbox" is _not_ Pattern B โ€” use `cos offload` (one-shot, auto-destroys) or `cos shell`. Reaching for `up` to satisfy "create a sandbox" makes the box outlive the task, and a later `cos down` destroys it along with anything else sharing that statefile. -**Ending a session: prefer `pause` over `down`** when the box has a warm toolchain the user will want again. `down` destroys and the next session reinstalls everything; `pause` snapshots disk *and* memory, stops compute billing, and brings everything back on `resume` โ€” measured end-to-end at around 6โ€“8 s each way through the CLI. Use `down` when the work is genuinely finished. +**Ending a session: prefer `pause` over `down`** when the box has a warm toolchain the user will want again. `down` destroys and the next session reinstalls everything; `pause` snapshots disk _and_ memory, stops compute billing, and brings everything back on `resume` โ€” measured end-to-end at around 6โ€“8 s each way through the CLI. Use `down` when the work is genuinely finished. If a box under this project's name is running but the statefile is gone (another checkout, another agent, created by hand), `up` **refuses** rather than adopting it โ€” adopting silently would let a later `cos down` destroy a box this project never created. `cos up -a` adopts explicitly, and an adopted box is never destroyed by `cos down`. @@ -128,12 +128,12 @@ If a box under this project's name is running but the statefile is gone (another `cos sync` defaults to **one-way (laptop โ†’ box)** โ€” the safe direction for a dev loop. -| Flag | Mode | Behavior | -|---|---|---| -| *(default)* | `one-way` | laptop wins; box changes NOT pulled back. **No bleed-back.** | -| `-2` | `two-way` | bidirectional; box-side writes (build output, deps) **flow back** to the local dir | -| `-M` | `mirror` | one-way **and deletes** box-side files absent locally | -| `-x ` | โ€” | exclude paths (repeatable) | +| Flag | Mode | Behavior | +| ----------- | --------- | ---------------------------------------------------------------------------------- | +| _(default)_ | `one-way` | laptop wins; box changes NOT pulled back. **No bleed-back.** | +| `-2` | `two-way` | bidirectional; box-side writes (build output, deps) **flow back** to the local dir | +| `-M` | `mirror` | one-way **and deletes** box-side files absent locally | +| `-x ` | โ€” | exclude paths (repeatable) | `.git` and the big regenerable dirs are excluded by default โ€” build deps inside the box with `cos run 'npm ci'` rather than syncing them up. Only reach for `-2` when a box-side process genuinely produces files you need back locally, and never on the user's repo root without saying so first; prefer `offload -o` for pulling artifacts. The local dir must resolve under `$HOME` or `/tmp`. The first sync downloads its sync engine, so allow a minute before edits propagate. @@ -180,8 +180,8 @@ Disk data lives in the user's own S3 account and region. `--path-style` is neede Load these when the task actually needs the depth โ€” the summaries above are enough for most work. -| File | Read it for | -|---|---| -| `references/offload-and-egress.md` | offload flag table, egress presets and how enforcement really behaves, fanout, upload excludes, heavy-build OOM/disk/bandwidth traps | -| `references/networking.md` | choosing between tunnel/expose/cluster/vpn, cluster DNS names, expose gotchas, WireGuard setup | +| File | Read it for | +| ------------------------------------ | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | +| `references/offload-and-egress.md` | offload flag table, egress presets and how enforcement really behaves, fanout, upload excludes, heavy-build OOM/disk/bandwidth traps | +| `references/networking.md` | choosing between tunnel/expose/cluster/vpn, cluster DNS names, expose gotchas, WireGuard setup | | `references/lifecycle-and-images.md` | pause/resume, auto-pause tuning, fork caveats, built-in rootfs vs custom templates, env vars, remote editor, self-terminating jobs, single-file transfer, measured timings | diff --git a/createos-sandbox/skills/using-createos-sandbox/references/lifecycle-and-images.md b/packages/claude-code-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md similarity index 90% rename from createos-sandbox/skills/using-createos-sandbox/references/lifecycle-and-images.md rename to packages/claude-code-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md index 0f62834..3cfb404 100644 --- a/createos-sandbox/skills/using-createos-sandbox/references/lifecycle-and-images.md +++ b/packages/claude-code-plugin/skills/using-createos-sandbox/references/lifecycle-and-images.md @@ -52,19 +52,19 @@ The clone is a fully independent box with its own id, IP, and quota ledger. The Two things to know: -- A fork is **not** tracked as the project box, so `cos down` leaves it running. It *is* recorded in the statefile: `cos status` lists forks, `cos down` names the survivors, and `cos down -f` reaps them. Otherwise destroy it yourself with `createos sandbox rm -y `. +- A fork is **not** tracked as the project box, so `cos down` leaves it running. It _is_ recorded in the statefile: `cos status` lists forks, `cos down` names the survivors, and `cos down -f` reaps them. Otherwise destroy it yourself with `createos sandbox rm -y `. - **Mounted disks do not carry across a fork.** If the source box had an S3 disk attached, re-attach it on the clone. ## Images: built-in rootfs and custom templates Built-ins are kept warm on the hosts, so they boot with no image pull: -| Rootfs | Notes | -|---|---| -| `devbox:1` | Debian, batteries included, the default โ€” has `sshd`, which `sync` and the editor path need | -| `ubuntu:26.04` | plain Ubuntu | -| `debian:13` | trixie | -| `alpine:3.20` | musl + busybox, far smaller; expect glibc-linked binaries and wheels not to work | +| Rootfs | Notes | +| -------------- | ------------------------------------------------------------------------------------------- | +| `devbox:1` | Debian, batteries included, the default โ€” has `sshd`, which `sync` and the editor path need | +| `ubuntu:26.04` | plain Ubuntu | +| `debian:13` | trixie | +| `alpine:3.20` | musl + busybox, far smaller; expect glibc-linked binaries and wheels not to work | `createos sandbox rootfs` lists what the account can actually boot. @@ -111,7 +111,7 @@ createos sandbox editor --remove # tear down the ~/.ssh/config entry It generates a per-box SSH key, starts `sshd` in the box, writes a `~/.ssh/config` block so plain `ssh ` works, and launches the editor pointed at the remote. It needs a rootfs with `sshd` (`devbox:1` has it) and a shape with more than 2 GiB of RAM โ€” language servers OOM on a 1 GiB box. -This is the right tool when the user wants to *work in* the sandbox rather than have an agent drive it. It launches a GUI editor, so it belongs to the user, not to an agent command. +This is the right tool when the user wants to _work in_ the sandbox rather than have an agent drive it. It launches a GUI editor, so it belongs to the user, not to an agent command. ## Letting a job end its own box diff --git a/createos-sandbox/skills/using-createos-sandbox/references/networking.md b/packages/claude-code-plugin/skills/using-createos-sandbox/references/networking.md similarity index 91% rename from createos-sandbox/skills/using-createos-sandbox/references/networking.md rename to packages/claude-code-plugin/skills/using-createos-sandbox/references/networking.md index a56a726..05ab935 100644 --- a/createos-sandbox/skills/using-createos-sandbox/references/networking.md +++ b/packages/claude-code-plugin/skills/using-createos-sandbox/references/networking.md @@ -12,12 +12,12 @@ Read this when something needs to talk to a sandbox: a browser, a teammate, anot ## Choosing between tunnel, expose, cluster, and VPN -| Need | Reach for | -|---|---| -| Hit a box-side dev server from this machine's browser | `tunnel` | -| Give a teammate a link, or point a webhook at the box | `expose` | +| Need | Reach for | +| --------------------------------------------------------------------------------- | --------- | +| Hit a box-side dev server from this machine's browser | `tunnel` | +| Give a teammate a link, or point a webhook at the box | `expose` | | Boxes that need to talk to each other (replication, p2p, load generator โ†’ target) | `cluster` | -| The laptop needs the whole private network, not one port | `vpn` | +| The laptop needs the whole private network, not one port | `vpn` | `tunnel` and `expose` operate on the project box, so `cos up` first. `cluster` manages its own set of boxes. `cos down` tears down tunnels, the public URL, and the cluster along with the project box. diff --git a/createos-sandbox/skills/using-createos-sandbox/references/offload-and-egress.md b/packages/claude-code-plugin/skills/using-createos-sandbox/references/offload-and-egress.md similarity index 78% rename from createos-sandbox/skills/using-createos-sandbox/references/offload-and-egress.md rename to packages/claude-code-plugin/skills/using-createos-sandbox/references/offload-and-egress.md index fddcdfb..e0d45a6 100644 --- a/createos-sandbox/skills/using-createos-sandbox/references/offload-and-egress.md +++ b/packages/claude-code-plugin/skills/using-createos-sandbox/references/offload-and-egress.md @@ -15,17 +15,17 @@ Read this when an offload needs tuning: restricting what a build can reach, sizi Flags come **before** the ` ` positionals โ€” `cos` parses with `getopts`, which stops at the first positional. -| Flag | Effect | -|---|---| -| `-s ` | box size (default `s-1vcpu-1gb`); list with `createos sandbox shapes` | -| `-r ` | base image or custom template (default `devbox:1`) | -| `-o ` | tar this path out of `/work` back into the local dir after the run | -| `-w ` | try to add a swapfile (best-effort โ€” see below) | -| `-K` | keep the box if the command exits non-zero, so the cache survives for a retry | -| `-e ` | allow one outbound destination (repeatable) | -| `-p ` | apply an egress preset (repeatable, composes with `-e`) | -| `-E` | explicitly unrestricted egress | -| `-x ` | extra upload exclude (repeatable) | +| Flag | Effect | +| ------------- | ----------------------------------------------------------------------------- | +| `-s ` | box size (default `s-1vcpu-1gb`); list with `createos sandbox shapes` | +| `-r ` | base image or custom template (default `devbox:1`) | +| `-o ` | tar this path out of `/work` back into the local dir after the run | +| `-w ` | try to add a swapfile (best-effort โ€” see below) | +| `-K` | keep the box if the command exits non-zero, so the cache survives for a retry | +| `-e ` | allow one outbound destination (repeatable) | +| `-p ` | apply an egress preset (repeatable, composes with `-e`) | +| `-E` | explicitly unrestricted egress | +| `-x ` | extra upload exclude (repeatable) | The box is destroyed on success, failure, or interrupt. Two things override that: `-K` on a failing command, and an infra/stream error โ€” both keep the box so the build cache survives, and `cos` prints the reconnect and destroy commands. @@ -37,7 +37,7 @@ The local tree goes to `/work` in the box, one-way. Box-side changes never touch Excluded from the upload by default: `.git`, `target`, `node_modules`, `__pycache__`, `.venv`, `.mypy_cache`, `.pytest_cache`, `.gradle`, `.cargo/registry`, `dist`, `build`, `.next`, `.turbo`, and large media (`*.gif`, `*.mp4`, `*.mov`, `*.zst`). Add more with `-x `. -Dependencies are meant to be built *inside* the box, not shipped into it โ€” that is why `node_modules` and friends are excluded rather than uploaded. +Dependencies are meant to be built _inside_ the box, not shipped into it โ€” that is why `node_modules` and friends are excluded rather than uploaded. ## Egress: how the firewall actually behaves @@ -56,12 +56,12 @@ DNS keeps resolving even for blocked destinations โ€” a blocked connection fails ## Egress presets -| Preset | Opens | -|---|---| -| `python-uv` | `astral.sh`, `releases.astral.sh`, `pypi.org`, `files.pythonhosted.org` | -| `rust-cargo` | `crates.io`, `static.crates.io`, `index.crates.io`, `static.rust-lang.org`, `cdn.pyke.io` | -| `npm` | `registry.npmjs.org` | -| `github` | `github.com`, `objects.githubusercontent.com`, `raw.githubusercontent.com`, `codeload.github.com` | +| Preset | Opens | +| ------------ | ------------------------------------------------------------------------------------------------- | +| `python-uv` | `astral.sh`, `releases.astral.sh`, `pypi.org`, `files.pythonhosted.org` | +| `rust-cargo` | `crates.io`, `static.crates.io`, `index.crates.io`, `static.rust-lang.org`, `cdn.pyke.io` | +| `npm` | `registry.npmjs.org` | +| `github` | `github.com`, `objects.githubusercontent.com`, `raw.githubusercontent.com`, `codeload.github.com` | `cdn.pyke.io` is the non-obvious one: `ort-sys` (ONNX Runtime, pulled in by a lot of ML crates) downloads prebuilt binaries from it, so a `cargo build` that looks pure-Rust fails without it. It is already in `rust-cargo`. diff --git a/packages/pi-extension/.gitignore b/packages/pi-extension/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/packages/pi-extension/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/packages/pi-extension/CLAUDE.md b/packages/pi-extension/CLAUDE.md new file mode 100644 index 0000000..8225020 --- /dev/null +++ b/packages/pi-extension/CLAUDE.md @@ -0,0 +1,137 @@ +# pi-createos-plugin + +Pi coding agent extension for [CreateOS Sandbox](https://nodeops.network/createos). +Runs all tool calls inside a remote sandbox while the agent runs locally. + +## Architecture + +CLI-only โ€” every operation shells out to the `createos` CLI via `pi.exec()`. +No HTTP client, no SDK, no API keys. Auth is handled by `createos login`. + +``` +Pi agent (local) โ†’ pi.exec('createos', [...]) โ†’ CreateOS API โ†’ Sandbox +``` + +## File layout + +| File | Purpose | +| ------------------ | -------------------------------------------------------------------------- | +| `index.ts` | Extension entry point: flags, slash commands, lifecycle hooks | +| `src/cli.ts` | All `createos` CLI wrappers (sandbox, network, disk, device, tunnel, sync) | +| `src/tools.ts` | 33 registered tools, each single-purpose with `sandbox_` prefix | +| `src/ops.ts` | BashOps/ReadOps/WriteOps/EditOps/LsOps backed by CLI exec/push/pull | +| `src/find-tool.ts` | Remote find via `createos sandbox exec` (rg/POSIX find fallback) | +| `src/grep-tool.ts` | Remote grep via `createos sandbox exec` (rg/POSIX grep fallback) | +| `src/util.ts` | `shellQuote`, `shortId`, `joinPath` | + +## Pi extension best practices (enforced) + +- **snake_case** tool names, all prefixed `sandbox_` +- **promptGuidelines** always name the specific tool: `"Use sandbox_xyz when..."` +- **description** describes what the tool does, never when to use it +- **promptSnippet** is a brief one-liner for the system prompt +- **Single-purpose tools** โ€” no action enum parameters +- **`terminate: true`** on destructive tools (`sandbox_pause`, `sandbox_destroy`) +- **Signal handling** on built-in tool replacements (find/grep check `signal?.aborted`) +- **No background resources from factory** โ€” all started in `session_start` +- **Cleanup in `session_shutdown`** โ€” temp SSH key + sandbox destroy + +## Tool inventory (33 tools) + +### Built-in replacements (7) + +`bash`, `read`, `write`, `edit`, `ls`, `find`, `grep` โ€” transparently routed +to the sandbox when `--createos` is active, local when off. + +### Sandbox lifecycle (7) + +`sandbox_create`, `sandbox_exec`, `sandbox_info`, `sandbox_list`, +`sandbox_pause`, `sandbox_resume`, `sandbox_fork`, `sandbox_destroy` + +### Sandbox config (5) + +`sandbox_ingress`, `sandbox_firewall`, `sandbox_bandwidth`, +`sandbox_shapes`, `sandbox_images` + +### Ports & sync (3) + +`sandbox_preview_url`, `sandbox_tunnel`, `sandbox_sync` + +### Networks (6) + +`sandbox_network_create`, `sandbox_network_list`, `sandbox_network_show`, +`sandbox_network_attach`, `sandbox_network_detach`, `sandbox_network_delete` + +### Disks (6) + +`sandbox_disk_create`, `sandbox_disk_list`, `sandbox_disk_show`, +`sandbox_disk_delete`, `sandbox_disk_attach`, `sandbox_disk_detach` + +### Device VPN (5) + +`sandbox_device_register`, `sandbox_device_status`, `sandbox_vpn_up`, +`sandbox_device_attach`, `sandbox_device_detach` + +## Flags + +| Flag | Type | Purpose | +| ------------ | ------- | ------------------------------------------------ | +| `--createos` | boolean | Activate the extension | +| `--shape` | string | Sandbox size (default: `s-2vcpu-2gb`) | +| `--rootfs` | string | Base image or template | +| `--network` | string | Network(s) to join at creation (comma-separated) | + +## Slash commands + +| Command | Purpose | +| ----------------------- | --------------------------------------------------- | +| `/sandbox` | Show active sandbox status | +| `/network [args]` | Network CRUD (create, ls, show, rm, attach, detach) | +| `/device [args]` | Device status, attach, detach | + +## Lifecycle + +1. `session_start` โ€” preflight checks, create sandbox, set up workspace +2. `before_agent_start` โ€” inject sandbox context into system prompt +3. `session_shutdown` โ€” clean up temp SSH key, destroy sandbox (ephemeral) or keep (persisted) + +## CLI JSON support + +Commands that return structured data must use `-o json`. The CLI auto-detects +non-TTY but Pi's `pi.exec()` may allocate a PTY, so we force `-o json` explicitly. + +Commands that don't support `-o json` (create, network create, disk create, fork) +were patched in `createos-cli` to support it via `output.Render()`. + +## Key design decisions + +- **CLI-only**: Zero HTTP client. The `createos` CLI handles auth, retries, and + all API complexity. Extension maintenance is near-zero โ€” new CLI features + work automatically. + +- **Temp SSH key for sync**: `sandbox_sync` uses Mutagen which needs SSH. + The user's key may be passphrase-protected (can't prompt non-interactively). + We generate a throwaway ed25519 key per session, cleaned up on shutdown. + +- **Tunnel flag order**: urfave/cli v2 stops parsing flags after positional + args. Flags go before the sandbox ID: `tunnel --remote 5432 `. + +- **No git/GitHub integration**: Removed. The agent uses `bash` for git + commands inside the sandbox. No auto-branch, no auto-push. + +- **`sandbox_vpn_up` is advisory**: VPN needs sudo โ€” the tool returns + the command for the user to run in another terminal instead of executing it. + +## Adding a new tool + +1. Add the CLI wrapper function in `src/cli.ts` +2. Register the tool in `src/tools.ts` with: + - `name: 'sandbox__'` + - `label: 'Title Case'` + - `description:` what it does (1-2 sentences) + - `promptSnippet:` one-liner + - `promptGuidelines: ['Use sandbox_ when...']` + - `parameters: Type.Object({...})` + - `execute()` with try/catch returning `txt()` on error +3. If destructive, add `terminate: true` to the return value +4. If it needs `-o json`, verify the CLI command supports it (add `output.Render()` if not) diff --git a/packages/pi-extension/README.md b/packages/pi-extension/README.md new file mode 100644 index 0000000..2929406 --- /dev/null +++ b/packages/pi-extension/README.md @@ -0,0 +1,94 @@ +# @createos/pi + +Pi coding agent extension that runs all tool calls inside a remote [CreateOS Sandbox](https://nodeops.network/createos) while the agent runs locally. + +## Install + +```bash +npm install -g @earendil-works/pi-coding-agent +pi install npm:@createos/pi +``` + +## Setup + +Just have the CreateOS CLI installed and logged in: + +```bash +createos login +``` + +That's it. No API keys, no env vars. + +## Usage + +```bash +pi --createos +``` + +Everything works inside the session โ€” sandbox, networks, device access, port forwarding. + +### Optional flags + +| Flag | Purpose | +| ------------------ | ------------------------------------------------ | +| `--shape ` | Sandbox size (default: `s-2vcpu-2gb`) | +| `--rootfs ` | Base image or template | +| `--network ` | Network(s) to join at creation (comma-separated) | + +### In-session commands + +| Command | Description | +| -------------------------- | ------------------------------------- | +| `/sandbox` | Show sandbox status | +| `/network create ` | Create a private network | +| `/network ls` | List your networks | +| `/network show ` | Show network members + IPs | +| `/network attach ` | Join this sandbox to a network | +| `/network detach ` | Leave a network | +| `/network rm ` | Delete a network | +| `/device status` | Show registered devices | +| `/device attach ` | Give your machine access to a network | +| `/device detach ` | Remove access | + +### Agent tools + +The LLM agent has these tools available automatically: + +- **`sandbox_manage`** โ€” full lifecycle: pause, resume, fork, destroy, ingress, firewall, bandwidth, shapes, images +- **`tunnel`** โ€” port-forward a sandbox port to `localhost` on the user's machine +- **`preview_url`** โ€” get a public HTTPS URL for any port in the sandbox +- **`network`** โ€” create/manage private networks for sandbox-to-sandbox communication +- **`device`** โ€” check device status, attach to networks for direct IP access + +### Private networks + +Sandboxes on the same network can reach each other by IP: + +```bash +pi --createos --network backend +``` + +## How it works + +1. On `pi --createos`, checks that `createos` CLI is installed and logged in +2. Creates a sandbox via `createos sandbox create` +3. All Pi tools (bash, read, write, edit, ls, find, grep) run inside the sandbox via `createos sandbox exec` / `push` / `pull` +4. On exit, ephemeral sessions destroy the sandbox; persisted sessions keep it for resume + +## Architecture + +``` +Your machine CreateOS +โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” โ”Œโ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ” +โ”‚ Pi agent (LLM) โ”‚ โ”‚ Sandbox โ”‚ +โ”‚ โ”‚ โ”€โ”€ createos โ”€โ”€โ–ถ โ”‚ code execution โ”‚ +โ”‚ tool calls โ”‚ CLI โ”‚ file I/O โ”‚ +โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ โ””โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”€โ”˜ + โ”‚ + โ”‚ private network + โ–ผ + Other sandboxes + (same network) +``` + +Zero HTTP client code. Every operation is a `createos` CLI call. diff --git a/packages/pi-extension/index.ts b/packages/pi-extension/index.ts new file mode 100644 index 0000000..8e72022 --- /dev/null +++ b/packages/pi-extension/index.ts @@ -0,0 +1,389 @@ +/** + * pi-createos โ€” run Pi's tools inside a remote, ephemeral CreateOS Sandbox. + * + * CLI-only: every operation goes through `createos` CLI. No HTTP client, + * no API key env vars โ€” just `createos login` and go. + */ + +import type { ExtensionAPI, ExtensionContext } from "@earendil-works/pi-coding-agent"; +import * as cli from "./src/cli.ts"; +import { registerTools } from "./src/tools.ts"; +import { sandboxExec, cleanupTempKey, autoInstallCLI } from "./src/cli.ts"; +import { shortId } from "./src/util.ts"; + +const SESSION_ENTRY = "createos-session"; + +interface SessionEntryData { + sandboxId: string; + cwd: string; +} +interface ActiveSandbox { + sandboxId: string; + cwd: string; +} + +/** The user's local working directory (where they launched Pi from). */ +const hostCwd = process.cwd(); + +export default function (pi: ExtensionAPI) { + pi.registerFlag("createos", { + description: "Run tools inside a CreateOS sandbox", + type: "boolean", + }); + pi.registerFlag("shape", { description: "Sandbox shape (default: s-2vcpu-2gb)", type: "string" }); + pi.registerFlag("rootfs", { description: "CreateOS rootfs or template to use", type: "string" }); + pi.registerFlag("network", { + description: "Network(s) to join (comma-separated)", + type: "string", + }); + + let active: ActiveSandbox | null = null; + + registerTools(pi, () => (active ? { sandboxId: active.sandboxId, cwd: active.cwd } : null)); + + // --- Commands --- + + pi.registerCommand("sandbox", { + description: "Show the active CreateOS sandbox's status", + handler: async (_args, ctx) => { + if (!active) { + ctx.ui.notify("No CreateOS sandbox is active. Launch Pi with --createos.", "warning"); + return; + } + try { + const info = await cli.getSandbox(pi, active.sandboxId); + const lines = [`โ˜ ${shortId(active.sandboxId)} ยท ${info.status}`, `cwd: ${active.cwd}`]; + if (info.ingress_url_template) lines.push(`ingress: ${info.ingress_url_template}`); + ctx.ui.notify(lines.join("\n"), "info"); + } catch (err) { + ctx.ui.notify(`Failed: ${errorMessage(err)}`, "error"); + } + }, + }); + + pi.registerCommand("network", { + description: "Manage CreateOS networks: create, ls, show, rm, attach, detach", + getArgumentCompletions() { + return [ + { value: "create", label: "Create a new network" }, + { value: "ls", label: "List your networks" }, + { value: "show", label: "Show network details" }, + { value: "rm", label: "Delete a network" }, + { value: "attach", label: "Attach this sandbox to a network" }, + { value: "detach", label: "Detach this sandbox from a network" }, + ]; + }, + handler: async (args, ctx) => { + if (!active) { + ctx.ui.notify("No sandbox active. Launch with --createos.", "warning"); + return; + } + const parts = (args ?? "").trim().split(/\s+/); + const sub = parts[0], + arg = parts.slice(1).join(" ").trim(); + try { + switch (sub) { + case "create": + if (!arg) { + ctx.ui.notify("Usage: /network create ", "warning"); + return; + } + const net = await cli.createNetwork(pi, arg); + ctx.ui.notify(`Network created: ${net.name} (${net.id})`, "info"); + break; + case "ls": + case "list": + const nets = await cli.listNetworks(pi); + if (!nets.length) { + ctx.ui.notify("No networks. Create with /network create ", "info"); + return; + } + ctx.ui.notify( + `Networks:\n${nets.map((n) => ` ${n.name} (${n.id}) ยท ${n.member_count ?? 0} members`).join("\n")}`, + "info", + ); + break; + case "show": + if (!arg) { + ctx.ui.notify("Usage: /network show ", "warning"); + return; + } + const detail = await cli.getNetwork(pi, arg); + const lines = [`Network: ${detail.name} (${detail.id})`]; + if (detail.members?.length) { + lines.push("Members:"); + for (const m of detail.members) + lines.push( + ` ${m.sandbox_id} ยท ${m.status} ยท ${m.ip}${m.name ? ` ยท ${m.name}` : ""}`, + ); + } else lines.push("No members"); + ctx.ui.notify(lines.join("\n"), "info"); + break; + case "rm": + case "delete": + if (!arg) { + ctx.ui.notify("Usage: /network rm ", "warning"); + return; + } + if (!(await ctx.ui.confirm("Delete network", `Delete "${arg}"?`))) return; + await cli.deleteNetwork(pi, arg); + ctx.ui.notify(`Network "${arg}" deleted`, "info"); + break; + case "attach": + if (!arg) { + ctx.ui.notify("Usage: /network attach ", "warning"); + return; + } + await cli.attachNetwork(pi, active.sandboxId, arg); + ctx.ui.notify(`Attached sandbox to network "${arg}"`, "info"); + break; + case "detach": + if (!arg) { + ctx.ui.notify("Usage: /network detach ", "warning"); + return; + } + await cli.detachNetwork(pi, active.sandboxId, arg); + ctx.ui.notify(`Detached sandbox from network "${arg}"`, "info"); + break; + default: + ctx.ui.notify("/network [args]", "info"); + } + } catch (err) { + ctx.ui.notify(`Network error: ${errorMessage(err)}`, "error"); + } + }, + }); + + pi.registerCommand("device", { + description: "Show device status and attach/detach to networks", + getArgumentCompletions() { + return [ + { value: "status", label: "Show registered devices" }, + { value: "attach", label: "Attach device to a network" }, + { value: "detach", label: "Detach device from a network" }, + ]; + }, + handler: async (args, ctx) => { + const parts = (args ?? "").trim().split(/\s+/); + const sub = parts[0] || "status", + arg = parts.slice(1).join(" ").trim(); + try { + switch (sub) { + case "status": { + const devs = await cli.listDevices(pi); + if (!devs.length) { + ctx.ui.notify( + "No device registered.\n\n createos sb devices register # one-time\n createos sb vpn up # connect", + "info", + ); + return; + } + const lines = devs.map( + (d) => ` ${d.name} ยท ${d.id ?? d.device_id} ยท IP: ${d.client_ip ?? "n/a"}`, + ); + ctx.ui.notify( + `Devices:\n${lines.join("\n")}\n\nRun \`createos sb vpn up\` to connect.`, + "info", + ); + break; + } + case "attach": { + if (!arg) { + ctx.ui.notify("Usage: /device attach ", "warning"); + return; + } + const devs = await cli.listDevices(pi); + if (!devs.length) { + ctx.ui.notify("No device. Run: createos sb devices register", "warning"); + return; + } + await cli.attachDeviceToNetwork(pi, devs[0].id ?? devs[0].device_id!, arg); + ctx.ui.notify( + `Device attached to "${arg}". Run \`createos sb vpn up\` to connect.`, + "info", + ); + break; + } + case "detach": { + if (!arg) { + ctx.ui.notify("Usage: /device detach ", "warning"); + return; + } + const devs = await cli.listDevices(pi); + if (!devs.length) { + ctx.ui.notify("No device registered.", "warning"); + return; + } + await cli.detachDeviceFromNetwork(pi, devs[0].id ?? devs[0].device_id!, arg); + ctx.ui.notify(`Device detached from "${arg}".`, "info"); + break; + } + default: + ctx.ui.notify("/device [network]", "info"); + } + } catch (err) { + ctx.ui.notify(`Device error: ${errorMessage(err)}`, "error"); + } + }, + }); + + // --- Lifecycle --- + + pi.on("session_start", async (event, ctx) => { + if (pi.getFlag("createos") !== true) return; + if (active) return; + + if (!(await cli.isCreateOSInstalled(pi))) { + setStatus(ctx, "โ˜ createos ยท installing CLIโ€ฆ"); + ctx.ui.notify("CreateOS CLI not found โ€” installing automaticallyโ€ฆ", "info"); + if (!(await autoInstallCLI(pi))) { + ctx.ui.notify( + "Failed to auto-install CreateOS CLI. Install manually: curl -sfL https://raw.githubusercontent.com/NodeOps-app/createos-cli/main/install.sh | sh", + "error", + ); + setStatus(ctx, undefined); + return; + } + ctx.ui.notify("CreateOS CLI installed successfully.", "info"); + } + if (!(await cli.isLoggedIn(pi))) { + ctx.ui.notify("Not logged in. Run: createos login", "error"); + return; + } + + const persisted = ctx.sessionManager.getSessionFile() !== undefined; + const sessionId = ctx.sessionManager.getSessionId(); + setStatus(ctx, "โ˜ createos ยท spinning up sandboxโ€ฆ"); + const startedAt = Date.now(); + let createdId: string | undefined; + + try { + // Reattach on resume/reload. + if (persisted && event.reason !== "fork") { + const prev = latestSessionEntry(ctx); + if (prev) { + try { + setStatus(ctx, "โ˜ createos ยท resuming sandboxโ€ฆ"); + const info = await cli.getSandbox(pi, prev.sandboxId); + if (info.status === "paused") { + await cli.resumeSandbox(pi, prev.sandboxId); + let retries = 30; + while (retries-- > 0) { + const check = await cli.getSandbox(pi, prev.sandboxId); + if (check.status === "running") break; + await new Promise((r) => setTimeout(r, 1000)); + } + } + active = { sandboxId: prev.sandboxId, cwd: prev.cwd }; + ctx.ui.notify(`Reattached ยท ${shortId(prev.sandboxId)}`, "info"); + setRunningStatus(ctx, prev.sandboxId, prev.cwd); + return; + } catch (err) { + if (!(err instanceof cli.CLIError && err.isNotFound)) throw err; + } + } + } + + const shape = stringFlag(pi.getFlag("shape")) ?? "s-2vcpu-2gb"; + const rootfs = stringFlag(pi.getFlag("rootfs")); + const networkFlag = stringFlag(pi.getFlag("network")); + const networks = networkFlag + ? networkFlag + .split(",") + .map((n) => n.trim()) + .filter(Boolean) + : undefined; + + const sandbox = await cli.createSandbox(pi, { + shape, + rootfs, + ingress: true, + networks, + name: `pi-${shortId(sessionId)}`, + }); + createdId = sandbox.id; + + // Create returns after the sandbox is running โ€” no polling needed. + const cwd = "/root/workspace"; + await sandboxExec(pi, sandbox.id, `mkdir -p ${cwd}`); + + active = { sandboxId: sandbox.id, cwd }; + if (persisted) + pi.appendEntry(SESSION_ENTRY, { sandboxId: sandbox.id, cwd } as SessionEntryData); + + const secs = ((Date.now() - startedAt) / 1000).toFixed(1); + ctx.ui.notify(`Sandbox ready ยท ${shortId(sandbox.id)} ยท ${secs}s`, "info"); + setRunningStatus(ctx, sandbox.id, cwd); + } catch (err) { + active = null; + if (createdId) + try { + await cli.destroySandbox(pi, createdId); + } catch {} + setStatus(ctx, undefined); + ctx.ui.notify(`CreateOS: failed โ€” ${errorMessage(err)}`, "error"); + } + }); + + pi.on("before_agent_start", (event) => { + if (!active) return; + const cwdLine = `Current working directory: ${active.cwd} (CreateOS sandbox ${shortId(active.sandboxId)})`; + let systemPrompt = event.systemPrompt.replace(/Current working directory: .*/g, cwdLine); + // Caveman-style: short, declarative, environment-aware. + // The model already sees tool descriptions/guidelines โ€” don't repeat them here. + systemPrompt += + "\n\n--- CreateOS Sandbox Environment ---" + + `\nSandbox: ${active.sandboxId}` + + `\nCwd: ${active.cwd}` + + `\nHost dir: ${hostCwd}` + + "\nAll tools run remotely in this sandbox. You know the sandbox ID and cwd โ€” never run pwd/hostname." + + "\n" + + "\nQuick rules:" + + `\n- "mount/sync this dir" โ†’ sandbox_sync local_dir="${hostCwd}" remote_dir="/root/project"` + + "\n- Port access โ†’ sandbox_preview_url (public URL) > sandbox_tunnel (localhost) > device VPN (last resort)" + + "\n- Multi-node โ†’ sandbox_network_create + sandbox_create with network + sandbox_exec on other sandboxes" + + "\n--- End CreateOS ---"; + return { systemPrompt }; + }); + + pi.on("session_shutdown", async (event, _ctx) => { + if (!active) return; + if (event.reason === "new" || event.reason === "resume" || event.reason === "fork") return; + const current = active; + active = null; + + // Clean up temp SSH key used by sync. + try { + await cleanupTempKey(pi); + } catch {} + + const persisted = _ctx.sessionManager.getSessionFile() !== undefined; + if (!persisted) { + try { + await cli.destroySandbox(pi, current.sandboxId); + } catch {} + } + }); +} + +function latestSessionEntry(ctx: ExtensionContext): SessionEntryData | undefined { + const entries = ctx.sessionManager.getEntries(); + for (let i = entries.length - 1; i >= 0; i--) { + const e = entries[i] as { type?: string; customType?: string; data?: unknown }; + if (e.type === "custom" && e.customType === SESSION_ENTRY) return e.data as SessionEntryData; + } + return undefined; +} + +function setStatus(ctx: ExtensionContext, text: string | undefined): void { + ctx.ui.setStatus("createos", text === undefined ? undefined : ctx.ui.theme.fg("accent", text)); +} +function setRunningStatus(ctx: ExtensionContext, id: string, cwd: string): void { + setStatus(ctx, `โ˜ createos ยท ${shortId(id)} ยท running ยท ${cwd}`); +} +function stringFlag(value: boolean | string | undefined): string | undefined { + return typeof value === "string" && value.length > 0 ? value : undefined; +} +function errorMessage(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} diff --git a/packages/pi-extension/package-lock.json b/packages/pi-extension/package-lock.json new file mode 100644 index 0000000..4d07f4a --- /dev/null +++ b/packages/pi-extension/package-lock.json @@ -0,0 +1,3292 @@ +{ + "name": "@createos/pi", + "version": "0.2.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@createos/pi", + "version": "0.2.0", + "license": "Apache-2.0", + "devDependencies": { + "@earendil-works/pi-ai": "^0.79.0", + "@earendil-works/pi-coding-agent": "^0.79.0", + "@types/node": "^22", + "typebox": "1.1.38", + "typescript": "^5.6.0" + }, + "peerDependencies": { + "@earendil-works/pi-ai": "^0.79.0", + "@earendil-works/pi-coding-agent": "^0.79.0", + "typebox": "^1.1.38" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "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/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-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-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "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.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/core": { + "version": "3.977.4", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.977.4.tgz", + "integrity": "sha512-CEkcQlMOQJCvul60U7wdAOACjtdgFWDsfJI+6wUOGdhGNV2lGbuJpi/R50QLpFG3Tp+sQxa/RmzC3X7KHbhuTA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@aws-sdk/xml-builder": "^3.972.37", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.31.1", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.65", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.65.tgz", + "integrity": "sha512-lJT2aRw9wCV8jPHyFJjdZLD4HTydL6/22AnCSOB8e/LqOc55nEJGLHkJQeSxhn8QiqyjFwPKQFtMw0ovjRUY/g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.67", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.67.tgz", + "integrity": "sha512-N7fw/15hSwI/CPxe5ohOyb7O4ge9f5me1gVIn8OIkBRB0squ8OJqQyDyH/HoL+Sb1W5xdC88jVC+bHkw73iu+Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.13.tgz", + "integrity": "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.10.tgz", + "integrity": "sha512-Zh9XRaPnDN9buO7GfWBubS22R6Nq5D6hbyYEMN05LiOnXugm/8WDjUx6y756bSPbdn3aJB2qG4zFW3bN82QhoQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/credential-provider-env": "^3.972.65", + "@aws-sdk/credential-provider-http": "^3.972.67", + "@aws-sdk/credential-provider-login": "^3.972.72", + "@aws-sdk/credential-provider-process": "^3.972.65", + "@aws-sdk/credential-provider-sso": "^3.973.9", + "@aws-sdk/credential-provider-web-identity": "^3.972.71", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.72", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.72.tgz", + "integrity": "sha512-zZapIKwaHp7TdTf9hbH1I3CVUdEupmt7FXO/BoTQGC+4h6NkXKWpqF2p5WyfpjurDLHCpSyh+BzMlAg8arqWLA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.76", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.76.tgz", + "integrity": "sha512-1yzLmRiYSgGC25v7ZZEwJn/auhHHTIHgFOmzL2f36hf1+7jSLcX+1QrAz4760WEzPiiQl8xmlpFhHfl2OoyVzA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.65", + "@aws-sdk/credential-provider-http": "^3.972.67", + "@aws-sdk/credential-provider-ini": "^3.973.10", + "@aws-sdk/credential-provider-process": "^3.972.65", + "@aws-sdk/credential-provider-sso": "^3.973.9", + "@aws-sdk/credential-provider-web-identity": "^3.972.71", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.65", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.65.tgz", + "integrity": "sha512-e5DbbNteOSalN58U83G6kFa4ECLEuGbGqNBHIXE7zYXA/m4GHblIGjFbSH7wYv6gBV8iNSDcRZBKfQZF5vF9nw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.9.tgz", + "integrity": "sha512-0V0u4t+KBku9fbh5CPCaC5hUWwSzDafp8nCuDy817zWbp2gz80jO44rMQkiwnZ+k54B+tjAtzRy00DJRGTKGBg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/token-providers": "3.1100.0", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1100.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1100.0.tgz", + "integrity": "sha512-THf3MkgY3fNJZ3zdgSenLqR7gSE68KccCj1RCKretlG73Ppszvues02VpCUO9NlB/tZDC483FvGCld+AiPCkvg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.71.tgz", + "integrity": "sha512-e4dwiRltGAaQ+2yxw57Hj0l/BF3BHiG14+QpYE7bGYBlpAq/fkIri2BDhjWon8c0mhhtd2txQBAkQb9BcTStFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/nested-clients": "^3.997.39", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.31", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.31.tgz", + "integrity": "sha512-/BRzvkp46mF6eXBL/l9WKPQQfifLlUPaWli6n9/T/WDLUg8he7TCyuNFnk6RvHP5j5W/kMj5Gxw7W778LJaXDA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.26", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.26.tgz", + "integrity": "sha512-2eIvouTZoxPu5ClHY6ij13De1yhY8Rmllt0dlGeBNXX3wmR7fU1pvMCGb50fKm1GxcuntWK0t1T0cMjTyDoUQA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.47", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.47.tgz", + "integrity": "sha512-UcPdY05u3TzvDah86NG6B9FgePYU6bXO7CRQIzQrieFL5xBBGajM7g1lCngmP4WA8EPed/kgT5CvM28cqSlBbA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.39.tgz", + "integrity": "sha512-wU5NPnj62Sb7A8xn/Zb+xThe05P3otNtDl37iOIi5DDMeCesNeCckaG+eXWGUs12Z9R34I8CD05TaTe6SIa61g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.977.4", + "@aws-sdk/signature-v4-multi-region": "^3.996.43", + "@aws-sdk/types": "^3.974.2", + "@smithy/core": "^3.31.1", + "@smithy/fetch-http-handler": "^5.6.13", + "@smithy/node-http-handler": "^4.9.13", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.9.13", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.13.tgz", + "integrity": "sha512-Nmd/Nl35zfYrd+a6OO2cDJb3GPh9bgTjIUhcM+JFfjpp8/osCgboDV5nCT1I01Pv6R13eSKDKLSoVa5ZB6Zsfw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.43", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.43.tgz", + "integrity": "sha512-lKekx8bLBXSv4O+cslk9Zfnw2XKSkWBs3uWL5QGhH2ZAQfNS7FE0vcSSN2vD/AhxX54ZTywWxR4STThoeOXlBA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.2", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.2", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.2.tgz", + "integrity": "sha512-3W6IUtSxFbH6X7Wb7DzGCV5QiFQsd0g8bOfntpmDxQlzBoKWUMBu/JPQR0DwkE+Hpnxd6db1tXbOwdeHddG6cA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.8.tgz", + "integrity": "sha512-uUbMs1cBZPafD0ohUj6EwNf0fPZ534NvBxHox4hjX+0Rxq5paSYUem7+hi833pYrzrcnBATKIYpR02MDXT5M9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.37.tgz", + "integrity": "sha512-zKq4HQum8JwDyEuyfuI4bbiAcU0KxP6qy+9PR/IsR92IyE/DaBAikzAS50tjxip4bqIIANpCcG+Yyj6CVhXupg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws/lambda-invoke-store": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.3.0.tgz", + "integrity": "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.7.tgz", + "integrity": "sha512-Nq8OhGWiZIZGV6hLHoyAKLLcJihP/xFeBMGJoUrxTX2psI8dCifzLhZISFb+VWS3wFMRDmCGw5R+dOySCqPLhw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-ai": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", + "integrity": "sha512-9jR23tOl0BIUdQMn70Gr72xYBpM7Xgl9Lyv7gAnU1USfkNRuYG/f/edLl+n/Dp/RafDW3JI4DF7y/GhgkORuew==", + "dev": true, + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.79.10.tgz", + "integrity": "sha512-YxaRhmgyDTvLDdGVbe7YzTHV80oL5mX5odg6EhGHz3w5Wu1Ix8DCw7bhtiOBLGQNFRcknia0zPmVWIj30XP1EA==", + "dev": true, + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-agent-core": "^0.79.10", + "@earendil-works/pi-ai": "^0.79.10", + "@earendil-works/pi-tui": "^0.79.10", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "glob": "13.0.6", + "highlight.js": "10.7.3", + "hosted-git-info": "9.0.3", + "ignore": "7.0.5", + "jiti": "2.7.0", + "minimatch": "10.2.5", + "proper-lockfile": "4.1.2", + "semver": "7.8.0", + "typebox": "1.1.38", + "undici": "8.5.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + }, + "optionalDependencies": { + "@mariozechner/clipboard": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { + "version": "0.91.1", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", + "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", + "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/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", + "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", + "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/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", + "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", + "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/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", + "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", + "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/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", + "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", + "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", + "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/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", + "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", + "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.974.11", + "@aws-sdk/credential-provider-node": "^3.972.42", + "@aws-sdk/eventstream-handler-node": "^3.972.16", + "@aws-sdk/middleware-eventstream": "^3.972.12", + "@aws-sdk/middleware-websocket": "^3.972.19", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { + "version": "3.974.11", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", + "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@aws-sdk/xml-builder": "^3.972.24", + "@aws/lambda-invoke-store": "^0.2.2", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", + "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.39", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", + "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", + "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-login": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", + "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.42", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", + "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.37", + "@aws-sdk/credential-provider-http": "^3.972.39", + "@aws-sdk/credential-provider-ini": "^3.972.41", + "@aws-sdk/credential-provider-process": "^3.972.37", + "@aws-sdk/credential-provider-sso": "^3.972.41", + "@aws-sdk/credential-provider-web-identity": "^3.972.41", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/credential-provider-imds": "^4.3.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.37", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", + "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", + "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/token-providers": "3.1048.0", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.41", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", + "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", + "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.12", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", + "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.19", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", + "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { + "version": "3.997.9", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", + "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", + "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.974.11", + "@aws-sdk/signature-v4-multi-region": "^3.996.27", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/fetch-http-handler": "^5.4.2", + "@smithy/node-http-handler": "^4.7.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.27", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", + "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/signature-v4": "^5.4.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { + "version": "3.973.8", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", + "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", + "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { + "version": "3.972.24", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", + "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@nodable/entities": "2.1.0", + "@smithy/types": "^4.14.1", + "fast-xml-parser": "5.7.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", + "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { + "version": "7.29.2", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", + "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.79.10.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "@earendil-works/pi-ai": "^0.79.10", + "ignore": "7.0.5", + "typebox": "1.1.38", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.79.10.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.91.1", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@google/genai": "1.52.0", + "@mistralai/mistralai": "2.2.6", + "@opentelemetry/api": "1.9.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.26.0", + "partial-json": "0.1.7", + "typebox": "1.1.38" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.79.10", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.79.10.tgz", + "dev": true, + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "dev": true, + "hasInstallScript": 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/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard/-/clipboard-0.3.9.tgz", + "integrity": "sha512-ABnA53mdfkGZwOFUdZNv2S0CWGO/EIuPj8Vv9xmBFmSYg/qFc7ihO6q5FcQjvoE67kZpWkEc4AhD6B/os04yuA==", + "dev": true, + "license": "MIT", + "optional": true, + "engines": { + "node": ">= 10" + }, + "optionalDependencies": { + "@mariozechner/clipboard-darwin-arm64": "0.3.9", + "@mariozechner/clipboard-darwin-universal": "0.3.9", + "@mariozechner/clipboard-darwin-x64": "0.3.9", + "@mariozechner/clipboard-linux-arm64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-arm64-musl": "0.3.9", + "@mariozechner/clipboard-linux-riscv64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-gnu": "0.3.9", + "@mariozechner/clipboard-linux-x64-musl": "0.3.9", + "@mariozechner/clipboard-win32-arm64-msvc": "0.3.9", + "@mariozechner/clipboard-win32-x64-msvc": "0.3.9" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-arm64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-arm64/-/clipboard-darwin-arm64-0.3.9.tgz", + "integrity": "sha512-BfgV7vCEWZwJwZJw03r6bP5+tf0iI/ANuQYCxi9RNn7FrWB3yzGuMKCrNLRl6V761vXRdL8+OqZ0wd4TqlsNOQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-universal": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-universal/-/clipboard-darwin-universal-0.3.9.tgz", + "integrity": "sha512-BGGR4iA9Z2shAjI65eI5xtyb3LYNlDW9X3gxKxDbqtbnREohsrqznov6zpKoIrsRWpzlYVEdKphS7ksJ0/ndSQ==", + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-darwin-x64": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-darwin-x64/-/clipboard-darwin-x64-0.3.9.tgz", + "integrity": "sha512-4kURmCbS6nt8uYhtmWpUcJWyPHfmAr5dTpXD1nO3pIfa+TSQ9DbrGOYCKH+aEFW47XhQ4Vp8ZTszie+wfFvDKg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-gnu/-/clipboard-linux-arm64-gnu-0.3.9.tgz", + "integrity": "sha512-g59OkUGP2DDfCOIKypHeYgv2M55u/cKvXa5dSxFbEJ34XvIQMdcVmpKCkGUro3ZgefXiGVdwguvTMQGpHWzIXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-arm64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-arm64-musl/-/clipboard-linux-arm64-musl-0.3.9.tgz", + "integrity": "sha512-AGuJdgKsmJdm4Pych7kv3sqe591ERRaAHW3xjLooiFzn8J+PxUyof++7YZrB5Y5tpnTO+K18Og3taj2NpluCRQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-riscv64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-riscv64-gnu/-/clipboard-linux-riscv64-gnu-0.3.9.tgz", + "integrity": "sha512-DXBEAiuMpk7dhS1a9NzNxVAFi1vaKoPu7rQNgY8LIDLGrK3lnIp3nT10DUum+PKVJoJppIP+NAA8IZe4DMNDPw==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-gnu": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-gnu/-/clipboard-linux-x64-gnu-0.3.9.tgz", + "integrity": "sha512-WORrMLd6EpElEME7JRKfSaY34nW1P5LbdgK5YNCS1ncG2LqmITsSMEJ8nh2mpvxb3TxqbOOKgY7k9eMJYlW9Mw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-linux-x64-musl": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-linux-x64-musl/-/clipboard-linux-x64-musl-0.3.9.tgz", + "integrity": "sha512-/DHn+1DrfL6oRaPPWXaOKvonFFrni666fxd+zFqiQEfvBH0tsHVWjq9iqBk0oDp0qaPA72lIMy5BptxISBEhZQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-arm64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-arm64-msvc/-/clipboard-win32-arm64-msvc-0.3.9.tgz", + "integrity": "sha512-O5FHD3ErkMwMhNzAfu3ggy0ug4z7btZuoQgwwxlzPrwV2bxlD6WDpqBY4NCgICAgZdDKdp+loUEKVAVt8aYnhQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mariozechner/clipboard-win32-x64-msvc": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/@mariozechner/clipboard-win32-x64-msvc/-/clipboard-win32-x64-msvc-0.3.9.tgz", + "integrity": "sha512-ihQC3EufqEY81vhXBgVBtK4prL+wc62zJsSvxrgz7K1hsdt6OObz6v9p3Rn1OG3GJksTTKMJF0u/guMISHPhSA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@nodable/entities": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@nodable/entities/-/entities-2.1.0.tgz", + "integrity": "sha512-nyT7T3nbMyBI/lvr6L5TyWbFJAI9FTgVRakNoBqCD+PmID8DzFrrNdLLtHMwMszOtqZa8PAOV24ZqDnQrhQINA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/nodable" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@opentelemetry/semantic-conventions": { + "version": "1.41.1", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.41.1.tgz", + "integrity": "sha512-/UhIkaZgPutTFmQ7RnIJGgDXZmtEJ7Dvi86xNTFWcnRxVRNk/aotsqDJYeEvDP+FSMB2SdW+pQzNMcWP0rwuNA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/base64": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", + "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/codegen": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/path": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", + "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/pool": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", + "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@protobufjs/utf8": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.1.tgz", + "integrity": "sha512-oOAWABowe8EAbMyWKM0tYDKi8Yaox52D+HWZhAIJqQXbqe0xI/GV7FhLWqlEKreMkfDjshR5FKgi3mnle0h6Eg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@silvia-odwyer/photon-node": { + "version": "0.3.4", + "resolved": "https://registry.npmjs.org/@silvia-odwyer/photon-node/-/photon-node-0.3.4.tgz", + "integrity": "sha512-bnly4BKB3KDTFxrUIcgCLbaeVVS8lrAkri1pEzskpmxu9MdfGQTy8b8EgcD83ywD3RPMsIulY8xJH5Awa+t9fA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/core": { + "version": "3.24.3", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.24.3.tgz", + "integrity": "sha512-Ep/7tPamGY8mgESE3LyLKtxJyy6U52WWAqr/3wial47Sj4u3PiIF73AOGI27UyLy9duTkhZbgzodOfLV4TduZg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@aws-crypto/crc32": "5.2.0", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/credential-provider-imds": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.3.3.tgz", + "integrity": "sha512-I2Bti0DKFo2IJyN28ijCsx51BAumEYR4/1yZ1FXyBygy9MqbnMqCev4JPth/MbpRfBSRAX35hITSnAdJRo1u5w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/fetch-http-handler": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.4.3.tgz", + "integrity": "sha512-F+DRf8IJazRJgYog2A/yJK7eYVc0rqTlRzO+5ZxjJd4WkZoKz0IJRncf7G6t1pdVT3kryJcwuTFhN1c5m6N47A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/signature-v4": { + "version": "5.4.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.4.3.tgz", + "integrity": "sha512-53+75QuPl6DL+ct6vVEB51FDO5oulXr20TPV46VvJZg76lIlXNWfxi8j+G2V/t0I2qxCBOa3vX/8bmjrpFVo9g==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/types": { + "version": "4.14.2", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.14.2.tgz", + "integrity": "sha512-P+otAxbV4CqBybp7EkcJCrig63yE2E7PuNVOmilVMRcx/O+QDzGULTrKsq4DV13gSfak9ObPrWaHl/9bL5YcWw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "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/@earendil-works/pi-coding-agent/node_modules/@smithy/util-utf8": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", + "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", + "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/@earendil-works/pi-coding-agent/node_modules/@types/node": { + "version": "22.19.19", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.19.tgz", + "integrity": "sha512-dyh/xO2Fh5bYrfWaaqGrRQQGkNdmYw6AmaAUvYeUMNTWQtvb796ikLdmTchRmOlOiIJ1TDXfWgVx1QkUlQ6Hew==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "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/@earendil-works/pi-coding-agent/node_modules/bignumber.js": { + "version": "9.3.1", + "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", + "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/bowser": { + "version": "2.14.1", + "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", + "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { + "version": "5.0.6", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.6.tgz", + "integrity": "sha512-kLpxurY4Z4r9sgMsyG0Z9uzsBlgiU/EFKhj/h91/8yHu0edo7XuixOIH3VcJ8kkxs6/jPzoI6U9Vj3WqbMQ94g==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/buffer-equal-constant-time": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", + "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/chalk": { + "version": "5.6.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", + "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", + "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/@earendil-works/pi-coding-agent/node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/data-uri-to-buffer": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", + "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/diff": { + "version": "8.0.4", + "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", + "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ecdsa-sig-formatter": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz", + "integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-builder": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/fast-xml-builder/-/fast-xml-builder-1.2.0.tgz", + "integrity": "sha512-00aAWieqff+ZJhsXA4g1g7M8k+7AYoMUUHF+/zFb5U6Uv/P0Vl4QZo84/IcufzYalLuEj9928bXN9PbbFzMF0Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "path-expression-matcher": "^1.5.0", + "xml-naming": "^0.1.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fast-xml-parser": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/fast-xml-parser/-/fast-xml-parser-5.7.3.tgz", + "integrity": "sha512-C0AaNuC+mscy6vrAQKAc/rMq+zAPHodfHGZu4sGVehvAQt/JLG1O5zEcYcXSY5zSqr4YVgxsB+pHXTq0i7eDlg==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "dependencies": { + "@nodable/entities": "^2.1.0", + "fast-xml-builder": "^1.1.7", + "path-expression-matcher": "^1.5.0", + "strnum": "^2.2.3" + }, + "bin": { + "fxparser": "src/cli/cli.js" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/fetch-blob": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", + "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", + "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/@earendil-works/pi-coding-agent/node_modules/formdata-polyfill": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", + "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", + "dev": true, + "license": "MIT", + "dependencies": { + "fetch-blob": "^3.1.2" + }, + "engines": { + "node": ">=12.20.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/gaxios": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.1.4.tgz", + "integrity": "sha512-bTIgTsM2bWn3XklZISBTQX7ZSddGW+IO3bMdGaemHZ3tbqExMENHLx6kKZ/KlejgrMtj8q7wBItt51yegqalrA==", + "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/@earendil-works/pi-coding-agent/node_modules/gcp-metadata": { + "version": "8.1.2", + "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", + "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", + "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/@earendil-works/pi-coding-agent/node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/glob": { + "version": "13.0.6", + "resolved": "https://registry.npmjs.org/glob/-/glob-13.0.6.tgz", + "integrity": "sha512-Wjlyrolmm8uDpm/ogGyXZXb1Z+Ca2B8NbJwqBVg0axK9GbBeoS7yGV6vjXnYdGm6X53iehEuxxbyiKp8QmN4Vw==", + "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/@earendil-works/pi-coding-agent/node_modules/google-auth-library": { + "version": "10.6.2", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.6.2.tgz", + "integrity": "sha512-e27Z6EThmVNNvtYASwQxose/G57rkRuaRbQyxM2bvYLLX/GqWZ5chWq2EBoUchJbCc57eC9ArzO5wMsEmWftCw==", + "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/@earendil-works/pi-coding-agent/node_modules/google-logging-utils": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", + "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/highlight.js": { + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz", + "integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": "*" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/hosted-git-info": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", + "integrity": "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg==", + "dev": true, + "license": "ISC", + "dependencies": { + "lru-cache": "^11.1.0" + }, + "engines": { + "node": "^20.17.0 || >=22.9.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/http-proxy-agent": { + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", + "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.0", + "debug": "^4.3.4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/https-proxy-agent": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", + "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "agent-base": "^7.1.2", + "debug": "4" + }, + "engines": { + "node": ">= 14" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ignore": { + "version": "7.0.5", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", + "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jiti": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", + "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", + "dev": true, + "license": "MIT", + "bin": { + "jiti": "lib/jiti-cli.mjs" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-bigint": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", + "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "bignumber.js": "^9.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/jwa": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", + "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", + "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/@earendil-works/pi-coding-agent/node_modules/jws": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", + "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", + "dev": true, + "license": "MIT", + "dependencies": { + "jwa": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/long": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", + "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/lru-cache": { + "version": "11.4.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.4.0.tgz", + "integrity": "sha512-W+R+kFL4HgVxONq2bhXPi3bGpzGe/yEhVOp233qw9wCRtgncJ15P3bC+e4zZMu4Cq7d+WAJjXGW0uUkifhcatA==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/marked": { + "version": "18.0.5", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.5.tgz", + "integrity": "sha512-S6GcvALHg6K4ohtu4E7x0a1AqhAjp6cV8KhLSyN9qVapnzJkusVBxZRcIU9AeYsbe6P1hKDusSbEOzGyyuce6w==", + "dev": true, + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/minimatch": { + "version": "10.2.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.5.tgz", + "integrity": "sha512-MULkVLfKGYDFYejP07QOurDLLQpcjk7Fw+7jXS2R2czRQzR56yHRveU5NDJEOviH+hETZKSkIk5c+T23GjFUMg==", + "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/@earendil-works/pi-coding-agent/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "license": "BlueOak-1.0.0", + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "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/@earendil-works/pi-coding-agent/node_modules/node-fetch": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", + "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", + "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/@earendil-works/pi-coding-agent/node_modules/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "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/@earendil-works/pi-coding-agent/node_modules/p-retry": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", + "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/retry": "0.12.0", + "retry": "^0.13.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/p-retry/node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/partial-json": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", + "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-expression-matcher": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/path-expression-matcher/-/path-expression-matcher-1.5.0.tgz", + "integrity": "sha512-cbrerZV+6rvdQrrD+iGMcZFEiiSrbv9Tfdkvnusy6y0x0GKBXREFg/Y65GhIfm0tnLntThhzCnfKwp1WRjeCyQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/path-scurry": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.2.tgz", + "integrity": "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg==", + "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/@earendil-works/pi-coding-agent/node_modules/proper-lockfile": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/proper-lockfile/-/proper-lockfile-4.1.2.tgz", + "integrity": "sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "retry": "^0.12.0", + "signal-exit": "^3.0.2" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/proper-lockfile/node_modules/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/protobufjs": { + "version": "7.6.4", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.4.tgz", + "integrity": "sha512-RJJPTTpvFfHcWLkIa2JFWK4XvtSzS0yEWDmunqHXli1h3JlkbcQZXDZdcWxv+JK3Xsl5/UFDPZ0iGm7DAengYw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "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/@earendil-works/pi-coding-agent/node_modules/semver": { + "version": "7.8.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.0.tgz", + "integrity": "sha512-AcM7dV/5ul4EekoQ29Agm5vri8JNqRyj39o0qpX6vDF2GZrtutZl5RwgD1XnZjiTAfncsJhMI48QQH3sN87YNA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/strnum": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/strnum/-/strnum-2.3.0.tgz", + "integrity": "sha512-ums3KNd42PGyx5xaoVTO1mjU1bH3NpY4vsrVlnv9PNGqQj8wd7rJ6nEypLrJ7z5vxK5RP0yMLo6J/Gsm62DI5Q==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.5.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.5.0.tgz", + "integrity": "sha512-xamtWoB1EshgjpmlXd7GGm2VfdDtw1+rD8uhry8pSNW3If6S8E0m2T2+orSKeZXEn/aPJMviCpDBA65WJt8zhg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/web-streams-polyfill": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", + "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/ws": { + "version": "8.21.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.0.tgz", + "integrity": "sha512-Vsp28b7DRcimFQvrqu2Wek3z1iYxDCWqHYB8Qsnk/S4RfaCQzPGPyBNuVjJV3cd6UiKtUtp6sNM77gWvzcCH+g==", + "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/@earendil-works/pi-coding-agent/node_modules/xml-naming": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/xml-naming/-/xml-naming-0.1.0.tgz", + "integrity": "sha512-k8KO9hrMyNk6tUWqUfkTEZbezRRpONVOzUTnc97VnCvyj6Tf9lyUR9EDAIeiVLv56jsMcoXEwjW8Kv5yPY52lw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/NaturalIntelligence" + } + ], + "license": "MIT", + "engines": { + "node": ">=16.0.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "dev": true, + "hasInstallScript": 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/@mistralai/mistralai": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", + "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@opentelemetry/semantic-conventions": "^1.40.0", + "ws": "^8.18.0", + "zod": "^3.25.0 || ^4.0.0", + "zod-to-json-schema": "^3.25.0" + }, + "peerDependencies": { + "@opentelemetry/api": "^1.9.0" + }, + "peerDependenciesMeta": { + "@opentelemetry/api": { + "optional": true + } + } + }, + "node_modules/@opentelemetry/api": { + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", + "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/@opentelemetry/semantic-conventions": { + "version": "1.43.0", + "resolved": "https://registry.npmjs.org/@opentelemetry/semantic-conventions/-/semantic-conventions-1.43.0.tgz", + "integrity": "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=14" + } + }, + "node_modules/@protobufjs/aspromise": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@protobufjs/aspromise/-/aspromise-1.1.2.tgz", + "integrity": "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ==", + "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.5", + "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", + "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/eventemitter": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", + "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@protobufjs/fetch": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", + "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", + "dev": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.1" + } + }, + "node_modules/@protobufjs/float": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", + "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", + "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.2", + "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", + "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@smithy/core": { + "version": "3.31.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.31.1.tgz", + "integrity": "sha512-CyogUINxvi7C7LDsh8Syo6hVJOT9ckz4rG8dRZfTJ8r91HkMY59PnNooaj7WcHyxEkxPfBAmbgztZU+xTo76lg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.4.16", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.16.tgz", + "integrity": "sha512-QfuLWAkLzptffFW980AFeHZFdqds2B64rpEd3uJ6lgs3xVn9QegGMUgUcj+4d7dRrAsya3r58ZKpku97WcFb4w==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.6.13", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.13.tgz", + "integrity": "sha512-4fW86pEUOMbrD5nkbyl/tTvPHHWJFbuB2odl6ps9lWfHoXf9HWh3Q/Smh59qH1g7+c/BSZghX6bbUk4gsiMs8A==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.6.12", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.12.tgz", + "integrity": "sha512-I6KLtq3H0qqSuV9vLglfi8puHqzygzWHOnI4z/Rdoo+q50vvo18vBRdPAvvEtcaKROz7Zn6qnPa14kRfPH6PcQ==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.31.1", + "@smithy/types": "^4.16.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.16.1", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.16.1.tgz", + "integrity": "sha512-0JFs3V2y2M9tKW5na/qxe69Zv+uxLMO7QBbhxF/FHu/Gp2NFZAAL9tWl9PU02xxo07pb3G9FTyjNc6D5uZrJIg==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/util-buffer-from": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", + "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", + "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/@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/@types/node": { + "version": "22.20.1", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.20.1.tgz", + "integrity": "sha512-EANqOCF9QFyra+4pfxUcX9STKJpCLjMbObVzljIJomAWSnuSIEAvyzEU53GaajbXJEgdh0iEcPL+DGvpUd4k1Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@types/retry": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", + "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", + "dev": true, + "license": "MIT" + }, + "node_modules/agent-base": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", + "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14" + } + }, + "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/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/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/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/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/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/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/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/gaxios": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.0.tgz", + "integrity": "sha512-RB5vLV+vvQeoFPCX4QMK6/hjVkbIamPp1QSUD0CiZcnj12qbpiL+pLbYtgD+oZkWl0tl9z+o2Utp+MpM3QRhBA==", + "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/google-auth-library": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "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/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/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/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/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/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-domexception": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", + "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", + "deprecated": "Use your platform's native DOMException instead", + "dev": true, + "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/openai": { + "version": "6.26.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", + "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", + "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/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/protobufjs": { + "version": "7.6.5", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", + "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", + "dev": true, + "hasInstallScript": true, + "license": "BSD-3-Clause", + "dependencies": { + "@protobufjs/aspromise": "^1.1.2", + "@protobufjs/base64": "^1.1.2", + "@protobufjs/codegen": "^2.0.5", + "@protobufjs/eventemitter": "^1.1.1", + "@protobufjs/fetch": "^1.1.1", + "@protobufjs/float": "^1.0.2", + "@protobufjs/path": "^1.1.2", + "@protobufjs/pool": "^1.1.0", + "@protobufjs/utf8": "^1.1.1", + "@types/node": ">=13.7.0", + "long": "^5.3.2" + }, + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/retry": { + "version": "0.13.1", + "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", + "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", + "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/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/typebox": { + "version": "1.1.38", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", + "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", + "dev": true, + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "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/ws": { + "version": "8.21.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", + "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", + "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/zod": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", + "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + }, + "node_modules/zod-to-json-schema": { + "version": "3.25.2", + "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", + "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", + "dev": true, + "license": "ISC", + "peerDependencies": { + "zod": "^3.25.28 || ^4" + } + } + } +} diff --git a/packages/pi-extension/package.json b/packages/pi-extension/package.json new file mode 100644 index 0000000..f0ca810 --- /dev/null +++ b/packages/pi-extension/package.json @@ -0,0 +1,43 @@ +{ + "name": "@createos/pi", + "version": "0.2.0", + "description": "Pi coding agent extension that runs all tool calls inside a remote CreateOS Sandbox while the agent runs locally", + "keywords": [ + "createos", + "pi-extension", + "pi-package", + "sandbox" + ], + "license": "Apache-2.0", + "author": "CreateOS", + "repository": { + "type": "git", + "url": "git+https://github.com/NodeOps-app/pi-extension.git" + }, + "files": [ + "index.ts", + "src", + "README.md" + ], + "type": "module", + "scripts": { + "typecheck": "tsc --noEmit" + }, + "devDependencies": { + "@earendil-works/pi-ai": "^0.79.0", + "@earendil-works/pi-coding-agent": "^0.79.0", + "@types/node": "^22", + "typebox": "1.1.38", + "typescript": "^5.6.0" + }, + "peerDependencies": { + "@earendil-works/pi-ai": "^0.79.0", + "@earendil-works/pi-coding-agent": "^0.79.0", + "typebox": "^1.1.38" + }, + "pi": { + "extensions": [ + "./index.ts" + ] + } +} diff --git a/packages/pi-extension/src/cli.ts b/packages/pi-extension/src/cli.ts new file mode 100644 index 0000000..901d230 --- /dev/null +++ b/packages/pi-extension/src/cli.ts @@ -0,0 +1,509 @@ +/** + * Thin wrapper around `createos` CLI commands via `pi.exec()`. + * + * Pi's exec may allocate a PTY, so the CLI can think it's interactive. + * We pass `-o json` explicitly on every command that returns parseable data. + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; + +export interface ExecResult { + code: number; + stdout: string; + stderr: string; +} + +async function run(pi: ExtensionAPI, args: string[]): Promise { + const res = await pi.exec("createos", args); + return { code: res.code, stdout: res.stdout ?? "", stderr: res.stderr ?? "" }; +} + +function parseJSON(stdout: string): T { + return JSON.parse(stdout) as T; +} + +// --- Sandbox lifecycle --- + +export interface SandboxInfo { + id: string; + status: string; + name?: string; + ip?: string; + host_id?: string; + region?: string; + ingress_url_template?: string; + [key: string]: unknown; +} + +export async function createSandbox( + pi: ExtensionAPI, + opts: { + shape?: string; + rootfs?: string; + ingress?: boolean; + networks?: string[]; + name?: string; + }, +): Promise { + const args = ["-o", "json", "sandbox", "create", "--shape", opts.shape ?? "s-2vcpu-2gb"]; + if (opts.rootfs) args.push("--rootfs", opts.rootfs); + if (opts.ingress) args.push("--ingress"); + if (opts.name) args.push("--name", opts.name); + if (opts.networks) { + for (const net of opts.networks) args.push("--network", net); + } + const res = await run(pi, args); + if (res.code !== 0) throw new CLIError("sandbox create", res); + return parseJSON(res.stdout); +} + +export async function getSandbox(pi: ExtensionAPI, id: string): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "get", id]); + if (res.code !== 0) throw new CLIError("sandbox get", res); + return parseJSON(res.stdout); +} + +export async function destroySandbox(pi: ExtensionAPI, id: string): Promise { + const res = await run(pi, ["sandbox", "rm", id, "--yes"]); + if (res.code !== 0) throw new CLIError("sandbox rm", res); +} + +export async function pauseSandbox(pi: ExtensionAPI, id: string): Promise { + const res = await run(pi, ["sandbox", "pause", id]); + if (res.code !== 0) throw new CLIError("sandbox pause", res); +} + +export async function resumeSandbox(pi: ExtensionAPI, id: string): Promise { + const res = await run(pi, ["sandbox", "resume", id]); + if (res.code !== 0) throw new CLIError("sandbox resume", res); +} + +export async function listSandboxes(pi: ExtensionAPI): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "list"]); + if (res.code !== 0) throw new CLIError("sandbox list", res); + const data = parseJSON(res.stdout); + return Array.isArray(data) ? data : data.data; +} + +export async function forkSandbox( + pi: ExtensionAPI, + id: string, + opts?: { paused?: boolean }, +): Promise { + const args = ["-o", "json", "sandbox", "fork", id]; + if (opts?.paused) args.push("--paused"); + const res = await run(pi, args); + if (res.code !== 0) throw new CLIError("sandbox fork", res); + return parseJSON(res.stdout); +} + +export async function editSandbox( + pi: ExtensionAPI, + id: string, + opts: { ingress?: boolean; egress?: string[] }, +): Promise { + const args = ["sandbox", "edit", id]; + if (opts.ingress === true) args.push("--ingress", "on"); + if (opts.ingress === false) args.push("--ingress", "off"); + if (opts.egress) { + for (const rule of opts.egress) args.push("--egress", rule); + } + const res = await run(pi, args); + if (res.code !== 0) throw new CLIError("sandbox edit", res); +} + +export async function listShapes(pi: ExtensionAPI): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "shapes"]); + if (res.code !== 0) throw new CLIError("sandbox shapes", res); + const data = parseJSON(res.stdout); + return Array.isArray(data) ? data : data.data; +} + +export async function listRootfs(pi: ExtensionAPI): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "rootfs"]); + if (res.code !== 0) throw new CLIError("sandbox rootfs", res); + return parseJSON(res.stdout); +} + +export async function getBandwidth( + pi: ExtensionAPI, + id: string, +): Promise<{ quota_bytes: number; used_bytes: number; remaining_bytes: number; capped: boolean }> { + // bandwidth is part of sandbox get response + const info = await getSandbox(pi, id); + return ( + (info as any).bandwidth ?? { quota_bytes: 0, used_bytes: 0, remaining_bytes: 0, capped: false } + ); +} + +// --- Tunnel --- + +export async function startTunnel( + pi: ExtensionAPI, + sandboxId: string, + remotePort: number, + localPort?: number, +): Promise<{ localPort: number; pid: string }> { + const local = localPort ?? remotePort; + // Run tunnel in background โ€” it's a long-lived process. + // We start it detached via shell so pi.exec returns immediately. + const args = [ + "sandbox", + "tunnel", + "--remote", + String(remotePort), + "--local", + String(local), + sandboxId, + ]; + // Fire and forget โ€” the tunnel runs until the sandbox is destroyed or session ends. + // We use pi.exec with a very short timeout just to verify it started, but the real + // process is spawned via nohup in the background. + const check = await run(pi, ["sandbox", "get", sandboxId]); + if (check.code !== 0) throw new CLIError("tunnel preflight", check); + + // Spawn the tunnel as a detached background process on the host. + const shellCmd = `nohup createos ${args.join(" ")} > /dev/null 2>&1 & echo $!`; + const res = await pi.exec("sh", ["-c", shellCmd]); + const pid = res.stdout?.trim() ?? ""; + return { localPort: local, pid }; +} + +// --- Sync --- + +let tempKeyPath: string | undefined; + +/** Generate a temporary unencrypted SSH key for sync. Reused across the session. */ +export async function ensureTempKey(pi: ExtensionAPI): Promise { + if (tempKeyPath) return tempKeyPath; + const res = await pi.exec("sh", [ + "-c", + 'dir=$(mktemp -d) && ssh-keygen -t ed25519 -f "$dir/id_sync" -N "" -q && echo "$dir/id_sync"', + ]); + if (res.code !== 0) throw new Error(`Failed to generate temp SSH key: ${res.stderr}`); + tempKeyPath = res.stdout!.trim(); + return tempKeyPath; +} + +/** Clean up the temp SSH key on shutdown. */ +export async function cleanupTempKey(pi: ExtensionAPI): Promise { + if (!tempKeyPath) return; + const dir = tempKeyPath.replace(/\/[^/]+$/, ""); + await pi.exec("rm", ["-rf", dir]); + tempKeyPath = undefined; +} + +export async function startSync( + pi: ExtensionAPI, + sandboxId: string, + localDir: string, + remoteDir: string, + opts?: { mode?: string; exclude?: string[] }, +): Promise<{ pid: string }> { + const check = await run(pi, ["sandbox", "get", sandboxId]); + if (check.code !== 0) throw new CLIError("sync preflight", check); + + const keyPath = await ensureTempKey(pi); + + const args = [ + "sandbox", + "sync", + "--local", + localDir, + "--remote", + remoteDir, + "--yes", + "-i", + keyPath, + ]; + if (opts?.mode) args.push("--mode", opts.mode); + if (opts?.exclude) { + for (const ex of opts.exclude) args.push("--exclude", ex); + } + args.push(sandboxId); + + const shellCmd = `nohup createos ${args.join(" ")} > /dev/null 2>&1 & echo $!`; + const res = await pi.exec("sh", ["-c", shellCmd]); + const pid = res.stdout?.trim() ?? ""; + return { pid }; +} + +// --- Exec --- + +export async function sandboxExec( + pi: ExtensionAPI, + id: string, + command: string, +): Promise<{ stdout: string; exitCode: number }> { + const res = await run(pi, ["sandbox", "exec", id, "--", "sh", "-c", command]); + // CLI preserves the inner command's exit code. + return { stdout: res.stdout, exitCode: res.code }; +} + +// --- Files --- + +export async function pullFile(pi: ExtensionAPI, id: string, remotePath: string): Promise { + const res = await run(pi, ["sandbox", "pull", id, remotePath, "-"]); + if (res.code !== 0) throw new CLIError("sandbox pull", res); + return res.stdout; +} + +export async function pushFile( + pi: ExtensionAPI, + id: string, + content: string, + remotePath: string, +): Promise { + // Write content to a temp file on host, then push it. + // Using exec with base64 is more portable than trying to pipe stdin. + const b64 = Buffer.from(content, "utf8").toString("base64"); + const res = await run(pi, [ + "sandbox", + "exec", + id, + "--", + "sh", + "-c", + `echo '${b64}' | base64 -d > '${remotePath.replace(/'/g, `'\\''`)}'`, + ]); + if (res.code !== 0) throw new CLIError("file write", res); +} + +// --- Networks --- + +export interface NetworkInfo { + id: string; + name: string; + member_count?: number; + members?: { sandbox_id: string; status: string; ip: string; name?: string }[]; + [key: string]: unknown; +} + +export async function createNetwork(pi: ExtensionAPI, name: string): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "network", "create", name]); + if (res.code !== 0) throw new CLIError("network create", res); + return parseJSON(res.stdout); +} + +export async function listNetworks(pi: ExtensionAPI): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "network", "ls"]); + if (res.code !== 0) throw new CLIError("network ls", res); + const data = parseJSON(res.stdout); + return Array.isArray(data) ? data : data.data; +} + +export async function getNetwork(pi: ExtensionAPI, idOrName: string): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "network", "show", idOrName]); + if (res.code !== 0) throw new CLIError("network show", res); + return parseJSON(res.stdout); +} + +export async function deleteNetwork(pi: ExtensionAPI, idOrName: string): Promise { + const res = await run(pi, ["sandbox", "network", "rm", idOrName, "--yes"]); + if (res.code !== 0) throw new CLIError("network rm", res); +} + +export async function attachNetwork( + pi: ExtensionAPI, + sandboxId: string, + netIdOrName: string, +): Promise { + const res = await run(pi, ["sandbox", "network", "attach", sandboxId, netIdOrName]); + if (res.code !== 0) throw new CLIError("network attach", res); +} + +export async function detachNetwork( + pi: ExtensionAPI, + sandboxId: string, + netIdOrName: string, +): Promise { + const res = await run(pi, ["sandbox", "network", "detach", sandboxId, netIdOrName, "--yes"]); + if (res.code !== 0) throw new CLIError("network detach", res); +} + +// --- Disks --- + +export interface DiskInfo { + id: string; + name: string; + kind?: string; + config?: { bucket?: string; endpoint?: string; region?: string }; + [key: string]: unknown; +} + +export async function createDisk( + pi: ExtensionAPI, + opts: { + name: string; + bucket: string; + endpoint: string; + accessKey: string; + secretKey: string; + region?: string; + pathStyle?: boolean; + }, +): Promise { + const args = [ + "-o", + "json", + "sandbox", + "disk", + "create", + opts.name, + "--bucket", + opts.bucket, + "--endpoint", + opts.endpoint, + "--access-key", + opts.accessKey, + "--secret-key", + opts.secretKey, + ]; + if (opts.region) args.push("--region", opts.region); + if (opts.pathStyle) args.push("--path-style"); + const res = await run(pi, args); + if (res.code !== 0) throw new CLIError("disk create", res); + return parseJSON(res.stdout); +} + +export async function listDisks(pi: ExtensionAPI): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "disk", "ls"]); + if (res.code !== 0) throw new CLIError("disk ls", res); + const data = parseJSON(res.stdout); + return Array.isArray(data) ? data : data.data; +} + +export async function getDisk(pi: ExtensionAPI, idOrName: string): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "disk", "show", idOrName]); + if (res.code !== 0) throw new CLIError("disk show", res); + return parseJSON(res.stdout); +} + +export async function deleteDisk(pi: ExtensionAPI, idOrName: string): Promise { + const res = await run(pi, ["sandbox", "disk", "rm", idOrName, "--yes"]); + if (res.code !== 0) throw new CLIError("disk rm", res); +} + +export async function attachDisk( + pi: ExtensionAPI, + sandboxId: string, + diskIdOrName: string, + mountPath: string, +): Promise { + const res = await run(pi, ["sandbox", "disk", "attach", sandboxId, diskIdOrName, mountPath]); + if (res.code !== 0) throw new CLIError("disk attach", res); +} + +export async function detachDisk( + pi: ExtensionAPI, + sandboxId: string, + diskIdOrName: string, + mountPath: string, +): Promise { + const res = await run(pi, [ + "sandbox", + "disk", + "detach", + sandboxId, + diskIdOrName, + mountPath, + "--yes", + ]); + if (res.code !== 0) throw new CLIError("disk detach", res); +} + +// --- Devices --- + +export interface DeviceInfo { + device_id?: string; + id?: string; + name: string; + client_ip?: string; + [key: string]: unknown; +} + +export async function listDevices(pi: ExtensionAPI): Promise { + const res = await run(pi, ["-o", "json", "sandbox", "devices", "ls"]); + if (res.code !== 0) throw new CLIError("devices ls", res); + try { + const data = parseJSON(res.stdout); + return Array.isArray(data) ? data : data.data; + } catch { + return []; + } +} + +export async function attachDeviceToNetwork( + pi: ExtensionAPI, + deviceId: string, + netIdOrName: string, +): Promise { + const res = await run(pi, ["sandbox", "network", "attach", deviceId, netIdOrName]); + if (res.code !== 0) throw new CLIError("device network attach", res); +} + +export async function detachDeviceFromNetwork( + pi: ExtensionAPI, + deviceId: string, + netIdOrName: string, +): Promise { + const res = await run(pi, ["sandbox", "network", "detach", deviceId, netIdOrName, "--yes"]); + if (res.code !== 0) throw new CLIError("device network detach", res); +} + +export async function registerDevice(pi: ExtensionAPI, name?: string): Promise { + const args = ["sandbox", "devices", "register"]; + if (name) args.push("--name", name); + const res = await run(pi, args); + if (res.code !== 0) throw new CLIError("devices register", res); + return res.stdout; +} + +// --- Helpers --- + +const CLI_INSTALL_URL = + "https://raw.githubusercontent.com/NodeOps-app/createos-cli/main/install.sh"; + +export async function isCreateOSInstalled(pi: ExtensionAPI): Promise { + try { + const res = await pi.exec("createos", ["version"]); + return res.code === 0; + } catch { + return false; + } +} + +export async function autoInstallCLI(pi: ExtensionAPI): Promise { + try { + const res = await pi.exec("bash", ["-c", `curl -sfL "${CLI_INSTALL_URL}" | sh -`]); + if (res.code !== 0) return false; + // Verify it landed on PATH + return isCreateOSInstalled(pi); + } catch { + return false; + } +} + +export async function isLoggedIn(pi: ExtensionAPI): Promise { + try { + const res = await pi.exec("createos", ["-o", "json", "sandbox", "shapes"]); + return res.code === 0; + } catch { + return false; + } +} + +export class CLIError extends Error { + constructor( + public readonly command: string, + public readonly result: ExecResult, + ) { + const msg = result.stderr.trim() || result.stdout.trim() || `exit code ${result.code}`; + super(`createos ${command}: ${msg}`); + this.name = "CLIError"; + } + + get isNotFound(): boolean { + return this.result.stderr.includes("not found") || this.result.code === 404; + } +} diff --git a/packages/pi-extension/src/find-tool.ts b/packages/pi-extension/src/find-tool.ts new file mode 100644 index 0000000..0b74ec2 --- /dev/null +++ b/packages/pi-extension/src/find-tool.ts @@ -0,0 +1,62 @@ +/** + * Remote find via `createos sandbox exec`. + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { sandboxExec } from "./cli.ts"; +import { joinPath, shellQuote } from "./util.ts"; + +export interface FindParams { + pattern: string; + path?: string; + limit?: number; +} + +export interface RemoteSearchResult { + content: { type: "text"; text: string }[]; + details: undefined; +} + +const DEFAULT_LIMIT = 1000; + +export async function runRemoteFind( + pi: ExtensionAPI, + sandboxId: string, + cwd: string, + params: FindParams, +): Promise { + const { pattern, path: searchDir = ".", limit } = params; + const max = Number.isFinite(limit ?? DEFAULT_LIMIT) + ? Math.max(1, Math.floor(limit ?? DEFAULT_LIMIT)) + : DEFAULT_LIMIT; + const searchPath = searchDir.startsWith("/") ? searchDir : joinPath(cwd, searchDir); + + let effective = pattern; + if ( + pattern.includes("/") && + !pattern.startsWith("/") && + !pattern.startsWith("**/") && + pattern !== "**" + ) { + effective = `**/${pattern}`; + } + const basename = effective.split("/").pop() || effective; + + const rg = `rg --files --hidden -g ${shellQuote("!**/.git/**")} -g ${shellQuote("!**/node_modules/**")} -g ${shellQuote(effective)}`; + const find = `find . -type f -name ${shellQuote(basename)} ! -path ${shellQuote("*/.git/*")} ! -path ${shellQuote("*/node_modules/*")}`; + + const command = [ + `cd ${shellQuote(searchPath)}`, + `if command -v rg >/dev/null 2>&1; then ${rg} 2>/dev/null | head -n ${max}; else ${find} 2>/dev/null | head -n ${max}; fi`, + "exit 0", + ].join("\n"); + + const res = await sandboxExec(pi, sandboxId, command); + const lines = res.stdout + .split("\n") + .map((l) => l.replace(/^\.\//, "").replace(/\r$/, "")) + .filter((l) => l.length > 0); + + const body = lines.length > 0 ? lines.join("\n") : "No files found matching pattern"; + return { content: [{ type: "text", text: body }], details: undefined }; +} diff --git a/packages/pi-extension/src/grep-tool.ts b/packages/pi-extension/src/grep-tool.ts new file mode 100644 index 0000000..b042045 --- /dev/null +++ b/packages/pi-extension/src/grep-tool.ts @@ -0,0 +1,62 @@ +/** + * Remote grep via `createos sandbox exec`. + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { sandboxExec } from "./cli.ts"; +import { shellQuote } from "./util.ts"; + +export interface GrepParams { + pattern: string; + path?: string; + glob?: string; + ignoreCase?: boolean; + literal?: boolean; + context?: number; + limit?: number; +} + +export interface RemoteGrepResult { + content: { type: "text"; text: string }[]; + details: undefined; +} + +const DEFAULT_LIMIT = 100; + +export async function runRemoteGrep( + pi: ExtensionAPI, + sandboxId: string, + cwd: string, + params: GrepParams, +): Promise { + const { pattern, path: searchDir = ".", glob, ignoreCase, literal, context, limit } = params; + const max = Number.isFinite(limit ?? DEFAULT_LIMIT) + ? Math.max(1, Math.floor(limit ?? DEFAULT_LIMIT)) + : DEFAULT_LIMIT; + const ctxLines = Number.isFinite(context ?? 0) && (context ?? 0) > 0 ? Math.floor(context!) : 0; + + const rg = ["rg", "--line-number", "--no-heading", "--color=never", "--hidden"]; + if (ignoreCase) rg.push("--ignore-case"); + if (literal) rg.push("--fixed-strings"); + if (ctxLines) rg.push("--context", String(ctxLines)); + if (glob) rg.push("--glob", shellQuote(glob)); + rg.push("--", shellQuote(pattern), shellQuote(searchDir)); + + const gp = ["grep", "-rnI"]; + if (ignoreCase) gp.push("-i"); + if (literal) gp.push("-F"); + if (ctxLines) gp.push("-C", String(ctxLines)); + if (glob) gp.push(`--include=${shellQuote(glob)}`); + gp.push("--", shellQuote(pattern), shellQuote(searchDir)); + + const command = [ + `cd ${shellQuote(cwd)}`, + `if command -v rg >/dev/null 2>&1; then ${rg.join(" ")} 2>/dev/null | head -n ${max}; else ${gp.join(" ")} 2>/dev/null | head -n ${max}; fi`, + "exit 0", + ].join("\n"); + + const res = await sandboxExec(pi, sandboxId, command); + const text = res.stdout.replace(/\s+$/, ""); + const body = text.length > 0 ? text : `No matches found for /${pattern}/ in ${searchDir}`; + return { content: [{ type: "text", text: body }], details: undefined }; +} diff --git a/packages/pi-extension/src/ops.ts b/packages/pi-extension/src/ops.ts new file mode 100644 index 0000000..331cbe1 --- /dev/null +++ b/packages/pi-extension/src/ops.ts @@ -0,0 +1,123 @@ +/** + * CreateOS-backed tool operations via the CLI. + * + * Every operation shells out to `createos sandbox exec` or + * `createos sandbox push/pull` โ€” no HTTP client needed. + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import type { + BashOperations, + EditOperations, + LsOperations, + ReadOperations, + WriteOperations, +} from "@earendil-works/pi-coding-agent"; +import { sandboxExec, pullFile, pushFile } from "./cli.ts"; +import { shellQuote } from "./util.ts"; + +const IMAGE_MIME_TYPES = new Set(["image/jpeg", "image/png", "image/gif", "image/webp"]); + +async function run(pi: ExtensionAPI, id: string, command: string) { + return sandboxExec(pi, id, command); +} + +function backgroundSafe(command: string): string { + return [ + '__pi_out=$(mktemp 2>/dev/null || echo "/tmp/pi-createos-$$.out")', + `( ${command}`, + ') >"$__pi_out" 2>&1', + "__pi_rc=$?", + 'cat "$__pi_out"', + 'rm -f "$__pi_out"', + "exit $__pi_rc", + ].join("\n"); +} + +export function createBashOps( + pi: ExtensionAPI, + sandboxId: string, + cwdOverride?: string, +): BashOperations { + return { + exec: async (command, cwd, { onData, signal }) => { + if (signal?.aborted) throw new Error("aborted"); + const effectiveCwd = cwdOverride ?? cwd; + const fullCmd = effectiveCwd + ? `cd ${shellQuote(effectiveCwd)} && ${backgroundSafe(command)}` + : backgroundSafe(command); + const res = await sandboxExec(pi, sandboxId, fullCmd); + if (res.stdout) onData(Buffer.from(res.stdout)); + return { exitCode: res.exitCode ?? null }; + }, + }; +} + +export function createReadOps(pi: ExtensionAPI, sandboxId: string): ReadOperations { + return { + readFile: async (path) => Buffer.from(await pullFile(pi, sandboxId, path)), + access: async (path) => { + const { exitCode } = await run(pi, sandboxId, `test -r ${shellQuote(path)}`); + if (exitCode !== 0) throw new Error(`File not readable: ${path}`); + }, + detectImageMimeType: async (path) => { + try { + const { stdout } = await run(pi, sandboxId, `file --mime-type -b ${shellQuote(path)}`); + const mime = stdout.trim(); + return IMAGE_MIME_TYPES.has(mime) ? mime : null; + } catch { + return null; + } + }, + }; +} + +export function createWriteOps(pi: ExtensionAPI, sandboxId: string): WriteOperations { + return { + writeFile: (path, content) => pushFile(pi, sandboxId, content, path), + mkdir: async (dir) => { + const { exitCode } = await run(pi, sandboxId, `mkdir -p ${shellQuote(dir)}`); + if (exitCode !== 0) throw new Error(`Failed to create directory: ${dir}`); + }, + }; +} + +export function createEditOps(pi: ExtensionAPI, sandboxId: string): EditOperations { + return { + readFile: async (path) => Buffer.from(await pullFile(pi, sandboxId, path)), + writeFile: (path, content) => pushFile(pi, sandboxId, content, path), + access: async (path) => { + const { exitCode } = await run( + pi, + sandboxId, + `test -r ${shellQuote(path)} && test -w ${shellQuote(path)}`, + ); + if (exitCode !== 0) throw new Error(`File not readable/writable: ${path}`); + }, + }; +} + +export function createLsOps(pi: ExtensionAPI, sandboxId: string): LsOperations { + return { + exists: async (path) => { + const { exitCode } = await run(pi, sandboxId, `test -e ${shellQuote(path)}`); + return exitCode === 0; + }, + stat: async (path) => { + const q = shellQuote(path); + const { stdout, exitCode } = await run( + pi, + sandboxId, + `test -e ${q} || exit 1; test -d ${q} && echo dir || echo other`, + ); + if (exitCode !== 0) throw new Error(`Path not found: ${path}`); + const isDir = stdout.trim() === "dir"; + return { isDirectory: () => isDir }; + }, + readdir: async (path) => { + const { stdout, exitCode } = await run(pi, sandboxId, `ls -1A ${shellQuote(path)}`); + if (exitCode !== 0) throw new Error(`Failed to read directory: ${path}`); + return stdout.split("\n").filter((line) => line.length > 0); + }, + }; +} diff --git a/packages/pi-extension/src/tools.ts b/packages/pi-extension/src/tools.ts new file mode 100644 index 0000000..3d869f3 --- /dev/null +++ b/packages/pi-extension/src/tools.ts @@ -0,0 +1,1134 @@ +/** + * Tool registration โ€” CLI-only edition. + * + * Each of Pi's built-in tools is replaced with a sandbox-backed variant. + * Each CreateOS capability is a focused, single-purpose tool following + * Pi extension best practices (snake_case, named promptGuidelines). + */ + +import type { ExtensionAPI } from "@earendil-works/pi-coding-agent"; +import { + createBashTool, + createEditTool, + createFindTool, + createGrepTool, + createLsTool, + createReadTool, + createWriteTool, +} from "@earendil-works/pi-coding-agent"; +import { Type } from "typebox"; +import * as cli from "./cli.ts"; +import { type FindParams, runRemoteFind } from "./find-tool.ts"; +import { type GrepParams, runRemoteGrep } from "./grep-tool.ts"; +import { createBashOps, createEditOps, createLsOps, createReadOps, createWriteOps } from "./ops.ts"; + +export interface ToolSandbox { + sandboxId: string; + cwd: string; +} + +export function registerTools(pi: ExtensionAPI, getActive: () => ToolSandbox | null): void { + const localCwd = process.cwd(); + const localBash = createBashTool(localCwd); + const localRead = createReadTool(localCwd); + const localWrite = createWriteTool(localCwd); + const localEdit = createEditTool(localCwd); + const localLs = createLsTool(localCwd); + const localFind = createFindTool(localCwd); + const localGrep = createGrepTool(localCwd); + + function requireSandbox(): ToolSandbox | null { + const active = getActive(); + if (active) return active; + if (pi.getFlag("createos") === true) { + throw new Error( + "CreateOS sandbox is unavailable โ€” the tool was NOT run on your host. Restart Pi.", + ); + } + return null; + } + + function sandboxTool unknown }>( + local: T, + makeRemote: (s: ToolSandbox) => T, + ): T { + return { + ...local, + execute: (...args: Parameters) => { + const active = requireSandbox(); + const tool = active ? makeRemote(active) : local; + return tool.execute(...args); + }, + } as T; + } + + // --- Built-in tool replacements (bash, read, write, edit, ls, find, grep) --- + + pi.registerTool( + sandboxTool(localBash, (s) => + createBashTool(s.cwd, { operations: createBashOps(pi, s.sandboxId) }), + ), + ); + pi.registerTool( + sandboxTool(localRead, (s) => + createReadTool(s.cwd, { operations: createReadOps(pi, s.sandboxId) }), + ), + ); + pi.registerTool( + sandboxTool(localWrite, (s) => + createWriteTool(s.cwd, { operations: createWriteOps(pi, s.sandboxId) }), + ), + ); + pi.registerTool( + sandboxTool(localEdit, (s) => + createEditTool(s.cwd, { operations: createEditOps(pi, s.sandboxId) }), + ), + ); + pi.registerTool( + sandboxTool(localLs, (s) => createLsTool(s.cwd, { operations: createLsOps(pi, s.sandboxId) })), + ); + + pi.registerTool({ + ...localFind, + async execute(id, params, signal, onUpdate) { + const active = requireSandbox(); + if (active) { + if (signal?.aborted) throw new Error("aborted"); + return runRemoteFind(pi, active.sandboxId, active.cwd, params as FindParams); + } + return localFind.execute(id, params, signal, onUpdate); + }, + }); + + pi.registerTool({ + ...localGrep, + async execute(id, params, signal, onUpdate) { + const active = requireSandbox(); + if (active) { + if (signal?.aborted) throw new Error("aborted"); + return runRemoteGrep(pi, active.sandboxId, active.cwd, params as GrepParams); + } + return localGrep.execute(id, params, signal, onUpdate); + }, + }); + + // --- Sandbox create --- + + pi.registerTool({ + name: "sandbox_create", + label: "Create Sandbox", + description: + "Create an additional sandbox. Use this when the user needs multiple sandboxes โ€” e.g. multi-node clusters, " + + "separate database servers, microservice setups. The new sandbox can be joined to a network so it can communicate with others.", + promptSnippet: "Create a new sandbox", + promptGuidelines: [ + "Use sandbox_create when the user needs additional sandboxes beyond the current one.", + "Use sandbox_create with networks to connect the new sandbox to existing sandboxes.", + "After creating, use sandbox_network_attach if the sandbox needs to join an existing network.", + ], + parameters: Type.Object({ + shape: Type.Optional( + Type.String({ + description: "Sandbox size (default: s-2vcpu-2gb). See sandbox_shapes for options.", + }), + ), + rootfs: Type.Optional(Type.String({ description: "Base image (default: devbox:1)" })), + name: Type.Optional(Type.String({ description: "Friendly name for the sandbox" })), + networks: Type.Optional( + Type.Array(Type.String(), { description: "Network names to join at creation" }), + ), + }), + async execute(_id, { shape, rootfs, name, networks }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const sb = await cli.createSandbox(pi, { shape, rootfs, name, networks, ingress: true }); + const lines = [ + `Sandbox created: ${sb.id}`, + `IP: ${sb.ip ?? "pending"}`, + `Shape: ${(sb as any).shape ?? shape ?? "s-2vcpu-2gb"}`, + ]; + if (sb.ingress_url_template) lines.push(`Ingress: ${sb.ingress_url_template}`); + if (networks?.length) lines.push(`Networks: ${networks.join(", ")}`); + return { content: [{ type: "text", text: lines.join("\n") }], details: { sandbox: sb } }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox exec (on any sandbox, not just the active one) --- + + pi.registerTool({ + name: "sandbox_exec", + label: "Exec on Sandbox", + description: + "Run a command on a specific sandbox by ID. Use this to execute commands on sandboxes other than the current one โ€” " + + "e.g. setting up a second node in a cluster, installing software on a database sandbox, etc.", + promptSnippet: "Run a command on a specific sandbox", + promptGuidelines: [ + "Use sandbox_exec to run commands on sandboxes created with sandbox_create.", + "The built-in bash tool runs on the current sandbox. Use sandbox_exec for any other sandbox.", + ], + parameters: Type.Object({ + sandbox_id: Type.String({ description: "ID of the target sandbox" }), + command: Type.String({ description: "Shell command to run" }), + }), + async execute(_id, { sandbox_id, command }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const res = await cli.sandboxExec(pi, sandbox_id, command); + return { + content: [{ type: "text", text: res.stdout || "(no output)" }], + details: { exit_code: res.exitCode }, + }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox info --- + + pi.registerTool({ + name: "sandbox_info", + label: "Sandbox Info", + description: "Get the current sandbox status, IP, shape, region, and ingress URL.", + promptSnippet: "Get sandbox details", + promptGuidelines: [ + "Use sandbox_info to check the current sandbox status, IP address, or ingress URL.", + ], + parameters: Type.Object({ + sandbox_id: Type.Optional(Type.String({ description: "Sandbox ID (defaults to current)" })), + }), + async execute(_id, { sandbox_id }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const targetId = sandbox_id ?? getActive()?.sandboxId; + if (!targetId) return txt("No sandbox active."); + try { + const info = await cli.getSandbox(pi, targetId); + const lines = [ + `ID: ${info.id}`, + `Status: ${info.status}`, + `Name: ${info.name ?? "n/a"}`, + `IP: ${info.ip ?? "n/a"}`, + `Shape: ${(info as any).shape ?? "n/a"}`, + `Region: ${info.region ?? "n/a"}`, + ]; + if (info.ingress_url_template) lines.push(`Ingress: ${info.ingress_url_template}`); + return { content: [{ type: "text", text: lines.join("\n") }], details: { sandbox: info } }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox list --- + + pi.registerTool({ + name: "sandbox_list", + label: "List Sandboxes", + description: "List all sandboxes owned by the user.", + promptSnippet: "List all sandboxes", + promptGuidelines: [ + "Use sandbox_list to see all sandboxes the user owns, including paused and running ones.", + ], + parameters: Type.Object({}), + async execute(_id, _params, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const sbs = await cli.listSandboxes(pi); + if (sbs.length === 0) return txt("No sandboxes found."); + const lines = sbs.map( + (s) => `${s.id} ยท ${s.status} ยท ${s.name ?? ""}${s.ip ? ` ยท ${s.ip}` : ""}`, + ); + return { content: [{ type: "text", text: lines.join("\n") }], details: { sandboxes: sbs } }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox pause --- + + pi.registerTool({ + name: "sandbox_pause", + label: "Pause Sandbox", + description: + "Pause the sandbox, saving its state. The sandbox becomes unavailable until resumed.", + promptSnippet: "Pause a sandbox", + promptGuidelines: [ + "Use sandbox_pause to snapshot and pause a sandbox. WARNING: this disconnects the current session if pausing the active sandbox.", + ], + parameters: Type.Object({ + sandbox_id: Type.Optional(Type.String({ description: "Sandbox ID (defaults to current)" })), + }), + async execute(_id, { sandbox_id }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const targetId = sandbox_id ?? getActive()?.sandboxId; + if (!targetId) return txt("No sandbox to pause."); + try { + await cli.pauseSandbox(pi, targetId); + return { + ...txt(`Sandbox ${targetId} is pausing. It will be unavailable until resumed.`), + terminate: true, + }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox resume --- + + pi.registerTool({ + name: "sandbox_resume", + label: "Resume Sandbox", + description: "Resume a paused sandbox.", + promptSnippet: "Resume a paused sandbox", + promptGuidelines: ["Use sandbox_resume to bring a paused sandbox back to running state."], + parameters: Type.Object({ + sandbox_id: Type.String({ description: "ID of the paused sandbox to resume" }), + }), + async execute(_id, { sandbox_id }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + await cli.resumeSandbox(pi, sandbox_id); + return txt(`Sandbox ${sandbox_id} is resuming.`); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox fork --- + + pi.registerTool({ + name: "sandbox_fork", + label: "Fork Sandbox", + description: "Clone a paused sandbox into a brand-new sandbox with the same state.", + promptSnippet: "Fork a sandbox", + promptGuidelines: [ + "Use sandbox_fork to create a copy of a sandbox. The source sandbox must be paused first.", + ], + parameters: Type.Object({ + sandbox_id: Type.Optional( + Type.String({ description: "Sandbox ID to fork (defaults to current)" }), + ), + paused: Type.Optional( + Type.Boolean({ description: "Keep the fork paused instead of auto-resuming" }), + ), + }), + async execute(_id, { sandbox_id, paused }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const targetId = sandbox_id ?? getActive()?.sandboxId; + if (!targetId) return txt("No sandbox to fork."); + try { + const forked = await cli.forkSandbox(pi, targetId, { paused }); + return { + content: [ + { + type: "text", + text: `Forked into ${forked.id} (${forked.status}). IP: ${forked.ip ?? "pending"}`, + }, + ], + details: { sandbox: forked }, + }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox destroy --- + + pi.registerTool({ + name: "sandbox_destroy", + label: "Destroy Sandbox", + description: "Permanently delete a sandbox. This cannot be undone.", + promptSnippet: "Destroy a sandbox", + promptGuidelines: [ + "Use sandbox_destroy to permanently delete a sandbox. Requires an explicit sandbox_id.", + ], + parameters: Type.Object({ + sandbox_id: Type.String({ description: "ID of the sandbox to destroy" }), + }), + async execute(_id, { sandbox_id }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + await cli.destroySandbox(pi, sandbox_id); + return { ...txt(`Sandbox ${sandbox_id} destroyed.`), terminate: true }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox ingress toggle --- + + pi.registerTool({ + name: "sandbox_ingress", + label: "Toggle Ingress", + description: "Enable or disable public HTTPS URL access for a sandbox.", + promptSnippet: "Toggle public URL on/off", + promptGuidelines: [ + "Use sandbox_ingress to enable or disable the public HTTPS URL for a sandbox.", + ], + parameters: Type.Object({ + enabled: Type.Boolean({ description: "true to enable, false to disable" }), + sandbox_id: Type.Optional(Type.String({ description: "Sandbox ID (defaults to current)" })), + }), + async execute(_id, { enabled, sandbox_id }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const targetId = sandbox_id ?? getActive()?.sandboxId; + if (!targetId) return txt("No sandbox active."); + try { + await cli.editSandbox(pi, targetId, { ingress: enabled }); + if (enabled) { + const info = await cli.getSandbox(pi, targetId); + return txt( + `Ingress enabled.${info.ingress_url_template ? ` URL: ${info.ingress_url_template}` : ""}`, + ); + } + return txt("Ingress disabled."); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox firewall --- + + pi.registerTool({ + name: "sandbox_firewall", + label: "Set Firewall", + description: + "Set egress firewall rules for a sandbox. Pass an empty list to allow all outbound traffic.", + promptSnippet: "Set sandbox egress rules", + promptGuidelines: [ + "Use sandbox_firewall to restrict which domains/IPs the sandbox can reach. Empty rules = allow all.", + ], + parameters: Type.Object({ + rules: Type.Array(Type.String(), { + description: + 'Allowed domains or IPs (e.g. ["pypi.org", "1.1.1.1:53"]). Empty array = allow all.', + }), + sandbox_id: Type.Optional(Type.String({ description: "Sandbox ID (defaults to current)" })), + }), + async execute(_id, { rules, sandbox_id }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const targetId = sandbox_id ?? getActive()?.sandboxId; + if (!targetId) return txt("No sandbox active."); + try { + await cli.editSandbox(pi, targetId, { egress: rules }); + return txt( + rules.length > 0 + ? `Firewall set: ${rules.join(", ")}` + : "Firewall cleared โ€” all outbound allowed.", + ); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox bandwidth --- + + pi.registerTool({ + name: "sandbox_bandwidth", + label: "Check Bandwidth", + description: "Check bandwidth usage and quota for a sandbox.", + promptSnippet: "Check bandwidth usage", + promptGuidelines: [ + "Use sandbox_bandwidth to check how much bandwidth the sandbox has used and whether it is capped.", + ], + parameters: Type.Object({ + sandbox_id: Type.Optional(Type.String({ description: "Sandbox ID (defaults to current)" })), + }), + async execute(_id, { sandbox_id }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const targetId = sandbox_id ?? getActive()?.sandboxId; + if (!targetId) return txt("No sandbox active."); + try { + const bw = await cli.getBandwidth(pi, targetId); + return txt( + `Bandwidth: ${fmtBytes(bw.used_bytes)} used of ${fmtBytes(bw.quota_bytes)} (${fmtBytes(bw.remaining_bytes)} remaining)${bw.capped ? " โ€” CAPPED" : ""}`, + ); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox shapes --- + + pi.registerTool({ + name: "sandbox_shapes", + label: "List Shapes", + description: "List available sandbox sizes (vCPU, RAM).", + promptSnippet: "List available sandbox sizes", + promptGuidelines: [ + "Use sandbox_shapes to see what sandbox sizes are available before creating a new sandbox.", + ], + parameters: Type.Object({}), + async execute(_id, _params, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const shapes = await cli.listShapes(pi); + const lines = shapes.map( + (s: any) => `${s.name ?? s.id} ยท ${s.vcpu} vCPU ยท ${s.mem_mib} MB RAM`, + ); + return { content: [{ type: "text", text: lines.join("\n") }], details: { shapes } }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sandbox images --- + + pi.registerTool({ + name: "sandbox_images", + label: "List Images", + description: "List available base images (rootfs) for sandbox creation.", + promptSnippet: "List available sandbox base images", + promptGuidelines: [ + "Use sandbox_images to see what base images are available before creating a new sandbox.", + ], + parameters: Type.Object({}), + async execute(_id, _params, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const rootfs = await cli.listRootfs(pi); + return { + content: [{ type: "text", text: JSON.stringify(rootfs, null, 2) }], + details: { rootfs }, + }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Preview URL --- + + pi.registerTool({ + name: "sandbox_preview_url", + label: "Preview URL", + description: "Get the public HTTPS URL for a port served inside the sandbox.", + promptSnippet: "Get a public URL for a sandbox port", + promptGuidelines: [ + "Use sandbox_preview_url after starting a server to give the user a clickable public link.", + ], + parameters: Type.Object({ + port: Type.Integer({ + minimum: 1, + maximum: 65535, + description: "The port the server listens on", + }), + }), + async execute(_id, { port }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const active = requireSandbox(); + if (!active) return txt("No active sandbox."); + try { + const info = await cli.getSandbox(pi, active.sandboxId); + if (info.ingress_url_template) { + return txt( + `Preview URL for port ${port}: ${info.ingress_url_template.replace("", String(port))}`, + ); + } + return txt("Ingress not enabled. Use sandbox_ingress to enable it first."); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Tunnel (port forward) --- + + pi.registerTool({ + name: "sandbox_tunnel", + label: "Port Forward", + description: "Forward a sandbox port to localhost on the user's machine. No setup needed.", + promptSnippet: "Forward a sandbox port to localhost", + promptGuidelines: [ + "Use sandbox_tunnel when the user wants to access a sandbox port from their machine.", + "Prefer sandbox_tunnel over sandbox_device_attach โ€” sandbox_tunnel requires no setup.", + ], + parameters: Type.Object({ + remote_port: Type.Integer({ + minimum: 1, + maximum: 65535, + description: "Port inside the sandbox", + }), + local_port: Type.Optional( + Type.Integer({ + minimum: 1, + maximum: 65535, + description: "Local port (defaults to remote_port)", + }), + ), + }), + async execute(_id, { remote_port, local_port }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const active = requireSandbox(); + if (!active) return txt("No active sandbox."); + try { + const result = await cli.startTunnel(pi, active.sandboxId, remote_port, local_port); + return txt( + `Port forward started: localhost:${result.localPort} โ†’ sandbox:${remote_port}\nAccess at: http://localhost:${result.localPort}`, + ); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Sync (directory mount) --- + + pi.registerTool({ + name: "sandbox_sync", + label: "Sync Directory", + description: + "Mount, sync, or mirror a local directory from the user's machine into the sandbox. " + + "Bidirectional by default โ€” changes on either side propagate to the other. Runs in the background until the session ends.", + promptSnippet: "Mount/sync a local directory into the sandbox", + promptGuidelines: [ + 'Use sandbox_sync when the user says "mount", "sync", "mirror", or "upload directory" into the sandbox.', + 'Use sandbox_sync with mode "one-way" if only local changes should push to the sandbox.', + 'Use sandbox_sync with mode "mirror" to make the sandbox directory identical to the local one.', + ], + parameters: Type.Object({ + local_dir: Type.String({ + description: "Absolute path to the local directory on the user's machine", + }), + remote_dir: Type.String({ + description: "Absolute path inside the sandbox (e.g. /root/project)", + }), + mode: Type.Optional( + Type.String({ + description: + 'Sync mode: "two-way" (default), "one-way" (local wins), or "mirror" (local wins + deletes extras)', + }), + ), + exclude: Type.Optional( + Type.Array(Type.String(), { + description: 'Glob patterns to exclude (e.g. ["node_modules", "*.log"])', + }), + ), + }), + async execute(_id, { local_dir, remote_dir, mode, exclude }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const active = requireSandbox(); + if (!active) return txt("No active sandbox."); + try { + const result = await cli.startSync(pi, active.sandboxId, local_dir, remote_dir, { + mode, + exclude, + }); + return txt( + `Sync started: ${local_dir} โ†” sandbox:${remote_dir}${mode ? ` (${mode})` : ""}\nChanges will propagate automatically. PID: ${result.pid}`, + ); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Network tools --- + + pi.registerTool({ + name: "sandbox_network_create", + label: "Create Network", + description: "Create a new private network for sandbox-to-sandbox communication.", + promptSnippet: "Create a private network", + promptGuidelines: [ + "Use sandbox_network_create when sandboxes need to communicate with each other.", + ], + parameters: Type.Object({ + name: Type.String({ description: "Network name" }), + }), + async execute(_id, { name }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const net = await cli.createNetwork(pi, name); + return { + content: [{ type: "text", text: `Network created: ${net.name} (${net.id})` }], + details: { network: net }, + }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_network_list", + label: "List Networks", + description: "List all private networks owned by the user.", + promptSnippet: "List private networks", + promptGuidelines: [ + "Use sandbox_network_list to see existing networks before creating or attaching.", + ], + parameters: Type.Object({}), + async execute(_id, _params, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const nets = await cli.listNetworks(pi); + if (nets.length === 0) return txt("No networks found."); + const lines = nets.map((n) => `${n.name} (${n.id}) ยท ${n.member_count ?? 0} members`); + return { content: [{ type: "text", text: lines.join("\n") }], details: { networks: nets } }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_network_show", + label: "Show Network", + description: "Show network details including member sandbox IPs.", + promptSnippet: "Show network members and their IPs", + promptGuidelines: [ + "Use sandbox_network_show to see which sandboxes are on a network and their IPs for configuring connections.", + ], + parameters: Type.Object({ + name: Type.String({ description: "Network name or id" }), + }), + async execute(_id, { name }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const net = await cli.getNetwork(pi, name); + const lines = [`Network: ${net.name} (${net.id})`]; + if (net.members?.length) { + lines.push("Members:"); + for (const m of net.members) + lines.push(` ${m.sandbox_id} ยท ${m.status} ยท ${m.ip}${m.name ? ` ยท ${m.name}` : ""}`); + } else lines.push("No members"); + return { content: [{ type: "text", text: lines.join("\n") }], details: { network: net } }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_network_attach", + label: "Attach to Network", + description: "Attach the current sandbox to a private network.", + promptSnippet: "Join a sandbox to a network", + promptGuidelines: [ + "Use sandbox_network_attach to join the current sandbox to a network so it can reach other members.", + ], + parameters: Type.Object({ + name: Type.String({ description: "Network name or id" }), + }), + async execute(_id, { name }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const active = requireSandbox(); + if (!active) return txt("No active sandbox."); + try { + await cli.attachNetwork(pi, active.sandboxId, name); + return txt(`Attached sandbox to network "${name}".`); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_network_detach", + label: "Detach from Network", + description: "Detach the current sandbox from a private network.", + promptSnippet: "Remove a sandbox from a network", + promptGuidelines: ["Use sandbox_network_detach to remove the current sandbox from a network."], + parameters: Type.Object({ + name: Type.String({ description: "Network name or id" }), + }), + async execute(_id, { name }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const active = requireSandbox(); + if (!active) return txt("No active sandbox."); + try { + await cli.detachNetwork(pi, active.sandboxId, name); + return txt(`Detached sandbox from network "${name}".`); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_network_delete", + label: "Delete Network", + description: "Delete a private network. The network must have no members.", + promptSnippet: "Delete a network", + promptGuidelines: [ + "Use sandbox_network_delete to remove a network. Detach all sandboxes first.", + ], + parameters: Type.Object({ + name: Type.String({ description: "Network name or id" }), + }), + async execute(_id, { name }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + await cli.deleteNetwork(pi, name); + return txt(`Network "${name}" deleted.`); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Disk tools --- + + pi.registerTool({ + name: "sandbox_disk_create", + label: "Create Disk", + description: + "Register an S3-compatible bucket as a persistent disk that can be mounted into sandboxes.", + promptSnippet: "Register an S3 bucket as a mountable disk", + promptGuidelines: [ + "Use sandbox_disk_create when the user wants persistent storage that survives sandbox destroy.", + ], + parameters: Type.Object({ + name: Type.String({ description: "Disk name" }), + bucket: Type.String({ description: "S3 bucket name" }), + endpoint: Type.String({ description: "S3 endpoint URL (e.g. https://s3.amazonaws.com)" }), + access_key: Type.String({ description: "Access key ID" }), + secret_key: Type.String({ description: "Secret access key" }), + region: Type.Optional(Type.String({ description: "AWS region (e.g. us-east-1)" })), + path_style: Type.Optional( + Type.Boolean({ description: "Use path-style URLs (needed for MinIO)" }), + ), + }), + async execute( + _id, + { name, bucket, endpoint, access_key, secret_key, region, path_style }, + signal, + ) { + if (signal?.aborted) throw new Error("aborted"); + try { + const disk = await cli.createDisk(pi, { + name, + bucket, + endpoint, + accessKey: access_key, + secretKey: secret_key, + region, + pathStyle: path_style, + }); + return { + content: [{ type: "text", text: `Disk created: ${disk.name} (${disk.id})` }], + details: { disk }, + }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_disk_list", + label: "List Disks", + description: "List all S3 disks registered by the user.", + promptSnippet: "List registered disks", + promptGuidelines: [ + "Use sandbox_disk_list to see available disks before attaching one to a sandbox.", + ], + parameters: Type.Object({}), + async execute(_id, _params, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const disks = await cli.listDisks(pi); + if (disks.length === 0) return txt("No disks registered."); + const lines = disks.map( + (d) => `${d.name} (${d.id})${d.config?.bucket ? ` ยท bucket: ${d.config.bucket}` : ""}`, + ); + return { content: [{ type: "text", text: lines.join("\n") }], details: { disks } }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_disk_show", + label: "Show Disk", + description: "Show details for a registered disk.", + promptSnippet: "Show disk details", + promptGuidelines: [ + "Use sandbox_disk_show to see a disk's configuration and which sandboxes it is attached to.", + ], + parameters: Type.Object({ + name: Type.String({ description: "Disk name or id" }), + }), + async execute(_id, { name }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const disk = await cli.getDisk(pi, name); + return { + content: [{ type: "text", text: JSON.stringify(disk, null, 2) }], + details: { disk }, + }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_disk_delete", + label: "Delete Disk", + description: "Delete a registered disk. The disk must not be attached to any sandbox.", + promptSnippet: "Delete a disk", + promptGuidelines: [ + "Use sandbox_disk_delete to remove a disk registration. Detach it from all sandboxes first.", + ], + parameters: Type.Object({ + name: Type.String({ description: "Disk name or id" }), + }), + async execute(_id, { name }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + await cli.deleteDisk(pi, name); + return txt(`Disk "${name}" deleted.`); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_disk_attach", + label: "Attach Disk", + description: "Mount a registered disk into a running sandbox at a given path.", + promptSnippet: "Mount a disk into a sandbox", + promptGuidelines: [ + "Use sandbox_disk_attach to mount persistent storage into a sandbox. The mount path must be absolute (e.g. /mnt/data).", + ], + parameters: Type.Object({ + disk_name: Type.String({ description: "Disk name or id" }), + mount_path: Type.String({ + description: "Absolute mount path inside the sandbox (e.g. /mnt/data)", + }), + sandbox_id: Type.Optional(Type.String({ description: "Sandbox ID (defaults to current)" })), + }), + async execute(_id, { disk_name, mount_path, sandbox_id }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const targetId = sandbox_id ?? getActive()?.sandboxId; + if (!targetId) return txt("No sandbox active."); + try { + await cli.attachDisk(pi, targetId, disk_name, mount_path); + return txt(`Disk "${disk_name}" mounted at ${mount_path}`); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_disk_detach", + label: "Detach Disk", + description: "Unmount a disk from a sandbox. The bucket data is untouched.", + promptSnippet: "Unmount a disk from a sandbox", + promptGuidelines: [ + "Use sandbox_disk_detach to unmount a disk. The data in the bucket remains.", + ], + parameters: Type.Object({ + disk_name: Type.String({ description: "Disk name or id" }), + mount_path: Type.String({ description: "Mount path to detach" }), + sandbox_id: Type.Optional(Type.String({ description: "Sandbox ID (defaults to current)" })), + }), + async execute(_id, { disk_name, mount_path, sandbox_id }, signal) { + if (signal?.aborted) throw new Error("aborted"); + const targetId = sandbox_id ?? getActive()?.sandboxId; + if (!targetId) return txt("No sandbox active."); + try { + await cli.detachDisk(pi, targetId, disk_name, mount_path); + return txt(`Disk "${disk_name}" detached from ${mount_path}`); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- Device tools --- + + pi.registerTool({ + name: "sandbox_device_register", + label: "Register Device", + description: + "Register the user's machine as a device so it can access sandbox networks directly. " + + "This is a one-time operation. Requires wireguard-tools installed on the host.", + promptSnippet: "Register this machine for direct sandbox access", + promptGuidelines: [ + "Use sandbox_device_register before sandbox_device_attach or sandbox_vpn_up.", + "Use sandbox_device_register only if sandbox_device_status shows no device registered.", + ], + parameters: Type.Object({ + name: Type.Optional(Type.String({ description: "Device name (defaults to hostname)" })), + }), + async execute(_id, { name }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const devices = await cli.listDevices(pi); + if (devices.length > 0) { + return txt( + `Device already registered: ${devices[0].name} (${devices[0].client_ip ?? "n/a"})`, + ); + } + const output = await cli.registerDevice(pi, name); + return txt(output || "Device registered."); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_device_status", + label: "Device Status", + description: "Check if the user has a registered device for direct sandbox access.", + promptSnippet: "Check device registration status", + promptGuidelines: [ + "Use sandbox_device_status to check if the user has a device registered before using sandbox_device_attach or sandbox_vpn_up.", + ], + parameters: Type.Object({}), + async execute(_id, _params, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const devices = await cli.listDevices(pi); + if (devices.length === 0) { + return txt("No device registered. Use sandbox_device_register to register this machine."); + } + const lines = devices.map( + (d) => `${d.name} ยท ${d.id ?? d.device_id} ยท IP: ${d.client_ip ?? "n/a"}`, + ); + return { + content: [{ type: "text", text: `Registered devices:\n${lines.join("\n")}` }], + details: { devices }, + }; + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_vpn_up", + label: "Start VPN", + description: + "Tell the user to start the VPN tunnel. This command requires sudo and must be run by the user in a separate terminal. " + + "Returns the exact command the user should run.", + promptSnippet: "Get the VPN start command for the user to run", + promptGuidelines: [ + "Use sandbox_vpn_up after sandbox_device_register and sandbox_device_attach.", + "sandbox_vpn_up does NOT start the VPN โ€” it tells the user the command to run in another terminal.", + ], + parameters: Type.Object({}), + async execute(_id, _params, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const devices = await cli.listDevices(pi); + if (devices.length === 0) { + return txt("No device registered. Use sandbox_device_register first."); + } + return txt( + "The user needs to run this command in a separate terminal (it requires sudo):\n\n createos sb vpn up\n\nOnce connected, sandbox IPs are reachable directly.", + ); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_device_attach", + label: "Attach Device to Network", + description: "Attach the user's device to a network so they can access sandbox IPs directly.", + promptSnippet: "Give user's machine direct access to a network", + promptGuidelines: [ + "Use sandbox_device_attach to give the user direct IP access. After attaching, tell them to run `createos sb vpn up`.", + ], + parameters: Type.Object({ + network: Type.String({ description: "Network name or id" }), + }), + async execute(_id, { network }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const devices = await cli.listDevices(pi); + if (devices.length === 0) + return txt("No device registered. Run: createos sb devices register"); + const devId = devices[0].id ?? devices[0].device_id!; + await cli.attachDeviceToNetwork(pi, devId, network); + return txt( + `Device attached to network "${network}".\nRun \`createos sb vpn up\` to access sandbox IPs directly.`, + ); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + pi.registerTool({ + name: "sandbox_device_detach", + label: "Detach Device from Network", + description: "Remove the user's device from a network.", + promptSnippet: "Remove device access to a network", + promptGuidelines: [ + "Use sandbox_device_detach to revoke the user's direct access to a network.", + ], + parameters: Type.Object({ + network: Type.String({ description: "Network name or id" }), + }), + async execute(_id, { network }, signal) { + if (signal?.aborted) throw new Error("aborted"); + try { + const devices = await cli.listDevices(pi); + if (devices.length === 0) return txt("No device registered."); + const devId = devices[0].id ?? devices[0].device_id!; + await cli.detachDeviceFromNetwork(pi, devId, network); + return txt(`Device detached from network "${network}".`); + } catch (err) { + throw new Error(errmsg(err)); + } + }, + }); + + // --- User bash routing --- + + pi.on("user_bash", () => { + const active = getActive(); + if (active) return { operations: createBashOps(pi, active.sandboxId, active.cwd) }; + if (pi.getFlag("createos") === true) { + return { + result: { + output: + "CreateOS sandbox is unavailable โ€” the command was NOT run on your host. Restart Pi.", + exitCode: 1, + cancelled: false, + truncated: false, + }, + }; + } + return; + }); +} + +// --- Helpers --- + +function txt(text: string) { + return { content: [{ type: "text" as const, text }], details: undefined }; +} + +function errmsg(err: unknown): string { + return err instanceof Error ? err.message : String(err); +} + +function fmtBytes(bytes: number): string { + if (bytes === 0) return "0 B"; + const units = ["B", "KB", "MB", "GB", "TB"]; + const i = Math.floor(Math.log(bytes) / Math.log(1024)); + return `${(bytes / Math.pow(1024, i)).toFixed(1)} ${units[i]}`; +} diff --git a/packages/pi-extension/src/util.ts b/packages/pi-extension/src/util.ts new file mode 100644 index 0000000..c1baad9 --- /dev/null +++ b/packages/pi-extension/src/util.ts @@ -0,0 +1,13 @@ +/** Small helpers shared across the extension. */ + +export function shellQuote(arg: string): string { + return `'${arg.replace(/'/g, `'\\''`)}'`; +} + +export function shortId(id: string): string { + return id.slice(0, 12); +} + +export function joinPath(base: string, child: string): string { + return `${base.replace(/[/]+$/, "")}/${child.replace(/^[/]+/, "")}`; +} diff --git a/packages/pi-extension/tsconfig.json b/packages/pi-extension/tsconfig.json new file mode 100644 index 0000000..7637fef --- /dev/null +++ b/packages/pi-extension/tsconfig.json @@ -0,0 +1,14 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ESNext", + "moduleResolution": "bundler", + "strict": true, + "noEmit": true, + "allowImportingTsExtensions": true, + "esModuleInterop": true, + "skipLibCheck": true, + "resolveJsonModule": true + }, + "include": ["index.ts", "src/**/*.ts"] +}