Compare commits
14
Commits
feat/gpt6
...
9d74def38a
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9d74def38a | ||
|
|
f6c69c7db0 | ||
|
|
0c7ac2b532 | ||
|
|
d54c9b7b06 | ||
|
|
dba6524ed1 | ||
|
|
74f2fa8f46 | ||
|
|
f527778b29 | ||
|
|
0b6f39b6d9 | ||
|
|
1fdc558ff7 | ||
|
|
5175a93c47 | ||
|
|
7da94c9223 | ||
|
|
0e99dfbaec | ||
|
|
d132060697 | ||
|
|
4f956effff |
@@ -16,6 +16,7 @@
|
||||
".roo/**",
|
||||
".windsurf/**",
|
||||
"tools/oxlint/anti-slop/**",
|
||||
"src/anti-overengineering.md",
|
||||
"node_modules/**",
|
||||
"dist/**",
|
||||
"coverage/**",
|
||||
|
||||
@@ -0,0 +1,3 @@
|
||||
# CI-safe tests and scripts
|
||||
|
||||
Do not assume local directories, files, tools, or generated artifacts exist in a clean CI environment. In particular, `/tmp/opencode` is an agent-environment convenience, not a CI guarantee. Tests should create temporary directories with `node:os` `tmpdir()` and `mkdtemp(join(tmpdir(), prefix))`, explicitly create required parent directories and fixtures, and clean up the files they create.
|
||||
@@ -1,71 +1,81 @@
|
||||
# stanp-opencode
|
||||
|
||||
stanp-opencode is an OpenCode V2 coordinator with specialist workers for fast, good, and deep tasks.
|
||||
## Release installation
|
||||
|
||||
## Install the latest release
|
||||
|
||||
Requires OpenCode 2.0.4, Bun 1.4.2+, Bash, curl, jq, and `sha256sum` on Linux or WSL.
|
||||
|
||||
Run the installer without sudo:
|
||||
Requires OpenCode 2.0.22, Bun 1.4.2+, Bash, curl, jq, and `sha256sum` on Linux/WSL.
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash
|
||||
# Upgrading the old monolith resets quota and invalidates its Multi-work bindings.
|
||||
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash -s -- --plugins agents,multi-work,quota
|
||||
opencode service restart
|
||||
# Connect OpenAI and select coordinate in OpenCode.
|
||||
```
|
||||
|
||||
Pass `--playwright` to let the plugin automatically configure a Playwright MCP server:
|
||||
Choose any plugin subset; Multi-work requires Agents first.
|
||||
|
||||
Quota shows ChatGPT limits for the connected OpenAI account and, when Claude Code is logged in, Claude subscription limits read from `${CLAUDE_CONFIG_DIR:-~/.claude}/.credentials.json` (read-only; Claude Code refreshes the token).
|
||||
|
||||
## Local installation
|
||||
|
||||
From a local checkout, run:
|
||||
|
||||
```sh
|
||||
curl -fsSL https://git.krtss.de/StanPonomarev/stanp-opencode/raw/branch/main/install.sh | bash -s -- --playwright
|
||||
bash /path/to/stanp-opencode/install-local.sh --plugins agents,multi-work,quota
|
||||
opencode service restart
|
||||
# Connect OpenAI and select coordinate in OpenCode.
|
||||
```
|
||||
|
||||
Rerun the command to upgrade to the latest published release.
|
||||
## GPT configuration
|
||||
|
||||
## Install from a local checkout
|
||||
Merge this into `~/.config/opencode/opencode.json`, preserving unrelated settings and plugins; update existing plugin entries instead of appending duplicates.
|
||||
|
||||
This requires Bash, jq, and Bun 1.4.2+:
|
||||
|
||||
```sh
|
||||
bash /path/to/stanp-opencode/install-local.sh
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
// Required at the root and in worktrees.
|
||||
"experimental": { "subagent_depth": 2 },
|
||||
"plugins": [
|
||||
{
|
||||
"package": "./plugins/stanp-opencode/agents",
|
||||
"options": {
|
||||
"mode": "gpt",
|
||||
"provider": "openai",
|
||||
// Priority may cost more; set false to opt out.
|
||||
"lunaPriority": true,
|
||||
},
|
||||
},
|
||||
{ "package": "./plugins/stanp-opencode/multi-work" },
|
||||
{ "package": "./plugins/stanp-opencode/quota" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
Add `--playwright` to this command to enable automatic Playwright MCP configuration.
|
||||
## Custom model configuration
|
||||
|
||||
The script installs dependencies and builds the checkout before installing it.
|
||||
Merge this into `~/.config/opencode/opencode.json`, preserving unrelated settings and plugins; update existing plugin entries instead of appending duplicates. The `deep-lo` and `deep-hi` assignments are optional as a pair; if either is omitted, both deep workers are disabled and orchestration uses `good-lo`.
|
||||
|
||||
After either installation, restart `opencode` or run `opencode service restart` for an existing service. Connect OpenAI and select `coordinate`. The required models must be available for the workers to run.
|
||||
|
||||
Both installers use `~/.config/opencode`, respect `XDG_CONFIG_HOME`, and accept `OPENCODE_CONFIG_DIR` as an override.
|
||||
|
||||
## Configuration
|
||||
|
||||
See [GPT configuration](examples/gpt.opencode.jsonc) and [custom model assignments](examples/custom.opencode.jsonc).
|
||||
|
||||
For an installed plugin, use `./plugins/stanp-opencode` as the plugin package path. In GPT mode with the `openai` provider, set top-level `lunaPriority` to `false` to avoid requesting priority pricing. Other providers do not accept this option. Custom top-level and per-agent `provider` and `lunaPriority` fields are invalid.
|
||||
Custom configurations require the six worker assignments and use `provider/model` model names.
|
||||
The legacy optional `coordinate` assignment is accepted for compatibility but ignored. Orchestration
|
||||
inherits `deep-lo`, or `good-lo` when the configured `deep-lo` model is unavailable.
|
||||
|
||||
## Multi-work
|
||||
|
||||
Select `multi-work` in a verified live Herdr root checkout to delegate local retrieval
|
||||
to `fast-lo` and independent work to `coordinate` children in either the current root
|
||||
checkout or new Herdr worktrees.
|
||||
It is primary-only and, like `coordinate`, uses the `deep-lo` model and effort,
|
||||
falling back to `good-lo` when `deep-lo` is unavailable. Clients viewing the
|
||||
same eligible location can see it; eligibility is revalidated before creation.
|
||||
|
||||
Configure `experimental.subagent_depth: 2` at the root and worktree locations.
|
||||
For new coordinate tasks, omit `worktree` to share the current root checkout, or pass
|
||||
`worktree: { base: "source-branch" }` for an isolated checkout. The tool retains native
|
||||
session/job handling. See [usage, permissions,
|
||||
failure handling, and verification limits](docs/multi-work.md).
|
||||
|
||||
## Playwright
|
||||
|
||||
When installed with `--playwright`, the plugin prefers a local Playwright MCP installation. If ordinary Playwright is installed, it can download and run the MCP package with `npx`; otherwise Docker is required for the automatic fallback. Automatically configured servers allow five minutes for startup because the first launch may need to download a package or container image. Docker reuses its cached image on later launches. Existing `playwright` configuration, including its timeout, is preserved.
|
||||
|
||||
If `opencode mcp list` reports `MCP error -32001: Request timed out`, prefetch the Docker fallback with `docker pull mcr.microsoft.com/playwright/mcp`, or install `@playwright/mcp` in the project so the plugin can launch it directly. Then reconnect the server from `/mcps` or restart OpenCode. A custom `playwright` entry under `mcp.servers` can also set its own `timeout.startup` in milliseconds.
|
||||
|
||||
## Uninstall
|
||||
|
||||
Remove `plugins/stanp-opencode` from the selected configuration directory and remove its configuration entry, then restart OpenCode.
|
||||
```jsonc
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
// Required at the root and in worktrees.
|
||||
"experimental": { "subagent_depth": 2 },
|
||||
"plugins": [
|
||||
{
|
||||
"package": "./plugins/stanp-opencode/agents",
|
||||
"options": {
|
||||
"mode": "custom",
|
||||
"agents": {
|
||||
"fast-lo": { "model": "openai/gpt-6-luna", "effort": "low" },
|
||||
"fast-hi": { "model": "openai/gpt-6-luna", "effort": "high" },
|
||||
"good-lo": { "model": "openai/gpt-6.1-sol", "effort": "low" },
|
||||
"good-hi": { "model": "openai/gpt-6.1-sol", "effort": "high" },
|
||||
"deep-lo": { "model": "openai/gpt-6-astra", "effort": "low" },
|
||||
"deep-hi": { "model": "openai/gpt-6-astra", "effort": "high" },
|
||||
},
|
||||
},
|
||||
},
|
||||
{ "package": "./plugins/stanp-opencode/multi-work" },
|
||||
{ "package": "./plugins/stanp-opencode/quota" },
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
@@ -9,22 +9,22 @@
|
||||
"zod": "4.6.5",
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode/client": "2.0.4",
|
||||
"@opencode/plugin": "2.0.4",
|
||||
"@opencode/schema": "2.0.4",
|
||||
"@opentui/core": "0.5.11",
|
||||
"@opentui/solid": "0.5.11",
|
||||
"@oxlint/plugins": "1.83.0",
|
||||
"@opencode/client": "2.0.22",
|
||||
"@opencode/plugin": "2.0.22",
|
||||
"@opencode/schema": "2.0.22",
|
||||
"@opentui/core": "0.5.14",
|
||||
"@opentui/solid": "0.5.14",
|
||||
"@oxlint/plugins": "1.86.0",
|
||||
"@types/bun": "1.4.2",
|
||||
"oxfmt": "0.68.0",
|
||||
"oxlint": "1.83.0",
|
||||
"oxfmt": "0.71.0",
|
||||
"oxlint": "1.86.0",
|
||||
"solid-js": "1.9.15",
|
||||
"typescript": "7.0.2",
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode/plugin": "2.0.4",
|
||||
"@opentui/core": ">=0.5.11",
|
||||
"@opentui/solid": ">=0.5.11",
|
||||
"@opencode/plugin": "2.0.22",
|
||||
"@opentui/core": ">=0.5.14",
|
||||
"@opentui/solid": ">=0.5.14",
|
||||
"solid-js": ">=1.9.0",
|
||||
},
|
||||
},
|
||||
@@ -202,162 +202,144 @@
|
||||
|
||||
"@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@2.0.4", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.0.4", "@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-T/2wTuhy/I1Wxdy/GdhtMqugpl1OWKgVp+8A45mtS1jDrTSeHq2x5YhkComkJZ6BwdG/OvU6afTGDPA7/vd4RA=="],
|
||||
"@opencode/ai": ["@opencode/ai@2.0.22", "", { "dependencies": { "@aws-sdk/credential-providers": "3.1057.0", "@opencode/schema": "2.0.22", "@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-wdD2ut4G/areDkA9bYizYiho6C1sRQ6mPqMSji93XzadavbkIcbhU/VyodOmExtZtOg2IjheJCUVCpDXQ8n8FA=="],
|
||||
|
||||
"@opencode/client": ["@opencode/client@2.0.4", "", { "dependencies": { "@opencode/protocol": "2.0.4", "@opencode/schema": "2.0.4" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-GBFWT2os+gEtGXpHRvDUNkf0H9x9njDUVbUvvc4drH2CuAihVcHqdi9Wm4UiwkBrSLjrKPs4U1NJBh2Z7/x4tQ=="],
|
||||
"@opencode/client": ["@opencode/client@2.0.22", "", { "dependencies": { "@opencode/protocol": "2.0.22", "@opencode/schema": "2.0.22" }, "peerDependencies": { "effect": "4.0.0-rc.112", "solid-js": ">=1.9.0" }, "optionalPeers": ["effect", "solid-js"] }, "sha512-FJKugH301QHlV/rGL8TvgL3ly+toVlww+TP+YdVTKQWytQvjLoUdYtvvm+Iq5k6pN6Ki3ybE/08E8Cg+e6Opbg=="],
|
||||
|
||||
"@opencode/plugin": ["@opencode/plugin@2.0.4", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.0.4", "@opencode/client": "2.0.4", "@opencode/protocol": "2.0.4", "@opencode/schema": "2.0.4", "@opencode/util": "2.0.4", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode/theme": "2.0.4", "@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-NsC1STARfXxe+ZoXjXzpxvtS9l4h9Aen8ZccaCd1d9+1eyYdp+q8DU3Rxh7Ch0jiwiQMybS2Z+KgAfZJBnTBbA=="],
|
||||
"@opencode/plugin": ["@opencode/plugin@2.0.22", "", { "dependencies": { "@ai-sdk/provider": "3.0.8", "@opencode/ai": "2.0.22", "@opencode/client": "2.0.22", "@opencode/protocol": "2.0.22", "@opencode/schema": "2.0.22", "@opencode/util": "2.0.22", "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112", "zod": "4.1.8" }, "peerDependencies": { "@opencode/theme": "2.0.22", "@opentui/core": ">=0.5.14", "@opentui/solid": ">=0.5.14", "solid-js": ">=1.9.0" }, "optionalPeers": ["@opencode/theme", "@opentui/core", "@opentui/solid", "solid-js"] }, "sha512-EJyr+qA+I41rfJqE2zfyKL2qNez+wE8QkNwXf5TddEKIPmL8FUvYMVT5lLhbnXyOGTDxsEc0/8bX7ErQfX5/Rg=="],
|
||||
|
||||
"@opencode/protocol": ["@opencode/protocol@2.0.4", "", { "dependencies": { "@opencode/schema": "2.0.4", "effect": "4.0.0-rc.112" } }, "sha512-uWe2dmMouprSkfBu29XLR6GCWckkhm0YOo9ve07kTRqJCSoJzgQDF/8pouWG0cDCJ6oWj3bRQajXriBQyWSpUw=="],
|
||||
"@opencode/protocol": ["@opencode/protocol@2.0.22", "", { "dependencies": { "@opencode/schema": "2.0.22", "effect": "4.0.0-rc.112" } }, "sha512-Ws3DKQW/rlBDwNz2cPE9+nGwbw7pgnZ+5bzgoHWCWxgTZmxdFulmLP05lxR+PPwMklUwzOGoUXFIzODPZ27WfA=="],
|
||||
|
||||
"@opencode/schema": ["@opencode/schema@2.0.4", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-UyGcCsRb2hTH98rnBqfJogFve8kJsIbsEreRxqhkBIEGrtDTqcUd5NnqB7qlJp2khesMvpOH0GmCHlgz44C+uQ=="],
|
||||
"@opencode/schema": ["@opencode/schema@2.0.22", "", { "dependencies": { "@standard-schema/spec": "1.1.0", "effect": "4.0.0-rc.112" } }, "sha512-PTqTrx3QHhlRL5ypfoMnSzXDU8XB2+vtKX8QKwIHP7B1F1yw90LN0XTmGP82k9rYoEcERq0TLuL+ykAsYJXJdA=="],
|
||||
|
||||
"@opencode/util": ["@opencode/util@2.0.4", "", { "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-jo6zhWqJ4WaU+fUjM8/Ea4i06gxTQFEFgfq2XG9J8gSDUhfsj4l9wJ3cLtSuKFQGeUeWqJJwRgDhCW+rUbis6Q=="],
|
||||
"@opencode/util": ["@opencode/util@2.0.22", "", { "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/api-logs": "0.219.0", "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/exporter-trace-otlp-http": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0", "@opentelemetry/sdk-trace-node": "2.8.0", "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", "open": "11.0.4", "pacote": "21.5.1" } }, "sha512-nipMu7ZAM9gCOKwH06eqAUVgATR+5XbqXEKSN+ECYLgm9PStsmxraxDN93gJLV3bYzpR+9Ow0Be/s0ZYKUxPKg=="],
|
||||
|
||||
"@opentelemetry/api": ["@opentelemetry/api@1.9.0", "", {}, "sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg=="],
|
||||
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-40lSJeqYO8Uz2Yj7u94/SJWE/wONa7rmMKjI1ZcIjgf3MHNHv1OZUCrCETGuaRF62d5pQD1wKIW+L4lmSMTzZA=="],
|
||||
"@opentelemetry/api-logs": ["@opentelemetry/api-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-FFx7YnaYJlIjqWW/AG/yAZ0L/NEY724PipXXXQLdtZPbLwBGbUMTGL1i/esI56TWfTUXxhLfpgrnWJCG8aUJyg=="],
|
||||
|
||||
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.6.1", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-XHzhwRNkBpeP8Fs/qjGrAf9r9PRv67wkJQ/7ZPaBQQ68DYlTBBx5MF9LvPx7mhuXcDessKK2b+DcxqwpgkcivQ=="],
|
||||
"@opentelemetry/context-async-hooks": ["@opentelemetry/context-async-hooks@2.8.0", "", { "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-/3FIraneMcng67SUJCxvyInk/oxzwsxyadufk0wwfOBLf5wqtAGX4MoQASwSbndBPeARzBryUM9Azr5kHIdWLw=="],
|
||||
|
||||
"@opentelemetry/core": ["@opentelemetry/core@2.6.1", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-8xHSGWpJP9wBxgBpnqGL0R3PbdWQndL1Qp50qrg71+B28zK5OQmUgcDKLJgzyAAV38t4tOyLMGDD60LneR5W8g=="],
|
||||
"@opentelemetry/core": ["@opentelemetry/core@2.8.0", "", { "dependencies": { "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-hd1Lfh8p545nNz+jq1Ejfz+Mn1hyLuxYn1YzTfFNrxr8urEWMNQLPf1Th8kjOH+HxwawCrtgBp8JpBUR4ZSgww=="],
|
||||
|
||||
"@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-exporter-base": "0.214.0", "@opentelemetry/otlp-transformer": "0.214.0", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-kIN8nTBMgV2hXzV/a20BCFilPZdAIMYYJGSgfMMRm/Xa+07y5hRDS2Vm12A/z8Cdu3Sq++ZvJfElokX2rkgGgw=="],
|
||||
"@opentelemetry/exporter-trace-otlp-http": ["@opentelemetry/exporter-trace-otlp-http@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-exporter-base": "0.219.0", "@opentelemetry/otlp-transformer": "0.219.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-9t6SvBXXBEjOBcIzgozvBbd3jWrv3Gt3ngGhl1fhdZ/zRc7oZDVOFEqbi2zlBpW9BXhgDMKv422J0DL/3iQWfw=="],
|
||||
|
||||
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.214.0", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/otlp-transformer": "0.214.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-u1Gdv0/E9wP+apqWf7Wv2npXmgJtxsW2XL0TEv9FZloTZRuMBKmu8cYVXwS4Hm3q/f/3FuCnPTgiwYvIqRSpRg=="],
|
||||
"@opentelemetry/otlp-exporter-base": ["@opentelemetry/otlp-exporter-base@0.219.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/otlp-transformer": "0.219.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-zvIxQX/AZUVKDU+hCuYx+7UkiP7GRdnk1ZbFQRYzHvYp47cAWR4j3IhoPhV9KaeXEv2xdGq3IA6PnpzDmLcmSA=="],
|
||||
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/sdk-logs": "0.214.0", "@opentelemetry/sdk-metrics": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1", "protobufjs": "^7.0.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-DSaYcuBRh6uozfsWN3R8HsN0yDhCuWP7tOFdkUOVaWD1KVJg8m4qiLUsg/tNhTLS9HUYUcwNpwL2eroLtsZZ/w=="],
|
||||
"@opentelemetry/otlp-transformer": ["@opentelemetry/otlp-transformer@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/sdk-logs": "0.219.0", "@opentelemetry/sdk-metrics": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" } }, "sha512-aaYKAyXhw9VchKZVGOopD3Gw/kPsyrX2c6IQ0AW32mTjqmZOh5Y6Gf5OYqTNqVktAeBjmFinhyFaCwW6GYK9YQ=="],
|
||||
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-lID/vxSuKWXM55XhAKNoYXu9Cutoq5hFdkbTdI/zDKQktXzcWBVhNsOkiZFTMU9UtEWuGRNe0HUgmsFldIdxVA=="],
|
||||
"@opentelemetry/resources": ["@opentelemetry/resources@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-qmXQ27ilDbUK/vGMqwL8D4/rhn76C+sherM4wTbjlfknR8Nvfc/hCxjRJPhkzZzUsPiNg16SA31NxMabwttRjg=="],
|
||||
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.214.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.214.0", "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-zf6acnScjhsaBUU22zXZ/sLWim1dfhUAbGXdMmHmNG3LfBnQ3DKsOCITb2IZwoUsNNMTogqFKBnlIPPftUgGwA=="],
|
||||
"@opentelemetry/sdk-logs": ["@opentelemetry/sdk-logs@0.219.0", "", { "dependencies": { "@opentelemetry/api-logs": "0.219.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.4.0 <1.10.0" } }, "sha512-s6lTKRakaPClvKoWHRChxnXjDMkM/TQ30ff78jN6EBGf7MI7VzANE5PU3f4z9qDUudWjvZjOLHG0rBnBKYvoXA=="],
|
||||
|
||||
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-9t9hJHX15meBy2NmTJxL+NJfXmnausR2xUDvE19XQce0Qi/GBtDGamU8nS1RMbdgDmhgpm3VaOu2+fiS/SfTpQ=="],
|
||||
"@opentelemetry/sdk-metrics": ["@opentelemetry/sdk-metrics@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.9.0 <1.10.0" } }, "sha512-UDBGaj6W0Rgy5rTTaoxs8gVGF/aGkAKyjurJv7se6wjRxJu7FoquTLT/vt54DZfo4crbprYfhX/SOK9+BPw1qg=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.6.1", "", { "dependencies": { "@opentelemetry/core": "2.6.1", "@opentelemetry/resources": "2.6.1", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-r86ut4T1e8vNwB35CqCcKd45yzqH6/6Wzvpk2/cZB8PsPLlZFTvrh8yfOS3CYZYcUmAx4hHTZJ8AO8Dj8nrdhw=="],
|
||||
"@opentelemetry/sdk-trace-base": ["@opentelemetry/sdk-trace-base@2.8.0", "", { "dependencies": { "@opentelemetry/core": "2.8.0", "@opentelemetry/resources": "2.8.0", "@opentelemetry/semantic-conventions": "^1.29.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.3.0 <1.10.0" } }, "sha512-mhU4jp+vW0mGbFRd+GeXHvmfA4aDqWjBjLC3pE5XMpLs0IE2ryYb019Ts2AQrOq67gaTF25D91+fgvEHDZEnuQ=="],
|
||||
|
||||
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.6.1", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.6.1", "@opentelemetry/core": "2.6.1", "@opentelemetry/sdk-trace-base": "2.6.1" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-Hh2i4FwHWRFhnO2Q/p6svMxy8MPsNCG0uuzUY3glqm0rwM0nQvbTO1dXSp9OqQoTKXcQzaz9q1f65fsurmOhNw=="],
|
||||
"@opentelemetry/sdk-trace-node": ["@opentelemetry/sdk-trace-node@2.8.0", "", { "dependencies": { "@opentelemetry/context-async-hooks": "2.8.0", "@opentelemetry/core": "2.8.0", "@opentelemetry/sdk-trace-base": "2.8.0" }, "peerDependencies": { "@opentelemetry/api": ">=1.0.0 <1.10.0" } }, "sha512-nZt9OGufioAc3AfoLTqA9bsAeaMJAictYDdI2VcNQ+PmT+3rfKjAZDZvgPfd8VPX0O5Bw1hdQF6kDK8VSpZiWg=="],
|
||||
|
||||
"@opentelemetry/semantic-conventions": ["@opentelemetry/semantic-conventions@1.43.0", "", {}, "sha512-eSYWTm620tTk45EKSedaUL8MFYI8hW164hIXsgIHyxu3VobUB3fFCu5t0hQby6OoWRPsG1KkKUG2M5UadiLiVg=="],
|
||||
|
||||
"@opentui/core": ["@opentui/core@0.5.11", "", { "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.11", "@opentui/core-darwin-x64": "0.5.11", "@opentui/core-linux-arm64": "0.5.11", "@opentui/core-linux-arm64-musl": "0.5.11", "@opentui/core-linux-x64": "0.5.11", "@opentui/core-linux-x64-musl": "0.5.11", "@opentui/core-win32-arm64": "0.5.11", "@opentui/core-win32-x64": "0.5.11" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-pImMfjCNx7JUp9Df1LRZBDLisWqgzOLXZKXO+hh3jA9ujBFPQnZOqbG+/N5uVAAX8IZGaqhYSQRwSZIFBmcfbQ=="],
|
||||
"@opentui/core": ["@opentui/core@0.5.14", "", { "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.14", "@opentui/core-darwin-x64": "0.5.14", "@opentui/core-linux-arm64": "0.5.14", "@opentui/core-linux-arm64-musl": "0.5.14", "@opentui/core-linux-x64": "0.5.14", "@opentui/core-linux-x64-musl": "0.5.14", "@opentui/core-win32-arm64": "0.5.14", "@opentui/core-win32-x64": "0.5.14" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-tfQ+PWQyeBnYloB3diEcPqbILv14xemH5jjAEICfPuyNDtGBqrjhUtThrhbvFRuPMcj6IEeXrAk6VE8e91h0kg=="],
|
||||
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.11", "", { "os": "darwin", "cpu": "arm64" }, "sha512-DRXY5ioq+n1ZNAMAcaFaBunr0cmi2gqucjbTW7lgFp8t9uN3fNZnLTDOqkCTQtT2XhrU4GXqySaPSQ4qFG/EAQ=="],
|
||||
"@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.5.14", "", { "os": "darwin", "cpu": "arm64" }, "sha512-wWmw41wRMBoI0lN9mgyzGRwYujwwfNkBP6jYM99k4Bx2XkeSZ3sCeu5bwX9vEJPYqjiBShbDhc0NnVSP7NwzVg=="],
|
||||
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.11", "", { "os": "darwin", "cpu": "x64" }, "sha512-yP/8GliJDiJNm8YYJKvgWuy6xyCEd8d4GwBVOIzCFOI7ZIbGP8GOTvmwIjRW4Paw20pTttWMWyRoQiCvOXvH2g=="],
|
||||
"@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.5.14", "", { "os": "darwin", "cpu": "x64" }, "sha512-7smHKDH8IhUaBsgYuAClMl2mHWu+yjpMrtdw3wEvBVtCzKMrHi/TJ5PtydO+IfUzR3BXpVubdbR1irD8BTcR/w=="],
|
||||
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-zBIsRFHlLUYFNhapRSNt9dz4mC8gZ4Wxcfy3A+2AwqsgCipcr2FkIuAXYqN08q+IvqFX7DfqgIFGWDNedHTPUg=="],
|
||||
"@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-xH1hP+NaLySEJeZkl21NlkZBMddMfQ1jU8NeX1AEBc2GNBOvDXU4Ud/xw87SrAvU1xG9K7/9C4oy4AmIMEpVGg=="],
|
||||
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.11", "", { "os": "linux", "cpu": "arm64" }, "sha512-x+xeR2LYibvIi/qQetRjJR008sFRve60QuDcO8ItxUwzFeKTDzl5CEiZpBXfm5I4FhRNuyuj0TSPIFadMvrjFQ=="],
|
||||
"@opentui/core-linux-arm64-musl": ["@opentui/core-linux-arm64-musl@0.5.14", "", { "os": "linux", "cpu": "arm64" }, "sha512-mnBBAuTb92NiRLAjOD755tS8/tNQemDztbg9tMvoCT90G52FtVrRb31Ge6OrYqfm0c9DkZGhEBOhunsId/4zSA=="],
|
||||
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-pSOXqOADrv+zINOgR3FDFA9zVRaim3zl8/yhtO+X9rEJ6f34z3gDund0Gf88hNJSMpZK5xWtipEVm28RY5VF8w=="],
|
||||
"@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-Hkk4kaDGMcn9bmJFJPW3/QOGGbPuWe3sCFV/CkiVb4+bCvcTm7EXQr4QTAA63BYy2dKE5bUFUi1zZlyeMkWpnA=="],
|
||||
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.11", "", { "os": "linux", "cpu": "x64" }, "sha512-MyqOnSs8pTYG2xmFr1xt6xZIuHu2Xu4pkle9my9JdE+WClmusHf0YN9Eas6jQLAq5XUi34r22wMeK0juk93zyw=="],
|
||||
"@opentui/core-linux-x64-musl": ["@opentui/core-linux-x64-musl@0.5.14", "", { "os": "linux", "cpu": "x64" }, "sha512-ngJ+U2grOGEteeQvZdAJNqn09M+At5mfWInauK5aS427bea1yLo+e6hor/CRmbn9SxEEF+SwoyekaOrLPWyU7w=="],
|
||||
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.11", "", { "os": "win32", "cpu": "arm64" }, "sha512-MGGRXIDJ//HyaqC5ndSr7/CUl+ICdYEAMcjbA00UWsthh4ZO/rYhPkyqOaTn5ck6+G4ca3/+M76J+jImMYTjNg=="],
|
||||
"@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.5.14", "", { "os": "win32", "cpu": "arm64" }, "sha512-T9kNqKXg2jysmTsyyZ1A8LBQotFBM+iPjzyRslxyexqrs8a1UcmxbApEo+COtxQuqukMTbvwyqEdAT8vcmxEkQ=="],
|
||||
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.11", "", { "os": "win32", "cpu": "x64" }, "sha512-sMEGX9rhiPd1gBa190jzj5uIdzKCMImxHmr22hJLxIRWqejxWvqGnRBkW68wT8NfMViDiMtZWavgWL7GgmYCAQ=="],
|
||||
"@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.5.14", "", { "os": "win32", "cpu": "x64" }, "sha512-mqKSkab8VdMLSmMdocna7+BTyMoIutkVXOV9lfmPrBO2g7Np5c6c4SJ4QIVZqPvast11XyT0/7FfXOYLKAS72w=="],
|
||||
|
||||
"@opentui/solid": ["@opentui/solid@0.5.11", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.11", "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-u8RJ4UMwzi+r9M1sopZdnT+7XFSHmLzVmhPhc6N2LfBrA9Db4lZ9sQA+ywgdSDkzdANpAOwGLNUQlY/F1OOCLA=="],
|
||||
"@opentui/solid": ["@opentui/solid@0.5.14", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.5.14", "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-bBRl34mZ0wFGhjHX6y1VNiZmJ3DSm2DVPm0PNSVDpvq9qxRHuywEPTCn/Lgte0C9450tgf11cDqcE3pR9cZwVA=="],
|
||||
|
||||
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.68.0", "", { "os": "android", "cpu": "arm" }, "sha512-dhfYPbzv/h9JgHjNkl2R6sOjUfxDyLGOZVb3g8/ScaTNwwJcYgmHh8kcYFDUhinuy1QAoANCWUvw1jlk+z6gAg=="],
|
||||
"@oxfmt/binding-android-arm-eabi": ["@oxfmt/binding-android-arm-eabi@0.71.0", "", { "os": "android", "cpu": "arm" }, "sha512-l31EyBfB4egJeFua11aQFEUE9nnDtfpWCadqQovm0WL30t/yUHtdFl/5HyFXvd5xT8Ewh3U4TI9f30GowrQ1fw=="],
|
||||
|
||||
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.68.0", "", { "os": "android", "cpu": "arm64" }, "sha512-v3Njdi6qY0O/5eGfg01ww2w6gTn2mUvZ72Bnx1/UN53A9wruh3Nk6otc3WkgJLkXD4Qgz1SOcVQieH1oD03V9Q=="],
|
||||
"@oxfmt/binding-android-arm64": ["@oxfmt/binding-android-arm64@0.71.0", "", { "os": "android", "cpu": "arm64" }, "sha512-E8q39SUZzSXQ08oZoFoTkxRADkdBNgL6De4qgZYr2O4JCunwqrZwAyCebxIF3yVHBafHM30WUkhTkDi+jNYZOw=="],
|
||||
|
||||
"@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.68.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-ei4MCMzHFREmZwPJ7KuWUB4kBuHdsgDrnXGJVcEAopU7fj7S42I8BChdFILWdHvhFqR08FLJtOfbZIr2CDw0cA=="],
|
||||
"@oxfmt/binding-darwin-arm64": ["@oxfmt/binding-darwin-arm64@0.71.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-pTteTrN88DicrmJ3DocBmpNDa6Umfh0iveEG8rnAlDFQpclmtDS7hnIvZbIEV9mDO0Ot1U0sjtoJQ1itd/ZMhQ=="],
|
||||
|
||||
"@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.68.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-UrKgzZxYhwB9DSvTX+vdgl9M32wLUNKJcAKIoiyx/Kzn/zveqi7W6kYVcRroFMhS3Kwz0KhTk3WBeSuQn4YCTg=="],
|
||||
"@oxfmt/binding-darwin-x64": ["@oxfmt/binding-darwin-x64@0.71.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-G5melRC3IiNEgXlNKJhSExJYQGAQIppHFp2cl/wFrqQcEwmc4k4TkrHaFTR29SDudjoW5AZq5cXPGJDqtlH/Yg=="],
|
||||
|
||||
"@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.68.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-6jrEKgpJbilM1QaRv7hEtKXr4p4AK4jvvyOtajwyhu0kOz3e0O7OLnSTk6tBotRqCcUC4ehZRJ1Zx+Y99wieLw=="],
|
||||
"@oxfmt/binding-freebsd-x64": ["@oxfmt/binding-freebsd-x64@0.71.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-Hb1P5yKb9aiH2VSbm3Ml37mKjzqpunYet9krw/YK7V5JsAUJDQlju3Yk/dEqQvJD/usBvpi01Vbk9ZDNo0a8Cw=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.68.0", "", { "os": "linux", "cpu": "arm" }, "sha512-YOIVnKOBaLeGullskS179N12hjSAdFYnzLjOaKiLhAKNWgnShq9w4xRdtmUm6BlnP65l2/EA9Aw/KlftNxDM7Q=="],
|
||||
"@oxfmt/binding-linux-arm-gnueabihf": ["@oxfmt/binding-linux-arm-gnueabihf@0.71.0", "", { "os": "linux", "cpu": "arm" }, "sha512-r7n4JEO+rkp0kaP3ia+0hv6FzK3PQIHqaF0jxGuhjJ4JHVX9ZVRbgpDVDW+OloyuJsRWpWFDKIHe8gfl1p/4mQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.68.0", "", { "os": "linux", "cpu": "arm" }, "sha512-xW5XoEHVNqydPBv2KXvk9lmEzyAlOQHVEazKoXUuAacqekjya+OdiaFjjEBl0oJD02raG8g3TRl9OVCh9PDIHA=="],
|
||||
"@oxfmt/binding-linux-arm-musleabihf": ["@oxfmt/binding-linux-arm-musleabihf@0.71.0", "", { "os": "linux", "cpu": "arm" }, "sha512-EEynTF4nakgqDhVCmd3cHQCmTcw0BM8ea6eHigTdtZ3yxdsBNv+dYkt+OpDDZx1n/jzjI7J+Ht7IgkqFWuJUxQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.68.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-QCvYwVVQieu6oyJglAgV9vH/YMDxZyR4cwVSYtoq9oOXd5N+D3TDUBjNwxFrLn5AdcJZOcvn/7IB17vJGp+2Og=="],
|
||||
"@oxfmt/binding-linux-arm64-gnu": ["@oxfmt/binding-linux-arm64-gnu@0.71.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7VgJIrywCwR/G6YMv+HqsccUt8Z4q4mbM2xR+Ry/6AeMk4pCmZ9aO4Srd3mmrvVu8PsCZGP7oHEHSXWTmyloaQ=="],
|
||||
|
||||
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.68.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-4TVz5iFQ8ndrHnhX50UXiz9BIWAtUSOHJ6Nus4qWFfJBXq/Ed/krXbF/ehJu42BXM5tIvBs99jNIM22s9agY5A=="],
|
||||
"@oxfmt/binding-linux-arm64-musl": ["@oxfmt/binding-linux-arm64-musl@0.71.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-AOCaminv/+fhinUXKvtPZT4POhNYmN9GfvaJdgJa1AbvuzClWGwDIXfJg8eu3sYaVL2B68jNvmS7LZGAYa0z3A=="],
|
||||
|
||||
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.68.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-qLe3ao0RP84bnPxBvRI+GnlK/jybo538NWu0Xrm+zYeTmJtpzqLhnnd5BH21NafqKnplbGZjtN1cnrOlWF73lw=="],
|
||||
"@oxfmt/binding-linux-ppc64-gnu": ["@oxfmt/binding-linux-ppc64-gnu@0.71.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-0SHxuaa4QRLM7jfsd8wc6L7KdLg+FR6/ah9k1eAvcrK/Sni7oxchMvsjwUsyakVAta+wBVDkjJTr7/RFU0uUGA=="],
|
||||
|
||||
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.68.0", "", { "os": "linux", "cpu": "none" }, "sha512-Yvyl7a6gbb0vM6r925KW2dO+/CmXySO5TVbcX7o/uZJ+d108HFOG0TxIyHApmn5USuj5mhDPxzhiVwOlGP7uSA=="],
|
||||
"@oxfmt/binding-linux-riscv64-gnu": ["@oxfmt/binding-linux-riscv64-gnu@0.71.0", "", { "os": "linux", "cpu": "none" }, "sha512-UNKj1+rPorTK6uSlQzpN96vDNbHbL/63zcpznweo0ImQdkDdtQCS7G3puxX++2cESPKT2B56Y6DICUmFTJde5A=="],
|
||||
|
||||
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.68.0", "", { "os": "linux", "cpu": "none" }, "sha512-mJlFuFVCxzrYM5sFStN433D/s/mb6Wq4aCQAM02vs/OudHywnaSAd2rb1vlYUJtqdYIciJtiasuxvfbYkv5fLg=="],
|
||||
"@oxfmt/binding-linux-riscv64-musl": ["@oxfmt/binding-linux-riscv64-musl@0.71.0", "", { "os": "linux", "cpu": "none" }, "sha512-5cVXKj7a0mWu7GU6/bZgNyv+KtLgM5S9cnT26Os2/abj8pK/bklHsbdYEHGLLniGjNYkyzwem8mZciPCaDtilw=="],
|
||||
|
||||
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.68.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-RlfSg++qs1hbKltRR6lYvV9EoI3MdlfSQD9w1hdHVYjHqjIn1tkH4FWOpMSmjKGN20zr+nI+W9o4ARogCDudGQ=="],
|
||||
"@oxfmt/binding-linux-s390x-gnu": ["@oxfmt/binding-linux-s390x-gnu@0.71.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-lNpcMuZvyU1V/CY5h19hac03m4E4PLSYRckcxZOynSPojI4Mwt6nEu9wT/gVf0bclH3xGEWjwqApnSTgjkd2oA=="],
|
||||
|
||||
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.68.0", "", { "os": "linux", "cpu": "x64" }, "sha512-nyzRB9U+dlYUKu3pMo3afHzZBUv/oTHZMG36ZfJViNVfOIzp70Q4GS8FFRGgYJ/p0zcyDCgpBvYISOdJOMh+jQ=="],
|
||||
"@oxfmt/binding-linux-x64-gnu": ["@oxfmt/binding-linux-x64-gnu@0.71.0", "", { "os": "linux", "cpu": "x64" }, "sha512-5/Z6pUewQpknXqC4/ykK6Zc6RiteAnPem1Ci7K1RZLVF6w6MMjwHjR4vsjijW4Czidgv7HKeVglGjElADliT9w=="],
|
||||
|
||||
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.68.0", "", { "os": "linux", "cpu": "x64" }, "sha512-iCx3sbZRIvGrL1RafphEiUKBaW1lc0/tAjKOIB/Wjw2+STRBEdu5+fH1Gc1faEWEmc2k5Ks4iUUV54Zd5C9a1A=="],
|
||||
"@oxfmt/binding-linux-x64-musl": ["@oxfmt/binding-linux-x64-musl@0.71.0", "", { "os": "linux", "cpu": "x64" }, "sha512-uVdG2N/4GEbOeljpQ+xv+NeEwJWJGj0WaxSiSYnoiqIYy3RWrWd3rGUmxWXP1A8+ferNvvwFoDAtvgsDUvBuSw=="],
|
||||
|
||||
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.68.0", "", { "os": "none", "cpu": "arm64" }, "sha512-x2X5AZez7OgyLLFpwIgItoXBUqudDM7yiaTsxv8R8vKQ6e81l0jVw0NFeUCXzcl1sAJq8h+tC8N4mY8EiMeL4w=="],
|
||||
"@oxfmt/binding-openharmony-arm64": ["@oxfmt/binding-openharmony-arm64@0.71.0", "", { "os": "none", "cpu": "arm64" }, "sha512-1LwEZmSDRFKeBaGd/Hgj2xI3SmgGFpF4YtgkSx0sTYdTiZfHWbcFvRIBjL1YiDNdQd9Khda3Ij8CoqBULjskDg=="],
|
||||
|
||||
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.68.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-AHVPjXkenLPQUh6kB8zSC8pX2ct9r4T1Edk9r/RNJyov6wPsS5uAfYtipwG4chn6+3bPFG5rI/3DxEeH8vib1w=="],
|
||||
"@oxfmt/binding-win32-arm64-msvc": ["@oxfmt/binding-win32-arm64-msvc@0.71.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-5tplmkeMXmza3PUptJQy25wCPJbIAg5g5r7zs+DI+7eEYOqbY+7roMdSH2zbDvv1Pb4oTDJ6NWYx9IRxBxXwhQ=="],
|
||||
|
||||
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.68.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-n09SjEk5VH7z8Hl4WVP7hho+cCwGViENkQFiM45vbW85dJd7kEhWaHRUobaPzEmrWyu6uumd4EuNfNyDKLtzDA=="],
|
||||
"@oxfmt/binding-win32-ia32-msvc": ["@oxfmt/binding-win32-ia32-msvc@0.71.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-T2LWgh3Vx4PYDjtcqRRGDyCsVxeF/VE5xTpUQiCm65bNLwMpRnJYj6Hlkf6qRM3+jxCIOqHD1kVHNQCCiAVMNQ=="],
|
||||
|
||||
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.68.0", "", { "os": "win32", "cpu": "x64" }, "sha512-gPe+dJLXaPuWPWqlpklDAJp0k+K9KhQPYiQLHfb+i2rmFuUGfJ/5Qlj6tr1mO6of5g0DiLjG/XCFHIaPhotqqA=="],
|
||||
"@oxfmt/binding-win32-x64-msvc": ["@oxfmt/binding-win32-x64-msvc@0.71.0", "", { "os": "win32", "cpu": "x64" }, "sha512-L06SVp7Hu1EQTkh3+PHnKK26dxVbEpOG2InMwCooTE4MA437b5FgfTleCCiGn4ykISHnHU6UHaIg7SiMO7rBHA=="],
|
||||
|
||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.83.0", "", { "os": "android", "cpu": "arm" }, "sha512-0yGY24EwsLk5YDe6F+VkmZyRHSwJDALa3nIrPpq7FXmp2lV2d0TzvBCGeZk+wgiULRGr5blhyr4QMp5KCXJUqA=="],
|
||||
"@oxlint/binding-android-arm-eabi": ["@oxlint/binding-android-arm-eabi@1.86.0", "", { "os": "android", "cpu": "arm" }, "sha512-63Ozq6yQn79B2UxjSZ9huJmI+3/ZPI7hxAe6TyFh9oQL447KnIMoffOdtog+h4KILbQr61ZRvFPW5gYbAUgpgQ=="],
|
||||
|
||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.83.0", "", { "os": "android", "cpu": "arm64" }, "sha512-hHfJ0vc17A4iUjH5p9BsTUPYbYRNxGpvD2lbu1aBRk54bzNIx9o5TtYF39QPZcV95DagZd+4DEAw2RH3G2ZsMg=="],
|
||||
"@oxlint/binding-android-arm64": ["@oxlint/binding-android-arm64@1.86.0", "", { "os": "android", "cpu": "arm64" }, "sha512-JdLGp44phbf/0Du236Dt06yfK6Qw8xhTFeDvHJdjk167r59OfP5+BL31mgMYVnswS8e+j6tFuccSY0QDC/lgCw=="],
|
||||
|
||||
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.83.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-hsOjYjszLb/3zym/TkzUMPAoQlTJcuzSyEPOAyA+skXJIX9M0o+4JfOtqopX/Vf4hSLrJ98j0nvFo23gzk8auQ=="],
|
||||
"@oxlint/binding-darwin-arm64": ["@oxlint/binding-darwin-arm64@1.86.0", "", { "os": "darwin", "cpu": "arm64" }, "sha512-h+vkOr4ik6KLFCXdtNVEj9xfnXfNUpPoF95QjORdhiSpLqQePzcN81kGHjAgsw1/G7WBWK5KsFKbI2UK8EyzMA=="],
|
||||
|
||||
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.83.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-mjh5oH2EA+wl5yRJYT9K9G61O2zFlpuv+yf2JwZOi0+dq2FnTUtm1h8i+5Ik0fXPWIu/k84I1psZR9aQsLAnyA=="],
|
||||
"@oxlint/binding-darwin-x64": ["@oxlint/binding-darwin-x64@1.86.0", "", { "os": "darwin", "cpu": "x64" }, "sha512-wbglRuH5eKsp68keTOOeKDHU1astWSRYFsh2bwKQTAPb44rD6kVgzYI1ZrAJRTc6/7PH55ZXFCQx0rQXrj0UdA=="],
|
||||
|
||||
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.83.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-fNHr64/YaO8YssuoDVC8+F4Uk5enR86q5uxfHkQrjAPs1dbAILOrD2uaud+J7MO8Fx774g44ERLD0IGIvZE48w=="],
|
||||
"@oxlint/binding-freebsd-x64": ["@oxlint/binding-freebsd-x64@1.86.0", "", { "os": "freebsd", "cpu": "x64" }, "sha512-yTQ7QSOlNfQsrfbeVweTiegQJLKPjUKgXp/JpGiFzza6Hp29S9LAoH77TM3L0RjK39iQRZB5lk5iq2t241Mfag=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.83.0", "", { "os": "linux", "cpu": "arm" }, "sha512-Qpwy3zzAwMj+8/lyYItHmkSMwbkprFNWTK7jPYDOxSyxEhaSLOWYUTCMkjF334J8/WD0nznCCsoBbIH6hpsuIw=="],
|
||||
"@oxlint/binding-linux-arm-gnueabihf": ["@oxlint/binding-linux-arm-gnueabihf@1.86.0", "", { "os": "linux", "cpu": "arm" }, "sha512-QYJGy0E2Tv8GAayyBIeHFTwQb8u+zrvrOYrifuDGZDr9ad5XsDhiMScpZymhS9fwWjqrgG2ZUwT4vpY0ISqp4Q=="],
|
||||
|
||||
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.83.0", "", { "os": "linux", "cpu": "arm" }, "sha512-s+BirYLFq7JL2k9sP0XI3ZXJ9dYvJ8sX3jLCLoag7tt+zrSHpZxP0jqznfL+Gdgwu7ay0dYgGYJXrQvq3iWloA=="],
|
||||
"@oxlint/binding-linux-arm-musleabihf": ["@oxlint/binding-linux-arm-musleabihf@1.86.0", "", { "os": "linux", "cpu": "arm" }, "sha512-oQo83CafLT8p3qh0AGkN0tSUIh5Hm63mfDsoqUmBQUBnSZLNBAbyRaAftfGY5a0cKZ34dG6jAtMYgcY5kZqt5g=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.83.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-7lihXt3vKr+GIyapNbHrnFHm/biiW30le6Zv/DExbAFPF6YwCQXVFlONPFehxs0CpGO4CBfYPM9rdDT+XMoIlg=="],
|
||||
"@oxlint/binding-linux-arm64-gnu": ["@oxlint/binding-linux-arm64-gnu@1.86.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-EM6wy5c2UM12qPb7iDJDBAZvHyfEJGH6iosFgaFiaRZpirz99//EHcUyD2KAskz07x1TULuiAtsojf4nvWWksw=="],
|
||||
|
||||
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.83.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-q63JalLYVkZiZvls1z3PPUnpmQluOMXp0khqQMznCeAPLGydfNY8JhvuA4WlK57JfrvikU8wB5lPVveqpIXvew=="],
|
||||
"@oxlint/binding-linux-arm64-musl": ["@oxlint/binding-linux-arm64-musl@1.86.0", "", { "os": "linux", "cpu": "arm64" }, "sha512-vCiUQb9ZNzZalxYRU4mlZwUqT5f+c7Sk9DwUOplWHtik3Dl4r3DVnUB5tBeRuTAUe6MUVCTylNW7TkkN8J1oTw=="],
|
||||
|
||||
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.83.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-krQmDF+dRbxvdqVPV88ZuOoPPu8X5BuqDA8Hd+qcS4YMRQCb+nexA57DazgGsc/rGdKBe3QmV0mnv0bdpW/p5g=="],
|
||||
"@oxlint/binding-linux-ppc64-gnu": ["@oxlint/binding-linux-ppc64-gnu@1.86.0", "", { "os": "linux", "cpu": "ppc64" }, "sha512-T0AzE89yjTYmOhB/hb6i+rWTpe2mYt+xiguBhs9DEPIJTZ1H8OYqsE4VxJHHw2Wlq8Mztwvp2P8NV87ZOuhPRw=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.83.0", "", { "os": "linux", "cpu": "none" }, "sha512-MmOl8Y6txEAXZU1RG8Rr264jQ6D7VPmqFsU/45x/FeWsGe32hklTqGrLE6UxHzp5Rjt0wP+20tY8YXKgSFB3mw=="],
|
||||
"@oxlint/binding-linux-riscv64-gnu": ["@oxlint/binding-linux-riscv64-gnu@1.86.0", "", { "os": "linux", "cpu": "none" }, "sha512-s49r0K2f5Oc7fJoMUO1WkdhYK5Ob79uYRwWQ/XNoGysaOq3RUGmGUbHSuXiyydTTA+P9xEdDEnnlZVXiaSJhlw=="],
|
||||
|
||||
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.83.0", "", { "os": "linux", "cpu": "none" }, "sha512-u1rMymh0W3JZkq370kzQsYPULGWqhE09pZRqnZvUSoYaI9pVO5yVX+iYIslmWuEgwuzH9YAaOsScJiobWCHoOw=="],
|
||||
"@oxlint/binding-linux-riscv64-musl": ["@oxlint/binding-linux-riscv64-musl@1.86.0", "", { "os": "linux", "cpu": "none" }, "sha512-JIa2tYR8ayb3nvcMSZzn4O/xTy13S7MHllu7cBD+B5qDQXXSRy/Vwk1aNpiTjZmBJGwKCHOy/NvnY5Xcwg27uQ=="],
|
||||
|
||||
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.83.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-y0zK3HNwGysu7rqtE+BQG/d0bx5gh/KwlOtghN8oWeK1KcWzeaLqtZrbm8owqdma1lFyrce/hTO5ismuNu+INQ=="],
|
||||
"@oxlint/binding-linux-s390x-gnu": ["@oxlint/binding-linux-s390x-gnu@1.86.0", "", { "os": "linux", "cpu": "s390x" }, "sha512-XwXMyBsAuWZkhZU74UNcfQKr1CJJAKnS80dZzOnjl6A4fKcf91IjFgidegQugMHA5SN8ulwDXEDV1yPntjXgQQ=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.83.0", "", { "os": "linux", "cpu": "x64" }, "sha512-rS5gM0NgD7ngmuJmbIehsidtrOwKkLFwCQbKEeb9KuyQrrWNq5Zkn0uV6AYdXOMJ0grrWEiLwBuvMxt8w5vsNw=="],
|
||||
"@oxlint/binding-linux-x64-gnu": ["@oxlint/binding-linux-x64-gnu@1.86.0", "", { "os": "linux", "cpu": "x64" }, "sha512-C1WjukSyMnr66b+w1/tV8RFVv6d9v0MzDf4p9IxVXknqgmTHBgZh1pccN1eHzFr0b9Tbb3OXoPsAAdAuHAQfeA=="],
|
||||
|
||||
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.83.0", "", { "os": "linux", "cpu": "x64" }, "sha512-W2IH4EtpcPaWcvNGCA95YoDg4vxqE/ZiPCi3arrxEEpsK7+JQN9WYwrlYFx9pcdP6KPXqRqkv3zdQPHcx7b6YQ=="],
|
||||
"@oxlint/binding-linux-x64-musl": ["@oxlint/binding-linux-x64-musl@1.86.0", "", { "os": "linux", "cpu": "x64" }, "sha512-ap6KLmvC38c6MdYzsIh25cXQupqYvjd37tMNftzrX1DCtkX1Gcf2B+S2B17dD4lKa5c5gJog7DQJyDo82PBKyw=="],
|
||||
|
||||
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.83.0", "", { "os": "none", "cpu": "arm64" }, "sha512-6LyKkUyoajssTPLlZmDbZIbu4IZ5B4bGuRUnBgCGpEvHP3FQMaYITncHA/unPUo7q+Z+pIu2HhdkQ+8d1SG7iA=="],
|
||||
"@oxlint/binding-openharmony-arm64": ["@oxlint/binding-openharmony-arm64@1.86.0", "", { "os": "none", "cpu": "arm64" }, "sha512-996Q9w/2GLwNyy5Djs39Jt09aFlr4b7TPhoj7dvo5FHmcyiF68nPz73HNc5UAYKIYeK+M9JzPQ4Wg1r1dXvP6A=="],
|
||||
|
||||
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.83.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-Uz/fObEtF0jmNJQJ8CGRBKfefYstS0/wjD3s6IGzP8nUwsJykHQJBiN3npHwKiGRGn/vvBEgNr4B3cCzmmatvg=="],
|
||||
"@oxlint/binding-win32-arm64-msvc": ["@oxlint/binding-win32-arm64-msvc@1.86.0", "", { "os": "win32", "cpu": "arm64" }, "sha512-9uTCHYTknNkug5+46ViSoUMuOuGWK61D87QnblU6CXaWKJC630Z7dbls61vdHxya/cR2ycQZ6Mm6ckgF6Cag5g=="],
|
||||
|
||||
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.83.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-u7XcvPW6Bk58tY5iWs2ESb0vJjoE/kuSpHxopbwp/p3ZtWVQXZ6wor5w3ssVTHOqd/v8b+QdhSFWQ4grEUNWpA=="],
|
||||
"@oxlint/binding-win32-ia32-msvc": ["@oxlint/binding-win32-ia32-msvc@1.86.0", "", { "os": "win32", "cpu": "ia32" }, "sha512-a47ga7EKVfMU8rO0i1m3mGwfNRj3FhQyt1d/ukO/E9u1HHF3KpR3tyb3VhuJBGEVPF8uR4exSyHUyx7xbE+uxw=="],
|
||||
|
||||
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.83.0", "", { "os": "win32", "cpu": "x64" }, "sha512-LZRubd7ph13QmAg4fFecTYVZkiYbROR2Htaxh/ufWRkDhPOm2wrwaEYR89e0YpPFD3dqBrPoxS7myBw5hmYA7Q=="],
|
||||
"@oxlint/binding-win32-x64-msvc": ["@oxlint/binding-win32-x64-msvc@1.86.0", "", { "os": "win32", "cpu": "x64" }, "sha512-az4dzifCRzES1crwu6pDFXzJTIanrdxXFL9TbiJWrx9LKcuacbR83aPvxDA1LKLNswi/RoqitC7Afcflhu3zBQ=="],
|
||||
|
||||
"@oxlint/plugins": ["@oxlint/plugins@1.83.0", "", {}, "sha512-qD64wr6SS1DL7YbJQ8WWjyUfFfrJsvAy9Vo0sG/PYUQy30w0uemN2h6j4xuvj4noyETDpzcGC20sv+FH7GrN5A=="],
|
||||
"@oxlint/plugins": ["@oxlint/plugins@1.86.0", "", {}, "sha512-JeAiaaRMyUiUEQHQ7zBsJn1aEdbLyt1HJsOs4XKIIlATNxcvWRrRxneZA+RF4G03cQobq3IhTqSfyLhOFv03qA=="],
|
||||
|
||||
"@pkgjs/parseargs": ["@pkgjs/parseargs@0.11.0", "", {}, "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg=="],
|
||||
|
||||
"@protobufjs/aspromise": ["@protobufjs/aspromise@1.1.2", "", {}, "sha512-j+gKExEuLmKwvz3OgROXtrJ2UG2x8Ch2YZUxahh+s1F2HZ+wAceUNLkvy6zKCPVRkU++ZWQrdxsUeQXmcg4uoQ=="],
|
||||
|
||||
"@protobufjs/base64": ["@protobufjs/base64@1.1.2", "", {}, "sha512-AZkcAA5vnN/v4PDqKyMR5lx7hZttPDgClv83E//FMNhR2TMcLUhfRUBHCmSl0oi9zMgDDqRUJkSxO3wm85+XLg=="],
|
||||
|
||||
"@protobufjs/codegen": ["@protobufjs/codegen@2.0.5", "", {}, "sha512-zgXFLzW3Ap33e6d0Wlj4MGIm6Ce8O89n/apUaGNB/jx+hw+ruWEp7EwGUshdLKVRCxZW12fp9r40E1mQrf/34g=="],
|
||||
|
||||
"@protobufjs/eventemitter": ["@protobufjs/eventemitter@1.1.1", "", {}, "sha512-vW1GmwMZNnL+gMRaovlh9yZX74kc+TTU3FObkkurpMaRtBfLP3ldjS9KQWlwZgraRE0+dheEEoAxdzcJQ8eXZg=="],
|
||||
|
||||
"@protobufjs/fetch": ["@protobufjs/fetch@1.1.1", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.1" } }, "sha512-GpptLrs57adMSuHi3VNj0mAF8dwh36LMaYF6XyJ6JMWlVsc+t42tm1HSEDmOs3A8fC9yyeisgLhsTVQokOZ0zw=="],
|
||||
|
||||
"@protobufjs/float": ["@protobufjs/float@1.0.2", "", {}, "sha512-Ddb+kVXlXst9d+R9PfTIxh1EdNkgoRe5tOX6t01f1lYWOvJnSPDBlG241QLzcyPdoNTsblLUdujGSE4RzrTZGQ=="],
|
||||
|
||||
"@protobufjs/path": ["@protobufjs/path@1.1.2", "", {}, "sha512-6JOcJ5Tm08dOHAbdR3GrvP+yUUfkjG5ePsHYczMFLq3ZmMkAD98cDgcT2iA1lJ9NVwFd4tH/iSSoe44YWkltEA=="],
|
||||
|
||||
"@protobufjs/pool": ["@protobufjs/pool@1.1.0", "", {}, "sha512-0kELaGSIDBKvcgS4zkjz1PeddatrjYcmMWOlAuAPwAeccUrPHdUqo/J6LiymHHEiJT5NrF1UVwxY14f+fy4WQw=="],
|
||||
|
||||
"@protobufjs/utf8": ["@protobufjs/utf8@1.1.2", "", {}, "sha512-b1UQwcEZ4yCnMCD8DAL1VlbvBJE9/IX4FTIp7BG1xYpf29SLazLSrqUkj4w7Y5y7cCVP6E5tcqqcI0xemPkHug=="],
|
||||
|
||||
"@redis/bloom": ["@redis/bloom@6.2.1", "", { "peerDependencies": { "@redis/client": "^6.2.1" } }, "sha512-huQgNLaCIZfQ9SeLn4q9124uOUd8HbZDYHwwUzNcRgHqCHiHKl2dDxMqJCeWh8cMqZAoWuHR8XnWbDMIf+o7ag=="],
|
||||
|
||||
"@redis/client": ["@redis/client@6.2.1", "", { "dependencies": { "cluster-key-slot": "1.1.2" }, "peerDependencies": { "@node-rs/xxhash": "^1.1.0", "@opentelemetry/api": ">=1 <2" }, "optionalPeers": ["@node-rs/xxhash", "@opentelemetry/api"] }, "sha512-LzxBY7SIBvvJiyCgcaJZZakE3fJrZZ++i24+EDW9fKpCl68D35uJcKFpZZwCfOoG9WZTbyZlMzMeM0gtOAMU9Q=="],
|
||||
@@ -492,6 +474,8 @@
|
||||
|
||||
"bun-types": ["bun-types@1.4.2", "", { "dependencies": { "@types/node": "*" } }, "sha512-bxV1FgK7yBIzjRe5zBozIM4Bem11ZJcCXSrjWRG3YWLt8yFDePu4cLjpebO8OvPeIE9trbyPF4fuj3Cia4Fj3w=="],
|
||||
|
||||
"bundle-name": ["bundle-name@4.1.1", "", { "dependencies": { "run-applescript": "^7.0.0" } }, "sha512-DdH81/zPLVS11EUgWq3tEu/xn+EzljlMYooDNdzWEnFha3R3NBMpMV1UqYIpjYHV/SgpFKMIX1Oh7o06SjM/oA=="],
|
||||
|
||||
"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=="],
|
||||
@@ -524,6 +508,12 @@
|
||||
|
||||
"debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" }, "peerDependencies": { "supports-color": "*" }, "optionalPeers": ["supports-color"] }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="],
|
||||
|
||||
"default-browser": ["default-browser@5.5.1", "", { "dependencies": { "bundle-name": "^4.1.0", "default-browser-id": "^5.0.0" } }, "sha512-m1pAzaJgZ/gssEqlOhJkPJp8Xly7QyW6xcrkUa2KKcDeDSEMP7X8xipU3snUcfisTQx0w1AGae+9UtJSfVnXGw=="],
|
||||
|
||||
"default-browser-id": ["default-browser-id@5.0.1", "", {}, "sha512-x1VCxdX4t+8wVfd1so/9w+vQ4vx7lKd2Qp5tDRutErwmR85OgmfX7RlLRMWafRMY7hbEiXIbudNrjOAPa/hL8Q=="],
|
||||
|
||||
"define-lazy-prop": ["define-lazy-prop@3.0.0", "", {}, "sha512-N+MeXYoqr3pOgn8xfyRPREN7gHakLYjhsHhWGT3fWAiL4IkAt0iDw14QiiEm2bE30c5XX5q0FtAA3CK5f9/BUg=="],
|
||||
|
||||
"detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="],
|
||||
|
||||
"diff": ["diff@9.0.0", "", {}, "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw=="],
|
||||
@@ -610,8 +600,16 @@
|
||||
|
||||
"is-core-module": ["is-core-module@2.16.2", "", { "dependencies": { "hasown": "^2.0.3" } }, "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA=="],
|
||||
|
||||
"is-docker": ["is-docker@3.0.0", "", { "bin": { "is-docker": "cli.js" } }, "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ=="],
|
||||
|
||||
"is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="],
|
||||
|
||||
"is-in-ssh": ["is-in-ssh@1.0.0", "", {}, "sha512-jYa6Q9rH90kR1vKB6NM7qqd1mge3Fx4Dhw5TVlK1MUBqhEOuCagrEHMevNuCcbECmXZ0ThXkRm+Ymr51HwEPAw=="],
|
||||
|
||||
"is-inside-container": ["is-inside-container@1.0.0", "", { "dependencies": { "is-docker": "^3.0.0" }, "bin": { "is-inside-container": "cli.js" } }, "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA=="],
|
||||
|
||||
"is-wsl": ["is-wsl@3.1.1", "", { "dependencies": { "is-inside-container": "^1.0.0" } }, "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw=="],
|
||||
|
||||
"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=="],
|
||||
@@ -642,8 +640,6 @@
|
||||
|
||||
"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=="],
|
||||
@@ -706,9 +702,11 @@
|
||||
|
||||
"npm-registry-fetch": ["npm-registry-fetch@19.1.1", "", { "dependencies": { "@npmcli/redact": "^4.0.0", "jsonparse": "^1.3.1", "make-fetch-happen": "^15.0.0", "minipass": "^7.0.2", "minipass-fetch": "^5.0.0", "minizlib": "^3.0.1", "npm-package-arg": "^13.0.0", "proc-log": "^6.0.0" } }, "sha512-TakBap6OM1w0H73VZVDf44iFXsOS3h+L4wVMXmbWOQroZgFhMch0juN6XSzBNlD965yIKvWg2dfu7NSiaYLxtw=="],
|
||||
|
||||
"oxfmt": ["oxfmt@0.68.0", "", { "dependencies": { "tinypool": "2.1.2" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.68.0", "@oxfmt/binding-android-arm64": "0.68.0", "@oxfmt/binding-darwin-arm64": "0.68.0", "@oxfmt/binding-darwin-x64": "0.68.0", "@oxfmt/binding-freebsd-x64": "0.68.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.68.0", "@oxfmt/binding-linux-arm-musleabihf": "0.68.0", "@oxfmt/binding-linux-arm64-gnu": "0.68.0", "@oxfmt/binding-linux-arm64-musl": "0.68.0", "@oxfmt/binding-linux-ppc64-gnu": "0.68.0", "@oxfmt/binding-linux-riscv64-gnu": "0.68.0", "@oxfmt/binding-linux-riscv64-musl": "0.68.0", "@oxfmt/binding-linux-s390x-gnu": "0.68.0", "@oxfmt/binding-linux-x64-gnu": "0.68.0", "@oxfmt/binding-linux-x64-musl": "0.68.0", "@oxfmt/binding-openharmony-arm64": "0.68.0", "@oxfmt/binding-win32-arm64-msvc": "0.68.0", "@oxfmt/binding-win32-ia32-msvc": "0.68.0", "@oxfmt/binding-win32-x64-msvc": "0.68.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-Z0XMofcXCGUXbcpBHnWyUiX93BGiw1B+lcHNbQDWEtOhX06ewoFfu4zXkyiLhRrNnMq0twqXRHUcJetf+GsiQQ=="],
|
||||
"open": ["open@11.0.4", "", { "dependencies": { "default-browser": "^5.5.1", "define-lazy-prop": "^3.0.0", "is-in-ssh": "^1.0.0", "is-inside-container": "^1.0.0", "powershell-utils": "^0.2.1", "wsl-utils": "^1.0.0" } }, "sha512-++Zlftm0kVLPmzC06t6epuWmcRMDbI4z5P3NNX979WA/k23+NtSOynEGzsVfZwguKw2mi5umVgnBlJQMwRz4Pg=="],
|
||||
|
||||
"oxlint": ["oxlint@1.83.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.83.0", "@oxlint/binding-android-arm64": "1.83.0", "@oxlint/binding-darwin-arm64": "1.83.0", "@oxlint/binding-darwin-x64": "1.83.0", "@oxlint/binding-freebsd-x64": "1.83.0", "@oxlint/binding-linux-arm-gnueabihf": "1.83.0", "@oxlint/binding-linux-arm-musleabihf": "1.83.0", "@oxlint/binding-linux-arm64-gnu": "1.83.0", "@oxlint/binding-linux-arm64-musl": "1.83.0", "@oxlint/binding-linux-ppc64-gnu": "1.83.0", "@oxlint/binding-linux-riscv64-gnu": "1.83.0", "@oxlint/binding-linux-riscv64-musl": "1.83.0", "@oxlint/binding-linux-s390x-gnu": "1.83.0", "@oxlint/binding-linux-x64-gnu": "1.83.0", "@oxlint/binding-linux-x64-musl": "1.83.0", "@oxlint/binding-openharmony-arm64": "1.83.0", "@oxlint/binding-win32-arm64-msvc": "1.83.0", "@oxlint/binding-win32-ia32-msvc": "1.83.0", "@oxlint/binding-win32-x64-msvc": "1.83.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2001", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-cyDzSzaw3uzP0TeCeq3lLRPPoaUxkbB4ZOXj+kn+5r+BX9V+4bNVGk9lxer+WrgcpebH4JxLlJ3KQjveVztOLQ=="],
|
||||
"oxfmt": ["oxfmt@0.71.0", "", { "dependencies": { "tinypool": "2.2.0" }, "optionalDependencies": { "@oxfmt/binding-android-arm-eabi": "0.71.0", "@oxfmt/binding-android-arm64": "0.71.0", "@oxfmt/binding-darwin-arm64": "0.71.0", "@oxfmt/binding-darwin-x64": "0.71.0", "@oxfmt/binding-freebsd-x64": "0.71.0", "@oxfmt/binding-linux-arm-gnueabihf": "0.71.0", "@oxfmt/binding-linux-arm-musleabihf": "0.71.0", "@oxfmt/binding-linux-arm64-gnu": "0.71.0", "@oxfmt/binding-linux-arm64-musl": "0.71.0", "@oxfmt/binding-linux-ppc64-gnu": "0.71.0", "@oxfmt/binding-linux-riscv64-gnu": "0.71.0", "@oxfmt/binding-linux-riscv64-musl": "0.71.0", "@oxfmt/binding-linux-s390x-gnu": "0.71.0", "@oxfmt/binding-linux-x64-gnu": "0.71.0", "@oxfmt/binding-linux-x64-musl": "0.71.0", "@oxfmt/binding-openharmony-arm64": "0.71.0", "@oxfmt/binding-win32-arm64-msvc": "0.71.0", "@oxfmt/binding-win32-ia32-msvc": "0.71.0", "@oxfmt/binding-win32-x64-msvc": "0.71.0" }, "peerDependencies": { "svelte": "^5.0.0", "vite-plus": "*" }, "optionalPeers": ["svelte", "vite-plus"], "bin": { "oxfmt": "bin/oxfmt" } }, "sha512-lUPUl0d/+Io5pDrsPXWs6rB4N/bpB78oj9CTDpnbulfDz+0r3XXcHPlQ7kRPJ2GjIT4nX+/mcqunOeP9BvsEtg=="],
|
||||
|
||||
"oxlint": ["oxlint@1.86.0", "", { "optionalDependencies": { "@oxlint/binding-android-arm-eabi": "1.86.0", "@oxlint/binding-android-arm64": "1.86.0", "@oxlint/binding-darwin-arm64": "1.86.0", "@oxlint/binding-darwin-x64": "1.86.0", "@oxlint/binding-freebsd-x64": "1.86.0", "@oxlint/binding-linux-arm-gnueabihf": "1.86.0", "@oxlint/binding-linux-arm-musleabihf": "1.86.0", "@oxlint/binding-linux-arm64-gnu": "1.86.0", "@oxlint/binding-linux-arm64-musl": "1.86.0", "@oxlint/binding-linux-ppc64-gnu": "1.86.0", "@oxlint/binding-linux-riscv64-gnu": "1.86.0", "@oxlint/binding-linux-riscv64-musl": "1.86.0", "@oxlint/binding-linux-s390x-gnu": "1.86.0", "@oxlint/binding-linux-x64-gnu": "1.86.0", "@oxlint/binding-linux-x64-musl": "1.86.0", "@oxlint/binding-openharmony-arm64": "1.86.0", "@oxlint/binding-win32-arm64-msvc": "1.86.0", "@oxlint/binding-win32-ia32-msvc": "1.86.0", "@oxlint/binding-win32-x64-msvc": "1.86.0" }, "peerDependencies": { "oxlint-tsgolint": ">=7.0.2003", "vite-plus": "*" }, "optionalPeers": ["oxlint-tsgolint", "vite-plus"], "bin": { "oxlint": "bin/oxlint" } }, "sha512-og0lhgvZfgGF//gOOmZXvtr+GmBbAGEnbEhv/QUg7UW2Wi4wHMnJbnMD+zuHgPdJxdklfgpPupdlAaAySyxrZg=="],
|
||||
|
||||
"p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="],
|
||||
|
||||
@@ -742,6 +740,8 @@
|
||||
|
||||
"postcss-selector-parser": ["postcss-selector-parser@7.1.6", "", { "dependencies": { "cssesc": "^3.0.0", "util-deprecate": "^1.0.2" } }, "sha512-7qASPzhKF2l2KLboRZux8CCTRMdGiV08vWmyKzPz22qZ7ZjQBOeY7rNzNoCLSUiftJ7HUq0GERHmxw/t0dCdMw=="],
|
||||
|
||||
"powershell-utils": ["powershell-utils@0.2.1", "", {}, "sha512-C+y9x90UElAddDZmV4qOx9W53B61PO7cIqWz2dQsWlwswuq4mr8NEwytdGKboYbQlGZ3awrkTeNvcZiZNHnQ8A=="],
|
||||
|
||||
"proc-log": ["proc-log@6.1.0", "", {}, "sha512-iG+GYldRf2BQ0UDUAd6JQ/RwzaQy6mXmsk/IzlYyal4A4SNFw54MeH4/tLkF4I5WoWG9SQwuqWzS99jaFQHBuQ=="],
|
||||
|
||||
"proggy": ["proggy@4.0.0", "", {}, "sha512-MbA4R+WQT76ZBm/5JUpV9yqcJt92175+Y0Bodg3HgiXzrmKu7Ggq+bpn6y6wHH+gN9NcyKn3yg1+d47VaKwNAQ=="],
|
||||
@@ -750,8 +750,6 @@
|
||||
|
||||
"promise-call-limit": ["promise-call-limit@3.0.2", "", {}, "sha512-mRPQO2T1QQVw11E7+UdCJu7S61eJVWknzml9sC1heAdj1jxl0fWMBypIt9ZOcLFf8FkG995ZD7RnVk7HH72fZw=="],
|
||||
|
||||
"protobufjs": ["protobufjs@7.6.6", "", { "dependencies": { "@protobufjs/aspromise": "^1.1.2", "@protobufjs/base64": "^1.1.2", "@protobufjs/codegen": "^2.0.5", "@protobufjs/eventemitter": "^1.1.1", "@protobufjs/fetch": "^1.1.1", "@protobufjs/float": "^1.0.2", "@protobufjs/path": "^1.1.2", "@protobufjs/pool": "^1.1.0", "@protobufjs/utf8": "^1.1.1", "@types/node": ">=13.7.0", "long": "^5.3.2" } }, "sha512-dYDWdjSl5RNb7SgPxGQcRU+GtvP7s2fpkrY0r432PcOIaZ0/rBcxEZnQN67iJhFuQiVw754JDoPruPCNdGsbjg=="],
|
||||
|
||||
"pure-rand": ["pure-rand@8.4.2", "", {}, "sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng=="],
|
||||
|
||||
"read-cmd-shim": ["read-cmd-shim@6.0.0", "", {}, "sha512-1zM5HuOfagXCBWMN83fuFI/x+T/UhZ7k+KIzhrHXcQoeX5+7gmaDYjELQHmmzIodumBHeByBJT4QYS7ufAgs7A=="],
|
||||
@@ -764,6 +762,8 @@
|
||||
|
||||
"rimraf": ["rimraf@5.0.10", "", { "dependencies": { "glob": "^10.3.7" }, "bin": { "rimraf": "dist/esm/bin.mjs" } }, "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ=="],
|
||||
|
||||
"run-applescript": ["run-applescript@7.1.0", "", {}, "sha512-DPe5pVFaAsinSaV6QjQ6gdiedWDcRCbUuiQfQa2wmWV7+xC9bGulGI8+TdRmoFkAPaBXk8CrAbnlY2ISniJ47Q=="],
|
||||
|
||||
"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=="],
|
||||
@@ -814,7 +814,7 @@
|
||||
|
||||
"tinyglobby": ["tinyglobby@0.2.17", "", { "dependencies": { "fdir": "^6.5.0", "picomatch": "^4.0.4" } }, "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g=="],
|
||||
|
||||
"tinypool": ["tinypool@2.1.2", "", {}, "sha512-9YodfrxS9g9IbFr/KOjE5bAeJ0p61n3bW6mqvy0jtoeKd1kTW1Cxm0oulm6KX2lyM9Gl6WIe8nEbY7LWv5ZJww=="],
|
||||
"tinypool": ["tinypool@2.2.0", "", {}, "sha512-jBrmx4lYmaC9k/mgPbylxs7kBUxHtD8256up+HjLaDFfXScKJQyil+SWXSvhAtT7XHo+yTVlpB81PGmHP8oLSQ=="],
|
||||
|
||||
"treeverse": ["treeverse@3.0.0", "", {}, "sha512-gcANaAnd2QDZFmHFEOF4k7uc1J/6a6z3DJMd/QwEyxLoKGiptJRwid582r7QIsFlFMIZ3SnxfS52S4hm2DHkuQ=="],
|
||||
|
||||
@@ -850,6 +850,8 @@
|
||||
|
||||
"ws": ["ws@8.21.3", "", { "peerDependencies": { "bufferutil": "^4.0.1", "utf-8-validate": ">=5.0.2" }, "optionalPeers": ["bufferutil", "utf-8-validate"] }, "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw=="],
|
||||
|
||||
"wsl-utils": ["wsl-utils@1.0.1", "", { "dependencies": { "is-wsl": "^3.1.0", "powershell-utils": "^0.2.0" } }, "sha512-c16PKLIRxBYpR3jSWsdS7SFp0COm5SOQdiFjY818dYFHJbMGuLBszpF07LBWmOS1uzrHa9VGdynsEgoeKy0oOQ=="],
|
||||
|
||||
"yallist": ["yallist@5.0.0", "", {}, "sha512-YgvUTfwqyc7UXVMrB+SImsVYSmTS8X/tSrtdNZMImM+n7+QTriRXyXim0mBrTXNeqzVF0KWGgHPeiyViFFrNDw=="],
|
||||
|
||||
"zod": ["zod@4.6.5", "", {}, "sha512-v5l/aFXZQeai4awLbOpSoHecE9UiMrnfx75tEXLjNonXVARxQ5mOeipTjROUchszUNCqnE+hqAMujRsRHsut2Q=="],
|
||||
|
||||
+23
-14
@@ -3,7 +3,7 @@
|
||||
`multi-work` is a **primary-only** agent for coordinating independent work in
|
||||
the current Herdr root checkout or new Herdr-managed Git worktrees. Like
|
||||
`coordinate`, it uses the configured `deep-lo` model and effort, falling back
|
||||
to `good-lo` when `deep-lo` is unavailable. No extra custom model assignment is
|
||||
to `good-lo` when `deep-lo` is unavailable or either custom deep assignment is omitted. No extra custom model assignment is
|
||||
required.
|
||||
|
||||
```text
|
||||
@@ -35,8 +35,16 @@ creation. Creating/opening the root workspace remains a user action.
|
||||
|
||||
## Configuration
|
||||
|
||||
Use OpenCode **2.0.4**, the version this integration targets. At both the root and
|
||||
destination locations, configure:
|
||||
Load **Agents before Multi-work**, at both root and destination locations. Multi-work fails closed without the native Agents RPC registration; there is no shared module-state registry. Its own storage identity is `stanp-opencode.multi-work`.
|
||||
|
||||
Upgrading from the old `stanp-opencode` monolith is a clean break: existing bindings cannot resume, and quota tracking resets. Start new delegated sessions. Existing worktrees, workspaces, and old host storage remain untouched. New bindings persist normally under the new identity.
|
||||
|
||||
Use OpenCode **2.0.22**, the version this integration targets. The package
|
||||
retains Effect `4.0.0-rc.112` because OpenCode 2.0.22's client requires that
|
||||
exact peer version. Solid `1.9.15` matches the host's override and shared runtime;
|
||||
OpenTUI Solid's exact `1.9.12` peer warning is expected.
|
||||
|
||||
At both the root and destination locations, configure:
|
||||
|
||||
```jsonc
|
||||
{
|
||||
@@ -122,10 +130,9 @@ starts. Root-local delegation performs no Herdr creation and therefore does not
|
||||
require `herdr_worktree`; worker edit/shell permissions remain inherited and
|
||||
enforced normally.
|
||||
|
||||
Worktree permissions inherit the base plugin's **effective build-permission
|
||||
Worktree permissions inherit Agents' **effective build-permission
|
||||
snapshot**, including configuration applied after plugin transforms. Its existing
|
||||
`agent.updated` listener refreshes that snapshot and replays the registry when it
|
||||
changes. Later `multi-work` agent rules and session rules retain their precedence;
|
||||
`agent.updated` listener refreshes that snapshot. Multi-work reads it through the native Agents RPC and replays when it changes. Later `multi-work` agent rules and session rules retain their precedence;
|
||||
inheritance does not turn a build deny into an unconditional execution-time veto
|
||||
against an explicitly configured later allow.
|
||||
|
||||
@@ -136,7 +143,7 @@ The plugin composes the original **Effect executor** in place, preserving the
|
||||
foreground results, background job handling, and completion notifications. It does
|
||||
not create substitute top-level sessions or a separate job scheduler.
|
||||
|
||||
OpenCode 2.0.4 awaits a child-ID progress callback after child creation and before
|
||||
OpenCode 2.0.22 awaits a child-ID progress callback after child creation and before
|
||||
prompting. The wrapper uses that boundary to create the worktree, move the explicit
|
||||
child ID, and confirm its actual session location before allowing the task prompt.
|
||||
A move-request acknowledgement alone is insufficient. Confirmation is bounded to
|
||||
@@ -162,22 +169,22 @@ already accepted by Herdr. Unready children are not resumable as completed setup
|
||||
|
||||
The ordinary suite uses isolated Git repositories and simulated Herdr responses.
|
||||
It covers gating, source refs, ordering, failures, and interruption. Composite tests
|
||||
run the exported plugin through the real Promise adapter, apply configuration after
|
||||
load Agents through the real Promise adapter followed by Multi-work, apply configuration after
|
||||
both plugin transforms, replay twice, and test separate location registries sharing
|
||||
disk-backed bindings across plugin scope restarts. Their executor is simulated.
|
||||
|
||||
An opt-in integration check runs the **real OpenCode 2.0.4 server and native
|
||||
An opt-in integration check runs the **real OpenCode 2.0.22 server and native
|
||||
subagent executor**:
|
||||
|
||||
```sh
|
||||
OPENCODE_204=/absolute/path/to/2.0.4/opencode bun scripts/test-multi-work-host.ts
|
||||
OPENCODE_2022=/absolute/path/to/2.0.22/opencode bun scripts/test-multi-work-host.ts
|
||||
```
|
||||
|
||||
It requires the exact-version binary (for Linux x64, published as
|
||||
`@opencode/cli-linux-x64@2.0.4`), uses fresh HOME/XDG/config/database paths under
|
||||
`/tmp/opencode`, and starts only loopback services. Herdr is a fake CLI performing
|
||||
real Git worktree operations in the fixture repository; the model is a deterministic
|
||||
HTTP endpoint. Fixture resources and logs are retained at the printed path.
|
||||
`@opencode/cli-linux-x64@2.0.22`), uses fresh HOME/XDG/config/database paths under
|
||||
the system temporary directory (or `TMPDIR`), and starts only loopback services.
|
||||
Herdr is a fake CLI performing real Git worktree operations in the fixture repository; the model is a deterministic
|
||||
HTTP endpoint. Fixture resources and logs are cleaned up after the host check.
|
||||
|
||||
Verified with this host check:
|
||||
|
||||
@@ -188,6 +195,8 @@ Verified with this host check:
|
||||
and its synthetic notification to the parent.
|
||||
- Interrupting a foreground RPC while the child awaits its model response causes
|
||||
the native executor to interrupt the child; the parent remains at the root.
|
||||
- Manual configurations with missing or later-loaded Agents fail Multi-work activation.
|
||||
- Quota alone activates without Agents or Multi-work, and its stable RPC returns null without credentials.
|
||||
|
||||
The fixture RPC invokes the actual wrapped executor; it does not exercise the
|
||||
parent model's tool-call dispatch or rendered tool cards. Recovery of an **in-flight**
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
// Required for multi-work -> coordinate -> workers. Apply in worktrees too.
|
||||
// Multi-work and coordinate use deep-lo, falling back to good-lo when unavailable.
|
||||
"experimental": { "subagent_depth": 2 },
|
||||
// All six worker assignments are required. Models use provider/model names.
|
||||
// An unavailable deep-lo omits deep workers and uses good-lo for orchestration.
|
||||
"plugins": [
|
||||
{
|
||||
"package": "stanp-opencode",
|
||||
"options": {
|
||||
"mode": "custom",
|
||||
"agents": {
|
||||
"fast-lo": { "model": "openai/gpt-6-luna", "effort": "low" },
|
||||
"fast-hi": { "model": "openai/gpt-6-luna", "effort": "high" },
|
||||
"good-lo": { "model": "openai/gpt-6-sol", "effort": "low" },
|
||||
"good-hi": { "model": "openai/gpt-6-sol", "effort": "high" },
|
||||
"deep-lo": { "model": "openai/gpt-6-astra", "effort": "low" },
|
||||
"deep-hi": { "model": "openai/gpt-6-astra", "effort": "high" },
|
||||
},
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
{
|
||||
"$schema": "https://opencode.ai/config.json",
|
||||
// Requires OpenCode 2.0.4. Native worktree delegation is version-sensitive.
|
||||
// Required for multi-work -> coordinate -> workers. Apply in worktrees too.
|
||||
"experimental": { "subagent_depth": 2 },
|
||||
// Availability uses the native catalog, including integrations and earlier plugins.
|
||||
"plugins": [
|
||||
{
|
||||
"package": "stanp-opencode",
|
||||
"options": {
|
||||
"mode": "gpt",
|
||||
"provider": "openai",
|
||||
// Priority may cost more. Only advertised tiers or official OpenAI
|
||||
// Luna endpoints enable it; false retains the provider's default.
|
||||
"lunaPriority": true,
|
||||
},
|
||||
},
|
||||
],
|
||||
// Select coordinate in the agent picker. Build and the default stay unchanged.
|
||||
}
|
||||
+10
-81
@@ -1,92 +1,21 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
playwright=false
|
||||
selection=()
|
||||
if (($#)); then
|
||||
if [[ "$#" -ne 1 || "$1" != "--playwright" ]]; then
|
||||
printf 'Usage: %s [--playwright]\n' "$0" >&2
|
||||
if [[ "$#" -ne 2 || "$1" != "--plugins" || ! "$2" =~ ^(agents|multi-work|quota)(,(agents|multi-work|quota))*$ ]]; then
|
||||
printf 'Usage: %s [--plugins agents,multi-work,quota]\n' "$0" >&2
|
||||
exit 2
|
||||
fi
|
||||
playwright=true
|
||||
if [[ ",$2," == *,multi-work,* && ",$2," != *,agents,* ]]; then
|
||||
printf 'Multi-work requires Agents\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
|
||||
for command in bun jq; do
|
||||
command -v "$command" >/dev/null || { printf 'Required command not found: %s\n' "$command" >&2; exit 1; }
|
||||
done
|
||||
|
||||
selection=("$2")
|
||||
fi
|
||||
command -v bun >/dev/null || { printf 'Required command not found: bun\n' >&2; exit 1; }
|
||||
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" && typeof plugin.effect !== "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 --argjson playwright "$playwright" '
|
||||
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
|
||||
if $playwright then
|
||||
.plugins |= map(if .package == "./plugins/stanp-opencode" then .options = ((.options // {}) + { playwright: true }) else . end)
|
||||
else . end
|
||||
else
|
||||
.plugins += [{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: ({ mode: "gpt", provider: "openai", lunaPriority: true } + if $playwright then { playwright: true } else {} end)
|
||||
}]
|
||||
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 opencode to load it.\n' "$checkout" "$config" "$config_file"
|
||||
bun "$checkout/dist/install.js" "$checkout" "$config" "${selection[@]}"
|
||||
|
||||
+9
-62
@@ -1,13 +1,17 @@
|
||||
#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
|
||||
playwright=false
|
||||
selection=()
|
||||
if (($#)); then
|
||||
if [[ "$#" -ne 1 || "$1" != "--playwright" ]]; then
|
||||
printf 'Usage: %s [--playwright]\n' "$0" >&2
|
||||
if [[ "$#" -ne 2 || "$1" != "--plugins" || ! "$2" =~ ^(agents|multi-work|quota)(,(agents|multi-work|quota))*$ ]]; then
|
||||
printf 'Usage: %s [--plugins agents,multi-work,quota]\n' "$0" >&2
|
||||
exit 2
|
||||
fi
|
||||
playwright=true
|
||||
if [[ ",$2," == *,multi-work,* && ",$2," != *,agents,* ]]; then
|
||||
printf 'Multi-work requires Agents\n' >&2
|
||||
exit 2
|
||||
fi
|
||||
selection=("$2")
|
||||
fi
|
||||
|
||||
for command in bun curl jq sha256sum tar; do
|
||||
@@ -43,61 +47,4 @@ if [[ ! "$actual" =~ ^[a-fA-F0-9]{64}$ || "$actual" != "$checksum" ]]; then
|
||||
fi
|
||||
|
||||
tar -xzf "$install/stanp-opencode.tgz" -C "$install"
|
||||
bun -e '
|
||||
if (!await Bun.file(process.argv[1]).exists()) {
|
||||
throw new Error("Latest release lacks the OpenCode V2 directory entrypoint. Publish a V2-compatible release first.");
|
||||
}
|
||||
const { default: plugin } = await import(process.argv[1]);
|
||||
if (!plugin || plugin.id !== "stanp-opencode" || (typeof plugin.setup !== "function" && typeof plugin.effect !== "function")) {
|
||||
throw new Error("Latest release is not a native OpenCode V2 plugin. Publish a V2-compatible release first.");
|
||||
}
|
||||
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"
|
||||
config_source="$config_file"
|
||||
if [[ ! -e "$config_source" ]]; then
|
||||
config_source="$install/empty-config.json"
|
||||
printf '{}\n' > "$config_source"
|
||||
fi
|
||||
jq -e --argjson playwright "$playwright" '
|
||||
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
|
||||
if $playwright then
|
||||
.plugins |= map(if .package == "./plugins/stanp-opencode" then .options = ((.options // {}) + { playwright: true }) else . end)
|
||||
else . end
|
||||
else
|
||||
.plugins += [{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: ({ mode: "gpt", provider: "openai", lunaPriority: true } + if $playwright then { playwright: true } else {} end)
|
||||
}]
|
||||
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"
|
||||
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","./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
|
||||
rm -rf "$backup"
|
||||
rm -f "$config/plugins/stanp-opencode.js"
|
||||
mv "$install/opencode.json" "$config_file"
|
||||
printf 'Installed stanp-opencode %s into %s/plugins/stanp-opencode and updated %s\nRestart opencode to load it.\n' "$tag" "$config" "$config_file"
|
||||
bun "$install/package/dist/install.js" "$install/package" "$config" "${selection[@]}"
|
||||
|
||||
+27
-19
@@ -3,27 +3,35 @@
|
||||
"version": "1.0.0",
|
||||
"description": "Stanp agents for OpenCode",
|
||||
"type": "module",
|
||||
"main": "./dist/index.js",
|
||||
"types": "./dist/index.d.ts",
|
||||
"exports": {
|
||||
".": {
|
||||
"types": "./dist/index.d.ts",
|
||||
"import": "./dist/index.js"
|
||||
"./agents": {
|
||||
"types": "./dist/agents.d.ts",
|
||||
"import": "./dist/agents.js"
|
||||
},
|
||||
"./multi-work": {
|
||||
"types": "./dist/multi-work.d.ts",
|
||||
"import": "./dist/multi-work.js"
|
||||
},
|
||||
"./quota": {
|
||||
"types": "./dist/quota-plugin.d.ts",
|
||||
"import": "./dist/quota-plugin.js"
|
||||
},
|
||||
"./tui": {
|
||||
"types": "./dist/tui.d.ts",
|
||||
"import": "./dist/tui.js"
|
||||
},
|
||||
"./quota/tui": {
|
||||
"types": "./dist/tui.d.ts",
|
||||
"import": "./dist/tui.js"
|
||||
},
|
||||
"./quota-rpc": {
|
||||
"types": "./dist/quota-rpc.d.ts",
|
||||
"import": "./dist/quota-rpc.js"
|
||||
}
|
||||
},
|
||||
"files": [
|
||||
"index.js",
|
||||
"dist",
|
||||
"docs",
|
||||
"examples"
|
||||
"docs"
|
||||
],
|
||||
"packageManager": "bun@1.4.2",
|
||||
"engines": {
|
||||
@@ -38,22 +46,22 @@
|
||||
"build": "bun scripts/build.ts && tsc --project tsconfig.build.json --emitDeclarationOnly"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@opencode/client": "2.0.4",
|
||||
"@opencode/plugin": "2.0.4",
|
||||
"@opencode/schema": "2.0.4",
|
||||
"@opentui/core": "0.5.11",
|
||||
"@opentui/solid": "0.5.11",
|
||||
"@oxlint/plugins": "1.83.0",
|
||||
"@opencode/client": "2.0.22",
|
||||
"@opencode/plugin": "2.0.22",
|
||||
"@opencode/schema": "2.0.22",
|
||||
"@opentui/core": "0.5.14",
|
||||
"@opentui/solid": "0.5.14",
|
||||
"@oxlint/plugins": "1.86.0",
|
||||
"@types/bun": "1.4.2",
|
||||
"oxfmt": "0.68.0",
|
||||
"oxlint": "1.83.0",
|
||||
"oxfmt": "0.71.0",
|
||||
"oxlint": "1.86.0",
|
||||
"solid-js": "1.9.15",
|
||||
"typescript": "7.0.2"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"@opencode/plugin": "2.0.4",
|
||||
"@opentui/core": ">=0.5.11",
|
||||
"@opentui/solid": ">=0.5.11",
|
||||
"@opencode/plugin": "2.0.22",
|
||||
"@opentui/core": ">=0.5.14",
|
||||
"@opentui/solid": ">=0.5.14",
|
||||
"solid-js": ">=1.9.0"
|
||||
},
|
||||
"dependencies": {
|
||||
|
||||
+15
-1
@@ -1,9 +1,23 @@
|
||||
import solidPlugin from "@opentui/solid/bun-plugin";
|
||||
import { rm } from "node:fs/promises";
|
||||
|
||||
export async function build() {
|
||||
for (const file of ["index.js", "index.d.ts", "playwright.d.ts"])
|
||||
await rm(`dist/${file}`, { force: true });
|
||||
const results = await Promise.all([
|
||||
Bun.build({
|
||||
entrypoints: ["src/index.ts", "src/quota-rpc.ts"],
|
||||
entrypoints: [
|
||||
"src/agents.ts",
|
||||
"src/multi-work.ts",
|
||||
"src/quota-plugin.ts",
|
||||
"src/quota-rpc.ts",
|
||||
],
|
||||
outdir: "dist",
|
||||
target: "bun",
|
||||
format: "esm",
|
||||
}),
|
||||
Bun.build({
|
||||
entrypoints: ["scripts/install.ts"],
|
||||
outdir: "dist",
|
||||
target: "bun",
|
||||
format: "esm",
|
||||
|
||||
@@ -0,0 +1,197 @@
|
||||
import { mkdir, rename, rm, copyFile } from "node:fs/promises";
|
||||
import { join } from "node:path";
|
||||
import { createInterface } from "node:readline/promises";
|
||||
import { z } from "zod";
|
||||
|
||||
const names = ["agents", "multi-work", "quota"] as const;
|
||||
const base = "./plugins/stanp-opencode";
|
||||
const entrySchema = z
|
||||
.object({ package: z.string(), options: z.record(z.string(), z.json()).optional() })
|
||||
.catchall(z.json());
|
||||
type Entry = z.infer<typeof entrySchema>;
|
||||
const pluginSchema = z.union([z.string(), entrySchema]);
|
||||
const registrationSchema = z
|
||||
.union([z.string().transform((packageName) => ({ package: packageName })), entrySchema])
|
||||
.pipe(entrySchema);
|
||||
const configSchema = z
|
||||
.object({ plugins: z.array(pluginSchema).optional(), $schema: z.string().optional() })
|
||||
.catchall(z.json());
|
||||
const nativePlugin = z.union([
|
||||
z.object({ id: z.string(), setup: z.function() }),
|
||||
z.object({ id: z.string(), effect: z.function() }),
|
||||
]);
|
||||
type DirectoryExports = { ".": string; "./tui"?: string };
|
||||
|
||||
export async function install(source: string, config: string, selection?: string) {
|
||||
const absoluteBase = join(config, "plugins", "stanp-opencode");
|
||||
const monolith = (target: string) =>
|
||||
target === base || target === absoluteBase || /^stanp-opencode(?:@[^/]+)?$/.test(target);
|
||||
const owned = new Set(
|
||||
names.flatMap((name) => [
|
||||
`${base}/${name}`,
|
||||
`${absoluteBase}/${name}`,
|
||||
`stanp-opencode/${name}`,
|
||||
]),
|
||||
);
|
||||
const isOwned = (target: string) => monolith(target) || owned.has(target);
|
||||
const configFile = join(config, "opencode.json");
|
||||
const current = configSchema.parse(
|
||||
(await Bun.file(configFile).exists()) ? await Bun.file(configFile).json() : {},
|
||||
);
|
||||
const entries = current.plugins ?? [];
|
||||
// Normalize only the lookup view; unrelated entries retain their original representation.
|
||||
const configured = entries.map((entry) => registrationSchema.parse(entry));
|
||||
const previous = configured.filter((entry) => isOwned(entry.package));
|
||||
const oldMonolith = previous.some((entry) => monolith(entry.package));
|
||||
const retained = oldMonolith
|
||||
? names
|
||||
: names.filter((name) =>
|
||||
previous.some(
|
||||
(entry) =>
|
||||
entry.package === `${base}/${name}` ||
|
||||
entry.package === `${absoluteBase}/${name}` ||
|
||||
entry.package === `stanp-opencode/${name}`,
|
||||
),
|
||||
);
|
||||
let chosen: string[];
|
||||
if (selection !== undefined) chosen = selection.split(",");
|
||||
else if (process.stdin.isTTY) {
|
||||
const input = createInterface({ input: process.stdin, output: process.stdout });
|
||||
try {
|
||||
const defaults = retained.length ? retained : names;
|
||||
chosen = (
|
||||
(
|
||||
await input.question(
|
||||
`Plugins (agents,multi-work,quota) [${defaults.join(",")}]: `,
|
||||
)
|
||||
).trim() || defaults.join(",")
|
||||
).split(",");
|
||||
if (chosen.includes("multi-work") && !chosen.includes("agents")) {
|
||||
if (
|
||||
/^(y|yes)?$/i.test(
|
||||
(
|
||||
await input.question("Multi-work requires Agents. Add Agents? [Y/n]: ")
|
||||
).trim(),
|
||||
)
|
||||
)
|
||||
chosen.push("agents");
|
||||
}
|
||||
} finally {
|
||||
input.close();
|
||||
}
|
||||
} else {
|
||||
if (!retained.length)
|
||||
throw new Error(
|
||||
"Noninteractive installation requires --plugins agents,multi-work,quota (or a valid subset)",
|
||||
);
|
||||
chosen = [...retained];
|
||||
}
|
||||
if (
|
||||
!z.array(z.enum(names)).min(1).safeParse(chosen).success ||
|
||||
new Set(chosen).size !== chosen.length
|
||||
)
|
||||
throw new Error("Invalid --plugins selection; use unique names: agents,multi-work,quota");
|
||||
if (chosen.includes("multi-work") && !chosen.includes("agents"))
|
||||
throw new Error("Multi-work requires Agents; select agents,multi-work");
|
||||
const selected = names.filter((name) => chosen.includes(name));
|
||||
const registrations: Entry[] = selected.map((name) => {
|
||||
const prior = previous.find(
|
||||
(entry) =>
|
||||
entry.package === `${base}/${name}` ||
|
||||
entry.package === `${absoluteBase}/${name}` ||
|
||||
entry.package === `stanp-opencode/${name}` ||
|
||||
(name === "agents" && monolith(entry.package)),
|
||||
);
|
||||
const options: NonNullable<Entry["options"]> = {};
|
||||
if (name === "agents" && !prior?.options)
|
||||
Object.assign(options, { mode: "gpt", provider: "openai", lunaPriority: true });
|
||||
Object.assign(options, prior?.options);
|
||||
delete options.playwright;
|
||||
const entry = entrySchema.parse({ ...prior, package: `${base}/${name}` });
|
||||
delete entry.options;
|
||||
if (Object.keys(options).length) entry.options = options;
|
||||
return entry;
|
||||
});
|
||||
const first = configured.findIndex((entry) => isOwned(entry.package));
|
||||
const next = entries.filter((_entry, index) => !isOwned(configured[index]!.package));
|
||||
next.splice(first < 0 ? next.length : first, 0, ...registrations);
|
||||
const updated = {
|
||||
...current,
|
||||
$schema: current.$schema ?? "https://opencode.ai/config.json",
|
||||
plugins: next,
|
||||
};
|
||||
const parent = join(config, "plugins");
|
||||
await mkdir(parent, { recursive: true });
|
||||
const staged = join(parent, `.stanp-opencode.new.${process.pid}`);
|
||||
const backup = join(parent, `.stanp-opencode.old.${process.pid}`);
|
||||
const target = join(parent, "stanp-opencode");
|
||||
const configStage = join(config, `.stanp-opencode.config.${process.pid}`);
|
||||
let saved = false;
|
||||
let activated = false;
|
||||
try {
|
||||
await mkdir(staged);
|
||||
for (const name of selected) {
|
||||
const file = name === "quota" ? "quota-plugin" : name;
|
||||
const entrypoint = join(source, "dist", `${file}.js`);
|
||||
const { default: definition } = await import(entrypoint);
|
||||
const plugin = nativePlugin.parse(definition);
|
||||
if (plugin.id !== `stanp-opencode.${name}`)
|
||||
throw new Error(`Invalid native OpenCode plugin: ${name}`);
|
||||
const directory = join(staged, name);
|
||||
await mkdir(directory);
|
||||
await copyFile(entrypoint, join(directory, "index.js"));
|
||||
const exports: DirectoryExports = { ".": "./index.js" };
|
||||
if (name === "quota") {
|
||||
const tui = join(source, "dist", "tui.js");
|
||||
if (
|
||||
!new Bun.Transpiler({ loader: "js" })
|
||||
.scan(await Bun.file(tui).text())
|
||||
.exports.includes("default")
|
||||
)
|
||||
throw new Error("Quota lacks its TUI entrypoint");
|
||||
await copyFile(tui, join(directory, "tui.js"));
|
||||
exports["./tui"] = "./tui.js";
|
||||
}
|
||||
await Bun.write(
|
||||
join(directory, "package.json"),
|
||||
JSON.stringify({
|
||||
name: `stanp-opencode-${name}`,
|
||||
type: "module",
|
||||
main: "./index.js",
|
||||
exports,
|
||||
}),
|
||||
);
|
||||
}
|
||||
await Bun.write(configStage, JSON.stringify(updated, null, 4) + "\n");
|
||||
try {
|
||||
await rename(target, backup);
|
||||
saved = true;
|
||||
} catch (error) {
|
||||
if (!(error instanceof Error && "code" in error && error.code === "ENOENT"))
|
||||
throw error;
|
||||
}
|
||||
await rename(staged, target);
|
||||
activated = true;
|
||||
await rename(configStage, configFile);
|
||||
} catch (error) {
|
||||
if (activated) await rm(target, { recursive: true, force: true });
|
||||
if (saved) await rename(backup, target);
|
||||
throw error;
|
||||
} finally {
|
||||
await rm(staged, { recursive: true, force: true });
|
||||
await rm(configStage, { force: true });
|
||||
}
|
||||
if (saved) await rm(backup, { recursive: true, force: true });
|
||||
await rm(join(parent, "stanp-opencode.js"), { force: true });
|
||||
console.log(`Installed ${selected.join(", ")}. Restart OpenCode.`);
|
||||
if (oldMonolith)
|
||||
console.log(
|
||||
"Clean break: quota tracking resets; old Multi-work bindings cannot resume. Existing worktrees are untouched.",
|
||||
);
|
||||
}
|
||||
|
||||
if (import.meta.main) {
|
||||
const [source, config, selection] = process.argv.slice(2);
|
||||
if (!source || !config) throw new Error("Expected package directory and config directory");
|
||||
await install(source, config, selection);
|
||||
}
|
||||
+140
-25
@@ -1,16 +1,17 @@
|
||||
// Opt-in real-host check: OPENCODE_204=/absolute/path/to/2.0.4/opencode bun scripts/test-multi-work-host.ts
|
||||
// Opt-in real-host check: OPENCODE_2022=/absolute/path/to/2.0.22/opencode bun scripts/test-multi-work-host.ts
|
||||
// Only fake Herdr/model servers and fresh temporary repositories/databases are used.
|
||||
import { mkdir, mkdtemp, writeFile, chmod, readFile } from "node:fs/promises";
|
||||
import { mkdir, mkdtemp, writeFile, chmod, readFile, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join, resolve } from "node:path";
|
||||
import { createServer } from "node:net";
|
||||
import { OpenCode } from "@opencode/client";
|
||||
import { command } from "../src/herdr.js";
|
||||
import { z } from "zod";
|
||||
|
||||
const binary = process.env.OPENCODE_204;
|
||||
if (!binary) throw new Error("Set OPENCODE_204 to an isolated OpenCode 2.0.4 binary");
|
||||
const temp = await mkdtemp("/tmp/opencode/native-multi-work-");
|
||||
console.log(`Fixture retained at ${temp}`);
|
||||
const binary = process.env.OPENCODE_2022;
|
||||
if (!binary) throw new Error("Set OPENCODE_2022 to an isolated OpenCode 2.0.22 binary");
|
||||
const temp = await mkdtemp(join(tmpdir(), "native-multi-work-"));
|
||||
try {
|
||||
const root = join(temp, "root"),
|
||||
destination = join(temp, "child"),
|
||||
bin = join(temp, "bin"),
|
||||
@@ -18,8 +19,8 @@ const root = join(temp, "root"),
|
||||
const herdrCreation = join(temp, "herdr-creation.txt");
|
||||
for (const path of [root, bin, config, join(temp, "plugin")]) await mkdir(path);
|
||||
const signal = AbortSignal.timeout(120_000);
|
||||
if ((await command([binary, "--version"], temp, signal)) !== "opencode v2.0.4")
|
||||
throw new Error("Exact 2.0.4 binary required");
|
||||
if ((await command([binary, "--version"], temp, signal)) !== "opencode v2.0.22")
|
||||
throw new Error("Exact 2.0.22 binary required");
|
||||
await command(["git", "init", "-b", "main"], root, signal);
|
||||
await command(
|
||||
[
|
||||
@@ -46,10 +47,22 @@ const inventory = (path: string, id: string) =>
|
||||
source_workspace_id: "w1",
|
||||
},
|
||||
worktrees: [
|
||||
{ path: root, is_linked_worktree: false, is_bare: false, open_workspace_id: "w1" },
|
||||
{
|
||||
path: root,
|
||||
is_linked_worktree: false,
|
||||
is_bare: false,
|
||||
open_workspace_id: "w1",
|
||||
},
|
||||
...(path === root
|
||||
? []
|
||||
: [{ path, is_linked_worktree: true, is_bare: false, open_workspace_id: id }]),
|
||||
: [
|
||||
{
|
||||
path,
|
||||
is_linked_worktree: true,
|
||||
is_bare: false,
|
||||
open_workspace_id: id,
|
||||
},
|
||||
]),
|
||||
],
|
||||
},
|
||||
});
|
||||
@@ -58,20 +71,30 @@ await writeFile(
|
||||
`#!/bin/sh\ncase "$1 $2" in\n"worktree create") [ "$9" = "--label" ] && [ "\${10}" = "Fix native host" ] || exit 8; printf '%s\\n%s\\n' "$6" "\${10}" > '${herdrCreation}'; git -C '${root}' worktree add -b "$6" '${destination}' "$8" >&2 || exit 1; printf '{"result":{"workspace":{"workspace_id":"w2"},"worktree":{"path":"${destination}","branch":"%s","is_linked_worktree":true}}}' "$6"; exit ;;\nesac\ncase "$1 $2 $4" in\n"workspace list ") echo '{"result":{"workspaces":[{"workspace_id":"w1"}]}}' ;;\n"worktree list w1") echo '${inventory(root, "w1")}' ;;\n"worktree list w2") echo '${inventory(destination, "w2")}' ;;\n*) exit 9 ;;\nesac\n`,
|
||||
);
|
||||
await chmod(join(bin, "herdr"), 0o755);
|
||||
for (const [name, fixture] of [
|
||||
["agents", "native-agents-plugin"],
|
||||
["multi-work", "native-host-plugin"],
|
||||
] as const) {
|
||||
const built = await Bun.build({
|
||||
entrypoints: [resolve("tests/fixtures/native-host-plugin.ts")],
|
||||
outdir: join(temp, "plugin"),
|
||||
entrypoints: [resolve(`tests/fixtures/${fixture}.ts`)],
|
||||
outdir: join(temp, "plugin", name),
|
||||
naming: "index.js",
|
||||
target: "bun",
|
||||
format: "esm",
|
||||
});
|
||||
if (!built.success) throw new Error(String(built.logs));
|
||||
}
|
||||
await writeFile(
|
||||
join(config, "opencode.json"),
|
||||
JSON.stringify({
|
||||
plugins: [join(temp, "plugin")],
|
||||
plugins: [
|
||||
{ package: join(temp, "plugin", "agents") },
|
||||
{ package: join(temp, "plugin", "multi-work") },
|
||||
],
|
||||
agents: {
|
||||
build: { permissions: [{ action: "herdr_worktree", resource: "*", effect: "deny" }] },
|
||||
build: {
|
||||
permissions: [{ action: "herdr_worktree", resource: "*", effect: "deny" }],
|
||||
},
|
||||
},
|
||||
experimental: { subagent_depth: 2 },
|
||||
snapshots: false,
|
||||
@@ -145,7 +168,7 @@ async function ready() {
|
||||
)?.[1];
|
||||
if (password)
|
||||
headers.authorization = `Basic ${Buffer.from(`opencode:${password}`).toString("base64")}`;
|
||||
await client.server.status();
|
||||
await client.server.info();
|
||||
return;
|
||||
} catch {
|
||||
await Bun.sleep(100);
|
||||
@@ -175,7 +198,9 @@ try {
|
||||
input: { parent: parent.id, background: false },
|
||||
});
|
||||
if (!z.string().parse(denied.output).includes("herdr_worktree requires explicit allow"))
|
||||
throw new Error(`Late configured build deny was not enforced: ${JSON.stringify(denied)}`);
|
||||
throw new Error(
|
||||
`Late configured build deny was not enforced: ${JSON.stringify(denied)}`,
|
||||
);
|
||||
await client.session.update({
|
||||
sessionID: parent.id,
|
||||
permissions: [{ action: "herdr_worktree", resource: "*", effect: "allow" }],
|
||||
@@ -223,7 +248,10 @@ try {
|
||||
});
|
||||
const background = z
|
||||
.object({
|
||||
metadata: z.object({ sessionID: z.literal(child.id), status: z.literal("running") }),
|
||||
metadata: z.object({
|
||||
sessionID: z.literal(child.id),
|
||||
status: z.literal("running"),
|
||||
}),
|
||||
})
|
||||
.parse(JSON.parse(z.string().parse(resumed.output)));
|
||||
console.log("Native background child", background.metadata.sessionID);
|
||||
@@ -241,16 +269,20 @@ try {
|
||||
)
|
||||
)
|
||||
break;
|
||||
if (attempt === 99) throw new Error("Missing native background completion notification");
|
||||
if (attempt === 99)
|
||||
throw new Error("Missing native background completion notification");
|
||||
await Bun.sleep(100);
|
||||
}
|
||||
const finalAdmissions = (await readFile(env.FIXTURE_TRACE, "utf8"))
|
||||
.trim()
|
||||
.split("\n")
|
||||
.map((line) => z.object({ id: z.string(), directory: z.string() }).parse(JSON.parse(line)));
|
||||
.map((line) =>
|
||||
z.object({ id: z.string(), directory: z.string() }).parse(JSON.parse(line)),
|
||||
);
|
||||
if (
|
||||
finalAdmissions.filter((entry) => entry.id === child.id && entry.directory === destination)
|
||||
.length !== 2
|
||||
finalAdmissions.filter(
|
||||
(entry) => entry.id === child.id && entry.directory === destination,
|
||||
).length !== 2
|
||||
)
|
||||
throw new Error("Expected two child prompts at destination");
|
||||
if ((await client.session.get({ sessionID: parent.id })).location.directory !== root)
|
||||
@@ -274,9 +306,13 @@ try {
|
||||
throw new Error("Foreground unexpectedly finished before cancellation");
|
||||
}),
|
||||
new Promise<never>((_resolve, reject) =>
|
||||
deadline.addEventListener("abort", () => reject(new Error("Model was not reached")), {
|
||||
deadline.addEventListener(
|
||||
"abort",
|
||||
() => reject(new Error("Model was not reached")),
|
||||
{
|
||||
once: true,
|
||||
}),
|
||||
},
|
||||
),
|
||||
),
|
||||
]);
|
||||
cancel.abort();
|
||||
@@ -286,12 +322,88 @@ try {
|
||||
});
|
||||
if (!rejected) throw new Error("Cancelled RPC did not reject");
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
if ((await client.session.get({ sessionID: child.id })).outcome === "interrupted") break;
|
||||
if ((await client.session.get({ sessionID: child.id })).outcome === "interrupted")
|
||||
break;
|
||||
if (attempt === 99) throw new Error("Native cancellation did not interrupt the child");
|
||||
await Bun.sleep(50);
|
||||
}
|
||||
releaseModel.resolve();
|
||||
holdModel = false;
|
||||
for (const plugins of [
|
||||
[{ package: join(temp, "plugin", "multi-work") }],
|
||||
[
|
||||
{ package: join(temp, "plugin", "multi-work") },
|
||||
{ package: join(temp, "plugin", "agents") },
|
||||
],
|
||||
]) {
|
||||
server.kill();
|
||||
await server.exited;
|
||||
await writeFile(
|
||||
join(config, "opencode.json"),
|
||||
JSON.stringify({ plugins, update: "disable" }),
|
||||
);
|
||||
server = start();
|
||||
await ready();
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
const { data } = await client.plugin.list({ location });
|
||||
const multi = data.find((plugin) => plugin.id === "stanp-opencode.multi-work");
|
||||
if (multi?.state.status === "failed") break;
|
||||
if (attempt === 99 || multi?.state.status === "active")
|
||||
throw new Error(
|
||||
`Missing or late Agents did not fail Multi-work activation: ${JSON.stringify(data)}`,
|
||||
);
|
||||
await Bun.sleep(100);
|
||||
}
|
||||
}
|
||||
server.kill();
|
||||
await server.exited;
|
||||
const quotaBuild = await Bun.build({
|
||||
entrypoints: [resolve("src/quota-plugin.ts")],
|
||||
outdir: join(temp, "plugin", "quota"),
|
||||
naming: "index.js",
|
||||
target: "bun",
|
||||
format: "esm",
|
||||
});
|
||||
if (!quotaBuild.success) throw new Error(String(quotaBuild.logs));
|
||||
await writeFile(
|
||||
join(config, "opencode.json"),
|
||||
JSON.stringify({
|
||||
plugins: [{ package: join(temp, "plugin", "quota") }],
|
||||
update: "disable",
|
||||
}),
|
||||
);
|
||||
server = start();
|
||||
await ready();
|
||||
for (let attempt = 0; attempt < 100; attempt++) {
|
||||
const independent = await client.plugin.list({ location });
|
||||
if (
|
||||
independent.data.some(
|
||||
(plugin) =>
|
||||
plugin.id === "stanp-opencode.agents" ||
|
||||
plugin.id === "stanp-opencode.multi-work",
|
||||
)
|
||||
)
|
||||
throw new Error("Quota-only configuration loaded Agents or Multi-work");
|
||||
if (
|
||||
independent.data.some(
|
||||
(plugin) =>
|
||||
plugin.id === "stanp-opencode.quota" && plugin.state.status === "active",
|
||||
)
|
||||
)
|
||||
break;
|
||||
if (attempt === 99) throw new Error("Quota did not activate independently");
|
||||
await Bun.sleep(100);
|
||||
}
|
||||
const quotaStatus = await client.rpc.call({
|
||||
rpcID: "stanp-opencode.quota",
|
||||
method: "status",
|
||||
location,
|
||||
input: {},
|
||||
});
|
||||
if (quotaStatus.output !== null)
|
||||
throw new Error("Unauthenticated Quota RPC should return null");
|
||||
console.log(
|
||||
"PASS: configured build deny + session override, native foreground result, child move-before-prompt, persisted binding resume after host restart, background completion + parent notification, native foreground interruption",
|
||||
"PASS: configured build deny + session override, native foreground result, child move-before-prompt, persisted binding resume after host restart, background completion + parent notification, native foreground interruption, missing/late Agents fail closed, independent Quota and stable RPC",
|
||||
);
|
||||
} finally {
|
||||
releaseModel.resolve();
|
||||
@@ -299,3 +411,6 @@ try {
|
||||
await server.exited;
|
||||
model.stop(true);
|
||||
}
|
||||
} finally {
|
||||
await rm(temp, { recursive: true, force: true });
|
||||
}
|
||||
|
||||
@@ -0,0 +1,20 @@
|
||||
import { Rpc } from "@opencode/plugin/rpc";
|
||||
import { z } from "zod";
|
||||
|
||||
// A native host boundary: Multi-work must load after Agents and use its final build permissions.
|
||||
export const AgentsRpc = Rpc.define({
|
||||
id: "stanp-opencode.agents",
|
||||
methods: {
|
||||
permissions: {
|
||||
input: z.object({}),
|
||||
output: z.array(
|
||||
z.object({
|
||||
action: z.string(),
|
||||
resource: z.string(),
|
||||
effect: z.enum(["allow", "ask", "deny"]),
|
||||
}),
|
||||
),
|
||||
},
|
||||
},
|
||||
events: {},
|
||||
});
|
||||
+303
@@ -0,0 +1,303 @@
|
||||
import { Agent, Model, Plugin, Provider } from "@opencode/plugin";
|
||||
import type { ModelInfo } from "@opencode/client";
|
||||
import { z } from "zod";
|
||||
import { gptAgents, settings, workerNames } from "./config.js";
|
||||
import { discover, modelKey } from "./provider.js";
|
||||
import { descriptions, routingGuide } from "./routing.js";
|
||||
import { Skill } from "@opencode/schema/skill";
|
||||
import { AbsolutePath } from "@opencode/schema/schema";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import antiOverengineering from "./anti-overengineering.md" with { type: "text" };
|
||||
import { AgentsRpc } from "./agents-rpc.js";
|
||||
|
||||
// Permission actions use wildcards, not filesystem globs.
|
||||
function matches(value: string, pattern: string) {
|
||||
const escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
||||
.replace(/\*/g, ".*")
|
||||
.replace(/\?/g, ".");
|
||||
return new RegExp(`^${escaped}$`, "s").test(value);
|
||||
}
|
||||
|
||||
const reading = [
|
||||
"read",
|
||||
"glob",
|
||||
"grep",
|
||||
"list",
|
||||
"todoread",
|
||||
"todowrite",
|
||||
"webfetch",
|
||||
"websearch",
|
||||
"question",
|
||||
"skill",
|
||||
"external_directory",
|
||||
"doom_loop",
|
||||
];
|
||||
const delegation = z.object({ agent: z.string() });
|
||||
export default Plugin.define({
|
||||
id: "stanp-opencode.agents",
|
||||
async setup(context) {
|
||||
const selected = settings.parse({ mode: "gpt", ...context.options });
|
||||
const lunaPriority =
|
||||
selected.mode === "gpt" &&
|
||||
selected.provider === "openai" &&
|
||||
selected.lunaPriority === true;
|
||||
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
|
||||
let inherited: Agent.Info["permissions"] = Agent.Info.default(
|
||||
Agent.ID.make("build"),
|
||||
).permissions;
|
||||
let permissionKey = JSON.stringify(inherited);
|
||||
let codegraphTools: string[] = [];
|
||||
let agentTransformReady = false;
|
||||
const controller = new AbortController();
|
||||
// Subscribe before the first read: configuration can publish its models while
|
||||
// asynchronous setup is running.
|
||||
const updates = (async () => {
|
||||
for await (const event of context.event.subscribe({ signal: controller.signal })) {
|
||||
if (event.location) {
|
||||
const workspaceID =
|
||||
"workspaceID" in event.location ? event.location.workspaceID : undefined;
|
||||
if (
|
||||
event.location.directory !== context.location.directory ||
|
||||
workspaceID !== context.location.workspaceID
|
||||
)
|
||||
continue;
|
||||
}
|
||||
if (event.type === "model.updated") {
|
||||
models = await discover(context, lunaPriority);
|
||||
await context.agent.reload();
|
||||
} else if (event.type === "agent.updated") {
|
||||
// The configured build transform may run after ours. Read the final
|
||||
// registry, then replay only when its effective permissions change.
|
||||
const { data } = await context.agent.list();
|
||||
const permissions =
|
||||
data.find((agent) => agent.id === "build")?.permissions ??
|
||||
Agent.Info.default(Agent.ID.make("build")).permissions;
|
||||
const key = JSON.stringify(permissions);
|
||||
if (key === permissionKey) continue;
|
||||
inherited = permissions;
|
||||
permissionKey = key;
|
||||
await context.agent.reload();
|
||||
}
|
||||
}
|
||||
})().catch((error) => {
|
||||
if (!controller.signal.aborted)
|
||||
console.error("stanp-opencode model update failed", error);
|
||||
});
|
||||
models = await discover(context, lunaPriority);
|
||||
const { data: initialAgents } = await context.agent.list();
|
||||
inherited = initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
|
||||
permissionKey = JSON.stringify(inherited);
|
||||
const registration = await context.rpc.register(AgentsRpc, {
|
||||
permissions: async () => inherited.map((rule) => ({ ...rule })),
|
||||
});
|
||||
const assignments =
|
||||
selected.mode === "gpt"
|
||||
? Object.fromEntries(
|
||||
workerNames.map((name) => [
|
||||
name,
|
||||
{
|
||||
...gptAgents[name],
|
||||
provider: selected.provider,
|
||||
},
|
||||
]),
|
||||
)
|
||||
: selected.agents;
|
||||
let workers: (typeof workerNames)[number][] = [];
|
||||
const active = new Set<string>();
|
||||
|
||||
await context.skill.transform((editor) => {
|
||||
if (editor.get("anti-overengineering")) return;
|
||||
editor.add({
|
||||
id: Skill.ID.make("anti-overengineering"),
|
||||
name: Skill.Name.make("anti-overengineering"),
|
||||
description: "Build the smallest correct solution without sacrificing correctness.",
|
||||
path: AbsolutePath.make(fileURLToPath(import.meta.url)),
|
||||
content: antiOverengineering,
|
||||
});
|
||||
});
|
||||
|
||||
await context.tool.transform((editor) => {
|
||||
const next = editor
|
||||
.list()
|
||||
.map((tool) => tool.id)
|
||||
.filter((id) => id.startsWith("codegraph_"))
|
||||
.sort();
|
||||
if (JSON.stringify(next) === JSON.stringify(codegraphTools)) return;
|
||||
codegraphTools = next;
|
||||
if (agentTransformReady)
|
||||
queueMicrotask(() => {
|
||||
void context.agent
|
||||
.reload()
|
||||
.catch((error) =>
|
||||
console.error("stanp-opencode agent reload failed", error),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await context.agent.transform((editor) => {
|
||||
const coordinates = (assignment: { model: string }) => {
|
||||
if (selected.mode === "gpt")
|
||||
return { provider: selected.provider, model: assignment.model };
|
||||
const slash = assignment.model.indexOf("/");
|
||||
return {
|
||||
provider: assignment.model.slice(0, slash),
|
||||
model: assignment.model.slice(slash + 1),
|
||||
};
|
||||
};
|
||||
const availableFor = (assignment: { model: string }) => {
|
||||
const { provider, model } = coordinates(assignment);
|
||||
return models.get(modelKey(provider, model));
|
||||
};
|
||||
const deepLo = assignments["deep-lo"];
|
||||
const deep =
|
||||
deepLo !== undefined &&
|
||||
assignments["deep-hi"] !== undefined &&
|
||||
availableFor(deepLo) !== undefined;
|
||||
const candidates = workerNames.filter((name) => deep || !name.startsWith("deep-"));
|
||||
const coordinate = deep ? deepLo : assignments["good-lo"];
|
||||
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
|
||||
(assignment) => assignment !== undefined && availableFor(assignment) !== undefined,
|
||||
)
|
||||
? candidates
|
||||
: [];
|
||||
active.clear();
|
||||
for (const name of workers) active.add(name);
|
||||
if (workers.length === 0) return;
|
||||
for (const name of workers) {
|
||||
const assignment = assignments[name]!;
|
||||
const available = availableFor(assignment)!;
|
||||
editor.update(name, (agent) => {
|
||||
const hasVariant = available.model.variants.some(
|
||||
({ id }) => id === assignment.effort,
|
||||
);
|
||||
agent.name = Agent.Name.make(name);
|
||||
agent.mode = "subagent";
|
||||
agent.hidden = false;
|
||||
agent.description = descriptions[name];
|
||||
agent.model = {
|
||||
providerID: Provider.ID.make(available.model.providerID),
|
||||
id: Model.ID.make(available.model.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(assignment.effort);
|
||||
agent.request = {
|
||||
settings: hasVariant ? {} : { reasoningEffort: assignment.effort },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
if (lunaPriority && available.serviceTier)
|
||||
agent.request.settings.serviceTier = available.serviceTier;
|
||||
agent.permissions = [
|
||||
...structuredClone(inherited),
|
||||
{ action: "subagent", resource: "*", effect: "deny" },
|
||||
];
|
||||
});
|
||||
}
|
||||
editor.update("coordinate", (agent) => {
|
||||
const available = availableFor(coordinate)!;
|
||||
const hasVariant = available.model.variants.some(
|
||||
({ id }) => id === coordinate.effort,
|
||||
);
|
||||
agent.name = Agent.Name.make("coordinate");
|
||||
agent.mode = "primary";
|
||||
agent.hidden = false;
|
||||
agent.description =
|
||||
"Read, plan, and coordinate specialist workers; never implement directly.";
|
||||
agent.model = {
|
||||
providerID: Provider.ID.make(available.model.providerID),
|
||||
id: Model.ID.make(available.model.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort);
|
||||
agent.request = {
|
||||
settings: hasVariant ? {} : { reasoningEffort: coordinate.effort },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
if (lunaPriority && available.serviceTier)
|
||||
agent.request.settings.serviceTier = available.serviceTier;
|
||||
agent.system = routingGuide(deep);
|
||||
agent.permissions = [
|
||||
{ action: "*", resource: "*", effect: "deny" },
|
||||
...reading.flatMap((action) =>
|
||||
inherited
|
||||
.filter((rule) => matches(action, rule.action))
|
||||
.map((rule) => ({ ...rule, action })),
|
||||
),
|
||||
...codegraphTools.flatMap((action) =>
|
||||
inherited
|
||||
.filter((rule) => matches(action, rule.action))
|
||||
.map((rule) => ({ ...rule, action })),
|
||||
),
|
||||
...workers.map((resource) => ({
|
||||
action: "subagent",
|
||||
resource,
|
||||
effect: "allow" as const,
|
||||
})),
|
||||
];
|
||||
});
|
||||
});
|
||||
agentTransformReady = true;
|
||||
async function assertMultiWork() {
|
||||
const { data } = await context.plugin.list();
|
||||
if (
|
||||
!data.some(
|
||||
(plugin) =>
|
||||
plugin.id === "stanp-opencode.multi-work" &&
|
||||
plugin.state.status === "active",
|
||||
)
|
||||
)
|
||||
throw new Error("coordinate cannot run as a subagent without Multi-work");
|
||||
}
|
||||
await context.session.hook("context", (event) => {
|
||||
if (workerNames.some((name) => name === event.agent)) {
|
||||
delete event.tools.subagent;
|
||||
return;
|
||||
}
|
||||
const tool = event.tools.subagent;
|
||||
if (event.agent === "coordinate" && workers.length === 0) {
|
||||
delete event.tools.subagent;
|
||||
return;
|
||||
}
|
||||
if (event.agent !== "coordinate" || !tool) return;
|
||||
tool.description = `Delegate a self-contained assignment to one of these workers:\n${workers.map((name) => `- ${name}: ${descriptions[name]}`).join("\n")}`;
|
||||
// The executable definition remains native; constrain only this request's schema.
|
||||
const properties = z.record(z.string(), z.unknown()).parse(tool.input.properties ?? {});
|
||||
tool.input = {
|
||||
...tool.input,
|
||||
properties: { ...properties, agent: { type: "string", enum: workers } },
|
||||
};
|
||||
});
|
||||
await context.tool.hook("execute.before", async (event) => {
|
||||
if (event.tool !== "subagent") return;
|
||||
const { agent } = delegation.parse(event.input);
|
||||
if (agent === "coordinate" && event.agent !== "multi-work")
|
||||
throw new Error("coordinate cannot run as a subagent without Multi-work");
|
||||
if (agent === "coordinate") await assertMultiWork();
|
||||
if (workerNames.some((name) => name === event.agent))
|
||||
throw new Error("Workers cannot delegate tasks");
|
||||
if (event.agent === "coordinate" && !active.has(agent))
|
||||
throw new Error("coordinate may delegate only to its available workers");
|
||||
});
|
||||
// Commands can create child sessions without going through the subagent tool.
|
||||
await context.session.hook("prompt", async (event) => {
|
||||
const child = await context.session.get({ sessionID: event.sessionID });
|
||||
if (!child.parentID) return;
|
||||
const parent = await context.session.get({ sessionID: child.parentID });
|
||||
if (child.agent === "coordinate" && parent.agent !== "multi-work")
|
||||
throw new Error("coordinate cannot run as a subagent without Multi-work");
|
||||
if (child.agent === "coordinate") await assertMultiWork();
|
||||
if (workerNames.some((name) => name === parent.agent))
|
||||
throw new Error("Workers cannot delegate tasks");
|
||||
if (
|
||||
parent.agent === "coordinate" &&
|
||||
(child.agent === undefined || !active.has(child.agent))
|
||||
)
|
||||
throw new Error("coordinate may delegate only to its available workers");
|
||||
});
|
||||
return async () => {
|
||||
controller.abort();
|
||||
await updates;
|
||||
await registration.dispose();
|
||||
};
|
||||
},
|
||||
});
|
||||
@@ -0,0 +1,139 @@
|
||||
---
|
||||
name: anti-overengineering
|
||||
description: Prevent unnecessary complexity while planning, implementing, fixing, refactoring, or reviewing code. Use whenever work may add abstractions, dependencies, configuration, generalized frameworks, speculative flexibility, broad rewrites, compatibility for unshipped code, or unrelated cleanup. Also use when the user asks for simple, minimal, surgical, KISS, YAGNI, or the least code.
|
||||
---
|
||||
|
||||
# Anti-overengineering
|
||||
|
||||
Build the smallest correct solution at the right layer. Minimize concepts, moving parts, and maintenance burden—not investigation, correctness, or raw line count.
|
||||
|
||||
## Core invariant
|
||||
|
||||
First understand, then reduce.
|
||||
|
||||
A tiny patch at the wrong layer is not minimal. A short implementation that omits required behavior is not simple. A reusable design with no current reuse is inventory.
|
||||
|
||||
## Correctness floor
|
||||
|
||||
Never simplify by weakening or skipping:
|
||||
|
||||
- explicit user requirements or observable behavior;
|
||||
- root-cause analysis and affected-caller discovery;
|
||||
- trust-boundary validation, authentication, authorization, privacy, or security;
|
||||
- data integrity, error handling that prevents corruption or silent failure, or required concurrency guarantees;
|
||||
- accessibility, compatibility promises, migrations, or public contracts;
|
||||
- measured performance requirements or operationally necessary observability;
|
||||
- the narrowest meaningful verification of the changed behavior.
|
||||
|
||||
Preserve an existing abstraction until you understand why it exists. Fewer lines are useful only when the result is easier to understand and equally correct.
|
||||
|
||||
## Before editing: establish the scope contract
|
||||
|
||||
Capture these facts internally before writing code. State them to the user only when they affect the chosen path.
|
||||
|
||||
1. **Required outcome:** one sentence describing the current behavior the request needs.
|
||||
2. **Current path:** the relevant implementation, callers, local conventions, and root cause for a bug.
|
||||
3. **Invariants:** behavior and boundaries that must remain unchanged.
|
||||
4. **In scope:** the smallest set of responsibilities that must change—not a guessed file count.
|
||||
5. **Out of scope:** cleanup, modernization, and future capabilities not needed for the outcome.
|
||||
6. **Acceptance signal:** the specific runtime scenario, diagnostic, or test that proves the outcome.
|
||||
|
||||
Every changed line must trace to the required outcome, a necessary invariant, or its verification. If it cannot, leave it untouched and mention it separately only when useful.
|
||||
|
||||
## Use the least-power ladder
|
||||
|
||||
Walk the ladder in order. Stop at the first option that completely satisfies the current requirement.
|
||||
|
||||
1. **No new code:** existing behavior, deletion, configuration, documentation, or a workflow change already solves it.
|
||||
2. **Reuse local behavior:** use the repository's existing helper, type, service path, error semantics, and test pattern.
|
||||
3. **Use a standard primitive:** prefer the language standard library, framework primitive, database/query feature, browser API, or platform capability.
|
||||
4. **Use an installed dependency:** do not add a second mechanism the repository already has.
|
||||
5. **Write direct concrete code:** implement the current case in the owning module with the fewest new concepts.
|
||||
6. **Introduce an abstraction:** only after it passes the evidence gate below.
|
||||
|
||||
A shorter expression does not outrank readability. A new dependency must remove more owned complexity than it adds and must solve a current need unavailable from earlier rungs.
|
||||
|
||||
## Make complexity prove its value
|
||||
|
||||
Interfaces, factories, strategies, adapters, registries, plugin systems, generic frameworks, feature flags, configuration knobs, wrappers, and new layers are presumed unnecessary until current evidence justifies them.
|
||||
|
||||
An abstraction is earned when at least one condition is true:
|
||||
|
||||
- multiple current consumers or implementations share the same semantics and the abstraction reduces total concepts now;
|
||||
- a real framework, process, trust, persistence, or public-API boundary requires it;
|
||||
- one authoritative implementation prevents a present consistency, security, or data-integrity risk;
|
||||
- measured performance or test isolation requires the seam and a direct alternative is inadequate.
|
||||
|
||||
Even then, it must have a clear owner, a narrow contract, and less branching than the concrete code it replaces. "We may need it later" is not evidence.
|
||||
|
||||
Apply these existence tests:
|
||||
|
||||
- Every parameter has a current caller that supplies meaningful variation.
|
||||
- Every option has a current use case.
|
||||
- Every branch handles a reachable state.
|
||||
- Every wrapper adds domain semantics, policy, or isolation—not only forwarding.
|
||||
- Every stored or passed value cannot be cheaply derived from values already in scope.
|
||||
- Every compatibility path protects code that has actually shipped or an explicit external contract.
|
||||
|
||||
Prefer two clear copies over a guessed shared abstraction. Extract after the pattern and reason to change are stable—often at the third real occurrence. Textually similar code that changes for different reasons is not duplication. If a shared helper accumulates flags and conditional paths for callers, inline it and let the real boundaries reappear.
|
||||
|
||||
## Implement one present-tense path
|
||||
|
||||
- Fix bugs at the cause, not with repeated caller fallbacks or symptom suppression.
|
||||
- Match established repository structure and style; never introduce a second convention.
|
||||
- Keep a concrete implementation concrete until variation exists.
|
||||
- Do not add unused hooks, optionality, extension points, scaffolding, placeholders, aliases, or `TODO` paths for imagined follow-up work.
|
||||
- Do not preserve compatibility for earlier, unshipped iterations. Update all callers and remove the obsolete path.
|
||||
- Do not mix unrelated cleanup, renames, reformatting, import churn, comment rewrites, type tightening, logging, or dependency updates into the change.
|
||||
- Separate optional structural cleanup from behavioral work. Perform a structural change in the same patch only when it is required for the correct fix.
|
||||
- Prefer obvious control flow and data structures over clever compression. Comments explain non-obvious reasons and constraints, not the conversation that produced the code.
|
||||
- Write for a maintainer who did not see the prompt, plan, or patch history.
|
||||
|
||||
When the user explicitly requests a broad refactor or new extensibility, that is current scope—not speculation. Still implement only the requested variants and contracts.
|
||||
|
||||
## Reject common rationalizations
|
||||
|
||||
| Rationalization | Required response |
|
||||
| --- | --- |
|
||||
| "We'll need this later." | Add it when the need and constraints exist. |
|
||||
| "This makes it reusable." | Name the second current use; otherwise keep it concrete. |
|
||||
| "This should be configurable." | Name current variation; otherwise keep the value local. |
|
||||
| "A clean architecture needs another layer." | Name the boundary or policy it owns; otherwise follow the existing direct path. |
|
||||
| "A wrapper keeps things consistent." | Put real policy in the wrapper or call the underlying API directly. |
|
||||
| "Defensive code is safer." | Prove the state is reachable; retain checks at real trust and integrity boundaries. |
|
||||
| "A local fallback is a smaller bug fix." | Trace the shared path and fix the root cause once. |
|
||||
| "We should clean this up while here." | Leave it untouched unless correctness requires it. |
|
||||
| "Keeping the old path is safer." | Keep it only for shipped behavior or an explicit compatibility contract. |
|
||||
| "More tests are always safer." | Add only tests that defend a changed observable contract or plausible regression. |
|
||||
|
||||
## Final subtraction pass
|
||||
|
||||
After the behavior works, inspect only the changed surface and ask:
|
||||
|
||||
1. Does each changed line trace to the scope contract?
|
||||
2. Did the change add a file, layer, dependency, option, parameter, state field, or public symbol that direct code can avoid?
|
||||
3. Is there now more than one path for the same behavior?
|
||||
4. Can any one-caller helper, forwarding wrapper, dead compatibility path, speculative branch, or explanatory comment disappear?
|
||||
5. Did a refactor leak beyond what the behavioral change requires?
|
||||
6. Does the result make sense without the conversation?
|
||||
7. Is the narrow acceptance signal proven?
|
||||
|
||||
Delete generated scaffolding and obsolete paths. Do not simplify untouched code as a drive-by. Do not remove an existing construct whose purpose remains unknown.
|
||||
|
||||
## Escalation
|
||||
|
||||
Proceed with the smaller correct path by default. Ask one precise question only when simplification would materially alter explicit behavior, a public API, persisted data, a security boundary, compatibility, or another difficult-to-reverse decision.
|
||||
|
||||
Treat an unexpectedly broad change as a diagnostic, not an automatic failure: explain why each responsibility must move, then look again for a narrower correct boundary. Do not use arbitrary line or file limits; optimize reviewer cognitive load and reversibility.
|
||||
|
||||
## Reporting
|
||||
|
||||
Keep implementation reports compact:
|
||||
|
||||
```text
|
||||
Minimal path: [the direct solution and owning layer]
|
||||
Avoided: [notable abstraction, dependency, configuration, or drive-by work omitted]
|
||||
Verified: [specific behavior exercised]
|
||||
```
|
||||
|
||||
Omit `Avoided` when nothing notable was rejected. For reviews, rank only actionable reductions by impact and include the safety caveat that preserves behavior. If no meaningful complexity can be removed, say so rather than inventing findings.
|
||||
@@ -0,0 +1,200 @@
|
||||
import { readFile } from "node:fs/promises";
|
||||
import { homedir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import type { Schema } from "effect";
|
||||
import { z } from "zod";
|
||||
import type { QuotaSnapshot, QuotaStatus, QuotaWindow } from "./quota.js";
|
||||
|
||||
const USAGE_URL = "https://api.anthropic.com/api/oauth/usage";
|
||||
const POLL_MS = 60_000;
|
||||
const TIMEOUT_MS = 5_000;
|
||||
const WEEK = 604_800;
|
||||
|
||||
type QuotaFetch = (input: string | URL | Request, init?: RequestInit) => Promise<Response>;
|
||||
|
||||
export function claudeCredentialsPath() {
|
||||
return join(process.env.CLAUDE_CONFIG_DIR ?? join(homedir(), ".claude"), ".credentials.json");
|
||||
}
|
||||
|
||||
const credentials = z.object({
|
||||
claudeAiOauth: z.object({
|
||||
accessToken: z.string(),
|
||||
expiresAt: z.number(),
|
||||
subscriptionType: z.string().nullish(),
|
||||
}),
|
||||
});
|
||||
|
||||
const usageWindow = z
|
||||
.object({ utilization: z.number().finite(), resets_at: z.string().nullish() })
|
||||
.nullish();
|
||||
const usagePayload = z.object({
|
||||
five_hour: usageWindow,
|
||||
seven_day: usageWindow,
|
||||
seven_day_opus: usageWindow,
|
||||
seven_day_sonnet: usageWindow,
|
||||
limits: z
|
||||
.array(
|
||||
z.object({
|
||||
kind: z.string(),
|
||||
percent: z.number().finite(),
|
||||
resets_at: z.string().nullish(),
|
||||
scope: z
|
||||
.object({
|
||||
model: z.object({ id: z.string(), display_name: z.string() }).nullish(),
|
||||
})
|
||||
.nullish(),
|
||||
}),
|
||||
)
|
||||
.nullish()
|
||||
.catch(undefined),
|
||||
});
|
||||
|
||||
function window(
|
||||
id: string,
|
||||
seconds: number,
|
||||
used: number,
|
||||
resetsAt: string | null | undefined,
|
||||
label: string | null = null,
|
||||
): QuotaWindow {
|
||||
return {
|
||||
id,
|
||||
label,
|
||||
seconds,
|
||||
used: Math.max(0, Math.min(100, used)),
|
||||
resetsAt: resetsAt ? Date.parse(resetsAt) : Number.NaN,
|
||||
};
|
||||
}
|
||||
|
||||
/** Normalize the Claude OAuth usage payload into quota windows. */
|
||||
export function normalizeClaudeQuota(
|
||||
input: Schema.Json,
|
||||
plan: string,
|
||||
fetchedAt = Date.now(),
|
||||
): QuotaSnapshot | undefined {
|
||||
const parsed = usagePayload.safeParse(input);
|
||||
if (!parsed.success) return;
|
||||
const data = parsed.data;
|
||||
const windows: QuotaWindow[] = [];
|
||||
for (const [key, seconds, label] of [
|
||||
["five_hour", 18_000, null],
|
||||
["seven_day", WEEK, null],
|
||||
["seven_day_opus", WEEK, "Opus"],
|
||||
["seven_day_sonnet", WEEK, "Sonnet"],
|
||||
] as const) {
|
||||
const item = data[key];
|
||||
if (item)
|
||||
windows.push(window(`claude:${key}`, seconds, item.utilization, item.resets_at, label));
|
||||
}
|
||||
for (const limit of data.limits ?? []) {
|
||||
const model = limit.scope?.model;
|
||||
if (limit.kind !== "weekly_scoped" || !model) continue;
|
||||
if (windows.some((item) => item.label === model.display_name)) continue;
|
||||
windows.push(
|
||||
window(
|
||||
`claude:weekly_scoped:${model.id}`,
|
||||
WEEK,
|
||||
limit.percent,
|
||||
limit.resets_at,
|
||||
model.display_name,
|
||||
),
|
||||
);
|
||||
}
|
||||
if (windows.length === 0) return;
|
||||
return { plan: plan.toLowerCase(), fetchedAt, windows };
|
||||
}
|
||||
|
||||
export class ClaudeQuotaTracker {
|
||||
private latest: QuotaSnapshot | undefined;
|
||||
private timer: ReturnType<typeof setInterval> | undefined;
|
||||
private request: Promise<void> | undefined;
|
||||
private controller: AbortController | undefined;
|
||||
private disposed = false;
|
||||
|
||||
constructor(
|
||||
private readonly changed: () => Promise<void>,
|
||||
private readonly fetcher: QuotaFetch = fetch,
|
||||
private readonly pollMs = POLL_MS,
|
||||
private readonly path = claudeCredentialsPath(),
|
||||
) {}
|
||||
|
||||
async init() {
|
||||
this.timer = setInterval(() => void this.refresh(), this.pollMs);
|
||||
await this.refresh();
|
||||
}
|
||||
|
||||
refresh(): Promise<void> {
|
||||
if (this.disposed) return Promise.resolve();
|
||||
this.request ??= this.query().finally(() => {
|
||||
this.request = undefined;
|
||||
});
|
||||
return this.request;
|
||||
}
|
||||
|
||||
private async query() {
|
||||
try {
|
||||
let credential: z.infer<typeof credentials>["claudeAiOauth"] | undefined;
|
||||
try {
|
||||
const parsed = credentials.safeParse(JSON.parse(await readFile(this.path, "utf8")));
|
||||
credential = parsed.success ? parsed.data.claudeAiOauth : undefined;
|
||||
} catch {
|
||||
credential = undefined;
|
||||
}
|
||||
if (!credential) {
|
||||
if (this.latest) {
|
||||
this.latest = undefined;
|
||||
await this.changed();
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (credential.expiresAt <= Date.now() || this.disposed) return;
|
||||
const controller = new AbortController();
|
||||
this.controller = controller;
|
||||
const timeout = setTimeout(() => controller.abort(), TIMEOUT_MS);
|
||||
let response: Response;
|
||||
try {
|
||||
response = await this.fetcher(USAGE_URL, {
|
||||
headers: {
|
||||
Authorization: `Bearer ${credential.accessToken}`,
|
||||
"anthropic-beta": "oauth-2025-04-20",
|
||||
Accept: "application/json",
|
||||
},
|
||||
signal: controller.signal,
|
||||
});
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
if (this.controller === controller) this.controller = undefined;
|
||||
}
|
||||
if (!response.ok) return;
|
||||
// SAFETY: Response.json yields JSON values; usagePayload validates fields used.
|
||||
const payload = (await response.json()) as Schema.Json;
|
||||
const snapshot = normalizeClaudeQuota(
|
||||
payload,
|
||||
credential.subscriptionType ?? "unknown",
|
||||
);
|
||||
if (!snapshot || this.disposed) return;
|
||||
this.latest = snapshot;
|
||||
await this.changed();
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
status(): QuotaStatus | undefined {
|
||||
if (!this.latest) return;
|
||||
return {
|
||||
...this.latest,
|
||||
windows: this.latest.windows.map((item) => ({
|
||||
...item,
|
||||
remaining: Math.max(0, 100 - item.used),
|
||||
})),
|
||||
};
|
||||
}
|
||||
|
||||
async dispose() {
|
||||
this.disposed = true;
|
||||
this.controller?.abort();
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
await this.request;
|
||||
}
|
||||
}
|
||||
+4
-10
@@ -24,14 +24,9 @@ const customAssignment = gptAssignment.extend({
|
||||
}, "model must use the provider/model convention"),
|
||||
});
|
||||
|
||||
const common = {
|
||||
playwright: z.boolean().default(false),
|
||||
};
|
||||
|
||||
export const settings = z.discriminatedUnion("mode", [
|
||||
z
|
||||
.strictObject({
|
||||
...common,
|
||||
mode: z.literal("gpt"),
|
||||
provider: z.string().trim().min(1).default("openai"),
|
||||
lunaPriority: z.boolean().optional(),
|
||||
@@ -50,15 +45,14 @@ export const settings = z.discriminatedUnion("mode", [
|
||||
: value;
|
||||
}),
|
||||
z.strictObject({
|
||||
...common,
|
||||
mode: z.literal("custom"),
|
||||
agents: z.strictObject({
|
||||
"fast-lo": customAssignment,
|
||||
"fast-hi": customAssignment,
|
||||
"good-lo": customAssignment,
|
||||
"good-hi": customAssignment,
|
||||
"deep-lo": customAssignment,
|
||||
"deep-hi": customAssignment,
|
||||
"deep-lo": customAssignment.optional(),
|
||||
"deep-hi": customAssignment.optional(),
|
||||
// Deprecated compatibility field. Coordinate is derived from deep-lo/good-lo.
|
||||
coordinate: customAssignment.optional(),
|
||||
}),
|
||||
@@ -68,8 +62,8 @@ export const settings = z.discriminatedUnion("mode", [
|
||||
export const gptAgents = {
|
||||
"fast-lo": { model: "gpt-6-luna", effort: "low" },
|
||||
"fast-hi": { model: "gpt-6-luna", effort: "high" },
|
||||
"good-lo": { model: "gpt-6-sol", effort: "low" },
|
||||
"good-hi": { model: "gpt-6-sol", effort: "high" },
|
||||
"good-lo": { model: "gpt-6.1-sol", effort: "low" },
|
||||
"good-hi": { model: "gpt-6.1-sol", effort: "high" },
|
||||
"deep-lo": { model: "gpt-6-astra", effort: "low" },
|
||||
"deep-hi": { model: "gpt-6-astra", effort: "high" },
|
||||
} satisfies Record<(typeof workerNames)[number], z.infer<typeof gptAssignment>>;
|
||||
|
||||
-429
@@ -1,429 +0,0 @@
|
||||
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, modelKey } 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";
|
||||
import { Plugin as EffectPlugin } from "@opencode/plugin/effect";
|
||||
import { fromPromise } from "@opencode/plugin/promise/adapter";
|
||||
import { Effect } from "effect";
|
||||
import { bindingKey, bindingSchema, installMultiWork } from "./multi-work.js";
|
||||
|
||||
// Permission actions use wildcards, not filesystem globs.
|
||||
function matches(value: string, pattern: string) {
|
||||
const escaped = pattern
|
||||
.replace(/[.+^${}()|[\]\\]/g, "\\$&")
|
||||
.replace(/\*/g, ".*")
|
||||
.replace(/\?/g, ".");
|
||||
return new RegExp(`^${escaped}$`, "s").test(value);
|
||||
}
|
||||
|
||||
const reading = [
|
||||
"read",
|
||||
"glob",
|
||||
"grep",
|
||||
"list",
|
||||
"todoread",
|
||||
"todowrite",
|
||||
"webfetch",
|
||||
"websearch",
|
||||
"question",
|
||||
"external_directory",
|
||||
"doom_loop",
|
||||
];
|
||||
const delegation = z.object({ agent: z.string() });
|
||||
const todos = z.array(
|
||||
z.object({
|
||||
id: z.string(),
|
||||
content: z.string(),
|
||||
status: z.enum(["pending", "in_progress", "completed", "cancelled"]),
|
||||
priority: z.enum(["high", "medium", "low"]),
|
||||
}),
|
||||
);
|
||||
|
||||
const makeBasePlugin = (permissionsChanged: (permissions: Agent.Info["permissions"]) => void) =>
|
||||
Plugin.define({
|
||||
id: "stanp-opencode",
|
||||
async setup(context) {
|
||||
const selected = settings.parse({ mode: "gpt", ...context.options });
|
||||
const lunaPriority =
|
||||
selected.mode === "gpt" &&
|
||||
selected.provider === "openai" &&
|
||||
selected.lunaPriority === true;
|
||||
let models = new Map<string, { model: ModelInfo; serviceTier: string | undefined }>();
|
||||
let inherited: Agent.Info["permissions"] = Agent.Info.default(
|
||||
Agent.ID.make("build"),
|
||||
).permissions;
|
||||
let permissionKey = JSON.stringify(inherited);
|
||||
let hasCodegraph = false;
|
||||
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 () => quota.status() ?? null,
|
||||
});
|
||||
await quota.init();
|
||||
// Subscribe before the first read: configuration can publish its models while
|
||||
// asynchronous setup is running.
|
||||
const updates = (async () => {
|
||||
for await (const event of context.event.subscribe({ signal: controller.signal })) {
|
||||
if (event.location) {
|
||||
const workspaceID =
|
||||
"workspaceID" in event.location
|
||||
? event.location.workspaceID
|
||||
: undefined;
|
||||
if (
|
||||
event.location.directory !== context.location.directory ||
|
||||
workspaceID !== context.location.workspaceID
|
||||
)
|
||||
continue;
|
||||
}
|
||||
if (event.type === "model.updated") {
|
||||
models = await discover(context, lunaPriority);
|
||||
await context.agent.reload();
|
||||
} else if (event.type === "agent.updated") {
|
||||
// The configured build transform may run after ours. Read the final
|
||||
// registry, then replay only when its effective permissions change.
|
||||
const { data } = await context.agent.list();
|
||||
const permissions =
|
||||
data.find((agent) => agent.id === "build")?.permissions ??
|
||||
Agent.Info.default(Agent.ID.make("build")).permissions;
|
||||
const key = JSON.stringify(permissions);
|
||||
if (key === permissionKey) continue;
|
||||
inherited = permissions;
|
||||
permissionsChanged(inherited);
|
||||
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) => {
|
||||
if (!controller.signal.aborted)
|
||||
console.error("stanp-opencode model update failed", error);
|
||||
});
|
||||
models = await discover(context, lunaPriority);
|
||||
const { data: initialAgents } = await context.agent.list();
|
||||
inherited =
|
||||
initialAgents.find((agent) => agent.id === "build")?.permissions ?? inherited;
|
||||
permissionsChanged(inherited);
|
||||
permissionKey = JSON.stringify(inherited);
|
||||
const assignments =
|
||||
selected.mode === "gpt"
|
||||
? Object.fromEntries(
|
||||
workerNames.map((name) => [
|
||||
name,
|
||||
{
|
||||
...gptAgents[name],
|
||||
provider: selected.provider,
|
||||
},
|
||||
]),
|
||||
)
|
||||
: selected.agents;
|
||||
let workers: (typeof workerNames)[number][] = [];
|
||||
const active = new Set<string>();
|
||||
const playwrightCommand = selected.playwright
|
||||
? await playwrightMcpCommand(context.location.directory)
|
||||
: undefined;
|
||||
|
||||
await context.mcp.transform((editor) => {
|
||||
if (playwrightCommand && !editor.get("playwright"))
|
||||
editor.set("playwright", {
|
||||
type: "local",
|
||||
command: playwrightCommand,
|
||||
timeout: { startup: 300_000 },
|
||||
});
|
||||
hasCodegraph = editor.get("codegraph") !== undefined;
|
||||
if (hasCodegraph)
|
||||
editor.update("codegraph", (server) => {
|
||||
server.codemode = false;
|
||||
});
|
||||
});
|
||||
await context.tool.transform((editor) => {
|
||||
const next = editor
|
||||
.list()
|
||||
.map((tool) => tool.id)
|
||||
.filter((id) => id.startsWith("codegraph_"))
|
||||
.sort();
|
||||
if (JSON.stringify(next) === JSON.stringify(codegraphTools)) return;
|
||||
codegraphTools = next;
|
||||
if (agentTransformReady)
|
||||
queueMicrotask(() => {
|
||||
void context.agent
|
||||
.reload()
|
||||
.catch((error) =>
|
||||
console.error("stanp-opencode agent reload failed", error),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
await context.agent.transform((editor) => {
|
||||
const coordinates = (assignment: { model: string }) => {
|
||||
if (selected.mode === "gpt")
|
||||
return { provider: selected.provider, model: assignment.model };
|
||||
const slash = assignment.model.indexOf("/");
|
||||
return {
|
||||
provider: assignment.model.slice(0, slash),
|
||||
model: assignment.model.slice(slash + 1),
|
||||
};
|
||||
};
|
||||
const availableFor = (
|
||||
assignment: (typeof assignments)[(typeof workerNames)[number]],
|
||||
) => {
|
||||
const { provider, model } = coordinates(assignment);
|
||||
return models.get(modelKey(provider, model));
|
||||
};
|
||||
const deep = availableFor(assignments["deep-lo"]) !== undefined;
|
||||
const candidates = workerNames.filter((name) => deep || !name.startsWith("deep-"));
|
||||
const coordinate = assignments[deep ? "deep-lo" : "good-lo"];
|
||||
workers = [coordinate, ...candidates.map((name) => assignments[name])].every(
|
||||
(assignment) => availableFor(assignment) !== undefined,
|
||||
)
|
||||
? candidates
|
||||
: [];
|
||||
active.clear();
|
||||
for (const name of workers) active.add(name);
|
||||
if (workers.length === 0) return;
|
||||
for (const name of workers) {
|
||||
const assignment = assignments[name];
|
||||
const available = availableFor(assignment)!;
|
||||
editor.update(name, (agent) => {
|
||||
const hasVariant = available.model.variants.some(
|
||||
({ id }) => id === assignment.effort,
|
||||
);
|
||||
agent.name = Agent.Name.make(name);
|
||||
agent.mode = "subagent";
|
||||
agent.hidden = false;
|
||||
agent.description = descriptions[name];
|
||||
agent.model = {
|
||||
providerID: Provider.ID.make(available.model.providerID),
|
||||
id: Model.ID.make(available.model.id),
|
||||
};
|
||||
if (hasVariant)
|
||||
agent.model.variant = Model.VariantID.make(assignment.effort);
|
||||
agent.request = {
|
||||
settings: hasVariant ? {} : { reasoningEffort: assignment.effort },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
if (lunaPriority && available.serviceTier)
|
||||
agent.request.settings.serviceTier = available.serviceTier;
|
||||
agent.permissions = [
|
||||
...structuredClone(inherited),
|
||||
{ action: "subagent", resource: "*", effect: "deny" },
|
||||
];
|
||||
});
|
||||
}
|
||||
editor.update("coordinate", (agent) => {
|
||||
const available = availableFor(coordinate)!;
|
||||
const hasVariant = available.model.variants.some(
|
||||
({ id }) => id === coordinate.effort,
|
||||
);
|
||||
agent.name = Agent.Name.make("coordinate");
|
||||
agent.mode = "primary";
|
||||
agent.hidden = false;
|
||||
agent.description =
|
||||
"Read, plan, and coordinate specialist workers; never implement directly.";
|
||||
agent.model = {
|
||||
providerID: Provider.ID.make(available.model.providerID),
|
||||
id: Model.ID.make(available.model.id),
|
||||
};
|
||||
if (hasVariant) agent.model.variant = Model.VariantID.make(coordinate.effort);
|
||||
agent.request = {
|
||||
settings: hasVariant ? {} : { reasoningEffort: coordinate.effort },
|
||||
headers: {},
|
||||
body: {},
|
||||
};
|
||||
if (lunaPriority && available.serviceTier)
|
||||
agent.request.settings.serviceTier = available.serviceTier;
|
||||
agent.system = routingGuide(deep);
|
||||
agent.permissions = [
|
||||
{ action: "*", resource: "*", effect: "deny" },
|
||||
...reading.flatMap((action) =>
|
||||
inherited
|
||||
.filter((rule) => matches(action, rule.action))
|
||||
.map((rule) => ({ ...rule, action })),
|
||||
),
|
||||
...(hasCodegraph
|
||||
? codegraphTools.flatMap((action) =>
|
||||
inherited
|
||||
.filter((rule) => matches(action, rule.action))
|
||||
.map((rule) => ({ ...rule, action })),
|
||||
)
|
||||
: []),
|
||||
...workers.map((resource) => ({
|
||||
action: "subagent",
|
||||
resource,
|
||||
effect: "allow" as const,
|
||||
})),
|
||||
];
|
||||
});
|
||||
});
|
||||
agentTransformReady = true;
|
||||
async function assertTodo(agentID: Agent.ID, action: string) {
|
||||
const { data: agent } = await context.agent.get({ agentID });
|
||||
const effect =
|
||||
agent.permissions.findLast(
|
||||
(rule) => matches(action, rule.action) && matches("*", rule.resource),
|
||||
)?.effect ?? "ask";
|
||||
if (effect !== "allow")
|
||||
throw new Error(
|
||||
`${action} requires an allow permission: OpenCode's public plugin API cannot request custom-tool permission approval (${effect}).`,
|
||||
);
|
||||
}
|
||||
await context.tool.transform((editor) => {
|
||||
if (!editor.get("todowrite"))
|
||||
editor.add({
|
||||
name: "todowrite",
|
||||
description:
|
||||
"Replace this session's todo list to track the work and its progress.",
|
||||
input: z.object({ todos }),
|
||||
options: { codemode: false },
|
||||
async execute(input, tool) {
|
||||
await assertTodo(tool.agent, "todowrite");
|
||||
await context.storage.set(`todos:${tool.sessionID}`, input.todos);
|
||||
return { content: JSON.stringify(input.todos) };
|
||||
},
|
||||
});
|
||||
if (!editor.get("todoread"))
|
||||
editor.add({
|
||||
name: "todoread",
|
||||
description: "Read this session's todo list.",
|
||||
input: z.object({}),
|
||||
options: { codemode: false },
|
||||
async execute(_input, tool) {
|
||||
await assertTodo(tool.agent, "todoread");
|
||||
const stored = await context.storage.get(`todos:${tool.sessionID}`);
|
||||
return {
|
||||
content: JSON.stringify(
|
||||
stored === undefined ? [] : todos.parse(stored),
|
||||
),
|
||||
};
|
||||
},
|
||||
});
|
||||
});
|
||||
await context.session.hook("context", (event) => {
|
||||
if (workerNames.some((name) => name === event.agent)) {
|
||||
delete event.tools.subagent;
|
||||
return;
|
||||
}
|
||||
const tool = event.tools.subagent;
|
||||
if (event.agent === "coordinate" && workers.length === 0) {
|
||||
delete event.tools.subagent;
|
||||
return;
|
||||
}
|
||||
if (event.agent !== "coordinate" || !tool) return;
|
||||
tool.description = `Delegate a self-contained assignment to one of these workers:\n${workers.map((name) => `- ${name}: ${descriptions[name]}`).join("\n")}`;
|
||||
// The executable definition remains native; constrain only this request's schema.
|
||||
const properties = z
|
||||
.record(z.string(), z.unknown())
|
||||
.parse(tool.input.properties ?? {});
|
||||
tool.input = {
|
||||
...tool.input,
|
||||
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);
|
||||
if (agent === "multi-work") throw new Error("multi-work is primary only");
|
||||
if (agent === "coordinate" && event.agent !== "multi-work")
|
||||
throw new Error(
|
||||
"coordinate cannot run as a subagent without a ready multi-work binding",
|
||||
);
|
||||
if (workerNames.some((name) => name === event.agent))
|
||||
throw new Error("Workers cannot delegate tasks");
|
||||
if (event.agent === "coordinate" && !active.has(agent))
|
||||
throw new Error("coordinate may delegate only to its available workers");
|
||||
});
|
||||
// Commands can create child sessions without going through the subagent tool.
|
||||
await context.session.hook("prompt", async (event) => {
|
||||
const child = await context.session.get({ sessionID: event.sessionID });
|
||||
if (!child.parentID) return;
|
||||
if (child.agent === "multi-work") throw new Error("multi-work is primary only");
|
||||
const parent = await context.session.get({ sessionID: child.parentID });
|
||||
if (child.agent === "coordinate") {
|
||||
const binding = bindingSchema.safeParse(
|
||||
await context.storage.get(bindingKey(event.sessionID)),
|
||||
);
|
||||
if (
|
||||
!binding.success ||
|
||||
!binding.data.ready ||
|
||||
binding.data.child !== event.sessionID ||
|
||||
binding.data.owner !== child.parentID ||
|
||||
binding.data.projectID !== child.projectID ||
|
||||
binding.data.projectID !== parent.projectID ||
|
||||
parent.agent !== "multi-work" ||
|
||||
child.location.directory !== binding.data.directory ||
|
||||
parent.location.directory !== binding.data.root.directory
|
||||
)
|
||||
throw new Error(
|
||||
"coordinate cannot run as a subagent without a ready multi-work binding",
|
||||
);
|
||||
}
|
||||
if (
|
||||
parent.agent === "multi-work" &&
|
||||
child.agent !== "coordinate" &&
|
||||
(child.agent !== "fast-lo" ||
|
||||
child.location.directory !== parent.location.directory)
|
||||
)
|
||||
throw new Error(
|
||||
"multi-work may delegate only to local fast-lo or a bound coordinate child",
|
||||
);
|
||||
if (workerNames.some((name) => name === parent.agent))
|
||||
throw new Error("Workers cannot delegate tasks");
|
||||
if (
|
||||
parent.agent === "coordinate" &&
|
||||
(child.agent === undefined || !active.has(child.agent))
|
||||
)
|
||||
throw new Error("coordinate may delegate only to its available workers");
|
||||
});
|
||||
return async () => {
|
||||
controller.abort();
|
||||
await updates;
|
||||
await quota.dispose();
|
||||
await quotaRegistration?.dispose();
|
||||
};
|
||||
},
|
||||
});
|
||||
|
||||
export const basePlugin = makeBasePlugin(() => {});
|
||||
|
||||
export default EffectPlugin.define({
|
||||
id: "stanp-opencode",
|
||||
effect: (context) =>
|
||||
Effect.gen(function* () {
|
||||
let inherited: Agent.Info["permissions"] = Agent.Info.default(
|
||||
Agent.ID.make("build"),
|
||||
).permissions;
|
||||
yield* fromPromise(
|
||||
makeBasePlugin((permissions) => {
|
||||
inherited = permissions;
|
||||
}),
|
||||
).effect(context);
|
||||
yield* installMultiWork(context, () => inherited);
|
||||
}),
|
||||
});
|
||||
+68
-3
@@ -3,7 +3,9 @@ import { Tool } from "@opencode/schema/tool";
|
||||
import { Session } from "@opencode/schema/session";
|
||||
import { AbsolutePath } from "@opencode/schema/schema";
|
||||
import type { Context } from "@opencode/plugin/effect/plugin";
|
||||
import { Cause, Effect, Schedule } from "effect";
|
||||
import { Plugin } from "@opencode/plugin/effect";
|
||||
import { AgentsRpc } from "./agents-rpc.js";
|
||||
import { Cause, Effect, Schedule, Stream } from "effect";
|
||||
import { z } from "zod";
|
||||
import { randomUUID } from "node:crypto";
|
||||
import { Herdr, rootSchema } from "./herdr.js";
|
||||
@@ -97,7 +99,7 @@ export const installMultiWork = (
|
||||
agent.system = `${agent.system ?? ""}\n${childProtocol}`;
|
||||
});
|
||||
if (!eligible) return;
|
||||
// The base plugin captures the final registry after late configuration
|
||||
// Agents captures the final registry after late configuration
|
||||
// transforms and reloads when it changes. Editor.build is too early.
|
||||
const inherited = buildPermissions();
|
||||
editor.update("multi-work", (agent) => {
|
||||
@@ -156,6 +158,34 @@ export const installMultiWork = (
|
||||
return binding;
|
||||
});
|
||||
|
||||
// Commands can create children without going through the subagent tool.
|
||||
yield* context.session.hook("prompt", (event) =>
|
||||
Effect.gen(function* () {
|
||||
const child = yield* context.session.get({ sessionID: event.sessionID });
|
||||
if (!child.parentID) return;
|
||||
if (child.agent === "multi-work") throw new Error("multi-work is primary only");
|
||||
const parent = yield* context.session.get({ sessionID: child.parentID });
|
||||
if (child.agent === "coordinate") yield* validate(event.sessionID);
|
||||
if (
|
||||
parent.agent === "multi-work" &&
|
||||
child.agent !== "coordinate" &&
|
||||
(child.agent !== "fast-lo" ||
|
||||
child.location.directory !== parent.location.directory)
|
||||
)
|
||||
throw new Error(
|
||||
"multi-work may delegate only to local fast-lo or a bound coordinate child",
|
||||
);
|
||||
}).pipe(Effect.orDie),
|
||||
);
|
||||
yield* context.tool.hook("execute.before", (event) => {
|
||||
if (
|
||||
event.tool === "subagent" &&
|
||||
z.object({ agent: z.string() }).parse(event.input).agent === "multi-work"
|
||||
)
|
||||
return Effect.fail(new Tool.Error({ message: "multi-work is primary only" }));
|
||||
return Effect.void;
|
||||
});
|
||||
|
||||
yield* context.session.hook("context", (event) =>
|
||||
Effect.gen(function* () {
|
||||
const session = yield* context.session.get({ sessionID: event.sessionID });
|
||||
@@ -317,7 +347,7 @@ export const installMultiWork = (
|
||||
...call,
|
||||
progress: (metadata) =>
|
||||
Effect.gen(function* () {
|
||||
// Native 2.0.4 awaits this update after creation and before prompt admission.
|
||||
// Native 2.0.22 awaits this update after creation and before prompt admission.
|
||||
yield* call.progress(metadata);
|
||||
if (initialized) return;
|
||||
const id = z.string().parse(metadata.sessionID);
|
||||
@@ -489,3 +519,38 @@ export const installMultiWork = (
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
export default Plugin.define({
|
||||
id: "stanp-opencode.multi-work",
|
||||
effect: (context) =>
|
||||
Effect.gen(function* () {
|
||||
const agents = context.rpc(AgentsRpc);
|
||||
let inherited = yield* agents
|
||||
.permissions({})
|
||||
.pipe(
|
||||
Effect.catch(() =>
|
||||
Effect.die(new Error("Multi-work requires Agents loaded before it")),
|
||||
),
|
||||
);
|
||||
yield* context.event.subscribe().pipe(
|
||||
Stream.filter(
|
||||
(event) =>
|
||||
event.type === "agent.updated" &&
|
||||
(!event.location ||
|
||||
(event.location.directory === context.location.directory &&
|
||||
event.location.workspaceID === context.location.workspaceID)),
|
||||
),
|
||||
Stream.runForEach(() =>
|
||||
Effect.gen(function* () {
|
||||
const next = yield* agents.permissions({});
|
||||
if (JSON.stringify(next) === JSON.stringify(inherited)) return;
|
||||
inherited = next;
|
||||
yield* context.agent.reload();
|
||||
}),
|
||||
),
|
||||
Effect.orDie,
|
||||
Effect.forkScoped,
|
||||
);
|
||||
yield* installMultiWork(context, () => inherited);
|
||||
}).pipe(Effect.orDie),
|
||||
});
|
||||
|
||||
@@ -1,137 +0,0 @@
|
||||
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", "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];
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { Plugin } from "@opencode/plugin";
|
||||
import { ClaudeQuotaTracker } from "./claude-quota.js";
|
||||
import { QuotaTracker } from "./quota.js";
|
||||
import { QuotaRpc } from "./quota-rpc.js";
|
||||
|
||||
export default Plugin.define({
|
||||
id: "stanp-opencode.quota",
|
||||
async setup(context) {
|
||||
const controller = new AbortController();
|
||||
const quota = new QuotaTracker(context, async () => {
|
||||
await registration.events.emit("updated", {});
|
||||
});
|
||||
const claude = new ClaudeQuotaTracker(async () => {
|
||||
await registration.events.emit("updated", {});
|
||||
});
|
||||
const registration = await context.rpc.register(QuotaRpc, {
|
||||
status: async () => quota.status() ?? null,
|
||||
claude: async () => claude.status() ?? null,
|
||||
});
|
||||
void claude.init();
|
||||
await quota.init();
|
||||
const updates = (async () => {
|
||||
for await (const event of context.event.subscribe({ signal: controller.signal })) {
|
||||
if (
|
||||
event.location &&
|
||||
(event.location.directory !== context.location.directory ||
|
||||
("workspaceID" in event.location
|
||||
? event.location.workspaceID
|
||||
: undefined) !== context.location.workspaceID)
|
||||
)
|
||||
continue;
|
||||
if (
|
||||
event.type === "session.execution.succeeded" ||
|
||||
event.type === "session.execution.failed" ||
|
||||
event.type === "session.execution.interrupted" ||
|
||||
event.type === "session.idle"
|
||||
) {
|
||||
await Promise.all([quota.complete(event.data.sessionID), claude.refresh()]);
|
||||
} 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) => {
|
||||
if (!controller.signal.aborted)
|
||||
console.error("stanp-opencode quota update failed", error);
|
||||
});
|
||||
await context.session.hook("model.request", async (event) => quota.start(event.sessionID));
|
||||
return async () => {
|
||||
controller.abort();
|
||||
await updates;
|
||||
await Promise.all([quota.dispose(), claude.dispose()]);
|
||||
await registration.dispose();
|
||||
};
|
||||
},
|
||||
});
|
||||
+6
-6
@@ -10,15 +10,15 @@ const window = z.object({
|
||||
remaining: z.number(),
|
||||
});
|
||||
|
||||
const status = z
|
||||
.object({ plan: z.string(), fetchedAt: z.number(), windows: z.array(window) })
|
||||
.nullable();
|
||||
|
||||
export const QuotaRpc = Rpc.define({
|
||||
id: "stanp-opencode.quota",
|
||||
methods: {
|
||||
status: {
|
||||
input: z.object({}),
|
||||
output: z
|
||||
.object({ plan: z.string(), fetchedAt: z.number(), windows: z.array(window) })
|
||||
.nullable(),
|
||||
},
|
||||
status: { input: z.object({}), output: status },
|
||||
claude: { input: z.object({}), output: status },
|
||||
},
|
||||
events: { updated: { schema: z.object({}) } },
|
||||
});
|
||||
|
||||
+1
-14
@@ -75,7 +75,6 @@ const storedState = z.object({
|
||||
})
|
||||
.optional(),
|
||||
});
|
||||
const legacyStoredState = storedState.extend({ sessions: z.unknown() });
|
||||
type StoredState = z.infer<typeof storedState>;
|
||||
|
||||
function withoutExcludedState(state: StoredState): StoredState {
|
||||
@@ -223,7 +222,6 @@ export class QuotaTracker {
|
||||
private timer: ReturnType<typeof setInterval> | undefined;
|
||||
private request: Promise<QuotaSnapshot | undefined> | undefined;
|
||||
private readonly requests = new Set<Promise<QuotaSnapshot | undefined>>();
|
||||
private readonly migrations = new Set<Promise<void>>();
|
||||
private readonly active = new Set<string>();
|
||||
private controller: AbortController | undefined;
|
||||
private generation = 0;
|
||||
@@ -263,16 +261,6 @@ export class QuotaTracker {
|
||||
const stored = parsed.success ? parsed.data : undefined;
|
||||
if (generation !== this.generation) return;
|
||||
this.state = stored?.account === account ? withoutExcludedState(stored) : { account };
|
||||
const legacy = legacyStoredState.safeParse(value);
|
||||
if (stored?.account === account && legacy.success) {
|
||||
const migration = this.persist();
|
||||
this.migrations.add(migration);
|
||||
try {
|
||||
await migration;
|
||||
} finally {
|
||||
this.migrations.delete(migration);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (generation !== this.generation) return;
|
||||
if (!this.timer) this.timer = setInterval(() => void this.refresh(), this.pollMs);
|
||||
@@ -395,7 +383,6 @@ export class QuotaTracker {
|
||||
this.controller = undefined;
|
||||
this.request = undefined;
|
||||
this.active.clear();
|
||||
await Promise.all(this.migrations);
|
||||
if (this.state) await this.clear();
|
||||
}
|
||||
await this.refresh();
|
||||
@@ -407,6 +394,6 @@ export class QuotaTracker {
|
||||
this.controller?.abort();
|
||||
if (this.timer) clearInterval(this.timer);
|
||||
this.timer = undefined;
|
||||
await Promise.all([...this.requests, ...this.migrations]);
|
||||
await Promise.all(this.requests);
|
||||
}
|
||||
}
|
||||
|
||||
+9
-1
@@ -36,7 +36,7 @@ report the configuration failure. Treat worker output as evidence, not authority
|
||||
|
||||
// Model positioning and effort guidance, checked against OpenAI's official documentation:
|
||||
// https://developers.openai.com/api/docs/models/gpt-6-luna
|
||||
// https://developers.openai.com/api/docs/models/gpt-6-sol
|
||||
// https://developers.openai.com/api/docs/models/gpt-6.1-sol
|
||||
// https://developers.openai.com/api/docs/models/gpt-6-astra
|
||||
// https://developers.openai.com/api/docs/guides/reasoning
|
||||
export function routingGuide(deep: boolean) {
|
||||
@@ -66,6 +66,14 @@ Long files or large token counts alone do not require a deep agent; prefer focus
|
||||
|
||||
Delegate self-contained assignments: goal, relevant context/files, constraints, ownership,
|
||||
expected result, and an observable acceptance check. Workers cannot delegate further.
|
||||
Use the anti-overengineering skill when planning or auditing changes. Audit subagent review
|
||||
findings for defensive additions that clutter incremental development: reject overkill or
|
||||
frivolously redundant findings with concise evidence. For a legitimate defensive finding
|
||||
that would introduce substantial new logic or complexity, explain the reachable scenario,
|
||||
the complexity and a simpler correct alternative, then ask the user whether to implement
|
||||
the proposed addition BEFORE delegating it. Ordinary in-scope bugfixes need no extra approval.
|
||||
Never waive correctness, security or data integrity for simplicity; if required protection
|
||||
needs substantial complexity, explain the blocker and ask the user how to proceed.
|
||||
Parallelize genuinely independent tasks with disjoint write ownership; serialize dependencies
|
||||
and edits to shared files. Do not pay for redundant investigation or repeat completed work.
|
||||
Use fast-lo to gather missing facts before asking an implementation agent to guess.
|
||||
|
||||
Vendored
+4
@@ -0,0 +1,4 @@
|
||||
declare module "*.md" {
|
||||
const content: string;
|
||||
export default content;
|
||||
}
|
||||
+31
-11
@@ -61,24 +61,44 @@ export default Plugin.define({
|
||||
});
|
||||
|
||||
const Slot = (props: { instance: string; centered?: boolean; visible?: () => boolean }) => {
|
||||
const [status] = createResource(
|
||||
const [statuses] = createResource(
|
||||
() => (props.visible?.() === false ? false : `${props.instance}:${revision()}`),
|
||||
() => loadQuotaStatus(() => quota.status({})),
|
||||
async () => {
|
||||
const [chatgpt, claude] = await Promise.all([
|
||||
loadQuotaStatus(() => quota.status({})),
|
||||
loadQuotaStatus(() => quota.claude({})),
|
||||
]);
|
||||
return chatgpt || claude ? { chatgpt, claude } : null;
|
||||
},
|
||||
);
|
||||
const Section = (section: { title: string; status: QuotaStatus | null }) => (
|
||||
<Show when={section.status}>
|
||||
{(status: () => QuotaStatus) => (
|
||||
<>
|
||||
<text fg={context.theme.text.default}>
|
||||
<b>
|
||||
{section.title} quota ({status().plan})
|
||||
</b>
|
||||
</text>
|
||||
<For each={status().windows}>
|
||||
{(window) => (
|
||||
<text fg={context.theme.text.muted}>
|
||||
{formatQuotaWindow(window)}
|
||||
</text>
|
||||
)}
|
||||
</For>
|
||||
</>
|
||||
)}
|
||||
</Show>
|
||||
);
|
||||
const Content = () => (
|
||||
<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>
|
||||
<Section title="ChatGPT" status={statuses()!.chatgpt} />
|
||||
<Section title="Claude" status={statuses()!.claude} />
|
||||
</box>
|
||||
);
|
||||
return (
|
||||
<Show when={status()}>
|
||||
<Show when={statuses()}>
|
||||
<Show when={props.centered} fallback={<Content />}>
|
||||
<box position="absolute" top={2} left={0} width="100%" alignItems="center">
|
||||
<Content />
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import { afterEach, beforeEach, expect, test } from "bun:test";
|
||||
import { mkdtemp, rm, writeFile } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { ClaudeQuotaTracker, normalizeClaudeQuota } from "../src/claude-quota.js";
|
||||
|
||||
const payload = {
|
||||
five_hour: { utilization: 12.5, resets_at: "2026-10-07T12:00:00+00:00" },
|
||||
seven_day: { utilization: 140, resets_at: null },
|
||||
seven_day_opus: null,
|
||||
seven_day_sonnet: { utilization: 3, resets_at: "2026-10-10T00:00:00+00:00" },
|
||||
limits: [
|
||||
{ kind: "session", percent: 12.5, resets_at: "2026-10-07T12:00:00+00:00", scope: null },
|
||||
{
|
||||
kind: "weekly_scoped",
|
||||
percent: 40,
|
||||
resets_at: "2026-10-10T00:00:00+00:00",
|
||||
scope: { model: { id: "claude-fable-5-1", display_name: "Fable" } },
|
||||
},
|
||||
{
|
||||
kind: "weekly_scoped",
|
||||
percent: 3,
|
||||
resets_at: "2026-10-10T00:00:00+00:00",
|
||||
scope: { model: { id: "claude-sonnet-5-5", display_name: "Sonnet" } },
|
||||
},
|
||||
],
|
||||
extra_usage: { is_enabled: false },
|
||||
};
|
||||
|
||||
let dir: string;
|
||||
let path: string;
|
||||
beforeEach(async () => {
|
||||
dir = await mkdtemp(join(tmpdir(), "claude-quota-"));
|
||||
path = join(dir, ".credentials.json");
|
||||
});
|
||||
afterEach(async () => {
|
||||
await rm(dir, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
function credentials(expiresAt: number) {
|
||||
return JSON.stringify({
|
||||
claudeAiOauth: {
|
||||
accessToken: "token-123",
|
||||
refreshToken: "refresh",
|
||||
expiresAt,
|
||||
scopes: ["user:inference"],
|
||||
subscriptionType: "Max",
|
||||
rateLimitTier: "default_claude_max_20x",
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
test("normalizes Claude usage windows", () => {
|
||||
expect(normalizeClaudeQuota(payload, "Max", 1)).toEqual({
|
||||
plan: "max",
|
||||
fetchedAt: 1,
|
||||
windows: [
|
||||
{
|
||||
id: "claude:five_hour",
|
||||
label: null,
|
||||
seconds: 18_000,
|
||||
used: 12.5,
|
||||
resetsAt: Date.parse("2026-10-07T12:00:00Z"),
|
||||
},
|
||||
{
|
||||
id: "claude:seven_day",
|
||||
label: null,
|
||||
seconds: 604_800,
|
||||
used: 100,
|
||||
resetsAt: Number.NaN,
|
||||
},
|
||||
{
|
||||
id: "claude:seven_day_sonnet",
|
||||
label: "Sonnet",
|
||||
seconds: 604_800,
|
||||
used: 3,
|
||||
resetsAt: Date.parse("2026-10-10T00:00:00Z"),
|
||||
},
|
||||
{
|
||||
id: "claude:weekly_scoped:claude-fable-5-1",
|
||||
label: "Fable",
|
||||
seconds: 604_800,
|
||||
used: 40,
|
||||
resetsAt: Date.parse("2026-10-10T00:00:00Z"),
|
||||
},
|
||||
],
|
||||
});
|
||||
expect(normalizeClaudeQuota({ five_hour: null, seven_day: null }, "pro")).toBeUndefined();
|
||||
});
|
||||
|
||||
test("reads credentials file and sends OAuth usage request", async () => {
|
||||
await writeFile(path, credentials(Date.now() + 3_600_000));
|
||||
const calls: Array<{ url: string; headers: Headers }> = [];
|
||||
let changed = 0;
|
||||
const tracker = new ClaudeQuotaTracker(
|
||||
async () => {
|
||||
changed++;
|
||||
},
|
||||
async (input, init) => {
|
||||
calls.push({ url: String(input), headers: new Headers(init?.headers) });
|
||||
return Response.json(payload);
|
||||
},
|
||||
60_000,
|
||||
path,
|
||||
);
|
||||
await tracker.init();
|
||||
expect(calls).toHaveLength(1);
|
||||
expect(calls[0]!.url).toBe("https://api.anthropic.com/api/oauth/usage");
|
||||
expect(calls[0]!.headers.get("authorization")).toBe("Bearer token-123");
|
||||
expect(calls[0]!.headers.get("anthropic-beta")).toBe("oauth-2025-04-20");
|
||||
expect(calls[0]!.headers.get("accept")).toBe("application/json");
|
||||
expect(tracker.status()?.plan).toBe("max");
|
||||
expect(tracker.status()?.windows[0]?.remaining).toBe(87.5);
|
||||
expect(changed).toBe(1);
|
||||
|
||||
await rm(path);
|
||||
await tracker.refresh();
|
||||
expect(tracker.status()).toBeUndefined();
|
||||
expect(changed).toBe(2);
|
||||
await tracker.dispose();
|
||||
});
|
||||
|
||||
test("missing credentials file yields no status", async () => {
|
||||
let calls = 0;
|
||||
const tracker = new ClaudeQuotaTracker(
|
||||
async () => {},
|
||||
async () => {
|
||||
calls++;
|
||||
return Response.json(payload);
|
||||
},
|
||||
60_000,
|
||||
path,
|
||||
);
|
||||
await tracker.init();
|
||||
expect(calls).toBe(0);
|
||||
expect(tracker.status()).toBeUndefined();
|
||||
await tracker.dispose();
|
||||
});
|
||||
|
||||
test("expired token skips fetch and keeps last snapshot", async () => {
|
||||
await writeFile(path, credentials(Date.now() + 3_600_000));
|
||||
let calls = 0;
|
||||
const tracker = new ClaudeQuotaTracker(
|
||||
async () => {},
|
||||
async () => {
|
||||
calls++;
|
||||
return Response.json(payload);
|
||||
},
|
||||
60_000,
|
||||
path,
|
||||
);
|
||||
await tracker.init();
|
||||
await writeFile(path, credentials(Date.now() - 1));
|
||||
await tracker.refresh();
|
||||
expect(calls).toBe(1);
|
||||
expect(tracker.status()?.windows).toHaveLength(4);
|
||||
await tracker.dispose();
|
||||
});
|
||||
+61
-9
@@ -11,9 +11,21 @@ import { AbsolutePath } from "@opencode/schema/schema";
|
||||
import type { Context } from "@opencode/plugin/effect/plugin";
|
||||
import type { AgentEditor } from "@opencode/plugin/effect/agent";
|
||||
import type { ToolEditor } from "@opencode/plugin/effect/tool";
|
||||
import type { SkillEditor } from "@opencode/plugin/effect/skill";
|
||||
import type { SessionHooks } from "@opencode/plugin/effect/session";
|
||||
import { z } from "zod";
|
||||
import plugin from "../src/index.js";
|
||||
import agentsPlugin from "../src/agents.js";
|
||||
import multiWorkPlugin from "../src/multi-work.js";
|
||||
import { fromPromise } from "@opencode/plugin/promise/adapter";
|
||||
import { AgentsRpc } from "../src/agents-rpc.js";
|
||||
|
||||
const plugin = {
|
||||
effect: (context: Context) =>
|
||||
Effect.gen(function* () {
|
||||
yield* fromPromise(agentsPlugin).effect(context);
|
||||
yield* multiWorkPlugin.effect(context);
|
||||
}),
|
||||
};
|
||||
import { command } from "../src/herdr.js";
|
||||
|
||||
// This is a composite plugin host fixture, not OpenCode's native executor/job runtime.
|
||||
@@ -126,6 +138,7 @@ function host(fixture: Awaited<ReturnType<typeof scenario>>, directory: string)
|
||||
PubSub.unbounded<Schema.Schema.Type<typeof Agent.Event.Updated>>(),
|
||||
);
|
||||
const registration = Effect.succeed({ dispose: Effect.void });
|
||||
let permissions: (() => Effect.Effect<Agent.Info["permissions"]>) | undefined;
|
||||
const editAgents: AgentEditor = {
|
||||
get: (id) => agents.get(id),
|
||||
list: () => [...agents.values()],
|
||||
@@ -218,7 +231,7 @@ function host(fixture: Awaited<ReturnType<typeof scenario>>, directory: string)
|
||||
const store = (key: string) =>
|
||||
join(fixture.directory, "store", `${encodeURIComponent(key)}.json`);
|
||||
const providerID = Provider.ID.make("fixture");
|
||||
const models = ["gpt-6-luna", "gpt-6-sol", "gpt-6-astra"].map((id) => ({
|
||||
const models = ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) => ({
|
||||
...Model.Info.default(providerID, Model.ID.make(id)),
|
||||
enabled: true,
|
||||
}));
|
||||
@@ -227,15 +240,47 @@ function host(fixture: Awaited<ReturnType<typeof scenario>>, directory: string)
|
||||
const context = {
|
||||
options: { provider: "fixture" },
|
||||
location,
|
||||
app: { version: "2.0.4-fixture" },
|
||||
app: { version: "2.0.22-fixture" },
|
||||
command: {},
|
||||
experimental: { terminal: {} },
|
||||
generate: {},
|
||||
provider: {},
|
||||
permission: {},
|
||||
plugin: {},
|
||||
plugin: {
|
||||
list: () =>
|
||||
Effect.succeed({
|
||||
location,
|
||||
data: [
|
||||
{
|
||||
id: "stanp-opencode.multi-work",
|
||||
source: { type: "builtin" },
|
||||
state: { status: "active" },
|
||||
features: { server: true },
|
||||
},
|
||||
],
|
||||
}),
|
||||
},
|
||||
reference: {},
|
||||
skill: {},
|
||||
skill: {
|
||||
transform: (callback: (editor: SkillEditor) => void) =>
|
||||
Effect.sync(() => {
|
||||
const skills = new Map<string, NonNullable<ReturnType<SkillEditor["get"]>>>();
|
||||
callback({
|
||||
list: () => [...skills.values()],
|
||||
get: (id) => skills.get(id),
|
||||
add: (skill) => {
|
||||
skills.set(skill.id, skill);
|
||||
},
|
||||
update(id, update) {
|
||||
const skill = skills.get(id);
|
||||
if (skill) update(skill);
|
||||
},
|
||||
remove: (id) => {
|
||||
skills.delete(id);
|
||||
},
|
||||
});
|
||||
}).pipe(Effect.andThen(registration)),
|
||||
},
|
||||
vcs: { branch: {} },
|
||||
websearch: {},
|
||||
worktree: {},
|
||||
@@ -250,10 +295,17 @@ function host(fixture: Awaited<ReturnType<typeof scenario>>, directory: string)
|
||||
resolve: () => Effect.succeed(undefined),
|
||||
},
|
||||
},
|
||||
rpc: {
|
||||
register: () =>
|
||||
Effect.succeed({ dispose: Effect.void, events: { emit: () => Effect.void } }),
|
||||
},
|
||||
rpc: Object.assign(() => ({ permissions: () => permissions!() }), {
|
||||
register: (
|
||||
definition: typeof AgentsRpc,
|
||||
handlers: { permissions: () => Effect.Effect<Agent.Info["permissions"]> },
|
||||
) =>
|
||||
Effect.sync(() => {
|
||||
expect(definition.id).toBe(AgentsRpc.id);
|
||||
permissions = handlers.permissions;
|
||||
return { dispose: Effect.void, events: { emit: () => Effect.void } };
|
||||
}),
|
||||
}),
|
||||
mcp: {
|
||||
transform: (callback: (editor: { get: () => undefined }) => void) =>
|
||||
Effect.sync(() => callback({ get: () => undefined })).pipe(
|
||||
|
||||
+27
@@ -0,0 +1,27 @@
|
||||
import { Plugin, Model, Provider } from "@opencode/plugin/effect";
|
||||
import { fromPromise } from "@opencode/plugin/promise/adapter";
|
||||
import { Effect } from "effect";
|
||||
import agents from "../../src/agents.js";
|
||||
|
||||
export default Plugin.define({
|
||||
id: agents.id,
|
||||
effect: (context) =>
|
||||
Effect.gen(function* () {
|
||||
const providerID = Provider.ID.make("fixture");
|
||||
yield* context.provider.transform((editor) =>
|
||||
editor.add({
|
||||
info: {
|
||||
...Provider.Info.empty(providerID),
|
||||
name: "Isolated fixture",
|
||||
activation: "enabled",
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
settings: { baseURL: process.env.FIXTURE_MODEL_URL! },
|
||||
},
|
||||
models: ["gpt-6-luna", "gpt-6.1-sol", "gpt-6-astra"].map((id) =>
|
||||
Model.Info.default(providerID, Model.ID.make(id)),
|
||||
),
|
||||
}),
|
||||
);
|
||||
yield* fromPromise(agents).effect({ ...context, options: { provider: "fixture" } });
|
||||
}),
|
||||
});
|
||||
Vendored
+4
-19
@@ -1,4 +1,4 @@
|
||||
import { Plugin, Agent, Model, Provider } from "@opencode/plugin/effect";
|
||||
import { Plugin, Agent } from "@opencode/plugin/effect";
|
||||
import { Rpc } from "@opencode/plugin/rpc";
|
||||
import { Session } from "@opencode/schema/session";
|
||||
import { SessionMessage } from "@opencode/schema/session-message";
|
||||
@@ -6,7 +6,7 @@ import { Tool } from "@opencode/schema/tool";
|
||||
import { Cause, Effect } from "effect";
|
||||
import { z } from "zod";
|
||||
import { appendFile } from "node:fs/promises";
|
||||
import plugin from "../../src/index.js";
|
||||
import multiWork from "../../src/multi-work.js";
|
||||
|
||||
const probe = Rpc.define({
|
||||
id: "multi-work-test",
|
||||
@@ -24,25 +24,10 @@ const probe = Rpc.define({
|
||||
});
|
||||
|
||||
export default Plugin.define({
|
||||
id: plugin.id,
|
||||
id: multiWork.id,
|
||||
effect: (context) =>
|
||||
Effect.gen(function* () {
|
||||
const providerID = Provider.ID.make("fixture");
|
||||
yield* context.provider.transform((editor) =>
|
||||
editor.add({
|
||||
info: {
|
||||
...Provider.Info.empty(providerID),
|
||||
name: "Isolated fixture",
|
||||
activation: "enabled",
|
||||
package: "@opencode/ai/providers/openai-compatible",
|
||||
settings: { baseURL: process.env.FIXTURE_MODEL_URL! },
|
||||
},
|
||||
models: ["gpt-6-luna", "gpt-6-sol", "gpt-6-astra"].map((id) =>
|
||||
Model.Info.default(providerID, Model.ID.make(id)),
|
||||
),
|
||||
}),
|
||||
);
|
||||
yield* plugin.effect({ ...context, options: { provider: "fixture" } });
|
||||
yield* multiWork.effect(context);
|
||||
let delegate: Tool.Info | undefined;
|
||||
yield* context.tool.transform((editor) => {
|
||||
delegate = editor.get("subagent");
|
||||
|
||||
@@ -1,236 +0,0 @@
|
||||
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"');
|
||||
});
|
||||
|
||||
test("enables Playwright only when requested", async () => {
|
||||
const setup = await fixture();
|
||||
const result = await run(
|
||||
["bash", join(setup.checkout, "install-local.sh"), "--playwright"],
|
||||
setup.cwd,
|
||||
setup.env,
|
||||
);
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
const installed = JSON.parse(await readFile(join(setup.config, "opencode.json"), "utf8"));
|
||||
expect(installed.plugins.at(-1).options.playwright).toBe(true);
|
||||
});
|
||||
|
||||
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);
|
||||
});
|
||||
}
|
||||
+438
-253
@@ -1,296 +1,481 @@
|
||||
import { afterEach, expect, test } from "bun:test";
|
||||
import { createHash } from "node:crypto";
|
||||
import {
|
||||
chmod,
|
||||
mkdir,
|
||||
mkdtemp,
|
||||
readFile,
|
||||
readdir,
|
||||
readlink,
|
||||
rm,
|
||||
stat,
|
||||
symlink,
|
||||
writeFile,
|
||||
} from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { dirname, join } from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { createHash } from "node:crypto";
|
||||
import { install } from "../scripts/install.js";
|
||||
|
||||
const installer = fileURLToPath(new URL("../install.sh", import.meta.url));
|
||||
const directories: string[] = [];
|
||||
const configContents = '{\n // Keep user settings and comments.\n "theme": "dark"\n}\n';
|
||||
const jsonConfig = { permissions: { shell: "ask" }, plugins: [{ package: "existing-plugin" }] };
|
||||
const previousPlugin = 'export default { id: "previous", setup() { return "preserved"; } };\n';
|
||||
const previousTarget = "../stanp-opencode/install.previous";
|
||||
|
||||
afterEach(async () => {
|
||||
await Promise.all(
|
||||
directories.splice(0).map((path) => rm(path, { recursive: true, force: true })),
|
||||
);
|
||||
for (const path of directories.splice(0)) await rm(path, { recursive: true, force: true });
|
||||
});
|
||||
|
||||
async function run(command: string[], cwd: string, env: NodeJS.ProcessEnv) {
|
||||
const child = Bun.spawn(command, {
|
||||
const unrelated = {
|
||||
theme: "dark",
|
||||
permissions: { shell: "ask" },
|
||||
mcp: {
|
||||
servers: {
|
||||
codegraph: { type: "local", command: ["user-codegraph"], codemode: true },
|
||||
browser: { type: "local", command: ["user-browser"] },
|
||||
},
|
||||
},
|
||||
};
|
||||
const names = ["agents", "multi-work", "quota"];
|
||||
const helper = new URL("../scripts/install.ts", import.meta.url);
|
||||
async function run(
|
||||
command: string[],
|
||||
cwd: string,
|
||||
env: NodeJS.ProcessEnv = Bun.env,
|
||||
input?: string,
|
||||
) {
|
||||
const process = Bun.spawn(command, {
|
||||
cwd,
|
||||
env,
|
||||
stdin: input === undefined ? "ignore" : "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
timeout: 20_000,
|
||||
timeout: 30_000,
|
||||
});
|
||||
if (input !== undefined && process.stdin) {
|
||||
process.stdin.write(input);
|
||||
process.stdin.end();
|
||||
}
|
||||
const [exitCode, stdout, stderr] = await Promise.all([
|
||||
child.exited,
|
||||
new Response(child.stdout).text(),
|
||||
new Response(child.stderr).text(),
|
||||
process.exited,
|
||||
new Response(process.stdout).text(),
|
||||
new Response(process.stderr).text(),
|
||||
]);
|
||||
return { exitCode, stdout, stderr };
|
||||
}
|
||||
|
||||
async function install(
|
||||
mode: "native" | "checksum" | "legacy",
|
||||
configured = false,
|
||||
playwright = false,
|
||||
) {
|
||||
const directory = await mkdtemp(join(tmpdir(), "stanp-install-"));
|
||||
async function fixture() {
|
||||
const directory = await mkdtemp(join(tmpdir(), "plugin-install-"));
|
||||
directories.push(directory);
|
||||
const config =
|
||||
mode === "native"
|
||||
? join(directory, "xdg", "opencode")
|
||||
: join(directory, "custom config with spaces");
|
||||
const bin = join(directory, "bin");
|
||||
const fixture = join(directory, "package");
|
||||
await Promise.all([
|
||||
mkdir(bin),
|
||||
mkdir(join(fixture, "dist"), { recursive: true }),
|
||||
mkdir(join(config, "plugins"), { recursive: true }),
|
||||
mkdir(join(config, "stanp-opencode", "install.previous"), { recursive: true }),
|
||||
]);
|
||||
await Promise.all([
|
||||
writeFile(join(config, "opencode.jsonc"), configContents),
|
||||
writeFile(
|
||||
join(config, "opencode.json"),
|
||||
JSON.stringify(
|
||||
configured
|
||||
? {
|
||||
...jsonConfig,
|
||||
plugins: [
|
||||
...jsonConfig.plugins,
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: {
|
||||
mode: "gpt",
|
||||
provider: "existing-provider",
|
||||
lunaPriority: false,
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
: jsonConfig,
|
||||
),
|
||||
),
|
||||
symlink(previousTarget, join(config, "plugins", "stanp-opencode")),
|
||||
writeFile(join(config, "stanp-opencode", "install.previous", "index.js"), previousPlugin),
|
||||
writeFile(
|
||||
join(config, "stanp-opencode", "install.previous", "package.json"),
|
||||
'{"name":"stanp-opencode","type":"module","main":"./index.js"}\n',
|
||||
),
|
||||
writeFile(join(fixture, "index.js"), 'export { default } from "./dist/index.js";\n'),
|
||||
writeFile(
|
||||
join(fixture, "package.json"),
|
||||
JSON.stringify({
|
||||
name: "stanp-opencode",
|
||||
version: "1.0.0",
|
||||
type: "module",
|
||||
main: "./dist/index.js",
|
||||
exports: { ".": "./dist/index.js", "./tui": "./dist/tui.js" },
|
||||
scripts: { postinstall: "bun ./postinstall.js" },
|
||||
}),
|
||||
),
|
||||
writeFile(
|
||||
join(fixture, "postinstall.js"),
|
||||
'await Bun.write(process.env.POSTINSTALL_MARKER, "executed");\n',
|
||||
),
|
||||
writeFile(
|
||||
join(fixture, "dist", "index.js"),
|
||||
'await Bun.write(process.env.IMPORT_MARKER, "imported");\n' +
|
||||
(mode === "legacy"
|
||||
? 'export default { id: "stanp-opencode", async server() { return {}; } };\n'
|
||||
: 'export default { id: "stanp-opencode", effect() { 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,
|
||||
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH ?? "/usr/bin:/bin"}`,
|
||||
HOME: directory,
|
||||
XDG_CONFIG_HOME: join(directory, "xdg"),
|
||||
OPENCODE_CONFIG_DIR: mode === "native" ? "" : config,
|
||||
FIXTURE_DIR: directory,
|
||||
POSTINSTALL_MARKER: join(directory, "postinstall-ran"),
|
||||
IMPORT_MARKER: join(directory, "import-ran"),
|
||||
};
|
||||
const packed = await run(["tar", "-czf", "stanp-opencode.tgz", "package"], directory, env);
|
||||
expect(packed, packed.stderr).toMatchObject({ exitCode: 0 });
|
||||
const digest =
|
||||
mode === "checksum"
|
||||
? "0".repeat(64)
|
||||
: createHash("sha256")
|
||||
.update(await readFile(join(directory, "stanp-opencode.tgz")))
|
||||
.digest("hex");
|
||||
await Promise.all([
|
||||
writeFile(join(directory, "stanp-opencode.tgz.sha256"), `${digest} stanp-opencode.tgz\n`),
|
||||
writeFile(
|
||||
join(directory, "release.json"),
|
||||
JSON.stringify({
|
||||
tag_name: "v1.0.0",
|
||||
assets: ["stanp-opencode.tgz", "stanp-opencode.tgz.sha256"].map((name, id) => ({
|
||||
id: id + 1,
|
||||
name,
|
||||
browser_download_url: `https://fixtures.invalid/${name}`,
|
||||
})),
|
||||
}),
|
||||
),
|
||||
writeFile(
|
||||
join(bin, "curl"),
|
||||
`#!/usr/bin/env bash
|
||||
set -euo pipefail
|
||||
url=""
|
||||
output=""
|
||||
while (($#)); do
|
||||
case "$1" in
|
||||
-o|--output) output="$2"; shift 2 ;;
|
||||
https://*) url="$1"; shift ;;
|
||||
*) shift ;;
|
||||
esac
|
||||
done
|
||||
case "$url" in
|
||||
https://git.krtss.de/api/v1/repos/StanPonomarev/stanp-opencode/releases/latest) source=release.json ;;
|
||||
https://fixtures.invalid/stanp-opencode.tgz?asset_id=1) source=stanp-opencode.tgz ;;
|
||||
https://fixtures.invalid/stanp-opencode.tgz.sha256?asset_id=2) source=stanp-opencode.tgz.sha256 ;;
|
||||
*) printf 'Unexpected fixture URL: %s\\n' "$url" >&2; exit 1 ;;
|
||||
esac
|
||||
if [[ -n "$output" ]]; then
|
||||
cp "$FIXTURE_DIR/$source" "$output"
|
||||
else
|
||||
cat "$FIXTURE_DIR/$source"
|
||||
fi
|
||||
`,
|
||||
),
|
||||
writeFile(
|
||||
join(bin, "sha256sum"),
|
||||
`#!/usr/bin/env bash
|
||||
if [[ "\${1:-}" == -* ]]; then
|
||||
printf 'usage: sha256sum [-bctwz] [files ...]\n' >&2
|
||||
exit 1
|
||||
fi
|
||||
PATH="\${PATH#*:}"
|
||||
export PATH
|
||||
exec sha256sum "$@"
|
||||
`,
|
||||
),
|
||||
]);
|
||||
await Promise.all([chmod(join(bin, "curl"), 0o755), chmod(join(bin, "sha256sum"), 0o755)]);
|
||||
const result = await run(
|
||||
["bash", installer, ...(playwright ? ["--playwright"] : [])],
|
||||
directory,
|
||||
env,
|
||||
const source = join(directory, "package with spaces");
|
||||
const config = join(directory, "config with spaces");
|
||||
await mkdir(join(source, "dist"), { recursive: true });
|
||||
await mkdir(join(source, "scripts"));
|
||||
await mkdir(config);
|
||||
for (const name of names)
|
||||
await writeFile(
|
||||
join(source, "dist", `${name === "quota" ? "quota-plugin" : name}.js`),
|
||||
`export default { id: "stanp-opencode.${name}", setup() {} };`,
|
||||
);
|
||||
return { directory, config, env, result };
|
||||
await writeFile(
|
||||
join(source, "dist", "tui.js"),
|
||||
'export default { id: "stanp-opencode.quota-tui", setup() {} };',
|
||||
);
|
||||
const built = await Bun.build({ entrypoints: [helper.pathname], target: "bun", format: "esm" });
|
||||
expect(built.success).toBe(true);
|
||||
await Bun.write(join(source, "dist", "install.js"), built.outputs[0]!);
|
||||
await writeFile(join(source, "package.json"), '{"type":"module"}');
|
||||
await writeFile(
|
||||
join(config, "opencode.json"),
|
||||
JSON.stringify({ ...unrelated, plugins: [{ package: "before", options: { keep: true } }] }),
|
||||
);
|
||||
await writeFile(join(config, "opencode.jsonc"), "// untouched user config\n{}\n");
|
||||
return { directory, source, config };
|
||||
}
|
||||
async function contents(config: string) {
|
||||
return JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
|
||||
}
|
||||
|
||||
test("installs an importable native plugin and default config under XDG without running scripts", async () => {
|
||||
const { directory, config, env, result } = await install("native");
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
// The generated package path is only known at runtime; exercise its module-loading boundary.
|
||||
const imported = await run(
|
||||
[
|
||||
process.execPath,
|
||||
"-e",
|
||||
"const { default: plugin } = await import(process.argv[1]); console.log(JSON.stringify([plugin.id, plugin.effect()]));",
|
||||
join(config, "plugins", "stanp-opencode"),
|
||||
],
|
||||
directory,
|
||||
env,
|
||||
for (const selection of [
|
||||
"agents",
|
||||
"quota",
|
||||
"agents,quota",
|
||||
"agents,multi-work",
|
||||
"agents,multi-work,quota",
|
||||
]) {
|
||||
test(`installs valid subset ${selection}, preserving unrelated config and MCP`, async () => {
|
||||
const f = await fixture();
|
||||
await install(f.source, f.config, selection);
|
||||
const result = await contents(f.config);
|
||||
expect(result).toMatchObject(unrelated);
|
||||
expect(result.plugins.map((entry: { package: string }) => entry.package)).toEqual([
|
||||
"before",
|
||||
...selection.split(",").map((name) => `./plugins/stanp-opencode/${name}`),
|
||||
]);
|
||||
const target = join(f.config, "plugins", "stanp-opencode");
|
||||
expect((await readdir(target)).sort()).toEqual(selection.split(",").sort());
|
||||
for (const name of selection.split(",")) {
|
||||
const manifest = await Bun.file(join(target, name, "package.json")).json();
|
||||
expect(manifest.exports["./tui"] !== undefined).toBe(name === "quota");
|
||||
expect(await Bun.file(join(target, name, "tui.js")).exists()).toBe(name === "quota");
|
||||
}
|
||||
expect(await readFile(join(f.config, "opencode.jsonc"), "utf8")).toBe(
|
||||
"// untouched user config\n{}\n",
|
||||
);
|
||||
expect(imported, imported.stderr).toMatchObject({
|
||||
exitCode: 0,
|
||||
stdout: '["stanp-opencode","native-ready"]\n',
|
||||
});
|
||||
expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents);
|
||||
expect(JSON.parse(await readFile(join(config, "opencode.json"), "utf8"))).toEqual({
|
||||
$schema: "https://opencode.ai/config.json",
|
||||
plugins: [
|
||||
{ package: "existing-plugin" },
|
||||
}
|
||||
|
||||
test("upgrades retain selection and options; explicit selection replaces only owned registrations", async () => {
|
||||
const f = await fixture();
|
||||
await install(f.source, f.config, "agents,quota");
|
||||
const current = await contents(f.config);
|
||||
current.plugins[1].options = { mode: "gpt", provider: "openai", lunaPriority: false };
|
||||
current.plugins.push({ package: "after", options: { untouched: 1 } });
|
||||
await writeFile(join(f.config, "opencode.json"), JSON.stringify(current));
|
||||
await install(f.source, f.config);
|
||||
expect((await contents(f.config)).plugins).toEqual(current.plugins);
|
||||
await install(f.source, f.config, "quota");
|
||||
expect((await contents(f.config)).plugins).toEqual([
|
||||
current.plugins[0],
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
current.plugins[3],
|
||||
]);
|
||||
});
|
||||
|
||||
test("mixed string/object registrations upgrade without flags and preserve unrelated entries verbatim", async () => {
|
||||
const f = await fixture();
|
||||
const before = "unrelated-plugin";
|
||||
const after = { package: "unrelated-object", options: { keep: [1, true, "value"] } };
|
||||
await writeFile(
|
||||
join(f.config, "opencode.json"),
|
||||
JSON.stringify({
|
||||
...unrelated,
|
||||
plugins: [before, { package: "./plugins/stanp-opencode/quota" }, after],
|
||||
}),
|
||||
);
|
||||
await install(f.source, f.config);
|
||||
expect((await contents(f.config)).plugins).toEqual([
|
||||
before,
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
after,
|
||||
]);
|
||||
await install(f.source, f.config, "agents,quota");
|
||||
const updated = await contents(f.config);
|
||||
expect(updated.plugins).toEqual([
|
||||
before,
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
package: "./plugins/stanp-opencode/agents",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
||||
},
|
||||
],
|
||||
permissions: { shell: "ask" },
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
after,
|
||||
]);
|
||||
expect(updated).toMatchObject(unrelated);
|
||||
});
|
||||
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);
|
||||
expect(await Bun.file(env.POSTINSTALL_MARKER).exists()).toBe(false);
|
||||
}, 60_000);
|
||||
|
||||
test("preserves an existing stanp-opencode configuration", async () => {
|
||||
const { config, result } = await install("native", true);
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
const installed = JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
|
||||
expect(installed.plugins).toEqual([
|
||||
{ package: "existing-plugin" },
|
||||
test("owned string registrations retain their selection and explicit selection removes mixed duplicates", async () => {
|
||||
const f = await fixture();
|
||||
await writeFile(
|
||||
join(f.config, "opencode.json"),
|
||||
JSON.stringify({
|
||||
plugins: [
|
||||
"before",
|
||||
"./plugins/stanp-opencode/agents",
|
||||
"./plugins/stanp-opencode/multi-work",
|
||||
"./plugins/stanp-opencode/quota",
|
||||
{ package: "stanp-opencode/quota" },
|
||||
"after",
|
||||
],
|
||||
}),
|
||||
);
|
||||
await install(f.source, f.config);
|
||||
const retained = await contents(f.config);
|
||||
expect(retained.plugins).toEqual([
|
||||
"before",
|
||||
{
|
||||
package: "./plugins/stanp-opencode/agents",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
||||
},
|
||||
{ package: "./plugins/stanp-opencode/multi-work" },
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
"after",
|
||||
]);
|
||||
await install(f.source, f.config, "quota");
|
||||
expect((await contents(f.config)).plugins).toEqual([
|
||||
"before",
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
"after",
|
||||
]);
|
||||
});
|
||||
|
||||
for (const target of ["./plugins/stanp-opencode", "stanp-opencode@1.0.0", "absolute"]) {
|
||||
test(`owned monolith string ${target} upgrades without a selection`, async () => {
|
||||
const f = await fixture();
|
||||
const packageName =
|
||||
target === "absolute" ? join(f.config, "plugins", "stanp-opencode") : target;
|
||||
await writeFile(
|
||||
join(f.config, "opencode.json"),
|
||||
JSON.stringify({ plugins: ["before", packageName, "after"] }),
|
||||
);
|
||||
await install(f.source, f.config);
|
||||
expect((await contents(f.config)).plugins).toEqual([
|
||||
"before",
|
||||
{
|
||||
package: "./plugins/stanp-opencode/agents",
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: true },
|
||||
},
|
||||
{ package: "./plugins/stanp-opencode/multi-work" },
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
"after",
|
||||
]);
|
||||
});
|
||||
}
|
||||
|
||||
test("local installer retains executable permissions and supports direct invocation", async () => {
|
||||
const f = await fixture();
|
||||
const path = new URL("../install-local.sh", import.meta.url).pathname;
|
||||
expect((await stat(path)).mode & 0o777).toBe(0o755);
|
||||
const result = await run([path, "--plugins", "multi-work"], f.directory);
|
||||
expect(result.exitCode).toBe(2);
|
||||
expect(result.stderr).toContain("Multi-work requires Agents");
|
||||
});
|
||||
|
||||
test("replaces the legacy monolith without duplicates and retains Agents options", async () => {
|
||||
const f = await fixture();
|
||||
await writeFile(
|
||||
join(f.config, "opencode.json"),
|
||||
JSON.stringify({
|
||||
...unrelated,
|
||||
plugins: [
|
||||
{ package: "before" },
|
||||
{
|
||||
package: "./plugins/stanp-opencode",
|
||||
options: {
|
||||
mode: "gpt",
|
||||
provider: "existing-provider",
|
||||
lunaPriority: false,
|
||||
options: { mode: "gpt", provider: "openai", lunaPriority: false },
|
||||
},
|
||||
},
|
||||
]);
|
||||
expect(installed.permissions).toEqual(jsonConfig.permissions);
|
||||
}, 60_000);
|
||||
|
||||
test("enables Playwright only when requested", async () => {
|
||||
const { config, result } = await install("native", false, true);
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
const installed = JSON.parse(await readFile(join(config, "opencode.json"), "utf8"));
|
||||
expect(installed.plugins.at(-1).options.playwright).toBe(true);
|
||||
}, 60_000);
|
||||
|
||||
for (const mode of ["checksum", "legacy"] as const) {
|
||||
test(`${mode} failure preserves the installed plugin in a custom path containing spaces`, async () => {
|
||||
const { directory, config, env, result } = await install(mode);
|
||||
expect(result.exitCode, result.stderr).not.toBe(0);
|
||||
expect(await readlink(join(config, "plugins", "stanp-opencode"))).toBe(previousTarget);
|
||||
expect(await readFile(join(config, "opencode.jsonc"), "utf8")).toBe(configContents);
|
||||
expect(JSON.parse(await readFile(join(config, "opencode.json"), "utf8"))).toEqual(
|
||||
jsonConfig,
|
||||
);
|
||||
expect(
|
||||
await readFile(join(config, "stanp-opencode", "install.previous", "index.js"), "utf8"),
|
||||
).toBe(previousPlugin);
|
||||
expect(await readdir(join(config, "stanp-opencode"))).toEqual(["install.previous"]);
|
||||
expect(await readdir(join(config, "plugins"))).toEqual(["stanp-opencode"]);
|
||||
// Load the preserved package from this test's runtime-selected configuration directory.
|
||||
const imported = await run(
|
||||
[
|
||||
process.execPath,
|
||||
"-e",
|
||||
"const { default: plugin } = await import(process.argv[1]); console.log(plugin.setup());",
|
||||
join(config, "plugins", "stanp-opencode"),
|
||||
{ package: "stanp-opencode" },
|
||||
{ package: "after" },
|
||||
],
|
||||
directory,
|
||||
env,
|
||||
}),
|
||||
);
|
||||
expect(imported, imported.stderr).toMatchObject({ exitCode: 0, stdout: "preserved\n" });
|
||||
expect(await Bun.file(env.POSTINSTALL_MARKER).exists()).toBe(false);
|
||||
expect(await Bun.file(env.IMPORT_MARKER).exists()).toBe(mode === "legacy");
|
||||
}, 60_000);
|
||||
await mkdir(join(f.config, "plugins"));
|
||||
await writeFile(join(f.config, "plugins", "stanp-opencode.js"), "legacy");
|
||||
await install(f.source, f.config);
|
||||
const result = await contents(f.config);
|
||||
expect(result.plugins.map((entry: { package: string }) => entry.package)).toEqual([
|
||||
"before",
|
||||
...names.map((name) => `./plugins/stanp-opencode/${name}`),
|
||||
"after",
|
||||
]);
|
||||
expect(result.plugins[1].options.lunaPriority).toBe(false);
|
||||
expect(await Bun.file(join(f.config, "plugins", "stanp-opencode.js")).exists()).toBe(false);
|
||||
expect(result).toMatchObject(unrelated);
|
||||
});
|
||||
|
||||
test("noninteractive fresh installs require a selection; invalid selections leave config unchanged", async () => {
|
||||
const f = await fixture();
|
||||
const original = await readFile(join(f.config, "opencode.json"), "utf8");
|
||||
await expect(install(f.source, f.config)).rejects.toThrow("requires --plugins");
|
||||
for (const selection of [
|
||||
"multi-work",
|
||||
"multi-work,quota",
|
||||
"",
|
||||
"agents,agents",
|
||||
"personal",
|
||||
"agents,",
|
||||
])
|
||||
await expect(install(f.source, f.config, selection)).rejects.toThrow();
|
||||
expect(await readFile(join(f.config, "opencode.json"), "utf8")).toBe(original);
|
||||
});
|
||||
|
||||
test("interactive Multi-work-only selection offers Agents", async () => {
|
||||
const f = await fixture();
|
||||
const command = `${process.execPath} '${helper.pathname}' '${f.source}' '${f.config}'`;
|
||||
const child = Bun.spawn(["script", "-qec", command, "/dev/null"], {
|
||||
cwd: f.directory,
|
||||
stdin: "pipe",
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
timeout: 10_000,
|
||||
});
|
||||
let output = "";
|
||||
let selected = false;
|
||||
let confirmed = false;
|
||||
const reader = child.stdout.getReader();
|
||||
while (true) {
|
||||
const chunk = await reader.read();
|
||||
if (chunk.done) break;
|
||||
output += new TextDecoder().decode(chunk.value);
|
||||
if (!selected && output.includes("Plugins (")) {
|
||||
child.stdin.write("multi-work\n");
|
||||
selected = true;
|
||||
}
|
||||
if (!confirmed && output.includes("Add Agents?")) {
|
||||
child.stdin.write("y\n");
|
||||
confirmed = true;
|
||||
}
|
||||
}
|
||||
expect(await child.exited, output).toBe(0);
|
||||
child.stdin.end();
|
||||
expect(output).toContain("Add Agents?");
|
||||
expect(
|
||||
(await contents(f.config)).plugins
|
||||
.slice(1)
|
||||
.map((entry: { package: string }) => entry.package),
|
||||
).toEqual(["./plugins/stanp-opencode/agents", "./plugins/stanp-opencode/multi-work"]);
|
||||
});
|
||||
|
||||
test("artifact validation failure leaves prior installation and config intact", async () => {
|
||||
const f = await fixture();
|
||||
await install(f.source, f.config, "quota");
|
||||
const original = await readFile(join(f.config, "opencode.json"), "utf8");
|
||||
await writeFile(
|
||||
join(f.source, "dist", "agents.js"),
|
||||
'export default { id: "wrong", server() {} };',
|
||||
);
|
||||
// Import in a new process to avoid the runtime's module cache from prior fixture validation.
|
||||
const result = await run(
|
||||
[process.execPath, helper.pathname, f.source, f.config, "agents"],
|
||||
f.directory,
|
||||
);
|
||||
expect(result.exitCode).not.toBe(0);
|
||||
expect(await readFile(join(f.config, "opencode.json"), "utf8")).toBe(original);
|
||||
expect(
|
||||
await Bun.file(join(f.config, "plugins", "stanp-opencode", "quota", "index.js")).exists(),
|
||||
).toBe(true);
|
||||
expect(await readdir(join(f.config, "plugins"))).toEqual(["stanp-opencode"]);
|
||||
});
|
||||
|
||||
test("replaces an owned installation symlink without touching its target or unrelated plugin directories", async () => {
|
||||
const f = await fixture();
|
||||
const external = join(f.directory, "old installation");
|
||||
const sibling = join(f.config, "plugins", "unrelated");
|
||||
await mkdir(external);
|
||||
await mkdir(sibling, { recursive: true });
|
||||
await writeFile(join(external, "keep.txt"), "old work is untouched");
|
||||
await writeFile(join(sibling, "index.js"), "unrelated plugin");
|
||||
await symlink(external, join(f.config, "plugins", "stanp-opencode"));
|
||||
await install(f.source, f.config, "quota");
|
||||
expect(await readFile(join(external, "keep.txt"), "utf8")).toBe("old work is untouched");
|
||||
expect(await readFile(join(sibling, "index.js"), "utf8")).toBe("unrelated plugin");
|
||||
expect(
|
||||
await Bun.file(join(f.config, "plugins", "stanp-opencode", "quota", "index.js")).exists(),
|
||||
).toBe(true);
|
||||
});
|
||||
|
||||
test("both shell installers reject explicit Multi-work-only selection before doing work", async () => {
|
||||
const f = await fixture();
|
||||
for (const file of ["install.sh", "install-local.sh"]) {
|
||||
const result = await run(
|
||||
["bash", new URL(`../${file}`, import.meta.url).pathname, "--plugins", "multi-work"],
|
||||
f.directory,
|
||||
);
|
||||
expect(result.exitCode).toBe(2);
|
||||
expect(result.stderr).toContain("Multi-work requires Agents");
|
||||
}
|
||||
expect((await contents(f.config)).plugins).toHaveLength(1);
|
||||
});
|
||||
|
||||
test("local shell installer builds its checkout and upgrades mixed entries without flags", async () => {
|
||||
const f = await fixture();
|
||||
const after = { package: "unrelated-object", options: { keep: true } };
|
||||
await writeFile(
|
||||
join(f.config, "opencode.json"),
|
||||
JSON.stringify({
|
||||
plugins: ["unrelated-plugin", { package: "./plugins/stanp-opencode/quota" }, after],
|
||||
}),
|
||||
);
|
||||
await Bun.write(
|
||||
join(f.source, "install-local.sh"),
|
||||
Bun.file(new URL("../install-local.sh", import.meta.url)),
|
||||
);
|
||||
const bin = join(f.directory, "bin");
|
||||
await mkdir(bin);
|
||||
await writeFile(
|
||||
join(bin, "bun"),
|
||||
`#!/usr/bin/env bash\nset -euo pipefail\nif [[ "$1" == install || "$1" == run ]]; then printf '%s\\n' "$PWD:$*" >> "$CALLS"; exit; fi\nexec "$REAL_BUN" "$@"\n`,
|
||||
);
|
||||
await chmod(join(bin, "bun"), 0o755);
|
||||
const env = {
|
||||
...Bun.env,
|
||||
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH}`,
|
||||
REAL_BUN: process.execPath,
|
||||
CALLS: join(f.directory, "calls"),
|
||||
OPENCODE_CONFIG_DIR: f.config,
|
||||
};
|
||||
const result = await run(["bash", join(f.source, "install-local.sh")], f.directory, env);
|
||||
expect(result, result.stderr).toMatchObject({ exitCode: 0 });
|
||||
expect(await readFile(env.CALLS, "utf8")).toBe(
|
||||
`${f.source}:install --frozen-lockfile\n${f.source}:run build\n`,
|
||||
);
|
||||
expect((await contents(f.config)).plugins).toEqual([
|
||||
"unrelated-plugin",
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
after,
|
||||
]);
|
||||
});
|
||||
|
||||
for (const validChecksum of [true, false])
|
||||
test(`release shell installer verifies packaged checksum (${validChecksum})`, async () => {
|
||||
const f = await fixture();
|
||||
if (validChecksum)
|
||||
await writeFile(
|
||||
join(f.config, "opencode.json"),
|
||||
JSON.stringify({ plugins: ["unrelated-plugin", "./plugins/stanp-opencode/quota"] }),
|
||||
);
|
||||
const archiveRoot = join(f.directory, "archive");
|
||||
await mkdir(archiveRoot);
|
||||
const packageRoot = join(archiveRoot, "package");
|
||||
await mkdir(packageRoot);
|
||||
// Build the release fixture under the actual archive package prefix.
|
||||
await run(["cp", "-R", `${f.source}/.`, packageRoot], f.directory);
|
||||
const archive = join(f.directory, "stanp-opencode.tgz");
|
||||
expect(
|
||||
(await run(["tar", "-czf", archive, "-C", archiveRoot, "package"], f.directory))
|
||||
.exitCode,
|
||||
).toBe(0);
|
||||
const hash = createHash("sha256")
|
||||
.update(new Uint8Array(await Bun.file(archive).arrayBuffer()))
|
||||
.digest("hex");
|
||||
const checksum = join(f.directory, "checksum");
|
||||
await writeFile(checksum, `${validChecksum ? hash : "0".repeat(64)} stanp-opencode.tgz\n`);
|
||||
const release = join(f.directory, "release.json");
|
||||
await writeFile(
|
||||
release,
|
||||
JSON.stringify({
|
||||
tag_name: "v2",
|
||||
assets: [
|
||||
{
|
||||
id: 1,
|
||||
name: "stanp-opencode.tgz",
|
||||
browser_download_url: "https://fixture/archive",
|
||||
},
|
||||
{
|
||||
id: 2,
|
||||
name: "stanp-opencode.tgz.sha256",
|
||||
browser_download_url: "https://fixture/checksum",
|
||||
},
|
||||
],
|
||||
}),
|
||||
);
|
||||
const bin = join(f.directory, "bin");
|
||||
await mkdir(bin);
|
||||
await writeFile(
|
||||
join(bin, "curl"),
|
||||
`#!/usr/bin/env bash\nset -euo pipefail\nfor ((i=1;i<=$#;i++)); do if [[ "\${!i}" == -o ]]; then j=$((i+1)); output="\${!j}"; fi; done\ncase "$*" in *https://fixture/archive*) cp "$ARCHIVE" "$output";; *https://fixture/checksum*) cp "$CHECKSUM" "$output";; *) cp "$RELEASE" "$output";; esac\n`,
|
||||
);
|
||||
await chmod(join(bin, "curl"), 0o755);
|
||||
const result = await run(
|
||||
["bash", new URL("../install.sh", import.meta.url).pathname],
|
||||
f.directory,
|
||||
{
|
||||
...Bun.env,
|
||||
PATH: `${bin}:${dirname(process.execPath)}:${Bun.env.PATH}`,
|
||||
ARCHIVE: archive,
|
||||
CHECKSUM: checksum,
|
||||
RELEASE: release,
|
||||
OPENCODE_CONFIG_DIR: f.config,
|
||||
},
|
||||
);
|
||||
expect(result.exitCode, result.stderr).toBe(validChecksum ? 0 : 1);
|
||||
if (validChecksum)
|
||||
expect((await contents(f.config)).plugins).toEqual([
|
||||
"unrelated-plugin",
|
||||
{ package: "./plugins/stanp-opencode/quota" },
|
||||
]);
|
||||
else {
|
||||
expect(result.stderr).toContain("checksum mismatch");
|
||||
expect((await contents(f.config)).plugins).toHaveLength(1);
|
||||
}
|
||||
});
|
||||
|
||||
@@ -143,6 +143,7 @@ function fixture() {
|
||||
const toolHooks: ((event: ToolHooks["execute.before"]) => Effect.Effect<void, Tool.Error>)[] =
|
||||
[];
|
||||
const hooks: ((event: SessionContext) => Effect.Effect<void>)[] = [];
|
||||
let prompt!: (event: Pick<SessionContext, "sessionID">) => Effect.Effect<void>;
|
||||
let transform!: (editor: AgentEditor) => void;
|
||||
const registration = Effect.succeed({ dispose: Effect.void });
|
||||
const editAgents = (): AgentEditor => ({
|
||||
@@ -214,9 +215,13 @@ function fixture() {
|
||||
}
|
||||
entered.resolve();
|
||||
}),
|
||||
hook: (_name: string, callback: (event: SessionContext) => Effect.Effect<void>) =>
|
||||
hook: (name: string, callback: (event: SessionContext) => Effect.Effect<void>) =>
|
||||
Effect.sync(() => {
|
||||
hooks.push(callback);
|
||||
if (name === "context") hooks.push(callback);
|
||||
if (name === "prompt") {
|
||||
// SAFETY: The prompt admission hook reads only the native session ID.
|
||||
prompt = callback as typeof prompt;
|
||||
}
|
||||
}).pipe(Effect.andThen(registration)),
|
||||
},
|
||||
tool: {
|
||||
@@ -256,6 +261,7 @@ function fixture() {
|
||||
order,
|
||||
entered,
|
||||
hooks,
|
||||
prompt: (event: Pick<SessionContext, "sessionID">) => prompt(event),
|
||||
toolHooks,
|
||||
call,
|
||||
progress,
|
||||
@@ -676,6 +682,45 @@ test("rejects foreign and changed-agent resumes, non-primary multi-work and unbo
|
||||
);
|
||||
});
|
||||
|
||||
test("command-created children require ready owned bindings and cannot bypass placement guards", async () => {
|
||||
const f = fixture();
|
||||
await Effect.runPromise(
|
||||
Effect.scoped(
|
||||
Effect.gen(function* () {
|
||||
yield* f.install;
|
||||
yield* f.invoke();
|
||||
const child = f.sessions.get("ses_child1")!;
|
||||
const binding = bindingSchema.parse(f.storage.get(bindingKey(child.id)));
|
||||
const admit = () => f.prompt({ sessionID: Session.ID.make(child.id) });
|
||||
yield* admit();
|
||||
f.storage.set(bindingKey(child.id), { ...binding, ready: false });
|
||||
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
|
||||
f.storage.set(bindingKey(child.id), binding);
|
||||
child.location.directory = "/relocated";
|
||||
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
|
||||
child.location.directory = binding.directory;
|
||||
child.projectID = "foreign";
|
||||
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
|
||||
child.projectID = binding.projectID;
|
||||
f.parent.location.directory = "/elsewhere";
|
||||
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
|
||||
f.parent.location.directory = binding.root.directory;
|
||||
f.parent.agent = "build";
|
||||
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
|
||||
f.parent.agent = "multi-work";
|
||||
child.agent = "multi-work";
|
||||
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
|
||||
child.agent = "fast-hi";
|
||||
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
|
||||
child.agent = "fast-lo";
|
||||
expect((yield* Effect.exit(admit()))._tag).toBe("Failure");
|
||||
child.location.directory = f.parent.location.directory;
|
||||
yield* admit();
|
||||
}),
|
||||
),
|
||||
);
|
||||
});
|
||||
|
||||
test("ineligible roots do not register multi-work; multi-work cannot execute shell even with session overrides", async () => {
|
||||
const f = fixture();
|
||||
f.herdr.eligible = false;
|
||||
|
||||
@@ -0,0 +1,50 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import { mkdir, mkdtemp, rm } from "node:fs/promises";
|
||||
import { tmpdir } from "node:os";
|
||||
import { join } from "node:path";
|
||||
import { Host } from "@opencode/plugin/host";
|
||||
import { build } from "../scripts/build.js";
|
||||
import { install } from "../scripts/install.js";
|
||||
|
||||
test("packed package has independent native plugins and quota-only TUI discovery", async () => {
|
||||
const directory = await mkdtemp(join(tmpdir(), "plugin-package-"));
|
||||
try {
|
||||
await build();
|
||||
const archive = join(directory, "stanp-opencode.tgz");
|
||||
const pack = Bun.spawn([process.execPath, "pm", "pack", "--filename", archive], {
|
||||
stdout: "pipe",
|
||||
stderr: "pipe",
|
||||
});
|
||||
const output = new Response(pack.stderr).text();
|
||||
expect(await pack.exited, await output).toBe(0);
|
||||
const unpack = Bun.spawn(["tar", "-xzf", archive, "-C", directory], {
|
||||
stdout: "ignore",
|
||||
stderr: "pipe",
|
||||
});
|
||||
expect(await unpack.exited).toBe(0);
|
||||
const source = join(directory, "package");
|
||||
const manifest = await Bun.file(join(source, "package.json")).json();
|
||||
expect(manifest.exports["."]).toBeUndefined();
|
||||
expect(await Bun.file(join(source, "index.js")).exists()).toBe(false);
|
||||
expect(await Bun.file(join(source, "dist", "index.js")).exists()).toBe(false);
|
||||
for (const name of ["agents", "multi-work", "quota"]) {
|
||||
const entrypoint = join(source, manifest.exports[`./${name}`].import);
|
||||
const { default: plugin } = await import(entrypoint);
|
||||
expect(plugin.id).toBe(`stanp-opencode.${name}`);
|
||||
}
|
||||
const config = join(directory, "config");
|
||||
await mkdir(config);
|
||||
await install(source, config, "agents,multi-work,quota");
|
||||
for (const name of ["agents", "multi-work", "quota"]) {
|
||||
const entries = Host.resolve({
|
||||
directory: join(config, "plugins", "stanp-opencode", name),
|
||||
});
|
||||
expect(entries.server).toBeDefined();
|
||||
expect(entries.tui !== undefined).toBe(name === "quota");
|
||||
}
|
||||
const { QuotaRpc } = await import(join(source, manifest.exports["./quota-rpc"].import));
|
||||
expect(QuotaRpc.id).toBe("stanp-opencode.quota");
|
||||
} finally {
|
||||
await rm(directory, { recursive: true, force: true });
|
||||
}
|
||||
}, 30_000);
|
||||
+267
-863
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import type { Plugin } from "@opencode/plugin";
|
||||
import type { Context } from "@opencode/plugin/effect/plugin";
|
||||
import { Effect } from "effect";
|
||||
import quota from "../src/quota-plugin.js";
|
||||
import multiWork from "../src/multi-work.js";
|
||||
import { QuotaRpc } from "../src/quota-rpc.js";
|
||||
import { AgentsRpc } from "../src/agents-rpc.js";
|
||||
|
||||
test("Quota loads independently and retains its RPC ID and nullable status", async () => {
|
||||
let controller: ReadableStreamDefaultController<unknown>;
|
||||
const stream = new ReadableStream({
|
||||
start(value) {
|
||||
controller = value;
|
||||
},
|
||||
});
|
||||
let status: (() => Promise<null>) | undefined;
|
||||
let disposed = false;
|
||||
let modelHook = false;
|
||||
const context = {
|
||||
options: {},
|
||||
location: { directory: "/fixture" },
|
||||
integration: {
|
||||
connection: {
|
||||
async active() {
|
||||
return undefined;
|
||||
},
|
||||
},
|
||||
},
|
||||
event: {
|
||||
subscribe({ signal }: { signal: AbortSignal }) {
|
||||
signal.addEventListener("abort", () => controller.close(), { once: true });
|
||||
return stream;
|
||||
},
|
||||
},
|
||||
rpc: {
|
||||
async register(definition: typeof QuotaRpc, handlers: { status: () => Promise<null> }) {
|
||||
expect(definition.id).toBe("stanp-opencode.quota");
|
||||
status = handlers.status;
|
||||
return {
|
||||
async dispose() {
|
||||
disposed = true;
|
||||
},
|
||||
events: { async emit() {} },
|
||||
};
|
||||
},
|
||||
},
|
||||
session: {
|
||||
async hook(name: string) {
|
||||
expect(name).toBe("model.request");
|
||||
modelHook = true;
|
||||
return { async dispose() {} };
|
||||
},
|
||||
},
|
||||
};
|
||||
// SAFETY: Quota with no credentials uses only these native host services.
|
||||
// Absent agent/model/MCP APIs fail if independence regresses.
|
||||
const setup = quota.setup as (
|
||||
context: Pick<Plugin.Context, "options">,
|
||||
) => ReturnType<typeof quota.setup>;
|
||||
const cleanup = await setup(context);
|
||||
expect(quota.id).toBe("stanp-opencode.quota");
|
||||
expect(await status!()).toBeNull();
|
||||
expect(modelHook).toBe(true);
|
||||
if (cleanup) await cleanup();
|
||||
expect(disposed).toBe(true);
|
||||
});
|
||||
|
||||
test("Multi-work rejects missing Agents before any Herdr operations or registrations", async () => {
|
||||
const context = {
|
||||
rpc: (definition: typeof AgentsRpc) => {
|
||||
expect(definition.id).toBe("stanp-opencode.agents");
|
||||
return { permissions: () => Effect.fail(new Error("RPC not registered")) };
|
||||
},
|
||||
};
|
||||
// SAFETY: Missing Agents fails at the first RPC call; no other host service is accessed.
|
||||
const activate = multiWork.effect as (
|
||||
context: Pick<Context, "options">,
|
||||
) => ReturnType<typeof multiWork.effect>;
|
||||
const result = await Effect.runPromiseExit(
|
||||
Effect.scoped(activate({ ...context, options: {} })),
|
||||
);
|
||||
expect(String(result)).toContain("Multi-work requires Agents loaded before it");
|
||||
});
|
||||
+1
-26
@@ -247,7 +247,7 @@ async function accountFingerprint(id: string) {
|
||||
return Buffer.from(bytes).toString("hex");
|
||||
}
|
||||
|
||||
test("reads old cached snapshots without session estimates and removes excluded windows", async () => {
|
||||
test("reads cached snapshots and removes excluded windows", async () => {
|
||||
const cached = {
|
||||
account: await accountFingerprint("account-one"),
|
||||
latest: {
|
||||
@@ -284,30 +284,6 @@ test("reads old cached snapshots without session estimates and removes excluded
|
||||
},
|
||||
],
|
||||
},
|
||||
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(
|
||||
{
|
||||
@@ -328,7 +304,6 @@ test("reads old cached snapshots without session estimates and removes excluded
|
||||
"codex_luna:primary",
|
||||
]);
|
||||
expect(status?.windows[0]).not.toHaveProperty("sessionConsumed");
|
||||
expect(result.stored.get("chatgpt-quota")).not.toHaveProperty("sessions");
|
||||
expect(result.calls()).toBe(1);
|
||||
await result.tracker.dispose();
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user