diff --git a/.github/workflows/dimcode-release.yml b/.github/workflows/dimcode-release.yml new file mode 100644 index 00000000000..a3d3e9a7e65 --- /dev/null +++ b/.github/workflows/dimcode-release.yml @@ -0,0 +1,69 @@ +name: dimcode prerelease + +on: + push: + tags: ['dimcode-v*-next.*'] + workflow_dispatch: + +permissions: + contents: read + +concurrency: + group: dimcode-prerelease + cancel-in-progress: false + +jobs: + check: + strategy: + matrix: + os: [ubuntu-latest, macos-15] + runs-on: ${{ matrix.os }} + defaults: + run: + working-directory: agent + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v5 + with: + node-version: '24' + cache: npm + cache-dependency-path: agent/npm-shrinkwrap.json + - run: npm ci + - run: npm run check + - run: sh -n install.sh + + publish: + needs: check + runs-on: ubuntu-latest + permissions: + contents: write + id-token: write + defaults: + run: + working-directory: agent + steps: + - uses: actions/checkout@v7 + - uses: actions/setup-node@v5 + with: + node-version: '24' + registry-url: https://registry.npmjs.org + - name: Validate release identity + run: | + node --input-type=module -e ' + import fs from "node:fs"; + const p = JSON.parse(fs.readFileSync("package.json")); + if (p.name !== "@spomichter/dimcode" || !/^\d+\.\d+\.\d+-next\.\d+$/.test(p.version) || process.env.GITHUB_REF !== `refs/tags/dimcode-v${p.version}`) process.exit(1); + ' + - run: npm ci && npm run build + - run: npm pack + - name: Publish npm next + run: npm publish ./spomichter-dimcode-*.tgz --tag next --access public --provenance + env: + NODE_AUTH_TOKEN: ${{ secrets.DIMCODE_NPM_TOKEN }} + - name: Publish installer and package + run: | + gh release create "$GITHUB_REF_NAME" ./spomichter-dimcode-*.tgz install.sh \ + --title "dimcode ${GITHUB_REF_NAME#dimcode-v}" --prerelease \ + --notes 'Dimcode test release: private Node bootstrap, agent-led DimOS installation, persistent Pi sessions and inline memory visualization. Linux x64/arm64 and Apple Silicon installer; Linux and macOS package checks run before publication.' + env: + GH_TOKEN: ${{ github.token }} diff --git a/.github/workflows/dimcode.yml b/.github/workflows/dimcode.yml new file mode 100644 index 00000000000..db0e330c8b0 --- /dev/null +++ b/.github/workflows/dimcode.yml @@ -0,0 +1,47 @@ +name: dimcode + +on: + pull_request: + paths: + - 'agent/**' + - 'web/sdk/**' + - 'web/shared/**' + - 'web/relay/**' + - '.github/workflows/dimcode.yml' + push: + branches: [main] + paths: + - 'agent/**' + - 'web/sdk/**' + - 'web/shared/**' + - 'web/relay/**' + - '.github/workflows/dimcode.yml' + +permissions: {} + +jobs: + check: + runs-on: ubuntu-22.04 + timeout-minutes: 15 + permissions: + contents: read + defaults: + run: + working-directory: agent + steps: + - uses: actions/checkout@v7 + with: + lfs: false + - uses: actions/setup-node@v7 + with: + node-version: 24 + cache: npm + cache-dependency-path: agent/npm-shrinkwrap.json + - uses: denoland/setup-deno@v2 + with: + deno-version: v2.6.10 + - run: npm ci + - run: npm run check + env: + DIMCODE_TEST_DENO: deno + - run: npm pack diff --git a/README.md b/README.md index fa0944c3a4c..92c08d0581b 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ [Hardware](#hardware) • [Installation](#installation) • [Agent CLI & MCP](#agent-cli-and-mcp) • +[dimcode terminal agent](agent/README.md) • [Blueprints](#blueprints) • [dimTELE: Remote Teleop](#dimtele-remote-teleop) • [Development](#development) diff --git a/agent/.gitattributes b/agent/.gitattributes new file mode 100644 index 00000000000..300cb12cb93 --- /dev/null +++ b/agent/.gitattributes @@ -0,0 +1 @@ +npm-shrinkwrap.json -diff linguist-generated diff --git a/agent/.gitignore b/agent/.gitignore new file mode 100644 index 00000000000..b5fab8d947a --- /dev/null +++ b/agent/.gitignore @@ -0,0 +1,4 @@ +node_modules/ +dist/ +*.tgz +.test/ diff --git a/agent/LICENSE b/agent/LICENSE new file mode 100644 index 00000000000..5e2927e3adb --- /dev/null +++ b/agent/LICENSE @@ -0,0 +1,17 @@ + Apache License + Version 2.0, January 2004 + http://www.apache.org/licenses/ + + Copyright 2025 Dimensional Inc. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. diff --git a/agent/README.md b/agent/README.md new file mode 100644 index 00000000000..dccb5ce708b --- /dev/null +++ b/agent/README.md @@ -0,0 +1,176 @@ +# dimcode + +Persistent terminal agent for DimOS, built on Pi. The gateway runs independently of robot blueprints. Existing MCP exposes skills; Pi's coding tools use the existing DimOS CLI/Python APIs. Media uses the existing Web SDK only while a renderer needs it. + +## Install + +Test release: **`@spomichter/dimcode@next`**. Install on Linux x64/arm64 or Apple Silicon: + +```sh +curl -fsSL https://github.com/dimensionalOS/dimos/releases/download/dimcode-v0.1.0-next.1/install.sh | sh +``` + +The installer downloads and checks a private Node 24 runtime, installs the npm package and creates `~/.local/bin/dimcode`. It offers to open setup immediately. No system Node upgrade, sudo, source checkout or build commands are needed. If `~/.local/bin` is not on your PATH, use the absolute launcher path printed by the installer. + +With Node **24 or 26** already installed, npm also works: + +```sh +npm install -g https://registry.npmjs.org/@spomichter/dimcode/-/dimcode-0.1.0-next.1.tgz +dimcode setup +``` + +`setup` signs you in, selects a model and asks whether to start the gateway at login, then opens **agent-led DimOS setup** in the terminal. Drop a checkout/environment path or describe what you want to build. The agent reads DimOS's own installation instructions and uses its ordinary coding tools to inspect, install, verify and connect it. There is no hard-coded DimOS installer in dimcode. + +Run `dimcode` to launch again. Existing credentials, recordings and sessions are preserved. The shell installer itself starts no daemon or blueprint; setup offers the optional Linux user service. Windows, Homebrew and WinGet distribution are not part of this prerelease. + +`dimcode` opens the fullscreen Pi-based terminal: scrollable chat, a multiline editor with command/file completion, compact tool output, and a persistent model/status footer. Shift+Enter adds a line; Ctrl-C detaches from the running gateway. + +Memory results appear **inside the agent's tool card**, with an overview combining DimOS's timeline, spatial map and selected frames. Click a panel label or enter `/panel 1`, `/panel 2`, etc. to see a larger individual view; `/panel 0` returns to the overview. `/expand` shows the latest tool's full output and source hashes. Use a terminal with image support (such as Kitty) for the SVG/PNG views; other terminals show image metadata. + +To inspect a saved cloud without a model call, enter these commands **inside dimcode**: + +```text +/inspect /absolute/path/to/cloud.json +/view +``` + +Cloud exports use `{ "points": [[x, y, z]], "frame": "world", "timestamp": 123.45 }`. In the inspector, **←/→** rotates, **+/-** zooms, **g** switches graphics/Braille, **d** shows source/hash, **0** resets the view, and **Esc** returns to chat. An export can supply `selectedIndices` to highlight the exact selection produced by its owning operation. Rotation and zoom change the view only; memory analysis and filtering stay in DimOS. + +Point-cloud results from `dimcode_render` open the same inspector through `/view`. The terminal verifies the source hash before loading interactive geometry; if the file changed, the original tool preview remains available. `/expand` toggles details for the latest tool. Images use supported terminal graphics; Braille supports point-cloud inspection without image support. + +A fresh `dimcode` launch opens setup automatically. Repeating `dimcode setup` lets you change the choices; configured providers offer **Use configured credentials** so you do not need to paste a key again. Credentials never enter chat. + +```sh +dimcode setup # repeat interactive setup +dimcode tui # open the terminal (same as dimcode) +dimcode --help # all launch commands +``` + +The bundled [`dimensional-install` skill](skills/dimensional-install/SKILL.md) handles DimOS onboarding. Existing environments and editable checkouts are inspected and reused; a new install follows the selected version's README and OS-specific instructions, including its Python requirements and requested extras. Commands and failures appear in normal tool cards. The resulting conversation can be detached and resumed like any session. Agent-led setup needs a working model; `setup --provider NAME --key-env VAR` configures credentials without launching a model or installing DimOS. + +## What is installed / where is the source? + +This package uses **upstream Pi 0.85.1**, pinned as npm dependencies. It is not a Pi fork or a native binary. `dimcode` is a Node CLI with a Dimensional gateway, terminal frontend and extensions. Pi supplies the agent runtime, provider login, coding tools, message components and tool cards. A fork is unnecessary for these customizations. + +All source is in this repository's [`agent/src/`](src): [`main.ts`](src/main.ts) launches the CLI; [`setup.ts`](src/setup.ts) handles private bootstrap; [`skills/dimensional-install`](skills/dimensional-install) guides agent-led installation; [`terminal.ts`](src/terminal.ts) renders chat; [`gateway.ts`](src/gateway.ts) owns persistent sessions; [`media.ts`](src/media.ts) lazily connects the existing Web SDK. [`skills/dimensional`](skills/dimensional) supplies DimOS instructions. + +## Authentication and service + +API keys are entered outside chat and stored by Pi in the user-only auth file. An environment key can be installed with `dimcode setup --provider openai --key-env OPENAI_API_KEY`. ChatGPT subscription login uses `--provider openai-codex --oauth`. Use Anthropic API keys for Claude in dimcode. Refresh/logout use Pi's provider implementation. + +```sh +dimcode service install +dimcode service status +dimcode service uninstall +``` + +This is a Linux **user** service. Without it, first attach starts a detached gateway. `dimcode stop` stops that gateway; `dimcode gateway` runs it in the foreground for diagnostics, or reports that it is already running. `dimcode --foreground` runs gateway and terminal in one foreground process. + +Config/auth live in `$XDG_CONFIG_HOME/dimcode` (or `~/.config/dimcode`), sessions in `$XDG_STATE_HOME/dimcode`, previews in `$XDG_CACHE_HOME/dimcode`. `DIMCODE_HOME` overrides the config directory. The runtime socket is user-only. Service removal preserves sessions, credentials and DimOS data. + +## Connect and work + +```sh +dimcode connect go2 http://127.0.0.1:9990/mcp +dimcode relay http://127.0.0.1:7780 my-robot +dimcode workspace /path/to/app # save the default workspace +dimcode --cwd /path/to/app +dimcode sessions +dimcode --session SESSION_ID +dimcode --session SESSION_ID --view +dimcode run "inspect the app and explain its blueprint" +``` + +Endpoints are explicit; ports are examples, not instance identities. Use `/reload` after changing endpoints. Every advertised skill is registered with its original schema, metadata and remote routing. Tool names are bounded and collision-resistant. There is no static copy of the robot's tools or a parallel lifecycle service. + +Terminal commands: `/new`, `/sessions`, `/resume ID`, `/models`, `/model PROVIDER MODEL`, `/login PROVIDER [oauth]`, `/logout PROVIDER`, `/abort`, `/steer TEXT`, `/follow TEXT`, `/reload`, `/image PATH`, `/panel N`, `/inspect PATH`, `/view`, `/expand`, `/exit`. Ctrl-C detaches. One terminal owns input; other viewers may observe. Detaching keeps the turn running. Restart restores Pi history and never automatically replays external actions. Very large histories show a bounded recent transcript with an omission notice; the complete agent history remains on disk. Session events carry their source identity so switching sessions cannot mix transcripts. + +Pi owns context loading, skills, compaction, models and coding-tool behavior. Workspace instructions and configured Pi extensions load normally. Attached terminals support serialized dialogs/notifications; executable extension UI factories belong in the terminal renderer and cannot be sent through a socket. + +## Tool rendering + +DimOS owns reusable visualizations and encodings for agent understanding: geometry, axes, units, timestamps, legends and selections. Dimcode owns terminal styling and interaction: cards, themes, camera/point presentation, resizing, controls and graphics/Braille adaptation. Terminal styling preserves the meaning of colors and labels in supplied visuals. + +DimOS memory owns video/frame/point-cloud analysis. Evaluate the memory operation once with `materialize()`, then use its existing `Space.to_svg()` / `Plot.to_svg()` and selected-frame exports. Pass those exports together to `dimcode_render`: + +```json +{"kind":"image","title":"Memory · plant search","views":[{"label":"Timeline","path":"timeline.svg"},{"label":"Spatial","path":"space.svg"},{"label":"Frames","path":"frames.png"}]} +``` + +The tool rasterizes the original SVG/PNG views for terminal display and model context. Every view reaches the model; the terminal presents a selectable overview. Each export retains its source path and SHA-256. MCP tool results containing multiple images also appear together automatically. No query or filter runs inside the renderer. Existing skills that return only a pose or JSON still require an explicit export; the harness does not invent missing views. + +Built-in presentation stays constrained to **point clouds and images/SVGs**, with one player for finite sequences of either. `dimcode_render({path:"result.json"})` detects a cloud or frame index; other image paths are rasterized. Numeric plots and graphs use DimOS/Python SVG exports, not a harness plotting engine. Cloud JSON may also supply `colors: [[r,g,b]]` (0–255) and `selectedIndices`; source RGB is preserved and explicit selections are highlighted. + +For a memory/replay interval, materialize the selection in DimOS and export an index alongside the existing frames: + +```json +{ + "type": "points", + "timeOrigin": 1766747348.2995782, + "source": "Go2 memory · lidar · seconds 1–5", + "frames": [ + {"path":"cloud-0.json","timestamp":1766747349.3433642}, + {"path":"cloud-1.json","timestamp":1766747349.4723642} + ] +} +``` + +Use `type:"image"` for PNG/JPEG frames or any custom SVG animation. Paths are relative to the index, timestamps are seconds and must increase; optional per-frame `sha256` checks the original exports. Use one recording origin for synchronized camera/cloud windows. This file indexes already exported results; it is not a new query or transport API. + +Call `dimcode_render({path:"clip.json",title:"Memory · seconds 1–5"})`, or use these commands **inside dimcode** without a model call: + +```text +/inspect /absolute/path/to/clip.json +``` + +The latest clip loops automatically, including after reconnect, with no player buttons or timeline. Ask the agent “show a single frame at 2 seconds” to inspect a still; it selects the timestamped result and renders that frame. A single-frame result stays static. Leaving the session or closing the terminal stops playback. Point-cloud clips share one camera, bounds and height scale; mismatched coordinate frames must be aligned in DimOS first. + +The terminal plays timestamped PNGs. A looping GIF is exported for sharing (GIF timing rounds to centiseconds); the model receives a contact sheet of up to six labeled frames. `dimcode_render({path:"clip.json",frame:17})` returns original zero-based frame 17 for closer inspection, even if preview sampling omitted it. The renderer reads saved files only and never repeats the source query. + +Previews are bounded to 60 seconds, at most 120 frames and approximately 10 Hz, retaining first/last timestamps and original indexes. Source-frame gaps remain visible in playback. Per-file reads are limited to 32 MiB, a selected clip to 128 MiB/two million points, generated PNGs to 64 MiB and GIFs to 32 MiB. If a limit is reached, export a smaller preview with DimOS. Individual clouds label point display sampling. Original recordings and exports remain unchanged. + +The agent is instructed to visualize each meaningful sensor/memory operation. When no supported type or existing visualizer applies, it generates a self-contained SVG with inline Python from the evaluated result and displays it through `dimcode_render({kind:"image",path:"result.svg"})`. This supports arbitrary plots, images, simple graphs and labeled proposed overlays. The same rendered image is returned to the model for visual inspection. Rendering failures remain explicit. + +Live tools select an existing relay/robot/channel. The terminal receives frames directly through the Web SDK and coalesces drawing to 10 Hz. The gateway retains one final snapshot for model context. Closing/cancelling the tool releases consumers; the last consumer closes the connection. MediaPool is generic over decoded SDK slots and accepts existing decoder registries. The initial live image renderer handles JPEG; other channel types use their owning decoder/renderer or saved exports. + +Graphics use Pi terminal-image support with text fallback. In Warp, terminal images are rendered as complete cell-row slices so partial scrolling crops the image without compressing its aspect ratio. Derived PNGs/GIFs have a bounded 128 MiB cache; evicted previews show an explicit unavailable state and can be regenerated from retained exports; original DimOS recordings stay with DimOS. No recording, raw continuous video, new transport protocol or new DimOS gateway is introduced. + +## Contributor development and validation + +These commands are for working on the source, not the user installation: + +```sh +cd agent +npm ci +npm run check +node dist/main.js setup +node dist/main.js +``` + +These commands run the source build without installing the `dimcode` command. `npm pack` produces the distributable tarball. `npm-shrinkwrap.json` pins the published dependency tree. + +Prereleases are published by [dimcode-release.yml](../.github/workflows/dimcode-release.yml) from `dimcode-vVERSION` tags matching the package's `-next.N` version. The workflow checks Linux/macOS, publishes to npm's `next` tag using `DIMCODE_NPM_TOKEN`, then creates a GitHub prerelease with the tarball and installer. Ordinary branch pushes do not publish. The personal npm scope is for this test release. + +Installer options: `DIMCODE_VERSION` selects a version/tag (default `next`), `DIMCODE_INSTALL_DIR` selects private runtime/releases, `DIMCODE_BIN_DIR` selects the launcher directory, and `DIMCODE_SETUP=0` skips the setup offer. Node downloads and npm tarballs are verified before installation. The installer resolves npm’s version metadata directly and checks the package SHA-512, so a newly published release does not depend on npm’s package-index propagation. Run the installer again to update the launcher; older releases remain available to running sessions. Existing gateways continue using their current release until restarted with `dimcode service install` after active turns finish. + +To run the complete integration suite with an existing DimOS environment and Go2 recording: + +```sh +DIMCODE_TEST_PYTHON=/path/to/dimos/.venv/bin/python \ +DIMCODE_TEST_DENO=/path/to/deno \ +DIMCODE_TEST_GO2_DB=/path/to/go2_bigoffice.db \ +npm test +``` + +The Go2 E2E test starts the standard `unitree-go2` blueprint in **recorded-data replay**, composed with the existing cockpit relay bridge. It starts its own relay and isolates transport discovery. No robot hardware or model API key is required. It verifies: + +- Multiple distinct, successfully decoded JPEG video frames and timestamped odometry. +- Full XYZ float32 point clouds, every coordinate finite, source timestamps/frame metadata, and byte-for-byte source SHA-256 matches. A test-only encoder/SDK decoder uses the existing codec registry; it does not mislabel the default XY lidar projection as XYZ. +- One shared SDK connection, lazy subscriptions, continued cloud reception after video closes, then zero viewers/subscriptions after the last renderer closes. +- Cleanup of the blueprint and relay, including failure paths. + +A separate memory test materializes seconds 1–5 once, exports full cloud/camera frames and a native DimOS plot, then checks GIF frame counts, shared time origin and every preview frame’s source hash. Playback unit tests cover looping, cancellation, stale files, RGB preservation and disposal. A scrolling regression checks exact visible pixels at multiple scroll positions and widths. + +Set `DIMCODE_TEST_REPORT=/absolute/path/report.json` to save measured counts. Without `DIMCODE_TEST_GO2_DB`, the large recording test is explicitly skipped. The Python and Deno variables separately enable the MCP-handler and QUIC relay tests. Unit/CLI tests also cover onboarding, private credentials, cancellation, `tui`, existing-gateway handling, session ownership, detach/recovery and renderer provenance. + +A video stream here means consecutive JPEG frames over WebTransport. This does not claim an H.264/WebCodecs decoder, browser UI coverage, or a physical robot test. The harness's initial live terminal renderer displays JPEG; saved point clouds, timed sequences and SVGs render from exported results. Other live types can plug into the generic SDK codec registry. diff --git a/agent/build.mjs b/agent/build.mjs new file mode 100644 index 00000000000..89ddb0e342b --- /dev/null +++ b/agent/build.mjs @@ -0,0 +1,23 @@ +import { build } from "esbuild"; +import { chmod, mkdir, rm } from "node:fs/promises"; +import { resolve } from "node:path"; + +await rm("dist", { recursive: true, force: true }); +await mkdir("dist", { recursive: true }); +await build({ + entryPoints: ["src/main.ts"], + outdir: "dist", + bundle: true, + platform: "node", + target: "node24", + format: "esm", + packages: "external", + sourcemap: true, + alias: { + "@dimos/sdk": resolve("../web/sdk/src/index.ts"), + "@dimos/shared/manifest": resolve("../web/shared/manifest.ts"), + "@dimos/shared": resolve("../web/shared/protocol.ts"), + }, + banner: { js: "#!/usr/bin/env node" }, +}); +await chmod("dist/main.js", 0o755); diff --git a/agent/install.sh b/agent/install.sh new file mode 100644 index 00000000000..487d4b5bf2b --- /dev/null +++ b/agent/install.sh @@ -0,0 +1,96 @@ +#!/bin/sh +# Installs the harness only. DimOS installation belongs to the agent's skill. +set -eu + +main() { + package='@spomichter/dimcode' + version=${DIMCODE_VERSION:-next} + data=${DIMCODE_INSTALL_DIR:-${XDG_DATA_HOME:-$HOME/.local/share}/dimcode} + bin=${DIMCODE_BIN_DIR:-$HOME/.local/bin} + case "$data:$bin" in /*:/*) ;; *) echo 'Install directories must be absolute.' >&2; exit 1 ;; esac + case "$(uname -s)-$(uname -m)" in + Linux-x86_64) platform=linux-x64 ;; + Linux-aarch64|Linux-arm64) platform=linux-arm64 ;; + Darwin-arm64) platform=darwin-arm64 ;; + *) echo 'This prerelease supports Linux x64/arm64 and Apple Silicon.' >&2; exit 1 ;; + esac + case "$version" in ''|*[!a-zA-Z0-9._-]*) echo 'Invalid DIMCODE_VERSION.' >&2; exit 1 ;; esac + command -v curl >/dev/null || { echo 'curl is required.' >&2; exit 1; } + mkdir -p "$data" "$bin" + staging=$(mktemp -d "$data/.install.XXXXXX") + trap 'rm -rf "$staging"' EXIT + trap 'exit 130' INT + trap 'exit 143' TERM + printf '\ndimcode · Install your Dimensional agent\n\n' + + node="$data/node/bin/node" + if ! "$node" -e 'process.exit(+process.versions.node.split(".")[0] === 24 ? 0 : 1)' 2>/dev/null; then + dist=https://nodejs.org/dist/latest-v24.x + printf 'Installing a private Node 24 runtime…\n' + curl -fsSL "$dist/SHASUMS256.txt" -o "$staging/checksums" + archive=$(awk -v suffix="-$platform.tar.gz" '$2 ~ /^node-v[0-9.]+-/ && substr($2,length($2)-length(suffix)+1)==suffix {print $2; exit}' "$staging/checksums") + [ -n "$archive" ] || { echo 'No compatible Node archive found.' >&2; exit 1; } + curl -fsSL "$dist/$archive" -o "$staging/$archive" + awk -v file="$archive" '$2==file {print}' "$staging/checksums" > "$staging/selected.sha256" + if command -v sha256sum >/dev/null 2>&1; then + (cd "$staging" && sha256sum -c selected.sha256) + elif command -v shasum >/dev/null 2>&1; then + (cd "$staging" && shasum -a 256 -c selected.sha256) + else + echo 'sha256sum or shasum is required to verify Node.' >&2; exit 1 + fi + tar -xzf "$staging/$archive" -C "$staging" + if [ -e "$data/node" ] || [ -L "$data/node" ]; then + echo "Existing runtime at $data/node is unusable; choose another DIMCODE_INSTALL_DIR." >&2 + exit 1 + fi + mv "$staging/${archive%.tar.gz}" "$data/node" + fi + PATH="$data/node/bin:$PATH" + export PATH + npm="$data/node/bin/npm" + curl -fsSL "https://registry.npmjs.org/$package/$version" -o "$staging/metadata.json" + resolved=$("$node" -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1])).version' "$staging/metadata.json") + case "$resolved" in ''|*[!a-zA-Z0-9._-]*) echo 'Invalid package version from registry.' >&2; exit 1 ;; esac + release="$data/releases/$resolved" + entry="$release/node_modules/$package/dist/main.js" + if [ ! -f "$entry" ]; then + printf 'Installing %s@%s…\n' "$package" "$resolved" + tarball=$("$node" -p 'JSON.parse(require("node:fs").readFileSync(process.argv[1])).dist.tarball' "$staging/metadata.json") + case "$tarball" in https://registry.npmjs.org/*) ;; *) echo 'Unexpected package registry.' >&2; exit 1 ;; esac + curl -fsSL "$tarball" -o "$staging/dimcode.tgz" + "$node" - "$staging/metadata.json" "$staging/dimcode.tgz" "$package" <<'NODE' +const fs = require('node:fs'); +const crypto = require('node:crypto'); +const metadata = JSON.parse(fs.readFileSync(process.argv[2])); +const actual = 'sha512-' + crypto.createHash('sha512').update(fs.readFileSync(process.argv[3])).digest('base64'); +if (metadata.name !== process.argv[4] || actual !== metadata.dist.integrity) throw new Error('Package integrity check failed'); +NODE + "$npm" install --prefix "$staging/package" --ignore-scripts --min-release-age=0 --omit=dev --no-audit --no-fund "$staging/dimcode.tgz" + "$node" "$staging/package/node_modules/$package/dist/main.js" --help >/dev/null + mkdir -p "$data/releases" + [ ! -e "$release" ] || { echo "Incomplete release at $release; choose another DIMCODE_INSTALL_DIR." >&2; exit 1; } + mv "$staging/package" "$release" + fi + "$node" - "$bin/dimcode" "$node" "$entry" <<'NODE' +const fs = require('node:fs'); +const [dest, node, entry] = process.argv.slice(2); +const quote = (value) => "'" + value.replaceAll("'", "'\\''") + "'"; +const temporary = `${dest}.install-${process.pid}`; +fs.writeFileSync(temporary, `#!/bin/sh\nexec ${quote(node)} ${quote(entry)} "$@"\n`, { mode: 0o755, flag: 'wx' }); +fs.renameSync(temporary, dest); +NODE + printf '\nInstalled %s@%s\n' "$package" "$resolved" + case ":${PATH}:" in + *":$bin:"*) printf 'Run: dimcode setup\nThen: dimcode\n' ;; + *) printf 'Run: %s/dimcode setup\nAdd %s to your shell PATH to use the short command.\n' "$bin" "$bin" ;; + esac + printf 'No daemon or robot was started. Setup asks about gateway startup.\n' + if [ "${DIMCODE_SETUP:-1}" = 1 ] && ( : /dev/null; then + printf '\nOpen interactive setup now? [Y/n] ' >/dev/tty + IFS= read -r answer =24 <25 || >=26 <27" + } + }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.123.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.123.0.tgz", + "integrity": "sha512-Y9oX9mPNGZClHQOFqrWRk43Srcu/UHuPq3rfxxOq7JgW0gi+lJA2MAOK4Ul3k/+AUrwRWFJvd0tK3oC0Pw25dw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "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/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/@aws-sdk/core": { + "version": "3.978.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/core/-/core-3.978.0.tgz", + "integrity": "sha512-2yX9LUmxPklVjSGTb8dfnWRJSiFQ3TeH2nn7G1mdKHTfnabzF0+gfrS8rYfLWmZrQ8A3mEcxMJjRc51dL5KWaA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@aws-sdk/xml-builder": "^3.972.40", + "@aws/lambda-invoke-store": "^0.3.0", + "@smithy/core": "^3.33.3", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "bowser": "^2.11.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-env": { + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-env/-/credential-provider-env-3.972.71.tgz", + "integrity": "sha512-JN+JHruYZw3GUZB8YGAlDk4wTDPOEAEEdEzj5nS0xodWR4smzHsN7PnK2j6IeOsDIj2aqua5DSbhXl9Gtf90FQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http": { + "version": "3.972.73", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-http/-/credential-provider-http-3.972.73.tgz", + "integrity": "sha512-uyYYnJOnlis8uQzaYGPd7N1JoioCoNpXgnkXYixsWJXHXgXyYi8WXJSDfofxJeWfQIGWLe2Nwyq60Uc7MZdVOg==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-http/node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-ini": { + "version": "3.973.16", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-ini/-/credential-provider-ini-3.973.16.tgz", + "integrity": "sha512-i++ly+0Uxa+u3ebSSyr0S/3CFhFJDxCXT3+Zj+mW2bXenEx5bKGCdTIKFu39SgXBNhWDjex/8cXUx9MUTMCrTw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/credential-provider-env": "^3.972.71", + "@aws-sdk/credential-provider-http": "^3.972.73", + "@aws-sdk/credential-provider-login": "^3.972.78", + "@aws-sdk/credential-provider-process": "^3.972.71", + "@aws-sdk/credential-provider-sso": "^3.973.15", + "@aws-sdk/credential-provider-web-identity": "^3.972.77", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-login": { + "version": "3.972.78", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-login/-/credential-provider-login-3.972.78.tgz", + "integrity": "sha512-eUtswnXu0+Ii9ieRK+0L7aPFV3Z/dnW2VntJzjBP9xs8s+8p5nBNuymIXtXwZ+5r5+XJP3e32nMkuZ/r0HozEA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-node": { + "version": "3.972.83", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-node/-/credential-provider-node-3.972.83.tgz", + "integrity": "sha512-jdso7ejzfRnatxMUZK4S/U6KbaDPCvfIV4XL+IQAPFDBt5rj5Fq595euqlK8Le4lNCMFR9oUpt+1l0aMgaayOQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/credential-provider-env": "^3.972.71", + "@aws-sdk/credential-provider-http": "^3.972.73", + "@aws-sdk/credential-provider-ini": "^3.973.16", + "@aws-sdk/credential-provider-process": "^3.972.71", + "@aws-sdk/credential-provider-sso": "^3.973.15", + "@aws-sdk/credential-provider-web-identity": "^3.972.77", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/credential-provider-imds": "^4.4.16", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-process": { + "version": "3.972.71", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-process/-/credential-provider-process-3.972.71.tgz", + "integrity": "sha512-lYmXJa4gvq4xN1lrT5NiP5vIYYKcGWAdj8y+8o6dlcateB5eF3Dn8DtmjjHKfMBrTPAMr2pebIiX/UOj8c1/UA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso": { + "version": "3.973.15", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-sso/-/credential-provider-sso-3.973.15.tgz", + "integrity": "sha512-6Jhcf4v0pSFdjk1EW2kvzuEBKD+UZ2uNcHUIglKKLndD20YhvkL2kdmDOV5/j4mYuWWwe/a1FQ1aomU86/Cg5Q==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/token-providers": "3.1129.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-sso/node_modules/@aws-sdk/token-providers": { + "version": "3.1129.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1129.0.tgz", + "integrity": "sha512-Sbl3rpzQdsG4ZK2zh0JWUYyZPKKorJlVOddA2T0DVbKJFrsW8J6wgnslxxUH04+WaBMr4A1HzJZvZX0xUvkniA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/credential-provider-web-identity": { + "version": "3.972.77", + "resolved": "https://registry.npmjs.org/@aws-sdk/credential-provider-web-identity/-/credential-provider-web-identity-3.972.77.tgz", + "integrity": "sha512-uylIQSUWpfLuH2LovxEEfwzJGM/SabLOfLMg6YXu/E8jJEKUdpdILCVCQCdFvHyu/7dLJOHPMfrSwduxO56NkQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/nested-clients": "^3.997.45", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/eventstream-handler-node": { + "version": "3.972.34", + "resolved": "https://registry.npmjs.org/@aws-sdk/eventstream-handler-node/-/eventstream-handler-node-3.972.34.tgz", + "integrity": "sha512-cTeVzpu1xEAkryTZBYhGwnQ6gOGyp8ZYZvmn0Sg/nI/ABmy/CRHHxPDJDUi9PxwxUtGGaatvfRUB3FCgT/rSWw==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-eventstream": { + "version": "3.972.29", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-eventstream/-/middleware-eventstream-3.972.29.tgz", + "integrity": "sha512-dlRzHCgyB8W6hLuDC5pcT5q+ziPt00n4QGgGBE17ucLVU4zMa6lsbuUdQ2Pm75Z5VA8GF+R/+SgrRcaTdIzSIQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/middleware-websocket": { + "version": "3.972.53", + "resolved": "https://registry.npmjs.org/@aws-sdk/middleware-websocket/-/middleware-websocket-3.972.53.tgz", + "integrity": "sha512-bIrDaMENQmYRBHntOiOheqkiw5+fhKW4Lqb+mS1uqF0VwvdWI22fW2HFgWrng66CmYd+4k8ePlpj38sEfTuMLQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients": { + "version": "3.997.45", + "resolved": "https://registry.npmjs.org/@aws-sdk/nested-clients/-/nested-clients-3.997.45.tgz", + "integrity": "sha512-mooq9Q+jLa18VoM7HouczmslZU60iiB0aKc/Ztnq/luIL1ud0z4DnYprLR/ZO1gp331S9tJctM1HZr7u6YKBXQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.978.0", + "@aws-sdk/signature-v4-multi-region": "^3.996.46", + "@aws-sdk/types": "^3.974.5", + "@smithy/core": "^3.33.3", + "@smithy/fetch-http-handler": "^5.7.2", + "@smithy/node-http-handler": "^4.11.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/nested-clients/node_modules/@smithy/node-http-handler": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.12.1.tgz", + "integrity": "sha512-ThMkboGeONWXAelq9FvGsuJC4rOi+qyC4/zhUF58xYpxUg5sQKx2VXZYJmtNjr4dSuBJ1HeJXETQILCz3wOHvw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@aws-sdk/signature-v4-multi-region": { + "version": "3.996.46", + "resolved": "https://registry.npmjs.org/@aws-sdk/signature-v4-multi-region/-/signature-v4-multi-region-3.996.46.tgz", + "integrity": "sha512-L+2xZTye/2T96f3lwCws0Zw6GG2JHZW9e8FpVgGBeeExSKyeoZ6CWRpBml/7DNiK/O26jrgPM9F+Ay8VkgzUWQ==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/types": "^3.974.5", + "@smithy/signature-v4": "^5.6.12", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/token-providers": { + "version": "3.1048.0", + "resolved": "https://registry.npmjs.org/@aws-sdk/token-providers/-/token-providers-3.1048.0.tgz", + "integrity": "sha512-k0y/GcuesuSfWyUM0WamrGyeZmltRYaPbHO82UDA6mZ/doB+FOHKutikPAtSXMn/hDz970cF+iRuuiYO9VEbAA==", + "license": "Apache-2.0", + "dependencies": { + "@aws-sdk/core": "^3.974.11", + "@aws-sdk/nested-clients": "^3.997.9", + "@aws-sdk/types": "^3.973.8", + "@smithy/core": "^3.24.2", + "@smithy/types": "^4.14.1", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/types": { + "version": "3.974.5", + "resolved": "https://registry.npmjs.org/@aws-sdk/types/-/types-3.974.5.tgz", + "integrity": "sha512-LkwLL2BLbC6wNNm4JaH9mbEqBMdOZCct6VAYqhdN4U1xrWM+fUJQEfbHwQgDypapOWTRtlk25akb5afM0P8CIQ==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/util-locate-window": { + "version": "3.965.10", + "resolved": "https://registry.npmjs.org/@aws-sdk/util-locate-window/-/util-locate-window-3.965.10.tgz", + "integrity": "sha512-ycwH6Zd2GhuSqdXX9ihbCjeGTB6xOJs+O3+Jb8/zDG9978XU80qs75dfkPJRMNKe5MvBZPuNeFpd4JZKPoUF4g==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=20.0.0" + } + }, + "node_modules/@aws-sdk/xml-builder": { + "version": "3.972.40", + "resolved": "https://registry.npmjs.org/@aws-sdk/xml-builder/-/xml-builder-3.972.40.tgz", + "integrity": "sha512-wlFmCIGUlwF4zx/kncw+bmxTQh1HeSJq4mYV/V5cZUSJadDP3kXvGW8Rn21cimj/7y9ju+47oYWXi97vF7czaA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.17.2", + "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-ai": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.85.1.tgz", + "integrity": "sha512-+VgVIJDkDO2efYJKEEqvPTH4zmnIaXdAppGbO+vKFA9qy5PdhFiAenuFAkU+oiCSfOC4dMHDyrjdQeL4ZoC5CQ==", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.123.0", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@earendil-works/pi-telemetry": "^0.85.1", + "@google/genai": "1.52.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.40.0", + "partial-json": "0.1.7", + "typebox": "1.3.7" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-coding-agent/-/pi-coding-agent-0.85.1.tgz", + "integrity": "sha512-FGRN+OHbWaefBPGaTggAdLjrIHW+s2PzLyglz/5dfLzb9of7uuXMXYC0fJIeZTw+shS32o2cuQ9jF7YSDuL/oQ==", + "hasShrinkwrap": true, + "license": "MIT", + "dependencies": { + "@earendil-works/chord": "^0.85.1", + "@earendil-works/pi-agent-core": "^0.85.1", + "@earendil-works/pi-ai": "^0.85.1", + "@earendil-works/pi-tui": "^0.85.1", + "@silvia-odwyer/photon-node": "0.3.4", + "chalk": "5.6.2", + "cross-spawn": "7.0.6", + "diff": "8.0.4", + "grok-mermaid": "0.2.2", + "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.3.7", + "undici": "8.9.0", + "yaml": "2.9.0" + }, + "bin": { + "pi": "dist/bundle/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.123.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.123.0.tgz", + "integrity": "sha512-Y9oX9mPNGZClHQOFqrWRk43Srcu/UHuPq3rfxxOq7JgW0gi+lJA2MAOK4Ul3k/+AUrwRWFJvd0tK3oC0Pw25dw==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1", + "standardwebhooks": "^1.0.0" + }, + "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/chord": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/chord/-/chord-0.85.1.tgz", + "license": "MIT", + "dependencies": { + "esbuild": "0.28.1" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-agent-core": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-agent-core/-/pi-agent-core-0.85.1.tgz", + "license": "MIT", + "dependencies": { + "@earendil-works/chord": "^0.85.1", + "@earendil-works/pi-ai": "^0.85.1", + "@earendil-works/pi-telemetry": "^0.85.1", + "diff": "8.0.4", + "ignore": "7.0.5", + "typebox": "1.3.7", + "yaml": "2.9.0" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-ai": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-ai/-/pi-ai-0.85.1.tgz", + "license": "MIT", + "dependencies": { + "@anthropic-ai/sdk": "0.123.0", + "@aws-sdk/client-bedrock-runtime": "3.1048.0", + "@earendil-works/pi-telemetry": "^0.85.1", + "@google/genai": "1.52.0", + "@smithy/node-http-handler": "4.7.3", + "http-proxy-agent": "7.0.2", + "https-proxy-agent": "7.0.6", + "openai": "6.40.0", + "partial-json": "0.1.7", + "typebox": "1.3.7" + }, + "bin": { + "pi-ai": "dist/cli.js" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-telemetry": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.85.1.tgz", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@earendil-works/pi-tui": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.85.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/@esbuild/aix-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.1.tgz", + "integrity": "sha512-Svl7tq8k/08+p6CXPpRjQ1fKX+1odH/BQbb48fV6fj3CWHhsoIOoY87w1oHXm0qEpkIK3ZfVgp0hed3XBXzXMQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/android-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.1.tgz", + "integrity": "sha512-0k2F129Xdio1TdJfzJ8sy1Q47vUD2NnwdhiAf7drUN1EBTfPf4hsFCtmMgu/6m8JSzsBrlmVjudMBQqOfG8usQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/android-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.1.tgz", + "integrity": "sha512-34EGEbCIAgosYz6goLcopX6Mo7NyGv9tfwEM2/7Ce2VcVRk568iSvniGWcUXIy7wEDR1wzolcxcriFVrWYcwBg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/android-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.1.tgz", + "integrity": "sha512-dbwY7ltSMDWsRatcRpCnES4F+im88OCUgGZjy52shC7GqHRE/cYlxNbB4Z4UpJswpcc4Qxd2oE/ufM0p61IKng==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/darwin-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.1.tgz", + "integrity": "sha512-TZbWkQY7kvTAXbXUT7uVACR5cMHsDiSz9z7ZKAX/RTq/WJEk3QyRr0wZpNhBDX+/0CtdqUIJlOiodQcta6tY3Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/darwin-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.1.tgz", + "integrity": "sha512-zfdzgK9ACBNZLI/CyHTOx81SyNbM6YXn7rxSgX97VjyiPl9W1i4Ka4fgKECEoFCKGpvBj5qArWIGgQjOwkgskQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.1.tgz", + "integrity": "sha512-wG2EA8ENdEI0qhkSZMjfqrdY+ziCYCPMmtZjjIwOmXFjmyzEHn+UUxk5of+SYsjtfs3VpnlC7QLzSI5hY/rOAw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/freebsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.1.tgz", + "integrity": "sha512-i7dZ9vQgnvSCzi/rYCXNgtF/U+eKZNJBzu3eTQbRgHnM7tNSizLOkRFAl3qzVc/Op/u5YkHHa4pf/3DOYHthLQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-arm": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.1.tgz", + "integrity": "sha512-qVXBOHQS+d5Y722GwJzJUtOLlX7km3CraOaGormF1pDtPd2C/l1SHRPgjLunLGe51Sh5YYWKMFDyV4SxgMQYTQ==", + "cpu": [ + "arm" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.1.tgz", + "integrity": "sha512-yHs+0uc8+nvEAfAfxrWQKK5peSNzBc4PegcMO0EJ2hT71uA7vB8Ihg2e77R2P7SG5uYjPbHlLLmve4LLLRCf0g==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.1.tgz", + "integrity": "sha512-d1z4ZuP0ajrfz/FhGT4vv278rX8KnPPJx8i5+AtK7TYbx9Le9F1hyzurZpkEyjkGa9dUGhQow4C1NmeGvqxN2w==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-loong64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.1.tgz", + "integrity": "sha512-M5sRjUVZrkm1OAPR3dlOYzNmN+loZKGVi1VUQGrwuqLcbR6qeAz+famMhjASeH3YVKvZz+zT1jlh/keC3Rj/lg==", + "cpu": [ + "loong64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-mips64el": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.1.tgz", + "integrity": "sha512-mRObBZeHh2OxcBFPWE/FjylkRgZdYuiTR3vaTozquCGOH14iP9oN4x4Ge81CoIDYQrXmIxpFumJBu5MtZpnQJQ==", + "cpu": [ + "mips64el" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-ppc64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.1.tgz", + "integrity": "sha512-slScBsMAb3GFDcdrCgLwZtPYRoH2H/youv10QiZyRjmsP48fznoveWytSgCI/R0ZcUgpc0ZhIUEx6LHts8yrfQ==", + "cpu": [ + "ppc64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-riscv64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.1.tgz", + "integrity": "sha512-kw0owk1o0GFETUJyW0jc0G4Yzs0BHZn0JDZ8JRT088vjJYX777BAs1fDGxAC+q831qOs2DTC96mNsG2opdfyyQ==", + "cpu": [ + "riscv64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-s390x": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.1.tgz", + "integrity": "sha512-/lAIjX8aYFRByhh6L5rYtPEDRqa9de/4V/juOXcta5frjvzXO4/sqEtyytse0g3zZFuWu5cDN0MkLz2qRDD2Ag==", + "cpu": [ + "s390x" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/linux-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.1.tgz", + "integrity": "sha512-u/anNYF2mmVOEDwLtnQ1wOr3EZ9sTNGLWrsYGYwHWzGA3Si84IOkHXlbWTD1NB+9/1lcnweYKO54uhxZydNzfA==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.1.tgz", + "integrity": "sha512-oks0DYbLwWMmaakTsCb+zL4E+aHRVLom9IJZOAthMQEPiQmydXHkziYEsGYRx0uNV/IjEKGAV941JzH02pflqw==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/netbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.1.tgz", + "integrity": "sha512-aeL6lAnN89Hz43Mlh1G8ARasbuoYvSITDEx0tHh5b7jJnHcssqgjy9Yx430GDpmCa6OyrKoS0aNRjKundRizGg==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.1.tgz", + "integrity": "sha512-MEFJe5C3R8pwXdZ5Y21oo6m7ePiS0d9pWucn99O/wvyJZChoIQKrQDxKrGeW8F5+T0okTHesAmDeiHDTIq0V/Q==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/openbsd-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.1.tgz", + "integrity": "sha512-i/ZLIOafE0Z8cI/XANJAixoJL/uRAoS2xOA3rb0xN+KK0K177cMAsQYkzHtBrtMXAKuAc7HGgcWiZ/sRC1Nxgw==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.1.tgz", + "integrity": "sha512-ge+Z7EXFNt2BO1oAMsVpiQ8EwndV9i1xXerAeTIK7AtPs3bKFXQM7nlRxDSIUIMeueR1CNXxqztLzdNeReKBJg==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/sunos-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.1.tgz", + "integrity": "sha512-BEjgtECkL3vY+SaSQ6nzVfiALUeFxpawyp8Jmf5PtYhf1Ug40N1h/hxlhts+f1FvSvarEigdxS3BlSMI2PJLcQ==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/win32-arm64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.1.tgz", + "integrity": "sha512-lCv9eK/H6ZJWbE7bh2nw54CZ9M2nupBxJcTsdk/QQnWkdSjKGuxmmH8/GWrlT1eMmZfn4dGcCjRte397WqfQXA==", + "cpu": [ + "arm64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/win32-ia32": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.1.tgz", + "integrity": "sha512-zvb/mB2bSCoJOpoCBgYKKpX6YM6mJBlBUVUtVj41DlZJVEB6/0CKlRYxP5wWl1C1ILiCoAU5wZZ4q1P3qeS6Eg==", + "cpu": [ + "ia32" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/@esbuild/win32-x64": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.1.tgz", + "integrity": "sha512-bm4Mowrv+GXMlpWX++EcXw/iLyd1o3+bJkC2DkWXYVvgZCqD/bSj9ctZeAMC3cIxgjRVR2Dufaiu4YPxr5gW1A==", + "cpu": [ + "x64" + ], + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "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/@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/@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/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "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.9", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", + "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "license": "MIT", + "dependencies": { + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "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/esbuild": { + "version": "0.28.1", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", + "integrity": "sha512-HrJrvZv5ayxBzPfwphOoNzkzOIIlifzk0KJrGK2c8R4+LKpMtpYLQeUdjnwjWv/LZlkH2laZk+4w78pi99D4Vw==", + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.1", + "@esbuild/android-arm": "0.28.1", + "@esbuild/android-arm64": "0.28.1", + "@esbuild/android-x64": "0.28.1", + "@esbuild/darwin-arm64": "0.28.1", + "@esbuild/darwin-x64": "0.28.1", + "@esbuild/freebsd-arm64": "0.28.1", + "@esbuild/freebsd-x64": "0.28.1", + "@esbuild/linux-arm": "0.28.1", + "@esbuild/linux-arm64": "0.28.1", + "@esbuild/linux-ia32": "0.28.1", + "@esbuild/linux-loong64": "0.28.1", + "@esbuild/linux-mips64el": "0.28.1", + "@esbuild/linux-ppc64": "0.28.1", + "@esbuild/linux-riscv64": "0.28.1", + "@esbuild/linux-s390x": "0.28.1", + "@esbuild/linux-x64": "0.28.1", + "@esbuild/netbsd-arm64": "0.28.1", + "@esbuild/netbsd-x64": "0.28.1", + "@esbuild/openbsd-arm64": "0.28.1", + "@esbuild/openbsd-x64": "0.28.1", + "@esbuild/openharmony-arm64": "0.28.1", + "@esbuild/sunos-x64": "0.28.1", + "@esbuild/win32-arm64": "0.28.1", + "@esbuild/win32-ia32": "0.28.1", + "@esbuild/win32-x64": "0.28.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-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "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/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/grok-mermaid": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/grok-mermaid/-/grok-mermaid-0.2.2.tgz", + "integrity": "sha512-XcJEP5dDC8liHBh52mlLjU18fNvu1ckFsu0QpIG3+APZ270fsj9wxpiA6cOURmbUEuoMVgjbC2+UYgTdCqqgzA==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + } + }, + "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/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.40.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", + "license": "Apache-2.0", + "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/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.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/@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/standardwebhooks": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz", + "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "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.3.7", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", + "license": "MIT" + }, + "node_modules/@earendil-works/pi-coding-agent/node_modules/undici": { + "version": "8.9.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.9.0.tgz", + "integrity": "sha512-aWZpUj7XoGonMClx4gdDRfgBjqeA+F473aDmROQQbM9n6PRfK/u1q/a0X4wMTgcHfT8H6fpbt98PFuDUwFg2YA==", + "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-telemetry": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-telemetry/-/pi-telemetry-0.85.1.tgz", + "integrity": "sha512-Bg/YN6kA7Swja/NQxka8xFdecb4E/auIEGF2G5A25EaQXhRnPj300/7/KpgsDDMYUzHTDAv4RyUxaQPJKW81Rw==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@earendil-works/pi-tui": { + "version": "0.85.1", + "resolved": "https://registry.npmjs.org/@earendil-works/pi-tui/-/pi-tui-0.85.1.tgz", + "integrity": "sha512-OIzw9efInmO4WOBnD4TxcTdBjmzvYJpzslkgoUro946nEGoYWg5rwv1p4fDt3/JvMx9QybryUCUwlm7j8Dreig==", + "license": "MIT", + "dependencies": { + "get-east-asian-width": "1.6.0", + "marked": "18.0.5" + }, + "engines": { + "node": ">=22.19.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.12.tgz", + "integrity": "sha512-Hhmwd6CInZ3dwpuGTF8fJG6yoWmsToE+vYgD4nytZVxcu1ulHpUQRAB1UJ8+N1Am3Mz4+xOByoQoSZf4D+CpkA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.12.tgz", + "integrity": "sha512-VJ+sKvNA/GE7Ccacc9Cha7bpS8nyzVv0jdVgwNDaR4gDMC/2TTRc33Ip8qrNYUcpkOHUT5OZ0bUcNNVZQ9RLlg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.12.tgz", + "integrity": "sha512-6AAmLG7zwD1Z159jCKPvAxZd4y/VTO0VkprYy+3N2FtJ8+BQWFXU+OxARIwA46c5tdD9SsKGZ/1ocqBS/gAKHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.12.tgz", + "integrity": "sha512-5jbb+2hhDHx5phYR2By8GTWEzn6I9UqR11Kwf22iKbNpYrsmRB18aX/9ivc5cabcUiAT/wM+YIZ6SG9QO6a8kg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.12.tgz", + "integrity": "sha512-N3zl+lxHCifgIlcMUP5016ESkeQjLj/959RxxNYIthIg+CQHInujFuXeWbWMgnTo4cp5XVHqFPmpyu9J65C1Yg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.12.tgz", + "integrity": "sha512-HQ9ka4Kx21qHXwtlTUVbKJOAnmG1ipXhdWTmNXiPzPfWKpXqASVcWdnf2bnL73wgjNrFXAa3yYvBSd9pzfEIpA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.12.tgz", + "integrity": "sha512-gA0Bx759+7Jve03K1S0vkOu5Lg/85dou3EseOGUes8flVOGxbhDDh/iZaoek11Y8mtyKPGF3vP8XhnkDEAmzeg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.12.tgz", + "integrity": "sha512-TGbO26Yw2xsHzxtbVFGEXBFH0FRAP7gtcPE7P5yP7wGy7cXK2oO7RyOhL5NLiqTlBh47XhmIUXuGciXEqYFfBQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.12.tgz", + "integrity": "sha512-lPDGyC1JPDou8kGcywY0YILzWlhhnRjdof3UlcoqYmS9El818LLfJJc3PXXgZHrHCAKs/Z2SeZtDJr5MrkxtOw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.12.tgz", + "integrity": "sha512-8bwX7a8FghIgrupcxb4aUmYDLp8pX06rGh5HqDT7bB+8Rdells6mHvrFHHW2JAOPZUbnjUpKTLg6ECyzvas2AQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.12.tgz", + "integrity": "sha512-0y9KrdVnbMM2/vG8KfU0byhUN+EFCny9+8g202gYqSSVMonbsCfLjUO+rCci7pM0WBEtz+oK/PIwHkzxkyharA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.12.tgz", + "integrity": "sha512-h///Lr5a9rib/v1GGqXVGzjL4TMvVTv+s1DPoxQdz7l/AYv6LDSxdIwzxkrPW438oUXiDtwM10o9PmwS/6Z0Ng==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.12.tgz", + "integrity": "sha512-iyRrM1Pzy9GFMDLsXn1iHUm18nhKnNMWscjmp4+hpafcZjrr2WbT//d20xaGljXDBYHqRcl8HnxbX6uaA/eGVw==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.12.tgz", + "integrity": "sha512-9meM/lRXxMi5PSUqEXRCtVjEZBGwB7P/D4yT8UG/mwIdze2aV4Vo6U5gD3+RsoHXKkHCfSxZKzmDssVlRj1QQA==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.12.tgz", + "integrity": "sha512-Zr7KR4hgKUpWAwb1f3o5ygT04MzqVrGEGXGLnj15YQDJErYu/BGg+wmFlIDOdJp0PmB0lLvxFIOXZgFRrdjR0w==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.12.tgz", + "integrity": "sha512-MsKncOcgTNvdtiISc/jZs/Zf8d0cl/t3gYWX8J9ubBnVOwlk65UIEEvgBORTiljloIWnBzLs4qhzPkJcitIzIg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.12.tgz", + "integrity": "sha512-uqZMTLr/zR/ed4jIGnwSLkaHmPjOjJvnm6TVVitAa08SLS9Z0VM8wIRx7gWbJB5/J54YuIMInDquWyYvQLZkgw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.12.tgz", + "integrity": "sha512-xXwcTq4GhRM7J9A8Gv5boanHhRa/Q9KLVmcyXHCTaM4wKfIpWkdXiMog/KsnxzJ0A1+nD+zoecuzqPmCRyBGjg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.12.tgz", + "integrity": "sha512-Ld5pTlzPy3YwGec4OuHh1aCVCRvOXdH8DgRjfDy/oumVovmuSzWfnSJg+VtakB9Cm0gxNO9BzWkj6mtO1FMXkQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.12.tgz", + "integrity": "sha512-fF96T6KsBo/pkQI950FARU9apGNTSlZGsv1jZBAlcLL1MLjLNIWPBkj5NlSz8aAzYKg+eNqknrUJ24QBybeR5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.12.tgz", + "integrity": "sha512-MZyXUkZHjQxUvzK7rN8DJ3SRmrVrke8ZyRusHlP+kuwqTcfWLyqMOE3sScPPyeIXN/mDJIfGXvcMqCgYKekoQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.25.12.tgz", + "integrity": "sha512-rm0YWsqUSRrjncSXGA7Zv78Nbnw4XL6/dzr20cyrQf7ZmRcsovpcRBdhD43Nuk3y7XIoW2OxMVvwuRvk9XdASg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.12.tgz", + "integrity": "sha512-3wGSCDyuTHQUzt0nV7bocDy72r2lI33QL3gkDNGkod22EsYl04sMf0qLb8luNKTOmgF/eDEDP5BFNwoBKH441w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.12.tgz", + "integrity": "sha512-rMmLrur64A7+DKlnSuwqUdRKyd3UE7oPJZmnljqEptesKM8wx9J8gx5u0+9Pq0fQQW8vqeKebwNXdfOyP+8Bsg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.12.tgz", + "integrity": "sha512-HkqnmmBoCbCwxUKKNPBixiWDGCpQGVsrQfJoVGYLPT41XWF8lHuE5N6WhVia2n4o5QK5M4tYr21827fNhi4byQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.25.12", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.12.tgz", + "integrity": "sha512-alJC0uCZpTFrSL0CCDjcgleBXPnCrEAhTBILpeAp7M/OFgoqtAetfBzX0xM00MUsVVPpVjlPuMbREqnZCXaTnA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@google/genai": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/@google/genai/-/genai-1.52.0.tgz", + "integrity": "sha512-gwSvbpiN/17O9TbsqSsE/OzZcpv5Fo4RQjdngGgogtuB9RsyJ8ZHhX5KjHj1bp5N9snN2eK8LDGXSaWW2hof8Q==", + "hasInstallScript": true, + "license": "Apache-2.0", + "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/@img/colour": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@img/colour/-/colour-1.1.0.tgz", + "integrity": "sha512-Td76q7j57o/tLVdgS746cYARfSyxk8iEfRxewL9h4OMzYhbW4TAcppl0mT4eyqXddh6L/jwoM75mo7ixa/pCeQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/@img/sharp-darwin-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.4.tgz", + "integrity": "sha512-Uhfl4V4lhP2nbUVF9+hyH1+luj86f1gUFeo8ALYxFoULoU+G87D43BfeMP8XHsk9boxAnCY/bf2EHwhA7MuGsA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-darwin-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.4.tgz", + "integrity": "sha512-hWniXY3bG5qKpkKrAwPe4y+VTPmf086YQAnkxWh7uA1YrlRouWGa0M0Mxj3ZjnXFkv7/TD1bTy9lGUK26vRvWw==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-darwin-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.4.tgz", + "integrity": "sha512-lIsKw/BU+kjB4eZjxrYrZmwOJYi3Ajrv66iAlBmUPyKc3HpnloevB1g3wxGD9P/5BbQ1brBGl65VRRrCvQDEqA==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.3.tgz", + "integrity": "sha512-suTBPTDGrI9WodccaDdwZItTSaBYASlBk1NSfElSHrUfzu3szG6lvIF58+WiFvnfzuK8ZBFS5zE00PxqxnRiPg==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-darwin-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.3.tgz", + "integrity": "sha512-FVJZ5mITMobmXIz/hPDTw0EintTW5H3WfrxwLqEqjiIihlu+hVRyGrFQ60xl0Lxn7Bt3zdpevPaQi0HEzqz9fw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "darwin" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.3.tgz", + "integrity": "sha512-3rbU4vqXXc3hY/OiXdl52xZvT0F1yEngWfvqudtPJg/KkyiaQw2DRsFrNzpmLvfavbwOq3qXn36GP8obHRULQA==", + "cpu": [ + "arm" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.3.tgz", + "integrity": "sha512-0DaL0A6Xu6sQSQFwe4iVCrKWU2cCTItnRsYsCdxAMm9NF6twAA9BKnoqy4hqz4+azQ0JHuA26qiUKsf1XJ/v5A==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-ppc64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.3.tgz", + "integrity": "sha512-cdn1OvUBwsXhbC0zSzJnNzf5MZ/mTrobawDvNXBTxe8VtqKAm0sRuEY2Evzovb/w9JMk4TvRxqt1mekSuJz64w==", + "cpu": [ + "ppc64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-riscv64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.3.tgz", + "integrity": "sha512-HjPVx7yKz+0lqdhDlTw1tt90wamBoxhiXpvl1XZpJLiHH4RCJ5yDTqH+VlYPv2fwFs89JFw4c1IexYOcQUi4IQ==", + "cpu": [ + "riscv64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-s390x": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.3.tgz", + "integrity": "sha512-neWLh+3yCNThxnfy3c4BbVBeGgt9aftno+XbT56iK28RgeDs3UOFWviLWlUu0bArYVYJaFDK+RRohbicUNCm8Q==", + "cpu": [ + "s390x" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linux-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.3.tgz", + "integrity": "sha512-4vKmvAst9nrowcqquKFAyZJUDolUaIp8uRiN0mWFguJ1IplC9/pitXtlnnlU4aa/eJw3J7i67V+pwUL+wZGdsA==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-arm64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.3.tgz", + "integrity": "sha512-Y9kQaLMuNoB0bPYOOdcZMaseNrFpPodIWWMrx+CZyydf2xn68j9WYc6sWWRrDwNkzCQjKYfc68L7jKjGlHMibw==", + "cpu": [ + "arm64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-libvips-linuxmusl-x64": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.3.tgz", + "integrity": "sha512-fj8Mv0HHfD1Rr+4I68+3agJynxDWtBFgicTbSOb9Bke6pIwzGcJ+RX/yHjmiEGFMCavY/dxvem7MyNaJF+wDiw==", + "cpu": [ + "x64" + ], + "license": "LGPL-3.0-or-later", + "optional": true, + "os": [ + "linux" + ], + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-linux-arm": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.4.tgz", + "integrity": "sha512-7OAS8gI0EReKGVN2HssHlM6umJgxF5VI3xN0p9FA91p/YO+ou5hiNghLdZ5BEHztwaaK5+bLKRf8x/o2L2nk9A==", + "cpu": [ + "arm" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.4.tgz", + "integrity": "sha512-De4jpEnAU8Hd5oT0j1G3uL4ZvTuipVMn7YC6vPaJhy6/7EwEae0SVAoBrUMYQbkLGDm85taVWwuPc1a44LTzCQ==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-ppc64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.4.tgz", + "integrity": "sha512-2oYZJeIl4kCcMGk4ouZVjnkCtFrpQFlNEtJ6GbxzhHQchwH0NH/qEb9ykmOl29dqwMq+JhFdZn+1ak2FKhI9fQ==", + "cpu": [ + "ppc64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-ppc64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-riscv64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.4.tgz", + "integrity": "sha512-cPbNChoRURAWdebDIHSenxRpgEdy7JkPydSnUxRm9VvKD7m0/xVaR/8Fzlu81pk5nHEvHH87UZUA7cTtwnbJSA==", + "cpu": [ + "riscv64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-riscv64": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-s390x": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.4.tgz", + "integrity": "sha512-RY0JFY8Fd6RonCBtHz+DvadaPkXDSI1AUn6yWL9TipqkZ1vY8w8evqdgyDFnkm4/K1ve1TvZiaePP5oSd4+WVQ==", + "cpu": [ + "s390x" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-s390x": "1.3.3" + } + }, + "node_modules/@img/sharp-linux-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.4.tgz", + "integrity": "sha512-9qvvEAuk8k89TfWUoX2htWjbAMX8p+NxCppjpcg5k6xMsjhBQPTsoIh36h9Qde4WRuGpJeYnOjdosDn/cnv+OA==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linux-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.4.tgz", + "integrity": "sha512-KB5jxpfWQTr0nc3xdHtWChdbifHrBGsd2SM62Eyxrl8afikm+f5qGBU75SJIZBT/S1MC8XyacdlXBMSWq6OURA==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3" + } + }, + "node_modules/@img/sharp-linuxmusl-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.4.tgz", + "integrity": "sha512-f+eZJZIQNEEd26RPSW+76chwOf1XtA2Y/O+5ocVyLliHkeih3e+jhLVBdNTd2rS3IbNXK8+ug93Vf5ZXtF5Lxg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-libvips-linuxmusl-x64": "1.3.3" + } + }, + "node_modules/@img/sharp-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.4.tgz", + "integrity": "sha512-zQnl4Kwp7Q6NHsENtU2T/00Zi+w3AQNwz3+UaTyVBy2FpXrzXzGjndpK61onhZjRtRpQXxCTeqw19bVyXOh7jA==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.4.tgz", + "integrity": "sha512-ESfNkywmCfPNyaZjxooddJQiQ+l/nTpGEOGthxiLnIHXC/CmcBixnfwUleX9mCz9ovrUUvKMap/pm8RYbzfwaA==", + "cpu": [ + "wasm32" + ], + "license": "Apache-2.0", + "optional": true, + "dependencies": { + "@img/sharp-wasm32": "0.35.4" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-arm64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.4.tgz", + "integrity": "sha512-iNdlBX9gLVvqe2I3uIJSIKTq6wckP/DYxZtcqxm09x5Gi24DnFBmPAWZmr60ZyYMG0xlzo6goG3670ar+RXvRw==", + "cpu": [ + "arm64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-ia32": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.4.tgz", + "integrity": "sha512-kqRsbaa5CS6KHlpxnN7WhE6vAAugXyZButpRdvDWetlv6Qv4N9WTcrWzF7tXfB9T7MsoadqdI8hmwLq6UlLvtw==", + "cpu": [ + "ia32" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-win32-x64": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.4.tgz", + "integrity": "sha512-XtmnYhBcrORsJ4XJngyzr/EWP0hRZLAZRFaApdKuviyqF78+ylxh2y06ZmtULAMOnObJ3ucpN0AcwSWnMowTRg==", + "cpu": [ + "x64" + ], + "license": "Apache-2.0 AND LGPL-3.0-or-later", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@modelcontextprotocol/client": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/client/-/client-2.0.0.tgz", + "integrity": "sha512-8f1OghQ2rjzIOfqgUCP+8GiUWqRs89njoWLNqAe8kWmDePv3s1fZXseej+QXemssEuuOvLLmLO/kqM3IQHtISw==", + "license": "MIT", + "dependencies": { + "@modelcontextprotocol/core": "2.0.0", + "cross-spawn": "^7.0.5", + "eventsource": "^3.0.2", + "eventsource-parser": "^3.0.0", + "jose": "^6.1.3", + "pkce-challenge": "^5.0.0", + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/@modelcontextprotocol/core": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/core/-/core-2.0.0.tgz", + "integrity": "sha512-pJCEwGG7Lfr/+PQp9ZTwKXNeO5wzbfKL7H3MYpCorM4oFBoQrdjnBgEoqG+RjhsvS1FKrDbKux+M1HhlnGWqcA==", + "license": "MIT", + "dependencies": { + "zod": "^4.2.0" + }, + "engines": { + "node": ">=20" + } + }, + "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.34.1", + "resolved": "https://registry.npmjs.org/@smithy/core/-/core-3.34.1.tgz", + "integrity": "sha512-dLcOUxz8YCv1RZUMKq6GbyUf95pLbrqh34bPvpCZ1+CByFF31BEAFewZjsGCnVsZTKdThNENfGyAgk2TJqVwSw==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/credential-provider-imds": { + "version": "4.5.2", + "resolved": "https://registry.npmjs.org/@smithy/credential-provider-imds/-/credential-provider-imds-4.5.2.tgz", + "integrity": "sha512-A9uSdn72ozbRUSit0eib0TW7nXuNPlaeM0zcGkJ+nE6tFcSDbnmtwoxbTCFBukVQcszDAyvsd7+rTduPTXpygg==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.2", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/fetch-http-handler": { + "version": "5.8.0", + "resolved": "https://registry.npmjs.org/@smithy/fetch-http-handler/-/fetch-http-handler-5.8.0.tgz", + "integrity": "sha512-ycSJu3tFAQ4v04CBB0agqFMVsSQ1iG3yw+SpgxRqKfaURpQD4CZ8Wn0zPMmSnOuTpTh65Vz+EA0rMrw089wvkA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.18.0", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/is-array-buffer": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@smithy/is-array-buffer/-/is-array-buffer-2.2.0.tgz", + "integrity": "sha512-GGP3O9QFD24uGeAXYUjwSTXARoqpZykHadOmA8G5vfJPK0/DC67qa//0qvqrJzL1xc8WQWX7/yc7fwudjPHPhA==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@smithy/node-http-handler": { + "version": "4.7.3", + "resolved": "https://registry.npmjs.org/@smithy/node-http-handler/-/node-http-handler-4.7.3.tgz", + "integrity": "sha512-/jPhevcTFPMVl6KNjbaI47iOg1zxC7IsnX4PQDGVZKMFceOXtB8IEYaB7a9VvkP/3oC60WzTeKocvSI7vLT0vA==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.24.3", + "@smithy/types": "^4.14.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/signature-v4": { + "version": "5.7.3", + "resolved": "https://registry.npmjs.org/@smithy/signature-v4/-/signature-v4-5.7.3.tgz", + "integrity": "sha512-7ImGm+FkHRLcBaRttIAMZ6bzJZWb2cJGoYjq46F2UjycujWzrL9GEN9h4w7eQyXJYnltrUhxbbieBAIRrdqpow==", + "license": "Apache-2.0", + "dependencies": { + "@smithy/core": "^3.33.3", + "@smithy/types": "^4.17.2", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/@smithy/types": { + "version": "4.18.0", + "resolved": "https://registry.npmjs.org/@smithy/types/-/types-4.18.0.tgz", + "integrity": "sha512-CgB6HHWer/vrKps24ulRIbpcpb7K4xAU7SkZ7YHzBPlwHsvsrCJFEXK421s+cJzX+ZrqtA/TuU5w1HzI7k9N8A==", + "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/@stablelib/base64": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@stablelib/base64/-/base64-1.0.1.tgz", + "integrity": "sha512-1bnPQqSxSuc3Ii6MhBysoWCg58j97aUjuCSZrGSmDxNqtytIi0k8utUenAwTZN4V5mXXYGsVUI9zeBqy+jBOSQ==", + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "24.13.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.4.tgz", + "integrity": "sha512-YJ7EqCstVTzIr0fMr7qul/977en+pQHrfmuKIo6Zr9i75Be21dr3MovcfvGtyvi2HAUrRerWps5sMO9I7WaxDw==", + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.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/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/cross-spawn/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/cross-spawn/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/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/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "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.25.12", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.12.tgz", + "integrity": "sha512-bbPBYYrtZbkt6Os6FiTLCTFxvq4tt3JKall1vRwshA3fdVztsLAatFaZobhkBC8/BrPetoa0oksYoKXoG4ryJg==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.25.12", + "@esbuild/android-arm": "0.25.12", + "@esbuild/android-arm64": "0.25.12", + "@esbuild/android-x64": "0.25.12", + "@esbuild/darwin-arm64": "0.25.12", + "@esbuild/darwin-x64": "0.25.12", + "@esbuild/freebsd-arm64": "0.25.12", + "@esbuild/freebsd-x64": "0.25.12", + "@esbuild/linux-arm": "0.25.12", + "@esbuild/linux-arm64": "0.25.12", + "@esbuild/linux-ia32": "0.25.12", + "@esbuild/linux-loong64": "0.25.12", + "@esbuild/linux-mips64el": "0.25.12", + "@esbuild/linux-ppc64": "0.25.12", + "@esbuild/linux-riscv64": "0.25.12", + "@esbuild/linux-s390x": "0.25.12", + "@esbuild/linux-x64": "0.25.12", + "@esbuild/netbsd-arm64": "0.25.12", + "@esbuild/netbsd-x64": "0.25.12", + "@esbuild/openbsd-arm64": "0.25.12", + "@esbuild/openbsd-x64": "0.25.12", + "@esbuild/openharmony-arm64": "0.25.12", + "@esbuild/sunos-x64": "0.25.12", + "@esbuild/win32-arm64": "0.25.12", + "@esbuild/win32-ia32": "0.25.12", + "@esbuild/win32-x64": "0.25.12" + } + }, + "node_modules/eventsource": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/eventsource/-/eventsource-3.0.7.tgz", + "integrity": "sha512-CRT1WTyuQoD771GW56XEZFQ/ZoSfWid1alKGDYMmkt2yl8UXrVR4pspqWNEcqKvVIzg6PAltWjxcSSPrboA4iA==", + "license": "MIT", + "dependencies": { + "eventsource-parser": "^3.0.1" + }, + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/eventsource-parser": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/eventsource-parser/-/eventsource-parser-3.1.1.tgz", + "integrity": "sha512-EKN1vKAMcZ8MlYMpaNuxN6R9yakzH6uajHcHVTqWJzvu5pWw9DyhbP35HH8MVBQ+dZjAfDxk+A8NiR9KWaXiyQ==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "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.3.1", + "resolved": "https://registry.npmjs.org/gaxios/-/gaxios-7.3.1.tgz", + "integrity": "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ==", + "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/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/google-auth-library": { + "version": "10.9.1", + "resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-10.9.1.tgz", + "integrity": "sha512-i1ydyHrqcIxXkWh/uBmVkzCvIuq5yiK2ATndIe5XxKholrG/MTYP9xGYka4sQhrbIAgGjL2B6NOE7rFaiF3fXw==", + "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/jose": { + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/panva" + } + }, + "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/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/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.40.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-6.40.0.tgz", + "integrity": "sha512-MWtTjd/gQt4jpbji61NTgFWJLoY/PdRJ6wG9/ZDRMYNMlBKrCrSlkLI+KgHP1vR1qT6LKSAyAqIxno6lcK9JiA==", + "license": "Apache-2.0", + "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/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/pkce-challenge": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/pkce-challenge/-/pkce-challenge-5.0.1.tgz", + "integrity": "sha512-wQ0b/W4Fr01qtpHlqSqspcj3EhBvimsdh0KlHhH8HRZnMsEa0ea2fTULOXOS9ccQr3om+GcGRk4e+isrZWV8qQ==", + "license": "MIT", + "engines": { + "node": ">=16.20.0" + } + }, + "node_modules/prettier": { + "version": "3.9.6", + "resolved": "https://registry.npmjs.org/prettier/-/prettier-3.9.6.tgz", + "integrity": "sha512-OpN0zzVdiaiAhxpuuj5efpIS4sY9j7bY6uR5mnj5yPzGkdkjNKSJeUThPb60Jw29QuAZgA4o+/iB49kFiaBX6g==", + "dev": true, + "license": "MIT", + "bin": { + "prettier": "bin/prettier.cjs" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/prettier/prettier?sponsor=1" + } + }, + "node_modules/protobufjs": { + "version": "7.6.6", + "resolved": "https://registry.npmjs.org/protobufjs/-/protobufjs-7.6.6.tgz", + "integrity": "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg==", + "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/rwebtransport": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/rwebtransport/-/rwebtransport-0.2.2.tgz", + "integrity": "sha512-ZjTUGjlyJTgyq6+4red14r0rqTgj3Ol9z4xUaZNqSdq1A0xkYFWHx2AOdQ4BcxxpyI707nCbinO5y+G71nfXrw==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=24 <25 || >=26 <27" + } + }, + "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/semver": { + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/sharp": { + "version": "0.35.4", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.4.tgz", + "integrity": "sha512-n++8XWcj+jCOr2IOl7h8LbKnGBDY4aPbmprMONBNFdn0ImXqpGVv5zliDs0V9HbmbCQLpbuo2ej9rAoOQTvMDA==", + "license": "Apache-2.0", + "dependencies": { + "@img/colour": "^1.1.0", + "detect-libc": "^2.1.2", + "semver": "^7.8.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + }, + "optionalDependencies": { + "@img/sharp-darwin-arm64": "0.35.4", + "@img/sharp-darwin-x64": "0.35.4", + "@img/sharp-freebsd-wasm32": "0.35.4", + "@img/sharp-libvips-darwin-arm64": "1.3.3", + "@img/sharp-libvips-darwin-x64": "1.3.3", + "@img/sharp-libvips-linux-arm": "1.3.3", + "@img/sharp-libvips-linux-arm64": "1.3.3", + "@img/sharp-libvips-linux-ppc64": "1.3.3", + "@img/sharp-libvips-linux-riscv64": "1.3.3", + "@img/sharp-libvips-linux-s390x": "1.3.3", + "@img/sharp-libvips-linux-x64": "1.3.3", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.3", + "@img/sharp-libvips-linuxmusl-x64": "1.3.3", + "@img/sharp-linux-arm": "0.35.4", + "@img/sharp-linux-arm64": "0.35.4", + "@img/sharp-linux-ppc64": "0.35.4", + "@img/sharp-linux-riscv64": "0.35.4", + "@img/sharp-linux-s390x": "0.35.4", + "@img/sharp-linux-x64": "0.35.4", + "@img/sharp-linuxmusl-arm64": "0.35.4", + "@img/sharp-linuxmusl-x64": "0.35.4", + "@img/sharp-webcontainers-wasm32": "0.35.4", + "@img/sharp-win32-arm64": "0.35.4", + "@img/sharp-win32-ia32": "0.35.4", + "@img/sharp-win32-x64": "0.35.4" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/standardwebhooks": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/standardwebhooks/-/standardwebhooks-1.1.1.tgz", + "integrity": "sha512-bCbX9ZEyFkWPsRz7Bl3NuQUJohmwGSev/yhr7vhaGPlc4AfIrspIRa6cPTBuI1ItmrTDJ4d/S2hCsfe4+vQGnQ==", + "license": "MIT", + "dependencies": { + "@stablelib/base64": "^1.0.0", + "fast-sha256": "^1.3.0" + } + }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, + "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/typebox": { + "version": "1.3.7", + "resolved": "https://registry.npmjs.org/typebox/-/typebox-1.3.7.tgz", + "integrity": "sha512-meKuifc33Pccx0O6PdIzYMq3Og8zvP4TIi/a+Bw3AEMZMxOD0+RHGQvpglEe6Zdy3wZ8nqn/j95h8LUZLk/6Hg==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "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.3", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz", + "integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/zod": { + "version": "4.6.2", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.2.tgz", + "integrity": "sha512-lh5RCAGFa1Cm2hjtNwLQhSs/AsqdWnTQaBER9fEwN/88pSh7KOtJavtBx/0VlkN/uFd61SwYmljLMDAsHlvzBQ==", + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/agent/package.json b/agent/package.json new file mode 100644 index 00000000000..4b89b201bcb --- /dev/null +++ b/agent/package.json @@ -0,0 +1,52 @@ +{ + "name": "@spomichter/dimcode", + "version": "0.1.0-next.1", + "description": "Persistent DimOS coding agent and terminal, built on Pi", + "type": "module", + "license": "Apache-2.0", + "bin": { + "dimcode": "dist/main.js" + }, + "files": [ + "dist", + "skills", + "README.md", + "install.sh", + "npm-shrinkwrap.json" + ], + "engines": { + "node": ">=24 <25 || >=26 <27" + }, + "scripts": { + "build": "node build.mjs", + "typecheck": "tsc --noEmit", + "test": "node test/run.mjs", + "check": "npm run typecheck && npm test && npm run build", + "format": "prettier --write src test *.mjs" + }, + "dependencies": { + "@earendil-works/pi-ai": "0.85.1", + "@earendil-works/pi-coding-agent": "0.85.1", + "@earendil-works/pi-tui": "0.85.1", + "@modelcontextprotocol/client": "2.0.0", + "rwebtransport": "0.2.2", + "sharp": "0.35.4", + "typebox": "1.3.7", + "zod": "^4.1.0" + }, + "devDependencies": { + "@types/node": "^24.3.0", + "esbuild": "^0.25.0", + "prettier": "^3.6.0", + "typescript": "5.9.3" + }, + "repository": { + "type": "git", + "url": "git+https://github.com/dimensionalOS/dimos.git", + "directory": "agent" + }, + "homepage": "https://github.com/dimensionalOS/dimos/tree/main/agent#readme", + "publishConfig": { + "access": "public" + } +} diff --git a/agent/skills/dimensional-install/SKILL.md b/agent/skills/dimensional-install/SKILL.md new file mode 100644 index 00000000000..12a13a57a32 --- /dev/null +++ b/agent/skills/dimensional-install/SKILL.md @@ -0,0 +1,18 @@ +--- +name: dimensional-install +description: Set up DimOS from an existing checkout/environment or a new installation, following its own version-specific instructions with the normal coding tools. +--- + +Start with one question: drop a path to an existing DimOS checkout/environment, or describe the robot/app you want to build. Wait for that answer. Do not assume the current directory needs a new `.venv`. + +Use the existing Read and Bash tools. There is no harness-owned DimOS install recipe. + +1. Inspect the supplied path, OS, available Python/package tools and existing DimOS installations. Distinguish a checkout, Python environment, app workspace and remote MCP endpoint. Reuse a working installation; when several match, ask which one to use. +2. Read that checkout/version's `README.md`, `pyproject.toml` and linked installation instructions. For a new package install, retrieve the official DimensionalOS/dimos README and linked OS-specific instructions from GitHub, selecting a release/ref with the user when needed. Report the source/ref you used. Do not invent commands if the instructions cannot be retrieved. Python requirements, extras and system dependencies come from those instructions, not this skill. +3. Explain the selected destination and required changes briefly, then execute the documented commands for the user's requested setup. Show command progress in normal tool cards, inspect failures and resolve them. Never replace an existing environment, install every robot extra by default, or start physical hardware as an install check. System changes needing elevated privileges require the user's involvement. +4. Verify the selected executable, `importlib.metadata.version("dimos")`, imported module path, CLI help and blueprint discovery. Run package checks from outside the checkout so the current directory cannot shadow the installed package. Distinguish installed distribution metadata from a checkout version string. Inspect `dimos status` when supported; distinguish installed software from a running blueprint. Only start an explicitly requested replay/simulation or hardware target. Do not claim installation succeeded from package-manager output alone. +5. Save verified selections through the existing CLI: `"$DIMCODE_NODE" "$DIMCODE_CLI" dimos /absolute/bin/dimos`, `python /absolute/bin/python`, and `workspace /absolute/app`. The launcher variables work for source builds without a global `dimcode` command. Use absolute paths in the current session; the saved workspace applies to subsequent launches. Configure `connect NAME URL` / `relay URL [ROBOT]` only for endpoints the user selected; use `/reload` after the turn to discover changed MCP skills. + +Provider credentials and the gateway-at-login choice belong to the bootstrap UI. Never request API keys in chat, read auth files, or dump environment variables. Use `/login` for model authentication. Do not stop/restart the gateway hosting this conversation from a Bash tool. If the user wants to change daemon startup later, give the exact `service install/status/uninstall` launcher command to run after detaching. + +Finish with the selected paths, verified version, available blueprint result, what is actually running, and any remaining dependency or connection issue. Setup is a normal persistent Pi conversation: the user can ask questions, correct the plan, detach or resume. diff --git a/agent/skills/dimensional/SKILL.md b/agent/skills/dimensional/SKILL.md new file mode 100644 index 00000000000..50ed3f952e8 --- /dev/null +++ b/agent/skills/dimensional/SKILL.md @@ -0,0 +1,32 @@ +--- +name: dimensional +description: Install DimOS, build and operate apps/blueprints, invoke deployed skills, and inspect real sensor/memory results. +--- + +Use the selected DimOS executable/Python environment and workspace from the session instructions. Check installed help when versions differ. Never replace an existing environment silently. + +- Installation: read the dimensional-install skill, inspect the selected environment and follow that DimOS version's own installation instructions using normal coding tools. Harness setup and model auth are independent of DimOS installation. +- Apps: `dimos list`, `dimos show-config`, `dimos bake`, source editing, and external package entry points in the `dimos.blueprints` group. Run external apps by the qualified `distribution-name.blueprint-name` shown by `dimos list`; bare names select built-ins. Do not edit the built-in registry for an external app. +- Runtime: `dimos run BLUEPRINT --daemon`; inspect `dimos status --run ID`, `dimos log --run ID`, `dimos stop --run ID`, `dimos restart --run ID`. Older versions may lack exact targeting: use the existing public run_registry APIs, never silently act on the latest run instead. +- Python-launched coordinators: `Dimos.connect()` and its public module/RPC inspection APIs; `stop()` on that connection disconnects only. Use separate configured transport buses/endpoints for independent coordinators. +- All deployed @skill tools come from the selected MCP endpoint. Use those tools to preserve capability arbitration and background stop semantics. Direct RPC is not a substitute for the MCP dispatcher. On endpoint changes use /reload. Missing tools do not mean every robot dependency should be installed. +- Full CLI access stays in Bash: runtime/config/apps; mcp and agent-send; shell/spy/lcmspy/agentspy/humancli/top; topic/map/rerun-bridge; mem/data/dataprep/cache; nav-eval/evals; hardware/go2tool/calibration; login/logout/whoami. Interactive programs need an interactive terminal, not a background Bash call. +- Dimensional cloud login and model-provider login are separate. Never ask for credentials in chat or tool arguments; use the terminal /login or setup flow. + +For memory, use existing stream operations. Evaluate a query once with materialize(), then derive scores, selected peaks, images and comparisons from that same cache. Preserve timestamps, observation IDs and coordinate frames in exports. The docs reconstruction example includes a skipped block/API TODO; verify the chosen callable operation on actual data. + +Show a visualization for each meaningful sensor/memory operation. Prefer a supported type or an existing DimOS visualizer. If neither supports the result, use inline Python to generate a self-contained SVG from the evaluated data, save it, and call `dimcode_render({kind:"image",path:"result.svg"})`. This fallback also supports custom plots, simple graphs and proposed overlays; label proposals distinctly from observations. Use the returned image as visual feedback when interpreting or refining the result. If rendering fails, report that explicitly and retain the original result. + +DimOS owns reusable, agent-independent visualization semantics: geometry, axes, units, timestamps, legends, selections and generic encodings for agent understanding. Dimcode owns terminal-specific styling and interaction: card layout, theme, viewport/camera defaults, point appearance, looping playback, resizing and graphics/Braille adaptation. Terminal styling must preserve semantic colors and labels supplied by DimOS or a custom SVG. Keep terminal-specific presets out of DimOS. + +All analysis of point-cloud frames and video belongs in DimOS memory. Prefer its existing `Space.to_svg()` and `Plot.to_svg()` renderers, alongside selected-frame exports from the same materialized query. Display the views together with `dimcode_render({kind:"image", title:"Memory · QUERY", views:[{label:"Timeline",path:"timeline.svg"},{label:"Spatial",path:"space.svg"},{label:"Frames",path:"frames.png"}]})`. Supply only views actually produced by that operation. Preserve the native colors, timestamps, peaks, coordinate frames and selection metadata; don't reconstruct them in the harness. The terminal displays an overview and individually selectable panels, and the model receives every image. MCP tools returning multiple `agent_encode` images are displayed together automatically; a skill returning only a pose does not provide these views. + +Built-in renderers only present point clouds and images/SVGs. Export clouds through existing extraction as `{points:[[x,y,z]], colors?:[[r,g,b]], frame?, timestamp?, selectedIndices?}`; RGB is 0–255. Generate all other plots/graphs with DimOS or inline Python as SVG. Retain original exports and metadata. The renderer labels display sampling and hashes every source. + +For memory/replay sequences, materialize the selected interval once in DimOS. Export its cloud JSONs or image/SVG frames, then write a finite index `{type:"points"|"image",timeOrigin:RECORDING_ORIGIN,source:"operation description",frames:[{path:"relative/frame.json",timestamp:CAPTURE_SECONDS,sha256?:DIGEST}]}`. Timestamps must strictly increase. Use the same recording origin for related streams and preserve their coordinate frames. Call `dimcode_render({path:"index.json",title:"Memory · seconds 1–5"})`; no kind is needed. The latest clip loops automatically with a fixed cloud camera and source timestamps, and exports a looping GIF. There are no player buttons or seek commands. When the user asks for a frame at a particular time, use the saved index and its timeOrigin to select the matching original frame, then call dimcode_render with that frame index; a single-frame result is static. The model receives a timestamped contact sheet; inspect any original zero-based frame with `dimcode_render({path:"index.json",frame:17})` when the sheet is insufficient. Never claim to have inspected every frame from a contact sheet alone. + +The preview selects approximately 10 Hz, at most 120 frames across at most 60 seconds, and preserves original frame indexes. Larger results need a smaller export selected in DimOS, not a query inside the harness. Images, custom SVG animations and point clouds share this same player. GIF time is quantized to centiseconds; terminal scheduling uses source timestamp differences. Recordings stay with DimOS and only disposable previews enter the harness cache. + +For live rendering, explicitly choose the existing relay, robot and channel. Web SDK subscriptions open only for the tool/view's lifetime. Label the view live: it is not a historical selection, and relay header timestamps may be send time. Only a selected final snapshot belongs in model context. Non-image encodings need their existing domain decoder/renderer or an explicit saved export. + +For validation, start with the standard unitree-go2 and unitree-go2-agentic blueprints in replay mode. A running gateway does not require a blueprint or its LLM client. Do not infer a physical robot target from a default IP. Inspect non-moving skills first on explicitly selected hardware. diff --git a/agent/src/artifacts.ts b/agent/src/artifacts.ts new file mode 100644 index 00000000000..61f131c3c2c --- /dev/null +++ b/agent/src/artifacts.ts @@ -0,0 +1,56 @@ +import { createHash } from "node:crypto"; +import { + mkdir, + readFile, + readdir, + stat, + unlink, + writeFile, +} from "node:fs/promises"; +import { join } from "node:path"; +import type { Paths } from "./config.js"; + +export async function readArtifact( + path: string, + digest?: string, +): Promise<{ bytes: Buffer; sha256: string }> { + if ((await stat(path)).size > 32 * 1024 * 1024) + throw new Error("Export exceeds 32 MiB; select a smaller view in DimOS."); + const bytes = await readFile(path); + const sha256 = createHash("sha256").update(bytes).digest("hex"); + if (digest && digest !== sha256) + throw new Error( + "Source changed since the agent inspected it; retained preview remains available.", + ); + return { bytes, sha256 }; +} + +export async function cacheImage( + paths: Paths, + png: Buffer, + extension: "png" | "gif" = "png", +): Promise { + await mkdir(paths.cache, { recursive: true, mode: 0o700 }); + const path = join( + paths.cache, + createHash("sha256").update(png).digest("hex") + "." + extension, + ); + await writeFile(path, png, { mode: 0o600 }); + const entries = ( + await Promise.allSettled( + (await readdir(paths.cache)) + .filter((name) => /^[a-f0-9]{64}\.(png|gif)$/.test(name)) + .map(async (name) => ({ + name, + info: await stat(join(paths.cache, name)), + })), + ) + ).flatMap((result) => (result.status === "fulfilled" ? [result.value] : [])); + let bytes = 0; + for (const entry of entries.sort((a, b) => b.info.mtimeMs - a.info.mtimeMs)) { + bytes += entry.info.size; + if (bytes > 128 * 1024 * 1024 && entry.name !== path.split("/").pop()) + await unlink(join(paths.cache, entry.name)).catch(() => {}); + } + return path; +} diff --git a/agent/src/clips.ts b/agent/src/clips.ts new file mode 100644 index 00000000000..d9781bc48a9 --- /dev/null +++ b/agent/src/clips.ts @@ -0,0 +1,240 @@ +import { dirname, resolve } from "node:path"; +import sharp from "sharp"; +import { z } from "zod"; +import { cacheImage, readArtifact } from "./artifacts.js"; +import type { Paths } from "./config.js"; +import { cloudSchema, fitClouds, paintCloud, type Cloud } from "./points.js"; + +const timestamp = z.number().finite(); +export const clipSourceSchema = z + .object({ + type: z.enum(["points", "image"]), + timeOrigin: timestamp.optional(), + source: z.string().optional(), + frames: z + .array( + z.object({ + path: z.string(), + timestamp, + sha256: z.string().optional(), + }), + ) + .min(1) + .max(10_000), + }) + .refine( + (clip) => + clip.frames.every( + (frame, i) => !i || frame.timestamp > clip.frames[i - 1].timestamp, + ), + "Frame timestamps must be strictly increasing", + ); +export const clipSchema = z.object({ + type: z.enum(["points", "image"]), + timeOrigin: timestamp, + sourceCount: z.number().int().positive(), + frames: z + .array( + z.object({ + path: z.string(), + timestamp, + sha256: z.string(), + source: z.string(), + sourceSha256: z.string(), + index: z.number().int().nonnegative(), + }), + ) + .min(1) + .max(120), + gif: z.string().optional(), +}); +export type Clip = z.infer; + +/** Preview selection only. All selected times and original indexes remain explicit. */ +export function previewIndexes(times: readonly number[]): number[] { + const selected = [0], + step = Math.max(0.1, (times.at(-1)! - times[0]) / 119); + for (let i = 1; i < times.length - 1; i++) + if (times[i] - times[selected.at(-1)!] >= step) selected.push(i); + if (times.length > 1) selected.push(times.length - 1); + return selected; +} +export function frameDelays(times: readonly number[]): number[] { + return times.map((time, i) => + Math.max(10, Math.round(((times[i + 1] ?? time + 0.1) - time) * 1000)), + ); +} +function timestampLabel(time: number): Buffer { + return Buffer.from( + `t = ${time.toFixed(3)} s`, + ); +} + +/** Local presentation of an already exported, finite result. No DimOS query executes here. */ +export async function renderClip( + paths: Paths, + source: string, + data: unknown, + signal?: AbortSignal, + frameIndex?: number, + progress?: (done: number, total: number) => void, +): Promise<{ clip: Clip; png: Buffer; summary: string }> { + const input = clipSourceSchema.parse(data), + times = input.frames.map((frame) => frame.timestamp); + if (times.at(-1)! - times[0] > 60) + throw new Error( + "Clip exceeds 60 seconds; select a smaller interval in DimOS.", + ); + if ( + frameIndex !== undefined && + (!Number.isInteger(frameIndex) || + frameIndex < 0 || + frameIndex >= times.length) + ) + throw new Error( + `Choose a source frame index from 0 to ${times.length - 1}.`, + ); + const indexes = previewIndexes(times); + const required = [ + ...new Set([...indexes, ...(frameIndex === undefined ? [] : [frameIndex])]), + ]; + const loaded = new Map< + number, + { bytes: Buffer; sha256: string; source: string; cloud?: Cloud } + >(); + let totalBytes = 0, + totalPoints = 0; + for (const i of required) { + signal?.throwIfAborted(); + const frame = input.frames[i], + file = resolve(dirname(source), frame.path); + const value = await readArtifact(file, frame.sha256); + totalBytes += value.bytes.length; + if (totalBytes > 128 * 1024 * 1024) + throw new Error( + "Clip source exceeds 128 MiB; select a smaller interval in DimOS.", + ); + const cloud = + input.type === "points" + ? cloudSchema.parse(JSON.parse(value.bytes.toString("utf8"))) + : undefined; + if (cloud) { + totalPoints += cloud.points.length; + if (totalPoints > 2_000_000) + throw new Error( + "Clip exceeds two million source points; export a smaller preview from DimOS.", + ); + if ( + cloud.timestamp !== undefined && + Math.abs(cloud.timestamp - frame.timestamp) > 0.000001 + ) + throw new Error("Cloud timestamp differs from its frame index."); + } + loaded.set(i, { ...value, source: file, cloud }); + } + const clouds = [...loaded.values()].flatMap((value) => + value.cloud ? [value.cloud] : [], + ); + if (new Set(clouds.map((cloud) => cloud.frame)).size > 1) + throw new Error( + "Mixed coordinate frames; align the data in DimOS before rendering.", + ); + const camera = clouds.length ? fitClouds(clouds) : undefined; + const selected = frameIndex === undefined ? indexes : [frameIndex]; + const clip: Clip = { + type: input.type, + timeOrigin: input.timeOrigin ?? times[0], + sourceCount: times.length, + frames: [], + }; + const images: Buffer[] = []; + let outputBytes = 0; + for (const index of selected) { + signal?.throwIfAborted(); + const value = loaded.get(index)!; + const pixels = value.cloud + ? await paintCloud(value.cloud, camera) + : value.bytes; + const png = await sharp(pixels, { density: 144 }) + .resize(900, 448, { fit: "contain", background: "#101b21" }) + .flatten({ background: "#101b21" }) + .extend({ bottom: 28, background: "#0c1216" }) + .composite([ + { + input: timestampLabel(times[index] - clip.timeOrigin), + top: 448, + left: 0, + }, + ]) + .removeAlpha() + .png() + .toBuffer(); + signal?.throwIfAborted(); + outputBytes += png.length; + if (outputBytes > 64 * 1024 * 1024) + throw new Error( + "Clip preview exceeds 64 MiB; export a smaller preview from DimOS.", + ); + const path = await cacheImage(paths, png); + const sha256 = path.split("/").at(-1)!.split(".")[0]; + clip.frames.push({ + path, + sha256, + timestamp: times[index], + source: value.source, + sourceSha256: value.sha256, + index, + }); + images.push(png); + progress?.(images.length, selected.length); + } + if (images.length > 1) { + const gif = await sharp(images, { join: { animated: true } }) + .gif({ + loop: 0, + delay: frameDelays(clip.frames.map((frame) => frame.timestamp)), + effort: 3, + }) + .toBuffer(); + signal?.throwIfAborted(); + if (gif.length > 32 * 1024 * 1024) + throw new Error( + "GIF exceeds 32 MiB; export a smaller preview from DimOS.", + ); + clip.gif = await cacheImage(paths, gif, "gif"); + } + const chosen = [ + ...new Set( + Array.from({ length: Math.min(6, images.length) }, (_, i) => + Math.round( + (i * (images.length - 1)) / + Math.max(1, Math.min(6, images.length) - 1), + ), + ), + ), + ]; + const tiles = await Promise.all( + chosen.map(async (index, i) => ({ + input: await sharp(images[index]).resize(360, 190).png().toBuffer(), + left: (i % 3) * 360, + top: Math.floor(i / 3) * 190, + })), + ); + const png = + images.length === 1 + ? images[0] + : await sharp({ + create: { + width: Math.min(3, tiles.length) * 360, + height: Math.ceil(tiles.length / 3) * 190, + channels: 3, + background: "#101b21", + }, + }) + .composite(tiles) + .png() + .toBuffer(); + const summary = `${selected.length}/${times.length} source frames · ${input.type} · ${(times[0] - clip.timeOrigin).toFixed(3)}–${(times.at(-1)! - clip.timeOrigin).toFixed(3)} s; ${camera ? "fixed camera and height scale; " : ""}model image shows ${chosen.length} timestamped frames; ${input.source ?? "source index"}`; + signal?.throwIfAborted(); + return { clip, png, summary }; +} diff --git a/agent/src/config.ts b/agent/src/config.ts new file mode 100644 index 00000000000..7f707008cd0 --- /dev/null +++ b/agent/src/config.ts @@ -0,0 +1,57 @@ +import { mkdir, readFile, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join, resolve } from "node:path"; +import { z } from "zod"; + +export const endpointSchema = z.object({ + name: z.string().regex(/^[a-z][a-z0-9_-]*$/), + url: z.url(), +}); +export const configSchema = z.object({ + workspace: z.string().default(process.cwd()), + python: z.string().optional(), + dimos: z.string().optional(), + mcp: z.array(endpointSchema).default([]), + relay: z.url().optional(), + robot: z.string().optional(), +}); +export type Config = z.infer; +export type Endpoint = z.infer; +export const paths = (env: NodeJS.ProcessEnv = process.env) => { + const home = homedir(); + const config = resolve( + env.DIMCODE_HOME ?? + join(env.XDG_CONFIG_HOME ?? join(home, ".config"), "dimcode"), + ); + const state = join( + env.XDG_STATE_HOME ?? join(home, ".local/state"), + "dimcode", + ); + return { + config, + state, + cache: join(env.XDG_CACHE_HOME ?? join(home, ".cache"), "dimcode"), + sessions: join(state, "sessions"), + socket: join(env.XDG_RUNTIME_DIR ?? state, "dimcode.sock"), + }; +}; +export type Paths = ReturnType; +export async function loadConfig(p: Paths): Promise { + try { + return configSchema.parse( + JSON.parse(await readFile(join(p.config, "config.json"), "utf8")), + ); + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") + return configSchema.parse({}); + throw error; + } +} +export async function saveConfig(p: Paths, config: Config): Promise { + await mkdir(p.config, { recursive: true, mode: 0o700 }); + await writeFile( + join(p.config, "config.json"), + JSON.stringify(configSchema.parse(config), null, 2) + "\n", + { mode: 0o600 }, + ); +} diff --git a/agent/src/gateway.ts b/agent/src/gateway.ts new file mode 100644 index 00000000000..c78c60d1631 --- /dev/null +++ b/agent/src/gateway.ts @@ -0,0 +1,536 @@ +import { randomUUID } from "node:crypto"; +import { chmod, lstat, mkdir, unlink } from "node:fs/promises"; +import { createServer, type Socket } from "node:net"; +import { dirname } from "node:path"; +import { SessionManager } from "@earendil-works/pi-coding-agent"; +import type { AuthInteraction, AuthPrompt } from "@earendil-works/pi-ai"; +import type { Config, Paths } from "./config.js"; +import { openSession, type SessionHandle } from "./session.js"; +import { + type Command, + type Event, + type Packet, + type Results, + type Snapshot, + readLines, + reachable, + requestSchema, + send, +} from "./protocol.js"; + +interface Entry { + handle: SessionHandle; + peers: Set; + owner?: Socket; + seq: number; + running: boolean; + text: string; + notices: string[]; + tools: Map; + accepted: Set; + dialogs: Map< + string, + { resolve: (value: string) => void; reject: (error: Error) => void } + >; +} +export type SessionFactory = typeof openSession; +export class Gateway { + private closing = false; + private readonly sockets = new Set(); + private readonly opening = new Map>(); + private readonly entries = new Map(); + private readonly peers = new Map(); + private readonly server = createServer((socket) => this.connect(socket)); + constructor( + private readonly config: Config, + private readonly paths: Paths, + private readonly factory: SessionFactory = openSession, + ) {} + async start(): Promise { + await mkdir(dirname(this.paths.socket), { recursive: true, mode: 0o700 }); + const listen = () => + new Promise((resolve, reject) => { + const failed = (error: Error) => { + this.server.off("listening", ready); + reject(error); + }; + const ready = () => { + this.server.off("error", failed); + resolve(); + }; + this.server.once("error", failed); + this.server.once("listening", ready); + this.server.listen(this.paths.socket); + }); + try { + await listen(); + } catch (error) { + if ( + !( + error instanceof Error && + "code" in error && + error.code === "EADDRINUSE" + ) || + !(await lstat(this.paths.socket)).isSocket() || + (await reachable(this.paths.socket)) + ) + throw error; + await unlink(this.paths.socket); + await listen(); + } + await chmod(this.paths.socket, 0o600); + } + private emit(entry: Entry, event: Event): void { + if (event.type === "notice" || event.type === "turn_error") { + entry.notices.push(event.message); + entry.notices = entry.notices.slice(-10); + } + const packet: Packet = { + type: "event", + sessionId: entry.handle.session.sessionId, + seq: ++entry.seq, + event, + }; + for (const peer of entry.peers) send(peer, packet); + } + private async restore( + id?: string, + cwd = this.config.workspace, + ): Promise { + const key = id ?? randomUUID(); + const pending = this.opening.get(key); + if (pending) return pending; + const creating = this.makeEntry(id, cwd); + this.opening.set(key, creating); + try { + return await creating; + } finally { + this.opening.delete(key); + } + } + private async makeEntry( + id?: string, + cwd = this.config.workspace, + ): Promise { + if (id && this.entries.has(id)) return this.entries.get(id)!; + const saved = id + ? (await SessionManager.listAll(this.paths.sessions)).find( + (item) => item.id === id, + ) + : undefined; + if (id && !saved) throw new Error("Unknown session"); + const manager = saved + ? SessionManager.open(saved.path, this.paths.sessions) + : SessionManager.create(cwd, this.paths.sessions); + const notices: string[] = []; + let entry: Entry | undefined; + const handle = await this.factory( + this.config, + this.paths, + manager, + (message) => + entry + ? this.emit(entry, { type: "notice", message }) + : notices.push(message), + ); + if (this.closing) { + await handle.close(); + throw new Error("Gateway shutting down"); + } + entry = { + handle, + peers: new Set(), + seq: 0, + running: false, + text: "", + notices, + tools: new Map(), + accepted: new Set(), + dialogs: new Map(), + }; + for (const item of manager.getEntries()) + if ( + item.type === "custom" && + item.customType === "dimcode.prompt" && + typeof item.data === "string" + ) + entry.accepted.add(item.data); + const current = entry; + handle.session.subscribe((event) => { + if (event.type === "message_update") { + const update = event.assistantMessageEvent; + if (update.type === "text_delta") { + current.text += update.delta; + this.emit(current, { type: "text_delta", delta: update.delta }); + } + return; + } + if (event.type === "message_start" && event.message.role === "assistant") + current.text = ""; + if (event.type === "message_end" && event.message.role === "assistant") + current.text = ""; + if ( + event.type === "tool_execution_start" || + event.type === "tool_execution_update" + ) + current.tools.set(event.toolCallId, event); + if (event.type === "tool_execution_end") + current.tools.delete(event.toolCallId); + this.emit(current, event); + }); + const baseUI = handle.session.extensionRunner.getUIContext(); + const interaction = this.interaction(current); + await handle.session.bindExtensions({ + mode: "rpc", + uiContext: { + ...baseUI, + select: (title, options, opts) => + interaction.prompt({ + type: "select", + message: title, + options: options.map((value) => ({ id: value, label: value })), + signal: opts?.signal, + }), + confirm: async (title, message, opts) => + (await interaction.prompt({ + type: "select", + message: title + "\n" + message, + options: [ + { id: "yes", label: "Yes" }, + { id: "no", label: "No" }, + ], + signal: opts?.signal, + })) === "yes", + input: (title, placeholder, opts) => + interaction.prompt({ + type: "text", + message: title, + placeholder, + signal: opts?.signal, + }), + editor: (title, prefill) => + interaction.prompt({ + type: "text", + message: title, + placeholder: prefill, + }), + notify: (message) => this.emit(current, { type: "notice", message }), + setStatus: (key, message) => { + if (message) + this.emit(current, { + type: "notice", + message: key + ": " + message, + }); + }, + custom: async () => { + throw new Error( + "Executable UI factories cannot cross a socket; install the component in the dimcode terminal renderer.", + ); + }, + }, + onError: (error) => + this.emit(current, { type: "notice", message: error.error }), + }); + this.entries.set(handle.session.sessionId, current); + if (saved) + notices.push( + "Resumed saved history. Interrupted external actions are not replayed; inspect DimOS status before continuing.", + ); + for (const message of notices) + handle.session.sessionManager.appendCustomEntry( + "dimcode.notice", + message, + ); + return current; + } + private snapshot(entry: Entry, socket: Socket): Snapshot { + const session = entry.handle.session; + const snapshot: Snapshot = { + model: session.model + ? { provider: session.model.provider, id: session.model.id } + : undefined, + sessionId: session.sessionId, + cwd: session.sessionManager.getCwd(), + seq: entry.seq, + busy: entry.running, + writable: entry.owner === socket, + messages: [], + text: entry.text.slice(-256_000), + notices: entry.notices.map((message) => message.slice(-4096)), + tools: [], + }; + // Bound only the terminal transfer; Pi retains the complete session and model context. + let budget = + 8 * 1024 * 1024 - Buffer.byteLength(JSON.stringify(snapshot)) - 1024; + for (const tool of entry.tools.values()) { + const size = Buffer.byteLength(JSON.stringify(tool)) + 1; + if (size > budget) continue; + snapshot.tools.push(tool); + budget -= size; + } + for (let index = session.messages.length - 1; index >= 0; index--) { + const message = session.messages[index]; + const size = Buffer.byteLength(JSON.stringify(message)) + 1; + if (size > budget) break; + snapshot.messages.unshift(message); + budget -= size; + } + const omitted = session.messages.length - snapshot.messages.length; + if (omitted) + snapshot.notices.push( + `${omitted} earlier messages omitted from this terminal view. Full agent history is retained on disk.`, + ); + if (snapshot.tools.length < entry.tools.size) + snapshot.notices.push( + "Oversized active tool previews omitted from this terminal view.", + ); + return snapshot; + } + private detach(socket: Socket): void { + const entry = this.peers.get(socket); + if (!entry) return; + entry.peers.delete(socket); + this.peers.delete(socket); + if (entry.owner === socket) { + entry.owner = undefined; + for (const dialog of entry.dialogs.values()) + dialog.reject(new Error("Input terminal detached")); + entry.dialogs.clear(); + } + } + private attach(entry: Entry, socket: Socket, writable: boolean): Snapshot { + if (writable && entry.owner && entry.owner !== socket) + throw new Error( + "Session has an input owner; attach with --view or detach the other terminal.", + ); + this.detach(socket); + entry.peers.add(socket); + this.peers.set(socket, entry); + if (writable) entry.owner = socket; + return this.snapshot(entry, socket); + } + private connect(socket: Socket): void { + this.sockets.add(socket); + socket.on("error", () => {}); + socket.on("close", () => { + this.detach(socket); + this.sockets.delete(socket); + }); + readLines(socket, (value) => { + const parsed = requestSchema.safeParse(value); + if (!parsed.success) { + socket.destroy(); + return; + } + const { id, command } = parsed.data; + void this.dispatch(socket, command, id).then( + (data) => send(socket, { type: "response", id, data }), + (error) => + send(socket, { + type: "response", + id, + error: error instanceof Error ? error.message : String(error), + }), + ); + }); + } + private interaction(entry: Entry): AuthInteraction { + return { + prompt: (prompt: AuthPrompt) => + new Promise((resolve, reject) => { + if (!entry.owner) { + reject( + new Error("This interaction needs an attached input terminal"), + ); + return; + } + if (entry.dialogs.size) { + reject(new Error("Finish the existing terminal dialog first")); + return; + } + if (prompt.signal?.aborted) { + reject(new Error("Interaction cancelled")); + return; + } + const promptId = randomUUID(); + const { signal, ...wirePrompt } = prompt; + const aborted = () => + entry.dialogs + .get(promptId) + ?.reject(new Error("Interaction cancelled")); + const cleanup = () => { + entry.dialogs.delete(promptId); + signal?.removeEventListener("abort", aborted); + }; + entry.dialogs.set(promptId, { + resolve: (value) => { + cleanup(); + resolve(value); + }, + reject: (error) => { + cleanup(); + reject(error); + }, + }); + signal?.addEventListener("abort", aborted, { once: true }); + send(entry.owner, { + type: "event", + sessionId: entry.handle.session.sessionId, + seq: ++entry.seq, + event: { type: "auth_prompt", promptId, prompt: wirePrompt }, + }); + }), + notify: (info) => { + if (entry.owner) + send(entry.owner, { + type: "event", + sessionId: entry.handle.session.sessionId, + seq: ++entry.seq, + event: { type: "auth_info", info }, + }); + }, + }; + } + private async dispatch( + socket: Socket, + command: Command, + id: string, + ): Promise { + if (this.closing) throw new Error("Gateway shutting down"); + if (command.type === "shutdown") { + setImmediate(() => void this.close()); + return null; + } + if (command.type === "new_session") + return this.attach( + await this.restore(undefined, command.cwd), + socket, + true, + ); + if (command.type === "attach") + return this.attach( + await this.restore(command.sessionId), + socket, + command.writable, + ); + if (command.type === "list_sessions") { + const saved = await SessionManager.listAll(this.paths.sessions); + const list = new Map( + saved.map((item) => [ + item.id, + { id: item.id, cwd: item.cwd, name: item.name ?? item.firstMessage }, + ]), + ); + for (const [sessionId, entry] of this.entries) + if (!list.has(sessionId)) + list.set(sessionId, { + id: sessionId, + cwd: entry.handle.session.sessionManager.getCwd(), + name: "", + }); + return [...list.values()]; + } + const entry = this.peers.get(socket); + if (!entry) throw new Error("Attach to a session first"); + const session = entry.handle.session; + if (command.type === "get_state") return this.snapshot(entry, socket); + if (command.type === "get_available_models") + return session.modelRuntime.getModels().map((model) => ({ + provider: model.provider, + id: model.id, + name: model.name, + })); + if (entry.owner !== socket) throw new Error("This terminal is read-only"); + switch (command.type) { + case "prompt": { + if (entry.accepted.has(id)) return null; + if (entry.running) + throw new Error("Agent is busy; use steer or follow_up"); + entry.accepted.add(id); + session.sessionManager.appendCustomEntry("dimcode.prompt", id); + entry.running = true; + // Acknowledge acceptance without tying turn lifetime to the connection. + void session + .prompt(command.message, { images: command.images }) + .then(() => { + const last = session.messages.at(-1); + if (last?.role === "assistant" && last.stopReason === "error") + throw new Error(last.errorMessage ?? "Model request failed"); + }) + .catch((error) => + this.emit(entry, { type: "turn_error", message: String(error) }), + ) + .finally(() => { + entry.running = false; + this.emit(entry, { type: "idle" }); + }); + return null; + } + case "steer": + await session.steer(command.message); + return null; + case "follow_up": + await session.followUp(command.message); + return null; + case "abort": + await session.abort(); + this.emit(entry, { + type: "notice", + message: + "Agent turn cancelled. Use the DimOS skill's stop operation to confirm any background robot action stopped.", + }); + return null; + case "ui_response": { + const dialog = entry.dialogs.get(command.promptId); + if (!dialog) throw new Error("Unknown or expired dialog"); + if (command.value === undefined) dialog.reject(new Error("Cancelled")); + else dialog.resolve(command.value); + return null; + } + case "login": + if (command.provider === "anthropic" && command.authType === "oauth") + throw new Error("Use an Anthropic API key for dimcode."); + await session.modelRuntime.login( + command.provider, + command.authType, + this.interaction(entry), + ); + return null; + case "logout": + await session.modelRuntime.logout(command.provider); + return null; + case "set_model": { + if (entry.running) + throw new Error("Change models after the current turn"); + const model = session.modelRuntime.getModel( + command.provider, + command.modelId, + ); + if (!model) throw new Error("Unknown provider/model"); + await session.setModel(model); + return null; + } + case "reload": + if (entry.running) throw new Error("Reload after the current turn"); + await session.reload(); + return null; + } + } + async close(): Promise { + if (this.closing) return; + this.closing = true; + for (const socket of this.sockets) socket.destroy(); + await Promise.allSettled( + [...this.entries.values()].map(async (entry) => { + try { + await entry.handle.session.abort(); + } finally { + await entry.handle.close(); + } + }), + ); + this.entries.clear(); + await new Promise((resolve) => this.server.close(() => resolve())); + await unlink(this.paths.socket).catch(() => {}); + } +} diff --git a/agent/src/input.ts b/agent/src/input.ts new file mode 100644 index 00000000000..96c30e71650 --- /dev/null +++ b/agent/src/input.ts @@ -0,0 +1,121 @@ +import { + CombinedAutocompleteProvider, + Editor, + Input, + matchesKey, + truncateToWidth, + type Component, + type TUI, +} from "@earendil-works/pi-tui"; +import { getSelectListTheme } from "@earendil-works/pi-coding-agent"; +import { accent } from "./terminal-style.js"; + +export const commands = [ + "help", + "new", + "sessions", + "resume", + "models", + "model", + "login", + "logout", + "abort", + "steer", + "follow", + "reload", + "image", + "inspect", + "view", + "panel", + "expand", + "exit", +]; + +/** Pi owns editing, multiline paste, history and file/command completion. */ +export class PromptEditor extends Editor { + private credential = new ChatInput(); + secret = false; + onEscape?: () => void; + constructor(tui: TUI) { + super( + tui, + { selectList: getSelectListTheme(), borderColor: accent }, + { paddingX: 1 }, + ); + } + setWorkspace(cwd: string): void { + this.setAutocompleteProvider( + new CombinedAutocompleteProvider( + commands.map((name) => ({ name })), + cwd, + ), + ); + } + getValue(): string { + return this.secret ? this.credential.getValue() : this.getExpandedText(); + } + setValue(value: string): void { + this.credential.secret = this.secret; + this.credential.setValue(value); + if (!this.secret) this.setText(value); + } + override handleInput(data: string): void { + if (matchesKey(data, "escape") && !this.isShowingAutocomplete()) { + this.onEscape?.(); + return; + } + if (this.secret) { + this.credential.onSubmit = (value) => this.onSubmit?.(value); + this.credential.handleInput(data); + } else super.handleInput(data); + } + override render(width: number): string[] { + if (this.secret) return this.credential.render(width); + return super.render(width); + } +} + +export class ChatInput implements Component { + private editor: Input; + private masked = false; + focused = false; + onSubmit?: (value: string) => void; + onEscape?: () => void; + constructor(private placeholder = "") { + this.editor = new Input({ prompt: "› ", placeholder }); + } + get secret(): boolean { + return this.masked; + } + set secret(value: boolean) { + if (value === this.masked) return; + this.masked = value; + // Discard the credential editor and its undo/kill-ring history on mode change. + this.editor = new Input({ prompt: "› ", placeholder: this.placeholder }); + } + getValue(): string { + return this.editor.getValue(); + } + setValue(value: string): void { + this.editor.setValue(value); + } + invalidate(): void { + this.editor.invalidate(); + } + handleInput(data: string): void { + this.editor.focused = this.focused; + this.editor.onSubmit = (value) => this.onSubmit?.(value); + this.editor.onEscape = () => this.onEscape?.(); + this.editor.handleInput(data); + } + render(width: number): string[] { + return this.secret + ? [ + truncateToWidth( + "credential › " + "*".repeat(this.getValue().length), + width, + ), + ] + : this.editor.render(width); + } +} diff --git a/agent/src/main.ts b/agent/src/main.ts new file mode 100644 index 00000000000..da02a0cc337 --- /dev/null +++ b/agent/src/main.ts @@ -0,0 +1,221 @@ +import { spawn } from "node:child_process"; +import { mkdir, rm, rmdir, stat } from "node:fs/promises"; +import { dirname, resolve } from "node:path"; +import { parseArgs } from "node:util"; +import { setTimeout as delay } from "node:timers/promises"; +import { paths, loadConfig, saveConfig } from "./config.js"; +import { Gateway } from "./gateway.js"; +import { Connection, reachable } from "./protocol.js"; +import { setup, needsSetup } from "./setup.js"; +import { service } from "./service.js"; +import { terminal } from "./terminal.js"; + +const { values, positionals } = parseArgs({ + allowPositionals: true, + options: { + foreground: { type: "boolean" }, + view: { type: "boolean" }, + session: { type: "string" }, + cwd: { type: "string" }, + provider: { type: "string" }, + oauth: { type: "boolean" }, + "key-env": { type: "string" }, + help: { type: "boolean" }, + }, +}); +const p = paths(); +let config = await loadConfig(p); +let onboarding = false; +const setupPrompt = + "Help me set up DimOS using the dimensional-install skill. Start by asking me to drop an existing checkout/environment path or describe what I want to build. Wait for my answer before installing anything."; +if (values.cwd) config.workspace = resolve(values.cwd); +async function ensureGateway(): Promise { + if (await reachable(p.socket)) return; + const lock = p.socket + ".starting"; + try { + if (Date.now() - (await stat(lock)).mtimeMs > 30000) + await rm(lock, { recursive: true, force: true }); + } catch {} + await mkdir(dirname(p.socket), { recursive: true, mode: 0o700 }); + try { + await mkdir(lock, { mode: 0o700 }); + try { + if (await reachable(p.socket)) return; + const child = spawn( + process.execPath, + [resolve(process.argv[1]), "gateway"], + { detached: true, stdio: "ignore", env: process.env }, + ); + child.unref(); + for (let attempt = 0; attempt < 100; attempt++) { + if (await reachable(p.socket)) return; + await delay(50); + } + throw new Error( + "Gateway failed to start; run dimcode gateway to inspect the error.", + ); + } finally { + await rmdir(lock); + } + } catch (error) { + if (!(error instanceof Error && "code" in error && error.code === "EEXIST")) + throw error; + for (let attempt = 0; attempt < 100; attempt++) { + if (await reachable(p.socket)) return; + await delay(50); + } + throw new Error("Gateway bootstrap did not complete. Inspect " + lock); + } +} +async function main(): Promise { + const [command, ...args] = positionals; + if (values.help || command === "help") { + console.log( + "dimcode [tui] [--session ID] [--cwd DIR] [--view]\ndimcode setup [--provider NAME] [--oauth | --key-env VAR]\ndimcode gateway | --foreground | service install/status/uninstall\ndimcode connect NAME MCP_URL | dimos PATH | python PATH | workspace PATH | relay URL [ROBOT]\ndimcode sessions | stop | run PROMPT | config", + ); + return; + } + if (command === "setup") { + onboarding = await setup(p, { + provider: values.provider, + oauth: values.oauth, + keyEnv: values["key-env"], + workspace: values.cwd, + }); + if (!onboarding) return; + config = await loadConfig(p); + } + if (command === "config") { + console.log(JSON.stringify(config, null, 2)); + return; + } + if (command === "connect") { + const [name, url] = args; + config.mcp = [ + ...config.mcp.filter((endpoint) => endpoint.name !== name), + { name, url }, + ]; + await saveConfig(p, config); + console.log("Saved endpoint; reload sessions to discover tools."); + return; + } + if (command === "dimos" || command === "python" || command === "workspace") { + if (!args[0]) throw new Error("Path required"); + if ( + command === "workspace" && + !(await stat(resolve(args[0]))).isDirectory() + ) + throw new Error("Workspace must be a directory"); + config[command] = resolve(args[0]); + await saveConfig(p, config); + return; + } + if (command === "relay") { + [config.relay, config.robot] = args; + await saveConfig(p, config); + return; + } + if (command === "service") { + await service(args[0], config, p, process.argv[1]); + return; + } + if ( + command && + !["gateway", "stop", "sessions", "run", "tui", "setup"].includes(command) + ) + throw new Error("Unknown command: " + command + ". Run dimcode --help."); + if ( + (!command || command === "tui" || values.foreground) && + (await needsSetup(p)) + ) { + onboarding = await setup(p, { workspace: values.cwd }); + config = await loadConfig(p); + } + if (command === "gateway" || values.foreground) { + if (!values.foreground && (await reachable(p.socket))) { + console.log( + "Gateway already running. Open the terminal with dimcode tui.", + ); + return; + } + if (values.foreground) p.socket = p.socket + "." + process.pid; + const gateway = new Gateway(config, p); + await gateway.start(); + if (values.foreground) { + try { + await terminal(p.socket, { + cwd: config.workspace, + initialPrompt: onboarding ? setupPrompt : undefined, + }); + } finally { + await gateway.close(); + } + return; + } + let closing = false; + for (const signal of ["SIGTERM", "SIGINT"] as const) + process.on(signal, () => { + if (!closing) { + closing = true; + void gateway.close().catch(console.error); + } + }); + return; + } + if (command === "stop") { + const c = new Connection(p.socket); + try { + await c.call({ type: "shutdown" }); + } finally { + c.close(); + } + return; + } + await ensureGateway(); + if (command === "sessions") { + const c = new Connection(p.socket); + try { + console.log(await c.call({ type: "list_sessions" })); + } finally { + c.close(); + } + return; + } + if (command === "run") { + const c = new Connection(p.socket); + try { + await c.call( + values.session + ? { type: "attach", sessionId: values.session, writable: true } + : { type: "new_session", cwd: config.workspace }, + ); + const done = new Promise((resolve, reject) => { + c.onEvent = (_seq, event) => { + if (event.type === "text_delta") process.stdout.write(event.delta); + if (event.type === "notice") + process.stderr.write(event.message + "\n"); + if (event.type === "turn_error") reject(new Error(event.message)); + if (event.type === "idle") resolve(); + }; + c.onClose = () => + reject(new Error("Gateway disconnected before completion")); + }); + await c.call({ type: "prompt", message: args.join(" ") }); + await done; + process.stdout.write("\n"); + } finally { + c.close(); + } + return; + } + await terminal(p.socket, { + sessionId: onboarding ? undefined : values.session, + cwd: config.workspace, + view: onboarding ? false : values.view, + initialPrompt: onboarding ? setupPrompt : undefined, + }); +} +await main().catch((error) => { + console.error(error instanceof Error ? error.message : String(error)); + process.exitCode = 1; +}); diff --git a/agent/src/mcp.ts b/agent/src/mcp.ts new file mode 100644 index 00000000000..86f4280589a --- /dev/null +++ b/agent/src/mcp.ts @@ -0,0 +1,213 @@ +import { createHash } from "node:crypto"; +import { + Client, + StreamableHTTPClientTransport, + type CallToolResult, + type ProgressNotification, +} from "@modelcontextprotocol/client"; +import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; +import type { + ExtensionFactory, + ToolDefinition, +} from "@earendil-works/pi-coding-agent"; +import { z } from "zod"; +import { Type, type TUnsafe } from "typebox"; +import type { Endpoint } from "./config.js"; + +export type McpDetails = { + endpoint: string; + tool: string; + result: Omit; +}; +export function resultContent( + content: CallToolResult["content"], +): Array { + return content.map((block) => { + if (block.type === "text") return { type: "text", text: block.text }; + if (block.type === "image") + return { type: "image", data: block.data, mimeType: block.mimeType }; + if (block.type === "resource" && "text" in block.resource) + return { type: "text", text: block.resource.text }; + return { type: "text", text: JSON.stringify(block) }; + }); +} +export const toolName = (endpoint: string, name: string): string => + "dimos_" + + endpoint.slice(0, 12) + + "_" + + name.replace(/[^a-zA-Z0-9_-]/g, "_").slice(0, 28) + + "_" + + createHash("sha256") + .update(endpoint + "\0" + name) + .digest("hex") + .slice(0, 10); + +type McpTool = ToolDefinition>, McpDetails>; + +export class McpTools { + private readonly clients = new Map(); + private readonly urls = new Map(); + private readonly progress = new Map< + string | number, + (value: ProgressNotification["params"]) => void + >(); + constructor( + private readonly endpoints: () => Promise, + private readonly report: (message: string) => void = () => {}, + ) {} + async tools(): Promise { + const tools: McpTool[] = []; + const endpoints = await this.endpoints(); + for (const [name, client] of this.clients) + if ( + !endpoints.some( + (endpoint) => + endpoint.name === name && endpoint.url === this.urls.get(name), + ) + ) { + await client.close(); + this.clients.delete(name); + } + for (const endpoint of endpoints) { + let client = this.clients.get(endpoint.name); + try { + if (!client) { + client = new Client({ name: "dimcode", version: "0.1.0" }); + client.onerror = (error) => + this.report(endpoint.name + ": " + error.message); + const connected = client; + client.onclose = () => { + if (this.clients.get(endpoint.name) === connected) + this.clients.delete(endpoint.name); + this.report(endpoint.name + ": disconnected"); + }; + client.setNotificationHandler( + "notifications/message", + (notification) => + this.report( + endpoint.name + ": " + JSON.stringify(notification.params.data), + ), + ); + client.setNotificationHandler( + "notifications/progress", + (notification) => { + const progress = notification.params; + const update = this.progress.get(progress.progressToken); + if (update) update(progress); + else + this.report( + endpoint.name + + " background " + + progress.progressToken + + ": " + + (progress.message ?? progress.progress), + ); + }, + ); + client.fallbackNotificationHandler = async (notification) => + this.report(endpoint.name + ": " + JSON.stringify(notification)); + await client.connect( + new StreamableHTTPClientTransport(new URL(endpoint.url)), + ); + this.clients.set(endpoint.name, client); + this.urls.set(endpoint.name, endpoint.url); + } + const connection = client; + const remoteTools = []; + let cursor: string | undefined; + do { + const listing = await connection.listTools({ cursor }); + remoteTools.push(...listing.tools); + cursor = listing.nextCursor; + } while (cursor); + const names = new Set(); + for (const remote of remoteTools) { + if (names.has(remote.name)) + throw new Error("Duplicate remote tool name: " + remote.name); + names.add(remote.name); + const parameters = Type.Unsafe>( + remote.inputSchema, + ); + const tool: ToolDefinition = { + name: toolName(endpoint.name, remote.name), + label: endpoint.name + " / " + remote.name, + description: + (remote.description ?? remote.name) + + "\nDimOS metadata: " + + JSON.stringify(remote._meta ?? {}), + parameters, + execute: async (id, args, signal, update) => { + if (this.clients.get(endpoint.name) !== connection) + throw new Error("MCP endpoint disconnected; refresh tools."); + this.progress.set(id, (progress) => + update?.({ + content: [ + { + type: "text", + text: progress.message ?? String(progress.progress), + }, + ], + details: { + endpoint: endpoint.name, + tool: remote.name, + result: {}, + }, + }), + ); + try { + const result = await connection.callTool( + { + name: remote.name, + arguments: args, + _meta: { progressToken: id }, + }, + { signal }, + ); + const { content, ...metadata } = result; + return { + content: resultContent(content), + details: { + endpoint: endpoint.name, + tool: remote.name, + result: metadata, + }, + }; + } finally { + this.progress.delete(id); + } + }, + }; + tools.push(tool); + } + } catch (error) { + await client?.close(); + this.clients.delete(endpoint.name); + this.report(endpoint.name + ": " + String(error)); + } + } + return tools; + } + extension(): ExtensionFactory { + return async (pi) => { + for (const tool of await this.tools()) pi.registerTool(tool); + pi.on("tool_result", (event) => { + const details = z + .object({ + endpoint: z.string(), + result: z.object({ isError: z.boolean().optional() }), + }) + .safeParse(event.details); + if (details.success && details.data.result.isError) + return { isError: true }; + return undefined; + }); + pi.on("session_shutdown", () => this.close()); + }; + } + async close(): Promise { + await Promise.all( + [...this.clients.values()].map((client) => client.close()), + ); + this.clients.clear(); + } +} diff --git a/agent/src/media.ts b/agent/src/media.ts new file mode 100644 index 00000000000..47db517007d --- /dev/null +++ b/agent/src/media.ts @@ -0,0 +1,91 @@ +import { + connect, + type ConnectOptions, + type Session, + type Slot, + type TransportDeps, +} from "@dimos/sdk"; + +export async function nodeTransport(): Promise { + const { WebTransport } = await import("rwebtransport"); + return { + createWebTransport: (info) => + new WebTransport(new URL("/viewer", info.wtUrl).href, { + serverCertificateHashes: info.certHash + ? [ + { + algorithm: "sha-256", + value: Uint8Array.from(Buffer.from(info.certHash, "base64")) + .buffer, + }, + ] + : [], + }), + }; +} +export type MediaSelection = Pick; +export type Decoder = (slot: Slot) => T; +export interface MediaLease { + current(): T | undefined; + close(): void; +} + +/** Only render consumers open connections; the last consumer closes the SDK session. */ +export class MediaPool { + private readonly connections = new Map< + string, + { session: Session; consumers: number } + >(); + private pending: Promise | undefined; + private closed = false; + constructor( + private readonly transport: () => Promise = nodeTransport, + private readonly options: Omit = {}, + private readonly create: typeof connect = connect, + ) {} + async acquire( + selection: MediaSelection, + channel: string, + decode: Decoder, + changed: () => void, + ): Promise> { + const key = JSON.stringify([selection.url, selection.robot]); + this.pending ??= this.transport(); + const deps = await this.pending; + if (this.closed) throw new Error("Media pool closed"); + let entry = this.connections.get(key); + if (!entry) { + entry = { + session: this.create({ ...this.options, ...selection }, deps), + consumers: 0, + }; + this.connections.set(key, entry); + } + entry.consumers++; + const held = entry; + const interest = held.session.subscribe(channel, changed); + const frames = held.session.store.subscribe(channel, changed); + let closed = false; + return { + current: () => { + const slot = held.session.store.get(channel); + return slot ? decode(slot) : undefined; + }, + close: () => { + if (closed) return; + closed = true; + frames(); + interest(); + if (--held.consumers === 0) { + held.session.close(); + this.connections.delete(key); + } + }, + }; + } + close(): void { + this.closed = true; + for (const { session } of this.connections.values()) session.close(); + this.connections.clear(); + } +} diff --git a/agent/src/playback.ts b/agent/src/playback.ts new file mode 100644 index 00000000000..13c92984e24 --- /dev/null +++ b/agent/src/playback.ts @@ -0,0 +1,95 @@ +import { Text, type Component } from "@earendil-works/pi-tui"; +import { readArtifact } from "./artifacts.js"; +import { frameDelays, type Clip } from "./clips.js"; +import { clean, muted } from "./terminal-style.js"; +import { TerminalImage } from "./terminal-image.js"; + +/** A looping presentation of saved frames. Frame selection belongs to the agent's tools. */ +export class Playback implements Component { + private images: TerminalImage[] = []; + private timer?: ReturnType; + private disposed = false; + private error?: string; + private index = 0; + private running = false; + readonly ready: Promise; + constructor( + readonly clip: Clip, + private readonly changed: () => void, + private autoplay = true, + ) { + this.ready = this.load() + .then(() => { + if (this.autoplay && !this.disposed && this.images.length > 1) { + this.running = true; + this.schedule(); + } + }) + .catch((error) => { + if (!this.disposed) { + this.error = clean(String(error)); + this.changed(); + } + }); + } + get frameIndex(): number { + return this.index; + } + get playing(): boolean { + return this.running; + } + private async load(): Promise { + for (const frame of this.clip.frames) { + if (this.disposed) return; + const { bytes } = await readArtifact(frame.path, frame.sha256); + if (this.disposed) return; + this.images.push( + new TerminalImage( + bytes.toString("base64"), + "image/png", + { fallbackColor: muted }, + { maxWidthCells: 120, maxHeightCells: 25 }, + this.changed, + ), + ); + } + this.changed(); + } + pause(): void { + this.autoplay = false; + this.running = false; + clearTimeout(this.timer); + } + private schedule(): void { + const delay = frameDelays(this.clip.frames.map((frame) => frame.timestamp))[ + this.index + ]; + this.timer = setTimeout(() => { + if (!this.running || this.disposed) return; + this.index = (this.index + 1) % this.images.length; + this.changed(); + this.schedule(); + }, delay); + } + close(): void { + this.disposed = true; + this.pause(); + for (const image of this.images) image.close(); + } + invalidate(): void { + for (const image of this.images) image.invalidate(); + } + render(width: number): string[] { + return this.images[this.index] && !this.error + ? this.images[this.index].render(width) + : new Text( + muted( + this.error + ? `Preview unavailable: ${this.error}` + : "Loading saved frames…", + ), + 0, + 0, + ).render(width); + } +} diff --git a/agent/src/points.ts b/agent/src/points.ts new file mode 100644 index 00000000000..94e7f2607bf --- /dev/null +++ b/agent/src/points.ts @@ -0,0 +1,133 @@ +import sharp from "sharp"; +import { z } from "zod"; +import { readArtifact } from "./artifacts.js"; + +const xyz = z.tuple([ + z.number().finite(), + z.number().finite(), + z.number().finite(), +]); +const rgb = z.tuple([ + z.number().int().min(0).max(255), + z.number().int().min(0).max(255), + z.number().int().min(0).max(255), +]); +export const cloudSchema = z + .object({ + points: z.array(xyz).min(1).max(1_000_000), + colors: z.array(rgb).optional(), + frame: z.string().optional(), + timestamp: z.number().finite().optional(), + selectedIndices: z.array(z.number().int().nonnegative()).optional(), + }) + .refine( + (cloud) => + cloud.selectedIndices?.every((i) => i < cloud.points.length) ?? true, + "Selection index outside point cloud", + ) + .refine( + (cloud) => !cloud.colors || cloud.colors.length === cloud.points.length, + "RGB count must match point count", + ); +export type Cloud = z.infer; +type XYZ = Cloud["points"][number]; +export interface Camera { + center: XYZ; + extent: number; + floor: number; + height: number; +} +export const POINT_LIMIT = 20_000; + +export async function readCloud( + path: string, + digest?: string, +): Promise<{ cloud: Cloud; sha256: string }> { + const { bytes, sha256 } = await readArtifact(path, digest); + return { + cloud: cloudSchema.parse(JSON.parse(bytes.toString("utf8"))), + sha256, + }; +} + +/** Shared presentation bounds; no registration, reconstruction or filtering. */ +export function fitClouds(clouds: readonly Cloud[]): Camera { + const low: XYZ = [Infinity, Infinity, Infinity], + high: XYZ = [-Infinity, -Infinity, -Infinity]; + for (const cloud of clouds) + for (const point of cloud.points) + for (let i = 0; i < 3; i++) { + low[i] = Math.min(low[i], point[i]); + high[i] = Math.max(high[i], point[i]); + } + return { + center: low.map((n, i) => (n + high[i]) / 2) as XYZ, + floor: low[2], + height: Math.max(high[2] - low[2], 0.001), + extent: Math.max(...high.map((n, i) => n - low[i]), 0.001), + }; +} +export function projectPoint( + point: XYZ, + camera: Camera, + width: number, + height: number, + angle = -0.58, + zoom = 1, +): [number, number] { + const [x, y, z] = point.map((v, i) => v - camera.center[i]); + const c = Math.cos(angle), + s = Math.sin(angle); + const scale = (Math.min(width / 1.65, height / 1.2) / camera.extent) * zoom; + return [ + width / 2 + (x * c - y * s) * scale, + height / 2 + (x * s + y * c) * scale * 0.5 - z * scale, + ]; +} +export async function paintCloud( + cloud: Cloud, + camera = fitClouds([cloud]), + angle = -0.58, + zoom = 1, +): Promise { + const selected = new Set(cloud.selectedIndices), + step = Math.max(1, Math.ceil(cloud.points.length / POINT_LIMIT)); + const circles: string[] = [], + grid: string[] = []; + for (let i = 0; i < cloud.points.length; i += step) { + const [x, y] = projectPoint(cloud.points[i], camera, 900, 400, angle, zoom); + const keep = selected.has(i), + elevation = (cloud.points[i][2] - camera.floor) / camera.height; + const color = keep + ? "#f5c078" + : cloud.colors + ? `rgb(${cloud.colors[i].join(",")})` + : "#6ae5cf"; + circles.push( + ``, + ); + } + for (let i = -3; i <= 3; i++) + for (const axis of [0, 1]) { + const a = [...camera.center] as XYZ, + b = [...camera.center] as XYZ; + a[axis] += (i * camera.extent) / 6; + b[axis] = a[axis]; + a[1 - axis] -= camera.extent / 2; + b[1 - axis] += camera.extent / 2; + a[2] = camera.floor; + b[2] = a[2]; + grid.push( + ``, + ); + } + const legend = `${cloud.points.length.toLocaleString()} points | display 1/${step} | ${cloud.colors ? "source RGB" : `height brightness z=${camera.floor.toFixed(2)}..${(camera.floor + camera.height).toFixed(2)}`} | ${selected.size} selected`; + return sharp( + Buffer.from( + `${grid.join("")}${circles.join("")}xzy${legend}`, + ), + ) + .removeAlpha() + .png() + .toBuffer(); +} diff --git a/agent/src/protocol.ts b/agent/src/protocol.ts new file mode 100644 index 00000000000..03329dce9af --- /dev/null +++ b/agent/src/protocol.ts @@ -0,0 +1,208 @@ +import { randomUUID } from "node:crypto"; +import { createConnection, type Socket } from "node:net"; +import { StringDecoder } from "node:string_decoder"; +import type { + AgentSession, + AgentSessionEvent, +} from "@earendil-works/pi-coding-agent"; +import type { AuthEvent, AuthPrompt } from "@earendil-works/pi-ai"; +import { z } from "zod"; + +const image = z.object({ + type: z.literal("image"), + data: z.string(), + mimeType: z.string(), +}); +export const commandSchema = z.discriminatedUnion("type", [ + z.object({ type: z.literal("new_session"), cwd: z.string().optional() }), + z.object({ + type: z.literal("attach"), + sessionId: z.string(), + writable: z.boolean().default(true), + }), + z.object({ type: z.literal("list_sessions") }), + z.object({ type: z.literal("shutdown") }), + z.object({ + type: z.literal("prompt"), + message: z.string(), + images: z.array(image).optional(), + }), + z.object({ type: z.literal("steer"), message: z.string() }), + z.object({ type: z.literal("follow_up"), message: z.string() }), + z.object({ type: z.literal("abort") }), + z.object({ type: z.literal("get_state") }), + z.object({ type: z.literal("get_available_models") }), + z.object({ + type: z.literal("set_model"), + provider: z.string(), + modelId: z.string(), + }), + z.object({ + type: z.literal("login"), + provider: z.string(), + authType: z.enum(["api_key", "oauth"]), + }), + z.object({ type: z.literal("logout"), provider: z.string() }), + z.object({ + type: z.literal("ui_response"), + promptId: z.string(), + value: z.string().optional(), + }), + z.object({ type: z.literal("reload") }), +]); +export const requestSchema = z.object({ id: z.uuid(), command: commandSchema }); +export type Command = z.infer; +export type Request = z.infer; +export type Kind = Command["type"]; +export type SessionEvent = + | Exclude + | { type: "text_delta"; delta: string }; +type WithoutSignal = T extends unknown ? Omit : never; +export type Event = + | SessionEvent + | { type: "notice" | "turn_error"; message: string } + | { type: "auth_prompt"; promptId: string; prompt: WithoutSignal } + | { type: "auth_info"; info: AuthEvent } + | { type: "idle" }; +export interface Snapshot { + model?: { provider: string; id: string }; + sessionId: string; + cwd: string; + seq: number; + busy: boolean; + writable: boolean; + messages: AgentSession["messages"]; + text: string; + notices: string[]; + tools: Array< + Extract< + AgentSessionEvent, + { type: "tool_execution_start" | "tool_execution_update" } + > + >; +} +export interface Results { + new_session: Snapshot; + attach: Snapshot; + get_state: Snapshot; + list_sessions: Array<{ id: string; cwd: string; name: string }>; + shutdown: null; + prompt: null; + steer: null; + follow_up: null; + abort: null; + get_available_models: Array<{ provider: string; id: string; name: string }>; + set_model: null; + login: null; + logout: null; + ui_response: null; + reload: null; +} +export type Packet = + | { type: "response"; id: string; data?: Results[Kind]; error?: string } + | { type: "event"; sessionId: string; seq: number; event: Event }; + +const MAX_BYTES = 16 * 1024 * 1024; +export function send(socket: Socket, packet: Packet | Request): void { + const line = JSON.stringify(packet) + "\n"; + if ( + Buffer.byteLength(line) > MAX_BYTES || + socket.writableLength > MAX_BYTES + ) { + socket.destroy( + new Error("Terminal connection exceeded its bounded buffer"), + ); + return; + } + socket.write(line); +} +export function readLines( + socket: Socket, + line: (value: unknown) => void, +): void { + const decoder = new StringDecoder("utf8"); + let pending = ""; + socket.on("data", (chunk: Buffer) => { + pending += decoder.write(chunk); + if (Buffer.byteLength(pending) > MAX_BYTES) { + socket.destroy(new Error("Message too large")); + return; + } + let end: number; + while ((end = pending.indexOf("\n")) >= 0) { + const next = pending.slice(0, end); + pending = pending.slice(end + 1); + try { + line(JSON.parse(next)); + } catch { + socket.destroy(new Error("Invalid protocol message")); + return; + } + } + }); +} +export class Connection { + private readonly socket: Socket; + private readonly pending = new Map< + string, + { resolve: (value: Results[Kind]) => void; reject: (error: Error) => void } + >(); + onEvent: (seq: number, event: Event, sessionId: string) => void = () => {}; + onClose: () => void = () => {}; + constructor(path: string) { + this.socket = createConnection(path); + readLines(this.socket, (value) => { + // Both ends share Packet; only the user-owned local socket is accepted. + const packet = value as Packet; + if (packet.type === "event") + this.onEvent(packet.seq, packet.event, packet.sessionId); + else if (packet.type === "response") { + const pending = this.pending.get(packet.id); + this.pending.delete(packet.id); + if (packet.error) pending?.reject(new Error(packet.error)); + else pending?.resolve(packet.data ?? null); + } else throw new Error("Unexpected packet"); + }); + this.socket.on("error", (error) => this.fail(error)); + this.socket.on("close", () => { + this.fail(new Error("Gateway disconnected")); + this.onClose(); + }); + } + call( + command: Extract, + id = randomUUID(), + ): Promise { + if (this.socket.destroyed) + return Promise.reject(new Error("Gateway disconnected")); + return new Promise((resolve, reject) => { + this.pending.set(id, { + resolve: (value) => resolve(value as Results[K]), + reject, + }); + send(this.socket, { id, command }); + }); + } + close(): void { + this.socket.destroy(); + } + private fail(error: Error): void { + for (const pending of this.pending.values()) pending.reject(error); + this.pending.clear(); + } +} + +export function reachable(path: string): Promise { + return new Promise((resolve) => { + const socket = createConnection(path); + socket.once("connect", () => { + socket.destroy(); + resolve(true); + }); + socket.once("error", () => resolve(false)); + socket.setTimeout(500, () => { + socket.destroy(); + resolve(false); + }); + }); +} diff --git a/agent/src/render.ts b/agent/src/render.ts new file mode 100644 index 00000000000..2082d0d91c7 --- /dev/null +++ b/agent/src/render.ts @@ -0,0 +1,341 @@ +import { TerminalImage as Image } from "./terminal-image.js"; +import { basename, extname, resolve } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; +import type { ExtensionFactory } from "@earendil-works/pi-coding-agent"; +import type { ImageContent, TextContent } from "@earendil-works/pi-ai"; +import { Container, Text, type Component } from "@earendil-works/pi-tui"; +import { Type } from "typebox"; +import { z } from "zod"; +import sharp from "sharp"; +import type { Paths } from "./config.js"; +import { MediaPool } from "./media.js"; +import { cacheImage, readArtifact } from "./artifacts.js"; +import { cloudSchema, paintCloud } from "./points.js"; +import { clipSchema, renderClip } from "./clips.js"; + +export const renderDetails = z.object({ + source: z.string(), + kind: z.enum(["image", "points", "sequence"]).optional(), + clip: clipSchema.optional(), + sha256: z.string().optional(), + summary: z.string(), + title: z.string().optional(), + views: z + .array( + z.object({ label: z.string(), source: z.string(), sha256: z.string() }), + ) + .optional(), + live: z + .object({ + url: z.string(), + robot: z.string().optional(), + channel: z.string(), + }) + .optional(), +}); +export type RenderDetails = z.infer; + +export function imageComponent( + data: string, + mimeType: string, + summary = "", + changed: () => void = () => {}, +): Component { + const block = new Container(); + if (summary) block.addChild(new Text(summary, 0, 0)); + block.addChild( + new Image( + data, + mimeType, + { fallbackColor: (text) => text }, + { maxHeightCells: 18, maxWidthCells: 80 }, + changed, + ), + ); + return block; +} + +/** Rasterize DimOS's own exports; never evaluate or reinterpret a memory query. */ +export async function renderFiles( + paths: Paths, + cwd: string, + views: Array<{ path: string; label: string }>, + title?: string, + signal?: AbortSignal, +): Promise<{ + content: Array; + details: RenderDetails; +}> { + if (!views.length || views.length > 6) + throw new Error("Choose 1–6 saved views from the same operation."); + const content: Array = []; + const sources: NonNullable = []; + for (const view of views) { + signal?.throwIfAborted(); + const source = resolve(cwd, view.path); + const { bytes, sha256 } = await readArtifact(source); + const png = await sharp(bytes, { density: 144 }) + .resize({ + width: 1600, + height: 1200, + fit: "inside", + withoutEnlargement: true, + }) + .flatten({ background: "#101b21" }) + .png() + .toBuffer(); + signal?.throwIfAborted(); + const preview = await cacheImage(paths, png); + sources.push({ label: view.label, source, sha256 }); + content.push( + { + type: "text", + text: `${view.label}\nSource: ${source}\nSHA-256: ${sha256}\nPreview: ${preview}`, + }, + { type: "image", data: png.toString("base64"), mimeType: "image/png" }, + ); + } + return { + content, + details: { + source: sources[0].source, + sha256: sources[0].sha256, + title, + views: sources, + summary: `${sources.length} saved views · original exports retained`, + }, + }; +} +/** Inspect saved results without coupling presentation to a DimOS runtime or transport. */ +export async function renderSaved( + paths: Paths, + cwd: string, + path: string, + options: { + kind?: "auto" | "image" | "points" | "sequence"; + title?: string; + frame?: number; + } = {}, + signal?: AbortSignal, + progress?: (done: number, total: number) => void, +): Promise<{ + content: Array; + details: RenderDetails; +}> { + const source = resolve(cwd, path); + let kind = options.kind ?? "auto"; + if ( + (kind === "image" || kind === "auto") && + extname(source).toLowerCase() !== ".json" + ) + return renderFiles( + paths, + cwd, + [{ path, label: basename(path) }], + options.title, + signal, + ); + const { bytes, sha256 } = await readArtifact(source); + signal?.throwIfAborted(); + const data: unknown = JSON.parse(bytes.toString("utf8")); + // A tagged frame index is authoritative, including when a caller supplies a still-view hint. + if ( + z + .object({ + type: z.enum(["points", "image"]), + frames: z.array(z.unknown()), + }) + .safeParse(data).success + ) + kind = "sequence"; + else if (kind === "auto") { + if (cloudSchema.safeParse(data).success) kind = "points"; + else + throw new Error( + "Unsupported JSON view. Export a plot/graph with DimOS or Python as SVG, then render that file.", + ); + } + let png: Buffer, summary: string, clip: RenderDetails["clip"]; + if (kind === "sequence") + ({ png, summary, clip } = await renderClip( + paths, + source, + data, + signal, + options.frame, + progress, + )); + else { + if (options.frame !== undefined) + throw new Error( + "frame selects an original sequence frame; provide a frame index file.", + ); + const cloud = cloudSchema.parse(data); + png = await paintCloud(cloud); + summary = `${cloud.points.length} source points; frame ${cloud.frame ?? "unspecified"}; timestamp ${cloud.timestamp ?? "unspecified"}; terminal projection, source retained`; + } + signal?.throwIfAborted(); + const preview = await cacheImage(paths, png); + return { + content: [ + { + type: "text", + text: `${summary}\nSource: ${source}\nSHA-256: ${sha256}\nPreview: ${preview}${clip?.gif ? "\nGIF: " + clip.gif : ""}${clip ? "\nOriginal frame indexes in preview: " + clip.frames.map((frame) => frame.index).join(", ") + "\nUse frame: INDEX to inspect any original frame without repeating the query." : ""}`, + }, + { type: "image", data: png.toString("base64"), mimeType: "image/png" }, + ], + details: { source, sha256, summary, kind, clip, title: options.title }, + }; +} +export const renderExtension = + (paths: Paths): ExtensionFactory => + (pi) => { + const pool = new MediaPool(); + pi.on("session_shutdown", () => pool.close()); + pi.registerTool({ + name: "dimcode_render", + label: "Inspect sensor result", + description: + "Display existing results in their terminal tool card. Auto-detect PNG/SVG images, XYZ JSON {points:[[x,y,z]],colors?:[[r,g,b]],frame?,timestamp?,selectedIndices?}, or a finite sequence index {type:'points'|'image',timeOrigin?,source?,frames:[{path,timestamp,sha256?}]}. Frame paths are relative to the index; timestamps are seconds, strictly increasing. Sequence previews use a fixed camera, source timing and a GIF; model receives a timestamped contact sheet. frame: INDEX returns one original source frame for closer inspection. views [{path,label}] groups up to 6 related SVG/PNG exports. Memory selection, filtering and analysis stay in DimOS. For any unsupported plot/graph, use DimOS Space/Plot or inline Python to export SVG and display it here. Never repeat a memory query to render it. live: explicit existing relay URL, robot and channel; only the final snapshot enters model context.", + parameters: Type.Object({ + kind: Type.Optional( + Type.Union([ + Type.Literal("auto"), + Type.Literal("image"), + Type.Literal("points"), + Type.Literal("sequence"), + Type.Literal("live"), + ]), + ), + frame: Type.Optional(Type.Integer({ minimum: 0 })), + path: Type.Optional(Type.String()), + title: Type.Optional(Type.String()), + views: Type.Optional( + Type.Array( + Type.Object({ path: Type.String(), label: Type.String() }), + { minItems: 1, maxItems: 6 }, + ), + ), + url: Type.Optional(Type.String()), + robot: Type.Optional(Type.String()), + channel: Type.Optional(Type.String()), + seconds: Type.Optional(Type.Number({ minimum: 1, maximum: 30 })), + }), + execute: async (_id, args, signal, update, ctx) => { + if (args.views) + return renderFiles(paths, ctx.cwd, args.views, args.title, signal); + if (args.kind !== "live") { + if (!args.path) throw new Error("Saved rendering requires a path"); + return renderSaved( + paths, + ctx.cwd, + args.path, + { kind: args.kind, title: args.title, frame: args.frame }, + signal, + (done, total) => + update?.({ + content: [ + { + type: "text", + text: `Rendering saved frames ${done}/${total}`, + }, + ], + details: { + source: resolve(ctx.cwd, args.path!), + summary: `Rendering saved frames ${done}/${total}`, + title: args.title, + }, + }), + ); + } + let png: Buffer, source: string, summary: string; + { + if (!args.url || !args.channel) + throw new Error( + "Live rendering requires an existing relay URL and channel.", + ); + source = + args.url + + " / " + + (args.robot ?? "selected robot") + + " / " + + args.channel; + const live = { + url: args.url, + robot: args.robot, + channel: args.channel, + }; + update?.({ + content: [{ type: "text", text: "Inspecting live channel" }], + details: { + source, + summary: "Live context; timestamps may be send time.", + live, + } satisfies RenderDetails, + }); + const lease = await pool.acquire( + live, + args.channel, + (slot) => slot, + () => {}, + ); + try { + const end = Date.now() + (args.seconds ?? 5) * 1000; + while (Date.now() < end) + await delay(Math.min(100, end - Date.now()), undefined, { + signal, + }); + const slot = lease.current(); + if (!slot) + throw new Error("No frame received from selected channel"); + if (!(slot.value instanceof Uint8Array)) + throw new Error( + "This channel needs its domain decoder/renderer; use an exported result for this encoding.", + ); + png = await sharp(slot.value) + .resize({ + width: 1280, + height: 960, + fit: "inside", + withoutEnlargement: true, + }) + .png() + .toBuffer(); + summary = + "Selected live snapshot, sequence " + + slot.seq + + ", header timestamp " + + slot.ts + + ". Header time is not necessarily capture time."; + } finally { + lease.close(); + } + } + const preview = await cacheImage(paths, png); + return { + content: [ + { + type: "text", + text: summary + "\nSource: " + source + "\nPreview: " + preview, + }, + { + type: "image", + data: png.toString("base64"), + mimeType: "image/png", + }, + ], + details: { source, summary } satisfies RenderDetails, + }; + }, + renderResult: (result) => { + const block = new Container(); + for (const item of result.content) + block.addChild( + item.type === "image" + ? imageComponent(item.data, item.mimeType) + : new Text(item.text, 0, 0), + ); + return block; + }, + }); + }; diff --git a/agent/src/service.ts b/agent/src/service.ts new file mode 100644 index 00000000000..5a8e7bd31d6 --- /dev/null +++ b/agent/src/service.ts @@ -0,0 +1,104 @@ +import { spawn } from "node:child_process"; +import { mkdir, unlink, writeFile } from "node:fs/promises"; +import { homedir } from "node:os"; +import { join, resolve } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; +import { Connection, reachable } from "./protocol.js"; +import type { Config, Paths } from "./config.js"; + +export function execute( + command: string, + args: readonly string[], + options: { cwd?: string; env?: NodeJS.ProcessEnv } = {}, +): Promise { + return new Promise((resolve, reject) => { + const child = spawn(command, args, { ...options, stdio: "inherit" }); + child.once("error", reject); + child.once("exit", (code, signal) => + code === 0 + ? resolve() + : reject(new Error(command + " exited " + (code ?? signal))), + ); + }); +} +const quote = (value: string) => + '"' + + value.replace(/%/g, "%%").replace(/\\/g, "\\\\").replace(/"/g, '\\"') + + '"'; +export function serviceUnit( + config: Config, + paths: Paths, + executable: string, +): string { + return [ + "[Unit]", + "Description=dimcode agent gateway", + "", + "[Service]", + "Type=simple", + "ExecStart=" + + quote(process.execPath) + + " " + + quote(executable) + + " gateway", + "WorkingDirectory=" + config.workspace.replace(/%/g, "%%"), + "Environment=" + quote("DIMCODE_HOME=" + paths.config), + "Restart=on-failure", + "RestartSec=2", + "UMask=0077", + "", + "[Install]", + "WantedBy=default.target", + "", + ].join("\n"); +} +export async function service( + action: string, + config: Config, + paths: Paths, + executable: string, +): Promise { + const dir = join( + process.env.XDG_CONFIG_HOME ?? join(homedir(), ".config"), + "systemd/user", + ); + const unit = join(dir, "dimcode.service"); + if (action === "install") { + const existing = new Connection(paths.socket); + try { + await existing.call({ type: "shutdown" }); + } catch { + } finally { + existing.close(); + } + for (let attempt = 0; await reachable(paths.socket); attempt++) { + if (attempt === 100) + throw new Error( + "Existing gateway did not stop; inspect dimcode service status.", + ); + await delay(50); + } + await mkdir(dir, { recursive: true }); + await writeFile(unit, serviceUnit(config, paths, resolve(executable)), { + mode: 0o600, + }); + await execute("systemctl", ["--user", "daemon-reload"]); + await execute("systemctl", [ + "--user", + "enable", + "--now", + "dimcode.service", + ]); + } else if (action === "uninstall") { + await execute("systemctl", [ + "--user", + "disable", + "--now", + "dimcode.service", + ]); + await unlink(unit); + await execute("systemctl", ["--user", "daemon-reload"]); + } else if (action === "status") + await execute("systemctl", ["--user", "status", "dimcode.service"]); + else throw new Error("Use service install, status or uninstall"); +} diff --git a/agent/src/session.ts b/agent/src/session.ts new file mode 100644 index 00000000000..27fd88c55d9 --- /dev/null +++ b/agent/src/session.ts @@ -0,0 +1,97 @@ +import { mkdir } from "node:fs/promises"; +import { dirname, join } from "node:path"; +import { fileURLToPath } from "node:url"; +import { + createAgentSessionServices, + createAgentSessionFromServices, + type AgentSession, + SessionManager, + createBashToolDefinition, +} from "@earendil-works/pi-coding-agent"; +import { loadConfig, type Config, type Paths } from "./config.js"; +import { McpTools } from "./mcp.js"; +import { renderExtension } from "./render.js"; + +export interface SessionHandle { + session: AgentSession; + close(): Promise; +} +export async function openSession( + config: Config, + paths: Paths, + manager: SessionManager, + report: (message: string) => void, +): Promise { + await mkdir(paths.config, { recursive: true, mode: 0o700 }); + await mkdir(paths.sessions, { recursive: true, mode: 0o700 }); + const mcp = new McpTools(async () => (await loadConfig(paths)).mcp, report); + let selected = config; + const services = await createAgentSessionServices({ + cwd: manager.getCwd(), + agentDir: paths.config, + resourceLoaderOptions: { + additionalSkillPaths: [ + fileURLToPath(new URL("../skills", import.meta.url)), + ], + extensionFactories: [ + mcp.extension(), + renderExtension(paths), + (pi) => { + pi.on("before_agent_start", async (event) => { + selected = await loadConfig(paths); + return { + systemPrompt: + event.systemPrompt + + "\nSelected configuration: " + + JSON.stringify(selected), + }; + }); + pi.registerTool( + createBashToolDefinition(manager.getCwd(), { + spawnHook: (context) => ({ + ...context, + env: { + ...context.env, + DIMCODE_NODE: process.execPath, + DIMCODE_CLI: process.argv[1], + PATH: [ + selected.dimos && dirname(selected.dimos), + selected.python && dirname(selected.python), + context.env.PATH, + ] + .filter(Boolean) + .join(":"), + }, + }), + }), + ); + }, + ], + appendSystemPrompt: [ + "You are dimcode, Dimensional's coding and robotics agent. Use DimOS CLI/public Python APIs and the advertised MCP skills. Do not create a parallel runtime or transport service. Select the intended run and endpoint explicitly; never assume the latest run is the intended one. Render actual saved results with dimcode_render. Live previews are labeled context, not historical query results.", + "Visualize each meaningful sensor or memory operation with dimcode_render. Prefer supported types and existing DimOS visualizers; otherwise generate a self-contained SVG with inline Python from the already evaluated result and render that file. Label proposed ideas or overlays distinctly from observations. Use returned images as visual feedback when interpreting results. DimOS owns reusable visualization semantics; dimcode owns terminal-specific styling and interaction. Built-in views are only point clouds and images/SVGs. For a saved sequence, use the Dimensional skill’s finite frame index format; the model sees a contact sheet and can request an original frame index for closer inspection. Clips loop inline without playback controls. If the user asks for a still at a timestamp, select the original frame from the saved index and render it. Never imply a contact sheet covers every frame.", + 'For installing or connecting DimOS, use the dimensional-install skill and the selected version’s own installation instructions. The dimcode CLI is available in Bash as "$DIMCODE_NODE" "$DIMCODE_CLI", even when dimcode is not on PATH. Never read or print provider credentials; use /login for authentication.', + ], + }, + }); + const { session } = await createAgentSessionFromServices({ + services, + sessionManager: manager, + }); + return { + session, + close: async () => { + try { + await session.extensionRunner.emit({ + type: "session_shutdown", + reason: "quit", + }); + } finally { + session.dispose(); + await mcp.close(); + } + }, + }; +} +export const authPath = (paths: Paths): string => + join(paths.config, "auth.json"); diff --git a/agent/src/setup.ts b/agent/src/setup.ts new file mode 100644 index 00000000000..03d14bcbcc5 --- /dev/null +++ b/agent/src/setup.ts @@ -0,0 +1,265 @@ +import { access } from "node:fs/promises"; +import { join, resolve } from "node:path"; +import { stdin, stdout } from "node:process"; +import { + ModelRuntime, + SettingsManager, + getSelectListTheme, + initTheme, +} from "@earendil-works/pi-coding-agent"; +import { + Container, + ProcessTerminal, + SelectList, + Text, + TuiMainScreen, +} from "@earendil-works/pi-tui"; +import type { AuthInteraction, AuthPrompt } from "@earendil-works/pi-ai"; +import { loadConfig, saveConfig, type Paths } from "./config.js"; +import { ChatInput } from "./input.js"; +import { service } from "./service.js"; + +/** Pi's login callbacks, with credentials confined to a disposable masked input. */ +function setupUI(): AuthInteraction & { close(): void } { + if (!stdin.isTTY) + throw new Error( + "Setup requires a terminal. For automation use dimcode setup --provider NAME --key-env VAR.", + ); + initTheme("dark", false); + const tui = new TuiMainScreen(new ProcessTerminal()); + const body = new Container(), + info = new Text("", 1, 1); + tui.addChild( + new Text("\x1b[1;36mdimcode\x1b[0m · Set up your Dimensional agent", 1, 1), + ); + tui.addChild(info); + tui.addChild(body); + tui.addChild( + new Text("\x1b[2m↑ ↓ choose · Enter continue · Esc cancel\x1b[0m", 1, 1), + ); + const abort = new AbortController(); + tui.addInputListener((data) => { + if (data === "\x03") { + abort.abort(); + return { consume: true }; + } + return undefined; + }); + tui.start(); + return { + signal: abort.signal, + prompt: async (question: AuthPrompt) => { + abort.signal.throwIfAborted(); + question.signal?.throwIfAborted(); + body.clear(); + body.addChild(new Text(question.message, 1, 1)); + return new Promise((resolve, reject) => { + const cancel = () => finish(undefined); + const input = new ChatInput( + question.type === "select" ? "" : question.placeholder, + ); + const finish = (value: string | undefined) => { + abort.signal.removeEventListener("abort", cancel); + question.signal?.removeEventListener("abort", cancel); + input.secret = false; + input.setValue(""); + body.clear(); + tui.requestRender(); + if (value === undefined) + reject( + new Error("Setup cancelled. Run dimcode setup to continue."), + ); + else resolve(value); + }; + abort.signal.addEventListener("abort", cancel, { once: true }); + question.signal?.addEventListener("abort", cancel, { once: true }); + if (question.type === "select") { + const select = new SelectList( + question.options.map((item) => ({ + value: item.id, + label: item.label, + description: item.description, + })), + 10, + getSelectListTheme(), + ); + select.onSelect = (item) => finish(item.value); + select.onCancel = cancel; + body.addChild(select); + tui.setFocus(select); + } else { + input.secret = + question.type === "secret" || question.type === "manual_code"; + input.onSubmit = finish; + input.onEscape = cancel; + body.addChild(input); + tui.setFocus(input); + } + tui.requestRender(); + }); + }, + notify: (event) => { + info.setText( + event.type === "auth_url" + ? event.url + + "\n" + + (event.instructions ?? "Open this link to sign in.") + : event.type === "device_code" + ? event.verificationUri + " · " + event.userCode + : event.message, + ); + tui.requestRender(); + }, + close: () => tui.stop(), + }; +} +export async function needsSetup(paths: Paths): Promise { + try { + await access(join(paths.config, "config.json")); + return false; + } catch (error) { + if (error instanceof Error && "code" in error && error.code === "ENOENT") + return true; + throw error; + } +} +export interface SetupOptions { + provider?: string; + oauth?: boolean; + keyEnv?: string; + workspace?: string; +} +export async function setup( + paths: Paths, + options: SetupOptions = {}, + interaction?: AuthInteraction, +): Promise { + const config = await loadConfig(paths); + if (options.workspace) config.workspace = resolve(options.workspace); + const runtime = await ModelRuntime.create({ + authPath: join(paths.config, "auth.json"), + modelsPath: join(paths.config, "models.json"), + }); + const wizard = !options.provider && !options.keyEnv && !options.oauth; + const ui = interaction ?? (options.keyEnv ? undefined : setupUI()); + const ask = (question: AuthPrompt) => { + if (!ui) throw new Error("Interactive setup requires a terminal"); + return ui.prompt(question); + }; + const choose = ( + message: string, + items: readonly { id: string; label: string; description?: string }[], + ) => ask({ type: "select", message, options: items }); + let atLogin = false; + try { + const providers = runtime + .getProviders() + .filter( + (p) => p.auth.apiKey?.login || (p.id !== "anthropic" && p.auth.oauth), + ); + const preferred = ["openai", "openai-codex", "anthropic"]; + providers.sort( + (a, b) => + (preferred.indexOf(a.id) + 1 || 99) - + (preferred.indexOf(b.id) + 1 || 99), + ); + const providerId = + options.provider ?? + (wizard + ? await choose( + "1. Choose your model provider", + providers.map((p) => ({ + id: p.id, + label: + p.id === "openai-codex" + ? "ChatGPT subscription (sign in)" + : p.name, + description: + p.id === "anthropic" + ? "Claude API key" + : p.id === "openai" + ? "OpenAI API key" + : p.id, + })), + ) + : "openai"); + const provider = runtime.getProvider(providerId); + if (!provider) throw new Error("Unknown provider: " + providerId); + if (providerId === "anthropic" && options.oauth) + throw new Error("Use an Anthropic API key for dimcode."); + const methods = [ + ...(wizard && runtime.hasConfiguredAuth(providerId) + ? [{ id: "existing", label: "Use configured credentials" }] + : []), + ...(provider.auth.apiKey?.login + ? [{ id: "api_key", label: provider.auth.apiKey.name }] + : []), + ...(providerId !== "anthropic" && provider.auth.oauth + ? [{ id: "oauth", label: provider.auth.oauth.name }] + : []), + ]; + const method = options.keyEnv + ? "api_key" + : options.oauth + ? "oauth" + : wizard && methods.length > 1 + ? await choose("Sign in", methods) + : methods[0]?.id; + if (!method) throw new Error("Provider has no supported login method"); + if (method !== "existing") { + if (options.keyEnv) { + const key = process.env[options.keyEnv]; + if (!key) + throw new Error("Requested credential environment variable is empty"); + await runtime.login(providerId, "api_key", { + prompt: async () => key, + notify: () => {}, + }); + } else { + if (method !== "api_key" && method !== "oauth") + throw new Error("Unknown login method"); + await runtime.login(providerId, method, ui!); + } + } + const models = await runtime.getAvailable(providerId); + if (!models.length) + throw new Error("No available models for " + providerId); + const settings = SettingsManager.create(config.workspace, paths.config); + const currentModel = + settings.getDefaultProvider() === providerId + ? settings.getDefaultModel() + : undefined; + const ordered = [...models].sort( + (a, b) => Number(b.id === currentModel) - Number(a.id === currentModel), + ); + const model = wizard + ? await choose( + "2. Choose a model", + ordered.map((m) => ({ id: m.id, label: m.id, description: m.name })), + ) + : ordered[0].id; + if (wizard) { + if (process.platform === "linux") + atLogin = + (await choose("3. Start the gateway at login?", [ + { id: "no", label: "Start when I open dimcode" }, + { + id: "yes", + label: "Start at login", + description: + "Keep dimcode available in the background; no robot starts", + }, + ])) === "yes"; + } + settings.setDefaultModelAndProvider(providerId, model); + await settings.flush(); + await saveConfig(paths, config); + } finally { + if (ui && "close" in ui && typeof ui.close === "function") ui.close(); + } + if (atLogin) await service("install", config, paths, process.argv[1]); + stdout.write( + wizard ? "Opening agent-led DimOS setup…\n" : "Provider configured.\n", + ); + return wizard; +} diff --git a/agent/src/spatial.ts b/agent/src/spatial.ts new file mode 100644 index 00000000000..ca8b8efbae0 --- /dev/null +++ b/agent/src/spatial.ts @@ -0,0 +1,222 @@ +import { TerminalImage as Image } from "./terminal-image.js"; +import { basename } from "node:path"; +import { + getCapabilities, + matchesKey, + Text, + truncateToWidth, + type Component, +} from "@earendil-works/pi-tui"; +import { + fitClouds, + paintCloud, + projectPoint, + POINT_LIMIT, + type Cloud, + type Camera, +} from "./points.js"; +export { readCloud } from "./points.js"; +import { accent, amber, clean, muted, StatusLine } from "./terminal-style.js"; + +/** A presentation of one immutable observation. View controls never execute a DimOS query. */ +export class SpatialView implements Component { + private angle = -0.58; + private zoom = 1; + private graphics = true; + private details = false; + private image?: Image; + private revision = 0; + private disposed = false; + private animation?: ReturnType; + private readonly camera: Camera; + private readonly selected: Set; + private readonly step: number; + private readonly heading: StatusLine; + expanded = false; + constructor( + readonly cloud: Cloud, + readonly source: string, + readonly sha256: string, + private readonly changed: () => void, + ) { + this.camera = fitClouds([cloud]); + this.selected = new Set(cloud.selectedIndices); + this.step = Math.max(1, Math.ceil(cloud.points.length / POINT_LIMIT)); + this.heading = new StatusLine(() => [ + accent("⠿ PointCloud") + muted(" " + clean(basename(source))), + muted( + `${cloud.points.length.toLocaleString()} points · ${clean(cloud.frame ?? "frame unspecified")}`, + ), + ]); + this.refresh(); + } + invalidate(): void { + this.image?.invalidate(); + } + close(): void { + this.disposed = true; + this.revision++; + clearInterval(this.animation); + this.image?.close(); + } + private retained(i: number): boolean { + return this.selected.has(i); + } + private async raster(revision: number): Promise { + const png = await paintCloud( + this.cloud, + this.camera, + this.angle, + this.zoom, + ); + if (this.disposed || revision !== this.revision) return; + this.image?.close(); + this.image = new Image( + png.toString("base64"), + "image/png", + { fallbackColor: muted }, + { + maxHeightCells: this.expanded ? 30 : 20, + maxWidthCells: this.expanded ? 150 : 100, + }, + this.changed, + ); + this.changed(); + } + private refresh(): void { + const revision = ++this.revision; + if (this.graphics && getCapabilities().images) + void this.raster(revision).catch(() => { + if (revision === this.revision) { + this.graphics = false; + this.changed(); + } + }); + this.changed(); + } + handleInput(key: string): void { + if (key === "r" || matchesKey(key, "right") || matchesKey(key, "left")) { + clearInterval(this.animation); + const direction = matchesKey(key, "left") ? -1 : 1; + let frames = 0; + this.animation = setInterval(() => { + this.angle += (direction * Math.PI) / 16; + this.refresh(); + if (++frames === 4) clearInterval(this.animation); + }, 80); + } else { + if (key === "+" || key === "=") this.zoom = Math.min(4, this.zoom * 1.2); + else if (key === "-") this.zoom = Math.max(0.25, this.zoom / 1.2); + else if (key === "g") { + this.graphics = !this.graphics; + this.image?.close(); + this.image = undefined; + } else if (key === "d") this.details = !this.details; + else if (key === "0") { + this.zoom = 1; + this.angle = -0.58; + } else return; + this.refresh(); + } + } + setExpanded(value: boolean): void { + this.expanded = value; + this.image?.close(); + this.image = undefined; + this.refresh(); + } + private braille(width: number): string[] { + const rows = this.expanded ? 26 : 14, + cols = Math.max(1, width - 2); + const cells = Array.from({ length: rows }, () => + Array.from({ length: cols }, () => ({ dots: 0, selected: false })), + ); + const bits = [ + [1, 8], + [2, 16], + [4, 32], + [64, 128], + ]; + for (let i = 0; i < this.cloud.points.length; i += this.step) { + const [px, py] = projectPoint( + this.cloud.points[i], + this.camera, + cols * 2, + rows * 4, + this.angle, + this.zoom, + ); + const x = Math.floor(px), + y = Math.floor(py); + if (x < 0 || y < 0 || x >= cols * 2 || y >= rows * 4) continue; + const cell = cells[Math.floor(y / 4)][Math.floor(x / 2)]; + cell.dots |= bits[y % 4][x % 2]; + cell.selected ||= this.retained(i); + } + const masked = !!this.cloud.selectedIndices; + return cells.map( + (row) => + " " + + row + .map((cell) => + cell.dots + ? (cell.selected ? amber : masked ? muted : accent)( + String.fromCharCode(0x2800 + cell.dots), + ) + : " ", + ) + .join(""), + ); + } + render(width: number): string[] { + const lines = [ + "", + ...this.heading.render(width), + muted("─".repeat(Math.max(0, width))), + ]; + lines.push( + ...(this.graphics && getCapabilities().images && this.image + ? this.image.render(width) + : this.braille(width)), + ); + lines.push( + truncateToWidth( + " " + + (this.cloud.selectedIndices + ? amber( + `${this.selected.size.toLocaleString()} source-selected points`, + ) + : muted("Source geometry")), + width, + ), + ); + lines.push( + truncateToWidth( + muted( + ` ${this.cloud.timestamp ?? "timestamp unspecified"} · display 1/${this.step} · ${this.graphics && getCapabilities().images ? "graphics" : "Braille"}`, + ), + width, + ), + ); + lines.push( + ...new Text( + muted( + this.expanded + ? " ←/→ rotate · +/- zoom · g graphics · d source · 0 reset · Esc close" + : " /view rotate, zoom and inspect · /expand tool details", + ), + 0, + 0, + ).render(width), + ); + if (this.details) + lines.push( + ...new Text( + clean(this.source) + "\nSHA-256: " + this.sha256, + 1, + 0, + ).render(width), + ); + return lines; + } +} diff --git a/agent/src/terminal-image.ts b/agent/src/terminal-image.ts new file mode 100644 index 00000000000..c0b020b9acf --- /dev/null +++ b/agent/src/terminal-image.ts @@ -0,0 +1,128 @@ +import { + Image, + getCapabilities, + getCellDimensions, + getImageDimensions, + truncateToWidth, + type ImageDimensions, + type Component, + type ImageOptions, + type ImageTheme, +} from "@earendil-works/pi-tui"; +import { calculateImageCellSize } from "@earendil-works/pi-tui/dist/terminal-image.js"; +import sharp from "sharp"; + +export function needsImageSlices(env = process.env): boolean { + return ( + env.TERM_PROGRAM?.toLowerCase() === "warpterminal" || + !!(env.WARP_SESSION_ID || env.WARP_TERMINAL_SESSION_UUID) + ); +} + +/** Warp ignores source-rectangle cropping. Whole cell-row images let Pi scroll by clipping rows. */ +export class TerminalImage implements Component { + private readonly fallback: Image; + private readonly dimensions: ImageDimensions | null; + private slices?: Image[]; + private key?: string; + private disposed = false; + private failed = false; + constructor( + private readonly data: string, + mimeType: string, + private readonly theme: ImageTheme, + private readonly options: ImageOptions, + private readonly changed: () => void, + ) { + this.fallback = new Image(data, mimeType, theme, options); + this.dimensions = getImageDimensions(data, mimeType); + } + close(): void { + this.disposed = true; + this.slices = undefined; + } + invalidate(): void { + this.fallback.invalidate(); + for (const slice of this.slices ?? []) slice.invalidate(); + } + private async prepare( + key: string, + columns: number, + rows: number, + ): Promise { + const cell = getCellDimensions(); + const width = Math.round(columns * cell.widthPx), + rowHeight = Math.round(cell.heightPx); + const pixels = await sharp(Buffer.from(this.data, "base64")) + .resize(width, rows * rowHeight, { + fit: "contain", + background: "#101b21", + }) + .flatten({ background: "#101b21" }) + .removeAlpha() + .toColourspace("srgb") + .raw() + .toBuffer(); + const slices = await Promise.all( + Array.from({ length: rows }, async (_, i) => { + const start = i * width * rowHeight * 3; + const png = await sharp( + pixels.subarray(start, start + width * rowHeight * 3), + { + raw: { width, height: rowHeight, channels: 3 }, + }, + ) + .png() + .toBuffer(); + return new Image(png.toString("base64"), "image/png", this.theme, { + maxWidthCells: columns, + maxHeightCells: 1, + }); + }), + ); + if (this.disposed || key !== this.key) return; + this.slices = slices; + this.changed(); + } + render(width: number): string[] { + if (!needsImageSlices() || getCapabilities().images !== "kitty") + return this.fallback.render(width); + const dimensions = this.dimensions; + if (!dimensions) return this.fallback.render(width); + const cell = getCellDimensions(); + const maxWidth = Math.max( + 1, + Math.min(width - 2, this.options.maxWidthCells ?? 60), + ); + const size = calculateImageCellSize( + dimensions, + maxWidth, + this.options.maxHeightCells ?? + Math.max(1, Math.ceil((maxWidth * cell.widthPx) / cell.heightPx)), + cell, + ); + const key = `${size.columns}:${size.rows}:${cell.widthPx}:${cell.heightPx}`; + if (key !== this.key && !this.disposed) { + this.key = key; + this.slices = undefined; + this.failed = false; + void this.prepare(key, size.columns, size.rows).catch(() => { + if (!this.disposed && key === this.key) { + this.failed = true; + this.changed(); + } + }); + } + return ( + this.slices?.map((slice) => slice.render(size.columns + 2)[0]) ?? + Array.from({ length: size.rows }, (_, i) => + i === 0 && this.failed + ? truncateToWidth( + this.theme.fallbackColor("Image preview unavailable"), + width, + ) + : "", + ) + ); + } +} diff --git a/agent/src/terminal-style.ts b/agent/src/terminal-style.ts new file mode 100644 index 00000000000..5ae0066dcb9 --- /dev/null +++ b/agent/src/terminal-style.ts @@ -0,0 +1,32 @@ +import { + truncateToWidth, + visibleWidth, + type Component, +} from "@earendil-works/pi-tui"; + +export const accent = (s: string) => `\x1b[38;2;106;229;207m${s}\x1b[39m`; +export const muted = (s: string) => `\x1b[38;2;139;164;168m${s}\x1b[39m`; +export const amber = (s: string) => `\x1b[38;2;245;192;120m${s}\x1b[39m`; +export const clean = (s: string): string => + s.replace(/[\x00-\x1f\x7f-\x9f]/g, " "); + +/** Width-aware chrome; contents come from the active session, never placeholder metrics. */ +export class StatusLine implements Component { + constructor(private readonly content: () => [string, string?]) {} + invalidate(): void {} + render(width: number): string[] { + const [left, right = ""] = this.content(); + const available = Math.max(0, width - visibleWidth(right) - 4); + if (available < 20) return [truncateToWidth(" " + left, width)]; + const clipped = truncateToWidth(left, available); + return [ + " " + + clipped + + " ".repeat( + Math.max(1, width - visibleWidth(clipped) - visibleWidth(right) - 2), + ) + + right + + " ", + ]; + } +} diff --git a/agent/src/terminal.ts b/agent/src/terminal.ts new file mode 100644 index 00000000000..da2793bfdf6 --- /dev/null +++ b/agent/src/terminal.ts @@ -0,0 +1,804 @@ +import { readFile } from "node:fs/promises"; +import { basename, resolve } from "node:path"; +import { + Container, + Markdown, + Spacer, + ProcessTerminal, + Text, + TuiAltScreen, + ScrollView, + VStack, + matchesKey, + type Component, +} from "@earendil-works/pi-tui"; +import sharp from "sharp"; +import { + ToolExecutionComponent, + AssistantMessageComponent, + UserMessageComponent, + getMarkdownTheme, + initTheme, + createBashToolDefinition, + createReadToolDefinition, + createEditToolDefinition, + createWriteToolDefinition, +} from "@earendil-works/pi-coding-agent"; +import { z } from "zod"; +import { Connection, type Event, type Snapshot } from "./protocol.js"; +import { imageComponent, renderDetails, renderSaved } from "./render.js"; +import { MediaPool, type MediaLease } from "./media.js"; +import { PromptEditor, commands } from "./input.js"; +import { SpatialView, readCloud } from "./spatial.js"; +import { paths } from "./config.js"; +import { Playback } from "./playback.js"; +import { needsImageSlices } from "./terminal-image.js"; +import { ResultImages } from "./tool-images.js"; +import { accent, clean, muted, StatusLine } from "./terminal-style.js"; +import type { Slot } from "@dimos/sdk"; + +const resultSchema = z.object({ + content: z + .array( + z.union([ + z.object({ type: z.literal("text"), text: z.string() }), + z.object({ + type: z.literal("image"), + data: z.string(), + mimeType: z.string(), + }), + ]), + ) + .default([]), + details: z.unknown().optional(), +}); +export async function terminal( + socket: string, + options: { + sessionId?: string; + cwd?: string; + view?: boolean; + initialPrompt?: string; + } = {}, +): Promise { + initTheme("dark", false); + const client = new Connection(socket), + pool = new MediaPool(); + const tui = new TuiAltScreen(new ProcessTerminal()), + transcript = new Container(); + const status = new Text("Connecting…", 0, 0), + input = new PromptEditor(tui); + let lastSpatial: SpatialView | undefined; + let lastImages: ResultImages | undefined; + let lastPlayback: Playback | undefined; + let localRender: AbortController | undefined; + let inspectorOpen = false; + let layout: VStack; + const inspect = (view = lastSpatial) => { + if (!view) { + notice( + "No point cloud yet. Use /inspect PATH or ask the agent to render one.", + ); + return; + } + inspectorOpen = true; + view.setExpanded(true); + const expanded = { + render: (width) => view.render(width), + invalidate: () => view.invalidate(), + handleInput: (data) => { + if (matchesKey(data, "escape")) { + view.setExpanded(false); + inspectorOpen = false; + tui.setLayoutRoot(layout); + tui.setFocus(input); + } else view.handleInput(data); + }, + } satisfies Component; + tui.setLayoutRoot( + new VStack([ + new StatusLine(() => [ + accent("⠿ dimcode / spatial inspector"), + muted("Esc returns to chat"), + ]), + { + component: new ScrollView(expanded, { primary: true }), + basis: 0, + grow: 1, + }, + ]), + ); + tui.setFocus(expanded); + }; + const cards = new Map< + string, + { + block: Container; + component: ToolExecutionComponent; + output: unknown; + expanded: boolean; + toolName: string; + args: unknown; + spatial?: SpatialView; + images?: ResultImages; + playback?: Playback; + loading?: boolean; + lease?: MediaLease; + close?: () => void; + } + >(); + let current: Snapshot, + assistant = new Container(), + text = "", + stopped = false; + let auth: Extract | undefined; + let restoring = true; + const pending: Array<{ sessionId: string; seq: number; event: Event }> = []; + const add = (value: string) => transcript.addChild(new Text(value, 0, 0)); + const setAssistant = (value: string) => { + assistant.clear(); + assistant.addChild(new Markdown(value, 1, 1, getMarkdownTheme())); + }; + const ready = () => + " " + (current.writable ? "Ready" : "Read-only") + " · /help commands"; + const notice = (error: unknown) => { + add(String(error)); + tui.requestRender(); + }; + const closeCards = () => { + localRender?.abort(); + localRender = undefined; + for (const card of cards.values()) { + card.close?.(); + card.spatial?.close(); + card.images?.close(); + card.playback?.close(); + } + lastSpatial = undefined; + lastImages = undefined; + lastPlayback = undefined; + cards.clear(); + }; + const draw = ( + id: string, + output: unknown, + toolName = "tool", + args: unknown = {}, + partial = false, + isError = false, + ) => { + let card = cards.get(id); + if (!card) { + const cwd = current?.cwd ?? options.cwd ?? process.cwd(); + const definitions = [ + createBashToolDefinition(cwd), + createReadToolDefinition(cwd), + createEditToolDefinition(cwd), + createWriteToolDefinition(cwd), + ]; + const component = new ToolExecutionComponent( + toolName, + id, + args, + { showImages: !needsImageSlices() }, + definitions.find((tool) => tool.name === toolName), + tui, + cwd, + ); + component.markExecutionStarted(); + component.setArgsComplete(); + card = { + block: new Container(), + component, + output, + expanded: false, + toolName, + args, + }; + lastPlayback?.pause(); + cards.set(id, card); + transcript.addChild(card.block); + } + card.close?.(); + card.close = undefined; + card.output = output; + card.block.clear(); + const result = resultSchema.safeParse(output); + const details = renderDetails.safeParse( + result.success ? result.data.details : undefined, + ); + const named = z + .object({ tool: z.string() }) + .safeParse(result.success ? result.data.details : undefined); + const title = + (details.success && details.data.title) || + (named.success && named.data.tool) || + card.toolName; + const builtin = ["bash", "read", "edit", "write"].includes(card.toolName); + if (builtin || card.expanded) card.block.addChild(card.component); + else { + card.block.addChild(new Spacer(1)); + card.block.addChild( + new Text( + accent(" ↳ ") + + clean(title) + + muted(isError ? " · failed" : partial ? " · running" : " · done"), + 0, + 0, + ), + ); + } + if (!result.success) { + card.block.addChild(new Text("Running…", 0, 0)); + return; + } + card.component.updateResult({ ...result.data, isError }, partial); + card.component.setExpanded(card.expanded); + if ( + details.success && + details.data.clip && + !partial && + !isError && + !card.playback + ) { + lastPlayback?.pause(); + card.playback = new Playback( + details.data.clip, + () => tui.requestRender(), + true, + ); + lastPlayback = card.playback; + } + const images = result.data.content.filter((item) => item.type === "image"); + if ( + images.length && + !card.spatial && + !card.playback && + (!card.images || + images.length !== card.images.images.length || + images.some((image, i) => image.data !== card.images?.images[i].data)) + ) { + card.images?.close(); + card.images = new ResultImages( + images.map((image, i) => ({ + ...image, + label: + (details.success && details.data.views?.[i].label) || + `Image ${i + 1}`, + })), + () => tui.requestRender(), + ); + lastImages = card.images; + } + if (!builtin && !card.expanded) { + const summary = details.success + ? details.data.summary.split(";")[0] + : result.data.content + .filter((item) => item.type === "text") + .map((item) => item.text) + .join("\n"); + card.block.addChild( + new Text(muted(" " + clean(summary.slice(0, 220))), 0, 0), + ); + } + if ((!builtin && !card.expanded) || needsImageSlices()) { + if (card.playback) card.block.addChild(card.playback); + else if (card.spatial) card.block.addChild(card.spatial); + else if (card.images) card.block.addChild(card.images); + } + const pointArgs = z + .object({ kind: z.literal("points") }) + .safeParse(card.args); + if ( + details.success && + (details.data.kind === "points" || + (!details.data.kind && pointArgs.success)) && + !partial && + !isError && + !card.spatial && + !card.loading + ) { + card.loading = true; + const held = card; + void readCloud(details.data.source, details.data.sha256) + .then(({ cloud, sha256 }) => { + if (stopped || cards.get(id) !== held) return; + held.spatial = new SpatialView( + cloud, + details.data.source, + sha256, + () => tui.requestRender(), + ); + lastSpatial = held.spatial; + draw(id, held.output, held.toolName, held.args); + }) + .catch((error) => { + if (!stopped && cards.get(id) === held) notice(error); + }); + } + if (details.success && details.data.live && !card.close) { + const live = details.data.live, + held = card; + const preview = new Container(); + held.block.addChild(preview); + let disposed = false, + drawing = false, + changed = true, + previous = -1; + const tick = async () => { + if (disposed || drawing || !changed) return; + const slot = held.lease?.current(); + if ( + !slot || + slot.version === previous || + !(slot.value instanceof Uint8Array) + ) + return; + drawing = true; + changed = false; + try { + const bytes = await sharp(slot.value) + .resize({ width: 960, height: 720, fit: "inside" }) + .png() + .toBuffer(); + if (disposed) return; + previous = slot.version; + preview.clear(); + preview.addChild( + imageComponent( + bytes.toString("base64"), + "image/png", + "LIVE · " + live.channel + " · seq " + slot.seq, + () => { + if (!disposed) tui.requestRender(); + }, + ), + ); + tui.requestRender(); + } catch (error) { + if (!disposed) notice(error); + } finally { + drawing = false; + } + }; + const timer = setInterval(() => void tick(), 100); + held.close = () => { + disposed = true; + clearInterval(timer); + held.lease?.close(); + }; + void pool + .acquire( + live, + live.channel, + (slot) => slot, + () => { + changed = true; + }, + ) + .then((lease) => { + if (disposed) lease.close(); + else held.lease = lease; + }) + .catch(notice); + } + }; + const apply = (event: Event) => { + if (event.type === "auth_prompt") { + auth = event; + input.secret = + event.prompt.type === "secret" || event.prompt.type === "manual_code"; + input.setValue(""); + status.setText( + event.prompt.message + + (event.prompt.type === "select" + ? "\n" + + event.prompt.options + .map((option) => option.id + ": " + option.label) + .join(" · ") + : ""), + ); + } else if (event.type === "auth_info") { + const info = event.info; + status.setText( + info.type === "auth_url" + ? info.url + "\n" + (info.instructions ?? "") + : info.type === "device_code" + ? info.verificationUri + " · " + info.userCode + : info.message, + ); + } else if (event.type === "text_delta") { + text += event.delta; + setAssistant(text); + } else if ( + event.type === "message_end" && + event.message.role === "assistant" + ) { + text = event.message.content + .filter((item) => item.type === "text") + .map((item) => item.text) + .join("\n"); + assistant.clear(); + assistant.addChild(new AssistantMessageComponent(event.message, true)); + } else if (event.type === "message_start") { + if (event.message.role === "assistant") { + text = ""; + assistant = new Container(); + transcript.addChild(assistant); + } else if (event.message.role === "user") { + const content = event.message.content; + transcript.addChild( + new UserMessageComponent( + typeof content === "string" + ? content + : content + .filter((item) => item.type === "text") + .map((item) => item.text) + .join("\n"), + ), + ); + } + } else if (event.type === "tool_execution_start") { + draw( + event.toolCallId, + { content: [{ type: "text", text: "Running…" }] }, + event.toolName, + event.args, + true, + ); + } else if (event.type === "tool_execution_update") + draw( + event.toolCallId, + event.partialResult, + event.toolName, + event.args, + true, + ); + else if (event.type === "tool_execution_end") + draw( + event.toolCallId, + event.result, + event.toolName, + {}, + false, + event.isError, + ); + else if (event.type === "notice" || event.type === "turn_error") + notice(event.message); + else if (event.type === "idle") { + current.busy = false; + status.setText(ready()); + } + tui.requestRender(); + }; + client.onEvent = (seq, event, sessionId) => { + if (restoring) pending.push({ sessionId, seq, event }); + else if (sessionId === current.sessionId) apply(event); + }; + const restore = (snapshot: Snapshot) => { + closeCards(); + transcript.clear(); + current = snapshot; + input.setWorkspace(current.cwd); + if (!current.messages.length) { + transcript.addChild(new Spacer(1)); + add(accent(" ⠿ Build apps. Run blueprints. Explore sensor memory.")); + add( + muted( + " Ask about this workspace, or show available DimOS blueprints.", + ), + ); + add( + muted( + " Memory results appear inside tool cards · /panel selects a view", + ), + ); + transcript.addChild(new Spacer(1)); + } + const argumentsByCall = new Map(); + for (const message of current.messages) { + if (message.role === "assistant") + for (const block of message.content) { + if (block.type === "toolCall") + argumentsByCall.set(block.id, block.arguments); + } + if ("content" in message) { + if (message.role === "toolResult") + draw( + message.toolCallId, + message, + message.toolName, + argumentsByCall.get(message.toolCallId), + false, + message.isError, + ); + else { + const content = + typeof message.content === "string" + ? message.content + : message.content + .filter((item) => item.type === "text") + .map((item) => item.text) + .join("\n"); + if (message.role === "assistant") + transcript.addChild(new AssistantMessageComponent(message, true)); + else if (message.role === "user") + transcript.addChild(new UserMessageComponent(content)); + else if (content) add(content); + } + } + } + for (const message of current.notices) add(message); + text = current.text; + assistant = new Container(); + if (text) setAssistant(text); + transcript.addChild(assistant); + for (const event of current.tools) apply(event); + status.setText(current.busy ? " Running · Esc to cancel" : ready()); + restoring = false; + for (const packet of pending.splice(0)) + if (packet.sessionId === current.sessionId && packet.seq > current.seq) + apply(packet.event); + tui.requestRender(); + }; + const stop = () => { + if (stopped) return; + stopped = true; + closeCards(); + pool.close(); + tui.stop(); + client.close(); + }; + try { + restore( + await client.call( + options.sessionId + ? { + type: "attach", + sessionId: options.sessionId, + writable: !options.view, + } + : { type: "new_session", cwd: options.cwd }, + ), + ); + layout = new VStack([ + new Spacer(1), + new StatusLine(() => [ + accent("⠿ dimcode") + " " + clean(basename(current.cwd)), + muted(current.writable ? "GATEWAY ATTACHED" : "VIEW ONLY"), + ]), + new StatusLine(() => [muted(clean(current.cwd)), ""]), + new Spacer(1), + { + component: new ScrollView(transcript, { + follow: "end", + primary: true, + }), + basis: 0, + grow: 1, + minSize: 1, + }, + status, + input, + new StatusLine(() => [ + current.model + ? clean(current.model.provider + " / " + current.model.id) + : muted("/models choose model"), + muted("Shift+Enter newline · Ctrl-C detach"), + ]), + ]); + tui.setLayoutRoot(layout); + tui.setFocus(input); + input.onSubmit = (value) => { + if (!auth && value.trim()) input.addToHistory(value); + input.setValue(""); + if (auth) { + const promptId = auth.promptId; + auth = undefined; + input.secret = false; + void client + .call({ type: "ui_response", promptId, value }) + .catch(notice); + return; + } + void (async () => { + if (value === "/exit") { + stop(); + return; + } + if (value === "/help") { + add(commands.map((command) => "/" + command).join(" · ")); + add("/resume ID · /model PROVIDER MODEL · /login PROVIDER [oauth]"); + add( + "/panel N: result view (0 overview) · /inspect PATH: saved result · /view: rotate/zoom · /expand: latest tool details", + ); + return; + } + if (value === "/sessions") { + notice( + await client + .call({ type: "list_sessions" }) + .then((list) => + list.map((item) => item.id + " " + item.name).join("\n"), + ), + ); + return; + } + if (value === "/new" || value.startsWith("/resume ")) { + restoring = true; + try { + restore( + await client.call( + value === "/new" + ? { type: "new_session", cwd: current.cwd } + : { + type: "attach", + sessionId: value.slice(8).trim(), + writable: true, + }, + ), + ); + } catch (error) { + restoring = false; + throw error; + } + return; + } + if (value === "/expand") { + const card = [...cards.entries()].at(-1); + if (card) { + const [id, tool] = card; + tool.expanded = !tool.expanded; + draw(id, tool.output, tool.toolName, tool.args); + } + return; + } + if (value === "/view") { + inspect(); + return; + } + if (value.startsWith("/panel ")) { + const index = Number(value.slice(7).trim()); + if (!lastImages) + throw new Error( + "No image result yet. Ask the agent to display the memory query's existing exports.", + ); + if ( + !Number.isInteger(index) || + index < 0 || + index > lastImages.images.length + ) + throw new Error(`Choose /panel 0–${lastImages.images.length}.`); + lastImages.select(index); + return; + } + if (value.startsWith("/inspect ")) { + const path = value.slice(9).trim(); + localRender?.abort(); + const controller = new AbortController(); + localRender = controller; + try { + const output = await renderSaved( + paths(), + current.cwd, + path, + {}, + controller.signal, + (done, total) => { + status.setText(`Rendering saved frames ${done}/${total}`); + tui.requestRender(); + }, + ); + if (!controller.signal.aborted) + draw(crypto.randomUUID(), output, "dimcode_render", { path }); + } finally { + if (localRender === controller) { + localRender = undefined; + status.setText(ready()); + } + } + return; + } + if (value === "/abort") { + localRender?.abort(); + await client.call({ type: "abort" }); + return; + } + if (value === "/reload") { + await client.call({ type: "reload" }); + return; + } + if (value === "/models") { + notice( + (await client.call({ type: "get_available_models" })) + .map((model) => model.provider + " " + model.id) + .join("\n"), + ); + return; + } + const [command, provider, option] = value.split(/\s+/); + if (command === "/login") { + await client.call({ + type: "login", + provider, + authType: option === "oauth" ? "oauth" : "api_key", + }); + status.setText("Login complete"); + return; + } + if (command === "/logout") { + await client.call({ type: "logout", provider }); + return; + } + if (command === "/model") { + await client.call({ type: "set_model", provider, modelId: option }); + current.model = { provider, id: option }; + return; + } + if (command === "/steer" || command === "/follow") { + await client.call({ + type: command === "/steer" ? "steer" : "follow_up", + message: value.slice(command.length + 1), + }); + return; + } + if (command === "/image") { + const bytes = await sharp( + await readFile(resolve(current.cwd, value.slice(7))), + ) + .resize({ width: 1280, height: 960, fit: "inside" }) + .png() + .toBuffer(); + await client.call({ + type: "prompt", + message: "Inspect this attached image.", + images: [ + { + type: "image", + mimeType: "image/png", + data: bytes.toString("base64"), + }, + ], + }); + return; + } + if (value.trim()) { + current.busy = true; + status.setText(" Running · Esc to cancel"); + await client.call({ type: "prompt", message: value }); + } + })() + .catch(notice) + .finally(() => tui.requestRender()); + }; + input.onEscape = () => { + if (auth) { + const promptId = auth.promptId; + auth = undefined; + input.secret = false; + input.setValue(""); + void client.call({ type: "ui_response", promptId }).catch(notice); + } else void client.call({ type: "abort" }).catch(notice); + }; + tui.addInputListener((data) => { + if (inspectorOpen && data !== "\x03") return undefined; + if (data === "\x03") { + stop(); + return { consume: true }; + } + return undefined; + }); + const disconnected = new Promise((resolve) => { + client.onClose = () => { + stop(); + resolve(); + }; + }); + tui.start(); + if (options.initialPrompt && !options.sessionId && !options.view) + input.onSubmit(options.initialPrompt); + await disconnected; + } finally { + stop(); + } +} diff --git a/agent/src/tool-images.ts b/agent/src/tool-images.ts new file mode 100644 index 00000000000..e1c122681d1 --- /dev/null +++ b/agent/src/tool-images.ts @@ -0,0 +1,165 @@ +import { TerminalImage as Image } from "./terminal-image.js"; +import { + Text, + truncateToWidth, + visibleWidth, + type Component, + type TuiMouseEvent, +} from "@earendil-works/pi-tui"; +import sharp from "sharp"; +import { accent, clean, muted } from "./terminal-style.js"; + +export interface ResultImage { + data: string; + mimeType: string; + label: string; +} + +/** Presentation only: preserves the images produced by the owning memory operation. */ +export class ResultImages implements Component { + private selected = 0; + private panels: Image[] = []; + private overview?: Image; + private disposed = false; + private error?: string; + private tabs: Array<{ start: number; end: number; index: number }> = []; + constructor( + readonly images: ResultImage[], + private readonly changed: () => void, + ) { + void this.prepare().catch((error) => { + if (!this.disposed) { + this.error = clean(String(error)); + this.changed(); + } + }); + } + close(): void { + this.disposed = true; + this.overview?.close(); + for (const panel of this.panels) panel.close(); + } + invalidate(): void { + this.overview?.invalidate(); + for (const panel of this.panels) panel.invalidate(); + } + select(index: number): void { + this.selected = Math.max(0, Math.min(this.images.length, index)); + this.changed(); + } + private component(png: Buffer): Image { + return new Image( + png.toString("base64"), + "image/png", + { fallbackColor: muted }, + { maxWidthCells: 120, maxHeightCells: 26 }, + this.changed, + ); + } + private async prepare(): Promise { + const bytes = await Promise.all( + this.images.map(async (image) => + sharp(Buffer.from(image.data, "base64"), { density: 144 }) + .resize({ + width: 1600, + height: 1200, + fit: "inside", + withoutEnlargement: true, + }) + .flatten({ background: "#101b21" }) + .png() + .toBuffer(), + ), + ); + if (this.disposed) return; + this.panels = bytes.map((png) => this.component(png)); + if (bytes.length === 1) this.overview = this.panels[0]; + else { + const shown = bytes.slice(0, 6); + const columns = 2; + const top = shown.length >= 3; + const height = + (top ? 230 : 0) + + Math.ceil((shown.length - Number(top)) / columns) * 270; + const tiles = await Promise.all( + shown.map(async (png, i) => { + const first = top && i === 0, + n = i - Number(top); + const width = first ? 1000 : 500; + const imageHeight = first ? 220 : 260; + const tile = await sharp(png) + .resize(width - 12, imageHeight - 12, { + fit: "contain", + background: "#101b21", + }) + .png() + .toBuffer(); + return { + input: tile, + left: first ? 6 : (n % columns) * 500 + 6, + top: first + ? 6 + : (top ? 230 : 0) + Math.floor(n / columns) * 270 + 6, + }; + }), + ); + const png = await sharp({ + create: { width: 1000, height, channels: 3, background: "#101b21" }, + }) + .composite(tiles) + .png() + .toBuffer(); + if (this.disposed) return; + this.overview = this.component(png); + } + this.changed(); + } + handleMouse( + event: TuiMouseEvent, + ): { handled: boolean; render: boolean } | undefined { + if (event.type !== "press" || event.y !== 0) return; + const tab = this.tabs.find( + (tab) => event.x >= tab.start && event.x < tab.end, + ); + if (!tab) return; + this.select(tab.index); + return { handled: true, render: true }; + } + render(width: number): string[] { + const labels = ["Overview", ...this.images.map((image) => image.label)]; + let line = " "; + this.tabs = []; + for (const [index, label] of labels.entries()) { + const text = `${index} ${clean(label).slice(0, 16)} `; + const start = visibleWidth(line), + end = start + visibleWidth(text); + if (end > width) break; + line += (index === this.selected ? accent : muted)(text); + this.tabs.push({ start, end, index }); + } + const panel = this.selected + ? this.panels[this.selected - 1] + : this.overview; + return [ + truncateToWidth(line, width), + ...(panel + ? panel.render(width) + : new Text( + muted( + this.error + ? ` Preview unavailable: ${this.error}` + : " Preparing result views…", + ), + 0, + 0, + ).render(width)), + ...new Text( + muted( + ` /panel 0–${this.images.length} selects a view · original tool output${this.images.length > 6 && !this.selected ? " · overview shows first 6" : ""}`, + ), + 0, + 0, + ).render(width), + ]; + } +} diff --git a/agent/test/blueprint.test.ts b/agent/test/blueprint.test.ts new file mode 100644 index 00000000000..a3e7f637175 --- /dev/null +++ b/agent/test/blueprint.test.ts @@ -0,0 +1,268 @@ +import assert from "node:assert/strict"; +import { spawn, type ChildProcess } from "node:child_process"; +import { createHash } from "node:crypto"; +import { createSocket } from "node:dgram"; +import { once } from "node:events"; +import { writeFile } from "node:fs/promises"; +import { resolve } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; +import { test } from "node:test"; +import { connect, createDecoderRegistry, type Slot } from "@dimos/sdk"; +import sharp from "sharp"; +import { z } from "zod"; +import { MediaPool, nodeTransport } from "../src/media.js"; + +const statsSchema = z.object({ + robots: z.array(z.object({ id: z.string() })), + viewers: z.number(), + perRobot: z.record(z.string(), z.object({ subs: z.array(z.string()) })), +}); +const cloudSchema = z.object({ + bytes: z.instanceof(Uint8Array), + meta: z.object({ + count: z.number().int().positive(), + frame: z.string().min(1), + capture_ts: z.number().positive(), + sha256: z.string(), + }), +}); +async function until( + check: () => boolean | Promise, + label: string, + timeout = 30000, +): Promise { + const deadline = Date.now() + timeout; + while (!(await check())) { + if (Date.now() > deadline) throw new Error("Timed out: " + label); + await delay(50); + } +} +async function stop(child: ChildProcess): Promise { + if (child.exitCode !== null || child.signalCode || !child.pid) return; + const exited = once(child, "exit"); + child.kill("SIGINT"); + const timer = setTimeout(() => { + try { + process.kill(-child.pid!, "SIGKILL"); + } catch {} + }, 10000); + try { + await exited; + } finally { + clearTimeout(timer); + } +} + +test( + "standard Go2 blueprint streams video, full XYZ and odometry through the Node Web SDK", + { + timeout: 150000, + skip: !( + process.env.DIMCODE_TEST_PYTHON && + process.env.DIMCODE_TEST_DENO && + process.env.DIMCODE_TEST_GO2_DB + ), + }, + async (t) => { + const relay = spawn( + process.env.DIMCODE_TEST_DENO!, + [ + "run", + "--allow-net", + "--allow-read", + "../web/relay/main.ts", + "--port", + "0", + ], + { detached: true, stdio: ["ignore", "pipe", "pipe"] }, + ); + t.after(() => stop(relay)); + let relayLog = "", + httpPort = 0; + relay.stderr.on("data", (chunk) => { + relayLog = (relayLog + chunk).slice(-8000); + }); + relay.on("error", (error) => { + relayLog += error.message; + }); + let buffer = ""; + relay.stdout.on("data", (chunk) => { + buffer += chunk; + const lines = buffer.split("\n"); + buffer = lines.pop()!; + for (const line of lines) + if (line.startsWith('{"event":"ready"')) + httpPort = z + .object({ httpPort: z.number() }) + .parse(JSON.parse(line)).httpPort; + }); + await until(() => { + assert.equal(relay.exitCode, null, relayLog); + return httpPort > 0; + }, "relay startup"); + const url = "http://127.0.0.1:" + httpPort; + const stats = async () => + statsSchema.parse(await (await fetch(url + "/api/stats")).json()); + const scout = createSocket("udp4"); + scout.bind(0, "127.0.0.1"); + await once(scout, "listening"); + const scoutPort = scout.address().port; + scout.close(); + const robot = "dimcode-e2e-" + process.pid; + const blueprint = spawn( + process.env.DIMCODE_TEST_PYTHON!, + ["-c", "from go2_media import main; main()"], + { + detached: true, + env: { + ...process.env, + PYTHONPATH: resolve("test/fixtures") + ":" + resolve(".."), + REPLAY: "true", + REPLAY_DB: resolve(process.env.DIMCODE_TEST_GO2_DB!), + VIEWER: "none", + RELAY_URL: url, + ROBOT_ID: robot, + ZENOH_SCOUT_ADDR: "239.255.79.22:" + scoutPort, + }, + stdio: ["ignore", "pipe", "pipe"], + }, + ); + t.after(() => stop(blueprint)); + let blueprintLog = ""; + const record = (chunk: Buffer) => { + blueprintLog = (blueprintLog + chunk).slice(-16000); + }; + blueprint.stdout.on("data", record); + blueprint.stderr.on("data", record); + blueprint.on("error", (error) => { + blueprintLog += error.message; + }); + try { + await until( + async () => { + assert.equal(blueprint.exitCode, null, blueprintLog); + return (await stats()).robots.some((item) => item.id === robot); + }, + "Go2 blueprint registration", + 90000, + ); + assert.equal((await stats()).viewers, 0); + assert.deepEqual((await stats()).perRobot[robot].subs, []); + const decoders = createDecoderRegistry(); + decoders.register("dimcode-test.xyz.v1", (bytes, header) => ({ + value: cloudSchema.parse({ bytes, meta: header.meta }), + })); + let opens = 0; + const pool = new MediaPool( + nodeTransport, + { decoders }, + (options, deps) => { + opens++; + return connect(options, deps); + }, + ); + t.after(() => pool.close()); + assert.equal(opens, 0); + const frames = new Map(), + clouds = new Map(), + poses = new Map(); + const capture = async (channel: string, values: Map) => { + const lease = await pool.acquire( + { url, robot }, + channel, + (slot) => slot, + () => { + const slot = lease.current(); + if (slot) values.set(slot.seq, slot); + }, + ); + t.after(() => lease.close()); + return lease; + }; + const camera = await capture("color_image", frames), + cloud = await capture("lidar", clouds), + pose = await capture("odom", poses); + assert.equal(opens, 1); + await until( + () => frames.size >= 12 && clouds.size >= 5 && poses.size >= 5, + "video, XYZ and pose frames", + ); + const hashes = new Set(); + for (const slot of frames.values()) { + assert(slot.value instanceof Uint8Array); + const decoded = await sharp(slot.value) + .raw() + .toBuffer({ resolveWithObject: true }); + assert(decoded.info.width > 100 && decoded.info.height > 100); + hashes.add(createHash("sha256").update(decoded.data).digest("hex")); + } + assert(hashes.size > 1, "video contains distinct decoded frames"); + const counts: number[] = []; + for (const slot of clouds.values()) { + const { bytes, meta } = cloudSchema.parse(slot.value); + assert.equal(bytes.byteLength, meta.count * 3 * 4); + assert.equal( + createHash("sha256").update(bytes).digest("hex"), + meta.sha256, + ); + const view = new DataView( + bytes.buffer, + bytes.byteOffset, + bytes.byteLength, + ); + let nonzeroZ = 0; + for (let i = 0; i < bytes.byteLength; i += 4) { + const value = view.getFloat32(i, true); + assert(Number.isFinite(value)); + if (i % 12 === 8 && value !== 0) nonzeroZ++; + } + assert(nonzeroZ > 0, "actual Z coordinates, not an XY projection"); + counts.push(meta.count); + } + for (const slot of poses.values()) + z.object({ + x: z.number(), + y: z.number(), + z: z.number(), + ts: z.number().positive(), + }).parse(slot.value); + camera.close(); + pose.close(); + await until( + async () => (await stats()).perRobot[robot].subs.join() === "lidar", + "unused channels unsubscribe", + ); + const previous = clouds.size; + await until( + () => clouds.size > previous, + "point clouds continue after camera closes", + ); + cloud.close(); + await until(async () => { + const value = await stats(); + return value.viewers === 0 && value.perRobot[robot].subs.length === 0; + }, "last renderer releases viewer and subscriptions"); + const report = { + blueprint: "unitree-go2 + cockpit", + videoFrames: frames.size, + distinctVideoFrames: hashes.size, + pointClouds: counts.length, + pointsPerCloud: counts, + odometryFrames: poses.size, + sourceHashesMatch: true, + finalViewers: 0, + finalSubscriptions: [], + }; + t.diagnostic(JSON.stringify(report)); + if (process.env.DIMCODE_TEST_REPORT) + await writeFile( + process.env.DIMCODE_TEST_REPORT, + JSON.stringify(report, null, 2) + "\n", + ); + } catch (error) { + t.diagnostic(blueprintLog); + t.diagnostic(relayLog); + throw error; + } + }, +); diff --git a/agent/test/cli.test.ts b/agent/test/cli.test.ts new file mode 100644 index 00000000000..d07fafc7013 --- /dev/null +++ b/agent/test/cli.test.ts @@ -0,0 +1,311 @@ +import assert from "node:assert/strict"; +import { execFile, spawn } from "node:child_process"; +import { once } from "node:events"; +import { mkdtemp, mkdir, rm, stat } from "node:fs/promises"; +import { createServer, type Socket } from "node:net"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { test } from "node:test"; +import { promisify } from "node:util"; +import { paths, saveConfig, configSchema } from "../src/config.js"; +import { reachable, readLines, requestSchema, send } from "../src/protocol.js"; + +const exec = promisify(execFile); +test( + "CLI opens tui, reports an existing gateway and rejects typos without starting one", + { timeout: 30000 }, + async (t) => { + const home = await mkdtemp(join(tmpdir(), "dimcode-cli-")); + const env = { + ...process.env, + DIMCODE_HOME: join(home, "config"), + XDG_STATE_HOME: join(home, "state"), + XDG_CACHE_HOME: join(home, "cache"), + XDG_RUNTIME_DIR: home, + }; + const cli = resolve(".test/src/main.js"); + const run = (...args: string[]) => + exec(process.execPath, [cli, ...args], { env }); + t.after(async () => { + try { + await run("stop"); + } catch {} + await rm(home, { recursive: true, force: true }); + }); + assert.match((await run("--help")).stdout, /dimcode \[tui\]/); + await assert.rejects(run("typo"), /Unknown command/); + assert(!(await reachable(paths(env).socket))); + const key = "cli-fixture-key"; + const configured = await exec( + process.execPath, + [ + cli, + "setup", + "--provider", + "openai", + "--key-env", + "DIMCODE_FIXTURE_KEY", + "--cwd", + home, + ], + { env: { ...env, DIMCODE_FIXTURE_KEY: key } }, + ); + assert(!configured.stdout.includes(key)); + assert( + !(await reachable(paths(env).socket)), + "provider-only setup does not launch an agent", + ); + const workspace = join(home, "app with spaces"); + await mkdir(workspace); + await run("workspace", workspace); + assert.equal(JSON.parse((await run("config")).stdout).workspace, workspace); + await assert.rejects(run("workspace", cli), /must be a directory/); + await assert.rejects( + run("install-dimos", join(home, "unexpected-env")), + /Unknown command/, + ); + await assert.rejects(stat(join(home, "unexpected-env")), { + code: "ENOENT", + }); + const child = spawn(process.execPath, [cli, "tui"], { + env, + stdio: ["pipe", "pipe", "pipe"], + }); + t.after(() => { + child.kill(); + }); + let output = "", + sent = false, + exited = false, + stderr = ""; + child.stderr.on("data", (chunk) => { + stderr += chunk; + }); + child.stdout.on("data", (chunk) => { + output += chunk; + if (!sent && output.includes("Ready")) { + sent = true; + child.stdin.write("/help\r"); + } + if (sent && !exited && output.includes("/resume ID")) { + exited = true; + child.stdin.end("/exit\r"); + } + }); + const timer = setTimeout(() => child.kill(), 15000); + const [code] = await once(child, "exit"); + clearTimeout(timer); + assert.equal(code, 0, stderr + output); + assert.match(output, /Build apps/); + assert.match(output, /\/resume ID/); + assert.match((await run("gateway")).stdout, /Gateway already running/); + }, +); + +test( + "interactive setup hands off once to a normal agent session without installing DimOS", + { timeout: 20000, skip: process.platform !== "linux" }, + async (t) => { + const home = await mkdtemp(join(tmpdir(), "dimcode-onboard-")); + t.after(() => rm(home, { recursive: true, force: true })); + const env = { + ...process.env, + DIMCODE_HOME: join(home, "config"), + XDG_STATE_HOME: join(home, "state"), + XDG_CACHE_HOME: join(home, "cache"), + XDG_CONFIG_HOME: join(home, "xdg-config"), + XDG_RUNTIME_DIR: home, + DIMCODE_FIXTURE_KEY: "onboarding-private-fixture", + TERM: "xterm-256color", + }; + const cli = resolve(".test/src/main.js"); + await exec( + process.execPath, + [ + cli, + "setup", + "--provider", + "openai", + "--key-env", + "DIMCODE_FIXTURE_KEY", + "--cwd", + home, + ], + { env }, + ); + const sockets = new Set(); + let prompts = 0; + const server = createServer((socket) => { + sockets.add(socket); + socket.on("error", () => {}); + socket.on("close", () => sockets.delete(socket)); + readLines(socket, (value) => { + const request = requestSchema.parse(value); + if (request.command.type === "new_session") { + send(socket, { + type: "response", + id: request.id, + data: { + sessionId: "onboarding", + cwd: home, + seq: 0, + busy: false, + writable: true, + messages: [], + text: "", + notices: [], + tools: [], + }, + }); + } else if (request.command.type === "prompt") { + prompts++; + assert.match(request.command.message, /dimensional-install skill/); + assert.match(request.command.message, /Wait for my answer/); + send(socket, { type: "response", id: request.id, data: null }); + send(socket, { + type: "event", + sessionId: "onboarding", + seq: 1, + event: { + type: "notice", + message: "fixture: agent received setup request", + }, + }); + } + }); + }); + await new Promise((done) => server.listen(paths(env).socket, done)); + t.after(() => { + for (const socket of sockets) socket.destroy(); + return new Promise((done) => server.close(() => done())); + }); + const quote = (arg: string) => "'" + arg.replaceAll("'", "'\\''") + "'"; + const child = spawn( + "script", + [ + "-q", + "-e", + "-c", + [process.execPath, cli, "setup"].map(quote).join(" "), + "/dev/null", + ], + { env, stdio: ["pipe", "pipe", "pipe"] }, + ); + t.after(() => child.kill()); + let output = "", + stage = 0; + const screens = [ + "1. Choose your model provider", + "Sign in", + "2. Choose a model", + "3. Start the gateway at login?", + "fixture: agent received setup request", + ]; + child.stdout.on("data", (chunk) => { + output += chunk; + if (stage < screens.length && output.includes(screens[stage])) { + stage++; + child.stdin.write(stage === screens.length ? "/exit\r" : "\r"); + } + }); + let errors = ""; + child.stderr.on("data", (chunk) => { + errors += chunk; + }); + const timer = setTimeout(() => child.kill(), 15000); + const [code] = await once(child, "exit"); + clearTimeout(timer); + assert.equal(code, 0, errors + output); + assert.equal(stage, screens.length, output); + assert.equal(prompts, 1); + assert(!output.includes(env.DIMCODE_FIXTURE_KEY)); + await assert.rejects(stat(join(home, ".venv")), { code: "ENOENT" }); + }, +); + +test( + "terminal drops old-session events queued during a session switch", + { timeout: 15000 }, + async (t) => { + const home = await mkdtemp(join(tmpdir(), "dimcode-switch-")); + const env = { ...process.env, DIMCODE_HOME: home, XDG_RUNTIME_DIR: home }; + const p = paths(env); + await saveConfig(p, configSchema.parse({ workspace: home })); + const sockets = new Set(); + let sessions = 0; + const server = createServer((socket) => { + sockets.add(socket); + socket.on("error", () => {}); + socket.on("close", () => sockets.delete(socket)); + readLines(socket, (value) => { + const request = requestSchema.parse(value); + if (request.command.type !== "new_session") return; + sessions++; + if (sessions === 2) + send(socket, { + type: "event", + sessionId: "session-a", + seq: 100, + event: { type: "notice", message: "STALE_A_EVENT" }, + }); + send(socket, { + type: "response", + id: request.id, + data: { + sessionId: sessions === 1 ? "session-a" : "session-b", + seq: 0, + cwd: home, + busy: false, + writable: true, + messages: [], + text: "", + notices: [], + tools: [], + }, + }); + if (sessions === 2) + send(socket, { + type: "event", + sessionId: "session-b", + seq: 1, + event: { type: "notice", message: "CURRENT_B_EVENT" }, + }); + }); + }); + t.after(async () => { + for (const socket of sockets) socket.destroy(); + await new Promise((resolve) => server.close(() => resolve())); + await rm(home, { recursive: true, force: true }); + }); + server.listen(p.socket); + await once(server, "listening"); + const child = spawn( + process.execPath, + [resolve(".test/src/main.js"), "tui"], + { env, stdio: ["pipe", "pipe", "pipe"] }, + ); + t.after(() => { + child.kill(); + }); + let output = "", + switched = false, + exited = false; + child.stdout.on("data", (chunk) => { + output += chunk; + if (!switched && output.includes("Ready")) { + switched = true; + child.stdin.write("/new\r"); + } + if (!exited && output.includes("CURRENT_B_EVENT")) { + exited = true; + child.stdin.end("/exit\r"); + } + }); + const timer = setTimeout(() => child.kill(), 10000); + const [code] = await once(child, "exit"); + clearTimeout(timer); + assert.equal(code, 0, output); + assert(!output.includes("STALE_A_EVENT")); + assert(output.includes("CURRENT_B_EVENT")); + }, +); diff --git a/agent/test/clips.test.ts b/agent/test/clips.test.ts new file mode 100644 index 00000000000..a4ff179c29a --- /dev/null +++ b/agent/test/clips.test.ts @@ -0,0 +1,283 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { test } from "node:test"; +import { promisify } from "node:util"; +import { setCapabilities, visibleWidth } from "@earendil-works/pi-tui"; +import sharp from "sharp"; +import { paths } from "../src/config.js"; +import { + clipSourceSchema, + frameDelays, + previewIndexes, + renderClip, +} from "../src/clips.js"; +import { readArtifact } from "../src/artifacts.js"; +import { fitClouds, projectPoint, type Cloud } from "../src/points.js"; +import { renderSaved } from "../src/render.js"; +import { Playback } from "../src/playback.js"; + +test("preview timing is bounded and retains source indexes; projection uses shared bounds", () => { + const times = Array.from({ length: 6000 }, (_, i) => 123 + i / 100); + const indexes = previewIndexes(times); + assert(indexes.length <= 120); + assert.equal(indexes[0], 0); + assert.equal(indexes.at(-1), times.length - 1); + const selected = indexes.map((i) => times[i]); + assert(selected.every((time, i) => !i || time > selected[i - 1])); + assert.deepEqual(frameDelays([0, 0.11, 0.26]), [110, 150, 100]); + const clouds: Cloud[] = [ + { + points: [ + [0, 0, 0], + [1, 2, 3], + ], + }, + { + points: [ + [1, 2, 3], + [8, 4, 2], + ], + }, + ]; + const camera = fitClouds(clouds); + assert.deepEqual( + projectPoint(clouds[0].points[1], camera, 900, 400), + projectPoint(clouds[1].points[0], camera, 900, 400), + ); + assert.equal(camera.extent, 8); + assert.equal(camera.floor, 0); + assert.equal(camera.height, 3); +}); + +test("saved image/SVG clips preserve colors, timestamps and hashes; playback loops without controls and disposes", async (t) => { + const dir = await mkdtemp(join(tmpdir(), "dimcode-clips-")); + t.after(() => rm(dir, { recursive: true, force: true })); + const p = paths({ XDG_CACHE_HOME: dir }); + const source = join(dir, "clip.json"); + const frames = ["#ff0000", "#00ff00", "#0000ff"].map((color, i) => ({ + path: `frame-${i}.svg`, + timestamp: [100, 100.11, 100.26][i], + color, + })); + for (const frame of frames) + await writeFile( + join(dir, frame.path), + ``, + ); + await writeFile( + source, + JSON.stringify({ type: "image", timeOrigin: 99, frames }), + ); + const result = await renderSaved(p, dir, source, { kind: "points" }); + const clip = result.details.clip!; + assert.equal(clip.frames.length, 3); + assert.equal( + result.content.filter((item) => item.type === "image").length, + 1, + "model gets a contact sheet, not every playback frame", + ); + assert.equal( + result.details.kind, + "sequence", + "tagged indexes override a still-view hint", + ); + for (const [i, frame] of clip.frames.entries()) { + assert.equal(frame.timestamp, frames[i].timestamp); + assert.equal(frame.index, i); + assert.equal( + frame.sourceSha256, + createHash("sha256") + .update(await readFile(frame.source)) + .digest("hex"), + ); + const { bytes } = await readArtifact(frame.path, frame.sha256); + const rgb = await sharp(bytes) + .extract({ left: 450, top: 200, width: 1, height: 1 }) + .raw() + .toBuffer(); + assert.deepEqual( + [...rgb], + [0, 1, 2].map((channel) => (channel === i ? 255 : 0)), + ); + } + const gif = await sharp(clip.gif!, { animated: true }).metadata(); + assert.equal(gif.pages, 3); + assert.deepEqual(gif.delay, [110, 150, 100]); + const single = await renderSaved(p, dir, source, { frame: 1 }); + assert.equal(single.details.clip?.frames[0].index, 1); + assert.equal(single.details.clip?.gif, undefined); + await assert.rejects( + renderSaved(p, dir, source, { frame: 3 }), + /source frame index/, + ); + setCapabilities({ images: null, trueColor: true, hyperlinks: false }); + let updates = 0; + let onChange = () => {}; + const view = new Playback(clip, () => { + updates++; + onChange(); + }); + t.after(() => view.close()); + await view.ready; + for (const width of [1, 20, 80]) + assert(view.render(width).every((line) => visibleWidth(line) <= width)); + assert.doesNotMatch(view.render(80).join("\n"), /Play|Pause|seek|timeline/); + const looped = new Promise((resolve, reject) => { + const timeout = setTimeout( + () => reject(new Error("Playback did not loop")), + 1500, + ); + let reachedLast = false; + onChange = () => { + reachedLast ||= view.frameIndex === 2; + if (reachedLast && view.frameIndex === 0) { + clearTimeout(timeout); + resolve(); + } + }; + }); + await looped; + assert.equal(view.playing, true, "clips loop without controls"); + view.pause(); + assert.equal(view.playing, false); + view.close(); + const closed = updates; + view.pause(); + assert.equal(updates, closed, "disposed views cannot notify or restart"); + const pausedWhileLoading = new Playback(clip, () => {}, true); + pausedWhileLoading.pause(); + await pausedWhileLoading.ready; + assert.equal( + pausedWhileLoading.playing, + false, + "loading cannot restart a paused or hidden clip", + ); + pausedWhileLoading.close(); + await writeFile(clip.frames[0].path, "changed"); + const stale = new Playback(clip, () => {}); + await stale.ready; + assert.match(stale.render(80).join("\n"), /Source changed/); + assert.equal(stale.playing, false); + stale.close(); +}); + +test("cloud clips reject changed sources, mismatched frames/times, invalid windows and cancellation", async (t) => { + const dir = await mkdtemp(join(tmpdir(), "dimcode-clouds-")); + t.after(() => rm(dir, { recursive: true, force: true })); + const p = paths({ XDG_CACHE_HOME: dir }), + source = join(dir, "clouds.json"); + const frames = [ + { path: "a.json", timestamp: 1 }, + { path: "b.json", timestamp: 1.2 }, + ]; + const cloud: Cloud = { + points: [ + [0, 0, 0], + [1, 2, 3], + ], + frame: "world", + colors: [ + [255, 0, 0], + [0, 255, 0], + ], + selectedIndices: [1], + }; + for (const frame of frames) + await writeFile( + join(dir, frame.path), + JSON.stringify({ ...cloud, timestamp: frame.timestamp }), + ); + const data = { type: "points", frames }; + const original = await readFile(join(dir, frames[0].path)); + const result = await renderClip(p, source, data); + assert.equal(result.clip.frames.length, 2); + assert.match(result.summary, /fixed camera and height scale/); + assert.deepEqual(await readFile(join(dir, frames[0].path)), original); + await assert.rejects( + renderClip(p, source, { + ...data, + frames: [{ ...frames[0], sha256: "wrong" }], + }), + /Source changed/, + ); + await writeFile( + join(dir, "b.json"), + JSON.stringify({ ...cloud, frame: "camera" }), + ); + await assert.rejects(renderClip(p, source, data), /Mixed coordinate/); + await writeFile( + join(dir, "b.json"), + JSON.stringify({ ...cloud, timestamp: 7 }), + ); + await assert.rejects(renderClip(p, source, data), /timestamp differs/); + assert.throws( + () => clipSourceSchema.parse({ ...data, frames: frames.toReversed() }), + /strictly increasing/, + ); + assert.throws(() => clipSourceSchema.parse({ ...data, frames: [] })); + await assert.rejects( + renderClip(p, source, { + ...data, + frames: [frames[0], { ...frames[1], timestamp: 100 }], + }), + /60 seconds/, + ); + const abort = new AbortController(); + abort.abort(); + await assert.rejects(renderClip(p, source, data, abort.signal), /abort/i); + await writeFile(source, JSON.stringify({ series: [] })); + await assert.rejects(renderSaved(p, dir, source), /DimOS or Python as SVG/); +}); + +test( + "Go2 memory seconds 1–5 export once and render cloud, camera and native plot", + { + timeout: 120000, + skip: !(process.env.DIMCODE_TEST_PYTHON && process.env.DIMCODE_TEST_GO2_DB), + }, + async (t) => { + const dir = await mkdtemp(join(tmpdir(), "dimcode-playback-go2-")); + t.after(() => rm(dir, { recursive: true, force: true })); + await promisify(execFile)( + process.env.DIMCODE_TEST_PYTHON!, + ["test/fixtures/clip_views.py", dir], + { env: { ...process.env, PYTHONPATH: resolve("..") }, timeout: 60000 }, + ); + const p = paths({ XDG_CACHE_HOME: dir }); + for (const type of ["points", "image"]) { + const source = join(dir, type + ".json"); + const index = clipSourceSchema.parse( + JSON.parse(await readFile(source, "utf8")), + ); + assert(index.frames.length > 20); + assert( + index.frames.every( + (frame) => + frame.timestamp >= index.timeOrigin! + 1 && + frame.timestamp < index.timeOrigin! + 5, + ), + ); + const result = await renderSaved(p, dir, source); + const clip = result.details.clip!; + assert(clip.frames.length > 15); + assert.equal(clip.sourceCount, index.frames.length); + for (const frame of clip.frames) { + assert.equal(frame.sourceSha256, index.frames[frame.index].sha256); + assert.equal(frame.timestamp, index.frames[frame.index].timestamp); + } + assert.equal( + (await sharp(clip.gif!, { animated: true }).metadata()).pages, + clip.frames.length, + ); + } + const plot = await renderSaved(p, dir, "point-count.svg"); + assert.equal( + plot.content.filter((item) => item.type === "image").length, + 1, + ); + }, +); diff --git a/agent/test/fixtures/clip_views.py b/agent/test/fixtures/clip_views.py new file mode 100644 index 00000000000..a16bbeea35a --- /dev/null +++ b/agent/test/fixtures/clip_views.py @@ -0,0 +1,83 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Export one real Go2 memory window through DimOS, independently of the harness.""" + +import hashlib +import json +import os +from pathlib import Path +import sys + +from dimos.memory.store.sqlite import SqliteStore +from dimos.memory.vis.plot.elements import Series +from dimos.memory.vis.plot.plot import Plot + + +def main() -> None: + out = Path(sys.argv[1]) + out.mkdir(parents=True, exist_ok=True) + store = SqliteStore(path=os.environ["DIMCODE_TEST_GO2_DB"], must_exist=True) + try: + origin = store.streams.lidar.first().ts + counts = [] + times = [] + report = {"source": os.environ["DIMCODE_TEST_GO2_DB"], "origin": origin, "interval": [1, 5]} + for name, kind in [("lidar", "points"), ("color_image", "image")]: + # Materialize once. Rendering reads these exports, never the database. + selected = store.stream(name).time_range(origin + 1, origin + 5).materialize() + frames = [] + for i, obs in enumerate(selected): + path = out / f"{name}-{i:03d}.{'json' if kind == 'points' else 'png'}" + if kind == "points": + xyz, rgb = obs.data.as_numpy() + data = {"points": xyz.tolist(), "frame": obs.data.frame_id, "timestamp": obs.ts} + if rgb is not None: + data["colors"] = (rgb * 255).round().clip(0, 255).astype(int).tolist() + path.write_text(json.dumps(data, separators=(",", ":"))) + counts.append(len(xyz)) + times.append(obs.ts) + else: + obs.data.save(str(path)) + frames.append( + { + "path": path.name, + "timestamp": obs.ts, + "sha256": hashlib.sha256(path.read_bytes()).hexdigest(), + } + ) + assert frames, f"Missing {name} observations" + index = { + "type": kind, + "timeOrigin": origin, + "source": f"Go2 memory.db · {name} · time_range(origin+1, origin+5).materialize()", + "frames": frames, + } + (out / f"{kind}.json").write_text(json.dumps(index, indent=2)) + report[kind] = { + "count": len(frames), + "first": frames[0]["timestamp"], + "last": frames[-1]["timestamp"], + } + Plot().add( + Series(ts=times, values=counts, label="source point count", color="#43bda7") + ).to_svg(str(out / "point-count.svg")) + (out / "query.json").write_text(json.dumps(report, indent=2)) + print(json.dumps(report)) + finally: + store.stop() + + +if __name__ == "__main__": + main() diff --git a/agent/test/fixtures/go2_media.py b/agent/test/fixtures/go2_media.py new file mode 100644 index 00000000000..a25524e186a --- /dev/null +++ b/agent/test/fixtures/go2_media.py @@ -0,0 +1,62 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Standard Go2 replay + cockpit; the test codec preserves every XYZ float.""" + +import hashlib + +from dimos.core.coordination.blueprints import autoconnect +from dimos.core.coordination.module_coordinator import ModuleCoordinator +from dimos.msgs.geometry_msgs.PoseStamped import PoseStamped +from dimos.msgs.sensor_msgs.Image import Image +from dimos.msgs.sensor_msgs.PointCloud2 import PointCloud2 +from dimos.robot.unitree.go2.blueprints.smart.unitree_go2 import unitree_go2 +from dimos.web.cockpit import Channel, cockpit +from dimos.web.codecs import EncodedPayload, web_encoder +from dimos.web.websocket_vis.websocket_vis_module import WebsocketVisModule + + +@web_encoder("dimcode-test.xyz.v1") +def encode_xyz(msg: PointCloud2) -> EncodedPayload: + points = msg.points_f32().astype(" None: + ModuleCoordinator.build(go2_media).loop() + + +if __name__ == "__main__": + main() diff --git a/agent/test/fixtures/mcp_server.py b/agent/test/fixtures/mcp_server.py new file mode 100644 index 00000000000..5f97052b902 --- /dev/null +++ b/agent/test/fixtures/mcp_server.py @@ -0,0 +1,50 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Actual DimOS MCP handlers with deterministic tools; no coordinator or robot.""" + +import json +import os + +import numpy as np +import uvicorn + +from dimos.agents.mcp.mcp_server import app +from dimos.agents.skill_result import SkillResult +from dimos.core.module import SkillInfo +from dimos.msgs.sensor_msgs.Image import Image + +app.state.skills = [ + SkillInfo( + class_name="Fixture", + func_name="echo", + args_schema=json.dumps( + {"type": "object", "properties": {"value": {"type": "string"}}, "required": ["value"]} + ), + ) +] +for name in ("image", "failure"): + app.state.skills.append( + SkillInfo( + class_name="Fixture", + func_name=name, + args_schema=json.dumps({"type": "object", "properties": {}}), + ) + ) +app.state.rpc_calls = { + "echo": lambda value, **kwargs: value, + "image": lambda **kwargs: Image(np.zeros((8, 8, 3), dtype=np.uint8)), + "failure": lambda **kwargs: SkillResult.fail("FIXTURE_FAILURE", "Expected failure"), +} +uvicorn.run(app, host="127.0.0.1", port=int(os.environ["DIMCODE_TEST_PORT"]), log_level="error") diff --git a/agent/test/fixtures/memory_views.py b/agent/test/fixtures/memory_views.py new file mode 100644 index 00000000000..8f41f1f0b84 --- /dev/null +++ b/agent/test/fixtures/memory_views.py @@ -0,0 +1,79 @@ +# Copyright 2026 Dimensional Inc. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +"""Recorded Go2 memory query; all analysis and exports use existing DimOS APIs.""" + +import json +import os +from pathlib import Path +import sys + +from dimos.memory.store.sqlite import SqliteStore +from dimos.memory.transform import peaks, significant +from dimos.memory.vis.plot.elements import Series, VLine +from dimos.memory.vis.plot.plot import Plot +from dimos.memory.vis.space.space import Space +from dimos.memory.vis.utils import mosaic +from dimos.models.embedding.clip import CLIPModel + + +def main() -> None: + out = Path(sys.argv[1]) + out.mkdir(parents=True, exist_ok=True) + store = SqliteStore(path=os.environ["DIMCODE_TEST_GO2_DB"], must_exist=True) + try: + vector = CLIPModel(device="cpu").embed_text("plant") + query = store.streams.color_image_embedded.search(vector).order_by("ts").materialize() + rows = list(query) + selected = query.transform(peaks(key=lambda obs: obs.similarity, distance=1.0)) + selected = selected.transform(significant(method="mad")).materialize() + matches = list(selected) + assert len(rows) > 10 and matches, "Recording must contain scored images and peaks" + plot = Plot().add( + Series( + ts=[row.ts for row in rows], + values=[row.similarity for row in rows], + color="#43bda7", + label="plant similarity", + connect=7.5, + ) + ) + for row in matches: + plot.add(VLine(row.ts, color="#e87c59")) + plot.to_svg(str(out / "timeline.svg")) + # The context cloud and selected image poses both come from this recording. + context = [store.streams.lidar.at(row.ts).last() for row in matches] + assert all(row is not None for row in context) + Space().add([row.data for row in context]).add(selected).to_svg(str(out / "space.svg")) + mosaic(selected, cols=min(3, len(matches)), cell_height=300).data.save( + str(out / "frames.png") + ) + report = { + "query": "plant", + "source": os.environ["DIMCODE_TEST_GO2_DB"], + "operation": "search(CLIP plant).order_by(ts).materialize() → peaks(distance=1) → significant(mad)", + "observations": len(rows), + "context_cloud_timestamps": [row.ts for row in context], + "selected": [ + {"id": row.id, "timestamp": row.ts, "similarity": row.similarity} for row in matches + ], + } + (out / "query.json").write_text(json.dumps(report, indent=2) + "\n") + print(json.dumps(report)) + finally: + store.stop() + + +if __name__ == "__main__": + main() diff --git a/agent/test/fixtures/session.ts b/agent/test/fixtures/session.ts new file mode 100644 index 00000000000..236e60b5a99 --- /dev/null +++ b/agent/test/fixtures/session.ts @@ -0,0 +1,134 @@ +import { + createAgentSessionServices, + createAgentSessionFromServices, + type ExtensionFactory, +} from "@earendil-works/pi-coding-agent"; +import { + createAssistantMessageEventStream, + type AssistantMessage, +} from "@earendil-works/pi-ai"; +import { Type } from "typebox"; +import type { SessionFactory } from "../../src/gateway.js"; + +export function deferred() { + let resolve!: (value: T) => void; + const promise = new Promise((done) => { + resolve = done; + }); + return { promise, resolve }; +} +export function fixtureFactory( + wait: Promise, + started: () => void, +): SessionFactory { + return async (_config, paths, manager) => { + const extension: ExtensionFactory = (pi) => { + pi.registerTool({ + name: "wait_for_test", + label: "Wait", + description: "Deterministic test operation", + parameters: Type.Object({}), + execute: async (_id, _args, signal) => { + started(); + await Promise.race([ + wait, + new Promise((_, reject) => + signal?.addEventListener( + "abort", + () => reject(new Error("aborted")), + { once: true }, + ), + ), + ]); + return { + content: [{ type: "text", text: "operation finished" }], + details: {}, + }; + }, + }); + pi.registerProvider("dimcode-fixture", { + baseUrl: "http://localhost.invalid", + apiKey: "nonsecret-fixture", + api: "openai-completions", + models: [ + { + id: "fixture", + name: "fixture", + reasoning: false, + input: ["text"], + cost: { input: 0, output: 0, cacheRead: 0, cacheWrite: 0 }, + contextWindow: 32000, + maxTokens: 100, + }, + ], + streamSimple: (model, context) => { + const stream = createAssistantMessageEventStream(); + queueMicrotask(() => { + const done = context.messages.at(-1)?.role === "toolResult"; + const message: AssistantMessage = { + role: "assistant", + api: model.api, + provider: model.provider, + model: model.id, + content: done + ? [{ type: "text", text: "done" }] + : [ + { + type: "toolCall", + id: "call-" + context.messages.length, + name: "wait_for_test", + arguments: {}, + }, + ], + stopReason: done ? "stop" : "toolUse", + timestamp: Date.now(), + usage: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + totalTokens: 0, + cost: { + input: 0, + output: 0, + cacheRead: 0, + cacheWrite: 0, + total: 0, + }, + }, + }; + stream.push({ type: "start", partial: message }); + stream.push({ + type: "done", + reason: done ? "stop" : "toolUse", + message, + }); + stream.end(); + }); + return stream; + }, + }); + }; + const services = await createAgentSessionServices({ + cwd: manager.getCwd(), + agentDir: paths.config, + resourceLoaderOptions: { + extensionFactories: [extension], + noExtensions: true, + noSkills: true, + }, + }); + const { session } = await createAgentSessionFromServices({ + services, + sessionManager: manager, + model: services.modelRuntime.getModel("dimcode-fixture", "fixture"), + }); + await session.bindExtensions({ mode: "rpc" }); + return { + session, + close: async () => { + session.dispose(); + }, + }; + }; +} diff --git a/agent/test/gateway.test.ts b/agent/test/gateway.test.ts new file mode 100644 index 00000000000..6e7e784e038 --- /dev/null +++ b/agent/test/gateway.test.ts @@ -0,0 +1,247 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { mkdtemp, mkdir, readFile, readdir, rm, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { randomUUID } from "node:crypto"; +import { Gateway } from "../src/gateway.js"; +import { Connection, type Event } from "../src/protocol.js"; +import { configSchema, type Paths } from "../src/config.js"; +import { deferred, fixtureFactory } from "./fixtures/session.js"; + +test( + "detached turn, ownership, deduplicated prompt and persisted resume", + { timeout: 20000 }, + async (t) => { + const root = await mkdtemp(join(tmpdir(), "dimcode-")); + t.after(() => rm(root, { recursive: true, force: true })); + const p: Paths = { + config: join(root, "config"), + state: root, + cache: join(root, "cache"), + sessions: join(root, "sessions"), + socket: join(root, "test.sock"), + }; + await mkdir(p.config); + await mkdir(p.sessions); + const release = deferred(), + started = deferred(); + const config = configSchema.parse({ workspace: root }); + let gateway = new Gateway( + config, + p, + fixtureFactory(release.promise, () => started.resolve()), + ); + await gateway.start(); + t.after(() => gateway.close()); + const first = new Connection(p.socket), + viewer = new Connection(p.socket); + t.after(() => { + first.close(); + viewer.close(); + }); + const snapshot = await first.call({ type: "new_session" }); + await viewer.call({ + type: "attach", + sessionId: snapshot.sessionId, + writable: false, + }); + await assert.rejects( + viewer.call({ type: "prompt", message: "forbidden" }), + /read-only/, + ); + // Provider credentials travel only to the input owner and Pi's credential store. + const auth = deferred>(); + const viewerEvents: Event[] = []; + first.onEvent = (_seq, event) => { + if (event.type === "auth_prompt") auth.resolve(event); + }; + viewer.onEvent = (_seq, event) => viewerEvents.push(event); + const login = first.call({ + type: "login", + provider: "openai", + authType: "api_key", + }); + const dialog = await auth.promise; + assert.equal(dialog.prompt.type, "secret"); + await assert.rejects( + viewer.call({ + type: "ui_response", + promptId: dialog.promptId, + value: "not-owner", + }), + /read-only/, + ); + const credential = "local-fixture-credential"; + await first.call({ + type: "ui_response", + promptId: dialog.promptId, + value: credential, + }); + await login; + assert( + !viewerEvents.some( + (event) => event.type === "auth_prompt" || event.type === "auth_info", + ), + ); + assert.equal((await stat(join(p.config, "auth.json"))).mode & 0o777, 0o600); + const id = randomUUID(); + await first.call({ type: "prompt", message: "wait" }, id); + await started.promise; + await first.call({ type: "prompt", message: "wait" }, id); + const closed = deferred(); + first.onClose = () => closed.resolve(); + first.close(); + await closed.promise; + const state = await viewer.call({ type: "get_state" }); + assert.equal(state.busy, true); + assert(state.tools.some((tool) => tool.type === "tool_execution_start")); + const finished = deferred(); + viewer.onEvent = (_seq, event: Event) => { + if (event.type === "idle") finished.resolve(); + }; + release.resolve(); + await finished.promise; + assert.equal( + (await viewer.call({ type: "get_state" })).messages.filter( + (message) => message.role === "user", + ).length, + 1, + ); + for (const name of await readdir(p.sessions, { recursive: true })) { + if (name.endsWith(".jsonl")) + assert( + !(await readFile(join(p.sessions, name), "utf8")).includes( + credential, + ), + ); + } + viewer.close(); + await gateway.close(); + gateway = new Gateway( + config, + p, + fixtureFactory(Promise.resolve(), () => {}), + ); + await gateway.start(); + const resumed = new Connection(p.socket); + t.after(() => resumed.close()); + const simultaneous = new Connection(p.socket); + t.after(() => simultaneous.close()); + const [restored, observed] = await Promise.all([ + resumed.call({ + type: "attach", + sessionId: snapshot.sessionId, + writable: true, + }), + simultaneous.call({ + type: "attach", + sessionId: snapshot.sessionId, + writable: false, + }), + ]); + assert.equal(restored.sessionId, observed.sessionId); + assert(restored.messages.some((message) => message.role === "toolResult")); + assert.equal(restored.busy, false); + await resumed.call({ type: "prompt", message: "wait" }, id); + assert.equal( + (await resumed.call({ type: "get_state" })).messages.filter( + (message) => message.role === "user", + ).length, + 1, + ); + const second = new Connection(p.socket); + t.after(() => second.close()); + const cwd = join(root, "second"); + await mkdir(cwd); + assert.equal((await second.call({ type: "new_session", cwd })).cwd, cwd); + const unattached = new Connection(p.socket); + const disconnected = deferred(); + unattached.onClose = () => disconnected.resolve(); + await gateway.close(); + await disconnected.promise; + await assert.rejects( + unattached.call({ type: "list_sessions" }), + /disconnected/, + ); + }, +); + +test( + "large image history remains attachable and events identify their originating session", + { timeout: 15000 }, + async (t) => { + const root = await mkdtemp(join(tmpdir(), "dimcode-history-")); + t.after(() => rm(root, { recursive: true, force: true })); + const p: Paths = { + config: join(root, "config"), + state: root, + cache: join(root, "cache"), + sessions: join(root, "sessions"), + socket: join(root, "test.sock"), + }; + await mkdir(p.config); + await mkdir(p.sessions); + const factory = fixtureFactory(Promise.resolve(), () => {}); + let completeHistory = 0; + const gateway = new Gateway( + configSchema.parse({ workspace: root }), + p, + async (...args) => { + const manager = args[2]; + for (let i = 0; i < 3; i++) + manager.appendMessage({ + role: "user", + content: [ + { + type: "image", + mimeType: "image/png", + data: "A".repeat(6 * 1024 * 1024), + }, + ], + timestamp: Date.now(), + }); + manager.appendMessage({ + role: "user", + content: "most recent message", + timestamp: Date.now(), + }); + const handle = await factory(...args); + completeHistory = handle.session.messages.length; + return handle; + }, + ); + await gateway.start(); + t.after(() => gateway.close()); + const client = new Connection(p.socket); + t.after(() => client.close()); + const first = await client.call({ type: "new_session" }); + assert.equal(completeHistory, 4); + assert(first.messages.length < completeHistory); + const latest = first.messages.at(-1); + assert(latest?.role === "user"); + assert.equal(latest.content, "most recent message"); + assert( + first.notices.some((text) => text.includes("earlier messages omitted")), + ); + const origin = deferred(); + client.onEvent = (_seq, _event, sessionId) => origin.resolve(sessionId); + await client.call({ type: "abort" }); + assert.equal(await origin.promise, first.sessionId); + const second = await client.call({ type: "new_session" }); + assert.notEqual(second.sessionId, first.sessionId); + const next = deferred(); + client.onEvent = (_seq, _event, sessionId) => next.resolve(sessionId); + await client.call({ type: "abort" }); + assert.equal(await next.promise, second.sessionId); + const resumed = await client.call({ + type: "attach", + sessionId: first.sessionId, + writable: true, + }); + const restored = resumed.messages.at(-1); + assert(restored?.role === "user"); + assert.equal(restored.content, "most recent message"); + assert.equal(completeHistory, 4); + }, +); diff --git a/agent/test/integration.test.ts b/agent/test/integration.test.ts new file mode 100644 index 00000000000..8a1d09bfa6c --- /dev/null +++ b/agent/test/integration.test.ts @@ -0,0 +1,208 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { spawn, type ChildProcess } from "node:child_process"; +import { createServer } from "node:net"; +import { once } from "node:events"; +import { resolve } from "node:path"; +import { setTimeout as delay } from "node:timers/promises"; +import { + Client, + StreamableHTTPClientTransport, +} from "@modelcontextprotocol/client"; +import { WebTransport } from "rwebtransport"; +import sharp from "sharp"; +import { + CONTROL_CHANNEL, + PROTOCOL_VERSION, + encodeDataFrame, + encodeDatagram, +} from "@dimos/shared"; +import { MediaPool } from "../src/media.js"; +import { McpTools, toolName } from "../src/mcp.js"; + +async function stop(child: ChildProcess): Promise { + if (child.exitCode !== null || child.signalCode) return; + child.kill("SIGTERM"); + await once(child, "exit"); +} +async function port(): Promise { + const server = createServer(); + server.listen(0, "127.0.0.1"); + await once(server, "listening"); + const address = server.address(); + assert(address && typeof address !== "string"); + await new Promise((resolve) => server.close(() => resolve())); + return address.port; +} +test( + "official MCP client consumes actual DimOS handlers, images and errors", + { timeout: 30000, skip: !process.env.DIMCODE_TEST_PYTHON }, + async (t) => { + const endpoint = await port(); + const child = spawn( + process.env.DIMCODE_TEST_PYTHON!, + ["test/fixtures/mcp_server.py"], + { + env: { + ...process.env, + PYTHONPATH: resolve(".."), + DIMCODE_TEST_PORT: String(endpoint), + }, + stdio: ["ignore", "ignore", "pipe"], + }, + ); + t.after(() => stop(child)); + const url = "http://127.0.0.1:" + endpoint + "/mcp"; + const deadline = Date.now() + 15000; + while (true) { + try { + await fetch(url, { + method: "POST", + body: JSON.stringify({ + jsonrpc: "2.0", + method: "notifications/initialized", + }), + }); + break; + } catch { + if (Date.now() > deadline) + throw new Error("MCP server startup deadline"); + await delay(50); + } + } + const client = new Client({ name: "dimcode-test", version: "0.1.0" }); + t.after(() => client.close()); + await client.connect(new StreamableHTTPClientTransport(new URL(url))); + const listing = await client.listTools(); + assert(listing.tools.some((tool) => tool.name === "image")); + assert.deepEqual( + (await client.callTool({ name: "echo", arguments: { value: "exact" } })) + .content, + [{ type: "text", text: "exact" }], + ); + assert.equal( + (await client.callTool({ name: "image", arguments: {} })).content[0].type, + "image", + ); + const failed = await client.callTool({ name: "failure", arguments: {} }); + assert.equal(failed.isError, true); + assert.match(JSON.stringify(failed.content), /FIXTURE_FAILURE/); + let endpoints = [{ name: "fixture", url }]; + const registry = new McpTools(async () => endpoints); + t.after(() => registry.close()); + const tools = await registry.tools(); + assert.equal(tools.length, listing.tools.length); + assert.notEqual(toolName("a", "tool.name"), toolName("a", "tool_name")); + assert( + toolName("long-endpoint".repeat(10), "long-tool".repeat(20)).length <= 64, + ); + endpoints = []; + assert.deepEqual(await registry.tools(), []); + endpoints = [{ name: "fixture", url }]; + assert.equal((await registry.tools()).length, listing.tools.length); + assert.deepEqual( + tools.find((tool) => tool.label.endsWith("/ echo"))?.parameters, + listing.tools.find((tool) => tool.name === "echo")?.inputSchema, + ); + }, +); + +test( + "Node Web SDK receives real JPEG over the existing Deno QUIC relay", + { timeout: 30000, skip: !process.env.DIMCODE_TEST_DENO }, + async (t) => { + const child = spawn( + process.env.DIMCODE_TEST_DENO!, + [ + "run", + "--allow-net", + "--allow-read", + "../web/relay/main.ts", + "--port", + "0", + ], + { stdio: ["ignore", "pipe", "pipe"] }, + ); + t.after(() => stop(child)); + const info = await new Promise<{ + httpPort: number; + wtUrl: string; + certHash: string; + }>((resolve, reject) => { + let buffer = ""; + child.stdout.on("data", (chunk) => { + buffer += chunk.toString(); + for (const line of buffer.split("\n").slice(0, -1)) { + if (line.startsWith('{"event":"ready"')) resolve(JSON.parse(line)); + } + buffer = buffer.slice(buffer.lastIndexOf("\n") + 1); + }); + child.once("error", reject); + child.once("exit", (code) => reject(new Error("Relay exited " + code))); + }); + const robot = new WebTransport(info.wtUrl + "/robot", { + serverCertificateHashes: [ + { + algorithm: "sha-256", + value: Uint8Array.from(Buffer.from(info.certHash, "base64")).buffer, + }, + ], + }); + t.after(() => robot.close()); + await robot.ready; + const send = async (ch: string, seq: number, bytes: Uint8Array) => { + const stream = await robot.createBidirectionalStream(); + const writer = stream.writable.getWriter(); + await writer.write( + encodeDataFrame({ ch, seq, ts: seq, delivery: "latest" }, bytes), + ); + await writer.close(); + }; + await send( + CONTROL_CHANNEL, + 0, + encodeDatagram({ + t: "hello", + v: PROTOCOL_VERSION, + role: "robot", + robot: { id: "fixture", name: "Fixture", model: "test" }, + manifest: { + version: 1, + channels: [ + { + ch: "camera", + encoding: "jpeg.v1", + delivery: "latest", + maxHz: 30, + }, + ], + }, + }), + ); + const reader = robot.datagrams.readable.getReader(); + await reader.read(); + reader.releaseLock(); + const pool = new MediaPool(); + t.after(() => pool.close()); + const lease = await pool.acquire( + { url: "http://127.0.0.1:" + info.httpPort, robot: "fixture" }, + "camera", + (slot) => slot, + () => {}, + ); + t.after(() => lease.close()); + const jpeg = await sharp({ + create: { width: 64, height: 32, channels: 3, background: "#40d0a0" }, + }) + .jpeg() + .toBuffer(); + const deadline = Date.now() + 10000; + let seq = 1; + while (!lease.current()) { + if (Date.now() > deadline) throw new Error("JPEG reception deadline"); + await send("camera", seq++, jpeg); + await delay(25); + } + assert.deepEqual(lease.current()?.value, new Uint8Array(jpeg)); + }, +); diff --git a/agent/test/lifecycle.test.ts b/agent/test/lifecycle.test.ts new file mode 100644 index 00000000000..ad115b748c1 --- /dev/null +++ b/agent/test/lifecycle.test.ts @@ -0,0 +1,94 @@ +import assert from "node:assert/strict"; +import { spawn } from "node:child_process"; +import { once } from "node:events"; +import { mkdtemp, rm } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { configSchema, type Paths } from "../src/config.js"; +import { Gateway } from "../src/gateway.js"; +import { Connection, reachable } from "../src/protocol.js"; +import { serviceUnit } from "../src/service.js"; +import { deferred, fixtureFactory } from "./fixtures/session.js"; + +test( + "recover a stale socket and interrupted turn without replaying the operation", + { timeout: 20000 }, + async (t) => { + const root = await mkdtemp(join(tmpdir(), "dimcode-recovery-")); + t.after(() => rm(root, { recursive: true, force: true })); + const p: Paths = { + config: join(root, "config"), + state: root, + sessions: join(root, "sessions"), + cache: join(root, "cache"), + socket: join(root, "gateway.sock"), + }; + const stale = spawn(process.execPath, [ + "--input-type=module", + "-e", + 'import {createServer} from "node:net";createServer().listen(process.argv[1],()=>process.stdout.write("ready"));', + p.socket, + ]); + t.after(() => { + stale.kill(); + }); + await once(stale.stdout, "data"); + stale.kill("SIGKILL"); + await once(stale, "exit"); + const started = deferred(), + release = deferred(); + let calls = 0; + const config = configSchema.parse({ workspace: root }); + let gateway = new Gateway( + config, + p, + fixtureFactory(release.promise, () => { + calls++; + started.resolve(); + }), + ); + await gateway.start(); + t.after(() => gateway.close()); + await assert.rejects(new Gateway(config, p).start(), /EADDRINUSE/); + assert.equal(await reachable(p.socket), true); + const client = new Connection(p.socket); + t.after(() => client.close()); + const snapshot = await client.call({ type: "new_session" }); + await client.call({ type: "prompt", message: "wait" }); + await started.promise; + await gateway.close(); + gateway = new Gateway( + config, + p, + fixtureFactory(Promise.resolve(), () => { + calls++; + }), + ); + await gateway.start(); + const resumed = new Connection(p.socket); + t.after(() => resumed.close()); + const state = await resumed.call({ + type: "attach", + sessionId: snapshot.sessionId, + writable: true, + }); + assert.equal(state.busy, false); + assert.equal(calls, 1); + assert(state.notices.some((notice) => notice.includes("not replayed"))); + assert(state.messages.some((message) => message.role === "user")); + const unit = serviceUnit( + config, + p, + join(root, "path with spaces", "100%.js"), + ); + assert( + unit.includes( + '"' + join(root, "path with spaces", "100%%.js") + '" gateway', + ), + ); + assert(unit.includes('Environment="DIMCODE_HOME=' + p.config + '"')); + assert(unit.includes("WorkingDirectory=" + root + "\n")); + assert(!unit.includes("api_key")); + }, +); diff --git a/agent/test/media.test.ts b/agent/test/media.test.ts new file mode 100644 index 00000000000..4721de0fafe --- /dev/null +++ b/agent/test/media.test.ts @@ -0,0 +1,105 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { connect, createDecoderRegistry } from "@dimos/sdk"; +import { + FakeRelayEnd, + INFO, + ROBOT_A, + manifest, + spec, + until, +} from "../../web/sdk/src/testing/fakeRelay.ts"; +import { MediaPool } from "../src/media.js"; + +test("generic media is lazy, shared, byte exact and released by its last consumer", async () => { + const relay = new FakeRelayEnd(); + let opens = 0, + loads = 0; + const decoders = createDecoderRegistry(); + decoders.register("test.xyz.v1", (bytes, header) => ({ + value: { bytes, meta: header.meta }, + })); + const pool = new MediaPool( + async () => { + loads++; + return { + fetchInfo: async () => INFO, + createWebTransport: () => relay.wt, + }; + }, + { decoders }, + (options, deps) => { + opens++; + return connect(options, deps); + }, + ); + assert.equal(loads, 0); + const first = await pool.acquire( + { robot: ROBOT_A.id }, + "cloud", + (slot) => slot, + () => {}, + ); + const second = await pool.acquire( + { robot: ROBOT_A.id }, + "cloud", + (slot) => slot, + () => {}, + ); + try { + assert.equal(opens, 1); + assert.equal(loads, 1); + relay.push({ t: "welcome", v: INFO.v }); + relay.push({ t: "robots", robots: [ROBOT_A] }); + relay.pushManifest( + ROBOT_A.id, + manifest([spec({ ch: "cloud", encoding: "test.xyz.v1" })]), + ); + await until(() => relay.subs().length === 1, "one wire subscription"); + const bytes = new Uint8Array(new Float32Array([1, 2, 3, -1, 0, 4]).buffer); + relay.pushRaw(1, bytes, "cloud", { + observation: "actual-selection", + frame: "map", + }); + await until(() => first.current() !== undefined, "decoded cloud"); + assert.deepEqual(first.current()?.value, { + bytes, + meta: { observation: "actual-selection", frame: "map" }, + }); + first.close(); + first.close(); + assert(second.current()); + second.close(); + assert.equal(opens, 1); + } finally { + first.close(); + second.close(); + pool.close(); + } +}); + +test("closing a pool during lazy initialization cannot create a leaked connection", async () => { + let complete!: (value: {}) => void; + const deps = new Promise<{}>((resolve) => { + complete = resolve; + }); + let opens = 0; + const pool = new MediaPool( + () => deps, + {}, + (options, transport) => { + opens++; + return connect(options, transport); + }, + ); + const pending = pool.acquire( + {}, + "image", + (slot) => slot, + () => {}, + ); + pool.close(); + complete({}); + await assert.rejects(pending, /closed/); + assert.equal(opens, 0); +}); diff --git a/agent/test/memory.test.ts b/agent/test/memory.test.ts new file mode 100644 index 00000000000..5a3b8d0c0c7 --- /dev/null +++ b/agent/test/memory.test.ts @@ -0,0 +1,155 @@ +import assert from "node:assert/strict"; +import { execFile } from "node:child_process"; +import { createHash } from "node:crypto"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join, resolve } from "node:path"; +import { test } from "node:test"; +import { promisify } from "node:util"; +import { setCapabilities, visibleWidth } from "@earendil-works/pi-tui"; +import sharp from "sharp"; +import { z } from "zod"; +import { paths } from "../src/config.js"; +import { renderFiles } from "../src/render.js"; +import { ResultImages } from "../src/tool-images.js"; + +test("memory tool keeps all SVG/PNG exports, provenance and selectable panels", async (t) => { + const dir = await mkdtemp(join(tmpdir(), "dimcode-memory-")); + t.after(() => rm(dir, { recursive: true, force: true })); + const p = paths({ XDG_CACHE_HOME: dir }); + const views = [ + { label: "Timeline", path: "timeline.svg" }, + { label: "Spatial", path: "space.svg" }, + { label: "Frames", path: "grid.png" }, + ]; + // Unit fixtures stay independent of the documentation's Git LFS assets. + const cwd = dir; + for (const [name, shape] of [ + ["timeline.svg", ''], + ["space.svg", ''], + ]) + await writeFile( + join(dir, name), + `${shape}`, + ); + await sharp({ + create: { width: 300, height: 200, channels: 3, background: "#8b7c35" }, + }) + .png() + .toFile(join(dir, "grid.png")); + const result = await renderFiles(p, cwd, views, "Memory · format fixture"); + const images = result.content.filter((item) => item.type === "image"); + assert.equal(images.length, 3, "every view reaches model context"); + assert.deepEqual( + result.details.views?.map((view) => view.label), + views.map((view) => view.label), + ); + for (const [i, view] of views.entries()) { + const source = resolve(cwd, view.path); + const bytes = await readFile(source); + assert.equal( + result.details.views?.[i].sha256, + createHash("sha256").update(bytes).digest("hex"), + ); + const metadata = await sharp( + Buffer.from(images[i].data, "base64"), + ).metadata(); + assert.equal(metadata.format, "png"); + assert(metadata.width > 100 && metadata.width <= 1600); + assert( + result.content.some( + (item) => item.type === "text" && item.text.includes(source), + ), + ); + } + setCapabilities({ images: null, trueColor: true, hyperlinks: false }); + let updated!: () => void; + const ready = new Promise((resolve) => { + updated = resolve; + }); + const gallery = new ResultImages( + images.map((image, i) => ({ ...image, label: views[i].label })), + updated, + ); + t.after(() => gallery.close()); + await ready; + const overview = gallery.render(100).join("\n"); + assert.match(overview, /Overview/); + assert.match(overview, /3 Frames/); + assert.doesNotMatch(overview, /Preparing|unavailable/); + gallery.select(3); + assert.notEqual(gallery.render(100).join("\n"), overview); + assert( + gallery.handleMouse({ + type: "press", + button: "left", + x: 2, + y: 0, + screenX: 2, + screenY: 0, + width: 100, + height: 30, + shift: false, + alt: false, + ctrl: false, + })?.handled, + ); + assert.equal(gallery.render(100).join("\n"), overview); + for (const width of [20, 40, 100]) + assert(gallery.render(width).every((line) => visibleWidth(line) <= width)); + await assert.rejects(renderFiles(p, cwd, []), /Choose/); + await assert.rejects( + renderFiles(p, cwd, [{ label: "Missing", path: "missing.png" }]), + /ENOENT/, + ); +}); + +test( + "recorded Go2 memory query exports its timeline, spatial selection and matching frames", + { + timeout: 90000, + skip: !(process.env.DIMCODE_TEST_PYTHON && process.env.DIMCODE_TEST_GO2_DB), + }, + async (t) => { + const dir = await mkdtemp(join(tmpdir(), "dimcode-memory-go2-")); + t.after(() => rm(dir, { recursive: true, force: true })); + await promisify(execFile)( + process.env.DIMCODE_TEST_PYTHON!, + ["test/fixtures/memory_views.py", dir], + { + env: { ...process.env, PYTHONPATH: resolve("..") }, + timeout: 80000, + }, + ); + const report = z + .object({ + observations: z.number().gt(10), + selected: z + .array( + z.object({ + timestamp: z.number().positive(), + similarity: z.number().finite(), + }), + ) + .min(1), + context_cloud_timestamps: z.array(z.number().positive()).min(1), + }) + .parse(JSON.parse(await readFile(join(dir, "query.json"), "utf8"))); + assert(report.selected.every((row) => row.similarity > 0)); + const result = await renderFiles( + paths({ XDG_CACHE_HOME: dir }), + dir, + [ + { label: "Timeline", path: "timeline.svg" }, + { label: "Spatial", path: "space.svg" }, + { label: "Frames", path: "frames.png" }, + ], + "Memory · plant", + ); + assert.equal( + result.content.filter((item) => item.type === "image").length, + 3, + ); + assert.equal(result.details.views?.length, 3); + }, +); diff --git a/agent/test/render.test.ts b/agent/test/render.test.ts new file mode 100644 index 00000000000..16476552268 --- /dev/null +++ b/agent/test/render.test.ts @@ -0,0 +1,35 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import sharp from "sharp"; +import { cloudSchema, paintCloud } from "../src/points.js"; +import { ChatInput } from "../src/input.js"; + +test("point presentation validates geometry without mutating source", async () => { + const input = cloudSchema.parse({ + points: [ + [0, 0, 0], + [1, 0, 2], + [1, 2, 3], + ], + frame: "map", + }); + const before = JSON.stringify(input); + const png = await paintCloud(input); + assert.equal(JSON.stringify(input), before); + assert.equal((await sharp(png).metadata()).width, 900); + assert.throws(() => cloudSchema.parse({ points: [[NaN, 1, 2]] })); + assert.throws(() => + cloudSchema.parse({ points: [[0, 1, 2]], selectedIndices: [1] }), + ); + assert.throws(() => cloudSchema.parse({ points: [[0, 1, 2]], colors: [] })); +}); +test("credentials cannot return through input undo after auth", () => { + const input = new ChatInput(); + input.secret = true; + input.handleInput("fixture-secret"); + assert(!input.render(80).join("").includes("fixture-secret")); + input.secret = false; + input.handleInput("\x1f"); + input.handleInput("\x19"); + assert.equal(input.getValue(), ""); +}); diff --git a/agent/test/run.mjs b/agent/test/run.mjs new file mode 100644 index 00000000000..0b0e2a6f30f --- /dev/null +++ b/agent/test/run.mjs @@ -0,0 +1,33 @@ +import { build } from "esbuild"; +import { readdir } from "node:fs/promises"; +import { resolve } from "node:path"; +import { spawn } from "node:child_process"; +const tests = (await readdir("test")).filter((name) => + name.endsWith(".test.ts"), +); +await build({ + entryPoints: [...tests.map((name) => "test/" + name), "src/main.ts"], + outbase: ".", + outdir: ".test", + bundle: true, + format: "esm", + platform: "node", + target: "node24", + packages: "external", + alias: { + "@dimos/sdk": resolve("../web/sdk/src/index.ts"), + "@dimos/shared/manifest": resolve("../web/shared/manifest.ts"), + "@dimos/shared": resolve("../web/shared/protocol.ts"), + }, +}); +const child = spawn( + process.execPath, + [ + "--test", + ...tests.map((name) => ".test/test/" + name.replace(/\.ts$/, ".js")), + ], + { stdio: "inherit" }, +); +child.on("exit", (code) => { + process.exitCode = code ?? 1; +}); diff --git a/agent/test/scroll-images.test.ts b/agent/test/scroll-images.test.ts new file mode 100644 index 00000000000..30596b3deda --- /dev/null +++ b/agent/test/scroll-images.test.ts @@ -0,0 +1,96 @@ +import assert from "node:assert/strict"; +import { test } from "node:test"; +import { setTimeout as delay } from "node:timers/promises"; +import { + ScrollView, + setCapabilities, + setCellDimensions, +} from "@earendil-works/pi-tui"; +import { renderLayoutFrame } from "@earendil-works/pi-tui/dist/layout.js"; +import sharp from "sharp"; +import { TerminalImage } from "../src/terminal-image.js"; + +// Decode the actual PNG transmissions as a terminal that ignores source-crop controls would. +function transmittedPng(line: string): Buffer { + const commands = [...line.matchAll(/\x1b_G([^;]*);([^\x1b]*)\x1b\\/g)]; + assert(commands.length); + assert.match(commands[0][1], /(?:^|,)r=1(?:,|$)/); + assert.doesNotMatch(commands[0][1], /(?:^|,)[yhw]=/); + return Buffer.from(commands.map((command) => command[2]).join(""), "base64"); +} + +test("Warp scrolling clips exact pixel rows instead of squeezing the original image", async (t) => { + const previous = process.env.TERM_PROGRAM; + process.env.TERM_PROGRAM = "WarpTerminal"; + t.after(() => { + if (previous === undefined) delete process.env.TERM_PROGRAM; + else process.env.TERM_PROGRAM = previous; + }); + setCapabilities({ images: "kitty", trueColor: true, hyperlinks: false }); + setCellDimensions({ widthPx: 10, heightPx: 20 }); + const png = await sharp( + Buffer.from( + '', + ), + ) + .removeAlpha() + .png() + .toBuffer(); + let updates = 0; + const image = new TerminalImage( + png.toString("base64"), + "image/png", + { fallbackColor: (text) => text }, + { maxWidthCells: 90, maxHeightCells: 25 }, + () => updates++, + ); + t.after(() => image.close()); + const ready = async (width: number) => { + const before = updates; + image.render(width); + const deadline = Date.now() + 3000; + while (updates === before && Date.now() < deadline) await delay(10); + assert(updates > before, "image preparation redraws the terminal"); + }; + await ready(92); + const scroll = new ScrollView(image, { follow: "none", scrollbar: "hidden" }); + renderLayoutFrame(scroll, 92, 7, () => {}); + for (const row of [0, 4, 11, 18, 0]) { + scroll.scrollTo(row); + const frame = renderLayoutFrame(scroll, 92, 7, () => {}); + assert.equal(frame.lines.length, 7); + const visible = Buffer.concat( + await Promise.all( + frame.lines.map(async (line) => { + const bytes = transmittedPng(line); + const metadata = await sharp(bytes).metadata(); + assert.equal(metadata.width, 900); + assert.equal(metadata.height, 20); + return sharp(bytes).raw().toBuffer(); + }), + ), + ); + const expected = await sharp(png) + .extract({ left: 0, top: row * 20, width: 900, height: 140 }) + .raw() + .toBuffer(); + assert.deepEqual( + visible, + expected, + "scrolling changes only which source rows are visible", + ); + } + await ready(47); + const narrow = image.render(47); + assert.equal(narrow.length, 13); + assert.equal((await sharp(transmittedPng(narrow[0])).metadata()).width, 450); + image.render(70); + image.close(); + const closed = updates; + await delay(50); + assert.equal( + updates, + closed, + "resizing then closing cannot redraw a disposed view", + ); +}); diff --git a/agent/test/setup.test.ts b/agent/test/setup.test.ts new file mode 100644 index 00000000000..52a18041af0 --- /dev/null +++ b/agent/test/setup.test.ts @@ -0,0 +1,94 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm, stat } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { stripVTControlCharacters } from "node:util"; +import { initTheme, SettingsManager } from "@earendil-works/pi-coding-agent"; +import { loadConfig, paths } from "../src/config.js"; +import { needsSetup, setup } from "../src/setup.js"; +import { ChatInput } from "../src/input.js"; + +for (const cancel of [false, true]) + test( + "onboarding " + + (cancel + ? "can cancel and resume" + : "saves provider, model and workspace with private credentials"), + async (t) => { + const home = await mkdtemp(join(tmpdir(), "dimcode-setup-")); + t.after(() => rm(home, { recursive: true, force: true })); + const p = paths({ DIMCODE_HOME: home }); + assert(await needsSetup(p)); + let asked = 0; + const configure = setup( + p, + { workspace: home }, + { + prompt: async (question) => { + asked++; + assert( + !/environment directory|MCP URL|Workspace directory/.test( + question.message, + ), + ); + if (cancel && asked === 2) throw new Error("Cancelled"); + if (question.type === "select") { + if (question.options.some((item) => item.id === "openai")) + return "openai"; + if (question.options.some((item) => item.id === "api_key")) + return "api_key"; + if (question.options.some((item) => item.id === "gpt-5.6-luna")) + return "gpt-5.6-luna"; + return question.options[0].id; + } + if (question.type === "secret") return "onboarding-fixture-key"; + return question.message.includes("Workspace") ? home : ""; + }, + notify: () => {}, + }, + ); + if (cancel) { + await assert.rejects(configure, /Cancelled/); + assert(await needsSetup(p)); + return; + } + assert.equal( + await configure, + true, + "interactive bootstrap hands off to the agent", + ); + await assert.rejects(stat(join(home, ".venv")), { code: "ENOENT" }); + assert(!(await needsSetup(p))); + assert.equal((await loadConfig(p)).workspace, home); + assert.equal( + SettingsManager.create(home, p.config).getDefaultProvider(), + "openai", + ); + assert.equal( + SettingsManager.create(home, p.config).getDefaultModel(), + "gpt-5.6-luna", + ); + assert.equal((await stat(join(home, "auth.json"))).mode & 0o777, 0o600); + assert( + !(await readFile(join(home, "config.json"), "utf8")).includes( + "fixture-key", + ), + ); + }, + ); + +test("setup fields use their own placeholder and discard credential history", () => { + initTheme("dark", false); + const input = new ChatInput("Drop a directory path…"); + const visible = () => stripVTControlCharacters(input.render(80).join("")); + assert.match(visible(), /Drop a directory path/); + assert(!input.render(80).join("").includes("Ask about")); + input.secret = true; + input.handleInput("fixture-secret"); + assert(!input.render(80).join("").includes("fixture-secret")); + input.secret = false; + input.handleInput("\x1f"); + assert.equal(input.getValue(), ""); + assert.match(visible(), /Drop a directory path/); +}); diff --git a/agent/test/terminal.test.ts b/agent/test/terminal.test.ts new file mode 100644 index 00000000000..78628776088 --- /dev/null +++ b/agent/test/terminal.test.ts @@ -0,0 +1,76 @@ +import assert from "node:assert/strict"; +import { mkdtemp, readFile, rm, writeFile } from "node:fs/promises"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; +import { test } from "node:test"; +import { setTimeout as delay } from "node:timers/promises"; +import { + ProcessTerminal, + TuiMainScreen, + setCapabilities, + visibleWidth, +} from "@earendil-works/pi-tui"; +import { initTheme } from "@earendil-works/pi-coding-agent"; +import { PromptEditor } from "../src/input.js"; +import { readCloud, SpatialView } from "../src/spatial.js"; + +test("Pi editor preserves multiline submissions and never puts credentials into chat history", () => { + initTheme("dark", false); + const editor = new PromptEditor(new TuiMainScreen(new ProcessTerminal())); + let submitted = ""; + editor.onSubmit = (value) => { + submitted = value; + }; + editor.handleInput("\x1b[200~line one\nline two\x1b[201~"); + editor.handleInput("\r"); + assert.equal(submitted, "line one\nline two"); + editor.setValue(""); + editor.secret = true; + editor.setValue(""); + editor.handleInput("private-fixture"); + assert(!editor.render(50).join("").includes("private-fixture")); + editor.secret = false; + editor.setValue(""); + for (const key of ["\x1f", "\x19", "\x1b[A"]) editor.handleInput(key); + assert(!editor.getValue().includes("private-fixture")); +}); + +test("spatial inspector rotates exact data, preserves source selections and rejects changed provenance", async (t) => { + const home = await mkdtemp(join(tmpdir(), "dimcode-spatial-")); + t.after(() => rm(home, { recursive: true, force: true })); + const source = join(home, "cloud.json"); + const data = { + points: [ + [1, 4, -0.1], + [2, 6, 0.4], + [-1, 8, 0.6], + ], + frame: "map", + timestamp: 123.45, + selectedIndices: [1], + }; + await writeFile(source, JSON.stringify(data)); + const { cloud, sha256 } = await readCloud(source); + setCapabilities({ images: null, trueColor: true, hyperlinks: false }); + let renders = 0; + const view = new SpatialView(cloud, source, sha256, () => renders++); + t.after(() => view.close()); + const before = view.render(80).join("\n"); + assert.match(before, /1 source-selected points/); + const start = renders; + view.handleInput("r"); + const deadline = Date.now() + 2000; + while (renders === start && Date.now() < deadline) await delay(20); + assert(renders > start, "rotation redraws the observation"); + assert.notEqual(view.render(80).join("\n"), before); + for (const width of [25, 40, 100]) + assert(view.render(width).every((line) => visibleWidth(line) <= width)); + assert.deepEqual(cloud, data); + assert.equal(await readFile(source, "utf8"), JSON.stringify(data)); + await writeFile(source, JSON.stringify({ ...data, timestamp: 124 })); + await assert.rejects(readCloud(source, sha256), /Source changed/); + view.close(); + const closed = renders; + await delay(80); + assert.equal(renders, closed, "no redraws after disposal"); +}); diff --git a/agent/tsconfig.json b/agent/tsconfig.json new file mode 100644 index 00000000000..e2a71fb44b3 --- /dev/null +++ b/agent/tsconfig.json @@ -0,0 +1,34 @@ +{ + "compilerOptions": { + "target": "ES2023", + "module": "ESNext", + "moduleResolution": "Bundler", + "strict": true, + "noUnusedLocals": true, + "noUnusedParameters": true, + "skipLibCheck": true, + "allowImportingTsExtensions": true, + "noEmit": true, + "types": [ + "node" + ], + "paths": { + "@dimos/sdk": [ + "../web/sdk/src/index.ts" + ], + "@dimos/shared": [ + "../web/shared/protocol.ts" + ], + "@dimos/shared/manifest": [ + "../web/shared/manifest.ts" + ] + }, + "customConditions": [ + "node" + ] + }, + "include": [ + "src/**/*.ts", + "test/**/*.ts" + ] +} diff --git a/dimos/agents/mcp/mcp_server.py b/dimos/agents/mcp/mcp_server.py index 61d7572af8c..cb9b3a4dc7f 100644 --- a/dimos/agents/mcp/mcp_server.py +++ b/dimos/agents/mcp/mcp_server.py @@ -32,6 +32,7 @@ from dimos.agents.annotation import skill from dimos.agents.capabilities import CapabilityRegistry from dimos.agents.mcp import tool_stream +from dimos.agents.skill_result import SkillResult from dimos.core.core import rpc from dimos.core.module import Module from dimos.core.rpc_client import RpcCall, RPCClient @@ -72,8 +73,26 @@ def _jsonrpc_result(req_id: Any, result: Any) -> dict[str, Any]: return {"jsonrpc": "2.0", "id": req_id, "result": result} -def _jsonrpc_result_text(req_id: Any, text: str) -> dict[str, Any]: - return _jsonrpc_result(req_id, {"content": [{"type": "text", "text": text}]}) +def _jsonrpc_result_text(req_id: Any, text: str, *, is_error: bool = False) -> dict[str, Any]: + return _jsonrpc_result( + req_id, {"content": [{"type": "text", "text": text}], "isError": is_error} + ) + + +def _mcp_content(blocks: list[dict[str, Any]]) -> list[dict[str, Any]]: + """Translate the existing agent image encoding at the MCP boundary.""" + content = [] + for block in blocks: + if block.get("type") == "image_url": + url = block["image_url"]["url"] + if not url.startswith("data:image/") or ";base64," not in url: + content.append({"type": "resource_link", "name": "image", "uri": url}) + continue + header, data = url.split(";base64,", 1) + content.append({"type": "image", "mimeType": header[5:], "data": data}) + else: + content.append(block) + return content def _jsonrpc_error(req_id: Any, code: int, message: str) -> dict[str, Any]: @@ -122,7 +141,7 @@ async def _handle_tools_call( rpc_call = rpc_calls.get(name) if rpc_call is None: logger.warning("MCP tool not found", tool=name) - return _jsonrpc_result_text(req_id, f"Tool not found: {name}") + return _jsonrpc_result_text(req_id, f"Tool not found: {name}", is_error=True) skill_info = app.state.skills_by_name.get(name) uses: list[str] = list(skill_info.uses) if skill_info is not None else [] @@ -173,6 +192,7 @@ def _can_wait(holder: str) -> bool: return _jsonrpc_result_text( req_id, f"Cannot start '{name}': capability '{cap}' is held by '{holder}'. {advice}", + is_error=True, ) logger.info("MCP tool call", tool=name, args=args, progress_token=progress_token) @@ -201,7 +221,7 @@ def _can_wait(holder: str) -> bool: ) except Exception as e: logger.exception("MCP tool error", tool=name, duration=f"{time.monotonic() - t0:.3f}s") - return _jsonrpc_result_text(req_id, f"Error running tool '{name}': {e}") + return _jsonrpc_result_text(req_id, f"Error running tool '{name}': {e}", is_error=True) if lifecycle == "background": # Hand ownership of the caps off to the tool-stream lifecycle. @@ -215,7 +235,13 @@ def _can_wait(holder: str) -> bool: if hasattr(result, "agent_encode"): logger.info("MCP tool done", tool=name, duration=duration, response=response) - return _jsonrpc_result(req_id, {"content": result.agent_encode()}) + return _jsonrpc_result( + req_id, + { + "content": _mcp_content(result.agent_encode()), + "isError": isinstance(result, SkillResult) and not result.success, + }, + ) logger.info("MCP tool done", tool=name, duration=duration, response=response) return _jsonrpc_result_text(req_id, str(result)) diff --git a/dimos/agents/mcp/test_mcp_server.py b/dimos/agents/mcp/test_mcp_server.py index 0e2a74925b9..024ed70a641 100644 --- a/dimos/agents/mcp/test_mcp_server.py +++ b/dimos/agents/mcp/test_mcp_server.py @@ -20,7 +20,8 @@ from unittest.mock import MagicMock from dimos.agents.capabilities import CapabilityRegistry -from dimos.agents.mcp.mcp_server import app, handle_request +from dimos.agents.mcp.mcp_server import _mcp_content, app, handle_request +from dimos.agents.skill_result import SkillResult from dimos.core.module import SkillInfo @@ -334,3 +335,18 @@ def test_instant_holder_conflict_waits_then_runs() -> None: app.state.skills_by_name = saved_skills app.state.cap_registry = saved_registry app.state.cap_acquire_timeout = saved_timeout + + +def test_mcp_image_and_structured_failure() -> None: + encoded = [{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,YWJj"}}] + assert _mcp_content(encoded) == [{"type": "image", "mimeType": "image/jpeg", "data": "YWJj"}] + result = asyncio.run( + handle_request( + {"id": 5, "method": "tools/call", "params": {"name": "fail", "arguments": {}}}, + [], + {"fail": lambda: SkillResult.fail("EXPECTED", "fixture")}, + ) + ) + assert result is not None + assert result["result"]["isError"] is True + assert "EXPECTED" in result["result"]["content"][0]["text"] diff --git a/dimos/cli/commands/lifecycle.py b/dimos/cli/commands/lifecycle.py index 63abf44a09b..86b44098ff5 100644 --- a/dimos/cli/commands/lifecycle.py +++ b/dimos/cli/commands/lifecycle.py @@ -37,7 +37,7 @@ write_daemon_status, ) from dimos.core.global_config import global_config -from dimos.core.run_registry import get_most_recent, is_pid_alive, stop_entry +from dimos.core.run_registry import get_most_recent, get_run, is_pid_alive, stop_entry from dimos.utils.cache import cache_usage_guard, cache_usage_locked from dimos.utils.logging_config import setup_logger @@ -286,7 +286,7 @@ def run( log_dir=str(log_dir), cli_args=list(blueprint_names), config_overrides=global_option_overrides, - original_argv=sys.argv, + original_argv=[sys.executable, *sys.orig_argv[1:]], ) entry.save() spawn_watchdog(run_id, log_dir=log_dir) @@ -324,7 +324,7 @@ def run( log_dir=str(log_dir), cli_args=list(blueprint_names), config_overrides=global_option_overrides, - original_argv=sys.argv, + original_argv=[sys.executable, *sys.orig_argv[1:]], ) entry.save() spawn_watchdog(run_id, log_dir=log_dir) @@ -342,9 +342,9 @@ def run( entry.remove() -def status() -> None: +def status(run_id: str = typer.Option("", "--run", "-r", help="Exact run ID")) -> None: """Show the running DimOS instance.""" - entry = get_most_recent(alive_only=True) + entry = get_run(run_id) if run_id else get_most_recent(alive_only=True) if not entry: typer.echo("No running DimOS instance") return @@ -367,18 +367,21 @@ def status() -> None: def stop( force: bool = typer.Option(False, "--force", "-f", help="Force kill (SIGKILL)"), + run_id: str = typer.Option("", "--run", "-r", help="Exact run ID"), ) -> None: """Stop the running DimOS instance.""" - entry = get_most_recent(alive_only=True) + entry = get_run(run_id) if run_id else get_most_recent(alive_only=True) if not entry: typer.echo("No running DimOS instance", err=True) raise typer.Exit(1) sig_name = "SIGKILL" if force else "SIGTERM" typer.echo(f"Stopping {entry.run_id} (PID {entry.pid}) with {sig_name}...") - msg, _ok = stop_entry(entry, force=force) + msg, ok = stop_entry(entry, force=force) typer.echo(f" {msg}") + if not ok: + raise typer.Exit(1) def log_cmd( @@ -417,9 +420,10 @@ def _on_sigint(_sig: int, _frame: object) -> None: def restart( force: bool = typer.Option(False, "--force", "-f", help="Force kill before restarting"), + run_id: str = typer.Option("", "--run", "-r", help="Exact run ID"), ) -> None: """Restart the running DimOS instance with the same arguments.""" - entry = get_most_recent(alive_only=True) + entry = get_run(run_id) if run_id else get_most_recent(alive_only=True) if not entry: typer.echo("No running DimOS instance to restart", err=True) raise typer.Exit(1) @@ -433,8 +437,10 @@ def restart( old_pid = entry.pid typer.echo(f"Restarting {entry.run_id} ({entry.blueprint})...") - msg, _ok = stop_entry(entry, force=force) + msg, ok = stop_entry(entry, force=force) typer.echo(f" {msg}") + if not ok: + raise typer.Exit(1) # Wait for the old process to fully exit so ports are released. for _ in range(20): # up to 2s diff --git a/dimos/core/run_registry.py b/dimos/core/run_registry.py index 119ac4b1fd6..6491190911b 100644 --- a/dimos/core/run_registry.py +++ b/dimos/core/run_registry.py @@ -148,6 +148,13 @@ def get_most_recent(alive_only: bool = True) -> RunEntry | None: return runs[-1] if runs else None +def get_run(run_id: str, alive_only: bool = True) -> RunEntry | None: + """Select an exact run ID; never fall back to another running instance.""" + return next( + (entry for entry in list_runs(alive_only=alive_only) if entry.run_id == run_id), None + ) + + def stop_entry(entry: RunEntry, force: bool = False) -> tuple[str, bool]: """Stop a DimOS instance by registry entry. diff --git a/dimos/core/test_daemon.py b/dimos/core/test_daemon.py index d95f6a00a0b..8fefabd8d2b 100644 --- a/dimos/core/test_daemon.py +++ b/dimos/core/test_daemon.py @@ -23,7 +23,10 @@ from unittest import mock import pytest +import typer +from typer.testing import CliRunner +from dimos.cli.commands import lifecycle from dimos.core import run_registry from dimos.core.run_registry import ( RunEntry, @@ -489,3 +492,36 @@ def test_get_most_recent_returns_latest(self, tmp_path, monkeypatch): latest = get_most_recent(alive_only=True) assert latest is not None assert latest.run_id == "20260306-110000-second" + + +@pytest.mark.parametrize("command", ["status", "stop", "restart"]) +def test_lifecycle_selects_exact_run( + command: str, tmp_path: Path, monkeypatch: pytest.MonkeyPatch +) -> None: + monkeypatch.setattr(run_registry, "REGISTRY_DIR", tmp_path / "runs") + monkeypatch.setattr(run_registry, "is_pid_alive", lambda pid: True) + first = _make_entry("first", 17321) + second = _make_entry("second", 28543) + first.original_argv = ["dimos", "run", "first", "--daemon"] + first.save() + second.save() + stopped: list[str] = [] + monkeypatch.setattr( + lifecycle, + "stop_entry", + lambda entry, force=False: (stopped.append(entry.run_id) or "stopped", True), + ) + monkeypatch.setattr(lifecycle, "is_pid_alive", lambda pid: False) + monkeypatch.setattr(lifecycle.os, "execvp", lambda file, args: None) + app = typer.Typer() + app.command()(getattr(lifecycle, command)) + result = CliRunner().invoke(app, ["--run", "first"]) + assert result.exit_code == 0, result.output + if command == "status": + assert "17321" in result.output and "28543" not in result.output + else: + assert stopped == ["first"] + stopped.clear() + missing = CliRunner().invoke(app, ["--run", "missing"]) + assert not stopped + assert "28543" not in missing.output diff --git a/pyproject.toml b/pyproject.toml index bc3dfd27b9b..5360f0773bb 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -758,6 +758,7 @@ ignore = [ "uv.lock", "dimos/experimental/isolated_python/example/python/uv.lock", "*/package-lock.json", + "agent/npm-shrinkwrap.json", "Cargo.lock", "*/Cargo.lock", "dimos/web/dimos_interface/themes.json",