diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index dc34b4d54..db1d8060f 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -45,6 +45,7 @@ jobs: integrations-ag2: ${{ steps.filter.outputs.integrations-ag2 }} integrations-hermes: ${{ steps.filter.outputs.integrations-hermes }} integrations-llamaindex: ${{ steps.filter.outputs.integrations-llamaindex }} + integrations-opencode: ${{ steps.filter.outputs.integrations-opencode }} dev: ${{ steps.filter.outputs.dev }} ci: ${{ steps.filter.outputs.ci }} # Secrets are available for internal PRs, pull_request_review, and workflow_dispatch. @@ -117,6 +118,8 @@ jobs: - 'hindsight-integrations/hermes/**' integrations-llamaindex: - 'hindsight-integrations/llamaindex/**' + integrations-opencode: + - 'hindsight-integrations/opencode/**' dev: - 'hindsight-dev/**' ci: @@ -326,6 +329,37 @@ jobs: working-directory: ./hindsight-integrations/ai-sdk run: npm run test:deno + build-opencode-integration: + needs: [detect-changes] + if: >- + github.event_name != 'pull_request_review' && + (github.event_name == 'workflow_dispatch' || + needs.detect-changes.outputs.integrations-opencode == 'true' || + needs.detect-changes.outputs.ci == 'true') + runs-on: ubuntu-latest + + steps: + - uses: actions/checkout@v6 + with: + ref: ${{ github.event.pull_request.head.sha || '' }} + + - name: Set up Node.js + uses: actions/setup-node@v6 + with: + node-version: '22' + + - name: Install dependencies + working-directory: ./hindsight-integrations/opencode + run: npm ci + + - name: Run tests + working-directory: ./hindsight-integrations/opencode + run: npm test + + - name: Build + working-directory: ./hindsight-integrations/opencode + run: npm run build + build-chat-integration: needs: [detect-changes] if: >- @@ -2427,6 +2461,7 @@ jobs: - test-codex-integration - build-ai-sdk-integration - test-ai-sdk-integration-deno + - build-opencode-integration - build-chat-integration - build-control-plane - build-docs diff --git a/hindsight-docs/docs-integrations/opencode.md b/hindsight-docs/docs-integrations/opencode.md new file mode 100644 index 000000000..93057dc98 --- /dev/null +++ b/hindsight-docs/docs-integrations/opencode.md @@ -0,0 +1,141 @@ +--- +sidebar_position: 20 +title: "OpenCode Persistent Memory with Hindsight | Integration" +description: "Add long-term memory to OpenCode with Hindsight. Automatically captures conversations and recalls relevant context across coding sessions." +--- + +# OpenCode + +Persistent long-term memory plugin for [OpenCode](https://opencode.ai) using [Hindsight](https://vectorize.io/hindsight). Automatically captures conversations, recalls relevant context on session start, and provides retain/recall/reflect tools the agent can call directly. + +## Quick Start + +```bash +# 1. Install the plugin +npm install @vectorize-io/opencode-hindsight +``` + +Add to your `opencode.json`: + +```json +{ + "plugin": ["@vectorize-io/opencode-hindsight"] +} +``` + +```bash +# 2. Configure your Hindsight server +export HINDSIGHT_API_URL="http://localhost:8888" + +# Optional: API key for Hindsight Cloud +export HINDSIGHT_API_TOKEN="your-api-key" + +# 3. Start OpenCode — the plugin activates automatically +opencode +``` + +## Features + +### Custom Tools + +The plugin registers three tools the agent can call explicitly: + +| Tool | Description | +|---|---| +| `hindsight_retain` | Store information in long-term memory | +| `hindsight_recall` | Search long-term memory for relevant information | +| `hindsight_reflect` | Generate a synthesized answer from long-term memory | + +### Auto-Retain + +When the session goes idle (`session.idle` event), the plugin automatically retains the conversation transcript to Hindsight. Configurable via `retainEveryNTurns` to control frequency. + +### Session Recall + +When a new session starts, the plugin recalls relevant project context and injects it into the system prompt, giving the agent access to memories from prior sessions. + +### Compaction Hook + +When OpenCode compacts the context window, the plugin: +1. Retains the current conversation before compaction +2. Recalls relevant memories and injects them into the compaction context + +This ensures memories survive context window trimming. + +## Configuration + +### Plugin Options + +```json +{ + "plugin": [ + ["@vectorize-io/opencode-hindsight", { + "hindsightApiUrl": "http://localhost:8888", + "hindsightApiToken": "your-api-key", + "bankId": "my-project", + "autoRecall": true, + "autoRetain": true, + "recallBudget": "mid", + "retainEveryNTurns": 10, + "debug": false + }] + ] +} +``` + +### Config File + +Create `~/.hindsight/opencode.json` for persistent configuration that applies across all projects: + +```json +{ + "hindsightApiUrl": "http://localhost:8888", + "hindsightApiToken": "your-api-key", + "recallBudget": "mid" +} +``` + +### Environment Variables + +| Variable | Description | Default | +|---|---|---| +| `HINDSIGHT_API_URL` | Hindsight API base URL | *(required)* | +| `HINDSIGHT_API_TOKEN` | API key for authentication | | +| `HINDSIGHT_BANK_ID` | Static memory bank ID | `opencode` | +| `HINDSIGHT_AGENT_NAME` | Agent name for dynamic bank IDs | `opencode` | +| `HINDSIGHT_AUTO_RECALL` | Auto-recall on session start | `true` | +| `HINDSIGHT_AUTO_RETAIN` | Auto-retain on session idle | `true` | +| `HINDSIGHT_RETAIN_MODE` | `full-session` or `last-turn` | `full-session` | +| `HINDSIGHT_RECALL_BUDGET` | Recall budget: `low`, `mid`, `high` | `mid` | +| `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens for recall results | `1024` | +| `HINDSIGHT_DYNAMIC_BANK_ID` | Enable dynamic bank ID derivation | `false` | +| `HINDSIGHT_BANK_MISSION` | Bank mission/context for reflect | | +| `HINDSIGHT_DEBUG` | Enable debug logging to stderr | `false` | + +Configuration priority (later wins): defaults < `~/.hindsight/opencode.json` < plugin options < env vars. + +## Dynamic Bank IDs + +For multi-project isolation, enable dynamic bank ID derivation: + +```bash +export HINDSIGHT_DYNAMIC_BANK_ID=true +``` + +The bank ID is composed from granularity fields (default: `agent::project`). Supported fields: `agent`, `project`, `channel`, `user`. + +For multi-user scenarios (e.g., shared agent serving multiple users): + +```bash +export HINDSIGHT_CHANNEL_ID="slack-general" +export HINDSIGHT_USER_ID="user123" +``` + +## How It Works + +1. **Plugin loads** when OpenCode starts — creates a `HindsightClient`, derives the bank ID, and registers tools + hooks +2. **Session starts** — `session.created` event triggers, plugin marks session for recall injection +3. **System transform** — on the first LLM call, recalled memories are injected into the system prompt +4. **Agent works** — can call `hindsight_recall` and `hindsight_retain` explicitly during the session +5. **Session idles** — `session.idle` event triggers auto-retain of the conversation +6. **Compaction** — if the context window fills up, memories are preserved through the compaction diff --git a/hindsight-docs/src/data/integrations.json b/hindsight-docs/src/data/integrations.json index d2faeb8eb..88b0b08d2 100644 --- a/hindsight-docs/src/data/integrations.json +++ b/hindsight-docs/src/data/integrations.json @@ -180,6 +180,16 @@ "link": "/sdks/integrations/autogen", "icon": "/img/icons/autogen.svg" }, + { + "id": "opencode", + "name": "OpenCode", + "description": "Persistent long-term memory plugin for OpenCode. Auto-retains conversations, recalls context on session start, and provides retain/recall/reflect tools.", + "type": "official", + "by": "hindsight", + "category": "tool", + "link": "/sdks/integrations/opencode", + "icon": "/img/icons/opencode.svg" + }, { "id": "hindclaw", "name": "HindClaw", diff --git a/hindsight-docs/static/img/icons/opencode.svg b/hindsight-docs/static/img/icons/opencode.svg new file mode 100644 index 000000000..c3d1d35ba --- /dev/null +++ b/hindsight-docs/static/img/icons/opencode.svg @@ -0,0 +1,4 @@ + + + OC + diff --git a/hindsight-integrations/opencode/README.md b/hindsight-integrations/opencode/README.md new file mode 100644 index 000000000..0e6b377c6 --- /dev/null +++ b/hindsight-integrations/opencode/README.md @@ -0,0 +1,144 @@ +# @vectorize-io/opencode-hindsight + +Hindsight memory plugin for [OpenCode](https://opencode.ai) — give your AI coding agent persistent long-term memory across sessions. + +## Features + +- **Custom tools**: `hindsight_retain`, `hindsight_recall`, `hindsight_reflect` — the agent calls these explicitly +- **Auto-retain**: Captures conversation on `session.idle` and stores to Hindsight +- **Memory injection**: Recalls relevant memories when a new session starts +- **Compaction hook**: Injects memories during context compaction so they survive window trimming + +## Quick Start + +### 1. Install + +```bash +npm install @vectorize-io/opencode-hindsight +``` + +### 2. Configure + +Add to your `opencode.json`: + +```json +{ + "plugin": ["@vectorize-io/opencode-hindsight"] +} +``` + +### 3. Set Environment Variables + +```bash +# Required: Hindsight API URL +export HINDSIGHT_API_URL="http://localhost:8888" + +# Optional: API key for Hindsight Cloud +export HINDSIGHT_API_TOKEN="your-api-key" + +# Optional: Override the memory bank ID +export HINDSIGHT_BANK_ID="my-project" +``` + +## Configuration + +### Plugin Options + +Pass options directly in `opencode.json`: + +```json +{ + "plugin": [ + ["@vectorize-io/opencode-hindsight", { + "hindsightApiUrl": "http://localhost:8888", + "bankId": "my-project", + "autoRecall": true, + "autoRetain": true, + "recallBudget": "mid" + }] + ] +} +``` + +### Config File + +Create `~/.hindsight/opencode.json` for persistent configuration: + +```json +{ + "hindsightApiUrl": "http://localhost:8888", + "hindsightApiToken": "your-api-key", + "recallBudget": "mid", + "retainEveryNTurns": 10, + "debug": false +} +``` + +### Environment Variables + +| Variable | Description | Default | +|---|---|---| +| `HINDSIGHT_API_URL` | Hindsight API base URL | (required) | +| `HINDSIGHT_API_TOKEN` | API key for authentication | (none) | +| `HINDSIGHT_BANK_ID` | Static memory bank ID | `opencode` | +| `HINDSIGHT_AGENT_NAME` | Agent name for dynamic bank IDs | `opencode` | +| `HINDSIGHT_AUTO_RECALL` | Auto-recall on session start | `true` | +| `HINDSIGHT_AUTO_RETAIN` | Auto-retain on session idle | `true` | +| `HINDSIGHT_RETAIN_MODE` | `full-session` or `last-turn` | `full-session` | +| `HINDSIGHT_RECALL_BUDGET` | Recall budget: `low`, `mid`, `high` | `mid` | +| `HINDSIGHT_RECALL_MAX_TOKENS` | Max tokens for recall results | `1024` | +| `HINDSIGHT_DYNAMIC_BANK_ID` | Enable dynamic bank ID derivation | `false` | +| `HINDSIGHT_BANK_MISSION` | Bank mission/context | (none) | +| `HINDSIGHT_DEBUG` | Enable debug logging | `false` | + +### Configuration Priority + +Settings are loaded in this order (later wins): + +1. Built-in defaults +2. `~/.hindsight/opencode.json` +3. Plugin options from `opencode.json` +4. Environment variables + +## Tools + +### `hindsight_retain` + +Store information in long-term memory. The agent uses this to save important facts, user preferences, project context, and decisions. + +### `hindsight_recall` + +Search long-term memory. The agent uses this proactively before answering questions where prior context would help. + +### `hindsight_reflect` + +Generate a synthesized answer from long-term memory. Unlike recall (raw memories), reflect produces a coherent summary. + +## Dynamic Bank IDs + +For multi-project setups, enable dynamic bank ID derivation: + +```bash +export HINDSIGHT_DYNAMIC_BANK_ID=true +``` + +The bank ID is composed from granularity fields (default: `agent::project`). Supported fields: `agent`, `project`, `channel`, `user`. + +**Note:** The bank ID is derived once when the plugin loads, from environment variables set before OpenCode starts. These dimensions are process-scoped — they don't change per session within a running OpenCode process. For per-user isolation, set the env vars before launching each user's OpenCode instance: + +```bash +export HINDSIGHT_CHANNEL_ID="slack-general" +export HINDSIGHT_USER_ID="user123" +``` + +## Development + +```bash +npm install +npm test # Run tests +npm run build # Build to dist/ +``` + +## License + +MIT diff --git a/hindsight-integrations/opencode/package-lock.json b/hindsight-integrations/opencode/package-lock.json new file mode 100644 index 000000000..dc49c72ea --- /dev/null +++ b/hindsight-integrations/opencode/package-lock.json @@ -0,0 +1,2686 @@ +{ + "name": "@vectorize-io/opencode-hindsight", + "version": "0.1.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@vectorize-io/opencode-hindsight", + "version": "0.1.0", + "license": "MIT", + "dependencies": { + "@vectorize-io/hindsight-client": "^0.4.19" + }, + "devDependencies": { + "@opencode-ai/plugin": "^1.3.13", + "@types/node": "^22.0.0", + "tsup": "^8.5.1", + "typescript": "^5.7.0", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=22" + }, + "peerDependencies": { + "@opencode-ai/plugin": ">=1.0.0" + } + }, + "node_modules/@emnapi/core": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.9.2.tgz", + "integrity": "sha512-UC+ZhH3XtczQYfOlu3lNEkdW/p4dsJ1r/bP7H8+rhao3TTTMO1ATq/4DdIi23XuGoFY+Cz0JmCbdVl0hz9jZcA==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "@emnapi/wasi-threads": "1.2.1", + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/runtime": { + "version": "1.9.2", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.2.tgz", + "integrity": "sha512-3U4+MIWHImeyu1wnmVygh5WlgfYDtyf0k8AbLhMFxOipihf6nrWC4syIm/SwEeec0mNSafiiNnMJwbza/Is6Lw==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.1.tgz", + "integrity": "sha512-uTII7OYF+/Mes/MrcIOYp5yOtSMLBWSIoLPpcgwipoiKbli6k322tcoFsxoIIxPDqW01SQGAgko4EzZi2BNv2w==", + "dev": true, + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.5.tgz", + "integrity": "sha512-nGsF/4C7uzUj+Nj/4J+Zt0bYQ6bz33Phz8Lb2N80Mti1HjGclTJdXZ+9APC4kLvONbjxN1zfvYNd8FEcbBK/MQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.5.tgz", + "integrity": "sha512-Cv781jd0Rfj/paoNrul1/r4G0HLvuFKYh7C9uHZ2Pl8YXstzvCyyeWENTFR9qFnRzNMCjXmsulZuvosDg10Mog==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.5.tgz", + "integrity": "sha512-Oeghq+XFgh1pUGd1YKs4DDoxzxkoUkvko+T/IVKwlghKLvvjbGFB3ek8VEDBmNvqhwuL0CQS3cExdzpmUyIrgA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.5.tgz", + "integrity": "sha512-nQD7lspbzerlmtNOxYMFAGmhxgzn8Z7m9jgFkh6kpkjsAhZee1w8tJW3ZlW+N9iRePz0oPUDrYrXidCPSImD0Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.5.tgz", + "integrity": "sha512-I+Ya/MgC6rr8oRWGRDF3BXDfP8K1BVUggHqN6VI2lUZLdDi1IM1v2cy0e3lCPbP+pVcK3Tv8cgUhHse1kaNZZw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.5.tgz", + "integrity": "sha512-MCjQUtC8wWJn/pIPM7vQaO69BFgwPD1jriEdqwTCKzWjGgkMbcg+M5HzrOhPhuYe1AJjXlHmD142KQf+jnYj8A==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.5.tgz", + "integrity": "sha512-X6xVS+goSH0UelYXnuf4GHLwpOdc8rgK/zai+dKzBMnncw7BTQIwquOodE7EKvY2UVUetSqyAfyZC1D+oqLQtg==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.5.tgz", + "integrity": "sha512-233X1FGo3a8x1ekLB6XT69LfZ83vqz+9z3TSEQCTYfMNY880A97nr81KbPcAMl9rmOFp11wO0dP+eB18KU/Ucg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.5.tgz", + "integrity": "sha512-0wkVrYHG4sdCCN/bcwQ7yYMXACkaHc3UFeaEOwSVW6e5RycMageYAFv+JS2bKLwHyeKVUvtoVH+5/RHq0fgeFw==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.5.tgz", + "integrity": "sha512-euKkilsNOv7x/M1NKsx5znyprbpsRFIzTV6lWziqJch7yWYayfLtZzDxDTl+LSQDJYAjd9TVb/Kt5UKIrj2e4A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.5.tgz", + "integrity": "sha512-hVRQX4+P3MS36NxOy24v/Cdsimy/5HYePw+tmPqnNN1fxV0bPrFWR6TMqwXPwoTM2VzbkA+4lbHWUKDd5ZDA/w==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.5.tgz", + "integrity": "sha512-mKqqRuOPALI8nDzhOBmIS0INvZOOFGGg5n1osGIXAx8oersceEbKd4t1ACNTHM3sJBXGFAlEgqM+svzjPot+ZQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.5.tgz", + "integrity": "sha512-EE/QXH9IyaAj1qeuIV5+/GZkBTipgGO782Ff7Um3vPS9cvLhJJeATy4Ggxikz2inZ46KByamMn6GqtqyVjhenA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.5.tgz", + "integrity": "sha512-0V2iF1RGxBf1b7/BjurA5jfkl7PtySjom1r6xOK2q9KWw/XCpAdtB6KNMO+9xx69yYfSCRR9FE0TyKfHA2eQMw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.5.tgz", + "integrity": "sha512-rYxThBx6G9HN6tFNuvB/vykeLi4VDsm5hE5pVwzqbAjZEARQrWu3noZSfbEnPZ/CRXP3271GyFk/49up2W190g==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.5.tgz", + "integrity": "sha512-uEP2q/4qgd8goEUc4QIdU/1P2NmEtZ/zX5u3OpLlCGhJIuBIv0s0wr7TB2nBrd3/A5XIdEkkS5ZLF0ULuvaaYQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.5.tgz", + "integrity": "sha512-+Gq47Wqq6PLOOZuBzVSII2//9yyHNKZLuwfzCemqexqOQCSz0zy0O26kIzyp9EMNMK+nZ0tFHBZrCeVUuMs/ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.5.tgz", + "integrity": "sha512-3F/5EG8VHfN/I+W5cO1/SV2H9Q/5r7vcHabMnBqhHK2lTWOh3F8vixNzo8lqxrlmBtZVFpW8pmITHnq54+Tq4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.5.tgz", + "integrity": "sha512-28t+Sj3CPN8vkMOlZotOmDgilQwVvxWZl7b8rxpn73Tt/gCnvrHxQUMng4uu3itdFvrtba/1nHejvxqz8xgEMA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.5.tgz", + "integrity": "sha512-Doz/hKtiuVAi9hMsBMpwBANhIZc8l238U2Onko3t2xUp8xtM0ZKdDYHMnm/qPFVthY8KtxkXaocwmMh6VolzMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.5.tgz", + "integrity": "sha512-WfGVaa1oz5A7+ZFPkERIbIhKT4olvGl1tyzTRaB5yoZRLqC0KwaO95FeZtOdQj/oKkjW57KcVF944m62/0GYtA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.5.tgz", + "integrity": "sha512-Xh+VRuh6OMh3uJ0JkCjI57l+DVe7VRGBYymen8rFPnTVgATBwA6nmToxM2OwTlSvrnWpPKkrQUj93+K9huYC6A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.5.tgz", + "integrity": "sha512-aC1gpJkkaUADHuAdQfuVTnqVUTLqqUNhAvEwHwVWcnVVZvNlDPGA0UveZsfXJJ9T6k9Po4eHi3c02gbdwO3g6w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.5.tgz", + "integrity": "sha512-0UNx2aavV0fk6UpZcwXFLztA2r/k9jTUa7OW7SAea1VYUhkug99MW1uZeXEnPn5+cHOd0n8myQay6TlFnBR07w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.5.tgz", + "integrity": "sha512-5nlJ3AeJWCTSzR7AEqVjT/faWyqKU86kCi1lLmxVqmNR+j4HrYdns+eTGjS/vmrzCIe8inGQckUadvS0+JkKdQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.5.tgz", + "integrity": "sha512-PWypQR+d4FLfkhBIV+/kHsUELAnMpx1bRvvsn3p+/sAERbnCzFrtDRG2Xw5n+2zPxBK2+iaP+vetsRl4Ti7WgA==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0.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/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.1.2.tgz", + "integrity": "sha512-sNXv5oLJ7ob93xkZ1XnxisYhGYXfaG9f65/ZgYuAu3qt7b3NadcOEhLvx28hv31PgX8SZJRYrAIPQilQmFpLVw==", + "dev": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@tybys/wasm-util": "^0.10.1" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" + }, + "peerDependencies": { + "@emnapi/core": "^1.7.1", + "@emnapi/runtime": "^1.7.1" + } + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.3.13.tgz", + "integrity": "sha512-zHgtWfdDz8Wu8srE8f8HUtPT9i6c3jTmgQKoFZUZ+RR5CMQF1kAlb1cxeEe9Xm2DRNFVJog9Cv/G1iUHYgXSUQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@opencode-ai/sdk": "1.3.13", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.1.95", + "@opentui/solid": ">=0.1.95" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.3.13.tgz", + "integrity": "sha512-/M6HlNnba+xf1EId6qFb2tG0cvq0db3PCQDug1glrf8wYOU57LYNF8WvHX9zoDKPTMv0F+O4pcP/8J+WvDaxHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@oxc-project/types": { + "version": "0.122.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.122.0.tgz", + "integrity": "sha512-oLAl5kBpV4w69UtFZ9xqcmTi+GENWOcPF7FCrczTiBbmC0ibXxCwyvZGbO39rCVEuLGAZM84DH0pUIyyv/YJzA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/Boshen" + } + }, + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-pv1y2Fv0JybcykuiiD3qBOBdz6RteYojRFY1d+b95WVuzx211CRh+ytI/+9iVyWQ6koTh5dawe4S/yRfOFjgaA==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-cFYr6zTG/3PXXF3pUO+umXxt1wkRK/0AYT8lDwuqvRC+LuKYWSAQAQZjCWDQpAH172ZV6ieYrNnFzVVcnSflAg==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-ZCsYknnHzeXYps0lGBz8JrF37GpE9bFVefrlmDrAQhOEi4IOIlcoU1+FwHEtyXGx2VkYAvhu7dyBf75EJQffBw==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.0.0-rc.12.tgz", + "integrity": "sha512-dMLeprcVsyJsKolRXyoTH3NL6qtsT0Y2xeuEA8WQJquWFXkEC4bcu1rLZZSnZRMtAqwtrF/Ib9Ddtpa/Gkge9Q==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.0.0-rc.12.tgz", + "integrity": "sha512-YqWjAgGC/9M1lz3GR1r1rP79nMgo3mQiiA+Hfo+pvKFK1fAJ1bCi0ZQVh8noOqNacuY1qIcfyVfP6HoyBRZ85Q==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-/I5AS4cIroLpslsmzXfwbe5OmWvSsrFuEw3mwvbQ1kDxJ822hFHIx+vsN/TAzNVyepI/j/GSzrtCIwQPeKCLIg==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-V6/wZztnBqlx5hJQqNWwFdxIKN0m38p8Jas+VoSfgH54HSj9tKTt1dZvG6JRHcjh6D7TvrJPWFGaY9UBVOaWPw==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-AP3E9BpcUYliZCxa3w5Kwj9OtEVDYK6sVoUzy4vTOJsjPOgdaJZKFmN4oOlX0Wp0RPV2ETfmIra9x1xuayFB7g==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-nWwpvUSPkoFmZo0kQazZYOrT7J5DGOJ/+QHHzjvNlooDZED8oH82Yg67HvehPPLAg5fUff7TfWFHQS8IV1n3og==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.0.0-rc.12.tgz", + "integrity": "sha512-RNrafz5bcwRy+O9e6P8Z/OCAJW/A+qtBczIqVYwTs14pf4iV1/+eKEjdOUta93q2TsT/FI0XYDP3TCky38LMAg==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.0.0-rc.12.tgz", + "integrity": "sha512-Jpw/0iwoKWx3LJ2rc1yjFrj+T7iHZn2JDg1Yny1ma0luviFS4mhAIcd1LFNxK3EYu3DHWCps0ydXQ5i/rrJ2ig==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.0.0-rc.12.tgz", + "integrity": "sha512-vRugONE4yMfVn0+7lUKdKvN4D5YusEiPilaoO2sgUWpCvrncvWgPMzK00ZFFJuiPgLwgFNP5eSiUlv2tfc+lpA==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-wasm32-wasi/-/binding-wasm32-wasi-1.0.0-rc.12.tgz", + "integrity": "sha512-ykGiLr/6kkiHc0XnBfmFJuCjr5ZYKKofkx+chJWDjitX+KsJuAmrzWhwyOMSHzPhzOHOy7u9HlFoa5MoAOJ/Zg==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-5eOND4duWkwx1AzCxadcOrNeighiLwMInEADT0YM7xeEOOFcovWZCq8dadXgcRHSf3Ulh1kFo/qvzoFiCLOL1Q==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.0.0-rc.12.tgz", + "integrity": "sha512-PyqoipaswDLAZtot351MLhrlrh6lcZPo2LSYE+VDxbVk24LVKAGOuE4hb8xZQmrPAuEtTZW8E6D2zc5EUZX4Lw==", + "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.12", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.0-rc.12.tgz", + "integrity": "sha512-HHMwmarRKvoFsJorqYlFeFRzXZqCt2ETQlEDOb9aqssrnVBB1/+xgTGtuTrIk5vzLNX1MjMtTf7W9z3tsSbrxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/@rollup/rollup-android-arm-eabi": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.1.tgz", + "integrity": "sha512-d6FinEBLdIiK+1uACUttJKfgZREXrF0Qc2SmLII7W2AD8FfiZ9Wjd+rD/iRuf5s5dWrr1GgwXCvPqOuDquOowA==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-android-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.1.tgz", + "integrity": "sha512-YjG/EwIDvvYI1YvYbHvDz/BYHtkY4ygUIXHnTdLhG+hKIQFBiosfWiACWortsKPKU/+dUwQQCKQM3qrDe8c9BA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ] + }, + "node_modules/@rollup/rollup-darwin-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.1.tgz", + "integrity": "sha512-mjCpF7GmkRtSJwon+Rq1N8+pI+8l7w5g9Z3vWj4T7abguC4Czwi3Yu/pFaLvA3TTeMVjnu3ctigusqWUfjZzvw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-darwin-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.1.tgz", + "integrity": "sha512-haZ7hJ1JT4e9hqkoT9R/19XW2QKqjfJVv+i5AGg57S+nLk9lQnJ1F/eZloRO3o9Scy9CM3wQ9l+dkXtcBgN5Ew==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@rollup/rollup-freebsd-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.1.tgz", + "integrity": "sha512-czw90wpQq3ZsAVBlinZjAYTKduOjTywlG7fEeWKUA7oCmpA8xdTkxZZlwNJKWqILlq0wehoZcJYfBvOyhPTQ6w==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-freebsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.1.tgz", + "integrity": "sha512-KVB2rqsxTHuBtfOeySEyzEOB7ltlB/ux38iu2rBQzkjbwRVlkhAGIEDiiYnO2kFOkJp+Z7pUXKyrRRFuFUKt+g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ] + }, + "node_modules/@rollup/rollup-linux-arm-gnueabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.1.tgz", + "integrity": "sha512-L+34Qqil+v5uC0zEubW7uByo78WOCIrBvci69E7sFASRl0X7b/MB6Cqd1lky/CtcSVTydWa2WZwFuWexjS5o6g==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm-musleabihf": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.1.tgz", + "integrity": "sha512-n83O8rt4v34hgFzlkb1ycniJh7IR5RCIqt6mz1VRJD6pmhRi0CXdmfnLu9dIUS6buzh60IvACM842Ffb3xd6Gg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.1.tgz", + "integrity": "sha512-Nql7sTeAzhTAja3QXeAI48+/+GjBJ+QmAH13snn0AJSNL50JsDqotyudHyMbO2RbJkskbMbFJfIJKWA6R1LCJQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-arm64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.1.tgz", + "integrity": "sha512-+pUymDhd0ys9GcKZPPWlFiZ67sTWV5UU6zOJat02M1+PiuSGDziyRuI/pPue3hoUwm2uGfxdL+trT6Z9rxnlMA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.1.tgz", + "integrity": "sha512-VSvgvQeIcsEvY4bKDHEDWcpW4Yw7BtlKG1GUT4FzBUlEKQK0rWHYBqQt6Fm2taXS+1bXvJT6kICu5ZwqKCnvlQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-loong64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.1.tgz", + "integrity": "sha512-4LqhUomJqwe641gsPp6xLfhqWMbQV04KtPp7/dIp0nzPxAkNY1AbwL5W0MQpcalLYk07vaW9Kp1PBhdpZYYcEw==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.1.tgz", + "integrity": "sha512-tLQQ9aPvkBxOc/EUT6j3pyeMD6Hb8QF2BTBnCQWP/uu1lhc9AIrIjKnLYMEroIz/JvtGYgI9dF3AxHZNaEH0rw==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-ppc64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.1.tgz", + "integrity": "sha512-RMxFhJwc9fSXP6PqmAz4cbv3kAyvD1etJFjTx4ONqFP9DkTkXsAMU4v3Vyc5BgzC+anz7nS/9tp4obsKfqkDHg==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.1.tgz", + "integrity": "sha512-QKgFl+Yc1eEk6MmOBfRHYF6lTxiiiV3/z/BRrbSiW2I7AFTXoBFvdMEyglohPj//2mZS4hDOqeB0H1ACh3sBbg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-riscv64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.1.tgz", + "integrity": "sha512-RAjXjP/8c6ZtzatZcA1RaQr6O1TRhzC+adn8YZDnChliZHviqIjmvFwHcxi4JKPSDAt6Uhf/7vqcBzQJy0PDJg==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-s390x-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.1.tgz", + "integrity": "sha512-wcuocpaOlaL1COBYiA89O6yfjlp3RwKDeTIA0hM7OpmhR1Bjo9j31G1uQVpDlTvwxGn2nQs65fBFL5UFd76FcQ==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.1.tgz", + "integrity": "sha512-77PpsFQUCOiZR9+LQEFg9GClyfkNXj1MP6wRnzYs0EeWbPcHs02AXu4xuUbM1zhwn3wqaizle3AEYg5aeoohhg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-linux-x64-musl": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.1.tgz", + "integrity": "sha512-5cIATbk5vynAjqqmyBjlciMJl1+R/CwX9oLk/EyiFXDWd95KpHdrOJT//rnUl4cUcskrd0jCCw3wpZnhIHdD9w==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@rollup/rollup-openbsd-x64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.1.tgz", + "integrity": "sha512-cl0w09WsCi17mcmWqqglez9Gk8isgeWvoUZ3WiJFYSR3zjBQc2J5/ihSjpl+VLjPqjQ/1hJRcqBfLjssREQILw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ] + }, + "node_modules/@rollup/rollup-openharmony-arm64": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.1.tgz", + "integrity": "sha512-4Cv23ZrONRbNtbZa37mLSueXUCtN7MXccChtKpUnQNgF010rjrjfHx3QxkS2PI7LqGT5xXyYs1a7LbzAwT0iCA==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ] + }, + "node_modules/@rollup/rollup-win32-arm64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.1.tgz", + "integrity": "sha512-i1okWYkA4FJICtr7KpYzFpRTHgy5jdDbZiWfvny21iIKky5YExiDXP+zbXzm3dUcFpkEeYNHgQ5fuG236JPq0g==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-ia32-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.1.tgz", + "integrity": "sha512-u09m3CuwLzShA0EYKMNiFgcjjzwqtUMLmuCJLeZWjjOYA3IT2Di09KaxGBTP9xVztWyIWjVdsB2E9goMjZvTQg==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-gnu": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.1.tgz", + "integrity": "sha512-k+600V9Zl1CM7eZxJgMyTUzmrmhB/0XZnF4pRypKAlAgxmedUA+1v9R+XOFv56W4SlHEzfeMtzujLJD22Uz5zg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@rollup/rollup-win32-x64-msvc": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.1.tgz", + "integrity": "sha512-lWMnixq/QzxyhTV6NjQJ4SFo1J6PvOX8vUx5Wb4bBPsEb+8xZ89Bz6kOXpfXj9ak9AHTQVQzlgzBEc1SyM27xQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ] + }, + "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/node": { + "version": "22.19.15", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.15.tgz", + "integrity": "sha512-F0R/h2+dsy5wJAUe3tAU6oqa2qbWY5TpNfL/RGmo1y38hiyO1w3x2jPtt76wmuaJI4DQnOBu21cNXQ2STIUUWg==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~6.21.0" + } + }, + "node_modules/@vectorize-io/hindsight-client": { + "version": "0.4.22", + "resolved": "https://registry.npmjs.org/@vectorize-io/hindsight-client/-/hindsight-client-0.4.22.tgz", + "integrity": "sha512-Z15ONaVranek2Awqq1qvitc42EQsnUxhzwxQPZBawQ6tpQz2gPNh3wQLYg4Pe+dCIPB4WmeQu9R8grmu5IBFfA==", + "license": "MIT" + }, + "node_modules/@vitest/expect": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.2.tgz", + "integrity": "sha512-gbu+7B0YgUJ2nkdsRJrFFW6X7NTP44WlhiclHniUhxADQJH5Szt9mZ9hWnJPJ8YwOK5zUOSSlSvyzRf0u1DSBQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "@types/chai": "^5.2.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "chai": "^6.2.2", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/mocker": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.2.tgz", + "integrity": "sha512-Ize4iQtEALHDttPRCmN+FKqOl2vxTiNUhzobQFFt/BM1lRUTG7zRCLOykG/6Vo4E4hnUdfVLo5/eqKPukcWW7Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/spy": "4.1.2", + "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" + }, + "peerDependenciesMeta": { + "msw": { + "optional": true + }, + "vite": { + "optional": true + } + } + }, + "node_modules/@vitest/pretty-format": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.2.tgz", + "integrity": "sha512-dwQga8aejqeuB+TvXCMzSQemvV9hNEtDDpgUKDzOmNQayl2OG241PSWeJwKRH3CiC+sESrmoFd49rfnq7T4RnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/runner": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.2.tgz", + "integrity": "sha512-Gr+FQan34CdiYAwpGJmQG8PgkyFVmARK8/xSijia3eTFgVfpcpztWLuP6FttGNfPLJhaZVP/euvujeNYar36OQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/utils": "4.1.2", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/snapshot": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.2.tgz", + "integrity": "sha512-g7yfUmxYS4mNxk31qbOYsSt2F4m1E02LFqO53Xpzg3zKMhLAPZAjjfyl9e6z7HrW6LvUdTwAQR3HHfLjpko16A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "@vitest/utils": "4.1.2", + "magic-string": "^0.30.21", + "pathe": "^2.0.3" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/spy": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.2.tgz", + "integrity": "sha512-DU4fBnbVCJGNBwVA6xSToNXrkZNSiw59H8tcuUspVMsBDBST4nfvsPsEHDHGtWRRnqBERBQu7TrTKskmjqTXKA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/@vitest/utils": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.2.tgz", + "integrity": "sha512-xw2/TiX82lQHA06cgbqRKFb5lCAy3axQ4H4SoUFhUsg+wztiet+co86IAMDtF6Vm1hc7J6j09oh/rgDn+JdKIQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/pretty-format": "4.1.2", + "convert-source-map": "^2.0.0", + "tinyrainbow": "^3.1.0" + }, + "funding": { + "url": "https://opencollective.com/vitest" + } + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/any-promise": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/any-promise/-/any-promise-1.3.0.tgz", + "integrity": "sha512-7UvmKalWRt1wgjL1RrGxoSJW/0QZFIegpeGvZG9kjp8vrRu55XTHbwnqq2GpXm9uLbcuhxm3IqX9OB4MZR1b2A==", + "dev": true, + "license": "MIT" + }, + "node_modules/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/bundle-require": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/bundle-require/-/bundle-require-5.1.0.tgz", + "integrity": "sha512-3WrrOuZiyaaZPWiEt4G3+IffISVC9HYlWueJEBWED4ZH4aIAC2PnkdnuRrR94M+w6yGWn4AglWtJtBI8YqvgoA==", + "dev": true, + "license": "MIT", + "dependencies": { + "load-tsconfig": "^0.2.3" + }, + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + }, + "peerDependencies": { + "esbuild": ">=0.18" + } + }, + "node_modules/cac": { + "version": "6.7.14", + "resolved": "https://registry.npmjs.org/cac/-/cac-6.7.14.tgz", + "integrity": "sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "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/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "dev": true, + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/commander": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", + "integrity": "sha512-NOKm8xhkzAjzFx8B2v5OAHT+u5pRQc2UCa2Vq9jYL/31o2wi9mxBA7LIFs3sV5VSC49z6pEhfbMULvShKj26WA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/confbox": { + "version": "0.1.8", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.1.8.tgz", + "integrity": "sha512-RMtmw0iFkeR4YV+fUOSucriAQNb9g8zFR52MWCtl+cCZOFRNL6zeB395vPzFhEjjn4fMxXudmELnl/KF/WrK6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, + "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/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/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/esbuild": { + "version": "0.27.5", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.5.tgz", + "integrity": "sha512-zdQoHBjuDqKsvV5OPaWansOwfSQ0Js+Uj9J85TBvj3bFW1JjWTSULMRwdQAc8qMeIScbClxeMK0jlrtB9linhA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.27.5", + "@esbuild/android-arm": "0.27.5", + "@esbuild/android-arm64": "0.27.5", + "@esbuild/android-x64": "0.27.5", + "@esbuild/darwin-arm64": "0.27.5", + "@esbuild/darwin-x64": "0.27.5", + "@esbuild/freebsd-arm64": "0.27.5", + "@esbuild/freebsd-x64": "0.27.5", + "@esbuild/linux-arm": "0.27.5", + "@esbuild/linux-arm64": "0.27.5", + "@esbuild/linux-ia32": "0.27.5", + "@esbuild/linux-loong64": "0.27.5", + "@esbuild/linux-mips64el": "0.27.5", + "@esbuild/linux-ppc64": "0.27.5", + "@esbuild/linux-riscv64": "0.27.5", + "@esbuild/linux-s390x": "0.27.5", + "@esbuild/linux-x64": "0.27.5", + "@esbuild/netbsd-arm64": "0.27.5", + "@esbuild/netbsd-x64": "0.27.5", + "@esbuild/openbsd-arm64": "0.27.5", + "@esbuild/openbsd-x64": "0.27.5", + "@esbuild/openharmony-arm64": "0.27.5", + "@esbuild/sunos-x64": "0.27.5", + "@esbuild/win32-arm64": "0.27.5", + "@esbuild/win32-ia32": "0.27.5", + "@esbuild/win32-x64": "0.27.5" + } + }, + "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/fix-dts-default-cjs-exports": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/fix-dts-default-cjs-exports/-/fix-dts-default-cjs-exports-1.0.1.tgz", + "integrity": "sha512-pVIECanWFC61Hzl2+oOCtoJ3F17kglZC/6N94eRWycFgBH35hHx0Li604ZIzhseh97mf2p0cv7vVrOZGoqhlEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "magic-string": "^0.30.17", + "mlly": "^1.7.4", + "rollup": "^4.34.8" + } + }, + "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/joycon": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/joycon/-/joycon-3.1.1.tgz", + "integrity": "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10" + } + }, + "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/lilconfig": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", + "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/antonk52" + } + }, + "node_modules/lines-and-columns": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", + "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", + "dev": true, + "license": "MIT" + }, + "node_modules/load-tsconfig": { + "version": "0.2.5", + "resolved": "https://registry.npmjs.org/load-tsconfig/-/load-tsconfig-0.2.5.tgz", + "integrity": "sha512-IXO6OCs9yg8tMKzfPZ1YmheJbZCiEsnBdcB03l0OcfK9prKnJb96siuHCr5Fl37/yo9DnKU+TLpxzTUspw9shg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + } + }, + "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/mlly": { + "version": "1.8.2", + "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.8.2.tgz", + "integrity": "sha512-d+ObxMQFmbt10sretNDytwt85VrbkhhUA/JBGm1MPaWJ65Cl4wOgLaB1NYvJSZ0Ef03MMEU/0xpPMXUIQ29UfA==", + "dev": true, + "license": "MIT", + "dependencies": { + "acorn": "^8.16.0", + "pathe": "^2.0.3", + "pkg-types": "^1.3.1", + "ufo": "^1.6.3" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true, + "license": "MIT" + }, + "node_modules/mz": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", + "integrity": "sha512-z81GNO7nnYMEhrGh9LeymoE4+Yr0Wn5McHIZMK5cfQCl+NDX08sCZgUc9/6MHni9IWuFLm1Z3HTCXu2z9fN62Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0", + "object-assign": "^4.0.1", + "thenify-all": "^1.0.0" + } + }, + "node_modules/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/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/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.4", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", + "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pirates": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/pirates/-/pirates-4.0.7.tgz", + "integrity": "sha512-TfySrs/5nm8fQJDcBDuUng3VOUKsd7S+zqvbOTiGXHfxX4wK31ard+hoNuvkicM/2YFzlpDgABOevKSsB4G/FA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, + "node_modules/pkg-types": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-1.3.1.tgz", + "integrity": "sha512-/Jm5M4RvtBFVkKWRu2BLUTNP8/M2a+UwuAX+ae4770q1qVGtfjG+WTCupoZixokjmHiry8uI+dlY8KXYV5HVVQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "confbox": "^0.1.8", + "mlly": "^1.7.4", + "pathe": "^2.0.1" + } + }, + "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/postcss-load-config": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/postcss-load-config/-/postcss-load-config-6.0.1.tgz", + "integrity": "sha512-oPtTM4oerL+UXmx+93ytZVN82RrlY/wPUV8IeDxFrzIjXOLF1pN+EmKPLbubvKHT2HC20xXsCAH2Z+CKV6Oz/g==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "lilconfig": "^3.1.1" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "jiti": ">=1.21.0", + "postcss": ">=8.0.9", + "tsx": "^4.8.1", + "yaml": "^2.4.2" + }, + "peerDependenciesMeta": { + "jiti": { + "optional": true + }, + "postcss": { + "optional": true + }, + "tsx": { + "optional": true + }, + "yaml": { + "optional": true + } + } + }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rolldown": { + "version": "1.0.0-rc.12", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.0.0-rc.12.tgz", + "integrity": "sha512-yP4USLIMYrwpPHEFB5JGH1uxhcslv6/hL0OyvTuY+3qlOSJvZ7ntYnoWpehBxufkgN0cvXxppuTu5hHa/zPh+A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@oxc-project/types": "=0.122.0", + "@rolldown/pluginutils": "1.0.0-rc.12" + }, + "bin": { + "rolldown": "bin/cli.mjs" + }, + "engines": { + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-arm64": "1.0.0-rc.12", + "@rolldown/binding-darwin-x64": "1.0.0-rc.12", + "@rolldown/binding-freebsd-x64": "1.0.0-rc.12", + "@rolldown/binding-linux-arm-gnueabihf": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-arm64-musl": "1.0.0-rc.12", + "@rolldown/binding-linux-ppc64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-s390x-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-gnu": "1.0.0-rc.12", + "@rolldown/binding-linux-x64-musl": "1.0.0-rc.12", + "@rolldown/binding-openharmony-arm64": "1.0.0-rc.12", + "@rolldown/binding-wasm32-wasi": "1.0.0-rc.12", + "@rolldown/binding-win32-arm64-msvc": "1.0.0-rc.12", + "@rolldown/binding-win32-x64-msvc": "1.0.0-rc.12" + } + }, + "node_modules/rollup": { + "version": "4.60.1", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.1.tgz", + "integrity": "sha512-VmtB2rFU/GroZ4oL8+ZqXgSA38O6GR8KSIvWmEFv63pQ0G6KaBH9s07PO8XTXP4vI+3UJUEypOfjkGfmSBBR0w==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "1.0.8" + }, + "bin": { + "rollup": "dist/bin/rollup" + }, + "engines": { + "node": ">=18.0.0", + "npm": ">=8.0.0" + }, + "optionalDependencies": { + "@rollup/rollup-android-arm-eabi": "4.60.1", + "@rollup/rollup-android-arm64": "4.60.1", + "@rollup/rollup-darwin-arm64": "4.60.1", + "@rollup/rollup-darwin-x64": "4.60.1", + "@rollup/rollup-freebsd-arm64": "4.60.1", + "@rollup/rollup-freebsd-x64": "4.60.1", + "@rollup/rollup-linux-arm-gnueabihf": "4.60.1", + "@rollup/rollup-linux-arm-musleabihf": "4.60.1", + "@rollup/rollup-linux-arm64-gnu": "4.60.1", + "@rollup/rollup-linux-arm64-musl": "4.60.1", + "@rollup/rollup-linux-loong64-gnu": "4.60.1", + "@rollup/rollup-linux-loong64-musl": "4.60.1", + "@rollup/rollup-linux-ppc64-gnu": "4.60.1", + "@rollup/rollup-linux-ppc64-musl": "4.60.1", + "@rollup/rollup-linux-riscv64-gnu": "4.60.1", + "@rollup/rollup-linux-riscv64-musl": "4.60.1", + "@rollup/rollup-linux-s390x-gnu": "4.60.1", + "@rollup/rollup-linux-x64-gnu": "4.60.1", + "@rollup/rollup-linux-x64-musl": "4.60.1", + "@rollup/rollup-openbsd-x64": "4.60.1", + "@rollup/rollup-openharmony-arm64": "4.60.1", + "@rollup/rollup-win32-arm64-msvc": "4.60.1", + "@rollup/rollup-win32-ia32-msvc": "4.60.1", + "@rollup/rollup-win32-x64-gnu": "4.60.1", + "@rollup/rollup-win32-x64-msvc": "4.60.1", + "fsevents": "~2.3.2" + } + }, + "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": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "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/sucrase": { + "version": "3.35.1", + "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", + "integrity": "sha512-DhuTmvZWux4H1UOnWMB3sk0sbaCVOoQZjv8u1rDoTV0HTdGem9hkAZtl4JZy8P2z4Bg0nT+YMeOFyVr4zcG5Tw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.2", + "commander": "^4.0.0", + "lines-and-columns": "^1.1.6", + "mz": "^2.7.0", + "pirates": "^4.0.1", + "tinyglobby": "^0.2.11", + "ts-interface-checker": "^0.1.9" + }, + "bin": { + "sucrase": "bin/sucrase", + "sucrase-node": "bin/sucrase-node" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/thenify": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/thenify/-/thenify-3.3.1.tgz", + "integrity": "sha512-RVZSIV5IG10Hk3enotrhvz0T9em6cyHBLkH/YAZuKqd8hRkKhSfCGIcP2KUY0EPxndzANBmNllzWPwak+bheSw==", + "dev": true, + "license": "MIT", + "dependencies": { + "any-promise": "^1.0.0" + } + }, + "node_modules/thenify-all": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/thenify-all/-/thenify-all-1.6.0.tgz", + "integrity": "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA==", + "dev": true, + "license": "MIT", + "dependencies": { + "thenify": ">= 3.1.0 < 4" + }, + "engines": { + "node": ">=0.8" + } + }, + "node_modules/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": "0.3.2", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-0.3.2.tgz", + "integrity": "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA==", + "dev": true, + "license": "MIT" + }, + "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/tree-kill": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/tree-kill/-/tree-kill-1.2.2.tgz", + "integrity": "sha512-L0Orpi8qGpRG//Nd+H90vFB+3iHnue1zSSGmNOOCh1GLJ7rUKVwV2HvijphGQS2UmhUZewS9VgvxYIdgr+fG1A==", + "dev": true, + "license": "MIT", + "bin": { + "tree-kill": "cli.js" + } + }, + "node_modules/ts-interface-checker": { + "version": "0.1.13", + "resolved": "https://registry.npmjs.org/ts-interface-checker/-/ts-interface-checker-0.1.13.tgz", + "integrity": "sha512-Y/arvbn+rrz3JCKl9C4kVNfTfSm2/mEp5FSz5EsZSANGPSlQrpRI5M4PKF+mJnE52jOO90PnPSc3Ur3bTQw0gA==", + "dev": true, + "license": "Apache-2.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/tsup": { + "version": "8.5.1", + "resolved": "https://registry.npmjs.org/tsup/-/tsup-8.5.1.tgz", + "integrity": "sha512-xtgkqwdhpKWr3tKPmCkvYmS9xnQK3m3XgxZHwSUjvfTjp7YfXe5tT3GgWi0F2N+ZSMsOeWeZFh7ZZFg5iPhing==", + "dev": true, + "license": "MIT", + "dependencies": { + "bundle-require": "^5.1.0", + "cac": "^6.7.14", + "chokidar": "^4.0.3", + "consola": "^3.4.0", + "debug": "^4.4.0", + "esbuild": "^0.27.0", + "fix-dts-default-cjs-exports": "^1.0.0", + "joycon": "^3.1.1", + "picocolors": "^1.1.1", + "postcss-load-config": "^6.0.1", + "resolve-from": "^5.0.0", + "rollup": "^4.34.8", + "source-map": "^0.7.6", + "sucrase": "^3.35.0", + "tinyexec": "^0.3.2", + "tinyglobby": "^0.2.11", + "tree-kill": "^1.2.2" + }, + "bin": { + "tsup": "dist/cli-default.js", + "tsup-node": "dist/cli-node.js" + }, + "engines": { + "node": ">=18" + }, + "peerDependencies": { + "@microsoft/api-extractor": "^7.36.0", + "@swc/core": "^1", + "postcss": "^8.4.12", + "typescript": ">=4.5.0" + }, + "peerDependenciesMeta": { + "@microsoft/api-extractor": { + "optional": true + }, + "@swc/core": { + "optional": true + }, + "postcss": { + "optional": true + }, + "typescript": { + "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/ufo": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", + "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "dev": true, + "license": "MIT" + }, + "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.3", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.0.3.tgz", + "integrity": "sha512-B9ifbFudT1TFhfltfaIPgjo9Z3mDynBTJSUYxTjOQruf/zHH+ezCQKcoqO+h7a9Pw9Nm/OtlXAiGT1axBgwqrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "lightningcss": "^1.32.0", + "picomatch": "^4.0.4", + "postcss": "^8.5.8", + "rolldown": "1.0.0-rc.12", + "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.2", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.2.tgz", + "integrity": "sha512-xjR1dMTVHlFLh98JE3i/f/WePqJsah4A0FK9cc8Ehp9Udk0AZk6ccpIZhh1qJ/yxVWRZ+Q54ocnD8TXmkhspGg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@vitest/expect": "4.1.2", + "@vitest/mocker": "4.1.2", + "@vitest/pretty-format": "4.1.2", + "@vitest/runner": "4.1.2", + "@vitest/snapshot": "4.1.2", + "@vitest/spy": "4.1.2", + "@vitest/utils": "4.1.2", + "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.1.0", + "vite": "^6.0.0 || ^7.0.0 || ^8.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.2", + "@vitest/browser-preview": "4.1.2", + "@vitest/browser-webdriverio": "4.1.2", + "@vitest/ui": "4.1.2", + "happy-dom": "*", + "jsdom": "*", + "vite": "^6.0.0 || ^7.0.0 || ^8.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/vitest/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/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" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/hindsight-integrations/opencode/package.json b/hindsight-integrations/opencode/package.json new file mode 100644 index 000000000..aa0105c49 --- /dev/null +++ b/hindsight-integrations/opencode/package.json @@ -0,0 +1,63 @@ +{ + "name": "@vectorize-io/opencode-hindsight", + "version": "0.1.0", + "description": "Hindsight memory plugin for OpenCode - Give your AI coding agent persistent long-term memory", + "main": "dist/index.js", + "types": "dist/index.d.ts", + "type": "module", + "exports": { + ".": { + "types": "./dist/index.d.ts", + "import": "./dist/index.js" + } + }, + "keywords": [ + "opencode", + "ai", + "memory", + "hindsight", + "agents", + "llm", + "long-term-memory", + "coding-agent" + ], + "author": "Vectorize ", + "license": "MIT", + "repository": { + "type": "git", + "url": "https://github.com/vectorize-io/hindsight.git", + "directory": "hindsight-integrations/opencode" + }, + "files": [ + "dist", + "README.md" + ], + "scripts": { + "build": "tsup", + "dev": "tsc --watch", + "clean": "rm -rf dist", + "test": "vitest run", + "test:watch": "vitest", + "prepublishOnly": "npm run clean && npm run build" + }, + "peerDependencies": { + "@opencode-ai/plugin": ">=1.0.0" + }, + "dependencies": { + "@vectorize-io/hindsight-client": "^0.4.19" + }, + "devDependencies": { + "@opencode-ai/plugin": "^1.3.13", + "@types/node": "^22.0.0", + "tsup": "^8.5.1", + "typescript": "^5.7.0", + "vitest": "^4.0.18" + }, + "engines": { + "node": ">=22" + }, + "overrides": { + "rollup": "^4.59.0", + "picomatch": ">=2.3.2 <3.0.0 || >=4.0.4" + } +} diff --git a/hindsight-integrations/opencode/src/bank.test.ts b/hindsight-integrations/opencode/src/bank.test.ts new file mode 100644 index 000000000..9ded28c50 --- /dev/null +++ b/hindsight-integrations/opencode/src/bank.test.ts @@ -0,0 +1,169 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { deriveBankId, ensureBankMission } from './bank.js'; +import type { HindsightConfig } from './config.js'; + +function makeConfig(overrides: Partial = {}): HindsightConfig { + return { + autoRecall: true, + recallBudget: 'mid', + recallMaxTokens: 1024, + recallTypes: ['world', 'experience'], + recallContextTurns: 1, + recallMaxQueryChars: 800, + recallPromptPreamble: '', + autoRetain: true, + retainMode: 'full-session', + retainEveryNTurns: 10, + retainOverlapTurns: 2, + retainContext: 'opencode', + retainTags: [], + retainMetadata: {}, + hindsightApiUrl: null, + hindsightApiToken: null, + bankId: null, + bankIdPrefix: '', + dynamicBankId: false, + dynamicBankGranularity: ['agent', 'project'], + bankMission: '', + retainMission: null, + agentName: 'opencode', + debug: false, + ...overrides, + }; +} + +describe('deriveBankId', () => { + const originalEnv = { ...process.env }; + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('returns default bank name in static mode', () => { + expect(deriveBankId(makeConfig(), '/home/user/project')).toBe('opencode'); + }); + + it('returns configured bankId in static mode', () => { + const config = makeConfig({ bankId: 'my-bank' }); + expect(deriveBankId(config, '/home/user/project')).toBe('my-bank'); + }); + + it('adds prefix in static mode', () => { + const config = makeConfig({ bankIdPrefix: 'dev', bankId: 'my-bank' }); + expect(deriveBankId(config, '/home/user/project')).toBe('dev-my-bank'); + }); + + it('composes from granularity fields in dynamic mode', () => { + const config = makeConfig({ + dynamicBankId: true, + dynamicBankGranularity: ['agent', 'project'], + agentName: 'opencode', + }); + expect(deriveBankId(config, '/home/user/my-project')).toBe('opencode::my-project'); + }); + + it('uses default granularity when not specified', () => { + const config = makeConfig({ + dynamicBankId: true, + dynamicBankGranularity: [], + }); + expect(deriveBankId(config, '/home/user/proj')).toBe('opencode::proj'); + }); + + it('URL-encodes special characters', () => { + const config = makeConfig({ + dynamicBankId: true, + dynamicBankGranularity: ['project'], + }); + expect(deriveBankId(config, '/home/user/my project')).toBe('my%20project'); + }); + + it('uses channel/user from env vars', () => { + process.env.HINDSIGHT_CHANNEL_ID = 'slack-general'; + process.env.HINDSIGHT_USER_ID = 'user123'; + const config = makeConfig({ + dynamicBankId: true, + dynamicBankGranularity: ['agent', 'channel', 'user'], + }); + expect(deriveBankId(config, '/home/user/proj')).toBe('opencode::slack-general::user123'); + }); + + it('uses defaults for missing env vars', () => { + delete process.env.HINDSIGHT_CHANNEL_ID; + delete process.env.HINDSIGHT_USER_ID; + const config = makeConfig({ + dynamicBankId: true, + dynamicBankGranularity: ['channel', 'user'], + }); + expect(deriveBankId(config, '/home/user/proj')).toBe('default::anonymous'); + }); + + it('adds prefix in dynamic mode', () => { + const config = makeConfig({ + dynamicBankId: true, + bankIdPrefix: 'dev', + dynamicBankGranularity: ['agent'], + }); + expect(deriveBankId(config, '/home/user/proj')).toBe('dev-opencode'); + }); +}); + +describe('ensureBankMission', () => { + it('calls createBank on first use', async () => { + const client = { createBank: vi.fn().mockResolvedValue({}) } as any; + const missionsSet = new Set(); + const config = makeConfig({ bankMission: 'Test mission' }); + + await ensureBankMission(client, 'test-bank', config, missionsSet); + + expect(client.createBank).toHaveBeenCalledWith('test-bank', { + reflectMission: 'Test mission', + retainMission: undefined, + }); + expect(missionsSet.has('test-bank')).toBe(true); + }); + + it('skips if already set', async () => { + const client = { createBank: vi.fn() } as any; + const missionsSet = new Set(['test-bank']); + const config = makeConfig({ bankMission: 'Test mission' }); + + await ensureBankMission(client, 'test-bank', config, missionsSet); + + expect(client.createBank).not.toHaveBeenCalled(); + }); + + it('skips if no mission configured', async () => { + const client = { createBank: vi.fn() } as any; + const missionsSet = new Set(); + const config = makeConfig({ bankMission: '' }); + + await ensureBankMission(client, 'test-bank', config, missionsSet); + + expect(client.createBank).not.toHaveBeenCalled(); + }); + + it('does not throw on client error', async () => { + const client = { createBank: vi.fn().mockRejectedValue(new Error('Network error')) } as any; + const missionsSet = new Set(); + const config = makeConfig({ bankMission: 'Mission' }); + + await expect( + ensureBankMission(client, 'test-bank', config, missionsSet), + ).resolves.not.toThrow(); + expect(missionsSet.has('test-bank')).toBe(false); + }); + + it('passes retainMission when configured', async () => { + const client = { createBank: vi.fn().mockResolvedValue({}) } as any; + const missionsSet = new Set(); + const config = makeConfig({ bankMission: 'Reflect', retainMission: 'Extract carefully' }); + + await ensureBankMission(client, 'test-bank', config, missionsSet); + + expect(client.createBank).toHaveBeenCalledWith('test-bank', { + reflectMission: 'Reflect', + retainMission: 'Extract carefully', + }); + }); +}); diff --git a/hindsight-integrations/opencode/src/bank.ts b/hindsight-integrations/opencode/src/bank.ts new file mode 100644 index 000000000..28cb3668b --- /dev/null +++ b/hindsight-integrations/opencode/src/bank.ts @@ -0,0 +1,94 @@ +/** + * Bank ID derivation and mission management. + * + * Port of Claude Code plugin's bank.py, adapted for OpenCode's context model. + * + * Dimensions for dynamic bank IDs: + * - agent → configured name or "opencode" + * - project → derived from working directory basename + */ + +import { basename } from 'node:path'; +import type { HindsightConfig } from './config.js'; +import { debugLog } from './config.js'; +import type { HindsightClient } from '@vectorize-io/hindsight-client'; + +const DEFAULT_BANK_NAME = 'opencode'; +const VALID_FIELDS = new Set(['agent', 'project', 'channel', 'user']); + +/** + * Derive a bank ID from context and config. + * + * Static mode: returns config.bankId or DEFAULT_BANK_NAME. + * Dynamic mode: composes from granularity fields joined by '::'. + */ +export function deriveBankId(config: HindsightConfig, directory: string): string { + const prefix = config.bankIdPrefix; + + if (!config.dynamicBankId) { + const base = config.bankId || DEFAULT_BANK_NAME; + return prefix ? `${prefix}-${base}` : base; + } + + const fields = config.dynamicBankGranularity?.length + ? config.dynamicBankGranularity + : ['agent', 'project']; + + for (const f of fields) { + if (!VALID_FIELDS.has(f)) { + console.error( + `[Hindsight] Unknown dynamicBankGranularity field "${f}" — ` + + `valid: ${[...VALID_FIELDS].sort().join(', ')}`, + ); + } + } + + const channelId = process.env.HINDSIGHT_CHANNEL_ID || ''; + const userId = process.env.HINDSIGHT_USER_ID || ''; + + const fieldMap: Record = { + agent: config.agentName || 'opencode', + project: directory ? basename(directory) : 'unknown', + channel: channelId || 'default', + user: userId || 'anonymous', + }; + + const segments = fields.map((f) => encodeURIComponent(fieldMap[f] || 'unknown')); + const baseBankId = segments.join('::'); + + return prefix ? `${prefix}-${baseBankId}` : baseBankId; +} + +/** + * Set bank mission on first use, skip if already set. + * Uses an in-memory Set (plugin is long-lived, unlike Claude Code's ephemeral hooks). + */ +export async function ensureBankMission( + client: HindsightClient, + bankId: string, + config: HindsightConfig, + missionsSet: Set, +): Promise { + const mission = config.bankMission; + if (!mission?.trim()) return; + if (missionsSet.has(bankId)) return; + + try { + await client.createBank(bankId, { + reflectMission: mission, + retainMission: config.retainMission || undefined, + }); + missionsSet.add(bankId); + // Cap tracked banks + if (missionsSet.size > 10000) { + const keys = [...missionsSet].sort(); + for (const k of keys.slice(0, keys.length >> 1)) { + missionsSet.delete(k); + } + } + debugLog(config, `Set mission for bank: ${bankId}`); + } catch (e) { + // Don't fail if mission set fails — bank may not exist yet + debugLog(config, `Could not set bank mission for ${bankId}: ${e}`); + } +} diff --git a/hindsight-integrations/opencode/src/config.test.ts b/hindsight-integrations/opencode/src/config.test.ts new file mode 100644 index 000000000..e2aafa82f --- /dev/null +++ b/hindsight-integrations/opencode/src/config.test.ts @@ -0,0 +1,127 @@ +import { describe, it, expect, beforeEach, afterEach, vi } from 'vitest'; +import { loadConfig, type HindsightConfig } from './config.js'; + +describe('loadConfig', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + // Clear all HINDSIGHT_ env vars + for (const key of Object.keys(process.env)) { + if (key.startsWith('HINDSIGHT_')) { + delete process.env[key]; + } + } + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('returns defaults when no config sources exist', () => { + const config = loadConfig(); + expect(config.autoRecall).toBe(true); + expect(config.autoRetain).toBe(true); + expect(config.recallBudget).toBe('mid'); + expect(config.recallMaxTokens).toBe(1024); + expect(config.retainContext).toBe('opencode'); + expect(config.agentName).toBe('opencode'); + expect(config.dynamicBankId).toBe(false); + expect(config.debug).toBe(false); + expect(config.hindsightApiUrl).toBeNull(); + expect(config.hindsightApiToken).toBeNull(); + expect(config.bankId).toBeNull(); + }); + + it('env vars override defaults', () => { + process.env.HINDSIGHT_API_URL = 'https://example.com'; + process.env.HINDSIGHT_API_TOKEN = 'secret-token'; + process.env.HINDSIGHT_BANK_ID = 'my-bank'; + process.env.HINDSIGHT_AUTO_RECALL = 'false'; + process.env.HINDSIGHT_AUTO_RETAIN = '0'; + process.env.HINDSIGHT_RECALL_MAX_TOKENS = '2048'; + process.env.HINDSIGHT_DEBUG = 'true'; + + const config = loadConfig(); + expect(config.hindsightApiUrl).toBe('https://example.com'); + expect(config.hindsightApiToken).toBe('secret-token'); + expect(config.bankId).toBe('my-bank'); + expect(config.autoRecall).toBe(false); + expect(config.autoRetain).toBe(false); + expect(config.recallMaxTokens).toBe(2048); + expect(config.debug).toBe(true); + }); + + it('plugin options override defaults', () => { + const config = loadConfig({ + bankId: 'plugin-bank', + autoRecall: false, + recallBudget: 'high', + }); + expect(config.bankId).toBe('plugin-bank'); + expect(config.autoRecall).toBe(false); + expect(config.recallBudget).toBe('high'); + }); + + it('env vars override plugin options', () => { + process.env.HINDSIGHT_BANK_ID = 'env-bank'; + const config = loadConfig({ bankId: 'plugin-bank' }); + expect(config.bankId).toBe('env-bank'); + }); + + it('boolean env var parsing', () => { + process.env.HINDSIGHT_AUTO_RECALL = 'true'; + expect(loadConfig().autoRecall).toBe(true); + + process.env.HINDSIGHT_AUTO_RECALL = '1'; + expect(loadConfig().autoRecall).toBe(true); + + process.env.HINDSIGHT_AUTO_RECALL = 'yes'; + expect(loadConfig().autoRecall).toBe(true); + + process.env.HINDSIGHT_AUTO_RECALL = 'false'; + expect(loadConfig().autoRecall).toBe(false); + + process.env.HINDSIGHT_AUTO_RECALL = 'no'; + expect(loadConfig().autoRecall).toBe(false); + }); + + it('integer env var parsing', () => { + process.env.HINDSIGHT_RECALL_MAX_TOKENS = '4096'; + expect(loadConfig().recallMaxTokens).toBe(4096); + + // Invalid integer keeps default + process.env.HINDSIGHT_RECALL_MAX_TOKENS = 'not-a-number'; + expect(loadConfig().recallMaxTokens).toBe(1024); + }); + + it('null plugin options are ignored', () => { + const config = loadConfig({ bankId: null, debug: undefined }); + expect(config.bankId).toBeNull(); // stays default null + expect(config.debug).toBe(false); // stays default + }); + + it('invalid retainMode falls back to full-session with warning', () => { + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const config = loadConfig({ retainMode: 'full_session' }); + expect(config.retainMode).toBe('full-session'); + expect(spy).toHaveBeenCalledWith(expect.stringContaining('Unknown retainMode')); + spy.mockRestore(); + }); + + it('invalid recallBudget falls back to mid with warning', () => { + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const config = loadConfig({ recallBudget: 'maximum' }); + expect(config.recallBudget).toBe('mid'); + expect(spy).toHaveBeenCalledWith(expect.stringContaining('Unknown recallBudget')); + spy.mockRestore(); + }); + + it('valid retainMode and recallBudget pass without warning', () => { + const spy = vi.spyOn(console, 'error').mockImplementation(() => {}); + const config = loadConfig({ retainMode: 'last-turn', recallBudget: 'high' }); + expect(config.retainMode).toBe('last-turn'); + expect(config.recallBudget).toBe('high'); + expect(spy).not.toHaveBeenCalled(); + spy.mockRestore(); + }); +}); diff --git a/hindsight-integrations/opencode/src/config.ts b/hindsight-integrations/opencode/src/config.ts new file mode 100644 index 000000000..c86c442d1 --- /dev/null +++ b/hindsight-integrations/opencode/src/config.ts @@ -0,0 +1,187 @@ +/** + * Configuration management for the Hindsight OpenCode plugin. + * + * Loading order (later entries win): + * 1. Built-in defaults + * 2. User config file (~/.hindsight/opencode.json) + * 3. Plugin options (from opencode.json plugin tuple) + * 4. Environment variable overrides + */ + +import { readFileSync } from 'node:fs'; +import { join } from 'node:path'; +import { homedir } from 'node:os'; + +export interface HindsightConfig { + // Recall + autoRecall: boolean; + recallBudget: string; + recallMaxTokens: number; + recallTypes: string[]; + recallContextTurns: number; + recallMaxQueryChars: number; + recallPromptPreamble: string; + + // Retain + autoRetain: boolean; + retainMode: string; + retainEveryNTurns: number; + retainOverlapTurns: number; + retainContext: string; + retainTags: string[]; + retainMetadata: Record; + + // Connection + hindsightApiUrl: string | null; + hindsightApiToken: string | null; + + // Bank + bankId: string | null; + bankIdPrefix: string; + dynamicBankId: boolean; + dynamicBankGranularity: string[]; + bankMission: string; + retainMission: string | null; + agentName: string; + + // Misc + debug: boolean; +} + +const DEFAULTS: HindsightConfig = { + // Recall + autoRecall: true, + recallBudget: 'mid', + recallMaxTokens: 1024, + recallTypes: ['world', 'experience'], + recallContextTurns: 1, + recallMaxQueryChars: 800, + recallPromptPreamble: + 'Relevant memories from past conversations (prioritize recent when ' + + 'conflicting). Only use memories that are directly useful to continue ' + + 'this conversation; ignore the rest:', + + // Retain + autoRetain: true, + retainMode: 'full-session', + retainEveryNTurns: 10, + retainOverlapTurns: 2, + retainContext: 'opencode', + retainTags: [], + retainMetadata: {}, + + // Connection + hindsightApiUrl: null, + hindsightApiToken: null, + + // Bank + bankId: null, + bankIdPrefix: '', + dynamicBankId: false, + dynamicBankGranularity: ['agent', 'project'], + bankMission: '', + retainMission: null, + agentName: 'opencode', + + // Misc + debug: false, +}; + +/** Env var → config key + type mapping */ +const ENV_OVERRIDES: Record = { + HINDSIGHT_API_URL: ['hindsightApiUrl', 'string'], + HINDSIGHT_API_TOKEN: ['hindsightApiToken', 'string'], + HINDSIGHT_BANK_ID: ['bankId', 'string'], + HINDSIGHT_AGENT_NAME: ['agentName', 'string'], + HINDSIGHT_AUTO_RECALL: ['autoRecall', 'bool'], + HINDSIGHT_AUTO_RETAIN: ['autoRetain', 'bool'], + HINDSIGHT_RETAIN_MODE: ['retainMode', 'string'], + HINDSIGHT_RECALL_BUDGET: ['recallBudget', 'string'], + HINDSIGHT_RECALL_MAX_TOKENS: ['recallMaxTokens', 'int'], + HINDSIGHT_RECALL_MAX_QUERY_CHARS: ['recallMaxQueryChars', 'int'], + HINDSIGHT_RECALL_CONTEXT_TURNS: ['recallContextTurns', 'int'], + HINDSIGHT_DYNAMIC_BANK_ID: ['dynamicBankId', 'bool'], + HINDSIGHT_BANK_MISSION: ['bankMission', 'string'], + HINDSIGHT_DEBUG: ['debug', 'bool'], +}; + +function castEnv(value: string, typ: 'string' | 'bool' | 'int'): string | boolean | number | null { + if (typ === 'bool') return ['true', '1', 'yes'].includes(value.toLowerCase()); + if (typ === 'int') { + const n = parseInt(value, 10); + return isNaN(n) ? null : n; + } + return value; +} + +function loadSettingsFile(path: string): Record { + try { + const raw = readFileSync(path, 'utf-8'); + return JSON.parse(raw); + } catch { + return {}; + } +} + +export function loadConfig(pluginOptions?: Record): HindsightConfig { + // 1. Start with defaults + const config: Record = { ...DEFAULTS }; + + // 2. User config file (~/.hindsight/opencode.json) + const userConfigPath = join(homedir(), '.hindsight', 'opencode.json'); + const fileConfig = loadSettingsFile(userConfigPath); + for (const [key, value] of Object.entries(fileConfig)) { + if (value !== null && value !== undefined) { + config[key] = value; + } + } + + // 3. Plugin options (from opencode.json: ["@vectorize-io/opencode-hindsight", { ... }]) + if (pluginOptions) { + for (const [key, value] of Object.entries(pluginOptions)) { + if (value !== null && value !== undefined) { + config[key] = value; + } + } + } + + // 4. Environment variable overrides (highest priority) + for (const [envName, [key, typ]] of Object.entries(ENV_OVERRIDES)) { + const val = process.env[envName]; + if (val !== undefined) { + const castVal = castEnv(val, typ); + if (castVal !== null) { + config[key] = castVal; + } + } + } + + const result = config as unknown as HindsightConfig; + + // Validate enum-like fields to catch typos early + const VALID_RETAIN_MODES = ['full-session', 'last-turn']; + if (!VALID_RETAIN_MODES.includes(result.retainMode)) { + console.error( + `[Hindsight] Unknown retainMode "${result.retainMode}" — ` + + `valid: ${VALID_RETAIN_MODES.join(', ')}. Falling back to "full-session".`, + ); + result.retainMode = 'full-session'; + } + + const VALID_BUDGETS = ['low', 'mid', 'high']; + if (!VALID_BUDGETS.includes(result.recallBudget)) { + console.error( + `[Hindsight] Unknown recallBudget "${result.recallBudget}" — ` + + `valid: ${VALID_BUDGETS.join(', ')}. Falling back to "mid".`, + ); + result.recallBudget = 'mid'; + } + + return result; +} + +export function debugLog(config: HindsightConfig, ...args: unknown[]): void { + if (config.debug) { + console.error('[Hindsight]', ...args); + } +} diff --git a/hindsight-integrations/opencode/src/content.test.ts b/hindsight-integrations/opencode/src/content.test.ts new file mode 100644 index 000000000..ea8505f55 --- /dev/null +++ b/hindsight-integrations/opencode/src/content.test.ts @@ -0,0 +1,194 @@ +import { describe, it, expect } from 'vitest'; +import { + stripMemoryTags, + formatMemories, + formatCurrentTime, + composeRecallQuery, + truncateRecallQuery, + sliceLastTurnsByUserBoundary, + prepareRetentionTranscript, +} from './content.js'; + +describe('stripMemoryTags', () => { + it('removes blocks', () => { + const input = 'before secret after'; + expect(stripMemoryTags(input)).toBe('before after'); + }); + + it('removes blocks', () => { + const input = 'before \nmultiline\n after'; + expect(stripMemoryTags(input)).toBe('before after'); + }); + + it('removes multiple blocks', () => { + const input = 'a middle b'; + expect(stripMemoryTags(input)).toBe(' middle '); + }); + + it('returns unchanged if no tags', () => { + expect(stripMemoryTags('hello world')).toBe('hello world'); + }); +}); + +describe('formatMemories', () => { + it('formats recall results with type and date', () => { + const results = [ + { text: 'User likes Python', type: 'world', mentioned_at: '2025-01-01' }, + { text: 'Met at conference', type: 'experience', mentioned_at: '2025-03-15' }, + ]; + const formatted = formatMemories(results); + expect(formatted).toContain('- User likes Python [world] (2025-01-01)'); + expect(formatted).toContain('- Met at conference [experience] (2025-03-15)'); + }); + + it('handles missing type and date', () => { + const results = [{ text: 'Some fact' }]; + expect(formatMemories(results)).toBe('- Some fact'); + }); + + it('returns empty string for empty array', () => { + expect(formatMemories([])).toBe(''); + }); + + it('separates entries with double newlines', () => { + const results = [{ text: 'A' }, { text: 'B' }]; + expect(formatMemories(results)).toBe('- A\n\n- B'); + }); +}); + +describe('formatCurrentTime', () => { + it('returns UTC time in YYYY-MM-DD HH:MM format', () => { + const time = formatCurrentTime(); + expect(time).toMatch(/^\d{4}-\d{2}-\d{2} \d{2}:\d{2}$/); + }); +}); + +describe('composeRecallQuery', () => { + const messages = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there' }, + { role: 'user', content: 'What is my name?' }, + ]; + + it('returns latest query when contextTurns <= 1', () => { + expect(composeRecallQuery('What is my name?', messages, 1)).toBe('What is my name?'); + }); + + it('returns latest query when messages empty', () => { + expect(composeRecallQuery('query', [], 3)).toBe('query'); + }); + + it('includes prior context when contextTurns > 1', () => { + const result = composeRecallQuery('What is my name?', messages, 3); + expect(result).toContain('Prior context:'); + expect(result).toContain('user: Hello'); + expect(result).toContain('assistant: Hi there'); + expect(result).toContain('What is my name?'); + }); + + it('does not duplicate latest query in context', () => { + const result = composeRecallQuery('What is my name?', messages, 3); + // "What is my name?" should appear once at the end, not also as "user: What is my name?" + const matches = result.match(/What is my name\?/g); + expect(matches?.length).toBe(1); + }); +}); + +describe('truncateRecallQuery', () => { + it('returns query unchanged if within limit', () => { + expect(truncateRecallQuery('short', 'short', 100)).toBe('short'); + }); + + it('truncates to latest when no prior context', () => { + const latest = 'my query'; + expect(truncateRecallQuery(latest, latest, 5)).toBe('my qu'); + }); + + it('drops oldest context lines first', () => { + const query = 'Prior context:\n\nuser: old\nassistant: older\nuser: recent\n\nlatest'; + const result = truncateRecallQuery(query, 'latest', 50); + expect(result).toContain('latest'); + // Should have dropped some old context + expect(result.length).toBeLessThanOrEqual(50); + }); +}); + +describe('sliceLastTurnsByUserBoundary', () => { + const messages = [ + { role: 'user', content: 'A' }, + { role: 'assistant', content: 'B' }, + { role: 'user', content: 'C' }, + { role: 'assistant', content: 'D' }, + { role: 'user', content: 'E' }, + ]; + + it('returns last N turns', () => { + const result = sliceLastTurnsByUserBoundary(messages, 2); + expect(result.length).toBe(3); // user:C, assistant:D, user:E + expect(result[0].content).toBe('C'); + }); + + it('returns all messages if turns > available', () => { + const result = sliceLastTurnsByUserBoundary(messages, 10); + expect(result.length).toBe(5); + }); + + it('returns empty for zero turns', () => { + expect(sliceLastTurnsByUserBoundary(messages, 0)).toEqual([]); + }); + + it('returns empty for empty messages', () => { + expect(sliceLastTurnsByUserBoundary([], 2)).toEqual([]); + }); +}); + +describe('prepareRetentionTranscript', () => { + const messages = [ + { role: 'user', content: 'Hello' }, + { role: 'assistant', content: 'Hi there' }, + { role: 'user', content: 'How are you?' }, + { role: 'assistant', content: 'I am doing well' }, + ]; + + it('retains last turn by default', () => { + const { transcript, messageCount } = prepareRetentionTranscript(messages); + expect(messageCount).toBe(2); + expect(transcript).toContain('[role: user]'); + expect(transcript).toContain('How are you?'); + expect(transcript).toContain('I am doing well'); + expect(transcript).not.toContain('Hello'); + }); + + it('retains full window when requested', () => { + const { transcript, messageCount } = prepareRetentionTranscript(messages, true); + expect(messageCount).toBe(4); + expect(transcript).toContain('Hello'); + expect(transcript).toContain('How are you?'); + }); + + it('returns null for empty messages', () => { + const { transcript, messageCount } = prepareRetentionTranscript([]); + expect(transcript).toBeNull(); + expect(messageCount).toBe(0); + }); + + it('strips memory tags from content', () => { + const msgs = [ + { role: 'user', content: 'Query data' }, + { role: 'assistant', content: 'Response' }, + ]; + const { transcript } = prepareRetentionTranscript(msgs); + expect(transcript).not.toContain('hindsight_memories'); + expect(transcript).toContain('Query'); + }); + + it('skips messages with empty content after stripping', () => { + const msgs = [ + { role: 'user', content: 'only tags' }, + { role: 'assistant', content: 'Response' }, + ]; + const { transcript, messageCount } = prepareRetentionTranscript(msgs, true); + expect(messageCount).toBe(1); // only assistant message + expect(transcript).toContain('Response'); + }); +}); diff --git a/hindsight-integrations/opencode/src/content.ts b/hindsight-integrations/opencode/src/content.ts new file mode 100644 index 000000000..208d6dc54 --- /dev/null +++ b/hindsight-integrations/opencode/src/content.ts @@ -0,0 +1,178 @@ +/** + * Content processing utilities. + * + * Port of the Claude Code plugin's content.py: + * - Memory tag stripping (anti-feedback-loop) + * - Recall query composition and truncation + * - Memory formatting for context injection + * - Retention transcript formatting + */ + +/** Strip and blocks to prevent retain feedback loops. */ +export function stripMemoryTags(content: string): string { + content = content.replace(/[\s\S]*?<\/hindsight_memories>/g, ''); + content = content.replace(/[\s\S]*?<\/relevant_memories>/g, ''); + return content; +} + +export interface RecallResult { + text: string; + type?: string | null; + mentioned_at?: string | null; +} + +/** Format recall results into human-readable text for context injection. */ +export function formatMemories(results: RecallResult[]): string { + if (!results.length) return ''; + return results + .map((r) => { + const typeStr = r.type ? ` [${r.type}]` : ''; + const dateStr = r.mentioned_at ? ` (${r.mentioned_at})` : ''; + return `- ${r.text}${typeStr}${dateStr}`; + }) + .join('\n\n'); +} + +/** Format current UTC time for recall context. */ +export function formatCurrentTime(): string { + const now = new Date(); + const y = now.getUTCFullYear(); + const m = String(now.getUTCMonth() + 1).padStart(2, '0'); + const d = String(now.getUTCDate()).padStart(2, '0'); + const h = String(now.getUTCHours()).padStart(2, '0'); + const min = String(now.getUTCMinutes()).padStart(2, '0'); + return `${y}-${m}-${d} ${h}:${min}`; +} + +export interface Message { + role: string; + content: string; +} + +/** + * Compose a multi-turn recall query from conversation history. + * + * When recallContextTurns > 1, includes prior context above the latest query. + */ +export function composeRecallQuery( + latestQuery: string, + messages: Message[], + recallContextTurns: number, +): string { + const latest = latestQuery.trim(); + if (recallContextTurns <= 1 || !messages.length) return latest; + + const contextual = sliceLastTurnsByUserBoundary(messages, recallContextTurns); + const contextLines: string[] = []; + + for (const msg of contextual) { + const content = stripMemoryTags(msg.content).trim(); + if (!content) continue; + if (msg.role === 'user' && content === latest) continue; + contextLines.push(`${msg.role}: ${content}`); + } + + if (!contextLines.length) return latest; + + return ['Prior context:', contextLines.join('\n'), latest].join('\n\n'); +} + +/** + * Truncate a composed recall query to maxChars. + * Preserves the latest user message, drops oldest context lines first. + */ +export function truncateRecallQuery(query: string, latestQuery: string, maxChars: number): string { + if (maxChars <= 0 || query.length <= maxChars) return query; + + const latest = latestQuery.trim(); + const latestOnly = latest.length > maxChars ? latest.slice(0, maxChars) : latest; + + if (!query.includes('Prior context:')) return latestOnly; + + const contextMarker = 'Prior context:\n\n'; + const markerIndex = query.indexOf(contextMarker); + if (markerIndex === -1) return latestOnly; + + const suffix = '\n\n' + latest; + const suffixIndex = query.lastIndexOf(suffix); + if (suffixIndex === -1) return latestOnly; + if (suffix.length >= maxChars) return latestOnly; + + const contextBody = query.slice(markerIndex + contextMarker.length, suffixIndex); + const contextLines = contextBody.split('\n').filter(Boolean); + + const kept: string[] = []; + for (let i = contextLines.length - 1; i >= 0; i--) { + kept.unshift(contextLines[i]); + const candidate = `${contextMarker}${kept.join('\n')}${suffix}`; + if (candidate.length > maxChars) { + kept.shift(); + break; + } + } + + if (kept.length) return `${contextMarker}${kept.join('\n')}${suffix}`; + return latestOnly; +} + +/** Slice messages to the last N turns, where a turn starts at a user message. */ +export function sliceLastTurnsByUserBoundary(messages: Message[], turns: number): Message[] { + if (!messages.length || turns <= 0) return []; + + let userTurnsSeen = 0; + let startIndex = -1; + + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === 'user') { + userTurnsSeen++; + if (userTurnsSeen >= turns) { + startIndex = i; + break; + } + } + } + + return startIndex === -1 ? [...messages] : messages.slice(startIndex); +} + +/** + * Format messages into a retention transcript. + * + * Uses [role: ...]...[role:end] markers for structured retention. + */ +export function prepareRetentionTranscript( + messages: Message[], + retainFullWindow: boolean = false, +): { transcript: string | null; messageCount: number } { + if (!messages.length) return { transcript: null, messageCount: 0 }; + + let targetMessages: Message[]; + if (retainFullWindow) { + targetMessages = messages; + } else { + // Default: retain only the last turn + let lastUserIdx = -1; + for (let i = messages.length - 1; i >= 0; i--) { + if (messages[i].role === 'user') { + lastUserIdx = i; + break; + } + } + if (lastUserIdx === -1) return { transcript: null, messageCount: 0 }; + targetMessages = messages.slice(lastUserIdx); + } + + const parts: string[] = []; + for (const msg of targetMessages) { + const content = stripMemoryTags(msg.content).trim(); + if (!content) continue; + parts.push(`[role: ${msg.role}]\n${content}\n[${msg.role}:end]`); + } + + if (!parts.length) return { transcript: null, messageCount: 0 }; + + const transcript = parts.join('\n\n'); + if (transcript.trim().length < 10) return { transcript: null, messageCount: 0 }; + + return { transcript, messageCount: parts.length }; +} diff --git a/hindsight-integrations/opencode/src/hooks.test.ts b/hindsight-integrations/opencode/src/hooks.test.ts new file mode 100644 index 000000000..18b8a6ba1 --- /dev/null +++ b/hindsight-integrations/opencode/src/hooks.test.ts @@ -0,0 +1,387 @@ +import { describe, it, expect, vi, beforeEach } from 'vitest'; +import { createHooks, type PluginState } from './hooks.js'; +import type { HindsightConfig } from './config.js'; + +function makeConfig(overrides: Partial = {}): HindsightConfig { + return { + autoRecall: true, + recallBudget: 'mid', + recallMaxTokens: 1024, + recallTypes: ['world', 'experience'], + recallContextTurns: 1, + recallMaxQueryChars: 800, + recallPromptPreamble: 'Relevant memories:', + autoRetain: true, + retainMode: 'full-session', + retainEveryNTurns: 1, + retainOverlapTurns: 2, + retainContext: 'opencode', + retainTags: [], + retainMetadata: {}, + hindsightApiUrl: 'http://localhost:8888', + hindsightApiToken: null, + bankId: null, + bankIdPrefix: '', + dynamicBankId: false, + dynamicBankGranularity: ['agent', 'project'], + bankMission: '', + retainMission: null, + agentName: 'opencode', + debug: false, + ...overrides, + }; +} + +function makeState(): PluginState { + return { + turnCount: 0, + missionsSet: new Set(), + recalledSessions: new Set(), + lastRetainedTurn: new Map(), + }; +} + +function makeClient() { + return { + retain: vi.fn().mockResolvedValue({}), + recall: vi.fn().mockResolvedValue({ results: [] }), + reflect: vi.fn().mockResolvedValue({ text: '' }), + createBank: vi.fn().mockResolvedValue({}), + } as any; +} + +function makeOpencodeClient(messages: Array<{ role: string; parts: Array<{ type: string; text?: string }> }> = []) { + return { + session: { + messages: vi.fn().mockResolvedValue({ data: messages }), + }, + }; +} + +describe('createHooks', () => { + it('returns all required hooks', () => { + const hooks = createHooks(makeClient(), 'bank', makeConfig(), makeState(), makeOpencodeClient()); + expect(hooks.event).toBeDefined(); + expect(hooks['experimental.session.compacting']).toBeDefined(); + expect(hooks['experimental.chat.system.transform']).toBeDefined(); + }); +}); + +describe('event hook — session.idle', () => { + it('auto-retains conversation on session.idle with document_id', async () => { + const client = makeClient(); + const messages = [ + { role: 'user', parts: [{ type: 'text', text: 'Hello' }] }, + { role: 'assistant', parts: [{ type: 'text', text: 'Hi there' }] }, + ]; + const opencodeClient = makeOpencodeClient(messages); + const state = makeState(); + const hooks = createHooks(client, 'bank', makeConfig(), state, opencodeClient); + + await hooks.event({ + event: { type: 'session.idle', properties: { sessionID: 'sess-1' } }, + }); + + expect(client.retain).toHaveBeenCalledTimes(1); + expect(client.retain.mock.calls[0][0]).toBe('bank'); + // Full-session mode uses session ID as document_id + const opts = client.retain.mock.calls[0][2]; + expect(opts.documentId).toBe('sess-1'); + expect(opts.metadata.session_id).toBe('sess-1'); + }); + + it('skips retain when autoRetain is false', async () => { + const client = makeClient(); + const messages = [ + { role: 'user', parts: [{ type: 'text', text: 'Hello' }] }, + { role: 'assistant', parts: [{ type: 'text', text: 'Hi' }] }, + ]; + const hooks = createHooks( + client, + 'bank', + makeConfig({ autoRetain: false }), + makeState(), + makeOpencodeClient(messages), + ); + + await hooks.event({ + event: { type: 'session.idle', properties: { sessionID: 'sess-1' } }, + }); + + expect(client.retain).not.toHaveBeenCalled(); + }); + + it('uses chunked document_id with overlap in last-turn mode', async () => { + const client = makeClient(); + const messages = [ + { role: 'user', parts: [{ type: 'text', text: 'Turn 1' }] }, + { role: 'assistant', parts: [{ type: 'text', text: 'Reply 1' }] }, + { role: 'user', parts: [{ type: 'text', text: 'Turn 2' }] }, + { role: 'assistant', parts: [{ type: 'text', text: 'Reply 2' }] }, + ]; + const config = makeConfig({ retainMode: 'last-turn', retainEveryNTurns: 1, retainOverlapTurns: 1 }); + const state = makeState(); + const hooks = createHooks(client, 'bank', config, state, makeOpencodeClient(messages)); + + await hooks.event({ + event: { type: 'session.idle', properties: { sessionID: 'sess-1' } }, + }); + + expect(client.retain).toHaveBeenCalledTimes(1); + const opts = client.retain.mock.calls[0][2]; + // Chunked mode uses session-timestamp format + expect(opts.documentId).toMatch(/^sess-1-\d+$/); + }); + + it('respects retainEveryNTurns', async () => { + const client = makeClient(); + const messages = [ + { role: 'user', parts: [{ type: 'text', text: 'Hello' }] }, + { role: 'assistant', parts: [{ type: 'text', text: 'Hi' }] }, + ]; + const config = makeConfig({ retainEveryNTurns: 5 }); + const state = makeState(); + const hooks = createHooks(client, 'bank', config, state, makeOpencodeClient(messages)); + + await hooks.event({ + event: { type: 'session.idle', properties: { sessionID: 'sess-1' } }, + }); + + // Only 1 user turn, needs 5 — should not retain + expect(client.retain).not.toHaveBeenCalled(); + }); + + it('does not throw on client error', async () => { + const client = makeClient(); + client.retain.mockRejectedValue(new Error('Network error')); + const messages = [ + { role: 'user', parts: [{ type: 'text', text: 'Hello' }] }, + { role: 'assistant', parts: [{ type: 'text', text: 'Hi' }] }, + ]; + const hooks = createHooks(client, 'bank', makeConfig(), makeState(), makeOpencodeClient(messages)); + + await expect( + hooks.event({ + event: { type: 'session.idle', properties: { sessionID: 'sess-1' } }, + }), + ).resolves.not.toThrow(); + }); +}); + +describe('event hook — session.created', () => { + it('tracks session for recall injection', async () => { + const state = makeState(); + const hooks = createHooks(makeClient(), 'bank', makeConfig(), state, makeOpencodeClient()); + + await hooks.event({ + event: { + type: 'session.created', + properties: { info: { id: 'sess-1', title: 'Test' } }, + }, + }); + + expect(state.recalledSessions.has('sess-1')).toBe(true); + }); + + it('does not track when autoRecall is false', async () => { + const state = makeState(); + const hooks = createHooks( + makeClient(), + 'bank', + makeConfig({ autoRecall: false }), + state, + makeOpencodeClient(), + ); + + await hooks.event({ + event: { + type: 'session.created', + properties: { info: { id: 'sess-1' } }, + }, + }); + + expect(state.recalledSessions.has('sess-1')).toBe(false); + }); +}); + +describe('compacting hook', () => { + it('retains before compaction and recalls context', async () => { + const client = makeClient(); + client.recall.mockResolvedValue({ + results: [{ text: 'Important fact', type: 'world' }], + }); + const messages = [ + { role: 'user', parts: [{ type: 'text', text: 'Build the feature' }] }, + { role: 'assistant', parts: [{ type: 'text', text: 'Working on it' }] }, + ]; + const output = { context: [] as string[], prompt: undefined }; + const hooks = createHooks(client, 'bank', makeConfig(), makeState(), makeOpencodeClient(messages)); + + await hooks['experimental.session.compacting']({ sessionID: 'sess-1' }, output); + + // Should have retained and recalled + expect(client.retain).toHaveBeenCalled(); + expect(client.recall).toHaveBeenCalled(); + expect(output.context.length).toBeGreaterThan(0); + expect(output.context[0]).toContain('hindsight_memories'); + expect(output.context[0]).toContain('Important fact'); + }); + + it('pre-compaction retain includes documentId and session metadata', async () => { + const client = makeClient(); + client.recall.mockResolvedValue({ results: [] }); + const messages = [ + { role: 'user', parts: [{ type: 'text', text: 'Hello' }] }, + { role: 'assistant', parts: [{ type: 'text', text: 'Hi' }] }, + ]; + const output = { context: [] as string[] }; + const hooks = createHooks(client, 'bank', makeConfig(), makeState(), makeOpencodeClient(messages)); + + await hooks['experimental.session.compacting']({ sessionID: 'sess-1' }, output); + + expect(client.retain).toHaveBeenCalledTimes(1); + const opts = client.retain.mock.calls[0][2]; + expect(opts.documentId).toBe('sess-1'); + expect(opts.metadata.session_id).toBe('sess-1'); + }); + + it('pre-compaction retain uses chunked documentId in last-turn mode', async () => { + const client = makeClient(); + client.recall.mockResolvedValue({ results: [] }); + const messages = [ + { role: 'user', parts: [{ type: 'text', text: 'Hello' }] }, + { role: 'assistant', parts: [{ type: 'text', text: 'Hi' }] }, + ]; + const config = makeConfig({ retainMode: 'last-turn', retainEveryNTurns: 1 }); + const output = { context: [] as string[] }; + const hooks = createHooks(client, 'bank', config, makeState(), makeOpencodeClient(messages)); + + await hooks['experimental.session.compacting']({ sessionID: 'sess-1' }, output); + + const opts = client.retain.mock.calls[0][2]; + expect(opts.documentId).toMatch(/^sess-1-\d+$/); + }); + + it('does not throw on error', async () => { + const client = makeClient(); + client.recall.mockRejectedValue(new Error('Failed')); + const messages = [ + { role: 'user', parts: [{ type: 'text', text: 'Test' }] }, + ]; + const output = { context: [] as string[] }; + const hooks = createHooks(client, 'bank', makeConfig(), makeState(), makeOpencodeClient(messages)); + + await expect( + hooks['experimental.session.compacting']({ sessionID: 's' }, output), + ).resolves.not.toThrow(); + }); +}); + +describe('system transform hook', () => { + it('injects memories for tracked sessions', async () => { + const client = makeClient(); + client.recall.mockResolvedValue({ + results: [{ text: 'User is a developer', type: 'world' }], + }); + const state = makeState(); + state.recalledSessions.add('sess-1'); + const output = { system: [] as string[] }; + const hooks = createHooks(client, 'bank', makeConfig(), state, makeOpencodeClient()); + + await hooks['experimental.chat.system.transform']( + { sessionID: 'sess-1', model: {} }, + output, + ); + + expect(output.system.length).toBeGreaterThan(0); + expect(output.system[0]).toContain('hindsight_memories'); + // Session should be removed after first injection + expect(state.recalledSessions.has('sess-1')).toBe(false); + }); + + it('skips untracked sessions', async () => { + const client = makeClient(); + const state = makeState(); + const output = { system: [] as string[] }; + const hooks = createHooks(client, 'bank', makeConfig(), state, makeOpencodeClient()); + + await hooks['experimental.chat.system.transform']( + { sessionID: 'sess-unknown', model: {} }, + output, + ); + + expect(output.system.length).toBe(0); + expect(client.recall).not.toHaveBeenCalled(); + }); + + it('consumes session on empty recall (no repeated queries for empty banks)', async () => { + const client = makeClient(); + // No results — empty bank + client.recall.mockResolvedValue({ results: [] }); + const state = makeState(); + state.recalledSessions.add('sess-1'); + const output = { system: [] as string[] }; + const hooks = createHooks(client, 'bank', makeConfig(), state, makeOpencodeClient()); + + await hooks['experimental.chat.system.transform']( + { sessionID: 'sess-1', model: {} }, + output, + ); + + // No injection, but session consumed — won't re-query on next transform + expect(output.system.length).toBe(0); + expect(state.recalledSessions.has('sess-1')).toBe(false); + }); + + it('retries recall on next transform after transient API failure', async () => { + const client = makeClient(); + // First call: API error (transient) + client.recall.mockRejectedValueOnce(new Error('Connection refused')); + // Second call: succeeds + client.recall.mockResolvedValueOnce({ + results: [{ text: 'Found it', type: 'world' }], + }); + const state = makeState(); + state.recalledSessions.add('sess-1'); + const hooks = createHooks(client, 'bank', makeConfig(), state, makeOpencodeClient()); + + // First attempt — API error, session preserved for retry + const output1 = { system: [] as string[] }; + await hooks['experimental.chat.system.transform']( + { sessionID: 'sess-1', model: {} }, + output1, + ); + expect(output1.system.length).toBe(0); + expect(state.recalledSessions.has('sess-1')).toBe(true); + + // Second attempt — succeeds, session consumed + const output2 = { system: [] as string[] }; + await hooks['experimental.chat.system.transform']( + { sessionID: 'sess-1', model: {} }, + output2, + ); + expect(output2.system.length).toBeGreaterThan(0); + expect(state.recalledSessions.has('sess-1')).toBe(false); + }); + + it('skips when autoRecall is false', async () => { + const client = makeClient(); + const state = makeState(); + state.recalledSessions.add('sess-1'); + const output = { system: [] as string[] }; + const hooks = createHooks( + client, + 'bank', + makeConfig({ autoRecall: false }), + state, + makeOpencodeClient(), + ); + + await hooks['experimental.chat.system.transform']( + { sessionID: 'sess-1', model: {} }, + output, + ); + + expect(output.system.length).toBe(0); + }); +}); diff --git a/hindsight-integrations/opencode/src/hooks.ts b/hindsight-integrations/opencode/src/hooks.ts new file mode 100644 index 000000000..3db7d72f2 --- /dev/null +++ b/hindsight-integrations/opencode/src/hooks.ts @@ -0,0 +1,308 @@ +/** + * Hook implementations for the Hindsight OpenCode plugin. + * + * Hooks: + * - event (session.created) → recall memories and inject into system prompt + * - event (session.idle) → auto-retain conversation transcript + * - experimental.session.compacting → inject memories into compaction context + */ + +import type { HindsightClient } from '@vectorize-io/hindsight-client'; +import type { HindsightConfig } from './config.js'; +import { debugLog } from './config.js'; +import { + formatMemories, + formatCurrentTime, + stripMemoryTags, + composeRecallQuery, + truncateRecallQuery, + prepareRetentionTranscript, + sliceLastTurnsByUserBoundary, + type Message, +} from './content.js'; +import { ensureBankMission } from './bank.js'; + +export interface PluginState { + turnCount: number; + missionsSet: Set; + /** Track sessions we've already injected recall into */ + recalledSessions: Set; + /** Track last retained turn count per session to avoid duplicates */ + lastRetainedTurn: Map; +} + +interface EventInput { + event: { + type: string; + properties: Record; + }; +} + +interface CompactingInput { + sessionID: string; +} + +interface CompactingOutput { + context: string[]; + prompt?: string; +} + +interface SystemTransformInput { + sessionID?: string; + model: unknown; +} + +interface SystemTransformOutput { + system: string[]; +} + +type OpencodeClient = { + session: { + messages: (opts: { path: { id: string } }) => Promise<{ data?: Array<{ role: string; parts?: Array<{ type: string; text?: string }> }> }>; + }; +}; + +export interface HindsightHooks { + event: (input: EventInput) => Promise; + 'experimental.session.compacting': ( + input: CompactingInput, + output: CompactingOutput, + ) => Promise; + 'experimental.chat.system.transform': ( + input: SystemTransformInput, + output: SystemTransformOutput, + ) => Promise; +} + +export function createHooks( + hindsightClient: HindsightClient, + bankId: string, + config: HindsightConfig, + state: PluginState, + opencodeClient: OpencodeClient, +): HindsightHooks { + interface RecallOutcome { + /** formatted context string, or null if no results */ + context: string | null; + /** true if the API call succeeded (even with 0 results) */ + ok: boolean; + } + + /** Recall memories and format as context string */ + async function recallForContext(query: string): Promise { + try { + const response = await hindsightClient.recall(bankId, query, { + budget: config.recallBudget as 'low' | 'mid' | 'high', + maxTokens: config.recallMaxTokens, + types: config.recallTypes, + }); + + const results = response.results || []; + if (!results.length) return { context: null, ok: true }; + + const formatted = formatMemories(results); + const context = + `\n` + + `${config.recallPromptPreamble}\n` + + `Current time: ${formatCurrentTime()} UTC\n\n` + + `${formatted}\n` + + ``; + return { context, ok: true }; + } catch (e) { + debugLog(config, 'Recall failed:', e); + return { context: null, ok: false }; + } + } + + /** Extract plain-text messages from an OpenCode session */ + async function getSessionMessages(sessionId: string): Promise { + try { + const response = await opencodeClient.session.messages({ + path: { id: sessionId }, + }); + const rawMessages = response.data || []; + const messages: Message[] = []; + for (const msg of rawMessages) { + const role = msg.role; + if (role !== 'user' && role !== 'assistant') continue; + const textParts = (msg.parts || []) + .filter((p: { type: string; text?: string }) => p.type === 'text' && p.text) + .map((p: { type: string; text?: string }) => p.text!); + if (textParts.length) { + messages.push({ role, content: textParts.join('\n') }); + } + } + return messages; + } catch (e) { + debugLog(config, 'Failed to get session messages:', e); + return []; + } + } + + /** + * Retain messages for a session, respecting retainMode and documentId semantics. + * Used by both idle-retain and pre-compaction retain. + */ + async function retainSession(sessionId: string, messages: Message[]): Promise { + const retainFullWindow = config.retainMode === 'full-session'; + let targetMessages: Message[]; + let documentId: string; + + if (retainFullWindow) { + targetMessages = messages; + // Full-session upserts the same document each time + documentId = sessionId; + } else { + // Sliding window: retainEveryNTurns + overlap + const windowTurns = config.retainEveryNTurns + config.retainOverlapTurns; + targetMessages = sliceLastTurnsByUserBoundary(messages, windowTurns); + // Chunked mode: unique document per chunk + documentId = `${sessionId}-${Date.now()}`; + } + + const { transcript } = prepareRetentionTranscript(targetMessages, true); + if (!transcript) return; + + await ensureBankMission(hindsightClient, bankId, config, state.missionsSet); + await hindsightClient.retain(bankId, transcript, { + documentId, + context: config.retainContext, + tags: config.retainTags.length ? config.retainTags : undefined, + metadata: Object.keys(config.retainMetadata).length + ? { ...config.retainMetadata, session_id: sessionId } + : { session_id: sessionId }, + async: true, + }); + } + + /** Auto-retain conversation transcript */ + async function handleSessionIdle(sessionId: string): Promise { + if (!config.autoRetain) return; + + const messages = await getSessionMessages(sessionId); + if (!messages.length) return; + + // Count user turns + const userTurns = messages.filter((m) => m.role === 'user').length; + const lastRetained = state.lastRetainedTurn.get(sessionId) || 0; + + // Only retain if enough new turns since last retain + if (userTurns - lastRetained < config.retainEveryNTurns) return; + + try { + await retainSession(sessionId, messages); + state.lastRetainedTurn.set(sessionId, userTurns); + debugLog(config, `Auto-retained ${messages.length} messages for session ${sessionId}`); + } catch (e) { + debugLog(config, 'Auto-retain failed:', e); + } + } + + const event = async (input: EventInput): Promise => { + try { + const { event: evt } = input; + + if (evt.type === 'session.idle') { + const sessionId = (evt.properties as { sessionID?: string }).sessionID; + if (sessionId) { + await handleSessionIdle(sessionId); + } + } + + if (evt.type === 'session.created') { + const session = evt.properties.info as { id?: string; title?: string } | undefined; + const sessionId = session?.id; + if (sessionId && config.autoRecall && !state.recalledSessions.has(sessionId)) { + state.recalledSessions.add(sessionId); + // Cap tracked sessions + if (state.recalledSessions.size > 1000) { + const first = state.recalledSessions.values().next().value; + if (first) state.recalledSessions.delete(first); + } + } + } + } catch (e) { + debugLog(config, 'Event hook error:', e); + } + }; + + const compacting = async ( + input: CompactingInput, + output: CompactingOutput, + ): Promise => { + try { + // First, retain what we have before compaction (using shared retention logic) + const messages = await getSessionMessages(input.sessionID); + if (messages.length && config.autoRetain) { + try { + await retainSession(input.sessionID, messages); + debugLog(config, 'Pre-compaction retain completed'); + } catch (e) { + debugLog(config, 'Pre-compaction retain failed:', e); + } + } + + // Then recall relevant memories to inject into compaction context + if (messages.length) { + const lastUserMsg = [...messages].reverse().find((m) => m.role === 'user'); + if (lastUserMsg) { + const query = composeRecallQuery( + lastUserMsg.content, + messages, + config.recallContextTurns, + ); + const truncated = truncateRecallQuery( + query, + lastUserMsg.content, + config.recallMaxQueryChars, + ); + const { context } = await recallForContext(truncated); + if (context) { + output.context.push(context); + } + } + } + } catch (e) { + debugLog(config, 'Compaction hook error:', e); + } + }; + + const systemTransform = async ( + input: SystemTransformInput, + output: SystemTransformOutput, + ): Promise => { + try { + if (!config.autoRecall) return; + const sessionId = input.sessionID; + if (!sessionId) return; + + // Only inject on first message of a session (tracked by recalledSessions) + if (!state.recalledSessions.has(sessionId)) return; + + await ensureBankMission(hindsightClient, bankId, config, state.missionsSet); + + // Use a generic project-context query for session start + const query = `project context and recent work`; + const { context, ok } = await recallForContext(query); + + // Consume after a successful API round-trip (even with 0 results). + // Only preserve retry for transient API failures (ok=false). + if (ok) { + state.recalledSessions.delete(sessionId); + } + + if (context) { + output.system.push(context); + debugLog(config, `Injected recall context for session ${sessionId}`); + } + } catch (e) { + debugLog(config, 'System transform hook error:', e); + } + }; + + return { + event, + 'experimental.session.compacting': compacting, + 'experimental.chat.system.transform': systemTransform, + }; +} diff --git a/hindsight-integrations/opencode/src/index.ts b/hindsight-integrations/opencode/src/index.ts new file mode 100644 index 000000000..4f4967b40 --- /dev/null +++ b/hindsight-integrations/opencode/src/index.ts @@ -0,0 +1,80 @@ +/** + * Hindsight OpenCode Plugin — persistent long-term memory for OpenCode agents. + * + * Provides: + * - Custom tools: hindsight_retain, hindsight_recall, hindsight_reflect + * - Auto-retain on session.idle + * - Memory injection on session.created via system transform + * - Memory preservation during context compaction + * + * @example + * ```json + * // opencode.json + * { "plugin": ["@vectorize-io/opencode-hindsight"] } + * + * // With options: + * { "plugin": [["@vectorize-io/opencode-hindsight", { "bankId": "my-bank" }]] } + * ``` + */ + +import type { Plugin, PluginModule } from '@opencode-ai/plugin'; +import { HindsightClient } from '@vectorize-io/hindsight-client'; +import { loadConfig } from './config.js'; +import { deriveBankId } from './bank.js'; +import { createTools } from './tools.js'; +import { createHooks, type PluginState } from './hooks.js'; +import { debugLog } from './config.js'; + +const HindsightPlugin: Plugin = async (input, options) => { + const config = loadConfig(options); + + const apiUrl = config.hindsightApiUrl; + if (!apiUrl) { + console.error( + '[Hindsight] No API URL configured. Set HINDSIGHT_API_URL environment variable ' + + 'or add hindsightApiUrl to ~/.hindsight/opencode.json', + ); + // Return empty hooks — graceful degradation + return {}; + } + + const client = new HindsightClient({ + baseUrl: apiUrl, + apiKey: config.hindsightApiToken || undefined, + }); + + const bankId = deriveBankId(config, input.directory); + debugLog(config, `Initialized with bank: ${bankId}, API: ${apiUrl}`); + + const state: PluginState = { + turnCount: 0, + missionsSet: new Set(), + recalledSessions: new Set(), + lastRetainedTurn: new Map(), + }; + + const tools = createTools(client, bankId, config, state.missionsSet); + const hooks = createHooks(client, bankId, config, state, input.client as unknown as Parameters[4]); + + return { + tool: tools, + ...hooks, + }; +}; + +// Named export for direct import +export { HindsightPlugin }; + +// Default export as PluginModule for OpenCode plugin loader +const module: PluginModule = { + id: 'hindsight', + server: HindsightPlugin, +}; + +export default module; + +// Re-export types for consumers +export type { HindsightConfig } from './config.js'; +export type { PluginState } from './hooks.js'; +export { loadConfig } from './config.js'; +export { deriveBankId } from './bank.js'; diff --git a/hindsight-integrations/opencode/src/plugin.test.ts b/hindsight-integrations/opencode/src/plugin.test.ts new file mode 100644 index 000000000..77ef3c6d2 --- /dev/null +++ b/hindsight-integrations/opencode/src/plugin.test.ts @@ -0,0 +1,102 @@ +import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest'; + +// Mock the HindsightClient before importing the plugin +vi.mock('@vectorize-io/hindsight-client', () => { + const MockHindsightClient = vi.fn(function (this: any) { + this.retain = vi.fn().mockResolvedValue({}); + this.recall = vi.fn().mockResolvedValue({ results: [] }); + this.reflect = vi.fn().mockResolvedValue({ text: '' }); + this.createBank = vi.fn().mockResolvedValue({}); + }); + return { HindsightClient: MockHindsightClient }; +}); + +import { HindsightPlugin } from './index.js'; +import { HindsightClient } from '@vectorize-io/hindsight-client'; + +const mockPluginInput = { + client: { + session: { + messages: vi.fn().mockResolvedValue({ data: [] }), + }, + }, + project: { id: 'test-project', worktree: '/tmp/test', vcs: 'git' }, + directory: '/tmp/test-project', + worktree: '/tmp/test-project', + serverUrl: new URL('http://localhost:3000'), + $: {} as any, +}; + +describe('HindsightPlugin', () => { + const originalEnv = { ...process.env }; + + beforeEach(() => { + for (const key of Object.keys(process.env)) { + if (key.startsWith('HINDSIGHT_')) delete process.env[key]; + } + vi.clearAllMocks(); + }); + + afterEach(() => { + process.env = { ...originalEnv }; + }); + + it('returns empty hooks when no API URL configured', async () => { + const result = await HindsightPlugin(mockPluginInput as any); + expect(result).toEqual({}); + expect(HindsightClient).not.toHaveBeenCalled(); + }); + + it('returns tools and hooks when configured', async () => { + process.env.HINDSIGHT_API_URL = 'http://localhost:8888'; + + const result = await HindsightPlugin(mockPluginInput as any); + + expect(HindsightClient).toHaveBeenCalledWith({ + baseUrl: 'http://localhost:8888', + apiKey: undefined, + }); + + expect(result.tool).toBeDefined(); + expect(result.tool!.hindsight_retain).toBeDefined(); + expect(result.tool!.hindsight_recall).toBeDefined(); + expect(result.tool!.hindsight_reflect).toBeDefined(); + expect(result.event).toBeDefined(); + expect(result['experimental.session.compacting']).toBeDefined(); + expect(result['experimental.chat.system.transform']).toBeDefined(); + }); + + it('passes API key when configured', async () => { + process.env.HINDSIGHT_API_URL = 'http://localhost:8888'; + process.env.HINDSIGHT_API_TOKEN = 'my-token'; + + await HindsightPlugin(mockPluginInput as any); + + expect(HindsightClient).toHaveBeenCalledWith({ + baseUrl: 'http://localhost:8888', + apiKey: 'my-token', + }); + }); + + it('accepts plugin options', async () => { + const result = await HindsightPlugin(mockPluginInput as any, { + hindsightApiUrl: 'http://example.com', + bankId: 'custom-bank', + }); + + expect(result.tool).toBeDefined(); + expect(HindsightClient).toHaveBeenCalledWith({ + baseUrl: 'http://example.com', + apiKey: undefined, + }); + }); +}); + +describe('PluginModule default export', () => { + it('exports correct module shape', async () => { + const mod = await import('./index.js'); + expect(mod.default).toBeDefined(); + expect(mod.default.id).toBe('hindsight'); + expect(typeof mod.default.server).toBe('function'); + }); +}); diff --git a/hindsight-integrations/opencode/src/tools.test.ts b/hindsight-integrations/opencode/src/tools.test.ts new file mode 100644 index 000000000..6e8f4dc92 --- /dev/null +++ b/hindsight-integrations/opencode/src/tools.test.ts @@ -0,0 +1,348 @@ +import { describe, it, expect, vi } from 'vitest'; +import { createTools } from './tools.js'; +import type { HindsightConfig } from './config.js'; + +function makeConfig(overrides: Partial = {}): HindsightConfig { + return { + autoRecall: true, + recallBudget: 'mid', + recallMaxTokens: 1024, + recallTypes: ['world', 'experience'], + recallContextTurns: 1, + recallMaxQueryChars: 800, + recallPromptPreamble: '', + autoRetain: true, + retainMode: 'full-session', + retainEveryNTurns: 10, + retainOverlapTurns: 2, + retainContext: 'opencode', + retainTags: [], + retainMetadata: {}, + hindsightApiUrl: null, + hindsightApiToken: null, + bankId: null, + bankIdPrefix: '', + dynamicBankId: false, + dynamicBankGranularity: ['agent', 'project'], + bankMission: '', + retainMission: null, + agentName: 'opencode', + debug: false, + ...overrides, + }; +} + +const mockContext = { + sessionID: 'sess-1', + messageID: 'msg-1', + agent: 'default', + directory: '/tmp', + worktree: '/tmp', + abort: new AbortController().signal, + metadata: vi.fn(), + ask: vi.fn(), +}; + +describe('createTools', () => { + it('creates all three tools', () => { + const client = { retain: vi.fn(), recall: vi.fn(), reflect: vi.fn() } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + expect(tools.hindsight_retain).toBeDefined(); + expect(tools.hindsight_recall).toBeDefined(); + expect(tools.hindsight_reflect).toBeDefined(); + }); + + it('all tools have description and execute', () => { + const client = { retain: vi.fn(), recall: vi.fn(), reflect: vi.fn() } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + for (const tool of Object.values(tools)) { + expect(tool.description).toBeTruthy(); + expect(typeof tool.execute).toBe('function'); + } + }); + + describe('hindsight_retain', () => { + it('calls client.retain with correct bank and content', async () => { + const client = { + retain: vi.fn().mockResolvedValue({}), + recall: vi.fn(), + reflect: vi.fn(), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + const result = await tools.hindsight_retain.execute( + { content: 'User likes TypeScript' }, + mockContext, + ); + + expect(client.retain).toHaveBeenCalledWith('test-bank', 'User likes TypeScript', { + context: 'opencode', + tags: undefined, + metadata: undefined, + }); + expect(result).toBe('Memory stored successfully.'); + }); + + it('passes optional context', async () => { + const client = { retain: vi.fn().mockResolvedValue({}), recall: vi.fn(), reflect: vi.fn() } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + await tools.hindsight_retain.execute( + { content: 'Fact', context: 'from conversation' }, + mockContext, + ); + + expect(client.retain).toHaveBeenCalledWith('test-bank', 'Fact', { + context: 'from conversation', + tags: undefined, + metadata: undefined, + }); + }); + + it('includes tags and metadata from config', async () => { + const client = { retain: vi.fn().mockResolvedValue({}), recall: vi.fn(), reflect: vi.fn() } as any; + const config = makeConfig({ + retainTags: ['coding'], + retainMetadata: { source: 'opencode' }, + }); + const tools = createTools(client, 'test-bank', config); + + await tools.hindsight_retain.execute({ content: 'Fact' }, mockContext); + + expect(client.retain).toHaveBeenCalledWith('test-bank', 'Fact', { + context: 'opencode', + tags: ['coding'], + metadata: { source: 'opencode' }, + }); + }); + }); + + describe('hindsight_recall', () => { + it('calls client.recall and formats results', async () => { + const client = { + retain: vi.fn(), + recall: vi.fn().mockResolvedValue({ + results: [ + { text: 'User likes Python', type: 'world', mentioned_at: '2025-01-01' }, + ], + }), + reflect: vi.fn(), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + const result = await tools.hindsight_recall.execute( + { query: 'user preferences' }, + mockContext, + ); + + expect(client.recall).toHaveBeenCalledWith('test-bank', 'user preferences', { + budget: 'mid', + maxTokens: 1024, + types: ['world', 'experience'], + }); + expect(result).toContain('User likes Python'); + expect(result).toContain('[world]'); + }); + + it('returns no-results message when empty', async () => { + const client = { + retain: vi.fn(), + recall: vi.fn().mockResolvedValue({ results: [] }), + reflect: vi.fn(), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + const result = await tools.hindsight_recall.execute({ query: 'unknown' }, mockContext); + expect(result).toBe('No relevant memories found.'); + }); + + it('uses config budget settings', async () => { + const client = { + retain: vi.fn(), + recall: vi.fn().mockResolvedValue({ results: [] }), + reflect: vi.fn(), + } as any; + const config = makeConfig({ recallBudget: 'high', recallMaxTokens: 4096 }); + const tools = createTools(client, 'test-bank', config); + + await tools.hindsight_recall.execute({ query: 'test' }, mockContext); + + expect(client.recall).toHaveBeenCalledWith('test-bank', 'test', { + budget: 'high', + maxTokens: 4096, + types: ['world', 'experience'], + }); + }); + }); + + describe('hindsight_reflect', () => { + it('calls client.reflect and returns text', async () => { + const client = { + retain: vi.fn(), + recall: vi.fn(), + reflect: vi.fn().mockResolvedValue({ text: 'The user is a Python developer.' }), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + const result = await tools.hindsight_reflect.execute( + { query: 'What do I know about this user?' }, + mockContext, + ); + + expect(client.reflect).toHaveBeenCalledWith( + 'test-bank', + 'What do I know about this user?', + { context: undefined, budget: 'mid' }, + ); + expect(result).toBe('The user is a Python developer.'); + }); + + it('returns fallback when no text', async () => { + const client = { + retain: vi.fn(), + recall: vi.fn(), + reflect: vi.fn().mockResolvedValue({ text: '' }), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + const result = await tools.hindsight_reflect.execute( + { query: 'something' }, + mockContext, + ); + expect(result).toBe('No relevant information found to reflect on.'); + }); + + it('passes context to reflect', async () => { + const client = { + retain: vi.fn(), + recall: vi.fn(), + reflect: vi.fn().mockResolvedValue({ text: 'Answer' }), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + await tools.hindsight_reflect.execute( + { query: 'Q', context: 'We are building an app' }, + mockContext, + ); + + expect(client.reflect).toHaveBeenCalledWith('test-bank', 'Q', { + context: 'We are building an app', + budget: 'mid', + }); + }); + }); + + describe('error propagation', () => { + it('propagates retain errors', async () => { + const client = { + retain: vi.fn().mockRejectedValue(new Error('Network error')), + recall: vi.fn(), + reflect: vi.fn(), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + await expect( + tools.hindsight_retain.execute({ content: 'test' }, mockContext), + ).rejects.toThrow('Network error'); + }); + + it('propagates recall errors', async () => { + const client = { + retain: vi.fn(), + recall: vi.fn().mockRejectedValue(new Error('Timeout')), + reflect: vi.fn(), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + await expect( + tools.hindsight_recall.execute({ query: 'test' }, mockContext), + ).rejects.toThrow('Timeout'); + }); + + it('propagates reflect errors', async () => { + const client = { + retain: vi.fn(), + recall: vi.fn(), + reflect: vi.fn().mockRejectedValue(new Error('Server error')), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + await expect( + tools.hindsight_reflect.execute({ query: 'test' }, mockContext), + ).rejects.toThrow('Server error'); + }); + }); + + describe('bank mission setup', () => { + it('calls ensureBankMission before retain when missionsSet provided', async () => { + const client = { + retain: vi.fn().mockResolvedValue({}), + recall: vi.fn(), + reflect: vi.fn(), + createBank: vi.fn().mockResolvedValue({}), + } as any; + const missionsSet = new Set(); + const config = makeConfig({ bankMission: 'Extract technical decisions' }); + const tools = createTools(client, 'test-bank', config, missionsSet); + + await tools.hindsight_retain.execute({ content: 'fact' }, mockContext); + + expect(client.createBank).toHaveBeenCalledWith('test-bank', { + reflectMission: 'Extract technical decisions', + retainMission: undefined, + }); + expect(missionsSet.has('test-bank')).toBe(true); + expect(client.retain).toHaveBeenCalled(); + }); + + it('calls ensureBankMission before reflect when missionsSet provided', async () => { + const client = { + retain: vi.fn(), + recall: vi.fn(), + reflect: vi.fn().mockResolvedValue({ text: 'answer' }), + createBank: vi.fn().mockResolvedValue({}), + } as any; + const missionsSet = new Set(); + const config = makeConfig({ bankMission: 'Synthesize project context' }); + const tools = createTools(client, 'test-bank', config, missionsSet); + + await tools.hindsight_reflect.execute({ query: 'summary' }, mockContext); + + expect(client.createBank).toHaveBeenCalled(); + expect(client.reflect).toHaveBeenCalled(); + }); + + it('skips mission setup when missionsSet not provided (backward compat)', async () => { + const client = { + retain: vi.fn().mockResolvedValue({}), + recall: vi.fn(), + reflect: vi.fn(), + } as any; + const tools = createTools(client, 'test-bank', makeConfig()); + + await tools.hindsight_retain.execute({ content: 'fact' }, mockContext); + + expect(client.retain).toHaveBeenCalled(); + // No createBank call since missionsSet wasn't passed + }); + }); + + it('always uses constructor bankId', async () => { + const client = { + retain: vi.fn().mockResolvedValue({}), + recall: vi.fn().mockResolvedValue({ results: [] }), + reflect: vi.fn().mockResolvedValue({ text: 'ok' }), + } as any; + const tools = createTools(client, 'fixed-bank', makeConfig()); + + await tools.hindsight_retain.execute({ content: 'x' }, mockContext); + await tools.hindsight_recall.execute({ query: 'x' }, mockContext); + await tools.hindsight_reflect.execute({ query: 'x' }, mockContext); + + expect(client.retain.mock.calls[0][0]).toBe('fixed-bank'); + expect(client.recall.mock.calls[0][0]).toBe('fixed-bank'); + expect(client.reflect.mock.calls[0][0]).toBe('fixed-bank'); + }); +}); diff --git a/hindsight-integrations/opencode/src/tools.ts b/hindsight-integrations/opencode/src/tools.ts new file mode 100644 index 000000000..78aacf9e7 --- /dev/null +++ b/hindsight-integrations/opencode/src/tools.ts @@ -0,0 +1,110 @@ +/** + * Custom tool definitions for the Hindsight OpenCode plugin. + * + * Registers hindsight_retain, hindsight_recall, and hindsight_reflect + * as tools the agent can call explicitly. + */ + +import { tool } from '@opencode-ai/plugin/tool'; +import type { ToolDefinition } from '@opencode-ai/plugin/tool'; +import type { HindsightClient } from '@vectorize-io/hindsight-client'; +import type { HindsightConfig } from './config.js'; +import { formatMemories, formatCurrentTime } from './content.js'; +import { ensureBankMission } from './bank.js'; +import type { PluginState } from './hooks.js'; + +export interface HindsightTools { + hindsight_retain: ToolDefinition; + hindsight_recall: ToolDefinition; + hindsight_reflect: ToolDefinition; +} + +export function createTools( + client: HindsightClient, + bankId: string, + config: HindsightConfig, + missionsSet?: Set, +): HindsightTools { + const hindsight_retain = tool({ + description: + 'Store information in long-term memory. Use this to remember important facts, ' + + 'user preferences, project context, decisions, and anything worth recalling in future sessions. ' + + 'Be specific — include who, what, when, and why.', + args: { + content: tool.schema.string().describe( + 'The information to remember. Be specific and self-contained.', + ), + context: tool.schema + .string() + .optional() + .describe('Optional context about where this information came from.'), + }, + async execute(args) { + if (missionsSet) { + await ensureBankMission(client, bankId, config, missionsSet); + } + await client.retain(bankId, args.content, { + context: args.context || config.retainContext, + tags: config.retainTags.length ? config.retainTags : undefined, + metadata: Object.keys(config.retainMetadata).length + ? config.retainMetadata + : undefined, + }); + return 'Memory stored successfully.'; + }, + }); + + const hindsight_recall = tool({ + description: + 'Search long-term memory for relevant information. Use this proactively before ' + + 'answering questions about past conversations, user preferences, project history, ' + + 'or any topic where prior context would help. When in doubt, recall first.', + args: { + query: tool.schema.string().describe( + 'Natural language search query. Be specific about what you need to know.', + ), + }, + async execute(args) { + const response = await client.recall(bankId, args.query, { + budget: config.recallBudget as 'low' | 'mid' | 'high', + maxTokens: config.recallMaxTokens, + types: config.recallTypes, + }); + + const results = response.results || []; + if (!results.length) return 'No relevant memories found.'; + + const formatted = formatMemories(results); + return `Found ${results.length} relevant memories (as of ${formatCurrentTime()} UTC):\n\n${formatted}`; + }, + }); + + const hindsight_reflect = tool({ + description: + 'Generate a thoughtful answer using long-term memory. Unlike recall (which returns ' + + 'raw memories), reflect synthesizes memories into a coherent answer. Use for questions ' + + 'like "What do you know about this user?" or "Summarize our project decisions."', + args: { + query: tool.schema.string().describe( + 'The question to answer using long-term memory.', + ), + context: tool.schema + .string() + .optional() + .describe('Optional additional context to guide the reflection.'), + }, + async execute(args) { + if (missionsSet) { + await ensureBankMission(client, bankId, config, missionsSet); + } + const response = await client.reflect(bankId, args.query, { + context: args.context, + budget: config.recallBudget as 'low' | 'mid' | 'high', + }); + + return response.text || 'No relevant information found to reflect on.'; + }, + }); + + return { hindsight_retain, hindsight_recall, hindsight_reflect }; +} diff --git a/hindsight-integrations/opencode/tsconfig.json b/hindsight-integrations/opencode/tsconfig.json new file mode 100644 index 000000000..518d02778 --- /dev/null +++ b/hindsight-integrations/opencode/tsconfig.json @@ -0,0 +1,18 @@ +{ + "compilerOptions": { + "target": "ES2022", + "module": "ES2022", + "lib": ["ES2022"], + "moduleResolution": "bundler", + "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/opencode/tsup.config.ts b/hindsight-integrations/opencode/tsup.config.ts new file mode 100644 index 000000000..c536a42d4 --- /dev/null +++ b/hindsight-integrations/opencode/tsup.config.ts @@ -0,0 +1,11 @@ +import { defineConfig } from 'tsup'; + +export default defineConfig({ + entry: ['src/index.ts'], + format: ['esm'], + dts: true, + outDir: 'dist', + clean: true, + sourcemap: true, + bundle: true, +}); diff --git a/hindsight-integrations/opencode/vitest.config.ts b/hindsight-integrations/opencode/vitest.config.ts new file mode 100644 index 000000000..7dd13254e --- /dev/null +++ b/hindsight-integrations/opencode/vitest.config.ts @@ -0,0 +1,9 @@ +import { defineConfig } from 'vitest/config'; + +export default defineConfig({ + test: { + globals: true, + environment: 'node', + include: ['src/**/*.test.ts'], + }, +}); diff --git a/scripts/release-integration.sh b/scripts/release-integration.sh index d5251558b..2ab814c33 100755 --- a/scripts/release-integration.sh +++ b/scripts/release-integration.sh @@ -13,7 +13,7 @@ print_info() { echo -e "${GREEN}[INFO]${NC} $1"; } print_warn() { echo -e "${YELLOW}[WARN]${NC} $1"; } print_error() { echo -e "${RED}[ERROR]${NC} $1"; } -VALID_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ag2" "ai-sdk" "chat" "openclaw" "langgraph" "llamaindex" "nemoclaw" "strands" "claude-code" "codex" "hermes" "autogen") +VALID_INTEGRATIONS=("litellm" "pydantic-ai" "crewai" "ag2" "ai-sdk" "chat" "openclaw" "langgraph" "llamaindex" "nemoclaw" "strands" "claude-code" "codex" "hermes" "autogen" "opencode") usage() { print_error "Usage: $0 "