From c433c719283b3b3f37a9636abff20738d0f3685e Mon Sep 17 00:00:00 2001 From: justin Date: Thu, 30 Apr 2026 21:04:28 -0400 Subject: [PATCH 1/2] feat: add kimi usage command --- bun.lock | 396 ++++++++++++++++++++++++++++++++++++++ package.json | 9 +- src/auth-refresh.ts | 119 ++++++++++++ src/auth-store.ts | 88 +++++++++ src/index.ts | 157 ++------------- src/tui.tsx | 146 ++++++++++++++ src/usage.ts | 139 +++++++++++++ test/auth-refresh.test.ts | 63 ++++++ test/exports.test.ts | 9 + test/usage.test.ts | 41 ++++ tsconfig.json | 4 +- tsconfig.tests.json | 2 +- 12 files changed, 1022 insertions(+), 151 deletions(-) create mode 100644 src/auth-refresh.ts create mode 100644 src/auth-store.ts create mode 100644 src/tui.tsx create mode 100644 src/usage.ts create mode 100644 test/auth-refresh.test.ts create mode 100644 test/usage.test.ts diff --git a/bun.lock b/bun.lock index 6228d7f..49c7188 100644 --- a/bun.lock +++ b/bun.lock @@ -6,6 +6,8 @@ "name": "opencode-kimi-full", "devDependencies": { "@opencode-ai/plugin": "^1.4.7", + "@opentui/core": "0.1.99", + "@opentui/solid": "0.1.99", "@types/node": "^22.0.0", "typescript": "^5.6.0", }, @@ -15,6 +17,130 @@ }, }, "packages": { + "@ampproject/remapping": ["@ampproject/remapping@2.3.0", "", { "dependencies": { "@jridgewell/gen-mapping": "^0.3.5", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw=="], + + "@babel/code-frame": ["@babel/code-frame@7.29.0", "", { "dependencies": { "@babel/helper-validator-identifier": "^7.28.5", "js-tokens": "^4.0.0", "picocolors": "^1.1.1" } }, "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw=="], + + "@babel/compat-data": ["@babel/compat-data@7.29.0", "", {}, "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg=="], + + "@babel/core": ["@babel/core@7.28.0", "", { "dependencies": { "@ampproject/remapping": "^2.2.0", "@babel/code-frame": "^7.27.1", "@babel/generator": "^7.28.0", "@babel/helper-compilation-targets": "^7.27.2", "@babel/helper-module-transforms": "^7.27.3", "@babel/helpers": "^7.27.6", "@babel/parser": "^7.28.0", "@babel/template": "^7.27.2", "@babel/traverse": "^7.28.0", "@babel/types": "^7.28.0", "convert-source-map": "^2.0.0", "debug": "^4.1.0", "gensync": "^1.0.0-beta.2", "json5": "^2.2.3", "semver": "^6.3.1" } }, "sha512-UlLAnTPrFdNGoFtbSXwcGFQBtQZJCNjaN6hQNP3UPvuNXT1i82N26KL3dZeIpNalWywr9IuQuncaAfUaS1g6sQ=="], + + "@babel/generator": ["@babel/generator@7.29.1", "", { "dependencies": { "@babel/parser": "^7.29.0", "@babel/types": "^7.29.0", "@jridgewell/gen-mapping": "^0.3.12", "@jridgewell/trace-mapping": "^0.3.28", "jsesc": "^3.0.2" } }, "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw=="], + + "@babel/helper-annotate-as-pure": ["@babel/helper-annotate-as-pure@7.27.3", "", { "dependencies": { "@babel/types": "^7.27.3" } }, "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg=="], + + "@babel/helper-compilation-targets": ["@babel/helper-compilation-targets@7.28.6", "", { "dependencies": { "@babel/compat-data": "^7.28.6", "@babel/helper-validator-option": "^7.27.1", "browserslist": "^4.24.0", "lru-cache": "^5.1.1", "semver": "^6.3.1" } }, "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA=="], + + "@babel/helper-create-class-features-plugin": ["@babel/helper-create-class-features-plugin@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/helper-replace-supers": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/traverse": "^7.28.6", "semver": "^6.3.1" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-dTOdvsjnG3xNT9Y0AUg1wAl38y+4Rl4sf9caSQZOXdNqVn+H+HbbJ4IyyHaIqNR6SW9oJpA/RuRjsjCw2IdIow=="], + + "@babel/helper-globals": ["@babel/helper-globals@7.28.0", "", {}, "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw=="], + + "@babel/helper-member-expression-to-functions": ["@babel/helper-member-expression-to-functions@7.28.5", "", { "dependencies": { "@babel/traverse": "^7.28.5", "@babel/types": "^7.28.5" } }, "sha512-cwM7SBRZcPCLgl8a7cY0soT1SptSzAlMH39vwiRpOQkJlh53r5hdHwLSCZpQdVLT39sZt+CRpNwYG4Y2v77atg=="], + + "@babel/helper-module-imports": ["@babel/helper-module-imports@7.28.6", "", { "dependencies": { "@babel/traverse": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw=="], + + "@babel/helper-module-transforms": ["@babel/helper-module-transforms@7.28.6", "", { "dependencies": { "@babel/helper-module-imports": "^7.28.6", "@babel/helper-validator-identifier": "^7.28.5", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA=="], + + "@babel/helper-optimise-call-expression": ["@babel/helper-optimise-call-expression@7.27.1", "", { "dependencies": { "@babel/types": "^7.27.1" } }, "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw=="], + + "@babel/helper-plugin-utils": ["@babel/helper-plugin-utils@7.28.6", "", {}, "sha512-S9gzZ/bz83GRysI7gAD4wPT/AI3uCnY+9xn+Mx/KPs2JwHJIz1W8PZkg2cqyt3RNOBM8ejcXhV6y8Og7ly/Dug=="], + + "@babel/helper-replace-supers": ["@babel/helper-replace-supers@7.28.6", "", { "dependencies": { "@babel/helper-member-expression-to-functions": "^7.28.5", "@babel/helper-optimise-call-expression": "^7.27.1", "@babel/traverse": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0" } }, "sha512-mq8e+laIk94/yFec3DxSjCRD2Z0TAjhVbEJY3UQrlwVo15Lmt7C2wAUbK4bjnTs4APkwsYLTahXRraQXhb1WCg=="], + + "@babel/helper-skip-transparent-expression-wrappers": ["@babel/helper-skip-transparent-expression-wrappers@7.27.1", "", { "dependencies": { "@babel/traverse": "^7.27.1", "@babel/types": "^7.27.1" } }, "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg=="], + + "@babel/helper-string-parser": ["@babel/helper-string-parser@7.27.1", "", {}, "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA=="], + + "@babel/helper-validator-identifier": ["@babel/helper-validator-identifier@7.28.5", "", {}, "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q=="], + + "@babel/helper-validator-option": ["@babel/helper-validator-option@7.27.1", "", {}, "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg=="], + + "@babel/helpers": ["@babel/helpers@7.29.2", "", { "dependencies": { "@babel/template": "^7.28.6", "@babel/types": "^7.29.0" } }, "sha512-HoGuUs4sCZNezVEKdVcwqmZN8GoHirLUcLaYVNBK2J0DadGtdcqgr3BCbvH8+XUo4NGjNl3VOtSjEKNzqfFgKw=="], + + "@babel/parser": ["@babel/parser@7.29.2", "", { "dependencies": { "@babel/types": "^7.29.0" }, "bin": "./bin/babel-parser.js" }, "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA=="], + + "@babel/plugin-syntax-jsx": ["@babel/plugin-syntax-jsx@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-wgEmr06G6sIpqr8YDwA2dSRTE3bJ+V0IfpzfSY3Lfgd7YWOaAdlykvJi13ZKBt8cZHfgH1IXN+CL656W3uUa4w=="], + + "@babel/plugin-syntax-typescript": ["@babel/plugin-syntax-typescript@7.28.6", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-+nDNmQye7nlnuuHDboPbGm00Vqg3oO8niRRL27/4LYHUsHYh0zJ1xWOz0uRwNFmM1Avzk8wZbc6rdiYhomzv/A=="], + + "@babel/plugin-transform-modules-commonjs": ["@babel/plugin-transform-modules-commonjs@7.28.6", "", { "dependencies": { "@babel/helper-module-transforms": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-jppVbf8IV9iWWwWTQIxJMAJCWBuuKx71475wHwYytrRGQ2CWiDvYlADQno3tcYpS/T2UUWFQp3nVtYfK/YBQrA=="], + + "@babel/plugin-transform-typescript": ["@babel/plugin-transform-typescript@7.28.6", "", { "dependencies": { "@babel/helper-annotate-as-pure": "^7.27.3", "@babel/helper-create-class-features-plugin": "^7.28.6", "@babel/helper-plugin-utils": "^7.28.6", "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", "@babel/plugin-syntax-typescript": "^7.28.6" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-0YWL2RFxOqEm9Efk5PvreamxPME8OyY0wM5wh5lHjF+VtVhdneCWGzZeSqzOfiobVqQaNCd2z0tQvnI9DaPWPw=="], + + "@babel/preset-typescript": ["@babel/preset-typescript@7.27.1", "", { "dependencies": { "@babel/helper-plugin-utils": "^7.27.1", "@babel/helper-validator-option": "^7.27.1", "@babel/plugin-syntax-jsx": "^7.27.1", "@babel/plugin-transform-modules-commonjs": "^7.27.1", "@babel/plugin-transform-typescript": "^7.27.1" }, "peerDependencies": { "@babel/core": "^7.0.0-0" } }, "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ=="], + + "@babel/template": ["@babel/template@7.28.6", "", { "dependencies": { "@babel/code-frame": "^7.28.6", "@babel/parser": "^7.28.6", "@babel/types": "^7.28.6" } }, "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ=="], + + "@babel/traverse": ["@babel/traverse@7.29.0", "", { "dependencies": { "@babel/code-frame": "^7.29.0", "@babel/generator": "^7.29.0", "@babel/helper-globals": "^7.28.0", "@babel/parser": "^7.29.0", "@babel/template": "^7.28.6", "@babel/types": "^7.29.0", "debug": "^4.3.1" } }, "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA=="], + + "@babel/types": ["@babel/types@7.29.0", "", { "dependencies": { "@babel/helper-string-parser": "^7.27.1", "@babel/helper-validator-identifier": "^7.28.5" } }, "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A=="], + + "@dimforge/rapier2d-simd-compat": ["@dimforge/rapier2d-simd-compat@0.17.3", "", {}, "sha512-bijvwWz6NHsNj5e5i1vtd3dU2pDhthSaTUZSh14DUGGKJfw8eMnlWZsxwHBxB/a3AXVNDjL9abuHw1k9FGR+jg=="], + + "@jimp/core": ["@jimp/core@1.6.0", "", { "dependencies": { "@jimp/file-ops": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "await-to-js": "^3.0.0", "exif-parser": "^0.1.12", "file-type": "^16.0.0", "mime": "3" } }, "sha512-EQQlKU3s9QfdJqiSrZWNTxBs3rKXgO2W+GxNXDtwchF3a4IqxDheFX1ti+Env9hdJXDiYLp2jTRjlxhPthsk8w=="], + + "@jimp/diff": ["@jimp/diff@1.6.0", "", { "dependencies": { "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "pixelmatch": "^5.3.0" } }, "sha512-+yUAQ5gvRC5D1WHYxjBHZI7JBRusGGSLf8AmPRPCenTzh4PA+wZ1xv2+cYqQwTfQHU5tXYOhA0xDytfHUf1Zyw=="], + + "@jimp/file-ops": ["@jimp/file-ops@1.6.0", "", {}, "sha512-Dx/bVDmgnRe1AlniRpCKrGRm5YvGmUwbDzt+MAkgmLGf+jvBT75hmMEZ003n9HQI/aPnm/YKnXjg/hOpzNCpHQ=="], + + "@jimp/js-bmp": ["@jimp/js-bmp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "bmp-ts": "^1.0.9" } }, "sha512-FU6Q5PC/e3yzLyBDXupR3SnL3htU7S3KEs4e6rjDP6gNEOXRFsWs6YD3hXuXd50jd8ummy+q2WSwuGkr8wi+Gw=="], + + "@jimp/js-gif": ["@jimp/js-gif@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "gifwrap": "^0.10.1", "omggif": "^1.0.10" } }, "sha512-N9CZPHOrJTsAUoWkWZstLPpwT5AwJ0wge+47+ix3++SdSL/H2QzyMqxbcDYNFe4MoI5MIhATfb0/dl/wmX221g=="], + + "@jimp/js-jpeg": ["@jimp/js-jpeg@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "jpeg-js": "^0.4.4" } }, "sha512-6vgFDqeusblf5Pok6B2DUiMXplH8RhIKAryj1yn+007SIAQ0khM1Uptxmpku/0MfbClx2r7pnJv9gWpAEJdMVA=="], + + "@jimp/js-png": ["@jimp/js-png@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "pngjs": "^7.0.0" } }, "sha512-AbQHScy3hDDgMRNfG0tPjL88AV6qKAILGReIa3ATpW5QFjBKpisvUaOqhzJ7Reic1oawx3Riyv152gaPfqsBVg=="], + + "@jimp/js-tiff": ["@jimp/js-tiff@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "utif2": "^4.1.0" } }, "sha512-zhReR8/7KO+adijj3h0ZQUOiun3mXUv79zYEAKvE0O+rP7EhgtKvWJOZfRzdZSNv0Pu1rKtgM72qgtwe2tFvyw=="], + + "@jimp/plugin-blit": ["@jimp/plugin-blit@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M+uRWl1csi7qilnSK8uxK4RJMSuVeBiO1AY0+7APnfUbQNZm6hCe0CCFv1Iyw1D/Dhb8ph8fQgm5mwM0eSxgVA=="], + + "@jimp/plugin-blur": ["@jimp/plugin-blur@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-zrM7iic1OTwUCb0g/rN5y+UnmdEsT3IfuCXCJJNs8SZzP0MkZ1eTvuwK9ZidCuMo4+J3xkzCidRwYXB5CyGZTw=="], + + "@jimp/plugin-circle": ["@jimp/plugin-circle@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-xt1Gp+LtdMKAXfDp3HNaG30SPZW6AQ7dtAtTnoRKorRi+5yCJjKqXRgkewS5bvj8DEh87Ko1ydJfzqS3P2tdWw=="], + + "@jimp/plugin-color": ["@jimp/plugin-color@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "tinycolor2": "^1.6.0", "zod": "^3.23.8" } }, "sha512-J5q8IVCpkBsxIXM+45XOXTrsyfblyMZg3a9eAo0P7VPH4+CrvyNQwaYatbAIamSIN1YzxmO3DkIZXzRjFSz1SA=="], + + "@jimp/plugin-contain": ["@jimp/plugin-contain@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-oN/n+Vdq/Qg9bB4yOBOxtY9IPAtEfES8J1n9Ddx+XhGBYT1/QTU/JYkGaAkIGoPnyYvmLEDqMz2SGihqlpqfzQ=="], + + "@jimp/plugin-cover": ["@jimp/plugin-cover@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Iow0h6yqSC269YUJ8HC3Q/MpCi2V55sMlbkkTTx4zPvd8mWZlC0ykrNDeAy9IJegrQ7v5E99rJwmQu25lygKLA=="], + + "@jimp/plugin-crop": ["@jimp/plugin-crop@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-KqZkEhvs+21USdySCUDI+GFa393eDIzbi1smBqkUPTE+pRwSWMAf01D5OC3ZWB+xZsNla93BDS9iCkLHA8wang=="], + + "@jimp/plugin-displace": ["@jimp/plugin-displace@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-4Y10X9qwr5F+Bo5ME356XSACEF55485j5nGdiyJ9hYzjQP9nGgxNJaZ4SAOqpd+k5sFaIeD7SQ0Occ26uIng5Q=="], + + "@jimp/plugin-dither": ["@jimp/plugin-dither@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0" } }, "sha512-600d1RxY0pKwgyU0tgMahLNKsqEcxGdbgXadCiVCoGd6V6glyCvkNrnnwC0n5aJ56Htkj88PToSdF88tNVZEEQ=="], + + "@jimp/plugin-fisheye": ["@jimp/plugin-fisheye@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-E5QHKWSCBFtpgZarlmN3Q6+rTQxjirFqo44ohoTjzYVrDI6B6beXNnPIThJgPr0Y9GwfzgyarKvQuQuqCnnfbA=="], + + "@jimp/plugin-flip": ["@jimp/plugin-flip@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-/+rJVDuBIVOgwoyVkBjUFHtP+wmW0r+r5OQ2GpatQofToPVbJw1DdYWXlwviSx7hvixTWLKVgRWQ5Dw862emDg=="], + + "@jimp/plugin-hash": ["@jimp/plugin-hash@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "any-base": "^1.1.0" } }, "sha512-wWzl0kTpDJgYVbZdajTf+4NBSKvmI3bRI8q6EH9CVeIHps9VWVsUvEyb7rpbcwVLWYuzDtP2R0lTT6WeBNQH9Q=="], + + "@jimp/plugin-mask": ["@jimp/plugin-mask@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-Cwy7ExSJMZszvkad8NV8o/Z92X2kFUFM8mcDAhNVxU0Q6tA0op2UKRJY51eoK8r6eds/qak3FQkXakvNabdLnA=="], + + "@jimp/plugin-print": ["@jimp/plugin-print@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/types": "1.6.0", "parse-bmfont-ascii": "^1.0.6", "parse-bmfont-binary": "^1.0.6", "parse-bmfont-xml": "^1.1.6", "simple-xml-to-json": "^1.2.2", "zod": "^3.23.8" } }, "sha512-zarTIJi8fjoGMSI/M3Xh5yY9T65p03XJmPsuNet19K/Q7mwRU6EV2pfj+28++2PV2NJ+htDF5uecAlnGyxFN2A=="], + + "@jimp/plugin-quantize": ["@jimp/plugin-quantize@1.6.0", "", { "dependencies": { "image-q": "^4.0.0", "zod": "^3.23.8" } }, "sha512-EmzZ/s9StYQwbpG6rUGBCisc3f64JIhSH+ncTJd+iFGtGo0YvSeMdAd+zqgiHpfZoOL54dNavZNjF4otK+mvlg=="], + + "@jimp/plugin-resize": ["@jimp/plugin-resize@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/types": "1.6.0", "zod": "^3.23.8" } }, "sha512-uSUD1mqXN9i1SGSz5ov3keRZ7S9L32/mAQG08wUwZiEi5FpbV0K8A8l1zkazAIZi9IJzLlTauRNU41Mi8IF9fA=="], + + "@jimp/plugin-rotate": ["@jimp/plugin-rotate@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-JagdjBLnUZGSG4xjCLkIpQOZZ3Mjbg8aGCCi4G69qR+OjNpOeGI7N2EQlfK/WE8BEHOW5vdjSyglNqcYbQBWRw=="], + + "@jimp/plugin-threshold": ["@jimp/plugin-threshold@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0", "zod": "^3.23.8" } }, "sha512-M59m5dzLoHOVWdM41O8z9SyySzcDn43xHseOH0HavjsfQsT56GGCC4QzU1banJidbUrePhzoEdS42uFE8Fei8w=="], + + "@jimp/types": ["@jimp/types@1.6.0", "", { "dependencies": { "zod": "^3.23.8" } }, "sha512-7UfRsiKo5GZTAATxm2qQ7jqmUXP0DxTArztllTcYdyw6Xi5oT4RaoXynVtCD4UyLK5gJgkZJcwonoijrhYFKfg=="], + + "@jimp/utils": ["@jimp/utils@1.6.0", "", { "dependencies": { "@jimp/types": "1.6.0", "tinycolor2": "^1.6.0" } }, "sha512-gqFTGEosKbOkYF/WFj26jMHOI5OH2jeP1MmC/zbK6BF6VJBf8rIC5898dPfSzZEbSA0wbbV5slbntWVc5PKLFA=="], + + "@jridgewell/gen-mapping": ["@jridgewell/gen-mapping@0.3.13", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0", "@jridgewell/trace-mapping": "^0.3.24" } }, "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA=="], + + "@jridgewell/resolve-uri": ["@jridgewell/resolve-uri@3.1.2", "", {}, "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw=="], + + "@jridgewell/sourcemap-codec": ["@jridgewell/sourcemap-codec@1.5.5", "", {}, "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og=="], + + "@jridgewell/trace-mapping": ["@jridgewell/trace-mapping@0.3.31", "", { "dependencies": { "@jridgewell/resolve-uri": "^3.1.0", "@jridgewell/sourcemap-codec": "^1.4.14" } }, "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw=="], + "@msgpackr-extract/msgpackr-extract-darwin-arm64": ["@msgpackr-extract/msgpackr-extract-darwin-arm64@3.0.3", "", { "os": "darwin", "cpu": "arm64" }, "sha512-QZHtlVgbAdy2zAqNA9Gu1UpIuI8Xvsd1v8ic6B2pZmeFnFcMWiPLfWXh7TVw4eGEZ/C9TH281KwhVoeQUKbyjw=="], "@msgpackr-extract/msgpackr-extract-darwin-x64": ["@msgpackr-extract/msgpackr-extract-darwin-x64@3.0.3", "", { "os": "darwin", "cpu": "x64" }, "sha512-mdzd3AVzYKuUmiWOQ8GNhl64/IoFGol569zNRdkLReh6LRLHOXxU4U8eq0JwaD8iFHdVGqSy4IjFL4reoWCDFw=="], @@ -31,26 +157,158 @@ "@opencode-ai/sdk": ["@opencode-ai/sdk@1.4.7", "", { "dependencies": { "cross-spawn": "7.0.6" } }, "sha512-onEtaooQyoDP5gTShQeQSf0Sd8V7949G9pPNyIyRXnVtFqyDIhUDLGtL/a/+EIW9x5s+Y6lDy/3oVoGMvQ0rQQ=="], + "@opentui/core": ["@opentui/core@0.1.99", "", { "dependencies": { "bun-ffi-structs": "0.1.2", "diff": "8.0.2", "jimp": "1.6.0", "marked": "17.0.1", "yoga-layout": "3.2.1" }, "optionalDependencies": { "@dimforge/rapier2d-simd-compat": "^0.17.3", "@opentui/core-darwin-arm64": "0.1.99", "@opentui/core-darwin-x64": "0.1.99", "@opentui/core-linux-arm64": "0.1.99", "@opentui/core-linux-x64": "0.1.99", "@opentui/core-win32-arm64": "0.1.99", "@opentui/core-win32-x64": "0.1.99", "bun-webgpu": "0.1.5", "planck": "^1.4.2", "three": "0.177.0" }, "peerDependencies": { "web-tree-sitter": "0.25.10" } }, "sha512-I3+AEgGzqNWIpWX9g2WOscSPwtQDNOm4KlBjxBWCZjLxkF07u77heWXF7OiAdhKLtNUW6TFiyt6yznqAZPdG3A=="], + + "@opentui/core-darwin-arm64": ["@opentui/core-darwin-arm64@0.1.99", "", { "os": "darwin", "cpu": "arm64" }, "sha512-bzVrqeX2vb5iWrc/ftOUOqeUY8XO+qSgoTwj5TXHuwagavgwD3Hpeyjx8+icnTTeM4pao0som1WR9xfye6/X5Q=="], + + "@opentui/core-darwin-x64": ["@opentui/core-darwin-x64@0.1.99", "", { "os": "darwin", "cpu": "x64" }, "sha512-VE4FrXBYpkxnvkqcCV1a8aN9jyyMJMihVW+V2NLCtp+4yQsj0AapG5TiUSN76XnmSZRptxDy5rBmEempeoIZbg=="], + + "@opentui/core-linux-arm64": ["@opentui/core-linux-arm64@0.1.99", "", { "os": "linux", "cpu": "arm64" }, "sha512-viXQsbpS7yHjYkl7+am32JdvG96QU9lvHh1UiZtpOxcNUUqiYmA2ZwZFPD2Bi54jNyj5l2hjH6YkD3DzE2FEWA=="], + + "@opentui/core-linux-x64": ["@opentui/core-linux-x64@0.1.99", "", { "os": "linux", "cpu": "x64" }, "sha512-WLoEFINOSp0tZSR9y4LUuGc7n4Y7H1wcpjUPzQ9vChkYDXrfZltEanzoDWbDcQ4kZQW5tHVC7LrZHpAsRLwFZg=="], + + "@opentui/core-win32-arm64": ["@opentui/core-win32-arm64@0.1.99", "", { "os": "win32", "cpu": "arm64" }, "sha512-yWMOLWCEO8HdrctU1dMkgZC8qGkiO4Dwr4/e11tTvVpRmYhDsP/IR89ZjEEtOwnKwFOFuB/MxvflqaEWVQ2g5Q=="], + + "@opentui/core-win32-x64": ["@opentui/core-win32-x64@0.1.99", "", { "os": "win32", "cpu": "x64" }, "sha512-aYRlsL2w8YRL6vPd7/hrqlNVkXU3QowWb01TOvAcHS8UAsXaGFUr47kSDyjxDi1wg1MzmVduCfsC7T3NoThV1w=="], + + "@opentui/solid": ["@opentui/solid@0.1.99", "", { "dependencies": { "@babel/core": "7.28.0", "@babel/preset-typescript": "7.27.1", "@opentui/core": "0.1.99", "babel-plugin-module-resolver": "5.0.2", "babel-preset-solid": "1.9.10", "entities": "7.0.1", "s-js": "^0.4.9" }, "peerDependencies": { "solid-js": "1.9.11" } }, "sha512-DrqqO4h2V88FmeIP2cErYkMU0ZK5MrUsZw3w6IzZpoXyyiL4/9qpWzUq+CXx+r16VP2iGxDJwGKUmtFAzUch2Q=="], + "@standard-schema/spec": ["@standard-schema/spec@1.1.0", "", {}, "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w=="], + "@tokenizer/token": ["@tokenizer/token@0.3.0", "", {}, "sha512-OvjF+z51L3ov0OyAU0duzsYuvO01PH7x4t6DJx+guahgTnBHkhJdG7soQeTSFLWN3efnHyibZ4Z8l2EuWwJN3A=="], + "@types/node": ["@types/node@22.19.17", "", { "dependencies": { "undici-types": "~6.21.0" } }, "sha512-wGdMcf+vPYM6jikpS/qhg6WiqSV/OhG+jeeHT/KlVqxYfD40iYJf9/AE1uQxVWFvU7MipKRkRv8NSHiCGgPr8Q=="], + "@webgpu/types": ["@webgpu/types@0.1.69", "", {}, "sha512-RPmm6kgRbI8e98zSD3RVACvnuktIja5+yLgDAkTmxLr90BEwdTXRQWNLF3ETTTyH/8mKhznZuN5AveXYFEsMGQ=="], + + "abort-controller": ["abort-controller@3.0.0", "", { "dependencies": { "event-target-shim": "^5.0.0" } }, "sha512-h8lQ8tacZYnR3vNQTgibj+tODHI5/+l06Au2Pcriv/Gmet0eaj4TwWH41sO9wnHDiQsEj19q0drzdWdeAHtweg=="], + + "any-base": ["any-base@1.1.0", "", {}, "sha512-uMgjozySS8adZZYePpaWs8cxB9/kdzmpX6SgJZ+wbz1K5eYk5QMYDVJaZKhxyIHUdnnJkfR7SVgStgH7LkGUyg=="], + + "await-to-js": ["await-to-js@3.0.0", "", {}, "sha512-zJAaP9zxTcvTHRlejau3ZOY4V7SRpiByf3/dxx2uyKxxor19tpmpV2QRsTKikckwhaPmr2dVpxxMr7jOCYVp5g=="], + + "babel-plugin-jsx-dom-expressions": ["babel-plugin-jsx-dom-expressions@0.40.6", "", { "dependencies": { "@babel/helper-module-imports": "7.18.6", "@babel/plugin-syntax-jsx": "^7.18.6", "@babel/types": "^7.20.7", "html-entities": "2.3.3", "parse5": "^7.1.2" }, "peerDependencies": { "@babel/core": "^7.20.12" } }, "sha512-v3P1MW46Lm7VMpAkq0QfyzLWWkC8fh+0aE5Km4msIgDx5kjenHU0pF2s+4/NH8CQn/kla6+Hvws+2AF7bfV5qQ=="], + + "babel-plugin-module-resolver": ["babel-plugin-module-resolver@5.0.2", "", { "dependencies": { "find-babel-config": "^2.1.1", "glob": "^9.3.3", "pkg-up": "^3.1.0", "reselect": "^4.1.7", "resolve": "^1.22.8" } }, "sha512-9KtaCazHee2xc0ibfqsDeamwDps6FZNo5S0Q81dUqEuFzVwPhcT4J5jOqIVvgCA3Q/wO9hKYxN/Ds3tIsp5ygg=="], + + "babel-preset-solid": ["babel-preset-solid@1.9.10", "", { "dependencies": { "babel-plugin-jsx-dom-expressions": "^0.40.3" }, "peerDependencies": { "@babel/core": "^7.0.0", "solid-js": "^1.9.10" }, "optionalPeers": ["solid-js"] }, "sha512-HCelrgua/Y+kqO8RyL04JBWS/cVdrtUv/h45GntgQY+cJl4eBcKkCDV3TdMjtKx1nXwRaR9QXslM/Npm1dxdZQ=="], + + "balanced-match": ["balanced-match@1.0.2", "", {}, "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw=="], + + "base64-js": ["base64-js@1.5.1", "", {}, "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA=="], + + "baseline-browser-mapping": ["baseline-browser-mapping@2.10.24", "", { "bin": { "baseline-browser-mapping": "dist/cli.cjs" } }, "sha512-I2NkZOOrj2XuguvWCK6OVh9GavsNjZjK908Rq3mIBK25+GD8vPX5w2WdxVqnQ7xx3SrZJiCiZFu+/Oz50oSYSA=="], + + "bmp-ts": ["bmp-ts@1.0.9", "", {}, "sha512-cTEHk2jLrPyi+12M3dhpEbnnPOsaZuq7C45ylbbQIiWgDFZq4UVYPEY5mlqjvsj/6gJv9qX5sa+ebDzLXT28Vw=="], + + "brace-expansion": ["brace-expansion@2.1.0", "", { "dependencies": { "balanced-match": "^1.0.0" } }, "sha512-TN1kCZAgdgweJhWWpgKYrQaMNHcDULHkWwQIspdtjV4Y5aurRdZpjAqn6yX3FPqTA9ngHCc4hJxMAMgGfve85w=="], + + "browserslist": ["browserslist@4.28.2", "", { "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" } }, "sha512-48xSriZYYg+8qXna9kwqjIVzuQxi+KYWp2+5nCYnYKPTr0LvD89Jqk2Or5ogxz0NUMfIjhh2lIUX/LyX9B4oIg=="], + + "buffer": ["buffer@6.0.3", "", { "dependencies": { "base64-js": "^1.3.1", "ieee754": "^1.2.1" } }, "sha512-FTiCpNxtwiZZHEZbcbTIcZjERVICn9yq/pDFkTl95/AxzD1naBctN7YO68riM/gLSDY7sdrMby8hofADYuuqOA=="], + + "bun-ffi-structs": ["bun-ffi-structs@0.1.2", "", { "peerDependencies": { "typescript": "^5" } }, "sha512-Lh1oQAYHDcnesJauieA4UNkWGXY9hYck7OA5IaRwE3Bp6K2F2pJSNYqq+hIy7P3uOvo3km3oxS8304g5gDMl/w=="], + + "bun-webgpu": ["bun-webgpu@0.1.5", "", { "dependencies": { "@webgpu/types": "^0.1.60" }, "optionalDependencies": { "bun-webgpu-darwin-arm64": "^0.1.5", "bun-webgpu-darwin-x64": "^0.1.5", "bun-webgpu-linux-x64": "^0.1.5", "bun-webgpu-win32-x64": "^0.1.5" } }, "sha512-91/K6S5whZKX7CWAm9AylhyKrLGRz6BUiiPiM/kXadSnD4rffljCD/q9cNFftm5YXhx4MvLqw33yEilxogJvwA=="], + + "bun-webgpu-darwin-arm64": ["bun-webgpu-darwin-arm64@0.1.7", "", { "os": "darwin", "cpu": "arm64" }, "sha512-mRrFFyHzPWjsTRidAZBRcu808CPQBOUL0P6b4nxLhp+XHcV/mbUHERZMgW9s58tsojQfSdzschiQa8q+JCgRWA=="], + + "bun-webgpu-darwin-x64": ["bun-webgpu-darwin-x64@0.1.7", "", { "os": "darwin", "cpu": "x64" }, "sha512-g0NXGNgvaVCSH/jCWWlfdiquOHkbUN6vP4zqzSkIxWKQeLnqm3oADcok7SO3yIgI7v5mKpRc/ks7NDEKNH+jNQ=="], + + "bun-webgpu-linux-x64": ["bun-webgpu-linux-x64@0.1.7", "", { "os": "linux", "cpu": "x64" }, "sha512-UEP7UZdEhx9otvkZczjsszL8ZVlrODANQvgl+C88/bNVmxDoFi7w1fWzGi1sZyakiETjmtFDq2/xCLhbSZxjqw=="], + + "bun-webgpu-win32-x64": ["bun-webgpu-win32-x64@0.1.7", "", { "os": "win32", "cpu": "x64" }, "sha512-KZktiFkBz6sN7PEm1NVdeaLP5Q5X/PlSHZqefY4nNuWtf0LNvh54NhZe7yVv/Plz/nGbv92b0KHMBY3ki/pp6g=="], + + "caniuse-lite": ["caniuse-lite@1.0.30001791", "", {}, "sha512-yk0l/YSrOnFZk3UROpDLQD9+kC1l4meK/wed583AXrzoarMGJcbRi2Q4RaUYbKxYAsZ8sWmaSa/DsLmdBeI1vQ=="], + + "convert-source-map": ["convert-source-map@2.0.0", "", {}, "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg=="], + "cross-spawn": ["cross-spawn@7.0.6", "", { "dependencies": { "path-key": "^3.1.0", "shebang-command": "^2.0.0", "which": "^2.0.1" } }, "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA=="], + "csstype": ["csstype@3.2.3", "", {}, "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ=="], + + "debug": ["debug@4.4.3", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA=="], + "detect-libc": ["detect-libc@2.1.2", "", {}, "sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ=="], + "diff": ["diff@8.0.2", "", {}, "sha512-sSuxWU5j5SR9QQji/o2qMvqRNYRDOcBTgsJ/DeCf4iSN4gW+gNMXM7wFIP+fdXZxoNiAnHUTGjCr+TSWXdRDKg=="], + "effect": ["effect@4.0.0-beta.48", "", { "dependencies": { "@standard-schema/spec": "^1.1.0", "fast-check": "^4.6.0", "find-my-way-ts": "^0.1.6", "ini": "^6.0.0", "kubernetes-types": "^1.30.0", "msgpackr": "^1.11.9", "multipasta": "^0.2.7", "toml": "^4.1.1", "uuid": "^13.0.0", "yaml": "^2.8.3" } }, "sha512-MMAM/ZabuNdNmgXiin+BAanQXK7qM8mlt7nfXDoJ/Gn9V8i89JlCq+2N0AiWmqFLXjGLA0u3FjiOjSOYQk5uMw=="], + "electron-to-chromium": ["electron-to-chromium@1.5.345", "", {}, "sha512-F9JXQGiMrz6yVNPI2qOVPvB9HzjH5cGzhs8oJ6A28V5L/YnzN/0KsuiibqF+F1Fd9qxFzD1BUnYSd8JfULxTwg=="], + + "entities": ["entities@7.0.1", "", {}, "sha512-TWrgLOFUQTH994YUyl1yT4uyavY5nNB5muff+RtWaqNVCAK408b5ZnnbNAUEWLTCpum9w6arT70i1XdQ4UeOPA=="], + + "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], + + "escalade": ["escalade@3.2.0", "", {}, "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA=="], + + "event-target-shim": ["event-target-shim@5.0.1", "", {}, "sha512-i/2XbnSz/uxRCU6+NdVJgKWDTM427+MqYbkQzD321DuCQJUqOuJKIA0IM2+W2xtYHdKOmZ4dR6fExsd4SXL+WQ=="], + + "events": ["events@3.3.0", "", {}, "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q=="], + + "exif-parser": ["exif-parser@0.1.12", "", {}, "sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw=="], + "fast-check": ["fast-check@4.6.0", "", { "dependencies": { "pure-rand": "^8.0.0" } }, "sha512-h7H6Dm0Fy+H4ciQYFxFjXnXkzR2kr9Fb22c0UBpHnm59K2zpr2t13aPTHlltFiNT6zuxp6HMPAVVvgur4BLdpA=="], + "file-type": ["file-type@16.5.4", "", { "dependencies": { "readable-web-to-node-stream": "^3.0.0", "strtok3": "^6.2.4", "token-types": "^4.1.1" } }, "sha512-/yFHK0aGjFEgDJjEKP0pWCplsPFPhwyfwevf/pVxiN0tmE4L9LmwWxWukdJSHdoCli4VgQLehjJtwQBnqmsKcw=="], + + "find-babel-config": ["find-babel-config@2.1.2", "", { "dependencies": { "json5": "^2.2.3" } }, "sha512-ZfZp1rQyp4gyuxqt1ZqjFGVeVBvmpURMqdIWXbPRfB97Bf6BzdK/xSIbylEINzQ0kB5tlDQfn9HkNXXWsqTqLg=="], + "find-my-way-ts": ["find-my-way-ts@0.1.6", "", {}, "sha512-a85L9ZoXtNAey3Y6Z+eBWW658kO/MwR7zIafkIUPUMf3isZG0NCs2pjW2wtjxAKuJPxMAsHUIP4ZPGv0o5gyTA=="], + "find-up": ["find-up@3.0.0", "", { "dependencies": { "locate-path": "^3.0.0" } }, "sha512-1yD6RmLI1XBfxugvORwlck6f75tYL+iR0jqwsOrOxMZyGYqUuDhJ0l4AXdO1iX/FTs9cBAMEk1gWSEx1kSbylg=="], + + "fs.realpath": ["fs.realpath@1.0.0", "", {}, "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw=="], + + "function-bind": ["function-bind@1.1.2", "", {}, "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA=="], + + "gensync": ["gensync@1.0.0-beta.2", "", {}, "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg=="], + + "gifwrap": ["gifwrap@0.10.1", "", { "dependencies": { "image-q": "^4.0.0", "omggif": "^1.0.10" } }, "sha512-2760b1vpJHNmLzZ/ubTtNnEx5WApN/PYWJvXvgS+tL1egTTthayFYIQQNi136FLEDcN/IyEY2EcGpIITD6eYUw=="], + + "glob": ["glob@9.3.5", "", { "dependencies": { "fs.realpath": "^1.0.0", "minimatch": "^8.0.2", "minipass": "^4.2.4", "path-scurry": "^1.6.1" } }, "sha512-e1LleDykUz2Iu+MTYdkSsuWX8lvAjAcs0Xef0lNIu0S2wOAzuTxCJtcd9S3cijlwYF18EsU3rzb8jPVobxDh9Q=="], + + "hasown": ["hasown@2.0.3", "", { "dependencies": { "function-bind": "^1.1.2" } }, "sha512-ej4AhfhfL2Q2zpMmLo7U1Uv9+PyhIZpgQLGT1F9miIGmiCJIoCgSmczFdrc97mWT4kVY72KA+WnnhJ5pghSvSg=="], + + "html-entities": ["html-entities@2.3.3", "", {}, "sha512-DV5Ln36z34NNTDgnz0EWGBLZENelNAtkiFA4kyNOG2tDI6Mz1uSWiq1wAKdyjnJwyDiDO7Fa2SO1CTxPXL8VxA=="], + + "ieee754": ["ieee754@1.2.1", "", {}, "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA=="], + + "image-q": ["image-q@4.0.0", "", { "dependencies": { "@types/node": "16.9.1" } }, "sha512-PfJGVgIfKQJuq3s0tTDOKtztksibuUEbJQIYT3by6wctQo+Rdlh7ef4evJ5NCdxY4CfMbvFkocEwbl4BF8RlJw=="], + "ini": ["ini@6.0.0", "", {}, "sha512-IBTdIkzZNOpqm7q3dRqJvMaldXjDHWkEDfrwGEQTs5eaQMWV+djAhR+wahyNNMAa+qpbDUhBMVt4ZKNwpPm7xQ=="], + "is-core-module": ["is-core-module@2.16.1", "", { "dependencies": { "hasown": "^2.0.2" } }, "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w=="], + "isexe": ["isexe@2.0.0", "", {}, "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw=="], + "jimp": ["jimp@1.6.0", "", { "dependencies": { "@jimp/core": "1.6.0", "@jimp/diff": "1.6.0", "@jimp/js-bmp": "1.6.0", "@jimp/js-gif": "1.6.0", "@jimp/js-jpeg": "1.6.0", "@jimp/js-png": "1.6.0", "@jimp/js-tiff": "1.6.0", "@jimp/plugin-blit": "1.6.0", "@jimp/plugin-blur": "1.6.0", "@jimp/plugin-circle": "1.6.0", "@jimp/plugin-color": "1.6.0", "@jimp/plugin-contain": "1.6.0", "@jimp/plugin-cover": "1.6.0", "@jimp/plugin-crop": "1.6.0", "@jimp/plugin-displace": "1.6.0", "@jimp/plugin-dither": "1.6.0", "@jimp/plugin-fisheye": "1.6.0", "@jimp/plugin-flip": "1.6.0", "@jimp/plugin-hash": "1.6.0", "@jimp/plugin-mask": "1.6.0", "@jimp/plugin-print": "1.6.0", "@jimp/plugin-quantize": "1.6.0", "@jimp/plugin-resize": "1.6.0", "@jimp/plugin-rotate": "1.6.0", "@jimp/plugin-threshold": "1.6.0", "@jimp/types": "1.6.0", "@jimp/utils": "1.6.0" } }, "sha512-YcwCHw1kiqEeI5xRpDlPPBGL2EOpBKLwO4yIBJcXWHPj5PnA5urGq0jbyhM5KoNpypQ6VboSoxc9D8HyfvngSg=="], + + "jpeg-js": ["jpeg-js@0.4.4", "", {}, "sha512-WZzeDOEtTOBK4Mdsar0IqEU5sMr3vSV2RqkAIzUEV2BHnUfKGyswWFPFwK5EeDo93K3FohSHbLAjj0s1Wzd+dg=="], + + "js-tokens": ["js-tokens@4.0.0", "", {}, "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ=="], + + "jsesc": ["jsesc@3.1.0", "", { "bin": { "jsesc": "bin/jsesc" } }, "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA=="], + + "json5": ["json5@2.2.3", "", { "bin": { "json5": "lib/cli.js" } }, "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg=="], + "kubernetes-types": ["kubernetes-types@1.30.0", "", {}, "sha512-Dew1okvhM/SQcIa2rcgujNndZwU8VnSapDgdxlYoB84ZlpAD43U6KLAFqYo17ykSFGHNPrg0qry0bP+GJd9v7Q=="], + "locate-path": ["locate-path@3.0.0", "", { "dependencies": { "p-locate": "^3.0.0", "path-exists": "^3.0.0" } }, "sha512-7AO748wWnIhNqAuaty2ZWHkQHRSNfPVIsPIfwEOWO22AmaoVrWavlOcMR5nzTLNYvp36X220/maaRsrec1G65A=="], + + "lru-cache": ["lru-cache@5.1.1", "", { "dependencies": { "yallist": "^3.0.2" } }, "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w=="], + + "marked": ["marked@17.0.1", "", { "bin": { "marked": "bin/marked.js" } }, "sha512-boeBdiS0ghpWcSwoNm/jJBwdpFaMnZWRzjA6SkUMYb40SVaN1x7mmfGKp0jvexGcx+7y2La5zRZsYFZI6Qpypg=="], + + "mime": ["mime@3.0.0", "", { "bin": { "mime": "cli.js" } }, "sha512-jSCU7/VB1loIWBZe14aEYHU/+1UMEHoaO7qxCOVJOw9GgH72VAWppxNcjU+x9a2k3GSIBXNKxXQFqRvvZ7vr3A=="], + + "minimatch": ["minimatch@8.0.7", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-V+1uQNdzybxa14e/p00HZnQNNcTjnRJjDxg2V8wtkjFctq4M7hXFws4oekyTP0Jebeq7QYtpFyOeBAjc88zvYg=="], + + "minipass": ["minipass@4.2.8", "", {}, "sha512-fNzuVyifolSLFL4NzpF+wEF4qrgqaaKX0haXPQEdQ7NKAN+WecoKMHV09YcuL/DHxrUsYQOK3MiuDf7Ip2OXfQ=="], + + "ms": ["ms@2.1.3", "", {}, "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="], + "msgpackr": ["msgpackr@1.11.9", "", { "optionalDependencies": { "msgpackr-extract": "^3.0.2" } }, "sha512-FkoAAyyA6HM8wL882EcEyFZ9s7hVADSwG9xrVx3dxxNQAtgADTrJoEWivID82Iv1zWDsv/OtbrrcZAzGzOMdNw=="], "msgpackr-extract": ["msgpackr-extract@3.0.3", "", { "dependencies": { "node-gyp-build-optional-packages": "5.2.2" }, "optionalDependencies": { "@msgpackr-extract/msgpackr-extract-darwin-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-darwin-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-arm64": "3.0.3", "@msgpackr-extract/msgpackr-extract-linux-x64": "3.0.3", "@msgpackr-extract/msgpackr-extract-win32-x64": "3.0.3" }, "bin": { "download-msgpackr-prebuilds": "bin/download-prebuilds.js" } }, "sha512-P0efT1C9jIdVRefqjzOQ9Xml57zpOXnIuS+csaB4MdZbTdmGDLo8XhzBG1N7aO11gKDDkJvBLULeFTo46wwreA=="], @@ -59,26 +317,164 @@ "node-gyp-build-optional-packages": ["node-gyp-build-optional-packages@5.2.2", "", { "dependencies": { "detect-libc": "^2.0.1" }, "bin": { "node-gyp-build-optional-packages": "bin.js", "node-gyp-build-optional-packages-optional": "optional.js", "node-gyp-build-optional-packages-test": "build-test.js" } }, "sha512-s+w+rBWnpTMwSFbaE0UXsRlg7hU4FjekKU4eyAih5T8nJuNZT1nNsskXpxmeqSK9UzkBl6UgRlnKc8hz8IEqOw=="], + "node-releases": ["node-releases@2.0.38", "", {}, "sha512-3qT/88Y3FbH/Kx4szpQQ4HzUbVrHPKTLVpVocKiLfoYvw9XSGOX2FmD2d6DrXbVYyAQTF2HeF6My8jmzx7/CRw=="], + + "omggif": ["omggif@1.0.10", "", {}, "sha512-LMJTtvgc/nugXj0Vcrrs68Mn2D1r0zf630VNtqtpI1FEO7e+O9FP4gqs9AcnBaSEeoHIPm28u6qgPR0oyEpGSw=="], + + "p-limit": ["p-limit@2.3.0", "", { "dependencies": { "p-try": "^2.0.0" } }, "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w=="], + + "p-locate": ["p-locate@3.0.0", "", { "dependencies": { "p-limit": "^2.0.0" } }, "sha512-x+12w/To+4GFfgJhBEpiDcLozRJGegY+Ei7/z0tSLkMmxGZNybVMSfWj9aJn8Z5Fc7dBUNJOOVgPv2H7IwulSQ=="], + + "p-try": ["p-try@2.2.0", "", {}, "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ=="], + + "pako": ["pako@1.0.11", "", {}, "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw=="], + + "parse-bmfont-ascii": ["parse-bmfont-ascii@1.0.6", "", {}, "sha512-U4RrVsUFCleIOBsIGYOMKjn9PavsGOXxbvYGtMOEfnId0SVNsgehXh1DxUdVPLoxd5mvcEtvmKs2Mmf0Mpa1ZA=="], + + "parse-bmfont-binary": ["parse-bmfont-binary@1.0.6", "", {}, "sha512-GxmsRea0wdGdYthjuUeWTMWPqm2+FAd4GI8vCvhgJsFnoGhTrLhXDDupwTo7rXVAgaLIGoVHDZS9p/5XbSqeWA=="], + + "parse-bmfont-xml": ["parse-bmfont-xml@1.1.6", "", { "dependencies": { "xml-parse-from-string": "^1.0.0", "xml2js": "^0.5.0" } }, "sha512-0cEliVMZEhrFDwMh4SxIyVJpqYoOWDJ9P895tFuS+XuNzI5UBmBk5U5O4KuJdTnZpSBI4LFA2+ZiJaiwfSwlMA=="], + + "parse5": ["parse5@7.3.0", "", { "dependencies": { "entities": "^6.0.0" } }, "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw=="], + + "path-exists": ["path-exists@3.0.0", "", {}, "sha512-bpC7GYwiDYQ4wYLe+FA8lhRjhQCMcQGuSgGGqDkg/QerRWw9CmGRT0iSOVRSZJ29NMLZgIzqaljJ63oaL4NIJQ=="], + "path-key": ["path-key@3.1.1", "", {}, "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q=="], + "path-parse": ["path-parse@1.0.7", "", {}, "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw=="], + + "path-scurry": ["path-scurry@1.11.1", "", { "dependencies": { "lru-cache": "^10.2.0", "minipass": "^5.0.0 || ^6.0.2 || ^7.0.0" } }, "sha512-Xa4Nw17FS9ApQFJ9umLiJS4orGjm7ZzwUrwamcGQuHSzDyth9boKDaycYdDcZDuqYATXw4HFXgaqWTctW/v1HA=="], + + "peek-readable": ["peek-readable@4.1.0", "", {}, "sha512-ZI3LnwUv5nOGbQzD9c2iDG6toheuXSZP5esSHBjopsXH4dg19soufvpUGA3uohi5anFtGb2lhAVdHzH6R/Evvg=="], + + "picocolors": ["picocolors@1.1.1", "", {}, "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA=="], + + "pixelmatch": ["pixelmatch@5.3.0", "", { "dependencies": { "pngjs": "^6.0.0" }, "bin": { "pixelmatch": "bin/pixelmatch" } }, "sha512-o8mkY4E/+LNUf6LzX96ht6k6CEDi65k9G2rjMtBe9Oo+VPKSvl+0GKHuH/AlG+GA5LPG/i5hrekkxUc3s2HU+Q=="], + + "pkg-up": ["pkg-up@3.1.0", "", { "dependencies": { "find-up": "^3.0.0" } }, "sha512-nDywThFk1i4BQK4twPQ6TA4RT8bDY96yeuCVBWL3ePARCiEKDRSrNGbFIgUJpLp+XeIR65v8ra7WuJOFUBtkMA=="], + + "planck": ["planck@1.5.0", "", { "peerDependencies": { "stage-js": "^1.0.0-alpha.12" } }, "sha512-dlvqJE+FscZgrGUXJ5ybd0o5bvZ5XXyZNbm08xGsXp9WjXeAyWSFT6n9s/1PQcUBo4546fDXA5RMA4wbDyZw6g=="], + + "pngjs": ["pngjs@7.0.0", "", {}, "sha512-LKWqWJRhstyYo9pGvgor/ivk2w94eSjE3RGVuzLGlr3NmD8bf7RcYGze1mNdEHRP6TRP6rMuDHk5t44hnTRyow=="], + + "process": ["process@0.11.10", "", {}, "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A=="], + "pure-rand": ["pure-rand@8.4.0", "", {}, "sha512-IoM8YF/jY0hiugFo/wOWqfmarlE6J0wc6fDK1PhftMk7MGhVZl88sZimmqBBFomLOCSmcCCpsfj7wXASCpvK9A=="], + "readable-stream": ["readable-stream@4.7.0", "", { "dependencies": { "abort-controller": "^3.0.0", "buffer": "^6.0.3", "events": "^3.3.0", "process": "^0.11.10", "string_decoder": "^1.3.0" } }, "sha512-oIGGmcpTLwPga8Bn6/Z75SVaH1z5dUut2ibSyAMVhmUggWpmDn2dapB0n7f8nwaSiRtepAsfJyfXIO5DCVAODg=="], + + "readable-web-to-node-stream": ["readable-web-to-node-stream@3.0.4", "", { "dependencies": { "readable-stream": "^4.7.0" } }, "sha512-9nX56alTf5bwXQ3ZDipHJhusu9NTQJ/CVPtb/XHAJCXihZeitfJvIRS4GqQ/mfIoOE3IelHMrpayVrosdHBuLw=="], + + "reselect": ["reselect@4.1.8", "", {}, "sha512-ab9EmR80F/zQTMNeneUr4cv+jSwPJgIlvEmVwLerwrWVbpLlBuls9XHzIeTFy4cegU2NHBp3va0LKOzU5qFEYQ=="], + + "resolve": ["resolve@1.22.12", "", { "dependencies": { "es-errors": "^1.3.0", "is-core-module": "^2.16.1", "path-parse": "^1.0.7", "supports-preserve-symlinks-flag": "^1.0.0" }, "bin": { "resolve": "bin/resolve" } }, "sha512-TyeJ1zif53BPfHootBGwPRYT1RUt6oGWsaQr8UyZW/eAm9bKoijtvruSDEmZHm92CwS9nj7/fWttqPCgzep8CA=="], + + "s-js": ["s-js@0.4.9", "", {}, "sha512-RtpOm+cM6O0sHg6IA70wH+UC3FZcND+rccBZpBAHzlUgNO2Bm5BN+FnM8+OBxzXdwpKWFwX11JGF0MFRkhSoIQ=="], + + "safe-buffer": ["safe-buffer@5.2.1", "", {}, "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ=="], + + "sax": ["sax@1.6.0", "", {}, "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA=="], + + "semver": ["semver@6.3.1", "", { "bin": { "semver": "bin/semver.js" } }, "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA=="], + + "seroval": ["seroval@1.5.2", "", {}, "sha512-xcRN39BdsnO9Tf+VzsE7b3JyTJASItIV1FVFewJKCFcW4s4haIKS3e6vj8PGB9qBwC7tnuOywQMdv5N4qkzi7Q=="], + + "seroval-plugins": ["seroval-plugins@1.5.2", "", { "peerDependencies": { "seroval": "^1.0" } }, "sha512-qpY0Cl+fKYFn4GOf3cMiq6l72CpuVaawb6ILjubOQ+diJ54LfOWaSSPsaswN8DRPIPW4Yq+tE1k5aKd7ILyaFg=="], + "shebang-command": ["shebang-command@2.0.0", "", { "dependencies": { "shebang-regex": "^3.0.0" } }, "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA=="], "shebang-regex": ["shebang-regex@3.0.0", "", {}, "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A=="], + "simple-xml-to-json": ["simple-xml-to-json@1.2.7", "", {}, "sha512-mz9VXphOxQWX3eQ/uXCtm6upltoN0DLx8Zb5T4TFC4FHB7S9FDPGre8CfLWqPWQQH/GrQYd2AXhhVM5LDpYx6Q=="], + + "solid-js": ["solid-js@1.9.11", "", { "dependencies": { "csstype": "^3.1.0", "seroval": "~1.5.0", "seroval-plugins": "~1.5.0" } }, "sha512-WEJtcc5mkh/BnHA6Yrg4whlF8g6QwpmXXRg4P2ztPmcKeHHlH4+djYecBLhSpecZY2RRECXYUwIc/C2r3yzQ4Q=="], + + "stage-js": ["stage-js@1.0.2", "", {}, "sha512-EWTRBYlg7Qv9wGUao99/PfRe3KaiQqWmgSvTOXvaWnu1Jk/q/vV8yJVu6bi/3EqDZeMVnCPAjheba6OFc5k1GQ=="], + + "string_decoder": ["string_decoder@1.3.0", "", { "dependencies": { "safe-buffer": "~5.2.0" } }, "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA=="], + + "strtok3": ["strtok3@6.3.0", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "peek-readable": "^4.1.0" } }, "sha512-fZtbhtvI9I48xDSywd/somNqgUHl2L2cstmXCCif0itOf96jeW18MBSyrLuNicYQVkvpOxkZtkzujiTJ9LW5Jw=="], + + "supports-preserve-symlinks-flag": ["supports-preserve-symlinks-flag@1.0.0", "", {}, "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w=="], + + "three": ["three@0.177.0", "", {}, "sha512-EiXv5/qWAaGI+Vz2A+JfavwYCMdGjxVsrn3oBwllUoqYeaBO75J63ZfyaQKoiLrqNHoTlUc6PFgMXnS0kI45zg=="], + + "tinycolor2": ["tinycolor2@1.6.0", "", {}, "sha512-XPaBkWQJdsf3pLKJV9p4qN/S+fm2Oj8AIPo1BTUhg5oxkvm9+SVEGFdhyOz7tTdUTfvxMiAs4sp6/eZO2Ew+pw=="], + + "token-types": ["token-types@4.2.1", "", { "dependencies": { "@tokenizer/token": "^0.3.0", "ieee754": "^1.2.1" } }, "sha512-6udB24Q737UD/SDsKAHI9FCRP7Bqc9D/MQUV02ORQg5iskjtLJlZJNdN4kKtcdtwCeWIwIHDGaUsTsCCAa8sFQ=="], + "toml": ["toml@4.1.1", "", {}, "sha512-EBJnVBr3dTXdA89WVFoAIPUqkBjxPMwRqsfuo1r240tKFHXv3zgca4+NJib/h6TyvGF7vOawz0jGuryJCdNHrw=="], "typescript": ["typescript@5.9.3", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw=="], "undici-types": ["undici-types@6.21.0", "", {}, "sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ=="], + "update-browserslist-db": ["update-browserslist-db@1.2.3", "", { "dependencies": { "escalade": "^3.2.0", "picocolors": "^1.1.1" }, "peerDependencies": { "browserslist": ">= 4.21.0" }, "bin": { "update-browserslist-db": "cli.js" } }, "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w=="], + + "utif2": ["utif2@4.1.0", "", { "dependencies": { "pako": "^1.0.11" } }, "sha512-+oknB9FHrJ7oW7A2WZYajOcv4FcDR4CfoGB0dPNfxbi4GO05RRnFmt5oa23+9w32EanrYcSJWspUiJkLMs+37w=="], + "uuid": ["uuid@13.0.0", "", { "bin": { "uuid": "dist-node/bin/uuid" } }, "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w=="], + "web-tree-sitter": ["web-tree-sitter@0.25.10", "", { "peerDependencies": { "@types/emscripten": "^1.40.0" }, "optionalPeers": ["@types/emscripten"] }, "sha512-Y09sF44/13XvgVKgO2cNDw5rGk6s26MgoZPXLESvMXeefBf7i6/73eFurre0IsTW6E14Y0ArIzhUMmjoc7xyzA=="], + "which": ["which@2.0.2", "", { "dependencies": { "isexe": "^2.0.0" }, "bin": { "node-which": "./bin/node-which" } }, "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA=="], + "xml-parse-from-string": ["xml-parse-from-string@1.0.1", "", {}, "sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g=="], + + "xml2js": ["xml2js@0.5.0", "", { "dependencies": { "sax": ">=0.6.0", "xmlbuilder": "~11.0.0" } }, "sha512-drPFnkQJik/O+uPKpqSgr22mpuFHqKdbS835iAQrUC73L2F5WkboIRd63ai/2Yg6I1jzifPFKH2NTK+cfglkIA=="], + + "xmlbuilder": ["xmlbuilder@11.0.1", "", {}, "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA=="], + + "yallist": ["yallist@3.1.1", "", {}, "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g=="], + "yaml": ["yaml@2.8.3", "", { "bin": { "yaml": "bin.mjs" } }, "sha512-AvbaCLOO2Otw/lW5bmh9d/WEdcDFdQp2Z2ZUH3pX9U2ihyUY0nvLv7J6TrWowklRGPYbB/IuIMfYgxaCPg5Bpg=="], + "yoga-layout": ["yoga-layout@3.2.1", "", {}, "sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ=="], + "zod": ["zod@4.1.8", "", {}, "sha512-5R1P+WwQqmmMIEACyzSvo4JXHY5WiAFHRMg+zBZKgKS+Q1viRa0C1hmUKtHltoIFKtIdki3pRxkmpP74jnNYHQ=="], + + "@jimp/plugin-blit/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-circle/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-color/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-contain/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-cover/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-crop/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-displace/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-fisheye/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-flip/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-mask/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-print/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-quantize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-resize/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-rotate/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/plugin-threshold/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "@jimp/types/zod": ["zod@3.25.76", "", {}, "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ=="], + + "babel-plugin-jsx-dom-expressions/@babel/helper-module-imports": ["@babel/helper-module-imports@7.18.6", "", { "dependencies": { "@babel/types": "^7.18.6" } }, "sha512-0NFvs3VkuSYbFi1x2Vd6tKrywq+z/cLeYC/RJNFrIX/30Bf5aiGYbtvGXolEktzJH8o5E5KJ3tT+nkxuuZFVlA=="], + + "image-q/@types/node": ["@types/node@16.9.1", "", {}, "sha512-QpLcX9ZSsq3YYUUnD3nFDY8H7wctAhQj/TFKL8Ya8v5fMm3CFXxo8zStsLAl780ltoYoo1WvKUVGBQK+1ifr7g=="], + + "parse5/entities": ["entities@6.0.1", "", {}, "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g=="], + + "path-scurry/lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], + + "path-scurry/minipass": ["minipass@7.1.3", "", {}, "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A=="], + + "pixelmatch/pngjs": ["pngjs@6.0.0", "", {}, "sha512-TRzzuFRRmEoSW/p1KVAmiOgPco2Irlah+bGFCeNfJXxxYGwSw7YwAOAcd7X28K/m5bjBWKsC29KyoMfHbypayg=="], } } diff --git a/package.json b/package.json index fd5e5ea..4dc8bdc 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,8 @@ "main": "./src/index.ts", "exports": { ".": "./src/index.ts", - "./server": "./src/index.ts" + "./server": "./src/index.ts", + "./tui": "./src/tui.tsx" }, "files": [ "src", @@ -46,7 +47,9 @@ }, "devDependencies": { "@opencode-ai/plugin": "^1.4.7", - "typescript": "^5.6.0", - "@types/node": "^22.0.0" + "@opentui/core": "0.1.99", + "@opentui/solid": "0.1.99", + "@types/node": "^22.0.0", + "typescript": "^5.6.0" } } diff --git a/src/auth-refresh.ts b/src/auth-refresh.ts new file mode 100644 index 0000000..669be33 --- /dev/null +++ b/src/auth-refresh.ts @@ -0,0 +1,119 @@ +import fs from "node:fs/promises" +import path from "node:path" +import { PROVIDER_ID, REFRESH_SAFETY_WINDOW_MS } from "./constants.ts" +import { + readAuth, + readAuthStoreEntry, + resolveAuthStorePath, + writeAuthStoreEntry, + type OAuthAuth, +} from "./auth-store.ts" +import { refreshToken } from "./oauth.ts" + +const REFRESH_LOCK_WAIT_MS = 15_000 +const REFRESH_LOCK_POLL_MS = 100 +const REFRESH_LOCK_STALE_MS = 120_000 + +type RefreshOptions = { + force?: boolean + readLatest?: () => Promise + persist: (auth: OAuthAuth) => Promise +} + +function sleep(ms: number) { + return new Promise((resolve) => setTimeout(resolve, ms)) +} + +function sameAuth(left: OAuthAuth, right: OAuthAuth) { + return left.access === right.access && left.refresh === right.refresh && left.expires === right.expires +} + +function withInvalidGrantHint(error: unknown) { + if (!(error instanceof Error) || !/invalid_grant/.test(error.message)) return error + const next = new Error( + `${error.message}. The token may have been rotated or revoked in another opencode session — run \`opencode auth login ${PROVIDER_ID}\` again if it does not self-heal.`, + ) as Error & { code?: string; status?: number } + next.code = (error as Error & { code?: string }).code + next.status = (error as Error & { status?: number }).status + return next +} + +export function isAuthExpiring(auth: OAuthAuth) { + return auth.expires - Date.now() < REFRESH_SAFETY_WINDOW_MS +} + +async function withRefreshLock(work: () => Promise) { + const authFile = await resolveAuthStorePath() + const lockDir = `${authFile}.${PROVIDER_ID}.refresh.lock` + await fs.mkdir(path.dirname(lockDir), { recursive: true }) + const deadline = Date.now() + REFRESH_LOCK_WAIT_MS + + while (true) { + try { + await fs.mkdir(lockDir) + break + } catch (error) { + const code = (error as NodeJS.ErrnoException).code + if (code !== "EEXIST") throw error + try { + const stat = await fs.stat(lockDir) + if (Date.now() - stat.mtimeMs > REFRESH_LOCK_STALE_MS) { + await fs.rm(lockDir, { recursive: true, force: true }) + continue + } + } catch {} + if (Date.now() >= deadline) { + throw new Error("kimi oauth: timed out waiting for the auth refresh lock") + } + await sleep(REFRESH_LOCK_POLL_MS) + } + } + + try { + return await work() + } finally { + await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined) + } +} + +export async function refreshAuthWithLock(auth: OAuthAuth, options: RefreshOptions) { + const force = options.force ?? false + return withRefreshLock(async () => { + const latest = await options.readLatest?.() + const current = latest ?? auth + if (latest && !sameAuth(latest, auth) && !force && !isAuthExpiring(latest)) return latest + if (!force && !isAuthExpiring(current)) return current + + try { + const tokens = await refreshToken(current.refresh) + const next: OAuthAuth = { + type: "oauth", + refresh: tokens.refresh_token, + access: tokens.access_token, + expires: Date.now() + tokens.expires_in * 1000, + } + await options.persist(next) + return next + } catch (error) { + const newest = await options.readLatest?.() + if (newest && !sameAuth(newest, current)) return newest + throw withInvalidGrantHint(error) + } + }) +} + +export async function ensureFreshStoredAuth() { + const store = await readAuthStoreEntry() + if (!store) { + throw new Error(`Kimi is not authenticated. Run \`opencode auth login ${PROVIDER_ID}\` first.`) + } + if (!isAuthExpiring(store.entry)) return store.entry + + return refreshAuthWithLock(store.entry, { + readLatest: readAuth, + persist: async (auth) => { + const latestStore = await readAuthStoreEntry() + await writeAuthStoreEntry(latestStore?.file ?? store.file, latestStore?.parsed ?? store.parsed, auth) + }, + }) +} diff --git a/src/auth-store.ts b/src/auth-store.ts new file mode 100644 index 0000000..a5bddbb --- /dev/null +++ b/src/auth-store.ts @@ -0,0 +1,88 @@ +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { PROVIDER_ID } from "./constants.ts" + +export type OAuthAuth = { + type: "oauth" + refresh: string + access: string + expires: number +} + +export type AuthStoreEntry = { + file: string + parsed: Record + entry: OAuthAuth +} + +export function isOAuthAuth(value: unknown): value is OAuthAuth { + if (!value || typeof value !== "object" || Array.isArray(value)) return false + const auth = value as Partial + return ( + auth.type === "oauth" && + typeof auth.access === "string" && + typeof auth.refresh === "string" && + typeof auth.expires === "number" + ) +} + +export function authStoreCandidates() { + const home = os.homedir() + if (process.env.XDG_DATA_HOME) { + return [path.join(process.env.XDG_DATA_HOME, "opencode", "auth.json")] + } + const candidates = new Set() + candidates.add(path.join(home, ".local", "share", "opencode", "auth.json")) + if (process.platform === "darwin") { + candidates.add(path.join(home, "Library", "Application Support", "opencode", "auth.json")) + } + if (process.platform === "win32") { + const local = process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local") + candidates.add(path.join(local, "opencode", "auth.json")) + if (process.env.APPDATA) { + candidates.add(path.join(process.env.APPDATA, "opencode", "auth.json")) + } + } + return [...candidates] +} + +export async function resolveAuthStorePath() { + const candidates = authStoreCandidates() + for (const file of candidates) { + try { + await fs.access(file) + return file + } catch {} + } + return candidates[0]! +} + +export async function readAuthStore() { + for (const file of authStoreCandidates()) { + try { + return { file, parsed: JSON.parse(await fs.readFile(file, "utf8")) as Record } + } catch {} + } + return { file: authStoreCandidates()[0]!, parsed: {} as Record } +} + +export async function readAuthStoreEntry(): Promise { + for (const file of authStoreCandidates()) { + try { + const parsed = JSON.parse(await fs.readFile(file, "utf8")) as Record + const entry = parsed[PROVIDER_ID] ?? parsed[`${PROVIDER_ID}/`] + if (isOAuthAuth(entry)) return { file, parsed, entry } + } catch {} + } + return +} + +export async function readAuth() { + return (await readAuthStoreEntry())?.entry +} + +export async function writeAuthStoreEntry(file: string, parsed: Record, auth: OAuthAuth) { + await fs.mkdir(path.dirname(file), { recursive: true }) + await fs.writeFile(file, `${JSON.stringify({ ...parsed, [PROVIDER_ID]: auth }, null, 2)}\n`) +} diff --git a/src/index.ts b/src/index.ts index ca9bd00..71c5e0c 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,10 +1,9 @@ -import fs from "node:fs/promises" -import os from "node:os" -import path from "node:path" import type { Plugin, PluginModule } from "@opencode-ai/plugin" -import { API_BASE_URL, MODEL_ID, PROVIDER_ID, REFRESH_SAFETY_WINDOW_MS } from "./constants.ts" +import { isAuthExpiring, refreshAuthWithLock } from "./auth-refresh.ts" +import { isOAuthAuth, readAuth, type OAuthAuth } from "./auth-store.ts" +import { API_BASE_URL, MODEL_ID, PROVIDER_ID } from "./constants.ts" import { kimiHeaders } from "./headers.ts" -import { type KimiModelInfo, listModels, pollDeviceToken, refreshToken, startDeviceAuth } from "./oauth.ts" +import { type KimiModelInfo, listModels, pollDeviceToken, startDeviceAuth } from "./oauth.ts" // IMPORTANT: this module must have exactly ONE export — the default // PluginModule object. opencode's plugin loader detects the v1 format @@ -15,13 +14,6 @@ import { type KimiModelInfo, listModels, pollDeviceToken, refreshToken, startDev // module namespace objects with unexpected non-function metadata. // Keep constants in constants.ts and import them here. -type OAuthAuth = { - type: "oauth" - refresh: string - access: string - expires: number -} - type ModelDiscovery = { model_id?: string context_length?: number @@ -73,120 +65,11 @@ type KimiHookInput = { const INTERNAL_PROMPT_CACHE_KEY_HEADER = "x-opencode-kimi-prompt-cache-key" const INTERNAL_REASONING_EFFORT_HEADER = "x-opencode-kimi-reasoning-effort" const INTERNAL_THINKING_TYPE_HEADER = "x-opencode-kimi-thinking-type" -const REFRESH_LOCK_WAIT_MS = 15_000 -const REFRESH_LOCK_POLL_MS = 100 -const REFRESH_LOCK_STALE_MS = 120_000 - -function sleep(ms: number) { - return new Promise((resolve) => setTimeout(resolve, ms)) -} - -function isOAuthAuth(value: unknown): value is OAuthAuth { - if (!value || typeof value !== "object" || Array.isArray(value)) return false - const auth = value as Partial - return ( - auth.type === "oauth" && - typeof auth.access === "string" && - typeof auth.refresh === "string" && - typeof auth.expires === "number" - ) -} - function asRecord(value: unknown): Record | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) return return value as Record } -function authStoreCandidates() { - const home = os.homedir() - if (process.env.XDG_DATA_HOME) { - return [path.join(process.env.XDG_DATA_HOME, "opencode", "auth.json")] - } - const candidates = new Set() - candidates.add(path.join(home, ".local", "share", "opencode", "auth.json")) - if (process.platform === "darwin") { - candidates.add(path.join(home, "Library", "Application Support", "opencode", "auth.json")) - } - if (process.platform === "win32") { - const local = process.env.LOCALAPPDATA ?? path.join(home, "AppData", "Local") - candidates.add(path.join(local, "opencode", "auth.json")) - if (process.env.APPDATA) { - candidates.add(path.join(process.env.APPDATA, "opencode", "auth.json")) - } - } - return [...candidates] -} - -async function resolveAuthStorePath() { - const candidates = authStoreCandidates() - for (const file of candidates) { - try { - await fs.access(file) - return file - } catch {} - } - return candidates[0]! -} - -async function readAuthStoreEntry() { - for (const file of authStoreCandidates()) { - try { - const parsed = JSON.parse(await fs.readFile(file, "utf8")) as Record - const entry = parsed[PROVIDER_ID] ?? parsed[`${PROVIDER_ID}/`] - if (isOAuthAuth(entry)) return entry - } catch {} - } - return -} - -function sameAuth(left: OAuthAuth, right: OAuthAuth) { - return left.access === right.access && left.refresh === right.refresh && left.expires === right.expires -} - -function withInvalidGrantHint(error: unknown) { - if (!(error instanceof Error) || !/invalid_grant/.test(error.message)) return error - const next = new Error( - `${error.message}. The token may have been rotated or revoked in another opencode session — run \`opencode auth login kimi-for-coding-oauth\` again if it does not self-heal.`, - ) as Error & { code?: string; status?: number } - next.code = (error as Error & { code?: string }).code - next.status = (error as Error & { status?: number }).status - return next -} - -async function withRefreshLock(work: () => Promise) { - const authFile = await resolveAuthStorePath() - const lockDir = `${authFile}.${PROVIDER_ID}.refresh.lock` - await fs.mkdir(path.dirname(lockDir), { recursive: true }) - const deadline = Date.now() + REFRESH_LOCK_WAIT_MS - - while (true) { - try { - await fs.mkdir(lockDir) - break - } catch (error) { - const code = (error as NodeJS.ErrnoException).code - if (code !== "EEXIST") throw error - try { - const stat = await fs.stat(lockDir) - if (Date.now() - stat.mtimeMs > REFRESH_LOCK_STALE_MS) { - await fs.rm(lockDir, { recursive: true, force: true }) - continue - } - } catch {} - if (Date.now() >= deadline) { - throw new Error("kimi oauth: timed out waiting for the auth refresh lock") - } - await sleep(REFRESH_LOCK_POLL_MS) - } - } - - try { - return await work() - } finally { - await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined) - } -} - function asThinking(value: unknown): KimiBodyFields["thinking"] | undefined { if (!value || typeof value !== "object" || Array.isArray(value)) return const type = (value as { type?: unknown }).type @@ -478,15 +361,13 @@ const plugin: Plugin = async ({ client }) => { syncProcessAuthContent(auth) } - const isExpiring = (auth: OAuthAuth) => auth.expires - Date.now() < REFRESH_SAFETY_WINDOW_MS - const rememberDiscovery = (discovery: ModelDiscovery) => { if (discovery.model_id) cachedDiscovery = discovery return cachedDiscovery } const readLiveAuth = async () => { - const auth = await readAuthStoreEntry() + const auth = await readAuth() if (auth) syncProcessAuthContent(auth) return auth } @@ -510,26 +391,10 @@ const plugin: Plugin = async ({ client }) => { if (refreshPromise) return refreshPromise refreshPromise = (async () => { try { - return await withRefreshLock(async () => { - const latest = await readLiveAuth() - const current = latest ?? auth - if (latest && !sameAuth(latest, auth) && !force && !isExpiring(latest)) return latest - if (!force && !isExpiring(current)) return current - try { - const tokens = await refreshToken(current.refresh) - const next: OAuthAuth = { - type: "oauth", - refresh: tokens.refresh_token, - access: tokens.access_token, - expires: Date.now() + tokens.expires_in * 1000, - } - await persistAuth(next) - return next - } catch (error) { - const newest = await readLiveAuth() - if (newest && !sameAuth(newest, current)) return newest - throw withInvalidGrantHint(error) - } + return await refreshAuthWithLock(auth, { + force, + readLatest: readLiveAuth, + persist: persistAuth, }) } finally { refreshPromise = undefined @@ -552,7 +417,7 @@ const plugin: Plugin = async ({ client }) => { const current = (await readCurrentAuth()) ?? ctx.auth let auth = current try { - if (isExpiring(auth)) auth = await refreshAuth(auth) + if (isAuthExpiring(auth)) auth = await refreshAuth(auth) return await discover(auth) } catch (error) { if (auth !== current || (error as { status?: number }).status !== 401) return provider.models @@ -615,7 +480,7 @@ const plugin: Plugin = async ({ client }) => { throw new Error( "kimi-for-coding-oauth: not logged in — run `opencode auth login kimi-for-coding-oauth`", ) - if (!force && !isExpiring(current)) return ensureDiscovered(current) + if (!force && !isAuthExpiring(current)) return ensureDiscovered(current) const next = await refreshAuth(current, force) // kimi-cli re-runs `refresh_managed_models` on every successful // refresh — we mirror that so entitlement or display-name changes diff --git a/src/tui.tsx b/src/tui.tsx new file mode 100644 index 0000000..e3483b6 --- /dev/null +++ b/src/tui.tsx @@ -0,0 +1,146 @@ +/** @jsxImportSource @opentui/solid */ +import type { JSX } from "@opentui/solid" +import type { TuiPlugin, TuiPluginApi, TuiPluginModule } from "@opencode-ai/plugin/tui" +import { ensureFreshStoredAuth } from "./auth-refresh.ts" +import { fetchUsage, parseUsagePayload, type UsageRow } from "./usage.ts" + +type UsageViewRow = UsageRow & { + remaining: number + percent: number + ratio: number +} + +const BAR_WIDTH = 56 + +function usageViewRow(row: UsageRow): UsageViewRow { + if (row.limit <= 0) { + return { ...row, remaining: 0, percent: 0, ratio: 0 } + } + const remaining = Math.min(Math.max(row.limit - row.used, 0), row.limit) + const ratio = remaining / row.limit + return { + ...row, + remaining, + ratio, + percent: Math.round(ratio * 100), + } +} + +function usageTone(api: TuiPluginApi, row: UsageViewRow) { + if (row.ratio <= 0.1) return api.theme.current.error + if (row.ratio <= 0.3) return api.theme.current.warning + return api.theme.current.primary +} + +function usageBar(row: UsageViewRow) { + const complete = Math.round(row.ratio * BAR_WIDTH) + return { + complete: "█".repeat(complete), + empty: "░".repeat(BAR_WIDTH - complete), + } +} + +function UsageLoadingRow(props: { label: string; theme: TuiPluginApi["theme"]["current"] }): JSX.Element { + return ( + + {props.label} + + + {"░".repeat(BAR_WIDTH)} + + + + loading + + + ) +} + +function UsageDialog(props: { api: TuiPluginApi; rows?: UsageRow[]; loading?: boolean }): JSX.Element { + const rows = (props.rows ?? []).map(usageViewRow) + const close = () => props.api.ui.dialog.clear() + const theme = props.api.theme.current + + return ( + + + + Kimi Usage + + + esc + + + + {props.loading ? ( + + + + + ) : rows.length === 0 ? ( + + No usage data available. + + ) : ( + + {rows.map((row) => { + const tone = usageTone(props.api, row) + const bar = usageBar(row) + return ( + + {row.label} + + + {bar.complete} + + + {bar.empty} + + + + {row.resetHint ?? ""} + {row.percent}% left + + + ) + })} + + )} + + ) +} + +const tui: TuiPlugin = async (api) => { + api.command.register(() => [ + { + title: "Kimi usage", + value: "kimi.usage", + description: "Show Kimi Code subscription usage", + category: "Kimi", + slash: { + name: "kimi:usage", + }, + onSelect: async () => { + api.ui.dialog.replace(() => ) + try { + const auth = await ensureFreshStoredAuth() + const payload = await fetchUsage(auth.access) + const rows = parseUsagePayload(payload) + api.ui.dialog.replace(() => ) + } catch (error) { + api.ui.dialog.clear() + api.ui.toast({ + message: error instanceof Error ? error.message : "Failed to fetch Kimi usage.", + variant: "error", + duration: 6_000, + }) + } + }, + }, + ]) +} + +export default { + id: "opencode-kimi-full-usage", + tui, +} satisfies TuiPluginModule diff --git a/src/usage.ts b/src/usage.ts new file mode 100644 index 0000000..66c0de6 --- /dev/null +++ b/src/usage.ts @@ -0,0 +1,139 @@ +import { API_BASE_URL, PROVIDER_ID } from "./constants.ts" +import { kimiHeaders } from "./headers.ts" + +export type UsageRow = { + label: string + used: number + limit: number + resetHint?: string +} + +const REQUEST_TIMEOUT_MS = 120_000 + +export async function fetchUsage(accessToken: string) { + const res = await fetch(`${API_BASE_URL}/usages`, { + headers: { + ...kimiHeaders(), + Authorization: `Bearer ${accessToken}`, + Accept: "application/json", + }, + signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS), + }) + const text = await res.text() + if (!res.ok) { + const message = + res.status === 401 + ? `Authorization failed. Run \`opencode auth login ${PROVIDER_ID}\` again.` + : `Kimi usage request failed (${res.status}): ${text.slice(0, 200)}` + const error = new Error(message) as Error & { status?: number } + error.status = res.status + throw error + } + try { + return JSON.parse(text) as Record + } catch { + throw new Error(`Kimi usage returned non-JSON response: ${text.slice(0, 200)}`) + } +} + +function asRecord(value: unknown): Record | undefined { + if (!value || typeof value !== "object" || Array.isArray(value)) return + return value as Record +} + +function toInt(value: unknown) { + const next = Number(value) + if (!Number.isFinite(next)) return + return Math.trunc(next) +} + +function formatDuration(seconds: number) { + const total = Math.max(0, Math.floor(seconds)) + const parts: string[] = [] + const days = Math.floor(total / 86_400) + if (days) parts.push(`${days}d`) + const hours = Math.floor((total % 86_400) / 3_600) + if (hours) parts.push(`${hours}h`) + const minutes = Math.floor((total % 3_600) / 60) + if (minutes) parts.push(`${minutes}m`) + const secs = total % 60 + if (secs && parts.length === 0) parts.push(`${secs}s`) + return parts.join(" ") || "0s" +} + +function formatResetTime(value: string) { + let input = value + if (input.includes(".") && input.endsWith("Z")) { + const [base, fraction] = input.slice(0, -1).split(".") + input = `${base}.${(fraction ?? "").slice(0, 3)}Z` + } + const resetAt = Date.parse(input) + if (Number.isNaN(resetAt)) return `resets at ${value}` + const seconds = Math.max(0, Math.floor((resetAt - Date.now()) / 1000)) + return seconds === 0 ? "reset now" : `resets in ${formatDuration(seconds)}` +} + +function resetHint(data: Record) { + const resetAt = data.reset_at ?? data.resetAt ?? data.reset_time ?? data.resetTime + if (resetAt) return formatResetTime(String(resetAt)) + + const seconds = toInt(data.reset_in ?? data.resetIn ?? data.ttl ?? data.window) + if (!seconds) return + return `resets in ${formatDuration(seconds)}` +} + +function toUsageRow(data: Record, defaultLabel: string): UsageRow | undefined { + const limit = toInt(data.limit) + let used = toInt(data.used) + const remaining = toInt(data.remaining) + if (used === undefined && remaining !== undefined && limit !== undefined) { + used = limit - remaining + } + if (used === undefined && limit === undefined) return + return { + label: String(data.name ?? data.title ?? defaultLabel), + used: used ?? 0, + limit: limit ?? 0, + resetHint: resetHint(data), + } +} + +function limitLabel(item: Record, detail: Record, idx: number) { + const explicit = item.name ?? item.title ?? item.scope ?? detail.name ?? detail.title ?? detail.scope + if (explicit) return String(explicit) + + const window = asRecord(item.window) + const duration = toInt(window?.duration ?? item.duration ?? detail.duration) + const unit = String(window?.timeUnit ?? item.timeUnit ?? detail.timeUnit ?? "") + if (duration) { + if (unit.includes("MINUTE")) { + return duration >= 60 && duration % 60 === 0 ? `${duration / 60}h limit` : `${duration}m limit` + } + if (unit.includes("HOUR")) return `${duration}h limit` + if (unit.includes("DAY")) return `${duration}d limit` + return `${duration}s limit` + } + + return `Limit #${idx + 1}` +} + +export function parseUsagePayload(payload: Record) { + const rows: UsageRow[] = [] + const usage = asRecord(payload.usage) + if (usage) { + const row = toUsageRow(usage, "Weekly limit") + if (row) rows.push(row) + } + + const limits = payload.limits + if (Array.isArray(limits)) { + for (let idx = 0; idx < limits.length; idx++) { + const item = asRecord(limits[idx]) + if (!item) continue + const detail = asRecord(item.detail) ?? item + const row = toUsageRow(detail, limitLabel(item, detail, idx)) + if (row) rows.push(row) + } + } + return rows +} diff --git a/test/auth-refresh.test.ts b/test/auth-refresh.test.ts new file mode 100644 index 0000000..362c3b8 --- /dev/null +++ b/test/auth-refresh.test.ts @@ -0,0 +1,63 @@ +import fs from "node:fs/promises" +import os from "node:os" +import path from "node:path" +import { afterEach, expect, test } from "bun:test" +import { ensureFreshStoredAuth } from "../src/auth-refresh.ts" +import { PROVIDER_ID } from "../src/constants.ts" +import { installFetchMock } from "./_util/fetchMock.ts" + +let mock: ReturnType | undefined +let root: string | undefined +let previousXdgDataHome: string | undefined + +afterEach(async () => { + mock?.restore() + mock = undefined + if (root) await fs.rm(root, { recursive: true, force: true }) + root = undefined + if (previousXdgDataHome === undefined) { + delete process.env.XDG_DATA_HOME + } else { + process.env.XDG_DATA_HOME = previousXdgDataHome + } + previousXdgDataHome = undefined +}) + +function authStorePath(base: string) { + return path.join(base, "opencode", "auth.json") +} + +async function writeAuthStore(base: string, entry: unknown) { + await fs.mkdir(path.dirname(authStorePath(base)), { recursive: true }) + await fs.writeFile(authStorePath(base), JSON.stringify({ [PROVIDER_ID]: entry }), "utf8") +} + +test("ensureFreshStoredAuth refreshes expiring auth and persists it", async () => { + root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-kimi-auth-refresh-")) + previousXdgDataHome = process.env.XDG_DATA_HOME + process.env.XDG_DATA_HOME = root + await writeAuthStore(root, { + type: "oauth", + access: "stale", + refresh: "refresh-1", + expires: Date.now() - 1_000, + }) + + mock = installFetchMock(() => ({ + body: { + access_token: "fresh", + refresh_token: "refresh-2", + token_type: "Bearer", + expires_in: 900, + }, + })) + + const auth = await ensureFreshStoredAuth() + expect(auth.access).toBe("fresh") + expect(auth.refresh).toBe("refresh-2") + + const stored = JSON.parse(await fs.readFile(authStorePath(root), "utf8")) as Record + expect(stored[PROVIDER_ID].access).toBe("fresh") + expect(stored[PROVIDER_ID].refresh).toBe("refresh-2") + expect(mock.calls).toHaveLength(1) +}) diff --git a/test/exports.test.ts b/test/exports.test.ts index 1556c8a..dfe1a25 100644 --- a/test/exports.test.ts +++ b/test/exports.test.ts @@ -1,4 +1,5 @@ import { test, expect } from "bun:test" +import fs from "node:fs" import * as mod from "../src/index.ts" // Regression guard for the 1.0.0 bug + the Windows loading fix: @@ -21,3 +22,11 @@ test("src/index.ts exports exactly one default PluginModule object", () => { expect(typeof obj.server).toBe("function") expect("id" in obj).toBe(true) }) + +test("package exposes a separate TUI entrypoint", () => { + const pkg = JSON.parse(fs.readFileSync(new URL("../package.json", import.meta.url), "utf8")) as { + exports?: Record + } + expect(pkg.exports?.["./tui"]).toBe("./src/tui.tsx") + expect(fs.existsSync(new URL("../src/tui.tsx", import.meta.url))).toBe(true) +}) diff --git a/test/usage.test.ts b/test/usage.test.ts new file mode 100644 index 0000000..756e377 --- /dev/null +++ b/test/usage.test.ts @@ -0,0 +1,41 @@ +import { expect, test } from "bun:test" +import { parseUsagePayload } from "../src/usage.ts" + +test("parseUsagePayload maps summary and rolling limits", () => { + const rows = parseUsagePayload({ + usage: { + limit: "100", + used: "1", + remaining: "99", + reset_in: 6 * 86_400 + 13 * 3_600 + 52 * 60, + }, + limits: [ + { + window: { + duration: 300, + timeUnit: "TIME_UNIT_MINUTE", + }, + detail: { + limit: "100", + remaining: "100", + reset_in: 3 * 3_600 + 52 * 60, + }, + }, + ], + }) + + expect(rows).toEqual([ + { + label: "Weekly limit", + used: 1, + limit: 100, + resetHint: "resets in 6d 13h 52m", + }, + { + label: "5h limit", + used: 0, + limit: 100, + resetHint: "resets in 3h 52m", + }, + ]) +}) diff --git a/tsconfig.json b/tsconfig.json index 68fff60..98803db 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -9,8 +9,10 @@ "skipLibCheck": true, "resolveJsonModule": true, "allowImportingTsExtensions": true, + "jsx": "preserve", + "jsxImportSource": "@opentui/solid", "noEmit": true, "types": ["node"] }, - "include": ["src/**/*.ts"] + "include": ["src/**/*.ts", "src/**/*.tsx"] } diff --git a/tsconfig.tests.json b/tsconfig.tests.json index 3104f33..09ed9b4 100644 --- a/tsconfig.tests.json +++ b/tsconfig.tests.json @@ -1,4 +1,4 @@ { "extends": "./tsconfig.json", - "include": ["src/**/*.ts", "test/**/*.ts", "test/**/*.d.ts"] + "include": ["src/**/*.ts", "src/**/*.tsx", "test/**/*.ts", "test/**/*.d.ts"] } From 0b7142fa496d7021d2d78b0769b21ff69eda2f98 Mon Sep 17 00:00:00 2001 From: justin Date: Fri, 1 May 2026 19:33:39 -0400 Subject: [PATCH 2/2] fix: address usage review findings --- bun.lock | 6 ++- package.json | 6 ++- src/auth-refresh.ts | 82 +++++++++++++++++++++++++++++++++++---- src/tui.tsx | 21 +++++++++- src/usage.ts | 4 +- test/auth-refresh.test.ts | 70 +++++++++++++++++++++++++++++++++ test/usage.test.ts | 12 ++++++ 7 files changed, 185 insertions(+), 16 deletions(-) diff --git a/bun.lock b/bun.lock index 49c7188..44b8dbf 100644 --- a/bun.lock +++ b/bun.lock @@ -4,10 +4,12 @@ "workspaces": { "": { "name": "opencode-kimi-full", - "devDependencies": { - "@opencode-ai/plugin": "^1.4.7", + "dependencies": { "@opentui/core": "0.1.99", "@opentui/solid": "0.1.99", + }, + "devDependencies": { + "@opencode-ai/plugin": "^1.4.7", "@types/node": "^22.0.0", "typescript": "^5.6.0", }, diff --git a/package.json b/package.json index 4dc8bdc..6b3f37e 100644 --- a/package.json +++ b/package.json @@ -45,10 +45,12 @@ "engines": { "opencode": ">=1.4.6" }, + "dependencies": { + "@opentui/core": "0.1.99", + "@opentui/solid": "0.1.99" + }, "devDependencies": { "@opencode-ai/plugin": "^1.4.7", - "@opentui/core": "0.1.99", - "@opentui/solid": "0.1.99", "@types/node": "^22.0.0", "typescript": "^5.6.0" } diff --git a/src/auth-refresh.ts b/src/auth-refresh.ts index 669be33..d2b6659 100644 --- a/src/auth-refresh.ts +++ b/src/auth-refresh.ts @@ -1,3 +1,4 @@ +import crypto from "node:crypto" import fs from "node:fs/promises" import path from "node:path" import { PROVIDER_ID, REFRESH_SAFETY_WINDOW_MS } from "./constants.ts" @@ -13,6 +14,8 @@ import { refreshToken } from "./oauth.ts" const REFRESH_LOCK_WAIT_MS = 15_000 const REFRESH_LOCK_POLL_MS = 100 const REFRESH_LOCK_STALE_MS = 120_000 +const REFRESH_LOCK_HEARTBEAT_MS = 30_000 +const REFRESH_LOCK_OWNER_FILE = "owner.json" type RefreshOptions = { force?: boolean @@ -24,6 +27,10 @@ function sleep(ms: number) { return new Promise((resolve) => setTimeout(resolve, ms)) } +function isNodeError(error: unknown, code: string) { + return (error as NodeJS.ErrnoException).code === code +} + function sameAuth(left: OAuthAuth, right: OAuthAuth) { return left.access === right.access && left.refresh === right.refresh && left.expires === right.expires } @@ -42,26 +49,77 @@ export function isAuthExpiring(auth: OAuthAuth) { return auth.expires - Date.now() < REFRESH_SAFETY_WINDOW_MS } +function lockOwner(token: string) { + return { + token, + pid: process.pid, + updatedAt: Date.now(), + } +} + +async function writeLockOwner(ownerFile: string, token: string) { + const tmpOwnerFile = `${ownerFile}.${process.pid}.${crypto.randomUUID()}.tmp` + try { + await fs.writeFile(tmpOwnerFile, JSON.stringify(lockOwner(token)), "utf8") + await fs.rename(tmpOwnerFile, ownerFile) + } catch (error) { + await fs.rm(tmpOwnerFile, { force: true }).catch(() => undefined) + throw error + } +} + +async function ownsLock(ownerFile: string, token: string) { + try { + const data = JSON.parse(await fs.readFile(ownerFile, "utf8")) as { token?: unknown } + return data.token === token + } catch (error) { + if (isNodeError(error, "ENOENT") || error instanceof SyntaxError) return false + throw error + } +} + +async function removeStaleLock(lockDir: string, ownerFile: string) { + const stat = await fs.stat(ownerFile).catch(async (error) => { + if (!isNodeError(error, "ENOENT")) throw error + return fs.stat(lockDir).catch((lockError) => { + if (isNodeError(lockError, "ENOENT")) return + throw lockError + }) + }) + if (!stat) return true + if (Date.now() - stat.mtimeMs <= REFRESH_LOCK_STALE_MS) return false + + const staleDir = `${lockDir}.stale.${process.pid}.${Date.now()}.${crypto.randomUUID()}` + try { + await fs.rename(lockDir, staleDir) + } catch (error) { + if (isNodeError(error, "ENOENT")) return true + throw error + } + await fs.rm(staleDir, { recursive: true, force: true }) + return true +} + async function withRefreshLock(work: () => Promise) { const authFile = await resolveAuthStorePath() const lockDir = `${authFile}.${PROVIDER_ID}.refresh.lock` + const ownerFile = path.join(lockDir, REFRESH_LOCK_OWNER_FILE) + const ownerToken = crypto.randomUUID() await fs.mkdir(path.dirname(lockDir), { recursive: true }) const deadline = Date.now() + REFRESH_LOCK_WAIT_MS while (true) { try { await fs.mkdir(lockDir) + await writeLockOwner(ownerFile, ownerToken).catch(async (error) => { + await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined) + throw error + }) break } catch (error) { const code = (error as NodeJS.ErrnoException).code if (code !== "EEXIST") throw error - try { - const stat = await fs.stat(lockDir) - if (Date.now() - stat.mtimeMs > REFRESH_LOCK_STALE_MS) { - await fs.rm(lockDir, { recursive: true, force: true }) - continue - } - } catch {} + if (await removeStaleLock(lockDir, ownerFile)) continue if (Date.now() >= deadline) { throw new Error("kimi oauth: timed out waiting for the auth refresh lock") } @@ -69,10 +127,18 @@ async function withRefreshLock(work: () => Promise) { } } + const heartbeat = setInterval(() => { + writeLockOwner(ownerFile, ownerToken).catch(() => undefined) + }, REFRESH_LOCK_HEARTBEAT_MS) + heartbeat.unref?.() + try { return await work() } finally { - await fs.rm(lockDir, { recursive: true, force: true }).catch(() => undefined) + clearInterval(heartbeat) + await ownsLock(ownerFile, ownerToken) + .then((owned) => (owned ? fs.rm(lockDir, { recursive: true, force: true }) : undefined)) + .catch(() => undefined) } } diff --git a/src/tui.tsx b/src/tui.tsx index e3483b6..11d7836 100644 --- a/src/tui.tsx +++ b/src/tui.tsx @@ -111,6 +111,8 @@ function UsageDialog(props: { api: TuiPluginApi; rows?: UsageRow[]; loading?: bo } const tui: TuiPlugin = async (api) => { + let usageRequestId = 0 + api.command.register(() => [ { title: "Kimi usage", @@ -121,13 +123,28 @@ const tui: TuiPlugin = async (api) => { name: "kimi:usage", }, onSelect: async () => { - api.ui.dialog.replace(() => ) + const requestId = ++usageRequestId + let dismissed = false + let replacing = false + const isCurrent = () => usageRequestId === requestId && !dismissed + const markDismissed = () => { + if (!replacing) dismissed = true + } + + api.ui.dialog.replace(() => , markDismissed) try { const auth = await ensureFreshStoredAuth() const payload = await fetchUsage(auth.access) const rows = parseUsagePayload(payload) - api.ui.dialog.replace(() => ) + if (!isCurrent()) return + replacing = true + try { + api.ui.dialog.replace(() => , markDismissed) + } finally { + replacing = false + } } catch (error) { + if (!isCurrent()) return api.ui.dialog.clear() api.ui.toast({ message: error instanceof Error ? error.message : "Failed to fetch Kimi usage.", diff --git a/src/usage.ts b/src/usage.ts index 66c0de6..4fc2042 100644 --- a/src/usage.ts +++ b/src/usage.ts @@ -78,8 +78,8 @@ function resetHint(data: Record) { if (resetAt) return formatResetTime(String(resetAt)) const seconds = toInt(data.reset_in ?? data.resetIn ?? data.ttl ?? data.window) - if (!seconds) return - return `resets in ${formatDuration(seconds)}` + if (seconds === undefined) return + return seconds === 0 ? "reset now" : `resets in ${formatDuration(seconds)}` } function toUsageRow(data: Record, defaultLabel: string): UsageRow | undefined { diff --git a/test/auth-refresh.test.ts b/test/auth-refresh.test.ts index 362c3b8..d82d41b 100644 --- a/test/auth-refresh.test.ts +++ b/test/auth-refresh.test.ts @@ -27,6 +27,10 @@ function authStorePath(base: string) { return path.join(base, "opencode", "auth.json") } +function refreshLockPath(base: string) { + return `${authStorePath(base)}.${PROVIDER_ID}.refresh.lock` +} + async function writeAuthStore(base: string, entry: unknown) { await fs.mkdir(path.dirname(authStorePath(base)), { recursive: true }) await fs.writeFile(authStorePath(base), JSON.stringify({ [PROVIDER_ID]: entry }), "utf8") @@ -61,3 +65,69 @@ test("ensureFreshStoredAuth refreshes expiring auth and persists it", async () = expect(stored[PROVIDER_ID].refresh).toBe("refresh-2") expect(mock.calls).toHaveLength(1) }) + +test("ensureFreshStoredAuth removes stale refresh locks", async () => { + root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-kimi-auth-refresh-")) + previousXdgDataHome = process.env.XDG_DATA_HOME + process.env.XDG_DATA_HOME = root + await writeAuthStore(root, { + type: "oauth", + access: "stale", + refresh: "refresh-1", + expires: Date.now() - 1_000, + }) + + const lockDir = refreshLockPath(root) + await fs.mkdir(lockDir) + await fs.writeFile(path.join(lockDir, "owner.json"), JSON.stringify({ token: "dead" }), "utf8") + const stale = new Date(Date.now() - 180_000) + await fs.utimes(path.join(lockDir, "owner.json"), stale, stale) + await fs.utimes(lockDir, stale, stale) + + mock = installFetchMock(() => ({ + body: { + access_token: "fresh", + refresh_token: "refresh-2", + token_type: "Bearer", + expires_in: 900, + }, + })) + + const auth = await ensureFreshStoredAuth() + expect(auth.access).toBe("fresh") + expect( + await fs + .access(lockDir) + .then(() => true) + .catch(() => false), + ).toBe(false) + expect(mock.calls).toHaveLength(1) +}) + +test("ensureFreshStoredAuth does not fail when cleanup cannot verify lock ownership", async () => { + root = await fs.mkdtemp(path.join(os.tmpdir(), "opencode-kimi-auth-refresh-")) + previousXdgDataHome = process.env.XDG_DATA_HOME + process.env.XDG_DATA_HOME = root + await writeAuthStore(root, { + type: "oauth", + access: "stale", + refresh: "refresh-1", + expires: Date.now() - 1_000, + }) + + mock = installFetchMock(async () => { + await fs.writeFile(path.join(refreshLockPath(root!), "owner.json"), "{", "utf8") + return { + body: { + access_token: "fresh", + refresh_token: "refresh-2", + token_type: "Bearer", + expires_in: 900, + }, + } + }) + + const auth = await ensureFreshStoredAuth() + expect(auth.access).toBe("fresh") + expect(mock.calls).toHaveLength(1) +}) diff --git a/test/usage.test.ts b/test/usage.test.ts index 756e377..3e019dc 100644 --- a/test/usage.test.ts +++ b/test/usage.test.ts @@ -39,3 +39,15 @@ test("parseUsagePayload maps summary and rolling limits", () => { }, ]) }) + +test("parseUsagePayload preserves immediate reset hints", () => { + const rows = parseUsagePayload({ + usage: { + limit: 100, + used: 100, + reset_in: 0, + }, + }) + + expect(rows[0]?.resetHint).toBe("reset now") +})