diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 8f2f2da..0275464 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,6 +22,9 @@ jobs: rustup component add clippy rustup default stable + - name: Bun + uses: oven-sh/setup-bun@v2 + - name: Cache cargo uses: actions/cache@v4 with: @@ -39,9 +42,8 @@ jobs: - name: Build run: cargo build --workspace --all-targets - # Runs the unit + module-system suite. The Path B integration tests are - # #[ignore]: they need the ~13 MB esbuild bundle (git-ignored; built with - # `node js/build-pi-full.mjs`) and, for the turn tests, an API key + proxy — - # so they're driven locally, not in CI. - name: Test run: cargo test --workspace + + - name: App text behavior tests + run: bun test apps/_shared/text.test.ts diff --git a/.gitignore b/.gitignore index 1316c36..2a4cbf6 100644 --- a/.gitignore +++ b/.gitignore @@ -10,9 +10,3 @@ node_modules __pycache__/ *.py[cod] artifacts/screenshots/ - -# The raw full-pi bundle (~9 MB) is a build intermediate — rebuild with -# `node js/build.mjs`. The gzip (pi-full.bundle.js.gz, ~1.8 MB) IS committed: the -# crate embeds it (include_bytes!) so a plain `cargo build` — including from the -# vendored submodule in cat — works with only Rust, no Node. -crates/pocket-pi/js/pi-full.bundle.js diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 4770cbb..ce5fffc 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -1,143 +1,85 @@ # Pocket Pi architecture -Pocket Pi is one runtime family with two agent profiles and three hosts. +Pocket Pi is a complete Agent-native runtime for embedded and dedicated +devices. The Agent is a resident system actor with a persistent workspace, +native capabilities, schedules and Agent-native Apps—not a desktop application +or a generic Agent SDK. -| Run mode | Host | Pocket Pi profile | Purpose | -|---|---|---|---| -| Native Mac | `hosts/macos` | full `pi-coding-agent` | Normal desktop Pocket Pi | -| ESP32 simulator on Mac | `hosts/esp32-p4-sim` | embedded `pi-agent-core` + device UI/tools | Fast development of embedded contracts and product flows | -| Physical ESP32-P4 | `firmware/esp32-p4` | embedded `pi-agent-core` + device UI/tools | Real PocketJS/QuickJS Agent on the board | +The current implementation has one supported hardware target and one companion +development tool: -These are not three products. They are three compositions of Pocket Pi. The -simulator and physical firmware share the embedded runtime, UI, tool contracts -and interaction semantics. The simulator may use simpler host implementations; -physical hardware remains the final acceptance target. +| Role | Composition | Status | +| --- | --- | --- | +| Reference hardware | `firmware/esp32-p4` | ESP32-P4 is the first fully supported Pocket Pi target | +| Development simulator | `hosts/esp32-p4-sim` | Runs the ESP32-P4 product contracts on macOS; not a desktop product or hardware target | -```text - full desktop profile embedded product profile - crates/pocket-pi crates/pocket-pi-embedded - │ │ - hosts/macos crates/pocket-pi-device-ui - │ - ┌────────────┴────────────┐ - │ │ - firmware/esp32-p4 hosts/esp32-p4-sim -``` +Both compositions use the same resident `pi-agent-core` System App and PocketJS +App bundles. The simulator substitutes development adapters; only the physical +composition proves hardware behavior. + +The authoritative detailed design is +[`docs/agentos-architecture.md`](docs/agentos-architecture.md). ## Ownership -- `crates/pocket-pi` runs the full, unmodified `pi-coding-agent` with its - desktop Node/Web compatibility layer. -- `crates/pocket-pi-embedded` runs the bounded upstream `pi-agent-core` loop. - Native host traits provide model and tool capabilities. -- `crates/pocket-pi-device-ui` is the single source for the 720x1280 PocketJS - draw list, fonts, touch hit map, Chat, Workspace browser, keyboard, message - reader, device Settings and system status. The host supplies the mounted - workspace root. +- `crates/pocket-pi-agentos` owns App catalog, runtime lifecycle, foreground + selection, schedules, App Tool routing, and App-scoped FS/SQLite mounts. +- `crates/pocket-pi-app-pack` composes the build-selected App artifacts without + moving their product logic into the AgentOS runtime or host adapters. +- `crates/pocket-pi-embedded` provides the bounded JavaScript Agent Loop bridge. + In AgentOS hosts, the loop is loaded from the Pi Agent System App release into + the same PocketJS Guest as its Root View. +- `apps/pi-agent` owns the Root View and Agent Loop release artifacts. +- `apps/robinhood` and `apps/exa` own their Tools, Tasks, SQLite schemas, and + PocketJS Views. +- `crates/pocket-pi-tools` owns portable native workspace, bounded shell, time, + device, and Agent schedule Tools. - `crates/pocket-pi-protocols` owns model/provider transport protocols. -- `crates/pocket-pi-tools` owns the portable native ESP tool registry: - filesystem tools, bounded bash, workspace context, time and schedules. -- Each host is a composition root. It connects the embedded Agent, shared UI, - filesystem, input, display and model adapter. - -Dependencies point inward: hosts depend on the runtime, UI and protocols. The -runtime and UI do not depend on a host. External products can populate a UI -projection or register a native tool without putting provider clients in core. - -## Repository map - -```text -crates/pocket-pi/ full desktop pi-coding-agent runtime -crates/pocket-pi-embedded/ bounded pi-agent-core guest + native host traits -crates/pocket-pi-tools/ portable native workspace, shell, time and schedule tools -crates/pocket-pi-protocols/ provider request/response and streaming codecs -crates/pocket-pi-device-ui/ shared PocketJS embedded UI and interaction state -hosts/macos/ desktop composition root -hosts/esp32-p4-sim/ macOS implementation of the embedded host adapters -firmware/esp32-p4/ ESP-IDF hardware composition root and adapters -tools/uart_bridge/ Mac Codex/Claude streaming adapters -tools/uart-model-bridge.py thin UART framing and provisioning CLI -``` - -The split follows ownership, not product features. A native tool is implemented -once in `pocket-pi-tools`; a provider codec belongs in `pocket-pi-protocols`; -hardware access stays in a host. Optional applications such as Exa or -Robinhood should be separate tool/plugin adapters and must not become -dependencies of the embedded runtime or shared UI. - -## ESP32 and simulator parity - -The physical ESP32-P4 firmware and macOS simulator compile the same: +- Hosts own hardware, transport, credentials, and rendering adapters. -- `pocket-pi-embedded` Agent runtime; -- `pocket-pi-device-ui` Rust source and exact font atlases; -- `ScreenState::handle_tap` coordinate hit map; -- 720x1280 PocketJS draw-list viewport. +There is no legacy Rust product UI or general-purpose desktop runtime. The +simulator and ESP32-P4 render the same +PocketJS App bundles at a 720x1280 logical viewport. Rust firmware supplies the +display/touch driver and renders the selected App's DrawList. -The simulator maps a mouse pointer into the same 720x1280 coordinates used by -the touch controller and calls the same `handle_tap` method. It substitutes -macOS filesystem, wgpu display and model adapters; it does not emulate the -ESP32 CPU or peripherals. +## Runtime lifecycle -Both embedded hosts construct the same `CoreToolHost`. The simulator executes -filesystem and schedule operations against its Mac workspace directory using -the exact ESP constraints. Only `device.status`, `wifi status` and -`reboot` cross a small `PlatformTools` adapter. +The Pi Agent is a first-class, always-resident System App. Its Agent Loop, +context, Tool Registry, and Root View share one Guest and one App lifecycle. +Opening Robinhood or Exa changes only the foreground View; it does not restart +or replace the Agent. Model and native Tool transport complete asynchronously +and return events to that persistent Guest. -Parity is contract-level, not peripheral emulation. The simulator must support -the real embedded Pi Agent, core tool registry, workspace flows and schedules. -It may use macOS storage, networking, deterministic fixtures and simplified -telemetry to do so. CPU load, LittleFS capacity, Wi-Fi/NVS behavior, touch, LCD -scanout and other ESP-IDF details are implemented and accepted only on physical -hardware. +Ordinary Apps receive capability-scoped data roots. Pi Agent alone owns the +top-level `/workspace` and cross-App Tool Registry. -The full macOS host does not link the device UI. Embedded products enable Chat, -Files and Settings. External applications such as Exa or Robinhood belong in -separate plugin/tool and UI adapter crates; Pocket Pi core contains none of -their domain models, clients, credentials or tools. - -Settings follows the same host boundary as the rest of the device UI. PocketJS -emits `SettingsCommand` values and renders `SettingsProjection`; only the ESP -host calls ESP-IDF Wi-Fi/NVS/restart APIs. The simulator handles the same -commands with deterministic hardware projections. Password input is transient, -masked, cleared after submit, and never enters the Agent workspace or context. - -The physical model boundary has two implementations: - -- `UartBackend` sends framed model decisions to the Mac bridge, which can use a - logged-in Codex or Claude Code CLI. -- `WirelessBackend` sends direct HTTPS requests over board Wi-Fi to OpenAI, - OpenRouter, or Anthropic. - -Provider JSON and streaming decoders live in `pocket-pi-protocols`; ESP-IDF and -desktop HTTP transports stay in their hosts. - -The UART development path is deliberately layered. The ESP firmware owns only -line framing and `UartBackend`; `tools/uart-model-bridge.py` routes those frames; -`tools/uart_bridge` adapts a logged-in Codex app-server or Claude Code stream. -Only decoded top-level `text` is forwarded as UI deltas, while tool-decision -JSON remains private to the Pi Harness. Because ESP logs share UART0, the -firmware disables ESP-IDF logging after the Pi Harness ready handshake so logs -cannot corrupt model frames. - -Device time follows the same dual-adapter rule. A standalone Wi-Fi device uses -SNTP. The UART development bridge may seed Unix time at boot, after which the -same persistent `schedule.*` tools and wake loop run unchanged. - -## Runtime separation - -Agent work runs on a worker thread. UI and touch remain responsive while model -deltas are projected into `ChatProjection`. The UI never owns network access, -model credentials or broker credentials. +## Repository map -## Build entry point +```text +apps/ PocketJS System/ordinary App sources and bundles +crates/pocket-pi-agentos/ App Supervisor and AgentOS contracts +crates/pocket-pi-app-pack/ build-selected embedded App composition +crates/pocket-pi-embedded/ embedded pi-agent-core bridge and host traits +crates/pocket-pi-tools/ native workspace/shell/time/schedule Tools +crates/pocket-pi-protocols/ provider codecs +hosts/esp32-p4-sim/ macOS development simulator for ESP32-P4 contracts +firmware/esp32-p4/ first supported target and reference implementation +tools/uart_bridge/ Mac Codex/Claude streaming adapters +tools/uart-model-bridge.py UART framing and provisioning CLI +``` -`cargo xtask` is the orchestration layer: +## Build entry points ```sh -cargo xtask build macos +cargo xtask build agentos-apps cargo xtask build esp32-p4 cargo xtask build esp32-p4-sim cargo xtask run esp32-p4-sim cargo xtask snapshot esp32-p4-sim ``` + +Simulator proof, firmware compilation, and physical-board proof are separate +evidence tiers. ESP32-P4 is the current reference hardware and physical-board +proof remains its final acceptance tier. Future device targets must provide +their own native composition and physical validation without moving product +logic into firmware. diff --git a/Cargo.lock b/Cargo.lock index faedc92..a437427 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -169,17 +169,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "async-lock" -version = "3.4.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "290f7f2596bd5b78a9fec8088ccd89180d7f9f55b94b0576823bbbdc72ee8311" -dependencies = [ - "event-listener", - "event-listener-strategy", - "pin-project-lite", -] - [[package]] name = "atomic-waker" version = "1.1.2" @@ -205,14 +194,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "72b3254f16251a8381aa12e40e3c4d2f0199f8c6508fbecb9d91f575e0fbb8c6" [[package]] -name = "base64-simd" -version = "0.8.0" +name = "base64" +version = "0.23.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "339abbe78e73178762e23bea9dfd08e697eb3f3301cd4be981c0f78ba5859195" -dependencies = [ - "outref", - "vsimd", -] +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" [[package]] name = "bindgen" @@ -223,7 +208,7 @@ dependencies = [ "bitflags 2.13.1", "cexpr", "clang-sys", - "itertools 0.13.0", + "itertools", "log", "prettyplease", "proc-macro2", @@ -359,15 +344,6 @@ dependencies = [ "wayland-client", ] -[[package]] -name = "castaway" -version = "0.2.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "dec551ab6e7578819132c713a93c022a05d60159dc86e7a7050223577484c55a" -dependencies = [ - "rustversion", -] - [[package]] name = "cc" version = "1.3.0" @@ -401,19 +377,6 @@ version = "0.2.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f079e83a288787bcd14a6aea84cee5c87a67c5a3e660c30f557a3d24761b3527" -[[package]] -name = "chrono" -version = "0.4.45" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1aa79e62e7697b8e29b513a68abacf485adcd1fe8284a4316c5ae868e6633327" -dependencies = [ - "iana-time-zone", - "js-sys", - "num-traits", - "wasm-bindgen", - "windows-link", -] - [[package]] name = "clang-sys" version = "1.9.1" @@ -425,15 +388,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "cobs" -version = "0.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" -dependencies = [ - "thiserror 2.0.19", -] - [[package]] name = "codespan-reporting" version = "0.12.0" @@ -461,20 +415,6 @@ dependencies = [ "memchr", ] -[[package]] -name = "compact_str" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9dfdd1c2274d9aa354115b09dc9a901d6c5576818cdf70d14cae2bdb47df00ab" -dependencies = [ - "castaway", - "cfg-if", - "itoa", - "rustversion", - "ryu", - "static_assertions", -] - [[package]] name = "concurrent-queue" version = "2.5.0" @@ -533,12 +473,6 @@ dependencies = [ "libc", ] -[[package]] -name = "cow-utils" -version = "0.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "417bef24afe1460300965a25ff4a24b8b45ad011948302ec221e8a0a81eb2c79" - [[package]] name = "crc32fast" version = "1.5.0" @@ -623,29 +557,6 @@ dependencies = [ "libloading", ] -[[package]] -name = "dlopen2" -version = "0.8.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5e2c5bd4158e66d1e215c49b837e11d62f3267b30c92f1d171c4d3105e3dc4d4" -dependencies = [ - "dlopen2_derive", - "libc", - "once_cell", - "winapi", -] - -[[package]] -name = "dlopen2_derive" -version = "0.4.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0fbbb781877580993a8707ec48672673ec7b81eeba04cfd2310bd28c08e47c8f" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "document-features" version = "0.2.12" @@ -667,30 +578,12 @@ version = "0.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d8b14ccef22fc6f5a8f4d7d768562a182c04ce9a3b3157b91390b52ddfdf1a76" -[[package]] -name = "dragonbox_ecma" -version = "0.1.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "fd8e701084c37e7ef62d3f9e453b618130cbc0ef3573847785952a3ac3f746bf" - [[package]] name = "either" version = "1.16.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "91622ff5e7162018101f2fea40d6ebf4a78bbe5a49736a2020649edf9693679e" -[[package]] -name = "embedded-io" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef1a6892d9eef45c8fa6b9e0086428a2cca8491aca8f787c534a3d6d0bcb3ced" - -[[package]] -name = "embedded-io" -version = "0.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "edd0f118536f44f5ccd48bcb8b111bdc3de888b58c74639dfb034a357d0f206d" - [[package]] name = "env_filter" version = "2.0.0" @@ -731,24 +624,16 @@ dependencies = [ ] [[package]] -name = "event-listener" -version = "5.4.1" +name = "fallible-iterator" +version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e13b66accf52311f30a0db42147dadea9850cb48cd070028831ae5f5d4b856ab" -dependencies = [ - "concurrent-queue", - "pin-project-lite", -] +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" [[package]] -name = "event-listener-strategy" -version = "0.5.4" +name = "fallible-streaming-iterator" +version = "0.1.9" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8be9f3dfaaffdae2972880079a491a1a8bb7cbed0b8dd7a347f668b4150a3b93" -dependencies = [ - "event-listener", - "pin-project-lite", -] +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" [[package]] name = "fastrand" @@ -778,7 +663,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "843fba2746e448b37e26a819579957415c8cef339bf08564fe8b7ddbd959573c" dependencies = [ "crc32fast", - "miniz_oxide 0.8.9", + "miniz_oxide", ] [[package]] @@ -1083,6 +968,15 @@ dependencies = [ "foldhash 0.1.5", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash 0.2.0", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -1094,6 +988,15 @@ dependencies = [ "foldhash 0.2.0", ] +[[package]] +name = "hashlink" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "heck" version = "0.5.0" @@ -1112,36 +1015,6 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfa686283ad6dd069f105e5ab091b04c62850d3e4cf5d67debad1933f55023df" -[[package]] -name = "hmac-sha1-compact" -version = "1.1.7" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b0b3ba31f6dc772cc8221ce81dbbbd64fa1e668255a6737d95eeace59b5a8823" - -[[package]] -name = "iana-time-zone" -version = "0.1.65" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e31bc9ad994ba00e440a8aa5c9ef0ec67d5cb5e5cb0cc7f8b744a35b389cc470" -dependencies = [ - "android_system_properties", - "core-foundation-sys", - "iana-time-zone-haiku", - "js-sys", - "log", - "wasm-bindgen", - "windows-core 0.62.2", -] - -[[package]] -name = "iana-time-zone-haiku" -version = "0.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f31827a206f56af32e590ba56d5d2d085f558508192593743f16b2306495269f" -dependencies = [ - "cc", -] - [[package]] name = "icu_collections" version = "2.2.0" @@ -1297,15 +1170,6 @@ dependencies = [ "either", ] -[[package]] -name = "itertools" -version = "0.15.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8b4baf93f58d4425749ca49a51c50ebab072c5df6994d08fed93541c331481dc" -dependencies = [ - "either", -] - [[package]] name = "itoa" version = "1.0.18" @@ -1427,12 +1291,6 @@ dependencies = [ "wasm-bindgen", ] -[[package]] -name = "json-escape-simd" -version = "3.1.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c22a2041e3874a055a4eb03ea2395aaccdefa84ce75b31d542d72a741c3c6ad3" - [[package]] name = "khronos-egl" version = "6.0.0" @@ -1490,6 +1348,17 @@ dependencies = [ "redox_syscall 0.9.1", ] +[[package]] +name = "libsqlite3-sys" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1584,15 +1453,6 @@ dependencies = [ "simd-adler32", ] -[[package]] -name = "miniz_oxide" -version = "0.9.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b63fbc4a50860e98e7b2aa7804ded1db5cbc3aff9193adaff57a6931bf7c4b4c" -dependencies = [ - "adler2", -] - [[package]] name = "moxcms" version = "0.8.1" @@ -1677,31 +1537,6 @@ dependencies = [ "minimal-lexical", ] -[[package]] -name = "nonmax" -version = "0.5.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "610a5acd306ec67f907abe5567859a3c693fb9886eb1f012ab8f2a47bef3db51" - -[[package]] -name = "num-bigint" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93e7820bc0a80a0238e650327316f929ba18d5be054b647490a3a6a339f3e7c0" -dependencies = [ - "num-integer", - "num-traits", -] - -[[package]] -name = "num-integer" -version = "0.1.46" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7969661fd2958a5cb096e56c8e1ad0444ac2bbcd0061bd28660485a44879858f" -dependencies = [ - "num-traits", -] - [[package]] name = "num-traits" version = "0.2.19" @@ -1977,12 +1812,6 @@ dependencies = [ "num-traits", ] -[[package]] -name = "outref" -version = "0.5.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1a80800c0488c3a21695ea981a54918fbb37abf04f4d0720c453632255e2ff0e" - [[package]] name = "owned_ttf_parser" version = "0.25.1" @@ -1993,536 +1822,175 @@ dependencies = [ ] [[package]] -name = "owo-colors" -version = "4.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d211803b9b6b570f68772237e415a029d5a50c65d382910b879fb19d3271f94d" - -[[package]] -name = "oxc-browserslist" -version = "3.0.11" +name = "parking_lot" +version = "0.12.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "50a403a3c6be65be7bc7730d07d959ec6c143e4ff8117da485df78cd5582260a" +checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" dependencies = [ - "miniz_oxide 0.9.1", - "postcard", - "rustc-hash 2.1.3", - "serde", - "thiserror 2.0.19", + "lock_api", + "parking_lot_core", ] [[package]] -name = "oxc-miette" -version = "3.0.1" +name = "parking_lot_core" +version = "0.9.12" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2e0df30faa68797917ca4263e7a2f889ec829e4da2dcb3d6dc752f7a494180f3" +checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" dependencies = [ "cfg-if", - "memchr", - "owo-colors", - "oxc-miette-derive", - "textwrap", - "thiserror 2.0.19", - "unicode-segmentation", - "unicode-width", + "libc", + "redox_syscall 0.5.18", + "smallvec", + "windows-link", ] [[package]] -name = "oxc-miette-derive" -version = "3.0.1" +name = "paste" +version = "1.0.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acc072d11d45ebe7801459b4e829184ba0934d68027fdc51d327335b53a95a49" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] +checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" [[package]] -name = "oxc_allocator" -version = "0.140.0" +name = "percent-encoding" +version = "2.3.2" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8245ba555b465d3577732d5f9d9306babb0aaa7b80e97a2ce21f74fae442a3" -dependencies = [ - "allocator-api2", - "hashbrown 0.17.1", - "oxc_data_structures", - "rustc-hash 2.1.3", -] +checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] -name = "oxc_ast" -version = "0.140.0" +name = "pin-project" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3305400b90fff2a30b272b58fe6080d25369407b2ac37c4ac652996a9677efe0" +checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" dependencies = [ - "bitflags 2.13.1", - "oxc_allocator", - "oxc_ast_macros", - "oxc_data_structures", - "oxc_diagnostics", - "oxc_estree", - "oxc_regular_expression", - "oxc_span", - "oxc_str", - "oxc_syntax", + "pin-project-internal", ] [[package]] -name = "oxc_ast_macros" -version = "0.140.0" +name = "pin-project-internal" +version = "1.1.13" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ef089097e96b74388a8025a9247bdb12774b99f5971926d9dafbe84a78f9efb7" +checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" dependencies = [ - "phf", "proc-macro2", "quote", "syn 2.0.119", ] [[package]] -name = "oxc_ast_visit" -version = "0.140.0" +name = "pin-project-lite" +version = "0.2.17" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4640e6d0de2e0f6c820d1444a468d070c710111df76ce90a1694ac386641e133" -dependencies = [ - "oxc_allocator", - "oxc_ast", - "oxc_span", - "oxc_syntax", -] +checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "oxc_codegen" -version = "0.140.0" +name = "pkg-config" +version = "0.3.33" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "316d3f320fab2647d2657f1016de952baa196b566e0eaa704d224e3fc25ccd9a" -dependencies = [ - "bitflags 2.13.1", - "cow-utils", - "dragonbox_ecma", - "itoa", - "oxc_allocator", - "oxc_ast", - "oxc_data_structures", - "oxc_index", - "oxc_semantic", - "oxc_sourcemap", - "oxc_span", - "oxc_str", - "oxc_syntax", - "rustc-hash 2.1.3", -] +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" [[package]] -name = "oxc_compat" -version = "0.140.0" +name = "plain" +version = "0.2.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0f8ecc06571912f29113f7e5669f7bd2915d3932270b0d877a3679686f5f4c7d" -dependencies = [ - "cow-utils", - "oxc-browserslist", - "oxc_syntax", - "rustc-hash 2.1.3", - "serde", -] +checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" [[package]] -name = "oxc_data_structures" -version = "0.140.0" +name = "png" +version = "0.18.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "acfb6e704f95e115c0e9ea8bef791c3d915cd2fcd1643661cf0bf5172c818097" +checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" dependencies = [ - "ropey", + "bitflags 2.13.1", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", ] [[package]] -name = "oxc_diagnostics" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b33d8c59c55e2c581ff692468a6a7ff1f12477f0e687b52e27fc6f6346a5d77" +name = "pocket-db" +version = "0.1.0" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ - "cow-utils", - "oxc-miette", - "percent-encoding", + "anyhow", + "base64 0.23.1", + "pocket-mod", + "pocketjs-core", + "rusqlite", + "serde_json", ] [[package]] -name = "oxc_ecmascript" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2b3321b606c9217e8e5527cd01906b98e17f232acbb6fab7eac05236e7731d9a" +name = "pocket-fs" +version = "0.1.0" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ - "cow-utils", - "num-bigint", - "num-traits", - "oxc_allocator", - "oxc_ast", - "oxc_regular_expression", - "oxc_span", - "oxc_syntax", - "smallvec", + "anyhow", + "base64 0.23.1", + "pocket-mod", + "pocketjs-core", + "serde_json", ] [[package]] -name = "oxc_estree" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ad383009532dc6504973f02f68892b98f64be1c17e86115166b0a1223c7511e7" - -[[package]] -name = "oxc_index" -version = "5.0.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "191884bee6c3744909a51acc7d78d4ae370d817b25875b10642f632327b6296e" -dependencies = [ - "nonmax", - "serde", -] - -[[package]] -name = "oxc_parser" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8abd68f81349d37ea79f1d99d2370e15f282cc9fbe66e8544d072595744ab38e" -dependencies = [ - "bitflags 2.13.1", - "cow-utils", - "memchr", - "num-bigint", - "num-traits", - "oxc_allocator", - "oxc_ast", - "oxc_data_structures", - "oxc_diagnostics", - "oxc_ecmascript", - "oxc_regular_expression", - "oxc_span", - "oxc_str", - "oxc_syntax", - "rustc-hash 2.1.3", - "seq-macro", -] - -[[package]] -name = "oxc_regular_expression" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "4eba6a56b1c633963c09c383062e47fb8a99ff4a4ded6bd883fcf554f8e1a48f" -dependencies = [ - "bitflags 2.13.1", - "oxc_allocator", - "oxc_ast_macros", - "oxc_diagnostics", - "oxc_span", - "oxc_str", - "phf", - "rustc-hash 2.1.3", - "unicode-id-start", -] - -[[package]] -name = "oxc_semantic" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5967f96881e1694d10b453311fa681b4df0f38760628e1de613b046566cd8c8e" -dependencies = [ - "itertools 0.15.0", - "memchr", - "oxc_allocator", - "oxc_ast", - "oxc_ast_visit", - "oxc_data_structures", - "oxc_diagnostics", - "oxc_ecmascript", - "oxc_index", - "oxc_span", - "oxc_str", - "oxc_syntax", - "rustc-hash 2.1.3", - "self_cell", - "smallvec", -] - -[[package]] -name = "oxc_sourcemap" -version = "8.1.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b415102a94b483bbd76d13e850fe87961197e5795c79a8523ebec5d82025f94f" -dependencies = [ - "base64-simd", - "json-escape-simd", - "rustc-hash 2.1.3", - "serde", - "serde_json", -] - -[[package]] -name = "oxc_span" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e83fa0a0fe6e5e2f5abb173a64afe8db711bb612acbc002c663fd13b08a8cbf3" -dependencies = [ - "compact_str", - "oxc-miette", - "oxc_allocator", - "oxc_ast_macros", - "oxc_estree", - "oxc_str", -] - -[[package]] -name = "oxc_str" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d9e9531af3bc70e5eb75d196c01011d5bc0de1f6de73f0fc8bf343384353794f" -dependencies = [ - "compact_str", - "hashbrown 0.17.1", - "oxc_allocator", - "oxc_estree", -] - -[[package]] -name = "oxc_syntax" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1f81863cdf973b6bcdcdbbae225619c08966be41efb64dc0ee0fd4b574f4ec18" +name = "pocket-mod" +version = "0.1.0" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ - "bitflags 2.13.1", - "cow-utils", - "dragonbox_ecma", - "nonmax", - "oxc_allocator", - "oxc_ast_macros", - "oxc_estree", - "oxc_index", - "oxc_span", - "oxc_str", - "phf", - "unicode-id-start", + "anyhow", + "log", + "pocketjs-core", + "rquickjs", ] [[package]] -name = "oxc_transformer" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c8bb6f328641800296233b28448f32e5ebb5e72854b3e329f20447d5331c9216" +name = "pocket-net" +version = "0.1.0" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ - "base64 0.22.1", - "compact_str", - "hmac-sha1-compact", - "indexmap", - "itoa", - "memchr", - "oxc_allocator", - "oxc_ast", - "oxc_ast_visit", - "oxc_compat", - "oxc_data_structures", - "oxc_diagnostics", - "oxc_ecmascript", - "oxc_regular_expression", - "oxc_semantic", - "oxc_span", - "oxc_str", - "oxc_syntax", - "oxc_traverse", - "rustc-hash 2.1.3", + "anyhow", + "pocket-mod", + "pocketjs-core", "serde", "serde_json", ] [[package]] -name = "oxc_traverse" -version = "0.140.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3ab67032768715dda6647be4043e6a8f9762a3a9090ca7a5d3795339ce87e18f" -dependencies = [ - "itoa", - "oxc_allocator", - "oxc_ast", - "oxc_ast_visit", - "oxc_data_structures", - "oxc_ecmascript", - "oxc_semantic", - "oxc_span", - "oxc_str", - "oxc_syntax", - "rustc-hash 2.1.3", -] - -[[package]] -name = "parking_lot" -version = "0.12.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93857453250e3077bd71ff98b6a65ea6621a19bb0f559a85248955ac12c45a1a" -dependencies = [ - "lock_api", - "parking_lot_core", -] - -[[package]] -name = "parking_lot_core" -version = "0.9.12" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2621685985a2ebf1c516881c026032ac7deafcda1a2c9b7850dc81e3dfcb64c1" -dependencies = [ - "cfg-if", - "libc", - "redox_syscall 0.5.18", - "smallvec", - "windows-link", -] - -[[package]] -name = "paste" -version = "1.0.15" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a" - -[[package]] -name = "percent-encoding" -version = "2.3.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" - -[[package]] -name = "phf" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "010378780309880b08997fae13be7834dba947d36393bd372f2b1556deb2a2f6" -dependencies = [ - "phf_macros", - "phf_shared", - "serde", -] - -[[package]] -name = "phf_generator" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "aeb62e0959d5a1bebc965f4d15d9e2b7cea002b6b0f5ba8cde6cc26738467100" -dependencies = [ - "fastrand", - "phf_shared", -] - -[[package]] -name = "phf_macros" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5fa8d0ca26d424d27630da600c6624696e7dec8bf7b3b492b383c5dc49e5e085" -dependencies = [ - "phf_generator", - "phf_shared", - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "phf_shared" -version = "0.14.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c6fd9027e2d9319be6349febd1db4e8d02aa544921200c9b777720ac34a3aa89" -dependencies = [ - "siphasher", -] - -[[package]] -name = "pin-project" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2466b2336ed02bcdca6b294417127b90ec92038d1d5c4fbeac971a922e0e0924" -dependencies = [ - "pin-project-internal", -] - -[[package]] -name = "pin-project-internal" -version = "1.1.13" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c96395f0a926bc13b1c17622aaddda1ecb55d49c8f1bf9777e4d877800a43f8b" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - -[[package]] -name = "pin-project-lite" -version = "0.2.17" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" - -[[package]] -name = "pkg-config" -version = "0.3.33" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" - -[[package]] -name = "plain" -version = "0.2.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b4596b6d070b27117e987119b4dac604f3c58cfb0b191112e24771b2faeac1a6" - -[[package]] -name = "png" -version = "0.18.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "60769b8b31b2a9f263dae2776c37b1b28ae246943cf719eb6946a1db05128a61" -dependencies = [ - "bitflags 2.13.1", - "crc32fast", - "fdeflate", - "flate2", - "miniz_oxide 0.8.9", -] - -[[package]] -name = "pocket-mod" +name = "pocket-pi-agentos" version = "0.1.0" -source = "git+https://github.com/pocket-stack/pocketjs.git?rev=4c5dc9ef1dd26e6f49b036c210931d399f2b52b2#4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" dependencies = [ "anyhow", "log", + "pocket-db", + "pocket-fs", + "pocket-mod", + "pocket-net", + "pocket-pi-embedded", + "pocket-ui-surface", "pocketjs-core", - "rquickjs", -] - -[[package]] -name = "pocket-pi" -version = "0.1.0" -dependencies = [ - "env_logger", - "flate2", - "log", - "oxc_allocator", - "oxc_codegen", - "oxc_parser", - "oxc_semantic", - "oxc_span", - "oxc_transformer", - "regex", - "rquickjs", + "serde", "serde_json", - "ureq", + "tempfile", ] [[package]] -name = "pocket-pi-device-ui" +name = "pocket-pi-app-pack" version = "0.1.0" dependencies = [ - "pocket-pi-protocols", - "pocketjs-core", + "anyhow", + "pocket-db", + "pocket-pi-agentos", + "pocket-pi-embedded", + "serde_json", + "tempfile", ] [[package]] name = "pocket-pi-embedded" version = "0.1.0" dependencies = [ + "pocket-mod", + "pocket-pi-protocols", "rquickjs", "serde_json", ] @@ -2534,13 +2002,14 @@ dependencies = [ "anyhow", "env_logger", "log", - "pocket-pi-device-ui", + "pocket-db", + "pocket-pi-agentos", + "pocket-pi-app-pack", "pocket-pi-embedded", "pocket-pi-protocols", "pocket-pi-tools", "pocket-ui-wgpu", "pocket3d", - "pocketjs-core", "serde_json", "tempfile", "ureq", @@ -2548,14 +2017,6 @@ dependencies = [ "winit", ] -[[package]] -name = "pocket-pi-macos" -version = "0.1.0" -dependencies = [ - "pocket-pi", - "serde_json", -] - [[package]] name = "pocket-pi-protocols" version = "0.1.0" @@ -2579,7 +2040,7 @@ dependencies = [ [[package]] name = "pocket-ui-surface" version = "0.1.0" -source = "git+https://github.com/pocket-stack/pocketjs.git?rev=4c5dc9ef1dd26e6f49b036c210931d399f2b52b2#4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ "anyhow", "log", @@ -2590,7 +2051,7 @@ dependencies = [ [[package]] name = "pocket-ui-wgpu" version = "0.1.0" -source = "git+https://github.com/pocket-stack/pocketjs.git?rev=4c5dc9ef1dd26e6f49b036c210931d399f2b52b2#4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ "anyhow", "bytemuck", @@ -2603,7 +2064,7 @@ dependencies = [ [[package]] name = "pocket3d" version = "0.1.0" -source = "git+https://github.com/pocket-stack/pocketjs.git?rev=4c5dc9ef1dd26e6f49b036c210931d399f2b52b2#4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ "anyhow", "bytemuck", @@ -2622,7 +2083,7 @@ dependencies = [ [[package]] name = "pocket3d-bsp" version = "0.1.0" -source = "git+https://github.com/pocket-stack/pocketjs.git?rev=4c5dc9ef1dd26e6f49b036c210931d399f2b52b2#4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ "anyhow", "glam", @@ -2632,7 +2093,7 @@ dependencies = [ [[package]] name = "pocketjs-core" version = "0.1.0" -source = "git+https://github.com/pocket-stack/pocketjs.git?rev=4c5dc9ef1dd26e6f49b036c210931d399f2b52b2#4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ "taffy", ] @@ -2672,18 +2133,6 @@ dependencies = [ "portable-atomic", ] -[[package]] -name = "postcard" -version = "1.1.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" -dependencies = [ - "cobs", - "embedded-io 0.4.0", - "embedded-io 0.6.1", - "serde", -] - [[package]] name = "potential_utf" version = "0.1.5" @@ -2866,24 +2315,12 @@ dependencies = [ "windows-sys 0.52.0", ] -[[package]] -name = "ropey" -version = "1.6.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "93411e420bcd1a75ddd1dc3caf18c23155eda2c090631a85af21ba19e97093b5" -dependencies = [ - "smallvec", - "str_indices", -] - [[package]] name = "rquickjs" version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7ce6b626d30ecbedaaf8097a04982bc3b081f958f5bdf9b8796be4ac00ee48bb" dependencies = [ - "either", - "indexmap", "rquickjs-core", "rquickjs-macro", ] @@ -2894,13 +2331,7 @@ version = "0.12.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d9334dd11023d5ae6c751f64496510a576208802ed1d022ef94afa7639c2c55e" dependencies = [ - "async-lock", - "chrono", - "dlopen2", - "either", "hashbrown 0.17.1", - "indexmap", - "phf", "relative-path", "rquickjs-sys", ] @@ -2915,8 +2346,6 @@ dependencies = [ "fnv", "ident_case", "indexmap", - "phf_generator", - "phf_shared", "proc-macro-crate", "proc-macro2", "quote", @@ -2934,6 +2363,31 @@ dependencies = [ "cc", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.19", +] + +[[package]] +name = "rusqlite" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" +dependencies = [ + "bitflags 2.13.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + [[package]] name = "rustc-hash" version = "1.1.0" @@ -3022,12 +2476,6 @@ version = "1.0.23" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cf54715a573b99ac80df0bc206da022bcd442c974952c7b9720069370852e21f" -[[package]] -name = "ryu" -version = "1.0.23" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" - [[package]] name = "same-file" version = "1.0.6" @@ -3062,24 +2510,12 @@ dependencies = [ "tiny-skia", ] -[[package]] -name = "self_cell" -version = "1.3.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "2ab42ca02749e120097e328d91d415325bdf43b1c72c4c8badf37375fe40a813" - [[package]] name = "semver" version = "1.0.28" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8a7852d02fc848982e0c167ef163aaff9cd91dc640ba85e263cb1ce46fae51cd" -[[package]] -name = "seq-macro" -version = "0.3.6" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1bc711410fbe7399f390ca1c3b60ad0f53f80e95c5eb935e52268a0e2cd49acc" - [[package]] name = "serde" version = "1.0.229" @@ -3157,12 +2593,6 @@ version = "0.1.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e3a9fe34e3e7a50316060351f37187a3f546bce95496156754b601a5fa71b76e" -[[package]] -name = "siphasher" -version = "1.0.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "8ee5873ec9cce0195efcb7a4e9507a04cd49aec9c83d0389df45b1ef7ba2e649" - [[package]] name = "slab" version = "0.4.12" @@ -3183,15 +2613,6 @@ name = "smallvec" version = "1.15.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" -dependencies = [ - "serde", -] - -[[package]] -name = "smawk" -version = "0.3.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "e8e2fb0f499abb4d162f2bedad68f5ef91a1682b5a03596ddb67efd37768d100" [[package]] name = "smithay-client-toolkit" @@ -3236,6 +2657,18 @@ dependencies = [ "bitflags 2.13.1", ] +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -3248,12 +2681,6 @@ version = "1.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a2eb9349b6444b326872e140eb1cf5e7c522154d69e7a0ffb0fb81c06b37543f" -[[package]] -name = "str_indices" -version = "0.4.4" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "d08889ec5408683408db66ad89e0e1f93dff55c73a4ccc71c427d5b277ee47e6" - [[package]] name = "strict-num" version = "0.1.1" @@ -3355,17 +2782,6 @@ dependencies = [ "winapi-util", ] -[[package]] -name = "textwrap" -version = "0.16.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "c13547615a44dc9c452a8a534638acdf07120d4b6847c8178705da06306a3057" -dependencies = [ - "smawk", - "unicode-linebreak", - "unicode-width", -] - [[package]] name = "thiserror" version = "1.0.69" @@ -3493,24 +2909,12 @@ version = "0.25.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d2df906b07856748fa3f6e0ad0cbaa047052d4a7dd609e231c4f72cee8c36f31" -[[package]] -name = "unicode-id-start" -version = "1.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "81b79ad29b5e19de4260020f8919b443b2ef0277d242ce532ec7b7a2cc8b6007" - [[package]] name = "unicode-ident" version = "1.0.24" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e6e4313cd5fcd3dad5cafa179702e2b244f760991f45397d14d4ebf38247da75" -[[package]] -name = "unicode-linebreak" -version = "0.1.5" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3b09c83c3c29d37506a3e260c08c03743a6bb66a9cd432c6934ab501a190571f" - [[package]] name = "unicode-segmentation" version = "1.13.3" @@ -3578,16 +2982,16 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "06abde3611657adf66d383f00b093d7faecc7fa57071cce2578660c9f1010821" [[package]] -name = "version_check" -version = "0.9.5" +name = "vcpkg" +version = "0.2.15" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" [[package]] -name = "vsimd" -version = "0.8.0" +name = "version_check" +version = "0.9.5" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c3082ca00d5a5ef149bb8b555a72ae84c9c59f7250f013ac822ac2e49b19c64" +checksum = "0b928f33d975fc6ad9f86c8f283853ad26bdd5b10b7f1542aa2fa15e2289105a" [[package]] name = "walkdir" @@ -3946,7 +3350,7 @@ dependencies = [ "web-sys", "wgpu-types", "windows", - "windows-core 0.58.0", + "windows-core", ] [[package]] @@ -3963,22 +3367,6 @@ dependencies = [ "web-sys", ] -[[package]] -name = "winapi" -version = "0.3.9" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "5c839a674fcd7a98952e593242ea400abe93992746761e38641405d28b00f419" -dependencies = [ - "winapi-i686-pc-windows-gnu", - "winapi-x86_64-pc-windows-gnu", -] - -[[package]] -name = "winapi-i686-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "ac3b87c63620426dd9b991e5ce0329eff545bccbbb34f3be09ff6fb6ab51b7b6" - [[package]] name = "winapi-util" version = "0.1.11" @@ -3988,19 +3376,13 @@ dependencies = [ "windows-sys 0.61.2", ] -[[package]] -name = "winapi-x86_64-pc-windows-gnu" -version = "0.4.0" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "712e227841d057c1ee1cd2fb22fa7e5a5461ae8e48fa2ca79ec42cfc1931183f" - [[package]] name = "windows" version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dd04d41d93c4992d421894c18c8b43496aa748dd4c081bac0dc93eb0489272b6" dependencies = [ - "windows-core 0.58.0", + "windows-core", "windows-targets", ] @@ -4010,26 +3392,13 @@ version = "0.58.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "6ba6d44ec8c2591c134257ce647b7ea6b20335bf6379a27dac5f1641fcf59f99" dependencies = [ - "windows-implement 0.58.0", - "windows-interface 0.58.0", - "windows-result 0.2.0", - "windows-strings 0.1.0", + "windows-implement", + "windows-interface", + "windows-result", + "windows-strings", "windows-targets", ] -[[package]] -name = "windows-core" -version = "0.62.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b8e83a14d34d0623b51dce9581199302a221863196a1dde71a7663a4c2be9deb" -dependencies = [ - "windows-implement 0.60.2", - "windows-interface 0.59.3", - "windows-link", - "windows-result 0.4.1", - "windows-strings 0.5.1", -] - [[package]] name = "windows-implement" version = "0.58.0" @@ -4041,17 +3410,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "windows-implement" -version = "0.60.2" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "053e2e040ab57b9dc951b72c264860db7eb3b0200ba345b4e4c3b14f67855ddf" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "windows-interface" version = "0.58.0" @@ -4063,17 +3421,6 @@ dependencies = [ "syn 2.0.119", ] -[[package]] -name = "windows-interface" -version = "0.59.3" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "3f316c4a2570ba26bbec722032c4099d8c8bc095efccdc15688708623367e358" -dependencies = [ - "proc-macro2", - "quote", - "syn 2.0.119", -] - [[package]] name = "windows-link" version = "0.2.1" @@ -4089,34 +3436,16 @@ dependencies = [ "windows-targets", ] -[[package]] -name = "windows-result" -version = "0.4.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7781fa89eaf60850ac3d2da7af8e5242a5ea78d1a11c49bf2910bb5a73853eb5" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-strings" version = "0.1.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4cd9b125c486025df0eabcb585e62173c6c9eddcec5d117d3b6e8c30e2ee4d10" dependencies = [ - "windows-result 0.2.0", + "windows-result", "windows-targets", ] -[[package]] -name = "windows-strings" -version = "0.5.1" -source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "7837d08f69c77cf6b07689544538e017c1bfcf57e34b4c0ff58e6c2cd3b37091" -dependencies = [ - "windows-link", -] - [[package]] name = "windows-sys" version = "0.52.0" diff --git a/Cargo.toml b/Cargo.toml index 9f7d3ba..8c7977b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,12 +1,11 @@ [workspace] resolver = "2" members = [ - "crates/pocket-pi", + "crates/pocket-pi-app-pack", + "crates/pocket-pi-agentos", "crates/pocket-pi-embedded", - "crates/pocket-pi-device-ui", "crates/pocket-pi-protocols", "crates/pocket-pi-tools", - "hosts/macos", "hosts/esp32-p4-sim", "tools/xtask", ] @@ -22,8 +21,33 @@ repository = "https://github.com/pocket-stack/pocket-pi" [workspace.dependencies] anyhow = "1" log = "0.4" +pocket-pi-protocols = { path = "crates/pocket-pi-protocols" } serde = { version = "1", default-features = false, features = ["derive", "alloc"] } serde_json = { version = "1", default-features = false, features = ["alloc"] } +[workspace.dependencies.pocketjs-core] +git = "https://github.com/pocket-stack/pocketjs.git" +rev = "9c809bbd047ddc75c27caa4990951a78d942477a" + +[workspace.dependencies.pocket-mod] +git = "https://github.com/pocket-stack/pocketjs.git" +rev = "9c809bbd047ddc75c27caa4990951a78d942477a" + +[workspace.dependencies.pocket-ui-surface] +git = "https://github.com/pocket-stack/pocketjs.git" +rev = "9c809bbd047ddc75c27caa4990951a78d942477a" + +[workspace.dependencies.pocket-db] +git = "https://github.com/pocket-stack/pocketjs.git" +rev = "9c809bbd047ddc75c27caa4990951a78d942477a" + +[workspace.dependencies.pocket-fs] +git = "https://github.com/pocket-stack/pocketjs.git" +rev = "9c809bbd047ddc75c27caa4990951a78d942477a" + +[workspace.dependencies.pocket-net] +git = "https://github.com/pocket-stack/pocketjs.git" +rev = "9c809bbd047ddc75c27caa4990951a78d942477a" + [profile.release] opt-level = 2 diff --git a/README.md b/README.md index 160f91e..801e39a 100644 --- a/README.md +++ b/README.md @@ -1,118 +1,85 @@ # Pocket Pi -Pocket Pi runs the [Pi coding agent](https://github.com/badlogic/pi-mono) in -QuickJS. It provides a full desktop runtime and an embedded profile whose -touch UI is built with [PocketJS](https://github.com/pocket-stack/pocketjs) for -the ESP32-P4 product. - -This repository contains one Pocket Pi runtime family with two profiles and -exactly three supported run modes: - -| Run mode | Agent profile | What it is for | -|---|---|---| -| Native macOS | Full `pi-coding-agent` | Normal desktop Pocket Pi, including sessions and extensions | -| ESP32 simulator on macOS | Embedded `pi-agent-core` | Fast development of the ESP32 product UI, tools and Agent flows | -| Physical ESP32-P4 | Embedded `pi-agent-core` | The real standalone [PocketJS](https://github.com/pocket-stack/pocketjs)/QuickJS device | - -They are not three forks. The simulator and firmware compile the same embedded -Agent, device UI, tool contracts and interaction state. Only their platform -adapters differ. - -> **Project status:** the macOS host, ESP32 simulator and physical -> Waveshare ESP32-P4 target all have working end-to-end paths. The embedded -> port is still board-specific and under active development; see +Pocket Pi is a complete Agent-native runtime for embedded and dedicated +devices. It makes the Agent a resident system actor instead of a user-launched +application on a general-purpose desktop or mobile OS. One device runtime +combines a persistent Pi Agent, local `/workspace`, native tools, schedules, +Agent-native Apps, local SQLite state and a PocketJS UI. + +Pocket Pi builds on: + +- [`pi-agent-core`](https://github.com/badlogic/pi-mono) for the Agent Loop and + Tool Call harness; +- [PocketJS](https://github.com/pocket-stack/pocketjs) and QuickJS for embedded + Apps and device UI; +- native target hosts for storage, credentials, networking, rendering, device + capabilities and lifecycle. + +**ESP32-P4 is the first fully supported hardware target and the current +reference implementation.** The macOS ESP32-P4 simulator is a development and +product-contract testing tool; it is not a desktop Pocket Pi product or a +second hardware target. + +> **Project status:** the Waveshare ESP32-P4 target has working end-to-end +> Agent, workspace, schedule, App, display/touch and provider paths. The first +> target remains board-specific and under active development; see > [Current validation](#current-validation) for the exact evidence and limits. -## Why two profiles? +## One complete device runtime -The desktop runtime can afford a broad Node/Web compatibility layer and embeds -the full, unmodified `pi-coding-agent`. An ESP32 cannot carry that entire -desktop platform unchanged, so the embedded profile runs upstream -`pi-agent-core` in QuickJS and implements the Pi tools as small native Rust -capabilities. - -The architecture keeps the Pi Harness design in both profiles: +The current runtime runs upstream `pi-agent-core` in QuickJS and implements Pi +tools as bounded native capabilities. Its architecture preserves the Pi Harness +contracts while adding the device-level responsibilities Pocket Pi owns: - the model decides when to call a tool; - tools have explicit schemas and return structured results; - the Agent loop is separate from model transports and platform APIs; -- workspaces and schedules are capabilities, not UI or prompt special cases. +- workspace and schedules are durable device capabilities; +- Apps combine Agent-facing Tools, local state, autonomous Tasks and a fixed + human-facing View; +- the native host owns credentials, transport, hardware and lifecycle. -The macOS simulator is a product-contract simulator, not an ESP32 CPU or -peripheral emulator. It gives the embedded Agent the same UI, tool registry and -workspace rules while replacing LCD, touch, storage and networking with macOS -adapters. Physical hardware remains the final acceptance target. +The companion simulator runs the same AgentOS, PocketJS App bundles, UI, Tool +registry and workspace contracts while replacing hardware adapters with macOS +implementations. It is not an ESP32 CPU/peripheral emulator, and simulator +success never replaces physical-device acceptance. ## Quick start ### Prerequisites -- Rust stable for the native macOS host and simulator; +- Rust stable for the development simulator; - Bun for rebuilding the embedded JavaScript guest; - a logged-in `codex` CLI for the simulator's local Codex backend; - the esp-rs/ESP-IDF toolchain, `nightly-2026-05-01`, and `espflash` for the - physical ESP32-P4 target. + supported ESP32-P4 target. The current firmware target is the **Waveshare ESP32-P4-WIFI6-Touch-LCD-5**. -### Build all three modes +### Build the device and development simulator ```sh -cargo xtask build macos +cargo xtask build agentos-apps cargo xtask build esp32-p4-sim cargo xtask build esp32-p4 ``` -### 1. Native Pocket Pi on macOS - -```sh -cargo xtask run macos 'Who are you?' -``` - -The host selects a model in this order: - -1. `OPENAI_API_KEY` with optional `OPENAI_MODEL`; -2. `ANTHROPIC_API_KEY` with optional `ANTHROPIC_MODEL`; -3. Pi's deterministic Faux Provider when neither key exists. - -The Faux Provider is an offline development fallback. It still runs through a -real `createAgentSession`; it is not evidence of a live provider request. - -### 2. ESP32 Pocket Pi simulator on macOS - -The simulator defaults to the Mac's existing Codex Coding Plan login: +Pi Agent is always included. Select ordinary Apps at build time with one flag; +omitting it keeps the default Robinhood + Exa image: ```sh -cargo xtask run esp32-p4-sim \ - --backend codex \ - --workspace target/esp32-workspace -``` - -Direct API-key backends are also available: - -```sh -OPENAI_API_KEY=... \ - cargo xtask run esp32-p4-sim --backend openai --model gpt-5.6 - -OPENROUTER_API_KEY=... \ - cargo xtask run esp32-p4-sim \ - --backend openrouter --model openai/gpt-5.6 - -ANTHROPIC_API_KEY=... \ - cargo xtask run esp32-p4-sim \ - --backend anthropic --model claude-sonnet-4-6 +cargo xtask build esp32-p4 --apps robinhood,exa +cargo xtask build esp32-p4 --apps robinhood +cargo xtask build esp32-p4 --apps exa +cargo xtask build esp32-p4 --apps none ``` -The window uses the ESP32's 720x1280 coordinate system. Mouse input is mapped -through the same hit-testing code as physical touch input. Generate a -deterministic UI snapshot with: +`--apps none` builds the smallest Pocket Pi device image: the resident Pi Agent, +workspace, native tools, schedules and Root View without ordinary Apps. It is +not a generic `pi-agent-core` SDK or desktop harness. -```sh -cargo xtask snapshot esp32-p4-sim -``` - -### 3. Physical Pocket Pi on ESP32-P4 +### 1. Pocket Pi on ESP32-P4 Build and flash the release firmware: @@ -121,6 +88,7 @@ cargo xtask build esp32-p4 DEVICE_PORT=/dev/cu.usbmodem... espflash flash --baud 921600 --port "$DEVICE_PORT" \ + --partition-table firmware/esp32-p4/partitions.csv \ firmware/esp32-p4/target/riscv32imafc-esp-espidf/release/pocket-pi-p4 ``` @@ -158,21 +126,68 @@ interactively. Wi-Fi can subsequently be changed from the device Settings UI without reflashing. Credentials are kept out of Agent workspace files and PocketJS UI state. +The UART bridge automatically reuses an existing authorized Robinhood session +from Keychain and injects its access token into the board's RAM-only boot +configuration. `--provision-robinhood` is only the interactive fallback when no +saved authorization exists. + +### 2. Develop with the ESP32-P4 simulator on macOS + +The simulator defaults to the Mac's existing Codex Coding Plan login: + +```sh +cargo xtask run esp32-p4-sim \ + --backend codex \ + --workspace target/esp32-workspace +``` + +Direct API-key backends are also available: + +```sh +OPENAI_API_KEY=... \ + cargo xtask run esp32-p4-sim --backend openai --model gpt-5.6 + +OPENROUTER_API_KEY=... \ + cargo xtask run esp32-p4-sim \ + --backend openrouter --model openai/gpt-5.6 + +ANTHROPIC_API_KEY=... \ + cargo xtask run esp32-p4-sim \ + --backend anthropic --model claude-sonnet-4-6 + +DEEPSEEK_API_KEY=... DEEPSEEK_THINKING_LEVEL=xhigh \ + cargo xtask run esp32-p4-sim --backend deepseek +``` + +The window uses the ESP32-P4 product's 720x1280 coordinate system. Mouse input +is mapped through the same hit-testing code as physical touch input. Generate a +deterministic UI snapshot with: + +```sh +cargo xtask snapshot esp32-p4-sim +``` + ## Model backends Backends belong to their host composition, not to the Agent core: | Host | Supported backends | |---|---| -| Native macOS | OpenAI, Anthropic, offline Faux Provider | -| ESP32 simulator | local Codex, OpenAI, OpenRouter, Anthropic | -| Physical ESP32-P4 | UART to Mac Codex or Claude Code; wireless OpenAI, OpenRouter or Anthropic | +| ESP32 simulator | local Codex, OpenAI, OpenRouter, Anthropic, DeepSeek V4 | +| Physical ESP32-P4 | UART to Mac Codex or Claude Code; wireless OpenAI, OpenRouter, Anthropic or DeepSeek V4 | -`UartBackend` and `WirelessBackend` expose the same streaming model boundary to -the embedded Agent. Provider request/streaming codecs live in +`UartBackend` and `WirelessBackend` implement the same model-completion +contract. Wireless providers may emit progress events internally; the current +UART bridge coalesces provider chunks into one final framed result before it +reaches the device. Provider request/streaming codecs live in `pocket-pi-protocols`; serial framing, desktop CLIs and ESP-IDF HTTPS stay in their platform layers. +DeepSeek defaults to `deepseek-v4-flash` with thinking level `high`. Set +`DEEPSEEK_THINKING_LEVEL=xhigh` in the simulator, or pass +`--thinking-level xhigh` while provisioning the physical board, to request the +provider's `max` reasoning effort. + ## Embedded Agent capabilities The simulator and physical firmware register the same portable core tools: @@ -195,100 +210,70 @@ memory and can create or revise its own recurring schedules. The shared [PocketJS](https://github.com/pocket-stack/pocketjs) device UI includes: -- Chat with streamed replies, recent-turn history and a full-message reader; +- Chat with provider-dependent incremental replies, recent-turn history and a + full-message reader; - Files with workspace metadata, file viewing and scrolling; - Settings with Wi-Fi scanning, selection and password entry; -- touch keyboard, system telemetry and next-schedule status. - -Settings is an embedded-device feature and is not linked into native macOS -Pocket Pi. This repository does not include Robinhood or Exa UI, clients, -credentials or tools. External products add their own plugin/tool and UI -adapters without changing Agent core. +- touch keyboard and next-schedule status. -## Desktop profile - -`crates/pocket-pi` embeds the full, unmodified `pi-coding-agent` bundle in one -QuickJS realm. It provides enough Node/Web compatibility for Pi sessions, -extensions, native tools and streaming model turns without requiring Node or -Bun on the destination machine. - -Desktop extensions use Pi's normal `(pi) => void` factory and may register -tools and lifecycle hooks. Pocket Pi transpiles TypeScript with oxc and injects -the factory through Pi's `extensionFactories` seam; Pi itself is not patched. - -The desktop compatibility layer includes real filesystem, path, buffer, -events, stream, process, synchronous subprocess and global streaming `fetch` -support. Socket servers, worker threads and several lower-level Node builtins -remain stubs. These desktop APIs are intentionally **not** part of the embedded -ESP32 contract. - -The full Pi bundle is committed and embedded, so normal Rust builds do not need -Node. Rebuild it only after changing the desktop JavaScript guest: - -```sh -npm --prefix js ci -npm --prefix js run build -``` +The embedded AgentOS ships Pi Agent as its resident System App and can include +Robinhood and Exa as build-selected ordinary Apps. Each ordinary App owns its Tool catalog, Data Action, +SQLite projection and fixed PocketJS View; hosts provide only scoped +credentials, transport and hardware adapters. ## Repository map ```text -crates/pocket-pi/ full desktop pi-coding-agent runtime -crates/pocket-pi-embedded/ embedded pi-agent-core guest and host traits +crates/pocket-pi-embedded/ bounded Agent Loop bridge used by device runtimes crates/pocket-pi-tools/ portable workspace, shell, time and schedule tools crates/pocket-pi-protocols/ model request, response and streaming codecs -crates/pocket-pi-device-ui/ shared PocketJS device UI and interaction state -hosts/macos/ native desktop composition root -hosts/esp32-p4-sim/ macOS adapters for the embedded product -firmware/esp32-p4/ ESP-IDF hardware composition root and adapters +crates/pocket-pi-agentos/ App Supervisor, System App lifecycle and App contracts +crates/pocket-pi-app-pack/ build-selected embedded App composition +hosts/esp32-p4-sim/ macOS development simulator for ESP32-P4 contracts +firmware/esp32-p4/ first supported device and reference implementation tools/uart_bridge/ Mac Codex and Claude Code streaming adapters tools/uart-model-bridge.py UART framing and provisioning CLI ``` Dependencies point inward: hosts depend on shared runtimes, tools, UI and protocols; those shared crates do not depend on a host. Hardware APIs remain in -the firmware, macOS APIs remain in hosts/tools, and optional external services -remain plugins. +the firmware, simulator adapters remain in `hosts/esp32-p4-sim`, and optional +external services remain Apps. See [ARCHITECTURE.md](ARCHITECTURE.md) for ownership and lifecycle boundaries, and [docs/esp32-p4-port.md](docs/esp32-p4-port.md) for board-specific details. ## Current validation -The following three paths were exercised end-to-end on **2026-08-05**: - -| Mode | Exercised path | Result | -|---|---|---| -| Native macOS | Full `PiRuntime` and `createAgentSession` with Pi's offline Faux Provider | Passed | -| ESP32 simulator | Embedded Agent + local Codex; `write -> read -> recurring schedule.set -> schedule.list`; shared UI rendering | Passed | -| Physical ESP32-P4 | Release firmware + UART Mac Codex; streamed reply; real LittleFS `write/read`; recurring schedule creation/listing | Passed | - -The workspace suite also completed with 46 passing tests and 3 ignored tests, -and workspace clippy passed with warnings denied. These results validate the -recorded revision; they are not a substitute for CI on later changes. +On **2026-08-12**, the embedded-only workspace completed 30 Rust tests and 3 +App text behavior tests with no failures, passed workspace Clippy with warnings +denied, and built the ESP32-P4 simulator with none, Exa-only, Robinhood-only and +combined App catalogs. -The physical `WirelessBackend` and provider codecs are implemented and compile, -but a live ESP32 Wi-Fi -> OpenAI request was **not** exercised in that session -because the available AP did not provide the required network route. Do not -interpret the UART E2E as evidence for that separate network path. +Physical firmware, boot, Wi-Fi/DHCP, provider calls and unattended memory +pressure remain separate evidence tiers. A successful simulator build is not a +substitute for fresh physical-board acceptance. ## Development checks ```sh cargo test --workspace +bun test apps/_shared/text.test.ts cargo clippy --workspace --all-targets -- -D warnings ``` -CI runs the workspace build, tests and clippy checks. LCD scanout, touch, +CI runs the workspace build, Rust/App behavior tests and clippy checks. LCD scanout, touch, LittleFS, Wi-Fi/NVS, memory pressure and real UART/wireless behavior still require physical-board acceptance. ## Non-goals +- providing a macOS/Windows/Linux desktop Pocket Pi product; +- providing a generic `pi-agent-core` SDK or standalone harness; - Emulating the ESP32 CPU or peripherals on macOS; - exposing arbitrary desktop shell or Node APIs on the microcontroller; - coupling model providers, UI, trading services or research services into Pi - Harness core; + Agent Loop core; - claiming that an API codec compile proves a live provider connection. ## License diff --git a/apps/README.md b/apps/README.md new file mode 100644 index 0000000..a34b27e --- /dev/null +++ b/apps/README.md @@ -0,0 +1,20 @@ +# Pocket Pi built-in Apps + +Each directory is one independently versioned PocketJS App source. The checked +in `dist/app.js` and `dist/app.pak` files are target artifacts embedded into the +firmware as the built-in release, then seeded into `/workspace` on boot. This +is not yet a signed install, rollback, or recovery-UI mechanism. + +- `pi-agent`: the privileged Root View; its filesystem mount is `/workspace`. +- `robinhood`: curated MCP tools, `refreshPortfolio` AppTask, SQLite and View. +- `exa`: Exa search/fetch tools, SQLite search history and View. + +Build all three from the pinned upstream PocketJS checkout: + +```sh +POCKETJS_ROOT=/path/to/pocketjs-main cargo xtask build agentos-apps +``` + +The task refuses a checkout other than revision +`9c809bbd047ddc75c27caa4990951a78d942477a`, so generated recovery bundles +cannot silently drift from the runtime modules linked into the firmware. diff --git a/apps/_shared/text.test.ts b/apps/_shared/text.test.ts new file mode 100644 index 0000000..674f3fa --- /dev/null +++ b/apps/_shared/text.test.ts @@ -0,0 +1,66 @@ +import { expect, mock, test } from "bun:test"; + +mock.module("@pocketjs/framework", () => ({ + getOps: () => ({ measureText: (value: string) => [...value].length * 8 }), +})); + +const { wrapTextPage } = await import("./text.ts"); + +test("wraps words and unbroken tokens within maxWidth", () => { + const page = wrapTextPage("alpha beta 123456789", 2, 48, 0, 0, 10); + const lines = page.text.split("\n"); + + expect(lines).toEqual(["alpha", "beta", "123456", "789"]); + expect(lines.every((line) => [...line].length * 8 <= 48)).toBe(true); + expect(page.hasMore).toBe(false); +}); + +test("paginates deterministically to EOF and always advances", () => { + const source = "one two three four five six seven eight"; + const rendered: string[] = []; + let offset = 0; + let sourceLine = 0; + let finished = false; + + for (let iteration = 0; iteration <= source.length; iteration += 1) { + const page = wrapTextPage(source, 2, 40, offset, sourceLine, 2); + expect(wrapTextPage(source, 2, 40, offset, sourceLine, 2)).toEqual(page); + expect(page.text.split("\n").length).toBeLessThanOrEqual(2); + rendered.push(page.text); + + if (!page.hasMore) { + finished = true; + break; + } + + expect(page.nextOffset).toBeGreaterThan(offset); + offset = page.nextOffset; + sourceLine = page.nextSourceLine; + } + + expect(finished).toBe(true); + expect(rendered.join("\n").split(/\s+/)).toEqual(source.split(/\s+/)); +}); + +test("tracks source lines across LF and CRLF", () => { + const source = "aa\nbb\r\ncc"; + const first = wrapTextPage(source, 2, 80, 0, 0, 2); + const second = wrapTextPage(source, 2, 80, first.nextOffset, first.nextSourceLine, 2); + + expect(first).toEqual({ + text: "aa\nbb", + nextOffset: "aa\nbb\r\n".length, + startSourceLine: 0, + nextSourceLine: 2, + lastSourceLine: 1, + hasMore: true, + }); + expect(second).toEqual({ + text: "cc", + nextOffset: source.length, + startSourceLine: 2, + nextSourceLine: 2, + lastSourceLine: 2, + hasMore: false, + }); +}); diff --git a/apps/_shared/text.ts b/apps/_shared/text.ts new file mode 100644 index 0000000..472a175 --- /dev/null +++ b/apps/_shared/text.ts @@ -0,0 +1,188 @@ +import { getOps } from "@pocketjs/framework"; + +// Dynamic App text is not visible to PocketJS's bundle-time font subsetter. +// Keep common English prose punctuation in every bundle that imports this +// module so model/provider text does not fall back to missing-glyph boxes. +const DYNAMIC_TEXT_GLYPHS = "‘’“”–—…•"; + +const PREVIEW_SOURCE_CHARACTERS = 512; + +function breakToken(token: string, width: (text: string) => number, maxWidth: number): string[] { + const chunks: string[] = []; + let chunk = ""; + let chunkWidth = 0; + for (const character of token) { + const characterWidth = width(character); + if (chunk && chunkWidth + characterWidth > maxWidth) { + chunks.push(chunk); + chunk = ""; + chunkWidth = 0; + } + chunk += character; + chunkWidth += characterWidth; + } + if (chunk) chunks.push(chunk); + return chunks; +} + +// PocketJS Text only breaks on explicit newlines. Measure once per unique +// string and insert those newlines before the DrawList reaches native UI. +export function wrapLines(text: string, fontSlot: number, maxWidth: number): string[] { + void DYNAMIC_TEXT_GLYPHS; + const widths = new Map(); + const width = (value: string) => { + let measured = widths.get(value); + if (measured === undefined) { + measured = getOps().measureText(value, fontSlot); + widths.set(value, measured); + } + return measured; + }; + const lines: string[] = []; + const spaceWidth = width(" "); + for (const paragraph of text.split("\n")) { + const words = paragraph.split(" ").flatMap((token) => + token && width(token) > maxWidth + ? breakToken(token, width, maxWidth) + : [token], + ); + let line = ""; + let lineWidth = 0; + for (const word of words) { + const wordWidth = width(word); + if (!line) { + line = word; + lineWidth = wordWidth; + } else if (lineWidth + spaceWidth + wordWidth <= maxWidth) { + line += " " + word; + lineWidth += spaceWidth + wordWidth; + } else { + lines.push(line); + line = word; + lineWidth = wordWidth; + } + } + lines.push(line); + } + return lines; +} + +export function wrapPreview( + text: string, + fontSlot: number, + maxWidth: number, + maxLines: number, +): string { + const source = text.slice(0, PREVIEW_SOURCE_CHARACTERS); + const lines = wrapLines(source, fontSlot, maxWidth); + if (source.length === text.length && lines.length <= maxLines) return lines.join("\n"); + const visible = lines.slice(0, maxLines); + const last = visible.length - 1; + visible[last] = visible[last].replace(/[\s.]+$/, "") + "…"; + return visible.join("\n"); +} + +export type WrappedTextPage = { + text: string; + nextOffset: number; + startSourceLine: number; + nextSourceLine: number; + lastSourceLine: number; + hasMore: boolean; +}; + +type VisualLine = { + text: string; + nextOffset: number; + sourceLineEnded: boolean; +}; + +function nextVisualLine( + text: string, + maxWidth: number, + startOffset: number, + width: (value: string) => number, +): VisualLine { + let offset = startOffset; + while (offset < text.length && text[offset] === " ") offset += 1; + + const lineStart = offset; + let line = ""; + let lineWidth = 0; + let lastSpaceOffset = -1; + let lastSpaceIndex = -1; + + while (offset < text.length) { + const character = text[offset]; + if (character === "\n" || character === "\r") { + const nextOffset = character === "\r" && text[offset + 1] === "\n" ? offset + 2 : offset + 1; + return { text: line.replace(/\s+$/, ""), nextOffset, sourceLineEnded: true }; + } + + const characterWidth = width(character); + if (line && lineWidth + characterWidth > maxWidth) { + if (lastSpaceOffset > lineStart) { + return { + text: line.slice(0, lastSpaceIndex).replace(/\s+$/, ""), + nextOffset: lastSpaceOffset, + sourceLineEnded: false, + }; + } + return { text: line, nextOffset: offset, sourceLineEnded: false }; + } + + line += character; + lineWidth += characterWidth; + offset += 1; + if (character === " ") { + lastSpaceOffset = offset; + lastSpaceIndex = line.length - 1; + } + } + + return { text: line.replace(/\s+$/, ""), nextOffset: offset, sourceLineEnded: false }; +} + +// Materialize only the visual lines on one page. The cursor advances directly +// through the source string, so even a multi-megabyte physical line never gets +// sliced, split, or fully wrapped in memory. +export function wrapTextPage( + text: string, + fontSlot: number, + maxWidth: number, + startOffset: number, + startSourceLine: number, + maxLines: number, +): WrappedTextPage { + const lineLimit = Math.max(0, Math.floor(maxLines)); + const lines: string[] = []; + let offset = Math.max(0, Math.min(Math.floor(startOffset), text.length)); + let sourceLine = Math.max(0, Math.floor(startSourceLine)); + let lastSourceLine = sourceLine; + const widths = new Map(); + const width = (value: string) => { + let measured = widths.get(value); + if (measured === undefined) { + measured = getOps().measureText(value, fontSlot); + widths.set(value, measured); + } + return measured; + }; + + while (offset < text.length && lines.length < lineLimit) { + const visualLine = nextVisualLine(text, maxWidth, offset, width); + lastSourceLine = sourceLine; + lines.push(visualLine.text); + offset = visualLine.nextOffset; + if (visualLine.sourceLineEnded) sourceLine += 1; + } + + return { + text: lines.join("\n"), + nextOffset: offset, + startSourceLine: Math.max(0, Math.floor(startSourceLine)), + nextSourceLine: sourceLine, + lastSourceLine, + hasMore: offset < text.length, + }; +} diff --git a/apps/_shared/ui.tsx b/apps/_shared/ui.tsx new file mode 100644 index 0000000..f66e2c8 --- /dev/null +++ b/apps/_shared/ui.tsx @@ -0,0 +1,121 @@ +import { Text, View } from "@pocketjs/framework/components"; + +// Pi Design v0.2. These are product-wide visual contracts built only from +// PocketJS public primitives. App data, navigation and side effects stay with +// the consuming App. +const type = { + appTitle: "text-2xl text-white font-bold", + pageTitle: "text-2xl text-slate-950 font-bold", + heading: "text-xl text-slate-900 font-bold", + label: "text-base text-slate-600 font-bold", + captionStrong: "text-base text-slate-500 font-bold", +} as const; + +export const statusBadge = { + neutral: { surface: "px-3 py-2 rounded-lg bg-slate-100", text: "text-base text-slate-600 font-bold" }, + info: { surface: "px-3 py-2 rounded-lg bg-indigo-100", text: "text-base text-indigo-700 font-bold" }, + success: { surface: "px-3 py-2 rounded-lg bg-emerald-100", text: "text-base text-emerald-700 font-bold" }, + warning: { surface: "px-3 py-2 rounded-lg bg-amber-100", text: "text-base text-amber-700 font-bold" }, + danger: { surface: "px-3 py-2 rounded-lg bg-red-100", text: "text-base text-red-500 font-bold" }, +} as const; + +type HeaderProps = { + title: string; + back?: boolean; + accent?: "ready" | "busy" | "danger" | "none"; + metaTop?: string; + metaBottom?: string; +}; + +export function PocketHeader(props: HeaderProps) { + const accent = () => props.accent === "busy" ? "w-[34] h-[34] rounded-lg bg-amber-400" + : props.accent === "danger" ? "w-[34] h-[34] rounded-lg bg-red-500" + : props.accent === "none" ? "w-[34] h-[34] rounded-lg bg-slate-800" + : "w-[34] h-[34] rounded-lg bg-emerald-500"; + return ( + + + {props.back + ? + : } + {props.title} + + + {props.metaTop ?? ""} + {props.metaBottom ?? ""} + + + ); +} + +export function PageIntro(props: { eyebrow: string; title: string; description: string; tone?: "brand" | "info" }) { + return ( + + {props.eyebrow} + {props.title} + {props.description} + + ); +} + +export function SectionHeading(props: { title: string; detail?: string; action?: boolean }) { + const trailing = () => props.action + ? (props.detail ? props.detail + " · VIEW ALL ›" : "VIEW ALL ›") + : props.detail ?? ""; + return ( + + {props.title} + {trailing()} + + ); +} + +export function ActionButton(props: { label: string; disabled?: boolean; tone?: "primary" | "danger" | "neutral" }) { + const container = () => props.disabled ? "w-full h-full items-center justify-center rounded-xl bg-slate-200" + : props.tone === "danger" ? "w-full h-full items-center justify-center rounded-xl bg-red-100" + : props.tone === "neutral" ? "w-full h-full items-center justify-center rounded-xl bg-slate-100" + : "w-full h-full items-center justify-center rounded-xl bg-orange-600"; + const label = () => props.disabled ? "text-lg text-slate-500 font-bold" + : props.tone === "danger" ? "text-lg text-red-500 font-bold" + : props.tone === "neutral" ? "text-lg text-slate-900 font-bold" + : "text-lg text-white font-bold"; + return {props.label}; +} + +export function EmptyState(props: { icon?: string; title: string; detail?: string; tone?: "info" | "neutral"; compact?: boolean }) { + return ( + + {props.icon ? {props.icon} : null} + {props.title} + {props.detail ? {props.detail} : null} + + ); +} + +export function MetricCard(props: { label: string; value: string; tone?: "neutral" | "success" | "danger" }) { + const valueClass = () => props.tone === "success" ? "text-xl text-emerald-600 font-bold" + : props.tone === "danger" ? "text-xl text-red-500 font-bold" + : "text-xl text-slate-900 font-bold"; + return ( + + {props.label} + {props.value} + + ); +} + +export function StatusBar(props: { text: string; tone?: "neutral" | "danger"; dark?: boolean }) { + const textClass = () => props.dark + ? (props.tone === "danger" ? "text-base text-red-300" : "text-base text-slate-300") + : (props.tone === "danger" ? "text-base text-red-500" : "text-base text-slate-500"); + return {props.text}; +} + +export function ScrollButtons(props: { top: string; bottom: string }) { + return ( + <> + UP + DN + + ); +} diff --git a/apps/exa/agent-app.json b/apps/exa/agent-app.json new file mode 100644 index 0000000..7cb54df --- /dev/null +++ b/apps/exa/agent-app.json @@ -0,0 +1,136 @@ +{ + "id": "exa", + "description": "Web research and local search history", + "version": "1.1.0", + "dataVersion": 5, + "toolNamespace": "research", + "nativeServices": { + "http": [ + { + "method": "POST", + "urls": [ + "https://api.exa.ai/search", + "https://api.exa.ai/contents" + ], + "allowedRequestHeaders": ["accept", "content-type"], + "credential": { + "id": "exa.api-key", + "header": "x-api-key" + } + } + ] + }, + "tools": [ + { + "name": "research.search", + "description": "Search the web through Exa and save a bounded, source-linked result set to local history. Pocket Pi defaults to auto so Exa can balance quality and speed; explicitly use deep or deep-reasoning for harder research, and fast or instant when latency matters. Use category and publication dates for targeted news, filings, company, or publication research. company and people categories cannot be combined with publication-date filters or excludeDomains.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "minLength": 1, + "maxLength": 2000, + "description": "Natural-language web search query. Put the research question here; use the dedicated filter fields instead of site: or date syntax." + }, + "numResults": { + "type": "integer", + "minimum": 1, + "maximum": 10, + "description": "Number of results to return. Defaults to Exa's standard 10-result page." + }, + "searchType": { + "type": "string", + "enum": ["auto", "fast", "instant", "deep-lite", "deep", "deep-reasoning"], + "description": "Exa search mode. auto is the default and balances quality and speed; deep modes increase research depth, latency, and cost." + }, + "category": { + "type": "string", + "enum": ["company", "publication", "news", "personal site", "financial report", "people"], + "description": "Optional Exa content category. Use financial report for filings and reports, news for current reporting, publication for papers, and company for official company pages." + }, + "includeDomains": { + "type": "array", + "items": {"type": "string", "minLength": 1, "maxLength": 255}, + "maxItems": 8, + "description": "Only return these domains or domain paths, for example sec.gov or investor.example.com." + }, + "excludeDomains": { + "type": "array", + "items": {"type": "string", "minLength": 1, "maxLength": 255}, + "maxItems": 8, + "description": "Exclude these domains or domain paths. Do not combine with company or people category." + }, + "startPublishedDate": { + "type": "string", + "minLength": 10, + "maxLength": 35, + "description": "Only return sources published at or after this ISO 8601 date or date-time, for example 2026-01-01." + }, + "endPublishedDate": { + "type": "string", + "minLength": 10, + "maxLength": 35, + "description": "Only return sources published at or before this ISO 8601 date or date-time." + }, + "userLocation": { + "type": "string", + "minLength": 2, + "maxLength": 2, + "description": "Optional two-letter ISO country code such as US for geographically relevant results." + }, + "additionalQueries": { + "type": "array", + "items": {"type": "string", "minLength": 1, "maxLength": 2000}, + "minItems": 1, + "maxItems": 10, + "description": "Optional query variations used only with deep-lite, deep, or deep-reasoning to broaden research." + }, + "maxAgeHours": { + "type": "integer", + "minimum": -1, + "maximum": 720, + "description": "Maximum cached content age in hours: 0 forces a fresh crawl, -1 uses cache only, and omission uses Exa's normal fallback policy." + }, + "moderation": { + "type": "boolean", + "description": "Ask Exa to filter unsafe content from search results." + } + }, + "required": ["query"], + "additionalProperties": false + } + }, + { + "name": "research.fetch", + "description": "Fetch one known URL through Exa as bounded clean text with metadata and crawl status. Use after research.search when highlights are insufficient. Set maxAgeHours to 0 only when a fresh crawl is necessary because it is slower and costs more.", + "parameters": { + "type": "object", + "properties": { + "url": { + "type": "string", + "minLength": 1, + "maxLength": 2048, + "description": "Exact HTTP or HTTPS URL returned by search or supplied by the user." + }, + "maxCharacters": { + "type": "integer", + "minimum": 200, + "maximum": 12000, + "description": "Maximum clean-text characters returned and saved. Defaults to 6000." + }, + "maxAgeHours": { + "type": "integer", + "minimum": -1, + "maximum": 720, + "description": "Maximum cached content age in hours: 0 forces a fresh crawl, -1 uses cache only, and omission uses Exa's normal fallback policy." + } + }, + "required": ["url"], + "additionalProperties": false + } + } + ], + "tasks": [], + "schedules": [] +} diff --git a/apps/exa/app.tsx b/apps/exa/app.tsx new file mode 100644 index 0000000..4dce417 --- /dev/null +++ b/apps/exa/app.tsx @@ -0,0 +1,145 @@ +import { createSignal, For, Show } from "solid-js"; +import { Text, View } from "@pocketjs/framework/components"; +import { mount } from "@pocketjs/framework"; +import { Database } from "@pocketjs/framework/db"; +import { EmptyState, PageIntro, PocketHeader, ScrollButtons, statusBadge, StatusBar } from "../_shared/ui"; + +const DB_SCHEMA_VERSION = 5; +const RETENTION_DAYS = 7; +const HISTORY_PAGE_SIZE = 10; +const HISTORY_VISIBLE_ROWS = 6; +const db = new Database("exa"); + +type SearchRow = { id: number; query: string; searched_at: number; status: string; result_count: number; top_title: string | null; error: string | null }; +const [history, setHistory] = createSignal([]); +const [hasMore, setHasMore] = createSignal(false); +const [historyOffset, setHistoryOffset] = createSignal(0); +const [status, setStatus] = createSignal("SEARCH HISTORY IS LOCAL"); +let loadedRevision = -1; +let loadingMore = false; + +function parse(value: string): any { try { return JSON.parse(value); } catch { return null; } } + +function searchTime(seconds: number): string { + const value = new Date(seconds * 1000).toISOString(); + return value.slice(0, 10) + " · " + value.slice(11, 16) + " UTC"; +} + +function historyPage(offset: number): SearchRow[] { + return db.query(` + SELECT id,query,searched_at,status,result_count,top_title,error + FROM searches ORDER BY id DESC LIMIT ? OFFSET ? + `).all(HISTORY_PAGE_SIZE + 1, offset) as unknown as SearchRow[]; +} + +function loadMore() { + if (loadingMore || !hasMore()) return; + loadingMore = true; + try { + const next = historyPage(history().length); + setHistory((current) => [...current, ...next.slice(0, HISTORY_PAGE_SIZE)]); + setHasMore(next.length > HISTORY_PAGE_SIZE); + } finally { + loadingMore = false; + } +} + +function scrollHistory(direction: -1 | 1) { + const step = 4; + if (direction > 0 && historyOffset() + HISTORY_VISIBLE_ROWS + step > history().length && hasMore()) loadMore(); + setHistoryOffset((offset) => direction < 0 + ? Math.max(0, offset - step) + : Math.min(Math.max(0, history().length - HISTORY_VISIBLE_ROWS), offset + step)); +} + +function loadView(revision: number) { + if (loadedRevision === revision) return; + try { + const schema = db.query("PRAGMA user_version").get() as unknown as { user_version?: number } | null; + if (Number(schema?.user_version ?? 0) !== DB_SCHEMA_VERSION) { + setHistory([]); + setHasMore(false); + setHistoryOffset(0); + setStatus("SEARCH HISTORY IS LOCAL"); + return; + } + const page = historyPage(0); + const next = page.slice(0, HISTORY_PAGE_SIZE); + setHistory(next); + setHasMore(page.length > HISTORY_PAGE_SIZE); + setHistoryOffset(0); + setStatus(next[0]?.status === "error" ? String(next[0].error || "EXA SEARCH FAILED").slice(0, 80) + : next.length ? "SEARCH HISTORY UPDATED FROM SQLITE" : "SEARCH HISTORY IS LOCAL"); + loadedRevision = revision; + } catch { + setHistory([]); + setHistoryOffset(0); + setStatus("SEARCH HISTORY IS LOCAL"); + } +} + +function Exa() { + return ( + + + + + + + {(item) => ( + + + {item.query.slice(0, 48)} + {(item.top_title || item.error || "No result title").slice(0, 58)} + {searchTime(item.searched_at)} + + + {item.status === "ok" ? item.result_count + " RESULTS" : "FAILED"} + + + )} + + + + + + + + + + ); +} + +loadView(0); +mount(() => ); + +(globalThis as any).PocketPiApp = { + tick() { return ""; }, + dataChanged(eventsLine: string) { + const events = parse(eventsLine); + const revision = Array.isArray(events) + ? events.reduce((latest: number, event: any) => Math.max(latest, Number(event?.revision ?? 0)), loadedRevision) + : loadedRevision; + loadView(revision); + return ""; + }, + tap(x: number, y: number) { + if (y < 112 && x < 100) return JSON.stringify({ type: "navigate", app: "pi-agent" }); + if (x >= 620 && y >= 294 && y <= 426) scrollHistory(-1); + else if (x >= 620 && y >= 1020 && y <= 1152) scrollHistory(1); + return ""; + }, +}; diff --git a/apps/exa/data-action.ts b/apps/exa/data-action.ts new file mode 100644 index 0000000..4a50518 --- /dev/null +++ b/apps/exa/data-action.ts @@ -0,0 +1,174 @@ +// Headless Exa data plane. Only search history consumed by the fixed View is +// persisted; fetched documents are returned directly to the Agent. +import { __pumpNet, fetch } from "@pocketjs/framework/net"; + +const nativeDb = (globalThis as any).db; +const handle = nativeDb.open("exa"); +if (handle < 0) throw new Error("open exa.sqlite"); + +const SCHEMA_VERSION = 5; +const RETENTION_DAYS = 7; +const RETENTION_SECONDS = RETENTION_DAYS * 24 * 60 * 60; + +function dbError(): string { return String(nativeDb.lastError(handle) || "SQLite operation failed"); } +function exec(sql: string): void { if (nativeDb.exec(handle, sql) !== 0) throw new Error(dbError()); } +function query(sql: string, args: any[] = []): any { + const result = JSON.parse(nativeDb.query(handle, sql, JSON.stringify(args))); + if (result.error) throw new Error(String(result.error)); + return result; +} +function run(sql: string, args: any[] = []): any { return query(sql, args); } + +const version = Number(query("PRAGMA user_version")?.rows?.[0]?.[0] ?? 0); +if (version !== SCHEMA_VERSION) { + exec(` + DROP TABLE IF EXISTS searches; + CREATE TABLE searches ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + query TEXT NOT NULL, + searched_at INTEGER NOT NULL, + status TEXT NOT NULL, + result_count INTEGER NOT NULL DEFAULT 0, + top_title TEXT, + error TEXT + ); + CREATE INDEX searches_retention ON searches(searched_at); + PRAGMA user_version=${SCHEMA_VERSION}; + `); +} + +function now(): number { return Math.floor(Date.now() / 1000); } +function cleanupExpired(referenceTime: number): void { + const cutoff = referenceTime - RETENTION_SECONDS; + run("DELETE FROM searches WHERE searched_at < ?", [cutoff]); +} + +async function post(path: "/search" | "/contents", body: any): Promise { + const response = await fetch(`https://api.exa.ai${path}`, { + method: "POST", + headers: { accept: "application/json", "content-type": "application/json" }, + body: JSON.stringify(body), + timeoutMs: (globalThis as any).app.remainingMs(), + maxBytes: 96 * 1024, + }); + const value = await response.json(); + if (!response.ok) throw new Error(`Exa HTTP ${response.status}: ${JSON.stringify(value)}`); + return value; +} + +function transaction(action: () => void): void { + exec("BEGIN IMMEDIATE"); + try { + action(); + exec("COMMIT"); + } catch (error) { + try { exec("ROLLBACK"); } catch {} + throw error; + } + (globalThis as any).app.commit(); +} + +async function search(args: any): Promise { + const searchQuery = String(args.query ?? "").trim(); + if (!searchQuery) throw new Error("query is required"); + const searchedAt = now(); + try { + const body: any = { + query: searchQuery, + type: args.searchType ?? "auto", + numResults: Math.max(1, Math.min(10, Number(args.numResults ?? 10))), + contents: { highlights: { maxCharacters: 800 } }, + }; + for (const key of [ + "includeDomains", "excludeDomains", "startPublishedDate", "endPublishedDate", + "category", "userLocation", "additionalQueries", "moderation", + ]) { + if (args[key] !== undefined) body[key] = args[key]; + } + if (args.maxAgeHours !== undefined) body.contents.maxAgeHours = args.maxAgeHours; + const value = await post("/search", body); + const results = Array.isArray(value?.results) ? value.results : []; + const topTitle = typeof results[0]?.title === "string" ? results[0].title : null; + transaction(() => { + run( + "INSERT INTO searches(query,searched_at,status,result_count,top_title,error) VALUES(?,?,?,?,?,NULL)", + [searchQuery, searchedAt, "ok", results.length, topTitle], + ); + cleanupExpired(searchedAt); + }); + return value; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + transaction(() => { + run( + "INSERT INTO searches(query,searched_at,status,result_count,top_title,error) VALUES(?,?,?,0,NULL,?)", + [searchQuery, searchedAt, "error", message], + ); + cleanupExpired(searchedAt); + }); + throw error; + } +} + +async function fetchDocument(args: any): Promise { + const requestedUrl = String(args.url ?? "").trim(); + if (!requestedUrl) throw new Error("url is required"); + const request: any = { + urls: [requestedUrl], + text: { + maxCharacters: Math.max(200, Math.min(12000, Number(args.maxCharacters ?? 6000))), + includeHtmlTags: false, + }, + }; + if (args.maxAgeHours !== undefined) request.maxAgeHours = args.maxAgeHours; + return post("/contents", request); +} + +let pendingResult: string | undefined; +let active = false; + +function failure(error: unknown): string { + return JSON.stringify({ + text: error instanceof Error ? error.message : String(error), + isError: true, + }); +} + +function begin(action: () => Promise): void { + if (active) throw new Error("Exa Data Action is already running"); + active = true; + pendingResult = undefined; + action().then( + (value) => { + pendingResult = JSON.stringify({ text: JSON.stringify(value), isError: false }); + active = false; + }, + (error) => { + pendingResult = failure(error); + active = false; + }, + ); +} + +(globalThis as any).PocketPiData = { + beginInvokeTask(name: string) { + begin(async () => { throw new Error("Unknown Exa Data Action: " + name); }); + }, + beginInvokeTool(name: string, argsLine: string) { + try { + const args = JSON.parse(argsLine); + begin(() => name === "research.search" ? search(args) + : name === "research.fetch" ? fetchDocument(args) + : Promise.reject(new Error("Unknown Exa tool: " + name))); + } catch (error) { + pendingResult = failure(error); + active = false; + } + }, + tick() { __pumpNet(); }, + pollResult() { + const result = pendingResult; + pendingResult = undefined; + return result; + }, +}; diff --git a/apps/exa/dist/app.js b/apps/exa/dist/app.js new file mode 100644 index 0000000..ff68885 --- /dev/null +++ b/apps/exa/dist/app.js @@ -0,0 +1,6 @@ +(()=>{var MJ={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return s0(this.context.count)},getNextContextId(){return s0(this.context.count++)}};function s0(J){let Q=String(J),$=Q.length-1;return MJ.context.id+($?String.fromCharCode(96+$):"")+Q}function a0(J){MJ.context=J}function _4(){return{...MJ.context,id:MJ.getNextContextId(),count:0}}var y4=!1,b4=(J,Q)=>J===Q,G0=Symbol("solid-proxy"),h4=typeof Proxy==="function",x4=Symbol("solid-track"),k2=Symbol("solid-dev-component"),NJ={equals:b4},r0=null,f4=$1,N=1,kJ=2,o0={owned:null,cleanups:null,context:null,owner:null},O=null,B=null,IJ=null,UJ=null,k=null,S=null,f=null,EJ=0;function vJ(J,Q){let $=k,Z=O,X=J.length===0,W=Q===void 0?Z:Q,Y=X?o0:{owned:null,cleanups:null,context:W?W.context:null,owner:W},j=X?J:()=>J(()=>r(()=>o(Y)));O=Y,k=null;try{return i(j,!0)}finally{k=$,O=Z}}function XJ(J,Q){Q=Q?Object.assign({},NJ,Q):NJ;let $={value:J,observers:null,observerSlots:null,comparator:Q.equals||void 0},Z=(X)=>{if(typeof X==="function")if(B&&B.running&&B.sources.has($))X=X($.tValue);else X=X($.value);return e0($,X)};return[t0.bind($),Z]}function a(J,Q,$){let Z=Q1(J,Q,!1,N);if(IJ&&B&&B.running)S.push(Z);else mJ(Z)}function VJ(J,Q,$){$=$?Object.assign({},NJ,$):NJ;let Z=Q1(J,Q,!0,0);if(Z.observers=null,Z.observerSlots=null,Z.comparator=$.equals||void 0,IJ&&B&&B.running)Z.tState=N,S.push(Z);else mJ(Z);return t0.bind(Z)}function r(J){if(!UJ&&k===null)return J();let Q=k;k=null;try{if(UJ)return UJ.untrack(J);return J()}finally{k=Q}}function F0(J){if(O===null);else if(O.cleanups===null)O.cleanups=[J];else O.cleanups.push(J);return J}function g4(J){if(B&&B.running)return J(),B.done;let Q=k,$=O;return Promise.resolve().then(()=>{k=Q,O=$;let Z;if(IJ||T4)Z=B||(B={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),Z.done||(Z.done=new Promise((X)=>Z.resolve=X)),Z.running=!0;return i(J,!1),k=O=null,Z?Z.done:void 0})}var[I2,n0]=XJ(!1),T4;function t0(){let J=B&&B.running;if(this.sources&&(J?this.tState:this.state))if((J?this.tState:this.state)===N)mJ(this);else{let Q=S;S=null,i(()=>lJ(this),!1),S=Q}if(k){let Q=this.observers;if(!Q||Q[Q.length-1]!==k){let $=Q?Q.length:0;if(!k.sources)k.sources=[this],k.sourceSlots=[$];else k.sources.push(this),k.sourceSlots.push($);if(!Q)this.observers=[k],this.observerSlots=[k.sources.length-1];else Q.push(k),this.observerSlots.push(k.sources.length-1)}}if(J&&B.sources.has(this))return this.tValue;return this.value}function e0(J,Q,$){let Z=B&&B.running&&B.sources.has(J)?J.tValue:J.value;if(!J.comparator||!J.comparator(Z,Q)){if(B){let X=B.running;if(X||!$&&B.sources.has(J))B.sources.add(J),J.tValue=Q;if(!X)J.value=Q}else J.value=Q;if(J.observers&&J.observers.length)i(()=>{for(let X=0;X1e6)throw S=[],Error()},!1)}return Q}function mJ(J){if(!J.fn)return;o(J);let Q=EJ;if(J1(J,B&&B.running&&B.sources.has(J)?J.tValue:J.value,Q),B&&!B.running&&B.sources.has(J))queueMicrotask(()=>{i(()=>{B&&(B.running=!0),k=O=J,J1(J,J.tValue,Q),k=O=null},!1)})}function J1(J,Q,$){let Z,X=O,W=k;k=O=J;try{Z=J.fn(Q)}catch(Y){if(J.pure)if(B&&B.running)J.tState=N,J.tOwned&&J.tOwned.forEach(o),J.tOwned=void 0;else J.state=N,J.owned&&J.owned.forEach(o),J.owned=null;return J.updatedAt=$+1,H0(Y)}finally{k=W,O=X}if(!J.updatedAt||J.updatedAt<=$){if(J.updatedAt!=null&&"observers"in J)e0(J,Z,!0);else if(B&&B.running&&J.pure){if(!B.sources.has(J))J.value=Z;B.sources.add(J),J.tValue=Z}else J.value=Z;J.updatedAt=$}}function Q1(J,Q,$,Z=N,X){let W={fn:J,state:Z,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:Q,owner:O,context:O?O.context:null,pure:$};if(B&&B.running)W.state=0,W.tState=Z;if(O===null);else if(O!==o0)if(B&&B.running&&O.pure)if(!O.tOwned)O.tOwned=[W];else O.tOwned.push(W);else if(!O.owned)O.owned=[W];else O.owned.push(W);if(UJ&&W.fn){let Y=W.fn,[j,M]=XJ(void 0,{equals:!1}),V=UJ.factory(Y,M);F0(()=>V.dispose());let D,G=()=>g4(M).then(()=>{if(D)D.dispose(),D=void 0});W.fn=(I)=>{if(j(),B&&B.running){if(!D)D=UJ.factory(Y,G);return D.track(I)}return V.track(I)}}return W}function B0(J){let Q=B&&B.running;if((Q?J.tState:J.state)===0)return;if((Q?J.tState:J.state)===kJ)return lJ(J);if(J.suspense&&r(J.suspense.inFallback))return J.suspense.effects.push(J);let $=[J];while((J=J.owner)&&(!J.updatedAt||J.updatedAt=0;Z--){if(J=$[Z],Q){let X=J,W=$[Z+1];while((X=X.owner)&&X!==W)if(B.disposed.has(X))return}if((Q?J.tState:J.state)===N)mJ(J);else if((Q?J.tState:J.state)===kJ){let X=S;S=null,i(()=>lJ(J,$[0]),!1),S=X}}}function i(J,Q){if(S)return J();let $=!1;if(!Q)S=[];if(f)$=!0;else f=[];EJ++;try{let Z=J();return N4($),Z}catch(Z){if(!$)f=null;S=null,H0(Z)}}function N4(J){if(S){if(IJ&&B&&B.running)E4(S);else $1(S);S=null}if(J)return;let Q;if(B){if(!B.promises.size&&!B.queue.size){let{sources:Z,disposed:X}=B;f.push.apply(f,B.effects),Q=B.resolve;for(let W of f)"tState"in W&&(W.state=W.tState),delete W.tState;B=null,i(()=>{for(let W of X)o(W);for(let W of Z){if(W.value=W.tValue,W.owned)for(let Y=0,j=W.owned.length;Yf4($),!1);if(Q)Q()}function $1(J){for(let Q=0;Q{Z.delete($),i(()=>{B.running=!0,B0($)},!1),B&&(B.running=!1)})}}function lJ(J,Q){let $=B&&B.running;if($)J.tState=0;else J.state=0;for(let Z=0;Z=0;Q--)o(J.tOwned[Q]);delete J.tOwned}if(B&&B.running&&J.pure)X1(J,!0);else if(J.owned){for(Q=J.owned.length-1;Q>=0;Q--)o(J.owned[Q]);J.owned=null}if(J.cleanups){for(Q=J.cleanups.length-1;Q>=0;Q--)J.cleanups[Q]();J.cleanups=null}if(B&&B.running)J.tState=0;else J.state=0}function X1(J,Q){if(!Q)J.tState=0,B.disposed.add(J);if(J.owned)for(let $=0;$1?[]:null;return F0(()=>q1(W)),()=>{let M=J()||[],V=M.length,D,G;return M[x4],r(()=>{let b,x,c,ZJ,BJ,H,F,z,U;if(V===0){if(Y!==0)q1(W),W=[],Z=[],X=[],Y=0,j&&(j=[]);if($.fallback)Z=[m4],X[0]=vJ((L)=>{return W[0]=L,$.fallback()}),Y=1}else if(Y===0){X=Array(V);for(G=0;G=H&&z>=H&&Z[F]===M[z];F--,z--)c[z]=X[F],ZJ[z]=W[F],j&&(BJ[z]=j[F]);b=new Map,x=Array(z+1);for(G=z;G>=H;G--)U=M[G],D=b.get(U),x[G]=D===void 0?-1:D,b.set(U,G);for(D=H;D<=F;D++)if(U=Z[D],G=b.get(U),G!==void 0&&G!==-1)c[G]=X[D],ZJ[G]=W[D],j&&(BJ[G]=j[D]),G=x[G],b.set(U,G);else W[D]();for(G=H;GJ(Q||{}));return a0($),Z}}return r(()=>J(Q||{}))}function uJ(){return!0}var p4={get(J,Q,$){if(Q===G0)return $;return J.get(Q)},has(J,Q){if(Q===G0)return!0;return J.has(Q)},set:uJ,deleteProperty:uJ,getOwnPropertyDescriptor(J,Q){return{configurable:!0,enumerable:!0,get(){return J.get(Q)},set:uJ,deleteProperty:uJ}},ownKeys(J){return J.keys()}};function K0(J){return!(J=typeof J==="function"?J():J)?{}:J}function i4(){for(let J=0,Q=this.length;J=0;j--){let M=K0(J[j])[Y];if(M!==void 0)return M}},has(Y){for(let j=J.length-1;j>=0;j--)if(Y in K0(J[j]))return!0;return!1},keys(){let Y=[];for(let j=0;j=0;Y--){let j=J[Y];if(!j)continue;let M=Object.getOwnPropertyNames(j);for(let V=M.length-1;V>=0;V--){let D=M[V];if(D==="__proto__"||D==="constructor")continue;let G=Object.getOwnPropertyDescriptor(j,D);if(!Z[D])Z[D]=G.get?{enumerable:!0,configurable:!0,get:i4.bind($[D]=[G.get.bind(j)])}:G.value!==void 0?G:void 0;else{let I=$[D];if(I){if(G.get)I.push(G.get.bind(j));else if(G.value!==void 0)I.push(()=>G.value)}}}}let X={},W=Object.keys(Z);for(let Y=W.length-1;Y>=0;Y--){let j=W[Y],M=Z[j];if(M&&M.get)Object.defineProperty(X,j,M);else X[j]=M?M.value:void 0}return X}var d4=(J)=>`Stale read from <${J}>.`;function s4(J){let Q="fallback"in J&&{fallback:()=>J.fallback};return VJ(l4(()=>J.each,J.children,Q||void 0))}function a4(J){let Q=J.keyed,$=VJ(()=>J.when,void 0,void 0),Z=Q?$:VJ($,void 0,{equals:(X,W)=>!X===!W});return VJ(()=>{let X=Z();if(X){let W=J.children;return typeof W==="function"&&W.length>0?r(()=>W(Q?X:()=>{if(!r(Z))throw d4("Show");return $()})):W}return J.fallback},void 0,void 0)}var r4=(J)=>VJ(()=>J());function o4({createElement:J,createTextNode:Q,isTextNode:$,replaceText:Z,insertNode:X,removeNode:W,setProperty:Y,getParentNode:j,getFirstChild:M,getNextSibling:V}){function D(H,F,z,U){if(z!==void 0&&!U)U=[];if(typeof F!=="function")return G(H,F,U,z);a((L)=>G(H,F(),L,z),U)}function G(H,F,z,U,L){while(typeof z==="function")z=z();if(F===z)return z;let C=typeof F,w=U!==void 0;if(C==="string"||C==="number"){if(C==="number")F=F.toString();if(w){let P=z[0];if(P&&$(P))Z(P,F);else P=Q(F);z=x(H,z,U,P)}else if(z!==""&&typeof z==="string")Z(M(H),z=F);else x(H,z,U,Q(F)),z=F}else if(F==null||C==="boolean")z=x(H,z,U);else if(C==="function")return a(()=>{let P=F();while(typeof P==="function")P=P();z=G(H,P,z,U)}),()=>z;else if(Array.isArray(F)){let P=[];if(I(P,F,L))return a(()=>z=G(H,P,z,U,!0)),()=>z;if(P.length===0){let OJ=x(H,z,U);if(w)return z=OJ}else if(Array.isArray(z))if(z.length===0)c(H,P,U);else b(H,z,P);else if(z==null||z==="")c(H,P);else b(H,w&&z||[M(H)],P);z=P}else{if(Array.isArray(z)){if(w)return z=x(H,z,U,F);x(H,z,null,F)}else if(z==null||z===""||!M(H))X(H,F);else ZJ(H,F,M(H));z=F}return z}function I(H,F,z){let U=!1;for(let L=0,C=F.length;Lp-P){let O2=F[w];while(P=0;w--){let P=F[w];if(L!==P){let OJ=j(P)===H;if(!C&&!w)OJ?ZJ(H,L,P):X(H,L,z);else OJ&&W(H,P)}else C=!0}}else X(H,L,z);return[L]}function c(H,F,z){for(let U=0,L=F.length;Uz.children=G(H,F.children,z.children));return a(()=>F.ref&&F.ref(H)),a(()=>{for(let L in F){if(L==="children"||L==="ref")continue;let C=F[L];if(C===z[L])continue;Y(H,L,C,z[L]),z[L]=C}}),z}return{render(H,F){let z;return vJ((U)=>{z=U,D(F,H())}),z},insert:D,spread(H,F,z){if(typeof F==="function")a((U)=>BJ(H,F(),U,z));else BJ(H,F,void 0,z)},createElement:J,createTextNode:Q,insertNode:X,setProp(H,F,z,U){return Y(H,F,z,U),z},mergeProps:Y1,effect:a,memo:r4,createComponent:c4,use(H,F,z){return r(()=>H(F,z))}}}function n4(J){let Q=o4(J);return Q.mergeProps=Y1,Q}var z1=480,j1=272,WJ={view:0,text:1,image:2},t4=1,e4=-1,J6={width:1,height:2,minW:3,minH:4,maxW:5,maxH:6,paddingT:8,paddingR:9,paddingB:10,paddingL:11,marginT:12,marginR:13,marginB:14,marginL:15,gap:16,flexDir:17,justify:18,align:19,grow:20,shrink:21,basis:22,flexWrap:23,posType:24,insetT:25,insetR:26,insetB:27,insetL:28,display:29,overflow:30,zIndex:31,hitPass:32,bgColor:64,gradFrom:65,gradTo:66,gradDir:67,radius:68,opacity:69,borderColor:70,borderWidth:71,shadow:72,bevelOuterLight:77,bevelOuterDark:78,bevelInnerLight:79,bevelInnerDark:80,bevelWidth:81,textColor:96,fontSlot:97,textAlign:98,lineHeight:99,tracking:100,translateX:128,translateY:129,scale:130,rotate:131,scaleX:132,scaleY:133,originX:134,originY:135,rotateX:136,rotateY:137,translateZ:138,perspective:139,arcStart:140,arcSweep:141,arcWidth:142},K={f32:0,color:1,int:2},Q6={width:K.f32,height:K.f32,minW:K.f32,minH:K.f32,maxW:K.f32,maxH:K.f32,paddingT:K.f32,paddingR:K.f32,paddingB:K.f32,paddingL:K.f32,marginT:K.f32,marginR:K.f32,marginB:K.f32,marginL:K.f32,gap:K.f32,flexDir:K.int,justify:K.int,align:K.int,grow:K.f32,shrink:K.f32,basis:K.f32,flexWrap:K.int,posType:K.int,insetT:K.f32,insetR:K.f32,insetB:K.f32,insetL:K.f32,display:K.int,overflow:K.int,zIndex:K.int,hitPass:K.int,bgColor:K.color,gradFrom:K.color,gradTo:K.color,gradDir:K.int,radius:K.f32,opacity:K.f32,borderColor:K.color,borderWidth:K.f32,shadow:K.int,bevelOuterLight:K.color,bevelOuterDark:K.color,bevelInnerLight:K.color,bevelInnerDark:K.color,bevelWidth:K.f32,textColor:K.color,fontSlot:K.int,textAlign:K.int,lineHeight:K.f32,tracking:K.f32,translateX:K.f32,translateY:K.f32,scale:K.f32,rotate:K.f32,scaleX:K.f32,scaleY:K.f32,originX:K.f32,originY:K.f32,rotateX:K.f32,rotateY:K.f32,translateZ:K.f32,perspective:K.f32,arcStart:K.f32,arcSweep:K.f32,arcWidth:K.f32},E={FlexDir:{Row:0,Col:1},Justify:{Start:0,Center:1,End:2,Between:3,Around:4},Align:{Start:0,Center:1,End:2,Stretch:3},PosType:{Relative:0,Absolute:1},Display:{Flex:0,None:1},Overflow:{Visible:0,Hidden:1},TextAlign:{Left:0,Center:1,Right:2},GradDir:{ToTop:0,ToBottom:1,ToLeft:2,ToRight:3},Easing:{Linear:0,EaseIn:1,EaseOut:2,EaseInOut:3,OutBack:4,Spring:5,SpringBouncy:6,CubicBezier:7}},$6={PSM_5650:0,PSM_4444:2,PSM_8888:3,PSM_T8:5},Z6=1,A2=2,S2=1,_2=2,y2=131072,b2=1,h2=262144,x2=1,f2=1,g2=1,T2=2,N2=4,E2=8,v2=16,m2=1,l2=2;function G1(J,Q,$,Z=255){return((Z&255)<<24|($&255)<<16|(Q&255)<<8|J&255)>>>0}var u2=1,X6=1263551300,W6=1,q6=32,Y6=24,m={SELECT:1,START:8,UP:16,RIGHT:32,DOWN:64,LEFT:128,LTRIGGER:256,RTRIGGER:512,TRIANGLE:4096,CIRCLE:8192,CROSS:16384,SQUARE:32768},qJ=32896,c2=0.016666666666666666;function F1(J){return J.__viewport??null}var M0=null;function z6(){return null}function B1(J,Q=z6()){if(!Q)return;if(typeof J.__host!=="string")throw Error(`PocketJS: this bundle targets "${Q.target}" but the native host predates platform `+"contracts — add __host/__hostAbi to its ui namespace (see framework/src/host.ts HostOps)");if(J.__host!==Q.target)throw Error(`PocketJS: native target mismatch (bundle=${Q.target}, host=${J.__host})`);if(J.__hostAbi!==Q.hostAbi)throw Error(`PocketJS: native host ABI mismatch (bundle=${Q.hostAbi}, host=${J.__hostAbi??"missing"})`)}function j6(J){let Q=globalThis.ui,$=Q!==void 0&&(typeof Q.__host==="string"||Q.__textures!==void 0);if(J){if(Q!==void 0&&J===Q&&$)return B1(Q),{ops:J,kind:"native",target:Q.__host??"unknown",strict:!1};return{ops:J,kind:"injected",target:J.__host??"injected",strict:!0}}if(Q!==void 0&&$)return B1(Q),{ops:Q,kind:"native",target:Q.__host??"unknown",strict:!1};if(Q)return{ops:Q,kind:"injected",target:"injected",strict:!0};throw Error("PocketJS: no host — pass HostOps to render() (web/test) or run under a native runtime (globalThis.ui)")}function G6(J){M0=J}function PJ(){if(!M0)throw Error("PocketJS: host not installed — call render() first");return M0}function h(){return PJ().ops}function F6(J){globalThis.frame=J}function B6(J){let Q=globalThis,$=Q.__pocketResizeViewport,Z=(X,W)=>J(X,W);return Q.__pocketResizeViewport=Z,()=>{if(Q.__pocketResizeViewport!==Z)return;if($)Q.__pocketResizeViewport=$;else delete Q.__pocketResizeViewport}}function H6(J){let Q=J.slice(1);if(Q.length===3)Q=Q[0]+Q[0]+Q[1]+Q[1]+Q[2]+Q[2];if(Q.length!==6&&Q.length!==8)throw Error(`PocketJS: bad color '${J}' (expected #rgb/#rrggbb/#rrggbbaa)`);if(!/^[0-9a-fA-F]+$/.test(Q))throw Error(`PocketJS: bad color '${J}'`);let $=parseInt(Q,16);if(Q.length===6)return G1($>>>16&255,$>>>8&255,$&255,255);return G1($>>>24&255,$>>>16&255,$>>>8&255,$&255)}function K6(J,Q){let $=Q6[J];if(typeof Q==="string"){if($===K.color)return H6(Q);let Z=Number(Q);if(Number.isNaN(Z))throw Error(`PocketJS: non-numeric value '${Q}' for prop '${J}'`);Q=Z}if($===K.color||$===K.int)return Q>>>0;return Q}var cJ=60,H1=[1,2,3,4,5,6,10,12,15,20,30,60],U0=cJ,n=-1,M6=0,AJ=[];function U6(J){if(!Number.isFinite(J)||J<=0)return cJ;let Q=H1[0];for(let $ of H1)if(Math.abs($-J)Q.at<=n).sort((Q,$)=>Q.at-$.at||Q.seq-$.seq);if(J.length===0)return;AJ=AJ.filter((Q)=>Q.at>n);for(let Q of J)Q.cb()}var V0=0.12,pJ=qJ;function D6(J){pJ=J===void 0?qJ:J&65535}function w6(){pJ=qJ}function U1(J){let Q=Math.max(-1,Math.min(1,(J-128)/127)),$=Math.abs(Q);if($>8&255)}function C6(){return U1(pJ&255)}var V1=new Set,O6=0;function k6(){V1.clear(),O6=0,w6()}function I6(J){for(let Q of[...V1])Q(J)}var YJ=null,P1=null;function A6(J,Q,$){let Z="";for(let X=0;X<$;X++)Z+=String.fromCharCode(J[Q+X]);return Z}function R1(J){let Q=new DataView(J);if(J.byteLength=J.length&&$.slice(0,J.length)===J)Q.push($);return Q.sort(),Q}function SJ(J){P0();let Q=YJ?YJ.get(J):void 0;if(!Q)throw Error("pak: missing key "+J+" (no __pak provided, or the pack is incomplete)");return P1.slice(Q.off,Q.off+Q.len)}var _J=null,A=null,zJ=null,iJ=0,jJ=[],D0=[],w0=[];function w1(J){if(_J=J,A=null,zJ=null,iJ=0,jJ.length=0,D0.length=0,w0.length=0,v){if(v.pressTarget=null,v.target=null,v.spriteDirty=!0,v.fresh=!0,v.vw=0,v.tex>=0){let Q=h();Q.setCursor?.(-1,0,0,0,0),Q.freeTexture?.(v.tex),v.tex=-1}}}function _6(J,Q){J.onPress=Q??void 0}function y6(J,Q){if(J.focusable=Q,I1(),!Q&&A===J)T(null)}function T(J){if(zJ&&zJ!==J)GJ(null);A=J,h().setFocus(J?J.id:0)}function GJ(J){if(zJ===J)return;let Q=h();if(zJ)Q.setActive?.(zJ.id,0);if(zJ=J,J)Q.setActive?.(J.id,1)}function L0(){return jJ.length>0?jJ[jJ.length-1]:_J}function C0(J,Q){if(!J)return;if(J.focusable)Q.push(J);if(!Array.isArray(J.children))return;for(let $=0;$=$.length)return;T($[X])}function x6(){if(!A)return null;let J=L0();if(J&&!l(A,J))return null;for(let Q=D0.length-1;Q>=0;Q--){let $=D0[Q];if(J&&!l($.node,J)&&!l(J,$.node))continue;if(l(A,$.node))return $}return null}function f6(J){let Q=x6();if(!Q)return!1;let $=[];if(C0(Q.node,$),$.length===0){if(A)T(null);return!0}let Z=Q.columns,X=A?$.indexOf(A):-1;if(X<0)return T(L1(J)===1?$[0]:$[$.length-1]),!0;let W=X;switch(J){case"right":if(X+1<$.length&&X%Z0)W=X-1;else if(Q.wrap)W=Math.min($.length-1,Math.floor(X/Z)*Z+Z-1);break;case"down":if(X+Z<$.length)W=X+Z;else if(Q.wrap)W=X%Z;break;case"up":if(X-Z>=0)W=X-Z;else if(Q.wrap){W=X%Z;while(W+Z<$.length)W+=Z}break}if(W!==X)T($[W]);return!0}function g6(){if(!A)return null;let J=L0();if(J&&!l(A,J))return null;for(let Q=w0.length-1;Q>=0;Q--){let $=w0[Q];if(J&&!l($.node,J)&&!l(J,$.node))continue;if(l(A,$.node))return $}return null}function dJ(J){let Q=g6();if(Q&&Q.move(J))return;if(f6(J))return;h6(J)}function C1(){O1(A)}function O1(J){let Q=J;while(Q){if(Q.onPress){Q.onPress();return}Q=Q.parent}}function sJ(J){GJ(J)}function T6(J){T(J),O1(J)}function l(J,Q){if(!J||!Q)return!1;let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function O0(J){if(!J)return null;if(J.focusable)return J;if(!Array.isArray(J.children))return null;for(let Q=0;Q=0;X--){let W=O0(Q.children[X]);if(W){T(W);return}}let Z=Q;while(Z){if(Z.focusable){T(Z);return}Z=Z.parent}}T(null)}var v=null,k1=0;function I1(){k1++}var E6=[1,3,5,9,17,33,65,129,257,513,1985,73,149,147,288,480],v6=[0,0,2,6,14,30,62,126,254,510,62,54,98,96,192,0];function m6(){let J=new Uint8Array(1024);for(let Q=0;Q<16;Q++)for(let $=0;$<16;$++){let Z=E6[Q]>>$&1,X=v6[Q]>>$&1;if(!Z&&!X)continue;let W=(Q*16+$)*4,Y=X?255:0;J[W]=Y,J[W+1]=Y,J[W+2]=Y,J[W+3]=255}return J}function l6(J,Q){let $=J.sprite;J.spriteDirty=!1;let Z=J.tex,X=-1,W=null;if(typeof $.image==="string")try{W=SJ($.image)}catch(Y){if(PJ().strict)throw Y;W=null}else if($.image)W=$.image;if(W){if(X=Q.uploadImgEntry?Q.uploadImgEntry(W):u6(Q,W),X<0&&PJ().strict)throw Error("enableCursor: cursor image rejected (malformed or RLE-only IMG entry)")}if(X<0)X=Q.uploadTexture(m6(),16,16,$6.PSM_8888);if(J.tex=X,Q.setCursor(X,$.hotspot[0],$.hotspot[1],$.size[0],$.size[1]),Z>=0&&Z!==X)Q.freeTexture?.(Z)}function u6(J,Q){if(Q.length<8)return-1;let $=new DataView(Q.buffer,Q.byteOffset,Q.byteLength);if(Q[5]&Z6)return-1;return J.uploadTexture(Q.subarray(8),$.getUint16(0,!0),$.getUint16(2,!0),Q[4])}function aJ(J,Q){if(!J||Q===0)return null;if(J.id===Q)return J;let $=J.children;if(!Array.isArray($))return null;for(let Z=0;Z<$.length;Z++){let X=aJ($[Z],Q);if(X)return X}return null}var rJ=null;function A1(J){rJ=J}function S1(J){let Q=jJ.length>0?jJ[jJ.length-1]:null,$=J;while($){if($.focusable&&(!Q||l($,Q)))return $;$=$.parent}return null}function _1(J,Q,$){return S1(y1(J,Q,$))}function y1(J,Q,$){if($!==void 0)return $===0?null:aJ(rJ??_J,$);let Z=h(),X=Z.hitTestBounds??Z.hitTest;if(!X)return null;return aJ(rJ??_J,X(J,Q))}function c6(J,Q,$){let Z=v,X=h();if(!X.hitTest||!X.setCursor||!X.setCursorPos)return!1;if(Z.vw===0){let G=F1(X);if(Z.vw=G?G.w:z1,Z.vh=G?G.h:j1,Z.x<0)Z.x=Math.floor(Z.vw/2),Z.y=Math.floor(Z.vh/2)}if(Z.spriteDirty)l6(Z,X);let W=L6()*Z.speed,Y=C6()*Z.speed;if(Z.dpadSpeed>0&&W===0&&Y===0){if(J&m.LEFT)W=-Z.dpadSpeed;if(J&m.RIGHT)W=Z.dpadSpeed;if(J&m.UP)Y=-Z.dpadSpeed;if(J&m.DOWN)Y=Z.dpadSpeed}let j=Z.fresh;if(W!==0||Y!==0){let G=V6()/60,I=Math.min(Math.max(Z.x+W*G,0),Z.vw-1),b=Math.min(Math.max(Z.y+Y*G,0),Z.vh-1);if(I!==Z.x||b!==Z.y)Z.x=I,Z.y=b,j=!0}if(j)X.setCursorPos(Z.x,Z.y);let M=(Q|$)&Z.button,V=k1;if(j||M!==0||V!==Z.gen)Z.gen=V,Z.fresh=!1,Z.target=S1(aJ(rJ??_J,X.hitTest(Z.x,Z.y)));let D=Z.target;if(D!==A)T(D);if(Q&Z.button&&D)Z.pressTarget=D;if(Z.pressTarget){if(GJ(D===Z.pressTarget?Z.pressTarget:null),$&Z.button){let G=D===Z.pressTarget;if(Z.pressTarget=null,GJ(null),G)C1()}}else if($&Z.button)GJ(null);return!0}function p6(J){let Q=J&~iJ,$=iJ&~J;if(iJ=J,v&&c6(J,Q,$))return;if($&m.CIRCLE)GJ(null);if(Q===0)return;if(Q&m.DOWN)dJ("down");if(Q&m.RIGHT)dJ("right");if(Q&m.UP)dJ("up");if(Q&m.LEFT)dJ("left");if(Q&m.CIRCLE)GJ(A),C1()}var k0=null;function b1(J){k0=J}function RJ(){if(I1(),k0)k0()}function i6(J,Q){J.debugName=Q||void 0,RJ()}var t={id:t4,type:WJ.view,parent:null,children:[],domNodeType:1,domTag:"root"},h1=Symbol.for("pocketjs.native-node"),I0=1,yJ=3,d=8,d6=new Set(["class","className","style","src","onPress","on:press","focusable","debugName","ref","nodeRef","key","children"]);function oJ(J){return J.domAttrs??={}}function x1(J,Q){let $=J.domNodeType??(bJ(J)?yJ:I0),Z=$===yJ?nJ(J.text??""):$===d?r6(J.domData??""):m1(J.domTag??eJ(J));for(let X of Object.keys(J.domAttrs??{}))A0(Z,X,J.domAttrs[X]);if(Q)for(let X of J.children)e(Z,x1(X,!0));return Z}function A0(J,Q,$){if(d6.has(Q)){wJ(J,Q,$,J.domAttrs?.[Q]);return}if($==null)delete oJ(J)[Q];else oJ(J)[Q]=$}function S0(J){if(J[h1]===!0)return J;return Object.defineProperty(J,h1,{value:!0}),Object.defineProperties(J,{nodeType:{configurable:!0,get(){return J.domNodeType??(bJ(J)?yJ:I0)}},nodeValue:{configurable:!0,get(){return J.domNodeType===d?J.domData??"":J.text??""},set($){if(J.domNodeType===d)J.domData=String($??"");else tJ(J,String($??""))}},data:{configurable:!0,get(){return J.domNodeType===d?J.domData??"":J.text??""},set($){if(J.domNodeType===d)J.domData=String($??"");else tJ(J,String($??""))}},textContent:{configurable:!0,get(){if(J.domNodeType===d)return J.domData??"";if(bJ(J))return J.text??"";return J.children.map(($)=>$.text??"").join("")},set($){let Z=String($??"");if(J.domNodeType===d)J.domData=Z;else if(bJ(J))tJ(J,Z);else if($8(J),Z)e(J,nJ(Z))}},parentNode:{configurable:!0,get(){return J.parent}},parentElement:{configurable:!0,get(){return J.parent}},childNodes:{configurable:!0,get(){return J.children}},firstChild:{configurable:!0,get(){return J.children[0]??null}},lastChild:{configurable:!0,get(){return J.children[J.children.length-1]??null}},nextSibling:{configurable:!0,get(){return u1(J)??null}},previousSibling:{configurable:!0,get(){let $=J.parent;if(!$)return null;let Z=$.children.indexOf(J);return Z>0?$.children[Z-1]:null}},tagName:{configurable:!0,get(){return(J.domTag??eJ(J)).toUpperCase()}},nodeName:{configurable:!0,get(){if(J.domNodeType===yJ)return"#text";if(J.domNodeType===d)return"#comment";return(J.domTag??eJ(J)).toUpperCase()}},className:{configurable:!0,get(){return String(J.domAttrs?.class??"")},set($){wJ(J,"class",$,J.domAttrs?.class)}},isConnected:{configurable:!0,get(){let $=J;while($){if($===t)return!0;$=$.parent}return!1}}}),Object.assign(J,{appendChild($){return e(J,$),$},insertBefore($,Z){return e(J,$,Z??null),$},removeChild($){return hJ(J,$),$},replaceChild($,Z){return e(J,$,Z),hJ(J,Z),Z},cloneNode($=!1){return x1(J,!!$)},remove(){if(J.parent)hJ(J.parent,J)},setAttribute($,Z){A0(J,$,Z)},removeAttribute($){A0(J,$,void 0)},getAttribute($){let Z=J.domAttrs?.[$];return Z==null?null:String(Z)},hasAttribute($){return J.domAttrs?.[$]!=null},hasChildNodes(){return J.children.length>0},contains($){let Z=$??null;while(Z){if(Z===J)return!0;Z=Z.parent}return!1},addEventListener(){},removeEventListener(){}},{style:{length:0,item:()=>""},classList:{add(){},remove(){}}}),J}S0(t);var _0=null;function s6(J){_0=J}var y0={unknownClass:0,unknownTexture:0},f1=new Map;function g1(J,Q){f1.set(J,Q)}var T1=new Map;function N1(J,Q){T1.set(J,Q)}var DJ=new Set,a6=new Set;function E1(J){if(!J)return!1;if(a6.has(J))return!0;if(J.children){for(let Q=0;Q - only view/text/image exist`);return S0({id:h().createNode(Q),type:Q,parent:null,children:[],domNodeType:I0,domTag:J})}function nJ(J){let Q=h(),$=Q.createNode(WJ.text);return Q.setText($,J),S0({id:$,type:WJ.text,parent:null,children:[],text:J,domNodeType:yJ,domTag:"#text"})}function r6(J=""){let Q=nJ("");return Q.domNodeType=d,Q.domTag="#comment",Q.domData=J,Q}function tJ(J,Q){h().replaceText(J.id,Q),J.text=Q,RJ()}function bJ(J){return J.type===WJ.text}function l1(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);if($>=0)Q.children.splice($,1);J.parent=null}function e(J,Q,$){let Z=h();if(l1(Q),DJ.delete(Q),Z.insertBefore(J.id,Q.id,$?$.id:0),$){let X=J.children.indexOf($);if(X<0)throw Error("PocketJS: insert anchor is not a child of parent");J.children.splice(X,0,Q)}else J.children.push(Q);Q.parent=J,RJ()}function hJ(J,Q){if(!Q)return;N6(Q),h().removeChild(J.id,Q.id),l1(Q),DJ.add(Q),RJ()}function o6(J){return J.parent??void 0}function n6(J){return J.children[0]}function u1(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);return $>=0?Q.children[$+1]:void 0}function t6(J,Q){let $=h();if(RJ(),Q==null||Q===""){$.setStyle(J.id,e4);return}if(typeof Q!=="string")throw Error("PocketJS: class must be a string literal of utilities");let Z=_0?_0(Q):void 0;if(Z===void 0){if(PJ().strict)throw Error(`PocketJS: unknown class "${Q}" - not in the compiled style table (dynamic classes must be ternaries of full literals)`);y0.unknownClass++;return}$.setStyle(J.id,Z)}function e6(J,Q){let $=h();if(Q==null||Q===""){$.setImage(J.id,-1);return}if(typeof Q!=="string")throw Error("PocketJS: src must be a string key");let Z=f1.get(Q);if(Z===void 0){if(PJ().strict)throw Error(`PocketJS: unknown image src "${Q}" - no texture registered under that key`);y0.unknownTexture++;return}$.setImage(J.id,Z)}function J8(J,Q){let $=h();if(Q==null||Q===""){$.setSprite(J.id,-1,0,0,0);return}if(typeof Q!=="string")throw Error("PocketJS: sprite must be a string key");let Z=T1.get(Q);if(Z===void 0){if(PJ().strict)throw Error(`PocketJS: unknown sprite "${Q}" - no sprite atlas registered under that key`);y0.unknownTexture++;return}$.setSprite(J.id,Z.handle,Z.frames,Z.cols,Z.step)}function Q8(J,Q,$){let Z=h(),X=Q??{},W=$??{},Y=!1;for(let j in X){let M=X[j];if(W[j]===M)continue;let V=J6[j];if(V===void 0)throw Error(`PocketJS: unknown style prop '${j}' (see spec PROP)`);Z.setProp(J.id,V,K6(j,M)),Y=!0}if(Y)RJ()}function wJ(J,Q,$,Z){if($===Z&&Q!=="style")return $;if(Q==="className")Q="class";if(Q!=="children"&&Q!=="key"&&Q!=="ref"&&Q!=="nodeRef")if($==null)delete oJ(J)[Q];else oJ(J)[Q]=$;switch(Q){case"class":return t6(J,$),$;case"onPress":case"on:press":return _6(J,$),$;case"src":return e6(J,$),$;case"sprite":return J8(J,$),$;case"style":return Q8(J,$,Z),$;case"focusable":return y6(J,!!$),$;case"debugName":return i6(J,$==null?void 0:String($)),$;case"ref":case"nodeRef":case"key":case"children":return $;default:break}if(Q==="classList")throw Error("PocketJS: classList is not supported - use ternaries of full class literals");if(Q.startsWith("on:")||Q.startsWith("bool:")||Q.startsWith("prop:"))throw Error(`PocketJS: unsupported namespaced attribute '${Q}'`);throw Error(`PocketJS: unknown property '${Q}' on <${eJ(J)}>`)}function $8(J){for(let Q of[...J.children])hJ(J,Q)}function eJ(J){for(let Q of Object.keys(WJ))if(WJ[Q]===J.type)return Q;return String(J.type)}function Z8(J,Q,$,Z){if(Q==="ref"&&typeof $==="function"){$(J);return}wJ(J,Q,$,Z)}var X8=n4({createElement:m1,createTextNode:nJ,replaceText:tJ,isTextNode:bJ,setProperty:Z8,insertNode(J,Q,$){e(J,Q,$)},removeNode(J,Q){hJ(J,Q)},getParentNode:o6,getFirstChild:n6,getNextSibling:u1}),{render:W8,effect:p2,memo:s,createComponent:R,createElement:c1,insert:i2,spread:q8,mergeProps:d2,use:s2}=X8,a2={linear:E.Easing.Linear,in:E.Easing.EaseIn,out:E.Easing.EaseOut,"in-out":E.Easing.EaseInOut,"out-back":E.Easing.OutBack,spring:E.Easing.Spring,"spring-bouncy":E.Easing.SpringBouncy},Y8=null;function p1(J){Y8=J}function z8(J,Q){if(!J)return;if(typeof J==="function")J(Q);else if("current"in J)J.current=Q}function i1(J,Q){let $=c1(J);return q8($,Q,!1),z8(Q.nodeRef,$),$}function _(J){return i1("view",J)}function y(J){return i1("text",J)}var r2=new WeakMap,o2=new WeakMap,j8={}!==null?Object.freeze({...{}}):Object.freeze({}),n2=Object.freeze({target:"",pixelRatio:Number.isInteger(1)?1:1,features:j8}),t2=new Map,JJ=36000,b0=30,G8=30,q={ops:null,transport:null,app:void 0,frame:0,tape:new Uint16Array(JJ),tapeAnalog:new Uint16Array(JJ),tapeTouch:null,tapeStart:0,tapeLen:0,tapeFirstFrame:0,replayMasks:null,replayAnalog:null,replayTouch:null,replayAt:0,paused:!1,stepQueued:0,inspectReportId:null,inspectAskedAt:0,treeDirty:!0,treeSentAt:-b0,saidHello:!1,hostCalls:0};function F8(J){let Q=globalThis;if(!Q.console)Q.console={log(){},warn(){},error(){}};q.ops=J,q.frame=0,q.tapeStart=0,q.tapeLen=0,q.tapeFirstFrame=0,q.tapeTouch=null,q.replayMasks=null,q.replayAnalog=null,q.replayTouch=null,q.paused=!1,q.stepQueued=0,q.inspectReportId=null,q.inspectAskedAt=0,q.treeDirty=!0,q.treeSentAt=-b0,q.saidHello=!1,q.hostCalls=0,q.app=globalThis.__pocketApp;let $=globalThis.__pocketDevtoolsTransport;if($)q.transport=$;else if(J.__dbgActive?.()&&J.__dbgPoll&&J.__dbgSend)q.transport={send:(Z)=>J.__dbgSend(Z),recv:()=>J.__dbgPoll(),everyFrames:10};else q.transport=null;if(q.transport)b1(()=>{q.treeDirty=!0}),R8();else b1(null);globalThis.__pocketDevtools=w8}function B8(J){return(Q,$,Z,X)=>{if(q.hostCalls++,q.transport)K8(),V8();let W=Q,Y=$===void 0?qJ:$&65535,j=Z,M=X;if(q.replayMasks)if(q.replayAt0?$.slice(0,8):null;if(Z&&!q.tapeTouch)q.tapeTouch=Array(JJ).fill(null);if(q.tapeLen1||Q.length===1&&Q[0][0]!==qJ)J.analog=Q;if(q.tapeTouch){let $=[];for(let Z=0;Z0)J.v=2,J.touch=$}return J}function a1(J,Q,$){let Z=new Uint16Array($).fill(Q),X=0;for(let[W,Y]of J)Z.fill(W,X,Math.min(X+Y,$)),X+=Y;return Z}function r1(J){let Q=0;for(let[,$]of J.masks)Q+=$;return a1(J.masks,0,Q)}function o1(J){let Q=0;for(let[,$]of J.masks)Q+=$;return a1(J.analog??[],qJ,Q)}function n1(J){let Q=0;for(let[,Z]of J.masks)Q+=Z;let $=Array(Q).fill(void 0);for(let[Z,X]of J.touch??[])if(Z>=0&&Z1&&q.hostCalls%Q!==0)return;if(!q.saidHello)q.saidHello=!0,g({t:"hello",app:q.app,host:D8(),frame:q.frame});for(let $=0;$<64;$++){let Z=J.recv();if(!Z)break;for(let X of Z.split(` +`))if(X.trim())M8(X)}}function M8(J){let Q;try{Q=JSON.parse(J)}catch{return}let $=q.ops;switch(Q.t){case"inspect":{let Z=typeof Q.id==="number"?Q.id:0;if($?.debugInspect?.(Z),q.inspectReportId=Z||null,q.inspectAskedAt=q.hostCalls,!Z)g({t:"inspect",id:0,rect:null});break}case"pause":q.paused=!0,q.stepQueued=0,$?.debugPause?.(!0),h0();break;case"resume":q.paused=!1,$?.debugPause?.(!1),h0();break;case"step":q.stepQueued+=typeof Q.n==="number"&&Q.n>0?Q.n:1;break;case"getTree":t1();break;case"eval":{let Z=!0,X;try{X=J0((0,eval)(String(Q.code)))}catch(W){Z=!1,X=W instanceof Error?`${W.name}: ${W.message}`:String(W)}g({t:"evalResult",id:Q.id,ok:Z,value:X});break}case"dumpTape":g({t:"tape",tape:s1()});break;case"devStats":{let Z=null,X=$?.debugStats?.();if(X)try{Z=JSON.parse(X)}catch{Z=null}g({t:"devStats",frame:q.frame,data:Z});break}case"screenshot":{if($?.__dbgShot?.())g({t:"screenshotRaw",file:"shot.raw",w:480,h:272,stride:512,frame:q.frame});else g({t:"log",level:"warn",args:["screenshot: not supported on this host"]});break}case"replay":{let Z=Q.tape;if(Z&&Array.isArray(Z.masks))q.replayMasks=r1(Z),q.replayAnalog=Z.analog?o1(Z):null,q.replayTouch=Z.touch?n1(Z):null,q.replayAt=0;break}default:break}}function U8(){if(q.treeDirty&&q.frame-q.treeSentAt>=b0)t1();if(q.frame%G8===0)h0()}function V8(){let J=q.inspectReportId;if(J==null)return;let Q=q.ops;if(!Q?.debugRectXY||!Q.debugRectWH){q.inspectReportId=null;return}let $=Q.debugRectXY();if($===-1){if(q.hostCalls-q.inspectAskedAt>60)q.inspectReportId=null,g({t:"inspect",id:J,rect:null});return}let Z=Q.debugRectWH();q.inspectReportId=null,g({t:"inspect",id:J,rect:[$<<16>>16,$>>16,Z&65535,Z>>16&65535]})}function h0(){g({t:"stats",frame:q.frame,nodes:Q4(t),tapeLen:q.tapeLen,paused:q.paused})}function t1(){q.treeDirty=!1,q.treeSentAt=q.frame,g({t:"tree",frame:q.frame,root:J4(t)})}function P8(J){if(J==null||typeof J!=="object")return!1;let Q=J;return typeof Q.id==="number"&&typeof Q.type==="number"}function x0(J,Q){if(Array.isArray(J)){for(let $ of J)x0($,Q);return}if(P8(J)){Q(J);return}if(J!=null&&typeof J==="object"){let $=J.nodes;if($!==void 0)x0($,Q)}}function e1(J,Q){let $=Array.isArray(J.children)?J.children:[];for(let Z of $)x0(Z,Q)}function J4(J){let Q={i:J.id,t:J.domTag??String(J.type)};if(J.debugName)Q.n=J.debugName;let $=J.domAttrs?.class;if(typeof $==="string"&&$)Q.c=$;if(J.text)Q.x=J.text.length>80?J.text.slice(0,79)+"…":J.text;let Z=[];if(e1(J,(X)=>{if(X.domNodeType===8)return;Z.push(J4(X))}),Z.length)Q.k=Z;return Q}function Q4(J){let Q=1;return e1(J,($)=>{Q+=Q4($)}),Q}function R8(){let J=globalThis;if(!J.console)J.console={};let Q=J.console;if(Q.__pocketBridged)return;Q.__pocketBridged=!0;for(let $ of["log","warn","error"]){let Z=Q[$];Q[$]=(...X)=>{g({t:"log",level:$,args:X.map((W)=>J0(W))}),Z?.apply(Q,X)}}}function J0(J,Q=0){if(J===void 0)return"undefined";if(J===null)return"null";let $=typeof J;if($==="string"){let W=J;return Q===0?$4(W):JSON.stringify($4(W))}if($==="number"||$==="boolean"||$==="bigint")return String(J);if($==="function"){let W=J.name;return W?`[function ${W}]`:"[function]"}if(Q>=3)return Array.isArray(J)?"[…]":"{…}";if(Array.isArray(J)){let W=J.slice(0,20).map((Y)=>J0(Y,Q+1));if(J.length>20)W.push(`… ${J.length-20} more`);return`[${W.join(", ")}]`}if(J instanceof Error)return`${J.name}: ${J.message}`;return`{${Object.entries(J).slice(0,20).map(([W,Y])=>`${W}: ${J0(Y,Q+1)}`).join(", ")}}`}function $4(J){return J.length>200?J.slice(0,199)+"…":J}function D8(){let J=q.ops;if(typeof J?.__host==="string")return J.__host;if(J?.__textures!==void 0)return"psp";if(typeof globalThis.document<"u")return"web";return"headless"}var w8={get frame(){return q.frame},dumpTape:()=>s1(),replay:(J)=>{q.replayMasks=r1(J),q.replayAnalog=J.analog?o1(J):null,q.replayTouch=J.touch?n1(J):null,q.replayAt=0}},Z4=new Map,f0=new Map;function X4(J){return J.trim().replace(/\s+/g," ")}function W4(J){return J.split(" ").sort().join(" ")}var q4=-1;function L8(J){for(let Q of Object.keys(J)){let $=J[Q],Z=X4(Q);Z4.set(Z,$);let X=W4(Z),W=f0.get(X);f0.set(X,W!==void 0&&W!==$?q4:$)}}function C8(J){let Q=X4(J),$=Z4.get(Q);if($!==void 0)return $;let Z=f0.get(W4(Q));return Z===q4?void 0:Z}var g0=9,O8=(1<{let X=($&I8)!==0,W=X?T0:g0,Y=X?A8:O8;return Object.freeze({id:$>>>(X?S8:k8)&255,x:$&Y,y:$>>>W&Y,hit:Q?.[Z]})}))}function y8(){return Q0}function b8(){Q0=N0}var Y4=8,QJ=8,h8=3,x8=8,f8=6,g8=0.5,u=1,xJ=2,E0=4,FJ=8,LJ=[],z4=0,$0=0,$J=Array.from({length:Y4},(J,Q)=>({slot:Q,used:!1,present:!1,id:0,x:0,y:0,startX:0,startY:0,dx:0,dy:0,fdx:0,fdy:0,vx:0,vy:0,downFrame:0,frames:0,histX:new Int16Array(QJ),histY:new Int16Array(QJ),histHead:0,histLen:0,owners:[],claimedBy:null}));function T8(J,Q){let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function N8(J,Q,$,Z){let X=J.opts.region;if(!X)return!0;let W=X.node?.();if(W){if(Z.hit===void 0)Z.hit=y1(Q,$,Z.fact);let j=Z.hit;if(j)return T8(j,W)}let Y=X.rect?.();if(!Y)return!1;return Q>=Y.x&&Q=Y.y&&$=0;Y--){let j=LJ[Y];if(j.disposed)continue;if(z4>0&&!j.opts.allowWhenBlocked)continue;if(!N8(j,$,Z,W))continue;j.flags[J.slot]=u,J.owners.push(j)}for(let Y of J.owners)Y.opts.onDown?.(J)}function m8(J,Q,$){if(J.present=!0,J.fdx=Q-J.x,J.fdy=$-J.y,J.x=Q,J.y=$,J.dx=Q-J.startX,J.dy=$-J.startY,J.frames++,J.histX[J.histHead]=Q,J.histY[J.histHead]=$,J.histHead=(J.histHead+1)%QJ,J.histLenY||Z>Y))X.flags[J.slot]|=xJ}if(!J.claimedBy)for(let X of J.owners){let W=X.flags[J.slot];if(!(W&u)||W&(xJ|E0))continue;if(!X.opts.onLongPress)continue;let Y=Math.max(1,Math.round((X.opts.longPressSeconds??g8)*K1()));if(J.frames=0)LJ.splice($,1)},cancel(){if(!Q.disposed)F4(Q)},get panning(){for(let $ of $J)if($.used&&Q.flags[$.slot]&FJ)return!0;return!1}}}function i8(J){let Q=p8(J);return F0(()=>Q.dispose()),Q}function B4(){LJ.length=0,z4=0,$0=0;for(let J of $J)J.used=!1,J.present=!1,J.owners.length=0,J.claimedBy=null}function d8(){return i8({onDown:(J)=>{let Q=_1(J.x,J.y,J.hit);if(Q)sJ(Q)},onTap:(J)=>{sJ(null);let Q=_1(J.x,J.y,J.hit);if(Q)T6(Q)},onUp:()=>sJ(null),onCancel:()=>sJ(null)})}var s8=1,v0=new Map,Z0=[];function a8(){let J=globalThis.__pocketEffectTrace;return typeof J==="function"?J:null}function r8(){s8=1,v0.clear(),Z0=[]}function o8(){if(Z0.length===0)return;let J=Z0;Z0=[];for(let{id:Q,result:$}of J){let Z=v0.get(Q);if(!Z)continue;v0.delete(Q),a8()?.({t:"delivery",frame:M1(),id:Q,kind:Z.kind}),Z.onResult($)}}var H4=new Set;function n8(){if(H4.size===0)return;for(let J of H4)J()}var t8={"flex-col w-full h-full bg-slate-50":0,"relative grow px-6 pt-4 flex-col":1,"absolute left-[628] top-[16] w-[68] h-[132] items-center justify-center rounded-xl bg-orange-100":2,"absolute left-[628] top-[742] w-[68] h-[132] items-center justify-center rounded-xl bg-orange-100":3,"w-[584] h-[890] flex-col gap-[12]":4,"h-[126] px-5 flex-row items-center justify-between rounded-xl shadow bg-white border-slate-100":5,"w-[390] flex-col gap-2":6,"text-lg text-slate-900 font-bold":7,"text-base text-slate-500":8,"text-base text-indigo-600 font-bold":9,"absolute left-[24] top-[16] w-[672] h-[890] bg-slate-50":10,"h-[96]":11,"relative flex-col w-full h-full bg-slate-50 overflow-hidden":12,"absolute inset-0 z-50 flex-col items-center justify-center":13,"absolute inset-0 bg-slate-950":14,"flex-col gap-2 w-[328] p-3 rounded-xl shadow-lg bg-white border-slate-200":15,"absolute left-3 right-3 bottom-3 flex-row items-center justify-between px-2 py-1 rounded-lg shadow-md bg-white border-slate-200":16,"flex-row flex-wrap":17,grow:18,"text-2xl text-white font-bold":19,"text-2xl text-slate-950 font-bold":20,"text-xl text-slate-900 font-bold":21,"text-base text-slate-600 font-bold":22,"text-base text-slate-500 font-bold":23,"px-3 py-2 rounded-lg bg-slate-100":24,"px-3 py-2 rounded-lg bg-indigo-100":25,"text-base text-indigo-700 font-bold":26,"px-3 py-2 rounded-lg bg-emerald-100":27,"text-base text-emerald-700 font-bold":28,"px-3 py-2 rounded-lg bg-amber-100":29,"text-base text-amber-700 font-bold":30,"px-3 py-2 rounded-lg bg-red-100":31,"text-base text-red-500 font-bold":32,"w-[34] h-[34] rounded-lg bg-amber-400":33,"w-[34] h-[34] rounded-lg bg-red-500":34,"w-[34] h-[34] rounded-lg bg-slate-800":35,"w-[34] h-[34] rounded-lg bg-emerald-500":36,"h-[112] px-6 flex-row items-center justify-between bg-slate-950":37,"flex-row items-center gap-4":38,"w-[34] text-2xl text-white font-bold":39,"w-[332] flex-col items-end gap-2":40,"text-base text-slate-300 font-bold":41,"text-base text-slate-400":42,"h-[166] px-6 pt-6 flex-col gap-3":43,"text-base text-orange-600 font-bold":44,"text-lg text-slate-600":45,"h-[44] px-1 flex-row items-center justify-between":46,"w-full h-full items-center justify-center rounded-xl bg-slate-200":47,"w-full h-full items-center justify-center rounded-xl bg-red-100":48,"w-full h-full items-center justify-center rounded-xl bg-slate-100":49,"w-full h-full items-center justify-center rounded-xl bg-orange-600":50,"text-lg text-slate-500 font-bold":51,"text-lg text-red-500 font-bold":52,"text-lg text-white font-bold":53,"w-full h-[150] px-5 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":54,"w-full h-[430] px-12 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":55,"w-[88] h-[88] items-center justify-center rounded-xl bg-indigo-100":56,"w-[88] h-[88] items-center justify-center rounded-xl bg-slate-100":57,"text-2xl text-indigo-700 font-bold":58,"text-2xl text-slate-600 font-bold":59,"pt-7 text-2xl text-slate-900 font-bold":60,"pt-4 text-lg text-slate-500":61,"text-xl text-emerald-600 font-bold":62,"text-xl text-red-500 font-bold":63,"w-full h-full px-5 py-4 flex-col gap-3 rounded-xl shadow bg-white border-slate-100":64,"text-base text-red-300":65,"text-base text-slate-300":66,"text-base text-red-500":67,"w-full h-full px-6 flex-row items-center bg-slate-950":68,"w-full h-full flex-row items-center":69};if(typeof globalThis.queueMicrotask!=="function")globalThis.queueMicrotask=(J)=>{Promise.resolve().then(J)};var e8="ui:styles",J2="ui:font.",K4="ui:img.",M4="ui:sprite.";function Q2(){return globalThis.ui}function $2(J){if(J.__textures)return;for(let Q of R0(K4)){let $=SJ(Q),Z;if(J.uploadImgEntry)Z=J.uploadImgEntry($);else{let X=new DataView($.buffer,$.byteOffset,$.byteLength);Z=J.uploadTexture($.subarray(8),X.getUint16(0,!0),X.getUint16(2,!0),$[4])}if(Z>=0)g1(Q.slice(K4.length),Z)}}function Z2(J){if(J.__sprites)return;for(let Q of R0(M4)){let $=SJ(Q),Z=new DataView($.buffer,$.byteOffset,$.byteLength),X=Z.getUint16(0,!0),W=Z.getUint16(2,!0),Y=$[4],j=Z.getUint16(6,!0),M=Z.getUint16(8,!0),V=Z.getUint16(10,!0),D=J.uploadTexture($.subarray(16),X,W,Y);if(D>=0)N1(Q.slice(M4.length),{handle:D,frames:j,cols:M,step:V})}}function U4(J){let Q=c1("view");return wJ(Q,"style",J,void 0),Q}var X0=null,W0=null;function X2(J,Q){if(!X0||!W0)return;wJ(X0,"style",{width:J,height:Q,overflow:E.Overflow.Hidden},void 0),wJ(W0,"style",{width:J,height:Q,posType:E.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000},void 0);let $=h();$.__viewport={w:J,h:Q}}function W2(J,Q={}){let $=j6(Q.ops);if(G6($),s6(C8),Q.styles)L8(Q.styles);let Z=$.kind==="native"?$.ops.__textures:void 0;if($.kind==="native"){if(Z)for(let I in Z)g1(I,Z[I]);let G=$.ops.__sprites;if(G)for(let I in G)N1(I,G[I])}if($.kind==="injected"||Z===void 0){if(Q.pak)D1(Q.pak);if(S6()){for(let G of R0())if(G===e8)$.ops.loadStyles?.(SJ(G));else if(G.startsWith(J2))$.ops.loadFontAtlas?.(SJ(G))}}let X=F1($.ops),W=X?.w??z1,Y=X?.h??j1,j=U4({width:W,height:Y,overflow:E.Overflow.Hidden}),M=U4({width:W,height:Y,posType:E.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000,hitPass:1});e(t,j),e(t,M),p1(M),X0=j,W0=M,w1(j),A1(t),k6(),B4(),d8(),P6(),r8(),F8($.ops),F6(B8((G,I,b,x)=>{R6(),D6(I),_8(b,x),n8(),o8(),c8(),I6(G),p6(G),v1()}));let V=W8(J,j),D=B6(X2);return()=>{D(),b8(),B4(),V(),w1(null),A1(null),p1(null),X0=null,W0=null;for(let G of t.children.splice(0))G.parent=null,$.ops.destroyNode(G.id);v1()}}function q2(J,Q={}){let $=Q.ops??Q2();if(!$)throw Error("PocketJS: mount() requires globalThis.ui or opts.ops");if(Q.pak)D1(Q.pak);return $2($),Z2($),W2(J,{ops:$,styles:Q.styles??t8,pak:Q.pak})}var V4="$b",Y2=9007199254740991,z2=":memory:",CJ="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";function j2(J){let Q="";for(let $=0;$>2]+CJ[(Z&3)<<4|X>>4],Q+=$+1>6]:"=",Q+=$+2Y2)throw Error("db: integer exceeds DB_MAX_SAFE_INTEGER");return J}function B2(J){if(J!==null&&typeof J==="object")return G2(J[V4]);return J}function H2(J){if(Array.isArray(J))return JSON.stringify(J.map(P4));let Q={};for(let[$,Z]of Object.entries(J))Q[$]=P4(Z);return JSON.stringify(Q)}class m0{cols=[];constructor(J,Q,$){this.ops=J,this.handle=Q,this.sql=$}get columnNames(){return this.cols}execute(J){let Q=this.ops.query(this.handle,this.sql,H2(J)),$=JSON.parse(Q);if($.error!==void 0)throw Error(`db: ${$.error}`);return this.cols=$.cols??[],$}get(...J){let Q=this.values(...J);if(Q.length===0)return null;let $={};return this.cols.forEach((Z,X)=>$[Z]=Q[0][X]),$}all(...J){return this.values(...J).map(($)=>{let Z={};return this.cols.forEach((X,W)=>Z[X]=$[W]),Z})}values(...J){let Q=R4(J);return(this.execute(Q).rows??[]).map((Z)=>Z.map(B2))}run(...J){let Q=this.execute(R4(J));return{changes:Q.changes??0,lastInsertRowid:Q.lastInsertRowid??0}}}function R4(J){if(J.length===1&&Array.isArray(J[0]))return J[0];if(J.length===1&&J[0]!==null&&typeof J[0]==="object"&&!(J[0]instanceof Uint8Array))return J[0];return J}class D4{statements=new Map;txDepth=0;constructor(J=z2){let Q=F2();if(!Q)throw Error("db: globalThis.db is not mounted — declare `data.sqlite` in pocket.json requires");let $=Q.open(J);if($<0)throw Error(`db: open(${JSON.stringify(J)}) refused`);this.ops=Q,this.handle=$}query(J){let Q=this.statements.get(J);if(!Q)Q=new m0(this.ops,this.handle,J),this.statements.set(J,Q);return Q}prepare(J){return new m0(this.ops,this.handle,J)}run(J,Q=[]){return this.query(J).run(Q)}exec(J){if(this.ops.exec(this.handle,J)!==0)throw Error(`db: ${this.ops.lastError(this.handle)}`)}transaction(J){return(...Q)=>{let $=`pocket_tx_${this.txDepth}`,[Z,X,W]=this.txDepth===0?["BEGIN","COMMIT","ROLLBACK"]:[`SAVEPOINT ${$}`,`RELEASE ${$}`,`ROLLBACK TO ${$}; RELEASE ${$}`];this.exec(Z),this.txDepth++;try{let Y=J(...Q);return this.txDepth--,this.exec(X),Y}catch(Y){throw this.txDepth--,this.exec(W),Y}}}close(){this.statements.clear(),this.ops.close(this.handle)}}var w4={appTitle:"text-2xl text-white font-bold",pageTitle:"text-2xl text-slate-950 font-bold",heading:"text-xl text-slate-900 font-bold",label:"text-base text-slate-600 font-bold",captionStrong:"text-base text-slate-500 font-bold"},q0={neutral:{surface:"px-3 py-2 rounded-lg bg-slate-100",text:"text-base text-slate-600 font-bold"},info:{surface:"px-3 py-2 rounded-lg bg-indigo-100",text:"text-base text-indigo-700 font-bold"},success:{surface:"px-3 py-2 rounded-lg bg-emerald-100",text:"text-base text-emerald-700 font-bold"},warning:{surface:"px-3 py-2 rounded-lg bg-amber-100",text:"text-base text-amber-700 font-bold"},danger:{surface:"px-3 py-2 rounded-lg bg-red-100",text:"text-base text-red-500 font-bold"}};function K2(J){let Q=()=>J.accent==="busy"?"w-[34] h-[34] rounded-lg bg-amber-400":J.accent==="danger"?"w-[34] h-[34] rounded-lg bg-red-500":J.accent==="none"?"w-[34] h-[34] rounded-lg bg-slate-800":"w-[34] h-[34] rounded-lg bg-emerald-500";return R(_,{class:"h-[112] px-6 flex-row items-center justify-between bg-slate-950",get children(){return[R(_,{class:"flex-row items-center gap-4",get children(){return[s(()=>s(()=>!!J.back)()?R(y,{class:"w-[34] text-2xl text-white font-bold",children:"‹"}):R(_,{get["class"](){return Q()}})),R(y,{get["class"](){return w4.appTitle},get children(){return J.title}})]}}),R(_,{class:"w-[332] flex-col items-end gap-2",get children(){return[R(y,{class:"text-base text-slate-300 font-bold",get children(){return J.metaTop??""}}),R(y,{class:"text-base text-slate-400",get children(){return J.metaBottom??""}})]}})]}})}function M2(J){return R(_,{class:"h-[166] px-6 pt-6 flex-col gap-3",get children(){return[R(y,{get["class"](){return J.tone==="info"?"text-base text-indigo-700 font-bold":"text-base text-orange-600 font-bold"},get children(){return J.eyebrow}}),R(y,{get["class"](){return w4.pageTitle},get children(){return J.title}}),R(y,{class:"text-lg text-slate-600",get children(){return J.description}})]}})}function U2(J){return R(_,{get["class"](){return J.compact?"w-full h-[150] px-5 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":"w-full h-[430] px-12 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100"},get children(){return[s(()=>s(()=>!!J.icon)()?R(_,{get["class"](){return J.tone==="info"?"w-[88] h-[88] items-center justify-center rounded-xl bg-indigo-100":"w-[88] h-[88] items-center justify-center rounded-xl bg-slate-100"},get children(){return R(y,{get["class"](){return J.tone==="info"?"text-2xl text-indigo-700 font-bold":"text-2xl text-slate-600 font-bold"},get children(){return J.icon}})}}):null),R(y,{get["class"](){return J.icon?"pt-7 text-2xl text-slate-900 font-bold":"text-lg text-slate-500 font-bold"},get children(){return J.title}}),s(()=>s(()=>!!J.detail)()?R(y,{class:"pt-4 text-lg text-slate-500",get children(){return J.detail}}):null)]}})}function V2(J){let Q=()=>J.dark?J.tone==="danger"?"text-base text-red-300":"text-base text-slate-300":J.tone==="danger"?"text-base text-red-500":"text-base text-slate-500";return R(_,{get["class"](){return J.dark?"w-full h-full px-6 flex-row items-center bg-slate-950":"w-full h-full flex-row items-center"},get children(){return R(y,{get["class"](){return Q()},get children(){return J.text}})}})}function P2(J){return[R(_,{get["class"](){return J.top},get children(){return R(y,{class:"text-base text-orange-600 font-bold",children:"UP"})}}),R(_,{get["class"](){return J.bottom},get children(){return R(y,{class:"text-base text-orange-600 font-bold",children:"DN"})}})]}var R2=5,gJ=10,l0=6,L4=new D4("exa"),[TJ,Y0]=XJ([]),[C4,u0]=XJ(!1),[c0,z0]=XJ(0),[O4,p0]=XJ("SEARCH HISTORY IS LOCAL"),j0=-1,i0=!1;function D2(J){try{return JSON.parse(J)}catch{return null}}function w2(J){let Q=new Date(J*1000).toISOString();return Q.slice(0,10)+" · "+Q.slice(11,16)+" UTC"}function k4(J){return L4.query(` + SELECT id,query,searched_at,status,result_count,top_title,error + FROM searches ORDER BY id DESC LIMIT ? OFFSET ? + `).all(gJ+1,J)}function L2(){if(i0||!C4())return;i0=!0;try{let J=k4(TJ().length);Y0((Q)=>[...Q,...J.slice(0,gJ)]),u0(J.length>gJ)}finally{i0=!1}}function I4(J){if(J>0&&c0()+l0+4>TJ().length&&C4())L2();z0(($)=>J<0?Math.max(0,$-4):Math.min(Math.max(0,TJ().length-l0),$+4))}function A4(J){if(j0===J)return;try{let Q=L4.query("PRAGMA user_version").get();if(Number(Q?.user_version??0)!==R2){Y0([]),u0(!1),z0(0),p0("SEARCH HISTORY IS LOCAL");return}let $=k4(0),Z=$.slice(0,gJ);Y0(Z),u0($.length>gJ),z0(0),p0(Z[0]?.status==="error"?String(Z[0].error||"EXA SEARCH FAILED").slice(0,80):Z.length?"SEARCH HISTORY UPDATED FROM SQLITE":"SEARCH HISTORY IS LOCAL"),j0=J}catch{Y0([]),z0(0),p0("SEARCH HISTORY IS LOCAL")}}function C2(){return R(_,{class:"flex-col w-full h-full bg-slate-50",get children(){return[R(K2,{title:"EXA RESEARCH",back:!0,metaTop:"POCKET APP",metaBottom:"SQLITE HISTORY"}),R(M2,{eyebrow:"AGENT RESEARCH MEMORY",title:"Search history",description:"Every research.search call is saved here automatically.",tone:"info"}),R(_,{class:"relative grow px-6 pt-4 flex-col",get children(){return[R(P2,{top:"absolute left-[628] top-[16] w-[68] h-[132] items-center justify-center rounded-xl bg-orange-100",bottom:"absolute left-[628] top-[742] w-[68] h-[132] items-center justify-center rounded-xl bg-orange-100"}),R(_,{class:"w-[584] h-[890] flex-col gap-[12]",get children(){return R(s4,{get each(){return TJ().slice(c0(),c0()+l0)},children:(J)=>R(_,{class:"h-[126] px-5 flex-row items-center justify-between rounded-xl shadow bg-white border-slate-100",get children(){return[R(_,{class:"w-[390] flex-col gap-2",get children(){return[R(y,{class:"text-lg text-slate-900 font-bold",get children(){return J.query.slice(0,48)}}),R(y,{class:"text-base text-slate-500",get children(){return(J.top_title||J.error||"No result title").slice(0,58)}}),R(y,{class:"text-base text-indigo-600 font-bold",get children(){return w2(J.searched_at)}})]}}),R(_,{get["class"](){return s(()=>J.status==="ok")()?q0.success.surface:q0.danger.surface},get children(){return R(y,{get["class"](){return s(()=>J.status==="ok")()?q0.success.text:q0.danger.text},get children(){return s(()=>J.status==="ok")()?J.result_count+" RESULTS":"FAILED"}})}})]}})})}}),R(a4,{get when(){return TJ().length===0},get children(){return R(_,{class:"absolute left-[24] top-[16] w-[672] h-[890] bg-slate-50",get children(){return R(U2,{icon:"E",title:"No searches yet",detail:`Ask Pi Agent to research a topic. +The search and its results will appear here.`,tone:"info"})}})}})]}}),R(_,{class:"h-[96]",get children(){return R(V2,{get text(){return O4()},get tone(){return O4().includes("FAILED")?"danger":"neutral"},dark:!0})}})]}})}A4(0),q2(()=>R(C2,{})),globalThis.PocketPiApp={tick(){return""},dataChanged(J){let Q=D2(J),$=Array.isArray(Q)?Q.reduce((Z,X)=>Math.max(Z,Number(X?.revision??0)),j0):j0;return A4($),""},tap(J,Q){if(Q<112&&J<100)return JSON.stringify({type:"navigate",app:"pi-agent"});if(J>=620&&Q>=294&&Q<=426)I4(-1);else if(J>=620&&Q>=1020&&Q<=1152)I4(1);return""}}})(); diff --git a/apps/exa/dist/app.pak b/apps/exa/dist/app.pak new file mode 100644 index 0000000..ad08f24 Binary files /dev/null and b/apps/exa/dist/app.pak differ diff --git a/apps/exa/dist/data-action.js b/apps/exa/dist/data-action.js new file mode 100644 index 0000000..9858d86 --- /dev/null +++ b/apps/exa/dist/data-action.js @@ -0,0 +1,14 @@ +(()=>{var U=["GET","HEAD","POST","PUT","PATCH","DELETE","OPTIONS"];var Z=65536,z=131072,K=262144,V=32,j=8192,F=30000,M=120000;var i={unavailable:"unavailable",invalidRequest:"invalid_request",busy:"busy",dns:"dns",connect:"connect",tls:"tls",timeout:"timeout",redirect:"redirect",responseTooLarge:"response_too_large",protocol:"protocol",cancelled:"cancelled",other:"other"};var E={};for(let f=0;f<64;f++)E["ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"[f]]=f;function J(f){let n=0;for(let h=0;h65535)h++;n+=a<128?1:a<2048?2:a<65536?3:4}let c=new Uint8Array(n),r=0;for(let h=0;h65535)h++;else if(a>=55296&&a<=57343)a=65533;if(a<128)c[r++]=a;else if(a<2048)c[r++]=192|a>>6,c[r++]=128|a&63;else if(a<65536)c[r++]=224|a>>12,c[r++]=128|a>>6&63,c[r++]=128|a&63;else c[r++]=240|a>>18,c[r++]=128|a>>12&63,c[r++]=128|a>>6&63,c[r++]=128|a&63}return c}function X(f){let n="",c=0;while(cf.length)throw Error("invalid UTF-8");for(let L=0;L1114111||h>=55296&&h<=57343||a===1&&h<128||a===2&&h<2048||a===3&&h<65536)throw Error("invalid UTF-8");if(h<65536)n+=String.fromCharCode(h);else h-=65536,n+=String.fromCharCode(55296+(h>>10),56320+(h&1023))}return n}var S=new Set;function Y(f){return S.add(f),()=>S.delete(f)}class w extends Error{constructor(f,n){super(n);this.name="NetError",this.code=f}}class O{constructor(f,n,c,r){this.status=f,this.url=n,this.headers=Object.freeze({...c}),this.ok=f>=200&&f<300,this.data=new Uint8Array(r)}get byteLength(){return this.data.byteLength}async bytes(){return this.data.slice()}async arrayBuffer(){return this.data.slice().buffer}async text(){try{return X(this.data)}catch{throw Error("net: response is not valid UTF-8")}}async json(){return JSON.parse(await this.text())}}var l=new Map,A=null,u=null;function p(){let f=globalThis.net;if(!f||typeof f!=="object")return null;let n=f;return typeof n.start==="function"&&typeof n.take==="function"&&typeof n.cancel==="function"&&typeof n.poll==="function"&&typeof n.lastError==="function"?n:null}function B(f){let n=String(f);for(let c of Object.values(i))if(c===n)return c;return i.other}function v(f){let n=l.get(f.h);if(!n)return;if(l.delete(f.h),f.t==="error")n.reject(new w(B(f.code),String(f.message||f.code)));else if(!Number.isInteger(f.status)||f.status<100||f.status>599||typeof f.url!=="string"||typeof f.headers!=="object"||f.headers===null||!Number.isInteger(f.bytes)||f.bytes<0||f.bytes>K)n.ops.cancel(f.h),n.reject(new w(i.protocol,"net: malformed done event"));else{let c=new ArrayBuffer(f.bytes);if(n.ops.take(f.h,c)!==f.bytes)n.ops.cancel(f.h),n.reject(new w(i.protocol,"net: response body transfer failed"));else n.resolve(new O(f.status,f.url,f.headers,c))}if(l.size===0&&A)A(),A=null,u=null}function W(){if(l.size===0)return;let f=u;if(!f)return;let n=f.poll();if(n!==void 0){let c=null;try{c=JSON.parse(n)}catch{}if(!Array.isArray(c))for(let[r,h]of l)f.cancel(r),l.delete(r),h.reject(new w(i.protocol,"net: malformed event batch"));else for(let r of c){if(!r||typeof r!=="object")continue;let h=r;if(!Number.isInteger(h.h)||h.t!=="done"&&h.t!=="error")continue;v(h)}}if(l.size===0&&A)A(),A=null,u=null}function Q(f,n){return Promise.reject(new w(f,n))}function t(f,n,c,r){if(!Number.isInteger(f)||fc)throw new w(i.invalidRequest,`net: ${r} must be ${n}..${c}`);return f}function e(f){let n=Object.create(null),c=0,r=0;for(let h of Object.keys(f??{})){let a=h.toLowerCase(),L=String(f[h]);if(!/^[!#$%&'*+.^_`|~0-9a-z-]+$/.test(a)||/[\r\n]/.test(L))throw new w(i.invalidRequest,`net: invalid header ${h}`);if(c++,r+=J(a).byteLength+J(L).byteLength+4,c>V||r>j)throw new w(i.invalidRequest,"net: request headers exceed limits");n[a]=L}return n}function o(f){if(f===void 0)return new Uint8Array(0);if(typeof f==="string")return J(f);if(f instanceof Uint8Array)return f.slice();if(f instanceof ArrayBuffer)return new Uint8Array(f.slice(0));throw new w(i.invalidRequest,"net: body must be string or bytes")}function P(f,n={}){let c=p();if(!c)return Q(i.unavailable,"net: host did not mount the net module");try{if(typeof f!=="string"||!/^https?:\/\/[^\s/]+(?:\/|$)/.test(f))throw new w(i.invalidRequest,"net: url must be absolute http:// or https://");let r=n.method??"GET";if(!U.includes(r))throw new w(i.invalidRequest,`net: unsupported method ${String(r)}`);let h=o(n.body);if((r==="GET"||r==="HEAD")&&h.byteLength>0)throw new w(i.invalidRequest,`net: ${r} cannot have a body`);if(h.byteLength>Z)throw new w(i.invalidRequest,"net: request body exceeds 64 KiB");let a=t(n.timeoutMs??F,1,M,"timeoutMs"),L=t(n.maxBytes??z,1,K,"maxBytes"),x=JSON.stringify({url:f,method:r,headers:e(n.headers),timeoutMs:a,maxBytes:L});if(u&&u!==c)throw new w(i.unavailable,"net: mounted host changed while requests are pending");let k=c.start(x,h.buffer);if(!Number.isInteger(k)||k<0){let q=c.lastError()||"unavailable: request refused",D=q.indexOf(":"),b=B(D<0?i.other:q.slice(0,D)),_=D<0?q:q.slice(D+1).trim();return Q(b,_)}return new Promise((q,D)=>{if(l.set(k,{ops:c,resolve:q,reject:D}),u=c,!A)A=Y(W)})}catch(r){return r instanceof w?Promise.reject(r):Q(i.invalidRequest,String(r))}}var H=globalThis.db,$=H.open("exa");if($<0)throw Error("open exa.sqlite");var s=5,ff=7,nf=ff*24*60*60;function cf(){return String(H.lastError($)||"SQLite operation failed")}function G(f){if(H.exec($,f)!==0)throw Error(cf())}function N(f,n=[]){let c=JSON.parse(H.query($,f,JSON.stringify(n)));if(c.error)throw Error(String(c.error));return c}function g(f,n=[]){return N(f,n)}var rf=Number(N("PRAGMA user_version")?.rows?.[0]?.[0]??0);if(rf!==s)G(` + DROP TABLE IF EXISTS searches; + CREATE TABLE searches ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + query TEXT NOT NULL, + searched_at INTEGER NOT NULL, + status TEXT NOT NULL, + result_count INTEGER NOT NULL DEFAULT 0, + top_title TEXT, + error TEXT + ); + CREATE INDEX searches_retention ON searches(searched_at); + PRAGMA user_version=${s}; + `);function hf(){return Math.floor(Date.now()/1000)}function y(f){let n=f-nf;g("DELETE FROM searches WHERE searched_at < ?",[n])}async function d(f,n){let c=await P(`https://api.exa.ai${f}`,{method:"POST",headers:{accept:"application/json","content-type":"application/json"},body:JSON.stringify(n),timeoutMs:globalThis.app.remainingMs(),maxBytes:98304}),r=await c.json();if(!c.ok)throw Error(`Exa HTTP ${c.status}: ${JSON.stringify(r)}`);return r}function T(f){G("BEGIN IMMEDIATE");try{f(),G("COMMIT")}catch(n){try{G("ROLLBACK")}catch{}throw n}globalThis.app.commit()}async function af(f){let n=String(f.query??"").trim();if(!n)throw Error("query is required");let c=hf();try{let r={query:n,type:f.searchType??"auto",numResults:Math.max(1,Math.min(10,Number(f.numResults??10))),contents:{highlights:{maxCharacters:800}}};for(let x of["includeDomains","excludeDomains","startPublishedDate","endPublishedDate","category","userLocation","additionalQueries","moderation"])if(f[x]!==void 0)r[x]=f[x];if(f.maxAgeHours!==void 0)r.contents.maxAgeHours=f.maxAgeHours;let h=await d("/search",r),a=Array.isArray(h?.results)?h.results:[],L=typeof a[0]?.title==="string"?a[0].title:null;return T(()=>{g("INSERT INTO searches(query,searched_at,status,result_count,top_title,error) VALUES(?,?,?,?,?,NULL)",[n,c,"ok",a.length,L]),y(c)}),h}catch(r){let h=r instanceof Error?r.message:String(r);throw T(()=>{g("INSERT INTO searches(query,searched_at,status,result_count,top_title,error) VALUES(?,?,?,0,NULL,?)",[n,c,"error",h]),y(c)}),r}}async function wf(f){let n=String(f.url??"").trim();if(!n)throw Error("url is required");let c={urls:[n],text:{maxCharacters:Math.max(200,Math.min(12000,Number(f.maxCharacters??6000))),includeHtmlTags:!1}};if(f.maxAgeHours!==void 0)c.maxAgeHours=f.maxAgeHours;return d("/contents",c)}var C,I=!1;function R(f){return JSON.stringify({text:f instanceof Error?f.message:String(f),isError:!0})}function m(f){if(I)throw Error("Exa Data Action is already running");I=!0,C=void 0,f().then((n)=>{C=JSON.stringify({text:JSON.stringify(n),isError:!1}),I=!1},(n)=>{C=R(n),I=!1})}globalThis.PocketPiData={beginInvokeTask(f){m(async()=>{throw Error("Unknown Exa Data Action: "+f)})},beginInvokeTool(f,n){try{let c=JSON.parse(n);m(()=>f==="research.search"?af(c):f==="research.fetch"?wf(c):Promise.reject(Error("Unknown Exa tool: "+f)))}catch(c){C=R(c),I=!1}},tick(){W()},pollResult(){let f=C;return C=void 0,f}};})(); diff --git a/apps/exa/pocket.json b/apps/exa/pocket.json new file mode 100644 index 0000000..e1bb43a --- /dev/null +++ b/apps/exa/pocket.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://pocketjs.dev/schema/pocket-2.json", + "pocket": 2, + "id": "dev.pocket-stack.exa", + "name": "exa", + "title": "Exa Research", + "version": "1.1.0", + "engine": {"capabilities":{"requires":["data.fs","data.sqlite","net.http"]}}, + "app": {"entry":"app.tsx","output":"app","framework":"solid","viewport":{"logical":[720,1280],"presentation":"fit"}} +} diff --git a/apps/pi-agent/agent-app.json b/apps/pi-agent/agent-app.json new file mode 100644 index 0000000..67c33e3 --- /dev/null +++ b/apps/pi-agent/agent-app.json @@ -0,0 +1,8 @@ +{ + "id": "pi-agent", + "description": "System Agent and workspace", + "version": "1.0.0", + "tools": [], + "tasks": [], + "schedules": [] +} diff --git a/apps/pi-agent/app.tsx b/apps/pi-agent/app.tsx new file mode 100644 index 0000000..e517e5c --- /dev/null +++ b/apps/pi-agent/app.tsx @@ -0,0 +1,644 @@ +import { batch, createSignal, For, Show } from "solid-js"; +import { Text, View } from "@pocketjs/framework/components"; +import { mount } from "@pocketjs/framework"; +import { readFileSync, readdirSync, type DirEntry } from "@pocketjs/framework/fs"; +import { ActionButton, PocketHeader, ScrollButtons } from "../_shared/ui"; +import { wrapLines, wrapPreview, wrapTextPage, type WrappedTextPage } from "../_shared/text"; + +const FONT_BODY = 3; +const FONT_CHAT = 4; +const FONT_READER = 4; +const FONT_FILE = 2; +const CHAT_TEXT_WIDTH = 536; +const READER_TEXT_WIDTH = 544; +const READER_PAGE_LINES = 36; +const FILE_PAGE_LINES = 39; + +type Message = { role: string; text: string }; +type Network = { ssid: string; rssiDbm: number; secured: boolean }; +type InstalledApp = { id: string; title: string; description: string; scheduleEveryMinutes?: number | null }; +type Projection = { + agent?: string; + model?: string; + messages?: Message[]; + schedule?: { name?: string | null; prompt?: string; next?: string; everyMinutes?: number | null }; + apps?: InstalledApp[]; + settings?: { + wifi?: { + connectedSsid?: string | null; + ipAddress?: string | null; + rssiDbm?: number | null; + scanning?: boolean; + networks?: Network[]; + status?: string; + }; + firmwareVersion?: string; + workspaceFree?: string; + }; +}; + +type Tab = "chat" | "files" | "apps" | "settings"; +type Screen = Tab | "keyboard" | "viewer" | "reader"; +type KeyboardPurpose = { type: "prompt" } | { type: "wifi"; ssid: string }; +type FileEntry = { name: string; kind: "file" | "dir"; size: number }; +type ViewerPageStart = { offset: number; sourceLine: number }; +type Viewer = { + path: string; + text: string; + pageIndex: number; + pageStarts: ViewerPageStart[]; + page: WrappedTextPage; +}; +type Reader = { author: "YOU" | "PI"; lines: string[] }; + +const [projection, setProjection] = createSignal({ + agent: "STARTING", + model: "CODEX / UART / MAC", + messages: [{ role: "assistant", text: "BOOTING PI AGENT..." }], +}); +const [screen, setScreen] = createSignal("chat"); +const [activeTab, setActiveTab] = createSignal("chat"); +const [chatScroll, setChatScroll] = createSignal(0); +const [filePath, setFilePath] = createSignal(""); +const [fileOffset, setFileOffset] = createSignal(0); +const [files, setFiles] = createSignal([]); +const [fileError, setFileError] = createSignal(""); +const [viewer, setViewer] = createSignal(null); +const [reader, setReader] = createSignal(null); +const [readerOffset, setReaderOffset] = createSignal(0); +const [input, setInput] = createSignal(""); +const [keyboardMode, setKeyboardMode] = createSignal<"letters" | "numbers">("letters"); +const [uppercase, setUppercase] = createSignal(false); +const [keyboardPurpose, setKeyboardPurpose] = createSignal({ type: "prompt" }); +const [pressedKey, setPressedKey] = createSignal(null); +const [wifiOffset, setWifiOffset] = createSignal(0); +const fileCache = new Map(); + +const letterRows = ["qwertyuiop", "asdfghjkl", "zxcvbnm"]; +const numberRows = ["1234567890", "-/:;()$&@", ".,?!'\"+"]; +const tabNames: Tab[] = ["chat", "files", "apps", "settings"]; + +function formatSize(size: number): string { + if (size < 1024) return size + " B"; + if (size < 1024 * 1024) return (size / 1024).toFixed(1) + " KB"; + return (size / (1024 * 1024)).toFixed(1) + " MB"; +} + +function joinPath(parent: string, name: string): string { + return parent ? parent + "/" + name : name; +} + +function filePage(text: string, start: ViewerPageStart): WrappedTextPage { + return wrapTextPage(text, FONT_FILE, READER_TEXT_WIDTH, start.offset, start.sourceLine, FILE_PAGE_LINES); +} + +function openViewer(path: string, text: string) { + const start = { offset: 0, sourceLine: 0 }; + setViewer({ path, text, pageIndex: 0, pageStarts: [start], page: filePage(text, start) }); + setScreen("viewer"); +} + +function moveViewerPage(direction: -1 | 1) { + const current = viewer(); + if (!current) return; + const nextIndex = current.pageIndex + direction; + if (nextIndex < 0 || (direction > 0 && !current.page.hasMore)) return; + let start = current.pageStarts[nextIndex]; + if (!start) { + start = { + offset: current.page.nextOffset, + sourceLine: current.page.nextSourceLine, + }; + current.pageStarts.push(start); + } + setViewer({ ...current, pageIndex: nextIndex, page: filePage(current.text, start) }); +} + +function refreshFiles(path = filePath(), force = false) { + const cached = fileCache.get(path); + if (!force && cached) { + setFiles(cached); + setFileError(""); + setFileOffset((value) => Math.min(value, Math.max(0, cached.length - 8))); + return; + } + try { + const next = (readdirSync(path, { withFileTypes: true }) as DirEntry[]).map((entry) => ({ + name: entry.name, + kind: entry.isDirectory() ? "dir" as const : "file" as const, + size: entry.size, + })); + fileCache.set(path, next); + setFiles(next); + setFileError(""); + setFileOffset((value) => Math.min(value, Math.max(0, next.length - 8))); + } catch (error) { + setFiles([]); + setFileError(error instanceof Error ? error.message : String(error)); + } +} + +function chatTurns(): Array<{ user: string; assistant: string }> { + const out: Array<{ user: string; assistant: string }> = []; + for (const message of projection().messages ?? []) { + if (message.role === "user") { + out.push({ user: message.text, assistant: "THINKING..." }); + } else if (out.length === 0) { + out.push({ user: "TYPE A MESSAGE", assistant: message.text || "THINKING..." }); + } else { + out[out.length - 1].assistant = message.text || "THINKING..."; + } + } + return out.length ? out : [{ user: "TYPE A MESSAGE", assistant: "BOOTING PI AGENT..." }]; +} + +function visibleTurns() { + const all = chatTurns(); + const max = Math.max(0, all.length - 2); + const scroll = Math.min(chatScroll(), max); + const end = all.length - scroll; + return all.slice(Math.max(0, end - 2), end); +} + +function Header(props: { title: string }) { + return ( + + ); +} + +function BottomBar() { + return ( + + {(name) => ( + + {name.toUpperCase()} + + )} + + ); +} + +function ChatScreen() { + const schedule = () => projection().schedule; + const schedulePreview = () => { + const current = schedule(); + return wrapPreview( + String(current?.name) + " " + String(current?.next ?? "") + "\n\n" + String(current?.prompt ?? ""), + FONT_BODY, + CHAT_TEXT_WIDTH, + 5, + ); + }; + return ( + +
+ + {(turn) => ( + + + + YOU + + {wrapPreview(turn.user, FONT_CHAT, CHAT_TEXT_WIDTH, 3)} + + + + PI + + {wrapPreview(turn.assistant, FONT_CHAT, CHAT_TEXT_WIDTH, 3)} + + )} + + + + NEXT WAKE + {"NO WAKE SCHEDULED\n\nASK PI TO CREATE ONE WITH SCHEDULE.SET"} + }> + + {schedulePreview()} + + + + + + + + + ); +} + +function FilesScreen() { + const visible = () => files().slice(fileOffset(), fileOffset() + 8); + return ( + +
+ + + {"/workspace" + (filePath() ? "/" + filePath() : "")} + + + {(entry) => ( + + + {entry.kind === "dir" ? "D" : "F"} + + + {(entry.name + (entry.kind === "dir" ? "/" : "")).slice(0, 52)} + {entry.kind === "dir" ? "FOLDER" : formatSize(entry.size)} + + + )} + + + {fileError() || "THIS DIRECTORY IS EMPTY"} + + 8}> + + + + ); +} + +function AppsScreen() { + const apps = () => projection().apps ?? []; + return ( + +
+ + {String(apps().length) + " INSTALLED APPS"} + 0} fallback={ + NO OPTIONAL APPS INSTALLED + }> + {(app) => ( + + + {app.title.slice(0, 1).toUpperCase()} + {app.title}{app.description}{"UPDATES EVERY " + String(app.scheduleEveryMinutes) + " MINUTES"} + + + + )} + + + {"APP DATA STAYS ISOLATED.\nPI AGENT CAN USE EACH APP'S TOOLS."} + + + + + ); +} + +function SettingsScreen() { + const settings = () => projection().settings ?? {}; + const wifi = () => settings().wifi ?? {}; + const networks = () => (wifi().networks ?? []).slice(wifiOffset(), wifiOffset() + 5); + const detail = () => wifi().ipAddress + ? "IP " + wifi().ipAddress + " RSSI " + String(wifi().rssiDbm ?? "--") + " DBM" + : wifi().status || "SCAN AND SELECT A NETWORK"; + return ( + +
+ + + WI-FI + {wifi().connectedSsid || "NOT CONNECTED"} + {detail()} + + {wifi().scanning ? "SCANNING" : "SCAN"} + + + AVAILABLE NETWORKS + + 0} fallback={ + {"NO NETWORK LIST YET\n\nTAP SCAN TO FIND WI-FI"} + }> + {(network) => ( + + {network.ssid} + {String(network.rssiDbm) + " DBM " + (network.secured ? "LOCK" : "OPEN")} + + )} + + 5}> + + + MODEL BACKEND + {projection().model ?? "UNKNOWN"} + {"FIRMWARE " + String(settings().firmwareVersion ?? "0.1.0") + " · WORKSPACE FREE " + String(settings().workspaceFree ?? "--")} + + + FORGET WI-FI + RESTART DEVICE + + + + + ); +} + +function KeyboardScreen() { + const purpose = () => keyboardPurpose(); + const rows = () => keyboardMode() === "letters" ? letterRows : numberRows; + const display = () => purpose().type === "wifi" ? "*".repeat(input().length) : input(); + return ( + +
+ + + {display() || (purpose().type === "wifi" ? "ENTER NETWORK PASSWORD..." : "TYPE YOUR MESSAGE...")} + + {String(input().length) + " / " + (purpose().type === "wifi" ? "63" : "256") + " CHARACTERS"}CLEAR + {(row, rowIndex) => ( + + {(key) => ( + {uppercase() ? key.toUpperCase() : key} + )} + DEL + + )} + + {keyboardMode() === "letters" ? "123" : "ABC"} + SPACE + {keyboardMode() === "letters" ? "SHIFT" : ". ?"} + {purpose().type === "wifi" ? "JOIN" : "SEND"} + + + CLOSE KEYBOARD + + ); +} + +function ViewerScreen() { + const current = () => viewer(); + return ( + +
+ + {current()?.path ?? "NO FILE OPEN"} + + {current()?.page.text ?? ""} + + {current() + ? "PAGE " + String(current()!.pageIndex + 1) + " · SOURCE LINES " + String(current()!.page.startSourceLine + 1) + "-" + String(current()!.page.lastSourceLine + 1) + : "NO FILE OPEN"} + + + + ); +} + +function ReaderScreen() { + const current = () => reader(); + return ( + +
+ + {current()?.author ?? "PI"} + {(current()?.lines ?? []).slice(readerOffset(), readerOffset() + READER_PAGE_LINES).join("\n")} + + + + ); +} + +function Root() { + return ( + + {screen() === "chat" ? + : screen() === "files" ? + : screen() === "apps" ? + : screen() === "settings" ? + : screen() === "keyboard" ? + : screen() === "viewer" ? + : } + + ); +} + +function openTab(tab: Tab) { + batch(() => { + setActiveTab(tab); + setScreen(tab); + if (tab === "files") refreshFiles(); + }); +} + +function keyboardCharacterAt(x: number, y: number): string | null { + const rows = keyboardMode() === "letters" ? letterRows : numberRows; + const ys = [488, 628, 768]; + for (let row = 0; row < 3; row++) { + if (y < ys[row] || y >= ys[row] + 120) continue; + const chars = rows[row]; + const available = row === 2 ? 560 : 672; + const width = available / chars.length; + const index = Math.floor((x - 24) / width); + if (index >= 0 && index < chars.length) return chars[index]; + } + return null; +} + +function keyboardButtonAt(x: number, y: number): string | null { + if (y >= 1164) return "close"; + if (x >= 548 && y >= 402 && y <= 482) return "clear"; + const character = keyboardCharacterAt(x, y); + if (character) return "char:" + character; + if (x >= 592 && y >= 768 && y <= 888) return "delete"; + if (y < 908 || y > 1064) return null; + if (x <= 116) return "mode"; + if (x <= 424) return "space"; + if (x <= 576) return "shift"; + return "submit"; +} + +function handleKeyboardTap(x: number, y: number): string { + if (y >= 1164) { + setInput(""); + setScreen(activeTab()); + return ""; + } + if (x >= 548 && y >= 402 && y <= 482) { + setInput(""); + return ""; + } + const key = keyboardCharacterAt(x, y); + if (key) { + const next = keyboardMode() === "letters" && uppercase() ? key.toUpperCase() : key; + const limit = keyboardPurpose().type === "wifi" ? 63 : 256; + if (input().length < limit) setInput(input() + next); + return ""; + } + if (x >= 592 && y >= 768 && y <= 888) { + setInput(input().slice(0, -1)); + return ""; + } + if (y >= 908 && y <= 1064) { + if (x <= 116) { + setKeyboardMode(keyboardMode() === "letters" ? "numbers" : "letters"); + setUppercase(false); + } else if (x <= 424) { + if (input()) setInput(input() + " "); + } else if (x <= 576) { + if (keyboardMode() === "letters") setUppercase(!uppercase()); + else setInput(input() + (x <= 500 ? "." : "?")); + } else if (input().trim()) { + const value = input().trim(); + const purpose = keyboardPurpose(); + setInput(""); + setKeyboardMode("letters"); + setUppercase(false); + setScreen(activeTab()); + if (purpose.type === "wifi") return JSON.stringify({ type: "settings", command: "connect", ssid: purpose.ssid, password: value }); + return JSON.stringify({ type: "submitPrompt", prompt: value }); + } + } + return ""; +} + +mount(() => ); +queueMicrotask(() => refreshFiles("", true)); + +(globalThis as any).PocketPiApp = { + tick() { + return ""; + }, + update(line: string) { + setProjection(JSON.parse(line)); + return ""; + }, + pointerDown(x: number, y: number) { + setPressedKey(screen() === "keyboard" ? keyboardButtonAt(x, y) : null); + return ""; + }, + pointerUp() { + setPressedKey(null); + return ""; + }, + tap(x: number, y: number) { + if (screen() === "keyboard") return handleKeyboardTap(x, y); + if (screen() === "viewer") { + const current = viewer(); + if (x < 104 && y < 112) { + setViewer(null); + setScreen("files"); + } else if (current && x >= 620 && y >= 170 && y <= 340) { + moveViewerPage(-1); + } else if (current && x >= 620 && y >= 920 && y <= 1100) { + moveViewerPage(1); + } + return ""; + } + if (screen() === "reader") { + if (x < 104 && y < 112) { + setReader(null); + setReaderOffset(0); + setScreen("chat"); + } else if (x >= 620 && y >= 170 && y <= 340) { + setReaderOffset(Math.max(0, readerOffset() - 18)); + } else if (x >= 620 && y >= 920 && y <= 1100) { + const lineCount = reader()?.lines.length ?? 0; + setReaderOffset(Math.min(Math.max(0, lineCount - READER_PAGE_LINES), readerOffset() + 18)); + } + return ""; + } + if (y >= 1172) { + openTab(tabNames[Math.min(3, Math.floor(x / 180))]); + return ""; + } + if (screen() === "chat") { + if (x >= 620 && y >= 140 && y <= 272) setChatScroll(chatScroll() + 2); + else if (x >= 620 && y >= 624 && y <= 756) setChatScroll(Math.max(0, chatScroll() - 2)); + else if (x < 610 && y >= 140 && y < 798) { + const row = Math.floor((y - 140) / 340); + const turn = visibleTurns()[row]; + if (turn) { + const isPi = y >= 140 + row * 340 + 160; + const text = isPi ? turn.assistant : turn.user; + setReader({ author: isPi ? "PI" : "YOU", lines: wrapLines(text, FONT_READER, READER_TEXT_WIDTH) }); + setReaderOffset(0); + setScreen("reader"); + } + } else if (y >= 1070 && y <= 1150) { + setKeyboardPurpose({ type: "prompt" }); + setInput(""); + setScreen("keyboard"); + } + return ""; + } + if (screen() === "files") { + if (x < 104 && y < 112 && filePath()) { + const parts = filePath().split("/"); + parts.pop(); + const next = parts.join("/"); + setFilePath(next); + setFileOffset(0); + refreshFiles(next); + } else if (x >= 620 && y >= 170 && y <= 340) { + setFileOffset(Math.max(0, fileOffset() - 4)); + } else if (x >= 620 && y >= 920 && y <= 1100) { + setFileOffset(Math.min(Math.max(0, files().length - 8), fileOffset() + 4)); + } else if (x < 610 && y >= 190) { + const row = Math.floor((y - 190) / 104); + const entry = files()[fileOffset() + row]; + if (entry) { + const path = joinPath(filePath(), entry.name); + if (entry.kind === "dir") { + setFilePath(path); + setFileOffset(0); + refreshFiles(path); + } else { + try { + openViewer("/workspace/" + path, readFileSync(path, "utf8")); + } catch (error) { + setFileError(error instanceof Error ? error.message : String(error)); + } + } + } + } + return ""; + } + if (screen() === "apps") { + const app = (projection().apps ?? [])[Math.floor((y - 162) / 166)]; + if (y >= 162 && app) return JSON.stringify({ type: "navigate", app: app.id }); + return ""; + } + if (screen() === "settings") { + const networks = projection().settings?.wifi?.networks ?? []; + if (x >= 480 && y >= 126 && y <= 218) return JSON.stringify({ type: "settings", command: "scan" }); + if (x >= 620 && y >= 330 && y <= 462) setWifiOffset(Math.max(0, wifiOffset() - 4)); + else if (x >= 620 && y >= 650 && y <= 782) setWifiOffset(Math.min(Math.max(0, networks.length - 5), wifiOffset() + 4)); + else if (x < 610 && y >= 330 && y < 790) { + const row = Math.floor((y - 330) / 92); + const network = networks[wifiOffset() + row]; + if (network) { + if (!network.secured) return JSON.stringify({ type: "settings", command: "connect", ssid: network.ssid, password: "" }); + setKeyboardPurpose({ type: "wifi", ssid: network.ssid }); + setInput(""); + setScreen("keyboard"); + } + } else if (x <= 340 && y >= 1010 && y <= 1090) return JSON.stringify({ type: "settings", command: "forget" }); + else if (x >= 356 && y >= 1010 && y <= 1090) return JSON.stringify({ type: "settings", command: "restart" }); + } + return ""; + }, +}; diff --git a/apps/pi-agent/dist/agent.js b/apps/pi-agent/dist/agent.js new file mode 100644 index 0000000..b22c52f --- /dev/null +++ b/apps/pi-agent/dist/agent.js @@ -0,0 +1,404 @@ +"use strict";(()=>{var __defProp=Object.defineProperty;var __defNormalProp=(obj,key,value)=>key in obj?__defProp(obj,key,{enumerable:true,configurable:true,writable:true,value}):obj[key]=value;var __export=(target,all)=>{for(var name in all)__defProp(target,name,{get:all[name],enumerable:true})};var __publicField=(obj,key,value)=>__defNormalProp(obj,typeof key!=="symbol"?key+"":key,value);var memory_exports={};__export(memory_exports,{Assign:()=>Assign,Clone:()=>Clone,Create:()=>Create,Discard:()=>Discard,Metrics:()=>Metrics,Update:()=>Update});var Metrics={assign:0,create:0,clone:0,discard:0,update:0};function Assign(left,right){Metrics.assign+=1;return{...left,...right}}var emit_exports={};__export(emit_exports,{And:()=>And,ArrayLiteral:()=>ArrayLiteral,ArrowFunction:()=>ArrowFunction,Call:()=>Call,ConstDeclaration:()=>ConstDeclaration,Constant:()=>Constant,Entries:()=>Entries2,Every:()=>Every2,HasPropertyKey:()=>HasPropertyKey2,If:()=>If,IsArray:()=>IsArray2,IsAsyncIterator:()=>IsAsyncIterator2,IsBigInt:()=>IsBigInt2,IsBoolean:()=>IsBoolean2,IsConstructor:()=>IsConstructor2,IsDeepEqual:()=>IsDeepEqual2,IsEqual:()=>IsEqual2,IsFunction:()=>IsFunction2,IsGreaterEqualThan:()=>IsGreaterEqualThan2, +IsGreaterThan:()=>IsGreaterThan2,IsInteger:()=>IsInteger2,IsIterator:()=>IsIterator2,IsLessEqualThan:()=>IsLessEqualThan2,IsLessThan:()=>IsLessThan2,IsMaxLength:()=>IsMaxLength3,IsMinLength:()=>IsMinLength3,IsNull:()=>IsNull2,IsNumber:()=>IsNumber2,IsObject:()=>IsObject2,IsObjectNotArray:()=>IsObjectNotArray2,IsString:()=>IsString2,IsSymbol:()=>IsSymbol2,IsUndefined:()=>IsUndefined2,Keys:()=>Keys2,Member:()=>Member,MultipleOf:()=>MultipleOf,New:()=>New,Not:()=>Not,Or:()=>Or,PrefixIncrement:()=>PrefixIncrement, +ReduceAnd:()=>ReduceAnd,ReduceOr:()=>ReduceOr,Return:()=>Return,Statements:()=>Statements,Ternary:()=>Ternary});var guard_exports={};__export(guard_exports,{Entries:()=>Entries,EntriesRegExp:()=>EntriesRegExp,Every:()=>Every,EveryAll:()=>EveryAll,GraphemeCount:()=>GraphemeCount2,HasPropertyKey:()=>HasPropertyKey,IsArray:()=>IsArray,IsAsyncIterator:()=>IsAsyncIterator,IsBigInt:()=>IsBigInt,IsBoolean:()=>IsBoolean,IsClassInstance:()=>IsClassInstance,IsConstructor:()=>IsConstructor,IsDeepEqual:()=>IsDeepEqual,IsEqual:()=>IsEqual,IsFunction:()=>IsFunction,IsGreaterEqualThan:()=>IsGreaterEqualThan,IsGreaterThan:()=>IsGreaterThan, +IsInteger:()=>IsInteger,IsIterator:()=>IsIterator,IsLessEqualThan:()=>IsLessEqualThan,IsLessThan:()=>IsLessThan,IsMaxLength:()=>IsMaxLength2,IsMinLength:()=>IsMinLength2,IsMultipleOf:()=>IsMultipleOf,IsNull:()=>IsNull,IsNumber:()=>IsNumber,IsObject:()=>IsObject,IsObjectNotArray:()=>IsObjectNotArray,IsString:()=>IsString,IsSymbol:()=>IsSymbol,IsUndefined:()=>IsUndefined,IsUnsafePropertyKey:()=>IsUnsafePropertyKey,IsValueLike:()=>IsValueLike,Keys:()=>Keys,Symbols:()=>Symbols,TakeLeft:()=>TakeLeft, +Values:()=>Values});function IsBetween(value,min,max){return value>=min&&value<=max}function IsRegionalIndicator(value){return IsBetween(value,127462,127487)}function IsVariationSelector(value){return IsBetween(value,65024,65039)}function IsCombiningMark(value){return IsBetween(value,768,879)||IsBetween(value,6832,6911)||IsBetween(value,7616,7679)||IsBetween(value,65056,65071)}function CodePointLength(value){return value>65535?2:1}function ConsumeModifiers(value,index2){while(index2=minLength)return true}return false}function IsMaxLength(value,maxLength){let count=0;let index2=0;while(index2maxLength)return false}return true}function IsMinLengthFast(value,minLength){if(minLength===0)return true;let index2=0;while(index2=minLength)return true}return false}function IsMaxLengthFast(value,maxLength){let index2=0;while(index2maxLength)return false}return true}function IsArray(value){return Array.isArray(value)}function IsAsyncIterator(value){return IsObject(value)&&Symbol.asyncIterator in value}function IsBigInt(value){return IsEqual(typeof value,"bigint")}function IsBoolean(value){return IsEqual(typeof value,"boolean")}function IsConstructor(value){if(IsUndefined(value)||!IsFunction(value))return false;const result=Function.prototype.toString.call(value);if(/^class\s/.test(result))return true;if(/\[native code\]/.test(result))return true;return false} +function IsFunction(value){return IsEqual(typeof value,"function")}function IsInteger(value){return Number.isInteger(value)}function IsIterator(value){return IsObject(value)&&Symbol.iterator in value}function IsNull(value){return IsEqual(value,null)}function IsNumber(value){return Number.isFinite(value)}function IsObjectNotArray(value){return IsObject(value)&&!IsArray(value)}function IsObject(value){return IsEqual(typeof value,"object")&&!IsNull(value)}function IsString(value){return IsEqual(typeof value, +"string")}function IsSymbol(value){return IsEqual(typeof value,"symbol")}function IsUndefined(value){return IsEqual(value,void 0)}function IsEqual(left,right){return left===right}function IsGreaterThan(left,right){return left>right}function IsLessThan(left,right){return left=right}function IsMultipleOf(dividend,divisor){if(IsBigInt(dividend)||IsBigInt(divisor)){return BigInt(dividend)% +BigInt(divisor)===0n}const tolerance=1e-10;if(!IsNumber(dividend))return true;if(IsInteger(dividend)&&1/divisor%1===0)return true;const mod=dividend%divisor;return Math.min(Math.abs(mod),Math.abs(mod-divisor))[new RegExp(`^${key}$`),value[key]])}function Entries(value){return Object.entries(value)}function Keys(value){return Object.getOwnPropertyNames(value)}function Symbols(value){return Object.getOwnPropertySymbols(value)}function Values(value){return Object.values(value)}function DeepEqualObject(left,right){if(!IsObject(right))return false;const keys=Keys(left);return IsEqual(keys.length,Keys(right).length)&&keys.every( +key=>IsDeepEqual(left[key],right[key]))}function DeepEqualArray(left,right){return IsArray(right)&&IsEqual(left.length,right.length)&&left.every((_,index2)=>IsDeepEqual(left[index2],right[index2]))}function IsDeepEqual(left,right){return IsArray(left)?DeepEqualArray(left,right):IsObject(left)?DeepEqualObject(left,right):IsEqual(left,right)}var identifierRegExp=/^[\p{ID_Start}_$][\p{ID_Continue}_$\u200C\u200D]*$/u;function IsIdentifier(value){return identifierRegExp.test(value)}function And(left,right){return`(${left} && ${right})`}function Or(left,right){return`(${left} || ${right})`}function Not(expr){return`!(${expr})`}function IsArray2(value){return`Array.isArray(${value})`}function IsAsyncIterator2(value){return`Guard.IsAsyncIterator(${value})`}function IsBigInt2(value){return`typeof ${value} === "bigint"`}function IsBoolean2(value){ +return`typeof ${value} === "boolean"`}function IsInteger2(value){return`Number.isInteger(${value})`}function IsIterator2(value){return`Guard.IsIterator(${value})`}function IsNull2(value){return`${value} === null`}function IsNumber2(value){return`Number.isFinite(${value})`}function IsObjectNotArray2(value){return And(IsObject2(value),Not(IsArray2(value)))}function IsObject2(value){return`typeof ${value} === "object" && ${value} !== null`}function IsString2(value){return`typeof ${value} === "strin\ +g"`}function IsSymbol2(value){return`typeof ${value} === "symbol"`}function IsUndefined2(value){return`${value} === undefined`}function IsFunction2(value){return`typeof ${value} === "function"`}function IsConstructor2(value){return`Guard.IsConstructor(${value})`}function IsEqual2(left,right){return`${left} === ${right}`}function IsGreaterThan2(left,right){return`${left} > ${right}`}function IsLessThan2(left,right){return`${left} < ${right}`}function IsLessEqualThan2(left,right){return`${left} <=\ + ${right}`}function IsGreaterEqualThan2(left,right){return`${left} >= ${right}`}function IsMinLength3(value,length){return`Guard.IsMinLength(${value}, ${length})`}function IsMaxLength3(value,length){return`Guard.IsMaxLength(${value}, ${length})`}function Every2(value,offset,params,expression){return IsEqual(offset,"0")?`${value}.every((${params[0]}, ${params[1]}) => ${expression})`:`((value, callback) => { for(let index = ${offset}; index < value.length; index++) if (!callback(value[index], inde\ +x)) return false; return true })(${value}, (${params[0]}, ${params[1]}) => ${expression})`}function Entries2(value){return`Object.entries(${value})`}function Keys2(value){return`Object.getOwnPropertyNames(${value})`}function HasPropertyKey2(value,key){const isProtoField=IsEqual(key,'"__proto__"')||IsEqual(key,'"constructor"');return isProtoField?`Object.prototype.hasOwnProperty.call(${value}, ${key})`:`${key} in ${value}`}function IsDeepEqual2(left,right){return`Guard.IsDeepEqual(${left}, ${right}\ +)`}function ArrayLiteral(elements){return`[${elements.join(", ")}]`}function ArrowFunction(parameters,body){return`((${parameters.join(", ")}) => ${body})`}function Call(value,arguments_){return`${value}(${arguments_.join(", ")})`}function New(value,arguments_){return`new ${value}(${arguments_.join(", ")})`}function Member(left,right){return`${left}${IsIdentifier(right)?`.${right}`:`[${Constant(right)}]`}`}function Constant(value){return IsString(value)?JSON.stringify(value):`${value}`}function Ternary(condition,true_,false_){ +return`(${condition} ? ${true_} : ${false_})`}function Statements(statements){return`{ ${statements.join("; ")}; }`}function ConstDeclaration(identifier,expression){return`const ${identifier} = ${expression}`}function If(condition,then){return`if(${condition}) { ${then} }`}function Return(expression){return`return ${expression}`}function ReduceAnd(operands){return IsEqual(operands.length,0)?"true":operands.reduce((left,right)=>And(left,right))}function ReduceOr(operands){return IsEqual(operands. +length,0)?"false":operands.reduce((left,right)=>Or(left,right))}function PrefixIncrement(expression){return`++${expression}`}function MultipleOf(dividend,divisor){return`Guard.IsMultipleOf(${dividend}, ${divisor})`}var globals_exports={};__export(globals_exports,{IsBigInt64Array:()=>IsBigInt64Array,IsBigUint64Array:()=>IsBigUint64Array,IsBoolean:()=>IsBoolean3,IsDate:()=>IsDate,IsFloat32Array:()=>IsFloat32Array,IsFloat64Array:()=>IsFloat64Array,IsInt16Array:()=>IsInt16Array,IsInt32Array:()=>IsInt32Array,IsInt8Array:()=>IsInt8Array,IsMap:()=>IsMap,IsNumber:()=>IsNumber3,IsRegExp:()=>IsRegExp,IsSet:()=>IsSet,IsString:()=>IsString3,IsTypeArray:()=>IsTypeArray,IsUint16Array:()=>IsUint16Array,IsUint32Array:()=>IsUint32Array, +IsUint8Array:()=>IsUint8Array,IsUint8ClampedArray:()=>IsUint8ClampedArray});function IsBoolean3(value){return value instanceof Boolean}function IsNumber3(value){return value instanceof Number}function IsString3(value){return value instanceof String}function IsTypeArray(value){return globalThis.ArrayBuffer.isView(value)}function IsInt8Array(value){return value instanceof globalThis.Int8Array}function IsUint8Array(value){return value instanceof globalThis.Uint8Array}function IsUint8ClampedArray(value){ +return value instanceof globalThis.Uint8ClampedArray}function IsInt16Array(value){return value instanceof globalThis.Int16Array}function IsUint16Array(value){return value instanceof globalThis.Uint16Array}function IsInt32Array(value){return value instanceof globalThis.Int32Array}function IsUint32Array(value){return value instanceof globalThis.Uint32Array}function IsFloat32Array(value){return value instanceof globalThis.Float32Array}function IsFloat64Array(value){return value instanceof globalThis. +Float64Array}function IsBigInt64Array(value){return value instanceof globalThis.BigInt64Array}function IsBigUint64Array(value){return value instanceof globalThis.BigUint64Array}function IsRegExp(value){return value instanceof globalThis.RegExp}function IsDate(value){return value instanceof globalThis.Date}function IsSet(value){return value instanceof globalThis.Set}function IsMap(value){return value instanceof globalThis.Map}function IsGuard(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"~guard")}function FromGuard(value){return value}function FromArray(value){return value.map(value2=>FromValue(value2))}function FromObject(value){const result={};const descriptors=Object.getOwnPropertyDescriptors(value);for(const key of Object.keys(descriptors)){const descriptor=descriptors[key];if(guard_exports.HasPropertyKey(descriptor,"value")){Object.defineProperty(result,key,{...descriptor,value:FromValue( +descriptor.value)})}}return result}function FromRegExp(value){return new RegExp(value.source,value.flags)}function FromUnknown(value){return value}function FromValue(value){return value instanceof RegExp?FromRegExp(value):IsGuard(value)?FromGuard(value):guard_exports.IsArray(value)?FromArray(value):guard_exports.IsObject(value)?FromObject(value):FromUnknown(value)}function Clone(value){Metrics.clone+=1;return FromValue(value)}var settings_exports={};__export(settings_exports,{Get:()=>Get,Reset:()=>Reset,Set:()=>Set2});var settings={immutableTypes:false,maxErrors:8,useAcceleration:true,exactOptionalPropertyTypes:false,enumerableKind:false,correctiveParse:false};function Reset(){settings.immutableTypes=false;settings.maxErrors=8;settings.useAcceleration=true;settings.exactOptionalPropertyTypes=false;settings.enumerableKind=false;settings.correctiveParse=false}function Set2(options){for(const key of guard_exports.Keys(options)){const value=options[key];if(value!==void 0){Object.defineProperty(settings,key,{value})}}} +function Get(){return settings}function MergeHidden(left,right){for(const key of Object.keys(right)){Object.defineProperty(left,key,{configurable:true,writable:true,enumerable:false,value:right[key]})}return left}function Merge(left,right){return{...left,...right}}function Create(hidden,enumerable,options={}){Metrics.create+=1;const settings2=settings_exports.Get();const withOptions=Merge(enumerable,options);const withHidden=settings2.enumerableKind?Merge(withOptions,hidden):MergeHidden(withOptions,hidden);return settings2.immutableTypes? +Object.freeze(withHidden):withHidden}function Discard(value,propertyKeys){Metrics.discard+=1;const result={};const descriptors=Object.getOwnPropertyDescriptors(Clone(value));const keysToDiscard=new Set(propertyKeys);for(const key of Object.keys(descriptors)){if(keysToDiscard.has(key))continue;Object.defineProperty(result,key,descriptors[key])}return result}function Update(current,hidden,enumerable){Metrics.update+=1;const settings2=settings_exports.Get();const result=Clone(current);for(const key of Object.keys(hidden)){Object.defineProperty(result,key,{configurable:true,writable:true,enumerable:settings2.enumerableKind,value:hidden[key]})}for(const key of Object.keys(enumerable)){Object.defineProperty(result,key,{configurable:true,enumerable:true,writable:true,value:enumerable[key]})}return result}function IsKind(value,kind){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"~kind")&&guard_exports.IsEqual(value["~kind"],kind)}function IsSchema(value){return guard_exports.IsObject(value)}function IsOptionalAddAction(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"~kind")&&guard_exports.HasPropertyKey(value,"type")&&guard_exports.IsEqual(value["~kind"],"OptionalAddAction")&&IsSchema(value.type)}function IsOptionalRemoveAction(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"~kind")&&guard_exports.HasPropertyKey(value,"type")&&guard_exports.IsEqual(value["~kind"],"OptionalRemoveAction")&&IsSchema(value.type)}function IsReadonlyAddAction(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"~kind")&&guard_exports.HasPropertyKey(value,"type")&&guard_exports.IsEqual(value["~kind"],"ReadonlyAddAction")&&IsSchema(value.type)}function IsReadonlyRemoveAction(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"~kind")&&guard_exports.HasPropertyKey(value,"type")&&guard_exports.IsEqual(value["~kind"],"ReadonlyRemoveAction")&&IsSchema(value.type)}function Deferred(action,parameters,options){return memory_exports.Create({"~kind":"Deferred"},{action,parameters,options},{})}function IsDeferred(value){return IsKind(value,"Deferred")}function _Promise_(item,options){return memory_exports.Create({["~kind"]:"Promise"},{type:"promise",item},options)}function IsPromise(value){return IsKind(value,"Promise")}function PromiseOptions(type){return memory_exports.Discard(type,["~kind","type","item"])}function ImmutableAdd(type){return memory_exports.Update(type,{"~immutable":true},{})}function Immutable(type){return ImmutableAdd(type)}function IsImmutable(value){return IsSchema(value)&&guard_exports.HasPropertyKey(value,"~immutable")}function OptionalRemove(type){const result=memory_exports.Discard(type,["~optional"]);return result}function OptionalAdd(type){return memory_exports.Update(type,{"~optional":true},{})}function Optional(type){return OptionalAdd(type)}function IsOptional(value){return IsSchema(value)&&guard_exports.HasPropertyKey(value,"~optional")}function ReadonlyRemove(type){return memory_exports.Discard(type,["~readonly"])}function ReadonlyAdd(type){return memory_exports.Update(type,{"~readonly":true},{})}function Readonly(type){return ReadonlyAdd(type)}function IsReadonly(value){return IsSchema(value)&&guard_exports.HasPropertyKey(value,"~readonly")}function BaseProperty(value){return{enumerable:settings_exports.Get().enumerableKind,writable:false,configurable:false,value}}var Base=class{constructor(){globalThis.Object.defineProperty(this,"~kind",BaseProperty("Base"));globalThis.Object.defineProperty(this,"~guard",BaseProperty({check:value=>this.Check(value),errors:value=>this.Errors(value)}))}Check(_value){return true}Errors(_value){return[]}Convert(value){return value}Clean(value){return value}Default(value){return value}Create(){throw new Error( +"Create not implemented")}Clone(){throw Error("Clone not implemented")}};function IsBase(value){return IsKind(value,"Base")}function _Array_(items,options){return memory_exports.Create({"~kind":"Array"},{type:"array",items},options)}function IsArray3(value){return IsKind(value,"Array")}function ArrayOptions(type){return memory_exports.Discard(type,["~kind","type","items"])}function AsyncIterator(iteratorItems,options){return memory_exports.Create({"~kind":"AsyncIterator"},{type:"asyncIterator",iteratorItems},options)}function IsAsyncIterator3(value){return IsKind(value,"AsyncIterator")}function AsyncIteratorOptions(type){return memory_exports.Discard(type,["~kind","type","iteratorItems"])}function Constructor(parameters,instanceType,options={}){return memory_exports.Create({"~kind":"Constructor"},{type:"constructor",parameters,instanceType},options)}function IsConstructor3(value){return IsKind(value,"Constructor")}function ConstructorOptions(type){return memory_exports.Discard(type,["~kind","type","parameters","instanceType"])}function _Function_(parameters,returnType,options={}){return memory_exports.Create({["~kind"]:"Function"},{type:"function",parameters,returnType},options)}function IsFunction3(value){return IsKind(value,"Function")}function FunctionOptions(type){return memory_exports.Discard(type,["~kind","type","parameters","returnType"])}function Ref(ref,options){return memory_exports.Create({["~kind"]:"Ref"},{$ref:ref},options)}function IsRef(value){return IsKind(value,"Ref")}function Generic(parameters,expression){return memory_exports.Create({"~kind":"Generic"},{type:"generic",parameters,expression})}function IsGeneric(value){return IsKind(value,"Generic")}function Any(options){return memory_exports.Create({["~kind"]:"Any"},{},options)}function IsAny(value){return IsKind(value,"Any")}var NeverPattern="(?!)";function Never(options){return memory_exports.Create({"~kind":"Never"},{not:{}},options)}function IsNever(value){return IsKind(value,"Never")}function RequiredArray(properties){return guard_exports.Keys(properties).filter(key=>!IsOptional(properties[key]))}function PropertyKeys(properties){return guard_exports.Keys(properties)}function PropertyValues(properties){return guard_exports.Values(properties)}function _Object_(properties,options={}){const requiredKeys=RequiredArray(properties);const required=requiredKeys.length>0?{required:requiredKeys}:{};return memory_exports.Create({"~kind":"Object"},{type:"object",...required,properties},options)}function IsObject3(value){return IsKind(value,"Object")}function ObjectOptions(type){return memory_exports.Discard(type,["~kind","type","properties","required"])}function Union(anyOf,options={}){return memory_exports.Create({"~kind":"Union"},{anyOf},options)}function IsUnion(value){return IsKind(value,"Union")}function UnionOptions(type){return memory_exports.Discard(type,["~kind","anyOf"])}function Unknown(options){return memory_exports.Create({["~kind"]:"Unknown"},{},options)}function IsUnknown(value){return IsKind(value,"Unknown")}function Cyclic($defs,$ref,options){const defs=guard_exports.Keys($defs).reduce((result,key)=>{return{...result,[key]:memory_exports.Update($defs[key],{},{$id:key})}},{});return memory_exports.Create({["~kind"]:"Cyclic"},{$defs:defs,$ref},options)}function IsCyclic(value){return IsKind(value,"Cyclic")}function IsUnsafe(value){return guard_exports.IsObjectNotArray(value)&&guard_exports.HasPropertyKey(value,"~unsafe")&&guard_exports.IsNull(value["~unsafe"])}var arguments_exports={};__export(arguments_exports,{Match:()=>Match});function Match(args,match){return match[args.length]?.(...args)??(()=>{throw Error("Invalid Arguments")})()}function IsInfer(value){return IsKind(value,"Infer")}function IsEnum(value){return IsKind(value,"Enum")}function Intersect(types,options={}){return memory_exports.Create({"~kind":"Intersect"},{allOf:types},options)}function IsIntersect(value){return IsKind(value,"Intersect")}function IntersectOptions(type){return memory_exports.Discard(type,["~kind","allOf"])}var environment_exports={};__export(environment_exports,{CanEvaluate:()=>CanEvaluate,Evaluate:()=>Evaluate});var supported=void 0;function TryEvaluate(){try{Evaluate("null")();return true}catch{return false}}function CanEvaluate(){if(guard_exports.IsUndefined(supported))supported=TryEvaluate();return supported&&settings_exports.Get().useAcceleration}function Evaluate(...args){return new globalThis.Function(...args)}var hash_exports={};__export(hash_exports,{Hash:()=>Hash,HashCode:()=>HashCode});function Unreachable(){throw new Error("Unreachable")}function InstanceKeys(value){const propertyKeys=new Set;let current=value;while(current&¤t!==Object.prototype){for(const key of Reflect.ownKeys(current)){if(key!=="constructor"&&typeof key!=="symbol")propertyKeys.add(key)}current=Object.getPrototypeOf(current)}return[...propertyKeys]}function IsIEEE754(value){return typeof value==="number"}var ByteMarker;(function(ByteMarker2){ByteMarker2[ByteMarker2["Array"]=0]="Array";ByteMarker2[ByteMarker2["BigInt"]=1]="BigInt";ByteMarker2[ByteMarker2["\ +Boolean"]=2]="Boolean";ByteMarker2[ByteMarker2["Date"]=3]="Date";ByteMarker2[ByteMarker2["Constructor"]=4]="Constructor";ByteMarker2[ByteMarker2["Function"]=5]="Function";ByteMarker2[ByteMarker2["Null"]=6]="Null";ByteMarker2[ByteMarker2["Number"]=7]="Number";ByteMarker2[ByteMarker2["Object"]=8]="Object";ByteMarker2[ByteMarker2["RegExp"]=9]="RegExp";ByteMarker2[ByteMarker2["String"]=10]="String";ByteMarker2[ByteMarker2["Symbol"]=11]="Symbol";ByteMarker2[ByteMarker2["TypeArray"]=12]="TypeArray";ByteMarker2[ByteMarker2["\ +Undefined"]=13]="Undefined"})(ByteMarker||(ByteMarker={}));var Accumulator=BigInt("14695981039346656037");var[Prime,Size]=[BigInt("1099511628211"),BigInt("18446744073709551616")];var Bytes=Array.from({length:256}).map((_,i)=>BigInt(i));var F64=new Float64Array(1);var F64In=new DataView(F64.buffer);var F64Out=new Uint8Array(F64.buffer);function FNV1A64_OP(byte){Accumulator=Accumulator^Bytes[byte];Accumulator=Accumulator*Prime%Size}function FromArray2(value){FNV1A64_OP(ByteMarker.Array);for(const item of value){ +FromValue2(item)}}function FromBigInt(value){FNV1A64_OP(ByteMarker.BigInt);F64In.setBigInt64(0,value);for(const byte of F64Out){FNV1A64_OP(byte)}}function FromBoolean(value){FNV1A64_OP(ByteMarker.Boolean);FNV1A64_OP(value?1:0)}function FromConstructor(value){FNV1A64_OP(ByteMarker.Constructor);FromValue2(value.toString())}function FromDate(value){FNV1A64_OP(ByteMarker.Date);FromValue2(value.getTime())}function FromFunction(value){FNV1A64_OP(ByteMarker.Function);FromValue2(value.toString())}function FromNull(_value){ +FNV1A64_OP(ByteMarker.Null)}function FromNumber(value){FNV1A64_OP(ByteMarker.Number);F64In.setFloat64(0,value,true);for(const byte of F64Out){FNV1A64_OP(byte)}}function FromObject2(value){FNV1A64_OP(ByteMarker.Object);for(const key of InstanceKeys(value).sort()){FromValue2(key);FromValue2(value[key])}}function FromRegExp2(value){FNV1A64_OP(ByteMarker.RegExp);FromString(value.toString())}var encoder=new TextEncoder;function FromString(value){FNV1A64_OP(ByteMarker.String);for(const byte of encoder. +encode(value)){FNV1A64_OP(byte)}}function FromSymbol(value){FNV1A64_OP(ByteMarker.Symbol);FromValue2(value.toString())}function FromTypeArray(value){FNV1A64_OP(ByteMarker.TypeArray);const buffer=new Uint8Array(value.buffer);for(let i=0;iIsRefinement(value2))}var BigIntPattern="-?(?:0|[1-9][0-9]*)n";function BigInt2(options){return memory_exports.Create({"~kind":"BigInt"},{type:"bigint"},options)}function IsBigInt3(value){return IsKind(value,"BigInt")}function IsBoolean4(value){return IsKind(value,"Boolean")}var IntegerPattern="-?(?:0|[1-9][0-9]*)";function Integer(options){return memory_exports.Create({"~kind":"Integer"},{type:"integer"},options)}function IsInteger3(value){return IsKind(value,"Integer")}function Iterator(iteratorItems,options){return memory_exports.Create({"~kind":"Iterator"},{type:"iterator",iteratorItems},options)}function IsIterator3(value){return IsKind(value,"Iterator")}function IteratorOptions(type){return memory_exports.Discard(type,["~kind","type","iteratorItems"])}var InvalidLiteralValue=class extends Error{constructor(value){super(`Invalid Literal value`);Object.defineProperty(this,"cause",{value:{value},writable:false,configurable:false,enumerable:false})}};function LiteralTypeName(value){return guard_exports.IsBigInt(value)?"bigint":guard_exports.IsBoolean(value)?"boolean":guard_exports.IsNumber(value)?"number":guard_exports.IsString(value)?"string":(()=>{throw new InvalidLiteralValue(value)})()}function Literal(value,options){return memory_exports.Create( +{"~kind":"Literal"},{type:LiteralTypeName(value),const:value},options)}function IsLiteralValue(value){return guard_exports.IsBigInt(value)||guard_exports.IsBoolean(value)||guard_exports.IsNumber(value)||guard_exports.IsString(value)}function IsLiteralBigInt(value){return IsLiteral(value)&&guard_exports.IsBigInt(value.const)}function IsLiteralBoolean(value){return IsLiteral(value)&&guard_exports.IsBoolean(value.const)}function IsLiteralNumber(value){return IsLiteral(value)&&guard_exports.IsNumber( +value.const)}function IsLiteralString(value){return IsLiteral(value)&&guard_exports.IsString(value.const)}function IsLiteral(value){return IsKind(value,"Literal")}function Null(options){return memory_exports.Create({"~kind":"Null"},{type:"null"},options)}function IsNull3(value){return IsKind(value,"Null")}var NumberPattern="-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?";function Number2(options){return memory_exports.Create({"~kind":"Number"},{type:"number"},options)}function IsNumber4(value){return IsKind(value,"Number")}function Symbol2(options){return memory_exports.Create({"~kind":"Symbol"},{type:"symbol"},options)}function IsSymbol3(value){return IsKind(value,"Symbol")}var StringPattern=".*";function String2(options){return memory_exports.Create({"~kind":"String"},{type:"string"},options)}function IsString4(value){return IsKind(value,"String")}function ParsePatternIntoTypes(pattern){const parsed=Pattern(pattern);const result=guard_exports.IsEqual(parsed.length,2)?parsed[0]:[];return result}function FromLiteral(_value){return true}function FromTypesReduce(types){return guard_exports.TakeLeft(types,(left,right)=>FromType(left)?FromTypesReduce(right):false,()=>true)}function FromTypes(types){const result=guard_exports.IsEqual(types.length,0)?false:FromTypesReduce(types);return result}function FromType(type){return IsUnion(type)?FromTypes(type.anyOf):IsLiteral(type)?FromLiteral(type.const):false}function IsTemplateLiteralFinite(types){const result=FromTypes(types);return result}function TemplateLiteralCreate(pattern){return memory_exports.Create({["~kind"]:"TemplateLiteral"},{type:"string",pattern},{})}function FromLiteralPush(variants,value,result=[]){return guard_exports.TakeLeft(variants,(left,right)=>FromLiteralPush(right,value,[...result,`${left}${value}`]),()=>result)}function FromLiteral2(variants,value){return guard_exports.IsEqual(variants.length,0)?[`${value}`]:FromLiteralPush(variants,value)}function FromUnion(variants,types,result=[]){return guard_exports.TakeLeft(types,(left,right)=>FromUnion(variants,right,[...result,...FromType2(variants,left)]),()=>result)}function FromType2(variants,type){ +const result=IsUnion(type)?FromUnion(variants,type.anyOf):IsLiteral(type)?FromLiteral2(variants,type.const):Unreachable();return result}function DecodeFromSpan(variants,types){return guard_exports.TakeLeft(types,(left,right)=>DecodeFromSpan(FromType2(variants,left),right),()=>variants)}function VariantsToLiterals(variants){return variants.map(variant=>Literal(variant))}function DecodeTypesAsUnion(types){const variants=DecodeFromSpan([],types);const literals=VariantsToLiterals(variants);const result=Union( +literals);return result}function DecodeTypes(types){return guard_exports.IsEqual(types.length,0)?Unreachable():guard_exports.IsEqual(types.length,1)&&IsLiteral(types[0])?types[0]:DecodeTypesAsUnion(types)}function TemplateLiteralDecodeUnsafe(pattern){const types=ParsePatternIntoTypes(pattern);const result=guard_exports.IsEqual(types.length,0)?String2():IsTemplateLiteralFinite(types)?DecodeTypes(types):TemplateLiteralCreate(pattern);return result}function TemplateLiteralDecode(pattern){const decoded=TemplateLiteralDecodeUnsafe( +pattern);const result=IsTemplateLiteral(decoded)?String2():decoded;return result}function CreateRecord(key,value){const type="object";const patternProperties={[key]:value};return memory_exports.Create({["~kind"]:"Record"},{type,patternProperties})}function FromAnyKey(value){return CreateRecord(StringKey,value)}function FromBooleanKey(value){return _Object_({true:value,false:value})}function FromEnumValue(value){return guard_exports.IsString(value)||guard_exports.IsNumber(value)?Literal(value):guard_exports.IsNull(value)?Null():Never()}function EnumValuesToVariants(values){const result=values.map(value=>FromEnumValue(value));return result}function EnumValuesToUnion(values){const variants=EnumValuesToVariants(values);const result=Union(variants);return result}function EnumToUnion(type){const result=EnumValuesToUnion(type.enum);return result}function FromEnumKey(values,value){const unionKey=EnumValuesToUnion(values);const result=FromKey(unionKey,value);return result}function FromIntegerKey(_key,value){const result=CreateRecord(IntegerKey,value);return result}function Tuple(types,options={}){const[items,minItems,additionalItems]=[types,types.length,false];return memory_exports.Create({["~kind"]:"Tuple"},{type:"array",additionalItems,items,minItems},options)}function IsTuple(value){return IsKind(value,"Tuple")}function TupleOptions(type){return memory_exports.Discard(type,["~kind","type","items","minItems","additionalItems"])}function TupleElementsToProperties(types){const result=types.reduceRight((result2,right,index2)=>{return{[index2]:right,...result2}},{});return result}function TupleToObject(type){const properties=TupleElementsToProperties(type.items);const result=_Object_(properties);return result}function IsReadonlyProperty(left,right){return IsReadonly(left)?IsReadonly(right)?true:false:false}function IsOptionalProperty(left,right){return IsOptional(left)?IsOptional(right)?true:false:false}function CompositeProperty(left,right){const isReadonly=IsReadonlyProperty(left,right);const isOptional=IsOptionalProperty(left,right);const evaluated=EvaluateIntersect([left,right]);const property=ReadonlyRemove(OptionalRemove(evaluated));return isReadonly&&isOptional?ReadonlyAdd(OptionalAdd(property)): +isReadonly&&!isOptional?ReadonlyAdd(property):!isReadonly&&isOptional?OptionalAdd(property):property}function CompositePropertyKey(left,right,key){return key in left?key in right?CompositeProperty(left[key],right[key]):left[key]:key in right?right[key]:Never()}function CompositeProperties(left,right){const keys=new Set([...guard_exports.Keys(right),...guard_exports.Keys(left)]);return[...keys].reduce((result,key)=>{return{...result,[key]:CompositePropertyKey(left,right,key)}},{})}function GetProperties(type){ +const result=IsObject3(type)?type.properties:IsTuple(type)?TupleElementsToProperties(type.items):Unreachable();return result}function Composite(left,right){const leftProperties=GetProperties(left);const rightProperties=GetProperties(right);const properties=CompositeProperties(leftProperties,rightProperties);return _Object_(properties)}function Narrow(left,right){const result=Compare(left,right);return guard_exports.IsEqual(result,ResultLeftInside)?left:guard_exports.IsEqual(result,ResultRightInside)?right:guard_exports.IsEqual(result,ResultEqual)?right:Never()}function IsObjectLike(type){return IsObject3(type)||IsTuple(type)}function IsUnionOperand(left,right){const isUnionLeft=IsUnion(left);const isUnionRight=IsUnion(right);const result=isUnionLeft||isUnionRight;return result}function DistributeOperation(left,right){const evaluatedLeft=EvaluateType(left);const evaluatedRight=EvaluateType(right);const isUnionOperand=IsUnionOperand(evaluatedLeft,evaluatedRight);const isObjectLeft=IsObjectLike(evaluatedLeft);const IsObjectRight=IsObjectLike(evaluatedRight); +const result=isUnionOperand?EvaluateIntersect([evaluatedLeft,evaluatedRight]):isObjectLeft&&IsObjectRight?Composite(evaluatedLeft,evaluatedRight):isObjectLeft&&!IsObjectRight?evaluatedLeft:!isObjectLeft&&IsObjectRight?evaluatedRight:Narrow(evaluatedLeft,evaluatedRight);return result}function DistributeType(type,types,result=[]){return guard_exports.TakeLeft(types,(left,right)=>DistributeType(type,right,[...result,DistributeOperation(type,left)]),()=>guard_exports.IsEqual(result.length,0)?[type]: +result)}function DistributeUnion(types,distribution,result=[]){return guard_exports.TakeLeft(types,(left,right)=>DistributeUnion(right,distribution,[...result,...Distribute([left],distribution)]),()=>result)}function Distribute(types,result=[]){return guard_exports.TakeLeft(types,(left,right)=>IsUnion(left)?Distribute(right,DistributeUnion(left.anyOf,result)):Distribute(right,DistributeType(left,result)),()=>result)}function EvaluateIntersect(types){const distribution=Distribute(types);const result=Broaden(distribution);return result}function EvaluateUnion(types){const result=Broaden(types);return result}function EvaluateType(type){return IsIntersect(type)?EvaluateIntersect(type.allOf):IsUnion(type)?EvaluateUnion(type.anyOf):type}function EvaluateUnionFast(types){const result=guard_exports.IsEqual(types.length,1)?types[0]:guard_exports.IsEqual(types.length,0)?Never():Union(types);return result}function FromIntersectKey(types,value){const evaluatedKey=EvaluateIntersect(types);const result=FromKey(evaluatedKey,value);return result}function FromLiteralKey(key,value){return guard_exports.IsString(key)||guard_exports.IsNumber(key)?_Object_({[key]:value}):guard_exports.IsEqual(key,false)?_Object_({false:value}):guard_exports.IsEqual(key,true)?_Object_({true:value}):_Object_({})}function FromNumberKey(_key,value){const result=CreateRecord(NumberKey,value);return result}function FromStringKey(key,value){return guard_exports.HasPropertyKey(key,"pattern")&&(guard_exports.IsString(key.pattern)||key.pattern instanceof RegExp)?CreateRecord(key.pattern.toString(),value):CreateRecord(StringKey,value)}function FromTemplateKey(pattern,value){const types=ParsePatternIntoTypes(pattern);const finite=IsTemplateLiteralFinite(types);const result=finite?FromKey(TemplateLiteralDecode(pattern),value):CreateRecord(pattern,value);return result}function FlattenType(type){const result=IsUnion(type)?Flatten(type.anyOf):[type];return result}function Flatten(types){return types.reduce((result,type)=>{return[...result,...FlattenType(type)]},[])}function StringOrNumberCheck(types){return types.some(type=>IsString4(type)||IsNumber4(type)||IsInteger3(type))}function TryBuildRecord(types,value){return guard_exports.IsEqual(StringOrNumberCheck(types),true)?CreateRecord(StringKey,value):void 0}function CreateProperties(types,value){return types.reduce((result,left)=>{return IsLiteral(left)&&(guard_exports.IsString(left.const)||guard_exports.IsNumber(left.const))?{...result,[left.const]:value}:result},{})}function CreateObject(types,value){const properties=CreateProperties( +types,value);const result=_Object_(properties);return result}function FromUnionKey(types,value){const flattened=Flatten(types);const record=TryBuildRecord(flattened,value);return IsSchema(record)?record:CreateObject(flattened,value)}function FromKey(key,value){const result=IsAny(key)?FromAnyKey(value):IsBoolean4(key)?FromBooleanKey(value):IsEnum(key)?FromEnumKey(key.enum,value):IsInteger3(key)?FromIntegerKey(key,value):IsIntersect(key)?FromIntersectKey(key.allOf,value):IsLiteral(key)?FromLiteralKey(key.const,value):IsNumber4(key)?FromNumberKey(key,value):IsUnion(key)?FromUnionKey(key.anyOf,value):IsString4(key)?FromStringKey(key,value):IsTemplateLiteral(key)?FromTemplateKey(key.pattern,value):_Object_({});return result}function RecordAction(key,value,options){const result=CanInstantiate([key])?memory_exports.Update(FromKey(key,value),{},options):RecordDeferred(key,value,options);return result}function RecordInstantiate(context,state2,key,value,options){const instantiatedKey=InstantiateType(context,state2,key);const instantiatedValue=InstantiateType(context,state2,value);return RecordAction(instantiatedKey,instantiatedValue,options)}var IntegerKey=`^${IntegerPattern}$`;var NumberKey=`^${NumberPattern}$`;var StringKey=`^${StringPattern}$`;function RecordDeferred(key,value,options={}){return Deferred("Record",[key,value],options)}function Record(key,value,options={}){return RecordAction(key,value,options)}function RecordFromPattern(key,value){return CreateRecord(key,value)}function RecordPattern(type){return guard_exports.Keys(type.patternProperties)[0]}function RecordKey(type){const pattern=RecordPattern(type);const result=guard_exports. +IsEqual(pattern,StringKey)?String2():guard_exports.IsEqual(pattern,IntegerKey)?Integer():guard_exports.IsEqual(pattern,NumberKey)?Number2():TemplateLiteralDecodeUnsafe(pattern);return result}function RecordValue(type){return type.patternProperties[RecordPattern(type)]}function IsRecord(value){return IsKind(value,"Record")}function Rest(type){return memory_exports.Create({"~kind":"Rest"},{type:"rest",items:type},{})}function IsRest(value){return IsKind(value,"Rest")}function IsThis(value){return IsKind(value,"This")}function Undefined(options){return memory_exports.Create({"~kind":"Undefined"},{type:"undefined"},options)}function IsUndefined3(value){return IsKind(value,"Undefined")}function IsVoid(value){return IsKind(value,"Void")}function PatternBigIntMapping(input){return BigInt2()}function PatternStringMapping(input){return String2()}function PatternNumberMapping(input){return Number2()}function PatternIntegerMapping(input){return Integer()}function PatternNeverMapping(input){return Never()}function PatternTextMapping(input){return Literal(input)}function PatternBaseMapping(input){return input}function PatternGroupMapping(input){return Union(input[1])}function PatternUnionMapping(input){return input.length===3?[...input[0], +...input[2]]:input.length===1?[...input[0]]:[]}function PatternTermMapping(input){return[input[0],...input[1]]}function PatternBodyMapping(input){return input}function PatternMapping(input){return input[1]}function IsMatch(value){return IsEqual(value.length,2)}function Match2(input,ok,fail){return IsMatch(input)?ok(input[0],input[1]):fail()}function TakeVariant(variant,input){return IsEqual(input.indexOf(variant),0)?[variant,input.slice(variant.length)]:[]}function Take(variants,input){for(let i=0;iString.fromCharCode(start+i))}var Alpha=[...Range(97,122),...Range(65,90)];var Zero="0";var NonZero=Range(49,57);var Digit=[Zero,...NonZero];var WhiteSpace=" ";var NewLine="\n";var UnderScore="_";var DollarSign="$";var LineComment="//";var OpenComment="/*";var CloseComment="*/";function DiscardMultilineComment(input){const index2=input.indexOf(CloseComment);const result=IsEqual(index2,-1)?"":input.slice(index2+2);return result}function DiscardLineComment(input){const index2=input.indexOf(NewLine);const result=IsEqual(index2,-1)?"":input.slice(index2);return result}function TrimStartUntilNewline(input){return input.replace(/^[ \t\r\f\v]+/,"")}function TrimWhitespace(input){const trimmed=TrimStartUntilNewline( +input);return trimmed.startsWith(OpenComment)?TrimWhitespace(DiscardMultilineComment(trimmed.slice(2))):trimmed.startsWith(LineComment)?TrimWhitespace(DiscardLineComment(trimmed.slice(2))):trimmed}function Trim(input){const trimmed=input.trimStart();return trimmed.startsWith(OpenComment)?Trim(DiscardMultilineComment(trimmed.slice(2))):trimmed.startsWith(LineComment)?Trim(DiscardLineComment(trimmed.slice(2))):trimmed}var AllowedDigits=[...Digit,UnderScore];function TakeConst(const_,input){return Take([const_],input)}function Const(const_,input){return IsEqual(const_,"")?["",input]:const_.startsWith(NewLine)?TakeConst(const_,TrimWhitespace(input)):const_.startsWith(WhiteSpace)?TakeConst(const_,input):TakeConst(const_,Trim(input))}var Initial=[...Alpha,UnderScore,DollarSign];var Remaining=[...Initial,...Digit];var AllowedDigits2=[...Digit,UnderScore];function TakeOne(input){const result=IsEqual(input,"")?[]:[input.slice(0,1),input.slice(1)];return result}function IsInputMatchSentinal(end,input){return TakeLeft(end,(left,right)=>input.startsWith(left)?true:IsInputMatchSentinal(right,input),()=>false)}function Until(end,input,result=""){return Match2(TakeOne(input),(One,Rest2)=>IsInputMatchSentinal(end,input)?[result,input]:Until(end,Rest2,`${result}${One}`),()=>[])}function Until_1(end,input){return Match2(Until(end,input),(Until2,UntilRest)=>IsEqual(Until2,"")?[]:[Until2,UntilRest],()=>[])}var If2=(result,left,right=()=>[])=>result.length===2?left(result):right();var PatternBigInt=input=>If2(Const("-?(?:0|[1-9][0-9]*)n",input),([_0,input2])=>[PatternBigIntMapping(_0),input2]);var PatternString=input=>If2(Const(".*",input),([_0,input2])=>[PatternStringMapping(_0),input2]);var PatternNumber=input=>If2(Const("-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?",input),([_0,input2])=>[PatternNumberMapping(_0),input2]);var PatternInteger=input=>If2(Const("-?(?:0|[1-9][0-9]*)",input),([_0,input2])=>[PatternIntegerMapping(_0),input2]);var PatternNever=input=>If2(Const("(?!)",input), +([_0,input2])=>[PatternNeverMapping(_0),input2]);var PatternText=input=>If2(Until_1(["-?(?:0|[1-9][0-9]*)n",".*","-?(?:0|[1-9][0-9]*)(?:.[0-9]+)?","-?(?:0|[1-9][0-9]*)","(?!)","(",")","$","|"],input),([_0,input2])=>[PatternTextMapping(_0),input2]);var PatternBase=input=>If2(If2(PatternBigInt(input),([_0,input2])=>[_0,input2],()=>If2(PatternString(input),([_0,input2])=>[_0,input2],()=>If2(PatternNumber(input),([_0,input2])=>[_0,input2],()=>If2(PatternInteger(input),([_0,input2])=>[_0,input2],()=>If2( +PatternNever(input),([_0,input2])=>[_0,input2],()=>If2(PatternGroup(input),([_0,input2])=>[_0,input2],()=>If2(PatternText(input),([_0,input2])=>[_0,input2],()=>[]))))))),([_0,input2])=>[PatternBaseMapping(_0),input2]);var PatternGroup=input=>If2(If2(Const("(",input),([_0,input2])=>If2(PatternBody(input2),([_1,input3])=>If2(Const(")",input3),([_2,input4])=>[[_0,_1,_2],input4]))),([_0,input2])=>[PatternGroupMapping(_0),input2]);var PatternUnion=input=>If2(If2(If2(PatternTerm(input),([_0,input2])=>If2( +Const("|",input2),([_1,input3])=>If2(PatternUnion(input3),([_2,input4])=>[[_0,_1,_2],input4]))),([_0,input2])=>[_0,input2],()=>If2(If2(PatternTerm(input),([_0,input2])=>[[_0],input2]),([_0,input2])=>[_0,input2],()=>If2([[],input],([_0,input2])=>[_0,input2],()=>[]))),([_0,input2])=>[PatternUnionMapping(_0),input2]);var PatternTerm=input=>If2(If2(PatternBase(input),([_0,input2])=>If2(PatternBody(input2),([_1,input3])=>[[_0,_1],input3])),([_0,input2])=>[PatternTermMapping(_0),input2]);var PatternBody=input=>If2( +If2(PatternUnion(input),([_0,input2])=>[_0,input2],()=>If2(PatternTerm(input),([_0,input2])=>[_0,input2],()=>[])),([_0,input2])=>[PatternBodyMapping(_0),input2]);var Pattern=input=>If2(If2(Const("^",input),([_0,input2])=>If2(PatternBody(input2),([_1,input3])=>If2(Const("$",input3),([_2,input4])=>[[_0,_1,_2],input4]))),([_0,input2])=>[PatternMapping(_0),input2]);function JoinString(input){return input.join("|")}function UnwrapTemplateLiteralPattern(pattern){return pattern.slice(1,pattern.length-1)}function EncodeLiteral(value,right,pattern){return EncodeTypes(right,`${pattern}${value}`)}function EncodeBigInt(right,pattern){return EncodeTypes(right,`${pattern}${BigIntPattern}`)}function EncodeInteger(right,pattern){return EncodeTypes(right,`${pattern}${IntegerPattern}`)}function EncodeNumber(right,pattern){return EncodeTypes(right,`${pattern}${NumberPattern}`)} +function EncodeBoolean(right,pattern){return EncodeType(Union([Literal("false"),Literal("true")]),right,pattern)}function EncodeString(right,pattern){return EncodeTypes(right,`${pattern}${StringPattern}`)}function EncodeTemplateLiteral(templatePattern,right,pattern){return EncodeTypes(right,`${pattern}${UnwrapTemplateLiteralPattern(templatePattern)}`)}function EncodeTemplateLiteralDeferred(types,right,pattern){const templateLiteral=TemplateLiteralAction(types,{});const result=EncodeType(templateLiteral, +right,pattern);return result}function EncodeEnum(types,right,pattern){const variants=EnumValuesToVariants(types);return EncodeUnion(variants,right,pattern)}function EncodeUnion(types,right,pattern,result=[]){return guard_exports.TakeLeft(types,(head,tail)=>EncodeUnion(tail,right,pattern,[...result,EncodeType(head,[],"")]),()=>EncodeTypes(right,`${pattern}(${JoinString(result)})`))}function EncodeType(type,right,pattern){return IsEnum(type)?EncodeEnum(type.enum,right,pattern):IsInteger3(type)?EncodeInteger( +right,pattern):IsLiteral(type)?EncodeLiteral(type.const,right,pattern):IsBigInt3(type)?EncodeBigInt(right,pattern):IsBoolean4(type)?EncodeBoolean(right,pattern):IsNumber4(type)?EncodeNumber(right,pattern):IsString4(type)?EncodeString(right,pattern):IsTemplateLiteral(type)?EncodeTemplateLiteral(type.pattern,right,pattern):IsTemplateLiteralDeferred(type)?EncodeTemplateLiteralDeferred(type.parameters[0],right,pattern):IsUnion(type)?EncodeUnion(type.anyOf,right,pattern):NeverPattern}function EncodeTypes(types,pattern){ +return guard_exports.TakeLeft(types,(left,right)=>EncodeType(left,right,pattern),()=>pattern)}function EncodePattern(types){const encoded=EncodeTypes(types,"");const result=`^${encoded}$`;return result}function TemplateLiteralEncode(types){const pattern=EncodePattern(types);const result=TemplateLiteralCreate(pattern);return result}function TemplateLiteralAction(types,options){const result=CanInstantiate(types)?memory_exports.Update(TemplateLiteralEncode(types),{},options):TemplateLiteralDeferred(types,options);return result}function TemplateLiteralInstantiate(context,state2,types,options){const instantiatedTypes=InstantiateTypes(context,state2,types);return TemplateLiteralAction(instantiatedTypes,options)}function TemplateLiteralDeferred(types,options={}){return Deferred("TemplateLiteral",[types],options)}function IsTemplateLiteralDeferred(value){return IsSchema(value)&&guard_exports.HasPropertyKey(value,"action")&&guard_exports.IsEqual(value.action,"TemplateLiteral")}function IsTemplateLiteral(value){return IsKind(value,"TemplateLiteral")}var result_exports={};__export(result_exports,{ExtendsFalse:()=>ExtendsFalse,ExtendsTrue:()=>ExtendsTrue,ExtendsUnion:()=>ExtendsUnion,IsExtendsFalse:()=>IsExtendsFalse,IsExtendsTrue:()=>IsExtendsTrue,IsExtendsTrueLike:()=>IsExtendsTrueLike,IsExtendsUnion:()=>IsExtendsUnion,Match:()=>Match3});function ExtendsUnion(inferred){return memory_exports.Create({["~kind"]:"ExtendsUnion"},{inferred})}function IsExtendsUnion(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"~kind")&&guard_exports.HasPropertyKey(value,"inferred")&&guard_exports.IsEqual(value["~kind"],"ExtendsUnion")&&guard_exports.IsObject(value.inferred)}function ExtendsTrue(inferred){return memory_exports.Create({["~kind"]:"ExtendsTrue"},{inferred})}function IsExtendsTrue(value){return guard_exports. +IsObject(value)&&guard_exports.HasPropertyKey(value,"~kind")&&guard_exports.HasPropertyKey(value,"inferred")&&guard_exports.IsEqual(value["~kind"],"ExtendsTrue")&&guard_exports.IsObject(value.inferred)}function ExtendsFalse(){return memory_exports.Create({["~kind"]:"ExtendsFalse"},{})}function IsExtendsFalse(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"~kind")&&guard_exports.IsEqual(value["~kind"],"ExtendsFalse")}function IsExtendsTrueLike(value){return IsExtendsUnion( +value)||IsExtendsTrue(value)}function Match3(result,true_,false_){return IsExtendsTrueLike(result)?true_(result.inferred):false_()}function ExtendsRightInfer(inferred,name,left,right){return Match3(ExtendsLeft(inferred,left,right),checkInferred=>ExtendsTrue(memory_exports.Assign(memory_exports.Assign(inferred,checkInferred),{[name]:left})),()=>ExtendsFalse())}function ExtendsRightAny(inferred,_left){return ExtendsTrue(inferred)}function ExtendsRightEnum(inferred,left,right){const union=EnumValuesToUnion(right);return ExtendsLeft(inferred,left,union)}function ExtendsRightIntersect(inferred,left,right){return guard_exports.TakeLeft( +right,(head,tail)=>Match3(ExtendsLeft(inferred,left,head),inferred2=>ExtendsRightIntersect(inferred2,left,tail),()=>ExtendsFalse()),()=>ExtendsTrue(inferred))}function ExtendsRightTemplateLiteral(inferred,left,right){const decoded=TemplateLiteralDecode(right);return ExtendsLeft(inferred,left,decoded)}function ExtendsRightUnion(inferred,left,right){return guard_exports.TakeLeft(right,(head,tail)=>Match3(ExtendsLeft(inferred,left,head),inferred2=>ExtendsTrue(inferred2),()=>ExtendsRightUnion(inferred, +left,tail)),()=>ExtendsFalse())}function ExtendsRight(inferred,left,right){return IsAny(right)?ExtendsRightAny(inferred,left):IsEnum(right)?ExtendsRightEnum(inferred,left,right.enum):IsInfer(right)?ExtendsRightInfer(inferred,right.name,left,right.extends):IsIntersect(right)?ExtendsRightIntersect(inferred,left,right.allOf):IsTemplateLiteral(right)?ExtendsRightTemplateLiteral(inferred,left,right.pattern):IsUnion(right)?ExtendsRightUnion(inferred,left,right.anyOf):IsUnknown(right)?ExtendsTrue(inferred): +ExtendsFalse()}function ExtendsAny(inferred,left,right){return IsInfer(right)?ExtendsRight(inferred,left,right):IsAny(right)?ExtendsTrue(inferred):IsUnknown(right)?ExtendsTrue(inferred):ExtendsUnion(inferred)}function ExtendsImmutable(left,right){const isImmutableLeft=IsImmutable(left);const isImmutableRight=IsImmutable(right);return isImmutableLeft&&isImmutableRight?true:!isImmutableLeft&&isImmutableRight?true:isImmutableLeft&&!isImmutableRight?false:true}function ExtendsArray(inferred,arrayLeft,left,right){return IsArray3(right)?ExtendsImmutable(arrayLeft,right)?ExtendsLeft(inferred,left,right.items):ExtendsFalse():ExtendsRight(inferred,arrayLeft,right)}function ExtendsAsyncIterator(inferred,left,right){return IsAsyncIterator3(right)?ExtendsLeft(inferred,left,right.iteratorItems):ExtendsRight(inferred,AsyncIterator(left),right)}function ExtendsBigInt(inferred,left,right){return IsBigInt3(right)?ExtendsTrue(inferred):ExtendsRight(inferred,left,right)}function ExtendsBoolean(inferred,left,right){return IsBoolean4(right)?ExtendsTrue(inferred):ExtendsRight(inferred,left,right)}function ParameterCompare(inferred,left,leftRest,right,rightRest){const checkLeft=IsInfer(right)?left:right;const checkRight=IsInfer(right)?right:left;const isLeftOptional=IsOptional(left);const isRightOptional=IsOptional(right);return!isLeftOptional&&isRightOptional?ExtendsFalse():Match3(ExtendsLeft(inferred,checkLeft,checkRight),inferred2=>ExtendsParameters(inferred2,leftRest,rightRest),()=>ExtendsFalse())}function ParameterRight(inferred,left,leftRest,rightRest){return guard_exports.TakeLeft( +rightRest,(head,tail)=>ParameterCompare(inferred,left,leftRest,head,tail),()=>IsOptional(left)?ExtendsTrue(inferred):ExtendsFalse())}function ParametersLeft(inferred,left,rightRest){return guard_exports.TakeLeft(left,(head,tail)=>ParameterRight(inferred,head,tail,rightRest),()=>ExtendsTrue(inferred))}function ExtendsParameters(inferred,left,right){return ParametersLeft(inferred,left,right)}function ExtendsReturnType(inferred,left,right){return IsVoid(right)?ExtendsTrue(inferred):ExtendsLeft(inferred,left,right)}function ExtendsConstructor(inferred,parameters,returnType,right){return IsAny(right)?ExtendsTrue(inferred):IsUnknown(right)?ExtendsTrue(inferred):IsConstructor3(right)?Match3(ExtendsParameters(inferred,parameters,right["parameters"]),inferred2=>ExtendsReturnType(inferred2,returnType,right["instanceType"]),()=>ExtendsFalse()):ExtendsFalse()}function ExtendsEnum(inferred,left,right){return ExtendsLeft(inferred,EnumToUnion(left),right)}function ExtendsFunction(inferred,parameters,returnType,right){return IsAny(right)?ExtendsTrue(inferred):IsUnknown(right)?ExtendsTrue(inferred):IsFunction3(right)?Match3(ExtendsParameters(inferred,parameters,right["parameters"]),inferred2=>ExtendsReturnType(inferred2,returnType,right["returnType"]),()=>ExtendsFalse()):ExtendsFalse()}function ExtendsInteger(inferred,left,right){return IsInteger3(right)?ExtendsTrue(inferred):IsNumber4(right)?ExtendsTrue(inferred):ExtendsRight(inferred,left,right)}function ExtendsIntersect(inferred,left,right){const evaluated=EvaluateIntersect(left);return ExtendsLeft(inferred,evaluated,right)}function ExtendsIterator(inferred,left,right){return IsIterator3(right)?ExtendsLeft(inferred,left,right.iteratorItems):ExtendsRight(inferred,Iterator(left),right)}function ExtendsLiteralValue(inferred,left,right){return left===right?ExtendsTrue(inferred):ExtendsFalse()}function ExtendsLiteralBigInt(inferred,left,right){return IsLiteral(right)?ExtendsLiteralValue(inferred,left,right.const):IsBigInt3(right)?ExtendsTrue(inferred):ExtendsRight(inferred,Literal(left),right)}function ExtendsLiteralBoolean(inferred,left,right){return IsLiteral(right)?ExtendsLiteralValue(inferred,left,right.const):IsBoolean4(right)?ExtendsTrue(inferred):ExtendsRight(inferred,Literal( +left),right)}function ExtendsLiteralNumber(inferred,left,right){return IsLiteral(right)?ExtendsLiteralValue(inferred,left,right.const):IsNumber4(right)?ExtendsTrue(inferred):ExtendsRight(inferred,Literal(left),right)}function ExtendsLiteralString(inferred,left,right){return IsLiteral(right)?ExtendsLiteralValue(inferred,left,right.const):IsString4(right)?ExtendsTrue(inferred):ExtendsRight(inferred,Literal(left),right)}function ExtendsLiteral(inferred,left,right){return guard_exports.IsBigInt(left. +const)?ExtendsLiteralBigInt(inferred,left.const,right):guard_exports.IsBoolean(left.const)?ExtendsLiteralBoolean(inferred,left.const,right):guard_exports.IsNumber(left.const)?ExtendsLiteralNumber(inferred,left.const,right):guard_exports.IsString(left.const)?ExtendsLiteralString(inferred,left.const,right):Unreachable()}function ExtendsNever(inferred,left,right){return IsInfer(right)?ExtendsRight(inferred,left,right):ExtendsTrue(inferred)}function ExtendsNull(inferred,left,right){return IsNull3(right)?ExtendsTrue(inferred):ExtendsRight(inferred,left,right)}function ExtendsNumber(inferred,left,right){return IsNumber4(right)?ExtendsTrue(inferred):ExtendsRight(inferred,left,right)}function ExtendsPropertyOptional(inferred,left,right){return IsOptional(left)?IsOptional(right)?ExtendsTrue(inferred):ExtendsFalse():ExtendsTrue(inferred)}function ExtendsProperty(inferred,left,right){return IsInfer(right)&&IsNever(right.extends)?ExtendsFalse():Match3(ExtendsLeft(inferred,left,right),inferred2=>ExtendsPropertyOptional(inferred2,left,right),()=>ExtendsFalse())}function ExtractInferredProperties(keys,properties){return keys.reduce((result,key)=>{return key in properties?IsExtendsTrueLike( +properties[key])?{...result,...properties[key].inferred}:Unreachable():Unreachable()},{})}function ExtendsPropertiesComparer(inferred,left,right){const properties={};for(const rightKey of guard_exports.Keys(right)){properties[rightKey]=rightKey in left?ExtendsProperty({},left[rightKey],right[rightKey]):IsOptional(right[rightKey])?IsInfer(right[rightKey])?ExtendsTrue(memory_exports.Assign(inferred,{[right[rightKey].name]:right[rightKey].extends})):ExtendsTrue(inferred):ExtendsFalse()}const checked=guard_exports. +Values(properties).every(result=>IsExtendsTrueLike(result));const extracted=checked?ExtractInferredProperties(guard_exports.Keys(properties),properties):{};return checked?ExtendsTrue(extracted):ExtendsFalse()}function ExtendsProperties(inferred,left,right){const compared=ExtendsPropertiesComparer(inferred,left,right);return IsExtendsTrueLike(compared)?ExtendsTrue(memory_exports.Assign(inferred,compared.inferred)):ExtendsFalse()}function ExtendsObjectToObject(inferred,left,right){return ExtendsProperties( +inferred,left,right)}function ExtendsObject(inferred,left,right){return IsObject3(right)?ExtendsObjectToObject(inferred,left,right.properties):ExtendsRight(inferred,_Object_(left),right)}function ExtendsPromise(inferred,left,right){return IsPromise(right)?ExtendsLeft(inferred,left,right.item):ExtendsRight(inferred,_Promise_(left),right)}function ExtendsString(inferred,left,right){return IsString4(right)?ExtendsTrue(inferred):ExtendsRight(inferred,left,right)}function ExtendsSymbol(inferred,left,right){return IsSymbol3(right)?ExtendsTrue(inferred):ExtendsRight(inferred,left,right)}function ExtendsTemplateLiteral(inferred,left,right){const decoded=TemplateLiteralDecode(left);return ExtendsLeft(inferred,decoded,right)}function Inferrable(name,type){return memory_exports.Create({"~kind":"Inferrable"},{name,type},{})}function IsInferable(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"~kind")&&guard_exports.HasPropertyKey(value,"name")&&guard_exports.HasPropertyKey(value,"type")&&guard_exports.IsEqual(value["~kind"],"Inferrable")&&guard_exports.IsString(value.name)&&guard_exports.IsObject(value.type)}function TryRestInferable(type){return IsRest(type)?IsInfer(type.items)?IsArray3( +type.items.extends)?Inferrable(type.items.name,type.items.extends.items):IsUnknown(type.items.extends)?Inferrable(type.items.name,type.items.extends):void 0:Unreachable():void 0}function TryInferable(type){return IsInfer(type)?Inferrable(type.name,type.extends):void 0}function TryInferResults(rest,right,result=[]){return guard_exports.TakeLeft(rest,(head,tail)=>Match3(ExtendsLeft({},head,right),()=>TryInferResults(tail,right,[...result,head]),()=>void 0),()=>result)}function InferTupleResult(inferred,name,left,right){ +const results=TryInferResults(left,right);return guard_exports.IsArray(results)?ExtendsTrue(memory_exports.Assign(inferred,{[name]:Tuple(results)})):ExtendsFalse()}function InferUnionResult(inferred,name,left,right){const results=TryInferResults(left,right);return guard_exports.IsArray(results)?ExtendsTrue(memory_exports.Assign(inferred,{[name]:Union(results)})):ExtendsFalse()}function Reverse(types){return[...types].reverse()}function ApplyReverse(types,reversed){return reversed?Reverse(types):types}function Reversed(types){const first=types.length>0?types[0]:void 0;const inferrable=IsSchema(first)?TryRestInferable(first):void 0;return IsSchema(inferrable)}function ElementsCompare(inferred,reversed,left,leftRest,right,rightRest){return Match3(ExtendsLeft(inferred,left,right),checkInferred=>Elements(checkInferred,reversed,leftRest,rightRest),()=>ExtendsFalse())}function ElementsLeft(inferred,reversed,leftRest,right,rightRest){ +const inferable=TryRestInferable(right);return IsInferable(inferable)?InferTupleResult(inferred,inferable["name"],ApplyReverse(leftRest,reversed),inferable["type"]):guard_exports.TakeLeft(leftRest,(head,tail)=>ElementsCompare(inferred,reversed,head,tail,right,rightRest),()=>ExtendsFalse())}function ElementsRight(inferred,reversed,leftRest,rightRest){return guard_exports.TakeLeft(rightRest,(head,tail)=>ElementsLeft(inferred,reversed,leftRest,head,tail),()=>guard_exports.IsEqual(leftRest.length,0)? +ExtendsTrue(inferred):ExtendsFalse())}function Elements(inferred,reversed,leftRest,rightRest){return ElementsRight(inferred,reversed,leftRest,rightRest)}function ExtendsTupleToTuple(inferred,left,right){const instantiatedRight=InstantiateElements(inferred,{callstack:[]},right);const reversed=Reversed(instantiatedRight);return Elements(inferred,reversed,ApplyReverse(left,reversed),ApplyReverse(instantiatedRight,reversed))}function ExtendsTupleToArray(inferred,left,right){const inferrable=TryInferable( +right);return IsInferable(inferrable)?InferUnionResult(inferred,inferrable["name"],left,inferrable["type"]):guard_exports.TakeLeft(left,(head,tail)=>Match3(ExtendsLeft(inferred,head,right),inferred2=>ExtendsTupleToArray(inferred2,tail,right),()=>ExtendsFalse()),()=>ExtendsTrue(inferred))}function ExtendsTuple(inferred,left,right){const instantiatedLeft=InstantiateElements(inferred,{callstack:[]},left);return IsTuple(right)?ExtendsTupleToTuple(inferred,instantiatedLeft,right.items):IsArray3(right)? +ExtendsTupleToArray(inferred,instantiatedLeft,right.items):ExtendsRight(inferred,Tuple(instantiatedLeft),right)}function ExtendsUndefined(inferred,left,right){return IsVoid(right)?ExtendsTrue(inferred):IsUndefined3(right)?ExtendsTrue(inferred):ExtendsRight(inferred,left,right)}function ExtendsUnionSome(inferred,type,unionTypes){return guard_exports.TakeLeft(unionTypes,(head,tail)=>Match3(ExtendsLeft(inferred,type,head),inferred2=>ExtendsTrue(inferred2),()=>ExtendsUnionSome(inferred,type,tail)),()=>ExtendsFalse())}function ExtendsUnionLeft(inferred,left,right){return guard_exports.TakeLeft(left,(head,tail)=>Match3(ExtendsUnionSome(inferred,head,right),inferred2=>ExtendsUnionLeft(inferred2,tail,right),()=>ExtendsFalse()),()=>ExtendsTrue(inferred))}function ExtendsUnion2(inferred,left,right){ +const inferrable=TryInferable(right);return IsInferable(inferrable)?InferUnionResult(inferred,inferrable.name,left,inferrable.type):IsUnion(right)?ExtendsUnionLeft(inferred,left,right.anyOf):ExtendsUnionLeft(inferred,left,[right])}function ExtendsUnknown(inferred,left,right){return IsInfer(right)?ExtendsRight(inferred,left,right):IsAny(right)?ExtendsTrue(inferred):IsUnknown(right)?ExtendsTrue(inferred):ExtendsFalse()}function ExtendsVoid(inferred,left,right){return IsVoid(right)?ExtendsTrue(inferred):ExtendsRight(inferred,left,right)}function ExtendsLeft(inferred,left,right){return IsAny(left)?ExtendsAny(inferred,left,right):IsArray3(left)?ExtendsArray(inferred,left,left.items,right):IsAsyncIterator3(left)?ExtendsAsyncIterator(inferred,left.iteratorItems,right):IsBigInt3(left)?ExtendsBigInt(inferred,left,right):IsBoolean4(left)?ExtendsBoolean(inferred,left,right):IsConstructor3(left)?ExtendsConstructor(inferred,left.parameters,left.instanceType,right):IsEnum(left)?ExtendsEnum(inferred,left,right):IsFunction3(left)?ExtendsFunction( +inferred,left.parameters,left.returnType,right):IsInteger3(left)?ExtendsInteger(inferred,left,right):IsIntersect(left)?ExtendsIntersect(inferred,left.allOf,right):IsIterator3(left)?ExtendsIterator(inferred,left.iteratorItems,right):IsLiteral(left)?ExtendsLiteral(inferred,left,right):IsNever(left)?ExtendsNever(inferred,left,right):IsNull3(left)?ExtendsNull(inferred,left,right):IsNumber4(left)?ExtendsNumber(inferred,left,right):IsObject3(left)?ExtendsObject(inferred,left.properties,right):IsPromise( +left)?ExtendsPromise(inferred,left.item,right):IsString4(left)?ExtendsString(inferred,left,right):IsSymbol3(left)?ExtendsSymbol(inferred,left,right):IsTemplateLiteral(left)?ExtendsTemplateLiteral(inferred,left.pattern,right):IsTuple(left)?ExtendsTuple(inferred,left.items,right):IsUndefined3(left)?ExtendsUndefined(inferred,left,right):IsUnion(left)?ExtendsUnion2(inferred,left.anyOf,right):IsUnknown(left)?ExtendsUnknown(inferred,left,right):IsVoid(left)?ExtendsVoid(inferred,left,right):ExtendsFalse()}function InterfaceOperation(heritage,properties){const result=EvaluateIntersect([...heritage,_Object_(properties)]);return result}function InterfaceAction(heritage,properties,options){const result=CanInstantiate(heritage)?memory_exports.Update(InterfaceOperation(heritage,properties),{},options):InterfaceDeferred(heritage,properties,options);return result}function InterfaceInstantiate(context,state2,heritage,properties,options){const instantiatedHeritage=InstantiateTypes(context,state2,heritage); +const instantiatedProperties=InstantiateProperties(context,state2,properties);return InterfaceAction(instantiatedHeritage,instantiatedProperties,options)}function InterfaceDeferred(heritage,properties,options={}){return Deferred("Interface",[heritage,properties],options)}function IsInterfaceDeferred(value){return IsSchema(value)&&guard_exports.HasPropertyKey(value,"action")&&guard_exports.IsEqual(value.action,"Interface")}function FromRef(stack,context,ref){return stack.includes(ref)?true:FromType3([...stack,ref],context,context[ref])}function FromProperties(stack,context,properties){const types=PropertyValues(properties);return FromTypes2(stack,context,types)}function FromTypes2(stack,context,types){return guard_exports.TakeLeft(types,(left,right)=>FromType3(stack,context,left)?true:FromTypes2(stack,context,right),()=>false)}function FromType3(stack,context,type){return IsRef(type)?FromRef(stack,context,type.$ref): +IsArray3(type)?FromType3(stack,context,type.items):IsAsyncIterator3(type)?FromType3(stack,context,type.iteratorItems):IsConstructor3(type)?FromTypes2(stack,context,[...type.parameters,type.instanceType]):IsFunction3(type)?FromTypes2(stack,context,[...type.parameters,type.returnType]):IsInterfaceDeferred(type)?FromProperties(stack,context,type.parameters[1]):IsIntersect(type)?FromTypes2(stack,context,type.allOf):IsIterator3(type)?FromType3(stack,context,type.iteratorItems):IsObject3(type)?FromProperties( +stack,context,type.properties):IsPromise(type)?FromType3(stack,context,type.item):IsUnion(type)?FromTypes2(stack,context,type.anyOf):IsTuple(type)?FromTypes2(stack,context,type.items):IsRecord(type)?FromType3(stack,context,RecordValue(type)):false}function CyclicCheck(stack,context,type){const result=FromType3(stack,context,type);return result}function ResolveCandidateKeys(context,keys){return keys.reduce((result,left)=>{return left in context?CyclicCheck([left],context,context[left])?[...result,left]:result:Unreachable()},[])}function CyclicCandidates(context){const keys=PropertyKeys(context);const result=ResolveCandidateKeys(context,keys);return result}function FromRef2(context,ref,result){return result.includes(ref)?result:ref in context?FromType4(context,context[ref],[...result,ref]):Unreachable()}function FromProperties2(context,properties,result){const types=PropertyValues(properties);return FromTypes3(context,types,result)}function FromTypes3(context,types,result){return types.reduce((result2,left)=>{return FromType4(context,left,result2)},result)}function FromType4(context,type,result){return IsRef(type)?FromRef2(context,type.$ref,result): +IsArray3(type)?FromType4(context,type.items,result):IsAsyncIterator3(type)?FromType4(context,type.iteratorItems,result):IsConstructor3(type)?FromTypes3(context,[...type.parameters,type.instanceType],result):IsFunction3(type)?FromTypes3(context,[...type.parameters,type.returnType],result):IsInterfaceDeferred(type)?FromProperties2(context,type.parameters[1],result):IsIntersect(type)?FromTypes3(context,type.allOf,result):IsIterator3(type)?FromType4(context,type.iteratorItems,result):IsObject3(type)? +FromProperties2(context,type.properties,result):IsPromise(type)?FromType4(context,type.item,result):IsUnion(type)?FromTypes3(context,type.anyOf,result):IsTuple(type)?FromTypes3(context,type.items,result):IsRecord(type)?FromType4(context,RecordValue(type),result):result}function CyclicDependencies(context,key,type){const result=FromType4(context,type,[key]);return result}function FromRef3(_ref){return Any()}function FromProperties3(properties){return guard_exports.Keys(properties).reduce((result,key)=>{return{...result,[key]:FromType5(properties[key])}},{})}function FromTypes4(types){return types.reduce((result,left)=>{return[...result,FromType5(left)]},[])}function FromType5(type){return IsRef(type)?FromRef3(type.$ref):IsArray3(type)?_Array_(FromType5(type.items),ArrayOptions(type)):IsAsyncIterator3(type)?AsyncIterator(FromType5(type.iteratorItems)):IsConstructor3( +type)?Constructor(FromTypes4(type.parameters),FromType5(type.instanceType)):IsFunction3(type)?_Function_(FromTypes4(type.parameters),FromType5(type.returnType)):IsIntersect(type)?Intersect(FromTypes4(type.allOf)):IsIterator3(type)?Iterator(FromType5(type.iteratorItems)):IsObject3(type)?_Object_(FromProperties3(type.properties)):IsPromise(type)?_Promise_(FromType5(type.item)):IsRecord(type)?Record(RecordKey(type),FromType5(RecordValue(type))):IsUnion(type)?Union(FromTypes4(type.anyOf)):IsTuple(type)? +Tuple(FromTypes4(type.items)):type}function CyclicAnyFromParameters(defs,ref){return ref in defs?FromType5(defs[ref]):Unknown()}function CyclicExtends(type){return CyclicAnyFromParameters(type.$defs,type.$ref)}function CyclicInterface(context,heritage,properties){const instantiatedHeritage=InstantiateTypes(context,{callstack:[]},heritage);const instantiatedProperties=InstantiateProperties({},{callstack:[]},properties);const evaluatedInterface=EvaluateIntersect([...instantiatedHeritage,_Object_(instantiatedProperties)]);return evaluatedInterface}function CyclicDefinitions(context,dependencies){const keys=guard_exports.Keys(context).filter(key=>dependencies.includes(key));return keys.reduce((result,key)=>{ +const type=context[key];const instantiatedType=IsInterfaceDeferred(type)?CyclicInterface(context,type.parameters[0],type.parameters[1]):type;return{...result,[key]:instantiatedType}},{})}function InstantiateCyclic(context,ref,type){const dependencies=CyclicDependencies(context,ref,type);const definitions=CyclicDefinitions(context,dependencies);const result=Cyclic(definitions,ref);return result}function Resolve(defs,ref){return ref in defs?IsRef(defs[ref])?Resolve(defs,defs[ref].$ref):defs[ref]:Never()}function CyclicTarget(defs,ref){const result=Resolve(defs,ref);return result}function Canonical(type){return IsCyclic(type)?CyclicExtends(type):IsUnsafe(type)?Unknown():type}function Extends(inferred,left,right){const canonicalLeft=Canonical(left);const canonicalRight=Canonical(right);return ExtendsLeft(inferred,canonicalLeft,canonicalRight)}var ResultEqual="equal";var ResultDisjoint="disjoint";var ResultLeftInside="left-inside";var ResultRightInside="right-inside";function Compare(left,right){const extendsCheck=[IsUnknown(left)?result_exports.ExtendsFalse():Extends({},left,right),IsUnknown(left)?result_exports.ExtendsTrue({}):Extends({},right,left)];return result_exports.IsExtendsTrueLike(extendsCheck[0])&&result_exports.IsExtendsTrueLike(extendsCheck[1])?ResultEqual:result_exports.IsExtendsTrueLike(extendsCheck[0])&&result_exports. +IsExtendsFalse(extendsCheck[1])?ResultLeftInside:result_exports.IsExtendsFalse(extendsCheck[0])&&result_exports.IsExtendsTrueLike(extendsCheck[1])?ResultRightInside:ResultDisjoint}function BroadFilter(type,types){return types.filter(left=>{return Compare(type,left)===ResultRightInside?false:true})}function IsBroadestType(type,types){const result=types.some(left=>{const result2=Compare(type,left);return guard_exports.IsEqual(result2,ResultLeftInside)||guard_exports.IsEqual(result2,ResultEqual)});return guard_exports.IsEqual(result,false)}function BroadenType(type,types){const evaluated=EvaluateType(type);return IsAny(evaluated)?[evaluated]:IsBroadestType(evaluated,types)?[ +...BroadFilter(evaluated,types),evaluated]:types}function BroadenTypes(types){return types.reduce((result,left)=>{return IsObject3(left)?[...result,left]:IsNever(left)?result:BroadenType(left,result)},[])}function Broaden(types){const broadened=BroadenTypes(types);const flattened=Flatten(broadened);const result=flattened.length===0?Never():flattened.length===1?flattened[0]:Union(flattened);return result}function EvaluateAction(type,options){const result=memory_exports.Update(EvaluateType(type),{},options);return result}function EvaluateInstantiate(context,state2,type,options){const instantiatedType=InstantiateType(context,state2,type);return EvaluateAction(instantiatedType,options)}function CollectDistributionNames(expression,result=[]){return IsDeferred(expression)&&guard_exports.IsEqual(expression.action,"Conditional")?IsRef(expression.parameters[0])?CollectDistributionNames(expression.parameters[2],CollectDistributionNames(expression.parameters[3],[...result,expression.parameters[0]["$ref"]])):CollectDistributionNames(expression.parameters[2],CollectDistributionNames(expression.parameters[3],result)):IsDeferred(expression)&&guard_exports.IsEqual(expression.action,"Mappe\ +d")?IsDeferred(expression.parameters[1])&&guard_exports.IsEqual(expression.parameters[1].action,"KeyOf")&&IsRef(expression.parameters[1].parameters[0])?[...result,expression.parameters[1].parameters[0]["$ref"]]:result:result}function BuildDistributionArray(parameters,names){return parameters.reduce((result,left)=>[...result,names.includes(left.name)],[])}function ZipDistributionArray(arguments_,distributionArray,result=[]){return guard_exports.TakeLeft(arguments_,(argumentLeft,argumentRight)=>guard_exports. +TakeLeft(distributionArray,(booleanLeft,booleanRight)=>ZipDistributionArray(argumentRight,booleanRight,[...result,[booleanLeft,argumentLeft]]),()=>result),()=>result)}function Expand(type){return IsUnion(type)?[...type.anyOf]:[type]}function Append(current,type){return current.reduce((result,left)=>[...result,[...left,type]],[])}function Cross(current,variants){return variants.reduce((result,left)=>{return[...result,...Append(current,left)]},[])}function Distribute2(zipped){return zipped.reduce( +(result,left)=>{return guard_exports.IsEqual(left[0],true)?Cross(result,Expand(left[1])):Cross(result,[left[1]])},[[]])}function DistributeArguments(parameters,arguments_,expression){const distributionNames=CollectDistributionNames(expression);const distributionArray=BuildDistributionArray(parameters,distributionNames);const zippedArguments=ZipDistributionArray(arguments_,distributionArray);return IsDeferred(expression)&&guard_exports.IsEqual(expression.action,"Conditional")?Distribute2(zippedArguments): +IsDeferred(expression)&&guard_exports.IsEqual(expression.action,"Mapped")?Distribute2(zippedArguments):[arguments_]}function FromNotResolvable(){return["(not-resolvable)",Never()]}function FromNotGeneric(){return["(not-generic)",Never()]}function FromGeneric(name,parameters,expression){return[name,Generic(parameters,expression)]}function FromRef4(context,ref,arguments_){return ref in context?FromType6(context,ref,context[ref],arguments_):FromNotResolvable()}function FromType6(context,name,target,arguments_){return IsGeneric(target)?FromGeneric(name,target.parameters,target.expression):IsRef(target)?FromRef4(context, +target.$ref,arguments_):FromNotGeneric()}function ResolveTarget(context,target,arguments_){return FromType6(context,"(anonymous)",target,arguments_)}function AssertArgumentExtends(name,type,extends_){if(IsInfer(type)||IsCall(type)||result_exports.IsExtendsTrueLike(Extends({},type,extends_)))return;const cause={parameter:name,expect:extends_,actual:type};throw new Error(`Argument for parameter ${name} does not satisfy constraint`,{cause})}function BindArgument(context,state2,name,extends_,type){const instantiatedArgument=InstantiateType(context,state2,type);AssertArgumentExtends(name,instantiatedArgument,extends_);return memory_exports.Assign( +context,{[name]:instantiatedArgument})}function BindArguments(context,state2,parameterLeft,parameterRight,arguments_){const instantiatedExtends=InstantiateType(context,state2,parameterLeft.extends);const instantiatedEquals=InstantiateType(context,state2,parameterLeft.equals);return guard_exports.TakeLeft(arguments_,(left,right)=>BindParameters(BindArgument(context,state2,parameterLeft["name"],instantiatedExtends,left),state2,parameterRight,right),()=>BindParameters(BindArgument(context,state2,parameterLeft["\ +name"],instantiatedExtends,instantiatedEquals),state2,parameterRight,[]))}function BindParameters(context,state2,parameters,arguments_){return guard_exports.TakeLeft(parameters,(left,right)=>BindArguments(context,state2,left,right,arguments_),()=>context)}function ResolveArgumentsContext(context,state2,parameters,arguments_){return BindParameters(context,state2,parameters,arguments_)}function Peek(state2){const result=guard_exports.IsGreaterThan(state2.callstack.length,0)?state2.callstack[state2.callstack.length-1]:"";return result}function IsTailCall(state2,name){const result=guard_exports.IsEqual(Peek(state2),name);return result}function CallDispatch(context,state2,target,parameters,expression,arguments_){const argumentsContext=ResolveArgumentsContext(context,state2,parameters,arguments_);const returnType=InstantiateType(argumentsContext,{callstack:[...state2.callstack,target. +$ref]},expression);return InstantiateType(context,state2,returnType)}function CallDistributed(context,state2,target,parameters,expression,distributedArguments){return distributedArguments.reduce((result,arguments_)=>[...result,CallDispatch(context,state2,target,parameters,expression,arguments_)],[])}function CallImmediate(context,state2,target,parameters,expression,arguments_){const distributedArguments=DistributeArguments(parameters,arguments_,expression);const returnTypes=CallDistributed(context, +state2,target,parameters,expression,distributedArguments);const result=guard_exports.IsEqual(returnTypes.length,1)?returnTypes[0]:EvaluateUnion(returnTypes);return result}function CallInstantiate(context,state2,target,arguments_){const instantiatedArguments=InstantiateTypes(context,state2,arguments_);const resolved=ResolveTarget(context,target,arguments_);const name=resolved[0];const type=resolved[1];const result=IsGeneric(type)?IsTailCall(state2,name)?CallConstruct(Ref(name),instantiatedArguments): +CallImmediate(context,state2,Ref(name),type.parameters,type.expression,instantiatedArguments):CallConstruct(target,instantiatedArguments);return result}function CallConstruct(target,arguments_){return memory_exports.Create({["~kind"]:"Call"},{target,arguments:arguments_},{})}function IsCall(value){return IsKind(value,"Call")}function ApplyMapping(mapping,value){return mapping(value)}function FromLiteral3(mapping,value){return guard_exports.IsString(value)?Literal(ApplyMapping(mapping,value)):Literal(value)}function FromTemplateLiteral(mapping,pattern){const decoded=TemplateLiteralDecode(pattern);const result=FromType7(mapping,decoded);return result}function FromUnion2(mapping,types){const result=types.map(type=>FromType7(mapping,type));return Union(result)}function FromType7(mapping,type){return IsLiteral(type)?FromLiteral3(mapping,type.const):IsTemplateLiteral(type)?FromTemplateLiteral(mapping,type.pattern):IsUnion(type)?FromUnion2(mapping,type.anyOf):type}function CapitalizeDeferred(type,options={}){return Deferred("Capitalize",[type],options)}function LowercaseDeferred(type,options={}){return Deferred("Lowercase",[type],options)}function UncapitalizeDeferred(type,options={}){return Deferred("Uncapitalize",[type],options)}function UppercaseDeferred(type,options={}){return Deferred("Uppercase",[type],options)}var CapitalizeMapping=input=>input[0].toUpperCase()+input.slice(1);var LowercaseMapping=input=>input.toLowerCase();var UncapitalizeMapping=input=>input[0].toLowerCase()+input.slice(1);var UppercaseMapping=input=>input.toUpperCase();function CapitalizeAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(FromType7(CapitalizeMapping,type),{},options):CapitalizeDeferred(type,options);return result}function LowercaseAction(type,options){const result=CanInstantiate([type])?memory_exports. +Update(FromType7(LowercaseMapping,type),{},options):LowercaseDeferred(type,options);return result}function UncapitalizeAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(FromType7(UncapitalizeMapping,type),{},options):UncapitalizeDeferred(type,options);return result}function UppercaseAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(FromType7(UppercaseMapping,type),{},options):UppercaseDeferred(type,options);return result}function CapitalizeInstantiate(context,state2,type,options){ +const instantiatedType=InstantiateType(context,state2,type);return CapitalizeAction(instantiatedType,options)}function LowercaseInstantiate(context,state2,type,options){const instantiatedType=InstantiateType(context,state2,type);return LowercaseAction(instantiatedType,options)}function UncapitalizeInstantiate(context,state2,type,options){const instantiatedType=InstantiateType(context,state2,type);return UncapitalizeAction(instantiatedType,options)}function UppercaseInstantiate(context,state2,type,options){ +const instantiatedType=InstantiateType(context,state2,type);return UppercaseAction(instantiatedType,options)}function ConditionalDeferred(left,right,true_,false_,options={}){return Deferred("Conditional",[left,right,true_,false_],options)}function ConditionalOperation(context,state2,left,right,true_,false_){const extendsResult=Extends(context,left,right);return result_exports.IsExtendsUnion(extendsResult)?Union([InstantiateType(extendsResult.inferred,state2,true_),InstantiateType(context,state2,false_)]):result_exports.IsExtendsTrue(extendsResult)?InstantiateType(extendsResult.inferred,state2,true_):InstantiateType(context,state2,false_)}function ConditionalAction(context,state2,left,right,true_,false_,options){const result=CanInstantiate( +[left,right])?memory_exports.Update(ConditionalOperation(context,state2,left,right,true_,false_),{},options):ConditionalDeferred(left,right,true_,false_,options);return result}function ConditionalInstantiate(context,state2,left,right,true_,false_,options){const instantiatedLeft=InstantiateType(context,state2,left);const instantiatedRight=InstantiateType(context,state2,right);return ConditionalAction(context,state2,instantiatedLeft,instantiatedRight,true_,false_,options)}function ConstructorParametersDeferred(type,options={}){return Deferred("ConstructorParameters",[type],options)}function ConstructorParametersOperation(type){const parameters=IsConstructor3(type)?type["parameters"]:[];const instantiatedParameters=InstantiateElements({},{callstack:[]},parameters);const result=Tuple(instantiatedParameters);return result}function ConstructorParametersAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(ConstructorParametersOperation(type),{},options):ConstructorParametersDeferred(type,options);return result}function ConstructorParametersInstantiate(context,state2,type,options){ +const instantiatedType=InstantiateType(context,state2,type);return ConstructorParametersAction(instantiatedType,options)}function ExcludeDeferred(left,right,options={}){return Deferred("Exclude",[left,right],options)}function ExcludeUnionLeft(types,right){return types.reduce((result,head)=>{return[...result,...ExcludeTypeLeft(head,right)]},[])}function ExcludeTypeLeft(left,right){const check=Extends({},left,right);const result=result_exports.IsExtendsTrueLike(check)?[]:[left];return result}function ExcludeOperation(left,right){const remaining=IsEnum(left)?ExcludeUnionLeft(EnumValuesToVariants(left.enum),right):IsUnion(left)?ExcludeUnionLeft(Flatten(left.anyOf),right):ExcludeTypeLeft(left,right);const result=EvaluateUnion( +remaining);return result}function ExcludeAction(left,right,options){const result=CanInstantiate([left,right])?memory_exports.Update(ExcludeOperation(left,right),{},options):ExcludeDeferred(left,right,options);return result}function ExcludeInstantiate(context,state2,left,right,options){const instantiatedLeft=InstantiateType(context,state2,left);const instantiatedRight=InstantiateType(context,state2,right);return ExcludeAction(instantiatedLeft,instantiatedRight,options)}function ExtractDeferred(left,right,options={}){return Deferred("Extract",[left,right],options)}function ExtractUnionLeft(types,right){return types.reduce((result,head)=>{return[...result,...ExtractTypeLeft(head,right)]},[])}function ExtractTypeLeft(left,right){const check=Extends({},left,right);const result=result_exports.IsExtendsTrueLike(check)?[left]:[];return result}function ExtractOperation(left,right){const remaining=IsEnum(left)?ExtractUnionLeft(EnumValuesToVariants(left.enum),right):IsUnion(left)?ExtractUnionLeft(Flatten(left.anyOf),right):ExtractTypeLeft(left,right);const result=EvaluateUnion( +remaining);return result}function ExtractAction(left,right,options){const result=CanInstantiate([left,right])?memory_exports.Update(ExtractOperation(left,right),{},options):ExtractDeferred(left,right,options);return result}function ExtractInstantiate(context,state2,left,right,options){const instantiatedLeft=InstantiateType(context,state2,left);const instantiatedRight=InstantiateType(context,state2,right);return ExtractAction(instantiatedLeft,instantiatedRight,options)}function IndexDeferred(type,indexer,options={}){return Deferred("Index",[type,indexer],options)}function FromCyclic(defs,ref){const target=CyclicTarget(defs,ref);const result=FromType8(target);return result}function CollapseIntersectProperties(left,right){const leftKeys=guard_exports.Keys(left).filter(key=>!guard_exports.HasPropertyKey(right,key));const rightKeys=guard_exports.Keys(right).filter(key=>!guard_exports.HasPropertyKey(left,key));const sharedKeys=guard_exports.Keys(left).filter(key=>guard_exports.HasPropertyKey(right,key));const leftProperties=leftKeys.reduce((result,key)=>({...result,[key]:left[key]}),{});const rightProperties=rightKeys.reduce((result,key)=>({...result,[key]:right[key]}), +{});const sharedProperties=sharedKeys.reduce((result,key)=>({...result,[key]:EvaluateIntersect([left[key],right[key]])}),{});const unique=memory_exports.Assign(leftProperties,rightProperties);const shared=memory_exports.Assign(unique,sharedProperties);return shared}function FromIntersect(types){return types.reduce((result,left)=>{return CollapseIntersectProperties(result,FromType8(left))},{})}function FromObject3(properties){return properties}function FromTuple(types){const object=TupleToObject(Tuple(types));const result=FromType8(object);return result}function CollapseUnionProperties(left,right){const sharedKeys=guard_exports.Keys(left).filter(key=>key in right);const result=sharedKeys.reduce((result2,key)=>{return{...result2,[key]:EvaluateUnion([left[key],right[key]])}},{});return result}function ReduceVariants(types,result){return guard_exports.TakeLeft(types,(left,right)=>ReduceVariants(right,CollapseUnionProperties(result,FromType8(left))),()=>result)}function FromUnion3(types){return guard_exports.TakeLeft(types,(left,right)=>ReduceVariants( +right,FromType8(left)),()=>Unreachable())}function FromType8(type){return IsCyclic(type)?FromCyclic(type.$defs,type.$ref):IsIntersect(type)?FromIntersect(type.allOf):IsUnion(type)?FromUnion3(type.anyOf):IsTuple(type)?FromTuple(type.items):IsObject3(type)?FromObject3(type.properties):{}}function CollapseToObject(type){const properties=FromType8(type);const result=_Object_(properties);return result}var integerKeyPattern=new RegExp("^(?:0|[1-9][0-9]*)$");function ConvertToIntegerKey(value){const normal=`${value}`;return integerKeyPattern.test(normal)?parseInt(normal):value}function NormalizeLiteral(value){return Literal(ConvertToIntegerKey(value))}function NormalizeIndexerTypes(types){return types.map(type=>NormalizeIndexer(type))}function NormalizeIndexer(type){return IsIntersect(type)?Intersect(NormalizeIndexerTypes(type.allOf)):IsUnion(type)?Union(NormalizeIndexerTypes(type.anyOf)):IsLiteral(type)?NormalizeLiteral(type.const):type}function FromArray3(type,indexer){const normalizedIndexer=NormalizeIndexer(indexer);const check=Extends({},normalizedIndexer,Number2()); +const result=result_exports.IsExtendsTrueLike(check)?type:IsLiteral(indexer)&&guard_exports.IsEqual(indexer.const,"length")?Number2():Never();return result}function FromCyclic2(defs,ref){const target=CyclicTarget(defs,ref);const result=FromType9(target);return result}function FromUnion4(types){return types.reduce((result,left)=>{return[...result,...FromType9(left)]},[])}function FromEnum(values){const variants=EnumValuesToVariants(values);const result=FromUnion4(variants);return result}function FromIntersect2(types){const evaluated=EvaluateIntersect(types);const result=FromType9(evaluated);return result}function FromLiteral4(value){const result=[`${value}`];return result}function FromTemplateLiteral2(pattern){const decoded=TemplateLiteralDecode(pattern);const result=FromType9(decoded);return result}function FromType9(type){return IsCyclic(type)?FromCyclic2(type.$defs,type.$ref):IsEnum(type)?FromEnum(type.enum):IsIntersect(type)?FromIntersect2(type.allOf):IsLiteral(type)?FromLiteral4(type.const):IsTemplateLiteral(type)?FromTemplateLiteral2(type.pattern):IsUnion(type)?FromUnion4(type.anyOf):[]}function ToIndexableKeys(type){const result=FromType9(type);return result}function FromTypes5(properties,types){return types.map(type=>FromType10(properties,type))}function FromType10(properties,type){return IsArray3(type)?_Array_(FromType10(properties,type.items)):IsAsyncIterator3(type)?AsyncIterator(FromType10(properties,type.iteratorItems)):IsConstructor3(type)?Constructor(FromTypes5(properties,type.parameters),FromType10(properties,type.instanceType)):IsFunction3(type)?_Function_(FromTypes5(properties,type.parameters),FromType10(properties,type.returnType)):IsIterator3( +type)?Iterator(FromType10(properties,type.iteratorItems)):IsPromise(type)?_Promise_(FromType10(properties,type.item)):IsTuple(type)?Tuple(FromTypes5(properties,type.items)):IsUnion(type)?Union(FromTypes5(properties,type.anyOf)):IsIntersect(type)?Intersect(FromTypes5(properties,type.allOf)):IsThis(type)?_Object_(properties):type}function ExpandThis(properties,type){const result=FromType10(properties,type);return result}function IndexProperty(properties,key){const selectedType=key in properties?properties[key]:Never();const result=ExpandThis(properties,selectedType);return result}function IndexProperties(properties,keys){return keys.reduce((result,left)=>{return[...result,IndexProperty(properties,left)]},[])}function FromIndexer(properties,indexer){const keys=ToIndexableKeys(indexer);const variants=IndexProperties(properties,keys);const result=EvaluateUnion(variants);return result}var NumericKeyPattern=new RegExp( +IntegerKey);function NumericKeys(keys){const result=keys.filter(key=>NumericKeyPattern.test(key));return result}function FromIndexerNumber(properties){const keys=PropertyKeys(properties);const numericKeys=NumericKeys(keys);const variants=IndexProperties(properties,numericKeys);const result=EvaluateUnion(variants);return result}function FromObject4(properties,indexer){const result=IsNumber4(indexer)?FromIndexerNumber(properties):FromIndexer(properties,indexer);return result}function ConvertLiteral(value){return Literal(ConvertToIntegerKey(value))}function ArrayIndexerTypes(types){return types.map(type=>FormatArrayIndexer(type))}function FormatArrayIndexer(type){return IsIntersect(type)?Intersect(ArrayIndexerTypes(type.allOf)):IsUnion(type)?Union(ArrayIndexerTypes(type.anyOf)):IsLiteral(type)?ConvertLiteral(type.const):type}function IndexElementsWithIndexer(types,indexer){return types.reduceRight((result,right,index2)=>{const check=Extends({},Literal(index2),indexer);return result_exports.IsExtendsTrueLike(check)?[right,...result]:result},[])}function FromTupleWithIndexer(types,indexer){const formattedArrayIndexer=FormatArrayIndexer(indexer);const elements=IndexElementsWithIndexer(types,formattedArrayIndexer);return EvaluateUnionFast(elements)}function FromTupleWithoutIndexer(types){return EvaluateUnionFast(types)} +function FromTuple2(types,indexer){return IsLiteral(indexer)&&guard_exports.IsEqual(indexer.const,"length")?Literal(types.length):IsNumber4(indexer)||IsInteger3(indexer)?FromTupleWithoutIndexer(types):FromTupleWithIndexer(types,indexer)}function FromType11(type,indexer){return IsArray3(type)?FromArray3(type.items,indexer):IsObject3(type)?FromObject4(type.properties,indexer):IsTuple(type)?FromTuple2(type.items,indexer):Never()}function NormalizeType(type){const result=IsCyclic(type)||IsIntersect(type)||IsUnion(type)?CollapseToObject(type):type;return result}function IndexAction(type,indexer,options){const result=CanInstantiate([type,indexer])?memory_exports.Update(FromType11(NormalizeType(type),indexer),{},options):IndexDeferred(type,indexer,options);return result}function IndexInstantiate(context,state2,type,indexer,options){const instantiatedType=InstantiateType(context,state2,type);const instantiatedIndexer=InstantiateType( +context,state2,indexer);return IndexAction(instantiatedType,instantiatedIndexer,options)}function InstanceTypeDeferred(type,options={}){return Deferred("InstanceType",[type],options)}function InstanceTypeOperation(type){return IsConstructor3(type)?type["instanceType"]:Never()}function InstanceTypeAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(InstanceTypeOperation(type),{},options):InstanceTypeDeferred(type,options);return result}function InstanceTypeInstantiate(context,state2,type,options={}){const instantiatedType=InstantiateType(context,state2,type);return InstanceTypeAction(instantiatedType,options)}function KeyOfDeferred(type,options={}){return Deferred("KeyOf",[type],options)}function FromAny(){return Union([Number2(),String2(),Symbol2()])}function FromArray4(_type){return Number2()}function FromPropertyKeys(keys){const result=keys.reduce((result2,left)=>{return IsLiteralValue(left)?[...result2,Literal(ConvertToIntegerKey(left))]:Unreachable()},[]);return result}function FromObject5(properties){const propertyKeys=guard_exports.Keys(properties);const variants=FromPropertyKeys(propertyKeys);const result=EvaluateUnionFast(variants);return result}function FromRecord(type){return RecordKey(type)}function FromTuple3(types){const result=types.map((_,index2)=>Literal(index2));return EvaluateUnionFast(result)}function FromType12(type){return IsAny(type)?FromAny():IsArray3(type)?FromArray4(type.items):IsObject3(type)?FromObject5(type.properties):IsRecord(type)?FromRecord(type):IsTuple(type)?FromTuple3(type.items):Never()}function NormalizeType2(type){const result=IsCyclic(type)||IsIntersect(type)||IsUnion(type)?CollapseToObject(type):type;return result}function KeyOfAction(type,options){return CanInstantiate([type])?memory_exports.Update(FromType12(NormalizeType2(type)),{},options):KeyOfDeferred(type,options)}function KeyOfInstantiate(context,state2,type,options){const instantiatedType=InstantiateType(context,state2,type);return KeyOfAction(instantiatedType,options)}function MappedDeferred(identifier,type,as,property,options={}){return Deferred("Mapped",[identifier,type,as,property],options)}function FromTemplateLiteral3(pattern){const decoded=TemplateLiteralDecode(pattern);const result=FromType13(decoded);return result}function FromUnion5(types){return types.reduce((result,left)=>{return[...result,...FromType13(left)]},[])}function FromLiteral5(value){const result=guard_exports.IsNumber(value)?[Literal(`${value}`)]:[Literal(value)];return result}function FromType13(type){const result=IsEnum(type)?FromUnion5(EnumValuesToVariants(type.enum)):IsLiteral(type)?FromLiteral5(type.const):IsTemplateLiteral( +type)?FromTemplateLiteral3(type.pattern):IsUnion(type)?FromUnion5(type.anyOf):[type];return result}function MappedVariants(type){const result=FromType13(type);return result}function CanonicalAs(instantiatedAs){const result=IsTemplateLiteral(instantiatedAs)?TemplateLiteralDecode(instantiatedAs.pattern):instantiatedAs;return result}function MappedVariant(context,state2,identifier,variant,as,property){const variantContext=memory_exports.Assign(context,{[identifier["name"]]:variant});const instantiatedAs=InstantiateType(variantContext,state2,as);const canonicalAs=CanonicalAs(instantiatedAs);const instantiatedProperty=InstantiateType(variantContext,state2,property);return IsLiteralNumber( +canonicalAs)||IsLiteralString(canonicalAs)?{[canonicalAs.const]:instantiatedProperty}:{}}function MappedProperties(context,state2,identifier,variants,as,property){return variants.reduce((result,left)=>{return[...result,MappedVariant(context,state2,identifier,left,as,property)]},[])}function MappedObjects(properties){return properties.reduce((result,left)=>{return[...result,_Object_(left)]},[])}function MappedOperation(context,state2,identifier,type,as,property){const variants=MappedVariants(type); +const mappedProperties=MappedProperties(context,state2,identifier,variants,as,property);const mappedObjects=MappedObjects(mappedProperties);const result=EvaluateIntersect(mappedObjects);return result}function MappedAction(context,state2,identifier,type,as,property,options){const result=CanInstantiate([type])?memory_exports.Update(MappedOperation(context,state2,identifier,type,as,property),{},options):MappedDeferred(identifier,type,as,property,options);return result}function MappedInstantiate(context,state2,identifier,type,as,property,options){const instantiatedType=InstantiateType(context,state2,type);return MappedAction(context,state2,identifier,instantiatedType,as,property,options)}function InstantiateCyclics(context,cyclicKeys){const keys=guard_exports.Keys(context).filter(key=>cyclicKeys.includes(key));return keys.reduce((result,key)=>{return{...result,[key]:InstantiateCyclic(context,key,context[key])}},{})}function InstantiateNonCyclics(context,cyclicKeys){const keys=guard_exports.Keys(context).filter(key=>!cyclicKeys.includes(key));return keys.reduce((result,key)=>{return{...result,[key]:InstantiateType(context,{callstack:[]},context[key])}},{})}function InstantiateModule(context,options){ +const cyclicCandidates=CyclicCandidates(context);const instantiatedCyclics=InstantiateCyclics(context,cyclicCandidates);const instantiatedNonCyclics=InstantiateNonCyclics(context,cyclicCandidates);const instantiatedModule={...instantiatedCyclics,...instantiatedNonCyclics};return memory_exports.Update(instantiatedModule,{},options)}function ModuleInstantiate(context,_state,properties,options){const moduleContext=memory_exports.Assign(context,properties);const instantiatedModule=InstantiateModule( +moduleContext,options);return instantiatedModule}function NonNullableDeferred(type,options={}){return Deferred("NonNullable",[type],options)}function NonNullableOperation(type){const excluded=Union([Null(),Undefined()]);return ExcludeAction(type,excluded,{})}function NonNullableAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(NonNullableOperation(type),{},options):NonNullableDeferred(type,options);return result}function NonNullableInstantiate(context,state2,type,options){const instantiatedType=InstantiateType(context,state2,type);return NonNullableAction(instantiatedType,options)}function OmitDeferred(type,indexer,options={}){return Deferred("Omit",[type,indexer],options)}function ToIndexable(type){const collapsed=CollapseToObject(type);const result=IsObject3(collapsed)?collapsed.properties:Unreachable();return result}function FromKeys(properties,keys){const result=guard_exports.Keys(properties).reduce((result2,key)=>{return keys.includes(key)?result2:{...result2,[key]:properties[key]}},{});return result}function FromType14(type,indexer){const indexable=ToIndexable(type);const indexableKeys=ToIndexableKeys(indexer);const omitted=FromKeys(indexable,indexableKeys);const result=_Object_(omitted);return result}function OmitAction(type,indexer,options){const result=CanInstantiate([type,indexer])?memory_exports.Update(FromType14(type,indexer),{},options):OmitDeferred(type,indexer,options);return result}function OmitInstantiate(context,state2,type,indexer,options){const instantiatedType=InstantiateType(context,state2,type);const instantiatedIndexer=InstantiateType(context,state2,indexer);return OmitAction(instantiatedType,instantiatedIndexer,options)}function OptionsDeferred(type,options){return Deferred("Options",[type,options],{})}function Options(type,options){return OptionsAction(type,options)}function OptionsAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(type,{},options):OptionsDeferred(type,options);return result}function OptionsInstantiate(context,state2,type,options){const instaniatedType=InstantiateType(context,state2,type);return OptionsAction(instaniatedType,options)}function ParametersDeferred(type,options={}){return Deferred("Parameters",[type],options)}function ParametersOperation(type){const parameters=IsFunction3(type)?type["parameters"]:[];const instantiatedParameters=InstantiateElements({},{callstack:[]},parameters);const result=Tuple(instantiatedParameters);return result}function ParametersAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(ParametersOperation(type),{},options):ParametersDeferred(type,options);return result}function ParametersInstantiate(context,state2,type,options){const instantiatedType=InstantiateType( +context,state2,type);return ParametersAction(instantiatedType,options)}function PartialDeferred(type,options={}){return Deferred("Partial",[type],options)}function FromCyclic3(defs,ref){const target=CyclicTarget(defs,ref);const partial=FromType15(target);const result=Cyclic(memory_exports.Assign(defs,{[ref]:partial}),ref);return result}function FromIntersect3(types){const result=types.map(type=>FromType15(type));return EvaluateIntersect(result)}function FromUnion6(types){const result=types.map(type=>FromType15(type));return Union(result)}function FromObject6(properties){const mapped=guard_exports.Keys(properties).reduce((result2,left)=>{return{...result2,[left]:Optional(properties[left])}},{});const result=_Object_(mapped);return result}function FromType15(type){return IsCyclic(type)?FromCyclic3(type.$defs,type.$ref):IsIntersect(type)?FromIntersect3(type.allOf):IsUnion(type)?FromUnion6(type.anyOf):IsObject3(type)?FromObject6(type.properties):_Object_({})}function PartialAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(FromType15(type),{},options):PartialDeferred(type,options);return result}function PartialInstantiate(context,state2,type,options){const instantiatedType=InstantiateType(context,state2,type);return PartialAction(instantiatedType,options)}function PickDeferred(type,indexer,options={}){return Deferred("Pick",[type,indexer],options)}function FromKeys2(properties,keys){const result=guard_exports.Keys(properties).reduce((result2,key)=>{return keys.includes(key)?memory_exports.Assign(result2,{[key]:properties[key]}):result2},{});return result}function FromType16(type,indexer){const indexable=ToIndexable(type);const keys=ToIndexableKeys(indexer);const applied=FromKeys2(indexable,keys);const result=_Object_(applied);return result}function PickAction(type,indexer,options){const result=CanInstantiate([type,indexer])?memory_exports.Update(FromType16(type,indexer),{},options):PickDeferred(type,indexer,options);return result}function PickInstantiate(context,state2,type,indexer,options){const instantiatedType=InstantiateType(context,state2,type);const instantiatedIndexer=InstantiateType(context,state2,indexer);return PickAction(instantiatedType,instantiatedIndexer,options)}function ReadonlyObjectDeferred(type,options={}){return Deferred("ReadonlyObject",[type],options)}function FromArray5(type){const result=Immutable(_Array_(type));return result}function FromCyclic4(defs,ref){const target=CyclicTarget(defs,ref);const partial=FromType17(target);const result=Cyclic(memory_exports.Assign(defs,{[ref]:partial}),ref);return result}function FromIntersect4(types){const result=types.map(type=>FromType17(type));return EvaluateIntersect(result)}function FromObject7(properties){const mapped=guard_exports.Keys(properties).reduce((result2,left)=>{return{...result2,[left]:Readonly(properties[left])}},{});const result=_Object_(mapped);return result}function FromTuple4(types){const result=Immutable(Tuple(types));return result}function FromUnion7(types){const result=types.map(type=>FromType17(type));return Union(result)}function FromType17(type){return IsArray3(type)?FromArray5(type.items):IsCyclic(type)?FromCyclic4(type.$defs,type.$ref):IsIntersect(type)?FromIntersect4(type.allOf):IsObject3(type)?FromObject7(type.properties):IsTuple(type)?FromTuple4(type.items):IsUnion(type)?FromUnion7(type.anyOf):type}function ReadonlyObjectAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(FromType17(type),{},options):ReadonlyObjectDeferred(type);return result}function ReadonlyObjectInstantiate(context,state2,type,options){const instantiatedType=InstantiateType(context,state2,type);return ReadonlyObjectAction(instantiatedType,options)}function RefInstantiate(context,state2,type,ref){return ref in context?CyclicCheck([ref],context,context[ref])?type:InstantiateType(context,state2,context[ref]):type}function FromCyclic5(defs,ref){const target=CyclicTarget(defs,ref);const partial=FromType18(target);const result=Cyclic(memory_exports.Assign(defs,{[ref]:partial}),ref);return result}function FromIntersect5(types){const result=types.map(type=>FromType18(type));return EvaluateIntersect(result)}function FromUnion8(types){const result=types.map(type=>FromType18(type));return Union(result)}function FromObject8(properties){const mapped=guard_exports.Keys(properties).reduce((result2,left)=>{return{...result2,[left]:OptionalRemove(properties[left])}},{});const result=_Object_(mapped);return result}function FromType18(type){return IsCyclic(type)?FromCyclic5(type.$defs,type.$ref):IsIntersect(type)?FromIntersect5(type.allOf):IsUnion(type)?FromUnion8(type.anyOf):IsObject3(type)?FromObject8(type.properties):_Object_({})}function RequiredDeferred(type,options={}){return Deferred("Required",[type],options)}function RequiredAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(FromType18(type),{},options):RequiredDeferred(type,options);return result}function RequiredInstantiate(context,state2,type,options){const instaniatedType=InstantiateType(context,state2,type);return RequiredAction(instaniatedType,options)}function ReturnTypeDeferred(type,options={}){return Deferred("ReturnType",[type],options)}function ReturnTypeOperation(type){return IsFunction3(type)?type["returnType"]:Never()}function ReturnTypeAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(ReturnTypeOperation(type),{},options):ReturnTypeDeferred(type,options);return result}function ReturnTypeInstantiate(context,state2,type,options={}){const instantiatedType=InstantiateType(context,state2,type);return ReturnTypeAction(instantiatedType,options)}function SpreadElement(type){const result=IsRest(type)?IsTuple(type.items)?RestSpread(type.items.items):IsInfer(type.items)?[type]:IsRef(type.items)?[type]:[Never()]:[type];return result}function RestSpread(types){const result=types.reduce((result2,left)=>{return[...result2,...SpreadElement(left)]},[]);return result}function CanInstantiate(types){return guard_exports.TakeLeft(types,(left,right)=>IsRef(left)?false:CanInstantiate(right),()=>true)}function ModifierActions(type,readonly,optional){return IsReadonlyRemoveAction(type)?ModifierActions(type.type,"remove",optional):IsOptionalRemoveAction(type)?ModifierActions(type.type,readonly,"remove"):IsReadonlyAddAction(type)?ModifierActions(type.type,"add",optional):IsOptionalAddAction(type)?ModifierActions(type.type,readonly,"add"):[type,readonly,optional]}function ApplyReadonly(action,type){ +return guard_exports.IsEqual(action,"remove")?ReadonlyRemove(type):guard_exports.IsEqual(action,"add")?ReadonlyAdd(type):type}function ApplyOptional(action,type){return guard_exports.IsEqual(action,"remove")?OptionalRemove(type):guard_exports.IsEqual(action,"add")?OptionalAdd(type):type}function InstantiateProperties(context,state2,properties){return guard_exports.Keys(properties).reduce((result,key)=>{return{...result,[key]:InstantiateType(context,state2,properties[key])}},{})}function InstantiateElements(context,state2,types){ +const elements=InstantiateTypes(context,state2,types);const result=RestSpread(elements);return result}function InstantiateTypes(context,state2,types){return types.map(type=>InstantiateType(context,state2,type))}function InstantiateDeferred(context,state2,action,parameters,options){return guard_exports.IsEqual(action,"Awaited")?AwaitedInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Capitalize")?CapitalizeInstantiate(context,state2,parameters[0],options):guard_exports. +IsEqual(action,"Conditional")?ConditionalInstantiate(context,state2,parameters[0],parameters[1],parameters[2],parameters[3],options):guard_exports.IsEqual(action,"ConstructorParameters")?ConstructorParametersInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Evaluate")?EvaluateInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Exclude")?ExcludeInstantiate(context,state2,parameters[0],parameters[1],options):guard_exports.IsEqual(action,"Ex\ +tract")?ExtractInstantiate(context,state2,parameters[0],parameters[1],options):guard_exports.IsEqual(action,"Index")?IndexInstantiate(context,state2,parameters[0],parameters[1],options):guard_exports.IsEqual(action,"InstanceType")?InstanceTypeInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Interface")?InterfaceInstantiate(context,state2,parameters[0],parameters[1],options):guard_exports.IsEqual(action,"KeyOf")?KeyOfInstantiate(context,state2,parameters[0],options): +guard_exports.IsEqual(action,"Lowercase")?LowercaseInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Mapped")?MappedInstantiate(context,state2,parameters[0],parameters[1],parameters[2],parameters[3],options):guard_exports.IsEqual(action,"Module")?ModuleInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"NonNullable")?NonNullableInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Pick")?PickInstantiate(context,state2, +parameters[0],parameters[1],options):guard_exports.IsEqual(action,"Options")?OptionsInstantiate(context,state2,parameters[0],parameters[1]):guard_exports.IsEqual(action,"Parameters")?ParametersInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Partial")?PartialInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Omit")?OmitInstantiate(context,state2,parameters[0],parameters[1],options):guard_exports.IsEqual(action,"ReadonlyObject")?ReadonlyObjectInstantiate( +context,state2,parameters[0],options):guard_exports.IsEqual(action,"Record")?RecordInstantiate(context,state2,parameters[0],parameters[1],options):guard_exports.IsEqual(action,"Required")?RequiredInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"ReturnType")?ReturnTypeInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"TemplateLiteral")?TemplateLiteralInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Uncapita\ +lize")?UncapitalizeInstantiate(context,state2,parameters[0],options):guard_exports.IsEqual(action,"Uppercase")?UppercaseInstantiate(context,state2,parameters[0],options):Deferred(action,parameters,options)}function InstantiateType(context,state2,input){const immutable=IsImmutable(input);const modifiers=ModifierActions(input,IsReadonly(input)?"add":"none",IsOptional(input)?"add":"none");const type=IsBase(modifiers[0])?modifiers[0].Clone():modifiers[0];const instantiated=IsRef(type)?RefInstantiate( +context,state2,type,type.$ref):IsArray3(type)?_Array_(InstantiateType(context,state2,type.items),ArrayOptions(type)):IsAsyncIterator3(type)?AsyncIterator(InstantiateType(context,state2,type.iteratorItems),AsyncIteratorOptions(type)):IsCall(type)?CallInstantiate(context,state2,type.target,type.arguments):IsConstructor3(type)?Constructor(InstantiateTypes(context,state2,type.parameters),InstantiateType(context,state2,type.instanceType),ConstructorOptions(type)):IsDeferred(type)?InstantiateDeferred( +context,state2,type.action,type.parameters,type.options):IsFunction3(type)?_Function_(InstantiateTypes(context,state2,type.parameters),InstantiateType(context,state2,type.returnType),FunctionOptions(type)):IsIntersect(type)?Intersect(InstantiateTypes(context,state2,type.allOf),IntersectOptions(type)):IsIterator3(type)?Iterator(InstantiateType(context,state2,type.iteratorItems),IteratorOptions(type)):IsObject3(type)?_Object_(InstantiateProperties(context,state2,type.properties),ObjectOptions(type)): +IsPromise(type)?_Promise_(InstantiateType(context,state2,type.item),PromiseOptions(type)):IsRecord(type)?RecordFromPattern(RecordPattern(type),InstantiateType(context,state2,RecordValue(type))):IsRest(type)?Rest(InstantiateType(context,state2,type.items)):IsTuple(type)?Tuple(InstantiateElements(context,state2,type.items),TupleOptions(type)):IsUnion(type)?Union(InstantiateTypes(context,state2,type.anyOf),UnionOptions(type)):type;const withImmutable=immutable?Immutable(instantiated):instantiated;const withModifiers=ApplyReadonly( +modifiers[1],ApplyOptional(modifiers[2],withImmutable));return withModifiers}function Instantiate(context,type){return InstantiateType(context,{callstack:[]},type)}function AwaitedOperation(type){return IsPromise(type)?AwaitedOperation(type.item):type}function AwaitedAction(type,options){const result=CanInstantiate([type])?memory_exports.Update(AwaitedOperation(type),{},options):AwaitedDeferred(type,options);return result}function AwaitedInstantiate(context,state2,type,options){const instantiatedType=InstantiateType(context,state2,type);return AwaitedAction(instantiatedType,options)}function AwaitedDeferred(type,options={}){return Deferred("Awaited",[type],options)}function Evaluate2(type,options={}){return EvaluateAction(type,options)}var EventStream=class{constructor(isComplete,extractResult){__publicField(this,"queue",[]);__publicField(this,"waiting",[]);__publicField(this,"done",false);__publicField(this,"finalResultPromise");__publicField(this,"resolveFinalResult");__publicField(this,"isComplete");__publicField(this,"extractResult");this.isComplete=isComplete;this.extractResult=extractResult;this.finalResultPromise=new Promise(resolve=>{this.resolveFinalResult=resolve})}push(event){if(this.done)return;if(this.isComplete(event)){ +this.done=true;this.resolveFinalResult(this.extractResult(event))}const waiter=this.waiting.shift();if(waiter){waiter({value:event,done:false})}else{this.queue.push(event)}}end(result){this.done=true;if(result!==void 0){this.resolveFinalResult(result)}while(this.waiting.length>0){const waiter=this.waiting.shift();waiter({value:void 0,done:true})}}async*[Symbol.asyncIterator](){while(true){if(this.queue.length>0){yield this.queue.shift()}else if(this.done){return}else{const result=await new Promise( +resolve=>this.waiting.push(resolve));if(result.done)return;yield result.value}}}result(){return this.finalResultPromise}};var AssistantMessageEventStream=class extends EventStream{constructor(){super(event=>event.type==="done"||event.type==="error",event=>{if(event.type==="done"){return event.message}else if(event.type==="error"){return event.error}throw new Error("Unexpected event type for final result")})}};function IsGuardInterface(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"check")&&guard_exports.HasPropertyKey(value,"errors")&&guard_exports.IsFunction(value.check)&&guard_exports.IsFunction(value.errors)}function IsGuard2(value){return guard_exports.HasPropertyKey(value,"~guard")&&IsGuardInterface(value["~guard"])}function IsRefine2(value){return guard_exports.HasPropertyKey(value,"~refine")&&guard_exports.IsArray(value["~refine"])&&guard_exports.Every(value["~refine"],0,value2=>guard_exports.IsObject(value2)&&guard_exports.HasPropertyKey(value2,"check")&&guard_exports.HasPropertyKey(value2,"error")&&guard_exports.IsFunction(value2.check)&&guard_exports.IsFunction(value2.error))}function IsSchemaObject(value){return guard_exports.IsObject(value)&&!guard_exports.IsArray(value)}function IsBooleanSchema(value){return guard_exports.IsBoolean(value)}function IsSchema2(value){return IsSchemaObject(value)||IsBooleanSchema(value)}function IsAdditionalItems(schema){return guard_exports.HasPropertyKey(schema,"additionalItems")&&IsSchema2(schema.additionalItems)}function IsAdditionalProperties(schema){return guard_exports.HasPropertyKey(schema,"additionalProperties")&&IsSchema2(schema.additionalProperties)}function IsAllOf(schema){return guard_exports.HasPropertyKey(schema,"allOf")&&guard_exports.IsArray(schema.allOf)&&schema.allOf.every(value=>IsSchema2(value))}function IsAnchor(schema){return guard_exports.HasPropertyKey(schema,"$anchor")&&guard_exports.IsString(schema.$anchor)}function IsAnyOf(schema){return guard_exports.HasPropertyKey(schema,"anyOf")&&guard_exports.IsArray(schema.anyOf)&&schema.anyOf.every(value=>IsSchema2(value))}function IsConst(value){return guard_exports.HasPropertyKey(value,"const")}function IsContains(schema){return guard_exports.HasPropertyKey(schema,"contains")&&IsSchema2(schema.contains)}function IsDefault(schema){return guard_exports.HasPropertyKey(schema,"default")}function IsDependencies(schema){return guard_exports.HasPropertyKey(schema,"dependencies")&&guard_exports.IsObject(schema.dependencies)&&Object.values(schema.dependencies).every(value=>IsSchema2(value)||guard_exports.IsArray(value)&&value.every(value2=>guard_exports.IsString(value2)))}function IsDependentRequired(schema){return guard_exports.HasPropertyKey(schema,"dependentRequired")&&guard_exports.IsObject(schema.dependentRequired)&&Object.values(schema.dependentRequired).every(value=>guard_exports.IsArray(value)&&value.every(value2=>guard_exports.IsString(value2)))}function IsDependentSchemas(schema){return guard_exports.HasPropertyKey(schema,"dependentSchemas")&&guard_exports.IsObject(schema.dependentSchemas)&&Object.values(schema.dependentSchemas).every(value=>IsSchema2(value))}function IsDynamicAnchor(schema){return guard_exports.HasPropertyKey(schema,"$dynamicAnchor")&&guard_exports.IsString(schema.$dynamicAnchor)}function IsDynamicRef(schema){return guard_exports.HasPropertyKey(schema,"$dynamicRef")&&guard_exports.IsString(schema.$dynamicRef)}function IsElse(schema){return guard_exports.HasPropertyKey(schema,"else")&&IsSchema2(schema.else)}function IsEnum2(schema){return guard_exports.HasPropertyKey(schema,"enum")&&guard_exports.IsArray(schema.enum)}function IsExclusiveMaximum(schema){return guard_exports.HasPropertyKey(schema,"exclusiveMaximum")&&(guard_exports.IsNumber(schema.exclusiveMaximum)||guard_exports.IsBigInt(schema.exclusiveMaximum))}function IsExclusiveMinimum(schema){return guard_exports.HasPropertyKey(schema,"exclusiveMinimum")&&(guard_exports.IsNumber(schema.exclusiveMinimum)||guard_exports.IsBigInt(schema.exclusiveMinimum))}function IsFormat(schema){return guard_exports.HasPropertyKey(schema,"format")&&guard_exports.IsString(schema.format)}function IsId(schema){return guard_exports.HasPropertyKey(schema,"$id")&&guard_exports.IsString(schema.$id)}function IsIf(schema){return guard_exports.HasPropertyKey(schema,"if")&&IsSchema2(schema.if)}function IsItems(schema){return guard_exports.HasPropertyKey(schema,"items")&&(IsSchema2(schema.items)||guard_exports.IsArray(schema.items)&&schema.items.every(value=>{return IsSchema2(value)}))}function IsItemsSized(schema){return IsItems(schema)&&guard_exports.IsArray(schema.items)}function IsMaximum(schema){return guard_exports.HasPropertyKey(schema,"maximum")&&(guard_exports.IsNumber(schema.maximum)||guard_exports.IsBigInt(schema.maximum))}function IsMaxContains(schema){return guard_exports.HasPropertyKey(schema,"maxContains")&&guard_exports.IsNumber(schema.maxContains)}function IsMaxItems(schema){return guard_exports.HasPropertyKey(schema,"maxItems")&&guard_exports.IsNumber(schema.maxItems)}function IsMaxLength4(schema){return guard_exports.HasPropertyKey(schema,"maxLength")&&guard_exports.IsNumber(schema.maxLength)}function IsMaxProperties(schema){return guard_exports.HasPropertyKey(schema,"maxProperties")&&guard_exports.IsNumber(schema.maxProperties)}function IsMinimum(schema){return guard_exports.HasPropertyKey(schema,"minimum")&&(guard_exports.IsNumber(schema.minimum)||guard_exports.IsBigInt(schema.minimum))}function IsMinContains(schema){return guard_exports.HasPropertyKey(schema,"minContains")&&guard_exports.IsNumber(schema.minContains)}function IsMinItems(schema){return guard_exports.HasPropertyKey(schema,"minItems")&&guard_exports.IsNumber(schema.minItems)}function IsMinLength4(schema){return guard_exports.HasPropertyKey(schema,"minLength")&&guard_exports.IsNumber(schema.minLength)}function IsMinProperties(schema){return guard_exports.HasPropertyKey(schema,"minProperties")&&guard_exports.IsNumber(schema.minProperties)}function IsMultipleOf2(schema){return guard_exports.HasPropertyKey(schema,"multipleOf")&&(guard_exports.IsNumber(schema.multipleOf)||guard_exports.IsBigInt(schema.multipleOf))}function IsNot(schema){return guard_exports.HasPropertyKey(schema,"not")&&IsSchema2(schema.not)}function IsOneOf(schema){return guard_exports.HasPropertyKey(schema,"oneOf")&&guard_exports.IsArray(schema.oneOf)&&schema.oneOf.every(value=>IsSchema2(value))}function IsPattern(schema){return guard_exports.HasPropertyKey(schema,"pattern")&&(guard_exports.IsString(schema.pattern)||schema.pattern instanceof RegExp)}function IsPatternProperties(schema){return guard_exports.HasPropertyKey(schema,"patternProperties")&&guard_exports.IsObject(schema.patternProperties)&&Object.values(schema.patternProperties).every(value=>IsSchema2(value))}function IsPrefixItems(schema){return guard_exports.HasPropertyKey(schema,"prefixItems")&&guard_exports.IsArray(schema.prefixItems)&&schema.prefixItems.every(schema2=>IsSchema2(schema2))}function IsProperties(schema){return guard_exports.HasPropertyKey(schema,"properties")&&guard_exports.IsObject(schema.properties)&&Object.values(schema.properties).every(value=>IsSchema2(value))}function IsPropertyNames(schema){return guard_exports.HasPropertyKey(schema,"propertyNames")&&(guard_exports.IsObject(schema.propertyNames)||IsSchema2(schema.propertyNames))}function IsRecursiveAnchor(schema){return guard_exports.HasPropertyKey(schema,"$recursiveAnchor")&&guard_exports.IsBoolean(schema.$recursiveAnchor)}function IsRecursiveAnchorTrue(schema){return IsRecursiveAnchor(schema)&&guard_exports.IsEqual(schema.$recursiveAnchor,true)}function IsRecursiveRef(schema){return guard_exports.HasPropertyKey(schema,"$recursiveRef")&&guard_exports.IsString(schema.$recursiveRef)}function IsRef2(schema){return guard_exports.HasPropertyKey(schema,"$ref")&&guard_exports.IsString(schema.$ref)}function IsRequired(schema){return guard_exports.HasPropertyKey(schema,"required")&&guard_exports.IsArray(schema.required)&&schema.required.every(value=>guard_exports.IsString(value))}function IsThen(schema){return guard_exports.HasPropertyKey(schema,"then")&&IsSchema2(schema.then)}function IsType(schema){return guard_exports.HasPropertyKey(schema,"type")&&(guard_exports.IsString(schema.type)||guard_exports.IsArray(schema.type)&&schema.type.every(value=>guard_exports.IsString(value)))}function IsUniqueItems(schema){return guard_exports.HasPropertyKey(schema,"uniqueItems")&&guard_exports.IsBoolean(schema.uniqueItems)}function IsUnevaluatedItems(schema){return guard_exports.HasPropertyKey(schema,"unevaluatedItems")&&IsSchema2(schema.unevaluatedItems)}function IsUnevaluatedProperties(schema){return guard_exports.HasPropertyKey(schema,"unevaluatedProperties")&&IsSchema2(schema.unevaluatedProperties)}function HasUnevaluatedFromObject(value){return IsUnevaluatedItems(value)||IsUnevaluatedProperties(value)||guard_exports.Keys(value).some(key=>HasUnevaluatedFromUnknown(value[key]))}function HasUnevaluatedFromArray(value){return value.some(value2=>HasUnevaluatedFromUnknown(value2))}function HasUnevaluatedFromUnknown(value){return guard_exports.IsArray(value)?HasUnevaluatedFromArray(value):guard_exports.IsObject(value)?HasUnevaluatedFromObject(value):false}function HasUnevaluated(context,schema){ +return HasUnevaluatedFromUnknown(schema)||guard_exports.Keys(context).some(key=>HasUnevaluatedFromUnknown(context[key]))}var BuildContext=class{constructor(hasUnevaluated){this.hasUnevaluated=hasUnevaluated}UseUnevaluated(){return this.hasUnevaluated}Push(){return emit_exports.Call(emit_exports.Member("context","Push"),[])}Pop(){return emit_exports.Call(emit_exports.Member("context","Pop"),[])}AddIndex(index2){return emit_exports.Call(emit_exports.Member("context","AddIndex"),[index2])}AddKey(key){ +return emit_exports.Call(emit_exports.Member("context","AddKey"),[key])}Merge(results){return emit_exports.Call(emit_exports.Member("context","Merge"),[results])}};var CheckContext=class{constructor(){const indices=new Set;const keys=new Set;this.stack=[{indices,keys}]}Push(){const indices=new Set;const keys=new Set;this.stack.push({indices,keys});return true}Pop(){this.stack.pop();return true}AddIndex(index2){this.GetIndices().add(index2);return true}AddKey(key){this.GetKeys().add(key);return true}GetIndices(){ +const top=this.stack[this.stack.length-1];return top.indices}GetKeys(){const top=this.stack[this.stack.length-1];return top.keys}Merge(results){for(const context of results){context.GetIndices().forEach(value=>this.GetIndices().add(value));context.GetKeys().forEach(value=>this.GetKeys().add(value))}return true}};var ErrorContext=class extends CheckContext{constructor(callback){super();this.callback=callback}AddError(error){this.callback(error);return false}};var AccumulatedErrorContext=class extends ErrorContext{constructor(){ +super(error=>this.errors.push(error));this.errors=[]}AddError(error){this.errors.push(error);return false}GetErrors(){return this.errors}};var state={identifier:"External",variables:[]};function CreateVariable(value){const call=`External[${state.variables.length}]`;state.variables.push(value);return call}function ResetExternal(){state.variables=[]}function GetExternal(){return{...state}}function BuildGuard(_stack,_context,schema,value){return emit_exports.Call(emit_exports.Member(emit_exports.Member(CreateVariable(schema),"~guard"),"check"),[value])}function CheckGuard(_stack,_context,schema,value){return schema["~guard"].check(value)}function ErrorGuard(_stack,context,schemaPath,instancePath,schema,value){return schema["~guard"].check(value)||context.AddError({keyword:"~guard",schemaPath,instancePath,params:{errors:schema["~guard"].errors(value)}})}function BuildRefine(_stack,_context,schema,value){const refinements=CreateVariable(schema["~refine"].map(refinement=>refinement));return emit_exports.Every(refinements,emit_exports.Constant(0),["refinement","_"],emit_exports.Call(emit_exports.Member("refinement","check"),[value]))}function CheckRefine(_stack,_context,schema,value){return guard_exports.Every(schema["~refine"],0,(refinement,_)=>refinement.check(value))}function ErrorRefine(_stack,context,schemaPath,instancePath,schema,value){return guard_exports. +EveryAll(schema["~refine"],0,(refinement,index2)=>{return refinement.check(value)||context.AddError({keyword:"~refine",schemaPath,instancePath,params:{index:index2,message:refinement.error(value)}})})}var index=0;function Unique(){return`var_${index++}`}function IsValid(schema){return IsItems(schema)&&guard_exports.IsArray(schema.items)}function BuildAdditionalItems(stack,context,schema,value){if(!IsValid(schema))return emit_exports.Constant(true);const[item,index2]=[Unique(),Unique()];const isSchema=BuildSchemaPushStack(stack,context,schema.additionalItems,item);const isLength=emit_exports.IsLessThan(index2,emit_exports.Constant(schema.items.length));const addIndex=context.AddIndex(index2);const guarded=context.UseUnevaluated()?emit_exports.Or( +isLength,emit_exports.And(isSchema,addIndex)):emit_exports.Or(isLength,isSchema);return emit_exports.Call(emit_exports.Member(value,"every"),[emit_exports.ArrowFunction([item,index2],guarded)])}function CheckAdditionalItems(stack,context,schema,value){if(!IsValid(schema))return true;const isAdditionalItems=value.every((item,index2)=>{return guard_exports.IsLessThan(index2,schema.items.length)||CheckSchemaPushStack(stack,context,schema.additionalItems,item)&&context.AddIndex(index2)});return isAdditionalItems} +function ErrorAdditionalItems(stack,context,schemaPath,instancePath,schema,value){if(!IsValid(schema))return true;const isAdditionalItems=value.every((item,index2)=>{const nextSchemaPath=`${schemaPath}/additionalItems`;const nextInstancePath=`${instancePath}/${index2}`;return guard_exports.IsLessThan(index2,schema.items.length)||ErrorSchemaPushStack(stack,context,nextSchemaPath,nextInstancePath,schema.additionalItems,item)&&context.AddIndex(index2)});return isAdditionalItems}function GetPropertyKeyAsPattern(key){const escaped=key.replace(/[.*+?^${}()|[\]\\]/g,"\\$&");return`^${escaped}$`}function GetPropertiesPattern(schema){const patterns=[];if(IsPatternProperties(schema))patterns.push(...guard_exports.Keys(schema.patternProperties));if(IsProperties(schema))patterns.push(...guard_exports.Keys(schema.properties).map(GetPropertyKeyAsPattern));return guard_exports.IsEqual(patterns.length,0)?"(?!)":`(${patterns.join("|")})`}function CanAdditionalPropertiesFast(_context,schema,_value){ +return IsRequired(schema)&&IsProperties(schema)&&!IsPatternProperties(schema)&&guard_exports.IsEqual(schema.additionalProperties,false)&&guard_exports.IsEqual(guard_exports.Keys(schema.properties).length,schema.required.length)}function BuildAdditionalPropertiesFast(_context,schema,value){return emit_exports.IsEqual(emit_exports.Member(emit_exports.Call(emit_exports.Member("Object","getOwnPropertyNames"),[value]),"length"),emit_exports.Constant(schema.required.length))}function BuildAdditionalPropertiesStandard(stack,context,schema,value){ +const[key,_index]=[Unique(),Unique()];const regexp=CreateVariable(new RegExp(GetPropertiesPattern(schema)));const isSchema=BuildSchemaPushStack(stack,context,schema.additionalProperties,`${value}[${key}]`);const isKey=emit_exports.Call(emit_exports.Member(regexp,"test"),[key]);const addKey=context.AddKey(key);const guarded=context.UseUnevaluated()?emit_exports.Or(isKey,emit_exports.And(isSchema,addKey)):emit_exports.Or(isKey,isSchema);const result=emit_exports.Every(emit_exports.Keys(value),emit_exports. +Constant(0),[key,_index],guarded);return result}function BuildAdditionalProperties(stack,context,schema,value){return CanAdditionalPropertiesFast(context,schema,value)?BuildAdditionalPropertiesFast(context,schema,value):BuildAdditionalPropertiesStandard(stack,context,schema,value)}function CheckAdditionalProperties(stack,context,schema,value){const regexp=new RegExp(GetPropertiesPattern(schema));const isAdditionalProperties=guard_exports.Every(guard_exports.Keys(value),0,(key,_index)=>{return regexp. +test(key)||CheckSchemaPushStack(stack,context,schema.additionalProperties,value[key])&&context.AddKey(key)});return isAdditionalProperties}function ErrorAdditionalProperties(stack,context,schemaPath,instancePath,schema,value){const regexp=new RegExp(GetPropertiesPattern(schema));const additionalProperties=[];const isAdditionalProperties=guard_exports.EveryAll(guard_exports.Keys(value),0,(key,_index)=>{const nextSchemaPath=`${schemaPath}/additionalProperties`;const nextInstancePath=`${instancePath}\ +/${key}`;const nextContext=new AccumulatedErrorContext;const isAdditionalProperty=regexp.test(key)||ErrorSchemaPushStack(stack,nextContext,nextSchemaPath,nextInstancePath,schema.additionalProperties,value[key])&&context.AddKey(key);if(!isAdditionalProperty)additionalProperties.push(key);return isAdditionalProperty});return isAdditionalProperties||context.AddError({keyword:"additionalProperties",schemaPath,instancePath,params:{additionalProperties}})}function Reducer(stack,context,schemas,value,check){const results=emit_exports.ConstDeclaration("results","[]");const context_n=schemas.map((_schema,index2)=>emit_exports.ConstDeclaration(`context_${index2}`,emit_exports.New("CheckContext",[])));const condition_n=schemas.map((schema,index2)=>emit_exports.ConstDeclaration(`condition_${index2}`,emit_exports.Call(emit_exports.ArrowFunction(["context"],BuildSchema(stack,context,schema,value)),[`context_${index2}`])));const checks=schemas.map((_schema,index2)=>emit_exports. +If(`condition_${index2}`,emit_exports.Call(emit_exports.Member("results","push"),[`context_${index2}`])));const returns=emit_exports.Return(emit_exports.And(check,context.Merge("results")));return emit_exports.Call(emit_exports.ArrowFunction([],emit_exports.Statements([results,...context_n,...condition_n,...checks,returns])),[])}function BuildAllOfStandard(stack,context,schema,value){return Reducer(stack,context,schema.allOf,value,emit_exports.IsEqual(emit_exports.Member("results","length"),emit_exports.Constant(schema.allOf.length)))}function BuildAllOfFast(stack,context,schema,value){return emit_exports.ReduceAnd(schema.allOf.map(schema2=>BuildSchema(stack,context,schema2,value)))}function BuildAllOf(stack,context,schema,value){return context.UseUnevaluated()?BuildAllOfStandard(stack,context,schema,value):BuildAllOfFast( +stack,context,schema,value)}function CheckAllOf(stack,context,schema,value){const results=schema.allOf.reduce((result,schema2)=>{const nextContext=new CheckContext;return CheckSchema(stack,nextContext,schema2,value)?[...result,nextContext]:result},[]);return guard_exports.IsEqual(results.length,schema.allOf.length)&&context.Merge(results)}function ErrorAllOf(stack,context,schemaPath,instancePath,schema,value){const failedContexts=[];const results=schema.allOf.reduce((result,schema2,index2)=>{const nextSchemaPath=`${schemaPath}\ +/allOf/${index2}`;const nextContext=new AccumulatedErrorContext;const isSchema=ErrorSchema(stack,nextContext,nextSchemaPath,instancePath,schema2,value);if(!isSchema)failedContexts.push(nextContext);return isSchema?[...result,nextContext]:result},[]);const isAllOf=guard_exports.IsEqual(results.length,schema.allOf.length)&&context.Merge(results);if(!isAllOf)failedContexts.forEach(failed=>failed.GetErrors().forEach(error=>context.AddError(error)));return isAllOf}function BuildAnyOfStandard(stack,context,schema,value){return Reducer(stack,context,schema.anyOf,value,emit_exports.IsGreaterThan(emit_exports.Member("results","length"),emit_exports.Constant(0)))}function BuildAnyOfFast(stack,context,schema,value){return emit_exports.ReduceOr(schema.anyOf.map(schema2=>BuildSchema(stack,context,schema2,value)))}function BuildAnyOf(stack,context,schema,value){return context.UseUnevaluated()?BuildAnyOfStandard(stack,context,schema,value):BuildAnyOfFast(stack,context, +schema,value)}function CheckAnyOf(stack,context,schema,value){const results=schema.anyOf.reduce((result,schema2)=>{const nextContext=new CheckContext;return CheckSchema(stack,nextContext,schema2,value)?[...result,nextContext]:result},[]);return guard_exports.IsGreaterThan(results.length,0)&&context.Merge(results)}function ErrorAnyOf(stack,context,schemaPath,instancePath,schema,value){const failedContexts=[];const results=schema.anyOf.reduce((result,schema2,index2)=>{const nextContext=new AccumulatedErrorContext; +const nextSchemaPath=`${schemaPath}/anyOf/${index2}`;const isSchema=ErrorSchema(stack,nextContext,nextSchemaPath,instancePath,schema2,value);if(!isSchema)failedContexts.push(nextContext);return isSchema?[...result,nextContext]:result},[]);const isAnyOf=guard_exports.IsGreaterThan(results.length,0)&&context.Merge(results);if(!isAnyOf)failedContexts.forEach(failed=>failed.GetErrors().forEach(error=>context.AddError(error)));return isAnyOf||context.AddError({keyword:"anyOf",schemaPath,instancePath, +params:{}})}function BuildBooleanSchema(_stack,_context,schema,_value){return schema?emit_exports.Constant(true):emit_exports.Constant(false)}function CheckBooleanSchema(_stack,_context,schema,_value){return schema}function ErrorBooleanSchema(stack,context,schemaPath,instancePath,schema,value){return CheckBooleanSchema(stack,context,schema,value)||context.AddError({keyword:"boolean",schemaPath,instancePath,params:{}})}function BuildConst(_stack,_context,schema,value){return guard_exports.IsValueLike(schema.const)?emit_exports.IsEqual(value,emit_exports.Constant(schema.const)):emit_exports.IsDeepEqual(value,CreateVariable(schema.const))}function CheckConst(_stack,_context,schema,value){return guard_exports.IsValueLike(schema.const)?guard_exports.IsEqual(value,schema.const):guard_exports.IsDeepEqual(value,schema.const)}function ErrorConst(stack,context,schemaPath,instancePath,schema,value){return CheckConst(stack, +context,schema,value)||context.AddError({keyword:"const",schemaPath,instancePath,params:{allowedValue:schema.const}})}function IsValid2(schema){return!(IsMinContains(schema)&&guard_exports.IsEqual(schema.minContains,0))}function BuildContains(stack,context,schema,value){if(!IsValid2(schema))return emit_exports.Constant(true);const item=Unique();const isLength=emit_exports.Not(emit_exports.IsEqual(emit_exports.Member(value,"length"),emit_exports.Constant(0)));const isSome=emit_exports.Call(emit_exports.Member(value,"some"),[emit_exports.ArrowFunction([item],BuildSchema(stack,context,schema.contains,item))]);return emit_exports. +And(isLength,isSome)}function CheckContains(stack,context,schema,value){if(!IsValid2(schema))return true;return!guard_exports.IsEqual(value.length,0)&&value.some(item=>CheckSchema(stack,context,schema.contains,item))}function ErrorContains(stack,context,schemaPath,instancePath,schema,value){return CheckContains(stack,context,schema,value)||context.AddError({keyword:"contains",schemaPath,instancePath,params:{minContains:1}})}function BuildDependencies(stack,context,schema,value){const isLength=emit_exports.IsEqual(emit_exports.Member(emit_exports.Keys(value),"length"),emit_exports.Constant(0));const isEveryDependency=emit_exports.ReduceAnd(guard_exports.Entries(schema.dependencies).map(([key,schema2])=>{const notKey=emit_exports.Not(emit_exports.HasPropertyKey(value,emit_exports.Constant(key)));const isSchema=BuildSchema(stack,context,schema2,value);const isEveryKey=schema3=>emit_exports.ReduceAnd(schema3.map(key2=>emit_exports. +HasPropertyKey(value,emit_exports.Constant(key2))));return emit_exports.Or(notKey,guard_exports.IsArray(schema2)?isEveryKey(schema2):isSchema)}));return emit_exports.Or(isLength,isEveryDependency)}function CheckDependencies(stack,context,schema,value){const isLength=guard_exports.IsEqual(guard_exports.Keys(value).length,0);const isEvery=guard_exports.Every(guard_exports.Entries(schema.dependencies),0,([key,schema2])=>{return!guard_exports.HasPropertyKey(value,key)||(guard_exports.IsArray(schema2)? +schema2.every(key2=>guard_exports.HasPropertyKey(value,key2)):CheckSchema(stack,context,schema2,value))});return isLength||isEvery}function ErrorDependencies(stack,context,schemaPath,instancePath,schema,value){const isLength=guard_exports.IsEqual(guard_exports.Keys(value).length,0);const isEvery=guard_exports.EveryAll(guard_exports.Entries(schema.dependencies),0,([key,schema2])=>{const nextSchemaPath=`${schemaPath}/dependencies/${key}`;return!guard_exports.HasPropertyKey(value,key)||(guard_exports. +IsArray(schema2)?schema2.every(dependency=>guard_exports.HasPropertyKey(value,dependency)||context.AddError({keyword:"dependencies",schemaPath,instancePath,params:{property:key,dependencies:schema2}})):ErrorSchema(stack,context,nextSchemaPath,instancePath,schema2,value))});return isLength||isEvery}function BuildDependentRequired(_stack,_context,schema,value){const isLength=emit_exports.IsEqual(emit_exports.Member(emit_exports.Keys(value),"length"),emit_exports.Constant(0));const isEvery=emit_exports.ReduceAnd(guard_exports.Entries(schema.dependentRequired).map(([key,keys])=>{const notKey=emit_exports.Not(emit_exports.HasPropertyKey(value,emit_exports.Constant(key)));const everyKey=emit_exports.ReduceAnd(keys.map(key2=>emit_exports.HasPropertyKey(value,emit_exports.Constant(key2))));return emit_exports. +Or(notKey,everyKey)}));return emit_exports.Or(isLength,isEvery)}function CheckDependentRequired(_stack,_context,schema,value){const isLength=guard_exports.IsEqual(guard_exports.Keys(value).length,0);const isEvery=guard_exports.Every(guard_exports.Entries(schema.dependentRequired),0,([key,keys])=>{return!guard_exports.HasPropertyKey(value,key)||keys.every(key2=>guard_exports.HasPropertyKey(value,key2))});return isLength||isEvery}function ErrorDependentRequired(_stack,context,schemaPath,instancePath,schema,value){ +const isLength=guard_exports.IsEqual(guard_exports.Keys(value).length,0);const isEveryEntry=guard_exports.EveryAll(guard_exports.Entries(schema.dependentRequired),0,([key,keys])=>{return!guard_exports.HasPropertyKey(value,key)||guard_exports.EveryAll(keys,0,dependency=>guard_exports.HasPropertyKey(value,dependency)||context.AddError({keyword:"dependentRequired",schemaPath,instancePath,params:{property:key,dependencies:keys}}))});return isLength||isEveryEntry}function BuildDependentSchemas(stack,context,schema,value){const isLength=emit_exports.IsEqual(emit_exports.Member(emit_exports.Keys(value),"length"),emit_exports.Constant(0));const isEvery=emit_exports.ReduceAnd(guard_exports.Entries(schema.dependentSchemas).map(([key,schema2])=>{const notKey=emit_exports.Not(emit_exports.HasPropertyKey(value,emit_exports.Constant(key)));const isSchema=BuildSchema(stack,context,schema2,value);return emit_exports.Or(notKey,isSchema)}));return emit_exports.Or(isLength, +isEvery)}function CheckDependentSchemas(stack,context,schema,value){const isLength=guard_exports.IsEqual(guard_exports.Keys(value).length,0);const isEvery=guard_exports.Every(guard_exports.Entries(schema.dependentSchemas),0,([key,schema2])=>{return!guard_exports.HasPropertyKey(value,key)||CheckSchema(stack,context,schema2,value)});return isLength||isEvery}function ErrorDependentSchemas(stack,context,schemaPath,instancePath,schema,value){const isLength=guard_exports.IsEqual(guard_exports.Keys(value). +length,0);const isEvery=guard_exports.EveryAll(guard_exports.Entries(schema.dependentSchemas),0,([key,schema2])=>{const nextSchemaPath=`${schemaPath}/dependentSchemas/${key}`;return!guard_exports.HasPropertyKey(value,key)||ErrorSchema(stack,context,nextSchemaPath,instancePath,schema2,value)});return isLength||isEvery}function BuildDynamicRef(stack,context,schema,value){const target=stack.DynamicRef(schema)??false;return CreateFunction(stack,context,target,value)}function CheckDynamicRef(stack,context,schema,value){const target=stack.DynamicRef(schema)??false;return IsSchema2(target)&&CheckSchema(stack,context,target,value)}function ErrorDynamicRef(stack,context,_schemaPath,instancePath,schema,value){const target=stack.DynamicRef(schema)??false;return IsSchema2(target)&&ErrorSchema(stack,context,"#",instancePath, +target,value)}function BuildEnum(_stack,_context,schema,value){return emit_exports.ReduceOr(schema.enum.map(option=>{if(guard_exports.IsValueLike(option))return emit_exports.IsEqual(value,emit_exports.Constant(option));const variable=CreateVariable(option);return emit_exports.IsDeepEqual(value,variable)}))}function CheckEnum(_stack,_context,schema,value){return schema.enum.some(option=>guard_exports.IsValueLike(option)?guard_exports.IsEqual(value,option):guard_exports.IsDeepEqual(value,option))}function ErrorEnum(stack,context,schemaPath,instancePath,schema,value){ +return CheckEnum(stack,context,schema,value)||context.AddError({keyword:"enum",schemaPath,instancePath,params:{allowedValues:schema.enum}})}function BuildExclusiveMaximum(_stack,_context,schema,value){return emit_exports.IsLessThan(value,emit_exports.Constant(schema.exclusiveMaximum))}function CheckExclusiveMaximum(_stack,_context,schema,value){return guard_exports.IsLessThan(value,schema.exclusiveMaximum)}function ErrorExclusiveMaximum(stack,context,schemaPath,instancePath,schema,value){return CheckExclusiveMaximum(stack,context,schema,value)||context.AddError({keyword:"exclusiveMaximum",schemaPath,instancePath,params:{comparison:"\ +<",limit:schema.exclusiveMaximum}})}function BuildExclusiveMinimum(_stack,_context,schema,value){return emit_exports.IsGreaterThan(value,emit_exports.Constant(schema.exclusiveMinimum))}function CheckExclusiveMinimum(_stack,_context,schema,value){return guard_exports.IsGreaterThan(value,schema.exclusiveMinimum)}function ErrorExclusiveMinimum(stack,context,schemaPath,instancePath,schema,value){return CheckExclusiveMinimum(stack,context,schema,value)||context.AddError({keyword:"exclusiveMinimum",schemaPath,instancePath,params:{comparison:"\ +>",limit:schema.exclusiveMinimum}})}var format_exports={};__export(format_exports,{Clear:()=>Clear,Entries:()=>Entries3,Get:()=>Get3,Has:()=>Has,IsDate:()=>IsDate2,IsDateTime:()=>IsDateTime,IsDuration:()=>IsDuration,IsEmail:()=>IsEmail,IsHostname:()=>IsHostname,IsIPv4:()=>IsIPv4,IsIPv6:()=>IsIPv6,IsIdnEmail:()=>IsIdnEmail,IsIdnHostname:()=>IsIdnHostname,IsIri:()=>IsIri,IsIriReference:()=>IsIriReference,IsJsonPointer:()=>IsJsonPointer,IsJsonPointerUriFragment:()=>IsJsonPointerUriFragment,IsRegex:()=>IsRegex,IsRelativeJsonPointer:()=>IsRelativeJsonPointer, +IsTime:()=>IsTime,IsUri:()=>IsUri,IsUriReference:()=>IsUriReference,IsUriTemplate:()=>IsUriTemplate,IsUrl:()=>IsUrl,IsUuid:()=>IsUuid,Reset:()=>Reset2,Set:()=>Set3,Test:()=>Test});var DAYS=[0,31,28,31,30,31,30,31,31,30,31,30,31];var DATE=/^(\d\d\d\d)-(\d\d)-(\d\d)$/;function IsLeapYear(year){return year%4===0&&(year%100!==0||year%400===0)}function IsDate2(value){const matches=DATE.exec(value);if(!matches)return false;const year=+matches[1];const month=+matches[2];const day=+matches[3];return month>=1&&month<=12&&day>=1&&day<=(month===2&&IsLeapYear(year)?29:DAYS[month])}var TIME=/^(\d\d):(\d\d):(\d\d(?:\.\d+)?)(?:Z|([+-])(\d\d):(\d\d))?$/i;function IsTime(value,strictTimeZone=true){const matches=TIME.exec(value);if(!matches)return false;const hr=+matches[1];const min=+matches[2];const sec=+matches[3];const tzSign=matches[4]==="-"?-1:1;const tzH=+(matches[5]||0);const tzM=+(matches[6]||0);if(tzH>23||tzM>59)return false;if(strictTimeZone&&!matches[4]&&value.toLowerCase().indexOf("z")===-1){return false}if(hr<=23&&min<=59&&sec<60)return true;const utcMin=min-tzM*tzSign; +const utcHr=hr-tzH*tzSign-(utcMin<0?1:0);return(utcHr===23||utcHr===-1)&&(utcMin===59||utcMin===-1)&&sec<61}function IsDateTime(value,strictTimeZone=true){const dateTime=value.split(/T/i);return dateTime.length===2&&IsDate2(dateTime[0])&&IsTime(dateTime[1],strictTimeZone)}var Duration=/^P((\d+Y(\d+M(\d+D)?)?|\d+M(\d+D)?|\d+D)(T(\d+H(\d+M(\d+S)?)?|\d+M(\d+S)?|\d+S))?|T(\d+H(\d+M(\d+S)?)?|\d+M(\d+S)?|\d+S)|\d+W)$/;function IsDuration(value){return Duration.test(value)}var Email=/^(?!.*\.\.)[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?(?:\.[a-z0-9](?:[a-z0-9-]{0,61}[a-z0-9])?)*$/i;function IsEmail(value){return Email.test(value)}var PUNYCODE_BASE=36;var PUNYCODE_TMIN=1;var PUNYCODE_TMAX=26;var PUNYCODE_SKEW=38;var PUNYCODE_DAMP=700;var PUNYCODE_INITIAL_BIAS=72;var PUNYCODE_INITIAL_N=128;function Adapt(delta,numPoints,firstTime){delta=firstTime?Math.floor(delta/PUNYCODE_DAMP):delta>>1;delta+=Math.floor(delta/numPoints);let k=0;while(delta>(PUNYCODE_BASE-PUNYCODE_TMIN)*PUNYCODE_TMAX>>1){delta=Math.floor(delta/(PUNYCODE_BASE-PUNYCODE_TMIN));k+=PUNYCODE_BASE}return k+Math.floor((PUNYCODE_BASE-PUNYCODE_TMIN+1)*delta/(delta+PUNYCODE_SKEW))} +function Decode(value){const output=[];let n=PUNYCODE_INITIAL_N;let i=0;let bias=PUNYCODE_INITIAL_BIAS;const delimIdx=value.lastIndexOf("-");if(delimIdx>0){for(let j=0;j=128)throw new Error("Invalid punycode: non-basic before delimiter");output.push(cp)}}let inIdx=delimIdx<0?0:delimIdx+1;while(inIdx=value.length)throw new Error("Invalid punycode: unexpected end of input"); +const ch=value.charCodeAt(inIdx++);let digit;if(ch>=97&&ch<=122)digit=ch-97;else if(ch>=48&&ch<=57)digit=ch-48+26;else if(ch>=65&&ch<=90)digit=ch-65;else throw new Error("Invalid punycode: bad digit character");i+=digit*w;const t=k<=bias?PUNYCODE_TMIN:k>=bias+PUNYCODE_TMAX?PUNYCODE_TMAX:k-bias;if(digit=1632&&cp<=1641}function IsExtendedArabicIndicDigit(cp){return cp>=1776&&cp<=1785}function IsVirama(cp){return VIRAMA_CPS.has(cp)}function IsUnicodeLabel(value){if(value.length===0)return false;const cps=[...value].map(c=>c.codePointAt(0));const len=cps.length;if(cps[0]===45||cps[len-1]===45)return false;if(len>=4&&cps[2]===45&&cps[3]===45)return false;if(IsCombiningMark2(cps[0]))return false;let hasJapanese=false;let hasArabicIndic=false; +let hasExtendedArabicIndic=false;for(let i=0;i=4&&value.charCodeAt(2)===45&&value.charCodeAt(3)===45)return false;for(let i=0;i=97&&ch<=122||ch>=65&&ch<=90|| +ch>=48&&ch<=57||ch===45))return false}return true}function IsPuny(value){return value.toLowerCase().startsWith("xn--")}function IsPunyLabel(value){try{return IsUnicodeLabel(Decode(value.slice(4)))}catch{return false}}function IsIdnLabel(value){if(value.length===0||value.length>63)return false;return IsPuny(value)?IsPunyLabel(value):IsUnicodeLabel(value)}function IsLabel(value){if(value.length===0||value.length>63)return false;return IsPuny(value)?IsPunyLabel(value):IsAsciiLabel(value)}function IsHostname(value){if(value.length===0||value.length>253)return false;if(value.charCodeAt(value.length-1)===46)return false;for(const label of value.split(".")){if(!IsLabel(label))return false}return true}var IdnEmail=/^(?!.*\.\.)[\p{L}\p{N}!#$%&'*+/=?^_`{|}~-]+(?:\.[\p{L}\p{N}!#$%&'*+/=?^_`{|}~-]+)*@[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?(?:\.[\p{L}\p{N}](?:[\p{L}\p{N}-]{0,61}[\p{L}\p{N}])?)*$/iu;function IsIdnEmail(value){return IdnEmail.test(value)}function IsIdnHostname(value){if(value.length===0||value.includes(" "))return false;const canonical=value.normalize("NFC").replace(/[\u002E\u3002\uFF0E\uFF61]/g,".");if(canonical.length>253)return false;for(const label of canonical.split(".")){if(!IsIdnLabel(label))return false}return true}function IsIPv4Internal(value,start,end){let dots=0;let num=0;let digits=0;let leading=0;for(let i=start;i255||leading===48&&digits>1)return false;dots++;num=0;digits=0;leading=0}else if(ch>=48&&ch<=57){if(digits===0)leading=ch;num=num*10+(ch-48);digits++}else{return false}}return dots===3&&digits>0&&num<=255&&!(leading===48&&digits>1)}function IsIPv4(value){return IsIPv4Internal(value,0,value.length)}function InRange(ch){return ch>=48&&ch<=57||ch>=65&&ch<=70||ch>=97&&ch<=102}function IsIPv6(value){const length=value.length;if(length===0)return false;let groups=0;let compressed=false;let i=0;if(value.charCodeAt(0)===58&&value.charCodeAt(1)===58){if(length===2)return true;compressed=true;i=2}while(i4)return false;groups++;if(i===length)break;if(next!==58)return false;i++;if(value.charCodeAt(i)===58){if(compressed)return false;if(value.charCodeAt(i+1)===58)return false;compressed=true;i++;if(i===length)break}}return compressed?groups<=7:groups===8}function TryUrl(value){try{new URL(value,"http://example.com");return true}catch{return false}}function IsIriReference(value){if(value.includes(" ")){return false}if(value.includes("\\")){return false}if(/[\x00-\x1F\x7F]/.test(value)){return false}if(/%(?![0-9a-fA-F]{2})/.test(value)){return false}if(value===""){return true}const colonIndex=value.indexOf(":");const hasValidSchemePrefix=colonIndex>0&&/^[a-zA-Z][a-zA-Z0-9+\-.]*$/.test(value.substring(0,colonIndex));if(hasValidSchemePrefix){return TryUrl( +value)}else{const looksLikeMalformedSchemeAndAuthority=value.match(/^([a-zA-Z][a-zA-Z0-9+\-.]*)(\/\/)/);if(looksLikeMalformedSchemeAndAuthority&&colonIndex===-1){return false}return TryUrl(value)}}function IsIri(value){try{new URL(value);return true}catch{return false}}var JsonPointerUriFragment=/^#(?:\/(?:[a-z0-9_\-.!$&'()*+,;:=@]|%[0-9a-f]{2}|~0|~1)*)*$/i;function IsJsonPointerUriFragment(value){return JsonPointerUriFragment.test(value)}var JsonPointer=/^(?:\/(?:[^~/]|~0|~1)*)*$/;function IsJsonPointer(value){return JsonPointer.test(value)}function IsRegex(value){if(value.length===0){return false}try{new RegExp(value);return true}catch{return false}}var RelativeJsonPointer=/^(?:0|[1-9][0-9]*)(?:#|(?:\/(?:[^~/]|~0|~1)*)*)$/;function IsRelativeJsonPointer(value){return RelativeJsonPointer.test(value)}var UriReference=/^(?!.*[^\x00-\x7F])(?!.*\\)(?:(?:[a-z][a-z0-9+\-.]*:)?(?:\/\/[^\s[\]{}<>^`|]*)?|[^\s[\]{}<>^`|]*)(?:\?[^\s[\]{}<>^`|]*)?(?:#[^\s[\]{}<>^`|]*)?$/i;function IsUriReference(value){return UriReference.test(value)}var UriTemplate=/^(?:(?:[^\x00-\x20"'<>%\\^`{|}]|%[0-9a-f]{2})|\{[+#./;?&=,!@|]?(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?(?:,(?:[a-z0-9_]|%[0-9a-f]{2})+(?::[1-9][0-9]{0,3}|\*)?)*\})*$/i;function IsUriTemplate(value){return UriTemplate.test(value)}function IsAlpha(ch){return ch>=97&&ch<=122||ch>=65&&ch<=90}function IsAlphaNumeric(ch){return IsAlpha(ch)||ch>=48&&ch<=57}function IsHex(ch){return ch>=48&&ch<=57||ch>=65&&ch<=70||ch>=97&&ch<=102}function IsSchemeChar(ch){return IsAlphaNumeric(ch)||ch===43||ch===45||ch===46}function IsUnreserved(ch){return IsAlphaNumeric(ch)||ch===45||ch===46||ch===95||ch===126}function IsSubDelim(ch){return ch===33||ch===36||ch===38||ch===39||ch===40||ch===41||ch===42||ch===43||ch===44||ch===59||ch===61}function IsPchar(ch){ +return IsUnreserved(ch)||IsSubDelim(ch)||ch===58||ch===64}function IsUri(value){const length=value.length;if(length===0)return false;if(!IsAlpha(value.charCodeAt(0)))return false;let i=1;while(i=atPos||!IsHex(value.charCodeAt(j+1))||!IsHex(value.charCodeAt(j+2)))return false;j+=2}else if(!IsUnreserved(ch)&&!IsSubDelim(ch)&&ch!==58)return false}i=atPos+1}if(value.charCodeAt(i)===91){i++;while(i57)return false;i++}}}while(i=length||!IsHex(value.charCodeAt(i+1))||!IsHex(value.charCodeAt(i+2)))return false;i+=2}else if(ch>127){return false}else if(!(IsPchar(ch)||ch===47||ch===63||ch===35)){return false} +i++}return true}var Url=/^(?:https?|ftp):\/\/(?:\S+(?::\S*)?@)?(?:(?!(?:10|127)(?:\.\d{1,3}){3})(?!(?:169\.254|192\.168)(?:\.\d{1,3}){2})(?!172\.(?:1[6-9]|2\d|3[0-1])(?:\.\d{1,3}){2})(?:[1-9]\d?|1\d\d|2[01]\d|22[0-3])(?:\.(?:1?\d{1,2}|2[0-4]\d|25[0-5])){2}(?:\.(?:[1-9]\d?|1\d\d|2[0-4]\d|25[0-4]))|(?:(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)(?:\.(?:[a-z0-9\u{00a1}-\u{ffff}]+-)*[a-z0-9\u{00a1}-\u{ffff}]+)*(?:\.(?:[a-z\u{00a1}-\u{ffff}]{2,})))(?::\d{2,5})?(?:\/[^\s]*)?$/iu;function IsUrl(value){return Url. +test(value)}var Uuid=/^(?:urn:uuid:)?[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}$/i;function IsUuid(value){return Uuid.test(value)}var formats=new Map;function Clear(){formats.clear()}function Entries3(){return[...formats.entries()]}function Set3(format,check){formats.set(format,check)}function Has(format){return formats.has(format)}function Get3(format){return formats.get(format)}function Test(format,value){return formats.get(format)?.(value)??true}function Reset2(){Clear();formats.set("date-time",IsDateTime);formats.set("date",IsDate2);formats.set("duration",IsDuration);formats.set("email",IsEmail);formats.set("hostname", +IsHostname);formats.set("idn-email",IsIdnEmail);formats.set("idn-hostname",IsIdnHostname);formats.set("ipv4",IsIPv4);formats.set("ipv6",IsIPv6);formats.set("iri-reference",IsIriReference);formats.set("iri",IsIri);formats.set("json-pointer-uri-fragment",IsJsonPointerUriFragment);formats.set("json-pointer",IsJsonPointer);formats.set("regex",IsRegex);formats.set("relative-json-pointer",IsRelativeJsonPointer);formats.set("time",IsTime);formats.set("uri-reference",IsUriReference);formats.set("uri-tem\ +plate",IsUriTemplate);formats.set("uri",IsUri);formats.set("url",IsUrl);formats.set("uuid",IsUuid)}Reset2();function BuildFormat(_stack,_context,schema,value){return emit_exports.Call(emit_exports.Member("Format","Test"),[emit_exports.Constant(schema.format),value])}function CheckFormat(_stack,_context,schema,value){return format_exports.Test(schema.format,value)}function ErrorFormat(stack,context,schemaPath,instancePath,schema,value){return CheckFormat(stack,context,schema,value)||context.AddError({keyword:"format",schemaPath,instancePath,params:{format:schema.format}})}function BuildIf(stack,context,schema,value){const thenSchema=IsThen(schema)?schema.then:true;const elseSchema=IsElse(schema)?schema.else:true;return emit_exports.Ternary(BuildSchema(stack,context,schema.if,value),BuildSchema(stack,context,thenSchema,value),BuildSchema(stack,context,elseSchema,value))}function CheckIf(stack,context,schema,value){const thenSchema=IsThen(schema)?schema.then:true;const elseSchema=IsElse(schema)?schema.else:true;return CheckSchema(stack,context,schema.if,value)?CheckSchema( +stack,context,thenSchema,value):CheckSchema(stack,context,elseSchema,value)}function ErrorIf(stack,context,schemaPath,instancePath,schema,value){const thenSchema=IsThen(schema)?schema.then:true;const elseSchema=IsElse(schema)?schema.else:true;const trueContext=new AccumulatedErrorContext;const isIf=ErrorSchema(stack,trueContext,`${schemaPath}/if`,instancePath,schema.if,value)?ErrorSchema(stack,trueContext,`${schemaPath}/then`,instancePath,thenSchema,value)||context.AddError({keyword:"if",schemaPath, +instancePath,params:{failingKeyword:"then"}}):ErrorSchema(stack,context,`${schemaPath}/else`,instancePath,elseSchema,value)||context.AddError({keyword:"if",schemaPath,instancePath,params:{failingKeyword:"else"}});if(isIf)context.Merge([trueContext]);return isIf}function BuildItemsSized(stack,context,schema,value){return emit_exports.ReduceAnd(schema.items.map((schema2,index2)=>{const isLength=emit_exports.IsLessEqualThan(emit_exports.Member(value,"length"),emit_exports.Constant(index2));const isSchema=BuildSchemaPushStack(stack,context,schema2,`${value}[${index2}]`);const addIndex=context.AddIndex(emit_exports.Constant(index2));const guarded=context.UseUnevaluated()?emit_exports.And(isSchema,addIndex):isSchema;return emit_exports.Or(isLength,guarded)}))} +function CheckItemsSized(stack,context,schema,value){return guard_exports.Every(schema.items,0,(schema2,index2)=>{return guard_exports.IsLessEqualThan(value.length,index2)||CheckSchemaPushStack(stack,context,schema2,value[index2])&&context.AddIndex(index2)})}function ErrorItemsSized(stack,context,schemaPath,instancePath,schema,value){return guard_exports.EveryAll(schema.items,0,(schema2,index2)=>{const nextSchemaPath=`${schemaPath}/items/${index2}`;const nextInstancePath=`${instancePath}/${index2}`; +return guard_exports.IsLessEqualThan(value.length,index2)||ErrorSchemaPushStack(stack,context,nextSchemaPath,nextInstancePath,schema2,value[index2])&&context.AddIndex(index2)})}function BuildItemsUnsized(stack,context,schema,value){const offset=IsPrefixItems(schema)?schema.prefixItems.length:0;const isSchema=BuildSchemaPushStack(stack,context,schema.items,"element");const addIndex=context.AddIndex("index");const guarded=context.UseUnevaluated()?emit_exports.And(isSchema,addIndex):isSchema;return emit_exports. +Every(value,emit_exports.Constant(offset),["element","index"],guarded)}function CheckItemsUnsized(stack,context,schema,value){const offset=IsPrefixItems(schema)?schema.prefixItems.length:0;return guard_exports.Every(value,offset,(element,index2)=>{return CheckSchemaPushStack(stack,context,schema.items,element)&&context.AddIndex(index2)})}function ErrorItemsUnsized(stack,context,schemaPath,instancePath,schema,value){const offset=IsPrefixItems(schema)?schema.prefixItems.length:0;return guard_exports. +EveryAll(value,offset,(element,index2)=>{const nextSchemaPath=`${schemaPath}/items`;const nextInstancePath=`${instancePath}/${index2}`;return ErrorSchemaPushStack(stack,context,nextSchemaPath,nextInstancePath,schema.items,element)&&context.AddIndex(index2)})}function BuildItems(stack,context,schema,value){return IsItemsSized(schema)?BuildItemsSized(stack,context,schema,value):BuildItemsUnsized(stack,context,schema,value)}function CheckItems(stack,context,schema,value){return IsItemsSized(schema)? +CheckItemsSized(stack,context,schema,value):CheckItemsUnsized(stack,context,schema,value)}function ErrorItems(stack,context,schemaPath,instancePath,schema,value){return IsItemsSized(schema)?ErrorItemsSized(stack,context,schemaPath,instancePath,schema,value):ErrorItemsUnsized(stack,context,schemaPath,instancePath,schema,value)}function IsValid3(schema){return IsContains(schema)}function BuildMaxContains(stack,context,schema,value){if(!IsValid3(schema))return emit_exports.Constant(true);const[result,item]=[Unique(),Unique()];const count=emit_exports.Call(emit_exports.Member(value,"reduce"),[emit_exports.ArrowFunction([result,item],emit_exports.Ternary(BuildSchema(stack,context,schema.contains,item),emit_exports.PrefixIncrement(result),result)),emit_exports.Constant(0)]);return emit_exports.IsLessEqualThan(count,emit_exports. +Constant(schema.maxContains))}function CheckMaxContains(stack,context,schema,value){if(!IsValid3(schema))return true;const count=value.reduce((result,item)=>CheckSchema(stack,context,schema.contains,item)?++result:result,0);return guard_exports.IsLessEqualThan(count,schema.maxContains)}function ErrorMaxContains(stack,context,schemaPath,instancePath,schema,value){const minContains=IsMinContains(schema)?schema.minContains:1;return CheckMaxContains(stack,context,schema,value)||context.AddError({keyword:"\ +contains",schemaPath,instancePath,params:{minContains,maxContains:schema.maxContains}})}function BuildMaximum(_stack,_context,schema,value){return emit_exports.IsLessEqualThan(value,emit_exports.Constant(schema.maximum))}function CheckMaximum(_stack,_context,schema,value){return guard_exports.IsLessEqualThan(value,schema.maximum)}function ErrorMaximum(stack,context,schemaPath,instancePath,schema,value){return CheckMaximum(stack,context,schema,value)||context.AddError({keyword:"maximum",schemaPath,instancePath,params:{comparison:"<=",limit:schema.maximum}})}function BuildMaxItems(_stack,_context,schema,value){return emit_exports.IsLessEqualThan(emit_exports.Member(value,"length"),emit_exports.Constant(schema.maxItems))}function CheckMaxItems(_stack,_context,schema,value){return guard_exports.IsLessEqualThan(value.length,schema.maxItems)}function ErrorMaxItems(stack,context,schemaPath,instancePath,schema,value){return CheckMaxItems(stack,context,schema,value)||context.AddError({keyword:"maxItems",schemaPath,instancePath,params:{limit:schema.maxItems}})}function BuildMaxLength(_stack,_context,schema,value){return emit_exports.IsMaxLength(value,emit_exports.Constant(schema.maxLength))}function CheckMaxLength(_stack,_context,schema,value){return guard_exports.IsMaxLength(value,schema.maxLength)}function ErrorMaxLength(stack,context,schemaPath,instancePath,schema,value){return CheckMaxLength(stack,context,schema,value)||context.AddError({keyword:"maxLength",schemaPath,instancePath,params:{limit:schema.maxLength}})}function BuildMaxProperties(_stack,_context,schema,value){return emit_exports.IsLessEqualThan(emit_exports.Member(emit_exports.Keys(value),"length"),emit_exports.Constant(schema.maxProperties))}function CheckMaxProperties(_stack,_context,schema,value){return guard_exports.IsLessEqualThan(guard_exports.Keys(value).length,schema.maxProperties)}function ErrorMaxProperties(stack,context,schemaPath,instancePath,schema,value){return CheckMaxProperties(stack,context,schema,value)||context.AddError({keyword:"\ +maxProperties",schemaPath,instancePath,params:{limit:schema.maxProperties}})}function IsValid4(schema){return IsContains(schema)}function BuildMinContains(stack,context,schema,value){if(!IsValid4(schema))return emit_exports.Constant(true);const[result,item]=[Unique(),Unique()];const count=emit_exports.Call(emit_exports.Member(value,"reduce"),[emit_exports.ArrowFunction([result,item],emit_exports.Ternary(BuildSchema(stack,context,schema.contains,item),emit_exports.PrefixIncrement(result),result)),emit_exports.Constant(0)]);return emit_exports.IsGreaterEqualThan(count,emit_exports. +Constant(schema.minContains))}function CheckMinContains(stack,context,schema,value){if(!IsValid4(schema))return true;const count=value.reduce((result,item)=>CheckSchema(stack,context,schema.contains,item)?++result:result,0);return guard_exports.IsGreaterEqualThan(count,schema.minContains)}function ErrorMinContains(stack,context,schemaPath,instancePath,schema,value){return CheckMinContains(stack,context,schema,value)||context.AddError({keyword:"contains",schemaPath,instancePath,params:{minContains:schema. +minContains}})}function BuildMinimum(_stack,_context,schema,value){return emit_exports.IsGreaterEqualThan(value,emit_exports.Constant(schema.minimum))}function CheckMinimum(_stack,_context,schema,value){return guard_exports.IsGreaterEqualThan(value,schema.minimum)}function ErrorMinimum(stack,context,schemaPath,instancePath,schema,value){return CheckMinimum(stack,context,schema,value)||context.AddError({keyword:"minimum",schemaPath,instancePath,params:{comparison:">=",limit:schema.minimum}})}function BuildMinItems(_stack,_context,schema,value){return emit_exports.IsGreaterEqualThan(emit_exports.Member(value,"length"),emit_exports.Constant(schema.minItems))}function CheckMinItems(_stack,_context,schema,value){return guard_exports.IsGreaterEqualThan(value.length,schema.minItems)}function ErrorMinItems(stack,context,schemaPath,instancePath,schema,value){return CheckMinItems(stack,context,schema,value)||context.AddError({keyword:"minItems",schemaPath,instancePath,params:{limit:schema.minItems}})}function BuildMinLength(_stack,_context,schema,value){return emit_exports.IsMinLength(value,emit_exports.Constant(schema.minLength))}function CheckMinLength(_stack,_context,schema,value){return guard_exports.IsMinLength(value,schema.minLength)}function ErrorMinLength(stack,context,schemaPath,instancePath,schema,value){return CheckMinLength(stack,context,schema,value)||context.AddError({keyword:"minLength",schemaPath,instancePath,params:{limit:schema.minLength}})}function BuildMinProperties(_stack,_context,schema,value){return emit_exports.IsGreaterEqualThan(emit_exports.Member(emit_exports.Keys(value),"length"),emit_exports.Constant(schema.minProperties))}function CheckMinProperties(_stack,_context,schema,value){return guard_exports.IsGreaterEqualThan(guard_exports.Keys(value).length,schema.minProperties)}function ErrorMinProperties(stack,context,schemaPath,instancePath,schema,value){return CheckMinProperties(stack,context,schema,value)||context.AddError( +{keyword:"minProperties",schemaPath,instancePath,params:{limit:schema.minProperties}})}function BuildMultipleOf(_stack,_context,schema,value){return emit_exports.MultipleOf(value,emit_exports.Constant(schema.multipleOf))}function CheckMultipleOf(_stack,_context,schema,value){return guard_exports.IsMultipleOf(value,schema.multipleOf)}function ErrorMultipleOf(stack,context,schemaPath,instancePath,schema,value){return CheckMultipleOf(stack,context,schema,value)||context.AddError({keyword:"multipleOf",schemaPath,instancePath,params:{multipleOf:schema.multipleOf}})}function BuildNotUnevaluated(stack,context,schema,value){return Reducer(stack,context,[schema.not],value,emit_exports.Not(emit_exports.IsEqual(emit_exports.Member("results","length"),emit_exports.Constant(1))))}function BuildNotFast(stack,context,schema,value){return emit_exports.Not(BuildSchema(stack,context,schema.not,value))}function BuildNot(stack,context,schema,value){return context.UseUnevaluated()?BuildNotUnevaluated(stack,context,schema,value):BuildNotFast(stack,context,schema,value)}function CheckNot(stack,context,schema,value){ +const nextContext=new CheckContext;const isSchema=!CheckSchema(stack,nextContext,schema.not,value);const isNot=isSchema&&context.Merge([nextContext]);return isNot}function ErrorNot(stack,context,schemaPath,instancePath,schema,value){return CheckNot(stack,context,schema,value)||context.AddError({keyword:"not",schemaPath,instancePath,params:{}})}function BuildOneOfUnevaluated(stack,context,schema,value){return Reducer(stack,context,schema.oneOf,value,emit_exports.IsEqual(emit_exports.Member("results","length"),emit_exports.Constant(1)))}function BuildOneOfFast(stack,context,schema,value){const results=emit_exports.ArrayLiteral(schema.oneOf.map(schema2=>BuildSchema(stack,context,schema2,value)));const count=emit_exports.Call(emit_exports.Member(results,"reduce"),[emit_exports.ArrowFunction(["count","result"],emit_exports.Ternary(emit_exports. +IsEqual("result",emit_exports.Constant(true)),emit_exports.PrefixIncrement("count"),"count")),emit_exports.Constant(0)]);return emit_exports.IsEqual(count,emit_exports.Constant(1))}function BuildOneOf(stack,context,schema,value){return context.UseUnevaluated()?BuildOneOfUnevaluated(stack,context,schema,value):BuildOneOfFast(stack,context,schema,value)}function CheckOneOf(stack,context,schema,value){const passedContexts=schema.oneOf.reduce((result,schema2)=>{const nextContext=new CheckContext;return CheckSchema( +stack,nextContext,schema2,value)?[...result,nextContext]:result},[]);return guard_exports.IsEqual(passedContexts.length,1)&&context.Merge(passedContexts)}function ErrorOneOf(stack,context,schemaPath,instancePath,schema,value){const failedContexts=[];const passingSchemas=[];const passedContexts=schema.oneOf.reduce((result,schema2,index2)=>{const nextContext=new AccumulatedErrorContext;const nextSchemaPath=`${schemaPath}/oneOf/${index2}`;const isSchema=ErrorSchema(stack,nextContext,nextSchemaPath, +instancePath,schema2,value);if(isSchema)passingSchemas.push(index2);if(!isSchema)failedContexts.push(nextContext);return isSchema?[...result,nextContext]:result},[]);const isOneOf=guard_exports.IsEqual(passedContexts.length,1)&&context.Merge(passedContexts);if(!isOneOf&&guard_exports.IsEqual(passingSchemas.length,0))failedContexts.forEach(failed=>failed.GetErrors().forEach(error=>context.AddError(error)));return isOneOf||context.AddError({keyword:"oneOf",schemaPath,instancePath,params:{passingSchemas}})}function BuildPattern(_stack,_context,schema,value){const regexp=CreateVariable(guard_exports.IsString(schema.pattern)?new RegExp(schema.pattern,"u"):schema.pattern);return emit_exports.Call(emit_exports.Member(regexp,"test"),[value])}function CheckPattern(_stack,_context,schema,value){const regexp=guard_exports.IsString(schema.pattern)?new RegExp(schema.pattern,"u"):schema.pattern;return regexp.test(value)}function ErrorPattern(stack,context,schemaPath,instancePath,schema,value){return CheckPattern( +stack,context,schema,value)||context.AddError({keyword:"pattern",schemaPath,instancePath,params:{pattern:schema.pattern}})}function BuildPatternProperties(stack,context,schema,value){return emit_exports.ReduceAnd(guard_exports.Entries(schema.patternProperties).map(([pattern,schema2])=>{const[key,prop]=[Unique(),Unique()];const regexp=CreateVariable(new RegExp(pattern,"u"));const notKey=emit_exports.Not(emit_exports.Call(emit_exports.Member(regexp,"test"),[key]));const isSchema=BuildSchemaPushStack(stack,context,schema2,prop);const addKey=context.AddKey(key);const guarded=context.UseUnevaluated()?emit_exports.Or(notKey, +emit_exports.And(isSchema,addKey)):emit_exports.Or(notKey,isSchema);return emit_exports.Every(emit_exports.Entries(value),emit_exports.Constant(0),[`[${key}, ${prop}]`,"_"],guarded)}))}function CheckPatternProperties(stack,context,schema,value){return guard_exports.Every(guard_exports.Entries(schema.patternProperties),0,([pattern,schema2])=>{const regexp=new RegExp(pattern,"u");return guard_exports.Every(guard_exports.Entries(value),0,([key,prop])=>{return!regexp.test(key)||CheckSchemaPushStack( +stack,context,schema2,prop)&&context.AddKey(key)})})}function ErrorPatternProperties(stack,context,schemaPath,instancePath,schema,value){return guard_exports.EveryAll(guard_exports.Entries(schema.patternProperties),0,([pattern,schema2])=>{const nextSchemaPath=`${schemaPath}/patternProperties/${pattern}`;const regexp=new RegExp(pattern,"u");return guard_exports.EveryAll(guard_exports.Entries(value),0,([key,value2])=>{const nextInstancePath=`${instancePath}/${key}`;const notKey=!regexp.test(key);return notKey|| +ErrorSchemaPushStack(stack,context,nextSchemaPath,nextInstancePath,schema2,value2)&&context.AddKey(key)})})}function BuildPrefixItems(stack,context,schema,value){return emit_exports.ReduceAnd(schema.prefixItems.map((schema2,index2)=>{const isLength=emit_exports.IsLessEqualThan(emit_exports.Member(value,"length"),emit_exports.Constant(index2));const isSchema=BuildSchemaPushStack(stack,context,schema2,`${value}[${index2}]`);const addIndex=context.AddIndex(emit_exports.Constant(index2));const guarded=context.UseUnevaluated()?emit_exports.And(isSchema,addIndex):isSchema;return emit_exports.Or(isLength,guarded)}))} +function CheckPrefixItems(stack,context,schema,value){return guard_exports.IsEqual(value.length,0)||guard_exports.Every(schema.prefixItems,0,(schema2,index2)=>{return guard_exports.IsLessEqualThan(value.length,index2)||CheckSchemaPushStack(stack,context,schema2,value[index2])&&context.AddIndex(index2)})}function ErrorPrefixItems(stack,context,schemaPath,instancePath,schema,value){return guard_exports.IsEqual(value.length,0)||guard_exports.EveryAll(schema.prefixItems,0,(schema2,index2)=>{const nextSchemaPath=`${schemaPath}\ +/prefixItems/${index2}`;const nextInstancePath=`${instancePath}/${index2}`;return guard_exports.IsLessEqualThan(value.length,index2)||ErrorSchemaPushStack(stack,context,nextSchemaPath,nextInstancePath,schema2,value[index2])&&context.AddIndex(index2)})}function IsExactOptional(required,key){return required.includes(key)||settings_exports.Get().exactOptionalPropertyTypes}function InexactOptionalBuild(value,key){return emit_exports.IsUndefined(emit_exports.Member(value,key))}function InexactOptionalCheck(value,key){return guard_exports.IsUndefined(value[key])}function BuildProperties(stack,context,schema,value){const required=IsRequired(schema)?schema.required:[];const everyKey=guard_exports.Entries(schema.properties).map(([key,schema2])=>{const notKey=emit_exports.Not(emit_exports.HasPropertyKey(value,emit_exports.Constant(key)));const isSchema=BuildSchemaPushStack(stack,context,schema2,emit_exports.Member(value,key));const addKey=context.AddKey(emit_exports.Constant(key));const guarded=context.UseUnevaluated()?emit_exports.And(isSchema,addKey):isSchema; +const isProperty=required.includes(key)?guarded:emit_exports.Or(notKey,guarded);return IsExactOptional(required,key)?isProperty:emit_exports.Or(InexactOptionalBuild(value,key),isProperty)});return emit_exports.ReduceAnd(everyKey)}function CheckProperties(stack,context,schema,value){const required=IsRequired(schema)?schema.required:[];const isProperties=guard_exports.Every(guard_exports.Entries(schema.properties),0,([key,schema2])=>{const isProperty=!guard_exports.HasPropertyKey(value,key)||CheckSchemaPushStack( +stack,context,schema2,value[key])&&context.AddKey(key);return IsExactOptional(required,key)?isProperty:InexactOptionalCheck(value,key)||isProperty});return isProperties}function ErrorProperties(stack,context,schemaPath,instancePath,schema,value){const required=IsRequired(schema)?schema.required:[];const isProperties=guard_exports.EveryAll(guard_exports.Entries(schema.properties),0,([key,schema2])=>{const nextSchemaPath=`${schemaPath}/properties/${key}`;const nextInstancePath=`${instancePath}/${key}`; +const isProperty=()=>!guard_exports.HasPropertyKey(value,key)||ErrorSchemaPushStack(stack,context,nextSchemaPath,nextInstancePath,schema2,value[key])&&context.AddKey(key);return IsExactOptional(required,key)?isProperty():InexactOptionalCheck(value,key)||isProperty()});return isProperties}function BuildPropertyNames(stack,context,schema,value){const[key,_index]=[Unique(),Unique()];return emit_exports.Every(emit_exports.Keys(value),emit_exports.Constant(0),[key,_index],BuildSchema(stack,context,schema.propertyNames,key))}function CheckPropertyNames(stack,context,schema,value){return guard_exports.Every(guard_exports.Keys(value),0,(key,_index)=>CheckSchema(stack,context,schema.propertyNames,key))}function ErrorPropertyNames(stack,context,schemaPath,instancePath,schema,value){const propertyNames=[]; +const isPropertyNames=guard_exports.EveryAll(guard_exports.Keys(value),0,(key,_index)=>{const nextInstancePath=`${instancePath}/${key}`;const nextSchemaPath=`${schemaPath}/propertyNames`;const nextContext=new AccumulatedErrorContext;const isPropertyName=ErrorSchema(stack,nextContext,nextSchemaPath,nextInstancePath,schema.propertyNames,key);if(!isPropertyName)propertyNames.push(key);return isPropertyName});return isPropertyNames||context.AddError({keyword:"propertyNames",schemaPath,instancePath,params:{ +propertyNames}})}function BuildRecursiveRef(stack,context,schema,value){const target=stack.RecursiveRef(schema)??false;return CreateFunction(stack,context,target,value)}function CheckRecursiveRef(stack,context,schema,value){const target=stack.RecursiveRef(schema)??false;return IsSchema2(target)&&CheckSchema(stack,context,target,value)}function ErrorRecursiveRef(stack,context,_schemaPath,instancePath,schema,value){const target=stack.RecursiveRef(schema)??false;return IsSchema2(target)&&ErrorSchema(stack,context,"\ +#",instancePath,target,value)}function BuildRefStandard(stack,context,target,value){const interior=emit_exports.ArrowFunction(["context","value"],CreateFunction(stack,context,target,"value"));const exterior=emit_exports.ArrowFunction(["context","value"],emit_exports.Statements([emit_exports.ConstDeclaration("nextContext",emit_exports.New("CheckContext",[])),emit_exports.ConstDeclaration("result",emit_exports.Call(interior,["nextContext","value"])),emit_exports.If("result",context.Merge("[nextContext]")),emit_exports.Return("\ +result")]));return emit_exports.Call(exterior,["context",value])}function BuildRefFast(stack,context,target,value){return CreateFunction(stack,context,target,value)}function BuildRef(stack,context,schema,value){const target=stack.Ref(schema)??false;return context.UseUnevaluated()?BuildRefStandard(stack,context,target,value):BuildRefFast(stack,context,target,value)}function CheckRef(stack,context,schema,value){const target=stack.Ref(schema)??false;const nextContext=new CheckContext;const result=IsSchema2( +target)&&CheckSchema(stack,nextContext,target,value);if(result)context.Merge([nextContext]);return result}function ErrorRef(stack,context,_schemaPath,instancePath,schema,value){const target=stack.Ref(schema)??false;const nextContext=new AccumulatedErrorContext;const result=IsSchema2(target)&&ErrorSchema(stack,nextContext,"#",instancePath,target,value);if(result)context.Merge([nextContext]);if(!result)nextContext.GetErrors().forEach(error=>context.AddError(error));return result}function BuildRequired(_stack,_context,schema,value){return emit_exports.ReduceAnd(schema.required.map(key=>emit_exports.HasPropertyKey(value,emit_exports.Constant(key))))}function CheckRequired(_stack,_context,schema,value){return guard_exports.Every(schema.required,0,key=>guard_exports.HasPropertyKey(value,key))}function ErrorRequired(_stack,context,schemaPath,instancePath,schema,value){const requiredProperties=[];const isRequired=guard_exports.EveryAll(schema.required,0,key=>{const hasKey=guard_exports. +HasPropertyKey(value,key);if(!hasKey)requiredProperties.push(key);return hasKey});return isRequired||context.AddError({keyword:"required",schemaPath,instancePath,params:{requiredProperties}})}function BuildTypeName(_stack,_context,type,value){return guard_exports.IsEqual(type,"object")?emit_exports.IsObjectNotArray(value):guard_exports.IsEqual(type,"array")?emit_exports.IsArray(value):guard_exports.IsEqual(type,"boolean")?emit_exports.IsBoolean(value):guard_exports.IsEqual(type,"integer")?emit_exports.IsInteger(value):guard_exports.IsEqual(type,"number")?emit_exports.IsNumber(value):guard_exports.IsEqual(type,"null")?emit_exports.IsNull(value):guard_exports.IsEqual(type,"string")?emit_exports. +IsString(value):guard_exports.IsEqual(type,"asyncIterator")?emit_exports.IsAsyncIterator(value):guard_exports.IsEqual(type,"bigint")?emit_exports.IsBigInt(value):guard_exports.IsEqual(type,"constructor")?emit_exports.IsConstructor(value):guard_exports.IsEqual(type,"function")?emit_exports.IsFunction(value):guard_exports.IsEqual(type,"iterator")?emit_exports.IsIterator(value):guard_exports.IsEqual(type,"symbol")?emit_exports.IsSymbol(value):guard_exports.IsEqual(type,"undefined")?emit_exports.IsUndefined( +value):guard_exports.IsEqual(type,"void")?emit_exports.IsUndefined(value):emit_exports.Constant(true)}function CheckTypeName(_stack,_context,type,_schema,value){return guard_exports.IsEqual(type,"object")?guard_exports.IsObjectNotArray(value):guard_exports.IsEqual(type,"array")?guard_exports.IsArray(value):guard_exports.IsEqual(type,"boolean")?guard_exports.IsBoolean(value):guard_exports.IsEqual(type,"integer")?guard_exports.IsInteger(value):guard_exports.IsEqual(type,"number")?guard_exports.IsNumber( +value):guard_exports.IsEqual(type,"null")?guard_exports.IsNull(value):guard_exports.IsEqual(type,"string")?guard_exports.IsString(value):guard_exports.IsEqual(type,"asyncIterator")?guard_exports.IsAsyncIterator(value):guard_exports.IsEqual(type,"bigint")?guard_exports.IsBigInt(value):guard_exports.IsEqual(type,"constructor")?guard_exports.IsConstructor(value):guard_exports.IsEqual(type,"function")?guard_exports.IsFunction(value):guard_exports.IsEqual(type,"iterator")?guard_exports.IsIterator(value): +guard_exports.IsEqual(type,"symbol")?guard_exports.IsSymbol(value):guard_exports.IsEqual(type,"undefined")?guard_exports.IsUndefined(value):guard_exports.IsEqual(type,"void")?guard_exports.IsUndefined(value):true}function BuildTypeNames(stack,context,typenames,value){return emit_exports.ReduceOr(typenames.map(type=>BuildTypeName(stack,context,type,value)))}function CheckTypeNames(stack,context,types,schema,value){return types.some(type=>CheckTypeName(stack,context,type,schema,value))}function BuildType(stack,context,schema,value){ +return guard_exports.IsArray(schema.type)?BuildTypeNames(stack,context,schema.type,value):BuildTypeName(stack,context,schema.type,value)}function CheckType(stack,context,schema,value){return guard_exports.IsArray(schema.type)?CheckTypeNames(stack,context,schema.type,schema,value):CheckTypeName(stack,context,schema.type,schema,value)}function ErrorType(stack,context,schemaPath,instancePath,schema,value){const isType=guard_exports.IsArray(schema.type)?CheckTypeNames(stack,context,schema.type,schema, +value):CheckTypeName(stack,context,schema.type,schema,value);return isType||context.AddError({keyword:"type",schemaPath,instancePath,params:{type:schema.type}})}function BuildUnevaluatedItems(stack,context,schema,value){const[index2,item]=[Unique(),Unique()];const indices=emit_exports.Call(emit_exports.Member("context","GetIndices"),[]);const hasIndex=emit_exports.Call(emit_exports.Member("indices","has"),[index2]);const isSchema=BuildSchema(stack,context,schema.unevaluatedItems,item);const addIndex=emit_exports.Call(emit_exports.Member("context","AddIndex"),[index2]);const isEvery=emit_exports.Every(value,emit_exports.Constant(0),[item,index2],emit_exports. +And(emit_exports.Or(hasIndex,isSchema),addIndex));return emit_exports.Call(emit_exports.ArrowFunction(["context"],emit_exports.Statements([emit_exports.ConstDeclaration("indices",indices),emit_exports.Return(isEvery)])),["context"])}function CheckUnevaluatedItems(stack,context,schema,value){const indices=context.GetIndices();return guard_exports.Every(value,0,(item,index2)=>{return(indices.has(index2)||CheckSchema(stack,context,schema.unevaluatedItems,item))&&context.AddIndex(index2)})}function ErrorUnevaluatedItems(stack,context,schemaPath,instancePath,schema,value){ +const indices=context.GetIndices();const unevaluatedItems=[];const isUnevaluatedItems=guard_exports.EveryAll(value,0,(item,index2)=>{const nextContext=new AccumulatedErrorContext;const isEvaluatedItem=(indices.has(index2)||ErrorSchema(stack,nextContext,schemaPath,instancePath,schema.unevaluatedItems,item))&&context.AddIndex(index2);if(!isEvaluatedItem)unevaluatedItems.push(index2);return isEvaluatedItem});return isUnevaluatedItems||context.AddError({keyword:"unevaluatedItems",schemaPath,instancePath, +params:{unevaluatedItems}})}function BuildUnevaluatedProperties(stack,context,schema,value){const[key,prop]=[Unique(),Unique()];const keys=emit_exports.Call(emit_exports.Member("context","GetKeys"),[]);const hasKey=emit_exports.Call(emit_exports.Member("keys","has"),[key]);const addKey=emit_exports.Call(emit_exports.Member("context","AddKey"),[key]);const isSchema=BuildSchema(stack,context,schema.unevaluatedProperties,prop);const isEvery=emit_exports.Every(emit_exports.Entries(value),emit_exports.Constant(0),[`[${key}, ${prop}\ +]`,"_"],emit_exports.Or(hasKey,emit_exports.And(isSchema,addKey)));return emit_exports.Call(emit_exports.ArrowFunction(["context"],emit_exports.Statements([emit_exports.ConstDeclaration("keys",keys),emit_exports.Return(isEvery)])),["context"])}function CheckUnevaluatedProperties(stack,context,schema,value){const keys=context.GetKeys();return guard_exports.Every(guard_exports.Entries(value),0,([key,prop])=>{return keys.has(key)||CheckSchema(stack,context,schema.unevaluatedProperties,prop)&&context. +AddKey(key)})}function ErrorUnevaluatedProperties(stack,context,schemaPath,instancePath,schema,value){const keys=context.GetKeys();const unevaluatedProperties=[];const isUnevaluatedProperties=guard_exports.EveryAll(guard_exports.Entries(value),0,([key,prop])=>{const nextContext=new AccumulatedErrorContext;const isEvaluatedProperty=keys.has(key)||ErrorSchema(stack,nextContext,schemaPath,instancePath,schema.unevaluatedProperties,prop)&&context.AddKey(key);if(!isEvaluatedProperty)unevaluatedProperties. +push(key);return isEvaluatedProperty});return isUnevaluatedProperties||context.AddError({keyword:"unevaluatedProperties",schemaPath,instancePath,params:{unevaluatedProperties}})}function IsValid5(schema){return!guard_exports.IsEqual(schema.uniqueItems,false)}function BuildUniqueItems(_stack,_context,schema,value){if(!IsValid5(schema))return emit_exports.Constant(true);const set=emit_exports.Member(emit_exports.New("Set",[emit_exports.Call(emit_exports.Member(value,"map"),[emit_exports.Member("Hashing","Hash")])]),"size");const isLength=emit_exports.Member(value,"length");return emit_exports.IsEqual(set,isLength)}function CheckUniqueItems(_stack,_context,schema,value){if(!IsValid5( +schema))return true;const set=new Set(value.map(hash_exports.Hash)).size;const isLength=value.length;return guard_exports.IsEqual(set,isLength)}function ErrorUniqueItems(_stack,context,schemaPath,instancePath,schema,value){if(!IsValid5(schema))return true;const set=new Set;const duplicateItems=value.reduce((result,value2,index2)=>{const hash=hash_exports.Hash(value2);if(set.has(hash))return[...result,index2];set.add(hash);return result},[]);const isUniqueItems=guard_exports.IsEqual(duplicateItems. +length,0);return isUniqueItems||context.AddError({keyword:"uniqueItems",schemaPath,instancePath,params:{duplicateItems}})}function HasTypeName(schema,typename){return IsType(schema)&&(guard_exports.IsArray(schema.type)&&schema.type.includes(typename)||guard_exports.IsEqual(schema.type,typename))}function HasObjectType(schema){return HasTypeName(schema,"object")}function HasObjectKeywords(schema){return IsSchemaObject(schema)&&(IsAdditionalProperties(schema)||IsDependencies(schema)||IsDependentRequired(schema)||IsDependentSchemas(schema)||IsProperties(schema)||IsPatternProperties(schema)||IsPropertyNames(schema)||IsMinProperties( +schema)||IsMaxProperties(schema)||IsRequired(schema)||IsUnevaluatedProperties(schema))}function HasArrayType(schema){return HasTypeName(schema,"array")}function HasArrayKeywords(schema){return IsSchemaObject(schema)&&(IsAdditionalItems(schema)||IsItems(schema)||IsContains(schema)||IsMaxContains(schema)||IsMaxItems(schema)||IsMinContains(schema)||IsMinItems(schema)||IsPrefixItems(schema)||IsUnevaluatedItems(schema)||IsUniqueItems(schema))}function HasStringType(schema){return HasTypeName(schema,"\ +string")}function HasStringKeywords(schema){return IsSchemaObject(schema)&&(IsMinLength4(schema)||IsMaxLength4(schema)||IsFormat(schema)||IsPattern(schema))}function HasNumberType(schema){return HasTypeName(schema,"number")||HasTypeName(schema,"bigint")}function HasNumberKeywords(schema){return IsSchemaObject(schema)&&(IsMinimum(schema)||IsMaximum(schema)||IsExclusiveMaximum(schema)||IsExclusiveMinimum(schema)||IsMultipleOf2(schema))}function BuildSchemaPushStack(stack,context,schema,value){return context. +UseUnevaluated()?emit_exports.And(emit_exports.And(context.Push(),BuildSchema(stack,context,schema,value)),context.Pop()):BuildSchema(stack,context,schema,value)}function BuildSchema(stack,context,schema,value){stack.Push(schema);const conditions=[];if(IsBooleanSchema(schema))return BuildBooleanSchema(stack,context,schema,value);if(IsType(schema))conditions.push(BuildType(stack,context,schema,value));if(HasObjectKeywords(schema)){const constraints=[];if(IsRequired(schema))constraints.push(BuildRequired( +stack,context,schema,value));if(IsAdditionalProperties(schema))constraints.push(BuildAdditionalProperties(stack,context,schema,value));if(IsDependencies(schema))constraints.push(BuildDependencies(stack,context,schema,value));if(IsDependentRequired(schema))constraints.push(BuildDependentRequired(stack,context,schema,value));if(IsDependentSchemas(schema))constraints.push(BuildDependentSchemas(stack,context,schema,value));if(IsPatternProperties(schema))constraints.push(BuildPatternProperties(stack, +context,schema,value));if(IsProperties(schema))constraints.push(BuildProperties(stack,context,schema,value));if(IsPropertyNames(schema))constraints.push(BuildPropertyNames(stack,context,schema,value));if(IsMinProperties(schema))constraints.push(BuildMinProperties(stack,context,schema,value));if(IsMaxProperties(schema))constraints.push(BuildMaxProperties(stack,context,schema,value));const reduced=emit_exports.ReduceAnd(constraints);const guarded=emit_exports.Or(emit_exports.Not(emit_exports.IsObjectNotArray( +value)),reduced);conditions.push(HasObjectType(schema)?reduced:guarded)}if(HasArrayKeywords(schema)){const constraints=[];if(IsAdditionalItems(schema))constraints.push(BuildAdditionalItems(stack,context,schema,value));if(IsContains(schema))constraints.push(BuildContains(stack,context,schema,value));if(IsItems(schema))constraints.push(BuildItems(stack,context,schema,value));if(IsMaxContains(schema))constraints.push(BuildMaxContains(stack,context,schema,value));if(IsMaxItems(schema))constraints.push( +BuildMaxItems(stack,context,schema,value));if(IsMinContains(schema))constraints.push(BuildMinContains(stack,context,schema,value));if(IsMinItems(schema))constraints.push(BuildMinItems(stack,context,schema,value));if(IsPrefixItems(schema))constraints.push(BuildPrefixItems(stack,context,schema,value));if(IsUniqueItems(schema))constraints.push(BuildUniqueItems(stack,context,schema,value));const reduced=emit_exports.ReduceAnd(constraints);const guarded=emit_exports.Or(emit_exports.Not(emit_exports.IsArray( +value)),reduced);conditions.push(HasArrayType(schema)?reduced:guarded)}if(HasStringKeywords(schema)){const constraints=[];if(IsMaxLength4(schema))constraints.push(BuildMaxLength(stack,context,schema,value));if(IsMinLength4(schema))constraints.push(BuildMinLength(stack,context,schema,value));if(IsFormat(schema))constraints.push(BuildFormat(stack,context,schema,value));if(IsPattern(schema))constraints.push(BuildPattern(stack,context,schema,value));const reduced=emit_exports.ReduceAnd(constraints); +const guarded=emit_exports.Or(emit_exports.Not(emit_exports.IsString(value)),reduced);conditions.push(HasStringType(schema)?reduced:guarded)}if(HasNumberKeywords(schema)){const constraints=[];if(IsExclusiveMaximum(schema))constraints.push(BuildExclusiveMaximum(stack,context,schema,value));if(IsExclusiveMinimum(schema))constraints.push(BuildExclusiveMinimum(stack,context,schema,value));if(IsMaximum(schema))constraints.push(BuildMaximum(stack,context,schema,value));if(IsMinimum(schema))constraints. +push(BuildMinimum(stack,context,schema,value));if(IsMultipleOf2(schema))constraints.push(BuildMultipleOf(stack,context,schema,value));const reduced=emit_exports.ReduceAnd(constraints);const guarded=emit_exports.Or(emit_exports.Not(emit_exports.Or(emit_exports.IsNumber(value),emit_exports.IsBigInt(value))),reduced);conditions.push(HasNumberType(schema)?reduced:guarded)}if(IsRef2(schema))conditions.push(BuildRef(stack,context,schema,value));if(IsRecursiveRef(schema))conditions.push(BuildRecursiveRef( +stack,context,schema,value));if(IsDynamicRef(schema))conditions.push(BuildDynamicRef(stack,context,schema,value));if(IsGuard2(schema))conditions.push(BuildGuard(stack,context,schema,value));if(IsConst(schema))conditions.push(BuildConst(stack,context,schema,value));if(IsEnum2(schema))conditions.push(BuildEnum(stack,context,schema,value));if(IsIf(schema))conditions.push(BuildIf(stack,context,schema,value));if(IsNot(schema))conditions.push(BuildNot(stack,context,schema,value));if(IsAllOf(schema))conditions. +push(BuildAllOf(stack,context,schema,value));if(IsAnyOf(schema))conditions.push(BuildAnyOf(stack,context,schema,value));if(IsOneOf(schema))conditions.push(BuildOneOf(stack,context,schema,value));if(IsUnevaluatedItems(schema))conditions.push(emit_exports.Or(emit_exports.Not(emit_exports.IsArray(value)),BuildUnevaluatedItems(stack,context,schema,value)));if(IsUnevaluatedProperties(schema))conditions.push(emit_exports.Or(emit_exports.Not(emit_exports.IsObject(value)),BuildUnevaluatedProperties(stack, +context,schema,value)));if(IsRefine2(schema))conditions.push(BuildRefine(stack,context,schema,value));const result=emit_exports.ReduceAnd(conditions);stack.Pop(schema);return result}function CheckSchemaPushStack(stack,context,schema,value){return context.Push()&&CheckSchema(stack,context,schema,value)&&context.Pop()}function CheckSchema(stack,context,schema,value){stack.Push(schema);const result=IsBooleanSchema(schema)?CheckBooleanSchema(stack,context,schema,value):(!IsType(schema)||CheckType(stack, +context,schema,value))&&(!(guard_exports.IsObject(value)&&!guard_exports.IsArray(value))||(!IsRequired(schema)||CheckRequired(stack,context,schema,value))&&(!IsAdditionalProperties(schema)||CheckAdditionalProperties(stack,context,schema,value))&&(!IsDependencies(schema)||CheckDependencies(stack,context,schema,value))&&(!IsDependentRequired(schema)||CheckDependentRequired(stack,context,schema,value))&&(!IsDependentSchemas(schema)||CheckDependentSchemas(stack,context,schema,value))&&(!IsPatternProperties( +schema)||CheckPatternProperties(stack,context,schema,value))&&(!IsProperties(schema)||CheckProperties(stack,context,schema,value))&&(!IsPropertyNames(schema)||CheckPropertyNames(stack,context,schema,value))&&(!IsMinProperties(schema)||CheckMinProperties(stack,context,schema,value))&&(!IsMaxProperties(schema)||CheckMaxProperties(stack,context,schema,value)))&&(!guard_exports.IsArray(value)||(!IsAdditionalItems(schema)||CheckAdditionalItems(stack,context,schema,value))&&(!IsContains(schema)||CheckContains( +stack,context,schema,value))&&(!IsItems(schema)||CheckItems(stack,context,schema,value))&&(!IsMaxContains(schema)||CheckMaxContains(stack,context,schema,value))&&(!IsMaxItems(schema)||CheckMaxItems(stack,context,schema,value))&&(!IsMinContains(schema)||CheckMinContains(stack,context,schema,value))&&(!IsMinItems(schema)||CheckMinItems(stack,context,schema,value))&&(!IsPrefixItems(schema)||CheckPrefixItems(stack,context,schema,value))&&(!IsUniqueItems(schema)||CheckUniqueItems(stack,context,schema, +value)))&&(!guard_exports.IsString(value)||(!IsMaxLength4(schema)||CheckMaxLength(stack,context,schema,value))&&(!IsMinLength4(schema)||CheckMinLength(stack,context,schema,value))&&(!IsFormat(schema)||CheckFormat(stack,context,schema,value))&&(!IsPattern(schema)||CheckPattern(stack,context,schema,value)))&&(!(guard_exports.IsNumber(value)||guard_exports.IsBigInt(value))||(!IsExclusiveMaximum(schema)||CheckExclusiveMaximum(stack,context,schema,value))&&(!IsExclusiveMinimum(schema)||CheckExclusiveMinimum( +stack,context,schema,value))&&(!IsMaximum(schema)||CheckMaximum(stack,context,schema,value))&&(!IsMinimum(schema)||CheckMinimum(stack,context,schema,value))&&(!IsMultipleOf2(schema)||CheckMultipleOf(stack,context,schema,value)))&&(!IsRef2(schema)||CheckRef(stack,context,schema,value))&&(!IsRecursiveRef(schema)||CheckRecursiveRef(stack,context,schema,value))&&(!IsDynamicRef(schema)||CheckDynamicRef(stack,context,schema,value))&&(!IsGuard2(schema)||CheckGuard(stack,context,schema,value))&&(!IsConst( +schema)||CheckConst(stack,context,schema,value))&&(!IsEnum2(schema)||CheckEnum(stack,context,schema,value))&&(!IsIf(schema)||CheckIf(stack,context,schema,value))&&(!IsNot(schema)||CheckNot(stack,context,schema,value))&&(!IsAllOf(schema)||CheckAllOf(stack,context,schema,value))&&(!IsAnyOf(schema)||CheckAnyOf(stack,context,schema,value))&&(!IsOneOf(schema)||CheckOneOf(stack,context,schema,value))&&(!IsUnevaluatedItems(schema)||(!guard_exports.IsArray(value)||CheckUnevaluatedItems(stack,context,schema, +value)))&&(!IsUnevaluatedProperties(schema)||(!guard_exports.IsObject(value)||CheckUnevaluatedProperties(stack,context,schema,value)))&&(!IsRefine2(schema)||CheckRefine(stack,context,schema,value));stack.Pop(schema);return result}function ErrorSchemaPushStack(stack,context,schemaPath,instancePath,schema,value){return context.Push()&&ErrorSchema(stack,context,schemaPath,instancePath,schema,value)&&context.Pop()}function ErrorSchema(stack,context,schemaPath,instancePath,schema,value){stack.Push(schema); +const result=IsBooleanSchema(schema)?ErrorBooleanSchema(stack,context,schemaPath,instancePath,schema,value):!!(+(!IsType(schema)||ErrorType(stack,context,schemaPath,instancePath,schema,value))&+(!(guard_exports.IsObject(value)&&!guard_exports.IsArray(value))||!!(+(!IsRequired(schema)||ErrorRequired(stack,context,schemaPath,instancePath,schema,value))&+(!IsAdditionalProperties(schema)||ErrorAdditionalProperties(stack,context,schemaPath,instancePath,schema,value))&+(!IsDependencies(schema)||ErrorDependencies( +stack,context,schemaPath,instancePath,schema,value))&+(!IsDependentRequired(schema)||ErrorDependentRequired(stack,context,schemaPath,instancePath,schema,value))&+(!IsDependentSchemas(schema)||ErrorDependentSchemas(stack,context,schemaPath,instancePath,schema,value))&+(!IsPatternProperties(schema)||ErrorPatternProperties(stack,context,schemaPath,instancePath,schema,value))&+(!IsProperties(schema)||ErrorProperties(stack,context,schemaPath,instancePath,schema,value))&+(!IsPropertyNames(schema)||ErrorPropertyNames( +stack,context,schemaPath,instancePath,schema,value))&+(!IsMinProperties(schema)||ErrorMinProperties(stack,context,schemaPath,instancePath,schema,value))&+(!IsMaxProperties(schema)||ErrorMaxProperties(stack,context,schemaPath,instancePath,schema,value))))&+(!guard_exports.IsArray(value)||!!(+(!IsAdditionalItems(schema)||ErrorAdditionalItems(stack,context,schemaPath,instancePath,schema,value))&+(!IsContains(schema)||ErrorContains(stack,context,schemaPath,instancePath,schema,value))&+(!IsItems(schema)|| +ErrorItems(stack,context,schemaPath,instancePath,schema,value))&+(!IsMaxContains(schema)||ErrorMaxContains(stack,context,schemaPath,instancePath,schema,value))&+(!IsMaxItems(schema)||ErrorMaxItems(stack,context,schemaPath,instancePath,schema,value))&+(!IsMinContains(schema)||ErrorMinContains(stack,context,schemaPath,instancePath,schema,value))&+(!IsMinItems(schema)||ErrorMinItems(stack,context,schemaPath,instancePath,schema,value))&+(!IsPrefixItems(schema)||ErrorPrefixItems(stack,context,schemaPath, +instancePath,schema,value))&+(!IsUniqueItems(schema)||ErrorUniqueItems(stack,context,schemaPath,instancePath,schema,value))))&+(!guard_exports.IsString(value)||!!(+(!IsMaxLength4(schema)||ErrorMaxLength(stack,context,schemaPath,instancePath,schema,value))&+(!IsMinLength4(schema)||ErrorMinLength(stack,context,schemaPath,instancePath,schema,value))&+(!IsFormat(schema)||ErrorFormat(stack,context,schemaPath,instancePath,schema,value))&+(!IsPattern(schema)||ErrorPattern(stack,context,schemaPath,instancePath, +schema,value))))&+(!(guard_exports.IsNumber(value)||guard_exports.IsBigInt(value))||!!(+(!IsExclusiveMaximum(schema)||ErrorExclusiveMaximum(stack,context,schemaPath,instancePath,schema,value))&+(!IsExclusiveMinimum(schema)||ErrorExclusiveMinimum(stack,context,schemaPath,instancePath,schema,value))&+(!IsMaximum(schema)||ErrorMaximum(stack,context,schemaPath,instancePath,schema,value))&+(!IsMinimum(schema)||ErrorMinimum(stack,context,schemaPath,instancePath,schema,value))&+(!IsMultipleOf2(schema)|| +ErrorMultipleOf(stack,context,schemaPath,instancePath,schema,value))))&+(!IsRef2(schema)||ErrorRef(stack,context,schemaPath,instancePath,schema,value))&+(!IsRecursiveRef(schema)||ErrorRecursiveRef(stack,context,schemaPath,instancePath,schema,value))&+(!IsDynamicRef(schema)||ErrorDynamicRef(stack,context,schemaPath,instancePath,schema,value))&+(!IsGuard2(schema)||ErrorGuard(stack,context,schemaPath,instancePath,schema,value))&+(!IsConst(schema)||ErrorConst(stack,context,schemaPath,instancePath,schema, +value))&+(!IsEnum2(schema)||ErrorEnum(stack,context,schemaPath,instancePath,schema,value))&+(!IsIf(schema)||ErrorIf(stack,context,schemaPath,instancePath,schema,value))&+(!IsNot(schema)||ErrorNot(stack,context,schemaPath,instancePath,schema,value))&+(!IsAllOf(schema)||ErrorAllOf(stack,context,schemaPath,instancePath,schema,value))&+(!IsAnyOf(schema)||ErrorAnyOf(stack,context,schemaPath,instancePath,schema,value))&+(!IsOneOf(schema)||ErrorOneOf(stack,context,schemaPath,instancePath,schema,value))& ++(!IsUnevaluatedItems(schema)||(!guard_exports.IsArray(value)||ErrorUnevaluatedItems(stack,context,schemaPath,instancePath,schema,value)))&+(!IsUnevaluatedProperties(schema)||(!guard_exports.IsObject(value)||ErrorUnevaluatedProperties(stack,context,schemaPath,instancePath,schema,value))))&&(!IsRefine2(schema)||ErrorRefine(stack,context,schemaPath,instancePath,schema,value));stack.Pop(schema);return result}var functions=new Map;function CreateCallExpression(context,_schema,hash,value){return context.UseUnevaluated()?emit_exports.Call(`check_${hash}`,["context",value]):emit_exports.Call(`check_${hash}`,[value])}function CreateFunctionExpression(stack,context,schema,hash){const expression=BuildSchema(stack,context,schema,"value");return context.UseUnevaluated()?emit_exports.ConstDeclaration(`check_${hash}`,emit_exports.ArrowFunction(["context","value"],expression)):emit_exports.ConstDeclaration(`che\ +ck_${hash}`,emit_exports.ArrowFunction(["value"],expression))}function ResetFunctions(){functions.clear()}function GetFunctions(){return[...functions.values()]}function CreateFunction(stack,context,schema,value){const hash=IsSchemaObject(schema)?hash_exports.Hash({__baseURL:stack.BaseURL().href,...schema}):hash_exports.Hash(schema);const call=CreateCallExpression(context,schema,hash,value);if(functions.has(hash))return call;functions.set(hash,"");functions.set(hash,CreateFunctionExpression(stack, +context,schema,hash));return call}var resolve_exports={};__export(resolve_exports,{DynamicRef:()=>DynamicRef,Ref:()=>Ref2});var pointer_exports={};__export(pointer_exports,{Delete:()=>Delete,Get:()=>Get4,Has:()=>Has2,Indices:()=>Indices,Set:()=>Set4});function AssertNotRoot(indices){if(indices.length===0)throw Error("Cannot set root")}function AssertCanSet(value){if(!guard_exports.IsObject(value))throw Error("Cannot set value")}function AssertIndex(index2){if(guard_exports.IsUnsafePropertyKey(index2))throw Error("Pointer contains unsafe property key")}function AssertIndices(indices){for(const index2 of indices)AssertIndex(index2)}function IsNumericIndex(index2){return/^(0|[1-9]\d*)$/.test(index2)}function TakeIndexRight(indices){return[indices. +slice(0,indices.length-1),indices.slice(indices.length-1)[0]]}function HasIndex(index2,value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,index2)}function GetIndex(index2,value){return guard_exports.IsObject(value)&&!guard_exports.IsUnsafePropertyKey(index2)?value[index2]:void 0}function GetIndices(indices,value){return indices.reduce((value2,index2)=>GetIndex(index2,value2),value)}function Indices(pointer){if(guard_exports.IsEqual(pointer.length,0))return[];const indices=pointer. +split("/").map(index2=>index2.replace(/~1/g,"/").replace(/~0/g,"~"));return indices.length>0&&indices[0]===""?indices.slice(1):indices}function Has2(value,pointer){let current=value;return Indices(pointer).every(index2=>{if(!HasIndex(index2,current))return false;current=current[index2];return true})}function Get4(value,pointer){const indices=Indices(pointer);return GetIndices(indices,value)}function Set4(value,pointer,next){const indices=Indices(pointer);AssertNotRoot(indices);AssertIndices(indices); +const[head,index2]=TakeIndexRight(indices);const parent=GetIndices(head,value);AssertCanSet(parent);parent[index2]=next;return value}function Delete(value,pointer){const indices=Indices(pointer);AssertNotRoot(indices);AssertIndices(indices);const[head,index2]=TakeIndexRight(indices);const parent=GetIndices(head,value);AssertCanSet(parent);if(guard_exports.IsArray(parent)&&IsNumericIndex(index2)){parent.splice(+index2,1)}else{delete parent[index2]}return value}function MatchId(schema,base,ref){if(schema.$id===ref.hash)return schema;const absoluteId=new URL(schema.$id,base.href);const absoluteRef=new URL(ref.href,base.href);if(guard_exports.IsEqual(absoluteId.pathname,absoluteRef.pathname)){return ref.hash.startsWith("#")?MatchHash(schema,base,ref):schema}return void 0}function MatchAnchor(schema,base,ref){const absoluteAnchor=new URL(`#${schema.$anchor}`,base.href);const absoluteRef=new URL(ref.href,base.href);return guard_exports.IsEqual(absoluteAnchor. +href,absoluteRef.href)?schema:void 0}function MatchDynamicAnchor(schema,base,ref){const absoluteAnchor=new URL(`#${schema.$dynamicAnchor}`,base.href);const absoluteRef=new URL(ref.href,base.href);return guard_exports.IsEqual(absoluteAnchor.href,absoluteRef.href)?schema:void 0}function MatchHash(schema,_base,ref){if(ref.href.endsWith("#"))return schema;if(!ref.hash.startsWith("#"))return void 0;const fragment=decodeURIComponent(ref.hash.slice(1));if(!fragment.startsWith("/"))return void 0;return pointer_exports. +Get(schema,fragment)}function Match4(schema,base,ref){if(IsId(schema)){const result=MatchId(schema,base,ref);if(!guard_exports.IsUndefined(result))return result}if(IsAnchor(schema)){const result=MatchAnchor(schema,base,ref);if(!guard_exports.IsUndefined(result))return result}if(IsDynamicAnchor(schema)){const result=MatchDynamicAnchor(schema,base,ref);if(!guard_exports.IsUndefined(result))return result}return MatchHash(schema,base,ref)}function FromArray6(schema,base,ref){return schema.reduce((result,item)=>{ +const match=FromValue3(item,base,ref);return!guard_exports.IsUndefined(match)?match:result},void 0)}function FromObject9(schema,base,ref){return guard_exports.Keys(schema).reduce((result,key)=>{const match=FromValue3(schema[key],base,ref);return!guard_exports.IsUndefined(match)?match:result},void 0)}function FromValue3(schema,base,ref){const nextBase=IsSchemaObject(schema)&&IsId(schema)?new URL(schema.$id,base.href):base;if(IsSchemaObject(schema)){const result=Match4(schema,nextBase,ref);if(!guard_exports. +IsUndefined(result))return result}if(guard_exports.IsArray(schema))return FromArray6(schema,nextBase,ref);if(guard_exports.IsObject(schema))return FromObject9(schema,nextBase,ref);return void 0}function Ref2(schema,ref){const defaultBase=new URL("http://unknown/");const initialBase=IsId(schema)?new URL(schema.$id,defaultBase.href):defaultBase;const initialRef=new URL(ref,initialBase.href);return FromValue3(schema,initialBase,initialRef)}function DynamicRef(root,base,dynamicRef,dynamicAnchors){const fragmentTarget=dynamicRef. +$dynamicRef.startsWith("#")?Ref2(base,dynamicRef.$dynamicRef):Ref2(root,dynamicRef.$dynamicRef);if(guard_exports.IsUndefined(fragmentTarget))return void 0;if(!IsSchemaObject(fragmentTarget)||!IsDynamicAnchor(fragmentTarget))return fragmentTarget;const fragment=new URL(dynamicRef.$dynamicRef,"http://unknown/").hash;if(fragment.startsWith("#/"))return fragmentTarget;const anchorTarget=dynamicAnchors.find(anchor=>anchor.$dynamicAnchor===fragmentTarget.$dynamicAnchor);return anchorTarget??fragmentTarget}var __classPrivateFieldGet=function(receiver,state2,kind,f){if(kind==="a"&&!f)throw new TypeError("Private accessor was defined without a getter");if(typeof state2==="function"?receiver!==state2||!f:!state2.has(receiver))throw new TypeError("Cannot read private member from an object whose class did not declare it");return kind==="m"?f:kind==="a"?f.call(receiver):f?f.value:state2.get(receiver)};var _Stack_instances;var _Stack_PushResourceAnchors;var _Stack_PopResourceAnchors;var _Stack_FromContext; +var _Stack_FromRef;var Stack=class{constructor(context,schema){_Stack_instances.add(this);this.context=context;this.schema=schema;this.ids=[];this.anchors=[];this.recursiveAnchors=[];this.dynamicAnchors=[]}BaseURL(){return this.ids.reduce((result,schema)=>new URL(schema.$id,result),new URL("http://unknown"))}Base(){return this.ids[this.ids.length-1]??this.schema}Push(schema){if(!IsSchemaObject(schema))return;if(IsId(schema)){this.ids.push(schema);__classPrivateFieldGet(this,_Stack_instances,"m", +_Stack_PushResourceAnchors).call(this,schema)}if(IsAnchor(schema))this.anchors.push(schema);if(IsRecursiveAnchorTrue(schema))this.recursiveAnchors.push(schema);if(IsDynamicAnchor(schema))this.dynamicAnchors.push(schema)}Pop(schema){if(!IsSchemaObject(schema))return;if(IsId(schema)){this.ids.pop();__classPrivateFieldGet(this,_Stack_instances,"m",_Stack_PopResourceAnchors).call(this,schema)}if(IsAnchor(schema))this.anchors.pop();if(IsRecursiveAnchorTrue(schema))this.recursiveAnchors.pop();if(IsDynamicAnchor( +schema))this.dynamicAnchors.pop()}Ref(ref){return __classPrivateFieldGet(this,_Stack_instances,"m",_Stack_FromContext).call(this,ref)??__classPrivateFieldGet(this,_Stack_instances,"m",_Stack_FromRef).call(this,ref)}RecursiveRef(recursiveRef){return IsRecursiveAnchorTrue(this.Base())?resolve_exports.Ref(this.recursiveAnchors[0],recursiveRef.$recursiveRef):resolve_exports.Ref(this.Base(),recursiveRef.$recursiveRef)}DynamicRef(dynamicRef){const root=this.schema;return resolve_exports.DynamicRef(root, +this.Base(),dynamicRef,this.dynamicAnchors)}};_Stack_instances=new WeakSet,_Stack_PushResourceAnchors=function _Stack_PushResourceAnchors2(schema,isRoot=true){if(!IsSchemaObject(schema))return;const current=schema;if(!isRoot&&IsId(current))return;if(!isRoot&&IsDynamicAnchor(current))this.dynamicAnchors.push(current);for(const key of guard_exports.Keys(current))__classPrivateFieldGet(this,_Stack_instances,"m",_Stack_PushResourceAnchors2).call(this,current[key],false)},_Stack_PopResourceAnchors=function _Stack_PopResourceAnchors2(schema,isRoot=true){ +if(!IsSchemaObject(schema))return;const current=schema;if(!isRoot&&IsId(current))return;if(!isRoot&&IsDynamicAnchor(current))this.dynamicAnchors.pop();for(const key of guard_exports.Keys(current))__classPrivateFieldGet(this,_Stack_instances,"m",_Stack_PopResourceAnchors2).call(this,current[key],false)},_Stack_FromContext=function _Stack_FromContext2(ref){return guard_exports.HasPropertyKey(this.context,ref.$ref)?this.context[ref.$ref]:void 0},_Stack_FromRef=function _Stack_FromRef2(ref){const root=this. +schema;return!ref.$ref.startsWith("#")?resolve_exports.Ref(root,ref.$ref):resolve_exports.Ref(this.Base(),ref.$ref)};function CreateCode(build){const functions2=build.Functions().join(";\n");const statements=build.UseUnevaluated()?["const context = new CheckContext({}, {})",`return ${build.Entry()}`]:[`return ${build.Entry()}`];return`${functions2}; return (value) => { ${statements.join("; ")} }`}function CreateEvaluatedCheck(build,code){const factory=environment_exports.Evaluate("CheckContext","Guard","Format","Hashing",build.External().identifier,code);return factory(CheckContext,guard_exports,format_exports, +hash_exports,build.External().variables)}function CreateDynamicCheck(build){const stack=new Stack(build.Context(),build.Schema());const context=new CheckContext;return value=>CheckSchema(stack,context,build.Schema(),value)}function CreateCheck(build,code){return environment_exports.CanEvaluate()?CreateEvaluatedCheck(build,code):CreateDynamicCheck(build)}var EvaluateResult=class{constructor(isAccelerated,code,check){this.isAccelerated=isAccelerated;this.code=code;this.check=check}IsAccelerated(){ +return this.isAccelerated}Code(){return this.code}Check(value){return this.check(value)}};var BuildResult=class{constructor(context,schema,external,functions2,entry,useUnevaluated){this.context=context;this.schema=schema;this.external=external;this.functions=functions2;this.entry=entry;this.useUnevaluated=useUnevaluated}Context(){return this.context}Schema(){return this.schema}UseUnevaluated(){return this.useUnevaluated}External(){return this.external}Functions(){return this.functions}Entry(){return this. +entry}Evaluate(){const code=CreateCode(this);const check=CreateCheck(this,code);return new EvaluateResult(environment_exports.CanEvaluate(),code,check)}};function Build(...args){const[context,schema]=arguments_exports.Match(args,{2:(context2,schema2)=>[context2,schema2],1:schema2=>[{},schema2]});ResetExternal();ResetFunctions();const stack=new Stack(context,schema);const build=new BuildContext(HasUnevaluated(context,schema));const call=CreateFunction(stack,build,schema,"value");const functions2=GetFunctions(); +const externals=GetExternal();return new BuildResult(context,schema,externals,functions2,call,build.UseUnevaluated())}function Errors(...args){const[context,schema,value]=arguments_exports.Match(args,{3:(context2,schema2,value2)=>[context2,schema2,value2],2:(schema2,value2)=>[{},schema2,value2]});const settings2=settings_exports.Get();const locale2=Get2();const errors=[];const stack=new Stack(context,schema);const errorContext=new ErrorContext(error=>{if(guard_exports.IsGreaterEqualThan(errors.length,settings2.maxErrors))return;return errors.push({...error,message:locale2(error)})});const result=ErrorSchema(stack, +errorContext,"#","",schema,value);return[result,errors]}function Check(...args){const[context,schema,value]=arguments_exports.Match(args,{3:(context2,schema2,value2)=>[context2,schema2,value2],2:(schema2,value2)=>[{},schema2,value2]});const stack=new Stack(context,schema);const checkContext=new CheckContext;return CheckSchema(stack,checkContext,schema,value)}function Check2(...args){const[context,type,value]=arguments_exports.Match(args,{3:(context2,type2,value2)=>[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});return Check(context,type,value)}function Errors2(...args){const[context,type,value]=arguments_exports.Match(args,{3:(context2,type2,value2)=>[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});const[_,errors]=Errors(context,type,value);return errors}var AssertError=class extends Error{constructor(source,value,errors){super(source);Object.defineProperty(this,"cause",{value:{source,errors,value},writable:false,configurable:false,enumerable:false})}};function Assert(...args){const[context,type,value]=arguments_exports.Match(args,{3:(context2,type2,value2)=>[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});const check=Check2(context,type,value);if(!check)throw new AssertError("Assert",value,Errors2(context,type,value))}function FromArray7(context,type,value){if(!guard_exports.IsArray(value))return value;return value.map(value2=>FromType19(context,type.items,value2))}function FromBase(_context,type,value){return type.Clean(value)}function FromCyclic6(context,type,value){return FromType19({...context,...type.$defs},Ref(type.$ref),value)}function EvaluateIntersection(context,type){const additionalProperties=guard_exports.HasPropertyKey(type,"unevaluatedProperties")?{additionalProperties:type.unevaluatedProperties}:{};const instantiated=Instantiate(context,type);const evaluated=Evaluate2(instantiated);return IsObject3(evaluated)?Options(evaluated,additionalProperties):evaluated}function FromIntersect6(context,type,value){const evaluated=EvaluateIntersection(context,type);return FromType19(context,evaluated,value)}function GetAdditionalProperties(type){const additionalProperties=guard_exports.HasPropertyKey(type,"additionalProperties")?type.additionalProperties:void 0;return additionalProperties}function FromObject10(context,type,value){if(!guard_exports.IsObject(value)||guard_exports.IsArray(value))return value;const additionalProperties=GetAdditionalProperties(type);for(const key of guard_exports.Keys(value)){if(guard_exports.HasPropertyKey(type.properties,key)){value[key]=FromType19(context,type.properties[key],value[key]);continue}const unknownCheck=guard_exports.IsBoolean(additionalProperties)&&guard_exports.IsEqual(additionalProperties,true)||IsSchema(additionalProperties)&&Check2( +context,additionalProperties,value[key]);if(unknownCheck){value[key]=FromType19(context,additionalProperties,value[key]);continue}delete value[key]}return value}function FromRecord2(context,type,value){if(!guard_exports.IsObject(value))return value;const additionalProperties=GetAdditionalProperties(type);const[recordPattern,recordValue]=[new RegExp(RecordPattern(type)),RecordValue(type)];for(const key of guard_exports.Keys(value)){if(recordPattern.test(key)){value[key]=FromType19(context,recordValue,value[key]);continue}const unknownCheck=guard_exports.IsBoolean(additionalProperties)&&guard_exports.IsEqual(additionalProperties,true)||IsSchema(additionalProperties)&& +Check2(context,additionalProperties,value[key]);if(unknownCheck){value[key]=FromType19(context,additionalProperties,value[key]);continue}delete value[key]}return value}function FromRef5(context,type,value){return guard_exports.HasPropertyKey(context,type.$ref)?FromType19(context,context[type.$ref],value):value}function FromTuple5(context,schema,value){if(!guard_exports.IsArray(value))return value;const length=Math.min(value.length,schema.items.length);for(let index2=0;index2Clone2( +element))}function FromTypedArray(value){return value.slice()}function FromMap(value){return new Map(Clone2([...value.entries()]))}function FromSet(value){return new Set(Clone2([...value.values()]))}function FromValue4(value){return value}function Clone2(value){return globals_exports.IsTypeArray(value)?FromTypedArray(value):globals_exports.IsMap(value)?FromMap(value):globals_exports.IsSet(value)?FromSet(value):guard_exports.IsArray(value)?FromArray8(value):guard_exports.IsObject(value)?FromObject11( +value):FromValue4(value)}function DeterministicCompare(left,right){return JSON.stringify(left).localeCompare(JSON.stringify(right))}function UnionPrioritySort(types,order=1){return types.sort((left,right)=>{const result=Compare(left,right);return(guard_exports.IsEqual(result,"disjoint")?DeterministicCompare(left,right):guard_exports.IsEqual(result,"right-inside")?1:guard_exports.IsEqual(result,"left-inside")?-1:DeterministicCompare(left,right))*order})}function FromUnion9(context,type,value){for(const schema of UnionPrioritySort(type.anyOf)){const clean=FromType19(context,schema,Clone2(value));if(Check2(context,schema,clean))return clean}return value}function FromType19(context,type,value){return IsArray3(type)?FromArray7(context,type,value):IsBase(type)?FromBase(context,type,value):IsCyclic(type)?FromCyclic6(context,type,value):IsIntersect(type)?FromIntersect6(context,type,value):IsObject3(type)?FromObject10(context,type,value):IsRecord(type)?FromRecord2(context,type,value):IsRef(type)?FromRef5(context,type,value):IsTuple(type)?FromTuple5(context,type,value):IsUnion(type)?FromUnion9(context,type,value):value}function Clean(...args){const[context,type,value]=arguments_exports.Match(args,{3:(context2,type2,value2)=>[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});return FromType19(context,type,value)}var try_exports={};__export(try_exports,{Fail:()=>Fail,IsOk:()=>IsOk,Ok:()=>Ok,TryArray:()=>TryArray,TryBigInt:()=>TryBigInt,TryBoolean:()=>TryBoolean,TryNull:()=>TryNull,TryNumber:()=>TryNumber,TryString:()=>TryString,TryUndefined:()=>TryUndefined});function IsOk(value){return guard_exports.IsObject(value)&&guard_exports.HasPropertyKey(value,"value")}function Ok(value){return{value}}function Fail(){return void 0}function TryArray(value){return guard_exports.IsArray(value)?Ok(value):Ok([value])}function FromBoolean2(value){return guard_exports.IsEqual(value,true)?Ok(BigInt(1)):Ok(BigInt(0))}var bigintPattern=/^-?(0|[1-9]\d*)n$/;var decimalPattern=/^-?(0|[1-9]\d*)\.\d+$/;var integerPattern=/^-?(0|[1-9]\d*)$/;function IsStringBigIntLike(value){return bigintPattern.test(value)}function IsStringDecimalLike(value){return decimalPattern.test(value)}function IsStringIntegerLike(value){return integerPattern.test(value)}function FromString2(value){const lowercase=value.toLowerCase();return IsStringBigIntLike( +value)?Ok(BigInt(value.slice(0,value.length-1))):IsStringDecimalLike(value)?Ok(BigInt(value.split(".")[0])):IsStringIntegerLike(value)?Ok(BigInt(value)):guard_exports.IsEqual(lowercase,"false")?Ok(BigInt(0)):guard_exports.IsEqual(lowercase,"true")?Ok(BigInt(1)):Fail()}function TryBigInt(value){return guard_exports.IsBigInt(value)?Ok(value):guard_exports.IsBoolean(value)?FromBoolean2(value):guard_exports.IsNumber(value)?Ok(BigInt(Math.trunc(value))):guard_exports.IsNull(value)?Ok(BigInt(0)):guard_exports. +IsString(value)?FromString2(value):guard_exports.IsUndefined(value)?Ok(BigInt(0)):Fail()}function FromBigInt2(value){return guard_exports.IsEqual(value,BigInt(0))?Ok(false):guard_exports.IsEqual(value,BigInt(1))?Ok(true):Fail()}function FromNumber2(value){return guard_exports.IsEqual(value,0)?Ok(false):guard_exports.IsEqual(value,1)?Ok(true):Fail()}function FromString3(value){return guard_exports.IsEqual(value.toLowerCase(),"false")?Ok(false):guard_exports.IsEqual(value.toLowerCase(),"true")?Ok(true):guard_exports.IsEqual(value,"0")?Ok(false):guard_exports.IsEqual(value,"1")?Ok(true): +Fail()}function TryBoolean(value){return guard_exports.IsBigInt(value)?FromBigInt2(value):guard_exports.IsBoolean(value)?Ok(value):guard_exports.IsNumber(value)?FromNumber2(value):guard_exports.IsNull(value)?Ok(false):guard_exports.IsString(value)?FromString3(value):guard_exports.IsUndefined(value)?Ok(false):Fail()}function FromBigInt3(value){return guard_exports.IsEqual(value,BigInt(0))?Ok(null):Fail()}function FromBoolean3(value){return guard_exports.IsEqual(value,false)?Ok(null):Fail()}function FromNumber3(value){return guard_exports.IsEqual(value,0)?Ok(null):Fail()}function FromString4(value){const lowercase=value.toLowerCase();const predicate=guard_exports.IsEqual(lowercase,"undefined")||guard_exports.IsEqual(lowercase,"null")||guard_exports.IsEqual(value,"")||guard_exports.IsEqual(value,"0");return predicate? +Ok(null):Fail()}function TryNull(value){return guard_exports.IsBigInt(value)?FromBigInt3(value):guard_exports.IsBoolean(value)?FromBoolean3(value):guard_exports.IsNumber(value)?FromNumber3(value):guard_exports.IsNull(value)?Ok(null):guard_exports.IsString(value)?FromString4(value):guard_exports.IsUndefined(value)?Ok(null):Fail()}var maxBigInt=BigInt(Number.MAX_SAFE_INTEGER);var minBigInt=BigInt(Number.MIN_SAFE_INTEGER);function FromBigInt4(value){return value<=maxBigInt&&value>=minBigInt?Ok(Number(value)):Fail()}function FromBoolean4(value){return Ok(value?1:0)}function FromString5(value){const coerced=+value;if(guard_exports.IsNumber(coerced))return Ok(coerced);const lowercase=value.toLowerCase();if(guard_exports.IsEqual(lowercase,"false"))return Ok(0);if(guard_exports.IsEqual(lowercase,"true"))return Ok(1);const result=TryBigInt( +value);if(IsOk(result))return result.value<=maxBigInt&&result.value>=minBigInt?Ok(Number(result.value)):Fail();return Fail()}function TryNumber(value){return guard_exports.IsBigInt(value)?FromBigInt4(value):guard_exports.IsBoolean(value)?FromBoolean4(value):guard_exports.IsNumber(value)?Ok(value):guard_exports.IsNull(value)?Ok(0):guard_exports.IsString(value)?FromString5(value):guard_exports.IsUndefined(value)?Ok(0):Fail()}function TryString(value){return guard_exports.IsBigInt(value)?Ok(value.toString()):guard_exports.IsBoolean(value)?Ok(value.toString()):guard_exports.IsNumber(value)?Ok(value.toString()):guard_exports.IsNull(value)?Ok("null"):guard_exports.IsString(value)?Ok(value):guard_exports.IsUndefined(value)?Ok(""):Fail()}function FromBigInt5(value){return guard_exports.IsEqual(value,BigInt(0))?Ok(void 0):Fail()}function FromBoolean5(value){return guard_exports.IsEqual(value,false)?Ok(void 0):Fail()}function FromNumber4(value){return guard_exports.IsEqual(value,0)?Ok(void 0):Fail()}function FromString6(value){const lowercase=value.toLowerCase();const predicate=guard_exports.IsEqual(lowercase,"undefined")||guard_exports.IsEqual(lowercase,"null")||guard_exports.IsEqual(value,"")||guard_exports.IsEqual(value,"0");return predicate? +Ok(void 0):Fail()}function TryUndefined(value){return guard_exports.IsBigInt(value)?FromBigInt5(value):guard_exports.IsBoolean(value)?FromBoolean5(value):guard_exports.IsNumber(value)?FromNumber4(value):guard_exports.IsNull(value)?Ok(void 0):guard_exports.IsString(value)?FromString6(value):guard_exports.IsUndefined(value)?Ok(value):Fail()}function FromArray9(context,type,value){const result=try_exports.TryArray(value);return result.value.map(value2=>FromType20(context,type.items,value2))}function FromBase2(_context,type,value){return type.Convert(value)}function FromBigInt6(_context,_type,value){const result=try_exports.TryBigInt(value);return try_exports.IsOk(result)?result.value:value}function FromBoolean6(_context,_type,value){const result=try_exports.TryBoolean(value);return try_exports.IsOk(result)?result.value:value}function FromCyclic7(context,type,value){return FromType20({...context,...type.$defs},Ref(type.$ref),value)}function FromUnion10(context,type,value){const matched=type.anyOf.some(type2=>Check2(context,type2,value));if(matched)return value;const candidates=type.anyOf.map(type2=>FromType20(context,type2,Clone2(value)));const selected=candidates.find(value2=>Check2(context,type,value2));return guard_exports.IsUndefined(selected)?value:selected}function FromEnum2(context,type,value){const union=EnumToUnion(type);return FromUnion10(context,union,value)}function FromInteger(_context,_type,value){const result=try_exports.TryNumber(value);return try_exports.IsOk(result)?Math.trunc(result.value):value}function FromIntersect7(context,type,value){const instantiated=Instantiate(context,type);const evaluated=Evaluate2(instantiated);return FromType20(context,evaluated,value)}function FromLiteralBigInt(_context,type,value){const result=try_exports.TryBigInt(value);return try_exports.IsOk(result)&&guard_exports.IsEqual(type.const,result.value)?result.value:value}function FromLiteralBoolean(_context,type,value){const result=try_exports.TryBoolean(value);return try_exports.IsOk(result)&&guard_exports.IsEqual(type.const,result.value)?result.value:value}function FromLiteralNumber(_context,type,value){const result=try_exports.TryNumber(value);return try_exports.IsOk(result)&& +guard_exports.IsEqual(type.const,result.value)?result.value:value}function FromLiteralString(_context,type,value){const result=try_exports.TryString(value);return try_exports.IsOk(result)&&guard_exports.IsEqual(type.const,result.value)?result.value:value}function FromLiteral6(context,type,value){if(guard_exports.IsEqual(type.const,value))return value;return IsLiteralBigInt(type)?FromLiteralBigInt(context,type,value):IsLiteralBoolean(type)?FromLiteralBoolean(context,type,value):IsLiteralNumber(type)? +FromLiteralNumber(context,type,value):IsLiteralString(type)?FromLiteralString(context,type,value):Unreachable()}function FromNull2(_context,_type,value){const result=try_exports.TryNull(value);return try_exports.IsOk(result)?result.value:value}function FromNumber5(_context,_type,value){const result=try_exports.TryNumber(value);return try_exports.IsOk(result)?result.value:value}function FromAdditionalProperties(context,entries,additionalProperties,value){const keys=guard_exports.Keys(value);for(const[regexp,_]of entries){for(const key of keys){if(!regexp.test(key)){value[key]=FromType20(context,additionalProperties,value[key])}}}return value}function IsOptionalUndefined(property,key,value){return IsOptional(property)&&guard_exports.IsUndefined(value[key])}function FromProperties4(context,type,value){const entries=guard_exports.EntriesRegExp(type.properties);const keys=guard_exports.Keys(value);for(const[regexp,property]of entries){for(const key of keys){if(!regexp.test(key)||IsOptionalUndefined(property,key,value))continue;value[key]=FromType20(context,property,value[key])}}return guard_exports.HasPropertyKey(type,"additionalProperties")&&guard_exports.IsObject(type.additionalProperties)?FromAdditionalProperties(context,entries,type.additionalProperties, +value):value}function FromObject12(context,type,value){return guard_exports.IsObjectNotArray(value)?FromProperties4(context,type,value):value}function FromPatternProperties(context,type,value){const entries=guard_exports.EntriesRegExp(type.patternProperties);const keys=guard_exports.Keys(value);for(const[regexp,schema]of entries){for(const key of keys){if(regexp.test(key)){value[key]=FromType20(context,schema,value[key])}}}return guard_exports.HasPropertyKey(type,"additionalProperties")&&guard_exports.IsObject(type.additionalProperties)?FromAdditionalProperties(context,entries,type.additionalProperties,value):value}function FromRecord3(context,type,value){ +return guard_exports.IsObjectNotArray(value)?FromPatternProperties(context,type,value):value}function FromRef6(context,type,value){return guard_exports.HasPropertyKey(context,type.$ref)?FromType20(context,context[type.$ref],value):value}function FromString7(_context,_type,value){const result=try_exports.TryString(value);return try_exports.IsOk(result)?result.value:value}function FromTemplateLiteral4(context,type,value){const decoded=TemplateLiteralDecode(type.pattern);return FromType20(context,decoded,value)}function FromTuple6(context,type,value){if(!guard_exports.IsArray(value))return value;for(let index2=0;index2[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});return FromType20(context,type,value)}function FromArray10(context,type,value){if(!guard_exports.IsArray(value))return value;for(let i=0;i[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});return FromType21(context,type,value)}function Pipeline(pipeline){return(...args)=>{const[context,type,value]=arguments_exports.Match(args,{3:(context2,type2,value2)=>[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});return pipeline.reduce((result,func)=>func(context,type,result),value)}}function Decode2(_context,type,value){return type["~codec"].decode(value)}function Encode(_context,type,value){return type["~codec"].encode(value)}function Callback(direction,context,type,value){if(!IsCodec(type))return value;return guard_exports.IsEqual(direction,"Decode")?Decode2(context,type,value):Encode(context,type,value)}function Decode3(direction,context,type,value){if(!guard_exports.IsArray(value))return Unreachable();for(let i=0;i({...results,...interior}),{})}function NonMatchingInterior(value,interiors){for(const interior of interiors)if(!guard_exports.IsDeepEqual(value,interior))return interior;return value}function Decode4(direction,context,type,value){if(guard_exports.IsEqual(type.allOf.length,0))return Callback(direction,context,type,value);const interiors=type.allOf.map(schema=>FromType22(direction,context,schema,Clean(schema,Clone2(value)))); +const structural=interiors.every(result=>guard_exports.IsObject(result));const exterior=structural?MergeInteriors(interiors):NonMatchingInterior(value,interiors);return Callback(direction,context,type,exterior)}function Encode3(direction,context,type,value){if(guard_exports.IsEqual(type.allOf.length,0))return Callback(direction,context,type,value);const exterior=Callback(direction,context,type,value);const interiors=type.allOf.map(schema=>FromType22(direction,context,schema,Clean(schema,Clone2(exterior)))); +const structural=interiors.every(result=>guard_exports.IsObject(result));if(structural)return MergeInteriors(interiors);return NonMatchingInterior(exterior,interiors)}function FromIntersect9(direction,context,type,value){return guard_exports.IsEqual(direction,"Decode")?Decode4(direction,context,type,value):Encode3(direction,context,type,value)}function Decode5(direction,context,type,value){if(!guard_exports.IsObjectNotArray(value))return Unreachable();for(const key of guard_exports.Keys(type.properties)){if(!guard_exports.HasPropertyKey(value,key)||IsOptionalUndefined(type.properties[key],key,value))continue;value[key]=FromType22(direction,context,type.properties[key],value[key])}return Callback(direction,context,type,value)}function Encode4(direction,context,type,value){const exterior=Callback(direction,context,type,value);if(!guard_exports. +IsObjectNotArray(exterior))return exterior;for(const key of guard_exports.Keys(type.properties)){if(!guard_exports.HasPropertyKey(exterior,key)||IsOptionalUndefined(type.properties[key],key,exterior))continue;exterior[key]=FromType22(direction,context,type.properties[key],exterior[key])}return exterior}function FromObject14(direction,context,type,value){return guard_exports.IsEqual(direction,"Decode")?Decode5(direction,context,type,value):Encode4(direction,context,type,value)}function Decode6(direction,context,type,value){if(!guard_exports.IsObjectNotArray(value))return Unreachable();const regexp=new RegExp(RecordPattern(type));for(const key of guard_exports.Keys(value)){if(!regexp.test(key))Unreachable();value[key]=FromType22(direction,context,RecordValue(type),value[key])}return Callback(direction,context,type,value)}function Encode5(direction,context,type,value){const exterior=Callback(direction,context,type,value);if(!guard_exports.IsObjectNotArray(exterior))return exterior; +const regexp=new RegExp(RecordPattern(type));for(const key of guard_exports.Keys(exterior)){if(!regexp.test(key))continue;exterior[key]=FromType22(direction,context,RecordValue(type),exterior[key])}return exterior}function FromRecord5(direction,context,type,value){return guard_exports.IsEqual(direction,"Decode")?Decode6(direction,context,type,value):Encode5(direction,context,type,value)}function ResolveRef(direction,context,type,value){return guard_exports.HasPropertyKey(context,type.$ref)?FromType22(direction,context,context[type.$ref],value):value}function FromRef8(direction,context,type,value){return guard_exports.IsEqual(direction,"Decode")?Callback(direction,context,type,ResolveRef(direction,context,type,value)):ResolveRef(direction,context,type,Callback(direction,context,type,value))}function Decode7(direction,context,type,value){if(!guard_exports.IsArray(value))return Unreachable();for(let i=0;iClone2(value),(context,type,value)=>Default(context,type,value),(context,type,value)=>Convert(context,type,value),(context,type,value)=>Clean( +context,type,value),(context,type,value)=>Assert2(context,type,value),(context,type,value)=>DecodeUnsafe(context,type,value)]);function Decode9(...args){const[context,type,value]=arguments_exports.Match(args,{3:(context2,type2,value2)=>[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});return Decoder(context,type,value)}var EncodeError=class extends AssertError{constructor(value,errors){super("Encode",value,errors)}};function Assert3(context,type,value){if(!Check2(context,type,value))throw new EncodeError(value,Errors2(context,type,value));return value}function EncodeUnsafe(context,type,value){return FromType22("Encode",context,type,value)}var Encoder=Pipeline([(_context,_type,value)=>Clone2(value),(context,type,value)=>EncodeUnsafe(context,type,value),(context,type,value)=>Default(context,type,value),(context,type,value)=>Convert( +context,type,value),(context,type,value)=>Clean(context,type,value),(context,type,value)=>Assert3(context,type,value)]);function Encode8(...args){const[context,type,value]=arguments_exports.Match(args,{3:(context2,type2,value2)=>[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});return Encoder(context,type,value)}function FromArray12(context,type){return IsCodec(type)||FromType23(context,type.items)}function FromCyclic10(context,type){return IsCodec(type)||FromRef9({...context,...type.$defs},Ref(type.$ref))}function FromIntersect10(context,type){return IsCodec(type)||type.allOf.some(type2=>FromType23(context,type2))}function FromObject15(context,type){return IsCodec(type)||guard_exports.Keys(type.properties).some(key=>{return FromType23(context,type.properties[key])})}function FromRecord6(context,type){return IsCodec( +type)||FromType23(context,RecordValue(type))}function FromRef9(context,type){if(visited.has(type.$ref))return false;visited.add(type.$ref);return IsCodec(type)||guard_exports.HasPropertyKey(context,type.$ref)&&FromType23(context,context[type.$ref])}function FromTuple9(context,type){return IsCodec(type)||type.items.some(type2=>FromType23(context,type2))}function FromUnion13(context,type){return IsCodec(type)||type.anyOf.some(type2=>FromType23(context,type2))}function FromType23(context,type){return IsArray3( +type)?FromArray12(context,type):IsCyclic(type)?FromCyclic10(context,type):IsIntersect(type)?FromIntersect10(context,type):IsObject3(type)?FromObject15(context,type):IsRecord(type)?FromRecord6(context,type):IsRef(type)?FromRef9(context,type):IsTuple(type)?FromTuple9(context,type):IsUnion(type)?FromUnion13(context,type):IsCodec(type)}var visited=new Set;function HasCodec(...args){const[context,type]=arguments_exports.Match(args,{2:(context2,type2)=>[context2,type2],1:type2=>[{},type2]});visited.clear(); +return FromType23(context,type)}var CreateError=class extends Error{constructor(type,message){super(message);this.type=type}};function FromDefault2(_context,schema){return guard_exports.IsFunction(schema.default)?schema.default(schema):guard_exports.IsObject(schema.default)?Clone2(schema.default):schema.default}function FromArray13(context,type){if(IsUniqueItems(type)&&!IsDefault(type))throw new CreateError(type,"Arrays with uniqueItems constraints must specify a default annotation");const length=IsMinItems(type)?type.minItems:0;return Array.from({length},()=>FromType24(context,type.items))}async function*CreateAsyncIterator(){}function FromAsyncIterator(_context,_type){return CreateAsyncIterator()}function FromBase4(_context,type){return type.Create()}function FromBigInt7(_context,type){return IsExclusiveMinimum(type)?BigInt(type.exclusiveMinimum)+BigInt(1):IsMinimum(type)?BigInt(type.minimum):BigInt(0)}function FromBoolean7(_context,_type){return false}function FromConstructor2(context,type){const instanceType=FromType24(context,type.instanceType);return class{constructor(){Object.assign(this,instanceType)}}}function FromCyclic11(context,type){return FromType24({...context,...type.$defs},Ref(type.$ref))}function FromEnum3(context,type){return FromType24(context,EnumToUnion(type))}function FromFunction2(context,type){const returnType=FromType24(context,type.returnType);return()=>returnType}function FromInteger2(_context,type){return IsExclusiveMinimum(type)&&guard_exports.IsNumber(type.exclusiveMinimum)?type.exclusiveMinimum+1:IsMinimum(type)?type.minimum:0}function FromIntersect11(context,type){const instantiated=Instantiate(context,type);const evaluated=Evaluate2(instantiated);return FromType24(context,evaluated)}function*CreateIterator(){}function FromIterator(_context,_type){return CreateIterator()}function FromLiteral7(_context,type){return type.const}function FromNever(_context,type){throw new CreateError(type,"Cannot create TNever types")}function FromNull3(_context,_type){return null}function FromNumber6(_context,type){return IsExclusiveMinimum(type)&&guard_exports.IsNumber(type.exclusiveMinimum)?type.exclusiveMinimum+1:IsMinimum(type)?type.minimum:0}function FromObject16(context,type){const required=guard_exports.IsUndefined(type.required)?[]:type.required;return required.reduce((result,key)=>{return{...result,[key]:FromType24(context,type.properties[key])}},{})}function FromPromise(context,type){return Promise.resolve(FromType24(context,type.item))}function FromRecord7(_context,type){if(IsMinProperties(type)&&!IsDefault(type))throw new CreateError(type,"Record with the minProperties constraint must have a default annotation");return{}}function FromRef10(context,type){return guard_exports.HasPropertyKey(context,type.$ref)?FromType24(context,context[type.$ref]):(()=>{throw new CreateError(type,"Unable to deref Ref")})()}function FromString8(_context,type){const needsDefault=(IsPattern(type)||IsFormat(type))&&!IsDefault(type);if(needsDefault)throw Error("Strings with format or pattern constraints must specify default");const minLength=IsMinLength4(type)?type.minLength:0;return"".padEnd(minLength)}function FromSymbol2(_context,_type){return Symbol()}function FromTemplateLiteral5(context,type){const decoded=TemplateLiteralDecode(type.pattern);if(IsString4(decoded))throw new CreateError(type,"Unable to create TemplateLiteral due to infinite type expansion");return FromType24(context,decoded)}function FromTuple10(context,type){return Array.from({length:type.minItems},(_,i)=>FromType24(context,type.items[i]))}function FromUndefined3(_context,_type){return void 0}function FromUnion14(context,type){if(guard_exports.IsEqual(type.anyOf.length,0)){throw Error("Unable to create Union with no variants")}return FromType24(context,type.anyOf[0])}function FromVoid2(_context,_type){return void 0}function FromType24(context,type){return IsDefault(type)?FromDefault2(context,type):IsArray3(type)?FromArray13(context,type):IsAsyncIterator3(type)?FromAsyncIterator(context,type):IsBase(type)?FromBase4(context,type):IsBigInt3(type)?FromBigInt7(context,type):IsBoolean4(type)?FromBoolean7(context,type):IsConstructor3(type)?FromConstructor2(context,type):IsCyclic(type)?FromCyclic11(context,type):IsEnum(type)?FromEnum3(context,type):IsFunction3(type)?FromFunction2(context,type):IsInteger3(type)?FromInteger2( +context,type):IsIntersect(type)?FromIntersect11(context,type):IsIterator3(type)?FromIterator(context,type):IsLiteral(type)?FromLiteral7(context,type):IsNever(type)?FromNever(context,type):IsNull3(type)?FromNull3(context,type):IsNumber4(type)?FromNumber6(context,type):IsObject3(type)?FromObject16(context,type):IsPromise(type)?FromPromise(context,type):IsRecord(type)?FromRecord7(context,type):IsRef(type)?FromRef10(context,type):IsString4(type)?FromString8(context,type):IsSymbol3(type)?FromSymbol2( +context,type):IsTemplateLiteral(type)?FromTemplateLiteral5(context,type):IsTuple(type)?FromTuple10(context,type):IsUndefined3(type)?FromUndefined3(context,type):IsUnion(type)?FromUnion14(context,type):IsVoid(type)?FromVoid2(context,type):void 0}function Create2(...args){const[context,type]=arguments_exports.Match(args,{2:(context2,type2)=>[context2,type2],1:type2=>[{},type2]});return FromType24(context,type)}function Equal(left,right){return guard_exports.IsDeepEqual(left,right)}function Hash2(value){return hash_exports.Hash(value)}var MutateError=class extends Error{constructor(message){super(message)}};function FromArray14(root,path,current,next){if(!guard_exports.IsArray(current)){pointer_exports.Set(root,path,Clone2(next))}else{for(let index2=0;index2Clone2(value),(context,type,value)=>Default(context,type,value),(context,type,value)=>Convert(context,type,value),(context,type,value)=>Clean(context,type,value),(context,type,value)=>Assert4(context,type,value)]);function Parse(...args){ +const[context,type,value]=arguments_exports.Match(args,{3:(context2,type2,value2)=>[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});const checked=Check2(context,type,value);if(checked)return value;if(settings_exports.Get().correctiveParse)return Parser(context,type,value);throw new ParseError2(value,Errors2(context,type,value))}function CreateUpdate(path,value){return{type:"update",path,value}}function CreateInsert(path,value){return{type:"insert",path,value}}function CreateDelete(path){return{type:"delete",path}}function AssertCanDiffObject(value){if(guard_exports.IsObject(value)&&guard_exports.IsEqual(guard_exports.Symbols(value).length,0))return;throw new Error("Cannot create diffs for objects with symbols keys")}function*FromObject18(path,left,right){if(!guard_exports.IsObject(right)||guard_exports.IsArray(right))return yield CreateUpdate( +path,right);AssertCanDiffObject(left);AssertCanDiffObject(right);const leftKeys=guard_exports.Keys(left);const rightKeys=guard_exports.Keys(right);for(const key of rightKeys){if(guard_exports.HasPropertyKey(left,key))continue;if(guard_exports.IsUnsafePropertyKey(key))continue;yield CreateInsert(`${path}/${key}`,right[key])}for(const key of leftKeys){if(!guard_exports.HasPropertyKey(right,key))continue;if(guard_exports.IsUnsafePropertyKey(key))continue;if(Equal(left,right))continue;yield*FromValue6( +`${path}/${key}`,left[key],right[key])}for(const key of leftKeys){if(guard_exports.HasPropertyKey(right,key))continue;if(guard_exports.IsUnsafePropertyKey(key))continue;yield CreateDelete(`${path}/${key}`)}}function*FromArray15(path,left,right){if(!guard_exports.IsArray(right))return yield CreateUpdate(path,right);for(let i=0;i=0;i--){if(i0&&edits[0].path===""&&edits[0].type==="update"}function IsEmpty(edits){return edits.length===0}function Patch(current,edits){if(IsRoot(edits))return Clone2(edits[0].value);if(IsEmpty(edits))return Clone2(current);const clone=Clone2(current);for(const edit of edits){switch(edit.type){case"insert":{pointer_exports.Set(clone,edit.path,edit.value);break}case"update":{pointer_exports.Set(clone,edit.path,edit.value);break}case"delete":{pointer_exports.Delete( +clone,edit.path);break}}}return clone}var RepairError=class extends Error{constructor(context,type,value,message){super(message);this.context=context;this.type=type;this.value=value}};function MakeUnique(values){const[hashes,result]=[new Set,[]];for(const value of values){const hash=Hash2(value);if(hashes.has(hash))continue;hashes.add(hash);result.push(value)}return result}function FromArray16(context,type,value){if(Check2(context,type,value))return value;const created=guard_exports.IsArray(value)?value:Create2(context,type);const minimum=IsMinItems(type)&&created.lengthCreate2(context,type))]: +created;const maximum=IsMaxItems(type)&&minimum.length>type.maxItems?minimum.slice(0,type.maxItems):minimum;const repaired=maximum.map(value2=>FromType25(context,type.items,value2));if(!IsUniqueItems(type)||IsUniqueItems(type)&&!guard_exports.IsEqual(type.uniqueItems,true))return repaired;const unique=MakeUnique(repaired);if(!Check2(context,type,unique))throw new RepairError(context,type,value,"Failed to repair Array due to uniqueItems constraint");return unique}function FromUnknown4(context,type,value){if(Check2(context,type,value))return value;const converted=Convert(context,type,value);if(Check2(context,type,converted))return converted;return Create2(context,type)}function FromBase5(context,type,value){return FromUnknown4(context,type,value)}function FromEnum4(context,type,value){const union=EnumToUnion(type);return FromType25(context,union,value)}function FromIntersect12(context,type,value){const instantiated=Instantiate(context,type);const evaluated=Evaluate2(instantiated);return FromType25(context,evaluated,value)}function FromObject19(context,type,value){if(Check2(context,type,value))return value;if(!guard_exports.IsObjectNotArray(value))return Create2(context,type);const required=new Set(guard_exports.IsUndefined(type.required)?[]:type.required);const result={};for(const[key,schema]of guard_exports.Entries(type.properties)){if(!required.has(key)&&guard_exports.IsUndefined(value[key]))continue;result[key]=key in value?FromType25(context,schema,value[key]):Create2(context,schema)}const evaluatedKeys=guard_exports. +Keys(type.properties);if(IsAdditionalProperties(type)&&guard_exports.IsObject(type.additionalProperties)){for(const key of guard_exports.Keys(value)){if(evaluatedKeys.includes(key))continue;result[key]=FromType25(context,type.additionalProperties,value[key])}}return result}function FromRecord8(context,type,value){if(Check2(context,type,value))return value;if(guard_exports.IsNull(value)||!guard_exports.IsObject(value)||guard_exports.IsArray(value))return Create2(context,type);const recordKey=new RegExp(RecordPattern(type));const recordValue=RecordValue(type);const evaluatedKeys=new Set;const result={};for(const[key,value_]of guard_exports.Entries(value)){if(!recordKey.test(key))continue;result[key]=FromType25(context,recordValue,value_);evaluatedKeys.add(key)}if(IsAdditionalProperties( +type)){for(const key of guard_exports.Keys(value)){if(evaluatedKeys.has(key))continue;result[key]=FromType25(context,type.additionalProperties,value[key])}}return result}function FromRef11(context,type,value){return guard_exports.HasPropertyKey(context,type.$ref)?FromType25(context,context[type.$ref],value):(()=>{throw new RepairError(context,type,value,"Unable to de-reference target type")})()}function FromTemplateLiteral6(context,type,value){const decoded=TemplateLiteralDecode(type.pattern);return FromType25(context,decoded,value)}function FromTuple11(context,schema,value){if(Check2(context,schema,value))return value;if(!guard_exports.IsArray(value))return Create2(context,schema);return schema.items.map((schema2,index2)=>FromType25(context,schema2,value[index2]))}function Deref(context,type,value){return IsRef(type)?guard_exports.HasPropertyKey(context,type.$ref)?Deref(context,context[type.$ref],value):(()=>{throw new Error("Unable to Deref target")})():type}function ScoreVariant(context,type,value){if(!(IsObject3(type)&&guard_exports.IsObject(value)))return 0;const keys=guard_exports.Keys(value);const entries=guard_exports.Entries(type.properties);return entries.reduce((result,[key,schema])=>{const literal=IsLiteral(schema)&&guard_exports.IsEqual(schema. +const,value[key])?100:0;const checks=Check2(context,schema,value[key])?10:0;const exists=keys.includes(key)?1:0;return result+(literal+checks+exists)},0)}function UnionScoreSelect(context,type,value){const schemas=type.anyOf.map(schema=>Deref(context,schema,value));let[select,best]=[schemas[0],0];for(const schema of schemas){const score=ScoreVariant(context,schema,value);if(score>best){select=schema;best=score}}return select}function RepairUnion(context,type,value){const union=Union(Flatten(type.anyOf));const schema=UnionScoreSelect(context,union,value);return FromType25(context,schema,value)}function FromUnion15(context,type,value){if(Check2(context,type,value))return Clone2(value);if(IsDefault(type))return Create2(context,type);return RepairUnion(context,type,value)}function AssertRepairableValue(context,type,value){const unsupported=globals_exports.IsDate(value)||globals_exports.IsMap(value)||globals_exports.IsSet(value)||globals_exports.IsTypeArray(value)||guard_exports.IsConstructor(value)||guard_exports.IsFunction(value);if(unsupported){throw new RepairError(context,type,value,"Value is not repairable")}}function AssertRepairableType(context,type,value){const unsupported=IsAsyncIterator3(type)||IsIterator3(type)||IsConstructor3(type)||IsFunction3(type)|| +IsNever(type)||IsPromise(type);if(unsupported){throw new RepairError(context,type,value,"Type is not repairable")}}function FinalizeRepair(context,type,repaired){return IsRefine(type)?Check2(context,type,repaired)?repaired:Create2(context,type):repaired}function FromType25(context,type,value){if(IsBase(type)){const repaired2=FromBase5(context,type,value);return FinalizeRepair(context,type,repaired2)}AssertRepairableValue(context,type,value);AssertRepairableType(context,type,value);const repaired=IsArray3( +type)?FromArray16(context,type,value):IsEnum(type)?FromEnum4(context,type,value):IsIntersect(type)?FromIntersect12(context,type,value):IsObject3(type)?FromObject19(context,type,value):IsRecord(type)?FromRecord8(context,type,value):IsRef(type)?FromRef11(context,type,value):IsTemplateLiteral(type)?FromTemplateLiteral6(context,type,value):IsTuple(type)?FromTuple11(context,type,value):IsUnion(type)?FromUnion15(context,type,value):FromUnknown4(context,type,value);return FinalizeRepair(context,type,repaired)}function Repair(...args){const[context,type,value]=arguments_exports.Match(args,{3:(context2,type2,value2)=>[context2,type2,value2],2:(type2,value2)=>[{},type2,value2]});const repaired=FromType25(context,type,value);Assert(context,type,repaired);return repaired}var value_exports={};__export(value_exports,{Assert:()=>Assert,Check:()=>Check2,Clean:()=>Clean,Clone:()=>Clone2,Convert:()=>Convert,Create:()=>Create2,Decode:()=>Decode9,Default:()=>Default,Diff:()=>Diff,Encode:()=>Encode8,Equal:()=>Equal,Errors:()=>Errors2,HasCodec:()=>HasCodec,Hash:()=>Hash2,Mutate:()=>Mutate,Parse:()=>Parse,Patch:()=>Patch,Pointer:()=>pointer_exports,Repair:()=>Repair});var Validator=class _Validator extends Base{constructor(...args){super();const matched=arguments_exports.Match(args,{3:(hasCodec,buildResult,evaluateResult)=>[hasCodec,buildResult,evaluateResult],2:(context,type)=>[context,type]});if(matched.length===3&&matched[1]instanceof BuildResult&&matched[2]instanceof EvaluateResult){const[hasCodec,buildResult,evaluateResult]=matched;this.hasCodec=hasCodec;this.buildResult=buildResult;this.evaluateResult=evaluateResult}else{const[context,type]=matched;this. +hasCodec=HasCodec(context,type);this.buildResult=Build(context,type);this.evaluateResult=this.buildResult.Evaluate()}}IsAccelerated(){return this.evaluateResult.IsAccelerated()}Context(){return this.buildResult.Context()}Type(){return this.buildResult.Schema()}Code(){return this.evaluateResult.Code()}Check(value){return this.evaluateResult.Check(value)}Parse(value){const checked=this.Check(value);if(checked)return value;if(settings_exports.Get().correctiveParse)return Parser(this.Context(),this. +Type(),value);throw new ParseError2(value,this.Errors(value))}Errors(value){if(this.IsAccelerated()&&this.Check(value))return[];return Errors2(this.Context(),this.Type(),value)}Clean(value){return Clean(this.Context(),this.Type(),value)}Convert(value){return Convert(this.Context(),this.Type(),value)}Create(){return Create2(this.Context(),this.Type())}Default(value){return Default(this.Context(),this.Type(),value)}Decode(value){const result=this.hasCodec?Decode9(this.Context(),this.Type(),value): +this.Parse(value);return result}Encode(value){const result=this.hasCodec?Encode8(this.Context(),this.Type(),value):this.Parse(value);return result}Clone(){return new _Validator(this.hasCodec,this.buildResult,this.evaluateResult)}};function Compile(...args){const[context,type]=arguments_exports.Match(args,{2:(context2,type2)=>[context2,type2],1:type2=>[{},type2]});return new Validator(context,type)}var validatorCache=new WeakMap;var TYPEBOX_KIND=Symbol.for("TypeBox.Kind");function getSchemaTypes(schema){if(typeof schema.type==="string"){return[schema.type]}if(Array.isArray(schema.type)){return schema.type.filter(type=>typeof type==="string")}return[]}function matchesJsonType(value,type){switch(type){case"number":return typeof value==="number";case"integer":return typeof value==="number"&&Number.isInteger(value);case"boolean":return typeof value==="boolean";case"string":return typeof value=== +"string";case"null":return value===null;case"array":return Array.isArray(value);case"object":return typeof value==="object"&&value!==null&&!Array.isArray(value);default:return false}}function getSubSchemaValidator(schema){try{return getValidator(schema)}catch{return void 0}}function coercePrimitiveByType(value,type){switch(type){case"number":{if(value===null){return 0}if(typeof value==="string"&&value.trim()!==""){const parsed=Number(value);if(Number.isFinite(parsed)){return parsed}}if(typeof value=== +"boolean"){return value?1:0}return value}case"integer":{if(value===null){return 0}if(typeof value==="string"&&value.trim()!==""){const parsed=Number(value);if(Number.isInteger(parsed)){return parsed}}if(typeof value==="boolean"){return value?1:0}return value}case"boolean":{if(value===null){return false}if(typeof value==="string"){if(value==="true"){return true}if(value==="false"){return false}}if(typeof value==="number"){if(value===1){return true}if(value===0){return false}}return value}case"str\ +ing":{if(value===null){return""}if(typeof value==="number"||typeof value==="boolean"){return String(value)}return value}case"null":{if(value===""||value===0||value===false){return null}return value}default:return value}}function applySchemaObjectCoercion(value,schema){const properties=schema.properties;const definedKeys=new Set(properties?Object.keys(properties):[]);if(properties){for(const[key,propertySchema]of Object.entries(properties)){if(!(key in value)){continue}value[key]=coerceWithJsonSchema( +value[key],propertySchema)}}if(schema.additionalProperties&&typeof schema.additionalProperties==="object"){for(const[key,propertyValue]of Object.entries(value)){if(definedKeys.has(key)){continue}value[key]=coerceWithJsonSchema(propertyValue,schema.additionalProperties)}}}function applySchemaArrayCoercion(value,schema){if(Array.isArray(schema.items)){for(let index2=0;index21&&schemaTypes.some(schemaType=>matchesJsonType(nextValue,schemaType));if(schemaTypes.length>0&&!matchesUnionMember){for(const schemaType of schemaTypes){ +const candidate=coercePrimitiveByType(nextValue,schemaType);if(candidate!==nextValue){nextValue=candidate;break}}}if(schemaTypes.includes("object")&&typeof nextValue==="object"&&nextValue!==null&&!Array.isArray(nextValue)){applySchemaObjectCoercion(nextValue,schema)}if(schemaTypes.includes("array")&&Array.isArray(nextValue)){applySchemaArrayCoercion(nextValue,schema)}return nextValue}function getValidator(schema){const key=schema;const cached=validatorCache.get(key);if(cached){return cached}const validator=Compile( +schema);validatorCache.set(key,validator);return validator}function formatValidationPath(error){if(error.keyword==="required"){const requiredProperties=error.params.requiredProperties;const requiredProperty=requiredProperties?.[0];if(requiredProperty){const basePath=error.instancePath.replace(/^\//,"").replace(/\//g,".");return basePath?`${basePath}.${requiredProperty}`:requiredProperty}}const path=error.instancePath.replace(/^\//,"").replace(/\//g,".");return path||"root"}function validateToolArguments(tool,toolCall){const args=structuredClone(toolCall.arguments);value_exports.Convert(tool.parameters,args);const validator=getValidator(tool.parameters);if(!Object.getOwnPropertySymbols(tool.parameters).includes(TYPEBOX_KIND)){const coerced=coerceWithJsonSchema(args,tool.parameters);if(coerced!==args){if(typeof args==="object"&&args!==null&&typeof coerced==="object"&&coerced!==null){for(const key of Object.keys(args)){delete args[key]}Object.assign(args,coerced)}else{ +return validator.Check(coerced)?coerced:args}}}if(validator.Check(args)){return args}const errors=validator.Errors(args).map(error=>` - ${formatValidationPath(error)}: ${error.message}`).join("\n")||"Unknown validation error";const errorMessage=`Validation failed for tool "${toolCall.name}": +${errors} + +Received arguments: +${JSON.stringify(toolCall.arguments,null,2)}`;throw new Error(errorMessage)}var defaultStreamFn;function getDefaultStreamFn(){if(!defaultStreamFn){throw new Error("No default stream function configured. Pass streamFn explicitly or call setDefaultStreamFn().")}return defaultStreamFn}async function runAgentLoop(prompts,context,config,emit,signal,streamFn){const newMessages=[...prompts];const currentContext={...context,messages:[...context.messages,...prompts]};await emit({type:"agent_start"});await emit({type:"turn_start"});for(const prompt2 of prompts){await emit({type:"message_start",message:prompt2});await emit({type:"message_end",message:prompt2})}await runLoop(currentContext,newMessages,config,signal,emit,streamFn??getDefaultStreamFn());return newMessages}async function runAgentLoopContinue(context,config,emit,signal,streamFn){ +if(context.messages.length===0){throw new Error("Cannot continue: no messages in context")}if(context.messages[context.messages.length-1].role==="assistant"){throw new Error("Cannot continue from message role: assistant")}const newMessages=[];const currentContext={...context};await emit({type:"agent_start"});await emit({type:"turn_start"});await runLoop(currentContext,newMessages,config,signal,emit,streamFn??getDefaultStreamFn());return newMessages}async function runLoop(initialContext,newMessages,initialConfig,signal,emit,streamFunction){let currentContext=initialContext;let config=initialConfig;let firstTurn=true;let pendingMessages=await config.getSteeringMessages?.()||[];while(true){let hasMoreToolCalls=true;while(hasMoreToolCalls||pendingMessages.length>0){if(!firstTurn){await emit({type:"turn_start"})}else{firstTurn=false}if(pendingMessages.length>0){for(const message2 of pendingMessages){await emit({type:"message_start",message:message2}); +await emit({type:"message_end",message:message2});currentContext.messages.push(message2);newMessages.push(message2)}pendingMessages=[]}const message=await streamAssistantResponse(currentContext,config,signal,emit,streamFunction);newMessages.push(message);if(message.stopReason==="error"||message.stopReason==="aborted"){await emit({type:"turn_end",message,toolResults:[]});await emit({type:"agent_end",messages:newMessages});return}const toolCalls=message.content.filter(c=>c.type==="toolCall");const toolResults=[]; +hasMoreToolCalls=false;if(toolCalls.length>0){const executedToolBatch=message.stopReason==="length"?await failToolCallsFromTruncatedMessage(toolCalls,emit):await executeToolCalls(currentContext,message,config,signal,emit);toolResults.push(...executedToolBatch.messages);hasMoreToolCalls=!executedToolBatch.terminate;for(const result of toolResults){currentContext.messages.push(result);newMessages.push(result)}}await emit({type:"turn_end",message,toolResults});const nextTurnContext={message,toolResults, +context:currentContext,newMessages};const nextTurnSnapshot=await config.prepareNextTurn?.(nextTurnContext);if(nextTurnSnapshot){currentContext=nextTurnSnapshot.context??currentContext;config={...config,model:nextTurnSnapshot.model??config.model,reasoning:nextTurnSnapshot.thinkingLevel===void 0?config.reasoning:nextTurnSnapshot.thinkingLevel==="off"?void 0:nextTurnSnapshot.thinkingLevel}}if(await config.shouldStopAfterTurn?.({message,toolResults,context:currentContext,newMessages})){await emit({type:"\ +agent_end",messages:newMessages});return}pendingMessages=await config.getSteeringMessages?.()||[]}const followUpMessages=await config.getFollowUpMessages?.()||[];if(followUpMessages.length>0){pendingMessages=followUpMessages;continue}break}await emit({type:"agent_end",messages:newMessages})}async function streamAssistantResponse(context,config,signal,emit,streamFunction){let messages=context.messages;if(config.transformContext){messages=await config.transformContext(messages,signal)}const llmMessages=await config. +convertToLlm(messages);const llmContext={systemPrompt:context.systemPrompt,messages:llmMessages,tools:context.tools};const resolvedApiKey=(config.getApiKey?await config.getApiKey(config.model.provider):void 0)||config.apiKey;const response=await streamFunction(config.model,llmContext,{...config,apiKey:resolvedApiKey,signal});let partialMessage=null;let addedPartial=false;for await(const event of response){switch(event.type){case"start":partialMessage=event.partial;context.messages.push(partialMessage); +addedPartial=true;await emit({type:"message_start",message:{...partialMessage}});break;case"text_start":case"text_delta":case"text_end":case"thinking_start":case"thinking_delta":case"thinking_end":case"toolcall_start":case"toolcall_delta":case"toolcall_end":if(partialMessage){partialMessage=event.partial;context.messages[context.messages.length-1]=partialMessage;await emit({type:"message_update",assistantMessageEvent:event,message:{...partialMessage}})}break;case"done":case"error":{const finalMessage2=await response. +result();if(addedPartial){context.messages[context.messages.length-1]=finalMessage2}else{context.messages.push(finalMessage2)}if(!addedPartial){await emit({type:"message_start",message:{...finalMessage2}})}await emit({type:"message_end",message:finalMessage2});return finalMessage2}}}const finalMessage=await response.result();if(addedPartial){context.messages[context.messages.length-1]=finalMessage}else{context.messages.push(finalMessage);await emit({type:"message_start",message:{...finalMessage}})} +await emit({type:"message_end",message:finalMessage});return finalMessage}async function failToolCallsFromTruncatedMessage(toolCalls,emit){const messages=[];for(const toolCall of toolCalls){await emit({type:"tool_execution_start",toolCallId:toolCall.id,toolName:toolCall.name,args:toolCall.arguments});const finalized={toolCall,result:createErrorToolResult(`Tool call "${toolCall.name}" was not executed: the response hit the output token limit, so its arguments may be truncated. Re-issue the tool c\ +all with complete arguments.`),isError:true};await emitToolExecutionEnd(finalized,emit);const toolResultMessage=createToolResultMessage(finalized);await emitToolResultMessage(toolResultMessage,emit);messages.push(toolResultMessage)}return{messages,terminate:false}}async function executeToolCalls(currentContext,assistantMessage,config,signal,emit){const toolCalls=assistantMessage.content.filter(c=>c.type==="toolCall");const hasSequentialToolCall=toolCalls.some(tc=>currentContext.tools?.find(t=>t. +name===tc.name)?.executionMode==="sequential");if(config.toolExecution==="sequential"||hasSequentialToolCall){return executeToolCallsSequential(currentContext,assistantMessage,toolCalls,config,signal,emit)}return executeToolCallsParallel(currentContext,assistantMessage,toolCalls,config,signal,emit)}async function executeToolCallsSequential(currentContext,assistantMessage,toolCalls,config,signal,emit){const finalizedCalls=[];const messages=[];for(const toolCall of toolCalls){await emit({type:"too\ +l_execution_start",toolCallId:toolCall.id,toolName:toolCall.name,args:toolCall.arguments});const preparation=await prepareToolCall(currentContext,assistantMessage,toolCall,config,signal);let finalized;if(preparation.kind==="immediate"){finalized={toolCall,result:preparation.result,isError:preparation.isError}}else{const executed=await executePreparedToolCall(preparation,signal,emit);finalized=await finalizeExecutedToolCall(currentContext,assistantMessage,preparation,executed,config,signal)}await emitToolExecutionEnd( +finalized,emit);const toolResultMessage=createToolResultMessage(finalized);await emitToolResultMessage(toolResultMessage,emit);finalizedCalls.push(finalized);messages.push(toolResultMessage);if(signal?.aborted){break}}return{messages,terminate:shouldTerminateToolBatch(finalizedCalls)}}async function executeToolCallsParallel(currentContext,assistantMessage,toolCalls,config,signal,emit){const finalizedCalls=[];for(const toolCall of toolCalls){await emit({type:"tool_execution_start",toolCallId:toolCall. +id,toolName:toolCall.name,args:toolCall.arguments});const preparation=await prepareToolCall(currentContext,assistantMessage,toolCall,config,signal);if(preparation.kind==="immediate"){const finalized={toolCall,result:preparation.result,isError:preparation.isError};await emitToolExecutionEnd(finalized,emit);finalizedCalls.push(finalized);if(signal?.aborted){break}continue}finalizedCalls.push(async()=>{const executed=await executePreparedToolCall(preparation,signal,emit);const finalized=await finalizeExecutedToolCall( +currentContext,assistantMessage,preparation,executed,config,signal);await emitToolExecutionEnd(finalized,emit);return finalized});if(signal?.aborted){break}}const orderedFinalizedCalls=await Promise.all(finalizedCalls.map(entry=>typeof entry==="function"?entry():Promise.resolve(entry)));const messages=[];for(const finalized of orderedFinalizedCalls){const toolResultMessage=createToolResultMessage(finalized);await emitToolResultMessage(toolResultMessage,emit);messages.push(toolResultMessage)}return{ +messages,terminate:shouldTerminateToolBatch(orderedFinalizedCalls)}}function shouldTerminateToolBatch(finalizedCalls){return finalizedCalls.length>0&&finalizedCalls.every(finalized=>finalized.result.terminate===true)}function prepareToolCallArguments(tool,toolCall){if(!tool.prepareArguments){return toolCall}const preparedArguments=tool.prepareArguments(toolCall.arguments);if(preparedArguments===toolCall.arguments){return toolCall}return{...toolCall,arguments:preparedArguments}}async function prepareToolCall(currentContext,assistantMessage,toolCall,config,signal){ +const tool=currentContext.tools?.find(t=>t.name===toolCall.name);if(!tool){return{kind:"immediate",result:createErrorToolResult(`Tool ${toolCall.name} not found`),isError:true}}try{const preparedToolCall=prepareToolCallArguments(tool,toolCall);const validatedArgs=validateToolArguments(tool,preparedToolCall);if(config.beforeToolCall){const beforeResult=await config.beforeToolCall({assistantMessage,toolCall,args:validatedArgs,context:currentContext},signal);if(signal?.aborted){return{kind:"immedia\ +te",result:createErrorToolResult("Operation aborted"),isError:true}}if(beforeResult?.block){return{kind:"immediate",result:createErrorToolResult(beforeResult.reason||"Tool execution was blocked"),isError:true}}}if(signal?.aborted){return{kind:"immediate",result:createErrorToolResult("Operation aborted"),isError:true}}return{kind:"prepared",toolCall,tool,args:validatedArgs}}catch(error){return{kind:"immediate",result:createErrorToolResult(error instanceof Error?error.message:String(error)),isError:true}}} +async function executePreparedToolCall(prepared,signal,emit){const updateEvents=[];let acceptingUpdates=true;try{const result=await prepared.tool.execute(prepared.toolCall.id,prepared.args,signal,partialResult=>{if(!acceptingUpdates)return;updateEvents.push(Promise.resolve(emit({type:"tool_execution_update",toolCallId:prepared.toolCall.id,toolName:prepared.toolCall.name,args:prepared.toolCall.arguments,partialResult})))});acceptingUpdates=false;await Promise.all(updateEvents);return{result,isError:false}}catch(error){ +acceptingUpdates=false;await Promise.all(updateEvents);return{result:createErrorToolResult(error instanceof Error?error.message:String(error)),isError:true}}finally{acceptingUpdates=false}}async function finalizeExecutedToolCall(currentContext,assistantMessage,prepared,executed,config,signal){let result=executed.result;let isError=executed.isError;if(config.afterToolCall){try{const afterResult=await config.afterToolCall({assistantMessage,toolCall:prepared.toolCall,args:prepared.args,result,isError, +context:currentContext},signal);if(afterResult){result={...result,content:afterResult.content??result.content,details:afterResult.details??result.details,usage:afterResult.usage??result.usage,terminate:afterResult.terminate??result.terminate};isError=afterResult.isError??isError}}catch(error){result=createErrorToolResult(error instanceof Error?error.message:String(error));isError=true}}return{toolCall:prepared.toolCall,result,isError}}function createErrorToolResult(message){return{content:[{type:"\ +text",text:message}],details:{}}}async function emitToolExecutionEnd(finalized,emit){await emit({type:"tool_execution_end",toolCallId:finalized.toolCall.id,toolName:finalized.toolCall.name,result:finalized.result,isError:finalized.isError})}function createToolResultMessage(finalized){return{role:"toolResult",toolCallId:finalized.toolCall.id,toolName:finalized.toolCall.name,content:finalized.result.content??[],details:finalized.result.details,usage:finalized.result.usage,...finalized.result.addedToolNames?. +length?{addedToolNames:finalized.result.addedToolNames}:{},isError:finalized.isError,timestamp:Date.now()}}async function emitToolResultMessage(toolResultMessage,emit){await emit({type:"message_start",message:toolResultMessage});await emit({type:"message_end",message:toolResultMessage})}function defaultConvertToLlm(messages){return messages.filter(message=>message.role==="user"||message.role==="assistant"||message.role==="toolResult")}var EMPTY_USAGE={input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}};var DEFAULT_MODEL={id:"unknown",name:"unknown",api:"unknown",provider:"unknown",baseUrl:"",reasoning:false,input:[],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:0,maxTokens:0};function createMutableAgentState(initialState){ +let tools=initialState?.tools?.slice()??[];let messages=initialState?.messages?.slice()??[];return{systemPrompt:initialState?.systemPrompt??"",model:initialState?.model??DEFAULT_MODEL,thinkingLevel:initialState?.thinkingLevel??"off",get tools(){return tools},set tools(nextTools){tools=nextTools.slice()},get messages(){return messages},set messages(nextMessages){messages=nextMessages.slice()},isStreaming:false,streamingMessage:void 0,pendingToolCalls:new Set,errorMessage:void 0}}var PendingMessageQueue=class{constructor(mode){ +__publicField(this,"messages",[]);__publicField(this,"mode");this.mode=mode}enqueue(message){this.messages.push(message)}hasItems(){return this.messages.length>0}drain(){if(this.mode==="all"){const drained=this.messages.slice();this.messages=[];return drained}const first=this.messages[0];if(!first){return[]}this.messages=this.messages.slice(1);return[first]}clear(){this.messages=[]}};var Agent=class{constructor(options){__publicField(this,"_state");__publicField(this,"listeners",new Set);__publicField( +this,"steeringQueue");__publicField(this,"followUpQueue");__publicField(this,"convertToLlm");__publicField(this,"transformContext");__publicField(this,"streamFunction");__publicField(this,"getApiKey");__publicField(this,"onPayload");__publicField(this,"onResponse");__publicField(this,"beforeToolCall");__publicField(this,"afterToolCall");__publicField(this,"prepareNextTurn");__publicField(this,"prepareNextTurnWithContext");__publicField(this,"activeRun");__publicField(this,"sessionId");__publicField( +this,"thinkingBudgets");__publicField(this,"transport");__publicField(this,"maxRetryDelayMs");__publicField(this,"toolExecution");const runtimeOptions=options??{};this._state=createMutableAgentState(runtimeOptions.initialState);this.convertToLlm=runtimeOptions.convertToLlm??defaultConvertToLlm;this.transformContext=runtimeOptions.transformContext;this.streamFunction=runtimeOptions.streamFn??getDefaultStreamFn();this.getApiKey=runtimeOptions.getApiKey;this.onPayload=runtimeOptions.onPayload;this. +onResponse=runtimeOptions.onResponse;this.beforeToolCall=runtimeOptions.beforeToolCall;this.afterToolCall=runtimeOptions.afterToolCall;this.prepareNextTurn=runtimeOptions.prepareNextTurn;this.prepareNextTurnWithContext=runtimeOptions.prepareNextTurnWithContext;this.steeringQueue=new PendingMessageQueue(runtimeOptions.steeringMode??"one-at-a-time");this.followUpQueue=new PendingMessageQueue(runtimeOptions.followUpMode??"one-at-a-time");this.sessionId=runtimeOptions.sessionId;this.thinkingBudgets= +runtimeOptions.thinkingBudgets;this.transport=runtimeOptions.transport??"auto";this.maxRetryDelayMs=runtimeOptions.maxRetryDelayMs;this.toolExecution=runtimeOptions.toolExecution??"parallel"}subscribe(listener){this.listeners.add(listener);return()=>this.listeners.delete(listener)}get state(){return this._state}set steeringMode(mode){this.steeringQueue.mode=mode}get steeringMode(){return this.steeringQueue.mode}set followUpMode(mode){this.followUpQueue.mode=mode}get followUpMode(){return this.followUpQueue. +mode}steer(message){this.steeringQueue.enqueue(message)}followUp(message){this.followUpQueue.enqueue(message)}clearSteeringQueue(){this.steeringQueue.clear()}clearFollowUpQueue(){this.followUpQueue.clear()}clearAllQueues(){this.clearSteeringQueue();this.clearFollowUpQueue()}hasQueuedMessages(){return this.steeringQueue.hasItems()||this.followUpQueue.hasItems()}get signal(){return this.activeRun?.abortController.signal}abort(){this.activeRun?.abortController.abort()}waitForIdle(){return this.activeRun?. +promise??Promise.resolve()}reset(){this._state.messages=[];this._state.isStreaming=false;this._state.streamingMessage=void 0;this._state.pendingToolCalls=new Set;this._state.errorMessage=void 0;this.clearFollowUpQueue();this.clearSteeringQueue()}async prompt(input,images){if(this.activeRun){throw new Error("Agent is already processing a prompt. Use steer() or followUp() to queue messages, or wait for completion.")}const messages=this.normalizePromptInput(input,images);await this.runPromptMessages( +messages)}async continue(){if(this.activeRun){throw new Error("Agent is already processing. Wait for completion before continuing.")}const lastMessage=this._state.messages[this._state.messages.length-1];if(!lastMessage){throw new Error("No messages to continue from")}if(lastMessage.role==="assistant"){const queuedSteering=this.steeringQueue.drain();if(queuedSteering.length>0){await this.runPromptMessages(queuedSteering,{skipInitialSteeringPoll:true});return}const queuedFollowUps=this.followUpQueue. +drain();if(queuedFollowUps.length>0){await this.runPromptMessages(queuedFollowUps);return}throw new Error("Cannot continue from message role: assistant")}await this.runContinuation()}normalizePromptInput(input,images){if(Array.isArray(input)){return input}if(typeof input!=="string"){return[input]}const content=[{type:"text",text:input}];if(images&&images.length>0){content.push(...images)}return[{role:"user",content,timestamp:Date.now()}]}async runPromptMessages(messages,options={}){await this.runWithLifecycle( +async signal=>{await runAgentLoop(messages,this.createContextSnapshot(),this.createLoopConfig(options),event=>this.processEvents(event),signal,this.streamFunction)})}async runContinuation(){await this.runWithLifecycle(async signal=>{await runAgentLoopContinue(this.createContextSnapshot(),this.createLoopConfig(),event=>this.processEvents(event),signal,this.streamFunction)})}createContextSnapshot(){return{systemPrompt:this._state.systemPrompt,messages:this._state.messages.slice(),tools:this._state. +tools.slice()}}createLoopConfig(options={}){let skipInitialSteeringPoll=options.skipInitialSteeringPoll===true;return{model:this._state.model,reasoning:this._state.thinkingLevel==="off"?void 0:this._state.thinkingLevel,sessionId:this.sessionId,onPayload:this.onPayload,onResponse:this.onResponse,transport:this.transport,thinkingBudgets:this.thinkingBudgets,maxRetryDelayMs:this.maxRetryDelayMs,toolExecution:this.toolExecution,beforeToolCall:this.beforeToolCall,afterToolCall:this.afterToolCall,prepareNextTurn:this. +prepareNextTurnWithContext||this.prepareNextTurn?async context=>{if(this.prepareNextTurnWithContext){return await this.prepareNextTurnWithContext(context,this.signal)}return await this.prepareNextTurn?.(this.signal)}:void 0,convertToLlm:this.convertToLlm,transformContext:this.transformContext,getApiKey:this.getApiKey,getSteeringMessages:async()=>{if(skipInitialSteeringPoll){skipInitialSteeringPoll=false;return[]}return this.steeringQueue.drain()},getFollowUpMessages:async()=>this.followUpQueue.drain()}}async runWithLifecycle(executor){ +if(this.activeRun){throw new Error("Agent is already processing.")}const abortController=new AbortController;let resolvePromise=()=>{};const promise=new Promise(resolve=>{resolvePromise=resolve});this.activeRun={promise,resolve:resolvePromise,abortController};this._state.isStreaming=true;this._state.streamingMessage=void 0;this._state.errorMessage=void 0;try{await executor(abortController.signal)}catch(error){await this.handleRunFailure(error,abortController.signal.aborted)}finally{this.finishRun()}}async handleRunFailure(error,aborted){ +const failureMessage={role:"assistant",content:[{type:"text",text:""}],api:this._state.model.api,provider:this._state.model.provider,model:this._state.model.id,usage:EMPTY_USAGE,stopReason:aborted?"aborted":"error",errorMessage:error instanceof Error?error.message:String(error),timestamp:Date.now()};await this.processEvents({type:"message_start",message:failureMessage});await this.processEvents({type:"message_end",message:failureMessage});await this.processEvents({type:"turn_end",message:failureMessage, +toolResults:[]});await this.processEvents({type:"agent_end",messages:[failureMessage]})}finishRun(){this._state.isStreaming=false;this._state.streamingMessage=void 0;this._state.pendingToolCalls=new Set;this.activeRun?.resolve();this.activeRun=void 0}async processEvents(event){switch(event.type){case"message_start":this._state.streamingMessage=event.message;break;case"message_update":this._state.streamingMessage=event.message;break;case"message_end":this._state.streamingMessage=void 0;this._state. +messages.push(event.message);break;case"tool_execution_start":{const pendingToolCalls=new Set(this._state.pendingToolCalls);pendingToolCalls.add(event.toolCallId);this._state.pendingToolCalls=pendingToolCalls;break}case"tool_execution_end":{const pendingToolCalls=new Set(this._state.pendingToolCalls);pendingToolCalls.delete(event.toolCallId);this._state.pendingToolCalls=pendingToolCalls;break}case"turn_end":if(event.message.role==="assistant"&&event.message.errorMessage){this._state.errorMessage= +event.message.errorMessage}break;case"agent_end":this._state.streamingMessage=void 0;break}const signal=this.activeRun?.abortController.signal;if(!signal){throw new Error("Agent listener invoked outside active run")}for(const listener of this.listeners){await listener(event,signal)}}};var events=[];var pendingModels=new Map;var pendingTools=new Map;var agent=null;var emptyUsage=()=>({input:0,output:0,cacheRead:0,cacheWrite:0,reasoning:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}});function modelFor(config){const provider=config.provider||"openai";const deepseek=provider==="deepseek";const anthropic=provider==="anthropic";const defaultModel=deepseek?"deepseek-v4-flash":"gpt-5-mini";return{id:config.model||defaultModel,name:config.model||defaultModel, +provider,api:anthropic?"anthropic-messages":"openai-completions",baseUrl:deepseek?"https://api.deepseek.com":anthropic?"https://api.anthropic.com":"https://api.openai.com/v1",reasoning:deepseek,thinkingLevelMap:deepseek?{off:null,minimal:null,low:null,medium:null,high:"high",xhigh:"max",max:"max"}:void 0,input:["text"],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:deepseek?1e6:128e3,maxTokens:deepseek?384e3:16384}}function hostStream(model,context,options={}){const stream=new AssistantMessageEventStream; +const partial={role:"assistant",content:[],api:model.api,provider:model.provider,model:model.id,usage:emptyUsage(),stopReason:"stop",timestamp:Date.now()};try{if(!globalThis.host)throw new Error("Pocket Pi Agent host is unavailable");const id=globalThis.host.startModel(JSON.stringify({model,context,options}));pendingModels.set(id,{stream,model,partial,started:false,thinkingStarted:false,textStarted:false,thinking:"",text:""})}catch(error){pushModelError(stream,model,String(error))}return stream} +function ensureModelStarted(pending){if(pending.started)return;pending.started=true;pending.stream.push({type:"start",partial:{...pending.partial}})}function syncContent(pending){const content=[];if(pending.thinkingStarted){content.push({type:"thinking",thinking:pending.thinking,thinkingSignature:"reasoning_content"})}if(pending.textStarted)content.push({type:"text",text:pending.text});pending.partial.content=content}function pushThinkingDelta(pending,delta){if(!delta)return;if(pending.textStarted) +throw new Error("thinking delta arrived after text output started");ensureModelStarted(pending);if(!pending.thinkingStarted){pending.thinkingStarted=true;syncContent(pending);pending.stream.push({type:"thinking_start",contentIndex:0,partial:{...pending.partial}})}pending.thinking+=delta;syncContent(pending);pending.stream.push({type:"thinking_delta",contentIndex:0,delta,partial:{...pending.partial}})}function textIndex(pending){return pending.thinkingStarted?1:0}function pushTextDelta(pending,delta){ +if(!delta)return;ensureModelStarted(pending);if(!pending.textStarted){pending.textStarted=true;syncContent(pending);pending.stream.push({type:"text_start",contentIndex:textIndex(pending),partial:{...pending.partial}})}pending.text+=delta;syncContent(pending);pending.stream.push({type:"text_delta",contentIndex:textIndex(pending),delta,partial:{...pending.partial}})}function appendFinalDelta(current,complete,append,label){if(current===complete)return;if(!complete.startsWith(current))throw new Error( +`${label} stream does not match final result`);append(complete.slice(current.length))}function finishModel(pending,result){ensureModelStarted(pending);if(typeof result.thinking!=="string"||typeof result.text!=="string"){throw new Error("model result is missing thinking or text")}if(!Array.isArray(result.toolCalls))throw new Error("model result is missing toolCalls");if(!result.usage||typeof result.usage!=="object"){throw new Error("model result is missing usage")}if(result.thinking&&typeof result. +thinkingSignature!=="string"){throw new Error("thinking result is missing thinkingSignature")}if(!["stop","length","toolUse"].includes(result.stopReason)){throw new Error("model result has an invalid stopReason")}appendFinalDelta(pending.thinking,result.thinking,delta=>pushThinkingDelta(pending,delta),"thinking");appendFinalDelta(pending.text,result.text,delta=>pushTextDelta(pending,delta),"text");pending.partial.usage={...emptyUsage(),...result.usage};if(pending.thinkingStarted){const thinking=pending. +partial.content[0];thinking.thinkingSignature=result.thinkingSignature;pending.stream.push({type:"thinking_end",contentIndex:0,content:pending.thinking,partial:{...pending.partial}})}if(pending.textStarted){pending.stream.push({type:"text_end",contentIndex:textIndex(pending),content:pending.text,partial:{...pending.partial}})}if(result.toolCalls.length>0&&result.stopReason!=="toolUse"){throw new Error("tool calls require toolUse stopReason")}if(result.toolCalls.length===0&&result.stopReason==="t\ +oolUse"){throw new Error("toolUse stopReason requires tool calls")}for(const call of result.toolCalls){if(!call.id||!call.name||!call.arguments||typeof call.arguments!=="object"||Array.isArray(call.arguments)){throw new Error("model result contains an invalid tool call")}const toolCall={type:"toolCall",id:call.id,name:call.name,arguments:call.arguments};const contentIndex=pending.partial.content.length;pending.partial.content=[...pending.partial.content,toolCall];pending.stream.push({type:"toolc\ +all_start",contentIndex,partial:{...pending.partial}});pending.stream.push({type:"toolcall_end",contentIndex,toolCall,partial:{...pending.partial}})}if(pending.partial.content.length===0)throw new Error("model result contains no decision");pending.partial.stopReason=result.stopReason;pending.stream.push({type:"done",reason:result.stopReason,message:{...pending.partial}})}function pushModelError(stream,model,message){stream.push({type:"error",reason:"error",error:{role:"assistant",content:[],api:model. +api,provider:model.provider,model:model.id,usage:emptyUsage(),stopReason:"error",errorMessage:message,timestamp:Date.now()}})}function boot(configJson){const config=JSON.parse(configJson);const model=modelFor(config);const tools=(config.tools||[]).map(tool=>({name:tool.name,label:tool.label||tool.name,description:tool.description||"",parameters:tool.parameters||{type:"object",properties:{}},executionMode:"sequential",execute:(id,args)=>new Promise((resolve,reject)=>{try{if(!globalThis.host)throw new Error( +"Pocket Pi Agent host is unavailable");const requestId=globalThis.host.startTool(id,tool.name,JSON.stringify(args||{}));pendingTools.set(requestId,{resolve,reject})}catch(error){reject(error instanceof Error?error:new Error(String(error)))}})}));agent=new Agent({initialState:{systemPrompt:config.systemPrompt||"You are Pocket Pi running on an embedded device.",model,thinkingLevel:model.reasoning?config.thinkingLevel||"high":"off",tools},streamFn:hostStream,toolExecution:"sequential"});agent.subscribe( +event=>{const compact={type:event.type};if(event.type==="message_update"){compact.kind=event.assistantMessageEvent?.type;compact.delta=event.assistantMessageEvent?.delta}else if(event.type==="message_end"){compact.role=event.message?.role;compact.stopReason=event.message?.stopReason}else if(event.type==="tool_execution_start"||event.type==="tool_execution_end"){compact.name=event.toolName;compact.toolCallId=event.toolCallId;compact.isError=Boolean(event.isError)}events.push(compact)});events.push( +{type:"agent_ready"})}function prompt(text){if(!agent)throw new Error("prompt before boot");void agent.prompt(text).catch(error=>events.push({type:"agent_error",message:String(error)}))}function tick(){const batch=JSON.parse(globalThis.host?.poll()||"[]");for(const event of batch){if(event.type==="model_progress"){const pending=pendingModels.get(event.id);if(!pending)continue;pushThinkingDelta(pending,event.thinkingDelta);pushTextDelta(pending,event.textDelta)}else if(event.type==="model_done"){ +const pending=pendingModels.get(event.id);if(!pending)continue;pendingModels.delete(event.id);try{finishModel(pending,JSON.parse(event.result))}catch(error){pushModelError(pending.stream,pending.model,String(error))}}else if(event.type==="model_error"){const pending=pendingModels.get(event.id);if(!pending)continue;pendingModels.delete(event.id);pushModelError(pending.stream,pending.model,event.error)}else if(event.type==="tool_done"){const pending=pendingTools.get(event.id);if(!pending)continue; +pendingTools.delete(event.id);try{const result=JSON.parse(event.result);if(result.isError)throw new Error(String(result.text||"App tool failed"));pending.resolve({content:[{type:"text",text:String(result.text||"")}],details:result.details,terminate:Boolean(result.terminate)})}catch(error){pending.reject(error instanceof Error?error:new Error(String(error)))}}}}function drain(){return JSON.stringify({phase:agent?.state.isStreaming?"thinking":agent?"ready":"idle",messages:agent?.state.messages.length|| +0,events:events.splice(0,events.length)})}globalThis.PocketPiEmbedded={boot,prompt,tick,drain};})(); diff --git a/apps/pi-agent/dist/app.js b/apps/pi-agent/dist/app.js new file mode 100644 index 0000000..4ffff7f --- /dev/null +++ b/apps/pi-agent/dist/app.js @@ -0,0 +1,16 @@ +(()=>{var kJ={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return H1(this.context.count)},getNextContextId(){return H1(this.context.count++)}};function H1(J){let Q=String(J),$=Q.length-1;return kJ.context.id+($?String.fromCharCode(96+$):"")+Q}function j1(J){kJ.context=J}function q6(){return{...kJ.context,id:kJ.getNextContextId(),count:0}}var X6=!1,Y6=(J,Q)=>J===Q,A0=Symbol("solid-proxy"),z6=typeof Proxy==="function",G6=Symbol("solid-track"),DQ=Symbol("solid-dev-component"),oJ={equals:Y6},K1=null,W6=V1,l=1,yJ=2,F1={owned:null,cleanups:null,context:null,owner:null},S=null,K=null,fJ=null,SJ=null,b=null,E=null,T=null,nJ=0;function tJ(J,Q){let $=b,Z=S,q=J.length===0,X=Q===void 0?Z:Q,z=q?F1:{owned:null,cleanups:null,context:X?X.context:null,owner:X},W=q?J:()=>J(()=>QJ(()=>$J(z)));S=z,b=null;try{return c(W,!0)}finally{b=$,S=Z}}function C(J,Q){Q=Q?Object.assign({},oJ,Q):oJ;let $={value:J,observers:null,observerSlots:null,comparator:Q.equals||void 0},Z=(q)=>{if(typeof q==="function")if(K&&K.running&&K.sources.has($))q=q($.tValue);else q=q($.value);return R1($,q)};return[U1.bind($),Z]}function JJ(J,Q,$){let Z=D1(J,Q,!1,l);if(fJ&&K&&K.running)E.push(Z);else eJ(Z)}function bJ(J,Q,$){$=$?Object.assign({},oJ,$):oJ;let Z=D1(J,Q,!0,0);if(Z.observers=null,Z.observerSlots=null,Z.comparator=$.equals||void 0,fJ&&K&&K.running)Z.tState=l,E.push(Z);else eJ(Z);return U1.bind(Z)}function B6(J){return c(J,!1)}function QJ(J){if(!SJ&&b===null)return J();let Q=b;b=null;try{if(SJ)return SJ.untrack(J);return J()}finally{b=Q}}function _0(J){if(S===null);else if(S.cleanups===null)S.cleanups=[J];else S.cleanups.push(J);return J}function H6(J){if(K&&K.running)return J(),K.done;let Q=b,$=S;return Promise.resolve().then(()=>{b=Q,S=$;let Z;if(fJ||j6)Z=K||(K={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),Z.done||(Z.done=new Promise((q)=>Z.resolve=q)),Z.running=!0;return c(J,!1),b=S=null,Z?Z.done:void 0})}var[VQ,M1]=C(!1),j6;function U1(){let J=K&&K.running;if(this.sources&&(J?this.tState:this.state))if((J?this.tState:this.state)===l)eJ(this);else{let Q=E;E=null,c(()=>J0(this),!1),E=Q}if(b){let Q=this.observers;if(!Q||Q[Q.length-1]!==b){let $=Q?Q.length:0;if(!b.sources)b.sources=[this],b.sourceSlots=[$];else b.sources.push(this),b.sourceSlots.push($);if(!Q)this.observers=[b],this.observerSlots=[b.sources.length-1];else Q.push(b),this.observerSlots.push(b.sources.length-1)}}if(J&&K.sources.has(this))return this.tValue;return this.value}function R1(J,Q,$){let Z=K&&K.running&&K.sources.has(J)?J.tValue:J.value;if(!J.comparator||!J.comparator(Z,Q)){if(K){let q=K.running;if(q||!$&&K.sources.has(J))K.sources.add(J),J.tValue=Q;if(!q)J.value=Q}else J.value=Q;if(J.observers&&J.observers.length)c(()=>{for(let q=0;q1e6)throw E=[],Error()},!1)}return Q}function eJ(J){if(!J.fn)return;$J(J);let Q=nJ;if(P1(J,K&&K.running&&K.sources.has(J)?J.tValue:J.value,Q),K&&!K.running&&K.sources.has(J))queueMicrotask(()=>{c(()=>{K&&(K.running=!0),b=S=J,P1(J,J.tValue,Q),b=S=null},!1)})}function P1(J,Q,$){let Z,q=S,X=b;b=S=J;try{Z=J.fn(Q)}catch(z){if(J.pure)if(K&&K.running)J.tState=l,J.tOwned&&J.tOwned.forEach($J),J.tOwned=void 0;else J.state=l,J.owned&&J.owned.forEach($J),J.owned=null;return J.updatedAt=$+1,S0(z)}finally{b=X,S=q}if(!J.updatedAt||J.updatedAt<=$){if(J.updatedAt!=null&&"observers"in J)R1(J,Z,!0);else if(K&&K.running&&J.pure){if(!K.sources.has(J))J.value=Z;K.sources.add(J),J.tValue=Z}else J.value=Z;J.updatedAt=$}}function D1(J,Q,$,Z=l,q){let X={fn:J,state:Z,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:Q,owner:S,context:S?S.context:null,pure:$};if(K&&K.running)X.state=0,X.tState=Z;if(S===null);else if(S!==F1)if(K&&K.running&&S.pure)if(!S.tOwned)S.tOwned=[X];else S.tOwned.push(X);else if(!S.owned)S.owned=[X];else S.owned.push(X);if(SJ&&X.fn){let z=X.fn,[W,U]=C(void 0,{equals:!1}),P=SJ.factory(z,U);_0(()=>P.dispose());let D,B=()=>H6(U).then(()=>{if(D)D.dispose(),D=void 0});X.fn=(_)=>{if(W(),K&&K.running){if(!D)D=SJ.factory(z,B);return D.track(_)}return P.track(_)}}return X}function k0(J){let Q=K&&K.running;if((Q?J.tState:J.state)===0)return;if((Q?J.tState:J.state)===yJ)return J0(J);if(J.suspense&&QJ(J.suspense.inFallback))return J.suspense.effects.push(J);let $=[J];while((J=J.owner)&&(!J.updatedAt||J.updatedAt=0;Z--){if(J=$[Z],Q){let q=J,X=$[Z+1];while((q=q.owner)&&q!==X)if(K.disposed.has(q))return}if((Q?J.tState:J.state)===l)eJ(J);else if((Q?J.tState:J.state)===yJ){let q=E;E=null,c(()=>J0(J,$[0]),!1),E=q}}}function c(J,Q){if(E)return J();let $=!1;if(!Q)E=[];if(T)$=!0;else T=[];nJ++;try{let Z=J();return K6($),Z}catch(Z){if(!$)T=null;E=null,S0(Z)}}function K6(J){if(E){if(fJ&&K&&K.running)F6(E);else V1(E);E=null}if(J)return;let Q;if(K){if(!K.promises.size&&!K.queue.size){let{sources:Z,disposed:q}=K;T.push.apply(T,K.effects),Q=K.resolve;for(let X of T)"tState"in X&&(X.state=X.tState),delete X.tState;K=null,c(()=>{for(let X of q)$J(X);for(let X of Z){if(X.value=X.tValue,X.owned)for(let z=0,W=X.owned.length;zW6($),!1);if(Q)Q()}function V1(J){for(let Q=0;Q{Z.delete($),c(()=>{K.running=!0,k0($)},!1),K&&(K.running=!1)})}}function J0(J,Q){let $=K&&K.running;if($)J.tState=0;else J.state=0;for(let Z=0;Z=0;Q--)$J(J.tOwned[Q]);delete J.tOwned}if(K&&K.running&&J.pure)O1(J,!0);else if(J.owned){for(Q=J.owned.length-1;Q>=0;Q--)$J(J.owned[Q]);J.owned=null}if(J.cleanups){for(Q=J.cleanups.length-1;Q>=0;Q--)J.cleanups[Q]();J.cleanups=null}if(K&&K.running)J.tState=0;else J.state=0}function O1(J,Q){if(!Q)J.tState=0,K.disposed.add(J);if(J.owned)for(let $=0;$1?[]:null;return _0(()=>A1(X)),()=>{let U=J()||[],P=U.length,D,B;return U[G6],QJ(()=>{let g,w,a,HJ,IJ,F,j,H,L;if(P===0){if(z!==0)A1(X),X=[],Z=[],q=[],z=0,W&&(W=[]);if($.fallback)Z=[U6],q[0]=tJ((A)=>{return X[0]=A,$.fallback()}),z=1}else if(z===0){q=Array(P);for(B=0;B=F&&H>=F&&Z[j]===U[H];j--,H--)a[H]=q[j],HJ[H]=X[j],W&&(IJ[H]=W[j]);g=new Map,w=Array(H+1);for(B=H;B>=F;B--)L=U[B],D=g.get(L),w[B]=D===void 0?-1:D,g.set(L,B);for(D=F;D<=j;D++)if(L=Z[D],B=g.get(L),B!==void 0&&B!==-1)a[B]=q[D],HJ[B]=X[D],W&&(IJ[B]=W[D]),B=w[B],g.set(L,B);else X[D]();for(B=F;BJ(Q||{}));return j1($),Z}}return QJ(()=>J(Q||{}))}function Q0(){return!0}var V6={get(J,Q,$){if(Q===A0)return $;return J.get(Q)},has(J,Q){if(Q===A0)return!0;return J.has(Q)},set:Q0,deleteProperty:Q0,getOwnPropertyDescriptor(J,Q){return{configurable:!0,enumerable:!0,get(){return J.get(Q)},set:Q0,deleteProperty:Q0}},ownKeys(J){return J.keys()}};function b0(J){return!(J=typeof J==="function"?J():J)?{}:J}function L6(){for(let J=0,Q=this.length;J=0;W--){let U=b0(J[W])[z];if(U!==void 0)return U}},has(z){for(let W=J.length-1;W>=0;W--)if(z in b0(J[W]))return!0;return!1},keys(){let z=[];for(let W=0;W=0;z--){let W=J[z];if(!W)continue;let U=Object.getOwnPropertyNames(W);for(let P=U.length-1;P>=0;P--){let D=U[P];if(D==="__proto__"||D==="constructor")continue;let B=Object.getOwnPropertyDescriptor(W,D);if(!Z[D])Z[D]=B.get?{enumerable:!0,configurable:!0,get:L6.bind($[D]=[B.get.bind(W)])}:B.value!==void 0?B:void 0;else{let _=$[D];if(_){if(B.get)_.push(B.get.bind(W));else if(B.value!==void 0)_.push(()=>B.value)}}}}let q={},X=Object.keys(Z);for(let z=X.length-1;z>=0;z--){let W=X[z],U=Z[W];if(U&&U.get)Object.defineProperty(q,W,U);else q[W]=U?U.value:void 0}return q}var O6=(J)=>`Stale read from <${J}>.`;function jJ(J){let Q="fallback"in J&&{fallback:()=>J.fallback};return bJ(R6(()=>J.each,J.children,Q||void 0))}function ZJ(J){let Q=J.keyed,$=bJ(()=>J.when,void 0,void 0),Z=Q?$:bJ($,void 0,{equals:(q,X)=>!q===!X});return bJ(()=>{let q=Z();if(q){let X=J.children;return typeof X==="function"&&X.length>0?QJ(()=>X(Q?q:()=>{if(!QJ(Z))throw O6("Show");return $()})):X}return J.fallback},void 0,void 0)}var I6=(J)=>bJ(()=>J());function A6({createElement:J,createTextNode:Q,isTextNode:$,replaceText:Z,insertNode:q,removeNode:X,setProperty:z,getParentNode:W,getFirstChild:U,getNextSibling:P}){function D(F,j,H,L){if(H!==void 0&&!L)L=[];if(typeof j!=="function")return B(F,j,L,H);JJ((A)=>B(F,j(),A,H),L)}function B(F,j,H,L,A){while(typeof H==="function")H=H();if(j===H)return H;let k=typeof j,I=L!==void 0;if(k==="string"||k==="number"){if(k==="number")j=j.toString();if(I){let O=H[0];if(O&&$(O))Z(O,j);else O=Q(j);H=w(F,H,L,O)}else if(H!==""&&typeof H==="string")Z(U(F),H=j);else w(F,H,L,Q(j)),H=j}else if(j==null||k==="boolean")H=w(F,H,L);else if(k==="function")return JJ(()=>{let O=j();while(typeof O==="function")O=O();H=B(F,O,H,L)}),()=>H;else if(Array.isArray(j)){let O=[];if(_(O,j,A))return JJ(()=>H=B(F,O,H,L,!0)),()=>H;if(O.length===0){let TJ=w(F,H,L);if(I)return H=TJ}else if(Array.isArray(H))if(H.length===0)a(F,O,L);else g(F,H,O);else if(H==null||H==="")a(F,O);else g(F,I&&H||[U(F)],O);H=O}else{if(Array.isArray(H)){if(I)return H=w(F,H,L,j);w(F,H,null,j)}else if(H==null||H===""||!U(F))q(F,j);else HJ(F,j,U(F));H=j}return H}function _(F,j,H){let L=!1;for(let A=0,k=j.length;Ar-O){let PQ=j[I];while(O=0;I--){let O=j[I];if(A!==O){let TJ=W(O)===F;if(!k&&!I)TJ?HJ(F,A,O):q(F,A,H);else TJ&&X(F,O)}else k=!0}}else q(F,A,H);return[A]}function a(F,j,H){for(let L=0,A=j.length;LH.children=B(F,j.children,H.children));return JJ(()=>j.ref&&j.ref(F)),JJ(()=>{for(let A in j){if(A==="children"||A==="ref")continue;let k=j[A];if(k===H[A])continue;z(F,A,k,H[A]),H[A]=k}}),H}return{render(F,j){let H;return tJ((L)=>{H=L,D(j,F())}),H},insert:D,spread(F,j,H){if(typeof j==="function")JJ((L)=>IJ(F,j(),L,H));else IJ(F,j,void 0,H)},createElement:J,createTextNode:Q,insertNode:q,setProp(F,j,H,L){return z(F,j,H,L),H},mergeProps:_1,effect:JJ,memo:I6,createComponent:D6,use(F,j,H){return QJ(()=>F(j,H))}}}function _6(J){let Q=A6(J);return Q.mergeProps=_1,Q}var k1=480,S1=272,KJ={view:0,text:1,image:2},k6=1,S6=-1,b6={width:1,height:2,minW:3,minH:4,maxW:5,maxH:6,paddingT:8,paddingR:9,paddingB:10,paddingL:11,marginT:12,marginR:13,marginB:14,marginL:15,gap:16,flexDir:17,justify:18,align:19,grow:20,shrink:21,basis:22,flexWrap:23,posType:24,insetT:25,insetR:26,insetB:27,insetL:28,display:29,overflow:30,zIndex:31,hitPass:32,bgColor:64,gradFrom:65,gradTo:66,gradDir:67,radius:68,opacity:69,borderColor:70,borderWidth:71,shadow:72,bevelOuterLight:77,bevelOuterDark:78,bevelInnerLight:79,bevelInnerDark:80,bevelWidth:81,textColor:96,fontSlot:97,textAlign:98,lineHeight:99,tracking:100,translateX:128,translateY:129,scale:130,rotate:131,scaleX:132,scaleY:133,originX:134,originY:135,rotateX:136,rotateY:137,translateZ:138,perspective:139,arcStart:140,arcSweep:141,arcWidth:142},R={f32:0,color:1,int:2},g6={width:R.f32,height:R.f32,minW:R.f32,minH:R.f32,maxW:R.f32,maxH:R.f32,paddingT:R.f32,paddingR:R.f32,paddingB:R.f32,paddingL:R.f32,marginT:R.f32,marginR:R.f32,marginB:R.f32,marginL:R.f32,gap:R.f32,flexDir:R.int,justify:R.int,align:R.int,grow:R.f32,shrink:R.f32,basis:R.f32,flexWrap:R.int,posType:R.int,insetT:R.f32,insetR:R.f32,insetB:R.f32,insetL:R.f32,display:R.int,overflow:R.int,zIndex:R.int,hitPass:R.int,bgColor:R.color,gradFrom:R.color,gradTo:R.color,gradDir:R.int,radius:R.f32,opacity:R.f32,borderColor:R.color,borderWidth:R.f32,shadow:R.int,bevelOuterLight:R.color,bevelOuterDark:R.color,bevelInnerLight:R.color,bevelInnerDark:R.color,bevelWidth:R.f32,textColor:R.color,fontSlot:R.int,textAlign:R.int,lineHeight:R.f32,tracking:R.f32,translateX:R.f32,translateY:R.f32,scale:R.f32,rotate:R.f32,scaleX:R.f32,scaleY:R.f32,originX:R.f32,originY:R.f32,rotateX:R.f32,rotateY:R.f32,translateZ:R.f32,perspective:R.f32,arcStart:R.f32,arcSweep:R.f32,arcWidth:R.f32},u={FlexDir:{Row:0,Col:1},Justify:{Start:0,Center:1,End:2,Between:3,Around:4},Align:{Start:0,Center:1,End:2,Stretch:3},PosType:{Relative:0,Absolute:1},Display:{Flex:0,None:1},Overflow:{Visible:0,Hidden:1},TextAlign:{Left:0,Center:1,Right:2},GradDir:{ToTop:0,ToBottom:1,ToLeft:2,ToRight:3},Easing:{Linear:0,EaseIn:1,EaseOut:2,EaseInOut:3,OutBack:4,Spring:5,SpringBouncy:6,CubicBezier:7}},w6={PSM_5650:0,PSM_4444:2,PSM_8888:3,PSM_T8:5},C6=1,LQ=2,OQ=1,IQ=2,AQ=131072,_Q=1,kQ=262144,SQ=1,bQ=1,gQ=1,wQ=2,CQ=4,NQ=8,EQ=16,hQ=1,TQ=2;function b1(J,Q,$,Z=255){return((Z&255)<<24|($&255)<<16|(Q&255)<<8|J&255)>>>0}var yQ=1,N6=1263551300,E6=1,h6=32,T6=24,d={SELECT:1,START:8,UP:16,RIGHT:32,DOWN:64,LEFT:128,LTRIGGER:256,RTRIGGER:512,TRIANGLE:4096,CIRCLE:8192,CROSS:16384,SQUARE:32768},FJ=32896,fQ=0.016666666666666666;function g1(J){return J.__viewport??null}var g0=null;function y6(){return null}function w1(J,Q=y6()){if(!Q)return;if(typeof J.__host!=="string")throw Error(`PocketJS: this bundle targets "${Q.target}" but the native host predates platform `+"contracts — add __host/__hostAbi to its ui namespace (see framework/src/host.ts HostOps)");if(J.__host!==Q.target)throw Error(`PocketJS: native target mismatch (bundle=${Q.target}, host=${J.__host})`);if(J.__hostAbi!==Q.hostAbi)throw Error(`PocketJS: native host ABI mismatch (bundle=${Q.hostAbi}, host=${J.__hostAbi??"missing"})`)}function f6(J){let Q=globalThis.ui,$=Q!==void 0&&(typeof Q.__host==="string"||Q.__textures!==void 0);if(J){if(Q!==void 0&&J===Q&&$)return w1(Q),{ops:J,kind:"native",target:Q.__host??"unknown",strict:!1};return{ops:J,kind:"injected",target:J.__host??"injected",strict:!0}}if(Q!==void 0&&$)return w1(Q),{ops:Q,kind:"native",target:Q.__host??"unknown",strict:!1};if(Q)return{ops:Q,kind:"injected",target:"injected",strict:!0};throw Error("PocketJS: no host — pass HostOps to render() (web/test) or run under a native runtime (globalThis.ui)")}function x6(J){g0=J}function gJ(){if(!g0)throw Error("PocketJS: host not installed — call render() first");return g0}function h(){return gJ().ops}function v6(J){globalThis.frame=J}function m6(J){let Q=globalThis,$=Q.__pocketResizeViewport,Z=(q,X)=>J(q,X);return Q.__pocketResizeViewport=Z,()=>{if(Q.__pocketResizeViewport!==Z)return;if($)Q.__pocketResizeViewport=$;else delete Q.__pocketResizeViewport}}function l6(J){let Q=J.slice(1);if(Q.length===3)Q=Q[0]+Q[0]+Q[1]+Q[1]+Q[2]+Q[2];if(Q.length!==6&&Q.length!==8)throw Error(`PocketJS: bad color '${J}' (expected #rgb/#rrggbb/#rrggbbaa)`);if(!/^[0-9a-fA-F]+$/.test(Q))throw Error(`PocketJS: bad color '${J}'`);let $=parseInt(Q,16);if(Q.length===6)return b1($>>>16&255,$>>>8&255,$&255,255);return b1($>>>24&255,$>>>16&255,$>>>8&255,$&255)}function u6(J,Q){let $=g6[J];if(typeof Q==="string"){if($===R.color)return l6(Q);let Z=Number(Q);if(Number.isNaN(Z))throw Error(`PocketJS: non-numeric value '${Q}' for prop '${J}'`);Q=Z}if($===R.color||$===R.int)return Q>>>0;return Q}var $0=60,C1=[1,2,3,4,5,6,10,12,15,20,30,60],w0=$0,qJ=-1,p6=0,xJ=[];function c6(J){if(!Number.isFinite(J)||J<=0)return $0;let Q=C1[0];for(let $ of C1)if(Math.abs($-J)Q.at<=qJ).sort((Q,$)=>Q.at-$.at||Q.seq-$.seq);if(J.length===0)return;xJ=xJ.filter((Q)=>Q.at>qJ);for(let Q of J)Q.cb()}var C0=0.12,Z0=FJ;function a6(J){Z0=J===void 0?FJ:J&65535}function r6(){Z0=FJ}function h1(J){let Q=Math.max(-1,Math.min(1,(J-128)/127)),$=Math.abs(Q);if($>8&255)}function n6(){return h1(Z0&255)}var T1=new Set,t6=0;function e6(){T1.clear(),t6=0,r6()}function J8(J){for(let Q of[...T1])Q(J)}var MJ=null,y1=null;function Q8(J,Q,$){let Z="";for(let q=0;q<$;q++)Z+=String.fromCharCode(J[Q+q]);return Z}function f1(J){let Q=new DataView(J);if(J.byteLength=J.length&&$.slice(0,J.length)===J)Q.push($);return Q.sort(),Q}function vJ(J){N0();let Q=MJ?MJ.get(J):void 0;if(!Q)throw Error("pak: missing key "+J+" (no __pak provided, or the pack is incomplete)");return y1.slice(Q.off,Q.off+Q.len)}var mJ=null,N=null,UJ=null,q0=0,RJ=[],h0=[],T0=[];function v1(J){if(mJ=J,N=null,UJ=null,q0=0,RJ.length=0,h0.length=0,T0.length=0,p){if(p.pressTarget=null,p.target=null,p.spriteDirty=!0,p.fresh=!0,p.vw=0,p.tex>=0){let Q=h();Q.setCursor?.(-1,0,0,0,0),Q.freeTexture?.(p.tex),p.tex=-1}}}function Z8(J,Q){J.onPress=Q??void 0}function q8(J,Q){if(J.focusable=Q,c1(),!Q&&N===J)x(null)}function x(J){if(UJ&&UJ!==J)PJ(null);N=J,h().setFocus(J?J.id:0)}function PJ(J){if(UJ===J)return;let Q=h();if(UJ)Q.setActive?.(UJ.id,0);if(UJ=J,J)Q.setActive?.(J.id,1)}function y0(){return RJ.length>0?RJ[RJ.length-1]:mJ}function f0(J,Q){if(!J)return;if(J.focusable)Q.push(J);if(!Array.isArray(J.children))return;for(let $=0;$=$.length)return;x($[q])}function z8(){if(!N)return null;let J=y0();if(J&&!i(N,J))return null;for(let Q=h0.length-1;Q>=0;Q--){let $=h0[Q];if(J&&!i($.node,J)&&!i(J,$.node))continue;if(i(N,$.node))return $}return null}function G8(J){let Q=z8();if(!Q)return!1;let $=[];if(f0(Q.node,$),$.length===0){if(N)x(null);return!0}let Z=Q.columns,q=N?$.indexOf(N):-1;if(q<0)return x(m1(J)===1?$[0]:$[$.length-1]),!0;let X=q;switch(J){case"right":if(q+1<$.length&&q%Z0)X=q-1;else if(Q.wrap)X=Math.min($.length-1,Math.floor(q/Z)*Z+Z-1);break;case"down":if(q+Z<$.length)X=q+Z;else if(Q.wrap)X=q%Z;break;case"up":if(q-Z>=0)X=q-Z;else if(Q.wrap){X=q%Z;while(X+Z<$.length)X+=Z}break}if(X!==q)x($[X]);return!0}function W8(){if(!N)return null;let J=y0();if(J&&!i(N,J))return null;for(let Q=T0.length-1;Q>=0;Q--){let $=T0[Q];if(J&&!i($.node,J)&&!i(J,$.node))continue;if(i(N,$.node))return $}return null}function X0(J){let Q=W8();if(Q&&Q.move(J))return;if(G8(J))return;Y8(J)}function l1(){u1(N)}function u1(J){let Q=J;while(Q){if(Q.onPress){Q.onPress();return}Q=Q.parent}}function Y0(J){PJ(J)}function B8(J){x(J),u1(J)}function i(J,Q){if(!J||!Q)return!1;let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function x0(J){if(!J)return null;if(J.focusable)return J;if(!Array.isArray(J.children))return null;for(let Q=0;Q=0;q--){let X=x0(Q.children[q]);if(X){x(X);return}}let Z=Q;while(Z){if(Z.focusable){x(Z);return}Z=Z.parent}}x(null)}var p=null,p1=0;function c1(){p1++}var j8=[1,3,5,9,17,33,65,129,257,513,1985,73,149,147,288,480],K8=[0,0,2,6,14,30,62,126,254,510,62,54,98,96,192,0];function F8(){let J=new Uint8Array(1024);for(let Q=0;Q<16;Q++)for(let $=0;$<16;$++){let Z=j8[Q]>>$&1,q=K8[Q]>>$&1;if(!Z&&!q)continue;let X=(Q*16+$)*4,z=q?255:0;J[X]=z,J[X+1]=z,J[X+2]=z,J[X+3]=255}return J}function M8(J,Q){let $=J.sprite;J.spriteDirty=!1;let Z=J.tex,q=-1,X=null;if(typeof $.image==="string")try{X=vJ($.image)}catch(z){if(gJ().strict)throw z;X=null}else if($.image)X=$.image;if(X){if(q=Q.uploadImgEntry?Q.uploadImgEntry(X):U8(Q,X),q<0&&gJ().strict)throw Error("enableCursor: cursor image rejected (malformed or RLE-only IMG entry)")}if(q<0)q=Q.uploadTexture(F8(),16,16,w6.PSM_8888);if(J.tex=q,Q.setCursor(q,$.hotspot[0],$.hotspot[1],$.size[0],$.size[1]),Z>=0&&Z!==q)Q.freeTexture?.(Z)}function U8(J,Q){if(Q.length<8)return-1;let $=new DataView(Q.buffer,Q.byteOffset,Q.byteLength);if(Q[5]&C6)return-1;return J.uploadTexture(Q.subarray(8),$.getUint16(0,!0),$.getUint16(2,!0),Q[4])}function z0(J,Q){if(!J||Q===0)return null;if(J.id===Q)return J;let $=J.children;if(!Array.isArray($))return null;for(let Z=0;Z<$.length;Z++){let q=z0($[Z],Q);if(q)return q}return null}var G0=null;function d1(J){G0=J}function i1(J){let Q=RJ.length>0?RJ[RJ.length-1]:null,$=J;while($){if($.focusable&&(!Q||i($,Q)))return $;$=$.parent}return null}function s1(J,Q,$){return i1(a1(J,Q,$))}function a1(J,Q,$){if($!==void 0)return $===0?null:z0(G0??mJ,$);let Z=h(),q=Z.hitTestBounds??Z.hitTest;if(!q)return null;return z0(G0??mJ,q(J,Q))}function R8(J,Q,$){let Z=p,q=h();if(!q.hitTest||!q.setCursor||!q.setCursorPos)return!1;if(Z.vw===0){let B=g1(q);if(Z.vw=B?B.w:k1,Z.vh=B?B.h:S1,Z.x<0)Z.x=Math.floor(Z.vw/2),Z.y=Math.floor(Z.vh/2)}if(Z.spriteDirty)M8(Z,q);let X=o6()*Z.speed,z=n6()*Z.speed;if(Z.dpadSpeed>0&&X===0&&z===0){if(J&d.LEFT)X=-Z.dpadSpeed;if(J&d.RIGHT)X=Z.dpadSpeed;if(J&d.UP)z=-Z.dpadSpeed;if(J&d.DOWN)z=Z.dpadSpeed}let W=Z.fresh;if(X!==0||z!==0){let B=d6()/60,_=Math.min(Math.max(Z.x+X*B,0),Z.vw-1),g=Math.min(Math.max(Z.y+z*B,0),Z.vh-1);if(_!==Z.x||g!==Z.y)Z.x=_,Z.y=g,W=!0}if(W)q.setCursorPos(Z.x,Z.y);let U=(Q|$)&Z.button,P=p1;if(W||U!==0||P!==Z.gen)Z.gen=P,Z.fresh=!1,Z.target=i1(z0(G0??mJ,q.hitTest(Z.x,Z.y)));let D=Z.target;if(D!==N)x(D);if(Q&Z.button&&D)Z.pressTarget=D;if(Z.pressTarget){if(PJ(D===Z.pressTarget?Z.pressTarget:null),$&Z.button){let B=D===Z.pressTarget;if(Z.pressTarget=null,PJ(null),B)l1()}}else if($&Z.button)PJ(null);return!0}function P8(J){let Q=J&~q0,$=q0&~J;if(q0=J,p&&R8(J,Q,$))return;if($&d.CIRCLE)PJ(null);if(Q===0)return;if(Q&d.DOWN)X0("down");if(Q&d.RIGHT)X0("right");if(Q&d.UP)X0("up");if(Q&d.LEFT)X0("left");if(Q&d.CIRCLE)PJ(N),l1()}var v0=null;function r1(J){v0=J}function wJ(){if(c1(),v0)v0()}function D8(J,Q){J.debugName=Q||void 0,wJ()}var XJ={id:k6,type:KJ.view,parent:null,children:[],domNodeType:1,domTag:"root"},o1=Symbol.for("pocketjs.native-node"),m0=1,lJ=3,o=8,V8=new Set(["class","className","style","src","onPress","on:press","focusable","debugName","ref","nodeRef","key","children"]);function W0(J){return J.domAttrs??={}}function n1(J,Q){let $=J.domNodeType??(uJ(J)?lJ:m0),Z=$===lJ?B0(J.text??""):$===o?I8(J.domData??""):q4(J.domTag??j0(J));for(let q of Object.keys(J.domAttrs??{}))l0(Z,q,J.domAttrs[q]);if(Q)for(let q of J.children)YJ(Z,n1(q,!0));return Z}function l0(J,Q,$){if(V8.has(Q)){NJ(J,Q,$,J.domAttrs?.[Q]);return}if($==null)delete W0(J)[Q];else W0(J)[Q]=$}function u0(J){if(J[o1]===!0)return J;return Object.defineProperty(J,o1,{value:!0}),Object.defineProperties(J,{nodeType:{configurable:!0,get(){return J.domNodeType??(uJ(J)?lJ:m0)}},nodeValue:{configurable:!0,get(){return J.domNodeType===o?J.domData??"":J.text??""},set($){if(J.domNodeType===o)J.domData=String($??"");else H0(J,String($??""))}},data:{configurable:!0,get(){return J.domNodeType===o?J.domData??"":J.text??""},set($){if(J.domNodeType===o)J.domData=String($??"");else H0(J,String($??""))}},textContent:{configurable:!0,get(){if(J.domNodeType===o)return J.domData??"";if(uJ(J))return J.text??"";return J.children.map(($)=>$.text??"").join("")},set($){let Z=String($??"");if(J.domNodeType===o)J.domData=Z;else if(uJ(J))H0(J,Z);else if(w8(J),Z)YJ(J,B0(Z))}},parentNode:{configurable:!0,get(){return J.parent}},parentElement:{configurable:!0,get(){return J.parent}},childNodes:{configurable:!0,get(){return J.children}},firstChild:{configurable:!0,get(){return J.children[0]??null}},lastChild:{configurable:!0,get(){return J.children[J.children.length-1]??null}},nextSibling:{configurable:!0,get(){return Y4(J)??null}},previousSibling:{configurable:!0,get(){let $=J.parent;if(!$)return null;let Z=$.children.indexOf(J);return Z>0?$.children[Z-1]:null}},tagName:{configurable:!0,get(){return(J.domTag??j0(J)).toUpperCase()}},nodeName:{configurable:!0,get(){if(J.domNodeType===lJ)return"#text";if(J.domNodeType===o)return"#comment";return(J.domTag??j0(J)).toUpperCase()}},className:{configurable:!0,get(){return String(J.domAttrs?.class??"")},set($){NJ(J,"class",$,J.domAttrs?.class)}},isConnected:{configurable:!0,get(){let $=J;while($){if($===XJ)return!0;$=$.parent}return!1}}}),Object.assign(J,{appendChild($){return YJ(J,$),$},insertBefore($,Z){return YJ(J,$,Z??null),$},removeChild($){return pJ(J,$),$},replaceChild($,Z){return YJ(J,$,Z),pJ(J,Z),Z},cloneNode($=!1){return n1(J,!!$)},remove(){if(J.parent)pJ(J.parent,J)},setAttribute($,Z){l0(J,$,Z)},removeAttribute($){l0(J,$,void 0)},getAttribute($){let Z=J.domAttrs?.[$];return Z==null?null:String(Z)},hasAttribute($){return J.domAttrs?.[$]!=null},hasChildNodes(){return J.children.length>0},contains($){let Z=$??null;while(Z){if(Z===J)return!0;Z=Z.parent}return!1},addEventListener(){},removeEventListener(){}},{style:{length:0,item:()=>""},classList:{add(){},remove(){}}}),J}u0(XJ);var p0=null;function L8(J){p0=J}var c0={unknownClass:0,unknownTexture:0},t1=new Map;function e1(J,Q){t1.set(J,Q)}var J4=new Map;function Q4(J,Q){J4.set(J,Q)}var CJ=new Set,O8=new Set;function $4(J){if(!J)return!1;if(O8.has(J))return!0;if(J.children){for(let Q=0;Q - only view/text/image exist`);return u0({id:h().createNode(Q),type:Q,parent:null,children:[],domNodeType:m0,domTag:J})}function B0(J){let Q=h(),$=Q.createNode(KJ.text);return Q.setText($,J),u0({id:$,type:KJ.text,parent:null,children:[],text:J,domNodeType:lJ,domTag:"#text"})}function I8(J=""){let Q=B0("");return Q.domNodeType=o,Q.domTag="#comment",Q.domData=J,Q}function H0(J,Q){h().replaceText(J.id,Q),J.text=Q,wJ()}function uJ(J){return J.type===KJ.text}function X4(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);if($>=0)Q.children.splice($,1);J.parent=null}function YJ(J,Q,$){let Z=h();if(X4(Q),CJ.delete(Q),Z.insertBefore(J.id,Q.id,$?$.id:0),$){let q=J.children.indexOf($);if(q<0)throw Error("PocketJS: insert anchor is not a child of parent");J.children.splice(q,0,Q)}else J.children.push(Q);Q.parent=J,wJ()}function pJ(J,Q){if(!Q)return;H8(Q),h().removeChild(J.id,Q.id),X4(Q),CJ.add(Q),wJ()}function A8(J){return J.parent??void 0}function _8(J){return J.children[0]}function Y4(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);return $>=0?Q.children[$+1]:void 0}function k8(J,Q){let $=h();if(wJ(),Q==null||Q===""){$.setStyle(J.id,S6);return}if(typeof Q!=="string")throw Error("PocketJS: class must be a string literal of utilities");let Z=p0?p0(Q):void 0;if(Z===void 0){if(gJ().strict)throw Error(`PocketJS: unknown class "${Q}" - not in the compiled style table (dynamic classes must be ternaries of full literals)`);c0.unknownClass++;return}$.setStyle(J.id,Z)}function S8(J,Q){let $=h();if(Q==null||Q===""){$.setImage(J.id,-1);return}if(typeof Q!=="string")throw Error("PocketJS: src must be a string key");let Z=t1.get(Q);if(Z===void 0){if(gJ().strict)throw Error(`PocketJS: unknown image src "${Q}" - no texture registered under that key`);c0.unknownTexture++;return}$.setImage(J.id,Z)}function b8(J,Q){let $=h();if(Q==null||Q===""){$.setSprite(J.id,-1,0,0,0);return}if(typeof Q!=="string")throw Error("PocketJS: sprite must be a string key");let Z=J4.get(Q);if(Z===void 0){if(gJ().strict)throw Error(`PocketJS: unknown sprite "${Q}" - no sprite atlas registered under that key`);c0.unknownTexture++;return}$.setSprite(J.id,Z.handle,Z.frames,Z.cols,Z.step)}function g8(J,Q,$){let Z=h(),q=Q??{},X=$??{},z=!1;for(let W in q){let U=q[W];if(X[W]===U)continue;let P=b6[W];if(P===void 0)throw Error(`PocketJS: unknown style prop '${W}' (see spec PROP)`);Z.setProp(J.id,P,u6(W,U)),z=!0}if(z)wJ()}function NJ(J,Q,$,Z){if($===Z&&Q!=="style")return $;if(Q==="className")Q="class";if(Q!=="children"&&Q!=="key"&&Q!=="ref"&&Q!=="nodeRef")if($==null)delete W0(J)[Q];else W0(J)[Q]=$;switch(Q){case"class":return k8(J,$),$;case"onPress":case"on:press":return Z8(J,$),$;case"src":return S8(J,$),$;case"sprite":return b8(J,$),$;case"style":return g8(J,$,Z),$;case"focusable":return q8(J,!!$),$;case"debugName":return D8(J,$==null?void 0:String($)),$;case"ref":case"nodeRef":case"key":case"children":return $;default:break}if(Q==="classList")throw Error("PocketJS: classList is not supported - use ternaries of full class literals");if(Q.startsWith("on:")||Q.startsWith("bool:")||Q.startsWith("prop:"))throw Error(`PocketJS: unsupported namespaced attribute '${Q}'`);throw Error(`PocketJS: unknown property '${Q}' on <${j0(J)}>`)}function w8(J){for(let Q of[...J.children])pJ(J,Q)}function j0(J){for(let Q of Object.keys(KJ))if(KJ[Q]===J.type)return Q;return String(J.type)}function C8(J,Q,$,Z){if(Q==="ref"&&typeof $==="function"){$(J);return}NJ(J,Q,$,Z)}var N8=_6({createElement:q4,createTextNode:B0,replaceText:H0,isTextNode:uJ,setProperty:C8,insertNode(J,Q,$){YJ(J,Q,$)},removeNode(J,Q){pJ(J,Q)},getParentNode:A8,getFirstChild:_8,getNextSibling:Y4}),{render:E8,effect:xQ,memo:v,createComponent:Y,createElement:z4,insert:vQ,spread:h8,mergeProps:mQ,use:lQ}=N8,uQ={linear:u.Easing.Linear,in:u.Easing.EaseIn,out:u.Easing.EaseOut,"in-out":u.Easing.EaseInOut,"out-back":u.Easing.OutBack,spring:u.Easing.Spring,"spring-bouncy":u.Easing.SpringBouncy},T8=null;function G4(J){T8=J}function y8(J,Q){if(!J)return;if(typeof J==="function")J(Q);else if("current"in J)J.current=Q}function W4(J,Q){let $=z4(J);return h8($,Q,!1),y8(Q.nodeRef,$),$}function M(J){return W4("view",J)}function V(J){return W4("text",J)}var pQ=new WeakMap,cQ=new WeakMap,f8={}!==null?Object.freeze({...{}}):Object.freeze({}),dQ=Object.freeze({target:"",pixelRatio:Number.isInteger(1)?1:1,features:f8}),iQ=new Map,zJ=36000,d0=30,x8=30,G={ops:null,transport:null,app:void 0,frame:0,tape:new Uint16Array(zJ),tapeAnalog:new Uint16Array(zJ),tapeTouch:null,tapeStart:0,tapeLen:0,tapeFirstFrame:0,replayMasks:null,replayAnalog:null,replayTouch:null,replayAt:0,paused:!1,stepQueued:0,inspectReportId:null,inspectAskedAt:0,treeDirty:!0,treeSentAt:-d0,saidHello:!1,hostCalls:0};function v8(J){let Q=globalThis;if(!Q.console)Q.console={log(){},warn(){},error(){}};G.ops=J,G.frame=0,G.tapeStart=0,G.tapeLen=0,G.tapeFirstFrame=0,G.tapeTouch=null,G.replayMasks=null,G.replayAnalog=null,G.replayTouch=null,G.paused=!1,G.stepQueued=0,G.inspectReportId=null,G.inspectAskedAt=0,G.treeDirty=!0,G.treeSentAt=-d0,G.saidHello=!1,G.hostCalls=0,G.app=globalThis.__pocketApp;let $=globalThis.__pocketDevtoolsTransport;if($)G.transport=$;else if(J.__dbgActive?.()&&J.__dbgPoll&&J.__dbgSend)G.transport={send:(Z)=>J.__dbgSend(Z),recv:()=>J.__dbgPoll(),everyFrames:10};else G.transport=null;if(G.transport)r1(()=>{G.treeDirty=!0}),s8();else r1(null);globalThis.__pocketDevtools=r8}function m8(J){return(Q,$,Z,q)=>{if(G.hostCalls++,G.transport)u8(),d8();let X=Q,z=$===void 0?FJ:$&65535,W=Z,U=q;if(G.replayMasks)if(G.replayAt0?$.slice(0,8):null;if(Z&&!G.tapeTouch)G.tapeTouch=Array(zJ).fill(null);if(G.tapeLen1||Q.length===1&&Q[0][0]!==FJ)J.analog=Q;if(G.tapeTouch){let $=[];for(let Z=0;Z0)J.v=2,J.touch=$}return J}function j4(J,Q,$){let Z=new Uint16Array($).fill(Q),q=0;for(let[X,z]of J)Z.fill(X,q,Math.min(q+z,$)),q+=z;return Z}function K4(J){let Q=0;for(let[,$]of J.masks)Q+=$;return j4(J.masks,0,Q)}function F4(J){let Q=0;for(let[,$]of J.masks)Q+=$;return j4(J.analog??[],FJ,Q)}function M4(J){let Q=0;for(let[,Z]of J.masks)Q+=Z;let $=Array(Q).fill(void 0);for(let[Z,q]of J.touch??[])if(Z>=0&&Z1&&G.hostCalls%Q!==0)return;if(!G.saidHello)G.saidHello=!0,y({t:"hello",app:G.app,host:a8(),frame:G.frame});for(let $=0;$<64;$++){let Z=J.recv();if(!Z)break;for(let q of Z.split(` +`))if(q.trim())p8(q)}}function p8(J){let Q;try{Q=JSON.parse(J)}catch{return}let $=G.ops;switch(Q.t){case"inspect":{let Z=typeof Q.id==="number"?Q.id:0;if($?.debugInspect?.(Z),G.inspectReportId=Z||null,G.inspectAskedAt=G.hostCalls,!Z)y({t:"inspect",id:0,rect:null});break}case"pause":G.paused=!0,G.stepQueued=0,$?.debugPause?.(!0),i0();break;case"resume":G.paused=!1,$?.debugPause?.(!1),i0();break;case"step":G.stepQueued+=typeof Q.n==="number"&&Q.n>0?Q.n:1;break;case"getTree":U4();break;case"eval":{let Z=!0,q;try{q=K0((0,eval)(String(Q.code)))}catch(X){Z=!1,q=X instanceof Error?`${X.name}: ${X.message}`:String(X)}y({t:"evalResult",id:Q.id,ok:Z,value:q});break}case"dumpTape":y({t:"tape",tape:H4()});break;case"devStats":{let Z=null,q=$?.debugStats?.();if(q)try{Z=JSON.parse(q)}catch{Z=null}y({t:"devStats",frame:G.frame,data:Z});break}case"screenshot":{if($?.__dbgShot?.())y({t:"screenshotRaw",file:"shot.raw",w:480,h:272,stride:512,frame:G.frame});else y({t:"log",level:"warn",args:["screenshot: not supported on this host"]});break}case"replay":{let Z=Q.tape;if(Z&&Array.isArray(Z.masks))G.replayMasks=K4(Z),G.replayAnalog=Z.analog?F4(Z):null,G.replayTouch=Z.touch?M4(Z):null,G.replayAt=0;break}default:break}}function c8(){if(G.treeDirty&&G.frame-G.treeSentAt>=d0)U4();if(G.frame%x8===0)i0()}function d8(){let J=G.inspectReportId;if(J==null)return;let Q=G.ops;if(!Q?.debugRectXY||!Q.debugRectWH){G.inspectReportId=null;return}let $=Q.debugRectXY();if($===-1){if(G.hostCalls-G.inspectAskedAt>60)G.inspectReportId=null,y({t:"inspect",id:J,rect:null});return}let Z=Q.debugRectWH();G.inspectReportId=null,y({t:"inspect",id:J,rect:[$<<16>>16,$>>16,Z&65535,Z>>16&65535]})}function i0(){y({t:"stats",frame:G.frame,nodes:D4(XJ),tapeLen:G.tapeLen,paused:G.paused})}function U4(){G.treeDirty=!1,G.treeSentAt=G.frame,y({t:"tree",frame:G.frame,root:P4(XJ)})}function i8(J){if(J==null||typeof J!=="object")return!1;let Q=J;return typeof Q.id==="number"&&typeof Q.type==="number"}function s0(J,Q){if(Array.isArray(J)){for(let $ of J)s0($,Q);return}if(i8(J)){Q(J);return}if(J!=null&&typeof J==="object"){let $=J.nodes;if($!==void 0)s0($,Q)}}function R4(J,Q){let $=Array.isArray(J.children)?J.children:[];for(let Z of $)s0(Z,Q)}function P4(J){let Q={i:J.id,t:J.domTag??String(J.type)};if(J.debugName)Q.n=J.debugName;let $=J.domAttrs?.class;if(typeof $==="string"&&$)Q.c=$;if(J.text)Q.x=J.text.length>80?J.text.slice(0,79)+"…":J.text;let Z=[];if(R4(J,(q)=>{if(q.domNodeType===8)return;Z.push(P4(q))}),Z.length)Q.k=Z;return Q}function D4(J){let Q=1;return R4(J,($)=>{Q+=D4($)}),Q}function s8(){let J=globalThis;if(!J.console)J.console={};let Q=J.console;if(Q.__pocketBridged)return;Q.__pocketBridged=!0;for(let $ of["log","warn","error"]){let Z=Q[$];Q[$]=(...q)=>{y({t:"log",level:$,args:q.map((X)=>K0(X))}),Z?.apply(Q,q)}}}function K0(J,Q=0){if(J===void 0)return"undefined";if(J===null)return"null";let $=typeof J;if($==="string"){let X=J;return Q===0?V4(X):JSON.stringify(V4(X))}if($==="number"||$==="boolean"||$==="bigint")return String(J);if($==="function"){let X=J.name;return X?`[function ${X}]`:"[function]"}if(Q>=3)return Array.isArray(J)?"[…]":"{…}";if(Array.isArray(J)){let X=J.slice(0,20).map((z)=>K0(z,Q+1));if(J.length>20)X.push(`… ${J.length-20} more`);return`[${X.join(", ")}]`}if(J instanceof Error)return`${J.name}: ${J.message}`;return`{${Object.entries(J).slice(0,20).map(([X,z])=>`${X}: ${K0(z,Q+1)}`).join(", ")}}`}function V4(J){return J.length>200?J.slice(0,199)+"…":J}function a8(){let J=G.ops;if(typeof J?.__host==="string")return J.__host;if(J?.__textures!==void 0)return"psp";if(typeof globalThis.document<"u")return"web";return"headless"}var r8={get frame(){return G.frame},dumpTape:()=>H4(),replay:(J)=>{G.replayMasks=K4(J),G.replayAnalog=J.analog?F4(J):null,G.replayTouch=J.touch?M4(J):null,G.replayAt=0}},L4=new Map,a0=new Map;function O4(J){return J.trim().replace(/\s+/g," ")}function I4(J){return J.split(" ").sort().join(" ")}var A4=-1;function o8(J){for(let Q of Object.keys(J)){let $=J[Q],Z=O4(Q);L4.set(Z,$);let q=I4(Z),X=a0.get(q);a0.set(q,X!==void 0&&X!==$?A4:$)}}function n8(J){let Q=O4(J),$=L4.get(Q);if($!==void 0)return $;let Z=a0.get(I4(Q));return Z===A4?void 0:Z}var r0=9,t8=(1<{let q=($&J2)!==0,X=q?o0:r0,z=q?Q2:t8;return Object.freeze({id:$>>>(q?$2:e8)&255,x:$&z,y:$>>>X&z,hit:Q?.[Z]})}))}function q2(){return F0}function X2(){F0=n0}var _4=8,GJ=8,Y2=3,z2=8,G2=6,W2=0.5,s=1,cJ=2,t0=4,DJ=8,EJ=[],k4=0,M0=0,WJ=Array.from({length:_4},(J,Q)=>({slot:Q,used:!1,present:!1,id:0,x:0,y:0,startX:0,startY:0,dx:0,dy:0,fdx:0,fdy:0,vx:0,vy:0,downFrame:0,frames:0,histX:new Int16Array(GJ),histY:new Int16Array(GJ),histHead:0,histLen:0,owners:[],claimedBy:null}));function B2(J,Q){let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function H2(J,Q,$,Z){let q=J.opts.region;if(!q)return!0;let X=q.node?.();if(X){if(Z.hit===void 0)Z.hit=a1(Q,$,Z.fact);let W=Z.hit;if(W)return B2(W,X)}let z=q.rect?.();if(!z)return!1;return Q>=z.x&&Q=z.y&&$=0;z--){let W=EJ[z];if(W.disposed)continue;if(k4>0&&!W.opts.allowWhenBlocked)continue;if(!H2(W,$,Z,X))continue;W.flags[J.slot]=s,J.owners.push(W)}for(let z of J.owners)z.opts.onDown?.(J)}function F2(J,Q,$){if(J.present=!0,J.fdx=Q-J.x,J.fdy=$-J.y,J.x=Q,J.y=$,J.dx=Q-J.startX,J.dy=$-J.startY,J.frames++,J.histX[J.histHead]=Q,J.histY[J.histHead]=$,J.histHead=(J.histHead+1)%GJ,J.histLenz||Z>z))q.flags[J.slot]|=cJ}if(!J.claimedBy)for(let q of J.owners){let X=q.flags[J.slot];if(!(X&s)||X&(cJ|t0))continue;if(!q.opts.onLongPress)continue;let z=Math.max(1,Math.round((q.opts.longPressSeconds??W2)*N1()));if(J.frames=0)EJ.splice($,1)},cancel(){if(!Q.disposed)g4(Q)},get panning(){for(let $ of WJ)if($.used&&Q.flags[$.slot]&DJ)return!0;return!1}}}function D2(J){let Q=P2(J);return _0(()=>Q.dispose()),Q}function w4(){EJ.length=0,k4=0,M0=0;for(let J of WJ)J.used=!1,J.present=!1,J.owners.length=0,J.claimedBy=null}function V2(){return D2({onDown:(J)=>{let Q=s1(J.x,J.y,J.hit);if(Q)Y0(Q)},onTap:(J)=>{Y0(null);let Q=s1(J.x,J.y,J.hit);if(Q)B8(Q)},onUp:()=>Y0(null),onCancel:()=>Y0(null)})}var L2=1,e0=new Map,U0=[];function O2(){let J=globalThis.__pocketEffectTrace;return typeof J==="function"?J:null}function I2(){L2=1,e0.clear(),U0=[]}function A2(){if(U0.length===0)return;let J=U0;U0=[];for(let{id:Q,result:$}of J){let Z=e0.get(Q);if(!Z)continue;e0.delete(Q),O2()?.({t:"delivery",frame:E1(),id:Q,kind:Z.kind}),Z.onResult($)}}var C4=new Set;function _2(){if(C4.size===0)return;for(let J of C4)J()}var k2={"h-[108] px-[10] py-4 flex-row gap-[10] bg-slate-950":0,"w-[165] h-[76] items-center justify-center bg-orange-600":1,"w-[165] h-[76] items-center justify-center bg-slate-900":2,"text-base text-white font-bold":3,"flex-col w-full h-full bg-slate-50":4,"h-[686] px-6 pt-7 flex-col gap-[22]":5,"w-[584] h-[318] px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100":6,"h-[30] flex-row items-center gap-3":7,"w-[10] h-[10] rounded bg-orange-500":8,"text-lg text-orange-600 font-bold":9,"h-[84] pt-3 text-xl text-slate-900":10,"h-[2] mx-1 my-4 bg-slate-100":11,"w-[10] h-[10] rounded bg-emerald-500":12,"text-lg text-emerald-700 font-bold":13,"absolute left-[628] top-[28] w-[68] h-[132] items-center justify-center bg-orange-100":14,"absolute left-[628] top-[512] w-[68] h-[132] items-center justify-center bg-orange-100":15,"h-[228] mx-6 px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100":16,"text-base text-slate-600 font-bold":17,"pt-6 text-lg text-slate-500":18,"pt-6 text-lg text-slate-900 font-bold":19,"h-[146] px-6 pt-7 flex-col bg-slate-50":20,"h-[80]":21,"h-[1060] px-6 pt-5 flex-col":22,"h-[48] px-4 justify-center bg-slate-100":23,"text-base text-slate-600":24,"pt-[10] flex-col gap-[12]":25,"w-[584] h-[92] px-[18] flex-row items-center gap-5 bg-white":26,"w-[48] h-[48] items-center justify-center bg-blue-100":27,"w-[48] h-[48] items-center justify-center bg-emerald-100":28,"text-lg text-blue-700 font-bold":29,"w-[474] flex-col gap-2":30,"text-lg text-slate-900 font-bold":31,"text-base text-slate-500":32,"pt-20 pl-10 text-lg text-slate-500":33,"absolute left-[628] top-[78] w-[68] h-[132] items-center justify-center bg-orange-100":34,"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100":35,"h-[1060] px-6 pt-7 flex-col gap-4":36,"text-base text-slate-500 font-bold":37,"h-[214] px-7 items-center justify-center bg-slate-100":38,"text-lg text-slate-500 font-bold":39,"w-[672] h-[150] px-6 flex-row items-center justify-between bg-white":40,"flex-row items-center gap-5":41,"w-[68] h-[68] items-center justify-center bg-orange-100":42,"text-xl text-orange-700 font-bold":43,"w-[500] flex-col gap-2":44,"text-xl text-slate-900 font-bold":45,"text-lg text-slate-600":46,"text-2xl text-orange-600":47,"mt-4 h-[112] px-6 justify-center bg-slate-100":48,"relative h-[154] px-5 pt-5 flex-col bg-white":49,"pt-3 text-lg text-orange-600 font-bold":50,"pt-3 text-base text-slate-500":51,"absolute left-[456] top-[14] w-[196] h-[72] items-center justify-center bg-orange-600":52,"text-lg text-white font-bold":53,"h-[40] pt-3 text-base text-slate-500":54,"h-[470] flex-col gap-2":55,"w-[584] h-[84] px-5 flex-row items-center justify-between bg-white":56,"absolute left-[604] top-[0] w-[68] h-[132] items-center justify-center bg-orange-100":57,"absolute left-[604] top-[320] w-[68] h-[132] items-center justify-center bg-orange-100":58,"h-[160] px-5 pt-5 flex-col bg-white":59,"pt-3 text-lg text-slate-900 font-bold":60,"pt-3 text-base text-slate-600":61,"h-[108] pt-7 flex-row gap-4":62,"w-[316] h-[80] items-center justify-center bg-slate-100":63,"w-[340] h-[80] items-center justify-center bg-red-100":64,"text-lg text-red-500 font-bold":65,"h-[1052] px-6 pt-5 flex-col":66,"h-[270] px-[22] pt-6 bg-white":67,"text-lg text-slate-900":68,"text-lg text-slate-400":69,"h-[86] px-1 flex-row items-center justify-between":70,"w-[132] h-[58] items-center justify-center bg-slate-300":71,"w-[132] h-[58] items-center justify-center bg-slate-100":72,"text-base text-slate-900 font-bold":73,"h-[140] flex-row gap-2":74,"grow h-[120] items-center justify-center bg-slate-300":75,"grow h-[120] items-center justify-center bg-white":76,"w-[104] h-[120] items-center justify-center bg-slate-300":77,"w-[104] h-[120] items-center justify-center bg-slate-100":78,"h-[176] flex-row gap-2":79,"w-[92] h-[156] items-center justify-center bg-slate-300":80,"w-[92] h-[156] items-center justify-center bg-slate-100":81,"w-[300] h-[156] items-center justify-center bg-slate-300":82,"w-[300] h-[156] items-center justify-center bg-slate-100":83,"w-[144] h-[156] items-center justify-center bg-slate-300":84,"w-[144] h-[156] items-center justify-center bg-slate-100":85,"w-[112] h-[156] items-center justify-center bg-emerald-700":86,"w-[112] h-[156] items-center justify-center bg-emerald-500":87,"text-base text-slate-950 font-bold":88,"h-[108] px-6 py-3 flex-col bg-slate-50":89,"h-[84] items-center justify-center bg-slate-300":90,"h-[84] items-center justify-center bg-slate-100":91,"h-[1168] px-6 pt-5 flex-col":92,"h-[82] px-4 justify-center bg-white":93,"w-[584] h-[900] px-5 pt-5 bg-slate-950":94,"text-base text-slate-200":95,"absolute left-[628] top-[58] w-[68] h-[132] items-center justify-center bg-orange-100":96,"w-[584] h-[900] px-5 pt-5 bg-white":97,"text-xl text-slate-900":98,"relative flex-col w-full h-full bg-slate-50 overflow-hidden":99,"absolute inset-0 z-50 flex-col items-center justify-center":100,"absolute inset-0 bg-slate-950":101,"flex-col gap-2 w-[328] p-3 rounded-xl shadow-lg bg-white border-slate-200":102,"absolute left-3 right-3 bottom-3 flex-row items-center justify-between px-2 py-1 rounded-lg shadow-md bg-white border-slate-200":103,"flex-row flex-wrap":104,grow:105,"text-2xl text-white font-bold":106,"text-2xl text-slate-950 font-bold":107,"px-3 py-2 rounded-lg bg-slate-100":108,"px-3 py-2 rounded-lg bg-indigo-100":109,"text-base text-indigo-700 font-bold":110,"px-3 py-2 rounded-lg bg-emerald-100":111,"text-base text-emerald-700 font-bold":112,"px-3 py-2 rounded-lg bg-amber-100":113,"text-base text-amber-700 font-bold":114,"px-3 py-2 rounded-lg bg-red-100":115,"text-base text-red-500 font-bold":116,"w-[34] h-[34] rounded-lg bg-amber-400":117,"w-[34] h-[34] rounded-lg bg-red-500":118,"w-[34] h-[34] rounded-lg bg-slate-800":119,"w-[34] h-[34] rounded-lg bg-emerald-500":120,"h-[112] px-6 flex-row items-center justify-between bg-slate-950":121,"flex-row items-center gap-4":122,"w-[34] text-2xl text-white font-bold":123,"w-[332] flex-col items-end gap-2":124,"text-base text-slate-300 font-bold":125,"text-base text-slate-400":126,"h-[166] px-6 pt-6 flex-col gap-3":127,"text-base text-orange-600 font-bold":128,"h-[44] px-1 flex-row items-center justify-between":129,"w-full h-full items-center justify-center rounded-xl bg-slate-200":130,"w-full h-full items-center justify-center rounded-xl bg-red-100":131,"w-full h-full items-center justify-center rounded-xl bg-slate-100":132,"w-full h-full items-center justify-center rounded-xl bg-orange-600":133,"w-full h-[150] px-5 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":134,"w-full h-[430] px-12 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":135,"w-[88] h-[88] items-center justify-center rounded-xl bg-indigo-100":136,"w-[88] h-[88] items-center justify-center rounded-xl bg-slate-100":137,"text-2xl text-indigo-700 font-bold":138,"text-2xl text-slate-600 font-bold":139,"pt-7 text-2xl text-slate-900 font-bold":140,"pt-4 text-lg text-slate-500":141,"text-xl text-emerald-600 font-bold":142,"text-xl text-red-500 font-bold":143,"w-full h-full px-5 py-4 flex-col gap-3 rounded-xl shadow bg-white border-slate-100":144,"text-base text-red-300":145,"text-base text-slate-300":146,"text-base text-red-500":147,"w-full h-full px-6 flex-row items-center bg-slate-950":148,"w-full h-full flex-row items-center":149};if(typeof globalThis.queueMicrotask!=="function")globalThis.queueMicrotask=(J)=>{Promise.resolve().then(J)};var S2="ui:styles",b2="ui:font.",N4="ui:img.",E4="ui:sprite.";function g2(){return globalThis.ui}function w2(J){if(J.__textures)return;for(let Q of E0(N4)){let $=vJ(Q),Z;if(J.uploadImgEntry)Z=J.uploadImgEntry($);else{let q=new DataView($.buffer,$.byteOffset,$.byteLength);Z=J.uploadTexture($.subarray(8),q.getUint16(0,!0),q.getUint16(2,!0),$[4])}if(Z>=0)e1(Q.slice(N4.length),Z)}}function C2(J){if(J.__sprites)return;for(let Q of E0(E4)){let $=vJ(Q),Z=new DataView($.buffer,$.byteOffset,$.byteLength),q=Z.getUint16(0,!0),X=Z.getUint16(2,!0),z=$[4],W=Z.getUint16(6,!0),U=Z.getUint16(8,!0),P=Z.getUint16(10,!0),D=J.uploadTexture($.subarray(16),q,X,z);if(D>=0)Q4(Q.slice(E4.length),{handle:D,frames:W,cols:U,step:P})}}function h4(J){let Q=z4("view");return NJ(Q,"style",J,void 0),Q}var R0=null,P0=null;function N2(J,Q){if(!R0||!P0)return;NJ(R0,"style",{width:J,height:Q,overflow:u.Overflow.Hidden},void 0),NJ(P0,"style",{width:J,height:Q,posType:u.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000},void 0);let $=h();$.__viewport={w:J,h:Q}}function E2(J,Q={}){let $=f6(Q.ops);if(x6($),L8(n8),Q.styles)o8(Q.styles);let Z=$.kind==="native"?$.ops.__textures:void 0;if($.kind==="native"){if(Z)for(let _ in Z)e1(_,Z[_]);let B=$.ops.__sprites;if(B)for(let _ in B)Q4(_,B[_])}if($.kind==="injected"||Z===void 0){if(Q.pak)x1(Q.pak);if($8()){for(let B of E0())if(B===S2)$.ops.loadStyles?.(vJ(B));else if(B.startsWith(b2))$.ops.loadFontAtlas?.(vJ(B))}}let q=g1($.ops),X=q?.w??k1,z=q?.h??S1,W=h4({width:X,height:z,overflow:u.Overflow.Hidden}),U=h4({width:X,height:z,posType:u.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000,hitPass:1});YJ(XJ,W),YJ(XJ,U),G4(U),R0=W,P0=U,v1(W),d1(XJ),e6(),w4(),V2(),i6(),I2(),v8($.ops),v6(m8((B,_,g,w)=>{s6(),a6(_),Z2(g,w),_2(),A2(),R2(),J8(B),P8(B),Z4()}));let P=E8(J,W),D=m6(N2);return()=>{D(),X2(),w4(),P(),v1(null),d1(null),G4(null),R0=null,P0=null;for(let B of XJ.children.splice(0))B.parent=null,$.ops.destroyNode(B.id);Z4()}}function h2(J,Q={}){let $=Q.ops??g2();if(!$)throw Error("PocketJS: mount() requires globalThis.ui or opts.ops");if(Q.pak)x1(Q.pak);return w2($),C2($),E2(J,{ops:$,styles:Q.styles??k2,pak:Q.pak})}var T2="$b",y2=65536,T4="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/",dJ={};for(let J=0;JJ.length)throw Error("invalid UTF-8");for(let z=0;z1114111||q>=55296&&q<=57343||X===1&&q<128||X===2&&q<2048||X===3&&q<65536)throw Error("invalid UTF-8");if(q<65536)Q+=String.fromCharCode(q);else q-=65536,Q+=String.fromCharCode(55296+(q>>10),56320+(q&1023))}return Q}function v2(){let J=globalThis.fs;if(!J||typeof J!=="object")return null;return typeof J.read==="function"?J:null}function y4(){let J=v2();if(!J)throw Error("fs: globalThis.fs is not mounted — declare `data.fs` in pocket.json requires");return J}function m2(J,Q){let $=[],Z=0;for(;;){let z=JSON.parse(J.read(Q,Z,y2));if(z.error!==void 0)throw Error(`fs: read ${Q}: ${z.error}`);let W=f2(z.data[T2]);if($.push(W),Z+=W.length,z.eof)break}if($.length===1)return $[0];let q=new Uint8Array(Z),X=0;for(let z of $)q.set(z,X),X+=z.length;return q}function l2(J,Q){let $=m2(y4(),J);return Q==="utf8"||Q==="utf-8"?x2($):$}function u2(J,Q){let $=y4(),Z=[],q=0;for(;;){let X=JSON.parse($.list(J,q));if(X.error!==void 0)throw Error(`fs: readdir ${J}: ${X.error}`);for(let z of X.entries)Z.push({...z,isFile:()=>z.kind==="file",isDirectory:()=>z.kind==="dir"});if(q+=X.entries.length,X.eof)break}return Q?.withFileTypes?Z:Z.map((X)=>X.name)}var p2={appTitle:"text-2xl text-white font-bold",pageTitle:"text-2xl text-slate-950 font-bold",heading:"text-xl text-slate-900 font-bold",label:"text-base text-slate-600 font-bold",captionStrong:"text-base text-slate-500 font-bold"};function c2(J){let Q=()=>J.accent==="busy"?"w-[34] h-[34] rounded-lg bg-amber-400":J.accent==="danger"?"w-[34] h-[34] rounded-lg bg-red-500":J.accent==="none"?"w-[34] h-[34] rounded-lg bg-slate-800":"w-[34] h-[34] rounded-lg bg-emerald-500";return Y(M,{class:"h-[112] px-6 flex-row items-center justify-between bg-slate-950",get children(){return[Y(M,{class:"flex-row items-center gap-4",get children(){return[v(()=>v(()=>!!J.back)()?Y(V,{class:"w-[34] text-2xl text-white font-bold",children:"‹"}):Y(M,{get["class"](){return Q()}})),Y(V,{get["class"](){return p2.appTitle},get children(){return J.title}})]}}),Y(M,{class:"w-[332] flex-col items-end gap-2",get children(){return[Y(V,{class:"text-base text-slate-300 font-bold",get children(){return J.metaTop??""}}),Y(V,{class:"text-base text-slate-400",get children(){return J.metaBottom??""}})]}})]}})}function d2(J){let Q=()=>J.disabled?"w-full h-full items-center justify-center rounded-xl bg-slate-200":J.tone==="danger"?"w-full h-full items-center justify-center rounded-xl bg-red-100":J.tone==="neutral"?"w-full h-full items-center justify-center rounded-xl bg-slate-100":"w-full h-full items-center justify-center rounded-xl bg-orange-600",$=()=>J.disabled?"text-lg text-slate-500 font-bold":J.tone==="danger"?"text-lg text-red-500 font-bold":J.tone==="neutral"?"text-lg text-slate-900 font-bold":"text-lg text-white font-bold";return Y(M,{get["class"](){return Q()},get children(){return Y(V,{get["class"](){return $()},get children(){return J.label}})}})}function iJ(J){return[Y(M,{get["class"](){return J.top},get children(){return Y(V,{class:"text-base text-orange-600 font-bold",children:"UP"})}}),Y(M,{get["class"](){return J.bottom},get children(){return Y(V,{class:"text-base text-orange-600 font-bold",children:"DN"})}})]}var sQ="‘’“”–—…•",i2=512;function s2(J,Q,$){let Z=[],q="",X=0;for(let z of J){let W=Q(z);if(q&&X+W>$)Z.push(q),q="",X=0;q+=z,X+=W}if(q)Z.push(q);return Z}function f4(J,Q,$){let Z=new Map,q=(W)=>{let U=Z.get(W);if(U===void 0)U=h().measureText(W,Q),Z.set(W,U);return U},X=[],z=q(" ");for(let W of J.split(` +`)){let U=W.split(" ").flatMap((B)=>B&&q(B)>$?s2(B,q,$):[B]),P="",D=0;for(let B of U){let _=q(B);if(!P)P=B,D=_;else if(D+z+_<=$)P+=" "+B,D+=z+_;else X.push(P),P=B,D=_}X.push(P)}return X}function J1(J,Q,$,Z){let q=J.slice(0,i2),X=f4(q,Q,$);if(q.length===J.length&&X.length<=Z)return X.join(` +`);let z=X.slice(0,Z),W=z.length-1;return z[W]=z[W].replace(/[\s.]+$/,"")+"…",z.join(` +`)}function a2(J,Q,$,Z){let q=$;while(qQ){if(U>X)return{text:z.slice(0,P).replace(/\s+$/,""),nextOffset:U,sourceLineEnded:!1};return{text:z,nextOffset:q,sourceLineEnded:!1}}if(z+=D,W+=B,q+=1,D===" ")U=q,P=z.length-1}return{text:z.replace(/\s+$/,""),nextOffset:q,sourceLineEnded:!1}}function r2(J,Q,$,Z,q,X){let z=Math.max(0,Math.floor(X)),W=[],U=Math.max(0,Math.min(Math.floor(Z),J.length)),P=Math.max(0,Math.floor(q)),D=P,B=new Map,_=(g)=>{let w=B.get(g);if(w===void 0)w=h().measureText(g,Q),B.set(g,w);return w};while(U0&&!Q.page.hasMore)return;let Z=Q.pageStarts[$];if(!Z)Z={offset:Q.page.nextOffset,sourceLine:Q.page.nextSourceLine},Q.pageStarts.push(Z);Y1({...Q,pageIndex:$,page:e4(Q.text,Z)})}function O0(J=VJ(),Q=!1){let $=r4.get(J);if(!Q&&$){q1($),D0(""),hJ((Z)=>Math.min(Z,Math.max(0,$.length-8)));return}try{let Z=u2(J,{withFileTypes:!0}).map((q)=>({name:q.name,kind:q.isDirectory()?"dir":"file",size:q.size}));r4.set(J,Z),q1(Z),D0(""),hJ((q)=>Math.min(q,Math.max(0,Z.length-8)))}catch(Z){q1([]),D0(Z instanceof Error?Z.message:String(Z))}}function YQ(){let J=[];for(let Q of n().messages??[])if(Q.role==="user")J.push({user:Q.text,assistant:"THINKING..."});else if(J.length===0)J.push({user:"TYPE A MESSAGE",assistant:Q.text||"THINKING..."});else J[J.length-1].assistant=Q.text||"THINKING...";return J.length?J:[{user:"TYPE A MESSAGE",assistant:"BOOTING PI AGENT..."}]}function Q6(){let J=YQ(),Q=Math.max(0,J.length-2),$=Math.min(Z1(),Q),Z=J.length-$;return J.slice(Math.max(0,Z-2),Z)}function OJ(J){return Y(c2,{get title(){return J.title},get accent(){return v(()=>n().agent==="FAULTED")()?"danger":n().agent==="THINKING"?"busy":"ready"}})}function I0(){return Y(M,{class:"h-[108] px-[10] py-4 flex-row gap-[10] bg-slate-950",get children(){return Y(jJ,{each:t4,children:(J)=>Y(M,{get["class"](){return $1()===J?"w-[165] h-[76] items-center justify-center bg-orange-600":"w-[165] h-[76] items-center justify-center bg-slate-900"},get children(){return Y(V,{class:"text-base text-white font-bold",get children(){return J.toUpperCase()}})}})})}})}function zQ(){let J=()=>n().schedule,Q=()=>{let $=J();return J1(String($?.name)+" "+String($?.next??"")+` + +`+String($?.prompt??""),o2,Q1,5)};return Y(M,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"ESP32 PI AGENT"}),Y(M,{class:"h-[686] px-6 pt-7 flex-col gap-[22]",get children(){return[Y(jJ,{get each(){return Q6()},children:($)=>Y(M,{class:"w-[584] h-[318] px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100",get children(){return[Y(M,{class:"h-[30] flex-row items-center gap-3",get children(){return[Y(M,{class:"w-[10] h-[10] rounded bg-orange-500"}),Y(V,{class:"text-lg text-orange-600 font-bold",children:"YOU"})]}}),Y(V,{class:"h-[84] pt-3 text-xl text-slate-900",get children(){return J1($.user,x4,Q1,3)}}),Y(M,{class:"h-[2] mx-1 my-4 bg-slate-100"}),Y(M,{class:"h-[30] flex-row items-center gap-3",get children(){return[Y(M,{class:"w-[10] h-[10] rounded bg-emerald-500"}),Y(V,{class:"text-lg text-emerald-700 font-bold",children:"PI"})]}}),Y(V,{class:"h-[84] pt-3 text-xl text-slate-900",get children(){return J1($.assistant,x4,Q1,3)}})]}})}),Y(iJ,{top:"absolute left-[628] top-[28] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[512] w-[68] h-[132] items-center justify-center bg-orange-100"})]}}),Y(M,{class:"h-[228] mx-6 px-6 py-5 flex-col rounded-xl shadow bg-white border-slate-100",get children(){return[Y(V,{class:"text-base text-slate-600 font-bold",children:"NEXT WAKE"}),Y(ZJ,{get when(){return J()?.name},get fallback(){return Y(V,{class:"pt-6 text-lg text-slate-500",children:`NO WAKE SCHEDULED + +ASK PI TO CREATE ONE WITH SCHEDULE.SET`})},get children(){return Y(V,{class:"pt-6 text-lg text-slate-900 font-bold",get children(){return Q()}})}})]}}),Y(M,{class:"h-[146] px-6 pt-7 flex-col bg-slate-50",get children(){return Y(M,{class:"h-[80]",get children(){return Y(d2,{label:"TYPE A MESSAGE"})}})}}),Y(I0,{})]}})}function GQ(){let J=()=>aJ().slice(sJ(),sJ()+8);return Y(M,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{get title(){return VJ()?"< WORKSPACE FILES":"WORKSPACE FILES"}}),Y(M,{class:"h-[1060] px-6 pt-5 flex-col",get children(){return[Y(M,{class:"h-[48] px-4 justify-center bg-slate-100",get children(){return Y(V,{class:"text-base text-slate-600",get children(){return"/workspace"+(VJ()?"/"+VJ():"")}})}}),Y(M,{class:"pt-[10] flex-col gap-[12]",get children(){return Y(jJ,{get each(){return J()},children:(Q)=>Y(M,{class:"w-[584] h-[92] px-[18] flex-row items-center gap-5 bg-white",get children(){return[Y(M,{get["class"](){return Q.kind==="dir"?"w-[48] h-[48] items-center justify-center bg-blue-100":"w-[48] h-[48] items-center justify-center bg-emerald-100"},get children(){return Y(V,{get["class"](){return Q.kind==="dir"?"text-lg text-blue-700 font-bold":"text-lg text-emerald-700 font-bold"},get children(){return Q.kind==="dir"?"D":"F"}})}}),Y(M,{class:"w-[474] flex-col gap-2",get children(){return[Y(V,{class:"text-lg text-slate-900 font-bold",get children(){return(Q.name+(Q.kind==="dir"?"/":"")).slice(0,52)}}),Y(V,{class:"text-base text-slate-500",get children(){return v(()=>Q.kind==="dir")()?"FOLDER":ZQ(Q.size)}})]}})]}})})}}),Y(ZJ,{get when(){return aJ().length===0},get children(){return Y(V,{class:"pt-20 pl-10 text-lg text-slate-500",get children(){return $Q()||"THIS DIRECTORY IS EMPTY"}})}}),Y(ZJ,{get when(){return aJ().length>8},get children(){return Y(iJ,{top:"absolute left-[628] top-[78] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100"})}})]}}),Y(I0,{})]}})}function WQ(){let J=()=>n().apps??[];return Y(M,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"APPS"}),Y(M,{class:"h-[1060] px-6 pt-7 flex-col gap-4",get children(){return[Y(V,{class:"text-base text-slate-500 font-bold",get children(){return String(J().length)+" INSTALLED APPS"}}),Y(ZJ,{get when(){return J().length>0},get fallback(){return Y(M,{class:"h-[214] px-7 items-center justify-center bg-slate-100",get children(){return Y(V,{class:"text-lg text-slate-500 font-bold",children:"NO OPTIONAL APPS INSTALLED"})}})},get children(){return Y(jJ,{get each(){return J()},children:(Q)=>Y(M,{class:"w-[672] h-[150] px-6 flex-row items-center justify-between bg-white",get children(){return[Y(M,{class:"flex-row items-center gap-5",get children(){return[Y(M,{class:"w-[68] h-[68] items-center justify-center bg-orange-100",get children(){return Y(V,{class:"text-xl text-orange-700 font-bold",get children(){return Q.title.slice(0,1).toUpperCase()}})}}),Y(M,{class:"w-[500] flex-col gap-2",get children(){return[Y(V,{class:"text-xl text-slate-900 font-bold",get children(){return Q.title}}),Y(V,{class:"text-lg text-slate-600",get children(){return Q.description}}),Y(ZJ,{get when(){return Q.scheduleEveryMinutes},get children(){return Y(V,{class:"text-base text-slate-500",get children(){return"UPDATES EVERY "+String(Q.scheduleEveryMinutes)+" MINUTES"}})}})]}})]}}),Y(V,{class:"text-2xl text-orange-600",children:"›"})]}})})}}),Y(M,{class:"mt-4 h-[112] px-6 justify-center bg-slate-100",get children(){return Y(V,{class:"text-base text-slate-600",children:`APP DATA STAYS ISOLATED. +PI AGENT CAN USE EACH APP'S TOOLS.`})}})]}}),Y(I0,{})]}})}function BQ(){let J=()=>n().settings??{},Q=()=>J().wifi??{},$=()=>(Q().networks??[]).slice(rJ(),rJ()+5),Z=()=>Q().ipAddress?"IP "+Q().ipAddress+" RSSI "+String(Q().rssiDbm??"--")+" DBM":Q().status||"SCAN AND SELECT A NETWORK";return Y(M,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"SETTINGS"}),Y(M,{class:"h-[1060] px-6 pt-5 flex-col",get children(){return[Y(M,{class:"relative h-[154] px-5 pt-5 flex-col bg-white",get children(){return[Y(V,{class:"text-lg text-slate-900 font-bold",children:"WI-FI"}),Y(V,{class:"pt-3 text-lg text-orange-600 font-bold",get children(){return Q().connectedSsid||"NOT CONNECTED"}}),Y(V,{class:"pt-3 text-base text-slate-500",get children(){return Z()}}),Y(M,{class:"absolute left-[456] top-[14] w-[196] h-[72] items-center justify-center bg-orange-600",get children(){return Y(V,{class:"text-lg text-white font-bold",get children(){return Q().scanning?"SCANNING":"SCAN"}})}})]}}),Y(V,{class:"h-[40] pt-3 text-base text-slate-500",children:"AVAILABLE NETWORKS"}),Y(M,{class:"h-[470] flex-col gap-2",get children(){return[Y(ZJ,{get when(){return $().length>0},get fallback(){return Y(M,{class:"h-[214] px-7 items-center justify-center bg-slate-100",get children(){return Y(V,{class:"text-lg text-slate-500 font-bold",children:`NO NETWORK LIST YET + +TAP SCAN TO FIND WI-FI`})}})},get children(){return Y(jJ,{get each(){return $()},children:(q)=>Y(M,{class:"w-[584] h-[84] px-5 flex-row items-center justify-between bg-white",get children(){return[Y(V,{class:"text-lg text-slate-900 font-bold",get children(){return q.ssid}}),Y(V,{class:"text-base text-slate-500",get children(){return String(q.rssiDbm)+" DBM "+(q.secured?"LOCK":"OPEN")}})]}})})}}),Y(ZJ,{get when(){return(Q().networks??[]).length>5},get children(){return Y(iJ,{top:"absolute left-[604] top-[0] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[604] top-[320] w-[68] h-[132] items-center justify-center bg-orange-100"})}})]}}),Y(M,{class:"h-[160] px-5 pt-5 flex-col bg-white",get children(){return[Y(V,{class:"text-base text-slate-500",children:"MODEL BACKEND"}),Y(V,{class:"pt-3 text-lg text-slate-900 font-bold",get children(){return n().model??"UNKNOWN"}}),Y(V,{class:"pt-3 text-base text-slate-600",get children(){return"FIRMWARE "+String(J().firmwareVersion??"0.1.0")+" · WORKSPACE FREE "+String(J().workspaceFree??"--")}})]}}),Y(M,{class:"h-[108] pt-7 flex-row gap-4",get children(){return[Y(M,{class:"w-[316] h-[80] items-center justify-center bg-slate-100",get children(){return Y(V,{class:"text-lg text-slate-900 font-bold",children:"FORGET WI-FI"})}}),Y(M,{class:"w-[340] h-[80] items-center justify-center bg-red-100",get children(){return Y(V,{class:"text-lg text-red-500 font-bold",children:"RESTART DEVICE"})}})]}})]}}),Y(I0,{})]}})}function HQ(){let J=()=>W1(),Q=()=>LJ()==="letters"?o4:n4,$=()=>J().type==="wifi"?"*".repeat(m().length):m();return Y(M,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{get title(){return J().type==="wifi"?"WIFI PASSWORD":"NEW MESSAGE"}}),Y(M,{class:"h-[1052] px-6 pt-5 flex-col",get children(){return[Y(M,{class:"h-[270] px-[22] pt-6 bg-white",get children(){return Y(V,{get["class"](){return m()?"text-lg text-slate-900":"text-lg text-slate-400"},get children(){return $()||(J().type==="wifi"?"ENTER NETWORK PASSWORD...":"TYPE YOUR MESSAGE...")}})}}),Y(M,{class:"h-[86] px-1 flex-row items-center justify-between",get children(){return[Y(V,{class:"text-base text-slate-500",get children(){return String(m().length)+" / "+(J().type==="wifi"?"63":"256")+" CHARACTERS"}}),Y(M,{get["class"](){return BJ()==="clear"?"w-[132] h-[58] items-center justify-center bg-slate-300":"w-[132] h-[58] items-center justify-center bg-slate-100"},get children(){return Y(V,{class:"text-base text-slate-900 font-bold",children:"CLEAR"})}})]}}),Y(jJ,{get each(){return Q()},children:(Z,q)=>Y(M,{class:"h-[140] flex-row gap-2",get children(){return[Y(jJ,{get each(){return Z.split("")},children:(X)=>Y(M,{get["class"](){return BJ()==="char:"+X?"grow h-[120] items-center justify-center bg-slate-300":"grow h-[120] items-center justify-center bg-white"},get children(){return Y(V,{class:"text-xl text-slate-900 font-bold",get children(){return v(()=>!!z1())()?X.toUpperCase():X}})}})}),Y(ZJ,{get when(){return q()===2},get children(){return Y(M,{get["class"](){return BJ()==="delete"?"w-[104] h-[120] items-center justify-center bg-slate-300":"w-[104] h-[120] items-center justify-center bg-slate-100"},get children(){return Y(V,{class:"text-base text-slate-900 font-bold",children:"DEL"})}})}})]}})}),Y(M,{class:"h-[176] flex-row gap-2",get children(){return[Y(M,{get["class"](){return BJ()==="mode"?"w-[92] h-[156] items-center justify-center bg-slate-300":"w-[92] h-[156] items-center justify-center bg-slate-100"},get children(){return Y(V,{class:"text-base text-slate-900 font-bold",get children(){return LJ()==="letters"?"123":"ABC"}})}}),Y(M,{get["class"](){return BJ()==="space"?"w-[300] h-[156] items-center justify-center bg-slate-300":"w-[300] h-[156] items-center justify-center bg-slate-100"},get children(){return Y(V,{class:"text-base text-slate-900 font-bold",children:"SPACE"})}}),Y(M,{get["class"](){return BJ()==="shift"?"w-[144] h-[156] items-center justify-center bg-slate-300":"w-[144] h-[156] items-center justify-center bg-slate-100"},get children(){return Y(V,{class:"text-base text-slate-900 font-bold",get children(){return LJ()==="letters"?"SHIFT":". ?"}})}}),Y(M,{get["class"](){return BJ()==="submit"?"w-[112] h-[156] items-center justify-center bg-emerald-700":"w-[112] h-[156] items-center justify-center bg-emerald-500"},get children(){return Y(V,{class:"text-base text-slate-950 font-bold",get children(){return J().type==="wifi"?"JOIN":"SEND"}})}})]}})]}}),Y(M,{class:"h-[108] px-6 py-3 flex-col bg-slate-50",get children(){return Y(M,{get["class"](){return BJ()==="close"?"h-[84] items-center justify-center bg-slate-300":"h-[84] items-center justify-center bg-slate-100"},get children(){return Y(V,{class:"text-base text-slate-900 font-bold",children:"CLOSE KEYBOARD"})}})}})]}})}function jQ(){let J=()=>X1();return Y(M,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"< FILE VIEWER"}),Y(M,{class:"h-[1168] px-6 pt-5 flex-col",get children(){return[Y(M,{class:"h-[82] px-4 justify-center bg-white",get children(){return Y(V,{class:"text-lg text-slate-900 font-bold",get children(){return J()?.path??"NO FILE OPEN"}})}}),Y(M,{class:"w-[584] h-[900] px-5 pt-5 bg-slate-950",get children(){return Y(V,{class:"text-base text-slate-200",get children(){return J()?.page.text??""}})}}),Y(V,{class:"pt-3 text-base text-slate-500",get children(){return v(()=>!!J())()?"PAGE "+String(J().pageIndex+1)+" · SOURCE LINES "+String(J().page.startSourceLine+1)+"-"+String(J().page.lastSourceLine+1):"NO FILE OPEN"}}),Y(iJ,{top:"absolute left-[628] top-[58] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100"})]}})]}})}function KQ(){let J=()=>p4();return Y(M,{class:"flex-col w-full h-full bg-slate-50",get children(){return[Y(OJ,{title:"< MESSAGE READER"}),Y(M,{class:"h-[1168] px-6 pt-5 flex-col",get children(){return[Y(M,{class:"h-[82] px-4 justify-center bg-white",get children(){return Y(V,{class:"text-lg text-orange-600 font-bold",get children(){return J()?.author??"PI"}})}}),Y(M,{class:"w-[584] h-[900] px-5 pt-5 bg-white",get children(){return Y(V,{class:"text-xl text-slate-900",get children(){return(J()?.lines??[]).slice(V0(),V0()+m4).join(` +`)}})}}),Y(iJ,{top:"absolute left-[628] top-[58] w-[68] h-[132] items-center justify-center bg-orange-100",bottom:"absolute left-[628] top-[828] w-[68] h-[132] items-center justify-center bg-orange-100"})]}})]}})}function FQ(){return Y(M,{class:"flex-col w-full h-full bg-slate-50",get children(){return v(()=>f()==="chat")()?Y(zQ,{}):v(()=>f()==="files")()?Y(GQ,{}):v(()=>f()==="apps")()?Y(WQ,{}):v(()=>f()==="settings")()?Y(BQ,{}):v(()=>f()==="keyboard")()?Y(HQ,{}):v(()=>f()==="viewer")()?Y(jQ,{}):Y(KQ,{})}})}function MQ(J){B6(()=>{if(QQ(J),t(J),J==="files")O0()})}function $6(J,Q){let $=LJ()==="letters"?o4:n4,Z=[488,628,768];for(let q=0;q<3;q++){if(Q=Z[q]+120)continue;let X=$[q],W=(q===2?560:672)/X.length,U=Math.floor((J-24)/W);if(U>=0&&U=1164)return"close";if(J>=548&&Q>=402&&Q<=482)return"clear";let $=$6(J,Q);if($)return"char:"+$;if(J>=592&&Q>=768&&Q<=888)return"delete";if(Q<908||Q>1064)return null;if(J<=116)return"mode";if(J<=424)return"space";if(J<=576)return"shift";return"submit"}function RQ(J,Q){if(Q>=1164)return e(""),t($1()),"";if(J>=548&&Q>=402&&Q<=482)return e(""),"";let $=$6(J,Q);if($){let Z=LJ()==="letters"&&z1()?$.toUpperCase():$,q=W1().type==="wifi"?63:256;if(m().length=592&&Q>=768&&Q<=888)return e(m().slice(0,-1)),"";if(Q>=908&&Q<=1064){if(J<=116)d4(LJ()==="letters"?"numbers":"letters"),G1(!1);else if(J<=424){if(m())e(m()+" ")}else if(J<=576)if(LJ()==="letters")G1(!z1());else e(m()+(J<=500?".":"?"));else if(m().trim()){let Z=m().trim(),q=W1();if(e(""),d4("letters"),G1(!1),t($1()),q.type==="wifi")return JSON.stringify({type:"settings",command:"connect",ssid:q.ssid,password:Z});return JSON.stringify({type:"submitPrompt",prompt:Z})}}return""}h2(()=>Y(FQ,{})),queueMicrotask(()=>O0("",!0)),globalThis.PocketPiApp={tick(){return""},update(J){return JQ(JSON.parse(J)),""},pointerDown(J,Q){return s4(f()==="keyboard"?UQ(J,Q):null),""},pointerUp(){return s4(null),""},tap(J,Q){if(f()==="keyboard")return RQ(J,Q);if(f()==="viewer"){let $=X1();if(J<104&&Q<112)Y1(null),t("files");else if($&&J>=620&&Q>=170&&Q<=340)J6(-1);else if($&&J>=620&&Q>=920&&Q<=1100)J6(1);return""}if(f()==="reader"){if(J<104&&Q<112)c4(null),L0(0),t("chat");else if(J>=620&&Q>=170&&Q<=340)L0(Math.max(0,V0()-18));else if(J>=620&&Q>=920&&Q<=1100){let $=p4()?.lines.length??0;L0(Math.min(Math.max(0,$-m4),V0()+18))}return""}if(Q>=1172)return MQ(t4[Math.min(3,Math.floor(J/180))]),"";if(f()==="chat"){if(J>=620&&Q>=140&&Q<=272)l4(Z1()+2);else if(J>=620&&Q>=624&&Q<=756)l4(Math.max(0,Z1()-2));else if(J<610&&Q>=140&&Q<798){let $=Math.floor((Q-140)/340),Z=Q6()[$];if(Z){let q=Q>=140+$*340+160,X=q?Z.assistant:Z.user;c4({author:q?"PI":"YOU",lines:f4(X,n2,v4)}),L0(0),t("reader")}}else if(Q>=1070&&Q<=1150)i4({type:"prompt"}),e(""),t("keyboard");return""}if(f()==="files"){if(J<104&&Q<112&&VJ()){let $=VJ().split("/");$.pop();let Z=$.join("/");u4(Z),hJ(0),O0(Z)}else if(J>=620&&Q>=170&&Q<=340)hJ(Math.max(0,sJ()-4));else if(J>=620&&Q>=920&&Q<=1100)hJ(Math.min(Math.max(0,aJ().length-8),sJ()+4));else if(J<610&&Q>=190){let $=Math.floor((Q-190)/104),Z=aJ()[sJ()+$];if(Z){let q=qQ(VJ(),Z.name);if(Z.kind==="dir")u4(q),hJ(0),O0(q);else try{XQ("/workspace/"+q,l2(q,"utf8"))}catch(X){D0(X instanceof Error?X.message:String(X))}}}return""}if(f()==="apps"){let $=(n().apps??[])[Math.floor((Q-162)/166)];if(Q>=162&&$)return JSON.stringify({type:"navigate",app:$.id});return""}if(f()==="settings"){let $=n().settings?.wifi?.networks??[];if(J>=480&&Q>=126&&Q<=218)return JSON.stringify({type:"settings",command:"scan"});if(J>=620&&Q>=330&&Q<=462)a4(Math.max(0,rJ()-4));else if(J>=620&&Q>=650&&Q<=782)a4(Math.min(Math.max(0,$.length-5),rJ()+4));else if(J<610&&Q>=330&&Q<790){let Z=Math.floor((Q-330)/92),q=$[rJ()+Z];if(q){if(!q.secured)return JSON.stringify({type:"settings",command:"connect",ssid:q.ssid,password:""});i4({type:"wifi",ssid:q.ssid}),e(""),t("keyboard")}}else if(J<=340&&Q>=1010&&Q<=1090)return JSON.stringify({type:"settings",command:"forget"});else if(J>=356&&Q>=1010&&Q<=1090)return JSON.stringify({type:"settings",command:"restart"})}return""}}})(); diff --git a/apps/pi-agent/dist/app.pak b/apps/pi-agent/dist/app.pak new file mode 100644 index 0000000..504153c Binary files /dev/null and b/apps/pi-agent/dist/app.pak differ diff --git a/apps/pi-agent/pocket.json b/apps/pi-agent/pocket.json new file mode 100644 index 0000000..0e24172 --- /dev/null +++ b/apps/pi-agent/pocket.json @@ -0,0 +1,19 @@ +{ + "$schema": "https://pocketjs.dev/schema/pocket-2.json", + "pocket": 2, + "id": "dev.pocket-stack.pi-agent", + "name": "pi-agent", + "title": "Pi Agent", + "version": "1.0.0", + "engine": { + "capabilities": { + "requires": ["data.fs", "data.sqlite"] + } + }, + "app": { + "entry": "app.tsx", + "output": "app", + "framework": "solid", + "viewport": { "logical": [720, 1280], "presentation": "fit" } + } +} diff --git a/apps/robinhood/TOOLS.md b/apps/robinhood/TOOLS.md new file mode 100644 index 0000000..3c02189 --- /dev/null +++ b/apps/robinhood/TOOLS.md @@ -0,0 +1,112 @@ +# Robinhood Tool contract + +Pocket Pi snapshots the upstream Robinhood Trading MCP catalog, but does not put +all upstream schemas in every model request. The checked-in snapshot currently +contains 54 upstream Tools. The initial Agent catalog contains three small Pocket +Pi Tools: + +- `robinhood.search_tools` returns matching upstream descriptions and exact JSON + Schemas from the local snapshot. It performs no provider request. +- `robinhood.call` validates one upstream call against that schema and routes it + through the native static allowlist. +- `robinhood.refresh_portfolio` refreshes the bounded fixed-View projection. + +This is Pocket Pi's cross-model fallback for deferred Tool loading. It follows +the same load-on-demand design as OpenAI client-executed Tool Search, but is not +the Responses API's native `tool_search` wire protocol. Native Tool Search is +currently limited to supported OpenAI models, while Pocket Pi also supports +other model backends. If a backend gains native deferred loading, it can expose +the same snapshot as dynamically loaded normal function definitions without +changing the Robinhood Data Action. + +References: + +- Robinhood Agentic Trading: +- Robinhood Agentic Trading overview: +- OpenAI Tool Search: +- GitHub Copilot CLI Tool Search: + +## Catalog and schema source + +`tool-catalog.json` is the Robinhood App-owned runtime catalog captured from an +authenticated MCP `initialize` and `tools/list` exchange with +`https://agent.robinhood.com/mcp/trading`. It stores the upstream name, +input schema, and one combined description containing upstream usage guidance +plus Pocket Pi safety and persistence behavior. Credentials are never written to the catalog, App +bundle, SQLite, or Agent context. Updating this catalog is an explicit App +maintenance change and must be reviewed together with the native allowlist and +contract tests. + +This file and the catalog have different roles: + +- `tool-catalog.json` is executable source data. The Data Action build inlines it + into `dist/data-action.js`, where `searchTools()` and + `validatedProviderCall()` use it at runtime. Rust contract tests also read the + source snapshot directly to keep all 54 names aligned with + `agent-app.json.providerOperations`. +- `TOOLS.md` is human-facing maintenance documentation. It is not imported by + the build or read by the device runtime. + +This catalog is not a global AgentOS requirement. Robinhood uses deferred +lookup because exposing 54 complete schemas in every model request would be +wasteful. Small Apps such as Exa declare their few Tools directly and need no +private catalog. Any future large App owns its own catalog under that App. + +The 54 Tools are split into eight searchable domains. Each domain has at most +nine Tools: `account_portfolio`, `equity_trading`, `equity_market_data`, +`option_trading`, `option_market_data`, `watchlists`, `scanners`, and `indexes`. +Exact-name lookup is preferred when the Agent already knows the upstream name. + +The current upstream schemas use `type`, `properties`, `required`, +`additionalProperties`, `items`, `minimum`, and `maximum`. Pocket Pi validates +all of those keywords before crossing the native service boundary. The native +allowlist is built from `agent-app.json.providerOperations`, so a Tool must be +present in both the snapshot and the installed App descriptor. + +## Minimal persistence policy + +SQLite is a fixed-View projection, not a generic Tool cache or audit log. A +provider result commits only when the current Robinhood View consumes it. + +| Upstream operation | SQLite effect | Reason | +| --- | --- | --- | +| `get_accounts` | Replace `accounts` | Account selector and Agentic-account badge | +| `get_portfolio` | Upsert `portfolio_current` and `total_value` | Dashboard values and chart | +| `get_equity_positions` | Replace that account's `positions` | Positions View | +| `get_equity_orders` | Replace that account's `activities` | Activity View | +| `get_realized_pnl` | Upsert day or week P&L in `portfolio_current` | Dashboard P&L | +| `place_equity_order`, `cancel_equity_order` | Upsert only the returned order state in `activities` | The Tool result directly changes Activity; no second provider call is needed | +| Other 47 upstream Tools | None | Current fixed View does not consume the result | + +`refresh_portfolio` is the only aggregate refresh. It obtains accounts first, +batches the required per-account portfolio/position/order/P&L calls, and writes +their bounded projections plus one `refresh_runs` record in a transaction. One +successful transaction emits one App revision; the foreground View then +re-queries its bounded projection at the frame boundary. Direct-return Tools do +not call `app.commit()` and therefore cause no SQLite write or View invalidation. + +Equity place/cancel deliberately does not start a nested MCP refresh after the +provider action. It uses the returned order payload to update `activities` in +one short transaction. This keeps the App Data Action within its bounded stack +and avoids extra network, CPU, SQLite, and revision work. Portfolio and +positions converge on the next normal scheduled or explicit refresh. + +No `tool_runs`, raw-response, quote cache, fundamentals cache, options cache, +watchlist cache, or scanner cache is created while the fixed View does not need +it. + +## Agent execution and safety + +An Agent first searches for the operation, reads the returned full description +and schema, then calls `robinhood.call` with the exact upstream name and a +schema-valid `arguments` object. The App completion path returns the actual +provider result to the pending Agent Tool call; a queued receipt is never +mistaken for completion. The provider JSON is serialized once as ToolResult +`text`; it is not duplicated into `details`, so large market-data and options +responses do not retain a second copy in the QuickJS/Agent bridge. + +Review Tools are explicitly described as non-submitting. Place, cancel, and +exercise Tools describe their real account effects, functional account limits, +and parameter contracts without embedding an interaction or authorization +policy. A repeated real-money order must reuse the same `ref_id`; an ambiguous +transport result must not be retried with a new ID. diff --git a/apps/robinhood/agent-app.json b/apps/robinhood/agent-app.json new file mode 100644 index 0000000..1070bf7 --- /dev/null +++ b/apps/robinhood/agent-app.json @@ -0,0 +1,157 @@ +{ + "id": "robinhood", + "description": "Portfolio, positions, and trading", + "version": "1.1.0", + "dataVersion": 5, + "nativeServices": { + "mcp": [ + { + "connection": "robinhood", + "url": "https://agent.robinhood.com/mcp/trading", + "credential": { + "id": "robinhood.oauth-access-token", + "header": "authorization", + "prefix": "Bearer " + } + } + ] + }, + "providerOperations": [ + "add_option_to_watchlist", + "add_to_watchlist", + "cancel_equity_order", + "cancel_option_exercise", + "cancel_option_order", + "create_scan", + "create_watchlist", + "exercise_option", + "follow_watchlist", + "get_accounts", + "get_earnings_calendar", + "get_earnings_results", + "get_equity_fundamentals", + "get_equity_historicals", + "get_equity_orders", + "get_equity_positions", + "get_equity_price_book", + "get_equity_quotes", + "get_equity_tax_lots", + "get_equity_technical_indicators", + "get_equity_tradability", + "get_financials", + "get_index_historicals", + "get_index_quotes", + "get_indexes", + "get_limited_margin_upgrade_info", + "get_option_chains", + "get_option_historicals", + "get_option_instruments", + "get_option_level_upgrade_info", + "get_option_orders", + "get_option_positions", + "get_option_quotes", + "get_option_watchlist", + "get_pnl_trade_history", + "get_popular_watchlists", + "get_portfolio", + "get_realized_pnl", + "get_scanner_filter_specs", + "get_scans", + "get_watchlist_items", + "get_watchlists", + "place_equity_order", + "place_option_order", + "remove_from_watchlist", + "remove_option_from_watchlist", + "review_equity_order", + "review_option_order", + "run_scan", + "search", + "unfollow_watchlist", + "update_scan_config", + "update_scan_filters", + "update_watchlist" + ], + "tools": [ + { + "name": "robinhood.search_tools", + "description": "Search Pocket Pi's checked-in Robinhood Trading MCP catalog. Use this before robinhood.call whenever the exact upstream Tool and arguments are not already present in the conversation. Returns the checked-in description, exact upstream JSON Schema, Pocket Pi persistence behavior, and whether the Tool is a real account or real-money action. This performs no Robinhood request and writes no SQLite data.", + "parameters": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Natural-language capability or Tool-name fragment to find." + }, + "names": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Optional exact upstream Tool names to retrieve." + }, + "limit": { + "type": "integer", + "minimum": 1, + "maximum": 8, + "description": "Maximum matches. Defaults to 5." + }, + "namespace": { + "type": [ + "null", + "string" + ], + "description": "Optional domain filter: account_portfolio, equity_trading, equity_market_data, option_trading, option_market_data, watchlists, scanners, or indexes." + } + }, + "additionalProperties": false + } + }, + { + "name": "robinhood.call", + "description": "Invoke one upstream Robinhood Trading MCP Tool by its exact unnamespaced name. First use robinhood.search_tools and follow the returned JSON Schema and safety instructions. Pocket Pi validates arguments locally, native code enforces the checked-in allowlist, and the completed provider result is returned to the Agent. Never retry a real-money action with a new ref_id after an ambiguous transport result.", + "parameters": { + "type": "object", + "properties": { + "name": { + "type": "string", + "description": "Exact upstream Tool name returned by robinhood.search_tools." + }, + "arguments": { + "type": "object", + "description": "Arguments that satisfy that Tool's returned JSON Schema.", + "additionalProperties": true + } + }, + "required": [ + "name", + "arguments" + ], + "additionalProperties": false + } + }, + { + "name": "robinhood.refresh_portfolio", + "description": "Refresh accounts, portfolio, equity positions, recent equity orders, and day/week realized P&L into robinhood.sqlite. This is a local Pocket Pi aggregate and does not place, modify, or cancel orders.", + "parameters": { + "type": "object", + "properties": {}, + "additionalProperties": false + } + } + ], + "tasks": [ + "refreshPortfolio" + ], + "schedules": [ + { + "id": "portfolio-refresh", + "everyMinutes": 5, + "task": "refreshPortfolio", + "args": {} + } + ] +} diff --git a/apps/robinhood/app.tsx b/apps/robinhood/app.tsx new file mode 100644 index 0000000..7e0a5ce --- /dev/null +++ b/apps/robinhood/app.tsx @@ -0,0 +1,526 @@ +import { batch, createMemo, createSignal, For, Show } from "solid-js"; +import { Text, View } from "@pocketjs/framework/components"; +import { mount } from "@pocketjs/framework"; +import { Database } from "@pocketjs/framework/db"; +import { ActionButton, EmptyState, MetricCard, PocketHeader, ScrollButtons, SectionHeading, statusBadge, StatusBar } from "../_shared/ui"; + +const DB_SCHEMA_VERSION = 5; +const db = new Database("robinhood"); + +type Screen = "dashboard" | "accounts" | "activity" | "positions"; +type Span = "day" | "week"; +type Account = { number: string; label: string; suffix: string; status: string }; +type Position = { symbol: string; quantity: string; averagePrice: string; marketValue: string }; +type Activity = { title: string; timestamp: string; detail: string; state: string; amount: string; side: string }; +type Dashboard = { + account: Account; + totalValue: string | null; + cash: string | null; + buyingPower: string | null; + pnlDay: string | null; + pnlWeek: string | null; + positions: Position[]; + activity: Activity[]; + positionsAvailable: boolean; + activityAvailable: boolean; + observedAt: number | null; +}; +type ChartPoint = { x: number; y: number }; +type ChartSegment = { x: number; y: number; width: number; angle: number }; +type ChartProjection = { points: ChartPoint[]; segments: ChartSegment[]; labels: string[]; trend: { change: string; percent: string; positive: boolean } }; +type Cached = { loadedRevision: number; value: T }; +type AccountRow = { account_number: string; label: string; suffix: string; status: string }; +type PortfolioRow = { account_number: string; cash: string | null; buying_power: string | null; day_pnl: string | null; week_pnl: string | null; observed_at: number }; +type PositionRow = { account_number: string; symbol: string; quantity: string | null; average_price: string | null; market_value: string | null }; +type ActivityRow = { account_number: string; activity_id: string; occurred_at: string | null; symbol: string | null; side: string | null; quantity: string | null; price: string | null; amount: string | null; state: string | null; activity_type: string | null }; + +const EMPTY_ACCOUNT: Account = { number: "", label: "ACCOUNT", suffix: "", status: "WAITING FOR ROBINHOOD" }; +const EMPTY_ACTIVITY: Activity = { title: "NO RECENT ACTIVITY", timestamp: "", detail: "", state: "", amount: "", side: "" }; +const UNAVAILABLE_ACTIVITY: Activity = { ...EMPTY_ACTIVITY, title: "ACTIVITY UNAVAILABLE" }; +const BLANK_ACTIVITY: Activity = { ...EMPTY_ACTIVITY, title: "" }; +const EMPTY_POSITION: Position = { symbol: "NO OPEN POSITIONS", quantity: "", averagePrice: "", marketValue: "" }; +const UNAVAILABLE_POSITION: Position = { ...EMPTY_POSITION, symbol: "POSITIONS UNAVAILABLE" }; +const BLANK_POSITION: Position = { ...EMPTY_POSITION, symbol: "" }; +const EMPTY: Dashboard = { + account: EMPTY_ACCOUNT, totalValue: null, cash: null, buyingPower: null, + pnlDay: null, pnlWeek: null, positions: [], activity: [], + positionsAvailable: true, activityAvailable: true, observedAt: null, +}; + +const [screen, setScreen] = createSignal("dashboard"); +const [span, setSpan] = createSignal("day"); +const [accounts, setAccounts] = createSignal([]); +const [selectedAccount, setSelectedAccount] = createSignal(""); +const [dashboard, setDashboard] = createSignal(EMPTY); +const [chartPoints, setChartPoints] = createSignal([]); +const [chartSegments, setChartSegments] = createSignal([]); +const [chartLabels, setChartLabels] = createSignal([]); +const [chartTrend, setChartTrend] = createSignal({ change: "$—", percent: "—", positive: true }); +const [accountScroll, setAccountScroll] = createSignal(0); +const [activityScroll, setActivityScroll] = createSignal(0); +const [positionScroll, setPositionScroll] = createSignal(0); +const [status, setStatus] = createSignal("WAITING FOR ROBINHOOD"); +const [refreshing, setRefreshing] = createSignal(false); +let currentRevision = 0; +let accountsLoadedRevision = -1; +let dashboardsLoadedRevision = -1; +let refreshLoadedRevision = -1; +const dashboardCache = new Map>(); +const chartCache = new Map>(); +function now(): number { return Math.floor(Date.now() / 1000); } +function parse(value: string | null | undefined): any { try { return value ? JSON.parse(value) : null; } catch { return null; } } + +function loadAccounts(): Account[] { + const rows = db.query( + "SELECT account_number,label,suffix,status FROM accounts ORDER BY label,account_number LIMIT 16", + ).all() as unknown as AccountRow[]; + return rows.map((row) => ({ number: row.account_number, label: row.label, suffix: row.suffix, status: row.status })); +} + +function loadDashboardCache(loadedRevision: number, accountRows: Account[]): void { + if (dashboardsLoadedRevision === loadedRevision) return; + const portfolios = db.query( + "SELECT account_number,cash,buying_power,day_pnl,week_pnl,observed_at FROM portfolio_current LIMIT 16", + ).all() as unknown as PortfolioRow[]; + const totals = db.query(` + SELECT value.account_number,value.value,value.observed_at + FROM total_value value + JOIN ( + SELECT account_number,MAX(observed_at) AS observed_at + FROM total_value GROUP BY account_number + ) latest ON latest.account_number=value.account_number AND latest.observed_at=value.observed_at + LIMIT 16 + `).all() as unknown as Array<{ account_number: string; value: string; observed_at: number }>; + const positions = db.query( + "SELECT account_number,symbol,quantity,average_price,market_value FROM positions ORDER BY account_number,CAST(market_value AS REAL) DESC,symbol LIMIT 1024", + ).all() as unknown as PositionRow[]; + const activities = db.query( + "SELECT account_number,activity_id,occurred_at,symbol,side,quantity,price,amount,state,activity_type FROM activities ORDER BY account_number,occurred_at DESC,observed_at DESC LIMIT 1024", + ).all() as unknown as ActivityRow[]; + const portfolioByAccount = new Map(portfolios.map((row) => [row.account_number, row])); + const totalByAccount = new Map(totals.map((row) => [row.account_number, row])); + const positionsByAccount = new Map(); + const activitiesByAccount = new Map(); + for (const row of positions) { + const rows = positionsByAccount.get(row.account_number) || []; + if (rows.length < 64) rows.push(row); + positionsByAccount.set(row.account_number, rows); + } + for (const row of activities) { + const rows = activitiesByAccount.get(row.account_number) || []; + if (rows.length < 64) rows.push(row); + activitiesByAccount.set(row.account_number, rows); + } + dashboardCache.clear(); + for (const account of accountRows) { + const portfolio = portfolioByAccount.get(account.number); + const total = totalByAccount.get(account.number); + const accountPositions = positionsByAccount.get(account.number) || []; + const accountActivities = activitiesByAccount.get(account.number) || []; + dashboardCache.set(account.number, { loadedRevision, value: { + account, + totalValue: total?.value ?? null, + cash: portfolio?.cash ?? null, + buyingPower: portfolio?.buying_power ?? null, + pnlDay: portfolio?.day_pnl ?? null, + pnlWeek: portfolio?.week_pnl ?? null, + positions: accountPositions.map((row) => ({ symbol: row.symbol, quantity: row.quantity || "—", averagePrice: row.average_price || "—", marketValue: row.market_value || "" })), + activity: accountActivities.map((row) => ({ + title: (row.side ? row.side + " " : "") + (row.symbol || "ORDER"), + timestamp: formatTime(row.occurred_at), + detail: row.quantity ? row.quantity + " SH · " + (row.activity_type || "ORDER") : row.activity_type || "ORDER", + state: row.state || "RECENT", + amount: row.amount || row.price || row.quantity || "", + side: row.side || "", + })), + positionsAvailable: portfolio !== undefined, + activityAvailable: portfolio !== undefined, + observedAt: Math.max(portfolio?.observed_at ?? 0, total?.observed_at ?? 0) || null, + }}); + } + dashboardsLoadedRevision = loadedRevision; +} + +function number(value: string | null): number | null { + if (value === null) return null; + const parsed = Number(value.replace(/[$,%]/g, "")); + return Number.isFinite(parsed) ? parsed : null; +} + +function formatTime(value: string | null): string { + if (!value) return "RECENT"; + const date = new Date(value); + if (!Number.isFinite(date.getTime())) return value.slice(0, 16).toUpperCase(); + return String(date.getMonth() + 1).padStart(2, "0") + "/" + String(date.getDate()).padStart(2, "0") + " " + String(date.getHours()).padStart(2, "0") + ":" + String(date.getMinutes()).padStart(2, "0"); +} + + +function relativeTime(seconds: number): string { + const age = Math.max(0, now() - seconds); + if (age < 60) return "JUST NOW"; + if (age < 3600) return Math.floor(age / 60) + " MIN AGO"; + if (age < 86400) return Math.floor(age / 3600) + " HR AGO"; + return Math.floor(age / 86400) + " DAY AGO"; +} + +function money(value: string | null | undefined): string { + if (!value) return "$—"; + const parsed = number(value); + if (parsed === null) return value; + const absolute = Math.abs(parsed); + const parts = absolute.toFixed(2).split("."); + const formatted = "$" + parts[0].replace(/\B(?=(\d{3})+(?!\d))/g, ",") + "." + parts[1]; + return parsed < 0 ? "-" + formatted : formatted; +} + +function loadChart(accountNumber: string, loadedRevision: number) { + const cacheKey = accountNumber + ":" + span(); + const cached = chartCache.get(cacheKey); + if (cached?.loadedRevision === loadedRevision) { + setChartPoints(cached.value.points); + setChartSegments(cached.value.segments); + setChartLabels(cached.value.labels); + setChartTrend(cached.value.trend); + return; + } + const windowSeconds = span() === "day" ? 86400 : 7 * 86400; + const end = now(); + const cutoff = end - windowSeconds; + const rows = db.query(` + WITH RECURSIVE buckets(bucket_index, bucket_time) AS ( + SELECT 0, ?1 + UNION ALL + SELECT bucket_index + 1, ?1 + CAST((?2 * (bucket_index + 1)) / 19 AS INTEGER) + FROM buckets WHERE bucket_index < 19 + ) + SELECT bucket_index, bucket_time, + (SELECT value FROM total_value + WHERE account_number = ?3 AND observed_at >= ?1 AND observed_at <= bucket_time + ORDER BY observed_at DESC LIMIT 1) AS total_value + FROM buckets ORDER BY bucket_index + `).all(cutoff, windowSeconds, accountNumber) as unknown as Array<{ bucket_index: number; bucket_time: number; total_value: string | null }>; + const buckets = rows.map((row) => ({ time: row.bucket_time, value: number(row.total_value) })); + const bucketTimes = buckets.map((bucket) => bucket.time); + const values = buckets.map((bucket) => bucket.value).filter((value): value is number => value !== null); + const labels = [bucketTimes[0], bucketTimes[9], bucketTimes[19]].map((time) => { + const date = new Date(time * 1000); + return span() === "day" + ? String(date.getHours()).padStart(2, "0") + ":" + String(date.getMinutes()).padStart(2, "0") + : String(date.getMonth() + 1) + "/" + String(date.getDate()); + }); + if (values.length === 0) { + const pnl = span() === "day" ? dashboard().pnlDay : dashboard().pnlWeek; + const value = number(pnl); + const projection = { points: [], segments: [], labels, trend: { change: money(pnl), percent: "—", positive: value === null || value >= 0 } }; + chartCache.set(cacheKey, { loadedRevision, value: projection }); + setChartPoints(projection.points); + setChartSegments(projection.segments); + setChartLabels(projection.labels); + setChartTrend(projection.trend); + return; + } + const low = Math.min(...values); + const high = Math.max(...values); + const range = Math.max(0.01, high - low); + const points = buckets.flatMap((bucket, index) => bucket.value === null ? [] : [{ + x: index * 622 / 19, + y: values.length === 1 ? 80 : 10 + (high - bucket.value) * 140 / range, + }]); + const segments = points.slice(1).map((point, index) => { + const previous = points[index]; + const dx = point.x - previous.x; + const dy = point.y - previous.y; + return { x: previous.x, y: previous.y, width: Math.sqrt(dx * dx + dy * dy), angle: Math.atan2(dy, dx) * 180 / Math.PI }; + }); + const delta = values[values.length - 1] - values[0]; + const trend = { change: money(String(delta)), percent: values[0] === 0 ? "—" : (delta * 100 / values[0]).toFixed(2) + "%", positive: delta >= 0 }; + chartCache.set(cacheKey, { loadedRevision, value: { points, segments, labels, trend } }); + setChartPoints(points); + setChartSegments(segments); + setChartLabels(labels); + setChartTrend(trend); +} + +function loadRefreshProjection(loadedRevision: number): void { + if (refreshLoadedRevision === loadedRevision) return; + const latestRun = db.query("SELECT status,error,completed_at FROM refresh_runs ORDER BY id DESC LIMIT 1").get() as unknown as { status?: string; error?: string | null; completed_at?: number | null } | null; + setRefreshing(false); + if (latestRun?.status === "failed") setStatus("REFRESH FAILED · " + String(latestRun.error || "UNKNOWN ERROR").slice(0, 52)); + else if (latestRun?.status === "partial") setStatus("LIVE WITH PARTIAL DATA"); + else if (dashboard().observedAt) setStatus("LIVE · " + relativeTime(dashboard().observedAt as number)); + else setStatus("WAITING FOR ROBINHOOD"); + refreshLoadedRevision = loadedRevision; +} + +function loadAccountProjection(accountNumber: string, loadedRevision: number): void { + if (!accountNumber) { + setDashboard(EMPTY); + setChartPoints([]); + setChartSegments([]); + if (!refreshing()) setStatus("WAITING FOR ROBINHOOD"); + return; + } + const cached = dashboardCache.get(accountNumber); + if (cached?.loadedRevision === loadedRevision) { + setDashboard(cached.value); + loadChart(accountNumber, loadedRevision); + if (cached.value.observedAt && !refreshing()) setStatus("LIVE · " + relativeTime(cached.value.observedAt)); + return; + } + setDashboard(EMPTY); + setChartPoints([]); + setChartSegments([]); +} + +function loadView(loadedRevision = currentRevision) { + // This is the only whole-App projection refresh. The host calls it once on + // initial activation and once for any number of commits coalesced at the + // foreground frame boundary. Normal frames never execute it. + try { + const schema = db.query("PRAGMA user_version").get() as unknown as { user_version?: number } | null; + if (Number(schema?.user_version ?? 0) !== DB_SCHEMA_VERSION) { + setAccounts([]); + setSelectedAccount(""); + setDashboard(EMPTY); + setChartPoints([]); + setChartSegments([]); + setStatus("WAITING FOR ROBINHOOD"); + return; + } + batch(() => { + currentRevision = Math.max(currentRevision, loadedRevision); + let nextAccounts = accounts(); + if (accountsLoadedRevision !== currentRevision) { + nextAccounts = loadAccounts(); + setAccounts(nextAccounts); + accountsLoadedRevision = currentRevision; + } + loadDashboardCache(currentRevision, nextAccounts); + let accountNumber = selectedAccount(); + if (!nextAccounts.some((item) => item.number === accountNumber)) { + accountNumber = nextAccounts[0]?.number || ""; + setSelectedAccount(accountNumber); + } + loadAccountProjection(accountNumber, currentRevision); + loadRefreshProjection(currentRevision); + }); + } catch { + batch(() => { + setAccounts([]); + setSelectedAccount(""); + setDashboard(EMPTY); + setChartPoints([]); + setChartSegments([]); + setStatus("WAITING FOR ROBINHOOD"); + }); + } +} + +function tick(): string { + return ""; +} + +function trend(): { change: string; percent: string; positive: boolean } { + return chartTrend(); +} + +function Header(props: { title: string; metaBottom?: string }) { + return ( + + ); +} + +function Metric(props: { label: string; value: string | null }) { + return ; +} + +function SectionTitle(props: { title: string; detail?: string }) { + return ; +} + +function activityPreview(index: number): Activity { + if (index === 0 && !dashboard().activityAvailable) return UNAVAILABLE_ACTIVITY; + return dashboard().activity[index] ?? (index === 0 ? EMPTY_ACTIVITY : BLANK_ACTIVITY); +} + +function CompactActivity(props: { index: number }) { + const item = () => activityPreview(props.index); + return {item().title}{item().timestamp ? item().timestamp + " · " + item().detail : ""}{item().amount ? money(item().amount) : ""}; +} + +function positionPreview(index: number): Position { + if (index === 0 && !dashboard().positionsAvailable) return UNAVAILABLE_POSITION; + return dashboard().positions[index] ?? (index === 0 ? EMPTY_POSITION : BLANK_POSITION); +} + +function CompactPosition(props: { index: number }) { + const item = () => positionPreview(props.index); + return {item().symbol}{item().quantity ? item().quantity + " SH" : ""}{item().averagePrice ? "AVG " + money(item().averagePrice) : ""}; +} + +function Chart() { + return ( + + + + {chartPoints().length < 2 ? "COLLECTING 5M VALUE HISTORY" : ""} + {(item) => } + {(item) => } + + {chartLabels()[0] || ""}{chartLabels()[1] || ""}{chartLabels()[2] || ""} + + ); +} + +function DashboardScreen() { + const currentTrend = () => trend(); + return ( + +
+ + ACCOUNT + {dashboard().account.label + (dashboard().account.suffix ? " ····" + dashboard().account.suffix + " " + (accounts().findIndex((item) => item.number === selectedAccount()) + 1) + "/" + accounts().length : "") + " ›"} + + + + {money(dashboard().totalValue)} + {currentTrend().change + " (" + currentTrend().percent + ")"} + + + + 1D1W{span() === "day" ? "TODAY" : "PAST WEEK"} + + + + {"REALIZED P&L"}{"EQUITIES / " + (span() === "day" ? "TODAY" : "WEEK")}= 0 ? "text-2xl text-emerald-600 font-bold" : "text-2xl text-red-500 font-bold"}>{money(span() === "day" ? dashboard().pnlDay : dashboard().pnlWeek)} + + + ); +} + +function SideButtons() { + return ; +} + +function AccountsScreen() { + const visible = createMemo(() => accounts().slice(accountScroll(), accountScroll() + 8)); + const selectedAtOpen = selectedAccount(); + return ( + +
+ {(account) => ( + + {account.label}{"····" + account.suffix} + {account.status}SELECTED + + )} + + + ); +} + +function ActivityScreen() { + const visible = createMemo(() => dashboard().activity.slice(activityScroll(), activityScroll() + 8)); + return ( + +
+ 0} fallback={ + + }>{(item) => ( + + {item.title}{money(item.amount)} + {item.timestamp + " · " + item.detail} + {item.state} + + )} + + + ); +} + +function PositionsScreen() { + const visible = createMemo(() => dashboard().positions.slice(positionScroll(), positionScroll() + 9)); + return ( + +
+ 0} fallback={ + + }>{(item) => ( + + {item.symbol}{item.quantity + " SH"} + {"AVERAGE COST " + money(item.averagePrice) + (item.marketValue ? " · VALUE " + money(item.marketValue) : "")} + + )} + + + ); +} + +function SubScreen() { + if (screen() === "accounts") return ; + if (screen() === "activity") return ; + return ; +} + +function Robinhood() { + return }>; +} + +loadView(); +mount(() => ); + +(globalThis as any).PocketPiApp = { + tick, + dataChanged(eventsLine: string) { + const events = parse(eventsLine); + const revision = Array.isArray(events) + ? events.reduce((latest: number, event: any) => Math.max(latest, Number(event?.revision ?? 0)), currentRevision) + : currentRevision; + loadView(revision); + return ""; + }, + tap(x: number, y: number) { + if (screen() !== "dashboard") { + if (y < 112 && x < 220) { setScreen("dashboard"); return ""; } + if (x >= 620 && y >= 140 && y < 310) { + if (screen() === "accounts") setAccountScroll((value) => Math.max(0, value - 1)); + if (screen() === "activity") setActivityScroll((value) => Math.max(0, value - 1)); + if (screen() === "positions") setPositionScroll((value) => Math.max(0, value - 1)); + return ""; + } + if (x >= 620 && y >= 940 && y < 1130) { + if (screen() === "accounts") setAccountScroll((value) => Math.min(Math.max(0, accounts().length - 8), value + 1)); + if (screen() === "activity") setActivityScroll((value) => Math.min(Math.max(0, dashboard().activity.length - 8), value + 1)); + if (screen() === "positions") setPositionScroll((value) => Math.min(Math.max(0, dashboard().positions.length - 9), value + 1)); + return ""; + } + if (screen() === "accounts" && x < 610 && y >= 126) { + const index = accountScroll() + Math.floor((y - 126) / 112); + const account = accounts()[index]; + if (account) { + batch(() => { + setSelectedAccount(account.number); + setScreen("dashboard"); + }); + loadAccountProjection(account.number, currentRevision); + } + } + return ""; + } + if (y < 112 && x < 100) return JSON.stringify({ type: "navigate", app: "pi-agent" }); + if (y >= 112 && y < 176) { setScreen("accounts"); return ""; } + if (y >= 480 && y < 540) { batch(() => { setSpan(x < 136 ? "day" : "week"); loadChart(selectedAccount(), currentRevision); }); return ""; } + if (y >= 666 && y < 866) { setScreen("activity"); return ""; } + if (y >= 866 && y < 1050) { setScreen("positions"); return ""; } + if (y >= 1176 && x >= 500) { + if (refreshing()) return ""; + setRefreshing(true); + setStatus("REFRESHING ROBINHOOD…"); + return JSON.stringify({ type: "invokeTask", task: "refreshPortfolio" }); + } + return ""; + }, +}; diff --git a/apps/robinhood/data-action.ts b/apps/robinhood/data-action.ts new file mode 100644 index 0000000..7db03bc --- /dev/null +++ b/apps/robinhood/data-action.ts @@ -0,0 +1,526 @@ +import toolCatalog from "./tool-catalog.json"; + +// Headless Robinhood data plane. Every declared provider Tool returns its live +// result to the Agent. Only data consumed by the fixed foreground View is +// normalized into SQLite; transient research/watchlist/options payloads never +// become App state merely because a Tool was called. +const nativeDb = (globalThis as any).db; +const handle = nativeDb.open("robinhood"); +if (handle < 0) throw new Error("open robinhood.sqlite"); + +const SCHEMA_VERSION = 5; + +function dbError(): string { return String(nativeDb.lastError(handle) || "SQLite operation failed"); } +function exec(sql: string): void { if (nativeDb.exec(handle, sql) !== 0) throw new Error(dbError()); } +function query(sql: string, args: any[] = []): any { + const result = JSON.parse(nativeDb.query(handle, sql, JSON.stringify(args))); + if (result.error) throw new Error(String(result.error)); + return result; +} +function run(sql: string, args: any[] = []): any { return query(sql, args); } +function insertRows(sql: string, rows: any[][]): void { + if (!rows.length) return; + const values = rows.map((row) => "(" + row.map(() => "?").join(",") + ")").join(","); + const args: any[] = []; + for (const row of rows) args.push(...row); + run(sql + " VALUES " + values, args); +} + +const version = Number(query("PRAGMA user_version")?.rows?.[0]?.[0] ?? 0); +if (version !== SCHEMA_VERSION) { + exec(` + CREATE TABLE IF NOT EXISTS accounts ( + account_number TEXT PRIMARY KEY, + label TEXT NOT NULL, + suffix TEXT NOT NULL, + account_type TEXT, + status TEXT NOT NULL, + agentic_allowed INTEGER NOT NULL DEFAULT 0, + updated_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS portfolio_current ( + account_number TEXT PRIMARY KEY, + cash TEXT, + buying_power TEXT, + day_pnl TEXT, + week_pnl TEXT, + observed_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS total_value ( + account_number TEXT NOT NULL, + observed_at INTEGER NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY(account_number, observed_at) + ); + CREATE TABLE IF NOT EXISTS positions ( + account_number TEXT NOT NULL, + symbol TEXT NOT NULL, + quantity TEXT, + average_price TEXT, + market_value TEXT, + observed_at INTEGER NOT NULL, + PRIMARY KEY(account_number, symbol) + ); + CREATE TABLE IF NOT EXISTS activities ( + account_number TEXT NOT NULL, + activity_id TEXT NOT NULL, + occurred_at TEXT, + observed_at INTEGER NOT NULL, + symbol TEXT, + side TEXT, + quantity TEXT, + price TEXT, + amount TEXT, + state TEXT, + activity_type TEXT, + PRIMARY KEY(account_number, activity_id) + ); + CREATE INDEX IF NOT EXISTS activities_account_recent ON activities(account_number, occurred_at DESC, observed_at DESC); + CREATE TABLE IF NOT EXISTS refresh_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + started_at INTEGER NOT NULL, + completed_at INTEGER NOT NULL, + status TEXT NOT NULL, + operation_count INTEGER NOT NULL, + success_count INTEGER NOT NULL, + error TEXT + ); + CREATE INDEX IF NOT EXISTS refresh_runs_recent ON refresh_runs(id DESC); + PRAGMA user_version=5; + `); +} + +type DomainUpdate = { operation: string; args: any; value: any; observedAt: number }; +type ToolMetadata = { name: string; namespace: string; description: string; inputSchema: any }; + +const providerTools = (toolCatalog as any).tools as ToolMetadata[]; +const providerToolByName = new Map(providerTools.map((tool) => [tool.name, tool])); + +function now(): number { return Math.floor(Date.now() / 1000); } +function text(value: unknown): string | null { + return value === null || value === undefined || typeof value === "object" ? null : String(value); +} +function deep(value: any, names: string[]): string | null { + if (value === null || value === undefined || typeof value !== "object") return null; + for (const name of names) { + const found = text(value[name]); + if (found !== null) return found; + } + for (const child of Object.values(value)) { + const found = deep(child, names); + if (found !== null) return found; + } + return null; +} +function deepArray(value: any, names: string[]): any[] { + if (value === null || value === undefined || typeof value !== "object") return []; + for (const name of names) if (Array.isArray(value[name])) return value[name]; + for (const child of Object.values(value)) { + const found = deepArray(child, names); + if (found.length) return found; + } + return []; +} +function bool(value: any, names: string[]): boolean { + const raw = deep(value, names)?.toLowerCase(); + return raw === "true" || raw === "1" || raw === "yes"; +} +function number(value: string | null): number | null { + if (value === null) return null; + const parsed = Number(value.replace(/[$,%]/g, "")); + return Number.isFinite(parsed) ? parsed : null; +} +function accountNumber(value: any, args: any): string { + const requested = args?.account_number; + return requested === null || requested === undefined || requested === "" + ? deep(value, ["account_number", "accountNumber", "account"]) || "" + : String(requested); +} +function list(value: any, names: string[]): any[] { + const nested = deepArray(value, names); + return nested.length ? nested : Array.isArray(value) ? value : value ? [value] : []; +} + +function searchTools(args: any): any { + const exact = Array.isArray(args?.names) + ? new Set(args.names.filter((name: any) => typeof name === "string")) + : new Set(); + const words = String(args?.query ?? "").toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean); + const namespace = typeof args?.namespace === "string" ? args.namespace : ""; + if (!exact.size && !words.length) throw new Error("search_tools requires query or names"); + const limit = Math.max(1, Math.min(8, Number(args?.limit ?? 5) || 5)); + const matches = providerTools + .filter((tool) => !namespace || tool.namespace === namespace) + .map((tool) => { + const name = tool.name.toLowerCase(); + const haystack = name + " " + tool.description.toLowerCase(); + const score = exact.has(tool.name) ? 1000 + : words.reduce((total, word) => total + (name === word ? 100 : name.includes(word) ? 20 : haystack.includes(word) ? 3 : 0), 0); + return { tool, score }; + }) + .filter((item) => item.score > 0) + .sort((left, right) => right.score - left.score || left.tool.name.localeCompare(right.tool.name)) + .slice(0, limit) + .map(({ tool }) => ({ + name: tool.name, + namespace: tool.namespace, + description: tool.description, + inputSchema: tool.inputSchema, + })); + return { + source: (toolCatalog as any).source, + protocolVersion: (toolCatalog as any).protocolVersion, + matches, + }; +} + +function matchesType(value: any, expected: string): boolean { + if (expected === "null") return value === null; + if (expected === "array") return Array.isArray(value); + if (expected === "object") return value !== null && typeof value === "object" && !Array.isArray(value); + if (expected === "integer") return typeof value === "number" && Number.isInteger(value); + if (expected === "number") return typeof value === "number" && Number.isFinite(value); + return typeof value === expected; +} + +function validateSchema(value: any, schema: any, path = "arguments"): void { + const types = Array.isArray(schema?.type) ? schema.type : schema?.type ? [schema.type] : []; + if (types.length && !types.some((expected: string) => matchesType(value, expected))) { + throw new Error(path + " must be " + types.join(" or ")); + } + if (value === null) return; + if (typeof value === "number") { + if (typeof schema.minimum === "number" && value < schema.minimum) throw new Error(path + " is below minimum " + schema.minimum); + if (typeof schema.maximum === "number" && value > schema.maximum) throw new Error(path + " exceeds maximum " + schema.maximum); + } + if (Array.isArray(value)) { + if (schema.items) value.forEach((item, index) => validateSchema(item, schema.items, path + "[" + index + "]")); + return; + } + if (typeof value !== "object") return; + const properties = schema.properties || {}; + for (const required of schema.required || []) { + if (!(required in value)) throw new Error(path + "." + required + " is required"); + } + if (schema.additionalProperties === false) { + for (const key of Object.keys(value)) { + if (!(key in properties)) throw new Error(path + "." + key + " is not allowed"); + } + } + for (const [key, child] of Object.entries(value)) { + if (properties[key]) validateSchema(child, properties[key], path + "." + key); + } +} + +function validatedProviderCall(args: any): any { + const operation = typeof args?.name === "string" ? args.name : ""; + const tool = providerToolByName.get(operation); + if (!tool) throw new Error("Unknown Robinhood provider Tool: " + operation); + const providerArgs = args?.arguments; + validateSchema(providerArgs, tool.inputSchema); + return invokeProviderTool(operation, providerArgs); +} + +function retryableOperation(name: string): boolean { + return name.startsWith("get_") || name.startsWith("review_") || name === "search" || name === "run_scan"; +} + +function callTool(operation: string, args: any): any { + const envelope = JSON.parse((globalThis as any).services.call( + "mcp.client", + "callTool", + JSON.stringify({ + connection: "robinhood", + name: operation, + arguments: args, + retryable: retryableOperation(operation), + }), + )); + if (!envelope.ok) throw new Error(envelope.error || "Robinhood service failed"); + return envelope.value; +} + +function callTools(calls: Array<{ operation: string; args: any }>): any[] { + const envelope = JSON.parse((globalThis as any).services.call( + "mcp.client", + "callTools", + JSON.stringify({ + connection: "robinhood", + calls: calls.map((call) => ({ name: call.operation, arguments: call.args })), + retryable: calls.every((call) => retryableOperation(call.operation)), + }), + )); + if (!envelope.ok) throw new Error(envelope.error || "Robinhood batch service failed"); + return Array.isArray(envelope.value?.results) ? envelope.value.results : []; +} + +function transaction(action: () => void): void { + exec("BEGIN IMMEDIATE"); + try { + action(); + exec("COMMIT"); + } catch (error) { + try { exec("ROLLBACK"); } catch {} + throw error; + } + (globalThis as any).app.commit(); +} + +function saveAccounts(value: any, observedAt: number): void { + const rows = list(value, ["accounts"]); + run("DELETE FROM accounts"); + const values: any[][] = []; + for (const item of rows) { + const account = accountNumber(item, {}); + if (!account) continue; + const accountType = (deep(item, ["nickname", "account_type", "type"]) || "").toUpperCase(); + const agentic = bool(item, ["agentic_allowed", "agenticAllowed"]); + const label = agentic ? "AGENTIC" + : accountType.includes("IRA") || accountType.includes("RETIRE") ? "RETIREMENT" + : accountType.includes("JOINT") ? "JOINT" : "PERSONAL"; + values.push([account, label, account.slice(-4), accountType, (deep(item, ["status"]) || "active").toUpperCase(), agentic ? 1 : 0, observedAt]); + } + insertRows("INSERT INTO accounts(account_number,label,suffix,account_type,status,agentic_allowed,updated_at)", values); +} + +function savePortfolio(value: any, args: any, observedAt: number): void { + const account = accountNumber(value, args); + if (!account) throw new Error("Robinhood portfolio is missing account_number"); + const cash = deep(value, ["cash", "cash_available", "withdrawable_amount"]); + const buyingPower = deep(value, ["buying_power", "buyingPower"]); + const dayPnl = deep(value, ["day_pnl", "dayPnl", "equity_change"]); + const weekPnl = deep(value, ["week_pnl", "weekPnl"]); + run( + `INSERT INTO portfolio_current(account_number,cash,buying_power,day_pnl,week_pnl,observed_at) + VALUES(?,?,?,?,?,?) + ON CONFLICT(account_number) DO UPDATE SET + cash=excluded.cash,buying_power=excluded.buying_power, + day_pnl=COALESCE(excluded.day_pnl,portfolio_current.day_pnl), + week_pnl=COALESCE(excluded.week_pnl,portfolio_current.week_pnl), + observed_at=excluded.observed_at`, + [account, cash, buyingPower, dayPnl, weekPnl, observedAt], + ); + const total = deep(value, ["total_value", "equity", "total_equity", "portfolio_value", "market_value"]); + if (total !== null) { + run("INSERT OR REPLACE INTO total_value(account_number,observed_at,value) VALUES(?,?,?)", [account, observedAt, total]); + } +} + +function savePositions(value: any, args: any, observedAt: number): void { + const account = accountNumber(value, args); + if (!account) throw new Error("Robinhood positions are missing account_number"); + const rows = list(value, ["positions"]).slice(0, 64); + run("DELETE FROM positions WHERE account_number=?", [account]); + const values: any[][] = []; + for (const item of rows) { + const symbol = deep(item, ["symbol"]); + if (!symbol) continue; + values.push([account, symbol, deep(item, ["quantity", "shares"]), deep(item, ["average_price", "averagePrice", "average_buy_price"]), deep(item, ["market_value", "marketValue", "equity"]), observedAt]); + } + insertRows("INSERT INTO positions(account_number,symbol,quantity,average_price,market_value,observed_at)", values); +} + +function saveActivities(value: any, args: any, observedAt: number): void { + const account = accountNumber(value, args); + if (!account) throw new Error("Robinhood activities are missing account_number"); + const rows = list(value, ["orders", "activities", "results"]).slice(0, 64); + run("DELETE FROM activities WHERE account_number=?", [account]); + const values: any[][] = []; + rows.forEach((item, index) => { + const symbol = deep(item, ["symbol"]); + const side = (deep(item, ["side"]) || "").toUpperCase(); + const quantity = deep(item, ["executed_quantity", "cumulative_quantity", "quantity"]); + const price = deep(item, ["average_price", "averagePrice", "executed_price", "price"]); + const occurredAt = deep(item, ["last_transaction_at", "created_at", "updated_at", "date"]); + const explicitId = deep(item, ["id", "order_id", "orderId", "activity_id"]); + const activityId = explicitId || [occurredAt || observedAt, symbol || "ORDER", side, index].join(":"); + const quantityNumber = number(quantity); + const priceNumber = number(price); + const amount = quantityNumber !== null && priceNumber !== null ? String(quantityNumber * priceNumber) : price || quantity; + values.push([account, activityId, occurredAt, observedAt, symbol, side, quantity, price, amount, (deep(item, ["state", "status"]) || "RECENT").toUpperCase(), (deep(item, ["type", "order_type"]) || "ORDER").toUpperCase()]); + }); + insertRows("INSERT INTO activities(account_number,activity_id,occurred_at,observed_at,symbol,side,quantity,price,amount,state,activity_type)", values); +} + +function saveRealizedPnl(value: any, args: any, observedAt: number): void { + const account = accountNumber(value, args); + if (!account) throw new Error("Robinhood P&L is missing account_number"); + const pnl = deep(value, ["total_returns", "realized_pnl", "total", "amount", "day_pnl", "week_pnl"]); + const span = String(args?.span ?? "day"); + run( + `INSERT INTO portfolio_current(account_number,day_pnl,week_pnl,observed_at) VALUES(?,?,?,?) + ON CONFLICT(account_number) DO UPDATE SET + day_pnl=COALESCE(excluded.day_pnl,portfolio_current.day_pnl), + week_pnl=COALESCE(excluded.week_pnl,portfolio_current.week_pnl), + observed_at=MAX(portfolio_current.observed_at,excluded.observed_at)`, + [account, span === "week" ? null : pnl, span === "week" ? pnl : null, observedAt], + ); +} + +function saveProjection(update: DomainUpdate): boolean { + if (update.operation === "get_accounts") saveAccounts(update.value, update.observedAt); + else if (update.operation === "get_portfolio") savePortfolio(update.value, update.args, update.observedAt); + else if (update.operation === "get_equity_positions") savePositions(update.value, update.args, update.observedAt); + else if (update.operation === "get_equity_orders") saveActivities(update.value, update.args, update.observedAt); + else if (update.operation === "get_realized_pnl") saveRealizedPnl(update.value, update.args, update.observedAt); + else return false; + return true; +} + +function isTransportFailure(message: string): boolean { + const value = message.toLowerCase(); + return value.includes("esp_err_http_connect") || value.includes("timeout") + || value.includes("tls") || value.includes("socket") || value.includes("network"); +} + +function refreshPortfolio(): any { + const startedAt = now(); + const updates: DomainUpdate[] = []; + const errors: string[] = []; + let operationCount = 0; + + const request = (operation: string, args: any): any | null => { + operationCount += 1; + try { + const value = callTool(operation, args); + updates.push({ operation, args, value, observedAt: now() }); + return value; + } catch (error) { + const message = error instanceof Error ? error.message : String(error); + errors.push(operation + ": " + message); + if (operation === "get_accounts" || isTransportFailure(message)) throw error; + return null; + } + }; + + let terminalError: string | null = null; + try { + const accountsValue = request("get_accounts", {}); + const accountRows = list(accountsValue, ["accounts"]); + const accountNumbers = accountRows.map((item) => accountNumber(item, {})).filter(Boolean); + if (!accountNumbers.length) throw new Error("Robinhood returned no brokerage accounts"); + const recentOrdersSince = new Date((startedAt - 7 * 24 * 60 * 60) * 1000).toISOString().slice(0, 10); + const calls: Array<{ operation: string; args: any }> = []; + for (const account of accountNumbers) { + const args = { account_number: account }; + calls.push( + { operation: "get_portfolio", args }, + { operation: "get_equity_positions", args }, + { operation: "get_equity_orders", args: { ...args, created_at_gte: recentOrdersSince } }, + { operation: "get_realized_pnl", args: { ...args, span: "day", asset_classes: ["equity"] } }, + { operation: "get_realized_pnl", args: { ...args, span: "week", asset_classes: ["equity"] } }, + ); + } + operationCount += calls.length; + const results = callTools(calls); + if (results.length !== calls.length) throw new Error("Robinhood batch returned an incomplete result set"); + results.forEach((result, index) => { + const call = calls[index]; + if (result?.ok) { + updates.push({ operation: call.operation, args: call.args, value: result.value, observedAt: now() }); + } else { + errors.push(call.operation + ": " + String(result?.error || "unknown provider error")); + } + }); + if (!updates.some((update) => update.operation === "get_portfolio")) { + throw new Error("Robinhood batch returned no portfolio data"); + } + } catch (error) { + terminalError = error instanceof Error ? error.message : String(error); + } + + const status = terminalError ? "failed" : errors.length ? "partial" : "succeeded"; + transaction(() => { + if (!terminalError) for (const update of updates) saveProjection(update); + run( + `INSERT INTO refresh_runs(started_at,completed_at,status,operation_count,success_count,error) + VALUES(?,?,?,?,?,?)`, + [startedAt, now(), status, operationCount, terminalError ? 0 : updates.length, terminalError || errors.join(" | ") || null], + ); + }); + if (terminalError) throw new Error(terminalError); + return { status, operationCount, successCount: updates.length }; +} + +function saveEquityAction(operation: string, value: any, args: any, observedAt: number): void { + const account = String(args?.account_number ?? ""); + if (!account) return; + const orderId = String(args?.order_id ?? deep(value, ["order_id", "orderId", "id"]) ?? ""); + const activityId = orderId || [observedAt, args?.symbol || "EQUITY", operation].join(":"); + const state = operation === "cancel_equity_order" + ? deep(value, ["state", "status"]) || "CANCEL_REQUESTED" + : deep(value, ["state", "status"]) || "SUBMITTED"; + run( + `INSERT INTO activities(account_number,activity_id,occurred_at,observed_at,symbol,side,quantity,price,amount,state,activity_type) + VALUES(?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(account_number,activity_id) DO UPDATE SET + occurred_at=COALESCE(excluded.occurred_at,activities.occurred_at), + observed_at=excluded.observed_at, + symbol=COALESCE(excluded.symbol,activities.symbol), + side=COALESCE(excluded.side,activities.side), + quantity=COALESCE(excluded.quantity,activities.quantity), + price=COALESCE(excluded.price,activities.price), + state=excluded.state, + activity_type=excluded.activity_type`, + [ + account, + activityId, + deep(value, ["created_at", "updated_at", "last_transaction_at"]), + observedAt, + args?.symbol ?? deep(value, ["symbol"]), + args?.side ?? deep(value, ["side"]), + args?.quantity ?? args?.dollar_amount ?? deep(value, ["quantity", "executed_quantity"]), + args?.limit_price ?? args?.stop_price ?? deep(value, ["price", "average_price"]), + args?.dollar_amount ?? null, + String(state).toUpperCase(), + operation === "cancel_equity_order" ? "EQUITY ORDER CANCEL" : "EQUITY ORDER", + ], + ); +} + +function invokeProviderTool(operation: string, args: any): any { + const value = callTool(operation, args); + const update = { operation, args, value, observedAt: now() }; + if (["get_accounts", "get_portfolio", "get_equity_positions", "get_equity_orders", "get_realized_pnl"].includes(operation)) { + transaction(() => saveProjection(update)); + } + if (operation === "place_equity_order" || operation === "cancel_equity_order") { + // The order result directly affects the View's activity projection. Avoid + // nesting a second MCP request in the same 128 KiB QuickJS call stack; + // portfolio and positions converge on the normal refresh task. + transaction(() => saveEquityAction(operation, value, args, update.observedAt)); + } + return value; +} + +function success(value: any): string { + // The Agent consumes Tool results through text. Keeping the same provider + // object in details would retain a second copy in QuickJS and again in the + // Rust/Agent message bridge, which is especially expensive for market-data + // and options responses. + return JSON.stringify({ text: JSON.stringify(value), isError: false }); +} + +(globalThis as any).PocketPiData = { + invokeTask(name: string) { + try { + if (name !== "refreshPortfolio") throw new Error("Unknown Robinhood Data Action: " + name); + const value = refreshPortfolio(); + return success(value); + } catch (error) { + return JSON.stringify({ text: error instanceof Error ? error.message : String(error), isError: true }); + } + }, + invokeTool(name: string, argsLine: string) { + try { + const args = JSON.parse(argsLine); + const value = name === "robinhood.refresh_portfolio" ? refreshPortfolio() + : name === "robinhood.search_tools" ? searchTools(args) + : name === "robinhood.call" ? validatedProviderCall(args) + : (() => { throw new Error("Unknown Robinhood Tool: " + name); })(); + return success(value); + } catch (error) { + return JSON.stringify({ text: error instanceof Error ? error.message : String(error), isError: true }); + } + }, +}; diff --git a/apps/robinhood/dist/app.js b/apps/robinhood/dist/app.js new file mode 100644 index 0000000..11d2076 --- /dev/null +++ b/apps/robinhood/dist/app.js @@ -0,0 +1,22 @@ +(()=>{var _J={context:void 0,registry:void 0,effects:void 0,done:!1,getContextId(){return M1(this.context.count)},getNextContextId(){return M1(this.context.count++)}};function M1(J){let Q=String(J),$=Q.length-1;return _J.context.id+($?String.fromCharCode(96+$):"")+Q}function P1(J){_J.context=J}function F6(){return{..._J.context,id:_J.getNextContextId(),count:0}}var B6=!1,K6=(J,Q)=>J===Q,A0=Symbol("solid-proxy"),U6=typeof Proxy==="function",M6=Symbol("solid-track"),AQ=Symbol("solid-dev-component"),nJ={equals:K6},V1=null,P6=_1,p=1,hJ=2,D1={owned:null,cleanups:null,context:null,owner:null},S=null,B=null,EJ=null,AJ=null,f=null,b=null,N=null,oJ=0;function tJ(J,Q){let $=f,Z=S,X=J.length===0,W=Q===void 0?Z:Q,Y=X?D1:{owned:null,cleanups:null,context:W?W.context:null,owner:W},F=X?J:()=>J(()=>WJ(()=>qJ(Y)));S=Y,f=null;try{return r(F,!0)}finally{f=$,S=Z}}function h(J,Q){Q=Q?Object.assign({},nJ,Q):nJ;let $={value:J,observers:null,observerSlots:null,comparator:Q.equals||void 0},Z=(X)=>{if(typeof X==="function")if(B&&B.running&&B.sources.has($))X=X($.tValue);else X=X($.value);return L1($,X)};return[O1.bind($),Z]}function XJ(J,Q,$){let Z=k1(J,Q,!1,p);if(EJ&&B&&B.running)b.push(Z);else J0(Z)}function QJ(J,Q,$){$=$?Object.assign({},nJ,$):nJ;let Z=k1(J,Q,!0,0);if(Z.observers=null,Z.observerSlots=null,Z.comparator=$.equals||void 0,EJ&&B&&B.running)Z.tState=p,b.push(Z);else J0(Z);return O1.bind(Z)}function eJ(J){return r(J,!1)}function WJ(J){if(!AJ&&f===null)return J();let Q=f;f=null;try{if(AJ)return AJ.untrack(J);return J()}finally{f=Q}}function w0(J){if(S===null);else if(S.cleanups===null)S.cleanups=[J];else S.cleanups.push(J);return J}function V6(J){if(B&&B.running)return J(),B.done;let Q=f,$=S;return Promise.resolve().then(()=>{f=Q,S=$;let Z;if(EJ||D6)Z=B||(B={sources:new Set,effects:[],promises:new Set,disposed:new Set,queue:new Set,running:!0}),Z.done||(Z.done=new Promise((X)=>Z.resolve=X)),Z.running=!0;return r(J,!1),f=S=null,Z?Z.done:void 0})}var[wQ,R1]=h(!1),D6;function O1(){let J=B&&B.running;if(this.sources&&(J?this.tState:this.state))if((J?this.tState:this.state)===p)J0(this);else{let Q=b;b=null,r(()=>Q0(this),!1),b=Q}if(f){let Q=this.observers;if(!Q||Q[Q.length-1]!==f){let $=Q?Q.length:0;if(!f.sources)f.sources=[this],f.sourceSlots=[$];else f.sources.push(this),f.sourceSlots.push($);if(!Q)this.observers=[f],this.observerSlots=[f.sources.length-1];else Q.push(f),this.observerSlots.push(f.sources.length-1)}}if(J&&B.sources.has(this))return this.tValue;return this.value}function L1(J,Q,$){let Z=B&&B.running&&B.sources.has(J)?J.tValue:J.value;if(!J.comparator||!J.comparator(Z,Q)){if(B){let X=B.running;if(X||!$&&B.sources.has(J))B.sources.add(J),J.tValue=Q;if(!X)J.value=Q}else J.value=Q;if(J.observers&&J.observers.length)r(()=>{for(let X=0;X1e6)throw b=[],Error()},!1)}return Q}function J0(J){if(!J.fn)return;qJ(J);let Q=oJ;if(I1(J,B&&B.running&&B.sources.has(J)?J.tValue:J.value,Q),B&&!B.running&&B.sources.has(J))queueMicrotask(()=>{r(()=>{B&&(B.running=!0),f=S=J,I1(J,J.tValue,Q),f=S=null},!1)})}function I1(J,Q,$){let Z,X=S,W=f;f=S=J;try{Z=J.fn(Q)}catch(Y){if(J.pure)if(B&&B.running)J.tState=p,J.tOwned&&J.tOwned.forEach(qJ),J.tOwned=void 0;else J.state=p,J.owned&&J.owned.forEach(qJ),J.owned=null;return J.updatedAt=$+1,y0(Y)}finally{f=W,S=X}if(!J.updatedAt||J.updatedAt<=$){if(J.updatedAt!=null&&"observers"in J)L1(J,Z,!0);else if(B&&B.running&&J.pure){if(!B.sources.has(J))J.value=Z;B.sources.add(J),J.tValue=Z}else J.value=Z;J.updatedAt=$}}function k1(J,Q,$,Z=p,X){let W={fn:J,state:Z,updatedAt:null,owned:null,sources:null,sourceSlots:null,cleanups:null,value:Q,owner:S,context:S?S.context:null,pure:$};if(B&&B.running)W.state=0,W.tState=Z;if(S===null);else if(S!==D1)if(B&&B.running&&S.pure)if(!S.tOwned)S.tOwned=[W];else S.tOwned.push(W);else if(!S.owned)S.owned=[W];else S.owned.push(W);if(AJ&&W.fn){let Y=W.fn,[F,D]=h(void 0,{equals:!1}),R=AJ.factory(Y,D);w0(()=>R.dispose());let U,H=()=>V6(D).then(()=>{if(U)U.dispose(),U=void 0});W.fn=(w)=>{if(F(),B&&B.running){if(!U)U=AJ.factory(Y,H);return U.track(w)}return R.track(w)}}return W}function S0(J){let Q=B&&B.running;if((Q?J.tState:J.state)===0)return;if((Q?J.tState:J.state)===hJ)return Q0(J);if(J.suspense&&WJ(J.suspense.inFallback))return J.suspense.effects.push(J);let $=[J];while((J=J.owner)&&(!J.updatedAt||J.updatedAt=0;Z--){if(J=$[Z],Q){let X=J,W=$[Z+1];while((X=X.owner)&&X!==W)if(B.disposed.has(X))return}if((Q?J.tState:J.state)===p)J0(J);else if((Q?J.tState:J.state)===hJ){let X=b;b=null,r(()=>Q0(J,$[0]),!1),b=X}}}function r(J,Q){if(b)return J();let $=!1;if(!Q)b=[];if(N)$=!0;else N=[];oJ++;try{let Z=J();return R6($),Z}catch(Z){if(!$)N=null;b=null,y0(Z)}}function R6(J){if(b){if(EJ&&B&&B.running)O6(b);else _1(b);b=null}if(J)return;let Q;if(B){if(!B.promises.size&&!B.queue.size){let{sources:Z,disposed:X}=B;N.push.apply(N,B.effects),Q=B.resolve;for(let W of N)"tState"in W&&(W.state=W.tState),delete W.tState;B=null,r(()=>{for(let W of X)qJ(W);for(let W of Z){if(W.value=W.tValue,W.owned)for(let Y=0,F=W.owned.length;YP6($),!1);if(Q)Q()}function _1(J){for(let Q=0;Q{Z.delete($),r(()=>{B.running=!0,S0($)},!1),B&&(B.running=!1)})}}function Q0(J,Q){let $=B&&B.running;if($)J.tState=0;else J.state=0;for(let Z=0;Z=0;Q--)qJ(J.tOwned[Q]);delete J.tOwned}if(B&&B.running&&J.pure)w1(J,!0);else if(J.owned){for(Q=J.owned.length-1;Q>=0;Q--)qJ(J.owned[Q]);J.owned=null}if(J.cleanups){for(Q=J.cleanups.length-1;Q>=0;Q--)J.cleanups[Q]();J.cleanups=null}if(B&&B.running)J.tState=0;else J.state=0}function w1(J,Q){if(!Q)J.tState=0,B.disposed.add(J);if(J.owned)for(let $=0;$1?[]:null;return w0(()=>y1(W)),()=>{let D=J()||[],R=D.length,U,H;return D[M6],WJ(()=>{let x,g,A,s,a,K,j,G,P;if(R===0){if(Y!==0)y1(W),W=[],Z=[],X=[],Y=0,F&&(F=[]);if($.fallback)Z=[I6],X[0]=tJ((k)=>{return W[0]=k,$.fallback()}),Y=1}else if(Y===0){X=Array(R);for(H=0;H=K&&G>=K&&Z[j]===D[G];j--,G--)A[G]=X[j],s[G]=W[j],F&&(a[G]=F[j]);x=new Map,g=Array(G+1);for(H=G;H>=K;H--)P=D[H],U=x.get(P),g[H]=U===void 0?-1:U,x.set(P,H);for(U=K;U<=j;U++)if(P=Z[U],H=x.get(P),H!==void 0&&H!==-1)A[H]=X[U],s[H]=W[U],F&&(a[H]=F[U]),H=g[H],x.set(P,H);else W[U]();for(H=K;HJ(Q||{}));return P1($),Z}}return WJ(()=>J(Q||{}))}function $0(){return!0}var w6={get(J,Q,$){if(Q===A0)return $;return J.get(Q)},has(J,Q){if(Q===A0)return!0;return J.has(Q)},set:$0,deleteProperty:$0,getOwnPropertyDescriptor(J,Q){return{configurable:!0,enumerable:!0,get(){return J.get(Q)},set:$0,deleteProperty:$0}},ownKeys(J){return J.keys()}};function f0(J){return!(J=typeof J==="function"?J():J)?{}:J}function S6(){for(let J=0,Q=this.length;J=0;F--){let D=f0(J[F])[Y];if(D!==void 0)return D}},has(Y){for(let F=J.length-1;F>=0;F--)if(Y in f0(J[F]))return!0;return!1},keys(){let Y=[];for(let F=0;F=0;Y--){let F=J[Y];if(!F)continue;let D=Object.getOwnPropertyNames(F);for(let R=D.length-1;R>=0;R--){let U=D[R];if(U==="__proto__"||U==="constructor")continue;let H=Object.getOwnPropertyDescriptor(F,U);if(!Z[U])Z[U]=H.get?{enumerable:!0,configurable:!0,get:S6.bind($[U]=[H.get.bind(F)])}:H.value!==void 0?H:void 0;else{let w=$[U];if(w){if(H.get)w.push(H.get.bind(F));else if(H.value!==void 0)w.push(()=>H.value)}}}}let X={},W=Object.keys(Z);for(let Y=W.length-1;Y>=0;Y--){let F=W[Y],D=Z[F];if(D&&D.get)Object.defineProperty(X,F,D);else X[F]=D?D.value:void 0}return X}var y6=(J)=>`Stale read from <${J}>.`;function NJ(J){let Q="fallback"in J&&{fallback:()=>J.fallback};return QJ(k6(()=>J.each,J.children,Q||void 0))}function Z0(J){let Q=J.keyed,$=QJ(()=>J.when,void 0,void 0),Z=Q?$:QJ($,void 0,{equals:(X,W)=>!X===!W});return QJ(()=>{let X=Z();if(X){let W=J.children;return typeof W==="function"&&W.length>0?WJ(()=>W(Q?X:()=>{if(!WJ(Z))throw y6("Show");return $()})):W}return J.fallback},void 0,void 0)}var f6=(J)=>QJ(()=>J());function x6({createElement:J,createTextNode:Q,isTextNode:$,replaceText:Z,insertNode:X,removeNode:W,setProperty:Y,getParentNode:F,getFirstChild:D,getNextSibling:R}){function U(K,j,G,P){if(G!==void 0&&!P)P=[];if(typeof j!=="function")return H(K,j,P,G);XJ((k)=>H(K,j(),k,G),P)}function H(K,j,G,P,k){while(typeof G==="function")G=G();if(j===G)return G;let _=typeof j,I=P!==void 0;if(_==="string"||_==="number"){if(_==="number")j=j.toString();if(I){let L=G[0];if(L&&$(L))Z(L,j);else L=Q(j);G=g(K,G,P,L)}else if(G!==""&&typeof G==="string")Z(D(K),G=j);else g(K,G,P,Q(j)),G=j}else if(j==null||_==="boolean")G=g(K,G,P);else if(_==="function")return XJ(()=>{let L=j();while(typeof L==="function")L=L();G=H(K,L,G,P)}),()=>G;else if(Array.isArray(j)){let L=[];if(w(L,j,k))return XJ(()=>G=H(K,L,G,P,!0)),()=>G;if(L.length===0){let bJ=g(K,G,P);if(I)return G=bJ}else if(Array.isArray(G))if(G.length===0)A(K,L,P);else x(K,G,L);else if(G==null||G==="")A(K,L);else x(K,I&&G||[D(K)],L);G=L}else{if(Array.isArray(G)){if(I)return G=g(K,G,P,j);g(K,G,null,j)}else if(G==null||G===""||!D(K))X(K,j);else s(K,j,D(K));G=j}return G}function w(K,j,G){let P=!1;for(let k=0,_=j.length;k<_;k++){let I=j[k],L;if(I==null||I===!0||I===!1);else if(Array.isArray(I))P=w(K,I)||P;else if((L=typeof I)==="string"||L==="number")K.push(Q(I));else if(L==="function")if(G){while(typeof I==="function")I=I();P=w(K,Array.isArray(I)?I:[I])||P}else K.push(I),P=!0;else K.push(I)}return P}function x(K,j,G){let P=G.length,k=j.length,_=P,I=0,L=0,bJ=R(j[k-1]),IJ=null;while(IJJ-L){let _Q=j[I];while(L=0;I--){let L=j[I];if(k!==L){let bJ=F(L)===K;if(!_&&!I)bJ?s(K,k,L):X(K,k,G);else bJ&&W(K,L)}else _=!0}}else X(K,k,G);return[k]}function A(K,j,G){for(let P=0,k=j.length;PG.children=H(K,j.children,G.children));return XJ(()=>j.ref&&j.ref(K)),XJ(()=>{for(let k in j){if(k==="children"||k==="ref")continue;let _=j[k];if(_===G[k])continue;Y(K,k,_,G[k]),G[k]=_}}),G}return{render(K,j){let G;return tJ((P)=>{G=P,U(j,K())}),G},insert:U,spread(K,j,G){if(typeof j==="function")XJ((P)=>a(K,j(),P,G));else a(K,j,void 0,G)},createElement:J,createTextNode:Q,insertNode:X,setProp(K,j,G,P){return Y(K,j,G,P),G},mergeProps:f1,effect:XJ,memo:f6,createComponent:A6,use(K,j,G){return WJ(()=>K(j,G))}}}function C6(J){let Q=x6(J);return Q.mergeProps=f1,Q}var x1=480,C1=272,KJ={view:0,text:1,image:2},g6=1,b6=-1,h6={width:1,height:2,minW:3,minH:4,maxW:5,maxH:6,paddingT:8,paddingR:9,paddingB:10,paddingL:11,marginT:12,marginR:13,marginB:14,marginL:15,gap:16,flexDir:17,justify:18,align:19,grow:20,shrink:21,basis:22,flexWrap:23,posType:24,insetT:25,insetR:26,insetB:27,insetL:28,display:29,overflow:30,zIndex:31,hitPass:32,bgColor:64,gradFrom:65,gradTo:66,gradDir:67,radius:68,opacity:69,borderColor:70,borderWidth:71,shadow:72,bevelOuterLight:77,bevelOuterDark:78,bevelInnerLight:79,bevelInnerDark:80,bevelWidth:81,textColor:96,fontSlot:97,textAlign:98,lineHeight:99,tracking:100,translateX:128,translateY:129,scale:130,rotate:131,scaleX:132,scaleY:133,originX:134,originY:135,rotateX:136,rotateY:137,translateZ:138,perspective:139,arcStart:140,arcSweep:141,arcWidth:142},M={f32:0,color:1,int:2},E6={width:M.f32,height:M.f32,minW:M.f32,minH:M.f32,maxW:M.f32,maxH:M.f32,paddingT:M.f32,paddingR:M.f32,paddingB:M.f32,paddingL:M.f32,marginT:M.f32,marginR:M.f32,marginB:M.f32,marginL:M.f32,gap:M.f32,flexDir:M.int,justify:M.int,align:M.int,grow:M.f32,shrink:M.f32,basis:M.f32,flexWrap:M.int,posType:M.int,insetT:M.f32,insetR:M.f32,insetB:M.f32,insetL:M.f32,display:M.int,overflow:M.int,zIndex:M.int,hitPass:M.int,bgColor:M.color,gradFrom:M.color,gradTo:M.color,gradDir:M.int,radius:M.f32,opacity:M.f32,borderColor:M.color,borderWidth:M.f32,shadow:M.int,bevelOuterLight:M.color,bevelOuterDark:M.color,bevelInnerLight:M.color,bevelInnerDark:M.color,bevelWidth:M.f32,textColor:M.color,fontSlot:M.int,textAlign:M.int,lineHeight:M.f32,tracking:M.f32,translateX:M.f32,translateY:M.f32,scale:M.f32,rotate:M.f32,scaleX:M.f32,scaleY:M.f32,originX:M.f32,originY:M.f32,rotateX:M.f32,rotateY:M.f32,translateZ:M.f32,perspective:M.f32,arcStart:M.f32,arcSweep:M.f32,arcWidth:M.f32},c={FlexDir:{Row:0,Col:1},Justify:{Start:0,Center:1,End:2,Between:3,Around:4},Align:{Start:0,Center:1,End:2,Stretch:3},PosType:{Relative:0,Absolute:1},Display:{Flex:0,None:1},Overflow:{Visible:0,Hidden:1},TextAlign:{Left:0,Center:1,Right:2},GradDir:{ToTop:0,ToBottom:1,ToLeft:2,ToRight:3},Easing:{Linear:0,EaseIn:1,EaseOut:2,EaseInOut:3,OutBack:4,Spring:5,SpringBouncy:6,CubicBezier:7}},N6={PSM_5650:0,PSM_4444:2,PSM_8888:3,PSM_T8:5},T6=1,SQ=2,yQ=1,fQ=2,xQ=131072,CQ=1,gQ=262144,bQ=1,hQ=1,EQ=1,NQ=2,TQ=4,vQ=8,mQ=16,lQ=1,uQ=2;function g1(J,Q,$,Z=255){return((Z&255)<<24|($&255)<<16|(Q&255)<<8|J&255)>>>0}var pQ=1,v6=1263551300,m6=1,l6=32,u6=24,n={SELECT:1,START:8,UP:16,RIGHT:32,DOWN:64,LEFT:128,LTRIGGER:256,RTRIGGER:512,TRIANGLE:4096,CIRCLE:8192,CROSS:16384,SQUARE:32768},UJ=32896,cQ=0.016666666666666666;function b1(J){return J.__viewport??null}var x0=null;function p6(){return null}function h1(J,Q=p6()){if(!Q)return;if(typeof J.__host!=="string")throw Error(`PocketJS: this bundle targets "${Q.target}" but the native host predates platform `+"contracts — add __host/__hostAbi to its ui namespace (see framework/src/host.ts HostOps)");if(J.__host!==Q.target)throw Error(`PocketJS: native target mismatch (bundle=${Q.target}, host=${J.__host})`);if(J.__hostAbi!==Q.hostAbi)throw Error(`PocketJS: native host ABI mismatch (bundle=${Q.hostAbi}, host=${J.__hostAbi??"missing"})`)}function c6(J){let Q=globalThis.ui,$=Q!==void 0&&(typeof Q.__host==="string"||Q.__textures!==void 0);if(J){if(Q!==void 0&&J===Q&&$)return h1(Q),{ops:J,kind:"native",target:Q.__host??"unknown",strict:!1};return{ops:J,kind:"injected",target:J.__host??"injected",strict:!0}}if(Q!==void 0&&$)return h1(Q),{ops:Q,kind:"native",target:Q.__host??"unknown",strict:!1};if(Q)return{ops:Q,kind:"injected",target:"injected",strict:!0};throw Error("PocketJS: no host — pass HostOps to render() (web/test) or run under a native runtime (globalThis.ui)")}function d6(J){x0=J}function wJ(){if(!x0)throw Error("PocketJS: host not installed — call render() first");return x0}function E(){return wJ().ops}function i6(J){globalThis.frame=J}function s6(J){let Q=globalThis,$=Q.__pocketResizeViewport,Z=(X,W)=>J(X,W);return Q.__pocketResizeViewport=Z,()=>{if(Q.__pocketResizeViewport!==Z)return;if($)Q.__pocketResizeViewport=$;else delete Q.__pocketResizeViewport}}function a6(J){let Q=J.slice(1);if(Q.length===3)Q=Q[0]+Q[0]+Q[1]+Q[1]+Q[2]+Q[2];if(Q.length!==6&&Q.length!==8)throw Error(`PocketJS: bad color '${J}' (expected #rgb/#rrggbb/#rrggbbaa)`);if(!/^[0-9a-fA-F]+$/.test(Q))throw Error(`PocketJS: bad color '${J}'`);let $=parseInt(Q,16);if(Q.length===6)return g1($>>>16&255,$>>>8&255,$&255,255);return g1($>>>24&255,$>>>16&255,$>>>8&255,$&255)}function r6(J,Q){let $=E6[J];if(typeof Q==="string"){if($===M.color)return a6(Q);let Z=Number(Q);if(Number.isNaN(Z))throw Error(`PocketJS: non-numeric value '${Q}' for prop '${J}'`);Q=Z}if($===M.color||$===M.int)return Q>>>0;return Q}var X0=60,E1=[1,2,3,4,5,6,10,12,15,20,30,60],C0=X0,zJ=-1,n6=0,TJ=[];function o6(J){if(!Number.isFinite(J)||J<=0)return X0;let Q=E1[0];for(let $ of E1)if(Math.abs($-J)Q.at<=zJ).sort((Q,$)=>Q.at-$.at||Q.seq-$.seq);if(J.length===0)return;TJ=TJ.filter((Q)=>Q.at>zJ);for(let Q of J)Q.cb()}var g0=0.12,W0=UJ;function Q8(J){W0=J===void 0?UJ:J&65535}function $8(){W0=UJ}function v1(J){let Q=Math.max(-1,Math.min(1,(J-128)/127)),$=Math.abs(Q);if($>8&255)}function X8(){return v1(W0&255)}var m1=new Set,W8=0;function q8(){m1.clear(),W8=0,$8()}function z8(J){for(let Q of[...m1])Q(J)}var MJ=null,l1=null;function Y8(J,Q,$){let Z="";for(let X=0;X<$;X++)Z+=String.fromCharCode(J[Q+X]);return Z}function u1(J){let Q=new DataView(J);if(J.byteLength=J.length&&$.slice(0,J.length)===J)Q.push($);return Q.sort(),Q}function vJ(J){b0();let Q=MJ?MJ.get(J):void 0;if(!Q)throw Error("pak: missing key "+J+" (no __pak provided, or the pack is incomplete)");return l1.slice(Q.off,Q.off+Q.len)}var mJ=null,C=null,PJ=null,q0=0,VJ=[],E0=[],N0=[];function c1(J){if(mJ=J,C=null,PJ=null,q0=0,VJ.length=0,E0.length=0,N0.length=0,d){if(d.pressTarget=null,d.target=null,d.spriteDirty=!0,d.fresh=!0,d.vw=0,d.tex>=0){let Q=E();Q.setCursor?.(-1,0,0,0,0),Q.freeTexture?.(d.tex),d.tex=-1}}}function H8(J,Q){J.onPress=Q??void 0}function j8(J,Q){if(J.focusable=Q,r1(),!Q&&C===J)v(null)}function v(J){if(PJ&&PJ!==J)DJ(null);C=J,E().setFocus(J?J.id:0)}function DJ(J){if(PJ===J)return;let Q=E();if(PJ)Q.setActive?.(PJ.id,0);if(PJ=J,J)Q.setActive?.(J.id,1)}function T0(){return VJ.length>0?VJ[VJ.length-1]:mJ}function v0(J,Q){if(!J)return;if(J.focusable)Q.push(J);if(!Array.isArray(J.children))return;for(let $=0;$=$.length)return;v($[X])}function K8(){if(!C)return null;let J=T0();if(J&&!o(C,J))return null;for(let Q=E0.length-1;Q>=0;Q--){let $=E0[Q];if(J&&!o($.node,J)&&!o(J,$.node))continue;if(o(C,$.node))return $}return null}function U8(J){let Q=K8();if(!Q)return!1;let $=[];if(v0(Q.node,$),$.length===0){if(C)v(null);return!0}let Z=Q.columns,X=C?$.indexOf(C):-1;if(X<0)return v(d1(J)===1?$[0]:$[$.length-1]),!0;let W=X;switch(J){case"right":if(X+1<$.length&&X%Z0)W=X-1;else if(Q.wrap)W=Math.min($.length-1,Math.floor(X/Z)*Z+Z-1);break;case"down":if(X+Z<$.length)W=X+Z;else if(Q.wrap)W=X%Z;break;case"up":if(X-Z>=0)W=X-Z;else if(Q.wrap){W=X%Z;while(W+Z<$.length)W+=Z}break}if(W!==X)v($[W]);return!0}function M8(){if(!C)return null;let J=T0();if(J&&!o(C,J))return null;for(let Q=N0.length-1;Q>=0;Q--){let $=N0[Q];if(J&&!o($.node,J)&&!o(J,$.node))continue;if(o(C,$.node))return $}return null}function z0(J){let Q=M8();if(Q&&Q.move(J))return;if(U8(J))return;B8(J)}function i1(){s1(C)}function s1(J){let Q=J;while(Q){if(Q.onPress){Q.onPress();return}Q=Q.parent}}function Y0(J){DJ(J)}function P8(J){v(J),s1(J)}function o(J,Q){if(!J||!Q)return!1;let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function m0(J){if(!J)return null;if(J.focusable)return J;if(!Array.isArray(J.children))return null;for(let Q=0;Q=0;X--){let W=m0(Q.children[X]);if(W){v(W);return}}let Z=Q;while(Z){if(Z.focusable){v(Z);return}Z=Z.parent}}v(null)}var d=null,a1=0;function r1(){a1++}var D8=[1,3,5,9,17,33,65,129,257,513,1985,73,149,147,288,480],R8=[0,0,2,6,14,30,62,126,254,510,62,54,98,96,192,0];function O8(){let J=new Uint8Array(1024);for(let Q=0;Q<16;Q++)for(let $=0;$<16;$++){let Z=D8[Q]>>$&1,X=R8[Q]>>$&1;if(!Z&&!X)continue;let W=(Q*16+$)*4,Y=X?255:0;J[W]=Y,J[W+1]=Y,J[W+2]=Y,J[W+3]=255}return J}function L8(J,Q){let $=J.sprite;J.spriteDirty=!1;let Z=J.tex,X=-1,W=null;if(typeof $.image==="string")try{W=vJ($.image)}catch(Y){if(wJ().strict)throw Y;W=null}else if($.image)W=$.image;if(W){if(X=Q.uploadImgEntry?Q.uploadImgEntry(W):I8(Q,W),X<0&&wJ().strict)throw Error("enableCursor: cursor image rejected (malformed or RLE-only IMG entry)")}if(X<0)X=Q.uploadTexture(O8(),16,16,N6.PSM_8888);if(J.tex=X,Q.setCursor(X,$.hotspot[0],$.hotspot[1],$.size[0],$.size[1]),Z>=0&&Z!==X)Q.freeTexture?.(Z)}function I8(J,Q){if(Q.length<8)return-1;let $=new DataView(Q.buffer,Q.byteOffset,Q.byteLength);if(Q[5]&T6)return-1;return J.uploadTexture(Q.subarray(8),$.getUint16(0,!0),$.getUint16(2,!0),Q[4])}function G0(J,Q){if(!J||Q===0)return null;if(J.id===Q)return J;let $=J.children;if(!Array.isArray($))return null;for(let Z=0;Z<$.length;Z++){let X=G0($[Z],Q);if(X)return X}return null}var H0=null;function n1(J){H0=J}function o1(J){let Q=VJ.length>0?VJ[VJ.length-1]:null,$=J;while($){if($.focusable&&(!Q||o($,Q)))return $;$=$.parent}return null}function t1(J,Q,$){return o1(e1(J,Q,$))}function e1(J,Q,$){if($!==void 0)return $===0?null:G0(H0??mJ,$);let Z=E(),X=Z.hitTestBounds??Z.hitTest;if(!X)return null;return G0(H0??mJ,X(J,Q))}function k8(J,Q,$){let Z=d,X=E();if(!X.hitTest||!X.setCursor||!X.setCursorPos)return!1;if(Z.vw===0){let H=b1(X);if(Z.vw=H?H.w:x1,Z.vh=H?H.h:C1,Z.x<0)Z.x=Math.floor(Z.vw/2),Z.y=Math.floor(Z.vh/2)}if(Z.spriteDirty)L8(Z,X);let W=Z8()*Z.speed,Y=X8()*Z.speed;if(Z.dpadSpeed>0&&W===0&&Y===0){if(J&n.LEFT)W=-Z.dpadSpeed;if(J&n.RIGHT)W=Z.dpadSpeed;if(J&n.UP)Y=-Z.dpadSpeed;if(J&n.DOWN)Y=Z.dpadSpeed}let F=Z.fresh;if(W!==0||Y!==0){let H=t6()/60,w=Math.min(Math.max(Z.x+W*H,0),Z.vw-1),x=Math.min(Math.max(Z.y+Y*H,0),Z.vh-1);if(w!==Z.x||x!==Z.y)Z.x=w,Z.y=x,F=!0}if(F)X.setCursorPos(Z.x,Z.y);let D=(Q|$)&Z.button,R=a1;if(F||D!==0||R!==Z.gen)Z.gen=R,Z.fresh=!1,Z.target=o1(G0(H0??mJ,X.hitTest(Z.x,Z.y)));let U=Z.target;if(U!==C)v(U);if(Q&Z.button&&U)Z.pressTarget=U;if(Z.pressTarget){if(DJ(U===Z.pressTarget?Z.pressTarget:null),$&Z.button){let H=U===Z.pressTarget;if(Z.pressTarget=null,DJ(null),H)i1()}}else if($&Z.button)DJ(null);return!0}function _8(J){let Q=J&~q0,$=q0&~J;if(q0=J,d&&k8(J,Q,$))return;if($&n.CIRCLE)DJ(null);if(Q===0)return;if(Q&n.DOWN)z0("down");if(Q&n.RIGHT)z0("right");if(Q&n.UP)z0("up");if(Q&n.LEFT)z0("left");if(Q&n.CIRCLE)DJ(C),i1()}var l0=null;function J4(J){l0=J}function SJ(){if(r1(),l0)l0()}function A8(J,Q){J.debugName=Q||void 0,SJ()}var YJ={id:g6,type:KJ.view,parent:null,children:[],domNodeType:1,domTag:"root"},Q4=Symbol.for("pocketjs.native-node"),u0=1,lJ=3,$J=8,w8=new Set(["class","className","style","src","onPress","on:press","focusable","debugName","ref","nodeRef","key","children"]);function j0(J){return J.domAttrs??={}}function $4(J,Q){let $=J.domNodeType??(uJ(J)?lJ:u0),Z=$===lJ?F0(J.text??""):$===$J?f8(J.domData??""):G4(J.domTag??K0(J));for(let X of Object.keys(J.domAttrs??{}))p0(Z,X,J.domAttrs[X]);if(Q)for(let X of J.children)GJ(Z,$4(X,!0));return Z}function p0(J,Q,$){if(w8.has(Q)){fJ(J,Q,$,J.domAttrs?.[Q]);return}if($==null)delete j0(J)[Q];else j0(J)[Q]=$}function c0(J){if(J[Q4]===!0)return J;return Object.defineProperty(J,Q4,{value:!0}),Object.defineProperties(J,{nodeType:{configurable:!0,get(){return J.domNodeType??(uJ(J)?lJ:u0)}},nodeValue:{configurable:!0,get(){return J.domNodeType===$J?J.domData??"":J.text??""},set($){if(J.domNodeType===$J)J.domData=String($??"");else B0(J,String($??""))}},data:{configurable:!0,get(){return J.domNodeType===$J?J.domData??"":J.text??""},set($){if(J.domNodeType===$J)J.domData=String($??"");else B0(J,String($??""))}},textContent:{configurable:!0,get(){if(J.domNodeType===$J)return J.domData??"";if(uJ(J))return J.text??"";return J.children.map(($)=>$.text??"").join("")},set($){let Z=String($??"");if(J.domNodeType===$J)J.domData=Z;else if(uJ(J))B0(J,Z);else if(N8(J),Z)GJ(J,F0(Z))}},parentNode:{configurable:!0,get(){return J.parent}},parentElement:{configurable:!0,get(){return J.parent}},childNodes:{configurable:!0,get(){return J.children}},firstChild:{configurable:!0,get(){return J.children[0]??null}},lastChild:{configurable:!0,get(){return J.children[J.children.length-1]??null}},nextSibling:{configurable:!0,get(){return j4(J)??null}},previousSibling:{configurable:!0,get(){let $=J.parent;if(!$)return null;let Z=$.children.indexOf(J);return Z>0?$.children[Z-1]:null}},tagName:{configurable:!0,get(){return(J.domTag??K0(J)).toUpperCase()}},nodeName:{configurable:!0,get(){if(J.domNodeType===lJ)return"#text";if(J.domNodeType===$J)return"#comment";return(J.domTag??K0(J)).toUpperCase()}},className:{configurable:!0,get(){return String(J.domAttrs?.class??"")},set($){fJ(J,"class",$,J.domAttrs?.class)}},isConnected:{configurable:!0,get(){let $=J;while($){if($===YJ)return!0;$=$.parent}return!1}}}),Object.assign(J,{appendChild($){return GJ(J,$),$},insertBefore($,Z){return GJ(J,$,Z??null),$},removeChild($){return pJ(J,$),$},replaceChild($,Z){return GJ(J,$,Z),pJ(J,Z),Z},cloneNode($=!1){return $4(J,!!$)},remove(){if(J.parent)pJ(J.parent,J)},setAttribute($,Z){p0(J,$,Z)},removeAttribute($){p0(J,$,void 0)},getAttribute($){let Z=J.domAttrs?.[$];return Z==null?null:String(Z)},hasAttribute($){return J.domAttrs?.[$]!=null},hasChildNodes(){return J.children.length>0},contains($){let Z=$??null;while(Z){if(Z===J)return!0;Z=Z.parent}return!1},addEventListener(){},removeEventListener(){}},{style:{length:0,item:()=>""},classList:{add(){},remove(){}}}),J}c0(YJ);var d0=null;function S8(J){d0=J}var i0={unknownClass:0,unknownTexture:0},Z4=new Map;function X4(J,Q){Z4.set(J,Q)}var W4=new Map;function q4(J,Q){W4.set(J,Q)}var yJ=new Set,y8=new Set;function z4(J){if(!J)return!1;if(y8.has(J))return!0;if(J.children){for(let Q=0;Q - only view/text/image exist`);return c0({id:E().createNode(Q),type:Q,parent:null,children:[],domNodeType:u0,domTag:J})}function F0(J){let Q=E(),$=Q.createNode(KJ.text);return Q.setText($,J),c0({id:$,type:KJ.text,parent:null,children:[],text:J,domNodeType:lJ,domTag:"#text"})}function f8(J=""){let Q=F0("");return Q.domNodeType=$J,Q.domTag="#comment",Q.domData=J,Q}function B0(J,Q){E().replaceText(J.id,Q),J.text=Q,SJ()}function uJ(J){return J.type===KJ.text}function H4(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);if($>=0)Q.children.splice($,1);J.parent=null}function GJ(J,Q,$){let Z=E();if(H4(Q),yJ.delete(Q),Z.insertBefore(J.id,Q.id,$?$.id:0),$){let X=J.children.indexOf($);if(X<0)throw Error("PocketJS: insert anchor is not a child of parent");J.children.splice(X,0,Q)}else J.children.push(Q);Q.parent=J,SJ()}function pJ(J,Q){if(!Q)return;V8(Q),E().removeChild(J.id,Q.id),H4(Q),yJ.add(Q),SJ()}function x8(J){return J.parent??void 0}function C8(J){return J.children[0]}function j4(J){let Q=J.parent;if(!Q)return;let $=Q.children.indexOf(J);return $>=0?Q.children[$+1]:void 0}function g8(J,Q){let $=E();if(SJ(),Q==null||Q===""){$.setStyle(J.id,b6);return}if(typeof Q!=="string")throw Error("PocketJS: class must be a string literal of utilities");let Z=d0?d0(Q):void 0;if(Z===void 0){if(wJ().strict)throw Error(`PocketJS: unknown class "${Q}" - not in the compiled style table (dynamic classes must be ternaries of full literals)`);i0.unknownClass++;return}$.setStyle(J.id,Z)}function b8(J,Q){let $=E();if(Q==null||Q===""){$.setImage(J.id,-1);return}if(typeof Q!=="string")throw Error("PocketJS: src must be a string key");let Z=Z4.get(Q);if(Z===void 0){if(wJ().strict)throw Error(`PocketJS: unknown image src "${Q}" - no texture registered under that key`);i0.unknownTexture++;return}$.setImage(J.id,Z)}function h8(J,Q){let $=E();if(Q==null||Q===""){$.setSprite(J.id,-1,0,0,0);return}if(typeof Q!=="string")throw Error("PocketJS: sprite must be a string key");let Z=W4.get(Q);if(Z===void 0){if(wJ().strict)throw Error(`PocketJS: unknown sprite "${Q}" - no sprite atlas registered under that key`);i0.unknownTexture++;return}$.setSprite(J.id,Z.handle,Z.frames,Z.cols,Z.step)}function E8(J,Q,$){let Z=E(),X=Q??{},W=$??{},Y=!1;for(let F in X){let D=X[F];if(W[F]===D)continue;let R=h6[F];if(R===void 0)throw Error(`PocketJS: unknown style prop '${F}' (see spec PROP)`);Z.setProp(J.id,R,r6(F,D)),Y=!0}if(Y)SJ()}function fJ(J,Q,$,Z){if($===Z&&Q!=="style")return $;if(Q==="className")Q="class";if(Q!=="children"&&Q!=="key"&&Q!=="ref"&&Q!=="nodeRef")if($==null)delete j0(J)[Q];else j0(J)[Q]=$;switch(Q){case"class":return g8(J,$),$;case"onPress":case"on:press":return H8(J,$),$;case"src":return b8(J,$),$;case"sprite":return h8(J,$),$;case"style":return E8(J,$,Z),$;case"focusable":return j8(J,!!$),$;case"debugName":return A8(J,$==null?void 0:String($)),$;case"ref":case"nodeRef":case"key":case"children":return $;default:break}if(Q==="classList")throw Error("PocketJS: classList is not supported - use ternaries of full class literals");if(Q.startsWith("on:")||Q.startsWith("bool:")||Q.startsWith("prop:"))throw Error(`PocketJS: unsupported namespaced attribute '${Q}'`);throw Error(`PocketJS: unknown property '${Q}' on <${K0(J)}>`)}function N8(J){for(let Q of[...J.children])pJ(J,Q)}function K0(J){for(let Q of Object.keys(KJ))if(KJ[Q]===J.type)return Q;return String(J.type)}function T8(J,Q,$,Z){if(Q==="ref"&&typeof $==="function"){$(J);return}fJ(J,Q,$,Z)}var v8=C6({createElement:G4,createTextNode:F0,replaceText:B0,isTextNode:uJ,setProperty:T8,insertNode(J,Q,$){GJ(J,Q,$)},removeNode(J,Q){pJ(J,Q)},getParentNode:x8,getFirstChild:C8,getNextSibling:j4}),{render:m8,effect:dQ,memo:m,createComponent:q,createElement:F4,insert:iQ,spread:l8,mergeProps:sQ,use:aQ}=v8,rQ={linear:c.Easing.Linear,in:c.Easing.EaseIn,out:c.Easing.EaseOut,"in-out":c.Easing.EaseInOut,"out-back":c.Easing.OutBack,spring:c.Easing.Spring,"spring-bouncy":c.Easing.SpringBouncy},u8=null;function B4(J){u8=J}function p8(J,Q){if(!J)return;if(typeof J==="function")J(Q);else if("current"in J)J.current=Q}function K4(J,Q){let $=F4(J);return l8($,Q,!1),p8(Q.nodeRef,$),$}function V(J){return K4("view",J)}function O(J){return K4("text",J)}var nQ=new WeakMap,oQ=new WeakMap,c8={}!==null?Object.freeze({...{}}):Object.freeze({}),tQ=Object.freeze({target:"",pixelRatio:Number.isInteger(1)?1:1,features:c8}),eQ=new Map,HJ=36000,s0=30,d8=30,z={ops:null,transport:null,app:void 0,frame:0,tape:new Uint16Array(HJ),tapeAnalog:new Uint16Array(HJ),tapeTouch:null,tapeStart:0,tapeLen:0,tapeFirstFrame:0,replayMasks:null,replayAnalog:null,replayTouch:null,replayAt:0,paused:!1,stepQueued:0,inspectReportId:null,inspectAskedAt:0,treeDirty:!0,treeSentAt:-s0,saidHello:!1,hostCalls:0};function i8(J){let Q=globalThis;if(!Q.console)Q.console={log(){},warn(){},error(){}};z.ops=J,z.frame=0,z.tapeStart=0,z.tapeLen=0,z.tapeFirstFrame=0,z.tapeTouch=null,z.replayMasks=null,z.replayAnalog=null,z.replayTouch=null,z.paused=!1,z.stepQueued=0,z.inspectReportId=null,z.inspectAskedAt=0,z.treeDirty=!0,z.treeSentAt=-s0,z.saidHello=!1,z.hostCalls=0,z.app=globalThis.__pocketApp;let $=globalThis.__pocketDevtoolsTransport;if($)z.transport=$;else if(J.__dbgActive?.()&&J.__dbgPoll&&J.__dbgSend)z.transport={send:(Z)=>J.__dbgSend(Z),recv:()=>J.__dbgPoll(),everyFrames:10};else z.transport=null;if(z.transport)J4(()=>{z.treeDirty=!0}),J2();else J4(null);globalThis.__pocketDevtools=$2}function s8(J){return(Q,$,Z,X)=>{if(z.hostCalls++,z.transport)r8(),t8();let W=Q,Y=$===void 0?UJ:$&65535,F=Z,D=X;if(z.replayMasks)if(z.replayAt0?$.slice(0,8):null;if(Z&&!z.tapeTouch)z.tapeTouch=Array(HJ).fill(null);if(z.tapeLen1||Q.length===1&&Q[0][0]!==UJ)J.analog=Q;if(z.tapeTouch){let $=[];for(let Z=0;Z0)J.v=2,J.touch=$}return J}function P4(J,Q,$){let Z=new Uint16Array($).fill(Q),X=0;for(let[W,Y]of J)Z.fill(W,X,Math.min(X+Y,$)),X+=Y;return Z}function V4(J){let Q=0;for(let[,$]of J.masks)Q+=$;return P4(J.masks,0,Q)}function D4(J){let Q=0;for(let[,$]of J.masks)Q+=$;return P4(J.analog??[],UJ,Q)}function R4(J){let Q=0;for(let[,Z]of J.masks)Q+=Z;let $=Array(Q).fill(void 0);for(let[Z,X]of J.touch??[])if(Z>=0&&Z1&&z.hostCalls%Q!==0)return;if(!z.saidHello)z.saidHello=!0,T({t:"hello",app:z.app,host:Q2(),frame:z.frame});for(let $=0;$<64;$++){let Z=J.recv();if(!Z)break;for(let X of Z.split(` +`))if(X.trim())n8(X)}}function n8(J){let Q;try{Q=JSON.parse(J)}catch{return}let $=z.ops;switch(Q.t){case"inspect":{let Z=typeof Q.id==="number"?Q.id:0;if($?.debugInspect?.(Z),z.inspectReportId=Z||null,z.inspectAskedAt=z.hostCalls,!Z)T({t:"inspect",id:0,rect:null});break}case"pause":z.paused=!0,z.stepQueued=0,$?.debugPause?.(!0),a0();break;case"resume":z.paused=!1,$?.debugPause?.(!1),a0();break;case"step":z.stepQueued+=typeof Q.n==="number"&&Q.n>0?Q.n:1;break;case"getTree":O4();break;case"eval":{let Z=!0,X;try{X=U0((0,eval)(String(Q.code)))}catch(W){Z=!1,X=W instanceof Error?`${W.name}: ${W.message}`:String(W)}T({t:"evalResult",id:Q.id,ok:Z,value:X});break}case"dumpTape":T({t:"tape",tape:M4()});break;case"devStats":{let Z=null,X=$?.debugStats?.();if(X)try{Z=JSON.parse(X)}catch{Z=null}T({t:"devStats",frame:z.frame,data:Z});break}case"screenshot":{if($?.__dbgShot?.())T({t:"screenshotRaw",file:"shot.raw",w:480,h:272,stride:512,frame:z.frame});else T({t:"log",level:"warn",args:["screenshot: not supported on this host"]});break}case"replay":{let Z=Q.tape;if(Z&&Array.isArray(Z.masks))z.replayMasks=V4(Z),z.replayAnalog=Z.analog?D4(Z):null,z.replayTouch=Z.touch?R4(Z):null,z.replayAt=0;break}default:break}}function o8(){if(z.treeDirty&&z.frame-z.treeSentAt>=s0)O4();if(z.frame%d8===0)a0()}function t8(){let J=z.inspectReportId;if(J==null)return;let Q=z.ops;if(!Q?.debugRectXY||!Q.debugRectWH){z.inspectReportId=null;return}let $=Q.debugRectXY();if($===-1){if(z.hostCalls-z.inspectAskedAt>60)z.inspectReportId=null,T({t:"inspect",id:J,rect:null});return}let Z=Q.debugRectWH();z.inspectReportId=null,T({t:"inspect",id:J,rect:[$<<16>>16,$>>16,Z&65535,Z>>16&65535]})}function a0(){T({t:"stats",frame:z.frame,nodes:k4(YJ),tapeLen:z.tapeLen,paused:z.paused})}function O4(){z.treeDirty=!1,z.treeSentAt=z.frame,T({t:"tree",frame:z.frame,root:I4(YJ)})}function e8(J){if(J==null||typeof J!=="object")return!1;let Q=J;return typeof Q.id==="number"&&typeof Q.type==="number"}function r0(J,Q){if(Array.isArray(J)){for(let $ of J)r0($,Q);return}if(e8(J)){Q(J);return}if(J!=null&&typeof J==="object"){let $=J.nodes;if($!==void 0)r0($,Q)}}function L4(J,Q){let $=Array.isArray(J.children)?J.children:[];for(let Z of $)r0(Z,Q)}function I4(J){let Q={i:J.id,t:J.domTag??String(J.type)};if(J.debugName)Q.n=J.debugName;let $=J.domAttrs?.class;if(typeof $==="string"&&$)Q.c=$;if(J.text)Q.x=J.text.length>80?J.text.slice(0,79)+"…":J.text;let Z=[];if(L4(J,(X)=>{if(X.domNodeType===8)return;Z.push(I4(X))}),Z.length)Q.k=Z;return Q}function k4(J){let Q=1;return L4(J,($)=>{Q+=k4($)}),Q}function J2(){let J=globalThis;if(!J.console)J.console={};let Q=J.console;if(Q.__pocketBridged)return;Q.__pocketBridged=!0;for(let $ of["log","warn","error"]){let Z=Q[$];Q[$]=(...X)=>{T({t:"log",level:$,args:X.map((W)=>U0(W))}),Z?.apply(Q,X)}}}function U0(J,Q=0){if(J===void 0)return"undefined";if(J===null)return"null";let $=typeof J;if($==="string"){let W=J;return Q===0?_4(W):JSON.stringify(_4(W))}if($==="number"||$==="boolean"||$==="bigint")return String(J);if($==="function"){let W=J.name;return W?`[function ${W}]`:"[function]"}if(Q>=3)return Array.isArray(J)?"[…]":"{…}";if(Array.isArray(J)){let W=J.slice(0,20).map((Y)=>U0(Y,Q+1));if(J.length>20)W.push(`… ${J.length-20} more`);return`[${W.join(", ")}]`}if(J instanceof Error)return`${J.name}: ${J.message}`;return`{${Object.entries(J).slice(0,20).map(([W,Y])=>`${W}: ${U0(Y,Q+1)}`).join(", ")}}`}function _4(J){return J.length>200?J.slice(0,199)+"…":J}function Q2(){let J=z.ops;if(typeof J?.__host==="string")return J.__host;if(J?.__textures!==void 0)return"psp";if(typeof globalThis.document<"u")return"web";return"headless"}var $2={get frame(){return z.frame},dumpTape:()=>M4(),replay:(J)=>{z.replayMasks=V4(J),z.replayAnalog=J.analog?D4(J):null,z.replayTouch=J.touch?R4(J):null,z.replayAt=0}},A4=new Map,n0=new Map;function w4(J){return J.trim().replace(/\s+/g," ")}function S4(J){return J.split(" ").sort().join(" ")}var y4=-1;function Z2(J){for(let Q of Object.keys(J)){let $=J[Q],Z=w4(Q);A4.set(Z,$);let X=S4(Z),W=n0.get(X);n0.set(X,W!==void 0&&W!==$?y4:$)}}function X2(J){let Q=w4(J),$=A4.get(Q);if($!==void 0)return $;let Z=n0.get(S4(Q));return Z===y4?void 0:Z}var o0=9,W2=(1<{let X=($&z2)!==0,W=X?t0:o0,Y=X?Y2:W2;return Object.freeze({id:$>>>(X?G2:q2)&255,x:$&Y,y:$>>>W&Y,hit:Q?.[Z]})}))}function j2(){return M0}function F2(){M0=e0}var f4=8,jJ=8,B2=3,K2=8,U2=6,M2=0.5,t=1,cJ=2,J1=4,RJ=8,xJ=[],x4=0,P0=0,FJ=Array.from({length:f4},(J,Q)=>({slot:Q,used:!1,present:!1,id:0,x:0,y:0,startX:0,startY:0,dx:0,dy:0,fdx:0,fdy:0,vx:0,vy:0,downFrame:0,frames:0,histX:new Int16Array(jJ),histY:new Int16Array(jJ),histHead:0,histLen:0,owners:[],claimedBy:null}));function P2(J,Q){let $=J;while($){if($===Q)return!0;$=$.parent}return!1}function V2(J,Q,$,Z){let X=J.opts.region;if(!X)return!0;let W=X.node?.();if(W){if(Z.hit===void 0)Z.hit=e1(Q,$,Z.fact);let F=Z.hit;if(F)return P2(F,W)}let Y=X.rect?.();if(!Y)return!1;return Q>=Y.x&&Q=Y.y&&$=0;Y--){let F=xJ[Y];if(F.disposed)continue;if(x4>0&&!F.opts.allowWhenBlocked)continue;if(!V2(F,$,Z,W))continue;F.flags[J.slot]=t,J.owners.push(F)}for(let Y of J.owners)Y.opts.onDown?.(J)}function O2(J,Q,$){if(J.present=!0,J.fdx=Q-J.x,J.fdy=$-J.y,J.x=Q,J.y=$,J.dx=Q-J.startX,J.dy=$-J.startY,J.frames++,J.histX[J.histHead]=Q,J.histY[J.histHead]=$,J.histHead=(J.histHead+1)%jJ,J.histLenY||Z>Y))X.flags[J.slot]|=cJ}if(!J.claimedBy)for(let X of J.owners){let W=X.flags[J.slot];if(!(W&t)||W&(cJ|J1))continue;if(!X.opts.onLongPress)continue;let Y=Math.max(1,Math.round((X.opts.longPressSeconds??M2)*N1()));if(J.frames=0)xJ.splice($,1)},cancel(){if(!Q.disposed)b4(Q)},get panning(){for(let $ of FJ)if($.used&&Q.flags[$.slot]&RJ)return!0;return!1}}}function A2(J){let Q=_2(J);return w0(()=>Q.dispose()),Q}function h4(){xJ.length=0,x4=0,P0=0;for(let J of FJ)J.used=!1,J.present=!1,J.owners.length=0,J.claimedBy=null}function w2(){return A2({onDown:(J)=>{let Q=t1(J.x,J.y,J.hit);if(Q)Y0(Q)},onTap:(J)=>{Y0(null);let Q=t1(J.x,J.y,J.hit);if(Q)P8(Q)},onUp:()=>Y0(null),onCancel:()=>Y0(null)})}var S2=1,Q1=new Map,V0=[];function y2(){let J=globalThis.__pocketEffectTrace;return typeof J==="function"?J:null}function f2(){S2=1,Q1.clear(),V0=[]}function x2(){if(V0.length===0)return;let J=V0;V0=[];for(let{id:Q,result:$}of J){let Z=Q1.get(Q);if(!Z)continue;Q1.delete(Q),y2()?.({t:"delivery",frame:T1(),id:Q,kind:Z.kind}),Z.onResult($)}}var E4=new Set;function C2(){if(E4.size===0)return;for(let J of E4)J()}var g2={"w-[210] h-[104]":0,"h-[72] px-5 flex-row items-center justify-between":1,"w-[450] flex-col gap-2":2,"text-lg text-slate-900 font-bold":3,"text-base text-slate-500":4,"text-base text-emerald-600 font-bold":5,"text-base text-slate-900 font-bold":6,"h-[64] px-5 flex-row items-center":7,"w-[240] text-lg text-slate-900 font-bold":8,"w-[156] text-base text-slate-500":9,"relative w-[632] h-[196] flex-col":10,"relative w-[632] h-[160] overflow-hidden":11,"absolute w-[632] h-[2] bg-slate-200":12,"absolute text-base text-slate-500 font-bold":13,"absolute rounded-lg bg-emerald-500":14,"absolute rounded-lg bg-red-500":15,"absolute w-[6] h-[6] rounded-lg bg-emerald-500":16,"absolute w-[6] h-[6] rounded-lg bg-red-500":17,"h-[36] px-1 flex-row items-center justify-between":18,"flex-col w-full h-full bg-slate-50":19,"h-[64] px-6 pt-2":20,"w-full h-[56] px-5 flex-row items-center justify-between rounded-xl shadow bg-white border-slate-100":21,"text-base text-slate-500 font-bold":22,"h-[304] px-6 pt-3":23,"h-[291] px-5 pt-4 flex-col rounded-xl shadow bg-white border-slate-100":24,"h-[64] flex-row items-end justify-between":25,"text-2xl text-slate-950 font-bold":26,"text-lg text-emerald-600 font-bold":27,"text-lg text-red-500 font-bold":28,"h-[60] px-6 pt-2 flex-row gap-3":29,"w-[100] h-[44] items-center justify-center rounded-lg bg-orange-600":30,"w-[100] h-[44] items-center justify-center rounded-lg bg-white":31,"text-base text-white font-bold":32,"pt-3 text-base text-slate-500":33,"h-[126] px-6 pt-3 flex-row items-start gap-[21]":34,"h-[200] px-6 flex-col":35,"h-[150] flex-col rounded-xl shadow bg-white border-slate-100":36,"h-[184] px-6 flex-col":37,"h-[136] flex-col rounded-xl shadow bg-white border-slate-100":38,"h-[126] px-6 pt-2":39,"w-full h-[110] px-5 flex-row items-center justify-between rounded-xl shadow bg-white border-slate-100":40,"flex-col gap-3":41,"text-xl text-slate-900 font-bold":42,"text-2xl text-emerald-600 font-bold":43,"text-2xl text-red-500 font-bold":44,"h-[104] px-6 flex-row items-center justify-between":45,"w-[460] h-[64]":46,"w-[176] h-[64]":47,"absolute left-[628] top-[156] w-[68] h-[132] items-center justify-center rounded-xl bg-orange-100":48,"absolute left-[628] top-[972] w-[68] h-[132] items-center justify-center rounded-xl bg-orange-100":49,"relative flex-col w-full h-full bg-slate-50":50,"px-6 pt-[14] flex-col gap-[12]":51,"w-[584] h-[100] px-5 flex-col justify-center gap-3 rounded-xl shadow bg-emerald-100 border-emerald-500":52,"w-[584] h-[100] px-5 flex-col justify-center gap-3 rounded-xl shadow bg-white border-slate-100":53,"flex-row items-center justify-between":54,"text-lg text-slate-500 font-bold":55,"px-6 pt-[14] flex-col gap-[14]":56,"w-[584] h-[112] px-5 flex-col justify-center gap-2 rounded-xl shadow bg-white border-slate-100":57,"w-[584] h-[98] px-5 flex-col justify-center gap-3 rounded-xl shadow bg-white border-slate-100":58,"flex-row items-center":59,"w-[170] text-xl text-slate-900 font-bold":60,"relative flex-col w-full h-full bg-slate-50 overflow-hidden":61,"absolute inset-0 z-50 flex-col items-center justify-center":62,"absolute inset-0 bg-slate-950":63,"flex-col gap-2 w-[328] p-3 rounded-xl shadow-lg bg-white border-slate-200":64,"absolute left-3 right-3 bottom-3 flex-row items-center justify-between px-2 py-1 rounded-lg shadow-md bg-white border-slate-200":65,"flex-row flex-wrap":66,grow:67,"text-2xl text-white font-bold":68,"text-base text-slate-600 font-bold":69,"px-3 py-2 rounded-lg bg-slate-100":70,"px-3 py-2 rounded-lg bg-indigo-100":71,"text-base text-indigo-700 font-bold":72,"px-3 py-2 rounded-lg bg-emerald-100":73,"text-base text-emerald-700 font-bold":74,"px-3 py-2 rounded-lg bg-amber-100":75,"text-base text-amber-700 font-bold":76,"px-3 py-2 rounded-lg bg-red-100":77,"text-base text-red-500 font-bold":78,"w-[34] h-[34] rounded-lg bg-amber-400":79,"w-[34] h-[34] rounded-lg bg-red-500":80,"w-[34] h-[34] rounded-lg bg-slate-800":81,"w-[34] h-[34] rounded-lg bg-emerald-500":82,"h-[112] px-6 flex-row items-center justify-between bg-slate-950":83,"flex-row items-center gap-4":84,"w-[34] text-2xl text-white font-bold":85,"w-[332] flex-col items-end gap-2":86,"text-base text-slate-300 font-bold":87,"text-base text-slate-400":88,"h-[166] px-6 pt-6 flex-col gap-3":89,"text-base text-orange-600 font-bold":90,"text-lg text-slate-600":91,"h-[44] px-1 flex-row items-center justify-between":92,"w-full h-full items-center justify-center rounded-xl bg-slate-200":93,"w-full h-full items-center justify-center rounded-xl bg-red-100":94,"w-full h-full items-center justify-center rounded-xl bg-slate-100":95,"w-full h-full items-center justify-center rounded-xl bg-orange-600":96,"text-lg text-white font-bold":97,"w-full h-[150] px-5 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":98,"w-full h-[430] px-12 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":99,"w-[88] h-[88] items-center justify-center rounded-xl bg-indigo-100":100,"w-[88] h-[88] items-center justify-center rounded-xl bg-slate-100":101,"text-2xl text-indigo-700 font-bold":102,"text-2xl text-slate-600 font-bold":103,"pt-7 text-2xl text-slate-900 font-bold":104,"pt-4 text-lg text-slate-500":105,"text-xl text-emerald-600 font-bold":106,"text-xl text-red-500 font-bold":107,"w-full h-full px-5 py-4 flex-col gap-3 rounded-xl shadow bg-white border-slate-100":108,"text-base text-red-300":109,"text-base text-slate-300":110,"text-base text-red-500":111,"w-full h-full px-6 flex-row items-center bg-slate-950":112,"w-full h-full flex-row items-center":113};if(typeof globalThis.queueMicrotask!=="function")globalThis.queueMicrotask=(J)=>{Promise.resolve().then(J)};var b2="ui:styles",h2="ui:font.",N4="ui:img.",T4="ui:sprite.";function E2(){return globalThis.ui}function N2(J){if(J.__textures)return;for(let Q of h0(N4)){let $=vJ(Q),Z;if(J.uploadImgEntry)Z=J.uploadImgEntry($);else{let X=new DataView($.buffer,$.byteOffset,$.byteLength);Z=J.uploadTexture($.subarray(8),X.getUint16(0,!0),X.getUint16(2,!0),$[4])}if(Z>=0)X4(Q.slice(N4.length),Z)}}function T2(J){if(J.__sprites)return;for(let Q of h0(T4)){let $=vJ(Q),Z=new DataView($.buffer,$.byteOffset,$.byteLength),X=Z.getUint16(0,!0),W=Z.getUint16(2,!0),Y=$[4],F=Z.getUint16(6,!0),D=Z.getUint16(8,!0),R=Z.getUint16(10,!0),U=J.uploadTexture($.subarray(16),X,W,Y);if(U>=0)q4(Q.slice(T4.length),{handle:U,frames:F,cols:D,step:R})}}function v4(J){let Q=F4("view");return fJ(Q,"style",J,void 0),Q}var D0=null,R0=null;function v2(J,Q){if(!D0||!R0)return;fJ(D0,"style",{width:J,height:Q,overflow:c.Overflow.Hidden},void 0),fJ(R0,"style",{width:J,height:Q,posType:c.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000},void 0);let $=E();$.__viewport={w:J,h:Q}}function m2(J,Q={}){let $=c6(Q.ops);if(d6($),S8(X2),Q.styles)Z2(Q.styles);let Z=$.kind==="native"?$.ops.__textures:void 0;if($.kind==="native"){if(Z)for(let w in Z)X4(w,Z[w]);let H=$.ops.__sprites;if(H)for(let w in H)q4(w,H[w])}if($.kind==="injected"||Z===void 0){if(Q.pak)p1(Q.pak);if(G8()){for(let H of h0())if(H===b2)$.ops.loadStyles?.(vJ(H));else if(H.startsWith(h2))$.ops.loadFontAtlas?.(vJ(H))}}let X=b1($.ops),W=X?.w??x1,Y=X?.h??C1,F=v4({width:W,height:Y,overflow:c.Overflow.Hidden}),D=v4({width:W,height:Y,posType:c.PosType.Absolute,insetT:0,insetR:0,insetB:0,insetL:0,zIndex:1000,hitPass:1});GJ(YJ,F),GJ(YJ,D),B4(D),D0=F,R0=D,c1(F),n1(YJ),q8(),h4(),w2(),e6(),f2(),i8($.ops),i6(s8((H,w,x,g)=>{J8(),Q8(w),H2(x,g),C2(),x2(),k2(),z8(H),_8(H),Y4()}));let R=m8(J,F),U=s6(v2);return()=>{U(),F2(),h4(),R(),c1(null),n1(null),B4(null),D0=null,R0=null;for(let H of YJ.children.splice(0))H.parent=null,$.ops.destroyNode(H.id);Y4()}}function l2(J,Q={}){let $=Q.ops??E2();if(!$)throw Error("PocketJS: mount() requires globalThis.ui or opts.ops");if(Q.pak)p1(Q.pak);return N2($),T2($),m2(J,{ops:$,styles:Q.styles??g2,pak:Q.pak})}var m4="$b",u2=9007199254740991,p2=":memory:",CJ="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";function c2(J){let Q="";for(let $=0;$>2]+CJ[(Z&3)<<4|X>>4],Q+=$+1>6]:"=",Q+=$+2u2)throw Error("db: integer exceeds DB_MAX_SAFE_INTEGER");return J}function s2(J){if(J!==null&&typeof J==="object")return d2(J[m4]);return J}function a2(J){if(Array.isArray(J))return JSON.stringify(J.map(l4));let Q={};for(let[$,Z]of Object.entries(J))Q[$]=l4(Z);return JSON.stringify(Q)}class $1{cols=[];constructor(J,Q,$){this.ops=J,this.handle=Q,this.sql=$}get columnNames(){return this.cols}execute(J){let Q=this.ops.query(this.handle,this.sql,a2(J)),$=JSON.parse(Q);if($.error!==void 0)throw Error(`db: ${$.error}`);return this.cols=$.cols??[],$}get(...J){let Q=this.values(...J);if(Q.length===0)return null;let $={};return this.cols.forEach((Z,X)=>$[Z]=Q[0][X]),$}all(...J){return this.values(...J).map(($)=>{let Z={};return this.cols.forEach((X,W)=>Z[X]=$[W]),Z})}values(...J){let Q=u4(J);return(this.execute(Q).rows??[]).map((Z)=>Z.map(s2))}run(...J){let Q=this.execute(u4(J));return{changes:Q.changes??0,lastInsertRowid:Q.lastInsertRowid??0}}}function u4(J){if(J.length===1&&Array.isArray(J[0]))return J[0];if(J.length===1&&J[0]!==null&&typeof J[0]==="object"&&!(J[0]instanceof Uint8Array))return J[0];return J}class p4{statements=new Map;txDepth=0;constructor(J=p2){let Q=i2();if(!Q)throw Error("db: globalThis.db is not mounted — declare `data.sqlite` in pocket.json requires");let $=Q.open(J);if($<0)throw Error(`db: open(${JSON.stringify(J)}) refused`);this.ops=Q,this.handle=$}query(J){let Q=this.statements.get(J);if(!Q)Q=new $1(this.ops,this.handle,J),this.statements.set(J,Q);return Q}prepare(J){return new $1(this.ops,this.handle,J)}run(J,Q=[]){return this.query(J).run(Q)}exec(J){if(this.ops.exec(this.handle,J)!==0)throw Error(`db: ${this.ops.lastError(this.handle)}`)}transaction(J){return(...Q)=>{let $=`pocket_tx_${this.txDepth}`,[Z,X,W]=this.txDepth===0?["BEGIN","COMMIT","ROLLBACK"]:[`SAVEPOINT ${$}`,`RELEASE ${$}`,`ROLLBACK TO ${$}; RELEASE ${$}`];this.exec(Z),this.txDepth++;try{let Y=J(...Q);return this.txDepth--,this.exec(X),Y}catch(Y){throw this.txDepth--,this.exec(W),Y}}}close(){this.statements.clear(),this.ops.close(this.handle)}}var O0={appTitle:"text-2xl text-white font-bold",pageTitle:"text-2xl text-slate-950 font-bold",heading:"text-xl text-slate-900 font-bold",label:"text-base text-slate-600 font-bold",captionStrong:"text-base text-slate-500 font-bold"},c4={neutral:{surface:"px-3 py-2 rounded-lg bg-slate-100",text:"text-base text-slate-600 font-bold"},info:{surface:"px-3 py-2 rounded-lg bg-indigo-100",text:"text-base text-indigo-700 font-bold"},success:{surface:"px-3 py-2 rounded-lg bg-emerald-100",text:"text-base text-emerald-700 font-bold"},warning:{surface:"px-3 py-2 rounded-lg bg-amber-100",text:"text-base text-amber-700 font-bold"},danger:{surface:"px-3 py-2 rounded-lg bg-red-100",text:"text-base text-red-500 font-bold"}};function r2(J){let Q=()=>J.accent==="busy"?"w-[34] h-[34] rounded-lg bg-amber-400":J.accent==="danger"?"w-[34] h-[34] rounded-lg bg-red-500":J.accent==="none"?"w-[34] h-[34] rounded-lg bg-slate-800":"w-[34] h-[34] rounded-lg bg-emerald-500";return q(V,{class:"h-[112] px-6 flex-row items-center justify-between bg-slate-950",get children(){return[q(V,{class:"flex-row items-center gap-4",get children(){return[m(()=>m(()=>!!J.back)()?q(O,{class:"w-[34] text-2xl text-white font-bold",children:"‹"}):q(V,{get["class"](){return Q()}})),q(O,{get["class"](){return O0.appTitle},get children(){return J.title}})]}}),q(V,{class:"w-[332] flex-col items-end gap-2",get children(){return[q(O,{class:"text-base text-slate-300 font-bold",get children(){return J.metaTop??""}}),q(O,{class:"text-base text-slate-400",get children(){return J.metaBottom??""}})]}})]}})}function n2(J){let Q=()=>J.action?J.detail?J.detail+" · VIEW ALL ›":"VIEW ALL ›":J.detail??"";return q(V,{class:"h-[44] px-1 flex-row items-center justify-between",get children(){return[q(O,{get["class"](){return O0.heading},get children(){return J.title}}),q(O,{get["class"](){return O0.captionStrong},get children(){return Q()}})]}})}function o2(J){let Q=()=>J.disabled?"w-full h-full items-center justify-center rounded-xl bg-slate-200":J.tone==="danger"?"w-full h-full items-center justify-center rounded-xl bg-red-100":J.tone==="neutral"?"w-full h-full items-center justify-center rounded-xl bg-slate-100":"w-full h-full items-center justify-center rounded-xl bg-orange-600",$=()=>J.disabled?"text-lg text-slate-500 font-bold":J.tone==="danger"?"text-lg text-red-500 font-bold":J.tone==="neutral"?"text-lg text-slate-900 font-bold":"text-lg text-white font-bold";return q(V,{get["class"](){return Q()},get children(){return q(O,{get["class"](){return $()},get children(){return J.label}})}})}function d4(J){return q(V,{get["class"](){return J.compact?"w-full h-[150] px-5 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100":"w-full h-[430] px-12 flex-col items-center justify-center rounded-xl shadow bg-white border-slate-100"},get children(){return[m(()=>m(()=>!!J.icon)()?q(V,{get["class"](){return J.tone==="info"?"w-[88] h-[88] items-center justify-center rounded-xl bg-indigo-100":"w-[88] h-[88] items-center justify-center rounded-xl bg-slate-100"},get children(){return q(O,{get["class"](){return J.tone==="info"?"text-2xl text-indigo-700 font-bold":"text-2xl text-slate-600 font-bold"},get children(){return J.icon}})}}):null),q(O,{get["class"](){return J.icon?"pt-7 text-2xl text-slate-900 font-bold":"text-lg text-slate-500 font-bold"},get children(){return J.title}}),m(()=>m(()=>!!J.detail)()?q(O,{class:"pt-4 text-lg text-slate-500",get children(){return J.detail}}):null)]}})}function t2(J){let Q=()=>J.tone==="success"?"text-xl text-emerald-600 font-bold":J.tone==="danger"?"text-xl text-red-500 font-bold":"text-xl text-slate-900 font-bold";return q(V,{class:"w-full h-full px-5 py-4 flex-col gap-3 rounded-xl shadow bg-white border-slate-100",get children(){return[q(O,{get["class"](){return O0.label},get children(){return J.label}}),q(O,{get["class"](){return Q()},get children(){return J.value}})]}})}function e2(J){let Q=()=>J.dark?J.tone==="danger"?"text-base text-red-300":"text-base text-slate-300":J.tone==="danger"?"text-base text-red-500":"text-base text-slate-500";return q(V,{get["class"](){return J.dark?"w-full h-full px-6 flex-row items-center bg-slate-950":"w-full h-full flex-row items-center"},get children(){return q(O,{get["class"](){return Q()},get children(){return J.text}})}})}function JQ(J){return[q(V,{get["class"](){return J.top},get children(){return q(O,{class:"text-base text-orange-600 font-bold",children:"UP"})}}),q(V,{get["class"](){return J.bottom},get children(){return q(O,{class:"text-base text-orange-600 font-bold",children:"DN"})}})]}var QQ=5,BJ=new p4("robinhood"),$Q={number:"",label:"ACCOUNT",suffix:"",status:"WAITING FOR ROBINHOOD"},Z1={title:"NO RECENT ACTIVITY",timestamp:"",detail:"",state:"",amount:"",side:""},ZQ={...Z1,title:"ACTIVITY UNAVAILABLE"},XQ={...Z1,title:""},X1={symbol:"NO OPEN POSITIONS",quantity:"",averagePrice:"",marketValue:""},WQ={...X1,symbol:"POSITIONS UNAVAILABLE"},qQ={...X1,symbol:""},iJ={account:$Q,totalValue:null,cash:null,buyingPower:null,pnlDay:null,pnlWeek:null,positions:[],activity:[],positionsAvailable:!0,activityAvailable:!0,observedAt:null},[i,sJ]=h("dashboard"),[l,zQ]=h("day"),[gJ,W1]=h([]),[L0,I0]=h(""),[y,aJ]=h(iJ),[i4,OJ]=h([]),[YQ,LJ]=h([]),[q1,z1]=h([]),[GQ,Y1]=h({change:"$—",percent:"—",positive:!0}),[G1,s4]=h(0),[a4,r4]=h(0),[n4,o4]=h(0),[t4,ZJ]=h("WAITING FOR ROBINHOOD"),[rJ,e4]=h(!1),u=0,J6=-1,Q6=-1,$6=-1,H1=new Map,j1=new Map;function Z6(){return Math.floor(Date.now()/1000)}function HQ(J){try{return J?JSON.parse(J):null}catch{return null}}function jQ(){return BJ.query("SELECT account_number,label,suffix,status FROM accounts ORDER BY label,account_number LIMIT 16").all().map((Q)=>({number:Q.account_number,label:Q.label,suffix:Q.suffix,status:Q.status}))}function FQ(J,Q){if(Q6===J)return;let $=BJ.query("SELECT account_number,cash,buying_power,day_pnl,week_pnl,observed_at FROM portfolio_current LIMIT 16").all(),Z=BJ.query(` + SELECT value.account_number,value.value,value.observed_at + FROM total_value value + JOIN ( + SELECT account_number,MAX(observed_at) AS observed_at + FROM total_value GROUP BY account_number + ) latest ON latest.account_number=value.account_number AND latest.observed_at=value.observed_at + LIMIT 16 + `).all(),X=BJ.query("SELECT account_number,symbol,quantity,average_price,market_value FROM positions ORDER BY account_number,CAST(market_value AS REAL) DESC,symbol LIMIT 1024").all(),W=BJ.query("SELECT account_number,activity_id,occurred_at,symbol,side,quantity,price,amount,state,activity_type FROM activities ORDER BY account_number,occurred_at DESC,observed_at DESC LIMIT 1024").all(),Y=new Map($.map((U)=>[U.account_number,U])),F=new Map(Z.map((U)=>[U.account_number,U])),D=new Map,R=new Map;for(let U of X){let H=D.get(U.account_number)||[];if(H.length<64)H.push(U);D.set(U.account_number,H)}for(let U of W){let H=R.get(U.account_number)||[];if(H.length<64)H.push(U);R.set(U.account_number,H)}H1.clear();for(let U of Q){let H=Y.get(U.number),w=F.get(U.number),x=D.get(U.number)||[],g=R.get(U.number)||[];H1.set(U.number,{loadedRevision:J,value:{account:U,totalValue:w?.value??null,cash:H?.cash??null,buyingPower:H?.buying_power??null,pnlDay:H?.day_pnl??null,pnlWeek:H?.week_pnl??null,positions:x.map((A)=>({symbol:A.symbol,quantity:A.quantity||"—",averagePrice:A.average_price||"—",marketValue:A.market_value||""})),activity:g.map((A)=>({title:(A.side?A.side+" ":"")+(A.symbol||"ORDER"),timestamp:BQ(A.occurred_at),detail:A.quantity?A.quantity+" SH · "+(A.activity_type||"ORDER"):A.activity_type||"ORDER",state:A.state||"RECENT",amount:A.amount||A.price||A.quantity||"",side:A.side||""})),positionsAvailable:H!==void 0,activityAvailable:H!==void 0,observedAt:Math.max(H?.observed_at??0,w?.observed_at??0)||null}})}Q6=J}function k0(J){if(J===null)return null;let Q=Number(J.replace(/[$,%]/g,""));return Number.isFinite(Q)?Q:null}function BQ(J){if(!J)return"RECENT";let Q=new Date(J);if(!Number.isFinite(Q.getTime()))return J.slice(0,16).toUpperCase();return String(Q.getMonth()+1).padStart(2,"0")+"/"+String(Q.getDate()).padStart(2,"0")+" "+String(Q.getHours()).padStart(2,"0")+":"+String(Q.getMinutes()).padStart(2,"0")}function X6(J){let Q=Math.max(0,Z6()-J);if(Q<60)return"JUST NOW";if(Q<3600)return Math.floor(Q/60)+" MIN AGO";if(Q<86400)return Math.floor(Q/3600)+" HR AGO";return Math.floor(Q/86400)+" DAY AGO"}function e(J){if(!J)return"$—";let Q=k0(J);if(Q===null)return J;let Z=Math.abs(Q).toFixed(2).split("."),X="$"+Z[0].replace(/\B(?=(\d{3})+(?!\d))/g,",")+"."+Z[1];return Q<0?"-"+X:X}function W6(J,Q){let $=J+":"+l(),Z=j1.get($);if(Z?.loadedRevision===Q){OJ(Z.value.points),LJ(Z.value.segments),z1(Z.value.labels),Y1(Z.value.trend);return}let X=l()==="day"?86400:604800,Y=Z6()-X,D=BJ.query(` + WITH RECURSIVE buckets(bucket_index, bucket_time) AS ( + SELECT 0, ?1 + UNION ALL + SELECT bucket_index + 1, ?1 + CAST((?2 * (bucket_index + 1)) / 19 AS INTEGER) + FROM buckets WHERE bucket_index < 19 + ) + SELECT bucket_index, bucket_time, + (SELECT value FROM total_value + WHERE account_number = ?3 AND observed_at >= ?1 AND observed_at <= bucket_time + ORDER BY observed_at DESC LIMIT 1) AS total_value + FROM buckets ORDER BY bucket_index + `).all(Y,X,J).map((j)=>({time:j.bucket_time,value:k0(j.total_value)})),R=D.map((j)=>j.time),U=D.map((j)=>j.value).filter((j)=>j!==null),H=[R[0],R[9],R[19]].map((j)=>{let G=new Date(j*1000);return l()==="day"?String(G.getHours()).padStart(2,"0")+":"+String(G.getMinutes()).padStart(2,"0"):String(G.getMonth()+1)+"/"+String(G.getDate())});if(U.length===0){let j=l()==="day"?y().pnlDay:y().pnlWeek,G=k0(j),P={points:[],segments:[],labels:H,trend:{change:e(j),percent:"—",positive:G===null||G>=0}};j1.set($,{loadedRevision:Q,value:P}),OJ(P.points),LJ(P.segments),z1(P.labels),Y1(P.trend);return}let w=Math.min(...U),x=Math.max(...U),g=Math.max(0.01,x-w),A=D.flatMap((j,G)=>j.value===null?[]:[{x:G*622/19,y:U.length===1?80:10+(x-j.value)*140/g}]),s=A.slice(1).map((j,G)=>{let P=A[G],k=j.x-P.x,_=j.y-P.y;return{x:P.x,y:P.y,width:Math.sqrt(k*k+_*_),angle:Math.atan2(_,k)*180/Math.PI}}),a=U[U.length-1]-U[0],K={change:e(String(a)),percent:U[0]===0?"—":(a*100/U[0]).toFixed(2)+"%",positive:a>=0};j1.set($,{loadedRevision:Q,value:{points:A,segments:s,labels:H,trend:K}}),OJ(A),LJ(s),z1(H),Y1(K)}function KQ(J){if($6===J)return;let Q=BJ.query("SELECT status,error,completed_at FROM refresh_runs ORDER BY id DESC LIMIT 1").get();if(e4(!1),Q?.status==="failed")ZJ("REFRESH FAILED · "+String(Q.error||"UNKNOWN ERROR").slice(0,52));else if(Q?.status==="partial")ZJ("LIVE WITH PARTIAL DATA");else if(y().observedAt)ZJ("LIVE · "+X6(y().observedAt));else ZJ("WAITING FOR ROBINHOOD");$6=J}function q6(J,Q){if(!J){if(aJ(iJ),OJ([]),LJ([]),!rJ())ZJ("WAITING FOR ROBINHOOD");return}let $=H1.get(J);if($?.loadedRevision===Q){if(aJ($.value),W6(J,Q),$.value.observedAt&&!rJ())ZJ("LIVE · "+X6($.value.observedAt));return}aJ(iJ),OJ([]),LJ([])}function z6(J=u){try{let Q=BJ.query("PRAGMA user_version").get();if(Number(Q?.user_version??0)!==QQ){W1([]),I0(""),aJ(iJ),OJ([]),LJ([]),ZJ("WAITING FOR ROBINHOOD");return}eJ(()=>{u=Math.max(u,J);let $=gJ();if(J6!==u)$=jQ(),W1($),J6=u;FQ(u,$);let Z=L0();if(!$.some((X)=>X.number===Z))Z=$[0]?.number||"",I0(Z);q6(Z,u),KQ(u)})}catch{eJ(()=>{W1([]),I0(""),aJ(iJ),OJ([]),LJ([]),ZJ("WAITING FOR ROBINHOOD")})}}function UQ(){return""}function F1(){return GQ()}function _0(J){return q(r2,{get title(){return J.title},back:!0,metaTop:"AGENTIC",get metaBottom(){return J.metaBottom??"AUTO · 5 MIN"}})}function B1(J){return q(V,{class:"w-[210] h-[104]",get children(){return q(t2,{get label(){return J.label},get value(){return e(J.value)}})}})}function Y6(J){return q(n2,{get title(){return J.title},get detail(){return J.detail},action:!0})}function MQ(J){if(J===0&&!y().activityAvailable)return ZQ;return y().activity[J]??(J===0?Z1:XQ)}function G6(J){let Q=()=>MQ(J.index);return q(V,{class:"h-[72] px-5 flex-row items-center justify-between",get children(){return[q(V,{class:"w-[450] flex-col gap-2",get children(){return[q(O,{class:"text-lg text-slate-900 font-bold",get children(){return Q().title}}),q(O,{class:"text-base text-slate-500",get children(){return m(()=>!!Q().timestamp)()?Q().timestamp+" · "+Q().detail:""}})]}}),q(O,{get["class"](){return Q().side==="SELL"?"text-base text-emerald-600 font-bold":"text-base text-slate-900 font-bold"},get children(){return m(()=>!!Q().amount)()?e(Q().amount):""}})]}})}function PQ(J){if(J===0&&!y().positionsAvailable)return WQ;return y().positions[J]??(J===0?X1:qQ)}function H6(J){let Q=()=>PQ(J.index);return q(V,{class:"h-[64] px-5 flex-row items-center",get children(){return[q(O,{class:"w-[240] text-lg text-slate-900 font-bold",get children(){return Q().symbol}}),q(O,{class:"w-[156] text-base text-slate-500",get children(){return m(()=>!!Q().quantity)()?Q().quantity+" SH":""}}),q(O,{class:"text-base text-slate-500",get children(){return m(()=>!!Q().averagePrice)()?"AVG "+e(Q().averagePrice):""}})]}})}function VQ(){return q(V,{class:"relative w-[632] h-[196] flex-col",get children(){return[q(V,{class:"relative w-[632] h-[160] overflow-hidden",get children(){return[q(V,{class:"absolute w-[632] h-[2] bg-slate-200",style:{posType:1,insetL:0,insetT:158}}),q(O,{class:"absolute text-base text-slate-500 font-bold",style:{posType:1,insetL:152,insetT:72},get children(){return i4().length<2?"COLLECTING 5M VALUE HISTORY":""}}),q(NJ,{get each(){return YQ()},children:(J)=>q(V,{get["class"](){return F1().positive?"absolute rounded-lg bg-emerald-500":"absolute rounded-lg bg-red-500"},get style(){return{posType:1,insetL:J.x,insetT:J.y-1,width:J.width,height:2,rotate:J.angle,originX:-0.5,originY:0}}})}),q(NJ,{get each(){return i4()},children:(J)=>q(V,{get["class"](){return F1().positive?"absolute w-[6] h-[6] rounded-lg bg-emerald-500":"absolute w-[6] h-[6] rounded-lg bg-red-500"},get style(){return{posType:1,insetL:J.x-3,insetT:J.y-3}}})})]}}),q(V,{class:"h-[36] px-1 flex-row items-center justify-between",get children(){return[q(O,{class:"text-base text-slate-500",get children(){return q1()[0]||""}}),q(O,{class:"text-base text-slate-500",get children(){return q1()[1]||""}}),q(O,{class:"text-base text-slate-500",get children(){return q1()[2]||""}})]}})]}})}function DQ(){let J=()=>F1();return q(V,{class:"flex-col w-full h-full bg-slate-50",get children(){return[q(_0,{title:"ROBINHOOD"}),q(V,{class:"h-[64] px-6 pt-2",get children(){return q(V,{class:"w-full h-[56] px-5 flex-row items-center justify-between rounded-xl shadow bg-white border-slate-100",get children(){return[q(O,{class:"text-base text-slate-500 font-bold",children:"ACCOUNT"}),q(O,{class:"text-base text-slate-900 font-bold",get children(){return y().account.label+(y().account.suffix?" ····"+y().account.suffix+" "+(gJ().findIndex((Q)=>Q.number===L0())+1)+"/"+gJ().length:"")+" ›"}})]}})}}),q(V,{class:"h-[304] px-6 pt-3",get children(){return q(V,{class:"h-[291] px-5 pt-4 flex-col rounded-xl shadow bg-white border-slate-100",get children(){return[q(V,{class:"h-[64] flex-row items-end justify-between",get children(){return[q(O,{class:"text-2xl text-slate-950 font-bold",get children(){return e(y().totalValue)}}),q(O,{get["class"](){return J().positive?"text-lg text-emerald-600 font-bold":"text-lg text-red-500 font-bold"},get children(){return J().change+" ("+J().percent+")"}})]}}),q(VQ,{})]}})}}),q(V,{class:"h-[60] px-6 pt-2 flex-row gap-3",get children(){return[q(V,{get["class"](){return l()==="day"?"w-[100] h-[44] items-center justify-center rounded-lg bg-orange-600":"w-[100] h-[44] items-center justify-center rounded-lg bg-white"},get children(){return q(O,{get["class"](){return l()==="day"?"text-base text-white font-bold":"text-base text-slate-500 font-bold"},children:"1D"})}}),q(V,{get["class"](){return l()==="week"?"w-[100] h-[44] items-center justify-center rounded-lg bg-orange-600":"w-[100] h-[44] items-center justify-center rounded-lg bg-white"},get children(){return q(O,{get["class"](){return l()==="week"?"text-base text-white font-bold":"text-base text-slate-500 font-bold"},children:"1W"})}}),q(O,{class:"pt-3 text-base text-slate-500",get children(){return l()==="day"?"TODAY":"PAST WEEK"}})]}}),q(V,{class:"h-[126] px-6 pt-3 flex-row items-start gap-[21]",get children(){return[q(B1,{label:"VALUE",get value(){return y().totalValue}}),q(B1,{label:"CASH",get value(){return y().cash}}),q(B1,{label:"BUY POWER",get value(){return y().buyingPower}})]}}),q(V,{class:"h-[200] px-6 flex-col",get children(){return[q(Y6,{title:"ACTIVITY",detail:"LAST 7 DAYS"}),q(V,{class:"h-[150] flex-col rounded-xl shadow bg-white border-slate-100",get children(){return[q(G6,{index:0}),q(G6,{index:1})]}})]}}),q(V,{class:"h-[184] px-6 flex-col",get children(){return[q(Y6,{title:"POSITIONS"}),q(V,{class:"h-[136] flex-col rounded-xl shadow bg-white border-slate-100",get children(){return[q(H6,{index:0}),q(H6,{index:1})]}})]}}),q(V,{class:"h-[126] px-6 pt-2",get children(){return q(V,{class:"w-full h-[110] px-5 flex-row items-center justify-between rounded-xl shadow bg-white border-slate-100",get children(){return[q(V,{class:"flex-col gap-3",get children(){return[q(O,{class:"text-xl text-slate-900 font-bold",children:"REALIZED P&L"}),q(O,{class:"text-base text-slate-500",get children(){return"EQUITIES / "+(l()==="day"?"TODAY":"WEEK")}})]}}),q(O,{get["class"](){return(k0(l()==="day"?y().pnlDay:y().pnlWeek)??0)>=0?"text-2xl text-emerald-600 font-bold":"text-2xl text-red-500 font-bold"},get children(){return e(l()==="day"?y().pnlDay:y().pnlWeek)}})]}})}}),q(V,{class:"h-[104] px-6 flex-row items-center justify-between",get children(){return[q(V,{class:"w-[460] h-[64]",get children(){return q(e2,{get text(){return t4()},get tone(){return t4().startsWith("REFRESH FAILED")?"danger":"neutral"}})}}),q(V,{class:"w-[176] h-[64]",get children(){return q(o2,{get label(){return rJ()?"REFRESHING":"REFRESH NOW"},get disabled(){return rJ()}})}})]}})]}})}function K1(){return q(JQ,{top:"absolute left-[628] top-[156] w-[68] h-[132] items-center justify-center rounded-xl bg-orange-100",bottom:"absolute left-[628] top-[972] w-[68] h-[132] items-center justify-center rounded-xl bg-orange-100"})}function RQ(){let J=QJ(()=>gJ().slice(G1(),G1()+8)),Q=L0();return q(V,{class:"relative flex-col w-full h-full bg-slate-50",get children(){return[q(_0,{title:"ACCOUNTS"}),q(V,{class:"px-6 pt-[14] flex-col gap-[12]",get children(){return q(NJ,{get each(){return J()},children:($)=>q(V,{get["class"](){return $.number===Q?"w-[584] h-[100] px-5 flex-col justify-center gap-3 rounded-xl shadow bg-emerald-100 border-emerald-500":"w-[584] h-[100] px-5 flex-col justify-center gap-3 rounded-xl shadow bg-white border-slate-100"},get children(){return[q(V,{class:"flex-row items-center justify-between",get children(){return[q(O,{class:"text-xl text-slate-900 font-bold",get children(){return $.label}}),q(O,{class:"text-lg text-slate-500 font-bold",get children(){return"····"+$.suffix}})]}}),q(V,{class:"flex-row items-center justify-between",get children(){return[q(V,{get["class"](){return c4.success.surface},get children(){return q(O,{get["class"](){return c4.success.text},get children(){return $.status}})}}),q(Z0,{get when(){return $.number===Q},get children(){return q(O,{class:"text-base text-emerald-600 font-bold",children:"SELECTED"})}})]}})]}})})}}),q(K1,{})]}})}function OQ(){let J=QJ(()=>y().activity.slice(a4(),a4()+8));return q(V,{class:"relative flex-col w-full h-full bg-slate-50",get children(){return[q(_0,{title:"ACTIVITY",metaBottom:"LAST 7 DAYS"}),q(V,{class:"px-6 pt-[14] flex-col gap-[14]",get children(){return q(Z0,{get when(){return m(()=>!!y().activityAvailable)()&&J().length>0},get fallback(){return q(d4,{get title(){return y().activityAvailable?"NO ACTIVITY YET":"ACTIVITY UNAVAILABLE"},compact:!0})},get children(){return q(NJ,{get each(){return J()},children:(Q)=>q(V,{class:"w-[584] h-[112] px-5 flex-col justify-center gap-2 rounded-xl shadow bg-white border-slate-100",get children(){return[q(V,{class:"flex-row items-center justify-between",get children(){return[q(O,{class:"text-lg text-slate-900 font-bold",get children(){return Q.title}}),q(O,{get["class"](){return Q.side==="SELL"?"text-lg text-emerald-600 font-bold":"text-lg text-slate-900 font-bold"},get children(){return e(Q.amount)}})]}}),q(O,{class:"text-base text-slate-500",get children(){return Q.timestamp+" · "+Q.detail}}),q(O,{class:"text-base text-emerald-600 font-bold",get children(){return Q.state}})]}})})}})}}),q(K1,{})]}})}function LQ(){let J=QJ(()=>y().positions.slice(n4(),n4()+9));return q(V,{class:"relative flex-col w-full h-full bg-slate-50",get children(){return[q(_0,{title:"POSITIONS"}),q(V,{class:"px-6 pt-[14] flex-col gap-[12]",get children(){return q(Z0,{get when(){return m(()=>!!y().positionsAvailable)()&&J().length>0},get fallback(){return q(d4,{get title(){return y().positionsAvailable?"NO OPEN POSITIONS":"POSITIONS UNAVAILABLE"},compact:!0})},get children(){return q(NJ,{get each(){return J()},children:(Q)=>q(V,{class:"w-[584] h-[98] px-5 flex-col justify-center gap-3 rounded-xl shadow bg-white border-slate-100",get children(){return[q(V,{class:"flex-row items-center",get children(){return[q(O,{class:"w-[170] text-xl text-slate-900 font-bold",get children(){return Q.symbol}}),q(O,{class:"text-lg text-slate-900 font-bold",get children(){return Q.quantity+" SH"}})]}}),q(O,{class:"text-base text-slate-500",get children(){return"AVERAGE COST "+e(Q.averagePrice)+(Q.marketValue?" · VALUE "+e(Q.marketValue):"")}})]}})})}})}}),q(K1,{})]}})}function IQ(){if(i()==="accounts")return q(RQ,{});if(i()==="activity")return q(OQ,{});return q(LQ,{})}function kQ(){return q(Z0,{get when(){return i()==="dashboard"},get fallback(){return q(IQ,{})},get children(){return q(DQ,{})}})}z6(),l2(()=>q(kQ,{})),globalThis.PocketPiApp={tick:UQ,dataChanged(J){let Q=HQ(J),$=Array.isArray(Q)?Q.reduce((Z,X)=>Math.max(Z,Number(X?.revision??0)),u):u;return z6($),""},tap(J,Q){if(i()!=="dashboard"){if(Q<112&&J<220)return sJ("dashboard"),"";if(J>=620&&Q>=140&&Q<310){if(i()==="accounts")s4(($)=>Math.max(0,$-1));if(i()==="activity")r4(($)=>Math.max(0,$-1));if(i()==="positions")o4(($)=>Math.max(0,$-1));return""}if(J>=620&&Q>=940&&Q<1130){if(i()==="accounts")s4(($)=>Math.min(Math.max(0,gJ().length-8),$+1));if(i()==="activity")r4(($)=>Math.min(Math.max(0,y().activity.length-8),$+1));if(i()==="positions")o4(($)=>Math.min(Math.max(0,y().positions.length-9),$+1));return""}if(i()==="accounts"&&J<610&&Q>=126){let $=G1()+Math.floor((Q-126)/112),Z=gJ()[$];if(Z)eJ(()=>{I0(Z.number),sJ("dashboard")}),q6(Z.number,u)}return""}if(Q<112&&J<100)return JSON.stringify({type:"navigate",app:"pi-agent"});if(Q>=112&&Q<176)return sJ("accounts"),"";if(Q>=480&&Q<540)return eJ(()=>{zQ(J<136?"day":"week"),W6(L0(),u)}),"";if(Q>=666&&Q<866)return sJ("activity"),"";if(Q>=866&&Q<1050)return sJ("positions"),"";if(Q>=1176&&J>=500){if(rJ())return"";return e4(!0),ZJ("REFRESHING ROBINHOOD…"),JSON.stringify({type:"invokeTask",task:"refreshPortfolio"})}return""}}})(); diff --git a/apps/robinhood/dist/app.pak b/apps/robinhood/dist/app.pak new file mode 100644 index 0000000..a715b60 Binary files /dev/null and b/apps/robinhood/dist/app.pak differ diff --git a/apps/robinhood/dist/data-action.js b/apps/robinhood/dist/data-action.js new file mode 100644 index 0000000..5421c3d --- /dev/null +++ b/apps/robinhood/dist/data-action.js @@ -0,0 +1,246 @@ +(()=>{var w={source:"https://agent.robinhood.com/mcp/trading",protocolVersion:"2025-06-18",retrievedAt:"2026-08-10T09:05:39+00:00",tools:[{name:"add_option_to_watchlist",namespace:"option_market_data",description:"Add option contracts to the user's options watchlist. Works for both equity options (AAPL, NVDA) and index options (SPX, NDX, RUT). Source option_ids from get_option_instruments. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{option_ids:{type:["null","array"],items:{type:"string"},description:"Option contract UUIDs to add. Each becomes a single-leg position on the user's options watchlist. Source from get_option_instruments."},position_type:{type:"string",description:'"long" (default) or "short". Applies to every option_id in this call. For mixed long/short adds, issue two calls.'}},required:["option_ids"],additionalProperties:!1}},{name:"add_to_watchlist",namespace:"watchlists",description:"Add items to a watchlist. Exactly one of symbols (stocks/ETFs), currency_pair_ids (crypto), or index_ids (market indexes like SPX, NDX) is required — mutually exclusive. For options use add_option_to_watchlist (separate dedicated watchlist). Futures still require the Robinhood app. Already-present items are no-ops. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{list_id:{type:"string",description:"UUID of the watchlist to add items to."},symbols:{type:["null","array"],items:{type:"string"},description:"Stock symbols to add (e.g. ['AAPL', 'NVDA']). US stocks and ETFs only. Mutually exclusive with currency_pair_ids and index_ids."},currency_pair_ids:{type:["null","array"],items:{type:"string"},description:"Currency-pair UUIDs to add, available as object_id from get_watchlist_items entries where object_type=currency_pair. Mutually exclusive with symbols and index_ids."},index_ids:{type:["null","array"],items:{type:"string"},description:"Market-index UUIDs to add (the id field from get_indexes; SPX, NDX, DJI, etc.). Mutually exclusive with symbols and currency_pair_ids."}},required:["list_id"],additionalProperties:!1}},{name:"cancel_equity_order",namespace:"equity_trading",description:"Cancel an open equity order by order_id. Resolve order_id via get_equity_orders and pass the same account_number. Requires an agentic_allowed=true account; non-agentic accounts are rejected. Cancellation may be rejected if the order has already filled, was already cancelled, or is otherwise ineligible. After the provider action succeeds, Pocket Pi writes the returned equity-order state into the activities View projection in robinhood.sqlite. Portfolio and positions converge on the next normal refresh.",inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account that owns the order, from get_accounts. Must be agentic_allowed=true. The upstream rejects mismatches against the order's owning account."},order_id:{type:"string",description:"Order UUID from get_equity_orders. Must live in account_number."}},required:["account_number","order_id"],additionalProperties:!1}},{name:"cancel_option_exercise",namespace:"option_trading",description:"Cancel all queued exercise requests for an option position. Pass the same account_number and option_id used for exercise_option. Internally looks up all queued exercise events for that option and cancels each one. Typically there is one; multiple means separate exercise batches were submitted. Only events in state=queued can be cancelled; events already processing are rejected by the broker. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account that owns the exercise. Must be agentic_allowed=true."},option_id:{type:"string",description:"Option instrument UUID — the same option_id used for exercise_option. The tool looks up the queued exercise for this option and cancels it."}},required:["account_number","option_id"],additionalProperties:!1}},{name:"cancel_option_order",namespace:"option_trading",description:"Cancel an open option order by account_number + order_id. Resolve order_id via get_option_orders and pass the same account_number. Requires an agentic_allowed=true account; non-agentic accounts are rejected. Cancellation may be rejected if the order has already filled, was already cancelled, or is otherwise ineligible. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account that owns the order, from get_accounts. Must be agentic_allowed=true. Mismatches against the order's owning account are rejected."},order_id:{type:"string",description:"Order UUID from get_option_orders. Must live in account_number."}},required:["account_number","order_id"],additionalProperties:!1}},{name:"create_scan",namespace:"scanners",description:`Create a new saved scanner (screener) on the user's account, optionally applying a preset and custom filters in a single call. Returns the new scan's id, title, applied filters, and the initial live market results. + +This tool composes multiple Beacon operations: + 1. Create an empty scan + 2. If a non-INITIAL preset was requested: apply that preset configuration (DAILY_GAINERS, DAILY_LOSERS, HIGH_OPTIONS_VOLUME_IV, UPCOMING_EARNINGS) + 3. If custom filters were provided: apply them (replaces any preset filters) + 4. If a custom title was provided: set the title + +If any step after the initial create fails, the scan still exists with the partial state — the response surfaces what was applied. Use update_scan_filters / update_scan_config to fix anything that failed. + +Parameters: +- preset (optional) — starting preset. Default: DAILY_GAINERS when no filters supplied, INITIAL when filters supplied. Valid values: INITIAL, DAILY_GAINERS, DAILY_LOSERS, HIGH_OPTIONS_VOLUME_IV, UPCOMING_EARNINGS. +- filters (optional) — array of custom filter specs. Call get_scanner_filter_specs to discover valid filter_type / predicate values. +- title (optional) — custom human-readable name for the scan. + +Example: to make "stocks with RSI > 70 and volume > 1M, sorted by volume desc", call with + preset = "INITIAL" + filters = [ + {"filter_type": "FILTER_TYPE_RSI", "predicate": ">", "values": ["70"], "interval": "1d", "length": 14}, + {"filter_type": "FILTER_TYPE_VOLUME", "predicate": ">", "values": ["1000000"], "interval": "1d"} + ] + title = "High RSI + High Volume" This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{preset:{type:"string",description:"Starting preset for the new scan. One of: INITIAL (no preset; only valid if filters are provided), DAILY_GAINERS, DAILY_LOSERS, HIGH_OPTIONS_VOLUME_IV, UPCOMING_EARNINGS. Defaults to DAILY_GAINERS when no filters are provided, INITIAL when filters are provided."},filters:{type:["null","array"],items:{type:"object",properties:{filter_type:{type:"string",description:'Wire-format enum name, e.g. "FILTER_TYPE_RSI". See the scanner-filter-specs resource for valid values. Omit when supplying expression.'},predicate:{type:"string",description:'Wire-format enum name, e.g. "PREDICATE_GREATER_THAN". See the scanner-filter-specs resource for the predicates supported by each filter.'},values:{type:["null","array"],items:{type:"string"},description:'Threshold values. Single-element for unary predicates, two-element for BETWEEN, multi-element for IN_LIST/ANY_OF. For a boolean expression screen, exactly ["True"].'},interval:{type:"string",description:`Time granularity for time-series filters (e.g. "1d"). Use one of the supported_intervals from the filter's scanner-filter-specs entry. Not used with expression — encode granularity inside the expression.`},length:{type:"integer",description:"Lookback length for filters that need one (e.g. RSI period of 14). Use one of the supported_lengths from the filter's scanner-filter-specs entry. Not used with expression.",minimum:-2147483648,maximum:2147483647},plot:{type:"string",description:`Plot / price-field input for filters that have one (e.g. "open" or "close" for % Change). Use one of the supported_plots from the filter's scanner-filter-specs entry. Not used with expression.`},expression:{type:"string",description:'Raw market-data expression to screen on, e.g. "dayVolume / volumeAvg(candleCount=30, candlePeriod=\\"1d\\", session=\\"all\\")" with a numeric predicate, or a whole comparison like "tradeAllDay.price > closeAvg(candleCount=50, candlePeriod=\\"1d\\", session=\\"all\\")" with predicate "=" and values ["True"]. Only preview_scan accepts expressions. Omit filter_type when set. Prefer an enum filter_type whenever one covers the request.'},display_title:{type:"string",description:'Optional short label for an expression filter, shown as the results-column header (e.g. "Relative volume (30D)"). Only used with expression.'}},required:["predicate","values"],additionalProperties:!1},description:"Custom filters to apply after the preset. Each filter has filter_type (FILTER_TYPE_... enum), predicate (>, <, =, BETWEEN, etc.), values, optional interval (e.g. 1d), and optional length (e.g. 14 for RSI). Call get_scanner_filter_specs first for valid filter_type / predicate combinations."},title:{type:"string",description:"Optional custom title for the saved scan. If omitted, Beacon assigns a default title based on the preset."}},additionalProperties:!1}},{name:"create_watchlist",namespace:"watchlists",description:"Create a new custom watchlist. Do not use this to follow a Robinhood-curated list; use follow_watchlist instead. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{display_name:{type:"string",description:"Name for the new watchlist (e.g. 'Tech Stocks'). Must be unique among the user's watchlists."},icon_emoji:{type:"string",description:"Emoji shown next to the name (one character)."},display_description:{type:"string",description:"Short description shown under the name."}},required:["display_name"],additionalProperties:!1}},{name:"exercise_option",namespace:"option_trading",description:`Exercise a long options position. A call exercises the right to buy the underlying shares at the strike price; a put exercises the right to sell. Exercise is irrevocable once state moves past queued. + +Position requirements: get_option_positions must show type=long and quantity > 0. + +Account requirements: get_accounts must show agentic_allowed=true and option_level_2 or option_level_3. Non-agentic and option_level_0 accounts are rejected. + +Index options cannot be manually exercised. Exercises submitted during market hours execute the same day; requests submitted after market close, including late-close trading days, are queued for overnight processing. + +Parameter rules: +- quantity must be a positive integer no greater than the available contracts. +- allow_shorts=true applies only to put exercises when the account lacks enough shares to deliver; it creates a short equity position in the underlying stock. +- reason is an optional explanation for the exercise. +- ref_id must reuse the same UUID when retrying the same logical exercise and use a new UUID for a new exercise. + +Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number. Must be agentic_allowed=true with option_level_2 or option_level_3."},option_id:{type:"string",description:"Option instrument UUID from get_option_positions or get_option_instruments. The position must be type=long."},quantity:{type:"integer",description:"Number of contracts to exercise (positive integer, minimum 1)."},ref_id:{type:"string",description:"Idempotency key (UUID). Generate once per logical exercise and re-send on retry. Omitting falls back to a server-generated key."},reason:{type:"string",description:"Optional exercise reason: covering_early_assignment | buying_stocks | not_enough_liquidity_or_spread_too_wide | hedging_position."},allow_shorts:{type:"boolean",description:"When true, allows a put exercise to proceed when the account does not own enough shares to deliver, creating a short equity position in the underlying stock. Default false."}},required:["account_number","option_id","quantity"],additionalProperties:!1}},{name:"follow_watchlist",namespace:"watchlists",description:"Follow a Robinhood-curated list so it appears in the user's watchlists. Use only for curated lists; custom lists are already owned by the user. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{list_id:{type:"string",description:"UUID of the Robinhood-curated list to follow. Obtain from get_popular_watchlists."}},required:["list_id"],additionalProperties:!1}},{name:"get_accounts",namespace:"account_portfolio",description:"List the user's brokerage accounts and return the account_number and capabilities needed by other tools. Does not return reliable buying power; use get_portfolio for buying power. Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.",inputSchema:{type:"object",additionalProperties:!1}},{name:"get_earnings_calendar",namespace:"equity_market_data",description:'List earnings reports scheduled across the market over a date window (up to 31 days), optionally limited to high-market-cap names. Returns one entry per report event — estimated/actual EPS, report date and timing (am/pm), and company-verification status. Use this for market-wide discovery ("what large-caps report this week?"). For a specific known ticker, use get_earnings_results instead. Read-only. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.',inputSchema:{type:"object",properties:{start_date:{type:"string",description:"Window anchor, YYYY-MM-DD. Defaults to today (US/Eastern) when omitted."},days:{type:"integer",description:"Window length in days, measured from start_date. Defaults to 7. Positive = forward window (e.g. 7 = the next 7 days, inclusive of start_date); negative = look-back window (e.g. -7 = the 7 days ending at start_date). Must be a non-zero value between -31 and 31 — windows wider than 31 days are rejected."},filter:{type:"string",description:"Optional result filter. Set to 'high_market_cap' to limit the calendar to high-market-cap names (market cap over $1B) — useful for 'what large-caps report this week' style questions. Omit for all names."}},additionalProperties:!1}},{name:"get_earnings_results",namespace:"equity_market_data",description:'Get recent and upcoming earnings for ONE equity symbol — estimated/actual EPS, report date and timing (am/pm), and company-verification status. Returns the trailing up to 8 quarters. Use this for earnings-timing questions ("does AAPL report this week?"), EPS surprise analysis, and screening for upcoming earnings risk on a specific stock. For market-wide earnings calendar queries across many symbols, use get_earnings_calendar. Read-only. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.',inputSchema:{type:"object",properties:{symbol:{type:"string",description:"Stock symbol to look up (one symbol per call). Exact-ticker match — no name or partial-ticker resolution. Lowercase and whitespace-padded input is normalized to uppercase-trimmed before forwarding. Returns the trailing up to 8 quarters of earnings for the symbol."}},required:["symbol"],additionalProperties:!1}},{name:"get_equity_fundamentals",namespace:"equity_market_data",description:"Get today's fundamentals for one or more stock symbols — valuation ratios (PE, P/B), capitalization (market cap, shares outstanding, float), today's session OHLCV, trailing volume averages, 52-week range, dividend schedule, and company profile. For real-time quotes use get_equity_quotes; for time-series price history use get_equity_historicals. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{symbols:{type:["null","array"],items:{type:"string"},description:"One or more stock symbols (max 10 per call). Exact-ticker match — no name or partial-ticker resolution. Lowercase and whitespace-padded input is normalized to uppercase-trimmed before forwarding."},bounds:{type:"string",description:"Trading session the day-level fields (open / high / low / volume / overnight_volume) are drawn from. One of 'regular' (regular trading hours), 'trading' (regular + post-market), 'extended' (pre-market + regular + post-market), '24_5' (24-hour, 5-day-trading-week). Does not affect valuation fields. overnight_volume only populates when bounds=24_5. Defaults to 'regular' when omitted."}},required:["symbols"],additionalProperties:!1}},{name:"get_equity_historicals",namespace:"equity_market_data",description:`Get OHLCV bars for one or more equity symbols across an explicit time range. Use this for charting, "recent activity" questions, and backtesting. The server auto-selects an interval when one is not provided. If the bar's interpolated field is true, bar was synthesized to fill a gap and carry no new information. + +Parameter rules: +- interval is optional; when omitted, the server auto-selects an interval that targets ~2,500 bars across the requested range. Provide an explicit interval only when you need a specific granularity. +- interval values are fixed; the server does NOT aggregate intermediate bars. For a custom interval (e.g. 3-minute), request the next-finer fixed interval and aggregate client-side. +- bounds defaults to 'regular' (RTH only); use 'extended' for pre-market through post-market data or '24_5' for overnight data. +- adjustment_type defaults to 'split' (split-adjusted, the right default for backtesting). Use 'none' for raw prices, 'all' for split + dividend adjustment. +- If the range would produce more bars than the upstream allows at the explicitly requested interval, narrow the range or coarsen the interval — the call is rejected before reaching upstream. The cap does not apply when interval is auto-selected. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{symbols:{type:["null","array"],items:{type:"string"},description:"One or more stock symbols (uppercase). Up to 10 per call."},start_time:{type:"string",description:"Start of the range (RFC3339 UTC, e.g. '2026-01-01T00:00:00Z'). Required."},end_time:{type:"string",description:"End of the range (RFC3339 UTC). Optional — when omitted, defaults to the current time."},interval:{type:"string",description:"Bar interval. Optional — when omitted, the server picks an interval that targets ~2,500 bars across the requested range. Intraday: 15second, 30second, minute, 5minute, 10minute, 30minute, hour, 4hour. Interday: day, week, month, 3month, 6month, year, 5year, 10year, 20year, 50year. Note: the 1-minute bar is named 'minute' (not '1minute')."},bounds:{type:"string",description:"Session bounds. One of 'regular' (RTH, default), 'extended', 'trading', '24_5', '24_7', 'hyper_trading'."},adjustment_type:{type:"string",description:"Corporate-action adjustment: 'none' (raw prices), 'split' (default; right for backtesting), or 'all' (split + dividend; intraday only)."}},required:["symbols","start_time"],additionalProperties:!1}},{name:"get_equity_orders",namespace:"equity_trading",description:`Fetch equity orders for an account — list mode (newest first; open and closed, including fills, cancellations, rejections) or single-order mode by passing order_id. When the user asks for "orders" without specifying an asset class, call get_equity_orders and get_option_orders in parallel. + +Filtering tips: +- Prefer narrow queries: combine state, symbol, and/or created_at_gte for specific questions (e.g. "my filled AAPL orders this week") — the per-page cap is fixed. +- created_at_gte: interpret relative times in the user's timezone, convert to UTC before sending. +- symbol forces a symbol→instrument lookup; omit it if you don't need it. Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts."},order_id:{type:"string",description:"Filter to a single order by UUID. The response shape is unchanged (orders[] with at most one entry); empty when the order does not belong to account_number."},state:{type:"string",description:"Filter by single state: new, queued, confirmed, unconfirmed, partially_filled, filled, cancelled, rejected, failed, voided."},symbol:{type:"string",description:"Filter to one symbol (triggers a symbol→instrument lookup before the orders call)."},created_at_gte:{type:"string",description:"Lower bound (inclusive). ISO 8601 UTC or YYYY-MM-DD; naive values are interpreted as UTC."},placed_agent:{type:"string",description:"Filter to one source: 'user', 'agentic' (MCP), 'recurring', 'drip', etc."},cursor:{type:"string",description:"Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL."}},required:["account_number"],additionalProperties:!1}},{name:"get_equity_positions",namespace:"equity_trading",description:"List open equity positions for a specific brokerage account. Returns symbol, quantity, average cost, and per-position hold breakdowns. Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.",inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts."},cursor:{type:"string",description:"Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL."}},required:["account_number"],additionalProperties:!1}},{name:"get_equity_price_book",namespace:"equity_market_data",description:"Get a real-time bid/ask order book (Level 2) snapshot for one or more equity symbols (max 4), showing the ladder of price levels and resting share size on each side. Use to read supply/demand depth before entering or exiting a position. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{symbols:{type:["null","array"],items:{type:"string"},description:"One or more stock symbols, max 4 per call."}},required:["symbols"],additionalProperties:!1}},{name:"get_equity_quotes",namespace:"equity_market_data",description:"Get real-time stock quotes and the official last-completed-session close for one or more symbols. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{symbols:{type:["null","array"],items:{type:"string"},description:"One or more stock symbols. Above 20 symbols, quotes still return but closes is omitted with closes_error set."}},required:["symbols"],additionalProperties:!1}},{name:"get_equity_tax_lots",namespace:"equity_trading",description:"List the open tax lots for one equity holding in an account — each lot is a separate acquisition with its own quantity, cost basis, acquisition date, and long/short-term status. Requires a symbol (tax lots are tracked per instrument). Use it for cost-basis, holding-period, or which-lots-would-sell questions. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts."},symbol:{type:"string",description:"Ticker symbol of the holding whose tax lots you want, e.g. AAPL. Tax lots are tracked per instrument — one symbol per call."},cursor:{type:"string",description:"Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL."}},required:["account_number","symbol"],additionalProperties:!1}},{name:"get_equity_technical_indicators",namespace:"equity_market_data",description:`Compute a technical indicator (RSI, MACD, Bollinger Bands, moving averages, ATR, VWAP, and more) over one equity symbol's OHLCV bars across a time range. For the raw OHLCV bars themselves, use get_equity_historicals. + +Parameter rules: +- The parameters an indicator accepts depend on type: + - period only: ema/sma (default 9); rsi/cci/atr/mfi (default 14); williams_r/adx (default 10); momentum (default 12); roc (default 14); donchian_channels (default 20). + - bollinger_bands: period (default 20) + num_std (default 2). + - macd: fast_period (12), slow_period (26), signal_period (9). + - keltner_channels: period (default 20) + multiplier (default 2). + - supertrend: period (default 10) + multiplier (default 3). + - pivot_points: method (only 'classic'). + - vwap, obv: no parameters. + Omit a parameter to use its default. Passing a parameter the chosen type does not accept is rejected. +- interval is REQUIRED — indicator periods are counted in bars, so there is no auto-selection. +- adjustment_type defaults to 'split'; 'all' (split + dividend) requires a day-or-coarser interval. +- If the requested range plus the indicator's warm-up exceeds the per-request bar cap, narrow the range or coarsen the interval. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{symbol:{type:"string",description:"Stock symbol (uppercase). Exactly one symbol per call."},type:{type:"string",description:"Indicator to compute. One of: ema, sma, rsi, momentum, roc, cci, williams_r, atr, mfi, adx, donchian_channels, bollinger_bands, macd, keltner_channels, supertrend, vwap, obv, pivot_points."},interval:{type:"string",description:"Required bar interval the indicator is computed on. Intraday: 15second, 30second, minute, 5minute, 10minute, 30minute, hour, 4hour. Interday: day, week, month, 3month, 6month, year, 5year, 10year, 20year, 50year. The 1-minute bar is named 'minute' (not '1minute')."},start_time:{type:"string",description:"Start of the range (RFC3339 UTC, e.g. '2026-01-01T00:00:00Z'). Required."},end_time:{type:"string",description:"End of the range (RFC3339 UTC). Optional — defaults to the current time when omitted."},bounds:{type:"string",description:"Session bounds. One of 'regular' (RTH, default) or 'extended'."},adjustment_type:{type:"string",description:"Corporate-action adjustment: 'none' (raw prices), 'split' (default), or 'all' (split + dividend; requires a day-or-coarser interval)."},output:{type:"string",description:"How much of the series to return: 'series' (default, full range), 'latest' (most recent bar only), or 'last:N' (most recent N bars). The indicator is always computed over the full range first; this only trims the response."},period:{type:["null","integer"],description:"Lookback period in bars. Applies to ema, sma, rsi, momentum, roc, cci, williams_r, atr, mfi, adx, donchian_channels, bollinger_bands, keltner_channels, supertrend. Omit to use the indicator's default."},num_std:{type:["null","number"],description:"Number of standard deviations for the bands. bollinger_bands only (default 2)."},fast_period:{type:["null","integer"],description:"Fast EMA period. macd only (default 12)."},slow_period:{type:["null","integer"],description:"Slow EMA period. macd only (default 26)."},signal_period:{type:["null","integer"],description:"Signal EMA period. macd only (default 9)."},multiplier:{type:["null","number"],description:"Band/offset multiplier. keltner_channels (default 2) and supertrend (default 3) only."},method:{type:"string",description:"Calculation method. pivot_points only; currently only 'classic'."}},required:["symbol","type","interval","start_time"],additionalProperties:!1}},{name:"get_equity_tradability",namespace:"equity_trading",description:"Check tradability for up to 10 equity symbols on a given account: per-session eligibility and fractional. Call before placing an order to surface restrictions. Exact-ticker match — no name or partial-ticker resolution. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts."},symbols:{type:["null","array"],items:{type:"string"},description:"Stock symbols, max 10 per call. With more than 10, split across multiple calls of 10 or fewer. Exact-ticker match only."}},required:["account_number","symbols"],additionalProperties:!1}},{name:"get_financials",namespace:"equity_market_data",description:"Get a company's reported financial metrics over time — revenue, gross profit, net income, and net margin — by fiscal period (annual or quarterly), for one or more symbols. Use this for fundamental analysis like revenue-growth and margin-trend tracking, profitability screens, and period-over-period comparisons. Read-only. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{symbols:{type:["null","array"],items:{type:"string"},description:"One or more stock symbols (max 20 per call). Exact-ticker match — no name or partial-ticker resolution. Lowercase and whitespace-padded input is normalized to uppercase-trimmed before forwarding."},period:{type:"string",description:"Reporting period: 'quarterly' or 'annual'. Defaults to 'quarterly' when omitted."},limit:{type:"integer",description:"Number of most-recent periods to return per symbol (e.g. 8 for the last 8 quarters or years). Defaults to 4; values above 40 are capped to 40."}},required:["symbols"],additionalProperties:!1}},{name:"get_index_historicals",namespace:"indexes",description:`Get OHLC value bars for one or more market indexes (by instrument UUID) across an explicit time range. Use this for charting an index's history and "recent movement" questions. If the bar's interpolated field is true, bar was synthesized to fill a gap and carry no new information. + +Parameter rules: +- instrument_ids are index instrument UUIDs from get_indexes. Resolve symbols there first; this tool does not accept ticker symbols. +- interval is required; pick the coarsest interval that answers the question. If the requested interval would produce too many bars for the range, the call is rejected — narrow the range or coarsen the interval. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{instrument_ids:{type:["null","array"],items:{type:"string"},description:"Index instrument UUIDs (from get_indexes). Up to 10 per call."},start_time:{type:"string",description:"Start of the range (RFC3339 UTC, e.g. '2026-01-01T00:00:00Z'). Required."},end_time:{type:"string",description:"End of the range (RFC3339 UTC). Optional — when omitted, defaults to the current time."},interval:{type:"string",description:"Bar interval. Required — there is no server auto-select for indexes. Intraday: 5second, 15second, 30second, minute, 5minute, 10minute, 30minute, hour, 4hour. Interday: day, week, month, 3month, 6month, year, 5year, 10year, 20year, 50year. Note: the 1-minute bar is named 'minute' (not '1minute')."}},required:["instrument_ids","start_time","interval"],additionalProperties:!1}},{name:"get_index_quotes",namespace:"indexes",description:"Get real-time values for one or more market indexes by instrument ID. Returns current index level, state, and timestamps. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{instrument_ids:{type:["null","array"],items:{type:"string"},description:"One or more index instrument IDs (UUIDs) to fetch current values for. Obtain IDs from the get_indexes tool."}},required:["instrument_ids"],additionalProperties:!1}},{name:"get_indexes",namespace:"indexes",description:`Get index data for market indexes by symbol. +Optionally pass a comma-separated list of symbols (e.g. 'SPX,NDX,DJI') to filter results. Omit symbols to return all available indexes. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{symbols:{type:"string",description:"Comma-separated list of index symbols to look up (e.g. 'SPX,NDX'). Omit to return all available indexes."}},additionalProperties:!1}},{name:"get_limited_margin_upgrade_info",namespace:"account_portfolio",description:"Check whether a cash account is eligible to upgrade to limited margin and return the links (web and mobile) that start the upgrade flow. Limited margin lets the account trade with unsettled funds — proceeds from a sale can go into a new order before that sale settles — while adding no borrowing or leverage. Call when the user asks about that capability, about trading with unsettled funds, or about enabling limited margin — or when a cash account shows unsettled_funds greater than 0 while you are reporting its funds, value, or buying power (the get_accounts and get_portfolio guides direct this).. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number to check. Obtain from get_accounts."}},required:["account_number"],additionalProperties:!1}},{name:"get_option_chains",namespace:"option_market_data",description:"List option chains for one or more underlyings. A chain describes the full set of expiration dates and contracts for a given underlying. One of underlying_symbol or ids is required. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{ids:{type:"string",description:"Comma-separated chain UUIDs."},underlying_symbol:{type:"string",description:"Ticker filter; covers equity and index underlyings (e.g. 'AAPL', 'SPX')."}},additionalProperties:!1}},{name:"get_option_historicals",namespace:"option_market_data",description:`Get OHLC price bars for one or more option contracts (by instrument UUID) across an explicit time range. Use this for charting an option's price history and "recent activity" questions. The server auto-selects an interval when one is not provided. If the bar's interpolated field is true, bar was synthesized to fill a gap and carry no new information. + +Parameter rules: +- instrument_ids are option contract UUIDs from get_option_instruments. Resolve underlying -> get_option_chains -> get_option_instruments first; this tool does not accept ticker symbols. +- interval is optional; when omitted the server auto-selects an interval that targets a bounded bar count across the range. Provide an explicit interval only when you need a specific granularity. +- bounds defaults to 'regular' (regular hours); use '24_5' or '24_7' for contracts and sessions that expose overnight data. +- If an explicitly requested interval would produce more bars than the upstream allows for the range, the call is rejected — narrow the range or coarsen the interval. The cap does not apply when interval is auto-selected. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{instrument_ids:{type:["null","array"],items:{type:"string"},description:"Option contract instrument UUIDs (from get_option_instruments). Up to 10 per call."},start_time:{type:"string",description:"Start of the range (RFC3339 UTC, e.g. '2026-01-01T00:00:00Z'). Required."},end_time:{type:"string",description:"End of the range (RFC3339 UTC). Optional — when omitted, defaults to the current time."},interval:{type:"string",description:"Bar interval. Optional — when omitted, the server auto-selects an interval that targets a bounded bar count across the range. Intraday: 15second, 30second, minute, 5minute, 10minute, 30minute, hour, 4hour. Interday: day, week, month, 3month, 6month, year, 5year, 10year, 20year, 50year. Note: the 1-minute bar is named 'minute' (not '1minute')."},bounds:{type:"string",description:"Session bounds. One of 'regular' (regular hours, default), '24_5', or '24_7'. Overnight data is available only for supported index and equity option contracts."}},required:["instrument_ids","start_time"],additionalProperties:!1}},{name:"get_option_instruments",namespace:"option_market_data",description:"List option contracts. One of chain_symbol, chain_id, or ids is required; narrow further with expiration_dates, strike_price, type, state. When looking up contracts for a specific expiration, call this in parallel for every chain whose expiration_dates (from get_option_chains) includes the date. For AM/PM/morning/evening preferences, first check settle_on_open on each chain via get_option_chains and only query matching chains. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{chain_id:{type:"string",description:"Chain UUID."},chain_symbol:{type:"string",description:"Underlying ticker (e.g. 'AAPL')."},expiration_dates:{type:"string",description:"Comma-separated YYYY-MM-DD expirations."},strike_price:{type:"string",description:"Exact strike (e.g. '150.0000')."},type:{type:"string",description:"'call' or 'put'."},state:{type:"string",description:"'active' (default), 'expired', or 'inactive'. Use 'expired' to find option contracts whose expiration date has passed; 'inactive' is for delisted/withdrawn contracts that never expired."},tradability:{type:"string",description:"'tradable' or 'untradable' (untradable is rejected at the tool layer)."},ids:{type:"string",description:"Comma-separated instrument UUIDs."},cursor:{type:"string",description:"Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL."}},additionalProperties:!1}},{name:"get_option_level_upgrade_info",namespace:"account_portfolio",description:"Get the upgrade URL to apply for options access on an account. Call when option_level is null. option_level_2 enables long calls/puts, covered calls, and cash-secured puts; option_level_3 adds spreads and complex strategies. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number to generate the upgrade URL for. Obtain from get_accounts."}},required:["account_number"],additionalProperties:!1}},{name:"get_option_orders",namespace:"option_trading",description:`Fetch options orders for an account — list mode (newest first; open and closed, including fills, cancellations, and rejections) or single-order mode by passing order_id. When the user asks for "orders" without specifying equity or options, call both get_option_orders and get_equity_orders in parallel. + +Filtering tips: +- Prefer narrow queries: combine state and/or created_at_gte for specific questions — the per-page cap is fixed. +- chain_ids filters by underlying chain UUID (from get_option_chains). +- created_at_gte: interpret relative times in the user's timezone, convert to UTC before sending. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts."},order_id:{type:"string",description:"Filter to a single order by UUID. The response shape is unchanged (orders[] with at most one entry); empty when the order does not belong to account_number."},state:{type:"string",description:"Filter by single state: queued, confirmed, partially_filled, filled, rejected, cancelled, failed, voided, pending_cancelled."},created_at_gte:{type:"string",description:"Lower bound (inclusive). ISO 8601 UTC or YYYY-MM-DD; naive values are interpreted as UTC."},chain_ids:{type:"string",description:"Comma-separated chain UUIDs (from get_option_chains) to filter by underlying."},underlying_type:{type:"string",description:"'equity' or 'index'."},placed_agent:{type:"string",description:"Filter to one source: 'user', 'agentic' (MCP), 'recurring', 'drip', etc."},cursor:{type:"string",description:"Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL."}},required:["account_number"],additionalProperties:!1}},{name:"get_option_positions",namespace:"option_trading",description:'List options positions for an account. Returns open and closed (zero-quantity) positions. Pass nonzero=true for "what options do I have" / "show me my positions" — the common case. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.',inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts."},nonzero:{type:"boolean",description:"True to return only currently-open positions; omit/false to include closed ones."},chain_ids:{type:"string",description:"Comma-separated chain UUIDs (from get_option_chains)."},option_ids:{type:"string",description:"Comma-separated instrument UUIDs."},type:{type:"string",description:"'long' or 'short'."},option_type:{type:"string",description:"'call' or 'put'."},expiration_date:{type:"string",description:"Exact expiration (YYYY-MM-DD)."},expiration_date_lte:{type:"string",description:"Upper bound on expiration (YYYY-MM-DD)."},expiration_date_gte:{type:"string",description:"Lower bound on expiration (YYYY-MM-DD)."},cursor:{type:"string",description:"Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL."}},required:["account_number"],additionalProperties:!1}},{name:"get_option_quotes",namespace:"option_market_data",description:"Get real-time quotes for one or more option contracts by instrument UUID, plus the official prior-session close for each. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{instrument_ids:{type:["null","array"],items:{type:"string"},description:"Option instrument UUIDs. Above 20, quotes still return but closes is omitted with closes_error set."}},required:["instrument_ids"],additionalProperties:!1}},{name:"get_option_watchlist",namespace:"option_market_data",description:"List the single-leg option contracts on the user's options watchlist. Use this instead of get_watchlist_items for the options watchlist — get_watchlist_items returns a generic shape that drops the option-specific title and the upstream rejects it with 400 anyway. Works for both equity options (AAPL, NVDA) and index options (SPX, NDX, RUT). Multi-leg strategies (verticals, condors, etc.) that may exist in the user's watchlist from app-side order placement are not shown — direct the user to the Robinhood app to view those. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",additionalProperties:!1}},{name:"get_pnl_trade_history",namespace:"account_portfolio",description:`Get a customer's per-trade realized profit & loss — a chronological, paginated list of closed/realizing trades (equities, options, crypto, prediction markets) with symbol, side, quantity, price, and realized gain/loss. This is the same data behind the app's PnL hub ("Realized profit & loss"). Read-only. Trades only. Use get_realized_pnl for aggregate/bucketed totals. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number (the rhs_account_number from get_accounts). Obtain it from get_accounts."},span:{type:"string",description:"Preset window: week (default), month, 3month, ytd, or all. Wormhole offers preset spans only (no arbitrary date range)."},symbol:{type:"string",description:"Optional single stock symbol filter (trimmed + uppercased). Omit for all symbols; one symbol per call."},cursor:{type:"string",description:"Pagination cursor from a previous response's next_cursor. Omit for the first page."}},required:["account_number"],additionalProperties:!1}},{name:"get_popular_watchlists",namespace:"watchlists",description:"Discover Robinhood-curated lists the user can follow (e.g. '100 Most Popular', 'Daily Movers'). Use to find a list_id, then pass it to follow_watchlist. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",additionalProperties:!1}},{name:"get_portfolio",namespace:"account_portfolio",description:`Get the account's portfolio market value breakdown by asset type and buying power. Use for "how much is my account worth?", "what's my portfolio breakdown?", "how much do I have in options?", and "how much can I spend / afford?" questions. Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number. Obtain from get_accounts."}},required:["account_number"],additionalProperties:!1}},{name:"get_realized_pnl",namespace:"account_portfolio",description:`Get a customer's realized profit & loss for an account over a time window — per-bucket realized gain ($ and %) and the number of closing trades, plus window totals. Read-only. Aggregate, bucketed numbers only (not individual trades). Use for post-trade analysis like "how did my last 90 days of trades do?". Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number (the rhs_account_number from get_accounts). Obtain it from get_accounts."},span:{type:"string",description:"Preset window: day, week, month, 3month, year, or all. Defaults to 3month ('last 90 days'). Mutually exclusive with start_date/end_date."},start_date:{type:"string",description:"Custom window start, YYYY-MM-DD, inclusive — interpreted at midnight in timezone (default US Eastern). Use with end_date instead of span; must be on or before end_date and not in the future."},end_date:{type:"string",description:"Custom window end, YYYY-MM-DD, inclusive — the entire end_date is covered (through 23:59:59 in timezone). Use with start_date instead of span; an end_date beyond today returns data through the present."},asset_classes:{type:["null","array"],items:{type:"string"},description:"Filter to one or more of equity, option, crypto. Omit for all asset classes available on the account."},display_currency:{type:"string",description:"Currency for returned amounts. Currently USD only; defaults to USD."},timezone:{type:"string",description:"IANA timezone for bucket day-boundaries (e.g. America/New_York). Defaults to the account timezone (US Eastern)."}},required:["account_number"],additionalProperties:!1}},{name:"get_scanner_filter_specs",namespace:"scanners",description:`List every valid scanner filter type and how to use it. Call this before constructing filters for create_scan or update_scan_filters — do not guess filter_type names. + +This tool takes no parameters. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",additionalProperties:!1}},{name:"get_scans",namespace:"scanners",description:`List the authenticated user's saved scanners (also called screeners). A scan is a saved set of filters and columns that filters the market for instruments matching specific criteria (e.g. "RSI > 70 and Volume > 1M"). The user creates these in Legend or via the create_scan tool. + +Returns one entry per scan with its id, title, active filters, configured columns, sort order, and a flag indicating whether the scan is managed by Cortex (Legend's AI agent). Cortex-managed scans are read-only via MCP — they can be run with run_scan but not modified with update_scan_filters or update_scan_config. + +This tool takes no parameters. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",additionalProperties:!1}},{name:"get_watchlist_items",namespace:"watchlists",description:"List the items in a watchlist. Items may be stocks/ETFs, crypto pairs, futures, indexes — distinguished by object_type. For the options watchlist, use get_option_watchlist instead — this tool returns a generic shape that drops the strategy-specific fields and the upstream rejects it with 400 anyway. This does not return live prices; use get_equity_quotes for stocks/ETFs or get_index_quotes for indexes. Pocket Pi does not currently expose a crypto quote Tool. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{list_id:{type:"string",description:"UUID of the watchlist whose items to fetch. Obtain from get_watchlists or get_popular_watchlists."}},required:["list_id"],additionalProperties:!1}},{name:"get_watchlists",namespace:"watchlists",description:"List the user's watchlists, including both user-created custom lists and Robinhood-curated lists the user follows. Use to look up list_id values for other watchlist tools. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",additionalProperties:!1}},{name:"place_equity_order",namespace:"equity_trading",description:`Place a real equity order with real money. Parameters mirror review_equity_order plus the optional ref_id. Requires an agentic_allowed=true account; non-agentic accounts are rejected. + +Call review_equity_order first to obtain the current quote, estimated cost, and pre-trade alerts. + +Idempotency: pass a fresh UUID as ref_id on the first call for each logical order and re-send the same ref_id after a transient transport failure. Use a new ref_id only for a new logical order. + +Parameter rules: +- For immediate fills with price protection, prefer a marketable limit at the current ask over a plain market. +- Outside regular hours, only limit orders execute. For an immediate fill during extended or overnight/24-hour sessions, place a limit order with market_hours set to that session. Market and stop orders are regular_hours-only; regular-hours orders placed after hours queue for the next regular open. +- Provide exactly one of quantity or dollar_amount; dollar_amount requires type=market. +- Fractional shares are supported only for type=market with market_hours=regular_hours on eligible accounts, up to 6 decimal places, with no short sells. +- limit_price is required for limit and stop_limit; stop_price is required for stop_market and stop_limit. +- Fractional and dollar-based orders are regular-hours only. +- tax_lots is sell-only specified-lot selection. Obtain open_lot_id values from get_equity_tax_lots and pass quantities that sum to the order quantity. It is limited to US accounts and cannot be combined with dollar_amount, stop orders, all_day_hours, or fractional limit orders. + +After the provider action succeeds, Pocket Pi writes the returned equity-order state into the activities View projection in robinhood.sqlite. Portfolio and positions converge on the next normal refresh.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts. Must be agentic_allowed=true; non-agentic accounts are rejected."},symbol:{type:"string",description:"Stock symbol."},side:{type:"string",description:"'buy' or 'sell'."},type:{type:"string",description:"'market', 'limit', 'stop_market', or 'stop_limit'."},quantity:{type:"string",description:"Number of shares. Decimals (fractional) allowed for market + regular_hours only."},dollar_amount:{type:"string",description:"USD notional (e.g. '100.00'). Only valid with type=market."},limit_price:{type:"string",description:"Limit price; required for limit or stop_limit."},stop_price:{type:"string",description:"Stop trigger price; required for stop_market or stop_limit."},time_in_force:{type:"string",description:"'gfd' or 'gtc'. Default: gfd."},market_hours:{type:"string",description:"'regular_hours' (default, 9:30–16:00 ET), 'extended_hours' (pre-/post-market), or 'all_day_hours' (the 24 Hour Market / overnight session). extended_hours and all_day_hours execute limit orders only — market, stop_market, and stop_limit are regular_hours-only and are rejected if tagged to another session."},tax_lots:{type:["null","array"],items:{type:"object",properties:{open_lot_id:{type:"string",description:"open_lot_id of the open tax lot to sell, from get_equity_tax_lots."},quantity:{type:"string",description:"Shares to sell from this lot as a decimal string; at most the lot's quantity_available."}},required:["open_lot_id","quantity"],additionalProperties:!1},description:"Optional specified-lot selection for a SELL order. To sell specific tax lots instead of the default FIFO cost basis, pass the exact lots as {open_lot_id, quantity} objects, where open_lot_id comes from get_equity_tax_lots and the quantities sum to the order quantity. Omit for default FIFO. Sell only; at most 30 lots; US accounts only. Not allowed with dollar_amount, stop_market/stop_limit, all_day_hours, or fractional-share limit orders."},ref_id:{type:"string",description:"Idempotency key (UUID). Generate once per logical order and re-send on retry — the upstream deduplicates by ref_id. Omitting falls back to a server-generated key (loses client↔gateway idempotency)."}},required:["account_number","symbol","side","type"],additionalProperties:!1}},{name:"place_option_order",namespace:"option_trading",description:`Place a real options order with real money. Call review_option_order first to obtain the current quote, fees, collateral, and pre-trade alerts. + +Capability: single-leg Level 2 strategies include covered calls, cash-secured puts, long calls, and long puts. Multi-leg Level 3 strategies require an option_level_3 account. The account must also have agentic_allowed=true. + +Idempotency: pass a fresh UUID as ref_id on the first call for each logical order and re-send the same ref_id after a transient transport failure. Use a new ref_id only for a new logical order. + +Resolve option_id through get_option_chains and get_option_instruments filtered by expiration_date, strike_price, and type. + +Parameter rules: +- A vertical spread has two opposite-side legs with the same expiration and different strikes. A calendar has the same strike and different expirations. An iron condor combines a put spread and call spread. A roll closes the existing leg and opens its replacement. +- Use the reviewed current strategy quote as the net price; do not sum stale individual-leg quotes. +- Multi-leg orders are unavailable on cash and retirement accounts through this tool. +- type supports limit (default), market, stop_limit, and stop_market. price is required for limit and stop_limit; stop_price is required for stop_market and stop_limit. +- market, stop_market, and stop_limit are single-leg only. market and stop_market are regular-hours GFD only. stop_market is sell-to-close only with stop_price below the current ask. +- Stock-option combo orders, meaning one option leg paired with 100 underlying shares, are not supported. + +Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts. Must be agentic_allowed=true."},legs:{type:["null","array"],items:{type:"object",properties:{option_id:{type:"string",description:"Option instrument UUID (from get_option_instruments)."},side:{type:"string",description:"'buy' or 'sell'."},position_effect:{type:"string",description:"'open' (new position) or 'close' (existing position). To close a long use sell; to close a short use buy."},ratio_quantity:{type:"integer",description:"Contracts for this leg per unit of the order's quantity. Defaults to 1, which is what standard spreads, condors, calendars, and rolls use. Must be 1 on a single-leg order; across legs the ratios must be in lowest terms (1:2, not 2:4)."}},required:["option_id","side","position_effect"],additionalProperties:!1},description:"1 to 4 legs, all on the same underlying and each a different contract. Several legs are filled together as one strategy. Must match the preceding review request."},direction:{type:"string",description:"Net direction of the whole order: 'debit' (you pay the net premium) or 'credit' (you receive it). Required with 2 or more legs; for one leg it is derived from that leg's side, so omit it."},type:{type:"string",description:"'limit' (default), 'market', 'stop_limit', or 'stop_market'. Must match the preceding review request. Only 'limit' is available with 2 or more legs."},quantity:{type:"string",description:"Positive integer contract count. With several legs it counts whole strategies — each leg fills quantity × its ratio_quantity contracts."},price:{type:"string",description:"Limit price. Per contract for one leg; with several legs it is the net premium of the whole strategy per unit of quantity, always positive — direction says whether it is paid or received. Required for limit/stop_limit; must be omitted for market/stop_market."},stop_price:{type:"string",description:"Stop trigger price per contract. Required for stop_limit/stop_market; must be omitted for limit/market."},time_in_force:{type:"string",description:"'gfd' (default) or 'gtc'. Market orders must be 'gfd'."},market_hours:{type:"string",description:"'regular_hours' (default), 'regular_curb_hours', or 'regular_curb_overnight_hours'. Non-limit-immediate orders only place in regular_hours. CURB requires an index chain with extended_hours_state='enabled'."},ref_id:{type:"string",description:"Idempotency key (UUID). Generate once per logical order and re-send on retry. Omitting falls back to a server-generated key."}},required:["account_number","legs","quantity"],additionalProperties:!1}},{name:"remove_from_watchlist",namespace:"watchlists",description:"Remove items from a watchlist. Exactly one of symbols (stocks/ETFs), currency_pair_ids (crypto), or index_ids (market indexes) is required. For options use remove_option_from_watchlist. Items not on the list are no-ops. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{list_id:{type:"string",description:"UUID of the watchlist to remove items from."},symbols:{type:["null","array"],items:{type:"string"},description:"Stock symbols to remove (e.g. ['AAPL']). Mutually exclusive with currency_pair_ids and index_ids."},currency_pair_ids:{type:["null","array"],items:{type:"string"},description:"Currency-pair UUIDs to remove. Mutually exclusive with symbols and index_ids."},index_ids:{type:["null","array"],items:{type:"string"},description:"Index UUIDs to remove. Mutually exclusive with symbols and currency_pair_ids."}},required:["list_id"],additionalProperties:!1}},{name:"remove_option_from_watchlist",namespace:"option_market_data",description:`Remove option contracts from the user's options watchlist. Specify the same position_type used when the contract was added; it defaults to "long". Contracts not on the list are no-ops. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{option_ids:{type:["null","array"],items:{type:"string"},description:'Option contract UUIDs to remove. The position_type must match how each contract was added (most likely "long").'},position_type:{type:"string",description:'"long" (default) or "short". Must match how the contract was originally added.'}},required:["option_ids"],additionalProperties:!1}},{name:"review_equity_order",namespace:"equity_trading",description:`Simulate a stock order without placing it. Returns the current quote, estimated cost, and pre-trade alerts such as buying power, PDT, and instrument halts. Use it before place_equity_order. Requires an agentic_allowed=true account; non-agentic accounts are rejected. + +Parameter rules: +- For immediate fills with price protection, prefer a marketable limit at the current ask over a plain market. +- Outside regular hours, only limit orders execute. Extended-hours and overnight orders must set the corresponding market_hours session. Market and stop orders are regular_hours-only. +- Provide exactly one of quantity or dollar_amount; dollar_amount requires type=market. +- Fractional shares are supported only for type=market with market_hours=regular_hours on eligible accounts, up to 6 decimal places, with no short sells. +- limit_price is required for limit and stop_limit; stop_price is required for stop_market and stop_limit. +- tax_lots is sell-only specified-lot selection using open_lot_id values from get_equity_tax_lots. The lot quantities must sum to the order quantity. It is limited to US accounts and cannot be combined with dollar_amount, stop orders, all_day_hours, or fractional limit orders. + +This tool does not submit the order. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts. Must be agentic_allowed=true; non-agentic accounts are rejected."},symbol:{type:"string",description:"Stock symbol."},side:{type:"string",description:"'buy' or 'sell'."},type:{type:"string",description:"'market', 'limit', 'stop_market', or 'stop_limit'."},quantity:{type:"string",description:"Number of shares. Decimals (fractional) allowed for market + regular_hours only."},dollar_amount:{type:"string",description:"USD notional (e.g. '100.00'). Only valid with type=market."},limit_price:{type:"string",description:"Limit price; required for limit or stop_limit."},stop_price:{type:"string",description:"Stop trigger price; required for stop_market or stop_limit."},time_in_force:{type:"string",description:"'gfd' (good for day) or 'gtc' (good till cancelled). Default: gfd."},market_hours:{type:"string",description:"'regular_hours' (default, 9:30–16:00 ET), 'extended_hours' (pre-/post-market), or 'all_day_hours' (the 24 Hour Market / overnight session). extended_hours and all_day_hours execute limit orders only — market, stop_market, and stop_limit are regular_hours-only and are rejected if tagged to another session."},tax_lots:{type:["null","array"],items:{type:"object",properties:{open_lot_id:{type:"string",description:"open_lot_id of the open tax lot to sell, from get_equity_tax_lots."},quantity:{type:"string",description:"Shares to sell from this lot as a decimal string; at most the lot's quantity_available."}},required:["open_lot_id","quantity"],additionalProperties:!1},description:"Optional specified-lot selection for a SELL order. To sell specific tax lots instead of the default FIFO cost basis, pass the exact lots as {open_lot_id, quantity} objects, where open_lot_id comes from get_equity_tax_lots and the quantities sum to the order quantity. Omit for default FIFO. Sell only; at most 30 lots; US accounts only. Not allowed with dollar_amount, stop_market/stop_limit, all_day_hours, or fractional-share limit orders."}},required:["account_number","symbol","side","type"],additionalProperties:!1}},{name:"review_option_order",namespace:"option_trading",description:`Simulate an options order without placing it. Returns the current strategy quote, fees, collateral, and pre-trade alerts. Use it before place_option_order. + +Capability: single-leg Level 2 strategies include covered calls, cash-secured puts, long calls, and long puts. Multi-leg Level 3 strategies require an option_level_3 account. The account must also have agentic_allowed=true. + +Parameter rules: +- legs contain option_id from get_option_instruments, side, position_effect, and optional ratio_quantity. +- A vertical spread has two opposite-side legs with the same expiration and different strikes. A calendar has the same strike and different expirations. An iron condor combines a put spread and call spread. A roll closes the existing leg and opens its replacement. +- Use the current strategy quote as the net price; do not sum stale individual-leg quotes. +- Multi-leg orders are unavailable on cash and retirement accounts through this tool. +- type supports limit (default), market, stop_limit, and stop_market. price is required for limit and stop_limit; stop_price is required for stop_market and stop_limit. +- market, stop_market, and stop_limit are single-leg only. market and stop_market are regular-hours GFD only. stop_market is sell-to-close only with stop_price below the current ask. Other non-limit-immediate types are blocked in extended-hours sessions. +- order_checks contains the broker's time, contract, and buying-power alerts. + +This tool does not submit the order. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{account_number:{type:"string",description:"Brokerage account number from get_accounts. Must be agentic_allowed=true."},legs:{type:["null","array"],items:{type:"object",properties:{option_id:{type:"string",description:"Option instrument UUID (from get_option_instruments)."},side:{type:"string",description:"'buy' or 'sell'."},position_effect:{type:"string",description:"'open' (new position) or 'close' (existing position). To close a long use sell; to close a short use buy."},ratio_quantity:{type:"integer",description:"Contracts for this leg per unit of the order's quantity. Defaults to 1, which is what standard spreads, condors, calendars, and rolls use. Must be 1 on a single-leg order; across legs the ratios must be in lowest terms (1:2, not 2:4)."}},required:["option_id","side","position_effect"],additionalProperties:!1},description:"1 to 4 legs, all on the same underlying and each a different contract. Several legs are filled together as one strategy."},direction:{type:"string",description:"Net direction of the whole order: 'debit' (you pay the net premium) or 'credit' (you receive it). Required with 2 or more legs; for one leg it is derived from that leg's side, so omit it."},type:{type:"string",description:"'limit' (default), 'market', 'stop_limit', or 'stop_market'. Only 'limit' is available with 2 or more legs."},quantity:{type:"string",description:"Positive integer contract count. With several legs it counts whole strategies — each leg fills quantity × its ratio_quantity contracts."},price:{type:"string",description:"Limit price (e.g. '1.50'). Per contract for one leg; with several legs it is the net premium of the whole strategy per unit of quantity, always positive — direction says whether it is paid or received. Required for limit/stop_limit; must be omitted for market/stop_market."},stop_price:{type:"string",description:"Stop trigger price per contract. Required for stop_limit/stop_market; must be omitted for limit/market. For sell-side stop_market, must be below the current ask."},time_in_force:{type:"string",description:"'gfd' (default) or 'gtc'. Market orders must be 'gfd'."},market_hours:{type:"string",description:"'regular_hours' (default), 'regular_curb_hours', or 'regular_curb_overnight_hours'. Extended-hours sessions only accept limit+immediate. CURB requires an index chain with extended_hours_state='enabled' (per get_option_chains); rejection surfaces at place time."},chain_symbol:{type:"string",description:"Underlying ticker (e.g. 'AAPL', 'SPXW'). Supply alongside underlying_type to include fees and collateral in the response — always do so when known."},underlying_type:{type:"string",description:"'equity' or 'index'. Required alongside chain_symbol to enable the fee + collateral fetch."}},required:["account_number","legs","quantity"],additionalProperties:!1}},{name:"run_scan",namespace:"scanners",description:`Execute a saved scanner (also called screener) and return live market results. A scan's filters are evaluated against current market data at request time — results are real-time, not cached. + +Returns the scan's title, the total number of matching instruments, a list of instrument rows (with ticker, instrument_id, type, and one cell per visible column), plus the active sort and filters. The agent should present results as a table and mention this is live data. + +Parameters: +- scan_id (required) — the scan identifier from get_scans or create_scan. Returns an error if the scan does not exist or does not belong to the calling user. + +Use get_scans first to discover available scan_ids if the user has not specified one. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{scan_id:{type:"string",description:"The scan identifier to execute. Get this from get_scans or create_scan."}},required:["scan_id"],additionalProperties:!1}},{name:"search",namespace:"equity_market_data",description:'Resolve a natural-language query to Robinhood instruments (stocks/ETFs), crypto pairs, or market indexes. Use when the user names an asset by name (or partial name) instead of a ticker/pair/index symbol, or when you need an instrument_id / currency-pair UUID / market-index id for a downstream tool. Defaults to instrument search; pass asset_type="currency_pair" for crypto or asset_type="market_index" for indexes (SPX, NDX, DJI, etc.). Instrument results carry symbol + instrument_id for equity market-data and trading tools. Crypto results carry a hyphenated symbol (e.g. BTC-USD) plus an id usable as currency_pair_ids in watchlist tools; Pocket Pi does not currently expose crypto quote or order Tools. Market-index results carry symbol + id — pass id to get_index_quotes/get_index_historicals or in the index_ids array of watchlist tools. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.',inputSchema:{type:"object",properties:{query:{type:"string",description:'Natural-language search query: company name, partial name, or ticker (e.g. "apple", "tesla motors", "AAPL"). Required.'},asset_type:{type:"string",description:'Asset category to search. Supported: "instrument" (US-listed stocks/ETFs), "currency_pair" (crypto pairs like BTC-USD), and "market_index" (e.g. SPX, NDX, DJI). Defaults to "instrument" when omitted. More categories (events, futures) will be added as their corresponding tools land.'},limit:{type:"integer",description:"Max results to return. Defaults to 10; clamped to 20."}},required:["query"],additionalProperties:!1}},{name:"unfollow_watchlist",namespace:"watchlists",description:"Stop following a Robinhood-curated list. The list itself is unchanged and no longer appears in the user's watchlists. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{list_id:{type:"string",description:"UUID of the Robinhood-curated list to unfollow."}},required:["list_id"],additionalProperties:!1}},{name:"update_scan_config",namespace:"scanners",description:`Change the sort order of a saved scan's results table. The scan's filters and columns are preserved; only the sort changes. + +Parameters: +- scan_id (required) — the scan to modify. +- sorting_column (required) — display name of the column to sort by. Must match a visible column on the scan; the error response lists available columns when no match is found. +- sorting_direction (required) — "asc" or "desc". + +Restrictions: +- Cortex-managed scans (cortex_managed: true in get_scans) are rejected. +- v1 supports only sort changes — column add/remove/visibility/reorder are not exposed by this tool. + +Returns the scan with the new sort applied and fresh live results in the new order. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{scan_id:{type:"string",description:"The scan to modify. Get this from get_scans or create_scan. Cortex-managed scans are rejected."},sorting_column:{type:"string",description:'Display name of the column to sort by (e.g. "Volume", "% Change", "RSI"). Must match a column currently visible on the scan — call get_scans / run_scan first to see available columns.'},sorting_direction:{type:"string",description:'"asc" or "desc" (ascending or descending).'}},required:["scan_id","sorting_column","sorting_direction"],additionalProperties:!1}},{name:"update_scan_filters",namespace:"scanners",description:`Replace the filters on an existing saved scan. The complete filter set provided replaces whatever filters the scan had — this is REPLACE semantics, not merge. To add a single filter to an existing scan, the agent must first read the scan (via get_scans or run_scan), then call this tool with all the existing filters plus the new one. + +Parameters: +- scan_id (required) — the scan to modify. Get from get_scans or create_scan. +- filters (required) — the complete new filter set. Send [] to clear all filters. Each filter has filter_type (FILTER_TYPE_... enum), predicate, values, optional interval/length. Call get_scanner_filter_specs for valid combinations. + +Restrictions: +- Cortex-managed scans (cortex_managed: true in get_scans) are rejected with a user-friendly error. +- Returns an error and does not apply any change if any filter fails validation. + +Returns the scan with its new filters and fresh live results. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.`,inputSchema:{type:"object",properties:{scan_id:{type:"string",description:"The scan to modify. Get this from get_scans or create_scan. Cortex-managed scans (cortex_managed: true in get_scans output) are rejected."},filters:{type:["null","array"],items:{type:"object",properties:{filter_type:{type:"string",description:'Wire-format enum name, e.g. "FILTER_TYPE_RSI". See the scanner-filter-specs resource for valid values. Omit when supplying expression.'},predicate:{type:"string",description:'Wire-format enum name, e.g. "PREDICATE_GREATER_THAN". See the scanner-filter-specs resource for the predicates supported by each filter.'},values:{type:["null","array"],items:{type:"string"},description:'Threshold values. Single-element for unary predicates, two-element for BETWEEN, multi-element for IN_LIST/ANY_OF. For a boolean expression screen, exactly ["True"].'},interval:{type:"string",description:`Time granularity for time-series filters (e.g. "1d"). Use one of the supported_intervals from the filter's scanner-filter-specs entry. Not used with expression — encode granularity inside the expression.`},length:{type:"integer",description:"Lookback length for filters that need one (e.g. RSI period of 14). Use one of the supported_lengths from the filter's scanner-filter-specs entry. Not used with expression.",minimum:-2147483648,maximum:2147483647},plot:{type:"string",description:`Plot / price-field input for filters that have one (e.g. "open" or "close" for % Change). Use one of the supported_plots from the filter's scanner-filter-specs entry. Not used with expression.`},expression:{type:"string",description:'Raw market-data expression to screen on, e.g. "dayVolume / volumeAvg(candleCount=30, candlePeriod=\\"1d\\", session=\\"all\\")" with a numeric predicate, or a whole comparison like "tradeAllDay.price > closeAvg(candleCount=50, candlePeriod=\\"1d\\", session=\\"all\\")" with predicate "=" and values ["True"]. Only preview_scan accepts expressions. Omit filter_type when set. Prefer an enum filter_type whenever one covers the request.'},display_title:{type:"string",description:'Optional short label for an expression filter, shown as the results-column header (e.g. "Relative volume (30D)"). Only used with expression.'}},required:["predicate","values"],additionalProperties:!1},description:"The complete set of filters the scan should have after the update. REPLACE semantics — to add a filter, supply all existing filters plus the new one. To remove a filter, omit it from the array. To clear all filters, send []. Each filter: filter_type (FILTER_TYPE_... enum), predicate (>, <, =, BETWEEN, etc.), values, optional interval (e.g. 1d), optional length (e.g. 14 for RSI). Call get_scanner_filter_specs first for valid filter_type / predicate / interval / length combinations."}},required:["scan_id","filters"],additionalProperties:!1}},{name:"update_watchlist",namespace:"watchlists",description:"Rename a custom watchlist or change its icon/description. Robinhood-curated lists cannot be renamed; the call will fail with 404. Provide at least one of display_name, icon_emoji, display_description. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.",inputSchema:{type:"object",properties:{list_id:{type:"string",description:"UUID of the watchlist to update. Obtain from get_watchlists."},display_name:{type:"string",description:"New name for the watchlist."},icon_emoji:{type:"string",description:"New emoji."},display_description:{type:"string",description:"New description."}},required:["list_id"],additionalProperties:!1}}]};var q=globalThis.db,x=q.open("robinhood");if(x<0)throw Error("open robinhood.sqlite");var F=5;function N(){return String(q.lastError(x)||"SQLite operation failed")}function k(e){if(q.exec(x,e)!==0)throw Error(N())}function A(e,t=[]){let i=JSON.parse(q.query(x,e,JSON.stringify(t)));if(i.error)throw Error(String(i.error));return i}function y(e,t=[]){return A(e,t)}function I(e,t){if(!t.length)return;let i=t.map((d)=>"("+d.map(()=>"?").join(",")+")").join(","),r=[];for(let d of t)r.push(...d);y(e+" VALUES "+i,r)}var Y=Number(A("PRAGMA user_version")?.rows?.[0]?.[0]??0);if(Y!==F)k(` + CREATE TABLE IF NOT EXISTS accounts ( + account_number TEXT PRIMARY KEY, + label TEXT NOT NULL, + suffix TEXT NOT NULL, + account_type TEXT, + status TEXT NOT NULL, + agentic_allowed INTEGER NOT NULL DEFAULT 0, + updated_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS portfolio_current ( + account_number TEXT PRIMARY KEY, + cash TEXT, + buying_power TEXT, + day_pnl TEXT, + week_pnl TEXT, + observed_at INTEGER NOT NULL + ); + CREATE TABLE IF NOT EXISTS total_value ( + account_number TEXT NOT NULL, + observed_at INTEGER NOT NULL, + value TEXT NOT NULL, + PRIMARY KEY(account_number, observed_at) + ); + CREATE TABLE IF NOT EXISTS positions ( + account_number TEXT NOT NULL, + symbol TEXT NOT NULL, + quantity TEXT, + average_price TEXT, + market_value TEXT, + observed_at INTEGER NOT NULL, + PRIMARY KEY(account_number, symbol) + ); + CREATE TABLE IF NOT EXISTS activities ( + account_number TEXT NOT NULL, + activity_id TEXT NOT NULL, + occurred_at TEXT, + observed_at INTEGER NOT NULL, + symbol TEXT, + side TEXT, + quantity TEXT, + price TEXT, + amount TEXT, + state TEXT, + activity_type TEXT, + PRIMARY KEY(account_number, activity_id) + ); + CREATE INDEX IF NOT EXISTS activities_account_recent ON activities(account_number, occurred_at DESC, observed_at DESC); + CREATE TABLE IF NOT EXISTS refresh_runs ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + started_at INTEGER NOT NULL, + completed_at INTEGER NOT NULL, + status TEXT NOT NULL, + operation_count INTEGER NOT NULL, + success_count INTEGER NOT NULL, + error TEXT + ); + CREATE INDEX IF NOT EXISTS refresh_runs_recent ON refresh_runs(id DESC); + PRAGMA user_version=5; + `);var O=w.tools,G=new Map(O.map((e)=>[e.name,e]));function b(){return Math.floor(Date.now()/1000)}function B(e){return e===null||e===void 0||typeof e==="object"?null:String(e)}function a(e,t){if(e===null||e===void 0||typeof e!=="object")return null;for(let i of t){let r=B(e[i]);if(r!==null)return r}for(let i of Object.values(e)){let r=a(i,t);if(r!==null)return r}return null}function j(e,t){if(e===null||e===void 0||typeof e!=="object")return[];for(let i of t)if(Array.isArray(e[i]))return e[i];for(let i of Object.values(e)){let r=j(i,t);if(r.length)return r}return[]}function z(e,t){let i=a(e,t)?.toLowerCase();return i==="true"||i==="1"||i==="yes"}function R(e){if(e===null)return null;let t=Number(e.replace(/[$,%]/g,""));return Number.isFinite(t)?t:null}function _(e,t){let i=t?.account_number;return i===null||i===void 0||i===""?a(e,["account_number","accountNumber","account"])||"":String(i)}function P(e,t){let i=j(e,t);return i.length?i:Array.isArray(e)?e:e?[e]:[]}function W(e){let t=Array.isArray(e?.names)?new Set(e.names.filter((s)=>typeof s==="string")):new Set,i=String(e?.query??"").toLowerCase().split(/[^a-z0-9_]+/).filter(Boolean),r=typeof e?.namespace==="string"?e.namespace:"";if(!t.size&&!i.length)throw Error("search_tools requires query or names");let d=Math.max(1,Math.min(8,Number(e?.limit??5)||5)),o=O.filter((s)=>!r||s.namespace===r).map((s)=>{let n=s.name.toLowerCase(),l=n+" "+s.description.toLowerCase(),c=t.has(s.name)?1000:i.reduce((u,p)=>u+(n===p?100:n.includes(p)?20:l.includes(p)?3:0),0);return{tool:s,score:c}}).filter((s)=>s.score>0).sort((s,n)=>n.score-s.score||s.tool.name.localeCompare(n.tool.name)).slice(0,d).map(({tool:s})=>({name:s.name,namespace:s.namespace,description:s.description,inputSchema:s.inputSchema}));return{source:w.source,protocolVersion:w.protocolVersion,matches:o}}function H(e,t){if(t==="null")return e===null;if(t==="array")return Array.isArray(e);if(t==="object")return e!==null&&typeof e==="object"&&!Array.isArray(e);if(t==="integer")return typeof e==="number"&&Number.isInteger(e);if(t==="number")return typeof e==="number"&&Number.isFinite(e);return typeof e===t}function U(e,t,i="arguments"){let r=Array.isArray(t?.type)?t.type:t?.type?[t.type]:[];if(r.length&&!r.some((o)=>H(e,o)))throw Error(i+" must be "+r.join(" or "));if(e===null)return;if(typeof e==="number"){if(typeof t.minimum==="number"&&et.maximum)throw Error(i+" exceeds maximum "+t.maximum)}if(Array.isArray(e)){if(t.items)e.forEach((o,s)=>U(o,t.items,i+"["+s+"]"));return}if(typeof e!=="object")return;let d=t.properties||{};for(let o of t.required||[])if(!(o in e))throw Error(i+"."+o+" is required");if(t.additionalProperties===!1){for(let o of Object.keys(e))if(!(o in d))throw Error(i+"."+o+" is not allowed")}for(let[o,s]of Object.entries(e))if(d[o])U(s,d[o],i+"."+o)}function X(e){let t=typeof e?.name==="string"?e.name:"",i=G.get(t);if(!i)throw Error("Unknown Robinhood provider Tool: "+t);let r=e?.arguments;return U(r,i.inputSchema),re(t,r)}function C(e){return e.startsWith("get_")||e.startsWith("review_")||e==="search"||e==="run_scan"}function L(e,t){let i=JSON.parse(globalThis.services.call("mcp.client","callTool",JSON.stringify({connection:"robinhood",name:e,arguments:t,retryable:C(e)})));if(!i.ok)throw Error(i.error||"Robinhood service failed");return i.value}function J(e){let t=JSON.parse(globalThis.services.call("mcp.client","callTools",JSON.stringify({connection:"robinhood",calls:e.map((i)=>({name:i.operation,arguments:i.args})),retryable:e.every((i)=>C(i.operation))})));if(!t.ok)throw Error(t.error||"Robinhood batch service failed");return Array.isArray(t.value?.results)?t.value.results:[]}function v(e){k("BEGIN IMMEDIATE");try{e(),k("COMMIT")}catch(t){try{k("ROLLBACK")}catch{}throw t}globalThis.app.commit()}function Z(e,t){let i=P(e,["accounts"]);y("DELETE FROM accounts");let r=[];for(let d of i){let o=_(d,{});if(!o)continue;let s=(a(d,["nickname","account_type","type"])||"").toUpperCase(),n=z(d,["agentic_allowed","agenticAllowed"]),l=n?"AGENTIC":s.includes("IRA")||s.includes("RETIRE")?"RETIREMENT":s.includes("JOINT")?"JOINT":"PERSONAL";r.push([o,l,o.slice(-4),s,(a(d,["status"])||"active").toUpperCase(),n?1:0,t])}I("INSERT INTO accounts(account_number,label,suffix,account_type,status,agentic_allowed,updated_at)",r)}function $(e,t,i){let r=_(e,t);if(!r)throw Error("Robinhood portfolio is missing account_number");let d=a(e,["cash","cash_available","withdrawable_amount"]),o=a(e,["buying_power","buyingPower"]),s=a(e,["day_pnl","dayPnl","equity_change"]),n=a(e,["week_pnl","weekPnl"]);y(`INSERT INTO portfolio_current(account_number,cash,buying_power,day_pnl,week_pnl,observed_at) + VALUES(?,?,?,?,?,?) + ON CONFLICT(account_number) DO UPDATE SET + cash=excluded.cash,buying_power=excluded.buying_power, + day_pnl=COALESCE(excluded.day_pnl,portfolio_current.day_pnl), + week_pnl=COALESCE(excluded.week_pnl,portfolio_current.week_pnl), + observed_at=excluded.observed_at`,[r,d,o,s,n,i]);let l=a(e,["total_value","equity","total_equity","portfolio_value","market_value"]);if(l!==null)y("INSERT OR REPLACE INTO total_value(account_number,observed_at,value) VALUES(?,?,?)",[r,i,l])}function Q(e,t,i){let r=_(e,t);if(!r)throw Error("Robinhood positions are missing account_number");let d=P(e,["positions"]).slice(0,64);y("DELETE FROM positions WHERE account_number=?",[r]);let o=[];for(let s of d){let n=a(s,["symbol"]);if(!n)continue;o.push([r,n,a(s,["quantity","shares"]),a(s,["average_price","averagePrice","average_buy_price"]),a(s,["market_value","marketValue","equity"]),i])}I("INSERT INTO positions(account_number,symbol,quantity,average_price,market_value,observed_at)",o)}function K(e,t,i){let r=_(e,t);if(!r)throw Error("Robinhood activities are missing account_number");let d=P(e,["orders","activities","results"]).slice(0,64);y("DELETE FROM activities WHERE account_number=?",[r]);let o=[];d.forEach((s,n)=>{let l=a(s,["symbol"]),c=(a(s,["side"])||"").toUpperCase(),u=a(s,["executed_quantity","cumulative_quantity","quantity"]),p=a(s,["average_price","averagePrice","executed_price","price"]),f=a(s,["last_transaction_at","created_at","updated_at","date"]),m=a(s,["id","order_id","orderId","activity_id"])||[f||i,l||"ORDER",c,n].join(":"),g=R(u),S=R(p),M=g!==null&&S!==null?String(g*S):p||u;o.push([r,m,f,i,l,c,u,p,M,(a(s,["state","status"])||"RECENT").toUpperCase(),(a(s,["type","order_type"])||"ORDER").toUpperCase()])}),I("INSERT INTO activities(account_number,activity_id,occurred_at,observed_at,symbol,side,quantity,price,amount,state,activity_type)",o)}function ee(e,t,i){let r=_(e,t);if(!r)throw Error("Robinhood P&L is missing account_number");let d=a(e,["total_returns","realized_pnl","total","amount","day_pnl","week_pnl"]),o=String(t?.span??"day");y(`INSERT INTO portfolio_current(account_number,day_pnl,week_pnl,observed_at) VALUES(?,?,?,?) + ON CONFLICT(account_number) DO UPDATE SET + day_pnl=COALESCE(excluded.day_pnl,portfolio_current.day_pnl), + week_pnl=COALESCE(excluded.week_pnl,portfolio_current.week_pnl), + observed_at=MAX(portfolio_current.observed_at,excluded.observed_at)`,[r,o==="week"?null:d,o==="week"?d:null,i])}function E(e){if(e.operation==="get_accounts")Z(e.value,e.observedAt);else if(e.operation==="get_portfolio")$(e.value,e.args,e.observedAt);else if(e.operation==="get_equity_positions")Q(e.value,e.args,e.observedAt);else if(e.operation==="get_equity_orders")K(e.value,e.args,e.observedAt);else if(e.operation==="get_realized_pnl")ee(e.value,e.args,e.observedAt);else return!1;return!0}function te(e){let t=e.toLowerCase();return t.includes("esp_err_http_connect")||t.includes("timeout")||t.includes("tls")||t.includes("socket")||t.includes("network")}function T(){let e=b(),t=[],i=[],r=0,d=(n,l)=>{r+=1;try{let c=L(n,l);return t.push({operation:n,args:l,value:c,observedAt:b()}),c}catch(c){let u=c instanceof Error?c.message:String(c);if(i.push(n+": "+u),n==="get_accounts"||te(u))throw c;return null}},o=null;try{let n=d("get_accounts",{}),c=P(n,["accounts"]).map((h)=>_(h,{})).filter(Boolean);if(!c.length)throw Error("Robinhood returned no brokerage accounts");let u=new Date((e-604800)*1000).toISOString().slice(0,10),p=[];for(let h of c){let m={account_number:h};p.push({operation:"get_portfolio",args:m},{operation:"get_equity_positions",args:m},{operation:"get_equity_orders",args:{...m,created_at_gte:u}},{operation:"get_realized_pnl",args:{...m,span:"day",asset_classes:["equity"]}},{operation:"get_realized_pnl",args:{...m,span:"week",asset_classes:["equity"]}})}r+=p.length;let f=J(p);if(f.length!==p.length)throw Error("Robinhood batch returned an incomplete result set");if(f.forEach((h,m)=>{let g=p[m];if(h?.ok)t.push({operation:g.operation,args:g.args,value:h.value,observedAt:b()});else i.push(g.operation+": "+String(h?.error||"unknown provider error"))}),!t.some((h)=>h.operation==="get_portfolio"))throw Error("Robinhood batch returned no portfolio data")}catch(n){o=n instanceof Error?n.message:String(n)}let s=o?"failed":i.length?"partial":"succeeded";if(v(()=>{if(!o)for(let n of t)E(n);y(`INSERT INTO refresh_runs(started_at,completed_at,status,operation_count,success_count,error) + VALUES(?,?,?,?,?,?)`,[e,b(),s,r,o?0:t.length,o||i.join(" | ")||null])}),o)throw Error(o);return{status:s,operationCount:r,successCount:t.length}}function ie(e,t,i,r){let d=String(i?.account_number??"");if(!d)return;let s=String(i?.order_id??a(t,["order_id","orderId","id"])??"")||[r,i?.symbol||"EQUITY",e].join(":"),n=e==="cancel_equity_order"?a(t,["state","status"])||"CANCEL_REQUESTED":a(t,["state","status"])||"SUBMITTED";y(`INSERT INTO activities(account_number,activity_id,occurred_at,observed_at,symbol,side,quantity,price,amount,state,activity_type) + VALUES(?,?,?,?,?,?,?,?,?,?,?) + ON CONFLICT(account_number,activity_id) DO UPDATE SET + occurred_at=COALESCE(excluded.occurred_at,activities.occurred_at), + observed_at=excluded.observed_at, + symbol=COALESCE(excluded.symbol,activities.symbol), + side=COALESCE(excluded.side,activities.side), + quantity=COALESCE(excluded.quantity,activities.quantity), + price=COALESCE(excluded.price,activities.price), + state=excluded.state, + activity_type=excluded.activity_type`,[d,s,a(t,["created_at","updated_at","last_transaction_at"]),r,i?.symbol??a(t,["symbol"]),i?.side??a(t,["side"]),i?.quantity??i?.dollar_amount??a(t,["quantity","executed_quantity"]),i?.limit_price??i?.stop_price??a(t,["price","average_price"]),i?.dollar_amount??null,String(n).toUpperCase(),e==="cancel_equity_order"?"EQUITY ORDER CANCEL":"EQUITY ORDER"])}function re(e,t){let i=L(e,t),r={operation:e,args:t,value:i,observedAt:b()};if(["get_accounts","get_portfolio","get_equity_positions","get_equity_orders","get_realized_pnl"].includes(e))v(()=>E(r));if(e==="place_equity_order"||e==="cancel_equity_order")v(()=>ie(e,i,t,r.observedAt));return i}function D(e){return JSON.stringify({text:JSON.stringify(e),isError:!1})}globalThis.PocketPiData={invokeTask(e){try{if(e!=="refreshPortfolio")throw Error("Unknown Robinhood Data Action: "+e);let t=T();return D(t)}catch(t){return JSON.stringify({text:t instanceof Error?t.message:String(t),isError:!0})}},invokeTool(e,t){try{let i=JSON.parse(t),r=e==="robinhood.refresh_portfolio"?T():e==="robinhood.search_tools"?W(i):e==="robinhood.call"?X(i):(()=>{throw Error("Unknown Robinhood Tool: "+e)})();return D(r)}catch(i){return JSON.stringify({text:i instanceof Error?i.message:String(i),isError:!0})}}};})(); diff --git a/apps/robinhood/pocket.json b/apps/robinhood/pocket.json new file mode 100644 index 0000000..4f0f2da --- /dev/null +++ b/apps/robinhood/pocket.json @@ -0,0 +1,10 @@ +{ + "$schema": "https://pocketjs.dev/schema/pocket-2.json", + "pocket": 2, + "id": "dev.pocket-stack.robinhood", + "name": "robinhood", + "title": "Robinhood", + "version": "1.1.0", + "engine": {"capabilities":{"requires":["data.fs","data.sqlite"]}}, + "app": {"entry":"app.tsx","output":"app","framework":"solid","viewport":{"logical":[720,1280],"presentation":"fit"}} +} diff --git a/apps/robinhood/tool-catalog.json b/apps/robinhood/tool-catalog.json new file mode 100644 index 0000000..a7324b7 --- /dev/null +++ b/apps/robinhood/tool-catalog.json @@ -0,0 +1,1849 @@ +{ + "source": "https://agent.robinhood.com/mcp/trading", + "protocolVersion": "2025-06-18", + "retrievedAt": "2026-08-10T09:05:39+00:00", + "tools": [ + { + "name": "add_option_to_watchlist", + "namespace": "option_market_data", + "description": "Add option contracts to the user's options watchlist. Works for both equity options (AAPL, NVDA) and index options (SPX, NDX, RUT). Source option_ids from get_option_instruments. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "option_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Option contract UUIDs to add. Each becomes a single-leg position on the user's options watchlist. Source from get_option_instruments." + }, + "position_type": { + "type": "string", + "description": "\"long\" (default) or \"short\". Applies to every option_id in this call. For mixed long/short adds, issue two calls." + } + }, + "required": [ + "option_ids" + ], + "additionalProperties": false + } + }, + { + "name": "add_to_watchlist", + "namespace": "watchlists", + "description": "Add items to a watchlist. Exactly one of symbols (stocks/ETFs), currency_pair_ids (crypto), or index_ids (market indexes like SPX, NDX) is required — mutually exclusive. For options use add_option_to_watchlist (separate dedicated watchlist). Futures still require the Robinhood app. Already-present items are no-ops. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "list_id": { + "type": "string", + "description": "UUID of the watchlist to add items to." + }, + "symbols": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Stock symbols to add (e.g. ['AAPL', 'NVDA']). US stocks and ETFs only. Mutually exclusive with currency_pair_ids and index_ids." + }, + "currency_pair_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Currency-pair UUIDs to add, available as object_id from get_watchlist_items entries where object_type=currency_pair. Mutually exclusive with symbols and index_ids." + }, + "index_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Market-index UUIDs to add (the id field from get_indexes; SPX, NDX, DJI, etc.). Mutually exclusive with symbols and currency_pair_ids." + } + }, + "required": [ + "list_id" + ], + "additionalProperties": false + } + }, + { + "name": "cancel_equity_order", + "namespace": "equity_trading", + "description": "Cancel an open equity order by order_id. Resolve order_id via get_equity_orders and pass the same account_number. Requires an agentic_allowed=true account; non-agentic accounts are rejected. Cancellation may be rejected if the order has already filled, was already cancelled, or is otherwise ineligible. After the provider action succeeds, Pocket Pi writes the returned equity-order state into the activities View projection in robinhood.sqlite. Portfolio and positions converge on the next normal refresh.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account that owns the order, from get_accounts. Must be agentic_allowed=true. The upstream rejects mismatches against the order's owning account." + }, + "order_id": { + "type": "string", + "description": "Order UUID from get_equity_orders. Must live in account_number." + } + }, + "required": [ + "account_number", + "order_id" + ], + "additionalProperties": false + } + }, + { + "name": "cancel_option_exercise", + "namespace": "option_trading", + "description": "Cancel all queued exercise requests for an option position. Pass the same account_number and option_id used for exercise_option. Internally looks up all queued exercise events for that option and cancels each one. Typically there is one; multiple means separate exercise batches were submitted. Only events in state=queued can be cancelled; events already processing are rejected by the broker. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account that owns the exercise. Must be agentic_allowed=true." + }, + "option_id": { + "type": "string", + "description": "Option instrument UUID — the same option_id used for exercise_option. The tool looks up the queued exercise for this option and cancels it." + } + }, + "required": [ + "account_number", + "option_id" + ], + "additionalProperties": false + } + }, + { + "name": "cancel_option_order", + "namespace": "option_trading", + "description": "Cancel an open option order by account_number + order_id. Resolve order_id via get_option_orders and pass the same account_number. Requires an agentic_allowed=true account; non-agentic accounts are rejected. Cancellation may be rejected if the order has already filled, was already cancelled, or is otherwise ineligible. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account that owns the order, from get_accounts. Must be agentic_allowed=true. Mismatches against the order's owning account are rejected." + }, + "order_id": { + "type": "string", + "description": "Order UUID from get_option_orders. Must live in account_number." + } + }, + "required": [ + "account_number", + "order_id" + ], + "additionalProperties": false + } + }, + { + "name": "create_scan", + "namespace": "scanners", + "description": "Create a new saved scanner (screener) on the user's account, optionally applying a preset and custom filters in a single call. Returns the new scan's id, title, applied filters, and the initial live market results.\n\nThis tool composes multiple Beacon operations:\n 1. Create an empty scan\n 2. If a non-INITIAL preset was requested: apply that preset configuration (DAILY_GAINERS, DAILY_LOSERS, HIGH_OPTIONS_VOLUME_IV, UPCOMING_EARNINGS)\n 3. If custom filters were provided: apply them (replaces any preset filters)\n 4. If a custom title was provided: set the title\n\nIf any step after the initial create fails, the scan still exists with the partial state — the response surfaces what was applied. Use update_scan_filters / update_scan_config to fix anything that failed.\n\nParameters:\n- preset (optional) — starting preset. Default: DAILY_GAINERS when no filters supplied, INITIAL when filters supplied. Valid values: INITIAL, DAILY_GAINERS, DAILY_LOSERS, HIGH_OPTIONS_VOLUME_IV, UPCOMING_EARNINGS.\n- filters (optional) — array of custom filter specs. Call get_scanner_filter_specs to discover valid filter_type / predicate values.\n- title (optional) — custom human-readable name for the scan.\n\nExample: to make \"stocks with RSI > 70 and volume > 1M, sorted by volume desc\", call with\n preset = \"INITIAL\"\n filters = [\n {\"filter_type\": \"FILTER_TYPE_RSI\", \"predicate\": \">\", \"values\": [\"70\"], \"interval\": \"1d\", \"length\": 14},\n {\"filter_type\": \"FILTER_TYPE_VOLUME\", \"predicate\": \">\", \"values\": [\"1000000\"], \"interval\": \"1d\"}\n ]\n title = \"High RSI + High Volume\" This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "preset": { + "type": "string", + "description": "Starting preset for the new scan. One of: INITIAL (no preset; only valid if filters are provided), DAILY_GAINERS, DAILY_LOSERS, HIGH_OPTIONS_VOLUME_IV, UPCOMING_EARNINGS. Defaults to DAILY_GAINERS when no filters are provided, INITIAL when filters are provided." + }, + "filters": { + "type": [ + "null", + "array" + ], + "items": { + "type": "object", + "properties": { + "filter_type": { + "type": "string", + "description": "Wire-format enum name, e.g. \"FILTER_TYPE_RSI\". See the scanner-filter-specs resource for valid values. Omit when supplying expression." + }, + "predicate": { + "type": "string", + "description": "Wire-format enum name, e.g. \"PREDICATE_GREATER_THAN\". See the scanner-filter-specs resource for the predicates supported by each filter." + }, + "values": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Threshold values. Single-element for unary predicates, two-element for BETWEEN, multi-element for IN_LIST/ANY_OF. For a boolean expression screen, exactly [\"True\"]." + }, + "interval": { + "type": "string", + "description": "Time granularity for time-series filters (e.g. \"1d\"). Use one of the supported_intervals from the filter's scanner-filter-specs entry. Not used with expression — encode granularity inside the expression." + }, + "length": { + "type": "integer", + "description": "Lookback length for filters that need one (e.g. RSI period of 14). Use one of the supported_lengths from the filter's scanner-filter-specs entry. Not used with expression.", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "plot": { + "type": "string", + "description": "Plot / price-field input for filters that have one (e.g. \"open\" or \"close\" for % Change). Use one of the supported_plots from the filter's scanner-filter-specs entry. Not used with expression." + }, + "expression": { + "type": "string", + "description": "Raw market-data expression to screen on, e.g. \"dayVolume / volumeAvg(candleCount=30, candlePeriod=\\\"1d\\\", session=\\\"all\\\")\" with a numeric predicate, or a whole comparison like \"tradeAllDay.price > closeAvg(candleCount=50, candlePeriod=\\\"1d\\\", session=\\\"all\\\")\" with predicate \"=\" and values [\"True\"]. Only preview_scan accepts expressions. Omit filter_type when set. Prefer an enum filter_type whenever one covers the request." + }, + "display_title": { + "type": "string", + "description": "Optional short label for an expression filter, shown as the results-column header (e.g. \"Relative volume (30D)\"). Only used with expression." + } + }, + "required": [ + "predicate", + "values" + ], + "additionalProperties": false + }, + "description": "Custom filters to apply after the preset. Each filter has filter_type (FILTER_TYPE_... enum), predicate (>, <, =, BETWEEN, etc.), values, optional interval (e.g. 1d), and optional length (e.g. 14 for RSI). Call get_scanner_filter_specs first for valid filter_type / predicate combinations." + }, + "title": { + "type": "string", + "description": "Optional custom title for the saved scan. If omitted, Beacon assigns a default title based on the preset." + } + }, + "additionalProperties": false + } + }, + { + "name": "create_watchlist", + "namespace": "watchlists", + "description": "Create a new custom watchlist. Do not use this to follow a Robinhood-curated list; use follow_watchlist instead. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "display_name": { + "type": "string", + "description": "Name for the new watchlist (e.g. 'Tech Stocks'). Must be unique among the user's watchlists." + }, + "icon_emoji": { + "type": "string", + "description": "Emoji shown next to the name (one character)." + }, + "display_description": { + "type": "string", + "description": "Short description shown under the name." + } + }, + "required": [ + "display_name" + ], + "additionalProperties": false + } + }, + { + "name": "exercise_option", + "namespace": "option_trading", + "description": "Exercise a long options position. A call exercises the right to buy the underlying shares at the strike price; a put exercises the right to sell. Exercise is irrevocable once state moves past queued.\n\nPosition requirements: get_option_positions must show type=long and quantity > 0.\n\nAccount requirements: get_accounts must show agentic_allowed=true and option_level_2 or option_level_3. Non-agentic and option_level_0 accounts are rejected.\n\nIndex options cannot be manually exercised. Exercises submitted during market hours execute the same day; requests submitted after market close, including late-close trading days, are queued for overnight processing.\n\nParameter rules:\n- quantity must be a positive integer no greater than the available contracts.\n- allow_shorts=true applies only to put exercises when the account lacks enough shares to deliver; it creates a short equity position in the underlying stock.\n- reason is an optional explanation for the exercise.\n- ref_id must reuse the same UUID when retrying the same logical exercise and use a new UUID for a new exercise.\n\nPocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number. Must be agentic_allowed=true with option_level_2 or option_level_3." + }, + "option_id": { + "type": "string", + "description": "Option instrument UUID from get_option_positions or get_option_instruments. The position must be type=long." + }, + "quantity": { + "type": "integer", + "description": "Number of contracts to exercise (positive integer, minimum 1)." + }, + "ref_id": { + "type": "string", + "description": "Idempotency key (UUID). Generate once per logical exercise and re-send on retry. Omitting falls back to a server-generated key." + }, + "reason": { + "type": "string", + "description": "Optional exercise reason: covering_early_assignment | buying_stocks | not_enough_liquidity_or_spread_too_wide | hedging_position." + }, + "allow_shorts": { + "type": "boolean", + "description": "When true, allows a put exercise to proceed when the account does not own enough shares to deliver, creating a short equity position in the underlying stock. Default false." + } + }, + "required": [ + "account_number", + "option_id", + "quantity" + ], + "additionalProperties": false + } + }, + { + "name": "follow_watchlist", + "namespace": "watchlists", + "description": "Follow a Robinhood-curated list so it appears in the user's watchlists. Use only for curated lists; custom lists are already owned by the user. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "list_id": { + "type": "string", + "description": "UUID of the Robinhood-curated list to follow. Obtain from get_popular_watchlists." + } + }, + "required": [ + "list_id" + ], + "additionalProperties": false + } + }, + { + "name": "get_accounts", + "namespace": "account_portfolio", + "description": "List the user's brokerage accounts and return the account_number and capabilities needed by other tools. Does not return reliable buying power; use get_portfolio for buying power. Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + }, + { + "name": "get_earnings_calendar", + "namespace": "equity_market_data", + "description": "List earnings reports scheduled across the market over a date window (up to 31 days), optionally limited to high-market-cap names. Returns one entry per report event — estimated/actual EPS, report date and timing (am/pm), and company-verification status. Use this for market-wide discovery (\"what large-caps report this week?\"). For a specific known ticker, use get_earnings_results instead. Read-only. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "start_date": { + "type": "string", + "description": "Window anchor, YYYY-MM-DD. Defaults to today (US/Eastern) when omitted." + }, + "days": { + "type": "integer", + "description": "Window length in days, measured from start_date. Defaults to 7. Positive = forward window (e.g. 7 = the next 7 days, inclusive of start_date); negative = look-back window (e.g. -7 = the 7 days ending at start_date). Must be a non-zero value between -31 and 31 — windows wider than 31 days are rejected." + }, + "filter": { + "type": "string", + "description": "Optional result filter. Set to 'high_market_cap' to limit the calendar to high-market-cap names (market cap over $1B) — useful for 'what large-caps report this week' style questions. Omit for all names." + } + }, + "additionalProperties": false + } + }, + { + "name": "get_earnings_results", + "namespace": "equity_market_data", + "description": "Get recent and upcoming earnings for ONE equity symbol — estimated/actual EPS, report date and timing (am/pm), and company-verification status. Returns the trailing up to 8 quarters. Use this for earnings-timing questions (\"does AAPL report this week?\"), EPS surprise analysis, and screening for upcoming earnings risk on a specific stock. For market-wide earnings calendar queries across many symbols, use get_earnings_calendar. Read-only. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "symbol": { + "type": "string", + "description": "Stock symbol to look up (one symbol per call). Exact-ticker match — no name or partial-ticker resolution. Lowercase and whitespace-padded input is normalized to uppercase-trimmed before forwarding. Returns the trailing up to 8 quarters of earnings for the symbol." + } + }, + "required": [ + "symbol" + ], + "additionalProperties": false + } + }, + { + "name": "get_equity_fundamentals", + "namespace": "equity_market_data", + "description": "Get today's fundamentals for one or more stock symbols — valuation ratios (PE, P/B), capitalization (market cap, shares outstanding, float), today's session OHLCV, trailing volume averages, 52-week range, dividend schedule, and company profile. For real-time quotes use get_equity_quotes; for time-series price history use get_equity_historicals. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "symbols": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "One or more stock symbols (max 10 per call). Exact-ticker match — no name or partial-ticker resolution. Lowercase and whitespace-padded input is normalized to uppercase-trimmed before forwarding." + }, + "bounds": { + "type": "string", + "description": "Trading session the day-level fields (open / high / low / volume / overnight_volume) are drawn from. One of 'regular' (regular trading hours), 'trading' (regular + post-market), 'extended' (pre-market + regular + post-market), '24_5' (24-hour, 5-day-trading-week). Does not affect valuation fields. overnight_volume only populates when bounds=24_5. Defaults to 'regular' when omitted." + } + }, + "required": [ + "symbols" + ], + "additionalProperties": false + } + }, + { + "name": "get_equity_historicals", + "namespace": "equity_market_data", + "description": "Get OHLCV bars for one or more equity symbols across an explicit time range. Use this for charting, \"recent activity\" questions, and backtesting. The server auto-selects an interval when one is not provided. If the bar's interpolated field is true, bar was synthesized to fill a gap and carry no new information.\n\nParameter rules:\n- interval is optional; when omitted, the server auto-selects an interval that targets ~2,500 bars across the requested range. Provide an explicit interval only when you need a specific granularity.\n- interval values are fixed; the server does NOT aggregate intermediate bars. For a custom interval (e.g. 3-minute), request the next-finer fixed interval and aggregate client-side.\n- bounds defaults to 'regular' (RTH only); use 'extended' for pre-market through post-market data or '24_5' for overnight data.\n- adjustment_type defaults to 'split' (split-adjusted, the right default for backtesting). Use 'none' for raw prices, 'all' for split + dividend adjustment.\n- If the range would produce more bars than the upstream allows at the explicitly requested interval, narrow the range or coarsen the interval — the call is rejected before reaching upstream. The cap does not apply when interval is auto-selected. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "symbols": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "One or more stock symbols (uppercase). Up to 10 per call." + }, + "start_time": { + "type": "string", + "description": "Start of the range (RFC3339 UTC, e.g. '2026-01-01T00:00:00Z'). Required." + }, + "end_time": { + "type": "string", + "description": "End of the range (RFC3339 UTC). Optional — when omitted, defaults to the current time." + }, + "interval": { + "type": "string", + "description": "Bar interval. Optional — when omitted, the server picks an interval that targets ~2,500 bars across the requested range. Intraday: 15second, 30second, minute, 5minute, 10minute, 30minute, hour, 4hour. Interday: day, week, month, 3month, 6month, year, 5year, 10year, 20year, 50year. Note: the 1-minute bar is named 'minute' (not '1minute')." + }, + "bounds": { + "type": "string", + "description": "Session bounds. One of 'regular' (RTH, default), 'extended', 'trading', '24_5', '24_7', 'hyper_trading'." + }, + "adjustment_type": { + "type": "string", + "description": "Corporate-action adjustment: 'none' (raw prices), 'split' (default; right for backtesting), or 'all' (split + dividend; intraday only)." + } + }, + "required": [ + "symbols", + "start_time" + ], + "additionalProperties": false + } + }, + { + "name": "get_equity_orders", + "namespace": "equity_trading", + "description": "Fetch equity orders for an account — list mode (newest first; open and closed, including fills, cancellations, rejections) or single-order mode by passing order_id. When the user asks for \"orders\" without specifying an asset class, call get_equity_orders and get_option_orders in parallel.\n\nFiltering tips:\n- Prefer narrow queries: combine state, symbol, and/or created_at_gte for specific questions (e.g. \"my filled AAPL orders this week\") — the per-page cap is fixed.\n- created_at_gte: interpret relative times in the user's timezone, convert to UTC before sending.\n- symbol forces a symbol→instrument lookup; omit it if you don't need it. Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts." + }, + "order_id": { + "type": "string", + "description": "Filter to a single order by UUID. The response shape is unchanged (orders[] with at most one entry); empty when the order does not belong to account_number." + }, + "state": { + "type": "string", + "description": "Filter by single state: new, queued, confirmed, unconfirmed, partially_filled, filled, cancelled, rejected, failed, voided." + }, + "symbol": { + "type": "string", + "description": "Filter to one symbol (triggers a symbol→instrument lookup before the orders call)." + }, + "created_at_gte": { + "type": "string", + "description": "Lower bound (inclusive). ISO 8601 UTC or YYYY-MM-DD; naive values are interpreted as UTC." + }, + "placed_agent": { + "type": "string", + "description": "Filter to one source: 'user', 'agentic' (MCP), 'recurring', 'drip', etc." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL." + } + }, + "required": [ + "account_number" + ], + "additionalProperties": false + } + }, + { + "name": "get_equity_positions", + "namespace": "equity_trading", + "description": "List open equity positions for a specific brokerage account. Returns symbol, quantity, average cost, and per-position hold breakdowns. Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL." + } + }, + "required": [ + "account_number" + ], + "additionalProperties": false + } + }, + { + "name": "get_equity_price_book", + "namespace": "equity_market_data", + "description": "Get a real-time bid/ask order book (Level 2) snapshot for one or more equity symbols (max 4), showing the ladder of price levels and resting share size on each side. Use to read supply/demand depth before entering or exiting a position. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "symbols": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "One or more stock symbols, max 4 per call." + } + }, + "required": [ + "symbols" + ], + "additionalProperties": false + } + }, + { + "name": "get_equity_quotes", + "namespace": "equity_market_data", + "description": "Get real-time stock quotes and the official last-completed-session close for one or more symbols. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "symbols": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "One or more stock symbols. Above 20 symbols, quotes still return but closes is omitted with closes_error set." + } + }, + "required": [ + "symbols" + ], + "additionalProperties": false + } + }, + { + "name": "get_equity_tax_lots", + "namespace": "equity_trading", + "description": "List the open tax lots for one equity holding in an account — each lot is a separate acquisition with its own quantity, cost basis, acquisition date, and long/short-term status. Requires a symbol (tax lots are tracked per instrument). Use it for cost-basis, holding-period, or which-lots-would-sell questions. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts." + }, + "symbol": { + "type": "string", + "description": "Ticker symbol of the holding whose tax lots you want, e.g. AAPL. Tax lots are tracked per instrument — one symbol per call." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL." + } + }, + "required": [ + "account_number", + "symbol" + ], + "additionalProperties": false + } + }, + { + "name": "get_equity_technical_indicators", + "namespace": "equity_market_data", + "description": "Compute a technical indicator (RSI, MACD, Bollinger Bands, moving averages, ATR, VWAP, and more) over one equity symbol's OHLCV bars across a time range. For the raw OHLCV bars themselves, use get_equity_historicals.\n\nParameter rules:\n- The parameters an indicator accepts depend on type:\n - period only: ema/sma (default 9); rsi/cci/atr/mfi (default 14); williams_r/adx (default 10); momentum (default 12); roc (default 14); donchian_channels (default 20).\n - bollinger_bands: period (default 20) + num_std (default 2).\n - macd: fast_period (12), slow_period (26), signal_period (9).\n - keltner_channels: period (default 20) + multiplier (default 2).\n - supertrend: period (default 10) + multiplier (default 3).\n - pivot_points: method (only 'classic').\n - vwap, obv: no parameters.\n Omit a parameter to use its default. Passing a parameter the chosen type does not accept is rejected.\n- interval is REQUIRED — indicator periods are counted in bars, so there is no auto-selection.\n- adjustment_type defaults to 'split'; 'all' (split + dividend) requires a day-or-coarser interval.\n- If the requested range plus the indicator's warm-up exceeds the per-request bar cap, narrow the range or coarsen the interval. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "symbol": { + "type": "string", + "description": "Stock symbol (uppercase). Exactly one symbol per call." + }, + "type": { + "type": "string", + "description": "Indicator to compute. One of: ema, sma, rsi, momentum, roc, cci, williams_r, atr, mfi, adx, donchian_channels, bollinger_bands, macd, keltner_channels, supertrend, vwap, obv, pivot_points." + }, + "interval": { + "type": "string", + "description": "Required bar interval the indicator is computed on. Intraday: 15second, 30second, minute, 5minute, 10minute, 30minute, hour, 4hour. Interday: day, week, month, 3month, 6month, year, 5year, 10year, 20year, 50year. The 1-minute bar is named 'minute' (not '1minute')." + }, + "start_time": { + "type": "string", + "description": "Start of the range (RFC3339 UTC, e.g. '2026-01-01T00:00:00Z'). Required." + }, + "end_time": { + "type": "string", + "description": "End of the range (RFC3339 UTC). Optional — defaults to the current time when omitted." + }, + "bounds": { + "type": "string", + "description": "Session bounds. One of 'regular' (RTH, default) or 'extended'." + }, + "adjustment_type": { + "type": "string", + "description": "Corporate-action adjustment: 'none' (raw prices), 'split' (default), or 'all' (split + dividend; requires a day-or-coarser interval)." + }, + "output": { + "type": "string", + "description": "How much of the series to return: 'series' (default, full range), 'latest' (most recent bar only), or 'last:N' (most recent N bars). The indicator is always computed over the full range first; this only trims the response." + }, + "period": { + "type": [ + "null", + "integer" + ], + "description": "Lookback period in bars. Applies to ema, sma, rsi, momentum, roc, cci, williams_r, atr, mfi, adx, donchian_channels, bollinger_bands, keltner_channels, supertrend. Omit to use the indicator's default." + }, + "num_std": { + "type": [ + "null", + "number" + ], + "description": "Number of standard deviations for the bands. bollinger_bands only (default 2)." + }, + "fast_period": { + "type": [ + "null", + "integer" + ], + "description": "Fast EMA period. macd only (default 12)." + }, + "slow_period": { + "type": [ + "null", + "integer" + ], + "description": "Slow EMA period. macd only (default 26)." + }, + "signal_period": { + "type": [ + "null", + "integer" + ], + "description": "Signal EMA period. macd only (default 9)." + }, + "multiplier": { + "type": [ + "null", + "number" + ], + "description": "Band/offset multiplier. keltner_channels (default 2) and supertrend (default 3) only." + }, + "method": { + "type": "string", + "description": "Calculation method. pivot_points only; currently only 'classic'." + } + }, + "required": [ + "symbol", + "type", + "interval", + "start_time" + ], + "additionalProperties": false + } + }, + { + "name": "get_equity_tradability", + "namespace": "equity_trading", + "description": "Check tradability for up to 10 equity symbols on a given account: per-session eligibility and fractional. Call before placing an order to surface restrictions. Exact-ticker match — no name or partial-ticker resolution. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts." + }, + "symbols": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Stock symbols, max 10 per call. With more than 10, split across multiple calls of 10 or fewer. Exact-ticker match only." + } + }, + "required": [ + "account_number", + "symbols" + ], + "additionalProperties": false + } + }, + { + "name": "get_financials", + "namespace": "equity_market_data", + "description": "Get a company's reported financial metrics over time — revenue, gross profit, net income, and net margin — by fiscal period (annual or quarterly), for one or more symbols. Use this for fundamental analysis like revenue-growth and margin-trend tracking, profitability screens, and period-over-period comparisons. Read-only. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "symbols": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "One or more stock symbols (max 20 per call). Exact-ticker match — no name or partial-ticker resolution. Lowercase and whitespace-padded input is normalized to uppercase-trimmed before forwarding." + }, + "period": { + "type": "string", + "description": "Reporting period: 'quarterly' or 'annual'. Defaults to 'quarterly' when omitted." + }, + "limit": { + "type": "integer", + "description": "Number of most-recent periods to return per symbol (e.g. 8 for the last 8 quarters or years). Defaults to 4; values above 40 are capped to 40." + } + }, + "required": [ + "symbols" + ], + "additionalProperties": false + } + }, + { + "name": "get_index_historicals", + "namespace": "indexes", + "description": "Get OHLC value bars for one or more market indexes (by instrument UUID) across an explicit time range. Use this for charting an index's history and \"recent movement\" questions. If the bar's interpolated field is true, bar was synthesized to fill a gap and carry no new information.\n\nParameter rules:\n- instrument_ids are index instrument UUIDs from get_indexes. Resolve symbols there first; this tool does not accept ticker symbols.\n- interval is required; pick the coarsest interval that answers the question. If the requested interval would produce too many bars for the range, the call is rejected — narrow the range or coarsen the interval. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "instrument_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Index instrument UUIDs (from get_indexes). Up to 10 per call." + }, + "start_time": { + "type": "string", + "description": "Start of the range (RFC3339 UTC, e.g. '2026-01-01T00:00:00Z'). Required." + }, + "end_time": { + "type": "string", + "description": "End of the range (RFC3339 UTC). Optional — when omitted, defaults to the current time." + }, + "interval": { + "type": "string", + "description": "Bar interval. Required — there is no server auto-select for indexes. Intraday: 5second, 15second, 30second, minute, 5minute, 10minute, 30minute, hour, 4hour. Interday: day, week, month, 3month, 6month, year, 5year, 10year, 20year, 50year. Note: the 1-minute bar is named 'minute' (not '1minute')." + } + }, + "required": [ + "instrument_ids", + "start_time", + "interval" + ], + "additionalProperties": false + } + }, + { + "name": "get_index_quotes", + "namespace": "indexes", + "description": "Get real-time values for one or more market indexes by instrument ID. Returns current index level, state, and timestamps. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "instrument_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "One or more index instrument IDs (UUIDs) to fetch current values for. Obtain IDs from the get_indexes tool." + } + }, + "required": [ + "instrument_ids" + ], + "additionalProperties": false + } + }, + { + "name": "get_indexes", + "namespace": "indexes", + "description": "Get index data for market indexes by symbol.\nOptionally pass a comma-separated list of symbols (e.g. 'SPX,NDX,DJI') to filter results. Omit symbols to return all available indexes. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "symbols": { + "type": "string", + "description": "Comma-separated list of index symbols to look up (e.g. 'SPX,NDX'). Omit to return all available indexes." + } + }, + "additionalProperties": false + } + }, + { + "name": "get_limited_margin_upgrade_info", + "namespace": "account_portfolio", + "description": "Check whether a cash account is eligible to upgrade to limited margin and return the links (web and mobile) that start the upgrade flow. Limited margin lets the account trade with unsettled funds — proceeds from a sale can go into a new order before that sale settles — while adding no borrowing or leverage. Call when the user asks about that capability, about trading with unsettled funds, or about enabling limited margin — or when a cash account shows unsettled_funds greater than 0 while you are reporting its funds, value, or buying power (the get_accounts and get_portfolio guides direct this).. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number to check. Obtain from get_accounts." + } + }, + "required": [ + "account_number" + ], + "additionalProperties": false + } + }, + { + "name": "get_option_chains", + "namespace": "option_market_data", + "description": "List option chains for one or more underlyings. A chain describes the full set of expiration dates and contracts for a given underlying. One of underlying_symbol or ids is required. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "ids": { + "type": "string", + "description": "Comma-separated chain UUIDs." + }, + "underlying_symbol": { + "type": "string", + "description": "Ticker filter; covers equity and index underlyings (e.g. 'AAPL', 'SPX')." + } + }, + "additionalProperties": false + } + }, + { + "name": "get_option_historicals", + "namespace": "option_market_data", + "description": "Get OHLC price bars for one or more option contracts (by instrument UUID) across an explicit time range. Use this for charting an option's price history and \"recent activity\" questions. The server auto-selects an interval when one is not provided. If the bar's interpolated field is true, bar was synthesized to fill a gap and carry no new information.\n\nParameter rules:\n- instrument_ids are option contract UUIDs from get_option_instruments. Resolve underlying -> get_option_chains -> get_option_instruments first; this tool does not accept ticker symbols.\n- interval is optional; when omitted the server auto-selects an interval that targets a bounded bar count across the range. Provide an explicit interval only when you need a specific granularity.\n- bounds defaults to 'regular' (regular hours); use '24_5' or '24_7' for contracts and sessions that expose overnight data.\n- If an explicitly requested interval would produce more bars than the upstream allows for the range, the call is rejected — narrow the range or coarsen the interval. The cap does not apply when interval is auto-selected. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "instrument_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Option contract instrument UUIDs (from get_option_instruments). Up to 10 per call." + }, + "start_time": { + "type": "string", + "description": "Start of the range (RFC3339 UTC, e.g. '2026-01-01T00:00:00Z'). Required." + }, + "end_time": { + "type": "string", + "description": "End of the range (RFC3339 UTC). Optional — when omitted, defaults to the current time." + }, + "interval": { + "type": "string", + "description": "Bar interval. Optional — when omitted, the server auto-selects an interval that targets a bounded bar count across the range. Intraday: 15second, 30second, minute, 5minute, 10minute, 30minute, hour, 4hour. Interday: day, week, month, 3month, 6month, year, 5year, 10year, 20year, 50year. Note: the 1-minute bar is named 'minute' (not '1minute')." + }, + "bounds": { + "type": "string", + "description": "Session bounds. One of 'regular' (regular hours, default), '24_5', or '24_7'. Overnight data is available only for supported index and equity option contracts." + } + }, + "required": [ + "instrument_ids", + "start_time" + ], + "additionalProperties": false + } + }, + { + "name": "get_option_instruments", + "namespace": "option_market_data", + "description": "List option contracts. One of chain_symbol, chain_id, or ids is required; narrow further with expiration_dates, strike_price, type, state. When looking up contracts for a specific expiration, call this in parallel for every chain whose expiration_dates (from get_option_chains) includes the date. For AM/PM/morning/evening preferences, first check settle_on_open on each chain via get_option_chains and only query matching chains. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "chain_id": { + "type": "string", + "description": "Chain UUID." + }, + "chain_symbol": { + "type": "string", + "description": "Underlying ticker (e.g. 'AAPL')." + }, + "expiration_dates": { + "type": "string", + "description": "Comma-separated YYYY-MM-DD expirations." + }, + "strike_price": { + "type": "string", + "description": "Exact strike (e.g. '150.0000')." + }, + "type": { + "type": "string", + "description": "'call' or 'put'." + }, + "state": { + "type": "string", + "description": "'active' (default), 'expired', or 'inactive'. Use 'expired' to find option contracts whose expiration date has passed; 'inactive' is for delisted/withdrawn contracts that never expired." + }, + "tradability": { + "type": "string", + "description": "'tradable' or 'untradable' (untradable is rejected at the tool layer)." + }, + "ids": { + "type": "string", + "description": "Comma-separated instrument UUIDs." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL." + } + }, + "additionalProperties": false + } + }, + { + "name": "get_option_level_upgrade_info", + "namespace": "account_portfolio", + "description": "Get the upgrade URL to apply for options access on an account. Call when option_level is null. option_level_2 enables long calls/puts, covered calls, and cash-secured puts; option_level_3 adds spreads and complex strategies. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number to generate the upgrade URL for. Obtain from get_accounts." + } + }, + "required": [ + "account_number" + ], + "additionalProperties": false + } + }, + { + "name": "get_option_orders", + "namespace": "option_trading", + "description": "Fetch options orders for an account — list mode (newest first; open and closed, including fills, cancellations, and rejections) or single-order mode by passing order_id. When the user asks for \"orders\" without specifying equity or options, call both get_option_orders and get_equity_orders in parallel.\n\nFiltering tips:\n- Prefer narrow queries: combine state and/or created_at_gte for specific questions — the per-page cap is fixed.\n- chain_ids filters by underlying chain UUID (from get_option_chains).\n- created_at_gte: interpret relative times in the user's timezone, convert to UTC before sending. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts." + }, + "order_id": { + "type": "string", + "description": "Filter to a single order by UUID. The response shape is unchanged (orders[] with at most one entry); empty when the order does not belong to account_number." + }, + "state": { + "type": "string", + "description": "Filter by single state: queued, confirmed, partially_filled, filled, rejected, cancelled, failed, voided, pending_cancelled." + }, + "created_at_gte": { + "type": "string", + "description": "Lower bound (inclusive). ISO 8601 UTC or YYYY-MM-DD; naive values are interpreted as UTC." + }, + "chain_ids": { + "type": "string", + "description": "Comma-separated chain UUIDs (from get_option_chains) to filter by underlying." + }, + "underlying_type": { + "type": "string", + "description": "'equity' or 'index'." + }, + "placed_agent": { + "type": "string", + "description": "Filter to one source: 'user', 'agentic' (MCP), 'recurring', 'drip', etc." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL." + } + }, + "required": [ + "account_number" + ], + "additionalProperties": false + } + }, + { + "name": "get_option_positions", + "namespace": "option_trading", + "description": "List options positions for an account. Returns open and closed (zero-quantity) positions. Pass nonzero=true for \"what options do I have\" / \"show me my positions\" — the common case. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts." + }, + "nonzero": { + "type": "boolean", + "description": "True to return only currently-open positions; omit/false to include closed ones." + }, + "chain_ids": { + "type": "string", + "description": "Comma-separated chain UUIDs (from get_option_chains)." + }, + "option_ids": { + "type": "string", + "description": "Comma-separated instrument UUIDs." + }, + "type": { + "type": "string", + "description": "'long' or 'short'." + }, + "option_type": { + "type": "string", + "description": "'call' or 'put'." + }, + "expiration_date": { + "type": "string", + "description": "Exact expiration (YYYY-MM-DD)." + }, + "expiration_date_lte": { + "type": "string", + "description": "Upper bound on expiration (YYYY-MM-DD)." + }, + "expiration_date_gte": { + "type": "string", + "description": "Lower bound on expiration (YYYY-MM-DD)." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor. Omit for the first page; for the next page, pass the cursor query param from the prior response's next URL." + } + }, + "required": [ + "account_number" + ], + "additionalProperties": false + } + }, + { + "name": "get_option_quotes", + "namespace": "option_market_data", + "description": "Get real-time quotes for one or more option contracts by instrument UUID, plus the official prior-session close for each. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "instrument_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Option instrument UUIDs. Above 20, quotes still return but closes is omitted with closes_error set." + } + }, + "required": [ + "instrument_ids" + ], + "additionalProperties": false + } + }, + { + "name": "get_option_watchlist", + "namespace": "option_market_data", + "description": "List the single-leg option contracts on the user's options watchlist. Use this instead of get_watchlist_items for the options watchlist — get_watchlist_items returns a generic shape that drops the option-specific title and the upstream rejects it with 400 anyway. Works for both equity options (AAPL, NVDA) and index options (SPX, NDX, RUT). Multi-leg strategies (verticals, condors, etc.) that may exist in the user's watchlist from app-side order placement are not shown — direct the user to the Robinhood app to view those. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + }, + { + "name": "get_pnl_trade_history", + "namespace": "account_portfolio", + "description": "Get a customer's per-trade realized profit & loss — a chronological, paginated list of closed/realizing trades (equities, options, crypto, prediction markets) with symbol, side, quantity, price, and realized gain/loss. This is the same data behind the app's PnL hub (\"Realized profit & loss\"). Read-only. Trades only. Use get_realized_pnl for aggregate/bucketed totals. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number (the rhs_account_number from get_accounts). Obtain it from get_accounts." + }, + "span": { + "type": "string", + "description": "Preset window: week (default), month, 3month, ytd, or all. Wormhole offers preset spans only (no arbitrary date range)." + }, + "symbol": { + "type": "string", + "description": "Optional single stock symbol filter (trimmed + uppercased). Omit for all symbols; one symbol per call." + }, + "cursor": { + "type": "string", + "description": "Pagination cursor from a previous response's next_cursor. Omit for the first page." + } + }, + "required": [ + "account_number" + ], + "additionalProperties": false + } + }, + { + "name": "get_popular_watchlists", + "namespace": "watchlists", + "description": "Discover Robinhood-curated lists the user can follow (e.g. '100 Most Popular', 'Daily Movers'). Use to find a list_id, then pass it to follow_watchlist. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + }, + { + "name": "get_portfolio", + "namespace": "account_portfolio", + "description": "Get the account's portfolio market value breakdown by asset type and buying power. Use for \"how much is my account worth?\", \"what's my portfolio breakdown?\", \"how much do I have in options?\", and \"how much can I spend / afford?\" questions. Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number. Obtain from get_accounts." + } + }, + "required": [ + "account_number" + ], + "additionalProperties": false + } + }, + { + "name": "get_realized_pnl", + "namespace": "account_portfolio", + "description": "Get a customer's realized profit & loss for an account over a time window — per-bucket realized gain ($ and %) and the number of closing trades, plus window totals. Read-only. Aggregate, bucketed numbers only (not individual trades). Use for post-trade analysis like \"how did my last 90 days of trades do?\". Pocket Pi normalizes the View fields from this response into robinhood.sqlite and publishes one App revision after commit.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number (the rhs_account_number from get_accounts). Obtain it from get_accounts." + }, + "span": { + "type": "string", + "description": "Preset window: day, week, month, 3month, year, or all. Defaults to 3month ('last 90 days'). Mutually exclusive with start_date/end_date." + }, + "start_date": { + "type": "string", + "description": "Custom window start, YYYY-MM-DD, inclusive — interpreted at midnight in timezone (default US Eastern). Use with end_date instead of span; must be on or before end_date and not in the future." + }, + "end_date": { + "type": "string", + "description": "Custom window end, YYYY-MM-DD, inclusive — the entire end_date is covered (through 23:59:59 in timezone). Use with start_date instead of span; an end_date beyond today returns data through the present." + }, + "asset_classes": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Filter to one or more of equity, option, crypto. Omit for all asset classes available on the account." + }, + "display_currency": { + "type": "string", + "description": "Currency for returned amounts. Currently USD only; defaults to USD." + }, + "timezone": { + "type": "string", + "description": "IANA timezone for bucket day-boundaries (e.g. America/New_York). Defaults to the account timezone (US Eastern)." + } + }, + "required": [ + "account_number" + ], + "additionalProperties": false + } + }, + { + "name": "get_scanner_filter_specs", + "namespace": "scanners", + "description": "List every valid scanner filter type and how to use it. Call this before constructing filters for create_scan or update_scan_filters — do not guess filter_type names.\n\nThis tool takes no parameters. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + }, + { + "name": "get_scans", + "namespace": "scanners", + "description": "List the authenticated user's saved scanners (also called screeners). A scan is a saved set of filters and columns that filters the market for instruments matching specific criteria (e.g. \"RSI > 70 and Volume > 1M\"). The user creates these in Legend or via the create_scan tool.\n\nReturns one entry per scan with its id, title, active filters, configured columns, sort order, and a flag indicating whether the scan is managed by Cortex (Legend's AI agent). Cortex-managed scans are read-only via MCP — they can be run with run_scan but not modified with update_scan_filters or update_scan_config.\n\nThis tool takes no parameters. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + }, + { + "name": "get_watchlist_items", + "namespace": "watchlists", + "description": "List the items in a watchlist. Items may be stocks/ETFs, crypto pairs, futures, indexes — distinguished by object_type. For the options watchlist, use get_option_watchlist instead — this tool returns a generic shape that drops the strategy-specific fields and the upstream rejects it with 400 anyway. This does not return live prices; use get_equity_quotes for stocks/ETFs or get_index_quotes for indexes. Pocket Pi does not currently expose a crypto quote Tool. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "list_id": { + "type": "string", + "description": "UUID of the watchlist whose items to fetch. Obtain from get_watchlists or get_popular_watchlists." + } + }, + "required": [ + "list_id" + ], + "additionalProperties": false + } + }, + { + "name": "get_watchlists", + "namespace": "watchlists", + "description": "List the user's watchlists, including both user-created custom lists and Robinhood-curated lists the user follows. Use to look up list_id values for other watchlist tools. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "additionalProperties": false + } + }, + { + "name": "place_equity_order", + "namespace": "equity_trading", + "description": "Place a real equity order with real money. Parameters mirror review_equity_order plus the optional ref_id. Requires an agentic_allowed=true account; non-agentic accounts are rejected.\n\nCall review_equity_order first to obtain the current quote, estimated cost, and pre-trade alerts.\n\nIdempotency: pass a fresh UUID as ref_id on the first call for each logical order and re-send the same ref_id after a transient transport failure. Use a new ref_id only for a new logical order.\n\nParameter rules:\n- For immediate fills with price protection, prefer a marketable limit at the current ask over a plain market.\n- Outside regular hours, only limit orders execute. For an immediate fill during extended or overnight/24-hour sessions, place a limit order with market_hours set to that session. Market and stop orders are regular_hours-only; regular-hours orders placed after hours queue for the next regular open.\n- Provide exactly one of quantity or dollar_amount; dollar_amount requires type=market.\n- Fractional shares are supported only for type=market with market_hours=regular_hours on eligible accounts, up to 6 decimal places, with no short sells.\n- limit_price is required for limit and stop_limit; stop_price is required for stop_market and stop_limit.\n- Fractional and dollar-based orders are regular-hours only.\n- tax_lots is sell-only specified-lot selection. Obtain open_lot_id values from get_equity_tax_lots and pass quantities that sum to the order quantity. It is limited to US accounts and cannot be combined with dollar_amount, stop orders, all_day_hours, or fractional limit orders.\n\nAfter the provider action succeeds, Pocket Pi writes the returned equity-order state into the activities View projection in robinhood.sqlite. Portfolio and positions converge on the next normal refresh.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts. Must be agentic_allowed=true; non-agentic accounts are rejected." + }, + "symbol": { + "type": "string", + "description": "Stock symbol." + }, + "side": { + "type": "string", + "description": "'buy' or 'sell'." + }, + "type": { + "type": "string", + "description": "'market', 'limit', 'stop_market', or 'stop_limit'." + }, + "quantity": { + "type": "string", + "description": "Number of shares. Decimals (fractional) allowed for market + regular_hours only." + }, + "dollar_amount": { + "type": "string", + "description": "USD notional (e.g. '100.00'). Only valid with type=market." + }, + "limit_price": { + "type": "string", + "description": "Limit price; required for limit or stop_limit." + }, + "stop_price": { + "type": "string", + "description": "Stop trigger price; required for stop_market or stop_limit." + }, + "time_in_force": { + "type": "string", + "description": "'gfd' or 'gtc'. Default: gfd." + }, + "market_hours": { + "type": "string", + "description": "'regular_hours' (default, 9:30–16:00 ET), 'extended_hours' (pre-/post-market), or 'all_day_hours' (the 24 Hour Market / overnight session). extended_hours and all_day_hours execute limit orders only — market, stop_market, and stop_limit are regular_hours-only and are rejected if tagged to another session." + }, + "tax_lots": { + "type": [ + "null", + "array" + ], + "items": { + "type": "object", + "properties": { + "open_lot_id": { + "type": "string", + "description": "open_lot_id of the open tax lot to sell, from get_equity_tax_lots." + }, + "quantity": { + "type": "string", + "description": "Shares to sell from this lot as a decimal string; at most the lot's quantity_available." + } + }, + "required": [ + "open_lot_id", + "quantity" + ], + "additionalProperties": false + }, + "description": "Optional specified-lot selection for a SELL order. To sell specific tax lots instead of the default FIFO cost basis, pass the exact lots as {open_lot_id, quantity} objects, where open_lot_id comes from get_equity_tax_lots and the quantities sum to the order quantity. Omit for default FIFO. Sell only; at most 30 lots; US accounts only. Not allowed with dollar_amount, stop_market/stop_limit, all_day_hours, or fractional-share limit orders." + }, + "ref_id": { + "type": "string", + "description": "Idempotency key (UUID). Generate once per logical order and re-send on retry — the upstream deduplicates by ref_id. Omitting falls back to a server-generated key (loses client↔gateway idempotency)." + } + }, + "required": [ + "account_number", + "symbol", + "side", + "type" + ], + "additionalProperties": false + } + }, + { + "name": "place_option_order", + "namespace": "option_trading", + "description": "Place a real options order with real money. Call review_option_order first to obtain the current quote, fees, collateral, and pre-trade alerts.\n\nCapability: single-leg Level 2 strategies include covered calls, cash-secured puts, long calls, and long puts. Multi-leg Level 3 strategies require an option_level_3 account. The account must also have agentic_allowed=true.\n\nIdempotency: pass a fresh UUID as ref_id on the first call for each logical order and re-send the same ref_id after a transient transport failure. Use a new ref_id only for a new logical order.\n\nResolve option_id through get_option_chains and get_option_instruments filtered by expiration_date, strike_price, and type.\n\nParameter rules:\n- A vertical spread has two opposite-side legs with the same expiration and different strikes. A calendar has the same strike and different expirations. An iron condor combines a put spread and call spread. A roll closes the existing leg and opens its replacement.\n- Use the reviewed current strategy quote as the net price; do not sum stale individual-leg quotes.\n- Multi-leg orders are unavailable on cash and retirement accounts through this tool.\n- type supports limit (default), market, stop_limit, and stop_market. price is required for limit and stop_limit; stop_price is required for stop_market and stop_limit.\n- market, stop_market, and stop_limit are single-leg only. market and stop_market are regular-hours GFD only. stop_market is sell-to-close only with stop_price below the current ask.\n- Stock-option combo orders, meaning one option leg paired with 100 underlying shares, are not supported.\n\nPocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts. Must be agentic_allowed=true." + }, + "legs": { + "type": [ + "null", + "array" + ], + "items": { + "type": "object", + "properties": { + "option_id": { + "type": "string", + "description": "Option instrument UUID (from get_option_instruments)." + }, + "side": { + "type": "string", + "description": "'buy' or 'sell'." + }, + "position_effect": { + "type": "string", + "description": "'open' (new position) or 'close' (existing position). To close a long use sell; to close a short use buy." + }, + "ratio_quantity": { + "type": "integer", + "description": "Contracts for this leg per unit of the order's quantity. Defaults to 1, which is what standard spreads, condors, calendars, and rolls use. Must be 1 on a single-leg order; across legs the ratios must be in lowest terms (1:2, not 2:4)." + } + }, + "required": [ + "option_id", + "side", + "position_effect" + ], + "additionalProperties": false + }, + "description": "1 to 4 legs, all on the same underlying and each a different contract. Several legs are filled together as one strategy. Must match the preceding review request." + }, + "direction": { + "type": "string", + "description": "Net direction of the whole order: 'debit' (you pay the net premium) or 'credit' (you receive it). Required with 2 or more legs; for one leg it is derived from that leg's side, so omit it." + }, + "type": { + "type": "string", + "description": "'limit' (default), 'market', 'stop_limit', or 'stop_market'. Must match the preceding review request. Only 'limit' is available with 2 or more legs." + }, + "quantity": { + "type": "string", + "description": "Positive integer contract count. With several legs it counts whole strategies — each leg fills quantity × its ratio_quantity contracts." + }, + "price": { + "type": "string", + "description": "Limit price. Per contract for one leg; with several legs it is the net premium of the whole strategy per unit of quantity, always positive — direction says whether it is paid or received. Required for limit/stop_limit; must be omitted for market/stop_market." + }, + "stop_price": { + "type": "string", + "description": "Stop trigger price per contract. Required for stop_limit/stop_market; must be omitted for limit/market." + }, + "time_in_force": { + "type": "string", + "description": "'gfd' (default) or 'gtc'. Market orders must be 'gfd'." + }, + "market_hours": { + "type": "string", + "description": "'regular_hours' (default), 'regular_curb_hours', or 'regular_curb_overnight_hours'. Non-limit-immediate orders only place in regular_hours. CURB requires an index chain with extended_hours_state='enabled'." + }, + "ref_id": { + "type": "string", + "description": "Idempotency key (UUID). Generate once per logical order and re-send on retry. Omitting falls back to a server-generated key." + } + }, + "required": [ + "account_number", + "legs", + "quantity" + ], + "additionalProperties": false + } + }, + { + "name": "remove_from_watchlist", + "namespace": "watchlists", + "description": "Remove items from a watchlist. Exactly one of symbols (stocks/ETFs), currency_pair_ids (crypto), or index_ids (market indexes) is required. For options use remove_option_from_watchlist. Items not on the list are no-ops. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "list_id": { + "type": "string", + "description": "UUID of the watchlist to remove items from." + }, + "symbols": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Stock symbols to remove (e.g. ['AAPL']). Mutually exclusive with currency_pair_ids and index_ids." + }, + "currency_pair_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Currency-pair UUIDs to remove. Mutually exclusive with symbols and index_ids." + }, + "index_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Index UUIDs to remove. Mutually exclusive with symbols and currency_pair_ids." + } + }, + "required": [ + "list_id" + ], + "additionalProperties": false + } + }, + { + "name": "remove_option_from_watchlist", + "namespace": "option_market_data", + "description": "Remove option contracts from the user's options watchlist. Specify the same position_type used when the contract was added; it defaults to \"long\". Contracts not on the list are no-ops. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "option_ids": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Option contract UUIDs to remove. The position_type must match how each contract was added (most likely \"long\")." + }, + "position_type": { + "type": "string", + "description": "\"long\" (default) or \"short\". Must match how the contract was originally added." + } + }, + "required": [ + "option_ids" + ], + "additionalProperties": false + } + }, + { + "name": "review_equity_order", + "namespace": "equity_trading", + "description": "Simulate a stock order without placing it. Returns the current quote, estimated cost, and pre-trade alerts such as buying power, PDT, and instrument halts. Use it before place_equity_order. Requires an agentic_allowed=true account; non-agentic accounts are rejected.\n\nParameter rules:\n- For immediate fills with price protection, prefer a marketable limit at the current ask over a plain market.\n- Outside regular hours, only limit orders execute. Extended-hours and overnight orders must set the corresponding market_hours session. Market and stop orders are regular_hours-only.\n- Provide exactly one of quantity or dollar_amount; dollar_amount requires type=market.\n- Fractional shares are supported only for type=market with market_hours=regular_hours on eligible accounts, up to 6 decimal places, with no short sells.\n- limit_price is required for limit and stop_limit; stop_price is required for stop_market and stop_limit.\n- tax_lots is sell-only specified-lot selection using open_lot_id values from get_equity_tax_lots. The lot quantities must sum to the order quantity. It is limited to US accounts and cannot be combined with dollar_amount, stop orders, all_day_hours, or fractional limit orders.\n\nThis tool does not submit the order. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts. Must be agentic_allowed=true; non-agentic accounts are rejected." + }, + "symbol": { + "type": "string", + "description": "Stock symbol." + }, + "side": { + "type": "string", + "description": "'buy' or 'sell'." + }, + "type": { + "type": "string", + "description": "'market', 'limit', 'stop_market', or 'stop_limit'." + }, + "quantity": { + "type": "string", + "description": "Number of shares. Decimals (fractional) allowed for market + regular_hours only." + }, + "dollar_amount": { + "type": "string", + "description": "USD notional (e.g. '100.00'). Only valid with type=market." + }, + "limit_price": { + "type": "string", + "description": "Limit price; required for limit or stop_limit." + }, + "stop_price": { + "type": "string", + "description": "Stop trigger price; required for stop_market or stop_limit." + }, + "time_in_force": { + "type": "string", + "description": "'gfd' (good for day) or 'gtc' (good till cancelled). Default: gfd." + }, + "market_hours": { + "type": "string", + "description": "'regular_hours' (default, 9:30–16:00 ET), 'extended_hours' (pre-/post-market), or 'all_day_hours' (the 24 Hour Market / overnight session). extended_hours and all_day_hours execute limit orders only — market, stop_market, and stop_limit are regular_hours-only and are rejected if tagged to another session." + }, + "tax_lots": { + "type": [ + "null", + "array" + ], + "items": { + "type": "object", + "properties": { + "open_lot_id": { + "type": "string", + "description": "open_lot_id of the open tax lot to sell, from get_equity_tax_lots." + }, + "quantity": { + "type": "string", + "description": "Shares to sell from this lot as a decimal string; at most the lot's quantity_available." + } + }, + "required": [ + "open_lot_id", + "quantity" + ], + "additionalProperties": false + }, + "description": "Optional specified-lot selection for a SELL order. To sell specific tax lots instead of the default FIFO cost basis, pass the exact lots as {open_lot_id, quantity} objects, where open_lot_id comes from get_equity_tax_lots and the quantities sum to the order quantity. Omit for default FIFO. Sell only; at most 30 lots; US accounts only. Not allowed with dollar_amount, stop_market/stop_limit, all_day_hours, or fractional-share limit orders." + } + }, + "required": [ + "account_number", + "symbol", + "side", + "type" + ], + "additionalProperties": false + } + }, + { + "name": "review_option_order", + "namespace": "option_trading", + "description": "Simulate an options order without placing it. Returns the current strategy quote, fees, collateral, and pre-trade alerts. Use it before place_option_order.\n\nCapability: single-leg Level 2 strategies include covered calls, cash-secured puts, long calls, and long puts. Multi-leg Level 3 strategies require an option_level_3 account. The account must also have agentic_allowed=true.\n\nParameter rules:\n- legs contain option_id from get_option_instruments, side, position_effect, and optional ratio_quantity.\n- A vertical spread has two opposite-side legs with the same expiration and different strikes. A calendar has the same strike and different expirations. An iron condor combines a put spread and call spread. A roll closes the existing leg and opens its replacement.\n- Use the current strategy quote as the net price; do not sum stale individual-leg quotes.\n- Multi-leg orders are unavailable on cash and retirement accounts through this tool.\n- type supports limit (default), market, stop_limit, and stop_market. price is required for limit and stop_limit; stop_price is required for stop_market and stop_limit.\n- market, stop_market, and stop_limit are single-leg only. market and stop_market are regular-hours GFD only. stop_market is sell-to-close only with stop_price below the current ask. Other non-limit-immediate types are blocked in extended-hours sessions.\n- order_checks contains the broker's time, contract, and buying-power alerts.\n\nThis tool does not submit the order. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "account_number": { + "type": "string", + "description": "Brokerage account number from get_accounts. Must be agentic_allowed=true." + }, + "legs": { + "type": [ + "null", + "array" + ], + "items": { + "type": "object", + "properties": { + "option_id": { + "type": "string", + "description": "Option instrument UUID (from get_option_instruments)." + }, + "side": { + "type": "string", + "description": "'buy' or 'sell'." + }, + "position_effect": { + "type": "string", + "description": "'open' (new position) or 'close' (existing position). To close a long use sell; to close a short use buy." + }, + "ratio_quantity": { + "type": "integer", + "description": "Contracts for this leg per unit of the order's quantity. Defaults to 1, which is what standard spreads, condors, calendars, and rolls use. Must be 1 on a single-leg order; across legs the ratios must be in lowest terms (1:2, not 2:4)." + } + }, + "required": [ + "option_id", + "side", + "position_effect" + ], + "additionalProperties": false + }, + "description": "1 to 4 legs, all on the same underlying and each a different contract. Several legs are filled together as one strategy." + }, + "direction": { + "type": "string", + "description": "Net direction of the whole order: 'debit' (you pay the net premium) or 'credit' (you receive it). Required with 2 or more legs; for one leg it is derived from that leg's side, so omit it." + }, + "type": { + "type": "string", + "description": "'limit' (default), 'market', 'stop_limit', or 'stop_market'. Only 'limit' is available with 2 or more legs." + }, + "quantity": { + "type": "string", + "description": "Positive integer contract count. With several legs it counts whole strategies — each leg fills quantity × its ratio_quantity contracts." + }, + "price": { + "type": "string", + "description": "Limit price (e.g. '1.50'). Per contract for one leg; with several legs it is the net premium of the whole strategy per unit of quantity, always positive — direction says whether it is paid or received. Required for limit/stop_limit; must be omitted for market/stop_market." + }, + "stop_price": { + "type": "string", + "description": "Stop trigger price per contract. Required for stop_limit/stop_market; must be omitted for limit/market. For sell-side stop_market, must be below the current ask." + }, + "time_in_force": { + "type": "string", + "description": "'gfd' (default) or 'gtc'. Market orders must be 'gfd'." + }, + "market_hours": { + "type": "string", + "description": "'regular_hours' (default), 'regular_curb_hours', or 'regular_curb_overnight_hours'. Extended-hours sessions only accept limit+immediate. CURB requires an index chain with extended_hours_state='enabled' (per get_option_chains); rejection surfaces at place time." + }, + "chain_symbol": { + "type": "string", + "description": "Underlying ticker (e.g. 'AAPL', 'SPXW'). Supply alongside underlying_type to include fees and collateral in the response — always do so when known." + }, + "underlying_type": { + "type": "string", + "description": "'equity' or 'index'. Required alongside chain_symbol to enable the fee + collateral fetch." + } + }, + "required": [ + "account_number", + "legs", + "quantity" + ], + "additionalProperties": false + } + }, + { + "name": "run_scan", + "namespace": "scanners", + "description": "Execute a saved scanner (also called screener) and return live market results. A scan's filters are evaluated against current market data at request time — results are real-time, not cached.\n\nReturns the scan's title, the total number of matching instruments, a list of instrument rows (with ticker, instrument_id, type, and one cell per visible column), plus the active sort and filters. The agent should present results as a table and mention this is live data.\n\nParameters:\n- scan_id (required) — the scan identifier from get_scans or create_scan. Returns an error if the scan does not exist or does not belong to the calling user.\n\nUse get_scans first to discover available scan_ids if the user has not specified one. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "scan_id": { + "type": "string", + "description": "The scan identifier to execute. Get this from get_scans or create_scan." + } + }, + "required": [ + "scan_id" + ], + "additionalProperties": false + } + }, + { + "name": "search", + "namespace": "equity_market_data", + "description": "Resolve a natural-language query to Robinhood instruments (stocks/ETFs), crypto pairs, or market indexes. Use when the user names an asset by name (or partial name) instead of a ticker/pair/index symbol, or when you need an instrument_id / currency-pair UUID / market-index id for a downstream tool. Defaults to instrument search; pass asset_type=\"currency_pair\" for crypto or asset_type=\"market_index\" for indexes (SPX, NDX, DJI, etc.). Instrument results carry symbol + instrument_id for equity market-data and trading tools. Crypto results carry a hyphenated symbol (e.g. BTC-USD) plus an id usable as currency_pair_ids in watchlist tools; Pocket Pi does not currently expose crypto quote or order Tools. Market-index results carry symbol + id — pass id to get_index_quotes/get_index_historicals or in the index_ids array of watchlist tools. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "query": { + "type": "string", + "description": "Natural-language search query: company name, partial name, or ticker (e.g. \"apple\", \"tesla motors\", \"AAPL\"). Required." + }, + "asset_type": { + "type": "string", + "description": "Asset category to search. Supported: \"instrument\" (US-listed stocks/ETFs), \"currency_pair\" (crypto pairs like BTC-USD), and \"market_index\" (e.g. SPX, NDX, DJI). Defaults to \"instrument\" when omitted. More categories (events, futures) will be added as their corresponding tools land." + }, + "limit": { + "type": "integer", + "description": "Max results to return. Defaults to 10; clamped to 20." + } + }, + "required": [ + "query" + ], + "additionalProperties": false + } + }, + { + "name": "unfollow_watchlist", + "namespace": "watchlists", + "description": "Stop following a Robinhood-curated list. The list itself is unchanged and no longer appears in the user's watchlists. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "list_id": { + "type": "string", + "description": "UUID of the Robinhood-curated list to unfollow." + } + }, + "required": [ + "list_id" + ], + "additionalProperties": false + } + }, + { + "name": "update_scan_config", + "namespace": "scanners", + "description": "Change the sort order of a saved scan's results table. The scan's filters and columns are preserved; only the sort changes.\n\nParameters:\n- scan_id (required) — the scan to modify.\n- sorting_column (required) — display name of the column to sort by. Must match a visible column on the scan; the error response lists available columns when no match is found.\n- sorting_direction (required) — \"asc\" or \"desc\".\n\nRestrictions:\n- Cortex-managed scans (cortex_managed: true in get_scans) are rejected.\n- v1 supports only sort changes — column add/remove/visibility/reorder are not exposed by this tool.\n\nReturns the scan with the new sort applied and fresh live results in the new order. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "scan_id": { + "type": "string", + "description": "The scan to modify. Get this from get_scans or create_scan. Cortex-managed scans are rejected." + }, + "sorting_column": { + "type": "string", + "description": "Display name of the column to sort by (e.g. \"Volume\", \"% Change\", \"RSI\"). Must match a column currently visible on the scan — call get_scans / run_scan first to see available columns." + }, + "sorting_direction": { + "type": "string", + "description": "\"asc\" or \"desc\" (ascending or descending)." + } + }, + "required": [ + "scan_id", + "sorting_column", + "sorting_direction" + ], + "additionalProperties": false + } + }, + { + "name": "update_scan_filters", + "namespace": "scanners", + "description": "Replace the filters on an existing saved scan. The complete filter set provided replaces whatever filters the scan had — this is REPLACE semantics, not merge. To add a single filter to an existing scan, the agent must first read the scan (via get_scans or run_scan), then call this tool with all the existing filters plus the new one.\n\nParameters:\n- scan_id (required) — the scan to modify. Get from get_scans or create_scan.\n- filters (required) — the complete new filter set. Send [] to clear all filters. Each filter has filter_type (FILTER_TYPE_... enum), predicate, values, optional interval/length. Call get_scanner_filter_specs for valid combinations.\n\nRestrictions:\n- Cortex-managed scans (cortex_managed: true in get_scans) are rejected with a user-friendly error.\n- Returns an error and does not apply any change if any filter fails validation.\n\nReturns the scan with its new filters and fresh live results. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "scan_id": { + "type": "string", + "description": "The scan to modify. Get this from get_scans or create_scan. Cortex-managed scans (cortex_managed: true in get_scans output) are rejected." + }, + "filters": { + "type": [ + "null", + "array" + ], + "items": { + "type": "object", + "properties": { + "filter_type": { + "type": "string", + "description": "Wire-format enum name, e.g. \"FILTER_TYPE_RSI\". See the scanner-filter-specs resource for valid values. Omit when supplying expression." + }, + "predicate": { + "type": "string", + "description": "Wire-format enum name, e.g. \"PREDICATE_GREATER_THAN\". See the scanner-filter-specs resource for the predicates supported by each filter." + }, + "values": { + "type": [ + "null", + "array" + ], + "items": { + "type": "string" + }, + "description": "Threshold values. Single-element for unary predicates, two-element for BETWEEN, multi-element for IN_LIST/ANY_OF. For a boolean expression screen, exactly [\"True\"]." + }, + "interval": { + "type": "string", + "description": "Time granularity for time-series filters (e.g. \"1d\"). Use one of the supported_intervals from the filter's scanner-filter-specs entry. Not used with expression — encode granularity inside the expression." + }, + "length": { + "type": "integer", + "description": "Lookback length for filters that need one (e.g. RSI period of 14). Use one of the supported_lengths from the filter's scanner-filter-specs entry. Not used with expression.", + "minimum": -2147483648, + "maximum": 2147483647 + }, + "plot": { + "type": "string", + "description": "Plot / price-field input for filters that have one (e.g. \"open\" or \"close\" for % Change). Use one of the supported_plots from the filter's scanner-filter-specs entry. Not used with expression." + }, + "expression": { + "type": "string", + "description": "Raw market-data expression to screen on, e.g. \"dayVolume / volumeAvg(candleCount=30, candlePeriod=\\\"1d\\\", session=\\\"all\\\")\" with a numeric predicate, or a whole comparison like \"tradeAllDay.price > closeAvg(candleCount=50, candlePeriod=\\\"1d\\\", session=\\\"all\\\")\" with predicate \"=\" and values [\"True\"]. Only preview_scan accepts expressions. Omit filter_type when set. Prefer an enum filter_type whenever one covers the request." + }, + "display_title": { + "type": "string", + "description": "Optional short label for an expression filter, shown as the results-column header (e.g. \"Relative volume (30D)\"). Only used with expression." + } + }, + "required": [ + "predicate", + "values" + ], + "additionalProperties": false + }, + "description": "The complete set of filters the scan should have after the update. REPLACE semantics — to add a filter, supply all existing filters plus the new one. To remove a filter, omit it from the array. To clear all filters, send []. Each filter: filter_type (FILTER_TYPE_... enum), predicate (>, <, =, BETWEEN, etc.), values, optional interval (e.g. 1d), optional length (e.g. 14 for RSI). Call get_scanner_filter_specs first for valid filter_type / predicate / interval / length combinations." + } + }, + "required": [ + "scan_id", + "filters" + ], + "additionalProperties": false + } + }, + { + "name": "update_watchlist", + "namespace": "watchlists", + "description": "Rename a custom watchlist or change its icon/description. Robinhood-curated lists cannot be renamed; the call will fail with 404. Provide at least one of display_name, icon_emoji, display_description. This changes Robinhood account metadata but does not place a trade. Pocket Pi returns this result directly to the Agent and does not persist it, because the current fixed View does not consume this data.", + "inputSchema": { + "type": "object", + "properties": { + "list_id": { + "type": "string", + "description": "UUID of the watchlist to update. Obtain from get_watchlists." + }, + "display_name": { + "type": "string", + "description": "New name for the watchlist." + }, + "icon_emoji": { + "type": "string", + "description": "New emoji." + }, + "display_description": { + "type": "string", + "description": "New description." + } + }, + "required": [ + "list_id" + ], + "additionalProperties": false + } + } + ] +} diff --git a/crates/pocket-pi-agentos/Cargo.toml b/crates/pocket-pi-agentos/Cargo.toml new file mode 100644 index 0000000..668327d --- /dev/null +++ b/crates/pocket-pi-agentos/Cargo.toml @@ -0,0 +1,24 @@ +[package] +name = "pocket-pi-agentos" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +description = "Pocket Pi AgentOS app supervisor, bundle runtime and app tool router" +publish = false + +[dependencies] +anyhow.workspace = true +log.workspace = true +pocket-db.workspace = true +pocket-fs.workspace = true +pocket-mod.workspace = true +pocket-net.workspace = true +pocket-ui-surface.workspace = true +pocket-pi-embedded = { path = "../pocket-pi-embedded" } +pocketjs-core.workspace = true +serde.workspace = true +serde_json.workspace = true + +[dev-dependencies] +tempfile = "3" diff --git a/crates/pocket-pi-agentos/src/lib.rs b/crates/pocket-pi-agentos/src/lib.rs new file mode 100644 index 0000000..45d5e76 --- /dev/null +++ b/crates/pocket-pi-agentos/src/lib.rs @@ -0,0 +1,1961 @@ +//! Pocket Pi's App runtime: one isolated PocketJS Guest per active App, +//! app-owned FS/SQLite state, namespaced Agent tools and native AppTask wakes. + +use std::cell::{Cell, RefCell}; +use std::collections::{BTreeMap, BTreeSet}; +use std::path::{Path, PathBuf}; +use std::rc::Rc; +use std::sync::atomic::{AtomicBool, AtomicU32, Ordering}; +use std::sync::{mpsc, Arc, Mutex}; +use std::time::{Duration, Instant, SystemTime, UNIX_EPOCH}; + +use anyhow::{anyhow, Context as _, Result}; +use pocket_db::{DbModule, Storage as DbStorage}; +use pocket_fs::{FsModule, Storage as FsStorage}; +use pocket_mod::qjs::{CatchResultExt as _, Function, Object}; +use pocket_mod::Guest; +pub use pocket_net::{HttpRequest, NetFailure, TransportCompletion}; +use pocket_net::{HttpTransport, NetSurface}; +use pocket_pi_embedded::{AgentEvent, GuestAgent, ModelBackend, ToolHost, ToolResult}; +use pocket_ui_surface::UiSurface; +use serde::{Deserialize, Serialize}; +use serde_json::{json, Value}; + +pub const ROOT_APP_ID: &str = "pi-agent"; +const BUILTIN_RELEASE: &str = "builtin-v1"; +const VIEWPORT: (f32, f32) = (720.0, 1280.0); + +// One end-to-end budget starts when the Agent routes an App Tool. Queueing, +// Data Action execution and native transport all consume this same deadline. +pub const APP_ACTION_TIMEOUT: Duration = Duration::from_secs(80); + +fn new_action_deadline() -> Instant { + Instant::now() + APP_ACTION_TIMEOUT +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppDescriptor { + pub id: String, + #[serde(skip)] + pub title: String, + pub description: String, + pub version: String, + #[serde(default)] + pub data_version: u32, + #[serde(default)] + pub tool_namespace: String, + #[serde(default)] + pub tools: Vec, + #[serde(default)] + pub provider_operations: Vec, + #[serde(default)] + pub tasks: Vec, + #[serde(default)] + pub schedules: Vec, + #[serde(default)] + pub native_services: NativeServices, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct AppSchedule { + pub id: String, + pub every_minutes: u64, + pub task: String, + #[serde(default)] + pub args: Value, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct NativeServices { + #[serde(default)] + pub http: Vec, + #[serde(default)] + pub mcp: Vec, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct CredentialBinding { + pub id: String, + pub header: String, + #[serde(default)] + pub prefix: String, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct HttpServicePolicy { + pub method: String, + pub urls: Vec, + #[serde(default)] + pub allowed_request_headers: Vec, + pub credential: Option, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +#[serde(rename_all = "camelCase")] +pub struct McpServicePolicy { + pub connection: String, + pub url: String, + pub credential: CredentialBinding, +} + +#[derive(Clone, Copy)] +pub struct EmbeddedApp { + pub descriptor_json: &'static str, + pub pocket_json: &'static str, + pub js: &'static str, + pub data_js: Option<&'static str>, + pub agent_js: Option<&'static str>, + pub pak: &'static [u8], +} + +impl EmbeddedApp { + pub const fn new( + descriptor_json: &'static str, + pocket_json: &'static str, + js: &'static str, + data_js: Option<&'static str>, + agent_js: Option<&'static str>, + pak: &'static [u8], + ) -> Self { + Self { + descriptor_json, + pocket_json, + js, + data_js, + agent_js, + pak, + } + } +} + +#[derive(Clone)] +struct CatalogApp { + descriptor: AppDescriptor, + bundle: EmbeddedApp, +} + +#[derive(Clone)] +pub struct AppCatalog { + apps: BTreeMap, + tool_owner: BTreeMap, +} + +impl AppCatalog { + pub fn new(bundles: impl IntoIterator) -> Result { + let mut apps = BTreeMap::new(); + for bundle in bundles { + let mut descriptor: AppDescriptor = serde_json::from_str(bundle.descriptor_json) + .context("parse embedded agent-app.json")?; + let pocket: Value = + serde_json::from_str(bundle.pocket_json).context("parse embedded pocket.json")?; + descriptor.title = pocket + .get("title") + .and_then(Value::as_str) + .filter(|title| !title.is_empty()) + .ok_or_else(|| anyhow!("App {} pocket.json is missing title", descriptor.id))? + .to_owned(); + anyhow::ensure!( + pocket.get("version").and_then(Value::as_str) == Some(descriptor.version.as_str()), + "App {} version differs between agent-app.json and pocket.json", + descriptor.id + ); + if descriptor.tool_namespace.is_empty() { + descriptor.tool_namespace.clone_from(&descriptor.id); + } + if apps.contains_key(&descriptor.id) { + anyhow::bail!("duplicate embedded App id: {}", descriptor.id); + } + apps.insert(descriptor.id.clone(), CatalogApp { descriptor, bundle }); + } + anyhow::ensure!( + apps.contains_key(ROOT_APP_ID), + "missing {ROOT_APP_ID} System App" + ); + anyhow::ensure!( + apps[ROOT_APP_ID].bundle.agent_js.is_some(), + "{ROOT_APP_ID} System App is missing agent.js" + ); + anyhow::ensure!( + apps.values() + .filter(|app| app.bundle.agent_js.is_some()) + .count() + == 1, + "only {ROOT_APP_ID} may contain agent.js" + ); + let mut tool_owner = BTreeMap::new(); + for app in apps.values() { + let provider_operations = app + .descriptor + .provider_operations + .iter() + .collect::>(); + anyhow::ensure!( + provider_operations.len() == app.descriptor.provider_operations.len(), + "App {} declares duplicate provider operations", + app.descriptor.id + ); + anyhow::ensure!( + provider_operations + .iter() + .all(|operation| !operation.is_empty()), + "App {} declares an empty provider operation", + app.descriptor.id + ); + for tool in &app.descriptor.tools { + let name = tool + .get("name") + .and_then(Value::as_str) + .ok_or_else(|| anyhow!("{} tool is missing name", app.descriptor.id))?; + if !name.starts_with(&format!("{}.", app.descriptor.tool_namespace)) { + anyhow::bail!("App {} owns non-namespaced tool {name}", app.descriptor.id); + } + if tool_owner + .insert(name.to_owned(), app.descriptor.id.clone()) + .is_some() + { + anyhow::bail!("duplicate App tool: {name}"); + } + } + } + Ok(Self { apps, tool_owner }) + } + + fn tool_definitions(&self) -> Vec { + self.apps + .values() + .flat_map(|app| app.descriptor.tools.clone()) + .collect() + } + + fn app_for_tool(&self, name: &str) -> Option<&str> { + self.tool_owner.get(name).map(String::as_str) + } + + pub fn descriptors(&self) -> impl Iterator { + self.apps.values().map(|app| &app.descriptor) + } + + pub fn descriptor(&self, id: &str) -> Option<&AppDescriptor> { + self.apps.get(id).map(|app| &app.descriptor) + } + + pub fn provider_operation_allowed(&self, app_id: &str, operation: &str) -> bool { + self.descriptor(app_id) + .is_some_and(|app| app.provider_operations.iter().any(|item| item == operation)) + } + + pub fn http_policy(&self, app_id: &str, method: &str, url: &str) -> Option<&HttpServicePolicy> { + self.descriptor(app_id)? + .native_services + .http + .iter() + .find(|policy| policy.method == method && policy.urls.iter().any(|item| item == url)) + } + + pub fn mcp_policy(&self, app_id: &str, connection: &str) -> Option<&McpServicePolicy> { + self.descriptor(app_id)? + .native_services + .mcp + .iter() + .find(|policy| policy.connection == connection) + } + + pub fn credential_ids(&self) -> BTreeSet { + self.descriptors() + .flat_map(|descriptor| { + descriptor + .native_services + .http + .iter() + .filter_map(|policy| policy.credential.as_ref()) + .chain( + descriptor + .native_services + .mcp + .iter() + .map(|policy| &policy.credential), + ) + }) + .map(|credential| credential.id.clone()) + .collect() + } + + fn app(&self, id: &str) -> Option<&CatalogApp> { + self.apps.get(id) + } +} + +/// The native transport/security boundary used by App bundles. Implementations +/// own TLS, credentials and MCP sessions; Apps own operation selection, +/// normalization, SQLite and View behavior. +pub trait AppServiceHost: Send + Sync { + /// Execute one policy-checked synchronous service call without outliving + /// the App Data Action's absolute deadline. + fn call( + &self, + app_id: &str, + service: &str, + operation: &str, + args: &Value, + deadline: Instant, + ) -> Result; + + /// Execute one policy-checked PocketJS HTTP request. This is called only + /// from the native NET worker, never from the QuickJS/App Data thread. + fn http( + &self, + _app_id: &str, + request: HttpRequest, + _deadline: Instant, + ) -> std::result::Result { + Err(NetFailure::new( + "unavailable", + format!("HTTP is unavailable for handle {}", request.handle), + )) + } + + /// True while a native App worker owns network/TLS activity. + fn busy(&self) -> bool { + false + } +} + +/// One SQLite module instance per App. View and background Data Action guests +/// share this owner, so ESP32's `unix-none` VFS never has two independent +/// connections racing on the same file. Network waits never hold this mutex; +/// only bounded SQLite operations and transactions do. +type SharedDb = Arc>; +type AppRevision = Arc; + +const DATA_ACTION_QUEUE: usize = 8; +const NET_COMPLETION_QUEUE: usize = 2; +const NET_WORKER_STACK_BYTES: usize = 96 * 1024; +pub const DATA_ACTION_STACK_BYTES: usize = 128 * 1024; + +#[derive(Clone, Copy)] +enum DataActionKind { + Task, + Tool, +} + +struct DataActionRequest { + run_id: u64, + app_id: String, + kind: DataActionKind, + name: String, + args: Value, + deadline: Instant, + response: Option>, +} + +#[derive(Clone)] +struct DataAppConfig { + app_id: String, + source_path: PathBuf, + database: SharedDb, + revision: AppRevision, + net: bool, +} + +struct AppHttpRequest { + request: HttpRequest, + deadline: Instant, +} + +struct AppNetTransport { + requests: mpsc::SyncSender, + completions: mpsc::Receiver, + cancelled: BTreeSet, + action_deadline: Rc>>, +} + +impl AppNetTransport { + fn start( + app_id: String, + services: Arc, + action_deadline: Rc>>, + ) -> Result { + let (request_tx, request_rx) = mpsc::sync_channel::(NET_COMPLETION_QUEUE); + let (completion_tx, completion_rx) = + mpsc::sync_channel::(NET_COMPLETION_QUEUE); + let worker_name = format!("net-{app_id}"); + std::thread::Builder::new() + .name(worker_name) + .stack_size(NET_WORKER_STACK_BYTES) + .spawn(move || { + while let Ok(mut work) = request_rx.recv() { + let handle = work.request.handle; + let completion = match remaining_timeout_ms(work.deadline) { + Ok(remaining_ms) => { + work.request.timeout_ms = work.request.timeout_ms.min(remaining_ms); + services + .http(&app_id, work.request, work.deadline) + .unwrap_or_else(|failure| TransportCompletion::Error { + handle, + failure, + }) + } + Err(failure) => TransportCompletion::Error { handle, failure }, + }; + if completion_tx.send(completion).is_err() { + break; + } + } + }) + .context("start App NET worker")?; + Ok(Self { + requests: request_tx, + completions: completion_rx, + cancelled: BTreeSet::new(), + action_deadline, + }) + } +} + +impl HttpTransport for AppNetTransport { + fn start(&mut self, mut request: HttpRequest) -> std::result::Result<(), NetFailure> { + let deadline = self.action_deadline.get().ok_or_else(|| { + NetFailure::new("unavailable", "HTTP request has no active App Data Action") + })?; + let remaining_ms = remaining_timeout_ms(deadline)?; + request.timeout_ms = request.timeout_ms.min(remaining_ms); + self.requests + .try_send(AppHttpRequest { request, deadline }) + .map_err(|_| NetFailure::new("busy", "native HTTP worker queue is full")) + } + + fn cancel(&mut self, handle: i32) { + self.cancelled.insert(handle); + } + + fn drain(&mut self, completions: &mut Vec) { + while let Ok(completion) = self.completions.try_recv() { + let handle = match &completion { + TransportCompletion::Done { handle, .. } + | TransportCompletion::Error { handle, .. } => *handle, + }; + if !self.cancelled.remove(&handle) { + completions.push(completion); + } + } + } +} + +fn remaining_timeout_ms(deadline: Instant) -> std::result::Result { + let remaining = deadline + .checked_duration_since(Instant::now()) + .filter(|duration| !duration.is_zero()) + .ok_or_else(|| NetFailure::new("timeout", "App Data Action deadline expired"))?; + Ok(remaining.as_millis().clamp(1, u128::from(u32::MAX)) as u32) +} + +struct DataActionRuntime { + guest: Guest, + net: Option>, + action_deadline: Rc>>, + _database: SharedDb, + _revision: AppRevision, +} + +impl DataActionRuntime { + fn load(config: &DataAppConfig, services: Arc) -> Result { + let guest = Guest::new()?; + mount_shared_db(&guest, config.database.clone())?; + let action_deadline = Rc::new(Cell::new(None)); + mount_data_lifecycle(&guest, config.revision.clone(), action_deadline.clone())?; + let net = if config.net { + let surface = NetSurface::new(AppNetTransport::start( + config.app_id.clone(), + services, + action_deadline.clone(), + )?); + surface.mount(&guest)?; + Some(surface) + } else { + mount_services( + &guest, + config.app_id.clone(), + services, + action_deadline.clone(), + )?; + None + }; + let source = std::fs::read_to_string(&config.source_path) + .with_context(|| format!("read {} Data Action", config.app_id))?; + guest.eval(&format!("{}-data-action", config.app_id), &source)?; + anyhow::ensure!( + guest.with(|ctx| ctx.globals().get::<_, Object>("PocketPiData").is_ok()), + "{} Data Action installed no PocketPiData", + config.app_id + ); + Ok(Self { + guest, + net, + action_deadline, + _database: config.database.clone(), + _revision: config.revision.clone(), + }) + } + + fn invoke(&self, request: &DataActionRequest) -> Result { + anyhow::ensure!( + Instant::now() < request.deadline, + "{} timed out before its Data Action started", + request.name + ); + self.action_deadline.set(Some(request.deadline)); + let result = if let Some(net) = &self.net { + self.invoke_net(request, net) + } else { + let method = match request.kind { + DataActionKind::Task => "invokeTask", + DataActionKind::Tool => "invokeTool", + }; + let line: Result = self.guest.with(|ctx| { + let data: Object = ctx + .globals() + .get("PocketPiData") + .map_err(|error| anyhow!("PocketPiData missing: {error}"))?; + let function: Function = data + .get(method) + .map_err(|error| anyhow!("PocketPiData.{method} missing: {error}"))?; + function + .call::<_, String>((request.name.clone(), request.args.to_string())) + .catch(&ctx) + .map_err(|error| anyhow!("PocketPiData.{method}: {error}")) + }); + line.and_then(|line| parse_data_result(&line)) + }; + self.action_deadline.set(None); + result + } + + fn invoke_net( + &self, + request: &DataActionRequest, + net: &NetSurface, + ) -> Result { + let method = match request.kind { + DataActionKind::Task => "beginInvokeTask", + DataActionKind::Tool => "beginInvokeTool", + }; + self.guest.with(|ctx| -> Result<()> { + let data: Object = ctx.globals().get("PocketPiData")?; + let function: Function = data.get(method)?; + function.call::<_, ()>((request.name.clone(), request.args.to_string()))?; + Ok(()) + })?; + loop { + net.begin_tick(); + let line = self.guest.with(|ctx| -> Result> { + let data: Object = ctx.globals().get("PocketPiData")?; + let tick: Function = data.get("tick")?; + tick.call::<_, ()>(())?; + let poll: Function = data.get("pollResult")?; + Ok(poll.call::<_, Option>(())?) + })?; + self.guest.drain_jobs(); + if let Some(line) = line { + return parse_data_result(&line); + } + anyhow::ensure!( + Instant::now() < request.deadline, + "PocketPiData.{method} timed out" + ); + yield_scheduler_tick(); + } + } +} + +fn parse_data_result(line: &str) -> Result { + let value: Value = serde_json::from_str(line).context("parse Data Action result")?; + Ok(ToolResult { + text: value + .get("text") + .and_then(Value::as_str) + .unwrap_or(line) + .to_owned(), + details: value.get("details").cloned().unwrap_or(Value::Null), + is_error: value + .get("isError") + .and_then(Value::as_bool) + .unwrap_or(false), + terminate: false, + }) +} + +struct AppDataRunner { + tx: mpsc::SyncSender, + next_run_id: AtomicU32, + busy: Arc, +} + +impl AppDataRunner { + fn start(configs: Vec, services: Arc) -> Result { + let (tx, rx) = mpsc::sync_channel::(DATA_ACTION_QUEUE); + let busy = Arc::new(AtomicBool::new(false)); + let worker_busy = busy.clone(); + std::thread::Builder::new() + .name("app-data".to_owned()) + .stack_size(DATA_ACTION_STACK_BYTES) + .spawn(move || { + let configs = configs + .into_iter() + .map(|config| (config.app_id.clone(), config)) + .collect::>(); + let mut runtimes = BTreeMap::::new(); + while let Ok(request) = rx.recv() { + worker_busy.store(true, Ordering::Release); + let result = (|| -> Result { + if !runtimes.contains_key(&request.app_id) { + let config = configs + .get(&request.app_id) + .ok_or_else(|| anyhow!("{} has no Data Action", request.app_id))?; + runtimes.insert( + request.app_id.clone(), + DataActionRuntime::load(config, services.clone())?, + ); + } + runtimes + .get(&request.app_id) + .expect("Data Action runtime inserted") + .invoke(&request) + })(); + if result.is_err() && Instant::now() >= request.deadline { + // Drop the timed-out Guest and its pending promises. + // The next request gets a clean runtime and NET worker. + runtimes.remove(&request.app_id); + } + let result = match result { + Ok(result) if result.is_error => { + log::warn!( + "App Data Action run={} {}.{} failed: {}", + request.run_id, + request.app_id, + request.name, + result.text + ); + result + } + Ok(result) => result, + Err(error) => { + log::error!( + "App Data Action run={} {}.{} crashed: {error:#}", + request.run_id, + request.app_id, + request.name + ); + tool_error(format!("{}: {error:#}", request.name)) + } + }; + if let Some(response) = request.response { + let _ = response.send(result); + } + worker_busy.store(false, Ordering::Release); + } + }) + .context("start App Data Action runner")?; + Ok(Self { + tx, + next_run_id: AtomicU32::new(1), + busy, + }) + } + + fn enqueue( + &self, + app_id: &str, + kind: DataActionKind, + name: &str, + args: Value, + deadline: Instant, + response: Option>, + ) -> Result { + let run_id = u64::from(self.next_run_id.fetch_add(1, Ordering::Relaxed)); + self.tx + .try_send(DataActionRequest { + run_id, + app_id: app_id.to_owned(), + kind, + name: name.to_owned(), + args, + deadline, + response, + }) + .map_err(|error| anyhow!("queue App Data Action: {error}"))?; + Ok(run_id) + } + + fn busy(&self) -> bool { + self.busy.load(Ordering::Acquire) + } +} + +struct AppRuntime { + guest: Guest, + surface: UiSurface, + _fs: Rc>, + _db: SharedDb, + revision: AppRevision, + last_seen_revision: Cell, + #[cfg(test)] + projection_refreshes: Cell, +} + +impl AppRuntime { + fn load( + app: &CatalogApp, + release_dir: &Path, + fs_root: &Path, + tmp_root: &Path, + db: SharedDb, + revision: AppRevision, + ) -> Result { + let descriptor: AppDescriptor = serde_json::from_slice( + &std::fs::read(release_dir.join("agent-app.json")).context("read agent-app.json")?, + ) + .context("parse installed agent-app.json")?; + anyhow::ensure!( + descriptor.id == app.descriptor.id, + "installed App id mismatch" + ); + anyhow::ensure!( + descriptor.version == app.descriptor.version, + "installed App version mismatch" + ); + let manifest: Value = serde_json::from_slice( + &std::fs::read(release_dir.join("pocket.json")).context("read pocket.json")?, + ) + .context("parse installed pocket.json")?; + anyhow::ensure!( + manifest.get("pocket").and_then(Value::as_u64) == Some(2), + "App requires pocket.json v2" + ); + anyhow::ensure!( + manifest.get("name").and_then(Value::as_str) == Some(app.descriptor.id.as_str()), + "pocket.json name does not match App id" + ); + for capability in manifest + .pointer("/engine/capabilities/requires") + .and_then(Value::as_array) + .into_iter() + .flatten() + .filter_map(Value::as_str) + { + anyhow::ensure!( + matches!(capability, "data.fs" | "data.sqlite" | "net.http"), + "unsupported App capability: {capability}" + ); + } + + std::fs::create_dir_all(fs_root)?; + let guest = Guest::new()?; + let surface = UiSurface::new(VIEWPORT); + let pak = std::fs::read(release_dir.join("app.pak")).context("read App pak")?; + surface.feed_pak(&pak); + surface.mount(&guest)?; + + let fs = Rc::new(RefCell::new(FsModule::with_quota( + FsStorage::Dir { + root: fs_root.to_owned(), + tmp: tmp_root.to_owned(), + }, + 2 * 1024 * 1024, + ))); + pocket_fs::mount(&guest, fs.clone())?; + mount_shared_db(&guest, db.clone())?; + + let source = + std::fs::read_to_string(release_dir.join("app.js")).context("read App bundle")?; + eval_bundle(&guest, &descriptor.id, &source)?; + anyhow::ensure!( + guest.has_frame(), + "{} bundle installed no frame()", + descriptor.id + ); + + let last_seen_revision = revision.load(Ordering::Acquire); + Ok(Self { + guest, + surface, + _fs: fs, + _db: db, + revision, + last_seen_revision: Cell::new(last_seen_revision), + #[cfg(test)] + projection_refreshes: Cell::new(0), + }) + } + + fn projection_is_stale(&self) -> bool { + self.revision.load(Ordering::Acquire) != self.last_seen_revision.get() + } + + fn advance(&self, render_surface: bool) -> Result<()> { + // A normal frame never queries SQLite. It only compares one in-memory + // App revision and lets the View refresh a bounded projection when a + // committed Data Action made that revision stale. + self.call_method("tick", ()).map(|_: String| ())?; + if render_surface { + let current_revision = self.revision.load(Ordering::Acquire); + if current_revision != self.last_seen_revision.get() { + // The counter is sampled once at the foreground frame + // boundary. Any number of commits since the previous frame is + // therefore coalesced into one bounded projection refresh. + // A commit racing this query remains visible as a newer + // revision and is picked up on the following frame. + self.call_method::<_, String>( + "dataChanged", + (json!([{"topic":"app","revision":current_revision}]).to_string(),), + )?; + self.last_seen_revision.set(current_revision); + #[cfg(test)] + self.projection_refreshes + .set(self.projection_refreshes.get().saturating_add(1)); + } + } + if render_surface { + self.guest.frame(0)?; + self.surface.tick(); + } + Ok(()) + } + + fn update(&self, projection: &Value) -> Result<()> { + self.call_method("update", (projection.to_string(),)) + .map(|_: String| ()) + } + + fn tap(&self, x: u16, y: u16) -> Result { + let line: String = self.call_method("tap", (x as i32, y as i32))?; + if line.is_empty() { + return Ok(Value::Null); + } + serde_json::from_str(&line).context("parse App tap action") + } + + fn pointer_down(&self, x: u16, y: u16) -> Result<()> { + self.call_optional_method("pointerDown", (x as i32, y as i32)) + } + + fn pointer_up(&self) -> Result<()> { + self.call_optional_method("pointerUp", ()) + } + + fn with_ui(&self, f: impl FnOnce(&mut pocketjs_core::Ui) -> R) -> R { + self.surface.with_ui(f) + } + + fn call_method(&self, name: &str, args: A) -> Result + where + A: for<'js> pocket_mod::qjs::function::IntoArgs<'js>, + R: for<'js> pocket_mod::qjs::FromJs<'js>, + { + self.guest.with(|ctx| { + let app: Object = ctx + .globals() + .get("PocketPiApp") + .map_err(|error| anyhow!("PocketPiApp missing: {error}"))?; + let function: Function = app + .get(name) + .map_err(|error| anyhow!("PocketPiApp.{name} missing: {error}"))?; + function + .call::<_, R>(args) + .catch(&ctx) + .map_err(|error| anyhow!("PocketPiApp.{name}: {error}")) + }) + } + + fn call_optional_method(&self, name: &str, args: A) -> Result<()> + where + A: for<'js> pocket_mod::qjs::function::IntoArgs<'js>, + { + self.guest.with(|ctx| { + let app: Object = ctx + .globals() + .get("PocketPiApp") + .map_err(|error| anyhow!("PocketPiApp missing: {error}"))?; + let Some(function) = app.get::<_, Function>(name).ok() else { + return Ok(()); + }; + function + .call::<_, String>(args) + .catch(&ctx) + .map_err(|error| anyhow!("PocketPiApp.{name}: {error}"))?; + Ok(()) + }) + } +} + +#[cfg(not(target_os = "espidf"))] +fn eval_bundle(guest: &Guest, label: &str, source: &str) -> Result<()> { + guest.eval(label, source) +} + +#[cfg(target_os = "espidf")] +fn eval_bundle(guest: &Guest, label: &str, source: &str) -> Result<()> { + guest.eval(label, source) +} + +fn mount_shared_db(guest: &Guest, db: SharedDb) -> Result<()> { + guest.mount("db", |ctx, ns| { + let module = db.clone(); + ns.set( + "open", + Function::new(ctx.clone(), move |name: String| -> i32 { + module.lock().map_or(-1, |mut db| db.open(&name)) + })?, + )?; + let module = db.clone(); + ns.set( + "close", + Function::new(ctx.clone(), move |handle: i32| { + if let Ok(mut db) = module.lock() { + db.close(handle); + } + })?, + )?; + let module = db.clone(); + ns.set( + "exec", + Function::new(ctx.clone(), move |handle: i32, sql: String| -> i32 { + let result = module.lock().map_or(1, |mut db| db.exec(handle, &sql)); + yield_after_db_call(); + result + })?, + )?; + let module = db.clone(); + ns.set( + "query", + Function::new( + ctx.clone(), + move |handle: i32, sql: String, args: String| -> String { + let result = module.lock().map_or_else( + |_| json!({"error":"App database owner is unavailable"}).to_string(), + |mut db| db.query(handle, &sql, &args), + ); + yield_after_db_call(); + result + }, + )?, + )?; + let module = db; + ns.set( + "lastError", + Function::new(ctx.clone(), move |handle: i32| -> String { + module.lock().map_or_else( + |_| "App database owner is unavailable".to_owned(), + |db| db.last_error(handle), + ) + })?, + )?; + Ok(()) + }) +} + +fn mount_services( + guest: &Guest, + app_id: String, + services: Arc, + action_deadline: Rc>>, +) -> Result<()> { + guest.mount("services", |ctx, ns| { + ns.set( + "call", + Function::new( + ctx.clone(), + move |service: String, operation: String, args_json: String| -> String { + let args = serde_json::from_str(&args_json).unwrap_or(Value::Null); + let result = action_deadline + .get() + .ok_or_else(|| "App service call has no active Data Action".to_owned()) + .and_then(|deadline| { + if Instant::now() >= deadline { + Err("App Data Action deadline expired".to_owned()) + } else { + services.call(&app_id, &service, &operation, &args, deadline) + } + }); + match result { + Ok(value) => json!({"ok":true,"value":value}).to_string(), + Err(error) => json!({"ok":false,"error":error}).to_string(), + } + }, + )?, + )?; + Ok(()) + }) +} + +#[cfg(target_os = "espidf")] +fn yield_scheduler_tick() { + unsafe extern "C" { + fn vTaskDelay(ticks: u32); + } + // The firmware runs FreeRTOS at 100 Hz. Block through one scheduler tick + // instead of sub-tick polling so the IDLE task gets a scheduling chance. + unsafe { vTaskDelay(1) }; +} + +#[cfg(not(target_os = "espidf"))] +fn yield_scheduler_tick() { + std::thread::sleep(Duration::from_millis(5)); +} + +#[cfg(target_os = "espidf")] +fn yield_after_db_call() { + yield_scheduler_tick(); +} + +#[cfg(not(target_os = "espidf"))] +fn yield_after_db_call() {} + +fn mount_data_lifecycle( + guest: &Guest, + revision: AppRevision, + action_deadline: Rc>>, +) -> Result<()> { + guest.mount("app", |ctx, ns| { + ns.set( + "commit", + Function::new(ctx.clone(), move || -> f64 { + // Called only after a successful App-owned SQLite COMMIT. + // Release pairs with the foreground View's Acquire load. + revision.fetch_add(1, Ordering::Release).saturating_add(1) as f64 + })?, + )?; + ns.set( + "remainingMs", + Function::new(ctx.clone(), move || -> f64 { + action_deadline.get().map_or(1, |deadline| { + deadline + .checked_duration_since(Instant::now()) + .map_or(1, |remaining| { + remaining.as_millis().clamp(1, u128::from(u32::MAX)) as u32 + }) + }) as f64 + })?, + )?; + Ok(()) + }) +} + +pub struct AppSupervisor { + workspace: PathBuf, + catalog: AppCatalog, + services: Arc, + data_runner: AppDataRunner, + /// The Pi Agent System App is booted once and remains resident for the + /// entire supervisor lifetime. Foreground navigation never replaces it. + system: AppRuntime, + agent: Option, + /// The small v1 catalog is loaded once at supervisor startup. Background + /// Data Actions do not advance these Views, and navigation only selects + /// an already resident surface. + runtimes: BTreeMap, + active_app: Option, + schedules: AppScheduleStore, +} + +impl AppSupervisor { + pub fn new( + workspace: impl Into, + catalog: AppCatalog, + services: Arc, + ) -> Result { + let workspace = workspace.into(); + seed_builtin_releases(&workspace, &catalog)?; + let schedules = AppScheduleStore::load(&workspace, &catalog)?; + let mut databases = BTreeMap::new(); + let mut revisions = BTreeMap::new(); + for descriptor in catalog.descriptors() { + let (_, _, db_root, _) = paths(&workspace, &descriptor.id); + std::fs::create_dir_all(&db_root)?; + reset_development_database(&workspace, descriptor, &db_root)?; + databases.insert( + descriptor.id.clone(), + Arc::new(Mutex::new(DbModule::new(DbStorage::Dir(db_root)))), + ); + revisions.insert(descriptor.id.clone(), Arc::new(AtomicU32::new(0))); + } + let data_configs = catalog + .apps + .values() + .filter_map(|app| { + let descriptor = &app.descriptor; + let (release_dir, _, _, _) = paths(&workspace, &descriptor.id); + let source_path = release_dir.join("data-action.js"); + source_path.is_file().then(|| DataAppConfig { + app_id: descriptor.id.clone(), + source_path, + database: databases + .get(&descriptor.id) + .expect("database created for descriptor") + .clone(), + revision: revisions + .get(&descriptor.id) + .expect("revision created for descriptor") + .clone(), + net: serde_json::from_str::(app.bundle.pocket_json) + .ok() + .and_then(|manifest| { + manifest + .pointer("/engine/capabilities/requires") + .and_then(Value::as_array) + .cloned() + }) + .is_some_and(|capabilities| { + capabilities.iter().any(|value| value == "net.http") + }), + }) + }) + .collect(); + let data_runner = AppDataRunner::start(data_configs, services.clone())?; + log::info!("preloading View Runtime: {ROOT_APP_ID}"); + let system = load_runtime(&workspace, &catalog, &databases, &revisions, ROOT_APP_ID)?; + log::info!("preloaded View Runtime: {ROOT_APP_ID}"); + // v1 has a small fixed App catalog. Load every ordinary View once at + // boot so foreground navigation is only a surface switch. Background + // Data Actions remain separate and are still loaded on demand. + let ordinary_app_ids = catalog + .descriptors() + .filter(|descriptor| descriptor.id != ROOT_APP_ID) + .map(|descriptor| descriptor.id.clone()) + .collect::>(); + let mut runtimes = BTreeMap::new(); + for app_id in ordinary_app_ids { + log::info!("preloading View Runtime: {app_id}"); + let runtime = load_runtime(&workspace, &catalog, &databases, &revisions, &app_id)?; + log::info!("preloaded View Runtime: {app_id}"); + runtimes.insert(app_id, runtime); + } + Ok(Self { + workspace, + catalog, + services, + data_runner, + system, + agent: None, + runtimes, + active_app: None, + schedules, + }) + } + + pub fn catalog(&self) -> &AppCatalog { + &self.catalog + } + + pub fn services_busy(&self) -> bool { + self.services.busy() || self.data_runner.busy() + } + + pub fn active_id(&self) -> &str { + self.active_app.as_deref().unwrap_or(ROOT_APP_ID) + } + + pub fn open(&mut self, app_id: &str) -> Result<()> { + if self.active_id() == app_id { + return Ok(()); + } + if app_id == ROOT_APP_ID { + self.active_app = None; + return Ok(()); + } + + anyhow::ensure!(self.runtimes.contains_key(app_id), "unknown App: {app_id}"); + self.active_app = Some(app_id.to_owned()); + Ok(()) + } + + pub fn boot_agent( + &mut self, + config_json: &str, + backend: Arc, + tools: Arc, + ) -> Result<()> { + anyhow::ensure!( + self.agent.is_none(), + "Pi Agent System App is already booted" + ); + let release = paths(&self.workspace, ROOT_APP_ID).0; + let agent_source = std::fs::read_to_string(release.join("agent.js")) + .context("read Pi Agent System App loop bundle")?; + self.agent = Some( + GuestAgent::mount_source( + &self.system.guest, + config_json, + backend, + tools, + &agent_source, + ) + .map_err(|error| anyhow!(error))?, + ); + Ok(()) + } + + pub fn prompt_agent(&self, text: &str) -> Result<()> { + let agent = self + .agent + .as_ref() + .ok_or_else(|| anyhow!("Pi Agent System App is not booted"))?; + agent + .prompt(&self.system.guest, text) + .map_err(|error| anyhow!(error)) + } + + /// Advance the resident System App and render the selected View. + pub fn frame(&self) -> Result> { + self.frame_render(true) + } + + /// Advance the Agent every host tick, but only ask the selected PocketJS + /// View to produce a new DrawList when the host knows it is dirty. + pub fn frame_render(&self, render_selected: bool) -> Result> { + let events = match &self.agent { + Some(agent) => agent + .tick(&self.system.guest) + .map_err(|error| anyhow!(error))?, + None => Vec::new(), + }; + self.system + .advance(render_selected && self.active_app.is_none())?; + for (app_id, runtime) in &self.runtimes { + runtime + .advance(render_selected && self.active_app.as_deref() == Some(app_id.as_str()))?; + } + Ok(events) + } + + pub fn update_root(&self, projection: &Value) -> Result<()> { + self.system.update(projection) + } + + pub fn tap(&self, x: u16, y: u16) -> Result { + self.active().tap(x, y) + } + + pub fn pointer_down(&self, x: u16, y: u16) -> Result<()> { + self.active().pointer_down(x, y) + } + + pub fn pointer_up(&self) -> Result<()> { + self.active().pointer_up() + } + + pub fn with_ui(&self, f: impl FnOnce(&mut pocketjs_core::Ui) -> R) -> R { + self.active().with_ui(f) + } + + /// A single atomic comparison lets the host wake the active View after a + /// background commit. It never queries SQLite and closed Apps stay idle. + pub fn active_projection_is_stale(&self) -> bool { + self.active().projection_is_stale() + } + + fn begin_agent_tool( + &self, + name: &str, + args_json: &str, + deadline: Instant, + response: mpsc::Sender, + ) { + let Some(app_id) = self.catalog.app_for_tool(name).map(str::to_owned) else { + let _ = response.send(tool_error(format!("unknown App tool: {name}"))); + return; + }; + let args = serde_json::from_str(args_json).unwrap_or(Value::Null); + match self.data_runner.enqueue( + &app_id, + DataActionKind::Tool, + name, + args, + deadline, + Some(response.clone()), + ) { + Ok(_) => {} + Err(error) => { + let _ = response.send(tool_error(format!("{name}: {error:#}"))); + } + } + } + + /// Runs a task requested by the currently visible App. The host calls this + /// after presenting the App's immediate pressed/loading state so slow + /// native services never hide touch feedback. + pub fn invoke_active_task(&mut self, name: &str, args: &Value) -> ToolResult { + let app_id = self.active_id().to_owned(); + match self.data_runner.enqueue( + &app_id, + DataActionKind::Task, + name, + args.clone(), + new_action_deadline(), + None, + ) { + Ok(run_id) => ToolResult { + text: format!("Queued {app_id}.{name} as App Data Action {run_id}"), + details: json!({"status":"queued","runId":run_id,"app":app_id}), + is_error: false, + terminate: false, + }, + Err(error) => tool_error(format!("{app_id}.{name}: {error:#}")), + } + } + + pub fn poll_due_tasks(&mut self) -> Vec<(String, ToolResult)> { + let mut results = Vec::new(); + while let Some(due) = self.schedules.claim_due() { + let label = format!("{}.{}", due.app_id, due.task); + let result = match self.data_runner.enqueue( + &due.app_id, + DataActionKind::Task, + &due.task, + due.args.clone(), + new_action_deadline(), + None, + ) { + Ok(run_id) => ToolResult { + text: format!("Queued {label} as App Data Action {run_id}"), + details: json!({"status":"queued","runId":run_id}), + is_error: false, + terminate: false, + }, + Err(error) => tool_error(format!("{label}: {error:#}")), + }; + self.schedules.finish(&due, !result.is_error); + results.push((label, result)); + } + results + } + + fn active(&self) -> &AppRuntime { + self.active_app + .as_deref() + .and_then(|app_id| self.runtimes.get(app_id)) + .unwrap_or(&self.system) + } +} + +fn load_runtime( + workspace: &Path, + catalog: &AppCatalog, + databases: &BTreeMap, + revisions: &BTreeMap, + app_id: &str, +) -> Result { + let app = catalog + .app(app_id) + .ok_or_else(|| anyhow!("unknown App: {app_id}"))?; + let (release_dir, fs_root, db_root, tmp_root) = paths(workspace, app_id); + let db = databases + .get(app_id) + .cloned() + .ok_or_else(|| anyhow!("App {app_id} has no database owner"))?; + let revision = revisions + .get(app_id) + .cloned() + .ok_or_else(|| anyhow!("App {app_id} has no revision owner"))?; + let _ = db_root; + AppRuntime::load(app, &release_dir, &fs_root, &tmp_root, db, revision) + .with_context(|| format!("load App {app_id}")) +} + +fn paths(workspace: &Path, app_id: &str) -> (PathBuf, PathBuf, PathBuf, PathBuf) { + if app_id == ROOT_APP_ID { + ( + workspace.join("data/view/releases").join(BUILTIN_RELEASE), + workspace.to_owned(), + workspace.join("data"), + workspace.join(".system/tmp/pi-agent"), + ) + } else { + let root = workspace.join("apps").join(app_id); + ( + root.join("releases").join(BUILTIN_RELEASE), + root.join("data"), + root.join("data"), + root.join("tmp"), + ) + } +} + +fn reset_development_database( + workspace: &Path, + descriptor: &AppDescriptor, + db_root: &Path, +) -> Result<()> { + if descriptor.data_version == 0 { + return Ok(()); + } + let marker = workspace + .join("apps") + .join(&descriptor.id) + .join(".data-version"); + let expected = descriptor.data_version.to_string(); + if std::fs::read_to_string(&marker).is_ok_and(|value| value == expected) { + return Ok(()); + } + let database = db_root.join(format!("{}.sqlite", descriptor.id)); + for path in [ + database.clone(), + database.with_extension("sqlite-wal"), + database.with_extension("sqlite-shm"), + database.with_extension("sqlite-journal"), + ] { + match std::fs::remove_file(&path) { + Ok(()) => {} + Err(error) if error.kind() == std::io::ErrorKind::NotFound => {} + Err(error) => return Err(error).with_context(|| format!("reset {}", path.display())), + } + } + atomic_write(&marker, expected.as_bytes()) +} + +fn seed_builtin_releases(workspace: &Path, catalog: &AppCatalog) -> Result<()> { + for app in catalog.apps.values() { + let (release_dir, fs_root, db_root, tmp_root) = paths(workspace, &app.descriptor.id); + std::fs::create_dir_all(&release_dir)?; + std::fs::create_dir_all(&fs_root)?; + std::fs::create_dir_all(&db_root)?; + std::fs::create_dir_all(&tmp_root)?; + atomic_write(&release_dir.join("app.js"), app.bundle.js.as_bytes())?; + if let Some(data_js) = app.bundle.data_js { + atomic_write(&release_dir.join("data-action.js"), data_js.as_bytes())?; + } + if let Some(agent_js) = app.bundle.agent_js { + atomic_write(&release_dir.join("agent.js"), agent_js.as_bytes())?; + } + atomic_write(&release_dir.join("app.pak"), app.bundle.pak)?; + atomic_write( + &release_dir.join("agent-app.json"), + app.bundle.descriptor_json.as_bytes(), + )?; + atomic_write( + &release_dir.join("pocket.json"), + app.bundle.pocket_json.as_bytes(), + )?; + let manifest: Value = serde_json::from_str(app.bundle.pocket_json)?; + let modules = manifest + .pointer("/engine/capabilities/requires") + .and_then(Value::as_array) + .cloned() + .unwrap_or_default(); + atomic_write( + &release_dir.join("plan.json"), + &serde_json::to_vec_pretty(&json!({ + "runtime":"pocket-pi-agentos", + "pocketjsRevision":"9c809bbd047ddc75c27caa4990951a78d942477a", + "app":app.descriptor.id, + "modules":modules + }))?, + )?; + let current = if app.descriptor.id == ROOT_APP_ID { + workspace.join("data/view/current") + } else { + workspace + .join("apps") + .join(&app.descriptor.id) + .join("current") + }; + atomic_write(¤t, BUILTIN_RELEASE.as_bytes())?; + } + Ok(()) +} + +fn atomic_write(path: &Path, bytes: &[u8]) -> Result<()> { + if std::fs::read(path).ok().as_deref() == Some(bytes) { + return Ok(()); + } + let parent = path.parent().ok_or_else(|| anyhow!("path has no parent"))?; + std::fs::create_dir_all(parent)?; + let temporary = parent.join(format!( + ".{}.tmp", + path.file_name() + .and_then(|name| name.to_str()) + .unwrap_or("release") + )); + { + use std::io::Write as _; + let mut file = std::fs::File::create(&temporary)?; + file.write_all(bytes)?; + file.sync_all()?; + } + std::fs::rename(&temporary, path)?; + Ok(()) +} + +pub struct AppToolRequest { + name: String, + args_json: String, + deadline: Instant, + response: mpsc::Sender, +} + +pub struct RoutedToolHost { + native: Arc, + catalog: AppCatalog, + app_tx: mpsc::Sender, +} + +impl RoutedToolHost { + pub fn new( + native: Arc, + catalog: AppCatalog, + ) -> (Self, mpsc::Receiver) { + let (app_tx, app_rx) = mpsc::channel(); + ( + Self { + native, + catalog, + app_tx, + }, + app_rx, + ) + } +} + +impl ToolHost for RoutedToolHost { + fn definitions(&self) -> Vec { + let mut definitions = self.native.definitions(); + definitions.extend(self.catalog.tool_definitions()); + definitions + } + + fn execute(&self, call_id: &str, name: &str, args_json: &str) -> ToolResult { + if self.catalog.app_for_tool(name).is_none() { + return self.native.execute(call_id, name, args_json); + } + let (response, response_rx) = mpsc::channel(); + let deadline = new_action_deadline(); + if self + .app_tx + .send(AppToolRequest { + name: name.to_owned(), + args_json: args_json.to_owned(), + deadline, + response, + }) + .is_err() + { + return tool_error("App Supervisor is unavailable"); + } + loop { + if Instant::now() >= deadline { + return tool_error(format!("App Tool timed out after {APP_ACTION_TIMEOUT:?}")); + } + match response_rx.try_recv() { + Ok(result) => return result, + Err(mpsc::TryRecvError::Disconnected) => { + return tool_error("App Data Action response channel closed"); + } + Err(mpsc::TryRecvError::Empty) => yield_scheduler_tick(), + } + } + } +} + +impl AppToolRequest { + pub fn handle(self, supervisor: &AppSupervisor) { + supervisor.begin_agent_tool(&self.name, &self.args_json, self.deadline, self.response); + } +} + +fn tool_error(text: impl Into) -> ToolResult { + ToolResult { + text: text.into(), + details: Value::Null, + is_error: true, + terminate: false, + } +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +struct StoredSchedule { + app_id: String, + schedule_id: String, + task: String, + args: Value, + every_seconds: u64, + next_run_at: u64, + last_ok: Option, +} + +struct DueTask { + app_id: String, + schedule_id: String, + task: String, + args: Value, + scheduled_at: u64, +} + +struct AppScheduleStore { + paths: BTreeMap, + schedules: Vec, +} + +impl AppScheduleStore { + fn load(workspace: &Path, catalog: &AppCatalog) -> Result { + // AppTask declarations travel with the App release, while their + // mutable scheduler cursor belongs to that App's private data root. + let now = unix_seconds(); + let mut paths = BTreeMap::new(); + let mut schedules = Vec::new(); + for app in catalog.descriptors() { + let path = if app.id == ROOT_APP_ID { + workspace.join("data/.system/schedules.json") + } else { + workspace + .join("apps") + .join(&app.id) + .join("data/.system/schedules.json") + }; + let mut prior: Vec = std::fs::read(&path) + .ok() + .and_then(|bytes| serde_json::from_slice(&bytes).ok()) + .unwrap_or_default(); + paths.insert(app.id.clone(), path); + for declaration in &app.schedules { + anyhow::ensure!( + app.tasks.iter().any(|task| task == &declaration.task), + "{}.{} schedule references missing task {}", + app.id, + declaration.id, + declaration.task + ); + let every_seconds = declaration.every_minutes.saturating_mul(60).max(60); + let existing = prior + .iter_mut() + .find(|item| item.app_id == app.id && item.schedule_id == declaration.id); + schedules.push(match existing { + Some(item) + if item.task == declaration.task && item.every_seconds == every_seconds => + { + item.clone() + } + _ => StoredSchedule { + app_id: app.id.clone(), + schedule_id: declaration.id.clone(), + task: declaration.task.clone(), + args: declaration.args.clone(), + every_seconds, + next_run_at: now.saturating_add(every_seconds), + last_ok: None, + }, + }); + } + } + let store = Self { paths, schedules }; + store.persist()?; + Ok(store) + } + + fn claim_due(&mut self) -> Option { + let now = unix_seconds(); + let item = self + .schedules + .iter_mut() + .filter(|item| item.next_run_at <= now) + .min_by_key(|item| item.next_run_at)?; + let scheduled_at = item.next_run_at; + item.next_run_at = next_schedule_run(item.next_run_at, item.every_seconds, now); + let due = DueTask { + app_id: item.app_id.clone(), + schedule_id: item.schedule_id.clone(), + task: item.task.clone(), + args: item.args.clone(), + scheduled_at, + }; + let _ = self.persist(); + Some(due) + } + + fn finish(&mut self, due: &DueTask, ok: bool) { + if let Some(item) = self + .schedules + .iter_mut() + .find(|item| item.app_id == due.app_id && item.schedule_id == due.schedule_id) + { + item.last_ok = Some(ok); + } + log::info!( + "AppTask {}.{} scheduled_at={} ok={ok}", + due.app_id, + due.task, + due.scheduled_at + ); + let _ = self.persist(); + } + + fn persist(&self) -> Result<()> { + for (app_id, path) in &self.paths { + let schedules = self + .schedules + .iter() + .filter(|schedule| &schedule.app_id == app_id) + .collect::>(); + atomic_write(path, &serde_json::to_vec(&schedules)?)?; + } + Ok(()) + } +} + +fn unix_seconds() -> u64 { + SystemTime::now() + .duration_since(UNIX_EPOCH) + .map(|duration| duration.as_secs()) + .unwrap_or(0) +} + +fn next_schedule_run(current: u64, interval: u64, now: u64) -> u64 { + let elapsed_intervals = now.saturating_sub(current) / interval; + current.saturating_add(interval.saturating_mul(elapsed_intervals.saturating_add(1))) +} + +#[cfg(test)] +mod tests { + use super::*; + + struct NoServices; + + impl AppServiceHost for NoServices { + fn call( + &self, + _app_id: &str, + _service: &str, + _operation: &str, + _args: &Value, + _deadline: Instant, + ) -> std::result::Result { + Err("unexpected App service call".into()) + } + } + + struct NoTools; + + impl ToolHost for NoTools { + fn definitions(&self) -> Vec { + Vec::new() + } + + fn execute(&self, _call_id: &str, name: &str, _args_json: &str) -> ToolResult { + tool_error(format!("unexpected native Tool: {name}")) + } + } + + #[test] + fn catalog_uses_each_apps_declared_tool_namespace() { + let catalog = AppCatalog::new([ + fixture( + r#"{"id":"pi-agent","description":"System","version":"1","tools":[],"tasks":[],"schedules":[]}"#, + Some("agent"), + ), + fixture( + r#"{"id":"search","description":"Research","version":"1","toolNamespace":"research","tools":[{"name":"research.query","parameters":{"type":"object"}}],"tasks":[],"schedules":[]}"#, + None, + ), + ]) + .unwrap(); + + assert_eq!(catalog.app_for_tool("research.query"), Some("search")); + assert_eq!(catalog.descriptors().count(), 2); + } + + #[test] + fn catalog_rejects_a_tool_outside_its_declared_namespace() { + let error = AppCatalog::new([ + fixture( + r#"{"id":"pi-agent","description":"System","version":"1","tools":[],"tasks":[],"schedules":[]}"#, + Some("agent"), + ), + fixture( + r#"{"id":"search","description":"Research","version":"1","tools":[{"name":"other.query","parameters":{"type":"object"}}],"tasks":[],"schedules":[]}"#, + None, + ), + ]) + .err() + .unwrap(); + + assert!(error.to_string().contains("non-namespaced tool")); + } + + #[test] + fn catalog_exposes_only_declared_native_policies() { + let catalog = AppCatalog::new([ + fixture( + r#"{"id":"pi-agent","description":"System","version":"1","tools":[],"tasks":[],"schedules":[]}"#, + Some("agent"), + ), + fixture( + r#"{"id":"search","description":"Research","version":"1","tools":[],"tasks":[],"schedules":[],"nativeServices":{"http":[{"method":"POST","urls":["https://example.com/search"],"allowedRequestHeaders":["content-type"],"credential":{"id":"search.api-key","header":"x-api-key"}}],"mcp":[]}}"#, + None, + ), + ]) + .unwrap(); + + assert!(catalog + .http_policy("search", "POST", "https://example.com/search") + .is_some()); + assert!(catalog + .http_policy("search", "GET", "https://example.com/search") + .is_none()); + assert_eq!( + catalog.credential_ids(), + BTreeSet::from(["search.api-key".to_owned()]) + ); + } + + #[test] + fn app_tool_request_carries_the_single_80_second_deadline() { + assert_eq!(APP_ACTION_TIMEOUT, Duration::from_secs(80)); + let catalog = AppCatalog::new([ + fixture( + r#"{"id":"pi-agent","description":"System","version":"1","tools":[],"tasks":[],"schedules":[]}"#, + Some("agent"), + ), + fixture( + r#"{"id":"search","description":"Research","version":"1","toolNamespace":"research","tools":[{"name":"research.query","parameters":{"type":"object"}}],"tasks":[],"schedules":[]}"#, + None, + ), + ]) + .unwrap(); + let (tools, requests) = RoutedToolHost::new(Arc::new(NoTools), catalog); + let call = std::thread::spawn(move || tools.execute("call", "research.query", "{}")); + let request = requests.recv().unwrap(); + let remaining = request.deadline.saturating_duration_since(Instant::now()); + assert!(!remaining.is_zero()); + assert!(remaining <= APP_ACTION_TIMEOUT); + request + .response + .send(ToolResult { + text: "done".into(), + ..ToolResult::default() + }) + .unwrap(); + assert_eq!(call.join().unwrap().text, "done"); + } + + #[test] + fn missed_schedules_advance_in_constant_time() { + let hour = 60 * 60; + let ten_years = 10 * 365 * 24 * hour; + let next = next_schedule_run(hour, hour, ten_years); + assert!(next > ten_years); + assert_eq!(next % hour, 0); + } + + #[test] + fn data_version_resets_only_that_apps_database_once() { + let temp = tempfile::tempdir().unwrap(); + let db_root = temp.path().join("apps/notes/data"); + std::fs::create_dir_all(&db_root).unwrap(); + let database = db_root.join("notes.sqlite"); + std::fs::write(&database, "old-schema").unwrap(); + let mut descriptor = AppDescriptor { + id: "notes".into(), + title: "Notes".into(), + description: "Notes App".into(), + version: "1.0.0".into(), + data_version: 3, + tool_namespace: "notes".into(), + tools: Vec::new(), + provider_operations: Vec::new(), + tasks: Vec::new(), + schedules: Vec::new(), + native_services: NativeServices::default(), + }; + + reset_development_database(temp.path(), &descriptor, &db_root).unwrap(); + assert!(!database.exists()); + std::fs::write(&database, "current-schema").unwrap(); + reset_development_database(temp.path(), &descriptor, &db_root).unwrap(); + assert_eq!( + std::fs::read_to_string(&database).unwrap(), + "current-schema" + ); + + descriptor.data_version = 4; + reset_development_database(temp.path(), &descriptor, &db_root).unwrap(); + assert!(!database.exists()); + } + + #[test] + fn app_revisions_coalesce_at_the_foreground_frame_boundary() { + let temp = tempfile::tempdir().unwrap(); + let catalog = AppCatalog::new([pi_agent_bundle(), exa_bundle()]).unwrap(); + let mut supervisor = + AppSupervisor::new(temp.path(), catalog, Arc::new(NoServices)).unwrap(); + supervisor.open("exa").unwrap(); + + let revision = supervisor.runtimes["exa"].revision.clone(); + revision.fetch_add(1, Ordering::Release); + revision.fetch_add(1, Ordering::Release); + revision.fetch_add(1, Ordering::Release); + assert!(supervisor.active_projection_is_stale()); + + supervisor.frame_render(false).unwrap(); + assert_eq!(supervisor.runtimes["exa"].projection_refreshes.get(), 0); + assert_eq!(supervisor.runtimes["exa"].last_seen_revision.get(), 0); + + supervisor.frame_render(true).unwrap(); + assert_eq!(supervisor.runtimes["exa"].projection_refreshes.get(), 1); + assert_eq!(supervisor.runtimes["exa"].last_seen_revision.get(), 3); + assert!(!supervisor.active_projection_is_stale()); + + for _ in 0..5 { + supervisor.frame_render(true).unwrap(); + } + assert_eq!(supervisor.runtimes["exa"].projection_refreshes.get(), 1); + + supervisor.open(ROOT_APP_ID).unwrap(); + revision.fetch_add(1, Ordering::Release); + revision.fetch_add(1, Ordering::Release); + for _ in 0..3 { + supervisor.frame_render(true).unwrap(); + } + assert_eq!(supervisor.runtimes["exa"].projection_refreshes.get(), 1); + + supervisor.open("exa").unwrap(); + supervisor.frame_render(true).unwrap(); + assert_eq!(supervisor.runtimes["exa"].projection_refreshes.get(), 2); + assert_eq!(supervisor.runtimes["exa"].last_seen_revision.get(), 5); + } + + fn fixture(descriptor_json: &'static str, agent_js: Option<&'static str>) -> EmbeddedApp { + EmbeddedApp::new( + descriptor_json, + r#"{"pocket":2,"name":"fixture","title":"Fixture","version":"1","engine":{"capabilities":{"requires":[]}}}"#, + "", + None, + agent_js, + &[], + ) + } + + fn pi_agent_bundle() -> EmbeddedApp { + EmbeddedApp::new( + include_str!("../../../apps/pi-agent/agent-app.json"), + include_str!("../../../apps/pi-agent/pocket.json"), + include_str!("../../../apps/pi-agent/dist/app.js"), + None, + Some(include_str!("../../../apps/pi-agent/dist/agent.js")), + include_bytes!("../../../apps/pi-agent/dist/app.pak"), + ) + } + + fn exa_bundle() -> EmbeddedApp { + EmbeddedApp::new( + include_str!("../../../apps/exa/agent-app.json"), + include_str!("../../../apps/exa/pocket.json"), + include_str!("../../../apps/exa/dist/app.js"), + Some(include_str!("../../../apps/exa/dist/data-action.js")), + None, + include_bytes!("../../../apps/exa/dist/app.pak"), + ) + } +} diff --git a/crates/pocket-pi-app-pack/Cargo.toml b/crates/pocket-pi-app-pack/Cargo.toml new file mode 100644 index 0000000..ab9b0df --- /dev/null +++ b/crates/pocket-pi-app-pack/Cargo.toml @@ -0,0 +1,18 @@ +[package] +name = "pocket-pi-app-pack" +version = "0.1.0" +edition.workspace = true +license.workspace = true +repository.workspace = true +publish = false +build = "build.rs" + +[dependencies] +anyhow.workspace = true +pocket-pi-agentos = { path = "../pocket-pi-agentos" } + +[dev-dependencies] +pocket-db.workspace = true +pocket-pi-embedded = { path = "../pocket-pi-embedded" } +serde_json.workspace = true +tempfile = "3" diff --git a/crates/pocket-pi-app-pack/build.rs b/crates/pocket-pi-app-pack/build.rs new file mode 100644 index 0000000..b21557b --- /dev/null +++ b/crates/pocket-pi-app-pack/build.rs @@ -0,0 +1,81 @@ +use std::env; +use std::fs; +use std::path::{Path, PathBuf}; + +const OPTIONAL_APPS: &[&str] = &["robinhood", "exa"]; + +fn main() { + println!("cargo:rerun-if-env-changed=POCKET_PI_APPS"); + let selected = selected_apps(); + let root = PathBuf::from(env::var_os("CARGO_MANIFEST_DIR").unwrap()) + .join("../..") + .canonicalize() + .unwrap(); + let mut source = String::from("pub fn embedded_apps() -> Vec {\n vec![\n"); + source.push_str(&bundle(&root, "pi-agent", true)); + for app in selected { + source.push_str(&bundle(&root, app, false)); + } + source.push_str(" ]\n}\n"); + fs::write( + PathBuf::from(env::var_os("OUT_DIR").unwrap()).join("apps.rs"), + source, + ) + .unwrap(); +} + +fn selected_apps() -> Vec<&'static str> { + let value = env::var("POCKET_PI_APPS").unwrap_or_else(|_| OPTIONAL_APPS.join(",")); + if value == "none" || value.is_empty() { + return Vec::new(); + } + let mut selected = Vec::new(); + for name in value.split(',') { + let name = name.trim(); + let app = OPTIONAL_APPS + .iter() + .copied() + .find(|candidate| *candidate == name) + .unwrap_or_else(|| panic!("unknown App {name}; expected robinhood, exa, or none")); + if selected.contains(&app) { + panic!("duplicate App {app}"); + } + selected.push(app); + } + selected +} + +fn bundle(root: &Path, app: &str, system: bool) -> String { + let app = root.join("apps").join(app); + for path in [ + app.join("agent-app.json"), + app.join("pocket.json"), + app.join("dist/app.js"), + app.join("dist/app.pak"), + ] { + println!("cargo:rerun-if-changed={}", path.display()); + } + let data = app.join("dist/data-action.js"); + let agent = app.join("dist/agent.js"); + println!("cargo:rerun-if-changed={}", data.display()); + if system { + println!("cargo:rerun-if-changed={}", agent.display()); + } + format!( + " EmbeddedApp::new(include_str!({descriptor:?}), include_str!({pocket:?}), include_str!({js:?}), {data}, {agent}, include_bytes!({pak:?})),\n", + descriptor = app.join("agent-app.json"), + pocket = app.join("pocket.json"), + js = app.join("dist/app.js"), + data = option_include_str(&data), + agent = if system { option_include_str(&agent) } else { "None".into() }, + pak = app.join("dist/app.pak"), + ) +} + +fn option_include_str(path: &Path) -> String { + if path.is_file() { + format!("Some(include_str!({path:?}))") + } else { + "None".into() + } +} diff --git a/crates/pocket-pi-app-pack/src/lib.rs b/crates/pocket-pi-app-pack/src/lib.rs new file mode 100644 index 0000000..90dc712 --- /dev/null +++ b/crates/pocket-pi-app-pack/src/lib.rs @@ -0,0 +1,56 @@ +use anyhow::Result; +use pocket_pi_agentos::{AppCatalog, EmbeddedApp}; + +include!(concat!(env!("OUT_DIR"), "/apps.rs")); + +pub fn catalog() -> Result { + AppCatalog::new(embedded_apps()) +} + +#[cfg(test)] +mod tests { + use super::*; + use std::collections::BTreeSet; + + #[test] + fn catalog_contains_exactly_the_build_selected_apps() { + let catalog = catalog().unwrap(); + let mut actual = catalog + .descriptors() + .map(|descriptor| descriptor.id.as_str()) + .collect::>(); + actual.sort_unstable(); + let mut expected = vec!["pi-agent"]; + let selected = option_env!("POCKET_PI_APPS").unwrap_or("robinhood,exa"); + if selected != "none" && !selected.is_empty() { + expected.extend(selected.split(',').map(str::trim)); + } + expected.sort_unstable(); + assert_eq!(actual, expected); + } + + #[test] + fn robinhood_catalog_matches_its_checked_in_snapshot() { + let catalog = catalog().unwrap(); + let Some(descriptor) = catalog.descriptor("robinhood") else { + return; + }; + let snapshot: serde_json::Value = + serde_json::from_str(include_str!("../../../apps/robinhood/tool-catalog.json")) + .unwrap(); + let upstream = snapshot["tools"] + .as_array() + .unwrap() + .iter() + .map(|tool| tool["name"].as_str().unwrap().to_owned()) + .collect::>(); + let allowed = descriptor + .provider_operations + .iter() + .cloned() + .collect::>(); + assert_eq!(allowed, upstream); + assert_eq!(upstream.len(), 54); + assert_eq!(descriptor.tools.len(), 3); + } +} diff --git a/crates/pocket-pi-app-pack/tests/runtime.rs b/crates/pocket-pi-app-pack/tests/runtime.rs new file mode 100644 index 0000000..aa552c1 --- /dev/null +++ b/crates/pocket-pi-app-pack/tests/runtime.rs @@ -0,0 +1,167 @@ +use std::collections::BTreeMap; +use std::sync::atomic::{AtomicUsize, Ordering}; +use std::sync::Arc; +use std::time::{Duration, Instant}; + +use pocket_pi_agentos::{ + AppServiceHost, AppSupervisor, HttpRequest, NetFailure, RoutedToolHost, TransportCompletion, + APP_ACTION_TIMEOUT, ROOT_APP_ID, +}; +use pocket_pi_app_pack::catalog; +use pocket_pi_embedded::{AgentEvent, ModelBackend, ToolHost, ToolResult}; +use serde_json::Value; + +struct Services; + +impl AppServiceHost for Services { + fn call( + &self, + _app_id: &str, + _service: &str, + _operation: &str, + _args: &Value, + _deadline: Instant, + ) -> Result { + Err("unexpected App service call".into()) + } + + fn http( + &self, + app_id: &str, + request: HttpRequest, + deadline: Instant, + ) -> Result { + assert_eq!(app_id, "exa"); + assert_eq!(request.url, "https://api.exa.ai/search"); + assert_eq!(request.method, "POST"); + let total_ms = APP_ACTION_TIMEOUT.as_millis() as u32; + assert!((1..=total_ms).contains(&request.timeout_ms)); + assert!(deadline.saturating_duration_since(Instant::now()) <= APP_ACTION_TIMEOUT); + let body: Value = serde_json::from_slice(&request.body).unwrap(); + assert_eq!(body["query"], "Pocket Pi architecture"); + let response = serde_json::json!({ + "results": [{ + "title": "Evidence result", + "url": "https://example.com/evidence" + }] + }); + Ok(TransportCompletion::Done { + handle: request.handle, + status: 200, + url: request.url, + headers: BTreeMap::from([("content-type".into(), "application/json".into())]), + body: serde_json::to_vec(&response).unwrap(), + }) + } +} + +struct Backend(AtomicUsize); + +impl ModelBackend for Backend { + fn complete( + &self, + request_json: &str, + on_event: &mut dyn FnMut(pocket_pi_embedded::ModelStreamEvent), + ) -> Result { + if self.0.fetch_add(1, Ordering::SeqCst) == 0 { + return Ok(serde_json::json!({ + "thinking": "", + "text": "", + "toolCalls": [{ + "id": "call_search", + "name": "research.search", + "arguments": {"query": "Pocket Pi architecture"} + }], + "usage": {}, + "stopReason": "toolUse" + }) + .to_string()); + } + assert!(request_json.contains("Evidence result"), "{request_json}"); + on_event(pocket_pi_embedded::ModelStreamEvent::Text( + "research complete".into(), + )); + Ok(serde_json::json!({ + "thinking": "", + "text": "research complete", + "toolCalls": [], + "usage": {}, + "stopReason": "stop" + }) + .to_string()) + } +} + +struct NoTools; + +impl ToolHost for NoTools { + fn definitions(&self) -> Vec { + Vec::new() + } + + fn execute(&self, _call_id: &str, name: &str, _args_json: &str) -> ToolResult { + ToolResult { + text: format!("unexpected Tool {name}"), + is_error: true, + ..ToolResult::default() + } + } +} + +#[test] +fn agent_routes_a_background_app_tool_through_http_and_sqlite() { + let temp = tempfile::tempdir().unwrap(); + let catalog = catalog().unwrap(); + assert!(catalog.descriptor("exa").is_some()); + assert!(catalog.descriptor("robinhood").is_some()); + let mut supervisor = AppSupervisor::new(temp.path(), catalog, Arc::new(Services)).unwrap(); + let (tools, requests) = RoutedToolHost::new(Arc::new(NoTools), supervisor.catalog().clone()); + supervisor + .boot_agent( + r#"{"model":"offline"}"#, + Arc::new(Backend(AtomicUsize::new(0))), + Arc::new(tools), + ) + .unwrap(); + supervisor.prompt_agent("research Pocket Pi").unwrap(); + supervisor.open("robinhood").unwrap(); + + let deadline = Instant::now() + Duration::from_secs(5); + let mut response = String::new(); + let mut finished = false; + while Instant::now() < deadline && !finished { + while let Ok(request) = requests.try_recv() { + request.handle(&supervisor); + } + for event in supervisor.frame_render(true).unwrap() { + match event { + AgentEvent::ResponseText(delta) => response.push_str(&delta), + AgentEvent::Done => finished = true, + AgentEvent::Failed(error) => panic!("Agent failed: {error}"), + AgentEvent::Ready => {} + } + } + std::thread::sleep(Duration::from_millis(2)); + } + + assert!(finished); + assert_eq!(response, "research complete"); + assert_eq!(supervisor.active_id(), "robinhood"); + let mut database = + pocket_db::DbModule::new(pocket_db::Storage::Dir(temp.path().join("apps/exa/data"))); + let handle = database.open("exa"); + assert!(handle >= 0); + let query: Value = serde_json::from_str(&database.query( + handle, + "SELECT query,status,result_count FROM searches", + "[]", + )) + .unwrap(); + database.close(handle); + assert_eq!( + query["rows"], + serde_json::json!([["Pocket Pi architecture", "ok", 1]]) + ); + supervisor.open(ROOT_APP_ID).unwrap(); + assert_eq!(supervisor.active_id(), ROOT_APP_ID); +} diff --git a/crates/pocket-pi-device-ui/Cargo.toml b/crates/pocket-pi-device-ui/Cargo.toml deleted file mode 100644 index 8219488..0000000 --- a/crates/pocket-pi-device-ui/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "pocket-pi-device-ui" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true -publish = false - -[dependencies] -pocket-pi-protocols = { path = "../pocket-pi-protocols" } -pocketjs-core = { git = "https://github.com/pocket-stack/pocketjs.git", rev = "4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" } diff --git a/crates/pocket-pi-device-ui/assets/fonts/INTER-LICENSE.txt b/crates/pocket-pi-device-ui/assets/fonts/INTER-LICENSE.txt deleted file mode 100644 index 9b2ca37..0000000 --- a/crates/pocket-pi-device-ui/assets/fonts/INTER-LICENSE.txt +++ /dev/null @@ -1,92 +0,0 @@ -Copyright (c) 2016 The Inter Project Authors (https://github.com/rsms/inter) - -This Font Software is licensed under the SIL Open Font License, Version 1.1. -This license is copied below, and is also available with a FAQ at: -http://scripts.sil.org/OFL - ------------------------------------------------------------ -SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007 ------------------------------------------------------------ - -PREAMBLE -The goals of the Open Font License (OFL) are to stimulate worldwide -development of collaborative font projects, to support the font creation -efforts of academic and linguistic communities, and to provide a free and -open framework in which fonts may be shared and improved in partnership -with others. - -The OFL allows the licensed fonts to be used, studied, modified and -redistributed freely as long as they are not sold by themselves. The -fonts, including any derivative works, can be bundled, embedded, -redistributed and/or sold with any software provided that any reserved -names are not used by derivative works. The fonts and derivatives, -however, cannot be released under any other type of license. The -requirement for fonts to remain under this license does not apply -to any document created using the fonts or their derivatives. - -DEFINITIONS -"Font Software" refers to the set of files released by the Copyright -Holder(s) under this license and clearly marked as such. This may -include source files, build scripts and documentation. - -"Reserved Font Name" refers to any names specified as such after the -copyright statement(s). - -"Original Version" refers to the collection of Font Software components as -distributed by the Copyright Holder(s). - -"Modified Version" refers to any derivative made by adding to, deleting, -or substituting -- in part or in whole -- any of the components of the -Original Version, by changing formats or by porting the Font Software to a -new environment. - -"Author" refers to any designer, engineer, programmer, technical -writer or other person who contributed to the Font Software. - -PERMISSION AND CONDITIONS -Permission is hereby granted, free of charge, to any person obtaining -a copy of the Font Software, to use, study, copy, merge, embed, modify, -redistribute, and sell modified and unmodified copies of the Font -Software, subject to the following conditions: - -1) Neither the Font Software nor any of its individual components, -in Original or Modified Versions, may be sold by itself. - -2) Original or Modified Versions of the Font Software may be bundled, -redistributed and/or sold with any software, provided that each copy -contains the above copyright notice and this license. These can be -included either as stand-alone text files, human-readable headers or -in the appropriate machine-readable metadata fields within text or -binary files as long as those fields can be easily viewed by the user. - -3) No Modified Version of the Font Software may use the Reserved Font -Name(s) unless explicit written permission is granted by the corresponding -Copyright Holder. This restriction only applies to the primary font name as -presented to the users. - -4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font -Software shall not be used to promote, endorse or advertise any -Modified Version, except to acknowledge the contribution(s) of the -Copyright Holder(s) and the Author(s) or with their explicit written -permission. - -5) The Font Software, modified or unmodified, in part or in whole, -must be distributed entirely under this license, and must not be -distributed under any other license. The requirement for fonts to -remain under this license does not apply to any document created -using the Font Software. - -TERMINATION -This license becomes null and void if any of the above conditions are -not met. - -DISCLAIMER -THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, -EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF -MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT -OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE -COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, -INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL -DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING -FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM -OTHER DEALINGS IN THE FONT SOFTWARE. diff --git a/crates/pocket-pi-device-ui/assets/fonts/SOURCE_PROVENANCE.md b/crates/pocket-pi-device-ui/assets/fonts/SOURCE_PROVENANCE.md deleted file mode 100644 index 572a24b..0000000 --- a/crates/pocket-pi-device-ui/assets/fonts/SOURCE_PROVENANCE.md +++ /dev/null @@ -1,17 +0,0 @@ -# PocketJS font atlases - -These `.pfa` files were generated with PocketJS's official -`framework/compiler/bake-font.ts` at the same pinned revision used by the -firmware (`4c5dc9ef1dd26e6f49b036c210931d399f2b52b2`). They contain the ASCII glyph -set plus the common smart punctuation `‘’“”–—…`, baked from PocketJS's bundled -Inter Regular and Inter Bold fonts. Model replies commonly use these -typographic characters even when the prompt contains plain ASCII. - -- slot 3: Inter Regular, 18 px; -- slot 6: Inter Regular, 36 px; -- slot 10: Inter Bold, 18 px; -- slot 12: Inter Bold, 24 px. - -The atlases use PocketJS font-atlas format v3 with one coverage sample per -logical pixel. Inter is distributed under the SIL Open Font License; the full -license is retained as `INTER-LICENSE.txt`. diff --git a/crates/pocket-pi-device-ui/assets/fonts/inter-18-bold.pfa b/crates/pocket-pi-device-ui/assets/fonts/inter-18-bold.pfa deleted file mode 100644 index e9eccfd..0000000 Binary files a/crates/pocket-pi-device-ui/assets/fonts/inter-18-bold.pfa and /dev/null differ diff --git a/crates/pocket-pi-device-ui/assets/fonts/inter-18-regular.pfa b/crates/pocket-pi-device-ui/assets/fonts/inter-18-regular.pfa deleted file mode 100644 index 6966865..0000000 Binary files a/crates/pocket-pi-device-ui/assets/fonts/inter-18-regular.pfa and /dev/null differ diff --git a/crates/pocket-pi-device-ui/assets/fonts/inter-24-bold.pfa b/crates/pocket-pi-device-ui/assets/fonts/inter-24-bold.pfa deleted file mode 100644 index a656cb9..0000000 Binary files a/crates/pocket-pi-device-ui/assets/fonts/inter-24-bold.pfa and /dev/null differ diff --git a/crates/pocket-pi-device-ui/assets/fonts/inter-36-regular.pfa b/crates/pocket-pi-device-ui/assets/fonts/inter-36-regular.pfa deleted file mode 100644 index ab30541..0000000 Binary files a/crates/pocket-pi-device-ui/assets/fonts/inter-36-regular.pfa and /dev/null differ diff --git a/crates/pocket-pi-device-ui/src/lib.rs b/crates/pocket-pi-device-ui/src/lib.rs deleted file mode 100644 index 59307b4..0000000 --- a/crates/pocket-pi-device-ui/src/lib.rs +++ /dev/null @@ -1,5 +0,0 @@ -mod model; -mod screen; - -pub use model::*; -pub use screen::*; diff --git a/crates/pocket-pi-device-ui/src/model.rs b/crates/pocket-pi-device-ui/src/model.rs deleted file mode 100644 index 5471b9a..0000000 --- a/crates/pocket-pi-device-ui/src/model.rs +++ /dev/null @@ -1,68 +0,0 @@ -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum AgentState { - Stopped, - Starting, - WaitingForAuth, - Idle, - Thinking, - Acting, - NetworkBlocked, - Faulted, -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub struct DeviceState { - pub agent: AgentState, -} - -impl Default for DeviceState { - fn default() -> Self { - Self { - agent: AgentState::Stopped, - } - } -} - -pub use pocket_pi_protocols::model::{ - ModelBackendSettings, ModelSettings, UartProvider, WirelessProvider, -}; - -#[derive(Clone, Debug, Default)] -pub struct ScheduleProjection { - pub name: Option, - pub prompt: String, - pub next_in_seconds: Option, - pub every_minutes: Option, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct WifiNetworkProjection { - pub ssid: String, - pub rssi_dbm: i16, - pub secured: bool, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct WifiSettingsProjection { - pub connected_ssid: Option, - pub ip_address: Option, - pub rssi_dbm: Option, - pub scanning: bool, - pub networks: Vec, - pub status: String, -} - -#[derive(Clone, Debug, Default, Eq, PartialEq)] -pub struct SettingsProjection { - pub wifi: WifiSettingsProjection, - pub firmware_version: String, - pub workspace_free_bytes: Option, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum SettingsCommand { - ScanWifi, - ConnectWifi { ssid: String, password: String }, - ForgetWifi, - Restart, -} diff --git a/crates/pocket-pi-device-ui/src/screen/font.rs b/crates/pocket-pi-device-ui/src/screen/font.rs deleted file mode 100644 index d36d101..0000000 --- a/crates/pocket-pi-device-ui/src/screen/font.rs +++ /dev/null @@ -1,165 +0,0 @@ -use pocketjs_core::{spec, Ui}; - -const BODY_SLOT: u8 = 3; -const BODY_BOLD_SLOT: u8 = 10; -const TITLE_SLOT: u8 = 12; -const COLUMN_WIDTH: usize = 16; - -const BODY_ATLAS: &[u8] = include_bytes!("../../assets/fonts/inter-18-regular.pfa"); -const BODY_BOLD_ATLAS: &[u8] = include_bytes!("../../assets/fonts/inter-18-bold.pfa"); -const TITLE_ATLAS: &[u8] = include_bytes!("../../assets/fonts/inter-24-bold.pfa"); - -#[derive(Clone, Copy)] -pub enum TextStyle { - Body, - Bold, - Title, -} - -impl TextStyle { - const fn slot(self) -> u8 { - match self { - Self::Body => BODY_SLOT, - Self::Bold => BODY_BOLD_SLOT, - Self::Title => TITLE_SLOT, - } - } -} - -pub fn load(ui: &mut Ui) -> bool { - [BODY_ATLAS, BODY_BOLD_ATLAS, TITLE_ATLAS] - .into_iter() - .all(|atlas| ui.load_font_atlas(atlas)) -} - -pub fn text_width(ui: &Ui, text: &str, style: TextStyle) -> i16 { - let Some(atlas) = ui.font_atlas(style.slot()) else { - return 0; - }; - let fallback = atlas.lookup_entry('?' as u32); - text.chars() - .filter_map(|character| atlas.lookup_entry(character as u32).or(fallback)) - .map(|entry| i32::from(entry.advance)) - .sum::() - .min(i32::from(i16::MAX)) as i16 -} - -#[allow(clippy::too_many_arguments)] -pub fn append_text( - ui: &Ui, - words: &mut Vec, - text: &str, - x: i16, - y: i16, - max_columns: usize, - max_rows: usize, - color: u32, - style: TextStyle, -) { - let slot = style.slot(); - let Some(atlas) = ui.font_atlas(slot) else { - return; - }; - let max_width = max_columns.saturating_mul(COLUMN_WIDTH) as i32; - let line_height = atlas.line_height as i32; - let fallback = atlas.lookup_entry('?' as u32); - let mut glyphs = Vec::new(); - let mut pen_x = 0i32; - let mut row = 0usize; - - for character in text.chars() { - if character == '\n' { - row += 1; - pen_x = 0; - if row >= max_rows { - break; - } - continue; - } - - let entry = atlas.lookup_entry(character as u32).or(fallback); - let Some(entry) = entry else { - continue; - }; - let advance = entry.advance as i32; - if pen_x > 0 && pen_x + advance > max_width { - row += 1; - pen_x = 0; - } - if row >= max_rows { - break; - } - - let glyph_x = x as i32 + pen_x - entry.xoff as i32; - let glyph_y = y as i32 + row as i32 * line_height; - glyphs.push(xy(glyph_x as i16, glyph_y as i16)); - glyphs.push(entry.gid as u32); - pen_x += advance; - } - - if glyphs.is_empty() { - return; - } - words.push(spec::draw_op::GLYPH_RUN); - words.push(slot as u32 | ((glyphs.len() as u32 / 2) << 16)); - words.push(color); - words.extend(glyphs); -} - -/// Wrap text with the same glyph advances used by `append_text`, so callers -/// can page through long content without guessing from byte or character count. -pub fn wrap_text(ui: &Ui, text: &str, max_columns: usize, style: TextStyle) -> Vec { - let Some(atlas) = ui.font_atlas(style.slot()) else { - return vec![text.to_owned()]; - }; - let max_width = max_columns.saturating_mul(COLUMN_WIDTH) as i32; - let fallback = atlas.lookup_entry('?' as u32); - let mut lines = Vec::new(); - let mut line = String::new(); - let mut pen_x = 0i32; - - for character in text.chars() { - if character == '\n' { - lines.push(core::mem::take(&mut line)); - pen_x = 0; - continue; - } - let Some(entry) = atlas.lookup_entry(character as u32).or(fallback) else { - continue; - }; - let advance = entry.advance as i32; - if pen_x > 0 && pen_x + advance > max_width { - lines.push(core::mem::take(&mut line)); - pen_x = 0; - } - line.push(character); - pen_x += advance; - } - lines.push(line); - lines -} - -const fn xy(x: i16, y: i16) -> u32 { - x as u16 as u32 | ((y as u16 as u32) << 16) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn atlases_cover_common_model_punctuation() { - let mut ui = Ui::new(); - assert!(load(&mut ui)); - for style in [TextStyle::Body, TextStyle::Bold, TextStyle::Title] { - let atlas = ui.font_atlas(style.slot()).expect("font atlas"); - for character in "‘’“”–—…".chars() { - assert!( - atlas.lookup_entry(character as u32).is_some(), - "missing {character:?} in font slot {}", - style.slot() - ); - } - } - } -} diff --git a/crates/pocket-pi-device-ui/src/screen/mod.rs b/crates/pocket-pi-device-ui/src/screen/mod.rs deleted file mode 100644 index 324915c..0000000 --- a/crates/pocket-pi-device-ui/src/screen/mod.rs +++ /dev/null @@ -1,1649 +0,0 @@ -#![allow( - clippy::if_same_then_else, - clippy::manual_div_ceil, - clippy::too_many_arguments -)] - -mod font; -mod workspace_browser; - -use std::collections::VecDeque; - -use crate::model::{ - AgentState, DeviceState, ModelBackendSettings, ModelSettings, ScheduleProjection, - SettingsCommand, SettingsProjection, UartProvider, WifiNetworkProjection, WirelessProvider, -}; -use pocketjs_core::{spec, Ui}; -use workspace_browser::{format_size, format_timestamp, WorkspaceBrowser}; - -pub fn load_fonts(ui: &mut Ui) -> bool { - font::load(ui) -} - -const PANEL_WIDTH: u16 = 720; -const PANEL_HEIGHT: u16 = 1280; -const HEADER_HEIGHT: i16 = 112; -const BOTTOM_BAR_Y: i16 = 1172; -const FILE_ROW_START_Y: i16 = 190; -const FILE_ROW_HEIGHT: i16 = 108; -const FILE_VISIBLE_ROWS: usize = 8; -const VIEWER_VISIBLE_LINES: usize = 39; -const MESSAGE_VISIBLE_LINES: usize = 39; -const MAX_CHAT_TURNS: usize = 10; -const CHAT_VISIBLE_TURNS: usize = 2; -const WIFI_ROW_START_Y: u16 = 330; -const WIFI_ROW_HEIGHT: u16 = 92; -const WIFI_VISIBLE_ROWS: usize = 5; -const COMPOSE_Y: u16 = 1070; -const MAX_PROMPT_BYTES: usize = 256; -const PENDING_ASSISTANT: &str = "THINKING..."; - -// PocketJS draw-list colors are packed ABGR, not ARGB. -const UI_ACCENT_GREEN: u32 = 0xff3b_d158; // RGB #58D13B -const UI_LOSS_RED: u32 = 0xff44_44ef; // RGB #EF4444 - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -pub enum ScreenView { - Chat, - Files, - Settings, - Viewer, - MessageReader, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -pub enum ScreenInteraction { - None, - Redraw, - SubmitPrompt(String), - Settings(SettingsCommand), -} - -#[derive(Clone, Copy, Debug, Eq, PartialEq)] -enum KeyboardMode { - Letters, - Numbers, -} - -#[derive(Clone, Debug, Eq, PartialEq)] -enum KeyboardPurpose { - Prompt, - WifiPassword { ssid: String }, -} - -#[derive(Clone, Copy, Debug, Default)] -pub struct SystemTelemetry { - pub psram_used_percent: u8, - pub psram_free_bytes: usize, - pub cpu_percent: Option, - pub ui_fps_tenths: u16, - pub lcd_refresh_hz: u16, -} - -#[derive(Clone, Debug)] -struct BackendProjection { - model: String, - link: String, - auth: String, -} - -impl Default for BackendProjection { - fn default() -> Self { - Self { - model: "CODEX".to_owned(), - link: "UART / MAC".to_owned(), - auth: "CODING PLAN".to_owned(), - } - } -} - -impl BackendProjection { - fn from_settings(settings: &ModelSettings) -> Self { - match &settings.backend { - ModelBackendSettings::Uart { provider } => match provider { - UartProvider::Codex => Self::default(), - UartProvider::ClaudeCode => Self { - model: "CLAUDE CODE".to_owned(), - link: "UART / MAC".to_owned(), - auth: "CLAUDE LOGIN".to_owned(), - }, - }, - ModelBackendSettings::Wireless { provider } => Self { - model: match provider { - WirelessProvider::OpenAi => "OPENAI API", - WirelessProvider::OpenRouter => "OPENROUTER", - WirelessProvider::Anthropic => "ANTHROPIC API", - } - .to_owned(), - link: "WIFI / DIRECT".to_owned(), - auth: "API KEY / RAM".to_owned(), - }, - } - } -} - -#[derive(Clone, Debug)] -pub struct ChatTurn { - pub user: String, - pub assistant: String, - pending: bool, -} - -#[derive(Debug)] -pub struct ChatProjection { - turns: VecDeque, -} - -#[derive(Debug)] -struct MessageReader { - author: &'static str, - lines: Vec, - line_offset: usize, -} - -impl ChatProjection { - pub fn new(user: impl Into, assistant: impl Into) -> Self { - let mut turns = VecDeque::new(); - turns.push_back(ChatTurn { - user: user.into(), - assistant: assistant.into(), - pending: false, - }); - Self { turns } - } - - pub fn push_turn(&mut self, user: impl Into, assistant: impl Into) { - self.turns.push_back(ChatTurn { - user: user.into(), - assistant: assistant.into(), - pending: false, - }); - while self.turns.len() > MAX_CHAT_TURNS { - self.turns.pop_front(); - } - } - - pub fn set_latest_assistant(&mut self, assistant: impl Into) { - if let Some(turn) = self.turns.back_mut() { - turn.assistant = assistant.into(); - turn.pending = false; - } - } - - pub fn push_pending(&mut self, user: impl Into) { - let user = user.into(); - if self - .turns - .back() - .is_some_and(|turn| turn.pending && turn.user == user) - { - return; - } - self.turns.push_back(ChatTurn { - user, - assistant: PENDING_ASSISTANT.to_owned(), - pending: true, - }); - while self.turns.len() > MAX_CHAT_TURNS { - self.turns.pop_front(); - } - } - - pub fn append_model_delta(&mut self, delta: &str) -> bool { - let Some(turn) = self.turns.back_mut().filter(|turn| turn.pending) else { - return false; - }; - if turn.assistant == PENDING_ASSISTANT { - turn.assistant.clear(); - } - turn.assistant.push_str(delta); - true - } - - pub fn finish_pending(&mut self) { - if let Some(turn) = self.turns.back_mut().filter(|turn| turn.pending) { - if turn.assistant.is_empty() || turn.assistant == PENDING_ASSISTANT { - turn.assistant = "TURN COMPLETE".to_owned(); - } - turn.pending = false; - } - } - - pub fn fail_pending(&mut self, error: impl Into) { - let error = error.into(); - if let Some(turn) = self.turns.back_mut() { - if turn.pending { - turn.assistant = error; - turn.pending = false; - return; - } - } - self.push_turn("SYSTEM", error); - } -} - -#[derive(Debug)] -pub struct ScreenState { - pub view: ScreenView, - pub browser: WorkspaceBrowser, - pub telemetry: SystemTelemetry, - schedule: ScheduleProjection, - chat_scroll: usize, - wifi_scroll: usize, - keyboard_open: bool, - keyboard_mode: KeyboardMode, - keyboard_uppercase: bool, - keyboard_purpose: KeyboardPurpose, - keyboard_input: String, - pressed_key: Option, - backend: BackendProjection, - settings: SettingsProjection, - message_reader: Option, -} - -impl ScreenState { - pub fn new(workspace_root: &str) -> Self { - Self { - view: ScreenView::Chat, - browser: WorkspaceBrowser::new(workspace_root), - telemetry: SystemTelemetry::default(), - schedule: ScheduleProjection::default(), - chat_scroll: 0, - wifi_scroll: 0, - keyboard_open: false, - keyboard_mode: KeyboardMode::Letters, - keyboard_uppercase: false, - keyboard_purpose: KeyboardPurpose::Prompt, - keyboard_input: String::new(), - pressed_key: None, - backend: BackendProjection::default(), - settings: SettingsProjection::default(), - message_reader: None, - } - } - - pub fn set_telemetry(&mut self, telemetry: SystemTelemetry) { - self.telemetry = telemetry; - } - - pub fn set_schedule(&mut self, schedule: ScheduleProjection) { - self.schedule = schedule; - } - - pub fn set_model_backend(&mut self, settings: &ModelSettings) { - self.backend = BackendProjection::from_settings(settings); - } - - pub fn set_backend_status(&mut self, model: &str, link: &str, auth: &str) { - self.backend = BackendProjection { - model: model.to_owned(), - link: link.to_owned(), - auth: auth.to_owned(), - }; - } - - pub fn set_settings(&mut self, settings: SettingsProjection) { - self.wifi_scroll = self.wifi_scroll.min( - settings - .wifi - .networks - .len() - .saturating_sub(WIFI_VISIBLE_ROWS), - ); - self.settings = settings; - } - - pub fn handle_touch_release(&mut self) -> bool { - self.pressed_key.take().is_some() && self.keyboard_open - } - - pub fn refresh_workspace(&mut self) { - self.browser.refresh(); - } - - pub fn show_latest_chat(&mut self) { - self.chat_scroll = 0; - } - - pub fn handle_tap( - &mut self, - x: u16, - y: u16, - chat: &ChatProjection, - ui: &Ui, - ) -> ScreenInteraction { - if self.keyboard_open { - return self.handle_keyboard_tap(x, y); - } - if !matches!(self.view, ScreenView::Viewer | ScreenView::MessageReader) - && y as i16 >= BOTTOM_BAR_Y - { - let next = match x { - 0..=239 => ScreenView::Chat, - 240..=479 => { - self.browser.refresh(); - ScreenView::Files - } - _ => ScreenView::Settings, - }; - let changed = self.view != next; - self.view = next; - self.keyboard_open = false; - self.pressed_key = None; - return if changed { - ScreenInteraction::Redraw - } else { - ScreenInteraction::None - }; - } - let changed = match self.view { - ScreenView::Chat => self.handle_chat_tap(x, y, chat, ui), - ScreenView::Files => self.handle_files_tap(x, y), - ScreenView::Settings => return self.handle_settings_tap(x, y), - ScreenView::Viewer => self.handle_viewer_tap(x, y), - ScreenView::MessageReader => self.handle_message_reader_tap(x, y), - }; - if changed { - ScreenInteraction::Redraw - } else { - ScreenInteraction::None - } - } - - fn handle_chat_tap(&mut self, x: u16, y: u16, chat: &ChatProjection, ui: &Ui) -> bool { - if (24..=696).contains(&x) && (COMPOSE_Y..=1150).contains(&y) { - self.keyboard_open = true; - self.keyboard_purpose = KeyboardPurpose::Prompt; - self.keyboard_uppercase = false; - self.keyboard_input.clear(); - self.pressed_key = None; - return true; - } - if x >= 620 && (140..=272).contains(&y) { - self.chat_scroll = self.chat_scroll.saturating_add(CHAT_VISIBLE_TURNS); - return true; - } - if x >= 620 && (610..=790).contains(&y) { - let before = self.chat_scroll; - self.chat_scroll = self.chat_scroll.saturating_sub(CHAT_VISIBLE_TURNS); - return before != self.chat_scroll; - } - if (24..610).contains(&x) { - let len = chat.turns.len(); - let scroll = self.chat_scroll.min(len.saturating_sub(CHAT_VISIBLE_TURNS)); - let end = len.saturating_sub(scroll); - let start = end.saturating_sub(CHAT_VISIBLE_TURNS); - for (row, turn) in chat.turns.iter().skip(start).take(end - start).enumerate() { - let top = 140 + row as u16 * 320; - if (top..top + 150).contains(&y) { - self.open_message(ui, "YOU", &turn.user); - return true; - } - if (top + 150..top + 298).contains(&y) { - self.open_message(ui, "PI", &turn.assistant); - return true; - } - } - } - false - } - - fn open_message(&mut self, ui: &Ui, author: &'static str, text: &str) { - self.message_reader = Some(MessageReader { - author, - lines: font::wrap_text(ui, text, 32, font::TextStyle::Body), - line_offset: 0, - }); - self.view = ScreenView::MessageReader; - } - - fn handle_message_reader_tap(&mut self, x: u16, y: u16) -> bool { - if x < 104 && y < HEADER_HEIGHT as u16 { - self.message_reader = None; - self.view = ScreenView::Chat; - return true; - } - let Some(reader) = self.message_reader.as_mut() else { - self.view = ScreenView::Chat; - return true; - }; - if x >= 620 && (170..=340).contains(&y) { - let before = reader.line_offset; - reader.line_offset = reader.line_offset.saturating_sub(18); - return before != reader.line_offset; - } - if x >= 620 && (920..=1100).contains(&y) { - let max_scroll = reader.lines.len().saturating_sub(MESSAGE_VISIBLE_LINES); - let before = reader.line_offset; - reader.line_offset = (reader.line_offset + 18).min(max_scroll); - return before != reader.line_offset; - } - false - } - - fn handle_keyboard_tap(&mut self, x: u16, y: u16) -> ScreenInteraction { - let max_input_bytes = match &self.keyboard_purpose { - KeyboardPurpose::Prompt => MAX_PROMPT_BYTES, - KeyboardPurpose::WifiPassword { .. } => 63, - }; - if (24..=696).contains(&x) && (1164..=1279).contains(&y) { - self.keyboard_open = false; - self.keyboard_input.clear(); - self.keyboard_uppercase = false; - self.pressed_key = None; - return ScreenInteraction::Redraw; - } - if (548..=680).contains(&x) && (360..=416).contains(&y) { - self.pressed_key = Some("CLEAR".to_owned()); - self.keyboard_input.clear(); - return ScreenInteraction::Redraw; - } - - let rows = match self.keyboard_mode { - KeyboardMode::Letters => ["qwertyuiop", "asdfghjkl", "zxcvbnm"], - KeyboardMode::Numbers => ["1234567890", "-/:;()$&@", ".,?!'\"+"], - }; - let character = if (488..=608).contains(&y) { - row_character(x, 24, 60, 8, rows[0]) - } else if (628..=748).contains(&y) { - row_character(x, 31, 66, 8, rows[1]) - } else if (768..=888).contains(&y) { - row_character(x, 24, 72, 8, rows[2]) - } else { - None - }; - if let Some(character) = character { - self.pressed_key = Some(character.to_ascii_uppercase().to_string()); - if self.keyboard_input.len() < max_input_bytes { - self.keyboard_input.push( - if self.keyboard_mode == KeyboardMode::Letters && self.keyboard_uppercase { - character.to_ascii_uppercase() - } else { - character - }, - ); - } - return ScreenInteraction::Redraw; - } - if (592..=696).contains(&x) && (768..=888).contains(&y) { - self.pressed_key = Some("DEL".to_owned()); - self.keyboard_input.pop(); - return ScreenInteraction::Redraw; - } - if (908..=1064).contains(&y) { - match x { - 24..=116 => { - self.pressed_key = Some( - if self.keyboard_mode == KeyboardMode::Letters { - "123" - } else { - "ABC" - } - .to_owned(), - ); - self.keyboard_mode = match self.keyboard_mode { - KeyboardMode::Letters => KeyboardMode::Numbers, - KeyboardMode::Numbers => KeyboardMode::Letters, - }; - self.keyboard_uppercase = false; - return ScreenInteraction::Redraw; - } - 124..=424 => { - self.pressed_key = Some("SPACE".to_owned()); - if !self.keyboard_input.is_empty() - && self.keyboard_input.len() < max_input_bytes - { - self.keyboard_input.push(' '); - } - return ScreenInteraction::Redraw; - } - 432..=500 => { - if self.keyboard_mode == KeyboardMode::Letters { - self.keyboard_uppercase = !self.keyboard_uppercase; - self.pressed_key = Some("SHIFT".to_owned()); - return ScreenInteraction::Redraw; - } - self.pressed_key = Some(".".to_owned()); - if self.keyboard_input.len() < max_input_bytes { - self.keyboard_input.push('.'); - } - return ScreenInteraction::Redraw; - } - 508..=576 => { - if self.keyboard_mode == KeyboardMode::Letters { - self.keyboard_uppercase = !self.keyboard_uppercase; - self.pressed_key = Some("SHIFT".to_owned()); - return ScreenInteraction::Redraw; - } - self.pressed_key = Some("?".to_owned()); - if self.keyboard_input.len() < max_input_bytes { - self.keyboard_input.push('?'); - } - return ScreenInteraction::Redraw; - } - 584..=696 => { - let value = self.keyboard_input.trim().to_owned(); - if value.is_empty() { - return ScreenInteraction::None; - } - let purpose = self.keyboard_purpose.clone(); - self.keyboard_input.clear(); - self.keyboard_open = false; - self.pressed_key = None; - self.keyboard_mode = KeyboardMode::Letters; - self.keyboard_uppercase = false; - return match purpose { - KeyboardPurpose::Prompt => ScreenInteraction::SubmitPrompt(value), - KeyboardPurpose::WifiPassword { ssid } => { - ScreenInteraction::Settings(SettingsCommand::ConnectWifi { - ssid, - password: value, - }) - } - }; - } - _ => {} - } - } - ScreenInteraction::None - } - - fn handle_settings_tap(&mut self, x: u16, y: u16) -> ScreenInteraction { - if (480..=696).contains(&x) && (142..=218).contains(&y) { - return ScreenInteraction::Settings(SettingsCommand::ScanWifi); - } - if x >= 620 && (330..=462).contains(&y) { - let before = self.wifi_scroll; - self.wifi_scroll = self.wifi_scroll.saturating_sub(WIFI_VISIBLE_ROWS - 1); - return if before == self.wifi_scroll { - ScreenInteraction::None - } else { - ScreenInteraction::Redraw - }; - } - if x >= 620 && (650..=782).contains(&y) { - let max_scroll = self - .settings - .wifi - .networks - .len() - .saturating_sub(WIFI_VISIBLE_ROWS); - let before = self.wifi_scroll; - self.wifi_scroll = (self.wifi_scroll + WIFI_VISIBLE_ROWS - 1).min(max_scroll); - return if before == self.wifi_scroll { - ScreenInteraction::None - } else { - ScreenInteraction::Redraw - }; - } - if x < 610 && (WIFI_ROW_START_Y..790).contains(&y) { - let row = ((y - WIFI_ROW_START_Y) / WIFI_ROW_HEIGHT) as usize; - let row_top = WIFI_ROW_START_Y + row as u16 * WIFI_ROW_HEIGHT; - if y >= row_top + 84 { - return ScreenInteraction::None; - } - if let Some(network) = self.settings.wifi.networks.get(self.wifi_scroll + row) { - if !network.secured { - return ScreenInteraction::Settings(SettingsCommand::ConnectWifi { - ssid: network.ssid.clone(), - password: String::new(), - }); - } - self.keyboard_open = true; - self.keyboard_mode = KeyboardMode::Letters; - self.keyboard_uppercase = false; - self.keyboard_purpose = KeyboardPurpose::WifiPassword { - ssid: network.ssid.clone(), - }; - self.keyboard_input.clear(); - self.pressed_key = None; - return ScreenInteraction::Redraw; - } - } - if (24..=340).contains(&x) && (1010..=1090).contains(&y) { - return ScreenInteraction::Settings(SettingsCommand::ForgetWifi); - } - if (356..=696).contains(&x) && (1010..=1090).contains(&y) { - return ScreenInteraction::Settings(SettingsCommand::Restart); - } - ScreenInteraction::None - } - - fn handle_files_tap(&mut self, x: u16, y: u16) -> bool { - if x < 96 && y < HEADER_HEIGHT as u16 && self.browser.can_go_up() { - self.browser.go_up(); - return true; - } - if x >= 620 && (170..=340).contains(&y) { - self.browser.scroll_list(-4, FILE_VISIBLE_ROWS); - return true; - } - if x >= 620 && (920..=1100).contains(&y) { - self.browser.scroll_list(4, FILE_VISIBLE_ROWS); - return true; - } - if x < 610 && y as i16 >= FILE_ROW_START_Y { - let row = ((y as i16 - FILE_ROW_START_Y) / FILE_ROW_HEIGHT) as usize; - if row < FILE_VISIBLE_ROWS && self.browser.activate_visible_row(row) { - if self.browser.open_file.is_some() { - self.view = ScreenView::Viewer; - } - return true; - } - } - false - } - - fn handle_viewer_tap(&mut self, x: u16, y: u16) -> bool { - if x < 104 && y < HEADER_HEIGHT as u16 { - self.browser.close_file(); - self.view = ScreenView::Files; - return true; - } - if x >= 620 && (170..=340).contains(&y) { - self.browser.scroll_file(-18, VIEWER_VISIBLE_LINES); - return true; - } - if x >= 620 && (920..=1100).contains(&y) { - self.browser.scroll_file(18, VIEWER_VISIBLE_LINES); - return true; - } - false - } - - pub fn draw_list(&self, ui: &Ui, state: &DeviceState, chat: &ChatProjection) -> Vec { - match self.view { - _ if self.keyboard_open => keyboard_draw_list( - ui, - state, - &self.keyboard_input, - self.keyboard_mode, - self.keyboard_uppercase, - self.pressed_key.as_deref(), - &self.keyboard_purpose, - self.telemetry, - ), - ScreenView::Chat => chat_draw_list( - ui, - state, - chat, - self.chat_scroll, - &self.schedule, - self.telemetry, - ), - ScreenView::Files => files_draw_list(ui, state, &self.browser, self.telemetry), - ScreenView::Settings => settings_draw_list( - ui, - state, - &self.settings, - self.wifi_scroll, - &self.backend, - self.telemetry, - ), - ScreenView::Viewer => viewer_draw_list(ui, state, &self.browser, self.telemetry), - ScreenView::MessageReader => { - message_reader_draw_list(ui, state, self.message_reader.as_ref(), self.telemetry) - } - } - } -} - -fn base_words(ui: &Ui, state: &DeviceState, title: &str, telemetry: SystemTelemetry) -> Vec { - let mut words = Vec::new(); - rect(&mut words, 0, 0, PANEL_WIDTH, PANEL_HEIGHT, 0xfff1_f5f9); - rect( - &mut words, - 0, - 0, - PANEL_WIDTH, - HEADER_HEIGHT as u16, - 0xff0f_172a, - ); - status_header( - ui, - &mut words, - state, - title, - telemetry, - 0xffff_ffff, - 0xffcb_d5e1, - 0xff94_a3b8, - ); - words -} - -#[allow(clippy::too_many_arguments)] -fn status_header( - ui: &Ui, - words: &mut Vec, - state: &DeviceState, - title: &str, - telemetry: SystemTelemetry, - title_color: u32, - primary_status_color: u32, - secondary_status_color: u32, -) { - const RIGHT_EDGE: i16 = 696; - rect(words, 24, 38, 36, 36, agent_state_color(state.agent)); - push_title(ui, words, title, 78, 40, 22, title_color); - let cpu = telemetry - .cpu_percent - .map(|value| format!("{value:02}%")) - .unwrap_or_else(|| "--".to_owned()); - let free_megabytes = telemetry.psram_free_bytes as f32 / (1024.0 * 1024.0); - let memory = format!( - "PSRAM {:02}% FREE {free_megabytes:.1}M", - telemetry.psram_used_percent - ); - let runtime = format!( - "CPU {cpu} UI {}.{}FPS LCD {}HZ", - telemetry.ui_fps_tenths / 10, - telemetry.ui_fps_tenths % 10, - telemetry.lcd_refresh_hz, - ); - push_text_right(ui, words, &memory, RIGHT_EDGE, 25, primary_status_color); - push_text_right(ui, words, &runtime, RIGHT_EDGE, 61, secondary_status_color); -} - -fn chat_draw_list( - ui: &Ui, - state: &DeviceState, - chat: &ChatProjection, - scroll: usize, - schedule: &ScheduleProjection, - telemetry: SystemTelemetry, -) -> Vec { - let mut words = base_words(ui, state, "ESP32 PI AGENT", telemetry); - let len = chat.turns.len(); - let max_scroll = len.saturating_sub(CHAT_VISIBLE_TURNS); - let scroll = scroll.min(max_scroll); - let end = len.saturating_sub(scroll); - let start = end.saturating_sub(CHAT_VISIBLE_TURNS); - for (row, turn) in chat.turns.iter().skip(start).take(end - start).enumerate() { - let y = 140 + row as i16 * 320; - rect(&mut words, 24, y, 584, 298, 0xffff_ffff); - rect(&mut words, 40, y + 18, 82, 34, 0xffdb_eafe); - push_text_bold(ui, &mut words, "YOU", 54, y + 27, 8, 0xff1d_4ed8); - push_text_limited(ui, &mut words, &turn.user, 42, y + 70, 34, 4, 0xff0f_172a); - rect(&mut words, 40, y + 160, 62, 34, 0xffdc_fce7); - push_text_bold(ui, &mut words, "PI", 56, y + 169, 8, 0xff04_7a55); - push_text_limited( - ui, - &mut words, - &turn.assistant, - 42, - y + 210, - 34, - 4, - 0xff0f_172a, - ); - } - chat_scroll_buttons(ui, &mut words); - schedule_panel(ui, &mut words, schedule); - compose_button(ui, &mut words); - bottom_bar(ui, &mut words, ScreenView::Chat); - words -} - -fn schedule_panel(ui: &Ui, words: &mut Vec, schedule: &ScheduleProjection) { - rect(words, 24, 798, 672, 252, 0xffe2_e8f0); - push_text_bold(ui, words, "NEXT WAKE", 44, 822, 28, 0xff33_4155); - if let (Some(name), Some(seconds)) = (&schedule.name, schedule.next_in_seconds) { - let remaining = if seconds < 60 { - "IN <1 MIN".to_owned() - } else if seconds < 3600 { - format!("IN {} MIN", (seconds + 59) / 60) - } else { - format!("IN {}H {}M", seconds / 3600, (seconds % 3600) / 60) - }; - let cadence = schedule - .every_minutes - .map(|minutes| format!("EVERY {minutes}M ")) - .unwrap_or_default(); - let time = format!("{name} {cadence}{remaining}"); - push_text_bold(ui, words, &time, 44, 870, 30, 0xff0f_172a); - rect(words, 42, 914, 636, 112, 0xffff_ffff); - push_text_limited(ui, words, &schedule.prompt, 58, 934, 36, 4, 0xff33_4155); - } else { - push_text_bold(ui, words, "NO WAKE SCHEDULED", 44, 884, 34, 0xff64_748b); - push_text_limited( - ui, - words, - "ASK PI TO CREATE ONE WITH SCHEDULE.SET", - 44, - 938, - 38, - 3, - 0xff64_748b, - ); - } -} - -fn compose_button(ui: &Ui, words: &mut Vec) { - rect(words, 24, COMPOSE_Y as i16, 672, 80, 0xff25_63eb); - push_text_bold( - ui, - words, - "TYPE A MESSAGE", - 238, - COMPOSE_Y as i16 + 27, - 18, - 0xffff_ffff, - ); -} - -fn keyboard_draw_list( - ui: &Ui, - state: &DeviceState, - input: &str, - mode: KeyboardMode, - uppercase: bool, - pressed_key: Option<&str>, - purpose: &KeyboardPurpose, - telemetry: SystemTelemetry, -) -> Vec { - let title = match purpose { - KeyboardPurpose::Prompt => "NEW MESSAGE", - KeyboardPurpose::WifiPassword { .. } => "WIFI PASSWORD", - }; - let mut words = base_words(ui, state, title, telemetry); - let display = match purpose { - KeyboardPurpose::Prompt => prompt_tail(input, MAX_PROMPT_BYTES).to_owned(), - KeyboardPurpose::WifiPassword { .. } => "*".repeat(input.len()), - }; - rect(&mut words, 24, 132, 672, 300, 0xffff_ffff); - push_text_limited( - ui, - &mut words, - &display, - 42, - 154, - 29, - 9, - if input.is_empty() { - 0xff94_a3b8 - } else { - 0xff0f_172a - }, - ); - if input.is_empty() { - push_text_limited( - ui, - &mut words, - match purpose { - KeyboardPurpose::Prompt => "TYPE YOUR MESSAGE...", - KeyboardPurpose::WifiPassword { .. } => "ENTER NETWORK PASSWORD...", - }, - 42, - 154, - 29, - 2, - 0xff94_a3b8, - ); - } - draw_key( - ui, - &mut words, - "CLEAR", - 548, - 360, - 132, - 56, - 0xffe2_e8f0, - pressed_key == Some("CLEAR"), - ); - push_text( - ui, - &mut words, - &format!( - "{} / {} ASCII BYTES", - input.len(), - match purpose { - KeyboardPurpose::Prompt => MAX_PROMPT_BYTES, - KeyboardPurpose::WifiPassword { .. } => 63, - } - ), - 30, - 442, - 34, - 0xff64_748b, - ); - - let rows = match mode { - KeyboardMode::Letters => ["qwertyuiop", "asdfghjkl", "zxcvbnm"], - KeyboardMode::Numbers => ["1234567890", "-/:;()$&@", ".,?!'\"+"], - }; - draw_character_row(ui, &mut words, rows[0], 24, 488, 60, 8, pressed_key); - draw_character_row(ui, &mut words, rows[1], 31, 628, 66, 8, pressed_key); - draw_character_row(ui, &mut words, rows[2], 24, 768, 72, 8, pressed_key); - draw_key( - ui, - &mut words, - "DEL", - 592, - 768, - 104, - 120, - 0xffe2_e8f0, - pressed_key == Some("DEL"), - ); - - draw_key( - ui, - &mut words, - if mode == KeyboardMode::Letters { - "123" - } else { - "ABC" - }, - 24, - 908, - 92, - 156, - 0xffe2_e8f0, - pressed_key - == Some(if mode == KeyboardMode::Letters { - "123" - } else { - "ABC" - }), - ); - draw_key( - ui, - &mut words, - "SPACE", - 124, - 908, - 300, - 156, - 0xffe2_e8f0, - pressed_key == Some("SPACE"), - ); - if mode == KeyboardMode::Letters { - draw_key( - ui, - &mut words, - "SHIFT", - 432, - 908, - 144, - 156, - if uppercase { 0xffbf_dbfe } else { 0xffe2_e8f0 }, - pressed_key == Some("SHIFT"), - ); - } else { - draw_key( - ui, - &mut words, - ".", - 432, - 908, - 68, - 156, - 0xffe2_e8f0, - pressed_key == Some("."), - ); - draw_key( - ui, - &mut words, - "?", - 508, - 908, - 68, - 156, - 0xffe2_e8f0, - pressed_key == Some("?"), - ); - } - draw_key( - ui, - &mut words, - match purpose { - KeyboardPurpose::Prompt => "SEND", - KeyboardPurpose::WifiPassword { .. } => "JOIN", - }, - 584, - 908, - 112, - 156, - UI_ACCENT_GREEN, - matches!(pressed_key, Some("SEND" | "JOIN")), - ); - - draw_key( - ui, - &mut words, - "CLOSE KEYBOARD", - 24, - 1172, - 672, - 84, - 0xffe2_e8f0, - false, - ); - words -} - -fn draw_character_row( - ui: &Ui, - words: &mut Vec, - characters: &str, - start_x: i16, - y: i16, - key_width: u16, - gap: i16, - pressed_key: Option<&str>, -) { - for (index, character) in characters.chars().enumerate() { - let x = start_x + index as i16 * (key_width as i16 + gap); - let label = character.to_ascii_uppercase().to_string(); - draw_key( - ui, - words, - &label, - x, - y, - key_width, - 120, - 0xffff_ffff, - pressed_key == Some(label.as_str()), - ); - } -} - -fn draw_key( - ui: &Ui, - words: &mut Vec, - label: &str, - x: i16, - y: i16, - width: u16, - height: u16, - color: u32, - pressed: bool, -) { - let fill = if pressed { 0xff47_3b33 } else { color }; - rect(words, x, y, width, height, fill); - let label_width = label.len() as i16 * 12; - let text_x = x + ((width as i16 - label_width) / 2).max(6); - push_text_bold( - ui, - words, - label, - text_x, - y + (height as i16 / 2) - 10, - label.len() + 1, - if pressed { - 0xffff_ffff - } else if color == UI_ACCENT_GREEN { - 0xff00_0000 - } else { - 0xff0f_172a - }, - ); -} - -fn row_character(x: u16, start_x: u16, key_width: u16, gap: u16, characters: &str) -> Option { - characters - .chars() - .enumerate() - .find_map(|(index, character)| { - let key_x = start_x + index as u16 * (key_width + gap); - (x >= key_x && x < key_x + key_width).then_some(character) - }) -} - -fn prompt_tail(input: &str, max_bytes: usize) -> &str { - if input.len() <= max_bytes { - return input; - } - let mut start = input.len() - max_bytes; - while !input.is_char_boundary(start) { - start += 1; - } - &input[start..] -} - -fn settings_draw_list( - ui: &Ui, - state: &DeviceState, - settings: &SettingsProjection, - wifi_scroll: usize, - backend: &BackendProjection, - telemetry: SystemTelemetry, -) -> Vec { - let mut words = base_words(ui, state, "SETTINGS", telemetry); - rect(&mut words, 24, 132, 672, 154, 0xffff_ffff); - push_text_bold(ui, &mut words, "WI-FI", 44, 154, 12, 0xff0f_172a); - let network = settings - .wifi - .connected_ssid - .as_deref() - .unwrap_or("NOT CONNECTED"); - push_text_bold(ui, &mut words, network, 44, 198, 30, 0xff25_63eb); - let detail = match (&settings.wifi.ip_address, settings.wifi.rssi_dbm) { - (Some(ip), Some(rssi)) => format!("IP {ip} RSSI {rssi} DBM"), - _ if !settings.wifi.status.is_empty() => settings.wifi.status.clone(), - _ => "SCAN AND SELECT A NETWORK".to_owned(), - }; - push_text(ui, &mut words, &detail, 44, 244, 42, 0xff64_748b); - rect(&mut words, 480, 146, 196, 72, 0xff25_63eb); - push_text_bold( - ui, - &mut words, - if settings.wifi.scanning { - "SCANNING" - } else { - "SCAN" - }, - 532, - 172, - 10, - 0xffff_ffff, - ); - - push_text( - ui, - &mut words, - "AVAILABLE NETWORKS", - 28, - 294, - 28, - 0xff64_748b, - ); - if settings.wifi.networks.is_empty() { - rect(&mut words, 24, 330, 672, 112, 0xffe2_e8f0); - push_text_bold( - ui, - &mut words, - "TAP SCAN TO FIND WI-FI", - 52, - 372, - 30, - 0xff64_748b, - ); - } else { - let max_scroll = settings - .wifi - .networks - .len() - .saturating_sub(WIFI_VISIBLE_ROWS); - for (row, network) in settings - .wifi - .networks - .iter() - .skip(wifi_scroll.min(max_scroll)) - .take(WIFI_VISIBLE_ROWS) - .enumerate() - { - wifi_network_row( - ui, - &mut words, - network, - WIFI_ROW_START_Y as i16 + row as i16 * WIFI_ROW_HEIGHT as i16, - ); - } - wifi_scroll_buttons(ui, &mut words, settings.wifi.networks.len()); - } - - rect(&mut words, 24, 806, 672, 176, 0xffff_ffff); - push_text(ui, &mut words, "MODEL BACKEND", 44, 832, 24, 0xff64_748b); - push_text_bold( - ui, - &mut words, - &format!("{} / {}", backend.model, backend.link), - 44, - 874, - 40, - 0xff0f_172a, - ); - push_text(ui, &mut words, &backend.auth, 44, 920, 34, 0xff33_4155); - let storage = settings - .workspace_free_bytes - .map(format_size) - .unwrap_or_else(|| "--".into()); - push_text( - ui, - &mut words, - &format!( - "FIRMWARE {} WORKSPACE FREE {storage}", - settings.firmware_version - ), - 44, - 956, - 44, - 0xff64_748b, - ); - - rect(&mut words, 24, 1010, 316, 80, 0xffe2_e8f0); - push_text_bold(ui, &mut words, "FORGET WI-FI", 94, 1038, 18, 0xff0f_172a); - rect(&mut words, 356, 1010, 340, 80, 0xfffe_e2e2); - push_text_bold(ui, &mut words, "RESTART DEVICE", 424, 1038, 20, UI_LOSS_RED); - bottom_bar(ui, &mut words, ScreenView::Settings); - words -} - -fn wifi_network_row(ui: &Ui, words: &mut Vec, network: &WifiNetworkProjection, y: i16) { - rect(words, 24, y, 584, 84, 0xffff_ffff); - push_text_bold(ui, words, &network.ssid, 44, y + 24, 34, 0xff0f_172a); - push_text( - ui, - words, - &format!( - "{} DBM {}", - network.rssi_dbm, - if network.secured { "LOCK" } else { "OPEN" } - ), - 444, - y + 28, - 18, - 0xff64_748b, - ); -} - -fn wifi_scroll_buttons(ui: &Ui, words: &mut Vec, network_count: usize) { - let color = if network_count > WIFI_VISIBLE_ROWS { - 0xff1d_4ed8 - } else { - 0xff94_a3b8 - }; - rect(words, 628, 330, 68, 132, 0xffdb_eafe); - push_text_bold(ui, words, "UP", 646, 384, 4, color); - rect(words, 628, 650, 68, 132, 0xffdb_eafe); - push_text_bold(ui, words, "DN", 646, 704, 4, color); -} - -fn files_draw_list( - ui: &Ui, - state: &DeviceState, - browser: &WorkspaceBrowser, - telemetry: SystemTelemetry, -) -> Vec { - let title = if browser.can_go_up() { - "< WORKSPACE FILES" - } else { - "WORKSPACE FILES" - }; - let mut words = base_words(ui, state, title, telemetry); - rect(&mut words, 24, 132, 672, 48, 0xffe2_e8f0); - push_text( - ui, - &mut words, - &browser.current_path(), - 40, - 146, - 39, - 0xff33_4155, - ); - for row in 0..FILE_VISIBLE_ROWS { - let index = browser.list_offset + row; - let Some(entry) = browser.entries.get(index) else { - break; - }; - let y = FILE_ROW_START_Y + row as i16 * FILE_ROW_HEIGHT; - rect(&mut words, 24, y, 584, 94, 0xffff_ffff); - rect( - &mut words, - 42, - y + 18, - 48, - 48, - if entry.is_dir { - 0xffbf_dbfe - } else { - 0xffdc_fce7 - }, - ); - push_text( - ui, - &mut words, - if entry.is_dir { "D" } else { "F" }, - 58, - y + 30, - 2, - if entry.is_dir { - 0xff1d_4ed8 - } else { - 0xff04_7a55 - }, - ); - let name = if entry.is_dir { - format!("{}/", entry.name) - } else { - entry.name.clone() - }; - push_text_bold(ui, &mut words, &name, 108, y + 14, 29, 0xff0f_172a); - let detail = if entry.is_dir { - entry - .timestamp - .map(|timestamp| format_timestamp(Some(timestamp))) - .unwrap_or_else(|| "FOLDER".to_owned()) - } else { - format!( - "{} {}", - format_size(entry.size), - format_timestamp(entry.timestamp) - ) - }; - push_text(ui, &mut words, &detail, 108, y + 52, 29, 0xff64_748b); - } - if browser.entries.is_empty() { - push_text( - ui, - &mut words, - "THIS DIRECTORY IS EMPTY", - 64, - 280, - 34, - 0xff64_748b, - ); - } - if let Some(status) = &browser.status { - rect(&mut words, 40, 1038, 560, 72, 0xfffe_e2e2); - push_text(ui, &mut words, status, 56, 1056, 32, 0xffb9_1c1c); - } - scroll_buttons(ui, &mut words); - bottom_bar(ui, &mut words, ScreenView::Files); - words -} - -fn viewer_draw_list( - ui: &Ui, - state: &DeviceState, - browser: &WorkspaceBrowser, - telemetry: SystemTelemetry, -) -> Vec { - let mut words = base_words(ui, state, "< FILE VIEWER", telemetry); - let Some(file) = browser.open_file.as_ref() else { - push_text(ui, &mut words, "NO FILE OPEN", 64, 240, 30, 0xff64_748b); - return words; - }; - rect(&mut words, 24, 132, 584, 82, 0xffff_ffff); - push_text_bold( - ui, - &mut words, - &file.relative_path, - 40, - 146, - 32, - 0xff0f_172a, - ); - push_text( - ui, - &mut words, - &format!( - "{} {}", - format_size(file.size), - format_timestamp(file.timestamp) - ), - 40, - 180, - 32, - 0xff64_748b, - ); - rect(&mut words, 24, 228, 584, 900, 0xff0b_1220); - let visible = file - .lines - .iter() - .skip(file.line_offset) - .take(VIEWER_VISIBLE_LINES) - .cloned() - .collect::>() - .join("\n"); - push_text_limited( - ui, - &mut words, - &visible, - 44, - 248, - 32, - VIEWER_VISIBLE_LINES, - 0xffe2_e8f0, - ); - push_text( - ui, - &mut words, - &format!( - "LINES {}-{} / {}", - file.line_offset + 1, - (file.line_offset + VIEWER_VISIBLE_LINES).min(file.lines.len()), - file.lines.len() - ), - 40, - 1138, - 32, - 0xff64_748b, - ); - scroll_buttons(ui, &mut words); - words -} - -fn message_reader_draw_list( - ui: &Ui, - state: &DeviceState, - reader: Option<&MessageReader>, - telemetry: SystemTelemetry, -) -> Vec { - let mut words = base_words(ui, state, "< MESSAGE READER", telemetry); - let Some(reader) = reader else { - push_text( - ui, - &mut words, - "NO MESSAGE SELECTED", - 64, - 240, - 30, - 0xff64_748b, - ); - return words; - }; - - rect(&mut words, 24, 132, 584, 82, 0xffff_ffff); - rect( - &mut words, - 40, - 154, - if reader.author == "YOU" { 82 } else { 62 }, - 42, - if reader.author == "YOU" { - 0xffdb_eafe - } else { - 0xffdc_fce7 - }, - ); - push_text_bold( - ui, - &mut words, - reader.author, - 54, - 166, - 8, - if reader.author == "YOU" { - 0xff1d_4ed8 - } else { - 0xff04_7a55 - }, - ); - - rect(&mut words, 24, 228, 584, 900, 0xffff_ffff); - let max_scroll = reader.lines.len().saturating_sub(MESSAGE_VISIBLE_LINES); - let offset = reader.line_offset.min(max_scroll); - let visible = reader - .lines - .iter() - .skip(offset) - .take(MESSAGE_VISIBLE_LINES) - .cloned() - .collect::>() - .join("\n"); - push_text_limited( - ui, - &mut words, - &visible, - 44, - 248, - 32, - MESSAGE_VISIBLE_LINES, - 0xff0f_172a, - ); - push_text( - ui, - &mut words, - &format!( - "LINES {}-{} / {}", - offset + 1, - (offset + MESSAGE_VISIBLE_LINES).min(reader.lines.len()), - reader.lines.len() - ), - 40, - 1138, - 32, - 0xff64_748b, - ); - scroll_buttons(ui, &mut words); - words -} - -fn bottom_bar(ui: &Ui, words: &mut Vec, active: ScreenView) { - rect(words, 0, BOTTOM_BAR_Y, PANEL_WIDTH, 108, 0xff0f_172a); - for (x, label, view) in [ - (10, "CHAT", ScreenView::Chat), - (250, "FILES", ScreenView::Files), - (490, "SETTINGS", ScreenView::Settings), - ] { - rect( - words, - x, - BOTTOM_BAR_Y + 16, - 220, - 76, - if active == view { - 0xff25_63eb - } else { - 0xff1e_293b - }, - ); - let label_x = x + (220 - label.len() as i16 * 12) / 2; - push_text_bold(ui, words, label, label_x, BOTTOM_BAR_Y + 44, 8, 0xffff_ffff); - } -} - -fn chat_scroll_buttons(ui: &Ui, words: &mut Vec) { - // Align the top edge with the first chat card at y=140. - rect(words, 628, 140, 68, 132, 0xffdb_eafe); - push_text_bold(ui, words, "UP", 646, 194, 4, 0xff1d_4ed8); - rect(words, 628, 624, 68, 132, 0xffdb_eafe); - push_text_bold(ui, words, "DN", 646, 678, 4, 0xff1d_4ed8); -} - -fn scroll_buttons(ui: &Ui, words: &mut Vec) { - rect(words, 628, 190, 68, 132, 0xffdb_eafe); - push_text_bold(ui, words, "UP", 646, 244, 4, 0xff1d_4ed8); - rect(words, 628, 940, 68, 132, 0xffdb_eafe); - push_text_bold(ui, words, "DN", 646, 994, 4, 0xff1d_4ed8); -} - -fn rect(words: &mut Vec, x: i16, y: i16, width: u16, height: u16, color: u32) { - words.extend_from_slice(&[spec::draw_op::RECT, xy(x, y), wh(width, height), color]); -} - -fn push_text( - ui: &Ui, - words: &mut Vec, - text: &str, - x: i16, - y: i16, - max_columns: usize, - color: u32, -) { - push_text_limited(ui, words, text, x, y, max_columns, 20, color); -} - -fn push_text_right(ui: &Ui, words: &mut Vec, text: &str, right: i16, y: i16, color: u32) { - let width = font::text_width(ui, text, font::TextStyle::Body); - push_text(ui, words, text, right - width, y, text.len(), color); -} - -fn push_text_bold( - ui: &Ui, - words: &mut Vec, - text: &str, - x: i16, - y: i16, - max_columns: usize, - color: u32, -) { - font::append_text( - ui, - words, - text, - x, - y, - max_columns, - 20, - color, - font::TextStyle::Bold, - ); -} - -fn push_title( - ui: &Ui, - words: &mut Vec, - text: &str, - x: i16, - y: i16, - max_columns: usize, - color: u32, -) { - font::append_text( - ui, - words, - text, - x, - y, - max_columns, - 1, - color, - font::TextStyle::Title, - ); -} - -fn push_text_limited( - ui: &Ui, - words: &mut Vec, - text: &str, - x: i16, - y: i16, - max_columns: usize, - max_rows: usize, - color: u32, -) { - font::append_text( - ui, - words, - text, - x, - y, - max_columns, - max_rows, - color, - font::TextStyle::Body, - ); -} - -const fn agent_state_color(state: AgentState) -> u32 { - match state { - AgentState::Stopped => 0xff64_748b, - AgentState::Starting | AgentState::WaitingForAuth => 0xfff5_9e0b, - AgentState::Idle | AgentState::Thinking | AgentState::Acting => 0xff10_b981, - AgentState::NetworkBlocked | AgentState::Faulted => UI_LOSS_RED, - } -} - -const fn xy(x: i16, y: i16) -> u32 { - x as u16 as u32 | ((y as u16 as u32) << 16) -} - -const fn wh(width: u16, height: u16) -> u32 { - width as u32 | ((height as u32) << 16) -} diff --git a/crates/pocket-pi-device-ui/src/screen/workspace_browser.rs b/crates/pocket-pi-device-ui/src/screen/workspace_browser.rs deleted file mode 100644 index ffae28f..0000000 --- a/crates/pocket-pi-device-ui/src/screen/workspace_browser.rs +++ /dev/null @@ -1,325 +0,0 @@ -use std::fs; -use std::path::{Path, PathBuf}; -use std::time::UNIX_EPOCH; - -const MAX_VIEWER_BYTES: u64 = 256 * 1024; -const VIEWER_COLUMNS: usize = 32; - -#[derive(Clone, Debug)] -pub struct FileEntry { - pub name: String, - pub is_dir: bool, - pub size: u64, - pub timestamp: Option, -} - -#[derive(Clone, Copy, Debug)] -pub struct FileTimestamp { - pub unix_seconds: u64, - kind: TimestampKind, -} - -#[derive(Clone, Copy, Debug)] -enum TimestampKind { - Created, - Updated, - Contents, -} - -#[derive(Clone, Debug)] -pub struct OpenFile { - pub relative_path: String, - pub size: u64, - pub timestamp: Option, - pub lines: Vec, - pub line_offset: usize, -} - -#[derive(Debug)] -pub struct WorkspaceBrowser { - root: PathBuf, - current_dir: PathBuf, - pub entries: Vec, - pub list_offset: usize, - pub open_file: Option, - pub status: Option, -} - -impl WorkspaceBrowser { - pub fn new(root: impl Into) -> Self { - Self { - root: root.into(), - current_dir: PathBuf::new(), - entries: Vec::new(), - list_offset: 0, - open_file: None, - status: None, - } - } - - pub fn current_path(&self) -> String { - if self.current_dir.as_os_str().is_empty() { - "/workspace".to_owned() - } else { - format!("/workspace/{}", path_text(&self.current_dir)) - } - } - - pub fn can_go_up(&self) -> bool { - !self.current_dir.as_os_str().is_empty() - } - - pub fn refresh(&mut self) { - self.status = None; - let directory = self.root.join(&self.current_dir); - let read_dir = match fs::read_dir(&directory) { - Ok(entries) => entries, - Err(error) => { - self.status = Some(format!("CANNOT READ DIRECTORY: {error}")); - self.entries.clear(); - return; - } - }; - let mut entries = Vec::new(); - for entry in read_dir.flatten() { - let name = entry.file_name().to_string_lossy().into_owned(); - if hidden_internal_name(&name) { - continue; - } - let Ok(metadata) = entry.metadata() else { - continue; - }; - let path = entry.path(); - entries.push(FileEntry { - name, - is_dir: metadata.is_dir(), - size: if metadata.is_file() { - metadata.len() - } else { - 0 - }, - timestamp: entry_timestamp(&path, &metadata), - }); - } - entries.sort_by(|left, right| { - right - .is_dir - .cmp(&left.is_dir) - .then_with(|| left.name.to_lowercase().cmp(&right.name.to_lowercase())) - }); - self.entries = entries; - self.list_offset = self.list_offset.min(self.entries.len().saturating_sub(1)); - } - - pub fn go_up(&mut self) { - if self.current_dir.pop() { - self.list_offset = 0; - self.refresh(); - } - } - - pub fn activate_visible_row(&mut self, row: usize) -> bool { - let Some(entry) = self.entries.get(self.list_offset + row).cloned() else { - return false; - }; - if entry.is_dir { - self.current_dir.push(&entry.name); - self.list_offset = 0; - self.refresh(); - return true; - } - self.open(&entry); - self.open_file.is_some() - } - - pub fn scroll_list(&mut self, delta: isize, visible_rows: usize) { - let maximum = self.entries.len().saturating_sub(visible_rows); - self.list_offset = self.list_offset.saturating_add_signed(delta).min(maximum); - } - - pub fn scroll_file(&mut self, delta: isize, visible_lines: usize) { - if let Some(file) = self.open_file.as_mut() { - let maximum = file.lines.len().saturating_sub(visible_lines); - file.line_offset = file.line_offset.saturating_add_signed(delta).min(maximum); - } - } - - pub fn close_file(&mut self) { - self.open_file = None; - self.refresh(); - } - - fn open(&mut self, entry: &FileEntry) { - self.status = None; - let relative = self.current_dir.join(&entry.name); - let path = self.root.join(&relative); - if entry.size > MAX_VIEWER_BYTES { - self.status = Some(format!("FILE TOO LARGE: {}", format_size(entry.size))); - return; - } - match fs::read_to_string(&path) { - Ok(content) => { - self.open_file = Some(OpenFile { - relative_path: path_text(&relative), - size: entry.size, - timestamp: entry.timestamp, - lines: wrap_text(&content, VIEWER_COLUMNS), - line_offset: 0, - }); - } - Err(error) => { - self.status = Some(format!("CANNOT OPEN TEXT FILE: {error}")); - } - } - } -} - -fn entry_timestamp(path: &Path, metadata: &fs::Metadata) -> Option { - file_timestamp(metadata).or_else(|| { - metadata.is_dir().then(|| { - let mut remaining = 256; - newest_descendant_timestamp(path, 0, &mut remaining).map(|timestamp| FileTimestamp { - unix_seconds: timestamp.unix_seconds, - kind: TimestampKind::Contents, - }) - })? - }) -} - -fn newest_descendant_timestamp( - directory: &Path, - depth: usize, - remaining: &mut usize, -) -> Option { - if depth >= 8 || *remaining == 0 { - return None; - } - let mut newest = None; - for entry in fs::read_dir(directory).ok()?.flatten() { - if *remaining == 0 { - break; - } - *remaining -= 1; - let name = entry.file_name().to_string_lossy().into_owned(); - if hidden_internal_name(&name) { - continue; - } - let Ok(metadata) = entry.metadata() else { - continue; - }; - let candidate = file_timestamp(&metadata).or_else(|| { - metadata - .is_dir() - .then(|| newest_descendant_timestamp(&entry.path(), depth + 1, remaining))? - }); - if candidate.is_some_and(|candidate| { - newest - .is_none_or(|current: FileTimestamp| candidate.unix_seconds > current.unix_seconds) - }) { - newest = candidate; - } - } - newest -} - -fn hidden_internal_name(name: &str) -> bool { - name == ".pi-agent" || (name.starts_with(".ppi-") && name.ends_with(".tmp")) -} - -fn file_timestamp(metadata: &fs::Metadata) -> Option { - if let Ok(created) = metadata.created() { - if let Ok(duration) = created.duration_since(UNIX_EPOCH) { - let seconds = duration.as_secs(); - if valid_wall_clock(seconds) { - return Some(FileTimestamp { - unix_seconds: seconds, - kind: TimestampKind::Created, - }); - } - } - } - // LittleFS primarily exposes mtime. Some VFS versions return a zero/epoch - // creation time even when the modification time is valid, so creation must - // not prevent this fallback. - let seconds = metadata - .modified() - .ok()? - .duration_since(UNIX_EPOCH) - .ok()? - .as_secs(); - valid_wall_clock(seconds).then_some(FileTimestamp { - unix_seconds: seconds, - kind: TimestampKind::Updated, - }) -} - -fn valid_wall_clock(seconds: u64) -> bool { - // An ESP32 without SNTP commonly starts at the Unix epoch. Do not present - // that as a real file date. - seconds >= 1_577_836_800 -} - -fn wrap_text(content: &str, columns: usize) -> Vec { - let mut output = Vec::new(); - for physical_line in content.replace('\r', "").split('\n') { - let characters = physical_line.chars().collect::>(); - if characters.is_empty() { - output.push(String::new()); - continue; - } - for chunk in characters.chunks(columns) { - output.push(chunk.iter().collect()); - } - } - if output.is_empty() { - output.push(String::new()); - } - output -} - -pub fn format_size(bytes: u64) -> String { - if bytes < 1024 { - format!("{bytes} B") - } else if bytes < 1024 * 1024 { - format!("{} KB", bytes.div_ceil(1024)) - } else { - format!("{} MB", bytes.div_ceil(1024 * 1024)) - } -} - -pub fn format_timestamp(timestamp: Option) -> String { - let Some(timestamp) = timestamp else { - return "TIME UNKNOWN".to_owned(); - }; - let seconds = timestamp.unix_seconds as i64; - let days = seconds.div_euclid(86_400); - let seconds_of_day = seconds.rem_euclid(86_400); - let (year, month, day) = civil_from_days(days); - let hour = seconds_of_day / 3_600; - let minute = (seconds_of_day % 3_600) / 60; - let label = match timestamp.kind { - TimestampKind::Created => "CREATED", - TimestampKind::Updated => "UPDATED", - TimestampKind::Contents => "CONTENTS", - }; - format!("{label} {year:04}-{month:02}-{day:02} {hour:02}:{minute:02}Z") -} - -fn civil_from_days(days_since_epoch: i64) -> (i64, i64, i64) { - let z = days_since_epoch + 719_468; - let era = if z >= 0 { z } else { z - 146_096 } / 146_097; - let day_of_era = z - era * 146_097; - let year_of_era = - (day_of_era - day_of_era / 1_460 + day_of_era / 36_524 - day_of_era / 146_096) / 365; - let mut year = year_of_era + era * 400; - let day_of_year = day_of_era - (365 * year_of_era + year_of_era / 4 - year_of_era / 100); - let month_prime = (5 * day_of_year + 2) / 153; - let day = day_of_year - (153 * month_prime + 2) / 5 + 1; - let month = month_prime + if month_prime < 10 { 3 } else { -9 }; - year += i64::from(month <= 2); - (year, month, day) -} - -fn path_text(path: &Path) -> String { - path.to_string_lossy().replace('\\', "/") -} diff --git a/crates/pocket-pi-embedded/Cargo.toml b/crates/pocket-pi-embedded/Cargo.toml index ee5c22d..5b7ac22 100644 --- a/crates/pocket-pi-embedded/Cargo.toml +++ b/crates/pocket-pi-embedded/Cargo.toml @@ -7,5 +7,9 @@ repository.workspace = true description = "Small pi-agent-core runtime for embedded Pocket Pi hosts" [dependencies] +pocket-mod.workspace = true +pocket-pi-protocols.workspace = true +# pocket-mod re-exports these types but does not enable target bindings. +# This direct edge unifies rquickjs features so ESP-IDF generates its bindings. rquickjs = { version = "=0.12.1", default-features = false, features = ["bindgen", "std"] } serde_json.workspace = true diff --git a/crates/pocket-pi-embedded/js/env.d.ts b/crates/pocket-pi-embedded/js/env.d.ts index e0ee54d..41e2105 100644 --- a/crates/pocket-pi-embedded/js/env.d.ts +++ b/crates/pocket-pi-embedded/js/env.d.ts @@ -1,15 +1,16 @@ declare global { var host: | { - modelComplete(request: string): string; - tool(callId: string, name: string, args: string): string; + startModel(request: string): number; + startTool(callId: string, name: string, args: string): number; + poll(): string; } | undefined; var PocketPiEmbedded: | { boot(config: string): void; prompt(text: string): void; - abort(): void; + tick(): void; drain(): string; } | undefined; diff --git a/crates/pocket-pi-embedded/js/pi-agent.bundle.js b/crates/pocket-pi-embedded/js/pi-agent.bundle.js index d9fb098..b22c52f 100644 --- a/crates/pocket-pi-embedded/js/pi-agent.bundle.js +++ b/crates/pocket-pi-embedded/js/pi-agent.bundle.js @@ -384,11 +384,21 @@ if(this.activeRun){throw new Error("Agent is already processing.")}const abortCo const failureMessage={role:"assistant",content:[{type:"text",text:""}],api:this._state.model.api,provider:this._state.model.provider,model:this._state.model.id,usage:EMPTY_USAGE,stopReason:aborted?"aborted":"error",errorMessage:error instanceof Error?error.message:String(error),timestamp:Date.now()};await this.processEvents({type:"message_start",message:failureMessage});await this.processEvents({type:"message_end",message:failureMessage});await this.processEvents({type:"turn_end",message:failureMessage, toolResults:[]});await this.processEvents({type:"agent_end",messages:[failureMessage]})}finishRun(){this._state.isStreaming=false;this._state.streamingMessage=void 0;this._state.pendingToolCalls=new Set;this.activeRun?.resolve();this.activeRun=void 0}async processEvents(event){switch(event.type){case"message_start":this._state.streamingMessage=event.message;break;case"message_update":this._state.streamingMessage=event.message;break;case"message_end":this._state.streamingMessage=void 0;this._state. messages.push(event.message);break;case"tool_execution_start":{const pendingToolCalls=new Set(this._state.pendingToolCalls);pendingToolCalls.add(event.toolCallId);this._state.pendingToolCalls=pendingToolCalls;break}case"tool_execution_end":{const pendingToolCalls=new Set(this._state.pendingToolCalls);pendingToolCalls.delete(event.toolCallId);this._state.pendingToolCalls=pendingToolCalls;break}case"turn_end":if(event.message.role==="assistant"&&event.message.errorMessage){this._state.errorMessage= -event.message.errorMessage}break;case"agent_end":this._state.streamingMessage=void 0;break}const signal=this.activeRun?.abortController.signal;if(!signal){throw new Error("Agent listener invoked outside active run")}for(const listener of this.listeners){await listener(event,signal)}}};var events=[];var agent=null;var emptyUsage=()=>({input:0,output:0,cacheRead:0,cacheWrite:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}});function modelFor(config){const provider=config.provider||"openai";return{id:config.model||"gpt-5-mini",name:config.model||"gpt-5-mini",provider,api:provider==="anthropic"?"anthropic-messages":"openai-responses",baseUrl:provider==="anthropic"?"https://api.anthropic.com":"https://api.openai.com/v1",reasoning:false,input:["text"],cost:{ -input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:128e3,maxTokens:1024}}function hostStream(model,context){const stream=new AssistantMessageEventStream;queueMicrotask(()=>{try{const result=JSON.parse(globalThis.host?.modelComplete(JSON.stringify({model,context}))||"{}");const partial={role:"assistant",content:[],api:model.api,provider:model.provider,model:model.id,usage:emptyUsage(),stopReason:result.stopReason||"stop",timestamp:Date.now()};stream.push({type:"start",partial});if(result.toolCall){ -const toolCall={type:"toolCall",id:result.toolCall.id||`tool_${Date.now()}`,name:result.toolCall.name,arguments:result.toolCall.arguments||{}};partial.content=[toolCall];partial.stopReason="toolUse";stream.push({type:"toolcall_start",contentIndex:0,partial:{...partial}});stream.push({type:"toolcall_end",contentIndex:0,toolCall,partial:{...partial}});stream.push({type:"done",reason:"toolUse",message:{...partial}})}else{const text=String(result.text||"");partial.content=[{type:"text",text}];stream. -push({type:"text_start",contentIndex:0,partial:{...partial}});stream.push({type:"text_delta",contentIndex:0,delta:text,partial:{...partial}});stream.push({type:"text_end",contentIndex:0,content:text,partial:{...partial}});stream.push({type:"done",reason:partial.stopReason,message:{...partial}})}}catch(error){stream.push({type:"error",reason:"error",error:{role:"assistant",content:[],api:model.api,provider:model.provider,model:model.id,usage:emptyUsage(),stopReason:"error",errorMessage:String(error), -timestamp:Date.now()}})}});return stream}function boot(configJson){const config=JSON.parse(configJson);const model=modelFor(config);const tools=(config.tools||[]).map(tool=>({name:tool.name,label:tool.label||tool.name,description:tool.description||"",parameters:tool.parameters||{type:"object",properties:{}},executionMode:"sequential",execute:async(id,args)=>{const result=JSON.parse(globalThis.host?.tool(id,tool.name,JSON.stringify(args||{}))||JSON.stringify({text:`tool unavailable: ${tool.name}`, -isError:true}));if(result.isError)throw new Error(String(result.text||`tool failed: ${tool.name}`));return{content:[{type:"text",text:String(result.text||"")}],details:result.details,terminate:Boolean(result.terminate)}}}));agent=new Agent({initialState:{systemPrompt:config.systemPrompt||"You are Pocket Pi running on an embedded device.",model,thinkingLevel:"off",tools},streamFn:hostStream,toolExecution:"sequential"});agent.subscribe(event=>{const compact={type:event.type};if(event.type==="messa\ -ge_update"){compact.kind=event.assistantMessageEvent?.type;compact.delta=event.assistantMessageEvent?.delta}else if(event.type==="message_end"){compact.role=event.message?.role;compact.stopReason=event.message?.stopReason}else if(event.type==="tool_execution_start"||event.type==="tool_execution_end"){compact.name=event.toolName;compact.toolCallId=event.toolCallId;compact.isError=Boolean(event.isError)}events.push(compact)});events.push({type:"agent_ready"})}function prompt(text){if(!agent)throw new Error( -"prompt before boot");void agent.prompt(text).catch(error=>events.push({type:"agent_error",message:String(error)}))}function abort(){agent?.abort()}function drain(){return JSON.stringify({phase:agent?.state.isStreaming?"thinking":agent?"ready":"idle",messages:agent?.state.messages.length||0,events:events.splice(0,events.length)})}globalThis.PocketPiEmbedded={boot,prompt,abort,drain};})(); +event.message.errorMessage}break;case"agent_end":this._state.streamingMessage=void 0;break}const signal=this.activeRun?.abortController.signal;if(!signal){throw new Error("Agent listener invoked outside active run")}for(const listener of this.listeners){await listener(event,signal)}}};var events=[];var pendingModels=new Map;var pendingTools=new Map;var agent=null;var emptyUsage=()=>({input:0,output:0,cacheRead:0,cacheWrite:0,reasoning:0,totalTokens:0,cost:{input:0,output:0,cacheRead:0,cacheWrite:0,total:0}});function modelFor(config){const provider=config.provider||"openai";const deepseek=provider==="deepseek";const anthropic=provider==="anthropic";const defaultModel=deepseek?"deepseek-v4-flash":"gpt-5-mini";return{id:config.model||defaultModel,name:config.model||defaultModel, +provider,api:anthropic?"anthropic-messages":"openai-completions",baseUrl:deepseek?"https://api.deepseek.com":anthropic?"https://api.anthropic.com":"https://api.openai.com/v1",reasoning:deepseek,thinkingLevelMap:deepseek?{off:null,minimal:null,low:null,medium:null,high:"high",xhigh:"max",max:"max"}:void 0,input:["text"],cost:{input:0,output:0,cacheRead:0,cacheWrite:0},contextWindow:deepseek?1e6:128e3,maxTokens:deepseek?384e3:16384}}function hostStream(model,context,options={}){const stream=new AssistantMessageEventStream; +const partial={role:"assistant",content:[],api:model.api,provider:model.provider,model:model.id,usage:emptyUsage(),stopReason:"stop",timestamp:Date.now()};try{if(!globalThis.host)throw new Error("Pocket Pi Agent host is unavailable");const id=globalThis.host.startModel(JSON.stringify({model,context,options}));pendingModels.set(id,{stream,model,partial,started:false,thinkingStarted:false,textStarted:false,thinking:"",text:""})}catch(error){pushModelError(stream,model,String(error))}return stream} +function ensureModelStarted(pending){if(pending.started)return;pending.started=true;pending.stream.push({type:"start",partial:{...pending.partial}})}function syncContent(pending){const content=[];if(pending.thinkingStarted){content.push({type:"thinking",thinking:pending.thinking,thinkingSignature:"reasoning_content"})}if(pending.textStarted)content.push({type:"text",text:pending.text});pending.partial.content=content}function pushThinkingDelta(pending,delta){if(!delta)return;if(pending.textStarted) +throw new Error("thinking delta arrived after text output started");ensureModelStarted(pending);if(!pending.thinkingStarted){pending.thinkingStarted=true;syncContent(pending);pending.stream.push({type:"thinking_start",contentIndex:0,partial:{...pending.partial}})}pending.thinking+=delta;syncContent(pending);pending.stream.push({type:"thinking_delta",contentIndex:0,delta,partial:{...pending.partial}})}function textIndex(pending){return pending.thinkingStarted?1:0}function pushTextDelta(pending,delta){ +if(!delta)return;ensureModelStarted(pending);if(!pending.textStarted){pending.textStarted=true;syncContent(pending);pending.stream.push({type:"text_start",contentIndex:textIndex(pending),partial:{...pending.partial}})}pending.text+=delta;syncContent(pending);pending.stream.push({type:"text_delta",contentIndex:textIndex(pending),delta,partial:{...pending.partial}})}function appendFinalDelta(current,complete,append,label){if(current===complete)return;if(!complete.startsWith(current))throw new Error( +`${label} stream does not match final result`);append(complete.slice(current.length))}function finishModel(pending,result){ensureModelStarted(pending);if(typeof result.thinking!=="string"||typeof result.text!=="string"){throw new Error("model result is missing thinking or text")}if(!Array.isArray(result.toolCalls))throw new Error("model result is missing toolCalls");if(!result.usage||typeof result.usage!=="object"){throw new Error("model result is missing usage")}if(result.thinking&&typeof result. +thinkingSignature!=="string"){throw new Error("thinking result is missing thinkingSignature")}if(!["stop","length","toolUse"].includes(result.stopReason)){throw new Error("model result has an invalid stopReason")}appendFinalDelta(pending.thinking,result.thinking,delta=>pushThinkingDelta(pending,delta),"thinking");appendFinalDelta(pending.text,result.text,delta=>pushTextDelta(pending,delta),"text");pending.partial.usage={...emptyUsage(),...result.usage};if(pending.thinkingStarted){const thinking=pending. +partial.content[0];thinking.thinkingSignature=result.thinkingSignature;pending.stream.push({type:"thinking_end",contentIndex:0,content:pending.thinking,partial:{...pending.partial}})}if(pending.textStarted){pending.stream.push({type:"text_end",contentIndex:textIndex(pending),content:pending.text,partial:{...pending.partial}})}if(result.toolCalls.length>0&&result.stopReason!=="toolUse"){throw new Error("tool calls require toolUse stopReason")}if(result.toolCalls.length===0&&result.stopReason==="t\ +oolUse"){throw new Error("toolUse stopReason requires tool calls")}for(const call of result.toolCalls){if(!call.id||!call.name||!call.arguments||typeof call.arguments!=="object"||Array.isArray(call.arguments)){throw new Error("model result contains an invalid tool call")}const toolCall={type:"toolCall",id:call.id,name:call.name,arguments:call.arguments};const contentIndex=pending.partial.content.length;pending.partial.content=[...pending.partial.content,toolCall];pending.stream.push({type:"toolc\ +all_start",contentIndex,partial:{...pending.partial}});pending.stream.push({type:"toolcall_end",contentIndex,toolCall,partial:{...pending.partial}})}if(pending.partial.content.length===0)throw new Error("model result contains no decision");pending.partial.stopReason=result.stopReason;pending.stream.push({type:"done",reason:result.stopReason,message:{...pending.partial}})}function pushModelError(stream,model,message){stream.push({type:"error",reason:"error",error:{role:"assistant",content:[],api:model. +api,provider:model.provider,model:model.id,usage:emptyUsage(),stopReason:"error",errorMessage:message,timestamp:Date.now()}})}function boot(configJson){const config=JSON.parse(configJson);const model=modelFor(config);const tools=(config.tools||[]).map(tool=>({name:tool.name,label:tool.label||tool.name,description:tool.description||"",parameters:tool.parameters||{type:"object",properties:{}},executionMode:"sequential",execute:(id,args)=>new Promise((resolve,reject)=>{try{if(!globalThis.host)throw new Error( +"Pocket Pi Agent host is unavailable");const requestId=globalThis.host.startTool(id,tool.name,JSON.stringify(args||{}));pendingTools.set(requestId,{resolve,reject})}catch(error){reject(error instanceof Error?error:new Error(String(error)))}})}));agent=new Agent({initialState:{systemPrompt:config.systemPrompt||"You are Pocket Pi running on an embedded device.",model,thinkingLevel:model.reasoning?config.thinkingLevel||"high":"off",tools},streamFn:hostStream,toolExecution:"sequential"});agent.subscribe( +event=>{const compact={type:event.type};if(event.type==="message_update"){compact.kind=event.assistantMessageEvent?.type;compact.delta=event.assistantMessageEvent?.delta}else if(event.type==="message_end"){compact.role=event.message?.role;compact.stopReason=event.message?.stopReason}else if(event.type==="tool_execution_start"||event.type==="tool_execution_end"){compact.name=event.toolName;compact.toolCallId=event.toolCallId;compact.isError=Boolean(event.isError)}events.push(compact)});events.push( +{type:"agent_ready"})}function prompt(text){if(!agent)throw new Error("prompt before boot");void agent.prompt(text).catch(error=>events.push({type:"agent_error",message:String(error)}))}function tick(){const batch=JSON.parse(globalThis.host?.poll()||"[]");for(const event of batch){if(event.type==="model_progress"){const pending=pendingModels.get(event.id);if(!pending)continue;pushThinkingDelta(pending,event.thinkingDelta);pushTextDelta(pending,event.textDelta)}else if(event.type==="model_done"){ +const pending=pendingModels.get(event.id);if(!pending)continue;pendingModels.delete(event.id);try{finishModel(pending,JSON.parse(event.result))}catch(error){pushModelError(pending.stream,pending.model,String(error))}}else if(event.type==="model_error"){const pending=pendingModels.get(event.id);if(!pending)continue;pendingModels.delete(event.id);pushModelError(pending.stream,pending.model,event.error)}else if(event.type==="tool_done"){const pending=pendingTools.get(event.id);if(!pending)continue; +pendingTools.delete(event.id);try{const result=JSON.parse(event.result);if(result.isError)throw new Error(String(result.text||"App tool failed"));pending.resolve({content:[{type:"text",text:String(result.text||"")}],details:result.details,terminate:Boolean(result.terminate)})}catch(error){pending.reject(error instanceof Error?error:new Error(String(error)))}}}}function drain(){return JSON.stringify({phase:agent?.state.isStreaming?"thinking":agent?"ready":"idle",messages:agent?.state.messages.length|| +0,events:events.splice(0,events.length)})}globalThis.PocketPiEmbedded={boot,prompt,tick,drain};})(); diff --git a/crates/pocket-pi-embedded/js/src/entry.ts b/crates/pocket-pi-embedded/js/src/entry.ts index 22f6966..d80b044 100644 --- a/crates/pocket-pi-embedded/js/src/entry.ts +++ b/crates/pocket-pi-embedded/js/src/entry.ts @@ -4,6 +4,7 @@ import { AssistantMessageEventStream } from "../node_modules/@earendil-works/pi- type Config = { model?: string; provider?: string; + thinkingLevel?: "high" | "xhigh"; systemPrompt?: string; tools?: Array<{ name: string; @@ -13,13 +14,44 @@ type Config = { }>; }; +type Usage = ReturnType; + type ModelResult = { - text?: string; - stopReason?: "stop" | "length"; - toolCall?: { id?: string; name: string; arguments?: Record }; + thinking: string; + thinkingSignature?: string; + text: string; + toolCalls: Array<{ + id: string; + name: string; + arguments: Record; + }>; + usage: Partial; + stopReason: "stop" | "length" | "toolUse"; +}; + +type HostEvent = + | { type: "model_progress"; id: number; thinkingDelta: string; textDelta: string } + | { type: "model_done"; id: number; result: string } + | { type: "model_error"; id: number; error: string } + | { type: "tool_done"; id: number; result: string }; + +type PendingModel = { + stream: AssistantMessageEventStream; + model: any; + partial: any; + started: boolean; + thinkingStarted: boolean; + textStarted: boolean; + thinking: string; + text: string; }; const events: unknown[] = []; +const pendingModels = new Map(); +const pendingTools = new Map< + number, + { resolve: (value: any) => void; reject: (error: Error) => void } +>(); let agent: Agent | null = null; const emptyUsage = () => ({ @@ -27,81 +59,245 @@ const emptyUsage = () => ({ output: 0, cacheRead: 0, cacheWrite: 0, + reasoning: 0, totalTokens: 0, cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0, total: 0 }, }); function modelFor(config: Config): any { const provider = config.provider || "openai"; + const deepseek = provider === "deepseek"; + const anthropic = provider === "anthropic"; + const defaultModel = deepseek ? "deepseek-v4-flash" : "gpt-5-mini"; return { - id: config.model || "gpt-5-mini", - name: config.model || "gpt-5-mini", + id: config.model || defaultModel, + name: config.model || defaultModel, provider, - api: provider === "anthropic" ? "anthropic-messages" : "openai-responses", - baseUrl: provider === "anthropic" ? "https://api.anthropic.com" : "https://api.openai.com/v1", - reasoning: false, + api: anthropic ? "anthropic-messages" : "openai-completions", + baseUrl: deepseek + ? "https://api.deepseek.com" + : anthropic + ? "https://api.anthropic.com" + : "https://api.openai.com/v1", + reasoning: deepseek, + thinkingLevelMap: deepseek + ? { off: null, minimal: null, low: null, medium: null, high: "high", xhigh: "max", max: "max" } + : undefined, input: ["text"], cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 128000, - maxTokens: 1024, + contextWindow: deepseek ? 1_000_000 : 128_000, + maxTokens: deepseek ? 384_000 : 16_384, }; } -function hostStream(model: any, context: any): AssistantMessageEventStream { +function hostStream(model: any, context: any, options: any = {}): AssistantMessageEventStream { const stream = new AssistantMessageEventStream(); - queueMicrotask(() => { - try { - const result = JSON.parse(globalThis.host?.modelComplete(JSON.stringify({ model, context })) || "{}") as ModelResult; - const partial: any = { - role: "assistant", - content: [], - api: model.api, - provider: model.provider, - model: model.id, - usage: emptyUsage(), - stopReason: result.stopReason || "stop", - timestamp: Date.now(), - }; - stream.push({ type: "start", partial }); - if (result.toolCall) { - const toolCall = { - type: "toolCall" as const, - id: result.toolCall.id || `tool_${Date.now()}`, - name: result.toolCall.name, - arguments: result.toolCall.arguments || {}, - }; - partial.content = [toolCall]; - partial.stopReason = "toolUse"; - stream.push({ type: "toolcall_start", contentIndex: 0, partial: { ...partial } }); - stream.push({ type: "toolcall_end", contentIndex: 0, toolCall, partial: { ...partial } }); - stream.push({ type: "done", reason: "toolUse", message: { ...partial } }); - } else { - const text = String(result.text || ""); - partial.content = [{ type: "text", text }]; - stream.push({ type: "text_start", contentIndex: 0, partial: { ...partial } }); - stream.push({ type: "text_delta", contentIndex: 0, delta: text, partial: { ...partial } }); - stream.push({ type: "text_end", contentIndex: 0, content: text, partial: { ...partial } }); - stream.push({ type: "done", reason: partial.stopReason, message: { ...partial } }); - } - } catch (error) { - stream.push({ - type: "error", - reason: "error", - error: { - role: "assistant", - content: [], - api: model.api, - provider: model.provider, - model: model.id, - usage: emptyUsage(), - stopReason: "error", - errorMessage: String(error), - timestamp: Date.now(), - }, - }); + const partial: any = { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: emptyUsage(), + stopReason: "stop", + timestamp: Date.now(), + }; + try { + if (!globalThis.host) throw new Error("Pocket Pi Agent host is unavailable"); + const id = globalThis.host.startModel(JSON.stringify({ model, context, options })); + pendingModels.set(id, { + stream, + model, + partial, + started: false, + thinkingStarted: false, + textStarted: false, + thinking: "", + text: "", + }); + } catch (error) { + pushModelError(stream, model, String(error)); + } + return stream; +} + +function ensureModelStarted(pending: PendingModel): void { + if (pending.started) return; + pending.started = true; + pending.stream.push({ type: "start", partial: { ...pending.partial } }); +} + +function syncContent(pending: PendingModel): void { + const content: any[] = []; + if (pending.thinkingStarted) { + content.push({ + type: "thinking", + thinking: pending.thinking, + thinkingSignature: "reasoning_content", + }); + } + if (pending.textStarted) content.push({ type: "text", text: pending.text }); + pending.partial.content = content; +} + +function pushThinkingDelta(pending: PendingModel, delta: string): void { + if (!delta) return; + if (pending.textStarted) throw new Error("thinking delta arrived after text output started"); + ensureModelStarted(pending); + if (!pending.thinkingStarted) { + pending.thinkingStarted = true; + syncContent(pending); + pending.stream.push({ type: "thinking_start", contentIndex: 0, partial: { ...pending.partial } }); + } + pending.thinking += delta; + syncContent(pending); + pending.stream.push({ + type: "thinking_delta", + contentIndex: 0, + delta, + partial: { ...pending.partial }, + }); +} + +function textIndex(pending: PendingModel): number { + return pending.thinkingStarted ? 1 : 0; +} + +function pushTextDelta(pending: PendingModel, delta: string): void { + if (!delta) return; + ensureModelStarted(pending); + if (!pending.textStarted) { + pending.textStarted = true; + syncContent(pending); + pending.stream.push({ + type: "text_start", + contentIndex: textIndex(pending), + partial: { ...pending.partial }, + }); + } + pending.text += delta; + syncContent(pending); + pending.stream.push({ + type: "text_delta", + contentIndex: textIndex(pending), + delta, + partial: { ...pending.partial }, + }); +} + +function appendFinalDelta( + current: string, + complete: string, + append: (delta: string) => void, + label: string, +): void { + if (current === complete) return; + if (!complete.startsWith(current)) throw new Error(`${label} stream does not match final result`); + append(complete.slice(current.length)); +} + +function finishModel(pending: PendingModel, result: ModelResult): void { + ensureModelStarted(pending); + if (typeof result.thinking !== "string" || typeof result.text !== "string") { + throw new Error("model result is missing thinking or text"); + } + if (!Array.isArray(result.toolCalls)) throw new Error("model result is missing toolCalls"); + if (!result.usage || typeof result.usage !== "object") { + throw new Error("model result is missing usage"); + } + if (result.thinking && typeof result.thinkingSignature !== "string") { + throw new Error("thinking result is missing thinkingSignature"); + } + if (!(["stop", "length", "toolUse"] as const).includes(result.stopReason)) { + throw new Error("model result has an invalid stopReason"); + } + appendFinalDelta( + pending.thinking, + result.thinking, + (delta) => pushThinkingDelta(pending, delta), + "thinking", + ); + appendFinalDelta( + pending.text, + result.text, + (delta) => pushTextDelta(pending, delta), + "text", + ); + + pending.partial.usage = { ...emptyUsage(), ...result.usage }; + if (pending.thinkingStarted) { + const thinking = pending.partial.content[0]; + thinking.thinkingSignature = result.thinkingSignature; + pending.stream.push({ + type: "thinking_end", + contentIndex: 0, + content: pending.thinking, + partial: { ...pending.partial }, + }); + } + if (pending.textStarted) { + pending.stream.push({ + type: "text_end", + contentIndex: textIndex(pending), + content: pending.text, + partial: { ...pending.partial }, + }); + } + + if (result.toolCalls.length > 0 && result.stopReason !== "toolUse") { + throw new Error("tool calls require toolUse stopReason"); + } + if (result.toolCalls.length === 0 && result.stopReason === "toolUse") { + throw new Error("toolUse stopReason requires tool calls"); + } + for (const call of result.toolCalls) { + if ( + !call.id || + !call.name || + !call.arguments || + typeof call.arguments !== "object" || + Array.isArray(call.arguments) + ) { + throw new Error("model result contains an invalid tool call"); } + const toolCall = { + type: "toolCall" as const, + id: call.id, + name: call.name, + arguments: call.arguments, + }; + const contentIndex = pending.partial.content.length; + pending.partial.content = [...pending.partial.content, toolCall]; + pending.stream.push({ type: "toolcall_start", contentIndex, partial: { ...pending.partial } }); + pending.stream.push({ + type: "toolcall_end", + contentIndex, + toolCall, + partial: { ...pending.partial }, + }); + } + if (pending.partial.content.length === 0) throw new Error("model result contains no decision"); + + pending.partial.stopReason = result.stopReason; + pending.stream.push({ type: "done", reason: result.stopReason, message: { ...pending.partial } }); +} + +function pushModelError(stream: AssistantMessageEventStream, model: any, message: string): void { + stream.push({ + type: "error", + reason: "error", + error: { + role: "assistant", + content: [], + api: model.api, + provider: model.provider, + model: model.id, + usage: emptyUsage(), + stopReason: "error", + errorMessage: message, + timestamp: Date.now(), + }, }); - return stream; } function boot(configJson: string): void { @@ -113,25 +309,23 @@ function boot(configJson: string): void { description: tool.description || "", parameters: tool.parameters || { type: "object", properties: {} }, executionMode: "sequential" as const, - execute: async (id: string, args: unknown) => { - const result = JSON.parse( - globalThis.host?.tool(id, tool.name, JSON.stringify(args || {})) || - JSON.stringify({ text: `tool unavailable: ${tool.name}`, isError: true }), - ); - if (result.isError) throw new Error(String(result.text || `tool failed: ${tool.name}`)); - return { - content: [{ type: "text" as const, text: String(result.text || "") }], - details: result.details, - terminate: Boolean(result.terminate), - }; - }, + execute: (id: string, args: unknown) => + new Promise((resolve, reject) => { + try { + if (!globalThis.host) throw new Error("Pocket Pi Agent host is unavailable"); + const requestId = globalThis.host.startTool(id, tool.name, JSON.stringify(args || {})); + pendingTools.set(requestId, { resolve, reject }); + } catch (error) { + reject(error instanceof Error ? error : new Error(String(error))); + } + }), })); agent = new Agent({ initialState: { systemPrompt: config.systemPrompt || "You are Pocket Pi running on an embedded device.", model, - thinkingLevel: "off", + thinkingLevel: model.reasoning ? config.thinkingLevel || "high" : "off", tools, }, streamFn: hostStream as any, @@ -160,8 +354,45 @@ function prompt(text: string): void { void agent.prompt(text).catch((error) => events.push({ type: "agent_error", message: String(error) })); } -function abort(): void { - agent?.abort(); +function tick(): void { + const batch = JSON.parse(globalThis.host?.poll() || "[]") as HostEvent[]; + for (const event of batch) { + if (event.type === "model_progress") { + const pending = pendingModels.get(event.id); + if (!pending) continue; + pushThinkingDelta(pending, event.thinkingDelta); + pushTextDelta(pending, event.textDelta); + } else if (event.type === "model_done") { + const pending = pendingModels.get(event.id); + if (!pending) continue; + pendingModels.delete(event.id); + try { + finishModel(pending, JSON.parse(event.result) as ModelResult); + } catch (error) { + pushModelError(pending.stream, pending.model, String(error)); + } + } else if (event.type === "model_error") { + const pending = pendingModels.get(event.id); + if (!pending) continue; + pendingModels.delete(event.id); + pushModelError(pending.stream, pending.model, event.error); + } else if (event.type === "tool_done") { + const pending = pendingTools.get(event.id); + if (!pending) continue; + pendingTools.delete(event.id); + try { + const result = JSON.parse(event.result); + if (result.isError) throw new Error(String(result.text || "App tool failed")); + pending.resolve({ + content: [{ type: "text" as const, text: String(result.text || "") }], + details: result.details, + terminate: Boolean(result.terminate), + }); + } catch (error) { + pending.reject(error instanceof Error ? error : new Error(String(error))); + } + } + } } function drain(): string { @@ -172,4 +403,4 @@ function drain(): string { }); } -globalThis.PocketPiEmbedded = { boot, prompt, abort, drain }; +globalThis.PocketPiEmbedded = { boot, prompt, tick, drain }; diff --git a/crates/pocket-pi-embedded/src/lib.rs b/crates/pocket-pi-embedded/src/lib.rs index 2baa5f8..373c64c 100644 --- a/crates/pocket-pi-embedded/src/lib.rs +++ b/crates/pocket-pi-embedded/src/lib.rs @@ -1,19 +1,19 @@ -use std::sync::{mpsc, Arc}; +use std::sync::atomic::{AtomicI32, Ordering}; +use std::sync::{mpsc, Arc, Mutex}; -use rquickjs::{CatchResultExt, Context, Function, Object, Runtime}; +use pocket_mod::qjs::{CatchResultExt, Function, Object}; +use pocket_mod::Guest; +pub use pocket_pi_protocols::model::ModelStreamEvent; -const AGENT_BUNDLE: &str = include_str!("../js/pi-agent.bundle.js"); const PRELUDE: &str = include_str!("../js/prelude.js"); -#[cfg(target_os = "espidf")] -const QUICKJS_STACK_LIMIT: usize = 96 * 1024; -#[cfg(not(target_os = "espidf"))] -const QUICKJS_STACK_LIMIT: usize = 512 * 1024; +pub const MODEL_WORKER_STACK_BYTES: usize = 64 * 1024; +const TOOL_WORKER_STACK_BYTES: usize = 16 * 1024; pub trait ModelBackend: Send + Sync { fn complete( &self, request_json: &str, - on_delta: &mut dyn FnMut(&str), + on_event: &mut dyn FnMut(ModelStreamEvent), ) -> Result; } @@ -33,55 +33,11 @@ pub struct ToolResult { #[derive(Clone, Debug, Eq, PartialEq)] pub enum AgentEvent { Ready, - Delta(String), + ResponseText(String), Done, Failed(String), } -pub fn spawn_agent_worker( - config_json: String, - backend: Arc, - tools: Arc, - stack_size: Option, -) -> Result<(mpsc::Sender, mpsc::Receiver), String> { - let (prompt_tx, prompt_rx) = mpsc::channel::(); - let (event_tx, event_rx) = mpsc::channel::(); - let mut builder = std::thread::Builder::new().name("pi-agent".to_owned()); - if let Some(stack_size) = stack_size { - builder = builder.stack_size(stack_size); - } - builder - .spawn(move || { - let delta_tx = event_tx.clone(); - let runtime = PiEmbedded::new( - &config_json, - backend, - tools, - Arc::new(move |delta| { - delta_tx.send(AgentEvent::Delta(delta)).ok(); - }), - ); - let runtime = match runtime { - Ok(runtime) => runtime, - Err(error) => { - event_tx.send(AgentEvent::Failed(error)).ok(); - return; - } - }; - event_tx.send(AgentEvent::Ready).ok(); - for prompt in prompt_rx { - let result = runtime.prompt(&prompt).and_then(|()| runtime.pump()); - let event = match result { - Ok(_) => AgentEvent::Done, - Err(error) => AgentEvent::Failed(error), - }; - event_tx.send(event).ok(); - } - }) - .map_err(|error| format!("spawn Pi Agent worker: {error}"))?; - Ok((prompt_tx, event_rx)) -} - impl ToolResult { pub fn text(text: impl Into) -> Self { Self { @@ -102,105 +58,242 @@ impl ToolResult { } } -pub struct PiEmbedded { - runtime: Runtime, - context: Context, -} +/// The Pi Agent half of the Pi Agent System App. +/// +/// This object does not own a QuickJS runtime. It mounts pi-agent-core into +/// the Root App's existing PocketJS Guest, so the Agent Loop, context, tools +/// and Root View have one App lifecycle. Slow model and tool work happens on +/// worker threads; `tick` only delivers completed events into the Guest. +pub struct GuestAgent; -impl PiEmbedded { - pub fn new( +impl GuestAgent { + pub fn mount_source( + guest: &Guest, config_json: &str, backend: Arc, tools: Arc, - on_delta: Arc, + agent_source: &str, ) -> Result { let config_json = config_with_tools(config_json, tools.definitions())?; - let runtime = Runtime::new().map_err(|error| error.to_string())?; - runtime.set_max_stack_size(QUICKJS_STACK_LIMIT); - let context = Context::full(&runtime).map_err(|error| error.to_string())?; - - context.with(|ctx| -> Result<(), String> { - let host = Object::new(ctx.clone()).map_err(|error| error.to_string())?; - host.set( - "modelComplete", - Function::new(ctx.clone(), move |request: String| -> String { - let mut emit = |delta: &str| on_delta(delta.to_owned()); + let (host_tx, host_rx) = mpsc::channel::(); + let host_rx = Arc::new(Mutex::new(host_rx)); + let next_request = Arc::new(AtomicI32::new(1)); + let (model_tx, model_rx) = mpsc::channel::<(i32, String)>(); + let model_host_tx = host_tx.clone(); + std::thread::Builder::new() + .name("pi-model".into()) + .stack_size(MODEL_WORKER_STACK_BYTES) + .spawn(move || { + while let Ok((id, request)) = model_rx.recv() { + let worker_tx = model_host_tx.clone(); + let mut emit = |event| { + let event = match event { + ModelStreamEvent::Thinking(delta) => serde_json::json!({ + "type":"model_progress", + "id":id, + "thinkingDelta":delta, + "textDelta":"", + }), + ModelStreamEvent::Text(delta) => serde_json::json!({ + "type":"model_progress", + "id":id, + "thinkingDelta":"", + "textDelta":delta, + }), + }; + let _ = worker_tx.send(event); + }; match backend.complete(&request, &mut emit) { - Ok(response) => response, + Ok(result) => { + let _ = worker_tx.send(serde_json::json!({ + "type":"model_done", + "id":id, + "result":result, + })); + } Err(error) => { - let text = format!("model backend failed: {error}"); - emit(&text); - serde_json::json!({"text":text}).to_string() + let _ = worker_tx.send(serde_json::json!({ + "type":"model_error", + "id":id, + "error":format!("model backend failed: {error}"), + })); } } - }) - .map_err(|error| error.to_string())?, - ) + } + }) + .map_err(|error| format!("spawn model worker: {error}"))?; + + guest + .mount("host", { + let tools = tools.clone(); + move |ctx, host| { + host.set( + "startModel", + Function::new(ctx.clone(), { + let model_tx = model_tx.clone(); + let host_tx = host_tx.clone(); + let next_request = next_request.clone(); + move |request: String| -> i32 { + let id = next_request.fetch_add(1, Ordering::Relaxed); + if model_tx.send((id, request)).is_err() { + let _ = host_tx.send(serde_json::json!({ + "type":"model_error", + "id":id, + "error":"model worker stopped", + })); + } + id + } + })?, + )?; + host.set( + "startTool", + Function::new(ctx.clone(), { + let tools = tools.clone(); + let host_tx = host_tx.clone(); + let next_request = next_request.clone(); + move |call_id: String, name: String, args: String| -> i32 { + let id = next_request.fetch_add(1, Ordering::Relaxed); + let tools = tools.clone(); + let host_tx = host_tx.clone(); + let worker_tx = host_tx.clone(); + let spawn = std::thread::Builder::new() + .name(format!("pi-tool-{id}")) + .stack_size(TOOL_WORKER_STACK_BYTES) + .spawn(move || { + let result = tools.execute(&call_id, &name, &args); + let _ = worker_tx.send(serde_json::json!({ + "type":"tool_done", + "id":id, + "result":result.to_json(), + })); + }); + if let Err(error) = spawn { + let result = ToolResult { + text: format!("spawn tool worker: {error}"), + is_error: true, + ..ToolResult::default() + }; + let _ = host_tx.send(serde_json::json!({ + "type":"tool_done", + "id":id, + "result":result.to_json(), + })); + } + id + } + })?, + )?; + host.set( + "poll", + Function::new(ctx.clone(), { + let host_rx = host_rx.clone(); + move || -> String { + let batch = host_rx + .lock() + .map(|receiver| coalesce_host_events(receiver.try_iter())) + .unwrap_or_default(); + serde_json::to_string(&batch).unwrap_or_else(|_| "[]".to_owned()) + } + })?, + )?; + Ok(()) + } + }) .map_err(|error| error.to_string())?; - host.set( - "tool", - Function::new( - ctx.clone(), - move |call_id: String, name: String, args: String| -> String { - tools.execute(&call_id, &name, &args).to_json() - }, - ) - .map_err(|error| error.to_string())?, - ) + + guest + .eval("pi-agent-prelude", PRELUDE) + .map_err(|error| error.to_string())?; + guest + .eval("pi-agent-core", agent_source) .map_err(|error| error.to_string())?; - ctx.globals() - .set("host", host) - .map_err(|error| error.to_string())?; - - ctx.eval::<(), _>(PRELUDE.as_bytes()) - .catch(&ctx) - .map_err(|error| format!("embedded prelude: {error}"))?; - ctx.eval::<(), _>(AGENT_BUNDLE.as_bytes()) - .catch(&ctx) - .map_err(|error| format!("embedded agent: {error}"))?; - let agent: Object = ctx - .globals() - .get("PocketPiEmbedded") - .map_err(|error| format!("PocketPiEmbedded missing: {error}"))?; - let boot: Function = agent.get("boot").map_err(|error| error.to_string())?; - boot.call::<_, ()>((config_json,)) - .catch(&ctx) - .map_err(|error| format!("embedded boot: {error}")) - })?; - - let runtime = Self { runtime, context }; - runtime.pump()?; - Ok(runtime) + call_agent::<_, ()>(guest, "boot", (config_json,))?; + guest.drain_jobs(); + + Ok(Self) } - pub fn prompt(&self, text: &str) -> Result<(), String> { - self.context.with(|ctx| { - let agent: Object = ctx - .globals() - .get("PocketPiEmbedded") - .map_err(|error| error.to_string())?; - let prompt: Function = agent.get("prompt").map_err(|error| error.to_string())?; - prompt - .call::<_, ()>((text.to_owned(),)) - .catch(&ctx) - .map_err(|error| format!("embedded prompt: {error}")) - }) + pub fn prompt(&self, guest: &Guest, text: &str) -> Result<(), String> { + call_agent(guest, "prompt", (text.to_owned(),)) } - pub fn pump(&self) -> Result { - while self.runtime.is_job_pending() { - self.runtime - .execute_pending_job() - .map_err(|error| error.to_string())?; + pub fn tick(&self, guest: &Guest) -> Result, String> { + call_agent::<_, ()>(guest, "tick", ())?; + guest.drain_jobs(); + let raw: String = call_agent(guest, "drain", ())?; + let payload: serde_json::Value = serde_json::from_str(&raw) + .map_err(|error| format!("parse Pi Agent events: {error}"))?; + let mut events = Vec::new(); + for event in payload["events"].as_array().into_iter().flatten() { + match event["type"].as_str() { + Some("agent_ready") => events.push(AgentEvent::Ready), + Some("message_update") if event["kind"] == "text_delta" => { + if let Some(delta) = event["delta"].as_str().filter(|delta| !delta.is_empty()) { + events.push(AgentEvent::ResponseText(delta.to_owned())); + } + } + Some("agent_end") => events.push(AgentEvent::Done), + Some("agent_error") => events.push(AgentEvent::Failed( + event["message"] + .as_str() + .unwrap_or("Pi Agent failed") + .to_owned(), + )), + _ => {} + } } - self.context - .with(|ctx| { - let agent: Object = ctx.globals().get("PocketPiEmbedded")?; - let drain: Function = agent.get("drain")?; - drain.call::<_, String>(()) - }) - .map_err(|error| error.to_string()) + Ok(events) + } +} + +fn coalesce_host_events( + source: impl IntoIterator, +) -> Vec { + let mut batch = Vec::::new(); + let mut progress = std::collections::BTreeMap::::new(); + for event in source { + if event["type"] != "model_progress" { + batch.push(event); + continue; + } + let id = event["id"].as_i64().unwrap_or_default(); + let entry = progress.entry(id).or_insert_with(|| { + batch.push(serde_json::Value::Null); + (batch.len() - 1, String::new(), String::new()) + }); + entry + .1 + .push_str(event["thinkingDelta"].as_str().unwrap_or("")); + entry.2.push_str(event["textDelta"].as_str().unwrap_or("")); + } + for (id, (index, thinking, text)) in progress { + batch[index] = serde_json::json!({ + "type":"model_progress", + "id":id, + "thinkingDelta":thinking, + "textDelta":text, + }); } + batch +} + +fn call_agent(guest: &Guest, name: &str, args: A) -> Result +where + A: for<'js> pocket_mod::qjs::function::IntoArgs<'js>, + R: for<'js> pocket_mod::qjs::FromJs<'js>, +{ + guest.with(|ctx| { + let agent: Object = ctx + .globals() + .get("PocketPiEmbedded") + .map_err(|error| format!("PocketPiEmbedded missing: {error}"))?; + let function: Function = agent.get(name).map_err(|error| error.to_string())?; + function + .call::<_, R>(args) + .catch(&ctx) + .map_err(|error| format!("PocketPiEmbedded.{name}: {error}")) + }) } fn config_with_tools( @@ -229,17 +322,30 @@ fn config_with_tools( #[cfg(test)] mod tests { use super::*; + use std::sync::atomic::AtomicUsize; + use std::time::Duration; - struct Backend; + const TEST_AGENT_BUNDLE: &str = include_str!("../js/pi-agent.bundle.js"); - impl ModelBackend for Backend { + struct BurstBackend; + + impl ModelBackend for BurstBackend { fn complete( &self, _request_json: &str, - on_delta: &mut dyn FnMut(&str), + on_event: &mut dyn FnMut(ModelStreamEvent), ) -> Result { - on_delta("embedded-ok"); - Ok(r#"{"text":"embedded-ok"}"#.into()) + for _ in 0..100 { + on_event(ModelStreamEvent::Text("x".into())); + } + Ok(serde_json::json!({ + "thinking":"", + "text":"x".repeat(100), + "toolCalls":[], + "usage":{}, + "stopReason":"stop" + }) + .to_string()) } } @@ -272,16 +378,180 @@ mod tests { } #[test] - fn boots_and_runs_a_real_pi_agent_turn() { - let runtime = PiEmbedded::new( + fn model_progress_is_coalesced_before_reaching_the_embedded_ui() { + let guest = Guest::new().unwrap(); + let agent = GuestAgent::mount_source( + &guest, r#"{"model":"offline"}"#, - Arc::new(Backend), + Arc::new(BurstBackend), Arc::new(Tools), - Arc::new(|_| {}), + TEST_AGENT_BUNDLE, ) .unwrap(); - runtime.prompt("hello").unwrap(); - let events = runtime.pump().unwrap(); - assert!(events.contains("message_end")); + agent.prompt(&guest, "burst").unwrap(); + + let mut deltas = Vec::new(); + let mut done = false; + for _ in 0..200 { + let events = agent.tick(&guest).unwrap(); + for event in events { + match event { + AgentEvent::ResponseText(text) => deltas.push(text), + AgentEvent::Done => { + assert_eq!(deltas, ["x".repeat(100)]); + done = true; + } + _ => {} + } + } + if done { + break; + } + std::thread::sleep(Duration::from_millis(2)); + } + assert!(done); + assert_eq!(deltas, ["x".repeat(100)]); + } + + struct ThinkingToolBackend { + calls: AtomicUsize, + requests: Arc>>, + threads: Arc>>, + } + + impl ModelBackend for ThinkingToolBackend { + fn complete( + &self, + request_json: &str, + on_event: &mut dyn FnMut(ModelStreamEvent), + ) -> Result { + self.threads + .lock() + .unwrap() + .push(std::thread::current().id()); + self.requests + .lock() + .unwrap() + .push(serde_json::from_str(request_json).unwrap()); + if self.calls.fetch_add(1, Ordering::SeqCst) == 0 { + on_event(ModelStreamEvent::Thinking("use both tools".into())); + return Ok(serde_json::json!({ + "thinking":"use both tools", + "thinkingSignature":"reasoning_content", + "text":"", + "toolCalls":[ + {"id":"call_first","name":"first","arguments":{"n":1}}, + {"id":"call_second","name":"second","arguments":{"n":2}} + ], + "usage":{"reasoning":3}, + "stopReason":"toolUse" + }) + .to_string()); + } + on_event(ModelStreamEvent::Thinking("both finished".into())); + on_event(ModelStreamEvent::Text("complete".into())); + Ok(serde_json::json!({ + "thinking":"both finished", + "thinkingSignature":"reasoning_content", + "text":"complete", + "toolCalls":[], + "usage":{"reasoning":2}, + "stopReason":"stop" + }) + .to_string()) + } + } + + struct OrderedTools(Arc>>); + + impl ToolHost for OrderedTools { + fn definitions(&self) -> Vec { + ["first", "second"] + .into_iter() + .map(|name| { + serde_json::json!({ + "name":name, + "description":name, + "parameters":{"type":"object","properties":{"n":{"type":"number"}}} + }) + }) + .collect() + } + + fn execute(&self, _call_id: &str, name: &str, _args_json: &str) -> ToolResult { + self.0.lock().unwrap().push(name.into()); + ToolResult::text(format!("{name}-done")) + } + } + + #[test] + fn one_model_worker_preserves_thinking_and_sequential_tools() { + let requests = Arc::new(Mutex::new(Vec::new())); + let threads = Arc::new(Mutex::new(Vec::new())); + let executed = Arc::new(Mutex::new(Vec::new())); + let guest = Guest::new().unwrap(); + let agent = GuestAgent::mount_source( + &guest, + r#"{"provider":"deepseek","model":"deepseek-v4-pro","thinkingLevel":"high"}"#, + Arc::new(ThinkingToolBackend { + calls: AtomicUsize::new(0), + requests: requests.clone(), + threads: threads.clone(), + }), + Arc::new(OrderedTools(executed.clone())), + TEST_AGENT_BUNDLE, + ) + .unwrap(); + agent.prompt(&guest, "run both").unwrap(); + + let mut text = String::new(); + let mut done = false; + for _ in 0..500 { + for event in agent.tick(&guest).unwrap() { + match event { + AgentEvent::ResponseText(delta) => text.push_str(&delta), + AgentEvent::Done => done = true, + AgentEvent::Failed(error) => panic!("agent failed: {error}"), + AgentEvent::Ready => {} + } + } + if done { + break; + } + std::thread::sleep(Duration::from_millis(2)); + } + + assert!(done); + assert_eq!(text, "complete"); + assert_eq!(*executed.lock().unwrap(), ["first", "second"]); + let threads = threads.lock().unwrap(); + assert_eq!(threads.len(), 2); + assert_eq!(threads[0], threads[1]); + let requests = requests.lock().unwrap(); + assert_eq!(requests.len(), 2); + assert_eq!(requests[0]["options"]["reasoning"], "high"); + let messages = requests[1]["context"]["messages"].as_array().unwrap(); + let assistant = messages + .iter() + .find(|message| message["role"] == "assistant") + .unwrap(); + assert_eq!(assistant["content"][0]["type"], "thinking"); + assert_eq!(assistant["content"][0]["thinking"], "use both tools"); + assert_eq!( + assistant["content"] + .as_array() + .unwrap() + .iter() + .filter(|block| block["type"] == "toolCall") + .count(), + 2 + ); + assert_eq!( + messages + .iter() + .filter(|message| message["role"] == "toolResult") + .count(), + 2 + ); } } diff --git a/crates/pocket-pi-protocols/src/anthropic_messages.rs b/crates/pocket-pi-protocols/src/anthropic_messages.rs index d973152..65b4ca0 100644 --- a/crates/pocket-pi-protocols/src/anthropic_messages.rs +++ b/crates/pocket-pi-protocols/src/anthropic_messages.rs @@ -1,9 +1,12 @@ +use alloc::collections::BTreeMap; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; use serde_json::{json, Map, Value}; +use crate::model::ModelStreamEvent; + pub fn build_request(request_json: &str) -> Result { let request: Value = serde_json::from_str(request_json) .map_err(|error| format!("parse Pi model request: {error}"))?; @@ -73,17 +76,22 @@ pub fn build_request(request_json: &str) -> Result { .map_err(|error| format!("serialize Anthropic request: {error}")) } +#[derive(Default)] +struct PendingToolCall { + id: String, + name: String, + arguments: String, +} + #[derive(Default)] pub struct Stream { text: String, - tool_id: String, - tool_name: String, - tool_arguments: String, + tool_calls: BTreeMap, stop_reason: Option, } impl Stream { - pub fn push(&mut self, data_json: &str) -> Result, String> { + pub fn push(&mut self, data_json: &str) -> Result, String> { let event: Value = serde_json::from_str(data_json) .map_err(|error| format!("parse Anthropic stream event: {error}"))?; match event.get("type").and_then(Value::as_str) { @@ -97,14 +105,16 @@ impl Stream { .and_then(Value::as_object) .ok_or_else(|| "Anthropic content block is missing".to_string())?; if block.get("type").and_then(Value::as_str) == Some("tool_use") { - self.tool_id = block.get("id").and_then(Value::as_str).unwrap_or("").into(); - self.tool_name = block + let index = event.get("index").and_then(Value::as_u64).unwrap_or(0); + let call = self.tool_calls.entry(index).or_default(); + call.id = block.get("id").and_then(Value::as_str).unwrap_or("").into(); + call.name = block .get("name") .and_then(Value::as_str) .unwrap_or("") .into(); } - Ok(None) + Ok(Vec::new()) } Some("content_block_delta") => { let delta = event @@ -115,15 +125,24 @@ impl Stream { Some("text_delta") => { let text = delta.get("text").and_then(Value::as_str).unwrap_or(""); self.text.push_str(text); - Ok((!text.is_empty()).then(|| text.to_string())) + Ok(if text.is_empty() { + Vec::new() + } else { + alloc::vec![ModelStreamEvent::Text(text.to_string())] + }) } Some("input_json_delta") => { if let Some(json) = delta.get("partial_json").and_then(Value::as_str) { - self.tool_arguments.push_str(json); + let index = event.get("index").and_then(Value::as_u64).unwrap_or(0); + self.tool_calls + .entry(index) + .or_default() + .arguments + .push_str(json); } - Ok(None) + Ok(Vec::new()) } - _ => Ok(None), + _ => Ok(Vec::new()), } } Some("message_delta") => { @@ -131,41 +150,51 @@ impl Stream { .pointer("/delta/stop_reason") .and_then(Value::as_str) .map(ToString::to_string); - Ok(None) + Ok(Vec::new()) } - _ => Ok(None), + _ => Ok(Vec::new()), } } pub fn finish(self) -> Result { - if !self.tool_name.is_empty() { - if self.tool_id.is_empty() { - return Err("Anthropic tool use is missing id".into()); + let stop_reason = match self.stop_reason.as_deref() { + Some("end_turn" | "stop_sequence") => "stop", + Some("tool_use") => "toolUse", + Some("max_tokens") if self.tool_calls.is_empty() => "length", + Some("max_tokens") => return Err("Anthropic truncated streamed tool calls".into()), + Some(reason) => return Err(format!("unsupported Anthropic stop reason: {reason}")), + None => return Err("Anthropic stream ended without stop_reason".into()), + }; + let mut tool_calls = Vec::new(); + for (_, call) in self.tool_calls { + if call.id.is_empty() || call.name.is_empty() { + return Err("Anthropic tool use is missing id or name".into()); } - let arguments: Value = serde_json::from_str(if self.tool_arguments.is_empty() { + let arguments: Value = serde_json::from_str(if call.arguments.is_empty() { "{}" } else { - &self.tool_arguments + &call.arguments }) .map_err(|error| format!("parse Anthropic tool input: {error}"))?; if !arguments.is_object() { return Err("Anthropic tool input must be an object".into()); } - return serde_json::to_string(&json!({ - "toolCall":{"id":self.tool_id,"name":self.tool_name,"arguments":arguments} - })) - .map_err(|error| format!("serialize Pi tool call: {error}")); + tool_calls.push(json!({"id":call.id,"name":call.name,"arguments":arguments})); } - if self.text.is_empty() { + if self.text.is_empty() && tool_calls.is_empty() { return Err("Anthropic stream contained no model decision".into()); } - let stop_reason = if self.stop_reason.as_deref() == Some("max_tokens") { - "length" - } else { - "stop" - }; - serde_json::to_string(&json!({"text":self.text,"stopReason":stop_reason})) - .map_err(|error| format!("serialize Pi text result: {error}")) + serde_json::to_string(&json!({ + "thinking":"", + "text":self.text, + "toolCalls":tool_calls, + "usage":{ + "input":0,"output":0,"cacheRead":0,"cacheWrite":0,"totalTokens":0, + "cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0} + }, + "stopReason":stop_reason + })) + .map_err(|error| format!("serialize Anthropic model result: {error}")) } } @@ -234,6 +263,7 @@ fn content_text(content: Option<&Value>) -> String { #[cfg(test)] mod tests { use super::*; + use alloc::vec; #[test] fn streams_text() { @@ -242,9 +272,29 @@ mod tests { stream .push(r#"{"type":"content_block_delta","delta":{"type":"text_delta","text":"hi"}}"#) .unwrap(), - Some("hi".into()) + vec![ModelStreamEvent::Text("hi".into())] ); + stream + .push(r#"{"type":"message_delta","delta":{"stop_reason":"end_turn"}}"#) + .unwrap(); let result: Value = serde_json::from_str(&stream.finish().unwrap()).unwrap(); assert_eq!(result["text"], "hi"); } + + #[test] + fn streams_multiple_tool_calls() { + let mut stream = Stream::default(); + for event in [ + r#"{"type":"content_block_start","index":0,"content_block":{"type":"tool_use","id":"one","name":"first"}}"#, + r#"{"type":"content_block_delta","index":0,"delta":{"type":"input_json_delta","partial_json":"{}"}}"#, + r#"{"type":"content_block_start","index":1,"content_block":{"type":"tool_use","id":"two","name":"second"}}"#, + r#"{"type":"content_block_delta","index":1,"delta":{"type":"input_json_delta","partial_json":"{\"n\":2}"}}"#, + r#"{"type":"message_delta","delta":{"stop_reason":"tool_use"}}"#, + ] { + stream.push(event).unwrap(); + } + let result: Value = serde_json::from_str(&stream.finish().unwrap()).unwrap(); + assert_eq!(result["toolCalls"].as_array().unwrap().len(), 2); + assert_eq!(result["stopReason"], "toolUse"); + } } diff --git a/crates/pocket-pi-protocols/src/codex_decision.rs b/crates/pocket-pi-protocols/src/codex_decision.rs index 9f49f13..7e09885 100644 --- a/crates/pocket-pi-protocols/src/codex_decision.rs +++ b/crates/pocket-pi-protocols/src/codex_decision.rs @@ -4,8 +4,8 @@ use alloc::vec::Vec; use serde_json::{json, Value}; -/// Build the same single-decision prompt used by the board's Codex bridge. -/// Codex chooses one ESP tool call or a final text response; it never executes +/// Build the same decision prompt used by the board's Codex bridge. +/// Codex chooses ESP tool calls or a final text response; it never executes /// host tools itself. pub fn build_prompt(request_json: &str) -> Result<(String, Vec), String> { let request: Value = serde_json::from_str(request_json) @@ -40,10 +40,10 @@ pub fn build_prompt(request_json: &str) -> Result<(String, Vec), String> .unwrap_or(""); let prompt = format!( "You are the model decision backend for a Pi Agent running on an ESP32-P4.\n\n\ - Do not call host tools or inspect Mac files. The JSON tools below run only on the ESP32 after you request one.\n\n\ + Do not call host tools or inspect Mac files. The JSON tools below run only on the ESP32 after you request them.\n\n\ Return exactly one compact JSON object and no Markdown. Choose either \ - {{\"toolCall\":{{\"name\":\"registered.name\",\"arguments\":{{...}}}}}} to take one action, \ - or {{\"text\":\"final response\"}} when the turn is complete. After a tool result you may request the next tool. \ + {{\"toolCalls\":[{{\"name\":\"registered.name\",\"arguments\":{{...}}}}]}} to take actions, \ + or {{\"text\":\"final response\"}} when the turn is complete. \ Never claim an action succeeded until its tool result appears in the conversation.\n\n\ System instruction: {system}\n\n\ Registered ESP32 tools: {}\n\n\ @@ -66,27 +66,58 @@ pub fn parse_response(raw: &str, tools: &[String], call_id: &str) -> Result "openai", Self::OpenRouter => "openrouter", Self::Anthropic => "anthropic", + Self::DeepSeek => "deepseek", } } pub fn default_model(self) -> Option<&'static str> { match self { Self::OpenAi => Some("gpt-5-mini"), + Self::DeepSeek => Some("deepseek-v4-flash"), Self::OpenRouter | Self::Anthropic => None, } } @@ -66,6 +72,24 @@ impl Default for ModelBackendSettings { pub struct ModelSettings { pub backend: ModelBackendSettings, pub model: Option, + pub thinking_level: ThinkingLevel, +} + +#[derive(Clone, Copy, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +#[serde(rename_all = "lowercase")] +pub enum ThinkingLevel { + #[default] + High, + Xhigh, +} + +impl ThinkingLevel { + pub fn id(self) -> &'static str { + match self { + Self::High => "high", + Self::Xhigh => "xhigh", + } + } } impl ModelSettings { @@ -93,10 +117,19 @@ mod tests { use super::*; #[test] - fn defaults_to_uart_codex_without_a_secret() { + fn resolves_backend_defaults_without_secrets() { let settings = ModelSettings::default(); assert_eq!(settings.resolved_model().unwrap(), "codex"); let json = serde_json::to_string(&settings).unwrap(); assert!(!json.contains("key")); + let settings = ModelSettings { + backend: ModelBackendSettings::Wireless { + provider: WirelessProvider::DeepSeek, + }, + model: None, + thinking_level: ThinkingLevel::Xhigh, + }; + assert_eq!(settings.resolved_model().unwrap(), "deepseek-v4-flash"); + assert_eq!(settings.thinking_level.id(), "xhigh"); } } diff --git a/crates/pocket-pi-protocols/src/openai_chat.rs b/crates/pocket-pi-protocols/src/openai_chat.rs index 2da47c9..decd911 100644 --- a/crates/pocket-pi-protocols/src/openai_chat.rs +++ b/crates/pocket-pi-protocols/src/openai_chat.rs @@ -1,23 +1,25 @@ +use alloc::collections::BTreeMap; use alloc::format; use alloc::string::{String, ToString}; use alloc::vec::Vec; use serde_json::{json, Map, Value}; +use crate::model::ModelStreamEvent; + +const DEEPSEEK_TOOL_SEPARATOR: &str = "_dot_"; + #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Dialect { OpenAi, OpenRouter, + DeepSeek, } -/// Convert the provider-neutral request emitted by embedded Pi into OpenAI's -/// streaming Chat Completions wire format. pub fn build_request(request_json: &str) -> Result { build_request_for(request_json, Dialect::OpenAi) } -/// OpenAI and OpenRouter share this wire format, but use different token field -/// names. Provider selection remains outside the Pi runtime. pub fn build_request_for(request_json: &str, dialect: Dialect) -> Result { let request: Value = serde_json::from_str(request_json) .map_err(|error| format!("parse Pi model request: {error}"))?; @@ -45,92 +47,185 @@ pub fn build_request_for(request_json: &str, dialect: Dialect) -> Result, String>>()?; + let max_tokens = request + .pointer("/options/maxTokens") + .and_then(Value::as_u64) + .or_else(|| request.pointer("/model/maxTokens").and_then(Value::as_u64)) + .unwrap_or(1024) + .clamp(1, 384_000); let mut body = Map::new(); body.insert("model".into(), Value::String(model.into())); body.insert("messages".into(), Value::Array(messages)); body.insert( - match dialect { - Dialect::OpenAi => "max_completion_tokens", - Dialect::OpenRouter => "max_tokens", + if dialect == Dialect::OpenAi { + "max_completion_tokens" + } else { + "max_tokens" } .into(), - Value::from( - request - .pointer("/model/maxTokens") - .and_then(Value::as_u64) - .unwrap_or(1024) - .clamp(1, 16_384), - ), + Value::from(max_tokens), ); body.insert("stream".into(), Value::Bool(true)); - body.insert("parallel_tool_calls".into(), Value::Bool(false)); + + if dialect == Dialect::DeepSeek { + let reasoning = request + .pointer("/options/reasoning") + .and_then(Value::as_str) + .unwrap_or("high"); + body.insert("thinking".into(), json!({"type":"enabled"})); + body.insert( + "reasoning_effort".into(), + Value::String(if matches!(reasoning, "xhigh" | "max") { + "max".into() + } else { + "high".into() + }), + ); + body.insert("stream_options".into(), json!({"include_usage":true})); + } else { + body.insert("parallel_tool_calls".into(), Value::Bool(false)); + } + if !tools.is_empty() { body.insert("tools".into(), Value::Array(tools)); - body.insert("tool_choice".into(), Value::String("auto".into())); + if dialect != Dialect::DeepSeek { + body.insert("tool_choice".into(), Value::String("auto".into())); + } } + serde_json::to_string(&Value::Object(body)) - .map_err(|error| format!("serialize OpenAI request: {error}")) + .map_err(|error| format!("serialize chat completions request: {error}")) +} + +#[derive(Default)] +struct PendingToolCall { + id: String, + name: String, + arguments: String, } #[derive(Default)] +struct Usage { + input: u64, + output: u64, + cache_read: u64, + reasoning: u64, + total: u64, +} + pub struct Stream { + dialect: Dialect, + thinking: String, text: String, - tool_id: String, - tool_name: String, - tool_arguments: String, + tool_calls: BTreeMap, + usage: Usage, stop_reason: Option, } +impl Default for Stream { + fn default() -> Self { + Self::new(Dialect::OpenAi) + } +} + impl Stream { - pub fn push(&mut self, data_json: &str) -> Result, String> { + pub fn new(dialect: Dialect) -> Self { + Self { + dialect, + thinking: String::new(), + text: String::new(), + tool_calls: BTreeMap::new(), + usage: Usage::default(), + stop_reason: None, + } + } + + pub fn push(&mut self, data_json: &str) -> Result, String> { let event: Value = serde_json::from_str(data_json) - .map_err(|error| format!("parse OpenAI stream event: {error}"))?; + .map_err(|error| format!("parse chat completions stream event: {error}"))?; if let Some(error) = event.get("error") { - return Err(format!("OpenAI stream error: {error}")); + return Err(format!("chat completions stream error: {error}")); + } + if let Some(usage) = event.get("usage").and_then(Value::as_object) { + self.usage.input = usage + .get("prompt_tokens") + .and_then(Value::as_u64) + .unwrap_or(0); + self.usage.output = usage + .get("completion_tokens") + .and_then(Value::as_u64) + .unwrap_or(0); + self.usage.total = usage + .get("total_tokens") + .and_then(Value::as_u64) + .unwrap_or(self.usage.input.saturating_add(self.usage.output)); + self.usage.cache_read = usage + .get("prompt_tokens_details") + .and_then(|details| details.get("cached_tokens")) + .and_then(Value::as_u64) + .or_else(|| usage.get("prompt_cache_hit_tokens").and_then(Value::as_u64)) + .unwrap_or(0); + self.usage.reasoning = usage + .get("completion_tokens_details") + .and_then(|details| details.get("reasoning_tokens")) + .and_then(Value::as_u64) + .unwrap_or(0); } + let Some(choice) = event .get("choices") .and_then(Value::as_array) .and_then(|choices| choices.first()) else { - return Ok(None); + return Ok(Vec::new()); }; if let Some(reason) = choice.get("finish_reason").and_then(Value::as_str) { self.stop_reason = Some(reason.into()); } let Some(delta) = choice.get("delta").and_then(Value::as_object) else { - return Ok(None); + return Ok(Vec::new()); }; + + let mut events = Vec::new(); + if let Some(thinking) = delta.get("reasoning_content").and_then(Value::as_str) { + if !thinking.is_empty() { + self.thinking.push_str(thinking); + events.push(ModelStreamEvent::Thinking(thinking.into())); + } + } if let Some(content) = delta.get("content").and_then(Value::as_str) { - self.text.push_str(content); - return Ok(Some(content.into())); + if !content.is_empty() { + self.text.push_str(content); + events.push(ModelStreamEvent::Text(content.into())); + } } for call in delta .get("tool_calls") @@ -138,57 +233,92 @@ impl Stream { .into_iter() .flatten() { - if call.get("index").and_then(Value::as_u64).unwrap_or(0) != 0 { - return Err("provider streamed multiple tool calls".into()); - } + let index = call.get("index").and_then(Value::as_u64).unwrap_or(0); + let pending = self.tool_calls.entry(index).or_default(); if let Some(id) = call.get("id").and_then(Value::as_str) { - self.tool_id.push_str(id); + pending.id.push_str(id); } if let Some(function) = call.get("function").and_then(Value::as_object) { if let Some(name) = function.get("name").and_then(Value::as_str) { - self.tool_name.push_str(name); + pending.name.push_str(name); } if let Some(arguments) = function.get("arguments").and_then(Value::as_str) { - self.tool_arguments.push_str(arguments); + pending.arguments.push_str(arguments); } } } - Ok(None) + Ok(events) } pub fn finish(self) -> Result { - if !self.tool_name.is_empty() { - if self.tool_id.is_empty() { - return Err("streamed tool call is missing id".into()); + let stop_reason = match self.stop_reason.as_deref() { + Some("stop") => "stop", + Some("tool_calls") => "toolUse", + Some("length") if self.tool_calls.is_empty() => "length", + Some("length") => return Err("provider truncated streamed tool calls".into()), + Some("content_filter") => { + return Err("provider content filter stopped the response".into()) + } + Some("insufficient_system_resource") => { + return Err("provider had insufficient system resources".into()) + } + Some(other) => return Err(format!("unsupported provider finish reason: {other}")), + None => return Err("provider stream ended without finish_reason".into()), + }; + + let mut tool_calls = Vec::new(); + for (_, call) in self.tool_calls { + if call.id.is_empty() || call.name.is_empty() { + return Err("streamed tool call is missing id or name".into()); } - let arguments: Value = serde_json::from_str(if self.tool_arguments.is_empty() { + let arguments: Value = serde_json::from_str(if call.arguments.is_empty() { "{}" } else { - &self.tool_arguments + &call.arguments }) .map_err(|error| format!("parse streamed tool arguments: {error}"))?; if !arguments.is_object() { return Err("streamed tool arguments must be a JSON object".into()); } - return serde_json::to_string(&json!({ - "toolCall":{"id":self.tool_id,"name":self.tool_name,"arguments":arguments} - })) - .map_err(|error| format!("serialize Pi tool call: {error}")); + tool_calls.push(json!({ + "id":call.id, + "name":decode_tool_name(&call.name, self.dialect), + "arguments":arguments + })); } - if self.text.is_empty() { - return Err("provider stream contained neither text nor a tool call".into()); + if self.text.is_empty() && tool_calls.is_empty() { + return Err("provider stream contained no model decision".into()); } - let stop_reason = if self.stop_reason.as_deref() == Some("length") { - "length" - } else { - "stop" - }; - serde_json::to_string(&json!({"text":self.text,"stopReason":stop_reason})) - .map_err(|error| format!("serialize Pi text result: {error}")) + + let mut result = Map::new(); + result.insert("thinking".into(), Value::String(self.thinking.clone())); + if !self.thinking.is_empty() { + result.insert( + "thinkingSignature".into(), + Value::String("reasoning_content".into()), + ); + } + result.insert("text".into(), Value::String(self.text)); + result.insert("toolCalls".into(), Value::Array(tool_calls)); + result.insert( + "usage".into(), + json!({ + "input":self.usage.input, + "output":self.usage.output, + "cacheRead":self.usage.cache_read, + "cacheWrite":0, + "reasoning":self.usage.reasoning, + "totalTokens":self.usage.total, + "cost":{"input":0,"output":0,"cacheRead":0,"cacheWrite":0,"total":0} + }), + ); + result.insert("stopReason".into(), Value::String(stop_reason.into())); + serde_json::to_string(&Value::Object(result)) + .map_err(|error| format!("serialize model result: {error}")) } } -fn convert_message(message: &Value) -> Result, String> { +fn convert_message(message: &Value, dialect: Dialect) -> Result, String> { let Some(role) = message.get("role").and_then(Value::as_str) else { return Ok(None); }; @@ -198,7 +328,6 @@ fn convert_message(message: &Value) -> Result, String> { "content":content_text(message.get("content")) }))), "assistant" => { - let text = content_text(message.get("content")); let mut tool_calls = Vec::new(); for block in message .get("content") @@ -217,6 +346,7 @@ fn convert_message(message: &Value) -> Result, String> { .get("name") .and_then(Value::as_str) .ok_or_else(|| "Pi assistant toolCall is missing name".to_string())?; + let provider_name = encode_tool_name(name, dialect)?; let arguments = serde_json::to_string( block.get("arguments").unwrap_or(&Value::Object(Map::new())), ) @@ -224,19 +354,21 @@ fn convert_message(message: &Value) -> Result, String> { tool_calls.push(json!({ "id":id, "type":"function", - "function":{"name":name,"arguments":arguments} + "function":{"name":provider_name,"arguments":arguments} })); } let mut converted = Map::new(); converted.insert("role".into(), Value::String("assistant".into())); converted.insert( "content".into(), - if text.is_empty() { - Value::Null - } else { - Value::String(text) - }, + Value::String(content_text(message.get("content"))), ); + if dialect == Dialect::DeepSeek { + let thinking = thinking_text(message.get("content")); + if !thinking.is_empty() { + converted.insert("reasoning_content".into(), Value::String(thinking)); + } + } if !tool_calls.is_empty() { converted.insert("tool_calls".into(), Value::Array(tool_calls)); } @@ -257,19 +389,59 @@ fn convert_message(message: &Value) -> Result, String> { } } +fn encode_tool_name(name: &str, dialect: Dialect) -> Result { + if dialect != Dialect::DeepSeek { + return Ok(name.into()); + } + if name.contains(DEEPSEEK_TOOL_SEPARATOR) { + return Err(format!( + "tool name {name:?} contains reserved DeepSeek separator {DEEPSEEK_TOOL_SEPARATOR:?}" + )); + } + let encoded = name.replace('.', DEEPSEEK_TOOL_SEPARATOR); + if encoded.len() > 64 + || !encoded + .bytes() + .all(|byte| byte.is_ascii_alphanumeric() || matches!(byte, b'_' | b'-')) + { + return Err(format!( + "tool name {name:?} cannot be represented for DeepSeek" + )); + } + Ok(encoded) +} + +fn decode_tool_name(name: &str, dialect: Dialect) -> String { + if dialect == Dialect::DeepSeek { + name.replace(DEEPSEEK_TOOL_SEPARATOR, ".") + } else { + name.into() + } +} + fn content_text(content: Option<&Value>) -> String { + block_text(content, "text", "text") +} + +fn thinking_text(content: Option<&Value>) -> String { + block_text(content, "thinking", "thinking") +} + +fn block_text(content: Option<&Value>, block_type: &str, field: &str) -> String { let Some(content) = content else { return String::new(); }; - if let Some(text) = content.as_str() { - return text.into(); + if block_type == "text" { + if let Some(text) = content.as_str() { + return text.into(); + } } content .as_array() .into_iter() .flatten() - .filter(|block| block.get("type").and_then(Value::as_str) == Some("text")) - .filter_map(|block| block.get("text").and_then(Value::as_str)) + .filter(|block| block.get("type").and_then(Value::as_str) == Some(block_type)) + .filter_map(|block| block.get(field).and_then(Value::as_str)) .collect::>() .join("") } @@ -277,42 +449,94 @@ fn content_text(content: Option<&Value>) -> String { #[cfg(test)] mod tests { use super::*; + use alloc::vec; - #[test] - fn maps_history_and_tools() { - let request = json!({ - "model":{"id":"gpt-5.6","maxTokens":768}, + fn deepseek_request(reasoning: &str) -> Value { + json!({ + "model":{"id":"deepseek-v4-pro","maxTokens":384000}, "context":{ "systemPrompt":"Use tools when needed.", "messages":[{"role":"user","content":"list files"}], "tools":[{"name":"ls","description":"List files","parameters":{"type":"object"}}] - } - }); - let body: Value = - serde_json::from_str(&build_request(&request.to_string()).unwrap()).unwrap(); - assert_eq!(body["model"], "gpt-5.6"); - assert_eq!(body["parallel_tool_calls"], false); - assert_eq!(body["tools"][0]["function"]["name"], "ls"); + }, + "options":{"reasoning":reasoning} + }) + } + + #[test] + fn deepseek_request_encodes_thinking_tools_and_replay() { + let mut request = deepseek_request("xhigh"); + request["context"]["tools"][0]["name"] = json!("time.now"); + request["context"]["messages"] = json!([{ + "role":"assistant", + "content":[ + {"type":"thinking","thinking":"need both tools","thinkingSignature":"reasoning_content"}, + {"type":"toolCall","id":"call_1","name":"time.now","arguments":{}}, + {"type":"toolCall","id":"call_2","name":"device.status","arguments":{"full":true}} + ] + }]); + let body: Value = serde_json::from_str( + &build_request_for(&request.to_string(), Dialect::DeepSeek).unwrap(), + ) + .unwrap(); + assert_eq!(body["model"], "deepseek-v4-pro"); + assert_eq!(body["max_tokens"], 384000); + assert_eq!(body["thinking"]["type"], "enabled"); + assert_eq!(body["reasoning_effort"], "max"); + assert_eq!(body["stream_options"]["include_usage"], true); + assert!(body.get("tool_choice").is_none()); + assert!(body.get("parallel_tool_calls").is_none()); + assert!(body.get("temperature").is_none()); + assert_eq!(body["tools"][0]["function"]["name"], "time_dot_now"); + assert_eq!(body["messages"][1]["content"], ""); + assert_eq!(body["messages"][1]["reasoning_content"], "need both tools"); + assert_eq!( + body["messages"][1]["tool_calls"].as_array().unwrap().len(), + 2 + ); + assert_eq!( + body["messages"][1]["tool_calls"][1]["function"]["name"], + "device_dot_status" + ); } #[test] - fn decodes_streamed_text() { - let mut stream = Stream::default(); + fn deepseek_stream_decodes_thinking_text_multiple_tools_and_usage() { + let mut stream = Stream::new(Dialect::DeepSeek); assert_eq!( stream - .push(r#"{"choices":[{"delta":{"content":"hel"}}]}"#) + .push(r#"{"choices":[{"delta":{"reasoning_content":"think "}}]}"#) .unwrap(), - Some("hel".into()) + vec![ModelStreamEvent::Thinking("think ".into())] ); - stream - .push(r#"{"choices":[{"delta":{"content":"lo"},"finish_reason":"stop"}]}"#) - .unwrap(); + assert_eq!( + stream + .push(r#"{"choices":[{"delta":{"reasoning_content":"more","content":"answer"}}]}"#) + .unwrap(), + vec![ + ModelStreamEvent::Thinking("more".into()), + ModelStreamEvent::Text("answer".into()) + ] + ); + stream.push(r#"{"choices":[{"delta":{"tool_calls":[{"index":1,"id":"call_2","function":{"name":"device_dot_status","arguments":"{\"b\":"}},{"index":0,"id":"call_1","function":{"name":"time_dot_now","arguments":"{}"}}]}}]}"#).unwrap(); + stream.push(r#"{"choices":[{"delta":{"tool_calls":[{"index":1,"function":{"arguments":"2}"}}]},"finish_reason":"tool_calls"}]}"#).unwrap(); + stream.push(r#"{"choices":[],"usage":{"prompt_tokens":10,"completion_tokens":20,"total_tokens":30,"prompt_cache_hit_tokens":4,"completion_tokens_details":{"reasoning_tokens":12}}}"#).unwrap(); let result: Value = serde_json::from_str(&stream.finish().unwrap()).unwrap(); - assert_eq!(result["text"], "hello"); + assert_eq!(result["thinking"], "think more"); + assert_eq!(result["text"], "answer"); + assert_eq!(result["toolCalls"][0]["name"], "time.now"); + assert_eq!(result["toolCalls"][1]["name"], "device.status"); + assert_eq!(result["toolCalls"][1]["arguments"]["b"], 2); + assert_eq!(result["usage"]["reasoning"], 12); + assert_eq!(result["stopReason"], "toolUse"); + + let mut truncated = Stream::new(Dialect::DeepSeek); + truncated.push(r#"{"choices":[{"delta":{"tool_calls":[{"index":0,"id":"call_1","function":{"name":"time_dot_now","arguments":"{"}}]},"finish_reason":"length"}]}"#).unwrap(); + assert!(truncated.finish().unwrap_err().contains("truncated")); } #[test] - fn openrouter_uses_its_token_field() { + fn openrouter_uses_max_tokens() { let request = json!({ "model":{"id":"vendor/model"}, "context":{"messages":[{"role":"user","content":"hi"}]} diff --git a/crates/pocket-pi-tools/src/lib.rs b/crates/pocket-pi-tools/src/lib.rs index d0b8903..8c35efb 100644 --- a/crates/pocket-pi-tools/src/lib.rs +++ b/crates/pocket-pi-tools/src/lib.rs @@ -104,13 +104,19 @@ impl ToolHost for CoreToolHost { Err(error) => return ToolResult::error(format!("invalid tool arguments: {error}")), }; match self.execute_value(call_id, name, &args) { - Ok(result) => ToolResult { - text: result.text, - details: result.details, - is_error: false, - terminate: result.terminate, - }, - Err(error) => ToolResult::error(error), + Ok(result) => { + log::info!("Native tool {name} completed"); + ToolResult { + text: result.text, + details: result.details, + is_error: false, + terminate: result.terminate, + } + } + Err(error) => { + log::warn!("Native tool {name} failed: {error}"); + ToolResult::error(error) + } } } } @@ -149,9 +155,6 @@ impl ToolResultExt for ToolResult { #[cfg(test)] mod tests { use super::*; - use std::sync::atomic::{AtomicUsize, Ordering}; - - use pocket_pi_embedded::{ModelBackend, PiEmbedded}; struct TestPlatform; @@ -235,66 +238,4 @@ mod tests { let restored = host(temp.path()).execute("call-3", "schedule.list", "{}"); assert!(restored.text.contains("market")); } - - struct ToolCallingBackend { - calls: AtomicUsize, - } - - impl ModelBackend for ToolCallingBackend { - fn complete( - &self, - request_json: &str, - on_delta: &mut dyn FnMut(&str), - ) -> Result { - let request: Value = serde_json::from_str(request_json).unwrap(); - let call = self.calls.fetch_add(1, Ordering::SeqCst); - if call == 0 { - let names = request["context"]["tools"] - .as_array() - .unwrap() - .iter() - .filter_map(|tool| tool["name"].as_str()) - .collect::>(); - assert!(names.contains(&"write")); - return Ok(json!({ - "toolCall":{ - "id":"write-1", - "name":"write", - "arguments":{"path":"agent-created.txt","content":"created through pi-agent-core"} - } - }) - .to_string()); - } - assert!(request["context"]["messages"] - .as_array() - .unwrap() - .iter() - .any(|message| message["role"] == "toolResult")); - on_delta("tool complete"); - Ok(json!({"text":"tool complete"}).to_string()) - } - } - - #[test] - fn pi_agent_executes_the_real_registered_tool() { - let temp = tempfile::tempdir().unwrap(); - let tools = Arc::new(host(temp.path())); - let runtime = PiEmbedded::new( - r#"{"provider":"test","model":"tool-test"}"#, - Arc::new(ToolCallingBackend { - calls: AtomicUsize::new(0), - }), - tools, - Arc::new(|_| {}), - ) - .unwrap(); - - runtime.prompt("create a file").unwrap(); - runtime.pump().unwrap(); - - assert_eq!( - std::fs::read_to_string(temp.path().join("agent-created.txt")).unwrap(), - "created through pi-agent-core" - ); - } } diff --git a/crates/pocket-pi-tools/src/schedule.rs b/crates/pocket-pi-tools/src/schedule.rs index 91551c6..a514778 100644 --- a/crates/pocket-pi-tools/src/schedule.rs +++ b/crates/pocket-pi-tools/src/schedule.rs @@ -43,8 +43,6 @@ impl ScheduleStore { let directory = workspace_root.join(".pi-agent"); let path = directory.join("schedule.json"); let _ = std::fs::create_dir_all(&directory); - // The previous recurring format is intentionally removed, not migrated. - let _ = std::fs::remove_file(directory.join("routines.json")); let state = match read_state(&path) { Ok(state) => state, Err(error) => { diff --git a/crates/pocket-pi/Cargo.toml b/crates/pocket-pi/Cargo.toml deleted file mode 100644 index a482cbb..0000000 --- a/crates/pocket-pi/Cargo.toml +++ /dev/null @@ -1,29 +0,0 @@ -[package] -name = "pocket-pi" -version = "0.1.0" -description = "A QuickJS runtime that runs the pi coding-agent core with no Node and no bun, on a PocketJS-style coalesced frame scheduler" -edition.workspace = true -license.workspace = true -repository.workspace = true - -[dependencies] -rquickjs = { version = "0.12", features = ["full-async"] } -serde_json = "1" -ureq = { version = "2", features = ["tls"] } -log = "0.4" - -# Runtime TypeScript → JavaScript for agent-authored plugins (Option A): the -# heavy "jiti + typescript" Node dependency, reimplemented as a native op. -oxc_allocator = "0.140" -oxc_parser = "0.140" -oxc_codegen = "0.140" -oxc_semantic = "0.140" -oxc_span = "0.140" -oxc_transformer = "0.140" -regex = "1" - -# Gzip decode for the embedded full-pi bundle (pure-Rust backend, no C). -flate2 = { version = "1", default-features = false, features = ["rust_backend"] } - -[dev-dependencies] -env_logger = "0.11" diff --git a/crates/pocket-pi/examples/chat.rs b/crates/pocket-pi/examples/chat.rs deleted file mode 100644 index 8ff7873..0000000 --- a/crates/pocket-pi/examples/chat.rs +++ /dev/null @@ -1,95 +0,0 @@ -//! A one-shot Pocket Pi chat demo. -//! -//! cargo run -p pocket-pi --example chat -- "Say hi in three words." -//! -//! With ANTHROPIC_API_KEY set it streams a real Anthropic turn; otherwise it -//! replays a scripted answer so the demo runs fully offline. Either way the -//! runtime is driven by a deliberately slow 2 Hz pump — proof that an agent -//! runtime doesn't need a hot loop; it needs a heartbeat. - -use pocket_pi::{PiRuntime, ToolResult}; -use std::io::Write; -use std::time::{Duration, Instant}; - -fn main() { - let prompt = std::env::args().skip(1).collect::>().join(" "); - let prompt = if prompt.is_empty() { - "In one short sentence, what are you?".to_string() - } else { - prompt - }; - - let mut rt = PiRuntime::new().expect("runtime"); - - // A trivial native tool, to show the agent can reach host capabilities. - rt.register_tool("current_time", |_args| { - let secs = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|d| d.as_secs()) - .unwrap_or(0); - ToolResult::text(format!("unix seconds: {secs}")) - }); - - rt.on_event(|ev| match ev.kind.as_str() { - "text" => { - if let Some(d) = ev.value.get("delta").and_then(|v| v.as_str()) { - print!("{d}"); - let _ = std::io::stdout().flush(); - } - } - "tool_start" => { - let name = ev.value.get("name").and_then(|v| v.as_str()).unwrap_or(""); - eprint!("\n[tool: {name}] "); - } - "end" => println!("\n[done]"), - "error" => eprintln!("\n[error] {}", ev.raw), - _ => {} - }); - - let tool = serde_json::json!({ - "name": "current_time", - "description": "Get the current unix time in seconds.", - "parameters": {"type": "object", "properties": {}} - }); - let cfg = if let Ok(key) = std::env::var("OPENAI_API_KEY") { - serde_json::json!({ - "provider": "openai", - "model": std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-5.6".into()), - "apiKey": key, - "maxTokens": 2048, - "systemPrompt": "You are Pocket Pi, a tiny agent living in a QuickJS runtime. Be brief.", - "tools": [tool] - }) - } else { - match std::env::var("ANTHROPIC_API_KEY") { - Ok(key) => serde_json::json!({ - "model": "claude-opus-4-8", - "apiKey": key, - "maxTokens": 256, - "systemPrompt": "You are Pocket Pi, a tiny agent living in a QuickJS runtime. Be brief.", - "tools": [tool] - }), - Err(_) => { - eprintln!("(no ANTHROPIC_API_KEY — running the offline scripted assistant)\n"); - serde_json::json!({ - "model": "offline", - "scripted": { "steps": [ - { "text": "I'm Pocket Pi — pi's agent core running inside QuickJS, no Node, no bun." } - ]} - }) - } - } - }; - - rt.boot(&cfg.to_string()).expect("boot"); - println!("you> {prompt}\npi > "); - rt.prompt(&prompt).expect("prompt"); - - // The whole point: pump slowly. LLM latency dwarfs a 500 ms frame. - let start = Instant::now(); - while !rt.is_idle() && start.elapsed() < Duration::from_secs(120) { - rt.pump().expect("pump"); - std::thread::sleep(Duration::from_millis(500)); - } - rt.pump().ok(); -} diff --git a/crates/pocket-pi/examples/hello.rs b/crates/pocket-pi/examples/hello.rs deleted file mode 100644 index 8e45543..0000000 --- a/crates/pocket-pi/examples/hello.rs +++ /dev/null @@ -1,56 +0,0 @@ -//! Dead-simple acceptance: boot the full, embedded pi on a real model and say -//! hello. This binary carries the WHOLE unmodified pi-coding-agent inside it. -//! -//! OPENAI_API_KEY=… cargo run --release --example hello -//! OPENAI_API_KEY=… OPENAI_MODEL=gpt-5.6 cargo run --release --example hello -use pocket_pi::{HostEvent, PiRuntime}; -use std::cell::RefCell; -use std::io::Write; -use std::rc::Rc; - -fn main() { - let key = std::env::var("OPENAI_API_KEY").expect("set OPENAI_API_KEY"); - let model = std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-5.6".into()); - - // The whole pi is embedded — new() stands it up with no external files. - let mut rt = PiRuntime::new().expect("runtime"); - - let reply = Rc::new(RefCell::new(String::new())); - let r = reply.clone(); - rt.on_event(move |ev: &HostEvent| match ev.kind.as_str() { - "text" => { - if let Some(d) = ev.value.get("delta").and_then(|v| v.as_str()) { - r.borrow_mut().push_str(d); - print!("{d}"); - std::io::stdout().flush().ok(); - } - } - "assistant_text" if r.borrow().is_empty() => { - if let Some(t) = ev.value.get("text").and_then(|v| v.as_str()) { - *r.borrow_mut() = t.to_string(); - print!("{t}"); - } - } - "error" => eprintln!("\n[error] {}", ev.raw), - _ => {} - }); - - let cfg = serde_json::json!({ - "provider": "openai", "model": model, "apiKey": key, - "maxTokens": 2048, - "systemPrompt": "You are a friendly assistant. Be very brief." - }); - eprintln!("booting full pi on {model} …"); - rt.boot(&cfg.to_string()).expect("boot"); - eprint!("pi > "); - rt.prompt("Say hello in a few words.").expect("prompt"); - while !rt.is_idle() { - rt.pump().expect("pump"); - std::thread::sleep(std::time::Duration::from_millis(50)); - } - println!(); - if reply.borrow().trim().is_empty() { - eprintln!("(no reply — check OPENAI_API_KEY / proxy)"); - std::process::exit(1); - } -} diff --git a/crates/pocket-pi/examples/self_contained.rs b/crates/pocket-pi/examples/self_contained.rs deleted file mode 100644 index 09f7bf6..0000000 --- a/crates/pocket-pi/examples/self_contained.rs +++ /dev/null @@ -1,9 +0,0 @@ -//! Pocket Pi is self-contained: the whole unmodified pi-coding-agent is embedded -//! in the binary, so `PiRuntime::new()` stands it up with no external files and -//! no Node. Run: `cargo run --release --example self_contained`. -use pocket_pi::PiRuntime; - -fn main() { - let rt = PiRuntime::new().expect("runtime"); - println!("full pi loaded: {:?}", rt.get_global_json("__piFullLoaded")); -} diff --git a/crates/pocket-pi/js/node/_bootstrap.js b/crates/pocket-pi/js/node/_bootstrap.js deleted file mode 100644 index a8764fe..0000000 --- a/crates/pocket-pi/js/node/_bootstrap.js +++ /dev/null @@ -1,4 +0,0 @@ -import { Buffer } from "node:buffer"; -globalThis.Buffer = Buffer; -globalThis.__nodeBuffer = Buffer; -globalThis.process.nextTick = (fn, ...args) => queueMicrotask(() => fn(...args)); diff --git a/crates/pocket-pi/js/node/_cjs-runtime.js b/crates/pocket-pi/js/node/_cjs-runtime.js deleted file mode 100644 index 861598d..0000000 --- a/crates/pocket-pi/js/node/_cjs-runtime.js +++ /dev/null @@ -1,27 +0,0 @@ -globalThis.__cjsCache = globalThis.__cjsCache || /* @__PURE__ */ new Map(); -globalThis.__cjsRequire = function(fromFile, spec) { - const r = JSON.parse(globalThis.__node.resolve(fromFile, spec)); - if (r.builtin != null) { - const exports = globalThis.__builtinExports[r.builtin] ?? globalThis.__builtinExports[r.builtin.split("/")[0]]; - if (exports === void 0) throw new Error("builtin not available: " + r.builtin); - return exports; - } - if (r.err) throw new Error("Cannot find module '" + spec + "' from '" + fromFile + "'"); - const p = r.path; - if (globalThis.__cjsCache.has(p)) return globalThis.__cjsCache.get(p); - if (p.endsWith(".json")) { - const val = JSON.parse(globalThis.__node.readText(p)); - globalThis.__cjsCache.set(p, val); - return val; - } - let src = globalThis.__node.readText(p); - if (p.endsWith(".ts") || p.endsWith(".cts")) src = host.transpile(p, src); - const module = { exports: {} }; - globalThis.__cjsCache.set(p, module.exports); - const dir = p.replace(/\/[^/]*$/, ""); - const fn = new Function("module", "exports", "require", "__filename", "__dirname", src); - fn(module, module.exports, (s) => globalThis.__cjsRequire(p, s), p, dir); - globalThis.__cjsCache.set(p, module.exports); - return module.exports; -}; -globalThis.require = (spec) => globalThis.__cjsRequire("/pocket-pi-bundle", spec); diff --git a/crates/pocket-pi/js/node/assert.js b/crates/pocket-pi/js/node/assert.js deleted file mode 100644 index aa99fac..0000000 --- a/crates/pocket-pi/js/node/assert.js +++ /dev/null @@ -1,33 +0,0 @@ -function assert(v, msg) { - if (!v) throw new Error(msg || "Assertion failed"); -} -assert.ok = assert; -assert.equal = (a, b, m) => { - if (a != b) throw new Error(m || a + " != " + b); -}; -assert.strictEqual = (a, b, m) => { - if (a !== b) throw new Error(m || a + " !== " + b); -}; -assert.deepEqual = (a, b, m) => { - if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(m || "deepEqual failed"); -}; -assert.deepStrictEqual = assert.deepEqual; -assert.notEqual = (a, b, m) => { - if (a == b) throw new Error(m || "notEqual failed"); -}; -assert.throws = (fn, m) => { - try { - fn(); - } catch { - return; - } - throw new Error(m || "expected throw"); -}; -assert.fail = (m) => { - throw new Error(m || "fail"); -}; -var assert_default = assert; -export { - assert, - assert_default as default -}; diff --git a/crates/pocket-pi/js/node/async_hooks.js b/crates/pocket-pi/js/node/async_hooks.js deleted file mode 100644 index 0decaa3..0000000 --- a/crates/pocket-pi/js/node/async_hooks.js +++ /dev/null @@ -1,66 +0,0 @@ -class AsyncLocalStorage { - run(_store, cb, ...args) { - return cb(...args); - } - getStore() { - return this._store; - } - enterWith(store) { - this._store = store; - } - exit(cb, ...args) { - return cb(...args); - } - disable() { - } -} -class AsyncResource { - constructor(type, opts) { - this.type = type; - this._opts = opts; - } - runInAsyncScope(fn, thisArg, ...args) { - return fn.apply(thisArg, args); - } - emitDestroy() { - return this; - } - asyncId() { - return 0; - } - triggerAsyncId() { - return 0; - } - bind(fn) { - return fn; - } - static bind(fn) { - return fn; - } -} -function createHook() { - return { enable() { - return this; - }, disable() { - return this; - } }; -} -function executionAsyncId() { - return 0; -} -function triggerAsyncId() { - return 0; -} -function executionAsyncResource() { - return {}; -} -var async_hooks_default = { AsyncLocalStorage, AsyncResource, createHook, executionAsyncId, triggerAsyncId, executionAsyncResource }; -export { - AsyncLocalStorage, - AsyncResource, - createHook, - async_hooks_default as default, - executionAsyncId, - executionAsyncResource, - triggerAsyncId -}; diff --git a/crates/pocket-pi/js/node/buffer.js b/crates/pocket-pi/js/node/buffer.js deleted file mode 100644 index ad27455..0000000 --- a/crates/pocket-pi/js/node/buffer.js +++ /dev/null @@ -1,127 +0,0 @@ -const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; -function utf8ToBytes(str) { - const out = []; - for (let i = 0; i < str.length; i++) { - let c = str.charCodeAt(i); - if (c < 128) out.push(c); - else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63); - else if (c >= 55296 && c <= 56319) { - const c2 = str.charCodeAt(++i); - c = 65536 + ((c & 1023) << 10) + (c2 & 1023); - out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63); - } else out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63); - } - return out; -} -function bytesToUtf8(bytes) { - let out = "", i = 0; - while (i < bytes.length) { - let c = bytes[i++]; - if (c < 128) out += String.fromCharCode(c); - else if (c >= 192 && c < 224) out += String.fromCharCode((c & 31) << 6 | bytes[i++] & 63); - else if (c >= 224 && c < 240) out += String.fromCharCode((c & 15) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63); - else { - const cp = (c & 7) << 18 | (bytes[i++] & 63) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63; - const off = cp - 65536; - out += String.fromCharCode(55296 + (off >> 10), 56320 + (off & 1023)); - } - } - return out; -} -function toBase64(bytes) { - let out = ""; - for (let i = 0; i < bytes.length; i += 3) { - const b0 = bytes[i], b1 = bytes[i + 1], b2 = bytes[i + 2]; - const n = b0 << 16 | (b1 || 0) << 8 | (b2 || 0); - out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + (i + 1 < bytes.length ? B64[n >> 6 & 63] : "=") + (i + 2 < bytes.length ? B64[n & 63] : "="); - } - return out; -} -function fromBase64(str) { - str = str.replace(/[^A-Za-z0-9+/]/g, ""); - const out = []; - for (let i = 0; i < str.length; i += 4) { - const n = B64.indexOf(str[i]) << 18 | B64.indexOf(str[i + 1]) << 12 | B64.indexOf(str[i + 2] || "A") << 6 | B64.indexOf(str[i + 3] || "A"); - out.push(n >> 16 & 255); - if (str[i + 2] && str[i + 2] !== "=") out.push(n >> 8 & 255); - if (str[i + 3] && str[i + 3] !== "=") out.push(n & 255); - } - return out; -} -class Buffer extends Uint8Array { - static from(value, encoding) { - if (typeof value === "string") { - if (encoding === "base64") return new Buffer(fromBase64(value)); - if (encoding === "hex") { - const out = []; - for (let i = 0; i < value.length; i += 2) out.push(parseInt(value.substr(i, 2), 16)); - return new Buffer(out); - } - if (encoding === "latin1" || encoding === "binary") { - const out = new Buffer(value.length); - for (let i = 0; i < value.length; i++) out[i] = value.charCodeAt(i) & 255; - return out; - } - return new Buffer(utf8ToBytes(value)); - } - if (value instanceof Uint8Array || Array.isArray(value)) return new Buffer(value); - if (value instanceof ArrayBuffer) return new Buffer(new Uint8Array(value)); - return new Buffer(0); - } - static alloc(size, fill) { - const b = new Buffer(size); - if (fill != null) b.fill(typeof fill === "string" ? fill.charCodeAt(0) : fill); - return b; - } - static allocUnsafe(size) { - return new Buffer(size); - } - static isBuffer(v) { - return v instanceof Buffer; - } - static concat(list, length) { - let total = length ?? list.reduce((n, b) => n + b.length, 0); - const out = new Buffer(total); - let off = 0; - for (const b of list) { - out.set(b.subarray(0, Math.min(b.length, total - off)), off); - off += b.length; - if (off >= total) break; - } - return out; - } - static byteLength(str, encoding) { - return Buffer.from(str, encoding).length; - } - toString(encoding, start, end) { - const view = this.subarray(start || 0, end == null ? this.length : end); - if (encoding === "base64") return toBase64(view); - if (encoding === "hex") return Array.from(view, (b) => b.toString(16).padStart(2, "0")).join(""); - if (encoding === "latin1" || encoding === "binary") return Array.from(view, (b) => String.fromCharCode(b)).join(""); - return bytesToUtf8(view); - } - toJSON() { - return { type: "Buffer", data: Array.from(this) }; - } - equals(other) { - return this.length === other.length && this.every((v, i) => v === other[i]); - } - write(str, offset = 0, length, encoding) { - const src = Buffer.from(str, typeof length === "string" ? length : encoding); - const n = Math.min(src.length, this.length - offset, typeof length === "number" ? length : Infinity); - this.set(src.subarray(0, n), offset); - return n; - } - slice(a, b) { - return new Buffer(this.subarray(a, b)); - } -} -const SlowBuffer = Buffer; -const constants = { MAX_LENGTH: 2147483647, MAX_STRING_LENGTH: 536870911 }; -var buffer_default = { Buffer, SlowBuffer, constants }; -export { - Buffer, - SlowBuffer, - constants, - buffer_default as default -}; diff --git a/crates/pocket-pi/js/node/child_process.js b/crates/pocket-pi/js/node/child_process.js deleted file mode 100644 index 23b012e..0000000 --- a/crates/pocket-pi/js/node/child_process.js +++ /dev/null @@ -1,82 +0,0 @@ -import { EventEmitter } from "node:events"; -const n = globalThis.__node; -function spawnSync(cmd, args = [], options = {}) { - const res = JSON.parse(n.spawnSync(String(cmd), JSON.stringify(args || []), JSON.stringify(options || {}))); - const enc = options.encoding; - const wrap = (s) => enc && enc !== "buffer" ? s : globalThis.Buffer.from(s); - return { - status: res.status ?? null, - signal: null, - stdout: wrap(res.stdout || ""), - stderr: wrap(res.stderr || ""), - error: res.error ? new Error(res.error) : void 0, - pid: 0 - }; -} -function execSync(command, options = {}) { - const shell = options.shell || "/bin/sh"; - const res = spawnSync(shell, ["-c", String(command)], options); - if (res.status && res.status !== 0) { - const e = new Error(`Command failed: ${command} -${res.stderr}`); - e.status = res.status; - e.stdout = res.stdout; - e.stderr = res.stderr; - throw e; - } - return res.stdout; -} -function execFileSync(file, args = [], options = {}) { - return spawnSync(file, args, options).stdout; -} -function spawn(cmd, args = [], options = {}) { - const ee = new EventEmitter(); - queueMicrotask(() => { - try { - const r = spawnSync(cmd, args, options); - ee.stdout && ee.stdout.emit && ee.stdout.emit("data", r.stdout); - ee.emit("close", r.status ?? 0); - } catch (e) { - ee.emit("error", e); - } - }); - ee.stdout = { on() { - } }; - ee.stderr = { on() { - } }; - ee.stdin = { write() { - }, end() { - } }; - return ee; -} -function exec(command, options, cb) { - if (typeof options === "function") { - cb = options; - options = {}; - } - queueMicrotask(() => { - try { - const out = execSync(command, options); - cb && cb(null, out, ""); - } catch (e) { - cb && cb(e, e.stdout || "", e.stderr || ""); - } - }); - return { on() { - } }; -} -const execFile = exec; -function fork() { - throw new Error("child_process.fork is not supported in Pocket Pi"); -} -var child_process_default = { spawnSync, execSync, execFileSync, spawn, exec, execFile, fork }; -export { - child_process_default as default, - exec, - execFile, - execFileSync, - execSync, - fork, - spawn, - spawnSync -}; diff --git a/crates/pocket-pi/js/node/console.js b/crates/pocket-pi/js/node/console.js deleted file mode 100644 index 04c452f..0000000 --- a/crates/pocket-pi/js/node/console.js +++ /dev/null @@ -1,82 +0,0 @@ -const g = globalThis.console || {}; -const out = (...a) => { - try { - (g.log || (() => { - }))(...a); - } catch { - } -}; -const err = (...a) => { - try { - (g.error || g.log || (() => { - }))(...a); - } catch { - } -}; -class Console { - constructor(_stdout, _stderr) { - } - log(...a) { - out(...a); - } - info(...a) { - out(...a); - } - debug(...a) { - out(...a); - } - dir(...a) { - out(...a); - } - warn(...a) { - err(...a); - } - error(...a) { - err(...a); - } - trace(...a) { - err(...a); - } - table(...a) { - out(...a); - } - group(...a) { - out(...a); - } - groupCollapsed(...a) { - out(...a); - } - groupEnd() { - } - assert(cond, ...a) { - if (!cond) err("Assertion failed:", ...a); - } - count() { - } - countReset() { - } - time() { - } - timeEnd() { - } - timeLog() { - } - clear() { - } -} -const instance = new Console(); -const log = instance.log; -const info = instance.info; -const warn = instance.warn; -const error = instance.error; -const debug = instance.debug; -var console_default = instance; -export { - Console, - debug, - console_default as default, - error, - info, - log, - warn -}; diff --git a/crates/pocket-pi/js/node/constants.js b/crates/pocket-pi/js/node/constants.js deleted file mode 100644 index 9da2c55..0000000 --- a/crates/pocket-pi/js/node/constants.js +++ /dev/null @@ -1,4 +0,0 @@ -var constants_default = {}; -export { - constants_default as default -}; diff --git a/crates/pocket-pi/js/node/crypto.js b/crates/pocket-pi/js/node/crypto.js deleted file mode 100644 index 2dffddf..0000000 --- a/crates/pocket-pi/js/node/crypto.js +++ /dev/null @@ -1,74 +0,0 @@ -function fnv(str) { - let h = 0xcbf29ce484222325n; - const bytes = globalThis.Buffer ? globalThis.Buffer.from(str) : new TextEncoder().encode(str); - for (const b of bytes) { - h ^= BigInt(b); - h = h * 0x100000001b3n & 0xffffffffffffffffn; - } - return h; -} -function randomBytes(n) { - const b = globalThis.Buffer ? globalThis.Buffer.alloc(n) : new Uint8Array(n); - for (let i = 0; i < n; i++) b[i] = Math.floor(Math.random() * 256); - return b; -} -function randomUUID() { - return globalThis.crypto.randomUUID(); -} -function randomFillSync(buf) { - for (let i = 0; i < buf.length; i++) buf[i] = Math.floor(Math.random() * 256); - return buf; -} -function createHash(_algo) { - let data = ""; - return { - update(chunk) { - data += typeof chunk === "string" ? chunk : globalThis.Buffer.from(chunk).toString(); - return this; - }, - digest(enc) { - let hex = ""; - for (let i = 0; i < 4; i++) hex += fnv(i + ":" + data).toString(16).padStart(16, "0"); - if (enc === "hex") return hex; - const bytes = globalThis.Buffer.from(hex, "hex"); - return enc ? bytes.toString(enc) : bytes; - } - }; -} -function createHmac(algo, key) { - const h = createHash(algo); - h.update(String(key) + ":"); - return h; -} -function getHashes() { - return ["sha1", "sha256", "sha384", "sha512", "md5"]; -} -function getCiphers() { - return []; -} -function timingSafeEqual(a, b) { - if (!a || !b || a.length !== b.length) return false; - let diff = 0; - for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; - return diff === 0; -} -const constants = {}; -const webcrypto = globalThis.crypto; -function getRandomValues(a) { - return globalThis.crypto.getRandomValues(a); -} -var crypto_default = { randomBytes, randomUUID, randomFillSync, createHash, createHmac, getHashes, getCiphers, timingSafeEqual, constants, webcrypto, getRandomValues }; -export { - constants, - createHash, - createHmac, - crypto_default as default, - getCiphers, - getHashes, - getRandomValues, - randomBytes, - randomFillSync, - randomUUID, - timingSafeEqual, - webcrypto -}; diff --git a/crates/pocket-pi/js/node/diagnostics_channel.js b/crates/pocket-pi/js/node/diagnostics_channel.js deleted file mode 100644 index 02905e9..0000000 --- a/crates/pocket-pi/js/node/diagnostics_channel.js +++ /dev/null @@ -1,72 +0,0 @@ -class Channel { - constructor(name) { - this.name = name; - this._subs = []; - } - get hasSubscribers() { - return this._subs.length > 0; - } - publish(msg) { - for (const s of this._subs.slice()) { - try { - s(msg, this.name); - } catch { - } - } - } - subscribe(fn) { - this._subs.push(fn); - } - unsubscribe(fn) { - this._subs = this._subs.filter((s) => s !== fn); - return true; - } -} -const registry = /* @__PURE__ */ new Map(); -function channel(name) { - let c = registry.get(name); - if (!c) { - c = new Channel(name); - registry.set(name, c); - } - return c; -} -function hasSubscribers(name) { - const c = registry.get(name); - return !!c && c.hasSubscribers; -} -function subscribe(name, fn) { - channel(name).subscribe(fn); -} -function unsubscribe(name, fn) { - return channel(name).unsubscribe(fn); -} -function tracingChannel(nameOrChannels) { - const base = typeof nameOrChannels === "string" ? nameOrChannels : ""; - const mk = (suffix) => channel(base ? `tracing:${base}:${suffix}` : suffix); - return { - start: mk("start"), - end: mk("end"), - asyncStart: mk("asyncStart"), - asyncEnd: mk("asyncEnd"), - error: mk("error"), - traceSync(fn, ctx, thisArg, ...a) { - return fn.apply(thisArg, a); - }, - tracePromise(fn, ctx, thisArg, ...a) { - return fn.apply(thisArg, a); - }, - traceCallback(fn, pos, ctx, thisArg, ...a) { - return fn.apply(thisArg, a); - } - }; -} -var diagnostics_channel_default = { channel, hasSubscribers, subscribe, unsubscribe, tracingChannel, Channel }; -export { - channel, - diagnostics_channel_default as default, - hasSubscribers, - subscribe, - tracingChannel, - unsubscribe -}; diff --git a/crates/pocket-pi/js/node/dns.js b/crates/pocket-pi/js/node/dns.js deleted file mode 100644 index 1795e59..0000000 --- a/crates/pocket-pi/js/node/dns.js +++ /dev/null @@ -1,15 +0,0 @@ -function lookup(host, _o, cb) { - const c = typeof _o === "function" ? _o : cb; - c && c(null, "127.0.0.1", 4); -} -function resolve(_h, cb) { - cb && cb(null, []); -} -const promises = { lookup: async () => ({ address: "127.0.0.1", family: 4 }), resolve: async () => [] }; -var dns_default = { lookup, resolve, promises }; -export { - dns_default as default, - lookup, - promises, - resolve -}; diff --git a/crates/pocket-pi/js/node/events.js b/crates/pocket-pi/js/node/events.js deleted file mode 100644 index 2dc8587..0000000 --- a/crates/pocket-pi/js/node/events.js +++ /dev/null @@ -1,89 +0,0 @@ -class EventEmitter { - constructor() { - this._events = /* @__PURE__ */ new Map(); - this._maxListeners = 10; - } - setMaxListeners(n) { - this._maxListeners = n; - return this; - } - getMaxListeners() { - return this._maxListeners; - } - on(type, fn) { - let arr = this._events.get(type); - if (!arr) { - arr = []; - this._events.set(type, arr); - } - arr.push(fn); - return this; - } - addListener(type, fn) { - return this.on(type, fn); - } - once(type, fn) { - const wrap = (...args) => { - this.off(type, wrap); - fn(...args); - }; - wrap.listener = fn; - return this.on(type, wrap); - } - prependListener(type, fn) { - let arr = this._events.get(type); - if (!arr) { - arr = []; - this._events.set(type, arr); - } - arr.unshift(fn); - return this; - } - off(type, fn) { - const arr = this._events.get(type); - if (arr) { - const i = arr.findIndex((f) => f === fn || f.listener === fn); - if (i !== -1) arr.splice(i, 1); - } - return this; - } - removeListener(type, fn) { - return this.off(type, fn); - } - removeAllListeners(type) { - if (type === void 0) this._events.clear(); - else this._events.delete(type); - return this; - } - emit(type, ...args) { - const arr = this._events.get(type); - if (!arr || arr.length === 0) { - if (type === "error") throw args[0] instanceof Error ? args[0] : new Error("Unhandled error"); - return false; - } - for (const fn of arr.slice()) fn.apply(this, args); - return true; - } - listeners(type) { - return (this._events.get(type) || []).slice(); - } - listenerCount(type) { - return (this._events.get(type) || []).length; - } - eventNames() { - return [...this._events.keys()]; - } -} -const once = (emitter, name) => new Promise((resolve, reject) => { - emitter.once(name, (...args) => resolve(args)); - emitter.once("error", reject); -}); -var events_default = EventEmitter; -EventEmitter.EventEmitter = EventEmitter; -EventEmitter.once = once; -EventEmitter.defaultMaxListeners = 10; -export { - EventEmitter, - events_default as default, - once -}; diff --git a/crates/pocket-pi/js/node/fs-promises.js b/crates/pocket-pi/js/node/fs-promises.js deleted file mode 100644 index a77f78a..0000000 --- a/crates/pocket-pi/js/node/fs-promises.js +++ /dev/null @@ -1,50 +0,0 @@ -import fs from "node:fs"; -const p = fs.promises; -const readFile = p.readFile; -const writeFile = p.writeFile; -const readdir = p.readdir; -const mkdir = p.mkdir; -const stat = p.stat; -const lstat = p.lstat; -const realpath = p.realpath; -const readlink = p.readlink; -const unlink = p.unlink; -const rm = p.rm; -const rmdir = p.rmdir; -const rename = p.rename; -const copyFile = p.copyFile; -const cp = p.cp; -const appendFile = p.appendFile; -const chmod = p.chmod; -const symlink = p.symlink; -const utimes = p.utimes; -const truncate = p.truncate; -const mkdtemp = p.mkdtemp; -const access = p.access; -const open = p.open; -var fs_promises_default = p; -export { - access, - appendFile, - chmod, - copyFile, - cp, - fs_promises_default as default, - lstat, - mkdir, - mkdtemp, - open, - readFile, - readdir, - readlink, - realpath, - rename, - rm, - rmdir, - stat, - symlink, - truncate, - unlink, - utimes, - writeFile -}; diff --git a/crates/pocket-pi/js/node/fs.js b/crates/pocket-pi/js/node/fs.js deleted file mode 100644 index 3a277a6..0000000 --- a/crates/pocket-pi/js/node/fs.js +++ /dev/null @@ -1,322 +0,0 @@ -const raw = globalThis.__node.fs; -const fs = { - readFile: (p) => JSON.parse(raw.readFile(p)), - writeFile: (p, b) => JSON.parse(raw.writeFile(p, b)), - exists: (p) => raw.exists(p), - readdir: (p) => JSON.parse(raw.readdir(p)), - mkdir: (p, r) => raw.mkdir(p, r), - stat: (p) => JSON.parse(raw.stat(p)), - realpath: (p) => JSON.parse(raw.realpath(p)), - unlink: (p) => raw.unlink(p) -}; -function decode(bytesJson, encoding) { - const bytes = bytesJson; - if (!encoding) return globalThis.Buffer ? globalThis.Buffer.from(bytes) : Uint8Array.from(bytes); - const B = globalThis.__nodeBuffer; - return B ? B.from(bytes).toString(encoding) : String.fromCharCode(...bytes); -} -function readFileSync(path, options) { - const encoding = typeof options === "string" ? options : options && options.encoding; - const res = fs.readFile(String(path)); - if (res.err) throw enoent(res.err, path); - return decode(res.bytes, encoding); -} -function writeFileSync(pathOrFd, data, options) { - if (typeof pathOrFd === "number") { - fdWrite(pathOrFd, data, options); - return; - } - const encoding = typeof options === "string" ? options : options && options.encoding || "utf8"; - let bytes; - if (typeof data === "string") { - const B = globalThis.__nodeBuffer; - bytes = B ? Array.from(B.from(data, encoding)) : Array.from(data, (c) => c.charCodeAt(0)); - } else bytes = Array.from(data); - const res = fs.writeFile(String(pathOrFd), bytes); - if (res.err) throw new Error(res.err); -} -function existsSync(path) { - return fs.exists(String(path)); -} -function readdirSync(path) { - const res = fs.readdir(String(path)); - if (res.err) throw enoent(res.err, path); - return res.entries; -} -function mkdirSync(path, options) { - fs.mkdir(String(path), !!(options && options.recursive)); -} -function statSync(path) { - const res = fs.stat(String(path)); - if (res.err) throw enoent(res.err, path); - return makeStat(res); -} -const lstatSync = statSync; -function realpathSync(path) { - const res = fs.realpath(String(path)); - return res.err ? String(path) : res.path; -} -function unlinkSync(path) { - fs.unlink(String(path)); -} -function rmSync(path) { - fs.unlink(String(path)); -} -const constants = { F_OK: 0, R_OK: 4, W_OK: 2, X_OK: 1 }; -function accessSync(path, _mode) { - if (!existsSync(path)) throw enoent("no access", path); -} -const nope = (name) => () => { - throw new Error(`fs.${name} is not implemented in Pocket Pi`); -}; -const __fds = /* @__PURE__ */ new Map(); -let __nextFd = 3; -function ebadf() { - const e = new Error("EBADF: bad file descriptor"); - e.code = "EBADF"; - return e; -} -function parseFlags(flags) { - const f = String(flags || "r"); - return { - write: /[wa+]/.test(f), - append: /a/.test(f), - create: /[wa]/.test(f), - excl: /x/.test(f), - truncate: /w/.test(f) && !/\+/.test(f), - read: /r|\+/.test(f) - }; -} -function openSync(path, flags, _mode) { - path = String(path); - const f = parseFlags(flags); - const exists2 = existsSync(path); - if (f.excl && exists2) { - const e = new Error(`EEXIST: file already exists, open '${path}'`); - e.code = "EEXIST"; - throw e; - } - if (!f.create && !exists2) throw enoent("open", path); - if (f.truncate || f.create && !exists2) writeFileSync(path, ""); - const bytes = f.read && !f.truncate && existsSync(path) ? fs.readFile(path).bytes || [] : []; - const fd = __nextFd++; - __fds.set(fd, { path, flags: f, bytes, pos: 0 }); - return fd; -} -function readSync(fd, buffer, offset, length, position) { - const e = __fds.get(fd); - if (!e) throw ebadf(); - const start = position == null || position < 0 ? e.pos : position; - let n = 0; - for (; n < length && start + n < e.bytes.length; n++) buffer[offset + n] = e.bytes[start + n]; - if (position == null || position < 0) e.pos = start + n; - return n; -} -function toStr(data, options) { - if (typeof data === "string") return data; - const enc = (typeof options === "string" ? options : options && options.encoding) || "utf8"; - const B = globalThis.__nodeBuffer; - return B ? B.from(data).toString(enc) : String.fromCharCode(...data); -} -function fdWrite(fd, data, options) { - const e = __fds.get(fd); - if (!e) throw ebadf(); - const prev = existsSync(e.path) ? readFileSync(e.path, "utf8") : ""; - writeFileSync(e.path, prev + toStr(data, options), "utf8"); -} -function writeSync(fd, data, _offOrPos, _length, _position) { - const s = typeof data === "string" ? data : toStr(data); - fdWrite(fd, s); - return typeof data === "string" ? s.length : data.length; -} -function closeSync(fd) { - __fds.delete(fd); -} -const fsyncSync = () => { -}; -const fdatasyncSync = () => { -}; -const ftruncateSync = () => { -}; -function appendFileSync(path, data, options) { - const prev = existsSync(path) ? readFileSync(path, "utf8") : ""; - writeFileSync(path, prev + (typeof data === "string" ? data : ""), options); -} -const copyFileSync = nope("copyFileSync"); -function renameSync(a, b) { - const d = readFileSync(a); - writeFileSync(b, d); - unlinkSync(a); -} -const rmdirSync = (p) => unlinkSync(p); -const cpSync = nope("cpSync"); -const chmodSync = () => { -}; -const symlinkSync = nope("symlinkSync"); -const readlinkSync = (p) => realpathSync(p); -const truncateSync = nope("truncateSync"); -const utimesSync = () => { -}; -const createReadStream = nope("createReadStream"); -const createWriteStream = nope("createWriteStream"); -const watchFile = () => { -}; -const unwatchFile = () => { -}; -function watch() { - return { close() { - }, on() { - }, unref() { - return this; - } }; -} -const opendirSync = nope("opendirSync"); -const mkdtempSync = (prefix) => { - const p = prefix + Math.random().toString(36).slice(2, 8); - mkdirSync(p, { recursive: true }); - return p; -}; -function makeStat(res) { - return { - size: res.size || 0, - mtimeMs: res.mtimeMs || 0, - isFile: () => res.isFile, - isDirectory: () => res.isDir, - isSymbolicLink: () => false - }; -} -function enoent(msg, path) { - const e = new Error(`ENOENT: ${msg}, '${path}'`); - e.code = "ENOENT"; - e.path = String(path); - return e; -} -const cbify = (fn) => (...args) => { - const cb = typeof args[args.length - 1] === "function" ? args.pop() : () => { - }; - queueMicrotask(() => { - try { - cb(null, fn(...args)); - } catch (e) { - cb(e); - } - }); -}; -const readFile = cbify(readFileSync); -const writeFile = cbify(writeFileSync); -const readdir = cbify(readdirSync); -const stat = cbify(statSync); -const lstat = cbify(statSync); -const mkdir = cbify(mkdirSync); -const access = cbify(accessSync); -const unlink = cbify(unlinkSync); -const realpath = cbify(realpathSync); -const rename = cbify(renameSync); -const rm = cbify(unlinkSync); -const exists = (p, cb) => queueMicrotask(() => cb(existsSync(p))); -const P = (fn) => (...args) => new Promise((res, rej) => { - try { - res(fn(...args)); - } catch (e) { - rej(e); - } -}); -const promises = { - readFile: P(readFileSync), - writeFile: P(writeFileSync), - readdir: P(readdirSync), - mkdir: P(mkdirSync), - stat: P(statSync), - lstat: P(statSync), - realpath: P(realpathSync), - readlink: P(readlinkSync), - unlink: P(unlinkSync), - rm: P(rmSync), - rmdir: P(rmdirSync), - rename: P(renameSync), - copyFile: P(copyFileSync), - cp: P(cpSync), - appendFile: P(appendFileSync), - chmod: P(chmodSync), - symlink: P(symlinkSync), - utimes: P(utimesSync), - truncate: P(truncateSync), - mkdtemp: P(mkdtempSync), - access: P(accessSync), - open: P((path) => ({ - fd: 0, - readFile: (opts) => readFileSync(path, opts), - writeFile: (data, opts) => writeFileSync(path, data, opts), - stat: () => statSync(path), - close: () => { - }, - read: () => ({ bytesRead: 0, buffer: null }), - write: () => ({ bytesWritten: 0 }) - })) -}; -var fs_default = { - readFileSync, - writeFileSync, - existsSync, - readdirSync, - mkdirSync, - statSync, - lstatSync, - realpathSync, - unlinkSync, - rmSync, - promises, - constants: { F_OK: 0, R_OK: 4, W_OK: 2, X_OK: 1 } -}; -export { - access, - accessSync, - appendFileSync, - chmodSync, - closeSync, - constants, - copyFileSync, - cpSync, - createReadStream, - createWriteStream, - fs_default as default, - exists, - existsSync, - fdatasyncSync, - fsyncSync, - ftruncateSync, - lstat, - lstatSync, - mkdir, - mkdirSync, - mkdtempSync, - openSync, - opendirSync, - promises, - readFile, - readFileSync, - readSync, - readdir, - readdirSync, - readlinkSync, - realpath, - realpathSync, - rename, - renameSync, - rm, - rmSync, - rmdirSync, - stat, - statSync, - symlinkSync, - truncateSync, - unlink, - unlinkSync, - unwatchFile, - utimesSync, - watch, - watchFile, - writeFile, - writeFileSync, - writeSync -}; diff --git a/crates/pocket-pi/js/node/http.js b/crates/pocket-pi/js/node/http.js deleted file mode 100644 index 04d4ede..0000000 --- a/crates/pocket-pi/js/node/http.js +++ /dev/null @@ -1,37 +0,0 @@ -import { EventEmitter } from "node:events"; -class Agent { - constructor(o) { - this.options = o || {}; - } -} -class Server extends EventEmitter { - listen() { - return this; - } - close() { - } -} -function request() { - throw new Error("http.request not supported (use fetch)"); -} -function get() { - throw new Error("http.get not supported (use fetch)"); -} -const globalAgent = new Agent(); -const METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]; -const STATUS_CODES = {}; -function createServer() { - return new Server(); -} -var http_default = { Agent, Server, request, get, globalAgent, METHODS, STATUS_CODES, createServer }; -export { - Agent, - METHODS, - STATUS_CODES, - Server, - createServer, - http_default as default, - get, - globalAgent, - request -}; diff --git a/crates/pocket-pi/js/node/https.js b/crates/pocket-pi/js/node/https.js deleted file mode 100644 index 1093fbe..0000000 --- a/crates/pocket-pi/js/node/https.js +++ /dev/null @@ -1,37 +0,0 @@ -import { EventEmitter } from "node:events"; -class Agent { - constructor(o) { - this.options = o || {}; - } -} -class Server extends EventEmitter { - listen() { - return this; - } - close() { - } -} -function request() { - throw new Error("https.request not supported (use fetch)"); -} -function get() { - throw new Error("https.get not supported (use fetch)"); -} -const globalAgent = new Agent(); -const METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]; -const STATUS_CODES = {}; -function createServer() { - return new Server(); -} -var https_default = { Agent, Server, request, get, globalAgent, METHODS, STATUS_CODES, createServer }; -export { - Agent, - METHODS, - STATUS_CODES, - Server, - createServer, - https_default as default, - get, - globalAgent, - request -}; diff --git a/crates/pocket-pi/js/node/module.js b/crates/pocket-pi/js/node/module.js deleted file mode 100644 index 6ba8515..0000000 --- a/crates/pocket-pi/js/node/module.js +++ /dev/null @@ -1,24 +0,0 @@ -function createRequire(from) { - let base = "/pocket-pi-bundle"; - if (typeof from === "string") base = from.startsWith("file://") ? from.slice(7) : from; - else if (from && from.href) base = String(from.href).replace(/^file:\/\//, ""); - return function require2(spec) { - return globalThis.__cjsRequire(base, spec); - }; -} -const builtinModules = ["fs", "path", "os", "events", "util", "buffer", "process", "crypto", "url", "child_process", "stream", "string_decoder", "module", "readline", "http", "https", "net", "tls", "zlib", "assert", "querystring"]; -function isBuiltin(m) { - return builtinModules.includes(String(m).replace(/^node:/, "")); -} -class Module { -} -Module.createRequire = createRequire; -Module.builtinModules = builtinModules; -var module_default = { createRequire, builtinModules, isBuiltin, Module }; -export { - Module, - builtinModules, - createRequire, - module_default as default, - isBuiltin -}; diff --git a/crates/pocket-pi/js/node/net.js b/crates/pocket-pi/js/node/net.js deleted file mode 100644 index c0d1fab..0000000 --- a/crates/pocket-pi/js/node/net.js +++ /dev/null @@ -1,56 +0,0 @@ -import { EventEmitter } from "node:events"; -class Socket extends EventEmitter { - connect() { - return this; - } - write() { - return true; - } - end() { - } - destroy() { - } - setTimeout() { - } - setNoDelay() { - } - setKeepAlive() { - } -} -class Server extends EventEmitter { - listen() { - return this; - } - close() { - } -} -function connect() { - return new Socket(); -} -function createConnection() { - return new Socket(); -} -function createServer() { - return new Server(); -} -function isIP(s) { - return /^\d+\.\d+\.\d+\.\d+$/.test(s) ? 4 : 0; -} -function isIPv4(s) { - return isIP(s) === 4; -} -function isIPv6() { - return false; -} -var net_default = { Socket, Server, connect, createConnection, createServer, isIP, isIPv4, isIPv6 }; -export { - Server, - Socket, - connect, - createConnection, - createServer, - net_default as default, - isIP, - isIPv4, - isIPv6 -}; diff --git a/crates/pocket-pi/js/node/os.js b/crates/pocket-pi/js/node/os.js deleted file mode 100644 index 265b912..0000000 --- a/crates/pocket-pi/js/node/os.js +++ /dev/null @@ -1,57 +0,0 @@ -const n = globalThis.__node; -function platform() { - return globalThis.process && globalThis.process.platform || "linux"; -} -function homedir() { - return n ? n.homedir() : "/"; -} -function tmpdir() { - return n ? n.tmpdir() : "/tmp"; -} -function hostname() { - return n && n.hostname ? n.hostname() : "localhost"; -} -function arch() { - return globalThis.process && globalThis.process.arch || "x64"; -} -function type() { - return platform() === "darwin" ? "Darwin" : platform() === "win32" ? "Windows_NT" : "Linux"; -} -function release() { - return "0.0.0"; -} -function cpus() { - return []; -} -function totalmem() { - return 0; -} -function freemem() { - return 0; -} -function uptime() { - return 0; -} -function userInfo() { - return { username: "user", homedir: homedir(), shell: null, uid: -1, gid: -1 }; -} -const EOL = "\n"; -const constants = { signals: {}, errno: {} }; -var os_default = { platform, homedir, tmpdir, hostname, arch, type, release, cpus, totalmem, freemem, uptime, userInfo, EOL, constants }; -export { - EOL, - arch, - constants, - cpus, - os_default as default, - freemem, - homedir, - hostname, - platform, - release, - tmpdir, - totalmem, - type, - uptime, - userInfo -}; diff --git a/crates/pocket-pi/js/node/path.js b/crates/pocket-pi/js/node/path.js deleted file mode 100644 index 22dc2ec..0000000 --- a/crates/pocket-pi/js/node/path.js +++ /dev/null @@ -1,167 +0,0 @@ -const sep = "/"; -function assertPath(p) { - if (typeof p !== "string") throw new TypeError("Path must be a string. Received " + typeof p); -} -function normalizeArray(parts, allowAboveRoot) { - const res = []; - for (const p of parts) { - if (!p || p === ".") continue; - if (p === "..") { - if (res.length && res[res.length - 1] !== "..") res.pop(); - else if (allowAboveRoot) res.push(".."); - } else res.push(p); - } - return res; -} -function normalize(path) { - assertPath(path); - if (path.length === 0) return "."; - const isAbs = path.charCodeAt(0) === 47; - const trailing = path.charCodeAt(path.length - 1) === 47; - let out = normalizeArray(path.split("/"), !isAbs).join("/"); - if (!out && !isAbs) out = "."; - if (out && trailing) out += "/"; - return (isAbs ? "/" : "") + out; -} -function isAbsolute(path) { - assertPath(path); - return path.length > 0 && path.charCodeAt(0) === 47; -} -function join(...args) { - if (args.length === 0) return "."; - let joined; - for (const arg of args) { - assertPath(arg); - if (arg.length > 0) joined = joined === void 0 ? arg : joined + "/" + arg; - } - if (joined === void 0) return "."; - return normalize(joined); -} -function resolve(...args) { - let resolved = ""; - let isAbs = false; - for (let i = args.length - 1; i >= -1 && !isAbs; i--) { - const path = i >= 0 ? args[i] : cwd(); - assertPath(path); - if (path.length === 0) continue; - resolved = path + "/" + resolved; - isAbs = path.charCodeAt(0) === 47; - } - const parts = normalizeArray(resolved.split("/"), !isAbs); - resolved = parts.join("/"); - if (isAbs) return "/" + resolved; - return resolved.length > 0 ? resolved : "."; -} -function cwd() { - return globalThis.process && globalThis.process.cwd && globalThis.process.cwd() || "/"; -} -function dirname(path) { - assertPath(path); - if (path.length === 0) return "."; - let end = -1; - let matchedSlash = true; - for (let i = path.length - 1; i >= 1; --i) { - if (path.charCodeAt(i) === 47) { - if (!matchedSlash) { - end = i; - break; - } - } else matchedSlash = false; - } - if (end === -1) return path.charCodeAt(0) === 47 ? "/" : "."; - if (end === 0 && path.charCodeAt(0) === 47) return "/"; - return path.slice(0, end); -} -function basename(path, ext) { - assertPath(path); - let start = 0, end = -1, matchedSlash = true; - for (let i = path.length - 1; i >= 0; --i) { - if (path.charCodeAt(i) === 47) { - if (!matchedSlash) { - start = i + 1; - break; - } - } else if (end === -1) { - matchedSlash = false; - end = i + 1; - } - } - let base = end === -1 ? "" : path.slice(start, end); - if (ext && base.endsWith(ext) && base !== ext) base = base.slice(0, -ext.length); - return base; -} -function extname(path) { - assertPath(path); - let startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0; - for (let i = path.length - 1; i >= 0; --i) { - const code = path.charCodeAt(i); - if (code === 47) { - if (!matchedSlash) { - startPart = i + 1; - break; - } - continue; - } - if (end === -1) { - matchedSlash = false; - end = i + 1; - } - if (code === 46) { - if (startDot === -1) startDot = i; - else if (preDotState !== 1) preDotState = 1; - } else if (startDot !== -1) preDotState = -1; - } - if (startDot === -1 || end === -1 || preDotState === 0 || preDotState === 1 && startDot === end - 1 && startDot === startPart + 1) return ""; - return path.slice(startDot, end); -} -function relative(from, to) { - from = resolve(from); - to = resolve(to); - if (from === to) return ""; - const fromParts = from.split("/").filter(Boolean); - const toParts = to.split("/").filter(Boolean); - let i = 0; - while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) i++; - const up = fromParts.length - i; - const out = []; - for (let j = 0; j < up; j++) out.push(".."); - return out.concat(toParts.slice(i)).join("/"); -} -function parse(path) { - const root = isAbsolute(path) ? "/" : ""; - const dir = dirname(path); - const base = basename(path); - const ext = extname(base); - return { root, dir, base, ext, name: ext ? base.slice(0, -ext.length) : base }; -} -function toNamespacedPath(path) { - return path; -} -function format(obj) { - const dir = obj.dir || obj.root || ""; - const base = obj.base || `${obj.name || ""}${obj.ext || ""}`; - if (!dir) return base; - return dir === obj.root ? `${dir}${base}` : `${dir}${sep}${base}`; -} -const posix = { sep, delimiter: ":", normalize, isAbsolute, join, resolve, dirname, basename, extname, relative, parse, format, toNamespacedPath }; -const win32 = { ...posix, sep: "\\", delimiter: ";" }; -const delimiter = ":"; -var path_default = { sep, delimiter, normalize, isAbsolute, join, resolve, dirname, basename, extname, relative, parse, format, toNamespacedPath, posix, win32 }; -export { - basename, - path_default as default, - delimiter, - dirname, - extname, - format, - isAbsolute, - join, - normalize, - parse, - posix, - relative, - resolve, - sep, - toNamespacedPath, - win32 -}; diff --git a/crates/pocket-pi/js/node/perf_hooks.js b/crates/pocket-pi/js/node/perf_hooks.js deleted file mode 100644 index d3c9d5b..0000000 --- a/crates/pocket-pi/js/node/perf_hooks.js +++ /dev/null @@ -1,13 +0,0 @@ -const performance = globalThis.performance || { now: () => Date.now(), timeOrigin: 0 }; -class PerformanceObserver { - observe() { - } - disconnect() { - } -} -var perf_hooks_default = { performance, PerformanceObserver }; -export { - PerformanceObserver, - perf_hooks_default as default, - performance -}; diff --git a/crates/pocket-pi/js/node/process.js b/crates/pocket-pi/js/node/process.js deleted file mode 100644 index 3f67a7c..0000000 --- a/crates/pocket-pi/js/node/process.js +++ /dev/null @@ -1,21 +0,0 @@ -const p = globalThis.process; -const env = p.env; -const platform = p.platform; -const argv = p.argv; -const version = p.version; -const versions = p.versions; -const cwd = p.cwd; -const nextTick = p.nextTick; -const exit = p.exit; -var process_default = p; -export { - argv, - cwd, - process_default as default, - env, - exit, - nextTick, - platform, - version, - versions -}; diff --git a/crates/pocket-pi/js/node/querystring.js b/crates/pocket-pi/js/node/querystring.js deleted file mode 100644 index 28cf980..0000000 --- a/crates/pocket-pi/js/node/querystring.js +++ /dev/null @@ -1,23 +0,0 @@ -function parse(str) { - const o = {}; - for (const p of String(str).split("&")) { - if (!p) continue; - const i = p.indexOf("="); - const k = decodeURIComponent(i < 0 ? p : p.slice(0, i)); - const v = i < 0 ? "" : decodeURIComponent(p.slice(i + 1)); - o[k] = v; - } - return o; -} -function stringify(obj) { - return Object.entries(obj || {}).map(([k, v]) => encodeURIComponent(k) + "=" + encodeURIComponent(v)).join("&"); -} -const decode = parse, encode = stringify; -var querystring_default = { parse, stringify, decode, encode }; -export { - decode, - querystring_default as default, - encode, - parse, - stringify -}; diff --git a/crates/pocket-pi/js/node/readline.js b/crates/pocket-pi/js/node/readline.js deleted file mode 100644 index 0cf2d97..0000000 --- a/crates/pocket-pi/js/node/readline.js +++ /dev/null @@ -1,14 +0,0 @@ -import { EventEmitter } from "node:events"; -function createInterface() { - const rl = new EventEmitter(); - rl.question = (_q, cb) => cb && cb(""); - rl.close = () => { - }; - rl.on = rl.on.bind(rl); - return rl; -} -var readline_default = { createInterface }; -export { - createInterface, - readline_default as default -}; diff --git a/crates/pocket-pi/js/node/stream-promises.js b/crates/pocket-pi/js/node/stream-promises.js deleted file mode 100644 index 90b94e0..0000000 --- a/crates/pocket-pi/js/node/stream-promises.js +++ /dev/null @@ -1,19 +0,0 @@ -function pipeline(...args) { - if (typeof args[args.length - 1] === "function") args.pop(); - return Promise.resolve(); -} -function finished(stream) { - return new Promise((res) => { - if (stream && stream.on) { - stream.on("end", res); - stream.on("finish", res); - } - queueMicrotask(res); - }); -} -var stream_promises_default = { pipeline, finished }; -export { - stream_promises_default as default, - finished, - pipeline -}; diff --git a/crates/pocket-pi/js/node/stream.js b/crates/pocket-pi/js/node/stream.js deleted file mode 100644 index 1530bc4..0000000 --- a/crates/pocket-pi/js/node/stream.js +++ /dev/null @@ -1,70 +0,0 @@ -import { EventEmitter } from "node:events"; -class Readable extends EventEmitter { - constructor(opts) { - super(); - this._opts = opts || {}; - } - push(chunk) { - if (chunk === null) this.emit("end"); - else this.emit("data", chunk); - return true; - } - pipe(dest) { - this.on("data", (c) => dest.write && dest.write(c)); - this.on("end", () => dest.end && dest.end()); - return dest; - } - read() { - return null; - } - static from(iterable) { - const r = new Readable(); - queueMicrotask(async () => { - for await (const c of iterable) r.push(c); - r.push(null); - }); - return r; - } -} -class Writable extends EventEmitter { - constructor(opts) { - super(); - this._opts = opts || {}; - } - write(chunk, _enc, cb) { - if (this._opts.write) this._opts.write(chunk, _enc, cb || (() => { - })); - else if (cb) cb(); - return true; - } - end(chunk, _enc, cb) { - if (chunk) this.write(chunk); - this.emit("finish"); - if (cb) cb(); - } -} -class Duplex extends Readable { -} -class Transform extends Duplex { -} -class PassThrough extends Transform { -} -var stream_default = { Readable, Writable, Duplex, Transform, PassThrough }; -function pipeline(...args) { - const cb = typeof args[args.length - 1] === "function" ? args.pop() : null; - queueMicrotask(() => cb && cb(null)); - return args[args.length - 1]; -} -function finished(stream, cb) { - queueMicrotask(() => cb && cb(null)); -} -export { - Duplex, - PassThrough, - Readable, - Transform, - Writable, - stream_default as default, - finished, - pipeline -}; diff --git a/crates/pocket-pi/js/node/string_decoder.js b/crates/pocket-pi/js/node/string_decoder.js deleted file mode 100644 index 7a61477..0000000 --- a/crates/pocket-pi/js/node/string_decoder.js +++ /dev/null @@ -1,17 +0,0 @@ -class StringDecoder { - constructor(encoding) { - this.encoding = encoding || "utf8"; - this._dec = new TextDecoder(); - } - write(buf) { - return this._dec.decode(buf instanceof Uint8Array ? buf : globalThis.Buffer.from(buf)); - } - end(buf) { - return buf ? this.write(buf) : ""; - } -} -var string_decoder_default = { StringDecoder }; -export { - StringDecoder, - string_decoder_default as default -}; diff --git a/crates/pocket-pi/js/node/timers.js b/crates/pocket-pi/js/node/timers.js deleted file mode 100644 index c2a3807..0000000 --- a/crates/pocket-pi/js/node/timers.js +++ /dev/null @@ -1,18 +0,0 @@ -const setTimeout = globalThis.setTimeout; -const clearTimeout = globalThis.clearTimeout; -const setInterval = globalThis.setInterval; -const clearInterval = globalThis.clearInterval; -const setImmediate = (fn, ...a) => globalThis.setTimeout(fn, 0, ...a); -const clearImmediate = globalThis.clearTimeout; -const promises = { setTimeout: (ms) => new Promise((r) => globalThis.setTimeout(r, ms)) }; -var timers_default = { setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate, promises }; -export { - clearImmediate, - clearInterval, - clearTimeout, - timers_default as default, - promises, - setImmediate, - setInterval, - setTimeout -}; diff --git a/crates/pocket-pi/js/node/tls.js b/crates/pocket-pi/js/node/tls.js deleted file mode 100644 index 78e985a..0000000 --- a/crates/pocket-pi/js/node/tls.js +++ /dev/null @@ -1,18 +0,0 @@ -import { Socket } from "node:net"; -class TLSSocket extends Socket { -} -function connect() { - return new TLSSocket(); -} -function createSecureContext() { - return {}; -} -const rootCertificates = []; -var tls_default = { TLSSocket, connect, createSecureContext, rootCertificates }; -export { - TLSSocket, - connect, - createSecureContext, - tls_default as default, - rootCertificates -}; diff --git a/crates/pocket-pi/js/node/tty.js b/crates/pocket-pi/js/node/tty.js deleted file mode 100644 index 9dee833..0000000 --- a/crates/pocket-pi/js/node/tty.js +++ /dev/null @@ -1,14 +0,0 @@ -function isatty() { - return false; -} -class ReadStream { -} -class WriteStream { -} -var tty_default = { isatty, ReadStream, WriteStream }; -export { - ReadStream, - WriteStream, - tty_default as default, - isatty -}; diff --git a/crates/pocket-pi/js/node/url.js b/crates/pocket-pi/js/node/url.js deleted file mode 100644 index f0bc1b7..0000000 --- a/crates/pocket-pi/js/node/url.js +++ /dev/null @@ -1,54 +0,0 @@ -const URL = globalThis.URL; -const URLSearchParams = globalThis.URLSearchParams; -function fileURLToPath(url) { - let s = typeof url === "string" ? url : url.href; - if (s.startsWith("file://")) s = s.slice(7); - return decodeURIComponent(s); -} -function pathToFileURL(path) { - return new globalThis.URL("file://" + encodeURI(path)); -} -function parse(str) { - try { - const u = new globalThis.URL(str); - return { href: u.href, protocol: u.protocol, host: u.host, hostname: u.hostname, port: u.port, pathname: u.pathname, search: u.search, hash: u.hash, query: u.search.replace(/^\?/, "") }; - } catch { - return { href: str, pathname: str, protocol: null, host: null, hostname: null, port: "", search: "", hash: "", query: "" }; - } -} -function format(obj) { - if (typeof obj === "string") return obj; - if (obj && typeof obj.href === "string" && obj.protocol) return obj.href; - const proto = obj.protocol ? obj.protocol.endsWith(":") ? obj.protocol : obj.protocol + ":" : ""; - const host = obj.host || (obj.hostname ? obj.hostname + (obj.port ? ":" + obj.port : "") : ""); - const search = obj.search || (obj.query ? "?" + (typeof obj.query === "string" ? obj.query : new globalThis.URLSearchParams(obj.query).toString()) : ""); - return (proto ? proto + "//" : "") + host + (obj.pathname || "") + search + (obj.hash || ""); -} -function resolve(from, to) { - try { - return new globalThis.URL(to, from).href; - } catch { - return to; - } -} -const Url = globalThis.URL; -function domainToASCII(d) { - return d; -} -function domainToUnicode(d) { - return d; -} -var url_default = { URL, URLSearchParams, fileURLToPath, pathToFileURL, parse, format, resolve, Url, domainToASCII, domainToUnicode }; -export { - URL, - URLSearchParams, - Url, - url_default as default, - domainToASCII, - domainToUnicode, - fileURLToPath, - format, - parse, - pathToFileURL, - resolve -}; diff --git a/crates/pocket-pi/js/node/util.js b/crates/pocket-pi/js/node/util.js deleted file mode 100644 index adfdf87..0000000 --- a/crates/pocket-pi/js/node/util.js +++ /dev/null @@ -1,115 +0,0 @@ -function inherits(ctor, superCtor) { - ctor.super_ = superCtor; - ctor.prototype = Object.create(superCtor.prototype, { - constructor: { value: ctor, enumerable: false, writable: true, configurable: true } - }); -} -function format(fmt, ...args) { - if (typeof fmt !== "string") return [fmt, ...args].map(inspect).join(" "); - let i = 0; - let out = fmt.replace(/%[sdifjoO%]/g, (m) => { - if (m === "%%") return "%"; - if (i >= args.length) return m; - const a = args[i++]; - switch (m) { - case "%s": - return String(a); - case "%d": - case "%i": - return String(parseInt(a, 10)); - case "%f": - return String(parseFloat(a)); - case "%j": - try { - return JSON.stringify(a); - } catch { - return "[Circular]"; - } - default: - return inspect(a); - } - }); - for (; i < args.length; i++) out += " " + (typeof args[i] === "string" ? args[i] : inspect(args[i])); - return out; -} -function inspect(obj) { - if (typeof obj === "string") return obj; - try { - return JSON.stringify(obj); - } catch { - return String(obj); - } -} -inspect.custom = Symbol.for("nodejs.util.inspect.custom"); -function promisify(fn) { - return function(...args) { - return new Promise((resolve, reject) => { - fn.call(this, ...args, (err, ...rest) => err ? reject(err) : resolve(rest.length > 1 ? rest : rest[0])); - }); - }; -} -function callbackify(fn) { - return function(...args) { - const cb = args.pop(); - fn.apply(this, args).then((v) => cb(null, v), (e) => cb(e)); - }; -} -function deprecate(fn) { - return fn; -} -function debuglog(section, cb) { - const env = globalThis.process && globalThis.process.env && globalThis.process.env.NODE_DEBUG || ""; - const on = env.split(/[\s,]+/).includes(section); - const fn = on ? (...args) => { - try { - console.error(`${section}:`, format(...args)); - } catch { - } - } : () => { - }; - if (typeof cb === "function") cb(fn); - return fn; -} -const debug = debuglog; -function inspect2() { -} -const isDeepStrictEqual = (a, b) => { - try { - return JSON.stringify(a) === JSON.stringify(b); - } catch { - return a === b; - } -}; -function stripVTControlCharacters(s) { - return String(s).replace(/\x1b\[[0-9;]*m/g, ""); -} -const _extend = Object.assign; -const types = { - isPromise: (v) => v && typeof v.then === "function", - isDate: (v) => v instanceof Date, - isRegExp: (v) => v instanceof RegExp, - isArrayBuffer: (v) => v instanceof ArrayBuffer, - isTypedArray: (v) => ArrayBuffer.isView(v) && !(v instanceof DataView), - isAsyncFunction: (v) => v && v.constructor && v.constructor.name === "AsyncFunction" -}; -const TextEncoder = globalThis.TextEncoder; -const TextDecoder = globalThis.TextDecoder; -var util_default = { inherits, format, inspect, promisify, callbackify, deprecate, debuglog, debug, isDeepStrictEqual, stripVTControlCharacters, _extend, types, TextEncoder, TextDecoder }; -export { - TextDecoder, - TextEncoder, - _extend, - callbackify, - debug, - debuglog, - util_default as default, - deprecate, - format, - inherits, - inspect, - inspect2, - isDeepStrictEqual, - promisify, - stripVTControlCharacters, - types -}; diff --git a/crates/pocket-pi/js/node/v8.js b/crates/pocket-pi/js/node/v8.js deleted file mode 100644 index 70b6ea6..0000000 --- a/crates/pocket-pi/js/node/v8.js +++ /dev/null @@ -1,4 +0,0 @@ -var v8_default = {}; -export { - v8_default as default -}; diff --git a/crates/pocket-pi/js/node/vm.js b/crates/pocket-pi/js/node/vm.js deleted file mode 100644 index 5718dde..0000000 --- a/crates/pocket-pi/js/node/vm.js +++ /dev/null @@ -1,4 +0,0 @@ -var vm_default = {}; -export { - vm_default as default -}; diff --git a/crates/pocket-pi/js/node/worker_threads.js b/crates/pocket-pi/js/node/worker_threads.js deleted file mode 100644 index 280b75a..0000000 --- a/crates/pocket-pi/js/node/worker_threads.js +++ /dev/null @@ -1,88 +0,0 @@ -class Worker { - constructor() { - throw new Error("worker_threads.Worker is not supported in Pocket Pi (single-threaded)"); - } -} -const isMainThread = true; -const parentPort = null; -const threadId = 0; -const workerData = null; -class MessageChannel { - constructor() { - this.port1 = new MessagePort(); - this.port2 = new MessagePort(); - } -} -class MessagePort { - postMessage() { - } - on() { - return this; - } - once() { - return this; - } - close() { - } - ref() { - return this; - } - unref() { - return this; - } - start() { - } -} -const BroadcastChannel = class BroadcastChannel2 { - postMessage() { - } - close() { - } - on() { - return this; - } -}; -function markAsUntransferable() { -} -function moveMessagePortToContext() { - throw new Error("not supported"); -} -function receiveMessageOnPort() { - return void 0; -} -function setEnvironmentData() { -} -function getEnvironmentData() { - return void 0; -} -var worker_threads_default = { - Worker, - isMainThread, - parentPort, - threadId, - workerData, - MessageChannel, - MessagePort, - BroadcastChannel, - markAsUntransferable, - moveMessagePortToContext, - receiveMessageOnPort, - setEnvironmentData, - getEnvironmentData -}; -export { - BroadcastChannel, - MessageChannel, - MessagePort, - Worker, - worker_threads_default as default, - getEnvironmentData, - isMainThread, - markAsUntransferable, - moveMessagePortToContext, - parentPort, - receiveMessageOnPort, - setEnvironmentData, - threadId, - workerData -}; diff --git a/crates/pocket-pi/js/node/zlib.js b/crates/pocket-pi/js/node/zlib.js deleted file mode 100644 index 09eb0d3..0000000 --- a/crates/pocket-pi/js/node/zlib.js +++ /dev/null @@ -1,25 +0,0 @@ -const nope = (n) => () => { - throw new Error("zlib." + n + " not supported"); -}; -const gzip = nope("gzip"), gunzip = nope("gunzip"), deflate = nope("deflate"), inflate = nope("inflate"); -const gzipSync = nope("gzipSync"), gunzipSync = nope("gunzipSync"), deflateSync = nope("deflateSync"), inflateSync = nope("inflateSync"), brotliCompressSync = nope("brotliCompressSync"), brotliDecompressSync = nope("brotliDecompressSync"); -const constants = {}; -function createGzip() { - throw new Error("zlib streams not supported"); -} -var zlib_default = { gzip, gunzip, deflate, inflate, gzipSync, gunzipSync, deflateSync, inflateSync, brotliCompressSync, brotliDecompressSync, constants, createGzip }; -export { - brotliCompressSync, - brotliDecompressSync, - constants, - createGzip, - zlib_default as default, - deflate, - deflateSync, - gunzip, - gunzipSync, - gzip, - gzipSync, - inflate, - inflateSync -}; diff --git a/crates/pocket-pi/js/package.json b/crates/pocket-pi/js/package.json deleted file mode 100644 index dae55ac..0000000 --- a/crates/pocket-pi/js/package.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "name": "@earendil-works/pi-coding-agent", - "version": "0.81.1", - "bin": { "pi": "cli.js" } -} diff --git a/crates/pocket-pi/js/pi-full.bundle.js.gz b/crates/pocket-pi/js/pi-full.bundle.js.gz deleted file mode 100644 index 190e178..0000000 Binary files a/crates/pocket-pi/js/pi-full.bundle.js.gz and /dev/null differ diff --git a/crates/pocket-pi/js/pi-full/driver.js b/crates/pocket-pi/js/pi-full/driver.js deleted file mode 100644 index e4378fc..0000000 --- a/crates/pocket-pi/js/pi-full/driver.js +++ /dev/null @@ -1,107 +0,0 @@ -(function() { - const P = globalThis.PiFull; - if (!P) throw new Error("PiFull not loaded \u2014 run the bundle first"); - const MODEL = { - id: "gpt-5.6", - name: "GPT-5.6", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 4e5, - maxTokens: 8192 - }; - const CWD = "/pocket-pi"; - const AGENT_DIR = "/pocket-pi/.pi"; - globalThis.__piResult = ""; - globalThis.__piError = null; - globalThis.__piDone = false; - globalThis.__piBind = null; - globalThis.__piLog = []; - const takeText = (msg) => { - if (!msg) return; - const content = msg.content; - if (Array.isArray(content)) { - let t = ""; - for (const b of content) { - if (!b) continue; - if (b.type === "text" && typeof b.text === "string") t += b.text; - else if (b.type === "error") t += "[ERROR] " + String(b.error || b.text || ""); - } - if (t) globalThis.__piResult = t; - } else if (typeof content === "string") { - globalThis.__piResult = content; - } - }; - globalThis.__piRun = async function(opts) { - opts = typeof opts === "string" ? opts.trim().startsWith("{") ? JSON.parse(opts) : { prompt: opts } : opts || {}; - try { - const modelRuntime = await P.ModelRuntime.create({ modelsPath: null }); - if (globalThis.__OPENAI_KEY) await modelRuntime.setRuntimeApiKey("openai", globalThis.__OPENAI_KEY); - const settingsManager = P.SettingsManager.inMemory({}); - const sessionManager = opts.sessionDir ? opts.resume ? P.SessionManager.continueRecent(CWD, opts.sessionDir) : P.SessionManager.create(CWD, opts.sessionDir) : P.SessionManager.inMemory(); - const extensionFactories = []; - if (opts.extensionPath) { - const mod = await import(opts.extensionPath); - if (typeof mod.default === "function") extensionFactories.push(mod.default); - else throw new Error("extension has no default factory export: " + opts.extensionPath); - } - const resourceLoader = new P.DefaultResourceLoader({ - cwd: CWD, - agentDir: AGENT_DIR, - settingsManager, - noExtensions: true, - noSkills: true, - noPromptTemplates: true, - noThemes: true, - noContextFiles: true, - extensionFactories - }); - if (resourceLoader.reload) await resourceLoader.reload(); - const sessionOpts = { - model: MODEL, - modelRuntime, - settingsManager, - sessionManager, - resourceLoader, - cwd: CWD, - agentDir: AGENT_DIR, - thinkingLevel: "off" - }; - if (opts.tools) sessionOpts.tools = opts.tools; - else sessionOpts.noTools = "all"; - const { session } = await P.createAgentSession(sessionOpts); - try { - const runner = session._extensionRunner; - const regTools = runner && runner.getAllRegisteredTools ? runner.getAllRegisteredTools() : []; - globalThis.__piBind = { - hasAgentStart: !!(runner && runner.hasHandlers && runner.hasHandlers("agent_start")), - registeredTools: regTools.map((t) => t && (t.name || t.definition && t.definition.name)).filter(Boolean) - }; - } catch (e) { - globalThis.__piBind = { error: String(e) }; - } - session.subscribe((event) => { - try { - if (!event) return; - globalThis.__piLastEvent = event.type; - if (event.type === "message_update" || event.type === "message_end") { - const msg = event.message; - if (msg && msg.role !== "user") takeText(msg); - if (msg && msg.stopReason === "error" && msg.errorMessage) { - globalThis.__piError = String(msg.errorMessage); - } - } - } catch { - } - }); - if (opts.prompt) await session.prompt(opts.prompt); - globalThis.__piDone = true; - } catch (e) { - globalThis.__piError = String(e && e.stack || e); - globalThis.__piDone = true; - } - }; -})(); diff --git a/crates/pocket-pi/js/pi-full/ext-probe.js b/crates/pocket-pi/js/pi-full/ext-probe.js deleted file mode 100644 index 6a12a8a..0000000 --- a/crates/pocket-pi/js/pi-full/ext-probe.js +++ /dev/null @@ -1,23 +0,0 @@ -globalThis.__piExtResult = null; -globalThis.__piExtError = null; -globalThis.__piExtDone = false; -globalThis.__piLoadExtension = async function(extPath) { - try { - const P = globalThis.PiFull; - if (!P) throw new Error("PiFull not loaded \u2014 run the bundle first"); - const mod = await import(extPath); - const factory = mod.default; - if (typeof factory !== "function") throw new Error("extension has no default factory export"); - const runtime = P.createExtensionRuntime(); - const eventBus = P.createEventBus(); - const ext = await P.loadExtensionFromFactory(factory, "/pocket-pi", eventBus, runtime, extPath); - globalThis.__piExtResult = { - tools: [...ext.tools.keys()], - handlers: [...ext.handlers.keys()] - }; - } catch (e) { - globalThis.__piExtError = String(e && e.stack || e); - } finally { - globalThis.__piExtDone = true; - } -}; diff --git a/crates/pocket-pi/js/pi-full/host.js b/crates/pocket-pi/js/pi-full/host.js deleted file mode 100644 index b20d507..0000000 --- a/crates/pocket-pi/js/pi-full/host.js +++ /dev/null @@ -1,173 +0,0 @@ -const P = globalThis.PiFull; -const CWD = "/pocket-pi"; -const AGENT_DIR = "/pocket-pi/.pi"; -const state = { session: null }; -const emit = (o) => globalThis.host.emit(JSON.stringify(o)); -function buildModel(cfg, provider) { - const base = { - id: cfg.model, - name: cfg.model, - provider, - reasoning: false, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 4e5, - maxTokens: cfg.maxTokens || 4096 - }; - if (provider === "anthropic") { - return { ...base, api: "anthropic-messages", baseUrl: "https://api.anthropic.com" }; - } - return { ...base, api: "openai-responses", baseUrl: "https://api.openai.com/v1" }; -} -function nativeToolsExtension(cfg) { - return (pi) => { - for (const t of cfg.tools || []) { - pi.registerTool({ - name: t.name, - description: t.description || "", - parameters: t.parameters || { type: "object", properties: {} }, - execute: async (_id, input) => { - const res = JSON.parse(globalThis.host.tool(t.name, JSON.stringify(input || {}))); - const content = []; - if (res.text) content.push({ type: "text", text: res.text }); - if (res.image_base64) { - content.push({ type: "image", data: res.image_base64, mimeType: res.mime_type || "image/jpeg" }); - } - if (content.length === 0) content.push({ type: "text", text: "" }); - return { content, details: res.terminate ? { terminate: true } : void 0 }; - } - }); - } - }; -} -function textOf(msg) { - const c = msg && msg.content; - if (typeof c === "string") return c; - if (Array.isArray(c)) return c.filter((b) => b && b.type === "text").map((b) => b.text).join(""); - return ""; -} -async function boot(configJson) { - globalThis.__ppBooted = false; - try { - const cfg = JSON.parse(configJson); - let provider = cfg.provider || (String(cfg.model || "").startsWith("gpt") ? "openai" : "anthropic"); - let model; - let nativeProvider; - if (cfg.scripted) { - const modelId = cfg.model || "offline"; - const faux = P.fauxProvider({ - api: "pocket-pi-offline", - provider: "offline", - models: [{ id: modelId, name: "Pocket Pi Offline" }] - }); - faux.setResponses( - (cfg.scripted.steps || []).map( - (step) => P.fauxAssistantMessage(typeof step === "string" ? step : String(step.text || "")) - ) - ); - nativeProvider = faux.provider; - provider = "offline"; - model = faux.getModel(modelId); - if (!model) throw new Error(`offline model not found: ${modelId}`); - } else { - model = buildModel(cfg, provider); - } - const modelRuntime = await P.ModelRuntime.create({ modelsPath: null }); - if (nativeProvider) modelRuntime.registerNativeProvider(nativeProvider); - if (cfg.apiKey) await modelRuntime.setRuntimeApiKey(provider, cfg.apiKey); - const settingsManager = P.SettingsManager.inMemory({}); - const sessionManager = P.SessionManager.inMemory(); - const resourceLoader = new P.DefaultResourceLoader({ - cwd: CWD, - agentDir: AGENT_DIR, - settingsManager, - noExtensions: true, - noSkills: true, - noPromptTemplates: true, - noThemes: true, - noContextFiles: true, - extensionFactories: [nativeToolsExtension(cfg)] - }); - if (resourceLoader.reload) await resourceLoader.reload(); - const { session } = await P.createAgentSession({ - model, - modelRuntime, - settingsManager, - sessionManager, - resourceLoader, - cwd: CWD, - agentDir: AGENT_DIR, - thinkingLevel: "off", - tools: (cfg.tools || []).map((t) => t.name) - }); - if (cfg.systemPrompt) { - try { - session._baseSystemPrompt = cfg.systemPrompt; - session.agent.state.systemPrompt = cfg.systemPrompt; - } catch { - } - } - let lastText = ""; - session.subscribe((event) => { - try { - switch (event.type) { - case "agent_start": - lastText = ""; - emit({ kind: "start" }); - break; - case "tool_execution_start": - emit({ kind: "tool_start", name: event.toolName }); - break; - case "message_update": - case "message_end": { - const msg = event.message; - if (msg && msg.role !== "user") { - const text = textOf(msg); - if (text && text.length > lastText.length && text.startsWith(lastText)) { - emit({ kind: "text", delta: text.slice(lastText.length) }); - lastText = text; - } else if (text && text !== lastText) { - emit({ kind: "assistant_text", text }); - lastText = text; - } - if (msg.stopReason === "error" && msg.errorMessage) { - emit({ kind: "error", message: msg.errorMessage }); - } - } - break; - } - case "agent_settled": - emit({ kind: "end" }); - break; - } - } catch { - } - }); - state.session = session; - } catch (e) { - emit({ kind: "error", message: String(e?.stack || e) }); - } finally { - globalThis.__ppBooted = true; - } -} -async function prompt(text) { - const s = state.session; - if (!s) { - emit({ kind: "error", message: "prompt before boot completed" }); - emit({ kind: "end" }); - return; - } - try { - await s.prompt(text); - } catch (e) { - emit({ kind: "error", message: String(e?.stack || e) }); - emit({ kind: "end" }); - } -} -function abort() { - try { - state.session?.abort?.(); - } catch { - } -} -globalThis.PocketPi = { boot, prompt, abort }; diff --git a/crates/pocket-pi/js/pi-full/persist-probe.js b/crates/pocket-pi/js/pi-full/persist-probe.js deleted file mode 100644 index 009af34..0000000 --- a/crates/pocket-pi/js/pi-full/persist-probe.js +++ /dev/null @@ -1,29 +0,0 @@ -globalThis.__piPersistResult = null; -globalThis.__piPersistError = null; -globalThis.__piPersist = function(sessionDir) { - try { - const P = globalThis.PiFull; - if (!P) throw new Error("PiFull not loaded \u2014 run the bundle first"); - const CWD = "/pocket-pi"; - const sm1 = P.SessionManager.create(CWD, sessionDir); - sm1.appendMessage({ role: "user", content: [{ type: "text", text: "remember the number 42" }] }); - sm1.appendMessage({ role: "assistant", content: [{ type: "text", text: "noted: 42" }] }); - const wrote = sm1.buildSessionContext().messages.length; - const sessionId = sm1.getSessionId(); - const sm2 = P.SessionManager.continueRecent(CWD, sessionDir); - const ctx = sm2.buildSessionContext(); - const texts = ctx.messages.map((m) => { - const content = m.content; - return Array.isArray(content) ? content.map((c) => c && c.text || "").join("") : String(content); - }); - globalThis.__piPersistResult = { - sessionId, - wrote, - resumedCount: ctx.messages.length, - resumedId: sm2.getSessionId(), - texts - }; - } catch (e) { - globalThis.__piPersistError = String(e && e.stack || e); - } -}; diff --git a/crates/pocket-pi/js/prelude.js b/crates/pocket-pi/js/prelude.js deleted file mode 100644 index f9e2876..0000000 --- a/crates/pocket-pi/js/prelude.js +++ /dev/null @@ -1,100 +0,0 @@ -(function() { - "use strict"; - if (typeof globalThis.global === "undefined") globalThis.global = globalThis; - if (typeof globalThis.setImmediate !== "function") - globalThis.setImmediate = (fn, ...a) => globalThis.setTimeout(fn, 0, ...a); - if (typeof globalThis.clearImmediate !== "function") - globalThis.clearImmediate = (id) => globalThis.clearTimeout(id); - const timers = /* @__PURE__ */ new Map(); - let nextTimer = 1; - globalThis.setTimeout = function(fn, delay, ...args) { - const id = nextTimer++; - timers.set(id, { due: Date.now() + (delay || 0), fn, args }); - return id; - }; - globalThis.clearTimeout = function(id) { - timers.delete(id); - }; - globalThis.setInterval = function() { - throw new Error("setInterval is not supported in Pocket Pi"); - }; - globalThis.clearInterval = function() { - }; - globalThis.__catpiTimers = function() { - if (timers.size === 0) return; - const now = Date.now(); - for (const [id, t] of timers) { - if (t.due <= now) { - timers.delete(id); - try { - t.fn(...t.args); - } catch (e) { - if (globalThis.host && host.emit) - host.emit(JSON.stringify({ kind: "error", message: "timer: " + String(e) })); - } - } - } - }; - if (typeof globalThis.queueMicrotask !== "function") { - globalThis.queueMicrotask = function(fn) { - Promise.resolve().then(fn); - }; - } - if (typeof globalThis.structuredClone !== "function") { - globalThis.structuredClone = function(v) { - return v === void 0 ? void 0 : JSON.parse(JSON.stringify(v)); - }; - } - if (typeof globalThis.AbortController !== "function") { - class PPAbortSignal { - constructor() { - this.aborted = false; - this.reason = void 0; - this._listeners = []; - } - addEventListener(type, cb) { - if (type === "abort") this._listeners.push(cb); - } - removeEventListener(type, cb) { - if (type === "abort") this._listeners = this._listeners.filter((l) => l !== cb); - } - _fire() { - if (this.aborted) return; - this.aborted = true; - for (const l of this._listeners.slice()) { - try { - l({ type: "abort" }); - } catch { - } - } - } - throwIfAborted() { - if (this.aborted) throw this.reason || new Error("Aborted"); - } - } - globalThis.AbortSignal = PPAbortSignal; - globalThis.AbortController = class { - constructor() { - this.signal = new PPAbortSignal(); - } - abort(reason) { - this.signal.reason = reason || new Error("Aborted"); - this.signal._fire(); - } - }; - } - if (typeof globalThis.crypto !== "object" || !globalThis.crypto) globalThis.crypto = {}; - if (typeof globalThis.crypto.randomUUID !== "function") { - globalThis.crypto.randomUUID = function() { - if (globalThis.host && host.uuid) return host.uuid(); - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function(c) { - const r = (Date.now() + Math.floor(Math.random() * 1e9)) % 16; - const v = c === "x" ? r : r & 3 | 8; - return v.toString(16); - }); - }; - } - if (typeof globalThis.process !== "object" || !globalThis.process) { - globalThis.process = { env: {}, platform: "pocket-pi", versions: {} }; - } -})(); diff --git a/crates/pocket-pi/js/web-globals.js b/crates/pocket-pi/js/web-globals.js deleted file mode 100644 index 86cb34d..0000000 --- a/crates/pocket-pi/js/web-globals.js +++ /dev/null @@ -1,600 +0,0 @@ -(function() { - "use strict"; - const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - if (typeof globalThis.Intl !== "object" || !globalThis.Intl) { - globalThis.Intl = { - Segmenter: class { - constructor() { - } - segment(str) { - const chars = [...String(str)]; - return { - [Symbol.iterator]() { - let i = 0; - return { - next() { - return i < chars.length ? { value: { segment: chars[i], index: i++, input: str }, done: false } : { value: void 0, done: true }; - } - }; - } - }; - } - }, - NumberFormat: class { - constructor() { - } - format(n) { - return String(n); - } - formatToParts() { - return []; - } - }, - DateTimeFormat: class { - constructor() { - } - format(d) { - return String(d); - } - formatToParts() { - return []; - } - }, - Collator: class { - constructor() { - } - compare(a, b) { - return a < b ? -1 : a > b ? 1 : 0; - } - }, - getCanonicalLocales: (l) => (Array.isArray(l) ? l : [l]).filter(Boolean) - }; - } - if (typeof globalThis.Blob === "undefined") { - globalThis.Blob = class Blob { - constructor(parts = [], opts = {}) { - this._parts = parts; - this.type = opts && opts.type || ""; - let size = 0; - for (const p of parts) { - if (typeof p === "string") size += p.length; - else if (p && p.byteLength != null) size += p.byteLength; - else if (p && p.size != null) size += p.size; - } - this.size = size; - } - async text() { - return this._parts.map((p) => typeof p === "string" ? p : "").join(""); - } - async arrayBuffer() { - return new globalThis.TextEncoder().encode(await this.text()).buffer; - } - slice() { - return new globalThis.Blob(this._parts, { type: this.type }); - } - }; - } - if (typeof globalThis.File === "undefined") { - globalThis.File = class File extends globalThis.Blob { - constructor(parts, name, opts = {}) { - super(parts, opts); - this.name = String(name); - this.lastModified = 0; - } - }; - } - if (typeof globalThis.FormData === "undefined") { - globalThis.FormData = class FormData { - constructor() { - this._entries = []; - } - append(k, v, filename) { - this._entries.push([String(k), v, filename]); - } - set(k, v) { - this._entries = this._entries.filter((e) => e[0] !== String(k)); - this._entries.push([String(k), v]); - } - get(k) { - const e = this._entries.find((e2) => e2[0] === String(k)); - return e ? e[1] : null; - } - getAll(k) { - return this._entries.filter((e) => e[0] === String(k)).map((e) => e[1]); - } - has(k) { - return this._entries.some((e) => e[0] === String(k)); - } - delete(k) { - this._entries = this._entries.filter((e) => e[0] !== String(k)); - } - forEach(cb, thisArg) { - for (const [k, v] of this._entries) cb.call(thisArg, v, k, this); - } - *entries() { - for (const e of this._entries) yield [e[0], e[1]]; - } - *keys() { - for (const e of this._entries) yield e[0]; - } - *values() { - for (const e of this._entries) yield e[1]; - } - [Symbol.iterator]() { - return this.entries(); - } - }; - } - if (typeof globalThis.EventTarget !== "function") { - globalThis.EventTarget = class EventTarget { - constructor() { - this.__l = {}; - } - addEventListener(t, cb) { - (this.__l[t] ||= []).push(cb); - } - removeEventListener(t, cb) { - this.__l[t] = (this.__l[t] || []).filter((f) => f !== cb); - } - dispatchEvent(e) { - for (const cb of this.__l[e && e.type] || []) { - try { - cb(e); - } catch { - } - } - return true; - } - }; - } - if (typeof globalThis.Event !== "function") { - globalThis.Event = class Event { - constructor(type, init = {}) { - this.type = type; - this.bubbles = !!init.bubbles; - this.defaultPrevented = false; - } - preventDefault() { - this.defaultPrevented = true; - } - stopPropagation() { - } - stopImmediatePropagation() { - } - }; - } - if (typeof globalThis.CustomEvent !== "function") { - globalThis.CustomEvent = class CustomEvent extends globalThis.Event { - constructor(type, init = {}) { - super(type, init); - this.detail = init.detail; - } - }; - } - if (typeof globalThis.MessagePort !== "function") { - globalThis.MessagePort = class MessagePort extends globalThis.EventTarget { - postMessage() { - } - start() { - } - close() { - } - on() { - return this; - } - once() { - return this; - } - ref() { - return this; - } - unref() { - return this; - } - }; - } - if (typeof globalThis.MessageChannel !== "function") { - globalThis.MessageChannel = class MessageChannel { - constructor() { - this.port1 = new globalThis.MessagePort(); - this.port2 = new globalThis.MessagePort(); - } - }; - } - if (typeof globalThis.DOMException !== "function") { - globalThis.DOMException = class DOMException extends Error { - constructor(message, name) { - super(message); - this.name = name || "Error"; - } - }; - } - if (typeof globalThis.TextEncoder !== "function") { - globalThis.TextEncoder = class TextEncoder { - get encoding() { - return "utf-8"; - } - encode(str) { - str = String(str); - const out = []; - for (let i = 0; i < str.length; i++) { - let c = str.charCodeAt(i); - if (c < 128) out.push(c); - else if (c < 2048) out.push(192 | c >> 6, 128 | c & 63); - else if (c >= 55296 && c <= 56319) { - const c2 = str.charCodeAt(++i); - c = 65536 + ((c & 1023) << 10) + (c2 & 1023); - out.push(240 | c >> 18, 128 | c >> 12 & 63, 128 | c >> 6 & 63, 128 | c & 63); - } else out.push(224 | c >> 12, 128 | c >> 6 & 63, 128 | c & 63); - } - return new Uint8Array(out); - } - }; - } - if (typeof globalThis.TextDecoder !== "function") { - globalThis.TextDecoder = class TextDecoder { - constructor(label) { - this._enc = label || "utf-8"; - } - get encoding() { - return "utf-8"; - } - decode(input) { - if (!input) return ""; - const bytes = input instanceof Uint8Array ? input : new Uint8Array(input.buffer || input); - let out = "", i = 0; - while (i < bytes.length) { - let c = bytes[i++]; - if (c < 128) out += String.fromCharCode(c); - else if (c >= 192 && c < 224) out += String.fromCharCode((c & 31) << 6 | bytes[i++] & 63); - else if (c >= 224 && c < 240) out += String.fromCharCode((c & 15) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63); - else { - const cp = (c & 7) << 18 | (bytes[i++] & 63) << 12 | (bytes[i++] & 63) << 6 | bytes[i++] & 63; - const off = cp - 65536; - out += String.fromCharCode(55296 + (off >> 10), 56320 + (off & 1023)); - } - } - return out; - } - }; - } - if (typeof globalThis.btoa !== "function") { - globalThis.btoa = function(bin) { - let out = ""; - for (let i = 0; i < bin.length; i += 3) { - const a = bin.charCodeAt(i), b = bin.charCodeAt(i + 1), c = bin.charCodeAt(i + 2); - const n = a << 16 | (isNaN(b) ? 0 : b) << 8 | (isNaN(c) ? 0 : c); - out += B64[n >> 18 & 63] + B64[n >> 12 & 63] + (isNaN(b) ? "=" : B64[n >> 6 & 63]) + (isNaN(c) ? "=" : B64[n & 63]); - } - return out; - }; - } - if (typeof globalThis.atob !== "function") { - globalThis.atob = function(b64) { - b64 = String(b64).replace(/[^A-Za-z0-9+/]/g, ""); - let out = ""; - for (let i = 0; i < b64.length; i += 4) { - const n = B64.indexOf(b64[i]) << 18 | B64.indexOf(b64[i + 1]) << 12 | B64.indexOf(b64[i + 2] || "A") << 6 | B64.indexOf(b64[i + 3] || "A"); - out += String.fromCharCode(n >> 16 & 255); - if (b64[i + 2] && b64[i + 2] !== "=") out += String.fromCharCode(n >> 8 & 255); - if (b64[i + 3] && b64[i + 3] !== "=") out += String.fromCharCode(n & 255); - } - return out; - }; - } - function b64ToBytes(b64) { - const bin = globalThis.atob(b64); - const u = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i); - return u; - } - if (typeof globalThis.crypto !== "object" || !globalThis.crypto) globalThis.crypto = {}; - if (typeof globalThis.crypto.getRandomValues !== "function") { - globalThis.crypto.getRandomValues = function(arr) { - for (let i = 0; i < arr.length; i++) arr[i] = Math.floor(Math.random() * 256); - return arr; - }; - } - if (typeof globalThis.Headers !== "function") { - globalThis.Headers = class Headers { - constructor(init) { - this._m = /* @__PURE__ */ new Map(); - if (init) { - const entries = init instanceof Headers ? init.entries() : Array.isArray(init) ? init : Object.entries(init); - for (const [k, v] of entries) this.set(k, v); - } - } - set(k, v) { - this._m.set(String(k).toLowerCase(), String(v)); - } - append(k, v) { - const p = this._m.get(String(k).toLowerCase()); - this.set(k, p ? p + ", " + v : v); - } - get(k) { - const v = this._m.get(String(k).toLowerCase()); - return v === void 0 ? null : v; - } - has(k) { - return this._m.has(String(k).toLowerCase()); - } - delete(k) { - this._m.delete(String(k).toLowerCase()); - } - forEach(fn) { - this._m.forEach((v, k) => fn(v, k, this)); - } - entries() { - return this._m.entries(); - } - keys() { - return this._m.keys(); - } - values() { - return this._m.values(); - } - [Symbol.iterator]() { - return this._m.entries(); - } - }; - } - if (typeof globalThis.URLSearchParams !== "function") { - globalThis.URLSearchParams = class URLSearchParams { - constructor(init) { - this._p = []; - if (typeof init === "string") { - for (const pair of init.replace(/^\?/, "").split("&")) { - if (!pair) continue; - const i = pair.indexOf("="); - this._p.push(i < 0 ? [decodeURIComponent(pair), ""] : [decodeURIComponent(pair.slice(0, i)), decodeURIComponent(pair.slice(i + 1))]); - } - } else if (init) for (const [k, v] of Object.entries(init)) this._p.push([k, String(v)]); - } - get(k) { - const e = this._p.find((x) => x[0] === k); - return e ? e[1] : null; - } - set(k, v) { - const e = this._p.find((x) => x[0] === k); - if (e) e[1] = String(v); - else this._p.push([k, String(v)]); - } - append(k, v) { - this._p.push([k, String(v)]); - } - has(k) { - return this._p.some((x) => x[0] === k); - } - delete(k) { - this._p = this._p.filter((x) => x[0] !== k); - } - forEach(fn) { - for (const [k, v] of this._p) fn(v, k, this); - } - toString() { - return this._p.map(([k, v]) => encodeURIComponent(k) + "=" + encodeURIComponent(v)).join("&"); - } - [Symbol.iterator]() { - return this._p[Symbol.iterator](); - } - }; - } - if (typeof globalThis.URL !== "function") { - let originOf = function(u) { - const m = /^([a-zA-Z]+:\/\/[^/?#]*)/.exec(u); - return m ? m[1] : u; - }; - globalThis.URL = class URL { - constructor(url, base) { - let full = String(url); - if (base && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(full)) { - const b = String(base).replace(/\/+$/, ""); - full = full.startsWith("/") ? originOf(b) + full : b + "/" + full; - } - const m = /^([a-zA-Z][a-zA-Z0-9+.-]*:)\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/.exec(full); - if (!m) throw new TypeError("Invalid URL: " + full); - this.protocol = m[1]; - this.host = m[2]; - this.hostname = m[2].split(":")[0]; - this.port = m[2].split(":")[1] || ""; - this.pathname = m[3] || "/"; - this.search = m[4] || ""; - this.hash = m[5] || ""; - this.searchParams = new globalThis.URLSearchParams(this.search); - this.origin = this.protocol + "//" + this.host; - } - get href() { - return this.origin + this.pathname + (this.searchParams.toString() ? "?" + this.searchParams.toString() : "") + this.hash; - } - toString() { - return this.href; - } - }; - } - class PPReadableStream { - constructor() { - this._queue = []; - this._closed = false; - this._error = null; - this._waiters = []; - } - _enqueue(chunk) { - this._queue.push(chunk); - this._flush(); - } - _close() { - this._closed = true; - this._flush(); - } - _fail(e) { - this._error = e; - this._flush(); - } - _flush() { - while (this._waiters.length) { - if (this._queue.length) this._waiters.shift().resolve({ value: this._queue.shift(), done: false }); - else if (this._error) this._waiters.shift().reject(this._error); - else if (this._closed) this._waiters.shift().resolve({ value: void 0, done: true }); - else break; - } - } - getReader() { - const self = this; - return { - read() { - return new Promise((resolve, reject) => { - self._waiters.push({ resolve, reject }); - self._flush(); - }); - }, - releaseLock() { - }, - cancel() { - self._closed = true; - return Promise.resolve(); - } - }; - } - [Symbol.asyncIterator]() { - const reader = this.getReader(); - return { next: () => reader.read(), return: () => { - reader.cancel(); - return Promise.resolve({ done: true }); - }, [Symbol.asyncIterator]() { - return this; - } }; - } - } - globalThis.ReadableStream = globalThis.ReadableStream || PPReadableStream; - class Response { - constructor(body, init) { - init = init || {}; - this.status = init.status ?? 200; - this.statusText = init.statusText ?? ""; - this.ok = this.status >= 200 && this.status < 300; - this.headers = init.headers instanceof globalThis.Headers ? init.headers : new globalThis.Headers(init.headers || {}); - this.url = init.url || ""; - this.body = body || null; - this._bodyUsed = false; - } - get bodyUsed() { - return this._bodyUsed; - } - async _consume() { - this._bodyUsed = true; - if (!this.body) return new Uint8Array(0); - if (this.body instanceof Uint8Array) return this.body; - const reader = this.body.getReader(); - const parts = []; - let total = 0; - for (; ; ) { - const { value, done } = await reader.read(); - if (done) break; - parts.push(value); - total += value.length; - } - const out = new Uint8Array(total); - let off = 0; - for (const p of parts) { - out.set(p, off); - off += p.length; - } - return out; - } - async arrayBuffer() { - return (await this._consume()).buffer; - } - async text() { - return new TextDecoder().decode(await this._consume()); - } - async json() { - return JSON.parse(await this.text()); - } - clone() { - return new Response(this.body, { status: this.status, statusText: this.statusText, headers: this.headers, url: this.url }); - } - } - globalThis.Response = globalThis.Response || Response; - const fetchTurns = /* @__PURE__ */ new Map(); - globalThis.fetch = function(input, init) { - init = init || {}; - const url = typeof input === "string" ? input : input.url; - const headers = {}; - if (init.headers) { - const h = init.headers instanceof globalThis.Headers ? init.headers.entries() : Array.isArray(init.headers) ? init.headers : Object.entries(init.headers); - for (const [k, v] of h) headers[k] = String(v); - } - let body = init.body; - if (body && typeof body !== "string") { - if (body instanceof Uint8Array) body = new TextDecoder().decode(body); - else body = String(body); - } - const request = { url, method: init.method || "GET", headers, body, raw: true }; - return new Promise((resolve, reject) => { - let turnId; - try { - turnId = host.http.start(JSON.stringify(request)); - } catch (e) { - reject(new Error(String(e && e.message ? e.message : e))); - return; - } - const turn = { resolve, reject, stream: null, resolved: false, url }; - fetchTurns.set(turnId, turn); - if (init.signal) { - init.signal.addEventListener("abort", () => { - try { - host.http.cancel(turnId); - } catch { - } - if (!turn.resolved) reject(new Error("aborted")); - else if (turn.stream) turn.stream._fail(new Error("aborted")); - fetchTurns.delete(turnId); - }); - } - }); - }; - globalThis.__catpiFetchPump = function() { - if (fetchTurns.size === 0) return; - for (const [turnId, turn] of fetchTurns) { - let out; - try { - out = JSON.parse(host.http.drain(turnId)); - } catch (e) { - if (!turn.resolved) turn.reject(new Error(String(e))); - else if (turn.stream) turn.stream._fail(new Error(String(e))); - fetchTurns.delete(turnId); - continue; - } - for (const line of out.lines) { - let msg; - try { - msg = JSON.parse(line); - } catch { - continue; - } - if (msg.__meta) { - turn.stream = new PPReadableStream(); - const resp = new Response(turn.stream, { - status: msg.__meta.status, - statusText: msg.__meta.statusText, - headers: msg.__meta.headers, - url: turn.url - }); - turn.resolved = true; - turn.resolve(resp); - } else if (msg.__chunk != null && turn.stream) { - turn.stream._enqueue(b64ToBytes(msg.__chunk)); - } - } - if (out.error) { - if (!turn.resolved) turn.reject(new Error(out.error)); - else if (turn.stream) turn.stream._fail(new Error(out.error)); - fetchTurns.delete(turnId); - } else if (out.done) { - if (turn.stream) turn.stream._close(); - else if (!turn.resolved) turn.reject(new Error("no response")); - fetchTurns.delete(turnId); - } - } - }; -})(); diff --git a/crates/pocket-pi/src/http.rs b/crates/pocket-pi/src/http.rs deleted file mode 100644 index a863992..0000000 --- a/crates/pocket-pi/src/http.rs +++ /dev/null @@ -1,365 +0,0 @@ -//! The native HTTP bridge for Pocket Pi. -//! -//! QuickJS is single-threaded and can't do blocking TLS, so each streaming -//! request runs on its own OS thread. The thread reads the Anthropic SSE body -//! line by line and pushes each decoded `data:` JSON into a per-turn mailbox. -//! The JS side never sees bytes — only complete event payloads it drains once -//! per host frame. This is the PocketJS `svc` mailbox pattern applied to LLM -//! streaming: the frame scheduler is the pump that moves the agent forward. - -use std::collections::{HashMap, VecDeque}; -use std::io::Read; -use std::sync::atomic::{AtomicBool, AtomicU64, Ordering}; -use std::sync::{Arc, Mutex}; - -#[derive(Default)] -struct Turn { - lines: VecDeque, - done: bool, - error: Option, - cancel: Arc, -} - -#[derive(Default)] -struct Inner { - turns: HashMap, -} - -/// Shared, thread-safe registry of in-flight requests. -#[derive(Clone, Default)] -pub struct HttpHub { - inner: Arc>, - next_id: Arc, -} - -/// What the JS side sees each time it drains a turn. -struct Drained { - lines: Vec, - done: bool, - error: Option, -} - -impl HttpHub { - pub fn new() -> Self { - HttpHub { - inner: Arc::new(Mutex::new(Inner::default())), - next_id: Arc::new(AtomicU64::new(1)), - } - } - - /// Begin a request. Two shapes share this path: - /// - provider streaming: `{url, apiKey, auth, body}` → POST, SSE `data:` lines. - /// - WHATWG fetch: `{url, method, headers, body, raw:true}` → a meta line then - /// base64 body chunks (backing `Response`/`ReadableStream`). - pub fn start(&self, request_json: &str) -> Result { - let v: serde_json::Value = - serde_json::from_str(request_json).map_err(|e| format!("bad request json: {e}"))?; - let url = v - .get("url") - .and_then(|x| x.as_str()) - .ok_or("missing url")? - .to_string(); - let raw = v.get("raw").and_then(|x| x.as_bool()).unwrap_or(false); - let method = v - .get("method") - .and_then(|x| x.as_str()) - .map(|s| s.to_uppercase()) - .unwrap_or_else(|| "POST".into()); - let api_key = v - .get("apiKey") - .and_then(|x| x.as_str()) - .unwrap_or("") - .to_string(); - let auth = v - .get("auth") - .and_then(|x| x.as_str()) - .unwrap_or("x-api-key") - .to_string(); - // Body: fetch passes a string; providers pass a JSON object to serialize. - let body = match v.get("body") { - Some(serde_json::Value::String(s)) => Some(s.clone()), - Some(other) if !other.is_null() => Some(other.to_string()), - _ => None, - }; - let mut headers: Vec<(String, String)> = Vec::new(); - if let Some(serde_json::Value::Object(h)) = v.get("headers") { - for (k, val) in h { - if let Some(s) = val.as_str() { - headers.push((k.clone(), s.to_string())); - } - } - } - let req = Req { - url, - method, - headers, - body, - api_key, - auth, - raw, - }; - - let id = self.next_id.fetch_add(1, Ordering::SeqCst); - let cancel = Arc::new(AtomicBool::new(false)); - { - let mut inner = self.inner.lock().unwrap(); - inner.turns.insert( - id, - Turn { - cancel: cancel.clone(), - ..Default::default() - }, - ); - } - let hub = self.inner.clone(); - std::thread::Builder::new() - .name(format!("pocket-pi-http-{id}")) - .spawn(move || run_request(hub, id, req, cancel)) - .map_err(|e| format!("spawn failed: {e}"))?; - Ok(id) - } - - /// Take everything the turn has accumulated so far. Returns a JSON string - /// `{lines:[...], done, error}` (the JS side JSON.parses it). - pub fn drain(&self, id: u64) -> String { - let mut inner = self.inner.lock().unwrap(); - let out = match inner.turns.get_mut(&id) { - Some(turn) => Drained { - lines: turn.lines.drain(..).collect(), - done: turn.done, - error: turn.error.clone(), - }, - None => Drained { - lines: Vec::new(), - done: true, - error: Some("unknown turn".into()), - }, - }; - // Once terminal and drained, forget the turn. - if out.done { - if let Some(turn) = inner.turns.get(&id) { - if turn.lines.is_empty() { - inner.turns.remove(&id); - } - } - } - serde_json::json!({ - "lines": out.lines, - "done": out.done, - "error": out.error, - }) - .to_string() - } - - pub fn cancel(&self, id: u64) { - let inner = self.inner.lock().unwrap(); - if let Some(turn) = inner.turns.get(&id) { - turn.cancel.store(true, Ordering::SeqCst); - } - } -} - -fn push_line(hub: &Arc>, id: u64, line: String) { - if let Some(turn) = hub.lock().unwrap().turns.get_mut(&id) { - turn.lines.push_back(line); - } -} - -fn finish(hub: &Arc>, id: u64, error: Option) { - if let Some(turn) = hub.lock().unwrap().turns.get_mut(&id) { - turn.done = true; - if error.is_some() { - turn.error = error; - } - } -} - -struct Req { - url: String, - method: String, - headers: Vec<(String, String)>, - body: Option, - api_key: String, - auth: String, - raw: bool, -} - -fn run_request(hub: Arc>, id: u64, req: Req, cancel: Arc) { - let mut builder = ureq::AgentBuilder::new().timeout_connect(std::time::Duration::from_secs(20)); - // Respect a system proxy (Clash/mihomo, corporate egress, …) like curl does. - if let Some(proxy_url) = std::env::var("HTTPS_PROXY") - .or_else(|_| std::env::var("https_proxy")) - .or_else(|_| std::env::var("ALL_PROXY")) - .or_else(|_| std::env::var("all_proxy")) - .ok() - .filter(|s| !s.is_empty()) - { - if let Ok(proxy) = ureq::Proxy::new(&proxy_url) { - builder = builder.proxy(proxy); - } - } - let agent = builder.build(); - - let mut r = agent.request(&req.method, &req.url); - let has_ct = req - .headers - .iter() - .any(|(k, _)| k.eq_ignore_ascii_case("content-type")); - if req.body.is_some() && !has_ct { - r = r.set("content-type", "application/json"); - } - for (k, v) in &req.headers { - r = r.set(k, v); - } - // Provider auth convenience (skipped for a plain fetch that sets its own). - if !req.api_key.is_empty() { - if req.auth == "bearer" { - r = r.set("authorization", &format!("Bearer {}", req.api_key)); - } else if req.api_key.starts_with("sk-ant-oat") { - r = r - .set("anthropic-version", "2023-06-01") - .set("authorization", &format!("Bearer {}", req.api_key)) - .set("anthropic-beta", "oauth-2025-04-20"); - } else { - r = r - .set("anthropic-version", "2023-06-01") - .set("x-api-key", &req.api_key); - } - } - - let send = match &req.body { - Some(b) => r.send_string(b), - None => r.call(), - }; - let resp = match send { - Ok(r) => r, - // For raw fetch we forward non-2xx as a real Response (fetch doesn't - // throw on 4xx/5xx); for SSE providers, surface it as an error. - Err(ureq::Error::Status(code, r)) if req.raw => r_or_status(r, code), - Err(ureq::Error::Status(code, r)) => { - let msg = r - .into_string() - .unwrap_or_else(|_| String::from("(no body)")); - finish( - &hub, - id, - Some(format!("http {code}: {}", truncate(&msg, 400))), - ); - return; - } - Err(e) => { - finish(&hub, id, Some(format!("request error: {e}"))); - return; - } - }; - - if req.raw { - // Deliver a meta line (status/headers) then base64 body chunks. - let mut headers = serde_json::Map::new(); - for name in resp.headers_names() { - if let Some(val) = resp.header(&name) { - headers.insert( - name.to_lowercase(), - serde_json::Value::String(val.to_string()), - ); - } - } - let meta = serde_json::json!({ "__meta": { - "status": resp.status(), "statusText": resp.status_text(), "headers": headers, - }}); - push_line(&hub, id, meta.to_string()); - let mut reader = resp.into_reader(); - let mut chunk = [0u8; 8192]; - loop { - if cancel.load(Ordering::SeqCst) { - finish(&hub, id, Some("aborted".into())); - return; - } - match reader.read(&mut chunk) { - Ok(0) => break, - Ok(n) => { - let msg = serde_json::json!({ "__chunk": base64_encode(&chunk[..n]) }); - push_line(&hub, id, msg.to_string()); - } - Err(e) => { - finish(&hub, id, Some(format!("read error: {e}"))); - return; - } - } - } - finish(&hub, id, None); - return; - } - - // SSE mode: forward `data:` payloads line by line. - let mut reader = resp.into_reader(); - let mut buf: Vec = Vec::with_capacity(8192); - let mut chunk = [0u8; 4096]; - loop { - if cancel.load(Ordering::SeqCst) { - finish(&hub, id, Some("aborted".into())); - return; - } - let n = match reader.read(&mut chunk) { - Ok(0) => break, - Ok(n) => n, - Err(e) => { - finish(&hub, id, Some(format!("read error: {e}"))); - return; - } - }; - buf.extend_from_slice(&chunk[..n]); - while let Some(pos) = buf.iter().position(|&b| b == b'\n') { - let line: Vec = buf.drain(..=pos).collect(); - let line = String::from_utf8_lossy(&line); - let line = line.trim_end_matches(['\r', '\n']); - if let Some(rest) = line.strip_prefix("data:") { - let payload = rest.trim(); - if payload.is_empty() || payload == "[DONE]" { - continue; - } - if std::env::var("POCKET_PI_DEBUG_SSE").is_ok() { - eprintln!("SSE< {}", &payload[..payload.len().min(300)]); - } - push_line(&hub, id, payload.to_string()); - } - } - } - finish(&hub, id, None); -} - -/// ureq consumes the Response in the Status error; return it unchanged for the -/// raw path (fetch surfaces non-2xx as a normal Response). -fn r_or_status(resp: ureq::Response, _code: u16) -> ureq::Response { - resp -} - -fn base64_encode(bytes: &[u8]) -> String { - const A: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - let mut out = String::with_capacity(bytes.len().div_ceil(3) * 4); - for c in bytes.chunks(3) { - let b = [c[0], *c.get(1).unwrap_or(&0), *c.get(2).unwrap_or(&0)]; - let n = ((b[0] as u32) << 16) | ((b[1] as u32) << 8) | b[2] as u32; - out.push(A[(n >> 18 & 63) as usize] as char); - out.push(A[(n >> 12 & 63) as usize] as char); - out.push(if c.len() > 1 { - A[(n >> 6 & 63) as usize] as char - } else { - '=' - }); - out.push(if c.len() > 2 { - A[(n & 63) as usize] as char - } else { - '=' - }); - } - out -} - -fn truncate(s: &str, max: usize) -> String { - if s.len() <= max { - s.to_string() - } else { - format!("{}…", &s[..max]) - } -} diff --git a/crates/pocket-pi/src/lib.rs b/crates/pocket-pi/src/lib.rs deleted file mode 100644 index d2e02f5..0000000 --- a/crates/pocket-pi/src/lib.rs +++ /dev/null @@ -1,536 +0,0 @@ -//! # Pocket Pi -//! -//! A QuickJS runtime that runs the **whole, unmodified `pi-coding-agent`** — the -//! real agent, sessions, extensions, and tool suite — with **no Node and no -//! bun**, on a PocketJS-style **coalesced frame scheduler**. The full pi bundle -//! is embedded in the crate, so [`PiRuntime::new`] stands it up self-contained. -//! -//! The agent loop, LLM streaming, and message state are pi's own JS, evaluated in -//! one QuickJS realm. Everything Node-ish is provided natively: HTTPS + SSE -//! streaming runs on background threads and lands in a per-turn mailbox -//! ([`http`]), and the realm is driven one **frame** at a time. Native -//! capabilities (a host's own tools, like a screenshot grabber) bridge into pi as -//! tools via [`PiRuntime::register_tool`]. -//! -//! ## Why a frame scheduler for an agent -//! -//! [`PiRuntime::pump`] is one frame: deliver any streamed LLM events, fire due -//! timers, then drain the QuickJS microtask/job queue so the agent loop advances. -//! Because LLM latency (seconds) dwarfs a frame period, the host can pump as -//! slowly as **2 Hz** when idle and lose nothing — the whole runtime coalesces -//! to near-zero CPU between token bursts. That is the PocketJS "demand-render" -//! idea applied to an agent: do work only when there is work. -//! -//! ```no_run -//! use pocket_pi::{PiRuntime, HostEvent}; -//! let mut rt = PiRuntime::new().unwrap(); -//! rt.on_event(|ev: &HostEvent| println!("{}: {}", ev.kind, ev.raw)); -//! rt.boot(r#"{"model":"claude-opus-4-8","apiKey":"...","systemPrompt":"Be terse."}"#).unwrap(); -//! rt.prompt("Say hi in three words.").unwrap(); -//! for _ in 0..600 { rt.pump().unwrap(); if rt.is_idle() { break } std::thread::sleep(std::time::Duration::from_millis(50)); } -//! ``` - -mod http; -mod node; -mod transpile; - -use rquickjs::{CatchResultExt, Context, Ctx, Function, Object, Runtime}; -use std::cell::RefCell; -use std::collections::HashMap; -use std::rc::Rc; - -pub use http::HttpHub; - -const PRELUDE: &str = include_str!("../js/prelude.js"); -const WEB_GLOBALS: &str = include_str!("../js/web-globals.js"); - -/// The full, unmodified pi-coding-agent — the whole agent, sessions, extensions, -/// tools — bundled to one ES module and embedded gzip'd. This IS the runtime: -/// every `PiRuntime` loads it. Committed (~1.8 MB gz) so the crate builds with -/// only Rust, no Node. -const FULL_PI_GZ: &[u8] = include_bytes!("../js/pi-full.bundle.js.gz"); - -/// The host harness: reimplements the `PocketPi.boot/prompt` API on top of the -/// full pi bundle (native tools via `host.tool`, events via `host.emit`). -const HOST_HARNESS: &str = include_str!("../js/pi-full/host.js"); - -/// The embedded full-pi bundle source, decompressed. Lets a host ship one -/// self-contained binary — the whole unmodified pi is inside the executable. -pub fn embedded_full_pi_bundle() -> String { - use std::io::Read; - let mut d = flate2::read::GzDecoder::new(FULL_PI_GZ); - let mut s = String::new(); - d.read_to_string(&mut s) - .expect("gunzip embedded full-pi bundle"); - s -} - -/// One event surfaced from the agent to the host, already decoded from the -/// guest's compact JSON vocabulary (`start`, `text`, `thinking`, -/// `assistant_text`, `tool_start`, `tool_end`, `end`, `error`, `booted`). -#[derive(Debug, Clone)] -pub struct HostEvent { - pub kind: String, - pub raw: String, - pub value: serde_json::Value, -} - -/// What a host tool returns. Text is fed back to the model; an image (base64) -/// becomes an image tool-result block — this is how the cat's "look at the -/// screen" tool hands a screenshot to the agent. -#[derive(Debug, Clone, Default)] -pub struct ToolResult { - pub text: Option, - pub image_base64: Option, - pub mime_type: Option, - pub terminate: bool, -} - -impl ToolResult { - pub fn text(s: impl Into) -> Self { - ToolResult { - text: Some(s.into()), - ..Default::default() - } - } - fn to_json(&self) -> serde_json::Value { - let mut o = serde_json::Map::new(); - if let Some(t) = &self.text { - o.insert("text".into(), serde_json::Value::String(t.clone())); - } - if let Some(img) = &self.image_base64 { - o.insert("image".into(), serde_json::Value::String(img.clone())); - o.insert( - "mimeType".into(), - serde_json::Value::String( - self.mime_type - .clone() - .unwrap_or_else(|| "image/jpeg".into()), - ), - ); - } - o.insert("terminate".into(), serde_json::Value::Bool(self.terminate)); - serde_json::Value::Object(o) - } -} - -type ToolFn = Box ToolResult>; -/// Optional host callback for agent events, shared with the realm's closures. -type EventSink = Rc>>>; - -struct HostState { - tools: HashMap, - emitted: Vec, -} - -/// The runtime. Not `Send` — it owns a QuickJS realm and must be driven from a -/// single thread. Cross-thread work (HTTP) lives behind the [`HttpHub`]. -pub struct PiRuntime { - rt: Runtime, - ctx: Context, - state: Rc>, - on_event: EventSink, - active: Rc>, -} - -impl PiRuntime { - /// Build a realm, install the prelude shims and the `host` native namespace, - /// then evaluate the bundled pi agent core. - pub fn new() -> Result { - let rt = Runtime::new().map_err(|e| e.to_string())?; - let ctx = Context::full(&rt).map_err(|e| e.to_string())?; - - // Node-flavored module resolution + loading (relative, node_modules, - // node: builtins, on-the-fly TS transpile). - rt.set_loader(node::NodeResolver, node::NodeLoader); - - let state = Rc::new(RefCell::new(HostState { - tools: HashMap::new(), - emitted: Vec::new(), - })); - let on_event: EventSink = Rc::new(RefCell::new(None)); - let active = Rc::new(RefCell::new(false)); - let hub = HttpHub::new(); - - ctx.with(|ctx| -> Result<(), String> { - install_console(&ctx).map_err(|e| e.to_string())?; - install_host(&ctx, &state, &hub).map_err(|e| e.to_string())?; - Ok(()) - })?; - - // Prelude first (defines globals the bundle relies on), then the full pi. - let full_pi = embedded_full_pi_bundle(); - ctx.with(|ctx| -> Result<(), String> { - ctx.eval::<(), _>(PRELUDE.as_bytes()) - .catch(&ctx) - .map_err(|e| format!("prelude eval: {e}"))?; - // Node globals (process, Buffer, __node fs ops) before the bundle. - node::install_node(&ctx) - .catch(&ctx) - .map_err(|e| format!("install_node: {e}"))?; - // Web globals (fetch/Response/ReadableStream/Headers/URL/…). - ctx.eval::<(), _>(WEB_GLOBALS.as_bytes()) - .catch(&ctx) - .map_err(|e| format!("web-globals eval: {e}"))?; - // The full, unmodified pi-coding-agent (one ES module → globalThis.PiFull). - // Declare + set import.meta.url before evaluating, since this in-memory - // module bypasses the loader that normally sets it (pi derives __dirname - // from import.meta.url). - let declared = rquickjs::module::Module::declare( - ctx.clone(), - "pocket-pi:pi-full", - full_pi.as_bytes(), - ) - .catch(&ctx) - .map_err(|e| format!("pi-full declare: {e}"))?; - if let Ok(meta) = declared.meta() { - let _ = meta.set("url", "file:///pocket-pi/js/pi-full.bundle.js"); - } - let (_evaluated, promise) = declared - .eval() - .catch(&ctx) - .map_err(|e| format!("pi-full eval start: {e}"))?; - promise - .finish::<()>() - .catch(&ctx) - .map_err(|e| format!("pi-full eval: {e}"))?; - // Host harness: PocketPi.boot/prompt on top of pi (native tools + events). - ctx.eval::<(), _>(HOST_HARNESS.as_bytes()) - .catch(&ctx) - .map_err(|e| format!("host harness eval: {e}"))?; - Ok(()) - })?; - - let mut this = PiRuntime { - rt, - ctx, - state, - on_event, - active, - }; - this.drain_jobs(); - this.flush_events(); - Ok(this) - } - - /// Register the host-side callback for agent events. - pub fn on_event(&mut self, cb: impl FnMut(&HostEvent) + 'static) { - *self.on_event.borrow_mut() = Some(Box::new(cb)); - } - - /// Register a native tool the agent can call. `run` executes synchronously - /// during a pump; keep it fast (or stash work and answer on a later turn). - pub fn register_tool( - &mut self, - name: impl Into, - run: impl FnMut(serde_json::Value) -> ToolResult + 'static, - ) { - self.state - .borrow_mut() - .tools - .insert(name.into(), Box::new(run)); - } - - /// Stand up a pi session from a JSON config: `provider`, `model`, `apiKey`, - /// `systemPrompt`, `maxTokens`, and `tools:[{name,description,parameters}]` - /// (each `tool` bridges to a native closure registered via - /// [`register_tool`](Self::register_tool)). Blocks until the session is ready. - pub fn boot(&mut self, config_json: &str) -> Result<(), String> { - let cfg = config_json.to_string(); - self.ctx.with(|ctx| -> Result<(), String> { - let pp: Object = ctx - .globals() - .get("PocketPi") - .map_err(|e| format!("PocketPi missing: {e}"))?; - let boot: Function = pp.get("boot").map_err(|e| e.to_string())?; - boot.call::<_, ()>((cfg,)) - .catch(&ctx) - .map_err(|e| format!("boot: {e}"))?; - Ok(()) - })?; - // The harness stands up the pi session asynchronously (createAgentSession); - // drive the job queue until it signals ready. It's offline, so this settles - // in a few frames. - for _ in 0..500 { - self.pump()?; - if self.get_global_json("__ppBooted") == Some(serde_json::Value::Bool(true)) { - break; - } - } - Ok(()) - } - - /// Send a user prompt. Returns immediately; results arrive as events across - /// subsequent [`pump`](Self::pump) calls. - pub fn prompt(&mut self, text: &str) -> Result<(), String> { - *self.active.borrow_mut() = true; - let text = text.to_string(); - self.ctx.with(|ctx| -> Result<(), String> { - let pp: Object = ctx.globals().get("PocketPi").map_err(|e| e.to_string())?; - let f: Function = pp.get("prompt").map_err(|e| e.to_string())?; - f.call::<_, ()>((text,)) - .catch(&ctx) - .map_err(|e| format!("prompt: {e}"))?; - Ok(()) - })?; - self.pump() - } - - /// Import and evaluate an ES module by specifier (absolute path, or a - /// `node:` builtin), driving the Node resolver/loader — relative imports, - /// `node_modules` packages, and `.ts` transpile all work. Milestone toward - /// running unmodified pi-coding-agent. Returns after the module settles. - pub fn run_module(&mut self, specifier: &str) -> Result<(), String> { - let spec = specifier.to_string(); - self.ctx.with(|ctx| -> Result<(), String> { - let promise = rquickjs::module::Module::import(&ctx, spec) - .catch(&ctx) - .map_err(|e| format!("import: {e}"))?; - promise - .finish::() - .catch(&ctx) - .map_err(|e| format!("module eval: {e}"))?; - Ok(()) - })?; - self.drain_jobs(); - Ok(()) - } - - /// Evaluate a script in the realm (advanced/tests). - pub fn eval_script(&mut self, source: &str) -> Result<(), String> { - let src = source.to_string(); - self.ctx.with(|ctx| -> Result<(), String> { - ctx.eval::<(), _>(src.as_bytes()) - .catch(&ctx) - .map_err(|e| format!("eval: {e}"))?; - Ok(()) - })?; - self.drain_jobs(); - Ok(()) - } - - /// Read a global (JSON-serialized) — for tests/introspection. - pub fn get_global_json(&self, name: &str) -> Option { - self.ctx.with(|ctx| { - let v: rquickjs::Value = ctx.globals().get(name).ok()?; - let s = ctx.json_stringify(v).ok()??; - serde_json::from_str(&s.to_string().ok()?).ok() - }) - } - - /// Abort the in-flight turn, if any. - pub fn abort(&mut self) -> Result<(), String> { - self.ctx.with(|ctx| -> Result<(), String> { - let pp: Object = ctx.globals().get("PocketPi").map_err(|e| e.to_string())?; - let f: Function = pp.get("abort").map_err(|e| e.to_string())?; - f.call::<_, ()>(()).catch(&ctx).map_err(|e| e.to_string())?; - Ok(()) - }) - } - - /// One frame: fire due timers, deliver streamed LLM events, drain the job - /// queue so the agent loop advances, then flush host events to the callback. - pub fn pump(&mut self) -> Result<(), String> { - self.ctx.with(|ctx| -> Result<(), String> { - call_global_void(&ctx, "__catpiTimers")?; - call_global_void(&ctx, "__catpiPump")?; - call_global_void(&ctx, "__catpiFetchPump")?; - Ok(()) - })?; - self.drain_jobs(); - self.flush_events(); - Ok(()) - } - - /// True when no turn is in flight (the last `end`/`error` event has fired). - pub fn is_idle(&self) -> bool { - !*self.active.borrow() - } - - fn drain_jobs(&self) { - loop { - match self.rt.execute_pending_job() { - Ok(true) => continue, - Ok(false) => break, - Err(e) => { - log::warn!("pocket-pi: pending job threw: {e:?}"); - } - } - } - } - - fn flush_events(&mut self) { - let drained: Vec = { - let mut st = self.state.borrow_mut(); - if st.emitted.is_empty() { - return; - } - std::mem::take(&mut st.emitted) - }; - for raw in drained { - let value: serde_json::Value = - serde_json::from_str(&raw).unwrap_or(serde_json::Value::Null); - let kind = value - .get("kind") - .and_then(|v| v.as_str()) - .unwrap_or("") - .to_string(); - if kind == "end" || kind == "error" { - *self.active.borrow_mut() = false; - } - let ev = HostEvent { kind, raw, value }; - if let Some(cb) = self.on_event.borrow_mut().as_mut() { - cb(&ev); - } - } - } -} - -fn call_global_void(ctx: &Ctx, name: &str) -> Result<(), String> { - if let Ok(f) = ctx.globals().get::<_, Function>(name) { - f.call::<_, ()>(()).catch(ctx).map_err(|e| e.to_string())?; - } - Ok(()) -} - -/// Mount `globalThis.host` — the entire native capability surface the guest can -/// reach. Deliberately small: HTTP streaming, tool dispatch, event emit, uuid. -fn install_host(ctx: &Ctx, state: &Rc>, hub: &HttpHub) -> rquickjs::Result<()> { - let host = Object::new(ctx.clone())?; - - // host.http.{start,drain,cancel} - let http = Object::new(ctx.clone())?; - let h = hub.clone(); - http.set( - "start", - Function::new( - ctx.clone(), - move |ctx: Ctx, req: String| -> rquickjs::Result { - match h.start(&req) { - Ok(id) => Ok(id as f64), - Err(e) => Err(ctx.throw(rquickjs::String::from_str(ctx.clone(), &e)?.into())), - } - }, - )?, - )?; - let h = hub.clone(); - http.set( - "drain", - Function::new(ctx.clone(), move |id: f64| -> String { h.drain(id as u64) })?, - )?; - let h = hub.clone(); - http.set( - "cancel", - Function::new(ctx.clone(), move |id: f64| h.cancel(id as u64))?, - )?; - host.set("http", http)?; - - // host.tool(name, argsJson) -> resultJson (synchronous dispatch to Rust) - let st = state.clone(); - host.set( - "tool", - Function::new( - ctx.clone(), - move |name: String, args_json: String| -> String { - let args: serde_json::Value = - serde_json::from_str(&args_json).unwrap_or(serde_json::Value::Null); - let mut st = st.borrow_mut(); - let result = match st.tools.get_mut(&name) { - Some(f) => f(args), - None => ToolResult::text(format!("(no such tool: {name})")), - }; - result.to_json().to_string() - }, - )?, - )?; - - // host.emit(jsonLine) -> buffers an agent event for the host to flush - let st = state.clone(); - host.set( - "emit", - Function::new(ctx.clone(), move |line: String| { - st.borrow_mut().emitted.push(line); - })?, - )?; - - // host.uuid() -> string - host.set( - "uuid", - Function::new(ctx.clone(), move || -> String { simple_uuid() })?, - )?; - - // host.transpile(name, tsSource) -> jsSource (agent-authored TS plugins) - host.set( - "transpile", - Function::new( - ctx.clone(), - move |ctx: Ctx, name: String, source: String| -> rquickjs::Result { - transpile::transpile_ts(&name, &source).map_err(|e| { - ctx.throw(rquickjs::String::from_str(ctx.clone(), &e).unwrap().into()) - }) - }, - )?, - )?; - - ctx.globals().set("host", host)?; - Ok(()) -} - -fn simple_uuid() -> String { - // Not cryptographic; sufficient for tool-call ids in a PoC. Seeds from the - // system clock and address-space entropy so ids don't collide within a run. - use std::time::{SystemTime, UNIX_EPOCH}; - let nanos = SystemTime::now() - .duration_since(UNIX_EPOCH) - .map(|d| d.as_nanos()) - .unwrap_or(0); - let salt = &nanos as *const _ as usize; - let mut x = nanos ^ (salt as u128).wrapping_mul(0x9E3779B97F4A7C15); - let mut hex = String::with_capacity(36); - for i in 0..32 { - if i == 8 || i == 12 || i == 16 || i == 20 { - hex.push('-'); - } - x = x - .wrapping_mul(6364136223846793005) - .wrapping_add(1442695040888963407); - let nibble = ((x >> 64) & 0xF) as u8; - hex.push(char::from_digit(nibble as u32, 16).unwrap()); - } - hex -} - -/// `console.*` → the `log` crate, target "pocket-pi.guest". -fn install_console(ctx: &Ctx) -> rquickjs::Result<()> { - let console = Object::new(ctx.clone())?; - for level in ["log", "info", "debug", "warn", "error"] { - console.set( - level, - Function::new( - ctx.clone(), - move |args: rquickjs::function::Rest| { - let mut out = String::new(); - for (i, v) in args.iter().enumerate() { - if i > 0 { - out.push(' '); - } - if let Some(s) = v.as_string() { - out.push_str(&s.to_string().unwrap_or_default()); - } else if let Ok(s) = v.ctx().json_stringify(v.clone()) { - out.push_str( - &s.map(|s| s.to_string().unwrap_or_default()) - .unwrap_or_default(), - ); - } - } - log::info!(target: "pocket-pi.guest", "{out}"); - }, - )?, - )?; - } - ctx.globals().set("console", console)?; - Ok(()) -} - -#[cfg(test)] -mod tests; diff --git a/crates/pocket-pi/src/node/builtins.rs b/crates/pocket-pi/src/node/builtins.rs deleted file mode 100644 index c47d7c1..0000000 --- a/crates/pocket-pi/src/node/builtins.rs +++ /dev/null @@ -1,136 +0,0 @@ -//! The builtin-module registry — the **single source of truth** for the `node:*` -//! modules Pocket Pi ships. -//! -//! The resolver ([`super::resolve`]), the loader ([`super`]), and the synchronous -//! CommonJS `require` bridge all derive from this one list, so adding a builtin is -//! a single line in [`BUILTINS`]. In particular the `require` bridge's -//! `__builtinExports` map is **generated** from this list by -//! [`cjs_bootstrap_source`] — there is no second place to keep in sync (a unit -//! test enforces that invariant, which is exactly the failure mode this design -//! removes). - -/// A builtin module: its Node name (`fs`, `fs/promises`) and its embedded JS -/// source. `source` is compiled straight into the binary via `include_str!`, so -/// the runtime needs no filesystem to serve builtins. -pub struct Builtin { - pub name: &'static str, - pub source: &'static str, -} - -/// Register a builtin by Node name and `js/node/`. -macro_rules! builtin { - ($name:literal, $file:literal) => { - Builtin { - name: $name, - source: include_str!(concat!("../../js/node/", $file)), - } - }; -} - -/// Every builtin Pocket Pi ships. Add a row to expose a new `node:*` module — the -/// resolver, loader, and `require` bridge all pick it up automatically. -pub static BUILTINS: &[Builtin] = &[ - builtin!("path", "path.js"), - builtin!("os", "os.js"), - builtin!("events", "events.js"), - builtin!("util", "util.js"), - builtin!("buffer", "buffer.js"), - builtin!("process", "process.js"), - builtin!("fs", "fs.js"), - builtin!("fs/promises", "fs-promises.js"), - builtin!("child_process", "child_process.js"), - builtin!("crypto", "crypto.js"), - builtin!("url", "url.js"), - builtin!("module", "module.js"), - builtin!("stream", "stream.js"), - builtin!("stream/promises", "stream-promises.js"), - builtin!("string_decoder", "string_decoder.js"), - builtin!("readline", "readline.js"), - builtin!("perf_hooks", "perf_hooks.js"), - builtin!("tty", "tty.js"), - builtin!("http", "http.js"), - builtin!("https", "https.js"), - builtin!("net", "net.js"), - builtin!("tls", "tls.js"), - builtin!("zlib", "zlib.js"), - builtin!("dns", "dns.js"), - builtin!("querystring", "querystring.js"), - builtin!("assert", "assert.js"), - builtin!("timers", "timers.js"), - builtin!("worker_threads", "worker_threads.js"), - builtin!("v8", "v8.js"), - builtin!("vm", "vm.js"), - builtin!("constants", "constants.js"), - builtin!("async_hooks", "async_hooks.js"), - builtin!("diagnostics_channel", "diagnostics_channel.js"), - builtin!("console", "console.js"), -]; - -/// The static `require` implementation appended after the generated builtin map. -const CJS_RUNTIME: &str = include_str!("../../js/node/_cjs-runtime.js"); - -/// Source for a builtin by name (`node:` prefix optional). Tries the exact name -/// first — so `fs/promises` gets its own module — then the root segment, so a -/// subpath like `path/win32` falls back to the `path` module. -pub fn builtin_source(name: &str) -> Option<&'static str> { - let bare = name.strip_prefix("node:").unwrap_or(name); - if let Some(b) = BUILTINS.iter().find(|b| b.name == bare) { - return Some(b.source); - } - let root = bare.split('/').next().unwrap_or(bare); - BUILTINS.iter().find(|b| b.name == root).map(|b| b.source) -} - -/// Whether `name` names a builtin we can serve. -pub fn is_builtin(name: &str) -> bool { - builtin_source(name).is_some() -} - -/// Build the CJS bootstrap module: `import * as` every builtin's namespace, expose -/// them on `globalThis.__builtinExports`, then append the static `require` -/// implementation. Because the imports and the map are generated from [`BUILTINS`], -/// the registry is the only place the builtin list lives. -pub fn cjs_bootstrap_source() -> String { - let mut imports = String::new(); - let mut map = String::from( - "const __pick = (ns) => (ns && ns.default !== undefined ? ns.default : ns);\n\ - globalThis.__builtinExports = {\n", - ); - for (i, b) in BUILTINS.iter().enumerate() { - imports.push_str(&format!("import * as __b{i} from \"node:{}\";\n", b.name)); - map.push_str(&format!(" {:?}: __pick(__b{i}),\n", b.name)); - } - map.push_str("};\n"); - format!("{imports}{map}{CJS_RUNTIME}") -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn exact_and_root_lookup() { - assert!(builtin_source("fs/promises").is_some()); - assert!(builtin_source("node:fs/promises").is_some()); - assert!(builtin_source("path/win32").is_some()); // root fallback - assert!(builtin_source("nonexistent").is_none()); - } - - /// The single-source-of-truth invariant: the generated `require` bridge must - /// expose *every* builtin — this is the exact bug (a builtin registered but - /// missing from `__builtinExports`) that the generated map eliminates. - #[test] - fn bootstrap_covers_every_builtin() { - let src = cjs_bootstrap_source(); - for b in BUILTINS { - let key = format!("{:?}: __pick(", b.name); - assert!( - src.contains(&key), - "builtin {:?} missing from __builtinExports", - b.name - ); - } - // One import per builtin. - assert_eq!(src.matches("import * as __b").count(), BUILTINS.len()); - } -} diff --git a/crates/pocket-pi/src/node/mod.rs b/crates/pocket-pi/src/node/mod.rs deleted file mode 100644 index 7fa07ae..0000000 --- a/crates/pocket-pi/src/node/mod.rs +++ /dev/null @@ -1,86 +0,0 @@ -//! A minimal Node/CommonJS-flavored **module system + builtins** for QuickJS — -//! the layer that lets unmodified npm packages (ultimately `pi-coding-agent`) -//! `import`/`require` and run with no Node and no bun. -//! -//! The design is deliberately factored so it stays extensible: -//! - [`builtins`] is the **single source of truth** for `node:*` modules; the -//! resolver, loader, and CJS `require` bridge all derive from its one list. -//! - [`resolve`] implements Node resolution (relative, `node_modules`, -//! `exports`/`imports`, `node:` builtins) for both the ESM [`Resolver`] and the -//! native `require` op. -//! - [`transform`] owns every source rewrite (TS erasure, JSON, CJS→ESM, the ESM -//! cycle-breaking re-export rewrite) behind one entry point. -//! - [`ops`] mounts the native `__node`/`process` surface the JS shims call. -//! -//! `mod.rs` itself only wires these together: the [`NodeLoader`] and the -//! [`install_node`] bootstrap. - -mod builtins; -mod ops; -mod resolve; -mod transform; - -pub use resolve::NodeResolver; - -use rquickjs::loader::{ImportAttributes, Loader}; -use rquickjs::module::{Declared, Module}; -use rquickjs::{Ctx, Error, Result}; - -/// The rquickjs loader hook: serve a builtin from the registry, or read the file, -/// transform it per its extension, and declare it. -pub struct NodeLoader; - -impl Loader for NodeLoader { - fn load<'js>( - &mut self, - ctx: &Ctx<'js>, - name: &str, - _attrs: Option>, - ) -> Result> { - if std::env::var("POCKET_PI_DEBUG_MODULES").is_ok() { - eprintln!("LOAD {name}"); - } - if let Some(src) = builtins::builtin_source(name) { - let m = Module::declare(ctx.clone(), name, src)?; - set_import_meta(&m, name); - return Ok(m); - } - let source = std::fs::read_to_string(name) - .map_err(|e| Error::new_loading_message(name.to_string(), e.to_string()))?; - let js = transform::prepare_module_source(name, &source) - .map_err(|e| Error::new_loading_message(name.to_string(), e))?; - let m = Module::declare(ctx.clone(), name, js)?; - set_import_meta(&m, name); - Ok(m) - } -} - -/// Populate `import.meta.url` so code that derives `__filename`/`__dirname` from -/// it works. Builtins get a synthetic `file:///node/` url. -fn set_import_meta(module: &Module<'_, Declared>, name: &str) { - if let Ok(meta) = module.meta() { - let url = match name.strip_prefix("node:") { - Some(bare) => format!("file:///node/{bare}"), - None => format!("file://{name}"), - }; - let _ = meta.set("url", url); - } -} - -const NODE_BOOTSTRAP: &str = include_str!("../../js/node/_bootstrap.js"); - -/// Install the Node layer onto a realm: native ops + `process`, the runtime -/// bootstrap (Buffer global, `nextTick`), and the CJS `require` bridge (whose -/// `__builtinExports` map is generated from the builtin registry). Call once, -/// after the resolver/loader are set and the prelude has run. -pub fn install_node(ctx: &Ctx) -> Result<()> { - ops::install_ops(ctx)?; - Module::evaluate(ctx.clone(), "pocket-pi:node-bootstrap", NODE_BOOTSTRAP)?.finish::<()>()?; - Module::evaluate( - ctx.clone(), - "pocket-pi:cjs-bootstrap", - builtins::cjs_bootstrap_source().as_str(), - )? - .finish::<()>()?; - Ok(()) -} diff --git a/crates/pocket-pi/src/node/ops.rs b/crates/pocket-pi/src/node/ops.rs deleted file mode 100644 index 4a3032e..0000000 --- a/crates/pocket-pi/src/node/ops.rs +++ /dev/null @@ -1,234 +0,0 @@ -//! Native ops backing the Node builtins: the `__node` namespace (resolution, file -//! I/O, subprocess) and the `process` global. The JS builtins in `js/node/*.js` -//! are thin shims over these — anything that must touch the OS lives here. -//! -//! Each fs op returns a JSON string the JS shim parses, rather than an rquickjs -//! `Object`, so no `Object` lifetime is threaded through a closure (that fails -//! borrowck in rquickjs). - -use rquickjs::{Ctx, Function, Object}; -use std::path::Path; - -use super::resolve::resolve_spec; - -/// Mount `globalThis.__node` and `globalThis.process`. -pub fn install_ops(ctx: &Ctx) -> rquickjs::Result<()> { - install_node_ns(ctx)?; - install_process(ctx)?; - Ok(()) -} - -fn install_node_ns(ctx: &Ctx) -> rquickjs::Result<()> { - let node = Object::new(ctx.clone())?; - - node.set("cwd", Function::new(ctx.clone(), cwd)?)?; - node.set( - "homedir", - Function::new(ctx.clone(), || -> String { - std::env::var("HOME").unwrap_or_else(|_| "/".into()) - })?, - )?; - node.set( - "tmpdir", - Function::new(ctx.clone(), || -> String { - std::env::temp_dir().to_string_lossy().to_string() - })?, - )?; - node.set( - "hostname", - Function::new(ctx.clone(), || -> String { "localhost".into() })?, - )?; - - // Resolution + file read for the synchronous CJS `require`. - node.set( - "resolve", - Function::new(ctx.clone(), |from: String, spec: String| -> String { - match resolve_spec(&from, &spec) { - Some(r) if r.starts_with("node:") => { - serde_json::json!({ "builtin": r.strip_prefix("node:").unwrap() }).to_string() - } - Some(path) => serde_json::json!({ "path": path }).to_string(), - None => serde_json::json!({ "err": "not found" }).to_string(), - } - })?, - )?; - node.set( - "readText", - Function::new(ctx.clone(), |path: String| -> String { - std::fs::read_to_string(&path).unwrap_or_default() - })?, - )?; - - node.set("fs", make_fs(ctx)?)?; - node.set("spawnSync", Function::new(ctx.clone(), spawn_sync)?)?; - - ctx.globals().set("__node", node)?; - Ok(()) -} - -fn make_fs<'js>(ctx: &Ctx<'js>) -> rquickjs::Result> { - let fs = Object::new(ctx.clone())?; - fs.set( - "readFile", - Function::new(ctx.clone(), |path: String| -> String { - match std::fs::read(&path) { - Ok(bytes) => serde_json::json!({ "bytes": bytes }).to_string(), - Err(e) => serde_json::json!({ "err": e.to_string() }).to_string(), - } - })?, - )?; - fs.set( - "writeFile", - Function::new(ctx.clone(), |path: String, bytes: Vec| -> String { - match std::fs::write(&path, &bytes) { - Ok(()) => "{}".into(), - Err(e) => serde_json::json!({ "err": e.to_string() }).to_string(), - } - })?, - )?; - fs.set( - "exists", - Function::new(ctx.clone(), |path: String| -> bool { - Path::new(&path).exists() - })?, - )?; - fs.set( - "readdir", - Function::new(ctx.clone(), |path: String| -> String { - match std::fs::read_dir(&path) { - Ok(rd) => { - let names: Vec = rd - .filter_map(|e| e.ok().map(|e| e.file_name().to_string_lossy().to_string())) - .collect(); - serde_json::json!({ "entries": names }).to_string() - } - Err(e) => serde_json::json!({ "err": e.to_string() }).to_string(), - } - })?, - )?; - fs.set( - "mkdir", - Function::new(ctx.clone(), |path: String, recursive: bool| { - let _ = if recursive { - std::fs::create_dir_all(&path) - } else { - std::fs::create_dir(&path) - }; - })?, - )?; - fs.set( - "stat", - Function::new(ctx.clone(), |path: String| -> String { - match std::fs::metadata(&path) { - Ok(m) => serde_json::json!({ - "isFile": m.is_file(), "isDir": m.is_dir(), "size": m.len() as f64, - }) - .to_string(), - Err(e) => serde_json::json!({ "err": e.to_string() }).to_string(), - } - })?, - )?; - fs.set( - "realpath", - Function::new(ctx.clone(), |path: String| -> String { - match std::fs::canonicalize(&path) { - Ok(p) => serde_json::json!({ "path": p.to_string_lossy() }).to_string(), - Err(e) => serde_json::json!({ "err": e.to_string() }).to_string(), - } - })?, - )?; - fs.set( - "unlink", - Function::new(ctx.clone(), |path: String| { - let _ = std::fs::remove_file(&path); - })?, - )?; - Ok(fs) -} - -fn install_process(ctx: &Ctx) -> rquickjs::Result<()> { - let process = Object::new(ctx.clone())?; - - let env = Object::new(ctx.clone())?; - for (k, v) in std::env::vars() { - env.set(k, v)?; - } - process.set("env", env)?; - process.set("platform", std::env::consts::OS.replace("macos", "darwin"))?; - process.set( - "arch", - if std::env::consts::ARCH == "aarch64" { - "arm64" - } else { - "x64" - }, - )?; - process.set("cwd", Function::new(ctx.clone(), cwd)?)?; - process.set("version", "v22.0.0")?; - let versions = Object::new(ctx.clone())?; - versions.set("node", "22.0.0")?; - process.set("versions", versions)?; - process.set("argv", vec!["node".to_string(), "pocket-pi".to_string()])?; - let exe = std::env::current_exe() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|_| "/pocket-pi".into()); - process.set("execPath", exe.clone())?; - process.set("argv0", exe)?; - process.set("execArgv", Vec::::new())?; - process.set("pid", std::process::id() as f64)?; - process.set("ppid", 0)?; - process.set("exit", Function::new(ctx.clone(), |_code: Option| {})?)?; - - // Minimal stdout/stderr so TUI + logging code can write() headlessly. - for (name, is_err) in [("stdout", false), ("stderr", true)] { - let stream = Object::new(ctx.clone())?; - stream.set("isTTY", false)?; - stream.set("columns", 80)?; - stream.set("rows", 24)?; - stream.set( - "write", - Function::new(ctx.clone(), move |s: String| -> bool { - if is_err { - eprint!("{s}"); - } else { - print!("{s}"); - } - true - })?, - )?; - stream.set("on", Function::new(ctx.clone(), || {})?)?; - stream.set("end", Function::new(ctx.clone(), || {})?)?; - process.set(name, stream)?; - } - - ctx.globals().set("process", process)?; - Ok(()) -} - -fn cwd() -> String { - std::env::current_dir() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_else(|_| "/".into()) -} - -/// `spawnSync(cmd, argsJson, optsJson) -> JSON {stdout, stderr, status, error?}`. -fn spawn_sync(cmd: String, args_json: String, opts_json: String) -> String { - let args: Vec = serde_json::from_str(&args_json).unwrap_or_default(); - let opts: serde_json::Value = - serde_json::from_str(&opts_json).unwrap_or(serde_json::Value::Null); - let mut command = std::process::Command::new(&cmd); - command.args(&args); - if let Some(dir) = opts.get("cwd").and_then(|v| v.as_str()) { - command.current_dir(dir); - } - match command.output() { - Ok(o) => serde_json::json!({ - "stdout": String::from_utf8_lossy(&o.stdout), - "stderr": String::from_utf8_lossy(&o.stderr), - "status": o.status.code(), - }) - .to_string(), - Err(e) => serde_json::json!({ "error": e.to_string(), "status": serde_json::Value::Null }) - .to_string(), - } -} diff --git a/crates/pocket-pi/src/node/resolve.rs b/crates/pocket-pi/src/node/resolve.rs deleted file mode 100644 index 5a8c4df..0000000 --- a/crates/pocket-pi/src/node/resolve.rs +++ /dev/null @@ -1,285 +0,0 @@ -//! Node module resolution — the subset Pocket Pi needs: relative paths, the -//! `node_modules` walk, `exports`/`imports` maps (conditions + a single `*` -//! wildcard), and `node:` builtins. Shared by the rquickjs [`Resolver`] and the -//! native `require` op (via [`resolve_spec`]). - -use rquickjs::loader::{ImportAttributes, Resolver}; -use rquickjs::{Ctx, Error, Result}; -use std::path::{Path, PathBuf}; - -use super::builtins::is_builtin; - -/// The rquickjs resolver hook — a thin wrapper over [`resolve_spec`]. -pub struct NodeResolver; - -impl Resolver for NodeResolver { - fn resolve<'js>( - &mut self, - _ctx: &Ctx<'js>, - base: &str, - name: &str, - _attributes: Option>, - ) -> Result { - resolve_spec(base, name) - .ok_or_else(|| Error::new_resolving(base.to_string(), name.to_string())) - } -} - -/// Resolve `name` imported from `base` to a canonical module id: `node:X` for a -/// builtin, or an absolute file path. Shared by the ESM resolver and the CJS -/// `require` op so both agree on resolution. -pub fn resolve_spec(base: &str, name: &str) -> Option { - if is_builtin(name) { - let bare = name.strip_prefix("node:").unwrap_or(name); - return Some(format!("node:{bare}")); - } - if name.starts_with('#') { - return resolve_internal(base, name); - } - if name.starts_with("./") || name.starts_with("../") || name.starts_with('/') { - let base_dir = Path::new(base).parent().unwrap_or_else(|| Path::new("/")); - return probe(&normalize(&base_dir.join(name))); - } - resolve_bare(base, name) -} - -/// Probe a path for the file that actually exists, trying the Node extension and -/// index-file candidates in priority order. -fn probe(p: &Path) -> Option { - let s = p.to_string_lossy().to_string(); - const EXTS: &[&str] = &["", ".ts", ".tsx", ".mts", ".js", ".mjs", ".cjs", ".json"]; - const INDEX: &[&str] = &["/index.ts", "/index.js", "/index.mjs"]; - EXTS.iter() - .map(|ext| format!("{s}{ext}")) - .chain(INDEX.iter().map(|idx| format!("{s}{idx}"))) - .find(|c| Path::new(c).is_file()) -} - -/// Walk `node_modules` from `base` upward, resolving a bare specifier -/// (`pkg` or `@scope/pkg` with an optional subpath) in the first package found. -fn resolve_bare(base: &str, name: &str) -> Option { - let (pkg, subpath) = split_package(name); - let mut dir = Path::new(base).parent().map(|p| p.to_path_buf()); - while let Some(d) = dir { - let pkg_dir = d.join("node_modules").join(&pkg); - if pkg_dir.is_dir() { - return resolve_in_package(&pkg_dir, subpath.as_deref()); - } - dir = d.parent().map(|p| p.to_path_buf()); - } - None -} - -/// Resolve a `#internal` import via the nearest package.json's `imports` map. -fn resolve_internal(base: &str, name: &str) -> Option { - let mut dir = Path::new(base).parent(); - while let Some(d) = dir { - let pj = d.join("package.json"); - if pj.is_file() { - let json = read_json(&pj)?; - let imports = json.get("imports")?; - let obj = imports.as_object()?; - // Exact match first, then a single-`*` wildcard pattern. - if let Some(v) = obj.get(name) { - let target = resolve_conditions(v)?; - return probe(&normalize(&d.join(target))); - } - for (pat, target) in obj { - if let Some(cap) = wildcard_capture(pat, name) { - let tgt = resolve_conditions(target)?.replace('*', &cap); - return probe(&normalize(&d.join(tgt))); - } - } - return None; // the nearest package.json is the resolution boundary - } - dir = d.parent(); - } - None -} - -/// Resolve a specifier within a package dir, honoring the `exports` map (exact, -/// conditional, and single-`*` wildcard subpaths), else `module`/`main`, else a -/// literal probe. -fn resolve_in_package(pkg_dir: &Path, subpath: Option<&str>) -> Option { - if let Some(json) = read_json(&pkg_dir.join("package.json")) { - let key = match subpath { - None => ".".to_string(), - Some(s) => format!("./{s}"), - }; - if let Some(target) = resolve_export(&json, &key) { - return probe(&normalize(&pkg_dir.join(target))); - } - // No exports match: use module/main for the root, else a literal probe. - if subpath.is_none() { - let entry = json - .get("module") - .and_then(|v| v.as_str()) - .or_else(|| json.get("main").and_then(|v| v.as_str())) - .unwrap_or("index.js"); - return probe(&normalize(&pkg_dir.join(entry))); - } - } - probe(&normalize(&pkg_dir.join(subpath.unwrap_or("index.js")))) -} - -/// Look up `key` (`.` or `./sub`) in a package's `exports`, resolving conditions -/// and a single-`*` wildcard. Returns the target path relative to the package dir. -fn resolve_export(json: &serde_json::Value, key: &str) -> Option { - let exports = json.get("exports")?; - // `"exports": "./x.js"` — sugar for the root entry only. - if let Some(s) = exports.as_str() { - return if key == "." { - Some(s.to_string()) - } else { - None - }; - } - let obj = exports.as_object()?; - // Exact subpath wins over any wildcard (Node's precedence). - if let Some(v) = obj.get(key) { - return resolve_conditions(v); - } - for (pat, target) in obj { - if let Some(cap) = wildcard_capture(pat, key) { - return Some(resolve_conditions(target)?.replace('*', &cap)); - } - } - None -} - -/// Pick a target string out of a conditional-exports value. Strings resolve -/// directly; objects are probed in a fixed priority (ESM-leaning, with `default` -/// and `require` as fallbacks). Nested objects recurse. -fn resolve_conditions(value: &serde_json::Value) -> Option { - if let Some(s) = value.as_str() { - return Some(s.to_string()); - } - // Arrays: first resolvable entry wins (Node's fallback-array semantics). - if let Some(arr) = value.as_array() { - return arr.iter().find_map(resolve_conditions); - } - for cond in ["import", "module", "browser", "default", "node", "require"] { - if let Some(v) = value.get(cond) { - if let Some(s) = resolve_conditions(v) { - return Some(s); - } - } - } - None -} - -/// Match a subpath pattern containing at most one `*` against `key`, returning the -/// `*` capture. A pattern without `*` matches only its exact equal. -fn wildcard_capture(pattern: &str, key: &str) -> Option { - match pattern.find('*') { - None => (pattern == key).then(String::new), - Some(i) => { - let (pre, post) = (&pattern[..i], &pattern[i + 1..]); - if key.len() >= pre.len() + post.len() && key.starts_with(pre) && key.ends_with(post) { - Some(key[pre.len()..key.len() - post.len()].to_string()) - } else { - None - } - } - } -} - -/// Split a bare specifier into `(package, subpath)`, handling `@scope/pkg`. -fn split_package(name: &str) -> (String, Option) { - let parts: Vec<&str> = name - .splitn(if name.starts_with('@') { 3 } else { 2 }, '/') - .collect(); - if name.starts_with('@') && parts.len() == 3 { - ( - format!("{}/{}", parts[0], parts[1]), - Some(parts[2].to_string()), - ) - } else if name.starts_with('@') { - (name.to_string(), None) - } else if parts.len() == 2 { - (parts[0].to_string(), Some(parts[1].to_string())) - } else { - (name.to_string(), None) - } -} - -/// Collapse `.`/`..` components without touching the filesystem. -fn normalize(p: &Path) -> PathBuf { - let mut out = PathBuf::new(); - for comp in p.components() { - use std::path::Component::*; - match comp { - ParentDir => { - out.pop(); - } - CurDir => {} - other => out.push(other.as_os_str()), - } - } - out -} - -fn read_json(path: &Path) -> Option { - let text = std::fs::read_to_string(path).ok()?; - serde_json::from_str(&text).ok() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn splits_scoped_and_plain_packages() { - assert_eq!(split_package("react"), ("react".into(), None)); - assert_eq!( - split_package("react/jsx-runtime"), - ("react".into(), Some("jsx-runtime".into())) - ); - assert_eq!(split_package("@scope/pkg"), ("@scope/pkg".into(), None)); - assert_eq!( - split_package("@scope/pkg/sub"), - ("@scope/pkg".into(), Some("sub".into())) - ); - } - - #[test] - fn wildcard_matches_prefix_and_suffix() { - assert_eq!(wildcard_capture("./*", "./foo"), Some("foo".into())); - assert_eq!( - wildcard_capture("./features/*.js", "./features/x.js"), - Some("x".into()) - ); - assert_eq!(wildcard_capture("./features/*.js", "./other/x.js"), None); - assert_eq!(wildcard_capture("./exact", "./exact"), Some(String::new())); - assert_eq!(wildcard_capture("./exact", "./nope"), None); - } - - #[test] - fn exports_exact_beats_wildcard() { - let json: serde_json::Value = - serde_json::from_str(r#"{"exports":{"./a":"./exact.js","./*":"./src/*.js"}}"#).unwrap(); - assert_eq!(resolve_export(&json, "./a").as_deref(), Some("./exact.js")); - assert_eq!(resolve_export(&json, "./b").as_deref(), Some("./src/b.js")); - } - - #[test] - fn conditions_prefer_import_then_fall_back() { - let dual: serde_json::Value = - serde_json::from_str(r#"{"import":"./m.mjs","require":"./m.cjs"}"#).unwrap(); - assert_eq!(resolve_conditions(&dual).as_deref(), Some("./m.mjs")); - let only_req: serde_json::Value = serde_json::from_str(r#"{"require":"./m.cjs"}"#).unwrap(); - assert_eq!(resolve_conditions(&only_req).as_deref(), Some("./m.cjs")); - let nested: serde_json::Value = - serde_json::from_str(r#"{"node":{"import":"./n.mjs"}}"#).unwrap(); - assert_eq!(resolve_conditions(&nested).as_deref(), Some("./n.mjs")); - } - - #[test] - fn builtins_resolve_with_node_prefix() { - assert_eq!(resolve_spec("/x.js", "fs").as_deref(), Some("node:fs")); - assert_eq!( - resolve_spec("/x.js", "node:path").as_deref(), - Some("node:path") - ); - } -} diff --git a/crates/pocket-pi/src/node/transform.rs b/crates/pocket-pi/src/node/transform.rs deleted file mode 100644 index d13065a..0000000 --- a/crates/pocket-pi/src/node/transform.rs +++ /dev/null @@ -1,212 +0,0 @@ -//! Source transforms applied by the loader before a module is declared: -//! TypeScript type-erasure, JSON wrapping, CommonJS→ESM bridging, and an ESM -//! re-export rewrite that breaks QuickJS linker cycles. One entry point, -//! [`prepare_module_source`], picks the transform from the file extension (and, -//! for ambiguous `.js`, a CJS/ESM classification). - -use crate::transpile::transpile_ts; -use std::path::Path; - -/// Turn a raw module `source` (already known to be a non-builtin file at `name`) -/// into the JS the loader declares. Errors only if TypeScript transpile fails. -pub fn prepare_module_source(name: &str, source: &str) -> std::result::Result { - let js = if is_typescript(name) { - rewrite_reexports(&transpile_ts(name, source)?) - } else if name.ends_with(".json") { - format!("export default {source};") - } else if name.ends_with(".cjs") || (!name.ends_with(".mjs") && is_cjs(source)) { - // CommonJS: wrap as an ESM module so `import { x }` works, routing its own - // `require(...)` through the synchronous native require. - wrap_cjs(name, source) - } else { - rewrite_reexports(source) - }; - Ok(js) -} - -fn is_typescript(name: &str) -> bool { - name.ends_with(".ts") - || name.ends_with(".tsx") - || name.ends_with(".mts") - || name.ends_with(".cts") -} - -/// Heuristic CJS classification for ambiguous `.js`/extensionless files: any -/// top-level ESM statement marks it ESM; otherwise a `require`/`exports` marker -/// marks it CJS. Deliberately a scan, not a parse — the loader runs this on every -/// file including the multi-megabyte Path B bundle, and `.cjs`/`.mjs`/`.ts` are -/// classified by extension without reaching here. -fn is_cjs(src: &str) -> bool { - for l in src.lines() { - let t = l.trim_start(); - if t.starts_with("import ") - || t.starts_with("import{") - || t.starts_with("import *") - || t.starts_with("import*") - || t.starts_with("export ") - || t.starts_with("export{") - || t.starts_with("export*") - || t.starts_with("export default") - { - return false; - } - } - src.contains("require(") - || src.contains("module.exports") - || src.contains("exports.") - || src.contains("exports[") -} - -/// Wrap a CommonJS module as an ES module: run its body with `module`/`exports`/ -/// `require`, cache the result, and re-export `default` plus the named exports we -/// can detect (a lightweight cjs-module-lexer, [`cjs_named_exports`]). -fn wrap_cjs(name: &str, src: &str) -> String { - let dir = Path::new(name) - .parent() - .map(|p| p.to_string_lossy().to_string()) - .unwrap_or_default(); - let mut named = String::new(); - for n in cjs_named_exports(src) { - named.push_str(&format!("export const {n} = __m[{:?}];\n", n)); - } - format!( - "const module = {{ exports: {{}} }};\n\ - let exports = module.exports;\n\ - const __filename = {name:?};\n\ - const __dirname = {dir:?};\n\ - const require = (s) => globalThis.__cjsRequire(__filename, s);\n\ - globalThis.__cjsCache = globalThis.__cjsCache || new Map();\n\ - globalThis.__cjsCache.set(__filename, module.exports);\n\ - (function (module, exports, require, __filename, __dirname) {{\n{src}\n}})(module, exports, require, __filename, __dirname);\n\ - const __m = module.exports;\n\ - globalThis.__cjsCache.set(__filename, __m);\n\ - export default __m;\n{named}" - ) -} - -/// Scan CJS source for its named exports (best-effort, covers TS-compiled CJS). -/// Uses `match_indices` so slicing always lands on char boundaries (a byte-index -/// slice here once panicked in the non-unwinding native op → SIGABRT). -fn cjs_named_exports(src: &str) -> Vec { - fn ident(s: &str) -> String { - s.chars() - .take_while(|c| c.is_alphanumeric() || *c == '_' || *c == '$') - .collect() - } - let mut set: Vec = Vec::new(); - let mut push = |n: String| { - if n != "default" && n != "__esModule" && !n.is_empty() && !set.contains(&n) { - set.push(n); - } - }; - // `exports.NAME =` / `exports.NAME[` - for (idx, _) in src.match_indices("exports.") { - let rest = &src[idx + "exports.".len()..]; - let name = ident(rest); - if !name.is_empty() { - let after = rest[name.len()..].trim_start(); - if after.starts_with('=') || after.starts_with('[') { - push(name); - } - } - } - // `Object.defineProperty(exports, "NAME"` and `__createBinding(exports, …, "NAME"` - for pat in ["defineProperty(exports,", "__createBinding(exports,"] { - for (idx, _) in src.match_indices(pat) { - let rest = &src[idx + pat.len()..]; - if let Some(q) = rest.find(['"', '\'']) { - push(ident(&rest[q + 1..])); - } - } - } - set -} - -/// Rewrite indirect named re-exports into an import + a local export: -/// `export { a, b as c } from "./y"` → -/// `import { a, b as c } from "./y"; export { a, c };` -/// QuickJS resolves an indirect export by chaining through modules, which trips -/// "circular reference" inside dependency cycles. A local binding sidesteps that -/// chain and is semantically identical outside cycles. -fn rewrite_reexports(src: &str) -> String { - use regex::Regex; - use std::sync::OnceLock; - static RE: OnceLock = OnceLock::new(); - let re = RE.get_or_init(|| { - Regex::new(r#"(?m)^\s*export\s*\{([^}]*)\}\s*from\s*(["'][^"']+["'])\s*;?"#).unwrap() - }); - let counter = std::cell::Cell::new(0usize); - re.replace_all(src, |caps: ®ex::Captures| { - let (list, spec) = (&caps[1], &caps[2]); - let mut imports = Vec::new(); - let mut exports = Vec::new(); - for item in list.split(',') { - let item = item.trim(); - if item.is_empty() { - continue; - } - // `orig` is the name in the target module, `exported` the outer name. - let (orig, exported) = match item.split_once(" as ") { - Some((a, b)) => (a.trim(), b.trim()), - None => (item, item), - }; - // A unique local avoids clashing with an existing `import { orig }`. - let n = counter.get(); - counter.set(n + 1); - let local = format!( - "__rx{n}_{}", - exported.replace(|c: char| !c.is_alphanumeric(), "_") - ); - imports.push(format!("{orig} as {local}")); - exports.push(format!("{local} as {exported}")); - } - format!( - "import {{ {} }} from {spec}; export {{ {} }};", - imports.join(", "), - exports.join(", ") - ) - }) - .into_owned() -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn classifies_cjs_and_esm() { - assert!(is_cjs("const x = require('y');\nmodule.exports = x;")); - assert!(!is_cjs("import x from 'y';\nexport default x;")); - assert!(!is_cjs("export const a = 1;")); - } - - #[test] - fn detects_cjs_named_exports() { - let names = - cjs_named_exports("exports.foo = 1;\nObject.defineProperty(exports, \"bar\", {});"); - assert!(names.contains(&"foo".to_string())); - assert!(names.contains(&"bar".to_string())); - assert!(!names.contains(&"default".to_string())); - } - - #[test] - fn rewrites_indirect_reexports_to_local_bindings() { - let out = rewrite_reexports("export { a, b as c } from \"./y\";"); - assert!(out.contains("import {"), "got: {out}"); - assert!(out.contains("as c"), "got: {out}"); - assert!( - !out.contains("export { a, b as c } from"), - "still indirect: {out}" - ); - } - - #[test] - fn prepare_dispatches_on_extension() { - assert!(prepare_module_source("/x.json", "{\"a\":1}") - .unwrap() - .starts_with("export default")); - assert!(prepare_module_source("/x.cjs", "module.exports = 1;") - .unwrap() - .contains("__cjsRequire")); - } -} diff --git a/crates/pocket-pi/src/tests.rs b/crates/pocket-pi/src/tests.rs deleted file mode 100644 index 771aabc..0000000 --- a/crates/pocket-pi/src/tests.rs +++ /dev/null @@ -1,641 +0,0 @@ -use super::*; -use std::cell::RefCell; -use std::rc::Rc; -use std::time::{Duration, Instant}; - -/// Pump until the runtime goes idle or a frame budget is hit. Returns the -/// collected host events. `hz` is the pump cadence — we deliberately run slow to -/// prove the coalesced scheduler carries a real turn to completion. -fn run(rt: &mut PiRuntime, sink: Rc>>, hz: f64, max_secs: f64) { - let period = Duration::from_secs_f64(1.0 / hz); - let start = Instant::now(); - while !rt.is_idle() && start.elapsed().as_secs_f64() < max_secs { - rt.pump().unwrap(); - std::thread::sleep(period); - } - // One last pump to flush any trailing events. - rt.pump().unwrap(); - let _ = &sink; -} - -fn collector() -> ( - Rc>>, - impl FnMut(&HostEvent) + 'static, -) { - let sink = Rc::new(RefCell::new(Vec::new())); - let s = sink.clone(); - (sink, move |ev: &HostEvent| s.borrow_mut().push(ev.clone())) -} - -/// Assemble the assistant reply the way the host does: streamed `text` deltas, -/// with an `assistant_text` full-text emit as a fallback. -fn reply_text(events: &[HostEvent]) -> String { - let mut out = String::new(); - for e in events { - match e.kind.as_str() { - "text" => { - if let Some(d) = e.value.get("delta").and_then(|v| v.as_str()) { - out.push_str(d); - } - } - "assistant_text" if out.is_empty() => { - if let Some(t) = e.value.get("text").and_then(|v| v.as_str()) { - out = t.to_string(); - } - } - _ => {} - } - } - out -} - -/// Milestone 1 of the Node-compat runtime: the module system resolves + loads -/// real modules — a relative `.ts` file (transpiled), `node:` builtins, and a -/// bare package from `node_modules` — and they run correctly. -#[test] -fn node_module_system_loads_ts_builtins_and_a_bare_package() { - use std::fs; - let dir = std::env::temp_dir().join(format!("pocketpi-node-test-{}", std::process::id())); - let _ = fs::remove_dir_all(&dir); - fs::create_dir_all(dir.join("node_modules/greet")).unwrap(); - fs::write( - dir.join("node_modules/greet/package.json"), - r#"{"name":"greet","version":"1.0.0","module":"index.js"}"#, - ) - .unwrap(); - fs::write( - dir.join("node_modules/greet/index.js"), - "export default (who) => \"hello \" + who;", - ) - .unwrap(); - fs::write( - dir.join("entry.ts"), - r#" -import { join } from "node:path"; -import { EventEmitter } from "node:events"; -import { Buffer } from "node:buffer"; -import { homedir } from "node:os"; -import greet from "greet"; - -type Result = { path: string; ee: boolean; b64: string; greet: string; hasHome: boolean }; -const ee = new EventEmitter(); -let fired = false; -ee.on("x", () => { fired = true; }); -ee.emit("x"); - -const out: Result = { - path: join("a", "b", "..", "c"), - ee: fired, - b64: Buffer.from("hi").toString("base64"), - greet: greet("cat"), - hasHome: typeof homedir() === "string", -}; -(globalThis as any).__nodeTest = out; -"#, - ) - .unwrap(); - - let mut rt = PiRuntime::new().expect("runtime"); - rt.run_module(dir.join("entry.ts").to_str().unwrap()) - .expect("run module"); - - let out = rt.get_global_json("__nodeTest").expect("__nodeTest set"); - assert_eq!(out["path"], "a/c", "path.join wrong: {out}"); - assert_eq!(out["ee"], true, "EventEmitter didn't fire: {out}"); - assert_eq!(out["b64"], "aGk=", "Buffer base64 wrong: {out}"); - assert_eq!( - out["greet"], "hello cat", - "bare package import wrong: {out}" - ); - assert_eq!(out["hasHome"], true, "os.homedir wrong: {out}"); - - let _ = fs::remove_dir_all(&dir); -} - -/// Load a piece of the REAL, unmodified pi-ai (`utils/event-stream.js`) straight -/// from node_modules through the Node loader and exercise its actual class. This -/// is the "run unmodified pi" thesis, proven on real pi code — and validates the -/// ESM-first (no-CJS) decision. Skipped if node_modules isn't installed. -#[test] -fn runs_real_unmodified_pi_ai_module() { - let manifest = env!("CARGO_MANIFEST_DIR"); - let real = - format!("{manifest}/../../js/node_modules/@mariozechner/pi-ai/dist/utils/event-stream.js"); - if !std::path::Path::new(&real).exists() { - eprintln!("skipping runs_real_unmodified_pi_ai_module: run `npm install` in js/ first"); - return; - } - - let dir = std::env::temp_dir().join(format!("pocketpi-piai-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write( - dir.join("entry.ts"), - format!( - r#" -import {{ AssistantMessageEventStream }} from "{real}"; -const s = new AssistantMessageEventStream(); -const msg = {{ role: "assistant", content: [{{ type: "text", text: "hi" }}] }}; -// The real stream completes on a "done" event and resolves result() to its message. -s.push({{ type: "text_delta", delta: "hi", partial: msg }}); -s.push({{ type: "done", message: msg }}); -s.result().then((m: any) => {{ - (globalThis as any).__piAi = {{ resolvedText: m.content[0].text, isReal: true }}; -}}); -"# - ), - ) - .unwrap(); - - let mut rt = PiRuntime::new().expect("runtime"); - rt.run_module(dir.join("entry.ts").to_str().unwrap()) - .expect("run real pi-ai module"); - // Drain so the result() promise settles. - for _ in 0..5 { - rt.pump().ok(); - } - - let out = rt.get_global_json("__piAi").expect("real pi-ai class ran"); - assert_eq!( - out["resolvedText"], "hi", - "real pi-ai EventStream misbehaved: {out}" - ); - assert_eq!(out["isReal"], true); - let _ = std::fs::remove_dir_all(&dir); -} - -/// WHATWG `fetch` → `Response` → `ReadableStream` → `json()`, end to end over the -/// native HTTP hub. Hits a real endpoint (needs network/proxy); skips if -/// unreachable. A 401 is fine — fetch doesn't throw on it, which is the point. -#[test] -fn whatwg_fetch_returns_a_readable_response() { - let mut rt = PiRuntime::new().expect("runtime"); - rt.eval_script( - r#" - globalThis.__f = { done: false }; - fetch("https://api.anthropic.com/v1/models") - .then(async (r) => { - const body = await r.text(); - let parsed = null; try { parsed = JSON.parse(body); } catch {} - globalThis.__f = { - done: true, status: r.status, ok: r.ok, - ct: r.headers.get("content-type"), - len: body.length, isObject: parsed !== null && typeof parsed === "object", - }; - }) - .catch((e) => { globalThis.__f = { done: true, err: String(e && e.message ? e.message : e) }; }); - "#, - ) - .expect("eval fetch"); - - let start = std::time::Instant::now(); - loop { - rt.pump().unwrap(); - let f = rt.get_global_json("__f").unwrap_or(serde_json::Value::Null); - if f.get("done").and_then(|v| v.as_bool()) == Some(true) { - if let Some(err) = f.get("err").and_then(|v| v.as_str()) { - eprintln!("skipping whatwg_fetch: endpoint unreachable ({err})"); - return; - } - eprintln!("fetch result: {f}"); - let status = f["status"].as_i64().unwrap_or(0); - assert!(status > 0, "no status: {f}"); - assert!(f["len"].as_i64().unwrap_or(0) > 0, "empty body: {f}"); - assert_eq!(f["isObject"], true, "body wasn't JSON: {f}"); - assert!( - f["ct"].as_str().unwrap_or("").contains("json"), - "content-type header missing: {f}" - ); - return; - } - if start.elapsed().as_secs() > 30 { - panic!("fetch never completed: {f}"); - } - std::thread::sleep(std::time::Duration::from_millis(30)); - } -} - -/// Live end-to-end against OpenAI. Skipped unless OPENAI_API_KEY is set. -#[test] -fn live_openai_turn() { - let Ok(key) = std::env::var("OPENAI_API_KEY") else { - eprintln!("skipping live_openai_turn: OPENAI_API_KEY not set"); - return; - }; - let model = std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-4o-mini".into()); - let (sink, cb) = collector(); - let mut rt = PiRuntime::new().expect("runtime"); - rt.on_event(cb); - let cfg = serde_json::json!({ - "provider": "openai", - "model": model, - "apiKey": key, - // Reasoning models (gpt-5.x) spend tokens thinking before answering — - // a tiny cap leaves nothing for the visible reply. - "maxTokens": 2048, - "systemPrompt": "Reply with exactly the word: pong" - }); - rt.boot(&cfg.to_string()).expect("boot"); - rt.prompt("ping").expect("prompt"); - run(&mut rt, sink.clone(), 4.0, 90.0); - - let events = sink.borrow(); - let kinds: Vec<&str> = events.iter().map(|e| e.kind.as_str()).collect(); - eprintln!("openai event kinds: {kinds:?}"); - for e in events.iter().filter(|e| e.kind == "error") { - eprintln!("openai error raw: {}", e.raw); - } - assert!(kinds.contains(&"end"), "turn did not complete: {kinds:?}"); - let text = reply_text(&events); - eprintln!("live openai said: {text:?}"); - assert!(!text.is_empty(), "no assistant text"); -} - -/// Live end-to-end against Anthropic. Skipped unless ANTHROPIC_API_KEY is set, -/// so `cargo test` is hermetic by default. -#[test] -fn live_anthropic_turn() { - let Ok(key) = std::env::var("ANTHROPIC_API_KEY") else { - eprintln!("skipping live_anthropic_turn: ANTHROPIC_API_KEY not set"); - return; - }; - let (sink, cb) = collector(); - let mut rt = PiRuntime::new().expect("runtime"); - rt.on_event(cb); - let cfg = serde_json::json!({ - "model": "claude-opus-4-8", - "apiKey": key, - "maxTokens": 64, - "systemPrompt": "Reply with exactly the word: pong" - }); - rt.boot(&cfg.to_string()).expect("boot"); - rt.prompt("ping").expect("prompt"); - run(&mut rt, sink.clone(), 4.0, 60.0); - - let events = sink.borrow(); - let kinds: Vec<&str> = events.iter().map(|e| e.kind.as_str()).collect(); - assert!(kinds.contains(&"end"), "turn did not complete: {kinds:?}"); - let text = reply_text(&events); - eprintln!("live model said: {text:?}"); - assert!(!text.is_empty(), "no assistant text"); -} - -/// The full, unmodified pi-coding-agent is embedded and loaded by every -/// `PiRuntime::new()`: `globalThis.PiFull` (createAgentSession, …) and the -/// `PocketPi` host harness are both present with no external bundle, no network. -#[test] -fn new_embeds_and_loads_full_pi() { - let mut rt = PiRuntime::new().expect("runtime"); - assert_eq!( - rt.get_global_json("__piFullLoaded"), - Some(serde_json::Value::Bool(true)), - "full pi did not initialize in new()" - ); - // The host harness is wired too. - rt.eval_script("globalThis.__hasPocketPi = typeof globalThis.PocketPi?.boot === 'function';") - .expect("probe"); - assert_eq!( - rt.get_global_json("__hasPocketPi"), - Some(serde_json::Value::Bool(true)) - ); -} - -/// The deterministic Mac fallback still runs a complete, unmodified -/// pi-coding-agent AgentSession; only its model provider is local and scripted. -#[test] -fn full_pi_runs_with_offline_provider() { - let (sink, cb) = collector(); - let mut rt = PiRuntime::new().expect("runtime"); - rt.on_event(cb); - let cfg = serde_json::json!({ - "model": "offline", - "scripted": {"steps": [{"text": "OFFLINE-E2E-OK"}]} - }); - rt.boot(&cfg.to_string()).expect("boot"); - rt.prompt("reply with the scripted response") - .expect("prompt"); - run(&mut rt, sink.clone(), 30.0, 5.0); - - let events = sink.borrow(); - let errors: Vec<&HostEvent> = events - .iter() - .filter(|event| event.kind == "error") - .collect(); - assert!(errors.is_empty(), "offline turn failed: {errors:?}"); - assert!( - events.iter().any(|event| event.kind == "end"), - "turn did not end" - ); - assert_eq!(reply_text(&events), "OFFLINE-E2E-OK"); -} - -/// Path B end-to-end: stand up an AgentSession from the UNMODIFIED bundled -/// pi-coding-agent and run one real turn against gpt-5.6 through Pocket Pi's -/// fetch + system proxy. Requires OPENAI_API_KEY and a reachable proxy, so it is -/// `#[ignore]`. Run with: -/// https_proxy=http://127.0.0.1:7897 OPENAI_API_KEY=... \ -/// cargo test -p pocket-pi runs_bundled_pi_turn -- --ignored --nocapture -#[ignore] -#[test] -fn runs_bundled_pi_turn() { - let manifest = env!("CARGO_MANIFEST_DIR"); - let driver = format!("{manifest}/js/pi-full/driver.js"); - let key = std::env::var("OPENAI_API_KEY").unwrap_or_default(); - if key.is_empty() { - eprintln!("skip: OPENAI_API_KEY not set"); - return; - } - - let mut rt = PiRuntime::new().expect("runtime"); - // Inject the key as a global (kept out of logs) then load the driver script. - rt.eval_script(&format!("globalThis.__OPENAI_KEY = {key:?};")) - .expect("inject key"); - let driver_src = std::fs::read_to_string(&driver).expect("driver.js"); - rt.eval_script(&driver_src).expect("driver eval"); - - // Kick off the async turn (fire-and-forget promise), then pump at 2Hz. - rt.eval_script("globalThis.__piRun('Reply with exactly: pocket pi lives');") - .expect("kick off"); - - let start = std::time::Instant::now(); - let mut done = false; - while start.elapsed().as_secs_f64() < 90.0 { - rt.pump().expect("pump"); - if rt.get_global_json("__piDone") == Some(serde_json::Value::Bool(true)) { - done = true; - break; - } - std::thread::sleep(std::time::Duration::from_millis(250)); - } - - let result = rt.get_global_json("__piResult"); - let err = rt.get_global_json("__piError"); - let last = rt.get_global_json("__piLastEvent"); - let log = rt.get_global_json("__piLog"); - eprintln!("TURN done={done} last_event={last:?}"); - eprintln!("TURN error={err:?}"); - eprintln!("TURN result={result:?}"); - if let Some(serde_json::Value::Array(items)) = log { - eprintln!("TURN log ({} events):", items.len()); - for it in items { - eprintln!(" - {}", it.as_str().unwrap_or_default()); - } - } - assert!(done, "turn did not complete within budget"); - assert_eq!(err, Some(serde_json::Value::Null), "agent errored"); - let text = result - .and_then(|v| v.as_str().map(String::from)) - .unwrap_or_default(); - assert!(!text.trim().is_empty(), "no assistant text produced"); -} - -/// Path B extensions (M6): load a real, unmodified pi extension through Pocket -/// Pi's OWN module loader — the `.ts` is transpiled natively with oxc, NOT jiti -/// (which needs Node internals QuickJS lacks) — and hand its default factory to -/// pi's unmodified `loadExtensionFromFactory`. Asserts the extension's tool and -/// lifecycle hook register. Offline; only needs the built bundle. Run with: -/// cargo test -p pocket-pi loads_pi_extension_via_our_loader -- --ignored --nocapture -#[test] -fn loads_pi_extension_via_our_loader() { - let manifest = env!("CARGO_MANIFEST_DIR"); - let probe = format!("{manifest}/js/pi-full/ext-probe.js"); - let ext = format!("{manifest}/../../js/src/pi-full/example-extension.ts"); - - let mut rt = PiRuntime::new().expect("runtime"); - let probe_src = std::fs::read_to_string(&probe).expect("ext-probe.js"); - rt.eval_script(&probe_src).expect("probe eval"); - rt.eval_script(&format!("globalThis.__piLoadExtension({ext:?});")) - .expect("kick off"); - - let start = std::time::Instant::now(); - while start.elapsed().as_secs_f64() < 15.0 { - rt.pump().expect("pump"); - if rt.get_global_json("__piExtDone") == Some(serde_json::Value::Bool(true)) { - break; - } - std::thread::sleep(std::time::Duration::from_millis(50)); - } - - let err = rt.get_global_json("__piExtError"); - let result = rt.get_global_json("__piExtResult"); - eprintln!("EXT error={err:?}"); - eprintln!("EXT result={result:?}"); - assert_eq!(err, Some(serde_json::Value::Null), "extension load errored"); - let result = result.expect("no extension result"); - let tools = result - .get("tools") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - let handlers = result - .get("handlers") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - assert!( - tools.iter().any(|t| t.as_str() == Some("echo")), - "extension tool 'echo' not registered (got {tools:?})" - ); - assert!( - handlers.iter().any(|h| h.as_str() == Some("agent_start")), - "extension hook 'agent_start' not registered (got {handlers:?})" - ); -} - -/// Pump `__piRun(optsJson)` (see driver.js) until it finishes or the budget is -/// hit. Returns nothing; results are read from globals by the caller. -fn drive_session(rt: &mut PiRuntime, opts_json: &str, max_secs: f64) -> bool { - rt.eval_script(&format!("globalThis.__piRun({opts_json:?});")) - .expect("kick off"); - let start = std::time::Instant::now(); - while start.elapsed().as_secs_f64() < max_secs { - rt.pump().expect("pump"); - if rt.get_global_json("__piDone") == Some(serde_json::Value::Bool(true)) { - return true; - } - std::thread::sleep(std::time::Duration::from_millis(100)); - } - false -} - -/// M6b: bind an unmodified pi extension into a *live* AgentSession via the -/// first-class `extensionFactories` seam (no jiti, no network) and confirm the -/// session's ExtensionRunner picked up the hook and tool. Offline; bundle-gated. -/// cargo test -p pocket-pi binds_extension_into_session -- --ignored --nocapture -#[test] -fn binds_extension_into_session() { - let manifest = env!("CARGO_MANIFEST_DIR"); - let driver = format!("{manifest}/js/pi-full/driver.js"); - let ext = format!("{manifest}/../../js/src/pi-full/example-extension.ts"); - let mut rt = PiRuntime::new().expect("runtime"); - rt.eval_script(&std::fs::read_to_string(&driver).expect("driver.js")) - .expect("driver eval"); - - let opts = serde_json::json!({ "extensionPath": ext }).to_string(); - let done = drive_session(&mut rt, &opts, 15.0); - let err = rt.get_global_json("__piError"); - let bind = rt.get_global_json("__piBind"); - eprintln!("BIND done={done} error={err:?}"); - eprintln!("BIND result={bind:?}"); - assert!(done, "session build did not finish"); - assert_eq!(err, Some(serde_json::Value::Null), "session build errored"); - let bind = bind.expect("no bind info"); - assert_eq!( - bind.get("hasAgentStart"), - Some(&serde_json::Value::Bool(true)), - "agent_start hook not bound into session" - ); - let tools = bind - .get("registeredTools") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - assert!( - tools.iter().any(|t| t.as_str() == Some("echo")), - "echo tool not registered in session (got {tools:?})" - ); -} - -/// M6b (online): run a real gpt-5.6 turn with the extension's `echo` tool active -/// and instruct the model to call it. Asserts the extension's lifecycle hook -/// fired and the tool actually executed. Network + bundle gated. Run with: -/// https_proxy=http://127.0.0.1:7897 OPENAI_API_KEY=... \ -/// cargo test -p pocket-pi runs_pi_turn_with_extension_tool -- --ignored --nocapture -#[ignore] -#[test] -fn runs_pi_turn_with_extension_tool() { - let manifest = env!("CARGO_MANIFEST_DIR"); - let driver = format!("{manifest}/js/pi-full/driver.js"); - let ext = format!("{manifest}/../../js/src/pi-full/example-extension.ts"); - let key = std::env::var("OPENAI_API_KEY").unwrap_or_default(); - if key.is_empty() { - eprintln!("skip: OPENAI_API_KEY not set"); - return; - } - let mut rt = PiRuntime::new().expect("runtime"); - rt.eval_script(&format!("globalThis.__OPENAI_KEY = {key:?};")) - .expect("inject key"); - rt.eval_script(&std::fs::read_to_string(&driver).expect("driver.js")) - .expect("driver eval"); - - let opts = serde_json::json!({ - "extensionPath": ext, - "tools": ["echo"], - "prompt": "Call the echo tool with text set to \"ping\". Use the tool; do not answer in prose.", - }) - .to_string(); - let done = drive_session(&mut rt, &opts, 90.0); - let err = rt.get_global_json("__piError"); - let hook = rt.get_global_json("__extAgentStartFired"); - let echoed = rt.get_global_json("__echoCalled"); - eprintln!("EXT-TURN done={done} error={err:?}"); - eprintln!("EXT-TURN agent_start_fired={hook:?} echoCalled={echoed:?}"); - assert!(done, "turn did not complete within budget"); - assert_eq!(err, Some(serde_json::Value::Null), "agent errored"); - assert_eq!( - hook, - Some(serde_json::Value::Bool(true)), - "extension agent_start hook did not fire during the turn" - ); - assert!( - echoed.is_some() && echoed != Some(serde_json::Value::Null), - "extension echo tool was not executed" - ); -} - -/// M7: persist a session to disk with pi's unmodified SessionManager (backed by -/// Pocket Pi's fs builtin), then resume it in a fresh manager and confirm the -/// history round-trips. Offline; bundle-gated. Run with: -/// cargo test -p pocket-pi persists_and_resumes_session -- --ignored --nocapture -#[test] -fn persists_and_resumes_session() { - let manifest = env!("CARGO_MANIFEST_DIR"); - let probe = format!("{manifest}/js/pi-full/persist-probe.js"); - let dir = std::env::temp_dir().join(format!("pocket-pi-sess-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - - let mut rt = PiRuntime::new().expect("runtime"); - rt.eval_script(&std::fs::read_to_string(&probe).expect("persist-probe.js")) - .expect("probe eval"); - rt.eval_script(&format!( - "globalThis.__piPersist({:?});", - dir.to_str().unwrap() - )) - .expect("run persist"); - rt.pump().expect("pump"); - - let err = rt.get_global_json("__piPersistError"); - let result = rt.get_global_json("__piPersistResult"); - eprintln!("PERSIST error={err:?}"); - eprintln!("PERSIST result={result:?}"); - // Confirm the session file actually hit disk. - let files: Vec<_> = std::fs::read_dir(&dir) - .unwrap() - .filter_map(|e| e.ok()) - .map(|e| e.file_name()) - .collect(); - eprintln!("PERSIST files={files:?}"); - let _ = std::fs::remove_dir_all(&dir); - - assert_eq!(err, Some(serde_json::Value::Null), "persistence errored"); - let result = result.expect("no persist result"); - assert_eq!( - result.get("wrote").and_then(|v| v.as_u64()), - Some(2), - "did not write 2 messages" - ); - assert_eq!( - result.get("resumedCount").and_then(|v| v.as_u64()), - Some(2), - "resumed session lost messages" - ); - let texts = result - .get("texts") - .and_then(|v| v.as_array()) - .cloned() - .unwrap_or_default(); - assert!( - texts - .iter() - .any(|t| t.as_str().is_some_and(|s| s.contains("42"))), - "resumed history missing content (got {texts:?})" - ); - assert!( - files - .iter() - .any(|f| f.to_string_lossy().ends_with(".jsonl")), - "no .jsonl session file on disk" - ); -} - -/// WIP integration probe toward loading unmodified pi-coding-agent. Run with -/// `cargo test -- --ignored probe_pi_coding_agent --nocapture`. Currently clears -/// the whole Node-builtin + CJS dependency surface and reaches pi-coding-agent's -/// own modules (blocked on a QuickJS ESM indirect-re-export cycle). -#[ignore] -#[test] -fn probe_pi_coding_agent() { - let manifest = env!("CARGO_MANIFEST_DIR"); - let sdk = - format!("{manifest}/../../js/node_modules/@mariozechner/pi-coding-agent/dist/core/sdk.js"); - if !std::path::Path::new(&sdk).exists() { - eprintln!("skip: not installed"); - return; - } - let dir = std::env::temp_dir().join(format!("pca-probe-{}", std::process::id())); - let _ = std::fs::remove_dir_all(&dir); - std::fs::create_dir_all(&dir).unwrap(); - std::fs::write(dir.join("e.ts"), format!( - "import {{ createAgentSession }} from \"{sdk}\";\n(globalThis as any).__pca = typeof createAgentSession;\n" - )).unwrap(); - let mut rt = PiRuntime::new().expect("rt"); - match rt.run_module(dir.join("e.ts").to_str().unwrap()) { - Ok(()) => eprintln!( - "PROBE OK: createAgentSession = {:?}", - rt.get_global_json("__pca") - ), - Err(e) => eprintln!("PROBE ERR: {e}"), - } - let _ = std::fs::remove_dir_all(&dir); -} diff --git a/crates/pocket-pi/src/transpile.rs b/crates/pocket-pi/src/transpile.rs deleted file mode 100644 index b3f2338..0000000 --- a/crates/pocket-pi/src/transpile.rs +++ /dev/null @@ -1,68 +0,0 @@ -//! Runtime TypeScript → JavaScript, the native op behind Pocket Pi's plugin -//! loader. -//! -//! Real pi loads its extensions — TypeScript files — at runtime through `jiti` -//! (`jiti` + `typescript`, a heavy Node toolchain). Pocket Pi has no Node, so -//! per the runtime's own rule ("heavy Node deps become native ops") it strips -//! types with **oxc**, a pure-Rust compiler, and hands the resulting JS to -//! QuickJS. Only type-erasure + light syntax lowering is needed: QuickJS is -//! ES2023, so we keep modern syntax and just remove the TypeScript. - -use oxc_allocator::Allocator; -use oxc_codegen::Codegen; -use oxc_parser::Parser; -use oxc_semantic::SemanticBuilder; -use oxc_span::SourceType; -use oxc_transformer::{TransformOptions, Transformer}; -use std::path::Path; - -/// Transpile a TypeScript source string to JavaScript. `filename` only informs -/// the source-type detection (`.ts` / `.tsx`) and diagnostics. -pub fn transpile_ts(filename: &str, source: &str) -> Result { - let allocator = Allocator::default(); - let path = Path::new(filename); - let source_type = SourceType::from_path(path).unwrap_or_else(|_| SourceType::ts()); - - let parsed = Parser::new(&allocator, source, source_type).parse(); - if parsed.panicked || !parsed.diagnostics.is_empty() { - let msgs: Vec = parsed.diagnostics.iter().map(|d| d.to_string()).collect(); - return Err(format!("TypeScript parse error: {}", msgs.join("; "))); - } - - let mut program = parsed.program; - let scoping = SemanticBuilder::new() - .build(&program) - .semantic - .into_scoping(); - let result = Transformer::new(&allocator, path, &TransformOptions::default()) - .build_with_scoping(scoping, &mut program); - if !result.diagnostics.is_empty() { - let msgs: Vec = result.diagnostics.iter().map(|d| d.to_string()).collect(); - return Err(format!("TypeScript transform error: {}", msgs.join("; "))); - } - - Ok(Codegen::new().build(&program).code) -} - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn strips_types_keeps_logic() { - let js = transpile_ts( - "p.ts", - "type N = number;\nexport default (a: N, b: N): N => a + b;", - ) - .unwrap(); - assert!(js.contains("export default"), "got: {js}"); - assert!(!js.contains(": N"), "types not stripped: {js}"); - assert!(!js.contains("type N"), "type alias not stripped: {js}"); - } - - #[test] - fn reports_parse_errors() { - let err = transpile_ts("p.ts", "export default function( {{{ ").unwrap_err(); - assert!(err.contains("parse"), "got: {err}"); - } -} diff --git a/docs/agentos-architecture.md b/docs/agentos-architecture.md new file mode 100644 index 0000000..fde9324 --- /dev/null +++ b/docs/agentos-architecture.md @@ -0,0 +1,1222 @@ +# Pocket Pi AgentOS 架构设计 + +Pocket Pi 是面向嵌入式设备和专用设备的完整 Agent-native runtime。Agent 是设备 +上的常驻 system actor,而不是运行在通用桌面或移动操作系统上的用户应用;设备的 +workspace、native tools、schedules、Agent-native Apps、本地状态、UI 和生命周期共同 +构成 Pocket Pi。 + +ESP32-P4 是第一台完整支持的硬件和当前 reference implementation。macOS 上的 +`esp32-p4-sim` 只用于开发和 product-contract 验证,不是 Pocket Pi 桌面产品、通用 +`pi-agent-core` SDK 或第二个硬件 target。本文件定义的是完整设备 runtime 的 +AgentOS/App 语义,不定义 standalone Agent harness。 + +状态:架构基线 + v1 实现记录(以“常驻 Pi Agent System App”为当前实现)。 + +截至 2026-08-12,当前工作树已经实现可运行的第一版:Pi Agent Root App、App +Supervisor、App Tool Catalog/Router、`AppTask` Schedule、`data.fs`、 +`data.sqlite`、后台 Data Action、revision-coalesced projection cache,以及 +Robinhood 和 Exa 两个可选 App,以及 build-time App selection。 +Simulator contract tests 已通过;常驻 System App refactor 已在 ESP32-P4 实机 +完成冷启动、LittleFS App 加载、Root View、MIPI-DSI/Touch 初始化、Agent Tool +Registry 启动和一次完整 UART model turn。运行中切换普通 App 的生命周期 contract +已有自动测试;实机触摸切换仍保留为发布前人工体验验收。 + +本文件同时保留最终架构约束。标为“待补齐”的部分不能因为 v1 已经能启动而 +被误认为已经完成。 + +## 1. 一句话结论 + +Pocket Pi 是一套设备级 Agent-native Runtime:Rust/native host 提供稳定的硬件、 +安全和生命周期机制;Pi Agent 作为常驻 System App 拥有顶层 `/workspace`;每个 App +是一个预编译 PocketJS Bundle,里面包含暴露给 Agent 的 Tools、内部 Tasks、 +Schedules、SQLite 数据和绑定这些数据的固定 View。 + +最短的理解方式是: + +```text +Firmware = 稳定机制 +Pi Agent = /workspace 的 owner + 特殊 System App +App = Public Tools + Data Actions + SQLite State + Cached Fixed View +``` + +### 1.1 三条核心设计原则 + +1. **App 是 Agent-facing capability、App-owned state 和 human-facing fixed View + 的同一个产品单元。** Tool 不是脱离 UI 的插件,View 也不是脱离能力的页面; + 二者读取同一份本地状态。 +2. **State 是能力与 View 之间唯一的协调面。** Agent Tool、App Schedule 和 UI + action 汇入同一个 Data Action;一次业务 transaction 成功后只发布一次单调 + revision,View 再查询 bounded SQLite projection。Agent 不手工同步 View,View + 也不直接执行 provider 副作用。 +3. **Agent 负责决定“为什么、何时做”,App 负责确定性地完成“怎么做、怎么保存、 + 怎么显示”。** AppTask、定时刷新和 View 渲染不需要模型 turn;模型是跨 App 的 + 编排者,而不是每个产品功能的运行时依赖。 + +Pi Agent 常驻 System App、Core/Bundle ownership 和前后台 Guest 生命周期,都是为了 +落实上述原则形成的架构约束;它们本身不是额外的产品 core concept。 + +### 1.2 PocketJS 与 Pocket Pi AgentOS 的层次 + +这两个层次不能合并理解: + +| 层 | 当前负责什么 | 不负责什么 | +| --- | --- | --- | +| PocketJS | 单个 QuickJS Guest、TS/TSX Bundle、UI tree/layout/render、`data.fs`、`data.sqlite`、`fetch()`/`pocket-net` 等 portable module contract | 不知道 Pi Agent、`/workspace` owner、有哪些 App、哪个 App 在前台、哪个 Tool 属于哪个 App | +| Pocket Pi AgentOS | `AppSupervisor`、System/ordinary App 生命周期、Tool Catalog/Router、Data Action queue、AppTask Schedule、revision delivery、App data ownership | 不重新实现 PocketJS 的 UI、JS engine、DB/FS/net module 形状 | +| Host adapter | 把 portable contract 接到 LittleFS/SQLite、ESP HTTP/TLS、MCP、Keychain/NVS、LCD/Touch 和资源限制 | 不拥有 Robinhood/Exa 的 schema、provider mapping 或 View | + +因此 `AppSupervisor`、`RoutedToolHost` 和 `AppDataRunner` 不是 PocketJS 缺失的基础能力, +而是建立在 PocketJS Guest/module primitives 之上的 **AgentOS 跨 App 产品语义**。 +PocketJS 类似可移植的嵌入式 application/UI runtime;它不应替具体产品决定 App +catalog、Agent Tool ownership 或后台任务生命周期。如果其中某个 primitive 将来被 +证明对所有 PocketJS 产品都通用,可以再上游抽象,但 Pocket Pi 的 ownership policy +仍留在 AgentOS。 + +## 2. 设计目标 + +1. 在嵌入式和专用设备上提供完整常驻 Agent runtime,而不是构建通用桌面 Agent、 + Node compatibility layer 或 standalone `pi-agent-core` SDK。 +2. Robinhood、Exa 等产品逻辑和 UI 不再进入通用 Rust 固件。 +3. App 可以给 Pi Agent 暴露 Tools,但不需要暴露内部表结构、凭据、网络协议 + 和 UI 实现。 +4. App 可以在本地按时运行任务,不需要每五分钟都唤醒模型。 +5. SQLite 是 App 数据的唯一持久化真相;成功写入后,正在显示的 View 自动 + 更新,但不能每帧轮询数据库。 +6. 普通 App 彼此隔离;Pi Agent 始终拥有整个 `/workspace`。 +7. 同一套 App 源码和 Module contract 可以在模拟器、ESP32-P4 和后续硬件上复用。 +8. Agent 工作与前台 View 导航解耦:模型/Tool 在运行时,触摸、键盘、切换 + Robinhood/Exa 和返回 Agent 都不能中断或重建 Agent。 + +## 3. 第一版明确不做什么 + +- 不做应用市场和远程分发协议。 +- ESP32 不修改 App/Root View 源码,不编译 PocketJS Bundle。 +- 不做 Agent 动态创建 Tools 或 workflow interpreter。 +- Tool 变化后可以 reload Agent session,不要求 live hot-plug。 +- ESP32 不提供通用 POSIX shell、进程、多用户或桌面式多任务。 +- 凭据永远不直接暴露给 App 或模型。 +- 不自动把上游 MCP `tools/list` 的所有 Tool 暴露给模型。 + +合法 Bundle 怎么进入 `/workspace/apps/` 不属于本架构。第一版可以预装,也 +可以通过 Mac 开发/部署路径复制。这里定义“如何加载和运行”,不定义“如何 +分发”。 + +当前预装由 `crates/pocket-pi-app-pack` 在 build time 组合。`pi-agent` 始终存在; +ordinary Apps 只通过一个 `--apps` build 参数选择,例如 `--apps robinhood,exa`、 +`--apps exa` 或 `--apps none`。未选择的 App 不进入 catalog、Tool definitions、 +native policy 或 Root Apps View;已有 App data 不自动删除。 + +## 4. 核心概念 + +### 4.1 Firmware / Runtime + +受信任的 Rust 代码,负责: + +- 硬件驱动; +- QuickJS 生命周期; +- PocketJS UI core 和渲染; +- SQLite 和文件系统挂载; +- 模型、网络、MCP、凭据; +- Scheduler; +- App 加载、隔离和路由; +- 资源限制与恢复。 + +Firmware 只提供机制,不包含 Robinhood、Exa、Weather 等产品逻辑。 + +### 4.2 Pi Agent Root Runtime + +Pi Agent 不在 `/workspace/apps/` 下。它的 home 和文件权限根就是 +`/workspace`。 + +它同时也是一个特殊 System App:选择它时,板子显示它的 PocketJS Root +View;打开其他 App 后,它可以继续在后台等待模型、Tool 或 Agent Schedule。 + +它不是两个拼接起来的 runtime。`pi-agent-core` Agent Loop、context、Tool +Registry 与 Root View 必须挂载在**同一个 PocketJS Guest**,共同构成一个 +`pi-agent` App instance。App Supervisor 在启动时创建一次这个 instance,并 +保持到系统关机或明确的 System App restart。 + +`foreground App` 只是“当前哪个 View 产生 DrawList、接收触摸”的选择,不是 +Agent 的生命周期开关。打开普通 App 不得 drop、reload 或 reset Pi Agent +Guest,也不得清空 conversation、pending model request 或 pending Tool call。 + +### 4.3 App + +一个 App 是一个独立版本单元: + +```text +Public Tools Agent 能看到的名称、描述和 JSON Schema +Data Actions Tool、Schedule、UI refresh 共用的后台数据入口 +SQLite State App 自己的唯一持久化业务真相 +Fixed View SQLite bounded projection 的内存 cache + PocketJS UI +``` + +“Fixed View”只表示当前 release 内固定,不表示写死在 Rust 固件里。 + +### 4.4 Native Module + +Native Module 用固定 spec 把一个受限 Rust 能力挂载给 QuickJS Guest。 + +PocketJS 已经定义了 `ui`、`data.sqlite`、`data.fs`、`fetch()`/`pocket-net` 的 +portable contract。Pocket Pi Host 为这些 contract 提供板级实现,并补充 model、 +MCP、schedule、shell、device Settings 和 App lifecycle;其中 TLS、credential、 +endpoint allowlist 等仍属于 Host policy,不属于 PocketJS portable API。 + +### 4.5 Runtime instance + +Pi Agent System App 的 Agent Loop 和 Root View 共用一个常驻 QuickJS Guest。 +普通 App 则可以有两个互不等待的 execution context:一个 foreground View Guest, +以及一个按需创建的 headless Data Action Guest。schema 变化时,Supervisor 先依据 +App descriptor 的 `dataVersion` 删除该 App 的旧 SQLite file;View 可以立即打开空库, +version guard 不执行任何业务 query。第一次 Tool/Schedule 再在后台初始化新 schema, +不阻塞 UI。二者共享同一个 App-owned +`DbModule` owner、同一个 SQLite 文件和同一个内存 revision counter,但不共享 +网络调用栈或 View reactive state。 + +慢模型、HTTP/MCP 和 Data Action 不在 UI tick 或 touch callback 内执行。Native +只拥有 transport、credential 和 SQLite primitive;App 的 provider mapping、 +完整 response-body 解析和 transaction 仍由 JS/TS Data Action 拥有。View Guest +从不拿网络 response,也不写业务表。 + +## 5. High-level 架构 + +```mermaid +flowchart TB + HW["硬件
Display · Touch · Wi-Fi · Flash · Clock"] + + subgraph FW["Firmware / Pocket AgentOS Runtime"] + DRIVERS["Platform Drivers"] + HOST["App Supervisor
System App lifetime + foreground selection"] + TOOLCAT["Tool Catalog + Router"] + SCHED["Native Scheduler"] + DATA["App Data Action Runner
headless JS/TS"] + REV["Per-App revision
frame-boundary coalescing"] + CONTEXT["Context Assembler"] + QJS["Runtime Manager
多个隔离 Runtime instance"] + RENDER["PocketJS UI Core + Renderer"] + + subgraph MODS["Native Modules"] + UI["ui"] + DB["data.sqlite"] + FS["data.fs"] + NET["net.http"] + MCP["mcp.client"] + MODEL["model.stream"] + SHELL["shell.bounded"] + DEVICE["device.settings"] + end + end + + subgraph ROOT["/workspace — Pi Agent 拥有"] + AGENT["常驻 Pi Agent System App
同一个 Guest: Agent Loop + Root View"] + AGENTDB["data/agent.sqlite"] + ROOTFILES["AGENTS.md · strategy.md · memory/"] + end + + subgraph APPS["/workspace/apps/"] + APP1["Robinhood Bundle
Tools · Data Actions · Schedules · View"] + CACHE1["View projection cache
Solid signals"] + APP1DB["robinhood/data/robinhood.sqlite"] + APP2["Other App Bundle"] + APP2DB["other/data/app.sqlite"] + end + + HW --> DRIVERS + DRIVERS --> HOST + HOST --> QJS + HOST --> RENDER + HOST --> TOOLCAT + HOST --> SCHED + HOST --> DATA + + QJS -->|"一个 QuickJS Guest"| AGENT + QJS -->|"一个 QuickJS Guest"| APP1 + QJS -->|"一个 QuickJS Guest"| APP2 + MODS --> AGENT + MODS --> APP1 + MODS --> APP2 + + ROOTFILES --> CONTEXT + CONTEXT --> AGENT + AGENT --> AGENTDB + APP1 --> APP1DB + DATA --> APP1DB + DATA --> REV + REV -->|"foreground + stale"| CACHE1 + CACHE1 --> APP1 + APP2 --> APP2DB + + AGENT --> TOOLCAT + TOOLCAT --> HOST + SCHED --> HOST + AGENT -->|"Root 被选中时"| RENDER + APP1 -->|"被选中时"| RENDER + RENDER --> HW +``` + +## 6. 必须一直成立的规则 + +1. `/workspace` 的 owner 是 Pi Agent,不是某个普通 App。 +2. 普通 App 只能访问 Host 为它挂载的 data root。 +3. Pi Agent 的 Agent Loop 和 Root View 只有一个常驻 Guest;普通 App 的 View + 与 headless Data Action 可以是两个 Guest,但必须属于同一个 App runtime。 +4. 同一个 App 只有一个 SQLite owner;View 与 Data Action 的 DB ops 经该 owner + 串行化,不能各自创建会竞争同一 LittleFS 文件的 connection。 +5. 只有 `AgentWake` Schedule 才会启动模型;普通 AppTask 不启动模型。 +6. App Tool、App Schedule、UI refresh 可以共用同一个 Data Action。 +7. 每次成功 SQLite transaction 立即递增一次 App revision;revision 通知在前台 + frame boundary 合并,render tick 不轮询 SQLite。 +8. 凭据留在 native 层,不进入 App data、Agent context 或 Tool 参数。 +9. Bundle 只能调用 Host 实际挂载的 capabilities。 +10. 所有硬件差异都留在 Host 和 Native Modules 后面。 +11. Pi Agent System App 在 Supervisor 生命周期内只创建一次;切换普通 App + 只能改变 foreground View,不能替换它。 +12. 一个 host tick、View `tick()` 或 touch callback 不能等待模型、HTTP/MCP + 或 Data Action;View 只操作内存 cache,后台 Data Action 在完整 body 返回后 + 才写 SQLite。 +13. 普通 frame 只能比较内存 revision;revision 未变化、App 在后台或该 + projection cache 已是最新时,SQLite query 数量必须为零。 +14. 产品 UI 不展示 CPU、PSRAM、FPS 或 LCD refresh telemetry;这些指标没有稳定、 + 低开销且可跨硬件复用的语义。底层性能诊断只走 UART/log instrumentation。 + +## 7. Workspace 布局 + +```text +/workspace/ + AGENTS.md + strategy.md + + memory/ + INDEX.md + .md + + .pi-agent/ + schedule.json Pi Agent 自己创建的 AgentWake 状态 + .system/ + app-catalog.json 可选的 App Catalog 缓存 + + data/ + agent.sqlite Pi Agent 数据 + view/ + current 当前 Root View release id + releases/ + / + pocket.json + plan.json + agent-app.json + app.js + agent.js Pi Agent Loop bundle,仅 System App + app.pak + + apps/ + robinhood/ + current + releases/ + / + pocket.json + plan.json + agent-app.json + app.js + data-action.js 可选;headless 数据入口,不挂载 UI + app.pak + migrations.json + data/ + robinhood.sqlite + .system/ + schedules.json Robinhood AppTask 的运行游标和最近结果 + <其他 App 文件> + tmp/ + + / + ... +``` + +这是目标布局与当前布局的并集。当前 v1 已写入 fixed `builtin-v1` release、`current`、 +App SQLite 和 App-local `schedules.json`;`data/agent.sqlite`、持久 +`app-catalog.json`、`migrations.json` 仍未实现。当前 `agent-app.json.nativeServices` +保存 build-time trusted、无 secret 的 endpoint/credential-reference policy;当前 `plan.json` 也是 +`seed_builtin_releases()` 生成的最小 runtime/module 记录,不是 PocketJS resolver 的 +完整 target-specific plan。 + +Pi Agent 的挂载: + +```text +data.fs root = /workspace +data.sqlite root = /workspace/data +``` + +普通 App 的挂载: + +```text +data.fs root = /workspace/apps//data +data.sqlite root = /workspace/apps//data +``` + +`current` 是保存 active release id 的小文件,不依赖 symlink。它只能在新 +release 完成校验后通过 `data.fs` atomic replace 切换。运行中的 Runtime 绝 +不能 eval 一个只写了一半的 Bundle。 + +## 8. App contract + +概念上,一个 App 可以这样定义: + +```ts +export default defineApp({ + id: "robinhood", + tools: { + search_tools: { parameters: searchSchema, action: "searchTools" }, + call: { parameters: deferredCallSchema, action: "validatedProviderCall" }, + refresh_portfolio: { parameters: {}, action: "refreshPortfolio" }, + }, + providerOperations: checkedInRobinhoodAllowlist, + actions: { searchTools, validatedProviderCall, refreshPortfolio }, + schedules: [{ id: "portfolio-refresh", everyMinutes: 5, + action: "refreshPortfolio", args: {} }], + view: RobinhoodView, +}); +``` + +当前 artifact 对应为: + +```text +agent-app.json Public Tools、Schedules、capability metadata +data-action.js 后台网络、完整 body decode、normalize、SQLite transaction +app.js + app.pak 前台 projection cache 和固定 View +tool-catalog.json 可选的 App-owned deferred Tool catalog 源文件 +``` + +Robinhood 的 `tool-catalog.json` 会在构建 `data-action.js` 时被 bundler 内联,运行时 +由 `searchTools()` 和 `validatedProviderCall()` 读取;Rust contract test 也直接读取 +源 snapshot,检查 54 个 operation 与 `agent-app.json.providerOperations` 完全一致。 +`TOOLS.md` 只是这套契约的人类可读维护说明,不参与 build 或 runtime。 + +构建后产生两种不同性质的 metadata: + +1. PocketJS `plan.json`:目标态由 PocketJS resolver 生成 target-specific build IR, + 保存 target、HostOps ABI、viewport、resolved capabilities 和 plan hash;当前 v1 + 只播种一份最小 placeholder。 +2. Pocket Pi `agent-app.json`:当前由 App checked in 的 runtime descriptor,保存 + id/version、Public Tool schemas、provider operation allowlist、Task/Schedule names + 和 App `dataVersion`。从 `defineApp()` 静态提取和 artifact hash 字段仍是目标态, + 当前代码尚未实现。 + +不能把这两个文件混为一谈。`plan.json` 继续遵守 PocketJS platform contract; +App Supervisor 读取 `agent-app.json` 建立不需要启动 Guest 的 Tool/Schedule +catalog。 + +完整 release 的目标 metadata 至少应包含: + +- App id 和版本; +- Public Tool schemas; +- Schedule declarations; +- capability requirements; +- viewport contract; +- artifact hashes。 + +当前 build-selected embedded catalog 已能只解析 descriptor 建立 Agent Tool Catalog,但 ordinary +View 仍会在 Supervisor 启动时全部 preload;动态发现和按需 residency 尚未实现。 + +### App 内部依赖方向 + +```text +Agent Tool ─────┐ +App Schedule ───┼──> Data Action ──> Native transport ──> SQLite transaction +UI refresh ─────┘ │ + ▼ commit + App revision++ + │ + foreground frame coalesces + │ + ▼ + bounded query -> memory cache -> View +``` + +这是一条所有 App 都必须遵守的依赖方向,不是 Robinhood 特例。View 不调用网络、 +不写业务表、也不把 response 直接 render;Data Action 不持有 View state。Agent +不需要知道 App 的 SQLite 表名。 + +## 9. App Supervisor + +当前 v1 的 App Supervisor 是受信任 Host 代码,实际负责: + +- 接收 build-time App pack,并解析所选 App 的 `agent-app.json`; +- 把固定 `builtin-v1` artifacts 原子写入各 App release 目录和 `current`; +- 根据 `dataVersion` 只重建变化 App 的开发期 SQLite; +- 启动一次 Pi Agent System App,并 preload 所选 ordinary View Runtime; +- 挂载正确的 data root 和 Modules; +- 切换前台 App; +- 路由 App Tool/AppTask,并用一个 bounded Data Action queue 串行执行; +- 注册、持久化并推进 App Schedules; +- 共享每个 App 的 SQLite owner 和 revision counter。 + +扫描任意安装 release、完整 plan/hash/signature 校验、动态创建/销毁 Runtime、迁移、 +上一版本回退和 recovery UI 尚未实现,统一列在 22.2/22.3,不能从本节职责反推为 +当前能力。 + +### 常驻 System App 与前台 View + +Supervisor 持有两种不同的引用: + +```text +system: PiAgentSystemRuntime // 启动一次,始终存在 +runtimes: Map // build-selected catalog 在启动时全部 preload +active_app: Option // None 表示显示 Root View +``` + +每个 host tick 都推进常驻 `system`;普通 App 的 `tick()` 只允许执行常量时间的 +View bookkeeping,不能 poll network、写 SQLite 或重建 projection。只有被选中的 +Runtime 执行 surface render;普通 App 不接管或复制 Agent Loop。由此保证: + +1. Agent turn 跨 App navigation 保持同一 identity/context; +2. 后台 model completion 和 Tool completion 继续进入 System App; +3. Root projection 即使暂时不可见也可更新,返回时直接显示当前状态; +4. foreground App 出错或被卸载不会连带终止 Agent。 + +### 前台与 headless 执行 + +如果 App 正在打开,View Runtime 只处理触摸和缓存 render。Agent Tool、Schedule +或 UI refresh 都进入同一个 bounded Data Action queue;runner 按需加载该 App 的 +`data-action.js`,在独立 headless Guest 中顺序执行。schema DDL 只在 +`PRAGMA user_version` 不匹配时执行;正常启动不重复执行 `CREATE TABLE IF NOT EXISTS`。 + +如果 App 不在前台,已 preload 的 View Guest 不运行 projection reload。Data +Action 仍可更新该 App SQLite 并递增 revision;下次选择这个 View 时,前台 frame +只读取一次当前 bounded projection。preload 解决的是 bundle/QuickJS cold load, +不把后台 App 变成 SQLite polling loop。 + +同一个 App 可以同时有一个 cached View Guest 和一个 headless Data Action Guest, +但只能有一个 Data Action 在执行,且两者共享同一个 SQLite owner。这不是两份 App +实例,而是同一个 App runtime 的 data plane 与 view plane。 + +当前 v1 进一步让所有 App 共用一个全局串行 Data Action worker。这是有意的资源与 +执行语义取舍,不作为当前缺陷:它换取 bounded concurrency、单一 SQLite/QuickJS +资源峰值和简单 completion ordering。只有实测出现不可接受的跨 App 阻塞后,才需要 +把 worker 拆成 per-App lease 或受限 worker pool。 + +## 10. Scheduler + +Scheduler 是 Rust 持有的时钟和持久 wake store。它支持两种 target: + +```rust +enum ScheduleTarget { + AgentWake { prompt: String }, + AppTask { + app_id: String, + task: String, + args_json: String, + }, +} +``` + +### 10.1 AgentWake + +启动一个 Agent turn,保留当前 `schedule.set/list/cancel/clear` 和自动唤醒 +能力。只有需要模型判断的任务才用它。 + +### 10.2 AppTask + +直接调用 App 声明的 Task,不调用模型。Robinhood 每五分钟刷新就是 +`AppTask`。 + +### 10.3 生命周期 + +1. Supervisor 校验 Schedule 指向的 Task 确实存在。 +2. 激活 release 时,按 `(app_id, schedule_id)` reconcile Schedule。 +3. Rust Scheduler 把 `next_run_at`、cadence、Task args 和最近一次 enqueue 状态 + 写入该 App 私有的 `data/.system/schedules.json`。App bundle 中的 + `agent-app.json` 是声明源;运行状态不再集中混放在 workspace 根目录。 +4. 到期后原子 claim wake。 +5. Supervisor 把 AppTask enqueue 到该 App 的 Data Action runner,并立即推进下次 + 时间;不会在 scheduler tick 内等待网络。 +6. Data Action 自己把 running/succeeded/failed 等 domain 结果写入 App SQLite; + Scheduler 的运行游标不复制这份业务状态。 + +ESP32 默认策略: + +- 同一 App 不并发执行; +- 重启后错过多个周期只合并补跑一次; +- 连续失败不会自动唤醒 Agent; +- App 可以从自己的 SQLite 显示 stale/error 状态。 + +当前 `last_ok` 只表示 Schedule 到期时是否成功进入 Data Action queue,不表示 provider +业务最终成功。业务 completion 由 App 自己写入 SQLite,例如 Robinhood +`refresh_runs.status`;Scheduler 不复制第二份业务结果。 + +## 11. Tool Catalog 和 Tool Router + +第一版 Pi Agent 看到两类 Tools: + +```text +Native Tools + read · write · edit · find · grep · ls + bash · device.status · time.now + workspace.context + schedule.set · schedule.list · schedule.cancel · schedule.clear + +App Tools + robinhood.search_tools + robinhood.call + robinhood.refresh_portfolio + research.search · research.fetch +``` + +Workspace 动态自定义 Tools 不属于 v1。 + +### 11.1 注册 + +1. Native modules 提供自己的 Tool definitions。 +2. Supervisor 从 active `agent-app.json` 读取 namespaced App Tool schemas。 +3. Tool Catalog 检查重名和 capability availability。 +4. 合并后的 definitions 注册进 Pi Agent session。 +5. v1 中启用、停用或更改 App Tools 后 reload Agent session;不要求 live + hot-plug。 + +### 11.2 调用 + +```text +Model 产生 Tool call + -> Pi Agent Tool adapter + -> Tool Router + Native name -> CoreToolHost / Native Module + App name -> App Supervisor.enqueue_data_action(app, tool, args) + -> data-action.js -> native transport + -> 仅在 Fixed View 消费结果时写 SQLite + app.commit() + -> Data Action worker 直接返回真实 ToolResult + -> normalized ToolResult + -> Model +``` + +Public Tool 参数先由 Agent Tool layer 按 `agent-app.json` schema 校验;Robinhood +`robinhood.call` 的 deferred upstream schema 再由 Data Action 使用 +`tool-catalog.json` 校验。Tool Router 只负责 ownership、namespaced routing 和 bounded +completion wait,不声称实现通用结果截断。所有 App Tool 都进入 headless Data Action, +不在 View Guest 执行。Agent 发起的 App Tool 等待 Data Action 的真实 completion; +UI Task 和 Schedule 只需要快速 enqueue receipt。一次 Agent App Tool 从进入 Router +起只有一个 80 秒绝对 deadline;排队、Data Action、PocketJS `fetch()` 和 native MCP +共同消费剩余时间,不能各自维护另一套业务 timeout。 + +## 12. Robinhood MCP 如何接入 + +MCP 是 Robinhood App 使用的上游协议,不是 Pi Agent 看到的 App 边界。 + +### 12.1 Native `mcp.client` + +Firmware 负责: + +- HTTPS/TLS; +- OAuth credential reference; +- MCP initialize 和 session id; +- JSON-RPC/SSE framing; +- retry 和 connection reset; +- response size limit; +- secret isolation。 + +### 12.2 Robinhood Bundle + +App 负责: + +- checked in 54 个 upstream operation snapshot 与 allowlist; +- 只向 Agent 常驻暴露 `search_tools`、`call`、`refresh_portfolio` 三个小 Tool; +- 按需返回某个 upstream Tool 的描述与完整 JSON Schema,并在调用前本地校验; +- 把本地 Tool/Task 映射到 MCP call; +- normalize provider response; +- 写 Robinhood SQLite; +- 定义 freshness、error 和 history; +- 渲染 View。 + +```ts +function refreshPortfolio(args) { // 只在 headless Data Action Guest + const snapshot = services.call("mcp.client", "callTool", args); + db.exec("BEGIN IMMEDIATE"); + savePortfolio(snapshot); // 完整 body 已返回并 normalize + db.exec("COMMIT"); + app.commit(); // 只 bump revision,不触碰 View +} +``` + +上游 `tools/list` 不会自动变成 54 份常驻模型 schema。App release 明确选择允许的 +operation,并用 deferred lookup 避免 prompt 膨胀;native 再以同一份 +`providerOperations` 做精确 allowlist。认证和 session 留在 native,具体操作决策仍由 +Agent 和 Tool description 负责。 + +Robinhood 的研究、仓位、风险判断和是否执行交易属于 Agent policy;Pocket Pi native +边界只强制 credential isolation、operation allowlist 和 transport contract,不再复制 +一套独立 Trading Manager 或风险决策引擎。这是有意的 Agent/App ownership,而不是 +待修复的安全缺口。 + +### 12.3 Exa 如何接入 + +Exa 使用正式 PocketJS `fetch()` / `pocket-net` contract,而不是私有同步 +`services.call("net.http")`。Bundle 选择 Exa endpoint,native policy 再把它限制为 +两个固定 URL;API key 不进入 Bundle: + +```text +research.search / research.fetch + -> Exa private task + -> fetch("https://api.exa.ai/search" | "/contents") + -> pocket-net start(立即返回 handle) + -> native HTTPS worker(注入 x-api-key) + -> tick boundary drain / fetch Promise resolve + -> normalize response + -> search transaction 只写固定 View 消费的 searches projection + -> app.commit() 递增 Exa revision + -> 前台下一 rendered frame 最多重查一次搜索历史 projection +``` + +Firmware 注入 credential、执行 host/path allowlist、TLS、timeout 和 response +limit;Exa Bundle 拥有 endpoint 选择、请求字段、结果归一化、SQLite schema 和 +搜索历史 View。native transport 不调用 QuickJS;只有 Data Action runner 的 tick +把完成事件带回 Guest。 + +Native transport 在进入 ESP HTTP/MCP 之前必须读取一个无锁 connection-ready +状态;未完成 association/DHCP 时立即向 Data Action 返回 error,不能让断网请求 +进入 ESP-Hosted。Data Action 再按本 App 语义写 terminal failure row,View 仍只在 +commit 后重读 SQLite。 + +`connection-ready` 只代表 association + DHCP,不伪装成互联网健康检查。ESP32-P4 +hardware adapter 必须在 link 被动断开/恢复时同步更新这个状态和 Settings projection。 +当前 ESP-Hosted 实机测量表明默认 `WIFI_PS_MIN_MODEM` 会把独立 TLS/HTTP 平均耗时 +放大到约 8 秒;该 adapter 因此在 station start 后使用 `WIFI_PS_NONE`。这是板级 +transport policy,不进入 App Bundle 或 PocketJS portable module contract。TCP/TLS +瞬时失败只重试 transport;只有 provider 明确返回 stale-session 语义时才清空 MCP +session id。 + +## 13. SQLite 和 View 更新 + +SQLite 是持久数据,不是每帧 reactive engine。每个 App v1 只有一个无锁内存 +`revision: AtomicU32`,不是持久表、不是 Boolean,也不是 SQLite watch。 + +```text +Data Action View Runtime +─────────── ──────────── +network / complete body +normalize +BEGIN +write App tables +COMMIT +app.commit(): revision++ ──► foreground frame 读取一次 revision + revision == loadedRevision -> 0 SQLite query + revision != loadedRevision -> bounded query + 更新内存 cache + render cache +``` + +### 13.1 Debounce / coalescing 的准确位置 + +每次成功 transaction 都必须立即 `revision++`,不能 debounce COMMIT,也不能用 +一个容易丢事件的 dirty Boolean。debounce 发生在 **notification delivery / cache +reload**:foreground View 每个 rendered frame 最多采样一次最新 revision;两个 +frame 之间发生 1 次或 20 次 COMMIT,都只执行一次 projection reload,并把 +`loadedRevision` 直接推进到最新值。 + +Data Action 不在网络前写 durable `running` row。UI 发起的 Action 可以用内存状态 +显示正在执行;网络完整返回后,业务表和 terminal run result 在同一 transaction +落库并只递增一次 revision。这样失败或中断不会留下永久 `running` projection。 + +### 13.2 Projection cache + +每个 View projection 在内存中保存: + +```ts +type ProjectionCache = { + key: string; // 例如 account + time span + loadedRevision: number; + value: T; +}; +``` + +第一版 runtime 使用一个 App revision,View 可以有多个 cache,例如 Robinhood +的 accounts、selected portfolio、activity、positions 和 chart。切换已经加载且 +`loadedRevision` 相同的 account/span 只切内存 signal;未加载或 stale 时只查该 +bounded projection,不能重读整个数据库。 + +只有在实测证明单一 revision 导致明显无关 reload 后,才增加 topic/table revision。 +不要一开始为每张表维护 Boolean:它会引入丢更新、跨表 transaction 一致性和大量 +订阅 bookkeeping。App-level monotonic revision 是最小正确 primitive。 + +### 13.3 何时允许读 SQLite + +- View 首次激活; +- foreground View 发现 App revision 变新; +- 用户切换到尚未加载或已经 stale 的 bounded projection。 + +除此之外不读。普通 frame、动画、scroll、已经缓存的 account/span 切换,以及 +后台 App 都是零 SQLite query。View 从不因为打开页面而发起网络请求。 + +### Query 规则 + +- 列表 query 必须有 `LIMIT`、分页或聚合; +- 大型时间序列先聚合成 View 大小的窗口; +- Robinhood 1D/1W chart 固定为 20 个 time buckets;一次 indexed SQLite query + 恰好返回 20 行,View 只绘制这些 bucket 的 point projection; +- migrations 必须创建必要索引; +- provider 的模型 progress 在一次 host poll 内合并为至多一次 Guest delta,不能让 + 每个 token 触发 JS event、layout 或 flash write;当前 UART bridge 则进一步把 + provider chunks 合并为一个 final result; +- schema version 和 migrations 由 App 管理。 + +### 13.4 App 数据库保存业务事实,不保存原始 Tool payload + +Tool、Schedule 和 UI refresh 共享 Data Action implementation。每个 provider body +完整返回后,Data Action 在内存中 normalize,再明确更新对应的 domain tables。 +View 不读取原始 provider JSON,也不在前台推断上游字段结构。通用的 append-only +`tool_events`、`results_json` 和 `document_json` 不属于产品 read model。 + +Robinhood v1 schema: + +- `accounts`:账户 identity 和当前状态; +- `portfolio_current`:cash、buying power、day/week P&L 等当前字段,不含 total value; +- `total_value`:按 `account_number + observed_at` 保存折线图所需的 value history; +- `positions`:每个账户的当前持仓 rows; +- `activities`:每个账户最近的 order/activity rows; +- `refresh_runs`:只保存 terminal succeeded/partial/failed result。 + +当前 schema v5 不创建 `equity_historicals`、`pnl_trades`、`order_reviews` 或通用 +raw-response cache。54 个 upstream Tool 都把结果返回 Agent;只有 Fixed View 当前消费 +的 `get_accounts`、`get_portfolio`、`get_equity_positions`、`get_equity_orders`、 +`get_realized_pnl`,以及 equity place/cancel 返回的 activity projection 会写 SQLite。 + +Exa v1 schema: + +- `searches`:固定 View 消费的 query、时间、terminal status、result count 和 + top result title。`research.fetch` 结果直接返回 Agent,不进入 SQLite。 + +开发阶段 schema 变化由 App descriptor 的 `dataVersion` 显式触发:Supervisor 在打开 +SQLite 前只删除该 App 的 `.sqlite`,写入 App-local version marker,然后由 +该 App 的 Data Action 创建新 schema;不维护 migration/backward compatibility。 +不得删除其他 App、该 App 的其他 files、顶层 workspace、NVS Wi-Fi 或 credentials。 + +## 14. Pi Agent Root Runtime + +Pi Agent 是拥有更宽 filesystem mount 的特殊 System App,负责: + +- Agent/model loop; +- Chat 和 Tool run 展示; +- `/workspace` coding Tools; +- memory 和 strategy; +- bounded context assembly; +- AgentWake Schedules; +- 发现和调用已安装 App Tools; +- Files、Runs/Schedules、Device Settings View; +- `agent.sqlite` 中的 conversation、message、run、Tool call。 + +这是 Root Runtime 的目标职责。当前 v1 已实现 Chat、Files、Apps、Settings 与 +Chat 内的 next AgentWake 摘要;独立 Runs/Schedules View 和 `agent.sqlite` 持久化仍 +以 22.2 的未实现状态为准。 + +Agent Loop 由 System App release 中的 JavaScript `agent.js`(`pi-agent-core`) +提供,并与 `app.js` Root View eval 到同一个 PocketJS Guest。Rust 只提供 +模型 transport、Native/App Tool 路由、调度和生命周期;它不拥有第二套 Agent +state machine。Root View 与 Agent Loop 同属 `pi-agent` release/runtime,这使 +Agent 能在未来通过更新自己的 release 一起演进 context、Tools adapter 和 UI。 + +第一版不要求 Pi Agent 修改 App code、构建 Bundle 或动态增加 Tool definitions。 + +### 14.1 ESP32 支持的自我管理 + +- 读写 Agent workspace files; +- 维护 `AGENTS.md`、strategy、memory; +- 组装 bounded context; +- 创建 AgentWake Schedules; +- 使用 Native Tools 和已安装 App Tools。 + +### 14.2 ESP32 不支持 + +- 作为产品能力编辑 PocketJS 源码; +- 编译 TSX、styles、fonts 或 `.pak`; +- 修改 firmware/Rust modules; +- v1 动态加载 Tool source。 + +### 14.3 开发机能力 + +开发机可以编辑源码和构建 Root/App Bundles;该构建流程不属于 ESP32 +portable contract,也不是独立的 Pocket Pi Agent Host。 + +## 15. Context Assembler + +Context Assembler 把选定 workspace files 组装成有严格上限的 Agent context, +不是把整个文件系统全部塞进 prompt。 + +第一版输入: + +```text +固定 system identity +/workspace/AGENTS.md +/workspace/strategy.md +/workspace/memory/INDEX.md +最新或被选中的 memory notes +紧凑的 device state +``` + +规则: + +- 单文件和总 context 都有 byte ceiling; +- 非 UTF-8 和不支持的文件不进入 prompt; +- App 业务表不会自动进入 context; +- App Tool schemas 进入 Tool list,不重复进 system prompt; +- credentials 不进入 context; +- Agent 需要 App 数据时调用 App Tools。 + +## 16. Native Module 归属 + +| Module | Rust/native 负责 | Bundle 负责 | +| --- | --- | --- | +| `ui` | retained tree、layout、text、animation、DrawList | JSX View 和 reactive state | +| `data.sqlite` | SQLite、handles、limits、storage binding | schema、migrations、queries、transactions | +| `data.fs` | confinement、quota、atomic replace | App files 和 config | +| `net.http` | TLS、credential、policy allowlist、limits、non-blocking transport | `fetch()`、endpoint 选择和 domain decoding | +| `mcp.client` | auth、session、framing、limits | safe operation mapping 和 domain semantics | +| `model.stream` | provider transport、可选的内部 stream decode 和完整 result | Agent policy;wireless progress 可在 host poll 合并后进入 Guest,当前 UART bridge 则把 provider chunks 合并为一个 final result | +| `schedule.wake` | clock、persistence、claiming | AgentWake 或 AppTask declaration | +| `shell.bounded` | allowlisted device/workspace operations | Agent 决定何时调用 | +| `device.settings` | Wi-Fi、NVS、restart | Settings View | +| `app.lifecycle` | catalog、activation、Data Action queue、per-App revision 和 frame-boundary coalescing | descriptor、`app.commit()` 和 projection caches | +| `app.data` | 后台 Guest 生命周期、bounded queue、共享 DB owner | provider mapping、完整 body decode、transaction | + +Native 层拥有稀缺资源、硬件和 secrets;Bundle 拥有产品行为。 + +## 17. 板子上呈现的 UI + +任意时刻只呈现一个 Runtime 的 foreground DrawList。 + +### 17.1 Root/Home + +Pi Agent Root View 当前提供 Chat、Apps、Files、Settings,以及 Chat 内的 next +AgentWake 摘要。独立 Runs/Schedules View 尚未实现。Root View 会 +替代当前产品 UI 的 Rust `ScreenState`。产品 UI 不保留 legacy Rust 实现或 +双轨 fallback;固件只负责渲染 PocketJS DrawList 和必要的底层硬件错误日志。 + +### 17.2 App foreground + +打开 Robinhood 后,显示 Robinhood View。常驻 Pi Agent System App 继续在原 +Guest 中运行,但 Root View 暂不产生前台 DrawList。用户仍可操作 Robinhood, +再切回时 Agent session 不变。 + +### 17.3 Background update + +- Robinhood 关闭时,五分钟 AppTask 只运行 headless Data Action;完整网络 body + 返回后写 SQLite 并递增 revision,不加载或查询 View; +- Robinhood 打开时,Data Action 与 View 仍分离。成功 COMMIT 后,下一次 + foreground rendered frame 发现 revision 变化,只更新 stale bounded cache。 + +## 18. 当前 Pocket Pi 能力映射 + +最终架构不主动删除任何现有能力;当前 v1 的保留程度见第 22 节。 + +| 当前能力 | 新架构归属 | +| --- | --- | +| model request 和 streaming | `model.stream` + Pi Agent Runtime | +| OpenAI/OpenRouter/Anthropic/DeepSeek/UART adapters | Host/provider adapters | +| workspace read/write/edit/find/grep/ls | Root Agent workspace Tools | +| bounded bash | `shell.bounded` | +| `device.status` 和 `time.now` | native device/time Tools | +| `workspace.context` | Context Assembler | +| Agent `schedule.*` 和 autonomous wake | Scheduler `AgentWake` | +| Chat、Files、keyboard、message reader | Root PocketJS View | +| Wi-Fi scan/connect/forget、restart | `device.settings` + Root View | +| simulator/physical contract parity | shared runtime/module contracts | + +新增而不是现有的能力包括:App Supervisor、App Catalog、App Tools、AppTask +Schedules、revision-coalesced projection cache 和 Bundle-based Views。 + +## 19. 设备 Target、开发 Simulator 与跨硬件 + +Pocket Pi 当前有一个完整支持的硬件 composition,以及一个配套开发 simulator: + +| 角色 | Composition | 说明 | +| --- | --- | --- | +| Reference hardware | ESP32-P4 firmware | 第一台完整支持的设备;实现 LittleFS、touch/LCD、Wi-Fi/NVS 和嵌入式 limits | +| Development tool | ESP32-P4 simulator | 用 macOS adapter 验证相同 product contracts;不是桌面产品或硬件 target | + +### 19.1 “一次适配,不同硬件跑”的准确含义 + +它不表示同一个 firmware binary 或同一份 byte-identical App artifact 在所有 +硬件上运行。 + +它表示: + +1. 新硬件只需要实现一次 Host 和它承诺支持的 Native Modules。 +2. Hardware target 发布真实的 capability 和 viewport profile。 +3. 任何 requirements 被满足的 App 都不需要写 App-specific Rust/hardware code。 +4. 同一份 App 源码和业务逻辑根据 ABI、viewport、raster density 和 assets + 生成 target-specific `app.js`/`app.pak`。 + +以 Robinhood 为例,Tool mapping、SQLite schema、refresh Task 和 View source +保持相同;ESP32 与 simulator 只是在同一 specs 后面提供不同 display、 +filesystem、network 和 credential 实现。 + +### 19.2 能做到跨硬件的条件 + +- App 不直接调用 ESP-IDF、wgpu、macOS 或 raw device API; +- capability ids 和 module specs 稳定且 append-only; +- `requires` 是硬条件,可选能力使用 `enhances`; +- UI 声明支持的 viewport policy 或 target variant; +- simulator 验证 contract,physical hardware 做最终验收; +- resource ceiling 是公开 contract,不是隐藏设备事实。 + +一个新 board 完成并通过这些 modules 的 contract tests 后,兼容 App 不需要再 +做 board-specific port。 + +## 20. 这是不是 AgentOS + +从产品和 runtime 的意义上,它符合 AgentOS: + +1. Agent 是系统一级 actor,不是某个 App 里的聊天框。 +2. Agent 拥有持久 workspace 和 context。 +3. App 原生暴露语义化 Tools 给 Agent。 +4. App 同时提供人类可见的 View。 +5. 本地 Schedule 让系统能自主工作,不需要模型参与每个循环。 +6. Native target composition 统一管理 capabilities、credentials、lifecycle 和 + hardware。 +7. Tools、Tasks、State 和 View 可以作为 App 单元演进,不改 firmware。 +8. Agent 的执行生命周期独立于当前前台 View,用户和 Agent 可以并行操作同一 + 套 App platform。 + +但它不是传统通用 OS:它不提供任意进程、多用户安全、POSIX 兼容或通用 +desktop。准确说法应当是: + +> Pocket Pi 是一套面向嵌入式和专用设备的完整 Agent-native runtime:Agent 作为 +> 常驻 system actor 拥有 workspace;本地 App 通过 Agent Tools、durable state、 +> autonomous Tasks 和 human View 同时服务 Agent 与用户。ESP32-P4 是它的第一台 +> 完整支持硬件。 + +这比“Agent UI”更准确,也比“替代传统操作系统”更克制。 + +## 21. 我对这套架构的理解 + +这套架构最重要的不是“ESP32 能运行 JavaScript”,而是 Agent 和用户用两种 +接口操作同一个本地软件: + +- Pi Agent 通过 App Tools 看见 App; +- 用户通过 App View 看见 App; +- 两条路径最终汇合到同一批 Data Actions 和 SQLite State。 + +Robinhood 是最直观的例子。Rust Schedule 每五分钟触发一次,或者模型调用 +`robinhood.refresh_portfolio`,两者最终都调用同一个 `refreshPortfolio` +Data Action。它使用 native MCP/network transport,等完整 body 返回后向 +Robinhood SQLite commit 一份 snapshot,然后结束。 + +如果 View 正在打开,revision 只使 cache 失效,并在 frame boundary 合并更新; +如果 View 关闭,不发生任何 UI 查询,下次打开直接读取最新 bounded projection。 + +所以 Rust 层应该“小但强”:它拥有 clock、secret、hardware、resource limit、 +isolation 和 lifecycle。App Bundle 拥有名称、schema、provider mapping、业务 +规则、数据库形状和 UI。Pi Agent 拥有 workspace,并决定什么时候使用这些 +能力,但普通 App 的后台任务不需要模型 turn。 + +正是这个分离,让 Pocket Pi 可以从一个 Agent demo 发展成 App platform,也 +可以从一块板扩展到多种硬件,而不需要把每个新产品重新写进 firmware。 + +## 22. 当前实现状态 + +### 22.1 已实现 + +1. PocketJS 固定在 upstream `origin/main` revision + `9c809bbd047ddc75c27caa4990951a78d942477a`;Simulator 和 ESP32-P4 共用 + 正式合并的 `pocket-fs`、`pocket-db`、`pocket-mod`、`pocket-net` 和 + `pocket-ui-surface` contracts。Exa Data Action 已使用正式 `fetch()` API; + `pocket-net` 的 `start/cancel/drain` 由独立 native worker 实现,completion 只在 + Data Action tick boundary 进入 Guest。 +2. Pi Agent 位于顶层 `/workspace`;Root View release 位于 + `/workspace/data/view`,其中 `app.js` 和 `agent.js` 是同一个 System App + release;普通 App 位于 `/workspace/apps/`。 +3. App Supervisor 会 seed/校验 build-selected embedded release,并在启动时创建一次常驻的 + Pi Agent System App;当前所选 catalog 中的普通 App View Runtime 也全部在启动 + 阶段 preload。普通 View Guest 被限制在自己的 `data/` 和 `tmp/`,切换它们不会 + 替换 System App;前台导航只选择已经存在的 surface。这里没有 Marketplace、 + LRU、pinning 或 residency policy。 +4. Tool Catalog/Router 合并 Native Tools 与 namespaced App Tools;Agent Loop + 和 Root View 已挂载在同一个 PocketJS Guest。模型请求由一个常驻 worker 顺序 + 执行;ESP32 wireless backend 在该 worker 内复用同一个 HTTPS client,连接出错 + 后丢弃。模型与 Native Tool 的慢 I/O 再以 event batch 回到 Guest,因此这些 + 路径不阻塞 UI tick。 +5. AgentWake store 持久化在 `.pi-agent/schedule.json`;Robinhood 的五分钟 + AppTask store 持久化在 `apps/robinhood/data/.system/schedules.json`; + refresh 是 `AppTask`,只 enqueue Data Action,不启动模型。 +6. 每个有后台数据能力的 App release 包含可选 `data-action.js`。一个 bounded + `AppDataRunner` 顺序执行 Tool/Schedule/UI refresh;Robinhood 和 Exa View + bundles 已删除网络调用和业务表写入。 +7. 每个 App 有一个共享 `DbModule` owner。View Guest 与 Data Action Guest 使用 + 同一 connection owner,避免 ESP32 `unix-none` VFS 上两个 connection 竞争同一 + LittleFS 文件;network wait 不持有 DB mutex。 +8. 每次成功 transaction 调用 `app.commit()`,递增该 App 的 `AtomicU32` + revision。只有 foreground rendered frame 会比较 revision;连续多个 commit + 合并成一次 `dataChanged`。普通 frame 与后台 App 不读 SQLite。 +9. Robinhood Data Action 拥有 54 个 operation 的 checked-in deferred catalog、schema + validation、MCP operation mapping、完整 response decode 和单次 refresh transaction; + Pi Agent 常驻只看到 `search_tools`、`call`、`refresh_portfolio` 三个 Tool。每个 + provider 结果都返回 Agent,但只有 Fixed View 消费的数据才更新 + domain table,不保存原始 Tool payload。View 只拥有 accounts/portfolio/positions/ + activity/chart 的 bounded projection 和 cache。chart 的 1D/1W 窗口固定为 20 个 + time buckets,SQLite query 最多返回 20 行,render 不读取 DB。 +10. Exa Data Action 拥有 `research.search`、`research.fetch`、PocketJS `fetch()` + mapping 与 search-history SQLite transaction;native 只允许 Exa 的 `/search`、 + `/contents` 并注入 API key。View 从 `searches` 先读取最新 10 条搜索历史,滚动 + 到边界后再按 10 条增量读取;每次 provider search 使用 Exa 标准的最多 10 条 + 结果,fetch 不落库。 +11. Root View 已提供 Chat、App 入口、Files、Settings 与 next AgentWake 摘要和 + 屏幕键盘;Agent policy/loop 在 JS,workspace/App Tools 的受限底层实现和 + AgentWake 由 Rust host 提供。Pocket Pi 的小型 shared Design System inventory + 单独记录在 `docs/pocket-pi-design-system.md`;它只包含 PocketJS 上的基础 + typography/recipes/components,不包含 App-specific View 或 native UI logic。 +12. ESP32-P4 使用 4 KiB PSRAM launcher,待 ESP-IDF entry task 退出后创建 64 + KiB internal AgentOS runtime stack;App bundle 构建后会 minify 以降低固件 + footprint,但不会把 minify 当作 stack isolation。App Data pthread 使用 128 + KiB PSRAM stack,并把 PSRAM allocation caps 继承给按需创建的 96 KiB NET + worker;常驻 model worker 使用 64 KiB PSRAM stack。System App 的其他线程仍 + 使用恢复后的 platform default。 +13. AgentOS 核心 contract tests 已证明:Agent turn 进行中打开 Robinhood,仍能收到完整回复 + 和 `agent_end`;普通 App 在前台时 Agent 仍能路由另一 App 的 Tool、写 SQLite + 并完成 turn。revision contract + test 证明 3 次 commit 在下一前台 frame 只 reload 一次,5 个普通 frame 不 + reload,后台 2 次 commit 在重新打开时只 reload 一次。 +14. Simulator 的核心 Data Action tests 已证明:Exa search 用一次 transaction 写入 + `searches` View projection;Robinhood 完整 fixture refresh 用一次 + transaction 写入各业务表与 terminal `refresh_runs`,形成固定 View 所需的 + projection;provider failure 只写 failed refresh run,不写入业务 projection。 +15. `legacy_main()`、Rust `ScreenState` product UI 和其专属 display path 已删除, + 固件没有保留旧 UI 或死代码。 +16. 删除全部 legacy Rust UI 后的基线固件已刷入 ESP32-P4;实机从 System App + release 的 `agent.js` 在 Root Guest 中启动 Agent,并经 UART Codex backend + 完成 prompt 和 `agent_end`。当前 embedded presentation contract 已进一步改为: + provider 可以在 host/transport 内部 stream。wireless host 会在每次 poll 内合并 + progress;当前实机 UART presentation 则在 bridge 内部合并 409 个 provider + chunks、615 个字符后只发送一个 final result;完成后继续观察 30 秒没有 task + watchdog。 +17. ESP32-P4 的 PocketJS View 已接入真实 PPA backend,矩形填充、A8 字形混合和 + SRM 图像转换不再走全屏 CPU software fallback;每个 triple-buffer framebuffer + 保留独立 incremental render state。实机启动日志已确认 + `RGB565 backend ready: FILL + A8 BLEND + SRM`。 +18. ESP32 UI owner 在运行期不再调用 `heap_caps_get_info`;此前该 API 在 TLS 使用 + 后扫描碎片化 PSRAM 会长期持有全局 heap lock,使 UI 卡住并产生蓝屏式 watchdog + dump。产品 UI 已删除全部 CPU、内存、FPS 和 LCD 状态展示,相关问题只通过 + UART/log diagnostics 观察。 +19. Host 不再无条件以 60Hz 重建所有 PocketJS View。只有当前 foreground View + dirty 时才执行 UI `frame()`、更新 retained DrawList 和提交 panel render。 + ESP32 owner 是原生 FreeRTOS task,主循环必须用 + `vTaskDelay` 明确让出 CPU,不能用 pthread 语义的 `std::thread::sleep` 代替; + 否则即使总 CPU/内存数字不高,CPU0 idle task 仍可能无法喂 watchdog。固件把 + FreeRTOS 固定为 100 Hz,因此 scheduler tick 周期是 10 ms;所有等待至少用 + `vTaskDelay(1)` 跨过一个调度点。App deadline 使用 monotonic `Instant` 计时, + 不改变 scheduler tick,也不创建亚 tick 轮询。MCP + `EAGAIN` 重试必须先让出一个 tick,并限制重复日志。ESP32 Router 不使用受 + wall-clock 校准影响的 pthread timed condition wait;它每 tick 检查一次 result + channel 和 monotonic deadline。 +20. 当前自动化只保留 Tool catalog、安全边界、App state ownership、Data Action + transaction、resident Agent lifecycle、Tool routing 和 revision coalescing 等核心 + contract;不保留 UI 坐标、按压视觉状态或重复 smoke tests。对应实现已通过 + ESP32-P4 release cross-build,并曾刷入实体板。稳态冷启动测得 Root、Exa、 + Robinhood View preload 分别约 2.0、2.8、3.3 秒,约 20.5 秒进入完整 UI;没有 + watchdog 或蓝屏。`dataVersion` reset 只发生一次,正常启动不重复 DDL。 +21. 实机断网 Tool 验证已证明:Agent 调用 `research.search` 后,Data Action 在 + native connection-ready gate 处 fail-fast,写一条 terminal error search,随后 + 删除不安全的 30 秒 ESP-Hosted 自动 reconnect 后,连续观察 120 秒没有 assertion + 或重启;用户仍可从 Settings 明确发起重连。 +22. Exa schema v5 只保存固定 View 消费的 bounded search-history row:query、时间、 + terminal status、result count、top title 和 error;provider 原始 JSON、其余结果 + 和 fetched document 都只返回 Agent,不进入 SQLite。每次 search transaction + 删除 7 天前的 searches;不自动执行 `VACUUM`。 +23. 正式 `pocket-net` 路径曾在 schema v4 镜像的实体 ESP32-P4 上完成 Exa provider + success、SQLite 持久化、重启恢复和 30 秒稳定性观察。schema v5 只收窄 App-owned + projection,不改变已验证的 `/search`、`/contents` transport contract。 +24. UART bridge 现在默认复用 Keychain 中已有的 Robinhood OAuth session,并只把 + access token 注入板子的 RAM-only boot config;`--provision-robinhood` 只在没有 + saved authorization 时交互补录。未传该 flag 的实机 `get_accounts` 已完成 MCP + initialize 与 provider HTTP 200,收到 7222-byte body,不再出现 + `OAuth token not provided`。 +25. Agent 发起 App Tool 时,`RoutedToolHost` 与 Data Action 共用从 Router 创建的 + 80 秒绝对 deadline;worker 直接把真实结果返回等待方,不再经过 Supervisor frame + 转发,也不把 queued receipt 当作模型结果。PocketJS HTTP 和 native + MCP 都只能使用剩余 budget,超时的 Data Action Guest 会被丢弃,避免 pending + Promise 在后续调用中恢复。Exa 和 Robinhood 的 provider payload 都只放进 + ToolResult `text`,不再同时复制到 `details`。 +26. 最新实机链路已完成 Exa search → fetch → DeepSeek 最终总结;模型 worker 内复用 + HTTPS client 后,该轮没有 TLS `-0x3000`、pthread 创建失败或重启。大 Tool Result + 同步进入 QuickJS context 时仍出现 task-watchdog warning,所以持续无人值守稳定性 + 不能视为已经闭合。 + +### 22.2 待补齐,不能视为已实现 + +- Root Files 已有只读文件阅读器;conversation/message/run/tool call 还没有落入 + `data/agent.sqlite`。 +- release 已有 descriptor/`pocket.json` 校验和 atomic `current` 写入,但由 + PocketJS resolver 生成并校验真实 `plan.json`、完整 artifact hash、migration + transaction、上一版本回退和独立 recovery UI 还没有完成。 +- 当前唯一完整支持的硬件 target 是 physical ESP32-P4;配套 simulator 只证明共享 + product contract,不能计作第二个硬件实现,最终验收仍以实体设备为准。 +- build-selected catalog 的普通 App View 现在全部在 Supervisor 启动时 preload;真实 + ESP32-P4 启动时长已量测,但持续切换仍需人工验收。后续如果 catalog + 扩大,再依据实测在 PocketJS runtime 层设计加载策略,不能先引入 Marketplace、 + LRU/residency policy,也不能把 App UI/数据逻辑写回 Rust。 +- Robinhood OAuth grant 与 Exa key 都由 Mac Keychain 复用,并只在本次 UART + boot config 中以内存态注入;credential 不进入 App DB、workspace 或 View。 + 当前 normalized schema 已分别取得 Exa search 和 Robinhood get_accounts 的实机 + provider success。 + `agent.robinhood.com` 从当前 AP 建连仍有波动,失败轮次会写 error batch,但不会 + 覆盖最后一次成功的 portfolio projection。 +- 实机已验证后台 Agent turn,但“turn 运行中连续触摸切换 Robinhood/Exa 再返回” + 仍需人工操作验收;自动 lifecycle/tool-routing test 已覆盖同一状态机路径。 +- 通用 Data Action runner 已在当前实机验证 FreeRTOS pthread stack、断网 + failure transaction、SQLite dump、network fail-fast 与 provider success;持续 + 触摸切换和成功/失败交替 retry 仍待验证。 +- v1 是一个 App-level revision。Robinhood 已将 DB read 限制在 initial/ + `dataChanged` 和 account/span 的 bounded cache miss,但所有 projection cache 的 + `loadedRevision` 仍需继续显式化,随后再用板上 query 计数确认交互路径为零重读。 + +### 22.3 日后扩展项目:Marketplace / Distribution + +当前 v1 使用 build-selected App pack,release id 仍为 `builtin-v1`,启动时把所选 artifact 播种到 `/workspace` 并 +preload 全部普通 View。Marketplace 是独立的后续项目,不属于当前 runtime 完成度。 +启动该项目时按下面的依赖顺序扩展: + +1. 定义完整 release manifest:真实 PocketJS `plan.json`、artifact hashes、签名、 + publisher identity、capability requirements 和兼容版本。 +2. 实现 staging install、完整校验、atomic activation、上一版本 rollback 和最小 + recovery UI;损坏或未授权 Bundle 不能成为 `current`。 +3. 为 `dataVersion` 增加 App-owned migration transaction、失败恢复和 downgrade + policy,替代开发期直接删除单个 SQLite 文件的策略。 +4. 增加用户可见的 capability approval,特别是 credential-backed provider、network、 + schedule、device 和 workspace scope;凭据本身仍不得进入 App Bundle。 +5. 在安装/启停后重建 Tool Catalog,并先采用 Agent session reload;只有真实需求出现 + 后再实现 live Tool hot-plug。 +6. catalog 规模扩大并取得板上内存/启动时长证据后,再设计 lazy load、pinning、LRU + 或 residency policy,不提前把这些策略写进 v1。 +7. 最后补 distribution index、版本 channel、更新策略,以及 Simulator/ESP32 + 同一 package 的兼容性验证。 + +仍明确延后:ESP32 source editing、Agent-authored Tools、通用 live Tool hot-plug,以及 +不经编译的 declarative View schema。 + +## 23. 验收标准 + +1. 当前 Agent workspace、model、Settings、Agent Schedule 全部继续工作。 +2. Robinhood Tool definitions 能进入 Pi Agent,但不编译进通用 Pocket Pi core。 +3. Agent Tool 和五分钟 App Schedule 调用同一个 refresh Task。 +4. App Schedule path 不发起模型请求。 +5. 每次成功 transaction 都递增 revision;同一 frame 间隔内的多次递增只让打开 + 的 View reload 一次,revision 不变的普通 frame 是零 SQLite query。 +6. 后台 App 不执行 View query;重新选择时只在 revision stale 时读取一次 bounded + projection,并立即显示当前数据。 +7. 普通 App 无法读取另一个 App 的 data root。 +8. Pi Agent 可以读取和管理顶层 `/workspace`。 +9. 重启后 App 数据保留,错过的 recurring run 按规则合并一次。 +10. 当前 v1 在 Tool schema 非法、capability 缺失或 embedded Bundle 损坏时 fail + closed;`dataVersion` 变化只重建对应 App SQLite。migration recovery 和保留上一 + 个合法 release 属于 22.3 的 Marketplace 扩展验收,不冒充当前能力。 +11. 同一份 Robinhood source 通过 simulator contract tests,并用对应 target + artifacts 在真实 ESP32-P4 上运行。 +12. Agent turn 进行中可以操作键盘、打开 Robinhood/Exa、在 App 内触摸交互并 + 返回 Root;Agent 不重启,pending turn 正常完成,conversation/context 保留。 +13. 切换 foreground 前后 `pi-agent` Guest identity 和 boot count 不变;普通 + App failure 不得终止 System App。 +14. 网络失败、超时或 malformed body 只能更新 App 的失败状态,不能阻塞 View、 + 触发 View 网络重试或让 revision delivery 进入无限 retry loop。 + +## References + +- [PocketJS core concepts](https://pocketjs.dev/docs/concepts/) +- [PocketJS platform contracts](https://pocketjs.dev/docs/platform-contracts/) +- [PocketJS DB module PR #231](https://github.com/pocket-stack/pocketjs/pull/231) +- [PocketJS FS module PR #238](https://github.com/pocket-stack/pocketjs/pull/238) +- [`ARCHITECTURE.md`](../ARCHITECTURE.md):当前已实现的 Pocket Pi profiles 和 + Host ownership diff --git a/docs/esp32-p4-port.md b/docs/esp32-p4-port.md index 2f3527f..cd89887 100644 --- a/docs/esp32-p4-port.md +++ b/docs/esp32-p4-port.md @@ -1,26 +1,29 @@ -# ESP32-P4 host +# ESP32-P4 reference target -Pocket Pi has two agent profiles and three hosts: +ESP32-P4 is Pocket Pi's first fully supported hardware target and current +reference implementation. It demonstrates the complete device runtime: +resident Pi Agent, `/workspace`, native tools, schedules, Agent-native Apps, +local state, PocketJS UI and native device lifecycle. -| Target | Agent profile | Runs on | +The repository also provides one companion development composition: + +| Role | Implementation | Runs on | |---|---|---| -| `macos` | full `pi-coding-agent` | macOS | -| `esp32-p4` | bounded `pi-agent-core` | ESP32-P4 | -| `esp32-p4-sim` | bounded `pi-agent-core` | macOS | +| Supported hardware target | `firmware/esp32-p4` | ESP32-P4 | +| Product-contract simulator | `hosts/esp32-p4-sim` | macOS development host | -The physical host and simulator use the same -`pocket-pi-device-ui` crate: one 720x1280 draw list, one set of fonts, and one -touch hit map. Simulator mouse clicks are converted to physical panel -coordinates and dispatched through the same `ScreenState::handle_tap` method. -The shared embedded UI displays Chat, Files and Settings. Settings is not part -of the normal macOS host. +The physical host and simulator use the same PocketJS App bundles and +`pocket-pi-agentos` supervisor at a 720x1280 logical viewport. Simulator mouse +clicks and physical touch coordinates are both dispatched to the selected App +View. The Pi Agent Root View displays Chat, Apps, Files and Settings; there is +no parallel Rust product UI. The simulator is not a desktop Pocket Pi product, +a generic Agent harness or a second supported target. The physical host selects `UartBackend` for a Mac Codex/Claude Code bridge or -`WirelessBackend` for direct OpenAI/OpenRouter/Anthropic HTTPS. These remain +`WirelessBackend` for direct OpenAI/OpenRouter/Anthropic/DeepSeek HTTPS. These remain host adapters; they do not belong in the UI or embedded Agent core. ```sh -cargo xtask build macos cargo xtask build esp32-p4 cargo xtask build esp32-p4-sim cargo xtask run esp32-p4-sim @@ -46,21 +49,23 @@ OPENROUTER_API_KEY=... cargo xtask run esp32-p4-sim \ --backend openrouter --model openai/gpt-5.6 ANTHROPIC_API_KEY=... cargo xtask run esp32-p4-sim \ --backend anthropic --model claude-sonnet-4-6 +DEEPSEEK_API_KEY=... DEEPSEEK_THINKING_LEVEL=xhigh \ + cargo xtask run esp32-p4-sim --backend deepseek # Real Agent -> native write tool -> simulated LittleFS workspace cargo xtask run esp32-p4-sim --backend codex \ --workspace target/esp32-workspace ``` -The Mac simulator and physical firmware register the same core tool contracts: +The development simulator and physical firmware register the same core tool contracts: `read/write/edit/find/grep/ls`, bounded `bash`, `device.status`, `time.now`, `workspace.context`, and the four `schedule.*` operations. The Pi runtime obtains these definitions directly from the executable tool registry, so advertised and executable tools cannot drift. -The simulator is a contract-level product simulator, not a CPU or peripheral +The simulator is a contract-level development tool, not a CPU or peripheral emulator. It must exercise the embedded Agent, tools, workspace, schedules and -plugin contracts, but may use simpler macOS adapters. ESP-IDF, PSRAM, PPA, +App contracts, but may use simpler macOS adapters. ESP-IDF, PSRAM, PPA, LittleFS capacity, MIPI-DSI, touch-controller and Wi-Fi/NVS behavior require a physical-board test. @@ -69,7 +74,8 @@ physical-board test. The thin bridge CLI provisions the boot-time model choice and routes framed model decisions. Its `uart_bridge` adapters reuse a logged-in Codex Coding Plan through the persistent Codex app-server, or Claude Code through `stream-json`. -Both paths forward real text deltas instead of waiting for the whole reply. +Both paths consume provider deltas internally, then coalesce them into one +final `PPI-RPC-STREAM` result for the current device-side `UartBackend`. Wi-Fi can be selected later from the on-device Settings page. ```sh @@ -92,9 +98,9 @@ UART provisioning also seeds the board clock from the Mac for development. Standalone operation uses ESP-IDF SNTP after Wi-Fi connects. Both feed the same native time and persistent schedule implementation. -## Shared UI boundary +## App UI boundary -`pocket-pi-device-ui` owns rendering, interaction state and the portable -workspace browser. Each host supplies a mounted workspace root and its model -adapter. External applications own their UI adapters, provider clients and -credentials; Pocket Pi core contains no Robinhood or Exa domain code. +`apps/pi-agent`, `apps/robinhood`, and `apps/exa` own their PocketJS Views. +`pocket-pi-agentos` selects the foreground View and keeps the Pi Agent System +App resident. Each host supplies the mounted workspace, capabilities, model +adapter, and renderer; product UI and domain logic do not live in Rust firmware. diff --git a/docs/pocket-pi-design-system.md b/docs/pocket-pi-design-system.md new file mode 100644 index 0000000..0003c1b --- /dev/null +++ b/docs/pocket-pi-design-system.md @@ -0,0 +1,58 @@ +# Pi Design component inventory + +状态:v0.2。Pi Design 是 Pocket Pi 的全局视觉语言;内置 App 不再各自维护一套 +Header、按钮、空状态或字号层级。 + +实现位于 `apps/_shared/ui.tsx` 和 `apps/_shared/text.ts`。共享层只能依赖 +PocketJS framework primitives,不能调用 ESP-IDF、Pocket Pi Rust 私有接口、 +网络、SQLite 或 App navigation。所有业务数据和 side effect 仍由 App 提供。 + +## Foundations + +| Foundation | 包含 | 当前规则 | +| --- | --- | --- | +| type hierarchy | app title、page title、heading、label、body、caption | 只保留 shared components 实际消费的 class recipe;ESP 主阅读文本由 14/16px 提升到 16/18px,24px title 保持不变 | +| spacing | 24px screen gutter、card padding、stack gap | 直接写在当前 concrete component recipe 中,不导出无人消费的 token map | +| surfaces | card、selected card、row、muted、shell recipes | arbitrary children 的容器保持 literal View;当前没有单独的 unused surface registry | +| `statusBadge` | neutral、info、success、warning、danger | 统一背景色、文字色和状态语义;保留为当前 shared component recipe | +| dynamic glyphs | curly quotes、en/em dash、ellipsis、bullet | 进入现有 Inter subset atlas | + +PocketJS 当前只提供 12 / 14 / 16 / 18 / 20 / 24 / 36px 的 baked font +slots。Pi Design 的“稍微放大”因此采用相邻 slot:caption/label 14→16px, +body 16→18px;不会引入 runtime font loader 或第二套字体。 + +## Components + +| Component | 包含什么 | 不包含什么 | 当前使用者 | +| --- | --- | --- | --- | +| `PocketHeader` | 112px shell header、back/status affordance、App title、两行 metadata | navigation side effect、App data loading | Pi Agent、Exa、Robinhood | +| `PageIntro` | eyebrow、page title、一行说明 | 页面 query、filter、scroll | Exa | +| `SectionHeading` | section title、optional detail、optional `VIEW ALL` affordance | list data、tap behavior | Robinhood | +| `ActionButton` | primary/neutral/danger/disabled visual state、统一 18px label | action execution、loading state machine | Pi Agent、Robinhood | +| `statusBadge` recipe | 短 label 的 neutral/info/success/warning/danger surface + text classes | 长状态文案、业务判断 | Exa result、Robinhood account status | +| `EmptyState` | optional icon、title、detail、regular/compact layout | empty 条件、retry action | Exa、Robinhood | +| `MetricCard` | metric label、formatted value、optional positive/negative tone | 数值计算、currency formatting、time range | Robinhood value/cash/buying power | +| `StatusBar` | 单行 runtime status、neutral/error tone、light/dark surface | log history、progress model、retry | Exa SQLite state、Robinhood refresh state | +| `ScrollButtons` | UP/DN controls 的统一文字和视觉 | offset、pagination、tap hit testing | Pi Agent、Exa、Robinhood | +| `wrapLines` / `wrapPreview` / `wrapTextPage` | `measureText` 缓存、显式换行、preview ellipsis、按字符游标只物化可见文件页 | rich text、Markdown、scroll state | Pi Agent dynamic text / Files viewer | + +## App-owned components + +这些组件仍含明确业务语义,不进入 Pi Design: + +- Pi Agent conversation turn、Bottom Navigation、keyboard、Files row、App row; +- Robinhood 20-point chart、time-range selector、account picker、activity row、 + position row、P&L projection; +- Exa search-history query、result projection 和 retention state。 + +如果一个模式仅在一个 App 中出现,或需要知道 Robinhood account / Exa result +schema,它先留在 App。只有语义稳定、可复用且完全建立在 PocketJS public UI +contract 上,才进入 Pi Design。 + +## Evolution rules + +1. 每次新增、删除或改变 component/token,都同步更新本 inventory 和 consumers。 +2. 共享 component 是纯展示函数:props in,native View/Text tree out。 +3. App 自己决定数据、loading/error 条件、tap hitbox、navigation 和 side effects。 +4. v1 仍由 TS/TSX 编译进各 App 的 `app.js` / `app.pak`;Marketplace/SDK 出现后 + 再提取为版本化 package,不预先实现 registry 或 runtime download。 diff --git a/firmware/esp32-p4/.cargo/config.toml b/firmware/esp32-p4/.cargo/config.toml index 659bcb2..d89bb60 100644 --- a/firmware/esp32-p4/.cargo/config.toml +++ b/firmware/esp32-p4/.cargo/config.toml @@ -16,3 +16,4 @@ ESP_IDF_TOOLS_INSTALL_DIR = "workspace" CC_riscv32imafc_esp_espidf = { value = "tools/esp32p4-cc", relative = true } AR_riscv32imafc_esp_espidf = { value = "tools/esp32p4-ar", relative = true } CFLAGS_riscv32imafc_esp_espidf = "-mabi=ilp32f -march=rv32imafc_zicsr_zifencei_xesppie -fno-pic -fno-PIC -Wno-error=incompatible-pointer-types" +LIBSQLITE3_FLAGS = "-DSQLITE_TEMP_STORE=3 -DSQLITE_DEFAULT_MEMSTATUS=0 -DSQLITE_MAX_MMAP_SIZE=0 -DSQLITE_OMIT_WAL -DSQLITE_OMIT_LOAD_EXTENSION -Dlstat=stat" diff --git a/firmware/esp32-p4/Cargo.lock b/firmware/esp32-p4/Cargo.lock index 211fda6..1dc5eee 100644 --- a/firmware/esp32-p4/Cargo.lock +++ b/firmware/esp32-p4/Cargo.lock @@ -68,6 +68,12 @@ version = "1.5.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f2032f911046de80f0a198e0901378627c33f59ea0ac00e363d481118bd70a53" +[[package]] +name = "base64" +version = "0.23.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ac07cdecf99051d9a5238b80f35af32cdeba5b336e55d957b318b50137e18da5" + [[package]] name = "bindgen" version = "0.71.1" @@ -643,6 +649,18 @@ dependencies = [ "which", ] +[[package]] +name = "fallible-iterator" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2acce4a10f12dc2fb14a218589d4f1f62ef011b2d0cc4b3cb1bba8e94da14649" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "fastrand" version = "2.5.0" @@ -768,6 +786,12 @@ dependencies = [ "walkdir", ] +[[package]] +name = "grid" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "b40ca9252762c466af32d0b1002e91e4e1bc5398f77455e55474deb466355ff5" + [[package]] name = "hash32" version = "0.3.1" @@ -777,6 +801,15 @@ dependencies = [ "byteorder", ] +[[package]] +name = "hashbrown" +version = "0.16.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "841d1cc9bed7f9236f321df977030373f4a4163ae1a7dbfe1a51a2c1a51d9100" +dependencies = [ + "foldhash", +] + [[package]] name = "hashbrown" version = "0.17.1" @@ -788,6 +821,15 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashlink" +version = "0.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "32069d97bb81e38fa67eab65e3393bf804bb85969f2bc06bf13f64aef5aba248" +dependencies = [ + "hashbrown 0.17.1", +] + [[package]] name = "heapless" version = "0.8.0" @@ -883,7 +925,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d466e9454f08e4a911e14806c24e16fba1b4c121d1ea474396f396069cf949d9" dependencies = [ "equivalent", - "hashbrown", + "hashbrown 0.17.1", ] [[package]] @@ -943,6 +985,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "libsqlite3-sys" +version = "0.38.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f6c19a05435c21ac299d71b6a9c13db3e3f47c520517d58990a462a1397a61db" +dependencies = [ + "cc", + "pkg-config", + "vcpkg", +] + [[package]] name = "linux-raw-sys" version = "0.4.15" @@ -1063,17 +1116,90 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a89322df9ebe1c1578d689c92318e070967d1042b512afbe49518723f4e6d5cd" [[package]] -name = "pocket-pi-device-ui" +name = "pkg-config" +version = "0.3.33" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "19f132c84eca552bf34cab8ec81f1c1dcc229b811638f9d283dceabe58c5569e" + +[[package]] +name = "pocket-db" version = "0.1.0" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ - "pocket-pi-protocols", + "anyhow", + "base64", + "pocket-mod", + "pocketjs-core", + "rusqlite", + "serde_json", +] + +[[package]] +name = "pocket-fs" +version = "0.1.0" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" +dependencies = [ + "anyhow", + "base64", + "pocket-mod", "pocketjs-core", + "serde_json", +] + +[[package]] +name = "pocket-mod" +version = "0.1.0" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" +dependencies = [ + "anyhow", + "log", + "pocketjs-core", + "rquickjs", +] + +[[package]] +name = "pocket-net" +version = "0.1.0" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" +dependencies = [ + "anyhow", + "pocket-mod", + "pocketjs-core", + "serde", + "serde_json", +] + +[[package]] +name = "pocket-pi-agentos" +version = "0.1.0" +dependencies = [ + "anyhow", + "log", + "pocket-db", + "pocket-fs", + "pocket-mod", + "pocket-net", + "pocket-pi-embedded", + "pocket-ui-surface", + "pocketjs-core", + "serde", + "serde_json", +] + +[[package]] +name = "pocket-pi-app-pack" +version = "0.1.0" +dependencies = [ + "anyhow", + "pocket-pi-agentos", ] [[package]] name = "pocket-pi-embedded" version = "0.1.0" dependencies = [ + "pocket-mod", + "pocket-pi-protocols", "rquickjs", "serde_json", ] @@ -1087,11 +1213,11 @@ dependencies = [ "embuild", "esp-idf-svc", "log", - "pocket-pi-device-ui", + "pocket-pi-agentos", + "pocket-pi-app-pack", "pocket-pi-embedded", "pocket-pi-protocols", "pocket-pi-tools", - "pocketjs-core", "pocketjs-esp32p4-ppa", "serde_json", ] @@ -1115,10 +1241,21 @@ dependencies = [ "serde_json", ] +[[package]] +name = "pocket-ui-surface" +version = "0.1.0" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" +dependencies = [ + "anyhow", + "log", + "pocket-mod", + "pocketjs-core", +] + [[package]] name = "pocketjs-core" version = "0.1.0" -source = "git+https://github.com/pocket-stack/pocketjs.git?rev=4c5dc9ef1dd26e6f49b036c210931d399f2b52b2#4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ "taffy", ] @@ -1126,7 +1263,7 @@ dependencies = [ [[package]] name = "pocketjs-esp32p4-ppa" version = "0.1.0" -source = "git+https://github.com/pocket-stack/pocketjs.git?rev=4c5dc9ef1dd26e6f49b036c210931d399f2b52b2#4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" +source = "git+https://github.com/pocket-stack/pocketjs.git?rev=9c809bbd047ddc75c27caa4990951a78d942477a#9c809bbd047ddc75c27caa4990951a78d942477a" dependencies = [ "pocketjs-core", ] @@ -1242,7 +1379,7 @@ version = "0.12.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "16e4f499ac5b943d97ee6dbc44f23c2c10426f420f7d2f1793d6318911b6608c" dependencies = [ - "hashbrown", + "hashbrown 0.17.1", "relative-path", "rquickjs-sys", ] @@ -1274,6 +1411,31 @@ dependencies = [ "cc", ] +[[package]] +name = "rsqlite-vfs" +version = "0.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c51c9ae4df8a7fba42103df5c621fa3c37eccf3a3c650879e90fc48b11cc192c" +dependencies = [ + "hashbrown 0.16.1", + "thiserror 2.0.19", +] + +[[package]] +name = "rusqlite" +version = "0.40.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "11438310b19e3109b6446c33d1ed5e889428cf2e278407bc7896bc4aaea43323" +dependencies = [ + "bitflags 2.13.1", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "smallvec", + "sqlite-wasm-rs", +] + [[package]] name = "rustc-hash" version = "2.1.3" @@ -1401,6 +1563,24 @@ dependencies = [ "version_check", ] +[[package]] +name = "smallvec" +version = "1.15.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8ed6a63f02c8539c91a8685a86f4099661ba3da017932f6ebbea6de3f0fa7c90" + +[[package]] +name = "sqlite-wasm-rs" +version = "0.5.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc3efc0da82635d7e1ced0053bbbfa8c7ab9645d0bf36ceb4f7127bb85315d75" +dependencies = [ + "cc", + "js-sys", + "rsqlite-vfs", + "wasm-bindgen", +] + [[package]] name = "stable_deref_trait" version = "1.2.1" @@ -1490,6 +1670,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "dfde4e2f8595f222ceaae1fb16b4963952e9b33e358869dc4cd6316b0e0790cd" dependencies = [ "arrayvec", + "grid", "serde", "slotmap", ] @@ -1604,6 +1785,12 @@ version = "0.2.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ebc1c04c71510c7f702b52b7c350734c9ff1295c464a03335b00bb84fc54f853" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/firmware/esp32-p4/Cargo.toml b/firmware/esp32-p4/Cargo.toml index 73d463c..9cd4fa1 100644 --- a/firmware/esp32-p4/Cargo.toml +++ b/firmware/esp32-p4/Cargo.toml @@ -25,14 +25,14 @@ opt-level = "z" anyhow = "1" log = "0.4" serde_json = "1" -pocket-pi-device-ui = { path = "../../crates/pocket-pi-device-ui" } +pocket-pi-agentos = { path = "../../crates/pocket-pi-agentos" } +pocket-pi-app-pack = { path = "../../crates/pocket-pi-app-pack" } pocket-pi-embedded = { path = "../../crates/pocket-pi-embedded" } pocket-pi-protocols = { path = "../../crates/pocket-pi-protocols" } pocket-pi-tools = { path = "../../crates/pocket-pi-tools" } esp-idf-svc = { version = "0.52.1", features = ["critical-section"] } embedded-svc = "0.29" -pocketjs-core = { git = "https://github.com/pocket-stack/pocketjs.git", rev = "4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" } -pocketjs-esp32p4-ppa = { git = "https://github.com/pocket-stack/pocketjs.git", rev = "4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" } +pocketjs-esp32p4-ppa = { git = "https://github.com/pocket-stack/pocketjs.git", rev = "9c809bbd047ddc75c27caa4990951a78d942477a", features = ["esp-idf"] } [build-dependencies] embuild = "0.33" @@ -41,6 +41,12 @@ embuild = "0.33" component_dirs = ["components/esp32_p4_wifi6_touch_lcd_5"] bindings_header = "src/waveshare_bindings.h" +[[package.metadata.esp-idf-sys.extra_components]] +component_dirs = ["components/pocketjs_ppa"] + +[[package.metadata.esp-idf-sys.extra_components]] +component_dirs = ["components/esp_hosted_late_response_fix"] + [[package.metadata.esp-idf-sys.extra_components]] remote_component = { name = "espressif/esp_wifi_remote", version = "0.14.*" } diff --git a/firmware/esp32-p4/components/esp32_p4_wifi6_touch_lcd_5/pi_p4_touch_bridge.c b/firmware/esp32-p4/components/esp32_p4_wifi6_touch_lcd_5/pi_p4_touch_bridge.c index 9ea508c..2508420 100644 --- a/firmware/esp32-p4/components/esp32_p4_wifi6_touch_lcd_5/pi_p4_touch_bridge.c +++ b/firmware/esp32-p4/components/esp32_p4_wifi6_touch_lcd_5/pi_p4_touch_bridge.c @@ -1,14 +1,11 @@ #include #include -#include #include "bsp/esp32_p4_wifi6_touch_lcd_5.h" #include "bsp/touch.h" #include "driver/i2c_master.h" #include "esp_lcd_panel_io.h" #include "esp_lcd_touch_gt911.h" -#include "freertos/FreeRTOS.h" -#include "freertos/task.h" esp_err_t pi_p4_touch_new(esp_lcd_touch_handle_t *ret_touch) { @@ -71,47 +68,3 @@ bool pi_p4_touch_read(esp_lcd_touch_handle_t touch, uint16_t *x, uint16_t *y) *y = point.y; return true; } - -bool pi_p4_cpu_load_percent(uint8_t *percent) -{ -#if configGENERATE_RUN_TIME_STATS - enum { MAX_TASKS = 64 }; - static TaskStatus_t tasks[MAX_TASKS]; - static configRUN_TIME_COUNTER_TYPE previous_total = 0; - static uint64_t previous_idle = 0; - configRUN_TIME_COUNTER_TYPE total = 0; - uint64_t idle = 0; - - if (percent == NULL) { - return false; - } - UBaseType_t count = uxTaskGetSystemState(tasks, MAX_TASKS, &total); - if (count == 0) { - return false; - } - for (UBaseType_t index = 0; index < count; ++index) { - if (tasks[index].pcTaskName != NULL && strncmp(tasks[index].pcTaskName, "IDLE", 4) == 0) { - idle += tasks[index].ulRunTimeCounter; - } - } - if (previous_total == 0 || total <= previous_total || idle < previous_idle) { - previous_total = total; - previous_idle = idle; - return false; - } - - const uint64_t elapsed = (uint64_t)(total - previous_total) * configNUMBER_OF_CORES; - const uint64_t idle_elapsed = idle - previous_idle; - previous_total = total; - previous_idle = idle; - if (elapsed == 0) { - return false; - } - const uint64_t idle_percent = (idle_elapsed * 100U) / elapsed; - *percent = (uint8_t)(idle_percent >= 100U ? 0U : 100U - idle_percent); - return true; -#else - (void)percent; - return false; -#endif -} diff --git a/firmware/esp32-p4/components/esp_hosted_late_response_fix/CMakeLists.txt b/firmware/esp32-p4/components/esp_hosted_late_response_fix/CMakeLists.txt new file mode 100644 index 0000000..60f570f --- /dev/null +++ b/firmware/esp32-p4/components/esp_hosted_late_response_fix/CMakeLists.txt @@ -0,0 +1,41 @@ +idf_component_register(PRIV_REQUIRES espressif__esp_hosted) + +idf_component_get_property(esp_hosted_dir espressif__esp_hosted COMPONENT_DIR) +set(rpc_core "${esp_hosted_dir}/host/drivers/rpc/core/rpc_core.c") +file(READ "${rpc_core}" source) + +set(before [=[ + if (g_h.funcs->_h_queue_item(rpc_rx_q, &elem, HOSTED_BLOCK_MAX)) { + ESP_LOGE(TAG, "RPC Q put fail"); + goto free_buffers; + } + + /* Call up rx ind to unblock caller */ + if (CALLBACK_AVAILABLE == is_sync_resp_sem_available(app_resp->uid)) + post_sync_resp_sem(app_resp); +]=]) +set(after [=[ + if (CALLBACK_AVAILABLE != is_sync_resp_sem_available(app_resp->uid)) { + ESP_LOGW(TAG, "Dropping late sync response [0x%x], uid %ld", + app_resp->msg_id, app_resp->uid); + goto free_buffers; + } + + if (g_h.funcs->_h_queue_item(rpc_rx_q, &elem, HOSTED_BLOCK_MAX)) { + ESP_LOGE(TAG, "RPC Q put fail"); + goto free_buffers; + } + + /* Call up rx ind to unblock caller */ + post_sync_resp_sem(app_resp); +]=]) + +string(FIND "${source}" "Dropping late sync response" already_patched) +if(already_patched EQUAL -1) + string(FIND "${source}" "${before}" patch_site) + if(patch_site EQUAL -1) + message(FATAL_ERROR "esp_hosted RPC response path no longer matches the pinned patch") + endif() + string(REPLACE "${before}" "${after}" source "${source}") + file(WRITE "${rpc_core}" "${source}") +endif() diff --git a/firmware/esp32-p4/components/pocketjs_ppa/CMakeLists.txt b/firmware/esp32-p4/components/pocketjs_ppa/CMakeLists.txt new file mode 100644 index 0000000..1e679f6 --- /dev/null +++ b/firmware/esp32-p4/components/pocketjs_ppa/CMakeLists.txt @@ -0,0 +1,17 @@ +idf_component_register( + SRCS "src/pocketjs_ppa.c" + INCLUDE_DIRS "include" + REQUIRES esp_driver_ppa + PRIV_REQUIRES log +) + +# EspIdfPpaOps normally references this C ABI from a Rust static archive. +# Root the symbols so the one-pass linker extracts this component regardless +# of archive ordering. +target_link_options(${COMPONENT_LIB} INTERFACE + "-Wl,--undefined=pocketjs_ppa_create" + "-Wl,--undefined=pocketjs_ppa_destroy" + "-Wl,--undefined=pocketjs_ppa_fill_rgb565" + "-Wl,--undefined=pocketjs_ppa_blend_a8_rgb565" + "-Wl,--undefined=pocketjs_ppa_srm_psm5650_rgb565" +) diff --git a/firmware/esp32-p4/components/pocketjs_ppa/LICENSE b/firmware/esp32-p4/components/pocketjs_ppa/LICENSE new file mode 100644 index 0000000..bac2cb1 --- /dev/null +++ b/firmware/esp32-p4/components/pocketjs_ppa/LICENSE @@ -0,0 +1,21 @@ +MIT License + +Copyright (c) 2026 Yifeng "Evan" Wang + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in all +copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +SOFTWARE. diff --git a/firmware/esp32-p4/components/pocketjs_ppa/README.md b/firmware/esp32-p4/components/pocketjs_ppa/README.md new file mode 100644 index 0000000..eba7b72 --- /dev/null +++ b/firmware/esp32-p4/components/pocketjs_ppa/README.md @@ -0,0 +1,8 @@ +# pocketjs_ppa + +ESP-IDF component implementing blocking RGB565 FILL, A8-over-RGB565 BLEND, and +PSP PSM5650-to-RGB565 SRM operations for PocketJS on ESP32-P4. + +The public C ABI is normally consumed by the `EspIdfPpaOps` Rust type. See the +[ESP32-P4 host documentation](../../README.md) for integration, compatibility, +buffer ownership, and build instructions. diff --git a/firmware/esp32-p4/components/pocketjs_ppa/SOURCE_PROVENANCE.md b/firmware/esp32-p4/components/pocketjs_ppa/SOURCE_PROVENANCE.md new file mode 100644 index 0000000..afb3cfe --- /dev/null +++ b/firmware/esp32-p4/components/pocketjs_ppa/SOURCE_PROVENANCE.md @@ -0,0 +1,8 @@ +# Source provenance + +The C ABI source is copied from `pocket-stack/pocketjs` at commit +`9c809bbd047ddc75c27caa4990951a78d942477a`, matching the Rust dependency +pinned in the firmware manifest. The component manifest retains the +ESP-IDF 5.5 compatibility range already proven by `esp32-pi-agent` on this +board. It provides the C ABI used by +`pocketjs_esp32p4_ppa::EspIdfPpaOps` and remains under PocketJS's MIT license. diff --git a/firmware/esp32-p4/components/pocketjs_ppa/idf_component.yml b/firmware/esp32-p4/components/pocketjs_ppa/idf_component.yml new file mode 100644 index 0000000..b2d393b --- /dev/null +++ b/firmware/esp32-p4/components/pocketjs_ppa/idf_component.yml @@ -0,0 +1,11 @@ +version: "0.1.0" +description: ESP-IDF PPA operations for the PocketJS ESP32-P4 RGB565 renderer +license: MIT +repository: https://github.com/pocket-stack/pocketjs +documentation: https://github.com/pocket-stack/pocketjs/tree/main/hosts/esp32p4 +targets: + - esp32p4 +dependencies: + # The PPA API used by this component is available on the board's pinned + # ESP-IDF 5.5 toolchain as well as the PocketJS upstream 6.x toolchain. + idf: ">=5.5,<6.2" diff --git a/firmware/esp32-p4/components/pocketjs_ppa/include/pocketjs_ppa.h b/firmware/esp32-p4/components/pocketjs_ppa/include/pocketjs_ppa.h new file mode 100644 index 0000000..b48fd91 --- /dev/null +++ b/firmware/esp32-p4/components/pocketjs_ppa/include/pocketjs_ppa.h @@ -0,0 +1,89 @@ +#pragma once + +#include +#include + +#include "esp_err.h" + +#ifdef __cplusplus +extern "C" { +#endif + +typedef struct pocketjs_ppa_context *pocketjs_ppa_handle_t; + +/** + * Register one blocking FILL, BLEND, and SRM client for a renderer. + * + * ESP-IDF recommends that each task own its clients. The returned handle must + * therefore be used and destroyed by the same rendering task. + */ +esp_err_t pocketjs_ppa_create(pocketjs_ppa_handle_t *out_handle); + +/** + * Unregister all clients and release the handle. A NULL handle is accepted. + */ +void pocketjs_ppa_destroy(pocketjs_ppa_handle_t handle); + +/** + * The following narrow C ABI is consumed by EspIdfPpaOps in the + * pocketjs-esp32p4-ppa crate. Each function returns 1 after a completed + * blocking transaction or 0 when the renderer must use its ordered software + * fallback. + */ +int pocketjs_ppa_fill_rgb565( + pocketjs_ppa_handle_t handle, + uint16_t *destination, + size_t destination_pixels, + uint32_t width, + uint32_t height, + uint32_t x, + uint32_t y, + uint32_t rect_width, + uint32_t rect_height, + uint16_t color +); + +int pocketjs_ppa_blend_a8_rgb565( + pocketjs_ppa_handle_t handle, + uint16_t *destination, + size_t destination_pixels, + uint32_t width, + uint32_t height, + const uint8_t *mask, + size_t mask_len, + uint32_t x, + uint32_t y, + uint32_t rect_width, + uint32_t rect_height, + uint8_t red, + uint8_t green, + uint8_t blue, + uint8_t global_alpha +); + +int pocketjs_ppa_srm_psm5650_rgb565( + pocketjs_ppa_handle_t handle, + uint16_t *destination, + size_t destination_pixels, + uint32_t width, + uint32_t height, + const uint8_t *source, + size_t source_len, + uint32_t source_width, + uint32_t source_height, + uint32_t source_x, + uint32_t source_y, + uint32_t source_rect_width, + uint32_t source_rect_height, + uint32_t destination_x, + uint32_t destination_y, + uint32_t destination_rect_width, + uint32_t destination_rect_height, + uint32_t quarter_turn, + int mirror_x, + int mirror_y +); + +#ifdef __cplusplus +} +#endif diff --git a/firmware/esp32-p4/components/pocketjs_ppa/src/pocketjs_ppa.c b/firmware/esp32-p4/components/pocketjs_ppa/src/pocketjs_ppa.c new file mode 100644 index 0000000..ea91e78 --- /dev/null +++ b/firmware/esp32-p4/components/pocketjs_ppa/src/pocketjs_ppa.c @@ -0,0 +1,468 @@ +#include "pocketjs_ppa.h" + +#include +#include +#include + +#include "driver/ppa.h" +#include "esp_log.h" + +static const char *TAG = "pocketjs_ppa"; + +struct pocketjs_ppa_context { + ppa_client_handle_t fill; + ppa_client_handle_t blend; + ppa_client_handle_t srm; + bool fill_error_logged; + bool blend_error_logged; + bool srm_error_logged; +}; + +static bool surface_is_valid( + const void *buffer, + size_t pixels, + uint32_t width, + uint32_t height, + size_t bytes_per_pixel +) +{ + if (buffer == NULL || width == 0 || height == 0 || bytes_per_pixel == 0) { + return false; + } + return (size_t)width <= SIZE_MAX / (size_t)height && + pixels == (size_t)width * (size_t)height && + pixels <= SIZE_MAX / bytes_per_pixel; +} + +static bool rect_is_valid( + uint32_t surface_width, + uint32_t surface_height, + uint32_t x, + uint32_t y, + uint32_t width, + uint32_t height +) +{ + return width > 0 && + height > 0 && + x <= surface_width && + y <= surface_height && + width <= surface_width - x && + height <= surface_height - y; +} + +static bool byte_ranges_overlap( + const void *first, + size_t first_size, + const void *second, + size_t second_size +) +{ + const uintptr_t first_start = (uintptr_t)first; + const uintptr_t second_start = (uintptr_t)second; + if (first_size > UINTPTR_MAX - first_start || + second_size > UINTPTR_MAX - second_start) { + return true; + } + const uintptr_t first_end = first_start + first_size; + const uintptr_t second_end = second_start + second_size; + return first_start < second_end && second_start < first_end; +} + +static color_pixel_argb8888_data_t rgb565_to_argb8888(uint16_t color) +{ + const uint8_t red5 = (uint8_t)((color >> 11) & 0x1FU); + const uint8_t green6 = (uint8_t)((color >> 5) & 0x3FU); + const uint8_t blue5 = (uint8_t)(color & 0x1FU); + const color_pixel_argb8888_data_t expanded = { + .r = (uint8_t)((red5 << 3) | (red5 >> 2)), + .g = (uint8_t)((green6 << 2) | (green6 >> 4)), + .b = (uint8_t)((blue5 << 3) | (blue5 >> 2)), + .a = UINT8_MAX, + }; + return expanded; +} + +static void log_operation_failure_once( + const char *operation, + esp_err_t error, + bool *already_logged +) +{ + if (!*already_logged) { + ESP_LOGW( + TAG, + "PPA %s failed (%s); using ordered RGB565 software fallback", + operation, + esp_err_to_name(error) + ); + *already_logged = true; + } +} + +static esp_err_t register_client( + ppa_operation_t operation, + ppa_client_handle_t *out_client +) +{ + const ppa_client_config_t config = { + .oper_type = operation, + .max_pending_trans_num = 1, + .data_burst_length = PPA_DATA_BURST_LENGTH_128, + }; + return ppa_register_client(&config, out_client); +} + +esp_err_t pocketjs_ppa_create(pocketjs_ppa_handle_t *out_handle) +{ + if (out_handle == NULL) { + return ESP_ERR_INVALID_ARG; + } + *out_handle = NULL; + + pocketjs_ppa_handle_t handle = calloc(1, sizeof(*handle)); + if (handle == NULL) { + return ESP_ERR_NO_MEM; + } + + esp_err_t result = register_client(PPA_OPERATION_FILL, &handle->fill); + if (result == ESP_OK) { + result = register_client(PPA_OPERATION_BLEND, &handle->blend); + } + if (result == ESP_OK) { + result = register_client(PPA_OPERATION_SRM, &handle->srm); + } + if (result != ESP_OK) { + ESP_LOGE( + TAG, + "failed to register PocketJS PPA clients: %s", + esp_err_to_name(result) + ); + pocketjs_ppa_destroy(handle); + return result; + } + + *out_handle = handle; + ESP_LOGI(TAG, "RGB565 backend ready: FILL + A8 BLEND + SRM"); + return ESP_OK; +} + +void pocketjs_ppa_destroy(pocketjs_ppa_handle_t handle) +{ + if (handle == NULL) { + return; + } + if (handle->srm != NULL) { + (void)ppa_unregister_client(handle->srm); + } + if (handle->blend != NULL) { + (void)ppa_unregister_client(handle->blend); + } + if (handle->fill != NULL) { + (void)ppa_unregister_client(handle->fill); + } + free(handle); +} + +int pocketjs_ppa_fill_rgb565( + pocketjs_ppa_handle_t handle, + uint16_t *destination, + size_t destination_pixels, + uint32_t width, + uint32_t height, + uint32_t x, + uint32_t y, + uint32_t rect_width, + uint32_t rect_height, + uint16_t color +) +{ + if (handle == NULL || + handle->fill == NULL || + !surface_is_valid( + destination, + destination_pixels, + width, + height, + sizeof(*destination) + ) || + !rect_is_valid(width, height, x, y, rect_width, rect_height)) { + return 0; + } + + const ppa_fill_oper_config_t operation = { + .out = { + .buffer = destination, + .buffer_size = destination_pixels * sizeof(*destination), + .pic_w = width, + .pic_h = height, + .block_offset_x = x, + .block_offset_y = y, + .fill_cm = PPA_FILL_COLOR_MODE_RGB565, + }, + .fill_block_w = rect_width, + .fill_block_h = rect_height, + // The fixed fill pixel is supplied as ARGB components even when the + // output mode is RGB565. A packed RGB565 word produces wrong colors. + .fill_argb_color = rgb565_to_argb8888(color), + .mode = PPA_TRANS_MODE_BLOCKING, + }; + const esp_err_t result = ppa_do_fill(handle->fill, &operation); + if (result != ESP_OK) { + log_operation_failure_once( + "fill", + result, + &handle->fill_error_logged + ); + return 0; + } + return 1; +} + +int pocketjs_ppa_blend_a8_rgb565( + pocketjs_ppa_handle_t handle, + uint16_t *destination, + size_t destination_pixels, + uint32_t width, + uint32_t height, + const uint8_t *mask, + size_t mask_len, + uint32_t x, + uint32_t y, + uint32_t rect_width, + uint32_t rect_height, + uint8_t red, + uint8_t green, + uint8_t blue, + uint8_t global_alpha +) +{ + if (global_alpha == 0) { + return 1; + } + if (handle == NULL || + handle->blend == NULL || + !surface_is_valid( + destination, + destination_pixels, + width, + height, + sizeof(*destination) + ) || + !surface_is_valid(mask, mask_len, width, height, sizeof(*mask)) || + !rect_is_valid(width, height, x, y, rect_width, rect_height)) { + return 0; + } + + ppa_blend_oper_config_t operation = { + .in_bg = { + .buffer = destination, + .pic_w = width, + .pic_h = height, + .block_w = rect_width, + .block_h = rect_height, + .block_offset_x = x, + .block_offset_y = y, + .blend_cm = PPA_BLEND_COLOR_MODE_RGB565, + }, + .in_fg = { + .buffer = mask, + .pic_w = width, + .pic_h = height, + .block_w = rect_width, + .block_h = rect_height, + .block_offset_x = x, + .block_offset_y = y, + .blend_cm = PPA_BLEND_COLOR_MODE_A8, + }, + .out = { + .buffer = destination, + .buffer_size = destination_pixels * sizeof(*destination), + .pic_w = width, + .pic_h = height, + .block_offset_x = x, + .block_offset_y = y, + .blend_cm = PPA_BLEND_COLOR_MODE_RGB565, + }, + .bg_alpha_update_mode = PPA_ALPHA_NO_CHANGE, + .fg_alpha_update_mode = PPA_ALPHA_NO_CHANGE, + .fg_fix_rgb_val = { + .r = red, + .g = green, + .b = blue, + }, + .mode = PPA_TRANS_MODE_BLOCKING, + }; + if (global_alpha < UINT8_MAX) { + const uint32_t fixed_alpha = + ((uint32_t)global_alpha * 256U + 127U) / 255U; + operation.fg_alpha_update_mode = PPA_ALPHA_SCALE; + operation.fg_alpha_scale_ratio = (float)fixed_alpha / 256.0f; + } + + const esp_err_t result = ppa_do_blend(handle->blend, &operation); + if (result != ESP_OK) { + log_operation_failure_once( + "blend", + result, + &handle->blend_error_logged + ); + return 0; + } + return 1; +} + +static bool exact_scale( + uint32_t source_extent, + uint32_t destination_extent, + float *out_scale +) +{ + if (source_extent == 0 || destination_extent > UINT32_MAX / 16U) { + return false; + } + const uint32_t sixteenths_numerator = destination_extent * 16U; + if ((sixteenths_numerator % source_extent) != 0) { + return false; + } + const uint32_t sixteenths = sixteenths_numerator / source_extent; + if (sixteenths == 0 || sixteenths >= 256U * 16U) { + return false; + } + *out_scale = (float)sixteenths / 16.0f; + return true; +} + +int pocketjs_ppa_srm_psm5650_rgb565( + pocketjs_ppa_handle_t handle, + uint16_t *destination, + size_t destination_pixels, + uint32_t width, + uint32_t height, + const uint8_t *source, + size_t source_len, + uint32_t source_width, + uint32_t source_height, + uint32_t source_x, + uint32_t source_y, + uint32_t source_rect_width, + uint32_t source_rect_height, + uint32_t destination_x, + uint32_t destination_y, + uint32_t destination_rect_width, + uint32_t destination_rect_height, + uint32_t quarter_turn, + int mirror_x, + int mirror_y +) +{ + if (handle == NULL || + handle->srm == NULL || + !surface_is_valid( + destination, + destination_pixels, + width, + height, + sizeof(*destination) + ) || + source == NULL || + source_width == 0 || + source_height == 0 || + (size_t)source_width > SIZE_MAX / (size_t)source_height || + (size_t)source_width * (size_t)source_height > SIZE_MAX / 2U) { + return 0; + } + + const size_t source_required = + (size_t)source_width * (size_t)source_height * 2U; + const size_t destination_size = + destination_pixels * sizeof(*destination); + if (source_len < source_required || + byte_ranges_overlap( + source, + source_required, + destination, + destination_size + ) || + !rect_is_valid( + source_width, + source_height, + source_x, + source_y, + source_rect_width, + source_rect_height + ) || + !rect_is_valid( + width, + height, + destination_x, + destination_y, + destination_rect_width, + destination_rect_height + ) || + quarter_turn > 3U) { + return 0; + } + + const bool swaps_axes = quarter_turn == 1U || quarter_turn == 3U; + const uint32_t scaled_width = swaps_axes + ? destination_rect_height + : destination_rect_width; + const uint32_t scaled_height = swaps_axes + ? destination_rect_width + : destination_rect_height; + float scale_x = 0.0f; + float scale_y = 0.0f; + if (!exact_scale(source_rect_width, scaled_width, &scale_x) || + !exact_scale(source_rect_height, scaled_height, &scale_y)) { + return 0; + } + + const ppa_srm_rotation_angle_t rotations[] = { + PPA_SRM_ROTATION_ANGLE_0, + PPA_SRM_ROTATION_ANGLE_90, + PPA_SRM_ROTATION_ANGLE_180, + PPA_SRM_ROTATION_ANGLE_270, + }; + const ppa_srm_oper_config_t operation = { + .in = { + .buffer = source, + .pic_w = source_width, + .pic_h = source_height, + .block_w = source_rect_width, + .block_h = source_rect_height, + .block_offset_x = source_x, + .block_offset_y = source_y, + .srm_cm = PPA_SRM_COLOR_MODE_RGB565, + }, + .out = { + .buffer = destination, + .buffer_size = destination_size, + .pic_w = width, + .pic_h = height, + .block_offset_x = destination_x, + .block_offset_y = destination_y, + .srm_cm = PPA_SRM_COLOR_MODE_RGB565, + }, + .rotation_angle = rotations[quarter_turn], + .scale_x = scale_x, + .scale_y = scale_y, + .mirror_x = mirror_x != 0, + .mirror_y = mirror_y != 0, + .rgb_swap = true, + .byte_swap = false, + .alpha_update_mode = PPA_ALPHA_NO_CHANGE, + .mode = PPA_TRANS_MODE_BLOCKING, + }; + const esp_err_t result = + ppa_do_scale_rotate_mirror(handle->srm, &operation); + if (result != ESP_OK) { + log_operation_failure_once( + "SRM", + result, + &handle->srm_error_logged + ); + return 0; + } + return 1; +} diff --git a/firmware/esp32-p4/components_esp32p4.lock b/firmware/esp32-p4/components_esp32p4.lock index d143cff..c852a02 100644 --- a/firmware/esp32-p4/components_esp32p4.lock +++ b/firmware/esp32-p4/components_esp32p4.lock @@ -153,6 +153,6 @@ direct_dependencies: - idf - joltwallet/littlefs - waveshare/esp_lcd_hx8394 -manifest_hash: a4c3032b83ef98ef2fd7e20dcd6f9fe17a1ade1defcb0e8bfa0184245cbf83f1 +manifest_hash: f580ce29f698ad36eb00e6342d57a6c0aa991afcd88977548bbc0fa306c9b088 target: esp32p4 version: 2.0.0 diff --git a/firmware/esp32-p4/sdkconfig.defaults b/firmware/esp32-p4/sdkconfig.defaults index a78cfc5..c578875 100644 --- a/firmware/esp32-p4/sdkconfig.defaults +++ b/firmware/esp32-p4/sdkconfig.defaults @@ -27,15 +27,24 @@ CONFIG_SPIRAM_TRY_ALLOCATE_WIFI_LWIP=y CONFIG_BSP_LCD_DPI_BUFFER_NUMS=3 CONFIG_BSP_LCD_COLOR_FORMAT_RGB565=y +# The IDF entry task only queues the small AgentOS launcher. Keeping this small +# releases enough contiguous internal RAM for the full runtime task afterward. CONFIG_ESP_MAIN_TASK_STACK_SIZE=16384 CONFIG_ESP_SYSTEM_EVENT_TASK_STACK_SIZE=4096 CONFIG_FREERTOS_IDLE_TASK_STACKSIZE=4096 CONFIG_PTHREAD_TASK_STACK_SIZE_DEFAULT=4096 +# AgentOS deliberately yields one scheduler tick in its native loops. Pin the +# 10 ms tick period so vTaskDelay(1) keeps yielding across builds. +CONFIG_FREERTOS_HZ=100 +CONFIG_ESP_TASK_WDT_EN=y +CONFIG_ESP_TASK_WDT_INIT=y +CONFIG_ESP_TASK_WDT_TIMEOUT_S=5 +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU0=y +CONFIG_ESP_TASK_WDT_CHECK_IDLE_TASK_CPU1=y + # Expose per-task runtime counters so the status bar can report real average # CPU utilization across both ESP32-P4 cores. -CONFIG_FREERTOS_USE_TRACE_FACILITY=y -CONFIG_FREERTOS_GENERATE_RUN_TIME_STATS=y # HTTPS clients need the ESP-IDF certificate bundle. Model endpoints # still perform normal hostname verification. diff --git a/firmware/esp32-p4/shim/sys/ioctl.h b/firmware/esp32-p4/shim/sys/ioctl.h new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/firmware/esp32-p4/shim/sys/ioctl.h @@ -0,0 +1 @@ + diff --git a/firmware/esp32-p4/src/agentos_main.rs b/firmware/esp32-p4/src/agentos_main.rs new file mode 100644 index 0000000..6044f88 --- /dev/null +++ b/firmware/esp32-p4/src/agentos_main.rs @@ -0,0 +1,759 @@ +use core::time::Duration; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::Arc; +use std::time::Instant; + +use pocket_pi_agentos::{ + AppCatalog, AppServiceHost, AppSupervisor, RoutedToolHost, DATA_ACTION_STACK_BYTES, +}; +use pocket_pi_app_pack::catalog; +use pocket_pi_embedded::{AgentEvent, ModelBackend, ToolHost, MODEL_WORKER_STACK_BYTES}; +use pocket_pi_protocols::model::ModelBackendSettings; +use pocket_pi_tools::CoreToolHost; +use pocketjs_esp32p4_ppa::{EspIdfPpaOps, RenderTargetState, Renderer}; +use serde_json::{json, Value}; + +use super::{ + app_services::EspAppServices, + backend, + device_state::{SettingsProjection, WifiSettingsProjection}, + esp_result, init_wifi, storage, transport, DisplayProbe, EspPlatform, + BOARD_NAME, PANEL_HEIGHT, PANEL_WIDTH, +}; + +const AGENTOS_FREERTOS_HZ: u32 = 100; +const _: () = assert!(esp_idf_svc::sys::configTICK_RATE_HZ == AGENTOS_FREERTOS_HZ); + +#[derive(Clone)] +struct Message { + role: &'static str, + text: String, +} + +pub fn run() -> anyhow::Result<()> { + let _workspace = storage::mount_workspace()?; + + let uart = Arc::new( + transport::UartLineTransport::new() + .map_err(|error| anyhow::anyhow!("initialize UART transport: {error}"))?, + ); + let runtime_config = + match transport::request_runtime_config(uart.as_ref(), Duration::from_secs(5)) { + Ok(config) => config, + Err(error) => { + log::warn!("No UART runtime config received: {error}"); + transport::RuntimeConfig::default() + } + }; + if let Some(seconds) = runtime_config.unix_time_seconds { + let time = esp_idf_svc::sys::timeval { + tv_sec: seconds as i64, + tv_usec: 0, + }; + if unsafe { esp_idf_svc::sys::settimeofday(&time, core::ptr::null()) } == 0 { + log::info!("clock seeded by UART bridge"); + } + } + + let mut wifi = match init_wifi( + runtime_config.wifi_ssid.as_deref(), + runtime_config.wifi_password.as_deref(), + ) { + Ok(wifi) => { + log::info!("C6-SDIO Wi-Fi and lwIP netif active"); + Some(wifi) + } + Err(error) => { + log::error!("C6-SDIO Wi-Fi radio probe failed: {error:#}"); + None + } + }; + let mut settings = wifi + .as_ref() + .map(|wifi| { + wifi.projection(if wifi.is_connecting() { + "CONNECTING..." + } else if wifi.is_connected() { + "CONNECTED" + } else { + "NOT CONNECTED" + }) + }) + .unwrap_or_else(|| SettingsProjection { + firmware_version: env!("CARGO_PKG_VERSION").into(), + workspace_free_bytes: storage::workspace_free_bytes().ok(), + wifi: WifiSettingsProjection { + status: "WI-FI DRIVER UNAVAILABLE".into(), + ..Default::default() + }, + }); + let _sntp = if wifi.is_some() { + esp_idf_svc::sntp::EspSntp::new_default().ok() + } else { + None + }; + + let model_settings = runtime_config.model.clone(); + let wireless_model = matches!( + model_settings.backend, + ModelBackendSettings::Wireless { .. } + ); + let provider = match model_settings.backend { + ModelBackendSettings::Uart { .. } => "uart", + ModelBackendSettings::Wireless { provider } => provider.id(), + }; + let resolved_model = model_settings + .resolved_model() + .unwrap_or_else(|_| "unknown".into()); + let uart_poc = matches!(model_settings.backend, ModelBackendSettings::Uart { .. }); + let backend: Arc = match model_settings.backend { + ModelBackendSettings::Uart { .. } => { + let transport: Arc = uart; + Arc::new(backend::UartBackend::new(transport)) + } + ModelBackendSettings::Wireless { provider } => Arc::new( + backend::WirelessBackend::new( + provider, + runtime_config + .model_api_key + .ok_or_else(|| anyhow::anyhow!("wireless backend is missing API key"))?, + ) + .map_err(anyhow::Error::msg)?, + ), + }; + + let network_ready = Arc::new(AtomicBool::new( + wifi.as_ref().is_some_and(|wifi| wifi.is_connected()), + )); + let catalog = catalog()?; + let services: Arc = Arc::new(EspAppServices::new( + network_ready.clone(), + catalog.clone(), + runtime_config.app_credentials, + )); + let mut supervisor = with_psram_pthread_config(DATA_ACTION_STACK_BYTES, || { + AppSupervisor::new(storage::WORKSPACE_ROOT, catalog, services) + })?; + supervisor.frame()?; + + let mut renderer = pocketjs_esp32p4_ppa::Renderer::new(Default::default()) + .ok_or_else(|| anyhow::anyhow!("invalid PocketJS renderer configuration"))?; + let mut ppa = EspIdfPpaOps::new() + .map_err(|error| anyhow::anyhow!("initialize PocketJS PPA: ESP-IDF error 0x{error:x}"))?; + log::info!("Pocket Pi AgentOS hardware boot: {BOARD_NAME}"); + let mut display = match init_display(&mut renderer, &mut ppa, &supervisor) { + Ok(display) => { + log::info!("MIPI-DSI panel active with PocketJS App View"); + Some(display) + } + Err(error) => { + log::error!("MIPI-DSI panel failed: {error:#}"); + None + } + }; + let native_tools = Arc::new(CoreToolHost::new( + storage::WORKSPACE_ROOT, + Arc::new(EspPlatform), + )); + let native: Arc = native_tools.clone(); + let (routed_tools, app_rx) = RoutedToolHost::new(native, supervisor.catalog().clone()); + let config = json!({ + "provider":provider, + "model":resolved_model, + "thinkingLevel":model_settings.thinking_level.id(), + "systemPrompt":"You are Pi Agent, the first-class system App in Pocket Pi AgentOS on an ESP32-P4. You can manage the top-level /workspace and use installed App tools. Use /workspace for durable memory, notes, plans, and artifacts; read and update relevant files when continuity matters. Installed Apps own their tools, state, tasks, and views. Be concise." + }); + with_psram_pthread_config(MODEL_WORKER_STACK_BYTES, || { + supervisor + .boot_agent(&config.to_string(), backend, Arc::new(routed_tools)) + .map_err(anyhow::Error::msg) + })?; + + let mut messages = vec![Message { + role: "assistant", + text: "Pocket Pi AgentOS is starting.".into(), + }]; + let mut agent_status = "STARTING"; + let mut busy = false; + let mut initial_prompt = runtime_config.initial_prompt; + let initial_prompt_not_before = + Instant::now() + Duration::from_secs(runtime_config.initial_prompt_delay_seconds); + let mut touch_was_down = false; + let mut redraw = true; + let mut projection_dirty = true; + let mut pending_ui_task: Option = None; + let mut last_tick = Instant::now(); + let mut last_runtime_pump = Instant::now(); + let mut last_projection_refresh = Instant::now(); + let mut last_heartbeat = Instant::now(); + let mut last_wifi_poll = Instant::now(); + let mut last_wifi_connected = wifi.as_ref().is_some_and(|wifi| wifi.is_connected()); + + loop { + while let Ok(request) = app_rx.try_recv() { + request.handle(&mut supervisor); + redraw = true; + projection_dirty = true; + } + + if let Some(display) = display.as_mut() { + if let Some((x, y)) = display.read_touch() { + if !touch_was_down { + supervisor.pointer_down(x, y)?; + let action = supervisor.tap(x, y)?; + match action.get("type").and_then(Value::as_str) { + Some("navigate") => { + if let Some(app) = action.get("app").and_then(Value::as_str) { + supervisor.open(app)?; + projection_dirty = true; + } + } + Some("submitPrompt") => { + if let Some(prompt) = action.get("prompt").and_then(Value::as_str) { + submit_prompt( + prompt.to_owned(), + &supervisor, + &mut messages, + &mut busy, + &mut agent_status, + ); + projection_dirty = true; + } + } + Some("invokeTask") => { + if let Some(task) = action.get("task").and_then(Value::as_str) { + pending_ui_task = Some(task.to_owned()); + } + } + Some("settings") => { + match action.get("command").and_then(Value::as_str) { + Some("scan") => match wifi.as_mut() { + Some(wifi) => { + settings.wifi.scanning = true; + settings.wifi.status = "SCANNING...".into(); + match wifi.scan() { + Ok(networks) => { + settings = wifi.projection(""); + settings.wifi.networks = networks; + } + Err(error) => { + settings.wifi.status = + format!("SCAN FAILED: {error}"); + } + } + settings.wifi.scanning = false; + } + None => { + settings.wifi.status = "WI-FI DRIVER UNAVAILABLE".into() + } + }, + Some("connect") => { + let ssid = + action.get("ssid").and_then(Value::as_str).unwrap_or(""); + let password = action + .get("password") + .and_then(Value::as_str) + .unwrap_or(""); + match wifi.as_mut() { + Some(wifi) => match wifi.begin_connect(ssid, password, true) { + Ok(()) => { + settings = wifi.projection("CONNECTING..."); + last_wifi_poll = Instant::now(); + } + Err(error) => { + settings.wifi.status = + format!("CONNECT FAILED: {error}") + } + }, + None => { + settings.wifi.status = "WI-FI DRIVER UNAVAILABLE".into() + } + } + } + Some("forget") => match wifi.as_mut() { + Some(wifi) => match wifi.forget() { + Ok(()) => settings = wifi.projection("NETWORK FORGOTTEN"), + Err(error) => { + settings.wifi.status = format!("FORGET FAILED: {error}") + } + }, + None => { + settings.wifi.status = "WI-FI DRIVER UNAVAILABLE".into() + } + }, + Some("restart") => unsafe { + esp_idf_svc::sys::esp_restart(); + }, + _ => {} + } + settings.workspace_free_bytes = storage::workspace_free_bytes().ok(); + projection_dirty = true; + } + _ => {} + } + redraw = true; + } + touch_was_down = true; + } else { + if touch_was_down { + supervisor.pointer_up()?; + redraw = true; + } + touch_was_down = false; + } + } + + if last_wifi_poll.elapsed() >= Duration::from_millis(500) { + last_wifi_poll = Instant::now(); + if let Some(wifi) = wifi.as_mut() { + match wifi.poll_connect() { + Some(Ok(())) => { + settings = wifi.projection("CONNECTED"); + projection_dirty = true; + redraw = true; + log::info!("Wi-Fi association and DHCP completed"); + } + Some(Err(error)) => { + settings = wifi.projection(format!("CONNECT FAILED: {error}")); + projection_dirty = true; + redraw = true; + log::warn!("Wi-Fi connection attempt failed: {error:#}"); + } + None => {} + } + } + let connected = wifi.as_ref().is_some_and(|wifi| wifi.is_connected()); + network_ready.store(connected, Ordering::Release); + if connected != last_wifi_connected { + last_wifi_connected = connected; + if let Some(wifi) = wifi.as_ref().filter(|wifi| !wifi.is_connecting()) { + let networks = core::mem::take(&mut settings.wifi.networks); + settings = wifi.projection(if connected { + "CONNECTED" + } else { + "NOT CONNECTED" + }); + settings.wifi.networks = networks; + projection_dirty = true; + redraw = true; + } + log::info!( + "Wi-Fi link state changed: {}", + if connected { "connected" } else { "disconnected" } + ); + } + } + + if last_tick.elapsed() >= Duration::from_secs(1) { + if !busy && agent_status == "IDLE" && initial_prompt.is_none() { + if let Some(wake) = native_tools.claim_due() { + submit_prompt( + wake.prompt, + &supervisor, + &mut messages, + &mut busy, + &mut agent_status, + ); + } + // The ESP32 v1 runtime has one App/SQLite execution owner. + // App schedules wait for an active Agent turn to complete so + // they cannot overlap an App Tool or its transaction. + let app_results = if busy { + Vec::new() + } else { + supervisor.poll_due_tasks() + }; + for (task, result) in &app_results { + log::info!("AppTask {task}: {}", result.text); + } + if !app_results.is_empty() { + redraw = true; + } + } + last_tick = Instant::now(); + } + + if last_projection_refresh.elapsed() >= Duration::from_secs(5) { + if supervisor.active_id() == pocket_pi_agentos::ROOT_APP_ID { + projection_dirty = true; + redraw = true; + } + last_projection_refresh = Instant::now(); + } + + if projection_dirty { + let projection = root_projection( + &messages, + agent_status, + provider, + &resolved_model, + &settings, + native_tools.as_ref(), + supervisor.catalog(), + ); + supervisor.update_root(&projection)?; + projection_dirty = false; + } + if supervisor.active_projection_is_stale() { + // A background App commit is itself a redraw cause. The check is + // one atomic revision comparison; SQLite is read only after the + // active View consumes the invalidation below. + redraw = true; + } + // Touch sampling stays at the display cadence, while entering QuickJS + // is event-driven and capped at 20 Hz. An idle system therefore leaves + // most CPU0 time to FreeRTOS and navigation is not queued behind + // redundant Agent/App tick calls. + if redraw || last_runtime_pump.elapsed() >= Duration::from_millis(50) { + for event in supervisor.frame_render(redraw)? { + match event { + AgentEvent::Ready => { + log::info!("Pi Agent System App ready with App Tool registry"); + if uart_poc { + unsafe { + esp_idf_svc::sys::esp_log_level_set( + c"*".as_ptr(), + esp_idf_svc::sys::esp_log_level_t_ESP_LOG_WARN, + ); + // Keep App transport stage boundaries visible on UART without + // restoring noisy global INFO logging or exposing response data. + esp_idf_svc::sys::esp_log_level_set( + c"pocket_pi_p4::app_services".as_ptr(), + esp_idf_svc::sys::esp_log_level_t_ESP_LOG_INFO, + ); + } + } + agent_status = "IDLE"; + messages[0].text = "ESP32-P4 Pi Agent is ready.".into(); + } + AgentEvent::ResponseText(text) => { + if let Some(message) = messages.last_mut() { + message.text.push_str(&text); + } + } + AgentEvent::Done => { + log::info!("Pi Agent turn completed"); + agent_status = "IDLE"; + busy = false; + } + AgentEvent::Failed(error) => { + log::error!("Pi Agent failed: {error}"); + if let Some(message) = messages.last_mut() { + message.text = format!("Agent failed: {error}"); + } + agent_status = "FAULTED"; + busy = false; + } + } + redraw = true; + projection_dirty = true; + } + last_runtime_pump = Instant::now(); + } + + if agent_status == "IDLE" + && !busy + && initial_prompt.is_some() + && Instant::now() >= initial_prompt_not_before + && (!wireless_model || network_ready.load(Ordering::Acquire)) + { + if let Some(prompt) = initial_prompt.take() { + submit_prompt( + prompt, + &supervisor, + &mut messages, + &mut busy, + &mut agent_status, + ); + redraw = true; + projection_dirty = true; + } + } + + if redraw { + if let Some(display) = display.as_mut() { + display.render_agentos(&mut renderer, &mut ppa, &supervisor)?; + } + redraw = false; + } + + // A UI-requested task runs only after its loading state has reached the + // panel. This preserves immediate touch feedback even when HTTPS is slow. + if !busy { + if let Some(task) = pending_ui_task.take() { + let started = Instant::now(); + let result = supervisor.invoke_active_task(&task, &Value::Null); + log::info!( + "UI AppTask {} finished in {}ms: {}", + task, + started.elapsed().as_millis(), + result.text + ); + supervisor.frame()?; + redraw = true; + } + } + + if last_heartbeat.elapsed() >= Duration::from_secs(5) { + log::info!( + "heartbeat app_data_action={} agent={} app={}", + if supervisor.services_busy() { + "running" + } else { + "idle" + }, + agent_status, + supervisor.active_id(), + ); + last_heartbeat = Instant::now(); + } + // AgentOS is a native FreeRTOS task, not a pthread. With a 100 Hz tick, + // delaying one tick keeps the loop at scheduler granularity and gives + // CPU0's idle task a watchdog-feeding scheduling opportunity. + unsafe { esp_idf_svc::sys::vTaskDelay(1) }; + } +} + +fn with_psram_pthread_config( + stack_size: usize, + action: impl FnOnce() -> anyhow::Result, +) -> anyhow::Result { + // Large persistent workers use PSRAM stacks. App Data passes this setting + // to its NET child; the platform default is restored after each spawn. + let default = unsafe { esp_idf_svc::sys::esp_pthread_get_default_config() }; + let mut config = default; + config.stack_size = stack_size; + config.inherit_cfg = true; + config.pin_to_core = 1; + config.stack_alloc_caps = + esp_idf_svc::sys::MALLOC_CAP_SPIRAM | esp_idf_svc::sys::MALLOC_CAP_8BIT; + let configured = unsafe { esp_idf_svc::sys::esp_pthread_set_cfg(&config) }; + if configured != esp_idf_svc::sys::ESP_OK { + anyhow::bail!("configure PSRAM pthread: ESP-IDF error 0x{configured:x}"); + } + let result = action(); + let restored = unsafe { esp_idf_svc::sys::esp_pthread_set_cfg(&default) }; + if restored != esp_idf_svc::sys::ESP_OK { + anyhow::bail!("restore pthread defaults: ESP-IDF error 0x{restored:x}"); + } + result +} + +fn submit_prompt( + prompt: String, + supervisor: &AppSupervisor, + messages: &mut Vec, + busy: &mut bool, + agent_status: &mut &'static str, +) { + if *busy || prompt.trim().is_empty() { + return; + } + messages.push(Message { + role: "user", + text: prompt.clone(), + }); + messages.push(Message { + role: "assistant", + text: String::new(), + }); + *busy = true; + *agent_status = "THINKING"; + if let Err(error) = supervisor.prompt_agent(&prompt) { + messages.last_mut().unwrap().text = format!("Agent is unavailable: {error:#}"); + *busy = false; + *agent_status = "FAULTED"; + } +} + +fn root_projection( + messages: &[Message], + agent_status: &str, + provider: &str, + model: &str, + settings: &SettingsProjection, + tools: &CoreToolHost, + catalog: &AppCatalog, +) -> Value { + let schedule = tools.schedule_projection(); + let schedule_text = match schedule.next_in_seconds { + Some(seconds) => schedule.every_minutes.map_or_else( + || format!("in {seconds}s"), + |minutes| format!("in {seconds}s · every {minutes}m"), + ), + None => "not scheduled".to_owned(), + }; + json!({ + "agent":agent_status, + "model":format!("{provider} / {model}"), + "messages":messages.iter().map(|message| json!({"role":message.role,"text":message.text})).collect::>(), + "schedule":{ + "name":schedule.name, + "prompt":schedule.prompt, + "next":schedule_text, + "everyMinutes":schedule.every_minutes, + }, + "apps":catalog.descriptors().filter(|app| app.id != pocket_pi_agentos::ROOT_APP_ID).map(|app| json!({ + "id":app.id, + "title":app.title, + "description":app.description, + "scheduleEveryMinutes":app.schedules.first().map(|schedule| schedule.every_minutes), + })).collect::>(), + "settings":{ + "wifi":{ + "connectedSsid":settings.wifi.connected_ssid, + "ipAddress":settings.wifi.ip_address, + "rssiDbm":settings.wifi.rssi_dbm, + "scanning":settings.wifi.scanning, + "networks":settings.wifi.networks.iter().map(|network| json!({ + "ssid":network.ssid, + "rssiDbm":network.rssi_dbm, + "secured":network.secured, + })).collect::>(), + "status":settings.wifi.status, + }, + "firmwareVersion":settings.firmware_version, + "workspaceFree":settings.workspace_free_bytes.map(|bytes| format!("{} KB", bytes / 1024)), + }, + }) +} + +fn init_display( + renderer: &mut Renderer, + ppa: &mut EspIdfPpaOps, + supervisor: &AppSupervisor, +) -> anyhow::Result { + unsafe { + let mut panel = core::ptr::null_mut(); + let mut io = core::ptr::null_mut(); + let mut touch = core::ptr::null_mut(); + let mut framebuffer_0 = core::ptr::null_mut(); + let mut framebuffer_1 = core::ptr::null_mut(); + let mut framebuffer_2 = core::ptr::null_mut(); + + esp_result( + "bsp_display_new", + esp_idf_svc::sys::bsp_display_new(core::ptr::null(), &mut panel, &mut io), + )?; + esp_result( + "esp_lcd_dpi_panel_get_frame_buffer", + esp_idf_svc::sys::esp_lcd_dpi_panel_get_frame_buffer( + panel, + 3, + &mut framebuffer_0, + &mut framebuffer_1, + &mut framebuffer_2, + ), + )?; + let framebuffers = [framebuffer_0, framebuffer_1, framebuffer_2]; + if framebuffers.iter().any(|framebuffer| framebuffer.is_null()) { + anyhow::bail!("esp_lcd_dpi_panel_get_frame_buffer returned a null buffer") + } + + let pixels = core::slice::from_raw_parts_mut( + framebuffers[0].cast::(), + PANEL_WIDTH as usize * PANEL_HEIGHT as usize, + ); + let mut render_states = [ + RenderTargetState::new(), + RenderTargetState::new(), + RenderTargetState::new(), + ]; + supervisor.with_ui(|ui| { + let words = ui.draw().words.clone(); + renderer + .render_incremental( + &mut render_states[0], + ui, + &words, + pixels, + PANEL_WIDTH, + PANEL_HEIGHT, + ppa, + ) + .ok_or_else(|| anyhow::anyhow!("PocketJS rejected the App framebuffer geometry")) + })?; + + esp_result( + "esp_lcd_dpi_panel_set_pattern", + esp_idf_svc::sys::esp_lcd_dpi_panel_set_pattern( + panel, + esp_idf_svc::sys::mipi_dsi_pattern_type_t_MIPI_DSI_PATTERN_NONE, + ), + )?; + esp_result( + "esp_lcd_panel_disp_on_off", + esp_idf_svc::sys::esp_lcd_panel_disp_on_off(panel, true), + )?; + esp_result("esp_lcd_panel_draw_bitmap", { + esp_idf_svc::sys::esp_lcd_panel_draw_bitmap( + panel, + 0, + 0, + PANEL_WIDTH as i32, + PANEL_HEIGHT as i32, + framebuffers[0], + ) + })?; + esp_result( + "bsp_display_backlight_on", + esp_idf_svc::sys::bsp_display_backlight_on(), + )?; + esp_result( + "pi_p4_touch_new", + esp_idf_svc::sys::pi_p4_touch_new(&mut touch), + )?; + + Ok(DisplayProbe { + panel, + _io: io, + touch, + framebuffers: framebuffers.map(|framebuffer| framebuffer.cast()), + render_states, + next_framebuffer: 1, + }) + } +} + +impl DisplayProbe { + fn render_agentos( + &mut self, + renderer: &mut Renderer, + ppa: &mut EspIdfPpaOps, + supervisor: &AppSupervisor, + ) -> anyhow::Result<()> { + let framebuffer = self.framebuffers[self.next_framebuffer]; + let pixels = unsafe { + core::slice::from_raw_parts_mut( + framebuffer, + PANEL_WIDTH as usize * PANEL_HEIGHT as usize, + ) + }; + supervisor.with_ui(|ui| { + let words = ui.draw().words.clone(); + renderer + .render_incremental( + &mut self.render_states[self.next_framebuffer], + ui, + &words, + pixels, + PANEL_WIDTH, + PANEL_HEIGHT, + ppa, + ) + .ok_or_else(|| anyhow::anyhow!("PocketJS rejected the App framebuffer geometry")) + })?; + esp_result("esp_lcd_panel_draw_bitmap", unsafe { + esp_idf_svc::sys::esp_lcd_panel_draw_bitmap( + self.panel, + 0, + 0, + PANEL_WIDTH as i32, + PANEL_HEIGHT as i32, + framebuffer.cast(), + ) + })?; + self.next_framebuffer = (self.next_framebuffer + 1) % self.framebuffers.len(); + Ok(()) + } +} diff --git a/firmware/esp32-p4/src/app_services.rs b/firmware/esp32-p4/src/app_services.rs new file mode 100644 index 0000000..88d6d37 --- /dev/null +++ b/firmware/esp32-p4/src/app_services.rs @@ -0,0 +1,887 @@ +use core::time::Duration; +use std::collections::BTreeMap; +use std::sync::atomic::{AtomicBool, Ordering}; +use std::sync::{Arc, Mutex}; +use std::time::Instant; + +use embedded_svc::http::{ + client::{Client as HttpClient, Request, Response}, + Method, +}; +use embedded_svc::io::Write as _; +use esp_idf_svc::http::client::{Configuration, EspHttpConnection}; +use esp_idf_svc::io::EspIOError; +use pocket_pi_agentos::{ + AppCatalog, AppServiceHost, CredentialBinding, HttpRequest, McpServicePolicy, NetFailure, + TransportCompletion, +}; +use serde_json::{json, Value}; + +use super::delay_current_task; + +const MCP_PROTOCOL_VERSION: &str = "2025-06-18"; +const MAX_MCP_RESPONSE: usize = 160 * 1024; +const MCP_RETRY_DELAY: Duration = Duration::from_millis(250); + +pub struct EspAppServices { + inner: Arc, +} + +struct EspAppServicesInner { + network_ready: Arc, + catalog: AppCatalog, + credentials: BTreeMap, + mcp: Mutex>, +} + +#[derive(Default)] +struct McpState { + session_id: Option, + next_id: u64, +} + +impl EspAppServices { + pub fn new( + network_ready: Arc, + catalog: AppCatalog, + mut credentials: BTreeMap, + ) -> Self { + let required = catalog.credential_ids(); + credentials.retain(|id, _| required.contains(id)); + let inner = Arc::new(EspAppServicesInner { + network_ready, + catalog, + credentials, + mcp: Mutex::new(BTreeMap::new()), + }); + Self { inner } + } +} + +impl EspAppServicesInner { + fn http( + &self, + app_id: &str, + request: HttpRequest, + deadline: Instant, + ) -> std::result::Result { + let policy = self + .catalog + .http_policy(app_id, &request.method, &request.url) + .ok_or_else(|| NetFailure::new("invalid_request", "HTTP request is not allowed"))?; + if request.headers.keys().any(|name| { + !policy + .allowed_request_headers + .iter() + .any(|item| item == name) + }) { + return Err(NetFailure::new( + "invalid_request", + "App supplied a forbidden HTTP header", + )); + } + execute_http( + request, + policy.credential.as_ref(), + &self.credentials, + deadline, + ) + } + + fn mcp_call(&self, app_id: &str, args: &Value, deadline: Instant) -> Result { + let connection = args + .get("connection") + .and_then(Value::as_str) + .ok_or_else(|| "mcp.client requires connection".to_owned())?; + let policy = self + .catalog + .mcp_policy(app_id, connection) + .cloned() + .ok_or_else(|| "App requested an unknown MCP connection".to_owned())?; + let operation = args + .get("name") + .and_then(Value::as_str) + .ok_or_else(|| "mcp.client callTool requires name".to_owned())?; + let arguments = args.get("arguments").unwrap_or(&Value::Null); + if !self + .catalog + .provider_operation_allowed(app_id, operation) + { + return Err(format!("MCP operation is not allowlisted: {operation}")); + } + let credential = self.credential(&policy.credential)?; + let retryable = args + .get("retryable") + .and_then(Value::as_bool) + .unwrap_or(false); + let mut state = self + .mcp + .lock() + .map_err(|_| "MCP state lock was poisoned".to_owned())?; + let state = state + .entry((app_id.to_owned(), connection.to_owned())) + .or_insert_with(|| McpState { + session_id: None, + next_id: 1, + }); + for attempt in 0..2 { + let result = self.mcp_once( + &policy, + &credential, + state, + operation, + arguments, + retryable, + deadline, + ); + match result { + Ok(value) => return Ok(value), + Err(error) if attempt == 0 && stale_mcp_session(&error) => { + log::warn!("MCP reconnecting after: {error}"); + state.session_id = None; + wait_to_retry(deadline)?; + } + Err(error) if attempt == 0 && retryable && transient_mcp_connect(&error) => { + log::warn!("MCP transport retry after: {error}"); + wait_to_retry(deadline)?; + } + Err(error) => return Err(error), + } + } + unreachable!() + } + + fn mcp_calls(&self, app_id: &str, args: &Value, deadline: Instant) -> Result { + let connection = args + .get("connection") + .and_then(Value::as_str) + .ok_or_else(|| "mcp.client requires connection".to_owned())?; + let policy = self + .catalog + .mcp_policy(app_id, connection) + .cloned() + .ok_or_else(|| "App requested an unknown MCP connection".to_owned())?; + let calls = args + .get("calls") + .and_then(Value::as_array) + .filter(|calls| !calls.is_empty() && calls.len() <= 16) + .ok_or_else(|| "mcp.client callTools requires 1 to 16 calls".to_owned())?; + for call in calls { + let name = call + .get("name") + .and_then(Value::as_str) + .ok_or_else(|| "mcp.client callTools requires a name for every call".to_owned())?; + if !self.catalog.provider_operation_allowed(app_id, name) { + return Err(format!("MCP operation is not allowlisted: {name}")); + } + } + let credential = self.credential(&policy.credential)?; + let retryable = args + .get("retryable") + .and_then(Value::as_bool) + .unwrap_or(false); + let mut state = self + .mcp + .lock() + .map_err(|_| "MCP state lock was poisoned".to_owned())?; + let state = state + .entry((app_id.to_owned(), connection.to_owned())) + .or_insert_with(|| McpState { + session_id: None, + next_id: 1, + }); + for attempt in 0..2 { + let result = + self.mcp_batch_once(&policy, &credential, state, calls, retryable, deadline); + match result { + Ok(value) => return Ok(value), + Err(error) if attempt == 0 && stale_mcp_session(&error) => { + log::warn!("MCP batch reconnecting after: {error}"); + state.session_id = None; + wait_to_retry(deadline)?; + } + Err(error) if attempt == 0 && retryable && transient_mcp_connect(&error) => { + log::warn!("MCP batch transport retry after: {error}"); + wait_to_retry(deadline)?; + } + Err(error) => return Err(error), + } + } + unreachable!() + } + + fn credential(&self, binding: &CredentialBinding) -> Result { + self.credentials + .get(&binding.id) + .map(|secret| format!("{}{}", binding.prefix, secret)) + .ok_or_else(|| format!("credential {} was not provisioned", binding.id)) + } + + fn mcp_batch_once( + &self, + policy: &McpServicePolicy, + credential: &str, + state: &mut McpState, + calls: &[Value], + retryable: bool, + deadline: Instant, + ) -> Result { + self.ensure_mcp_session(policy, credential, state, deadline)?; + let mut requests = Vec::with_capacity(calls.len()); + let mut request_ids = Vec::with_capacity(calls.len()); + for call in calls { + let id = state.next_id; + state.next_id = state.next_id.saturating_add(1); + request_ids.push(id); + requests.push(json!({ + "jsonrpc":"2.0", + "id":id, + "method":"tools/call", + "params":{ + "name":call.get("name").and_then(Value::as_str).unwrap_or_default(), + "arguments":call.get("arguments").unwrap_or(&Value::Null) + } + })); + } + let started = Instant::now(); + let (responses, returned_session) = post_mcp_batch( + policy, + credential, + state.session_id.as_deref(), + &requests, + request_ids.len(), + retryable, + deadline, + )?; + if returned_session.is_some() { + state.session_id = returned_session; + } + let mut by_id = BTreeMap::new(); + for response in responses { + if let Some(id) = response.get("id").and_then(Value::as_u64) { + by_id.insert(id, response); + } + } + let mut results = Vec::with_capacity(calls.len()); + for (index, call) in calls.iter().enumerate() { + let name = call.get("name").and_then(Value::as_str).unwrap_or_default(); + let response = by_id + .remove(&request_ids[index]) + .ok_or_else(|| format!("MCP batch omitted response for {name}"))?; + match normalize_mcp_result(&response) { + Ok(value) => results.push(json!({"name":name,"ok":true,"value":value})), + Err(error) => results.push(json!({"name":name,"ok":false,"error":error})), + } + } + log::info!( + "MCP batch calls={} completed in {}ms", + calls.len(), + started.elapsed().as_millis() + ); + Ok(json!({"results":results})) + } + + fn ensure_mcp_session( + &self, + policy: &McpServicePolicy, + credential: &str, + state: &mut McpState, + deadline: Instant, + ) -> Result<(), String> { + if state.session_id.is_some() { + return Ok(()); + } + let request = json!({ + "jsonrpc":"2.0", + "id":state.next_id, + "method":"initialize", + "params":{"protocolVersion":MCP_PROTOCOL_VERSION,"capabilities":{},"clientInfo":{"name":"pocket-pi-agentos","version":"0.1.0"}} + }); + state.next_id = state.next_id.saturating_add(1); + let (body, session) = post_mcp(policy, credential, None, &request, true, deadline)?; + if let Some(error) = body.get("error") { + return Err(format!("MCP initialize failed: {error}")); + } + let session = session.ok_or_else(|| "MCP omitted session id".to_owned())?; + let notification = json!({"jsonrpc":"2.0","method":"notifications/initialized"}); + let _ = post_mcp( + policy, + credential, + Some(&session), + ¬ification, + true, + deadline, + )?; + state.session_id = Some(session); + log::info!("MCP session initialized"); + Ok(()) + } + + fn mcp_once( + &self, + policy: &McpServicePolicy, + credential: &str, + state: &mut McpState, + operation: &str, + args: &Value, + retryable: bool, + deadline: Instant, + ) -> Result { + let operation_started = Instant::now(); + log::info!("MCP {operation} started"); + self.ensure_mcp_session(policy, credential, state, deadline)?; + let request = json!({ + "jsonrpc":"2.0", + "id":state.next_id, + "method":"tools/call", + "params":{"name":operation,"arguments":args} + }); + state.next_id = state.next_id.saturating_add(1); + let (body, returned_session) = post_mcp( + policy, + credential, + state.session_id.as_deref(), + &request, + retryable, + deadline, + )?; + if returned_session.is_some() { + state.session_id = returned_session; + } + let value = normalize_mcp_result(&body)?; + log::info!( + "MCP {operation} completed in {}ms", + operation_started.elapsed().as_millis() + ); + Ok(value) + } +} + +impl AppServiceHost for EspAppServices { + fn call( + &self, + app_id: &str, + service: &str, + operation: &str, + args: &Value, + deadline: Instant, + ) -> Result { + if !self.inner.network_ready.load(Ordering::Acquire) { + return Err("Network is not connected; App data was not changed".to_owned()); + } + match (service, operation) { + ("mcp.client", "callTool") => self.inner.mcp_call(app_id, args, deadline), + ("mcp.client", "callTools") => self.inner.mcp_calls(app_id, args, deadline), + _ => Err(format!("App {app_id} cannot access service {service}")), + } + } + + fn http( + &self, + app_id: &str, + request: HttpRequest, + deadline: Instant, + ) -> std::result::Result { + if !self.inner.network_ready.load(Ordering::Acquire) { + return Err(NetFailure::new("unavailable", "network is not connected")); + } + self.inner.http(app_id, request, deadline) + } +} + +fn connection(timeout: Duration) -> Result { + EspHttpConnection::new(&Configuration { + buffer_size: Some(8 * 1024), + buffer_size_tx: Some(4 * 1024), + timeout: Some(timeout), + crt_bundle_attach: Some(esp_idf_svc::sys::esp_crt_bundle_attach), + ..Default::default() + }) + .map_err(|error| format!("initialize HTTPS client: {error}")) +} + +fn client(timeout: Duration) -> Result, String> { + connection(timeout).map(HttpClient::wrap) +} + +fn stale_mcp_session(error: &str) -> bool { + let lower = error.to_ascii_lowercase(); + (lower.contains("mcp http 400") || lower.contains("mcp http 404")) && lower.contains("session") +} + +fn transient_mcp_connect(error: &str) -> bool { + error.contains("ESP_ERR_HTTP_CONNECT") +} + +fn remaining(deadline: Instant) -> Result { + deadline + .checked_duration_since(Instant::now()) + .filter(|duration| !duration.is_zero()) + .ok_or_else(|| "App Data Action deadline expired".to_owned()) +} + +fn yield_current_task() { + unsafe { esp_idf_svc::sys::vTaskDelay(1) }; +} + +fn wait_to_retry(deadline: Instant) -> Result<(), String> { + let delay = remaining(deadline)?.min(MCP_RETRY_DELAY); + delay_current_task(delay); + remaining(deadline).map(|_| ()) +} + +fn execute_http( + meta: HttpRequest, + credential: Option<&CredentialBinding>, + credentials: &BTreeMap, + deadline: Instant, +) -> std::result::Result { + let length = meta.body.len().to_string(); + let mut values = meta.headers; + values.insert("content-length".into(), length); + values.insert("connection".into(), "close".into()); + values.insert("user-agent".into(), "pocket-pi-agentos/0.1".into()); + if let Some(binding) = credential { + let secret = credentials.get(&binding.id).ok_or_else(|| { + NetFailure::new( + "unavailable", + format!("credential {} was not provisioned", binding.id), + ) + })?; + values.insert( + binding.header.clone(), + format!("{}{}", binding.prefix, secret), + ); + } + let headers = values + .iter() + .map(|(name, value)| (name.as_str(), value.as_str())) + .collect::>(); + let mut client = client(remaining(deadline).map_err(net_failure)?).map_err(net_failure)?; + let mut request = client + .request(http_method(&meta.method)?, &meta.url, &headers) + .map_err(|error| net_failure(format!("create HTTPS request: {error}")))?; + request + .write_all(&meta.body) + .map_err(|error| net_failure(format!("write HTTPS request: {error}")))?; + request + .flush() + .map_err(|error| net_failure(format!("flush HTTPS request: {error}")))?; + let mut response = request + .submit() + .map_err(|error| net_failure(format!("send HTTPS request: {error}")))?; + let status = response.status(); + let content_type = response.header("content-type").map(str::to_owned); + let expected_length = response + .header("content-length") + .and_then(|value| value.parse::().ok()); + let body = read_bounded(&mut response, meta.max_bytes, expected_length, deadline) + .map_err(net_failure)?; + let mut response_headers = BTreeMap::new(); + if let Some(content_type) = content_type { + response_headers.insert("content-type".to_owned(), content_type); + } + Ok(TransportCompletion::Done { + handle: meta.handle, + status, + url: meta.url, + headers: response_headers, + body, + }) +} + +fn http_method(method: &str) -> std::result::Result { + match method { + "GET" => Ok(Method::Get), + "POST" => Ok(Method::Post), + "PUT" => Ok(Method::Put), + "DELETE" => Ok(Method::Delete), + "PATCH" => Ok(Method::Patch), + _ => Err(NetFailure::new( + "invalid_request", + format!("unsupported HTTP method {method}"), + )), + } +} + +fn net_failure(message: String) -> NetFailure { + let lower = message.to_ascii_lowercase(); + let code = if lower.contains("exceeded") { + "response_too_large" + } else if lower.contains("timeout") { + "timeout" + } else if lower.contains("tls") || lower.contains("certificate") { + "tls" + } else if lower.contains("connect") { + "connect" + } else { + "other" + }; + NetFailure::new(code, message) +} + +fn submit_mcp( + policy: &McpServicePolicy, + credential: &str, + session: Option<&str>, + payload: &str, + request_label: &str, + retryable: bool, + deadline: Instant, +) -> Result, String> { + let length = payload.len().to_string(); + for attempt in 0..2 { + let mut headers = vec![ + ("accept", "application/json, text/event-stream"), + ("content-type", "application/json"), + ("content-length", length.as_str()), + (policy.credential.header.as_str(), credential), + ("user-agent", "pocket-pi-agentos/0.1"), + ]; + if let Some(session) = session { + headers.push(("mcp-session-id", session)); + } + let mut connection = connection(remaining(deadline)?)?; + connection + .initiate_request(Method::Post, &policy.url, &headers) + .map_err(|error| format!("create MCP request: {error:?}"))?; + let mut request = Request::wrap(connection); + request + .write_all(payload.as_bytes()) + .map_err(|error| format!("write MCP request: {error:?}"))?; + request + .flush() + .map_err(|error| format!("flush MCP request: {error:?}"))?; + let started = Instant::now(); + match request.submit() { + Ok(response) => { + log::info!( + "MCP HTTP {request_label} headers status={} in {}ms", + response.status(), + started.elapsed().as_millis() + ); + return Ok(response); + } + Err(error) + if attempt == 0 + && retryable + && error.0.code() == -esp_idf_svc::sys::ESP_ERR_HTTP_EAGAIN => + { + log::warn!( + "MCP {request_label} received no response headers in {}ms; reconnecting once", + started.elapsed().as_millis() + ); + wait_to_retry(deadline)?; + } + Err(error) => { + return Err(format!("send MCP request: {error:?}")); + } + } + } + unreachable!() +} + +fn post_mcp( + policy: &McpServicePolicy, + credential: &str, + session: Option<&str>, + body: &Value, + retryable: bool, + deadline: Instant, +) -> Result<(Value, Option), String> { + let request_label = body + .get("params") + .and_then(|params| params.get("name")) + .and_then(Value::as_str) + .or_else(|| body.get("method").and_then(Value::as_str)) + .unwrap_or("unknown"); + let payload = body.to_string(); + let mut response = submit_mcp( + policy, + credential, + session, + &payload, + request_label, + retryable, + deadline, + )?; + let status = response.status(); + let returned_session = response.header("Mcp-Session-Id").map(str::to_owned); + let is_event_stream = response + .header("content-type") + .is_some_and(|value| value.to_ascii_lowercase().contains("text/event-stream")); + // JSON-RPC notifications have no response payload. The MCP endpoint keeps + // its HTTP connection alive, so waiting for EOF here would block forever. + if body.get("id").is_none() && (200..300).contains(&status) { + return Ok((Value::Null, returned_session)); + } + let body_started = Instant::now(); + let bytes = if is_event_stream { + read_sse_event(&mut response, MAX_MCP_RESPONSE, deadline)? + } else { + let expected_length = response + .header("content-length") + .and_then(|value| value.parse::().ok()); + read_bounded(&mut response, MAX_MCP_RESPONSE, expected_length, deadline)? + }; + log::info!( + "MCP HTTP {request_label} body bytes={} in {}ms", + bytes.len(), + body_started.elapsed().as_millis() + ); + if !(200..300).contains(&status) { + return Err(format!( + "MCP HTTP {status}: {}", + String::from_utf8_lossy(&bytes) + )); + } + let body = parse_json_or_sse(&bytes)?; + Ok((body, returned_session)) +} + +fn post_mcp_batch( + policy: &McpServicePolicy, + credential: &str, + session: Option<&str>, + bodies: &[Value], + expected_responses: usize, + retryable: bool, + deadline: Instant, +) -> Result<(Vec, Option), String> { + let payload = + serde_json::to_string(bodies).map_err(|error| format!("encode MCP batch: {error}"))?; + let mut response = submit_mcp( + policy, + credential, + session, + &payload, + "batch", + retryable, + deadline, + )?; + let status = response.status(); + let returned_session = response.header("Mcp-Session-Id").map(str::to_owned); + if !(200..300).contains(&status) { + let bytes = read_bounded(&mut response, MAX_MCP_RESPONSE, None, deadline)?; + return Err(format!( + "MCP batch HTTP {status}: {}", + String::from_utf8_lossy(&bytes) + )); + } + let body_started = Instant::now(); + let responses = read_sse_values( + &mut response, + MAX_MCP_RESPONSE, + expected_responses, + deadline, + )?; + log::info!( + "MCP HTTP batch responses={} in {}ms", + responses.len(), + body_started.elapsed().as_millis() + ); + Ok((responses, returned_session)) +} + +fn read_bounded( + reader: &mut impl embedded_svc::io::Read, + limit: usize, + expected_length: Option, + deadline: Instant, +) -> Result, String> { + if expected_length.is_some_and(|length| length > limit) { + return Err(format!("HTTPS response exceeded {limit} bytes")); + } + let mut out = Vec::with_capacity(8 * 1024); + let mut chunk = [0u8; 2048]; + loop { + remaining(deadline)?; + let count = reader + .read(&mut chunk) + .map_err(|error| format!("read HTTPS response: {error:?}"))?; + if count == 0 { + if let Some(expected) = expected_length { + if out.len() < expected { + return Err(format!( + "HTTPS response ended after {} of {expected} bytes", + out.len() + )); + } + } + break; + } + if out.len().saturating_add(count) > limit { + return Err(format!("HTTPS response exceeded {limit} bytes")); + } + out.extend_from_slice(&chunk[..count]); + if expected_length.is_some_and(|expected| out.len() >= expected) { + break; + } + } + Ok(out) +} + +fn read_sse_event( + reader: &mut impl embedded_svc::io::Read, + limit: usize, + deadline: Instant, +) -> Result, String> { + let mut out = Vec::with_capacity(8 * 1024); + // Stop after the first complete SSE event because the server keeps this + // connection alive. Reading one byte at a time made a normal MCP payload + // expensive enough to trip the ESP32 task watchdog, so consume the bytes + // already available from esp_http_client in bounded chunks instead. + let mut chunk = [0u8; 512]; + loop { + remaining(deadline)?; + let count = reader + .read(&mut chunk) + .map_err(|error| format!("read MCP SSE response: {error:?}"))?; + if count == 0 { + break; + } + if out.len().saturating_add(count) > limit { + return Err(format!("MCP SSE response exceeded {limit} bytes")); + } + out.extend_from_slice(&chunk[..count]); + if let Some(end) = sse_event_end(&out) { + out.truncate(end); + return Ok(out); + } + } + Ok(out) +} + +fn read_sse_values( + reader: &mut R, + limit: usize, + expected: usize, + deadline: Instant, +) -> Result, String> +where + R: embedded_svc::io::Read, +{ + let mut buffered = Vec::with_capacity(16 * 1024); + let mut consumed = 0; + let mut values = Vec::with_capacity(expected); + let mut chunk = [0u8; 2048]; + let mut waiting_logged = false; + while values.len() < expected { + remaining(deadline)?; + let count = match reader.read(&mut chunk) { + Ok(count) => count, + Err(error) + if error.0.code() == -esp_idf_svc::sys::ESP_ERR_HTTP_EAGAIN + && Instant::now() < deadline => + { + if !waiting_logged { + log::info!( + "MCP batch waiting for SSE responses ({}/{expected})", + values.len() + ); + waiting_logged = true; + } + yield_current_task(); + continue; + } + Err(error) if error.0.code() == -esp_idf_svc::sys::ESP_ERR_HTTP_EAGAIN => { + return Err(format!( + "MCP batch timed out after receiving {} of {expected} responses", + values.len() + )); + } + Err(error) => return Err(format!("read MCP batch SSE response: {error:?}")), + }; + if count == 0 { + break; + } + if buffered.len().saturating_add(count) > limit { + return Err(format!("MCP batch SSE response exceeded {limit} bytes")); + } + buffered.extend_from_slice(&chunk[..count]); + while let Some(relative_end) = buffered[consumed..].iter().position(|byte| *byte == b'\n') { + let end = consumed + relative_end; + let line = &buffered[consumed..end]; + let line = line.strip_suffix(b"\r").unwrap_or(line); + if let Some(data) = line.strip_prefix(b"data:") { + let data = data.strip_prefix(b" ").unwrap_or(data); + values.push( + serde_json::from_slice(data) + .map_err(|error| format!("parse MCP batch SSE JSON: {error}"))?, + ); + } + consumed = end + 1; + } + } + if values.len() != expected { + return Err(format!( + "MCP batch returned {} of {expected} responses", + values.len() + )); + } + Ok(values) +} + +fn sse_event_end(bytes: &[u8]) -> Option { + // Some MCP servers send one complete JSON value on a `data:` line but keep + // the chunked stream open without promptly sending the optional blank + // event separator. A terminated data line is already a complete response. + for start in 0..bytes.len() { + if (start == 0 || bytes[start - 1] == b'\n') && bytes[start..].starts_with(b"data:") { + if let Some(relative_end) = bytes[start..].iter().position(|byte| *byte == b'\n') { + return Some(start + relative_end + 1); + } + } + } + bytes + .windows(4) + .position(|window| window == b"\r\n\r\n") + .map(|index| index + 4) + .or_else(|| { + bytes + .windows(2) + .position(|window| window == b"\n\n") + .map(|index| index + 2) + }) +} + +fn parse_json_or_sse(bytes: &[u8]) -> Result { + // JSON-RPC notifications (notably MCP notifications/initialized) have no + // response body on success. Treat that as a valid null result; tools/call + // still goes through normalize_mcp_result and therefore requires result. + if bytes.iter().all(u8::is_ascii_whitespace) { + return Ok(Value::Null); + } + if let Ok(value) = serde_json::from_slice(bytes) { + return Ok(value); + } + for line in String::from_utf8_lossy(bytes).lines().rev() { + if let Some(data) = line.strip_prefix("data: ") { + return serde_json::from_str(data) + .map_err(|error| format!("parse MCP SSE JSON: {error}")); + } + } + Err("MCP response was neither JSON nor SSE".to_owned()) +} + +fn normalize_mcp_result(body: &Value) -> Result { + if let Some(error) = body.get("error") { + return Err(format!("MCP error: {error}")); + } + let result = body + .get("result") + .ok_or_else(|| "MCP response omitted result".to_owned())?; + let text = result + .get("content") + .and_then(Value::as_array) + .into_iter() + .flatten() + .find(|item| item.get("type").and_then(Value::as_str) == Some("text")) + .and_then(|item| item.get("text")) + .and_then(Value::as_str) + .ok_or_else(|| "MCP tool returned no text".to_owned())?; + if result.get("isError").and_then(Value::as_bool) == Some(true) { + return Err(text.to_owned()); + } + Ok(serde_json::from_str(text).unwrap_or_else(|_| Value::String(text.to_owned()))) +} diff --git a/firmware/esp32-p4/src/backend/uart.rs b/firmware/esp32-p4/src/backend/uart.rs index 00ddeac..9ee821f 100644 --- a/firmware/esp32-p4/src/backend/uart.rs +++ b/firmware/esp32-p4/src/backend/uart.rs @@ -2,6 +2,7 @@ use std::sync::Arc; use std::time::Duration; use pocket_pi_embedded::ModelBackend; +use pocket_pi_protocols::model::ModelStreamEvent; use crate::transport::LineTransport; @@ -23,7 +24,7 @@ impl ModelBackend for UartBackend { fn complete( &self, request_json: &str, - on_delta: &mut dyn FnMut(&str), + _on_event: &mut dyn FnMut(ModelStreamEvent), ) -> Result { self.transport.write_line("PPI-RPC-WAITING"); self.transport @@ -42,12 +43,6 @@ impl ModelBackend for UartBackend { ) .map_err(|error| format!("UART stream JSON: {error}"))?; match event.get("type").and_then(serde_json::Value::as_str) { - Some("text_delta") => on_delta( - event - .get("text") - .and_then(serde_json::Value::as_str) - .ok_or_else(|| "UART text delta is missing text".to_owned())?, - ), Some("done") => { return serde_json::to_string( event diff --git a/firmware/esp32-p4/src/backend/wireless.rs b/firmware/esp32-p4/src/backend/wireless.rs index 2302515..cb68b50 100644 --- a/firmware/esp32-p4/src/backend/wireless.rs +++ b/firmware/esp32-p4/src/backend/wireless.rs @@ -1,15 +1,18 @@ use core::time::Duration; +use std::cell::RefCell; use embedded_svc::http::{client::Client as HttpClient, Method}; use embedded_svc::io::Write; use esp_idf_svc::http::client::{Configuration, EspHttpConnection}; use pocket_pi_embedded::ModelBackend; use pocket_pi_protocols::anthropic_messages; -use pocket_pi_protocols::model::WirelessProvider; +use pocket_pi_protocols::model::{ModelStreamEvent, WirelessProvider}; use pocket_pi_protocols::openai_chat; -const MAX_REQUEST_BYTES: usize = 128 * 1024; -const MAX_RESPONSE_BYTES: usize = 128 * 1024; +// EspHttpConnection is thread-affine, so the resident model worker owns it. +thread_local! { + static MODEL_CLIENT: RefCell>> = const { RefCell::new(None) }; +} pub struct WirelessBackend { provider: WirelessProvider, @@ -34,42 +37,18 @@ impl WirelessBackend { "https://openrouter.ai/api/v1/chat/completions", openai_chat::build_request_for(pi_request, openai_chat::Dialect::OpenRouter)?, )), + WirelessProvider::DeepSeek => Ok(( + "https://api.deepseek.com/chat/completions", + openai_chat::build_request_for(pi_request, openai_chat::Dialect::DeepSeek)?, + )), WirelessProvider::Anthropic => Ok(( "https://api.anthropic.com/v1/messages", anthropic_messages::build_request(pi_request)?, )), } } -} - -impl ModelBackend for WirelessBackend { - fn complete( - &self, - request_json: &str, - on_delta: &mut dyn FnMut(&str), - ) -> Result { - let (endpoint, body) = self.request(request_json)?; - if body.len() > MAX_REQUEST_BYTES { - return Err("model request exceeded 128 KiB".into()); - } - let content_length = body.len().to_string(); - let bearer = format!("Bearer {}", self.api_key); - let mut headers = vec![ - ("accept", "text/event-stream"), - ("content-type", "application/json"), - ("content-length", content_length.as_str()), - ("user-agent", "pocket-pi-p4/0.1"), - ]; - match self.provider { - WirelessProvider::Anthropic => { - headers.push(("x-api-key", self.api_key.as_str())); - headers.push(("anthropic-version", "2023-06-01")); - } - WirelessProvider::OpenAi | WirelessProvider::OpenRouter => { - headers.push(("authorization", bearer.as_str())); - } - } + fn connect() -> Result, String> { let configuration = Configuration { buffer_size: Some(4 * 1024), buffer_size_tx: Some(4 * 1024), @@ -77,70 +56,150 @@ impl ModelBackend for WirelessBackend { crt_bundle_attach: Some(esp_idf_svc::sys::esp_crt_bundle_attach), ..Default::default() }; - let mut client = HttpClient::wrap( + Ok(HttpClient::wrap( EspHttpConnection::new(&configuration) .map_err(|error| format!("initialize HTTPS: {error}"))?, - ); + )) + } + + fn send( + &self, + client: &mut HttpClient, + endpoint: &str, + body: &str, + headers: &[(&str, &str)], + on_event: &mut dyn FnMut(ModelStreamEvent), + ) -> Result { let mut request = client - .request(Method::Post, endpoint, &headers) - .map_err(|error| format!("create model request: {error}"))?; + .request(Method::Post, endpoint, headers) + .map_err(|error| SendError::BeforeResponse(format!("create model request: {error}")))?; request .write_all(body.as_bytes()) - .map_err(|error| format!("write model request: {error}"))?; + .map_err(|error| SendError::BeforeResponse(format!("write model request: {error}")))?; request .flush() - .map_err(|error| format!("flush model request: {error}"))?; + .map_err(|error| SendError::BeforeResponse(format!("flush model request: {error}")))?; let mut response = request .submit() - .map_err(|error| format!("send model request: {error}"))?; + .map_err(|error| SendError::BeforeResponse(format!("send model request: {error}")))?; let status = response.status(); let mut decoder = match self.provider { WirelessProvider::Anthropic => ProviderStream::Anthropic(Default::default()), + WirelessProvider::DeepSeek => { + ProviderStream::Chat(openai_chat::Stream::new(openai_chat::Dialect::DeepSeek)) + } WirelessProvider::OpenAi | WirelessProvider::OpenRouter => { ProviderStream::Chat(Default::default()) } }; - let mut received = 0usize; let mut pending = Vec::with_capacity(4 * 1024); let mut chunk = [0u8; 2 * 1024]; loop { let count = response .read(&mut chunk) - .map_err(|error| format!("read model stream: {error}"))?; + .map_err(|error| SendError::AfterResponse(format!("read model stream: {error}")))?; if count == 0 { break; } - received = received.saturating_add(count); - if received > MAX_RESPONSE_BYTES { - return Err("model response exceeded 128 KiB".into()); - } pending.extend_from_slice(&chunk[..count]); if (200..300).contains(&status) { - drain_sse_lines(&mut pending, &mut decoder, on_delta)?; + drain_sse_lines(&mut pending, &mut decoder, on_event) + .map_err(SendError::AfterResponse)?; } } if !(200..300).contains(&status) { - return Err(format!( + return Err(SendError::AfterResponse(format!( "{} returned HTTP {status}: {}", self.provider.id(), String::from_utf8_lossy(&pending) .chars() .take(400) .collect::() - )); + ))); } if !pending.is_empty() { pending.push(b'\n'); - drain_sse_lines(&mut pending, &mut decoder, on_delta)?; + drain_sse_lines(&mut pending, &mut decoder, on_event) + .map_err(SendError::AfterResponse)?; + } + decoder.finish().map_err(SendError::AfterResponse) + } +} + +impl ModelBackend for WirelessBackend { + fn complete( + &self, + request_json: &str, + on_event: &mut dyn FnMut(ModelStreamEvent), + ) -> Result { + let (endpoint, body) = self.request(request_json)?; + let content_length = body.len().to_string(); + let bearer = format!("Bearer {}", self.api_key); + let mut headers = vec![ + ("accept", "text/event-stream"), + ("content-type", "application/json"), + ("content-length", content_length.as_str()), + ("user-agent", "pocket-pi-p4/0.1"), + ]; + match self.provider { + WirelessProvider::Anthropic => { + headers.push(("x-api-key", self.api_key.as_str())); + headers.push(("anthropic-version", "2023-06-01")); + } + WirelessProvider::OpenAi + | WirelessProvider::OpenRouter + | WirelessProvider::DeepSeek => { + headers.push(("authorization", bearer.as_str())); + } + } + + MODEL_CLIENT.with(|slot| { + let mut client = slot.borrow_mut(); + let mut retried = false; + loop { + if client.is_none() { + *client = Some(Self::connect()?); + } + match self.send( + client.as_mut().unwrap(), + endpoint, + &body, + &headers, + on_event, + ) { + Ok(value) => return Ok(value), + Err(SendError::BeforeResponse(error)) if !retried => { + log::warn!("model transport retry after: {error}"); + *client = None; + retried = true; + } + Err(error) => { + *client = None; + return Err(error.message()); + } + } + } + }) + } +} + +enum SendError { + BeforeResponse(String), + AfterResponse(String), +} + +impl SendError { + fn message(self) -> String { + match self { + Self::BeforeResponse(message) | Self::AfterResponse(message) => message, } - decoder.finish() } } fn drain_sse_lines( pending: &mut Vec, decoder: &mut ProviderStream, - on_delta: &mut dyn FnMut(&str), + on_event: &mut dyn FnMut(ModelStreamEvent), ) -> Result<(), String> { while let Some(end) = pending.iter().position(|byte| *byte == b'\n') { let mut line = pending.drain(..=end).collect::>(); @@ -154,8 +213,8 @@ fn drain_sse_lines( }; let data = data.trim_start(); if !data.is_empty() && data != "[DONE]" { - if let Some(delta) = decoder.push(data)? { - on_delta(&delta); + for event in decoder.push(data)? { + on_event(event); } } } @@ -168,7 +227,7 @@ enum ProviderStream { } impl ProviderStream { - fn push(&mut self, data: &str) -> Result, String> { + fn push(&mut self, data: &str) -> Result, String> { match self { Self::Chat(stream) => stream.push(data), Self::Anthropic(stream) => stream.push(data), diff --git a/firmware/esp32-p4/src/device_state.rs b/firmware/esp32-p4/src/device_state.rs new file mode 100644 index 0000000..85aa570 --- /dev/null +++ b/firmware/esp32-p4/src/device_state.rs @@ -0,0 +1,23 @@ +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct WifiNetworkProjection { + pub ssid: String, + pub rssi_dbm: i16, + pub secured: bool, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct WifiSettingsProjection { + pub connected_ssid: Option, + pub ip_address: Option, + pub rssi_dbm: Option, + pub scanning: bool, + pub networks: Vec, + pub status: String, +} + +#[derive(Clone, Debug, Default, Eq, PartialEq)] +pub struct SettingsProjection { + pub wifi: WifiSettingsProjection, + pub firmware_version: String, + pub workspace_free_bytes: Option, +} diff --git a/firmware/esp32-p4/src/main.rs b/firmware/esp32-p4/src/main.rs index 6018436..19bf735 100644 --- a/firmware/esp32-p4/src/main.rs +++ b/firmware/esp32-p4/src/main.rs @@ -1,383 +1,99 @@ use core::time::Duration; -use std::sync::Arc; use std::time::Instant; -use embedded_svc::http::{client::Client as HttpClient, Method}; use esp_idf_svc::eventloop::EspSystemEventLoop; use esp_idf_svc::hal::peripherals::Peripherals; use esp_idf_svc::handle::RawHandle; -use esp_idf_svc::http::client::{Configuration as HttpConfiguration, EspHttpConnection}; use esp_idf_svc::netif::{EspNetif, NetifStack}; use esp_idf_svc::nvs::{EspDefaultNvs, EspDefaultNvsPartition}; use esp_idf_svc::wifi::{AuthMethod, ClientConfiguration, Configuration, WifiDriver}; -use pocket_pi_device_ui::{ - load_fonts, AgentState, ChatProjection, DeviceState, ScreenInteraction, ScreenState, - SettingsCommand, SettingsProjection, SystemTelemetry, -}; -use pocket_pi_embedded::{spawn_agent_worker, AgentEvent, ModelBackend}; -use pocket_pi_protocols::model::ModelBackendSettings; -use pocket_pi_tools::{CoreToolHost, PlatformTools}; -use pocketjs_core::Ui; -use pocketjs_esp32p4_ppa::{PpaOps, Rect, Renderer, RendererConfig, SrmTransform}; +use pocket_pi_tools::PlatformTools; +use pocketjs_esp32p4_ppa::RenderTargetState; +mod agentos_main; +mod app_services; mod backend; +mod device_state; mod storage; mod transport; +use device_state::{SettingsProjection, WifiNetworkProjection}; + const BOARD_NAME: &str = "Waveshare ESP32-P4-WIFI6-Touch-LCD-5"; const PANEL_WIDTH: u32 = 720; const PANEL_HEIGHT: u32 = 1280; -const LCD_REFRESH_HZ: u16 = 32; const WIFI_NVS_NAMESPACE: &str = "pocket_pi"; const WIFI_NVS_SSID_KEY: &str = "wifi_ssid"; const WIFI_NVS_PASSWORD_KEY: &str = "wifi_pass"; +const AGENTOS_LAUNCHER_STACK_BYTES: u32 = 4 * 1024; +const AGENTOS_TASK_STACK_BYTES: u32 = 64 * 1024; + +fn delay_current_task(delay: Duration) { + let ticks = esp_idf_svc::hal::delay::TickType::from(delay) + .ticks() + .max(1); + unsafe { esp_idf_svc::sys::vTaskDelay(ticks) }; +} fn main() -> anyhow::Result<()> { esp_idf_svc::sys::link_patches(); esp_idf_svc::log::EspLogger::initialize_default(); - let mut ui = Ui::new(); - ui.set_viewport(PANEL_WIDTH as f32, PANEL_HEIGHT as f32); - if !load_fonts(&mut ui) { - anyhow::bail!("PocketJS rejected the shared Inter font atlases") - } - let _workspace = storage::mount_workspace()?; - - let mut device = DeviceState { - agent: AgentState::Starting, - }; - let mut chat = ChatProjection::new("TYPE A MESSAGE", "BOOTING PI AGENT..."); - let mut screen = ScreenState::new(storage::WORKSPACE_ROOT); - screen.refresh_workspace(); - screen.set_telemetry(system_telemetry(0)); - - let mut renderer = Renderer::new(RendererConfig::default()) - .ok_or_else(|| anyhow::anyhow!("invalid PocketJS renderer configuration"))?; - log::info!("Pocket Pi ESP32-P4 hardware probe: {BOARD_NAME}"); - log::info!( - "PocketJS shared device UI ready: viewport={:?} scale={}", - ui.viewport(), - renderer.config().scale, - ); - - let mut display = match init_display_probe(&mut renderer, &ui, &device, &chat, &screen) { - Ok(display) => { - log::info!("MIPI-DSI panel active"); - Some(display) - } - Err(error) => { - log::error!("MIPI-DSI panel failed: {error:#}"); - None - } + let mut task = core::ptr::null_mut(); + let result = unsafe { + esp_idf_svc::sys::xTaskCreatePinnedToCoreWithCaps( + Some(agentos_launcher_task), + c"agentos-launch".as_ptr(), + AGENTOS_LAUNCHER_STACK_BYTES, + core::ptr::null_mut(), + esp_idf_svc::sys::ESP_TASK_MAIN_PRIO, + &mut task, + 0, + esp_idf_svc::sys::MALLOC_CAP_SPIRAM | esp_idf_svc::sys::MALLOC_CAP_8BIT, + ) }; + if result != 1 || task.is_null() { + anyhow::bail!("could not create AgentOS launcher task (result={result})") + } + log::info!("AgentOS runtime launcher queued"); + Ok(()) +} - let uart = Arc::new( - transport::UartLineTransport::new() - .map_err(|error| anyhow::anyhow!("initialize UART transport: {error}"))?, +unsafe extern "C" fn agentos_launcher_task(_argument: *mut core::ffi::c_void) { + // Let ESP-IDF delete its entry task first. That releases enough contiguous + // internal RAM for the large runtime stack required by QuickJS. The tiny + // launcher itself lives in PSRAM and never performs flash I/O. + delay_current_task(Duration::from_millis(100)); + let mut task = core::ptr::null_mut(); + let result = esp_idf_svc::sys::xTaskCreatePinnedToCoreWithCaps( + Some(agentos_task), + c"agentos".as_ptr(), + AGENTOS_TASK_STACK_BYTES, + core::ptr::null_mut(), + esp_idf_svc::sys::ESP_TASK_MAIN_PRIO, + &mut task, + 0, + esp_idf_svc::sys::MALLOC_CAP_INTERNAL | esp_idf_svc::sys::MALLOC_CAP_8BIT, ); - let runtime_config = - match transport::request_runtime_config(uart.as_ref(), Duration::from_secs(5)) { - Ok(config) => config, - Err(error) => { - log::warn!("No UART runtime config received: {error}"); - transport::RuntimeConfig::default() - } - }; - let clock_seeded_by_uart = if let Some(seconds) = runtime_config.unix_time_seconds { - let time = esp_idf_svc::sys::timeval { - tv_sec: seconds as i64, - tv_usec: 0, - }; - if unsafe { esp_idf_svc::sys::settimeofday(&time, core::ptr::null()) } == 0 { - log::info!("clock seeded by UART bridge"); - true - } else { - log::warn!("UART bridge clock seed failed"); - false - } - } else { - false - }; - screen.set_model_backend(&runtime_config.model); - - let mut wifi = match init_wifi( - runtime_config.wifi_ssid.as_deref(), - runtime_config.wifi_password.as_deref(), - ) { - Ok(wifi) => { - log::info!("C6-SDIO Wi-Fi and lwIP netif active"); - Some(wifi) - } - Err(error) => { - log::error!("C6-SDIO Wi-Fi radio probe failed: {error:#}"); - None - } - }; - let mut settings = wifi - .as_ref() - .map(|wifi| wifi.projection("READY")) - .unwrap_or_else(|| SettingsProjection { - firmware_version: env!("CARGO_PKG_VERSION").into(), - workspace_free_bytes: storage::workspace_free_bytes().ok(), - wifi: pocket_pi_device_ui::WifiSettingsProjection { - status: "WI-FI DRIVER UNAVAILABLE".into(), - ..Default::default() - }, - ..Default::default() - }); - screen.set_settings(settings.clone()); - let _sntp = if wifi.is_some() { - match esp_idf_svc::sntp::EspSntp::new_default() { - Ok(sntp) => { - if !clock_seeded_by_uart && wifi.as_ref().is_some_and(WifiConnection::is_connected) - { - let started = Instant::now(); - while started.elapsed() < Duration::from_secs(15) - && sntp.get_sync_status() != esp_idf_svc::sntp::SyncStatus::Completed - { - std::thread::sleep(Duration::from_millis(200)); - } - if sntp.get_sync_status() == esp_idf_svc::sntp::SyncStatus::Completed { - log::info!("SNTP clock synchronized"); - } else { - log::warn!("SNTP synchronization is still pending"); - } - } - Some(sntp) - } - Err(error) => { - log::error!("SNTP initialization failed: {error}"); - None - } - } - } else { - None - }; - let direct_wireless = matches!( - &runtime_config.model.backend, - ModelBackendSettings::Wireless { .. } - ); - if direct_wireless && wifi.as_ref().is_some_and(WifiConnection::is_connected) { + if result == 1 && !task.is_null() { log::info!( - "network targets staged: openai={} codex_plan={}", - pocket_pi_protocols::model::OPENAI_API_BASE_URL, - pocket_pi_protocols::model::CODEX_BACKEND_BASE_URL, + "AgentOS runtime started on a {} KiB internal stack", + AGENTOS_TASK_STACK_BYTES / 1024 ); - match probe_https_origins() { - Ok(reachability) => log::info!("HTTPS connectivity probe completed: {reachability:?}"), - Err(error) => log::error!("HTTPS connectivity probe failed: {error:#}"), - } - } else if !direct_wireless { - log::info!("direct model HTTPS probes skipped for UART backend"); + } else { + log::error!("could not create AgentOS runtime task (result={result})"); } - - let uart_poc = matches!( - runtime_config.model.backend, - ModelBackendSettings::Uart { .. } - ); - let backend: Arc = match runtime_config.model.backend { - ModelBackendSettings::Uart { .. } => { - let transport: Arc = uart; - Arc::new(backend::UartBackend::new(transport)) - } - ModelBackendSettings::Wireless { provider } => Arc::new( - backend::WirelessBackend::new( - provider, - runtime_config - .model_api_key - .ok_or_else(|| anyhow::anyhow!("wireless backend is missing API key"))?, - ) - .map_err(anyhow::Error::msg)?, - ), - }; - let tools = Arc::new(CoreToolHost::new( - storage::WORKSPACE_ROOT, - Arc::new(EspPlatform), - )); - let provider = match runtime_config.model.backend { - ModelBackendSettings::Uart { .. } => "uart", - ModelBackendSettings::Wireless { provider } => provider.id(), - }; - let config = serde_json::json!({ - "provider":provider, - "model":runtime_config.model.resolved_model().unwrap_or_else(|_| "unknown".into()), - "systemPrompt":"You are Pocket Pi on an ESP32-P4. Be concise." - }); - let (prompt_tx, agent_rx) = - spawn_agent_worker(config.to_string(), backend, tools.clone(), Some(64 * 1024)) - .map_err(anyhow::Error::msg)?; - let mut initial_prompt = runtime_config.initial_prompt; - - let mut touch_was_down = false; - let mut redraw = true; - let mut pending_settings = None; - let mut last_telemetry = Instant::now(); - let mut last_heartbeat = Instant::now(); loop { - while let Ok(event) = agent_rx.try_recv() { - match event { - AgentEvent::Ready => { - log::info!("PocketJS Pi Harness ready"); - if uart_poc { - unsafe { - esp_idf_svc::sys::esp_log_level_set( - c"*".as_ptr(), - esp_idf_svc::sys::esp_log_level_t_ESP_LOG_NONE, - ); - } - } - chat.set_latest_assistant("ESP32-P4 PI AGENT READY."); - if let Some(prompt) = initial_prompt.take() { - chat.push_pending(prompt.clone()); - screen.show_latest_chat(); - device.agent = AgentState::Thinking; - if prompt_tx.send(prompt).is_err() { - chat.fail_pending("AGENT WORKER IS NOT AVAILABLE"); - device.agent = AgentState::Faulted; - } - } else { - device.agent = AgentState::Idle; - } - } - AgentEvent::Delta(delta) => { - if chat.append_model_delta(&delta) { - screen.show_latest_chat(); - } - } - AgentEvent::Done => { - chat.finish_pending(); - screen.refresh_workspace(); - device.agent = AgentState::Idle; - } - AgentEvent::Failed(error) => { - log::error!("PocketJS Pi Harness failed: {error}"); - chat.fail_pending(format!("AGENT FAILED: {error}")); - device.agent = AgentState::Faulted; - } - } - redraw = true; - } - - if let Some(display) = display.as_mut() { - if let Some((x, y)) = display.read_touch() { - if !touch_was_down { - match screen.handle_tap(x, y, &chat, &ui) { - ScreenInteraction::None => {} - ScreenInteraction::Redraw => redraw = true, - ScreenInteraction::SubmitPrompt(prompt) => { - chat.push_pending(prompt.clone()); - screen.show_latest_chat(); - device.agent = AgentState::Thinking; - if prompt_tx.send(prompt).is_err() { - chat.fail_pending("AGENT WORKER IS NOT AVAILABLE"); - device.agent = AgentState::Faulted; - } - redraw = true; - } - ScreenInteraction::Settings(command) => { - if command == SettingsCommand::ScanWifi { - settings.wifi.scanning = true; - settings.wifi.status = "SCANNING...".into(); - screen.set_settings(settings.clone()); - } - pending_settings = Some(command); - redraw = true; - } - } - } - touch_was_down = true; - } else { - if touch_was_down && screen.handle_touch_release() { - redraw = true; - } - touch_was_down = false; - } - } + delay_current_task(Duration::from_secs(10)); + } +} - if last_telemetry.elapsed() >= Duration::from_secs(2) { - let ui_fps_tenths = display - .as_mut() - .map(DisplayProbe::sample_ui_fps_tenths) - .unwrap_or(0); - screen.set_telemetry(system_telemetry(ui_fps_tenths)); - settings.workspace_free_bytes = storage::workspace_free_bytes().ok(); - screen.set_settings(settings.clone()); - let schedule = tools.schedule_projection(); - screen.set_schedule(pocket_pi_device_ui::ScheduleProjection { - name: schedule.name, - prompt: schedule.prompt, - next_in_seconds: schedule.next_in_seconds, - every_minutes: schedule.every_minutes, - }); - if device.agent == AgentState::Idle { - if let Some(wake) = tools.claim_due() { - chat.push_pending(wake.prompt.clone()); - screen.show_latest_chat(); - device.agent = AgentState::Thinking; - if prompt_tx.send(wake.prompt).is_err() { - chat.fail_pending("AGENT WORKER IS NOT AVAILABLE"); - device.agent = AgentState::Faulted; - } - } - } - redraw = true; - last_telemetry = Instant::now(); - } - if redraw { - if let Some(display) = display.as_mut() { - display.render(&mut renderer, &ui, &device, &chat, &screen)?; - } - redraw = false; - } - if let Some(command) = pending_settings.take() { - match command { - SettingsCommand::ScanWifi => match wifi.as_mut() { - Some(wifi) => match wifi.scan() { - Ok(networks) => { - settings = wifi.projection(""); - settings.wifi.networks = networks; - } - Err(error) => { - settings.wifi.scanning = false; - settings.wifi.status = format!("SCAN FAILED: {error}"); - } - }, - None => settings.wifi.status = "WI-FI DRIVER UNAVAILABLE".into(), - }, - SettingsCommand::ConnectWifi { ssid, password } => match wifi.as_mut() { - Some(wifi) => match wifi.connect(&ssid, &password) { - Ok(()) => settings = wifi.projection("CONNECTED"), - Err(error) => settings.wifi.status = format!("CONNECT FAILED: {error}"), - }, - None => settings.wifi.status = "WI-FI DRIVER UNAVAILABLE".into(), - }, - SettingsCommand::ForgetWifi => match wifi.as_mut() { - Some(wifi) => match wifi.forget() { - Ok(()) => settings = wifi.projection("NETWORK FORGOTTEN"), - Err(error) => settings.wifi.status = format!("FORGET FAILED: {error}"), - }, - None => settings.wifi.status = "WI-FI DRIVER UNAVAILABLE".into(), - }, - SettingsCommand::Restart => { - let _ = EspPlatform.reboot(); - settings.wifi.status = "RESTARTING...".into(); - } - } - settings.wifi.scanning = false; - screen.set_settings(settings.clone()); - redraw = true; - } - if last_heartbeat.elapsed() >= Duration::from_secs(5) { - let memory = memory_snapshot(); - log::info!( - "heartbeat heap={} psram_free={} agent={:?}", - memory.free_heap, - memory.psram_free, - device.agent, - ); - last_heartbeat = Instant::now(); - } - std::thread::sleep(Duration::from_millis(16)); +unsafe extern "C" fn agentos_task(_argument: *mut core::ffi::c_void) { + if let Err(error) = agentos_main::run() { + log::error!("AgentOS runtime stopped: {error:#}"); + } + loop { + delay_current_task(Duration::from_secs(10)); } } @@ -390,12 +106,7 @@ impl PlatformTools for EspPlatform { "board":"esp32-p4", "piHarness":"pi-agent-core", "jsRuntime":"QuickJS via PocketJS host", - "freeHeapBytes":unsafe { esp_idf_svc::sys::esp_get_free_heap_size() }, - "freePsramBytes":unsafe { - esp_idf_svc::sys::heap_caps_get_free_size( - esp_idf_svc::sys::MALLOC_CAP_SPIRAM, - ) - } + "memoryTelemetry":"boot projection only" }) } @@ -424,7 +135,7 @@ impl PlatformTools for EspPlatform { .name("delayed-reboot".to_owned()) .stack_size(4 * 1024) .spawn(|| { - std::thread::sleep(Duration::from_millis(750)); + delay_current_task(Duration::from_millis(750)); unsafe { esp_idf_svc::sys::esp_restart() } }) .map_err(|error| format!("schedule reboot: {error}"))?; @@ -432,79 +143,6 @@ impl PlatformTools for EspPlatform { } } -#[derive(Clone, Copy, Debug, Default, Eq, PartialEq)] -struct HttpsReachability { - openai_api: bool, - codex_backend: bool, -} - -fn probe_https_origins() -> anyhow::Result { - let probes = [ - ("control", "https://api.github.com/zen", true), - ("openai-api", "https://api.openai.com/v1/models", false), - ( - "codex-backend", - "https://chatgpt.com/backend-api/models", - false, - ), - ]; - - let mut control_ok = false; - let mut target_ok = 0u8; - let mut reachability = HttpsReachability::default(); - for (name, url, control) in probes { - let configuration = HttpConfiguration { - timeout: Some(Duration::from_secs(10)), - crt_bundle_attach: Some(esp_idf_svc::sys::esp_crt_bundle_attach), - ..Default::default() - }; - let mut client = HttpClient::wrap(EspHttpConnection::new(&configuration)?); - let request = match client.request( - Method::Get, - url, - &[ - ("accept", "application/json"), - ("user-agent", "pocket-pi-p4/0.1"), - ], - ) { - Ok(request) => request, - Err(error) => { - log::warn!("HTTPS probe setup failed: origin={name} error={error}"); - continue; - } - }; - match request.submit() { - Ok(response) => { - let status = response.status(); - if !(100..600).contains(&status) { - log::warn!("HTTPS probe invalid status: origin={name} status={status}"); - continue; - } - log::info!("HTTPS probe complete: origin={name} status={status}"); - if control { - control_ok = true; - } else { - target_ok = target_ok.saturating_add(1); - match name { - "openai-api" => reachability.openai_api = true, - "codex-backend" => reachability.codex_backend = true, - _ => {} - } - } - } - Err(error) => { - log::warn!("HTTPS probe unavailable: origin={name} error={error}"); - } - } - } - - if !control_ok { - anyhow::bail!("control HTTPS origin was unreachable") - } - log::info!("target HTTPS reachability: {target_ok}/2"); - Ok(reachability) -} - fn init_wifi( provisioned_ssid: Option<&str>, provisioned_password: Option<&str>, @@ -521,7 +159,7 @@ fn init_wifi( if Instant::now() >= deadline { anyhow::bail!("C6 Wi-Fi driver did not stop before lwIP attach") } - std::thread::sleep(Duration::from_millis(25)); + delay_current_task(Duration::from_millis(25)); } let sta_netif = EspNetif::new(NetifStack::Sta)?; esp_result("esp_netif_attach_wifi_station", unsafe { @@ -534,11 +172,12 @@ fn init_wifi( driver, sta_netif, nvs, + pending: None, }; match load_wifi_credentials(wifi.nvs.clone(), provisioned_ssid, provisioned_password) { Ok((ssid, password)) => { - if let Err(error) = wifi.connect(&ssid, &password) { - log::warn!("saved Wi-Fi did not connect: {error:#}"); + if let Err(error) = wifi.begin_connect(&ssid, &password, false) { + log::warn!("saved Wi-Fi connection could not start: {error:#}"); } } Err(error) => log::warn!("Wi-Fi is not configured: {error:#}"), @@ -550,14 +189,18 @@ struct WifiConnection { driver: WifiDriver<'static>, sta_netif: EspNetif, nvs: EspDefaultNvsPartition, + pending: Option, } -impl WifiConnection { - fn is_connected(&self) -> bool { - self.driver.is_connected().unwrap_or(false) && self.sta_netif.is_up().unwrap_or(false) - } +struct PendingWifiConnect { + ssid: String, + password: String, + started_at: Instant, + persist_on_success: bool, +} - fn scan(&mut self) -> anyhow::Result> { +impl WifiConnection { + fn scan(&mut self) -> anyhow::Result> { if !self.driver.is_started()? { self.driver.start()?; } @@ -565,7 +208,7 @@ impl WifiConnection { let mut networks = access_points .into_iter() .filter(|access_point| !access_point.ssid.is_empty()) - .map(|access_point| pocket_pi_device_ui::WifiNetworkProjection { + .map(|access_point| WifiNetworkProjection { ssid: access_point.ssid.as_str().to_owned(), rssi_dbm: access_point.signal_strength as i16, secured: access_point.auth_method != Some(AuthMethod::None), @@ -577,13 +220,22 @@ impl WifiConnection { Ok(networks) } - fn connect(&mut self, ssid: &str, password: &str) -> anyhow::Result<()> { + fn begin_connect( + &mut self, + ssid: &str, + password: &str, + persist_on_success: bool, + ) -> anyhow::Result<()> { validate_wifi_ssid(ssid)?; if !password.is_empty() { validate_wifi_password(password)?; } - if self.driver.is_connected()? { - self.driver.disconnect()?; + if self.driver.is_started()? { + // A timed-out ESP-Hosted association may still be in the remote + // driver's connecting state even though is_connected() is false. + // Clear that state before every explicit attempt or retry. + let _ = self.driver.disconnect(); + delay_current_task(Duration::from_millis(50)); } self.driver .set_configuration(&Configuration::Client(ClientConfiguration { @@ -601,21 +253,66 @@ impl WifiConnection { if !self.driver.is_started()? { self.driver.start()?; } + esp_result("disable Wi-Fi modem power save", unsafe { + esp_idf_svc::sys::esp_wifi_set_ps(esp_idf_svc::sys::wifi_ps_type_t_WIFI_PS_NONE) + })?; self.driver.connect()?; - let deadline = Instant::now() + Duration::from_secs(15); - while !self.driver.is_connected()? || !self.sta_netif.is_up()? { - if Instant::now() >= deadline { - anyhow::bail!("Wi-Fi association or DHCP timed out") + self.pending = Some(PendingWifiConnect { + ssid: ssid.to_owned(), + password: password.to_owned(), + started_at: Instant::now(), + persist_on_success, + }); + Ok(()) + } + + fn poll_connect(&mut self) -> Option> { + let pending = self.pending.as_ref()?; + match (self.driver.is_connected(), self.sta_netif.is_up()) { + (Ok(true), Ok(true)) => { + let pending = self.pending.take().expect("pending Wi-Fi connect"); + if pending.persist_on_success { + let result = (|| { + let storage = EspDefaultNvs::new( + self.nvs.clone(), + WIFI_NVS_NAMESPACE, + true, + )?; + storage.set_str(WIFI_NVS_SSID_KEY, &pending.ssid)?; + storage.set_str(WIFI_NVS_PASSWORD_KEY, &pending.password)?; + Ok::<(), anyhow::Error>(()) + })(); + return Some(result); + } + Some(Ok(())) + } + (Err(error), _) => { + self.pending = None; + Some(Err(error.into())) + } + (_, Err(error)) => { + self.pending = None; + Some(Err(error.into())) } - std::thread::sleep(Duration::from_millis(50)); + _ if pending.started_at.elapsed() >= Duration::from_secs(15) => { + let _ = self.driver.disconnect(); + self.pending = None; + Some(Err(anyhow::anyhow!("Wi-Fi association or DHCP timed out"))) + } + _ => None, } - let storage = EspDefaultNvs::new(self.nvs.clone(), WIFI_NVS_NAMESPACE, true)?; - storage.set_str(WIFI_NVS_SSID_KEY, ssid)?; - storage.set_str(WIFI_NVS_PASSWORD_KEY, password)?; - Ok(()) + } + + fn is_connecting(&self) -> bool { + self.pending.is_some() + } + + fn is_connected(&self) -> bool { + self.driver.is_connected().unwrap_or(false) && self.sta_netif.is_up().unwrap_or(false) } fn forget(&mut self) -> anyhow::Result<()> { + self.pending = None; if self.driver.is_connected()? { self.driver.disconnect()?; } @@ -625,8 +322,8 @@ impl WifiConnection { Ok(()) } - fn projection(&self, status: impl Into) -> pocket_pi_device_ui::SettingsProjection { - let mut projection = pocket_pi_device_ui::SettingsProjection { + fn projection(&self, status: impl Into) -> SettingsProjection { + let mut projection = SettingsProjection { firmware_version: env!("CARGO_PKG_VERSION").into(), workspace_free_bytes: storage::workspace_free_bytes().ok(), ..Default::default() @@ -702,200 +399,21 @@ fn validate_wifi_password(password: &str) -> anyhow::Result<()> { Ok(()) } -#[derive(Debug)] struct DisplayProbe { panel: esp_idf_svc::sys::esp_lcd_panel_handle_t, _io: esp_idf_svc::sys::esp_lcd_panel_io_handle_t, touch: esp_idf_svc::sys::esp_lcd_touch_handle_t, framebuffers: [*mut u16; 3], + render_states: [RenderTargetState; 3], next_framebuffer: usize, - presented_frames: u32, - fps_window_started: Instant, -} - -fn init_display_probe( - renderer: &mut Renderer, - ui: &Ui, - device: &DeviceState, - chat: &ChatProjection, - screen: &ScreenState, -) -> anyhow::Result { - unsafe { - let mut panel = core::ptr::null_mut(); - let mut io = core::ptr::null_mut(); - let mut touch = core::ptr::null_mut(); - let mut framebuffer_0 = core::ptr::null_mut(); - let mut framebuffer_1 = core::ptr::null_mut(); - let mut framebuffer_2 = core::ptr::null_mut(); - - esp_result( - "bsp_display_new", - esp_idf_svc::sys::bsp_display_new(core::ptr::null(), &mut panel, &mut io), - )?; - esp_result( - "esp_lcd_dpi_panel_get_frame_buffer", - esp_idf_svc::sys::esp_lcd_dpi_panel_get_frame_buffer( - panel, - 3, - &mut framebuffer_0, - &mut framebuffer_1, - &mut framebuffer_2, - ), - )?; - let framebuffers = [framebuffer_0, framebuffer_1, framebuffer_2]; - if framebuffers.iter().any(|framebuffer| framebuffer.is_null()) { - anyhow::bail!("esp_lcd_dpi_panel_get_frame_buffer returned a null buffer") - } - - let pixels = core::slice::from_raw_parts_mut( - framebuffers[0].cast::(), - PANEL_WIDTH as usize * PANEL_HEIGHT as usize, - ); - let words = screen.draw_list(ui, device, chat); - let mut software = SoftwareOnly; - let stats = renderer - .render(ui, &words, pixels, PANEL_WIDTH, PANEL_HEIGHT, &mut software) - .ok_or_else(|| anyhow::anyhow!("PocketJS rejected the panel framebuffer geometry"))?; - - esp_result( - "esp_lcd_dpi_panel_set_pattern", - esp_idf_svc::sys::esp_lcd_dpi_panel_set_pattern( - panel, - esp_idf_svc::sys::mipi_dsi_pattern_type_t_MIPI_DSI_PATTERN_NONE, - ), - )?; - esp_result( - "esp_lcd_panel_disp_on_off", - esp_idf_svc::sys::esp_lcd_panel_disp_on_off(panel, true), - )?; - esp_result( - "esp_lcd_panel_draw_bitmap", - esp_idf_svc::sys::esp_lcd_panel_draw_bitmap( - panel, - 0, - 0, - PANEL_WIDTH as i32, - PANEL_HEIGHT as i32, - framebuffers[0], - ), - )?; - esp_result( - "bsp_display_backlight_on", - esp_idf_svc::sys::bsp_display_backlight_on(), - )?; - esp_result( - "pi_p4_touch_new", - esp_idf_svc::sys::pi_p4_touch_new(&mut touch), - )?; - - log::info!( - "PocketJS triple-buffer probe: fb0={:p} fb1={:p} fb2={:p} stats={stats:?}", - framebuffers[0], - framebuffers[1], - framebuffers[2] - ); - Ok(DisplayProbe { - panel, - _io: io, - touch, - framebuffers: framebuffers.map(|framebuffer| framebuffer.cast()), - next_framebuffer: 1, - presented_frames: 1, - fps_window_started: Instant::now(), - }) - } } impl DisplayProbe { - fn render( - &mut self, - renderer: &mut Renderer, - ui: &Ui, - device: &DeviceState, - chat: &ChatProjection, - screen: &ScreenState, - ) -> anyhow::Result<()> { - let framebuffer = self.framebuffers[self.next_framebuffer]; - let pixels = unsafe { - core::slice::from_raw_parts_mut( - framebuffer, - PANEL_WIDTH as usize * PANEL_HEIGHT as usize, - ) - }; - let words = screen.draw_list(ui, device, chat); - let mut software = SoftwareOnly; - renderer - .render(ui, &words, pixels, PANEL_WIDTH, PANEL_HEIGHT, &mut software) - .ok_or_else(|| anyhow::anyhow!("PocketJS rejected the status framebuffer geometry"))?; - - esp_result("esp_lcd_panel_draw_bitmap", unsafe { - esp_idf_svc::sys::esp_lcd_panel_draw_bitmap( - self.panel, - 0, - 0, - PANEL_WIDTH as i32, - PANEL_HEIGHT as i32, - framebuffer.cast(), - ) - })?; - self.next_framebuffer = (self.next_framebuffer + 1) % self.framebuffers.len(); - self.presented_frames = self.presented_frames.saturating_add(1); - Ok(()) - } - fn read_touch(&mut self) -> Option<(u16, u16)> { let mut x = 0u16; let mut y = 0u16; unsafe { esp_idf_svc::sys::pi_p4_touch_read(self.touch, &mut x, &mut y).then_some((x, y)) } } - - fn sample_ui_fps_tenths(&mut self) -> u16 { - let elapsed = self.fps_window_started.elapsed().as_secs_f32(); - let fps_tenths = if elapsed > 0.0 { - ((self.presented_frames as f32 / elapsed) * 10.0).round() as u16 - } else { - 0 - }; - self.presented_frames = 0; - self.fps_window_started = Instant::now(); - fps_tenths - } -} - -struct SoftwareOnly; - -impl PpaOps for SoftwareOnly { - fn fill_rgb565(&mut self, _: &mut [u16], _: u32, _: u32, _: Rect, _: u16) -> bool { - false - } - - fn blend_a8_rgb565( - &mut self, - _: &mut [u16], - _: u32, - _: u32, - _: &[u8], - _: Rect, - _: [u8; 3], - _: u8, - ) -> bool { - false - } - - fn srm_psm5650_to_rgb565( - &mut self, - _: &mut [u16], - _: u32, - _: u32, - _: &[u8], - _: u32, - _: u32, - _: Rect, - _: Rect, - _: SrmTransform, - ) -> bool { - false - } } fn esp_result(operation: &str, code: esp_idf_svc::sys::esp_err_t) -> anyhow::Result<()> { @@ -905,44 +423,3 @@ fn esp_result(operation: &str, code: esp_idf_svc::sys::esp_err_t) -> anyhow::Res anyhow::bail!("{operation} returned ESP-IDF error 0x{code:x}") } } - -#[derive(Debug)] -struct MemorySnapshot { - free_heap: u32, - psram_total: usize, - psram_free: usize, -} - -fn memory_snapshot() -> MemorySnapshot { - unsafe { - MemorySnapshot { - free_heap: esp_idf_svc::sys::esp_get_free_heap_size(), - psram_total: esp_idf_svc::sys::heap_caps_get_total_size( - esp_idf_svc::sys::MALLOC_CAP_SPIRAM, - ), - psram_free: esp_idf_svc::sys::heap_caps_get_free_size( - esp_idf_svc::sys::MALLOC_CAP_SPIRAM, - ), - } - } -} - -fn system_telemetry(ui_fps_tenths: u16) -> SystemTelemetry { - let memory = memory_snapshot(); - let used = memory.psram_total.saturating_sub(memory.psram_free); - let used_percent = if memory.psram_total == 0 { - 0 - } else { - ((used.saturating_mul(100)) / memory.psram_total).min(100) as u8 - }; - let mut cpu = 0u8; - let cpu_percent = - unsafe { esp_idf_svc::sys::pi_p4_cpu_load_percent(&mut cpu).then_some(cpu.min(100)) }; - SystemTelemetry { - psram_used_percent: used_percent, - psram_free_bytes: memory.psram_free, - cpu_percent, - ui_fps_tenths, - lcd_refresh_hz: LCD_REFRESH_HZ, - } -} diff --git a/firmware/esp32-p4/src/transport/provisioning.rs b/firmware/esp32-p4/src/transport/provisioning.rs index ff769a2..dd706ce 100644 --- a/firmware/esp32-p4/src/transport/provisioning.rs +++ b/firmware/esp32-p4/src/transport/provisioning.rs @@ -1,7 +1,8 @@ +use std::collections::BTreeMap; use std::time::Duration; use pocket_pi_protocols::model::{ - ModelBackendSettings, ModelSettings, UartProvider, WirelessProvider, + ModelBackendSettings, ModelSettings, ThinkingLevel, UartProvider, WirelessProvider, }; use super::LineTransport; @@ -15,7 +16,9 @@ pub struct RuntimeConfig { pub wifi_password: Option, pub model: ModelSettings, pub model_api_key: Option, + pub app_credentials: BTreeMap, pub initial_prompt: Option, + pub initial_prompt_delay_seconds: u64, pub unix_time_seconds: Option, } @@ -41,7 +44,7 @@ pub fn request_runtime_config( ("uart", "claude-code") => ModelBackendSettings::Uart { provider: UartProvider::ClaudeCode, }, - ("wireless", "openai" | "openrouter" | "anthropic") => { + ("wireless", "openai" | "openrouter" | "anthropic" | "deepseek") => { if model_api_key.is_none() { return Err(format!("{provider} requires modelApiKey")); } @@ -50,19 +53,27 @@ pub fn request_runtime_config( "openai" => WirelessProvider::OpenAi, "openrouter" => WirelessProvider::OpenRouter, "anthropic" => WirelessProvider::Anthropic, + "deepseek" => WirelessProvider::DeepSeek, _ => unreachable!(), }, } } ("uart", _) => return Err("UART provider must be codex or claude-code".into()), ("wireless", _) => { - return Err("wireless provider must be openai, openrouter or anthropic".into()) + return Err( + "wireless provider must be openai, openrouter, anthropic or deepseek".into(), + ) } _ => return Err("model backend must be uart or wireless".into()), }; let model = ModelSettings { backend: model_backend, model: text(&value, "model", 128)?, + thinking_level: match text(&value, "thinkingLevel", 8)?.as_deref() { + None | Some("high") => ThinkingLevel::High, + Some("xhigh") => ThinkingLevel::Xhigh, + Some(_) => return Err("thinkingLevel must be high or xhigh".into()), + }, }; model.resolved_model()?; Ok(RuntimeConfig { @@ -70,28 +81,69 @@ pub fn request_runtime_config( wifi_password: secret(&value, "wifiPassword", 63)?, model, model_api_key, - initial_prompt: text(&value, "initialPrompt", 4_000)?, + app_credentials: app_credentials(&value)?, + initial_prompt: string(&value, "initialPrompt")?.map(str::to_owned), + initial_prompt_delay_seconds: value + .get("initialPromptDelaySeconds") + .and_then(serde_json::Value::as_u64) + .unwrap_or(0) + .min(120), unix_time_seconds: value .get("unixTimeSeconds") .and_then(serde_json::Value::as_u64), }) } +fn app_credentials(value: &serde_json::Value) -> Result, String> { + let Some(credentials) = value.get("appCredentials") else { + return Ok(BTreeMap::new()); + }; + let credentials = credentials + .as_object() + .ok_or_else(|| "appCredentials must be an object".to_owned())?; + if credentials.len() > 16 { + return Err("appCredentials has too many entries".into()); + } + credentials + .iter() + .map(|(id, value)| { + if id.is_empty() || id.len() > 128 || !id.is_ascii() { + return Err("appCredentials contains an invalid id".into()); + } + let secret = value + .as_str() + .filter(|secret| !secret.is_empty() && secret.len() <= 4096 && secret.is_ascii()) + .ok_or_else(|| format!("appCredentials.{id} is invalid"))?; + Ok((id.clone(), secret.to_owned())) + }) + .collect() +} + fn text( value: &serde_json::Value, field: &str, max_bytes: usize, ) -> Result, String> { + let Some(value) = string(value, field)? else { + return Ok(None); + }; + if value.len() > max_bytes { + return Err(format!("{field} has invalid length")); + } + Ok(Some(value.to_owned())) +} + +fn string<'a>(value: &'a serde_json::Value, field: &str) -> Result, String> { let Some(value) = value.get(field) else { return Ok(None); }; let value = value .as_str() .ok_or_else(|| format!("{field} must be a string"))?; - if value.is_empty() || value.len() > max_bytes { + if value.is_empty() { return Err(format!("{field} has invalid length")); } - Ok(Some(value.to_owned())) + Ok(Some(value)) } fn secret( diff --git a/firmware/esp32-p4/src/transport/uart.rs b/firmware/esp32-p4/src/transport/uart.rs index 1b380fa..f98d6d6 100644 --- a/firmware/esp32-p4/src/transport/uart.rs +++ b/firmware/esp32-p4/src/transport/uart.rs @@ -70,8 +70,6 @@ impl LineTransport for UartLineTransport { } else { frame.clear(); } - } else { - std::thread::sleep(Duration::from_millis(1)); } } Err(format!("timed out waiting for {prefix}")) diff --git a/firmware/esp32-p4/src/waveshare_bindings.h b/firmware/esp32-p4/src/waveshare_bindings.h index 7f7f89b..f47fe59 100644 --- a/firmware/esp32-p4/src/waveshare_bindings.h +++ b/firmware/esp32-p4/src/waveshare_bindings.h @@ -11,4 +11,3 @@ esp_err_t pi_p4_touch_new(esp_lcd_touch_handle_t *ret_touch); bool pi_p4_touch_read(esp_lcd_touch_handle_t touch, uint16_t *x, uint16_t *y); -bool pi_p4_cpu_load_percent(uint8_t *percent); diff --git a/firmware/esp32-p4/tools/esp32p4-cc b/firmware/esp32-p4/tools/esp32p4-cc index 53fbf59..23dc1bc 100755 --- a/firmware/esp32-p4/tools/esp32p4-cc +++ b/firmware/esp32-p4/tools/esp32p4-cc @@ -7,7 +7,7 @@ for compiler in \ "$firmware_root"/.embuild/espressif/tools/riscv32-esp-elf/*/riscv32-esp-elf/bin/riscv32-esp-elf-gcc do if [ -x "$compiler" ]; then - exec "$compiler" "$@" + exec "$compiler" -isystem "$firmware_root/shim" "$@" fi done diff --git a/hosts/esp32-p4-sim/Cargo.toml b/hosts/esp32-p4-sim/Cargo.toml index 225e943..8cba247 100644 --- a/hosts/esp32-p4-sim/Cargo.toml +++ b/hosts/esp32-p4-sim/Cargo.toml @@ -10,15 +10,18 @@ publish = false anyhow.workspace = true env_logger = "0.11" log.workspace = true -pocket-pi-device-ui = { path = "../../crates/pocket-pi-device-ui" } +pocket-pi-agentos = { path = "../../crates/pocket-pi-agentos" } +pocket-pi-app-pack = { path = "../../crates/pocket-pi-app-pack" } pocket-pi-embedded = { path = "../../crates/pocket-pi-embedded" } pocket-pi-protocols = { path = "../../crates/pocket-pi-protocols" } pocket-pi-tools = { path = "../../crates/pocket-pi-tools" } -pocketjs-core = { git = "https://github.com/pocket-stack/pocketjs.git", rev = "4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" } -pocket-ui-wgpu = { git = "https://github.com/pocket-stack/pocketjs.git", rev = "4c5dc9ef1dd26e6f49b036c210931d399f2b52b2" } -pocket3d = { git = "https://github.com/pocket-stack/pocketjs.git", rev = "4c5dc9ef1dd26e6f49b036c210931d399f2b52b2", default-features = false } +pocket-ui-wgpu = { git = "https://github.com/pocket-stack/pocketjs.git", rev = "9c809bbd047ddc75c27caa4990951a78d942477a" } +pocket3d = { git = "https://github.com/pocket-stack/pocketjs.git", rev = "9c809bbd047ddc75c27caa4990951a78d942477a", default-features = false } serde_json.workspace = true tempfile = "3" ureq = { version = "2.12", features = ["json"] } wgpu = "25" winit = "0.30" + +[dev-dependencies] +pocket-db.workspace = true diff --git a/hosts/esp32-p4-sim/src/backend.rs b/hosts/esp32-p4-sim/src/backend.rs index d95f4a3..76ab3cb 100644 --- a/hosts/esp32-p4-sim/src/backend.rs +++ b/hosts/esp32-p4-sim/src/backend.rs @@ -4,7 +4,7 @@ use std::sync::Arc; use std::time::Duration; use pocket_pi_embedded::ModelBackend; -use pocket_pi_protocols::model::WirelessProvider; +use pocket_pi_protocols::model::{ModelStreamEvent, WirelessProvider}; use pocket_pi_protocols::{anthropic_messages, codex_decision, openai_chat}; use serde_json::{json, Value}; @@ -13,6 +13,7 @@ pub enum BackendChoice { provider: WirelessProvider, api_key: String, model: String, + thinking_level: String, }, Codex { model: Option, @@ -22,11 +23,12 @@ pub enum BackendChoice { impl BackendChoice { pub fn from_name(name: &str, model: Option) -> Result { match name { - "openai" | "openrouter" | "anthropic" => { + "openai" | "openrouter" | "anthropic" | "deepseek" => { let provider = match name { "openai" => WirelessProvider::OpenAi, "openrouter" => WirelessProvider::OpenRouter, "anthropic" => WirelessProvider::Anthropic, + "deepseek" => WirelessProvider::DeepSeek, _ => unreachable!(), }; let prefix = name.to_ascii_uppercase(); @@ -36,31 +38,41 @@ impl BackendChoice { .or_else(|| std::env::var(format!("{prefix}_MODEL")).ok()) .or_else(|| provider.default_model().map(str::to_owned)) .ok_or_else(|| format!("--backend {name} requires --model"))?; + let thinking_level = std::env::var(format!("{prefix}_THINKING_LEVEL")) + .unwrap_or_else(|_| "high".into()); + if !matches!(thinking_level.as_str(), "high" | "xhigh") { + return Err(format!("{prefix}_THINKING_LEVEL must be high or xhigh")); + } Ok(Self::Wireless { provider, api_key, model, + thinking_level, }) } "codex" => Ok(Self::Codex { model: model.or_else(|| std::env::var("CODEX_MODEL").ok()), }), other => Err(format!( - "unknown backend {other:?}; expected openai, openrouter, anthropic or codex" + "unknown backend {other:?}; expected openai, openrouter, anthropic, deepseek or codex" )), } } pub fn agent_config(&self) -> String { - let (provider, model) = match self { + let (provider, model, thinking_level) = match self { Self::Wireless { - provider, model, .. - } => (provider.id(), model.as_str()), - Self::Codex { model } => ("codex", model.as_deref().unwrap_or("coding-plan")), + provider, + model, + thinking_level, + .. + } => (provider.id(), model.as_str(), thinking_level.as_str()), + Self::Codex { model } => ("codex", model.as_deref().unwrap_or("coding-plan"), "high"), }; json!({ "provider": provider, "model": model, + "thinkingLevel": thinking_level, "systemPrompt": "You are Pocket Pi in the ESP32 simulator. Be concise." }) .to_string() @@ -85,7 +97,7 @@ impl ModelBackend for WirelessBackend { fn complete( &self, request_json: &str, - on_delta: &mut dyn FnMut(&str), + on_event: &mut dyn FnMut(ModelStreamEvent), ) -> Result { let (endpoint, body) = match self.provider { WirelessProvider::OpenAi => ( @@ -100,6 +112,10 @@ impl ModelBackend for WirelessBackend { "https://api.anthropic.com/v1/messages", anthropic_messages::build_request(request_json)?, ), + WirelessProvider::DeepSeek => ( + "https://api.deepseek.com/chat/completions", + openai_chat::build_request_for(request_json, openai_chat::Dialect::DeepSeek)?, + ), }; let agent = ureq::AgentBuilder::new() .timeout_connect(Duration::from_secs(20)) @@ -113,7 +129,9 @@ impl ModelBackend for WirelessBackend { WirelessProvider::Anthropic => request .set("x-api-key", &self.api_key) .set("anthropic-version", "2023-06-01"), - WirelessProvider::OpenAi | WirelessProvider::OpenRouter => { + WirelessProvider::OpenAi + | WirelessProvider::OpenRouter + | WirelessProvider::DeepSeek => { request.set("authorization", &format!("Bearer {}", self.api_key)) } }; @@ -132,6 +150,9 @@ impl ModelBackend for WirelessBackend { let mut stream = match self.provider { WirelessProvider::Anthropic => SimProviderStream::Anthropic(Default::default()), + WirelessProvider::DeepSeek => { + SimProviderStream::Chat(openai_chat::Stream::new(openai_chat::Dialect::DeepSeek)) + } WirelessProvider::OpenAi | WirelessProvider::OpenRouter => { SimProviderStream::Chat(Default::default()) } @@ -145,8 +166,8 @@ impl ModelBackend for WirelessBackend { if data == "[DONE]" { break; } - if let Some(delta) = stream.push(data)? { - on_delta(&delta); + for event in stream.push(data)? { + on_event(event); } } stream.finish() @@ -159,7 +180,7 @@ enum SimProviderStream { } impl SimProviderStream { - fn push(&mut self, data: &str) -> Result, String> { + fn push(&mut self, data: &str) -> Result, String> { match self { Self::Chat(stream) => stream.push(data), Self::Anthropic(stream) => stream.push(data), @@ -182,7 +203,7 @@ impl ModelBackend for CodexBackend { fn complete( &self, request_json: &str, - on_delta: &mut dyn FnMut(&str), + on_event: &mut dyn FnMut(ModelStreamEvent), ) -> Result { let workspace = tempfile::tempdir().map_err(|error| error.to_string())?; let mut command = Command::new("codex"); @@ -243,7 +264,7 @@ impl ModelBackend for CodexBackend { .ok() .and_then(|value| value.get("text").and_then(Value::as_str).map(str::to_owned)) { - on_delta(&text); + on_event(ModelStreamEvent::Text(text)); } Ok(result) } diff --git a/hosts/esp32-p4-sim/src/main.rs b/hosts/esp32-p4-sim/src/main.rs index 965e0dc..ad47380 100644 --- a/hosts/esp32-p4-sim/src/main.rs +++ b/hosts/esp32-p4-sim/src/main.rs @@ -1,19 +1,20 @@ +use std::collections::BTreeMap; use std::path::{Path, PathBuf}; -use std::sync::mpsc::{Receiver, Sender}; +use std::sync::mpsc::Receiver; use std::sync::Arc; use std::time::{Duration, Instant}; use anyhow::{anyhow, Result}; use pocket3d::gpu::{Gpu, OffscreenTarget}; -use pocket_pi_device_ui::{ - load_fonts, AgentState, ChatProjection, DeviceState, ModelBackendSettings, ModelSettings, - ScreenInteraction, ScreenState, ScreenView, SettingsCommand, SettingsProjection, - SystemTelemetry, UartProvider, WifiNetworkProjection, +use pocket_pi_agentos::{ + AppServiceHost, AppSupervisor, AppToolRequest, HttpRequest, NetFailure, RoutedToolHost, + TransportCompletion, ROOT_APP_ID, }; -use pocket_pi_embedded::{spawn_agent_worker, AgentEvent}; +use pocket_pi_app_pack::catalog; +use pocket_pi_embedded::{AgentEvent, ToolHost}; use pocket_pi_tools::{CoreToolHost, PlatformTools}; use pocket_ui_wgpu::UiRenderer; -use pocketjs_core::Ui; +use serde_json::{json, Value}; use winit::application::ApplicationHandler; use winit::event::{ElementState, MouseButton, WindowEvent}; use winit::event_loop::{ActiveEventLoop, EventLoop}; @@ -33,7 +34,8 @@ struct Args { screenshot: Option, prompt: Option, workspace: PathBuf, - view: ScreenView, + app: String, + root_tap: Option<(u16, u16)>, backend: BackendChoice, } @@ -42,9 +44,22 @@ fn main() -> Result<()> { let args = parse_args()?; prepare_workspace(&args.workspace)?; if let Some(path) = args.screenshot { - headless(path, args.workspace, args.prompt, args.view, args.backend) + headless( + path, + args.workspace, + args.prompt, + args.app, + args.root_tap, + args.backend, + ) } else { - windowed(args.workspace, args.prompt, args.view, args.backend) + windowed( + args.workspace, + args.prompt, + args.app, + args.root_tap, + args.backend, + ) } } @@ -53,7 +68,8 @@ fn parse_args() -> Result { let mut prompt = None; let mut workspace = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../../target/esp32-p4-sim/workspace"); - let mut view = ScreenView::Chat; + let mut app = ROOT_APP_ID.to_owned(); + let mut root_tap = None; let mut backend = std::env::var("POCKET_PI_BACKEND").unwrap_or_else(|_| "codex".into()); let mut model = None; let mut args = std::env::args().skip(1); @@ -62,16 +78,37 @@ fn parse_args() -> Result { "--screenshot" => screenshot = Some(PathBuf::from(next(&mut args, "--screenshot")?)), "--prompt" => prompt = Some(next(&mut args, "--prompt")?), "--workspace" => workspace = PathBuf::from(next(&mut args, "--workspace")?), - "--view" => { - view = match next(&mut args, "--view")?.as_str() { - "chat" => ScreenView::Chat, - "workspace" | "files" => ScreenView::Files, - "settings" => ScreenView::Settings, - value => return Err(anyhow!("unknown view: {value}")), + "--app" => { + app = match next(&mut args, &argument)?.as_str() { + "pi-agent" => ROOT_APP_ID.to_owned(), + "files" => { + root_tap = Some((270, 1220)); + ROOT_APP_ID.to_owned() + } + "apps" => { + root_tap = Some((450, 1220)); + ROOT_APP_ID.to_owned() + } + "settings" => { + root_tap = Some((630, 1220)); + ROOT_APP_ID.to_owned() + } + "keyboard" => { + root_tap = Some((350, 1110)); + ROOT_APP_ID.to_owned() + } + value => value.to_owned(), } } "--backend" => backend = next(&mut args, "--backend")?, "--model" => model = Some(next(&mut args, "--model")?), + "--tap" => { + let value = next(&mut args, "--tap")?; + let (x, y) = value + .split_once(',') + .ok_or_else(|| anyhow!("--tap expects x,y"))?; + root_tap = Some((x.parse()?, y.parse()?)); + } other => return Err(anyhow!("unknown argument: {other}")), } } @@ -79,7 +116,8 @@ fn parse_args() -> Result { screenshot, prompt, workspace, - view, + app, + root_tap, backend: BackendChoice::from_name(&backend, model).map_err(anyhow::Error::msg)?, }) } @@ -99,110 +137,251 @@ fn prepare_workspace(root: &Path) -> Result<()> { } let notes = root.join("notes.txt"); if !notes.exists() { - std::fs::write(notes, "Physical ESP32 and simulator share one device UI.\n")?; + std::fs::write(notes, "Pi Agent owns this top-level workspace.\n")?; } Ok(()) } -fn new_ui() -> Result { - let mut ui = Ui::new(); - ui.set_viewport(PANEL_WIDTH as f32, PANEL_HEIGHT as f32); - if !load_fonts(&mut ui) { - return Err(anyhow!("PocketJS rejected the shared Inter font atlases")); - } - Ok(ui) -} - struct SimPlatform; impl PlatformTools for SimPlatform { - fn device_status(&self) -> serde_json::Value { - serde_json::json!({ + fn device_status(&self) -> Value { + json!({ "status":"ok", "board":"esp32-p4-sim", - "piHarness":"pi-agent-core", - "jsRuntime":"QuickJS via PocketJS host", + "agentOs":true, + "jsRuntime":"QuickJS via PocketJS", "simulated":true }) } - fn wifi_status(&self) -> serde_json::Value { - serde_json::json!({ - "status":"connected", - "ssid":"macOS host network", - "simulated":true - }) + fn wifi_status(&self) -> Value { + json!({"status":"connected","ssid":"macOS host network","simulated":true}) } - fn reboot(&self) -> Result { - Ok(serde_json::json!({"status":"scheduled","simulated":true})) + fn reboot(&self) -> Result { + Ok(json!({"status":"scheduled","simulated":true})) } } +/// Deterministic native fixtures keep the simulator useful without credentials. +/// The App code, SQLite writes and View are exactly the same bundles as the board. +struct SimAppServices; + +impl AppServiceHost for SimAppServices { + fn call( + &self, + app_id: &str, + service: &str, + operation: &str, + args: &Value, + deadline: Instant, + ) -> Result { + if Instant::now() >= deadline { + return Err("App Data Action deadline expired".into()); + } + let tool_name = args.get("name").and_then(Value::as_str).unwrap_or(""); + let tool_args = args.get("arguments").unwrap_or(&Value::Null); + if app_id == "robinhood" + && std::env::var("POCKET_PI_SIM_ROBINHOOD_FAIL").as_deref() == Ok("1") + { + return Err("simulated Robinhood service outage".into()); + } + if (app_id, service, operation) == ("robinhood", "mcp.client", "callTools") { + let calls = args + .get("calls") + .and_then(Value::as_array) + .ok_or_else(|| "simulated callTools requires calls".to_owned())?; + let results = calls + .iter() + .map(|call| { + let name = call.get("name").and_then(Value::as_str).unwrap_or(""); + let single = json!({ + "name":name, + "arguments":call.get("arguments").unwrap_or(&Value::Null) + }); + match self.call(app_id, service, "callTool", &single, deadline) { + Ok(value) => json!({"name":name,"ok":true,"value":value}), + Err(error) => json!({"name":name,"ok":false,"error":error}), + } + }) + .collect::>(); + return Ok(json!({"results":results})); + } + match (app_id, service, operation, tool_name) { + ("robinhood", "mcp.client", "callTool", "get_accounts") => Ok(json!({ + "accounts":[ + {"account_number":"SIM-AGENT-001","status":"active","type":"cash","agentic_allowed":true}, + {"account_number":"SIM-IRA-002","status":"active","type":"traditional_ira"}, + {"account_number":"SIM-JOINT-003","status":"active","type":"joint"} + ] + })), + ("robinhood", "mcp.client", "callTool", "get_portfolio") => { + let account = tool_args + .get("account_number") + .and_then(Value::as_str) + .unwrap_or("SIM-AGENT-001"); + let (equity, cash, buying_power, day_pnl, week_pnl) = match account { + "SIM-IRA-002" => ("84220.18", "4120.40", "4120.40", "-182.14", "1250.42"), + "SIM-JOINT-003" => ("31008.75", "5280.05", "10560.10", "205.80", "935.22"), + _ => ("15320.42", "1280.20", "2560.40", "128.35", "412.80"), + }; + Ok( + json!({"account_number":account,"equity":equity,"cash":cash,"buying_power":buying_power,"day_pnl":day_pnl,"week_pnl":week_pnl}), + ) + } + ("robinhood", "mcp.client", "callTool", "get_equity_positions") => Ok(json!({ + "positions":[ + {"symbol":"NVDA","quantity":"8","average_buy_price":"712.48","market_value":"7344.00"}, + {"symbol":"AAPL","quantity":"12","average_buy_price":"218.32","market_value":"2784.00"}, + {"symbol":"MSFT","quantity":"6","average_buy_price":"405.15","market_value":"2586.00"}, + {"symbol":"META","quantity":"3","average_buy_price":"502.10","market_value":"1581.00"}, + {"symbol":"AMZN","quantity":"5","average_buy_price":"191.25","market_value":"1012.50"}, + {"symbol":"GOOGL","quantity":"4","average_buy_price":"174.82","market_value":"724.00"}, + {"symbol":"TSLA","quantity":"2","average_buy_price":"332.50","market_value":"690.00"}, + {"symbol":"AMD","quantity":"3","average_buy_price":"168.40","market_value":"522.00"}, + {"symbol":"PLTR","quantity":"4","average_buy_price":"112.20","market_value":"468.00"}, + {"symbol":"VTI","quantity":"2","average_buy_price":"289.75","market_value":"602.00"} + ] + })), + ("robinhood", "mcp.client", "callTool", "get_equity_orders") => Ok(json!({"orders":[ + {"symbol":"NVDA","side":"buy","state":"filled","type":"limit","executed_quantity":"2","average_price":"918.00","created_at":"2026-08-08T09:42:00Z"}, + {"symbol":"AAPL","side":"sell","state":"filled","type":"market","executed_quantity":"4","average_price":"232.00","created_at":"2026-08-07T15:14:00Z"}, + {"symbol":"MSFT","side":"buy","state":"filled","type":"market","executed_quantity":"1","average_price":"431.00","created_at":"2026-08-06T18:28:00Z"}, + {"symbol":"META","side":"sell","state":"filled","type":"limit","executed_quantity":"1","average_price":"527.00","created_at":"2026-08-05T11:20:00Z"}, + {"symbol":"AMZN","side":"buy","state":"filled","type":"limit","executed_quantity":"3","average_price":"202.50","created_at":"2026-08-04T10:05:00Z"}, + {"symbol":"GOOGL","side":"buy","state":"cancelled","type":"limit","quantity":"2","price":"179.00","created_at":"2026-08-03T14:01:00Z"}, + {"symbol":"TSLA","side":"sell","state":"filled","type":"market","executed_quantity":"2","average_price":"345.00","created_at":"2026-08-02T16:32:00Z"}, + {"symbol":"AMD","side":"buy","state":"filled","type":"limit","executed_quantity":"3","average_price":"174.00","created_at":"2026-08-01T12:18:00Z"}, + {"symbol":"PLTR","side":"buy","state":"filled","type":"market","executed_quantity":"4","average_price":"117.00","created_at":"2026-07-31T17:11:00Z"} + ]})), + ("robinhood", "mcp.client", "callTool", "get_realized_pnl") => { + Ok(json!({"realized_pnl":"682.15"})) + } + ("robinhood", "mcp.client", "callTool", name) => { + Ok(json!({"operation":name,"simulated":true,"args":tool_args})) + } + _ => Err(format!( + "unsupported simulated service call: {app_id}/{service}/{operation}" + )), + } + } + + fn http( + &self, + app_id: &str, + request: HttpRequest, + deadline: Instant, + ) -> std::result::Result { + if Instant::now() >= deadline { + return Err(NetFailure::new( + "timeout", + "App Data Action deadline expired", + )); + } + if app_id != "exa" || request.method != "POST" { + return Err(NetFailure::new( + "invalid_request", + "simulator denied HTTP request", + )); + } + let body: Value = serde_json::from_slice(&request.body) + .map_err(|error| NetFailure::new("invalid_request", error.to_string()))?; + let value = match request.url.as_str() { + "https://api.exa.ai/search" => { + let query = body + .get("query") + .and_then(Value::as_str) + .unwrap_or("Pocket Pi"); + json!({"results":[ + {"title":format!("Research result for {query}"),"url":"https://example.com/research"}, + {"title":"PocketJS runtime notes","url":"https://pocketjs.dev/docs/concepts/"} + ]}) + } + "https://api.exa.ai/contents" => json!({ + "results":[{"title":"Simulated Exa document","url":"https://example.com/research","text":"Local simulator fixture"}] + }), + _ => { + return Err(NetFailure::new( + "invalid_request", + "simulator denied HTTP URL", + )) + } + }; + Ok(TransportCompletion::Done { + handle: request.handle, + status: 200, + url: request.url, + headers: BTreeMap::from([("content-type".into(), "application/json".into())]), + body: serde_json::to_vec(&value) + .map_err(|error| NetFailure::new("other", error.to_string()))?, + }) + } +} + +#[derive(Clone)] +struct Message { + role: &'static str, + text: String, +} + struct Product { - chat: ChatProjection, - screen: ScreenState, - device: DeviceState, - prompt_tx: Sender, - agent_rx: Receiver, - tools: Arc, - settings: SettingsProjection, + messages: Vec, + agent_status: &'static str, + model_label: String, + native_tools: Arc, + app_rx: Receiver, + supervisor: AppSupervisor, last_schedule_poll: Instant, busy: bool, - dirty: bool, + projection_dirty: bool, + pending_ui_task: Option, + wifi_connected: Option, + wifi_networks: Vec<(&'static str, i32, bool)>, + wifi_status: String, } impl Product { - fn new(workspace: PathBuf, backend: BackendChoice, view: ScreenView) -> Result { - let local_codex = matches!(backend, BackendChoice::Codex { .. }); - let model_settings = settings_for(&backend); - let tools = Arc::new(CoreToolHost::new(workspace.clone(), Arc::new(SimPlatform))); - let config = backend.agent_config(); - let backend = backend.build(); - let (prompt_tx, agent_rx) = - spawn_agent_worker(config, backend, tools.clone(), None).map_err(anyhow::Error::msg)?; - let workspace = workspace - .to_str() - .ok_or_else(|| anyhow!("workspace path is not valid UTF-8"))?; - let mut screen = ScreenState::new(workspace); - let settings = SettingsProjection { - wifi: pocket_pi_device_ui::WifiSettingsProjection { - connected_ssid: Some("macOS host network".into()), - ip_address: Some("192.0.2.2".into()), - rssi_dbm: Some(-42), - status: "SIMULATED ESP32 WI-FI".into(), - ..Default::default() - }, - firmware_version: env!("CARGO_PKG_VERSION").into(), - workspace_free_bytes: None, + fn new(workspace: PathBuf, backend: BackendChoice, app: &str) -> Result { + let model_label = match &backend { + BackendChoice::Wireless { + provider, model, .. + } => { + format!("{} / {model}", provider.id()) + } + BackendChoice::Codex { model } => { + format!("Codex / {}", model.as_deref().unwrap_or("coding-plan")) + } }; - screen.view = view; - screen.set_model_backend(&model_settings); - if local_codex { - screen.set_backend_status("CODEX", "LOCAL / MAC", "CODING PLAN"); - } - screen.set_telemetry(SystemTelemetry { - psram_used_percent: 25, - psram_free_bytes: 24 * 1024 * 1024, - cpu_percent: None, - ui_fps_tenths: 0, - lcd_refresh_hz: 32, - }); - screen.refresh_workspace(); - screen.set_settings(settings.clone()); + let services: Arc = Arc::new(SimAppServices); + let mut supervisor = AppSupervisor::new(workspace.clone(), catalog()?, services)?; + supervisor.open(app)?; + + let native_tools = Arc::new(CoreToolHost::new(workspace.clone(), Arc::new(SimPlatform))); + let catalog = supervisor.catalog().clone(); + let native: Arc = native_tools.clone(); + let (routed, app_rx) = RoutedToolHost::new(native, catalog); + let config = backend.agent_config(); + let model = backend.build(); + supervisor.boot_agent(&config, model, Arc::new(routed))?; + Ok(Self { - chat: ChatProjection::new("TYPE A MESSAGE", "ESP32-P4 PI AGENT SIMULATOR READY."), - screen, - device: DeviceState { - agent: AgentState::Idle, - }, - prompt_tx, - agent_rx, - tools, - settings, + messages: vec![Message { + role: "assistant", + text: "Pocket Pi AgentOS is ready.".into(), + }], + agent_status: "IDLE", + model_label, + native_tools, + app_rx, + supervisor, last_schedule_poll: Instant::now(), busy: false, - dirty: true, + projection_dirty: true, + pending_ui_task: None, + wifi_connected: Some("macOS host network".into()), + wifi_networks: Vec::new(), + wifi_status: "SIMULATED NETWORK READY".into(), }) } @@ -210,153 +389,189 @@ impl Product { if self.busy || prompt.trim().is_empty() { return; } - self.chat.push_pending(prompt.clone()); - self.screen.show_latest_chat(); - self.device.agent = AgentState::Thinking; + self.messages.push(Message { + role: "user", + text: prompt.clone(), + }); + self.messages.push(Message { + role: "assistant", + text: String::new(), + }); + self.agent_status = "THINKING"; self.busy = true; - if self.prompt_tx.send(prompt).is_err() { - self.chat.fail_pending("AGENT WORKER IS NOT AVAILABLE"); - self.device.agent = AgentState::Faulted; + if let Err(error) = self.supervisor.prompt_agent(&prompt) { + self.messages.last_mut().unwrap().text = format!("Agent is unavailable: {error:#}"); + self.agent_status = "FAULTED"; self.busy = false; } - self.dirty = true; + self.projection_dirty = true; } - fn tap(&mut self, x: u16, y: u16, ui: &Ui) { - match self.screen.handle_tap(x, y, &self.chat, ui) { - ScreenInteraction::None => {} - ScreenInteraction::Redraw => self.dirty = true, - ScreenInteraction::SubmitPrompt(prompt) => self.send_prompt(prompt), - ScreenInteraction::Settings(command) => self.handle_settings(command), - } - } - - fn handle_settings(&mut self, command: SettingsCommand) { - match command { - SettingsCommand::ScanWifi => { - self.settings.wifi.networks = vec![ - WifiNetworkProjection { - ssid: "POCKET-PI-LAB".into(), - rssi_dbm: -38, - secured: true, - }, - WifiNetworkProjection { - ssid: "PHONE-HOTSPOT".into(), - rssi_dbm: -56, - secured: true, - }, - WifiNetworkProjection { - ssid: "GUEST".into(), - rssi_dbm: -71, - secured: false, - }, - WifiNetworkProjection { - ssid: "OFFICE".into(), - rssi_dbm: -74, - secured: true, - }, - WifiNetworkProjection { - ssid: "CAFE".into(), - rssi_dbm: -78, - secured: false, - }, - WifiNetworkProjection { - ssid: "PHONE-2".into(), - rssi_dbm: -82, - secured: true, - }, - ]; - self.settings.wifi.status.clear(); - } - SettingsCommand::ConnectWifi { ssid, password } => { - if !password.is_empty() && !(8..=63).contains(&password.len()) { - self.settings.wifi.status = "PASSWORD MUST BE 8-63 BYTES".into(); - } else { - self.settings.wifi.connected_ssid = Some(ssid); - self.settings.wifi.ip_address = Some("192.0.2.2".into()); - self.settings.wifi.rssi_dbm = Some(-40); - self.settings.wifi.status = "CONNECTED (SIMULATED)".into(); + fn tap(&mut self, x: u16, y: u16) -> Result<()> { + let action = self.supervisor.tap(x, y)?; + match action.get("type").and_then(Value::as_str) { + Some("navigate") => { + if let Some(app) = action.get("app").and_then(Value::as_str) { + self.supervisor.open(app)?; + self.projection_dirty = true; } } - SettingsCommand::ForgetWifi => { - self.settings.wifi.connected_ssid = None; - self.settings.wifi.ip_address = None; - self.settings.wifi.rssi_dbm = None; - self.settings.wifi.status = "NETWORK FORGOTTEN (SIMULATED)".into(); + Some("submitPrompt") => { + if let Some(prompt) = action.get("prompt").and_then(Value::as_str) { + self.send_prompt(prompt.to_owned()); + } } - SettingsCommand::Restart => { - self.settings.wifi.status = "RESTART REQUESTED (SIMULATED)".into(); + Some("invokeTask") => { + if let Some(task) = action.get("task").and_then(Value::as_str) { + self.pending_ui_task = Some(task.to_owned()); + } } + Some("settings") => match action.get("command").and_then(Value::as_str) { + Some("scan") => { + self.wifi_networks = vec![ + ("PocketPi Lab", -42, true), + ("Studio Guest", -61, false), + ("ESP32 Testbench", -73, true), + ]; + self.wifi_status = "3 NETWORKS FOUND".into(); + self.projection_dirty = true; + } + Some("connect") => { + if let Some(ssid) = action.get("ssid").and_then(Value::as_str) { + self.wifi_connected = Some(ssid.to_owned()); + self.wifi_status = format!("CONNECTED TO {ssid}"); + self.projection_dirty = true; + } + } + Some("forget") => { + self.wifi_connected = None; + self.wifi_status = "WI-FI CREDENTIALS FORGOTTEN".into(); + self.projection_dirty = true; + } + Some("restart") => { + self.wifi_status = "SIMULATED RESTART REQUESTED".into(); + self.projection_dirty = true; + } + _ => {} + }, + _ => {} } - self.screen.set_settings(self.settings.clone()); - self.dirty = true; + Ok(()) } - fn release_touch(&mut self) { - self.dirty |= self.screen.handle_touch_release(); + fn pointer_down(&mut self, x: u16, y: u16) -> Result<()> { + self.supervisor.pointer_down(x, y)?; + self.tap(x, y) } - fn poll_agent(&mut self) { - while let Ok(event) = self.agent_rx.try_recv() { - match event { - AgentEvent::Ready => { - self.device.agent = AgentState::Idle; + fn pointer_up(&mut self) -> Result<()> { + self.supervisor.pointer_up() + } + + fn run_pending_ui_task(&mut self) { + let Some(task) = self.pending_ui_task.take() else { + return; + }; + let started = Instant::now(); + let result = self.supervisor.invoke_active_task(&task, &Value::Null); + log::info!( + "UI AppTask {} finished in {}ms: {}", + task, + started.elapsed().as_millis(), + result.text + ); + self.projection_dirty = true; + } + + fn poll(&mut self) -> Result<()> { + while let Ok(request) = self.app_rx.try_recv() { + request.handle(&self.supervisor); + self.projection_dirty = true; + } + if self.last_schedule_poll.elapsed() >= Duration::from_secs(1) { + if !self.busy { + if let Some(wake) = self.native_tools.claim_due() { + self.send_prompt(wake.prompt); } - AgentEvent::Delta(delta) => { - if self.chat.append_model_delta(&delta) { - self.screen.show_latest_chat(); + } + for (task, result) in self.supervisor.poll_due_tasks() { + log::info!("AppTask {task}: {}", result.text); + } + self.last_schedule_poll = Instant::now(); + self.projection_dirty = true; + } + + if self.projection_dirty { + self.supervisor.update_root(&self.root_projection())?; + self.projection_dirty = false; + } + // frame() always advances the Pi Agent System App, even while another + // App owns the visible View. Model/tool work itself remains off-thread. + for event in self.supervisor.frame()? { + match event { + AgentEvent::Ready => self.agent_status = "IDLE", + AgentEvent::ResponseText(text) => { + if let Some(message) = self.messages.last_mut() { + message.text.push_str(&text); } } AgentEvent::Done => { - self.chat.finish_pending(); - self.screen.refresh_workspace(); - self.device.agent = AgentState::Idle; + self.agent_status = "IDLE"; self.busy = false; } AgentEvent::Failed(error) => { - self.chat.fail_pending(format!("AGENT FAILED: {error}")); - self.device.agent = AgentState::Faulted; + if let Some(message) = self.messages.last_mut() { + message.text = format!("Agent failed: {error}"); + } + self.agent_status = "FAULTED"; self.busy = false; } } - self.dirty = true; - } - if self.last_schedule_poll.elapsed() >= Duration::from_secs(1) { - let schedule = self.tools.schedule_projection(); - self.screen - .set_schedule(pocket_pi_device_ui::ScheduleProjection { - name: schedule.name, - prompt: schedule.prompt, - next_in_seconds: schedule.next_in_seconds, - every_minutes: schedule.every_minutes, - }); - if !self.busy { - if let Some(wake) = self.tools.claim_due() { - self.send_prompt(wake.prompt); - } - } - self.last_schedule_poll = Instant::now(); - self.dirty = true; + self.projection_dirty = true; } + Ok(()) } - fn words(&self, ui: &Ui) -> Vec { - self.screen.draw_list(ui, &self.device, &self.chat) - } -} - -fn settings_for(backend: &BackendChoice) -> ModelSettings { - let backend = match backend { - BackendChoice::Wireless { provider, .. } => ModelBackendSettings::Wireless { - provider: *provider, - }, - BackendChoice::Codex { .. } => ModelBackendSettings::Uart { - provider: UartProvider::Codex, - }, - }; - ModelSettings { - backend, - model: None, + fn root_projection(&self) -> Value { + let schedule = self.native_tools.schedule_projection(); + let schedule_next = match schedule.next_in_seconds { + Some(seconds) => schedule.every_minutes.map_or_else( + || format!("in {seconds}s"), + |minutes| format!("in {seconds}s · every {minutes}m"), + ), + None => "not scheduled".to_owned(), + }; + json!({ + "agent":self.agent_status, + "model":self.model_label, + "messages":self.messages.iter().map(|message| json!({"role":message.role,"text":message.text})).collect::>(), + "schedule":{ + "name":schedule.name, + "prompt":schedule.prompt, + "next":schedule_next, + "everyMinutes":schedule.every_minutes, + }, + "apps":self.supervisor.catalog().descriptors().filter(|app| app.id != ROOT_APP_ID).map(|app| json!({ + "id":app.id, + "title":app.title, + "description":app.description, + "scheduleEveryMinutes":app.schedules.first().map(|schedule| schedule.every_minutes), + })).collect::>(), + "settings":{ + "wifi":{ + "connectedSsid":self.wifi_connected, + "ipAddress":if self.wifi_connected.is_some() { Some("192.168.4.20") } else { None }, + "rssiDbm":if self.wifi_connected.is_some() { Some(-42) } else { None }, + "scanning":false, + "networks":self.wifi_networks.iter().map(|(ssid,rssi,secured)| json!({ + "ssid":ssid,"rssiDbm":rssi,"secured":secured + })).collect::>(), + "status":self.wifi_status, + }, + "firmwareVersion":env!("CARGO_PKG_VERSION"), + "workspaceFree":"SIMULATED 24 MB", + } + }) } } @@ -364,18 +579,30 @@ fn headless( output: PathBuf, workspace: PathBuf, prompt: Option, - view: ScreenView, + app: String, + root_tap: Option<(u16, u16)>, backend: BackendChoice, ) -> Result<()> { - let ui = new_ui()?; - let mut product = Product::new(workspace, backend, view)?; + let mut product = Product::new(workspace, backend, &app)?; + if let Some((x, y)) = root_tap { + product.tap(x, y)?; + product.run_pending_ui_task(); + } let wait_for_turn = prompt.is_some(); if let Some(prompt) = prompt { product.send_prompt(prompt); } - for _ in 0..7_500 { - product.poll_agent(); - if !wait_for_turn || !product.busy { + let mut settled_frames = 0; + for frame in 0..7_500 { + product.poll()?; + // Give PocketJS a few ticks to settle reactive insertions and layout + // before taking a deterministic screenshot. + if (!wait_for_turn || !product.busy) && !product.supervisor.services_busy() { + settled_frames += 1; + } else { + settled_frames = 0; + } + if frame >= 2 && settled_frames >= 2 { break; } std::thread::sleep(Duration::from_millis(16)); @@ -385,16 +612,18 @@ fn headless( let target = OffscreenTarget::new(&gpu, PANEL_WIDTH, PANEL_HEIGHT); let mut renderer = UiRenderer::new(&gpu, pocket3d::gpu::OFFSCREEN_FORMAT); let mut encoder = gpu.device.create_command_encoder(&Default::default()); - let words = product.words(&ui); - renderer.render_words( - &gpu, - &ui, - &words, - &mut encoder, - &target.view, - (PANEL_WIDTH, PANEL_HEIGHT), - wgpu::LoadOp::Clear(wgpu::Color::BLACK), - )?; + product.supervisor.with_ui(|ui| { + let words = ui.draw().words.clone(); + renderer.render_words( + &gpu, + ui, + &words, + &mut encoder, + &target.view, + (PANEL_WIDTH, PANEL_HEIGHT), + wgpu::LoadOp::Clear(wgpu::Color::BLACK), + ) + })?; gpu.queue.submit([encoder.finish()]); if let Some(parent) = output.parent() { std::fs::create_dir_all(parent)?; @@ -410,18 +639,16 @@ struct WindowState { surface: wgpu::Surface<'static>, config: wgpu::SurfaceConfiguration, renderer: UiRenderer, - ui: Ui, product: Product, cursor: (u16, u16), touch_down: bool, - frames: u32, - fps_started: Instant, } struct WindowApp { workspace: PathBuf, initial_prompt: Option, - initial_view: ScreenView, + initial_app: String, + initial_root_tap: Option<(u16, u16)>, backend: Option, state: Option, error: Option, @@ -430,14 +657,16 @@ struct WindowApp { fn windowed( workspace: PathBuf, prompt: Option, - view: ScreenView, + app: String, + root_tap: Option<(u16, u16)>, backend: BackendChoice, ) -> Result<()> { let event_loop = EventLoop::new()?; let mut app = WindowApp { workspace, initial_prompt: prompt, - initial_view: view, + initial_app: app, + initial_root_tap: root_tap, backend: Some(backend), state: None, error: None, @@ -448,11 +677,10 @@ fn windowed( impl WindowApp { fn init(&mut self, event_loop: &ActiveEventLoop) -> Result { - let ui = new_ui()?; let window = Arc::new( event_loop.create_window( Window::default_attributes() - .with_title("Pocket Pi — ESP32-P4 Simulator") + .with_title("Pocket Pi AgentOS — ESP32-P4 Simulator") .with_inner_size(winit::dpi::LogicalSize::new(WINDOW_WIDTH, WINDOW_HEIGHT)) .with_resizable(false), )?, @@ -471,7 +699,10 @@ impl WindowApp { .backend .take() .ok_or_else(|| anyhow!("simulator backend was already consumed"))?; - let mut product = Product::new(self.workspace.clone(), backend, self.initial_view)?; + let mut product = Product::new(self.workspace.clone(), backend, &self.initial_app)?; + if let Some((x, y)) = self.initial_root_tap.take() { + product.tap(x, y)?; + } if let Some(prompt) = self.initial_prompt.take() { product.send_prompt(prompt); } @@ -481,27 +712,14 @@ impl WindowApp { surface, config, renderer, - ui, product, cursor: (0, 0), touch_down: false, - frames: 0, - fps_started: Instant::now(), }) } fn redraw(state: &mut WindowState) -> Result<()> { - state.product.poll_agent(); - state.frames += 1; - let elapsed = state.fps_started.elapsed(); - if elapsed >= Duration::from_secs(1) { - state.product.screen.telemetry.ui_fps_tenths = - ((state.frames as f32 / elapsed.as_secs_f32()) * 10.0).round() as u16; - state.frames = 0; - state.fps_started = Instant::now(); - state.product.dirty = true; - } - + state.product.poll()?; let frame = match state.surface.get_current_texture() { Ok(frame) => frame, Err(wgpu::SurfaceError::Lost | wgpu::SurfaceError::Outdated) => { @@ -513,21 +731,23 @@ impl WindowApp { let view = frame.texture.create_view(&Default::default()); let mut encoder = state.gpu.device.create_command_encoder(&Default::default()); let scale = state.config.width as f32 / PANEL_WIDTH as f32; - let words = state.product.words(&state.ui); - state.renderer.render_words_scaled( - &state.gpu, - &state.ui, - &words, - &mut encoder, - &view, - (state.config.width, state.config.height), - scale, - wgpu::LoadOp::Clear(wgpu::Color::BLACK), - )?; + state.product.supervisor.with_ui(|ui| { + let words = ui.draw().words.clone(); + state.renderer.render_words_scaled( + &state.gpu, + ui, + &words, + &mut encoder, + &view, + (state.config.width, state.config.height), + scale, + wgpu::LoadOp::Clear(wgpu::Color::BLACK), + ) + })?; state.gpu.queue.submit([encoder.finish()]); state.window.pre_present_notify(); frame.present(); - state.product.dirty = false; + state.product.run_pending_ui_task(); state.window.request_redraw(); Ok(()) } @@ -574,7 +794,10 @@ impl ApplicationHandler for WindowApp { .. } if !state.touch_down => { state.touch_down = true; - state.product.tap(state.cursor.0, state.cursor.1, &state.ui); + if let Err(error) = state.product.pointer_down(state.cursor.0, state.cursor.1) { + self.error = Some(error); + event_loop.exit(); + } } WindowEvent::MouseInput { button: MouseButton::Left, @@ -582,15 +805,17 @@ impl ApplicationHandler for WindowApp { .. } => { state.touch_down = false; - state.product.release_touch(); - } - WindowEvent::KeyboardInput { event, .. } => { - if event.state == ElementState::Pressed - && matches!(event.physical_key, PhysicalKey::Code(KeyCode::Escape)) - { + if let Err(error) = state.product.pointer_up() { + self.error = Some(error); event_loop.exit(); } } + WindowEvent::KeyboardInput { event, .. } + if event.state == ElementState::Pressed + && matches!(event.physical_key, PhysicalKey::Code(KeyCode::Escape)) => + { + event_loop.exit(); + } WindowEvent::RedrawRequested => { if let Err(error) = Self::redraw(state) { self.error = Some(error); @@ -605,134 +830,222 @@ impl ApplicationHandler for WindowApp { #[cfg(test)] mod tests { use super::*; + use pocket_pi_embedded::ToolResult; - #[test] - fn bottom_navigation_uses_the_physical_screen_hit_map() { - let temp = tempfile::tempdir().unwrap(); - let ui = new_ui().unwrap(); - let mut product = Product::new( - temp.path().to_owned(), - BackendChoice::Codex { model: None }, - ScreenView::Chat, - ) - .unwrap(); + fn init_logs() { + let _ = env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")) + .is_test(true) + .try_init(); + } - product.tap(360, 1220, &ui); + struct FailingRobinhood; - assert_eq!(product.screen.view, ScreenView::Files); - product.screen.view = ScreenView::Chat; - product.tap(650, 1220, &ui); - assert_eq!(product.screen.view, ScreenView::Settings); + impl AppServiceHost for FailingRobinhood { + fn call( + &self, + _app_id: &str, + service: &str, + operation: &str, + _args: &Value, + _deadline: Instant, + ) -> Result { + let _ = (service, operation); + Err("simulated Robinhood outage".into()) + } } - #[test] - fn settings_scan_and_wifi_password_use_the_shared_touch_keyboard() { - let temp = tempfile::tempdir().unwrap(); - let ui = new_ui().unwrap(); - let mut product = Product::new( - temp.path().to_owned(), - BackendChoice::Codex { model: None }, - ScreenView::Settings, - ) - .unwrap(); + struct NoTools; - product.tap(580, 180, &ui); - product.tap(120, 350, &ui); - for _ in 0..8 { - product.tap(54, 548, &ui); + impl ToolHost for NoTools { + fn definitions(&self) -> Vec { + Vec::new() } - product.tap(640, 986, &ui); - assert_eq!( - product.settings.wifi.connected_ssid.as_deref(), - Some("POCKET-PI-LAB") - ); - assert!(!product.screen.handle_touch_release()); + fn execute(&self, _call_id: &str, name: &str, _args_json: &str) -> ToolResult { + ToolResult { + text: format!("unexpected Tool {name}"), + is_error: true, + ..ToolResult::default() + } + } } - #[test] - fn settings_wifi_list_scrolls_to_later_networks() { - let temp = tempfile::tempdir().unwrap(); - let ui = new_ui().unwrap(); - let mut product = Product::new( - temp.path().to_owned(), - BackendChoice::Codex { model: None }, - ScreenView::Settings, - ) - .unwrap(); + fn route_tool(supervisor: &mut AppSupervisor, name: &str, args_json: &str) -> ToolResult { + let (tools, requests) = + RoutedToolHost::new(Arc::new(NoTools), supervisor.catalog().clone()); + let name = name.to_owned(); + let args_json = args_json.to_owned(); + let call = std::thread::spawn(move || tools.execute("test", &name, &args_json)); + let deadline = Instant::now() + Duration::from_secs(5); + while !call.is_finished() { + while let Ok(request) = requests.try_recv() { + request.handle(supervisor); + } + supervisor.frame_render(true).unwrap(); + assert!(Instant::now() < deadline, "timed out routing App Tool"); + std::thread::sleep(Duration::from_millis(2)); + } + call.join().unwrap() + } - product.tap(580, 180, &ui); - assert!(product.settings.wifi.status.is_empty()); - product.tap(660, 710, &ui); - product.tap(120, 626, &ui); + fn wait_for(mut ready: impl FnMut() -> bool) { + for _ in 0..100 { + if ready() { + return; + } + std::thread::sleep(Duration::from_millis(20)); + } + panic!("timed out waiting for background App Data Action"); + } - assert_eq!( - product.settings.wifi.connected_ssid.as_deref(), - Some("CAFE") - ); + fn try_query_app_database( + workspace: &Path, + app_id: &str, + database_name: &str, + sql: &str, + ) -> Option> { + let mut database = pocket_db::DbModule::new(pocket_db::Storage::Dir( + workspace.join("apps").join(app_id).join("data"), + )); + let handle = database.open(database_name); + if handle < 0 { + return None; + } + let value: Value = serde_json::from_str(&database.query(handle, sql, "[]")).ok()?; + database.close(handle); + if value.get("error").is_some() { + return None; + } + value["rows"].as_array().cloned() + } + + fn query_app_database( + workspace: &Path, + app_id: &str, + database_name: &str, + sql: &str, + ) -> Vec { + try_query_app_database(workspace, app_id, database_name, sql) + .unwrap_or_else(|| panic!("query {app_id}/{database_name}: {sql}")) } #[test] - fn mouse_coordinates_drive_the_same_keyboard_as_touch() { + fn exa_tool_writes_app_owned_sqlite() { + init_logs(); let temp = tempfile::tempdir().unwrap(); - let ui = new_ui().unwrap(); - let mut screen = ScreenState::new(temp.path().to_str().unwrap()); - let chat = ChatProjection::new("YOU", "PI"); - - assert_eq!( - screen.handle_tap(360, 1100, &chat, &ui), - ScreenInteraction::Redraw - ); - assert_eq!( - screen.handle_tap(54, 548, &chat, &ui), - ScreenInteraction::Redraw + let mut supervisor = + AppSupervisor::new(temp.path(), catalog().unwrap(), Arc::new(SimAppServices)).unwrap(); + let result = route_tool( + &mut supervisor, + "research.search", + r#"{"query":"NVIDIA FY2026 annual report 10-K revenue data center guidance","numResults":5,"includeDomains":["investor.nvidia.com","sec.gov"]}"#, ); - assert_eq!( - screen.handle_tap(640, 986, &chat, &ui), - ScreenInteraction::SubmitPrompt("q".into()) + assert!(!result.is_error, "{}", result.text); + assert!(temp.path().join("apps/exa/data/exa.sqlite").exists()); + let rows = query_app_database( + temp.path(), + "exa", + "exa", + "SELECT status,result_count FROM searches ORDER BY id DESC LIMIT 1", ); + assert_eq!(rows, vec![json!(["ok", 2])]); } #[test] - fn touch_keyboard_can_type_uppercase_wifi_passwords() { + fn exa_write_removes_rows_older_than_seven_days() { + init_logs(); let temp = tempfile::tempdir().unwrap(); - let ui = new_ui().unwrap(); - let mut screen = ScreenState::new(temp.path().to_str().unwrap()); - let chat = ChatProjection::new("YOU", "PI"); - - assert_eq!( - screen.handle_tap(360, 1100, &chat, &ui), - ScreenInteraction::Redraw + let mut supervisor = + AppSupervisor::new(temp.path(), catalog().unwrap(), Arc::new(SimAppServices)).unwrap(); + let first = route_tool( + &mut supervisor, + "research.search", + r#"{"query":"expired search"}"#, ); + assert!(!first.is_error, "{}", first.text); + + let mut database = + pocket_db::DbModule::new(pocket_db::Storage::Dir(temp.path().join("apps/exa/data"))); + let handle = database.open("exa"); + assert!(handle >= 0); assert_eq!( - screen.handle_tap(450, 986, &chat, &ui), - ScreenInteraction::Redraw + database.exec(handle, "UPDATE searches SET searched_at=0;"), + 0, + "{}", + database.last_error(handle) ); - assert_eq!( - screen.handle_tap(54, 548, &chat, &ui), - ScreenInteraction::Redraw + database.close(handle); + + let second = route_tool( + &mut supervisor, + "research.search", + r#"{"query":"new search"}"#, ); - assert_eq!( - screen.handle_tap(640, 986, &chat, &ui), - ScreenInteraction::SubmitPrompt("Q".into()) + assert!(!second.is_error, "{}", second.text); + let rows = query_app_database( + temp.path(), + "exa", + "exa", + "SELECT id,query FROM searches ORDER BY id", ); + assert_eq!(rows, vec![json!([2, "new search"])]); } #[test] - fn workspace_row_opens_the_physical_file_viewer() { + fn robinhood_refresh_failure_records_no_view_data() { + init_logs(); let temp = tempfile::tempdir().unwrap(); - std::fs::write(temp.path().join("memory.md"), "shared-ui").unwrap(); - let ui = new_ui().unwrap(); - let mut product = Product::new( - temp.path().to_owned(), - BackendChoice::Codex { model: None }, - ScreenView::Chat, - ) - .unwrap(); + let mut supervisor = + AppSupervisor::new(temp.path(), catalog().unwrap(), Arc::new(FailingRobinhood)) + .unwrap(); + supervisor.open("robinhood").unwrap(); + + let failed = supervisor.invoke_active_task("refreshPortfolio", &Value::Null); + assert!(!failed.is_error); + wait_for(|| { + try_query_app_database( + temp.path(), + "robinhood", + "robinhood", + "SELECT status FROM refresh_runs ORDER BY id DESC LIMIT 1", + ) + .is_some_and(|rows| rows.first() == Some(&json!(["failed"]))) + }); + supervisor.frame_render(true).unwrap(); + + let rows = query_app_database( + temp.path(), + "robinhood", + "robinhood", + "SELECT COUNT(*) FROM total_value", + ); + assert_eq!(rows, vec![json!([0])]); + } - product.tap(360, 1220, &ui); - product.tap(120, 220, &ui); + #[test] + fn robinhood_refresh_writes_the_fixed_view_projection() { + init_logs(); + let temp = tempfile::tempdir().unwrap(); + let mut supervisor = + AppSupervisor::new(temp.path(), catalog().unwrap(), Arc::new(SimAppServices)).unwrap(); + supervisor.open("robinhood").unwrap(); + let refreshed = route_tool(&mut supervisor, "robinhood.refresh_portfolio", "{}"); + assert!(!refreshed.is_error, "{}", refreshed.text); + supervisor.frame_render(true).unwrap(); - assert_eq!(product.screen.view, ScreenView::Viewer); + let rows = query_app_database( + temp.path(), + "robinhood", + "robinhood", + "SELECT status,operation_count,success_count FROM refresh_runs ORDER BY id DESC LIMIT 1", + ); + assert_eq!(rows, vec![json!(["succeeded", 16, 16])]); + let rows = query_app_database( + temp.path(), + "robinhood", + "robinhood", + "SELECT (SELECT COUNT(*) FROM accounts),(SELECT COUNT(*) FROM portfolio_current),(SELECT COUNT(*) FROM total_value)", + ); + assert_eq!(rows, vec![json!([3, 3, 3])]); } } diff --git a/hosts/macos/Cargo.toml b/hosts/macos/Cargo.toml deleted file mode 100644 index 0b04235..0000000 --- a/hosts/macos/Cargo.toml +++ /dev/null @@ -1,11 +0,0 @@ -[package] -name = "pocket-pi-macos" -version = "0.1.0" -edition.workspace = true -license.workspace = true -repository.workspace = true -publish = false - -[dependencies] -pocket-pi = { path = "../../crates/pocket-pi" } -serde_json.workspace = true diff --git a/hosts/macos/src/main.rs b/hosts/macos/src/main.rs deleted file mode 100644 index ec1972b..0000000 --- a/hosts/macos/src/main.rs +++ /dev/null @@ -1,85 +0,0 @@ -use std::cell::RefCell; -use std::io::Write; -use std::rc::Rc; -use std::time::{Duration, Instant}; - -use pocket_pi::{PiRuntime, ToolResult}; - -fn main() -> Result<(), String> { - let prompt = std::env::args().skip(1).collect::>().join(" "); - let prompt = if prompt.is_empty() { - "In one short sentence, what are you?".to_owned() - } else { - prompt - }; - - let mut runtime = PiRuntime::new()?; - runtime.register_tool("current_time", |_| { - let seconds = std::time::SystemTime::now() - .duration_since(std::time::UNIX_EPOCH) - .map(|duration| duration.as_secs()) - .unwrap_or_default(); - ToolResult::text(format!("unix seconds: {seconds}")) - }); - let error = Rc::new(RefCell::new(None)); - let callback_error = error.clone(); - runtime.on_event(move |event| match event.kind.as_str() { - "text" => { - if let Some(delta) = event.value.get("delta").and_then(|value| value.as_str()) { - print!("{delta}"); - std::io::stdout().flush().ok(); - } - } - "end" => println!(), - "error" => { - let message = event - .value - .get("message") - .and_then(|value| value.as_str()) - .unwrap_or(&event.raw) - .to_owned(); - eprintln!("\n{message}"); - *callback_error.borrow_mut() = Some(message); - } - _ => {} - }); - - let config = if let Ok(api_key) = std::env::var("OPENAI_API_KEY") { - serde_json::json!({ - "provider": "openai", - "model": std::env::var("OPENAI_MODEL").unwrap_or_else(|_| "gpt-5.6".into()), - "apiKey": api_key, - "systemPrompt": "You are Pocket Pi on macOS. Be concise." - }) - } else if let Ok(api_key) = std::env::var("ANTHROPIC_API_KEY") { - serde_json::json!({ - "provider": "anthropic", - "model": std::env::var("ANTHROPIC_MODEL") - .unwrap_or_else(|_| "claude-opus-4-8".into()), - "apiKey": api_key, - "systemPrompt": "You are Pocket Pi on macOS. Be concise." - }) - } else { - serde_json::json!({ - "model": "offline", - "scripted": {"steps": [{"text": "Pocket Pi desktop runtime is ready."}]} - }) - }; - runtime.boot(&config.to_string())?; - println!("you> {prompt}\npi > "); - runtime.prompt(&prompt)?; - - let started = Instant::now(); - while !runtime.is_idle() && started.elapsed() < Duration::from_secs(120) { - runtime.pump()?; - std::thread::sleep(Duration::from_millis(33)); - } - runtime.pump().ok(); - if let Some(message) = error.borrow_mut().take() { - return Err(format!("model backend failed: {message}")); - } - if !runtime.is_idle() { - return Err("model backend timed out after 120 seconds".into()); - } - Ok(()) -} diff --git a/js/.gitignore b/js/.gitignore deleted file mode 100644 index 3c3629e..0000000 --- a/js/.gitignore +++ /dev/null @@ -1 +0,0 @@ -node_modules diff --git a/js/build.mjs b/js/build.mjs deleted file mode 100644 index 82f7c5f..0000000 --- a/js/build.mjs +++ /dev/null @@ -1,91 +0,0 @@ -// Unified build for all of Pocket Pi's JavaScript guest layer. -// -// Sources are TypeScript under js/src/; this emits the artifacts the Rust crate -// embeds. Run with plain Node: `npm install && node js/build.mjs`. -// -// src/runtime/** → crates/pocket-pi/js/** (per-file transpile; embedded via include_str!) -// src/pi-full/{driver,ext-probe,persist-probe} -// → crates/pocket-pi/js/pi-full/** (test harness scripts) -// src/pi-full/entry → crates/pocket-pi/js/pi-full.bundle.js(.gz) (full unmodified pi) -// -// pi is a real, unmodified npm dependency (package.json) — sync with `npm update` -// and rerun this. Runtime modules and the compressed full-pi bundle are committed -// so `cargo` builds Rust-only; only the raw full-pi bundle is git-ignored. - -import * as esbuild from "esbuild"; -import { execFileSync } from "node:child_process"; -import { gzipSync } from "node:zlib"; -import { readFileSync, writeFileSync, readdirSync, statSync, existsSync } from "node:fs"; -import { fileURLToPath } from "node:url"; -import { dirname, join, relative } from "node:path"; - -const here = dirname(fileURLToPath(import.meta.url)); -const root = join(here, ".."); -const outJs = join(root, "crates/pocket-pi/js"); -const nm = join(here, "node_modules"); - -if (!existsSync(join(nm, "@earendil-works/pi-coding-agent"))) { - console.error("pi packages not installed — run `npm install` in js/ first."); - process.exit(1); -} - -const mb = (n) => (n / 1048576).toFixed(1); -function walk(dir) { - return readdirSync(dir).flatMap((name) => { - const p = join(dir, name); - return statSync(p).isDirectory() ? walk(p) : [p]; - }); -} - -// 1. Typecheck gate over Pocket Pi's own orchestration code (see tsconfig.json). -console.log("• typecheck (tsc --noEmit)"); -execFileSync(join(nm, ".bin/tsc"), ["--noEmit", "-p", join(here, "tsconfig.json")], { stdio: "inherit" }); - -// 2. Runtime glue: per-file transpile (type-strip), structure preserved. These -// are loaded individually as modules by the runtime, so no bundling. -const runtimeSrc = join(here, "src/runtime"); -console.log("• runtime glue → crates/pocket-pi/js"); -await esbuild.build({ - entryPoints: walk(runtimeSrc).filter((f) => f.endsWith(".ts")), - outdir: outJs, - outbase: runtimeSrc, - bundle: false, - format: "esm", - platform: "neutral", - logLevel: "warning", -}); - -// 3. The host harness (PocketPi on full pi) + test-harness scripts, eval'd as -// plain scripts by the runtime / the Rust tests. -console.log("• harness scripts → crates/pocket-pi/js/pi-full"); -await esbuild.build({ - entryPoints: ["host", "driver", "ext-probe", "persist-probe"].map((n) => join(here, `src/pi-full/${n}.ts`)), - outdir: join(outJs, "pi-full"), - bundle: false, - format: "esm", - platform: "neutral", - logLevel: "warning", -}); - -// 4. Full, unmodified pi-coding-agent bundle. Whitespace-minify only (identifier -// and syntax minification emit tokens the embedded QuickJS parser rejects); -// line-limit wraps the long lines QuickJS chokes on; undici → stub. -console.log("• full pi bundle → pi-full.bundle.js(.gz)"); -const fullOut = join(outJs, "pi-full.bundle.js"); -await esbuild.build({ - entryPoints: [join(here, "src/pi-full/entry.ts")], - outfile: fullOut, - bundle: true, - format: "esm", - platform: "node", - legalComments: "none", - minifyWhitespace: true, - lineLimit: 500, - alias: { undici: join(here, "src/pi-full/undici-stub.ts") }, - logLevel: "warning", -}); -const src = readFileSync(fullOut); -const gz = gzipSync(src, { level: 9 }); -writeFileSync(`${fullOut}.gz`, gz); - -console.log(`\n✓ built. full pi: ${mb(src.length)} MB minified → ${mb(gz.length)} MB gzip`); diff --git a/js/env.d.ts b/js/env.d.ts deleted file mode 100644 index 7923ee2..0000000 --- a/js/env.d.ts +++ /dev/null @@ -1,91 +0,0 @@ -// Ambient types for Pocket Pi's runtime environment. The glue is a deliberately -// loose Node/Web compatibility layer over QuickJS + the native `host`/`__node` -// ops, so the runtime surface is typed as `any` — this is shim code, not typed -// application logic. Its job is to let the TypeScript build (transpile + -// `tsc --noEmit` gate) run over the sources. - -export {}; - -declare global { - // Native surface mounted by Rust. - var host: any; - var __node: any; - - // Node/Web globals the glue defines on globalThis (and reads back). - var process: any; - var Buffer: any; - var __nodeBuffer: any; - var global: any; - var require: any; - var console: any; - var performance: any; - var crypto: any; - - var fetch: any; - var Headers: any; - var Request: any; - var Response: any; - var URL: any; - var URLSearchParams: any; - var ReadableStream: any; - var TextEncoder: any; - var TextDecoder: any; - var Blob: any; - var File: any; - var FormData: any; - var AbortController: any; - var AbortSignal: any; - var Event: any; - var EventTarget: any; - var CustomEvent: any; - var MessagePort: any; - var MessageChannel: any; - var DOMException: any; - - var atob: any; - var btoa: any; - var structuredClone: any; - var queueMicrotask: any; - var setTimeout: any; - var clearTimeout: any; - var setInterval: any; - var clearInterval: any; - var setImmediate: any; - var clearImmediate: any; - - // Pocket Pi coordination surface (guest entry points + host-poll flags). - var PocketPi: any; - var PiFull: any; - var __builtinExports: any; - var __cjsRequire: any; - var __cjsCache: any; - var __catpiTimers: any; - var __catpiPump: any; - var __catpiFetchPump: any; - - // Test-harness globals (driver / probes exchange results with the Rust side). - var __OPENAI_KEY: any; - var __piRun: any; - var __piResult: any; - var __piError: any; - var __piDone: any; - var __piBind: any; - var __piLog: any; - var __piLastEvent: any; - var __piFullLoaded: any; - var __piLoadExtension: any; - var __piExtResult: any; - var __piExtError: any; - var __piExtDone: any; - var __piPersist: any; - var __piPersistResult: any; - var __piPersistError: any; -} - -// The `node:*` builtins are served by Pocket Pi's own loader at runtime; type -// them loosely so cross-builtin imports (`import { EventEmitter } from -// "node:events"`) don't need @types/node. -declare module "node:*" { - const anything: any; - export = anything; -} diff --git a/js/package-lock.json b/js/package-lock.json deleted file mode 100644 index 4f06da4..0000000 --- a/js/package-lock.json +++ /dev/null @@ -1,3619 +0,0 @@ -{ - "name": "pocket-pi-guest", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "pocket-pi-guest", - "dependencies": { - "@earendil-works/pi-agent-core": "0.81.1", - "@earendil-works/pi-ai": "0.81.1", - "@earendil-works/pi-coding-agent": "0.81.1", - "@google/genai": "^2.12.0", - "@mistralai/mistralai": "^2.5.0", - "@opentelemetry/api": "^1.9.1", - "@opentelemetry/api-logs": "^0.220.0" - }, - "devDependencies": { - "esbuild": "0.24.2", - "typescript": "5.7.2" - } - }, - "node_modules/@anthropic-ai/sdk": { - "version": "0.91.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", - "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "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==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@aws-sdk/core": { - "version": "3.975.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.975.3.tgz", - "integrity": "sha512-7ur3kCKuvPLqlsZ2XlvnNBVQ7KkpSu6Y6dOTwSPHLrFpTEfZM8isLBJc4cgv96WB7GifeVM436mpycwxBd2vEA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.2", - "@aws-sdk/xml-builder": "^3.972.36", - "@aws/lambda-invoke-store": "^0.3.0", - "@smithy/core": "^3.29.4", - "@smithy/signature-v4": "^5.6.5", - "@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.59", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.59.tgz", - "integrity": "sha512-Ny5e4Mfh3QPmiAc0AiUe+cbTXDlxkU3Rc+EpWOfyWeWEy6yp7Fa1KmfNeCc+1a8by9zQ9gtohmiQUkMPScF3ng==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.61", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.61.tgz", - "integrity": "sha512-8jAjgStl5Ytq4+HF3X/9f+EmRinaRbGRRtQGktlPfBRVx73H+R1y48vIeXerQtYGFaUqkEp3fT6jP854rVO2yQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/fetch-http-handler": "^5.6.6", - "@smithy/node-http-handler": "^4.9.6", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.973.4", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.4.tgz", - "integrity": "sha512-e6ZvVsj90aRALf1kHP+J4iqC1496ZpVgqI/+u0LJ5HL7q7ATauGy4gdDvRCP13L1pN/fMiZLah162PGIYkbUVQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/credential-provider-env": "^3.972.59", - "@aws-sdk/credential-provider-http": "^3.972.61", - "@aws-sdk/credential-provider-login": "^3.972.66", - "@aws-sdk/credential-provider-process": "^3.972.59", - "@aws-sdk/credential-provider-sso": "^3.973.3", - "@aws-sdk/credential-provider-web-identity": "^3.972.65", - "@aws-sdk/nested-clients": "^3.997.33", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/credential-provider-imds": "^4.4.9", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.66", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.66.tgz", - "integrity": "sha512-g2fsqm87r/nKthLZ0VkkDBElkGg0PvSa8d97HQ6EilMbJTZ6hxa8FxkSZyJfgPfFdZn0TTmkOffQmTSUcAHIng==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/nested-clients": "^3.997.33", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.70", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.70.tgz", - "integrity": "sha512-3xzvkGdykBunxqh8WudmUpSyLWvIhfI6aBQo1b5rb3mDO5mNLadK+0hiI0qBQBMVynJbfLO+Ajy9dztMwy9O8w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.59", - "@aws-sdk/credential-provider-http": "^3.972.61", - "@aws-sdk/credential-provider-ini": "^3.973.4", - "@aws-sdk/credential-provider-process": "^3.972.59", - "@aws-sdk/credential-provider-sso": "^3.973.3", - "@aws-sdk/credential-provider-web-identity": "^3.972.65", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/credential-provider-imds": "^4.4.9", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.59", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.59.tgz", - "integrity": "sha512-DlZF2/MhLlatDdlrIy3CUCpfdbLrKx+3SMjVo+WyHnPpwzkc/M3vwAHw4OVJf7DMvO+4vfRqSCMc/E9I1auN0g==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.973.3", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.3.tgz", - "integrity": "sha512-hmdDHoy2G5Es2e8IgelNMYUuSQI6uCIAKZMJ2u2PdKDhxvbk1uWD/g4+R7R5c/tJfKEB1+KjjWiaoCr/S+ZTiQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/nested-clients": "^3.997.33", - "@aws-sdk/token-providers": "3.1088.0", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@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.1088.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1088.0.tgz", - "integrity": "sha512-4ObatWt2qpJg5FBk4LOOKrTQYzaqeewAtdO3r9ZO8lH9YqLtpTzLyIdy0mJ+nVdfYOnqISkKNfmzP22bNDhwyw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/nested-clients": "^3.997.33", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@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.65", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.65.tgz", - "integrity": "sha512-gHQb/Kt0chjk/JQDa/GJDqmAvEuVn8n7z10wK2h0LFM9TUDRkohgOO4aEF+s2sBLM0br7Cl5W6P7phgjrrJvLQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/nested-clients": "^3.997.33", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.29", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.29.tgz", - "integrity": "sha512-t3tKQRTVXsI2QNPE3CaNjHl0wRO9Xi3acZkAyti2RQsiFmZ9Gi0kArX2ighlRJ1BtDVuul413gThAgzyTfgmWA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.24.tgz", - "integrity": "sha512-oykin4mDWxNOuYQ7SF1cHzgYeuFEkF4cdRwgvjFFbIklkx09qIFBiOgsORafG9sXZFO3TayMmQuAQYgADXhI8w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.41.tgz", - "integrity": "sha512-LSbGvvYmjc4Br9BPYI2dTLnIclmrSiQbahkP4D6nRGVEv4qsCZ8csVuKBPVEEFCVD+EEngGh8ROls6XpumtwMg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/fetch-http-handler": "^5.6.6", - "@smithy/signature-v4": "^5.6.5", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@aws-sdk/nested-clients": { - "version": "3.997.33", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.33.tgz", - "integrity": "sha512-dVZOroI/r3/ENvqNGgjMPul+jjlz9GddfVusgTXlVjfZj5isibOxecLkGQbRPp8XOuX+RAfjXLFgPkD1JS5xrw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.975.3", - "@aws-sdk/signature-v4-multi-region": "^3.996.41", - "@aws-sdk/types": "^3.974.2", - "@smithy/core": "^3.29.4", - "@smithy/fetch-http-handler": "^5.6.6", - "@smithy/node-http-handler": "^4.9.6", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.41.tgz", - "integrity": "sha512-QMUytg+FQMGouc8gHS00KoYih3+N6cqmVI/pQGOIo7Nr7OpQaiXjSYOuL+vsPZ1tymY4LAQ8MYcHJmws5LRxng==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.974.2", - "@smithy/signature-v4": "^5.6.5", - "@smithy/types": "^4.16.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==", - "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==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@aws-sdk/xml-builder": { - "version": "3.972.36", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.36.tgz", - "integrity": "sha512-RdGmS1GLrtaTOLE1ElSluMldNrpk9Emq6uYs8SS8iHlu5xTAmM9rRkM91o48+rIRryBtyO9t+uLYCoMG6jVMVA==", - "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==", - "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==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@earendil-works/pi-agent-core": { - "version": "0.81.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.81.1.tgz", - "integrity": "sha512-yqbh68CyhqxMov/jUogFJfMqlu2Gd37GAki+tr59YCmAPHfomiCA5ESzusXtpGzABeiZFC/OrRdQ4GwCCOMIHA==", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-ai": "^0.81.1", - "ignore": "7.0.5", - "typebox": "1.1.38", - "yaml": "2.9.0" - }, - "engines": { - "node": ">=22.19.0" - } - }, - "node_modules/@earendil-works/pi-agent-core/node_modules/ignore": { - "version": "7.0.5", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-7.0.5.tgz", - "integrity": "sha512-Hs59xBNfUIunMFgWAbGX5cq6893IbWg4KnrjbYwX3tx0ztorVgTDA6B2sxf8ejHJ4wz8BqGUMYlnzNBer5NvGg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/@earendil-works/pi-agent-core/node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", - "license": "MIT" - }, - "node_modules/@earendil-works/pi-ai": { - "version": "0.81.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.81.1.tgz", - "integrity": "sha512-hzHE7Z8l5mgJk+ke67Lge0rwS2+wbKJrFKl9o5M1R1rh33+cCT7D1AHz1OAtX5wFs90E1/BTGhyJRTUHaMxGvQ==", - "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-ai/node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", - "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/credential-provider-node": "^3.972.42", - "@aws-sdk/eventstream-handler-node": "^3.972.16", - "@aws-sdk/middleware-eventstream": "^3.972.12", - "@aws-sdk/middleware-websocket": "^3.972.19", - "@aws-sdk/token-providers": "3.1048.0", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-ai/node_modules/@aws-sdk/token-providers": { - "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", - "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-ai/node_modules/@google/genai": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", - "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", - "hasInstallScript": true, - "license": "Apache-2.0", - "dependencies": { - "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-ai/node_modules/@mistralai/mistralai": { - "version": "2.2.6", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.2.6.tgz", - "integrity": "sha512-W8pX7zHxjJvMIpw8JMxeJEleapXX0Q9NPszdNzqkM3MIEoIGPObdodujj+WHteXEvGfaP/AMwlNyRfEzSY6dQQ==", - "license": "Apache-2.0", - "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-ai/node_modules/@opentelemetry/api": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.0.tgz", - "integrity": "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@earendil-works/pi-ai/node_modules/@smithy/node-http-handler": { - "version": "4.7.3", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", - "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.24.3", - "@smithy/types": "^4.14.2", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@earendil-works/pi-ai/node_modules/typebox": { - "version": "1.1.38", - "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.1.38.tgz", - "integrity": "sha512-pZ0aQPmMmXoUvSbeuWf/Hzsc+avNw/Zd6VeE8CFgkVGWyuHPJvqeJJDeJqLve+K70LvjYIoleGcoJHPT17cWoA==", - "license": "MIT" - }, - "node_modules/@earendil-works/pi-coding-agent": { - "version": "0.81.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.81.1.tgz", - "integrity": "sha512-r6ovAsZOgAqbC/aU6s+/dPnv/sGZBuWyZNvi3pXjpbuX5wvp3XvGkQI7/VLvX2o9XpmpFaPUxKNym1WfkN/P8A==", - "hasShrinkwrap": true, - "license": "MIT", - "dependencies": { - "@earendil-works/pi-agent-core": "^0.81.1", - "@earendil-works/pi-ai": "^0.81.1", - "@earendil-works/pi-tui": "^0.81.1", - "@silvia-odwyer/photon-node": "0.3.4", - "chalk": "5.6.2", - "cross-spawn": "7.0.6", - "diff": "8.0.4", - "glob": "13.0.6", - "highlight.js": "10.7.3", - "hosted-git-info": "9.0.3", - "ignore": "7.0.5", - "jiti": "2.7.0", - "minimatch": "10.2.5", - "proper-lockfile": "4.1.2", - "semver": "7.8.0", - "typebox": "1.1.38", - "undici": "8.5.0", - "yaml": "2.9.0" - }, - "bin": { - "pi": "dist/cli.js" - }, - "engines": { - "node": ">=22.19.0" - }, - "optionalDependencies": { - "@mariozechner/clipboard": "0.3.9" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@anthropic-ai/sdk": { - "version": "0.91.1", - "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.91.1.tgz", - "integrity": "sha512-LAmu761tSN9r66ixvmciswUj/ZC+1Q4iAfpedTfSVLeswRwnY3n2Nb6Tsk+cLPP28aLOPWeMgIuTuCcMC6W/iw==", - "license": "MIT", - "dependencies": { - "json-schema-to-ts": "^3.1.1" - }, - "bin": { - "anthropic-ai-sdk": "bin/cli" - }, - "peerDependencies": { - "zod": "^3.25.0 || ^4.0.0" - }, - "peerDependenciesMeta": { - "zod": { - "optional": true - } - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/crc32": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/crc32/-/crc32-5.2.0.tgz", - "integrity": "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-browser": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-browser/-/sha256-browser-5.2.0.tgz", - "integrity": "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-js": "^5.2.0", - "@aws-crypto/supports-web-crypto": "^5.2.0", - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "@aws-sdk/util-locate-window": "^3.0.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/sha256-js": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/sha256-js/-/sha256-js-5.2.0.tgz", - "integrity": "sha512-FFQQyu7edu4ufvIZ+OadFpHHOt+eSTBaYaki44c+akjg7qZg9oOQeLlk77F6tSYqjDAFClrHJk9tMf0HdVyOvA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/util": "^5.2.0", - "@aws-sdk/types": "^3.222.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=16.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/supports-web-crypto": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/supports-web-crypto/-/supports-web-crypto-5.2.0.tgz", - "integrity": "sha512-iAvUotm021kM33eCdNfwIN//F77/IADDSs58i+MDaOqFrVjZo9bAal0NK7HurRuWLLpF1iLX7gbWrjHjeo+YFg==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-crypto/util": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/@aws-crypto/util/-/util-5.2.0.tgz", - "integrity": "sha512-4RkU9EsI6ZpBve5fseQlGNUWKMa1RLPQ1dnjnQoe07ldfIzcsGb5hC5W0Dm7u423KWzawlrpbjXBrXCEv9zazQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.222.0", - "@smithy/util-utf8": "^2.0.0", - "tslib": "^2.6.2" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/client-bedrock-runtime": { - "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/client-bedrock-runtime/-/client-bedrock-runtime-3.1048.0.tgz", - "integrity": "sha512-u+NT61JZEkRFtpL0CAw1N1dwxnaLgwVXQl/zjJxTGgLyS/jTIdg2SdoEoCTHxgDyCnqa1HEi9QOoE9/pYRNpOQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/credential-provider-node": "^3.972.42", - "@aws-sdk/eventstream-handler-node": "^3.972.16", - "@aws-sdk/middleware-eventstream": "^3.972.12", - "@aws-sdk/middleware-websocket": "^3.972.19", - "@aws-sdk/token-providers": "3.1048.0", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/core": { - "version": "3.974.11", - "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.974.11.tgz", - "integrity": "sha512-QpnINq5FZH6EOaDEkmHdT7eUunbvD27pDNQypaWjFyYz7Zl1q3UCMQErBZxpmfGfI7MvI2TlK8KTkgNpv8b1ug==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@aws-sdk/xml-builder": "^3.972.24", - "@aws/lambda-invoke-store": "^0.2.2", - "@smithy/core": "^3.24.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", - "bowser": "^2.11.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-env": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.37.tgz", - "integrity": "sha512-/jpPvEh6f7ntmIzf7dNxoNX6Q8vt8UpesCjbW6mFfk4V1NW6bIy9qxcQ6WbA8As5yQhsZOe+xeNd4xHX8kdY2Q==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-http": { - "version": "3.972.39", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.39.tgz", - "integrity": "sha512-pIgTpisWyWg7X1bUbzSjuUYosYTD0Ghz2M0hkSTmb3a6i3qV3uU+NYJPI/E2XSC0HcsZh5rsLPzeXrkb2DS0Cg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-ini": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.972.41.tgz", - "integrity": "sha512-u2tyjaxJJzW8UtW4SM1ZcPMDwO6y+kV+llvou+Adts0FAKyzes5jG4izQN+KX3yE8ZROpS5y1LJ//xL2iSf76w==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/credential-provider-env": "^3.972.37", - "@aws-sdk/credential-provider-http": "^3.972.39", - "@aws-sdk/credential-provider-login": "^3.972.41", - "@aws-sdk/credential-provider-process": "^3.972.37", - "@aws-sdk/credential-provider-sso": "^3.972.41", - "@aws-sdk/credential-provider-web-identity": "^3.972.41", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-login": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.41.tgz", - "integrity": "sha512-0LBitxXiAiaE5nlFPfpNIww/8FRY/I7WIndWsc9GmNFOM7cE1wNpVNQEGEk9Outg5l8xl+3vybxFyUy4l9q/LQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-node": { - "version": "3.972.42", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.42.tgz", - "integrity": "sha512-D4oon2zbqqsWOJUM99Gm3/ZyJ0IJvTXVN3PyloGb3kQEyI36fjCZheZj422lAgTWWd6TSHgiImLt3RIaLdv3dQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/credential-provider-env": "^3.972.37", - "@aws-sdk/credential-provider-http": "^3.972.39", - "@aws-sdk/credential-provider-ini": "^3.972.41", - "@aws-sdk/credential-provider-process": "^3.972.37", - "@aws-sdk/credential-provider-sso": "^3.972.41", - "@aws-sdk/credential-provider-web-identity": "^3.972.41", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/credential-provider-imds": "^4.3.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-process": { - "version": "3.972.37", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.37.tgz", - "integrity": "sha512-7nVaHBUaWIddASYfVaA9O4D5ZVjewU3sCol9WqZPGfW0nR+0WqE0xHZnD/U2L33PlOB8KNXGKZ6wOES/QijKzg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-sso": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.972.41.tgz", - "integrity": "sha512-IOWAWEHe5LkjSKkkUUX9ciV6Y1scHTsnfEkdt5yyC4Slrc7AGbkLPrpntjqh18ksJAMOaVhoBsO8p2WyTcY2wQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/token-providers": "3.1048.0", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/credential-provider-web-identity": { - "version": "3.972.41", - "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.41.tgz", - "integrity": "sha512-mbACk9Yypa8nm4iGZLs0PofOXEcTDOUw6wDnsPXNDNSd2WNXs1tSo+6nc/fh0jLYdfVZThhBL98PHW4aXFsG5A==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/eventstream-handler-node": { - "version": "3.972.16", - "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.16.tgz", - "integrity": "sha512-yedpPgKftqjU5SlPFHfqWpOw6xSCRieWRG1euWOlXn4WJxt2VX92VprCa2PpSOXjVCAeK6dTjW9eJRXVig9yGA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-eventstream": { - "version": "3.972.12", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.12.tgz", - "integrity": "sha512-tHTHHCHNrq6XklQvlzHBDJG4Iuhh7NVPRdtmvP+nHFA+5sxPlIDzlAHHgfoYHGvT3NXP1yVP/L5c3opUn6T3Qg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/middleware-websocket": { - "version": "3.972.19", - "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.19.tgz", - "integrity": "sha512-mkEhOGYozqKQkbFaVrjwr0faiwwZza1v5/jSY6Tucm3bD+uKTazIUH/4Yo6aMnQD2ua2W9cMP6s8mvwTcjtqHw==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/nested-clients": { - "version": "3.997.9", - "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.9.tgz", - "integrity": "sha512-jPR3rnmRI4hWYyzfmTGBr7NblMp8QYYeflHXba1H6+7CGrWVqWKQzaXFQ4qbExqPRsXN3T3L3JxFhr6aouXUGQ==", - "license": "Apache-2.0", - "dependencies": { - "@aws-crypto/sha256-browser": "5.2.0", - "@aws-crypto/sha256-js": "5.2.0", - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/signature-v4-multi-region": "^3.996.27", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/fetch-http-handler": "^5.4.2", - "@smithy/node-http-handler": "^4.7.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/signature-v4-multi-region": { - "version": "3.996.27", - "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.27.tgz", - "integrity": "sha512-0Phbz4t6HI3D3skxvG2uI+VWU034/nSIw1T8d+FPzzQG9EQTrw94o9mOKO2Gv3n3Oc8P7JD7RAUxkoneLWv5Eg==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/signature-v4": "^5.4.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/token-providers": { - "version": "3.1048.0", - "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", - "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", - "license": "Apache-2.0", - "dependencies": { - "@aws-sdk/core": "^3.974.11", - "@aws-sdk/nested-clients": "^3.997.9", - "@aws-sdk/types": "^3.973.8", - "@smithy/core": "^3.24.2", - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/types": { - "version": "3.973.8", - "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.973.8.tgz", - "integrity": "sha512-gjlAdtHMbtR9X5iIhVUvbVcy55KnznpC6bkDUWW9z915bi0ckdUr5cjf16Kp6xq0bP5HBD2xzgbL9F9Quv5vUw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/types": "^4.14.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/util-locate-window": { - "version": "3.965.5", - "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.5.tgz", - "integrity": "sha512-WhlJNNINQB+9qtLtZJcpQdgZw3SCDCpXdUJP7cToGwHbCWCnRckGlc6Bx/OhWwIYFNAn+FIydY8SZ0QmVu3xTQ==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws-sdk/xml-builder": { - "version": "3.972.24", - "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.24.tgz", - "integrity": "sha512-V8z5YcDPfsvzrBlj0xR1vhRtocblhYbqdreCJB/voGd4Sr5zjNAeWxexbnqVtskTJe0vFb5KMqbSL++ePl+zRw==", - "license": "Apache-2.0", - "dependencies": { - "@nodable/entities": "2.1.0", - "@smithy/types": "^4.14.1", - "fast-xml-parser": "5.7.3", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=20.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@aws/lambda-invoke-store": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/@aws/lambda-invoke-store/-/lambda-invoke-store-0.2.4.tgz", - "integrity": "sha512-iY8yvjE0y651BixKNPgmv1WrQc+GZ142sb0z4gYnChDDY2YqI4P/jsSopBWrKfAt7LOJAkOXt7rC/hms+WclQQ==", - "license": "Apache-2.0", - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { - "version": "0.81.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.81.1.tgz", - "license": "MIT", - "dependencies": { - "@earendil-works/pi-ai": "^0.81.1", - "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.81.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.81.1.tgz", - "license": "MIT", - "dependencies": { - "@anthropic-ai/sdk": "0.91.1", - "@aws-sdk/client-bedrock-runtime": "3.1048.0", - "@google/genai": "1.52.0", - "@mistralai/mistralai": "2.2.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.81.1", - "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.81.1.tgz", - "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==", - "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==", - "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" - ], - "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==", - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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" - ], - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "license": "MIT" - }, - "node_modules/@earendil-works/pi-coding-agent/node_modules/brace-expansion": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.7.tgz", - "integrity": "sha512-7oFy703dxfY3/NLxC1fh2SUCQ0H9rmAY+5EpDVfXjUTTs+HEwR2nYaqLv+GWcTsumwxPfiz6CzCNkwXwBUwqCA==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "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==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.24.2.tgz", - "integrity": "sha512-thpVCb/rhxE/BnMLQ7GReQLLN8q9qbHmI55F4489/ByVg2aQaQ6kbcLb6FHkocZzQhxc4gx0sCk0tJkKBFzDhA==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.24.2.tgz", - "integrity": "sha512-tmwl4hJkCfNHwFB3nBa8z1Uy3ypZpxqxfTQOcHX+xRByyYgunVbZ9MzUUfb0RxaHIMnbHagwAxuTL+tnNM+1/Q==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.24.2.tgz", - "integrity": "sha512-cNLgeqCqV8WxfcTIOeL4OAtSmL8JjcN6m09XIgro1Wi7cF4t/THaWEa7eL5CMoMBdjoHOTh/vwTO/o2TRXIyzg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.24.2.tgz", - "integrity": "sha512-B6Q0YQDqMx9D7rvIcsXfmJfvUYLoP722bgfBlO5cGvNVb5V/+Y7nhBE3mHV9OpxBf4eAS2S68KZztiPaWq4XYw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "android" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.24.2.tgz", - "integrity": "sha512-kj3AnYWc+CekmZnS5IPu9D+HWtUI49hbnyqk0FLEJDbzCIQt7hg7ucF1SQAilhtYpIujfaHr6O0UHlzzSPdOeA==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.24.2.tgz", - "integrity": "sha512-WeSrmwwHaPkNR5H3yYfowhZcbriGqooyu3zI/3GGpF8AyUdsrrP0X6KumITGA9WOyiJavnGZUwPGvxvwfWPHIA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.24.2.tgz", - "integrity": "sha512-UN8HXjtJ0k/Mj6a9+5u6+2eZ2ERD7Edt1Q9IZiB5UZAIdPnVKDoG7mdTVGhHJIeEml60JteamR3qhsr1r8gXvg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.24.2.tgz", - "integrity": "sha512-TvW7wE/89PYW+IevEJXZ5sF6gJRDY/14hyIGFXdIucxCsbRmLUcjseQu1SyTko+2idmCw94TgyaEZi9HUSOe3Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.24.2.tgz", - "integrity": "sha512-n0WRM/gWIdU29J57hJyUdIsk0WarGd6To0s+Y+LwvlC55wt+GT/OgkwoXCXvIue1i1sSNWblHEig00GBWiJgfA==", - "cpu": [ - "arm" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.24.2.tgz", - "integrity": "sha512-7HnAD6074BW43YvvUmE/35Id9/NB7BeX5EoNkK9obndmZBUk8xmJJeU7DwmUeN7tkysslb2eSl6CTrYz6oEMQg==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.24.2.tgz", - "integrity": "sha512-sfv0tGPQhcZOgTKO3oBE9xpHuUqguHvSo4jl+wjnKwFpapx+vUDcawbwPNuBIAYdRAvIDBfZVvXprIj3HA+Ugw==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.24.2.tgz", - "integrity": "sha512-CN9AZr8kEndGooS35ntToZLTQLHEjtVB5n7dl8ZcTZMonJ7CCfStrYhrzF97eAecqVbVJ7APOEe18RPI4KLhwQ==", - "cpu": [ - "loong64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.24.2.tgz", - "integrity": "sha512-iMkk7qr/wl3exJATwkISxI7kTcmHKE+BlymIAbHO8xanq/TjHaaVThFF6ipWzPHryoFsesNQJPE/3wFJw4+huw==", - "cpu": [ - "mips64el" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.24.2.tgz", - "integrity": "sha512-shsVrgCZ57Vr2L8mm39kO5PPIb+843FStGt7sGGoqiiWYconSxwTiuswC1VJZLCjNiMLAMh34jg4VSEQb+iEbw==", - "cpu": [ - "ppc64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.24.2.tgz", - "integrity": "sha512-4eSFWnU9Hhd68fW16GD0TINewo1L6dRrB+oLNNbYyMUAeOD2yCK5KXGK1GH4qD/kT+bTEXjsyTCiJGHPZ3eM9Q==", - "cpu": [ - "riscv64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.24.2.tgz", - "integrity": "sha512-S0Bh0A53b0YHL2XEXC20bHLuGMOhFDO6GN4b3YjRLK//Ep3ql3erpNcPlEFed93hsQAjAQDNsvcK+hV90FubSw==", - "cpu": [ - "s390x" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/linux-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.24.2.tgz", - "integrity": "sha512-8Qi4nQcCTbLnK9WoMjdC9NiTG6/E38RNICU6sUNqK0QFxCYgoARqVqxdFmWkdonVsvGqWhmm7MO0jyTqLqwj0Q==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.24.2.tgz", - "integrity": "sha512-wuLK/VztRRpMt9zyHSazyCVdCXlpHkKm34WUyinD2lzK07FAHTq0KQvZZlXikNWkDGoT6x3TD51jKQ7gMVpopw==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.24.2.tgz", - "integrity": "sha512-VefFaQUc4FMmJuAxmIHgUmfNiLXY438XrL4GDNV1Y1H/RW3qow68xTwjZKfj/+Plp9NANmzbH5R40Meudu8mmw==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.24.2.tgz", - "integrity": "sha512-YQbi46SBct6iKnszhSvdluqDmxCJA+Pu280Av9WICNwQmMxV7nLRHZfjQzwbPs3jeWnuAhE9Jy0NrnJ12Oz+0A==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.24.2.tgz", - "integrity": "sha512-+iDS6zpNM6EnJyWv0bMGLWSWeXGN/HTaF/LXHXHwejGsVi+ooqDfMCCTerNFxEkM3wYVcExkeGXNqshc9iMaOA==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.24.2.tgz", - "integrity": "sha512-hTdsW27jcktEvpwNHJU4ZwWFGkz2zRJUz8pvddmXPtXDzVKTTINmlmga3ZzwcuMpUvLw7JkLy9QLKyGpD2Yxig==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.24.2.tgz", - "integrity": "sha512-LihEQ2BBKVFLOC9ZItT9iFprsE9tqjDjnbulhHoFxYQtQfai7qfluVODIYxt1PgdoyQkz23+01rzwNwYfutxUQ==", - "cpu": [ - "arm64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.24.2.tgz", - "integrity": "sha512-q+iGUwfs8tncmFC9pcnD5IvRHAzmbwQ3GPS5/ceCyHdjXubwQWI12MKWSNSMYLJMq23/IUCvJMS76PDqXe1fxA==", - "cpu": [ - "ia32" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/win32-x64": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.24.2.tgz", - "integrity": "sha512-7VTgWzgMGvup6aSqDPLiW5zHaxYJGTO4OokMjIlrCtf+VpEL+cXKtCvg723iguPYI5oaUNdS+/V7OU2gvXVWEg==", - "cpu": [ - "x64" - ], - "dev": true, - "license": "MIT", - "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@google/genai": { - "version": "2.12.0", - "resolved": "https://registry.npmjs.org/@google/genai/-/genai-2.12.0.tgz", - "integrity": "sha512-LUr972DZosqPUhf9Mb3CIVu/B99woD3QW6ZJV1T9aNgxaoimAZARmo+IyyDsxIL+zouFiYSdA4hzfEWXc9oNIQ==", - "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.5.0", - "resolved": "https://registry.npmjs.org/@mistralai/mistralai/-/mistralai-2.5.0.tgz", - "integrity": "sha512-S/r6tkiUHblaDJGsb84WS0ePTF2YHVta2EoTi1NJqHNt5mfRRS2uE4v7hCYV134+an+fk6WgG3+aFfinJbQBjQ==", - "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.1", - "resolved": "https://registry.npmjs.org/@opentelemetry/api/-/api-1.9.1.tgz", - "integrity": "sha512-gLyJlPHPZYdAk1JENA9LeHejZe1Ti77/pTeFm/nMXmQH/HFZlcS/O2XJB+L8fkbrNSqhdtlvjBVjxwUYanNH5Q==", - "license": "Apache-2.0", - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/@opentelemetry/api-logs": { - "version": "0.220.0", - "resolved": "https://registry.npmjs.org/@opentelemetry/api-logs/-/api-logs-0.220.0.tgz", - "integrity": "sha512-CmVa4ImJ+ynfrPMNaAXHET6Bhb44SwzmfyVJFq9ni2jgXJR/l7C6gfVFddNmHP+ZOkP9cf4f9DBe68qVLTHc9w==", - "license": "Apache-2.0", - "dependencies": { - "@opentelemetry/api": "^1.3.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==", - "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==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/base64": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/base64/-/base64-1.1.2.tgz", - "integrity": "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/codegen": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@protobufjs/codegen/-/codegen-2.0.5.tgz", - "integrity": "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/eventemitter": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/eventemitter/-/eventemitter-1.1.1.tgz", - "integrity": "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/fetch": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/@protobufjs/fetch/-/fetch-1.1.1.tgz", - "integrity": "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw==", - "license": "BSD-3-Clause", - "dependencies": { - "@protobufjs/aspromise": "^1.1.1" - } - }, - "node_modules/@protobufjs/float": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@protobufjs/float/-/float-1.0.2.tgz", - "integrity": "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/path": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/path/-/path-1.1.2.tgz", - "integrity": "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/pool": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@protobufjs/pool/-/pool-1.1.0.tgz", - "integrity": "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw==", - "license": "BSD-3-Clause" - }, - "node_modules/@protobufjs/utf8": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/@protobufjs/utf8/-/utf8-1.1.2.tgz", - "integrity": "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug==", - "license": "BSD-3-Clause" - }, - "node_modules/@smithy/core": { - "version": "3.29.6", - "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.29.6.tgz", - "integrity": "sha512-TO3w25cdGWBeYqKNDaqH3v4O3jjMPpKwf39YlG5X5xhqWfpOWJbi5gQi1lrllukuwohdhY0TPB8jBEv6UC50Vg==", - "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.11", - "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.4.11.tgz", - "integrity": "sha512-6CUvZwS0tCcVCrcvh2TpwTXxmAkuY6JGNPeKODYRLjHtUUhFLGS3dNkNdRvT/ttJyqimqnhFMTS2nqp4pDZ7oQ==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.29.6", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/fetch-http-handler": { - "version": "5.6.8", - "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.6.8.tgz", - "integrity": "sha512-AFuLou893FesRZeQcKMh87P9x4PF2/ksPOYLLI1ctW7WJxm55SWInFSHAhaNRBPBmbgZyUcCCDKepBX+1jZBBw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.29.6", - "@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==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/node-http-handler": { - "version": "4.9.8", - "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.9.8.tgz", - "integrity": "sha512-ArSSIN4t1wLutcIkHzaL6N11J7xpZK7W3T0pFz9cep9zIpEr9x5+lhJRcVUgObGI3OIMbnROq7w8bwzx+Nkf8A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.29.6", - "@smithy/types": "^4.16.1", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/signature-v4": { - "version": "5.6.7", - "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.6.7.tgz", - "integrity": "sha512-32PmEsuZV9lz7SZk3gJcm+EfIAoIVu83AJyEzgALpwmSqLvuacdAu0fvCVNMbDbegyk1S0lHUDrMWIfR47Micw==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/core": "^3.29.6", - "@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==", - "license": "Apache-2.0", - "dependencies": { - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/@smithy/util-buffer-from": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/@smithy/util-buffer-from/-/util-buffer-from-2.2.0.tgz", - "integrity": "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/is-array-buffer": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@smithy/util-utf8": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/@smithy/util-utf8/-/util-utf8-2.3.0.tgz", - "integrity": "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A==", - "license": "Apache-2.0", - "dependencies": { - "@smithy/util-buffer-from": "^2.2.0", - "tslib": "^2.6.2" - }, - "engines": { - "node": ">=14.0.0" - } - }, - "node_modules/@types/node": { - "version": "26.1.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-26.1.1.tgz", - "integrity": "sha512-nxAkRSVkN1Y0JC1W8ky/fTfkGsMmcrRsbx+3XoZE+rMOX71kLYTV7fLXpqud1GpbpP5TuffXFqfX7fH2GgZREw==", - "license": "MIT", - "dependencies": { - "undici-types": "~8.3.0" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" - }, - "node_modules/agent-base": { - "version": "7.1.4", - "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-7.1.4.tgz", - "integrity": "sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==", - "license": "MIT", - "engines": { - "node": ">= 14" - } - }, - "node_modules/base64-js": { - "version": "1.5.1", - "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", - "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/bignumber.js": { - "version": "9.3.1", - "resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.1.tgz", - "integrity": "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/bowser": { - "version": "2.14.1", - "resolved": "https://registry.npmjs.org/bowser/-/bowser-2.14.1.tgz", - "integrity": "sha512-tzPjzCxygAKWFOJP011oxFHs57HzIhOEracIgAePE4pqB3LikALKnSzUyU4MGs9/iCEUuHlAJTjTc5M+u7YEGg==", - "license": "MIT" - }, - "node_modules/buffer-equal-constant-time": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz", - "integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==", - "license": "BSD-3-Clause" - }, - "node_modules/data-uri-to-buffer": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/data-uri-to-buffer/-/data-uri-to-buffer-4.0.1.tgz", - "integrity": "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "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==", - "license": "Apache-2.0", - "dependencies": { - "safe-buffer": "^5.0.1" - } - }, - "node_modules/esbuild": { - "version": "0.24.2", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.24.2.tgz", - "integrity": "sha512-+9egpBW8I3CD5XPe0n6BfT5fxLzxrlDzqydF3aviG+9ni1lDC/OvMHcxqEFV0+LANZG5R1bFMWfUrjVsdwxJvA==", - "dev": true, - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.24.2", - "@esbuild/android-arm": "0.24.2", - "@esbuild/android-arm64": "0.24.2", - "@esbuild/android-x64": "0.24.2", - "@esbuild/darwin-arm64": "0.24.2", - "@esbuild/darwin-x64": "0.24.2", - "@esbuild/freebsd-arm64": "0.24.2", - "@esbuild/freebsd-x64": "0.24.2", - "@esbuild/linux-arm": "0.24.2", - "@esbuild/linux-arm64": "0.24.2", - "@esbuild/linux-ia32": "0.24.2", - "@esbuild/linux-loong64": "0.24.2", - "@esbuild/linux-mips64el": "0.24.2", - "@esbuild/linux-ppc64": "0.24.2", - "@esbuild/linux-riscv64": "0.24.2", - "@esbuild/linux-s390x": "0.24.2", - "@esbuild/linux-x64": "0.24.2", - "@esbuild/netbsd-arm64": "0.24.2", - "@esbuild/netbsd-x64": "0.24.2", - "@esbuild/openbsd-arm64": "0.24.2", - "@esbuild/openbsd-x64": "0.24.2", - "@esbuild/sunos-x64": "0.24.2", - "@esbuild/win32-arm64": "0.24.2", - "@esbuild/win32-ia32": "0.24.2", - "@esbuild/win32-x64": "0.24.2" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/fetch-blob": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/fetch-blob/-/fetch-blob-3.2.0.tgz", - "integrity": "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "paypal", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "dependencies": { - "node-domexception": "^1.0.0", - "web-streams-polyfill": "^3.0.3" - }, - "engines": { - "node": "^12.20 || >= 14.13" - } - }, - "node_modules/formdata-polyfill": { - "version": "4.0.10", - "resolved": "https://registry.npmjs.org/formdata-polyfill/-/formdata-polyfill-4.0.10.tgz", - "integrity": "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g==", - "license": "MIT", - "dependencies": { - "fetch-blob": "^3.1.2" - }, - "engines": { - "node": ">=12.20.0" - } - }, - "node_modules/gaxios": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.2.0.tgz", - "integrity": "sha512-CUVb4wcYe+771XevyH6HtGmXFAGGKkIC3kswAP8Z1JCe0j80JMaTPZH930DWFrvo0atjh18Arc0pEyUCWa5bfg==", - "license": "Apache-2.0", - "dependencies": { - "extend": "^3.0.2", - "https-proxy-agent": "^7.0.1", - "node-fetch": "^3.3.2" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/gcp-metadata": { - "version": "8.1.2", - "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-8.1.2.tgz", - "integrity": "sha512-zV/5HKTfCeKWnxG0Dmrw51hEWFGfcF2xiXqcA3+J90WDuP0SvoiSO5ORvcBsifmx/FoIjgQN3oNOGaQ5PhLFkg==", - "license": "Apache-2.0", - "dependencies": { - "gaxios": "^7.0.0", - "google-logging-utils": "^1.0.0", - "json-bigint": "^1.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-auth-library": { - "version": "10.9.0", - "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.0.tgz", - "integrity": "sha512-xtvUqvINPhTaBm7nXqlYPcrMHJPm1lCNdSovxnKKhTm+4JsvQ+KGVYJViLoH9Yxu8w+T0Qv5HubzYT9BLrppJg==", - "license": "Apache-2.0", - "dependencies": { - "base64-js": "^1.3.0", - "ecdsa-sig-formatter": "^1.0.11", - "gaxios": "^7.1.4", - "gcp-metadata": "8.1.2", - "google-logging-utils": "1.1.3", - "jws": "^4.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/google-logging-utils": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-1.1.3.tgz", - "integrity": "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA==", - "license": "Apache-2.0", - "engines": { - "node": ">=14" - } - }, - "node_modules/http-proxy-agent": { - "version": "7.0.2", - "resolved": "https://registry.npmjs.org/http-proxy-agent/-/http-proxy-agent-7.0.2.tgz", - "integrity": "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.0", - "debug": "^4.3.4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/https-proxy-agent": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-7.0.6.tgz", - "integrity": "sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==", - "license": "MIT", - "dependencies": { - "agent-base": "^7.1.2", - "debug": "4" - }, - "engines": { - "node": ">= 14" - } - }, - "node_modules/json-bigint": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz", - "integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==", - "license": "MIT", - "dependencies": { - "bignumber.js": "^9.0.0" - } - }, - "node_modules/json-schema-to-ts": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", - "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.18.3", - "ts-algebra": "^2.0.0" - }, - "engines": { - "node": ">=16" - } - }, - "node_modules/jwa": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.1.tgz", - "integrity": "sha512-hRF04fqJIP8Abbkq5NKGN0Bbr3JxlQ+qhZufXVr0DvujKy93ZCbXZMHDL4EOtodSbCWxOqR8MS1tXA5hwqCXDg==", - "license": "MIT", - "dependencies": { - "buffer-equal-constant-time": "^1.0.1", - "ecdsa-sig-formatter": "1.0.11", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/jws": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/jws/-/jws-4.0.1.tgz", - "integrity": "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA==", - "license": "MIT", - "dependencies": { - "jwa": "^2.0.1", - "safe-buffer": "^5.0.1" - } - }, - "node_modules/long": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", - "integrity": "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==", - "license": "Apache-2.0" - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/node-domexception": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/node-domexception/-/node-domexception-1.0.0.tgz", - "integrity": "sha512-/jKZoMpw0F8GRwl4/eLROPA3cfcXtLApP0QzLmUT/HuPCZWyB7IY9ZrMeKw2O/nFIqPQB3PVM9aYm0F312AXDQ==", - "deprecated": "Use your platform's native DOMException instead", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/jimmywarting" - }, - { - "type": "github", - "url": "https://paypal.me/jimmywarting" - } - ], - "license": "MIT", - "engines": { - "node": ">=10.5.0" - } - }, - "node_modules/node-fetch": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-3.3.2.tgz", - "integrity": "sha512-dRB78srN/l6gqWulah9SrxeYnxeddIG30+GOqK/9OlLVyLg3HPnr6SqOWTWOXKRwC2eGYCkZ59NNuSgvSrpgOA==", - "license": "MIT", - "dependencies": { - "data-uri-to-buffer": "^4.0.0", - "fetch-blob": "^3.1.4", - "formdata-polyfill": "^4.0.10" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/node-fetch" - } - }, - "node_modules/openai": { - "version": "6.26.0", - "resolved": "https://registry.npmjs.org/openai/-/openai-6.26.0.tgz", - "integrity": "sha512-zd23dbWTjiJ6sSAX6s0HrCZi41JwTA1bQVs0wLQPZ2/5o2gxOJA5wh7yOAUgwYybfhDXyhwlpeQf7Mlgx8EOCA==", - "license": "Apache-2.0", - "bin": { - "openai": "bin/cli" - }, - "peerDependencies": { - "ws": "^8.18.0", - "zod": "^3.25 || ^4.0" - }, - "peerDependenciesMeta": { - "ws": { - "optional": true - }, - "zod": { - "optional": true - } - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/partial-json": { - "version": "0.1.7", - "resolved": "https://registry.npmjs.org/partial-json/-/partial-json-0.1.7.tgz", - "integrity": "sha512-Njv/59hHaokb/hRUjce3Hdv12wd60MtM9Z5Olmn+nehe0QDAsRtRbJPvJ0Z91TusF0SuZRIvnM+S4l6EIP8leA==", - "license": "MIT" - }, - "node_modules/protobufjs": { - "version": "7.6.5", - "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.5.tgz", - "integrity": "sha512-/FPD0nUc9jH6rfFjji9IBqOz4pcSE3CsT1m7Ep6Mdb0LxSUMj8hgl6GomOvZzpNpAqqGaXA0P3VSrZLFzIhQrw==", - "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==", - "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==", - "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==", - "license": "MIT" - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/typescript": { - "version": "5.7.2", - "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.2.tgz", - "integrity": "sha512-i5t66RHxDvVN40HfDd1PsEThGNnlMCMT3jMUuoh9/0TaqWevNontacunWyN02LA9/fIbEWlcHZcgTKb9QoaLfg==", - "dev": true, - "license": "Apache-2.0", - "bin": { - "tsc": "bin/tsc", - "tsserver": "bin/tsserver" - }, - "engines": { - "node": ">=14.17" - } - }, - "node_modules/undici-types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.3.0.tgz", - "integrity": "sha512-j375ScV60dom+YkPFIfTLcOiPxkN/buHz5GobjLhixFuANaNs3C9l4GmrWqejgXWJ7BbJcFYpTEUkS1Ge8bpZQ==", - "license": "MIT" - }, - "node_modules/web-streams-polyfill": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/web-streams-polyfill/-/web-streams-polyfill-3.3.3.tgz", - "integrity": "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/ws": { - "version": "8.21.1", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.1.tgz", - "integrity": "sha512-+0NTnW77fFN/DjQi6k/Sq/Yvk4Sgajw7urW8V+asjXnRgDs9gyGkdb7EzgfhA4goXsRIZKE28fzIXBHEzhuiWw==", - "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/yaml": { - "version": "2.9.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", - "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", - "license": "ISC", - "bin": { - "yaml": "bin.mjs" - }, - "engines": { - "node": ">= 14.6" - }, - "funding": { - "url": "https://github.com/sponsors/eemeli" - } - }, - "node_modules/zod": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", - "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/colinhacks" - } - }, - "node_modules/zod-to-json-schema": { - "version": "3.25.2", - "resolved": "https://registry.npmjs.org/zod-to-json-schema/-/zod-to-json-schema-3.25.2.tgz", - "integrity": "sha512-O/PgfnpT1xKSDeQYSCfRI5Gy3hPf91mKVDuYLUHZJMiDFptvP41MSnWofm8dnCm0256ZNfZIM7DSzuSMAFnjHA==", - "license": "ISC", - "peerDependencies": { - "zod": "^3.25.28 || ^4" - } - } - } -} diff --git a/js/package.json b/js/package.json deleted file mode 100644 index 10513e8..0000000 --- a/js/package.json +++ /dev/null @@ -1,22 +0,0 @@ -{ - "name": "pocket-pi-guest", - "private": true, - "description": "Build inputs for Pocket Pi's guest bundle. pi's agent core is a real, unmodified upstream dependency here — sync it with `npm update` and rebuild with `node build.mjs`.", - "type": "module", - "scripts": { - "build": "node build.mjs" - }, - "dependencies": { - "@earendil-works/pi-agent-core": "0.81.1", - "@earendil-works/pi-ai": "0.81.1", - "@earendil-works/pi-coding-agent": "0.81.1", - "@google/genai": "^2.12.0", - "@mistralai/mistralai": "^2.5.0", - "@opentelemetry/api": "^1.9.1", - "@opentelemetry/api-logs": "^0.220.0" - }, - "devDependencies": { - "esbuild": "0.24.2", - "typescript": "5.7.2" - } -} diff --git a/js/src/pi-full/driver.ts b/js/src/pi-full/driver.ts deleted file mode 100644 index 036d6ac..0000000 --- a/js/src/pi-full/driver.ts +++ /dev/null @@ -1,144 +0,0 @@ -// Path B session harness: stand up an AgentSession from the UNMODIFIED bundled -// pi-coding-agent and (optionally) run one turn, load an extension, enable tools, -// or persist/resume — all through Pocket Pi's runtime (fetch → __catpiFetchPump → -// Rust HTTP hub → system proxy; our oxc loader for extension .ts; our fs for the -// session store). Loaded as a plain script after pi-full.bundle.js. Results land -// on globalThis for the Rust side to poll. -// -// __piRun(opts) — opts (object or JSON string): -// prompt?: string — send one turn (omit to just build the session) -// extensionPath?: string — absolute path to a .ts/.js extension (our loader) -// tools?: string[] — active tool names (omit → noTools:"all") -// sessionDir?: string — persist the session here (SessionManager.create) -// resume?: bool — resume the most recent session in sessionDir - -(function () { - const P = globalThis.PiFull as import("./entry").PiFullApi; - if (!P) throw new Error("PiFull not loaded — run the bundle first"); - - const MODEL = { - id: "gpt-5.6", - name: "GPT-5.6", - api: "openai-responses", - provider: "openai", - baseUrl: "https://api.openai.com/v1", - reasoning: false, - input: ["text"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 400000, - maxTokens: 8192, - }; - const CWD = "/pocket-pi"; - const AGENT_DIR = "/pocket-pi/.pi"; - - globalThis.__piResult = ""; - globalThis.__piError = null; - globalThis.__piDone = false; - globalThis.__piBind = null; - globalThis.__piLog = []; - - const takeText = (msg) => { - if (!msg) return; - const content = msg.content; - if (Array.isArray(content)) { - let t = ""; - for (const b of content) { - if (!b) continue; - if (b.type === "text" && typeof b.text === "string") t += b.text; - else if (b.type === "error") t += "[ERROR] " + String(b.error || b.text || ""); - } - if (t) globalThis.__piResult = t; - } else if (typeof content === "string") { - globalThis.__piResult = content; - } - }; - - globalThis.__piRun = async function (opts) { - opts = typeof opts === "string" ? (opts.trim().startsWith("{") ? JSON.parse(opts) : { prompt: opts }) : (opts || {}); - try { - // 0.81: model + auth live on a ModelRuntime (in-memory, offline). The - // custom gpt-5.6 model is passed straight to createAgentSession; auth is a - // runtime API-key override for the "openai" provider. - const modelRuntime = await P.ModelRuntime.create({ modelsPath: null }); - if (globalThis.__OPENAI_KEY) await modelRuntime.setRuntimeApiKey("openai", globalThis.__OPENAI_KEY); - - const settingsManager = P.SettingsManager.inMemory({}); - const sessionManager = opts.sessionDir - ? (opts.resume ? P.SessionManager.continueRecent(CWD, opts.sessionDir) : P.SessionManager.create(CWD, opts.sessionDir)) - : P.SessionManager.inMemory(); - - // Load an extension through OUR loader (oxc .ts transpile), not jiti, and - // inject its factory — DefaultResourceLoader wires it via loadExtensionFromFactory. - const extensionFactories = []; - if (opts.extensionPath) { - const mod = await import(opts.extensionPath); - if (typeof mod.default === "function") extensionFactories.push(mod.default); - else throw new Error("extension has no default factory export: " + opts.extensionPath); - } - - const resourceLoader = new P.DefaultResourceLoader({ - cwd: CWD, - agentDir: AGENT_DIR, - settingsManager, - noExtensions: true, - noSkills: true, - noPromptTemplates: true, - noThemes: true, - noContextFiles: true, - extensionFactories, - }); - if (resourceLoader.reload) await resourceLoader.reload(); - - const sessionOpts: Record = { - model: MODEL, - modelRuntime, - settingsManager, - sessionManager, - resourceLoader, - cwd: CWD, - agentDir: AGENT_DIR, - thinkingLevel: "off", - }; - if (opts.tools) sessionOpts.tools = opts.tools; - else sessionOpts.noTools = "all"; - - const { session } = await P.createAgentSession(sessionOpts); - - // Report extension binding into the live session (offline-observable). - // Deliberate introspection into pi internals — cast past the private field. - try { - const runner = (session as any)._extensionRunner; - const regTools = runner && runner.getAllRegisteredTools ? runner.getAllRegisteredTools() : []; - globalThis.__piBind = { - hasAgentStart: !!(runner && runner.hasHandlers && runner.hasHandlers("agent_start")), - registeredTools: regTools.map((t) => t && (t.name || (t.definition && t.definition.name))).filter(Boolean), - }; - } catch (e) { - globalThis.__piBind = { error: String(e) }; - } - - session.subscribe((event) => { - try { - if (!event) return; - globalThis.__piLastEvent = event.type; - if (event.type === "message_update" || event.type === "message_end") { - // Error fields live on the assistant message (stopReason "error") — - // pi has no separate "error" event — and aren't on the base message - // union, so read them through a cast. - const msg = event.message as any; - if (msg && msg.role !== "user") takeText(msg); - if (msg && msg.stopReason === "error" && msg.errorMessage) { - globalThis.__piError = String(msg.errorMessage); - } - } - } catch {} - }); - - if (opts.prompt) await session.prompt(opts.prompt); - globalThis.__piDone = true; - } catch (e) { - globalThis.__piError = String((e && e.stack) || e); - globalThis.__piDone = true; - } - }; -})(); diff --git a/js/src/pi-full/entry.ts b/js/src/pi-full/entry.ts deleted file mode 100644 index 3afa021..0000000 --- a/js/src/pi-full/entry.ts +++ /dev/null @@ -1,48 +0,0 @@ -// Bundle entry for the unmodified pi-coding-agent core. -import { - createAgentSession, - SessionManager, - SettingsManager, - ModelRegistry, - DefaultResourceLoader, - createExtensionRuntime, - createEventBus, -} from "@earendil-works/pi-coding-agent"; -// A few symbols aren't on the top-level barrel (the package's exports map only -// exposes "." and "./hooks"). Reach them by file path — esbuild dedupes to the -// same bundled module, so pi stays unmodified. This is what lets Pocket Pi wire -// model/auth/extensions headlessly. -import { ModelRuntime } from "../../node_modules/@earendil-works/pi-coding-agent/dist/core/model-runtime.js"; -import { - AuthStorage, - InMemoryAuthStorageBackend, -} from "../../node_modules/@earendil-works/pi-coding-agent/dist/core/auth-storage.js"; -import { loadExtensionFromFactory } from "../../node_modules/@earendil-works/pi-coding-agent/dist/core/extensions/loader.js"; -import { - fauxAssistantMessage, - fauxProvider, -} from "../../node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai/dist/providers/faux.js"; - -const PiFull = { - createAgentSession, - SessionManager, - SettingsManager, - ModelRegistry, - ModelRuntime, - AuthStorage, - InMemoryAuthStorageBackend, - DefaultResourceLoader, - createExtensionRuntime, - createEventBus, - loadExtensionFromFactory, - fauxAssistantMessage, - fauxProvider, -}; -globalThis.PiFull = PiFull; -globalThis.__piFullLoaded = typeof createAgentSession === "function"; - -// The exact shape of globalThis.PiFull, carrying pi's REAL types. The harness -// (loaded as a separate script after this bundle) casts globalThis.PiFull to -// this, so its session/model/auth calls are typechecked against pi's actual API -// — a pi bump that changes those signatures fails `tsc`, not just a runtime test. -export type PiFullApi = typeof PiFull; diff --git a/js/src/pi-full/example-extension.ts b/js/src/pi-full/example-extension.ts deleted file mode 100644 index ecec491..0000000 --- a/js/src/pi-full/example-extension.ts +++ /dev/null @@ -1,42 +0,0 @@ -// A minimal pi extension in its normal, unmodified shape — a default-exported -// factory that receives the extension `pi` API and registers a tool and a -// lifecycle hook. Pocket Pi loads this through its OWN oxc TypeScript loader (the -// `.ts` is transpiled natively, no jiti), proving extension compatibility. - -interface Pi { - registerTool(tool: { - name: string; - description: string; - parameters: unknown; - execute: ( - toolCallId: string, - input: Record, - signal?: unknown, - onUpdate?: unknown, - ctx?: unknown, - ) => Promise; - }): void; - on(event: string, handler: (event: unknown) => void): void; -} - -export default (pi: Pi): void => { - pi.registerTool({ - name: "echo", - description: "Echo the given text back to the caller.", - parameters: { - type: "object", - properties: { text: { type: "string" } }, - required: ["text"], - }, - // pi calls execute(toolCallId, input, ...); the result must carry a `content` - // array of blocks (the shape pi converts into a tool-result message). - execute: async (_toolCallId, input): Promise => { - (globalThis as Record).__echoCalled = input; - return { content: [{ type: "text", text: String(input?.text ?? "") }] }; - }, - }); - - pi.on("agent_start", (): void => { - (globalThis as Record).__extAgentStartFired = true; - }); -}; diff --git a/js/src/pi-full/ext-probe.ts b/js/src/pi-full/ext-probe.ts deleted file mode 100644 index b9d7519..0000000 --- a/js/src/pi-full/ext-probe.ts +++ /dev/null @@ -1,34 +0,0 @@ -// Load a real pi extension through Pocket Pi's own module loader (oxc `.ts` -// transpile — NOT jiti), then hand its default-export factory to pi's unmodified -// loadExtensionFromFactory. Proves an unmodified extension registers its tools -// and hooks under Pocket Pi. Results land on globalThis for the Rust side. - -globalThis.__piExtResult = null; -globalThis.__piExtError = null; -globalThis.__piExtDone = false; - -globalThis.__piLoadExtension = async function (extPath) { - try { - const P = globalThis.PiFull as import("./entry").PiFullApi; - if (!P) throw new Error("PiFull not loaded — run the bundle first"); - - // Dynamic import routes through Pocket Pi's NodeResolver/NodeLoader, which - // transpiles the .ts natively and returns the factory as the default export. - const mod = await import(extPath); - const factory = mod.default; - if (typeof factory !== "function") throw new Error("extension has no default factory export"); - - const runtime = P.createExtensionRuntime(); - const eventBus = P.createEventBus(); - const ext = await P.loadExtensionFromFactory(factory, "/pocket-pi", eventBus, runtime, extPath); - - globalThis.__piExtResult = { - tools: [...ext.tools.keys()], - handlers: [...ext.handlers.keys()], - }; - } catch (e) { - globalThis.__piExtError = String((e && e.stack) || e); - } finally { - globalThis.__piExtDone = true; - } -}; diff --git a/js/src/pi-full/host.ts b/js/src/pi-full/host.ts deleted file mode 100644 index 63791d7..0000000 --- a/js/src/pi-full/host.ts +++ /dev/null @@ -1,200 +0,0 @@ -// Pocket Pi host harness. Reimplements the `PocketPi.boot/prompt/abort` surface -// — the API a Rust host (register_tool + boot + prompt + on_event + pump) drives -// — on top of the full, unmodified pi-coding-agent. Native tools bridge through -// `host.tool`; agent events map to the host's compact vocabulary via `host.emit`. -// -// This is loaded once by PiRuntime::new(), right after the full pi bundle. -import type { PiFullApi } from "./entry"; - -const P = globalThis.PiFull as PiFullApi; - -const CWD = "/pocket-pi"; -const AGENT_DIR = "/pocket-pi/.pi"; -const state: { session: any } = { session: null }; - -const emit = (o: unknown) => globalThis.host.emit(JSON.stringify(o)); - -// Build a model descriptor from the boot config for the requested provider. -function buildModel(cfg: any, provider: string): any { - const base = { - id: cfg.model, - name: cfg.model, - provider, - reasoning: false, - input: ["text", "image"], - cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, - contextWindow: 400000, - maxTokens: cfg.maxTokens || 4096, - }; - if (provider === "anthropic") { - return { ...base, api: "anthropic-messages", baseUrl: "https://api.anthropic.com" }; - } - return { ...base, api: "openai-responses", baseUrl: "https://api.openai.com/v1" }; -} - -// A pi extension that exposes each host-registered native tool (name + -// description + JSON-schema params from the boot config) as a pi tool whose -// execute() calls back into native Rust via host.tool. -function nativeToolsExtension(cfg: any) { - return (pi: any) => { - for (const t of cfg.tools || []) { - pi.registerTool({ - name: t.name, - description: t.description || "", - parameters: t.parameters || { type: "object", properties: {} }, - execute: async (_id: string, input: any) => { - const res = JSON.parse(globalThis.host.tool(t.name, JSON.stringify(input || {}))); - const content: any[] = []; - if (res.text) content.push({ type: "text", text: res.text }); - if (res.image_base64) { - content.push({ type: "image", data: res.image_base64, mimeType: res.mime_type || "image/jpeg" }); - } - if (content.length === 0) content.push({ type: "text", text: "" }); - return { content, details: res.terminate ? { terminate: true } : undefined }; - }, - }); - } - }; -} - -// Turn a message's content blocks into plain text. -function textOf(msg: any): string { - const c = msg && msg.content; - if (typeof c === "string") return c; - if (Array.isArray(c)) return c.filter((b: any) => b && b.type === "text").map((b: any) => b.text).join(""); - return ""; -} - -async function boot(configJson: string): Promise { - globalThis.__ppBooted = false; - try { - const cfg = JSON.parse(configJson); - let provider = cfg.provider || (String(cfg.model || "").startsWith("gpt") ? "openai" : "anthropic"); - let model: any; - let nativeProvider: any; - if (cfg.scripted) { - const modelId = cfg.model || "offline"; - const faux = P.fauxProvider({ - api: "pocket-pi-offline", - provider: "offline", - models: [{ id: modelId, name: "Pocket Pi Offline" }], - }); - faux.setResponses( - (cfg.scripted.steps || []).map((step: any) => - P.fauxAssistantMessage(typeof step === "string" ? step : String(step.text || "")), - ), - ); - nativeProvider = faux.provider; - provider = "offline"; - model = faux.getModel(modelId); - if (!model) throw new Error(`offline model not found: ${modelId}`); - } else { - model = buildModel(cfg, provider); - } - - const modelRuntime = await P.ModelRuntime.create({ modelsPath: null } as any); - if (nativeProvider) modelRuntime.registerNativeProvider(nativeProvider); - if (cfg.apiKey) await modelRuntime.setRuntimeApiKey(provider, cfg.apiKey); - - const settingsManager = P.SettingsManager.inMemory({}); - const sessionManager = P.SessionManager.inMemory(); - const resourceLoader = new P.DefaultResourceLoader({ - cwd: CWD, - agentDir: AGENT_DIR, - settingsManager, - noExtensions: true, - noSkills: true, - noPromptTemplates: true, - noThemes: true, - noContextFiles: true, - extensionFactories: [nativeToolsExtension(cfg)], - } as any); - if (resourceLoader.reload) await resourceLoader.reload(); - - const { session } = await P.createAgentSession({ - model, - modelRuntime, - settingsManager, - sessionManager, - resourceLoader, - cwd: CWD, - agentDir: AGENT_DIR, - thinkingLevel: "off", - tools: (cfg.tools || []).map((t: any) => t.name), - } as any); - - // Custom system prompt (base prompt the agent starts each turn with). - if (cfg.systemPrompt) { - try { - (session as any)._baseSystemPrompt = cfg.systemPrompt; - (session as any).agent.state.systemPrompt = cfg.systemPrompt; - } catch {} - } - - // Stream agent events to the host in its compact vocabulary. - let lastText = ""; - session.subscribe((event: any) => { - try { - switch (event.type) { - case "agent_start": - lastText = ""; - emit({ kind: "start" }); - break; - case "tool_execution_start": - emit({ kind: "tool_start", name: event.toolName }); - break; - case "message_update": - case "message_end": { - const msg = event.message; - if (msg && msg.role !== "user") { - const text = textOf(msg); - if (text && text.length > lastText.length && text.startsWith(lastText)) { - emit({ kind: "text", delta: text.slice(lastText.length) }); - lastText = text; - } else if (text && text !== lastText) { - emit({ kind: "assistant_text", text }); - lastText = text; - } - if (msg.stopReason === "error" && msg.errorMessage) { - emit({ kind: "error", message: msg.errorMessage }); - } - } - break; - } - case "agent_settled": - emit({ kind: "end" }); - break; - } - } catch {} - }); - - state.session = session; - } catch (e) { - emit({ kind: "error", message: String((e as any)?.stack || e) }); - } finally { - globalThis.__ppBooted = true; - } -} - -async function prompt(text: string): Promise { - const s = state.session; - if (!s) { - emit({ kind: "error", message: "prompt before boot completed" }); - emit({ kind: "end" }); - return; - } - try { - await s.prompt(text); - } catch (e) { - emit({ kind: "error", message: String((e as any)?.stack || e) }); - emit({ kind: "end" }); - } -} - -function abort(): void { - try { - state.session?.abort?.(); - } catch {} -} - -(globalThis as any).PocketPi = { boot, prompt, abort }; diff --git a/js/src/pi-full/persist-probe.ts b/js/src/pi-full/persist-probe.ts deleted file mode 100644 index c6008ea..0000000 --- a/js/src/pi-full/persist-probe.ts +++ /dev/null @@ -1,42 +0,0 @@ -// Session persistence (M7): write a session to disk with pi's unmodified -// SessionManager (backed by Pocket Pi's fs builtin), then resume it in a fresh -// manager over the same directory and confirm the history round-trips. Offline — -// no LLM, just the disk store. Results land on globalThis for the Rust side. - -globalThis.__piPersistResult = null; -globalThis.__piPersistError = null; - -globalThis.__piPersist = function (sessionDir) { - try { - const P = globalThis.PiFull as import("./entry").PiFullApi; - if (!P) throw new Error("PiFull not loaded — run the bundle first"); - const CWD = "/pocket-pi"; - - // Write a couple of messages through a persistent SessionManager. These are - // minimal test messages (not the full pi Message shape) — cast to pass them. - const sm1 = P.SessionManager.create(CWD, sessionDir); - sm1.appendMessage({ role: "user", content: [{ type: "text", text: "remember the number 42" }] } as any); - sm1.appendMessage({ role: "assistant", content: [{ type: "text", text: "noted: 42" }] } as any); - const wrote = sm1.buildSessionContext().messages.length; - const sessionId = sm1.getSessionId(); - - // Resume in a fresh manager over the same dir (exercises the header peek + - // full-file read path through our fs). - const sm2 = P.SessionManager.continueRecent(CWD, sessionDir); - const ctx = sm2.buildSessionContext(); - const texts = ctx.messages.map((m) => { - const content = (m as any).content; - return Array.isArray(content) ? content.map((c) => (c && c.text) || "").join("") : String(content); - }); - - globalThis.__piPersistResult = { - sessionId, - wrote, - resumedCount: ctx.messages.length, - resumedId: sm2.getSessionId(), - texts, - }; - } catch (e) { - globalThis.__piPersistError = String((e && e.stack) || e); - } -}; diff --git a/js/src/pi-full/undici-stub.ts b/js/src/pi-full/undici-stub.ts deleted file mode 100644 index 66b2416..0000000 --- a/js/src/pi-full/undici-stub.ts +++ /dev/null @@ -1,46 +0,0 @@ -// Build-time stub for `undici`. Pocket Pi routes all HTTP through its native, -// proxy-aware streaming fetch (the runtime's one real transport op), so pi's -// undici-backed transport is never used. pi only reaches for undici when an HTTP -// *proxy is configured in its own settings* — which Pocket Pi never does (the -// native hub handles proxying). This stub exists only so `import * as undici` -// loads without dragging in undici's entire web-fetch/websocket/cache stack. -// -// This substitutes a transitive transport dependency, not pi's logic. Pi's own -// source remains unmodified. - -class NoopDispatcher { - dispatch() { return false; } - close() { return Promise.resolve(); } - destroy() { return Promise.resolve(); } - compose() { return this; } - on() { return this; } - once() { return this; } - off() { return this; } -} -export class Client extends NoopDispatcher {} -export class Pool extends NoopDispatcher {} -export class BalancedPool extends NoopDispatcher {} -export class Agent extends NoopDispatcher {} -export class ProxyAgent extends NoopDispatcher {} -export class EnvHttpProxyAgent extends NoopDispatcher {} -export class MockAgent extends NoopDispatcher {} -export class RetryAgent extends NoopDispatcher {} - -let globalDispatcher = new Agent(); -export function setGlobalDispatcher(d) { globalDispatcher = d; } -export function getGlobalDispatcher() { return globalDispatcher; } -export function install() {} // pi calls this optionally; keep our globals in place -export function fetch(...args) { return globalThis.fetch(...args); } - -export const Headers = globalThis.Headers; -export const Response = globalThis.Response; -export const Request = globalThis.Request; -export const FormData = globalThis.FormData; -export const interceptors = {}; -export const errors = {}; - -export default { - Client, Pool, BalancedPool, Agent, ProxyAgent, EnvHttpProxyAgent, MockAgent, RetryAgent, - setGlobalDispatcher, getGlobalDispatcher, install, fetch, - Headers, Response, Request, FormData, interceptors, errors, -}; diff --git a/js/src/runtime/node/_bootstrap.ts b/js/src/runtime/node/_bootstrap.ts deleted file mode 100644 index 4d7db37..0000000 --- a/js/src/runtime/node/_bootstrap.ts +++ /dev/null @@ -1,9 +0,0 @@ -// Node runtime bootstrap — evaluated once when the Node layer is installed, -// before any guest module loads. Hoists Buffer to a global (many packages assume -// `Buffer` exists without importing it) and wires `process.nextTick` onto the -// microtask queue. Kept as a file (not a Rust string literal) so it stays -// readable and lintable. -import { Buffer } from "node:buffer"; -globalThis.Buffer = Buffer; -globalThis.__nodeBuffer = Buffer; -globalThis.process.nextTick = (fn, ...args) => queueMicrotask(() => fn(...args)); diff --git a/js/src/runtime/node/_cjs-runtime.ts b/js/src/runtime/node/_cjs-runtime.ts deleted file mode 100644 index 99078bc..0000000 --- a/js/src/runtime/node/_cjs-runtime.ts +++ /dev/null @@ -1,49 +0,0 @@ -// Synchronous CommonJS `require`, over the native resolver + file reader. This is -// the static tail of the CJS bootstrap: the generated head (see -// builtins::cjs_bootstrap_source) has already populated globalThis.__builtinExports -// from the builtin registry, so this file only implements the require semantics. -// Kept separate from the generated part so the logic is readable and lintable. - -globalThis.__cjsCache = globalThis.__cjsCache || new Map(); - -// Resolve + load a CommonJS (or JSON, or transpiled-TS) module synchronously. -// `fromFile` is the requiring module's path; `spec` is the import specifier. -globalThis.__cjsRequire = function (fromFile, spec) { - const r = JSON.parse(globalThis.__node.resolve(fromFile, spec)); - - // A node: builtin — served straight from the registry-populated map. - if (r.builtin != null) { - const exports = - globalThis.__builtinExports[r.builtin] ?? - globalThis.__builtinExports[r.builtin.split("/")[0]]; - if (exports === undefined) throw new Error("builtin not available: " + r.builtin); - return exports; - } - if (r.err) throw new Error("Cannot find module '" + spec + "' from '" + fromFile + "'"); - - const p = r.path; - if (globalThis.__cjsCache.has(p)) return globalThis.__cjsCache.get(p); - - // JSON modules resolve to their parsed value. - if (p.endsWith(".json")) { - const val = JSON.parse(globalThis.__node.readText(p)); - globalThis.__cjsCache.set(p, val); - return val; - } - - let src = globalThis.__node.readText(p); - if (p.endsWith(".ts") || p.endsWith(".cts")) src = host.transpile(p, src); - - const module = { exports: {} }; - // Seed the cache before running the body so require cycles terminate. - globalThis.__cjsCache.set(p, module.exports); - const dir = p.replace(/\/[^/]*$/, ""); - const fn = new Function("module", "exports", "require", "__filename", "__dirname", src); - fn(module, module.exports, (s) => globalThis.__cjsRequire(p, s), p, dir); - globalThis.__cjsCache.set(p, module.exports); - return module.exports; -}; - -// esbuild-bundled CJS emits a runtime `require(...)` for anything left external -// (our node: builtins); delegate it to the synchronous require above. -globalThis.require = (spec) => globalThis.__cjsRequire("/pocket-pi-bundle", spec); diff --git a/js/src/runtime/node/assert.ts b/js/src/runtime/node/assert.ts deleted file mode 100644 index a5e097d..0000000 --- a/js/src/runtime/node/assert.ts +++ /dev/null @@ -1,11 +0,0 @@ -function assert(v, msg) { if (!v) throw new Error(msg || "Assertion failed"); } -assert.ok = assert; -assert.equal = (a, b, m) => { if (a != b) throw new Error(m || a + " != " + b); }; -assert.strictEqual = (a, b, m) => { if (a !== b) throw new Error(m || a + " !== " + b); }; -assert.deepEqual = (a, b, m) => { if (JSON.stringify(a) !== JSON.stringify(b)) throw new Error(m || "deepEqual failed"); }; -assert.deepStrictEqual = assert.deepEqual; -assert.notEqual = (a, b, m) => { if (a == b) throw new Error(m || "notEqual failed"); }; -assert.throws = (fn, m) => { try { fn(); } catch { return; } throw new Error(m || "expected throw"); }; -assert.fail = (m) => { throw new Error(m || "fail"); }; -export default assert; -export { assert }; diff --git a/js/src/runtime/node/async_hooks.ts b/js/src/runtime/node/async_hooks.ts deleted file mode 100644 index 5a94760..0000000 --- a/js/src/runtime/node/async_hooks.ts +++ /dev/null @@ -1,24 +0,0 @@ -// node:async_hooks — single-threaded, synchronous stubs. AsyncLocalStorage runs -// callbacks inline; AsyncResource is a real (no-op) base class so libraries that -// `class X extends AsyncResource {}` (e.g. undici) load and construct. -export class AsyncLocalStorage { - run(_store, cb, ...args) { return cb(...args); } - getStore() { return this._store; } - enterWith(store) { this._store = store; } - exit(cb, ...args) { return cb(...args); } - disable() {} -} -export class AsyncResource { - constructor(type, opts) { this.type = type; this._opts = opts; } - runInAsyncScope(fn, thisArg, ...args) { return fn.apply(thisArg, args); } - emitDestroy() { return this; } - asyncId() { return 0; } - triggerAsyncId() { return 0; } - bind(fn) { return fn; } - static bind(fn) { return fn; } -} -export function createHook() { return { enable() { return this; }, disable() { return this; } }; } -export function executionAsyncId() { return 0; } -export function triggerAsyncId() { return 0; } -export function executionAsyncResource() { return {}; } -export default { AsyncLocalStorage, AsyncResource, createHook, executionAsyncId, triggerAsyncId, executionAsyncResource }; diff --git a/js/src/runtime/node/buffer.ts b/js/src/runtime/node/buffer.ts deleted file mode 100644 index 99999a5..0000000 --- a/js/src/runtime/node/buffer.ts +++ /dev/null @@ -1,112 +0,0 @@ -// node:buffer — a functional Buffer subset over Uint8Array. Covers the encodings -// pi + its deps actually touch (utf8, base64, hex, latin1) and the common ops. -const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - -function utf8ToBytes(str) { - const out = []; - for (let i = 0; i < str.length; i++) { - let c = str.charCodeAt(i); - if (c < 0x80) out.push(c); - else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); - else if (c >= 0xd800 && c <= 0xdbff) { - const c2 = str.charCodeAt(++i); - c = 0x10000 + ((c & 0x3ff) << 10) + (c2 & 0x3ff); - out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); - } else out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); - } - return out; -} -function bytesToUtf8(bytes) { - let out = "", i = 0; - while (i < bytes.length) { - let c = bytes[i++]; - if (c < 0x80) out += String.fromCharCode(c); - else if (c >= 0xc0 && c < 0xe0) out += String.fromCharCode(((c & 0x1f) << 6) | (bytes[i++] & 0x3f)); - else if (c >= 0xe0 && c < 0xf0) out += String.fromCharCode(((c & 0xf) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f)); - else { - const cp = ((c & 7) << 18) | ((bytes[i++] & 0x3f) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f); - const off = cp - 0x10000; - out += String.fromCharCode(0xd800 + (off >> 10), 0xdc00 + (off & 0x3ff)); - } - } - return out; -} -function toBase64(bytes) { - let out = ""; - for (let i = 0; i < bytes.length; i += 3) { - const b0 = bytes[i], b1 = bytes[i + 1], b2 = bytes[i + 2]; - const n = (b0 << 16) | ((b1 || 0) << 8) | (b2 || 0); - out += B64[(n >> 18) & 63] + B64[(n >> 12) & 63] + (i + 1 < bytes.length ? B64[(n >> 6) & 63] : "=") + (i + 2 < bytes.length ? B64[n & 63] : "="); - } - return out; -} -function fromBase64(str) { - str = str.replace(/[^A-Za-z0-9+/]/g, ""); - const out = []; - for (let i = 0; i < str.length; i += 4) { - const n = (B64.indexOf(str[i]) << 18) | (B64.indexOf(str[i + 1]) << 12) | (B64.indexOf(str[i + 2] || "A") << 6) | B64.indexOf(str[i + 3] || "A"); - out.push((n >> 16) & 255); - if (str[i + 2] && str[i + 2] !== "=") out.push((n >> 8) & 255); - if (str[i + 3] && str[i + 3] !== "=") out.push(n & 255); - } - return out; -} - -export class Buffer extends Uint8Array { - static from(value, encoding) { - if (typeof value === "string") { - if (encoding === "base64") return new Buffer(fromBase64(value)); - if (encoding === "hex") { - const out = []; - for (let i = 0; i < value.length; i += 2) out.push(parseInt(value.substr(i, 2), 16)); - return new Buffer(out); - } - if (encoding === "latin1" || encoding === "binary") { - const out = new Buffer(value.length); - for (let i = 0; i < value.length; i++) out[i] = value.charCodeAt(i) & 255; - return out; - } - return new Buffer(utf8ToBytes(value)); - } - if (value instanceof Uint8Array || Array.isArray(value)) return new Buffer(value); - if (value instanceof ArrayBuffer) return new Buffer(new Uint8Array(value)); - return new Buffer(0); - } - static alloc(size, fill) { - const b = new Buffer(size); - if (fill != null) b.fill(typeof fill === "string" ? fill.charCodeAt(0) : fill); - return b; - } - static allocUnsafe(size) { return new Buffer(size); } - static isBuffer(v) { return v instanceof Buffer; } - static concat(list, length) { - let total = length ?? list.reduce((n, b) => n + b.length, 0); - const out = new Buffer(total); - let off = 0; - for (const b of list) { out.set(b.subarray(0, Math.min(b.length, total - off)), off); off += b.length; if (off >= total) break; } - return out; - } - static byteLength(str, encoding) { - return Buffer.from(str, encoding).length; - } - toString(encoding, start, end) { - const view = this.subarray(start || 0, end == null ? this.length : end); - if (encoding === "base64") return toBase64(view); - if (encoding === "hex") return Array.from(view, (b) => b.toString(16).padStart(2, "0")).join(""); - if (encoding === "latin1" || encoding === "binary") return Array.from(view, (b) => String.fromCharCode(b)).join(""); - return bytesToUtf8(view); - } - toJSON() { return { type: "Buffer", data: Array.from(this) }; } - equals(other) { return this.length === other.length && this.every((v, i) => v === other[i]); } - write(str, offset = 0, length, encoding) { - const src = Buffer.from(str, typeof length === "string" ? length : encoding); - const n = Math.min(src.length, this.length - offset, typeof length === "number" ? length : Infinity); - this.set(src.subarray(0, n), offset); - return n; - } - slice(a, b) { return new Buffer(this.subarray(a, b)); } -} - -export const SlowBuffer = Buffer; -export const constants = { MAX_LENGTH: 0x7fffffff, MAX_STRING_LENGTH: 0x1fffffff }; -export default { Buffer, SlowBuffer, constants }; diff --git a/js/src/runtime/node/child_process.ts b/js/src/runtime/node/child_process.ts deleted file mode 100644 index 4a372ee..0000000 --- a/js/src/runtime/node/child_process.ts +++ /dev/null @@ -1,58 +0,0 @@ -// node:child_process — spawnSync/execSync real (native), async forms best-effort. -import { EventEmitter } from "node:events"; -const n = globalThis.__node; -export function spawnSync(cmd, args = [], options = {}) { - const res = JSON.parse(n.spawnSync(String(cmd), JSON.stringify(args || []), JSON.stringify(options || {}))); - const enc = options.encoding; - const wrap = (s) => (enc && enc !== "buffer" ? s : globalThis.Buffer.from(s)); - return { - status: res.status ?? null, - signal: null, - stdout: wrap(res.stdout || ""), - stderr: wrap(res.stderr || ""), - error: res.error ? new Error(res.error) : undefined, - pid: 0, - }; -} -export function execSync(command, options = {}) { - const shell = options.shell || "/bin/sh"; - const res = spawnSync(shell, ["-c", String(command)], options); - if (res.status && res.status !== 0) { - const e = new Error(`Command failed: ${command}\n${res.stderr}`); - e.status = res.status; - e.stdout = res.stdout; - e.stderr = res.stderr; - throw e; - } - return res.stdout; -} -export function execFileSync(file, args = [], options = {}) { - return spawnSync(file, args, options).stdout; -} -// Async forms: minimal EventEmitter-ish; adequate for code that imports but -// rarely runs them on the session path. (Real async spawn is a later milestone.) -export function spawn(cmd, args = [], options = {}) { - const ee = new EventEmitter(); - queueMicrotask(() => { - try { - const r = spawnSync(cmd, args, options); - ee.stdout && ee.stdout.emit && ee.stdout.emit("data", r.stdout); - ee.emit("close", r.status ?? 0); - } catch (e) { ee.emit("error", e); } - }); - ee.stdout = { on() {} }; - ee.stderr = { on() {} }; - ee.stdin = { write() {}, end() {} }; - return ee; -} -export function exec(command, options, cb) { - if (typeof options === "function") { cb = options; options = {}; } - queueMicrotask(() => { - try { const out = execSync(command, options); cb && cb(null, out, ""); } - catch (e) { cb && cb(e, e.stdout || "", e.stderr || ""); } - }); - return { on() {} }; -} -export const execFile = exec; -export function fork() { throw new Error("child_process.fork is not supported in Pocket Pi"); } -export default { spawnSync, execSync, execFileSync, spawn, exec, execFile, fork }; diff --git a/js/src/runtime/node/console.ts b/js/src/runtime/node/console.ts deleted file mode 100644 index 8890c47..0000000 --- a/js/src/runtime/node/console.ts +++ /dev/null @@ -1,35 +0,0 @@ -// node:console — wraps QuickJS's global console. The `Console` class lets code -// construct its own logger (undici's mock formatter does `new Console(...)`). -const g = globalThis.console || {}; -const out = (...a) => { try { (g.log || (() => {}))(...a); } catch {} }; -const err = (...a) => { try { (g.error || g.log || (() => {}))(...a); } catch {} }; - -export class Console { - constructor(_stdout, _stderr) {} - log(...a) { out(...a); } - info(...a) { out(...a); } - debug(...a) { out(...a); } - dir(...a) { out(...a); } - warn(...a) { err(...a); } - error(...a) { err(...a); } - trace(...a) { err(...a); } - table(...a) { out(...a); } - group(...a) { out(...a); } - groupCollapsed(...a) { out(...a); } - groupEnd() {} - assert(cond, ...a) { if (!cond) err("Assertion failed:", ...a); } - count() {} - countReset() {} - time() {} - timeEnd() {} - timeLog() {} - clear() {} -} - -const instance = new Console(); -export const log = instance.log; -export const info = instance.info; -export const warn = instance.warn; -export const error = instance.error; -export const debug = instance.debug; -export default instance; diff --git a/js/src/runtime/node/constants.ts b/js/src/runtime/node/constants.ts deleted file mode 100644 index c6be336..0000000 --- a/js/src/runtime/node/constants.ts +++ /dev/null @@ -1,2 +0,0 @@ -// node:constants — minimal stub. -export default {}; diff --git a/js/src/runtime/node/crypto.ts b/js/src/runtime/node/crypto.ts deleted file mode 100644 index 90cbb08..0000000 --- a/js/src/runtime/node/crypto.ts +++ /dev/null @@ -1,42 +0,0 @@ -// node:crypto — the subset pi touches. Digests use a fast non-cryptographic hash -// (FNV-1a) — fine for cache keys / ids; NOT for security. -function fnv(str) { - let h = 0xcbf29ce484222325n; - const bytes = globalThis.Buffer ? globalThis.Buffer.from(str) : new TextEncoder().encode(str); - for (const b of bytes) { h ^= BigInt(b); h = (h * 0x100000001b3n) & 0xffffffffffffffffn; } - return h; -} -export function randomBytes(n) { - const b = globalThis.Buffer ? globalThis.Buffer.alloc(n) : new Uint8Array(n); - for (let i = 0; i < n; i++) b[i] = Math.floor(Math.random() * 256); - return b; -} -export function randomUUID() { return globalThis.crypto.randomUUID(); } -export function randomFillSync(buf) { for (let i = 0; i < buf.length; i++) buf[i] = Math.floor(Math.random() * 256); return buf; } -export function createHash(_algo) { - let data = ""; - return { - update(chunk) { data += typeof chunk === "string" ? chunk : globalThis.Buffer.from(chunk).toString(); return this; }, - digest(enc) { - // 256-bit-ish by concatenating four FNV rounds with salts. - let hex = ""; - for (let i = 0; i < 4; i++) hex += fnv(i + ":" + data).toString(16).padStart(16, "0"); - if (enc === "hex") return hex; - const bytes = globalThis.Buffer.from(hex, "hex"); - return enc ? bytes.toString(enc) : bytes; - }, - }; -} -export function createHmac(algo, key) { const h = createHash(algo); h.update(String(key) + ":"); return h; } -export function getHashes() { return ["sha1", "sha256", "sha384", "sha512", "md5"]; } -export function getCiphers() { return []; } -export function timingSafeEqual(a, b) { - if (!a || !b || a.length !== b.length) return false; - let diff = 0; - for (let i = 0; i < a.length; i++) diff |= a[i] ^ b[i]; - return diff === 0; -} -export const constants = {}; -export const webcrypto = globalThis.crypto; -export function getRandomValues(a) { return globalThis.crypto.getRandomValues(a); } -export default { randomBytes, randomUUID, randomFillSync, createHash, createHmac, getHashes, getCiphers, timingSafeEqual, constants, webcrypto, getRandomValues }; diff --git a/js/src/runtime/node/diagnostics_channel.ts b/js/src/runtime/node/diagnostics_channel.ts deleted file mode 100644 index 0e0ef81..0000000 --- a/js/src/runtime/node/diagnostics_channel.ts +++ /dev/null @@ -1,29 +0,0 @@ -// node:diagnostics_channel — minimal no-op channel registry (undici probes this). -class Channel { - constructor(name) { this.name = name; this._subs = []; } - get hasSubscribers() { return this._subs.length > 0; } - publish(msg) { for (const s of this._subs.slice()) { try { s(msg, this.name); } catch {} } } - subscribe(fn) { this._subs.push(fn); } - unsubscribe(fn) { this._subs = this._subs.filter((s) => s !== fn); return true; } -} -const registry = new Map(); -export function channel(name) { - let c = registry.get(name); - if (!c) { c = new Channel(name); registry.set(name, c); } - return c; -} -export function hasSubscribers(name) { const c = registry.get(name); return !!c && c.hasSubscribers; } -export function subscribe(name, fn) { channel(name).subscribe(fn); } -export function unsubscribe(name, fn) { return channel(name).unsubscribe(fn); } -export function tracingChannel(nameOrChannels) { - const base = typeof nameOrChannels === "string" ? nameOrChannels : ""; - const mk = (suffix) => channel(base ? `tracing:${base}:${suffix}` : suffix); - return { - start: mk("start"), end: mk("end"), asyncStart: mk("asyncStart"), - asyncEnd: mk("asyncEnd"), error: mk("error"), - traceSync(fn, ctx, thisArg, ...a) { return fn.apply(thisArg, a); }, - tracePromise(fn, ctx, thisArg, ...a) { return fn.apply(thisArg, a); }, - traceCallback(fn, pos, ctx, thisArg, ...a) { return fn.apply(thisArg, a); }, - }; -} -export default { channel, hasSubscribers, subscribe, unsubscribe, tracingChannel, Channel }; diff --git a/js/src/runtime/node/dns.ts b/js/src/runtime/node/dns.ts deleted file mode 100644 index 9d3efbb..0000000 --- a/js/src/runtime/node/dns.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function lookup(host, _o, cb) { const c = typeof _o === "function" ? _o : cb; c && c(null, "127.0.0.1", 4); } -export function resolve(_h, cb) { cb && cb(null, []); } -export const promises = { lookup: async () => ({ address: "127.0.0.1", family: 4 }), resolve: async () => [] }; -export default { lookup, resolve, promises }; diff --git a/js/src/runtime/node/events.ts b/js/src/runtime/node/events.ts deleted file mode 100644 index 0459777..0000000 --- a/js/src/runtime/node/events.ts +++ /dev/null @@ -1,62 +0,0 @@ -// node:events — EventEmitter, pure JS. -export class EventEmitter { - constructor() { - this._events = new Map(); - this._maxListeners = 10; - } - setMaxListeners(n) { this._maxListeners = n; return this; } - getMaxListeners() { return this._maxListeners; } - on(type, fn) { - let arr = this._events.get(type); - if (!arr) { arr = []; this._events.set(type, arr); } - arr.push(fn); - return this; - } - addListener(type, fn) { return this.on(type, fn); } - once(type, fn) { - const wrap = (...args) => { this.off(type, wrap); fn(...args); }; - wrap.listener = fn; - return this.on(type, wrap); - } - prependListener(type, fn) { - let arr = this._events.get(type); - if (!arr) { arr = []; this._events.set(type, arr); } - arr.unshift(fn); - return this; - } - off(type, fn) { - const arr = this._events.get(type); - if (arr) { - const i = arr.findIndex((f) => f === fn || f.listener === fn); - if (i !== -1) arr.splice(i, 1); - } - return this; - } - removeListener(type, fn) { return this.off(type, fn); } - removeAllListeners(type) { - if (type === undefined) this._events.clear(); - else this._events.delete(type); - return this; - } - emit(type, ...args) { - const arr = this._events.get(type); - if (!arr || arr.length === 0) { - if (type === "error") throw args[0] instanceof Error ? args[0] : new Error("Unhandled error"); - return false; - } - for (const fn of arr.slice()) fn.apply(this, args); - return true; - } - listeners(type) { return (this._events.get(type) || []).slice(); } - listenerCount(type) { return (this._events.get(type) || []).length; } - eventNames() { return [...this._events.keys()]; } -} -export const once = (emitter, name) => - new Promise((resolve, reject) => { - emitter.once(name, (...args) => resolve(args)); - emitter.once("error", reject); - }); -export default EventEmitter; -EventEmitter.EventEmitter = EventEmitter; -EventEmitter.once = once; -EventEmitter.defaultMaxListeners = 10; diff --git a/js/src/runtime/node/fs-promises.ts b/js/src/runtime/node/fs-promises.ts deleted file mode 100644 index ba28635..0000000 --- a/js/src/runtime/node/fs-promises.ts +++ /dev/null @@ -1,26 +0,0 @@ -// node:fs/promises -import fs from "node:fs"; -const p = fs.promises; -export const readFile = p.readFile; -export const writeFile = p.writeFile; -export const readdir = p.readdir; -export const mkdir = p.mkdir; -export const stat = p.stat; -export const lstat = p.lstat; -export const realpath = p.realpath; -export const readlink = p.readlink; -export const unlink = p.unlink; -export const rm = p.rm; -export const rmdir = p.rmdir; -export const rename = p.rename; -export const copyFile = p.copyFile; -export const cp = p.cp; -export const appendFile = p.appendFile; -export const chmod = p.chmod; -export const symlink = p.symlink; -export const utimes = p.utimes; -export const truncate = p.truncate; -export const mkdtemp = p.mkdtemp; -export const access = p.access; -export const open = p.open; -export default p; diff --git a/js/src/runtime/node/fs.ts b/js/src/runtime/node/fs.ts deleted file mode 100644 index 04abd32..0000000 --- a/js/src/runtime/node/fs.ts +++ /dev/null @@ -1,232 +0,0 @@ -// node:fs — sync + promise subset, backed by native ops (globalThis.__node.fs). -// Native ops return JSON strings; wrap them so callers see plain objects. -const raw = globalThis.__node.fs; -const fs = { - readFile: (p) => JSON.parse(raw.readFile(p)), - writeFile: (p, b) => JSON.parse(raw.writeFile(p, b)), - exists: (p) => raw.exists(p), - readdir: (p) => JSON.parse(raw.readdir(p)), - mkdir: (p, r) => raw.mkdir(p, r), - stat: (p) => JSON.parse(raw.stat(p)), - realpath: (p) => JSON.parse(raw.realpath(p)), - unlink: (p) => raw.unlink(p), -}; - -function decode(bytesJson, encoding) { - // Native returns a JSON array of bytes; decode per requested encoding. - const bytes = bytesJson; - if (!encoding) return globalThis.Buffer ? globalThis.Buffer.from(bytes) : Uint8Array.from(bytes); - const B = globalThis.__nodeBuffer; - return B ? B.from(bytes).toString(encoding) : String.fromCharCode(...bytes); -} - -export function readFileSync(path, options) { - const encoding = typeof options === "string" ? options : options && options.encoding; - const res = fs.readFile(String(path)); - if (res.err) throw enoent(res.err, path); - return decode(res.bytes, encoding); -} -export function writeFileSync(pathOrFd, data, options) { - // writeFileSync also accepts a file descriptor (SessionManager uses this). - if (typeof pathOrFd === "number") { fdWrite(pathOrFd, data, options); return; } - const encoding = typeof options === "string" ? options : (options && options.encoding) || "utf8"; - let bytes; - if (typeof data === "string") { - const B = globalThis.__nodeBuffer; - bytes = B ? Array.from(B.from(data, encoding)) : Array.from(data, (c) => c.charCodeAt(0)); - } else bytes = Array.from(data); - const res = fs.writeFile(String(pathOrFd), bytes); - if (res.err) throw new Error(res.err); -} -export function existsSync(path) { - return fs.exists(String(path)); -} -export function readdirSync(path) { - const res = fs.readdir(String(path)); - if (res.err) throw enoent(res.err, path); - return res.entries; -} -export function mkdirSync(path, options) { - fs.mkdir(String(path), !!(options && options.recursive)); -} -export function statSync(path) { - const res = fs.stat(String(path)); - if (res.err) throw enoent(res.err, path); - return makeStat(res); -} -export const lstatSync = statSync; -export function realpathSync(path) { - const res = fs.realpath(String(path)); - return res.err ? String(path) : res.path; -} -export function unlinkSync(path) { - fs.unlink(String(path)); -} -export function rmSync(path) { - fs.unlink(String(path)); -} -export const constants = { F_OK: 0, R_OK: 4, W_OK: 2, X_OK: 1 }; -export function accessSync(path, _mode) { - if (!existsSync(path)) throw enoent("no access", path); -} - -// Broader fs surface as stubs so any `import { … } from "fs"` resolves. The ones -// pi actually exercises headlessly are implemented above; the rest throw or no-op -// until a milestone needs them. -const nope = (name) => () => { throw new Error(`fs.${name} is not implemented in Pocket Pi`); }; - -// fd-backed I/O over the whole-file native ops. openSync honors the read/write/ -// append/exclusive flags SessionManager uses ("r", "wx", "a", …); reads slurp the -// file and readSync copies a window; fd writes append through to the path. -const __fds = new Map(); -let __nextFd = 3; -function ebadf() { const e = new Error("EBADF: bad file descriptor"); e.code = "EBADF"; return e; } -function parseFlags(flags) { - const f = String(flags || "r"); - return { - write: /[wa+]/.test(f), - append: /a/.test(f), - create: /[wa]/.test(f), - excl: /x/.test(f), - truncate: /w/.test(f) && !/\+/.test(f), - read: /r|\+/.test(f), - }; -} -export function openSync(path, flags, _mode) { - path = String(path); - const f = parseFlags(flags); - const exists = existsSync(path); - if (f.excl && exists) { const e = new Error(`EEXIST: file already exists, open '${path}'`); e.code = "EEXIST"; throw e; } - if (!f.create && !exists) throw enoent("open", path); - if (f.truncate || (f.create && !exists)) writeFileSync(path, ""); // create/truncate - const bytes = f.read && !f.truncate && existsSync(path) ? (fs.readFile(path).bytes || []) : []; - const fd = __nextFd++; - __fds.set(fd, { path, flags: f, bytes, pos: 0 }); - return fd; -} -export function readSync(fd, buffer, offset, length, position) { - const e = __fds.get(fd); - if (!e) throw ebadf(); - const start = position == null || position < 0 ? e.pos : position; - let n = 0; - for (; n < length && start + n < e.bytes.length; n++) buffer[offset + n] = e.bytes[start + n]; - if (position == null || position < 0) e.pos = start + n; - return n; -} -function toStr(data, options) { - if (typeof data === "string") return data; - const enc = (typeof options === "string" ? options : options && options.encoding) || "utf8"; - const B = globalThis.__nodeBuffer; - return B ? B.from(data).toString(enc) : String.fromCharCode(...data); -} -// Append `data` through the fd to its file (native writeFile overwrites, so we -// read-modify-write). Fine for the small, append-mostly session store. -function fdWrite(fd, data, options) { - const e = __fds.get(fd); - if (!e) throw ebadf(); - const prev = existsSync(e.path) ? readFileSync(e.path, "utf8") : ""; - writeFileSync(e.path, prev + toStr(data, options), "utf8"); -} -export function writeSync(fd, data, _offOrPos, _length, _position) { - const s = typeof data === "string" ? data : toStr(data); - fdWrite(fd, s); - return typeof data === "string" ? s.length : data.length; -} -export function closeSync(fd) { __fds.delete(fd); } -export const fsyncSync = () => {}; -export const fdatasyncSync = () => {}; -export const ftruncateSync = () => {}; -export function appendFileSync(path, data, options) { - const prev = existsSync(path) ? readFileSync(path, "utf8") : ""; - writeFileSync(path, prev + (typeof data === "string" ? data : ""), options); -} -export const copyFileSync = nope("copyFileSync"); -export function renameSync(a, b) { const d = readFileSync(a); writeFileSync(b, d); unlinkSync(a); } -export const rmdirSync = (p) => unlinkSync(p); -export const cpSync = nope("cpSync"); -export const chmodSync = () => {}; -export const symlinkSync = nope("symlinkSync"); -export const readlinkSync = (p) => realpathSync(p); -export const truncateSync = nope("truncateSync"); -export const utimesSync = () => {}; -export const createReadStream = nope("createReadStream"); -export const createWriteStream = nope("createWriteStream"); -export const watchFile = () => {}; -export const unwatchFile = () => {}; -export function watch() { return { close() {}, on() {}, unref() { return this; } }; } -export const opendirSync = nope("opendirSync"); -export const mkdtempSync = (prefix) => { const p = prefix + Math.random().toString(36).slice(2, 8); mkdirSync(p, { recursive: true }); return p; }; - -function makeStat(res) { - return { - size: res.size || 0, - mtimeMs: res.mtimeMs || 0, - isFile: () => res.isFile, - isDirectory: () => res.isDir, - isSymbolicLink: () => false, - }; -} -function enoent(msg, path) { - const e = new Error(`ENOENT: ${msg}, '${path}'`); - e.code = "ENOENT"; - e.path = String(path); - return e; -} - -// Callback-style async API (fs.readFile(path, cb), etc.) — wrap the sync forms. -const cbify = (fn) => (...args) => { - const cb = typeof args[args.length - 1] === "function" ? args.pop() : () => {}; - queueMicrotask(() => { try { cb(null, fn(...args)); } catch (e) { cb(e); } }); -}; -export const readFile = cbify(readFileSync); -export const writeFile = cbify(writeFileSync); -export const readdir = cbify(readdirSync); -export const stat = cbify(statSync); -export const lstat = cbify(statSync); -export const mkdir = cbify(mkdirSync); -export const access = cbify(accessSync); -export const unlink = cbify(unlinkSync); -export const realpath = cbify(realpathSync); -export const rename = cbify(renameSync); -export const rm = cbify(unlinkSync); -export const exists = (p, cb) => queueMicrotask(() => cb(existsSync(p))); - -const P = (fn) => (...args) => new Promise((res, rej) => { try { res(fn(...args)); } catch (e) { rej(e); } }); -export const promises = { - readFile: P(readFileSync), - writeFile: P(writeFileSync), - readdir: P(readdirSync), - mkdir: P(mkdirSync), - stat: P(statSync), - lstat: P(statSync), - realpath: P(realpathSync), - readlink: P(readlinkSync), - unlink: P(unlinkSync), - rm: P(rmSync), - rmdir: P(rmdirSync), - rename: P(renameSync), - copyFile: P(copyFileSync), - cp: P(cpSync), - appendFile: P(appendFileSync), - chmod: P(chmodSync), - symlink: P(symlinkSync), - utimes: P(utimesSync), - truncate: P(truncateSync), - mkdtemp: P(mkdtempSync), - access: P(accessSync), - open: P((path) => ({ - fd: 0, - readFile: (opts) => readFileSync(path, opts), - writeFile: (data, opts) => writeFileSync(path, data, opts), - stat: () => statSync(path), - close: () => {}, - read: () => ({ bytesRead: 0, buffer: null }), - write: () => ({ bytesWritten: 0 }), - })), -}; - -export default { - readFileSync, writeFileSync, existsSync, readdirSync, mkdirSync, statSync, lstatSync, - realpathSync, unlinkSync, rmSync, promises, - constants: { F_OK: 0, R_OK: 4, W_OK: 2, X_OK: 1 }, -}; diff --git a/js/src/runtime/node/http.ts b/js/src/runtime/node/http.ts deleted file mode 100644 index 9549fec..0000000 --- a/js/src/runtime/node/http.ts +++ /dev/null @@ -1,12 +0,0 @@ -// node:http — stub; Pocket Pi routes HTTP through fetch(). Imports resolve; the -// classic client APIs throw if actually used. -import { EventEmitter } from "node:events"; -export class Agent { constructor(o) { this.options = o || {}; } } -export class Server extends EventEmitter { listen() { return this; } close() {} } -export function request() { throw new Error("http.request not supported (use fetch)"); } -export function get() { throw new Error("http.get not supported (use fetch)"); } -export const globalAgent = new Agent(); -export const METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]; -export const STATUS_CODES = {}; -export function createServer() { return new Server(); } -export default { Agent, Server, request, get, globalAgent, METHODS, STATUS_CODES, createServer }; diff --git a/js/src/runtime/node/https.ts b/js/src/runtime/node/https.ts deleted file mode 100644 index b5fd3d9..0000000 --- a/js/src/runtime/node/https.ts +++ /dev/null @@ -1,12 +0,0 @@ -// node:https — stub; Pocket Pi routes HTTP through fetch(). Imports resolve; the -// classic client APIs throw if actually used. -import { EventEmitter } from "node:events"; -export class Agent { constructor(o) { this.options = o || {}; } } -export class Server extends EventEmitter { listen() { return this; } close() {} } -export function request() { throw new Error("https.request not supported (use fetch)"); } -export function get() { throw new Error("https.get not supported (use fetch)"); } -export const globalAgent = new Agent(); -export const METHODS = ["GET", "POST", "PUT", "DELETE", "PATCH", "HEAD", "OPTIONS"]; -export const STATUS_CODES = {}; -export function createServer() { return new Server(); } -export default { Agent, Server, request, get, globalAgent, METHODS, STATUS_CODES, createServer }; diff --git a/js/src/runtime/node/module.ts b/js/src/runtime/node/module.ts deleted file mode 100644 index dde490d..0000000 --- a/js/src/runtime/node/module.ts +++ /dev/null @@ -1,13 +0,0 @@ -// node:module — createRequire delegates to the synchronous CJS require. -export function createRequire(from) { - let base = "/pocket-pi-bundle"; - if (typeof from === "string") base = from.startsWith("file://") ? from.slice(7) : from; - else if (from && from.href) base = String(from.href).replace(/^file:\/\//, ""); - return function require(spec) { return globalThis.__cjsRequire(base, spec); }; -} -export const builtinModules = ["fs", "path", "os", "events", "util", "buffer", "process", "crypto", "url", "child_process", "stream", "string_decoder", "module", "readline", "http", "https", "net", "tls", "zlib", "assert", "querystring"]; -export function isBuiltin(m) { return builtinModules.includes(String(m).replace(/^node:/, "")); } -export class Module {} -Module.createRequire = createRequire; -Module.builtinModules = builtinModules; -export default { createRequire, builtinModules, isBuiltin, Module }; diff --git a/js/src/runtime/node/net.ts b/js/src/runtime/node/net.ts deleted file mode 100644 index 967d57a..0000000 --- a/js/src/runtime/node/net.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { EventEmitter } from "node:events"; -export class Socket extends EventEmitter { connect() { return this; } write() { return true; } end() {} destroy() {} setTimeout() {} setNoDelay() {} setKeepAlive() {} } -export class Server extends EventEmitter { listen() { return this; } close() {} } -export function connect() { return new Socket(); } -export function createConnection() { return new Socket(); } -export function createServer() { return new Server(); } -export function isIP(s) { return /^\d+\.\d+\.\d+\.\d+$/.test(s) ? 4 : 0; } -export function isIPv4(s) { return isIP(s) === 4; } -export function isIPv6() { return false; } -export default { Socket, Server, connect, createConnection, createServer, isIP, isIPv4, isIPv6 }; diff --git a/js/src/runtime/node/os.ts b/js/src/runtime/node/os.ts deleted file mode 100644 index 5712893..0000000 --- a/js/src/runtime/node/os.ts +++ /dev/null @@ -1,42 +0,0 @@ -// node:os — backed by native ops for the machine-specific bits. -const n = globalThis.__node; - -export function platform() { - return (globalThis.process && globalThis.process.platform) || "linux"; -} -export function homedir() { - return n ? n.homedir() : "/"; -} -export function tmpdir() { - return n ? n.tmpdir() : "/tmp"; -} -export function hostname() { - return n && n.hostname ? n.hostname() : "localhost"; -} -export function arch() { - return (globalThis.process && globalThis.process.arch) || "x64"; -} -export function type() { - return platform() === "darwin" ? "Darwin" : platform() === "win32" ? "Windows_NT" : "Linux"; -} -export function release() { - return "0.0.0"; -} -export function cpus() { - return []; -} -export function totalmem() { - return 0; -} -export function freemem() { - return 0; -} -export function uptime() { - return 0; -} -export function userInfo() { - return { username: "user", homedir: homedir(), shell: null, uid: -1, gid: -1 }; -} -export const EOL = "\n"; -export const constants = { signals: {}, errno: {} }; -export default { platform, homedir, tmpdir, hostname, arch, type, release, cpus, totalmem, freemem, uptime, userInfo, EOL, constants }; diff --git a/js/src/runtime/node/path.ts b/js/src/runtime/node/path.ts deleted file mode 100644 index 64998cc..0000000 --- a/js/src/runtime/node/path.ts +++ /dev/null @@ -1,144 +0,0 @@ -// node:path (POSIX subset) — pure JS, enough for pi + its deps. -const sep = "/"; - -function assertPath(p) { - if (typeof p !== "string") throw new TypeError("Path must be a string. Received " + typeof p); -} - -function normalizeArray(parts, allowAboveRoot) { - const res = []; - for (const p of parts) { - if (!p || p === ".") continue; - if (p === "..") { - if (res.length && res[res.length - 1] !== "..") res.pop(); - else if (allowAboveRoot) res.push(".."); - } else res.push(p); - } - return res; -} - -export function normalize(path) { - assertPath(path); - if (path.length === 0) return "."; - const isAbs = path.charCodeAt(0) === 47; - const trailing = path.charCodeAt(path.length - 1) === 47; - let out = normalizeArray(path.split("/"), !isAbs).join("/"); - if (!out && !isAbs) out = "."; - if (out && trailing) out += "/"; - return (isAbs ? "/" : "") + out; -} - -export function isAbsolute(path) { - assertPath(path); - return path.length > 0 && path.charCodeAt(0) === 47; -} - -export function join(...args) { - if (args.length === 0) return "."; - let joined; - for (const arg of args) { - assertPath(arg); - if (arg.length > 0) joined = joined === undefined ? arg : joined + "/" + arg; - } - if (joined === undefined) return "."; - return normalize(joined); -} - -export function resolve(...args) { - let resolved = ""; - let isAbs = false; - for (let i = args.length - 1; i >= -1 && !isAbs; i--) { - const path = i >= 0 ? args[i] : cwd(); - assertPath(path); - if (path.length === 0) continue; - resolved = path + "/" + resolved; - isAbs = path.charCodeAt(0) === 47; - } - const parts = normalizeArray(resolved.split("/"), !isAbs); - resolved = parts.join("/"); - if (isAbs) return "/" + resolved; - return resolved.length > 0 ? resolved : "."; -} - -function cwd() { - return (globalThis.process && globalThis.process.cwd && globalThis.process.cwd()) || "/"; -} - -export function dirname(path) { - assertPath(path); - if (path.length === 0) return "."; - let end = -1; - let matchedSlash = true; - for (let i = path.length - 1; i >= 1; --i) { - if (path.charCodeAt(i) === 47) { - if (!matchedSlash) { end = i; break; } - } else matchedSlash = false; - } - if (end === -1) return path.charCodeAt(0) === 47 ? "/" : "."; - if (end === 0 && path.charCodeAt(0) === 47) return "/"; - return path.slice(0, end); -} - -export function basename(path, ext) { - assertPath(path); - let start = 0, end = -1, matchedSlash = true; - for (let i = path.length - 1; i >= 0; --i) { - if (path.charCodeAt(i) === 47) { - if (!matchedSlash) { start = i + 1; break; } - } else if (end === -1) { matchedSlash = false; end = i + 1; } - } - let base = end === -1 ? "" : path.slice(start, end); - if (ext && base.endsWith(ext) && base !== ext) base = base.slice(0, -ext.length); - return base; -} - -export function extname(path) { - assertPath(path); - let startDot = -1, startPart = 0, end = -1, matchedSlash = true, preDotState = 0; - for (let i = path.length - 1; i >= 0; --i) { - const code = path.charCodeAt(i); - if (code === 47) { if (!matchedSlash) { startPart = i + 1; break; } continue; } - if (end === -1) { matchedSlash = false; end = i + 1; } - if (code === 46) { if (startDot === -1) startDot = i; else if (preDotState !== 1) preDotState = 1; } - else if (startDot !== -1) preDotState = -1; - } - if (startDot === -1 || end === -1 || preDotState === 0 || (preDotState === 1 && startDot === end - 1 && startDot === startPart + 1)) return ""; - return path.slice(startDot, end); -} - -export function relative(from, to) { - from = resolve(from); - to = resolve(to); - if (from === to) return ""; - const fromParts = from.split("/").filter(Boolean); - const toParts = to.split("/").filter(Boolean); - let i = 0; - while (i < fromParts.length && i < toParts.length && fromParts[i] === toParts[i]) i++; - const up = fromParts.length - i; - const out = []; - for (let j = 0; j < up; j++) out.push(".."); - return out.concat(toParts.slice(i)).join("/"); -} - -export function parse(path) { - const root = isAbsolute(path) ? "/" : ""; - const dir = dirname(path); - const base = basename(path); - const ext = extname(base); - return { root, dir, base, ext, name: ext ? base.slice(0, -ext.length) : base }; -} - -export function toNamespacedPath(path) { return path; } // no-op on POSIX -export function format(obj) { - const dir = obj.dir || obj.root || ""; - const base = obj.base || `${obj.name || ""}${obj.ext || ""}`; - if (!dir) return base; - return dir === obj.root ? `${dir}${base}` : `${dir}${sep}${base}`; -} - -export const posix = { sep, delimiter: ":", normalize, isAbsolute, join, resolve, dirname, basename, extname, relative, parse, format, toNamespacedPath }; -// We only implement POSIX semantics; win32 is a passthrough so imports resolve. -export const win32 = { ...posix, sep: "\\", delimiter: ";" }; -export { sep }; -export const delimiter = ":"; -export default { sep, delimiter, normalize, isAbsolute, join, resolve, dirname, basename, extname, relative, parse, format, toNamespacedPath, posix, win32 }; diff --git a/js/src/runtime/node/perf_hooks.ts b/js/src/runtime/node/perf_hooks.ts deleted file mode 100644 index 6e0d84f..0000000 --- a/js/src/runtime/node/perf_hooks.ts +++ /dev/null @@ -1,4 +0,0 @@ -// node:perf_hooks -export const performance = globalThis.performance || { now: () => Date.now(), timeOrigin: 0 }; -export class PerformanceObserver { observe() {} disconnect() {} } -export default { performance, PerformanceObserver }; diff --git a/js/src/runtime/node/process.ts b/js/src/runtime/node/process.ts deleted file mode 100644 index 119d5b1..0000000 --- a/js/src/runtime/node/process.ts +++ /dev/null @@ -1,11 +0,0 @@ -// node:process — re-exports the global process object the runtime installs. -const p = globalThis.process; -export const env = p.env; -export const platform = p.platform; -export const argv = p.argv; -export const version = p.version; -export const versions = p.versions; -export const cwd = p.cwd; -export const nextTick = p.nextTick; -export const exit = p.exit; -export default p; diff --git a/js/src/runtime/node/querystring.ts b/js/src/runtime/node/querystring.ts deleted file mode 100644 index f500686..0000000 --- a/js/src/runtime/node/querystring.ts +++ /dev/null @@ -1,4 +0,0 @@ -export function parse(str) { const o = {}; for (const p of String(str).split("&")) { if (!p) continue; const i = p.indexOf("="); const k = decodeURIComponent(i < 0 ? p : p.slice(0, i)); const v = i < 0 ? "" : decodeURIComponent(p.slice(i + 1)); o[k] = v; } return o; } -export function stringify(obj) { return Object.entries(obj || {}).map(([k, v]) => encodeURIComponent(k) + "=" + encodeURIComponent(v)).join("&"); } -export const decode = parse, encode = stringify; -export default { parse, stringify, decode, encode }; diff --git a/js/src/runtime/node/readline.ts b/js/src/runtime/node/readline.ts deleted file mode 100644 index 2a05a00..0000000 --- a/js/src/runtime/node/readline.ts +++ /dev/null @@ -1,10 +0,0 @@ -// node:readline — stub sufficient for import; interactive input isn't used headless. -import { EventEmitter } from "node:events"; -export function createInterface() { - const rl = new EventEmitter(); - rl.question = (_q, cb) => cb && cb(""); - rl.close = () => {}; - rl.on = rl.on.bind(rl); - return rl; -} -export default { createInterface }; diff --git a/js/src/runtime/node/stream-promises.ts b/js/src/runtime/node/stream-promises.ts deleted file mode 100644 index d3107bd..0000000 --- a/js/src/runtime/node/stream-promises.ts +++ /dev/null @@ -1,12 +0,0 @@ -// node:stream/promises -export function pipeline(...args) { - if (typeof args[args.length - 1] === "function") args.pop(); - return Promise.resolve(); -} -export function finished(stream) { - return new Promise((res) => { - if (stream && stream.on) { stream.on("end", res); stream.on("finish", res); } - queueMicrotask(res); - }); -} -export default { pipeline, finished }; diff --git a/js/src/runtime/node/stream.ts b/js/src/runtime/node/stream.ts deleted file mode 100644 index 85a59b0..0000000 --- a/js/src/runtime/node/stream.ts +++ /dev/null @@ -1,24 +0,0 @@ -// node:stream — minimal Readable/Writable/Transform/PassThrough over EventEmitter. -import { EventEmitter } from "node:events"; -export class Readable extends EventEmitter { - constructor(opts) { super(); this._opts = opts || {}; } - push(chunk) { if (chunk === null) this.emit("end"); else this.emit("data", chunk); return true; } - pipe(dest) { this.on("data", (c) => dest.write && dest.write(c)); this.on("end", () => dest.end && dest.end()); return dest; } - read() { return null; } - static from(iterable) { const r = new Readable(); queueMicrotask(async () => { for await (const c of iterable) r.push(c); r.push(null); }); return r; } -} -export class Writable extends EventEmitter { - constructor(opts) { super(); this._opts = opts || {}; } - write(chunk, _enc, cb) { if (this._opts.write) this._opts.write(chunk, _enc, cb || (() => {})); else if (cb) cb(); return true; } - end(chunk, _enc, cb) { if (chunk) this.write(chunk); this.emit("finish"); if (cb) cb(); } -} -export class Duplex extends Readable {} -export class Transform extends Duplex {} -export class PassThrough extends Transform {} -export default { Readable, Writable, Duplex, Transform, PassThrough }; -export function pipeline(...args) { - const cb = typeof args[args.length - 1] === "function" ? args.pop() : null; - queueMicrotask(() => cb && cb(null)); - return args[args.length - 1]; -} -export function finished(stream, cb) { queueMicrotask(() => cb && cb(null)); } diff --git a/js/src/runtime/node/string_decoder.ts b/js/src/runtime/node/string_decoder.ts deleted file mode 100644 index aa528db..0000000 --- a/js/src/runtime/node/string_decoder.ts +++ /dev/null @@ -1,7 +0,0 @@ -// node:string_decoder -export class StringDecoder { - constructor(encoding) { this.encoding = encoding || "utf8"; this._dec = new TextDecoder(); } - write(buf) { return this._dec.decode(buf instanceof Uint8Array ? buf : globalThis.Buffer.from(buf)); } - end(buf) { return buf ? this.write(buf) : ""; } -} -export default { StringDecoder }; diff --git a/js/src/runtime/node/timers.ts b/js/src/runtime/node/timers.ts deleted file mode 100644 index 95f2358..0000000 --- a/js/src/runtime/node/timers.ts +++ /dev/null @@ -1,8 +0,0 @@ -export const setTimeout = globalThis.setTimeout; -export const clearTimeout = globalThis.clearTimeout; -export const setInterval = globalThis.setInterval; -export const clearInterval = globalThis.clearInterval; -export const setImmediate = (fn, ...a) => globalThis.setTimeout(fn, 0, ...a); -export const clearImmediate = globalThis.clearTimeout; -export const promises = { setTimeout: (ms) => new Promise((r) => globalThis.setTimeout(r, ms)) }; -export default { setTimeout, clearTimeout, setInterval, clearInterval, setImmediate, clearImmediate, promises }; diff --git a/js/src/runtime/node/tls.ts b/js/src/runtime/node/tls.ts deleted file mode 100644 index 6cad175..0000000 --- a/js/src/runtime/node/tls.ts +++ /dev/null @@ -1,6 +0,0 @@ -import { Socket } from "node:net"; -export class TLSSocket extends Socket {} -export function connect() { return new TLSSocket(); } -export function createSecureContext() { return {}; } -export const rootCertificates = []; -export default { TLSSocket, connect, createSecureContext, rootCertificates }; diff --git a/js/src/runtime/node/tty.ts b/js/src/runtime/node/tty.ts deleted file mode 100644 index 799b5db..0000000 --- a/js/src/runtime/node/tty.ts +++ /dev/null @@ -1,5 +0,0 @@ -// node:tty — stub (Pocket Pi runs headless). -export function isatty() { return false; } -export class ReadStream {} -export class WriteStream {} -export default { isatty, ReadStream, WriteStream }; diff --git a/js/src/runtime/node/url.ts b/js/src/runtime/node/url.ts deleted file mode 100644 index 36c725d..0000000 --- a/js/src/runtime/node/url.ts +++ /dev/null @@ -1,35 +0,0 @@ -// node:url -export const URL = globalThis.URL; -export const URLSearchParams = globalThis.URLSearchParams; -export function fileURLToPath(url) { - let s = typeof url === "string" ? url : url.href; - if (s.startsWith("file://")) s = s.slice(7); - return decodeURIComponent(s); -} -export function pathToFileURL(path) { - return new globalThis.URL("file://" + encodeURI(path)); -} -// Legacy url.parse/format/resolve. -export function parse(str) { - try { - const u = new globalThis.URL(str); - return { href: u.href, protocol: u.protocol, host: u.host, hostname: u.hostname, port: u.port, pathname: u.pathname, search: u.search, hash: u.hash, query: u.search.replace(/^\?/, "") }; - } catch { - return { href: str, pathname: str, protocol: null, host: null, hostname: null, port: "", search: "", hash: "", query: "" }; - } -} -export function format(obj) { - if (typeof obj === "string") return obj; - if (obj && typeof obj.href === "string" && obj.protocol) return obj.href; - const proto = obj.protocol ? (obj.protocol.endsWith(":") ? obj.protocol : obj.protocol + ":") : ""; - const host = obj.host || (obj.hostname ? obj.hostname + (obj.port ? ":" + obj.port : "") : ""); - const search = obj.search || (obj.query ? "?" + (typeof obj.query === "string" ? obj.query : new globalThis.URLSearchParams(obj.query).toString()) : ""); - return (proto ? proto + "//" : "") + host + (obj.pathname || "") + search + (obj.hash || ""); -} -export function resolve(from, to) { - try { return new globalThis.URL(to, from).href; } catch { return to; } -} -export const Url = globalThis.URL; -export function domainToASCII(d) { return d; } -export function domainToUnicode(d) { return d; } -export default { URL, URLSearchParams, fileURLToPath, pathToFileURL, parse, format, resolve, Url, domainToASCII, domainToUnicode }; diff --git a/js/src/runtime/node/util.ts b/js/src/runtime/node/util.ts deleted file mode 100644 index d307df5..0000000 --- a/js/src/runtime/node/util.ts +++ /dev/null @@ -1,76 +0,0 @@ -// node:util — the commonly-used subset. -export function inherits(ctor, superCtor) { - ctor.super_ = superCtor; - ctor.prototype = Object.create(superCtor.prototype, { - constructor: { value: ctor, enumerable: false, writable: true, configurable: true }, - }); -} - -export function format(fmt, ...args) { - if (typeof fmt !== "string") return [fmt, ...args].map(inspect).join(" "); - let i = 0; - let out = fmt.replace(/%[sdifjoO%]/g, (m) => { - if (m === "%%") return "%"; - if (i >= args.length) return m; - const a = args[i++]; - switch (m) { - case "%s": return String(a); - case "%d": case "%i": return String(parseInt(a, 10)); - case "%f": return String(parseFloat(a)); - case "%j": try { return JSON.stringify(a); } catch { return "[Circular]"; } - default: return inspect(a); - } - }); - for (; i < args.length; i++) out += " " + (typeof args[i] === "string" ? args[i] : inspect(args[i])); - return out; -} - -export function inspect(obj) { - if (typeof obj === "string") return obj; - try { return JSON.stringify(obj); } catch { return String(obj); } -} -inspect.custom = Symbol.for("nodejs.util.inspect.custom"); - -export function promisify(fn) { - return function (...args) { - return new Promise((resolve, reject) => { - fn.call(this, ...args, (err, ...rest) => (err ? reject(err) : resolve(rest.length > 1 ? rest : rest[0]))); - }); - }; -} - -export function callbackify(fn) { - return function (...args) { - const cb = args.pop(); - fn.apply(this, args).then((v) => cb(null, v), (e) => cb(e)); - }; -} - -export function deprecate(fn) { return fn; } -// debuglog(section) → a logger; enabled only if NODE_DEBUG lists the section. -export function debuglog(section, cb) { - const env = (globalThis.process && globalThis.process.env && globalThis.process.env.NODE_DEBUG) || ""; - const on = env.split(/[\s,]+/).includes(section); - const fn = on ? (...args) => { try { console.error(`${section}:`, format(...args)); } catch {} } : () => {}; - if (typeof cb === "function") cb(fn); - return fn; -} -export const debug = debuglog; -export function inspect2() {} -export const isDeepStrictEqual = (a, b) => { try { return JSON.stringify(a) === JSON.stringify(b); } catch { return a === b; } }; -export function stripVTControlCharacters(s) { return String(s).replace(/\x1b\[[0-9;]*m/g, ""); } -export const _extend = Object.assign; - -export const types = { - isPromise: (v) => v && typeof v.then === "function", - isDate: (v) => v instanceof Date, - isRegExp: (v) => v instanceof RegExp, - isArrayBuffer: (v) => v instanceof ArrayBuffer, - isTypedArray: (v) => ArrayBuffer.isView(v) && !(v instanceof DataView), - isAsyncFunction: (v) => v && v.constructor && v.constructor.name === "AsyncFunction", -}; - -export const TextEncoder = globalThis.TextEncoder; -export const TextDecoder = globalThis.TextDecoder; - -export default { inherits, format, inspect, promisify, callbackify, deprecate, debuglog, debug, isDeepStrictEqual, stripVTControlCharacters, _extend, types, TextEncoder, TextDecoder }; diff --git a/js/src/runtime/node/v8.ts b/js/src/runtime/node/v8.ts deleted file mode 100644 index 1d6dd84..0000000 --- a/js/src/runtime/node/v8.ts +++ /dev/null @@ -1,2 +0,0 @@ -// node:v8 — minimal stub. -export default {}; diff --git a/js/src/runtime/node/vm.ts b/js/src/runtime/node/vm.ts deleted file mode 100644 index e3cdff0..0000000 --- a/js/src/runtime/node/vm.ts +++ /dev/null @@ -1,2 +0,0 @@ -// node:vm — minimal stub. -export default {}; diff --git a/js/src/runtime/node/worker_threads.ts b/js/src/runtime/node/worker_threads.ts deleted file mode 100644 index 5abc477..0000000 --- a/js/src/runtime/node/worker_threads.ts +++ /dev/null @@ -1,40 +0,0 @@ -// node:worker_threads — stub. Pocket Pi is single-threaded (one QuickJS realm), -// so there are no real workers; these exist so imports resolve. `Worker` throws -// if actually constructed. -export class Worker { - constructor() { - throw new Error("worker_threads.Worker is not supported in Pocket Pi (single-threaded)"); - } -} -export const isMainThread = true; -export const parentPort = null; -export const threadId = 0; -export const workerData = null; -export class MessageChannel { - constructor() { this.port1 = new MessagePort(); this.port2 = new MessagePort(); } -} -export class MessagePort { - postMessage() {} - on() { return this; } - once() { return this; } - close() {} - ref() { return this; } - unref() { return this; } - start() {} -} -export const BroadcastChannel = class BroadcastChannel { - postMessage() {} - close() {} - on() { return this; } -}; -export function markAsUntransferable() {} -export function moveMessagePortToContext() { throw new Error("not supported"); } -export function receiveMessageOnPort() { return undefined; } -export function setEnvironmentData() {} -export function getEnvironmentData() { return undefined; } -export default { - Worker, isMainThread, parentPort, threadId, workerData, - MessageChannel, MessagePort, BroadcastChannel, - markAsUntransferable, moveMessagePortToContext, receiveMessageOnPort, - setEnvironmentData, getEnvironmentData, -}; diff --git a/js/src/runtime/node/zlib.ts b/js/src/runtime/node/zlib.ts deleted file mode 100644 index 58e2e77..0000000 --- a/js/src/runtime/node/zlib.ts +++ /dev/null @@ -1,6 +0,0 @@ -const nope = (n) => () => { throw new Error("zlib." + n + " not supported"); }; -export const gzip = nope("gzip"), gunzip = nope("gunzip"), deflate = nope("deflate"), inflate = nope("inflate"); -export const gzipSync = nope("gzipSync"), gunzipSync = nope("gunzipSync"), deflateSync = nope("deflateSync"), inflateSync = nope("inflateSync"), brotliCompressSync = nope("brotliCompressSync"), brotliDecompressSync = nope("brotliDecompressSync"); -export const constants = {}; -export function createGzip() { throw new Error("zlib streams not supported"); } -export default { gzip, gunzip, deflate, inflate, gzipSync, gunzipSync, deflateSync, inflateSync, brotliCompressSync, brotliDecompressSync, constants, createGzip }; diff --git a/js/src/runtime/prelude.ts b/js/src/runtime/prelude.ts deleted file mode 100644 index 931efc0..0000000 --- a/js/src/runtime/prelude.ts +++ /dev/null @@ -1,121 +0,0 @@ -// Pocket Pi prelude — the Web/Node globals pi's agent core expects that classic -// QuickJS does not ship. Evaluated once before the agent bundle. Everything here -// is pure JS on top of QuickJS's ES2023 baseline (Promise, Map, Set, async -// generators, Date, JSON) plus the native `host` namespace mounted by Rust. -// -// Deliberately minimal: byte decoding and HTTPS live in Rust, so no TextDecoder -// or fetch is needed on the JS side — only timers, abort, microtasks, and uuid. - -(function () { - "use strict"; - - // Node's `global` alias + setImmediate (many CJS deps assume them). - if (typeof globalThis.global === "undefined") globalThis.global = globalThis; - if (typeof globalThis.setImmediate !== "function") - globalThis.setImmediate = (fn, ...a) => globalThis.setTimeout(fn, 0, ...a); - if (typeof globalThis.clearImmediate !== "function") - globalThis.clearImmediate = (id) => globalThis.clearTimeout(id); - - // --- timers, advanced by the host frame pump (globalThis.__catpiTimers) --- - const timers = new Map(); - let nextTimer = 1; - globalThis.setTimeout = function (fn, delay, ...args) { - const id = nextTimer++; - timers.set(id, { due: Date.now() + (delay || 0), fn, args }); - return id; - }; - globalThis.clearTimeout = function (id) { - timers.delete(id); - }; - globalThis.setInterval = function () { - throw new Error("setInterval is not supported in Pocket Pi"); - }; - globalThis.clearInterval = function () {}; - globalThis.__catpiTimers = function () { - if (timers.size === 0) return; - const now = Date.now(); - for (const [id, t] of timers) { - if (t.due <= now) { - timers.delete(id); - try { - t.fn(...t.args); - } catch (e) { - if (globalThis.host && host.emit) - host.emit(JSON.stringify({ kind: "error", message: "timer: " + String(e) })); - } - } - } - }; - - // --- queueMicrotask (QuickJS drains the job queue after each host frame) --- - if (typeof globalThis.queueMicrotask !== "function") { - globalThis.queueMicrotask = function (fn) { - Promise.resolve().then(fn); - }; - } - - // --- structuredClone (JSON-safe deep clone is enough for tool arguments) --- - if (typeof globalThis.structuredClone !== "function") { - globalThis.structuredClone = function (v) { - return v === undefined ? undefined : JSON.parse(JSON.stringify(v)); - }; - } - - // --- AbortController / AbortSignal --- - if (typeof globalThis.AbortController !== "function") { - class PPAbortSignal { - constructor() { - this.aborted = false; - this.reason = undefined; - this._listeners = []; - } - addEventListener(type, cb) { - if (type === "abort") this._listeners.push(cb); - } - removeEventListener(type, cb) { - if (type === "abort") this._listeners = this._listeners.filter((l) => l !== cb); - } - _fire() { - if (this.aborted) return; - this.aborted = true; - for (const l of this._listeners.slice()) { - try { - l({ type: "abort" }); - } catch {} - } - } - throwIfAborted() { - if (this.aborted) throw this.reason || new Error("Aborted"); - } - } - globalThis.AbortSignal = PPAbortSignal; - globalThis.AbortController = class { - constructor() { - this.signal = new PPAbortSignal(); - } - abort(reason) { - this.signal.reason = reason || new Error("Aborted"); - this.signal._fire(); - } - }; - } - - // --- crypto.randomUUID (host-backed for real entropy; JS fallback otherwise) --- - if (typeof globalThis.crypto !== "object" || !globalThis.crypto) globalThis.crypto = {}; - if (typeof globalThis.crypto.randomUUID !== "function") { - globalThis.crypto.randomUUID = function () { - if (globalThis.host && host.uuid) return host.uuid(); - // Fallback: not cryptographically strong; fine for tool-call ids. - return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, function (c) { - const r = (Date.now() + Math.floor(Math.random() * 1e9)) % 16; - const v = c === "x" ? r : (r & 0x3) | 0x8; - return v.toString(16); - }); - }; - } - - // --- process shim: pi guards most access with typeof process checks --- - if (typeof globalThis.process !== "object" || !globalThis.process) { - globalThis.process = { env: {}, platform: "pocket-pi", versions: {} }; - } -})(); diff --git a/js/src/runtime/web-globals.ts b/js/src/runtime/web-globals.ts deleted file mode 100644 index 2bb7166..0000000 --- a/js/src/runtime/web-globals.ts +++ /dev/null @@ -1,442 +0,0 @@ -// WHATWG Web globals for Pocket Pi — fetch / Response / ReadableStream / Headers -// / URL / atob / btoa / crypto.getRandomValues. Backed by the native HTTP hub -// (raw mode) and driven by the frame pump, so a real npm package that does -// `fetch(...).then(r => r.body.getReader())` works unmodified. - -(function () { - "use strict"; - const B64 = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/"; - - // --- Intl (minimal; QuickJS ships none) --- - if (typeof globalThis.Intl !== "object" || !globalThis.Intl) { - globalThis.Intl = { - Segmenter: class { - constructor() {} - segment(str) { - const chars = [...String(str)]; // code-point granularity ≈ grapheme - return { - [Symbol.iterator]() { - let i = 0; - return { - next() { - return i < chars.length - ? { value: { segment: chars[i], index: i++, input: str }, done: false } - : { value: undefined, done: true }; - }, - }; - }, - }; - } - }, - NumberFormat: class { constructor() {} format(n) { return String(n); } formatToParts() { return []; } }, - DateTimeFormat: class { constructor() {} format(d) { return String(d); } formatToParts() { return []; } }, - Collator: class { constructor() {} compare(a, b) { return a < b ? -1 : a > b ? 1 : 0; } }, - getCanonicalLocales: (l) => (Array.isArray(l) ? l : [l]).filter(Boolean), - }; - } - - // --- Blob / File / FormData (referenced by the OpenAI transport even for - // JSON requests; we send JSON bodies, so these just need to exist) --- - if (typeof globalThis.Blob === "undefined") { - globalThis.Blob = class Blob { - constructor(parts = [], opts = {}) { - this._parts = parts; - this.type = (opts && opts.type) || ""; - let size = 0; - for (const p of parts) { - if (typeof p === "string") size += p.length; - else if (p && p.byteLength != null) size += p.byteLength; - else if (p && p.size != null) size += p.size; - } - this.size = size; - } - async text() { - return this._parts.map((p) => (typeof p === "string" ? p : "")).join(""); - } - async arrayBuffer() { - return new globalThis.TextEncoder().encode(await this.text()).buffer; - } - slice() { return new globalThis.Blob(this._parts, { type: this.type }); } - }; - } - if (typeof globalThis.File === "undefined") { - globalThis.File = class File extends globalThis.Blob { - constructor(parts, name, opts = {}) { - super(parts, opts); - this.name = String(name); - this.lastModified = 0; - } - }; - } - if (typeof globalThis.FormData === "undefined") { - globalThis.FormData = class FormData { - constructor() { this._entries = []; } - append(k, v, filename) { this._entries.push([String(k), v, filename]); } - set(k, v) { - this._entries = this._entries.filter((e) => e[0] !== String(k)); - this._entries.push([String(k), v]); - } - get(k) { const e = this._entries.find((e) => e[0] === String(k)); return e ? e[1] : null; } - getAll(k) { return this._entries.filter((e) => e[0] === String(k)).map((e) => e[1]); } - has(k) { return this._entries.some((e) => e[0] === String(k)); } - delete(k) { this._entries = this._entries.filter((e) => e[0] !== String(k)); } - forEach(cb, thisArg) { for (const [k, v] of this._entries) cb.call(thisArg, v, k, this); } - *entries() { for (const e of this._entries) yield [e[0], e[1]]; } - *keys() { for (const e of this._entries) yield e[0]; } - *values() { for (const e of this._entries) yield e[1]; } - [Symbol.iterator]() { return this.entries(); } - }; - } - - // --- Web event/message globals (undici's webidl references these as globals - // during module init; we don't do real message passing, so they're stubs) --- - if (typeof globalThis.EventTarget !== "function") { - globalThis.EventTarget = class EventTarget { - constructor() { this.__l = {}; } - addEventListener(t, cb) { (this.__l[t] ||= []).push(cb); } - removeEventListener(t, cb) { this.__l[t] = (this.__l[t] || []).filter((f) => f !== cb); } - dispatchEvent(e) { for (const cb of this.__l[e && e.type] || []) { try { cb(e); } catch {} } return true; } - }; - } - if (typeof globalThis.Event !== "function") { - globalThis.Event = class Event { - constructor(type, init = {}) { this.type = type; this.bubbles = !!init.bubbles; this.defaultPrevented = false; } - preventDefault() { this.defaultPrevented = true; } - stopPropagation() {} - stopImmediatePropagation() {} - }; - } - if (typeof globalThis.CustomEvent !== "function") { - globalThis.CustomEvent = class CustomEvent extends globalThis.Event { - constructor(type, init = {}) { super(type, init); this.detail = init.detail; } - }; - } - if (typeof globalThis.MessagePort !== "function") { - globalThis.MessagePort = class MessagePort extends globalThis.EventTarget { - postMessage() {} start() {} close() {} - on() { return this; } once() { return this; } - ref() { return this; } unref() { return this; } - }; - } - if (typeof globalThis.MessageChannel !== "function") { - globalThis.MessageChannel = class MessageChannel { - constructor() { this.port1 = new globalThis.MessagePort(); this.port2 = new globalThis.MessagePort(); } - }; - } - if (typeof globalThis.DOMException !== "function") { - globalThis.DOMException = class DOMException extends Error { - constructor(message, name) { super(message); this.name = name || "Error"; } - }; - } - - // --- TextEncoder / TextDecoder (UTF-8) --- - if (typeof globalThis.TextEncoder !== "function") { - globalThis.TextEncoder = class TextEncoder { - get encoding() { return "utf-8"; } - encode(str) { - str = String(str); - const out = []; - for (let i = 0; i < str.length; i++) { - let c = str.charCodeAt(i); - if (c < 0x80) out.push(c); - else if (c < 0x800) out.push(0xc0 | (c >> 6), 0x80 | (c & 0x3f)); - else if (c >= 0xd800 && c <= 0xdbff) { - const c2 = str.charCodeAt(++i); - c = 0x10000 + ((c & 0x3ff) << 10) + (c2 & 0x3ff); - out.push(0xf0 | (c >> 18), 0x80 | ((c >> 12) & 0x3f), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); - } else out.push(0xe0 | (c >> 12), 0x80 | ((c >> 6) & 0x3f), 0x80 | (c & 0x3f)); - } - return new Uint8Array(out); - } - }; - } - if (typeof globalThis.TextDecoder !== "function") { - globalThis.TextDecoder = class TextDecoder { - constructor(label) { this._enc = label || "utf-8"; } - get encoding() { return "utf-8"; } - decode(input) { - if (!input) return ""; - const bytes = input instanceof Uint8Array ? input : new Uint8Array(input.buffer || input); - let out = "", i = 0; - while (i < bytes.length) { - let c = bytes[i++]; - if (c < 0x80) out += String.fromCharCode(c); - else if (c >= 0xc0 && c < 0xe0) out += String.fromCharCode(((c & 0x1f) << 6) | (bytes[i++] & 0x3f)); - else if (c >= 0xe0 && c < 0xf0) out += String.fromCharCode(((c & 0xf) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f)); - else { - const cp = ((c & 7) << 18) | ((bytes[i++] & 0x3f) << 12) | ((bytes[i++] & 0x3f) << 6) | (bytes[i++] & 0x3f); - const off = cp - 0x10000; - out += String.fromCharCode(0xd800 + (off >> 10), 0xdc00 + (off & 0x3ff)); - } - } - return out; - } - }; - } - - // --- atob / btoa --- - if (typeof globalThis.btoa !== "function") { - globalThis.btoa = function (bin) { - let out = ""; - for (let i = 0; i < bin.length; i += 3) { - const a = bin.charCodeAt(i), b = bin.charCodeAt(i + 1), c = bin.charCodeAt(i + 2); - const n = (a << 16) | ((isNaN(b) ? 0 : b) << 8) | (isNaN(c) ? 0 : c); - out += B64[(n >> 18) & 63] + B64[(n >> 12) & 63] + (isNaN(b) ? "=" : B64[(n >> 6) & 63]) + (isNaN(c) ? "=" : B64[n & 63]); - } - return out; - }; - } - if (typeof globalThis.atob !== "function") { - globalThis.atob = function (b64) { - b64 = String(b64).replace(/[^A-Za-z0-9+/]/g, ""); - let out = ""; - for (let i = 0; i < b64.length; i += 4) { - const n = (B64.indexOf(b64[i]) << 18) | (B64.indexOf(b64[i + 1]) << 12) | (B64.indexOf(b64[i + 2] || "A") << 6) | B64.indexOf(b64[i + 3] || "A"); - out += String.fromCharCode((n >> 16) & 255); - if (b64[i + 2] && b64[i + 2] !== "=") out += String.fromCharCode((n >> 8) & 255); - if (b64[i + 3] && b64[i + 3] !== "=") out += String.fromCharCode(n & 255); - } - return out; - }; - } - function b64ToBytes(b64) { - const bin = globalThis.atob(b64); - const u = new Uint8Array(bin.length); - for (let i = 0; i < bin.length; i++) u[i] = bin.charCodeAt(i); - return u; - } - - // --- crypto.getRandomValues (non-cryptographic fallback; fine for request ids) --- - if (typeof globalThis.crypto !== "object" || !globalThis.crypto) globalThis.crypto = {}; - if (typeof globalThis.crypto.getRandomValues !== "function") { - globalThis.crypto.getRandomValues = function (arr) { - for (let i = 0; i < arr.length; i++) arr[i] = Math.floor(Math.random() * 256); - return arr; - }; - } - - // --- Headers --- - if (typeof globalThis.Headers !== "function") { - globalThis.Headers = class Headers { - constructor(init) { - this._m = new Map(); - if (init) { - const entries = init instanceof Headers ? init.entries() : Array.isArray(init) ? init : Object.entries(init); - for (const [k, v] of entries) this.set(k, v); - } - } - set(k, v) { this._m.set(String(k).toLowerCase(), String(v)); } - append(k, v) { const p = this._m.get(String(k).toLowerCase()); this.set(k, p ? p + ", " + v : v); } - get(k) { const v = this._m.get(String(k).toLowerCase()); return v === undefined ? null : v; } - has(k) { return this._m.has(String(k).toLowerCase()); } - delete(k) { this._m.delete(String(k).toLowerCase()); } - forEach(fn) { this._m.forEach((v, k) => fn(v, k, this)); } - entries() { return this._m.entries(); } - keys() { return this._m.keys(); } - values() { return this._m.values(); } - [Symbol.iterator]() { return this._m.entries(); } - }; - } - - // --- URL / URLSearchParams (minimal but base-aware) --- - if (typeof globalThis.URLSearchParams !== "function") { - globalThis.URLSearchParams = class URLSearchParams { - constructor(init) { - this._p = []; - if (typeof init === "string") { - for (const pair of init.replace(/^\?/, "").split("&")) { - if (!pair) continue; - const i = pair.indexOf("="); - this._p.push(i < 0 ? [decodeURIComponent(pair), ""] : [decodeURIComponent(pair.slice(0, i)), decodeURIComponent(pair.slice(i + 1))]); - } - } else if (init) for (const [k, v] of Object.entries(init)) this._p.push([k, String(v)]); - } - get(k) { const e = this._p.find((x) => x[0] === k); return e ? e[1] : null; } - set(k, v) { const e = this._p.find((x) => x[0] === k); if (e) e[1] = String(v); else this._p.push([k, String(v)]); } - append(k, v) { this._p.push([k, String(v)]); } - has(k) { return this._p.some((x) => x[0] === k); } - delete(k) { this._p = this._p.filter((x) => x[0] !== k); } - forEach(fn) { for (const [k, v] of this._p) fn(v, k, this); } - toString() { return this._p.map(([k, v]) => encodeURIComponent(k) + "=" + encodeURIComponent(v)).join("&"); } - [Symbol.iterator]() { return this._p[Symbol.iterator](); } - }; - } - if (typeof globalThis.URL !== "function") { - globalThis.URL = class URL { - constructor(url, base) { - let full = String(url); - if (base && !/^[a-zA-Z][a-zA-Z0-9+.-]*:/.test(full)) { - const b = String(base).replace(/\/+$/, ""); - full = full.startsWith("/") ? originOf(b) + full : b + "/" + full; - } - const m = /^([a-zA-Z][a-zA-Z0-9+.-]*:)\/\/([^/?#]*)([^?#]*)(\?[^#]*)?(#.*)?$/.exec(full); - if (!m) throw new TypeError("Invalid URL: " + full); - this.protocol = m[1]; - this.host = m[2]; - this.hostname = m[2].split(":")[0]; - this.port = m[2].split(":")[1] || ""; - this.pathname = m[3] || "/"; - this.search = m[4] || ""; - this.hash = m[5] || ""; - this.searchParams = new globalThis.URLSearchParams(this.search); - this.origin = this.protocol + "//" + this.host; - } - get href() { return this.origin + this.pathname + (this.searchParams.toString() ? "?" + this.searchParams.toString() : "") + this.hash; } - toString() { return this.href; } - }; - function originOf(u) { const m = /^([a-zA-Z]+:\/\/[^/?#]*)/.exec(u); return m ? m[1] : u; } - } - - // --- ReadableStream (byte chunks pushed by the fetch pump) --- - class PPReadableStream { - constructor() { - this._queue = []; - this._closed = false; - this._error = null; - this._waiters = []; - } - _enqueue(chunk) { this._queue.push(chunk); this._flush(); } - _close() { this._closed = true; this._flush(); } - _fail(e) { this._error = e; this._flush(); } - _flush() { - while (this._waiters.length) { - if (this._queue.length) this._waiters.shift().resolve({ value: this._queue.shift(), done: false }); - else if (this._error) this._waiters.shift().reject(this._error); - else if (this._closed) this._waiters.shift().resolve({ value: undefined, done: true }); - else break; - } - } - getReader() { - const self = this; - return { - read() { return new Promise((resolve, reject) => { self._waiters.push({ resolve, reject }); self._flush(); }); }, - releaseLock() {}, - cancel() { self._closed = true; return Promise.resolve(); }, - }; - } - [Symbol.asyncIterator]() { - const reader = this.getReader(); - return { next: () => reader.read(), return: () => { reader.cancel(); return Promise.resolve({ done: true }); }, [Symbol.asyncIterator]() { return this; } }; - } - } - globalThis.ReadableStream = globalThis.ReadableStream || PPReadableStream; - - // --- Response --- - class Response { - constructor(body, init) { - init = init || {}; - this.status = init.status ?? 200; - this.statusText = init.statusText ?? ""; - this.ok = this.status >= 200 && this.status < 300; - this.headers = init.headers instanceof globalThis.Headers ? init.headers : new globalThis.Headers(init.headers || {}); - this.url = init.url || ""; - this.body = body || null; - this._bodyUsed = false; - } - get bodyUsed() { return this._bodyUsed; } - async _consume() { - this._bodyUsed = true; - if (!this.body) return new Uint8Array(0); - if (this.body instanceof Uint8Array) return this.body; - const reader = this.body.getReader(); - const parts = []; - let total = 0; - for (;;) { - const { value, done } = await reader.read(); - if (done) break; - parts.push(value); - total += value.length; - } - const out = new Uint8Array(total); - let off = 0; - for (const p of parts) { out.set(p, off); off += p.length; } - return out; - } - async arrayBuffer() { return (await this._consume()).buffer; } - async text() { return new TextDecoder().decode(await this._consume()); } - async json() { return JSON.parse(await this.text()); } - clone() { return new Response(this.body, { status: this.status, statusText: this.statusText, headers: this.headers, url: this.url }); } - } - globalThis.Response = globalThis.Response || Response; - - // --- fetch --- - const fetchTurns = new Map(); - - globalThis.fetch = function (input, init) { - init = init || {}; - const url = typeof input === "string" ? input : input.url; - const headers = {}; - if (init.headers) { - const h = init.headers instanceof globalThis.Headers ? init.headers.entries() : Array.isArray(init.headers) ? init.headers : Object.entries(init.headers); - for (const [k, v] of h) headers[k] = String(v); - } - let body = init.body; - if (body && typeof body !== "string") { - if (body instanceof Uint8Array) body = new TextDecoder().decode(body); - else body = String(body); - } - const request = { url, method: init.method || "GET", headers, body, raw: true }; - - return new Promise((resolve, reject) => { - let turnId; - try { - turnId = host.http.start(JSON.stringify(request)); - } catch (e) { - reject(new Error(String(e && e.message ? e.message : e))); - return; - } - const turn = { resolve, reject, stream: null, resolved: false, url }; - fetchTurns.set(turnId, turn); - if (init.signal) { - init.signal.addEventListener("abort", () => { - try { host.http.cancel(turnId); } catch {} - if (!turn.resolved) reject(new Error("aborted")); - else if (turn.stream) turn.stream._fail(new Error("aborted")); - fetchTurns.delete(turnId); - }); - } - }); - }; - - // Drained once per host frame (added to the pump). - globalThis.__catpiFetchPump = function () { - if (fetchTurns.size === 0) return; - for (const [turnId, turn] of fetchTurns) { - let out; - try { - out = JSON.parse(host.http.drain(turnId)); - } catch (e) { - if (!turn.resolved) turn.reject(new Error(String(e))); - else if (turn.stream) turn.stream._fail(new Error(String(e))); - fetchTurns.delete(turnId); - continue; - } - for (const line of out.lines) { - let msg; - try { msg = JSON.parse(line); } catch { continue; } - if (msg.__meta) { - turn.stream = new PPReadableStream(); - const resp = new Response(turn.stream, { - status: msg.__meta.status, - statusText: msg.__meta.statusText, - headers: msg.__meta.headers, - url: turn.url, - }); - turn.resolved = true; - turn.resolve(resp); - } else if (msg.__chunk != null && turn.stream) { - turn.stream._enqueue(b64ToBytes(msg.__chunk)); - } - } - if (out.error) { - if (!turn.resolved) turn.reject(new Error(out.error)); - else if (turn.stream) turn.stream._fail(new Error(out.error)); - fetchTurns.delete(turnId); - } else if (out.done) { - if (turn.stream) turn.stream._close(); - else if (!turn.resolved) turn.reject(new Error("no response")); - fetchTurns.delete(turnId); - } - } - }; -})(); diff --git a/js/tsconfig.json b/js/tsconfig.json deleted file mode 100644 index 437fc65..0000000 --- a/js/tsconfig.json +++ /dev/null @@ -1,20 +0,0 @@ -{ - "compilerOptions": { - "target": "ES2023", - "module": "ESNext", - "moduleResolution": "bundler", - "lib": ["ES2023"], - "types": [], - "strict": false, - "noImplicitAny": false, - "noEmit": true, - "skipLibCheck": true, - "allowImportingTsExtensions": true, - "allowJs": true, - "isolatedModules": true, - "forceConsistentCasingInFileNames": true - }, - "//": "esbuild transpiles all sources. tsc typechecks Pocket Pi's pi-full harness; runtime polyfills and the undici adapter intentionally stay outside this gate because they reimplement upstream runtime shapes.", - "include": ["src/pi-full/**/*.ts", "env.d.ts"], - "exclude": ["src/pi-full/undici-stub.ts"] -} diff --git a/tools/build-agentos-data.ts b/tools/build-agentos-data.ts new file mode 100644 index 0000000..d23e62f --- /dev/null +++ b/tools/build-agentos-data.ts @@ -0,0 +1,23 @@ +import { basename, dirname } from "node:path"; + +const [entry, output, pocketjsRoot] = Bun.argv.slice(2); +if (!entry || !output || !pocketjsRoot) { + throw new Error("usage: build-agentos-data.ts "); +} + +const compiler = await import(`${pocketjsRoot}/framework/compiler/jsx-plugin.ts`); +const result = await Bun.build({ + entrypoints: [entry], + outdir: dirname(output), + naming: basename(output), + target: "browser", + format: "iife", + minify: true, + conditions: ["browser"], + define: { "process.env.NODE_ENV": '"production"' }, + plugins: [compiler.jsxPlugin("solid", { entry })], +}); +if (!result.success) { + for (const log of result.logs) console.error(log); + process.exit(1); +} diff --git a/tools/uart-model-bridge.py b/tools/uart-model-bridge.py index 6f2c925..1c3e7b4 100755 --- a/tools/uart-model-bridge.py +++ b/tools/uart-model-bridge.py @@ -22,6 +22,66 @@ WAITING = "PPI-RPC-WAITING" REQUEST = "PPI-RPC-REQUEST:" STREAM = "PPI-RPC-STREAM:" +ROBINHOOD_KEYCHAIN_SERVICE = "Codex MCP Credentials" +ROBINHOOD_KEYCHAIN_ACCOUNT = "robinhood-trading|5cbe81c78ff5ae58" +EXA_KEYCHAIN_SERVICE = "Pocket Pi Credentials" +EXA_KEYCHAIN_ACCOUNT = "exa-api-key" +DEEPSEEK_KEYCHAIN_SERVICE = "Pocket Pi Credentials" +DEEPSEEK_KEYCHAIN_ACCOUNT = "deepseek-api-key" + + +def keychain_secret(service: str, account: str) -> str | None: + """Read one generic-password value without ever printing it.""" + try: + result = subprocess.run( + ["security", "find-generic-password", "-s", service, "-a", account, "-w"], + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError: + return None + value = result.stdout.strip() + return value if result.returncode == 0 and value else None + + +def robinhood_access_token() -> str | None: + """Reuse a completed Codex MCP OAuth grant without exposing it to disk.""" + try: + result = subprocess.run( + [ + "security", + "find-generic-password", + "-s", + ROBINHOOD_KEYCHAIN_SERVICE, + "-a", + ROBINHOOD_KEYCHAIN_ACCOUNT, + "-w", + ], + capture_output=True, + text=True, + check=False, + ) + except FileNotFoundError: + return None + if result.returncode != 0: + return None + try: + credential = json.loads(result.stdout) + token = credential.get("token_response", {}).get("access_token") + return token if isinstance(token, str) and token else None + except (json.JSONDecodeError, AttributeError): + return None + + +def exa_api_key() -> str | None: + """Load the user-provided Exa key from macOS Keychain.""" + return keychain_secret(EXA_KEYCHAIN_SERVICE, EXA_KEYCHAIN_ACCOUNT) + + +def deepseek_api_key() -> str | None: + """Load the user-provided DeepSeek key from macOS Keychain.""" + return keychain_secret(DEEPSEEK_KEYCHAIN_SERVICE, DEEPSEEK_KEYCHAIN_ACCOUNT) def write_line(fd: int, line: str) -> None: @@ -35,6 +95,29 @@ def write_line(fd: int, line: str) -> None: payload = payload[count:] +def log_tool_failure(request: dict[str, object]) -> None: + """Print only failed ESP tool text, never successful account payloads.""" + context = request.get("context") + if not isinstance(context, dict): + return + messages = context.get("messages") + if not isinstance(messages, list): + return + for message in reversed(messages): + if not isinstance(message, dict) or message.get("role") != "toolResult": + continue + if message.get("isError") is not True: + return + content = message.get("content") + if not isinstance(content, list): + return + for item in content: + if isinstance(item, dict) and item.get("type") == "text": + failure = str(item.get("text", "App tool failed"))[:400] + print(f"[bridge] ESP tool failure: {failure}", flush=True) + return + + def config(args: argparse.Namespace) -> dict[str, object]: provider = args.provider or ("codex" if args.backend == "uart" else "openai") value: dict[str, object] = { @@ -44,13 +127,43 @@ def config(args: argparse.Namespace) -> dict[str, object]: } if args.model: value["model"] = args.model + value["thinkingLevel"] = args.thinking_level if args.provision_wifi: value["wifiSsid"] = input("Wi-Fi SSID: ").strip() value["wifiPassword"] = getpass.getpass("Wi-Fi password: ") if args.backend == "wireless": - value["modelApiKey"] = getpass.getpass(f"{provider} API key: ") + key = deepseek_api_key() if provider == "deepseek" else None + if key: + value["modelApiKey"] = key + print("DeepSeek: reusing Keychain API key (RAM only on device)", flush=True) + else: + value["modelApiKey"] = getpass.getpass(f"{provider} API key: ") + app_credentials: dict[str, str] = {} + key = exa_api_key() + if key: + app_credentials["exa.api-key"] = key + print("Exa: reusing Keychain API key (RAM only on device)", flush=True) + elif args.provision_exa: + key = getpass.getpass("Exa API key: ") + if key: + app_credentials["exa.api-key"] = key + token = robinhood_access_token() + if token: + app_credentials["robinhood.oauth-access-token"] = token + print( + "Robinhood: reusing existing authorized Codex MCP session (RAM only)", + flush=True, + ) + elif args.provision_robinhood: + token = getpass.getpass("Robinhood OAuth access token: ") + if token: + app_credentials["robinhood.oauth-access-token"] = token + if app_credentials: + value["appCredentials"] = app_credentials if args.prompt: value["initialPrompt"] = args.prompt + if args.prompt_delay_seconds: + value["initialPromptDelaySeconds"] = args.prompt_delay_seconds return value @@ -60,17 +173,28 @@ def main() -> int: parser.add_argument("--backend", choices=("uart", "wireless"), default="uart") parser.add_argument( "--provider", - choices=("codex", "claude-code", "openai", "openrouter", "anthropic"), + choices=("codex", "claude-code", "openai", "openrouter", "anthropic", "deepseek"), ) parser.add_argument("--model") + parser.add_argument("--thinking-level", choices=("high", "xhigh"), default="high") parser.add_argument("--prompt", help="submit one prompt after the board agent is ready") + parser.add_argument( + "--prompt-delay-seconds", + type=int, + choices=range(0, 121), + default=0, + metavar="0..120", + help="delay the repeatable boot prompt while device services settle", + ) parser.add_argument("--provision-wifi", action="store_true") + parser.add_argument("--provision-exa", action="store_true") + parser.add_argument("--provision-robinhood", action="store_true") args = parser.parse_args() provider = args.provider or ("codex" if args.backend == "uart" else "openai") if args.backend == "uart" and provider not in ("codex", "claude-code"): parser.error("UART provider must be codex or claude-code") - if args.backend == "wireless" and provider not in ("openai", "openrouter", "anthropic"): - parser.error("wireless provider must be openai, openrouter or anthropic") + if args.backend == "wireless" and provider not in ("openai", "openrouter", "anthropic", "deepseek"): + parser.error("wireless provider must be openai, openrouter, anthropic or deepseek") runtime_config = config(args) model_backend = create_backend(provider) if args.backend == "uart" else None @@ -119,34 +243,30 @@ def stop(_signum: int, _frame: object) -> None: if model_backend is None: raise RuntimeError("wireless mode does not provide a UART model backend") stream_stats = [0, 0] + request_payload = json.loads(line[len(REQUEST) :]) + log_tool_failure(request_payload) def emit_delta(delta: str) -> None: stream_stats[0] += 1 stream_stats[1] += len(delta) - write_line( - fd, - STREAM - + json.dumps( - {"type": "text_delta", "text": delta}, - separators=(",", ":"), - ), - ) result = model_backend.complete( - json.loads(line[len(REQUEST) :]), + request_payload, emit_delta, ) - call = result.get("toolCall") - if isinstance(call, dict): - print( - f"[bridge] ESP tool {call.get('name')}: " - + json.dumps(call.get("arguments", {}), ensure_ascii=False), - flush=True, - ) + calls = result.get("toolCalls") + if isinstance(calls, list) and calls: + for call in calls: + print( + f"[bridge] ESP tool {call.get('name')}: " + + json.dumps(call.get("arguments", {}), ensure_ascii=False), + flush=True, + ) elif isinstance(result.get("text"), str): print(f"[bridge] Pi reply: {result['text']}", flush=True) print( - f"[bridge] streamed {stream_stats[0]} chunks / {stream_stats[1]} chars", + f"[bridge] coalesced {stream_stats[0]} provider chunks / " + f"{stream_stats[1]} chars into one UART result", flush=True, ) write_line( diff --git a/tools/uart_bridge/backends.py b/tools/uart_bridge/backends.py index 9bb4b95..8779c9d 100644 --- a/tools/uart_bridge/backends.py +++ b/tools/uart_bridge/backends.py @@ -25,7 +25,7 @@ def decision_prompt(request: dict[str, object]) -> tuple[str, list[object]]: "You are the model decision backend for a Pi Agent running on an ESP32-P4.", "Do not call Mac tools. The registered tools below run only on the ESP32.", "Return exactly one compact JSON object and no Markdown: " - '{"toolCall":{"name":"registered.name","arguments":{...}}} for one action, or ' + '{"toolCalls":[{"name":"registered.name","arguments":{...}}]} for actions, or ' '{"text":"final response"} when the turn is complete. Never claim success before ' "the corresponding tool result appears in the conversation.", f"System instruction: {system}" if system else "", @@ -49,29 +49,45 @@ def parse_decision(raw: str, tools: list[object], provider: str) -> dict[str, ob raise ValueError(f"{provider} returned invalid decision JSON: {error}") from error if not isinstance(value, dict): raise ValueError(f"{provider} decision must be a JSON object") - call = value.get("toolCall") - if isinstance(call, dict): + calls = value.get("toolCalls") + if isinstance(calls, list) and calls: registered = { tool.get("name") for tool in tools if isinstance(tool, dict) and isinstance(tool.get("name"), str) } - name = call.get("name") - arguments = call.get("arguments", {}) - if name not in registered: - raise ValueError(f"{provider} requested unregistered ESP32 tool: {name}") - if not isinstance(arguments, dict): - raise ValueError("toolCall.arguments must be a JSON object") - return { - "toolCall": { - "id": f"esp_{int(time.time() * 1000)}", + result_calls: list[dict[str, object]] = [] + base_id = int(time.time() * 1000) + for index, call in enumerate(calls): + if not isinstance(call, dict): + raise ValueError(f"{provider} toolCalls entries must be objects") + name = call.get("name") + arguments = call.get("arguments", {}) + if name not in registered: + raise ValueError(f"{provider} requested unregistered ESP32 tool: {name}") + if not isinstance(arguments, dict): + raise ValueError("toolCalls arguments must be JSON objects") + result_calls.append({ + "id": f"esp_{base_id}_{index}", "name": name, "arguments": arguments, - } + }) + return { + "thinking": "", + "text": "", + "toolCalls": result_calls, + "usage": {}, + "stopReason": "toolUse", } if isinstance(value.get("text"), str): - return {"text": value["text"]} - raise ValueError(f"{provider} decision needs text or toolCall") + return { + "thinking": "", + "text": value["text"], + "toolCalls": [], + "usage": {}, + "stopReason": "stop", + } + raise ValueError(f"{provider} decision needs text or non-empty toolCalls") def _partial_text(raw: str) -> str | None: diff --git a/tools/xtask/src/main.rs b/tools/xtask/src/main.rs index 5d59fed..ec73cd5 100644 --- a/tools/xtask/src/main.rs +++ b/tools/xtask/src/main.rs @@ -4,15 +4,26 @@ use std::process::{Command, ExitStatus}; use anyhow::{bail, Context, Result}; fn main() -> Result<()> { - let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("../.."); + let root = PathBuf::from(env!("CARGO_MANIFEST_DIR")) + .join("../..") + .canonicalize()?; let mut args = std::env::args().skip(1); - match (args.next().as_deref(), args.next().as_deref()) { - (Some("build"), Some("macos")) => cargo(&root, ["build", "-p", "pocket-pi-macos"]), + let command_name = args.next(); + let target = args.next(); + let mut rest = args.collect::>(); + let apps = take_apps(&mut rest)?; + match (command_name.as_deref(), target.as_deref()) { + (Some("build"), Some("agentos-apps")) => { + build_embedded_guest(&root)?; + build_agentos_apps(&root, &apps) + } (Some("build"), Some("esp32-p4")) => { build_embedded_guest(&root)?; + build_agentos_apps(&root, &apps)?; command( Command::new("rustup") .current_dir(root.join("firmware/esp32-p4")) + .env("POCKET_PI_APPS", &apps) .args([ "run", "nightly-2026-05-01", @@ -25,36 +36,132 @@ fn main() -> Result<()> { } (Some("build"), Some("esp32-p4-sim")) => { build_embedded_guest(&root)?; - cargo(&root, ["build", "-p", "pocket-pi-esp32-p4-sim"]) - } - (Some("run"), Some("macos")) => { - let rest = args.collect::>(); - cargo_with_args(&root, ["run", "-p", "pocket-pi-macos", "--"], &rest) + build_agentos_apps(&root, &apps)?; + cargo(&root, &apps, ["build", "-p", "pocket-pi-esp32-p4-sim"]) } (Some("run"), Some("esp32-p4-sim")) => { build_embedded_guest(&root)?; - let rest = args.collect::>(); - cargo_with_args(&root, ["run", "-p", "pocket-pi-esp32-p4-sim", "--"], &rest) + build_agentos_apps(&root, &apps)?; + cargo_with_args( + &root, + &apps, + ["run", "-p", "pocket-pi-esp32-p4-sim", "--"], + &rest, + ) } (Some("snapshot"), Some("esp32-p4-sim")) => { build_embedded_guest(&root)?; + build_agentos_apps(&root, &apps)?; let output = root.join("artifacts/screenshots/esp32-p4-sim.png"); std::fs::create_dir_all(output.parent().unwrap())?; cargo_with_args( &root, + &apps, ["run", "-p", "pocket-pi-esp32-p4-sim", "--"], &["--screenshot".into(), output.display().to_string()], ) } _ => { eprintln!( - "usage:\n cargo xtask build macos|esp32-p4|esp32-p4-sim\n cargo xtask run macos|esp32-p4-sim [args]\n cargo xtask snapshot esp32-p4-sim" + "usage:\n cargo xtask build agentos-apps|esp32-p4|esp32-p4-sim [--apps robinhood,exa|exa|robinhood|none]\n cargo xtask run esp32-p4-sim [--apps ...] [args]\n cargo xtask snapshot esp32-p4-sim [--apps ...]" ); bail!("unknown xtask command") } } } +fn take_apps(args: &mut Vec) -> Result { + let Some(index) = args.iter().position(|arg| arg == "--apps") else { + return Ok("robinhood,exa".into()); + }; + args.remove(index); + let value = args + .get(index) + .cloned() + .context("--apps requires a value")?; + args.remove(index); + selected_apps(&value)?; + Ok(value) +} + +fn selected_apps(value: &str) -> Result> { + if value == "none" { + return Ok(Vec::new()); + } + let mut selected = Vec::new(); + for app in value.split(',') { + if !matches!(app, "robinhood" | "exa") { + bail!("unknown App {app}; expected robinhood, exa, or none"); + } + if selected.contains(&app) { + bail!("duplicate App {app}"); + } + selected.push(app); + } + Ok(selected) +} + +fn build_agentos_apps(root: &Path, apps: &str) -> Result<()> { + const POCKETJS_REV: &str = "9c809bbd047ddc75c27caa4990951a78d942477a"; + let pocketjs = std::env::var_os("POCKETJS_ROOT") + .map(PathBuf::from) + .unwrap_or_else(|| root.parent().unwrap_or(root).join("pocketjs")); + let revision = Command::new("git") + .current_dir(&pocketjs) + .args(["rev-parse", "HEAD"]) + .output() + .with_context(|| format!("inspect PocketJS checkout at {}", pocketjs.display()))?; + let actual = String::from_utf8_lossy(&revision.stdout).trim().to_owned(); + if !revision.status.success() || actual != POCKETJS_REV { + bail!( + "POCKETJS_ROOT={} must be checked out at the pinned upstream PocketJS revision {POCKETJS_REV}; found {actual}", + pocketjs.display() + ); + } + install_if_missing(&pocketjs)?; + for app in std::iter::once("pi-agent").chain(selected_apps(apps)?) { + let app_root = root.join("apps").join(app); + command( + Command::new("bun") + .current_dir(&pocketjs) + .arg("tools/build.ts") + .arg(app_root.join("app.tsx")) + .arg(format!("--outdir={}", app_root.join("dist").display())) + .arg("--framework=solid"), + &format!("building AgentOS App {app}"), + )?; + minify_agentos_bundle(&app_root)?; + let data_entry = app_root.join("data-action.ts"); + if data_entry.is_file() { + command( + Command::new("bun") + .arg(root.join("tools/build-agentos-data.ts")) + .arg(&data_entry) + .arg(app_root.join("dist/data-action.js")) + .arg(&pocketjs), + &format!("building AgentOS data action {app}"), + )?; + } + } + Ok(()) +} + +fn minify_agentos_bundle(app_root: &Path) -> Result<()> { + let bundle = app_root.join("dist/app.js"); + let minified = app_root.join("dist/app.min.js"); + command( + Command::new("bun") + .arg("build") + .arg(&bundle) + .arg(format!("--outfile={}", minified.display())) + .arg("--minify"), + &format!("minifying {} for the device image", bundle.display()), + )?; + std::fs::rename(&minified, &bundle) + .with_context(|| format!("replace {} with minified bundle", bundle.display()))?; + Ok(()) +} + fn build_embedded_guest(root: &Path) -> Result<()> { let embedded = root.join("crates/pocket-pi-embedded/js"); install_if_missing(&embedded)?; @@ -63,7 +170,18 @@ fn build_embedded_guest(root: &Path) -> Result<()> { .current_dir(&embedded) .args(["run", "build"]), "building embedded Pi guest", - ) + )?; + let source = embedded.join("pi-agent.bundle.js"); + let destination = root.join("apps/pi-agent/dist/agent.js"); + std::fs::create_dir_all(destination.parent().unwrap())?; + std::fs::copy(&source, &destination).with_context(|| { + format!( + "install Pi Agent loop bundle {} -> {}", + source.display(), + destination.display() + ) + })?; + Ok(()) } fn install_if_missing(directory: &Path) -> Result<()> { @@ -76,17 +194,26 @@ fn install_if_missing(directory: &Path) -> Result<()> { Ok(()) } -fn cargo(root: &Path, args: [&str; N]) -> Result<()> { +fn cargo(root: &Path, apps: &str, args: [&str; N]) -> Result<()> { command( - Command::new("cargo").current_dir(root).args(args), + Command::new("cargo") + .current_dir(root) + .env("POCKET_PI_APPS", apps) + .args(args), "running cargo", ) } -fn cargo_with_args(root: &Path, base: [&str; N], rest: &[String]) -> Result<()> { +fn cargo_with_args( + root: &Path, + apps: &str, + base: [&str; N], + rest: &[String], +) -> Result<()> { command( Command::new("cargo") .current_dir(root) + .env("POCKET_PI_APPS", apps) .args(base) .args(rest), "running cargo",