diff --git a/.github/workflows/validate.yml b/.github/workflows/validate.yml new file mode 100644 index 0000000..9a19157 --- /dev/null +++ b/.github/workflows/validate.yml @@ -0,0 +1,63 @@ +name: Validate Mika Agent Assist + +on: + pull_request: + branches: + - main + push: + branches: + - main + - "codex/**" + workflow_dispatch: + +jobs: + frontend-build: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: 22 + cache: npm + + - name: Install dependencies + run: npm install --legacy-peer-deps + + - name: Build frontend + run: npm run build + + edge-functions: + runs-on: ubuntu-latest + + steps: + - name: Checkout + uses: actions/checkout@v4 + + - name: Setup Deno + uses: denoland/setup-deno@v2 + with: + deno-version: 2.7.14 + + - name: Type-check edge functions + shell: bash + run: | + files=( + supabase/functions/_shared/hermes-config.ts + supabase/functions/_shared/runtime-sync.ts + supabase/functions/sync-agent-runtime/index.ts + supabase/functions/sync-agent-skills/index.ts + supabase/functions/keep-alive-agents/index.ts + supabase/functions/provision-agent/index.ts + supabase/functions/publish-skill-version/index.ts + supabase/functions/railway-webhook/index.ts + supabase/functions/update-agent-config/index.ts + supabase/functions/list-ollama-models/index.ts + supabase/functions/oauth-callback/index.ts + supabase/functions/disconnect-integration/index.ts + supabase/functions/refresh-integration-token/index.ts + ) + deno check --config supabase/functions/deno.json "${files[@]}" diff --git a/package-lock.json b/package-lock.json index 64b0398..acd1c94 100644 --- a/package-lock.json +++ b/package-lock.json @@ -7,6 +7,9 @@ "name": "tanstack_start_ts", "dependencies": { "@cloudflare/vite-plugin": "^1.25.5", + "@codemirror/lang-markdown": "^6.5.0", + "@codemirror/theme-one-dark": "^6.1.3", + "@codemirror/view": "^6.41.1", "@hookform/resolvers": "^5.2.2", "@lovable.dev/cloud-auth-js": "^1.1.1", "@radix-ui/react-accordion": "^1.2.12", @@ -41,6 +44,8 @@ "@tanstack/react-router": "^1.168.0", "@tanstack/react-start": "^1.167.14", "@tanstack/router-plugin": "^1.167.10", + "@tanstack/zod-adapter": "^1.166.9", + "@uiw/react-codemirror": "^4.25.9", "class-variance-authority": "^0.7.1", "clsx": "^2.1.1", "cmdk": "^1.1.1", @@ -54,8 +59,10 @@ "react-dom": "^19.2.0", "react-hook-form": "^7.72.1", "react-imask": "^7.6.1", + "react-markdown": "^10.1.0", "react-resizable-panels": "^4.6.5", "recharts": "^2.15.4", + "remark-gfm": "^4.0.1", "sonner": "^2.0.7", "tailwind-merge": "^3.5.0", "tailwindcss": "^4.2.1", @@ -533,6 +540,159 @@ "node": ">=16" } }, + "node_modules/@codemirror/autocomplete": { + "version": "6.20.1", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.20.1.tgz", + "integrity": "sha512-1cvg3Vz1dSSToCNlJfRA2WSI4ht3K+WplO0UMOgmUYPivCyy2oueZY6Lx7M9wThm7SDUBViRmuT+OG/i8+ON9A==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.10.3", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.10.3.tgz", + "integrity": "sha512-JFRiqhKu+bvSkDLI+rUhJwSxQxYb759W5GBezE8Uc8mHLqC9aV/9aTC7yJSqCtB3F00pylrLCwnyS91Ap5ej4Q==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.6.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.11", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.11.tgz", + "integrity": "sha512-9NsXp7Nwp891pQchI7gPdTwBuSuT3K65NGTHWHNJ55HjYcHLllr0rbIZNdOzas9ztc1EUVBlHou85FFZS4BNnw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.12" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.5", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.5.tgz", + "integrity": "sha512-zD4e5mS+50htS7F+TYjBPsiIFGanfVqg4HyUz6WNFikgOPf2BgKlx+TQedI1w6n/IqRBVBbBWmGFdLB/7uxO4A==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.5.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.5.0.tgz", + "integrity": "sha512-0K40bZ35jpHya6FriukbgaleaqzBLZfOh7HuzqbMxBXkbYMJDxfF39c23xOgxFezR+3G+tR2/Mup+Xk865OMvw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.7.1", + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.3.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.2.1", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.12.3", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.12.3.tgz", + "integrity": "sha512-QwCZW6Tt1siP37Jet9Tb02Zs81TQt6qQrZR2H+eGMcFsL1zMrk2/b9CLC7/9ieP1fjIUMgviLWMmgiHoJrj+ZA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.9.5", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.9.5.tgz", + "integrity": "sha512-GElsbU9G7QT9xXhpUg1zWGmftA/7jamh+7+ydKRuT0ORpWS3wOSP0yT1FOlIZa7mIJjpVPipErsyvVqB9cfTFA==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.7.0.tgz", + "integrity": "sha512-ZvGm99wc/s2cITtMT15LFdn8aH/aS+V+DqyGq/N5ZlV5vWtH+nILvC2nw0zX7ByNoHHDZ2IxxdW38O0tc5nVHg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.37.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.6.0", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.6.0.tgz", + "integrity": "sha512-4nbvra5R5EtiCzr9BTHiTLc+MLXK2QGiAVYMyi8PkQd3SR+6ixar/Q/01Fa21TBIDOZXgeWV4WppsQolSreAPQ==", + "license": "MIT", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/theme-one-dark": { + "version": "6.1.3", + "resolved": "https://registry.npmjs.org/@codemirror/theme-one-dark/-/theme-one-dark-6.1.3.tgz", + "integrity": "sha512-NzBdIvEJmx6fjeremiGp3t/okrLPYT0d9orIc7AFun8oZcRk58aejkqhv6spnz4MLAevrKNPMQYXEWMg4s+sKA==", + "license": "MIT", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.41.1", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.41.1.tgz", + "integrity": "sha512-ToDnWKbBnke+ZLrP6vgTTDScGi5H37YYuZGniQaBzxMVdtCxMrslsmtnOvbPZk4RX9bvkQqnWR/WS/35tJA0qg==", + "license": "MIT", + "dependencies": { + "@codemirror/state": "^6.6.0", + "crelt": "^1.0.6", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, "node_modules/@cspotcode/source-map-support": { "version": "0.8.1", "resolved": "https://registry.npmjs.org/@cspotcode/source-map-support/-/source-map-support-0.8.1.tgz", @@ -1782,6 +1942,73 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@lezer/common": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.5.2.tgz", + "integrity": "sha512-sxQE460fPZyU3sdc8lafxiPwJHBzZRy/udNFynGQky1SePYBdhkBl1kOagA9uT3pxR8K09bOrmTUqA9wb/PjSQ==", + "license": "MIT" + }, + "node_modules/@lezer/css": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.3.3.tgz", + "integrity": "sha512-RzBo8r+/6QJeow7aPHIpGVIH59xTcJXp399820gZoMo9noQDRVpJLheIBUicYwKcsbOYoBRoLZlf2720dG/4Tg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.3.tgz", + "integrity": "sha512-qXdH7UqTvGfdVBINrgKhDsVTJTxactNNxLk7+UMwZhU13lMHaOBlJe9Vqp907ya56Y3+ed2tlqzys7jDkTmW0g==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.3.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.13", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.13.tgz", + "integrity": "sha512-oI7n6NJml729m7pjm9lvLvmXbdoMoi2f+1pwSDJkl9d68zGr7a9Btz8NdHTGQZtW2DA25ybeuv/SyDb9D5tseg==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.5.4.tgz", + "integrity": "sha512-vvYx3MhWqeZtGPwDStM2dwgljd5smolYD2lR2UyFcHfxbBQebqx8yjmFmxtJ/E6nN6u1D9srOiVWm3Rb4tmcUA==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.10.tgz", + "integrity": "sha512-rnCpTIBafOx4mRp43xOxDJbFipJm/c0cia/V5TiGlhmMa+wsSdoGmUN3w5Bqrks/09Q/D4tNAmWaT8p6NRi77A==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.6.3", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.6.3.tgz", + "integrity": "sha512-jpGm5Ps+XErS+xA4urw7ogEGkeZOahVQF21Z6oECF0sj+2liwZopd2+I8uH5I/vZsRuuze3OxBREIANLf6KKUw==", + "license": "MIT", + "dependencies": { + "@lezer/common": "^1.5.0", + "@lezer/highlight": "^1.0.0" + } + }, "node_modules/@lovable.dev/cloud-auth-js": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/@lovable.dev/cloud-auth-js/-/cloud-auth-js-1.1.1.tgz", @@ -1827,6 +2054,12 @@ } } }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==", + "license": "MIT" + }, "node_modules/@nodelib/fs.scandir": { "version": "2.1.5", "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", @@ -4888,6 +5121,23 @@ "url": "https://github.com/sponsors/tannerlinsley" } }, + "node_modules/@tanstack/zod-adapter": { + "version": "1.166.9", + "resolved": "https://registry.npmjs.org/@tanstack/zod-adapter/-/zod-adapter-1.166.9.tgz", + "integrity": "sha512-HHllQ/CKGi8YBbftv6OmzojtHM6Rk4UszAFICAgUMbwiqtKqjlIZQ/7mv2IPNxBb8YlOQgzyQ4jz2UTEXIi6YA==", + "license": "MIT", + "engines": { + "node": ">=20.19" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/tannerlinsley" + }, + "peerDependencies": { + "@tanstack/react-router": ">=1.43.2", + "zod": "^3.23.8" + } + }, "node_modules/@types/babel__core": { "version": "7.20.5", "resolved": "https://registry.npmjs.org/@types/babel__core/-/babel__core-7.20.5.tgz", @@ -4996,13 +5246,39 @@ "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==", "license": "MIT" }, + "node_modules/@types/debug": { + "version": "4.1.13", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", + "integrity": "sha512-KSVgmQmzMwPlmtljOomayoR89W4FynCAi3E8PPs7vmDVPe84hT+vGPKkJfThkmXs0x0jAaa9U8uW8bbfyS2fWw==", + "license": "MIT", + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/estree": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "dev": true, "license": "MIT" }, + "node_modules/@types/estree-jsx": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", + "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", + "license": "MIT", + "dependencies": { + "@types/estree": "*" + } + }, + "node_modules/@types/hast": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", + "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, "node_modules/@types/json-schema": { "version": "7.0.15", "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", @@ -5010,6 +5286,21 @@ "dev": true, "license": "MIT" }, + "node_modules/@types/mdast": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", + "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", + "license": "MIT", + "dependencies": { + "@types/unist": "*" + } + }, + "node_modules/@types/ms": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", + "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", + "license": "MIT" + }, "node_modules/@types/node": { "version": "22.19.17", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.19.17.tgz", @@ -5039,6 +5330,12 @@ "@types/react": "^19.2.0" } }, + "node_modules/@types/unist": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", + "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", + "license": "MIT" + }, "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -5343,6 +5640,65 @@ "url": "https://opencollective.com/eslint" } }, + "node_modules/@uiw/codemirror-extensions-basic-setup": { + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/codemirror-extensions-basic-setup/-/codemirror-extensions-basic-setup-4.25.9.tgz", + "integrity": "sha512-QFAqr+pu6lDmNpAlecODcF49TlsrZ0bj15zPzfhiqSDl+Um3EsDLFLppixC7kFLn+rdDM2LTvVjn5CPvefpRgw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@codemirror/autocomplete": ">=6.0.0", + "@codemirror/commands": ">=6.0.0", + "@codemirror/language": ">=6.0.0", + "@codemirror/lint": ">=6.0.0", + "@codemirror/search": ">=6.0.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/view": ">=6.0.0" + } + }, + "node_modules/@uiw/react-codemirror": { + "version": "4.25.9", + "resolved": "https://registry.npmjs.org/@uiw/react-codemirror/-/react-codemirror-4.25.9.tgz", + "integrity": "sha512-HftqCBUYShAOH0pGi1CHP8vfm5L8fQ3+0j0VI6lQD6QpK+UBu3J7nxfEN5O/BXMilMNf9ZyFJRvRcuMMOLHMng==", + "license": "MIT", + "dependencies": { + "@babel/runtime": "^7.18.6", + "@codemirror/commands": "^6.1.0", + "@codemirror/state": "^6.1.1", + "@codemirror/theme-one-dark": "^6.0.0", + "@uiw/codemirror-extensions-basic-setup": "4.25.9", + "codemirror": "^6.0.0" + }, + "funding": { + "url": "https://jaywcjlove.github.io/#/sponsor" + }, + "peerDependencies": { + "@babel/runtime": ">=7.11.0", + "@codemirror/state": ">=6.0.0", + "@codemirror/theme-one-dark": ">=6.0.0", + "@codemirror/view": ">=6.0.0", + "codemirror": ">=6.0.0", + "react": ">=17.0.0", + "react-dom": ">=17.0.0" + } + }, + "node_modules/@ungap/structured-clone": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", + "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "license": "ISC" + }, "node_modules/@vitejs/plugin-react": { "version": "5.2.0", "resolved": "https://registry.npmjs.org/@vitejs/plugin-react/-/plugin-react-5.2.0.tgz", @@ -5504,6 +5860,16 @@ "@babel/types": "^7.23.6" } }, + "node_modules/bail": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", + "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -5643,6 +6009,16 @@ ], "license": "CC-BY-4.0" }, + "node_modules/ccount": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", + "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/chalk": { "version": "4.1.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", @@ -5660,6 +6036,46 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/character-entities": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", + "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-html4": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", + "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-entities-legacy": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", + "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/character-reference-invalid": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", + "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/cheerio": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.2.0.tgz", @@ -5763,6 +6179,21 @@ "react-dom": "^18 || ^19 || ^19.0.0-rc" } }, + "node_modules/codemirror": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/codemirror/-/codemirror-6.0.2.tgz", + "integrity": "sha512-VhydHotNW5w1UGK0Qj96BwSk/Zqbp9WbnyK2W/eVMv4QyF41INRGpjUhFJY7/uDNuudSc33a/PKr4iDqRduvHw==", + "license": "MIT", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/commands": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0" + } + }, "node_modules/color-convert": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", @@ -5783,6 +6214,16 @@ "dev": true, "license": "MIT" }, + "node_modules/comma-separated-tokens": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", + "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/commander": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz", @@ -5836,6 +6277,12 @@ "url": "https://opencollective.com/core-js" } }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==", + "license": "MIT" + }, "node_modules/cross-spawn": { "version": "7.0.6", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", @@ -6058,6 +6505,19 @@ "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==", "license": "MIT" }, + "node_modules/decode-named-character-reference": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.3.0.tgz", + "integrity": "sha512-GtpQYB283KrPp6nRw50q3U9/VfOutZOe103qlN7BPP6Ad27xYnOIWv4lPzo8HCAL+mMZofJ9KEy30fq6MfaK6Q==", + "license": "MIT", + "dependencies": { + "character-entities": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/deep-is": { "version": "0.1.4", "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.4.tgz", @@ -6065,6 +6525,15 @@ "dev": true, "license": "MIT" }, + "node_modules/dequal": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", + "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, "node_modules/detect-libc": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.1.2.tgz", @@ -6080,6 +6549,19 @@ "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==", "license": "MIT" }, + "node_modules/devlop": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", + "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", + "license": "MIT", + "dependencies": { + "dequal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/didyoumean": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/didyoumean/-/didyoumean-1.2.2.tgz", @@ -6550,6 +7032,16 @@ "node": ">=4.0" } }, + "node_modules/estree-util-is-identifier-name": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", + "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/esutils": { "version": "2.0.3", "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", @@ -6572,6 +7064,12 @@ "integrity": "sha512-LmDxfWXwcTArk8fUEnOfSZpHOJ6zOMUJKOtFLFqJLoKJetuQG874Uc7/Kki7zFLzYybmZhp1M7+98pfMqeX8yA==", "license": "MIT" }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "license": "MIT" + }, "node_modules/fast-deep-equal": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", @@ -6882,6 +7380,56 @@ "node": ">= 0.4" } }, + "node_modules/hast-util-to-jsx-runtime": { + "version": "2.3.6", + "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", + "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", + "license": "MIT", + "dependencies": { + "@types/estree": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/unist": "^3.0.0", + "comma-separated-tokens": "^2.0.0", + "devlop": "^1.0.0", + "estree-util-is-identifier-name": "^3.0.0", + "hast-util-whitespace": "^3.0.0", + "mdast-util-mdx-expression": "^2.0.0", + "mdast-util-mdx-jsx": "^3.0.0", + "mdast-util-mdxjs-esm": "^2.0.0", + "property-information": "^7.0.0", + "space-separated-tokens": "^2.0.0", + "style-to-js": "^1.0.0", + "unist-util-position": "^5.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/hast-util-whitespace": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", + "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/html-url-attributes": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/html-url-attributes/-/html-url-attributes-3.0.1.tgz", + "integrity": "sha512-ol6UPyBWqsrO6EJySPz2O7ZSr856WDrEzM5zMqp+FJJLGMW35cLYmmZnl0vztAZxRUoNZJFTCohfjuIJ8I4QBQ==", + "license": "MIT", + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/htmlparser2": { "version": "10.1.0", "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-10.1.0.tgz", @@ -6983,6 +7531,12 @@ "node": ">=0.8.19" } }, + "node_modules/inline-style-parser": { + "version": "0.2.7", + "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.7.tgz", + "integrity": "sha512-Nb2ctOyNR8DqQoR0OwRG95uNWIC0C1lCgf5Naz5H6Ji72KZ8OcFZLz2P5sNgwlyoJ8Yif11oMuYs5pBQa86csA==", + "license": "MIT" + }, "node_modules/input-otp": { "version": "1.4.2", "resolved": "https://registry.npmjs.org/input-otp/-/input-otp-1.4.2.tgz", @@ -7002,6 +7556,30 @@ "node": ">=12" } }, + "node_modules/is-alphabetical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", + "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/is-alphanumerical": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", + "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", + "license": "MIT", + "dependencies": { + "is-alphabetical": "^2.0.0", + "is-decimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-binary-path": { "version": "2.1.0", "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", @@ -7030,6 +7608,16 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-decimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", + "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-extglob": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", @@ -7051,6 +7639,16 @@ "node": ">=0.10.0" } }, + "node_modules/is-hexadecimal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", + "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/is-number": { "version": "7.0.0", "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", @@ -7060,6 +7658,18 @@ "node": ">=0.12.0" } }, + "node_modules/is-plain-obj": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", + "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/isbot": { "version": "5.1.38", "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.38.tgz", @@ -7479,6 +8089,16 @@ "dev": true, "license": "MIT" }, + "node_modules/longest-streak": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", + "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/loose-envify": { "version": "1.4.0", "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", @@ -7593,6 +8213,298 @@ "@jridgewell/sourcemap-codec": "^1.5.5" } }, + "node_modules/markdown-table": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", + "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/mdast-util-find-and-replace": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", + "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "escape-string-regexp": "^5.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", + "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/mdast-util-from-markdown": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", + "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark": "^4.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", + "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", + "license": "MIT", + "dependencies": { + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-gfm-autolink-literal": "^2.0.0", + "mdast-util-gfm-footnote": "^2.0.0", + "mdast-util-gfm-strikethrough": "^2.0.0", + "mdast-util-gfm-table": "^2.0.0", + "mdast-util-gfm-task-list-item": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-autolink-literal": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", + "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "ccount": "^2.0.0", + "devlop": "^1.0.0", + "mdast-util-find-and-replace": "^3.0.0", + "micromark-util-character": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-strikethrough": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", + "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-table": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", + "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "markdown-table": "^3.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-gfm-task-list-item": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", + "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-expression": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", + "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdx-jsx": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", + "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "ccount": "^2.0.0", + "devlop": "^1.1.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0", + "parse-entities": "^4.0.0", + "stringify-entities": "^4.0.0", + "unist-util-stringify-position": "^4.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-mdxjs-esm": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", + "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.0", + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "mdast-util-from-markdown": "^2.0.0", + "mdast-util-to-markdown": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-phrasing": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", + "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-hast": { + "version": "13.2.1", + "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.1.tgz", + "integrity": "sha512-cctsq2wp5vTsLIcaymblUriiTcZd0CwWtCbLvrOzYCDZoWyMNV8sZ7krj09FSnsiJi3WVsHLM4k6Dq/yaPyCXA==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "@ungap/structured-clone": "^1.0.0", + "devlop": "^1.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "trim-lines": "^3.0.0", + "unist-util-position": "^5.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-markdown": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", + "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "@types/unist": "^3.0.0", + "longest-streak": "^3.0.0", + "mdast-util-phrasing": "^4.0.0", + "mdast-util-to-string": "^4.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-decode-string": "^2.0.0", + "unist-util-visit": "^5.0.0", + "zwitch": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/mdast-util-to-string": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", + "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -7603,6 +8515,569 @@ "node": ">= 8" } }, + "node_modules/micromark": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", + "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "@types/debug": "^4.0.0", + "debug": "^4.0.0", + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-core-commonmark": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", + "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "devlop": "^1.0.0", + "micromark-factory-destination": "^2.0.0", + "micromark-factory-label": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-factory-title": "^2.0.0", + "micromark-factory-whitespace": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-subtokenize": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-extension-gfm": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", + "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", + "license": "MIT", + "dependencies": { + "micromark-extension-gfm-autolink-literal": "^2.0.0", + "micromark-extension-gfm-footnote": "^2.0.0", + "micromark-extension-gfm-strikethrough": "^2.0.0", + "micromark-extension-gfm-table": "^2.0.0", + "micromark-extension-gfm-tagfilter": "^2.0.0", + "micromark-extension-gfm-task-list-item": "^2.0.0", + "micromark-util-combine-extensions": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-autolink-literal": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", + "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-footnote": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", + "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-core-commonmark": "^2.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-normalize-identifier": "^2.0.0", + "micromark-util-sanitize-uri": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-strikethrough": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", + "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-classify-character": "^2.0.0", + "micromark-util-resolve-all": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-table": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", + "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-tagfilter": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", + "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-extension-gfm-task-list-item": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", + "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/micromark-factory-destination": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", + "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-label": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", + "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-space": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", + "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-title": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", + "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-factory-whitespace": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", + "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-factory-space": "^2.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-character": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", + "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-chunked": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", + "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-classify-character": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", + "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-combine-extensions": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", + "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-chunked": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-numeric-character-reference": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", + "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-decode-string": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", + "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "decode-named-character-reference": "^1.0.0", + "micromark-util-character": "^2.0.0", + "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-encode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", + "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-html-tag-name": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", + "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-normalize-identifier": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", + "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-resolve-all": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", + "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-sanitize-uri": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", + "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-character": "^2.0.0", + "micromark-util-encode": "^2.0.0", + "micromark-util-symbol": "^2.0.0" + } + }, + "node_modules/micromark-util-subtokenize": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", + "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "devlop": "^1.0.0", + "micromark-util-chunked": "^2.0.0", + "micromark-util-symbol": "^2.0.0", + "micromark-util-types": "^2.0.0" + } + }, + "node_modules/micromark-util-symbol": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", + "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, + "node_modules/micromark-util-types": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", + "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT" + }, "node_modules/micromatch": { "version": "4.0.8", "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", @@ -7840,6 +9315,31 @@ "node": ">=6" } }, + "node_modules/parse-entities": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", + "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^2.0.0", + "character-entities-legacy": "^3.0.0", + "character-reference-invalid": "^2.0.0", + "decode-named-character-reference": "^1.0.0", + "is-alphanumerical": "^2.0.0", + "is-decimal": "^2.0.0", + "is-hexadecimal": "^2.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/parse-entities/node_modules/@types/unist": { + "version": "2.0.11", + "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", + "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", + "license": "MIT" + }, "node_modules/parse5": { "version": "7.3.0", "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", @@ -8184,6 +9684,16 @@ "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", "license": "MIT" }, + "node_modules/property-information": { + "version": "7.1.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", + "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/punycode": { "version": "2.3.1", "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", @@ -8296,6 +9806,33 @@ "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==", "license": "MIT" }, + "node_modules/react-markdown": { + "version": "10.1.0", + "resolved": "https://registry.npmjs.org/react-markdown/-/react-markdown-10.1.0.tgz", + "integrity": "sha512-qKxVopLT/TyA6BX3Ue5NwabOsAzm0Q7kAPwq6L+wWDwisYs7R8vZ0nRXqq6rkueboxpkjvLGU9fWifiX/ZZFxQ==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "devlop": "^1.0.0", + "hast-util-to-jsx-runtime": "^2.0.0", + "html-url-attributes": "^3.0.0", + "mdast-util-to-hast": "^13.0.0", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.0.0", + "unified": "^11.0.0", + "unist-util-visit": "^5.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + }, + "peerDependencies": { + "@types/react": ">=18", + "react": ">=18" + } + }, "node_modules/react-refresh": { "version": "0.18.0", "resolved": "https://registry.npmjs.org/react-refresh/-/react-refresh-0.18.0.tgz", @@ -8482,6 +10019,72 @@ "decimal.js-light": "^2.4.1" } }, + "node_modules/remark-gfm": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", + "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-gfm": "^3.0.0", + "micromark-extension-gfm": "^3.0.0", + "remark-parse": "^11.0.0", + "remark-stringify": "^11.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-parse": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", + "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-from-markdown": "^2.0.0", + "micromark-util-types": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-rehype": { + "version": "11.1.2", + "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", + "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", + "license": "MIT", + "dependencies": { + "@types/hast": "^3.0.0", + "@types/mdast": "^4.0.0", + "mdast-util-to-hast": "^13.0.0", + "unified": "^11.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/remark-stringify": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", + "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", + "license": "MIT", + "dependencies": { + "@types/mdast": "^4.0.0", + "mdast-util-to-markdown": "^2.0.0", + "unified": "^11.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/resolve": { "version": "1.22.12", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.12.tgz", @@ -8758,6 +10361,16 @@ "node": ">=0.10.0" } }, + "node_modules/space-separated-tokens": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", + "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/srvx": { "version": "0.11.15", "resolved": "https://registry.npmjs.org/srvx/-/srvx-0.11.15.tgz", @@ -8770,6 +10383,20 @@ "node": ">=20.16.0" } }, + "node_modules/stringify-entities": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", + "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", + "license": "MIT", + "dependencies": { + "character-entities-html4": "^2.0.0", + "character-entities-legacy": "^3.0.0" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/strip-json-comments": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", @@ -8783,6 +10410,30 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/style-mod": { + "version": "4.1.3", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.3.tgz", + "integrity": "sha512-i/n8VsZydrugj3Iuzll8+x/00GH2vnYsk1eomD8QiRrSAeW6ItbCQDtfXCeJHd0iwiNagqjQkvpvREEPtW3IoQ==", + "license": "MIT" + }, + "node_modules/style-to-js": { + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.21.tgz", + "integrity": "sha512-RjQetxJrrUJLQPHbLku6U/ocGtzyjbJMP9lCNK7Ag0CNh690nSH8woqWH9u16nMjYBAok+i7JO1NP2pOy8IsPQ==", + "license": "MIT", + "dependencies": { + "style-to-object": "1.0.14" + } + }, + "node_modules/style-to-object": { + "version": "1.0.14", + "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.14.tgz", + "integrity": "sha512-LIN7rULI0jBscWQYaSswptyderlarFkjQ+t79nzty8tcIAceVomEVlLzH5VP4Cmsv6MtKhs7qaAiwlcp+Mgaxw==", + "license": "MIT", + "dependencies": { + "inline-style-parser": "0.2.7" + } + }, "node_modules/sucrase": { "version": "3.35.1", "resolved": "https://registry.npmjs.org/sucrase/-/sucrase-3.35.1.tgz", @@ -8934,6 +10585,26 @@ "node": ">=8.0" } }, + "node_modules/trim-lines": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", + "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, + "node_modules/trough": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", + "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } + }, "node_modules/ts-api-utils": { "version": "2.5.0", "resolved": "https://registry.npmjs.org/ts-api-utils/-/ts-api-utils-2.5.0.tgz", @@ -9546,6 +11217,93 @@ "pathe": "^2.0.3" } }, + "node_modules/unified": { + "version": "11.0.5", + "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", + "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "bail": "^2.0.0", + "devlop": "^1.0.0", + "extend": "^3.0.0", + "is-plain-obj": "^4.0.0", + "trough": "^2.0.0", + "vfile": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-is": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.1.tgz", + "integrity": "sha512-LsiILbtBETkDz8I9p1dQ0uyRUWuaQzd/cuEeS1hoRSyW5E5XGmTzlwY1OrNzzakGowI9Dr/I8HVaw4hTtnxy8g==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-position": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", + "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-stringify-position": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", + "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.1.0.tgz", + "integrity": "sha512-m+vIdyeCOpdr/QeQCu2EzxX/ohgS8KbnPDgFni4dQsfSCtpz8UqDyY5GjRru8PDKuYn7Fq19j1CQ+nJSsGKOzg==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0", + "unist-util-visit-parents": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/unist-util-visit-parents": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.2.tgz", + "integrity": "sha512-goh1s1TBrqSqukSc8wrjwWhL0hiJxgA8m4kFxGlQ+8FYQ3C/m11FcTs4YYem7V664AhHVvgoQLk890Ssdsr2IQ==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-is": "^6.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/unplugin": { "version": "2.3.11", "resolved": "https://registry.npmjs.org/unplugin/-/unplugin-2.3.11.tgz", @@ -9673,6 +11431,34 @@ "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc" } }, + "node_modules/vfile": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", + "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "vfile-message": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, + "node_modules/vfile-message": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", + "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", + "license": "MIT", + "dependencies": { + "@types/unist": "^3.0.0", + "unist-util-stringify-position": "^4.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/unified" + } + }, "node_modules/victory-vendor": { "version": "36.9.2", "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", @@ -10287,6 +12073,12 @@ } } }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==", + "license": "MIT" + }, "node_modules/webpack-virtual-modules": { "version": "0.6.2", "resolved": "https://registry.npmjs.org/webpack-virtual-modules/-/webpack-virtual-modules-0.6.2.tgz", @@ -10938,6 +12730,16 @@ "funding": { "url": "https://github.com/sponsors/colinhacks" } + }, + "node_modules/zwitch": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", + "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", + "license": "MIT", + "funding": { + "type": "github", + "url": "https://github.com/sponsors/wooorm" + } } } } diff --git a/src/components/mika/cronjobs/CronjobCard.tsx b/src/components/mika/cronjobs/CronjobCard.tsx index c59ba99..955e723 100644 --- a/src/components/mika/cronjobs/CronjobCard.tsx +++ b/src/components/mika/cronjobs/CronjobCard.tsx @@ -23,6 +23,7 @@ import { useUpdateCronjobStatus, } from "@/hooks/use-cronjobs"; import { useAvailableMcps, useUserIntegrations } from "@/hooks/use-integrations"; +import { syncAgentRuntime } from "@/lib/sync-agent-runtime"; interface Props { job: ScheduledJob; @@ -45,6 +46,8 @@ export function CronjobCard({ job }: Props) { const isActive = job.status === "active"; const isAutoPaused = job.status === "auto_paused"; + const hasRuntimeError = job.runtime_state === "error" || job.runtime_last_status === "error"; + const runtimeErrorText = job.runtime_last_delivery_error ?? job.runtime_last_error; async function toggle() { try { @@ -53,6 +56,11 @@ export function CronjobCard({ job }: Props) { status: isActive ? "paused" : "active", }); toast.success(isActive ? "Automação pausada." : "Automação ativada."); + + const { error: syncError } = await syncAgentRuntime(job.agent_instance_id, "cronjobs"); + if (syncError) { + toast.warning("Status salvo, mas o runtime do agente não sincronizou."); + } } catch (e) { toast.error(e instanceof Error ? e.message : "Erro ao atualizar."); } @@ -63,6 +71,11 @@ export function CronjobCard({ job }: Props) { await deleteMut.mutateAsync(job.id); toast.success("Automação excluída."); setConfirmDelete(false); + + const { error: syncError } = await syncAgentRuntime(job.agent_instance_id, "cronjobs"); + if (syncError) { + toast.warning("Automação removida, mas o runtime do agente não sincronizou."); + } } catch (e) { toast.error(e instanceof Error ? e.message : "Erro ao excluir."); } @@ -76,11 +89,15 @@ export function CronjobCard({ job }: Props) {

{job.name}

{isActive && Ativa} {job.status === "paused" && Pausada} + {job.status === "error" && Erro} {isAutoPaused && ( Auto-pausada )} + {hasRuntimeError && !isAutoPaused && ( + Runtime com erro + )}

{job.human_readable} @@ -92,6 +109,10 @@ export function CronjobCard({ job }: Props) {

{job.auto_paused_reason}

)} + {hasRuntimeError && runtimeErrorText && ( +

{runtimeErrorText}

+ )} + {missingMcps.length > 0 && isActive && (
diff --git a/src/components/mika/cronjobs/CronjobWizard.tsx b/src/components/mika/cronjobs/CronjobWizard.tsx index 69d40f0..5b8aff8 100644 --- a/src/components/mika/cronjobs/CronjobWizard.tsx +++ b/src/components/mika/cronjobs/CronjobWizard.tsx @@ -10,6 +10,7 @@ import { Label } from "@/components/ui/label"; import { Textarea } from "@/components/ui/textarea"; import { Badge } from "@/components/ui/badge"; import { invokeFunction } from "@/lib/invoke-function"; +import { syncAgentRuntime } from "@/lib/sync-agent-runtime"; import { useCreateCronjob } from "@/hooks/use-cronjobs"; import { useAvailableMcps, useUserIntegrations } from "@/hooks/use-integrations"; import { useAgentInstance } from "@/hooks/use-agent-instance"; @@ -131,6 +132,12 @@ export function CronjobWizard({ onCreated, onCancel }: Props) { next_run_at: parsed?.next_run_at ?? null, }); toast.success("Automação criada!"); + + const { error: syncError } = await syncAgentRuntime(agent.id, "cronjobs"); + if (syncError) { + toast.warning("Automação criada, mas o runtime do agente não sincronizou."); + } + onCreated?.(job.id); } catch (e) { const msg = e instanceof Error ? e.message : "Erro ao criar automação"; diff --git a/src/components/mika/integrations/DisconnectMCPDialog.tsx b/src/components/mika/integrations/DisconnectMCPDialog.tsx index 8b94891..7551e16 100644 --- a/src/components/mika/integrations/DisconnectMCPDialog.tsx +++ b/src/components/mika/integrations/DisconnectMCPDialog.tsx @@ -43,7 +43,11 @@ export function DisconnectMCPDialog({ async function handleDisconnect() { setSubmitting(true); - const { error } = await invokeFunction("disconnect-integration", { + const { data, error } = await invokeFunction<{ + success: boolean; + paused_jobs_count: number; + runtime_sync_warning?: string | null; + }>("disconnect-integration", { integration_id: integrationId, }); setSubmitting(false); @@ -52,6 +56,9 @@ export function DisconnectMCPDialog({ return; } toast.success(`${mcpName} desconectado.`); + if (data?.runtime_sync_warning) { + toast.warning("Integração removida, mas o runtime do agente não sincronizou."); + } queryClient.invalidateQueries({ queryKey: ["user-integrations"] }); queryClient.invalidateQueries({ queryKey: ["user-integration-limits"] }); onOpenChange(false); diff --git a/src/components/mika/skills/SkillCard.tsx b/src/components/mika/skills/SkillCard.tsx index 09de28e..687071f 100644 --- a/src/components/mika/skills/SkillCard.tsx +++ b/src/components/mika/skills/SkillCard.tsx @@ -8,6 +8,7 @@ import { formatDistanceToNow } from "date-fns"; import { ptBR } from "date-fns/locale"; import { toast } from "sonner"; import { supabase } from "@/integrations/supabase/client"; +import { syncAgentSkills } from "@/lib/sync-agent-skills"; import { Button } from "@/components/ui/button"; import { DropdownMenu, @@ -39,6 +40,15 @@ export function SkillCard({ skill }: { skill: Skill }) { const isArchived = skill.status === "archived"; + async function syncRuntimeAfterMutation(actionLabel: string) { + const { error } = await syncAgentSkills(skill.agent_instance_id); + if (error) { + toast.warning(`${actionLabel}, mas o sync com o container falhou.`, { + description: error.message, + }); + } + } + const updateStatus = useMutation({ mutationFn: async (newStatus: string) => { const { error } = await supabase @@ -56,16 +66,21 @@ export function SkillCard({ skill }: { skill: Skill }) { const handleToggleActive = () => { const next = skill.status === "active" ? "disabled" : "active"; updateStatus.mutate(next, { - onSuccess: () => toast.success(next === "active" ? "Skill ativada" : "Skill desativada"), + onSuccess: async () => { + const actionLabel = next === "active" ? "Skill ativada" : "Skill desativada"; + toast.success(actionLabel); + await syncRuntimeAfterMutation(actionLabel); + }, onError: (e: unknown) => toast.error(e instanceof Error ? e.message : "Erro ao atualizar"), }); }; const handleArchive = () => { updateStatus.mutate("archived", { - onSuccess: () => { + onSuccess: async () => { toast.success("Skill arquivada"); setConfirmArchive(false); + await syncRuntimeAfterMutation("Skill arquivada"); }, onError: (e: unknown) => toast.error(e instanceof Error ? e.message : "Erro ao arquivar"), }); @@ -73,7 +88,10 @@ export function SkillCard({ skill }: { skill: Skill }) { const handleRestore = () => { updateStatus.mutate("draft", { - onSuccess: () => toast.success("Skill restaurada como rascunho"), + onSuccess: async () => { + toast.success("Skill restaurada como rascunho"); + await syncRuntimeAfterMutation("Skill restaurada como rascunho"); + }, onError: (e: unknown) => { if ((e as { code?: string })?.code === "23505") { toast.error("Já existe outra skill ativa com esse nome. Renomeie antes de restaurar."); @@ -89,11 +107,12 @@ export function SkillCard({ skill }: { skill: Skill }) { const { error } = await supabase.from("skills").delete().eq("id", skill.id); if (error) throw error; }, - onSuccess: () => { + onSuccess: async () => { toast.success("Skill deletada"); qc.invalidateQueries({ queryKey: ["skills"] }); qc.invalidateQueries({ queryKey: ["user-limits"] }); setConfirmDelete(false); + await syncRuntimeAfterMutation("Skill deletada"); }, onError: (e: unknown) => toast.error(e instanceof Error ? e.message : "Erro ao deletar"), }); diff --git a/src/hooks/use-cronjobs.ts b/src/hooks/use-cronjobs.ts index 694a2fc..6dd65c7 100644 --- a/src/hooks/use-cronjobs.ts +++ b/src/hooks/use-cronjobs.ts @@ -15,10 +15,15 @@ export interface ScheduledJob { human_readable: string; action_prompt: string; required_mcp_slugs: string[]; - status: "active" | "paused" | "auto_paused"; + status: "active" | "paused" | "auto_paused" | "error" | "archived"; auto_paused_reason: string | null; last_run_at: string | null; next_run_at: string | null; + runtime_state: "scheduled" | "paused" | "completed" | "error" | null; + runtime_last_status: "ok" | "error" | null; + runtime_last_error: string | null; + runtime_last_delivery_error: string | null; + runtime_synced_at: string | null; timezone: string; created_at: string; updated_at: string; diff --git a/src/integrations/supabase/types.ts b/src/integrations/supabase/types.ts index 23a8b39..9e43fae 100644 --- a/src/integrations/supabase/types.ts +++ b/src/integrations/supabase/types.ts @@ -475,6 +475,11 @@ export type Database = { natural_language_input: string next_run_at: string | null required_mcp_slugs: Json + runtime_last_delivery_error: string | null + runtime_last_error: string | null + runtime_last_status: string | null + runtime_state: string | null + runtime_synced_at: string | null status: string timezone: string updated_at: string @@ -494,6 +499,11 @@ export type Database = { natural_language_input: string next_run_at?: string | null required_mcp_slugs?: Json + runtime_last_delivery_error?: string | null + runtime_last_error?: string | null + runtime_last_status?: string | null + runtime_state?: string | null + runtime_synced_at?: string | null status?: string timezone?: string updated_at?: string @@ -513,6 +523,11 @@ export type Database = { natural_language_input?: string next_run_at?: string | null required_mcp_slugs?: Json + runtime_last_delivery_error?: string | null + runtime_last_error?: string | null + runtime_last_status?: string | null + runtime_state?: string | null + runtime_synced_at?: string | null status?: string timezone?: string updated_at?: string diff --git a/src/lib/ollama-models.ts b/src/lib/ollama-models.ts new file mode 100644 index 0000000..fea2d92 --- /dev/null +++ b/src/lib/ollama-models.ts @@ -0,0 +1,35 @@ +export const DEFAULT_OLLAMA_PROVIDER = "ollama-cloud"; +export const DEFAULT_OLLAMA_MODEL = "gemma4:31b-cloud"; + +const LEGACY_MODEL_ALIASES: Record = { + "openrouter/google/gemma-4-27b-a4b-it": DEFAULT_OLLAMA_MODEL, + "openrouter/google/gemma-4-31b-it": DEFAULT_OLLAMA_MODEL, + "ollama-cloud/gemma4:31b-cloud": DEFAULT_OLLAMA_MODEL, +}; + +export function normalizeOllamaModelSelection(value?: string | null): string { + const trimmed = (value ?? "").trim(); + if (!trimmed) return DEFAULT_OLLAMA_MODEL; + + const mapped = LEGACY_MODEL_ALIASES[trimmed]; + if (mapped) return mapped; + + if (trimmed.includes("/") && trimmed.includes(":")) { + const candidate = trimmed.split("/").pop()?.trim(); + if (candidate) return candidate; + } + + return trimmed; +} + +export function formatOllamaModelLabel(modelName: string): string { + if (modelName === DEFAULT_OLLAMA_MODEL) { + return "Gemma 4 31B Cloud — Padrão Mika"; + } + + if (modelName.startsWith("gemma4:")) { + return modelName.replace("gemma4:", "Gemma 4 "); + } + + return modelName; +} diff --git a/src/lib/sync-agent-runtime.ts b/src/lib/sync-agent-runtime.ts new file mode 100644 index 0000000..b4ebbf5 --- /dev/null +++ b/src/lib/sync-agent-runtime.ts @@ -0,0 +1,24 @@ +import { invokeFunction } from "@/lib/invoke-function"; + +type RuntimeSyncScope = "cronjobs" | "integrations" | "all"; + +export async function syncAgentRuntime( + agentInstanceId: string, + scope: RuntimeSyncScope = "all", +) { + return await invokeFunction<{ + success: boolean; + agent_instance_id: string; + public_url: string; + public_domain: string; + cronjobs_synced_count: number; + integrations_synced_count: number; + runtime_responses: { + cronjobs: unknown; + integrations: unknown; + }; + }>("sync-agent-runtime", { + agent_instance_id: agentInstanceId, + scope, + }); +} diff --git a/src/lib/sync-agent-skills.ts b/src/lib/sync-agent-skills.ts new file mode 100644 index 0000000..f52e289 --- /dev/null +++ b/src/lib/sync-agent-skills.ts @@ -0,0 +1,18 @@ +"use client"; + +import { invokeFunction } from "@/lib/invoke-function"; + +export interface SyncAgentSkillsResponse { + success?: boolean; + agent_instance_id?: string; + public_url?: string; + public_domain?: string; + synced_count?: number; + runtime_response?: unknown; +} + +export async function syncAgentSkills(agentInstanceId: string) { + return await invokeFunction("sync-agent-skills", { + agent_instance_id: agentInstanceId, + }); +} diff --git a/src/routes/admin.agente.$id.tsx b/src/routes/admin.agente.$id.tsx index c922d8d..aff3a95 100644 --- a/src/routes/admin.agente.$id.tsx +++ b/src/routes/admin.agente.$id.tsx @@ -16,6 +16,11 @@ import { import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/hooks/use-auth"; import { invokeFunction } from "@/lib/invoke-function"; +import { + DEFAULT_OLLAMA_MODEL, + formatOllamaModelLabel, + normalizeOllamaModelSelection, +} from "@/lib/ollama-models"; import { Button } from "@/components/ui/button"; import { Badge } from "@/components/ui/badge"; import { Skeleton } from "@/components/ui/skeleton"; @@ -34,19 +39,6 @@ export const Route = createFileRoute("/admin/agente/$id")({ component: AgentDetailPage, }); -const MODEL_OPTIONS = [ - { - value: "openrouter/google/gemma-4-27b-a4b-it", - label: "Gemma 4 27B — Rápido e gratuito (Basic/Starter)", - plans: ["basic", "starter"], - }, - { - value: "openrouter/google/gemma-4-31b-it", - label: "Gemma 4 31B — Mais capaz (Professional)", - plans: ["professional", "enterprise"], - }, -]; - interface AgentDetail { id: string; user_id: string; @@ -73,6 +65,15 @@ interface AgentDetail { subscription: { plans: { slug: string; name: string } | null } | null; } +interface OllamaModelRow { + name: string; + raw_name: string; + modified_at: string | null; + size: number | null; + digest: string | null; + details: Record; +} + function AgentDetailPage() { const { id } = Route.useParams(); const { user, loading: authLoading } = useAuth(); @@ -178,6 +179,19 @@ function AgentDetailPage() { }, }); + const { data: availableModels } = useQuery({ + queryKey: ["ollama-models"], + enabled: isAdmin === true, + staleTime: 5 * 60_000, + queryFn: async () => { + const { data, error } = await invokeFunction<{ + models?: OllamaModelRow[]; + }>("list-ollama-models"); + if (error) throw new Error(error.message); + return data?.models ?? []; + }, + }); + // Backfill: se o agente não tem telegram_user_chat_id mas já recebeu mensagens, // pega a primeira mensagem incoming e popula automaticamente. useEffect(() => { @@ -210,11 +224,7 @@ function AgentDetailPage() { // ===== Estado do formulário ===== const fullName = agent?.profile?.full_name?.trim() || "Usuário"; const firstName = fullName.split(" ")[0] || "Usuário"; - const planSlug = agent?.subscription?.plans?.slug ?? "basic"; - const isPro = ["professional", "enterprise"].includes(planSlug); - const defaultModel = isPro - ? "openrouter/google/gemma-4-31b-it" - : "openrouter/google/gemma-4-27b-a4b-it"; + const defaultModel = DEFAULT_OLLAMA_MODEL; const cfg = (agent?.model_config ?? {}) as Record; const defaultAgentName = agent?.agent_name?.trim() || cfg.agent_name || `Mika de ${firstName}`; @@ -231,16 +241,29 @@ function AgentDetailPage() { const [tts, setTts] = useState("disabled"); const [busy, setBusy] = useState(false); const [initialized, setInitialized] = useState(false); + const modelOptions = useMemo(() => { + const options = new Map(); + options.set(DEFAULT_OLLAMA_MODEL, formatOllamaModelLabel(DEFAULT_OLLAMA_MODEL)); + + for (const row of availableModels ?? []) { + options.set(row.name, formatOllamaModelLabel(row.name)); + } + + const currentModel = normalizeOllamaModelSelection(cfg.model || cfg.provider || defaultModel); + options.set(currentModel, formatOllamaModelLabel(currentModel)); + + return Array.from(options.entries()).map(([value, label]) => ({ value, label })); + }, [availableModels, cfg.model, cfg.provider, defaultModel]); useEffect(() => { if (!agent || initialized) return; setAgentName(defaultAgentName); setSoul(defaultSoul); - setModel(cfg.provider || defaultModel); + setModel(normalizeOllamaModelSelection(cfg.model || cfg.provider || defaultModel)); setStt(cfg.stt || "local"); setTts(cfg.tts || "disabled"); setInitialized(true); - }, [agent, initialized, defaultAgentName, defaultSoul, cfg.provider, cfg.stt, cfg.tts, defaultModel]); + }, [agent, initialized, defaultAgentName, defaultSoul, cfg.model, cfg.provider, cfg.stt, cfg.tts, defaultModel]); // ===== Auth guard ===== useEffect(() => { @@ -479,13 +502,16 @@ function AgentDetailPage() { - {MODEL_OPTIONS.map((m) => ( + {modelOptions.map((m) => ( {m.label} ))} +

+ Catálogo carregado dinamicamente do Ollama Cloud. +

diff --git a/src/routes/painel.cronjobs.$id.tsx b/src/routes/painel.cronjobs.$id.tsx index 4704c8b..a7809d9 100644 --- a/src/routes/painel.cronjobs.$id.tsx +++ b/src/routes/painel.cronjobs.$id.tsx @@ -17,11 +17,13 @@ import { AlertDialogTitle, } from "@/components/ui/alert-dialog"; import { + type ScheduledJob, useCronjob, useDeleteCronjob, useUpdateCronjobStatus, } from "@/hooks/use-cronjobs"; import { useAvailableMcps, useUserIntegrations } from "@/hooks/use-integrations"; +import { syncAgentRuntime } from "@/lib/sync-agent-runtime"; export const Route = createFileRoute("/painel/cronjobs/$id")({ component: CronjobDetailPage, @@ -65,6 +67,7 @@ function CronjobDetailPage() { const isActive = job.status === "active"; const isAutoPaused = job.status === "auto_paused"; + const hasRuntimeError = job.runtime_state === "error" || job.runtime_last_status === "error"; async function toggle() { try { @@ -73,6 +76,11 @@ function CronjobDetailPage() { status: isActive ? "paused" : "active", }); toast.success(isActive ? "Pausada." : "Ativada."); + + const { error: syncError } = await syncAgentRuntime(job!.agent_instance_id, "cronjobs"); + if (syncError) { + toast.warning("Status salvo, mas o runtime do agente não sincronizou."); + } } catch (e) { toast.error(e instanceof Error ? e.message : "Erro"); } @@ -82,6 +90,12 @@ function CronjobDetailPage() { try { await deleteMut.mutateAsync(job!.id); toast.success("Excluída."); + + const { error: syncError } = await syncAgentRuntime(job!.agent_instance_id, "cronjobs"); + if (syncError) { + toast.warning("Automação removida, mas o runtime do agente não sincronizou."); + } + navigate({ to: "/painel/cronjobs" }); } catch (e) { toast.error(e instanceof Error ? e.message : "Erro"); @@ -102,11 +116,15 @@ function CronjobDetailPage() {
{isActive && Ativa} {job.status === "paused" && Pausada} + {job.status === "error" && Erro} {isAutoPaused && ( Auto-pausada )} + {hasRuntimeError && !isAutoPaused && ( + Runtime com erro + )}
@@ -138,6 +156,22 @@ function CronjobDetailPage() {
)} + {hasRuntimeError && ( +
+

Falha reportada pelo runtime

+

+ {job.runtime_last_error ?? "O runtime marcou a última execução como erro."} +

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

Falha na entrega do resultado

+

{job.runtime_last_delivery_error}

+
+ )} + {missingMcps.length > 0 && isActive && (

@@ -182,6 +216,23 @@ function CronjobDetailPage() { : "Nunca" } /> +

+
+
{job.description &&
} @@ -265,3 +316,29 @@ function Section({
); } + +function formatRuntimeState(state: ScheduledJob["runtime_state"]): string { + switch (state) { + case "scheduled": + return "Agendado"; + case "paused": + return "Pausado"; + case "completed": + return "Concluído"; + case "error": + return "Erro"; + default: + return "Desconhecido"; + } +} + +function formatRuntimeLastStatus(status: ScheduledJob["runtime_last_status"]): string { + switch (status) { + case "ok": + return "Sucesso"; + case "error": + return "Erro"; + default: + return "Ainda sem execução"; + } +} diff --git a/src/routes/painel.integracoes.$slug.tsx b/src/routes/painel.integracoes.$slug.tsx index 508c985..0335280 100644 --- a/src/routes/painel.integracoes.$slug.tsx +++ b/src/routes/painel.integracoes.$slug.tsx @@ -119,7 +119,11 @@ function IntegrationDetailPage() { async function handleRefresh() { setRefreshing(true); - const { error } = await invokeFunction("refresh-integration-token", { + const { data, error } = await invokeFunction<{ + success: boolean; + expires_at: string | null; + runtime_sync_warning?: string | null; + }>("refresh-integration-token", { integration_id: integration!.id, }); setRefreshing(false); @@ -127,6 +131,9 @@ function IntegrationDetailPage() { toast.error(error.message); } else { toast.success("Token renovado."); + if (data?.runtime_sync_warning) { + toast.warning("Token renovado, mas o runtime do agente não sincronizou."); + } } queryClient.invalidateQueries({ queryKey: ["user-integrations"] }); } diff --git a/src/routes/painel.skills.$id.tsx b/src/routes/painel.skills.$id.tsx index 0c1eb4b..90494f9 100644 --- a/src/routes/painel.skills.$id.tsx +++ b/src/routes/painel.skills.$id.tsx @@ -12,6 +12,7 @@ import { toast } from "sonner"; import { supabase } from "@/integrations/supabase/client"; import { useAuth } from "@/hooks/use-auth"; import { useSkill } from "@/hooks/use-skills"; +import { syncAgentSkills } from "@/lib/sync-agent-skills"; import { SkillStatusBadge } from "@/components/mika/skills/SkillStatusBadge"; import { SkillTestPanel } from "@/components/mika/skills/SkillTestPanel"; import { Button } from "@/components/ui/button"; @@ -131,7 +132,13 @@ function SkillDetailPage() { // Publish version const publishVersion = useMutation({ mutationFn: async (versionId: string) => { - const { data, error } = await supabase.functions.invoke("publish-skill-version", { + const { data, error } = await supabase.functions.invoke<{ + success?: boolean; + no_op?: boolean; + version_number?: number; + synced?: boolean; + sync_error?: string; + }>("publish-skill-version", { body: { skill_version_id: versionId }, }); if (error) throw error; @@ -140,6 +147,10 @@ function SkillDetailPage() { onSuccess: (data) => { if (data.no_op) { toast.info("Esta versão já está publicada"); + } else if (data.synced === false) { + toast.warning(`Versão ${data.version_number} publicada, mas o sync falhou.`, { + description: data.sync_error || "Tente novamente após o próximo deploy.", + }); } else { toast.success(`Versão ${data.version_number} publicada!`); } @@ -170,6 +181,13 @@ function SkillDetailPage() { toast.success("Skill arquivada"); qc.invalidateQueries({ queryKey: ["skills"] }); qc.invalidateQueries({ queryKey: ["user-limits"] }); + void syncAgentSkills(skill.data.agent_instance_id).then(({ error }) => { + if (error) { + toast.warning("Skill arquivada, mas o sync com o container falhou.", { + description: error.message, + }); + } + }); navigate({ to: "/painel/skills" }); }, onError: (e: unknown) => toast.error(e instanceof Error ? e.message : "Erro"), diff --git a/src/routes/painel.skills.preview.tsx b/src/routes/painel.skills.preview.tsx index 38b4920..0c35c2b 100644 --- a/src/routes/painel.skills.preview.tsx +++ b/src/routes/painel.skills.preview.tsx @@ -118,12 +118,20 @@ function SkillPreviewPage() { // 3. Optionally publish if (publish) { - const { data: pubData, error: pubErr } = await supabase.functions.invoke( + const { data: pubData, error: pubErr } = await supabase.functions.invoke<{ + success?: boolean; + synced?: boolean; + sync_error?: string; + }>( "publish-skill-version", { body: { skill_version_id: ver.id } }, ); if (pubErr) { toast.error("Skill salva, mas falha ao publicar: " + pubErr.message); + } else if (pubData?.synced === false) { + toast.warning("Skill publicada, mas o sync com o container falhou.", { + description: pubData.sync_error || "Tente novamente após o próximo deploy.", + }); } else { toast.success("Skill publicada com sucesso!"); } diff --git a/supabase/functions/_shared/hermes-config.ts b/supabase/functions/_shared/hermes-config.ts new file mode 100644 index 0000000..634b583 --- /dev/null +++ b/supabase/functions/_shared/hermes-config.ts @@ -0,0 +1,23 @@ +export const DEFAULT_OLLAMA_PROVIDER = "ollama-cloud"; +export const DEFAULT_OLLAMA_MODEL = "gemma4:31b-cloud"; + +const LEGACY_MODEL_ALIASES: Record = { + "openrouter/google/gemma-4-27b-a4b-it": DEFAULT_OLLAMA_MODEL, + "openrouter/google/gemma-4-31b-it": DEFAULT_OLLAMA_MODEL, + "ollama-cloud/gemma4:31b-cloud": DEFAULT_OLLAMA_MODEL, +}; + +export function normalizeOllamaModelSelection(value?: string | null): string { + const trimmed = (value ?? "").trim(); + if (!trimmed) return DEFAULT_OLLAMA_MODEL; + + const mapped = LEGACY_MODEL_ALIASES[trimmed]; + if (mapped) return mapped; + + if (trimmed.includes("/") && trimmed.includes(":")) { + const candidate = trimmed.split("/").pop()?.trim(); + if (candidate) return candidate; + } + + return trimmed; +} diff --git a/supabase/functions/_shared/railway.ts b/supabase/functions/_shared/railway.ts index 4429b27..1fe73a6 100644 --- a/supabase/functions/_shared/railway.ts +++ b/supabase/functions/_shared/railway.ts @@ -4,15 +4,13 @@ const RAILWAY_GRAPHQL = "https://backboard.railway.app/graphql/v2"; /** * Start command padrão dos containers Hermes. - * - Verifica HERMES_SUSPENDED no início: se true, dorme infinitamente (agente "pausado") - * - Aplica HERMES_SOUL_OVERRIDE em /opt/data/SOUL.md se presente - * - Inicia o gateway Hermes + * - Encaminha para o entrypoint custom da imagem `hermes-agent-custom` + * - O próprio entrypoint aplica SOUL.md, model/provider, STT/TTS e suspensão * * IMPORTANTE: este comando deve ser idêntico ao configurado nos serviços Railway * existentes. Para serviços antigos, atualize manualmente via UI/Agent do Railway. */ -export const HERMES_START_COMMAND = - `/bin/bash -c 'if [ "$HERMES_SUSPENDED" = "true" ]; then echo "Agent suspended" && sleep infinity; fi && if [ -n "$HERMES_SOUL_OVERRIDE" ]; then echo "$HERMES_SOUL_OVERRIDE" > /opt/data/SOUL.md; fi && /opt/hermes/docker/entrypoint.sh gateway run'`; +export const HERMES_START_COMMAND = `/opt/hermes-custom/entrypoint.sh`; export interface RailwayError { message: string; @@ -308,6 +306,151 @@ export async function getServiceEnvironmentId(opts: { return (await getServiceContext(opts)).environmentId; } +export interface RailwayServiceDomainInfo { + id: string; + domain: string; + suffix?: string | null; + certificateStatus?: string | null; +} + +export async function listRailwayServiceDomains(opts: { + token: string; + serviceId: string; + environmentId: string; + projectId?: string | null; +}): Promise<{ serviceDomains: RailwayServiceDomainInfo[]; customDomains: RailwayServiceDomainInfo[] }> { + const query = ` + query Domains($environmentId: String!, $serviceId: String!, $projectId: String) { + domains(environmentId: $environmentId, serviceId: $serviceId, projectId: $projectId) { + serviceDomains { + id + domain + suffix + } + customDomains { + id + domain + status { + certificateStatus + } + } + } + } + `; + const res = await railwayQuery<{ + domains: { + serviceDomains?: { id: string; domain: string; suffix?: string | null }[]; + customDomains?: { id: string; domain: string; status?: { certificateStatus?: string | null } | null }[]; + }; + }>( + query, + { + environmentId: opts.environmentId, + serviceId: opts.serviceId, + projectId: opts.projectId ?? null, + }, + opts.token, + ); + + if (res.errors?.length) { + throw new Error(`domains query failed: ${JSON.stringify(res.errors)}`); + } + + const serviceDomains = (res.data?.domains?.serviceDomains ?? []).map((item) => ({ + id: item.id, + domain: item.domain, + suffix: item.suffix ?? null, + certificateStatus: "ISSUED", + })); + + const customDomains = (res.data?.domains?.customDomains ?? []).map((item) => ({ + id: item.id, + domain: item.domain, + certificateStatus: item.status?.certificateStatus ?? null, + })); + + return { serviceDomains, customDomains }; +} + +export async function createRailwayServiceDomain(opts: { + token: string; + serviceId: string; + environmentId: string; + targetPort?: number; +}): Promise { + const mutation = ` + mutation ServiceDomainCreate($input: ServiceDomainCreateInput!) { + serviceDomainCreate(input: $input) { + id + domain + suffix + } + } + `; + + const input: Record = { + serviceId: opts.serviceId, + environmentId: opts.environmentId, + }; + if (opts.targetPort !== undefined) { + input.targetPort = opts.targetPort; + } + + const res = await railwayQuery<{ + serviceDomainCreate: { id: string; domain: string; suffix?: string | null }; + }>(mutation, { input }, opts.token); + + if (res.errors?.length) { + throw new Error(`serviceDomainCreate failed: ${JSON.stringify(res.errors)}`); + } + + const created = res.data?.serviceDomainCreate; + if (!created?.domain) { + throw new Error("serviceDomainCreate returned no domain"); + } + + return { + id: created.id, + domain: created.domain, + suffix: created.suffix ?? null, + certificateStatus: "ISSUED", + }; +} + +export async function ensureRailwayServiceDomain(opts: { + token: string; + serviceId: string; + environmentId: string; + projectId?: string | null; + targetPort?: number; +}): Promise { + const existing = await listRailwayServiceDomains({ + token: opts.token, + serviceId: opts.serviceId, + environmentId: opts.environmentId, + projectId: opts.projectId, + }); + + const serviceDomain = existing.serviceDomains.find((item) => item.domain); + if (serviceDomain) { + return serviceDomain; + } + + const issuedCustomDomain = existing.customDomains.find((item) => + item.domain && (!item.certificateStatus || item.certificateStatus === "ISSUED") + ); + if (issuedCustomDomain) { + return issuedCustomDomain; + } + + return await createRailwayServiceDomain({ + token: opts.token, + serviceId: opts.serviceId, + environmentId: opts.environmentId, + targetPort: opts.targetPort, + }); +} + /** Apaga o webhook do Telegram para que o Hermes assuma via polling. */ export async function deleteTelegramWebhook(botToken: string): Promise { const url = `https://api.telegram.org/bot${botToken}/deleteWebhook?drop_pending_updates=false`; diff --git a/supabase/functions/_shared/runtime-sync.ts b/supabase/functions/_shared/runtime-sync.ts new file mode 100644 index 0000000..4c85532 --- /dev/null +++ b/supabase/functions/_shared/runtime-sync.ts @@ -0,0 +1,1035 @@ +import { + ensureRailwayServiceDomain, + getServiceContext, +} from "./railway.ts"; + +interface AgentInstanceRecord { + id: string; + user_id: string; + railway_service_id: string | null; + vps_pool_id: string | null; +} + +interface ActiveSkillRow { + id: string; + agent_instance_id: string; + name: string; + description: string; + trigger_keywords: string; + current_version_id: string; + updated_at: string; +} + +interface SkillVersionRow { + id: string; + version_number: number; + markdown_content: string; + form_inputs: Record | null; + created_at: string; +} + +interface ScheduledJobRow { + id: string; + agent_instance_id: string; + name: string; + description: string | null; + natural_language_input: string; + cron_expression: string; + human_readable: string; + action_prompt: string; + required_mcp_slugs: string[] | null; + status: string; + auto_paused_reason: string | null; + last_run_at: string | null; + next_run_at: string | null; + timezone: string; + created_at: string; + updated_at: string; +} + +interface AvailableMcpRow { + id: string; + slug: string; + name: string; + provider: string; + description: string; + oauth_authorize_url: string; + oauth_token_url: string; + oauth_revoke_url: string | null; + required_scopes: string[] | null; + supports_refresh_token: boolean; +} + +interface UserIntegrationRow { + id: string; + user_id: string; + mcp_id: string; + status: string; + connected_account_email: string | null; + connected_account_name: string | null; + granted_scopes: string[] | null; + token_expires_at: string | null; + last_refreshed_at: string | null; + error_message: string | null; + access_token_vault_id: string | null; + refresh_token_vault_id: string | null; + created_at: string; + updated_at: string; + mcp: AvailableMcpRow | AvailableMcpRow[] | null; +} + +interface RuntimeTarget { + serviceId: string; + projectId: string; + environmentId: string; + publicUrl: string; + publicDomain: string; +} + +type RuntimeSyncScope = "cronjobs" | "integrations" | "all"; + +export interface AgentSkillsSyncPayload { + agent_instance_id: string; + synced_at: string; + skills: Array<{ + skill_id: string; + version_id: string; + version_number: number; + name: string; + description: string; + trigger_keywords: string; + updated_at: string; + created_at: string; + form_inputs: Record | null; + markdown_content: string; + }>; +} + +export interface AgentSkillsSyncResult { + agent_instance_id: string; + public_url: string; + public_domain: string; + synced_count: number; + response: unknown; +} + +export interface AgentCronjobsSyncPayload { + agent_instance_id: string; + synced_at: string; + cronjobs: Array<{ + job_id: string; + name: string; + description: string | null; + natural_language_input: string; + cron_expression: string; + human_readable: string; + action_prompt: string; + required_mcp_slugs: string[]; + status: string; + auto_paused_reason: string | null; + last_run_at: string | null; + next_run_at: string | null; + timezone: string; + created_at: string; + updated_at: string; + }>; +} + +export interface AgentCronjobsSyncResult { + agent_instance_id: string; + public_url: string; + public_domain: string; + synced_count: number; + response: unknown; +} + +interface RuntimeManagedCronjobRecord { + id: string; + state?: string | null; + last_status?: string | null; + last_error?: string | null; + last_delivery_error?: string | null; + last_run_at?: string | null; + next_run_at?: string | null; +} + +interface RuntimeCronjobsListResponse { + cronjobs?: RuntimeManagedCronjobRecord[]; +} + +export interface AgentCronjobsRuntimePullResult { + agent_instance_id: string; + public_url: string; + public_domain: string; + synced_count: number; + updated_count: number; + response: unknown; +} + +interface RuntimeMcpHint { + server_url: string | null; + transport: "streamable-http" | null; + auth_mode: "oauth" | "unknown"; + notes: string[]; +} + +export interface AgentIntegrationsSyncPayload { + agent_instance_id: string; + user_id: string; + synced_at: string; + integrations: Array<{ + integration_id: string; + mcp_id: string; + slug: string; + name: string; + provider: string; + description: string; + status: string; + connected_account_email: string | null; + connected_account_name: string | null; + granted_scopes: string[]; + required_scopes: string[]; + token_expires_at: string | null; + last_refreshed_at: string | null; + error_message: string | null; + oauth_authorize_url: string; + oauth_token_url: string; + oauth_revoke_url: string | null; + supports_refresh_token: boolean; + access_token: string | null; + refresh_token: string | null; + runtime_mcp: RuntimeMcpHint; + created_at: string; + updated_at: string; + }>; +} + +export interface AgentIntegrationsSyncResult { + agent_instance_id: string; + public_url: string; + public_domain: string; + synced_count: number; + response: unknown; +} + +export interface AgentRuntimeSyncResult { + agent_instance_id: string; + public_url: string; + public_domain: string; + cronjobs_synced_count: number; + integrations_synced_count: number; + responses: { + cronjobs: unknown; + integrations: unknown; + }; +} + +function yamlQuoted(value: string): string { + return JSON.stringify(value ?? ""); +} + +function ensureStringArray(value: unknown): string[] { + return Array.isArray(value) + ? value.filter((item): item is string => typeof item === "string" && item.trim().length > 0) + : []; +} + +function ensureSkillFrontmatter(skill: { + name: string; + description: string; + trigger_keywords: string; + markdown_content: string; +}): string { + const markdown = (skill.markdown_content ?? "").trim(); + if (markdown.startsWith("---")) { + return markdown; + } + + return [ + "---", + `name: ${yamlQuoted(skill.name)}`, + `description: ${yamlQuoted(skill.description)}`, + `trigger_keywords: ${yamlQuoted(skill.trigger_keywords)}`, + "---", + "", + markdown, + ].join("\n"); +} + +function sleep(ms: number): Promise { + return new Promise((resolve) => setTimeout(resolve, ms)); +} + +function asNullableString(value: unknown): string | null { + return typeof value === "string" && value.trim().length > 0 ? value : null; +} + +function normalizeRuntimeState(value: unknown): "scheduled" | "paused" | "completed" | "error" | null { + switch (value) { + case "scheduled": + case "paused": + case "completed": + case "error": + return value; + default: + return null; + } +} + +function normalizeRuntimeLastStatus(value: unknown): "ok" | "error" | null { + switch (value) { + case "ok": + case "error": + return value; + default: + return null; + } +} + +function getOfficialRuntimeMcpHint(slug: string): RuntimeMcpHint { + switch (slug) { + case "notion": + return { + server_url: "https://mcp.notion.com/mcp", + transport: "streamable-http", + auth_mode: "oauth", + notes: [ + "Official hosted MCP available via interactive OAuth.", + "Mika runtime currently uses native provider bridge tools for headless execution.", + ], + }; + case "todoist": + return { + server_url: "https://ai.todoist.net/mcp", + transport: "streamable-http", + auth_mode: "oauth", + notes: [ + "Official hosted MCP available via Todoist AI MCP docs.", + "Mika runtime currently uses native provider bridge tools for headless execution.", + ], + }; + case "calcom": + return { + server_url: "https://mcp.cal.com/mcp", + transport: "streamable-http", + auth_mode: "oauth", + notes: [ + "Official hosted MCP available via interactive OAuth.", + "Mika runtime currently uses native provider bridge tools for headless execution.", + ], + }; + default: + return { + server_url: null, + transport: null, + auth_mode: "unknown", + notes: ["No official remote MCP endpoint mapped in Mika runtime yet."], + }; + } +} + +// deno-lint-ignore no-explicit-any +async function loadAgentRecord(supabase: any, agentInstanceId: string): Promise { + const { data: agent, error: agentErr } = await supabase + .from("agent_instances") + .select("id, user_id, railway_service_id, vps_pool_id") + .eq("id", agentInstanceId) + .maybeSingle(); + + if (agentErr || !agent) { + throw new Error(`agent_instance not found: ${agentErr?.message ?? agentInstanceId}`); + } + + return agent as AgentInstanceRecord; +} + +async function loadAgentServiceTarget( + // deno-lint-ignore no-explicit-any + supabase: any, + agentInstanceId: string, + railwayToken: string, +): Promise<{ + agent: AgentInstanceRecord; + serviceId: string; + projectId: string; + environmentId: string; +}> { + const agent = await loadAgentRecord(supabase, agentInstanceId); + + if (!agent.railway_service_id) { + throw new Error("agent has no railway_service_id"); + } + + let projectId: string | null = null; + let environmentId: string | null = null; + + if (agent.vps_pool_id) { + const { data: pool, error: poolErr } = await supabase + .from("vps_pool") + .select("railway_project_id, railway_environment_id") + .eq("id", agent.vps_pool_id) + .maybeSingle(); + if (poolErr) { + throw new Error(`failed to load vps_pool for agent ${agent.id}: ${poolErr.message}`); + } + projectId = pool?.railway_project_id ?? null; + environmentId = pool?.railway_environment_id ?? null; + } + + if (!projectId || !environmentId) { + const ctx = await getServiceContext({ + token: railwayToken, + serviceId: agent.railway_service_id, + }); + projectId = projectId ?? ctx.projectId; + environmentId = environmentId ?? ctx.environmentId; + } + + if (!projectId || !environmentId) { + throw new Error("failed to resolve railway project/environment for agent"); + } + + return { + agent, + serviceId: agent.railway_service_id, + projectId, + environmentId, + }; +} + +async function resolveRuntimeTarget(opts: { + // deno-lint-ignore no-explicit-any + supabase: any; + agentInstanceId: string; + railwayToken: string; +}): Promise { + const target = await loadAgentServiceTarget(opts.supabase, opts.agentInstanceId, opts.railwayToken); + const publicDomain = await ensureRailwayServiceDomain({ + token: opts.railwayToken, + serviceId: target.serviceId, + environmentId: target.environmentId, + projectId: target.projectId, + }); + + return { + serviceId: target.serviceId, + projectId: target.projectId, + environmentId: target.environmentId, + publicDomain: publicDomain.domain, + publicUrl: `https://${publicDomain.domain}`, + }; +} + +// deno-lint-ignore no-explicit-any +async function vaultDecryptSecret(supabase: any, secretId: string | null): Promise { + if (!secretId) return null; + + const { data, error } = await supabase + .rpc("vault_decrypt_secret", { secret_id: secretId }) + .single(); + + if (error || !data) { + return null; + } + + if (typeof data === "string") { + return data; + } + + if (Array.isArray(data) && data.length > 0 && typeof data[0]?.decrypted_secret === "string") { + return data[0].decrypted_secret; + } + + if (typeof (data as { decrypted_secret?: unknown }).decrypted_secret === "string") { + return (data as { decrypted_secret: string }).decrypted_secret; + } + + return null; +} + +async function postPayloadToAgent(opts: { + publicUrl: string; + apiKey: string; + path: string; + payload: unknown; + maxAttempts?: number; +}): Promise { + const maxAttempts = opts.maxAttempts ?? 4; + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const res = await fetch(`${opts.publicUrl.replace(/\/$/, "")}${opts.path}`, { + method: "POST", + headers: { + Authorization: `Bearer ${opts.apiKey}`, + "Content-Type": "application/json", + }, + body: JSON.stringify(opts.payload), + }); + + if (res.ok) { + return await res.json().catch(() => ({})); + } + + const detail = await res.text().catch(() => ""); + const shouldRetry = ![400, 401, 403].includes(res.status); + lastError = new Error(`${opts.path} HTTP ${res.status}: ${detail}`); + if (!shouldRetry || attempt === maxAttempts) { + throw lastError; + } + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)); + if (attempt === maxAttempts) { + throw lastError; + } + } + + await sleep(attempt * 1000); + } + + throw lastError ?? new Error(`${opts.path} sync failed`); +} + +async function getPayloadFromAgent(opts: { + publicUrl: string; + apiKey: string; + path: string; + maxAttempts?: number; +}): Promise { + const maxAttempts = opts.maxAttempts ?? 4; + let lastError: Error | null = null; + + for (let attempt = 1; attempt <= maxAttempts; attempt++) { + try { + const res = await fetch(`${opts.publicUrl.replace(/\/$/, "")}${opts.path}`, { + method: "GET", + headers: { + Authorization: `Bearer ${opts.apiKey}`, + }, + }); + + if (res.ok) { + return await res.json().catch(() => ({})); + } + + const detail = await res.text().catch(() => ""); + const shouldRetry = ![400, 401, 403].includes(res.status); + lastError = new Error(`${opts.path} HTTP ${res.status}: ${detail}`); + if (!shouldRetry || attempt === maxAttempts) { + throw lastError; + } + } catch (err) { + lastError = err instanceof Error ? err : new Error(String(err)); + if (attempt === maxAttempts) { + throw lastError; + } + } + + await sleep(attempt * 1000); + } + + throw lastError ?? new Error(`${opts.path} fetch failed`); +} + +async function pullCronjobsRuntimeStateFromTarget(opts: { + // deno-lint-ignore no-explicit-any + supabase: any; + agentInstanceId: string; + publicUrl: string; + publicDomain: string; + apiKey: string; +}): Promise { + const response = await getPayloadFromAgent({ + publicUrl: opts.publicUrl, + apiKey: opts.apiKey, + path: "/api/cronjobs", + }); + + const runtimeCronjobs = Array.isArray((response as RuntimeCronjobsListResponse)?.cronjobs) + ? ((response as RuntimeCronjobsListResponse).cronjobs ?? []) + : []; + + const syncedAt = new Date().toISOString(); + let updatedCount = 0; + + for (const runtimeJob of runtimeCronjobs) { + const jobId = asNullableString(runtimeJob?.id); + if (!jobId) continue; + + const { error } = await opts.supabase + .from("scheduled_jobs") + .update({ + last_run_at: asNullableString(runtimeJob.last_run_at), + next_run_at: asNullableString(runtimeJob.next_run_at), + runtime_state: normalizeRuntimeState(runtimeJob.state), + runtime_last_status: normalizeRuntimeLastStatus(runtimeJob.last_status), + runtime_last_error: asNullableString(runtimeJob.last_error), + runtime_last_delivery_error: asNullableString(runtimeJob.last_delivery_error), + runtime_synced_at: syncedAt, + }) + .eq("id", jobId) + .eq("agent_instance_id", opts.agentInstanceId); + + if (error) { + throw new Error(`failed to reconcile runtime cronjob ${jobId}: ${error.message}`); + } + + updatedCount += 1; + } + + await opts.supabase + .from("agent_instances") + .update({ last_health_check_at: syncedAt }) + .eq("id", opts.agentInstanceId); + + return { + agent_instance_id: opts.agentInstanceId, + public_url: opts.publicUrl, + public_domain: opts.publicDomain, + synced_count: runtimeCronjobs.length, + updated_count: updatedCount, + response, + }; +} + +export async function buildActiveSkillsPayload( + // deno-lint-ignore no-explicit-any + supabase: any, + agentInstanceId: string, +): Promise { + const { data: skillsData, error: skillsErr } = await supabase + .from("skills") + .select("id, agent_instance_id, name, description, trigger_keywords, current_version_id, updated_at") + .eq("agent_instance_id", agentInstanceId) + .eq("status", "active") + .not("current_version_id", "is", null) + .order("updated_at", { ascending: false }); + + if (skillsErr) { + throw new Error(`failed to load skills: ${skillsErr.message}`); + } + + const skills = (skillsData ?? []) as ActiveSkillRow[]; + const versionIds = Array.from( + new Set(skills.map((skill) => skill.current_version_id).filter(Boolean)), + ); + + let versionsById = new Map(); + if (versionIds.length > 0) { + const { data: versionsData, error: versionsErr } = await supabase + .from("skill_versions") + .select("id, version_number, markdown_content, form_inputs, created_at") + .in("id", versionIds); + + if (versionsErr) { + throw new Error(`failed to load skill_versions: ${versionsErr.message}`); + } + + versionsById = new Map( + ((versionsData ?? []) as SkillVersionRow[]).map((version) => [version.id, version]), + ); + } + + const payloadSkills = skills.flatMap((skill) => { + const version = versionsById.get(skill.current_version_id); + if (!version) return []; + + return [{ + skill_id: skill.id, + version_id: version.id, + version_number: version.version_number, + name: skill.name, + description: skill.description, + trigger_keywords: skill.trigger_keywords, + updated_at: skill.updated_at, + created_at: version.created_at, + form_inputs: version.form_inputs ?? null, + markdown_content: ensureSkillFrontmatter({ + name: skill.name, + description: skill.description, + trigger_keywords: skill.trigger_keywords, + markdown_content: version.markdown_content, + }), + }]; + }); + + return { + agent_instance_id: agentInstanceId, + synced_at: new Date().toISOString(), + skills: payloadSkills, + }; +} + +export async function pushSkillsPayloadToAgent(opts: { + publicUrl: string; + apiKey: string; + payload: AgentSkillsSyncPayload; + maxAttempts?: number; +}): Promise { + return await postPayloadToAgent({ + publicUrl: opts.publicUrl, + apiKey: opts.apiKey, + path: "/api/skills/sync", + payload: opts.payload, + maxAttempts: opts.maxAttempts, + }); +} + +export async function syncAgentSkillsSnapshot(opts: { + // deno-lint-ignore no-explicit-any + supabase: any; + agentInstanceId: string; + railwayToken: string; + apiKey: string; +}): Promise { + if (!opts.railwayToken) { + throw new Error("RAILWAY_API_TOKEN not configured"); + } + if (!opts.apiKey) { + throw new Error("HERMES_API_SERVER_KEY not configured"); + } + + const payload = await buildActiveSkillsPayload(opts.supabase, opts.agentInstanceId); + const target = await resolveRuntimeTarget({ + supabase: opts.supabase, + agentInstanceId: opts.agentInstanceId, + railwayToken: opts.railwayToken, + }); + const response = await pushSkillsPayloadToAgent({ + publicUrl: target.publicUrl, + apiKey: opts.apiKey, + payload, + }); + + return { + agent_instance_id: opts.agentInstanceId, + public_url: target.publicUrl, + public_domain: target.publicDomain, + synced_count: payload.skills.length, + response, + }; +} + +export async function buildManagedCronjobsPayload( + // deno-lint-ignore no-explicit-any + supabase: any, + agentInstanceId: string, +): Promise { + const { data, error } = await supabase + .from("scheduled_jobs") + .select( + "id, agent_instance_id, name, description, natural_language_input, cron_expression, human_readable, action_prompt, required_mcp_slugs, status, auto_paused_reason, last_run_at, next_run_at, timezone, created_at, updated_at", + ) + .eq("agent_instance_id", agentInstanceId) + .neq("status", "archived") + .order("created_at", { ascending: true }); + + if (error) { + throw new Error(`failed to load scheduled_jobs: ${error.message}`); + } + + const rows = (data ?? []) as ScheduledJobRow[]; + + return { + agent_instance_id: agentInstanceId, + synced_at: new Date().toISOString(), + cronjobs: rows.map((job) => ({ + job_id: job.id, + name: job.name, + description: job.description ?? null, + natural_language_input: job.natural_language_input, + cron_expression: job.cron_expression, + human_readable: job.human_readable, + action_prompt: job.action_prompt, + required_mcp_slugs: ensureStringArray(job.required_mcp_slugs), + status: job.status, + auto_paused_reason: job.auto_paused_reason ?? null, + last_run_at: job.last_run_at ?? null, + next_run_at: job.next_run_at ?? null, + timezone: job.timezone, + created_at: job.created_at, + updated_at: job.updated_at, + })), + }; +} + +export async function pushCronjobsPayloadToAgent(opts: { + publicUrl: string; + apiKey: string; + payload: AgentCronjobsSyncPayload; + maxAttempts?: number; +}): Promise { + return await postPayloadToAgent({ + publicUrl: opts.publicUrl, + apiKey: opts.apiKey, + path: "/api/cronjobs/sync", + payload: opts.payload, + maxAttempts: opts.maxAttempts, + }); +} + +export async function syncAgentCronjobsSnapshot(opts: { + // deno-lint-ignore no-explicit-any + supabase: any; + agentInstanceId: string; + railwayToken: string; + apiKey: string; +}): Promise { + if (!opts.railwayToken) { + throw new Error("RAILWAY_API_TOKEN not configured"); + } + if (!opts.apiKey) { + throw new Error("HERMES_API_SERVER_KEY not configured"); + } + + const payload = await buildManagedCronjobsPayload(opts.supabase, opts.agentInstanceId); + const target = await resolveRuntimeTarget({ + supabase: opts.supabase, + agentInstanceId: opts.agentInstanceId, + railwayToken: opts.railwayToken, + }); + const response = await pushCronjobsPayloadToAgent({ + publicUrl: target.publicUrl, + apiKey: opts.apiKey, + payload, + }); + const pullResult = await pullCronjobsRuntimeStateFromTarget({ + supabase: opts.supabase, + agentInstanceId: opts.agentInstanceId, + publicUrl: target.publicUrl, + publicDomain: target.publicDomain, + apiKey: opts.apiKey, + }); + + return { + agent_instance_id: opts.agentInstanceId, + public_url: target.publicUrl, + public_domain: target.publicDomain, + synced_count: payload.cronjobs.length, + response: { + push: response, + pull: pullResult.response, + updated_count: pullResult.updated_count, + }, + }; +} + +export async function pullAgentCronjobsRuntimeState(opts: { + // deno-lint-ignore no-explicit-any + supabase: any; + agentInstanceId: string; + railwayToken: string; + apiKey: string; +}): Promise { + if (!opts.railwayToken) { + throw new Error("RAILWAY_API_TOKEN not configured"); + } + if (!opts.apiKey) { + throw new Error("HERMES_API_SERVER_KEY not configured"); + } + + const target = await resolveRuntimeTarget({ + supabase: opts.supabase, + agentInstanceId: opts.agentInstanceId, + railwayToken: opts.railwayToken, + }); + + return await pullCronjobsRuntimeStateFromTarget({ + supabase: opts.supabase, + agentInstanceId: opts.agentInstanceId, + publicUrl: target.publicUrl, + publicDomain: target.publicDomain, + apiKey: opts.apiKey, + }); +} + +export async function buildManagedIntegrationsPayload( + // deno-lint-ignore no-explicit-any + supabase: any, + agentInstanceId: string, +): Promise { + const agent = await loadAgentRecord(supabase, agentInstanceId); + + const { data, error } = await supabase + .from("user_integrations") + .select( + "id, user_id, mcp_id, status, connected_account_email, connected_account_name, granted_scopes, token_expires_at, last_refreshed_at, error_message, access_token_vault_id, refresh_token_vault_id, created_at, updated_at, mcp:available_mcps(id, slug, name, provider, description, oauth_authorize_url, oauth_token_url, oauth_revoke_url, required_scopes, supports_refresh_token)", + ) + .eq("user_id", agent.user_id) + .order("created_at", { ascending: true }); + + if (error) { + throw new Error(`failed to load user_integrations: ${error.message}`); + } + + const rows = (data ?? []) as UserIntegrationRow[]; + const integrations = []; + + for (const row of rows) { + const mcp = Array.isArray(row.mcp) ? row.mcp[0] : row.mcp; + if (!mcp) continue; + + const accessToken = row.status === "active" + ? await vaultDecryptSecret(supabase, row.access_token_vault_id) + : null; + const refreshToken = row.status === "active" + ? await vaultDecryptSecret(supabase, row.refresh_token_vault_id) + : null; + + integrations.push({ + integration_id: row.id, + mcp_id: row.mcp_id, + slug: mcp.slug, + name: mcp.name, + provider: mcp.provider, + description: mcp.description, + status: row.status, + connected_account_email: row.connected_account_email ?? null, + connected_account_name: row.connected_account_name ?? null, + granted_scopes: ensureStringArray(row.granted_scopes), + required_scopes: ensureStringArray(mcp.required_scopes), + token_expires_at: row.token_expires_at ?? null, + last_refreshed_at: row.last_refreshed_at ?? null, + error_message: row.error_message ?? null, + oauth_authorize_url: mcp.oauth_authorize_url, + oauth_token_url: mcp.oauth_token_url, + oauth_revoke_url: mcp.oauth_revoke_url ?? null, + supports_refresh_token: !!mcp.supports_refresh_token, + access_token: accessToken, + refresh_token: refreshToken, + runtime_mcp: getOfficialRuntimeMcpHint(mcp.slug), + created_at: row.created_at, + updated_at: row.updated_at, + }); + } + + return { + agent_instance_id: agentInstanceId, + user_id: agent.user_id, + synced_at: new Date().toISOString(), + integrations, + }; +} + +export async function pushIntegrationsPayloadToAgent(opts: { + publicUrl: string; + apiKey: string; + payload: AgentIntegrationsSyncPayload; + maxAttempts?: number; +}): Promise { + return await postPayloadToAgent({ + publicUrl: opts.publicUrl, + apiKey: opts.apiKey, + path: "/api/integrations/sync", + payload: opts.payload, + maxAttempts: opts.maxAttempts, + }); +} + +export async function syncAgentIntegrationsSnapshot(opts: { + // deno-lint-ignore no-explicit-any + supabase: any; + agentInstanceId: string; + railwayToken: string; + apiKey: string; +}): Promise { + if (!opts.railwayToken) { + throw new Error("RAILWAY_API_TOKEN not configured"); + } + if (!opts.apiKey) { + throw new Error("HERMES_API_SERVER_KEY not configured"); + } + + const payload = await buildManagedIntegrationsPayload(opts.supabase, opts.agentInstanceId); + const target = await resolveRuntimeTarget({ + supabase: opts.supabase, + agentInstanceId: opts.agentInstanceId, + railwayToken: opts.railwayToken, + }); + const response = await pushIntegrationsPayloadToAgent({ + publicUrl: target.publicUrl, + apiKey: opts.apiKey, + payload, + }); + + return { + agent_instance_id: opts.agentInstanceId, + public_url: target.publicUrl, + public_domain: target.publicDomain, + synced_count: payload.integrations.length, + response, + }; +} + +export async function syncAgentRuntimeSnapshot(opts: { + // deno-lint-ignore no-explicit-any + supabase: any; + agentInstanceId: string; + railwayToken: string; + apiKey: string; + scope?: RuntimeSyncScope; +}): Promise { + if (!opts.railwayToken) { + throw new Error("RAILWAY_API_TOKEN not configured"); + } + if (!opts.apiKey) { + throw new Error("HERMES_API_SERVER_KEY not configured"); + } + + const scope = opts.scope ?? "all"; + const target = await resolveRuntimeTarget({ + supabase: opts.supabase, + agentInstanceId: opts.agentInstanceId, + railwayToken: opts.railwayToken, + }); + + let cronjobsSyncedCount = 0; + let integrationsSyncedCount = 0; + let cronjobsResponse: unknown = { skipped: true }; + let integrationsResponse: unknown = { skipped: true }; + + if (scope === "cronjobs" || scope === "all") { + const cronjobsPayload = await buildManagedCronjobsPayload(opts.supabase, opts.agentInstanceId); + const cronjobsPushResponse = await pushCronjobsPayloadToAgent({ + publicUrl: target.publicUrl, + apiKey: opts.apiKey, + payload: cronjobsPayload, + }); + const cronjobsPullResult = await pullCronjobsRuntimeStateFromTarget({ + supabase: opts.supabase, + agentInstanceId: opts.agentInstanceId, + publicUrl: target.publicUrl, + publicDomain: target.publicDomain, + apiKey: opts.apiKey, + }); + cronjobsResponse = { + push: cronjobsPushResponse, + pull: cronjobsPullResult.response, + updated_count: cronjobsPullResult.updated_count, + }; + cronjobsSyncedCount = cronjobsPayload.cronjobs.length; + } + + if (scope === "integrations" || scope === "all") { + const integrationsPayload = await buildManagedIntegrationsPayload(opts.supabase, opts.agentInstanceId); + integrationsResponse = await pushIntegrationsPayloadToAgent({ + publicUrl: target.publicUrl, + apiKey: opts.apiKey, + payload: integrationsPayload, + }); + integrationsSyncedCount = integrationsPayload.integrations.length; + } + + return { + agent_instance_id: opts.agentInstanceId, + public_url: target.publicUrl, + public_domain: target.publicDomain, + cronjobs_synced_count: cronjobsSyncedCount, + integrations_synced_count: integrationsSyncedCount, + responses: { + cronjobs: cronjobsResponse, + integrations: integrationsResponse, + }, + }; +} diff --git a/supabase/functions/disconnect-integration/index.ts b/supabase/functions/disconnect-integration/index.ts index 4a940c8..7080753 100644 --- a/supabase/functions/disconnect-integration/index.ts +++ b/supabase/functions/disconnect-integration/index.ts @@ -11,6 +11,10 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; import { corsHeaders } from "../_shared/cors.ts"; import { revokeToken, type ProviderSlug } from "../_shared/oauth-providers.ts"; +import { syncAgentRuntimeSnapshot } from "../_shared/runtime-sync.ts"; + +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN") ?? ""; +const HERMES_API_SERVER_KEY = Deno.env.get("HERMES_API_SERVER_KEY") ?? ""; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { @@ -164,8 +168,36 @@ Deno.serve(async (req) => { return jsonResponse({ error: "Falha ao remover integração" }, 500); } - // TODO Fase 5: notify Hermes container that MCP was disconnected - return jsonResponse({ success: true, paused_jobs_count: pausedCount }); + const { data: agent } = await admin + .from("agent_instances") + .select("id") + .eq("user_id", userId) + .maybeSingle(); + + let runtimeSyncError: string | null = null; + if (agent?.id) { + try { + await syncAgentRuntimeSnapshot({ + supabase: admin, + agentInstanceId: agent.id, + railwayToken: RAILWAY_API_TOKEN, + apiKey: HERMES_API_SERVER_KEY, + scope: "all", + }); + } catch (syncErr) { + runtimeSyncError = syncErr instanceof Error ? syncErr.message : "unknown"; + console.error( + "disconnect-integration runtime sync warning", + runtimeSyncError, + ); + } + } + + return jsonResponse({ + success: true, + paused_jobs_count: pausedCount, + runtime_sync_warning: runtimeSyncError, + }); } catch (err) { console.error("disconnect-integration fatal", err instanceof Error ? err.message : "unknown"); return jsonResponse( diff --git a/supabase/functions/keep-alive-agents/index.ts b/supabase/functions/keep-alive-agents/index.ts index f306675..90aa1db 100644 --- a/supabase/functions/keep-alive-agents/index.ts +++ b/supabase/functions/keep-alive-agents/index.ts @@ -1,7 +1,10 @@ // keep-alive-agents -// Mantém todos os containers Railway dos agentes ativos "acordados" fazendo -// uma request GET /getMe ao Telegram para cada agente. Isso força tráfego de -// saída no container, evitando que o Railway hiberne instâncias ociosas. +// Mantém todos os containers Railway dos agentes ativos "acordados" e aproveita +// o ciclo para reconciliar o estado operacional dos cronjobs de volta no banco. +// +// Estratégia: +// 1. Faz GET /getMe no Telegram quando o agente já tem bot configurado +// 2. Puxa /api/cronjobs do runtime Hermes e atualiza scheduled_jobs // // Substitui completamente o UptimeRobot — não precisa de configuração externa // por agente. Roda via pg_cron a cada 4 minutos. @@ -10,14 +13,18 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { corsHeaders } from "../_shared/cors.ts"; +import { pullAgentCronjobsRuntimeState } from "../_shared/runtime-sync.ts"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN") ?? ""; +const HERMES_API_SERVER_KEY = Deno.env.get("HERMES_API_SERVER_KEY") ?? ""; interface AgentRow { id: string; user_id: string; - telegram_bot_token_vault_id: string; + railway_service_id: string | null; + telegram_bot_token_vault_id: string | null; telegram_bot_username: string | null; } @@ -28,12 +35,11 @@ Deno.serve(async (req) => { auth: { persistSession: false, autoRefreshToken: false }, }); - // 1) Buscar agentes ativos com token configurado + // 1) Buscar agentes ativos; Telegram e runtime são tratados separadamente const { data: agents, error: agentsErr } = await supabase .from("agent_instances") - .select("id, user_id, telegram_bot_token_vault_id, telegram_bot_username") - .eq("status", "active") - .not("telegram_bot_token_vault_id", "is", null); + .select("id, user_id, railway_service_id, telegram_bot_token_vault_id, telegram_bot_username") + .eq("status", "active"); if (agentsErr) { console.error("keep-alive: failed to load agents:", agentsErr.message); @@ -41,43 +47,78 @@ Deno.serve(async (req) => { } const list = (agents ?? []) as AgentRow[]; - let success = 0; - let failed = 0; + let telegramSuccess = 0; + let telegramFailed = 0; + let telegramSkipped = 0; + let runtimeSyncSuccess = 0; + let runtimeSyncFailed = 0; + let runtimeSyncSkipped = 0; + const runtimeSyncEnabled = Boolean(RAILWAY_API_TOKEN && HERMES_API_SERVER_KEY); - // 2) Para cada agente, decrypt token + GET /getMe (em paralelo, mas sem quebrar o loop) + // 2) Para cada agente, ping no Telegram + reconciliação de runtime await Promise.all( list.map(async (agent) => { + if (agent.telegram_bot_token_vault_id) { + try { + const { data: secret, error: secretErr } = await supabase.rpc("vault_decrypt_secret", { + secret_id: agent.telegram_bot_token_vault_id, + }); + + if (secretErr || !secret?.[0]?.decrypted_secret) { + console.warn(`keep-alive: missing token for agent ${agent.id} (${agent.telegram_bot_username ?? "?"})`); + telegramFailed++; + } else { + const token = secret[0].decrypted_secret as string; + const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, { method: "GET" }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + console.warn(`keep-alive: getMe failed for agent ${agent.id} (${agent.telegram_bot_username ?? "?"}): ${res.status} ${text.slice(0, 200)}`); + telegramFailed++; + } else { + telegramSuccess++; + } + } + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.warn(`keep-alive: telegram exception for agent ${agent.id}: ${msg}`); + telegramFailed++; + } + } else { + telegramSkipped++; + } + + if (!runtimeSyncEnabled || !agent.railway_service_id) { + runtimeSyncSkipped++; + return; + } + try { - const { data: secret, error: secretErr } = await supabase.rpc("vault_decrypt_secret", { - secret_id: agent.telegram_bot_token_vault_id, + await pullAgentCronjobsRuntimeState({ + supabase, + agentInstanceId: agent.id, + railwayToken: RAILWAY_API_TOKEN, + apiKey: HERMES_API_SERVER_KEY, }); - - if (secretErr || !secret?.[0]?.decrypted_secret) { - console.warn(`keep-alive: missing token for agent ${agent.id} (${agent.telegram_bot_username ?? "?"})`); - failed++; - return; - } - - const token = secret[0].decrypted_secret as string; - const res = await fetch(`https://api.telegram.org/bot${token}/getMe`, { method: "GET" }); - - if (!res.ok) { - const text = await res.text().catch(() => ""); - console.warn(`keep-alive: getMe failed for agent ${agent.id} (${agent.telegram_bot_username ?? "?"}): ${res.status} ${text.slice(0, 200)}`); - failed++; - return; - } - - success++; + runtimeSyncSuccess++; } catch (e) { const msg = e instanceof Error ? e.message : String(e); - console.warn(`keep-alive: exception for agent ${agent.id}: ${msg}`); - failed++; + console.warn(`keep-alive: runtime sync exception for agent ${agent.id}: ${msg}`); + runtimeSyncFailed++; } }), ); - const summary = { total: list.length, success, failed }; + const summary = { + total: list.length, + telegram_success: telegramSuccess, + telegram_failed: telegramFailed, + telegram_skipped: telegramSkipped, + runtime_sync_enabled: runtimeSyncEnabled, + runtime_sync_success: runtimeSyncSuccess, + runtime_sync_failed: runtimeSyncFailed, + runtime_sync_skipped: runtimeSyncSkipped, + }; console.log("keep-alive summary:", JSON.stringify(summary)); return jsonResponse(200, summary); }); diff --git a/supabase/functions/list-ollama-models/index.ts b/supabase/functions/list-ollama-models/index.ts new file mode 100644 index 0000000..486d840 --- /dev/null +++ b/supabase/functions/list-ollama-models/index.ts @@ -0,0 +1,110 @@ +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { + DEFAULT_OLLAMA_MODEL, + normalizeOllamaModelSelection, +} from "../_shared/hermes-config.ts"; + +const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; +const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; +const OLLAMA_API_KEY = Deno.env.get("OLLAMA_API_KEY"); + +function jsonResponse(body: unknown, status = 200): Response { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} + +interface OllamaTagModel { + name?: string; + model?: string; + modified_at?: string; + size?: number; + digest?: string; + details?: Record; +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + try { + const authHeader = req.headers.get("Authorization") ?? ""; + const jwt = authHeader.replace(/^Bearer\s+/i, ""); + if (!jwt) return jsonResponse({ error: "missing authorization" }, 401); + + const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { persistSession: false, autoRefreshToken: false }, + }); + + const { data: userData, error: userErr } = await admin.auth.getUser(jwt); + if (userErr || !userData?.user) { + return jsonResponse({ error: "invalid token" }, 401); + } + + const { data: isAdmin, error: roleErr } = await admin.rpc("has_role", { + _user_id: userData.user.id, + _role: "admin", + }); + if (roleErr || !isAdmin) { + return jsonResponse({ error: "admin role required" }, 403); + } + + if (!OLLAMA_API_KEY) { + return jsonResponse({ error: "OLLAMA_API_KEY not configured" }, 500); + } + + const res = await fetch("https://ollama.com/api/tags", { + headers: { + Authorization: `Bearer ${OLLAMA_API_KEY}`, + }, + }); + + if (!res.ok) { + const text = await res.text().catch(() => ""); + return jsonResponse( + { + error: `ollama tags request failed: ${res.status}`, + detail: text, + }, + 502, + ); + } + + const payload = await res.json().catch(() => ({})); + const models = Array.isArray(payload?.models) ? (payload.models as OllamaTagModel[]) : []; + + const normalized = models + .map((item) => { + const raw = item.name || item.model || ""; + const name = normalizeOllamaModelSelection(raw); + return { + name, + raw_name: raw, + modified_at: item.modified_at ?? null, + size: item.size ?? null, + digest: item.digest ?? null, + details: item.details ?? {}, + }; + }) + .filter((item) => /(?:-cloud|:cloud)$/.test(item.name)) + .sort((a, b) => { + if (a.name === DEFAULT_OLLAMA_MODEL) return -1; + if (b.name === DEFAULT_OLLAMA_MODEL) return 1; + return a.name.localeCompare(b.name); + }); + + return jsonResponse({ + models: normalized, + default_model: DEFAULT_OLLAMA_MODEL, + source_endpoint: "https://ollama.com/api/tags", + }); + } catch (err) { + return jsonResponse( + { error: err instanceof Error ? err.message : "unexpected error" }, + 500, + ); + } +}); diff --git a/supabase/functions/oauth-callback/index.ts b/supabase/functions/oauth-callback/index.ts index 17d0d2d..352150c 100644 --- a/supabase/functions/oauth-callback/index.ts +++ b/supabase/functions/oauth-callback/index.ts @@ -9,6 +9,10 @@ import { getProviderEnv, type ProviderSlug, } from "../_shared/oauth-providers.ts"; +import { syncAgentRuntimeSnapshot } from "../_shared/runtime-sync.ts"; + +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN") ?? ""; +const HERMES_API_SERVER_KEY = Deno.env.get("HERMES_API_SERVER_KEY") ?? ""; function siteUrl(): string { return Deno.env.get("SITE_URL") ?? "https://798b89e5-0dc6-412a-81be-a4b6dfea7b6c.lovable.app"; @@ -175,7 +179,29 @@ Deno.serve(async (req) => { return redirect("/painel/integracoes?error=db_error"); } - // TODO Fase 5: notify Hermes container that MCP is now available + const { data: agent } = await admin + .from("agent_instances") + .select("id") + .eq("user_id", stateRow.user_id) + .maybeSingle(); + + if (agent?.id) { + try { + await syncAgentRuntimeSnapshot({ + supabase: admin, + agentInstanceId: agent.id, + railwayToken: RAILWAY_API_TOKEN, + apiKey: HERMES_API_SERVER_KEY, + scope: "all", + }); + } catch (syncErr) { + console.error( + "oauth-callback runtime sync warning", + syncErr instanceof Error ? syncErr.message : String(syncErr), + ); + } + } + return redirect(`/painel/integracoes?status=success&mcp=${encodeURIComponent(slug)}`); } catch (err) { console.error("oauth-callback fatal", err instanceof Error ? err.message : "unknown"); diff --git a/supabase/functions/provision-agent/index.ts b/supabase/functions/provision-agent/index.ts index 6fb762c..1ac44c3 100644 --- a/supabase/functions/provision-agent/index.ts +++ b/supabase/functions/provision-agent/index.ts @@ -14,6 +14,11 @@ import { findRailwayServiceByName, upsertRailwayVariableCollection, } from "../_shared/railway.ts"; +import { + DEFAULT_OLLAMA_MODEL, + DEFAULT_OLLAMA_PROVIDER, + normalizeOllamaModelSelection, +} from "../_shared/hermes-config.ts"; interface RequestBody { agent_instance_id: string; @@ -128,8 +133,7 @@ Deno.serve(async (req) => { // deno-lint-ignore no-explicit-any const planSlug = ((subscription as any)?.plans?.slug as string | undefined) ?? "basic"; - const isPro = ["professional", "enterprise"].includes(planSlug); - console.log(`[provision-agent] plano=${planSlug} isPro=${isPro}`); + console.log(`[provision-agent] plano=${planSlug}`); // 2) Buscar pool disponível (com IDs Railway preenchidos e capacidade) const { data: pool, error: poolErr } = await supabase @@ -215,11 +219,15 @@ Deno.serve(async (req) => { const defaultSoul = `Você se chama ${agentName}. Você é um assistente pessoal de IA criado pela DomCo. exclusivamente para ${fullName}. Seu estilo: Direto e objetivo, sempre em português brasileiro, respostas curtas no Telegram, use emojis com moderação, trate ${firstName} pelo primeiro nome. Suas prioridades: produtividade, automação proativa. Identidade: você é ${agentName} da DomCo., nunca se identifique como Hermes ou qualquer outro modelo.`; const soulContent = body.soul_content?.trim() || defaultSoul; + const modelFinal = normalizeOllamaModelSelection(body.model || DEFAULT_OLLAMA_MODEL); + const envVars: Record = { HERMES_HOME: "/opt/data/.hermes", API_SERVER_ENABLED: "true", API_SERVER_KEY: Deno.env.get("HERMES_API_SERVER_KEY") ?? "", GATEWAY_ALLOW_ALL_USERS: "false", + HERMES_MODEL_DEFAULT: modelFinal, + HERMES_MODEL_PROVIDER: DEFAULT_OLLAMA_PROVIDER, HERMES_SOUL_OVERRIDE: soulContent, HERMES_STT_PROVIDER: sttProvider, HERMES_TTS_PROVIDER: ttsProvider, @@ -230,10 +238,7 @@ Deno.serve(async (req) => { TELEGRAM_HOME_CHANNEL: chatIdStr, }; - // Modelo é definido pelo config.yaml embutido na imagem custom (ollama-cloud + gemma4:31b-cloud). - // NÃO injetar HERMES_MODEL como env var — sobrescreve o config.yaml e quebra o bot. const agentNameFinal = agentName; - const modelFinal = isPro ? "ollama-cloud/gemma4:31b-cloud" : "ollama-cloud/gemma4:31b-cloud"; // 7) Criar serviço no Railway const serviceName = `mika-${agent.uuid_tenant.replace(/-/g, "").slice(0, 8)}`; @@ -307,7 +312,8 @@ Deno.serve(async (req) => { vps_pool_id: pool.id, agent_name: agentNameFinal, model_config: { - provider: modelFinal, + provider: DEFAULT_OLLAMA_PROVIDER, + model: modelFinal, stt: sttProvider, tts: ttsProvider, agent_name: agentNameFinal, @@ -431,15 +437,10 @@ async function handleUpdateExistingService( // deno-lint-ignore no-explicit-any const planSlug = ((subscription as any)?.plans?.slug as string | undefined) ?? "basic"; - const isPro = ["professional", "enterprise"].includes(planSlug); - const defaultSoul = `Você se chama ${agentName}. Você é um assistente pessoal de IA criado pela DOMCO para ${fullName}. Você é proativo, direto e fala sempre em português brasileiro. Você ajuda ${firstName} a ser mais produtivo — gerenciando emails, agenda, tarefas e automatizando o que puder. Seja conciso nas respostas via Telegram. Nunca se identifique como Hermes ou como produto da Nous Research — você é Mika.`; const soulContent = body.soul_content?.trim() || defaultSoul; - const defaultModel = isPro - ? "openrouter/google/gemma-4-31b-it" - : "openrouter/google/gemma-4-27b-a4b-it"; - const model = body.model || defaultModel; + const model = normalizeOllamaModelSelection(body.model || DEFAULT_OLLAMA_MODEL); const sttProvider = body.stt_provider || "local"; const ttsProvider = body.tts_provider || "disabled"; @@ -470,10 +471,9 @@ async function handleUpdateExistingService( return jsonResponse(500, { error: "failed to resolve railway project/environment" }); } - // Upsert das vars principais (não mexemos em token Telegram aqui — preservado) - // NÃO injetar HERMES_MODEL: a imagem custom já tem config.yaml com ollama-cloud/gemma4:31b-cloud. - // Sobrescrever via env var quebra o bot (model: "" / 404 not found). const variables: Record = { + HERMES_MODEL_DEFAULT: model, + HERMES_MODEL_PROVIDER: DEFAULT_OLLAMA_PROVIDER, HERMES_SOUL_OVERRIDE: soulContent, HERMES_STT_PROVIDER: sttProvider, HERMES_TTS_PROVIDER: ttsProvider, @@ -515,7 +515,8 @@ async function handleUpdateExistingService( .from("agent_instances") .update({ model_config: { - provider: model, + provider: DEFAULT_OLLAMA_PROVIDER, + model, stt: sttProvider, tts: ttsProvider, agent_name: agentName, diff --git a/supabase/functions/publish-skill-version/index.ts b/supabase/functions/publish-skill-version/index.ts index b7064a9..4edbb4c 100644 --- a/supabase/functions/publish-skill-version/index.ts +++ b/supabase/functions/publish-skill-version/index.ts @@ -1,11 +1,13 @@ // Promove uma skill_version a "live" de forma atômica e idempotente. // Garantia adicional: unique index parcial skill_versions_one_live_per_skill no banco. -// TODO Fase 5: dispatch SSH deploy to container after publish import { createClient } from "npm:@supabase/supabase-js@2"; import { corsHeaders } from "../_shared/cors.ts"; +import { syncAgentSkillsSnapshot } from "../_shared/runtime-sync.ts"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN") ?? ""; +const HERMES_API_SERVER_KEY = Deno.env.get("HERMES_API_SERVER_KEY") ?? ""; const admin = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY); @@ -53,7 +55,7 @@ Deno.serve(async (req) => { // Carrega versão + skill (verifica ownership e estado atual) const { data: versionRow, error: vErr } = await admin .from("skill_versions") - .select("id, skill_id, version_number, is_live, skills!inner(id, user_id)") + .select("id, skill_id, version_number, is_live, skills!inner(id, user_id, agent_instance_id)") .eq("id", skill_version_id) .maybeSingle(); @@ -81,6 +83,8 @@ Deno.serve(async (req) => { } const skillId: string = versionRow.skill_id; + // @ts-expect-error nested + const agentInstanceId: string = versionRow.skills.agent_instance_id; // Postgres não permite transação multi-statement via supabase-js. // Estratégia: 1) zera todos is_live da skill, 2) marca a alvo como live, 3) atualiza skills. @@ -141,8 +145,35 @@ Deno.serve(async (req) => { }); } + let syncResult: + | { synced: true; public_url: string; public_domain: string; synced_count: number } + | { synced: false; sync_error: string } = { synced: true, public_url: "", public_domain: "", synced_count: 0 }; + + try { + const result = await syncAgentSkillsSnapshot({ + supabase: admin, + agentInstanceId, + railwayToken: RAILWAY_API_TOKEN, + apiKey: HERMES_API_SERVER_KEY, + }); + syncResult = { + synced: true, + public_url: result.public_url, + public_domain: result.public_domain, + synced_count: result.synced_count, + }; + } catch (e) { + const msg = e instanceof Error ? e.message : String(e); + console.error("publish skill sync failed:", msg); + syncResult = { synced: false, sync_error: msg }; + } + return new Response( - JSON.stringify({ success: true, version_number: versionRow.version_number }), + JSON.stringify({ + success: true, + version_number: versionRow.version_number, + ...syncResult, + }), { status: 200, headers: { ...corsHeaders, "Content-Type": "application/json" } }, ); }); diff --git a/supabase/functions/railway-webhook/index.ts b/supabase/functions/railway-webhook/index.ts index 416a91e..4bd1af2 100644 --- a/supabase/functions/railway-webhook/index.ts +++ b/supabase/functions/railway-webhook/index.ts @@ -10,11 +10,17 @@ import { createClient } from "https://esm.sh/@supabase/supabase-js@2.45.4"; import { corsHeaders } from "../_shared/cors.ts"; +import { + syncAgentRuntimeSnapshot, + syncAgentSkillsSnapshot, +} from "../_shared/runtime-sync.ts"; const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; const ADMIN_TELEGRAM_BOT_TOKEN = Deno.env.get("ADMIN_TELEGRAM_BOT_TOKEN"); const ADMIN_TELEGRAM_CHAT_ID = Deno.env.get("ADMIN_TELEGRAM_CHAT_ID"); +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN") ?? ""; +const HERMES_API_SERVER_KEY = Deno.env.get("HERMES_API_SERVER_KEY") ?? ""; async function notifyAdmin(message: string): Promise { if (!ADMIN_TELEGRAM_BOT_TOKEN || !ADMIN_TELEGRAM_CHAT_ID) return; @@ -138,8 +144,61 @@ Deno.serve(async (req) => { } } + let runtimeSyncError: string | null = null; + try { + const runtimeResult = await syncAgentRuntimeSnapshot({ + supabase, + agentInstanceId: agent.id, + railwayToken: RAILWAY_API_TOKEN, + apiKey: HERMES_API_SERVER_KEY, + scope: "all", + }); + console.log( + `railway-webhook: runtime sincronizado para agent ${agent.id} (${runtimeResult.cronjobs_synced_count} cronjobs, ${runtimeResult.integrations_synced_count} integrations)`, + ); + } catch (e) { + runtimeSyncError = e instanceof Error ? e.message : String(e); + console.error(`railway-webhook: falha ao sincronizar runtime do agent ${agent.id}:`, runtimeSyncError); + if (wasProvisioning) { + const fullName = await loadFullName(); + await notifyAdmin( + `⚠️ Agente subiu, mas o sync operacional falhou\n\n` + + `👤 Cliente: ${fullName}\n` + + `🚀 Railway: ${agent.railway_service_id}\n` + + `❗ Erro: ${runtimeSyncError}\n\n` + + `➡️ Revisar no admin`, + ); + } + } + + let skillsSyncError: string | null = null; + try { + const syncResult = await syncAgentSkillsSnapshot({ + supabase, + agentInstanceId: agent.id, + railwayToken: RAILWAY_API_TOKEN, + apiKey: HERMES_API_SERVER_KEY, + }); + console.log( + `railway-webhook: skills sincronizadas para agent ${agent.id} (${syncResult.synced_count} skills)`, + ); + } catch (e) { + skillsSyncError = e instanceof Error ? e.message : String(e); + console.error(`railway-webhook: falha ao sincronizar skills do agent ${agent.id}:`, skillsSyncError); + if (wasProvisioning) { + const fullName = await loadFullName(); + await notifyAdmin( + `⚠️ Agente subiu, mas o sync de skills falhou\n\n` + + `👤 Cliente: ${fullName}\n` + + `🚀 Railway: ${agent.railway_service_id}\n` + + `❗ Erro: ${skillsSyncError}\n\n` + + `➡️ Revisar no admin`, + ); + } + } + // Notifica admin somente se era um auto-provisionamento (status anterior=provisioning) - if (wasProvisioning) { + if (wasProvisioning && !skillsSyncError && !runtimeSyncError) { const fullName = await loadFullName(); await notifyAdmin( `✅ Agente provisionado automaticamente!\n\n` + diff --git a/supabase/functions/refresh-integration-token/index.ts b/supabase/functions/refresh-integration-token/index.ts index 1de3936..8f19fb8 100644 --- a/supabase/functions/refresh-integration-token/index.ts +++ b/supabase/functions/refresh-integration-token/index.ts @@ -8,6 +8,10 @@ import { type ProviderSlug, refreshAccessToken, } from "../_shared/oauth-providers.ts"; +import { syncAgentRuntimeSnapshot } from "../_shared/runtime-sync.ts"; + +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN") ?? ""; +const HERMES_API_SERVER_KEY = Deno.env.get("HERMES_API_SERVER_KEY") ?? ""; function jsonResponse(body: unknown, status = 200): Response { return new Response(JSON.stringify(body), { @@ -174,7 +178,33 @@ Deno.serve(async (req) => { }) .eq("id", integration_id); - return jsonResponse({ success: true, expires_at: expiresAt }); + let runtimeSyncError: string | null = null; + const { data: agent } = await admin + .from("agent_instances") + .select("id") + .eq("user_id", userId) + .maybeSingle(); + + if (agent?.id) { + try { + await syncAgentRuntimeSnapshot({ + supabase: admin, + agentInstanceId: agent.id, + railwayToken: RAILWAY_API_TOKEN, + apiKey: HERMES_API_SERVER_KEY, + scope: "integrations", + }); + } catch (syncErr) { + runtimeSyncError = syncErr instanceof Error ? syncErr.message : String(syncErr); + console.error("refresh-integration-token runtime sync warning", runtimeSyncError); + } + } + + return jsonResponse({ + success: true, + expires_at: expiresAt, + runtime_sync_warning: runtimeSyncError, + }); } catch (err) { console.error("refresh-integration-token fatal", err instanceof Error ? err.message : "unknown"); return jsonResponse( diff --git a/supabase/functions/sync-agent-runtime/index.ts b/supabase/functions/sync-agent-runtime/index.ts new file mode 100644 index 0000000..524d490 --- /dev/null +++ b/supabase/functions/sync-agent-runtime/index.ts @@ -0,0 +1,109 @@ +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { syncAgentRuntimeSnapshot } from "../_shared/runtime-sync.ts"; + +const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; +const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; +const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY")!; +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN") ?? ""; +const HERMES_API_SERVER_KEY = Deno.env.get("HERMES_API_SERVER_KEY") ?? ""; + +type RuntimeSyncScope = "cronjobs" | "integrations" | "all"; + +function jsonResponse(status: number, body: unknown) { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} + +function isValidScope(scope: unknown): scope is RuntimeSyncScope { + return scope === "cronjobs" || scope === "integrations" || scope === "all"; +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + const authHeader = req.headers.get("Authorization") ?? ""; + const jwt = authHeader.replace(/^Bearer\s+/i, ""); + if (!jwt) { + return jsonResponse(401, { error: "missing authorization" }); + } + + const userClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { + global: { headers: { Authorization: `Bearer ${jwt}` } }, + auth: { persistSession: false, autoRefreshToken: false }, + }); + + const { data: userData, error: userErr } = await userClient.auth.getUser(); + if (userErr || !userData?.user) { + return jsonResponse(401, { error: "invalid token" }); + } + + const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { persistSession: false, autoRefreshToken: false }, + }); + + let body: { agent_instance_id?: string; scope?: RuntimeSyncScope }; + try { + body = await req.json(); + } catch { + return jsonResponse(400, { error: "invalid json body" }); + } + + if (!body.agent_instance_id) { + return jsonResponse(400, { error: "agent_instance_id required" }); + } + + if (body.scope && !isValidScope(body.scope)) { + return jsonResponse(400, { error: "invalid scope" }); + } + + const { data: agent, error: agentErr } = await supabase + .from("agent_instances") + .select("id, user_id") + .eq("id", body.agent_instance_id) + .maybeSingle(); + + if (agentErr || !agent) { + return jsonResponse(404, { error: "agent_instance not found" }); + } + + const { data: isAdmin, error: roleErr } = await supabase.rpc("has_role", { + _user_id: userData.user.id, + _role: "admin", + }); + if (roleErr) { + return jsonResponse(500, { error: "failed to resolve role" }); + } + + if (agent.user_id !== userData.user.id && !isAdmin) { + return jsonResponse(403, { error: "forbidden" }); + } + + try { + const result = await syncAgentRuntimeSnapshot({ + supabase, + agentInstanceId: agent.id, + railwayToken: RAILWAY_API_TOKEN, + apiKey: HERMES_API_SERVER_KEY, + scope: body.scope ?? "all", + }); + + return jsonResponse(200, { + success: true, + agent_instance_id: result.agent_instance_id, + public_url: result.public_url, + public_domain: result.public_domain, + cronjobs_synced_count: result.cronjobs_synced_count, + integrations_synced_count: result.integrations_synced_count, + runtime_responses: result.responses, + }); + } catch (e) { + const detail = e instanceof Error ? e.message : String(e); + console.error("sync-agent-runtime failed:", detail); + return jsonResponse(500, { error: "runtime sync failed", detail }); + } +}); diff --git a/supabase/functions/sync-agent-skills/index.ts b/supabase/functions/sync-agent-skills/index.ts new file mode 100644 index 0000000..4bdd14e --- /dev/null +++ b/supabase/functions/sync-agent-skills/index.ts @@ -0,0 +1,97 @@ +import { createClient } from "https://esm.sh/@supabase/supabase-js@2.57.4"; +import { corsHeaders } from "../_shared/cors.ts"; +import { syncAgentSkillsSnapshot } from "../_shared/runtime-sync.ts"; + +const SUPABASE_URL = Deno.env.get("SUPABASE_URL")!; +const SUPABASE_SERVICE_ROLE_KEY = Deno.env.get("SUPABASE_SERVICE_ROLE_KEY")!; +const SUPABASE_ANON_KEY = Deno.env.get("SUPABASE_ANON_KEY")!; +const RAILWAY_API_TOKEN = Deno.env.get("RAILWAY_API_TOKEN") ?? ""; +const HERMES_API_SERVER_KEY = Deno.env.get("HERMES_API_SERVER_KEY") ?? ""; + +function jsonResponse(status: number, body: unknown) { + return new Response(JSON.stringify(body), { + status, + headers: { ...corsHeaders, "Content-Type": "application/json" }, + }); +} + +Deno.serve(async (req) => { + if (req.method === "OPTIONS") { + return new Response(null, { headers: corsHeaders }); + } + + const authHeader = req.headers.get("Authorization") ?? ""; + const jwt = authHeader.replace(/^Bearer\s+/i, ""); + if (!jwt) { + return jsonResponse(401, { error: "missing authorization" }); + } + + const userClient = createClient(SUPABASE_URL, SUPABASE_ANON_KEY, { + global: { headers: { Authorization: `Bearer ${jwt}` } }, + auth: { persistSession: false, autoRefreshToken: false }, + }); + + const { data: userData, error: userErr } = await userClient.auth.getUser(); + if (userErr || !userData?.user) { + return jsonResponse(401, { error: "invalid token" }); + } + + const supabase = createClient(SUPABASE_URL, SUPABASE_SERVICE_ROLE_KEY, { + auth: { persistSession: false, autoRefreshToken: false }, + }); + + let body: { agent_instance_id?: string }; + try { + body = await req.json(); + } catch { + return jsonResponse(400, { error: "invalid json body" }); + } + + if (!body.agent_instance_id) { + return jsonResponse(400, { error: "agent_instance_id required" }); + } + + const { data: agent, error: agentErr } = await supabase + .from("agent_instances") + .select("id, user_id") + .eq("id", body.agent_instance_id) + .maybeSingle(); + + if (agentErr || !agent) { + return jsonResponse(404, { error: "agent_instance not found" }); + } + + const { data: isAdmin, error: roleErr } = await supabase.rpc("has_role", { + _user_id: userData.user.id, + _role: "admin", + }); + if (roleErr) { + return jsonResponse(500, { error: "failed to resolve role" }); + } + + if (agent.user_id !== userData.user.id && !isAdmin) { + return jsonResponse(403, { error: "forbidden" }); + } + + try { + const result = await syncAgentSkillsSnapshot({ + supabase, + agentInstanceId: agent.id, + railwayToken: RAILWAY_API_TOKEN, + apiKey: HERMES_API_SERVER_KEY, + }); + + return jsonResponse(200, { + success: true, + agent_instance_id: result.agent_instance_id, + public_url: result.public_url, + public_domain: result.public_domain, + synced_count: result.synced_count, + runtime_response: result.response, + }); + } catch (e) { + const detail = e instanceof Error ? e.message : String(e); + console.error("sync-agent-skills failed:", detail); + return jsonResponse(500, { error: "skills sync failed", detail }); + } +}); diff --git a/supabase/functions/update-agent-config/index.ts b/supabase/functions/update-agent-config/index.ts index 2491410..fdd82a1 100644 --- a/supabase/functions/update-agent-config/index.ts +++ b/supabase/functions/update-agent-config/index.ts @@ -10,6 +10,11 @@ import { getServiceContext, upsertRailwayVariableCollection, } from "../_shared/railway.ts"; +import { + DEFAULT_OLLAMA_MODEL, + DEFAULT_OLLAMA_PROVIDER, + normalizeOllamaModelSelection, +} from "../_shared/hermes-config.ts"; interface RequestBody { agent_instance_id: string; @@ -102,7 +107,11 @@ Deno.serve(async (req) => { } // 4) Upsert variáveis (incluindo HERMES_SOUL_OVERRIDE editado pelo admin) + const model = normalizeOllamaModelSelection(body.model || DEFAULT_OLLAMA_MODEL); + const variables: Record = { + HERMES_MODEL_DEFAULT: model, + HERMES_MODEL_PROVIDER: DEFAULT_OLLAMA_PROVIDER, HERMES_SOUL_OVERRIDE: body.soul_content, HERMES_STT_PROVIDER: body.stt_provider || "local", HERMES_TTS_PROVIDER: body.tts_provider || "disabled", @@ -134,7 +143,8 @@ Deno.serve(async (req) => { .from("agent_instances") .update({ model_config: { - provider: body.model, + provider: DEFAULT_OLLAMA_PROVIDER, + model, stt: body.stt_provider || "local", tts: body.tts_provider || "disabled", agent_name: body.agent_name ?? null, diff --git a/supabase/migrations/20260430121500_runtime_cronjob_observability.sql b/supabase/migrations/20260430121500_runtime_cronjob_observability.sql new file mode 100644 index 0000000..bbfa0c6 --- /dev/null +++ b/supabase/migrations/20260430121500_runtime_cronjob_observability.sql @@ -0,0 +1,25 @@ +-- ============================================================================ +-- FASE 5 — Observabilidade do runtime de cronjobs +-- ============================================================================ + +-- A UI e as Edge Functions já usam o estado "auto_paused", mas a migration +-- original ainda não o aceitava no CHECK da coluna status. +ALTER TABLE public.scheduled_jobs + DROP CONSTRAINT IF EXISTS scheduled_jobs_status_check; + +ALTER TABLE public.scheduled_jobs + ADD CONSTRAINT scheduled_jobs_status_check + CHECK (status IN ('active', 'paused', 'auto_paused', 'error', 'archived')); + +-- Espelha no Supabase o estado operacional retornado pelo runtime do Hermes. +ALTER TABLE public.scheduled_jobs + ADD COLUMN IF NOT EXISTS runtime_state text + CHECK (runtime_state IS NULL OR runtime_state IN ('scheduled', 'paused', 'completed', 'error')), + ADD COLUMN IF NOT EXISTS runtime_last_status text + CHECK (runtime_last_status IS NULL OR runtime_last_status IN ('ok', 'error')), + ADD COLUMN IF NOT EXISTS runtime_last_error text, + ADD COLUMN IF NOT EXISTS runtime_last_delivery_error text, + ADD COLUMN IF NOT EXISTS runtime_synced_at timestamptz; + +CREATE INDEX IF NOT EXISTS idx_scheduled_jobs_agent_runtime_synced_at + ON public.scheduled_jobs (agent_instance_id, runtime_synced_at DESC NULLS LAST);