diff --git a/.vscode/settings.json b/.vscode/settings.json index 124d331e..145f6642 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,5 +4,6 @@ "editor.codeActionsOnSave": { "source.fixAll.eslint": "explicit", "source.organizeImports": "never" - } + }, + "typescript.tsdk": "node_modules/typescript/lib" } diff --git a/README.md b/README.md index bd4a7164..f2ead0c7 100644 --- a/README.md +++ b/README.md @@ -40,18 +40,17 @@ npx copilot-api --port 8080 ## Command Line Options -The command accepts several command line options: +The following command line options are available: -| Option | Description | Default | -| ------------- | ------------------------------------ | ------- | -| --port, -p | Port to listen on | 4141 | -| --verbose, -v | Enable verbose logging | false | -| --log-file | File to log request/response details | - | +| Option | Description | Default | +| ------------- | ---------------------- | ------- | +| --port, -p | Port to listen on | 4141 | +| --verbose, -v | Enable verbose logging | false | -Example with options: +Example usage: ```sh -npx copilot-api@latest --port 8080 --verbose --log-file copilot-api.txt +npx copilot-api@latest --port 8080 --verbose ``` ## Running from Source @@ -70,9 +69,19 @@ bun run dev bun run start ``` -## Tips to not hit the rate limit +## Usage Tips -- Use a free model from free provider like Gemini/Mistral/Openrouter for the weak model -- Rarely use architect mode -- Do not enable automatic yes in aider config -- Claude 3.7 thinking mode uses more tokens. Use it sparingly +To avoid rate limiting and optimize your experience: + +- Consider using free models (e.g., Gemini, Mistral, Openrouter) as the `weak-model` +- Use architect mode sparingly +- Disable `yes-always` in your aider configuration +- Be mindful that Claude 3.7 thinking mode consume more tokens + +## Roadmap + +- [ ] Manual authentication flow +- [ ] Manual request approval system +- [ ] Rate limiting implementation +- [ ] Token usage tracking and monitoring +- [ ] Enhanced error handling and recovery diff --git a/bun.lock b/bun.lock index 2abe97cc..76c1477c 100644 --- a/bun.lock +++ b/bun.lock @@ -7,25 +7,21 @@ "citty": "^0.1.6", "consola": "^3.4.0", "fetch-event-stream": "^0.1.5", - "get-port-please": "^3.1.2", "hono": "^4.7.4", - "ofetch": "^1.4.1", - "pathe": "^2.0.3", - "srvx": "^0.1.4", - "zod": "^3.24.2", + "srvx": "^0.2.5", }, "devDependencies": { "@echristian/eslint-config": "^0.0.32", "@types/bun": "^1.2.4", "bumpp": "^10.0.3", "eslint": "^9.22.0", + "jiti": "^2.4.2", "knip": "^5.45.0", "lint-staged": "^15.4.3", + "prettier-plugin-packagejson": "^2.5.10", "simple-git-hooks": "^2.11.1", - "tinyexec": "^0.3.2", "tsup": "^8.4.0", "typescript": "^5.8.2", - "vitest": "^3.0.8", }, }, }, @@ -226,20 +222,6 @@ "@typescript-eslint/visitor-keys": ["@typescript-eslint/visitor-keys@8.26.0", "", { "dependencies": { "@typescript-eslint/types": "8.26.0", "eslint-visitor-keys": "^4.2.0" } }, "sha512-2z8JQJWAzPdDd51dRQ/oqIJxe99/hoLIqmf8RMCAJQtYDc535W/Jt2+RTP4bP0aKeBG1F65yjIZuczOXCmbWwg=="], - "@vitest/expect": ["@vitest/expect@3.0.8", "", { "dependencies": { "@vitest/spy": "3.0.8", "@vitest/utils": "3.0.8", "chai": "^5.2.0", "tinyrainbow": "^2.0.0" } }, "sha512-Xu6TTIavTvSSS6LZaA3EebWFr6tsoXPetOWNMOlc7LO88QVVBwq2oQWBoDiLCN6YTvNYsGSjqOO8CAdjom5DCQ=="], - - "@vitest/mocker": ["@vitest/mocker@3.0.8", "", { "dependencies": { "@vitest/spy": "3.0.8", "estree-walker": "^3.0.3", "magic-string": "^0.30.17" }, "peerDependencies": { "msw": "^2.4.9", "vite": "^5.0.0 || ^6.0.0" }, "optionalPeers": ["msw", "vite"] }, "sha512-n3LjS7fcW1BCoF+zWZxG7/5XvuYH+lsFg+BDwwAz0arIwHQJFUEsKBQ0BLU49fCxuM/2HSeBPHQD8WjgrxMfow=="], - - "@vitest/pretty-format": ["@vitest/pretty-format@3.0.8", "", { "dependencies": { "tinyrainbow": "^2.0.0" } }, "sha512-BNqwbEyitFhzYMYHUVbIvepOyeQOSFA/NeJMIP9enMntkkxLgOcgABH6fjyXG85ipTgvero6noreavGIqfJcIg=="], - - "@vitest/runner": ["@vitest/runner@3.0.8", "", { "dependencies": { "@vitest/utils": "3.0.8", "pathe": "^2.0.3" } }, "sha512-c7UUw6gEcOzI8fih+uaAXS5DwjlBaCJUo7KJ4VvJcjL95+DSR1kova2hFuRt3w41KZEFcOEiq098KkyrjXeM5w=="], - - "@vitest/snapshot": ["@vitest/snapshot@3.0.8", "", { "dependencies": { "@vitest/pretty-format": "3.0.8", "magic-string": "^0.30.17", "pathe": "^2.0.3" } }, "sha512-x8IlMGSEMugakInj44nUrLSILh/zy1f2/BgH0UeHpNyOocG18M9CWVIFBaXPt8TrqVZWmcPjwfG/ht5tnpba8A=="], - - "@vitest/spy": ["@vitest/spy@3.0.8", "", { "dependencies": { "tinyspy": "^3.0.2" } }, "sha512-MR+PzJa+22vFKYb934CejhR4BeRpMSoxkvNoDit68GQxRLSf11aT6CTj3XaqUU9rxgWJFnqicN/wxw6yBRkI1Q=="], - - "@vitest/utils": ["@vitest/utils@3.0.8", "", { "dependencies": { "@vitest/pretty-format": "3.0.8", "loupe": "^3.1.3", "tinyrainbow": "^2.0.0" } }, "sha512-nkBC3aEhfX2PdtQI/QwAWp8qZWwzASsU4Npbcd5RdMPBSSLCpkZp52P3xku3s3uA0HIEhGvEcF8rNkBsz9dQ4Q=="], - "acorn": ["acorn@8.14.0", "", { "bin": { "acorn": "bin/acorn" } }, "sha512-cl669nCJTZBsL97OF4kUQm5g5hC2uihk0NxY3WENAC0TYdILVkAyHymAntgxGkl7K+t0cXIrH5siy5S4XkFycA=="], "acorn-jsx": ["acorn-jsx@5.3.2", "", { "peerDependencies": { "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ=="], @@ -272,8 +254,6 @@ "arraybuffer.prototype.slice": ["arraybuffer.prototype.slice@1.0.4", "", { "dependencies": { "array-buffer-byte-length": "^1.0.1", "call-bind": "^1.0.8", "define-properties": "^1.2.1", "es-abstract": "^1.23.5", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "is-array-buffer": "^3.0.4" } }, "sha512-BNoCY6SXXPQ7gF2opIP4GBE+Xw7U+pHMYKuzjgCN3GwiaIR09UUeKfheyIry77QtrCBlC0KK0q5/TER/tYh3PQ=="], - "assertion-error": ["assertion-error@2.0.1", "", {}, "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA=="], - "ast-types-flow": ["ast-types-flow@0.0.8", "", {}, "sha512-OH/2E5Fg20h2aPrbe+QL8JZQFko0YZaF+j4mnQ7BGhfavO7OpSLa8a0y9sBwomHdSbkhTS8TQNayBfnW5DwbvQ=="], "async-function": ["async-function@1.0.0", "", {}, "sha512-hsU18Ae8CDTR6Kgu9DYf0EbCr/a5iGL0rytQDobUcdpYOKokk8LEjVphnXkDkgpi0wYVsqrXuP0bZxJaTqdgoA=="], @@ -316,12 +296,8 @@ "caniuse-lite": ["caniuse-lite@1.0.30001702", "", {}, "sha512-LoPe/D7zioC0REI5W73PeR1e1MLCipRGq/VkovJnd6Df+QVqT+vT33OXCp8QUd7kA7RZrHWxb1B36OQKI/0gOA=="], - "chai": ["chai@5.2.0", "", { "dependencies": { "assertion-error": "^2.0.1", "check-error": "^2.1.1", "deep-eql": "^5.0.1", "loupe": "^3.1.0", "pathval": "^2.0.0" } }, "sha512-mCuXncKXk5iCLhfhwTc0izo0gtEmpz5CtG2y8GiOINBlMVS6v8TMRc5TaLWKS6692m9+dVVfzgeVxR5UxWHTYw=="], - "chalk": ["chalk@4.1.2", "", { "dependencies": { "ansi-styles": "^4.1.0", "supports-color": "^7.1.0" } }, "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA=="], - "check-error": ["check-error@2.1.1", "", {}, "sha512-OAlb+T7V4Op9OwdkjmguYRqncdlx5JiofwOAUkmTF+jNdHwzTaTs4sRAGpzLF3oOz5xAyDGrPgeIDFQmDOTiJw=="], - "chokidar": ["chokidar@4.0.3", "", { "dependencies": { "readdirp": "^4.0.1" } }, "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA=="], "chownr": ["chownr@2.0.0", "", {}, "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ=="], @@ -360,7 +336,7 @@ "consola": ["consola@3.4.0", "", {}, "sha512-EiPU8G6dQG0GFHNR8ljnZFki/8a+cQwEQ+7wpxdChl02Q8HXlwEZWD5lqAF8vC2sEC3Tehr8hy7vErz88LHyUA=="], - "cookie-es": ["cookie-es@1.2.2", "", {}, "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg=="], + "cookie-es": ["cookie-es@2.0.0", "", {}, "sha512-RAj4E421UYRgqokKUmotqAwuplYw15qtdXfY+hGzgCJ/MBjCVZcSoHK/kH9kocfjRjcDME7IiDWR/1WX1TM2Pg=="], "core-js-compat": ["core-js-compat@3.41.0", "", { "dependencies": { "browserslist": "^4.24.4" } }, "sha512-RFsU9LySVue9RTwdDVX/T0e2Y6jRYWXERKElIjpuEOEnxaXffI0X7RUwVzfYLfzuLXSNJDYoRYUAmRUcyln20A=="], @@ -376,8 +352,6 @@ "debug": ["debug@4.4.0", "", { "dependencies": { "ms": "^2.1.3" } }, "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA=="], - "deep-eql": ["deep-eql@5.0.2", "", {}, "sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q=="], - "deep-is": ["deep-is@0.1.4", "", {}, "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ=="], "defaults": ["defaults@1.0.4", "", { "dependencies": { "clone": "^1.0.2" } }, "sha512-eFuaLoy/Rxalv2kr+lqMlUnrDWV+3j4pljOIJgLIhI058IQfWJ7vXhyEIHu+HtC738klGALYxOKDO0bQP3tg8A=="], @@ -406,7 +380,7 @@ "emoji-regex": ["emoji-regex@9.2.2", "", {}, "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="], - "enhanced-resolve": ["enhanced-resolve@5.18.0", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-0/r0MySGYG8YqlayBZ6MuCfECmHFdJ5qyPh8s8wa5Hnm6SaFLSK1VYCbj+NKp090Nm1caZhD+QTnmxO7esYGyQ=="], + "enhanced-resolve": ["enhanced-resolve@5.18.1", "", { "dependencies": { "graceful-fs": "^4.2.4", "tapable": "^2.2.0" } }, "sha512-ZSW3ma5GkcQBIpwZTSRAI8N71Uuwgs93IezB7mf7R60tC8ZbJideoDNKjHn2O9KIlx6rkGTTEk1xUCK2E1Y2Yg=="], "environment": ["environment@1.1.0", "", {}, "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q=="], @@ -416,8 +390,6 @@ "es-errors": ["es-errors@1.3.0", "", {}, "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw=="], - "es-module-lexer": ["es-module-lexer@1.6.0", "", {}, "sha512-qqnD1yMU6tk/jnaMosogGySTZP8YtUgAffA9nMN+E/rjxcfRQ6IEk7IiozUjgxKoFHBGjTLnrHB/YC45r/59EQ=="], - "es-object-atoms": ["es-object-atoms@1.1.1", "", { "dependencies": { "es-errors": "^1.3.0" } }, "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA=="], "es-set-tostringtag": ["es-set-tostringtag@2.1.0", "", { "dependencies": { "es-errors": "^1.3.0", "get-intrinsic": "^1.2.6", "has-tostringtag": "^1.0.2", "hasown": "^2.0.2" } }, "sha512-j6vWzfrGVfyXxge+O0x5sh6cvxAog0a/4Rdd2K36zCMV5eJ+/+tOAngRO8cODMNWbVRdVlmGZQL2YS3yR8bIUA=="], @@ -482,16 +454,12 @@ "estraverse": ["estraverse@5.3.0", "", {}, "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA=="], - "estree-walker": ["estree-walker@3.0.3", "", { "dependencies": { "@types/estree": "^1.0.0" } }, "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g=="], - "esutils": ["esutils@2.0.3", "", {}, "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g=="], "eventemitter3": ["eventemitter3@5.0.1", "", {}, "sha512-GWkBvjiSZK87ELrYOSESUYeVIc9mvLLf/nXalMOS5dYrgZq9o5OVkbZAVM06CVxYsCwH9BDZFPlQTlPA1j4ahA=="], "execa": ["execa@8.0.1", "", { "dependencies": { "cross-spawn": "^7.0.3", "get-stream": "^8.0.1", "human-signals": "^5.0.0", "is-stream": "^3.0.0", "merge-stream": "^2.0.0", "npm-run-path": "^5.1.0", "onetime": "^6.0.0", "signal-exit": "^4.1.0", "strip-final-newline": "^3.0.0" } }, "sha512-VyhnebXciFV2DESc+p6B+y0LjSm0krU4OgJN44qFAhBY0TJ+1V61tYD2+wHusZ6F9n5K+vl8k0sTy7PEfV4qpg=="], - "expect-type": ["expect-type@1.2.0", "", {}, "sha512-80F22aiJ3GLyVnS/B3HzgR6RelZVumzj9jkL0Rhz4h0xYbNW9PjlQz5h3J/SShErbXBc295vseR4/MIbVmUbeA=="], - "fast-deep-equal": ["fast-deep-equal@3.1.3", "", {}, "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="], "fast-diff": ["fast-diff@1.3.0", "", {}, "sha512-VxPP4NqbUjj6MaAOafWeUn2cXWLcCtljklUtZf0Ind4XQ+QPtmA0b18zZy0jIQx+ExRVCR/ZQpBmik5lXshNsw=="], @@ -540,8 +508,6 @@ "get-intrinsic": ["get-intrinsic@1.3.0", "", { "dependencies": { "call-bind-apply-helpers": "^1.0.2", "es-define-property": "^1.0.1", "es-errors": "^1.3.0", "es-object-atoms": "^1.1.1", "function-bind": "^1.1.2", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-symbols": "^1.1.0", "hasown": "^2.0.2", "math-intrinsics": "^1.1.0" } }, "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ=="], - "get-port-please": ["get-port-please@3.1.2", "", {}, "sha512-Gxc29eLs1fbn6LQ4jSU4vXjlwyZhF5HsGuMAa7gqBP4Rw4yxxltyDUuF5MBclFzDTXO+ACchGQoeela4DSfzdQ=="], - "get-proto": ["get-proto@1.0.1", "", { "dependencies": { "dunder-proto": "^1.0.1", "es-object-atoms": "^1.0.0" } }, "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g=="], "get-stdin": ["get-stdin@9.0.0", "", {}, "sha512-dVKBjfWisLAicarI2Sf+JuBE/DghV4UzNAVe9yhEJuzeREd3JhOTE9cUaJTeSa77fsbQUK3pcOpJfM59+VKZaA=="], @@ -714,12 +680,8 @@ "log-update": ["log-update@6.1.0", "", { "dependencies": { "ansi-escapes": "^7.0.0", "cli-cursor": "^5.0.0", "slice-ansi": "^7.1.0", "strip-ansi": "^7.1.0", "wrap-ansi": "^9.0.0" } }, "sha512-9ie8ItPR6tjY5uYJh8K/Zrv/RMZ5VOlOWvtZdEHYSTFKZfIBPQa9tOAEeAWhd+AnIneLJ22w5fjOYtoutpWq5w=="], - "loupe": ["loupe@3.1.3", "", {}, "sha512-kkIp7XSkP78ZxJEsSxW3712C6teJVoeHHwgo9zJ380de7IYyJ2ISlxojcH2pC5OFLewESmnRi/+XCDIEEVyoug=="], - "lru-cache": ["lru-cache@10.4.3", "", {}, "sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ=="], - "magic-string": ["magic-string@0.30.17", "", { "dependencies": { "@jridgewell/sourcemap-codec": "^1.5.0" } }, "sha512-sNPKHvyjVf7gyjwS4xGTaW/mCnF8wnjtifKBEhxfZ7E/S8tQ0rssrwGNn6q8JH/ohItJfSQp9mBtQYuTlH5QnA=="], - "math-intrinsics": ["math-intrinsics@1.1.0", "", {}, "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g=="], "merge-stream": ["merge-stream@2.0.0", "", {}, "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w=="], @@ -756,7 +718,7 @@ "natural-orderby": ["natural-orderby@5.0.0", "", {}, "sha512-kKHJhxwpR/Okycz4HhQKKlhWe4ASEfPgkSWNmKFHd7+ezuQlxkA5cM3+XkBPvm1gmHen3w53qsYAv+8GwRrBlg=="], - "node-fetch-native": ["node-fetch-native@1.6.4", "", {}, "sha512-IhOigYzAKHd244OC0JIMIUrjzctirCmPkaIfhDeGcEETWof5zKYUW7e7MYvChGWh/4CJeXEgsRyGzuF334rOOQ=="], + "node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], "node-releases": ["node-releases@2.0.19", "", {}, "sha512-xxOWJsBKtzAq7DY0J+DTzuz58K8e7sJbdgwkbMWQe8UYB6ekmsQ45q0M/tJDsGaZmbC+l7n57UV8Hl5tHxO9uw=="], @@ -778,8 +740,6 @@ "object.values": ["object.values@1.2.1", "", { "dependencies": { "call-bind": "^1.0.8", "call-bound": "^1.0.3", "define-properties": "^1.2.1", "es-object-atoms": "^1.0.0" } }, "sha512-gXah6aZrcUxjWg2zR2MwouP2eHlCBzdV4pygudehaKXSGW4v2AsRQUK+lwwXhii6KFZcunEnmSUoYp5CXibxtA=="], - "ofetch": ["ofetch@1.4.1", "", { "dependencies": { "destr": "^2.0.3", "node-fetch-native": "^1.6.4", "ufo": "^1.5.4" } }, "sha512-QZj2DfGplQAr2oj9KzceK9Hwz6Whxazmn85yYeVuS3u9XTMOGMRx0kO95MQ+vLsj/S/NwBDMMLU5hpxvI6Tklw=="], - "ohash": ["ohash@1.1.4", "", {}, "sha512-FlDryZAahJmEF3VR3w1KogSEdWX3WhA5GPakFx4J81kEAiHyLMpdLLElS8n8dfNadMgAne/MywcvmogzscVt4g=="], "onetime": ["onetime@6.0.0", "", { "dependencies": { "mimic-fn": "^4.0.0" } }, "sha512-1FlR+gjXK7X+AsAHso35MnyN5KqGwJRi/31ft6x0M194ht7S+rWAvd7PHss9xSKMzE0asv1pyIHaJYq+BbacAQ=="], @@ -812,9 +772,7 @@ "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=="], - "pathe": ["pathe@2.0.3", "", {}, "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w=="], - - "pathval": ["pathval@2.0.0", "", {}, "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA=="], + "pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], "perfect-debounce": ["perfect-debounce@1.0.0", "", {}, "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA=="], @@ -842,6 +800,8 @@ "prettier-linter-helpers": ["prettier-linter-helpers@1.0.0", "", { "dependencies": { "fast-diff": "^1.1.2" } }, "sha512-GbK2cP9nraSSUF9N2XwUwqfzlAFlMNYYl+ShE/V+H8a9uNl/oUqB1w2EL54Jh0OlyRSd8RfWYJ3coVS4TROP2w=="], + "prettier-plugin-packagejson": ["prettier-plugin-packagejson@2.5.10", "", { "dependencies": { "sort-package-json": "2.15.1", "synckit": "0.9.2" }, "peerDependencies": { "prettier": ">= 1.16.0" }, "optionalPeers": ["prettier"] }, "sha512-LUxATI5YsImIVSaaLJlJ3aE6wTD+nvots18U3GuQMJpUyClChaZlQrqx3dBnbhF20OnKWZyx8EgyZypQtBDtgQ=="], + "pretty-ms": ["pretty-ms@9.2.0", "", { "dependencies": { "parse-ms": "^4.0.0" } }, "sha512-4yf0QO/sllf/1zbZWYnvWw3NxCQwLXKzIj0G849LSufP15BXKM0rbD2Z3wVnkMfjdn/CB0Dpp444gYAACdsplg=="], "prompts": ["prompts@2.4.2", "", { "dependencies": { "kleur": "^3.0.3", "sisteransi": "^1.0.5" } }, "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q=="], @@ -912,8 +872,6 @@ "side-channel-weakmap": ["side-channel-weakmap@1.0.2", "", { "dependencies": { "call-bound": "^1.0.2", "es-errors": "^1.3.0", "get-intrinsic": "^1.2.5", "object-inspect": "^1.13.3", "side-channel-map": "^1.0.1" } }, "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A=="], - "siginfo": ["siginfo@2.0.0", "", {}, "sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g=="], - "signal-exit": ["signal-exit@4.1.0", "", {}, "sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw=="], "simple-git-hooks": ["simple-git-hooks@2.11.1", "", { "bin": { "simple-git-hooks": "cli.js" } }, "sha512-tgqwPUMDcNDhuf1Xf6KTUsyeqGdgKMhzaH4PAZZuzguOgTl5uuyeYe/8mWgAr6IBxB5V06uqEf6Dy37gIWDtDg=="], @@ -926,7 +884,7 @@ "sort-object-keys": ["sort-object-keys@1.1.3", "", {}, "sha512-855pvK+VkU7PaKYPc+Jjnmt4EzejQHyhhF33q31qG8x7maDzkeFhAAThdCYay11CISO+qAMwjOBP+fPZe0IPyg=="], - "sort-package-json": ["sort-package-json@3.0.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "get-stdin": "^9.0.0", "git-hooks-list": "^3.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-vfZWx4DnFNB8R9Vg4Dnx21s20auNzWH15ZaCBfADAiyrCwemRmhWstTgvLjMek1DW3+MHcNaqkp86giCF24rMA=="], + "sort-package-json": ["sort-package-json@2.15.1", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.0", "get-stdin": "^9.0.0", "git-hooks-list": "^3.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.6.0", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.9" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-9x9+o8krTT2saA9liI4BljNjwAbvUnWf11Wq+i/iZt8nl2UGYnf3TH5uBydE7VALmP7AGwlfszuEeL8BDyb0YA=="], "source-map": ["source-map@0.8.0-beta.0", "", { "dependencies": { "whatwg-url": "^7.0.0" } }, "sha512-2ymg6oRBpebeZi9UUNsgQ89bhx01TcTkmNTGnNO88imTmbSgy4nfujrgVEFKWpMTEGA11EDkTt7mqObTPdigIA=="], @@ -940,11 +898,7 @@ "spdx-license-ids": ["spdx-license-ids@3.0.21", "", {}, "sha512-Bvg/8F5XephndSK3JffaRqdT+gyhfqIPwDHpX80tJrF8QQRYMo8sNMeaZ2Dp5+jhwKnUmIOyFFQfHRkjJm5nXg=="], - "srvx": ["srvx@0.1.4", "", { "dependencies": { "cookie-es": "^1.2.2" } }, "sha512-hHt1/s+3o4tOOjC2YCr7bwi4msAXYJYErVpz2w/FcvG3ODRV0GZsdHsBjeKqY46psZmRbItfPLMp2oP7JsZaow=="], - - "stackback": ["stackback@0.0.2", "", {}, "sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw=="], - - "std-env": ["std-env@3.8.0", "", {}, "sha512-Bc3YwwCB+OzldMxOXJIIvC6cPRWr/LxOp48CdQTOkPyk/t4JWWJbrilwBd7RJzKV8QW7tJkcgAmeuLLJugl5/w=="], + "srvx": ["srvx@0.2.5", "", { "dependencies": { "cookie-es": "^2.0.0" } }, "sha512-G63uf9Emf8PQPlWkBKFfcqTkVjwIF5Z8lfECidSiaAXrd19Pj6ijU676yRfYP3KShZY7KmLsfb4/unIOCtnWfA=="], "string-argv": ["string-argv@0.3.2", "", {}, "sha512-aqD2Q0144Z+/RqG52NeHEkZauTAUWJO8c6yTftGJKO3Tja5tUgIfmIl6kExvhtxSDP7fXB6DvzkfMpCd/F3G+Q=="], @@ -988,18 +942,10 @@ "thenify-all": ["thenify-all@1.6.0", "", { "dependencies": { "thenify": ">= 3.1.0 < 4" } }, "sha512-RNxQH/qI8/t3thXJDwcstUO4zeqo64+Uy/+sNVRBx4Xn2OX+OZ9oP+iJnNFqplFra2ZUVeKCSa2oVWi3T4uVmA=="], - "tinybench": ["tinybench@2.9.0", "", {}, "sha512-0+DUvqWMValLmha6lr4kD8iAMK1HzV0/aKnCtWb9v9641TnP/MFb7Pc2bxoxQjTXAErryXVgUOfv2YqNllqGeg=="], - "tinyexec": ["tinyexec@0.3.2", "", {}, "sha512-KQQR9yN7R5+OSwaK0XQoj22pwHoTlgYqmUscPYoknOoWCWfj/5/ABTMRi69FrKU5ffPVh5QcFikpWJI/P1ocHA=="], "tinyglobby": ["tinyglobby@0.2.10", "", { "dependencies": { "fdir": "^6.4.2", "picomatch": "^4.0.2" } }, "sha512-Zc+8eJlFMvgatPZTl6A9L/yht8QqdmUNtURHaKZLmKBE12hNPSrqNkUp2cs3M/UKmNVVAMFQYSjYIVHDjW5zew=="], - "tinypool": ["tinypool@1.0.2", "", {}, "sha512-al6n+QEANGFOMf/dmUMsuS5/r9B06uwlyNjZZql/zv8J7ybHCgoihBNORZCY2mzUuAnomQa2JdhyHKzZxPCrFA=="], - - "tinyrainbow": ["tinyrainbow@2.0.0", "", {}, "sha512-op4nsTR47R6p0vMUUoYl/a+ljLFVtlfaXkLQmqfLR1qHma1h/ysYk4hEXZ880bf2CYgTskvTa/e196Vd5dDQXw=="], - - "tinyspy": ["tinyspy@3.0.2", "", {}, "sha512-n1cw8k1k0x4pgA2+9XrOkFydTerNcJ1zWCO5Nn9scWHTD+5tp8dghT2x1uduQePZTZgd3Tupf+x9BxJjeJi77Q=="], - "to-regex-range": ["to-regex-range@5.0.1", "", { "dependencies": { "is-number": "^7.0.0" } }, "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ=="], "tr46": ["tr46@1.0.1", "", { "dependencies": { "punycode": "^2.1.0" } }, "sha512-dTpowEjclQ7Kgx5SdBkqRzVhERQXov8/l9Ft9dVM9fmg0W0KQSVaXX9T4i6twCPNtYiZM53lpSSUAwJbFPOHxA=="], @@ -1048,12 +994,6 @@ "validate-npm-package-name": ["validate-npm-package-name@6.0.0", "", {}, "sha512-d7KLgL1LD3U3fgnvWEY1cQXoO/q6EQ1BSz48Sa149V/5zVTAbgmZIpyI8TRi6U9/JNyeYLlTKsEMPtLC27RFUg=="], - "vite": ["vite@6.2.0", "", { "dependencies": { "esbuild": "^0.25.0", "postcss": "^8.5.3", "rollup": "^4.30.1" }, "optionalDependencies": { "fsevents": "~2.3.3" }, "peerDependencies": { "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "jiti": ">=1.21.0", "less": "*", "lightningcss": "^1.21.0", "sass": "*", "sass-embedded": "*", "stylus": "*", "sugarss": "*", "terser": "^5.16.0", "tsx": "^4.8.1", "yaml": "^2.4.2" }, "optionalPeers": ["@types/node", "jiti", "less", "lightningcss", "sass", "sass-embedded", "stylus", "sugarss", "terser", "tsx", "yaml"], "bin": { "vite": "bin/vite.js" } }, "sha512-7dPxoo+WsT/64rDcwoOjk76XHj+TqNTIvHKcuMQ1k4/SeHDaQt5GFAeLYzrimZrMpn/O6DtdI03WUjdxuPM0oQ=="], - - "vite-node": ["vite-node@3.0.8", "", { "dependencies": { "cac": "^6.7.14", "debug": "^4.4.0", "es-module-lexer": "^1.6.0", "pathe": "^2.0.3", "vite": "^5.0.0 || ^6.0.0" }, "bin": { "vite-node": "vite-node.mjs" } }, "sha512-6PhR4H9VGlcwXZ+KWCdMqbtG649xCPZqfI9j2PsK1FcXgEzro5bGHcVKFCTqPLaNKZES8Evqv4LwvZARsq5qlg=="], - - "vitest": ["vitest@3.0.8", "", { "dependencies": { "@vitest/expect": "3.0.8", "@vitest/mocker": "3.0.8", "@vitest/pretty-format": "^3.0.8", "@vitest/runner": "3.0.8", "@vitest/snapshot": "3.0.8", "@vitest/spy": "3.0.8", "@vitest/utils": "3.0.8", "chai": "^5.2.0", "debug": "^4.4.0", "expect-type": "^1.1.0", "magic-string": "^0.30.17", "pathe": "^2.0.3", "std-env": "^3.8.0", "tinybench": "^2.9.0", "tinyexec": "^0.3.2", "tinypool": "^1.0.2", "tinyrainbow": "^2.0.0", "vite": "^5.0.0 || ^6.0.0", "vite-node": "3.0.8", "why-is-node-running": "^2.3.0" }, "peerDependencies": { "@edge-runtime/vm": "*", "@types/debug": "^4.1.12", "@types/node": "^18.0.0 || ^20.0.0 || >=22.0.0", "@vitest/browser": "3.0.8", "@vitest/ui": "3.0.8", "happy-dom": "*", "jsdom": "*" }, "optionalPeers": ["@edge-runtime/vm", "@types/debug", "@types/node", "@vitest/browser", "@vitest/ui", "happy-dom", "jsdom"], "bin": { "vitest": "vitest.mjs" } }, "sha512-dfqAsNqRGUc8hB9OVR2P0w8PZPEckti2+5rdZip0WIz9WW0MnImJ8XiR61QhqLa92EQzKP2uPkzenKOAHyEIbA=="], - "wcwidth": ["wcwidth@1.0.1", "", { "dependencies": { "defaults": "^1.0.3" } }, "sha512-XHPEwS0q6TaxcvG85+8EYkbiCux2XtWG2mkc47Ng2A77BQu9+DqIOJldST4HgPkuea7dvKSj5VgX3P1d4rW8Tg=="], "webidl-conversions": ["webidl-conversions@4.0.2", "", {}, "sha512-YQ+BmxuTgd6UXZW3+ICGfyqRyHXVlD5GtQr5+qjiNW7bF0cqrzX500HVXPBOvgXb5YnzDd+h0zqyv61KUD7+Sg=="], @@ -1070,8 +1010,6 @@ "which-typed-array": ["which-typed-array@1.1.19", "", { "dependencies": { "available-typed-arrays": "^1.0.7", "call-bind": "^1.0.8", "call-bound": "^1.0.4", "for-each": "^0.3.5", "get-proto": "^1.0.1", "gopd": "^1.2.0", "has-tostringtag": "^1.0.2" } }, "sha512-rEvr90Bck4WZt9HHFC4DJMsjvu7x+r6bImz0/BrbWb7A2djJ8hnZMrWnHo9F8ssv0OMErasDhftrfROTyqSDrw=="], - "why-is-node-running": ["why-is-node-running@2.3.0", "", { "dependencies": { "siginfo": "^2.0.0", "stackback": "0.0.2" }, "bin": { "why-is-node-running": "cli.js" } }, "sha512-hUrmaWBdVDcxvYqnyh09zunKzROWjbZTiNy8dBEjkS7ehEDQibXJ7XvlmtbwuTclUiIyN+CyXQD4Vmko8fNm8w=="], - "word-wrap": ["word-wrap@1.2.5", "", {}, "sha512-BN22B5eaMMI9UMtjrGd5g5eCYPpCPDUy0FJXbYsaT5zYxjFOckS53SQDE3pWkVoWpHXVb3BrYcEN4Twa55B5cA=="], "wrap-ansi": ["wrap-ansi@9.0.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "string-width": "^7.0.0", "strip-ansi": "^7.1.0" } }, "sha512-G8ura3S+3Z2G+mkgNRq8dqaFZAuxfsxpBB8OCTGRTCtp+l/v9nbFNmCUP1BZMts3G1142MsZfn6eeUKrr4PD1Q=="], @@ -1130,8 +1068,6 @@ "aggregate-error/indent-string": ["indent-string@4.0.0", "", {}, "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg=="], - "c12/pathe": ["pathe@1.1.2", "", {}, "sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ=="], - "citty/consola": ["consola@3.3.3", "", {}, "sha512-Qil5KwghMzlqd51UXM0b6fyaGHtOC22scxrwrz4A2882LyUMwQjnvaedN1HAeXzphspQ6CpHkzMAWxBTUruDLg=="], "clean-regexp/escape-string-regexp": ["escape-string-regexp@1.0.5", "", {}, "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg=="], @@ -1142,6 +1078,8 @@ "cliui/wrap-ansi": ["wrap-ansi@7.0.0", "", { "dependencies": { "ansi-styles": "^4.0.0", "string-width": "^4.1.0", "strip-ansi": "^6.0.0" } }, "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q=="], + "eslint-plugin-package-json/sort-package-json": ["sort-package-json@3.0.0", "", { "dependencies": { "detect-indent": "^7.0.1", "detect-newline": "^4.0.1", "get-stdin": "^9.0.0", "git-hooks-list": "^3.0.0", "is-plain-obj": "^4.1.0", "semver": "^7.7.1", "sort-object-keys": "^1.1.3", "tinyglobby": "^0.2.12" }, "bin": { "sort-package-json": "cli.js" } }, "sha512-vfZWx4DnFNB8R9Vg4Dnx21s20auNzWH15ZaCBfADAiyrCwemRmhWstTgvLjMek1DW3+MHcNaqkp86giCF24rMA=="], + "eslint-plugin-perfectionist/@typescript-eslint/types": ["@typescript-eslint/types@8.24.1", "", {}, "sha512-9kqJ+2DkUXiuhoiYIUvIYjGcwle8pcPpdlfkemGvTObzgmYfJ5d0Qm6jwb4NBXP9W1I5tss0VIAnWFumz3mC5A=="], "eslint-plugin-perfectionist/@typescript-eslint/utils": ["@typescript-eslint/utils@8.24.1", "", { "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", "@typescript-eslint/scope-manager": "8.24.1", "@typescript-eslint/types": "8.24.1", "@typescript-eslint/typescript-estree": "8.24.1" }, "peerDependencies": { "eslint": "^8.57.0 || ^9.0.0", "typescript": ">=4.8.4 <5.8.0" } }, "sha512-OOcg3PMMQx9EXspId5iktsI3eMaXVwlhC8BvNnX6B5w9a4dVgpkQZuU8Hy67TolKcl+iFWq0XX+jbDGN4xWxjQ=="], @@ -1154,8 +1092,6 @@ "fs-minipass/minipass": ["minipass@3.3.6", "", { "dependencies": { "yallist": "^4.0.0" } }, "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw=="], - "giget/node-fetch-native": ["node-fetch-native@1.6.6", "", {}, "sha512-8Mc2HhqPdlIfedsuZoc3yioPuzp6b+L5jRCRY1QzuWZh2EGJVQrGppC6V6cF0bLdbW0+O2YpqCA25aF/1lvipQ=="], - "giget/pathe": ["pathe@2.0.2", "", {}, "sha512-15Ztpk+nov8DR524R4BF7uEuzESgzUEAV4Ah7CUMNGXdE5ELuvxElxGXndBl32vMSsWa1jpNf22Z+Er3sKwq+w=="], "glob/minimatch": ["minimatch@9.0.5", "", { "dependencies": { "brace-expansion": "^2.0.1" } }, "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow=="], @@ -1166,8 +1102,6 @@ "jsonc-eslint-parser/espree": ["espree@9.6.1", "", { "dependencies": { "acorn": "^8.9.0", "acorn-jsx": "^5.3.2", "eslint-visitor-keys": "^3.4.1" } }, "sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ=="], - "knip/zod": ["zod@3.24.1", "", {}, "sha512-muH7gBL9sI1nciMZV67X5fTKKBLtwpZ5VBp1vsOQzj1MhrBZ4wlVCm3gedKZWLp0Oyel8sIGfeiz54Su+OVT+A=="], - "lint-staged/chalk": ["chalk@5.4.1", "", {}, "sha512-zgVZuo2WcZgfUEmsn6eO3kINexW8RAE4maiQ8QNs8CtpPCSyMiYsULR3HQYkm3w8FIA3SberyMJMSldGsW+U3w=="], "log-update/slice-ansi": ["slice-ansi@7.1.0", "", { "dependencies": { "ansi-styles": "^6.2.1", "is-fullwidth-code-point": "^5.0.0" } }, "sha512-bSiSngZ/jWeX93BqeIAbImyTbEihizcwNjFoRUIY/T1wWQsfsm2Vw1agPKylXvQTU7iASGdHhyqRlqQzfz+Htg=="], @@ -1246,6 +1180,12 @@ "cliui/string-width/is-fullwidth-code-point": ["is-fullwidth-code-point@3.0.0", "", {}, "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg=="], + "eslint-plugin-package-json/sort-package-json/detect-indent": ["detect-indent@7.0.1", "", {}, "sha512-Mc7QhQ8s+cLrnUfU/Ji94vG/r8M26m8f++vyres4ZoojaRDpZ1eSIh/EpzLNwlWuvzSZ3UbDFspjFvTDXe6e/g=="], + + "eslint-plugin-package-json/sort-package-json/detect-newline": ["detect-newline@4.0.1", "", {}, "sha512-qE3Veg1YXzGHQhlA6jzebZN2qVf6NX+A7m7qlhCGG30dJixrAQhYOsJjsnBjJkCSmuOPpCk30145fr8FV0bzog=="], + + "eslint-plugin-package-json/sort-package-json/tinyglobby": ["tinyglobby@0.2.12", "", { "dependencies": { "fdir": "^6.4.3", "picomatch": "^4.0.2" } }, "sha512-qkf4trmKSIiMTs/E63cxH+ojC2unam7rJ0WrauAzpT3ECNTxGRMlaXxVbfxMUC/w0LaYk6jQ4y/nGR9uBO3tww=="], + "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/scope-manager": ["@typescript-eslint/scope-manager@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1" } }, "sha512-OdQr6BNBzwRjNEXMQyaGyZzgg7wzjYKfX2ZBV3E04hUCBDv3GQCHiz9RpqdUIiVrMgJGkXm3tcEh4vFSHreS2Q=="], "eslint-plugin-perfectionist/@typescript-eslint/utils/@typescript-eslint/typescript-estree": ["@typescript-eslint/typescript-estree@8.24.1", "", { "dependencies": { "@typescript-eslint/types": "8.24.1", "@typescript-eslint/visitor-keys": "8.24.1", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", "minimatch": "^9.0.4", "semver": "^7.6.0", "ts-api-utils": "^2.0.1" }, "peerDependencies": { "typescript": ">=4.8.4 <5.8.0" } }, "sha512-UPyy4MJ/0RE648DSKQe9g0VDSehPINiejjA6ElqnFaFIhI6ZEiZAkUI0D5MCk0bQcTf/LVqZStvQ6K4lPn/BRg=="], diff --git a/eslint.config.js b/eslint.config.js deleted file mode 100644 index f3e96aeb..00000000 --- a/eslint.config.js +++ /dev/null @@ -1,3 +0,0 @@ -import config from "@echristian/eslint-config" - -export default config() diff --git a/eslint.config.ts b/eslint.config.ts new file mode 100644 index 00000000..c9f79bea --- /dev/null +++ b/eslint.config.ts @@ -0,0 +1,7 @@ +import config from "@echristian/eslint-config" + +export default config({ + prettier: { + plugins: ["prettier-plugin-packagejson"], + }, +}) diff --git a/knip.config.ts b/knip.config.ts deleted file mode 100644 index f9ac749d..00000000 --- a/knip.config.ts +++ /dev/null @@ -1,6 +0,0 @@ -import type { KnipConfig } from "knip" - -export default { - entry: ["./src/main.ts"], - ignore: ["scripts/*.ts"], -} satisfies KnipConfig diff --git a/package.json b/package.json index fd046d35..f9d4f39b 100644 --- a/package.json +++ b/package.json @@ -29,8 +29,7 @@ "prepack": "bun run build", "prepare": "simple-git-hooks", "release": "bumpp && bun publish --access public", - "start": "NODE_ENV=production bun run ./src/main.ts", - "test": "vitest" + "start": "NODE_ENV=production bun run ./src/main.ts" }, "simple-git-hooks": { "pre-commit": "bunx lint-staged" @@ -42,24 +41,20 @@ "citty": "^0.1.6", "consola": "^3.4.0", "fetch-event-stream": "^0.1.5", - "get-port-please": "^3.1.2", "hono": "^4.7.4", - "ofetch": "^1.4.1", - "pathe": "^2.0.3", - "srvx": "^0.1.4", - "zod": "^3.24.2" + "srvx": "^0.2.5" }, "devDependencies": { "@echristian/eslint-config": "^0.0.32", "@types/bun": "^1.2.4", "bumpp": "^10.0.3", "eslint": "^9.22.0", + "jiti": "^2.4.2", "knip": "^5.45.0", "lint-staged": "^15.4.3", + "prettier-plugin-packagejson": "^2.5.10", "simple-git-hooks": "^2.11.1", - "tinyexec": "^0.3.2", "tsup": "^8.4.0", - "typescript": "^5.8.2", - "vitest": "^3.0.8" + "typescript": "^5.8.2" } } diff --git a/src/lib/api-config.ts b/src/lib/api-config.ts new file mode 100644 index 00000000..3a13370c --- /dev/null +++ b/src/lib/api-config.ts @@ -0,0 +1,40 @@ +import type { State } from "./state" + +export const standardHeaders = () => ({ + "content-type": "application/json", + accept: "application/json", +}) + +export const COPILOT_API_BASE_URL = "https://api.individual.githubcopilot.com" +export const copilotHeaders = (state: State) => ({ + Authorization: `Bearer ${state.copilotToken}`, + "content-type": standardHeaders()["content-type"], + "copilot-integration-id": "vscode-chat", + "editor-version": `vscode/${state.vsCodeVersion}`, + "editor-plugin-version": "copilot-chat/0.24.1", + "openai-intent": "conversation-panel", + "x-github-api-version": "2024-12-15", + "x-request-id": globalThis.crypto.randomUUID(), + "x-vscode-user-agent-library-version": "electron-fetch", +}) + +export const GITHUB_API_BASE_URL = "https://api.github.com" +export const githubHeaders = (state: State) => ({ + ...standardHeaders(), + authorization: `token ${state.githubToken}`, + "editor-version": `vscode/${state.vsCodeVersion}`, + "editor-plugin-version": "copilot-chat/0.24.1", + "user-agent": "GitHubCopilotChat/0.24.1", + "x-github-api-version": "2024-12-15", + "x-vscode-user-agent-library-version": "electron-fetch", +}) + +export const GITHUB_BASE_URL = "https://github.com" +export const GITHUB_CLIENT_ID = "01ab8ac9400c4e429b23" +export const GITHUB_APP_SCOPES = [ + "read:org", + "read:user", + "repo", + "user:email", + "workflow", +].join(" ") diff --git a/src/lib/constants.ts b/src/lib/constants.ts deleted file mode 100644 index bf749d96..00000000 --- a/src/lib/constants.ts +++ /dev/null @@ -1,31 +0,0 @@ -// VSCode client ID -const GITHUB_CLIENT_ID = "01ab8ac9400c4e429b23" -const GITHUB_OAUTH_SCOPES = [ - "read:org", - "read:user", - "repo", - "user:email", - "workflow", -].join(" ") - -export const ENV = { - GITHUB_CLIENT_ID, - GITHUB_OAUTH_SCOPES, -} - -export const COPILOT_API_CONFIG = { - baseURL: "https://api.individual.githubcopilot.com", - headers: { - "copilot-integration-id": "vscode-chat", - "copilot-vision-request": "true", - "editor-version": "vscode/1.98.0-insider", - }, -} as const - -export const GITHUB_API_CONFIG = { - baseURL: "https://api.github.com", -} as const - -export const GITHUB_WEB_API_CONFIG = { - baseURL: "https://github.com", -} as const diff --git a/src/lib/forward-error.ts b/src/lib/forward-error.ts new file mode 100644 index 00000000..c0a1e02c --- /dev/null +++ b/src/lib/forward-error.ts @@ -0,0 +1,33 @@ +import type { Context } from "hono" +import type { ContentfulStatusCode } from "hono/utils/http-status" + +import consola from "consola" + +import { HTTPError } from "./http-error" + +export async function forwardError(c: Context, error: unknown) { + consola.error("Error occurred:", error) + + if (error instanceof HTTPError) { + const errorText = await error.response.text() + return c.json( + { + error: { + message: errorText, + type: "error", + }, + }, + error.response.status as ContentfulStatusCode, + ) + } + + return c.json( + { + error: { + message: (error as Error).message, + type: "error", + }, + }, + 500, + ) +} diff --git a/src/lib/http-error.ts b/src/lib/http-error.ts new file mode 100644 index 00000000..352d3c62 --- /dev/null +++ b/src/lib/http-error.ts @@ -0,0 +1,8 @@ +export class HTTPError extends Error { + response: Response + + constructor(message: string, response: Response) { + super(message) + this.response = response + } +} diff --git a/src/lib/initialization.ts b/src/lib/initialization.ts deleted file mode 100644 index c421b0f2..00000000 --- a/src/lib/initialization.ts +++ /dev/null @@ -1,75 +0,0 @@ -import consola from "consola" -import fs from "node:fs/promises" -import { FetchError } from "ofetch" - -import { PATHS } from "~/lib/paths" -import { tokenService } from "~/lib/token" -import { getGitHubUser } from "~/services/github/get-user/service" - -import { getModels } from "../services/copilot/get-models/service" -import { getGitHubToken } from "../services/github/get-token/service" - -// Extract to individual functions for each initialization step -async function initializeAppDirectory(): Promise { - await fs.mkdir(PATHS.APP_DIR, { recursive: true }) - await fs.writeFile(PATHS.GITHUB_TOKEN_PATH, "", { flag: "a" }) -} - -async function initializeGithubAuthentication(): Promise { - const githubToken = await tokenService.getGithubToken() - - try { - if (githubToken) { - // Set token in the service so github fetcher can use it - await tokenService.setGithubToken(githubToken) - await logUser() - } else { - throw new Error("No GitHub token available") - } - } catch (error) { - if (error instanceof FetchError && error.statusCode !== 401) { - consola.error("Authentication error:", { - error, - request: error.request, - options: error.options, - response: error.response, - data: error.response?._data as Record, - }) - throw error - } - - consola.info("Not logged in, getting new access token") - const newToken = await initializeGithubToken() - await tokenService.setGithubToken(newToken) - await logUser() - } -} - -async function initializeCopilotToken(): Promise { - await tokenService.initCopilotToken() -} - -async function logModelInformation(): Promise { - const models = await getModels() - consola.info( - `Available models: \n${models.data.map((model) => `- ${model.id}`).join("\n")}`, - ) -} - -async function initializeGithubToken() { - consola.start("Getting GitHub device code") - const tokenResponse = await getGitHubToken() - return tokenResponse.access_token -} - -async function logUser() { - const user = await getGitHubUser() - consola.info(`Logged in as ${JSON.stringify(user.login)}\n`) -} - -export async function initializeApp() { - await initializeAppDirectory() - await initializeGithubAuthentication() - await initializeCopilotToken() - await logModelInformation() -} diff --git a/src/lib/logger.ts b/src/lib/logger.ts deleted file mode 100644 index a6eaf8ba..00000000 --- a/src/lib/logger.ts +++ /dev/null @@ -1,100 +0,0 @@ -import consola from "consola" -import fs from "node:fs/promises" -import path from "pathe" - -export interface LoggerOptions { - enabled: boolean - filePath?: string -} - -export const logger = { - options: { - enabled: false, - filePath: undefined, - } as LoggerOptions, - - async initialize(filePath?: string): Promise { - if (!filePath) { - this.options.enabled = false - return - } - - try { - // Ensure the directory exists - await fs.mkdir(path.dirname(filePath), { recursive: true }) - - // Initialize the log file with a header - const timestamp = new Date().toISOString() - await fs.writeFile( - filePath, - `# API Request/Response Log\n# Started: ${timestamp}\n\n`, - { flag: "w" }, - ) - - this.options.enabled = true - this.options.filePath = filePath - consola.info(`Logging enabled to: ${filePath}`) - } catch (error) { - consola.error(`Failed to initialize log file`, error) - this.options.enabled = false - } - }, - - async logRequest( - endpoint: string, - method: string, - payload: unknown, - headers?: Record, - ): Promise { - if (!this.options.enabled || !this.options.filePath) return - - const timestamp = new Date().toISOString() - const logEntry = [ - `## Request - ${timestamp}`, - `Endpoint: ${endpoint}`, - `Method: ${method}`, - headers ? - `Headers:\n\`\`\`json\n${JSON.stringify(headers, null, 2)}\n\`\`\`` - : "", - `Payload:`, - `\`\`\`json`, - JSON.stringify(payload, null, 2), - `\`\`\``, - `\n`, - ].join("\n") - - try { - await fs.appendFile(this.options.filePath, logEntry) - } catch (error) { - consola.error(`Failed to write to log file`, error) - } - }, - - async logResponse( - endpoint: string, - response: unknown, - headers?: Record, - ): Promise { - if (!this.options.enabled || !this.options.filePath) return - - const timestamp = new Date().toISOString() - const logEntry = [ - `## Response - ${timestamp}`, - `Endpoint: ${endpoint}`, - headers ? - `Headers:\n\`\`\`json\n${JSON.stringify(headers, null, 2)}\n\`\`\`` - : "", - `Response:`, - `\`\`\`json`, - JSON.stringify(response, null, 2), - `\`\`\``, - `\n`, - ].join("\n") - - try { - await fs.appendFile(this.options.filePath, logEntry) - } catch (error) { - consola.error(`Failed to write to log file`, error) - } - }, -} diff --git a/src/lib/models.ts b/src/lib/models.ts index 2f0ada3f..d6a3516b 100644 --- a/src/lib/models.ts +++ b/src/lib/models.ts @@ -1,13 +1,14 @@ -import type { GetModelsResponse } from "~/services/copilot/get-models/types" +import consola from "consola" -export const modelsCache = { - _models: null as GetModelsResponse | null, +import { getModels } from "~/services/copilot/get-models" - setModels(models: GetModelsResponse) { - this._models = models - }, +import { state } from "./state" - getModels() { - return this._models - }, +export async function cacheModels(): Promise { + const models = await getModels() + state.models = models + + consola.info( + `Available models: \n${models.data.map((model) => `- ${model.id}`).join("\n")}`, + ) } diff --git a/src/lib/paths.ts b/src/lib/paths.ts index 83bd25fc..8d0a9f02 100644 --- a/src/lib/paths.ts +++ b/src/lib/paths.ts @@ -1,15 +1,26 @@ +import fs from "node:fs/promises" import os from "node:os" -import path from "pathe" +import path from "node:path" const APP_DIR = path.join(os.homedir(), ".local", "share", "copilot-api") const GITHUB_TOKEN_PATH = path.join(APP_DIR, "github_token") -const LOG_PATH = path.join(APP_DIR, "logs") -const LOG_FILE = path.join(LOG_PATH, "app.log") export const PATHS = { APP_DIR, GITHUB_TOKEN_PATH, - LOG_PATH, - LOG_FILE, +} + +export async function ensurePaths(): Promise { + await fs.mkdir(PATHS.APP_DIR, { recursive: true }) + await ensureFile(PATHS.GITHUB_TOKEN_PATH) +} + +async function ensureFile(filePath: string): Promise { + try { + await fs.access(filePath, fs.constants.W_OK) + } catch { + await fs.writeFile(filePath, "") + await fs.chmod(filePath, 0o600) + } } diff --git a/src/lib/port.ts b/src/lib/port.ts deleted file mode 100644 index 8a5262ec..00000000 --- a/src/lib/port.ts +++ /dev/null @@ -1,19 +0,0 @@ -import consola from "consola" -import { getPort } from "get-port-please" - -export async function initializePort(requestedPort?: number): Promise { - const port = await getPort({ - name: "copilot-api", - port: requestedPort, - portRange: [4142, 4200], - random: false, - }) - - if (port !== requestedPort) { - consola.warn( - `Default port ${requestedPort} is already in use. Using port ${port} instead.`, - ) - } - - return port -} diff --git a/src/lib/sleep.ts b/src/lib/sleep.ts new file mode 100644 index 00000000..35b2fd53 --- /dev/null +++ b/src/lib/sleep.ts @@ -0,0 +1,4 @@ +export const sleep = (ms: number) => + new Promise((resolve) => { + setTimeout(resolve, ms) + }) diff --git a/src/lib/state.ts b/src/lib/state.ts new file mode 100644 index 00000000..aa718e9b --- /dev/null +++ b/src/lib/state.ts @@ -0,0 +1,11 @@ +import type { ModelsResponse } from "~/services/copilot/get-models" + +export interface State { + githubToken?: string + copilotToken?: string + + models?: ModelsResponse + vsCodeVersion?: string +} + +export const state: State = {} diff --git a/src/lib/token.ts b/src/lib/token.ts index c3c34b09..7fbad77a 100644 --- a/src/lib/token.ts +++ b/src/lib/token.ts @@ -2,61 +2,73 @@ import consola from "consola" import fs from "node:fs/promises" import { PATHS } from "~/lib/paths" -import { getCopilotToken } from "~/services/copilot/get-token/copilot-token" - -// Simple token manager with basic encapsulation -export const tokenService = { - // Private token storage - _tokens: { - github: undefined as string | undefined, - copilot: undefined as string | undefined, - }, - - // Get GitHub token - async getGithubToken(): Promise { - if (!this._tokens.github) { - try { - this._tokens.github = await fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8") - } catch (error) { - consola.warn("Failed to load GitHub token", error) - } +import { getCopilotToken } from "~/services/github/get-copilot-token" +import { getDeviceCode } from "~/services/github/get-device-code" +import { getGitHubUser } from "~/services/github/get-user" +import { pollAccessToken } from "~/services/github/poll-access-token" + +import { HTTPError } from "./http-error" +import { state } from "./state" + +const readGithubToken = () => fs.readFile(PATHS.GITHUB_TOKEN_PATH, "utf8") + +const writeGithubToken = (token: string) => + fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token) + +export const setupCopilotToken = async () => { + const { token, refresh_in } = await getCopilotToken() + state.copilotToken = token + + const refreshInterval = (refresh_in - 60) * 1000 + + setInterval(async () => { + consola.start("Refreshing Copilot token") + try { + const { token } = await getCopilotToken() + state.copilotToken = token + } catch (error) { + consola.error("Failed to refresh Copilot token:", error) + throw error + } + }, refreshInterval) +} + +export async function setupGitHubToken(): Promise { + try { + const githubToken = await readGithubToken() + + if (githubToken) { + state.githubToken = githubToken + await logUser() + + return } - return this._tokens.github - }, - - // Set GitHub token - async setGithubToken(token: string): Promise { - this._tokens.github = token - await fs.writeFile(PATHS.GITHUB_TOKEN_PATH, token) - }, - - // Get Copilot token - getCopilotToken(): string | undefined { - return this._tokens.copilot - }, - - // Set Copilot token - setCopilotToken(token: string): void { - this._tokens.copilot = token - }, - - // Initialize Copilot token with auto-refresh - async initCopilotToken(): Promise { - const { token, refresh_in } = await getCopilotToken() - this.setCopilotToken(token) - - // Set up refresh timer - const refreshInterval = (refresh_in - 60) * 1000 - setInterval(async () => { - consola.start("Refreshing Copilot token") - try { - const { token: newToken } = await getCopilotToken() - this.setCopilotToken(newToken) - consola.success("Copilot token refreshed") - } catch (error) { - consola.error("Failed to refresh Copilot token:", error) - } - }, refreshInterval) - }, + consola.info("Not logged in, getting new access token") + const response = await getDeviceCode() + consola.debug("Device code response:", response) + + consola.info( + `Please enter the code "${response.user_code}" in ${response.verification_uri}`, + ) + + const token = await pollAccessToken(response) + await writeGithubToken(token) + state.githubToken = token + + await logUser() + } catch (error) { + if (error instanceof HTTPError) { + consola.error("Failed to get GitHub token:", await error.response.json()) + throw error + } + + consola.error("Failed to get GitHub token:", error) + throw error + } +} + +async function logUser() { + const user = await getGitHubUser() + consola.info(`Logged in as ${user.login}`) } diff --git a/src/lib/vscode-version.ts b/src/lib/vscode-version.ts new file mode 100644 index 00000000..5b330113 --- /dev/null +++ b/src/lib/vscode-version.ts @@ -0,0 +1,12 @@ +import consola from "consola" + +import { getVSCodeVersion } from "~/services/get-vscode-version" + +import { state } from "./state" + +export const cacheVSCodeVersion = async () => { + const response = await getVSCodeVersion() + state.vsCodeVersion = response + + consola.info(`Using VSCode version: ${response}`) +} diff --git a/src/main.ts b/src/main.ts index 99cca2d9..bc7568dd 100644 --- a/src/main.ts +++ b/src/main.ts @@ -4,32 +4,35 @@ import { defineCommand, runMain } from "citty" import consola from "consola" import { serve, type ServerHandler } from "srvx" -import { initializeApp } from "./lib/initialization" -import { logger } from "./lib/logger" -import { initializePort } from "./lib/port" +import { cacheModels } from "./lib/models" +import { ensurePaths } from "./lib/paths" +import { setupCopilotToken, setupGitHubToken } from "./lib/token" +import { cacheVSCodeVersion } from "./lib/vscode-version" import { server } from "./server" -export async function runServer(options: { +interface RunServerOptions { port: number verbose: boolean - logFile?: string -}): Promise { +} + +export async function runServer(options: RunServerOptions): Promise { if (options.verbose) { consola.level = 5 consola.info("Verbose logging enabled") } - const port = await initializePort(options.port) - await logger.initialize(options.logFile) - - await initializeApp() + await ensurePaths() + await cacheVSCodeVersion() + await setupGitHubToken() + await setupCopilotToken() + await cacheModels() - const serverUrl = `http://localhost:${port}` + const serverUrl = `http://localhost:${options.port}` consola.box(`Server started at ${serverUrl}`) serve({ fetch: server.fetch as ServerHandler, - port, + port: options.port, }) } @@ -47,10 +50,6 @@ const main = defineCommand({ default: false, description: "Enable verbose logging", }, - "log-file": { - type: "string", - description: "File to log request/response details", - }, }, run({ args }) { const port = Number.parseInt(args.port, 10) @@ -58,7 +57,6 @@ const main = defineCommand({ return runServer({ port, verbose: args.verbose, - logFile: args["log-file"], }) }, }) diff --git a/src/routes/chat-completions/handler.ts b/src/routes/chat-completions/handler.ts index 57f447eb..d0ecb4ae 100644 --- a/src/routes/chat-completions/handler.ts +++ b/src/routes/chat-completions/handler.ts @@ -1,107 +1,21 @@ import type { Context } from "hono" -import consola from "consola" import { streamSSE, type SSEMessage } from "hono/streaming" -import type { ChatCompletionsPayload } from "~/services/copilot/chat-completions/types" -import type { ChatCompletionChunk } from "~/services/copilot/chat-completions/types-streaming" +import type { + ChatCompletionResponse, + ChatCompletionsPayload, +} from "~/services/copilot/chat-completions/types" import { isNullish } from "~/lib/is-nullish" -import { logger } from "~/lib/logger" -import { modelsCache } from "~/lib/models" -import { chatCompletions } from "~/services/copilot/chat-completions/service" -import { chatCompletionsStream } from "~/services/copilot/chat-completions/service-streaming" - -function createCondensedStreamingResponse( - finalChunk: ChatCompletionChunk, - collectedContent: string, -) { - return { - id: finalChunk.id, - model: finalChunk.model, - created: finalChunk.created, - object: "chat.completion", - system_fingerprint: finalChunk.system_fingerprint, - usage: finalChunk.usage, - choices: [ - { - index: 0, - finish_reason: finalChunk.choices[0].finish_reason, - message: { - role: "assistant", - content: collectedContent, - }, - content_filter_results: finalChunk.choices[0].content_filter_results, - }, - ], - } -} - -function handleStreaming(c: Context, payload: ChatCompletionsPayload) { - return streamSSE(c, async (stream) => { - const response = await chatCompletionsStream(payload) - - // For collecting the complete streaming response - let collectedContent = "" - let finalChunk: ChatCompletionChunk | null = null - - for await (const chunk of response) { - await stream.writeSSE(chunk as SSEMessage) - - if (!logger.options.enabled) continue - - // Check if chunk data is "DONE" or not a valid JSON string - if (!chunk.data || chunk.data === "[DONE]") { - continue // Skip processing this chunk for logging - } - - try { - const data = JSON.parse(chunk.data) as ChatCompletionChunk - - // Keep track of the latest chunk for metadata - finalChunk = data - - // Accumulate content from each delta - if (typeof data.choices[0].delta.content === "string") { - collectedContent += data.choices[0].delta.content - } - } catch (error) { - // Handle JSON parsing errors gracefully - consola.error(`Error parsing SSE chunk data`, error) - // Continue processing other chunks - } - } - - // After streaming completes, log the condensed response - if (finalChunk) { - const condensedResponse = createCondensedStreamingResponse( - finalChunk, - collectedContent, - ) - - await logger.logResponse("/chat/completions", condensedResponse, {}) - } - }) -} - -async function handleNonStreaming(c: Context, payload: ChatCompletionsPayload) { - const response = await chatCompletions(payload) - - // Get response headers if any - const responseHeaders = {} // Empty placeholder for response headers - - // Log the non-streaming response with headers - await logger.logResponse("/chat/completions", response, responseHeaders) - - return c.json(response) -} +import { state } from "~/lib/state" +import { createChatCompletions } from "~/services/copilot/create-chat-completions" export async function handleCompletion(c: Context) { - const models = modelsCache.getModels() let payload = await c.req.json() if (isNullish(payload.max_tokens)) { - const selectedModel = models?.data.find( + const selectedModel = state.models?.data.find( (model) => model.id === payload.model, ) @@ -111,15 +25,19 @@ export async function handleCompletion(c: Context) { } } - // Convert request headers to a regular object from Headers - const requestHeaders = c.req.header() - - // Log the request at the beginning for both streaming and non-streaming cases - await logger.logRequest("/chat/completions", "POST", payload, requestHeaders) + const response = await createChatCompletions(payload) - if (payload.stream) { - return handleStreaming(c, payload) + if (isNonStreaming(response)) { + return c.json(response) } - return handleNonStreaming(c, payload) + return streamSSE(c, async (stream) => { + for await (const chunk of response) { + await stream.writeSSE(chunk as SSEMessage) + } + }) } + +const isNonStreaming = ( + response: Awaited>, +): response is ChatCompletionResponse => Object.hasOwn(response, "choices") diff --git a/src/routes/chat-completions/route.ts b/src/routes/chat-completions/route.ts index 64aca155..c55a3a7b 100644 --- a/src/routes/chat-completions/route.ts +++ b/src/routes/chat-completions/route.ts @@ -1,11 +1,7 @@ -import type { BlankEnv, BlankInput } from "hono/types" -import type { ContentfulStatusCode } from "hono/utils/http-status" +import { Hono } from "hono" -import consola from "consola" -import { Hono, type Context } from "hono" -import { FetchError } from "ofetch" +import { forwardError } from "~/lib/forward-error" -import { logger } from "../../lib/logger" import { handleCompletion } from "./handler" export const completionRoutes = new Hono() @@ -14,150 +10,6 @@ completionRoutes.post("/", async (c) => { try { return await handleCompletion(c) } catch (error) { - consola.error("Error occurred:", error) - return handleError(c, error) + return await forwardError(c, error) } }) - -// Handle different error types with specific handlers -async function handleError( - c: Context, - error: unknown, -) { - if (error instanceof FetchError) { - return handleFetchError(c, error) - } - - if (error instanceof Response) { - return await handleResponseError(c, error) - } - - if (error instanceof Error) { - return handleGenericError(c, error) - } - - // Fallback for unknown error types - void logger.logResponse("/v1/chat/completions", { - error: { - message: "An unknown error occurred", - type: "unknown_error", - }, - }) - - return c.json( - { - error: { - message: "An unknown error occurred", - type: "unknown_error", - }, - }, - 500, - ) -} - -function handleFetchError( - c: Context, - error: FetchError, -) { - const status = error.response?.status ?? 500 - const responseData = error.response?._data as unknown - const headers: Record = {} - - // Forward all headers from the error response - for (const [key, value] of error.response?.headers.entries()) { - c.header(key, value) - headers[key] = value - } - - // Log the error response - void logger.logResponse( - "/v1/chat/completions", - { - error: { - message: error.message, - type: "fetch_error", - data: responseData, - status, - }, - }, - headers, - ) - - return c.json( - { - error: { - message: error.message, - type: "fetch_error", - data: responseData, - }, - }, - status as ContentfulStatusCode, - ) -} - -async function handleResponseError( - c: Context, - error: Response, -) { - const errorText = await error.text() - consola.error( - `Request failed: ${error.status} ${error.statusText}: ${errorText}`, - ) - - const headers: Record = {} - - // Forward all headers from the error response - for (const [key, value] of error.headers.entries()) { - c.header(key, value) - headers[key] = value - } - - // Log the error response - void logger.logResponse( - "/v1/chat/completions", - { - error: { - message: error.statusText || "Request failed", - type: "response_error", - status: error.status, - details: errorText, - }, - }, - headers, - ) - - return c.json( - { - error: { - message: error.statusText || "Request failed", - type: "response_error", - status: error.status, - details: errorText, - }, - }, - error.status as ContentfulStatusCode, - ) -} - -function handleGenericError( - c: Context, - error: Error, -) { - // Log the error response - void logger.logResponse("/v1/chat/completions", { - error: { - message: error.message, - type: "error", - }, - }) - - return c.json( - { - error: { - message: error.message, - type: "error", - }, - }, - 500, - ) -} diff --git a/src/routes/embeddings/route.ts b/src/routes/embeddings/route.ts index 3e3076bb..f18c8645 100644 --- a/src/routes/embeddings/route.ts +++ b/src/routes/embeddings/route.ts @@ -1,21 +1,20 @@ -import consola from "consola" import { Hono } from "hono" -import { FetchError } from "ofetch" -import type { EmbeddingRequest } from "~/services/copilot/embedding/types" - -import { embedding } from "~/services/copilot/embedding/service" +import { forwardError } from "~/lib/forward-error" +import { + createEmbeddings, + type EmbeddingRequest, +} from "~/services/copilot/create-embeddings" export const embeddingRoutes = new Hono() embeddingRoutes.post("/", async (c) => { try { - const embeddings = await embedding(await c.req.json()) - return c.json(embeddings) + const paylod = await c.req.json() + const response = await createEmbeddings(paylod) + + return c.json(response) } catch (error) { - if (error instanceof FetchError) { - consola.error(`Request failed: ${error.message}`, error.response?._data) - } - throw error + return await forwardError(c, error) } }) diff --git a/src/routes/models/route.ts b/src/routes/models/route.ts index 7cd87022..8e282a39 100644 --- a/src/routes/models/route.ts +++ b/src/routes/models/route.ts @@ -1,8 +1,7 @@ -import consola from "consola" import { Hono } from "hono" -import { FetchError } from "ofetch" -import { getModels } from "~/services/copilot/get-models/service" +import { forwardError } from "~/lib/forward-error" +import { getModels } from "~/services/copilot/get-models" export const modelRoutes = new Hono() @@ -11,9 +10,6 @@ modelRoutes.get("/", async (c) => { const models = await getModels() return c.json(models) } catch (error) { - if (error instanceof FetchError) { - consola.error(`Request failed: ${error.message}`, error.response?._data) - } - throw error + return await forwardError(c, error) } }) diff --git a/src/services/api-instance.ts b/src/services/api-instance.ts deleted file mode 100644 index 63bc523f..00000000 --- a/src/services/api-instance.ts +++ /dev/null @@ -1,57 +0,0 @@ -import consola from "consola" -import { FetchError, ofetch } from "ofetch" - -import { - COPILOT_API_CONFIG, - GITHUB_API_CONFIG, - GITHUB_WEB_API_CONFIG, -} from "~/lib/constants" -import { modelsCache } from "~/lib/models" -import { tokenService } from "~/lib/token" - -export const copilot = ofetch.create({ - baseURL: COPILOT_API_CONFIG.baseURL, - headers: COPILOT_API_CONFIG.headers, - - onRequest({ options }) { - options.headers.set( - "authorization", - `Bearer ${tokenService.getCopilotToken()}`, - ) - }, - - onRequestError({ error, options }) { - if (error instanceof FetchError) { - consola.error(`Request failed: ${options.body} \n ${error}`) - } - }, - - onResponse({ response }) { - if (response.url.endsWith("/models") && response._data) { - modelsCache.setModels(response._data) - } - }, - - onResponseError({ error, response, options }) { - if (error instanceof FetchError) { - consola.error( - // eslint-disable-next-line @typescript-eslint/no-base-to-string, @typescript-eslint/restrict-template-expressions - `Request failed: ${options.body} \n ${error} \n with response: ${JSON.stringify(response)}`, - ) - } - }, -}) - -export const github = ofetch.create({ - baseURL: GITHUB_API_CONFIG.baseURL, - - async onRequest({ options }) { - const token = await tokenService.getGithubToken() - options.headers.set("authorization", `token ${token}`) - }, -}) - -// Only used for device flow auth -export const _github = ofetch.create({ - baseURL: GITHUB_WEB_API_CONFIG.baseURL, -}) diff --git a/src/services/copilot/chat-completions/service-streaming.ts b/src/services/copilot/chat-completions/service-streaming.ts deleted file mode 100644 index a8b6849b..00000000 --- a/src/services/copilot/chat-completions/service-streaming.ts +++ /dev/null @@ -1,21 +0,0 @@ -import { stream } from "fetch-event-stream" - -import { COPILOT_API_CONFIG } from "~/lib/constants" -import { tokenService } from "~/lib/token" - -import type { ChatCompletionsPayload } from "./types" - -export const chatCompletionsStream = async ( - payload: ChatCompletionsPayload, -) => { - const copilotToken = tokenService.getCopilotToken() - - return stream(`${COPILOT_API_CONFIG.baseURL}/chat/completions`, { - method: "POST", - headers: { - ...COPILOT_API_CONFIG.headers, - authorization: `Bearer ${copilotToken}`, - }, - body: JSON.stringify(payload), - }) -} diff --git a/src/services/copilot/chat-completions/service.ts b/src/services/copilot/chat-completions/service.ts deleted file mode 100644 index d407b68a..00000000 --- a/src/services/copilot/chat-completions/service.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { copilot } from "~/services/api-instance" - -import type { ChatCompletionResponse, ChatCompletionsPayload } from "./types" - -export const chatCompletions = (payload: ChatCompletionsPayload) => - copilot("/chat/completions", { - method: "POST", - body: { - ...payload, - stream: false, - }, - }) diff --git a/src/services/copilot/create-chat-completions.ts b/src/services/copilot/create-chat-completions.ts new file mode 100644 index 00000000..bab69030 --- /dev/null +++ b/src/services/copilot/create-chat-completions.ts @@ -0,0 +1,31 @@ +import { events } from "fetch-event-stream" + +import { copilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" +import { state } from "~/lib/state" + +import type { + ChatCompletionResponse, + ChatCompletionsPayload, +} from "./chat-completions/types" + +export const createChatCompletions = async ( + payload: ChatCompletionsPayload, +) => { + if (!state.copilotToken) throw new Error("Copilot token not found") + + const response = await fetch(`${COPILOT_API_BASE_URL}/chat/completions`, { + method: "POST", + headers: copilotHeaders(state), + body: JSON.stringify(payload), + }) + + if (!response.ok) + throw new HTTPError("Failed to create chat completions", response) + + if (payload.stream) { + return events(response) + } + + return (await response.json()) as ChatCompletionResponse +} diff --git a/src/services/copilot/create-embeddings.ts b/src/services/copilot/create-embeddings.ts new file mode 100644 index 00000000..075aed91 --- /dev/null +++ b/src/services/copilot/create-embeddings.ts @@ -0,0 +1,38 @@ +import { copilotHeaders, COPILOT_API_BASE_URL } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" +import { state } from "~/lib/state" + +export const createEmbeddings = async (payload: EmbeddingRequest) => { + if (!state.copilotToken) throw new Error("Copilot token not found") + + const response = await fetch(`${COPILOT_API_BASE_URL}/embeddings`, { + method: "POST", + headers: copilotHeaders(state), + body: JSON.stringify(payload), + }) + + if (!response.ok) throw new HTTPError("Failed to create embeddings", response) + + return (await response.json()) as EmbeddingResponse +} + +export interface EmbeddingRequest { + input: string | Array + model: string +} + +export interface Embedding { + object: string + embedding: Array + index: number +} + +export interface EmbeddingResponse { + object: string + data: Array + model: string + usage: { + prompt_tokens: number + total_tokens: number + } +} diff --git a/src/services/copilot/embedding/service.ts b/src/services/copilot/embedding/service.ts deleted file mode 100644 index babee8f2..00000000 --- a/src/services/copilot/embedding/service.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { EmbeddingRequest, EmbeddingResponse } from "./types" - -import { copilot } from "../../api-instance" - -export const embedding = (payload: EmbeddingRequest) => - copilot("/embeddings", { - method: "POST", - body: { - ...payload, - }, - }) diff --git a/src/services/copilot/embedding/types.ts b/src/services/copilot/embedding/types.ts deleted file mode 100644 index cf54610f..00000000 --- a/src/services/copilot/embedding/types.ts +++ /dev/null @@ -1,20 +0,0 @@ -export interface EmbeddingRequest { - input: string | Array - model: string -} - -export interface Embedding { - object: string - embedding: Array - index: number -} - -export interface EmbeddingResponse { - object: string - data: Array - model: string - usage: { - prompt_tokens: number - total_tokens: number - } -} diff --git a/src/services/copilot/get-models/types.ts b/src/services/copilot/get-models.ts similarity index 59% rename from src/services/copilot/get-models/types.ts rename to src/services/copilot/get-models.ts index 7078433f..dd940a7f 100644 --- a/src/services/copilot/get-models/types.ts +++ b/src/services/copilot/get-models.ts @@ -1,3 +1,22 @@ +import { COPILOT_API_BASE_URL, copilotHeaders } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" +import { state } from "~/lib/state" + +export const getModels = async () => { + const response = await fetch(`${COPILOT_API_BASE_URL}/models`, { + headers: copilotHeaders(state), + }) + + if (!response.ok) throw new HTTPError("Failed to get models", response) + + return (await response.json()) as ModelsResponse +} + +export interface ModelsResponse { + data: Array + object: string +} + interface ModelLimits { max_context_window_tokens?: number max_output_tokens?: number @@ -34,8 +53,3 @@ interface Model { terms: string } } - -export interface GetModelsResponse { - data: Array - object: string -} diff --git a/src/services/copilot/get-models/service.ts b/src/services/copilot/get-models/service.ts deleted file mode 100644 index eded14a3..00000000 --- a/src/services/copilot/get-models/service.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { GetModelsResponse } from "./types" - -import { copilot } from "../../api-instance" - -export const getModels = () => - copilot("/models", { - method: "GET", - }) diff --git a/src/services/copilot/get-token/copilot-token.ts b/src/services/copilot/get-token/copilot-token.ts deleted file mode 100644 index 3bd7f07a..00000000 --- a/src/services/copilot/get-token/copilot-token.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { GetCopilotTokenResponse } from "./types" - -import { github } from "../../api-instance" - -export const getCopilotToken = async () => - github("/copilot_internal/v2/token", { - method: "GET", - }) diff --git a/src/services/copilot/get-token/types.ts b/src/services/copilot/get-token/types.ts deleted file mode 100644 index dd456646..00000000 --- a/src/services/copilot/get-token/types.ts +++ /dev/null @@ -1,31 +0,0 @@ -export interface GetCopilotTokenResponse { - annotations_enabled: boolean - chat_enabled: boolean - chat_jetbrains_enabled: boolean - code_quote_enabled: boolean - code_review_enabled: boolean - codesearch: boolean - copilotignore_enabled: boolean - endpoints: { - api: string - "origin-tracker": string - proxy: string - telemetry: string - } - expires_at: number - individual: boolean - limited_user_quotas: null - limited_user_reset_date: null - nes_enabled: boolean - prompt_8k: boolean - public_suggestions: "disabled" - refresh_in: number - sku: "free_educational" - snippy_load_test_enabled: boolean - telemetry: "disabled" - token: string - tracking_id: string - vsc_electron_fetcher_v2: boolean - xcode: boolean - xcode_chat: boolean -} diff --git a/src/services/get-vscode-version.ts b/src/services/get-vscode-version.ts new file mode 100644 index 00000000..ce330838 --- /dev/null +++ b/src/services/get-vscode-version.ts @@ -0,0 +1,19 @@ +const FALLBACK = "1.98.1" + +export async function getVSCodeVersion() { + const response = await fetch( + "https://aur.archlinux.org/cgit/aur.git/plain/PKGBUILD?h=visual-studio-code-bin", + ) + + const pkgbuild = await response.text() + const pkgverRegex = /pkgver=([0-9.]+)/ + const match = pkgbuild.match(pkgverRegex) + + if (match) { + return match[1] + } + + return FALLBACK +} + +await getVSCodeVersion() diff --git a/src/services/github/get-copilot-token.ts b/src/services/github/get-copilot-token.ts new file mode 100644 index 00000000..55701f30 --- /dev/null +++ b/src/services/github/get-copilot-token.ts @@ -0,0 +1,23 @@ +import { GITHUB_API_BASE_URL, githubHeaders } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" +import { state } from "~/lib/state" + +export const getCopilotToken = async () => { + const response = await fetch( + `${GITHUB_API_BASE_URL}/copilot_internal/v2/token`, + { + headers: githubHeaders(state), + }, + ) + + if (!response.ok) throw new HTTPError("Failed to get Copilot token", response) + + return (await response.json()) as GetCopilotTokenResponse +} + +// Trimmed for the sake of simplicity +interface GetCopilotTokenResponse { + expires_at: number + refresh_in: number + token: string +} diff --git a/src/services/github/get-device-code.ts b/src/services/github/get-device-code.ts new file mode 100644 index 00000000..1c3bebbb --- /dev/null +++ b/src/services/github/get-device-code.ts @@ -0,0 +1,30 @@ +import { + GITHUB_APP_SCOPES, + GITHUB_BASE_URL, + GITHUB_CLIENT_ID, + standardHeaders, +} from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" + +export async function getDeviceCode(): Promise { + const response = await fetch(`${GITHUB_BASE_URL}/login/device/code`, { + method: "POST", + headers: standardHeaders(), + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + scope: GITHUB_APP_SCOPES, + }), + }) + + if (!response.ok) throw new HTTPError("Failed to get device code", response) + + return (await response.json()) as DeviceCodeResponse +} + +export interface DeviceCodeResponse { + device_code: string + user_code: string + verification_uri: string + expires_in: number + interval: number +} diff --git a/src/services/github/get-token/service.ts b/src/services/github/get-token/service.ts deleted file mode 100644 index a126b37c..00000000 --- a/src/services/github/get-token/service.ts +++ /dev/null @@ -1,59 +0,0 @@ -import consola from "consola" - -import { ENV } from "~/lib/constants" -import { _github } from "~/services/api-instance" - -interface DeviceCodeResponse { - device_code: string - user_code: string - verification_uri: string - expires_in: number - interval: number -} - -interface AccessTokenResponse { - access_token: string - token_type: string - scope: string -} - -export async function getGitHubToken() { - const response = await _github("/login/device/code", { - method: "POST", - body: { - client_id: ENV.GITHUB_CLIENT_ID, - scope: ENV.GITHUB_OAUTH_SCOPES, - }, - }) - - consola.info( - `Please enter the code "${response.user_code}" in ${response.verification_uri}`, - ) - - while (true) { - const pollResponse = await _github( - "/login/oauth/access_token", - { - method: "POST", - body: { - client_id: ENV.GITHUB_CLIENT_ID, - device_code: response.device_code, - grant_type: "urn:ietf:params:oauth:grant-type:device_code", - }, - }, - ) - - if (pollResponse.access_token) { - consola.info( - `Got token ${pollResponse.access_token.replaceAll(/./g, "*")}`, - ) - return pollResponse - } else { - // Interval is in seconds, we need to multiply by 1000 to get milliseconds - // I'm also adding another second, just to be safe - await new Promise((resolve) => - setTimeout(resolve, (response.interval + 1) * 1000), - ) - } - } -} diff --git a/src/services/github/get-user.ts b/src/services/github/get-user.ts new file mode 100644 index 00000000..21590754 --- /dev/null +++ b/src/services/github/get-user.ts @@ -0,0 +1,21 @@ +import { GITHUB_API_BASE_URL, standardHeaders } from "~/lib/api-config" +import { HTTPError } from "~/lib/http-error" +import { state } from "~/lib/state" + +export async function getGitHubUser() { + const response = await fetch(`${GITHUB_API_BASE_URL}/user`, { + headers: { + authorization: `token ${state.githubToken}`, + ...standardHeaders(), + }, + }) + + if (!response.ok) throw new HTTPError("Failed to get GitHub user", response) + + return (await response.json()) as GithubUserResponse +} + +// Trimmed for the sake of simplicity +interface GithubUserResponse { + login: string +} diff --git a/src/services/github/get-user/service.ts b/src/services/github/get-user/service.ts deleted file mode 100644 index cd2c2758..00000000 --- a/src/services/github/get-user/service.ts +++ /dev/null @@ -1,56 +0,0 @@ -import { github } from "~/services/api-instance" - -export async function getGitHubUser() { - return github("/user", { - method: "GET", - }) -} - -interface GithubUser { - login: string - id: number - node_id: string - avatar_url: string - gravatar_id: string - url: string - html_url: string - followers_url: string - following_url: string - gists_url: string - starred_url: string - subscriptions_url: string - organizations_url: string - repos_url: string - events_url: string - received_events_url: string - type: "User" - user_view_type: "private" - site_admin: boolean - name: string - company: string | null - blog: string - location: string - email: null - hireable: null - bio: string - twitter_username: string | null - notification_email: null - public_repos: number - public_gists: number - followers: number - following: number - created_at: string - updated_at: string - private_gists: number - total_private_repos: number - owned_private_repos: number - disk_usage: number - collaborators: number - two_factor_authentication: boolean - plan: { - name: "pro" - space: number - collaborators: number - private_repos: number - } -} diff --git a/src/services/github/poll-access-token.ts b/src/services/github/poll-access-token.ts new file mode 100644 index 00000000..938ff70b --- /dev/null +++ b/src/services/github/poll-access-token.ts @@ -0,0 +1,58 @@ +import consola from "consola" + +import { + GITHUB_BASE_URL, + GITHUB_CLIENT_ID, + standardHeaders, +} from "~/lib/api-config" +import { sleep } from "~/lib/sleep" + +import type { DeviceCodeResponse } from "./get-device-code" + +export async function pollAccessToken( + deviceCode: DeviceCodeResponse, +): Promise { + // Interval is in seconds, we need to multiply by 1000 to get milliseconds + // I'm also adding another second, just to be safe + const sleepDuration = (deviceCode.interval + 1) * 1000 + consola.debug(`Polling access token with interval of ${sleepDuration}ms`) + + while (true) { + const response = await fetch( + `${GITHUB_BASE_URL}/login/oauth/access_token`, + { + method: "POST", + headers: standardHeaders(), + body: JSON.stringify({ + client_id: GITHUB_CLIENT_ID, + device_code: deviceCode.device_code, + grant_type: "urn:ietf:params:oauth:grant-type:device_code", + }), + }, + ) + + if (!response.ok) { + await sleep(sleepDuration) + consola.error("Failed to poll access token:", await response.text()) + + continue + } + + const json = await response.json() + consola.debug("Polling access token response:", json) + + const { access_token } = json as AccessTokenResponse + + if (access_token) { + return access_token + } else { + await sleep(sleepDuration) + } + } +} + +interface AccessTokenResponse { + access_token: string + token_type: string + scope: string +} diff --git a/test/main.test.ts b/test/main.test.ts deleted file mode 100644 index 52d7826d..00000000 --- a/test/main.test.ts +++ /dev/null @@ -1,84 +0,0 @@ -import { stream } from "fetch-event-stream" -import { x } from "tinyexec" -import { describe, it, beforeAll, afterAll, expect } from "vitest" - -import type { ChatCompletionsPayload } from "../src/services/copilot/chat-completions/types" - -import { ChatCompletionChunkSchema } from "../src/services/copilot/chat-completions/types-streaming" - -describe("Server API Tests", () => { - const TEST_PORT = 4142 - const BASE_URL = `http://localhost:${TEST_PORT}` - - let serverProcess: ReturnType - - beforeAll(async () => { - // Start the server as a separate process - serverProcess = x("bun", ["run", "start", "--port", TEST_PORT.toString()]) - - // Wait a bit for server to be ready - await new Promise((resolve) => setTimeout(resolve, 5000)) - }) - - afterAll(() => { - serverProcess.kill("SIGTERM") - }) - - it("POST /chat/completions should return valid completion (streaming)", async () => { - const payload: ChatCompletionsPayload = { - messages: [{ role: "user", content: "Write a short greeting" }], - model: "gpt-3.5-turbo", - stream: true, // Make sure to set stream to true - } - - let receivedChunks = 0 - let hasContent = false - let hasFinishReason = false - - try { - const response = await stream(`${BASE_URL}/chat/completions`, { - method: "POST", - headers: { - "Content-Type": "application/json", - }, - body: JSON.stringify(payload), - }) - - for await (const chunk of response) { - console.log(chunk) - - if (chunk.data === "[DONE]") break - - // Validate each chunk against our schema - const parseResult = ChatCompletionChunkSchema.safeParse( - JSON.parse(chunk.data ?? "{}"), - ) - - if (!parseResult.success) { - console.error("Invalid chunk format:", parseResult.error) - throw new Error(`Invalid chunk format: ${parseResult.error.message}`) - } - - receivedChunks++ - - // Check if we're getting content in the delta - if (parseResult.data.choices[0]?.delta?.content) { - hasContent = true - } - - // Check if we get a finish reason (indicates completion) - if (parseResult.data.choices[0]?.finish_reason) { - hasFinishReason = true - } - } - - // Add assertions to verify the response was correct - expect(receivedChunks).toBeGreaterThan(0) - expect(hasContent).toBe(true) - expect(hasFinishReason).toBe(true) - } catch (error) { - console.error("Streaming test failed:", error) - throw error - } - }) -}) diff --git a/tsconfig.json b/tsconfig.json index c453b492..bfff5e6b 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,28 +1,21 @@ { "compilerOptions": { - // Enable latest features - "lib": ["ESNext", "DOM"], "target": "ESNext", + "lib": ["ESNext"], "module": "ESNext", - "moduleDetection": "force", - "jsx": "react-jsx", - "allowJs": true, + "skipLibCheck": true, - // Bundler mode - "moduleResolution": "bundler", - "allowImportingTsExtensions": true, + "moduleResolution": "Bundler", + "moduleDetection": "force", + "erasableSyntaxOnly": true, "verbatimModuleSyntax": true, "noEmit": true, - // Best practices "strict": true, - "skipLibCheck": true, + "noUnusedLocals": true, + "noUnusedParameters": true, "noFallthroughCasesInSwitch": true, - - // Some stricter flags (disabled by default) - "noUnusedLocals": false, - "noUnusedParameters": false, - "noPropertyAccessFromIndexSignature": false, + "noUncheckedSideEffectImports": true, "baseUrl": ".", "paths": { diff --git a/tsup.config.ts b/tsup.config.ts index 22ecbd8c..4e007873 100644 --- a/tsup.config.ts +++ b/tsup.config.ts @@ -4,14 +4,12 @@ export default defineConfig({ entry: ["src/main.ts"], format: ["esm"], - target: "esnext", + target: "es2022", platform: "node", - dts: true, - removeNodeProtocol: false, - sourcemap: true, - shims: true, + minify: true, clean: true, + removeNodeProtocol: false, env: { NODE_ENV: "production",