diff --git a/.gitignore b/.gitignore index 5ef6a52..6d805f8 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,4 @@ yarn-error.log* # typescript *.tsbuildinfo next-env.d.ts +.env diff --git a/package-lock.json b/package-lock.json index 8a89218..bb41e39 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,12 @@ "name": "ai-job-search-app", "version": "0.1.0", "dependencies": { + "@prisma/client": "^6.19.3", + "gray-matter": "^4.0.3", + "marked": "^18.0.9", "next": "16.3.1", + "openai": "^7.4.0", + "prisma": "^6.19.3", "react": "19.2.8", "react-dom": "19.2.8" }, @@ -20,6 +25,7 @@ "eslint": "^9", "eslint-config-next": "16.3.1", "tailwindcss": "^4", + "tsx": "^4.23.12", "typescript": "^5" } }, @@ -309,6 +315,448 @@ "tslib": "^2.4.0" } }, + "node_modules/@esbuild/aix-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.28.2.tgz", + "integrity": "sha512-XExcO+dvLKvVtNTibSTBej1NCAbaGhWn9Ww1ZPx80qsahhPFe/8jgWP0IchNe0F3HwkU7n8ejhH8bjonqht8mQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "aix" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.28.2.tgz", + "integrity": "sha512-kXXoiPVVGQcnIYGOeaovwOURpniDBpSq4A03qkQ+BMQqtGG6HYap3xne9C1O1yo4TR3qxlCX5IqqmX6fFo2Lqg==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.28.2.tgz", + "integrity": "sha512-5YfKeeI8qWfBZIX+u2xZC3Zlb3Os/gLS2sbEKM+I4ZOcsWmHS2WLysCcQZDAFRslDUU5Oiq44gf6PYN1vGwG5A==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/android-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.28.2.tgz", + "integrity": "sha512-O387ite7SzUyCcy3JQX4P4bLtEA7bLLkx+esve5JHnyYfNTxcVpXZo9jhdB0lTKN44gztELTdU7nS8Nr16Fs1Q==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.28.2.tgz", + "integrity": "sha512-n4KqkOQrraxHJcgjM1RvwbigfQKIKJVpM7xp+KsxiyUSrRdIXnt73VhrPAx0fV44hgfmIVKjxMN9J1t5jySVkw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/darwin-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.28.2.tgz", + "integrity": "sha512-uq6suIWYP37qzGddBKPw5QEQPi6HiLGsO7UmkpfyaYNQ3D+rN6w6WfwH+nuqcGXWvawGwxOEroO4YGnFh95azw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.28.2.tgz", + "integrity": "sha512-n+I0BTSRIoy+d6RPKnEVwql5UwBJolytvY4mAOIEJorKlqgPII8ix6slVVrfZ5Tnj7glIZvloylbB/EJPMWEXw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/freebsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.28.2.tgz", + "integrity": "sha512-78XJTJkvPs0kz2w61301PJjXl4g7q3JqiYMZ/M/yVI73EHBrCRTgkhu9oqG7vPqq+a/yadEW8aD+agKlk5xrmg==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.28.2.tgz", + "integrity": "sha512-XlDnu2q5yoqems+xay6wSAcg9DDD7K9RLKZEBOMZm3ckNpJBvOX20tSfby8KfrrhINDyv9V2YVZKY/SpoGJI8w==", + "cpu": [ + "arm" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.28.2.tgz", + "integrity": "sha512-pW4AC0P3it8c7do9MVM4p51FzHzdM/TZrerurgRcHJ2WTa1VQ1CIq18xncfpBJw4ojkiZZrKW2yIBWBP92j6Ug==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.28.2.tgz", + "integrity": "sha512-CYbnj78HsIeA+DhgUKgFCfvNsTHFhMMrinUrMZpDXJXKN8T3XViTZ/+wtHeVxEWY8ewSzTFN+nRmSwO2tZaLUQ==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-loong64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.28.2.tgz", + "integrity": "sha512-buwkd8nsph4R+ajRvw0qM5Hja/TXQow3ptzWO2EbG/cqcIkHloRrdlBtQlshyYGTNFvfkfJ5tpPLVkY4DtsPfQ==", + "cpu": [ + "loong64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-mips64el": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.28.2.tgz", + "integrity": "sha512-ZVykbDyk7519VwiNb9Lcj9m8XM6v5V9uKPvrEMkkEedVewf+0itkhahp4HDpgERXhwLRpWFypsGbG/J8s0QjJA==", + "cpu": [ + "mips64el" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-ppc64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.28.2.tgz", + "integrity": "sha512-CAXl+Dtd9UUuJd8pKKdwh6MLm3MUMiqMPmhZ3tTSXPqfyQ3vDl6R5hZdZ/kYojK4ofXtdfSv1tFq8XzWx3heNQ==", + "cpu": [ + "ppc64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-riscv64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.28.2.tgz", + "integrity": "sha512-GeXCej4IQtU1B+QlDV8W/RRvbzI3O/Stss+/bCXv4lZls5WGRtu2a+3JkA3i4qIUlMXpcHebWpF8AkJhATowuA==", + "cpu": [ + "riscv64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-s390x": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.28.2.tgz", + "integrity": "sha512-3H1weTYZPxt/WOhByszQZybS9w5lKzUn1FDMsgEChbHWQwHYQQRfBxgCcZvPhjHfKyJjIievvMmEUawJrdY9Dg==", + "cpu": [ + "s390x" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/linux-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.28.2.tgz", + "integrity": "sha512-4xTZr1FUmSoQW4XIWmit3tzQrUTZM+N3P0XV8xROKYF50XfI7xeO90+1bZvNwxIufQ9hDQVRJH5YhgPVF8A/HQ==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.28.2.tgz", + "integrity": "sha512-sSATRjPeDBg3pdgHoQfoYBob11Kk1FGa9lui5RIHZCoCkJa9QKlvl3/vKz2usCmYYjs7ymJR/2Nnsqe+Hjt5nw==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/netbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.28.2.tgz", + "integrity": "sha512-lqnzCV+mM0gIADaKihiCg6ifgfU2L3h5E33rNQBN1Y4MaVGnzryzmvvf7UHxprpQdE8hpqLolJ9Rl+SkIRDpyw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "netbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.28.2.tgz", + "integrity": "sha512-AL2qJILH7lNjrDmCQDvdxMfAUIv8KMNZOvrwAQ8i8//ntL9FflhOyMJ8OZSMBb8/AWXe3/5v5S20y3zCoZWKoQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openbsd-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.28.2.tgz", + "integrity": "sha512-QtiuPytchRyC4rwUKhexJdQKvDuZ6hWloi3igqPQNUJCS1/v9EiO3UTOXR6A3FoMo4fnAKbWJdqaIwhOzh8qEw==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openbsd" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/openharmony-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.28.2.tgz", + "integrity": "sha512-WkhYDmpTjLvGlScA1rwjRUmhl4k8oXR3cIbtqWmELgU/dFeHHlEllxDvdWcNJV9rbzCexB5vz8gtNewWLgCT7Q==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "openharmony" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/sunos-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.28.2.tgz", + "integrity": "sha512-GPMSkTOtMnv2U2F8gxe4Io6qmVs+YKyp832Etqqxr0hFngmXQ3rzwytelm3GIn7T4VviRUlf3sOgBOiTdvaf7g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "sunos" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-arm64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.28.2.tgz", + "integrity": "sha512-PIhhEkE9uPBleRBrQEJpUn7MBnibZzbGzYWPmY3x+YoVg/95zbjB4CxPPOQ8l5tYYM4mMaCthF8/1DIfBQQyWQ==", + "cpu": [ + "arm64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-ia32": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.28.2.tgz", + "integrity": "sha512-YmJbfTlvU7Sdn9BB+4PRES4oB6pxgS37MAONj+hBr/cpXS1aBPKXxNnDbu+QCWPj0o9dgyxeq79g6c5P8KeuYA==", + "cpu": [ + "ia32" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, + "node_modules/@esbuild/win32-x64": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.28.2.tgz", + "integrity": "sha512-5ebpxr3nWMzrL/rnUI755Jkuee0bHL/Gq0WTF9lvcpv73wAp5eu8MfBUgWK9bhWvZjj7yX8etf/8tI8Ney695g==", + "cpu": [ + "x64" + ], + "dev": true, + "license": "MIT", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">=18" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.10.1", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.10.1.tgz", @@ -1377,6 +1825,85 @@ "node": ">=12.4.0" } }, + "node_modules/@prisma/client": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.19.3.tgz", + "integrity": "sha512-mKq3jQFhjvko5LTJFHGilsuQs+W+T3Gm451NzuTDGQxwCzwXHYnIu2zGkRoW+Exq3Rob7yp2MfzSrdIiZVhrBg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "prisma": "*", + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "prisma": { + "optional": true + }, + "typescript": { + "optional": true + } + } + }, + "node_modules/@prisma/config": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/config/-/config-6.19.3.tgz", + "integrity": "sha512-CBPT44BjlQxEt8kiMEauji2WHTDoVBOKl7UlewXmUgBPnr/oPRZC3psci5chJnYmH0ivEIog2OU9PGWoki3DLQ==", + "license": "Apache-2.0", + "dependencies": { + "c12": "3.1.0", + "deepmerge-ts": "7.1.5", + "effect": "3.21.0", + "empathic": "2.0.0" + } + }, + "node_modules/@prisma/debug": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.19.3.tgz", + "integrity": "sha512-ljkJ+SgpXNktLG0Q/n4JGYCkKf0f8oYLyjImS2I8e2q2WCfdRRtWER062ZV/ixaNP2M2VKlWXVJiGzZaUgbKZw==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/engines": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.19.3.tgz", + "integrity": "sha512-RSYxtlYFl5pJ8ZePgMv0lZ9IzVCOdTPOegrs2qcbAEFrBI1G33h6wyC9kjQvo0DnYEhEVY0X4LsuFHXLKQk88g==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/fetch-engine": "6.19.3", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/engines-version": { + "version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7.tgz", + "integrity": "sha512-03bgb1VD5gvuumNf+7fVGBzfpJPjmqV423l/WxsWk2cNQ42JD0/SsFBPhN6z8iAvdHs07/7ei77SKu7aZfq8bA==", + "license": "Apache-2.0" + }, + "node_modules/@prisma/fetch-engine": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.19.3.tgz", + "integrity": "sha512-tKtl/qco9Nt7LU5iKhpultD8O4vMCZcU2CHjNTnRrL1QvSUr5W/GcyFPjNL87GtRrwBc7ubXXD9xy4EvLvt8JA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3", + "@prisma/engines-version": "7.1.1-3.c2990dca591cba766e3b7ef5d9e8a84796e47ab7", + "@prisma/get-platform": "6.19.3" + } + }, + "node_modules/@prisma/get-platform": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.19.3.tgz", + "integrity": "sha512-xFj1VcJ1N3MKooOQAGO0W5tsd0W2QzIvW7DD7c/8H14Zmp4jseeWAITm+w2LLoLrlhoHdPPh0NMZ8mfL6puoHA==", + "license": "Apache-2.0", + "dependencies": { + "@prisma/debug": "6.19.3" + } + }, "node_modules/@rtsao/scc": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/@rtsao/scc/-/scc-1.1.0.tgz", @@ -1384,6 +1911,12 @@ "dev": true, "license": "MIT" }, + "node_modules/@standard-schema/spec": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@standard-schema/spec/-/spec-1.1.0.tgz", + "integrity": "sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==", + "license": "MIT" + }, "node_modules/@swc/helpers": { "version": "0.5.23", "resolved": "https://registry.npmjs.org/@swc/helpers/-/helpers-0.5.23.tgz", @@ -2750,6 +3283,34 @@ "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" } }, + "node_modules/c12": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/c12/-/c12-3.1.0.tgz", + "integrity": "sha512-uWoS8OU1MEIsOv8p/5a82c3H31LsWVR5qiyXVfBNOzfffjUWtPnhAb4BYI2uG2HfGmZmFjCtui5XNWaps+iFuw==", + "license": "MIT", + "dependencies": { + "chokidar": "^4.0.3", + "confbox": "^0.2.2", + "defu": "^6.1.4", + "dotenv": "^16.6.1", + "exsolve": "^1.0.7", + "giget": "^2.0.0", + "jiti": "^2.4.2", + "ohash": "^2.0.11", + "pathe": "^2.0.3", + "perfect-debounce": "^1.0.0", + "pkg-types": "^2.2.0", + "rc9": "^2.1.2" + }, + "peerDependencies": { + "magicast": "^0.3.5" + }, + "peerDependenciesMeta": { + "magicast": { + "optional": true + } + } + }, "node_modules/call-bind": { "version": "1.0.9", "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.9.tgz", @@ -2847,6 +3408,30 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chokidar": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz", + "integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==", + "license": "MIT", + "dependencies": { + "readdirp": "^4.0.1" + }, + "engines": { + "node": ">= 14.16.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + } + }, + "node_modules/citty": { + "version": "0.1.6", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.1.6.tgz", + "integrity": "sha512-tskPPKEs8D2KPafUypv2gxwJP8h/OaJmC82QQGGDQcHvXX43xF2VDACcJVmZ0EuSxkpO9Kc4MlrA3q0+FG58AQ==", + "license": "MIT", + "dependencies": { + "consola": "^3.2.3" + } + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -2880,6 +3465,21 @@ "dev": true, "license": "MIT" }, + "node_modules/confbox": { + "version": "0.2.4", + "resolved": "https://registry.npmjs.org/confbox/-/confbox-0.2.4.tgz", + "integrity": "sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==", + "license": "MIT" + }, + "node_modules/consola": { + "version": "3.4.2", + "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", + "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", + "license": "MIT", + "engines": { + "node": "^14.18.0 || >=16.10.0" + } + }, "node_modules/convert-source-map": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", @@ -2995,6 +3595,15 @@ "dev": true, "license": "MIT" }, + "node_modules/deepmerge-ts": { + "version": "7.1.5", + "resolved": "https://registry.npmjs.org/deepmerge-ts/-/deepmerge-ts-7.1.5.tgz", + "integrity": "sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==", + "license": "BSD-3-Clause", + "engines": { + "node": ">=16.0.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -3031,6 +3640,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/defu": { + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", + "license": "MIT" + }, + "node_modules/destr": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/destr/-/destr-2.0.5.tgz", + "integrity": "sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==", + "license": "MIT" + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -3054,6 +3675,18 @@ "node": ">=0.10.0" } }, + "node_modules/dotenv": { + "version": "16.6.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.6.1.tgz", + "integrity": "sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==", + "license": "BSD-2-Clause", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://dotenvx.com" + } + }, "node_modules/dunder-proto": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", @@ -3069,6 +3702,16 @@ "node": ">= 0.4" } }, + "node_modules/effect": { + "version": "3.21.0", + "resolved": "https://registry.npmjs.org/effect/-/effect-3.21.0.tgz", + "integrity": "sha512-PPN80qRokCd1f015IANNhrwOnLO7GrrMQfk4/lnZRE/8j7UPWrNNjPV0uBrZutI/nHzernbW+J0hdqQysHiSnQ==", + "license": "MIT", + "dependencies": { + "@standard-schema/spec": "^1.0.0", + "fast-check": "^3.23.1" + } + }, "node_modules/electron-to-chromium": { "version": "1.5.407", "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.407.tgz", @@ -3083,6 +3726,15 @@ "dev": true, "license": "MIT" }, + "node_modules/empathic": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/empathic/-/empathic-2.0.0.tgz", + "integrity": "sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, "node_modules/enhanced-resolve": { "version": "5.24.5", "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.24.5.tgz", @@ -3296,6 +3948,48 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/esbuild": { + "version": "0.28.2", + "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.2.tgz", + "integrity": "sha512-HKVLS8dvII+xoKW9kmqxbRKrnWEXfJJr/FZhhJmiqIB0e053QNYFqOBouTMO/k5sID4MvCiUCvv8b9M4h32wIA==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "bin": { + "esbuild": "bin/esbuild" + }, + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "@esbuild/aix-ppc64": "0.28.2", + "@esbuild/android-arm": "0.28.2", + "@esbuild/android-arm64": "0.28.2", + "@esbuild/android-x64": "0.28.2", + "@esbuild/darwin-arm64": "0.28.2", + "@esbuild/darwin-x64": "0.28.2", + "@esbuild/freebsd-arm64": "0.28.2", + "@esbuild/freebsd-x64": "0.28.2", + "@esbuild/linux-arm": "0.28.2", + "@esbuild/linux-arm64": "0.28.2", + "@esbuild/linux-ia32": "0.28.2", + "@esbuild/linux-loong64": "0.28.2", + "@esbuild/linux-mips64el": "0.28.2", + "@esbuild/linux-ppc64": "0.28.2", + "@esbuild/linux-riscv64": "0.28.2", + "@esbuild/linux-s390x": "0.28.2", + "@esbuild/linux-x64": "0.28.2", + "@esbuild/netbsd-arm64": "0.28.2", + "@esbuild/netbsd-x64": "0.28.2", + "@esbuild/openbsd-arm64": "0.28.2", + "@esbuild/openbsd-x64": "0.28.2", + "@esbuild/openharmony-arm64": "0.28.2", + "@esbuild/sunos-x64": "0.28.2", + "@esbuild/win32-arm64": "0.28.2", + "@esbuild/win32-ia32": "0.28.2", + "@esbuild/win32-x64": "0.28.2" + } + }, "node_modules/escalade": { "version": "3.2.0", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", @@ -3679,6 +4373,19 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/esprima": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", + "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", + "license": "BSD-2-Clause", + "bin": { + "esparse": "bin/esparse.js", + "esvalidate": "bin/esvalidate.js" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/esquery": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.7.0.tgz", @@ -3725,6 +4432,46 @@ "node": ">=0.10.0" } }, + "node_modules/exsolve": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/exsolve/-/exsolve-1.1.1.tgz", + "integrity": "sha512-9U/jZUgjnSGyntRr6y5Muu1MJcwFl6kPu7k8qLF0IMNfLqvw0NZ4nnVDq0RVoZ0RvCyumib4Ez3KYrVfilrw+g==", + "license": "MIT" + }, + "node_modules/extend-shallow": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", + "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", + "license": "MIT", + "dependencies": { + "is-extendable": "^0.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/fast-check": { + "version": "3.23.2", + "resolved": "https://registry.npmjs.org/fast-check/-/fast-check-3.23.2.tgz", + "integrity": "sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT", + "dependencies": { + "pure-rand": "^6.1.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -3866,6 +4613,21 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, "node_modules/function-bind": { "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", @@ -4000,6 +4762,23 @@ "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" } }, + "node_modules/giget": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/giget/-/giget-2.0.0.tgz", + "integrity": "sha512-L5bGsVkxJbJgdnwyuheIunkGatUF/zssUoxxjACCseZYAVbaqdh9Tsmmlkl8vYan09H7sbvKt4pS8GqKLBrEzA==", + "license": "MIT", + "dependencies": { + "citty": "^0.1.6", + "consola": "^3.4.0", + "defu": "^6.1.4", + "node-fetch-native": "^1.6.6", + "nypm": "^0.6.0", + "pathe": "^2.0.3" + }, + "bin": { + "giget": "dist/cli.mjs" + } + }, "node_modules/glob-parent": { "version": "6.0.2", "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", @@ -4063,6 +4842,43 @@ "dev": true, "license": "ISC" }, + "node_modules/gray-matter": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", + "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", + "license": "MIT", + "dependencies": { + "js-yaml": "^3.13.1", + "kind-of": "^6.0.2", + "section-matter": "^1.0.0", + "strip-bom-string": "^1.0.0" + }, + "engines": { + "node": ">=6.0" + } + }, + "node_modules/gray-matter/node_modules/argparse": { + "version": "1.0.10", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", + "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", + "license": "MIT", + "dependencies": { + "sprintf-js": "~1.0.2" + } + }, + "node_modules/gray-matter/node_modules/js-yaml": { + "version": "3.15.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.15.1.tgz", + "integrity": "sha512-S99WuO3HlhO3XN41EtYUNl9zzXjoJx7QvmipxsJVxtCBT0YHEFy+iOJhjSvrmV12nYhWpZaM8lPHkJm0yUMbag==", + "license": "MIT", + "dependencies": { + "argparse": "^1.0.7", + "esprima": "^4.0.0" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, "node_modules/has-bigints": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-bigints/-/has-bigints-1.1.0.tgz", @@ -4400,6 +5216,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-extendable": { + "version": "0.1.1", + "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", + "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -4693,7 +5518,6 @@ "version": "2.7.0", "resolved": "https://registry.npmjs.org/jiti/-/jiti-2.7.0.tgz", "integrity": "sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==", - "dev": true, "license": "MIT", "bin": { "jiti": "lib/jiti-cli.mjs" @@ -4802,6 +5626,15 @@ "json-buffer": "3.0.1" } }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/language-subtag-registry": { "version": "0.3.23", "resolved": "https://registry.npmjs.org/language-subtag-registry/-/language-subtag-registry-0.3.23.tgz", @@ -5165,6 +5998,18 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/marked": { + "version": "18.0.9", + "resolved": "https://registry.npmjs.org/marked/-/marked-18.0.9.tgz", + "integrity": "sha512-/Sa4qiiHZxf0/FQdBBowr9q4r10krCwMvpK48FUBdXdUXScDxiQGR9zCPrFgRVR5LU3iySOiIjy09ZQvADir1w==", + "license": "MIT", + "bin": { + "marked": "bin/marked.js" + }, + "engines": { + "node": ">= 20" + } + }, "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -5370,6 +6215,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/node-fetch-native": { + "version": "1.6.7", + "resolved": "https://registry.npmjs.org/node-fetch-native/-/node-fetch-native-1.6.7.tgz", + "integrity": "sha512-g9yhqoedzIUm0nTnTqAQvueMPVOuIY16bqgAJJC8XOOubYFNwz6IER9qs0Gq2Xd0+CecCKFjtdDTMA4u4xG06Q==", + "license": "MIT" + }, "node_modules/node-releases": { "version": "2.0.53", "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.53.tgz", @@ -5380,6 +6231,29 @@ "node": ">=18" } }, + "node_modules/nypm": { + "version": "0.6.9", + "resolved": "https://registry.npmjs.org/nypm/-/nypm-0.6.9.tgz", + "integrity": "sha512-zxlE2yvSWZWmHcNdT3+5zV2lrCogeE9YOklHrR3dFjqutq5wO7GFDYLFDRXLsYnJzwvy/im9fYoxePvS0VTW0w==", + "license": "MIT", + "dependencies": { + "citty": "^0.2.2", + "pathe": "^2.0.3", + "tinyexec": "^1.2.4" + }, + "bin": { + "nypm": "dist/cli.mjs" + }, + "engines": { + "node": ">=18" + } + }, + "node_modules/nypm/node_modules/citty": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/citty/-/citty-0.2.2.tgz", + "integrity": "sha512-+6vJA3L98yv+IdfKGZHBNiGW5KHn22e/JwID0Strsz8h4S/csAu/OuICwxrg44k5MRiZHWIo8XXuJgQTriRP4w==", + "license": "MIT" + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -5503,6 +6377,45 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/ohash": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", + "license": "MIT" + }, + "node_modules/openai": { + "version": "7.4.0", + "resolved": "https://registry.npmjs.org/openai/-/openai-7.4.0.tgz", + "integrity": "sha512-+C9Muit5x8j9R8ej8ZzVgKcrVDtqFqTy9gxFdov0EItLgU68zrJtF9ZeT0cyqJQW9S3PCJkdFgADtRGquRBtew==", + "license": "Apache-2.0", + "engines": { + "node": ">=22.0.0" + }, + "peerDependencies": { + "@aws-sdk/credential-provider-node": ">=3.972.0 <4", + "@smithy/hash-node": ">=4.3.0 <5", + "@smithy/signature-v4": ">=5.4.0 <6", + "ws": "^8.18.0", + "zod": "^3.25 || ^4.0" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-provider-node": { + "optional": true + }, + "@smithy/hash-node": { + "optional": true + }, + "@smithy/signature-v4": { + "optional": true + }, + "ws": { + "optional": true + }, + "zod": { + "optional": true + } + } + }, "node_modules/optionator": { "version": "0.9.4", "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.9.4.tgz", @@ -5612,6 +6525,18 @@ "dev": true, "license": "MIT" }, + "node_modules/pathe": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/pathe/-/pathe-2.0.3.tgz", + "integrity": "sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==", + "license": "MIT" + }, + "node_modules/perfect-debounce": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/perfect-debounce/-/perfect-debounce-1.0.0.tgz", + "integrity": "sha512-xCy9V055GLEqoFaHoC1SoLIaLmWctgCUaBaWxDZ7/Zx4CTyX7cJQLJOok/orfjZAh9kEYpjJa4d0KcJmCbctZA==", + "license": "MIT" + }, "node_modules/picocolors": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", @@ -5631,6 +6556,17 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/pkg-types": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/pkg-types/-/pkg-types-2.3.1.tgz", + "integrity": "sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==", + "license": "MIT", + "dependencies": { + "confbox": "^0.2.4", + "exsolve": "^1.0.8", + "pathe": "^2.0.3" + } + }, "node_modules/possible-typed-array-names": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/possible-typed-array-names/-/possible-typed-array-names-1.1.0.tgz", @@ -5680,6 +6616,31 @@ "node": ">= 0.8.0" } }, + "node_modules/prisma": { + "version": "6.19.3", + "resolved": "https://registry.npmjs.org/prisma/-/prisma-6.19.3.tgz", + "integrity": "sha512-++ZJ0ijLrDJF6hNB4t4uxg2br3fC4H9Yc9tcbjr2fcNFP3rh/SBNrAgjhsqBU4Ght8JPrVofG/ZkXfnSfnYsFg==", + "hasInstallScript": true, + "license": "Apache-2.0", + "dependencies": { + "@prisma/config": "6.19.3", + "@prisma/engines": "6.19.3" + }, + "bin": { + "prisma": "build/index.js" + }, + "engines": { + "node": ">=18.18" + }, + "peerDependencies": { + "typescript": ">=5.1.0" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/prop-types": { "version": "15.8.1", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", @@ -5702,6 +6663,22 @@ "node": ">=6" } }, + "node_modules/pure-rand": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/pure-rand/-/pure-rand-6.1.0.tgz", + "integrity": "sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==", + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/dubzzz" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fast-check" + } + ], + "license": "MIT" + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -5723,6 +6700,16 @@ ], "license": "MIT" }, + "node_modules/rc9": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/rc9/-/rc9-2.1.2.tgz", + "integrity": "sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==", + "license": "MIT", + "dependencies": { + "defu": "^6.1.4", + "destr": "^2.0.3" + } + }, "node_modules/react": { "version": "19.2.8", "resolved": "https://registry.npmjs.org/react/-/react-19.2.8.tgz", @@ -5751,6 +6738,19 @@ "dev": true, "license": "MIT" }, + "node_modules/readdirp": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz", + "integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==", + "license": "MIT", + "engines": { + "node": ">= 14.18.0" + }, + "funding": { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + }, "node_modules/reflect.getprototypeof": { "version": "1.0.10", "resolved": "https://registry.npmjs.org/reflect.getprototypeof/-/reflect.getprototypeof-1.0.10.tgz", @@ -5935,6 +6935,19 @@ "integrity": "sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==", "license": "MIT" }, + "node_modules/section-matter": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", + "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", + "license": "MIT", + "dependencies": { + "extend-shallow": "^2.0.1", + "kind-of": "^6.0.0" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/semver": { "version": "6.3.1", "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", @@ -6165,6 +7178,12 @@ "node": ">=0.10.0" } }, + "node_modules/sprintf-js": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", + "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", + "license": "BSD-3-Clause" + }, "node_modules/stable-hash": { "version": "0.0.5", "resolved": "https://registry.npmjs.org/stable-hash/-/stable-hash-0.0.5.tgz", @@ -6310,6 +7329,15 @@ "node": ">=4" } }, + "node_modules/strip-bom-string": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", + "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -6393,6 +7421,15 @@ "url": "https://opencollective.com/webpack" } }, + "node_modules/tinyexec": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.0.tgz", + "integrity": "sha512-QKAl9m8gWWGHV8jZcPeym6j+XULi6tOf1mT83WYJ4Lk2ytW/uwAWkrP0uFsdoYMdueVJ0qs26wZ+23xeB4ibNQ==", + "license": "MIT", + "engines": { + "node": ">=18" + } + }, "node_modules/tinyglobby": { "version": "0.2.17", "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", @@ -6499,6 +7536,25 @@ "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", "license": "0BSD" }, + "node_modules/tsx": { + "version": "4.23.12", + "resolved": "https://registry.npmjs.org/tsx/-/tsx-4.23.12.tgz", + "integrity": "sha512-FDf4L4sYzKtzWYhU/Xm0AQFdTjdIxNo9ElTf2mxXM6k8YMHXzYUe4yODVaXP4V9uMFbVg8c0qyBccK2OOxb45Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "esbuild": "~0.28.0" + }, + "bin": { + "tsx": "dist/cli.mjs" + }, + "engines": { + "node": ">=18.0.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.3" + } + }, "node_modules/type-check": { "version": "0.4.0", "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.4.0.tgz", @@ -6594,7 +7650,7 @@ "version": "5.9.3", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "bin": { "tsc": "bin/tsc", @@ -6872,7 +7928,7 @@ "version": "4.4.3", "resolved": "https://registry.npmjs.org/zod/-/zod-4.4.3.tgz", "integrity": "sha512-ytENFjIJFl2UwYglde2jchW2Hwm4GJFLDiSXWdTrJQBIN9Fcyp7n4DhxJEiWNAJMV1/BqWfW/kkg71UDcHJyTQ==", - "dev": true, + "devOptional": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/package.json b/package.json index d33e5aa..632a7d0 100644 --- a/package.json +++ b/package.json @@ -9,7 +9,12 @@ "lint": "eslint" }, "dependencies": { + "@prisma/client": "^6.19.3", + "gray-matter": "^4.0.3", + "marked": "^18.0.9", "next": "16.3.1", + "openai": "^7.4.0", + "prisma": "^6.19.3", "react": "19.2.8", "react-dom": "19.2.8" }, @@ -21,6 +26,7 @@ "eslint": "^9", "eslint-config-next": "16.3.1", "tailwindcss": "^4", + "tsx": "^4.23.12", "typescript": "^5" } } diff --git a/prisma/dev.db b/prisma/dev.db new file mode 100644 index 0000000..0a579b2 Binary files /dev/null and b/prisma/dev.db differ diff --git a/prisma/migrations/20260816110117_init/migration.sql b/prisma/migrations/20260816110117_init/migration.sql new file mode 100644 index 0000000..e2c75d3 --- /dev/null +++ b/prisma/migrations/20260816110117_init/migration.sql @@ -0,0 +1,91 @@ +-- CreateTable +CREATE TABLE "Job" ( + "id" TEXT NOT NULL PRIMARY KEY, + "title" TEXT NOT NULL, + "company" TEXT NOT NULL, + "source" TEXT, + "url" TEXT, + "applyUrl" TEXT, + "payMin" REAL, + "payMax" REAL, + "payCurrency" TEXT, + "payPeriod" TEXT, + "location" TEXT, + "remoteType" TEXT, + "phEligibility" TEXT NOT NULL DEFAULT 'unclear', + "description" TEXT, + "requirements" TEXT, + "tags" TEXT, + "status" TEXT NOT NULL DEFAULT 'prospect', + "notes" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "Resume" ( + "id" TEXT NOT NULL PRIMARY KEY, + "name" TEXT NOT NULL, + "kind" TEXT NOT NULL DEFAULT 'tailored', + "markdown" TEXT NOT NULL, + "htmlRendered" TEXT, + "version" INTEGER NOT NULL DEFAULT 1, + "jobId" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Resume_jobId_fkey" FOREIGN KEY ("jobId") REFERENCES "Job" ("id") ON DELETE SET NULL ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "CoverLetter" ( + "id" TEXT NOT NULL PRIMARY KEY, + "markdown" TEXT NOT NULL, + "version" INTEGER NOT NULL DEFAULT 1, + "jobId" TEXT NOT NULL, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "CoverLetter_jobId_fkey" FOREIGN KEY ("jobId") REFERENCES "Job" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Application" ( + "id" TEXT NOT NULL PRIMARY KEY, + "jobId" TEXT NOT NULL, + "method" TEXT NOT NULL DEFAULT 'manual', + "submittedAt" DATETIME, + "confirmation" TEXT, + "answers" TEXT, + "log" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "updatedAt" DATETIME NOT NULL, + CONSTRAINT "Application_jobId_fkey" FOREIGN KEY ("jobId") REFERENCES "Job" ("id") ON DELETE CASCADE ON UPDATE CASCADE +); + +-- CreateTable +CREATE TABLE "Profile" ( + "id" TEXT NOT NULL PRIMARY KEY DEFAULT 'main', + "fullName" TEXT NOT NULL, + "email" TEXT, + "phone" TEXT, + "location" TEXT, + "linkedin" TEXT, + "github" TEXT, + "portfolio" TEXT, + "headline" TEXT, + "summary" TEXT, + "skills" TEXT, + "workHistory" TEXT, + "updatedAt" DATETIME NOT NULL +); + +-- CreateTable +CREATE TABLE "AiRun" ( + "id" TEXT NOT NULL PRIMARY KEY, + "type" TEXT NOT NULL, + "model" TEXT, + "inputSummary" TEXT, + "resultRef" TEXT, + "ok" BOOLEAN NOT NULL DEFAULT true, + "error" TEXT, + "createdAt" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP +); diff --git a/prisma/migrations/migration_lock.toml b/prisma/migrations/migration_lock.toml new file mode 100644 index 0000000..2a5a444 --- /dev/null +++ b/prisma/migrations/migration_lock.toml @@ -0,0 +1,3 @@ +# Please do not edit this file manually +# It should be added in your version-control system (e.g., Git) +provider = "sqlite" diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 0000000..ed45093 --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,97 @@ +generator client { + provider = "prisma-client-js" +} + +datasource db { + provider = "sqlite" + url = env("DATABASE_URL") +} + +model Job { + id String @id @default(cuid()) + title String + company String + source String? + url String? + applyUrl String? + payMin Float? + payMax Float? + payCurrency String? // USD, PHP + payPeriod String? // hour, month, year + location String? + remoteType String? // remote, hybrid, onsite + phEligibility String @default("unclear") // yes, unclear, no + description String? + requirements String? + tags String? // comma-separated + status String @default("prospect") // prospect, reviewing, tailoring, ready, applied, interviewing, offer, rejected, dead + notes String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt + resumes Resume[] + coverLetters CoverLetter[] + applications Application[] +} + +model Resume { + id String @id @default(cuid()) + name String + kind String @default("tailored") // base, tailored + markdown String + htmlRendered String? + version Int @default(1) + jobId String? + job Job? @relation(fields: [jobId], references: [id], onDelete: SetNull) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model CoverLetter { + id String @id @default(cuid()) + markdown String + version Int @default(1) + jobId String + job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Application { + id String @id @default(cuid()) + jobId String + job Job @relation(fields: [jobId], references: [id], onDelete: Cascade) + method String @default("manual") // manual, semi, auto + submittedAt DateTime? + confirmation String? + answers String? // JSON: [{question, answer}] + log String? + createdAt DateTime @default(now()) + updatedAt DateTime @updatedAt +} + +model Profile { + id String @id @default("main") + fullName String + email String? + phone String? + location String? + linkedin String? + github String? + portfolio String? + headline String? + summary String? + skills String? // JSON array + workHistory String? // JSON array of {position, company, unit, start, end, supervisor, bullets[]} + updatedAt DateTime @updatedAt +} + +model AiRun { + id String @id @default(cuid()) + type String // parse, tailor, cover, answers + model String? + inputSummary String? + resultRef String? // id of created entity + ok Boolean @default(true) + error String? + createdAt DateTime @default(now()) +} diff --git a/scripts/import-legacy.ts b/scripts/import-legacy.ts new file mode 100644 index 0000000..9490446 --- /dev/null +++ b/scripts/import-legacy.ts @@ -0,0 +1,169 @@ +/** + * One-time import of the legacy markdown job files + resumes into the DB. + * Usage: npx tsx scripts/import-legacy.ts + */ +import { PrismaClient } from "@prisma/client"; +import matter from "gray-matter"; +import fs from "node:fs"; +import path from "node:path"; +import os from "node:os"; + +const prisma = new PrismaClient(); +const LEGACY = path.join(os.homedir(), "development/personal/ai-job-search"); + +function section(body: string, name: string): string | undefined { + const re = new RegExp(`##\\s*${name}\\s*\\n([\\s\\S]*?)(?=\\n##\\s|$)`, "i"); + return body.match(re)?.[1]?.trim(); +} + +function firstMatch(body: string, re: RegExp): string | undefined { + return body.match(re)?.[1]?.trim(); +} + +async function importJobs() { + const dir = path.join(LEGACY, "jobs"); + const files = fs.readdirSync(dir).filter((f) => f.endsWith(".md")); + for (const file of files) { + const raw = fs.readFileSync(path.join(dir, file), "utf8"); + const { data, content } = matter(raw); + const title = (data.title as string) ?? file.replace(/\.md$/, ""); + const company = (data.company as string) ?? "Unknown"; + const existing = await prisma.job.findFirst({ where: { title, company } }); + if (existing) { + console.log(`skip (exists): ${file}`); + continue; + } + const url = firstMatch(content, /\*\*Link:\*\*\s*(\S+)/); + const applyUrl = firstMatch(content, /\*\*Apply(?: link)?:\*\*\s*(\S+)/); + const details = section(content, "Details"); + const payLine = details ? firstMatch(details, /\*\*Pay(?:\s*Range)?:\*\*\s*(.+)/) : undefined; + const location = details ? firstMatch(details, /\*\*Location:\*\*\s*(.+)/) : undefined; + + // crude pay parse: "$15-20/hr", "₱150-170k/mo", "$65-120/hr" + let payMin: number | undefined, payMax: number | undefined, payCurrency: string | undefined, payPeriod: string | undefined; + if (payLine) { + const m = payLine.match(/([$₱P])\s*([\d,.]+)\s*[-–]\s*([\d,.]+)(k?)\s*\/?\s*(hr|hour|mo|month|yr|year)?/i); + if (m) { + const mult = m[4] === "k" ? 1000 : 1; + payMin = parseFloat(m[2].replace(/,/g, "")) * mult; + payMax = parseFloat(m[3].replace(/,/g, "")) * mult; + payCurrency = m[1] === "$" ? "USD" : "PHP"; + payPeriod = /hr|hour/i.test(m[5] ?? "") ? "hour" : /mo/i.test(m[5] ?? "") ? "month" : /yr|year/i.test(m[5] ?? "") ? "year" : undefined; + } + } + + const phEligibility = /philippines|ph eligible|170\+ countries|worldwide/i.test(content) ? "yes" : "unclear"; + const status = (data.status as string)?.replace(/\s+/g, "-") === "not-applied" ? "prospect" : "prospect"; + + await prisma.job.create({ + data: { + title, + company, + source: file.replace(/\.md$/, ""), + url, + applyUrl, + payMin, + payMax, + payCurrency, + payPeriod, + location, + remoteType: "remote", + phEligibility, + description: section(content, "Description"), + requirements: section(content, "Requirements"), + notes: section(content, "Notes") ?? section(content, "Tailoring notes"), + status, + }, + }); + console.log(`imported: ${file}`); + } +} + +async function importResumes() { + const base = path.join(LEGACY, "resume/resume.md"); + if (fs.existsSync(base)) { + const md = fs.readFileSync(base, "utf8"); + const exists = await prisma.resume.findFirst({ where: { kind: "base" } }); + if (!exists) { + await prisma.resume.create({ data: { name: "Base Resume", kind: "base", markdown: md } }); + console.log("imported: base resume"); + } + } + const tailoredDir = path.join(LEGACY, "resume/tailored"); + if (fs.existsSync(tailoredDir)) { + for (const f of fs.readdirSync(tailoredDir).filter((f) => f.endsWith(".md"))) { + const name = `Tailored: ${f.replace(/\.md$/, "")}`; + const exists = await prisma.resume.findFirst({ where: { name } }); + if (exists) continue; + await prisma.resume.create({ + data: { name, kind: "tailored", markdown: fs.readFileSync(path.join(tailoredDir, f), "utf8") }, + }); + console.log(`imported: tailored ${f}`); + } + } +} + +async function seedProfile() { + const exists = await prisma.profile.findUnique({ where: { id: "main" } }); + if (exists) return; + await prisma.profile.create({ + data: { + id: "main", + fullName: "Joshua C. Sarmiento", + email: "joshuacsarmiento@gmail.com", + location: "Philippines", + headline: "Software Developer / AI Engineer — Prompt Engineering, Conversational AI, Backend Systems", + summary: + "Software developer with 8+ years of experience spanning enterprise IT, full-stack web development, and applied AI: building LLM-powered APIs and RAG systems with FastAPI and Amazon Bedrock.", + skills: JSON.stringify([ + "Prompt engineering", "Amazon Bedrock", "RAG", "Conversational AI", "Python", "FastAPI", + "REST API design", "Active Directory", "Full-stack web development", "Bash/PowerShell automation", + ]), + workHistory: JSON.stringify([ + { + position: "Project Technical Specialist III", + company: "Philippine Institute of Volcanology and Seismology (PHIVOLCS)", + unit: "Geomatics and Hazard Assessment Services Section", + start: "2025-03", end: null, supervisor: "Eric Santos", + bullets: [ + "Contributed to CLUP and CDRA systems", + "Gates Handa: AI APIs with FastAPI + Amazon Bedrock, chatbot, RAG document/report analyzer", + "Design system diagrams and plans; support project management", + ], + }, + { + position: "Computer Programmer II", + company: "Presidential Commission for the Urban Poor (PCUP)", + unit: "IT Unit", + start: "2019-08", end: "2024-02", supervisor: "Jason Ronidel", + bullets: [ + "Developed major web apps: Attendance Monitoring, Service Workflow Management, UPO Accreditation, Reservation, Directory", + "Managed commission network infrastructure and devices", + "MS Access + VBA database applications", + ], + }, + { + position: "IT Associate", + company: "Manulife Business Process Services", + unit: "Global Access Management (GAM) Division", + start: "2016-11", end: "2017-10", supervisor: "Emory Lopez", + bullets: [ + "Automated Active Directory user management with scripts (.sh, .ps1, .bat)", + "Managed AD, mainframe, Oracle, FTP, and email accounts (Exchange, Lotus Notes, O365)", + "ServiceNow request handling; VPN token creation", + ], + }, + ]), + }, + }); + console.log("seeded: profile"); +} + +async function main() { + await importJobs(); + await importResumes(); + await seedProfile(); + console.log("done"); +} + +main().finally(() => prisma.$disconnect()); diff --git a/src/app/api/ai/cover/route.ts b/src/app/api/ai/cover/route.ts new file mode 100644 index 0000000..2cc12a7 --- /dev/null +++ b/src/app/api/ai/cover/route.ts @@ -0,0 +1,26 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { aiComplete } from "@/lib/ai"; + +const SYSTEM = `You write concise, specific cover letters (250-350 words, markdown). +Truthful only — never invent experience. Reference the company and role by name. +Output ONLY the letter markdown.`; + +export async function POST(req: NextRequest) { + const { jobId } = await req.json(); + const job = await prisma.job.findUnique({ where: { id: jobId } }); + if (!job) return NextResponse.json({ error: "job not found" }, { status: 404 }); + const profile = await prisma.profile.findUnique({ where: { id: "main" } }); + const base = await prisma.resume.findFirst({ where: { kind: "base" } }); + + const markdown = await aiComplete({ + type: "cover", + system: SYSTEM, + user: `CANDIDATE PROFILE:\n${JSON.stringify(profile)}\n\nRESUME:\n${base?.markdown ?? ""}\n\nJOB: ${job.title} at ${job.company}\n${job.description ?? ""}\n${job.requirements ?? ""}`, + inputSummary: `${job.company} — ${job.title}`, + }); + + const version = (await prisma.coverLetter.count({ where: { jobId } })) + 1; + const letter = await prisma.coverLetter.create({ data: { jobId, markdown, version } }); + return NextResponse.json(letter, { status: 201 }); +} diff --git a/src/app/api/ai/parse/route.ts b/src/app/api/ai/parse/route.ts new file mode 100644 index 0000000..e8410d1 --- /dev/null +++ b/src/app/api/ai/parse/route.ts @@ -0,0 +1,44 @@ +import { NextRequest, NextResponse } from "next/server"; +import { aiComplete, extractJson } from "@/lib/ai"; + +const SYSTEM = `You are a job-posting parser. Extract structured data from the job posting text. +Reply with ONLY a JSON object with these keys (null when unknown): +{"title","company","url","applyUrl","payMin","payMax","payCurrency","payPeriod","location","remoteType","phEligibility","description","requirements","tags"} +- payCurrency: "USD" or "PHP". payPeriod: "hour"|"month"|"year". +- remoteType: "remote"|"hybrid"|"onsite". +- phEligibility: "yes" if Philippines applicants are explicitly allowed or the role is worldwide; "no" if excluded; otherwise "unclear". +- tags: comma-separated keywords. +- description: 2-4 sentence summary. requirements: bullet list as plain text.`; + +export async function POST(req: NextRequest) { + const { text, url } = await req.json(); + let input = text as string | undefined; + + if (!input && url) { + // Server-side fetch; many boards (JobStreet) will block — caller falls back to pasting text. + const res = await fetch(url, { headers: { "User-Agent": "Mozilla/5.0" } }); + if (!res.ok) { + return NextResponse.json( + { error: `Fetch failed (${res.status}). Paste the job text instead.` }, + { status: 422 }, + ); + } + const html = await res.text(); + input = html.replace(//gi, "").replace(/<[^>]+>/g, " ").replace(/\s+/g, " ").slice(0, 20000); + } + if (!input) return NextResponse.json({ error: "Provide text or url" }, { status: 400 }); + + const reply = await aiComplete({ + type: "parse", + system: SYSTEM, + user: input, + inputSummary: url ?? input.slice(0, 120), + }); + try { + const parsed = extractJson>(reply); + if (url && !parsed.url) parsed.url = url; + return NextResponse.json(parsed); + } catch { + return NextResponse.json({ error: "AI reply was not valid JSON", raw: reply }, { status: 502 }); + } +} diff --git a/src/app/api/ai/tailor/route.ts b/src/app/api/ai/tailor/route.ts new file mode 100644 index 0000000..53c9026 --- /dev/null +++ b/src/app/api/ai/tailor/route.ts @@ -0,0 +1,36 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; +import { aiComplete } from "@/lib/ai"; + +const SYSTEM = `You are a resume-tailoring assistant. Given a base resume (markdown) and a job posting, produce a tailored resume in markdown. +Rules: +- Keep all facts truthful — never invent experience, employers, or dates. +- Reorder and reword to emphasize what the job asks for. +- Keep it one page worth of content. +- Keep the candidate's real contact lines exactly as in the base resume. +- Output ONLY the resume markdown, no commentary.`; + +export async function POST(req: NextRequest) { + const { jobId } = await req.json(); + const job = await prisma.job.findUnique({ where: { id: jobId } }); + if (!job) return NextResponse.json({ error: "job not found" }, { status: 404 }); + const base = await prisma.resume.findFirst({ where: { kind: "base" }, orderBy: { updatedAt: "desc" } }); + if (!base) return NextResponse.json({ error: "no base resume" }, { status: 400 }); + + const jobText = `# ${job.title} — ${job.company}\n${job.description ?? ""}\nRequirements:\n${job.requirements ?? ""}\nNotes:\n${job.notes ?? ""}`; + const markdown = await aiComplete({ + type: "tailor", + system: SYSTEM, + user: `BASE RESUME:\n${base.markdown}\n\nJOB POSTING:\n${jobText}`, + inputSummary: `${job.company} — ${job.title}`, + }); + + const version = (await prisma.resume.count({ where: { jobId } })) + 1; + const resume = await prisma.resume.create({ + data: { name: `Tailored: ${job.company} — ${job.title} v${version}`, kind: "tailored", markdown, jobId, version }, + }); + if (job.status === "prospect" || job.status === "reviewing") { + await prisma.job.update({ where: { id: jobId }, data: { status: "tailoring" } }); + } + return NextResponse.json(resume, { status: 201 }); +} diff --git a/src/app/api/applications/route.ts b/src/app/api/applications/route.ts new file mode 100644 index 0000000..c6f8a32 --- /dev/null +++ b/src/app/api/applications/route.ts @@ -0,0 +1,19 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET() { + const apps = await prisma.application.findMany({ + orderBy: { updatedAt: "desc" }, + include: { job: { select: { title: true, company: true } } }, + }); + return NextResponse.json(apps); +} + +export async function POST(req: NextRequest) { + const data = await req.json(); + const app = await prisma.application.create({ data }); + if (data.submittedAt) { + await prisma.job.update({ where: { id: data.jobId }, data: { status: "applied" } }); + } + return NextResponse.json(app, { status: 201 }); +} diff --git a/src/app/api/jobs/[id]/route.ts b/src/app/api/jobs/[id]/route.ts new file mode 100644 index 0000000..029b1ad --- /dev/null +++ b/src/app/api/jobs/[id]/route.ts @@ -0,0 +1,27 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +type Params = { params: Promise<{ id: string }> }; + +export async function GET(_req: NextRequest, { params }: Params) { + const { id } = await params; + const job = await prisma.job.findUnique({ + where: { id }, + include: { resumes: true, coverLetters: true, applications: true }, + }); + if (!job) return NextResponse.json({ error: "not found" }, { status: 404 }); + return NextResponse.json(job); +} + +export async function PATCH(req: NextRequest, { params }: Params) { + const { id } = await params; + const data = await req.json(); + const job = await prisma.job.update({ where: { id }, data }); + return NextResponse.json(job); +} + +export async function DELETE(_req: NextRequest, { params }: Params) { + const { id } = await params; + await prisma.job.delete({ where: { id } }); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/jobs/route.ts b/src/app/api/jobs/route.ts new file mode 100644 index 0000000..4276c81 --- /dev/null +++ b/src/app/api/jobs/route.ts @@ -0,0 +1,22 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET(req: NextRequest) { + const status = req.nextUrl.searchParams.get("status") ?? undefined; + const q = req.nextUrl.searchParams.get("q") ?? undefined; + const jobs = await prisma.job.findMany({ + where: { + ...(status ? { status } : {}), + ...(q ? { OR: [{ title: { contains: q } }, { company: { contains: q } }] } : {}), + }, + orderBy: { updatedAt: "desc" }, + include: { applications: true, resumes: { select: { id: true, name: true } } }, + }); + return NextResponse.json(jobs); +} + +export async function POST(req: NextRequest) { + const data = await req.json(); + const job = await prisma.job.create({ data }); + return NextResponse.json(job, { status: 201 }); +} diff --git a/src/app/api/profile/route.ts b/src/app/api/profile/route.ts new file mode 100644 index 0000000..cf9a4b4 --- /dev/null +++ b/src/app/api/profile/route.ts @@ -0,0 +1,17 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET() { + const profile = await prisma.profile.findUnique({ where: { id: "main" } }); + return NextResponse.json(profile); +} + +export async function PATCH(req: NextRequest) { + const data = await req.json(); + const profile = await prisma.profile.upsert({ + where: { id: "main" }, + update: data, + create: { id: "main", fullName: data.fullName ?? "Me", ...data }, + }); + return NextResponse.json(profile); +} diff --git a/src/app/api/resumes/[id]/route.ts b/src/app/api/resumes/[id]/route.ts new file mode 100644 index 0000000..235b108 --- /dev/null +++ b/src/app/api/resumes/[id]/route.ts @@ -0,0 +1,24 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +type Params = { params: Promise<{ id: string }> }; + +export async function GET(_req: NextRequest, { params }: Params) { + const { id } = await params; + const resume = await prisma.resume.findUnique({ where: { id } }); + if (!resume) return NextResponse.json({ error: "not found" }, { status: 404 }); + return NextResponse.json(resume); +} + +export async function PATCH(req: NextRequest, { params }: Params) { + const { id } = await params; + const data = await req.json(); + const resume = await prisma.resume.update({ where: { id }, data }); + return NextResponse.json(resume); +} + +export async function DELETE(_req: NextRequest, { params }: Params) { + const { id } = await params; + await prisma.resume.delete({ where: { id } }); + return NextResponse.json({ ok: true }); +} diff --git a/src/app/api/resumes/route.ts b/src/app/api/resumes/route.ts new file mode 100644 index 0000000..a493622 --- /dev/null +++ b/src/app/api/resumes/route.ts @@ -0,0 +1,16 @@ +import { NextRequest, NextResponse } from "next/server"; +import { prisma } from "@/lib/db"; + +export async function GET() { + const resumes = await prisma.resume.findMany({ + orderBy: { updatedAt: "desc" }, + include: { job: { select: { title: true, company: true } } }, + }); + return NextResponse.json(resumes); +} + +export async function POST(req: NextRequest) { + const data = await req.json(); + const resume = await prisma.resume.create({ data }); + return NextResponse.json(resume, { status: 201 }); +} diff --git a/src/app/applications/page.tsx b/src/app/applications/page.tsx new file mode 100644 index 0000000..c16b727 --- /dev/null +++ b/src/app/applications/page.tsx @@ -0,0 +1,27 @@ +import { prisma } from "@/lib/db"; +import Link from "next/link"; + +export const dynamic = "force-dynamic"; + +export default async function ApplicationsPage() { + const apps = await prisma.application.findMany({ + orderBy: { updatedAt: "desc" }, + include: { job: { select: { id: true, title: true, company: true, status: true } } }, + }); + return ( +
+

