From fca31832ad3fec4bf82d271fbb8295b70556af05 Mon Sep 17 00:00:00 2001 From: "Felix F." Date: Wed, 11 Mar 2026 23:39:34 +0000 Subject: [PATCH 01/10] chore: install Anthropic and Google Gemini SDK dependencies --- package-lock.json | 730 +++++++++++++++++++++++++++++++++------------- package.json | 6 +- 2 files changed, 539 insertions(+), 197 deletions(-) diff --git a/package-lock.json b/package-lock.json index e9b933c..ae6498c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,14 +1,16 @@ { "name": "assertify", - "version": "0.1.0", + "version": "0.1.1", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "assertify", - "version": "0.1.0", + "version": "0.1.1", "dependencies": { - "next": "14.1.0", + "@anthropic-ai/sdk": "^0.78.0", + "@google/generative-ai": "^0.24.1", + "next": "^14.2.35", "openai": "^4.28.0", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -21,7 +23,7 @@ "@typescript-eslint/parser": "^8.48.0", "autoprefixer": "^10.4.16", "eslint": "8.57.1", - "eslint-config-next": "14.2.18", + "eslint-config-next": "^14.1.0", "eslint-config-prettier": "^9.1.0", "husky": "^9.1.7", "postcss": "^8.4.32", @@ -43,6 +45,35 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@anthropic-ai/sdk": { + "version": "0.78.0", + "resolved": "https://registry.npmjs.org/@anthropic-ai/sdk/-/sdk-0.78.0.tgz", + "integrity": "sha512-PzQhR715td/m1UaaN5hHXjYB8Gl2lF9UVhrrGrZeysiF6Rb74Wc9GCB8hzLdzmQtBd1qe89F9OptgB9Za1Ib5w==", + "license": "MIT", + "dependencies": { + "json-schema-to-ts": "^3.1.1" + }, + "bin": { + "anthropic-ai-sdk": "bin/cli" + }, + "peerDependencies": { + "zod": "^3.25.0 || ^4.0.0" + }, + "peerDependenciesMeta": { + "zod": { + "optional": true + } + } + }, + "node_modules/@babel/runtime": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.6.tgz", + "integrity": "sha512-05WQkdpL9COIMz4LjTxGpPNCdlpyimKppYNoJ5Di5EUObifl8t4tuLuUBBZEpoLYOmfvIWrsp9fCl0HoPRVTdA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@emnapi/core": { "version": "1.7.1", "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.7.1.tgz", @@ -78,9 +109,9 @@ } }, "node_modules/@eslint-community/eslint-utils": { - "version": "4.9.0", - "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.0.tgz", - "integrity": "sha512-ayVFHdtZ+hsq1t2Dy24wCmGXGe4q9Gu3smhLYALJrr473ZH27MsnSL+LKUlimp4BWJqMDMLmPpx/Q9R3OAlL4g==", + "version": "4.9.1", + "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.9.1.tgz", + "integrity": "sha512-phrYmNiYppR7znFEdqgfWHXR6NCkZEK7hwWDHZUjit/2/U0r6XvkDl0SYnoM51Hq7FhCGdLDT6zxCCOY1hexsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -152,9 +183,9 @@ } }, "node_modules/@eslint/eslintrc/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -174,6 +205,15 @@ "node": "^12.22.0 || ^14.17.0 || >=16.0.0" } }, + "node_modules/@google/generative-ai": { + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", + "license": "Apache-2.0", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/@humanwhocodes/config-array": { "version": "0.13.0", "resolved": "https://registry.npmjs.org/@humanwhocodes/config-array/-/config-array-0.13.0.tgz", @@ -202,9 +242,9 @@ } }, "node_modules/@humanwhocodes/config-array/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -268,13 +308,13 @@ } }, "node_modules/@isaacs/cliui/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -336,15 +376,15 @@ } }, "node_modules/@next/env": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/env/-/env-14.1.0.tgz", - "integrity": "sha512-Py8zIo+02ht82brwwhTg36iogzFqGLPXlRGKQw5s+qP/kMNc4MAyDeEwBKDijk6zTIbegEgu8Qy7C1LboslQAw==", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.35.tgz", + "integrity": "sha512-DuhvCtj4t9Gwrx80dmz2F4t/zKQ4ktN8WrMwOuVzkJfBilwAwGr6v16M5eI8yCuZ63H9TTuEU09Iu2HqkzFPVQ==", "license": "MIT" }, "node_modules/@next/eslint-plugin-next": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.2.18.tgz", - "integrity": "sha512-KyYTbZ3GQwWOjX3Vi1YcQbekyGP0gdammb7pbmmi25HBUCINzDReyrzCMOJIeZisK1Q3U6DT5Rlc4nm2/pQeXA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/@next/eslint-plugin-next/-/eslint-plugin-next-14.1.0.tgz", + "integrity": "sha512-x4FavbNEeXx/baD/zC/SdrvkjSby8nBn8KcCREqk6UuwvwoAPZmaV8TFCAuo/cpovBRTIY67mHhe86MQQm/68Q==", "dev": true, "license": "MIT", "dependencies": { @@ -352,9 +392,9 @@ } }, "node_modules/@next/swc-darwin-arm64": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.1.0.tgz", - "integrity": "sha512-nUDn7TOGcIeyQni6lZHfzNoo9S0euXnu0jhsbMOmMJUBfgsnESdjN97kM7cBqQxZa8L/bM9om/S5/1dzCrW6wQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-arm64/-/swc-darwin-arm64-14.2.33.tgz", + "integrity": "sha512-HqYnb6pxlsshoSTubdXKu15g3iivcbsMXg4bYpjL2iS/V6aQot+iyF4BUc2qA/J/n55YtvE4PHMKWBKGCF/+wA==", "cpu": [ "arm64" ], @@ -368,9 +408,9 @@ } }, "node_modules/@next/swc-darwin-x64": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.1.0.tgz", - "integrity": "sha512-1jgudN5haWxiAl3O1ljUS2GfupPmcftu2RYJqZiMJmmbBT5M1XDffjUtRUzP4W3cBHsrvkfOFdQ71hAreNQP6g==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-darwin-x64/-/swc-darwin-x64-14.2.33.tgz", + "integrity": "sha512-8HGBeAE5rX3jzKvF593XTTFg3gxeU4f+UWnswa6JPhzaR6+zblO5+fjltJWIZc4aUalqTclvN2QtTC37LxvZAA==", "cpu": [ "x64" ], @@ -384,9 +424,9 @@ } }, "node_modules/@next/swc-linux-arm64-gnu": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.1.0.tgz", - "integrity": "sha512-RHo7Tcj+jllXUbK7xk2NyIDod3YcCPDZxj1WLIYxd709BQ7WuRYl3OWUNG+WUfqeQBds6kvZYlc42NJJTNi4tQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-gnu/-/swc-linux-arm64-gnu-14.2.33.tgz", + "integrity": "sha512-JXMBka6lNNmqbkvcTtaX8Gu5by9547bukHQvPoLe9VRBx1gHwzf5tdt4AaezW85HAB3pikcvyqBToRTDA4DeLw==", "cpu": [ "arm64" ], @@ -400,9 +440,9 @@ } }, "node_modules/@next/swc-linux-arm64-musl": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.1.0.tgz", - "integrity": "sha512-v6kP8sHYxjO8RwHmWMJSq7VZP2nYCkRVQ0qolh2l6xroe9QjbgV8siTbduED4u0hlk0+tjS6/Tuy4n5XCp+l6g==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-arm64-musl/-/swc-linux-arm64-musl-14.2.33.tgz", + "integrity": "sha512-Bm+QulsAItD/x6Ih8wGIMfRJy4G73tu1HJsrccPW6AfqdZd0Sfm5Imhgkgq2+kly065rYMnCOxTBvmvFY1BKfg==", "cpu": [ "arm64" ], @@ -416,9 +456,9 @@ } }, "node_modules/@next/swc-linux-x64-gnu": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.1.0.tgz", - "integrity": "sha512-zJ2pnoFYB1F4vmEVlb/eSe+VH679zT1VdXlZKX+pE66grOgjmKJHKacf82g/sWE4MQ4Rk2FMBCRnX+l6/TVYzQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-gnu/-/swc-linux-x64-gnu-14.2.33.tgz", + "integrity": "sha512-FnFn+ZBgsVMbGDsTqo8zsnRzydvsGV8vfiWwUo1LD8FTmPTdV+otGSWKc4LJec0oSexFnCYVO4hX8P8qQKaSlg==", "cpu": [ "x64" ], @@ -432,9 +472,9 @@ } }, "node_modules/@next/swc-linux-x64-musl": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.1.0.tgz", - "integrity": "sha512-rbaIYFt2X9YZBSbH/CwGAjbBG2/MrACCVu2X0+kSykHzHnYH5FjHxwXLkcoJ10cX0aWCEynpu+rP76x0914atg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.33.tgz", + "integrity": "sha512-345tsIWMzoXaQndUTDv1qypDRiebFxGYx9pYkhwY4hBRaOLt8UGfiWKr9FSSHs25dFIf8ZqIFaPdy5MljdoawA==", "cpu": [ "x64" ], @@ -448,9 +488,9 @@ } }, "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.1.0.tgz", - "integrity": "sha512-o1N5TsYc8f/HpGt39OUQpQ9AKIGApd3QLueu7hXk//2xq5Z9OxmV6sQfNp8C7qYmiOlHYODOGqNNa0e9jvchGQ==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.33.tgz", + "integrity": "sha512-nscpt0G6UCTkrT2ppnJnFsYbPDQwmum4GNXYTeoTIdsmMydSKFz9Iny2jpaRupTb+Wl298+Rh82WKzt9LCcqSQ==", "cpu": [ "arm64" ], @@ -464,9 +504,9 @@ } }, "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.1.0.tgz", - "integrity": "sha512-XXIuB1DBRCFwNO6EEzCTMHT5pauwaSj4SWs7CYnME57eaReAKBXCnkUE80p/pAZcewm7hs+vGvNqDPacEXHVkw==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.33.tgz", + "integrity": "sha512-pc9LpGNKhJ0dXQhZ5QMmYxtARwwmWLpeocFmVG5Z0DzWq5Uf0izcI8tLc+qOpqxO1PWqZ5A7J1blrUIKrIFc7Q==", "cpu": [ "ia32" ], @@ -480,9 +520,9 @@ } }, "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.1.0.tgz", - "integrity": "sha512-9WEbVRRAqJ3YFVqEZIxUqkiO8l1nool1LmNxygr5HWF8AcSYsEpneUDhmjUVJEzO2A04+oPtZdombzzPPkTtgg==", + "version": "14.2.33", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.33.tgz", + "integrity": "sha512-nOjfZMy8B94MdisuzZo9/57xuFVLHJaDj5e/xrduJp9CV2/HrfxTRH2fbyLe+K9QT41WBLUd4iXX3R7jBp0EUg==", "cpu": [ "x64" ], @@ -562,18 +602,25 @@ "license": "MIT" }, "node_modules/@rushstack/eslint-patch": { - "version": "1.15.0", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.15.0.tgz", - "integrity": "sha512-ojSshQPKwVvSMR8yT2L/QtUkV5SXi/IfDiJ4/8d6UbTPjiHVmxZzUAzGD8Tzks1b9+qQkZa0isUOvYObedITaw==", + "version": "1.16.1", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.16.1.tgz", + "integrity": "sha512-TvZbIpeKqGQQ7X0zSCvPH9riMSFQFSggnfBjFZ1mEoILW+UuXCKwOoPcgjMwiUtRqFZ8jWhPJc4um14vC6I4ag==", "dev": true, "license": "MIT" }, + "node_modules/@swc/counter": { + "version": "0.1.3", + "resolved": "https://registry.npmjs.org/@swc/counter/-/counter-0.1.3.tgz", + "integrity": "sha512-e2BR4lsJkkRlKZ/qCHPw9ZaSxc0MVUd7gtbtaB7aMvHeJVYe8sOB8DBZkP2DtISHGSku9sCK6T6cnY0CtXrOCQ==", + "license": "Apache-2.0" + }, "node_modules/@swc/helpers": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.2.tgz", - "integrity": "sha512-E4KcWTpoLHqwPHLxidpOqQbcrZVgi0rsmmZXUle1jXmJfuIf/UWpczUJ7MZZ5tlxytgJXyp0w4PGkkeLiuIdZw==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.5.tgz", + "integrity": "sha512-KGYxvIOXcceOAbEk4bi/dVLEK9z8sZ0uBB3Il5b1rhfClSpcX0yfRO0KmTkqR2cnQDymwLB+25ZyMzICg/cm/A==", "license": "Apache-2.0", "dependencies": { + "@swc/counter": "^0.1.3", "tslib": "^2.4.0" } }, @@ -643,20 +690,20 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.50.0.tgz", - "integrity": "sha512-O7QnmOXYKVtPrfYzMolrCTfkezCJS9+ljLdKW/+DCvRsc3UAz+sbH6Xcsv7p30+0OwUbeWfUDAQE0vpabZ3QLg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.57.0.tgz", + "integrity": "sha512-qeu4rTHR3/IaFORbD16gmjq9+rEs9fGKdX0kF6BKSfi+gCuG3RCKLlSBYzn/bGsY9Tj7KE/DAQStbp8AHJGHEQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/type-utils": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "ignore": "^7.0.0", + "@eslint-community/regexpp": "^4.12.2", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/type-utils": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "ignore": "^7.0.5", "natural-compare": "^1.4.0", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -666,23 +713,23 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.50.0", - "eslint": "^8.57.0 || ^9.0.0", + "@typescript-eslint/parser": "^8.57.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.50.0.tgz", - "integrity": "sha512-6/cmF2piao+f6wSxUsJLZjck7OQsYyRtcOZS02k7XINSNlz93v6emM8WutDQSXnroG2xwYlEVHJI+cPA7CPM3Q==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.57.0.tgz", + "integrity": "sha512-XZzOmihLIr8AD1b9hL9ccNMzEMWt/dE2u7NyTY9jJG6YNiNthaD5XtUHVF2uCXZ15ng+z2hT3MVuxnUYhq6k1g==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "debug": "^4.3.4" + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -692,20 +739,20 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.50.0.tgz", - "integrity": "sha512-Cg/nQcL1BcoTijEWyx4mkVC56r8dj44bFDvBdygifuS20f3OZCHmFbjF34DPSi07kwlFvqfv/xOLnJ5DquxSGQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.57.0.tgz", + "integrity": "sha512-pR+dK0BlxCLxtWfaKQWtYr7MhKmzqZxuii+ZjuFlZlIGRZm22HnXFqa2eY+90MUz8/i80YJmzFGDUsi8dMOV5w==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.50.0", - "@typescript-eslint/types": "^8.50.0", - "debug": "^4.3.4" + "@typescript-eslint/tsconfig-utils": "^8.57.0", + "@typescript-eslint/types": "^8.57.0", + "debug": "^4.4.3" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -719,14 +766,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.50.0.tgz", - "integrity": "sha512-xCwfuCZjhIqy7+HKxBLrDVT5q/iq7XBVBXLn57RTIIpelLtEIZHXAF/Upa3+gaCpeV1NNS5Z9A+ID6jn50VD4A==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.57.0.tgz", + "integrity": "sha512-nvExQqAHF01lUM66MskSaZulpPL5pgy5hI5RfrxviLgzZVffB5yYzw27uK/ft8QnKXI2X0LBrHJFr1TaZtAibw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0" + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -737,9 +784,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.50.0.tgz", - "integrity": "sha512-vxd3G/ybKTSlm31MOA96gqvrRGv9RJ7LGtZCn2Vrc5htA0zCDvcMqUkifcjrWNNKXHUU3WCkYOzzVSFBd0wa2w==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.57.0.tgz", + "integrity": "sha512-LtXRihc5ytjJIQEH+xqjB0+YgsV4/tW35XKX3GTZHpWtcC8SPkT/d4tqdf1cKtesryHm2bgp6l555NYcT2NLvA==", "dev": true, "license": "MIT", "engines": { @@ -754,17 +801,17 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.50.0.tgz", - "integrity": "sha512-7OciHT2lKCewR0mFoBrvZJ4AXTMe/sYOe87289WAViOocEmDjjv8MvIOT2XESuKj9jp8u3SZYUSh89QA4S1kQw==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.57.0.tgz", + "integrity": "sha512-yjgh7gmDcJ1+TcEg8x3uWQmn8ifvSupnPfjP21twPKrDP/pTHlEQgmKcitzF/rzPSmv7QjJ90vRpN4U+zoUjwQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0", - "@typescript-eslint/utils": "8.50.0", - "debug": "^4.3.4", - "ts-api-utils": "^2.1.0" + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0", + "@typescript-eslint/utils": "8.57.0", + "debug": "^4.4.3", + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -774,14 +821,14 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/types": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.50.0.tgz", - "integrity": "sha512-iX1mgmGrXdANhhITbpp2QQM2fGehBse9LbTf0sidWK6yg/NE+uhV5dfU1g6EYPlcReYmkE9QLPq/2irKAmtS9w==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.57.0.tgz", + "integrity": "sha512-dTLI8PEXhjUC7B9Kre+u0XznO696BhXcTlOn0/6kf1fHaQW8+VjJAVHJ3eTI14ZapTxdkOmc80HblPQLaEeJdg==", "dev": true, "license": "MIT", "engines": { @@ -793,21 +840,21 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.50.0.tgz", - "integrity": "sha512-W7SVAGBR/IX7zm1t70Yujpbk+zdPq/u4soeFSknWFdXIFuWsBGBOUu/Tn/I6KHSKvSh91OiMuaSnYp3mtPt5IQ==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.57.0.tgz", + "integrity": "sha512-m7faHcyVg0BT3VdYTlX8GdJEM7COexXxS6KqGopxdtkQRvBanK377QDHr4W/vIPAR+ah9+B/RclSW5ldVniO1Q==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.50.0", - "@typescript-eslint/tsconfig-utils": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/visitor-keys": "8.50.0", - "debug": "^4.3.4", - "minimatch": "^9.0.4", - "semver": "^7.6.0", + "@typescript-eslint/project-service": "8.57.0", + "@typescript-eslint/tsconfig-utils": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/visitor-keys": "8.57.0", + "debug": "^4.4.3", + "minimatch": "^10.2.2", + "semver": "^7.7.3", "tinyglobby": "^0.2.15", - "ts-api-utils": "^2.1.0" + "ts-api-utils": "^2.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -821,16 +868,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.50.0.tgz", - "integrity": "sha512-87KgUXET09CRjGCi2Ejxy3PULXna63/bMYv72tCAlDJC3Yqwln0HiFJ3VJMst2+mEtNtZu5oFvX4qJGjKsnAgg==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.57.0.tgz", + "integrity": "sha512-5iIHvpD3CZe06riAsbNxxreP+MuYgVUsV0n4bwLH//VJmgtt54sQeY2GszntJ4BjYCpMzrfVh2SBnUQTtys2lQ==", "dev": true, "license": "MIT", "dependencies": { - "@eslint-community/eslint-utils": "^4.7.0", - "@typescript-eslint/scope-manager": "8.50.0", - "@typescript-eslint/types": "8.50.0", - "@typescript-eslint/typescript-estree": "8.50.0" + "@eslint-community/eslint-utils": "^4.9.1", + "@typescript-eslint/scope-manager": "8.57.0", + "@typescript-eslint/types": "8.57.0", + "@typescript-eslint/typescript-estree": "8.57.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -840,19 +887,19 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "eslint": "^8.57.0 || ^9.0.0", + "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.0.0" } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.50.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.50.0.tgz", - "integrity": "sha512-Xzmnb58+Db78gT/CCj/PVCvK+zxbnsw6F+O1oheYszJbBSdEjVhQi3C/Xttzxgi/GLmpvOggRs1RFpiJ8+c34Q==", + "version": "8.57.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.57.0.tgz", + "integrity": "sha512-zm6xx8UT/Xy2oSr2ZXD0pZo7Jx2XsCoID2IUh9YSTFRu7z+WdwYTRk6LhUftm1crwqbuoF6I8zAFeCMw0YjwDg==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.50.0", - "eslint-visitor-keys": "^4.2.1" + "@typescript-eslint/types": "8.57.0", + "eslint-visitor-keys": "^5.0.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -863,13 +910,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys/node_modules/eslint-visitor-keys": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-4.2.1.tgz", - "integrity": "sha512-Uhdk5sfqcee/9H/rCOJikYz67o0a2Tw2hGRPOG2Y1R2dg7brRe1uG0yaNQDHu+TO/uQPF/5eCapvYSmHUjt7JQ==", + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/eslint-visitor-keys/-/eslint-visitor-keys-5.0.1.tgz", + "integrity": "sha512-tD40eHxA35h0PEIZNeIjkHoDR4YjjJp34biM0mDvplBe//mB+IHCqHDGV7pxF+7MklTvighcCPPZC7ynWyjdTA==", "dev": true, "license": "Apache-2.0", "engines": { - "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + "node": "^20.19.0 || ^22.13.0 || >=24" }, "funding": { "url": "https://opencollective.com/eslint" @@ -1199,9 +1246,9 @@ } }, "node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", + "version": "6.14.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.14.0.tgz", + "integrity": "sha512-IWrosm/yrn43eiKqkfkHis7QioDleaXQHdDVPKg0FSwwd/DuvyX79TZnFOnYpB7dcsFAMmtFztZuXPDvSePkFw==", "dev": true, "license": "MIT", "dependencies": { @@ -1326,6 +1373,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/array-union": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", + "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/array.prototype.findlast": { "version": "1.2.5", "resolved": "https://registry.npmjs.org/array.prototype.findlast/-/array.prototype.findlast-1.2.5.tgz", @@ -1573,13 +1630,26 @@ } }, "node_modules/brace-expansion": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", - "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "version": "5.0.4", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.4.tgz", + "integrity": "sha512-h+DEnpVvxmfVefa4jFbCf5HdH5YMDXRsmKflpf1pILZWRFlTbJpxeU55nJl4Smt5HQaGzg1o6RHFPJaOqnmBDg==", "dev": true, "license": "MIT", "dependencies": { - "balanced-match": "^1.0.0" + "balanced-match": "^4.0.2" + }, + "engines": { + "node": "18 || 20 || >=22" + } + }, + "node_modules/brace-expansion/node_modules/balanced-match": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-4.0.4.tgz", + "integrity": "sha512-BLrgEcRTwX2o6gGxGOCNyMvGSp35YofuYzw9h1IMTRmKqttAZZVU67bdb9Pr2vUHA8+j3i2tJfjO6C6+4myGTA==", + "dev": true, + "license": "MIT", + "engines": { + "node": "18 || 20 || >=22" } }, "node_modules/braces": { @@ -2012,6 +2082,19 @@ "dev": true, "license": "Apache-2.0" }, + "node_modules/dir-glob": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", + "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dlv": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", @@ -2321,16 +2404,15 @@ } }, "node_modules/eslint-config-next": { - "version": "14.2.18", - "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.2.18.tgz", - "integrity": "sha512-SuDRcpJY5VHBkhz5DijJ4iA4bVnBA0n48Rb+YSJSCDr+h7kKAcb1mZHusLbW+WA8LDB6edSolomXA55eG3eOVA==", + "version": "14.1.0", + "resolved": "https://registry.npmjs.org/eslint-config-next/-/eslint-config-next-14.1.0.tgz", + "integrity": "sha512-SBX2ed7DoRFXC6CQSLc/SbLY9Ut6HxNB2wPTcoIWjUMd7aF7O/SIE7111L8FdZ9TXsNV4pulUDnfthpyPtbFUg==", "dev": true, "license": "MIT", "dependencies": { - "@next/eslint-plugin-next": "14.2.18", + "@next/eslint-plugin-next": "14.1.0", "@rushstack/eslint-patch": "^1.3.3", - "@typescript-eslint/eslint-plugin": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", - "@typescript-eslint/parser": "^5.4.2 || ^6.0.0 || ^7.0.0 || ^8.0.0", + "@typescript-eslint/parser": "^5.4.2 || ^6.0.0", "eslint-import-resolver-node": "^0.3.6", "eslint-import-resolver-typescript": "^3.5.2", "eslint-plugin-import": "^2.28.1", @@ -2348,6 +2430,153 @@ } } }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/parser": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-6.21.0.tgz", + "integrity": "sha512-tbsV1jPne5CkFQCgPBcDOt30ItF7aJoZL997JSF7MhGQqOeT3svWRYxiqlfA5RUdlHN6Fi+EI9bxqbdyAUZjYQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/scope-manager": "6.21.0", + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/typescript-estree": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependencies": { + "eslint": "^7.0.0 || ^8.0.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/scope-manager": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-6.21.0.tgz", + "integrity": "sha512-OwLUIWZJry80O99zvqXVEioyniJMa+d2GrqpUTqi5/v5D5rOrppJVBPa0yKCblcigC0/aYAzxxqQ1B+DS2RYsg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/types": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-6.21.0.tgz", + "integrity": "sha512-1kFmZ1rOm5epu9NZEZm1kckCDGj5UJEf7P1kliH4LKu/RkwpsfqqGmY2OOcUs18lSlQBKLDYBOGxRVtrMN5lpg==", + "dev": true, + "license": "MIT", + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/typescript-estree": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-6.21.0.tgz", + "integrity": "sha512-6npJTkZcO+y2/kr+z0hc4HwNfrrP4kNYh57ek7yCNlrBjWQ1Y0OS7jiZTkgumrvkX5HkEKXFZkkdFNkaW2wmUQ==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "@typescript-eslint/visitor-keys": "6.21.0", + "debug": "^4.3.4", + "globby": "^11.1.0", + "is-glob": "^4.0.3", + "minimatch": "9.0.3", + "semver": "^7.5.4", + "ts-api-utils": "^1.0.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, + "node_modules/eslint-config-next/node_modules/@typescript-eslint/visitor-keys": { + "version": "6.21.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-6.21.0.tgz", + "integrity": "sha512-JJtkDduxLi9bivAB+cYOVMtbkqdPOhZ+ZI5LC47MIRrDV4Yn2o+ZnW10Nkmr28xRpSpdJ6Sm42Hjf2+REYXm0A==", + "dev": true, + "license": "MIT", + "dependencies": { + "@typescript-eslint/types": "6.21.0", + "eslint-visitor-keys": "^3.4.1" + }, + "engines": { + "node": "^16.0.0 || >=18.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/typescript-eslint" + } + }, + "node_modules/eslint-config-next/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/eslint-config-next/node_modules/minimatch": { + "version": "9.0.3", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.3.tgz", + "integrity": "sha512-RHiac9mvaRw0x3AYRgDC1CxAP7HTcNrrECeA8YYJeWnpo+2Q5CegtZjaotWTWxDG3UeGA1coE05iH1mPjT/2mg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/eslint-config-next/node_modules/ts-api-utils": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-1.4.3.tgz", + "integrity": "sha512-i3eMG77UTMD0hZhgRS562pv83RC6ukSAC2GMNWc+9dieh/+jDM5u5YG+NHX6VNDRHQcHwmsTHctP9LhbC3WxVw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=16" + }, + "peerDependencies": { + "typescript": ">=4.2.0" + } + }, "node_modules/eslint-config-prettier": { "version": "9.1.2", "resolved": "https://registry.npmjs.org/eslint-config-prettier/-/eslint-config-prettier-9.1.2.tgz", @@ -2515,9 +2744,9 @@ } }, "node_modules/eslint-plugin-import/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2579,9 +2808,9 @@ } }, "node_modules/eslint-plugin-jsx-a11y/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2662,9 +2891,9 @@ } }, "node_modules/eslint-plugin-react/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -2754,9 +2983,9 @@ } }, "node_modules/eslint/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -3191,6 +3420,7 @@ "version": "10.3.10", "resolved": "https://registry.npmjs.org/glob/-/glob-10.3.10.tgz", "integrity": "sha512-fa46+tv1Ak0UPK1TOy/pZrIybNNt4HCv7SDzwyfiOZkvZLEbjsZkJBPtDHVshZjbecAoAGSC20MjLDG/qr679g==", + "deprecated": "Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me", "dev": true, "license": "ISC", "dependencies": { @@ -3223,6 +3453,32 @@ "node": ">=10.13.0" } }, + "node_modules/glob/node_modules/brace-expansion": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.2.tgz", + "integrity": "sha512-Jt0vHyM+jmUBqojB7E1NIYadt0vI0Qxjxd2TErW94wDz+E2LAm5vKMXXwg6ZZBTHPuUlDgQHKXvjGBdfcF1ZDQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/glob/node_modules/minimatch": { + "version": "9.0.9", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.9.tgz", + "integrity": "sha512-OBwBN9AL4dqmETlpS2zasx+vTeWclWzkblfZk7KTA5j3jeOONz/tRCnZomUyvNg83wL5Zv9Ss6HMJXAgL8R2Yg==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.2" + }, + "engines": { + "node": ">=16 || 14 >=14.17" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, "node_modules/globals": { "version": "13.24.0", "resolved": "https://registry.npmjs.org/globals/-/globals-13.24.0.tgz", @@ -3256,6 +3512,37 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/globby": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", + "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", + "dev": true, + "license": "MIT", + "dependencies": { + "array-union": "^2.1.0", + "dir-glob": "^3.0.1", + "fast-glob": "^3.2.9", + "ignore": "^5.2.0", + "merge2": "^1.4.1", + "slash": "^3.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/globby/node_modules/ignore": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", + "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4" + } + }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3990,6 +4277,19 @@ "dev": true, "license": "MIT" }, + "node_modules/json-schema-to-ts": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/json-schema-to-ts/-/json-schema-to-ts-3.1.1.tgz", + "integrity": "sha512-+DWg8jCJG2TEnpy7kOm/7/AxaYoaRbjVB4LFZLySZlWn8exGs3A4OLJR966cVvU26N7X9TWxl+Jsw7dzAqKT6g==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.3", + "ts-algebra": "^2.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -4194,16 +4494,16 @@ } }, "node_modules/minimatch": { - "version": "9.0.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-9.0.5.tgz", - "integrity": "sha512-G6T0ZX48xgozx7587koeX9Ys2NYy6Gmv//P89sEte9V9whIapMNF4idKxnW2QtCcLiTWlb/wfCabAtAFWhhBow==", + "version": "10.2.4", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.2.4.tgz", + "integrity": "sha512-oRjTw/97aTBN0RHbYCdtF1MQfvusSIBQM0IZEgzl6426+8jSC0nF1a/GmnVLpfB9yyr6g6FTqWqiZVbxrtaCIg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { - "brace-expansion": "^2.0.1" + "brace-expansion": "^5.0.2" }, "engines": { - "node": ">=16 || 14 >=14.17" + "node": "18 || 20 || >=22" }, "funding": { "url": "https://github.com/sponsors/isaacs" @@ -4220,11 +4520,11 @@ } }, "node_modules/minipass": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz", - "integrity": "sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==", + "version": "7.1.3", + "resolved": "https://registry.npmjs.org/minipass/-/minipass-7.1.3.tgz", + "integrity": "sha512-tEBHqDnIoM/1rXME1zgka9g6Q2lcoCkxHLuc7ODJ5BxbP5d4c2Z5cGgtXAku59200Cx7diuHTOYfSBD8n6mm8A==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "engines": { "node": ">=16 || 14 >=14.17" } @@ -4289,14 +4589,13 @@ "license": "MIT" }, "node_modules/next": { - "version": "14.1.0", - "resolved": "https://registry.npmjs.org/next/-/next-14.1.0.tgz", - "integrity": "sha512-wlzrsbfeSU48YQBjZhDzOwhWhGsy+uQycR8bHAOt1LY1bn3zZEcDyHQOEoN3aWzQ8LHCAJ1nqrWCc9XF2+O45Q==", - "deprecated": "This version has a security vulnerability. Please upgrade to a patched version. See https://nextjs.org/blog/security-update-2025-12-11 for more details.", + "version": "14.2.35", + "resolved": "https://registry.npmjs.org/next/-/next-14.2.35.tgz", + "integrity": "sha512-KhYd2Hjt/O1/1aZVX3dCwGXM1QmOV4eNM2UTacK5gipDdPN/oHHK/4oVGy7X8GMfPMsUTUEmGlsy0EY1YGAkig==", "license": "MIT", "dependencies": { - "@next/env": "14.1.0", - "@swc/helpers": "0.5.2", + "@next/env": "14.2.35", + "@swc/helpers": "0.5.5", "busboy": "1.6.0", "caniuse-lite": "^1.0.30001579", "graceful-fs": "^4.2.11", @@ -4310,18 +4609,19 @@ "node": ">=18.17.0" }, "optionalDependencies": { - "@next/swc-darwin-arm64": "14.1.0", - "@next/swc-darwin-x64": "14.1.0", - "@next/swc-linux-arm64-gnu": "14.1.0", - "@next/swc-linux-arm64-musl": "14.1.0", - "@next/swc-linux-x64-gnu": "14.1.0", - "@next/swc-linux-x64-musl": "14.1.0", - "@next/swc-win32-arm64-msvc": "14.1.0", - "@next/swc-win32-ia32-msvc": "14.1.0", - "@next/swc-win32-x64-msvc": "14.1.0" + "@next/swc-darwin-arm64": "14.2.33", + "@next/swc-darwin-x64": "14.2.33", + "@next/swc-linux-arm64-gnu": "14.2.33", + "@next/swc-linux-arm64-musl": "14.2.33", + "@next/swc-linux-x64-gnu": "14.2.33", + "@next/swc-linux-x64-musl": "14.2.33", + "@next/swc-win32-arm64-msvc": "14.2.33", + "@next/swc-win32-ia32-msvc": "14.2.33", + "@next/swc-win32-x64-msvc": "14.2.33" }, "peerDependencies": { "@opentelemetry/api": "^1.1.0", + "@playwright/test": "^1.41.2", "react": "^18.2.0", "react-dom": "^18.2.0", "sass": "^1.3.0" @@ -4330,6 +4630,9 @@ "@opentelemetry/api": { "optional": true }, + "@playwright/test": { + "optional": true + }, "sass": { "optional": true } @@ -4743,6 +5046,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/path-type": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", + "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5226,9 +5539,9 @@ } }, "node_modules/rimraf/node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", + "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", "dev": true, "license": "ISC", "dependencies": { @@ -5500,6 +5813,16 @@ "url": "https://github.com/sponsors/isaacs" } }, + "node_modules/slash": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", + "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, "node_modules/source-map-js": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", @@ -5593,13 +5916,13 @@ } }, "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -5978,10 +6301,16 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, + "node_modules/ts-algebra": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ts-algebra/-/ts-algebra-2.0.0.tgz", + "integrity": "sha512-FPAhNPFMrkwz76P7cdjdmiShwMynZYN6SgOujD1urY4oNm80Ou9oMdmbR45LotcKOXoy7wSmHkRFE6Mxbrhefw==", + "license": "MIT" + }, "node_modules/ts-api-utils": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.1.0.tgz", - "integrity": "sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==", + "version": "2.4.0", + "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.4.0.tgz", + "integrity": "sha512-3TaVTaAv2gTiMB35i3FiGJaRfwb3Pyn/j3m/bfAvGe8FB7CF6u+LMYqYlDh7reQf7UNvoTvdfAqHGmPGOSsPmA==", "dev": true, "license": "MIT", "engines": { @@ -6469,13 +6798,13 @@ } }, "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.2.0.tgz", + "integrity": "sha512-yDPMNjp4WyfYBkHnjIRLfca1i6KMyGCtsVgoKe/z1+6vukgaENdgGBZt+ZmKPc4gavvEZ5OgHfHdrazhgNyG7w==", "dev": true, "license": "MIT", "dependencies": { - "ansi-regex": "^6.0.1" + "ansi-regex": "^6.2.2" }, "engines": { "node": ">=12" @@ -6503,6 +6832,17 @@ "funding": { "url": "https://github.com/sponsors/sindresorhus" } + }, + "node_modules/zod": { + "version": "3.25.76", + "resolved": "https://registry.npmjs.org/zod/-/zod-3.25.76.tgz", + "integrity": "sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==", + "license": "MIT", + "optional": true, + "peer": true, + "funding": { + "url": "https://github.com/sponsors/colinhacks" + } } } } diff --git a/package.json b/package.json index 0d7f952..a9bd635 100644 --- a/package.json +++ b/package.json @@ -12,7 +12,9 @@ "format:check": "prettier --check ." }, "dependencies": { - "next": "14.1.0", + "@anthropic-ai/sdk": "^0.78.0", + "@google/generative-ai": "^0.24.1", + "next": "^14.2.35", "openai": "^4.28.0", "react": "^18.3.1", "react-dom": "^18.3.1" @@ -25,7 +27,7 @@ "@typescript-eslint/parser": "^8.48.0", "autoprefixer": "^10.4.16", "eslint": "8.57.1", - "eslint-config-next": "14.2.18", + "eslint-config-next": "^14.1.0", "eslint-config-prettier": "^9.1.0", "husky": "^9.1.7", "postcss": "^8.4.32", From 9922920a4c01e669b416b17e41b26532a3a66041 Mon Sep 17 00:00:00 2001 From: "Felix F." Date: Wed, 11 Mar 2026 23:40:54 +0000 Subject: [PATCH 02/10] feat: add LLM provider abstraction layer --- lib/llm/anthropic.ts | 31 ++++++++++++++++++++ lib/llm/errors.ts | 36 +++++++++++++++++++++++ lib/llm/gemini.ts | 41 ++++++++++++++++++++++++++ lib/llm/index.ts | 32 ++++++++++++++++++++ lib/llm/openai.ts | 22 ++++++++++++++ lib/llm/types.ts | 69 ++++++++++++++++++++++++++++++++++++++++++++ 6 files changed, 231 insertions(+) create mode 100644 lib/llm/anthropic.ts create mode 100644 lib/llm/errors.ts create mode 100644 lib/llm/gemini.ts create mode 100644 lib/llm/index.ts create mode 100644 lib/llm/openai.ts create mode 100644 lib/llm/types.ts diff --git a/lib/llm/anthropic.ts b/lib/llm/anthropic.ts new file mode 100644 index 0000000..d9f58da --- /dev/null +++ b/lib/llm/anthropic.ts @@ -0,0 +1,31 @@ +import Anthropic from "@anthropic-ai/sdk"; +import type { ChatMessage, CompletionOptions, LLMProvider } from "./types"; + +export class AnthropicProvider implements LLMProvider { + private client: Anthropic; + private defaultModel: string; + + constructor(apiKey: string, defaultModel = "claude-sonnet-4-20250514") { + this.client = new Anthropic({ apiKey }); + this.defaultModel = defaultModel; + } + + async chatCompletion(messages: ChatMessage[], options?: CompletionOptions): Promise { + // Anthropic requires the system message to be passed separately + const systemMessage = messages.find((m) => m.role === "system"); + const nonSystemMessages = messages.filter((m) => m.role !== "system"); + + const response = await this.client.messages.create({ + model: options?.model ?? this.defaultModel, + max_tokens: 4096, + system: systemMessage?.content, + messages: nonSystemMessages.map((m) => ({ + role: m.role as "user" | "assistant", + content: m.content, + })), + }); + + const textBlock = response.content.find((block) => block.type === "text"); + return textBlock && textBlock.type === "text" ? textBlock.text : ""; + } +} diff --git a/lib/llm/errors.ts b/lib/llm/errors.ts new file mode 100644 index 0000000..91f6644 --- /dev/null +++ b/lib/llm/errors.ts @@ -0,0 +1,36 @@ +export interface NormalizedLLMError { + status: number; + message: string; + isAuthError: boolean; +} + +const AUTH_ERROR_PATTERNS = [ + "401", + "unauthorized", + "invalid api key", + "invalid x-goog-api-key", + "api key not valid", + "authentication", + "permission denied", + "api_key_invalid", +]; + +export function normalizeProviderError(error: unknown): NormalizedLLMError { + const err = error as Record; + + const status = + typeof err?.status === "number" + ? err.status + : typeof err?.statusCode === "number" + ? err.statusCode + : 500; + + const message = typeof err?.message === "string" ? err.message : String(error); + + const isAuthError = + status === 401 || + status === 403 || + AUTH_ERROR_PATTERNS.some((pattern) => message.toLowerCase().includes(pattern)); + + return { status: isAuthError ? 401 : status, message, isAuthError }; +} diff --git a/lib/llm/gemini.ts b/lib/llm/gemini.ts new file mode 100644 index 0000000..b0e43c7 --- /dev/null +++ b/lib/llm/gemini.ts @@ -0,0 +1,41 @@ +import { GoogleGenerativeAI } from "@google/generative-ai"; +import type { ChatMessage, CompletionOptions, LLMProvider } from "./types"; + +export class GeminiProvider implements LLMProvider { + private genAI: GoogleGenerativeAI; + private defaultModel: string; + + constructor(apiKey: string, defaultModel = "gemini-2.0-flash") { + this.genAI = new GoogleGenerativeAI(apiKey); + this.defaultModel = defaultModel; + } + + async chatCompletion(messages: ChatMessage[], options?: CompletionOptions): Promise { + const modelName = options?.model ?? this.defaultModel; + const systemMessage = messages.find((m) => m.role === "system"); + const nonSystemMessages = messages.filter((m) => m.role !== "system"); + + const model = this.genAI.getGenerativeModel({ + model: modelName, + systemInstruction: systemMessage?.content, + generationConfig: { + temperature: options?.temperature ?? 0.7, + }, + }); + + if (nonSystemMessages.length === 0) { + throw new Error("At least one non-system message is required"); + } + + const chat = model.startChat({ + history: nonSystemMessages.slice(0, -1).map((m) => ({ + role: m.role === "assistant" ? "model" : "user", + parts: [{ text: m.content }], + })), + }); + + const lastMessage = nonSystemMessages[nonSystemMessages.length - 1]; + const result = await chat.sendMessage(lastMessage.content); + return result.response.text(); + } +} diff --git a/lib/llm/index.ts b/lib/llm/index.ts new file mode 100644 index 0000000..4a458a4 --- /dev/null +++ b/lib/llm/index.ts @@ -0,0 +1,32 @@ +import type { LLMProvider, ProviderKey } from "./types"; +import { OpenAIProvider } from "./openai"; +import { AnthropicProvider } from "./anthropic"; +import { GeminiProvider } from "./gemini"; + +export type { + LLMProvider, + ChatMessage, + CompletionOptions, + ProviderKey, + ProviderMeta, +} from "./types"; +export { PROVIDER_META, PROVIDER_KEYS, isValidProviderKey } from "./types"; +export { normalizeProviderError } from "./errors"; +export type { NormalizedLLMError } from "./errors"; + +export function createProvider( + providerKey: ProviderKey, + apiKey: string, + model?: string +): LLMProvider { + switch (providerKey) { + case "openai": + return new OpenAIProvider(apiKey, model); + case "anthropic": + return new AnthropicProvider(apiKey, model); + case "gemini": + return new GeminiProvider(apiKey, model); + default: + throw new Error(`Unsupported LLM provider: ${providerKey}`); + } +} diff --git a/lib/llm/openai.ts b/lib/llm/openai.ts new file mode 100644 index 0000000..c7e6d44 --- /dev/null +++ b/lib/llm/openai.ts @@ -0,0 +1,22 @@ +import { OpenAI } from "openai"; +import type { ChatMessage, CompletionOptions, LLMProvider } from "./types"; + +export class OpenAIProvider implements LLMProvider { + private client: OpenAI; + private defaultModel: string; + + constructor(apiKey: string, defaultModel = "gpt-4") { + this.client = new OpenAI({ apiKey }); + this.defaultModel = defaultModel; + } + + async chatCompletion(messages: ChatMessage[], options?: CompletionOptions): Promise { + const response = await this.client.chat.completions.create({ + model: options?.model ?? this.defaultModel, + messages, + temperature: options?.temperature ?? 0.7, + }); + + return response.choices[0].message.content || ""; + } +} diff --git a/lib/llm/types.ts b/lib/llm/types.ts new file mode 100644 index 0000000..e28669c --- /dev/null +++ b/lib/llm/types.ts @@ -0,0 +1,69 @@ +export interface ChatMessage { + role: "system" | "user" | "assistant"; + content: string; +} + +export interface CompletionOptions { + model?: string; + temperature?: number; +} + +export interface LLMProvider { + chatCompletion(messages: ChatMessage[], options?: CompletionOptions): Promise; +} + +export type ProviderKey = "openai" | "anthropic" | "gemini"; + +export interface ProviderMeta { + key: ProviderKey; + name: string; + icon: string; + keyPlaceholder: string; + keyHelpUrl: string; + keyHelpLabel: string; + defaultModel: string; + models: string[]; + validateKey: (key: string) => boolean; +} + +export const PROVIDER_META: Record = { + openai: { + key: "openai", + name: "OpenAI", + icon: "fa-robot", + keyPlaceholder: "sk-proj-...", + keyHelpUrl: "https://platform.openai.com/account/api-keys", + keyHelpLabel: "platform.openai.com/account/api-keys", + defaultModel: "gpt-4", + models: ["gpt-4", "gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano"], + validateKey: (key: string) => key.startsWith("sk-"), + }, + anthropic: { + key: "anthropic", + name: "Anthropic", + icon: "fa-brain", + keyPlaceholder: "sk-ant-...", + keyHelpUrl: "https://console.anthropic.com/settings/keys", + keyHelpLabel: "console.anthropic.com/settings/keys", + defaultModel: "claude-sonnet-4-20250514", + models: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-3-5-haiku-20241022"], + validateKey: (key: string) => key.startsWith("sk-ant-"), + }, + gemini: { + key: "gemini", + name: "Google Gemini", + icon: "fa-gem", + keyPlaceholder: "AIza...", + keyHelpUrl: "https://aistudio.google.com/apikey", + keyHelpLabel: "aistudio.google.com/apikey", + defaultModel: "gemini-2.0-flash", + models: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"], + validateKey: (key: string) => key.length > 10, + }, +}; + +export const PROVIDER_KEYS = Object.keys(PROVIDER_META) as ProviderKey[]; + +export function isValidProviderKey(value: unknown): value is ProviderKey { + return typeof value === "string" && PROVIDER_KEYS.includes(value as ProviderKey); +} From 5c1cafacbd4317e8bb648a0a3e1a4a54593e5b4c Mon Sep 17 00:00:00 2001 From: "Felix F." Date: Wed, 11 Mar 2026 23:41:51 +0000 Subject: [PATCH 03/10] feat: extend settings with provider and model fields --- lib/settings.ts | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/lib/settings.ts b/lib/settings.ts index f0a5527..aa63cae 100644 --- a/lib/settings.ts +++ b/lib/settings.ts @@ -1,6 +1,10 @@ import { TestType, TEST_TYPE_VALUES } from "./testTypes"; +import { PROVIDER_META, PROVIDER_KEYS } from "./llm/types"; +import type { ProviderKey } from "./llm/types"; export { TestType } from "./testTypes"; +export type { ProviderKey } from "./llm/types"; +export { PROVIDER_META, PROVIDER_KEYS } from "./llm/types"; export const boilerplateOptions = [ "vitest", @@ -20,6 +24,8 @@ export interface Settings { disabledTestTypes: Record; boilerplateSampleSize: number; disabledBoilerplates: Record; + provider: ProviderKey; + model: string; } export const SETTINGS_STORAGE_KEY = "tcg_settings"; @@ -43,9 +49,22 @@ export const defaultSettings: Settings = { }, {} as Record ), + provider: "openai", + model: PROVIDER_META.openai.defaultModel, }; export function sanitizeSettings(partial?: Partial): Settings { + const provider: ProviderKey = + partial?.provider && PROVIDER_KEYS.includes(partial.provider) + ? partial.provider + : defaultSettings.provider; + + const providerMeta = PROVIDER_META[provider]; + const model = + partial?.model && providerMeta.models.includes(partial.model) + ? partial.model + : providerMeta.defaultModel; + const merged: Settings = { defaultContext: partial?.defaultContext ?? defaultSettings.defaultContext, boilerplateSampleSize: @@ -60,6 +79,8 @@ export function sanitizeSettings(partial?: Partial): Settings { ...defaultSettings.disabledBoilerplates, ...(partial?.disabledBoilerplates ?? {}), }, + provider, + model, }; merged.boilerplateSampleSize = Math.min(5, Math.max(1, Math.round(merged.boilerplateSampleSize))); From 78005396eb21b3cd3fdae80f068e8eeade76e018 Mon Sep 17 00:00:00 2001 From: "Felix F." Date: Wed, 11 Mar 2026 23:43:29 +0000 Subject: [PATCH 04/10] refactor: update API routes to use LLM abstraction layer --- app/api/classify/route.ts | 51 +++++++++++++++-------------- app/api/generate-questions/route.ts | 34 ++++++++++--------- app/api/generate/route.ts | 40 +++++++++++++--------- 3 files changed, 68 insertions(+), 57 deletions(-) diff --git a/app/api/classify/route.ts b/app/api/classify/route.ts index f75e3b7..6c6c854 100644 --- a/app/api/classify/route.ts +++ b/app/api/classify/route.ts @@ -1,9 +1,9 @@ import { NextRequest, NextResponse } from "next/server"; -import { OpenAI } from "openai"; +import { createProvider, isValidProviderKey, normalizeProviderError } from "@/lib/llm"; export async function POST(req: NextRequest) { try { - const { projectDescription, apiKey } = await req.json(); + const { projectDescription, apiKey, provider = "openai", model } = await req.json(); if (!projectDescription) { return NextResponse.json({ error: "Project description required" }, { status: 400 }); @@ -13,9 +13,11 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "API key required" }, { status: 400 }); } - const openai = new OpenAI({ - apiKey: apiKey, - }); + if (!isValidProviderKey(provider)) { + return NextResponse.json({ error: `Unsupported provider: "${provider}"` }, { status: 400 }); + } + + const llm = createProvider(provider, apiKey, model); const categories = [ "backend-api", @@ -26,39 +28,38 @@ export async function POST(req: NextRequest) { "data-pipeline", ]; - const message = await openai.chat.completions.create({ - model: "gpt-4", - messages: [ - { - role: "system", - content: `You are a project classifier. Classify the given project description into one of these categories: ${categories.join( - ", " - )}. Respond with ONLY the category name, nothing else.`, - }, - { - role: "user", - content: projectDescription, - }, - ], - }); + const content = await llm.chatCompletion([ + { + role: "system", + content: `You are a project classifier. Classify the given project description into one of these categories: ${categories.join( + ", " + )}. Respond with ONLY the category name, nothing else.`, + }, + { + role: "user", + content: projectDescription, + }, + ]); - let category = (message.choices[0].message.content || "other").trim().toLowerCase(); + let category = (content || "other").trim().toLowerCase(); if (!categories.includes(category)) { category = "other"; } return NextResponse.json({ category }); - } catch (error: any) { + } catch (error: unknown) { console.error("Classification error:", error); - if (error.status === 401) { + const normalized = normalizeProviderError(error); + + if (normalized.isAuthError) { return NextResponse.json({ error: "Unauthorized: Invalid API key" }, { status: 401 }); } return NextResponse.json( - { error: "Classification failed", details: String(error.message) }, - { status: 500 } + { error: "Classification failed", details: normalized.message }, + { status: normalized.status } ); } } diff --git a/app/api/generate-questions/route.ts b/app/api/generate-questions/route.ts index 7fb4501..686193f 100644 --- a/app/api/generate-questions/route.ts +++ b/app/api/generate-questions/route.ts @@ -1,13 +1,14 @@ import { NextRequest, NextResponse } from "next/server"; -import { OpenAI } from "openai"; +import { createProvider, isValidProviderKey, normalizeProviderError } from "@/lib/llm"; export async function POST(req: NextRequest) { try { - const { projectDescription, category, apiKey } = await req.json(); + const { projectDescription, category, apiKey, provider = "openai", model } = await req.json(); console.log("Received:", { projectDescription: !!projectDescription, category, + provider, apiKey: !!apiKey, }); @@ -22,9 +23,11 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "API key required" }, { status: 400 }); } - const openai = new OpenAI({ - apiKey: apiKey, - }); + if (!isValidProviderKey(provider)) { + return NextResponse.json({ error: `Unsupported provider: "${provider}"` }, { status: 400 }); + } + + const llm = createProvider(provider, apiKey, model); const prompt = `You are a QA expert. Based on the following project description and category, generate exactly 10 specific, actionable questions that a developer should answer to help write comprehensive tests for this project. @@ -42,9 +45,8 @@ Format your response as a JSON array of strings, like this: Respond ONLY with the JSON array, no additional text or markdown.`; - const message = await openai.chat.completions.create({ - model: "gpt-4", - messages: [ + const content = await llm.chatCompletion( + [ { role: "system", content: @@ -55,10 +57,8 @@ Respond ONLY with the JSON array, no additional text or markdown.`; content: prompt, }, ], - temperature: 0.7, - }); - - const content = message.choices[0].message.content || "[]"; + { temperature: 0.7 } + ); let questions; try { @@ -84,16 +84,18 @@ Respond ONLY with the JSON array, no additional text or markdown.`; } return NextResponse.json({ questions }); - } catch (error: any) { + } catch (error: unknown) { console.error("Generate questions error:", error); - if (error.status === 401) { + const normalized = normalizeProviderError(error); + + if (normalized.isAuthError) { return NextResponse.json({ error: "Unauthorized: Invalid API key" }, { status: 401 }); } return NextResponse.json( - { error: "Failed to generate questions", details: String(error.message) }, - { status: 500 } + { error: "Failed to generate questions", details: normalized.message }, + { status: normalized.status } ); } } diff --git a/app/api/generate/route.ts b/app/api/generate/route.ts index 27fbf14..c087750 100644 --- a/app/api/generate/route.ts +++ b/app/api/generate/route.ts @@ -1,9 +1,16 @@ import { NextRequest, NextResponse } from "next/server"; -import { OpenAI } from "openai"; +import { createProvider, isValidProviderKey, normalizeProviderError } from "@/lib/llm"; export async function POST(req: NextRequest) { try { - const { projectDescription, category, answers, apiKey } = await req.json(); + const { + projectDescription, + category, + answers, + apiKey, + provider = "openai", + model, + } = await req.json(); if (!projectDescription || !category || !answers) { return NextResponse.json({ error: "Missing required fields" }, { status: 400 }); @@ -13,9 +20,11 @@ export async function POST(req: NextRequest) { return NextResponse.json({ error: "API key required" }, { status: 400 }); } - const openai = new OpenAI({ - apiKey: apiKey, - }); + if (!isValidProviderKey(provider)) { + return NextResponse.json({ error: `Unsupported provider: "${provider}"` }, { status: 400 }); + } + + const llm = createProvider(provider, apiKey, model); const prompt = `You are a QA expert. Generate comprehensive test cases for the following project: @@ -52,9 +61,8 @@ Generate 12-15 diverse test cases covering: Respond ONLY with valid JSON, no markdown or extra text.`; - const message = await openai.chat.completions.create({ - model: "gpt-4", - messages: [ + const content = await llm.chatCompletion( + [ { role: "system", content: @@ -65,10 +73,8 @@ Respond ONLY with valid JSON, no markdown or extra text.`; content: prompt, }, ], - temperature: 0.7, - }); - - const content = message.choices[0].message.content || "{}"; + { temperature: 0.7 } + ); let parsedResponse; try { @@ -83,16 +89,18 @@ Respond ONLY with valid JSON, no markdown or extra text.`; } return NextResponse.json(parsedResponse); - } catch (error: any) { + } catch (error: unknown) { console.error("Generation error:", error); - if (error.status === 401) { + const normalized = normalizeProviderError(error); + + if (normalized.isAuthError) { return NextResponse.json({ error: "Unauthorized: Invalid API key" }, { status: 401 }); } return NextResponse.json( - { error: "Test case generation failed", details: String(error.message) }, - { status: 500 } + { error: "Test case generation failed", details: normalized.message }, + { status: normalized.status } ); } } From e46f72a77c472d3e9875be23e4533b0f203e6b96 Mon Sep 17 00:00:00 2001 From: "Felix F." Date: Wed, 11 Mar 2026 23:45:27 +0000 Subject: [PATCH 05/10] feat: add provider selector UI and dynamic API key management --- app/page.tsx | 98 ++++++++++++++++++++++++++++++++++-------- app/questions/page.tsx | 10 ++++- app/settings/page.tsx | 58 ++++++++++++++++++++++++- 3 files changed, 144 insertions(+), 22 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index b7c0f2f..5d5ceee 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -10,6 +10,7 @@ import { buildAutoContext } from "@/lib/autoContext"; import { useSettings } from "@/components/SettingsProvider"; import { filterTestCasesBySettings } from "@/lib/testCaseUtils"; import { useThemeMode } from "@/components/ThemeProvider"; +import { PROVIDER_META, PROVIDER_KEYS } from "@/lib/settings"; export default function Home() { const [input, setInput] = useState(""); @@ -25,7 +26,7 @@ export default function Home() { const router = useRouter(); const { addToast } = useToast(); const { confirm } = useConfirmDialog(); - const { settings } = useSettings(); + const { settings, updateSettings } = useSettings(); const { themeMode, themeLabel, toggleTheme, mounted } = useThemeMode(); useEffect(() => { @@ -38,29 +39,35 @@ export default function Home() { return () => document.removeEventListener("mousedown", handleClickOutside); }, []); + const providerMeta = PROVIDER_META[settings.provider]; + const storageKeyName = `llm_api_key_${settings.provider}`; + useEffect(() => { - const savedKey = localStorage.getItem("openai_api_key"); + const savedKey = localStorage.getItem(storageKeyName); if (savedKey) { setApiKey(savedKey); setApiKeySaved(true); + } else { + setApiKey(""); + setApiKeySaved(false); } - }, []); + }, [storageKeyName]); const handleSaveApiKey = () => { if (!apiKey.trim()) { - addToast({ message: "Please enter your OpenAI API key.", type: "error" }); + addToast({ message: `Please enter your ${providerMeta.name} API key.`, type: "error" }); return; } - if (!apiKey.startsWith("sk-")) { + if (!providerMeta.validateKey(apiKey)) { addToast({ - message: 'Invalid API key. It should start with "sk-".', + message: `Invalid API key format for ${providerMeta.name}.`, type: "error", }); return; } - localStorage.setItem("openai_api_key", apiKey); + localStorage.setItem(storageKeyName, apiKey); setApiKeySaved(true); setShowApiKeyInput(false); addToast({ @@ -72,8 +79,7 @@ export default function Home() { const handleRemoveApiKey = async () => { const confirmed = await confirm({ title: "Remove API key?", - description: - "This will remove your saved OpenAI API key from this browser. You can add it again later.", + description: `This will remove your saved ${providerMeta.name} API key from this browser. You can add it again later.`, confirmLabel: "Remove key", variant: "danger", }); @@ -82,7 +88,7 @@ export default function Home() { return; } - localStorage.removeItem("openai_api_key"); + localStorage.removeItem(storageKeyName); setApiKey(""); setApiKeySaved(false); addToast({ message: "API key removed.", type: "info" }); @@ -114,12 +120,15 @@ export default function Home() { .filter(Boolean) .join("\n\n"); try { + const currentApiKey = localStorage.getItem(storageKeyName); const response = await fetch("/api/classify", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ projectDescription: classificationPayload, - apiKey: localStorage.getItem("openai_api_key"), + apiKey: currentApiKey, + provider: settings.provider, + model: settings.model, }), }); @@ -174,7 +183,9 @@ export default function Home() { projectDescription: generationPayload, category: normalizedCategory, answers: autoContext, - apiKey: localStorage.getItem("openai_api_key"), + apiKey: currentApiKey, + provider: settings.provider, + model: settings.model, }), }); @@ -268,13 +279,62 @@ export default function Home() { )} - {/* API Key Section */} + {/* Provider & API Key Section */}
+ {/* Provider selector */} +
+ +
+ {PROVIDER_KEYS.map((key) => { + const meta = PROVIDER_META[key]; + const isActive = settings.provider === key; + return ( + + ); + })} +
+
+ + {/* Model selector */} +
+ + +
+ + {/* API key status / input */}

- OpenAI API Key + {providerMeta.name} API Key

{apiKeySaved ? (

@@ -284,7 +344,7 @@ export default function Home() { ) : (

- Please add your OpenAI API key to continue + Please add your {providerMeta.name} API key to continue

)}
@@ -299,21 +359,21 @@ export default function Home() { {showApiKeyInput && (

- Get your free API key at{" "} + Get your API key at{" "} - platform.openai.com/account/api-keys + {providerMeta.keyHelpLabel}

setApiKey(e.target.value)} - placeholder="sk-proj-..." + placeholder={providerMeta.keyPlaceholder} className="w-full p-3 border-2 border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white text-sm" />

diff --git a/app/questions/page.tsx b/app/questions/page.tsx index 766881b..f187602 100644 --- a/app/questions/page.tsx +++ b/app/questions/page.tsx @@ -38,7 +38,8 @@ export default function QuestionsPage() { // Fetch custom questions based on project description setGeneratingQuestions(true); try { - const apiKey = localStorage.getItem("openai_api_key"); + const storageKeyName = `llm_api_key_${settings.provider}`; + const apiKey = localStorage.getItem(storageKeyName); const response = await fetch("/api/generate-questions", { method: "POST", @@ -47,6 +48,8 @@ export default function QuestionsPage() { projectDescription: savedProject, category: savedCategory, apiKey: apiKey, + provider: settings.provider, + model: settings.model, }), }); @@ -93,7 +96,8 @@ export default function QuestionsPage() { setLoading(true); try { - const apiKey = localStorage.getItem("openai_api_key"); + const storageKeyName = `llm_api_key_${settings.provider}`; + const apiKey = localStorage.getItem(storageKeyName); const supplementalContext: string[] = []; if (projectRequirements.trim()) { @@ -111,6 +115,8 @@ export default function QuestionsPage() { category, answers: [...answers, ...supplementalContext], apiKey: apiKey, + provider: settings.provider, + model: settings.model, }), }); diff --git a/app/settings/page.tsx b/app/settings/page.tsx index 7c490c4..440bb68 100644 --- a/app/settings/page.tsx +++ b/app/settings/page.tsx @@ -4,7 +4,13 @@ import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { useSettings } from "@/components/SettingsProvider"; import { useToast } from "@/components/ToastProvider"; -import { boilerplateOptions, defaultSettings, testTypeOptions } from "@/lib/settings"; +import { + boilerplateOptions, + defaultSettings, + testTypeOptions, + PROVIDER_META, + PROVIDER_KEYS, +} from "@/lib/settings"; export default function SettingsPage() { const router = useRouter(); @@ -46,6 +52,56 @@ export default function SettingsPage() {

+
+

LLM Provider

+

+ Choose which AI provider and model to use for test generation. +

+
+ {PROVIDER_KEYS.map((key) => { + const meta = PROVIDER_META[key]; + const isActive = formState.provider === key; + return ( + + ); + })} +
+
+ + +
+
+

Default context From 3473bd2e4e756622d166ec8e1d127319b3ec9ae6 Mon Sep 17 00:00:00 2001 From: "Felix F." Date: Thu, 12 Mar 2026 00:35:22 +0000 Subject: [PATCH 06/10] fix: resolve Gemini model loading and API version compatibility --- app/page.tsx | 2 +- lib/llm/gemini.ts | 51 +++++++++++++++++++++++++++++++++-------------- lib/llm/types.ts | 18 +++++++++++------ 3 files changed, 49 insertions(+), 22 deletions(-) diff --git a/app/page.tsx b/app/page.tsx index 5d5ceee..01e7aa7 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -145,7 +145,7 @@ export default function Home() { if (!response.ok) { addToast({ - message: `Error: ${data.error || "Failed to classify project"}`, + message: `Error: ${data.error || "Failed to classify project"}${data.details ? ` (${data.details})` : ""}`, type: "error", }); return; diff --git a/lib/llm/gemini.ts b/lib/llm/gemini.ts index b0e43c7..7cfab8b 100644 --- a/lib/llm/gemini.ts +++ b/lib/llm/gemini.ts @@ -15,27 +15,48 @@ export class GeminiProvider implements LLMProvider { const systemMessage = messages.find((m) => m.role === "system"); const nonSystemMessages = messages.filter((m) => m.role !== "system"); - const model = this.genAI.getGenerativeModel({ - model: modelName, - systemInstruction: systemMessage?.content, - generationConfig: { - temperature: options?.temperature ?? 0.7, + const model = this.genAI.getGenerativeModel( + { + model: modelName, + generationConfig: { + temperature: options?.temperature ?? 0.7, + }, }, - }); + { apiVersion: "v1beta" } + ); if (nonSystemMessages.length === 0) { throw new Error("At least one non-system message is required"); } - const chat = model.startChat({ - history: nonSystemMessages.slice(0, -1).map((m) => ({ - role: m.role === "assistant" ? "model" : "user", - parts: [{ text: m.content }], - })), - }); + try { + // Prepend system message content to the first message if it exists + let firstMessageContent = nonSystemMessages[0].content; + if (systemMessage) { + firstMessageContent = `[SYSTEM INSTRUCTION]\n${systemMessage.content}\n\n[USER INPUT]\n${firstMessageContent}`; + } - const lastMessage = nonSystemMessages[nonSystemMessages.length - 1]; - const result = await chat.sendMessage(lastMessage.content); - return result.response.text(); + // Use simpler generateContent for single-turn requests + if (nonSystemMessages.length === 1) { + const result = await model.generateContent(firstMessageContent); + return result.response.text(); + } + + // Use chat for multi-turn requests + const chat = model.startChat({ + history: nonSystemMessages.slice(0, -1).map((m) => ({ + role: m.role === "assistant" ? "model" : "user", + parts: [{ text: m.content }], + })), + }); + + const lastMessage = nonSystemMessages[nonSystemMessages.length - 1]; + const result = await chat.sendMessage(lastMessage.content); + return result.response.text(); + } catch (error: any) { + console.error("Gemini API Error:", error); + // Re-throw to be caught by the route handler + throw error; + } } } diff --git a/lib/llm/types.ts b/lib/llm/types.ts index e28669c..3aeab26 100644 --- a/lib/llm/types.ts +++ b/lib/llm/types.ts @@ -34,8 +34,8 @@ export const PROVIDER_META: Record = { keyPlaceholder: "sk-proj-...", keyHelpUrl: "https://platform.openai.com/account/api-keys", keyHelpLabel: "platform.openai.com/account/api-keys", - defaultModel: "gpt-4", - models: ["gpt-4", "gpt-4o", "gpt-4o-mini", "gpt-4.1", "gpt-4.1-mini", "gpt-4.1-nano"], + defaultModel: "gpt-4o-mini", + models: ["gpt-4o", "gpt-4o-mini", "gpt-4-turbo", "gpt-4", "gpt-3.5-turbo"], validateKey: (key: string) => key.startsWith("sk-"), }, anthropic: { @@ -45,8 +45,14 @@ export const PROVIDER_META: Record = { keyPlaceholder: "sk-ant-...", keyHelpUrl: "https://console.anthropic.com/settings/keys", keyHelpLabel: "console.anthropic.com/settings/keys", - defaultModel: "claude-sonnet-4-20250514", - models: ["claude-sonnet-4-20250514", "claude-opus-4-20250514", "claude-3-5-haiku-20241022"], + defaultModel: "claude-3-5-sonnet-latest", + models: [ + "claude-3-5-sonnet-latest", + "claude-3-5-haiku-latest", + "claude-3-opus-latest", + "claude-3-sonnet-20240229", + "claude-3-haiku-20240307", + ], validateKey: (key: string) => key.startsWith("sk-ant-"), }, gemini: { @@ -56,8 +62,8 @@ export const PROVIDER_META: Record = { keyPlaceholder: "AIza...", keyHelpUrl: "https://aistudio.google.com/apikey", keyHelpLabel: "aistudio.google.com/apikey", - defaultModel: "gemini-2.0-flash", - models: ["gemini-2.5-pro", "gemini-2.5-flash", "gemini-2.0-flash"], + defaultModel: "gemini-2.5-flash", + models: ["gemini-2.5-flash", "gemini-2.5-pro", "gemini-2.0-flash", "gemini-flash-latest"], validateKey: (key: string) => key.length > 10, }, }; From ad1f43e62cf30f757d916c8621b8819f633ac1a1 Mon Sep 17 00:00:00 2001 From: "Felix F." Date: Thu, 12 Mar 2026 00:41:58 +0000 Subject: [PATCH 07/10] docs: remove .env requirement and update installation instructions --- .env.example | 3 --- README.md | 21 ++++++++++----------- 2 files changed, 10 insertions(+), 14 deletions(-) delete mode 100644 .env.example diff --git a/.env.example b/.env.example deleted file mode 100644 index 61c6c81..0000000 --- a/.env.example +++ /dev/null @@ -1,3 +0,0 @@ -# OpenAI API Key -# Get your key at: https://platform.openai.com/account/api-keys -OPENAI_API_KEY=sk_your_api_key_here \ No newline at end of file diff --git a/README.md b/README.md index 5250ae8..7f93232 100644 --- a/README.md +++ b/README.md @@ -37,27 +37,26 @@ Assertify is an intelligent testing assistant that analyzes your project descrip - Next.js 14 - TypeScript - Tailwind CSS -- OpenAI API +- Multi-LLM Support (OpenAI, Anthropic Claude, Google Gemini) ## Installation 1. Install dependencies: `npm install` -2. Copy the environment file: `cp .env.example .env.local` -3. Add your OpenAI API key to `.env.local` -4. Start the dev server: `npm run dev` -5. Open http://localhost:3000 in your browser +2. Start the dev server: `npm run dev` +3. Open http://localhost:3000 in your browser +4. **Enter your API key securely directly in the UI** (Keys are stored locally in your browser, no `.env` file required!) ## How to Use -1. Provide your project description from the landing page; the app automatically classifies the category. -2. Answer the context questions (or skip) so the generator can tailor scenarios to your needs. -3. Review generated tests on the results page, filter by type or priority, and inspect the suggested testing strategy and risk areas. -4. Generate boilerplate code for your preferred frameworks or export the dataset as JSON/CSV. -5. Manage settings at `/settings` to define default context, disable frameworks, and control boilerplate sample sizes. +1. Select your preferred LLM provider (OpenAI, Anthropic, or Gemini) and model from the landing page or settings. +2. Provide your project description from the landing page; the app automatically classifies the category. +3. Answer the context questions (or skip) so the generator can tailor scenarios to your needs. +4. Review generated tests on the results page, filter by type or priority, and inspect the suggested testing strategy and risk areas. +5. Generate boilerplate code for your preferred frameworks or export the dataset as JSON/CSV. +6. Manage settings at `/settings` to define default context, disable frameworks, and control boilerplate sample sizes. ## Future Improvements -- Allow selecting different LLM providers and models per generation so teams can optimize for latency or cost. - Offer more granular configuration for question generation (e.g., required question count, tone, or domain presets). - Optimize large test suites by streaming responses and deduplicating similar scenarios before persistence. - Provide deeper integrations with CI/CD by exporting ready-to-run suites or syncing with test management tools. From 923f3d108c2ce1cc6eb6d6440ed928798aae6e14 Mon Sep 17 00:00:00 2001 From: "Felix F." Date: Thu, 12 Mar 2026 01:07:16 +0000 Subject: [PATCH 08/10] fix: extract human-readable error messages from nested vendor SDK JSON payloads --- lib/llm/errors.ts | 18 +++++++++++++++++- 1 file changed, 17 insertions(+), 1 deletion(-) diff --git a/lib/llm/errors.ts b/lib/llm/errors.ts index 91f6644..210840d 100644 --- a/lib/llm/errors.ts +++ b/lib/llm/errors.ts @@ -25,7 +25,23 @@ export function normalizeProviderError(error: unknown): NormalizedLLMError { ? err.statusCode : 500; - const message = typeof err?.message === "string" ? err.message : String(error); + let message = typeof err?.message === "string" ? err.message : String(error); + + // Sometimes SDKs return a JSON payload embedded in the error string. + try { + const jsonStart = message.indexOf("{"); + const jsonEnd = message.lastIndexOf("}"); + if (jsonStart !== -1 && jsonEnd > jsonStart) { + const parsed = JSON.parse(message.substring(jsonStart, jsonEnd + 1)); + if (parsed.error && typeof parsed.error.message === "string") { + message = parsed.error.message; + } else if (typeof parsed.message === "string") { + message = parsed.message; + } + } + } catch { + // Ignore parsing errors, stick to the original message + } const isAuthError = status === 401 || From 20d45b0dbabb0192a68ae762602f02b3fe57088f Mon Sep 17 00:00:00 2001 From: "Felix F." Date: Sat, 14 Mar 2026 00:05:50 +0000 Subject: [PATCH 09/10] Feedback Implementation --- README.md | 2 +- app/page.tsx | 11 ++++++----- app/questions/page.tsx | 6 +++--- lib/llm/anthropic.ts | 2 +- lib/llm/gemini.ts | 6 +++--- lib/llm/index.ts | 21 +++++++++++++++------ lib/llm/openai.ts | 2 +- package.json | 2 +- 8 files changed, 31 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 7f93232..7c06ebb 100644 --- a/README.md +++ b/README.md @@ -44,7 +44,7 @@ Assertify is an intelligent testing assistant that analyzes your project descrip 1. Install dependencies: `npm install` 2. Start the dev server: `npm run dev` 3. Open http://localhost:3000 in your browser -4. **Enter your API key securely directly in the UI** (Keys are stored locally in your browser, no `.env` file required!) +4. **Enter your API key in the UI**. Keys are stored in your browser's `sessionStorage` in plaintext for convenience; they are not encrypted or transmitted to our servers. Be aware that browser extensions or any cross-site scripting (XSS) vulnerability in this tab could potentially access this session data. ## How to Use diff --git a/app/page.tsx b/app/page.tsx index 01e7aa7..8898d29 100644 --- a/app/page.tsx +++ b/app/page.tsx @@ -43,7 +43,7 @@ export default function Home() { const storageKeyName = `llm_api_key_${settings.provider}`; useEffect(() => { - const savedKey = localStorage.getItem(storageKeyName); + const savedKey = sessionStorage.getItem(storageKeyName); if (savedKey) { setApiKey(savedKey); setApiKeySaved(true); @@ -67,7 +67,7 @@ export default function Home() { return; } - localStorage.setItem(storageKeyName, apiKey); + sessionStorage.setItem(storageKeyName, apiKey); setApiKeySaved(true); setShowApiKeyInput(false); addToast({ @@ -88,7 +88,7 @@ export default function Home() { return; } - localStorage.removeItem(storageKeyName); + sessionStorage.removeItem(storageKeyName); setApiKey(""); setApiKeySaved(false); addToast({ message: "API key removed.", type: "info" }); @@ -120,7 +120,7 @@ export default function Home() { .filter(Boolean) .join("\n\n"); try { - const currentApiKey = localStorage.getItem(storageKeyName); + const currentApiKey = sessionStorage.getItem(storageKeyName); const response = await fetch("/api/classify", { method: "POST", headers: { "Content-Type": "application/json" }, @@ -377,7 +377,8 @@ export default function Home() { className="w-full p-3 border-2 border-slate-300 dark:border-slate-600 rounded-lg focus:outline-none focus:ring-2 focus:ring-blue-500 dark:bg-slate-700 dark:text-white text-sm" />

- Your API key is stored locally in your browser only. Never shared with us. + Your API key is kept locally for the current session. Note: Browser storage is not + encrypted.