diff --git a/package.json b/package.json index 97bb40e..31f2e6e 100644 --- a/package.json +++ b/package.json @@ -64,10 +64,14 @@ "prepublishOnly": "pnpm run build && pnpm test" }, "devDependencies": { - "@opencode-ai/plugin": "latest", + "@opencode-ai/plugin": "1.15.13", + "@opentui/core": "^0.4.5", + "@opentui/keymap": "^0.4.5", + "@opentui/solid": "^0.4.5", "@types/node": "^25.5.0", "oxfmt": "0.41.0", "oxlint": "1.56.0", + "solid-js": "^1.9.14", "typescript": "^5.0.0" }, "peerDependencies": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index ccd2115..b0bc943 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -9,8 +9,17 @@ importers: .: devDependencies: '@opencode-ai/plugin': - specifier: latest - version: 1.2.27 + specifier: 1.15.13 + version: 1.15.13(@opentui/core@0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10))(@opentui/keymap@0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10)) + '@opentui/core': + specifier: ^0.4.5 + version: 0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/keymap': + specifier: ^0.4.5 + version: 0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/solid': + specifier: ^0.4.5 + version: 0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) '@types/node': specifier: ^25.5.0 version: 25.5.0 @@ -20,17 +29,280 @@ importers: oxlint: specifier: 1.56.0 version: 1.56.0 + solid-js: + specifier: ^1.9.14 + version: 1.9.14 typescript: specifier: ^5.0.0 version: 5.9.3 packages: - '@opencode-ai/plugin@1.2.27': - resolution: {integrity: sha512-h+8Bw9v9nghMg7T+SUCTzxlIhOrsTqXW7U0HVLGQST5DjbN7uyCUM51roZWZ8LRjGxzbzFhvPnY1bj8i+ioZyw==} + '@ampproject/remapping@2.3.0': + resolution: {integrity: sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==} + engines: {node: '>=6.0.0'} - '@opencode-ai/sdk@1.2.27': - resolution: {integrity: sha512-Wk0o/I+Fo+wE3zgvlJDs8Fb67KlKqX0PrV8dK5adSDkANq6r4Z25zXJg2iOir+a8ntg3rAcpel1OY4FV/TwRUA==} + '@babel/code-frame@7.29.7': + resolution: {integrity: sha512-Aup7aUOfpbAUg2ROOJN6Iw5f9DMBlzu0mIkm/malLQFN/YQgO48wCj0Kxa3sEHJvPVFg7siR+qRInwXd2qhQKw==} + engines: {node: '>=6.9.0'} + + '@babel/compat-data@7.29.7': + resolution: {integrity: sha512-locTkQyKvwIEgBzVrn8693ebc97F2U8ZHjbXwDXJ5Fn2TCpNwTlKcaKLkdHop5c/icOFE7qt7Q9JC5hnKNa6Gg==} + engines: {node: '>=6.9.0'} + + '@babel/core@7.28.0': + resolution: {integrity: sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ==} + engines: {node: '>=6.9.0'} + + '@babel/generator@7.29.7': + resolution: {integrity: sha512-DkXD5OJQaAQIdZ1bt3UZdEnHAn9Imd3IVBdX03UFe+ony9Ojw5pzr9YVKGDY1jt+Gcn/FnGkNf8r+Vj5NOJWtQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-annotate-as-pure@7.29.7': + resolution: {integrity: sha512-OoK6239jHPuSQOoS0kfTVKn0b/rVTk0seKq4Gd2UMLtmOVLjDC0ki3e+c90Trqv2gMfvJFqkiljrr568+qddiw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-compilation-targets@7.29.7': + resolution: {integrity: sha512-wem6WaBj4NaVYVdNhLPPVacES6ZJ+KBBfSkTMD3YZxbP3rm3Di85tJU5ljaUNhaOynt+Aj0xruhYuzQBt8n71g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-create-class-features-plugin@7.29.7': + resolution: {integrity: sha512-IY3ZD9Tmooqr3TUhc3DUWxiuo8xx1DWLhd5M7hQ+ZWJamqM2BbalrBJb2MisSLoYorOj75U03qULCxQTY9r3hg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-globals@7.29.7': + resolution: {integrity: sha512-3nQVUAtvkKH9zahfWgw96Jc/uFOmjACE1kQz82E2lqWmHBgjzbNlsC22nuQTfahmWeQtTq5nQ/4Nnd2A1wj4zA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-member-expression-to-functions@7.29.7': + resolution: {integrity: sha512-j+7JYmk1JYDtACIGj0QJqqWZjoUpMoEikQGADMaHgCMCSDqd2+P32rfcibUNrGOMWrlzK1WJBdxrB3JJQZwWtg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.18.6': + resolution: {integrity: sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-imports@7.29.7': + resolution: {integrity: sha512-ejHwrQQYcm9xnTivShn2IDOlIzInN34AXskvq9QicvCtEzq1Vzclu/tKF8Jq1Cg8JG2GL6/EmjgsCT7lXepE3g==} + engines: {node: '>=6.9.0'} + + '@babel/helper-module-transforms@7.29.7': + resolution: {integrity: sha512-UPUVSyXbOh627KiCIGQSgwWzGeBKLkaJ9PJEdrngIwMSzxLR4jS4+f1f1jb7VzBbg8nFLaYotvVPFCTqdrmTAg==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-optimise-call-expression@7.29.7': + resolution: {integrity: sha512-+kmGVjcT9RGYzoDwdwEqEvGgKe3BYq+O1iGzjFubaNgZHwYHP6lsF2Yghf4kEuv9BV7tYDZ913aBW9am6YKong==} + engines: {node: '>=6.9.0'} + + '@babel/helper-plugin-utils@7.29.7': + resolution: {integrity: sha512-G7sHYigPY17oO5SYWnfD/0MTBwVR781S/JI643e/JhUYgVgWE/61SoW3NH9KWUKyKq5LVh3npif99Wkt6j86Jw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-replace-supers@7.29.7': + resolution: {integrity: sha512-atfGXWSeCiF4DnKZIfmJfQRkSw9b9gNNXR1kqKjbhG4pGYCOnkp8OcTB8E3NXjBu8NpheSnOeNKz8KT7UNFTmQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0 + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + resolution: {integrity: sha512-brcMGQaVzIeUb+6/bs1Av0f8YuNNjKY2JyvfRCsFuFsdKccEQ5Ges2y74D74NZ1Rz8lKJ9ksJkfqwQFJ/iNEyQ==} + engines: {node: '>=6.9.0'} + + '@babel/helper-string-parser@7.29.7': + resolution: {integrity: sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-identifier@7.29.7': + resolution: {integrity: sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==} + engines: {node: '>=6.9.0'} + + '@babel/helper-validator-option@7.29.7': + resolution: {integrity: sha512-N9ZErrD+yW5geCDtBqnOoxmR8+tNKiGuxKlDpuJxfsqpa2dFcexaziGAE/qoHLiDDreVNMupxGmSoNlyvsA3gw==} + engines: {node: '>=6.9.0'} + + '@babel/helpers@7.29.7': + resolution: {integrity: sha512-1k2lAGRMfHTcwuNYcCNUmaUffmQv8KWMfh2iJUUeRlwlwH4FdNG7mfPI10NPfLHJFThE4Tyr4mv7kTNZOiPuBg==} + engines: {node: '>=6.9.0'} + + '@babel/parser@7.29.7': + resolution: {integrity: sha512-hnORnjP/1P/zFEndoeX+n+t1RwWRJiJpM/jO7FW32Kn9r5+sJB2JWOdYo4L6k78j15eCwY3Gm/7364B1EMwtNg==} + engines: {node: '>=6.0.0'} + hasBin: true + + '@babel/plugin-syntax-jsx@7.29.7': + resolution: {integrity: sha512-TSu8+mHCoEaaCDEZ0I3+6mvTBYR4PCxQwf2z9/r5Tbztv6NaLR3B9thGTTxX2WGuGHJqRiAbKPeGTJ5XWXVg6A==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-syntax-typescript@7.29.7': + resolution: {integrity: sha512-ngr+82Sh0xMz25TPCZi+nC2iTzjfCdWS2ONXTp/PtSCHCgaCNBpdMqgvJ2ccdLlClVZ7sisIgB914j/JFe+RZA==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-modules-commonjs@7.29.7': + resolution: {integrity: sha512-j0vCldybPC5b5dwCQOJ21uKtHzt7hxLygJTg9eF1ScfaikEDNfzn94XoW5Fi+seBR0nCyL23xaBFFkq7dTM8XQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/plugin-transform-typescript@7.29.7': + resolution: {integrity: sha512-jK52h8LaLc7JarhQV2ofeFMts4H7vnOXnqZNA6fYglBTZewRBE51KWt3BUltW1P+KoPsYkHoJeXePuz4zo2LMw==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/preset-typescript@7.27.1': + resolution: {integrity: sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==} + engines: {node: '>=6.9.0'} + peerDependencies: + '@babel/core': ^7.0.0-0 + + '@babel/template@7.29.7': + resolution: {integrity: sha512-puq+Gf35oI24FeN11LkoUQFqv9uwNeWpxXZi/Ji3rRIoKAzKnxRaZ+Gkj0vKS9ZCiTESfng1N9LyOyXvo+m+Gg==} + engines: {node: '>=6.9.0'} + + '@babel/traverse@7.29.7': + resolution: {integrity: sha512-EhlfNQtZ+NK22w5BM61ciuiq1m58ed33Wr1Xan//ZRTy6hgjnwyCffRYwzsGXdASJSUJ1guZILsErh1eQcl+zw==} + engines: {node: '>=6.9.0'} + + '@babel/types@7.29.7': + resolution: {integrity: sha512-4zBIxpPzowiZpusoFkyGVwakdRJUyuH5PxQ/PrqghfdFWWasvnCdPfQXHrenDai+gyLARulZjZowCOj6fjT4pA==} + engines: {node: '>=6.9.0'} + + '@jridgewell/gen-mapping@0.3.13': + resolution: {integrity: sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==} + + '@jridgewell/resolve-uri@3.1.2': + resolution: {integrity: sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==} + engines: {node: '>=6.0.0'} + + '@jridgewell/sourcemap-codec@1.5.5': + resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + + '@jridgewell/trace-mapping@0.3.31': + resolution: {integrity: sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==} + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + resolution: {integrity: sha512-LCkGo6JDfaBhgST7UpPWgNgLINpcpabaHfyz5OBx75nUYxBsaEPxjnyNjWpeb/xBup/682QnBfRBy2/LvPutZQ==} + cpu: [arm64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + resolution: {integrity: sha512-zExlW9zUJKZH/tOtVMttwjKa4Xm/3KcNjnE3dPN92uCktwavMxpgCA3MoJK/DOnTWsQgo224OaST27/mPNAf+w==} + cpu: [x64] + os: [darwin] + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + resolution: {integrity: sha512-dgX0P/9wGPJeHFBG+ZmhgE6bmtMt7NP5CRBGyyktpopdk/mW4POnrpQsSLtKI1dwpc+pPLuXHDh6vvskyQE/sw==} + cpu: [arm64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + resolution: {integrity: sha512-Tg3yX65f5GbtXLkrYEHE5oibZG9epyYWas7FogTTEJeDEF9JlXJzKgXaNhT3UXlTOeA+AfZpYZYZ0uPj7Cfquw==} + cpu: [arm] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + resolution: {integrity: sha512-8TNXMEjJc3QEy7R/x1INhgiU+XakDAFUzBhaz7+Rbrs8NH5UQeHQxxmzsSBJGyV6I1jW79undiQm8tOI+D+8FQ==} + cpu: [x64] + os: [linux] + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + resolution: {integrity: sha512-CmCXPQrkbwExx3j946/PtHWHbYJiCRBRDl4BlkRQcJB/YOwQxJRTpoo7aTsortjgoJ1x7opzTSxn7C+ASSLVjQ==} + cpu: [x64] + os: [win32] + + '@opencode-ai/plugin@1.15.13': + resolution: {integrity: sha512-NFwZGhmxIPijtfz9swPJXDmhOpq4UWP8WjEE7GEMr7FwtJrK/hv6v36nFimed5+OKk+pQCrTJn/vhRW7Io72IA==} + peerDependencies: + '@opentui/core': '>=0.2.16' + '@opentui/keymap': '>=0.2.16' + '@opentui/solid': '>=0.2.16' + peerDependenciesMeta: + '@opentui/core': + optional: true + '@opentui/keymap': + optional: true + '@opentui/solid': + optional: true + + '@opencode-ai/sdk@1.15.13': + resolution: {integrity: sha512-4TwojIoQ8EG6/mVBuUVYZXiFcwNmiiytEnjnvyuvSJjGwFIlw2YIBFxtSVC3FbwwbwHT63teh1RHiQUUC4U5xw==} + + '@opentui/core-darwin-arm64@0.4.5': + resolution: {integrity: sha512-8KUG0oRidnR+oW1RSZJ72/PhZLl+qRRMk5U/mieF4c0SJ5V3tYACpBZAKzQfHNd1f7QzD8FHZct1lPpQgtmkWg==} + cpu: [arm64] + os: [darwin] + + '@opentui/core-darwin-x64@0.4.5': + resolution: {integrity: sha512-R2bocsg55gwjOqCp/MWFgFYzRmsduKegB6nzgFAPCvAD/L5Jf30xpWJWFlSg3x8vxe1L9WJ84dfqa4M7mZZ3wA==} + cpu: [x64] + os: [darwin] + + '@opentui/core-linux-arm64-musl@0.4.5': + resolution: {integrity: sha512-ieqdyKI6EIYPalYAETB2wsdP83hr5Ifi+dFnBFUmdEEFHsoKwBmn2S7bsTOYlX7Bg03F4/YPIg+IvRpeC+cUJw==} + cpu: [arm64] + os: [linux] + libc: [musl] + + '@opentui/core-linux-arm64@0.4.5': + resolution: {integrity: sha512-R4MZ25a4CzOAGVjW9aj1hUfzQGVfCJwrwBDbNs2SXaIvzcZqkxCVtU4FoQ5LsaD0j/BdNQVg2CIfFkFsm1fDuQ==} + cpu: [arm64] + os: [linux] + + '@opentui/core-linux-x64-musl@0.4.5': + resolution: {integrity: sha512-mKVKcIcPiSVVZZsdPSBoWwoa2/TCeQAaMDeHF7PFw2kt5bTXZPP7xxWfRQLCNIcA1eaGl59UuwUWHDR2Ve548Q==} + cpu: [x64] + os: [linux] + libc: [musl] + + '@opentui/core-linux-x64@0.4.5': + resolution: {integrity: sha512-SNyuQoxMKI1vuJhgxSSW96adWM6LqFl2SoS3GM4tGeneGOanVVG2Y06PvlytXvF4cKik97t0rqkVMRetmOs93w==} + cpu: [x64] + os: [linux] + + '@opentui/core-win32-arm64@0.4.5': + resolution: {integrity: sha512-GHTTsqeR45q2Iek9Rb7ty+x/hAKn2jZ1ujlCgPR8LBKyF7h0E1dNFryoZ7ehMc3kJndP1sKn836IemKFqxuDdQ==} + cpu: [arm64] + os: [win32] + + '@opentui/core-win32-x64@0.4.5': + resolution: {integrity: sha512-Y8T/yXCDGagRGiQrtmuB6AhRcPucKFs/Dre3v8kJwNYqDccI4FzUPKclZ7djfmRZNjl7JUqPhZZP/PwDpQocMg==} + cpu: [x64] + os: [win32] + + '@opentui/core@0.4.5': + resolution: {integrity: sha512-JsgRTPkA6e+Vxmumxai6SElOSlRQkbzNKHlCfemlArRiLhfC1IZ9RXJo2QH4xSu+uBOWAM90uss73/pPlkdEig==} + peerDependencies: + web-tree-sitter: 0.25.10 + + '@opentui/keymap@0.4.5': + resolution: {integrity: sha512-S1wzKHhF70zT6bH+VBFY+lSeTImLcIFW28JNQiME8MoPcy6KGPs7rKFSHrb/U7P8rsTJeRfW5A4d1Cy6PKodDg==} + peerDependencies: + '@opentui/react': 0.4.5 + '@opentui/solid': 0.4.5 + react: '>=19.2.0' + solid-js: 1.9.12 + peerDependenciesMeta: + '@opentui/react': + optional: true + '@opentui/solid': + optional: true + react: + optional: true + solid-js: + optional: true + + '@opentui/solid@0.4.5': + resolution: {integrity: sha512-B0RSkXnrtPVfEJOX+Hj+axjLJ3lzbG1BZw5I7Pvb9OPp48Vzg2cW2a3cSa86/q48ndLt647i/XwFPIw/jqnI5g==} + peerDependencies: + solid-js: 1.9.12 '@oxfmt/binding-android-arm-eabi@0.41.0': resolution: {integrity: sha512-REfrqeMKGkfMP+m/ScX4f5jJBSmVNYcpoDF8vP8f8eYPDuPGZmzp56NIUsYmx3h7f6NzC6cE3gqh8GDWrJHCKw==} @@ -276,9 +548,224 @@ packages: cpu: [x64] os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} + '@types/node@25.5.0': resolution: {integrity: sha512-jp2P3tQMSxWugkCUKLRPVUpGaL5MVFwF8RDuSRztfwgN1wmqJeMSbKlnEtQqU8UrhTmzEmZdu2I6v2dpp7XIxw==} + ansi-regex@6.2.2: + resolution: {integrity: sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==} + engines: {node: '>=12'} + + babel-plugin-jsx-dom-expressions@0.40.7: + resolution: {integrity: sha512-/O6JWUmjv03OI9lL2ry9bUjpD5S3PclM55RRJEyCdcFZ5W2SEA/59d+l2hNsk3gI6kiWRdRPdOtqZmsQzFN1pQ==} + peerDependencies: + '@babel/core': ^7.20.12 + + babel-plugin-module-resolver@5.0.2: + resolution: {integrity: sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg==} + + babel-preset-solid@1.9.12: + resolution: {integrity: sha512-LLqnuKVDlKpyBlMPcH6qEvs/wmS9a+NczppxJ3ryS/c0O5IiSFOIBQi9GzyiGDSbcJpx4Gr87jyFTos1MyEuWg==} + peerDependencies: + '@babel/core': ^7.0.0 + solid-js: ^1.9.12 + peerDependenciesMeta: + solid-js: + optional: true + + balanced-match@1.0.2: + resolution: {integrity: sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==} + + baseline-browser-mapping@2.11.8: + resolution: {integrity: sha512-zAgkquC2WYF0PIc6XbNYkA2uuxxFavzgmX61R+dHDUa558V8Ejf8ozTZFR6QzM24RWu4kBcRkhJ5kpz77j9fnQ==} + engines: {node: '>=6.0.0'} + hasBin: true + + brace-expansion@2.1.4: + resolution: {integrity: sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==} + + browserslist@4.28.7: + resolution: {integrity: sha512-JxV13hNrFxqjOc8alRbq9dK1MM79NEXYpma2B2J4wAtpWS5zIEIKqWPGCl7N4o7Uc7B7itylh7SuDujATRyyTw==} + engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} + hasBin: true + + bun-ffi-structs@0.2.4: + resolution: {integrity: sha512-AJzsqoVFs1KBbJbWHIYrVZLDC3NhTqqh25awRXqzoLzmBAKr5oqk6+CwuYHAekKx+VBCYVohBoKuRq40dV+TYg==} + peerDependencies: + typescript: ^5 + + caniuse-lite@1.0.30001806: + resolution: {integrity: sha512-72Cuvd95zbSYPKq6Fhg8eDJRlzgWDf7/mtoZv6Qe/DYNCEBdNxoA3+rZAU2ZhGCpZlns3EssFavaZomckT5Uuw==} + + convert-source-map@2.0.0: + resolution: {integrity: sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + + debug@4.4.3: + resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} + engines: {node: '>=6.0'} + peerDependencies: + supports-color: '*' + peerDependenciesMeta: + supports-color: + optional: true + + detect-libc@2.1.2: + resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==} + engines: {node: '>=8'} + + diff@9.0.0: + resolution: {integrity: sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==} + engines: {node: '>=0.3.1'} + + effect@4.0.0-beta.66: + resolution: {integrity: sha512-4arEr62cziFa8BBVDUwJCJJmaVepXf/kRg7KtC0h8+bufngscrHbwWFhr9c+HonwOF+31U3iD3xUJmw9KzX7Dw==} + + electron-to-chromium@1.5.399: + resolution: {integrity: sha512-lEcqhErbHjXRvd41rnWLpzbyU/IXfIYo7QwaFWmxGeLiLyY2TBCdHnWY88vB+p3ubnihRypDm66panXl7TylLA==} + + emoji-regex@10.6.0: + resolution: {integrity: sha512-toUI84YS5YmxW219erniWD0CIVOo46xGKColeNQRgOzDorgBi1v4D71/OFzgD9GO2UGKIv1C3Sp8DAn0+j5w7A==} + + entities@6.0.1: + resolution: {integrity: sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==} + engines: {node: '>=0.12'} + + entities@7.0.1: + resolution: {integrity: sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA==} + engines: {node: '>=0.12'} + + es-errors@1.3.0: + resolution: {integrity: sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==} + engines: {node: '>= 0.4'} + + escalade@3.2.0: + resolution: {integrity: sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==} + engines: {node: '>=6'} + + fast-check@4.9.0: + resolution: {integrity: sha512-7ms6T7SybUev/PQITciI0yLM2pOSFy5zpG8Ty7tQofcVaQUvrMXp6CBwqF6fThLCLOrfBtuHAtwq6Yu4XPCllg==} + engines: {node: '>=12.17.0'} + + find-babel-config@2.1.2: + resolution: {integrity: sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg==} + + find-my-way-ts@0.1.6: + resolution: {integrity: sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA==} + + find-up@3.0.0: + resolution: {integrity: sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg==} + engines: {node: '>=6'} + + fs.realpath@1.0.0: + resolution: {integrity: sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==} + + function-bind@1.1.2: + resolution: {integrity: sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==} + + gensync@1.0.0-beta.2: + resolution: {integrity: sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==} + engines: {node: '>=6.9.0'} + + get-east-asian-width@1.6.0: + resolution: {integrity: sha512-QRbvDIbx6YklUe6RxeTeleMR0yv3cYH6PsPZHcnVn7xv7zO1BHN8r0XETu8n6Ye3Q+ahtSarc3WgtNWmehIBfA==} + engines: {node: '>=18'} + + glob@9.3.5: + resolution: {integrity: sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q==} + engines: {node: '>=16 || 14 >=14.17'} + 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 + + hasown@2.0.4: + resolution: {integrity: sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==} + engines: {node: '>= 0.4'} + + html-entities@2.3.3: + resolution: {integrity: sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA==} + + ini@6.0.0: + resolution: {integrity: sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ==} + engines: {node: ^20.17.0 || >=22.9.0} + + is-core-module@2.16.2: + resolution: {integrity: sha512-evOr8xfXKxE6qSR0hSXL2r3sd7ALj8+7jQEUvPYcm5sgZFdJ+AYzT6yNmJenvIYQBgIGwfwz08sL8zoL7yq2BA==} + engines: {node: '>= 0.4'} + + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + + js-tokens@4.0.0: + resolution: {integrity: sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==} + + jsesc@3.1.0: + resolution: {integrity: sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==} + engines: {node: '>=6'} + hasBin: true + + json5@2.2.3: + resolution: {integrity: sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==} + engines: {node: '>=6'} + hasBin: true + + kubernetes-types@1.30.0: + resolution: {integrity: sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q==} + + locate-path@3.0.0: + resolution: {integrity: sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A==} + engines: {node: '>=6'} + + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + + lru-cache@5.1.1: + resolution: {integrity: sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==} + + marked@17.0.1: + resolution: {integrity: sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg==} + engines: {node: '>= 20'} + hasBin: true + + minimatch@8.0.7: + resolution: {integrity: sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg==} + engines: {node: '>=16 || 14 >=14.17'} + + minipass@4.2.8: + resolution: {integrity: sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ==} + engines: {node: '>=8'} + + minipass@7.1.3: + resolution: {integrity: sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==} + engines: {node: '>=16 || 14 >=14.17'} + + ms@2.1.3: + resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + + msgpackr-extract@3.0.4: + resolution: {integrity: sha512-4kmO/MdyUIkLIvTPr8VHLil4AtoKIoniWPIEk5+CDy0xnWC84azhSFmuJ7PxZdsYtiP5kEeQsORAVIeMgxT+Hw==} + hasBin: true + + msgpackr@1.12.1: + resolution: {integrity: sha512-4EUH9tQHnMmEgzW/MdAP0KIfa1T9AF+htl0ffe2n5vb2EKn9y2co8ccpgWko6S52Jy1PQZKwRnx5/KkYjtd9MQ==} + + multipasta@0.2.8: + resolution: {integrity: sha512-ZPWuMKyv0cSO29f7hozp+k6+crZbQijV8ipMvxNxRf2SwtYGTX1ZX89Kd20VV4H9Znonx+EQn+iy1wGQsJ+b+Q==} + + node-gyp-build-optional-packages@5.2.2: + resolution: {integrity: sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw==} + hasBin: true + + node-releases@2.0.51: + resolution: {integrity: sha512-wRNIrw4DmVLKQlbgOMdkMx27Wrpzes2hh5Jtbi2bjPd+4wJstWIqP5A+lscnqbm0xxmT5Bpg8Lec5ItEBwx6BQ==} + engines: {node: '>=18'} + oxfmt@0.41.0: resolution: {integrity: sha512-sKLdJZdQ3bw6x9qKiT7+eID4MNEXlDHf5ZacfIircrq6Qwjk0L6t2/JQlZZrVHTXJawK3KaMuBoJnEJPcqCEdg==} engines: {node: ^20.19.0 || >=22.12.0} @@ -294,10 +781,102 @@ packages: oxlint-tsgolint: optional: true + p-limit@2.3.0: + resolution: {integrity: sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==} + engines: {node: '>=6'} + + p-locate@3.0.0: + resolution: {integrity: sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ==} + engines: {node: '>=6'} + + p-try@2.2.0: + resolution: {integrity: sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==} + engines: {node: '>=6'} + + parse5@7.3.0: + resolution: {integrity: sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==} + + path-exists@3.0.0: + resolution: {integrity: sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ==} + engines: {node: '>=4'} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + + path-parse@1.0.7: + resolution: {integrity: sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==} + + path-scurry@1.11.1: + resolution: {integrity: sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA==} + engines: {node: '>=16 || 14 >=14.18'} + + picocolors@1.1.1: + resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + + pkg-up@3.1.0: + resolution: {integrity: sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA==} + engines: {node: '>=8'} + + pure-rand@8.4.2: + resolution: {integrity: sha512-vvuOGgcuPJAirlHvuQw1TrOiw7ptaIXXmIbNuiNOY6lNGJJH49PQ1Kj4nd783nPdQhQdicgOjVI2yI/9BD6/Ng==} + + reselect@4.1.8: + resolution: {integrity: sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ==} + + resolve@1.22.12: + resolution: {integrity: sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA==} + engines: {node: '>= 0.4'} + hasBin: true + + s-js@0.4.9: + resolution: {integrity: sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ==} + + semver@6.3.1: + resolution: {integrity: sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==} + hasBin: true + + seroval-plugins@1.5.6: + resolution: {integrity: sha512-HXuLAX2pu/UByPpaeo/TaMfvMIi+1QqIoPJYCcAtU8QkVNwgR6MPlGuCQTErV1JwraaMbYaWVIBX7mppzGLATQ==} + engines: {node: '>=10'} + peerDependencies: + seroval: ^1.0 + + seroval@1.5.6: + resolution: {integrity: sha512-rVQVWjjSvlINzaQPZH5JFqsqEsIWdTxY3iJZCnTL/5gQbXIRooVZKI60tVCkOVfzcRPejboxO2t0P89dg5mQaA==} + engines: {node: '>=10'} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + + solid-js@1.9.14: + resolution: {integrity: sha512-sAEXC0Kk0S1EDg+8ysEWJDbYhA3RRoEjwuySUGlKIemeo0I5YZfOyumNjNs9Sv3y2nmhD+0rW66ag2HsMuQiGQ==} + + string-width@7.2.0: + resolution: {integrity: sha512-tsaTIkKW9b4N+AEj+SVA+WhJzV7/zMhcSu78mLKWSk7cXMOSHsBKFWUs0fWwq8QyK3MgJBQRX6Gbi4kYbdvGkQ==} + engines: {node: '>=18'} + + strip-ansi@7.1.2: + resolution: {integrity: sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==} + engines: {node: '>=12'} + + supports-preserve-symlinks-flag@1.0.0: + resolution: {integrity: sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==} + engines: {node: '>= 0.4'} + tinypool@2.1.0: resolution: {integrity: sha512-Pugqs6M0m7Lv1I7FtxN4aoyToKg1C4tu+/381vH35y8oENM/Ai7f7C4StcoK4/+BSw9ebcS8jRiVrORFKCALLw==} engines: {node: ^20.0.0 || >=22.0.0} + toml@4.3.0: + resolution: {integrity: sha512-lVb8X9BsPVuH0M4BKeS91tXAmJvCjQ5UIyAbQFaxkKGyUFK2RPkhwaFSQH8vbpl1d23eu/IBH+dwVMHWaq9A5A==} + engines: {node: '>=20'} + typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -306,17 +885,351 @@ packages: undici-types@7.18.2: resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + update-browserslist-db@1.2.3: + resolution: {integrity: sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==} + hasBin: true + peerDependencies: + browserslist: '>= 4.21.0' + + uuid@13.0.2: + resolution: {integrity: sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==} + hasBin: true + + web-tree-sitter@0.25.10: + resolution: {integrity: sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA==} + peerDependencies: + '@types/emscripten': ^1.40.0 + peerDependenciesMeta: + '@types/emscripten': + optional: true + + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + + yallist@3.1.1: + resolution: {integrity: sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==} + + yaml@2.9.0: + resolution: {integrity: sha512-2AvhNX3mb8zd6Zy7INTtSpl1F15HW6Wnqj0srWlkKLcpYl/gMIMJiyuGq2KeI2YFxUPjdlB+3Lc10seMLtL4cA==} + engines: {node: '>= 14.6'} + hasBin: true + zod@4.1.8: resolution: {integrity: sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ==} snapshots: - '@opencode-ai/plugin@1.2.27': + '@ampproject/remapping@2.3.0': + dependencies: + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + + '@babel/code-frame@7.29.7': + dependencies: + '@babel/helper-validator-identifier': 7.29.7 + js-tokens: 4.0.0 + picocolors: 1.1.1 + + '@babel/compat-data@7.29.7': {} + + '@babel/core@7.28.0': + dependencies: + '@ampproject/remapping': 2.3.0 + '@babel/code-frame': 7.29.7 + '@babel/generator': 7.29.7 + '@babel/helper-compilation-targets': 7.29.7 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.28.0) + '@babel/helpers': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/template': 7.29.7 + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + convert-source-map: 2.0.0 + debug: 4.4.3 + gensync: 1.0.0-beta.2 + json5: 2.2.3 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/generator@7.29.7': + dependencies: + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + '@jridgewell/gen-mapping': 0.3.13 + '@jridgewell/trace-mapping': 0.3.31 + jsesc: 3.1.0 + + '@babel/helper-annotate-as-pure@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/helper-compilation-targets@7.29.7': + dependencies: + '@babel/compat-data': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + browserslist: 4.28.7 + lru-cache: 5.1.1 + semver: 6.3.1 + + '@babel/helper-create-class-features-plugin@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@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/core@7.28.0) + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/traverse': 7.29.7 + semver: 6.3.1 + transitivePeerDependencies: + - supports-color + + '@babel/helper-globals@7.29.7': {} + + '@babel/helper-member-expression-to-functions@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-imports@7.18.6': + dependencies: + '@babel/types': 7.29.7 + + '@babel/helper-module-imports@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-module-transforms@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-optimise-call-expression@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/helper-plugin-utils@7.29.7': {} + + '@babel/helper-replace-supers@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-member-expression-to-functions': 7.29.7 + '@babel/helper-optimise-call-expression': 7.29.7 + '@babel/traverse': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-skip-transparent-expression-wrappers@7.29.7': + dependencies: + '@babel/traverse': 7.29.7 + '@babel/types': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/helper-string-parser@7.29.7': {} + + '@babel/helper-validator-identifier@7.29.7': {} + + '@babel/helper-validator-option@7.29.7': {} + + '@babel/helpers@7.29.7': + dependencies: + '@babel/template': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/parser@7.29.7': + dependencies: + '@babel/types': 7.29.7 + + '@babel/plugin-syntax-jsx@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-syntax-typescript@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.29.7 + + '@babel/plugin-transform-modules-commonjs@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-transforms': 7.29.7(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.29.7 + transitivePeerDependencies: + - supports-color + + '@babel/plugin-transform-typescript@7.29.7(@babel/core@7.28.0)': + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-annotate-as-pure': 7.29.7 + '@babel/helper-create-class-features-plugin': 7.29.7(@babel/core@7.28.0) + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-skip-transparent-expression-wrappers': 7.29.7 + '@babel/plugin-syntax-typescript': 7.29.7(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/preset-typescript@7.27.1(@babel/core@7.28.0)': dependencies: - '@opencode-ai/sdk': 1.2.27 + '@babel/core': 7.28.0 + '@babel/helper-plugin-utils': 7.29.7 + '@babel/helper-validator-option': 7.29.7 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.28.0) + '@babel/plugin-transform-modules-commonjs': 7.29.7(@babel/core@7.28.0) + '@babel/plugin-transform-typescript': 7.29.7(@babel/core@7.28.0) + transitivePeerDependencies: + - supports-color + + '@babel/template@7.29.7': + dependencies: + '@babel/code-frame': 7.29.7 + '@babel/parser': 7.29.7 + '@babel/types': 7.29.7 + + '@babel/traverse@7.29.7': + 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.4.3 + transitivePeerDependencies: + - supports-color + + '@babel/types@7.29.7': + dependencies: + '@babel/helper-string-parser': 7.29.7 + '@babel/helper-validator-identifier': 7.29.7 + + '@jridgewell/gen-mapping@0.3.13': + dependencies: + '@jridgewell/sourcemap-codec': 1.5.5 + '@jridgewell/trace-mapping': 0.3.31 + + '@jridgewell/resolve-uri@3.1.2': {} + + '@jridgewell/sourcemap-codec@1.5.5': {} + + '@jridgewell/trace-mapping@0.3.31': + dependencies: + '@jridgewell/resolve-uri': 3.1.2 + '@jridgewell/sourcemap-codec': 1.5.5 + + '@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-arm@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-linux-x64@3.0.4': + optional: true + + '@msgpackr-extract/msgpackr-extract-win32-x64@3.0.4': + optional: true + + '@opencode-ai/plugin@1.15.13(@opentui/core@0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10))(@opentui/keymap@0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))': + dependencies: + '@opencode-ai/sdk': 1.15.13 + effect: 4.0.0-beta.66 zod: 4.1.8 + optionalDependencies: + '@opentui/core': 0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/keymap': 0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) + '@opentui/solid': 0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) + + '@opencode-ai/sdk@1.15.13': + dependencies: + cross-spawn: 7.0.6 - '@opencode-ai/sdk@1.2.27': {} + '@opentui/core-darwin-arm64@0.4.5': + optional: true + + '@opentui/core-darwin-x64@0.4.5': + optional: true + + '@opentui/core-linux-arm64-musl@0.4.5': + optional: true + + '@opentui/core-linux-arm64@0.4.5': + optional: true + + '@opentui/core-linux-x64-musl@0.4.5': + optional: true + + '@opentui/core-linux-x64@0.4.5': + optional: true + + '@opentui/core-win32-arm64@0.4.5': + optional: true + + '@opentui/core-win32-x64@0.4.5': + optional: true + + '@opentui/core@0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10)': + dependencies: + bun-ffi-structs: 0.2.4(typescript@5.9.3) + diff: 9.0.0 + marked: 17.0.1 + string-width: 7.2.0 + strip-ansi: 7.1.2 + web-tree-sitter: 0.25.10 + optionalDependencies: + '@opentui/core-darwin-arm64': 0.4.5 + '@opentui/core-darwin-x64': 0.4.5 + '@opentui/core-linux-arm64': 0.4.5 + '@opentui/core-linux-arm64-musl': 0.4.5 + '@opentui/core-linux-x64': 0.4.5 + '@opentui/core-linux-x64-musl': 0.4.5 + '@opentui/core-win32-arm64': 0.4.5 + '@opentui/core-win32-x64': 0.4.5 + transitivePeerDependencies: + - typescript + + '@opentui/keymap@0.4.5(@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10))(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10)': + dependencies: + '@opentui/core': 0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10) + optionalDependencies: + '@opentui/solid': 0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10) + solid-js: 1.9.14 + transitivePeerDependencies: + - typescript + - web-tree-sitter + + '@opentui/solid@0.4.5(solid-js@1.9.14)(typescript@5.9.3)(web-tree-sitter@0.25.10)': + dependencies: + '@babel/core': 7.28.0 + '@babel/preset-typescript': 7.27.1(@babel/core@7.28.0) + '@opentui/core': 0.4.5(typescript@5.9.3)(web-tree-sitter@0.25.10) + babel-plugin-module-resolver: 5.0.2 + babel-preset-solid: 1.9.12(@babel/core@7.28.0)(solid-js@1.9.14) + entities: 7.0.1 + s-js: 0.4.9 + solid-js: 1.9.14 + transitivePeerDependencies: + - supports-color + - typescript + - web-tree-sitter '@oxfmt/binding-android-arm-eabi@0.41.0': optional: true @@ -432,10 +1345,203 @@ snapshots: '@oxlint/binding-win32-x64-msvc@1.56.0': optional: true + '@standard-schema/spec@1.1.0': {} + '@types/node@25.5.0': dependencies: undici-types: 7.18.2 + ansi-regex@6.2.2: {} + + babel-plugin-jsx-dom-expressions@0.40.7(@babel/core@7.28.0): + dependencies: + '@babel/core': 7.28.0 + '@babel/helper-module-imports': 7.18.6 + '@babel/plugin-syntax-jsx': 7.29.7(@babel/core@7.28.0) + '@babel/types': 7.29.7 + html-entities: 2.3.3 + parse5: 7.3.0 + + babel-plugin-module-resolver@5.0.2: + dependencies: + find-babel-config: 2.1.2 + glob: 9.3.5 + pkg-up: 3.1.0 + reselect: 4.1.8 + resolve: 1.22.12 + + babel-preset-solid@1.9.12(@babel/core@7.28.0)(solid-js@1.9.14): + dependencies: + '@babel/core': 7.28.0 + babel-plugin-jsx-dom-expressions: 0.40.7(@babel/core@7.28.0) + optionalDependencies: + solid-js: 1.9.14 + + balanced-match@1.0.2: {} + + baseline-browser-mapping@2.11.8: {} + + brace-expansion@2.1.4: + dependencies: + balanced-match: 1.0.2 + + browserslist@4.28.7: + dependencies: + baseline-browser-mapping: 2.11.8 + caniuse-lite: 1.0.30001806 + electron-to-chromium: 1.5.399 + node-releases: 2.0.51 + update-browserslist-db: 1.2.3(browserslist@4.28.7) + + bun-ffi-structs@0.2.4(typescript@5.9.3): + dependencies: + typescript: 5.9.3 + + caniuse-lite@1.0.30001806: {} + + convert-source-map@2.0.0: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + + debug@4.4.3: + dependencies: + ms: 2.1.3 + + detect-libc@2.1.2: + optional: true + + diff@9.0.0: {} + + effect@4.0.0-beta.66: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 4.9.0 + find-my-way-ts: 0.1.6 + ini: 6.0.0 + kubernetes-types: 1.30.0 + msgpackr: 1.12.1 + multipasta: 0.2.8 + toml: 4.3.0 + uuid: 13.0.2 + yaml: 2.9.0 + + electron-to-chromium@1.5.399: {} + + emoji-regex@10.6.0: {} + + entities@6.0.1: {} + + entities@7.0.1: {} + + es-errors@1.3.0: {} + + escalade@3.2.0: {} + + fast-check@4.9.0: + dependencies: + pure-rand: 8.4.2 + + find-babel-config@2.1.2: + dependencies: + json5: 2.2.3 + + find-my-way-ts@0.1.6: {} + + find-up@3.0.0: + dependencies: + locate-path: 3.0.0 + + fs.realpath@1.0.0: {} + + function-bind@1.1.2: {} + + gensync@1.0.0-beta.2: {} + + get-east-asian-width@1.6.0: {} + + glob@9.3.5: + dependencies: + fs.realpath: 1.0.0 + minimatch: 8.0.7 + minipass: 4.2.8 + path-scurry: 1.11.1 + + hasown@2.0.4: + dependencies: + function-bind: 1.1.2 + + html-entities@2.3.3: {} + + ini@6.0.0: {} + + is-core-module@2.16.2: + dependencies: + hasown: 2.0.4 + + isexe@2.0.0: {} + + js-tokens@4.0.0: {} + + jsesc@3.1.0: {} + + json5@2.2.3: {} + + kubernetes-types@1.30.0: {} + + locate-path@3.0.0: + dependencies: + p-locate: 3.0.0 + path-exists: 3.0.0 + + lru-cache@10.4.3: {} + + lru-cache@5.1.1: + dependencies: + yallist: 3.1.1 + + marked@17.0.1: {} + + minimatch@8.0.7: + dependencies: + brace-expansion: 2.1.4 + + minipass@4.2.8: {} + + minipass@7.1.3: {} + + ms@2.1.3: {} + + msgpackr-extract@3.0.4: + dependencies: + node-gyp-build-optional-packages: 5.2.2 + 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 + optional: true + + msgpackr@1.12.1: + optionalDependencies: + msgpackr-extract: 3.0.4 + + multipasta@0.2.8: {} + + node-gyp-build-optional-packages@5.2.2: + dependencies: + detect-libc: 2.1.2 + optional: true + + node-releases@2.0.51: {} + oxfmt@0.41.0: dependencies: tinypool: 2.1.0 @@ -482,10 +1588,106 @@ snapshots: '@oxlint/binding-win32-ia32-msvc': 1.56.0 '@oxlint/binding-win32-x64-msvc': 1.56.0 + p-limit@2.3.0: + dependencies: + p-try: 2.2.0 + + p-locate@3.0.0: + dependencies: + p-limit: 2.3.0 + + p-try@2.2.0: {} + + parse5@7.3.0: + dependencies: + entities: 6.0.1 + + path-exists@3.0.0: {} + + path-key@3.1.1: {} + + path-parse@1.0.7: {} + + path-scurry@1.11.1: + dependencies: + lru-cache: 10.4.3 + minipass: 7.1.3 + + picocolors@1.1.1: {} + + pkg-up@3.1.0: + dependencies: + find-up: 3.0.0 + + pure-rand@8.4.2: {} + + reselect@4.1.8: {} + + resolve@1.22.12: + dependencies: + es-errors: 1.3.0 + is-core-module: 2.16.2 + path-parse: 1.0.7 + supports-preserve-symlinks-flag: 1.0.0 + + s-js@0.4.9: {} + + semver@6.3.1: {} + + seroval-plugins@1.5.6(seroval@1.5.6): + dependencies: + seroval: 1.5.6 + + seroval@1.5.6: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + + solid-js@1.9.14: + dependencies: + csstype: 3.2.3 + seroval: 1.5.6 + seroval-plugins: 1.5.6(seroval@1.5.6) + + string-width@7.2.0: + dependencies: + emoji-regex: 10.6.0 + get-east-asian-width: 1.6.0 + strip-ansi: 7.1.2 + + strip-ansi@7.1.2: + dependencies: + ansi-regex: 6.2.2 + + supports-preserve-symlinks-flag@1.0.0: {} + tinypool@2.1.0: {} + toml@4.3.0: {} + typescript@5.9.3: {} undici-types@7.18.2: {} + update-browserslist-db@1.2.3(browserslist@4.28.7): + dependencies: + browserslist: 4.28.7 + escalade: 3.2.0 + picocolors: 1.1.1 + + uuid@13.0.2: {} + + web-tree-sitter@0.25.10: {} + + which@2.0.2: + dependencies: + isexe: 2.0.0 + + yallist@3.1.1: {} + + yaml@2.9.0: {} + zod@4.1.8: {} diff --git a/src/index.test.ts b/src/index.test.ts index 9c356a3..757c7a8 100644 --- a/src/index.test.ts +++ b/src/index.test.ts @@ -127,6 +127,7 @@ const SOURCE_FILES = [ "credentials.ts", "logger.ts", "http.ts", + "usage.ts", ] as const async function copySourceFiles( diff --git a/src/index.ts b/src/index.ts index fff9c6c..4f8d0d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -17,6 +17,7 @@ import { transformBody, transformResponseStream, } from "./transforms.ts" +import { recordUsageFromHeaders } from "./usage.ts" import { getCachedCredentials, reloadCredentialsFromSource, @@ -60,6 +61,15 @@ export { computeVersionSuffix, extractFirstUserMessageText, } from "./signing.ts" +export { + getUsageStatePaths, + parseUsageHeaders, + recordUsageFromHeaders, + resetUsageWriteCache, + writeUsageSnapshot, + type UsageSnapshot, + type UsageWindow, +} from "./usage.ts" function getCliVersion(): string { return process.env.ANTHROPIC_CLI_VERSION ?? config.ccVersion @@ -600,6 +610,13 @@ const plugin: Plugin = async () => { .catch(() => {}) } + // Quota headers ride on every /v1/messages response, including the + // 429 that says the quota is spent. Read from the response that is + // actually being returned — after the 401, rotation and long-context + // retries above — so the recorded numbers match the call that + // counted, not one of the attempts that was thrown away. + recordUsageFromHeaders(response.headers) + // A 401 that survived recovery carries an error body, not an SSE // stream. Deciding here rather than from a flag set mid-flight // makes the retried and non-retried paths behave identically. diff --git a/src/usage.test.ts b/src/usage.test.ts new file mode 100644 index 0000000..3f9dfde --- /dev/null +++ b/src/usage.test.ts @@ -0,0 +1,232 @@ +import assert from "node:assert/strict" +import { describe, it, beforeEach, afterEach } from "node:test" +import { mkdtempSync, readFileSync, existsSync, rmSync } from "node:fs" +import { join } from "node:path" +import { tmpdir, homedir } from "node:os" +import { + getUsageStatePaths, + parseUsageHeaders, + recordUsageFromHeaders, + resetUsageWriteCache, + writeUsageSnapshot, + type UsageSnapshot, +} from "./usage.ts" + +function restore(key: string, value: string | undefined): void { + if (value === undefined) delete process.env[key] + else process.env[key] = value +} + +/** A response carrying the full unified rate-limit family. */ +function quotaHeaders(overrides: Record = {}): Headers { + return new Headers({ + "anthropic-ratelimit-unified-status": "allowed", + "anthropic-ratelimit-unified-representative-claim": "five_hour", + "anthropic-ratelimit-unified-5h-utilization": "0.26", + "anthropic-ratelimit-unified-5h-reset": "1775455200", + "anthropic-ratelimit-unified-7d-utilization": "0.19", + "anthropic-ratelimit-unified-7d-reset": "1775973600", + ...overrides, + }) +} + +describe("parseUsageHeaders", () => { + it("reads both windows as fractions with epoch-second resets", () => { + const snapshot = parseUsageHeaders(quotaHeaders()) + + assert.ok(snapshot) + assert.equal(snapshot.status, "allowed") + assert.equal(snapshot.representativeClaim, "five_hour") + assert.deepEqual(snapshot.fiveHour, { + utilization: 0.26, + resetsAt: 1775455200, + }) + assert.deepEqual(snapshot.sevenDay, { + utilization: 0.19, + resetsAt: 1775973600, + }) + }) + + it("returns null when the response carries no quota headers", () => { + // count_tokens calls and request-validation errors come back without the + // family. Returning a blank snapshot would blank out a good reading. + const snapshot = parseUsageHeaders( + new Headers({ "content-type": "application/json" }), + ) + + assert.equal(snapshot, null) + }) + + it("keeps a window whose reset header is absent", () => { + const headers = quotaHeaders() + headers.delete("anthropic-ratelimit-unified-5h-reset") + + const snapshot = parseUsageHeaders(headers) + + assert.deepEqual(snapshot?.fiveHour, { utilization: 0.26, resetsAt: null }) + }) + + it("reports a rejection, which is what a spent quota looks like", () => { + const snapshot = parseUsageHeaders( + quotaHeaders({ + "anthropic-ratelimit-unified-status": "rejected", + "anthropic-ratelimit-unified-5h-utilization": "1", + }), + ) + + assert.equal(snapshot?.status, "rejected") + assert.equal(snapshot?.fiveHour?.utilization, 1) + }) + + it("survives a status header with no windows at all", () => { + const snapshot = parseUsageHeaders( + new Headers({ "anthropic-ratelimit-unified-status": "allowed" }), + ) + + assert.ok(snapshot) + assert.equal(snapshot.fiveHour, null) + assert.equal(snapshot.sevenDay, null) + }) + + it("ignores a non-numeric utilization rather than emitting NaN", () => { + const snapshot = parseUsageHeaders( + quotaHeaders({ "anthropic-ratelimit-unified-5h-utilization": "n/a" }), + ) + + assert.equal(snapshot?.fiveHour, null) + assert.ok(snapshot?.sevenDay) + }) + + it("captures the overage pool when present", () => { + const snapshot = parseUsageHeaders( + quotaHeaders({ + "anthropic-ratelimit-unified-overage-status": "rejected", + "anthropic-ratelimit-unified-overage-disabled-reason": + "org_level_disabled", + }), + ) + + assert.equal(snapshot?.overage?.status, "rejected") + assert.equal(snapshot?.overage?.disabledReason, "org_level_disabled") + }) + + it("omits the overage pool when the account has none", () => { + assert.equal(parseUsageHeaders(quotaHeaders())?.overage, null) + }) +}) + +describe("getUsageStatePaths", () => { + it("covers every root OpenCode may have been installed under", () => { + const paths = getUsageStatePaths() + + assert.ok(paths.length >= 1) + assert.ok(paths.every((path) => path.endsWith("claude-usage.json"))) + assert.ok( + paths.includes( + join(homedir(), ".local", "share", "opencode", "claude-usage.json"), + ), + ) + if (process.platform === "win32") { + // Different Windows install methods read different roots, and the TUI + // that consumes this has no say in which one it got. + assert.equal(paths.length, 2) + } + }) +}) + +describe("writeUsageSnapshot", () => { + let tmpDir: string + let originalUserProfile: string | undefined + let originalHome: string | undefined + let originalLocalAppData: string | undefined + + const snapshot: UsageSnapshot = { + updatedAt: 1, + status: "allowed", + representativeClaim: "five_hour", + fiveHour: { utilization: 0.26, resetsAt: 1775455200 }, + sevenDay: { utilization: 0.19, resetsAt: 1775973600 }, + overage: null, + } + + beforeEach(() => { + tmpDir = mkdtempSync(join(tmpdir(), "claude-auth-usage-test-")) + originalUserProfile = process.env.USERPROFILE + originalHome = process.env.HOME + originalLocalAppData = process.env.LOCALAPPDATA + // homedir() reads USERPROFILE on Windows and HOME elsewhere, so both have + // to move or this suite writes to the developer's real state file. + process.env.USERPROFILE = tmpDir + process.env.HOME = tmpDir + process.env.LOCALAPPDATA = join(tmpDir, "AppData", "Local") + resetUsageWriteCache() + }) + + afterEach(() => { + restore("USERPROFILE", originalUserProfile) + restore("HOME", originalHome) + restore("LOCALAPPDATA", originalLocalAppData) + rmSync(tmpDir, { recursive: true, force: true }) + resetUsageWriteCache() + }) + + it("creates the directory and writes readable JSON", () => { + writeUsageSnapshot(snapshot) + + const written = getUsageStatePaths().filter((path) => existsSync(path)) + assert.ok(written.length > 0, "expected at least one snapshot on disk") + for (const path of written) { + assert.deepEqual(JSON.parse(readFileSync(path, "utf-8")), snapshot) + } + }) + + it("leaves no .tmp file behind, so a reader never sees a torn write", () => { + writeUsageSnapshot(snapshot) + + for (const path of getUsageStatePaths()) { + assert.ok(!existsSync(`${path}.tmp`)) + } + }) + + it("skips a write when only updatedAt differs", () => { + writeUsageSnapshot(snapshot) + const path = getUsageStatePaths().find((candidate) => existsSync(candidate)) + assert.ok(path) + const first = readFileSync(path, "utf-8") + + // updatedAt changes on every single response; deduplicating on it would + // defeat the check entirely and rewrite both files continuously. + writeUsageSnapshot({ ...snapshot, updatedAt: 999 }) + + assert.equal(readFileSync(path, "utf-8"), first) + }) + + it("writes again once a reading actually changes", () => { + writeUsageSnapshot(snapshot) + writeUsageSnapshot({ + ...snapshot, + updatedAt: 999, + fiveHour: { utilization: 0.5, resetsAt: 1775455200 }, + }) + + const path = getUsageStatePaths().find((candidate) => existsSync(candidate)) + assert.ok(path) + const stored = JSON.parse(readFileSync(path, "utf-8")) as UsageSnapshot + assert.equal(stored.fiveHour?.utilization, 0.5) + }) + + it("recordUsageFromHeaders persists a parsed response", () => { + recordUsageFromHeaders(quotaHeaders()) + + const path = getUsageStatePaths().find((candidate) => existsSync(candidate)) + assert.ok(path) + const stored = JSON.parse(readFileSync(path, "utf-8")) as UsageSnapshot + assert.equal(stored.fiveHour?.utilization, 0.26) + }) + + it("recordUsageFromHeaders writes nothing for a quota-less response", () => { + recordUsageFromHeaders(new Headers({ "content-type": "application/json" })) + + assert.ok(getUsageStatePaths().every((path) => !existsSync(path))) + }) +}) diff --git a/src/usage.ts b/src/usage.ts new file mode 100644 index 0000000..3879ee2 --- /dev/null +++ b/src/usage.ts @@ -0,0 +1,175 @@ +import { existsSync, mkdirSync, renameSync, writeFileSync } from "node:fs" +import { homedir } from "node:os" +import { dirname, join } from "node:path" +import { log } from "./logger.ts" + +/** + * Claude subscription quota, read off the `anthropic-ratelimit-unified-*` + * headers Anthropic returns on every /v1/messages response. + * + * Deliberately the only source. Anthropic also exposes the numbers at + * GET /api/oauth/usage, but polling that spends a request per refresh on an + * undocumented endpoint to learn what the responses already carry for free. + * A window whose reset time has passed is re-derived as empty by the reader + * instead, which is what that poll was really for. + */ +export type UsageWindow = { + /** 0..1 */ + utilization: number + /** Unix epoch SECONDS, or null when the window has never been entered. */ + resetsAt: number | null +} + +export type UsageSnapshot = { + /** Unix epoch MILLISECONDS this snapshot was produced. */ + updatedAt: number + /** allowed | allowed_warning | rejected */ + status: string | null + /** Which window is currently the binding constraint. */ + representativeClaim: string | null + fiveHour: UsageWindow | null + sevenDay: UsageWindow | null + overage: { + status: string | null + utilization: number | null + resetsAt: number | null + disabledReason: string | null + } | null +} + +const USAGE_FILENAME = "claude-usage.json" + +/** + * Mirrors getAuthJsonPaths() in credentials.ts. The same Windows split applies: + * different OpenCode installation methods read from different roots, and the + * TUI process that renders this has no say in which one it got. + */ +export function getUsageStatePaths(): string[] { + const xdgPath = join(homedir(), ".local", "share", "opencode", USAGE_FILENAME) + if (process.platform !== "win32") return [xdgPath] + const appData = + process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local") + return [xdgPath, join(appData, "opencode", USAGE_FILENAME)] +} + +function num(raw: string | null): number | null { + if (raw === null) return null + const parsed = Number(raw) + return Number.isFinite(parsed) ? parsed : null +} + +function readWindow(headers: Headers, abbrev: "5h" | "7d"): UsageWindow | null { + const utilization = num( + headers.get(`anthropic-ratelimit-unified-${abbrev}-utilization`), + ) + if (utilization === null) return null + return { + utilization, + resetsAt: num(headers.get(`anthropic-ratelimit-unified-${abbrev}-reset`)), + } +} + +/** + * Reads the `anthropic-ratelimit-unified-*` family off a live response. + * + * Returns null when the family is absent rather than an empty snapshot — + * count_tokens calls and request-validation errors carry no quota headers, + * and overwriting a good snapshot with blanks would make the display flicker + * to nothing on every such call. + */ +export function parseUsageHeaders(headers: Headers): UsageSnapshot | null { + const fiveHour = readWindow(headers, "5h") + const sevenDay = readWindow(headers, "7d") + const status = headers.get("anthropic-ratelimit-unified-status") + if (!fiveHour && !sevenDay && !status) return null + + const overageStatus = headers.get( + "anthropic-ratelimit-unified-overage-status", + ) + const overageUtilization = num( + headers.get("anthropic-ratelimit-unified-overage-utilization"), + ) + + return { + updatedAt: Date.now(), + status, + representativeClaim: headers.get( + "anthropic-ratelimit-unified-representative-claim", + ), + fiveHour, + sevenDay, + overage: + overageStatus || overageUtilization !== null + ? { + status: overageStatus, + utilization: overageUtilization, + resetsAt: num( + headers.get("anthropic-ratelimit-unified-overage-reset"), + ), + disabledReason: headers.get( + "anthropic-ratelimit-unified-overage-disabled-reason", + ), + } + : null, + } +} + +/** + * Serialised form of the last write, used to skip no-op writes. Compared + * without `updatedAt`, since that changes on every single response and would + * defeat the check entirely. + */ +let lastWritten: string | null = null + +function comparable(snapshot: UsageSnapshot): string { + const { updatedAt: _ignored, ...rest } = snapshot + return JSON.stringify(rest) +} + +export function writeUsageSnapshot(snapshot: UsageSnapshot): void { + const key = comparable(snapshot) + if (key === lastWritten) return + + const serialized = JSON.stringify(snapshot, null, 2) + for (const path of getUsageStatePaths()) { + try { + const dir = dirname(path) + if (!existsSync(dir)) mkdirSync(dir, { recursive: true }) + // Written aside then renamed: the TUI polls this file on a timer and a + // torn read would surface as a parse error exactly when usage is highest. + const tmp = `${path}.tmp` + writeFileSync(tmp, serialized, "utf-8") + renameSync(tmp, path) + } catch (err) { + log("usage_write_error", { + path, + error: err instanceof Error ? err.message : String(err), + }) + } + } + lastWritten = key +} + +/** Parse-and-persist in one call, for the fetch path. Never throws. */ +export function recordUsageFromHeaders(headers: Headers): void { + try { + const snapshot = parseUsageHeaders(headers) + if (!snapshot) return + log("usage_recorded", { + status: snapshot.status, + fiveHour: snapshot.fiveHour?.utilization ?? null, + sevenDay: snapshot.sevenDay?.utilization ?? null, + representativeClaim: snapshot.representativeClaim, + }) + writeUsageSnapshot(snapshot) + } catch (err) { + log("usage_record_threw", { + error: err instanceof Error ? err.message : String(err), + }) + } +} + +/** Test seam: clears the write-deduplication cache. */ +export function resetUsageWriteCache(): void { + lastWritten = null +} diff --git a/tui/claude-usage.tsx b/tui/claude-usage.tsx new file mode 100644 index 0000000..2b667d6 --- /dev/null +++ b/tui/claude-usage.tsx @@ -0,0 +1,372 @@ +/** @jsxImportSource @opentui/solid */ +import type { + TuiPlugin, + TuiPluginModule, + TuiThemeCurrent, +} from "@opencode-ai/plugin/tui" +import type { JSX } from "@opentui/solid" +import { appendFileSync, existsSync, readFileSync } from "node:fs" +import { homedir } from "node:os" +import { join } from "node:path" +import { createMemo, createSignal, onCleanup } from "solid-js" +import type { UsageSnapshot, UsageWindow } from "../src/usage.ts" + +/** + * The TUI runs in its own process and OpenCode swallows a plugin's console + * output, so a failure here is otherwise completely silent. + * + * Tracing turns on by the presence of the log file itself, not just an env var: + * OpenCode's TUI is launched by the user's terminal, so there is no reliable + * moment to export one. Create the file to enable, delete it to disable. + */ +const DEBUG_LOG_PATH = join( + homedir(), + ".local", + "share", + "opencode", + "claude-usage-tui.log", +) +const DEBUG_LOG = + process.env.CLAUDE_AUTH_DEBUG || existsSync(DEBUG_LOG_PATH) + ? DEBUG_LOG_PATH + : null + +function debug(event: string, detail?: unknown): void { + if (!DEBUG_LOG) return + try { + const payload = + detail instanceof Error + ? `${detail.message}\n${detail.stack ?? ""}` + : detail === undefined + ? "" + : JSON.stringify(detail) + appendFileSync( + DEBUG_LOG, + `${new Date().toISOString()} ${event} ${payload}\n`, + "utf-8", + ) + } catch { + // Tracing must never be the reason the plugin fails. + } +} + +debug("module_evaluated", { platform: process.platform }) + +/** + * Renders the Claude subscription quota the server half of this plugin records + * off every Anthropic response. + * + * The two halves cannot talk directly — a server plugin and a TUI plugin are + * separate modules loaded into separate processes — so they meet at the JSON + * file written by src/usage.ts. This side only ever reads it. + */ + +const USAGE_FILENAME = "claude-usage.json" +const POLL_INTERVAL_MS = 5_000 + +/** + * Beyond this the snapshot predates the current 7-day window, so even the + * reset-derived zero below would be a guess. Show nothing instead. + */ +const STALE_AFTER_MS = 7 * 24 * 60 * 60 * 1000 + +/** Must stay in step with getUsageStatePaths() in src/usage.ts. */ +function usageStatePaths(): string[] { + const xdg = join(homedir(), ".local", "share", "opencode", USAGE_FILENAME) + if (process.platform !== "win32") return [xdg] + const appData = + process.env.LOCALAPPDATA ?? join(homedir(), "AppData", "Local") + return [xdg, join(appData, "opencode", USAGE_FILENAME)] +} + +/** + * Both candidate paths are read and the newer wins. Which one the server half + * wrote depends on how OpenCode was installed, and this side has no way to + * know — reading both is cheaper than being wrong. + */ +function readSnapshot(): UsageSnapshot | null { + let newest: UsageSnapshot | null = null + for (const path of usageStatePaths()) { + try { + const parsed = JSON.parse(readFileSync(path, "utf-8")) as UsageSnapshot + if (typeof parsed?.updatedAt !== "number") continue + if (!newest || parsed.updatedAt > newest.updatedAt) newest = parsed + } catch { + // Missing until the first request of the first ever session, and + // momentarily unreadable if caught mid-write. Both are normal. + } + } + if (!newest) return null + return Date.now() - newest.updatedAt > STALE_AFTER_MS ? null : newest +} + +/** + * A window whose reset time has passed is empty, whatever the last response + * said it was. Deriving that here is what lets the server half get away with + * recording quota only from responses: an idle session stops sending requests, + * but its window still empties, and the display follows without a single extra + * API call. + */ +function currentWindow(window: UsageWindow | null): UsageWindow | null { + if (!window) return null + if (window.resetsAt !== null && window.resetsAt * 1000 <= Date.now()) { + return { utilization: 0, resetsAt: null } + } + return window +} + +function percent(utilization: number): number { + return Math.round(utilization * 100) +} + +/** "2h13m", "47m", "<1m" — narrow enough to sit in the prompt bar. */ +function countdown(resetsAtSeconds: number | null): string | null { + if (resetsAtSeconds === null) return null + const remainingMs = resetsAtSeconds * 1000 - Date.now() + if (remainingMs <= 0) return null + const minutes = Math.floor(remainingMs / 60_000) + if (minutes < 1) return "<1m" + const hours = Math.floor(minutes / 60) + return hours > 0 ? `${hours}h${minutes % 60}m` : `${minutes}m` +} + +function severityColor(theme: TuiThemeCurrent, utilization: number) { + const pct = percent(utilization) + if (pct >= 90) return theme.error + if (pct >= 70) return theme.warning + return theme.text +} + +/** + * Where the readout is rendered. Configurable because none of these is + * obviously right and the trade-offs are visual, not technical: + * + * - `sidebar_title` top of the sidebar, directly above Context. Replaces the + * host title block, so this plugin re-renders the title. + * - `sidebar_content` bottom of the sidebar, below Models. + * - `sidebar_footer` the sidebar's last line — note this is `single_winner`, + * so choosing it hides OpenCode's own "Open Code" footer. + * - `session_prompt_right` / `home_prompt_right` + * right edge of the prompt bar; can be clipped on a narrow + * terminal, since it shares that edge with other readouts. + */ +type TuiSlotCtx = { theme: { current: TuiThemeCurrent } } +type SidebarTitleProps = { title: string; share_url?: string } + +const SLOT_NAMES = [ + "sidebar_title", + "sidebar_content", + "sidebar_footer", + "session_prompt_right", + "home_prompt_right", +] as const +type SlotName = (typeof SLOT_NAMES)[number] + +const DEFAULT_SLOTS: SlotName[] = ["sidebar_title"] +const DEFAULT_LABEL = "Claude quota" + +/** + * The sidebar placements sit among the host's own labelled sections, so they + * get a heading to match. The prompt-bar placements are a single line with no + * room for one. + */ +const LABELLED_SLOTS: ReadonlySet = new Set([ + "sidebar_title", + "sidebar_content", +]) + +function resolveLabel(options: unknown): string { + const requested = (options as { label?: unknown } | undefined)?.label + return typeof requested === "string" && requested.trim().length > 0 + ? requested + : DEFAULT_LABEL +} + +function resolveSlots(options: unknown): SlotName[] { + const requested = (options as { slots?: unknown } | undefined)?.slots + if (!Array.isArray(requested)) return DEFAULT_SLOTS + const valid = requested.filter((name): name is SlotName => + SLOT_NAMES.includes(name as SlotName), + ) + const rejected = requested.filter((name) => !valid.includes(name as SlotName)) + if (rejected.length > 0) debug("unknown_slots_ignored", { rejected }) + return valid.length > 0 ? valid : DEFAULT_SLOTS +} + +const plugin: TuiPluginModule & { id: string } = { + id: "claude-auth-usage", + tui: (async (api, options) => { + debug("tui_setup_start") + const initial = readSnapshot() + debug("initial_snapshot", { found: initial !== null }) + const [snapshot, setSnapshot] = createSignal(initial) + + // One poller for the whole plugin rather than one per slot: the prompt-right + // slot remounts on every session switch, and a per-slot interval would make + // the read rate a function of how much the user navigates. + let lastSeen = "" + const poll = () => { + const next = readSnapshot() + const key = JSON.stringify(next) + // Signals compare by identity, so re-setting an equal-but-new object every + // 5s would re-render the prompt bar forever on an idle session. + if (key === lastSeen) return + lastSeen = key + setSnapshot(next) + } + const timer = setInterval(poll, POLL_INTERVAL_MS) + timer.unref?.() + api.lifecycle.onDispose(() => clearInterval(timer)) + + const label = resolveLabel(options) + const rendersTraced = new Map() + const Usage = (props: { theme: TuiThemeCurrent; slot: SlotName }) => { + const seen = rendersTraced.get(props.slot) ?? 0 + if (seen < 2) { + rendersTraced.set(props.slot, seen + 1) + debug("slot_render", { + slot: props.slot, + hasSnapshot: snapshot() !== null, + }) + } + // The countdown has to move without the file changing, so it gets its own + // clock rather than riding on the poll signal. + const [tick, setTick] = createSignal(0) + const clock = setInterval(() => setTick((n) => n + 1), 30_000) + clock.unref?.() + onCleanup(() => clearInterval(clock)) + + const current = () => snapshot() + const fiveHour = () => currentWindow(current()?.fiveHour ?? null) + const sevenDay = () => currentWindow(current()?.sevenDay ?? null) + + /** + * Reading tick() is what makes the clock above do anything: setting a + * signal nothing reads schedules no re-render, so without this the + * countdown would only advance when a response happened to rewrite the + * snapshot — that is, never on the idle session it exists for. + * + * A memo rather than a plain accessor so the two reads below cannot + * straddle a minute boundary and disagree about whether there is any + * time left to show. + */ + const fiveHourCountdown = createMemo(() => { + tick() + const window = fiveHour() + return window ? countdown(window.resetsAt) : null + }) + + // A recorded rejection expires with the window that caused it, so it is + // read off the raw reset time rather than the derived window — which has + // already been zeroed by then and could no longer tell us. + const rejected = () => { + const snap = current() + if (snap?.status !== "rejected") return false + const raw = snap.fiveHour + return !raw?.resetsAt || raw.resetsAt * 1000 > Date.now() + } + + const readout = () => ( + + {rejected() ? ( + limit + ) : ( + "" + )} + {fiveHour() ? ( + <> + 5h{" "} + + {percent(fiveHour()!.utilization)}% + + {fiveHourCountdown() ? ` ${fiveHourCountdown()}` : ""} + + ) : ( + "" + )} + {fiveHour() && sevenDay() ? " · " : ""} + {sevenDay() ? ( + <> + 7d{" "} + + {percent(sevenDay()!.utilization)}% + + + ) : ( + "" + )} + + ) + + if (!LABELLED_SLOTS.has(props.slot)) return readout() + + // paddingTop keeps this off the line above it, matching the gap the host + // puts between its own sidebar sections. + return ( + + {label} + {readout()} + + ) + } + + const enabled = resolveSlots(options) + + const renderers: Record< + SlotName, + (ctx: TuiSlotCtx, props: never) => JSX.Element + > = { + /** + * `sidebar_title` is `single_winner`, so winning it means the host's + * title block stops rendering and this has to stand in for it — hence + * the title and share URL below. That is the price of the only slot + * that sits above Context. + */ + sidebar_title: (ctx, props) => { + const title = props as SidebarTitleProps + return ( + + + {title.title} + + {/* null, not "": a bare string child of a is an orphan text + node in OpenTUI and throws. Inside the empty-string + fallbacks are fine, which is why only this one differs. */} + {title.share_url ? ( + {title.share_url} + ) : null} + + + ) + }, + sidebar_content: (ctx) => ( + + ), + sidebar_footer: (ctx) => ( + + ), + session_prompt_right: (ctx) => ( + + ), + home_prompt_right: (ctx) => ( + + ), + } + + const slots = Object.fromEntries( + enabled.map((name) => [name, renderers[name]]), + ) + const registeredAs = api.slots.register({ slots }) + debug("slots_registered", { registeredAs, slots: enabled }) + }) satisfies TuiPlugin, +} + +export default plugin diff --git a/tui/smoke.ts b/tui/smoke.ts new file mode 100644 index 0000000..c9855b2 --- /dev/null +++ b/tui/smoke.ts @@ -0,0 +1,193 @@ +/** + * Load-and-render smoke test for the TUI plugin. + * + * OpenCode transpiles plugin `.tsx` at import time with OpenTUI's Solid + * transform; this reproduces that, then actually renders the registered slots + * into a test terminal, so a JSX or reactivity error surfaces here rather than + * on the user's next restart. + * + * bun tui/smoke.ts + * + * Runs against a synthetic snapshot in a temporary HOME rather than the real + * one, so it neither depends on this machine having used the plugin before nor + * reports whatever the developer's live quota happens to be. + */ +import "@opentui/solid/runtime-plugin-support" +import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs" +import { tmpdir } from "node:os" +import { join } from "node:path" + +const home = mkdtempSync(join(tmpdir(), "claude-usage-smoke-")) +// homedir() reads USERPROFILE on Windows and HOME elsewhere. Set before the +// plugin is imported: it resolves its state and log paths from them. +process.env.HOME = home +process.env.USERPROFILE = home +process.env.LOCALAPPDATA = join(home, "AppData", "Local") + +const FIVE_HOUR_UTILIZATION = 0.26 +const SEVEN_DAY_UTILIZATION = 0.19 + +function seedSnapshot(): void { + const dir = join(home, ".local", "share", "opencode") + mkdirSync(dir, { recursive: true }) + writeFileSync( + join(dir, "claude-usage.json"), + JSON.stringify({ + updatedAt: Date.now(), + status: "allowed", + representativeClaim: "five_hour", + fiveHour: { + utilization: FIVE_HOUR_UTILIZATION, + // Far enough out that the countdown cannot expire mid-run. + resetsAt: Math.floor(Date.now() / 1000) + 2 * 3600 + 13 * 60, + }, + sevenDay: { + utilization: SEVEN_DAY_UTILIZATION, + resetsAt: Math.floor(Date.now() / 1000) + 5 * 24 * 3600, + }, + overage: null, + }), + "utf-8", + ) +} + +const { testRender } = (await import("@opentui/solid")) as { + testRender: ( + node: () => unknown, + config?: { width?: number; height?: number }, + ) => Promise<{ + renderOnce: () => Promise + captureCharFrame: () => string + }> +} + +const mod = (await import("./claude-usage.tsx")) as { + default?: { id?: string; tui?: unknown } +} +const plugin = mod.default + +if (!plugin) throw new Error("no default export") +if (typeof plugin.id !== "string") throw new Error("missing string id") +if (typeof plugin.tui !== "function") throw new Error("tui is not a function") + +type SlotFn = (ctx: unknown, props: unknown) => unknown + +// Exercise every placement, not just the default, so a broken renderer for an +// opt-in slot is caught here rather than by whoever opts into it. +const requestedSlots = [ + "sidebar_title", + "sidebar_content", + "sidebar_footer", + "session_prompt_right", + "home_prompt_right", +] + +const theme = { + current: { + text: "#e6e6e6", + textMuted: "#8a8a8a", + warning: "#e5c07b", + error: "#e06c75", + }, +} + +async function renderSlots(): Promise> { + let registered: Record = {} + await (plugin!.tui as (api: unknown, options: unknown) => Promise)( + { + lifecycle: { onDispose: () => () => {} }, + slots: { + register: (input: { slots: Record }) => { + registered = input.slots + return plugin!.id as string + }, + }, + }, + { slots: requestedSlots }, + ) + + const names = Object.keys(registered) + if (names.length !== requestedSlots.length) { + throw new Error( + `expected ${requestedSlots.length} slots, registered ${names.join(", ")}`, + ) + } + + const frames: Record = {} + for (const name of names) { + const setup = await testRender( + () => + registered[name]!( + { theme }, + { + session_id: "ses_abcdef123456", + title: "Display session status in opencode", + }, + ), + { width: 60, height: 4 }, + ) + await setup.renderOnce() + frames[name] = setup + .captureCharFrame() + .split("\n") + .map((line) => line.trimEnd()) + .filter((line) => line.length > 0) + } + return frames +} + +try { + console.log("id:", plugin.id) + + // Phase 1: no snapshot on disk — the readout must stay out of the way rather + // than render a half-empty row of labels. + const empty = await renderSlots() + for (const [name, frame] of Object.entries(empty)) { + const showsQuota = frame.some((line) => line.includes("5h")) + if (showsQuota) { + throw new Error(`${name} rendered a quota with no snapshot on disk`) + } + } + console.log("no-snapshot: every slot renders no readout (ok)") + + // Phase 2: with a snapshot, every placement must show the readout, and only + // the sidebar ones carry the heading. + seedSnapshot() + const seeded = await renderSlots() + for (const [name, frame] of Object.entries(seeded)) { + console.log(`[${name}] ${JSON.stringify(frame)}`) + if (frame.length === 0) throw new Error(`${name} rendered nothing`) + + const expected = `5h ${Math.round(FIVE_HOUR_UTILIZATION * 100)}%` + if (!frame.some((line) => line.includes(expected))) { + throw new Error(`${name} is missing "${expected}"`) + } + // Shape, not an exact value: the countdown floors to the minute, so the + // second that passes between seeding and rendering turns 2h13m into 2h12m. + if (!frame.some((line) => /\b\d+h\d+m\b/.test(line))) { + throw new Error(`${name} is missing the reset countdown`) + } + // A countdown that lost a race against the minute boundary renders as the + // string "null" rather than disappearing, so assert it never appears. + if (frame.some((line) => line.includes("null"))) { + throw new Error(`${name} rendered a null countdown`) + } + + const labelled = name === "sidebar_title" || name === "sidebar_content" + const hasLabel = frame.some((line) => line.includes("Claude quota")) + if (labelled !== hasLabel) { + throw new Error( + `${name}: expected label ${labelled}, got ${hasLabel} — ${JSON.stringify(frame)}`, + ) + } + } + + console.log("OK") +} catch (error) { + // An unhandled top-level rejection does not set a non-zero exit code here, + // which would leave this passing however badly it failed. + console.error(error instanceof Error ? error.message : error) + process.exitCode = 1 +} finally { + rmSync(home, { recursive: true, force: true }) +} diff --git a/tui/tsconfig.json b/tui/tsconfig.json new file mode 100644 index 0000000..20a30df --- /dev/null +++ b/tui/tsconfig.json @@ -0,0 +1,11 @@ +{ + "extends": "../tsconfig.json", + "compilerOptions": { + "noEmit": true, + "rootDir": "..", + "jsx": "preserve", + "jsxImportSource": "@opentui/solid", + "lib": ["ESNext", "DOM", "DOM.Iterable"] + }, + "include": ["./**/*.tsx", "../src/usage.ts"] +}