diff --git a/README.md b/README.md index bf740e0..11e01e0 100644 --- a/README.md +++ b/README.md @@ -136,17 +136,6 @@ Both fine-grained PATs (`github_pat_...`) and classic PATs (`ghp_...`) should wo Tier options: `free`, `pro`, `pro+`, `business`, `enterprise` -PAT scope guidance (read-only first): - -- Personal/user-billed Copilot usage: fine-grained PAT with **Account permissions > Plan > Read**. -- Organization-managed Copilot usage metrics: classic token with **`read:org`** (or fine-grained org permission **Organization Copilot metrics: read** when using org metrics endpoints). -- Enterprise-managed Copilot usage metrics: classic token with **`read:enterprise`**. - -GitHub notes that user-level billing endpoints may not include usage for org/enterprise-managed seats; use org/enterprise metrics endpoints in that case. - -When both Copilot OAuth auth and `copilot-quota-token.json` are present, the plugin prefers the PAT billing path for quota metrics. -Run `/quota_status` and check `copilot_quota_auth` to confirm `pat_state`, candidate paths checked, and `effective_source`/`override`. -
diff --git a/package-lock.json b/package-lock.json index 720eeca..7b89001 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,8 +9,7 @@ "version": "2.4.1", "license": "MIT", "dependencies": { - "opencode-antigravity-auth": "^1.2.8", - "xdg-basedir": "^5.1.0" + "opencode-antigravity-auth": "^1.2.8" }, "devDependencies": { "@opencode-ai/plugin": "^1.1.14", @@ -563,9 +562,9 @@ "license": "MIT" }, "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.59.0.tgz", - "integrity": "sha512-upnNBkA6ZH2VKGcBj9Fyl9IGNPULcjXRlg0LLeaioQWueH30p6IXtJEbKAgvyv+mJaMxSm1l6xwDXYjpEMiLMg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.55.2.tgz", + "integrity": "sha512-21J6xzayjy3O6NdnlO6aXi/urvSRjm6nCI6+nF6ra2YofKruGixN9kfT+dt55HVNwfDmpDHJcaS3JuP/boNnlA==", "cpu": [ "arm" ], @@ -577,9 +576,9 @@ ] }, "node_modules/@rollup/rollup-android-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.59.0.tgz", - "integrity": "sha512-hZ+Zxj3SySm4A/DylsDKZAeVg0mvi++0PYVceVyX7hemkw7OreKdCvW2oQ3T1FMZvCaQXqOTHb8qmBShoqk69Q==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.55.2.tgz", + "integrity": "sha512-eXBg7ibkNUZ+sTwbFiDKou0BAckeV6kIigK7y5Ko4mB/5A1KLhuzEKovsmfvsL8mQorkoincMFGnQuIT92SKqA==", "cpu": [ "arm64" ], @@ -591,9 +590,9 @@ ] }, "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.59.0.tgz", - "integrity": "sha512-W2Psnbh1J8ZJw0xKAd8zdNgF9HRLkdWwwdWqubSVk0pUuQkoHnv7rx4GiF9rT4t5DIZGAsConRE3AxCdJ4m8rg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.55.2.tgz", + "integrity": "sha512-UCbaTklREjrc5U47ypLulAgg4njaqfOVLU18VrCrI+6E5MQjuG0lSWaqLlAJwsD7NpFV249XgB0Bi37Zh5Sz4g==", "cpu": [ "arm64" ], @@ -605,9 +604,9 @@ ] }, "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.59.0.tgz", - "integrity": "sha512-ZW2KkwlS4lwTv7ZVsYDiARfFCnSGhzYPdiOU4IM2fDbL+QGlyAbjgSFuqNRbSthybLbIJ915UtZBtmuLrQAT/w==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.55.2.tgz", + "integrity": "sha512-dP67MA0cCMHFT2g5XyjtpVOtp7y4UyUxN3dhLdt11at5cPKnSm4lY+EhwNvDXIMzAMIo2KU+mc9wxaAQJTn7sQ==", "cpu": [ "x64" ], @@ -619,9 +618,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.59.0.tgz", - "integrity": "sha512-EsKaJ5ytAu9jI3lonzn3BgG8iRBjV4LxZexygcQbpiU0wU0ATxhNVEpXKfUa0pS05gTcSDMKpn3Sx+QB9RlTTA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.55.2.tgz", + "integrity": "sha512-WDUPLUwfYV9G1yxNRJdXcvISW15mpvod1Wv3ok+Ws93w1HjIVmCIFxsG2DquO+3usMNCpJQ0wqO+3GhFdl6Fow==", "cpu": [ "arm64" ], @@ -633,9 +632,9 @@ ] }, "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.59.0.tgz", - "integrity": "sha512-d3DuZi2KzTMjImrxoHIAODUZYoUUMsuUiY4SRRcJy6NJoZ6iIqWnJu9IScV9jXysyGMVuW+KNzZvBLOcpdl3Vg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.55.2.tgz", + "integrity": "sha512-Ng95wtHVEulRwn7R0tMrlUuiLVL/HXA8Lt/MYVpy88+s5ikpntzZba1qEulTuPnPIZuOPcW9wNEiqvZxZmgmqQ==", "cpu": [ "x64" ], @@ -647,9 +646,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.59.0.tgz", - "integrity": "sha512-t4ONHboXi/3E0rT6OZl1pKbl2Vgxf9vJfWgmUoCEVQVxhW6Cw/c8I6hbbu7DAvgp82RKiH7TpLwxnJeKv2pbsw==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.55.2.tgz", + "integrity": "sha512-AEXMESUDWWGqD6LwO/HkqCZgUE1VCJ1OhbvYGsfqX2Y6w5quSXuyoy/Fg3nRqiwro+cJYFxiw5v4kB2ZDLhxrw==", "cpu": [ "arm" ], @@ -661,9 +660,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.59.0.tgz", - "integrity": "sha512-CikFT7aYPA2ufMD086cVORBYGHffBo4K8MQ4uPS/ZnY54GKj36i196u8U+aDVT2LX4eSMbyHtyOh7D7Zvk2VvA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.55.2.tgz", + "integrity": "sha512-ZV7EljjBDwBBBSv570VWj0hiNTdHt9uGznDtznBB4Caj3ch5rgD4I2K1GQrtbvJ/QiB+663lLgOdcADMNVC29Q==", "cpu": [ "arm" ], @@ -675,9 +674,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.59.0.tgz", - "integrity": "sha512-jYgUGk5aLd1nUb1CtQ8E+t5JhLc9x5WdBKew9ZgAXg7DBk0ZHErLHdXM24rfX+bKrFe+Xp5YuJo54I5HFjGDAA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.55.2.tgz", + "integrity": "sha512-uvjwc8NtQVPAJtq4Tt7Q49FOodjfbf6NpqXyW/rjXoV+iZ3EJAHLNAnKT5UJBc6ffQVgmXTUL2ifYiLABlGFqA==", "cpu": [ "arm64" ], @@ -689,9 +688,9 @@ ] }, "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.59.0.tgz", - "integrity": "sha512-peZRVEdnFWZ5Bh2KeumKG9ty7aCXzzEsHShOZEFiCQlDEepP1dpUl/SrUNXNg13UmZl+gzVDPsiCwnV1uI0RUA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.55.2.tgz", + "integrity": "sha512-s3KoWVNnye9mm/2WpOZ3JeUiediUVw6AvY/H7jNA6qgKA2V2aM25lMkVarTDfiicn/DLq3O0a81jncXszoyCFA==", "cpu": [ "arm64" ], @@ -703,9 +702,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.59.0.tgz", - "integrity": "sha512-gbUSW/97f7+r4gHy3Jlup8zDG190AuodsWnNiXErp9mT90iCy9NKKU0Xwx5k8VlRAIV2uU9CsMnEFg/xXaOfXg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.55.2.tgz", + "integrity": "sha512-gi21faacK+J8aVSyAUptML9VQN26JRxe484IbF+h3hpG+sNVoMXPduhREz2CcYr5my0NE3MjVvQ5bMKX71pfVA==", "cpu": [ "loong64" ], @@ -717,9 +716,9 @@ ] }, "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.59.0.tgz", - "integrity": "sha512-yTRONe79E+o0FWFijasoTjtzG9EBedFXJMl888NBEDCDV9I2wGbFFfJQQe63OijbFCUZqxpHz1GzpbtSFikJ4Q==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.55.2.tgz", + "integrity": "sha512-qSlWiXnVaS/ceqXNfnoFZh4IiCA0EwvCivivTGbEu1qv2o+WTHpn1zNmCTAoOG5QaVr2/yhCoLScQtc/7RxshA==", "cpu": [ "loong64" ], @@ -731,9 +730,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.59.0.tgz", - "integrity": "sha512-sw1o3tfyk12k3OEpRddF68a1unZ5VCN7zoTNtSn2KndUE+ea3m3ROOKRCZxEpmT9nsGnogpFP9x6mnLTCaoLkA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.55.2.tgz", + "integrity": "sha512-rPyuLFNoF1B0+wolH277E780NUKf+KoEDb3OyoLbAO18BbeKi++YN6gC/zuJoPPDlQRL3fIxHxCxVEWiem2yXw==", "cpu": [ "ppc64" ], @@ -745,9 +744,9 @@ ] }, "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.59.0.tgz", - "integrity": "sha512-+2kLtQ4xT3AiIxkzFVFXfsmlZiG5FXYW7ZyIIvGA7Bdeuh9Z0aN4hVyXS/G1E9bTP/vqszNIN/pUKCk/BTHsKA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.55.2.tgz", + "integrity": "sha512-g+0ZLMook31iWV4PvqKU0i9E78gaZgYpSrYPed/4Bu+nGTgfOPtfs1h11tSSRPXSjC5EzLTjV/1A7L2Vr8pJoQ==", "cpu": [ "ppc64" ], @@ -759,9 +758,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.59.0.tgz", - "integrity": "sha512-NDYMpsXYJJaj+I7UdwIuHHNxXZ/b/N2hR15NyH3m2qAtb/hHPA4g4SuuvrdxetTdndfj9b1WOmy73kcPRoERUg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.55.2.tgz", + "integrity": "sha512-i+sGeRGsjKZcQRh3BRfpLsM3LX3bi4AoEVqmGDyc50L6KfYsN45wVCSz70iQMwPWr3E5opSiLOwsC9WB4/1pqg==", "cpu": [ "riscv64" ], @@ -773,9 +772,9 @@ ] }, "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.59.0.tgz", - "integrity": "sha512-nLckB8WOqHIf1bhymk+oHxvM9D3tyPndZH8i8+35p/1YiVoVswPid2yLzgX7ZJP0KQvnkhM4H6QZ5m0LzbyIAg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.55.2.tgz", + "integrity": "sha512-C1vLcKc4MfFV6I0aWsC7B2Y9QcsiEcvKkfxprwkPfLaN8hQf0/fKHwSF2lcYzA9g4imqnhic729VB9Fo70HO3Q==", "cpu": [ "riscv64" ], @@ -787,9 +786,9 @@ ] }, "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.59.0.tgz", - "integrity": "sha512-oF87Ie3uAIvORFBpwnCvUzdeYUqi2wY6jRFWJAy1qus/udHFYIkplYRW+wo+GRUP4sKzYdmE1Y3+rY5Gc4ZO+w==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.55.2.tgz", + "integrity": "sha512-68gHUK/howpQjh7g7hlD9DvTTt4sNLp1Bb+Yzw2Ki0xvscm2cOdCLZNJNhd2jW8lsTPrHAHuF751BygifW4bkQ==", "cpu": [ "s390x" ], @@ -801,9 +800,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.59.0.tgz", - "integrity": "sha512-3AHmtQq/ppNuUspKAlvA8HtLybkDflkMuLK4DPo77DfthRb71V84/c4MlWJXixZz4uruIH4uaa07IqoAkG64fg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.55.2.tgz", + "integrity": "sha512-1e30XAuaBP1MAizaOBApsgeGZge2/Byd6wV4a8oa6jPdHELbRHBiw7wvo4dp7Ie2PE8TZT4pj9RLGZv9N4qwlw==", "cpu": [ "x64" ], @@ -815,9 +814,9 @@ ] }, "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.59.0.tgz", - "integrity": "sha512-2UdiwS/9cTAx7qIUZB/fWtToJwvt0Vbo0zmnYt7ED35KPg13Q0ym1g442THLC7VyI6JfYTP4PiSOWyoMdV2/xg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.55.2.tgz", + "integrity": "sha512-4BJucJBGbuGnH6q7kpPqGJGzZnYrpAzRd60HQSt3OpX/6/YVgSsJnNzR8Ot74io50SeVT4CtCWe/RYIAymFPwA==", "cpu": [ "x64" ], @@ -829,9 +828,9 @@ ] }, "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.59.0.tgz", - "integrity": "sha512-M3bLRAVk6GOwFlPTIxVBSYKUaqfLrn8l0psKinkCFxl4lQvOSz8ZrKDz2gxcBwHFpci0B6rttydI4IpS4IS/jQ==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.55.2.tgz", + "integrity": "sha512-cT2MmXySMo58ENv8p6/O6wI/h/gLnD3D6JoajwXFZH6X9jz4hARqUhWpGuQhOgLNXscfZYRQMJvZDtWNzMAIDw==", "cpu": [ "x64" ], @@ -843,9 +842,9 @@ ] }, "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.59.0.tgz", - "integrity": "sha512-tt9KBJqaqp5i5HUZzoafHZX8b5Q2Fe7UjYERADll83O4fGqJ49O1FsL6LpdzVFQcpwvnyd0i+K/VSwu/o/nWlA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.55.2.tgz", + "integrity": "sha512-sZnyUgGkuzIXaK3jNMPmUIyJrxu/PjmATQrocpGA1WbCPX8H5tfGgRSuYtqBYAvLuIGp8SPRb1O4d1Fkb5fXaQ==", "cpu": [ "arm64" ], @@ -857,9 +856,9 @@ ] }, "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.59.0.tgz", - "integrity": "sha512-V5B6mG7OrGTwnxaNUzZTDTjDS7F75PO1ae6MJYdiMu60sq0CqN5CVeVsbhPxalupvTX8gXVSU9gq+Rx1/hvu6A==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.55.2.tgz", + "integrity": "sha512-sDpFbenhmWjNcEbBcoTV0PWvW5rPJFvu+P7XoTY0YLGRupgLbFY0XPfwIbJOObzO7QgkRDANh65RjhPmgSaAjQ==", "cpu": [ "arm64" ], @@ -871,9 +870,9 @@ ] }, "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.59.0.tgz", - "integrity": "sha512-UKFMHPuM9R0iBegwzKF4y0C4J9u8C6MEJgFuXTBerMk7EJ92GFVFYBfOZaSGLu6COf7FxpQNqhNS4c4icUPqxA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.55.2.tgz", + "integrity": "sha512-GvJ03TqqaweWCigtKQVBErw2bEhu1tyfNQbarwr94wCGnczA9HF8wqEe3U/Lfu6EdeNP0p6R+APeHVwEqVxpUQ==", "cpu": [ "ia32" ], @@ -885,9 +884,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.59.0.tgz", - "integrity": "sha512-laBkYlSS1n2L8fSo1thDNGrCTQMmxjYY5G0WFWjFFYZkKPjsMBsgJfGf4TLxXrF6RyhI60L8TMOjBMvXiTcxeA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.55.2.tgz", + "integrity": "sha512-KvXsBvp13oZz9JGe5NYS7FNizLe99Ny+W8ETsuCyjXiKdiGrcz2/J/N8qxZ/RSwivqjQguug07NLHqrIHrqfYw==", "cpu": [ "x64" ], @@ -899,9 +898,9 @@ ] }, "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.59.0.tgz", - "integrity": "sha512-2HRCml6OztYXyJXAvdDXPKcawukWY2GpR5/nxKp4iBgiO3wcoEGkAaqctIbZcNB6KlUQBIqt8VYkNSj2397EfA==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.55.2.tgz", + "integrity": "sha512-xNO+fksQhsAckRtDSPWaMeT1uIM+JrDRXlerpnWNXhn1TdB3YZ6uKBMBTKP0eX9XtYEP978hHk1f8332i2AW8Q==", "cpu": [ "x64" ], @@ -1353,9 +1352,9 @@ "license": "ISC" }, "node_modules/hono": { - "version": "4.12.5", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.12.5.tgz", - "integrity": "sha512-3qq+FUBtlTHhtYxbxheZgY8NIFnkkC/MR8u5TTsr7YZ3wixryQ3cCwn3iZbg8p8B88iDBBAYSfZDS75t8MN7Vg==", + "version": "4.11.9", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.11.9.tgz", + "integrity": "sha512-Eaw2YTGM6WOxA6CXbckaEvslr2Ne4NFsKrvc0v97JD5awbmeBLO5w9Ho9L9kmKonrwF9RJlW6BxT1PVv/agBHQ==", "license": "MIT", "peer": true, "engines": { @@ -1726,9 +1725,9 @@ "license": "MIT" }, "node_modules/rollup": { - "version": "4.59.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.59.0.tgz", - "integrity": "sha512-2oMpl67a3zCH9H79LeMcbDhXW/UmWG/y2zuqnF2jQq5uq9TbM9TVyXvA4+t+ne2IIkBdrLpAaRQAvo7YI/Yyeg==", + "version": "4.55.2", + "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.55.2.tgz", + "integrity": "sha512-PggGy4dhwx5qaW+CKBilA/98Ql9keyfnb7lh4SR6shQ91QQQi1ORJ1v4UinkdP2i87OBs9AQFooQylcrrRfIcg==", "dev": true, "license": "MIT", "dependencies": { @@ -1742,31 +1741,31 @@ "npm": ">=8.0.0" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.59.0", - "@rollup/rollup-android-arm64": "4.59.0", - "@rollup/rollup-darwin-arm64": "4.59.0", - "@rollup/rollup-darwin-x64": "4.59.0", - "@rollup/rollup-freebsd-arm64": "4.59.0", - "@rollup/rollup-freebsd-x64": "4.59.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.59.0", - "@rollup/rollup-linux-arm-musleabihf": "4.59.0", - "@rollup/rollup-linux-arm64-gnu": "4.59.0", - "@rollup/rollup-linux-arm64-musl": "4.59.0", - "@rollup/rollup-linux-loong64-gnu": "4.59.0", - "@rollup/rollup-linux-loong64-musl": "4.59.0", - "@rollup/rollup-linux-ppc64-gnu": "4.59.0", - "@rollup/rollup-linux-ppc64-musl": "4.59.0", - "@rollup/rollup-linux-riscv64-gnu": "4.59.0", - "@rollup/rollup-linux-riscv64-musl": "4.59.0", - "@rollup/rollup-linux-s390x-gnu": "4.59.0", - "@rollup/rollup-linux-x64-gnu": "4.59.0", - "@rollup/rollup-linux-x64-musl": "4.59.0", - "@rollup/rollup-openbsd-x64": "4.59.0", - "@rollup/rollup-openharmony-arm64": "4.59.0", - "@rollup/rollup-win32-arm64-msvc": "4.59.0", - "@rollup/rollup-win32-ia32-msvc": "4.59.0", - "@rollup/rollup-win32-x64-gnu": "4.59.0", - "@rollup/rollup-win32-x64-msvc": "4.59.0", + "@rollup/rollup-android-arm-eabi": "4.55.2", + "@rollup/rollup-android-arm64": "4.55.2", + "@rollup/rollup-darwin-arm64": "4.55.2", + "@rollup/rollup-darwin-x64": "4.55.2", + "@rollup/rollup-freebsd-arm64": "4.55.2", + "@rollup/rollup-freebsd-x64": "4.55.2", + "@rollup/rollup-linux-arm-gnueabihf": "4.55.2", + "@rollup/rollup-linux-arm-musleabihf": "4.55.2", + "@rollup/rollup-linux-arm64-gnu": "4.55.2", + "@rollup/rollup-linux-arm64-musl": "4.55.2", + "@rollup/rollup-linux-loong64-gnu": "4.55.2", + "@rollup/rollup-linux-loong64-musl": "4.55.2", + "@rollup/rollup-linux-ppc64-gnu": "4.55.2", + "@rollup/rollup-linux-ppc64-musl": "4.55.2", + "@rollup/rollup-linux-riscv64-gnu": "4.55.2", + "@rollup/rollup-linux-riscv64-musl": "4.55.2", + "@rollup/rollup-linux-s390x-gnu": "4.55.2", + "@rollup/rollup-linux-x64-gnu": "4.55.2", + "@rollup/rollup-linux-x64-musl": "4.55.2", + "@rollup/rollup-openbsd-x64": "4.55.2", + "@rollup/rollup-openharmony-arm64": "4.55.2", + "@rollup/rollup-win32-arm64-msvc": "4.55.2", + "@rollup/rollup-win32-ia32-msvc": "4.55.2", + "@rollup/rollup-win32-x64-gnu": "4.55.2", + "@rollup/rollup-win32-x64-msvc": "4.55.2", "fsevents": "~2.3.2" } }, diff --git a/src/lib/copilot.ts b/src/lib/copilot.ts index b286160..4252e34 100644 --- a/src/lib/copilot.ts +++ b/src/lib/copilot.ts @@ -10,7 +10,6 @@ */ import type { - AuthData, CopilotAuthData, CopilotQuotaConfig, CopilotTier, @@ -21,9 +20,9 @@ import type { } from "./types.js"; import { fetchWithTimeout } from "./http.js"; import { readAuthFile } from "./opencode-auth.js"; -import { getOpencodeRuntimeDirCandidates } from "./opencode-runtime-paths.js"; import { existsSync, readFileSync } from "fs"; +import { homedir } from "os"; import { join } from "path"; // ============================================================================= @@ -40,7 +39,11 @@ const EDITOR_VERSION = "vscode/1.107.0"; const EDITOR_PLUGIN_VERSION = `copilot-chat/${COPILOT_VERSION}`; const USER_AGENT = `GitHubCopilotChat/${COPILOT_VERSION}`; -const COPILOT_QUOTA_CONFIG_FILENAME = "copilot-quota-token.json"; +const COPILOT_QUOTA_CONFIG_PATH = join( + process.env.XDG_CONFIG_HOME || join(homedir(), ".config"), + "opencode", + "copilot-quota-token.json", +); // ============================================================================= // Helpers @@ -74,61 +77,6 @@ function buildLegacyTokenHeaders(token: string): Record { type GitHubRestAuthScheme = "bearer" | "token"; -type CopilotAuthKeyName = "github-copilot" | "copilot" | "copilot-chat"; - -type CopilotPatTokenKind = "github_pat" | "ghp" | "other"; - -export type CopilotPatState = "absent" | "invalid" | "valid"; - -export interface CopilotPatReadResult { - state: CopilotPatState; - checkedPaths: string[]; - selectedPath?: string; - config?: CopilotQuotaConfig; - error?: string; - tokenKind?: CopilotPatTokenKind; -} - -export interface CopilotQuotaAuthDiagnostics { - pat: CopilotPatReadResult; - oauth: { - configured: boolean; - keyName: CopilotAuthKeyName | null; - hasRefreshToken: boolean; - hasAccessToken: boolean; - }; - effectiveSource: "pat" | "oauth" | "none"; - override: "pat_overrides_oauth" | "none"; -} - -function classifyPatTokenKind(token: string): CopilotPatTokenKind { - const trimmed = token.trim(); - if (trimmed.startsWith("github_pat_")) return "github_pat"; - if (trimmed.startsWith("ghp_")) return "ghp"; - return "other"; -} - -function dedupePaths(paths: string[]): string[] { - const out: string[] = []; - const seen = new Set(); - - for (const path of paths) { - if (!path) continue; - if (seen.has(path)) continue; - seen.add(path); - out.push(path); - } - - return out; -} - -export function getCopilotPatConfigCandidatePaths(): string[] { - const candidates = getOpencodeRuntimeDirCandidates(); - return dedupePaths( - candidates.configDirs.map((configDir) => join(configDir, COPILOT_QUOTA_CONFIG_FILENAME)), - ); -} - function buildGitHubRestHeaders( token: string, scheme: GitHubRestAuthScheme, @@ -176,93 +124,6 @@ async function readGitHubRestErrorMessage(response: Response): Promise { return text.slice(0, 160); } -function validateQuotaConfig(raw: unknown): { config: CopilotQuotaConfig | null; error?: string } { - if (!raw || typeof raw !== "object") { - return { config: null, error: "Config must be a JSON object" }; - } - - const obj = raw as Record; - const token = typeof obj.token === "string" ? obj.token.trim() : ""; - const tierRaw = typeof obj.tier === "string" ? obj.tier.trim() : ""; - const usernameRaw = obj.username; - - if (!token) { - return { config: null, error: "Missing required string field: token" }; - } - - const validTiers: CopilotTier[] = ["free", "pro", "pro+", "business", "enterprise"]; - if (!validTiers.includes(tierRaw as CopilotTier)) { - return { - config: null, - error: "Invalid tier; expected one of: free, pro, pro+, business, enterprise", - }; - } - - let username: string | undefined; - if (usernameRaw != null) { - if (typeof usernameRaw !== "string") { - return { config: null, error: "username must be a non-empty string when provided" }; - } - const trimmed = usernameRaw.trim(); - if (!trimmed) { - return { config: null, error: "username must be a non-empty string when provided" }; - } - username = trimmed; - } - - return { - config: { - token, - tier: tierRaw as CopilotTier, - username, - }, - }; -} - -export function readQuotaConfigWithMeta(): CopilotPatReadResult { - const checkedPaths = getCopilotPatConfigCandidatePaths(); - - for (const path of checkedPaths) { - if (!existsSync(path)) continue; - - try { - const content = readFileSync(path, "utf-8"); - const parsed = JSON.parse(content) as unknown; - const validated = validateQuotaConfig(parsed); - - if (!validated.config) { - return { - state: "invalid", - checkedPaths, - selectedPath: path, - error: validated.error ?? "Invalid config", - }; - } - - return { - state: "valid", - checkedPaths, - selectedPath: path, - config: validated.config, - tokenKind: classifyPatTokenKind(validated.config.token), - }; - } catch (err) { - const msg = err instanceof Error ? err.message : String(err); - return { - state: "invalid", - checkedPaths, - selectedPath: path, - error: msg, - }; - } - } - - return { - state: "absent", - checkedPaths, - }; -} - async function fetchGitHubRestJsonOnce( url: string, token: string, @@ -293,66 +154,51 @@ async function fetchGitHubRestJsonOnce( */ async function readCopilotAuth(): Promise { const authData = await readAuthFile(); - return selectCopilotAuth(authData).auth; + if (!authData) return null; + + // Try known key names in priority order + const copilotAuth = + authData["github-copilot"] ?? + (authData as Record)["copilot"] ?? + (authData as Record)["copilot-chat"]; + + if (!copilotAuth || copilotAuth.type !== "oauth" || !copilotAuth.refresh) { + return null; + } + + return copilotAuth; } /** - * Select Copilot OAuth auth entry from auth.json-shaped data. + * Read optional Copilot quota config from user's config file. + * Returns null if file doesn't exist or is invalid. */ -function selectCopilotAuth( - authData: AuthData | null, -): { auth: CopilotAuthData | null; keyName: CopilotAuthKeyName | null } { - if (!authData) { - return { auth: null, keyName: null }; - } +function readQuotaConfig(): CopilotQuotaConfig | null { + try { + if (!existsSync(COPILOT_QUOTA_CONFIG_PATH)) { + return null; + } - const candidates: Array<[CopilotAuthKeyName, CopilotAuthData | undefined]> = [ - ["github-copilot", authData["github-copilot"]], - ["copilot", (authData as Record).copilot], - ["copilot-chat", (authData as Record)["copilot-chat"]], - ]; - - for (const [keyName, candidate] of candidates) { - if (!candidate) continue; - if (candidate.type !== "oauth") continue; - if (!candidate.refresh) continue; - return { auth: candidate, keyName }; - } + const content = readFileSync(COPILOT_QUOTA_CONFIG_PATH, "utf-8"); + const parsed = JSON.parse(content) as CopilotQuotaConfig; - return { auth: null, keyName: null }; -} + if (!parsed || typeof parsed !== "object") return null; -export function getCopilotQuotaAuthDiagnostics(authData: AuthData | null): CopilotQuotaAuthDiagnostics { - const pat = readQuotaConfigWithMeta(); - const { auth, keyName } = selectCopilotAuth(authData); - const oauthConfigured = Boolean(auth); + if (typeof parsed.token !== "string" || parsed.token.trim() === "") return null; + if (typeof parsed.tier !== "string" || parsed.tier.trim() === "") return null; - let effectiveSource: "pat" | "oauth" | "none" = "none"; - if (pat.state === "valid") { - effectiveSource = "pat"; - } else if (oauthConfigured) { - effectiveSource = "oauth"; - } + // Username is optional now that we prefer the /user/... billing endpoint. + if (parsed.username != null) { + if (typeof parsed.username !== "string" || parsed.username.trim() === "") return null; + } - return { - pat, - oauth: { - configured: oauthConfigured, - keyName, - hasRefreshToken: Boolean(auth?.refresh), - hasAccessToken: Boolean(auth?.access), - }, - effectiveSource, - override: pat.state === "valid" && oauthConfigured ? "pat_overrides_oauth" : "none", - }; -} + const validTiers: CopilotTier[] = ["free", "pro", "pro+", "business", "enterprise"]; + if (!validTiers.includes(parsed.tier as CopilotTier)) return null; -function computePercentRemainingFromUsed(params: { used: number; total: number }): number { - const { used, total } = params; - if (!Number.isFinite(total) || total <= 0) return 0; - if (!Number.isFinite(used) || used <= 0) return 100; - const usedPct = Math.max(0, Math.min(100, Math.ceil((used / total) * 100))); - return 100 - usedPct; + return parsed; + } catch { + return null; + } } // Public billing API response types (keep local; only used here) @@ -459,12 +305,12 @@ function toQuotaResultFromBilling( throw new Error(`Unsupported Copilot tier: ${tier}`); } - const normalizedUsed = Math.max(0, used); - const percentRemaining = computePercentRemainingFromUsed({ used: normalizedUsed, total }); + const remaining = Math.max(0, total - used); + const percentRemaining = Math.max(0, Math.min(100, Math.round((remaining / total) * 100))); return { success: true, - used: normalizedUsed, + used, total, percentRemaining, resetTimeIso: getApproxNextResetIso(), @@ -572,11 +418,11 @@ async function fetchCopilotUsage(authData: CopilotAuthData): Promise { // Strategy 1: Try public billing API with user's fine-grained PAT. - const quotaConfigRead = readQuotaConfigWithMeta(); - if (quotaConfigRead.state === "valid" && quotaConfigRead.config) { + const quotaConfig = readQuotaConfig(); + if (quotaConfig) { try { - const billing = await fetchPublicBillingUsage(quotaConfigRead.config); - return toQuotaResultFromBilling(billing, quotaConfigRead.config.tier); + const billing = await fetchPublicBillingUsage(quotaConfig); + return toQuotaResultFromBilling(billing, quotaConfig.tier); } catch (err) { return { success: false, @@ -613,30 +459,8 @@ export async function queryCopilotQuota(): Promise { } const total = premium.entitlement; - if (!Number.isFinite(total) || total <= 0) { - return { - success: false, - error: "Invalid premium quota entitlement", - } as QuotaError; - } - - const remainingRaw = - typeof premium.remaining === "number" - ? premium.remaining - : typeof premium.quota_remaining === "number" - ? premium.quota_remaining - : NaN; - - if (!Number.isFinite(remainingRaw)) { - return { - success: false, - error: "Invalid premium quota remaining value", - } as QuotaError; - } - - const remaining = Math.max(0, Math.min(total, remainingRaw)); - const used = Math.max(0, total - remaining); - const percentRemaining = computePercentRemainingFromUsed({ used, total }); + const used = total - premium.remaining; + const percentRemaining = Math.round(premium.percent_remaining); return { success: true, diff --git a/src/lib/quota-status.ts b/src/lib/quota-status.ts index 6b41bfd..a25e980 100644 --- a/src/lib/quota-status.ts +++ b/src/lib/quota-status.ts @@ -6,7 +6,6 @@ import { getGoogleTokenCachePath } from "./google-token-cache.js"; import { getAntigravityAccountsCandidatePaths, readAntigravityAccounts } from "./google.js"; import { getFirmwareKeyDiagnostics } from "./firmware.js"; import { getChutesKeyDiagnostics } from "./chutes.js"; -import { getCopilotQuotaAuthDiagnostics } from "./copilot.js"; import { computeQwenQuota, getQwenLocalQuotaPath, @@ -335,31 +334,6 @@ export async function buildQuotaStatusReport(params: { lines.push(`- chutes api key checked: ${chutesDiag.checkedPaths.join(" | ")}`); } - const copilotDiag = getCopilotQuotaAuthDiagnostics(authData); - lines.push(""); - lines.push("copilot_quota_auth:"); - lines.push(`- pat_state: ${copilotDiag.pat.state}`); - if (copilotDiag.pat.selectedPath) { - lines.push(`- pat_path: ${copilotDiag.pat.selectedPath}`); - } - if (copilotDiag.pat.tokenKind) { - lines.push(`- pat_token_kind: ${copilotDiag.pat.tokenKind}`); - } - if (copilotDiag.pat.config?.tier) { - lines.push(`- pat_tier: ${copilotDiag.pat.config.tier}`); - } - if (copilotDiag.pat.error) { - lines.push(`- pat_error: ${copilotDiag.pat.error}`); - } - lines.push( - `- pat_checked_paths: ${copilotDiag.pat.checkedPaths.length ? copilotDiag.pat.checkedPaths.join(" | ") : "(none)"}`, - ); - lines.push( - `- oauth_configured: ${copilotDiag.oauth.configured ? "true" : "false"} key=${copilotDiag.oauth.keyName ?? "(none)"} refresh=${copilotDiag.oauth.hasRefreshToken ? "true" : "false"} access=${copilotDiag.oauth.hasAccessToken ? "true" : "false"}`, - ); - lines.push(`- effective_source: ${copilotDiag.effectiveSource}`); - lines.push(`- override: ${copilotDiag.override}`); - const googleTokenCachePath = getGoogleTokenCachePath(); lines.push( `- google token cache: ${googleTokenCachePath}${(await pathExists(googleTokenCachePath)) ? "" : " (missing)"}`, diff --git a/src/lib/types.ts b/src/lib/types.ts index e65e424..4152a70 100644 --- a/src/lib/types.ts +++ b/src/lib/types.ts @@ -117,9 +117,8 @@ export type CopilotTier = "free" | "pro" | "pro+" | "business" | "enterprise"; * Copilot quota token configuration. * * Stored locally in: - * - OpenCode runtime config candidate directories as - * `.../opencode/copilot-quota-token.json` - * (for example `$XDG_CONFIG_HOME/opencode` or `~/.config/opencode`) + * - $XDG_CONFIG_HOME/opencode/copilot-quota-token.json, or + * - ~/.config/opencode/copilot-quota-token.json * * Users can create a fine-grained PAT with "Plan" read permission * to enable quota checking via GitHub's public billing API. diff --git a/tests/lib.copilot.test.ts b/tests/lib.copilot.test.ts index 3af9884..d57f23b 100644 --- a/tests/lib.copilot.test.ts +++ b/tests/lib.copilot.test.ts @@ -1,52 +1,34 @@ import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; -const fsMocks = vi.hoisted(() => ({ - existsSync: vi.fn<(path: string) => boolean>(() => false), - readFileSync: vi.fn<(path: string, encoding: BufferEncoding) => string>(() => ""), -})); - -const runtimeMocks = vi.hoisted(() => ({ - getOpencodeRuntimeDirCandidates: vi.fn(() => ({ - dataDirs: ["/home/test/.local/share/opencode"], - configDirs: [ - "/home/test/.config/opencode", - "/home/test/Library/Application Support/opencode", - ], - cacheDirs: ["/home/test/.cache/opencode"], - stateDirs: ["/home/test/.local/state/opencode"], - })), - getOpencodeRuntimeDirs: vi.fn(() => ({ - dataDir: "/home/test/.local/share/opencode", - configDir: "/home/test/.config/opencode", - cacheDir: "/home/test/.cache/opencode", - stateDir: "/home/test/.local/state/opencode", - })), -})); - -const authMocks = vi.hoisted(() => ({ - readAuthFile: vi.fn(), -})); - vi.mock("fs", async (importOriginal) => { const mod = await importOriginal(); return { ...mod, - existsSync: fsMocks.existsSync, - readFileSync: fsMocks.readFileSync, + // Prevent test environment from accidentally using a real local PAT config. + existsSync: vi.fn(() => false), }; }); vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirCandidates: runtimeMocks.getOpencodeRuntimeDirCandidates, - getOpencodeRuntimeDirs: runtimeMocks.getOpencodeRuntimeDirs, + getOpencodeRuntimeDirCandidates: () => ({ + dataDirs: ["/home/test/.local/share/opencode"], + configDirs: ["/home/test/.config/opencode"], + cacheDirs: ["/home/test/.cache/opencode"], + stateDirs: ["/home/test/.local/state/opencode"], + }), + getOpencodeRuntimeDirs: () => ({ + dataDir: "/home/test/.local/share/opencode", + configDir: "/home/test/.config/opencode", + cacheDir: "/home/test/.cache/opencode", + stateDir: "/home/test/.local/state/opencode", + }), })); vi.mock("../src/lib/opencode-auth.js", () => ({ - readAuthFile: authMocks.readAuthFile, + readAuthFile: vi.fn(), })); const realEnv = process.env; -const patPath = "/home/test/.config/opencode/copilot-quota-token.json"; describe("queryCopilotQuota", () => { beforeEach(() => { @@ -54,14 +36,6 @@ describe("queryCopilotQuota", () => { vi.setSystemTime(new Date("2026-01-15T12:00:00.000Z")); process.env = { ...realEnv }; vi.resetModules(); - - fsMocks.existsSync.mockReset(); - fsMocks.readFileSync.mockReset(); - authMocks.readAuthFile.mockReset(); - - fsMocks.existsSync.mockReturnValue(false); - fsMocks.readFileSync.mockReturnValue(""); - authMocks.readAuthFile.mockResolvedValue({}); }); afterEach(() => { @@ -70,80 +44,29 @@ describe("queryCopilotQuota", () => { it("returns null when not configured and no PAT config", async () => { const { queryCopilotQuota } = await import("../src/lib/copilot.js"); - authMocks.readAuthFile.mockResolvedValueOnce({}); + const { readAuthFile } = await import("../src/lib/opencode-auth.js"); + (readAuthFile as any).mockResolvedValueOnce({}); await expect(queryCopilotQuota()).resolves.toBeNull(); }); - it("uses PAT billing API when PAT config exists and overrides OAuth auth", async () => { + it("uses token exchange when legacy internal call fails", async () => { const { queryCopilotQuota } = await import("../src/lib/copilot.js"); - - fsMocks.existsSync.mockImplementation((path) => path === patPath); - fsMocks.readFileSync.mockReturnValue( - JSON.stringify({ - token: "github_pat_123456789", - tier: "pro", - }), - ); - - authMocks.readAuthFile.mockResolvedValueOnce({ - "github-copilot": { type: "oauth", refresh: "gho_oauth_token" }, + const { readAuthFile } = await import("../src/lib/opencode-auth.js"); + (readAuthFile as any).mockResolvedValueOnce({ + "github-copilot": { type: "oauth", refresh: "gho_abc" }, }); - const fetchMock = vi.fn(async (url: unknown, opts: RequestInit | undefined) => { + const fetchMock = vi.fn(async (url: any, _opts: any) => { const s = String(url); - if (s.includes("/user/settings/billing/premium_request/usage")) { - expect((opts?.headers as Record | undefined)?.Authorization).toBe( - "Bearer github_pat_123456789", - ); - - return new Response( - JSON.stringify({ - timePeriod: { year: 2026, month: 1 }, - user: "halfwalker", - usageItems: [ - { - product: "copilot", - sku: "Copilot Premium Request", - unitType: "count", - grossQuantity: 1, - netQuantity: 1, - limit: 300, - }, - ], - }), - { status: 200 }, - ); - } - - return new Response("not found", { status: 404 }); - }); - - vi.stubGlobal("fetch", fetchMock as any); - - const out = await queryCopilotQuota(); - expect(out && out.success ? out.total : -1).toBe(300); - expect(out && out.success ? out.used : -1).toBe(1); - expect(out && out.success ? out.percentRemaining : -1).toBe(99); - expect(authMocks.readAuthFile).not.toHaveBeenCalled(); - expect(fetchMock).toHaveBeenCalledTimes(1); - }); - - it("falls back to OAuth/internal flow when PAT config is invalid", async () => { - const { getCopilotQuotaAuthDiagnostics, queryCopilotQuota } = await import("../src/lib/copilot.js"); - - fsMocks.existsSync.mockImplementation((path) => path === patPath); - fsMocks.readFileSync.mockReturnValue("{bad-json"); - - const oauthAuth = { - "github-copilot": { type: "oauth", refresh: "gho_abc" }, - }; - authMocks.readAuthFile.mockResolvedValueOnce(oauthAuth); - - const fetchMock = vi.fn(async (url: unknown) => { - const s = String(url); if (s.includes("/copilot_internal/user")) { + // first attempt: legacy auth call fails, second attempt: bearer works + const auth = _opts?.headers?.Authorization || _opts?.headers?.authorization; + if (typeof auth === "string" && auth.startsWith("token ")) { + return new Response("forbidden", { status: 403 }); + } + return new Response( JSON.stringify({ copilot_plan: "pro", @@ -151,8 +74,8 @@ describe("queryCopilotQuota", () => { quota_snapshots: { premium_interactions: { entitlement: 300, - remaining: 299, - percent_remaining: 100, + remaining: 200, + percent_remaining: 66.7, unlimited: false, overage_count: 0, overage_permitted: false, @@ -165,89 +88,13 @@ describe("queryCopilotQuota", () => { ); } - return new Response("not found", { status: 404 }); - }); - - vi.stubGlobal("fetch", fetchMock as any); - - const out = await queryCopilotQuota(); - expect(out && out.success ? out.total : -1).toBe(300); - expect(out && out.success ? out.used : -1).toBe(1); - expect(out && out.success ? out.percentRemaining : -1).toBe(99); - - const diag = getCopilotQuotaAuthDiagnostics(oauthAuth as any); - expect(diag.pat.state).toBe("invalid"); - expect(diag.pat.selectedPath).toBe(patPath); - expect(diag.effectiveSource).toBe("oauth"); - expect(diag.override).toBe("none"); - }); - - it("returns PAT error and does not fall back to OAuth when PAT is rejected", async () => { - const { queryCopilotQuota } = await import("../src/lib/copilot.js"); - - fsMocks.existsSync.mockImplementation((path) => path === patPath); - fsMocks.readFileSync.mockReturnValue( - JSON.stringify({ - token: "github_pat_123456789", - tier: "pro", - }), - ); - - authMocks.readAuthFile.mockResolvedValueOnce({ - "github-copilot": { type: "oauth", refresh: "gho_should_not_be_used" }, - }); - - const fetchMock = vi.fn(async (url: unknown) => { - const s = String(url); - - if (s.includes("/user/settings/billing/premium_request/usage")) { - return new Response(JSON.stringify({ message: "Forbidden" }), { status: 403 }); - } - - if (s.includes("/copilot_internal/user")) { - return new Response("unexpected oauth fallback", { status: 200 }); - } - - return new Response("not found", { status: 404 }); - }); - - vi.stubGlobal("fetch", fetchMock as any); - - const out = await queryCopilotQuota(); - expect(out && !out.success ? out.error : "").toContain("GitHub API error 403"); - expect(fetchMock.mock.calls.some(([url]) => String(url).includes("/copilot_internal/user"))).toBe( - false, - ); - expect(authMocks.readAuthFile).not.toHaveBeenCalled(); - }); - - it("computes remaining percentage from entitlement/remaining when OAuth response percent is stale", async () => { - const { queryCopilotQuota } = await import("../src/lib/copilot.js"); - - authMocks.readAuthFile.mockResolvedValueOnce({ - "github-copilot": { type: "oauth", refresh: "gho_abc" }, - }); - - const fetchMock = vi.fn(async (url: unknown) => { - const s = String(url); - - if (s.includes("/copilot_internal/user")) { + if (s.includes("/copilot_internal/v2/token")) { return new Response( JSON.stringify({ - copilot_plan: "pro", - quota_reset_date: "2026-02-01T00:00:00.000Z", - quota_snapshots: { - premium_interactions: { - entitlement: 300, - remaining: 299, - percent_remaining: 100, - unlimited: false, - overage_count: 0, - overage_permitted: false, - quota_id: "x", - quota_remaining: 299, - }, - }, + token: "cpt_sess", + expires_at: Date.now() + 60_000, + refresh_in: 30_000, + endpoints: { api: "https://api.github.com" }, }), { status: 200 }, ); @@ -259,7 +106,7 @@ describe("queryCopilotQuota", () => { vi.stubGlobal("fetch", fetchMock as any); const out = await queryCopilotQuota(); - expect(out && out.success ? out.used : -1).toBe(1); - expect(out && out.success ? out.percentRemaining : -1).toBe(99); + expect(out && out.success ? out.total : -1).toBe(300); + expect(out && out.success ? out.used : -1).toBe(100); }); }); diff --git a/tests/lib.quota-status.copilot-diagnostics.test.ts b/tests/lib.quota-status.copilot-diagnostics.test.ts deleted file mode 100644 index 5fb3c8a..0000000 --- a/tests/lib.quota-status.copilot-diagnostics.test.ts +++ /dev/null @@ -1,195 +0,0 @@ -import { beforeEach, describe, expect, it, vi } from "vitest"; - -const copilotMocks = vi.hoisted(() => ({ - getCopilotQuotaAuthDiagnostics: vi.fn(), -})); - -vi.mock("fs/promises", () => ({ - stat: vi.fn(async () => ({ mtimeMs: Date.now() })), -})); - -vi.mock("../src/lib/opencode-auth.js", () => ({ - getAuthPath: () => "/home/test/.config/opencode/auth.json", - getAuthPaths: () => ["/home/test/.config/opencode/auth.json"], - readAuthFileCached: vi.fn(async () => ({ - "github-copilot": { type: "oauth", refresh: "gho_test" }, - })), -})); - -vi.mock("../src/lib/opencode-runtime-paths.js", () => ({ - getOpencodeRuntimeDirs: () => ({ - dataDir: "/home/test/.local/share/opencode", - configDir: "/home/test/.config/opencode", - cacheDir: "/home/test/.cache/opencode", - stateDir: "/home/test/.local/state/opencode", - }), -})); - -vi.mock("../src/lib/google-token-cache.js", () => ({ - getGoogleTokenCachePath: () => "/home/test/.cache/opencode/google-token-cache.json", -})); - -vi.mock("../src/lib/google.js", () => ({ - getAntigravityAccountsCandidatePaths: () => ["/home/test/.config/opencode/antigravity-accounts.json"], - readAntigravityAccounts: vi.fn(async () => []), -})); - -vi.mock("../src/lib/firmware.js", () => ({ - getFirmwareKeyDiagnostics: vi.fn(async () => ({ configured: false, source: null, checkedPaths: [] })), -})); - -vi.mock("../src/lib/chutes.js", () => ({ - getChutesKeyDiagnostics: vi.fn(async () => ({ configured: false, source: null, checkedPaths: [] })), -})); - -vi.mock("../src/lib/qwen-local-quota.js", () => ({ - computeQwenQuota: () => ({ - day: { used: 0, limit: 1000 }, - rpm: { used: 0, limit: 60 }, - }), - getQwenLocalQuotaPath: () => "/home/test/.local/state/opencode/opencode-quota/qwen-local-quota.json", - readQwenLocalQuotaState: vi.fn(async () => ({ day: { used: 0 }, minute: [] })), -})); - -vi.mock("../src/lib/qwen-auth.js", () => ({ - hasQwenOAuthAuth: () => false, -})); - -vi.mock("../src/lib/modelsdev-pricing.js", () => ({ - getPricingSnapshotHealth: () => ({ ageMs: 0, maxAgeMs: 259200000, stale: false }), - getPricingRefreshPolicy: () => ({ maxAgeMs: 259200000 }), - getPricingSnapshotMeta: () => ({ - source: "bundled", - generatedAt: Date.parse("2026-01-01T00:00:00.000Z"), - units: "usd_1m_tokens", - }), - getPricingSnapshotSource: () => "bundled", - getRuntimePricingRefreshStatePath: () => "/home/test/.cache/opencode/opencode-quota/pricing-refresh-state.json", - getRuntimePricingSnapshotPath: () => "/home/test/.cache/opencode/opencode-quota/pricing.json", - listProviders: () => [], - getProviderModelCount: () => 0, - hasProvider: () => false, - readPricingRefreshState: vi.fn(async () => null), -})); - -vi.mock("../src/providers/registry.js", () => ({ - getProviders: () => [], -})); - -vi.mock("../src/lib/version.js", () => ({ - getPackageVersion: vi.fn(async () => "2.4.0-test"), -})); - -vi.mock("../src/lib/opencode-storage.js", () => ({ - getOpenCodeDbPath: () => "/home/test/.local/share/opencode/opencode.db", - getOpenCodeDbPathCandidates: () => ["/home/test/.local/share/opencode/opencode.db"], - getOpenCodeDbStats: vi.fn(async () => ({ - sessionCount: 0, - messageCount: 0, - assistantMessageCount: 0, - })), -})); - -vi.mock("../src/lib/quota-stats.js", () => ({ - aggregateUsage: vi.fn(async () => ({ - byModel: [], - unknown: [], - unpriced: [], - bySourceProvider: [], - totals: { - unknown: { input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 }, - unpriced: { input: 0, output: 0, reasoning: 0, cache_read: 0, cache_write: 0 }, - }, - })), -})); - -vi.mock("../src/lib/copilot.js", () => ({ - getCopilotQuotaAuthDiagnostics: copilotMocks.getCopilotQuotaAuthDiagnostics, -})); - -describe("buildQuotaStatusReport copilot diagnostics", () => { - beforeEach(() => { - copilotMocks.getCopilotQuotaAuthDiagnostics.mockReset(); - }); - - it("renders PAT override details when PAT is effective source", async () => { - copilotMocks.getCopilotQuotaAuthDiagnostics.mockReturnValue({ - pat: { - state: "valid", - checkedPaths: [ - "/home/test/.config/opencode/copilot-quota-token.json", - "/home/test/Library/Application Support/opencode/copilot-quota-token.json", - ], - selectedPath: "/home/test/.config/opencode/copilot-quota-token.json", - tokenKind: "github_pat", - config: { - token: "github_pat_hidden", - tier: "enterprise", - }, - }, - oauth: { - configured: true, - keyName: "github-copilot", - hasRefreshToken: true, - hasAccessToken: false, - }, - effectiveSource: "pat", - override: "pat_overrides_oauth", - }); - - const { buildQuotaStatusReport } = await import("../src/lib/quota-status.js"); - const report = await buildQuotaStatusReport({ - configSource: "test", - configPaths: [], - enabledProviders: "auto", - onlyCurrentModel: false, - sessionModelLookup: "no_session", - providerAvailability: [], - googleRefresh: { attempted: false }, - }); - - expect(report).toContain("copilot_quota_auth:"); - expect(report).toContain("- pat_state: valid"); - expect(report).toContain( - "- pat_checked_paths: /home/test/.config/opencode/copilot-quota-token.json | /home/test/Library/Application Support/opencode/copilot-quota-token.json", - ); - expect(report).toContain("- effective_source: pat"); - expect(report).toContain("- override: pat_overrides_oauth"); - expect(copilotMocks.getCopilotQuotaAuthDiagnostics).toHaveBeenCalledOnce(); - }); - - it("renders invalid PAT and oauth effective source details", async () => { - copilotMocks.getCopilotQuotaAuthDiagnostics.mockReturnValue({ - pat: { - state: "invalid", - checkedPaths: ["/home/test/.config/opencode/copilot-quota-token.json"], - selectedPath: "/home/test/.config/opencode/copilot-quota-token.json", - error: "Unexpected token b in JSON at position 1", - }, - oauth: { - configured: true, - keyName: "github-copilot", - hasRefreshToken: true, - hasAccessToken: true, - }, - effectiveSource: "oauth", - override: "none", - }); - - const { buildQuotaStatusReport } = await import("../src/lib/quota-status.js"); - const report = await buildQuotaStatusReport({ - configSource: "test", - configPaths: [], - enabledProviders: "auto", - onlyCurrentModel: false, - sessionModelLookup: "no_session", - providerAvailability: [], - googleRefresh: { attempted: false }, - }); - - expect(report).toContain("- pat_state: invalid"); - expect(report).toContain("- pat_error: Unexpected token b in JSON at position 1"); - expect(report).toContain("- effective_source: oauth"); - expect(report).toContain("- override: none"); - }); -});