create-nextblock 0.12.9 → 0.12.12

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (293) hide show
  1. package/docker-template/.dockerignore +23 -23
  2. package/docker-template/.env.docker.example +69 -69
  3. package/docker-template/docker/db/init/99-jwt.sql +6 -6
  4. package/docker-template/docker/db/init/99-roles.sql +25 -25
  5. package/docker-template/docker/kong/kong.yml +112 -112
  6. package/docker-template/scripts/docker-setup.mjs +310 -310
  7. package/libs/db/tsconfig.lib.json +3 -3
  8. package/libs/editor/tsconfig.lib.json +3 -3
  9. package/libs/ui/tsconfig.lib.json +3 -3
  10. package/libs/utils/tsconfig.json +3 -3
  11. package/package.json +1 -1
  12. package/project.json +19 -19
  13. package/templates/nextblock-template/.browserslistrc +11 -11
  14. package/templates/nextblock-template/.dockerignore +23 -23
  15. package/templates/nextblock-template/README.md +34 -34
  16. package/templates/nextblock-template/app/(auth-pages)/layout.tsx +9 -9
  17. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -27
  18. package/templates/nextblock-template/app/(auth-pages)/sign-up/SignUpForm.tsx +141 -141
  19. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +46 -46
  20. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -91
  21. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +123 -123
  22. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -51
  23. package/templates/nextblock-template/app/ToasterProvider.tsx +17 -17
  24. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -7
  25. package/templates/nextblock-template/app/actions/consent.ts +57 -57
  26. package/templates/nextblock-template/app/actions/interactions.test.ts +301 -301
  27. package/templates/nextblock-template/app/actions/interactions.ts +373 -373
  28. package/templates/nextblock-template/app/actions/postActions.ts +146 -146
  29. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +22 -22
  30. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -153
  31. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -96
  32. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +964 -964
  33. package/templates/nextblock-template/app/api/brand/email-logo/route.ts +48 -48
  34. package/templates/nextblock-template/app/api/cms/check-updates/route.ts +44 -44
  35. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -33
  36. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -63
  37. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3490 -3490
  38. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +5597 -5597
  39. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +39 -39
  40. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -92
  41. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -43
  42. package/templates/nextblock-template/app/api/media/library/route.ts +69 -69
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -53
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -160
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -43
  46. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +441 -441
  47. package/templates/nextblock-template/app/auth/callback/route.ts +31 -31
  48. package/templates/nextblock-template/app/cart/page.tsx +7 -7
  49. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -160
  50. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -149
  51. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -167
  52. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  53. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +90 -90
  54. package/templates/nextblock-template/app/cms/components/ConnectGitHubButton.tsx +122 -122
  55. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -58
  56. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -244
  57. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +36 -36
  58. package/templates/nextblock-template/app/cms/components/SystemAlertsBanner.tsx +112 -112
  59. package/templates/nextblock-template/app/cms/components/TwoFactorReminderBanner.tsx +45 -45
  60. package/templates/nextblock-template/app/cms/components/github-connect-actions.ts +102 -102
  61. package/templates/nextblock-template/app/cms/components/system-alerts-actions.ts +31 -31
  62. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -66
  63. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -519
  64. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -1522
  65. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -256
  66. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -384
  67. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -221
  68. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -12
  69. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -438
  70. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -228
  71. package/templates/nextblock-template/app/cms/dashboard/components/DashboardOnboarding.tsx +130 -130
  72. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +431 -391
  73. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -226
  74. package/templates/nextblock-template/app/cms/interactions/InteractionsModerationClient.tsx +408 -408
  75. package/templates/nextblock-template/app/cms/interactions/page.tsx +51 -51
  76. package/templates/nextblock-template/app/cms/layout.tsx +73 -73
  77. package/templates/nextblock-template/app/cms/media/components/FolderNavigator.tsx +273 -273
  78. package/templates/nextblock-template/app/cms/media/components/FolderTree.tsx +122 -122
  79. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  80. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -69
  81. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +149 -158
  82. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -101
  83. package/templates/nextblock-template/app/cms/navigation/components/NavigationMenuDnd.tsx +3 -3
  84. package/templates/nextblock-template/app/cms/pages/page.tsx +18 -17
  85. package/templates/nextblock-template/app/cms/posts/page.tsx +19 -18
  86. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +12 -0
  87. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -12
  88. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -12
  89. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -13
  90. package/templates/nextblock-template/app/cms/products/new/page.tsx +135 -135
  91. package/templates/nextblock-template/app/cms/products/page.tsx +39 -27
  92. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -133
  93. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -5
  94. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -456
  95. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -115
  96. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -31
  97. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -19
  98. package/templates/nextblock-template/app/cms/revisions/utils.ts +132 -132
  99. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -1004
  100. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -29
  101. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -93
  102. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -129
  103. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -24
  104. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +497 -497
  105. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -410
  106. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +246 -246
  107. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -80
  108. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -331
  109. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -494
  110. package/templates/nextblock-template/app/cms/settings/email/actions.ts +64 -64
  111. package/templates/nextblock-template/app/cms/settings/email/components/EmailForm.tsx +181 -181
  112. package/templates/nextblock-template/app/cms/settings/email/page.tsx +28 -28
  113. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -767
  114. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -93
  115. package/templates/nextblock-template/app/cms/settings/google-analytics/actions.ts +60 -60
  116. package/templates/nextblock-template/app/cms/settings/google-analytics/components/GoogleAnalyticsForm.tsx +129 -129
  117. package/templates/nextblock-template/app/cms/settings/google-analytics/page.tsx +26 -26
  118. package/templates/nextblock-template/app/cms/settings/languages/actions.ts +16 -16
  119. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  120. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -339
  121. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -21
  122. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -20
  123. package/templates/nextblock-template/app/cms/settings/logos/components/SetActiveLogoButton.tsx +42 -42
  124. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -133
  125. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  126. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -122
  127. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +52 -52
  128. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +148 -148
  129. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +27 -27
  130. package/templates/nextblock-template/app/cms/settings/registration/actions.ts +44 -44
  131. package/templates/nextblock-template/app/cms/settings/registration/components/RegistrationForm.tsx +65 -65
  132. package/templates/nextblock-template/app/cms/settings/registration/page.tsx +27 -27
  133. package/templates/nextblock-template/app/cms/settings/security/actions.ts +284 -284
  134. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +522 -522
  135. package/templates/nextblock-template/app/cms/settings/security/page.tsx +33 -33
  136. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -21
  137. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -20
  138. package/templates/nextblock-template/app/cms/users/actions.ts +222 -222
  139. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +12 -12
  140. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +44 -44
  141. package/templates/nextblock-template/app/lib/site-settings.ts +105 -105
  142. package/templates/nextblock-template/app/lib/sitemap-utils.ts +253 -253
  143. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -73
  144. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -16
  145. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -9
  146. package/templates/nextblock-template/app/profile/account-data.ts +37 -37
  147. package/templates/nextblock-template/app/profile/account-links.ts +22 -22
  148. package/templates/nextblock-template/app/profile/account-types.ts +11 -11
  149. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -124
  150. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -79
  151. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -32
  152. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -19
  153. package/templates/nextblock-template/app/profile/page.tsx +42 -42
  154. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -128
  155. package/templates/nextblock-template/app/profile/password/actions.ts +59 -59
  156. package/templates/nextblock-template/app/profile/password/page.tsx +27 -27
  157. package/templates/nextblock-template/app/setup/SetupWizard.tsx +678 -678
  158. package/templates/nextblock-template/app/setup/layout.tsx +13 -13
  159. package/templates/nextblock-template/app/setup/page.tsx +111 -111
  160. package/templates/nextblock-template/app/sitemap.ts +130 -130
  161. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -7
  162. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -23
  163. package/templates/nextblock-template/components/DeferredGoogleAnalytics.tsx +70 -70
  164. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -70
  165. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -69
  166. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -47
  167. package/templates/nextblock-template/components/FooterNavigation.tsx +32 -32
  168. package/templates/nextblock-template/components/GitHubLoginButton.tsx +15 -15
  169. package/templates/nextblock-template/components/HtmlScriptExecutor.tsx +47 -47
  170. package/templates/nextblock-template/components/LanguageSwitcher.tsx +2 -2
  171. package/templates/nextblock-template/components/PostCommentsSection.tsx +369 -369
  172. package/templates/nextblock-template/components/ProductReviewsSection.tsx +419 -419
  173. package/templates/nextblock-template/components/PublicEnvBootstrap.tsx +30 -30
  174. package/templates/nextblock-template/components/ResponsiveNav.tsx +14 -14
  175. package/templates/nextblock-template/components/auth/AuthBotProtection.tsx +182 -182
  176. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -12
  177. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -12
  178. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -23
  179. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +249 -249
  180. package/templates/nextblock-template/components/blocks/types.ts +7 -7
  181. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  182. package/templates/nextblock-template/components/form-message.tsx +32 -32
  183. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +170 -170
  184. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +70 -70
  185. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -28
  186. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -166
  187. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -464
  188. package/templates/nextblock-template/components/submit-button.tsx +23 -23
  189. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  190. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -21
  191. package/templates/nextblock-template/context/AuthContext.tsx +23 -23
  192. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  193. package/templates/nextblock-template/context/language-rest-client.ts +32 -32
  194. package/templates/nextblock-template/docker/db/init/99-jwt.sql +6 -6
  195. package/templates/nextblock-template/docker/db/init/99-roles.sql +25 -25
  196. package/templates/nextblock-template/docker/kong/kong.yml +112 -112
  197. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -94
  198. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -364
  199. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -202
  200. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +246 -246
  201. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +176 -176
  202. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -146
  203. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -1319
  204. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -222
  205. package/templates/nextblock-template/docs/11-SELF-HOSTED-DOCKER.md +173 -173
  206. package/templates/nextblock-template/docs/12-VERCEL-DEPLOYMENT.md +170 -170
  207. package/templates/nextblock-template/docs/13-STAYING-UP-TO-DATE.md +151 -151
  208. package/templates/nextblock-template/docs/README.md +39 -39
  209. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12505 -12505
  210. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -21
  211. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -101
  212. package/templates/nextblock-template/index.d.ts +7 -7
  213. package/templates/nextblock-template/lib/app-secrets.ts +39 -39
  214. package/templates/nextblock-template/lib/auth/cookies.ts +47 -47
  215. package/templates/nextblock-template/lib/auth/crypto.ts +45 -45
  216. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -92
  217. package/templates/nextblock-template/lib/auth/twoFactor.ts +208 -208
  218. package/templates/nextblock-template/lib/auth-redirects.ts +46 -46
  219. package/templates/nextblock-template/lib/blocks/README.md +13 -13
  220. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +6 -6
  221. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -18
  222. package/templates/nextblock-template/lib/botProtection/verify.ts +134 -134
  223. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +100 -77
  224. package/templates/nextblock-template/lib/cms-transfer/csv.ts +403 -399
  225. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -2243
  226. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -145
  227. package/templates/nextblock-template/lib/config/email-settings.ts +217 -217
  228. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -87
  229. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -178
  230. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -140
  231. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +88 -88
  232. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -256
  233. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -227
  234. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -279
  235. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -14
  236. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -172
  237. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -213
  238. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -22
  239. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -193
  240. package/templates/nextblock-template/lib/email/branding-format.test.ts +133 -133
  241. package/templates/nextblock-template/lib/email/branding-format.ts +123 -123
  242. package/templates/nextblock-template/lib/email/branding.ts +76 -76
  243. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -121
  244. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -206
  245. package/templates/nextblock-template/lib/full-backup/server.ts +743 -743
  246. package/templates/nextblock-template/lib/logos/active-logo.ts +53 -53
  247. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +54 -54
  248. package/templates/nextblock-template/lib/onboarding/actions.ts +31 -31
  249. package/templates/nextblock-template/lib/onboarding/status.ts +222 -222
  250. package/templates/nextblock-template/lib/posts/readTime.ts +60 -60
  251. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -57
  252. package/templates/nextblock-template/lib/privacy/contact-emails.ts +64 -64
  253. package/templates/nextblock-template/lib/privacy/settings.ts +115 -115
  254. package/templates/nextblock-template/lib/privacy/types.ts +69 -69
  255. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -74
  256. package/templates/nextblock-template/lib/promotions/server.ts +741 -741
  257. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -142
  258. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -255
  259. package/templates/nextblock-template/lib/search/server.ts +585 -585
  260. package/templates/nextblock-template/lib/search/types.ts +27 -27
  261. package/templates/nextblock-template/lib/setup/actions.ts +460 -460
  262. package/templates/nextblock-template/lib/setup/env-status.ts +125 -125
  263. package/templates/nextblock-template/lib/setup/env-write.ts +111 -111
  264. package/templates/nextblock-template/lib/setup/migrations-bundle.ts +37 -37
  265. package/templates/nextblock-template/lib/setup/provisioning.ts +59 -59
  266. package/templates/nextblock-template/lib/setup/schema-apply.ts +408 -408
  267. package/templates/nextblock-template/lib/setup/system-config.ts +105 -105
  268. package/templates/nextblock-template/lib/setup/types.ts +18 -18
  269. package/templates/nextblock-template/lib/site-url.ts +48 -48
  270. package/templates/nextblock-template/lib/storage/provider.ts +66 -66
  271. package/templates/nextblock-template/lib/storage/supabase-storage.ts +103 -103
  272. package/templates/nextblock-template/lib/updates/check-upstream.ts +441 -441
  273. package/templates/nextblock-template/lib/updates/github-device.ts +206 -206
  274. package/templates/nextblock-template/lib/updates/repo-identity.ts +56 -56
  275. package/templates/nextblock-template/package.json +1 -1
  276. package/templates/nextblock-template/postcss.config.js +6 -6
  277. package/templates/nextblock-template/scripts/backup.js +115 -115
  278. package/templates/nextblock-template/scripts/docker-setup.mjs +310 -310
  279. package/templates/nextblock-template/scripts/restore.js +385 -385
  280. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +98 -98
  281. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -62
  282. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -537
  283. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -58
  284. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -188
  285. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -123
  286. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -133
  287. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -65
  288. package/templates/nextblock-template/tailwind.config.js +25 -25
  289. package/templates/nextblock-template/tools/build-migrate.mjs +209 -209
  290. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -282
  291. package/templates/nextblock-template/types/jsdom.d.ts +6 -6
  292. package/tsconfig.base.json +3 -3
  293. package/templates/nextblock-template/tsconfig.tsbuildinfo +0 -1
