From d284de28c7ad034380ab88766a2dc74349d5813f Mon Sep 17 00:00:00 2001 From: Ben <62250174+benfrank241@users.noreply.github.com> Date: Fri, 20 Mar 2026 10:16:55 -0400 Subject: [PATCH] feat(nemoclaw): add hindsight-nemoclaw setup CLI package (#630) * feat(nemoclaw): add hindsight-nemoclaw setup CLI package Automates the full NemoClaw sandbox setup: - Installs @vectorize-io/hindsight-openclaw plugin - Configures external API mode in ~/.openclaw/openclaw.json - Reads current openshell sandbox policy, merges Hindsight egress rule, re-applies - Restarts the OpenClaw gateway Options: --dry-run, --skip-policy, --skip-plugin-install 36 unit tests passing * docs: add NEMOCLAW.md setup guide * feat(nemoclaw): add README, docs page, and release pipeline * revert: remove release.yml changes from nemoclaw PR --- .../docs/sdks/integrations/nemoclaw.md | 246 +++ hindsight-integrations/nemoclaw/NEMOCLAW.md | 196 +++ hindsight-integrations/nemoclaw/README.md | 60 + .../nemoclaw/package-lock.json | 1318 +++++++++++++++++ hindsight-integrations/nemoclaw/package.json | 51 + hindsight-integrations/nemoclaw/src/cli.ts | 84 ++ .../nemoclaw/src/openclaw-config.test.ts | 101 ++ .../nemoclaw/src/openclaw-config.ts | 106 ++ .../nemoclaw/src/policy-reader.test.ts | 102 ++ .../nemoclaw/src/policy-reader.ts | 88 ++ .../nemoclaw/src/policy-writer.test.ts | 107 ++ .../nemoclaw/src/policy-writer.ts | 59 + .../nemoclaw/src/setup.test.ts | 173 +++ hindsight-integrations/nemoclaw/src/setup.ts | 148 ++ hindsight-integrations/nemoclaw/src/types.ts | 63 + hindsight-integrations/nemoclaw/tsconfig.json | 18 + .../nemoclaw/vitest.config.ts | 9 + 17 files changed, 2929 insertions(+) create mode 100644 hindsight-docs/docs/sdks/integrations/nemoclaw.md create mode 100644 hindsight-integrations/nemoclaw/NEMOCLAW.md create mode 100644 hindsight-integrations/nemoclaw/README.md create mode 100644 hindsight-integrations/nemoclaw/package-lock.json create mode 100644 hindsight-integrations/nemoclaw/package.json create mode 100644 hindsight-integrations/nemoclaw/src/cli.ts create mode 100644 hindsight-integrations/nemoclaw/src/openclaw-config.test.ts create mode 100644 hindsight-integrations/nemoclaw/src/openclaw-config.ts create mode 100644 hindsight-integrations/nemoclaw/src/policy-reader.test.ts create mode 100644 hindsight-integrations/nemoclaw/src/policy-reader.ts create mode 100644 hindsight-integrations/nemoclaw/src/policy-writer.test.ts create mode 100644 hindsight-integrations/nemoclaw/src/policy-writer.ts create mode 100644 hindsight-integrations/nemoclaw/src/setup.test.ts create mode 100644 hindsight-integrations/nemoclaw/src/setup.ts create mode 100644 hindsight-integrations/nemoclaw/src/types.ts create mode 100644 hindsight-integrations/nemoclaw/tsconfig.json create mode 100644 hindsight-integrations/nemoclaw/vitest.config.ts diff --git a/hindsight-docs/docs/sdks/integrations/nemoclaw.md b/hindsight-docs/docs/sdks/integrations/nemoclaw.md new file mode 100644 index 00000000..a56e132a --- /dev/null +++ b/hindsight-docs/docs/sdks/integrations/nemoclaw.md @@ -0,0 +1,246 @@ +--- +sidebar_position: 5 +--- + +# NemoClaw + +Persistent memory for [NemoClaw](https://nemoclaw.ai) sandboxed agents using [Hindsight](https://hindsight.vectorize.io). + +NemoClaw runs [OpenClaw](https://openclaw.ai) inside an OpenShell sandbox with controlled filesystem, process, and network egress policies. The `hindsight-nemoclaw` package automates adding Hindsight memory to a sandbox in one command — no code changes required. + +## Quick Start + +```bash +npx @vectorize-io/hindsight-nemoclaw setup \ + --sandbox my-assistant \ + --api-url https://api.hindsight.vectorize.io \ + --api-token \ + --bank-prefix my-sandbox +``` + +Get an API key at [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup). + +You'll see output like: + +``` +[0] Preflight checks... + ✓ openshell found + ✓ openclaw found + +[1] Installing @vectorize-io/hindsight-openclaw plugin... + ✓ Plugin installed + +[2] Configuring plugin in ~/.openclaw/openclaw.json... + ✓ Plugin config written (bank: my-sandbox-openclaw) + +[3] Applying Hindsight network policy to sandbox "my-assistant"... + ✓ Policy version 2 submitted + ✓ Policy version 2 loaded (active version: 2) + +[4] Restarting OpenClaw gateway... + ✓ Gateway restarted + +✓ Setup complete! +``` + +## How It Works + +### The sandbox problem + +OpenShell enforces strict network egress — every outbound endpoint must be explicitly permitted in the sandbox policy. By default, the Hindsight API (`api.hindsight.vectorize.io`) is not in that list. + +The `hindsight-openclaw` plugin supports **external API mode**, where it skips the local daemon entirely and makes direct HTTPS calls to Hindsight Cloud. This is the natural fit for sandboxed environments: the plugin becomes a thin HTTP client, and the only sandbox change needed is one egress rule. + +### What the setup command does + +1. **Preflight** — verifies `openshell` and `openclaw` are installed +2. **Install plugin** — runs `openclaw plugins install @vectorize-io/hindsight-openclaw` +3. **Configure plugin** — writes external API mode config to `~/.openclaw/openclaw.json` +4. **Apply policy** — reads the current sandbox policy, merges the Hindsight egress block, and re-applies via `openshell policy set` +5. **Restart gateway** — runs `openclaw gateway restart` + +### Memory flow + +Once set up, the `hindsight-openclaw` plugin hooks into the OpenClaw gateway lifecycle: + +- **`before_agent_start`** — recalls relevant memories from past sessions and injects them into context +- **`agent_end`** — retains the conversation to the Hindsight memory bank + +The sandbox doesn't interfere with either step — it sees the Hindsight calls as normal HTTPS egress to a permitted endpoint. + +## CLI Reference + +``` +hindsight-nemoclaw setup [options] + +Options: + --sandbox NemoClaw sandbox name (required) + --api-url Hindsight API URL (required) + --api-token Hindsight API token (required) + --bank-prefix Memory bank prefix (default: "nemoclaw") + --skip-policy Skip sandbox network policy update + --skip-plugin-install Skip openclaw plugin installation + --dry-run Preview changes without applying + --help Show help +``` + +Use `--dry-run` to preview all changes before applying anything. Use `--skip-policy` if you manage sandbox policies manually. + +## Manual Setup + +If you prefer to apply the steps yourself instead of using the CLI: + +### 1. Install the plugin + +```bash +openclaw plugins install @vectorize-io/hindsight-openclaw +``` + +### 2. Configure `~/.openclaw/openclaw.json` + +```json +{ + "plugins": { + "entries": { + "hindsight-openclaw": { + "enabled": true, + "config": { + "hindsightApiUrl": "https://api.hindsight.vectorize.io", + "hindsightApiToken": "", + "llmProvider": "claude-code", + "dynamicBankId": false, + "bankIdPrefix": "my-sandbox" + } + } + } + } +} +``` + +`llmProvider: "claude-code"` uses the Claude Code process already present in the sandbox — no additional API key needed. + +### 3. Add the Hindsight network policy + +`openshell policy set` replaces the entire policy document. Export your current policy first, add the Hindsight block, then re-apply: + +```yaml +network_policies: + hindsight: + name: hindsight + endpoints: + - host: api.hindsight.vectorize.io + port: 443 + protocol: rest + tls: terminate + enforcement: enforce + rules: + - allow: + method: GET + path: /** + - allow: + method: POST + path: /** + - allow: + method: PUT + path: /** + binaries: + - path: /usr/local/bin/openclaw +``` + +```bash +openshell policy set my-sandbox --policy /path/to/full-policy.yaml --wait +openclaw gateway restart +``` + +## Configuration Options + +| Option | Type | Default | Description | +|---|---|---|---| +| `hindsightApiUrl` | string | — | Hindsight API base URL | +| `hindsightApiToken` | string | — | API token for authentication | +| `llmProvider` | string | auto-detect | LLM provider for memory extraction | +| `dynamicBankId` | boolean | `false` | Isolate memory per user (`true`) or share across sessions (`false`) | +| `bankIdPrefix` | string | `"nemoclaw"` | Prefix for the memory bank name | + +### Bank naming + +When `dynamicBankId: false`, all sessions write to a single bank named `{bankIdPrefix}-openclaw`. When `dynamicBankId: true`, each user gets an isolated bank — useful for multi-tenant deployments. + +## Verifying It Works + +After setup, check the gateway logs: + +```bash +tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight +``` + +On startup you should see: + +``` +[Hindsight] Plugin loaded successfully +[Hindsight] ✓ Using external API: https://api.hindsight.vectorize.io +[Hindsight] External API health: {"status":"healthy","database":"connected"} +[Hindsight] Default bank: my-sandbox-openclaw +[Hindsight] ✓ Ready (external API mode) +``` + +After a conversation: + +``` +[Hindsight] before_agent_start - bank: my-sandbox-openclaw, channel: undefined/webchat +[Hindsight Hook] agent_end triggered - bank: my-sandbox-openclaw +[Hindsight] Retained 6 messages to bank my-sandbox-openclaw for session agent:main:... +``` + +## Pitfalls + +### Policy replacement is full-document + +`openshell policy set` replaces the entire policy document. The `hindsight-nemoclaw setup` command handles this automatically. If you're applying manually, export the current policy first so existing rules aren't lost. + +### LaunchAgent can't follow symlinks on macOS + +On macOS, the OpenClaw gateway runs as a LaunchAgent under a restricted security context. `openclaw plugins install --link` creates a symlink the LaunchAgent can't follow — the setup command installs as a copy instead. If you see `EPERM: operation not permitted, scandir` in gateway logs, this is the cause. + +### Memory retention is asynchronous + +Fact extraction and entity resolution happen in the background after `retain`. If you open a new session immediately after closing one, the most recent memories may not be indexed yet — typically a few seconds. + +### Binary-scoped egress + +The `binaries` field in the network policy restricts the egress rule to a specific executable path. If OpenClaw updates and the binary path changes, the rule silently stops working. Check your binary path after upgrades. + +## Troubleshooting + +### Plugin not loading + +```bash +openclaw plugins list | grep hindsight +# Should show: ✓ enabled │ Hindsight Memory │ ... + +# Reinstall +openclaw plugins install @vectorize-io/hindsight-openclaw +``` + +### Egress blocked + +If calls to `api.hindsight.vectorize.io` are being blocked, check the active sandbox policy: + +```bash +openshell sandbox get my-assistant +``` + +Verify the `hindsight` block is present and the `binaries` path matches your OpenClaw binary: + +```bash +which openclaw +``` + +### External API not connecting + +```bash +tail -f /tmp/openclaw/openclaw-*.log | grep Hindsight + +# If you see daemon startup messages instead of "Using external API", +# the plugin config isn't being read — check ~/.openclaw/openclaw.json +``` diff --git a/hindsight-integrations/nemoclaw/NEMOCLAW.md b/hindsight-integrations/nemoclaw/NEMOCLAW.md new file mode 100644 index 00000000..6989d3a4 --- /dev/null +++ b/hindsight-integrations/nemoclaw/NEMOCLAW.md @@ -0,0 +1,196 @@ +# Using hindsight-openclaw with NemoClaw + +This guide covers running the `hindsight-openclaw` plugin inside a [NemoClaw](https://nemoclaw.ai) sandbox. NemoClaw runs OpenClaw inside an OpenShell sandbox, so the plugin's outbound calls to `api.hindsight.vectorize.io` must be explicitly allowed in the sandbox's network egress policy. + +## Prerequisites + +- NemoClaw installed and a sandbox created (`nemoclaw onboard`) +- OpenClaw installed (`brew install openclaw` or equivalent) +- A Hindsight API key from [ui.hindsight.vectorize.io](https://ui.hindsight.vectorize.io) +- The plugin source built (`npm run build` in this directory) + +## Step 1: Create a Hindsight memory bank + +Create the bank the plugin will write to. The bank ID follows the pattern `{bankIdPrefix}-openclaw` when `dynamicBankId` is false: + +```bash +curl -X PUT "https://api.hindsight.vectorize.io/v1/default/banks/my-sandbox-openclaw" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"mission": "Memory bank for my NemoClaw sandbox."}' +``` + +## Step 2: Install the plugin + +Install the plugin as a copy (not a symlink) so the OpenClaw LaunchAgent can access it: + +```bash +# Build first if you haven't already +npm run build + +# Install (copy, not link — required for LaunchAgent access) +openclaw plugins install /path/to/hindsight-integrations/openclaw +``` + +Alternatively, install from npm: + +```bash +openclaw plugins install @vectorize-io/hindsight-openclaw +``` + +## Step 3: Configure the plugin + +Add the plugin config to `~/.openclaw/openclaw.json` under `plugins.entries.hindsight-openclaw`: + +```json +{ + "plugins": { + "entries": { + "hindsight-openclaw": { + "enabled": true, + "config": { + "hindsightApiUrl": "https://api.hindsight.vectorize.io", + "hindsightApiToken": "", + "llmProvider": "claude-code", + "dynamicBankId": false, + "bankIdPrefix": "my-sandbox" + } + } + } + } +} +``` + +**Config notes:** + +| Field | Value | Why | +|-------|-------|-----| +| `hindsightApiUrl` + `hindsightApiToken` | External API URL + key | Skips the local daemon; no `uvx`/`uv` required inside the sandbox | +| `llmProvider: "claude-code"` | `"claude-code"` | Satisfies LLM detection without a separate API key — Claude Code is available in the sandbox via the `claude_code` policy | +| `dynamicBankId: false` | `false` | All conversations write to one bank; easier to verify during testing | +| `bankIdPrefix` | e.g. `"my-sandbox"` | Results in bank ID `my-sandbox-openclaw` | + +> **Note:** The gateway log will say `Dynamic bank IDs disabled - using static bank: openclaw` — this is a misleading log message. The actual bank ID used at runtime correctly applies the prefix (e.g. `my-sandbox-openclaw`). You can verify by watching for `[Hindsight] Default bank: my-sandbox-openclaw` in the logs after full initialization. + +## Step 4: Add the Hindsight network policy to the sandbox + +The sandbox blocks all outbound traffic by default. You need to add `api.hindsight.vectorize.io` to the egress policy. + +Get the current full policy by running `openshell sandbox get ` and save it to a YAML file, then add the `hindsight` block under `network_policies`: + +```yaml +network_policies: + # ... your existing policies ... + hindsight: + name: hindsight + endpoints: + - host: api.hindsight.vectorize.io + port: 443 + protocol: rest + tls: terminate + enforcement: enforce + rules: + - allow: + method: GET + path: /** + - allow: + method: POST + path: /** + - allow: + method: PUT + path: /** + binaries: + - path: /usr/local/bin/openclaw +``` + +Apply it: + +```bash +openshell policy set --policy /path/to/full-policy.yaml --wait +``` + +> **Important:** `openshell policy set` replaces the entire policy, not just patches it. Make sure your YAML includes all existing network policies or they will be removed. + +Verify the policy loaded: + +```bash +openshell policy get +# Should show: Status: Loaded, and version incremented +``` + +## Step 5: Restart the OpenClaw gateway + +```bash +openclaw gateway restart +``` + +Watch the logs to confirm the plugin loaded and the API is reachable: + +```bash +# Should see: +# [Hindsight] Plugin loaded successfully +# [Hindsight] ✓ Using external API: https://api.hindsight.vectorize.io +# [Hindsight] External API health: {"status":"healthy","database":"connected"} +# [Hindsight] Default bank: my-sandbox-openclaw +# [Hindsight] ✓ Ready (external API mode) +grep Hindsight ~/.openclaw/logs/gateway.log | tail -20 +``` + +## Step 6: Test + +Send a message to the agent: + +```bash +openclaw agent --agent main --session-id test-1 \ + -m "My name is Ben and I work on Hindsight. I prefer detailed commit messages." +``` + +Verify memory was retained (check logs): + +```bash +grep "Retained\|agent_end" ~/.openclaw/logs/gateway.log | tail -5 +# Should see: [Hindsight] Retained N messages to bank my-sandbox-openclaw for session ... +``` + +Test recall in a new session: + +```bash +openclaw agent --agent main --session-id test-2 \ + -m "What do you remember about me?" +# Should recall your name and preferences from the previous session +``` + +You can also verify directly against the API: + +```bash +curl -s -X POST "https://api.hindsight.vectorize.io/v1/default/banks/my-sandbox-openclaw/memories/recall" \ + -H "Authorization: Bearer " \ + -H "Content-Type: application/json" \ + -d '{"query": "what do you know about the user", "max_tokens": 512}' +``` + +## Troubleshooting + +**Plugin fails to load with `EPERM: operation not permitted, scandir`** + +You used `--link` when installing. The OpenClaw LaunchAgent runs under a restricted macOS security context and cannot access `~/Documents` or other user directories by symlink. Reinstall without `--link`: + +```bash +openclaw plugins uninstall hindsight-openclaw +openclaw plugins install /path/to/hindsight-integrations/openclaw # no --link +``` + +**`[Hindsight] Failed to retain memory (HTTP 403)`** + +The sandbox network policy is blocking the outbound call. Check that: +1. The `hindsight` network policy block is present in your policy YAML +2. The policy was applied and shows `Status: Loaded` (`openshell policy get `) +3. The `binaries` list includes `/usr/local/bin/openclaw` + +**Gateway restart times out but then recovers** + +This is normal on first restart after installing a plugin — the LaunchAgent takes a moment to reload. The gateway is healthy if `openclaw gateway status` shows `RPC probe: ok`. + +**`openclaw agent` fails with `Pass --to, --session-id, or --agent`** + +You need to specify a session. Use `--agent main` to use the default agent, or `--session-id ` to create a named session. diff --git a/hindsight-integrations/nemoclaw/README.md b/hindsight-integrations/nemoclaw/README.md new file mode 100644 index 00000000..a422406d --- /dev/null +++ b/hindsight-integrations/nemoclaw/README.md @@ -0,0 +1,60 @@ +# hindsight-nemoclaw + +One-command setup for [Hindsight](https://hindsight.vectorize.io) persistent memory on [NemoClaw](https://nemoclaw.ai) sandboxes. + +NemoClaw runs [OpenClaw](https://openclaw.ai) inside an OpenShell sandbox with strict network egress policies. This package automates the full setup: installing the `hindsight-openclaw` plugin, configuring external API mode, merging the Hindsight egress rule into your sandbox policy, and restarting the gateway. + +## Quick Start + +```bash +npx @vectorize-io/hindsight-nemoclaw setup \ + --sandbox my-assistant \ + --api-url https://api.hindsight.vectorize.io \ + --api-token \ + --bank-prefix my-sandbox +``` + +Get an API key at [Hindsight Cloud](https://ui.hindsight.vectorize.io/signup). + +## Documentation + +Full setup guide, pitfalls, and troubleshooting: + +**[NemoClaw Integration Documentation](https://vectorize.io/hindsight/sdks/integrations/nemoclaw)** + +Or see [NEMOCLAW.md](./NEMOCLAW.md) in this directory for a step-by-step walkthrough. + +## CLI Reference + +``` +hindsight-nemoclaw setup [options] + +Options: + --sandbox NemoClaw sandbox name (required) + --api-url Hindsight API URL (required) + --api-token Hindsight API token (required) + --bank-prefix Memory bank prefix (default: "nemoclaw") + --skip-policy Skip sandbox network policy update + --skip-plugin-install Skip openclaw plugin installation + --dry-run Preview changes without applying + --help Show help +``` + +## What It Does + +1. **Preflight** — verifies `openshell` and `openclaw` are installed +2. **Install plugin** — runs `openclaw plugins install @vectorize-io/hindsight-openclaw` +3. **Configure plugin** — writes external API mode config to `~/.openclaw/openclaw.json` +4. **Apply policy** — reads current sandbox policy, merges Hindsight egress rule, re-applies via `openshell policy set` +5. **Restart gateway** — runs `openclaw gateway restart` + +## Links + +- [Hindsight Documentation](https://vectorize.io/hindsight) +- [NemoClaw](https://nemoclaw.ai) +- [OpenClaw](https://openclaw.ai) +- [GitHub Repository](https://github.com/vectorize-io/hindsight) + +## License + +MIT diff --git a/hindsight-integrations/nemoclaw/package-lock.json b/hindsight-integrations/nemoclaw/package-lock.json new file mode 100644 index 00000000..e1f9a179 --- /dev/null +++ b/hindsight-integrations/nemoclaw/package-lock.json @@ -0,0 +1,1318 @@ +{ + "name": "@vectorize-io/hindsight-nemoclaw", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vectorize-io/hindsight-nemoclaw", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "js-yaml": "^4.1.0" + }, + "bin": { + "hindsight-nemoclaw": "dist/cli.js" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.0.0", + "typescript": "^5.3.0", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=22" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.1.tgz", + "integrity": "sha512-mukuNALVsoix/w1BJwFzwXBN/dHeejQtuVzcDsfOEsdpCumXb/E9j8w11h5S54tT1xhifGfbbSm/ICrObRb3KA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.0", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", + "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.0.tgz", + "integrity": "sha512-N10dEJNSsUx41Z6pZsXU8FjPjpBEplgH24sfkmITrBED1/U2Esum9F3lfLrMjKHHjmi557zQn7kR9R+XWXu5Rg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true, + "license": "MIT" + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.1.tgz", + "integrity": "sha512-p64ah1M1ld8xjWv3qbvFwHiFVWrq1yFvV4f7w+mzaqiR4IlSgkqhcRdHwsGgomwzBH51sRY4NEowLxnaBjcW/A==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1", + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + } + }, + "node_modules/@oxc-project/types": { + "version": "0.120.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.120.0.tgz", + "integrity": "sha512-k1YNu55DuvAip/MGE1FTsIuU3FUCn6v/ujG9V7Nq5Df/kX2CWb13hhwD0lmJGMGqE+bE1MXvv9SZVnMzEXlWcg==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-jOHxwXhxmFKuXztiu1ORieJeTbx5vrTkcOkkkn2d35726+iwhrY1w/+nYY/AGgF12thg33qC3R1LMBF5tHTZHg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-gED05Teg/vtTZbIJBc4VNMAxAFDUPkuO/rAIyyxZjTj1a1/s6z5TII/5yMGZ0uLRCifEtwUQn8OlYzuYc0m70w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-rI15NcM1mA48lqrIxVkHfAqcyFLcQwyXWThy+BQ5+mkKKPvSO26ir+ZDp36AgYoYVkqvMcdS8zOE6SeBsR9e8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.10.tgz", + "integrity": "sha512-XZRXHdTa+4ME1MuDVp021+doQ+z6Ei4CCFmNc5/sKbqb8YmkiJdj8QKlV3rCI0AJtAeSB5n0WGPuJWNL9p/L2w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.10.tgz", + "integrity": "sha512-R0SQMRluISSLzFE20sPWYHVmJdDQnRyc/FzSCN72BqQmh2SOZUFG+N3/vBZpR4C6WpEUVYJLrYUXaj43sJsNLA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-Y1reMrV/o+cwpduYhJuOE3OMKx32RMYCidf14y+HssARRmhDuWXJ4yVguDg2R/8SyyGNo+auzz64LnPK9Hq6jg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-vELN+HNb2IzuzSBUOD4NHmP9yrGwl1DVM29wlQvx1OLSclL0NgVWnVDKl/8tEks79EFek/kebQKnNJkIAA4W2g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-ZqrufYTgzxbHwpqOjzSsb0UV/aV2TFIY5rP8HdsiPTv/CuAgCRjM6s9cYFwQ4CNH+hf9Y4erHW1GjZuZ7WoI7w==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-gSlmVS1FZJSRicA6IyjoRoKAFK7IIHBs7xJuHRSmjImqk3mPPWbR7RhbnfH2G6bcmMEllCt2vQ/7u9e6bBnByg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.10.tgz", + "integrity": "sha512-eOCKUpluKgfObT2pHjztnaWEIbUabWzk3qPZ5PuacuPmr4+JtQG4k2vGTY0H15edaTnicgU428XW/IH6AimcQw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.10.tgz", + "integrity": "sha512-Xdf2jQbfQowJnLcgYfD/m0Uu0Qj5OdxKallD78/IPPfzaiaI4KRAwZzHcKQ4ig1gtg1SuzC7jovNiM2TzQsBXA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.10.tgz", + "integrity": "sha512-o1hYe8hLi1EY6jgPFyxQgQ1wcycX+qz8eEbVmot2hFkgUzPxy9+kF0u0NIQBeDq+Mko47AkaFFaChcvZa9UX9Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-wasm32-wasi": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.10.tgz", + "integrity": "sha512-Ugv9o7qYJudqQO5Y5y2N2SOo6S4WiqiNOpuQyoPInnhVzCY+wi/GHltcLHypG9DEUYMB0iTB/huJrpadiAcNcA==", + "cpu": [ + "wasm32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@napi-rs/wasm-runtime": "^1.1.1" + }, + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-7UODQb4fQUNT/vmgDZBl3XOBAIOutP5R3O/rkxg0aLfEGQ4opbCgU5vOw/scPe4xOqBwL9fw7/RP1vAMZ6QlAQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.10.tgz", + "integrity": "sha512-PYxKHMVHOb5NJuDL53vBUl1VwUjymDcYI6rzpIni0C9+9mTiJedvUxSk7/RPp7OOAm3v+EjgMu9bIy3N6b408w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.10.tgz", + "integrity": "sha512-UkVDEFk1w3mveXeKgaTuYfKWtPbvgck1dT8TUG3bnccrH0XtLTuAyfCoks4Q/M5ZGToSVJTIQYCzy2g/atAOeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.1.tgz", + "integrity": "sha512-9tTaPJLSiejZKx+Bmog4uSubteqTvFrVrURwkmHixBo0G4seD0zUxp98E1DzUBJxLQ3NPwXrGKDiVjwx/DpPsg==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@types/chai": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/@types/chai/-/chai-5.2.3.tgz", + "integrity": "sha512-Mw558oeA9fFbv65/y4mHtXDs9bPnFMZAL/jxdPFUpOHHIXX91mcgEHbS5Lahr+pwZFR8A7GQleRWeI6cGFC2UA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/deep-eql": "*", + "assertion-error": "^2.0.1" + } + }, + "node_modules/@types/deep-eql": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@types/deep-eql/-/deep-eql-4.0.2.tgz", + "integrity": "sha512-c9h9dVVMigMPc4bwTvC5dxqtqJZwQPePsWjPlpSOnojbor6pGqdk541lfA7AqFQr5pB1BRdq0juY9db81BwyFw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/js-yaml": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/js-yaml/-/js-yaml-4.0.9.tgz", + "integrity": "sha512-k4MGaQl5TGo/iipqb2UDG2UwjXziSWkh0uysQelTlJpX1qGlpUZYm8PnO4DxG1qBomtJUdYJ6qR6xdIah10JLg==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "20.19.37", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.37.tgz", + "integrity": "sha512-8kzdPJ3FsNsVIurqBs7oodNnCEVbni9yUEkaHbgptDACOPW04jimGagZ51E6+lXUwJjgnBw+hyko/lkFWCldqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vitest/expect": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.0.tgz", + "integrity": "sha512-EIxG7k4wlWweuCLG9Y5InKFwpMEOyrMb6ZJ1ihYu02LVj/bzUwn2VMU+13PinsjRW75XnITeFrQBMH5+dLvCDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "chai": "^6.2.2", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.0.tgz", + "integrity": "sha512-evxREh+Hork43+Y4IOhTo+h5lGmVRyjqI739Rz4RlUPqwrkFFDF6EMvOOYjTx4E8Tl6gyCLRL8Mu7Ry12a13Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.0", + "estree-walker": "^3.0.3", + "magic-string": "^0.30.21" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "msw": "^2.4.9", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.0.tgz", + "integrity": "sha512-3RZLZlh88Ib0J7NQTRATfc/3ZPOnSUn2uDBUoGNn5T36+bALixmzphN26OUD3LRXWkJu4H0s5vvUeqBiw+kS0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.0.tgz", + "integrity": "sha512-Duvx2OzQ7d6OjchL+trw+aSrb9idh7pnNfxrklo14p3zmNL4qPCDeIJAK+eBKYjkIwG96Bc6vYuxhqDXQOWpoQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.0", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.0.tgz", + "integrity": "sha512-0Vy9euT1kgsnj1CHttwi9i9o+4rRLEaPRSOJ5gyv579GJkNpgJK+B4HSv/rAWixx2wdAFci1X4CEPjiu2bXIMg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "@vitest/utils": "4.1.0", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.0.tgz", + "integrity": "sha512-pz77k+PgNpyMDv2FV6qmk5ZVau6c3R8HC8v342T2xlFxQKTrSeYw9waIJG8KgV9fFwAtTu4ceRzMivPTH6wSxw==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.0.tgz", + "integrity": "sha512-XfPXT6a8TZY3dcGY8EdwsBulFCIw+BeeX0RZn2x/BtiY/75YGh8FeWGG8QISN/WhaqSrE2OrlDgtF8q5uhOTmw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.0", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "license": "Python-2.0" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + } + }, + "node_modules/chai": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", + "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true, + "license": "MIT" + }, + "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==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=8" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/estree-walker": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", + "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0" + } + }, + "node_modules/expect-type": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/expect-type/-/expect-type-1.3.0.tgz", + "integrity": "sha512-knvyeauYhqjOYvQ66MznSMs83wmHrCycNEN6Ao+2AeYEfxUIkuiVxdEa1qlGEPK+We3n0THiDciYSsCcgW/DoA==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=12.0.0" + } + }, + "node_modules/fdir": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", + "integrity": "sha512-tIbYtZbucOs0BRGqPJkshJUYdL+SDH7dVM8gjy+ERp3WAUjLEFJE+02kanyHtwjWOnwrKYBiwAmM0p4kLJAnXg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "picomatch": "^3 || ^4" + }, + "peerDependenciesMeta": { + "picomatch": { + "optional": true + } + } + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/js-yaml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "license": "MIT", + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/lightningcss": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.32.0.tgz", + "integrity": "sha512-NXYBzinNrblfraPGyrbPoD19C1h9lfI/1mzgWYvXUTe414Gz/X1FD2XBZSZM7rRTrMA8JL3OtAaGifrIKhQ5yQ==", + "dev": true, + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.32.0", + "lightningcss-darwin-arm64": "1.32.0", + "lightningcss-darwin-x64": "1.32.0", + "lightningcss-freebsd-x64": "1.32.0", + "lightningcss-linux-arm-gnueabihf": "1.32.0", + "lightningcss-linux-arm64-gnu": "1.32.0", + "lightningcss-linux-arm64-musl": "1.32.0", + "lightningcss-linux-x64-gnu": "1.32.0", + "lightningcss-linux-x64-musl": "1.32.0", + "lightningcss-win32-arm64-msvc": "1.32.0", + "lightningcss-win32-x64-msvc": "1.32.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.32.0.tgz", + "integrity": "sha512-YK7/ClTt4kAK0vo6w3X+Pnm0D2cf2vPHbhOXdoNti1Ga0al1P4TBZhwjATvjNwLEBCnKvjJc2jQgHXH0NEwlAg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.32.0.tgz", + "integrity": "sha512-RzeG9Ju5bag2Bv1/lwlVJvBE3q6TtXskdZLLCyfg5pt+HLz9BqlICO7LZM7VHNTTn/5PRhHFBSjk5lc4cmscPQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.32.0.tgz", + "integrity": "sha512-U+QsBp2m/s2wqpUYT/6wnlagdZbtZdndSmut/NJqlCcMLTWp5muCrID+K5UJ6jqD2BFshejCYXniPDbNh73V8w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.32.0.tgz", + "integrity": "sha512-JCTigedEksZk3tHTTthnMdVfGf61Fky8Ji2E4YjUTEQX14xiy/lTzXnu1vwiZe3bYe0q+SpsSH/CTeDXK6WHig==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.32.0.tgz", + "integrity": "sha512-x6rnnpRa2GL0zQOkt6rts3YDPzduLpWvwAF6EMhXFVZXD4tPrBkEFqzGowzCsIWsPjqSK+tyNEODUBXeeVHSkw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.32.0.tgz", + "integrity": "sha512-0nnMyoyOLRJXfbMOilaSRcLH3Jw5z9HDNGfT/gwCPgaDjnx0i8w7vBzFLFR1f6CMLKF8gVbebmkUN3fa/kQJpQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.32.0.tgz", + "integrity": "sha512-UpQkoenr4UJEzgVIYpI80lDFvRmPVg6oqboNHfoH4CQIfNA+HOrZ7Mo7KZP02dC6LjghPQJeBsvXhJod/wnIBg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.32.0.tgz", + "integrity": "sha512-V7Qr52IhZmdKPVr+Vtw8o+WLsQJYCTd8loIfpDaMRWGUZfBOYEJeyJIkqGIDMZPwPx24pUMfwSxxI8phr/MbOA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.32.0.tgz", + "integrity": "sha512-bYcLp+Vb0awsiXg/80uCRezCYHNg1/l3mt0gzHnWV9XP1W5sKa5/TCdGWaR/zBM2PeF/HbsQv/j2URNOiVuxWg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.32.0.tgz", + "integrity": "sha512-8SbC8BR40pS6baCM8sbtYDSwEVQd4JlFTOlaD3gWGHfThTcABnNDBda6eTZeqbofalIJhFx0qKzgHJmcPTnGdw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.32.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.32.0.tgz", + "integrity": "sha512-Amq9B/SoZYdDi1kFrojnoqPLxYhQ4Wo5XiL8EVJrVsB8ARoC1PWW6VGtT0WKCemjy8aC+louJnjS7U18x3b06Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/magic-string": { + "version": "0.30.21", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", + "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.5" + } + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/obug": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", + "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "dev": true, + "funding": [ + "https://github.com/sponsors/sxzz", + "https://opencollective.com/debug" + ], + "license": "MIT" + }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true, + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.3.tgz", + "integrity": "sha512-5gTmgEY/sqK6gFXLIsQNH19lWb4ebPDLA4SdLP7dsWkIXHWlG66oPuVvXSGFPppYZz8ZDZq0dYYrbHfBCVUb1Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.10", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.10.tgz", + "integrity": "sha512-q7j6vvarRFmKpgJUT8HCAUljkgzEp4LAhPlJUvQhA5LA1SUL36s5QCysMutErzL3EbNOZOkoziSx9iZC4FddKA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.120.0", + "@rolldown/pluginutils": "1.0.0-rc.10" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.10", + "@rolldown/binding-darwin-x64": "1.0.0-rc.10", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.10", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.10", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.10", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.10", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.10", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.10", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.10", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.10" + } + }, + "node_modules/siginfo": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/siginfo/-/siginfo-2.0.0.tgz", + "integrity": "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==", + "dev": true, + "license": "ISC" + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/stackback": { + "version": "0.0.2", + "resolved": "https://registry.npmjs.org/stackback/-/stackback-0.0.2.tgz", + "integrity": "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==", + "dev": true, + "license": "MIT" + }, + "node_modules/std-env": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/std-env/-/std-env-4.0.0.tgz", + "integrity": "sha512-zUMPtQ/HBY3/50VbpkupYHbRroTRZJPRLvreamgErJVys0ceuzMkD44J/QjqhHjOzK42GQ3QZIeFG1OYfOtKqQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinybench": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/tinybench/-/tinybench-2.9.0.tgz", + "integrity": "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg==", + "dev": true, + "license": "MIT" + }, + "node_modules/tinyexec": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", + "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=18" + } + }, + "node_modules/tinyglobby": { + "version": "0.2.15", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", + "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "fdir": "^6.5.0", + "picomatch": "^4.0.3" + }, + "engines": { + "node": ">=12.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/SuperchupuDev" + } + }, + "node_modules/tinyrainbow": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/tinyrainbow/-/tinyrainbow-3.1.0.tgz", + "integrity": "sha512-Bf+ILmBgretUrdJxzXM0SgXLZ3XfiaUuOj/IKQHuTXip+05Xn+uyEYdVg0kYDipTBcLrCVyUzAPz7QmArb0mmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.0.0" + } + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", + "dev": true, + "license": "0BSD", + "optional": true + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", + "integrity": "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/vite": { + "version": "8.0.1", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.1.tgz", + "integrity": "sha512-wt+Z2qIhfFt85uiyRt5LPU4oVEJBXj8hZNWKeqFG4gRG/0RaRGJ7njQCwzFVjO+v4+Ipmf5CY7VdmZRAYYBPHw==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.3", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.10", + "tinyglobby": "^0.2.15" + }, + "bin": { + "vite": "bin/vite.js" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "funding": { + "url": "https://github.com/vitejs/vite?sponsor=1" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + }, + "peerDependencies": { + "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.1.0", + "esbuild": "^0.27.0", + "jiti": ">=1.21.0", + "less": "^4.0.0", + "sass": "^1.70.0", + "sass-embedded": "^1.70.0", + "stylus": ">=0.54.8", + "sugarss": "^5.0.0", + "terser": "^5.16.0", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + }, + "@vitejs/devtools": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "jiti": { + "optional": true + }, + "less": { + "optional": true + }, + "sass": { + "optional": true + }, + "sass-embedded": { + "optional": true + }, + "stylus": { + "optional": true + }, + "sugarss": { + "optional": true + }, + "terser": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/vitest": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.0.tgz", + "integrity": "sha512-YbDrMF9jM2Lqc++2530UourxZHmkKLxrs4+mYhEwqWS97WJ7wOYEkcr+QfRgJ3PW9wz3odRijLZjHEaRLTNbqw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.0", + "@vitest/mocker": "4.1.0", + "@vitest/pretty-format": "4.1.0", + "@vitest/runner": "4.1.0", + "@vitest/snapshot": "4.1.0", + "@vitest/spy": "4.1.0", + "@vitest/utils": "4.1.0", + "es-module-lexer": "^2.0.0", + "expect-type": "^1.3.0", + "magic-string": "^0.30.21", + "obug": "^2.1.1", + "pathe": "^2.0.3", + "picomatch": "^4.0.3", + "std-env": "^4.0.0-rc.1", + "tinybench": "^2.9.0", + "tinyexec": "^1.0.2", + "tinyglobby": "^0.2.15", + "tinyrainbow": "^3.0.3", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0", + "why-is-node-running": "^2.3.0" + }, + "bin": { + "vitest": "vitest.mjs" + }, + "engines": { + "node": "^20.0.0 || ^22.0.0 || >=24.0.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + }, + "peerDependencies": { + "@edge-runtime/vm": "*", + "@opentelemetry/api": "^1.9.0", + "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", + "@vitest/browser-playwright": "4.1.0", + "@vitest/browser-preview": "4.1.0", + "@vitest/browser-webdriverio": "4.1.0", + "@vitest/ui": "4.1.0", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.0.0-0" + }, + "peerDependenciesMeta": { + "@edge-runtime/vm": { + "optional": true + }, + "@opentelemetry/api": { + "optional": true + }, + "@types/node": { + "optional": true + }, + "@vitest/browser-playwright": { + "optional": true + }, + "@vitest/browser-preview": { + "optional": true + }, + "@vitest/browser-webdriverio": { + "optional": true + }, + "@vitest/ui": { + "optional": true + }, + "happy-dom": { + "optional": true + }, + "jsdom": { + "optional": true + }, + "vite": { + "optional": false + } + } + }, + "node_modules/why-is-node-running": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", + "integrity": "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w==", + "dev": true, + "license": "MIT", + "dependencies": { + "siginfo": "^2.0.0", + "stackback": "0.0.2" + }, + "bin": { + "why-is-node-running": "cli.js" + }, + "engines": { + "node": ">=8" + } + } + } +} diff --git a/hindsight-integrations/nemoclaw/package.json b/hindsight-integrations/nemoclaw/package.json new file mode 100644 index 00000000..d0bd8664 --- /dev/null +++ b/hindsight-integrations/nemoclaw/package.json @@ -0,0 +1,51 @@ +{ + "name": "@vectorize-io/hindsight-nemoclaw", + "version": "0.1.0", + "description": "Setup CLI for hindsight-openclaw on NemoClaw sandboxes — installs the plugin, configures external API mode, and applies the OpenShell network policy", + "type": "module", + "main": "dist/cli.js", + "bin": { + "hindsight-nemoclaw": "dist/cli.js" + }, + "keywords": [ + "nemoclaw", + "openclaw", + "memory", + "ai", + "agent", + "hindsight", + "openshell", + "nvidia" + ], + "author": "Vectorize ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/vectorize-io/hindsight.git", + "directory": "hindsight-integrations/nemoclaw" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsc && node -e \"const f='dist/cli.js',s=require('fs');s.writeFileSync(f,'#!/usr/bin/env node\\n'+s.readFileSync(f,'utf8'));s.chmodSync(f,0o755)\"", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "test": "vitest run src", + "test:watch": "vitest src", + "prepublishOnly": "npm run clean && npm run build" + }, + "dependencies": { + "js-yaml": "^4.1.0" + }, + "devDependencies": { + "@types/js-yaml": "^4.0.9", + "@types/node": "^20.0.0", + "typescript": "^5.3.0", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=22" + } +} diff --git a/hindsight-integrations/nemoclaw/src/cli.ts b/hindsight-integrations/nemoclaw/src/cli.ts new file mode 100644 index 00000000..860e2a9a --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/cli.ts @@ -0,0 +1,84 @@ +import { runSetup } from './setup.js'; +import type { CliArgs } from './types.js'; + +function usage(): void { + process.stdout.write(` +hindsight-nemoclaw — Setup CLI for Hindsight memory on NemoClaw sandboxes + +Usage: + hindsight-nemoclaw setup [options] + +Required options: + --sandbox NemoClaw sandbox name (e.g. my-assistant) + --api-url Hindsight Cloud API URL (https://api.hindsight.vectorize.io) + --api-token Hindsight API key from https://ui.hindsight.vectorize.io + --bank-prefix Bank ID prefix (memories go to -openclaw) + +Optional options: + --skip-policy Skip the openshell policy update + --skip-plugin-install Skip openclaw plugins install + --dry-run Print what would be changed without executing + --help Show this help + +Example: + hindsight-nemoclaw setup \\ + --sandbox my-assistant \\ + --api-url https://api.hindsight.vectorize.io \\ + --api-token hsk_abc123 \\ + --bank-prefix my-sandbox +`); +} + +function parseArgs(argv: string[]): CliArgs | null { + const args = argv.slice(2); + + if (args.length === 0 || args.includes('--help') || args.includes('-h')) { + usage(); + return null; + } + + if (args[0] !== 'setup') { + process.stderr.write(`Unknown command: ${args[0]}\nRun with --help for usage.\n`); + process.exit(1); + } + + const get = (flag: string): string | undefined => { + const idx = args.indexOf(flag); + if (idx === -1 || idx + 1 >= args.length) return undefined; + return args[idx + 1]; + }; + + const sandbox = get('--sandbox'); + const apiUrl = get('--api-url'); + const apiToken = get('--api-token'); + const bankPrefix = get('--bank-prefix'); + + const missing: string[] = []; + if (!sandbox) missing.push('--sandbox'); + if (!apiUrl) missing.push('--api-url'); + if (!apiToken) missing.push('--api-token'); + if (!bankPrefix) missing.push('--bank-prefix'); + + if (missing.length > 0) { + process.stderr.write(`Missing required options: ${missing.join(', ')}\nRun with --help for usage.\n`); + process.exit(1); + } + + return { + sandbox: sandbox!, + apiUrl: apiUrl!, + apiToken: apiToken!, + bankPrefix: bankPrefix!, + skipPolicy: args.includes('--skip-policy'), + skipPluginInstall: args.includes('--skip-plugin-install'), + dryRun: args.includes('--dry-run'), + }; +} + +const args = parseArgs(process.argv); +if (args) { + runSetup(args).catch(err => { + process.stderr.write(`\nError: ${err instanceof Error ? err.message : String(err)}\n`); + process.exit(1); + }); +} diff --git a/hindsight-integrations/nemoclaw/src/openclaw-config.test.ts b/hindsight-integrations/nemoclaw/src/openclaw-config.test.ts new file mode 100644 index 00000000..437658a4 --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/openclaw-config.test.ts @@ -0,0 +1,101 @@ +import { describe, it, expect } from 'vitest'; +import { mergePluginConfig } from './openclaw-config.js'; +import type { OpenClawConfig, HindsightPluginConfig } from './openclaw-config.js'; + +const PLUGIN_CONFIG: HindsightPluginConfig = { + hindsightApiUrl: 'https://api.hindsight.vectorize.io', + hindsightApiToken: 'hsk_test123', + llmProvider: 'claude-code', + dynamicBankId: false, + bankIdPrefix: 'my-sandbox', +}; + +const BASE_CONFIG: OpenClawConfig = { + meta: { lastTouchedVersion: '2026.3.2' }, + gateway: { port: 18789, mode: 'local' }, + agents: { + defaults: { model: { primary: 'openai/gpt-5' } }, + }, + plugins: { + slots: { memory: 'memory-core' }, + entries: { + 'memory-core': { enabled: false }, + }, + }, +}; + +describe('mergePluginConfig', () => { + it('sets hindsight-openclaw as the memory slot', () => { + const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG); + expect(result.plugins?.slots?.memory).toBe('hindsight-openclaw'); + }); + + it('enables the hindsight-openclaw entry', () => { + const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG); + expect(result.plugins?.entries?.['hindsight-openclaw']?.enabled).toBe(true); + }); + + it('writes the full plugin config', () => { + const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG); + const config = result.plugins?.entries?.['hindsight-openclaw']?.config; + expect(config?.hindsightApiUrl).toBe('https://api.hindsight.vectorize.io'); + expect(config?.hindsightApiToken).toBe('hsk_test123'); + expect(config?.llmProvider).toBe('claude-code'); + expect(config?.dynamicBankId).toBe(false); + expect(config?.bankIdPrefix).toBe('my-sandbox'); + }); + + it('preserves existing top-level config fields', () => { + const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG); + expect(result.gateway).toEqual({ port: 18789, mode: 'local' }); + expect(result.agents).toBeDefined(); + }); + + it('preserves existing plugin entries', () => { + const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG); + expect(result.plugins?.entries?.['memory-core']?.enabled).toBe(false); + }); + + it('merges into existing hindsight-openclaw entry without overwriting other fields', () => { + const configWithExisting: OpenClawConfig = { + ...BASE_CONFIG, + plugins: { + ...BASE_CONFIG.plugins, + entries: { + ...BASE_CONFIG.plugins?.entries, + 'hindsight-openclaw': { + enabled: true, + config: { embedPackagePath: '/some/local/path' }, + }, + }, + }, + }; + const result = mergePluginConfig(configWithExisting, PLUGIN_CONFIG); + const config = result.plugins?.entries?.['hindsight-openclaw']?.config; + // New fields written + expect(config?.hindsightApiUrl).toBe('https://api.hindsight.vectorize.io'); + // Existing custom field preserved + expect(config?.embedPackagePath).toBe('/some/local/path'); + }); + + it('handles missing plugins section gracefully', () => { + const minimal: OpenClawConfig = { gateway: { port: 18789 } }; + const result = mergePluginConfig(minimal, PLUGIN_CONFIG); + expect(result.plugins?.slots?.memory).toBe('hindsight-openclaw'); + expect(result.plugins?.entries?.['hindsight-openclaw']?.enabled).toBe(true); + }); + + it('does not mutate the original config', () => { + const original = JSON.parse(JSON.stringify(BASE_CONFIG)) as OpenClawConfig; + mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG); + expect(JSON.stringify(BASE_CONFIG)).toBe(JSON.stringify(original)); + }); + + it('records install metadata', () => { + const result = mergePluginConfig(BASE_CONFIG, PLUGIN_CONFIG); + const install = result.plugins?.installs?.['hindsight-openclaw'] as Record; + expect(install?.source).toBe('npm'); + expect(install?.version).toBe('latest'); + expect(typeof install?.installedAt).toBe('string'); + }); +}); diff --git a/hindsight-integrations/nemoclaw/src/openclaw-config.ts b/hindsight-integrations/nemoclaw/src/openclaw-config.ts new file mode 100644 index 00000000..b4e17d34 --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/openclaw-config.ts @@ -0,0 +1,106 @@ +import { readFile, writeFile, rename } from 'fs/promises'; +import { join, dirname } from 'path'; +import { homedir } from 'os'; +import { randomBytes } from 'crypto'; + +const CONFIG_PATH = join(homedir(), '.openclaw', 'openclaw.json'); + +export interface HindsightPluginConfig { + hindsightApiUrl: string; + hindsightApiToken: string; + llmProvider: string; + dynamicBankId: boolean; + bankIdPrefix: string; +} + +export interface OpenClawConfig { + plugins?: { + slots?: Record; + entries?: Record }>; + installs?: Record; + [key: string]: unknown; + }; + [key: string]: unknown; +} + +export async function readOpenClawConfig(configPath = CONFIG_PATH): Promise { + let raw: string; + try { + raw = await readFile(configPath, 'utf8'); + } catch (err: unknown) { + const code = (err as NodeJS.ErrnoException).code; + if (code === 'ENOENT') { + throw new Error( + `OpenClaw config not found at ${configPath}.\n` + + `Run \`openclaw\` once to initialize it, then re-run setup.` + ); + } + throw err; + } + return JSON.parse(raw) as OpenClawConfig; +} + +export function mergePluginConfig( + config: OpenClawConfig, + pluginConfig: HindsightPluginConfig +): OpenClawConfig { + const plugins = config.plugins ?? {}; + const entries = plugins.entries ?? {}; + const existing = entries['hindsight-openclaw'] ?? { enabled: true }; + + return { + ...config, + plugins: { + ...plugins, + slots: { + ...(plugins.slots ?? {}), + memory: 'hindsight-openclaw', + }, + entries: { + ...entries, + 'hindsight-openclaw': { + ...existing, + enabled: true, + config: { + ...(existing.config ?? {}), + hindsightApiUrl: pluginConfig.hindsightApiUrl, + hindsightApiToken: pluginConfig.hindsightApiToken, + llmProvider: pluginConfig.llmProvider, + dynamicBankId: pluginConfig.dynamicBankId, + bankIdPrefix: pluginConfig.bankIdPrefix, + }, + }, + }, + installs: { + ...(plugins.installs ?? {}), + 'hindsight-openclaw': { + source: 'npm', + version: 'latest', + installedAt: new Date().toISOString(), + }, + }, + }, + }; +} + +export async function writeOpenClawConfig( + config: OpenClawConfig, + configPath = CONFIG_PATH +): Promise { + const contents = JSON.stringify(config, null, 2) + '\n'; + const tmp = `${configPath}.${randomBytes(6).toString('hex')}.tmp`; + await writeFile(tmp, contents, 'utf8'); + await rename(tmp, configPath); +} + +export async function applyPluginConfig( + pluginConfig: HindsightPluginConfig, + configPath = CONFIG_PATH +): Promise { + const current = await readOpenClawConfig(configPath); + const updated = mergePluginConfig(current, pluginConfig); + await writeOpenClawConfig(updated, configPath); +} + +export { CONFIG_PATH }; +export { dirname }; diff --git a/hindsight-integrations/nemoclaw/src/policy-reader.test.ts b/hindsight-integrations/nemoclaw/src/policy-reader.test.ts new file mode 100644 index 00000000..799e09a9 --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/policy-reader.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect } from 'vitest'; +import { stripAnsi, extractPolicyYaml, parseSandboxPolicy } from './policy-reader.js'; +import { serializePolicy } from './policy-writer.js'; + +// Fixture: actual output of `openshell sandbox get my-assistant` +// (ANSI codes represented as escape sequences) +const FIXTURE_RAW = `\x1b[1m\x1b[36mSandbox:\x1b[39m\x1b[0m + + \x1b[2mId:\x1b[0m 61c993f1-010f-4eca-a1ac-d6ddec9d604a + \x1b[2mName:\x1b[0m my-assistant + \x1b[2mNamespace:\x1b[0m openshell + \x1b[2mPhase:\x1b[0m Ready + +\x1b[1m\x1b[36mPolicy:\x1b[39m\x1b[0m + + \x1b[2mversion\x1b[0m\x1b[2m:\x1b[0m 1 + \x1b[2mfilesystem_policy\x1b[0m\x1b[2m:\x1b[0m + \x1b[2minclude_workdir\x1b[0m\x1b[2m:\x1b[0m true + \x1b[2mread_only\x1b[0m\x1b[2m:\x1b[0m + \x1b[2m- \x1b[0m/usr + \x1b[2m- \x1b[0m/lib + \x1b[2mread_write\x1b[0m\x1b[2m:\x1b[0m + \x1b[2m- \x1b[0m/sandbox + \x1b[2m- \x1b[0m/tmp + \x1b[2mnetwork_policies\x1b[0m\x1b[2m:\x1b[0m + \x1b[2mclaude_code\x1b[0m\x1b[2m:\x1b[0m + \x1b[2mname\x1b[0m\x1b[2m:\x1b[0m claude_code + \x1b[2mendpoints\x1b[0m\x1b[2m:\x1b[0m + \x1b[2m- \x1b[0mhost: api.anthropic.com + \x1b[2mport\x1b[0m\x1b[2m:\x1b[0m 443 + \x1b[2mrules\x1b[0m\x1b[2m:\x1b[0m + \x1b[2m- \x1b[0mallow: + \x1b[2mmethod\x1b[0m\x1b[2m:\x1b[0m '*' + \x1b[2mpath\x1b[0m\x1b[2m:\x1b[0m /** + \x1b[2mbinaries\x1b[0m\x1b[2m:\x1b[0m + \x1b[2m- \x1b[0mpath: /usr/local/bin/claude +`; + +describe('stripAnsi', () => { + it('removes ANSI escape codes', () => { + expect(stripAnsi('\x1b[1m\x1b[36mHello\x1b[39m\x1b[0m')).toBe('Hello'); + }); + + it('leaves plain strings unchanged', () => { + expect(stripAnsi('version: 1')).toBe('version: 1'); + }); + + it('handles strings with no ANSI codes', () => { + expect(stripAnsi(' - /usr')).toBe(' - /usr'); + }); +}); + +describe('extractPolicyYaml', () => { + it('extracts the Policy: section and dedents by 2 spaces', () => { + const result = extractPolicyYaml(FIXTURE_RAW); + expect(result).toContain('version: 1'); + expect(result).toContain('filesystem_policy:'); + expect(result).toContain('network_policies:'); + }); + + it('does not include the Sandbox: section', () => { + const result = extractPolicyYaml(FIXTURE_RAW); + expect(result).not.toContain('Sandbox:'); + expect(result).not.toContain('my-assistant'); + }); + + it('throws if Policy: section is missing', () => { + expect(() => extractPolicyYaml('no policy here')).toThrow('Could not find "Policy:"'); + }); +}); + +describe('parseSandboxPolicy', () => { + it('parses version field', () => { + const policy = parseSandboxPolicy(FIXTURE_RAW); + expect(policy.version).toBe(1); + }); + + it('parses filesystem_policy', () => { + const policy = parseSandboxPolicy(FIXTURE_RAW); + expect(policy.filesystem_policy?.include_workdir).toBe(true); + expect(policy.filesystem_policy?.read_only).toContain('/usr'); + }); + + it('parses network_policies', () => { + const policy = parseSandboxPolicy(FIXTURE_RAW); + expect(policy.network_policies).toBeDefined(); + expect(policy.network_policies?.claude_code).toBeDefined(); + expect(policy.network_policies?.claude_code?.name).toBe('claude_code'); + }); + + it('is idempotent — parse → serialize → parse yields same structure', () => { + const policy1 = parseSandboxPolicy(FIXTURE_RAW); + const yamlStr = serializePolicy(policy1); + // Re-wrap in a Policy: header to match the expected format + const wrapped = 'Policy:\n' + yamlStr.split('\n').map((l: string) => ` ${l}`).join('\n'); + const policy2 = parseSandboxPolicy(wrapped); + expect(policy2.version).toBe(policy1.version); + expect(Object.keys(policy2.network_policies ?? {})).toEqual( + Object.keys(policy1.network_policies ?? {}) + ); + }); +}); diff --git a/hindsight-integrations/nemoclaw/src/policy-reader.ts b/hindsight-integrations/nemoclaw/src/policy-reader.ts new file mode 100644 index 00000000..55535ac0 --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/policy-reader.ts @@ -0,0 +1,88 @@ +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import yaml from 'js-yaml'; +import type { SandboxPolicy } from './types.js'; + +const execFileAsync = promisify(execFile); + +/** Strip ANSI escape codes from a string */ +export function stripAnsi(str: string): string { + return str.replace(/\x1B\[[0-9;]*m/g, ''); +} + +/** + * Extract and dedent the policy section from `openshell sandbox get` output. + * The output looks like: + * + * Sandbox: + * Id: ... + * Name: ... + * + * Policy: + * version: 1 + * filesystem_policy: + * ... + * + * We need to extract everything after `Policy:` and dedent by 2 spaces. + */ +export function extractPolicyYaml(raw: string): string { + const stripped = stripAnsi(raw); + const lines = stripped.split('\n'); + + const policyHeaderIdx = lines.findIndex(l => l.trimEnd() === 'Policy:'); + if (policyHeaderIdx === -1) { + throw new Error('Could not find "Policy:" section in `openshell sandbox get` output'); + } + + const policyLines = lines.slice(policyHeaderIdx + 1); + + // Dedent by 2 spaces (the policy block is indented under `Policy:`) + const dedented = policyLines.map(l => { + if (l.startsWith(' ')) return l.slice(2); + return l; + }); + + // Drop trailing empty lines + while (dedented.length > 0 && dedented[dedented.length - 1].trim() === '') { + dedented.pop(); + } + + return dedented.join('\n'); +} + +/** + * Parse `openshell sandbox get ` output into a SandboxPolicy object. + * Throws a descriptive error if parsing fails. + */ +export function parseSandboxPolicy(rawOutput: string): SandboxPolicy { + const policyYaml = extractPolicyYaml(rawOutput); + + try { + const parsed = yaml.load(policyYaml); + if (typeof parsed !== 'object' || parsed === null) { + throw new Error('Parsed policy is not an object'); + } + return parsed as SandboxPolicy; + } catch (err) { + throw new Error( + `Failed to parse sandbox policy YAML.\n` + + `This may mean the openshell output format has changed.\n` + + `Apply the Hindsight policy manually using the instructions in NEMOCLAW.md.\n` + + `Parse error: ${err}` + ); + } +} + +/** Run `openshell sandbox get ` and return parsed policy */ +export async function readSandboxPolicy(sandboxName: string): Promise { + let stdout: string; + try { + const result = await execFileAsync('openshell', ['sandbox', 'get', sandboxName]); + stdout = result.stdout; + } catch (err: unknown) { + const msg = err instanceof Error ? err.message : String(err); + throw new Error(`Failed to run \`openshell sandbox get ${sandboxName}\`: ${msg}`); + } + + return parseSandboxPolicy(stdout); +} diff --git a/hindsight-integrations/nemoclaw/src/policy-writer.test.ts b/hindsight-integrations/nemoclaw/src/policy-writer.test.ts new file mode 100644 index 00000000..ac2b9a5c --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/policy-writer.test.ts @@ -0,0 +1,107 @@ +import { describe, it, expect } from 'vitest'; +import { hasHindsightPolicy, mergeHindsightPolicy, serializePolicy } from './policy-writer.js'; +import { parseSandboxPolicy } from './policy-reader.js'; +import type { SandboxPolicy } from './types.js'; +import { HINDSIGHT_HOST, OPENCLAW_BINARY } from './types.js'; + +const BASE_POLICY: SandboxPolicy = { + version: 1, + filesystem_policy: { + include_workdir: true, + read_only: ['/usr', '/lib'], + read_write: ['/sandbox', '/tmp'], + }, + network_policies: { + claude_code: { + name: 'claude_code', + endpoints: [ + { + host: 'api.anthropic.com', + port: 443, + rules: [{ allow: { method: '*', path: '/**' } }], + }, + ], + binaries: [{ path: '/usr/local/bin/claude' }], + }, + }, +}; + +describe('hasHindsightPolicy', () => { + it('returns false when no hindsight policy exists', () => { + expect(hasHindsightPolicy(BASE_POLICY)).toBe(false); + }); + + it('returns false when network_policies is undefined', () => { + expect(hasHindsightPolicy({ version: 1 })).toBe(false); + }); + + it('returns true when hindsight policy is present', () => { + const withHindsight = mergeHindsightPolicy(BASE_POLICY); + expect(hasHindsightPolicy(withHindsight)).toBe(true); + }); +}); + +describe('mergeHindsightPolicy', () => { + it('adds the hindsight network policy block', () => { + const result = mergeHindsightPolicy(BASE_POLICY); + expect(result.network_policies?.hindsight).toBeDefined(); + expect(result.network_policies?.hindsight?.endpoints[0].host).toBe(HINDSIGHT_HOST); + }); + + it('preserves all existing network policies', () => { + const result = mergeHindsightPolicy(BASE_POLICY); + expect(result.network_policies?.claude_code).toBeDefined(); + expect(result.network_policies?.claude_code?.name).toBe('claude_code'); + }); + + it('sets the correct binary path', () => { + const result = mergeHindsightPolicy(BASE_POLICY); + const binaries = result.network_policies?.hindsight?.binaries ?? []; + expect(binaries.some(b => b.path === OPENCLAW_BINARY)).toBe(true); + }); + + it('includes GET, POST, and PUT rules', () => { + const result = mergeHindsightPolicy(BASE_POLICY); + const rules = result.network_policies?.hindsight?.endpoints[0].rules ?? []; + const methods = rules.map(r => r.allow.method); + expect(methods).toContain('GET'); + expect(methods).toContain('POST'); + expect(methods).toContain('PUT'); + }); + + it('is idempotent — merging twice yields the same result', () => { + const once = mergeHindsightPolicy(BASE_POLICY); + const twice = mergeHindsightPolicy(once); + expect(JSON.stringify(twice.network_policies?.hindsight)).toBe( + JSON.stringify(once.network_policies?.hindsight) + ); + }); + + it('does not mutate the original policy', () => { + const original = JSON.parse(JSON.stringify(BASE_POLICY)) as SandboxPolicy; + mergeHindsightPolicy(BASE_POLICY); + expect(BASE_POLICY.network_policies?.hindsight).toBeUndefined(); + expect(JSON.stringify(BASE_POLICY)).toBe(JSON.stringify(original)); + }); +}); + +describe('serializePolicy', () => { + it('produces valid YAML that round-trips through parseSandboxPolicy', () => { + const merged = mergeHindsightPolicy(BASE_POLICY); + const yamlStr = serializePolicy(merged); + // Wrap in Policy: header as parseSandboxPolicy expects + const wrapped = 'Policy:\n' + yamlStr.split('\n').map(l => ` ${l}`).join('\n'); + const reparsed = parseSandboxPolicy(wrapped); + expect(reparsed.version).toBe(merged.version); + expect(reparsed.network_policies?.hindsight?.endpoints[0].host).toBe(HINDSIGHT_HOST); + expect(reparsed.network_policies?.claude_code).toBeDefined(); + }); + + it('includes all network policies in output', () => { + const merged = mergeHindsightPolicy(BASE_POLICY); + const yaml = serializePolicy(merged); + expect(yaml).toContain('claude_code:'); + expect(yaml).toContain('hindsight:'); + expect(yaml).toContain(HINDSIGHT_HOST); + }); +}); diff --git a/hindsight-integrations/nemoclaw/src/policy-writer.ts b/hindsight-integrations/nemoclaw/src/policy-writer.ts new file mode 100644 index 00000000..fe77dba1 --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/policy-writer.ts @@ -0,0 +1,59 @@ +import yaml from 'js-yaml'; +import type { SandboxPolicy } from './types.js'; +import { HINDSIGHT_POLICY_NAME, HINDSIGHT_HOST, OPENCLAW_BINARY } from './types.js'; + +const HINDSIGHT_NETWORK_POLICY = { + name: HINDSIGHT_POLICY_NAME, + endpoints: [ + { + host: HINDSIGHT_HOST, + port: 443, + protocol: 'rest', + tls: 'terminate', + enforcement: 'enforce', + rules: [ + { allow: { method: 'GET', path: '/**' } }, + { allow: { method: 'POST', path: '/**' } }, + { allow: { method: 'PUT', path: '/**' } }, + ], + }, + ], + binaries: [{ path: OPENCLAW_BINARY }], +}; + +/** + * Returns true if the policy already has a correct Hindsight network policy entry. + */ +export function hasHindsightPolicy(policy: SandboxPolicy): boolean { + const np = policy.network_policies?.[HINDSIGHT_POLICY_NAME]; + if (!np) return false; + return np.endpoints?.some(e => e.host === HINDSIGHT_HOST) ?? false; +} + +/** + * Merge the Hindsight network policy block into a SandboxPolicy. + * Idempotent — if the block already exists and is correct, returns policy unchanged. + */ +export function mergeHindsightPolicy(policy: SandboxPolicy): SandboxPolicy { + const updated: SandboxPolicy = { + ...policy, + network_policies: { + ...(policy.network_policies ?? {}), + [HINDSIGHT_POLICY_NAME]: HINDSIGHT_NETWORK_POLICY, + }, + }; + return updated; +} + +/** + * Serialize a SandboxPolicy to a YAML string suitable for `openshell policy set`. + */ +export function serializePolicy(policy: SandboxPolicy): string { + return yaml.dump(policy, { + indent: 2, + lineWidth: -1, + noRefs: true, + quotingType: '"', + forceQuotes: false, + }); +} diff --git a/hindsight-integrations/nemoclaw/src/setup.test.ts b/hindsight-integrations/nemoclaw/src/setup.test.ts new file mode 100644 index 00000000..30ed2d2f --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/setup.test.ts @@ -0,0 +1,173 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import type { CliArgs } from './types.js'; + +// Mock all external I/O before importing setup +vi.mock('child_process', () => ({ + execFile: vi.fn(), +})); +vi.mock('./policy-reader.js', () => ({ + readSandboxPolicy: vi.fn(), +})); +vi.mock('./policy-writer.js', () => ({ + hasHindsightPolicy: vi.fn(), + mergeHindsightPolicy: vi.fn(), + serializePolicy: vi.fn(), +})); +vi.mock('./openclaw-config.js', () => ({ + applyPluginConfig: vi.fn(), +})); +vi.mock('fs/promises', () => ({ + writeFile: vi.fn(), + rm: vi.fn(), +})); + +const BASE_ARGS: CliArgs = { + sandbox: 'my-assistant', + apiUrl: 'https://api.hindsight.vectorize.io', + apiToken: 'hsk_test123', + bankPrefix: 'my-sandbox', + skipPolicy: false, + skipPluginInstall: false, + dryRun: false, +}; + +describe('runSetup', () => { + beforeEach(async () => { + vi.clearAllMocks(); + + const { execFile } = await import('child_process'); + const execFileMock = vi.mocked(execFile); + + // Default: all shell commands succeed + execFileMock.mockImplementation((_cmd, _args, callback?: unknown) => { + if (typeof callback === 'function') { + (callback as (err: null, result: { stdout: string; stderr: string }) => void)( + null, { stdout: '', stderr: '' } + ); + } + return {} as ReturnType; + }); + + const { readSandboxPolicy } = await import('./policy-reader.js'); + vi.mocked(readSandboxPolicy).mockResolvedValue({ + version: 1, + network_policies: { claude_code: { name: 'claude_code', endpoints: [] } }, + }); + + const { hasHindsightPolicy, mergeHindsightPolicy, serializePolicy } = await import('./policy-writer.js'); + vi.mocked(hasHindsightPolicy).mockReturnValue(false); + vi.mocked(mergeHindsightPolicy).mockImplementation(p => ({ ...p, network_policies: { ...p.network_policies, hindsight: { name: 'hindsight', endpoints: [] } } })); + vi.mocked(serializePolicy).mockReturnValue('version: 1\n'); + + const { applyPluginConfig } = await import('./openclaw-config.js'); + vi.mocked(applyPluginConfig).mockResolvedValue(undefined); + }); + + it('runs all steps in order for a clean install', async () => { + const { execFile } = await import('child_process'); + const calls: string[] = []; + + vi.mocked(execFile).mockImplementation((cmd, args, callback?: unknown) => { + calls.push(`${cmd} ${(args as string[]).join(' ')}`); + if (typeof callback === 'function') { + (callback as (err: null, result: { stdout: string; stderr: string }) => void)( + null, { stdout: '', stderr: '' } + ); + } + return {} as ReturnType; + }); + + const { runSetup } = await import('./setup.js'); + await runSetup(BASE_ARGS); + + expect(calls.some(c => c.includes('which openshell'))).toBe(true); + expect(calls.some(c => c.includes('which openclaw'))).toBe(true); + expect(calls.some(c => c.includes('openclaw plugins install @vectorize-io/hindsight-openclaw'))).toBe(true); + expect(calls.some(c => c.includes('openshell policy set my-assistant'))).toBe(true); + expect(calls.some(c => c.includes('openclaw gateway restart'))).toBe(true); + }); + + it('skips plugin install when --skip-plugin-install is set', async () => { + const { execFile } = await import('child_process'); + const calls: string[] = []; + vi.mocked(execFile).mockImplementation((cmd, args, callback?: unknown) => { + calls.push(`${cmd} ${(args as string[]).join(' ')}`); + if (typeof callback === 'function') { + (callback as (err: null, result: { stdout: string; stderr: string }) => void)( + null, { stdout: '', stderr: '' } + ); + } + return {} as ReturnType; + }); + + const { runSetup } = await import('./setup.js'); + await runSetup({ ...BASE_ARGS, skipPluginInstall: true }); + expect(calls.some(c => c.includes('plugins install'))).toBe(false); + }); + + it('skips policy update when --skip-policy is set', async () => { + const { runSetup } = await import('./setup.js'); + const { readSandboxPolicy } = await import('./policy-reader.js'); + await runSetup({ ...BASE_ARGS, skipPolicy: true }); + expect(vi.mocked(readSandboxPolicy)).not.toHaveBeenCalled(); + }); + + it('skips policy set when Hindsight policy already exists', async () => { + const { hasHindsightPolicy } = await import('./policy-writer.js'); + vi.mocked(hasHindsightPolicy).mockReturnValue(true); + + const { execFile } = await import('child_process'); + const calls: string[] = []; + vi.mocked(execFile).mockImplementation((cmd, args, callback?: unknown) => { + calls.push(`${cmd} ${(args as string[]).join(' ')}`); + if (typeof callback === 'function') { + (callback as (err: null, result: { stdout: string; stderr: string }) => void)( + null, { stdout: '', stderr: '' } + ); + } + return {} as ReturnType; + }); + + const { runSetup } = await import('./setup.js'); + await runSetup(BASE_ARGS); + expect(calls.some(c => c.includes('openshell policy set'))).toBe(false); + }); + + it('does not execute any shell commands in dry-run mode', async () => { + const { execFile } = await import('child_process'); + const { applyPluginConfig } = await import('./openclaw-config.js'); + const { writeFile } = await import('fs/promises'); + + const { runSetup } = await import('./setup.js'); + await runSetup({ ...BASE_ARGS, dryRun: true }); + + // which checks still run (preflight), but no actual commands + const execCalls = vi.mocked(execFile).mock.calls.map(c => `${c[0]} ${(c[1] as string[]).join(' ')}`); + expect(execCalls.some(c => c.includes('plugins install'))).toBe(false); + expect(execCalls.some(c => c.includes('policy set'))).toBe(false); + expect(execCalls.some(c => c.includes('gateway restart'))).toBe(false); + expect(vi.mocked(applyPluginConfig)).not.toHaveBeenCalled(); + expect(vi.mocked(writeFile)).not.toHaveBeenCalled(); + }); + + it('fails early if openshell is not on PATH', async () => { + const { execFile } = await import('child_process'); + vi.mocked(execFile).mockImplementation((cmd, args, callback?: unknown) => { + if (cmd === 'which' && (args as string[])[0] === 'openshell') { + if (typeof callback === 'function') { + (callback as (err: Error) => void)(new Error('not found')); + } + } else { + if (typeof callback === 'function') { + (callback as (err: null, result: { stdout: string; stderr: string }) => void)( + null, { stdout: '', stderr: '' } + ); + } + } + return {} as ReturnType; + }); + + const { runSetup } = await import('./setup.js'); + await expect(runSetup(BASE_ARGS)).rejects.toThrow('openshell'); + }); +}); diff --git a/hindsight-integrations/nemoclaw/src/setup.ts b/hindsight-integrations/nemoclaw/src/setup.ts new file mode 100644 index 00000000..745948d1 --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/setup.ts @@ -0,0 +1,148 @@ +import { execFile } from 'child_process'; +import { promisify } from 'util'; +import { writeFile, rm } from 'fs/promises'; +import { tmpdir } from 'os'; +import { join } from 'path'; +import { randomBytes } from 'crypto'; +import type { CliArgs } from './types.js'; +import { readSandboxPolicy } from './policy-reader.js'; +import { hasHindsightPolicy, mergeHindsightPolicy, serializePolicy } from './policy-writer.js'; +import { applyPluginConfig } from './openclaw-config.js'; + +const execFileAsync = promisify(execFile); + +function log(msg: string) { + process.stdout.write(`${msg}\n`); +} + +function step(n: number, msg: string) { + log(`\n[${n}] ${msg}`); +} + +async function which(bin: string): Promise { + try { + await execFileAsync('which', [bin]); + return true; + } catch { + return false; + } +} + +export async function runSetup(args: CliArgs): Promise { + log('\nhindsight-nemoclaw setup'); + log('─'.repeat(40)); + + // Step 0 — Preflight + step(0, 'Preflight checks...'); + const [hasOpenshell, hasOpenclaw] = await Promise.all([which('openshell'), which('openclaw')]); + if (!hasOpenshell) { + throw new Error('`openshell` not found on PATH. Install it from https://openshell.ai'); + } + if (!hasOpenclaw) { + throw new Error('`openclaw` not found on PATH. Install it from https://openclaw.ai'); + } + log(' ✓ openshell found'); + log(' ✓ openclaw found'); + + // Step 1 — Install hindsight-openclaw plugin + if (!args.skipPluginInstall) { + step(1, 'Installing @vectorize-io/hindsight-openclaw plugin...'); + if (args.dryRun) { + log(' [dry-run] would run: openclaw plugins install @vectorize-io/hindsight-openclaw'); + } else { + const { stdout } = await execFileAsync('openclaw', [ + 'plugins', 'install', '@vectorize-io/hindsight-openclaw', + ]); + log(stdout.trim() || ' ✓ Plugin installed'); + } + } else { + step(1, 'Skipping plugin install (--skip-plugin-install)'); + } + + // Step 2 — Configure ~/.openclaw/openclaw.json + step(2, 'Configuring plugin in ~/.openclaw/openclaw.json...'); + const pluginConfig = { + hindsightApiUrl: args.apiUrl, + hindsightApiToken: args.apiToken, + llmProvider: 'claude-code', + dynamicBankId: false, + bankIdPrefix: args.bankPrefix, + }; + if (args.dryRun) { + log(` [dry-run] would write plugin config to ~/.openclaw/openclaw.json`); + log(` config: ${JSON.stringify(pluginConfig, null, 4).split('\n').join('\n ')}`); + } else { + await applyPluginConfig(pluginConfig); + log(` ✓ Plugin config written (bank: ${args.bankPrefix}-openclaw)`); + } + + // Step 3 — Apply OpenShell network policy + if (!args.skipPolicy) { + step(3, `Applying Hindsight network policy to sandbox "${args.sandbox}"...`); + + const currentPolicy = await readSandboxPolicy(args.sandbox); + + if (hasHindsightPolicy(currentPolicy)) { + log(' ✓ Hindsight policy already present — skipping'); + } else { + const updatedPolicy = mergeHindsightPolicy(currentPolicy); + const policyYaml = serializePolicy(updatedPolicy); + + if (args.dryRun) { + log(' [dry-run] would apply policy:'); + log(policyYaml.split('\n').map(l => ` ${l}`).join('\n')); + } else { + const tmpFile = join(tmpdir(), `hindsight-policy-${randomBytes(6).toString('hex')}.yaml`); + try { + await writeFile(tmpFile, policyYaml, 'utf8'); + const { stdout } = await execFileAsync('openshell', [ + 'policy', 'set', args.sandbox, '--policy', tmpFile, '--wait', + ]); + log(stdout.trim() || ` ✓ Policy applied to sandbox "${args.sandbox}"`); + } finally { + await rm(tmpFile, { force: true }); + } + } + } + } else { + step(3, 'Skipping policy update (--skip-policy)'); + log(' Add the following block to your sandbox network_policies manually:'); + log(''); + log(' hindsight:'); + log(' name: hindsight'); + log(' endpoints:'); + log(' - host: api.hindsight.vectorize.io'); + log(' port: 443'); + log(' protocol: rest'); + log(' tls: terminate'); + log(' enforcement: enforce'); + log(' rules:'); + log(' - allow: { method: GET, path: /** }'); + log(' - allow: { method: POST, path: /** }'); + log(' - allow: { method: PUT, path: /** }'); + log(' binaries:'); + log(' - path: /usr/local/bin/openclaw'); + } + + // Step 4 — Restart gateway + step(4, 'Restarting OpenClaw gateway...'); + if (args.dryRun) { + log(' [dry-run] would run: openclaw gateway restart'); + } else { + await execFileAsync('openclaw', ['gateway', 'restart']); + log(' ✓ Gateway restarted'); + } + + log('\n' + '─'.repeat(40)); + log('✓ Setup complete!\n'); + log(` Bank ID: ${args.bankPrefix}-openclaw`); + log(` API URL: ${args.apiUrl}`); + log(''); + log(' Watch gateway logs to confirm:'); + log(' grep Hindsight ~/.openclaw/logs/gateway.log | tail -5'); + log(' Expected: [Hindsight] ✓ Ready (external API mode)'); + log(''); + log(' Test memory retention:'); + log(` openclaw agent --agent main --session-id test-1 -m "My name is Ben."`); + log(` openclaw agent --agent main --session-id test-2 -m "What do you remember about me?"`); +} diff --git a/hindsight-integrations/nemoclaw/src/types.ts b/hindsight-integrations/nemoclaw/src/types.ts new file mode 100644 index 00000000..ca0897a6 --- /dev/null +++ b/hindsight-integrations/nemoclaw/src/types.ts @@ -0,0 +1,63 @@ +export interface CliArgs { + sandbox: string; + apiUrl: string; + apiToken: string; + bankPrefix: string; + skipPolicy: boolean; + skipPluginInstall: boolean; + dryRun: boolean; +} + +export interface PolicyEndpointRule { + allow: { + method: string; + path: string; + }; +} + +export interface PolicyEndpoint { + host: string; + port: number; + protocol?: string; + tls?: string; + enforcement?: string; + access?: string; + rules?: PolicyEndpointRule[]; +} + +export interface PolicyBinary { + path: string; +} + +export interface NetworkPolicy { + name: string; + endpoints: PolicyEndpoint[]; + binaries?: PolicyBinary[]; +} + +export interface FilesystemPolicy { + include_workdir?: boolean; + read_only?: string[]; + read_write?: string[]; +} + +export interface Landlock { + compatibility?: string; +} + +export interface ProcessPolicy { + run_as_user?: string; + run_as_group?: string; +} + +export interface SandboxPolicy { + version?: number; + filesystem_policy?: FilesystemPolicy; + landlock?: Landlock; + process?: ProcessPolicy; + network_policies?: Record; +} + +export const HINDSIGHT_POLICY_NAME = 'hindsight'; +export const HINDSIGHT_HOST = 'api.hindsight.vectorize.io'; +export const OPENCLAW_BINARY = '/usr/local/bin/openclaw'; diff --git a/hindsight-integrations/nemoclaw/tsconfig.json b/hindsight-integrations/nemoclaw/tsconfig.json new file mode 100644 index 00000000..d1f4cf02 --- /dev/null +++ b/hindsight-integrations/nemoclaw/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "node", + "declaration": true, + "outDir": "./dist", + "rootDir": "./src", + "strict": true, + "esModuleInterop": true, + "skipLibCheck": true, + "forceConsistentCasingInFileNames": true, + "resolveJsonModule": true + }, + "include": ["src/**/*"], + "exclude": ["node_modules", "dist", "src/**/*.test.ts"] +} diff --git a/hindsight-integrations/nemoclaw/vitest.config.ts b/hindsight-integrations/nemoclaw/vitest.config.ts new file mode 100644 index 00000000..7dd13254 --- /dev/null +++ b/hindsight-integrations/nemoclaw/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +});