create-nextblock 0.2.78 → 0.8.0

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 (413) hide show
  1. package/bin/create-nextblock.js +740 -459
  2. package/package.json +1 -2
  3. package/scripts/sync-template.js +18 -1
  4. package/templates/nextblock-template/.browserslistrc +11 -0
  5. package/templates/nextblock-template/.swcrc +30 -30
  6. package/templates/nextblock-template/README.md +23 -114
  7. package/templates/nextblock-template/app/(auth-pages)/post-sign-in/page.tsx +27 -28
  8. package/templates/nextblock-template/app/(auth-pages)/sign-in/page.tsx +50 -25
  9. package/templates/nextblock-template/app/(auth-pages)/sign-up/page.tsx +111 -56
  10. package/templates/nextblock-template/app/(auth-pages)/two-factor/actions.ts +91 -0
  11. package/templates/nextblock-template/app/(auth-pages)/two-factor/components/TwoFactorForm.tsx +118 -0
  12. package/templates/nextblock-template/app/(auth-pages)/two-factor/page.tsx +51 -0
  13. package/templates/nextblock-template/app/.well-known/ucp/route.ts +16 -0
  14. package/templates/nextblock-template/app/[slug]/PageClientContent.tsx +48 -28
  15. package/templates/nextblock-template/app/[slug]/page.tsx +63 -6
  16. package/templates/nextblock-template/app/[slug]/page.utils.ts +374 -157
  17. package/templates/nextblock-template/app/[slug]/pageClientActions.ts +7 -0
  18. package/templates/nextblock-template/app/actions/consent.ts +57 -0
  19. package/templates/nextblock-template/app/actions/formActions.ts +130 -11
  20. package/templates/nextblock-template/app/actions/languageActions.ts +31 -30
  21. package/templates/nextblock-template/app/actions/package-actions.ts +183 -0
  22. package/templates/nextblock-template/app/actions/postActions.ts +146 -48
  23. package/templates/nextblock-template/app/actions/twoFactorEmail.ts +21 -0
  24. package/templates/nextblock-template/app/actions/visualEditingActions.test.ts +179 -0
  25. package/templates/nextblock-template/app/actions/visualEditingActions.ts +345 -0
  26. package/templates/nextblock-template/app/actions.ts +67 -12
  27. package/templates/nextblock-template/app/api/ai/cortex/build-widget/route.ts +153 -0
  28. package/templates/nextblock-template/app/api/ai/generate-blocks/route.ts +96 -0
  29. package/templates/nextblock-template/app/api/ai/global-agent/route.ts +965 -0
  30. package/templates/nextblock-template/app/api/checkout/freemius/sync/route.ts +29 -0
  31. package/templates/nextblock-template/app/api/checkout/route.ts +146 -0
  32. package/templates/nextblock-template/app/api/cms/full-backup/export/route.ts +33 -0
  33. package/templates/nextblock-template/app/api/cms/full-backup/restore/route.ts +63 -0
  34. package/templates/nextblock-template/app/api/cron/reset-sandbox/route.ts +3413 -17
  35. package/templates/nextblock-template/app/api/cron/reset-sandbox/sandboxResetSql.ts +7830 -0
  36. package/templates/nextblock-template/app/api/cron/sync-currencies/route.ts +35 -0
  37. package/templates/nextblock-template/app/api/custom-blocks/db-relations/route.ts +92 -0
  38. package/templates/nextblock-template/app/api/custom-blocks/editor-definitions/route.ts +43 -0
  39. package/templates/nextblock-template/app/api/draft/disable/route.ts +25 -0
  40. package/templates/nextblock-template/app/api/draft/route.ts +93 -0
  41. package/templates/nextblock-template/app/api/draft/start/route.ts +77 -0
  42. package/templates/nextblock-template/app/api/media/library/route.ts +65 -0
  43. package/templates/nextblock-template/app/api/media/r2-presigned/route.ts +53 -0
  44. package/templates/nextblock-template/app/api/media/record/route.ts +160 -0
  45. package/templates/nextblock-template/app/api/search/route.ts +43 -0
  46. package/templates/nextblock-template/app/api/visual-editing/block-draft/route.ts +47 -0
  47. package/templates/nextblock-template/app/api/visual-editing/product-draft/route.ts +47 -0
  48. package/templates/nextblock-template/app/api/webhooks/freemius/route.ts +34 -0
  49. package/templates/nextblock-template/app/api/webhooks/stripe/route.ts +27 -0
  50. package/templates/nextblock-template/app/article/[slug]/PostClientContent.tsx +392 -128
  51. package/templates/nextblock-template/app/article/[slug]/page.tsx +179 -127
  52. package/templates/nextblock-template/app/article/[slug]/page.utils.ts +262 -77
  53. package/templates/nextblock-template/app/auth/callback/route.ts +31 -58
  54. package/templates/nextblock-template/app/cart/page.tsx +7 -0
  55. package/templates/nextblock-template/app/checkout/UcpCartHydrator.tsx +20 -0
  56. package/templates/nextblock-template/app/checkout/page.tsx +52 -0
  57. package/templates/nextblock-template/app/checkout/success/actions.ts +136 -0
  58. package/templates/nextblock-template/app/checkout/success/page.tsx +186 -0
  59. package/templates/nextblock-template/app/cms/CmsClientLayout.tsx +163 -33
  60. package/templates/nextblock-template/app/cms/blocks/actions.ts +424 -235
  61. package/templates/nextblock-template/app/cms/blocks/components/BackgroundSelector.tsx +212 -151
  62. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorArea.tsx +41 -20
  63. package/templates/nextblock-template/app/cms/blocks/components/BlockEditorModal.tsx +152 -19
  64. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeCard.tsx +25 -17
  65. package/templates/nextblock-template/app/cms/blocks/components/BlockTypeSelector.tsx +200 -18
  66. package/templates/nextblock-template/app/cms/blocks/components/ColumnEditor.tsx +33 -16
  67. package/templates/nextblock-template/app/cms/blocks/components/CustomBlockEditorPreview.tsx +160 -0
  68. package/templates/nextblock-template/app/cms/blocks/components/EditableBlock.tsx +37 -18
  69. package/templates/nextblock-template/app/cms/blocks/components/MediaLibraryModal.tsx +149 -67
  70. package/templates/nextblock-template/app/cms/blocks/components/SectionConfigPanel.tsx +108 -31
  71. package/templates/nextblock-template/app/cms/blocks/editors/DynamicCustomBlockEditor.tsx +167 -0
  72. package/templates/nextblock-template/app/cms/blocks/editors/FeaturedProductBlockEditor.tsx +31 -0
  73. package/templates/nextblock-template/app/cms/blocks/editors/FormBlockEditor.tsx +2 -2
  74. package/templates/nextblock-template/app/cms/blocks/editors/HeadingBlockEditor.tsx +1 -1
  75. package/templates/nextblock-template/app/cms/blocks/editors/ImageBlockEditor.tsx +29 -29
  76. package/templates/nextblock-template/app/cms/blocks/editors/PostsGridBlockEditor.tsx +14 -18
  77. package/templates/nextblock-template/app/cms/blocks/editors/ProductGridBlockEditor.tsx +41 -0
  78. package/templates/nextblock-template/app/cms/blocks/editors/SectionBlockEditor.tsx +318 -118
  79. package/templates/nextblock-template/app/cms/blocks/editors/TextBlockEditor.tsx +98 -21
  80. package/templates/nextblock-template/app/cms/blocks/editors/VideoEmbedBlockEditor.tsx +1 -1
  81. package/templates/nextblock-template/app/cms/components/ContentLanguageSwitcher.tsx +27 -9
  82. package/templates/nextblock-template/app/cms/components/CopyContentFromLanguage.tsx +1 -1
  83. package/templates/nextblock-template/app/cms/components/CortexAiActiveContext.tsx +23 -0
  84. package/templates/nextblock-template/app/cms/components/CortexAiPageContext.tsx +58 -0
  85. package/templates/nextblock-template/app/cms/components/CortexGlobalAgentChat.tsx +1507 -0
  86. package/templates/nextblock-template/app/cms/components/DraftStatusActions.tsx +145 -0
  87. package/templates/nextblock-template/app/cms/components/FeatureImageField.tsx +244 -0
  88. package/templates/nextblock-template/app/cms/components/FeedbackModal.tsx +38 -24
  89. package/templates/nextblock-template/app/cms/coupons/[id]/edit/page.tsx +16 -0
  90. package/templates/nextblock-template/app/cms/coupons/page.tsx +16 -0
  91. package/templates/nextblock-template/app/cms/custom-blocks/[id]/edit/page.tsx +66 -0
  92. package/templates/nextblock-template/app/cms/custom-blocks/actions.ts +519 -0
  93. package/templates/nextblock-template/app/cms/custom-blocks/components/BlockComposer.tsx +1522 -0
  94. package/templates/nextblock-template/app/cms/custom-blocks/components/BlocksLibraryTransferControls.tsx +256 -0
  95. package/templates/nextblock-template/app/cms/custom-blocks/components/DBRelationSelect.tsx +384 -0
  96. package/templates/nextblock-template/app/cms/custom-blocks/components/ImageR2Picker.tsx +221 -0
  97. package/templates/nextblock-template/app/cms/custom-blocks/new/page.tsx +12 -0
  98. package/templates/nextblock-template/app/cms/custom-blocks/page.tsx +438 -0
  99. package/templates/nextblock-template/app/cms/dashboard/actions.ts +228 -98
  100. package/templates/nextblock-template/app/cms/dashboard/components/DashboardComponents.tsx +200 -0
  101. package/templates/nextblock-template/app/cms/dashboard/page.tsx +182 -154
  102. package/templates/nextblock-template/app/cms/import-export/ContentTransferControls.tsx +391 -0
  103. package/templates/nextblock-template/app/cms/import-export/actions.ts +226 -0
  104. package/templates/nextblock-template/app/cms/layout.tsx +29 -10
  105. package/templates/nextblock-template/app/cms/media/UploadFolderContext.tsx +22 -22
  106. package/templates/nextblock-template/app/cms/media/actions.ts +45 -124
  107. package/templates/nextblock-template/app/cms/media/components/DeleteMediaButtonClient.tsx +1 -1
  108. package/templates/nextblock-template/app/cms/media/components/MediaEditForm.tsx +26 -26
  109. package/templates/nextblock-template/app/cms/media/components/MediaGridClient.tsx +69 -64
  110. package/templates/nextblock-template/app/cms/media/components/MediaPickerDialog.tsx +227 -158
  111. package/templates/nextblock-template/app/cms/media/components/MediaUploadForm.tsx +101 -89
  112. package/templates/nextblock-template/app/cms/media/page.tsx +1 -1
  113. package/templates/nextblock-template/app/cms/navigation/components/NavigationItemForm.tsx +2 -2
  114. package/templates/nextblock-template/app/cms/orders/[id]/MarkPaidButton.tsx +44 -0
  115. package/templates/nextblock-template/app/cms/orders/[id]/page.tsx +16 -0
  116. package/templates/nextblock-template/app/cms/orders/actions.ts +201 -0
  117. package/templates/nextblock-template/app/cms/orders/page.tsx +20 -0
  118. package/templates/nextblock-template/app/cms/orders/types.ts +20 -0
  119. package/templates/nextblock-template/app/cms/pages/[id]/edit/EditPageClient.tsx +156 -121
  120. package/templates/nextblock-template/app/cms/pages/[id]/edit/page.tsx +79 -26
  121. package/templates/nextblock-template/app/cms/pages/actions.ts +54 -38
  122. package/templates/nextblock-template/app/cms/pages/components/DeletePageButtonClient.tsx +1 -1
  123. package/templates/nextblock-template/app/cms/pages/components/PageForm.tsx +267 -116
  124. package/templates/nextblock-template/app/cms/pages/page.tsx +25 -18
  125. package/templates/nextblock-template/app/cms/payments/page.tsx +16 -0
  126. package/templates/nextblock-template/app/cms/posts/[id]/edit/page.tsx +132 -90
  127. package/templates/nextblock-template/app/cms/posts/actions.ts +71 -72
  128. package/templates/nextblock-template/app/cms/posts/components/DeletePostButtonClient.tsx +1 -1
  129. package/templates/nextblock-template/app/cms/posts/components/PostForm.tsx +256 -245
  130. package/templates/nextblock-template/app/cms/posts/new/page.tsx +1 -1
  131. package/templates/nextblock-template/app/cms/posts/page.tsx +20 -13
  132. package/templates/nextblock-template/app/cms/products/ClientNotionEditor.tsx +16 -0
  133. package/templates/nextblock-template/app/cms/products/ProductFormClientShell.tsx +56 -0
  134. package/templates/nextblock-template/app/cms/products/[id]/edit/page.tsx +292 -0
  135. package/templates/nextblock-template/app/cms/products/attributes/page.tsx +12 -0
  136. package/templates/nextblock-template/app/cms/products/categories/page.tsx +12 -0
  137. package/templates/nextblock-template/app/cms/products/inventory/page.tsx +13 -0
  138. package/templates/nextblock-template/app/cms/products/new/page.tsx +143 -0
  139. package/templates/nextblock-template/app/cms/products/page.tsx +42 -0
  140. package/templates/nextblock-template/app/cms/products/productFormData.ts +133 -0
  141. package/templates/nextblock-template/app/cms/products/settings/page.tsx +5 -0
  142. package/templates/nextblock-template/app/cms/promotions/PromotionsWorkspace.tsx +456 -0
  143. package/templates/nextblock-template/app/cms/promotions/actions.ts +115 -0
  144. package/templates/nextblock-template/app/cms/promotions/page.tsx +31 -0
  145. package/templates/nextblock-template/app/cms/revisions/RevisionHistoryButton.tsx +2 -2
  146. package/templates/nextblock-template/app/cms/revisions/actions.ts +285 -285
  147. package/templates/nextblock-template/app/cms/revisions/service.ts +19 -16
  148. package/templates/nextblock-template/app/cms/revisions/utils.ts +8 -3
  149. package/templates/nextblock-template/app/cms/settings/backup-restore/BackupRestoreWorkspace.tsx +1004 -0
  150. package/templates/nextblock-template/app/cms/settings/backup-restore/page.tsx +29 -0
  151. package/templates/nextblock-template/app/cms/settings/bot-protection/actions.ts +93 -0
  152. package/templates/nextblock-template/app/cms/settings/bot-protection/components/BotProtectionForm.tsx +129 -0
  153. package/templates/nextblock-template/app/cms/settings/bot-protection/page.tsx +24 -0
  154. package/templates/nextblock-template/app/cms/settings/copyright/actions.ts +1 -1
  155. package/templates/nextblock-template/app/cms/settings/copyright/components/CopyrightForm.tsx +2 -2
  156. package/templates/nextblock-template/app/cms/settings/copyright/page.tsx +1 -1
  157. package/templates/nextblock-template/app/cms/settings/cortex-ai/SandboxCortexAiSettingsClient.tsx +496 -0
  158. package/templates/nextblock-template/app/cms/settings/cortex-ai/StoredCortexAiSettingsClient.tsx +410 -0
  159. package/templates/nextblock-template/app/cms/settings/cortex-ai/actions.ts +248 -0
  160. package/templates/nextblock-template/app/cms/settings/cortex-ai/page.tsx +80 -0
  161. package/templates/nextblock-template/app/cms/settings/currencies/actions.ts +331 -0
  162. package/templates/nextblock-template/app/cms/settings/currencies/page.tsx +494 -0
  163. package/templates/nextblock-template/app/cms/settings/extra-translations/ExtraTranslationsWorkspace.tsx +767 -0
  164. package/templates/nextblock-template/app/cms/settings/extra-translations/actions.ts +203 -44
  165. package/templates/nextblock-template/app/cms/settings/extra-translations/page.tsx +93 -242
  166. package/templates/nextblock-template/app/cms/settings/global-css/actions.ts +65 -0
  167. package/templates/nextblock-template/app/cms/settings/global-css/components/GlobalCssForm.tsx +46 -0
  168. package/templates/nextblock-template/app/cms/settings/global-css/page.tsx +24 -0
  169. package/templates/nextblock-template/app/cms/settings/languages/components/DeleteLanguageButton.tsx +1 -1
  170. package/templates/nextblock-template/app/cms/settings/languages/components/LanguageForm.tsx +2 -2
  171. package/templates/nextblock-template/app/cms/settings/languages/page.tsx +1 -1
  172. package/templates/nextblock-template/app/cms/settings/logos/[id]/edit/page.tsx +7 -7
  173. package/templates/nextblock-template/app/cms/settings/logos/actions.ts +82 -6
  174. package/templates/nextblock-template/app/cms/settings/logos/components/BrandingSettingsForm.tsx +339 -0
  175. package/templates/nextblock-template/app/cms/settings/logos/components/DeleteLogoButton.tsx +21 -18
  176. package/templates/nextblock-template/app/cms/settings/logos/components/LogoForm.tsx +20 -16
  177. package/templates/nextblock-template/app/cms/settings/logos/components/SiteSeoSettingsForm.tsx +133 -0
  178. package/templates/nextblock-template/app/cms/settings/logos/new/page.tsx +8 -8
  179. package/templates/nextblock-template/app/cms/settings/logos/page.tsx +120 -82
  180. package/templates/nextblock-template/app/cms/settings/logos/types.ts +8 -8
  181. package/templates/nextblock-template/app/cms/settings/packages/activation-form.tsx +84 -0
  182. package/templates/nextblock-template/app/cms/settings/packages/package-card.tsx +122 -0
  183. package/templates/nextblock-template/app/cms/settings/packages/page.tsx +49 -0
  184. package/templates/nextblock-template/app/cms/settings/privacy/actions.ts +53 -0
  185. package/templates/nextblock-template/app/cms/settings/privacy/components/PrivacyForm.tsx +196 -0
  186. package/templates/nextblock-template/app/cms/settings/privacy/page.tsx +26 -0
  187. package/templates/nextblock-template/app/cms/settings/security/actions.ts +251 -0
  188. package/templates/nextblock-template/app/cms/settings/security/components/SecurityPanel.tsx +453 -0
  189. package/templates/nextblock-template/app/cms/settings/security/page.tsx +13 -0
  190. package/templates/nextblock-template/app/cms/settings/taxes/page.tsx +21 -0
  191. package/templates/nextblock-template/app/cms/shipping/page.tsx +20 -0
  192. package/templates/nextblock-template/app/cms/users/[id]/edit/page.tsx +28 -23
  193. package/templates/nextblock-template/app/cms/users/actions.ts +105 -40
  194. package/templates/nextblock-template/app/cms/users/components/DeleteUserButton.tsx +1 -1
  195. package/templates/nextblock-template/app/cms/users/components/UserForm.tsx +65 -152
  196. package/templates/nextblock-template/app/cms/users/page.tsx +15 -10
  197. package/templates/nextblock-template/app/globals.css +9 -0
  198. package/templates/nextblock-template/app/layout.tsx +372 -120
  199. package/templates/nextblock-template/app/lib/seo.test.ts +52 -0
  200. package/templates/nextblock-template/app/lib/seo.ts +279 -0
  201. package/templates/nextblock-template/app/lib/site-settings.ts +87 -0
  202. package/templates/nextblock-template/app/lib/sitemap-utils.ts +224 -39
  203. package/templates/nextblock-template/app/lib/ucp/protocol.ts +190 -0
  204. package/templates/nextblock-template/app/lib/ucp/server.test.ts +56 -0
  205. package/templates/nextblock-template/app/lib/ucp/server.ts +1914 -0
  206. package/templates/nextblock-template/app/page.tsx +165 -73
  207. package/templates/nextblock-template/app/product/[slug]/page.tsx +433 -0
  208. package/templates/nextblock-template/app/profile/ProfileAccountSidebar.tsx +73 -0
  209. package/templates/nextblock-template/app/profile/ProfilePageHeader.tsx +16 -0
  210. package/templates/nextblock-template/app/profile/ProfilePageMissingState.tsx +9 -0
  211. package/templates/nextblock-template/app/profile/account-data.ts +37 -0
  212. package/templates/nextblock-template/app/profile/account-links.ts +22 -0
  213. package/templates/nextblock-template/app/profile/account-types.ts +11 -0
  214. package/templates/nextblock-template/app/profile/orders/CustomerOrdersPageClient.tsx +124 -0
  215. package/templates/nextblock-template/app/profile/orders/[id]/CustomerOrderDetailPageClient.tsx +79 -0
  216. package/templates/nextblock-template/app/profile/orders/[id]/page.tsx +32 -0
  217. package/templates/nextblock-template/app/profile/orders/page.tsx +19 -0
  218. package/templates/nextblock-template/app/profile/page.tsx +51 -0
  219. package/templates/nextblock-template/app/profile/password/PasswordSettingsPageClient.tsx +128 -0
  220. package/templates/nextblock-template/app/profile/password/actions.ts +59 -0
  221. package/templates/nextblock-template/app/profile/password/page.tsx +27 -0
  222. package/templates/nextblock-template/app/providers.tsx +55 -17
  223. package/templates/nextblock-template/app/robots.txt/route.ts +11 -1
  224. package/templates/nextblock-template/app/sitemap.ts +128 -0
  225. package/templates/nextblock-template/app/ucp/v1/carts/[id]/cancel/route.ts +38 -0
  226. package/templates/nextblock-template/app/ucp/v1/carts/[id]/route.ts +68 -0
  227. package/templates/nextblock-template/app/ucp/v1/carts/route.ts +35 -0
  228. package/templates/nextblock-template/app/ucp/v1/catalog/lookup/route.ts +35 -0
  229. package/templates/nextblock-template/app/ucp/v1/catalog/product/route.ts +35 -0
  230. package/templates/nextblock-template/app/ucp/v1/catalog/search/route.ts +34 -0
  231. package/templates/nextblock-template/components/AppShell.tsx +154 -0
  232. package/templates/nextblock-template/components/BlockRenderer.tsx +210 -64
  233. package/templates/nextblock-template/components/CartDrawerLoader.tsx +7 -0
  234. package/templates/nextblock-template/components/CartTranslator.tsx +210 -0
  235. package/templates/nextblock-template/components/CurrentContentSetter.tsx +25 -0
  236. package/templates/nextblock-template/components/DeferredCartDrawer.tsx +23 -0
  237. package/templates/nextblock-template/components/DeferredCartTranslator.tsx +51 -0
  238. package/templates/nextblock-template/components/DeferredGlobalSearch.tsx +68 -0
  239. package/templates/nextblock-template/components/DeferredGoogleTagManager.tsx +70 -0
  240. package/templates/nextblock-template/components/DeferredSpeedInsights.tsx +69 -0
  241. package/templates/nextblock-template/components/FeatureImageHero.tsx +47 -0
  242. package/templates/nextblock-template/components/GitHubLoginButton.tsx +36 -0
  243. package/templates/nextblock-template/components/GlobalSearch.tsx +557 -0
  244. package/templates/nextblock-template/components/Header.tsx +49 -41
  245. package/templates/nextblock-template/components/LanguageSwitcher.tsx +55 -32
  246. package/templates/nextblock-template/components/ResponsiveNav.tsx +138 -43
  247. package/templates/nextblock-template/components/blocks/PostCardSkeleton.tsx +12 -8
  248. package/templates/nextblock-template/components/blocks/PostsGridBlock.tsx +12 -55
  249. package/templates/nextblock-template/components/blocks/PostsGridClient.tsx +42 -37
  250. package/templates/nextblock-template/components/blocks/TestimonialBlock.tsx +6 -2
  251. package/templates/nextblock-template/components/blocks/ecommerceRendererLoaders.ts +23 -0
  252. package/templates/nextblock-template/components/blocks/publicRendererLoaders.ts +25 -0
  253. package/templates/nextblock-template/components/blocks/renderers/ButtonBlockRenderer.tsx +92 -84
  254. package/templates/nextblock-template/components/blocks/renderers/CartBlockRenderer.tsx +17 -0
  255. package/templates/nextblock-template/components/blocks/renderers/CheckoutBlockRenderer.tsx +19 -0
  256. package/templates/nextblock-template/components/blocks/renderers/ClientTextBlockRenderer.tsx +262 -8
  257. package/templates/nextblock-template/components/blocks/renderers/FeaturedProductBlockRenderer.tsx +22 -0
  258. package/templates/nextblock-template/components/blocks/renderers/FormBlockRenderer.tsx +320 -37
  259. package/templates/nextblock-template/components/blocks/renderers/HeadingBlockRenderer.tsx +11 -8
  260. package/templates/nextblock-template/components/blocks/renderers/ImageBlockRenderer.tsx +12 -3
  261. package/templates/nextblock-template/components/blocks/renderers/PostsGridBlockRenderer.tsx +18 -13
  262. package/templates/nextblock-template/components/blocks/renderers/ProductDetailsBlockRenderer.tsx +90 -0
  263. package/templates/nextblock-template/components/blocks/renderers/ProductGridBlockRenderer.tsx +31 -0
  264. package/templates/nextblock-template/components/blocks/renderers/SectionBlockRenderer.tsx +424 -55
  265. package/templates/nextblock-template/components/blocks/renderers/SectionSlider.tsx +137 -0
  266. package/templates/nextblock-template/components/blocks/renderers/TestimonialBlockRenderer.tsx +57 -0
  267. package/templates/nextblock-template/components/blocks/renderers/TextBlockRenderer.tsx +37 -22
  268. package/templates/nextblock-template/components/blocks/renderers/VideoEmbedBlockRenderer.tsx +23 -15
  269. package/templates/nextblock-template/components/blocks/renderers/inline/AlertWidgetRenderer.tsx +1 -3
  270. package/templates/nextblock-template/components/blocks/renderers/inline/CtaWidgetRenderer.tsx +1 -3
  271. package/templates/nextblock-template/components/blocks/types.ts +7 -6
  272. package/templates/nextblock-template/components/env-var-warning.tsx +3 -3
  273. package/templates/nextblock-template/components/form-message.tsx +32 -26
  274. package/templates/nextblock-template/components/header-auth.tsx +69 -17
  275. package/templates/nextblock-template/components/privacy/ConsentBanner.tsx +127 -0
  276. package/templates/nextblock-template/components/privacy/ConsentGatedAnalytics.tsx +59 -0
  277. package/templates/nextblock-template/components/renderers/CachedDynamicLayoutEngine.tsx +28 -0
  278. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.test.tsx +166 -0
  279. package/templates/nextblock-template/components/renderers/DynamicLayoutEngine.tsx +464 -0
  280. package/templates/nextblock-template/components/theme-switcher.tsx +8 -8
  281. package/templates/nextblock-template/components/visual-editing/DeferredVisualEditing.tsx +21 -0
  282. package/templates/nextblock-template/components/visual-editing/NextblockVisualEditing.tsx +1172 -0
  283. package/templates/nextblock-template/context/AuthContext.tsx +23 -90
  284. package/templates/nextblock-template/context/CurrentContentContext.tsx +10 -4
  285. package/templates/nextblock-template/context/LanguageContext.tsx +16 -16
  286. package/templates/nextblock-template/context/language-rest-client.ts +31 -0
  287. package/templates/nextblock-template/docs/01-PROJECT-OVERVIEW.md +94 -0
  288. package/templates/nextblock-template/docs/02-ECOMMERCE-CAPABILITIES.md +364 -0
  289. package/templates/nextblock-template/docs/03-CMS-AND-EDITOR.md +202 -0
  290. package/templates/nextblock-template/docs/04-DATABASE-AND-AUTH.md +252 -0
  291. package/templates/nextblock-template/docs/05-DEVELOPER-GUIDE.md +238 -0
  292. package/templates/nextblock-template/docs/06-CLI-AND-SCAFFOLDING.md +125 -0
  293. package/templates/nextblock-template/docs/07-BLOCK-SDK-AND-EXTENSIBILITY.md +146 -0
  294. package/templates/nextblock-template/docs/08-NEXTBLOCK-CORTEX-AI-ARCHITECTURE.md +1319 -0
  295. package/templates/nextblock-template/docs/09-LIVE-DRAFT-MODE.md +104 -0
  296. package/templates/nextblock-template/docs/10-CUSTOM-BLOCKS.md +222 -0
  297. package/templates/nextblock-template/docs/README.md +34 -0
  298. package/templates/nextblock-template/docs/TECHNICAL_SPECIFICATION.md +12507 -0
  299. package/templates/nextblock-template/hooks/use-hotkeys.ts +21 -14
  300. package/templates/nextblock-template/hooks/useGlobalSearch.ts +101 -0
  301. package/templates/nextblock-template/index.d.ts +2 -0
  302. package/templates/nextblock-template/lib/ai-block-generation.ts +339 -0
  303. package/templates/nextblock-template/lib/ai-client.ts +247 -0
  304. package/templates/nextblock-template/lib/ai-config.ts +81 -0
  305. package/templates/nextblock-template/lib/ai-cortex-widget-builder.ts +125 -0
  306. package/templates/nextblock-template/lib/ai-global-agent-custom-block-tools.ts +363 -0
  307. package/templates/nextblock-template/lib/ai-global-agent-db-tools.test.ts +405 -0
  308. package/templates/nextblock-template/lib/ai-global-agent-db-tools.ts +1228 -0
  309. package/templates/nextblock-template/lib/ai-global-agent-ecommerce.ts +5 -0
  310. package/templates/nextblock-template/lib/ai-global-agent-tools-stats.test.ts +223 -0
  311. package/templates/nextblock-template/lib/ai-global-agent-tools.test.ts +2183 -0
  312. package/templates/nextblock-template/lib/ai-global-agent-tools.ts +4807 -0
  313. package/templates/nextblock-template/lib/ai-key-crypto.test.ts +70 -0
  314. package/templates/nextblock-template/lib/ai-key-crypto.ts +132 -0
  315. package/templates/nextblock-template/lib/ai-model-catalog.test.ts +49 -0
  316. package/templates/nextblock-template/lib/ai-model-catalog.ts +41 -0
  317. package/templates/nextblock-template/lib/ai-model-registry.test.ts +231 -0
  318. package/templates/nextblock-template/lib/ai-model-registry.ts +522 -0
  319. package/templates/nextblock-template/lib/auth/cookies.ts +47 -0
  320. package/templates/nextblock-template/lib/auth/crypto.ts +42 -0
  321. package/templates/nextblock-template/lib/auth/trustedDevices.ts +92 -0
  322. package/templates/nextblock-template/lib/auth/twoFactor.ts +167 -0
  323. package/templates/nextblock-template/lib/auth-redirects.ts +46 -0
  324. package/templates/nextblock-template/lib/blocks/FeaturedProductBlock.tsx +94 -0
  325. package/templates/nextblock-template/lib/blocks/ProductGridBlock.tsx +137 -0
  326. package/templates/nextblock-template/lib/blocks/README.md +13 -670
  327. package/templates/nextblock-template/lib/blocks/blockRegistry.ts +138 -56
  328. package/templates/nextblock-template/lib/blocks/blockTypes.ts +18 -0
  329. package/templates/nextblock-template/lib/blocks/ecommerce-block-schemas.ts +31 -0
  330. package/templates/nextblock-template/lib/cms-transfer/csv.test.ts +77 -0
  331. package/templates/nextblock-template/lib/cms-transfer/csv.ts +399 -0
  332. package/templates/nextblock-template/lib/cms-transfer/server.ts +2243 -0
  333. package/templates/nextblock-template/lib/cms-transfer/types.ts +145 -0
  334. package/templates/nextblock-template/lib/cortex-widget-registry.test.ts +199 -0
  335. package/templates/nextblock-template/lib/cortex-widget-registry.ts +88 -0
  336. package/templates/nextblock-template/lib/cortex-widget-schema.test.tsx +237 -0
  337. package/templates/nextblock-template/lib/cortex-widget-schema.ts +393 -0
  338. package/templates/nextblock-template/lib/custom-block-definitions.ts +87 -0
  339. package/templates/nextblock-template/lib/custom-block-r2-upload-shared.ts +178 -0
  340. package/templates/nextblock-template/lib/custom-block-r2-upload.test.ts +140 -0
  341. package/templates/nextblock-template/lib/custom-block-r2-upload.ts +68 -0
  342. package/templates/nextblock-template/lib/custom-block-relation-registry.ts +256 -0
  343. package/templates/nextblock-template/lib/custom-block-relations.test.ts +227 -0
  344. package/templates/nextblock-template/lib/custom-block-relations.ts +279 -0
  345. package/templates/nextblock-template/lib/custom-block-safelist.ts +14 -0
  346. package/templates/nextblock-template/lib/editor/dynamic-extension-core.test.ts +172 -0
  347. package/templates/nextblock-template/lib/editor/dynamic-extension-core.ts +213 -0
  348. package/templates/nextblock-template/lib/editor/dynamic-extension-loader.ts +22 -0
  349. package/templates/nextblock-template/lib/editor/dynamic-extensions.tsx +193 -0
  350. package/templates/nextblock-template/lib/full-backup/manifest.test.ts +121 -0
  351. package/templates/nextblock-template/lib/full-backup/manifest.ts +206 -0
  352. package/templates/nextblock-template/lib/full-backup/server.ts +743 -0
  353. package/templates/nextblock-template/lib/media/resolveMediaUrl.ts +45 -0
  354. package/templates/nextblock-template/lib/posts/readTime.ts +60 -0
  355. package/templates/nextblock-template/lib/privacy/consent-client.ts +57 -0
  356. package/templates/nextblock-template/lib/privacy/settings.ts +103 -0
  357. package/templates/nextblock-template/lib/privacy/types.ts +67 -0
  358. package/templates/nextblock-template/lib/promotions/server.test.ts +74 -0
  359. package/templates/nextblock-template/lib/promotions/server.ts +741 -0
  360. package/templates/nextblock-template/lib/resolve-block-relations.test.ts +142 -0
  361. package/templates/nextblock-template/lib/resolve-block-relations.ts +255 -0
  362. package/templates/nextblock-template/lib/search/server.ts +585 -0
  363. package/templates/nextblock-template/lib/search/types.ts +27 -0
  364. package/templates/nextblock-template/lib/visual-editing/draft-content.test.ts +105 -0
  365. package/templates/nextblock-template/lib/visual-editing/draft-content.ts +380 -0
  366. package/templates/nextblock-template/lib/visual-editing/draft-route.test.ts +42 -0
  367. package/templates/nextblock-template/lib/visual-editing/draft-route.ts +82 -0
  368. package/templates/nextblock-template/lib/visual-editing/edit-info.test.ts +143 -0
  369. package/templates/nextblock-template/lib/visual-editing/edit-info.ts +94 -0
  370. package/templates/nextblock-template/lib/visual-editing/mutations.ts +190 -0
  371. package/templates/nextblock-template/lib/visual-editing/product-drafts.test.ts +81 -0
  372. package/templates/nextblock-template/lib/visual-editing/product-drafts.ts +511 -0
  373. package/templates/nextblock-template/lib/visual-editing/types.ts +122 -0
  374. package/templates/nextblock-template/lib/zod-config.ts +5 -0
  375. package/templates/nextblock-template/next.config.js +190 -66
  376. package/templates/nextblock-template/package.json +34 -30
  377. package/templates/nextblock-template/proxy.ts +435 -253
  378. package/templates/nextblock-template/public/images/NBcover.webp +0 -0
  379. package/templates/nextblock-template/public/images/cap.webp +0 -0
  380. package/templates/nextblock-template/public/images/commerce-plan.webp +0 -0
  381. package/templates/nextblock-template/public/images/commerce-square.webp +0 -0
  382. package/templates/nextblock-template/public/images/commerce-wide.webp +0 -0
  383. package/templates/nextblock-template/public/images/cortex-ai-square.webp +0 -0
  384. package/templates/nextblock-template/public/images/cortex-ai.webp +0 -0
  385. package/templates/nextblock-template/public/images/extensibility.webp +0 -0
  386. package/templates/nextblock-template/public/images/goals.webp +0 -0
  387. package/templates/nextblock-template/public/images/included.webp +0 -0
  388. package/templates/nextblock-template/public/images/nx-graph.webp +0 -0
  389. package/templates/nextblock-template/public/images/pants.webp +0 -0
  390. package/templates/nextblock-template/public/images/t-shirt.webp +0 -0
  391. package/templates/nextblock-template/scripts/validate-editor-block-schema.ts +112 -0
  392. package/templates/nextblock-template/scripts/verify-cortex-ai-build-widget.tsx +100 -0
  393. package/templates/nextblock-template/scripts/verify-cortex-ai-generate-blocks.ts +62 -0
  394. package/templates/nextblock-template/scripts/verify-cortex-ai-global-tools.ts +537 -0
  395. package/templates/nextblock-template/scripts/verify-cortex-ai-routing.ts +58 -0
  396. package/templates/nextblock-template/scripts/verify-custom-block-definitions.ts +188 -0
  397. package/templates/nextblock-template/scripts/verify-dynamic-custom-block-extensions.ts +123 -0
  398. package/templates/nextblock-template/scripts/verify-dynamic-layout-engine.tsx +133 -0
  399. package/templates/nextblock-template/scripts/verify-milestone-2-custom-blocks.ts +65 -0
  400. package/templates/nextblock-template/tailwind.config.js +1 -0
  401. package/templates/nextblock-template/tools/configure-supabase-auth.js +282 -0
  402. package/templates/nextblock-template/tools/deploy-supabase.js +69 -71
  403. package/templates/nextblock-template/tsconfig.json +52 -66
  404. package/templates/nextblock-template/tsconfig.tsbuildinfo +1 -1
  405. package/templates/nextblock-template/types/jsdom.d.ts +6 -0
  406. package/templates/nextblock-template/app/force-styles.tsx +0 -31
  407. package/templates/nextblock-template/app/sitemap.xml/route.ts +0 -63
  408. package/templates/nextblock-template/components/blocks/renderers/HeroBlockRenderer.tsx +0 -273
  409. package/templates/nextblock-template/docs/How to Create a Custom Block.md +0 -149
  410. package/templates/nextblock-template/docs/cms-application-overview.md +0 -56
  411. package/templates/nextblock-template/docs/cms-architecture-overview.md +0 -73
  412. package/templates/nextblock-template/docs/files-structure.md +0 -426
  413. package/templates/nextblock-template/docs/tiptap-bundle-optimization-summary.md +0 -174
@@ -0,0 +1,965 @@
1
+ import { stepCountIs, streamText } from 'ai';
2
+ import { NextResponse } from 'next/server';
3
+ import { z } from '../../../../lib/zod-config';
4
+
5
+ import {
6
+ createClient,
7
+ getServiceRoleSupabaseClient,
8
+ verifyPackageOnline,
9
+ } from '@nextblock-cms/db/server';
10
+
11
+ import {
12
+ buildCortexAiRoutingPolicy,
13
+ createCortexAiOpenRouterClient,
14
+ isOpenRouterRateLimitError,
15
+ omitUnsupportedCortexAiModelOptions,
16
+ summarizeCortexAiRoutingError,
17
+ } from '../../../../lib/ai-client';
18
+ import { safeParseCortexAiModelSelection } from '../../../../lib/ai-model-registry';
19
+ import {
20
+ buildVisibleContactIntroActionPlan,
21
+ cortexAiPageContextSchema,
22
+ createCortexGlobalAgentTools,
23
+ executeCmsActionPlan,
24
+ executeCreateCmsPage,
25
+ executeCreateCmsPost,
26
+ executeCreateCmsProduct,
27
+ executeDeleteCmsItem,
28
+ executeInsertContentBlock,
29
+ executeUpdateContentBlock,
30
+ executeUpdateCmsItemField,
31
+ executeUpdateCurrentCmsFields,
32
+ executeUpdateFooter,
33
+ executeUpdateNavigationBar,
34
+ executeUpdateSectionColumnBlock,
35
+ type CortexAiPageContext,
36
+ } from '../../../../lib/ai-global-agent-tools';
37
+ import {
38
+ executeDatabaseActionPlan,
39
+ executeDatabaseMutation,
40
+ } from '../../../../lib/ai-global-agent-db-tools';
41
+ import { executeDeleteCustomBlock } from '../../../../lib/ai-global-agent-custom-block-tools';
42
+
43
+ export const dynamic = 'force-dynamic';
44
+
45
+ const GLOBAL_AGENT_MODEL_ATTEMPT_TIMEOUT_MS = 30000;
46
+
47
+ const globalAgentMessageSchema = z.strictObject({
48
+ content: z.string().min(1).max(8000),
49
+ role: z.enum(['system', 'user', 'assistant']),
50
+ });
51
+
52
+ const confirmedToolCallSchema = z.strictObject({
53
+ confirmationPhrase: z.string().min(1).max(500),
54
+ input: z.unknown(),
55
+ toolName: z.enum([
56
+ 'create_cms_page',
57
+ 'create_cms_post',
58
+ 'create_cms_product',
59
+ 'delete_cms_item',
60
+ 'delete_custom_block',
61
+ 'execute_database_action_plan',
62
+ 'execute_database_mutation',
63
+ 'execute_cms_action_plan',
64
+ 'insert_content_block',
65
+ 'update_cms_item_field',
66
+ 'update_content_block',
67
+ 'update_current_cms_fields',
68
+ 'update_footer',
69
+ 'update_navigation_bar',
70
+ 'update_section_column_block',
71
+ ]),
72
+ });
73
+
74
+ const globalAgentRequestSchema = z.strictObject({
75
+ confirmedToolCall: confirmedToolCallSchema.optional(),
76
+ messages: z.array(globalAgentMessageSchema).min(1).max(40),
77
+ pageContext: cortexAiPageContextSchema.nullable().optional(),
78
+ });
79
+
80
+ const GLOBAL_AGENT_SYSTEM_PROMPT = [
81
+ 'You are NextBlock Cortex AI, the global dashboard agent for a block-based CMS.',
82
+ 'Operate as a Planner, Executor, and Evaluator.',
83
+ 'Plan the smallest safe change, execute only through typed tools, evaluate the tool result, then answer concisely.',
84
+ '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.',
85
+ '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(...).',
86
+ '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.',
87
+ '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.',
88
+ '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.',
89
+ '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.',
90
+ '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.',
91
+ '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.',
92
+ '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.',
93
+ '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.',
94
+ 'Use mode "replace" only when the user explicitly asks to rebuild or replace the entire navigation menu and you provide the full menu.',
95
+ 'Use update_footer for public footer links or copyright settings.',
96
+ '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.',
97
+ '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).',
98
+ '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.',
99
+ '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.',
100
+ '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.',
101
+ '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.',
102
+ '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.',
103
+ '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.',
104
+ '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.',
105
+ '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.',
106
+ 'Use search_documentation before answering implementation or CMS usage questions that require factual project context.',
107
+ '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.',
108
+ '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.',
109
+ 'Use fetch_ecommerce_stats for quantitative questions about revenue, products, or order counts. This tool is read-only.',
110
+ '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.',
111
+ 'Never invent database fields, raw SQL, markdown content, or unsupported tool arguments.',
112
+ ].join(' ');
113
+
114
+ type CortexAgentStreamEvent =
115
+ | {
116
+ credentialSource: string;
117
+ modelId: string;
118
+ type: 'meta';
119
+ }
120
+ | {
121
+ text: string;
122
+ type: 'text-delta';
123
+ }
124
+ | {
125
+ input?: unknown;
126
+ toolCallId?: string;
127
+ toolName: string;
128
+ type: 'tool-call';
129
+ }
130
+ | {
131
+ output?: unknown;
132
+ toolCallId?: string;
133
+ toolName: string;
134
+ type: 'tool-result';
135
+ }
136
+ | {
137
+ message: string;
138
+ toolCallId?: string;
139
+ toolName?: string;
140
+ type: 'tool-error';
141
+ }
142
+ | {
143
+ message: string;
144
+ type: 'error';
145
+ }
146
+ | {
147
+ type: 'finish';
148
+ };
149
+
150
+ type CortexAgentStreamPart = {
151
+ error?: unknown;
152
+ id?: string;
153
+ input?: unknown;
154
+ output?: unknown;
155
+ text?: string;
156
+ toolCallId?: string;
157
+ toolName?: string;
158
+ type: string;
159
+ };
160
+
161
+ async function requireAdminAccess() {
162
+ const supabase = createClient();
163
+ const {
164
+ data: { user },
165
+ error: userError,
166
+ } = await supabase.auth.getUser();
167
+
168
+ if (userError || !user) {
169
+ return null;
170
+ }
171
+
172
+ const { data: profile, error: profileError } = await supabase
173
+ .from('profiles')
174
+ .select('role')
175
+ .eq('id', user.id)
176
+ .single();
177
+
178
+ return !profileError && profile?.role === 'ADMIN' ? { userId: user.id } : null;
179
+ }
180
+
181
+ function jsonError(message: string, status: number) {
182
+ return NextResponse.json({ error: message }, { status });
183
+ }
184
+
185
+ const encoder = new TextEncoder();
186
+
187
+ function encodeStreamEvent(event: CortexAgentStreamEvent) {
188
+ return encoder.encode(`data: ${JSON.stringify(event)}\n\n`);
189
+ }
190
+
191
+ function formatPageContextForPrompt(pageContext: CortexAiPageContext | null | undefined) {
192
+ if (!pageContext) {
193
+ return 'No current CMS edit context was supplied. Ask the user to open the relevant edit screen before using page-aware editing tools.';
194
+ }
195
+
196
+ return [
197
+ `Current CMS edit context: contentType=${pageContext.contentType}`,
198
+ `entityId=${String(pageContext.entityId)}`,
199
+ pageContext.slug ? `slug=${pageContext.slug}` : null,
200
+ pageContext.title ? `title=${pageContext.title}` : null,
201
+ pageContext.translationGroupId ? `translationGroupId=${pageContext.translationGroupId}` : null,
202
+ pageContext.languageId ? `languageId=${pageContext.languageId}` : null,
203
+ pageContext.currentEditor?.field ? `currentField=${pageContext.currentEditor.field}` : null,
204
+ pageContext.currentEditor?.blockId ? `currentBlockId=${pageContext.currentEditor.blockId}` : null,
205
+ pageContext.currentEditor?.blockType ? `currentBlockType=${pageContext.currentEditor.blockType}` : null,
206
+ ]
207
+ .filter(Boolean)
208
+ .join(', ');
209
+ }
210
+
211
+ function buildGlobalAgentSystemPrompt(pageContext: CortexAiPageContext | null | undefined) {
212
+ return [
213
+ GLOBAL_AGENT_SYSTEM_PROMPT,
214
+ formatPageContextForPrompt(pageContext),
215
+ 'When the user says "this page", "this post", "this product", "this field", or "this block", interpret that through the supplied current CMS edit context.',
216
+ 'Do not update content outside the supplied current CMS context.',
217
+ ].join(' ');
218
+ }
219
+
220
+ function serializeStreamError(error: unknown) {
221
+ return summarizeCortexAiRoutingError(
222
+ error,
223
+ error instanceof Error ? error.message : 'Cortex AI global agent failed.'
224
+ );
225
+ }
226
+
227
+ function getToolCallId(part: CortexAgentStreamPart) {
228
+ return part.toolCallId || part.id;
229
+ }
230
+
231
+ function looksLikeRawToolCallLeak(value: string) {
232
+ const normalized = value.toLowerCase();
233
+
234
+ return (
235
+ normalized.includes('<toolcall') ||
236
+ normalized.includes('</toolcall') ||
237
+ normalized.includes('"arguments"') ||
238
+ normalized.includes('"update_navigation_bar"') ||
239
+ normalized.includes('"update_footer"') ||
240
+ normalized.includes('"search_documentation"') ||
241
+ normalized.includes('"read_current_cms_item"') ||
242
+ normalized.includes('"update_current_cms_fields"') ||
243
+ normalized.includes('"update_cms_item_field"') ||
244
+ normalized.includes('"update_content_block"') ||
245
+ normalized.includes('"insert_content_block"') ||
246
+ normalized.includes('"update_section_column_block"') ||
247
+ normalized.includes('"create_cms_page"') ||
248
+ normalized.includes('"create_cms_post"') ||
249
+ normalized.includes('"create_cms_product"') ||
250
+ normalized.includes('"execute_cms_action_plan"') ||
251
+ normalized.includes('"prepare_delete_cms_item"') ||
252
+ normalized.includes('"delete_cms_item"')
253
+ );
254
+ }
255
+
256
+ function looksLikeRateLimitText(value: string) {
257
+ const normalized = value.toLowerCase();
258
+
259
+ return (
260
+ normalized.includes('rate limit exceeded') ||
261
+ normalized.includes('free-models-per-day') ||
262
+ normalized.includes('too many requests')
263
+ );
264
+ }
265
+
266
+ function readNumberField(value: unknown, key: string) {
267
+ if (!value || typeof value !== 'object' || !(key in value)) {
268
+ return null;
269
+ }
270
+
271
+ const parsed = Number((value as Record<string, unknown>)[key]);
272
+ return Number.isFinite(parsed) ? parsed : null;
273
+ }
274
+
275
+ function isRecord(value: unknown): value is Record<string, unknown> {
276
+ return Boolean(value && typeof value === 'object' && !Array.isArray(value));
277
+ }
278
+
279
+ function readStringField(value: unknown, key: string) {
280
+ if (!isRecord(value)) {
281
+ return null;
282
+ }
283
+
284
+ const fieldValue = value[key];
285
+
286
+ return typeof fieldValue === 'string' ? fieldValue : null;
287
+ }
288
+
289
+ function pluralize(count: number, singular: string, plural = `${singular}s`) {
290
+ return `${count} ${count === 1 ? singular : plural}`;
291
+ }
292
+
293
+ function getConfirmationSummary(toolName?: string, output?: unknown) {
294
+ if (!isRecord(output) || !isRecord(output.preview)) {
295
+ return 'Complete the requested CMS change.';
296
+ }
297
+
298
+ const preview = output.preview;
299
+ const summary = readStringField(preview, 'summary');
300
+
301
+ if (summary) {
302
+ const actionSummaries = Array.isArray(preview.actionSummaries)
303
+ ? preview.actionSummaries.filter((item): item is string => typeof item === 'string')
304
+ : [];
305
+
306
+ return actionSummaries.length > 0
307
+ ? `${summary}\n\n${actionSummaries.map((item, index) => `${index + 1}. ${item}`).join('\n')}`
308
+ : summary;
309
+ }
310
+
311
+ const title = readStringField(preview, 'title');
312
+ const slug = readStringField(preview, 'slug');
313
+ const status = readStringField(preview, 'status');
314
+ const contentType = readStringField(preview, 'contentType');
315
+ const field = readStringField(preview, 'field');
316
+ const mode = readStringField(preview, 'mode');
317
+ const languageCode = readStringField(preview, 'languageCode');
318
+ const blockCount = readNumberField(preview, 'blockCount');
319
+ const itemCount = readNumberField(preview, 'itemCount');
320
+ const affectedCount = readNumberField(preview, 'affectedCount');
321
+
322
+ if (toolName === 'create_cms_page' || toolName === 'create_cms_post') {
323
+ return `Create ${status || 'draft'} ${toolName === 'create_cms_page' ? 'page' : 'post'} "${title || slug || 'Untitled'}"${slug ? ` at slug "${slug}"` : ''}${blockCount !== null ? ` with ${pluralize(blockCount, 'content block')}` : ''}.`;
324
+ }
325
+
326
+ if (toolName === 'create_cms_product') {
327
+ return `Create ${status || 'draft'} product "${title || slug || 'Untitled'}"${slug ? ` at slug "${slug}"` : ''}.`;
328
+ }
329
+
330
+ if (toolName === 'update_cms_item_field') {
331
+ return `Update ${field || 'one field'} on the ${contentType || 'CMS item'} "${title || slug || 'selected item'}".`;
332
+ }
333
+
334
+ if (toolName === 'update_navigation_bar') {
335
+ return `${mode === 'append' ? 'Add' : mode === 'update' ? 'Update' : 'Replace'} ${itemCount !== null ? pluralize(itemCount, 'navigation item') : 'navigation items'} in the ${languageCode || 'selected'} header navigation.`;
336
+ }
337
+
338
+ if (toolName === 'update_footer') {
339
+ const linkCount = readNumberField(preview, 'linkCount');
340
+ return `Update the ${languageCode || 'selected'} footer${linkCount !== null ? ` with ${pluralize(linkCount, 'link')}` : ''}.`;
341
+ }
342
+
343
+ if (toolName === 'update_content_block') {
344
+ return `Update the selected ${readStringField(preview, 'blockType') || 'content'} block.`;
345
+ }
346
+
347
+ if (toolName === 'insert_content_block') {
348
+ return `Insert ${readStringField(preview, 'blockType') || 'content'} block on the ${contentType || 'CMS item'} "${title || slug || 'selected item'}".`;
349
+ }
350
+
351
+ if (toolName === 'update_section_column_block') {
352
+ return `Update the selected nested ${readStringField(preview, 'nestedBlockType') || 'section'} block.`;
353
+ }
354
+
355
+ if (toolName === 'delete_cms_item' || toolName === 'prepare_delete_cms_item') {
356
+ return `Delete ${affectedCount !== null ? pluralize(affectedCount, contentType || 'CMS item') : `the selected ${contentType || 'CMS item'}`}${title || slug ? ` for "${title || slug}"` : ''}.`;
357
+ }
358
+
359
+ return 'Complete the requested CMS change.';
360
+ }
361
+
362
+ function getToolCompletionMessage(toolName?: string, output?: unknown) {
363
+ if (isRecord(output)) {
364
+ if (output.requiresConfirmation === true) {
365
+ return `Please confirm for me to complete:\n\n${getConfirmationSummary(toolName, output)}`;
366
+ }
367
+
368
+ if (output.unsupported === true || output.success === false) {
369
+ return readStringField(output, 'message') || 'I could not complete that request.';
370
+ }
371
+ }
372
+
373
+ const mutationExecuted = isRecord(output) && output.mutationExecuted === true;
374
+
375
+ if (toolName === 'update_navigation_bar') {
376
+ const insertedCount = readNumberField(output, 'insertedCount');
377
+ const skippedCount = readNumberField(output, 'skippedCount');
378
+
379
+ if ((insertedCount ?? 0) === 0 && (skippedCount ?? 0) > 0) {
380
+ return 'That navigation link already exists, so I left the header unchanged.';
381
+ }
382
+
383
+ return 'Done. I updated the navigation bar.';
384
+ }
385
+
386
+ if (toolName === 'update_footer') {
387
+ return 'Done. I updated the footer.';
388
+ }
389
+
390
+ if (toolName === 'search_documentation') {
391
+ return 'I searched the documentation, but the model was interrupted before it could finish a summary.';
392
+ }
393
+
394
+ if (toolName === 'read_current_cms_item') {
395
+ return 'I read the current CMS item, but the model was interrupted before it could finish a summary.';
396
+ }
397
+
398
+ if (toolName === 'fetch_ecommerce_stats') {
399
+ return 'I fetched the latest ecommerce statistics for you.';
400
+ }
401
+
402
+ if (toolName === 'describe_database_schema') {
403
+ return 'I inspected the available database schema.';
404
+ }
405
+
406
+ if (toolName === 'read_database_records') {
407
+ return 'I read the requested database records.';
408
+ }
409
+
410
+ if (toolName === 'execute_database_mutation') {
411
+ const affectedCount = readNumberField(output, 'affectedCount');
412
+ const table = readStringField(output, 'table');
413
+ const auditLogged = isRecord(output) && output.auditLogged === true;
414
+
415
+ return mutationExecuted
416
+ ? `Done. I updated ${affectedCount ? pluralize(affectedCount, 'database row') : 'the database'}${table ? ` in ${table}` : ''}.${auditLogged ? ' Audit logged.' : ''}`
417
+ : 'I prepared the database mutation.';
418
+ }
419
+
420
+ if (toolName === 'execute_database_action_plan') {
421
+ const actionCount = readNumberField(output, 'actionCount');
422
+ const auditLogged = isRecord(output) && output.auditLogged === true;
423
+
424
+ return mutationExecuted
425
+ ? `Done. I completed ${actionCount ? pluralize(actionCount, 'database action') : 'the database action plan'}.${auditLogged ? ' Audit logged.' : ''}`
426
+ : 'I prepared the database action plan.';
427
+ }
428
+
429
+ if (toolName === 'update_current_cms_fields') {
430
+ return 'Done. I updated the current CMS fields.';
431
+ }
432
+
433
+ if (toolName === 'update_cms_item_field') {
434
+ return mutationExecuted
435
+ ? 'Done. I updated that CMS field.'
436
+ : 'I prepared the CMS field update.';
437
+ }
438
+
439
+ if (toolName === 'update_content_block') {
440
+ return 'Done. I updated the current content block.';
441
+ }
442
+
443
+ if (toolName === 'insert_content_block') {
444
+ return 'Done. I inserted the content block.';
445
+ }
446
+
447
+ if (toolName === 'update_section_column_block') {
448
+ return 'Done. I updated the nested section block.';
449
+ }
450
+
451
+ if (toolName === 'create_cms_page') {
452
+ return mutationExecuted ? 'Done. I created the page.' : 'I prepared the page creation.';
453
+ }
454
+
455
+ if (toolName === 'create_cms_post') {
456
+ return mutationExecuted ? 'Done. I created the post.' : 'I prepared the post creation.';
457
+ }
458
+
459
+ if (toolName === 'create_cms_product') {
460
+ return mutationExecuted ? 'Done. I created the product.' : 'I prepared the product creation.';
461
+ }
462
+
463
+ if (toolName === 'prepare_delete_cms_item') {
464
+ return getToolCompletionMessage('delete_cms_item', output);
465
+ }
466
+
467
+ if (toolName === 'delete_cms_item') {
468
+ return mutationExecuted ? 'Done. I deleted that CMS item.' : 'I prepared the delete request.';
469
+ }
470
+
471
+ if (toolName === 'execute_cms_action_plan') {
472
+ const actionCount = readNumberField(output, 'actionCount');
473
+
474
+ return mutationExecuted
475
+ ? `Done. I completed ${actionCount ? pluralize(actionCount, 'CMS action') : 'the CMS action plan'}.`
476
+ : 'I prepared the CMS action plan.';
477
+ }
478
+
479
+ return 'Done. I completed the requested update.';
480
+ }
481
+
482
+ function completeToolBackedText(text: string, toolName?: string, output?: unknown) {
483
+ const trimmedText = text.trim();
484
+
485
+ if (!isRecord(output)) {
486
+ return trimmedText || getToolCompletionMessage(toolName, output);
487
+ }
488
+
489
+ if (output.requiresConfirmation === true) {
490
+ return getToolCompletionMessage(toolName, output);
491
+ }
492
+
493
+ if ((output.unsupported === true || output.success === false) && !trimmedText) {
494
+ return getToolCompletionMessage(toolName, output);
495
+ }
496
+
497
+ return trimmedText || getToolCompletionMessage(toolName, output);
498
+ }
499
+
500
+ async function executeConfirmedToolCall(params: {
501
+ context: Parameters<typeof createCortexGlobalAgentTools>[0];
502
+ input: unknown;
503
+ toolName: z.infer<typeof confirmedToolCallSchema>['toolName'];
504
+ }) {
505
+ switch (params.toolName) {
506
+ case 'create_cms_page':
507
+ return executeCreateCmsPage(params.input as any, params.context);
508
+ case 'create_cms_post':
509
+ return executeCreateCmsPost(params.input as any, params.context);
510
+ case 'create_cms_product':
511
+ return executeCreateCmsProduct(params.input as any, params.context);
512
+ case 'delete_cms_item':
513
+ return executeDeleteCmsItem(params.input as any, params.context);
514
+ case 'delete_custom_block':
515
+ return executeDeleteCustomBlock(params.input as any, params.context);
516
+ case 'execute_database_action_plan':
517
+ return executeDatabaseActionPlan(params.input as any, params.context);
518
+ case 'execute_database_mutation':
519
+ return executeDatabaseMutation(params.input as any, params.context);
520
+ case 'execute_cms_action_plan':
521
+ return executeCmsActionPlan(params.input as any, params.context);
522
+ case 'update_cms_item_field':
523
+ return executeUpdateCmsItemField(params.input as any, params.context);
524
+ case 'update_content_block':
525
+ return executeUpdateContentBlock(params.input as any, params.context);
526
+ case 'insert_content_block':
527
+ return executeInsertContentBlock(params.input as any, params.context);
528
+ case 'update_current_cms_fields':
529
+ return executeUpdateCurrentCmsFields(params.input as any, params.context);
530
+ case 'update_footer':
531
+ return executeUpdateFooter(params.input as any, params.context);
532
+ case 'update_navigation_bar':
533
+ return executeUpdateNavigationBar(params.input as any, params.context);
534
+ case 'update_section_column_block':
535
+ return executeUpdateSectionColumnBlock(params.input as any, params.context);
536
+ }
537
+ }
538
+
539
+ function createConfirmedToolCallStream(params: {
540
+ context: Parameters<typeof createCortexGlobalAgentTools>[0];
541
+ input: unknown;
542
+ toolName: z.infer<typeof confirmedToolCallSchema>['toolName'];
543
+ }) {
544
+ return new ReadableStream({
545
+ async start(controller) {
546
+ const toolCallId = `confirmed-${Date.now()}`;
547
+
548
+ controller.enqueue(
549
+ encodeStreamEvent({
550
+ input: params.input,
551
+ toolCallId,
552
+ toolName: params.toolName,
553
+ type: 'tool-call',
554
+ })
555
+ );
556
+
557
+ try {
558
+ const output = await executeConfirmedToolCall(params);
559
+
560
+ controller.enqueue(
561
+ encodeStreamEvent({
562
+ output,
563
+ toolCallId,
564
+ toolName: params.toolName,
565
+ type: 'tool-result',
566
+ })
567
+ );
568
+ controller.enqueue(
569
+ encodeStreamEvent({
570
+ text: getToolCompletionMessage(params.toolName, output),
571
+ type: 'text-delta',
572
+ })
573
+ );
574
+ } catch (error) {
575
+ controller.enqueue(
576
+ encodeStreamEvent({
577
+ message: serializeStreamError(error),
578
+ toolCallId,
579
+ toolName: params.toolName,
580
+ type: 'tool-error',
581
+ })
582
+ );
583
+ controller.enqueue(
584
+ encodeStreamEvent({
585
+ message: serializeStreamError(error),
586
+ type: 'error',
587
+ })
588
+ );
589
+ }
590
+
591
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
592
+ controller.close();
593
+ },
594
+ });
595
+ }
596
+
597
+ function getRetryableStreamError(
598
+ error: unknown,
599
+ sawRawToolCallLeak: boolean,
600
+ sawRateLimitText: boolean
601
+ ) {
602
+ if (sawRawToolCallLeak) {
603
+ return new Error('OpenRouter returned an invalid raw tool-call payload.');
604
+ }
605
+
606
+ if (sawRateLimitText) {
607
+ return new Error('OpenRouter rate limit exceeded.');
608
+ }
609
+
610
+ return error;
611
+ }
612
+
613
+ function createAttemptAbortSignal(requestSignal: AbortSignal) {
614
+ const controller = new AbortController();
615
+ const timeoutId = setTimeout(() => {
616
+ controller.abort(new Error('Cortex AI response timed out. Please try again.'));
617
+ }, GLOBAL_AGENT_MODEL_ATTEMPT_TIMEOUT_MS);
618
+ const abortFromRequest = () => controller.abort(requestSignal.reason);
619
+
620
+ if (requestSignal.aborted) {
621
+ abortFromRequest();
622
+ } else {
623
+ requestSignal.addEventListener('abort', abortFromRequest, { once: true });
624
+ }
625
+
626
+ return {
627
+ cleanup: () => {
628
+ clearTimeout(timeoutId);
629
+ requestSignal.removeEventListener('abort', abortFromRequest);
630
+ },
631
+ signal: controller.signal,
632
+ };
633
+ }
634
+
635
+ export async function POST(request: Request) {
636
+ try {
637
+ const adminAccess = await requireAdminAccess();
638
+
639
+ if (!adminAccess) {
640
+ return jsonError('You do not have permission to use the global Cortex AI agent.', 403);
641
+ }
642
+
643
+ const isCortexAiActive = await verifyPackageOnline('cortex-ai');
644
+
645
+ if (!isCortexAiActive) {
646
+ return jsonError('NextBlock Cortex AI is not active for this workspace.', 403);
647
+ }
648
+
649
+ const body = await request.json().catch(() => null);
650
+ const parsedRequest = globalAgentRequestSchema.safeParse(body);
651
+
652
+ if (!parsedRequest.success) {
653
+ return jsonError('Invalid Cortex AI global-agent request.', 400);
654
+ }
655
+
656
+ const pageContext = parsedRequest.data.pageContext ?? null;
657
+ const latestUserMessage =
658
+ [...parsedRequest.data.messages].reverse().find((message) => message.role === 'user')
659
+ ?.content ?? '';
660
+
661
+ if (parsedRequest.data.confirmedToolCall) {
662
+ const confirmedToolCall = parsedRequest.data.confirmedToolCall;
663
+ const stream = createConfirmedToolCallStream({
664
+ context: {
665
+ actorUserId: adminAccess.userId,
666
+ latestUserMessage: confirmedToolCall.confirmationPhrase,
667
+ pageContext,
668
+ supabase: getServiceRoleSupabaseClient(),
669
+ },
670
+ input: confirmedToolCall.input,
671
+ toolName: confirmedToolCall.toolName,
672
+ });
673
+
674
+ return new Response(stream, {
675
+ headers: {
676
+ 'Cache-Control': 'no-cache, no-transform',
677
+ Connection: 'keep-alive',
678
+ 'Content-Type': 'text/event-stream; charset=utf-8',
679
+ 'X-Accel-Buffering': 'no',
680
+ },
681
+ });
682
+ }
683
+
684
+ const directActionPlan = buildVisibleContactIntroActionPlan(latestUserMessage);
685
+
686
+ if (directActionPlan) {
687
+ const stream = createConfirmedToolCallStream({
688
+ context: {
689
+ actorUserId: adminAccess.userId,
690
+ latestUserMessage,
691
+ pageContext,
692
+ supabase: getServiceRoleSupabaseClient(),
693
+ },
694
+ input: directActionPlan,
695
+ toolName: 'execute_cms_action_plan',
696
+ });
697
+
698
+ return new Response(stream, {
699
+ headers: {
700
+ 'Cache-Control': 'no-cache, no-transform',
701
+ Connection: 'keep-alive',
702
+ 'Content-Type': 'text/event-stream; charset=utf-8',
703
+ 'X-Accel-Buffering': 'no',
704
+ },
705
+ });
706
+ }
707
+
708
+ const sandboxKey = process.env.NEXT_PUBLIC_IS_SANDBOX === 'true' ? request.headers.get('x-sandbox-openrouter-key') : null;
709
+ const sandboxModelRaw = process.env.NEXT_PUBLIC_IS_SANDBOX === 'true' ? request.headers.get('x-sandbox-openrouter-model') : null;
710
+
711
+ let modelSelection = null;
712
+ if (sandboxModelRaw) {
713
+ try {
714
+ modelSelection = safeParseCortexAiModelSelection(JSON.parse(sandboxModelRaw));
715
+ } catch {
716
+ // Ignore parse errors from headers
717
+ }
718
+ }
719
+
720
+ const client = await createCortexAiOpenRouterClient({
721
+ apiKey: sandboxKey || undefined,
722
+ modelSelection: sandboxKey && modelSelection ? modelSelection : undefined,
723
+ });
724
+ const routingPolicy = buildCortexAiRoutingPolicy({
725
+ credentialSource: client.credentialSource,
726
+ selectedModel: client.modelSelection,
727
+ });
728
+ const modelIds = routingPolicy.modelIds;
729
+ const tools = createCortexGlobalAgentTools({
730
+ actorUserId: adminAccess.userId,
731
+ cortexAiApiKey: sandboxKey,
732
+ cortexAiModelSelection: sandboxKey && modelSelection ? modelSelection : undefined,
733
+ latestUserMessage,
734
+ pageContext,
735
+ supabase: getServiceRoleSupabaseClient(),
736
+ });
737
+ const systemPrompt = buildGlobalAgentSystemPrompt(pageContext);
738
+
739
+ const stream = new ReadableStream({
740
+ async start(controller) {
741
+ let completed = false;
742
+ let lastError: unknown = null;
743
+
744
+ for (const [index, modelId] of modelIds.entries()) {
745
+ let textBuffer = '';
746
+ let sawRawToolCallLeak = false;
747
+ let sawRateLimitText = false;
748
+ let hasToolCall = false;
749
+ let hasSuccessfulToolResult = false;
750
+ let lastToolName: string | undefined;
751
+ let lastToolOutput: unknown;
752
+
753
+ controller.enqueue(
754
+ encodeStreamEvent({
755
+ credentialSource: client.credentialSource,
756
+ modelId,
757
+ type: 'meta',
758
+ })
759
+ );
760
+
761
+ try {
762
+ const attemptAbort = createAttemptAbortSignal(request.signal);
763
+ const attemptOptions = omitUnsupportedCortexAiModelOptions(
764
+ {
765
+ abortSignal: attemptAbort.signal,
766
+ maxOutputTokens: 2000,
767
+ messages: parsedRequest.data.messages,
768
+ maxRetries: 0,
769
+ stopWhen: stepCountIs(6),
770
+ system: systemPrompt,
771
+ temperature: 0.1,
772
+ tools,
773
+ } as Record<string, unknown>,
774
+ {
775
+ modelId,
776
+ modelSelection: routingPolicy.modelSelection,
777
+ }
778
+ );
779
+ const result = streamText({
780
+ ...attemptOptions,
781
+ model: client.model(modelId),
782
+ } as Parameters<typeof streamText>[0]);
783
+
784
+ try {
785
+ for await (const rawPart of result.fullStream) {
786
+ const part = rawPart as CortexAgentStreamPart;
787
+
788
+ if (part.type === 'text-delta' && part.text) {
789
+ textBuffer = `${textBuffer}${part.text}`;
790
+ sawRawToolCallLeak = sawRawToolCallLeak || looksLikeRawToolCallLeak(textBuffer);
791
+ sawRateLimitText = sawRateLimitText || looksLikeRateLimitText(textBuffer);
792
+ }
793
+
794
+ if (part.type === 'tool-call' && part.toolName) {
795
+ hasToolCall = true;
796
+ lastToolName = part.toolName;
797
+ controller.enqueue(
798
+ encodeStreamEvent({
799
+ input: part.input,
800
+ toolCallId: getToolCallId(part),
801
+ toolName: part.toolName,
802
+ type: 'tool-call',
803
+ })
804
+ );
805
+ }
806
+
807
+ if (part.type === 'tool-result' && part.toolName) {
808
+ hasSuccessfulToolResult = true;
809
+ lastToolName = part.toolName;
810
+ lastToolOutput = part.output;
811
+ controller.enqueue(
812
+ encodeStreamEvent({
813
+ output: part.output,
814
+ toolCallId: getToolCallId(part),
815
+ toolName: part.toolName,
816
+ type: 'tool-result',
817
+ })
818
+ );
819
+ }
820
+
821
+ if (part.type === 'tool-error') {
822
+ hasToolCall = true;
823
+ controller.enqueue(
824
+ encodeStreamEvent({
825
+ message: serializeStreamError(part.error),
826
+ toolCallId: getToolCallId(part),
827
+ toolName: part.toolName,
828
+ type: 'tool-error',
829
+ })
830
+ );
831
+ }
832
+
833
+ if (part.type === 'error') {
834
+ throw part.error || new Error('Cortex AI stream failed.');
835
+ }
836
+ }
837
+ } finally {
838
+ attemptAbort.cleanup();
839
+ }
840
+
841
+ if ((sawRawToolCallLeak || sawRateLimitText) && !hasToolCall) {
842
+ lastError = getRetryableStreamError(
843
+ null,
844
+ sawRawToolCallLeak,
845
+ sawRateLimitText
846
+ );
847
+
848
+ if (index < modelIds.length - 1) {
849
+ continue;
850
+ }
851
+
852
+ throw lastError;
853
+ }
854
+
855
+ if ((sawRawToolCallLeak || sawRateLimitText) && !hasSuccessfulToolResult) {
856
+ lastError = getRetryableStreamError(
857
+ null,
858
+ sawRawToolCallLeak,
859
+ sawRateLimitText
860
+ );
861
+ throw lastError;
862
+ }
863
+
864
+ if ((sawRawToolCallLeak || sawRateLimitText) && hasSuccessfulToolResult) {
865
+ controller.enqueue(
866
+ encodeStreamEvent({
867
+ text: getToolCompletionMessage(lastToolName, lastToolOutput),
868
+ type: 'text-delta',
869
+ })
870
+ );
871
+ } else if (textBuffer.trim() && !looksLikeRawToolCallLeak(textBuffer)) {
872
+ controller.enqueue(
873
+ encodeStreamEvent({
874
+ text: hasSuccessfulToolResult
875
+ ? completeToolBackedText(textBuffer, lastToolName, lastToolOutput)
876
+ : textBuffer,
877
+ type: 'text-delta',
878
+ })
879
+ );
880
+ } else if (hasSuccessfulToolResult) {
881
+ controller.enqueue(
882
+ encodeStreamEvent({
883
+ text: getToolCompletionMessage(lastToolName, lastToolOutput),
884
+ type: 'text-delta',
885
+ })
886
+ );
887
+ }
888
+
889
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
890
+ completed = true;
891
+ break;
892
+ } catch (error) {
893
+ lastError = getRetryableStreamError(error, sawRawToolCallLeak, sawRateLimitText);
894
+
895
+ if (hasSuccessfulToolResult) {
896
+ controller.enqueue(
897
+ encodeStreamEvent({
898
+ text: completeToolBackedText(textBuffer, lastToolName, lastToolOutput),
899
+ type: 'text-delta',
900
+ })
901
+ );
902
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
903
+ completed = true;
904
+ break;
905
+ }
906
+
907
+ if (
908
+ !hasToolCall &&
909
+ isOpenRouterRateLimitError(lastError) &&
910
+ index < modelIds.length - 1
911
+ ) {
912
+ continue;
913
+ }
914
+
915
+ if (
916
+ !hasToolCall &&
917
+ (sawRawToolCallLeak || sawRateLimitText) &&
918
+ index < modelIds.length - 1
919
+ ) {
920
+ continue;
921
+ }
922
+
923
+ controller.enqueue(
924
+ encodeStreamEvent({
925
+ message: serializeStreamError(lastError),
926
+ type: 'error',
927
+ })
928
+ );
929
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
930
+ completed = true;
931
+ break;
932
+ }
933
+
934
+ if (completed) {
935
+ break;
936
+ }
937
+ }
938
+
939
+ if (!completed) {
940
+ controller.enqueue(
941
+ encodeStreamEvent({
942
+ message: serializeStreamError(lastError),
943
+ type: 'error',
944
+ })
945
+ );
946
+ controller.enqueue(encodeStreamEvent({ type: 'finish' }));
947
+ }
948
+
949
+ controller.close();
950
+ },
951
+ });
952
+
953
+ return new Response(stream, {
954
+ headers: {
955
+ 'Cache-Control': 'no-cache, no-transform',
956
+ Connection: 'keep-alive',
957
+ 'Content-Type': 'text/event-stream; charset=utf-8',
958
+ 'X-Accel-Buffering': 'no',
959
+ },
960
+ });
961
+ } catch (error) {
962
+ console.error('[Cortex AI] Global agent failed:', error);
963
+ return jsonError(error instanceof Error ? error.message : 'Global agent failed.', 500);
964
+ }
965
+ }