@@ -1,964 +1,964 @@
1
- import { stepCountIs, streamText } from 'ai';
2
- import { NextResponse } from 'next/server';
3
-
4
- import {
5
- createClient,
6
- getServiceRoleSupabaseClient,
7
- verifyPackageOnline,
8
- } from '@nextblock-cms/db/server';
9
- import {
10
- buildCortexAiRoutingPolicy,
11
- buildVisibleContactIntroActionPlan,
12
- cortexAiPageContextSchema,
13
- createCortexGlobalAgentTools,
14
- createCortexAiOpenRouterClient,
15
- executeCmsActionPlan,
16
- executeCreateCmsPage,
17
- executeCreateCmsPost,
18
- executeCreateCmsProduct,
19
- executeDatabaseActionPlan,
20
- executeDatabaseMutation,
21
- executeDeleteCmsItem,
22
- executeDeleteCustomBlock,
23
- executeInsertContentBlock,
24
- executeUpdateContentBlock,
25
- executeUpdateCmsItemField,
26
- executeUpdateCurrentCmsFields,
27
- executeUpdateFooter,
28
- executeUpdateNavigationBar,
29
- executeUpdateSectionColumnBlock,
30
- isOpenRouterRateLimitError,
31
- omitUnsupportedCortexAiModelOptions,
32
- safeParseCortexAiModelSelection,
33
- summarizeCortexAiRoutingError,
34
- type CortexAiPageContext,
35
- z,
36
- } from '@nextblock-cms/cortex';
37
- import { validateBlockContent } from '../../../../lib/blocks/blockRegistry';
38
-
39
- export const dynamic = 'force-dynamic';
40
-
41
- const GLOBAL_AGENT_MODEL_ATTEMPT_TIMEOUT_MS = 30000;
42
-
43
- const globalAgentMessageSchema = z.strictObject({
44
- content: z.string().min(1).max(8000),
45
- role: z.enum(['system', 'user', 'assistant']),
46
- });
47
-
48
- const confirmedToolCallSchema = z.strictObject({
49
- confirmationPhrase: z.string().min(1).max(500),
50
- input: z.unknown(),
51
- toolName: z.enum([
52
- 'create_cms_page',
53
- 'create_cms_post',
54
- 'create_cms_product',
55
- 'delete_cms_item',
56
- 'delete_custom_block',
57
- 'execute_database_action_plan',
58
- 'execute_database_mutation',
59
- 'execute_cms_action_plan',
60
- 'insert_content_block',
61
- 'update_cms_item_field',
62
- 'update_content_block',
63
- 'update_current_cms_fields',
64
- 'update_footer',
65
- 'update_navigation_bar',
66
- 'update_section_column_block',
67
- ]),
68
- });
69
-
70
- const globalAgentRequestSchema = z.strictObject({
71
- confirmedToolCall: confirmedToolCallSchema.optional(),
72
- messages: z.array(globalAgentMessageSchema).min(1).max(40),
73
- pageContext: cortexAiPageContextSchema.nullable().optional(),
74
- });
75
-
76
- const GLOBAL_AGENT_SYSTEM_PROMPT = [
77
- 'You are NextBlock Cortex AI, the global dashboard agent for a block-based CMS.',
78
- 'Operate as a Planner, Executor, and Evaluator.',
79
- 'Plan the smallest safe change, execute only through typed tools, evaluate the tool result, then answer concisely.',
80
- 'If the user asks for multiple CMS mutations in one prompt, such as creating a page and adding a navigation link, use execute_cms_action_plan so the user sees one combined confirmation and one Confirm button. The action plan must include every requested mutation; do not fall back to confirming only the first task.',
81
- 'For execute_cms_action_plan, actions must be JSON objects, for example { "tool": "create_cms_page", "input": { "title": "Contact Us" } }, never strings like create_cms_page(...).',
82
- 'Every mutating tool is confirmed two-step. First call the right tool with the exact normalized payload. If the tool returns requiresConfirmation, do not say the work is done; say "Please confirm for me to complete:" and summarize the requested change. Do not print confirmationPhrase unless the user explicitly asks for the raw phrase.',
83
- 'When the latest user message is an exact confirmation phrase, call the same mutating tool again with the same payload so the tool can execute. Only report success after the tool result has mutationExecuted=true.',
84
- 'Use create_cms_page, create_cms_post, and create_cms_product for CMS creation. New pages, posts, and products default to draft unless the user explicitly asks for a public/active status.',
85
- 'Use update_cms_item_field for one precise field update at a time, such as price, stock, sale_price, title, slug, status, or SEO metadata. Interpret "public" as published for pages/posts and active for products.',
86
- 'Use prepare_delete_cms_item or delete_cms_item for delete requests. Do not delete anything until the user sends the exact confirmation phrase returned by the tool.',
87
- 'If a user asks for sale start/end dates or scheduled specials, explain that scheduled specials are not supported by the current schema; you may offer to set or clear sale_price only.',
88
- 'Use update_navigation_bar for public header navigation changes. For requests that ask to add a header link, use update_navigation_bar with mode "append" unless the user clearly asks to replace the whole menu.',
89
- 'For requests that ask to rename or change one existing navigation link, use update_navigation_bar with mode "update" and identify the existing item with match.label or match.url. Never use mode "replace" for a one-link rename.',
90
- 'Use mode "replace" only when the user explicitly asks to rebuild or replace the entire navigation menu and you provide the full menu.',
91
- 'Use update_footer for public footer links or copyright settings.',
92
- 'For custom/reusable block types (a "custom block", "block type", "widget", or a request to design a new kind of block such as a product card, testimonial, or feature card), use the global custom block tools, which do NOT need an open page/post/product: create_custom_block to build a new block from a description, update_custom_block to edit one by slug, delete_custom_block to remove one, and list_custom_blocks to find a slug. Never tell the user to open a page first for these; never use insert_content_block or page-aware tools to define a new block type. create_custom_block and update_custom_block run immediately; after success, tell the user the block was added to their Custom Blocks library and can now be dropped onto any page.',
93
- 'Distinguish adding content to the current page (page-aware tools, needs page context) from defining a reusable block type (custom block tools, global, no page context).',
94
- 'When editing a CMS page, post, product, or block, use page-aware tools only. Use read_current_cms_item before updating content unless the user provided exact field/block data.',
95
- 'Use update_current_cms_fields for current page/post/product metadata and product description_json. Use update_content_block for top-level page/post blocks. Use update_section_column_block for nested blocks inside section or hero blocks.',
96
- 'When the user asks to add a visible title, heading, intro, description, or copy above/below a form or other block, use insert_content_block with a text or heading block. Do not treat visible page copy as meta_title, meta_description, or SEO metadata unless the user explicitly says SEO/meta.',
97
- 'For requests like "add a title and description to both pages and incite them to contact us", use execute_cms_action_plan with one insert_content_block action per translated page, usually a text block before the form with localized heading and paragraph HTML.',
98
- 'Do not use update_section_column_block to change an existing nested block from one block type to another. That tool edits only the content of the existing nested block type.',
99
- 'When the user asks to add a new nested block inside a hero or section, such as adding a button to a hero, use update_content_block on the parent hero/section block. Prefer content.append_block, for example { block_type: "button", content: { text: "Contact Us", url: "/contact" } }, so the tool preserves existing column_blocks and layout fields.',
100
- 'When a user names a language, pass that language name or its locale code in languageCode; examples: French maps to fr, English maps to en.',
101
- 'For follow-up requests like "also add it in French", use the prior requested item and apply it to the named language. For page/post/product translations, pass the current translationGroupId into the creation tool so the backend links the language versions.',
102
- 'Use search_documentation before answering implementation or CMS usage questions that require factual project context.',
103
- 'Use describe_database_schema, read_database_records, execute_database_mutation, and execute_database_action_plan for direct database tasks that are not covered by a more specific CMS tool. Use typed CRUD tools only; never ask for or invent raw SQL.',
104
- 'For direct database mutations, always return the confirmation preview first. Never claim a database mutation is complete until the confirmed tool result has mutationExecuted=true. Do not edit auth users, profiles, user addresses, password fields, API keys, tokens, secrets, private keys, credentials, or the cortex_ai_openrouter_api_key site setting.',
105
- 'Use fetch_ecommerce_stats for quantitative questions about revenue, products, or order counts. This tool is read-only.',
106
- 'For order-status questions like "how many pending orders" or "how many trial orders", use the tool result report.matchingOrderStatus or report.orderStatusCounts, and use all_time unless the user names a specific time period.',
107
- 'Never invent database fields, raw SQL, markdown content, or unsupported tool arguments.',
108
- ].join(' ');
109
-
110
- type CortexAgentStreamEvent =
111
- | {
112
- credentialSource: string;
113
- modelId: string;
114
- type: 'meta';
115
- }
116
- | {
117
- text: string;
118
- type: 'text-delta';
119
- }
120
- | {
121
- input?: unknown;
122
- toolCallId?: string;
123
- toolName: string;
124
- type: 'tool-call';
125
- }
126
- | {
127
- output?: unknown;
128
- toolCallId?: string;
129
- toolName: string;
130
- type: 'tool-result';
131
- }
132
- | {
133
- message: string;
134
- toolCallId?: string;
135
- toolName?: string;
136
- type: 'tool-error';
137
- }
138
- | {
139
- message: string;
140
- type: 'error';
141
- }
142
- | {
143
- type: 'finish';
144
- };
145
-
146
- type CortexAgentStreamPart = {
147
- error?: unknown;
148
- id?: string;
149
- input?: unknown;
150
- output?: unknown;
151
- text?: string;
152
- toolCallId?: string;
153
- toolName?: string;
154
- type: string;
155
- };
156
-
157
- async function requireAdminAccess() {
158
- const supabase = createClient();
159
- const {
160
- data: { user },
161
- error: userError,
162
- } = await supabase.auth.getUser();
163
-
164
- if (userError || !user) {
165
- return null;
166
- }
167
-
168
- const { data: profile, error: profileError } = await supabase
169
- .from('profiles')
170
- .select('role')
171
- .eq('id', user.id)
172
- .single();
173
-
174
- return !profileError && profile?.role === 'ADMIN' ? { userId: user.id } : null;
175
- }
176
-
177
- function jsonError(message: string, status: number) {
178
- return NextResponse.json({ error: message }, { status });
179
- }
180
-
181
- const encoder = new TextEncoder();
182
-
183
- function encodeStreamEvent(event: CortexAgentStreamEvent) {
184
- return encoder.encode(`data: ${JSON.stringify(event)}\n\n`);
185
- }
186
-
187
- function formatPageContextForPrompt(pageContext: CortexAiPageContext | null | undefined) {
188
- if (!pageContext) {
189
- return 'No current CMS edit context was supplied. Ask the user to open the relevant edit screen before using page-aware editing tools.';
190
- }
191
-
192
- return [
193
- `Current CMS edit context: contentType=${pageContext.contentType}`,
194
- `entityId=${String(pageContext.entityId)}`,
195
- pageContext.slug ? `slug=${pageContext.slug}` : null,
196
- pageContext.title ? `title=${pageContext.title}` : null,
197
- pageContext.translationGroupId ? `translationGroupId=${pageContext.translationGroupId}` : null,
198
- pageContext.languageId ? `languageId=${pageContext.languageId}` : null,
199
- pageContext.currentEditor?.field ? `currentField=${pageContext.currentEditor.field}` : null,
200
- pageContext.currentEditor?.blockId ? `currentBlockId=${pageContext.currentEditor.blockId}` : null,
201
- pageContext.currentEditor?.blockType ? `currentBlockType=${pageContext.currentEditor.blockType}` : null,
202
- ]
203
- .filter(Boolean)
204
- .join(', ');
205
- }
206
-
207
- function buildGlobalAgentSystemPrompt(pageContext: CortexAiPageContext | null | undefined) {
208
- return [
209
- GLOBAL_AGENT_SYSTEM_PROMPT,
210
- formatPageContextForPrompt(pageContext),
211
- 'When the user says "this page", "this post", "this product", "this field", or "this block", interpret that through the supplied current CMS edit context.',
212
- 'Do not update content outside the supplied current CMS context.',
213
- ].join(' ');
214
- }
215
-
216
- function serializeStreamError(error: unknown) {
217
- return summarizeCortexAiRoutingError(
218
- error,
219
- error instanceof Error ? error.message : 'Cortex AI global agent failed.'
220
- );
221
- }
222
-
223
- function getToolCallId(part: CortexAgentStreamPart) {
224
- return part.toolCallId || part.id;
225
- }
226
-
227
- function looksLikeRawToolCallLeak(value: string) {
228
- const normalized = value.toLowerCase();
229
-
230
- return (
231
- normalized.includes('<toolcall') ||
232
- normalized.includes('</toolcall') ||
233
- normalized.includes('"arguments"') ||
234
- normalized.includes('"update_navigation_bar"') ||
235
- normalized.includes('"update_footer"') ||
236
- normalized.includes('"search_documentation"') ||
237
- normalized.includes('"read_current_cms_item"') ||
238
- normalized.includes('"update_current_cms_fields"') ||
239
- normalized.includes('"update_cms_item_field"') ||
240
- normalized.includes('"update_content_block"') ||
241
- normalized.includes('"insert_content_block"') ||
242
- normalized.includes('"update_section_column_block"') ||
243
- normalized.includes('"create_cms_page"') ||
244
- normalized.includes('"create_cms_post"') ||
245
- normalized.includes('"create_cms_product"') ||
246
- normalized.includes('"execute_cms_action_plan"') ||
247
- normalized.includes('"prepare_delete_cms_item"') ||
248
- normalized.includes('"delete_cms_item"')
249
- );
250
- }
251
-
252
- function looksLikeRateLimitText(value: string) {
253
- const normalized = value.toLowerCase();
254
-
255
- return (
256
- normalized.includes('rate limit exceeded') ||
257
- normalized.includes('free-models-per-day') ||
258
- normalized.includes('too many requests')
259
- );
260
- }
261
-
262
- function readNumberField(value: unknown, key: string) {
263
- if (!value || typeof value !== 'object' || !(key in value)) {
264
- return null;
265
- }
266
-
267
- const parsed = Number((value as Record<string, unknown>)[key]);
268
- return Number.isFinite(parsed) ? parsed : null;
269
- }
270
-
271
- function isRecord(value: unknown): value is Record<string, unknown> {
272
- return Boolean(value && typeof value === 'object' && !Array.isArray(value));
273
- }
274
-
275
- function readStringField(value: unknown, key: string) {
276
- if (!isRecord(value)) {
277
- return null;
278
- }
279
-
280
- const fieldValue = value[key];
281
-
282
- return typeof fieldValue === 'string' ? fieldValue : null;
283
- }
284
-
285
- function pluralize(count: number, singular: string, plural = `${singular}s`) {
286
- return `${count} ${count === 1 ? singular : plural}`;
287
- }
288
-
289
- function getConfirmationSummary(toolName?: string, output?: unknown) {
290
- if (!isRecord(output) || !isRecord(output.preview)) {
291
- return 'Complete the requested CMS change.';
292
- }
293
-
294
- const preview = output.preview;
295
- const summary = readStringField(preview, 'summary');
296
-
297
- if (summary) {
298
- const actionSummaries = Array.isArray(preview.actionSummaries)
299
- ? preview.actionSummaries.filter((item): item is string => typeof item === 'string')
300
- : [];
301
-
302
- return actionSummaries.length > 0
303
- ? `${summary}\n\n${actionSummaries.map((item, index) => `${index + 1}. ${item}`).join('\n')}`
304
- : summary;
305
- }
306
-
307
- const title = readStringField(preview, 'title');
308
- const slug = readStringField(preview, 'slug');
309
- const status = readStringField(preview, 'status');
310
- const contentType = readStringField(preview, 'contentType');
311
- const field = readStringField(preview, 'field');
312
- const mode = readStringField(preview, 'mode');
313
- const languageCode = readStringField(preview, 'languageCode');
314
- const blockCount = readNumberField(preview, 'blockCount');
315
- const itemCount = readNumberField(preview, 'itemCount');
316
- const affectedCount = readNumberField(preview, 'affectedCount');
317
-
318
- if (toolName === 'create_cms_page' || toolName === 'create_cms_post') {
319
- return `Create ${status || 'draft'} ${toolName === 'create_cms_page' ? 'page' : 'post'} "${title || slug || 'Untitled'}"${slug ? ` at slug "${slug}"` : ''}${blockCount !== null ? ` with ${pluralize(blockCount, 'content block')}` : ''}.`;
320
- }
321
-
322
- if (toolName === 'create_cms_product') {
323
- return `Create ${status || 'draft'} product "${title || slug || 'Untitled'}"${slug ? ` at slug "${slug}"` : ''}.`;
324
- }
325
-
326
- if (toolName === 'update_cms_item_field') {
327
- return `Update ${field || 'one field'} on the ${contentType || 'CMS item'} "${title || slug || 'selected item'}".`;
328
- }
329
-
330
- if (toolName === 'update_navigation_bar') {
331
- return `${mode === 'append' ? 'Add' : mode === 'update' ? 'Update' : 'Replace'} ${itemCount !== null ? pluralize(itemCount, 'navigation item') : 'navigation items'} in the ${languageCode || 'selected'} header navigation.`;
332
- }
333
-
334
- if (toolName === 'update_footer') {
335
- const linkCount = readNumberField(preview, 'linkCount');
336
- return `Update the ${languageCode || 'selected'} footer${linkCount !== null ? ` with ${pluralize(linkCount, 'link')}` : ''}.`;
337
- }
338
-
339
- if (toolName === 'update_content_block') {
340
- return `Update the selected ${readStringField(preview, 'blockType') || 'content'} block.`;
341
- }
342
-
343
- if (toolName === 'insert_content_block') {
344
- return `Insert ${readStringField(preview, 'blockType') || 'content'} block on the ${contentType || 'CMS item'} "${title || slug || 'selected item'}".`;
345
- }
346
-
347
- if (toolName === 'update_section_column_block') {
348
- return `Update the selected nested ${readStringField(preview, 'nestedBlockType') || 'section'} block.`;
349
- }
350
-
351
- if (toolName === 'delete_cms_item' || toolName === 'prepare_delete_cms_item') {
352
- return `Delete ${affectedCount !== null ? pluralize(affectedCount, contentType || 'CMS item') : `the selected ${contentType || 'CMS item'}`}${title || slug ? ` for "${title || slug}"` : ''}.`;
353
- }
354
-
355
- return 'Complete the requested CMS change.';
356
- }
357
-
358
- function getToolCompletionMessage(toolName?: string, output?: unknown) {
359
- if (isRecord(output)) {
360
- if (output.requiresConfirmation === true) {
361
- return `Please confirm for me to complete:\n\n${getConfirmationSummary(toolName, output)}`;
362
- }
363
-
364
- if (output.unsupported === true || output.success === false) {
365
- return readStringField(output, 'message') || 'I could not complete that request.';
366
- }
367
- }
368
-
369
- const mutationExecuted = isRecord(output) && output.mutationExecuted === true;
370
-
371
- if (toolName === 'update_navigation_bar') {
372
- const insertedCount = readNumberField(output, 'insertedCount');
373
- const skippedCount = readNumberField(output, 'skippedCount');
374
-
375
- if ((insertedCount ?? 0) === 0 && (skippedCount ?? 0) > 0) {
376
- return 'That navigation link already exists, so I left the header unchanged.';
377
- }
378
-
379
- return 'Done. I updated the navigation bar.';
380
- }
381
-
382
- if (toolName === 'update_footer') {
383
- return 'Done. I updated the footer.';
384
- }
385
-
386
- if (toolName === 'search_documentation') {
387
- return 'I searched the documentation, but the model was interrupted before it could finish a summary.';
388
- }
389
-
390
- if (toolName === 'read_current_cms_item') {
391
- return 'I read the current CMS item, but the model was interrupted before it could finish a summary.';
392
- }
393
-
394
- if (toolName === 'fetch_ecommerce_stats') {
395
- return 'I fetched the latest ecommerce statistics for you.';
396
- }
397
-
398
- if (toolName === 'describe_database_schema') {
399
- return 'I inspected the available database schema.';
400
- }
401
-
402
- if (toolName === 'read_database_records') {
403
- return 'I read the requested database records.';
404
- }
405
-
406
- if (toolName === 'execute_database_mutation') {
407
- const affectedCount = readNumberField(output, 'affectedCount');
408
- const table = readStringField(output, 'table');
409
- const auditLogged = isRecord(output) && output.auditLogged === true;
410
-
411
- return mutationExecuted
412
- ? `Done. I updated ${affectedCount ? pluralize(affectedCount, 'database row') : 'the database'}${table ? ` in ${table}` : ''}.${auditLogged ? ' Audit logged.' : ''}`
413
- : 'I prepared the database mutation.';
414
- }
415
-
416
- if (toolName === 'execute_database_action_plan') {
417
- const actionCount = readNumberField(output, 'actionCount');
418
- const auditLogged = isRecord(output) && output.auditLogged === true;
419
-
420
- return mutationExecuted
421
- ? `Done. I completed ${actionCount ? pluralize(actionCount, 'database action') : 'the database action plan'}.${auditLogged ? ' Audit logged.' : ''}`
422
- : 'I prepared the database action plan.';
423
- }
424
-
425
- if (toolName === 'update_current_cms_fields') {
426
- return 'Done. I updated the current CMS fields.';
427
- }
428
-
429
- if (toolName === 'update_cms_item_field') {
430
- return mutationExecuted
431
- ? 'Done. I updated that CMS field.'
432
- : 'I prepared the CMS field update.';
433
- }
434
-
435
- if (toolName === 'update_content_block') {
436
- return 'Done. I updated the current content block.';
437
- }
438
-
439
- if (toolName === 'insert_content_block') {
440
- return 'Done. I inserted the content block.';
441
- }
442
-
443
- if (toolName === 'update_section_column_block') {
444
- return 'Done. I updated the nested section block.';
445
- }
446
-
447
- if (toolName === 'create_cms_page') {
448
- return mutationExecuted ? 'Done. I created the page.' : 'I prepared the page creation.';
449
- }
450
-
451
- if (toolName === 'create_cms_post') {
452
- return mutationExecuted ? 'Done. I created the post.' : 'I prepared the post creation.';
453
- }
454
-
455
- if (toolName === 'create_cms_product') {
456
- return mutationExecuted ? 'Done. I created the product.' : 'I prepared the product creation.';
457
- }
458
-
459
- if (toolName === 'prepare_delete_cms_item') {
460
- return getToolCompletionMessage('delete_cms_item', output);
461
- }
462
-
463
- if (toolName === 'delete_cms_item') {
464
- return mutationExecuted ? 'Done. I deleted that CMS item.' : 'I prepared the delete request.';
465
- }
466
-
467
- if (toolName === 'execute_cms_action_plan') {
468
- const actionCount = readNumberField(output, 'actionCount');
469
-
470
- return mutationExecuted
471
- ? `Done. I completed ${actionCount ? pluralize(actionCount, 'CMS action') : 'the CMS action plan'}.`
472
- : 'I prepared the CMS action plan.';
473
- }
474
-
475
- return 'Done. I completed the requested update.';
476
- }
477
-
478
- function completeToolBackedText(text: string, toolName?: string, output?: unknown) {
479
- const trimmedText = text.trim();
480
-
481
- if (!isRecord(output)) {
482
- return trimmedText || getToolCompletionMessage(toolName, output);
483
- }
484
-
485
- if (output.requiresConfirmation === true) {
486
- return getToolCompletionMessage(toolName, output);
487
- }
488
-
489
- if ((output.unsupported === true || output.success === false) && !trimmedText) {
490
- return getToolCompletionMessage(toolName, output);
491
- }
492
-
493
- return trimmedText || getToolCompletionMessage(toolName, output);
494
- }
495
-
496
- async function executeConfirmedToolCall(params: {
497
- context: Parameters<typeof createCortexGlobalAgentTools>[0];
498
- input: unknown;
499
- toolName: z.infer<typeof confirmedToolCallSchema>['toolName'];
500
- }) {
501
- switch (params.toolName) {
502
- case 'create_cms_page':
503
- return executeCreateCmsPage(params.input as any, params.context);
504
- case 'create_cms_post':
505
- return executeCreateCmsPost(params.input as any, params.context);
506
- case 'create_cms_product':
507
- return executeCreateCmsProduct(params.input as any, params.context);
508
- case 'delete_cms_item':
509
- return executeDeleteCmsItem(params.input as any, params.context);
510
- case 'delete_custom_block':
511
- return executeDeleteCustomBlock(params.input as any, params.context);
512
- case 'execute_database_action_plan':
513
- return executeDatabaseActionPlan(params.input as any, params.context);
514
- case 'execute_database_mutation':
515
- return executeDatabaseMutation(params.input as any, params.context);
516
- case 'execute_cms_action_plan':
517
- return executeCmsActionPlan(params.input as any, params.context);
518
- case 'update_cms_item_field':
519
- return executeUpdateCmsItemField(params.input as any, params.context);
520
- case 'update_content_block':
521
- return executeUpdateContentBlock(params.input as any, params.context);
522
- case 'insert_content_block':
523
- return executeInsertContentBlock(params.input as any, params.context);
524
- case 'update_current_cms_fields':
525
- return executeUpdateCurrentCmsFields(params.input as any, params.context);
526
- case 'update_footer':
527
- return executeUpdateFooter(params.input as any, params.context);
528
- case 'update_navigation_bar':
529
- return executeUpdateNavigationBar(params.input as any, params.context);
530
- case 'update_section_column_block':
531
- return executeUpdateSectionColumnBlock(params.input as any, params.context);
532
- }
533
- }
534
-
535
- function createConfirmedToolCallStream(params: {
536
- context: Parameters<typeof createCortexGlobalAgentTools>[0];
537
- input: unknown;
538
- toolName: z.infer<typeof confirmedToolCallSchema>['toolName'];
539
- }) {
540
- return new ReadableStream({
541
- async start(controller) {
542
- const toolCallId = `confirmed-${Date.now()}`;
543
-
544
- controller.enqueue(
545
- encodeStreamEvent({
546
- input: params.input,
547
- toolCallId,
548
- toolName: params.toolName,
549
- type: 'tool-call',
550
- })
551
- );
552
-
553
- try {
554
- const output = await executeConfirmedToolCall(params);
555
-
556
- controller.enqueue(
557
- encodeStreamEvent({
558
- output,
559
- toolCallId,
560
- toolName: params.toolName,
561
- type: 'tool-result',
562
- })
563
- );
564
- controller.enqueue(
565
- encodeStreamEvent({
566
- text: getToolCompletionMessage(params.toolName, output),
567
- type: 'text-delta',
568
- })
569
- );
570
- } catch (error) {
571
- controller.enqueue(
572
- encodeStreamEvent({
573
- message: serializeStreamError(error),
574
- toolCallId,
575
- toolName: params.toolName,
576
- type: 'tool-error',
577
- })
578
- );
579
- controller.enqueue(
580
- encodeStreamEvent({
581
- message: serializeStreamError(error),
582
- type: 'error',
583
- })
584
- );
585
- }
586
-
587
- controller.enqueue(encodeStreamEvent({ type: 'finish' }));
588
- controller.close();
589
- },
590
- });
591
- }
592
-
593
- function getRetryableStreamError(
594
- error: unknown,
595
- sawRawToolCallLeak: boolean,
596
- sawRateLimitText: boolean
597
- ) {
598
- if (sawRawToolCallLeak) {
599
- return new Error('OpenRouter returned an invalid raw tool-call payload.');
600
- }
601
-
602
- if (sawRateLimitText) {
603
- return new Error('OpenRouter rate limit exceeded.');
604
- }
605
-
606
- return error;
607
- }
608
-
609
- function createAttemptAbortSignal(requestSignal: AbortSignal) {
610
- const controller = new AbortController();
611
- const timeoutId = setTimeout(() => {
612
- controller.abort(new Error('Cortex AI response timed out. Please try again.'));
613
- }, GLOBAL_AGENT_MODEL_ATTEMPT_TIMEOUT_MS);
614
- const abortFromRequest = () => controller.abort(requestSignal.reason);
615
-
616
- if (requestSignal.aborted) {
617
- abortFromRequest();
618
- } else {
619
- requestSignal.addEventListener('abort', abortFromRequest, { once: true });
620
- }
621
-
622
- return {
623
- cleanup: () => {
624
- clearTimeout(timeoutId);
625
- requestSignal.removeEventListener('abort', abortFromRequest);
626
- },
627
- signal: controller.signal,
628
- };
629
- }
630
-
631
- export async function POST(request: Request) {
632
- try {
633
- const adminAccess = await requireAdminAccess();
634
-
635
- if (!adminAccess) {
636
- return jsonError('You do not have permission to use the global Cortex AI agent.', 403);
637
- }
638
-
639
- const isCortexAiActive = await verifyPackageOnline('cortex-ai');
640
-
641
- if (!isCortexAiActive) {
642
- return jsonError('NextBlock Cortex AI is not active for this workspace.', 403);
643
- }
644
-
645
- const body = await request.json().catch(() => null);
646
- const parsedRequest = globalAgentRequestSchema.safeParse(body);
647
-
648
- if (!parsedRequest.success) {
649
- return jsonError('Invalid Cortex AI global-agent request.', 400);
650
- }
651
-
652
- const pageContext = parsedRequest.data.pageContext ?? null;
653
- const latestUserMessage =
654
- [...parsedRequest.data.messages].reverse().find((message) => message.role === 'user')
655
- ?.content ?? '';
656
-
657
- if (parsedRequest.data.confirmedToolCall) {
658
- const confirmedToolCall = parsedRequest.data.confirmedToolCall;
659
- const stream = createConfirmedToolCallStream({
660
- context: {
661
- actorUserId: adminAccess.userId,
662
- latestUserMessage: confirmedToolCall.confirmationPhrase,
663
- pageContext,
664
- supabase: getServiceRoleSupabaseClient(),
665
- validateBlockContent,
666
- },
667
- input: confirmedToolCall.input,
668
- toolName: confirmedToolCall.toolName,
669
- });
670
-
671
- return new Response(stream, {
672
- headers: {
673
- 'Cache-Control': 'no-cache, no-transform',
674
- Connection: 'keep-alive',
675
- 'Content-Type': 'text/event-stream; charset=utf-8',
676
- 'X-Accel-Buffering': 'no',
677
- },
678
- });
679
- }
680
-
681
- const directActionPlan = buildVisibleContactIntroActionPlan(latestUserMessage);
682
-
683
- if (directActionPlan) {
684
- const stream = createConfirmedToolCallStream({
685
- context: {
686
- actorUserId: adminAccess.userId,
687
- latestUserMessage,
688
- pageContext,
689
- supabase: getServiceRoleSupabaseClient(),
690
- validateBlockContent,
691
- },
692
- input: directActionPlan,
693
- toolName: 'execute_cms_action_plan',
694
- });
695
-
696
- return new Response(stream, {
697
- headers: {
698
- 'Cache-Control': 'no-cache, no-transform',
699
- Connection: 'keep-alive',
700
- 'Content-Type': 'text/event-stream; charset=utf-8',
701
- 'X-Accel-Buffering': 'no',
702
- },
703
- });
704
- }
705
-
706
- const sandboxKey = process.env.NEXT_PUBLIC_IS_SANDBOX === 'true' ? request.headers.get('x-sandbox-openrouter-key') : null;
707
- const sandboxModelRaw = process.env.NEXT_PUBLIC_IS_SANDBOX === 'true' ? request.headers.get('x-sandbox-openrouter-model') : null;
708
-
709
- let modelSelection = null;
710
- if (sandboxModelRaw) {
711
- try {
712
- modelSelection = safeParseCortexAiModelSelection(JSON.parse(sandboxModelRaw));
713
- } catch {
714
- // Ignore parse errors from headers
715
- }
716
- }
717
-
718
- const client = await createCortexAiOpenRouterClient({
719
- apiKey: sandboxKey || undefined,
720
- modelSelection: sandboxKey && modelSelection ? modelSelection : undefined,
721
- });
722
- const routingPolicy = buildCortexAiRoutingPolicy({
723
- credentialSource: client.credentialSource,
724
- selectedModel: client.modelSelection,
725
- });
726
- const modelIds = routingPolicy.modelIds;
727
- const tools = createCortexGlobalAgentTools({
728
- actorUserId: adminAccess.userId,
729
- cortexAiApiKey: sandboxKey,
730
- cortexAiModelSelection: sandboxKey && modelSelection ? modelSelection : undefined,
731
- latestUserMessage,
732
- pageContext,
733
- supabase: getServiceRoleSupabaseClient(),
734
- validateBlockContent,
735
- });
736
- const systemPrompt = buildGlobalAgentSystemPrompt(pageContext);
737
-
738
- const stream = new ReadableStream({
739
- async start(controller) {
740
- let completed = false;
741
- let lastError: unknown = null;
742
-
743
- for (const [index, modelId] of modelIds.entries()) {
744
- let textBuffer = '';
745
- let sawRawToolCallLeak = false;
746
- let sawRateLimitText = false;
747
- let hasToolCall = false;
748
- let hasSuccessfulToolResult = false;
749
- let lastToolName: string | undefined;
750
- let lastToolOutput: unknown;
751
-
752
- controller.enqueue(
753
- encodeStreamEvent({
754
- credentialSource: client.credentialSource,
755
- modelId,
756
- type: 'meta',
757
- })
758
- );
759
-
760
- try {
761
- const attemptAbort = createAttemptAbortSignal(request.signal);
762
- const attemptOptions = omitUnsupportedCortexAiModelOptions(
763
- {
764
- abortSignal: attemptAbort.signal,
765
- maxOutputTokens: 2000,
766
- messages: parsedRequest.data.messages,
767
- maxRetries: 0,
768
- stopWhen: stepCountIs(6),
769
- system: systemPrompt,
770
- temperature: 0.1,
771
- tools,
772
- } as Record<string, unknown>,
773
- {
774
- modelId,
775
- modelSelection: routingPolicy.modelSelection,
776
- }
777
- );
778
- const result = streamText({
779
- ...attemptOptions,
780
- model: client.model(modelId),
781
- } as Parameters<typeof streamText>[0]);
782
-
783
- try {
784
- for await (const rawPart of result.fullStream) {
785
- const part = rawPart as CortexAgentStreamPart;
786
-
787
- if (part.type === 'text-delta' && part.text) {
788
- textBuffer = `${textBuffer}${part.text}`;
789
- sawRawToolCallLeak = sawRawToolCallLeak || looksLikeRawToolCallLeak(textBuffer);
790
- sawRateLimitText = sawRateLimitText || looksLikeRateLimitText(textBuffer);
791
- }
792
-
793
- if (part.type === 'tool-call' && part.toolName) {
794
- hasToolCall = true;
795
- lastToolName = part.toolName;
796
- controller.enqueue(
797
- encodeStreamEvent({
798
- input: part.input,
799
- toolCallId: getToolCallId(part),
800
- toolName: part.toolName,
801
- type: 'tool-call',
802
- })
803
- );
804
- }
805
-
806
- if (part.type === 'tool-result' && part.toolName) {
807
- hasSuccessfulToolResult = true;
808
- lastToolName = part.toolName;
809
- lastToolOutput = part.output;
810
- controller.enqueue(
811
- encodeStreamEvent({
812
- output: part.output,
813
- toolCallId: getToolCallId(part),
814
- toolName: part.toolName,
815
- type: 'tool-result',
816
- })
817
- );
818
- }
819
-
820
- if (part.type === 'tool-error') {
821
- hasToolCall = true;
822
- controller.enqueue(
823
- encodeStreamEvent({
824
- message: serializeStreamError(part.error),
825
- toolCallId: getToolCallId(part),
826
- toolName: part.toolName,
827
- type: 'tool-error',
828
- })
829
- );
830
- }
831
-
832
- if (part.type === 'error') {
833
- throw part.error || new Error('Cortex AI stream failed.');
834
- }
835
- }
836
- } finally {
837
- attemptAbort.cleanup();
838
- }
839
-
840
- if ((sawRawToolCallLeak || sawRateLimitText) && !hasToolCall) {
841
- lastError = getRetryableStreamError(
842
- null,
843
- sawRawToolCallLeak,
844
- sawRateLimitText
845
- );
846
-
847
- if (index < modelIds.length - 1) {
848
- continue;
849
- }
850
-
851
- throw lastError;
852
- }
853
-
854
- if ((sawRawToolCallLeak || sawRateLimitText) && !hasSuccessfulToolResult) {
855
- lastError = getRetryableStreamError(
856
- null,
857
- sawRawToolCallLeak,
858
- sawRateLimitText
859
- );
860
- throw lastError;
861
- }
862
-
863
- if ((sawRawToolCallLeak || sawRateLimitText) && hasSuccessfulToolResult) {
864
- controller.enqueue(
865
- encodeStreamEvent({
866
- text: getToolCompletionMessage(lastToolName, lastToolOutput),
867
- type: 'text-delta',
868
- })
869
- );
870
- } else if (textBuffer.trim() && !looksLikeRawToolCallLeak(textBuffer)) {
871
- controller.enqueue(
872
- encodeStreamEvent({
873
- text: hasSuccessfulToolResult
874
- ? completeToolBackedText(textBuffer, lastToolName, lastToolOutput)
875
- : textBuffer,
876
- type: 'text-delta',
877
- })
878
- );
879
- } else if (hasSuccessfulToolResult) {
880
- controller.enqueue(
881
- encodeStreamEvent({
882
- text: getToolCompletionMessage(lastToolName, lastToolOutput),
883
- type: 'text-delta',
884
- })
885
- );
886
- }
887
-
888
- controller.enqueue(encodeStreamEvent({ type: 'finish' }));
889
- completed = true;
890
- break;
891
- } catch (error) {
892
- lastError = getRetryableStreamError(error, sawRawToolCallLeak, sawRateLimitText);
893
-
894
- if (hasSuccessfulToolResult) {
895
- controller.enqueue(
896
- encodeStreamEvent({
897
- text: completeToolBackedText(textBuffer, lastToolName, lastToolOutput),
898
- type: 'text-delta',
899
- })
900
- );
901
- controller.enqueue(encodeStreamEvent({ type: 'finish' }));
902
- completed = true;
903
- break;
904
- }
905
-
906
- if (
907
- !hasToolCall &&
908
- isOpenRouterRateLimitError(lastError) &&
909
- index < modelIds.length - 1
910
- ) {
911
- continue;
912
- }
913
-
914
- if (
915
- !hasToolCall &&
916
- (sawRawToolCallLeak || sawRateLimitText) &&
917
- index < modelIds.length - 1
918
- ) {
919
- continue;
920
- }
921
-
922
- controller.enqueue(
923
- encodeStreamEvent({
924
- message: serializeStreamError(lastError),
925
- type: 'error',
926
- })
927
- );
928
- controller.enqueue(encodeStreamEvent({ type: 'finish' }));
929
- completed = true;
930
- break;
931
- }
932
-
933
- if (completed) {
934
- break;
935
- }
936
- }
937
-
938
- if (!completed) {
939
- controller.enqueue(
940
- encodeStreamEvent({
941
- message: serializeStreamError(lastError),
942
- type: 'error',
943
- })
944
- );
945
- controller.enqueue(encodeStreamEvent({ type: 'finish' }));
946
- }
947
-
948
- controller.close();
949
- },
950
- });
951
-
952
- return new Response(stream, {
953
- headers: {
954
- 'Cache-Control': 'no-cache, no-transform',
955
- Connection: 'keep-alive',
956
- 'Content-Type': 'text/event-stream; charset=utf-8',
957
- 'X-Accel-Buffering': 'no',
958
- },
959
- });
960
- } catch (error) {
961
- console.error('[Cortex AI] Global agent failed:', error);
962
- return jsonError(error instanceof Error ? error.message : 'Global agent failed.', 500);
963
- }
964
- }
1
+ import { stepCountIs, streamText } from 'ai';
2
+ import { NextResponse } from 'next/server';
3
+
4
+ import {
5
+ createClient,
6
+ getServiceRoleSupabaseClient,
7
+ verifyPackageOnline,
8
+ } from '@nextblock-cms/db/server';
9
+ import {
10
+ buildCortexAiRoutingPolicy,
11
+ buildVisibleContactIntroActionPlan,
12
+ cortexAiPageContextSchema,
13
+ createCortexGlobalAgentTools,
14
+ createCortexAiOpenRouterClient,
15
+ executeCmsActionPlan,
16
+ executeCreateCmsPage,
17
+ executeCreateCmsPost,
18
+ executeCreateCmsProduct,
19
+ executeDatabaseActionPlan,
20
+ executeDatabaseMutation,
21
+ executeDeleteCmsItem,
22
+ executeDeleteCustomBlock,
23
+ executeInsertContentBlock,
24
+ executeUpdateContentBlock,
25
+ executeUpdateCmsItemField,
26
+ executeUpdateCurrentCmsFields,
27
+ executeUpdateFooter,
28
+ executeUpdateNavigationBar,
29
+ executeUpdateSectionColumnBlock,
30
+ isOpenRouterRateLimitError,
31
+ omitUnsupportedCortexAiModelOptions,
32
+ safeParseCortexAiModelSelection,
33
+ summarizeCortexAiRoutingError,
34
+ type CortexAiPageContext,
35
+ z,
36
+ } from '@nextblock-cms/cortex';
37
+ import { validateBlockContent } from '../../../../lib/blocks/blockRegistry';
38
+
39
+ export const dynamic = 'force-dynamic';
40
+
41
+ const GLOBAL_AGENT_MODEL_ATTEMPT_TIMEOUT_MS = 30000;
42
+
43
+ const globalAgentMessageSchema = z.strictObject({
44
+ content: z.string().min(1).max(8000),
45
+ role: z.enum(['system', 'user', 'assistant']),
46
+ });
47
+
48
+ const confirmedToolCallSchema = z.strictObject({
49
+ confirmationPhrase: z.string().min(1).max(500),
50
+ input: z.unknown(),
51
+ toolName: z.enum([
52
+ 'create_cms_page',
53
+ 'create_cms_post',
54
+ 'create_cms_product',
55
+ 'delete_cms_item',
56
+ 'delete_custom_block',
57
+ 'execute_database_action_plan',
58
+ 'execute_database_mutation',
59
+ 'execute_cms_action_plan',
60
+ 'insert_content_block',
61
+ 'update_cms_item_field',
62
+ 'update_content_block',
63
+ 'update_current_cms_fields',
64
+ 'update_footer',
65
+ 'update_navigation_bar',
66
+ 'update_section_column_block',
67
+ ]),
68
+ });
69
+
70
+ const globalAgentRequestSchema = z.strictObject({
71
+ confirmedToolCall: confirmedToolCallSchema.optional(),
72
+ messages: z.array(globalAgentMessageSchema).min(1).max(40),
73
+ pageContext: cortexAiPageContextSchema.nullable().optional(),
74
+ });
75
+
76
+ const GLOBAL_AGENT_SYSTEM_PROMPT = [
77
+ 'You are NextBlock Cortex AI, the global dashboard agent for a block-based CMS.',
78
+ 'Operate as a Planner, Executor, and Evaluator.',
79
+ 'Plan the smallest safe change, execute only through typed tools, evaluate the tool result, then answer concisely.',
80
+ 'If the user asks for multiple CMS mutations in one prompt, such as creating a page and adding a navigation link, use execute_cms_action_plan so the user sees one combined confirmation and one Confirm button. The action plan must include every requested mutation; do not fall back to confirming only the first task.',
81
+ 'For execute_cms_action_plan, actions must be JSON objects, for example { "tool": "create_cms_page", "input": { "title": "Contact Us" } }, never strings like create_cms_page(...).',
82
+ 'Every mutating tool is confirmed two-step. First call the right tool with the exact normalized payload. If the tool returns requiresConfirmation, do not say the work is done; say "Please confirm for me to complete:" and summarize the requested change. Do not print confirmationPhrase unless the user explicitly asks for the raw phrase.',
83
+ 'When the latest user message is an exact confirmation phrase, call the same mutating tool again with the same payload so the tool can execute. Only report success after the tool result has mutationExecuted=true.',
84
+ 'Use create_cms_page, create_cms_post, and create_cms_product for CMS creation. New pages, posts, and products default to draft unless the user explicitly asks for a public/active status.',
85
+ 'Use update_cms_item_field for one precise field update at a time, such as price, stock, sale_price, title, slug, status, or SEO metadata. Interpret "public" as published for pages/posts and active for products.',
86
+ 'Use prepare_delete_cms_item or delete_cms_item for delete requests. Do not delete anything until the user sends the exact confirmation phrase returned by the tool.',
87
+ 'If a user asks for sale start/end dates or scheduled specials, explain that scheduled specials are not supported by the current schema; you may offer to set or clear sale_price only.',
88
+ 'Use update_navigation_bar for public header navigation changes. For requests that ask to add a header link, use update_navigation_bar with mode "append" unless the user clearly asks to replace the whole menu.',
89
+ 'For requests that ask to rename or change one existing navigation link, use update_navigation_bar with mode "update" and identify the existing item with match.label or match.url. Never use mode "replace" for a one-link rename.',
90
+ 'Use mode "replace" only when the user explicitly asks to rebuild or replace the entire navigation menu and you provide the full menu.',
91
+ 'Use update_footer for public footer links or copyright settings.',
92
+ 'For custom/reusable block types (a "custom block", "block type", "widget", or a request to design a new kind of block such as a product card, testimonial, or feature card), use the global custom block tools, which do NOT need an open page/post/product: create_custom_block to build a new block from a description, update_custom_block to edit one by slug, delete_custom_block to remove one, and list_custom_blocks to find a slug. Never tell the user to open a page first for these; never use insert_content_block or page-aware tools to define a new block type. create_custom_block and update_custom_block run immediately; after success, tell the user the block was added to their Custom Blocks library and can now be dropped onto any page.',
93
+ 'Distinguish adding content to the current page (page-aware tools, needs page context) from defining a reusable block type (custom block tools, global, no page context).',
94
+ 'When editing a CMS page, post, product, or block, use page-aware tools only. Use read_current_cms_item before updating content unless the user provided exact field/block data.',
95
+ 'Use update_current_cms_fields for current page/post/product metadata and product description_json. Use update_content_block for top-level page/post blocks. Use update_section_column_block for nested blocks inside section or hero blocks.',
96
+ 'When the user asks to add a visible title, heading, intro, description, or copy above/below a form or other block, use insert_content_block with a text or heading block. Do not treat visible page copy as meta_title, meta_description, or SEO metadata unless the user explicitly says SEO/meta.',
97
+ 'For requests like "add a title and description to both pages and incite them to contact us", use execute_cms_action_plan with one insert_content_block action per translated page, usually a text block before the form with localized heading and paragraph HTML.',
98
+ 'Do not use update_section_column_block to change an existing nested block from one block type to another. That tool edits only the content of the existing nested block type.',
99
+ 'When the user asks to add a new nested block inside a hero or section, such as adding a button to a hero, use update_content_block on the parent hero/section block. Prefer content.append_block, for example { block_type: "button", content: { text: "Contact Us", url: "/contact" } }, so the tool preserves existing column_blocks and layout fields.',
100
+ 'When a user names a language, pass that language name or its locale code in languageCode; examples: French maps to fr, English maps to en.',
101
+ 'For follow-up requests like "also add it in French", use the prior requested item and apply it to the named language. For page/post/product translations, pass the current translationGroupId into the creation tool so the backend links the language versions.',
102
+ 'Use search_documentation before answering implementation or CMS usage questions that require factual project context.',
103
+ 'Use describe_database_schema, read_database_records, execute_database_mutation, and execute_database_action_plan for direct database tasks that are not covered by a more specific CMS tool. Use typed CRUD tools only; never ask for or invent raw SQL.',
104
+ 'For direct database mutations, always return the confirmation preview first. Never claim a database mutation is complete until the confirmed tool result has mutationExecuted=true. Do not edit auth users, profiles, user addresses, password fields, API keys, tokens, secrets, private keys, credentials, or the cortex_ai_openrouter_api_key site setting.',
105
+ 'Use fetch_ecommerce_stats for quantitative questions about revenue, products, or order counts. This tool is read-only.',
106
+ 'For order-status questions like "how many pending orders" or "how many trial orders", use the tool result report.matchingOrderStatus or report.orderStatusCounts, and use all_time unless the user names a specific time period.',
107
+ 'Never invent database fields, raw SQL, markdown content, or unsupported tool arguments.',
108
+ ].join(' ');
109
+
110
+ type CortexAgentStreamEvent =
111
+ | {
112
+ credentialSource: string;
113
+ modelId: string;
114
+ type: 'meta';
115
+ }
116
+ | {
117
+ text: string;
118
+ type: 'text-delta';
119
+ }
120
+ | {
121
+ input?: unknown;
122
+ toolCallId?: string;
123
+ toolName: string;
124
+ type: 'tool-call';
125
+ }
126
+ | {
127
+ output?: unknown;
128
+ toolCallId?: string;
129
+ toolName: string;
130
+ type: 'tool-result';
131
+ }
132
+ | {
133
+ message: string;
134
+ toolCallId?: string;
135
+ toolName?: string;
136
+ type: 'tool-error';
137
+ }
138
+ | {
139
+ message: string;
140
+ type: 'error';
141
+ }
142
+ | {
143
+ type: 'finish';
144
+ };
145
+
146
+ type CortexAgentStreamPart = {
147
+ error?: unknown;
148
+ id?: string;
149
+ input?: unknown;
150
+ output?: unknown;
151
+ text?: string;
152
+ toolCallId?: string;
153
+ toolName?: string;
154
+ type: string;
155
+ };
156
+
157
+ async function requireAdminAccess() {
158
+ const supabase = createClient();
159
+ const {
160
+ data: { user },
161
+ error: userError,
162
+ } = await supabase.auth.getUser();
163
+
164
+ if (userError || !user) {
165
+ return null;
166
+ }
167
+
168
+ const { data: profile, error: profileError } = await supabase
169
+ .from('profiles')
170
+ .select('role')
171
+ .eq('id', user.id)
172
+ .single();
173
+
174
+ return !profileError && profile?.role === 'ADMIN' ? { userId: user.id } : null;
175
+ }
176
+
177
+ function jsonError(message: string, status: number) {
178
+ return NextResponse.json({ error: message }, { status });
179
+ }
180
+
181
+ const encoder = new TextEncoder();
182
+
183
+ function encodeStreamEvent(event: CortexAgentStreamEvent) {
184
+ return encoder.encode(`data: ${JSON.stringify(event)}\n\n`);
185
+ }
186
+
187
+ function formatPageContextForPrompt(pageContext: CortexAiPageContext | null | undefined) {
188
+ if (!pageContext) {
189
+ return 'No current CMS edit context was supplied. Ask the user to open the relevant edit screen before using page-aware editing tools.';
190
+ }
191
+
192
+ return [
193
+ `Current CMS edit context: contentType=${pageContext.contentType}`,
194
+ `entityId=${String(pageContext.entityId)}`,
195
+ pageContext.slug ? `slug=${pageContext.slug}` : null,
196
+ pageContext.title ? `title=${pageContext.title}` : null,
197
+ pageContext.translationGroupId ? `translationGroupId=${pageContext.translationGroupId}` : null,
198
+ pageContext.languageId ? `languageId=${pageContext.languageId}` : null,
199
+ pageContext.currentEditor?.field ? `currentField=${pageContext.currentEditor.field}` : null,
200
+ pageContext.currentEditor?.blockId ? `currentBlockId=${pageContext.currentEditor.blockId}` : null,
201
+ pageContext.currentEditor?.blockType ? `currentBlockType=${pageContext.currentEditor.blockType}` : null,
202
+ ]
203
+ .filter(Boolean)
204
+ .join(', ');
205
+ }
206
+
207
+ function buildGlobalAgentSystemPrompt(pageContext: CortexAiPageContext | null | undefined) {
208
+ return [
209
+ GLOBAL_AGENT_SYSTEM_PROMPT,
210
+ formatPageContextForPrompt(pageContext),
211
+ 'When the user says "this page", "this post", "this product", "this field", or "this block", interpret that through the supplied current CMS edit context.',
212
+ 'Do not update content outside the supplied current CMS context.',
213
+ ].join(' ');
214
+ }
215
+
216
+ function serializeStreamError(error: unknown) {
217
+ return summarizeCortexAiRoutingError(
218
+ error,
219
+ error instanceof Error ? error.message : 'Cortex AI global agent failed.'
220
+ );
221
+ }
222
+
223
+ function getToolCallId(part: CortexAgentStreamPart) {
224
+ return part.toolCallId || part.id;
225
+ }
226
+
227
+ function looksLikeRawToolCallLeak(value: string) {
228
+ const normalized = value.toLowerCase();
229
+
230
+ return (
231
+ normalized.includes('<toolcall') ||
232
+ normalized.includes('</toolcall') ||
233
+ normalized.includes('"arguments"') ||
234
+ normalized.includes('"update_navigation_bar"') ||
235
+ normalized.includes('"update_footer"') ||
236
+ normalized.includes('"search_documentation"') ||
237
+ normalized.includes('"read_current_cms_item"') ||
238
+ normalized.includes('"update_current_cms_fields"') ||
239
+ normalized.includes('"update_cms_item_field"') ||
240
+ normalized.includes('"update_content_block"') ||
241
+ normalized.includes('"insert_content_block"') ||
242
+ normalized.includes('"update_section_column_block"') ||
243
+ normalized.includes('"create_cms_page"') ||
244
+ normalized.includes('"create_cms_post"') ||
245
+ normalized.includes('"create_cms_product"') ||
246
+ normalized.includes('"execute_cms_action_plan"') ||
247
+ normalized.includes('"prepare_delete_cms_item"') ||
248
+ normalized.includes('"delete_cms_item"')
249
+ );
250
+ }
251
+
252
+ function looksLikeRateLimitText(value: string) {
253
+ const normalized = value.toLowerCase();
254
+
255
+ return (
256
+ normalized.includes('rate limit exceeded') ||
257
+ normalized.includes('free-models-per-day') ||
258
+ normalized.includes('too many requests')
259
+ );
260
+ }
261
+
262
+ function readNumberField(value: unknown, key: string) {
263
+ if (!value || typeof value !== 'object' || !(key in value)) {
264
+ return null;
265
+ }
266
+
267
+ const parsed = Number((value as Record<string, unknown>)[key]);
268
+ return Number.isFinite(parsed) ? parsed : null;
269
+ }
270
+
271
+ function isRecord(value: unknown): value is Record<string, unknown> {
272
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
273
+ }
274
+
275
+ function readStringField(value: unknown, key: string) {
276
+ if (!isRecord(value)) {
277
+ return null;
278
+ }
279
+
280
+ const fieldValue = value[key];
281
+
282
+ return typeof fieldValue === 'string' ? fieldValue : null;
283
+ }
284
+
285
+ function pluralize(count: number, singular: string, plural = `${singular}s`) {
286
+ return `${count} ${count === 1 ? singular : plural}`;
287
+ }
288
+
289
+ function getConfirmationSummary(toolName?: string, output?: unknown) {
290
+ if (!isRecord(output) || !isRecord(output.preview)) {
291
+ return 'Complete the requested CMS change.';
292
+ }
293
+
294
+ const preview = output.preview;
295
+ const summary = readStringField(preview, 'summary');
296
+
297
+ if (summary) {
298
+ const actionSummaries = Array.isArray(preview.actionSummaries)
299
+ ? preview.actionSummaries.filter((item): item is string => typeof item === 'string')
300
+ : [];
301
+
302
+ return actionSummaries.length > 0
303
+ ? `${summary}\n\n${actionSummaries.map((item, index) => `${index + 1}. ${item}`).join('\n')}`
304
+ : summary;
305
+ }
306
+
307
+ const title = readStringField(preview, 'title');
308
+ const slug = readStringField(preview, 'slug');
309
+ const status = readStringField(preview, 'status');
310
+ const contentType = readStringField(preview, 'contentType');
311
+ const field = readStringField(preview, 'field');
312
+ const mode = readStringField(preview, 'mode');
313
+ const languageCode = readStringField(preview, 'languageCode');
314
+ const blockCount = readNumberField(preview, 'blockCount');
315
+ const itemCount = readNumberField(preview, 'itemCount');
316
+ const affectedCount = readNumberField(preview, 'affectedCount');
317
+
318
+ if (toolName === 'create_cms_page' || toolName === 'create_cms_post') {
319
+ return `Create ${status || 'draft'} ${toolName === 'create_cms_page' ? 'page' : 'post'} "${title || slug || 'Untitled'}"${slug ? ` at slug "${slug}"` : ''}${blockCount !== null ? ` with ${pluralize(blockCount, 'content block')}` : ''}.`;
320
+ }
321
+
322
+ if (toolName === 'create_cms_product') {
323
+ return `Create ${status || 'draft'} product "${title || slug || 'Untitled'}"${slug ? ` at slug "${slug}"` : ''}.`;
324
+ }
325
+
326
+ if (toolName === 'update_cms_item_field') {
327
+ return `Update ${field || 'one field'} on the ${contentType || 'CMS item'} "${title || slug || 'selected item'}".`;
328
+ }
329
+
330
+ if (toolName === 'update_navigation_bar') {
331
+ return `${mode === 'append' ? 'Add' : mode === 'update' ? 'Update' : 'Replace'} ${itemCount !== null ? pluralize(itemCount, 'navigation item') : 'navigation items'} in the ${languageCode || 'selected'} header navigation.`;
332
+ }
333
+
334
+ if (toolName === 'update_footer') {
335
+ const linkCount = readNumberField(preview, 'linkCount');
336
+ return `Update the ${languageCode || 'selected'} footer${linkCount !== null ? ` with ${pluralize(linkCount, 'link')}` : ''}.`;
337
+ }
338
+
339
+ if (toolName === 'update_content_block') {
340
+ return `Update the selected ${readStringField(preview, 'blockType') || 'content'} block.`;
341
+ }
342
+
343
+ if (toolName === 'insert_content_block') {
344
+ return `Insert ${readStringField(preview, 'blockType') || 'content'} block on the ${contentType || 'CMS item'} "${title || slug || 'selected item'}".`;
345
+ }
346
+
347
+ if (toolName === 'update_section_column_block') {
348
+ return `Update the selected nested ${readStringField(preview, 'nestedBlockType') || 'section'} block.`;
349
+ }
350
+
351
+ if (toolName === 'delete_cms_item' || toolName === 'prepare_delete_cms_item') {
352
+ return `Delete ${affectedCount !== null ? pluralize(affectedCount, contentType || 'CMS item') : `the selected ${contentType || 'CMS item'}`}${title || slug ? ` for "${title || slug}"` : ''}.`;
353
+ }
354
+
355
+ return 'Complete the requested CMS change.';
356
+ }
357
+
358
+ function getToolCompletionMessage(toolName?: string, output?: unknown) {
359
+ if (isRecord(output)) {
360
+ if (output.requiresConfirmation === true) {
361
+ return `Please confirm for me to complete:\n\n${getConfirmationSummary(toolName, output)}`;
362
+ }
363
+
364
+ if (output.unsupported === true || output.success === false) {
365
+ return readStringField(output, 'message') || 'I could not complete that request.';
366
+ }
367
+ }
368
+
369
+ const mutationExecuted = isRecord(output) && output.mutationExecuted === true;
370
+
371
+ if (toolName === 'update_navigation_bar') {
372
+ const insertedCount = readNumberField(output, 'insertedCount');
373
+ const skippedCount = readNumberField(output, 'skippedCount');
374
+
375
+ if ((insertedCount ?? 0) === 0 && (skippedCount ?? 0) > 0) {
376
+ return 'That navigation link already exists, so I left the header unchanged.';
377
+ }
378
+
379
+ return 'Done. I updated the navigation bar.';
380
+ }
381
+
382
+ if (toolName === 'update_footer') {
383
+ return 'Done. I updated the footer.';
384
+ }
385
+
386
+ if (toolName === 'search_documentation') {
387
+ return 'I searched the documentation, but the model was interrupted before it could finish a summary.';
388
+ }
389
+
390
+ if (toolName === 'read_current_cms_item') {
391
+ return 'I read the current CMS item, but the model was interrupted before it could finish a summary.';
392
+ }
393
+
394
+ if (toolName === 'fetch_ecommerce_stats') {
395
+ return 'I fetched the latest ecommerce statistics for you.';
396
+ }
397
+
398
+ if (toolName === 'describe_database_schema') {
399
+ return 'I inspected the available database schema.';
400
+ }
401
+
402
+ if (toolName === 'read_database_records') {
403
+ return 'I read the requested database records.';
404
+ }
405
+
406
+ if (toolName === 'execute_database_mutation') {
407
+ const affectedCount = readNumberField(output, 'affectedCount');
408
+ const table = readStringField(output, 'table');
409
+ const auditLogged = isRecord(output) && output.auditLogged === true;
410
+
411
+ return mutationExecuted
412
+ ? `Done. I updated ${affectedCount ? pluralize(affectedCount, 'database row') : 'the database'}${table ? ` in ${table}` : ''}.${auditLogged ? ' Audit logged.' : ''}`
413
+ : 'I prepared the database mutation.';
414
+ }
415
+
416
+ if (toolName === 'execute_database_action_plan') {
417
+ const actionCount = readNumberField(output, 'actionCount');
418
+ const auditLogged = isRecord(output) && output.auditLogged === true;
419
+
420
+ return mutationExecuted
421
+ ? `Done. I completed ${actionCount ? pluralize(actionCount, 'database action') : 'the database action plan'}.${auditLogged ? ' Audit logged.' : ''}`
422
+ : 'I prepared the database action plan.';
423
+ }
424
+
425
+ if (toolName === 'update_current_cms_fields') {
426
+ return 'Done. I updated the current CMS fields.';
427
+ }
428
+
429
+ if (toolName === 'update_cms_item_field') {
430
+ return mutationExecuted
431
+ ? 'Done. I updated that CMS field.'
432
+ : 'I prepared the CMS field update.';
433
+ }
434
+
435
+ if (toolName === 'update_content_block') {
436
+ return 'Done. I updated the current content block.';
437
+ }
438
+
439
+ if (toolName === 'insert_content_block') {
440
+ return 'Done. I inserted the content block.';
441
+ }
442
+
443
+ if (toolName === 'update_section_column_block') {
444
+ return 'Done. I updated the nested section block.';
445
+ }
446
+
447
+ if (toolName === 'create_cms_page') {
448
+ return mutationExecuted ? 'Done. I created the page.' : 'I prepared the page creation.';
449
+ }
450
+
451
+ if (toolName === 'create_cms_post') {
452
+ return mutationExecuted ? 'Done. I created the post.' : 'I prepared the post creation.';
453
+ }
454
+
455
+ if (toolName === 'create_cms_product') {
456
+ return mutationExecuted ? 'Done. I created the product.' : 'I prepared the product creation.';
457
+ }
458
+
459
+ if (toolName === 'prepare_delete_cms_item') {
460
+ return getToolCompletionMessage('delete_cms_item', output);
461
+ }
462
+
463
+ if (toolName === 'delete_cms_item') {
464
+ return mutationExecuted ? 'Done. I deleted that CMS item.' : 'I prepared the delete request.';
465
+ }
466
+
467
+ if (toolName === 'execute_cms_action_plan') {
468
+ const actionCount = readNumberField(output, 'actionCount');
469
+
470
+ return mutationExecuted
471
+ ? `Done. I completed ${actionCount ? pluralize(actionCount, 'CMS action') : 'the CMS action plan'}.`
472
+ : 'I prepared the CMS action plan.';
473
+ }
474
+
475
+ return 'Done. I completed the requested update.';
476
+ }
477
+
478
+ function completeToolBackedText(text: string, toolName?: string, output?: unknown) {
479
+ const trimmedText = text.trim();
480
+
481
+ if (!isRecord(output)) {
482
+ return trimmedText || getToolCompletionMessage(toolName, output);
483
+ }
484
+
485
+ if (output.requiresConfirmation === true) {
486
+ return getToolCompletionMessage(toolName, output);
487
+ }
488
+
489
+ if ((output.unsupported === true || output.success === false) && !trimmedText) {
490
+ return getToolCompletionMessage(toolName, output);
491
+ }
492
+
493
+ return trimmedText || getToolCompletionMessage(toolName, output);
494
+ }
495
+
496
+ async function executeConfirmedToolCall(params: {
497
+ context: Parameters<typeof createCortexGlobalAgentTools>[0];
498
+ input: unknown;
499
+ toolName: z.infer<typeof confirmedToolCallSchema>['toolName'];
500
+ }) {
501
+ switch (params.toolName) {
502
+ case 'create_cms_page':
503
+ return executeCreateCmsPage(params.input as any, params.context);
504
+ case 'create_cms_post':
505
+ return executeCreateCmsPost(params.input as any, params.context);
506
+ case 'create_cms_product':
507
+ return executeCreateCmsProduct(params.input as any, params.context);
508
+ case 'delete_cms_item':
509
+ return executeDeleteCmsItem(params.input as any, params.context);
510
+ case 'delete_custom_block':
511
+ return executeDeleteCustomBlock(params.input as any, params.context);
512
+ case 'execute_database_action_plan':
513
+ return executeDatabaseActionPlan(params.input as any, params.context);
514
+ case 'execute_database_mutation':
515
+ return executeDatabaseMutation(params.input as any, params.context);
516
+ case 'execute_cms_action_plan':
517
+ return executeCmsActionPlan(params.input as any, params.context);
518
+ case 'update_cms_item_field':
519
+ return executeUpdateCmsItemField(params.input as any, params.context);
520
+ case 'update_content_block':
521
+ return executeUpdateContentBlock(params.input as any, params.context);
522
+ case 'insert_content_block':
523
+ return executeInsertContentBlock(params.input as any, params.context);
524
+ case 'update_current_cms_fields':
525
+ return executeUpdateCurrentCmsFields(params.input as any, params.context);
526
+ case 'update_footer':
527
+ return executeUpdateFooter(params.input as any, params.context);
528
+ case 'update_navigation_bar':
529
+ return executeUpdateNavigationBar(params.input as any, params.context);
530
+ case 'update_section_column_block':
531
+ return executeUpdateSectionColumnBlock(params.input as any, params.context);
532
+ }
533
+ }
534
+
535
+ function createConfirmedToolCallStream(params: {
536
+ context: Parameters<typeof createCortexGlobalAgentTools>[0];
537
+ input: unknown;
538
+ toolName: z.infer<typeof confirmedToolCallSchema>['toolName'];
539
+ }) {
540
+ return new ReadableStream({
541
+ async start(controller) {
542
+ const toolCallId = `confirmed-${Date.now()}`;
543
+
544
+ controller.enqueue(
545
+ encodeStreamEvent({
546
+ input: params.input,
547
+ toolCallId,
548
+ toolName: params.toolName,
549
+ type: 'tool-call',
550
+ })
551
+ );
552
+
553
+ try {
554
+ const output = await executeConfirmedToolCall(params);
555
+
556
+ controller.enqueue(
557
+ encodeStreamEvent({
558
+ output,
559
+ toolCallId,
560
+ toolName: params.toolName,
561
+ type: 'tool-result',
562
+ })
563
+ );
564
+ controller.enqueue(
565
+ encodeStreamEvent({
566
+ text: getToolCompletionMessage(params.toolName, output),
567
+ type: 'text-delta',
568
+ })
569
+ );
570
+ } catch (error) {
571
+ controller.enqueue(
572
+ encodeStreamEvent({
573
+ message: serializeStreamError(error),
574
+ toolCallId,
575
+ toolName: params.toolName,
576
+ type: 'tool-error',
577
+ })
578
+ );
579
+ controller.enqueue(
580
+ encodeStreamEvent({
581
+ message: serializeStreamError(error),
582
+ type: 'error',
583
+ })
584
+ );
585
+ }
586
+
587
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
588
+ controller.close();
589
+ },
590
+ });
591
+ }
592
+
593
+ function getRetryableStreamError(
594
+ error: unknown,
595
+ sawRawToolCallLeak: boolean,
596
+ sawRateLimitText: boolean
597
+ ) {
598
+ if (sawRawToolCallLeak) {
599
+ return new Error('OpenRouter returned an invalid raw tool-call payload.');
600
+ }
601
+
602
+ if (sawRateLimitText) {
603
+ return new Error('OpenRouter rate limit exceeded.');
604
+ }
605
+
606
+ return error;
607
+ }
608
+
609
+ function createAttemptAbortSignal(requestSignal: AbortSignal) {
610
+ const controller = new AbortController();
611
+ const timeoutId = setTimeout(() => {
612
+ controller.abort(new Error('Cortex AI response timed out. Please try again.'));
613
+ }, GLOBAL_AGENT_MODEL_ATTEMPT_TIMEOUT_MS);
614
+ const abortFromRequest = () => controller.abort(requestSignal.reason);
615
+
616
+ if (requestSignal.aborted) {
617
+ abortFromRequest();
618
+ } else {
619
+ requestSignal.addEventListener('abort', abortFromRequest, { once: true });
620
+ }
621
+
622
+ return {
623
+ cleanup: () => {
624
+ clearTimeout(timeoutId);
625
+ requestSignal.removeEventListener('abort', abortFromRequest);
626
+ },
627
+ signal: controller.signal,
628
+ };
629
+ }
630
+
631
+ export async function POST(request: Request) {
632
+ try {
633
+ const adminAccess = await requireAdminAccess();
634
+
635
+ if (!adminAccess) {
636
+ return jsonError('You do not have permission to use the global Cortex AI agent.', 403);
637
+ }
638
+
639
+ const isCortexAiActive = await verifyPackageOnline('cortex-ai');
640
+
641
+ if (!isCortexAiActive) {
642
+ return jsonError('NextBlock Cortex AI is not active for this workspace.', 403);
643
+ }
644
+
645
+ const body = await request.json().catch(() => null);
646
+ const parsedRequest = globalAgentRequestSchema.safeParse(body);
647
+
648
+ if (!parsedRequest.success) {
649
+ return jsonError('Invalid Cortex AI global-agent request.', 400);
650
+ }
651
+
652
+ const pageContext = parsedRequest.data.pageContext ?? null;
653
+ const latestUserMessage =
654
+ [...parsedRequest.data.messages].reverse().find((message) => message.role === 'user')
655
+ ?.content ?? '';
656
+
657
+ if (parsedRequest.data.confirmedToolCall) {
658
+ const confirmedToolCall = parsedRequest.data.confirmedToolCall;
659
+ const stream = createConfirmedToolCallStream({
660
+ context: {
661
+ actorUserId: adminAccess.userId,
662
+ latestUserMessage: confirmedToolCall.confirmationPhrase,
663
+ pageContext,
664
+ supabase: getServiceRoleSupabaseClient(),
665
+ validateBlockContent,
666
+ },
667
+ input: confirmedToolCall.input,
668
+ toolName: confirmedToolCall.toolName,
669
+ });
670
+
671
+ return new Response(stream, {
672
+ headers: {
673
+ 'Cache-Control': 'no-cache, no-transform',
674
+ Connection: 'keep-alive',
675
+ 'Content-Type': 'text/event-stream; charset=utf-8',
676
+ 'X-Accel-Buffering': 'no',
677
+ },
678
+ });
679
+ }
680
+
681
+ const directActionPlan = buildVisibleContactIntroActionPlan(latestUserMessage);
682
+
683
+ if (directActionPlan) {
684
+ const stream = createConfirmedToolCallStream({
685
+ context: {
686
+ actorUserId: adminAccess.userId,
687
+ latestUserMessage,
688
+ pageContext,
689
+ supabase: getServiceRoleSupabaseClient(),
690
+ validateBlockContent,
691
+ },
692
+ input: directActionPlan,
693
+ toolName: 'execute_cms_action_plan',
694
+ });
695
+
696
+ return new Response(stream, {
697
+ headers: {
698
+ 'Cache-Control': 'no-cache, no-transform',
699
+ Connection: 'keep-alive',
700
+ 'Content-Type': 'text/event-stream; charset=utf-8',
701
+ 'X-Accel-Buffering': 'no',
702
+ },
703
+ });
704
+ }
705
+
706
+ const sandboxKey = process.env.NEXT_PUBLIC_IS_SANDBOX === 'true' ? request.headers.get('x-sandbox-openrouter-key') : null;
707
+ const sandboxModelRaw = process.env.NEXT_PUBLIC_IS_SANDBOX === 'true' ? request.headers.get('x-sandbox-openrouter-model') : null;
708
+
709
+ let modelSelection = null;
710
+ if (sandboxModelRaw) {
711
+ try {
712
+ modelSelection = safeParseCortexAiModelSelection(JSON.parse(sandboxModelRaw));
713
+ } catch {
714
+ // Ignore parse errors from headers
715
+ }
716
+ }
717
+
718
+ const client = await createCortexAiOpenRouterClient({
719
+ apiKey: sandboxKey || undefined,
720
+ modelSelection: sandboxKey && modelSelection ? modelSelection : undefined,
721
+ });
722
+ const routingPolicy = buildCortexAiRoutingPolicy({
723
+ credentialSource: client.credentialSource,
724
+ selectedModel: client.modelSelection,
725
+ });
726
+ const modelIds = routingPolicy.modelIds;
727
+ const tools = createCortexGlobalAgentTools({
728
+ actorUserId: adminAccess.userId,
729
+ cortexAiApiKey: sandboxKey,
730
+ cortexAiModelSelection: sandboxKey && modelSelection ? modelSelection : undefined,
731
+ latestUserMessage,
732
+ pageContext,
733
+ supabase: getServiceRoleSupabaseClient(),
734
+ validateBlockContent,
735
+ });
736
+ const systemPrompt = buildGlobalAgentSystemPrompt(pageContext);
737
+
738
+ const stream = new ReadableStream({
739
+ async start(controller) {
740
+ let completed = false;
741
+ let lastError: unknown = null;
742
+
743
+ for (const [index, modelId] of modelIds.entries()) {
744
+ let textBuffer = '';
745
+ let sawRawToolCallLeak = false;
746
+ let sawRateLimitText = false;
747
+ let hasToolCall = false;
748
+ let hasSuccessfulToolResult = false;
749
+ let lastToolName: string | undefined;
750
+ let lastToolOutput: unknown;
751
+
752
+ controller.enqueue(
753
+ encodeStreamEvent({
754
+ credentialSource: client.credentialSource,
755
+ modelId,
756
+ type: 'meta',
757
+ })
758
+ );
759
+
760
+ try {
761
+ const attemptAbort = createAttemptAbortSignal(request.signal);
762
+ const attemptOptions = omitUnsupportedCortexAiModelOptions(
763
+ {
764
+ abortSignal: attemptAbort.signal,
765
+ maxOutputTokens: 2000,
766
+ messages: parsedRequest.data.messages,
767
+ maxRetries: 0,
768
+ stopWhen: stepCountIs(6),
769
+ system: systemPrompt,
770
+ temperature: 0.1,
771
+ tools,
772
+ } as Record<string, unknown>,
773
+ {
774
+ modelId,
775
+ modelSelection: routingPolicy.modelSelection,
776
+ }
777
+ );
778
+ const result = streamText({
779
+ ...attemptOptions,
780
+ model: client.model(modelId),
781
+ } as Parameters<typeof streamText>[0]);
782
+
783
+ try {
784
+ for await (const rawPart of result.fullStream) {
785
+ const part = rawPart as CortexAgentStreamPart;
786
+
787
+ if (part.type === 'text-delta' && part.text) {
788
+ textBuffer = `${textBuffer}${part.text}`;
789
+ sawRawToolCallLeak = sawRawToolCallLeak || looksLikeRawToolCallLeak(textBuffer);
790
+ sawRateLimitText = sawRateLimitText || looksLikeRateLimitText(textBuffer);
791
+ }
792
+
793
+ if (part.type === 'tool-call' && part.toolName) {
794
+ hasToolCall = true;
795
+ lastToolName = part.toolName;
796
+ controller.enqueue(
797
+ encodeStreamEvent({
798
+ input: part.input,
799
+ toolCallId: getToolCallId(part),
800
+ toolName: part.toolName,
801
+ type: 'tool-call',
802
+ })
803
+ );
804
+ }
805
+
806
+ if (part.type === 'tool-result' && part.toolName) {
807
+ hasSuccessfulToolResult = true;
808
+ lastToolName = part.toolName;
809
+ lastToolOutput = part.output;
810
+ controller.enqueue(
811
+ encodeStreamEvent({
812
+ output: part.output,
813
+ toolCallId: getToolCallId(part),
814
+ toolName: part.toolName,
815
+ type: 'tool-result',
816
+ })
817
+ );
818
+ }
819
+
820
+ if (part.type === 'tool-error') {
821
+ hasToolCall = true;
822
+ controller.enqueue(
823
+ encodeStreamEvent({
824
+ message: serializeStreamError(part.error),
825
+ toolCallId: getToolCallId(part),
826
+ toolName: part.toolName,
827
+ type: 'tool-error',
828
+ })
829
+ );
830
+ }
831
+
832
+ if (part.type === 'error') {
833
+ throw part.error || new Error('Cortex AI stream failed.');
834
+ }
835
+ }
836
+ } finally {
837
+ attemptAbort.cleanup();
838
+ }
839
+
840
+ if ((sawRawToolCallLeak || sawRateLimitText) && !hasToolCall) {
841
+ lastError = getRetryableStreamError(
842
+ null,
843
+ sawRawToolCallLeak,
844
+ sawRateLimitText
845
+ );
846
+
847
+ if (index < modelIds.length - 1) {
848
+ continue;
849
+ }
850
+
851
+ throw lastError;
852
+ }
853
+
854
+ if ((sawRawToolCallLeak || sawRateLimitText) && !hasSuccessfulToolResult) {
855
+ lastError = getRetryableStreamError(
856
+ null,
857
+ sawRawToolCallLeak,
858
+ sawRateLimitText
859
+ );
860
+ throw lastError;
861
+ }
862
+
863
+ if ((sawRawToolCallLeak || sawRateLimitText) && hasSuccessfulToolResult) {
864
+ controller.enqueue(
865
+ encodeStreamEvent({
866
+ text: getToolCompletionMessage(lastToolName, lastToolOutput),
867
+ type: 'text-delta',
868
+ })
869
+ );
870
+ } else if (textBuffer.trim() && !looksLikeRawToolCallLeak(textBuffer)) {
871
+ controller.enqueue(
872
+ encodeStreamEvent({
873
+ text: hasSuccessfulToolResult
874
+ ? completeToolBackedText(textBuffer, lastToolName, lastToolOutput)
875
+ : textBuffer,
876
+ type: 'text-delta',
877
+ })
878
+ );
879
+ } else if (hasSuccessfulToolResult) {
880
+ controller.enqueue(
881
+ encodeStreamEvent({
882
+ text: getToolCompletionMessage(lastToolName, lastToolOutput),
883
+ type: 'text-delta',
884
+ })
885
+ );
886
+ }
887
+
888
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
889
+ completed = true;
890
+ break;
891
+ } catch (error) {
892
+ lastError = getRetryableStreamError(error, sawRawToolCallLeak, sawRateLimitText);
893
+
894
+ if (hasSuccessfulToolResult) {
895
+ controller.enqueue(
896
+ encodeStreamEvent({
897
+ text: completeToolBackedText(textBuffer, lastToolName, lastToolOutput),
898
+ type: 'text-delta',
899
+ })
900
+ );
901
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
902
+ completed = true;
903
+ break;
904
+ }
905
+
906
+ if (
907
+ !hasToolCall &&
908
+ isOpenRouterRateLimitError(lastError) &&
909
+ index < modelIds.length - 1
910
+ ) {
911
+ continue;
912
+ }
913
+
914
+ if (
915
+ !hasToolCall &&
916
+ (sawRawToolCallLeak || sawRateLimitText) &&
917
+ index < modelIds.length - 1
918
+ ) {
919
+ continue;
920
+ }
921
+
922
+ controller.enqueue(
923
+ encodeStreamEvent({
924
+ message: serializeStreamError(lastError),
925
+ type: 'error',
926
+ })
927
+ );
928
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
929
+ completed = true;
930
+ break;
931
+ }
932
+
933
+ if (completed) {
934
+ break;
935
+ }
936
+ }
937
+
938
+ if (!completed) {
939
+ controller.enqueue(
940
+ encodeStreamEvent({
941
+ message: serializeStreamError(lastError),
942
+ type: 'error',
943
+ })
944
+ );
945
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
946
+ }
947
+
948
+ controller.close();
949
+ },
950
+ });
951
+
952
+ return new Response(stream, {
953
+ headers: {
954
+ 'Cache-Control': 'no-cache, no-transform',
955
+ Connection: 'keep-alive',
956
+ 'Content-Type': 'text/event-stream; charset=utf-8',
957
+ 'X-Accel-Buffering': 'no',
958
+ },
959
+ });
960
+ } catch (error) {
961
+ console.error('[Cortex AI] Global agent failed:', error);
962
+ return jsonError(error instanceof Error ? error.message : 'Global agent failed.', 500);
963
+ }
964
+ }