From a4900d41c8f42d870377f39927f827eb2d7c4887 Mon Sep 17 00:00:00 2001 From: Ariel Weinberger Date: Thu, 4 Sep 2025 13:43:29 -0500 Subject: [PATCH 1/7] feat: generate page description with llm script --- package.json | 8 + pnpm-lock.yaml | 497 ++++++++++++++++++++++++++-- scripts/llm-generate-description.ts | 159 +++++++++ svelte.config.js | 38 ++- 4 files changed, 650 insertions(+), 52 deletions(-) create mode 100644 scripts/llm-generate-description.ts diff --git a/package.json b/package.json index 4c6279a4c3..cf082c1759 100644 --- a/package.json +++ b/package.json @@ -13,6 +13,7 @@ "format": "prettier --write .", "format:check": "prettier --check .", "generate:icons": "node ./src/icons/optimize.js", + "generate:page-description": "tsx ./scripts/llm-generate-description.ts", "icons:build": "node ./src/icons/build.js", "icons:generate": "node ./src/icons/optimize.js && node ./src/icons/build.js", "icons:optimize": "node ./src/icons/optimize.js", @@ -63,6 +64,13 @@ "clsx": "^2.1.1", "cva": "npm:class-variance-authority@^0.7.1", "date-fns": "^3.6.0", + "dedent": "^1.7.0", + "@ai-sdk/openai": "^2.0.23", + "@markdoc/markdoc": "^0.5.4", + "ai": "^5.0.30", + "front-matter": "^4.0.2", + "jsdom": "^26.1.0", + "tsx": "^4.20.5", "dequal": "^2.0.3", "embla-carousel": "^8.5.2", "embla-carousel-auto-scroll": "^8.5.2", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3981d57df1..854cfc2638 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -18,6 +18,9 @@ importers: specifier: ^0.33.5 version: 0.33.5 devDependencies: + '@ai-sdk/openai': + specifier: ^2.0.23 + version: 2.0.23(zod@3.25.76) '@appwrite.io/console': specifier: ^0.6.4 version: 0.6.4 @@ -45,6 +48,9 @@ importers: '@lucide/svelte': specifier: ^0.539.0 version: 0.539.0(svelte@5.38.3) + '@markdoc/markdoc': + specifier: ^0.5.4 + version: 0.5.4 '@melt-ui/pp': specifier: ^0.3.2 version: 0.3.2(@melt-ui/svelte@0.86.6(svelte@5.38.3))(svelte@5.38.3) @@ -59,16 +65,16 @@ importers: version: 1.55.0 '@sveltejs/adapter-node': specifier: ^5.2.12 - version: 5.3.1(@sveltejs/kit@2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1))) + version: 5.3.1(@sveltejs/kit@2.36.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1))) '@sveltejs/enhanced-img': specifier: ^0.4.4 - version: 0.4.4(rollup@4.48.1)(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + version: 0.4.4(rollup@4.48.1)(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) '@sveltejs/kit': specifier: ^2.20.2 - version: 2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + version: 2.36.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) '@sveltejs/vite-plugin-svelte': specifier: ^5.0.3 - version: 5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + version: 5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) '@tailwindcss/postcss': specifier: ^4.1.4 version: 4.1.12 @@ -96,6 +102,9 @@ importers: '@types/proj4': specifier: ^2.5.6 version: 2.19.0 + ai: + specifier: ^5.0.30 + version: 5.0.30(zod@3.25.76) analytics: specifier: ^0.8.16 version: 0.8.19(@types/dlv@1.1.5) @@ -114,6 +123,9 @@ importers: date-fns: specifier: ^3.6.0 version: 3.6.0 + dedent: + specifier: ^1.7.0 + version: 1.7.0 dequal: specifier: ^2.0.3 version: 2.0.3 @@ -138,6 +150,9 @@ importers: eslint-plugin-svelte: specifier: ^2.46.1 version: 2.46.1(eslint@9.34.0(jiti@2.5.1))(svelte@5.38.3) + front-matter: + specifier: ^4.0.2 + version: 4.0.2 fuse.js: specifier: ^7.0.0 version: 7.1.0 @@ -147,6 +162,9 @@ importers: highlight.js: specifier: ^11.11.1 version: 11.11.1 + jsdom: + specifier: ^26.1.0 + version: 26.1.0 linkedom: specifier: ^0.18.9 version: 0.18.12 @@ -234,6 +252,9 @@ importers: tslib: specifier: ^2.8.1 version: 2.8.1 + tsx: + specifier: ^4.20.5 + version: 4.20.5 typescript: specifier: ^5.8.2 version: 5.9.2 @@ -245,25 +266,47 @@ importers: version: 1.0.0-next.7(svelte@5.38.3) vite: specifier: ^6.2.4 - version: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + version: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) vite-plugin-dynamic-import: specifier: ^1.6.0 version: 1.6.0 vite-plugin-image-optimizer: specifier: ^1.1.8 - version: 1.1.9(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + version: 1.1.9(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) vite-plugin-manifest-sri: specifier: ^0.2.0 version: 0.2.0 vitest: specifier: ^3.1.1 - version: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + version: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) zod: specifier: ^3.24.2 version: 3.25.76 packages: + '@ai-sdk/gateway@1.0.15': + resolution: {integrity: sha512-xySXoQ29+KbGuGfmDnABx+O6vc7Gj7qugmj1kGpn0rW0rQNn6UKUuvscKMzWyv1Uv05GyC1vqHq8ZhEOLfXscQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/openai@2.0.23': + resolution: {integrity: sha512-uOXk8HzmMUoCmD0JMX/Y1HC/ABOR/Jza2Z2rkCaJISDYz3fp5pnb6eNjcPRL48JSMzRAGp9UP5p0OpxS06IJZg==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/provider-utils@3.0.7': + resolution: {integrity: sha512-o3BS5/t8KnBL3ubP8k3w77AByOypLm+pkIL/DCw0qKkhDbvhCy+L3hRTGPikpdb8WHcylAeKsjgwOxhj4cqTUA==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + + '@ai-sdk/provider@2.0.0': + resolution: {integrity: sha512-6o7Y2SeO9vFKB8lArHXehNuusnpddKPk7xqL7T2/b+OvXMRIXUO1rR4wcv1hAFUAT9avGZshty3Wlua/XA7TvA==} + engines: {node: '>=18'} + '@alloc/quick-lru@5.2.0': resolution: {integrity: sha512-UrcABB+4bUrFABwbluTIBErXwvbsU/V7TZWfmbgJfbkwiBuziS9gxdODUyuiecfdGQ85jglMW6juS3+z5TsKLw==} engines: {node: '>=10'} @@ -302,10 +345,41 @@ packages: resolution: {tarball: https://codeload.github.com/appwrite/appwrite/tar.gz/fec157e271bc94f81d7179d51cfc5fdedaeaf727} version: 0.0.0 + '@asamuzakjp/css-color@3.2.0': + resolution: {integrity: sha512-K1A6z8tS3XsmCMM86xoWdn7Fkdn9m6RSVtocUrJYIwZnFVkng/PvkEoWtOWmP+Scc6saYWHWZYbndEEXxl24jw==} + '@babel/runtime@7.28.3': resolution: {integrity: sha512-9uIQ10o0WGdpP6GDhXcdOJPJuDgFtIDtN/9+ArJQ2NAfAmiuhTQdzkaTGR33v43GYS2UrSA0eX2pPPHoFVvpxA==} engines: {node: '>=6.9.0'} + '@csstools/color-helpers@5.1.0': + resolution: {integrity: sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==} + engines: {node: '>=18'} + + '@csstools/css-calc@2.1.4': + resolution: {integrity: sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-color-parser@3.1.0': + resolution: {integrity: sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-parser-algorithms': ^3.0.5 + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-parser-algorithms@3.0.5': + resolution: {integrity: sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==} + engines: {node: '>=18'} + peerDependencies: + '@csstools/css-tokenizer': ^3.0.4 + + '@csstools/css-tokenizer@3.0.4': + resolution: {integrity: sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==} + engines: {node: '>=18'} + '@emnapi/runtime@1.4.5': resolution: {integrity: sha512-++LApOtY0pEEz1zrd9vy1/zXVaVJJ/EbAF3u0fXIzPJEDtnITsBGbbK0EkM72amhl/R5b+5xx0Y/QhcVOpuulg==} @@ -1133,6 +1207,10 @@ packages: peerDependencies: svelte: ^4 || ^5 + '@opentelemetry/api@1.9.0': + resolution: {integrity: sha512-3giAOQvZiH5F9bMlMiv8+GSPMeqg0dbaeo58/0SlA9sxSqZhnUtxzX9/2FzyhS9sWQf5S0GJE0AKBrFqjpeYcg==} + engines: {node: '>=8.0.0'} + '@parcel/watcher-android-arm64@2.5.1': resolution: {integrity: sha512-KF8+j9nNbUN8vzOFDpRMsaKBHZ/mcjEjMToVMJOhTozkDonQFFrRcfdLWn6yWKCmJKmdVxSgHiYvTCef4/qcBA==} engines: {node: '>= 10.0.0'} @@ -1833,6 +1911,10 @@ packages: resolution: {integrity: sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==} engines: {node: '>= 6.0.0'} + agent-base@7.1.4: + resolution: {integrity: sha512-MnA+YT8fwfJPgBx3m60MNqakm30XOkyIoH1y6huTQvC0PwZG7ki8NacLBcrPbNoo8vEZy7Jpuk7+jMO+CUovTQ==} + engines: {node: '>= 14'} + agentkeepalive@4.6.0: resolution: {integrity: sha512-kja8j7PjmncONqaTsB8fQ+wE2mSU2DJ9D4XKoJ5PFWIdRMa6SLSN1ff4mOr4jCbfRSsxR4keIiySJU0N9T5hIQ==} engines: {node: '>= 8.0.0'} @@ -1841,6 +1923,12 @@ packages: resolution: {integrity: sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==} engines: {node: '>=8'} + ai@5.0.30: + resolution: {integrity: sha512-u7WdsDde9BasP+h8Q64CtU32GFShCYmxVtBa2h5dxM1f0w/AMKwzpmIDI1t3M3ean+L6uBiwOtRs8B2KA+OHgQ==} + engines: {node: '>=18'} + peerDependencies: + zod: ^3.25.76 || ^4 + ajv@6.12.6: resolution: {integrity: sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==} @@ -1893,6 +1981,9 @@ packages: engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} deprecated: This package is no longer supported. + argparse@1.0.10: + resolution: {integrity: sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==} + argparse@2.0.1: resolution: {integrity: sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==} @@ -2146,6 +2237,10 @@ packages: cssom@0.5.0: resolution: {integrity: sha512-iKuQcq+NdHqlAcwUY0o/HL69XQrUaQdMjmStJ8JFmUaiiQErlhrmuigkg/CU4E2J0IyUKUrMAgl36TvN67MqTw==} + cssstyle@4.6.0: + resolution: {integrity: sha512-2z+rWdzbbSZv6/rhtvzvqeZQHrBaqgogqt85sqFNbabZOuFbCVFb8kPeEtZjiKkbrm395irpNKiYeFeLiQnFPg==} + engines: {node: '>=18'} + cubic2quad@1.2.1: resolution: {integrity: sha512-wT5Y7mO8abrV16gnssKdmIhIbA9wSkeMzhh27jAguKrV82i24wER0vL5TGhUJ9dbJNDcigoRZ0IAHFEEEI4THQ==} @@ -2153,6 +2248,10 @@ packages: resolution: {integrity: sha512-0R9ikRb668HB7QDxT1vkpuUBtqc53YyAwMwGeUFKRojY/NWKvdZ+9UYtRfGmhqNbRkTSVpMbmyhXipFFv2cb/A==} engines: {node: '>= 12'} + data-urls@5.0.0: + resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} + engines: {node: '>=18'} + date-fns@3.6.0: resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} @@ -2165,6 +2264,17 @@ packages: supports-color: optional: true + decimal.js@10.6.0: + resolution: {integrity: sha512-YpgQiITW3JXGntzdUmyUR1V812Hn8T1YVXhCu+wO3OpS4eU9l4YdD3qjyiKdV6mvV29zapkMeD390UVEf2lkUg==} + + dedent@1.7.0: + resolution: {integrity: sha512-HGFtf8yhuhGhqO07SV79tRp+br4MnbdjeVxotpn1QBl30pcLLCQjX5b2295ll0fv8RKDKsmWYrl05usHM9CewQ==} + peerDependencies: + babel-plugin-macros: ^3.1.0 + peerDependenciesMeta: + babel-plugin-macros: + optional: true + deep-eql@5.0.2: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} @@ -2400,6 +2510,11 @@ packages: resolution: {integrity: sha512-oruZaFkjorTpF32kDSI5/75ViwGeZginGGy2NoOSg3Q9bnwlnmDm4HLnkl0RE3n+njDXR037aY1+x58Z/zFdwQ==} engines: {node: ^12.22.0 || ^14.17.0 || >=16.0.0} + esprima@4.0.1: + resolution: {integrity: sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==} + engines: {node: '>=4'} + hasBin: true + esquery@1.6.0: resolution: {integrity: sha512-ca9pw9fomFcKPvFLXhBKUK90ZvGibiGOvRJNbjljY7s7uq/5YO4BOzcYtJqExdx99rF6aAcnRxHmcUHcz6sQsg==} engines: {node: '>=0.10'} @@ -2433,6 +2548,10 @@ packages: resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} engines: {node: '>=0.8.x'} + eventsource-parser@3.0.6: + resolution: {integrity: sha512-Vo1ab+QXPzZ4tCa8SwIHJFaSzy4R6SHf7BY79rFBDf0idraZWAkYrDjDj8uWaSm3S2TK+hJ7/t1CEmZ7jXw+pg==} + engines: {node: '>=18.0.0'} + exif-parser@0.1.12: resolution: {integrity: sha512-c2bQfLNbMzLPmzQuOr8fy0csy84WmwnER81W88DzTp9CYNPJ6yzOj2EZAh9pywYpqHnshVLHQJ8WzldAyfY+Iw==} @@ -2542,6 +2661,9 @@ packages: react-dom: optional: true + front-matter@4.0.2: + resolution: {integrity: sha512-I8ZuJ/qG92NWX8i5x1Y8qyj3vizhXS31OxjKDu3LKP+7/qBgfIKValiZIEwoVoJKUHlhWtYrktkxV1XsX+pPlg==} + fs-extra@11.2.0: resolution: {integrity: sha512-PmDi3uwK5nFuXh7XDTlVnS17xJS7vW36is2+w3xcv8SVxiB4NyATf4ctkVY5bkSjX0Y4nbvZCq1/EjtEyr9ktw==} engines: {node: '>=14.14'} @@ -2587,6 +2709,9 @@ packages: resolution: {integrity: sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==} engines: {node: '>= 0.4'} + get-tsconfig@4.10.1: + resolution: {integrity: sha512-auHyJ4AgMz7vgS8Hp3N6HXSmlMdUyhSUrfBF16w153rxtLIEOE+HGqaBppczZvnHLqQJfiHotCYpNhl0lUROFQ==} + gifwrap@0.9.4: resolution: {integrity: sha512-MDMwbhASQuVeD4JKd1fKgNgCRL3fGqMM4WaqpNhWO0JiMOAjbQdumbs4BbBZEy9/M00EHEjKN3HieVhCUlwjeQ==} @@ -2666,6 +2791,10 @@ packages: resolution: {integrity: sha512-Xwwo44whKBVCYoliBQwaPvtd/2tYFkRQtXDWj1nackaV2JPXx3L0+Jvd8/qCJ2p+ML0/XVkJ2q+Mr+UVdpJK5w==} engines: {node: '>=12.0.0'} + html-encoding-sniffer@4.0.0: + resolution: {integrity: sha512-Y22oTqIU4uuPgEemfz7NDJz6OeKf12Lsu+QC+s3BVpda64lTiMYCyGwg5ki4vFxkMwQdeZDl2adZoqUgdFuTgQ==} + engines: {node: '>=18'} + html-escaper@3.0.3: resolution: {integrity: sha512-RuMffC89BOWQoY0WKGpIhn5gX3iI54O6nRA0yC124NYVtzjmFWBIiFd8M0x+ZdX0P9R4lADg1mgP8C7PxGOWuQ==} @@ -2679,10 +2808,18 @@ packages: resolution: {integrity: sha512-n2hY8YdoRE1i7r6M0w9DIw5GgZN0G25P8zLCRQ8rjXtTU3vsNFBI/vWK/UIeE6g5MUUz6avwAPXmL6Fy9D/90w==} engines: {node: '>= 6'} + http-proxy-agent@7.0.2: + resolution: {integrity: sha512-T1gkAiYYDWYx3V5Bmyu7HcfcvL7mUrTWiM6yOfa3PIphViJ/gFPbvidQ+veqSOHci/PxBcDabeUNCzpOODJZig==} + engines: {node: '>= 14'} + https-proxy-agent@5.0.1: resolution: {integrity: sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==} engines: {node: '>= 6'} + https-proxy-agent@7.0.6: + resolution: {integrity: sha512-vK9P5/iUfdl95AI+JVyUuIcVtd4ofvtrOr3HNtM2yxC9bnMbEdp3x01OhQNnjb8IJYi38VlTE3mBXwcfvywuSw==} + engines: {node: '>= 14'} + humanize-ms@1.2.1: resolution: {integrity: sha512-Fl70vYtsAFb/C06PTS9dZBo7ihau+Tu/DNCk/OyHhea07S+aeMWpFFkUaXRa8fI+ScZbEI8dfSxwY7gxZ9SAVQ==} @@ -2790,6 +2927,9 @@ packages: resolution: {integrity: sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==} engines: {node: '>=8'} + is-potential-custom-element-name@1.0.1: + resolution: {integrity: sha512-bCYeRA2rVibKZd+s2625gGnGF/t7DSqDs4dP7CrLA1m7jKWz6pps0LpYLJN8Q64HtmPKJ1hrN3nzPNKFEKOUiQ==} + is-reference@1.2.1: resolution: {integrity: sha512-U82MsXXiFIrjCK4otLT+o2NA2Cd2g5MLoOVXUZjIOhLurrRxpEXzI8O0KZHr3IjLvlAH1kTPYSuqer5T9ZVBKQ==} @@ -2843,16 +2983,32 @@ packages: js-tokens@9.0.1: resolution: {integrity: sha512-mxa9E9ITFOt0ban3j6L5MpjwegGz6lBQmM1IJkWeBZGcMxto50+eWdjC/52xDbS2vy0k7vIMK0Fe2wfL9OQSpQ==} + js-yaml@3.14.1: + resolution: {integrity: sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==} + hasBin: true + js-yaml@4.1.0: resolution: {integrity: sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==} hasBin: true + jsdom@26.1.0: + resolution: {integrity: sha512-Cvc9WUhxSMEo4McES3P7oK3QaXldCfNWp7pl2NNeiIFlCoLr3kfq9kb1fxftiwk1FLV7CvpvDfonxtzUDeSOPg==} + engines: {node: '>=18'} + peerDependencies: + canvas: ^3.0.0 + peerDependenciesMeta: + canvas: + optional: true + json-buffer@3.0.1: resolution: {integrity: sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==} json-schema-traverse@0.4.1: resolution: {integrity: sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==} + json-schema@0.4.0: + resolution: {integrity: sha512-es94M3nTIfsEPisRafak+HDLfHXnKBhV3vU5eqPcS3flIWqcxJWgXHXiey3YrpaNsanY5ei1VoYEbOzijuq9BA==} + json-stable-stringify-without-jsonify@1.0.1: resolution: {integrity: sha512-Bdboy+l7tA3OGW6FjyFHWkP5LuByj1Tk33Ljyq0axyzdk9//JSi2u3fP1QSmd1KNwq6VOKYGlAu87CisVir6Pw==} @@ -3249,6 +3405,9 @@ packages: number-flow@0.5.8: resolution: {integrity: sha512-FPr1DumWyGi5Nucoug14bC6xEz70A1TnhgSHhKyfqjgji2SOTz+iLJxKtv37N5JyJbteGYCm6NQ9p1O4KZ7iiA==} + nwsapi@2.2.21: + resolution: {integrity: sha512-o6nIY3qwiSXl7/LuOU0Dmuctd34Yay0yeuZRLFmDPrrdHpXKFndPj3hM+YEPVHYC5fx2otBx4Ilc/gyYSAUaIA==} + object-assign@4.1.1: resolution: {integrity: sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==} engines: {node: '>=0.10.0'} @@ -3627,6 +3786,9 @@ packages: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} + resolve-pkg-maps@1.0.0: + resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + resolve@1.22.10: resolution: {integrity: sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==} engines: {node: '>= 0.4'} @@ -3653,6 +3815,9 @@ packages: engines: {node: '>=18.0.0', npm: '>=8.0.0'} hasBin: true + rrweb-cssom@0.8.0: + resolution: {integrity: sha512-guoltQEx+9aMf2gDZ0s62EcV8lsXR+0w8915TC3ITdn2YueuNjdAYh/levpU9nFaoChh9RUS5ZdQMrKfVEN9tw==} + run-parallel@1.2.0: resolution: {integrity: sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==} @@ -3682,6 +3847,10 @@ packages: sax@1.4.1: resolution: {integrity: sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==} + saxes@6.0.0: + resolution: {integrity: sha512-xAg7SOnEhrm5zI3puOOKyy1OMcMlIJZYNJY7xLBwSze0UjhPLnWfj2GF2EpT0jmzaJKIWKHLsaSSajf35bcYnA==} + engines: {node: '>=v12.22.7'} + semver@7.7.2: resolution: {integrity: sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==} engines: {node: '>=10'} @@ -3749,6 +3918,9 @@ packages: split2@1.1.1: resolution: {integrity: sha512-cfurE2q8LamExY+lJ9Ex3ZfBwqAPduzOKVscPDXNCLLMvyaeD3DTz1yk7fVIs6Chco+12XeD0BB6HEoYzPYbXA==} + sprintf-js@1.0.3: + resolution: {integrity: sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==} + ssri@9.0.1: resolution: {integrity: sha512-o57Wcn66jMQvfHG1FlYbWeZWW/dHZhJXjpIcTfXldXEk5nz5lStPo3mK0OJQfGR3RbZUlbISexbljkJzuEj/8Q==} engines: {node: ^12.13.0 || ^14.15.0 || >=16.0.0} @@ -3885,6 +4057,9 @@ packages: '@types/svgicons2svgfont': optional: true + symbol-tree@3.2.4: + resolution: {integrity: sha512-9QNk5KwDF+Bvz+PyObkmSYjI5ksVUYtjW7AU22r2NKcfLJcXp96hkDWU3+XndOsUb+AQ9QhfzfCT2O+CNWT5Tw==} + tabbable@6.2.0: resolution: {integrity: sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==} @@ -3947,6 +4122,13 @@ packages: resolution: {integrity: sha512-t2T/WLB2WRgZ9EpE4jgPJ9w+i66UZfDc8wHh0xrwiRNN+UwH98GIJkTeZqX9rg0i0ptwzqW+uYeIF0T4F8LR7A==} engines: {node: '>=14.0.0'} + tldts-core@6.1.86: + resolution: {integrity: sha512-Je6p7pkk+KMzMv2XXKmAE3McmolOQFdxkKw0R8EYNr7sELW46JqnNeTX8ybPiQgvg1ymCoF8LXs5fzFaZvJPTA==} + + tldts@6.1.86: + resolution: {integrity: sha512-WMi/OQ2axVTf/ykqCQgXiIct+mSQDFdH2fkwhPwgEwvJ1kSzZRiinb0zF2Xb8u4+OqPChmyI6MEu4EezNJz+FQ==} + hasBin: true + to-regex-range@5.0.1: resolution: {integrity: sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==} engines: {node: '>=8.0'} @@ -3963,9 +4145,17 @@ packages: resolution: {integrity: sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==} engines: {node: '>=6'} + tough-cookie@5.1.2: + resolution: {integrity: sha512-FVDYdxtnj0G6Qm/DhNPSb8Ju59ULcup3tuJxkFb5K8Bv2pUXILbf0xZWU8PX8Ov19OXljbUyveOFwRMwkXzO+A==} + engines: {node: '>=16'} + tr46@0.0.3: resolution: {integrity: sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==} + tr46@5.1.1: + resolution: {integrity: sha512-hdF5ZgjTqgAntKkklYw0R03MG2x/bSzTtkxmIRw/sTNV8YXsCJ1tfLAX23lhxhHJlEf3CRCOCGGWw3vI3GaSPw==} + engines: {node: '>=18'} + ts-api-utils@2.1.0: resolution: {integrity: sha512-CUgTZL1irw8u29bzrOD/nH85jqyc74D6SshFgujOIA7osm2Rz7dYH77agkx7H4FBNxDq7Cjf+IjaX/8zwFW+ZQ==} engines: {node: '>=18.12'} @@ -3978,6 +4168,11 @@ packages: tslib@2.8.1: resolution: {integrity: sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==} + tsx@4.20.5: + resolution: {integrity: sha512-+wKjMNU9w/EaQayHXb7WA7ZaHY6hN8WgfvHNQ3t1PnU91/7O8TcTnIhCDYTZwnt8JsO9IBqZ30Ln1r7pPF52Aw==} + engines: {node: '>=18.0.0'} + hasBin: true + ttf2eot@3.1.0: resolution: {integrity: sha512-aHTbcYosNHVqb2Qtt9Xfta77ae/5y0VfdwNLUS6sGBeGr22cX2JDMo/i5h3uuOf+FAD3akYOr17+fYd5NK8aXw==} hasBin: true @@ -4158,6 +4353,10 @@ packages: jsdom: optional: true + w3c-xmlserializer@5.0.0: + resolution: {integrity: sha512-o8qghlI8NZHU1lLPrpi2+Uq7abh4GGPpYANlalzWxyWteJOCsr/P+oPBA49TOLu5FTZO4d3F9MnWJfiMo4BkmA==} + engines: {node: '>=18'} + web-streams-polyfill@3.3.3: resolution: {integrity: sha512-d2JWLCivmZYTSIoge9MsgFCZrt571BikcWGYkjC1khllbTeDlGqZ2D8vD8E/lJa8WGWbb7Plm8/XJYV7IJHZZw==} engines: {node: '>= 8'} @@ -4168,6 +4367,10 @@ packages: webidl-conversions@3.0.1: resolution: {integrity: sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==} + webidl-conversions@7.0.0: + resolution: {integrity: sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==} + engines: {node: '>=12'} + whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} @@ -4176,6 +4379,10 @@ packages: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} engines: {node: '>=18'} + whatwg-url@14.2.0: + resolution: {integrity: sha512-De72GdQZzNTUBBChsXueQUnPKDkg/5A5zp7pFDuQAj5UFoENpiACU0wlCvzpAGnTkj++ihpKwKyYewn/XNUbKw==} + engines: {node: '>=18'} + whatwg-url@5.0.0: resolution: {integrity: sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==} @@ -4214,9 +4421,25 @@ packages: wrappy@1.0.2: resolution: {integrity: sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==} + ws@8.18.3: + resolution: {integrity: sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==} + engines: {node: '>=10.0.0'} + peerDependencies: + bufferutil: ^4.0.1 + utf-8-validate: '>=5.0.2' + peerDependenciesMeta: + bufferutil: + optional: true + utf-8-validate: + optional: true + xhr@2.6.0: resolution: {integrity: sha512-/eCGLb5rxjx5e3mF1A7s+pLlR6CGyqWN91fv1JgER5mVWg1MZmlhBvy9kjcsOdRk8RrIujotWyJamfyrp+WIcA==} + xml-name-validator@5.0.0: + resolution: {integrity: sha512-EvGK8EJ3DhaHfbRlETOWAS5pO9MZITeauHKJyb8wyajUfQUenkIg2MvLDTZ4T/TgIcm3HU0TFBgWWboAZ30UHg==} + engines: {node: '>=18'} + xml-parse-from-string@1.0.1: resolution: {integrity: sha512-ErcKwJTF54uRzzNMXq2X5sMIy88zJvfN2DmdoQvy7PAFJ+tPRU6ydWuOKNMyfmOjdyBQTFREi60s0Y0SyI0G0g==} @@ -4228,6 +4451,9 @@ packages: resolution: {integrity: sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==} engines: {node: '>=4.0'} + xmlchars@2.2.0: + resolution: {integrity: sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==} + xtend@4.0.2: resolution: {integrity: sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==} engines: {node: '>=0.4'} @@ -4284,6 +4510,29 @@ packages: snapshots: + '@ai-sdk/gateway@1.0.15(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.7(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/openai@2.0.23(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.7(zod@3.25.76) + zod: 3.25.76 + + '@ai-sdk/provider-utils@3.0.7(zod@3.25.76)': + dependencies: + '@ai-sdk/provider': 2.0.0 + '@standard-schema/spec': 1.0.0 + eventsource-parser: 3.0.6 + zod: 3.25.76 + + '@ai-sdk/provider@2.0.0': + dependencies: + json-schema: 0.4.0 + '@alloc/quick-lru@5.2.0': {} '@analytics/cookie-utils@0.2.14': @@ -4332,8 +4581,36 @@ snapshots: '@appwrite.io/repo@https://codeload.github.com/appwrite/appwrite/tar.gz/fec157e271bc94f81d7179d51cfc5fdedaeaf727': {} + '@asamuzakjp/css-color@3.2.0': + dependencies: + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-color-parser': 3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + lru-cache: 10.4.3 + '@babel/runtime@7.28.3': {} + '@csstools/color-helpers@5.1.0': {} + + '@csstools/css-calc@2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-color-parser@3.1.0(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/color-helpers': 5.1.0 + '@csstools/css-calc': 2.1.4(@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4))(@csstools/css-tokenizer@3.0.4) + '@csstools/css-parser-algorithms': 3.0.5(@csstools/css-tokenizer@3.0.4) + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-parser-algorithms@3.0.5(@csstools/css-tokenizer@3.0.4)': + dependencies: + '@csstools/css-tokenizer': 3.0.4 + + '@csstools/css-tokenizer@3.0.4': {} + '@emnapi/runtime@1.4.5': dependencies: tslib: 2.8.1 @@ -5075,6 +5352,8 @@ snapshots: number-flow: 0.5.8 svelte: 5.38.3 + '@opentelemetry/api@1.9.0': {} + '@parcel/watcher-android-arm64@2.5.1': optional: true @@ -5300,31 +5579,31 @@ snapshots: dependencies: acorn: 8.15.0 - '@sveltejs/adapter-node@5.3.1(@sveltejs/kit@2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)))': + '@sveltejs/adapter-node@5.3.1(@sveltejs/kit@2.36.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)))': dependencies: '@rollup/plugin-commonjs': 28.0.6(rollup@4.48.1) '@rollup/plugin-json': 6.1.0(rollup@4.48.1) '@rollup/plugin-node-resolve': 16.0.1(rollup@4.48.1) - '@sveltejs/kit': 2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + '@sveltejs/kit': 2.36.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) rollup: 4.48.1 - '@sveltejs/enhanced-img@0.4.4(rollup@4.48.1)(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1))': + '@sveltejs/enhanced-img@0.4.4(rollup@4.48.1)(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1))': dependencies: magic-string: 0.30.18 sharp: 0.33.5 svelte: 5.38.3 svelte-parse-markup: 0.1.5(svelte@5.38.3) - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) vite-imagetools: 7.1.1(rollup@4.48.1) zimmerframe: 1.1.2 transitivePeerDependencies: - rollup - '@sveltejs/kit@2.36.2(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1))': + '@sveltejs/kit@2.36.2(@opentelemetry/api@1.9.0)(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@standard-schema/spec': 1.0.0 '@sveltejs/acorn-typescript': 1.0.5(acorn@8.15.0) - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) '@types/cookie': 0.6.0 acorn: 8.15.0 cookie: 0.6.0 @@ -5337,27 +5616,29 @@ snapshots: set-cookie-parser: 2.7.1 sirv: 3.0.1 svelte: 5.38.3 - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) + optionalDependencies: + '@opentelemetry/api': 1.9.0 - '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte-inspector@4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte': 5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) debug: 4.4.1 svelte: 5.38.3 - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - supports-color - '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1))': + '@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1))': dependencies: - '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + '@sveltejs/vite-plugin-svelte-inspector': 4.0.1(@sveltejs/vite-plugin-svelte@5.1.1(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)))(svelte@5.38.3)(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) debug: 4.4.1 deepmerge: 4.3.1 kleur: 4.1.5 magic-string: 0.30.18 svelte: 5.38.3 - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) - vitefu: 1.1.1(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) + vitefu: 1.1.1(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) transitivePeerDependencies: - supports-color @@ -5705,13 +5986,13 @@ snapshots: chai: 5.3.3 tinyrainbow: 2.0.0 - '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1))': + '@vitest/mocker@3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1))': dependencies: '@vitest/spy': 3.2.4 estree-walker: 3.0.3 magic-string: 0.30.18 optionalDependencies: - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) '@vitest/pretty-format@3.2.4': dependencies: @@ -5759,6 +6040,8 @@ snapshots: transitivePeerDependencies: - supports-color + agent-base@7.1.4: {} + agentkeepalive@4.6.0: dependencies: humanize-ms: 1.2.1 @@ -5768,6 +6051,14 @@ snapshots: clean-stack: 2.2.0 indent-string: 4.0.0 + ai@5.0.30(zod@3.25.76): + dependencies: + '@ai-sdk/gateway': 1.0.15(zod@3.25.76) + '@ai-sdk/provider': 2.0.0 + '@ai-sdk/provider-utils': 3.0.7(zod@3.25.76) + '@opentelemetry/api': 1.9.0 + zod: 3.25.76 + ajv@6.12.6: dependencies: fast-deep-equal: 3.1.3 @@ -5815,6 +6106,10 @@ snapshots: delegates: 1.0.0 readable-stream: 3.6.2 + argparse@1.0.10: + dependencies: + sprintf-js: 1.0.3 + argparse@2.0.1: {} aria-query@5.3.2: {} @@ -6100,16 +6395,30 @@ snapshots: cssom@0.5.0: {} + cssstyle@4.6.0: + dependencies: + '@asamuzakjp/css-color': 3.2.0 + rrweb-cssom: 0.8.0 + cubic2quad@1.2.1: {} data-uri-to-buffer@4.0.1: {} + data-urls@5.0.0: + dependencies: + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + date-fns@3.6.0: {} debug@4.4.1: dependencies: ms: 2.1.3 + decimal.js@10.6.0: {} + + dedent@1.7.0: {} + deep-eql@5.0.2: {} deep-is@0.1.4: {} @@ -6386,6 +6695,8 @@ snapshots: acorn-jsx: 5.3.2(acorn@8.15.0) eslint-visitor-keys: 3.4.3 + esprima@4.0.1: {} + esquery@1.6.0: dependencies: estraverse: 5.3.0 @@ -6412,6 +6723,8 @@ snapshots: events@3.3.0: {} + eventsource-parser@3.0.6: {} + exif-parser@0.1.12: {} expect-type@1.2.2: {} @@ -6508,6 +6821,10 @@ snapshots: motion-utils: 12.23.6 tslib: 2.8.1 + front-matter@4.0.2: + dependencies: + js-yaml: 3.14.1 + fs-extra@11.2.0: dependencies: graceful-fs: 4.2.11 @@ -6561,6 +6878,10 @@ snapshots: dunder-proto: 1.0.1 es-object-atoms: 1.1.1 + get-tsconfig@4.10.1: + dependencies: + resolve-pkg-maps: 1.0.0 + gifwrap@0.9.4: dependencies: image-q: 4.0.0 @@ -6654,6 +6975,10 @@ snapshots: highlight.js@11.11.1: {} + html-encoding-sniffer@4.0.0: + dependencies: + whatwg-encoding: 3.1.1 + html-escaper@3.0.3: {} htmlparser2@10.0.0: @@ -6673,6 +6998,13 @@ snapshots: transitivePeerDependencies: - supports-color + http-proxy-agent@7.0.2: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + https-proxy-agent@5.0.1: dependencies: agent-base: 6.0.2 @@ -6680,6 +7012,13 @@ snapshots: transitivePeerDependencies: - supports-color + https-proxy-agent@7.0.6: + dependencies: + agent-base: 7.1.4 + debug: 4.4.1 + transitivePeerDependencies: + - supports-color + humanize-ms@1.2.1: dependencies: ms: 2.1.3 @@ -6756,6 +7095,8 @@ snapshots: is-path-inside@3.0.3: {} + is-potential-custom-element-name@1.0.1: {} + is-reference@1.2.1: dependencies: '@types/estree': 1.0.8 @@ -6821,14 +7162,48 @@ snapshots: js-tokens@9.0.1: {} + js-yaml@3.14.1: + dependencies: + argparse: 1.0.10 + esprima: 4.0.1 + js-yaml@4.1.0: dependencies: argparse: 2.0.1 + jsdom@26.1.0: + dependencies: + cssstyle: 4.6.0 + data-urls: 5.0.0 + decimal.js: 10.6.0 + html-encoding-sniffer: 4.0.0 + http-proxy-agent: 7.0.2 + https-proxy-agent: 7.0.6 + is-potential-custom-element-name: 1.0.1 + nwsapi: 2.2.21 + parse5: 7.3.0 + rrweb-cssom: 0.8.0 + saxes: 6.0.0 + symbol-tree: 3.2.4 + tough-cookie: 5.1.2 + w3c-xmlserializer: 5.0.0 + webidl-conversions: 7.0.0 + whatwg-encoding: 3.1.1 + whatwg-mimetype: 4.0.0 + whatwg-url: 14.2.0 + ws: 8.18.3 + xml-name-validator: 5.0.0 + transitivePeerDependencies: + - bufferutil + - supports-color + - utf-8-validate + json-buffer@3.0.1: {} json-schema-traverse@0.4.1: {} + json-schema@0.4.0: {} + json-stable-stringify-without-jsonify@1.0.1: {} jsonc-eslint-parser@2.4.0: @@ -7211,6 +7586,8 @@ snapshots: dependencies: esm-env: 1.2.2 + nwsapi@2.2.21: {} + object-assign@4.1.1: {} omggif@1.0.10: {} @@ -7511,6 +7888,8 @@ snapshots: resolve-from@4.0.0: {} + resolve-pkg-maps@1.0.0: {} + resolve@1.22.10: dependencies: is-core-module: 2.16.1 @@ -7553,6 +7932,8 @@ snapshots: '@rollup/rollup-win32-x64-msvc': 4.48.1 fsevents: 2.3.3 + rrweb-cssom@0.8.0: {} + run-parallel@1.2.0: dependencies: queue-microtask: 1.2.3 @@ -7582,6 +7963,10 @@ snapshots: sax@1.4.1: {} + saxes@6.0.0: + dependencies: + xmlchars: 2.2.0 + semver@7.7.2: {} set-blocking@2.0.0: {} @@ -7688,6 +8073,8 @@ snapshots: dependencies: through2: 2.0.5 + sprintf-js@1.0.3: {} + ssri@9.0.1: dependencies: minipass: 3.3.6 @@ -7878,6 +8265,8 @@ snapshots: - bluebird - supports-color + symbol-tree@3.2.4: {} + tabbable@6.2.0: {} tailwind-merge@3.3.1: {} @@ -7938,6 +8327,12 @@ snapshots: tinyspy@4.0.3: {} + tldts-core@6.1.86: {} + + tldts@6.1.86: + dependencies: + tldts-core: 6.1.86 + to-regex-range@5.0.1: dependencies: is-number: 7.0.0 @@ -7953,8 +8348,16 @@ snapshots: totalist@3.0.1: {} + tough-cookie@5.1.2: + dependencies: + tldts: 6.1.86 + tr46@0.0.3: {} + tr46@5.1.1: + dependencies: + punycode: 2.3.1 + ts-api-utils@2.1.0(typescript@5.9.2): dependencies: typescript: 5.9.2 @@ -7963,6 +8366,13 @@ snapshots: tslib@2.8.1: {} + tsx@4.20.5: + dependencies: + esbuild: 0.25.9 + get-tsconfig: 4.10.1 + optionalDependencies: + fsevents: 2.3.3 + ttf2eot@3.1.0: dependencies: argparse: 2.0.1 @@ -8049,13 +8459,13 @@ snapshots: transitivePeerDependencies: - rollup - vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1): + vite-node@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1): dependencies: cac: 6.7.14 debug: 4.4.1 es-module-lexer: 1.7.0 pathe: 2.0.3 - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) transitivePeerDependencies: - '@types/node' - jiti @@ -8077,15 +8487,15 @@ snapshots: fast-glob: 3.3.3 magic-string: 0.30.18 - vite-plugin-image-optimizer@1.1.9(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)): + vite-plugin-image-optimizer@1.1.9(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)): dependencies: ansi-colors: 4.1.3 pathe: 1.1.2 - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) vite-plugin-manifest-sri@0.2.0: {} - vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1): + vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1): dependencies: esbuild: 0.25.9 fdir: 6.5.0(picomatch@4.0.3) @@ -8099,17 +8509,18 @@ snapshots: jiti: 2.5.1 lightningcss: 1.30.1 sass: 1.90.0 + tsx: 4.20.5 yaml: 2.8.1 - vitefu@1.1.1(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)): + vitefu@1.1.1(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)): optionalDependencies: - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) - vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1): + vitest@3.2.4(@types/node@24.3.0)(jiti@2.5.1)(jsdom@26.1.0)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1): dependencies: '@types/chai': 5.2.2 '@vitest/expect': 3.2.4 - '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1)) + '@vitest/mocker': 3.2.4(vite@6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1)) '@vitest/pretty-format': 3.2.4 '@vitest/runner': 3.2.4 '@vitest/snapshot': 3.2.4 @@ -8127,11 +8538,12 @@ snapshots: tinyglobby: 0.2.14 tinypool: 1.1.1 tinyrainbow: 2.0.0 - vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) - vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(yaml@2.8.1) + vite: 6.3.5(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) + vite-node: 3.2.4(@types/node@24.3.0)(jiti@2.5.1)(lightningcss@1.30.1)(sass@1.90.0)(tsx@4.20.5)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/node': 24.3.0 + jsdom: 26.1.0 transitivePeerDependencies: - jiti - less @@ -8146,18 +8558,29 @@ snapshots: - tsx - yaml + w3c-xmlserializer@5.0.0: + dependencies: + xml-name-validator: 5.0.0 + web-streams-polyfill@3.3.3: {} web-vitals@4.2.4: {} webidl-conversions@3.0.1: {} + webidl-conversions@7.0.0: {} + whatwg-encoding@3.1.1: dependencies: iconv-lite: 0.6.3 whatwg-mimetype@4.0.0: {} + whatwg-url@14.2.0: + dependencies: + tr46: 5.1.1 + webidl-conversions: 7.0.0 + whatwg-url@5.0.0: dependencies: tr46: 0.0.3 @@ -8196,6 +8619,8 @@ snapshots: wrappy@1.0.2: {} + ws@8.18.3: {} + xhr@2.6.0: dependencies: global: 4.4.0 @@ -8203,6 +8628,8 @@ snapshots: parse-headers: 2.0.6 xtend: 4.0.2 + xml-name-validator@5.0.0: {} + xml-parse-from-string@1.0.1: {} xml2js@0.5.0: @@ -8212,6 +8639,8 @@ snapshots: xmlbuilder@11.0.1: {} + xmlchars@2.2.0: {} + xtend@4.0.2: {} y18n@5.0.8: {} diff --git a/scripts/llm-generate-description.ts b/scripts/llm-generate-description.ts new file mode 100644 index 0000000000..a5480ee448 --- /dev/null +++ b/scripts/llm-generate-description.ts @@ -0,0 +1,159 @@ +import fs from 'fs/promises'; +import path from 'path'; +import fm from 'front-matter'; +import { markdocSchema } from '../svelte.config.js'; +// @ts-ignore +import { markdoc } from 'svelte-markdoc-preprocess'; +import { preprocessMeltUI, sequence } from '@melt-ui/pp'; +import { JSDOM } from 'jsdom'; +import { generateText } from 'ai'; +import { openai } from '@ai-sdk/openai'; +import dedent from 'dedent'; + +const apiKey = process.env.OPENAI_API_KEY; + +if (!apiKey) { + throw new Error('OPENAI_API_KEY is not set'); +} + +export async function generateDescription({ + articleText, + frontmatterAttributes, + previousAttempt, +}: { + articleText: string; + frontmatterAttributes: Record; + previousAttempt?: { + text: string; + characterCount: number; + }; +}) { + console.log(`Generating description...`); + const { text } = await generateText({ + model: openai("gpt-5-mini"), + messages: [ + { + role: 'system', + content: dedent` + You are a helpful technical writer. Your goal is to help Appwrite generate descriptions for their documentation pages. + You will be given the content of a docs page, and you need to generate a description for it. + + Rules: + - You MUST limit your response to 250 characters maximum. + - The output must be SEO-optimized, you are not meant to just summarize the page in every detail. It should give a user/crawler a good idea of what the page covers. + - Avoid deeply technical terms and jargon, this is just an SEO description. + - Output must be worthy of being used as a meta description. + ` + }, + { + role: 'user', + content: ` +Here are the frontmatter attributes: + +${JSON.stringify(frontmatterAttributes, null, 2)} + + +And here is the body of the page: +
+${articleText} +
+ +Generate the description. + +${previousAttempt && ` +Pay attention, you have made a previous attempt at generating the description, but it exceeded the character count limit (${previousAttempt.characterCount} characters). +This is your previous attempt: + +${previousAttempt.text} + + +Make sure you stick to the character count limit of 250. +`} + ` + } + ] + }); + + const characterCount = text.split(' ').length; + + if (characterCount > 250) { + console.log(`Character count is too long (${characterCount}), generating again...`); + return generateDescription({ + articleText, + frontmatterAttributes, + previousAttempt: { + text, + characterCount + } + }); + } + + console.log(`Description generated successfully (${characterCount} characters)`); + return { + description: text, + characterCount + }; +} + +export async function getDocPageContent(markdocPath: string) { + const fileContent = await fs.readFile(markdocPath, 'utf8'); + + const seq = sequence([markdoc(markdocSchema), preprocessMeltUI()]); + + if (!seq || !seq.markup) { + throw new Error('Sequence is undefined'); + } + + // Get the frontmatter + const frontmatter = fm(fileContent); + const markup = await seq.markup({ content: frontmatter.body, filename: markdocPath }); + const html = (markup as any).toString(); + + // Use JSDOM to parse the HTML and extract text content from
+ const dom = new JSDOM(html); + const articleElement = dom.window.document.querySelector('article'); + const articleText = articleElement ? articleElement.textContent : ''; + return { + articleText, + frontmatterAttributes: frontmatter.attributes + }; +} + +async function main() { + const filePathArg = extractArg('file-path'); + + if (!filePathArg) { + throw new Error('File path is required'); + } + + const filePath = path.resolve(filePathArg); + const { articleText, frontmatterAttributes } = await getDocPageContent(filePath); + + if (!articleText || !frontmatterAttributes) { + throw new Error('Article text or frontmatter attributes are undefined'); + } + + const { description, characterCount } = await generateDescription({ articleText, frontmatterAttributes }); + console.log(`================ DESCRIPTION START (character count: ${characterCount}) =================`) + console.log(description); + console.log(`===================== DESCRIPTION END ======================`) + } + +main(); + +function extractArg(name: string): string | null { + const args = process.argv; + const prefix = `--${name}=`; + + const inlineArg = args.find((arg) => arg.startsWith(prefix)); + if (inlineArg) { + return inlineArg.slice(prefix.length); + } + + const keyIndex = args.findIndex((arg) => arg === `--${name}`); + if (keyIndex !== -1 && keyIndex + 1 < args.length) { + return args[keyIndex + 1]; + } + + return null; +} diff --git a/svelte.config.js b/svelte.config.js index ae777679c6..a0943345f8 100644 --- a/svelte.config.js +++ b/svelte.config.js @@ -5,30 +5,32 @@ import { dirname, join } from 'path'; import { markdoc } from 'svelte-markdoc-preprocess'; import { fileURLToPath } from 'url'; +export const markdocSchema = { + generateSchema: true, + nodes: absolute('./src/markdoc/nodes/_Module.svelte'), + tags: absolute('./src/markdoc/tags/_Module.svelte'), + partials: absolute('./src/partials'), + layouts: { + default: absolute('./src/markdoc/layouts/Article.svelte'), + article: absolute('./src/markdoc/layouts/Article.svelte'), + tutorial: absolute('./src/markdoc/layouts/Tutorial.svelte'), + post: absolute('./src/markdoc/layouts/Post.svelte'), + partner: absolute('./src/markdoc/layouts/Partner.svelte'), + author: absolute('./src/markdoc/layouts/Author.svelte'), + category: absolute('./src/markdoc/layouts/Category.svelte'), + policy: absolute('./src/markdoc/layouts/Policy.svelte'), + changelog: absolute('./src/markdoc/layouts/Changelog.svelte'), + integration: absolute('./src/markdoc/layouts/Integration.svelte') + } +} + /** @type {import('@sveltejs/kit').Config}*/ const config = { // Consult https://kit.svelte.dev/docs/integrations#preprocessors // for more information about preprocessors preprocess: sequence([ vitePreprocess(), - markdoc({ - generateSchema: true, - nodes: absolute('./src/markdoc/nodes/_Module.svelte'), - tags: absolute('./src/markdoc/tags/_Module.svelte'), - partials: absolute('./src/partials'), - layouts: { - default: absolute('./src/markdoc/layouts/Article.svelte'), - article: absolute('./src/markdoc/layouts/Article.svelte'), - tutorial: absolute('./src/markdoc/layouts/Tutorial.svelte'), - post: absolute('./src/markdoc/layouts/Post.svelte'), - partner: absolute('./src/markdoc/layouts/Partner.svelte'), - author: absolute('./src/markdoc/layouts/Author.svelte'), - category: absolute('./src/markdoc/layouts/Category.svelte'), - policy: absolute('./src/markdoc/layouts/Policy.svelte'), - changelog: absolute('./src/markdoc/layouts/Changelog.svelte'), - integration: absolute('./src/markdoc/layouts/Integration.svelte') - } - }), + markdoc(markdocSchema), preprocessMeltUI() ]), extensions: ['.markdoc', '.svelte', '.md'], From 36c547504b36c245337c3ac2e798a5a53263048d Mon Sep 17 00:00:00 2001 From: Ariel Weinberger Date: Thu, 4 Sep 2025 13:47:52 -0500 Subject: [PATCH 2/7] feat: expose reusable function --- scripts/llm-generate-description.ts | 22 ++++++++++++++-------- 1 file changed, 14 insertions(+), 8 deletions(-) diff --git a/scripts/llm-generate-description.ts b/scripts/llm-generate-description.ts index a5480ee448..253d1e57ec 100644 --- a/scripts/llm-generate-description.ts +++ b/scripts/llm-generate-description.ts @@ -119,6 +119,18 @@ export async function getDocPageContent(markdocPath: string) { }; } +export async function generateDescriptionForDocsPage(filePath: string) { + const resolvedPath = path.resolve(filePath); + const { articleText, frontmatterAttributes } = await getDocPageContent(resolvedPath); + + if (!articleText || !frontmatterAttributes) { + throw new Error('Article text or frontmatter attributes are undefined'); + } + + const { description, characterCount } = await generateDescription({ articleText, frontmatterAttributes }); + return { description, characterCount }; +} + async function main() { const filePathArg = extractArg('file-path'); @@ -126,14 +138,8 @@ async function main() { throw new Error('File path is required'); } - const filePath = path.resolve(filePathArg); - const { articleText, frontmatterAttributes } = await getDocPageContent(filePath); - - if (!articleText || !frontmatterAttributes) { - throw new Error('Article text or frontmatter attributes are undefined'); - } - - const { description, characterCount } = await generateDescription({ articleText, frontmatterAttributes }); + const resolvedPath = path.resolve(filePathArg); + const { description, characterCount } = await generateDescriptionForDocsPage(resolvedPath); console.log(`================ DESCRIPTION START (character count: ${characterCount}) =================`) console.log(description); console.log(`===================== DESCRIPTION END ======================`) From ba957573787c1dd34bdf0b85bc20bd9007dbb989 Mon Sep 17 00:00:00 2001 From: Ariel Weinberger Date: Thu, 4 Sep 2025 13:53:42 -0500 Subject: [PATCH 3/7] do not run main() if imported --- scripts/llm-generate-description.ts | 37 +++++++++++++++++------------ 1 file changed, 22 insertions(+), 15 deletions(-) diff --git a/scripts/llm-generate-description.ts b/scripts/llm-generate-description.ts index 253d1e57ec..df2fba0059 100644 --- a/scripts/llm-generate-description.ts +++ b/scripts/llm-generate-description.ts @@ -9,14 +9,13 @@ import { JSDOM } from 'jsdom'; import { generateText } from 'ai'; import { openai } from '@ai-sdk/openai'; import dedent from 'dedent'; +import { pathToFileURL } from 'url'; -const apiKey = process.env.OPENAI_API_KEY; - -if (!apiKey) { +if (!process.env.OPENAI_API_KEY) { throw new Error('OPENAI_API_KEY is not set'); } -export async function generateDescription({ +async function generateDescription({ articleText, frontmatterAttributes, previousAttempt, @@ -132,20 +131,28 @@ export async function generateDescriptionForDocsPage(filePath: string) { } async function main() { - const filePathArg = extractArg('file-path'); - - if (!filePathArg) { - throw new Error('File path is required'); - } + const filePathArg = extractArg('file-path'); - const resolvedPath = path.resolve(filePathArg); - const { description, characterCount } = await generateDescriptionForDocsPage(resolvedPath); - console.log(`================ DESCRIPTION START (character count: ${characterCount}) =================`) - console.log(description); - console.log(`===================== DESCRIPTION END ======================`) + if (!filePathArg) { + throw new Error('File path is required'); } -main(); + const resolvedPath = path.resolve(filePathArg); + const { description, characterCount } = await generateDescriptionForDocsPage(resolvedPath); + console.log(`================ DESCRIPTION START (character count: ${characterCount}) =================`) + console.log(description); + console.log(`===================== DESCRIPTION END ======================`) +} + +// Runs only if invoked via CLI +// @ts-ignore +const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; +if (isDirect) { + main().catch((err) => { + console.error(err); + process.exit(1); + }); +} function extractArg(name: string): string | null { const args = process.argv; From b0cd081250b4a8c4c90353a8e40290781c8f95e0 Mon Sep 17 00:00:00 2001 From: Ariel Weinberger Date: Thu, 4 Sep 2025 13:54:45 -0500 Subject: [PATCH 4/7] better looking output --- scripts/llm-generate-description.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/scripts/llm-generate-description.ts b/scripts/llm-generate-description.ts index df2fba0059..743502739d 100644 --- a/scripts/llm-generate-description.ts +++ b/scripts/llm-generate-description.ts @@ -139,9 +139,7 @@ async function main() { const resolvedPath = path.resolve(filePathArg); const { description, characterCount } = await generateDescriptionForDocsPage(resolvedPath); - console.log(`================ DESCRIPTION START (character count: ${characterCount}) =================`) - console.log(description); - console.log(`===================== DESCRIPTION END ======================`) + console.log(`Generated description (${characterCount} characters):\n\n${description}\n`); } // Runs only if invoked via CLI From 37da5ed921b66e70ff04aa6f22333b916141e1ea Mon Sep 17 00:00:00 2001 From: Ariel Weinberger Date: Thu, 4 Sep 2025 13:55:24 -0500 Subject: [PATCH 5/7] fix output --- scripts/llm-generate-description.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/scripts/llm-generate-description.ts b/scripts/llm-generate-description.ts index 743502739d..5282adfa6d 100644 --- a/scripts/llm-generate-description.ts +++ b/scripts/llm-generate-description.ts @@ -139,7 +139,7 @@ async function main() { const resolvedPath = path.resolve(filePathArg); const { description, characterCount } = await generateDescriptionForDocsPage(resolvedPath); - console.log(`Generated description (${characterCount} characters):\n\n${description}\n`); + console.log(`Generated description:\n\n${description}\n`); } // Runs only if invoked via CLI From 720413495e12a6b612724760cb5eb944ead1d748 Mon Sep 17 00:00:00 2001 From: Ariel Weinberger Date: Thu, 4 Sep 2025 14:15:15 -0500 Subject: [PATCH 6/7] fix: formatting --- scripts/llm-generate-description.ts | 172 ++++++++++++++-------------- 1 file changed, 89 insertions(+), 83 deletions(-) diff --git a/scripts/llm-generate-description.ts b/scripts/llm-generate-description.ts index 5282adfa6d..2bf4db11cf 100644 --- a/scripts/llm-generate-description.ts +++ b/scripts/llm-generate-description.ts @@ -12,28 +12,28 @@ import dedent from 'dedent'; import { pathToFileURL } from 'url'; if (!process.env.OPENAI_API_KEY) { - throw new Error('OPENAI_API_KEY is not set'); + throw new Error('OPENAI_API_KEY is not set'); } async function generateDescription({ - articleText, - frontmatterAttributes, - previousAttempt, + articleText, + frontmatterAttributes, + previousAttempt }: { - articleText: string; - frontmatterAttributes: Record; - previousAttempt?: { - text: string; - characterCount: number; - }; + articleText: string; + frontmatterAttributes: Record; + previousAttempt?: { + text: string; + characterCount: number; + }; }) { - console.log(`Generating description...`); - const { text } = await generateText({ - model: openai("gpt-5-mini"), - messages: [ - { - role: 'system', - content: dedent` + console.log(`Generating description...`); + const { text } = await generateText({ + model: openai('gpt-5-mini'), + messages: [ + { + role: 'system', + content: dedent` You are a helpful technical writer. Your goal is to help Appwrite generate descriptions for their documentation pages. You will be given the content of a docs page, and you need to generate a description for it. @@ -43,10 +43,10 @@ async function generateDescription({ - Avoid deeply technical terms and jargon, this is just an SEO description. - Output must be worthy of being used as a meta description. ` - }, - { - role: 'user', - content: ` + }, + { + role: 'user', + content: ` Here are the frontmatter attributes: ${JSON.stringify(frontmatterAttributes, null, 2)} @@ -59,7 +59,9 @@ ${articleText} Generate the description. -${previousAttempt && ` +${ + previousAttempt && + ` Pay attention, you have made a previous attempt at generating the description, but it exceeded the character count limit (${previousAttempt.characterCount} characters). This is your previous attempt: @@ -67,89 +69,93 @@ ${previousAttempt.text} Make sure you stick to the character count limit of 250. -`} +` +} ` - } - ] - }); - - const characterCount = text.split(' ').length; - - if (characterCount > 250) { - console.log(`Character count is too long (${characterCount}), generating again...`); - return generateDescription({ - articleText, - frontmatterAttributes, - previousAttempt: { - text, - characterCount - } + } + ] }); - } - console.log(`Description generated successfully (${characterCount} characters)`); - return { - description: text, - characterCount - }; + const characterCount = text.split(' ').length; + + if (characterCount > 250) { + console.log(`Character count is too long (${characterCount}), generating again...`); + return generateDescription({ + articleText, + frontmatterAttributes, + previousAttempt: { + text, + characterCount + } + }); + } + + console.log(`Description generated successfully (${characterCount} characters)`); + return { + description: text, + characterCount + }; } export async function getDocPageContent(markdocPath: string) { - const fileContent = await fs.readFile(markdocPath, 'utf8'); - - const seq = sequence([markdoc(markdocSchema), preprocessMeltUI()]); - - if (!seq || !seq.markup) { - throw new Error('Sequence is undefined'); - } - - // Get the frontmatter - const frontmatter = fm(fileContent); - const markup = await seq.markup({ content: frontmatter.body, filename: markdocPath }); - const html = (markup as any).toString(); - - // Use JSDOM to parse the HTML and extract text content from
- const dom = new JSDOM(html); - const articleElement = dom.window.document.querySelector('article'); - const articleText = articleElement ? articleElement.textContent : ''; - return { - articleText, - frontmatterAttributes: frontmatter.attributes - }; + const fileContent = await fs.readFile(markdocPath, 'utf8'); + + const seq = sequence([markdoc(markdocSchema), preprocessMeltUI()]); + + if (!seq || !seq.markup) { + throw new Error('Sequence is undefined'); + } + + // Get the frontmatter + const frontmatter = fm(fileContent); + const markup = await seq.markup({ content: frontmatter.body, filename: markdocPath }); + const html = (markup as any).toString(); + + // Use JSDOM to parse the HTML and extract text content from
+ const dom = new JSDOM(html); + const articleElement = dom.window.document.querySelector('article'); + const articleText = articleElement ? articleElement.textContent : ''; + return { + articleText, + frontmatterAttributes: frontmatter.attributes + }; } export async function generateDescriptionForDocsPage(filePath: string) { - const resolvedPath = path.resolve(filePath); - const { articleText, frontmatterAttributes } = await getDocPageContent(resolvedPath); + const resolvedPath = path.resolve(filePath); + const { articleText, frontmatterAttributes } = await getDocPageContent(resolvedPath); - if (!articleText || !frontmatterAttributes) { - throw new Error('Article text or frontmatter attributes are undefined'); - } + if (!articleText || !frontmatterAttributes) { + throw new Error('Article text or frontmatter attributes are undefined'); + } - const { description, characterCount } = await generateDescription({ articleText, frontmatterAttributes }); - return { description, characterCount }; + const { description, characterCount } = await generateDescription({ + articleText, + frontmatterAttributes + }); + return { description, characterCount }; } async function main() { - const filePathArg = extractArg('file-path'); + const filePathArg = extractArg('file-path'); - if (!filePathArg) { - throw new Error('File path is required'); - } + if (!filePathArg) { + throw new Error('File path is required'); + } - const resolvedPath = path.resolve(filePathArg); - const { description, characterCount } = await generateDescriptionForDocsPage(resolvedPath); - console.log(`Generated description:\n\n${description}\n`); + const resolvedPath = path.resolve(filePathArg); + const { description, characterCount } = await generateDescriptionForDocsPage(resolvedPath); + console.log(`Generated description:\n\n${description}\n`); } // Runs only if invoked via CLI // @ts-ignore const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; if (isDirect) { - main().catch((err) => { - console.error(err); - process.exit(1); - }); + main().catch((err) => { + console.error(err); + process.exit(1); + }); } function extractArg(name: string): string | null { From ea9b7895e886e25902082317f251e2110b0e8909 Mon Sep 17 00:00:00 2001 From: Tessa Date: Thu, 11 Sep 2025 19:37:57 -0400 Subject: [PATCH 7/7] improvements to the llm generator --- scripts/llm-generate-description.ts | 330 ++++++++++++++++------------ 1 file changed, 185 insertions(+), 145 deletions(-) diff --git a/scripts/llm-generate-description.ts b/scripts/llm-generate-description.ts index 2bf4db11cf..c46039c577 100644 --- a/scripts/llm-generate-description.ts +++ b/scripts/llm-generate-description.ts @@ -1,176 +1,216 @@ -import fs from 'fs/promises'; -import path from 'path'; -import fm from 'front-matter'; -import { markdocSchema } from '../svelte.config.js'; -// @ts-ignore -import { markdoc } from 'svelte-markdoc-preprocess'; -import { preprocessMeltUI, sequence } from '@melt-ui/pp'; -import { JSDOM } from 'jsdom'; -import { generateText } from 'ai'; -import { openai } from '@ai-sdk/openai'; -import dedent from 'dedent'; -import { pathToFileURL } from 'url'; - -if (!process.env.OPENAI_API_KEY) { - throw new Error('OPENAI_API_KEY is not set'); +import { generateText } from "ai"; +import { openai } from "@ai-sdk/openai"; +import { readFileSync } from "fs"; +import { pathToFileURL } from "url"; +import path from "path"; +import frontMatter from "front-matter"; + +function extractArg(name: string): string | undefined { + const argIndex = process.argv.findIndex((arg) => arg.startsWith(`--${name}`)); + if (argIndex === -1) return undefined; + + const arg = process.argv[argIndex]; + const value = arg.split("=")[1]; + if (value) return value; + + const nextArg = process.argv[argIndex + 1]; + return nextArg && !nextArg.startsWith("--") ? nextArg : undefined; +} + +function extractBooleanArg(name: string): boolean { + return process.argv.includes(`--${name}`); } async function generateDescription({ - articleText, - frontmatterAttributes, - previousAttempt + articleText, + frontmatterAttributes, }: { - articleText: string; - frontmatterAttributes: Record; - previousAttempt?: { - text: string; - characterCount: number; - }; + articleText: string; + frontmatterAttributes: Record< + string, + string | number | boolean | Date | string[] | number[] | boolean[] + >; }) { - console.log(`Generating description...`); - const { text } = await generateText({ - model: openai('gpt-5-mini'), - messages: [ - { - role: 'system', - content: dedent` - You are a helpful technical writer. Your goal is to help Appwrite generate descriptions for their documentation pages. - You will be given the content of a docs page, and you need to generate a description for it. - - Rules: - - You MUST limit your response to 250 characters maximum. - - The output must be SEO-optimized, you are not meant to just summarize the page in every detail. It should give a user/crawler a good idea of what the page covers. - - Avoid deeply technical terms and jargon, this is just an SEO description. - - Output must be worthy of being used as a meta description. - ` - }, - { - role: 'user', - content: ` -Here are the frontmatter attributes: - -${JSON.stringify(frontmatterAttributes, null, 2)} - - -And here is the body of the page: -
-${articleText} -
+ const systemPrompt = `You are an expert at writing SEO-optimized page descriptions for technical documentation websites targeting senior software engineers. -Generate the description. +Generate a concise, professional description (maximum 250 characters) that: +- Accurately summarizes the technical content +- Uses natural language with standard punctuation (use regular hyphens, not em dashes) +- Speaks directly to experienced developers and engineering leaders +- Includes relevant technical keywords for SEO +- Avoids AI-generated language patterns or marketing fluff +- Uses a professional, authoritative tone that resonates with senior engineers -${ - previousAttempt && - ` -Pay attention, you have made a previous attempt at generating the description, but it exceeded the character count limit (${previousAttempt.characterCount} characters). -This is your previous attempt: - -${previousAttempt.text} - +The description should be suitable for use in HTML meta descriptions and social media previews.`; -Make sure you stick to the character count limit of 250. -` -} - ` - } - ] + const userPrompt = `Generate a page description for this documentation page: + +Title: ${frontmatterAttributes?.title || "Untitled"} +Summary: ${frontmatterAttributes?.summary || "No summary provided"} + +Content: +${articleText} + +Generate a description that captures the essence of this page in 250 characters or less.`; + + try { + const { text: description } = await generateText({ + model: openai("gpt-4o-mini"), + system: systemPrompt, + prompt: userPrompt, + maxTokens: 100, }); - const characterCount = text.split(' ').length; + const trimmedDescription = description.trim(); + const characterCount = trimmedDescription.length; + // If the description is too long, try again with a more specific prompt if (characterCount > 250) { - console.log(`Character count is too long (${characterCount}), generating again...`); - return generateDescription({ - articleText, - frontmatterAttributes, - previousAttempt: { - text, - characterCount - } - }); - } + const retryPrompt = `The previous description was too long (${characterCount} characters). Generate a shorter description (maximum 250 characters) for this page: - console.log(`Description generated successfully (${characterCount} characters)`); - return { - description: text, - characterCount - }; -} +Title: ${frontmatterAttributes?.title || "Untitled"} +Content: ${articleText.substring(0, 500)}... -export async function getDocPageContent(markdocPath: string) { - const fileContent = await fs.readFile(markdocPath, 'utf8'); +Make it concise and under 250 characters.`; - const seq = sequence([markdoc(markdocSchema), preprocessMeltUI()]); + const { text: retryDescription } = await generateText({ + model: openai("gpt-4o-mini"), + system: systemPrompt, + prompt: retryPrompt, + maxTokens: 80, + }); - if (!seq || !seq.markup) { - throw new Error('Sequence is undefined'); + const finalDescription = retryDescription.trim(); + return { + description: finalDescription, + characterCount: finalDescription.length, + }; } - // Get the frontmatter - const frontmatter = fm(fileContent); - const markup = await seq.markup({ content: frontmatter.body, filename: markdocPath }); - const html = (markup as any).toString(); + return { description: trimmedDescription, characterCount }; + } catch (error) { + throw new Error( + `Failed to generate description: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } +} + +export async function getDocPageContent(markdocPath: string) { + try { + const fileContent = readFileSync(markdocPath, "utf-8"); + const { attributes: frontmatterAttributes, body } = + frontMatter(fileContent); + + // Use raw markdoc content directly - simpler and faster + // The LLM can understand markdown syntax just fine + const articleText = body; - // Use JSDOM to parse the HTML and extract text content from
- const dom = new JSDOM(html); - const articleElement = dom.window.document.querySelector('article'); - const articleText = articleElement ? articleElement.textContent : ''; return { - articleText, - frontmatterAttributes: frontmatter.attributes + articleText: articleText.trim(), + frontmatterAttributes, }; + } catch (error) { + throw new Error( + `Failed to parse markdoc file ${markdocPath}: ${error instanceof Error ? error.message : "Unknown error"}`, + ); + } } -export async function generateDescriptionForDocsPage(filePath: string) { - const resolvedPath = path.resolve(filePath); - const { articleText, frontmatterAttributes } = await getDocPageContent(resolvedPath); - - if (!articleText || !frontmatterAttributes) { - throw new Error('Article text or frontmatter attributes are undefined'); - } - - const { description, characterCount } = await generateDescription({ - articleText, - frontmatterAttributes - }); - return { description, characterCount }; +export async function generateDescriptionForDocsPage( + filePath: string, + options: { skipIfExists?: boolean } = {}, +) { + const resolvedPath = path.resolve(filePath); + const { articleText, frontmatterAttributes } = + await getDocPageContent(resolvedPath); + + if (!frontmatterAttributes) { + throw new Error( + "Frontmatter attributes are undefined - file may be malformed", + ); + } + + // Check if description already exists and skip if requested + if (options.skipIfExists && frontmatterAttributes.description) { + console.log( + `⏭️ Skipping ${filePath} - description already exists: "${frontmatterAttributes.description}"`, + ); + return { + description: frontmatterAttributes.description, + characterCount: frontmatterAttributes.description.length, + skipped: true, + }; + } + + // If article text is empty, use frontmatter as fallback + const contentToUse = + articleText && articleText.trim() + ? articleText + : frontmatterAttributes.summary || + frontmatterAttributes.title || + "No content available"; + + const { description, characterCount } = await generateDescription({ + articleText: contentToUse, + frontmatterAttributes, + }); + return { description, characterCount, skipped: false }; } async function main() { - const filePathArg = extractArg('file-path'); - - if (!filePathArg) { - throw new Error('File path is required'); - } - - const resolvedPath = path.resolve(filePathArg); - const { description, characterCount } = await generateDescriptionForDocsPage(resolvedPath); - console.log(`Generated description:\n\n${description}\n`); + const filePathArg = extractArg("file-path"); + const skipIfExists = extractBooleanArg("skip-existing"); + const showHelp = extractBooleanArg("help"); + + if (showHelp) { + console.log(` +📝 LLM Description Generator + +Usage: + npm run generate:page-description -- --file-path [options] + +Options: + --file-path Path to the markdoc file to process + --skip-existing Skip files that already have descriptions + --help Show this help message + +Examples: + npm run generate:page-description -- --file-path ./blog-post.markdoc + npm run generate:page-description -- --file-path ./blog-post.markdoc --skip-existing + +For CI/CD usage: + npm run generate:page-description -- --file-path ./new-post.markdoc --skip-existing +`); + return; + } + + if (!filePathArg) { + throw new Error("File path is required. Use --help for usage information."); + } + + const resolvedPath = path.resolve(filePathArg); + const { description, characterCount, skipped } = + await generateDescriptionForDocsPage(resolvedPath, { skipIfExists }); + + if (skipped) { + console.log("✅ File skipped - description already exists"); + return; + } + + console.log( + `================ DESCRIPTION START (character count: ${characterCount}) =================`, + ); + console.log(description); + console.log(`===================== DESCRIPTION END ======================`); } // Runs only if invoked via CLI -// @ts-ignore -const isDirect = import.meta.url === pathToFileURL(process.argv[1]).href; +// Check if this file is being run directly (not imported) +const isDirect = + process.argv[1] && process.argv[1].endsWith("llm-generate-description.ts"); if (isDirect) { - main().catch((err) => { - console.error(err); - process.exit(1); - }); -} - -function extractArg(name: string): string | null { - const args = process.argv; - const prefix = `--${name}=`; - - const inlineArg = args.find((arg) => arg.startsWith(prefix)); - if (inlineArg) { - return inlineArg.slice(prefix.length); - } - - const keyIndex = args.findIndex((arg) => arg === `--${name}`); - if (keyIndex !== -1 && keyIndex + 1 < args.length) { - return args[keyIndex + 1]; - } - - return null; + main().catch((err) => { + console.error(err); + process.exit(1); + }); }