diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 0000000..99b0c21 --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,12 @@ +version: 2 +updates: + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 5 diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 2b1fccc..b88e198 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -4,7 +4,7 @@ on: push: branches: [main] pull_request: - branches: [main] + branches: ["**"] permissions: contents: read @@ -12,11 +12,15 @@ permissions: jobs: test: name: test (node ${{ matrix.node }}) - runs-on: ubuntu-latest + runs-on: ${{ matrix.os }} strategy: fail-fast: false matrix: node: ["20.11", "20", "22"] + os: [ubuntu-latest, macos-latest, windows-latest] + include: + - node: "24" + os: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -25,14 +29,20 @@ jobs: with: node-version: ${{ matrix.node }} + - name: Install dependencies + run: npm ci + - name: Unit tests - run: node --test + run: npm test + + - name: TypeScript check + run: npx tsc --noEmit - name: Demo smoke test (deterministic) run: node tools/demo.mjs --ci - name: Pack check - run: npm pack --dry-run + run: npm run pack:check verify-plugin: name: verify plugin under @opentui/solid (bun) @@ -43,11 +53,8 @@ jobs: - name: Setup Bun uses: oven-sh/setup-bun@v2 - # solid-js / @opentui/solid are *optional* peerDependencies, which npm won't - # install even when asked directly; install-peers.mjs works around that by - # installing them in a clean project and copying them into node_modules. - - name: Install peer runtime - run: node tools/install-peers.mjs + - name: Install Node dependencies + run: npm ci - name: Verify plugin module loads + wires correctly - run: bun tools/verify-plugin.mjs + run: npm run verify:plugin diff --git a/.github/workflows/release.yml b/.github/workflows/release.yml index cf988de..5e02ef8 100644 --- a/.github/workflows/release.yml +++ b/.github/workflows/release.yml @@ -33,6 +33,12 @@ jobs: node-version: "20" registry-url: "https://registry.npmjs.org" # writes .npmrc using NODE_AUTH_TOKEN + - name: Install dependencies + run: npm ci + + - name: Setup Bun + uses: oven-sh/setup-bun@v2 + - name: Verify version matches the tag run: | set -euo pipefail @@ -46,7 +52,10 @@ jobs: fi - name: Unit tests - run: node --test + run: npm test + + - name: Verify plugin runtime + run: npm run verify:plugin - name: Demo smoke test (deterministic) run: node tools/demo.mjs --ci @@ -55,7 +64,12 @@ jobs: run: npm pack --dry-run - name: Publish to npm - run: npm publish --provenance --access public + run: | + if [[ "$GITHUB_REF_NAME" == *-* ]]; then + npm publish --provenance --access public --tag next + else + npm publish --provenance --access public + fi env: NODE_AUTH_TOKEN: ${{ secrets.NPM_TOKEN }} diff --git a/.gitignore b/.gitignore index 1ed10b6..1cbd135 100644 --- a/.gitignore +++ b/.gitignore @@ -3,9 +3,8 @@ node_modules/ .pnp .pnp.js -# Lockfiles are intentionally not committed (library convention; CI/tests run -# without an install). Generated locally by `npm install`. -package-lock.json +# npm-shrinkwrap is for published application locks; this package commits +# package-lock.json for reproducible development and CI installs. npm-shrinkwrap.json # Build / cache @@ -32,3 +31,6 @@ yarn-error.log* # Test / coverage artifacts coverage/ .nyc_output/ +*.tgz +*.bak +*.bak.* diff --git a/README.md b/README.md index 81a2507..6455387 100644 --- a/README.md +++ b/README.md @@ -94,7 +94,7 @@ Add it to your OpenCode config dir (e.g. `~/.config/opencode`): ```jsonc // tui.json -{ "$schema": "https://opencode.ai/tui.json", "plugin": ["@devinoldenburg/opencode-tps-meter"] } +{ "$schema": "https://opencode.ai/tui.json", "plugin": ["@devinoldenburg/opencode-tps-meter/tui"] } ``` ```jsonc @@ -112,7 +112,7 @@ Pass options via the OpenCode plugin tuple in `tui.json`: { "$schema": "https://opencode.ai/tui.json", "plugin": [ - ["@devinoldenburg/opencode-tps-meter", { "metric": "output", "gapMs": 1000, "detail": "compact" }] + ["@devinoldenburg/opencode-tps-meter/tui", { "metric": "output", "gapMs": 1000, "detail": "compact" }] ] } ``` diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..970d25e --- /dev/null +++ b/package-lock.json @@ -0,0 +1,1787 @@ +{ + "name": "@devinoldenburg/opencode-tps-meter", + "version": "0.1.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "@devinoldenburg/opencode-tps-meter", + "version": "0.1.1", + "license": "MIT", + "bin": { + "opencode-tps-meter": "scripts/install.mjs", + "opencode-tps-meter-install": "scripts/install.mjs" + }, + "devDependencies": { + "@opencode-ai/plugin": "1.17.6", + "@opentui/solid": "0.4.1", + "@types/node": "^24.0.3", + "solid-js": "1.9.12", + "typescript": "^5.9.2" + }, + "engines": { + "node": ">=20.11" + }, + "peerDependencies": { + "@opencode-ai/plugin": ">=1.15.0", + "@opentui/solid": ">=0.4.1 <1", + "solid-js": ">=1.9.12 <2" + }, + "peerDependenciesMeta": { + "@opencode-ai/plugin": { + "optional": true + }, + "@opentui/solid": { + "optional": true + }, + "solid-js": { + "optional": true + } + } + }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.7.tgz", + "integrity": "sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.29.7", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.7.tgz", + "integrity": "sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.0.tgz", + "integrity": "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.27.1", + "@babel/generator": "^7.28.0", + "@babel/helper-compilation-targets": "^7.27.2", + "@babel/helper-module-transforms": "^7.27.3", + "@babel/helpers": "^7.27.6", + "@babel/parser": "^7.28.0", + "@babel/template": "^7.27.2", + "@babel/traverse": "^7.28.0", + "@babel/types": "^7.28.0", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.7.tgz", + "integrity": "sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==", + "dev": true, + "dependencies": { + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.29.7.tgz", + "integrity": "sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.29.7.tgz", + "integrity": "sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.29.7", + "@babel/helper-validator-option": "^7.29.7", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.29.7.tgz", + "integrity": "sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/helper-replace-supers": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/traverse": "^7.29.7", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.29.7.tgz", + "integrity": "sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.29.7.tgz", + "integrity": "sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.29.7.tgz", + "integrity": "sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.29.7.tgz", + "integrity": "sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.29.7.tgz", + "integrity": "sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.29.7.tgz", + "integrity": "sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.29.7.tgz", + "integrity": "sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==", + "dev": true, + "dependencies": { + "@babel/helper-member-expression-to-functions": "^7.29.7", + "@babel/helper-optimise-call-expression": "^7.29.7", + "@babel/traverse": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.29.7.tgz", + "integrity": "sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==", + "dev": true, + "dependencies": { + "@babel/traverse": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.29.7.tgz", + "integrity": "sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.29.7.tgz", + "integrity": "sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==", + "dev": true, + "dependencies": { + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.7.tgz", + "integrity": "sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.29.7" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.29.7.tgz", + "integrity": "sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.29.7.tgz", + "integrity": "sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.29.7.tgz", + "integrity": "sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.29.7.tgz", + "integrity": "sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.29.7", + "@babel/helper-create-class-features-plugin": "^7.29.7", + "@babel/helper-plugin-utils": "^7.29.7", + "@babel/helper-skip-transparent-expression-wrappers": "^7.29.7", + "@babel/plugin-syntax-typescript": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", + "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.27.1", + "@babel/helper-validator-option": "^7.27.1", + "@babel/plugin-syntax-jsx": "^7.27.1", + "@babel/plugin-transform-modules-commonjs": "^7.27.1", + "@babel/plugin-transform-typescript": "^7.27.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/template": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.29.7.tgz", + "integrity": "sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.7.tgz", + "integrity": "sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.29.7", + "@babel/generator": "^7.29.7", + "@babel/helper-globals": "^7.29.7", + "@babel/parser": "^7.29.7", + "@babel/template": "^7.29.7", + "@babel/types": "^7.29.7", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.7.tgz", + "integrity": "sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "dev": true, + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "dev": true, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "dev": true + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "dev": true, + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.4.tgz", + "integrity": "sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-darwin-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-x64/-/msgpackr-extract-darwin-x64-3.0.4.tgz", + "integrity": "sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm/-/msgpackr-extract-linux-arm-3.0.4.tgz", + "integrity": "sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==", + "cpu": [ + "arm" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-arm64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-arm64/-/msgpackr-extract-linux-arm64-3.0.4.tgz", + "integrity": "sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-linux-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-linux-x64/-/msgpackr-extract-linux-x64-3.0.4.tgz", + "integrity": "sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@msgpackr-extract/msgpackr-extract-win32-x64": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-win32-x64/-/msgpackr-extract-win32-x64-3.0.4.tgz", + "integrity": "sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opencode-ai/plugin": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/@opencode-ai/plugin/-/plugin-1.17.6.tgz", + "integrity": "sha512-xa7MBAP9mFch8yZvpNMo5O1TEITKv2Mvo6+IDiMIl+F0nswLBCE2K/45CLP1pB/p/jpFJd11ZrnnYkMYk0XNVg==", + "dev": true, + "dependencies": { + "@opencode-ai/sdk": "1.17.6", + "effect": "4.0.0-beta.74", + "zod": "4.1.8" + }, + "peerDependencies": { + "@opentui/core": ">=0.3.4", + "@opentui/keymap": ">=0.3.4", + "@opentui/solid": ">=0.3.4" + }, + "peerDependenciesMeta": { + "@opentui/core": { + "optional": true + }, + "@opentui/keymap": { + "optional": true + }, + "@opentui/solid": { + "optional": true + } + } + }, + "node_modules/@opencode-ai/sdk": { + "version": "1.17.6", + "resolved": "https://registry.npmjs.org/@opencode-ai/sdk/-/sdk-1.17.6.tgz", + "integrity": "sha512-kCzdd9Git2sjDjXSHj4mMWuU1RXZ38d7gFEcq2rzlVz7XV6PPwNw8pelWsk99La2CtDkpfoW5lfHnpCaK6/oBQ==", + "dev": true, + "dependencies": { + "cross-spawn": "7.0.6" + } + }, + "node_modules/@opentui/core": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/core/-/core-0.4.1.tgz", + "integrity": "sha512-ejlunoFCGLcghYGdfamI/DlWHsgCTLbuoL2JeOmFuLsN+DM5phje3CQbGR4tpl24cadCgHJQFomjoQ9Htvin+Q==", + "dev": true, + "dependencies": { + "bun-ffi-structs": "0.2.3", + "diff": "9.0.0", + "marked": "17.0.1", + "string-width": "7.2.0", + "strip-ansi": "7.1.2" + }, + "optionalDependencies": { + "@opentui/core-darwin-arm64": "0.4.1", + "@opentui/core-darwin-x64": "0.4.1", + "@opentui/core-linux-arm64": "0.4.1", + "@opentui/core-linux-arm64-musl": "0.4.1", + "@opentui/core-linux-x64": "0.4.1", + "@opentui/core-linux-x64-musl": "0.4.1", + "@opentui/core-win32-arm64": "0.4.1", + "@opentui/core-win32-x64": "0.4.1" + }, + "peerDependencies": { + "web-tree-sitter": "0.25.10" + } + }, + "node_modules/@opentui/core-darwin-arm64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/core-darwin-arm64/-/core-darwin-arm64-0.4.1.tgz", + "integrity": "sha512-ocs73hj9n0zLArOTpUWIXCWU6ERThG+3wQzO78EvfaR4hb5FRrDHGKWTzXpr6ukSKsUtKdztK5XYTPsJ5e3vww==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@opentui/core-darwin-x64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/core-darwin-x64/-/core-darwin-x64-0.4.1.tgz", + "integrity": "sha512-YogRtDBfxGeOkcSHDMsGkKFBIt3cWPMPGNu2AmEN6a5KKjDYwAZCudwbDJaUbZDCJjfAUHz9iXjhJVXJBXs9vQ==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "darwin" + ] + }, + "node_modules/@opentui/core-linux-arm64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-arm64/-/core-linux-arm64-0.4.1.tgz", + "integrity": "sha512-sBZTS1eEGeVSQ8fAmDALKQcT7FckrhK64oHfEO7W0lJ+lXapfJuOKtTM33na54V56GAM9guk4RD4cbPeTXEh4g==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-linux-arm64-musl": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-arm64-musl/-/core-linux-arm64-musl-0.4.1.tgz", + "integrity": "sha512-Eps9qB+vQ/Lel4ZYqMH87Um9oiU17Vu4oWzvRi40Yf+69vA1a3R4D7KUCeY3OxKWnRnwAHkMU9TxNnjKngPH/w==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-linux-x64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-x64/-/core-linux-x64-0.4.1.tgz", + "integrity": "sha512-9/xjYGzX5RdUl0qmGQY0OCayjJ4VffDhsBmApQdseUkMT6LGL3RumI4zPK3Y9vo1fuy6ffLnriLFOktOgutXDg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-linux-x64-musl": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/core-linux-x64-musl/-/core-linux-x64-musl-0.4.1.tgz", + "integrity": "sha512-UYcp8XGX4DZXN+VYUVuCrJkbFMJ0L+VUVu0t5KqqaeJ74fI4NZ+DmwNqPPg1+C+EIzoW4QChlEUdhlZRdiEQiA==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "linux" + ] + }, + "node_modules/@opentui/core-win32-arm64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/core-win32-arm64/-/core-win32-arm64-0.4.1.tgz", + "integrity": "sha512-hkIbpUJcKd4iLetTygPlFS45teOBTto49aXuxNeafYQUNd3ehiSwJENNBlAGULLfq+KP3dMJoWUiOcbuPVOQRA==", + "cpu": [ + "arm64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opentui/core-win32-x64": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/core-win32-x64/-/core-win32-x64-0.4.1.tgz", + "integrity": "sha512-s1kGBcloy4ksl3wFCMqqOUFtXWlTTpzxe6pkLFhFhzgqKsMXHE9pwebiS3pzJkkFUxah5MYG+kbcn2Dw2Gdncg==", + "cpu": [ + "x64" + ], + "dev": true, + "optional": true, + "os": [ + "win32" + ] + }, + "node_modules/@opentui/solid": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@opentui/solid/-/solid-0.4.1.tgz", + "integrity": "sha512-CD3M3mlUFwy2r8fd43MFY8kR7bFO+CfL1LHjV6+adp7T4jpadb9zuhfKKJpMzSdgAqQof+F5ifGMx6RljiCmwQ==", + "dev": true, + "dependencies": { + "@babel/core": "7.28.0", + "@babel/preset-typescript": "7.27.1", + "@opentui/core": "0.4.1", + "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" + } + }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "dev": true + }, + "node_modules/@types/node": { + "version": "24.13.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.13.2.tgz", + "integrity": "sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==", + "dev": true, + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/ansi-regex": { + "version": "6.2.2", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", + "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", + "dev": true, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/ansi-regex?sponsor=1" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions": { + "version": "0.40.7", + "resolved": "https://registry.npmjs.org/babel-plugin-jsx-dom-expressions/-/babel-plugin-jsx-dom-expressions-0.40.7.tgz", + "integrity": "sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "7.18.6", + "@babel/plugin-syntax-jsx": "^7.18.6", + "@babel/types": "^7.20.7", + "html-entities": "2.3.3", + "parse5": "^7.1.2" + }, + "peerDependencies": { + "@babel/core": "^7.20.12" + } + }, + "node_modules/babel-plugin-jsx-dom-expressions/node_modules/@babel/helper-module-imports": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.18.6.tgz", + "integrity": "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/babel-plugin-module-resolver": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/babel-plugin-module-resolver/-/babel-plugin-module-resolver-5.0.2.tgz", + "integrity": "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==", + "dev": true, + "dependencies": { + "find-babel-config": "^2.1.1", + "glob": "^9.3.3", + "pkg-up": "^3.1.0", + "reselect": "^4.1.7", + "resolve": "^1.22.8" + } + }, + "node_modules/babel-preset-solid": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/babel-preset-solid/-/babel-preset-solid-1.9.12.tgz", + "integrity": "sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==", + "dev": true, + "dependencies": { + "babel-plugin-jsx-dom-expressions": "^0.40.6" + }, + "peerDependencies": { + "@babel/core": "^7.0.0", + "solid-js": "^1.9.12" + }, + "peerDependenciesMeta": { + "solid-js": { + "optional": true + } + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.38", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.38.tgz", + "integrity": "sha512-31/02mVB4yuQU6adKk5SlY6m+mxDwUq5KZkyYgnLrrKl7TEm1+3PyDtDBz2kOv/wxZz41GHsvV1A/u6RmiyBvw==", + "dev": true, + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/brace-expansion": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.1.tgz", + "integrity": "sha512-WR1cURNjuvBLMZBMbqM0UoE+WAfdUcEV1ccD8PVBVOI+Z3ND4+SZbN8RsfT2bMuG1qwz5RFvPukSZm5fF2D5eA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/browserslist": { + "version": "4.28.2", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.2.tgz", + "integrity": "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "baseline-browser-mapping": "^2.10.12", + "caniuse-lite": "^1.0.30001782", + "electron-to-chromium": "^1.5.328", + "node-releases": "^2.0.36", + "update-browserslist-db": "^1.2.3" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/bun-ffi-structs": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/bun-ffi-structs/-/bun-ffi-structs-0.2.3.tgz", + "integrity": "sha512-pgJiXP+hEgFo9qG51J6ItfY4ocs3vniwNzJ9WhoakB3QB2GdzQxX2EXssentPYlB2hOfJrTjO6iIQkWYzUodpg==", + "dev": true, + "peerDependencies": { + "typescript": "^5" + } + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001799", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001799.tgz", + "integrity": "sha512-hG1bReV+OUU+MOqK4t/ZWI0tZOyz3rqS9XuhOUz1cIcbwBKjOyJEJuw9ER5JuNyqxNk8u/JUVbGibBOL1yrjFw==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ] + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "dev": true + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/detect-libc": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", + "integrity": "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==", + "dev": true, + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/diff": { + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/effect": { + "version": "4.0.0-beta.74", + "resolved": "https://registry.npmjs.org/effect/-/effect-4.0.0-beta.74.tgz", + "integrity": "sha512-Yx+Kh12U+i2FmjwEfKs+ePFmpMd43RPD1oGqc/VraSS9bYzvF0Ff3PojwEFEVEewp8xc92Uxu28gTspU4qyvHA==", + "dev": true, + "dependencies": { + "@standard-schema/spec": "^1.1.0", + "fast-check": "^4.8.0", + "find-my-way-ts": "^0.1.6", + "ini": "^7.0.0", + "kubernetes-types": "^1.30.0", + "msgpackr": "^2.0.1", + "multipasta": "^0.2.7", + "toml": "^4.1.1", + "uuid": "^14.0.0", + "yaml": "^2.9.0" + } + }, + "node_modules/electron-to-chromium": { + "version": "1.5.376", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.376.tgz", + "integrity": "sha512-cUVA7/RvbFTEuw/i3obUwDTRIXojaxkResf+ibByPFxjc6XK3VNtcQXV0NSbAlJ0FMjcJGgftVVB4Qo184EXvA==", + "dev": true + }, + "node_modules/emoji-regex": { + "version": "10.6.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-10.6.0.tgz", + "integrity": "sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==", + "dev": true + }, + "node_modules/entities": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-7.0.1.tgz", + "integrity": "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "dev": true, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/fast-check": { + "version": "4.8.0", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-4.8.0.tgz", + "integrity": "sha512-GOJ158CUMnN6cSahsv4+ExARvIDuzzinFjkp0E9WtiBa5zcVeLozVkWaE4IzFcc+Y48Wp1EDlUZsXRyAztQcSg==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "dependencies": { + "pure-rand": "^8.0.0" + }, + "engines": { + "node": ">=12.17.0" + } + }, + "node_modules/find-babel-config": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/find-babel-config/-/find-babel-config-2.1.2.tgz", + "integrity": "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==", + "dev": true, + "dependencies": { + "json5": "^2.2.3" + } + }, + "node_modules/find-my-way-ts": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/find-my-way-ts/-/find-my-way-ts-0.1.6.tgz", + "integrity": "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==", + "dev": true + }, + "node_modules/find-up": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-3.0.0.tgz", + "integrity": "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==", + "dev": true, + "dependencies": { + "locate-path": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/get-east-asian-width": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/get-east-asian-width/-/get-east-asian-width-1.6.0.tgz", + "integrity": "sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/glob/-/glob-9.3.5.tgz", + "integrity": "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "minimatch": "^8.0.2", + "minipass": "^4.2.4", + "path-scurry": "^1.6.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/hasown": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", + "dev": true, + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/html-entities": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.3.3.tgz", + "integrity": "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==", + "dev": true + }, + "node_modules/ini": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/ini/-/ini-7.0.0.tgz", + "integrity": "sha512-ifK0CgjALofS5bkrcTy4RaQ9Vx2Knf/eLeIO+NaswQEpH1UblrtTSCIvN71qQDMq0PeQ/SSPojvEJp9vvvfr+w==", + "dev": true, + "engines": { + "node": "^22.22.2 || ^24.15.0 || >=26.0.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.2", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.2.tgz", + "integrity": "sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==", + "dev": true, + "dependencies": { + "hasown": "^2.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "dev": true + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kubernetes-types": { + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/kubernetes-types/-/kubernetes-types-1.30.0.tgz", + "integrity": "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==", + "dev": true + }, + "node_modules/locate-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-3.0.0.tgz", + "integrity": "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==", + "dev": true, + "dependencies": { + "p-locate": "^3.0.0", + "path-exists": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/marked": { + "version": "17.0.1", + "resolved": "https://registry.npmjs.org/marked/-/marked-17.0.1.tgz", + "integrity": "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==", + "dev": true, + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, + "node_modules/minimatch": { + "version": "8.0.7", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-8.0.7.tgz", + "integrity": "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/minipass": { + "version": "4.2.8", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-4.2.8.tgz", + "integrity": "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/msgpackr": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/msgpackr/-/msgpackr-2.0.4.tgz", + "integrity": "sha512-o1C5KRmuRt+apqMr1HuGSqWStZoRBUpEsCsl15uM9VdAF1qHLtvMOU2En747EnTyEl6c4pzPewRMFF31s1CNbA==", + "dev": true, + "optionalDependencies": { + "msgpackr-extract": "^3.0.4" + } + }, + "node_modules/msgpackr-extract": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/msgpackr-extract/-/msgpackr-extract-3.0.4.tgz", + "integrity": "sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "dependencies": { + "node-gyp-build-optional-packages": "5.2.2" + }, + "bin": { + "download-msgpackr-prebuilds": "bin/download-prebuilds.js" + }, + "optionalDependencies": { + "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.4", + "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.4" + } + }, + "node_modules/multipasta": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/multipasta/-/multipasta-0.2.7.tgz", + "integrity": "sha512-KPA58d68KgGil15oDqXjkUBEBYc00XvbPj5/X+dyzeo/lWm9Nc25pQRlf1D+gv4OpK7NM0J1odrbu9JNNGvynA==", + "dev": true + }, + "node_modules/node-gyp-build-optional-packages": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/node-gyp-build-optional-packages/-/node-gyp-build-optional-packages-5.2.2.tgz", + "integrity": "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==", + "dev": true, + "optional": true, + "dependencies": { + "detect-libc": "^2.0.1" + }, + "bin": { + "node-gyp-build-optional-packages": "bin.js", + "node-gyp-build-optional-packages-optional": "optional.js", + "node-gyp-build-optional-packages-test": "build-test.js" + } + }, + "node_modules/node-releases": { + "version": "2.0.48", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.48.tgz", + "integrity": "sha512-1uz8041X6LoI6ZSdZacM9lVY28vuzDlSKitnpbSNK0RfKoIJkX29NBPVEFXhnuSuEOA9Ww0xnPJ+ILWbGAv8DA==", + "dev": true, + "engines": { + "node": ">=18" + } + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-3.0.0.tgz", + "integrity": "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==", + "dev": true, + "dependencies": { + "p-limit": "^2.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/parse5": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", + "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", + "dev": true, + "dependencies": { + "entities": "^6.0.0" + }, + "funding": { + "url": "https://github.com/inikulin/parse5?sponsor=1" + } + }, + "node_modules/parse5/node_modules/entities": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", + "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/path-exists": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-3.0.0.tgz", + "integrity": "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true + }, + "node_modules/path-scurry": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-1.11.1.tgz", + "integrity": "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==", + "dev": true, + "dependencies": { + "lru-cache": "^10.2.0", + "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" + }, + "engines": { + "node": ">=16 || 14 >=14.18" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/path-scurry/node_modules/lru-cache": { + "version": "10.4.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.4.3.tgz", + "integrity": "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==", + "dev": true + }, + "node_modules/path-scurry/node_modules/minipass": { + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", + "dev": true, + "engines": { + "node": ">=16 || 14 >=14.17" + } + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "dev": true + }, + "node_modules/pkg-up": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-3.1.0.tgz", + "integrity": "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==", + "dev": true, + "dependencies": { + "find-up": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/pure-rand": { + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-8.4.0.tgz", + "integrity": "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ] + }, + "node_modules/reselect": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/reselect/-/reselect-4.1.8.tgz", + "integrity": "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==", + "dev": true + }, + "node_modules/resolve": { + "version": "1.22.12", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", + "integrity": "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==", + "dev": true, + "dependencies": { + "es-errors": "^1.3.0", + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/s-js": { + "version": "0.4.9", + "resolved": "https://registry.npmjs.org/s-js/-/s-js-0.4.9.tgz", + "integrity": "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ==", + "dev": true + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/seroval": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval/-/seroval-1.5.4.tgz", + "integrity": "sha512-46uFvgrXTVxZcUorgSSRZ4y+ieqLLQRMlG4bnCZKW3qI6BZm7Rg4ntMW4p1mILEEBZWrFlcpp0AyIIlM6jD9iw==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/seroval-plugins": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/seroval-plugins/-/seroval-plugins-1.5.4.tgz", + "integrity": "sha512-S0xQPhUTefAhNvNWFg0c1J8qJArHt5KdtJ/cFAofo06KD1MVSeFWyl4iiu+ApDIuw0WhjpOfCdgConOfAnLgkw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "seroval": "^1.0" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/solid-js": { + "version": "1.9.12", + "resolved": "https://registry.npmjs.org/solid-js/-/solid-js-1.9.12.tgz", + "integrity": "sha512-QzKaSJq2/iDrWR1As6MHZQ8fQkdOBf8GReYb7L5iKwMGceg7HxDcaOHk0at66tNgn9U2U7dXo8ZZpLIAmGMzgw==", + "dev": true, + "dependencies": { + "csstype": "^3.1.0", + "seroval": "~1.5.0", + "seroval-plugins": "~1.5.0" + } + }, + "node_modules/string-width": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-7.2.0.tgz", + "integrity": "sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==", + "dev": true, + "dependencies": { + "emoji-regex": "^10.3.0", + "get-east-asian-width": "^1.0.0", + "strip-ansi": "^7.1.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/strip-ansi": { + "version": "7.1.2", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", + "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "dev": true, + "dependencies": { + "ansi-regex": "^6.0.1" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/chalk/strip-ansi?sponsor=1" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/toml": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/toml/-/toml-4.1.1.tgz", + "integrity": "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw==", + "dev": true, + "engines": { + "node": ">=20" + } + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/uuid": { + "version": "14.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-14.0.1.tgz", + "integrity": "sha512-6ZxzVpzDXDa3bJWaHilVayA+BH/1zmxCJoVgvmqJnid/gPoKHxUrS/aC/T6LGQtNHT+XHG9fXPJB4d+IrU30Ew==", + "dev": true, + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "bin": { + "uuid": "dist-node/bin/uuid" + } + }, + "node_modules/web-tree-sitter": { + "version": "0.25.10", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.25.10.tgz", + "integrity": "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==", + "dev": true, + "peer": true, + "peerDependencies": { + "@types/emscripten": "^1.40.0" + }, + "peerDependenciesMeta": { + "@types/emscripten": { + "optional": true + } + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, + "node_modules/yaml": { + "version": "2.9.0", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.0.tgz", + "integrity": "sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==", + "dev": true, + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, + "node_modules/zod": { + "version": "4.1.8", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.1.8.tgz", + "integrity": "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } + } + } +} diff --git a/package.json b/package.json index 7f13541..de185b9 100644 --- a/package.json +++ b/package.json @@ -3,9 +3,10 @@ "version": "0.1.1", "description": "A precise tokens-per-second (TPS) meter for the OpenCode TUI sidebar — live throughput, time-to-first-token, and per-session averages, measured from the OpenCode event stream.", "type": "module", - "main": "plugins/tps-meter.tsx", + "main": "plugins/tps/root.js", + "types": "plugins/tps/index.d.ts", "exports": { - ".": "./plugins/tps-meter.tsx", + ".": "./plugins/tps/root.js", "./tui": "./plugins/tps-meter.tsx", "./core": "./plugins/tps/index.js", "./package.json": "./package.json" @@ -71,12 +72,16 @@ "registry": "https://registry.npmjs.org/" }, "devDependencies": { - "@opencode-ai/plugin": "1.17.6" + "@opencode-ai/plugin": "1.17.6", + "@opentui/solid": "0.4.1", + "@types/node": "^24.0.3", + "solid-js": "1.9.12", + "typescript": "^5.9.2" }, "peerDependencies": { "@opencode-ai/plugin": ">=1.15.0", - "@opentui/solid": "*", - "solid-js": "*" + "@opentui/solid": ">=0.4.1 <1", + "solid-js": ">=1.9.12 <2" }, "peerDependenciesMeta": { "@opencode-ai/plugin": { diff --git a/plugins/tps-meter.tsx b/plugins/tps-meter.tsx index 3478157..7f1e339 100644 --- a/plugins/tps-meter.tsx +++ b/plugins/tps-meter.tsx @@ -44,32 +44,37 @@ import { buildView } from "./tps/view.js"; import { resolveConfig, TONE_TO_THEME } from "./tps/config.js"; const id = "opencode-tps-meter"; +type AnyRecord = Record; /** * The per-session sidebar view. One instance is mounted per session slot; it owns * a RateMeter and the calibration state for that session and tears them down on * cleanup. */ -function TpsView(props) { - const cfg = props.cfg; +function TpsView(props: AnyRecord) { + const cfg = props.cfg as AnyRecord; const theme = () => props.api.theme.current; - const toneColor = (tone) => { + const toneColor = (tone: string) => { const override = cfg.colors && cfg.colors[tone]; if (override) return override; const t = theme() || {}; - return t[TONE_TO_THEME[tone] || "text"] || t.text; + const themeKey = (TONE_TO_THEME as Record)[tone] || "text"; + return t[themeKey] || t.text; }; // The sparkline uses a windowed instantaneous rate (visual texture). The PRECISE // numbers come from per-message GenerationTimers, which measure active-generation // time only — excluding tool calls, permission waits, and stalls inside a turn. const meter = new RateMeter({ windowMs: cfg.windowMs, seriesLength: cfg.seriesLength }); - const timers = new Map(); // messageID -> GenerationTimer (active-gen time, gap-excluded) - const partLen = new Map(); // partID -> last observed text length - let ratio = DEFAULT_CHARS_PER_TOKEN; // chars/token, self-calibrated per model - let currentMsgId = null; // message currently streaming (for the live headline) + const timers = new Map(); // messageID -> active-gen timer + const partLen = new Map(); + const ratioByModel = new Map(); // modelID/providerID -> chars/token calibration + let currentMsgId: string | null = null; // most recently observed streaming message - const timerFor = (messageID) => { + const modelKey = (info: AnyRecord | null | undefined) => `${info?.providerID ?? ""}/${info?.modelID ?? ""}`; + const ratioFor = (info: AnyRecord | null | undefined) => ratioByModel.get(modelKey(info)) ?? DEFAULT_CHARS_PER_TOKEN; + + const timerFor = (messageID: string) => { let t = timers.get(messageID); if (!t) { t = new GenerationTimer({ gapThresholdMs: cfg.gapMs }); @@ -77,7 +82,7 @@ function TpsView(props) { } return t; }; - const timingFor = (messageID) => { + const timingFor = (messageID: string) => { const t = timers.get(messageID); return t ? { firstTokenAt: t.firstAt, activeMs: t.activeMs, idleMs: t.idleMs, gaps: t.gaps, primeTokens: t.primeTokens } @@ -86,27 +91,30 @@ function TpsView(props) { const [tick, setTick] = createSignal(0); const bump = () => setTick((x) => (x + 1) % 1_000_000); + let timer: ReturnType | null = null; + onCleanup(() => { + if (timer !== null) clearInterval(timer); + }); // ── Precise live deltas + calibration via the event bus ──────────────────── - const offs = []; + const offs: Array<() => void> = []; try { const bus = props.api.event; if (bus && typeof bus.on === "function") { - const onPart = (event) => { + const onPart = (event: AnyRecord) => { try { const part = event?.properties?.part; - if (!part || part.sessionID !== props.sessionID) return; + if (!part || String(part.sessionID) !== String(props.sessionID)) return; if (part.type !== "text" && part.type !== "reasoning") return; const now = Date.now(); const full = typeof part.text === "string" ? part.text.length : 0; const delta = event?.properties?.delta; - const added = - typeof delta === "string" && delta.length > 0 - ? delta.length - : Math.max(0, full - (partLen.get(part.id) || 0)); - partLen.set(part.id, full); + const prev = partLen.get(part.id); + const added = typeof delta === "string" ? delta.length : prev ? Math.max(0, full - prev.length) : 0; + partLen.set(part.id, { length: full, messageID: part.messageID }); if (added > 0) { - const tokens = tokensFromChars(added, ratio); + const msg = part.messageID ? safeMessage(part.messageID) : null; + const tokens = tokensFromChars(added, ratioFor(msg)); if (part.messageID) { timerFor(part.messageID).push(tokens, now); // precise active-gen time currentMsgId = part.messageID; @@ -118,10 +126,10 @@ function TpsView(props) { /* ignore a single malformed event */ } }; - const onMessage = (event) => { + const onMessage = (event: AnyRecord) => { try { const info = event?.properties?.info; - if (info && info.sessionID === props.sessionID && isAssistant(info) && info.time?.completed) { + if (info && String(info.sessionID) === String(props.sessionID) && isAssistant(info) && info.time?.completed) { const generated = (Number(info.tokens?.output) || 0) + (Number(info.tokens?.reasoning) || 0); let chars = 0; try { @@ -133,9 +141,17 @@ function TpsView(props) { partLen.delete(p.id); } } catch { - /* parts unavailable on this build — skip calibration this round */ + for (const [partID, state] of partLen) { + if (state.messageID === info.id) { + chars += state.length; + partLen.delete(partID); + } + } + } + if (chars > 0 && generated > 0) { + const key = modelKey(info); + ratioByModel.set(key, calibrateRatio(ratioByModel.get(key), chars, generated)); } - if (chars > 0 && generated > 0) ratio = calibrateRatio(ratio, chars, generated); // Lock this message's timer to the exact generated count; the measured // active time is unchanged, so its rate becomes exact. const t = timers.get(info.id); @@ -150,19 +166,22 @@ function TpsView(props) { // Keep the per-message timers from accumulating orphans. A timer is needed for // as long as its message is in the session (the view recomputes exact stats // for all of them), so only drop it when the message is actually removed. - const onMessageRemoved = (event) => { + const onMessageRemoved = (event: AnyRecord) => { try { const messageID = event?.properties?.messageID; if (messageID) { timers.delete(messageID); if (currentMsgId === messageID) currentMsgId = null; + for (const [partID, state] of partLen) { + if (state.messageID === messageID) partLen.delete(partID); + } } } catch { /* ignore */ } bump(); }; - const onPartRemoved = (event) => { + const onPartRemoved = (event: AnyRecord) => { try { const partID = event?.properties?.partID; if (partID) partLen.delete(partID); @@ -192,7 +211,7 @@ function TpsView(props) { } // ── Live tick: re-sample the windowed rate (sparkline + decay) and repaint ── - const timer = setInterval(() => { + timer = setInterval(() => { try { meter.sample(Date.now()); } catch { @@ -200,7 +219,6 @@ function TpsView(props) { } bump(); }, cfg.pollMs); - onCleanup(() => clearInterval(timer)); onCleanup(() => { for (const off of offs) { try { @@ -211,6 +229,15 @@ function TpsView(props) { } }); + function safeMessage(messageID: string) { + try { + const messages = props.api.state.session.messages(props.sessionID) || []; + return messages.find((m: AnyRecord) => m?.id === messageID) || null; + } catch { + return null; + } + } + // ── Derived view model: exact stats from reactive state + the live snapshot ─ const view = createMemo(() => { tick(); // re-run on every live tick / event @@ -226,7 +253,7 @@ function TpsView(props) { let inflightId = null; for (const m of messages) { if (!isAssistant(m)) continue; - const s = messageStats(m, timingFor(m.id)); + const s = messageStats(m, timingFor(m.id)) as AnyRecord | null; if (!s) continue; stats.push(s); if (s.done) last = s; // most-recent completed message @@ -297,15 +324,15 @@ function TpsView(props) { } /** @type {import("@opencode-ai/plugin/tui").TuiPlugin} */ -const tui = async (api, options) => { +const tui = async (api: AnyRecord, options: AnyRecord) => { try { - const cfg = resolveConfig(options, typeof process !== "undefined" ? process.env : {}); + const cfg = resolveConfig(options, typeof process !== "undefined" ? process.env : {}) as AnyRecord; if (!cfg.enabled) return; if (!api?.slots?.register) return; // runtime without the slot API → no-op api.slots.register({ order: cfg.order, slots: { - [cfg.slot](_ctx, props) { + [cfg.slot](_ctx: AnyRecord, props: AnyRecord) { if (!props?.session_id) return undefined; return ; }, diff --git a/plugins/tps/config.js b/plugins/tps/config.js index be8119c..43f000c 100644 --- a/plugins/tps/config.js +++ b/plugins/tps/config.js @@ -40,9 +40,19 @@ export const DEFAULTS = { }; const DETAILS = ["full", "compact", "minimal"]; +const MIN_ORDER = 0; +const MIN_POLL_MS = 50; +const MIN_WINDOW_MS = 250; +const MIN_GAP_MS = 100; +const MIN_SERIES_LENGTH = 1; +const MIN_SPARK_WIDTH = 0; export function isFalsy(v) { - return v === false || v === 0 || v === "0" || v === "false" || v === "off" || v === "no"; + return v === false || v === 0 || v === "" || v === "0" || v === "false" || v === "off" || v === "no"; +} + +function isTruthy(v) { + return v === true || v === 1 || v === "1" || v === "true" || v === "on" || v === "yes"; } function num(value, fallback, min) { @@ -60,37 +70,41 @@ export function resolveConfig(options, env) { const o = options && typeof options === "object" ? options : {}; const e = env || {}; const disabled = - o.enabled === false || - isFalsy(e.OPENCODE_TPS_METER) || - e.OPENCODE_TPS_METER_DISABLE === "1" || - e.OPENCODE_TPS_METER_DISABLE === "true"; + o.enabled !== true && + (o.enabled === false || isFalsy(e.OPENCODE_TPS_METER) || isTruthy(e.OPENCODE_TPS_METER_DISABLE)); // "output" only if explicitly requested; default (and anything else) → generated. - const metric = - o.metric === "output" || e.OPENCODE_TPS_METER_METRIC === "output" ? "output" : "generated"; + const metricRaw = e.OPENCODE_TPS_METER_METRIC ?? o.metric; + const metric = metricRaw === "output" ? "output" : "generated"; const detailRaw = o.detail || e.OPENCODE_TPS_METER_DETAIL || DEFAULTS.detail; const detail = DETAILS.includes(detailRaw) ? detailRaw : DEFAULTS.detail; return { enabled: !disabled, - order: num(o.order, DEFAULTS.order), + order: num(o.order, DEFAULTS.order, MIN_ORDER), slot: typeof o.slot === "string" && o.slot ? o.slot : e.OPENCODE_TPS_METER_SLOT || DEFAULTS.slot, - pollMs: num(o.pollMs, DEFAULTS.pollMs, 50), - windowMs: num(o.windowMs ?? e.OPENCODE_TPS_METER_WINDOW_MS, DEFAULTS.windowMs, 250), - gapMs: num(o.gapMs ?? e.OPENCODE_TPS_METER_GAP_MS, DEFAULTS.gapMs, 100), - seriesLength: Math.floor(num(o.seriesLength, DEFAULTS.seriesLength, 1)), + pollMs: num(o.pollMs ?? e.OPENCODE_TPS_METER_POLL_MS, DEFAULTS.pollMs, MIN_POLL_MS), + windowMs: num(o.windowMs ?? e.OPENCODE_TPS_METER_WINDOW_MS, DEFAULTS.windowMs, MIN_WINDOW_MS), + gapMs: num(o.gapMs ?? e.OPENCODE_TPS_METER_GAP_MS, DEFAULTS.gapMs, MIN_GAP_MS), + seriesLength: Math.floor(num(o.seriesLength ?? e.OPENCODE_TPS_METER_SERIES_LENGTH, DEFAULTS.seriesLength, MIN_SERIES_LENGTH)), metric, detail, icon: typeof o.icon === "string" ? o.icon : DEFAULTS.icon, label: typeof o.label === "string" ? o.label : DEFAULTS.label, unit: typeof o.unit === "string" ? o.unit : DEFAULTS.unit, - sparkWidth: Math.floor(num(o.sparkWidth, DEFAULTS.sparkWidth, 0)), - showSparkline: o.showSparkline !== false, - showSession: o.showSession !== false, - showWaits: o.showWaits !== false, - showTotals: o.showTotals === true, - showCost: o.showCost === true, - showCache: o.showCache === true, + sparkWidth: Math.floor(num(o.sparkWidth ?? e.OPENCODE_TPS_METER_SPARK_WIDTH, DEFAULTS.sparkWidth, MIN_SPARK_WIDTH)), + showSparkline: bool(o.showSparkline, e.OPENCODE_TPS_METER_SHOW_SPARKLINE, DEFAULTS.showSparkline), + showSession: bool(o.showSession, e.OPENCODE_TPS_METER_SHOW_SESSION, DEFAULTS.showSession), + showWaits: bool(o.showWaits, e.OPENCODE_TPS_METER_SHOW_WAITS, DEFAULTS.showWaits), + showTotals: bool(o.showTotals, e.OPENCODE_TPS_METER_SHOW_TOTALS, DEFAULTS.showTotals), + showCost: bool(o.showCost, e.OPENCODE_TPS_METER_SHOW_COST, DEFAULTS.showCost), + showCache: bool(o.showCache, e.OPENCODE_TPS_METER_SHOW_CACHE, DEFAULTS.showCache), colors: o.colors && typeof o.colors === "object" ? o.colors : null, }; } + +function bool(option, envValue, fallback) { + if (option !== undefined) return !isFalsy(option); + if (envValue !== undefined) return !isFalsy(envValue); + return fallback; +} diff --git a/plugins/tps/format.js b/plugins/tps/format.js index 5f5b6a6..f1f7808 100644 --- a/plugins/tps/format.js +++ b/plugins/tps/format.js @@ -19,7 +19,7 @@ export function fmtRate(value, placeholder = "–") { if (value === null || value === undefined) return placeholder; // unknown, not zero const n = Number(value); if (!Number.isFinite(n) || n < 0) return placeholder; - if (n >= 1000) return trimZero((n / 1000).toFixed(1)) + "k"; + if (n >= 999.5) return trimZero((n / 1000).toFixed(1)) + "k"; if (n >= 100) return String(Math.round(n)); if (n >= 10) return trimZero(n.toFixed(1)); return trimZero(n.toFixed(1)); @@ -35,7 +35,7 @@ export function fmtInt(value) { /** Token counts: <1000 plain, else "k"/"M" with one decimal. */ export function fmtTokens(value) { const n = Number(value); - if (!Number.isFinite(n)) return "0"; + if (!Number.isFinite(n) || n < 0) return "0"; const abs = Math.abs(n); if (abs >= 1_000_000) return trimZero((n / 1_000_000).toFixed(1)) + "M"; if (abs >= 1000) return trimZero((n / 1000).toFixed(1)) + "k"; @@ -49,15 +49,16 @@ export function fmtMs(value) { if (n < 1000) return `${Math.round(n)}ms`; const s = n / 1000; if (s < 60) return `${trimZero(s.toFixed(s < 10 ? 1 : 0))}s`; - const m = Math.floor(s / 60); - const rem = Math.round(s - m * 60); + const rounded = Math.round(s); + const m = Math.floor(rounded / 60); + const rem = rounded % 60; return `${m}m${String(rem).padStart(2, "0")}s`; } /** Cost in USD: "$0.0123" small, "$1.23" larger, "$0" for zero. */ export function fmtCost(value) { const n = Number(value); - if (!Number.isFinite(n) || n === 0) return "$0"; + if (!Number.isFinite(n) || n <= 0) return "$0"; if (n < 0.01) return "$" + n.toFixed(4); if (n < 1) return "$" + n.toFixed(3); return "$" + n.toFixed(2); @@ -90,7 +91,8 @@ export function sparkline(values, opts = {}) { else pad = width - arr.length; const min = Number.isFinite(Number(opts.min)) ? Number(opts.min) : 0; - const max = Number.isFinite(Number(opts.max)) ? Number(opts.max) : Math.max(min, ...cells, 0); + const rawMax = Number.isFinite(Number(opts.max)) ? Number(opts.max) : Math.max(min, ...cells, 0); + const max = Math.max(min, rawMax); const range = max - min; const body = cells diff --git a/plugins/tps/gen.js b/plugins/tps/gen.js index 6991994..515834d 100644 --- a/plugins/tps/gen.js +++ b/plugins/tps/gen.js @@ -56,6 +56,7 @@ export class GenerationTimer { this._gaps = 0; // count of excluded gaps this._firstAt = null; // first chunk arrival (for TTFT) this._lastAt = null; // most recent chunk arrival + this._pendingPrime = false; // tokenless burst opener; next token inherits prime } /** @@ -70,6 +71,7 @@ export class GenerationTimer { const at = Number(t); const tok = Number(tokens); if (!Number.isFinite(at)) return this; + if (this._lastAt !== null && at < this._lastAt) return this; let prime = false; if (this._lastAt === null) { prime = true; // very first chunk → prefill window @@ -84,10 +86,14 @@ export class GenerationTimer { } } } + if (this._pendingPrime) prime = true; if (this._firstAt === null) this._firstAt = at; if (Number.isFinite(tok) && tok > 0) { this._tokens += tok; if (prime) this._primeTokens += tok; + this._pendingPrime = false; + } else if (prime) { + this._pendingPrime = true; } this._lastAt = at; return this; @@ -101,7 +107,11 @@ export class GenerationTimer { */ setTokens(exact) { const n = Number(exact); - if (Number.isFinite(n) && n >= 0) this._tokens = n; + if (Number.isFinite(n) && n >= 0) { + const ratio = this._tokens > 0 ? this._primeTokens / this._tokens : 0; + this._tokens = n; + this._primeTokens = Math.min(n, n * ratio); + } return this; } diff --git a/plugins/tps/index.d.ts b/plugins/tps/index.d.ts new file mode 100644 index 0000000..bd3dd1f --- /dev/null +++ b/plugins/tps/index.d.ts @@ -0,0 +1,57 @@ +export class RateMeter { + constructor(opts?: { windowMs?: number; minSpanMs?: number; seriesLength?: number; halfLifeMs?: number }); + reset(): void; + push(tokens: number, t: number): this; + rate(now: number): number; + smooth(now: number): number; + sample(now: number): number; + active(now: number): boolean; + series(): number[]; + readonly peak: number; + readonly total: number; + readonly count: number; +} + +export class GenerationTimer { + constructor(opts?: { gapThresholdMs?: number }); + reset(): void; + push(tokens: number, t: number): this; + setTokens(exact: number): this; + tps(): number | null; + snapshot(): Record; + readonly activeMs: number; + readonly idleMs: number; + readonly gaps: number; + readonly tokens: number; + readonly firstAt: number | null; + readonly lastAt: number | null; + readonly primeTokens: number; + readonly decodeTokens: number; +} + +export const DEFAULT_GAP_THRESHOLD_MS: number; +export const DEFAULT_CHARS_PER_TOKEN: number; +export const SPARK_CHARS: string[]; +export function messageStats(msg: unknown, timing?: number | object): object | null; +export function aggregate(statList: Array): object; +export function calibrateRatio(prev: number | null | undefined, chars: number, tokens: number, alpha?: number): number; +export function tokensFromChars(chars: number, ratio?: number): number; +export function rate(tokens: number, ms: number): number | null; +export function isAssistant(msg: unknown): boolean; +export function fmtRate(value: unknown, placeholder?: string): string; +export function fmtInt(value: unknown): string; +export function fmtTokens(value: unknown): string; +export function fmtMs(value: unknown): string; +export function fmtCost(value: unknown): string; +export function sparkline(values: number[], opts?: object): string; +export function bar(fraction: number, width?: number, full?: string, rest?: string): string; +export function trimZero(str: string): string; +export function buildView(input?: object): object; +export function renderText(view: object): string; +export const VIEW_DEFAULTS: object; +export function resolveConfig(options?: object, env?: object): object; +export const TONE_TO_THEME: object; +export const DEFAULTS: object; +export function isFalsy(value: unknown): boolean; +declare const plugin: { id: string; tui: (...args: unknown[]) => Promise }; +export default plugin; diff --git a/plugins/tps/meter.js b/plugins/tps/meter.js index 3744372..0df5f6b 100644 --- a/plugins/tps/meter.js +++ b/plugins/tps/meter.js @@ -52,6 +52,7 @@ export class RateMeter { this._ewma = null; this._peak = 0; this._series = []; + this._head = 0; } /** @@ -63,16 +64,21 @@ export class RateMeter { const tok = Number(tokens); const at = Number(t); if (!Number.isFinite(at)) return this; - if (!Number.isFinite(tok) || tok <= 0) return this; + if (this._lastAt !== null && at < this._lastAt) return this; + if (!Number.isFinite(tok) || tok <= 0) { + this._lastAt = at; + return this; + } if (this._startedAt === null) this._startedAt = at; // Continuous-time EWMA of the instantaneous gap rate. if (this._prevAt !== null && at > this._prevAt) { const dt = at - this._prevAt; const inst = tok / (dt / 1000); - const alpha = 1 - Math.exp((-dt / this.halfLifeMs) * LN2); - this._ewma = this._ewma === null ? inst : this._ewma + alpha * (inst - this._ewma); - } else if (this._ewma === null) { - this._ewma = 0; // first sample: no gap yet, seed at 0 so it ramps up + if (this._ewma === null) this._ewma = inst; + else { + const alpha = 1 - Math.exp((-dt / this.halfLifeMs) * LN2); + this._ewma = this._ewma + alpha * (inst - this._ewma); + } } this._samples.push({ t: at, tok }); this._windowSum += tok; @@ -88,13 +94,16 @@ export class RateMeter { _prune(now) { const left = now - this.windowMs; const s = this._samples; - let i = 0; - while (i < s.length && s[i].t <= left) { - this._windowSum -= s[i].tok; - i++; + while (this._head < s.length && s[this._head].t <= left) { + this._windowSum -= s[this._head].tok; + this._head++; + } + if (this._head > 64 && this._head * 2 > s.length) { + s.splice(0, this._head); + this._head = 0; } - if (i > 0) s.splice(0, i); - if (this._windowSum < 0) this._windowSum = 0; // guard fp drift + if (this._windowSum < -1e-9) throw new Error("RateMeter window sum invariant violated"); + if (this._windowSum < 0) this._windowSum = 0; // tolerate tiny fp drift } /** @@ -107,7 +116,8 @@ export class RateMeter { this._prune(at); if (this._windowSum <= 0) return 0; const left = Math.max(at - this.windowMs, this._startedAt); - const span = Math.max(at - left, this.minSpanMs); + const rawSpan = at - left; + const span = this._samples.length - this._head > 1 ? Math.max(rawSpan, 1) : Math.max(rawSpan, this.minSpanMs); return this._windowSum / (span / 1000); } @@ -144,7 +154,7 @@ export class RateMeter { if (this._lastAt === null) return false; const at = Number(now); if (!Number.isFinite(at)) return false; - return at - this._lastAt <= this.windowMs; + return at - this._lastAt < this.windowMs; } series() { diff --git a/plugins/tps/root.js b/plugins/tps/root.js new file mode 100644 index 0000000..f2f6804 --- /dev/null +++ b/plugins/tps/root.js @@ -0,0 +1,9 @@ +export * from "./index.js"; + +export default { + id: "opencode-tps-meter", + async tui(...args) { + const plugin = await import("../tps-meter.tsx"); + return plugin.default.tui(...args); + }, +}; diff --git a/plugins/tps/tps.js b/plugins/tps/tps.js index 3d6fee4..7b00ddd 100644 --- a/plugins/tps/tps.js +++ b/plugins/tps/tps.js @@ -25,13 +25,13 @@ /** Default characters-per-token before any calibration (English-ish average). */ export const DEFAULT_CHARS_PER_TOKEN = 4; -const MIN_RATIO = 1.2; +const MIN_RATIO = 0.25; const MAX_RATIO = 12; /** Coerce to a finite number, else 0. */ function n0(v) { const n = Number(v); - return Number.isFinite(n) ? n : 0; + return Number.isFinite(n) && n > 0 ? n : 0; } /** Coerce to a finite number, else null (used for optional timestamps). */ @@ -223,6 +223,7 @@ export function aggregate(statList) { avgE2eTps: rate(output, e2eMs), peakTps: peakTps || null, avgTtftMs: ttftCount ? ttftSum / ttftCount : null, + decodeSource: [...new Set((statList || []).filter((s) => s && s.done).map((s) => s.decodeSource))].length > 1 ? "mixed" : (statList || []).find((s) => s && s.done)?.decodeSource ?? null, }; } @@ -246,6 +247,9 @@ export function calibrateRatio(prev, chars, tokens, alpha = 0.3) { const sample = clampRatio(c / t); if (base === null) return sample; const a = Number.isFinite(alpha) ? Math.min(1, Math.max(0, alpha)) : 0.3; + if (a === 0 && sample !== base && typeof process !== "undefined" && process.emitWarning) { + process.emitWarning("calibrateRatio alpha=0 ignores a valid calibration sample", { code: "OPENCODE_TPS_ALPHA_ZERO" }); + } return clampRatio(base * (1 - a) + sample * a); } diff --git a/plugins/tps/view.js b/plugins/tps/view.js index 64c35a0..8ac2968 100644 --- a/plugins/tps/view.js +++ b/plugins/tps/view.js @@ -13,23 +13,7 @@ */ import { fmtRate, fmtTokens, fmtMs, fmtCost, sparkline } from "./format.js"; - -const DEFAULTS = { - icon: "", - label: "TPS", - unit: "tok/s", - detail: "full", // "full" | "compact" | "minimal" - metric: "generated", // "generated" (output+reasoning) | "output" — which TPS to headline - sparkWidth: 24, - showSparkline: true, - showSession: true, - showWaits: true, // surface the excluded tool/wait time as a precision signal - // OpenCode's native Context section already shows total tokens, % context, and - // cost — so those are OFF by default here to avoid duplicating native stats. - showTotals: false, - showCost: false, - showCache: false, -}; +import { DEFAULTS } from "./config.js"; /** * @param {object} input @@ -66,7 +50,7 @@ export function buildView(input = {}) { const headline = streaming ? (live ? live.tps : null) : last ? last[metricKey] : null; const lines = []; - const push = (key, segments) => lines.push({ key, segments: segments.filter((s) => s && s.text != null) }); + const push = (key, segments) => lines.push({ key, segments: segments.filter((s) => s && s.text != null && s.text !== "") }); // ── Header + headline number (active-generation TPS) ────────────────────── const headerLabel = cfg.icon ? `${cfg.icon} ${cfg.label}` : cfg.label; @@ -95,7 +79,8 @@ export function buildView(input = {}) { { text: `${fmtRate(last[metricKey])} ${cfg.unit}`, tone: "value" }, { text: detail.length ? ` ${detail.join(" · ")}` : "", tone: "muted" }, ]); - } else if (streaming && live) { + } + if (streaming && live) { // Active message not yet finalized: peak so far + any wait already excluded. const bits = []; if (live.peak) bits.push(`peak ${fmtRate(live.peak)} ${cfg.unit}`); diff --git a/scripts/install.mjs b/scripts/install.mjs index 1360e21..e3df59a 100755 --- a/scripts/install.mjs +++ b/scripts/install.mjs @@ -31,7 +31,18 @@ import { execFileSync } from "node:child_process"; // unscoped name `opencode-tps-meter` is already owned on npm by a different // author. OpenCode resolves the TUI plugin by THIS name from node_modules. const PKG_NAME = "@devinoldenburg/opencode-tps-meter"; +const PLUGIN_SPEC = `${PKG_NAME}/tui`; const REPO_ROOT = resolve(dirname(fileURLToPath(import.meta.url)), ".."); +const HELP = `install.mjs — wire opencode-tps-meter into an OpenCode config so the TUI loads it on next launch. + +Usage: + node scripts/install.mjs # add to ~/.config/opencode (npm spec) + node scripts/install.mjs --local # link THIS checkout (file: dependency) + node scripts/install.mjs --dir # target a specific config dir + node scripts/install.mjs --no-install # edit config only, skip npm install + node scripts/install.mjs --dry-run # print what would change, write nothing + node scripts/install.mjs --uninstall # remove plugin and dependency + node scripts/install.mjs --print # print manual instructions and exit`; function parseArgs(argv) { const a = { _: [] }; @@ -42,7 +53,11 @@ function parseArgs(argv) { else if (t === "--dry-run") a.dryRun = true; else if (t === "--uninstall" || t === "--remove") a.uninstall = true; else if (t === "--print") a.print = true; - else if (t === "--dir") a.dir = argv[++i]; + else if (t === "--dir") { + const next = argv[i + 1]; + if (!next || next.startsWith("--")) throw new Error("--dir requires a path argument"); + a.dir = argv[++i]; + } else if (t === "--help" || t === "-h") a.help = true; else a._.push(t); } @@ -64,10 +79,13 @@ function configDir(args) { } function readJson(file, fallback) { + if (!existsSync(file)) { + return fallback; + } try { return JSON.parse(readFileSync(file, "utf8")); - } catch { - return fallback; + } catch (err) { + throw new Error(`Cannot read valid JSON from ${file}: ${err?.message || err}`); } } @@ -85,7 +103,7 @@ function writeJson(file, obj, dryRun) { } if (existsSync(file)) { try { - copyFileSync(file, `${file}.bak`); + copyFileSync(file, backupPath(file)); } catch { /* best-effort backup */ } @@ -93,11 +111,27 @@ function writeJson(file, obj, dryRun) { writeFileSync(file, text); } +function backupPath(file) { + let n = 0; + let candidate = `${file}.bak`; + while (existsSync(candidate)) candidate = `${file}.bak.${++n}`; + return candidate; +} + +function pluginName(entry) { + return Array.isArray(entry) ? entry[0] : entry; +} + +function isPluginEntry(entry) { + const name = pluginName(entry); + return name === PKG_NAME || name === PLUGIN_SPEC; +} + function printManual() { console.log(`${C.bold("Manual install")} — add to your OpenCode config dir (e.g. ~/.config/opencode): ${C.cyan("tui.json")} - { "$schema": "https://opencode.ai/tui.json", "plugin": ["${PKG_NAME}"] } + { "$schema": "https://opencode.ai/tui.json", "plugin": ["${PLUGIN_SPEC}"] } ${C.cyan("package.json")} (so the TUI can resolve it from node_modules) { "dependencies": { "${PKG_NAME}": "latest" } } @@ -108,7 +142,7 @@ function printManual() { function main() { const args = parseArgs(process.argv.slice(2)); if (args.help) { - console.log(readFileSync(fileURLToPath(import.meta.url), "utf8").split("\n").slice(2, 26).join("\n").replace(/^ \*\/?/gm, "")); + console.log(HELP); return; } if (args.print) return printManual(); @@ -128,26 +162,34 @@ function main() { // ── tui.json: the plugin list the TUI reads ──────────────────────────────── const tui = readJson(tuiPath, { $schema: "https://opencode.ai/tui.json" }); if (!Array.isArray(tui.plugin)) tui.plugin = []; - const had = tui.plugin.includes(PKG_NAME); + const existingEntry = tui.plugin.find(isPluginEntry); + const had = !!existingEntry; if (args.uninstall) { if (!had) { console.log(C.yellow(` ${PKG_NAME} is not in tui.json — nothing to remove.`)); return; } - tui.plugin = tui.plugin.filter((p) => p !== PKG_NAME); + tui.plugin = tui.plugin.filter((p) => !isPluginEntry(p)); writeJson(tuiPath, tui, args.dryRun); - console.log(C.green(` ✓ removed ${PKG_NAME} from tui.json`)); - console.log(C.dim(` (left package.json + node_modules untouched)`)); + const pkg = readJson(pkgPath, {}); + if (pkg.dependencies && Object.hasOwn(pkg.dependencies, PKG_NAME)) { + delete pkg.dependencies[PKG_NAME]; + writeJson(pkgPath, pkg, args.dryRun); + } + console.log(C.green(` ✓ removed ${PKG_NAME} from tui.json and package.json`)); return; } - if (!had) { - tui.plugin.push(PKG_NAME); + const normalizedEntry = Array.isArray(existingEntry) ? [PLUGIN_SPEC, ...existingEntry.slice(1)] : PLUGIN_SPEC; + const alreadyCurrent = tui.plugin.some((p) => pluginName(p) === PLUGIN_SPEC); + if (!had || !alreadyCurrent || tui.plugin.some((p) => pluginName(p) === PKG_NAME)) { + tui.plugin = tui.plugin.filter((p) => !isPluginEntry(p)); + tui.plugin.push(had ? normalizedEntry : PLUGIN_SPEC); writeJson(tuiPath, tui, args.dryRun); - console.log(C.green(` ✓ added ${PKG_NAME} to tui.json`)); + console.log(C.green(` ✓ ${had ? "updated" : "added"} ${PLUGIN_SPEC} in tui.json`)); } else { - console.log(C.dim(` • tui.json already lists ${PKG_NAME}`)); + console.log(C.dim(` • tui.json already lists ${PLUGIN_SPEC}`)); } // ── package.json: how node resolves the package from node_modules ─────────── @@ -174,6 +216,8 @@ function main() { } catch (err) { console.log(C.yellow(` ! npm install failed: ${err?.message || err}`)); console.log(C.dim(` finish manually: cd ${dir} && npm install`)); + process.exitCode = 1; + return; } } @@ -191,4 +235,9 @@ function npmSpec() { } } -main(); +try { + main(); +} catch (err) { + console.error(C.yellow(` ! ${err?.message || err}`)); + process.exit(1); +} diff --git a/tests/config.test.mjs b/tests/config.test.mjs index 9e09078..07d0cd5 100644 --- a/tests/config.test.mjs +++ b/tests/config.test.mjs @@ -32,7 +32,10 @@ test("options override defaults", () => { test("env can disable and override", () => { assert.equal(resolveConfig({}, { OPENCODE_TPS_METER_DISABLE: "1" }).enabled, false); + assert.equal(resolveConfig({}, { OPENCODE_TPS_METER_DISABLE: "yes" }).enabled, false); assert.equal(resolveConfig({}, { OPENCODE_TPS_METER: "off" }).enabled, false); + assert.equal(resolveConfig({}, { OPENCODE_TPS_METER: "" }).enabled, false); + assert.equal(resolveConfig({ enabled: true }, { OPENCODE_TPS_METER_DISABLE: "1" }).enabled, true); assert.equal(resolveConfig({}, { OPENCODE_TPS_METER_METRIC: "generated" }).metric, "generated"); assert.equal(resolveConfig({}, { OPENCODE_TPS_METER_SLOT: "sidebar_footer" }).slot, "sidebar_footer"); assert.equal(resolveConfig({}, { OPENCODE_TPS_METER_WINDOW_MS: "1500" }).windowMs, 1500); @@ -56,6 +59,14 @@ test("metric defaults to generated; either source can opt into 'output'", () => assert.equal(resolveConfig({ metric: "output" }, {}).metric, "output"); assert.equal(resolveConfig({}, { OPENCODE_TPS_METER_METRIC: "output" }).metric, "output"); assert.equal(resolveConfig({ metric: "generated" }, { OPENCODE_TPS_METER_METRIC: "output" }).metric, "output"); + assert.equal(resolveConfig({ metric: "output" }, { OPENCODE_TPS_METER_METRIC: "generated" }).metric, "generated"); +}); + +test("numeric and boolean config clamps cover edge cases", () => { + const c = resolveConfig({ order: -999, showTotals: true }, { OPENCODE_TPS_METER_SHOW_COST: "yes" }); + assert.equal(c.order, 0); + assert.equal(c.showTotals, true); + assert.equal(c.showCost, true); }); test("gapMs override + clamp", () => { diff --git a/tests/format.test.mjs b/tests/format.test.mjs index ffddc61..fab74f0 100644 --- a/tests/format.test.mjs +++ b/tests/format.test.mjs @@ -20,6 +20,7 @@ test("fmtRate buckets and placeholder", () => { assert.equal(fmtRate(100), "100"); assert.equal(fmtRate(247.6), "248"); assert.equal(fmtRate(1234), "1.2k"); + assert.equal(fmtRate(999.5), "1k"); assert.equal(fmtRate(0), "0"); assert.equal(fmtRate(null), "–"); assert.equal(fmtRate(-1), "–"); @@ -36,6 +37,7 @@ test("fmtTokens compacts k/M", () => { assert.equal(fmtTokens(1500), "1.5k"); assert.equal(fmtTokens(1234567), "1.2M"); assert.equal(fmtTokens(2000000), "2M"); + assert.equal(fmtTokens(-1500), "0"); }); test("fmtMs scales ms/s/m", () => { @@ -45,6 +47,8 @@ test("fmtMs scales ms/s/m", () => { assert.equal(fmtMs(9000), "9s"); assert.equal(fmtMs(12000), "12s"); assert.equal(fmtMs(65000), "1m05s"); + assert.equal(fmtMs(59999), "60s"); + assert.equal(fmtMs(119999), "2m00s"); assert.equal(fmtMs(-1), "–"); }); @@ -54,6 +58,7 @@ test("fmtCost formats by magnitude", () => { assert.equal(fmtCost(0.0123), "$0.012"); assert.equal(fmtCost(0.5), "$0.500"); assert.equal(fmtCost(1.5), "$1.50"); + assert.equal(fmtCost(-0.01), "$0"); }); test("sparkline maps a ramp across all glyphs", () => { @@ -86,6 +91,10 @@ test("sparkline width 0 is empty", () => { assert.equal(sparkline([1, 2, 3], { width: 0 }), ""); }); +test("sparkline tolerates an inverted min/max", () => { + assert.equal(sparkline([1, 2, 3], { width: 3, min: 10, max: 1 }), "▁▁▁"); +}); + test("bar fills proportionally", () => { assert.equal(bar(0.6, 10), "██████░░░░"); assert.equal(bar(0, 5), "░░░░░"); diff --git a/tests/gen.test.mjs b/tests/gen.test.mjs index 89345fb..f3f9367 100644 --- a/tests/gen.test.mjs +++ b/tests/gen.test.mjs @@ -45,8 +45,8 @@ test("setTokens makes the rate exact at completion without changing measured tim const activeBefore = g.activeMs; g.setTokens(80); // exact provider count (prime offset = 7) assert.equal(g.activeMs, activeBefore); // time untouched - assert.equal(g.decodeTokens, 73); // 80 - 7 prime - assert.equal(g.tps(), 73 / (activeBefore / 1000)); + assert.equal(g.decodeTokens, 80 - 80 * (7 / 77)); // prime offset scales with exact total + assert.equal(g.tps(), (80 - 80 * (7 / 77)) / (activeBefore / 1000)); }); test("gap threshold boundary is exclusive of the threshold", () => { @@ -64,6 +64,24 @@ test("gap threshold boundary is exclusive of the threshold", () => { assert.equal(at.gaps, 1); }); +test("tokenless chunks do not consume the post-gap prime marker", () => { + const g = new GenerationTimer({ gapThresholdMs: 1000 }); + g.push(10, 0); + g.push(10, 100); + g.push(0, 2100); + g.push(10, 2200); + assert.equal(g.gaps, 1); + assert.equal(g.primeTokens, 20); + assert.equal(g.decodeTokens, 10); +}); + +test("non-monotonic timestamps are ignored", () => { + const g = new GenerationTimer(); + g.push(10, 1000).push(10, 900).push(10, 1100); + assert.equal(g.tokens, 20); + assert.equal(g.activeMs, 100); +}); + test("invalid threshold falls back to the default", () => { assert.equal(new GenerationTimer({ gapThresholdMs: -5 }).gapThresholdMs, DEFAULT_GAP_THRESHOLD_MS); assert.equal(new GenerationTimer({ gapThresholdMs: "x" }).gapThresholdMs, DEFAULT_GAP_THRESHOLD_MS); diff --git a/tests/meter.test.mjs b/tests/meter.test.mjs index 74ce4c1..f6bad7b 100644 --- a/tests/meter.test.mjs +++ b/tests/meter.test.mjs @@ -30,12 +30,11 @@ test("steady stream converges to the true rate", () => { assert.equal(m.rate(3000), 100); }); -test("minSpan guard caps an early burst instead of reporting infinity", () => { +test("multiple deltas use their actual span for peak precision", () => { const m = new RateMeter({ windowMs: 3000, minSpanMs: 250 }); m.push(100, 0); m.push(100, 10); - // real span is 10ms; guard floors it at 250ms -> 200 / 0.25 = 800 (not 20000) - assert.equal(m.rate(10), 800); + assert.equal(m.rate(10), 20000); }); test("windowed rate decays monotonically when the stream stops", () => { @@ -53,10 +52,30 @@ test("active() tracks the trailing window boundary", () => { const m = new RateMeter({ windowMs: 3000 }); m.push(10, 1000); assert.equal(m.active(1000), true); - assert.equal(m.active(4000), true); // exactly windowMs later, inclusive + assert.equal(m.active(4000), false); // exactly windowMs later is outside the window assert.equal(m.active(4001), false); }); +test("smooth() seeds from the first measurable instantaneous rate", () => { + const m = new RateMeter({ halfLifeMs: 900 }); + m.push(100, 0); + m.push(100, 100); + assert.equal(m.smooth(100), 1000); +}); + +test("ignored zero deltas still refresh active state", () => { + const m = new RateMeter({ windowMs: 3000 }); + m.push(10, 0); + m.push(0, 2500); + assert.equal(m.active(3000), true); +}); + +test("non-monotonic deltas are ignored", () => { + const m = new RateMeter(); + m.push(10, 1000).push(10, 900).push(10, 1100); + assert.equal(m.total, 20); +}); + test("sample() builds a capped series and tracks the peak", () => { const m = new RateMeter({ windowMs: 3000, seriesLength: 4 }); for (let t = 0; t <= 1000; t += 100) m.push(20, t); diff --git a/tests/view.test.mjs b/tests/view.test.mjs index 91615c5..1ab15ce 100644 --- a/tests/view.test.mjs +++ b/tests/view.test.mjs @@ -54,6 +54,21 @@ test("streaming surfaces excluded wait time when a tool gap occurred", () => { assert.ok(lines.some((l) => l.includes("−5s wait")), `expected an excluded-wait note in ${JSON.stringify(lines)}`); }); +test("empty detail segments are filtered out", () => { + const last = messageStats({ ...LAST_MSG, time: { created: 1000, completed: 3500 } }); + const v = buildView({ last, session: aggregate([last]), status: "idle" }); + assert.equal(v.lines.find((l) => l.key === "last").segments.some((s) => s.text === ""), false); +}); + +test("streaming with history shows both live and last details", () => { + const last = messageStats(LAST_MSG, 1420); + const live = { tps: 80, active: true, peak: 90, series: [80], gaps: 0, idleMs: 0 }; + const v = buildView({ live, last, session: aggregate([last]), status: "busy" }); + const keys = v.lines.map((l) => l.key); + assert.ok(keys.includes("last")); + assert.ok(keys.includes("live-detail")); +}); + test("live headline segment is toned 'accent', idle is 'value'", () => { const live = { tps: 80, active: true, peak: 80, series: [80], gaps: 0, idleMs: 0 }; const liveView = buildView({ live, status: "busy" }); diff --git a/tools/demo.mjs b/tools/demo.mjs index 0f193fc..b798e03 100755 --- a/tools/demo.mjs +++ b/tools/demo.mjs @@ -17,7 +17,7 @@ import { RateMeter } from "../plugins/tps/meter.js"; import { GenerationTimer } from "../plugins/tps/gen.js"; -import { messageStats, aggregate, tokensFromChars } from "../plugins/tps/tps.js"; +import { messageStats, aggregate } from "../plugins/tps/tps.js"; import { buildView, renderText } from "../plugins/tps/view.js"; const args = parseArgs(process.argv.slice(2)); @@ -131,7 +131,7 @@ async function animate() { const now = Date.now() - start; while (fed < schedule.chunks.length && schedule.chunks[fed].t <= now) { const c = schedule.chunks[fed++]; - const tok = tokensFromChars(c.chars, RATIO); + const tok = c.tokens; timer.push(tok, c.t); meter.push(tok, c.t); } @@ -159,7 +159,7 @@ function runCi() { const schedule = buildSchedule(); const meter = new RateMeter({ windowMs: 3000, seriesLength: 28 }); const timer = new GenerationTimer(); - const toolStart = schedule.chunks.find((_, i) => i > 0 && schedule.chunks[i].t - schedule.chunks[i - 1].t > 1000); + const toolStart = schedule.chunks.find((_, i) => i > 0 && schedule.chunks[i].t - schedule.chunks[i - 1].t >= TOOL_GAP); const stops = [ { label: "warmup", at: TTFT_MS + 500 }, { label: "during tool call", at: (toolStart ? toolStart.t : schedule.completedAt) - 200 }, @@ -172,7 +172,7 @@ function runCi() { for (let now = 0; now <= schedule.completedAt + 2000; now += POLL_MS) { while (fed < schedule.chunks.length && schedule.chunks[fed].t <= now) { const c = schedule.chunks[fed++]; - const tok = tokensFromChars(c.chars, RATIO); + const tok = c.tokens; timer.push(tok, c.t); meter.push(tok, c.t); } @@ -186,7 +186,7 @@ function runCi() { for (const f of frames) { console.log(`${ANSI.muted}── ${f.label} ──${ANSI.reset}`); console.log(colorize(f.view)); - if (!renderText(f.view).includes("TPS")) throw new Error(`demo frame "${f.label}" missing TPS header`); + if (!renderText(f.view).split("\n")[0]?.startsWith("TPS")) throw new Error(`demo frame "${f.label}" missing TPS header`); console.log(); } console.log(`${ANSI.muted}(headline TPS holds across the tool call; the sparkline dips. Animate: run in a TTY without --ci)${ANSI.reset}`); @@ -213,6 +213,7 @@ try { if (CI) runCi(); else await animate(); } catch (err) { + process.stdout.write("\x1b[?25h"); console.error("demo failed:", err); process.exit(1); } diff --git a/tools/install-peers.mjs b/tools/install-peers.mjs index 77e7cd5..a5a4a3b 100755 --- a/tools/install-peers.mjs +++ b/tools/install-peers.mjs @@ -12,17 +12,18 @@ * Dev/CI only — never shipped (the package `files` allowlist excludes tools/). */ -import { mkdtempSync, writeFileSync, mkdirSync, cpSync, existsSync } from "node:fs"; +import { mkdtempSync, writeFileSync, mkdirSync, cpSync, existsSync, rmSync, readdirSync } from "node:fs"; import { join, dirname, resolve } from "node:path"; import { tmpdir } from "node:os"; import { fileURLToPath } from "node:url"; import { execFileSync } from "node:child_process"; -const PEERS = ["solid-js", "@opentui/solid"]; +const PEERS = ["solid-js@1.9.12", "@opentui/solid@0.4.1"]; +const PEER_DIRS = ["solid-js", "@opentui/solid"]; const REPO = resolve(dirname(fileURLToPath(import.meta.url)), ".."); const dest = join(REPO, "node_modules"); -const already = PEERS.every((p) => existsSync(join(dest, ...p.split("/")))); +const already = PEER_DIRS.every((p) => existsSync(join(dest, ...p.split("/")))); if (already && !process.argv.includes("--force")) { console.log("✓ peer runtime already present in node_modules (pass --force to reinstall)"); process.exit(0); @@ -32,14 +33,41 @@ const tmp = mkdtempSync(join(tmpdir(), "tps-peers-")); writeFileSync(join(tmp, "package.json"), JSON.stringify({ name: "tps-peers", private: true }) + "\n"); console.log(`• installing ${PEERS.join(" ")} in a clean project (${tmp}) …`); -execFileSync("npm", ["install", "--no-audit", "--no-fund", ...PEERS], { cwd: tmp, stdio: "inherit" }); +try { + execFileSync("npm", ["install", "--no-audit", "--no-fund", "--save-exact", ...PEERS], { cwd: tmp, stdio: "inherit" }); -mkdirSync(dest, { recursive: true }); -cpSync(join(tmp, "node_modules"), dest, { recursive: true }); + copyMissingPackages(join(tmp, "node_modules"), dest); -const ok = PEERS.every((p) => existsSync(join(dest, ...p.split("/")))); -if (!ok) { - console.error("✗ peer runtime did not land in node_modules"); - process.exit(1); + const ok = PEER_DIRS.every((p) => existsSync(join(dest, ...p.split("/")))); + if (!ok) { + console.error("✗ peer runtime did not land in node_modules"); + process.exit(1); + } + console.log(`✓ peer runtime copied into ${dest}`); +} finally { + rmSync(tmp, { recursive: true, force: true }); +} + +function copyMissingPackages(from, to) { + mkdirSync(to, { recursive: true }); + for (const entry of readdirSync(from, { withFileTypes: true })) { + if (!entry.isDirectory()) continue; + const src = join(from, entry.name); + const dst = join(to, entry.name); + if (entry.name.startsWith("@")) { + mkdirSync(dst, { recursive: true }); + for (const scoped of readdirSync(src, { withFileTypes: true })) { + if (!scoped.isDirectory()) continue; + copyPackage(join(src, scoped.name), join(dst, scoped.name)); + } + } else { + copyPackage(src, dst); + } + } +} + +function copyPackage(src, dst) { + if (existsSync(dst)) return; + mkdirSync(dirname(dst), { recursive: true }); + cpSync(src, dst, { recursive: true, force: false, errorOnExist: true }); } -console.log(`✓ peer runtime copied into ${dest}`); diff --git a/tools/verify-plugin.mjs b/tools/verify-plugin.mjs index b2f2989..da36807 100755 --- a/tools/verify-plugin.mjs +++ b/tools/verify-plugin.mjs @@ -66,8 +66,13 @@ assert( ); // The slot renderer must no-op (return undefined) when there's no session id. -const empty = registered.slots.sidebar_content({}, {}); -assert(empty === undefined, "sidebar_content returns undefined without a session_id"); +try { + const empty = registered.slots.sidebar_content({}, {}); + assert(empty === undefined, "sidebar_content returns undefined without a session_id"); +} catch (err) { + console.error("✗ sidebar_content threw for an empty session:", err); + process.exit(1); +} // Build the component for a real session. We have no live terminal renderer in a // headless harness, so opentui's intrinsic / creation legitimately @@ -79,7 +84,7 @@ try { console.log("✓ sidebar_content built a component for a session"); } catch (err) { const msg = String(err?.message || err); - if (/renderer/i.test(msg)) { + if (msg.includes("No renderer found")) { console.log("✓ sidebar_content ran to JSX construction (no live renderer in headless harness — expected)"); } else { console.error("✗ sidebar_content threw an unexpected error for a real session:", err); diff --git a/tsconfig.json b/tsconfig.json index 04c2d0f..198693e 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -13,7 +13,7 @@ "skipLibCheck": true, "esModuleInterop": true, "resolveJsonModule": true, - "types": [] + "types": ["node"] }, "include": ["plugins/**/*", "tests/**/*", "tools/**/*"], "exclude": ["node_modules"]