Applications ({apps.length})

+ {apps.length === 0 &&

No applications yet. Open a job and click “Mark Applied”.

} +
    + {apps.map((a) => ( +
  • + {a.job.title} — {a.job.company} +
    + {a.method} · {a.submittedAt ? `submitted ${a.submittedAt.toISOString().slice(0, 10)}` : "not submitted"} · job status: {a.job.status} +
    +
  • + ))} +
+
+ ); +} diff --git a/src/app/jobs/[id]/job-actions.tsx b/src/app/jobs/[id]/job-actions.tsx new file mode 100644 index 0000000..9d65600 --- /dev/null +++ b/src/app/jobs/[id]/job-actions.tsx @@ -0,0 +1,79 @@ +"use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +const STATUSES = ["prospect", "reviewing", "tailoring", "ready", "applied", "interviewing", "offer", "rejected", "dead"]; + +export default function JobActions({ job }: { job: { id: string; status: string } }) { + const router = useRouter(); + const [busy, setBusy] = useState(""); + const [msg, setMsg] = useState(""); + + async function setStatus(status: string) { + await fetch(`/api/jobs/${job.id}`, { + method: "PATCH", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ status }), + }); + router.refresh(); + } + + async function run(kind: "tailor" | "cover") { + setBusy(kind); setMsg(""); + try { + const res = await fetch(`/api/ai/${kind}`, { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jobId: job.id }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error ?? "failed"); + setMsg(kind === "tailor" ? "Tailored resume created ✅" : "Cover letter created ✅"); + router.refresh(); + } catch (e) { setMsg(String(e)); } finally { setBusy(""); } + } + + async function markApplied() { + await fetch("/api/applications", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ jobId: job.id, method: "manual", submittedAt: new Date().toISOString() }), + }); + router.refresh(); + } + + async function del() { + if (!confirm("Delete this job?")) return; + await fetch(`/api/jobs/${job.id}`, { method: "DELETE" }); + router.push("/jobs"); + } + + return ( +
+
+ Status: + +
+
+ + + + +
+ {msg &&
{msg}
} +
+ ); +} diff --git a/src/app/jobs/[id]/page.tsx b/src/app/jobs/[id]/page.tsx new file mode 100644 index 0000000..0ea01bd --- /dev/null +++ b/src/app/jobs/[id]/page.tsx @@ -0,0 +1,71 @@ +import { prisma } from "@/lib/db"; +import { notFound } from "next/navigation"; +import Link from "next/link"; +import JobActions from "./job-actions"; + +export const dynamic = "force-dynamic"; + +export default async function JobDetail({ params }: { params: Promise<{ id: string }> }) { + const { id } = await params; + const job = await prisma.job.findUnique({ + where: { id }, + include: { resumes: true, coverLetters: true, applications: true }, + }); + if (!job) notFound(); + + return ( +
+
+ ← Jobs +

{job.title}

+
{job.company} · {job.location ?? "Remote"} · PH: {job.phEligibility}
+
+ {job.url && Listing ↗} + {job.applyUrl && Apply page ↗} +
+
+ + + + {job.description && ( +
+

Description

+

{job.description}

+
+ )} + {job.requirements && ( +
+

Requirements

+

{job.requirements}

+
+ )} + {job.notes && ( +
+

Notes

+

{job.notes}

+
+ )} + + {job.resumes.length > 0 && ( +
+

Tailored resumes

+
    + {job.resumes.map((r) => ( +
  • {r.name}
  • + ))} +
+
+ )} + {job.coverLetters.length > 0 && ( +
+

Cover letters

+
    + {job.coverLetters.map((c) => ( +
  • {c.markdown}
  • + ))} +
+
+ )} +
+ ); +} diff --git a/src/app/jobs/new/page.tsx b/src/app/jobs/new/page.tsx new file mode 100644 index 0000000..518d983 --- /dev/null +++ b/src/app/jobs/new/page.tsx @@ -0,0 +1,85 @@ +"use client"; +import { useState } from "react"; +import { useRouter } from "next/navigation"; + +type Parsed = Record; + +export default function NewJobPage() { + const router = useRouter(); + const [url, setUrl] = useState(""); + const [text, setText] = useState(""); + const [parsed, setParsed] = useState(null); + const [busy, setBusy] = useState(false); + const [err, setErr] = useState(""); + + async function parse() { + setBusy(true); setErr(""); + try { + const res = await fetch("/api/ai/parse", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ url: url || undefined, text: text || undefined }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error ?? "parse failed"); + setParsed(data); + } catch (e) { setErr(String(e)); } finally { setBusy(false); } + } + + async function save() { + if (!parsed) return; + setBusy(true); + const res = await fetch("/api/jobs", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(parsed), + }); + const job = await res.json(); + router.push(`/jobs/${job.id}`); + } + + function set(k: string, v: string) { setParsed((p) => ({ ...p!, [k]: v || null })); } + + return ( +
+

Add Job

+ {!parsed && ( + <> + setUrl(e.target.value)} placeholder="Job URL (may fail on Cloudflare sites)" + className="w-full rounded border border-zinc-300 dark:border-zinc-700 bg-white dark:bg-zinc-900 p-2 text-sm" /> +
— or paste job text (more reliable) —
+