Compare commits

...
8 Commits
Author SHA1 Message Date
StanPonomarev 5b6d5fc301 Merge pull request 'fix tmp dirs' (#2) from fix/wrong_tmp_dir into main
Release / release (push) Successful in 9s
Reviewed-on: #2
2026-09-12 19:10:43 +02:00
StanPonomarev 62b56b6a8a fix tmp dirs
CI / verify (pull_request) Successful in 8s
2026-09-12 19:09:49 +02:00
StanPonomarev 99a0090a92 Merge pull request 'feat: add playwright' (#1) from feat/playwright into main
Release / release (push) Failing after 7s
Reviewed-on: #1
2026-09-12 18:59:28 +02:00
StanPonomarev 75e08a5c63 udate readme 2026-09-12 18:58:55 +02:00
StanPonomarev dfe9926c7c feat: add playwright 2026-09-12 18:56:00 +02:00
StanPonomarev a6cb0beade chore: fix release
Release / release (push) Successful in 9s
2026-09-12 10:36:45 +02:00
StanPonomarev c48fe99b96 chore: update opencode api to v2
Release / release (push) Failing after 5s
2026-09-12 10:29:24 +02:00
StanPonomarev 411cdd13ff feat: add quota tracking 2026-09-12 09:51:04 +02:00
19 changed files with 2315 additions and 58 deletions
+29
View File
@@ -0,0 +1,29 @@
name: CI
on:
pull_request:
permissions:
code: read
jobs:
verify:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: https://github.com/actions/checkout@v4
with:
github-server-url: ${{ gitea.server_url }}
persist-credentials: false
- name: Install Bun
uses: https://github.com/oven-sh/setup-bun@v2
with:
bun-version: "1.3.14"
- name: Verify and build
run: |
bun install --frozen-lockfile
bun run check
bun run lint
bun run format:check
bun run test
bun run build
+15 -28
View File
@@ -1,10 +1,10 @@
# stanp-opencode
OpenCode V2 plugin providing a `coordinate` agent and fast/good/deep worker pairs, with GPT routing or explicit custom model assignments. Your default agent and the built-in `build` agent remain unchanged.
stanp-opencode is an OpenCode V2 coordinator with specialist workers for fast, good, and deep tasks.
## Install the latest release
Requires **OpenCode V2** (`opencode2`), **Bun 1.3.14+**, Bash, curl, jq, and `sha256sum` (Linux/WSL). The native plugin API is pinned to `@opencode/plugin@0.0.0-beta-19378`; use the matching OpenCode beta.
Requires OpenCode V2 2.0.2 (`opencode2`), Bun 1.3.14+, Bash, curl, jq, and `sha256sum` on Linux or WSL.
Run the installer without sudo:
@@ -12,45 +12,32 @@ Run the installer without sudo:
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash
```
From a checkout, run `bash install.sh` directly. Rerun the same script to upgrade to the latest published, non-prerelease Gitea release.
Rerun the command to upgrade to the latest published release.
The installer downloads `stanp-opencode.tgz` and its SHA-256 checksum, verifies and extracts the self-contained archive, then activates the plugin only after importing and checking the native V2 export. Download, extraction, and validation failures leave the previous active plugin untouched. The checksum detects corruption; it is not a signature independent of the release server.
## Install from a local checkout
Installation defaults to `~/.config/opencode` and respects `XDG_CONFIG_HOME`. Override it explicitly when needed:
This requires Bash, jq, and Bun 1.3.14+:
```sh
OPENCODE_CONFIG_DIR=/path/to/opencode-config bash install.sh
bash /path/to/stanp-opencode/install-local.sh
```
The installer atomically replaces the package directory at `plugins/stanp-opencode` and adds its default GPT configuration to `opencode.json`. Existing unrelated JSON settings and `opencode.jsonc` are preserved. It does not create symlinks, install package dependencies, install OpenCode itself, or configure provider credentials.
The script installs dependencies and builds the checkout before installing it.
Restart `opencode2` after installation (or `opencode2 service restart` for an existing service). Connect your provider, then select `coordinate` in the agent picker. The default configuration uses OpenAI GPT models; agents are only enabled when the required models are available.
After either installation, restart `opencode2` or run `opencode2 service restart` for an existing service. Connect OpenAI and select `coordinate`. The required models must be available for the workers to run.
## Configure
Both installers use `~/.config/opencode`, respect `XDG_CONFIG_HOME`, and accept `OPENCODE_CONFIG_DIR` as an override.
See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc). When using the installer, point the example's plugin package entry at the installed package directory rather than the unpublished npm package name. Configured local plugin paths must be directories; pointing at `plugins/stanp-opencode.js` causes OpenCode to reject that configured entry and auto-load the file without its options:
## Configuration
```jsonc
{
"plugins": [
{
"package": "./plugins/stanp-opencode",
"options": {
"mode": "gpt",
"provider": "openai",
"lunaPriority": true,
},
},
],
}
```
See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc).
This relative path assumes the configuration file is in the selected global config directory. Use an absolute path in project configuration. Remove any old V1 `plugin` entry when migrating; V2 uses `plugins`. `lunaPriority` requests priority service where supported and can increase cost; set it to `false` to use the provider default.
For an installed plugin, use `./plugins/stanp-opencode` as the plugin package path. Set `lunaPriority` to `false` to avoid requesting priority pricing.
The plugin supplies session-local `todoread` and `todowrite` tools when no other plugin provides them. In this pinned V2 beta, custom tools cannot open permission prompts: an `ask` or `deny` rule blocks these todo tools rather than bypassing the rule. Explicitly allow them if you want to use them.
## Playwright
When an MCP server named `codegraph` is already configured, its tools are exposed directly to `coordinate` and retain the built-in `build` agent's matching allow, ask, and deny rules. The plugin does not configure or start a missing server, and no other MCP server is exposed this way.
The plugin prefers a local Playwright MCP installation. If no local installation is available, Docker is required for the automatic fallback. Existing `playwright` configuration is preserved.
## Uninstall
Remove `plugins/stanp-opencode` from the selected config directory and remove any explicit configuration entry for it, then restart OpenCode.
Remove `plugins/stanp-opencode` from the selected configuration directory and remove its configuration entry, then restart OpenCode.
+256 -13
View File
@@ -8,22 +8,31 @@
"zod": "4.1.8",
},
"devDependencies": {
"@opencode/client": "0.0.0-beta-19378",
"@opencode/plugin": "0.0.0-beta-19378",
"@opencode/client": "2.0.2",
"@opencode/plugin": "2.0.2",
"@opencode/schema": "2.0.2",
"@opentui/core": "0.5.10",
"@opentui/solid": "0.5.10",
"@oxlint/plugins": "1.82.0",
"@types/bun": "1.3.14",
"oxfmt": "0.67.0",
"oxlint": "1.82.0",
"solid-js": "1.9.15",
"typescript": "7.0.2",
},
"peerDependencies": {
"@opencode/plugin": "0.0.0-beta-19378",
"@opencode/plugin": "2.0.2",
"@opentui/core": ">=0.5.10",
"@opentui/solid": ">=0.5.10",
"solid-js": ">=1.9.0",
},
},
},
"packages": {
"@ai-sdk/provider": ["@ai-sdk/provider@3.0.8", "", { "dependencies": { "json-schema": "^0.4.0" } }, "sha512-oGMAgGoQdBXbZqNG0Ze56CHjDZ1IDYOwGYxYjO5KLSlz5HiNQ9udIXsPZ61VWaHGZ5XW/jyjmr6t2xz2jGVwbQ=="],
"@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="],
"@aws-crypto/crc32": ["@aws-crypto/crc32@5.2.0", "", { "dependencies": { "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "tslib": "^2.6.2" } }, "sha512-nLbCWqQNgUiwwtFsen1AdzAtvuLRsQS8rYgMuxCrdKf9kOssamGLuPwyTY9wyYblNr9+1XM8v6zoDTPPSIeANg=="],
"@aws-crypto/sha256-browser": ["@aws-crypto/sha256-browser@5.2.0", "", { "dependencies": { "@aws-crypto/sha256-js": "^5.2.0", "@aws-crypto/supports-web-crypto": "^5.2.0", "@aws-crypto/util": "^5.2.0", "@aws-sdk/types": "^3.222.0", "@aws-sdk/util-locate-window": "^3.0.0", "@smithy/util-utf8": "^2.0.0", "tslib": "^2.6.2" } }, "sha512-AXfN/lGotSQwu6HNcEsIASo7kWXZ5HYWvfOmSNKDsEqC4OashTp8alTmaz+F7TC2L083SFv5RdB+qU3Vs1kZqw=="],
@@ -72,6 +81,62 @@
"@aws/lambda-invoke-store": ["@aws/lambda-invoke-store@0.3.0", "", {}, "sha512-sl4Bm6yiMNYrZKkqqDFWN0UfnWhlS8ivKxrYl+6t0gCLrqr8y3B2IqZZbFRkfaVVp7C/baApyh71P+LeE1A2sQ=="],
"@babel/code-frame": ["@babel/code-frame@7.29.7", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.29.7", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw=="],
"@babel/compat-data": ["@babel/compat-data@7.29.7", "", {}, "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg=="],
"@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="],
"@babel/generator": ["@babel/generator@7.29.8", "", { "dependencies": { "@babel/parser": "^7.29.8", "@babel/types": "^7.29.8", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-gZbepsdh3WDtgZKWL+vTPh71LSBrm/Y4/QDZBVCcYfmeTEEuoOYwlSy+G1StfJg+/Zy550u/3TATbm7qDbbMtg=="],
"@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw=="],
"@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.29.7", "", { "dependencies": { "@babel/compat-data": "^7.29.7", "@babel/helper-validator-option": "^7.29.7", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g=="],
"@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/helper-replace-supers": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/traverse": "^7.29.7", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg=="],
"@babel/helper-globals": ["@babel/helper-globals@7.29.7", "", {}, "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA=="],
"@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg=="],
"@babel/helper-module-imports": ["@babel/helper-module-imports@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g=="],
"@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.29.7", "", { "dependencies": { "@babel/helper-module-imports": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg=="],
"@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.29.7", "", { "dependencies": { "@babel/types": "^7.29.7" } }, "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong=="],
"@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.29.7", "", {}, "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw=="],
"@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.29.7", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.29.7", "@babel/helper-optimise-call-expression": "^7.29.7", "@babel/traverse": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ=="],
"@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.29.7", "", { "dependencies": { "@babel/traverse": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ=="],
"@babel/helper-string-parser": ["@babel/helper-string-parser@7.29.7", "", {}, "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw=="],
"@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.29.7", "", {}, "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg=="],
"@babel/helper-validator-option": ["@babel/helper-validator-option@7.29.7", "", {}, "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw=="],
"@babel/helpers": ["@babel/helpers@7.29.7", "", { "dependencies": { "@babel/template": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg=="],
"@babel/parser": ["@babel/parser@7.29.8", "", { "dependencies": { "@babel/types": "^7.29.8" }, "bin": "./bin/babel-parser.js" }, "sha512-E8lTAYNB1KW+FH+VGJuZM1ioAx2E6oVlvQFRrf5P8ZZmsiJXYAD9vTFV7yyEURNzgh1dFqMZuO6tUwcARbqFCA=="],
"@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A=="],
"@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.29.7", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA=="],
"@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.29.7", "", { "dependencies": { "@babel/helper-module-transforms": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ=="],
"@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.29.7", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.29.7", "@babel/helper-create-class-features-plugin": "^7.29.7", "@babel/helper-plugin-utils": "^7.29.7", "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", "@babel/plugin-syntax-typescript": "^7.29.7" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw=="],
"@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="],
"@babel/template": ["@babel/template@7.29.7", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/parser": "^7.29.7", "@babel/types": "^7.29.7" } }, "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg=="],
"@babel/traverse": ["@babel/traverse@7.29.8", "", { "dependencies": { "@babel/code-frame": "^7.29.7", "@babel/generator": "^7.29.8", "@babel/helper-globals": "^7.29.7", "@babel/parser": "^7.29.8", "@babel/template": "^7.29.7", "@babel/types": "^7.29.8", "debug": "^4.3.1" } }, "sha512-I5z7H3bf/41ktsNVLtpN0wAa336HkqIHQ5BuPLEhTkt1jVSyZpeNKIzTgEWmlxjdg81R0IgUCcaE+Ok3NvrfZg=="],
"@babel/types": ["@babel/types@7.29.8", "", { "dependencies": { "@babel/helper-string-parser": "^7.29.7", "@babel/helper-validator-identifier": "^7.29.7" } }, "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg=="],
"@effect/opentelemetry": ["@effect/opentelemetry@4.0.0-rc.112", "", { "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <2.0.0", "@opentelemetry/api-logs": ">=0.203.0 <0.300.0", "@opentelemetry/resources": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-logs": ">=0.203.0 <0.300.0", "@opentelemetry/sdk-metrics": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-base": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-node": ">=2.0.0 <3.0.0", "@opentelemetry/sdk-trace-web": ">=2.0.0 <3.0.0", "@opentelemetry/semantic-conventions": ">=1.33.0 <2.0.0", "effect": "^4.0.0-rc.112" }, "optionalPeers": ["@opentelemetry/api", "@opentelemetry/api-logs", "@opentelemetry/resources", "@opentelemetry/sdk-logs", "@opentelemetry/sdk-metrics", "@opentelemetry/sdk-trace-base", "@opentelemetry/sdk-trace-node", "@opentelemetry/sdk-trace-web"] }, "sha512-OTRv1DxTHUmnakgJ6XVM8wVgF1KgZH4UXnOemwSLUwUjXO+RCikzF8oR/rlVmOGq81KtQzj1URM4M4nchlQOuQ=="],
"@effect/platform-node": ["@effect/platform-node@4.0.0-rc.112", "", { "dependencies": { "@effect/platform-node-shared": "^4.0.0-rc.112", "mime": "^4.1.0", "undici": "^8.10.0" }, "peerDependencies": { "effect": "^4.0.0-rc.112", "redis": ">=5.0.0 <7.0.0" } }, "sha512-/BMAcdNGQQskLmI0Zoa95KfTZkr9HV9N4NSxaSrusG6GeW6Ulp9KvZ+Rlaiw8lnOt43CXjFLdfll5/k5rxL4hQ=="],
@@ -86,6 +151,14 @@
"@isaacs/string-locale-compare": ["@isaacs/string-locale-compare@1.1.0", "", {}, "sha512-SQ7Kzhh9+D+ZW9MA0zkYv3VXhIDNx+LzM6EJ+/65I3QY+enU6Itte7E5XX7EWrqLW2FN4n06GWzBnPoC3th2aQ=="],
"@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="],
"@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="],
"@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.6.0", "", {}, "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw=="],
"@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="],
"@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4", "", { "os": "darwin", "cpu": "arm64" }, "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ=="],
"@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4", "", { "os": "darwin", "cpu": "x64" }, "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w=="],
@@ -128,17 +201,17 @@
"@npmcli/run-script": ["@npmcli/run-script@10.0.4", "", { "dependencies": { "@npmcli/node-gyp": "^5.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "node-gyp": "^12.1.0", "proc-log": "^6.0.0" } }, "sha512-mGUWr1uMnf0le2TwfOZY4SFxZGXGfm4Jtay/nwAa2FLNAKXUoUwaGwBMNH36UHPtinWfTSJ3nqFQr0091CxVGg=="],
"@opencode/ai": ["@opencode/ai@0.0.0-beta-19378", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "0.0.0-beta-19378", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.0-rc.112", "google-auth-library": "10.5.0" } }, "sha512-mk20W9+HH82jT8VLowhu9weV6Qkch+ydbfPRp2FJFh53RP9up1YaHLDu1o913biSJQNa36Q/9shln7YIOJ2PNA=="],
"@opencode/ai": ["@opencode/ai@2.0.2", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.0.2", "@smithy/eventstream-codec": "4.2.14", "@smithy/util-utf8": "4.2.2", "aws4fetch": "1.0.20", "effect": "4.0.0-rc.112", "google-auth-library": "10.5.0" } }, "sha512-Byq9c3kRdIl+AfGidPkd3JUC6DnjaNXGem77sAFRSxYwA0AMJMImPm+ua6ibnRqJ+6HlEq9JSh+Z1nQ8s8xzrQ=="],
"@opencode/client": ["@opencode/client@0.0.0-beta-19378", "", { "dependencies": { "@opencode/protocol": "0.0.0-beta-19378", "@opencode/schema": "0.0.0-beta-19378" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-tYhOVbCt4RFIxw3DYvX9NREomExR4QCE/K/mFoUxkU8dlhQNCDDT8ztztDkLyLycYwwvAJ+wKt4La6vH0/s8Pg=="],
"@opencode/client": ["@opencode/client@2.0.2", "", { "dependencies": { "@opencode/protocol": "2.0.2", "@opencode/schema": "2.0.2" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-w8rrSsxY9qnuFrsQAHdLDE6mrEqUKQeo7fNsw2i/tFtimgspT53/8/3ckEVRuRUM5k6v68KiU2deG0W2gEIVDQ=="],
"@opencode/plugin": ["@opencode/plugin@0.0.0-beta-19378", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "0.0.0-beta-19378", "@opencode/client": "0.0.0-beta-19378", "@opencode/protocol": "0.0.0-beta-19378", "@opencode/schema": "0.0.0-beta-19378", "@opencode/util": "0.0.0-beta-19378", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode/theme": "0.0.0-beta-19378", "@opentui/core": ">=0.5.10", "@opentui/solid": ">=0.5.10", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-Kzlp3gpem5QSzmEkrUOZPABnFy6a+m5wRGskXqt1KrJbXMIjrW9xnluFn81iHpiHEWRkQZOVZssw6VKPgyXQtA=="],
"@opencode/plugin": ["@opencode/plugin@2.0.2", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.0.2", "@opencode/client": "2.0.2", "@opencode/protocol": "2.0.2", "@opencode/schema": "2.0.2", "@opencode/util": "2.0.2", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode/theme": "2.0.2", "@opentui/core": ">=0.5.10", "@opentui/solid": ">=0.5.10", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-lFXcsyg1fhzCpzuMqBhb2yNddMeynXZwXL3aHs1T9ftbFu4buFAdtE9ox09Q+CL/r7yGbbX4mdtjKYdnt0dn5Q=="],
"@opencode/protocol": ["@opencode/protocol@0.0.0-beta-19378", "", { "dependencies": { "@opencode/schema": "0.0.0-beta-19378", "effect": "4.0.0-rc.112" } }, "sha512-BXh+ygV/xah3LYD12i0eVH69JW6m+7vVK7MiSUnKm+T88G8DT0ytJmqFcLYtoa1EcDNnujcmHMrLtHheTn4b4g=="],
"@opencode/protocol": ["@opencode/protocol@2.0.2", "", { "dependencies": { "@opencode/schema": "2.0.2", "effect": "4.0.0-rc.112" } }, "sha512-FxTxdHPNz46NkG8ZBB2S60QzXbZDlVEyZ6KeYTt2G7hcWB5JlMYBh8jeqHlsLbquKinvMKA2iuEsA1NaccN+YQ=="],
"@opencode/schema": ["@opencode/schema@0.0.0-beta-19378", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-aBilB58tyzWEE44gNmZXZpu5Xw2W3mNV3vrdKCX19z2VRoaZGY8BRhjDwFNGGBoZ9U5KXp9e0C0GKydUuKxT/w=="],
"@opencode/schema": ["@opencode/schema@2.0.2", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-jF23H41j6zM2Az1pYLD6NFfzpoQ79CX0AxRkfNxTZFbPfjGGc+EPSOzVQP/r4Y/5bkSSqVga0Vn7MyoP8QhwDQ=="],
"@opencode/util": ["@opencode/util@0.0.0-beta-19378", "", { "dependencies": { "@effect/opentelemetry": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", "@effect/platform-node-shared": "4.0.0-rc.112", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "cross-spawn": "7.0.6", "effect": "4.0.0-rc.112", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "pacote": "21.5.1" } }, "sha512-YNAWKFt348p9UZWQJ5wyazTq8TOPrStaLnd7bA8m5E17CaULtKnxeiKBIcG095cpeqIZ+/JCQBurcqOZ7UbDrA=="],
"@opencode/util": ["@opencode/util@2.0.2", "", { "dependencies": { "@effect/opentelemetry": "4.0.0-rc.112", "@effect/platform-node": "4.0.0-rc.112", "@effect/platform-node-shared": "4.0.0-rc.112", "@npmcli/arborist": "9.4.0", "@npmcli/config": "10.8.1", "@opentelemetry/api": "1.9.0", "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/exporter-trace-otlp-http": "0.214.0", "@opentelemetry/sdk-trace-base": "2.6.1", "@opentelemetry/sdk-trace-node": "2.6.1", "cross-spawn": "7.0.6", "effect": "4.0.0-rc.112", "glob": "13.0.5", "mime-types": "3.0.2", "minimatch": "10.2.5", "npm-package-arg": "13.0.2", "pacote": "21.5.1" } }, "sha512-AKnFr44s1Q7/Ud+Pl4QMCEJoIp33xyDiTi693BofXn+HBDHdJmKrwq87YpG+Kwt9t48gVv6Cx1glmqJ4lVJpHg=="],
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
@@ -166,6 +239,26 @@
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
"@opentui/core": ["@opentui/core@0.5.10", "", { "dependencies": { "bun-ffi-structs": "0.3.1", "diff": "9.0.0", "marked": "17.0.1", "string-width": "7.2.0", "strip-ansi": "7.1.2" }, "optionalDependencies": { "@opentui/core-darwin-arm64": "0.5.10", "@opentui/core-darwin-x64": "0.5.10", "@opentui/core-linux-arm64": "0.5.10", "@opentui/core-linux-arm64-musl": "0.5.10", "@opentui/core-linux-x64": "0.5.10", "@opentui/core-linux-x64-musl": "0.5.10", "@opentui/core-win32-arm64": "0.5.10", "@opentui/core-win32-x64": "0.5.10" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-C3a2UbmefeAjIxAgm4BqjuSxKT4oqutfvYFwVvUgMxmGRHkNbBc/s7sukV0JgwcxFcV3uMFrXxo+E+BQtvuOiw=="],
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.10", "", { "os": "darwin", "cpu": "arm64" }, "sha512-Vyb+nTbhab8ZcRy5gg1loEEGwRcIbjAeVRIBfHBcbFDqmITBOg7x2gqJ+x/TnoOy4uwMhCmICUN2wiyREw3r1Q=="],
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.10", "", { "os": "darwin", "cpu": "x64" }, "sha512-tTFLcM7Oj1gTyhm/bUdAt3C6grZdCxPk6+/g2azcZBUlI3/62LwbeRS6HbQKFFmm+1fUmX8cq6kWrtul885mVg=="],
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-ncJXcgudhBf2GdJyF3xVQN/Ec+1F7GOL+pRrURmgBYSj2v1w6EyoDQFAACtPTK2c3R38W6fvZwL4JSLlm4EFXQ=="],
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.10", "", { "os": "linux", "cpu": "arm64" }, "sha512-dGMphDKexSdeYqwl0wgoFBP88Ta/cdi1Zc1mk29/ENkSCGz+74zlCHgqTHRNGLmI8W5TfuUtCyktQH11/Z+TBQ=="],
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-5qtYaOgwVycZD1GaGshTRsi0rXPAmVExO03N1JQaHu+NYxK/vXSOc7Bu4QW0sPXx3Sp0SpzpP+FHjXABfoK66g=="],
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.10", "", { "os": "linux", "cpu": "x64" }, "sha512-Oj4H9hApuvuTKPWxh4SoZAgGJorR7vbvnrZA/cAkSMAk2VGSoHRRcqeXQbcH8IcdjVZ0KFpv8Zkl/D5Ye+2mew=="],
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.10", "", { "os": "win32", "cpu": "arm64" }, "sha512-A9VhgvTxQoUdZ+8LmUumEng1sQNbj9QQQT3NYG9mSxI54qTANi7vOWNSphMiY6RMVsr22pgm6nUvSSvJXv7Jog=="],
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.10", "", { "os": "win32", "cpu": "x64" }, "sha512-u3KHa7kEeWrmKVDRJYpxSGO+g5E9cMGlrmTsPN3GVPHUmQMiREUawLXUvsU8+IHaQnqG3Q5nuE1yf4fPBzS+Qw=="],
"@opentui/solid": ["@opentui/solid@0.5.10", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.10", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.12", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.12" } }, "sha512-KrmMIsHiKBHOABTC0brOwqWm+sGq1ZX2sGCAx6WgtBbE3STMup9n8TAy/6gUYhwcjC9zugT53ytfSVwCwVWZUg=="],
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.67.0", "", { "os": "android", "cpu": "arm" }, "sha512-2olh3ioEmc4gRzQm7jxyB1b/PFBoFvTq8KdgYySeNpysDtA6DEg2Mvya4/I6flhL7G0eOrE8RD7JCNCIMhE16Q=="],
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.67.0", "", { "os": "android", "cpu": "arm64" }, "sha512-ulfw8EHN1MBq/MFFDXw2/M1VAFu5mRUcnuZ8Hqbv9viAnFzO9t1jKSAsDqKYYDGMlytF/uj6Z5z5n/tHupnKhw=="],
@@ -370,10 +463,18 @@
"aws4fetch": ["aws4fetch@1.0.20", "", {}, "sha512-/djoAN709iY65ETD6LKCtyyEI04XIBP5xVvfmNxsEP0uJB5tyaGBztSryRr4HqMStr9R06PisQE7m9zDTXKu6g=="],
"babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.10", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-lxve6Y02YiZTldB7efKpnbf1BH00XCFZNYYW235jSGsYaJNFtHrYlKV6/O+miHbjqpIr9FTe5+0no4hofAMbfA=="],
"babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="],
"babel-preset-solid": ["babel-preset-solid@1.9.12", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.6" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.12" }, "optionalPeers": ["solid-js"] }, "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg=="],
"balanced-match": ["balanced-match@4.0.4", "", {}, "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA=="],
"base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="],
"baseline-browser-mapping": ["baseline-browser-mapping@2.11.22", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-pWc4w51fBFd7mav43/zKRC+RI6f4yfzQoVlfvE8dECePyfkn1bzLp01Fj0QACcyCZyFhiEMyD2qScfKRWgWibA=="],
"bignumber.js": ["bignumber.js@9.3.1", "", {}, "sha512-Ko0uX15oIUS7wJ3Rb30Fs6SkVbLmPBAKdlm7q9+ak9bbIeFf0MwuBsQV6z7+X768/cHsfg+WlysDWJcmthjsjQ=="],
"bin-links": ["bin-links@6.0.2", "", { "dependencies": { "cmd-shim": "^8.0.0", "npm-normalize-package-bin": "^5.0.0", "proc-log": "^6.0.0", "read-cmd-shim": "^6.0.0", "write-file-atomic": "^7.0.0" } }, "sha512-frE1t78WOwJ45PKV2cF2tNPjTcs9L1J9s6VkrV59wanRP4GlaomuxYPVma7BwthMg8WnfSory4w5PTE6FZZ81w=="],
@@ -382,12 +483,18 @@
"brace-expansion": ["brace-expansion@5.0.9", "", { "dependencies": { "balanced-match": "^4.0.2" } }, "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg=="],
"browserslist": ["browserslist@4.28.9", "", { "dependencies": { "baseline-browser-mapping": "^2.11.20", "caniuse-lite": "^1.0.30001810", "electron-to-chromium": "^1.5.420", "node-releases": "^2.0.54", "update-browserslist-db": "^1.3.2" }, "bin": { "browserslist": "cli.js" } }, "sha512-EWazOblFYUvlGZcfGhPUPmYh3nikUxBVb+y9MJun5f3hBi812X+8MSQTujLBtgK3cf51fJWbWfOjyeO954d+Eg=="],
"buffer-equal-constant-time": ["buffer-equal-constant-time@1.0.1", "", {}, "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA=="],
"bun-ffi-structs": ["bun-ffi-structs@0.3.1", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-3gM7PpVWLyrwxWjcilSiGuhWanhZivvo6l0u573NziPH6f/gwk6McbaYgn7oJWov6pKGRTDbrg94W5DcJsKTtQ=="],
"bun-types": ["bun-types@1.3.14", "", { "dependencies": { "@types/node": "*" } }, "sha512-4N0ig0fEomHt5R0KCFWjovxow98rIoRwKolrYdCcknNwMekCXRnWEUvgu5soYV8QXtVsrUD8B95MBOZGPvr6KQ=="],
"cacache": ["cacache@20.0.4", "", { "dependencies": { "@npmcli/fs": "^5.0.0", "fs-minipass": "^3.0.0", "glob": "^13.0.0", "lru-cache": "^11.1.0", "minipass": "^7.0.3", "minipass-collect": "^2.0.1", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "p-map": "^7.0.2", "ssri": "^13.0.0" } }, "sha512-M3Lab8NPYlZU2exsL3bMVvMrMqgwCnMWfdZbK28bn3pK6APT/Te/I8hjRPNu1uwORY9a1eEQoifXbKPQMfMTOA=="],
"caniuse-lite": ["caniuse-lite@1.0.30001810", "", {}, "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg=="],
"chownr": ["chownr@3.0.0", "", {}, "sha512-+IxzY9BZOQd/XuYPRmrvEVjF/nqj5kgT4kEq7VofrDoM1MxoRjEWkrCC3EtLi59TVawxTAn+orJwFQcrqEN1+g=="],
"ci-info": ["ci-info@4.4.0", "", {}, "sha512-77PSwercCZU2Fc4sX94eF8k8Pxte6JAwL4/ICZLFjJLqegs7kCuAsqqj/70NQF6TvDpgFjkubQB2FW2ZZddvQg=="],
@@ -404,26 +511,40 @@
"content-type": ["content-type@2.1.0", "", {}, "sha512-mj7UPXE0jaqaOsukNZRUEfEi2AcL7C/vwmwcHV0O97eO1E1pxBZuyjlZrx5seTaNBg1U6+o35wpa35Qfcc+7ag=="],
"convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="],
"cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="],
"cssesc": ["cssesc@3.0.0", "", { "bin": { "cssesc": "bin/cssesc" } }, "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg=="],
"csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="],
"data-uri-to-buffer": ["data-uri-to-buffer@4.0.1", "", {}, "sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A=="],
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
"diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
"eastasianwidth": ["eastasianwidth@0.2.0", "", {}, "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA=="],
"ecdsa-sig-formatter": ["ecdsa-sig-formatter@1.0.11", "", { "dependencies": { "safe-buffer": "^5.0.1" } }, "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ=="],
"effect": ["effect@4.0.0-rc.112", "", { "dependencies": { "fast-check": "^4.9.0", "msgpackr": "^2.0.5" } }, "sha512-wXxwuh1Ywnv4cPRM3Wfa0vDwuOHnZ1TsTgHJkG9XgzND6inhBH9n1vBxhg3iIXOia/OrpmvVmd3lrD4vq6bF3A=="],
"emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"electron-to-chromium": ["electron-to-chromium@1.5.427", "", {}, "sha512-n14zb3FdsChZ2BNobqNHAJMcP3ifFv4paox2LvCrfVAQcqGiSURgbJl+PfMpHVCNFkStnNc+RRVtPBTVW5PDgw=="],
"emoji-regex": ["emoji-regex@10.6.0", "", {}, "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A=="],
"entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="],
"env-paths": ["env-paths@2.2.1", "", {}, "sha512-+h1lkLKhZMTYjog1VEpJNG7NZJWcuc2DDk/qsqSTRRCOXiLjeQ1d1/udrUGhqMxUgAlwKNZ0cf2uqan5GLuS2A=="],
"es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="],
"escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="],
"exponential-backoff": ["exponential-backoff@3.1.3", "", {}, "sha512-ZgEeZXj30q+I0EN+CbSSpIyPaJ5HVQD18Z1m+u1FXbAeT94mr1zw50q4q6jiiC447Nl/YTcIYSAftiGqetwXCA=="],
"extend": ["extend@3.0.2", "", {}, "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="],
@@ -434,16 +555,28 @@
"fetch-blob": ["fetch-blob@3.2.0", "", { "dependencies": { "node-domexception": "^1.0.0", "web-streams-polyfill": "^3.0.3" } }, "sha512-7yAQpD2UMJzLi1Dqv7qFYnPbaPx7ZfFK6PiIxQ4PfkGPyNyl2Ugx+a/umUonmKqjhM4DnfbMvdX6otXq83soQQ=="],
"find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="],
"find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="],
"foreground-child": ["foreground-child@3.3.1", "", { "dependencies": { "cross-spawn": "^7.0.6", "signal-exit": "^4.0.1" } }, "sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw=="],
"formdata-polyfill": ["formdata-polyfill@4.0.10", "", { "dependencies": { "fetch-blob": "^3.1.2" } }, "sha512-buewHzMvYL29jdeQTVILecSaZKnt/RJWjoZCF5OW60Z67/GmSLBkOFM7qh1PI3zFNtJbaZL5eQu1vLfazOwj4g=="],
"fs-minipass": ["fs-minipass@3.0.3", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-XUBA9XClHbnJWSfBzjkm6RvPsyg3sryZt06BEQoXcF7EK/xpGaQYJgQKDJSUH5SGZ76Y7pFx1QBnXz09rU5Fbw=="],
"fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="],
"function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="],
"gaxios": ["gaxios@7.3.1", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2" } }, "sha512-kB3rzJV7d9juLZh8/56QTXCwQfxyhdOMdyYk1HdQKFtF8TJTDTZQJtixWIwXdE9Jji91mC41DUNpjleo4L4eAQ=="],
"gcp-metadata": ["gcp-metadata@8.1.4", "", { "dependencies": { "gaxios": "7.1.3", "google-logging-utils": "1.1.3", "json-bigint": "^1.0.0" } }, "sha512-iJ9KMsiu+xKtNRX0PmGLSaIU3bUBAyzWTyqKemKPzNPsmmsBCQYmlNg+brEbES7IHSXtdVwzBPzx1vz3FAaipw=="],
"gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="],
"get-east-asian-width": ["get-east-asian-width@1.6.0", "", {}, "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA=="],
"glob": ["glob@13.0.5", "", { "dependencies": { "minimatch": "^10.2.1", "minipass": "^7.1.2", "path-scurry": "^2.0.0" } }, "sha512-BzXxZg24Ibra1pbQ/zE7Kys4Ua1ks7Bn6pKLkVPZ9FZe4JQS6/Q7ef3LG1H+k7lUf5l4T3PLSyYyYJVYUvfgTw=="],
"google-auth-library": ["google-auth-library@10.5.0", "", { "dependencies": { "base64-js": "^1.3.0", "ecdsa-sig-formatter": "^1.0.11", "gaxios": "^7.0.0", "gcp-metadata": "^8.0.0", "google-logging-utils": "^1.0.0", "gtoken": "^8.0.0", "jws": "^4.0.0" } }, "sha512-7ABviyMOlX5hIVD60YOfHw4/CxOfBhyduaYB+wbFWCWoni4N7SLcV46hrVRktuBbZjFC9ONyqamZITN7q3n32w=="],
@@ -454,8 +587,12 @@
"gtoken": ["gtoken@8.0.0", "", { "dependencies": { "gaxios": "^7.0.0", "jws": "^4.0.0" } }, "sha512-+CqsMbHPiSTdtSO14O51eMNlrp9N79gmeqmXeouJOhfucAedHw9noVe/n5uJk3tbKE6a+6ZCQg3RPhVhHByAIw=="],
"hasown": ["hasown@2.0.4", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A=="],
"hosted-git-info": ["hosted-git-info@9.0.3", "", { "dependencies": { "lru-cache": "^11.1.0" } }, "sha512-Hc+ghLoSt6QaYZUv0WBiIvmMDZuZZ7oaDvdH8MbfOO4lOsxdXLEvuC6ePoGs9H1X9oCLyq6+NVN0MKqD+ydxyg=="],
"html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="],
"http-cache-semantics": ["http-cache-semantics@4.2.0", "", {}, "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ=="],
"http-proxy-agent": ["http-proxy-agent@7.0.2", "", { "dependencies": { "agent-base": "^7.1.0", "debug": "^4.3.4" } }, "sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig=="],
@@ -470,12 +607,18 @@
"ip-address": ["ip-address@10.7.0", "", {}, "sha512-BGFsyJd5mpXp3rK6jIdADLNgpJUK1jnjzvYF8lK+VyDab9JAmqN0YOKDdP17HlgKb2+ehPgDc8EtnRLbGCAMhA=="],
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
"isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="],
"jackspeak": ["jackspeak@3.4.3", "", { "dependencies": { "@isaacs/cliui": "^8.0.2" }, "optionalDependencies": { "@pkgjs/parseargs": "^0.11.0" } }, "sha512-OGlZQpz2yfahA/Rd1Y8Cd9SIEsqvXkLVoSw/cgwhnhFMDbsQFeZYoJJ7bIZBS9BcamUW96asq/npPWugM+RQBw=="],
"js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="],
"jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="],
"json-bigint": ["json-bigint@1.0.0", "", { "dependencies": { "bignumber.js": "^9.0.0" } }, "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ=="],
"json-parse-even-better-errors": ["json-parse-even-better-errors@5.0.0", "", {}, "sha512-ZF1nxZ28VhQouRWhUcVlUIN3qwSgPuswK05s/HIaoetAoE/9tngVmCHjSxmSQPav1nd+lPtTL0YZ/2AFdR/iYQ=="],
@@ -484,6 +627,8 @@
"json-stringify-nice": ["json-stringify-nice@1.1.4", "", {}, "sha512-5Z5RFW63yxReJ7vANgW6eZFGWaQvnPE3WNmZoOJrSkGju2etKA2L5rrOa1sm877TVTFt57A80BH1bArcmlLfPw=="],
"json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="],
"jsonparse": ["jsonparse@1.3.1", "", {}, "sha512-POQXvpdL69+CluYsillJ7SUhKvytYjW9vG/GKpnf+xP8UWgYEM/RaMzHHofbALDiKbbP1W8UEYmgGl39WkPZsg=="],
"just-diff": ["just-diff@6.0.2", "", {}, "sha512-S59eriX5u3/QhMNq3v/gm8Kd0w8OS6Tz2FS1NG4blv+z0MuQcBRJyFWjdovM0Rad4/P4aUPFtnkNjMjyMlMSYA=="],
@@ -494,12 +639,16 @@
"jws": ["jws@4.0.1", "", { "dependencies": { "jwa": "^2.0.1", "safe-buffer": "^5.0.1" } }, "sha512-EKI/M/yqPncGUUh44xz0PxSidXFr/+r0pA70+gIYhjv+et7yxM+s29Y+VGDkovRofQem0fs7Uvf4+YmAdyRduA=="],
"locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="],
"long": ["long@5.3.2", "", {}, "sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA=="],
"lru-cache": ["lru-cache@11.5.2", "", {}, "sha512-4pfM1Ff0x50o0tQwb5ucw/RzNyD0/YJME6IVcStalZuMWxdt3sR3huStTtxz4PUmvZfRguvDejasvQ2kifR11g=="],
"make-fetch-happen": ["make-fetch-happen@15.0.6", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/agent": "^4.0.0", "@npmcli/redact": "^4.0.0", "cacache": "^20.0.1", "http-cache-semantics": "^4.1.1", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minipass-flush": "^1.0.5", "minipass-pipeline": "^1.2.4", "negotiator": "^1.0.0", "proc-log": "^6.0.0", "ssri": "^13.0.0" } }, "sha512-Je0fLJ0F5atA7F+eIlLzk+Wkcl57JDf4kf+EW8xiP5E31xOQxkIxTbgf1Oi1Lw9tRI9UEMRdI5Vz2xTzoNU1Jw=="],
"marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="],
"mime": ["mime@4.1.0", "", { "bin": { "mime": "bin/cli.js" } }, "sha512-X5ju04+cAzsojXKes0B/S4tcYtFAJ6tTMuSPBEn9CPGlrWr8Fiw7qYeLT0XyH80HSoAoqWCaz+MWKh22P7G1cw=="],
"mime-db": ["mime-db@1.54.0", "", {}, "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ=="],
@@ -538,6 +687,8 @@
"node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="],
"node-releases": ["node-releases@2.0.55", "", {}, "sha512-mIrE/Cw9y+9Au6dS5vDKDhQza9YvG6w+ZrS6X+ZzA7yFW/soAeaups4Qzn1bL6g5FVy8WtP79+0j82oPIbqRjQ=="],
"nopt": ["nopt@9.0.0", "", { "dependencies": { "abbrev": "^4.0.0" }, "bin": { "nopt": "bin/nopt.js" } }, "sha512-Zhq3a+yFKrYwSBluL4H9XP3m3y5uvQkB/09CwDruCiRmR/UJYnn9W4R48ry0uGC70aeTPKLynBtscP9efFFcPw=="],
"npm-bundled": ["npm-bundled@5.0.0", "", { "dependencies": { "npm-normalize-package-bin": "^5.0.0" } }, "sha512-JLSpbzh6UUXIEoqPsYBvVNVmyrjVZ1fzEFbqxKkTJQkWBO3xFzFT+KDnSKQWwOQNbuWRwt5LSD6HOTLGIWzfrw=="],
@@ -558,20 +709,36 @@
"oxlint": ["oxlint@1.82.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.82.0", "@oxlint/binding-android-arm64": "1.82.0", "@oxlint/binding-darwin-arm64": "1.82.0", "@oxlint/binding-darwin-x64": "1.82.0", "@oxlint/binding-freebsd-x64": "1.82.0", "@oxlint/binding-linux-arm-gnueabihf": "1.82.0", "@oxlint/binding-linux-arm-musleabihf": "1.82.0", "@oxlint/binding-linux-arm64-gnu": "1.82.0", "@oxlint/binding-linux-arm64-musl": "1.82.0", "@oxlint/binding-linux-ppc64-gnu": "1.82.0", "@oxlint/binding-linux-riscv64-gnu": "1.82.0", "@oxlint/binding-linux-riscv64-musl": "1.82.0", "@oxlint/binding-linux-s390x-gnu": "1.82.0", "@oxlint/binding-linux-x64-gnu": "1.82.0", "@oxlint/binding-linux-x64-musl": "1.82.0", "@oxlint/binding-openharmony-arm64": "1.82.0", "@oxlint/binding-win32-arm64-msvc": "1.82.0", "@oxlint/binding-win32-ia32-msvc": "1.82.0", "@oxlint/binding-win32-x64-msvc": "1.82.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-+iFM1BGw1ntYJt3QngbJmjbrGxPaKMUADOXOijpWGnYcBPq8YZnQftSS1C+pVcDYy9YxqDVJKQqQkTazTQMboQ=="],
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
"p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="],
"p-map": ["p-map@7.0.7", "", {}, "sha512-VaWRu2i4FJNRtiRWCuuQRgfQ1B7a6+gMSrO+3j0EQi/k0ULfS9kosRxGoiqwzIjZTDI02tGfk5mXXltLg6QtfQ=="],
"p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="],
"package-json-from-dist": ["package-json-from-dist@1.0.1", "", {}, "sha512-UEZIS3/by4OC8vL3P2dTXRETpebLI2NiI5vIrjaD/5UtrkFX/tNbwjTSRAGC/+7CAo2pIcBaRgWmcBBHcsaCIw=="],
"pacote": ["pacote@21.5.1", "", { "dependencies": { "@gar/promise-retry": "^1.0.0", "@npmcli/git": "^7.0.0", "@npmcli/installed-package-contents": "^4.0.0", "@npmcli/package-json": "^7.0.0", "@npmcli/promise-spawn": "^9.0.0", "@npmcli/run-script": "^10.0.0", "cacache": "^20.0.0", "fs-minipass": "^3.0.0", "minipass": "^7.0.2", "npm-package-arg": "^13.0.0", "npm-packlist": "^10.0.1", "npm-pick-manifest": "^11.0.1", "npm-registry-fetch": "^19.0.0", "proc-log": "^6.0.0", "sigstore": "^4.0.0", "ssri": "^13.0.0", "tar": "^7.4.3" }, "bin": { "pacote": "bin/index.js" } }, "sha512-KvcJ9iy3crysCsgqc4+PknH/w6jkrp8JN36mpZBPwNaDRwTfMZD37YzRazNstiZUOhuF5pno9f78n9mEJBavwg=="],
"parse-conflict-json": ["parse-conflict-json@5.0.1", "", { "dependencies": { "json-parse-even-better-errors": "^5.0.0", "just-diff": "^6.0.0", "just-diff-apply": "^5.2.0" } }, "sha512-ZHEmNKMq1wyJXNwLxyHnluPfRAFSIliBvbK/UiOceROt4Xh9Pz0fq49NytIaeaCUf5VR86hwQ/34FCcNU5/LKQ=="],
"parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="],
"path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="],
"path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="],
"path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="],
"path-scurry": ["path-scurry@2.0.2", "", { "dependencies": { "lru-cache": "^11.0.0", "minipass": "^7.1.2" } }, "sha512-3O/iVVsJAPsOnpwWIeD+d6z/7PmqApyQePUtCndjatj/9I5LylHvt5qluFaBT3I5h3r1ejfR056c+FCv+NnNXg=="],
"picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="],
"picomatch": ["picomatch@4.0.7", "", {}, "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA=="],
"pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="],
"postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="],
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
@@ -590,13 +757,23 @@
"redis": ["redis@6.2.1", "", { "dependencies": { "@redis/bloom": "6.2.1", "@redis/client": "6.2.1", "@redis/json": "6.2.1", "@redis/search": "6.2.1", "@redis/time-series": "6.2.1" } }, "sha512-Z9VHtgYs48PiQC77X9O2Er8Hj4T+5BtFjT91/vi5Is1D04N72cA946ZslM1ImJw8ZctFBZWAVjM7S5wJNeHMpg=="],
"reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="],
"resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="],
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
"s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="],
"safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="],
"safer-buffer": ["safer-buffer@2.1.2", "", {}, "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="],
"semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="],
"seroval": ["seroval@1.5.6", "", {}, "sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA=="],
"seroval-plugins": ["seroval-plugins@1.5.6", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ=="],
"shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="],
@@ -612,6 +789,8 @@
"socks-proxy-agent": ["socks-proxy-agent@8.0.5", "", { "dependencies": { "agent-base": "^7.1.2", "debug": "^4.3.4", "socks": "^2.8.3" } }, "sha512-HehCEsotFqbPW9sJ8WVYB6UbmIMv7kUUORIF2Nncq4VQvBfNBLibW9YZR5dlYCSUhwcD628pRllm7n+E+YTzJw=="],
"solid-js": ["solid-js@1.9.15", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.4", "seroval-plugins": "~1.5.4" } }, "sha512-EeiY2xfpZJqPLjXspVEKjAII4yv8NyG//NxZ3IpOFHdUNnnTyL0uJOeS9LWGvA7cFCz5y94cjFwYlmw5Luncsg=="],
"spdx-exceptions": ["spdx-exceptions@2.5.0", "", {}, "sha512-PiU42r+xO4UbUS1buo3LPJkjlO7430Xn5SVAhdpzzsPHsjbYVflnnFdATgabnLude+Cqu25p6N+g2lw/PFsa4w=="],
"spdx-expression-parse": ["spdx-expression-parse@4.0.0", "", { "dependencies": { "spdx-exceptions": "^2.1.0", "spdx-license-ids": "^3.0.0" } }, "sha512-Clya5JIij/7C6bRR22+tnGXbc4VKlibKSVj2iHvVeX5iMW7s1SIQlqu699JkODJJIhh/pUu8L0/VLh8xflD+LQ=="],
@@ -620,14 +799,16 @@
"ssri": ["ssri@13.0.1", "", { "dependencies": { "minipass": "^7.0.3" } }, "sha512-QUiRf1+u9wPTL/76GTYlKttDEBWV1ga9ZXW8BG6kfdeyyM8LGPix9gROyg9V2+P0xNyF3X2Go526xKFdMZrHSQ=="],
"string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"string-width": ["string-width@7.2.0", "", { "dependencies": { "emoji-regex": "^10.3.0", "get-east-asian-width": "^1.0.0", "strip-ansi": "^7.1.0" } }, "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ=="],
"string-width-cjs": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
"strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"strip-ansi": ["strip-ansi@7.1.2", "", { "dependencies": { "ansi-regex": "^6.0.1" } }, "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA=="],
"strip-ansi-cjs": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="],
"tar": ["tar@7.5.22", "", { "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", "minipass": "^7.1.2", "minizlib": "^3.1.0", "yallist": "^5.0.0" } }, "sha512-MFO/QzvtAOmJbkhOaCTvbGcFN9L9b+JunIsDwaKljSOdcLMea3NJ1k9Usz/rjdfSXTq4dfzfeS7W4p4YOAAHeA=="],
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
@@ -646,6 +827,8 @@
"undici-types": ["undici-types@8.9.0", "", {}, "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg=="],
"update-browserslist-db": ["update-browserslist-db@1.3.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ=="],
"util-deprecate": ["util-deprecate@1.0.2", "", {}, "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw=="],
"validate-npm-package-name": ["validate-npm-package-name@7.0.2", "", {}, "sha512-hVDIBwsRruT73PbK7uP5ebUt+ezEtCmzZz3F59BSr2F6OVFnJ/6h8liuvdLrQ88Xmnk6/+xGGuq+pG9WwTuy3A=="],
@@ -654,6 +837,8 @@
"web-streams-polyfill": ["web-streams-polyfill@3.3.3", "", {}, "sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw=="],
"web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="],
"which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="],
"wrap-ansi": ["wrap-ansi@8.1.0", "", { "dependencies": { "ansi-styles": "^6.1.0", "string-width": "^5.0.1", "strip-ansi": "^7.0.1" } }, "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ=="],
@@ -672,10 +857,32 @@
"@aws-crypto/util/@smithy/util-utf8": ["@smithy/util-utf8@2.3.0", "", { "dependencies": { "@smithy/util-buffer-from": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-R8Rdn8Hy72KKcebgLiv8jQcQkXoLMOGGv5uI1/k0l+snqkOzQ1R0ChUBCxWMlBsFMekWjq0wRudIweFs7sKT5A=="],
"@babel/helper-compilation-targets/lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="],
"@isaacs/cliui/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"@isaacs/cliui/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"@npmcli/arborist/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@npmcli/config/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@npmcli/fs/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@npmcli/git/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@npmcli/git/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
"@npmcli/metavuln-calculator/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@npmcli/package-json/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"@npmcli/promise-spawn/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
"babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="],
"babel-plugin-module-resolver/glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="],
"gcp-metadata/gaxios": ["gaxios@7.1.3", "", { "dependencies": { "extend": "^3.0.2", "https-proxy-agent": "^7.0.1", "node-fetch": "^3.3.2", "rimraf": "^5.0.1" } }, "sha512-YGGyuEdVIjqxkxVH1pUTMY/XtmmsApXrCVv5EU25iX6inEPbV+VakJfLealkBtJN69AQmh1eGOdCl9Sm1UP6XQ=="],
"gcp-metadata/google-logging-utils": ["google-logging-utils@1.1.3", "", {}, "sha512-eAmLkjDjAFCVXg7A1unxHsLf961m6y17QFqXqAXGj/gVkKFrEICfStRfwUlGNfeCEjNRa32JEWOUTlYXPyyKvA=="],
@@ -684,18 +891,34 @@
"minipass-pipeline/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="],
"node-gyp/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"node-gyp/undici": ["undici@6.28.1", "", {}, "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA=="],
"node-gyp/which": ["which@6.0.1", "", { "dependencies": { "isexe": "^4.0.0" }, "bin": { "node-which": "bin/which.js" } }, "sha512-oGLe46MIrCRqX7ytPUf66EAYvdeMIZYn3WaocqqKZAxrBpkqHfL/qvTyJ/bTk5+AqHCjXmrv3CEWgy368zhRUg=="],
"npm-install-checks/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"npm-package-arg/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"npm-pick-manifest/semver": ["semver@7.8.5", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA=="],
"parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="],
"rimraf/glob": ["glob@10.5.0", "", { "dependencies": { "foreground-child": "^3.1.0", "jackspeak": "^3.1.2", "minimatch": "^9.0.4", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^1.11.1" }, "bin": { "glob": "dist/esm/bin.mjs" } }, "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg=="],
"string-width/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"string-width-cjs/emoji-regex": ["emoji-regex@8.0.0", "", {}, "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A=="],
"string-width-cjs/strip-ansi": ["strip-ansi@6.0.1", "", { "dependencies": { "ansi-regex": "^5.0.1" } }, "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A=="],
"strip-ansi-cjs/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"wrap-ansi/string-width": ["string-width@5.1.2", "", { "dependencies": { "eastasianwidth": "^0.2.0", "emoji-regex": "^9.2.2", "strip-ansi": "^7.0.1" } }, "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA=="],
"wrap-ansi/strip-ansi": ["strip-ansi@7.2.0", "", { "dependencies": { "ansi-regex": "^6.2.2" } }, "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w=="],
"wrap-ansi-cjs/ansi-styles": ["ansi-styles@4.3.0", "", { "dependencies": { "color-convert": "^2.0.1" } }, "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg=="],
"wrap-ansi-cjs/string-width": ["string-width@4.2.3", "", { "dependencies": { "emoji-regex": "^8.0.0", "is-fullwidth-code-point": "^3.0.0", "strip-ansi": "^6.0.1" } }, "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g=="],
@@ -706,10 +929,20 @@
"@aws-crypto/util/@smithy/util-utf8/@smithy/util-buffer-from": ["@smithy/util-buffer-from@2.2.0", "", { "dependencies": { "@smithy/is-array-buffer": "^2.2.0", "tslib": "^2.6.2" } }, "sha512-IJdWBbTcMQ6DA0gdNhh/BwrLkDR+ADW5Kr1aZmd4k3DIF6ezMV4R2NIAmT08wQJ3yUK82thHWmC/TnK/wpMMIA=="],
"@babel/helper-compilation-targets/lru-cache/yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="],
"@isaacs/cliui/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"@npmcli/git/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
"@npmcli/promise-spawn/which/isexe": ["isexe@4.0.0", "", {}, "sha512-FFUtZMpoZ8RqHS3XeXEmHWLA4thH+ZxCv2lOiPIn1Xc7CxrqhWzNSDzD+/chS/zbYezmiwWLdQC09JdQKmthOw=="],
"babel-plugin-module-resolver/glob/minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="],
"babel-plugin-module-resolver/glob/minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="],
"babel-plugin-module-resolver/glob/path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="],
"minipass-flush/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
"minipass-pipeline/minipass/yallist": ["yallist@4.0.0", "", {}, "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A=="],
@@ -726,10 +959,20 @@
"wrap-ansi-cjs/strip-ansi/ansi-regex": ["ansi-regex@5.0.1", "", {}, "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ=="],
"wrap-ansi/string-width/emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="],
"babel-plugin-module-resolver/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
"babel-plugin-module-resolver/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"babel-plugin-module-resolver/glob/path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="],
"rimraf/glob/minimatch/brace-expansion": ["brace-expansion@2.1.4", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg=="],
"rimraf/glob/path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="],
"babel-plugin-module-resolver/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
"rimraf/glob/minimatch/brace-expansion/balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="],
}
}
+79
View File
@@ -0,0 +1,79 @@
#!/usr/bin/env bash
set -euo pipefail
for command in bun jq; do
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
done
checkout=$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd -P)
config="${OPENCODE_CONFIG_DIR:-${XDG_CONFIG_HOME:-$HOME/.config}/opencode}"
mkdir -p "$config"
config=$(cd "$config" && pwd -P)
mkdir -p "$config/plugins"
install=$(mktemp -d "$config/install.stanp-opencode.XXXXXXXX")
staged=""
backup=""
cleanup() {
[[ -z "$install" ]] || rm -rf "$install"
[[ -z "$staged" ]] || rm -rf "$staged"
}
trap cleanup EXIT
(cd "$checkout" && bun install --frozen-lockfile && bun run build)
bun -e '
if (!await Bun.file(process.argv[1]).exists()) {
throw new Error("Local build lacks the OpenCode V2 directory entrypoint.");
}
const { default: plugin } = await import(process.argv[1]);
if (!plugin || plugin.id !== "stanp-opencode" || typeof plugin.setup !== "function") {
throw new Error("Local build is not a native OpenCode V2 plugin.");
}
const tuiFile = Bun.file(process.argv[2]);
const manifest = await Bun.file(process.argv[3]).json();
const exportsTui = manifest?.exports?.["./tui"];
if (!await tuiFile.exists() || !new Bun.Transpiler({ loader: "js" }).scan(await tuiFile.text()).exports.includes("default") || (typeof exportsTui !== "string" && typeof exportsTui?.import !== "string")) {
throw new Error("Local build lacks the ChatGPT quota TUI plugin.");
}
' "$checkout/dist/index.js" "$checkout/dist/tui.js" "$checkout/package.json"
config_file="$config/opencode.json"
config_source="$config_file"
if [[ ! -e "$config_source" ]]; then
config_source="$install/empty-config.json"
printf '{}\n' > "$config_source"
fi
jq -e '
if type != "object" then error("opencode.json must contain an object") else . end
| if (.plugins // []) | type != "array" then error("opencode.json plugins must be an array") else . end
| .["$schema"] = (.["$schema"] // "https://opencode.ai/config.json")
| .plugins = (.plugins // [])
| if any(.plugins[]?; .package == "./plugins/stanp-opencode") then . else
.plugins += [{
package: "./plugins/stanp-opencode",
options: { mode: "gpt", provider: "openai", lunaPriority: true }
}]
end
' "$config_source" > "$install/opencode.json"
target="$config/plugins/stanp-opencode"
staged="$config/plugins/.stanp-opencode.new.$$"
backup="$config/plugins/.stanp-opencode.old.$$"
mkdir "$staged"
cp "$checkout/dist/index.js" "$staged/index.js"
cp "$checkout/dist/tui.js" "$staged/tui.js"
cat > "$staged/package.json" <<'EOF'
{"name":"stanp-opencode","type":"module","main":"./index.js","exports":{".":"./index.js","./tui":"./tui.js"}}
EOF
if [[ -e "$target" || -L "$target" ]]; then mv "$target" "$backup"; fi
if ! mv "$staged" "$target"; then
if [[ -e "$backup" || -L "$backup" ]]; then mv "$backup" "$target"; fi
printf 'Could not activate plugin; installation unchanged\n' >&2
exit 1
fi
staged=""
rm -rf "$backup"
backup=""
rm -f "$config/plugins/stanp-opencode.js"
mv "$install/opencode.json" "$config_file"
printf 'Built and installed stanp-opencode from %s into %s/plugins/stanp-opencode and updated %s\nRestart opencode2 to load it.\n' "$checkout" "$config" "$config_file"
+9 -2
View File
@@ -42,7 +42,13 @@ bun -e '
if (!plugin || plugin.id !== "stanp-opencode" || typeof plugin.setup !== "function") {
throw new Error("Latest release is not a native OpenCode V2 plugin. Publish a V2-compatible release first.");
}
' "$install/package/dist/index.js"
const tuiFile = Bun.file(process.argv[2]);
const manifest = await Bun.file(process.argv[3]).json();
const exportsTui = manifest?.exports?.["./tui"];
if (!await tuiFile.exists() || !new Bun.Transpiler({ loader: "js" }).scan(await tuiFile.text()).exports.includes("default") || (typeof exportsTui !== "string" && typeof exportsTui?.import !== "string")) {
throw new Error("Latest release lacks the ChatGPT quota TUI plugin.");
}
' "$install/package/dist/index.js" "$install/package/dist/tui.js" "$install/package/package.json"
tag=$(jq -r '.tag_name' "$install/release.json")
config_file="$config/opencode.json"
@@ -68,8 +74,9 @@ staged="$config/plugins/.stanp-opencode.new.$$"
backup="$config/plugins/.stanp-opencode.old.$$"
mkdir "$staged"
mv "$install/package/dist/index.js" "$staged/index.js"
mv "$install/package/dist/tui.js" "$staged/tui.js"
cat > "$staged/package.json" <<'EOF'
{"name":"stanp-opencode","type":"module","main":"./index.js","exports":"./index.js"}
{"name":"stanp-opencode","type":"module","main":"./index.js","exports":{".":"./index.js","./tui":"./tui.js"}}
EOF
if [[ -e "$target" || -L "$target" ]]; then mv "$target" "$backup"; fi
if ! mv "$staged" "$target"; then
+20 -5
View File
@@ -9,6 +9,14 @@
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js"
},
"./tui": {
"types": "./dist/tui.d.ts",
"import": "./dist/tui.js"
},
"./quota-rpc": {
"types": "./dist/quota-rpc.d.ts",
"import": "./dist/quota-rpc.js"
}
},
"files": [
@@ -25,20 +33,27 @@
"lint": "oxlint --deny-warnings .",
"format": "oxfmt --write .",
"format:check": "oxfmt --check .",
"test": "bun test",
"build": "bun build src/index.ts --outdir dist --target bun --format esm && tsc --project tsconfig.build.json --emitDeclarationOnly"
"test": "bun test --conditions=browser",
"build": "bun scripts/build.ts && tsc --project tsconfig.build.json --emitDeclarationOnly"
},
"devDependencies": {
"@opencode/client": "0.0.0-beta-19378",
"@opencode/plugin": "0.0.0-beta-19378",
"@opencode/client": "2.0.2",
"@opencode/plugin": "2.0.2",
"@opencode/schema": "2.0.2",
"@opentui/core": "0.5.10",
"@opentui/solid": "0.5.10",
"@oxlint/plugins": "1.82.0",
"@types/bun": "1.3.14",
"oxfmt": "0.67.0",
"oxlint": "1.82.0",
"solid-js": "1.9.15",
"typescript": "7.0.2"
},
"peerDependencies": {
"@opencode/plugin": "0.0.0-beta-19378"
"@opencode/plugin": "2.0.2",
"@opentui/core": ">=0.5.10",
"@opentui/solid": ">=0.5.10",
"solid-js": ">=1.9.0"
},
"dependencies": {
"zod": "4.1.8"
+28
View File
@@ -0,0 +1,28 @@
import solidPlugin from "@opentui/solid/bun-plugin";
export async function build() {
const results = await Promise.all([
Bun.build({
entrypoints: ["src/index.ts", "src/quota-rpc.ts"],
outdir: "dist",
target: "bun",
format: "esm",
}),
Bun.build({
entrypoints: ["src/tui.tsx"],
outdir: "dist",
target: "bun",
format: "esm",
external: ["@opencode/plugin/tui", "@opentui/solid", "solid-js"],
plugins: [solidPlugin],
}),
]);
for (const result of results) {
if (result.success) continue;
for (const log of result.logs) console.error(log);
throw new Error("Build failed");
}
}
if (import.meta.main) await build();
+39 -1
View File
@@ -1,9 +1,13 @@
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
import type { RpcRegistration } from "@opencode/plugin/promise/rpc";
import type { ModelInfo } from "@opencode/client";
import { z } from "zod";
import { gptAgents, settings, workerNames } from "./config.js";
import { discover } from "./provider.js";
import { descriptions, routingGuide } from "./routing.js";
import { QuotaTracker } from "./quota.js";
import { QuotaRpc } from "./quota-rpc.js";
import { playwrightMcpCommand } from "./playwright.js";
// Permission actions use wildcards, not filesystem globs.
function matches(value: string, pattern: string) {
@@ -50,10 +54,24 @@ export default Plugin.define({
let codegraphTools: string[] = [];
let agentTransformReady = false;
const controller = new AbortController();
let quotaRegistration: RpcRegistration<typeof QuotaRpc> | undefined;
const quota = new QuotaTracker(context, async () => {
await quotaRegistration?.events.emit("updated", {});
});
quotaRegistration = await context.rpc.register(QuotaRpc, {
status: async ({ sessionID }) => (await quota.status(sessionID)) ?? null,
});
await quota.init();
// Subscribe before the first read: configuration can publish its catalog while
// asynchronous setup is running.
const updates = (async () => {
for await (const event of context.event.subscribe({ signal: controller.signal })) {
if (
event.location &&
(event.location.directory !== context.location.directory ||
event.location.workspaceID !== context.location.workspaceID)
)
continue;
if (event.type === "catalog.updated") {
models = await discover(context, selected.provider);
await context.agent.reload();
@@ -69,6 +87,20 @@ export default Plugin.define({
inherited = permissions;
permissionKey = key;
await context.agent.reload();
} else if (
event.type === "session.execution.succeeded" ||
event.type === "session.execution.failed" ||
event.type === "session.execution.interrupted" ||
event.type === "session.idle"
) {
await quota.complete(event.data.sessionID);
} else if (event.type === "credential.updated") {
await quota.authChanged();
} else if (
event.type === "credential.switched" &&
String(event.data.integrationID) === "openai"
) {
await quota.authChanged(true);
}
}
})().catch((error) => {
@@ -82,8 +114,11 @@ export default Plugin.define({
const assignments = selected.mode === "gpt" ? gptAgents : selected.agents;
let workers: (typeof workerNames)[number][] = [];
const active = new Set<string>();
const playwrightCommand = await playwrightMcpCommand(context.location.directory);
await context.mcp.transform((editor) => {
if (!editor.get("playwright"))
editor.set("playwright", { type: "local", command: playwrightCommand });
hasCodegraph = editor.get("codegraph") !== undefined;
if (hasCodegraph)
editor.update("codegraph", (server) => {
@@ -198,7 +233,7 @@ export default Plugin.define({
)?.effect ?? "ask";
if (effect !== "allow")
throw new Error(
`${action} requires an allow permission: OpenCode beta's public plugin API cannot request custom-tool permission approval (${effect}).`,
`${action} requires an allow permission: OpenCode's public plugin API cannot request custom-tool permission approval (${effect}).`,
);
}
await context.tool.transform((editor) => {
@@ -251,6 +286,7 @@ export default Plugin.define({
properties: { ...properties, agent: { type: "string", enum: workers } },
};
});
await context.session.hook("model.request", async (event) => quota.start(event.sessionID));
await context.tool.hook("execute.before", (event) => {
if (event.tool !== "subagent") return;
const { agent } = delegation.parse(event.input);
@@ -278,6 +314,8 @@ export default Plugin.define({
return async () => {
controller.abort();
await updates;
await quota.dispose();
await quotaRegistration?.dispose();
};
},
});
+145
View File
@@ -0,0 +1,145 @@
import { access, readFile, realpath } from "node:fs/promises";
import { constants } from "node:fs";
import { dirname, join, parse, resolve } from "node:path";
import { z } from "zod";
const dockerCommand = [
"docker",
"run",
"-i",
"--rm",
"--init",
"--pull=always",
"mcr.microsoft.com/playwright/mcp",
];
const directPackageManifest = z.object({
name: z.literal("@playwright/mcp"),
bin: z.string(),
});
const namedPackageManifest = z.object({
name: z.literal("@playwright/mcp"),
bin: z.object({ "playwright-mcp": z.string() }),
});
const playwrightPackageManifest = z.object({
name: z.literal("playwright"),
bin: z.object({ playwright: z.string() }),
});
const playwrightTestPackageManifest = z.object({
name: z.literal("@playwright/test"),
bin: z.object({ playwright: z.string() }),
dependencies: z.object({ playwright: z.string() }),
});
async function packageExecutable(packageRoot: string) {
try {
const input = JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8"));
const direct = directPackageManifest.safeParse(input);
const bin = direct.success
? direct.data.bin
: namedPackageManifest.parse(input).bin["playwright-mcp"];
const executable = resolve(packageRoot, bin);
await access(executable, constants.X_OK);
return executable;
} catch {
return;
}
}
async function ordinaryPackageExecutable(packageRoot: string) {
try {
const manifest = playwrightPackageManifest.parse(
JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")),
);
const executable = resolve(packageRoot, manifest.bin.playwright);
await access(executable, constants.X_OK);
return executable;
} catch {
return;
}
}
async function playwrightDependency(packageRoot: string) {
let current = await realpath(packageRoot);
const root = parse(current).root;
while (true) {
const executable = await ordinaryPackageExecutable(
join(current, "node_modules/playwright"),
);
if (executable) return executable;
if (current === root) return;
current = dirname(current);
}
}
async function testPackageExecutable(packageRoot: string) {
try {
const manifest = playwrightTestPackageManifest.parse(
JSON.parse(await readFile(join(packageRoot, "package.json"), "utf8")),
);
if (!(await playwrightDependency(packageRoot))) return;
const executable = resolve(packageRoot, manifest.bin.playwright);
await access(executable, constants.X_OK);
return executable;
} catch {
return;
}
}
async function verifiedPathExecutable(candidate: string, kind: "mcp" | "playwright") {
try {
await access(candidate, constants.X_OK);
const target = await realpath(candidate);
let directory = dirname(target);
const root = parse(directory).root;
while (true) {
const executable =
kind === "mcp"
? await packageExecutable(directory)
: ((await ordinaryPackageExecutable(directory)) ??
(await testPackageExecutable(directory)));
if (executable && (await realpath(executable)) === target) return candidate;
if (directory === root) return;
directory = dirname(directory);
}
} catch {
return;
}
}
export async function playwrightMcpCommand(
directory: string,
path = process.env.PATH ?? "",
): Promise<string[]> {
let current = resolve(directory);
const root = parse(current).root;
while (true) {
const executable = await packageExecutable(join(current, "node_modules/@playwright/mcp"));
if (executable) return [executable];
if (current === root) break;
current = dirname(current);
}
for (const entry of path.split(process.platform === "win32" ? ";" : ":")) {
if (!entry) continue;
const executable = await verifiedPathExecutable(join(entry, "playwright-mcp"), "mcp");
if (executable) return [executable];
}
current = resolve(directory);
while (true) {
const playwright = await ordinaryPackageExecutable(
join(current, "node_modules/playwright"),
);
const test = await testPackageExecutable(join(current, "node_modules/@playwright/test"));
if (playwright || test) return ["npx", "-y", "@playwright/mcp@latest"];
if (current === root) break;
current = dirname(current);
}
for (const entry of path.split(process.platform === "win32" ? ";" : ":")) {
if (!entry) continue;
const executable = await verifiedPathExecutable(join(entry, "playwright"), "playwright");
if (executable) return ["npx", "-y", "@playwright/mcp@latest"];
}
return [...dockerCommand];
}
+25
View File
@@ -0,0 +1,25 @@
import { Rpc } from "@opencode/plugin/rpc";
import { z } from "zod";
const window = z.object({
id: z.string(),
label: z.string().nullable(),
seconds: z.number(),
used: z.number(),
resetsAt: z.number(),
remaining: z.number(),
sessionConsumed: z.number(),
});
export const QuotaRpc = Rpc.define({
id: "stanp-opencode.quota",
methods: {
status: {
input: z.object({ sessionID: z.string() }),
output: z
.object({ plan: z.string(), fetchedAt: z.number(), windows: z.array(window) })
.nullable(),
},
},
events: { updated: { schema: z.object({}) } },
});
+494
View File
@@ -0,0 +1,494 @@
import type { Plugin } from "@opencode/plugin";
import type { Credential } from "@opencode/schema/credential";
import type { Schema } from "effect";
import { z } from "zod";
const INTEGRATION_ID = "openai";
const USAGE_URL = "https://chatgpt.com/backend-api/wham/usage";
const POLL_MS = 60_000;
const TIMEOUT_MS = 5_000;
const STORAGE_KEY = "chatgpt-quota";
function compactLimitIdentity(value: string) {
return value.toLowerCase().replace(/[^a-z0-9]+/g, "");
}
function isSparkLimit(meteredFeature?: string, limitName?: string | null) {
return (
(meteredFeature !== undefined &&
compactLimitIdentity(meteredFeature) === "codexbengalfox") ||
(limitName != null && compactLimitIdentity(limitName) === "gpt53codexspark")
);
}
function isReserveLimit(meteredFeature?: string, limitName?: string | null) {
const hasReserveToken = (value: string) => /(^|[^a-z0-9])reserve($|[^a-z0-9])/i.test(value);
return (
(meteredFeature !== undefined && hasReserveToken(meteredFeature)) ||
(limitName != null && hasReserveToken(limitName))
);
}
function isExcludedLimit(meteredFeature?: string, limitName?: string | null) {
return isSparkLimit(meteredFeature, limitName) || isReserveLimit(meteredFeature, limitName);
}
function isExcludedWindow(window: Pick<QuotaWindow, "id" | "label">) {
return isExcludedLimit(window.id.split(":", 1)[0], window.label);
}
export interface QuotaWindow {
id: string;
label: string | null;
seconds: number;
used: number;
resetsAt: number;
}
export interface QuotaSnapshot {
plan: string;
fetchedAt: number;
windows: QuotaWindow[];
}
export interface QuotaStatus {
plan: string;
fetchedAt: number;
windows: Array<QuotaWindow & { remaining: number; sessionConsumed: number }>;
}
const storedState = z.object({
account: z.string(),
latest: z
.object({
plan: z.string(),
fetchedAt: z.number(),
windows: z.array(
z.object({
id: z.string(),
label: z.string().nullable(),
seconds: z.number(),
used: z.number(),
resetsAt: z.number(),
}),
),
})
.optional(),
sessions: z.record(
z.string(),
z.record(
z.string(),
z.object({ resetsAt: z.number(), lastUsed: z.number(), consumed: z.number() }),
),
),
});
type StoredState = z.infer<typeof storedState>;
function withoutExcludedState(state: StoredState): StoredState {
const windows = state.latest?.windows.filter((window) => !isExcludedWindow(window));
return {
...state,
latest: state.latest && windows?.length ? { ...state.latest, windows } : undefined,
sessions: Object.fromEntries(
Object.entries(state.sessions).map(([session, tracked]) => [
session,
Object.fromEntries(
Object.entries(tracked).filter(
([id]) => !isExcludedLimit(id.split(":", 1)[0], undefined),
),
),
]),
),
};
}
type QuotaContext = Pick<Plugin.Context, "integration" | "session" | "storage">;
type QuotaFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
const quotaWindow = z.object({
limit_window_seconds: z.number().finite().positive(),
used_percent: z.number().finite(),
reset_at: z.number().finite(),
});
const additionalRateLimitIdentity = z.object({
limit_name: z.string().optional(),
metered_feature: z.string().optional(),
});
const quotaPayload = z.object({
plan_type: z.string(),
account_id: z.string().optional(),
rate_limit: z
.object({
allowed: z.boolean().optional(),
primary_window: quotaWindow.nullish(),
secondary_window: quotaWindow.nullish(),
})
.nullish(),
additional_rate_limits: z.preprocess(
(value) =>
Array.isArray(value)
? value.filter((item) => {
const identity = additionalRateLimitIdentity.safeParse(item);
return (
!identity.success ||
!isExcludedLimit(identity.data.metered_feature, identity.data.limit_name)
);
})
: value,
z
.array(
z.object({
limit_name: z.string(),
metered_feature: z.string(),
rate_limit: z
.object({
allowed: z.boolean().optional(),
primary_window: quotaWindow.nullish(),
secondary_window: quotaWindow.nullish(),
})
.nullish(),
}),
)
.nullish(),
),
});
function normalizeWindow(
id: string,
item: z.infer<typeof quotaWindow>,
label?: string,
): QuotaWindow {
return {
id,
label: label ?? null,
seconds: item.limit_window_seconds,
used: Math.max(0, Math.min(100, item.used_percent)),
resetsAt: item.reset_at < 10_000_000_000 ? item.reset_at * 1000 : item.reset_at,
};
}
/** Normalize valid windows from the documented Codex usage payload. */
export function normalizeQuota(
input: Schema.Json,
fetchedAt = Date.now(),
): QuotaSnapshot | undefined {
const parsed = quotaPayload.safeParse(input);
if (!parsed.success) return;
const plan = parsed.data.plan_type.toLowerCase();
const windows: QuotaWindow[] = [];
if (parsed.data.rate_limit)
for (const [position, window] of [
["primary", parsed.data.rate_limit.primary_window],
["secondary", parsed.data.rate_limit.secondary_window],
] as const)
if (window) windows.push(normalizeWindow(`codex:${position}`, window));
for (const additional of parsed.data.additional_rate_limits ?? []) {
if (!additional.rate_limit) continue;
for (const [position, window] of [
["primary", additional.rate_limit.primary_window],
["secondary", additional.rate_limit.secondary_window],
] as const)
if (window)
windows.push(
normalizeWindow(
`${additional.metered_feature}:${position}`,
window,
additional.limit_name,
),
);
}
if (windows.length === 0) return;
return { plan, fetchedAt, windows };
}
function accountID(credential: Credential.OAuth) {
const metadataID = z
.string()
.safeParse(credential.metadata?.accountID ?? credential.metadata?.accountId);
if (metadataID.success) return metadataID.data;
const parts = credential.access.split(".");
if (parts.length !== 3) return;
try {
const claims = z
.object({
chatgpt_account_id: z.string().optional(),
"https://api.openai.com/auth": z
.object({ chatgpt_account_id: z.string().optional() })
.optional(),
organizations: z.array(z.object({ id: z.string() })).optional(),
})
.safeParse(JSON.parse(Buffer.from(parts[1]!, "base64url").toString()));
if (!claims.success) return;
return (
claims.data.chatgpt_account_id ??
claims.data["https://api.openai.com/auth"]?.chatgpt_account_id ??
claims.data.organizations?.[0]?.id
);
} catch {
return;
}
}
async function fingerprint(value: string) {
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(value));
return Buffer.from(bytes).toString("hex");
}
export class QuotaTracker {
private state: StoredState | undefined;
private timer: ReturnType<typeof setInterval> | undefined;
private request: Promise<QuotaSnapshot | undefined> | undefined;
private readonly requests = new Set<Promise<QuotaSnapshot | undefined>>();
private readonly active = new Map<string, Set<string>>();
private controller: AbortController | undefined;
private generation = 0;
private disposed = false;
constructor(
private readonly context: QuotaContext,
private readonly changed: () => Promise<void>,
private readonly fetcher: QuotaFetch = fetch,
private readonly pollMs = POLL_MS,
) {}
async init() {
try {
await this.resolveCredential();
} catch {
return;
}
}
private async resolveCredential(generation = this.generation) {
const connection = await this.context.integration.connection.active(INTEGRATION_ID);
if (generation !== this.generation) return;
const credential = connection
? await this.context.integration.connection.resolve(connection)
: undefined;
if (generation !== this.generation) return;
if (credential?.type !== "oauth") {
if (this.state) await this.clear();
return;
}
const id = accountID(credential);
const account = id
? await fingerprint(`account:${id}`)
: (this.state?.account ?? (await fingerprint(`credential:${credential.refresh}`)));
if (generation !== this.generation) return;
if (this.state?.account !== account) {
const parsed = storedState.safeParse(await this.context.storage.get(STORAGE_KEY));
if (generation !== this.generation) return;
const stored = parsed.success ? parsed.data : undefined;
if (generation !== this.generation) return;
this.state =
stored?.account === account
? withoutExcludedState(stored)
: { account, sessions: {} };
}
if (generation !== this.generation) return;
if (!this.timer) this.timer = setInterval(() => void this.refresh(), this.pollMs);
return credential;
}
private async clear() {
this.state = undefined;
if (this.timer) clearInterval(this.timer);
this.timer = undefined;
await this.context.storage.remove(STORAGE_KEY);
await this.changed();
}
async refresh(): Promise<QuotaSnapshot | undefined> {
const generation = this.generation;
if (this.disposed) return;
if (this.request) return this.request;
const request = this.query();
this.request = request;
this.requests.add(request);
try {
const result = await request;
if (generation !== this.generation) return;
return result ?? this.state?.latest;
} finally {
this.requests.delete(request);
if (this.request === request) this.request = undefined;
}
}
private async query() {
const generation = this.generation;
try {
const credential = await this.resolveCredential(generation);
if (!credential || !this.state || generation !== this.generation) return;
const controller = new AbortController();
this.controller = controller;
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
let response: Response;
try {
const headers = new Headers({
Authorization: `Bearer ${credential.access}`,
Accept: "application/json",
});
const id = accountID(credential);
if (id) headers.set("ChatGPT-Account-Id", id);
response = await this.fetcher(USAGE_URL, { headers, signal: controller.signal });
} finally {
clearTimeout(timeout);
if (this.controller === controller) this.controller = undefined;
}
if (!response.ok || generation !== this.generation) return;
// SAFETY: JSON.parse semantics from Response.json produce only JSON values;
// quotaPayload validates all fields used below.
const payload = (await response.json()) as Schema.Json;
if (generation !== this.generation || !this.state) return;
const parsed = quotaPayload.safeParse(payload);
if (parsed.success && parsed.data.account_id) {
const account = await fingerprint(`account:${parsed.data.account_id}`);
if (generation !== this.generation || !this.state) return;
if (account !== this.state.account) this.state = { account, sessions: {} };
}
const snapshot = normalizeQuota(payload);
if (!snapshot) return;
this.apply(snapshot);
await this.persist();
await this.changed();
return snapshot;
} catch {
return;
}
}
private apply(snapshot: QuotaSnapshot) {
if (!this.state) return;
for (const root of this.active.keys()) {
const tracked = (this.state.sessions[root] ??= {});
for (const window of snapshot.windows) {
const previous = tracked[window.id];
if (!previous) {
tracked[window.id] = {
resetsAt: window.resetsAt,
lastUsed: window.used,
consumed: 0,
};
continue;
}
if (previous.resetsAt !== window.resetsAt) {
previous.resetsAt = window.resetsAt;
previous.lastUsed = window.used;
previous.consumed += window.used;
} else if (window.used >= previous.lastUsed) {
previous.consumed += window.used - previous.lastUsed;
previous.lastUsed = window.used;
}
}
}
this.state.latest = snapshot;
}
private async root(sessionID: string) {
let current = sessionID;
const seen = new Set<string>();
while (!seen.has(current)) {
seen.add(current);
const session = await this.context.session.get({ sessionID: current });
if (!session.parentID) return current;
current = session.parentID;
}
return sessionID;
}
async start(sessionID: string) {
const generation = this.generation;
try {
const root = await this.root(sessionID);
if (generation !== this.generation) return;
const family = this.active.get(root);
if (family?.has(sessionID)) return;
const snapshot = await this.refresh();
if (!snapshot || !this.state || generation !== this.generation) return;
if (!family) {
const previous = this.state.sessions[root] ?? {};
this.state.sessions[root] = Object.fromEntries(
snapshot.windows.map((window) => [
window.id,
{
resetsAt: window.resetsAt,
lastUsed: window.used,
consumed: previous[window.id]?.consumed ?? 0,
},
]),
);
}
await this.persist();
if (generation !== this.generation) return;
await this.changed();
if (generation !== this.generation) return;
const active = family ?? new Set<string>();
active.add(sessionID);
this.active.set(root, active);
} catch {
return;
}
}
async complete(sessionID: string) {
try {
const root = await this.root(sessionID);
const family = this.active.get(root);
if (!family?.has(sessionID)) return;
await this.refresh();
family.delete(sessionID);
if (family.size === 0) this.active.delete(root);
await this.persist();
await this.changed();
} catch {
return;
}
}
async status(sessionID: string): Promise<QuotaStatus | undefined> {
if (!this.state?.latest) return;
const tracked = this.state.sessions[await this.root(sessionID)];
return {
plan: this.state.latest.plan,
fetchedAt: this.state.latest.fetchedAt,
windows: this.state.latest.windows.map((window) => ({
...window,
remaining: Math.max(0, 100 - window.used),
sessionConsumed: tracked?.[window.id]?.consumed ?? 0,
})),
};
}
private async persist() {
if (!this.state) return;
const value: Schema.Json = this.state.latest
? {
account: this.state.account,
sessions: this.state.sessions,
latest: this.state.latest,
}
: { account: this.state.account, sessions: this.state.sessions };
await this.context.storage.set(STORAGE_KEY, value);
}
async authChanged(switched = false) {
if (switched) {
this.generation++;
this.controller?.abort();
this.controller = undefined;
this.request = undefined;
if (this.state) await this.clear();
}
await this.refresh();
}
async dispose() {
this.disposed = true;
this.generation++;
this.controller?.abort();
if (this.timer) clearInterval(this.timer);
this.timer = undefined;
await Promise.all(this.requests);
}
}
+78
View File
@@ -0,0 +1,78 @@
import { Plugin } from "@opencode/plugin/tui";
import { createResource, createSignal, For, Show } from "solid-js";
import type { QuotaStatus, QuotaWindow } from "./quota.js";
import { QuotaRpc } from "./quota-rpc.js";
function duration(seconds: number) {
if (seconds % 86_400 === 0) return `${seconds / 86_400}d`;
if (seconds % 3_600 === 0) return `${seconds / 3_600}h`;
return `${Math.round(seconds / 60)}m`;
}
function percent(value: number) {
return value < 0.1 && value > 0 ? "<0.1" : value.toFixed(value < 10 ? 1 : 0);
}
export function formatQuotaWindow(
window: QuotaWindow & { remaining: number; sessionConsumed: number },
) {
const label = window.label
? `${window.label} ${duration(window.seconds)}`
: duration(window.seconds);
return `${label}: ${percent(window.remaining)}% left · ~${percent(window.sessionConsumed)}pp this session`;
}
export async function loadQuotaStatus(load: () => Promise<QuotaStatus | null>) {
try {
return await load();
} catch {
return null;
}
}
export default Plugin.define({
id: "stanp-opencode.quota-tui",
setup(context) {
const quota = context.client.rpc(QuotaRpc);
const [revision, setRevision] = createSignal(0);
const retry = setInterval(() => {
setRevision((value) => value + 1);
}, 60_000);
const unsubscribe = quota.events.on("updated", () => {
setRevision((value) => value + 1);
});
const Slot = (props: { sessionID: string }) => {
const [status] = createResource(
() => `${props.sessionID}:${revision()}`,
() => loadQuotaStatus(() => quota.status({ sessionID: props.sessionID })),
);
return (
<Show when={status()}>
<box flexDirection="column">
<text fg={context.theme.text.default}>
<b>ChatGPT quota ({status()!.plan})</b>
</text>
<For each={status()!.windows}>
{(window) => (
<text fg={context.theme.text.muted}>
{formatQuotaWindow(window)}
</text>
)}
</For>
</box>
</Show>
);
};
const remove = context.ui.slot({
append: "sidebar.footer",
render: (input) => <Slot sessionID={input.sessionID} />,
});
return () => {
remove();
unsubscribe();
clearInterval(retry);
};
},
});
+224
View File
@@ -0,0 +1,224 @@
import { afterEach, expect, test } from "bun:test";
import { chmod, mkdir, mkdtemp, readFile, readdir, rm, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
const installer = fileURLToPath(new URL("../install-local.sh", import.meta.url));
const directories: string[] = [];
const jsonc = '{\n // This file must remain untouched.\n "theme": "dark"\n}\n';
const oldPlugin = 'export default { id: "old", setup() { return "old"; } };\n';
type FixtureConfig =
| { permissions: { shell: string }; plugins: { package: string }[] }
| {
theme: string;
plugins: {
package: string;
options: { mode: string; coordinate: string; lunaPriority: boolean };
}[];
}
| { plugins: string };
afterEach(async () => {
await Promise.all(
directories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
);
});
async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) {
const child = Bun.spawn(command, {
cwd,
env,
stdout: "pipe",
stderr: "pipe",
timeout: 20_000,
});
const [exitCode, stdout, stderr] = await Promise.all([
child.exited,
new Response(child.stdout).text(),
new Response(child.stderr).text(),
]);
return { exitCode, stdout, stderr };
}
async function fixture(configValue?: FixtureConfig) {
const directory = await mkdtemp(join(tmpdir(), "stanp-local-install-"));
directories.push(directory);
const checkout = join(directory, "checkout with spaces");
const cwd = join(directory, "different cwd");
const config = join(directory, "config with spaces");
const bin = join(directory, "bin");
const installed = join(config, "plugins", "stanp-opencode");
await Promise.all([
mkdir(checkout),
mkdir(cwd),
mkdir(bin),
mkdir(installed, { recursive: true }),
]);
await Promise.all([
Bun.write(join(checkout, "install-local.sh"), Bun.file(installer)),
writeFile(
join(checkout, "package.json"),
JSON.stringify({
name: "stanp-opencode",
version: "1.0.0",
type: "module",
exports: { ".": "./dist/index.js", "./tui": { import: "./dist/tui.js" } },
}),
),
writeFile(join(checkout, "bun.lock"), ""),
writeFile(join(config, "opencode.jsonc"), jsonc),
writeFile(
join(config, "opencode.json"),
JSON.stringify(
configValue ?? {
permissions: { shell: "ask" },
plugins: [{ package: "existing-plugin" }],
},
),
),
writeFile(join(installed, "index.js"), oldPlugin),
writeFile(
join(installed, "package.json"),
'{"name":"stanp-opencode","type":"module","main":"./index.js"}\n',
),
writeFile(join(config, "plugins", "stanp-opencode.js"), "legacy\n"),
writeFile(
join(bin, "bun"),
`#!/usr/bin/env bash
set -euo pipefail
if [[ "\${1:-}" == "install" ]]; then
[[ "\${2:-}" == "--frozen-lockfile" ]]
printf '%s\n' "$PWD" >> "$CALLS"
[[ "$MODE" != "dependency" ]]
exit
fi
if [[ "\${1:-}" == "run" && "\${2:-}" == "build" ]]; then
printf '%s\n' "$PWD" >> "$CALLS"
[[ "$MODE" != "build" ]]
mkdir -p dist
if [[ "$MODE" == "validation" ]]; then
printf '%s\n' 'export default { id: "stanp-opencode", server() {} };' > dist/index.js
else
printf 'export default { id: "stanp-opencode", setup() { return "%s"; } };\n' "$GENERATION" > dist/index.js
fi
printf '%s\n' 'export default { id: "stanp-opencode.quota-tui", setup() {} };' > dist/tui.js
exit
fi
exec "$REAL_BUN" "$@"
`,
),
writeFile(
join(bin, "curl"),
`#!/usr/bin/env bash
touch "$FETCH_MARKER"
printf 'release fetching is forbidden\n' >&2
exit 99
`,
),
]);
await Promise.all([chmod(join(bin, "bun"), 0o755), chmod(join(bin, "curl"), 0o755)]);
const env = {
...Bun.env,
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH ?? "/usr/bin:/bin"}`,
HOME: join(directory, "home"),
XDG_CONFIG_HOME: join(directory, "ignored-xdg"),
OPENCODE_CONFIG_DIR: config,
REAL_BUN: process.execPath,
MODE: "success",
GENERATION: "first",
CALLS: join(directory, "calls"),
FETCH_MARKER: join(directory, "release-fetch-attempted"),
};
return { directory, checkout, cwd, config, installed, env };
}
test("builds from its checkout and installs defaults when invoked elsewhere", async () => {
const setup = await fixture();
const result = await run(
["bash", join(setup.checkout, "install-local.sh")],
setup.cwd,
setup.env,
);
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
expect(await readFile(setup.env.CALLS, "utf8")).toBe(`${setup.checkout}\n${setup.checkout}\n`);
expect(await Bun.file(setup.env.FETCH_MARKER).exists()).toBe(false);
expect(await readFile(join(setup.config, "opencode.jsonc"), "utf8")).toBe(jsonc);
expect(JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"))).toEqual({
$schema: "https://opencode.ai/config.json",
permissions: { shell: "ask" },
plugins: [
{ package: "existing-plugin" },
{
package: "./plugins/stanp-opencode",
options: { mode: "gpt", provider: "openai", lunaPriority: true },
},
],
});
const imported = await run(
[
process.execPath,
"-e",
"const { default: plugin } = await import(process.argv[1]); console.log(plugin.setup());",
setup.installed,
],
setup.cwd,
setup.env,
);
expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: "first\n" });
expect((await readdir(setup.installed)).sort()).toEqual(["index.js", "package.json", "tui.js"]);
expect(await Bun.file(join(setup.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
});
test("reinstall replaces bundles while preserving existing settings and plugin options", async () => {
const configured = {
theme: "dark",
plugins: [
{
package: "./plugins/stanp-opencode",
options: { mode: "custom", coordinate: "provider/model", lunaPriority: false },
},
],
};
const setup = await fixture(configured);
const first = await run(
["bash", join(setup.checkout, "install-local.sh")],
setup.cwd,
setup.env,
);
expect(first, first.stderr).toMatchObject({ exitCode: 0 });
const second = await run(["bash", join(setup.checkout, "install-local.sh")], setup.cwd, {
...setup.env,
GENERATION: "second",
});
expect(second, second.stderr).toMatchObject({ exitCode: 0 });
expect(JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"))).toEqual({
$schema: "https://opencode.ai/config.json",
...configured,
});
expect(await readFile(join(setup.installed, "index.js"), "utf8")).toContain('return "second"');
});
for (const mode of ["dependency", "build", "validation", "config"] as const) {
test(`${mode} failure leaves the previous installation and configuration untouched`, async () => {
const invalidConfig = mode === "config" ? { plugins: "not-an-array" } : undefined;
const setup = await fixture(invalidConfig);
const beforeConfig = await readFile(join(setup.config, "opencode.json"), "utf8");
const result = await run(["bash", join(setup.checkout, "install-local.sh")], setup.cwd, {
...setup.env,
MODE: mode === "config" ? "success" : mode,
});
expect(result.exitCode, result.stderr).not.toBe(0);
expect(await readFile(join(setup.installed, "index.js"), "utf8")).toBe(oldPlugin);
expect(await readFile(join(setup.config, "opencode.json"), "utf8")).toBe(beforeConfig);
expect(await readFile(join(setup.config, "opencode.jsonc"), "utf8")).toBe(jsonc);
expect(await readFile(join(setup.config, "plugins", "stanp-opencode.js"), "utf8")).toBe(
"legacy\n",
);
expect((await readdir(join(setup.config, "plugins"))).sort()).toEqual([
"stanp-opencode",
"stanp-opencode.js",
]);
expect(await Bun.file(setup.env.FETCH_MARKER).exists()).toBe(false);
});
}
+6 -1
View File
@@ -96,7 +96,7 @@ async function install(mode: "native" | "checksum" | "legacy", configured = fals
version: "1.0.0",
type: "module",
main: "./dist/index.js",
exports: "./dist/index.js",
exports: { ".": "./dist/index.js", "./tui": "./dist/tui.js" },
scripts: { postinstall: "bun ./postinstall.js" },
}),
),
@@ -111,6 +111,10 @@ async function install(mode: "native" | "checksum" | "legacy", configured = fals
? 'export default { id: "stanp-opencode", async server() { return {}; } };\n'
: 'export default { id: "stanp-opencode", setup() { return "native-ready"; } };\n'),
),
writeFile(
join(fixture, "dist", "tui.js"),
'export default { id: "stanp-opencode.quota-tui", setup() { return "tui-ready"; } };\n',
),
]);
const env = {
...Bun.env,
@@ -220,6 +224,7 @@ test("installs an importable native plugin and default config under XDG without
expect(await Bun.file(join(config, "plugins", "stanp-opencode", "package.json")).exists()).toBe(
true,
);
expect(await Bun.file(join(config, "plugins", "stanp-opencode", "tui.js")).exists()).toBe(true);
expect(
await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"),
).toBe(previousPlugin);
+208 -6
View File
@@ -1,4 +1,7 @@
import { afterEach, expect, test } from "bun:test";
import { chmod, mkdir, mkdtemp, rm, symlink, writeFile } from "node:fs/promises";
import { tmpdir } from "node:os";
import { join } from "node:path";
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
import type { AgentEditor } from "@opencode/plugin/promise/agent";
import type { DeepMutable } from "@opencode/plugin/promise/types";
@@ -8,6 +11,7 @@ import type { SessionContext } from "@opencode/plugin/promise/session";
import { z } from "zod";
import { gptAgents } from "../src/config.js";
import plugin from "../src/index.js";
import { playwrightMcpCommand } from "../src/playwright.js";
const providerID = Provider.ID.make("test");
function model(id: string): ModelInfo {
@@ -32,13 +36,14 @@ async function host(
{ type: "local"; command: string[]; codemode?: boolean; disabled?: boolean }
> = {},
configuredMcpTools: string[] = [],
directory = "/test",
) {
const agents = new Map<string, DeepMutable<Agent.Info>>();
const build: DeepMutable<Agent.Info> = Agent.Info.default(Agent.ID.make("build"));
build.permissions.push({ action: "shell", resource: "git push *", effect: "ask" });
agents.set("build", build);
const originalBuild = structuredClone(build);
const sessionHooks: Record<string, (event: HookEvent) => Promise<void> | void> = {};
const sessionHooks: Record<string, ((event: HookEvent) => Promise<void> | void)[]> = {};
let before!: (event: { tool: string; agent: string; input: unknown }) => Promise<void> | void;
let parent = "build";
let child = "fast-lo";
@@ -46,8 +51,14 @@ async function host(
const registration = { async dispose() {} };
let transform!: (editor: AgentEditor) => void;
let reloaded = Promise.withResolvers<void>();
let events!: ReadableStreamDefaultController<{ type: string }>;
const stream = new ReadableStream<{ type: string }>({
let events!: ReadableStreamDefaultController<{
type: string;
location?: { directory: string; workspaceID?: string };
}>;
const stream = new ReadableStream<{
type: string;
location?: { directory: string; workspaceID?: string };
}>({
start(controller) {
events = controller;
},
@@ -58,11 +69,15 @@ async function host(
const mcpServers = new Map(Object.entries(structuredClone(configuredMcp)));
let mcpTransform!: (editor: {
get(name: string): (typeof configuredMcp)[string] | undefined;
set(name: string, server: (typeof configuredMcp)[string]): void;
update(name: string, update: (server: (typeof configuredMcp)[string]) => void): void;
}) => void;
const toolTransforms: ((editor: ToolEditor) => void)[] = [];
const editMcp = () => ({
get: (name: string) => mcpServers.get(name),
set(name: string, server: (typeof configuredMcp)[string]) {
mcpServers.set(name, structuredClone(server));
},
update(name: string, update: (server: (typeof configuredMcp)[string]) => void) {
const server = mcpServers.get(name);
if (server) update(server);
@@ -101,6 +116,7 @@ async function host(
// SAFETY: This test host implements the native APIs used by setup; unrelated context
// services are deliberately absent so unexpected new dependencies fail the test.
const context = {
location: { directory },
options,
catalog: {
model: {
@@ -122,6 +138,24 @@ async function host(
async set(key: string, value: JsonValue) {
storage.set(key, value);
},
async remove(key: string) {
storage.delete(key);
},
},
integration: {
connection: {
async active() {
return undefined;
},
async resolve() {
return undefined;
},
},
},
rpc: {
async register() {
return { ...registration, events: { async emit() {} } };
},
},
mcp: {
async transform(callback: typeof mcpTransform) {
@@ -165,7 +199,7 @@ async function host(
},
session: {
async hook(name: string, callback: (event: HookEvent) => Promise<void> | void) {
sessionHooks[name] = callback;
(sessionHooks[name] ??= []).push(callback);
return registration;
},
async get({ sessionID }: { sessionID: string }) {
@@ -209,6 +243,10 @@ async function host(
events.enqueue({ type: "agent.updated" });
await reloaded.promise;
},
changeBuildPermissionsElsewhere(permissions: Agent.Info["permissions"]) {
build.permissions = [...permissions];
events.enqueue({ type: "agent.updated", location: { directory: "/other" } });
},
async changeCodegraph(
server:
| { type: "local"; command: string[]; codemode?: boolean; disabled?: boolean }
@@ -236,7 +274,7 @@ async function host(
async command(caller: string, target: string) {
parent = caller;
child = target;
await sessionHooks.prompt!({ sessionID: "child" });
for (const hook of sessionHooks.prompt ?? []) await hook({ sessionID: "child" });
},
async tools(caller: string) {
const event: ModelContext = {
@@ -251,7 +289,7 @@ async function host(
},
},
};
await sessionHooks.context!(event);
for (const hook of sessionHooks.context ?? []) await hook(event);
return event.tools;
},
};
@@ -379,6 +417,155 @@ test("coordinator inherits only codegraph MCP permissions when that server exist
});
});
test("playwright MCP prefers direct MCP, uses npx for Playwright, and preserves fallback behavior", async () => {
const originalPath = process.env.PATH;
const root = await mkdtemp(join(tmpdir(), "stanp-playwright-"));
try {
process.env.PATH = "";
const packageRoot = join(root, "local", "node_modules", "@playwright", "mcp");
const executable = join(packageRoot, "cli.js");
const localProject = join(root, "local", "project");
const globalProject = join(root, "global-project");
await mkdir(localProject, { recursive: true });
await mkdir(globalProject);
await mkdir(packageRoot, { recursive: true });
await writeFile(
join(packageRoot, "package.json"),
JSON.stringify({ name: "@playwright/mcp", bin: { "playwright-mcp": "cli.js" } }),
);
await writeFile(executable, "#!/usr/bin/env node\n");
await chmod(executable, 0o755);
const local = await host(allModels, { provider: "test" }, {}, [], localProject);
expect(local.mcpServers.get("playwright")).toEqual({
type: "local",
command: [executable],
});
const globalPackage = join(root, "global", "@playwright", "mcp");
const globalExecutable = join(globalPackage, "cli.js");
const globalBin = join(root, "bin");
const pathExecutable = join(globalBin, "playwright-mcp");
await mkdir(globalPackage, { recursive: true });
await mkdir(globalBin, { recursive: true });
await writeFile(
join(globalPackage, "package.json"),
JSON.stringify({ name: "@playwright/mcp", bin: { "playwright-mcp": "cli.js" } }),
);
await writeFile(globalExecutable, "#!/usr/bin/env node\n");
await chmod(globalExecutable, 0o755);
await symlink(globalExecutable, pathExecutable);
expect(await playwrightMcpCommand(globalProject, globalBin)).toEqual([pathExecutable]);
const ordinaryPackage = join(root, "ordinary-project", "node_modules", "playwright");
const ordinaryProject = join(root, "ordinary-project", "project");
const ordinaryExecutable = join(ordinaryPackage, "cli.js");
await mkdir(ordinaryPackage, { recursive: true });
await mkdir(ordinaryProject);
await writeFile(
join(ordinaryPackage, "package.json"),
JSON.stringify({ name: "playwright", bin: { playwright: "cli.js" } }),
);
await writeFile(ordinaryExecutable, "#!/usr/bin/env node\n");
await chmod(ordinaryExecutable, 0o755);
const ordinary = await host(allModels, { provider: "test" }, {}, [], ordinaryProject);
expect(ordinary.mcpServers.get("playwright")?.command).toEqual([
"npx",
"-y",
"@playwright/mcp@latest",
]);
const testPackage = join(root, "test-project", "node_modules", "@playwright", "test");
const testDependency = join(testPackage, "node_modules", "playwright");
const testProject = join(root, "test-project", "project");
await mkdir(testDependency, { recursive: true });
await mkdir(testProject);
await writeFile(
join(testPackage, "package.json"),
JSON.stringify({
name: "@playwright/test",
bin: { playwright: "cli.js" },
dependencies: { playwright: "1.0.0" },
}),
);
await writeFile(join(testPackage, "cli.js"), "#!/usr/bin/env node\n");
await chmod(join(testPackage, "cli.js"), 0o755);
await writeFile(
join(testDependency, "package.json"),
JSON.stringify({ name: "playwright", bin: { playwright: "cli.js" } }),
);
await writeFile(join(testDependency, "cli.js"), "#!/usr/bin/env node\n");
await chmod(join(testDependency, "cli.js"), 0o755);
expect(await playwrightMcpCommand(testProject, "")).toEqual([
"npx",
"-y",
"@playwright/mcp@latest",
]);
const pathPackage = join(root, "path-playwright", "playwright");
const pathBin = join(root, "playwright-bin");
const pathPlaywright = join(pathBin, "playwright");
await mkdir(pathPackage, { recursive: true });
await mkdir(pathBin);
await writeFile(
join(pathPackage, "package.json"),
JSON.stringify({ name: "playwright", bin: { playwright: "cli.js" } }),
);
await writeFile(join(pathPackage, "cli.js"), "#!/usr/bin/env node\n");
await chmod(join(pathPackage, "cli.js"), 0o755);
await symlink(join(pathPackage, "cli.js"), pathPlaywright);
expect(await playwrightMcpCommand(globalProject, pathBin)).toEqual([
"npx",
"-y",
"@playwright/mcp@latest",
]);
const ordinaryBin = join(root, "ordinary-bin");
const fakePlaywright = join(ordinaryBin, "playwright");
await mkdir(ordinaryBin);
await writeFile(fakePlaywright, "#!/usr/bin/env node\n");
await chmod(fakePlaywright, 0o755);
expect((await playwrightMcpCommand(globalProject, ordinaryBin))[0]).toBe("docker");
const fallback = await host();
expect(fallback.mcpServers.get("playwright")).toEqual({
type: "local",
command: [
"docker",
"run",
"-i",
"--rm",
"--init",
"--pull=always",
"mcr.microsoft.com/playwright/mcp",
],
});
const existing = await host(
allModels,
{ provider: "test" },
{
playwright: {
type: "local",
command: ["custom-playwright-mcp"],
disabled: true,
},
docs: { type: "local", command: ["docs"], codemode: true },
},
);
expect(existing.mcpServers.get("playwright")).toEqual({
type: "local",
command: ["custom-playwright-mcp"],
disabled: true,
});
expect(existing.mcpServers.get("docs")?.codemode).toBe(true);
} finally {
if (originalPath === undefined) delete process.env.PATH;
else process.env.PATH = originalPath;
await rm(root, { recursive: true, force: true });
}
});
test("manual calls and command-created child sessions cannot bypass delegation restrictions", async () => {
const result = await host(allModels.slice(0, 2));
for (const invoke of [result.delegate, result.command]) {
@@ -459,6 +646,21 @@ test("late build permission overrides remain effective for workers and coordinat
).toBe("deny");
});
test("events from another location do not reload this location's agents", async () => {
const result = await host();
result.changeBuildPermissionsElsewhere([
...result.build.permissions,
{ action: "read", resource: "*.env", effect: "deny" },
]);
await result.refresh(allModels);
expect(
result.agents
.get("coordinate")
?.permissions.filter((rule) => rule.action === "read" && rule.resource === "*.env")
.at(-1)?.effect,
).toBe("ask");
});
test("todo lists persist within one session without leaking to another", async () => {
const result = await host();
const todos = [{ id: "1", content: "Implement", status: "in_progress", priority: "high" }];
+548
View File
@@ -0,0 +1,548 @@
import { expect, test } from "bun:test";
import type { Schema } from "effect";
import { normalizeQuota, QuotaTracker } from "../src/quota.js";
import { formatQuotaWindow, loadQuotaStatus } from "../src/tui.js";
const plus = {
plan_type: "plus",
account_id: "account-one",
rate_limit: {
allowed: true,
limit_reached: false,
primary_window: {
used_percent: 12.5,
limit_window_seconds: 18_000,
reset_after_seconds: 1_000,
reset_at: 2_000_000_000,
},
secondary_window: {
used_percent: 41,
limit_window_seconds: 604_800,
reset_after_seconds: 2_000,
reset_at: 2_000_500_000,
},
},
};
test("normalizes representative Plus and Pro payloads and hides absent windows", () => {
expect(normalizeQuota(plus, 123)).toEqual({
plan: "plus",
fetchedAt: 123,
windows: [
{
id: "codex:primary",
label: null,
seconds: 18_000,
used: 12.5,
resetsAt: 2_000_000_000_000,
},
{
id: "codex:secondary",
label: null,
seconds: 604_800,
used: 41,
resetsAt: 2_000_500_000_000,
},
],
});
expect(normalizeQuota({ ...plus, plan_type: "pro" }, 123)?.windows).toHaveLength(2);
expect(
normalizeQuota({
plan_type: "pro",
rate_limit: { primary_window: plus.rate_limit.primary_window },
})?.windows,
).toEqual([
{
id: "codex:primary",
label: null,
seconds: 18_000,
used: 12.5,
resetsAt: 2_000_000_000_000,
},
]);
expect(normalizeQuota({ plan_type: "plus", rate_limit: null })).toBeUndefined();
});
test("normalizes additional named limits with stable IDs distinct from equal durations", () => {
const normalized = normalizeQuota({
...plus,
additional_rate_limits: [
{
limit_name: "GPT-5.6 Luna",
metered_feature: "codex_luna",
rate_limit: { primary_window: plus.rate_limit.primary_window },
},
],
});
expect(normalized?.windows.map((window) => window.id)).toEqual([
"codex:primary",
"codex:secondary",
"codex_luna:primary",
]);
expect(normalized?.windows[2]?.label).toBe("GPT-5.6 Luna");
expect(
normalizeQuota({
plan_type: "pro",
rate_limit: null,
additional_rate_limits: [
{
limit_name: "GPT-5.6 Luna",
metered_feature: "codex_luna",
rate_limit: { primary_window: plus.rate_limit.primary_window },
},
],
})?.windows.map((window) => window.id),
).toEqual(["codex_luna:primary"]);
});
test("ignores Spark and representative reserve limits while preserving other quotas", () => {
const normalized = normalizeQuota({
...plus,
additional_rate_limits: [
{
limit_name: "gpt 5_3 codex spark",
metered_feature: "CODEX-BENGALFOX",
rate_limit: {
primary_window: { used_percent: "invalid" },
},
},
{
limit_name: "Luna Reserve",
metered_feature: "codex_luna",
rate_limit: { primary_window: { used_percent: "invalid" } },
},
{
limit_name: "Luna overflow",
metered_feature: "codex_luna_reserve",
rate_limit: { primary_window: { used_percent: "invalid" } },
},
{
limit_name: "GPT-5.6 Luna",
metered_feature: "codex_luna",
rate_limit: { primary_window: plus.rate_limit.primary_window },
},
{
limit_name: "Preserved capacity",
metered_feature: "codex_preserved",
rate_limit: { primary_window: plus.rate_limit.primary_window },
},
],
});
expect(normalized?.windows.map((window) => window.id)).toEqual([
"codex:primary",
"codex:secondary",
"codex_luna:primary",
"codex_preserved:primary",
]);
expect(normalized?.windows.slice(2).map((window) => window.label)).toEqual([
"GPT-5.6 Luna",
"Preserved capacity",
]);
});
test("returns no quota for a Spark-only payload", () => {
expect(
normalizeQuota({
plan_type: "pro",
additional_rate_limits: [
{
limit_name: "GPT-5.3-Codex-Spark",
metered_feature: "codex_bengalfox",
rate_limit: { primary_window: plus.rate_limit.primary_window },
},
],
}),
).toBeUndefined();
});
function fixture(
credential:
| {
type: "oauth";
access: string;
refresh: string;
metadata?: { accountID?: string; accountId?: string };
}
| {
type: "key";
key: string;
},
payloads: unknown[],
pollMs = 1_000_000,
storageReads: Promise<Schema.Json | undefined>[] = [],
) {
const stored = new Map<string, Schema.Json>();
const sessions = new Map([
["root", {}],
["child", { parentID: "root" }],
]);
let calls = 0;
let storageCalls = 0;
let changes = 0;
let current: typeof credential | undefined = credential;
const headers: Headers[] = [];
const tracker = new QuotaTracker(
// SAFETY: This focused fixture implements only the native calls exercised by
// QuotaTracker; branded connection and session fields are irrelevant here.
{
integration: {
connection: {
async active() {
// SAFETY: Only presence is observed; QuotaTracker passes this value back unchanged.
return current ? ({ id: "connection" } as never) : undefined;
},
async resolve() {
// SAFETY: The fixture covers the credential discriminant and fields QuotaTracker reads.
return current as never;
},
},
} as never,
session: {
async get({ sessionID }: { sessionID: string }) {
// SAFETY: QuotaTracker reads only parentID from this focused fixture.
return sessions.get(sessionID) as never;
},
} as never,
storage: {
async get(key: string) {
storageCalls++;
const deferred = storageReads.shift();
if (deferred) return deferred;
return stored.get(key);
},
async set(key: string, value: Schema.Json) {
stored.set(key, value);
},
async remove(key: string) {
stored.delete(key);
},
async scan() {
return { entries: [] };
},
},
},
async () => {
changes++;
},
async (_url, init) => {
calls++;
const requestHeaders = new Headers(init?.headers);
headers.push(requestHeaders);
expect(requestHeaders.get("authorization")).toStartWith("Bearer ");
const payload = await (payloads.shift() ?? plus);
if (payload instanceof Error) throw payload;
return Response.json(payload);
},
pollMs,
);
return {
tracker,
calls: () => calls,
storageCalls: () => storageCalls,
changes: () => changes,
headers,
switch(credential: typeof current) {
current = credential;
},
logout: () => {
current = undefined;
},
};
}
async function accountFingerprint(id: string) {
const bytes = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(`account:${id}`));
return Buffer.from(bytes).toString("hex");
}
test("removes cached Spark and representative reserve windows and consumption while offline", async () => {
const cached = {
account: await accountFingerprint("account-one"),
latest: {
plan: "pro",
fetchedAt: 123,
windows: [
{
id: "codex:primary",
label: null,
seconds: 18_000,
used: 12.5,
resetsAt: 2_000_000_000_000,
},
{
id: "CODEX-BENGALFOX:primary",
label: "GPT 5.3 Codex Spark",
seconds: 18_000,
used: 80,
resetsAt: 2_000_000_000_000,
},
{
id: "codex_luna_reserve:primary",
label: "Luna Reserve",
seconds: 18_000,
used: 60,
resetsAt: 2_000_000_000_000,
},
{
id: "codex_luna:primary",
label: "GPT-5.6 Luna",
seconds: 18_000,
used: 20,
resetsAt: 2_000_000_000_000,
},
],
},
sessions: {
root: {
"codex:primary": {
resetsAt: 2_000_000_000_000,
lastUsed: 12.5,
consumed: 2,
},
"CODEX-BENGALFOX:primary": {
resetsAt: 2_000_000_000_000,
lastUsed: 80,
consumed: 40,
},
"codex_luna_reserve:primary": {
resetsAt: 2_000_000_000_000,
lastUsed: 60,
consumed: 30,
},
"codex_luna:primary": {
resetsAt: 2_000_000_000_000,
lastUsed: 20,
consumed: 3,
},
},
},
};
const result = fixture(
{
type: "oauth",
access: "opaque",
refresh: "refresh",
metadata: { accountID: "account-one" },
},
[new Error("offline")],
1_000_000,
[Promise.resolve(cached)],
);
await result.tracker.init();
const status = await result.tracker.status("root");
expect(status?.windows.map((window) => window.id)).toEqual([
"codex:primary",
"codex_luna:primary",
]);
expect(status?.windows[0]?.sessionConsumed).toBe(2);
expect(status?.windows[1]?.sessionConsumed).toBe(3);
await result.tracker.dispose();
});
test("activates only for OAuth and establishes an awaited root baseline shared by children", async () => {
const key = fixture({ type: "key", key: "secret" }, [plus]);
await key.tracker.init();
await key.tracker.start("root");
expect(key.calls()).toBe(0);
expect(await key.tracker.status("root")).toBeUndefined();
await key.tracker.dispose();
const oauth = fixture(
{
type: "oauth",
access: "opaque",
refresh: "refresh",
metadata: { accountID: "account-one" },
},
[
plus,
{
...plus,
rate_limit: {
...plus.rate_limit,
primary_window: { ...plus.rate_limit.primary_window, used_percent: 17.5 },
},
},
],
);
await oauth.tracker.init();
await oauth.tracker.start("root");
expect(oauth.calls()).toBe(1);
expect(oauth.headers[0]?.get("chatgpt-account-id")).toBe("account-one");
await oauth.tracker.refresh();
const child = await oauth.tracker.status("child");
expect(child?.windows[0]?.sessionConsumed).toBe(5);
expect(child?.windows[0]?.remaining).toBe(82.5);
await oauth.tracker.dispose();
});
test("polls, carries consumption across quota resets, tolerates errors, and clears on logout", async () => {
const reset = {
...plus,
rate_limit: {
...plus.rate_limit,
primary_window: {
...plus.rate_limit.primary_window,
used_percent: 2,
reset_at: 2_000_100_000,
},
},
};
const result = fixture(
{
type: "oauth",
access: "opaque",
refresh: "refresh",
metadata: { accountID: "account-one" },
},
[plus, new Error("offline"), reset, reset, reset, reset],
10,
);
await result.tracker.init();
await result.tracker.start("root");
await Bun.sleep(35);
const status = await result.tracker.status("root");
expect(result.calls()).toBeGreaterThanOrEqual(3);
expect(status?.windows[0]?.sessionConsumed).toBe(2);
result.logout();
await result.tracker.authChanged(true);
expect(await result.tracker.status("root")).toBeUndefined();
const calls = result.calls();
await result.tracker.dispose();
await Bun.sleep(20);
expect(result.calls()).toBe(calls);
});
test("sidebar text places estimated percentage-point use beside latest remaining quota", () => {
expect(
formatQuotaWindow({
seconds: 604_800,
id: "codex:secondary",
label: null,
used: 25,
remaining: 75,
sessionConsumed: 2.25,
resetsAt: 0,
}),
).toBe("7d: 75% left · ~2.3pp this session");
});
test("inactive roots freeze, resume from a cached baseline on failure, and deactivate by family", async () => {
const used = (percent: number) => ({
...plus,
rate_limit: {
...plus.rate_limit,
primary_window: { ...plus.rate_limit.primary_window, used_percent: percent },
},
});
const result = fixture(
{
type: "oauth",
access: "opaque",
refresh: "refresh",
metadata: { accountID: "account-one" },
},
[used(10), used(11), used(30), new Error("temporary"), used(30), used(33), used(35)],
);
await result.tracker.init();
await result.tracker.start("root");
await result.tracker.complete("root");
expect((await result.tracker.status("root"))?.windows[0]?.sessionConsumed).toBe(1);
await result.tracker.refresh();
expect((await result.tracker.status("root"))?.windows[0]?.sessionConsumed).toBe(1);
await result.tracker.start("root");
await result.tracker.start("child");
await result.tracker.complete("root");
await result.tracker.refresh();
await result.tracker.complete("child");
expect((await result.tracker.status("root"))?.windows[0]?.sessionConsumed).toBe(6);
await result.tracker.dispose();
});
test("an auth switch detaches an in-flight old-account response and immediately fetches the new account", async () => {
const pending = Promise.withResolvers<unknown>();
const second = {
...plus,
account_id: "account-two",
rate_limit: {
...plus.rate_limit,
primary_window: { ...plus.rate_limit.primary_window, used_percent: 70 },
},
};
const result = fixture(
{ type: "oauth", access: "old", refresh: "old", metadata: { accountID: "account-one" } },
[
pending.promise,
second,
{
...second,
rate_limit: {
...second.rate_limit,
primary_window: { ...second.rate_limit.primary_window, used_percent: 75 },
},
},
],
);
await result.tracker.init();
const old = result.tracker.start("root");
while (result.calls() === 0) await Bun.sleep(1);
result.switch({
type: "oauth",
access: "new",
refresh: "new",
metadata: { accountID: "account-two" },
});
await result.tracker.authChanged(true);
expect(result.calls()).toBe(2);
pending.resolve(plus);
await old;
expect((await result.tracker.status("root"))?.windows[0]?.used).toBe(70);
expect(result.headers[1]?.get("chatgpt-account-id")).toBe("account-two");
await result.tracker.refresh();
expect((await result.tracker.status("root"))?.windows[0]?.sessionConsumed).toBe(0);
await result.tracker.dispose();
});
test("an auth switch during an old account storage read cannot restore stale state or polling", async () => {
const pending = Promise.withResolvers<Schema.Json | undefined>();
const second = {
...plus,
account_id: "account-two",
rate_limit: {
...plus.rate_limit,
primary_window: { ...plus.rate_limit.primary_window, used_percent: 64 },
},
};
const result = fixture(
{ type: "oauth", access: "old", refresh: "old", metadata: { accountID: "account-one" } },
[second],
1_000_000,
[pending.promise],
);
const old = result.tracker.init();
while (result.storageCalls() === 0) await Bun.sleep(1);
result.switch({
type: "oauth",
access: "new",
refresh: "new",
metadata: { accountID: "account-two" },
});
await result.tracker.authChanged(true);
expect(result.calls()).toBe(1);
pending.resolve(undefined);
await old;
expect((await result.tracker.status("root"))?.windows[0]?.used).toBe(64);
expect(result.headers[0]?.get("chatgpt-account-id")).toBe("account-two");
await result.tracker.dispose();
});
test("TUI quota loading converts RPC failures to an empty render state", async () => {
expect(
await loadQuotaStatus(async () => {
throw new Error("disconnected");
}),
).toBeNull();
});
+108
View File
@@ -0,0 +1,108 @@
import { expect, test } from "bun:test";
import type { Plugin } from "@opencode/plugin/tui";
import { testRender, type JSX } from "@opentui/solid";
import { createSignal } from "solid-js";
import { build } from "../scripts/build.js";
import type { QuotaStatus } from "../src/quota.js";
type SlotRender = (input: { readonly sessionID: string }) => JSX.Element;
const status = (plan: string, remaining: number): QuotaStatus => ({
plan,
fetchedAt: 1,
windows: [
{
id: "codex:primary",
label: null,
seconds: 18_000,
used: 100 - remaining,
resetsAt: 2,
remaining,
sessionConsumed: 1.5,
},
],
});
test("built quota slot stays reactive across pending, empty, failure, update, and session states", async () => {
await build();
// Resolve the built bundle at runtime; tsc runs before build creates dist/tui.js.
const tui: typeof import("../src/tui.js") = await import(
new URL("../dist/tui.js", import.meta.url).href
);
const { default: plugin } = tui;
const pending = Promise.withResolvers<QuotaStatus | null>();
const responses: Array<() => Promise<QuotaStatus | null>> = [() => pending.promise];
const sessions: string[] = [];
let slotRender: SlotRender | undefined;
let notifyUpdated: (() => void) | undefined;
const quota = {
status(input: { sessionID: string }) {
sessions.push(input.sessionID);
const response = responses.shift();
if (!response) throw new Error("Missing test response");
return response();
},
events: {
on(_event: "updated", callback: () => void) {
notifyUpdated = callback;
return () => undefined;
},
},
};
const contextFixture = {
client: { rpc: () => quota },
ui: {
slot(claim: { render: SlotRender }) {
slotRender = claim.render;
return () => undefined;
},
},
theme: { text: { default: "#ffffff", muted: "#888888" } },
};
// SAFETY: The slot setup only reads the client RPC, slot registration, and theme
// members implemented by this focused rendering fixture.
const context = Object.assign({} as Plugin.Context, contextFixture);
const cleanup = await plugin.setup(context);
if (!slotRender || !notifyUpdated) throw new Error("Plugin did not register its slot");
const [sessionID, setSessionID] = createSignal("session-one");
const renderSlot = slotRender;
const setup = await testRender(
() =>
renderSlot({
get sessionID() {
return sessionID();
},
}),
{ width: 70, height: 5 },
);
try {
await setup.renderOnce();
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
pending.resolve(status("plus", 75));
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (plus)"));
expect(setup.captureCharFrame()).toContain("5h: 75% left · ~1.5pp this session");
responses.push(async () => null);
notifyUpdated();
await setup.waitForFrame((frame) => !frame.includes("ChatGPT quota"));
responses.push(async () => {
throw new Error("disconnected");
});
notifyUpdated();
await setup.waitFor(() => sessions.length === 3);
expect(setup.captureCharFrame()).not.toContain("ChatGPT quota");
responses.push(async () => status("pro", 42));
setSessionID("session-two");
await setup.waitForFrame((frame) => frame.includes("ChatGPT quota (pro)"));
expect(sessions).toEqual(["session-one", "session-one", "session-one", "session-two"]);
} finally {
setup.renderer.destroy();
if (cleanup) await cleanup();
}
});
+1 -1
View File
@@ -3,5 +3,5 @@
"compilerOptions": {
"rootDir": "src"
},
"include": ["src/**/*.ts"]
"include": ["src/**/*.ts", "src/**/*.tsx"]
}
+3 -1
View File
@@ -12,10 +12,12 @@
"noUnusedLocals": true,
"noUnusedParameters": true,
"verbatimModuleSyntax": true,
"jsx": "preserve",
"jsxImportSource": "@opentui/solid",
"skipLibCheck": true,
"declaration": true,
"rootDir": ".",
"outDir": "dist"
},
"include": ["src/**/*.ts", "tests/**/*.ts"]
"include": ["src/**/*.ts", "src/**/*.tsx", "scripts/**/*.ts", "tests/**/*.ts"]
}