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 @@
+
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 "