@useatlas/create 0.0.1

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 (515) hide show
  1. package/README.md +231 -0
  2. package/index.ts +829 -0
  3. package/package.json +38 -0
  4. package/templates/docker/.env.example +67 -0
  5. package/templates/docker/Dockerfile +52 -0
  6. package/templates/docker/bin/__tests__/benchmark.test.ts +598 -0
  7. package/templates/docker/bin/__tests__/duckdb-ingest.test.ts +171 -0
  8. package/templates/docker/bin/__tests__/eval.test.ts +434 -0
  9. package/templates/docker/bin/__tests__/matview-partition.test.ts +615 -0
  10. package/templates/docker/bin/__tests__/multi-source.test.ts +113 -0
  11. package/templates/docker/bin/__tests__/plugin-cli.test.ts +322 -0
  12. package/templates/docker/bin/__tests__/profiler-heuristics.test.ts +608 -0
  13. package/templates/docker/bin/__tests__/query.test.ts +240 -0
  14. package/templates/docker/bin/__tests__/schema-drift.test.ts +542 -0
  15. package/templates/docker/bin/__tests__/view-yaml-generation.test.ts +146 -0
  16. package/templates/docker/bin/atlas.ts +5044 -0
  17. package/templates/docker/bin/benchmark.ts +695 -0
  18. package/templates/docker/bin/enrich.ts +559 -0
  19. package/templates/docker/bin/eval.ts +770 -0
  20. package/templates/docker/bin/smoke.ts +438 -0
  21. package/templates/docker/data/.gitkeep +0 -0
  22. package/templates/docker/data/cybersec.sql +1961 -0
  23. package/templates/docker/data/demo-semantic/catalog.yml +40 -0
  24. package/templates/docker/data/demo-semantic/entities/accounts.yml +170 -0
  25. package/templates/docker/data/demo-semantic/entities/companies.yml +207 -0
  26. package/templates/docker/data/demo-semantic/entities/people.yml +145 -0
  27. package/templates/docker/data/demo-semantic/glossary.yml +22 -0
  28. package/templates/docker/data/demo-semantic/metrics/accounts.yml +38 -0
  29. package/templates/docker/data/demo-semantic/metrics/companies.yml +89 -0
  30. package/templates/docker/data/demo.sql +373 -0
  31. package/templates/docker/data/ecommerce.sql +1690 -0
  32. package/templates/docker/data/init-demo-db.sql +8 -0
  33. package/templates/docker/docker-compose.yml +34 -0
  34. package/templates/docker/docs/deploy.md +390 -0
  35. package/templates/docker/eslint.config.mjs +18 -0
  36. package/templates/docker/gitignore +5 -0
  37. package/templates/docker/next.config.ts +9 -0
  38. package/templates/docker/package.json +59 -0
  39. package/templates/docker/postcss.config.mjs +8 -0
  40. package/templates/docker/public/.gitkeep +0 -0
  41. package/templates/docker/public/favicon.svg +4 -0
  42. package/templates/docker/railway.json +13 -0
  43. package/templates/docker/render.yaml +34 -0
  44. package/templates/docker/semantic/catalog.yml +5 -0
  45. package/templates/docker/semantic/entities/.gitkeep +0 -0
  46. package/templates/docker/semantic/glossary.yml +6 -0
  47. package/templates/docker/semantic/metrics/.gitkeep +0 -0
  48. package/templates/docker/sidecar/Dockerfile +28 -0
  49. package/templates/docker/sidecar/railway.json +14 -0
  50. package/templates/docker/sidecar/server.ts +188 -0
  51. package/templates/docker/src/api/__tests__/actions.test.ts +683 -0
  52. package/templates/docker/src/api/__tests__/admin.test.ts +820 -0
  53. package/templates/docker/src/api/__tests__/auth.test.ts +165 -0
  54. package/templates/docker/src/api/__tests__/chat.test.ts +376 -0
  55. package/templates/docker/src/api/__tests__/conversations.test.ts +555 -0
  56. package/templates/docker/src/api/__tests__/cors.test.ts +135 -0
  57. package/templates/docker/src/api/__tests__/health-plugin.test.ts +169 -0
  58. package/templates/docker/src/api/__tests__/health.test.ts +261 -0
  59. package/templates/docker/src/api/__tests__/query.test.ts +891 -0
  60. package/templates/docker/src/api/__tests__/scheduled-tasks.test.ts +601 -0
  61. package/templates/docker/src/api/__tests__/slack.test.ts +847 -0
  62. package/templates/docker/src/api/index.ts +117 -0
  63. package/templates/docker/src/api/routes/actions.ts +274 -0
  64. package/templates/docker/src/api/routes/admin.ts +757 -0
  65. package/templates/docker/src/api/routes/auth.ts +48 -0
  66. package/templates/docker/src/api/routes/chat.ts +465 -0
  67. package/templates/docker/src/api/routes/conversations.ts +266 -0
  68. package/templates/docker/src/api/routes/health.ts +287 -0
  69. package/templates/docker/src/api/routes/openapi.ts +390 -0
  70. package/templates/docker/src/api/routes/query.ts +318 -0
  71. package/templates/docker/src/api/routes/scheduled-tasks.ts +467 -0
  72. package/templates/docker/src/api/routes/slack.ts +611 -0
  73. package/templates/docker/src/api/server.ts +226 -0
  74. package/templates/docker/src/app/api/[...route]/route.ts +33 -0
  75. package/templates/docker/src/app/error.tsx +24 -0
  76. package/templates/docker/src/app/globals.css +126 -0
  77. package/templates/docker/src/app/layout.tsx +19 -0
  78. package/templates/docker/src/app/page.tsx +14 -0
  79. package/templates/docker/src/global.d.ts +1 -0
  80. package/templates/docker/src/lib/__tests__/agent-cache.test.ts +437 -0
  81. package/templates/docker/src/lib/__tests__/agent-dialect.test.ts +114 -0
  82. package/templates/docker/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
  83. package/templates/docker/src/lib/__tests__/agent-integration.test.ts +514 -0
  84. package/templates/docker/src/lib/__tests__/config-actions.test.ts +166 -0
  85. package/templates/docker/src/lib/__tests__/config.test.ts +1063 -0
  86. package/templates/docker/src/lib/__tests__/conversations.test.ts +589 -0
  87. package/templates/docker/src/lib/__tests__/errors.test.ts +256 -0
  88. package/templates/docker/src/lib/__tests__/logger.test.ts +200 -0
  89. package/templates/docker/src/lib/__tests__/providers.test.ts +99 -0
  90. package/templates/docker/src/lib/__tests__/rls.test.ts +435 -0
  91. package/templates/docker/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
  92. package/templates/docker/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
  93. package/templates/docker/src/lib/__tests__/semantic-index.test.ts +547 -0
  94. package/templates/docker/src/lib/__tests__/semantic-multisource.test.ts +544 -0
  95. package/templates/docker/src/lib/__tests__/semantic.test.ts +363 -0
  96. package/templates/docker/src/lib/__tests__/startup-actions.test.ts +452 -0
  97. package/templates/docker/src/lib/__tests__/startup.test.ts +465 -0
  98. package/templates/docker/src/lib/__tests__/tracing.test.ts +28 -0
  99. package/templates/docker/src/lib/action-types.ts +95 -0
  100. package/templates/docker/src/lib/agent-query.ts +178 -0
  101. package/templates/docker/src/lib/agent.ts +505 -0
  102. package/templates/docker/src/lib/api-url.ts +2 -0
  103. package/templates/docker/src/lib/auth/__tests__/audit.test.ts +418 -0
  104. package/templates/docker/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
  105. package/templates/docker/src/lib/auth/__tests__/byot.test.ts +366 -0
  106. package/templates/docker/src/lib/auth/__tests__/detect.test.ts +190 -0
  107. package/templates/docker/src/lib/auth/__tests__/managed.test.ts +173 -0
  108. package/templates/docker/src/lib/auth/__tests__/middleware.test.ts +456 -0
  109. package/templates/docker/src/lib/auth/__tests__/migrate.test.ts +201 -0
  110. package/templates/docker/src/lib/auth/__tests__/permissions.test.ts +225 -0
  111. package/templates/docker/src/lib/auth/__tests__/server.test.ts +34 -0
  112. package/templates/docker/src/lib/auth/__tests__/simple-key.test.ts +176 -0
  113. package/templates/docker/src/lib/auth/__tests__/types.test.ts +44 -0
  114. package/templates/docker/src/lib/auth/audit.ts +89 -0
  115. package/templates/docker/src/lib/auth/byot.ts +158 -0
  116. package/templates/docker/src/lib/auth/client.ts +35 -0
  117. package/templates/docker/src/lib/auth/detect.ts +83 -0
  118. package/templates/docker/src/lib/auth/managed.ts +73 -0
  119. package/templates/docker/src/lib/auth/middleware.ts +208 -0
  120. package/templates/docker/src/lib/auth/migrate.ts +111 -0
  121. package/templates/docker/src/lib/auth/permissions.ts +156 -0
  122. package/templates/docker/src/lib/auth/server.ts +142 -0
  123. package/templates/docker/src/lib/auth/simple-key.ts +92 -0
  124. package/templates/docker/src/lib/auth/types.ts +49 -0
  125. package/templates/docker/src/lib/config.ts +704 -0
  126. package/templates/docker/src/lib/conversation-types.ts +29 -0
  127. package/templates/docker/src/lib/conversations.ts +270 -0
  128. package/templates/docker/src/lib/db/__tests__/connection.test.ts +69 -0
  129. package/templates/docker/src/lib/db/__tests__/duckdb.test.ts +141 -0
  130. package/templates/docker/src/lib/db/__tests__/internal.test.ts +387 -0
  131. package/templates/docker/src/lib/db/__tests__/registry-health.test.ts +207 -0
  132. package/templates/docker/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
  133. package/templates/docker/src/lib/db/__tests__/registry.test.ts +595 -0
  134. package/templates/docker/src/lib/db/__tests__/salesforce.test.ts +339 -0
  135. package/templates/docker/src/lib/db/__tests__/snowflake.test.ts +217 -0
  136. package/templates/docker/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
  137. package/templates/docker/src/lib/db/connection.ts +753 -0
  138. package/templates/docker/src/lib/db/duckdb.ts +122 -0
  139. package/templates/docker/src/lib/db/internal.ts +273 -0
  140. package/templates/docker/src/lib/db/salesforce.ts +342 -0
  141. package/templates/docker/src/lib/db/source-rate-limit.ts +191 -0
  142. package/templates/docker/src/lib/errors.ts +154 -0
  143. package/templates/docker/src/lib/logger.ts +98 -0
  144. package/templates/docker/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
  145. package/templates/docker/src/lib/plugins/__tests__/hooks.test.ts +529 -0
  146. package/templates/docker/src/lib/plugins/__tests__/migrate.test.ts +521 -0
  147. package/templates/docker/src/lib/plugins/__tests__/registry.test.ts +346 -0
  148. package/templates/docker/src/lib/plugins/__tests__/tools.test.ts +49 -0
  149. package/templates/docker/src/lib/plugins/__tests__/wiring.test.ts +585 -0
  150. package/templates/docker/src/lib/plugins/hooks.ts +162 -0
  151. package/templates/docker/src/lib/plugins/index.ts +9 -0
  152. package/templates/docker/src/lib/plugins/migrate.ts +309 -0
  153. package/templates/docker/src/lib/plugins/registry.ts +231 -0
  154. package/templates/docker/src/lib/plugins/tools.ts +39 -0
  155. package/templates/docker/src/lib/plugins/wiring.ts +291 -0
  156. package/templates/docker/src/lib/providers.ts +102 -0
  157. package/templates/docker/src/lib/rls.ts +321 -0
  158. package/templates/docker/src/lib/scheduled-task-types.ts +132 -0
  159. package/templates/docker/src/lib/scheduled-tasks.ts +475 -0
  160. package/templates/docker/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
  161. package/templates/docker/src/lib/scheduler/__tests__/engine.test.ts +248 -0
  162. package/templates/docker/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
  163. package/templates/docker/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
  164. package/templates/docker/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
  165. package/templates/docker/src/lib/scheduler/delivery.ts +248 -0
  166. package/templates/docker/src/lib/scheduler/engine.ts +317 -0
  167. package/templates/docker/src/lib/scheduler/executor.ts +73 -0
  168. package/templates/docker/src/lib/scheduler/format-email.ts +109 -0
  169. package/templates/docker/src/lib/scheduler/format-slack.ts +35 -0
  170. package/templates/docker/src/lib/scheduler/format-webhook.ts +37 -0
  171. package/templates/docker/src/lib/scheduler/index.ts +7 -0
  172. package/templates/docker/src/lib/security.ts +11 -0
  173. package/templates/docker/src/lib/semantic-index.ts +503 -0
  174. package/templates/docker/src/lib/semantic.ts +387 -0
  175. package/templates/docker/src/lib/sidecar-types.ts +16 -0
  176. package/templates/docker/src/lib/slack/__tests__/api.test.ts +160 -0
  177. package/templates/docker/src/lib/slack/__tests__/format.test.ts +237 -0
  178. package/templates/docker/src/lib/slack/__tests__/store.test.ts +188 -0
  179. package/templates/docker/src/lib/slack/__tests__/threads.test.ts +112 -0
  180. package/templates/docker/src/lib/slack/__tests__/verify.test.ts +111 -0
  181. package/templates/docker/src/lib/slack/api.ts +102 -0
  182. package/templates/docker/src/lib/slack/format.ts +209 -0
  183. package/templates/docker/src/lib/slack/store.ts +107 -0
  184. package/templates/docker/src/lib/slack/threads.ts +64 -0
  185. package/templates/docker/src/lib/slack/verify.ts +71 -0
  186. package/templates/docker/src/lib/startup.ts +730 -0
  187. package/templates/docker/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
  188. package/templates/docker/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
  189. package/templates/docker/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
  190. package/templates/docker/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
  191. package/templates/docker/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
  192. package/templates/docker/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
  193. package/templates/docker/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
  194. package/templates/docker/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
  195. package/templates/docker/src/lib/tools/__tests__/registry.test.ts +235 -0
  196. package/templates/docker/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
  197. package/templates/docker/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
  198. package/templates/docker/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
  199. package/templates/docker/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
  200. package/templates/docker/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
  201. package/templates/docker/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
  202. package/templates/docker/src/lib/tools/__tests__/sql.test.ts +1012 -0
  203. package/templates/docker/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
  204. package/templates/docker/src/lib/tools/actions/__tests__/email.test.ts +378 -0
  205. package/templates/docker/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
  206. package/templates/docker/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
  207. package/templates/docker/src/lib/tools/actions/audit.ts +47 -0
  208. package/templates/docker/src/lib/tools/actions/email.ts +191 -0
  209. package/templates/docker/src/lib/tools/actions/handler.ts +591 -0
  210. package/templates/docker/src/lib/tools/actions/index.ts +23 -0
  211. package/templates/docker/src/lib/tools/actions/jira.ts +220 -0
  212. package/templates/docker/src/lib/tools/explore-nsjail.ts +343 -0
  213. package/templates/docker/src/lib/tools/explore-sandbox.ts +264 -0
  214. package/templates/docker/src/lib/tools/explore-sidecar.ts +163 -0
  215. package/templates/docker/src/lib/tools/explore.ts +379 -0
  216. package/templates/docker/src/lib/tools/registry.ts +221 -0
  217. package/templates/docker/src/lib/tools/salesforce.ts +138 -0
  218. package/templates/docker/src/lib/tools/soql-validation.ts +172 -0
  219. package/templates/docker/src/lib/tools/sql.ts +680 -0
  220. package/templates/docker/src/lib/tracing.ts +40 -0
  221. package/templates/docker/src/lib/utils.ts +6 -0
  222. package/templates/docker/src/test-setup.ts +38 -0
  223. package/templates/docker/src/types/vercel-sandbox.d.ts +54 -0
  224. package/templates/docker/src/ui/components/actions/action-approval-card.tsx +295 -0
  225. package/templates/docker/src/ui/components/actions/action-status-badge.tsx +50 -0
  226. package/templates/docker/src/ui/components/admin/admin-layout.tsx +26 -0
  227. package/templates/docker/src/ui/components/admin/admin-sidebar.tsx +96 -0
  228. package/templates/docker/src/ui/components/admin/empty-state.tsx +24 -0
  229. package/templates/docker/src/ui/components/admin/entity-detail.tsx +233 -0
  230. package/templates/docker/src/ui/components/admin/entity-list.tsx +96 -0
  231. package/templates/docker/src/ui/components/admin/error-banner.tsx +22 -0
  232. package/templates/docker/src/ui/components/admin/feature-disabled.tsx +44 -0
  233. package/templates/docker/src/ui/components/admin/health-badge.tsx +30 -0
  234. package/templates/docker/src/ui/components/admin/loading-state.tsx +14 -0
  235. package/templates/docker/src/ui/components/admin/stat-card.tsx +32 -0
  236. package/templates/docker/src/ui/components/atlas-chat.tsx +370 -0
  237. package/templates/docker/src/ui/components/chart/chart-detection.ts +261 -0
  238. package/templates/docker/src/ui/components/chart/result-chart.tsx +375 -0
  239. package/templates/docker/src/ui/components/chat/api-key-bar.tsx +66 -0
  240. package/templates/docker/src/ui/components/chat/copy-button.tsx +25 -0
  241. package/templates/docker/src/ui/components/chat/data-table.tsx +102 -0
  242. package/templates/docker/src/ui/components/chat/error-banner.tsx +32 -0
  243. package/templates/docker/src/ui/components/chat/explore-card.tsx +41 -0
  244. package/templates/docker/src/ui/components/chat/loading-card.tsx +10 -0
  245. package/templates/docker/src/ui/components/chat/managed-auth-card.tsx +116 -0
  246. package/templates/docker/src/ui/components/chat/markdown.tsx +72 -0
  247. package/templates/docker/src/ui/components/chat/sql-block.tsx +30 -0
  248. package/templates/docker/src/ui/components/chat/sql-result-card.tsx +144 -0
  249. package/templates/docker/src/ui/components/chat/starter-prompts.ts +6 -0
  250. package/templates/docker/src/ui/components/chat/tool-part.tsx +40 -0
  251. package/templates/docker/src/ui/components/chat/typing-indicator.tsx +19 -0
  252. package/templates/docker/src/ui/components/conversations/conversation-item.tsx +120 -0
  253. package/templates/docker/src/ui/components/conversations/conversation-list.tsx +66 -0
  254. package/templates/docker/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
  255. package/templates/docker/src/ui/components/conversations/delete-confirmation.tsx +27 -0
  256. package/templates/docker/src/ui/context.tsx +78 -0
  257. package/templates/docker/src/ui/hooks/use-admin-fetch.ts +104 -0
  258. package/templates/docker/src/ui/hooks/use-conversations.ts +184 -0
  259. package/templates/docker/src/ui/hooks/use-dark-mode.ts +17 -0
  260. package/templates/docker/src/ui/lib/action-types.ts +63 -0
  261. package/templates/docker/src/ui/lib/helpers.ts +104 -0
  262. package/templates/docker/src/ui/lib/types.ts +145 -0
  263. package/templates/docker/tsconfig.json +41 -0
  264. package/templates/docker/vercel.json +3 -0
  265. package/templates/nextjs-standalone/.env.example +68 -0
  266. package/templates/nextjs-standalone/bin/__tests__/benchmark.test.ts +598 -0
  267. package/templates/nextjs-standalone/bin/__tests__/duckdb-ingest.test.ts +171 -0
  268. package/templates/nextjs-standalone/bin/__tests__/eval.test.ts +434 -0
  269. package/templates/nextjs-standalone/bin/__tests__/matview-partition.test.ts +615 -0
  270. package/templates/nextjs-standalone/bin/__tests__/multi-source.test.ts +113 -0
  271. package/templates/nextjs-standalone/bin/__tests__/plugin-cli.test.ts +322 -0
  272. package/templates/nextjs-standalone/bin/__tests__/profiler-heuristics.test.ts +608 -0
  273. package/templates/nextjs-standalone/bin/__tests__/query.test.ts +240 -0
  274. package/templates/nextjs-standalone/bin/__tests__/schema-drift.test.ts +542 -0
  275. package/templates/nextjs-standalone/bin/__tests__/view-yaml-generation.test.ts +146 -0
  276. package/templates/nextjs-standalone/bin/atlas.ts +5044 -0
  277. package/templates/nextjs-standalone/bin/benchmark.ts +695 -0
  278. package/templates/nextjs-standalone/bin/enrich.ts +559 -0
  279. package/templates/nextjs-standalone/bin/eval.ts +770 -0
  280. package/templates/nextjs-standalone/bin/smoke.ts +438 -0
  281. package/templates/nextjs-standalone/data/.gitkeep +0 -0
  282. package/templates/nextjs-standalone/data/cybersec.sql +1961 -0
  283. package/templates/nextjs-standalone/data/demo-semantic/catalog.yml +40 -0
  284. package/templates/nextjs-standalone/data/demo-semantic/entities/accounts.yml +170 -0
  285. package/templates/nextjs-standalone/data/demo-semantic/entities/companies.yml +207 -0
  286. package/templates/nextjs-standalone/data/demo-semantic/entities/people.yml +145 -0
  287. package/templates/nextjs-standalone/data/demo-semantic/glossary.yml +22 -0
  288. package/templates/nextjs-standalone/data/demo-semantic/metrics/accounts.yml +38 -0
  289. package/templates/nextjs-standalone/data/demo-semantic/metrics/companies.yml +89 -0
  290. package/templates/nextjs-standalone/data/demo.sql +373 -0
  291. package/templates/nextjs-standalone/data/ecommerce.sql +1690 -0
  292. package/templates/nextjs-standalone/data/init-demo-db.sql +8 -0
  293. package/templates/nextjs-standalone/docs/deploy.md +390 -0
  294. package/templates/nextjs-standalone/eslint.config.mjs +18 -0
  295. package/templates/nextjs-standalone/gitignore +5 -0
  296. package/templates/nextjs-standalone/next.config.ts +10 -0
  297. package/templates/nextjs-standalone/package.json +63 -0
  298. package/templates/nextjs-standalone/postcss.config.mjs +8 -0
  299. package/templates/nextjs-standalone/semantic/catalog.yml +5 -0
  300. package/templates/nextjs-standalone/semantic/entities/.gitkeep +0 -0
  301. package/templates/nextjs-standalone/semantic/glossary.yml +6 -0
  302. package/templates/nextjs-standalone/semantic/metrics/.gitkeep +0 -0
  303. package/templates/nextjs-standalone/src/api/__tests__/actions.test.ts +683 -0
  304. package/templates/nextjs-standalone/src/api/__tests__/admin.test.ts +820 -0
  305. package/templates/nextjs-standalone/src/api/__tests__/auth.test.ts +165 -0
  306. package/templates/nextjs-standalone/src/api/__tests__/chat.test.ts +376 -0
  307. package/templates/nextjs-standalone/src/api/__tests__/conversations.test.ts +555 -0
  308. package/templates/nextjs-standalone/src/api/__tests__/cors.test.ts +135 -0
  309. package/templates/nextjs-standalone/src/api/__tests__/health-plugin.test.ts +169 -0
  310. package/templates/nextjs-standalone/src/api/__tests__/health.test.ts +261 -0
  311. package/templates/nextjs-standalone/src/api/__tests__/query.test.ts +891 -0
  312. package/templates/nextjs-standalone/src/api/__tests__/scheduled-tasks.test.ts +601 -0
  313. package/templates/nextjs-standalone/src/api/__tests__/slack.test.ts +847 -0
  314. package/templates/nextjs-standalone/src/api/index.ts +117 -0
  315. package/templates/nextjs-standalone/src/api/routes/actions.ts +274 -0
  316. package/templates/nextjs-standalone/src/api/routes/admin.ts +757 -0
  317. package/templates/nextjs-standalone/src/api/routes/auth.ts +48 -0
  318. package/templates/nextjs-standalone/src/api/routes/chat.ts +465 -0
  319. package/templates/nextjs-standalone/src/api/routes/conversations.ts +266 -0
  320. package/templates/nextjs-standalone/src/api/routes/health.ts +287 -0
  321. package/templates/nextjs-standalone/src/api/routes/openapi.ts +390 -0
  322. package/templates/nextjs-standalone/src/api/routes/query.ts +318 -0
  323. package/templates/nextjs-standalone/src/api/routes/scheduled-tasks.ts +467 -0
  324. package/templates/nextjs-standalone/src/api/routes/slack.ts +611 -0
  325. package/templates/nextjs-standalone/src/api/server.ts +226 -0
  326. package/templates/nextjs-standalone/src/app/api/[...route]/route.ts +33 -0
  327. package/templates/nextjs-standalone/src/app/error.tsx +24 -0
  328. package/templates/nextjs-standalone/src/app/global-error.tsx +68 -0
  329. package/templates/nextjs-standalone/src/app/globals.css +126 -0
  330. package/templates/nextjs-standalone/src/app/layout.tsx +19 -0
  331. package/templates/nextjs-standalone/src/app/page.tsx +14 -0
  332. package/templates/nextjs-standalone/src/lib/__tests__/agent-cache.test.ts +437 -0
  333. package/templates/nextjs-standalone/src/lib/__tests__/agent-dialect.test.ts +114 -0
  334. package/templates/nextjs-standalone/src/lib/__tests__/agent-health-annotations.test.ts +164 -0
  335. package/templates/nextjs-standalone/src/lib/__tests__/agent-integration.test.ts +514 -0
  336. package/templates/nextjs-standalone/src/lib/__tests__/config-actions.test.ts +166 -0
  337. package/templates/nextjs-standalone/src/lib/__tests__/config.test.ts +1063 -0
  338. package/templates/nextjs-standalone/src/lib/__tests__/conversations.test.ts +589 -0
  339. package/templates/nextjs-standalone/src/lib/__tests__/errors.test.ts +256 -0
  340. package/templates/nextjs-standalone/src/lib/__tests__/logger.test.ts +200 -0
  341. package/templates/nextjs-standalone/src/lib/__tests__/providers.test.ts +99 -0
  342. package/templates/nextjs-standalone/src/lib/__tests__/rls.test.ts +435 -0
  343. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-task-types.test.ts +124 -0
  344. package/templates/nextjs-standalone/src/lib/__tests__/scheduled-tasks.test.ts +550 -0
  345. package/templates/nextjs-standalone/src/lib/__tests__/semantic-index.test.ts +547 -0
  346. package/templates/nextjs-standalone/src/lib/__tests__/semantic-multisource.test.ts +544 -0
  347. package/templates/nextjs-standalone/src/lib/__tests__/semantic.test.ts +363 -0
  348. package/templates/nextjs-standalone/src/lib/__tests__/startup-actions.test.ts +452 -0
  349. package/templates/nextjs-standalone/src/lib/__tests__/startup.test.ts +465 -0
  350. package/templates/nextjs-standalone/src/lib/__tests__/tracing.test.ts +28 -0
  351. package/templates/nextjs-standalone/src/lib/action-types.ts +95 -0
  352. package/templates/nextjs-standalone/src/lib/agent-query.ts +178 -0
  353. package/templates/nextjs-standalone/src/lib/agent.ts +505 -0
  354. package/templates/nextjs-standalone/src/lib/api-url.ts +3 -0
  355. package/templates/nextjs-standalone/src/lib/auth/__tests__/audit.test.ts +418 -0
  356. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot-integration.test.ts +222 -0
  357. package/templates/nextjs-standalone/src/lib/auth/__tests__/byot.test.ts +366 -0
  358. package/templates/nextjs-standalone/src/lib/auth/__tests__/detect.test.ts +190 -0
  359. package/templates/nextjs-standalone/src/lib/auth/__tests__/managed.test.ts +173 -0
  360. package/templates/nextjs-standalone/src/lib/auth/__tests__/middleware.test.ts +456 -0
  361. package/templates/nextjs-standalone/src/lib/auth/__tests__/migrate.test.ts +201 -0
  362. package/templates/nextjs-standalone/src/lib/auth/__tests__/permissions.test.ts +225 -0
  363. package/templates/nextjs-standalone/src/lib/auth/__tests__/server.test.ts +34 -0
  364. package/templates/nextjs-standalone/src/lib/auth/__tests__/simple-key.test.ts +176 -0
  365. package/templates/nextjs-standalone/src/lib/auth/__tests__/types.test.ts +44 -0
  366. package/templates/nextjs-standalone/src/lib/auth/audit.ts +89 -0
  367. package/templates/nextjs-standalone/src/lib/auth/byot.ts +158 -0
  368. package/templates/nextjs-standalone/src/lib/auth/client.ts +23 -0
  369. package/templates/nextjs-standalone/src/lib/auth/detect.ts +83 -0
  370. package/templates/nextjs-standalone/src/lib/auth/managed.ts +73 -0
  371. package/templates/nextjs-standalone/src/lib/auth/middleware.ts +208 -0
  372. package/templates/nextjs-standalone/src/lib/auth/migrate.ts +111 -0
  373. package/templates/nextjs-standalone/src/lib/auth/permissions.ts +156 -0
  374. package/templates/nextjs-standalone/src/lib/auth/server.ts +142 -0
  375. package/templates/nextjs-standalone/src/lib/auth/simple-key.ts +92 -0
  376. package/templates/nextjs-standalone/src/lib/auth/types.ts +49 -0
  377. package/templates/nextjs-standalone/src/lib/config.ts +704 -0
  378. package/templates/nextjs-standalone/src/lib/conversation-types.ts +29 -0
  379. package/templates/nextjs-standalone/src/lib/conversations.ts +270 -0
  380. package/templates/nextjs-standalone/src/lib/db/__tests__/connection.test.ts +69 -0
  381. package/templates/nextjs-standalone/src/lib/db/__tests__/duckdb.test.ts +141 -0
  382. package/templates/nextjs-standalone/src/lib/db/__tests__/internal.test.ts +387 -0
  383. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-health.test.ts +207 -0
  384. package/templates/nextjs-standalone/src/lib/db/__tests__/registry-pool-limits.test.ts +156 -0
  385. package/templates/nextjs-standalone/src/lib/db/__tests__/registry.test.ts +595 -0
  386. package/templates/nextjs-standalone/src/lib/db/__tests__/salesforce.test.ts +339 -0
  387. package/templates/nextjs-standalone/src/lib/db/__tests__/snowflake.test.ts +217 -0
  388. package/templates/nextjs-standalone/src/lib/db/__tests__/source-rate-limit.test.ts +130 -0
  389. package/templates/nextjs-standalone/src/lib/db/connection.ts +753 -0
  390. package/templates/nextjs-standalone/src/lib/db/duckdb.ts +122 -0
  391. package/templates/nextjs-standalone/src/lib/db/internal.ts +273 -0
  392. package/templates/nextjs-standalone/src/lib/db/salesforce.ts +342 -0
  393. package/templates/nextjs-standalone/src/lib/db/source-rate-limit.ts +191 -0
  394. package/templates/nextjs-standalone/src/lib/errors.ts +154 -0
  395. package/templates/nextjs-standalone/src/lib/logger.ts +98 -0
  396. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks-integration.test.ts +202 -0
  397. package/templates/nextjs-standalone/src/lib/plugins/__tests__/hooks.test.ts +529 -0
  398. package/templates/nextjs-standalone/src/lib/plugins/__tests__/migrate.test.ts +521 -0
  399. package/templates/nextjs-standalone/src/lib/plugins/__tests__/registry.test.ts +346 -0
  400. package/templates/nextjs-standalone/src/lib/plugins/__tests__/tools.test.ts +49 -0
  401. package/templates/nextjs-standalone/src/lib/plugins/__tests__/wiring.test.ts +585 -0
  402. package/templates/nextjs-standalone/src/lib/plugins/hooks.ts +162 -0
  403. package/templates/nextjs-standalone/src/lib/plugins/index.ts +9 -0
  404. package/templates/nextjs-standalone/src/lib/plugins/migrate.ts +309 -0
  405. package/templates/nextjs-standalone/src/lib/plugins/registry.ts +231 -0
  406. package/templates/nextjs-standalone/src/lib/plugins/tools.ts +39 -0
  407. package/templates/nextjs-standalone/src/lib/plugins/wiring.ts +291 -0
  408. package/templates/nextjs-standalone/src/lib/providers.ts +102 -0
  409. package/templates/nextjs-standalone/src/lib/rls.ts +321 -0
  410. package/templates/nextjs-standalone/src/lib/scheduled-task-types.ts +132 -0
  411. package/templates/nextjs-standalone/src/lib/scheduled-tasks.ts +475 -0
  412. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/delivery.test.ts +192 -0
  413. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/engine.test.ts +248 -0
  414. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-email.test.ts +96 -0
  415. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-slack.test.ts +78 -0
  416. package/templates/nextjs-standalone/src/lib/scheduler/__tests__/format-webhook.test.ts +78 -0
  417. package/templates/nextjs-standalone/src/lib/scheduler/delivery.ts +248 -0
  418. package/templates/nextjs-standalone/src/lib/scheduler/engine.ts +317 -0
  419. package/templates/nextjs-standalone/src/lib/scheduler/executor.ts +73 -0
  420. package/templates/nextjs-standalone/src/lib/scheduler/format-email.ts +109 -0
  421. package/templates/nextjs-standalone/src/lib/scheduler/format-slack.ts +35 -0
  422. package/templates/nextjs-standalone/src/lib/scheduler/format-webhook.ts +37 -0
  423. package/templates/nextjs-standalone/src/lib/scheduler/index.ts +7 -0
  424. package/templates/nextjs-standalone/src/lib/security.ts +11 -0
  425. package/templates/nextjs-standalone/src/lib/semantic-index.ts +503 -0
  426. package/templates/nextjs-standalone/src/lib/semantic.ts +387 -0
  427. package/templates/nextjs-standalone/src/lib/sidecar-types.ts +16 -0
  428. package/templates/nextjs-standalone/src/lib/slack/__tests__/api.test.ts +160 -0
  429. package/templates/nextjs-standalone/src/lib/slack/__tests__/format.test.ts +237 -0
  430. package/templates/nextjs-standalone/src/lib/slack/__tests__/store.test.ts +188 -0
  431. package/templates/nextjs-standalone/src/lib/slack/__tests__/threads.test.ts +112 -0
  432. package/templates/nextjs-standalone/src/lib/slack/__tests__/verify.test.ts +111 -0
  433. package/templates/nextjs-standalone/src/lib/slack/api.ts +102 -0
  434. package/templates/nextjs-standalone/src/lib/slack/format.ts +209 -0
  435. package/templates/nextjs-standalone/src/lib/slack/store.ts +107 -0
  436. package/templates/nextjs-standalone/src/lib/slack/threads.ts +64 -0
  437. package/templates/nextjs-standalone/src/lib/slack/verify.ts +71 -0
  438. package/templates/nextjs-standalone/src/lib/startup.ts +730 -0
  439. package/templates/nextjs-standalone/src/lib/tools/__tests__/action-permissions.test.ts +594 -0
  440. package/templates/nextjs-standalone/src/lib/tools/__tests__/custom-validation.test.ts +238 -0
  441. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-backend.test.ts +267 -0
  442. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-nsjail.test.ts +492 -0
  443. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-plugin.test.ts +374 -0
  444. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sdk-compat.test.ts +82 -0
  445. package/templates/nextjs-standalone/src/lib/tools/__tests__/explore-sidecar.test.ts +208 -0
  446. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry-actions.test.ts +144 -0
  447. package/templates/nextjs-standalone/src/lib/tools/__tests__/registry.test.ts +235 -0
  448. package/templates/nextjs-standalone/src/lib/tools/__tests__/salesforce-tool.test.ts +154 -0
  449. package/templates/nextjs-standalone/src/lib/tools/__tests__/soql-validation.test.ts +303 -0
  450. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-audit.test.ts +225 -0
  451. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-connection-whitelist.test.ts +98 -0
  452. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-duckdb.test.ts +233 -0
  453. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql-ratelimit.test.ts +225 -0
  454. package/templates/nextjs-standalone/src/lib/tools/__tests__/sql.test.ts +1012 -0
  455. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/audit.test.ts +211 -0
  456. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/email.test.ts +378 -0
  457. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/handler.test.ts +681 -0
  458. package/templates/nextjs-standalone/src/lib/tools/actions/__tests__/jira.test.ts +427 -0
  459. package/templates/nextjs-standalone/src/lib/tools/actions/audit.ts +47 -0
  460. package/templates/nextjs-standalone/src/lib/tools/actions/email.ts +191 -0
  461. package/templates/nextjs-standalone/src/lib/tools/actions/handler.ts +591 -0
  462. package/templates/nextjs-standalone/src/lib/tools/actions/index.ts +23 -0
  463. package/templates/nextjs-standalone/src/lib/tools/actions/jira.ts +220 -0
  464. package/templates/nextjs-standalone/src/lib/tools/explore-nsjail.ts +343 -0
  465. package/templates/nextjs-standalone/src/lib/tools/explore-sandbox.ts +264 -0
  466. package/templates/nextjs-standalone/src/lib/tools/explore-sidecar.ts +163 -0
  467. package/templates/nextjs-standalone/src/lib/tools/explore.ts +379 -0
  468. package/templates/nextjs-standalone/src/lib/tools/registry.ts +221 -0
  469. package/templates/nextjs-standalone/src/lib/tools/salesforce.ts +138 -0
  470. package/templates/nextjs-standalone/src/lib/tools/soql-validation.ts +172 -0
  471. package/templates/nextjs-standalone/src/lib/tools/sql.ts +680 -0
  472. package/templates/nextjs-standalone/src/lib/tracing.ts +40 -0
  473. package/templates/nextjs-standalone/src/lib/utils.ts +6 -0
  474. package/templates/nextjs-standalone/src/test-setup.ts +38 -0
  475. package/templates/nextjs-standalone/src/ui/components/actions/action-approval-card.tsx +295 -0
  476. package/templates/nextjs-standalone/src/ui/components/actions/action-status-badge.tsx +50 -0
  477. package/templates/nextjs-standalone/src/ui/components/admin/admin-layout.tsx +26 -0
  478. package/templates/nextjs-standalone/src/ui/components/admin/admin-sidebar.tsx +96 -0
  479. package/templates/nextjs-standalone/src/ui/components/admin/empty-state.tsx +24 -0
  480. package/templates/nextjs-standalone/src/ui/components/admin/entity-detail.tsx +233 -0
  481. package/templates/nextjs-standalone/src/ui/components/admin/entity-list.tsx +96 -0
  482. package/templates/nextjs-standalone/src/ui/components/admin/error-banner.tsx +22 -0
  483. package/templates/nextjs-standalone/src/ui/components/admin/feature-disabled.tsx +44 -0
  484. package/templates/nextjs-standalone/src/ui/components/admin/health-badge.tsx +30 -0
  485. package/templates/nextjs-standalone/src/ui/components/admin/loading-state.tsx +14 -0
  486. package/templates/nextjs-standalone/src/ui/components/admin/stat-card.tsx +32 -0
  487. package/templates/nextjs-standalone/src/ui/components/atlas-chat.tsx +370 -0
  488. package/templates/nextjs-standalone/src/ui/components/chart/chart-detection.ts +261 -0
  489. package/templates/nextjs-standalone/src/ui/components/chart/result-chart.tsx +375 -0
  490. package/templates/nextjs-standalone/src/ui/components/chat/api-key-bar.tsx +66 -0
  491. package/templates/nextjs-standalone/src/ui/components/chat/copy-button.tsx +25 -0
  492. package/templates/nextjs-standalone/src/ui/components/chat/data-table.tsx +102 -0
  493. package/templates/nextjs-standalone/src/ui/components/chat/error-banner.tsx +32 -0
  494. package/templates/nextjs-standalone/src/ui/components/chat/explore-card.tsx +41 -0
  495. package/templates/nextjs-standalone/src/ui/components/chat/loading-card.tsx +10 -0
  496. package/templates/nextjs-standalone/src/ui/components/chat/managed-auth-card.tsx +116 -0
  497. package/templates/nextjs-standalone/src/ui/components/chat/markdown.tsx +72 -0
  498. package/templates/nextjs-standalone/src/ui/components/chat/sql-block.tsx +30 -0
  499. package/templates/nextjs-standalone/src/ui/components/chat/sql-result-card.tsx +144 -0
  500. package/templates/nextjs-standalone/src/ui/components/chat/starter-prompts.ts +6 -0
  501. package/templates/nextjs-standalone/src/ui/components/chat/tool-part.tsx +40 -0
  502. package/templates/nextjs-standalone/src/ui/components/chat/typing-indicator.tsx +19 -0
  503. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-item.tsx +120 -0
  504. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-list.tsx +66 -0
  505. package/templates/nextjs-standalone/src/ui/components/conversations/conversation-sidebar.tsx +78 -0
  506. package/templates/nextjs-standalone/src/ui/components/conversations/delete-confirmation.tsx +27 -0
  507. package/templates/nextjs-standalone/src/ui/context.tsx +78 -0
  508. package/templates/nextjs-standalone/src/ui/hooks/use-admin-fetch.ts +104 -0
  509. package/templates/nextjs-standalone/src/ui/hooks/use-conversations.ts +184 -0
  510. package/templates/nextjs-standalone/src/ui/hooks/use-dark-mode.ts +17 -0
  511. package/templates/nextjs-standalone/src/ui/lib/action-types.ts +63 -0
  512. package/templates/nextjs-standalone/src/ui/lib/helpers.ts +104 -0
  513. package/templates/nextjs-standalone/src/ui/lib/types.ts +145 -0
  514. package/templates/nextjs-standalone/tsconfig.json +32 -0
  515. package/templates/nextjs-standalone/vercel.json +4 -0
@@ -0,0 +1,475 @@
1
+ /**
2
+ * Scheduled task persistence — CRUD operations for scheduled tasks and runs.
3
+ *
4
+ * Pattern follows conversations.ts: hasInternalDB() guard, CrudResult/CrudDataResult
5
+ * discriminated unions, fire-and-forget for non-critical writes.
6
+ */
7
+
8
+ import { Cron } from "croner";
9
+ import { createLogger } from "@atlas/api/lib/logger";
10
+ import {
11
+ hasInternalDB,
12
+ internalQuery,
13
+ internalExecute,
14
+ } from "@atlas/api/lib/db/internal";
15
+ import type {
16
+ ScheduledTask,
17
+ ScheduledTaskRun,
18
+ ScheduledTaskWithRuns,
19
+ DeliveryChannel,
20
+ Recipient,
21
+ RunStatus,
22
+ } from "@atlas/api/lib/scheduled-task-types";
23
+ import { isRecipient } from "@atlas/api/lib/scheduled-task-types";
24
+ import type { ActionApprovalMode } from "@atlas/api/lib/action-types";
25
+
26
+ const log = createLogger("scheduled-tasks");
27
+
28
+ // Re-export types for consumers
29
+ export type { ScheduledTask, ScheduledTaskRun, ScheduledTaskWithRuns };
30
+
31
+ // ---------------------------------------------------------------------------
32
+ // Types
33
+ // ---------------------------------------------------------------------------
34
+
35
+ import type { CrudResult, CrudDataResult, CrudFailReason } from "@atlas/api/lib/conversations";
36
+ export type { CrudResult, CrudDataResult, CrudFailReason };
37
+
38
+ // ---------------------------------------------------------------------------
39
+ // Helpers
40
+ // ---------------------------------------------------------------------------
41
+
42
+ function rowToScheduledTask(r: Record<string, unknown>): ScheduledTask {
43
+ let recipients: Recipient[] = [];
44
+ try {
45
+ const raw = typeof r.recipients === "string" ? JSON.parse(r.recipients) : r.recipients;
46
+ if (Array.isArray(raw)) {
47
+ recipients = raw.filter(isRecipient);
48
+ if (recipients.length < raw.length) {
49
+ log.warn({ taskId: r.id, total: raw.length, valid: recipients.length }, "Some recipients failed validation — dropped invalid entries");
50
+ }
51
+ } else {
52
+ log.warn({ taskId: r.id, recipientsType: typeof raw }, "recipients column is not an array — defaulting to empty");
53
+ }
54
+ } catch (err) {
55
+ log.error(
56
+ { taskId: r.id, err: err instanceof Error ? err.message : String(err) },
57
+ "Failed to parse recipients JSONB — task will have no delivery targets",
58
+ );
59
+ }
60
+
61
+ return {
62
+ id: r.id as string,
63
+ ownerId: r.owner_id as string,
64
+ name: r.name as string,
65
+ question: r.question as string,
66
+ cronExpression: r.cron_expression as string,
67
+ deliveryChannel: (r.delivery_channel as DeliveryChannel) ?? "webhook",
68
+ recipients,
69
+ connectionId: (r.connection_id as string) ?? null,
70
+ approvalMode: (r.approval_mode as ActionApprovalMode) ?? "auto",
71
+ enabled: r.enabled === true,
72
+ lastRunAt: r.last_run_at ? String(r.last_run_at) : null,
73
+ nextRunAt: r.next_run_at ? String(r.next_run_at) : null,
74
+ createdAt: String(r.created_at),
75
+ updatedAt: String(r.updated_at),
76
+ };
77
+ }
78
+
79
+ function rowToScheduledTaskRun(r: Record<string, unknown>): ScheduledTaskRun {
80
+ return {
81
+ id: r.id as string,
82
+ taskId: r.task_id as string,
83
+ startedAt: String(r.started_at),
84
+ completedAt: r.completed_at ? String(r.completed_at) : null,
85
+ status: (r.status as RunStatus) ?? "running",
86
+ conversationId: (r.conversation_id as string) ?? null,
87
+ actionId: (r.action_id as string) ?? null,
88
+ error: (r.error as string) ?? null,
89
+ tokensUsed: typeof r.tokens_used === "number" ? r.tokens_used : null,
90
+ createdAt: String(r.created_at),
91
+ };
92
+ }
93
+
94
+ // ---------------------------------------------------------------------------
95
+ // Cron helpers
96
+ // ---------------------------------------------------------------------------
97
+
98
+ /** Validate a cron expression. Returns { valid: true } or { valid: false, error }. */
99
+ export function validateCronExpression(expr: string): { valid: boolean; error?: string } {
100
+ try {
101
+ // Croner validates on construction — dispose immediately
102
+ const job = new Cron(expr, { paused: true }, () => {});
103
+ job.stop();
104
+ return { valid: true };
105
+ } catch (err) {
106
+ return { valid: false, error: err instanceof Error ? err.message : String(err) };
107
+ }
108
+ }
109
+
110
+ /** Compute the next run time for a cron expression. */
111
+ export function computeNextRun(expr: string, after?: Date): Date | null {
112
+ try {
113
+ const job = new Cron(expr, { paused: true }, () => {});
114
+ const next = job.nextRun(after ?? new Date());
115
+ job.stop();
116
+ return next;
117
+ } catch (err) {
118
+ log.warn(
119
+ { cronExpression: expr, err: err instanceof Error ? err.message : String(err) },
120
+ "Failed to compute next run time — task will not be scheduled",
121
+ );
122
+ return null;
123
+ }
124
+ }
125
+
126
+ // ---------------------------------------------------------------------------
127
+ // CRUD — Tasks
128
+ // ---------------------------------------------------------------------------
129
+
130
+ export async function createScheduledTask(opts: {
131
+ ownerId: string;
132
+ name: string;
133
+ question: string;
134
+ cronExpression: string;
135
+ deliveryChannel?: DeliveryChannel;
136
+ recipients?: Recipient[];
137
+ connectionId?: string | null;
138
+ approvalMode?: ActionApprovalMode;
139
+ }): Promise<CrudDataResult<ScheduledTask>> {
140
+ if (!hasInternalDB()) return { ok: false, reason: "no_db" };
141
+
142
+ const validation = validateCronExpression(opts.cronExpression);
143
+ if (!validation.valid) {
144
+ log.warn({ cronExpression: opts.cronExpression, error: validation.error }, "Invalid cron expression rejected in createScheduledTask");
145
+ return { ok: false, reason: "error" };
146
+ }
147
+
148
+ const nextRun = computeNextRun(opts.cronExpression);
149
+
150
+ try {
151
+ const rows = await internalQuery<Record<string, unknown>>(
152
+ `INSERT INTO scheduled_tasks (owner_id, name, question, cron_expression, delivery_channel, recipients, connection_id, approval_mode, next_run_at)
153
+ VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
154
+ RETURNING *`,
155
+ [
156
+ opts.ownerId,
157
+ opts.name,
158
+ opts.question,
159
+ opts.cronExpression,
160
+ opts.deliveryChannel ?? "webhook",
161
+ JSON.stringify(opts.recipients ?? []),
162
+ opts.connectionId ?? null,
163
+ opts.approvalMode ?? "auto",
164
+ nextRun?.toISOString() ?? null,
165
+ ],
166
+ );
167
+ if (rows.length === 0) return { ok: false, reason: "error" };
168
+ return { ok: true, data: rowToScheduledTask(rows[0]) };
169
+ } catch (err) {
170
+ log.error({ err: err instanceof Error ? err.message : String(err) }, "createScheduledTask failed");
171
+ return { ok: false, reason: "error" };
172
+ }
173
+ }
174
+
175
+ export async function getScheduledTask(
176
+ id: string,
177
+ ownerId?: string,
178
+ ): Promise<CrudDataResult<ScheduledTask>> {
179
+ if (!hasInternalDB()) return { ok: false, reason: "no_db" };
180
+ try {
181
+ const rows = ownerId
182
+ ? await internalQuery<Record<string, unknown>>(
183
+ `SELECT * FROM scheduled_tasks WHERE id = $1 AND owner_id = $2`,
184
+ [id, ownerId],
185
+ )
186
+ : await internalQuery<Record<string, unknown>>(
187
+ `SELECT * FROM scheduled_tasks WHERE id = $1`,
188
+ [id],
189
+ );
190
+ if (rows.length === 0) return { ok: false, reason: "not_found" };
191
+ return { ok: true, data: rowToScheduledTask(rows[0]) };
192
+ } catch (err) {
193
+ log.error({ err: err instanceof Error ? err.message : String(err) }, "getScheduledTask failed");
194
+ return { ok: false, reason: "error" };
195
+ }
196
+ }
197
+
198
+ export async function listScheduledTasks(opts?: {
199
+ ownerId?: string;
200
+ enabled?: boolean;
201
+ limit?: number;
202
+ offset?: number;
203
+ }): Promise<{ tasks: ScheduledTask[]; total: number }> {
204
+ const empty = { tasks: [], total: 0 };
205
+ if (!hasInternalDB()) return empty;
206
+
207
+ const limit = opts?.limit ?? 20;
208
+ const offset = opts?.offset ?? 0;
209
+
210
+ try {
211
+ const conditions: string[] = [];
212
+ const params: unknown[] = [];
213
+ let paramIdx = 1;
214
+
215
+ if (opts?.ownerId) {
216
+ conditions.push(`owner_id = $${paramIdx++}`);
217
+ params.push(opts.ownerId);
218
+ }
219
+ if (opts?.enabled !== undefined) {
220
+ conditions.push(`enabled = $${paramIdx++}`);
221
+ params.push(opts.enabled);
222
+ }
223
+
224
+ const where = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
225
+
226
+ const countRows = await internalQuery<Record<string, unknown>>(
227
+ `SELECT COUNT(*)::int AS total FROM scheduled_tasks ${where}`,
228
+ params,
229
+ );
230
+ const dataRows = await internalQuery<Record<string, unknown>>(
231
+ `SELECT * FROM scheduled_tasks ${where}
232
+ ORDER BY created_at DESC LIMIT $${paramIdx++} OFFSET $${paramIdx++}`,
233
+ [...params, limit, offset],
234
+ );
235
+
236
+ const total = (countRows[0]?.total as number) ?? 0;
237
+ return { tasks: dataRows.map(rowToScheduledTask), total };
238
+ } catch (err) {
239
+ log.error({ err: err instanceof Error ? err.message : String(err) }, "listScheduledTasks failed");
240
+ return empty;
241
+ }
242
+ }
243
+
244
+ export async function updateScheduledTask(
245
+ id: string,
246
+ ownerId: string,
247
+ updates: {
248
+ name?: string;
249
+ question?: string;
250
+ cronExpression?: string;
251
+ deliveryChannel?: DeliveryChannel;
252
+ recipients?: Recipient[];
253
+ connectionId?: string | null;
254
+ approvalMode?: ActionApprovalMode;
255
+ enabled?: boolean;
256
+ },
257
+ ): Promise<CrudResult> {
258
+ if (!hasInternalDB()) return { ok: false, reason: "no_db" };
259
+
260
+ const setClauses: string[] = [];
261
+ const params: unknown[] = [];
262
+ let paramIdx = 1;
263
+
264
+ if (updates.name !== undefined) {
265
+ setClauses.push(`name = $${paramIdx++}`);
266
+ params.push(updates.name);
267
+ }
268
+ if (updates.question !== undefined) {
269
+ setClauses.push(`question = $${paramIdx++}`);
270
+ params.push(updates.question);
271
+ }
272
+ if (updates.cronExpression !== undefined) {
273
+ const validation = validateCronExpression(updates.cronExpression);
274
+ if (!validation.valid) {
275
+ log.warn({ cronExpression: updates.cronExpression, error: validation.error }, "Invalid cron expression rejected in updateScheduledTask");
276
+ return { ok: false, reason: "error" };
277
+ }
278
+ setClauses.push(`cron_expression = $${paramIdx++}`);
279
+ params.push(updates.cronExpression);
280
+ const nextRun = computeNextRun(updates.cronExpression);
281
+ setClauses.push(`next_run_at = $${paramIdx++}`);
282
+ params.push(nextRun?.toISOString() ?? null);
283
+ }
284
+ if (updates.deliveryChannel !== undefined) {
285
+ setClauses.push(`delivery_channel = $${paramIdx++}`);
286
+ params.push(updates.deliveryChannel);
287
+ }
288
+ if (updates.recipients !== undefined) {
289
+ setClauses.push(`recipients = $${paramIdx++}`);
290
+ params.push(JSON.stringify(updates.recipients));
291
+ }
292
+ if (updates.connectionId !== undefined) {
293
+ setClauses.push(`connection_id = $${paramIdx++}`);
294
+ params.push(updates.connectionId);
295
+ }
296
+ if (updates.approvalMode !== undefined) {
297
+ setClauses.push(`approval_mode = $${paramIdx++}`);
298
+ params.push(updates.approvalMode);
299
+ }
300
+ if (updates.enabled !== undefined) {
301
+ setClauses.push(`enabled = $${paramIdx++}`);
302
+ params.push(updates.enabled);
303
+ }
304
+
305
+ if (setClauses.length === 0) return { ok: true };
306
+
307
+ setClauses.push(`updated_at = now()`);
308
+
309
+ try {
310
+ const rows = await internalQuery<{ id: string }>(
311
+ `UPDATE scheduled_tasks SET ${setClauses.join(", ")}
312
+ WHERE id = $${paramIdx++} AND owner_id = $${paramIdx++}
313
+ RETURNING id`,
314
+ [...params, id, ownerId],
315
+ );
316
+ return rows.length > 0 ? { ok: true } : { ok: false, reason: "not_found" };
317
+ } catch (err) {
318
+ log.error({ err: err instanceof Error ? err.message : String(err) }, "updateScheduledTask failed");
319
+ return { ok: false, reason: "error" };
320
+ }
321
+ }
322
+
323
+ /** Soft delete: sets enabled=false to preserve audit trail. */
324
+ export async function deleteScheduledTask(
325
+ id: string,
326
+ ownerId?: string,
327
+ ): Promise<CrudResult> {
328
+ if (!hasInternalDB()) return { ok: false, reason: "no_db" };
329
+ try {
330
+ const rows = ownerId
331
+ ? await internalQuery<{ id: string }>(
332
+ `UPDATE scheduled_tasks SET enabled = false, updated_at = now()
333
+ WHERE id = $1 AND owner_id = $2 RETURNING id`,
334
+ [id, ownerId],
335
+ )
336
+ : await internalQuery<{ id: string }>(
337
+ `UPDATE scheduled_tasks SET enabled = false, updated_at = now()
338
+ WHERE id = $1 RETURNING id`,
339
+ [id],
340
+ );
341
+ return rows.length > 0 ? { ok: true } : { ok: false, reason: "not_found" };
342
+ } catch (err) {
343
+ log.error({ err: err instanceof Error ? err.message : String(err) }, "deleteScheduledTask failed");
344
+ return { ok: false, reason: "error" };
345
+ }
346
+ }
347
+
348
+ // ---------------------------------------------------------------------------
349
+ // CRUD — Runs
350
+ // ---------------------------------------------------------------------------
351
+
352
+ /** Create a new run record. Returns the run ID or null on failure. */
353
+ export async function createTaskRun(taskId: string): Promise<string | null> {
354
+ if (!hasInternalDB()) return null;
355
+ try {
356
+ const rows = await internalQuery<{ id: string }>(
357
+ `INSERT INTO scheduled_task_runs (task_id) VALUES ($1) RETURNING id`,
358
+ [taskId],
359
+ );
360
+ return rows[0]?.id ?? null;
361
+ } catch (err) {
362
+ log.error({ err: err instanceof Error ? err.message : String(err), taskId }, "createTaskRun failed");
363
+ return null;
364
+ }
365
+ }
366
+
367
+ /** Update a run record on completion. Fire-and-forget. */
368
+ export function completeTaskRun(
369
+ runId: string,
370
+ status: RunStatus,
371
+ opts?: { error?: string; tokensUsed?: number; conversationId?: string },
372
+ ): void {
373
+ if (!hasInternalDB()) return;
374
+ internalExecute(
375
+ `UPDATE scheduled_task_runs SET
376
+ status = $1,
377
+ completed_at = now(),
378
+ error = $2,
379
+ tokens_used = $3,
380
+ conversation_id = $4
381
+ WHERE id = $5`,
382
+ [status, opts?.error ?? null, opts?.tokensUsed ?? null, opts?.conversationId ?? null, runId],
383
+ );
384
+ }
385
+
386
+ export async function listTaskRuns(
387
+ taskId: string,
388
+ opts?: { limit?: number },
389
+ ): Promise<ScheduledTaskRun[]> {
390
+ if (!hasInternalDB()) return [];
391
+ const limit = opts?.limit ?? 20;
392
+ try {
393
+ const rows = await internalQuery<Record<string, unknown>>(
394
+ `SELECT * FROM scheduled_task_runs WHERE task_id = $1
395
+ ORDER BY started_at DESC LIMIT $2`,
396
+ [taskId, limit],
397
+ );
398
+ return rows.map(rowToScheduledTaskRun);
399
+ } catch (err) {
400
+ log.error({ err: err instanceof Error ? err.message : String(err), taskId }, "listTaskRuns failed");
401
+ return [];
402
+ }
403
+ }
404
+
405
+ // ---------------------------------------------------------------------------
406
+ // Scheduler helpers
407
+ // ---------------------------------------------------------------------------
408
+
409
+ /** Get tasks that are due for execution (enabled + next_run_at <= now). */
410
+ export async function getTasksDueForExecution(): Promise<ScheduledTask[]> {
411
+ if (!hasInternalDB()) return [];
412
+ try {
413
+ const rows = await internalQuery<Record<string, unknown>>(
414
+ `SELECT * FROM scheduled_tasks
415
+ WHERE enabled = true AND next_run_at <= now()
416
+ ORDER BY next_run_at ASC`,
417
+ );
418
+ return rows.map(rowToScheduledTask);
419
+ } catch (err) {
420
+ log.error({ err: err instanceof Error ? err.message : String(err) }, "getTasksDueForExecution failed");
421
+ return [];
422
+ }
423
+ }
424
+
425
+ /**
426
+ * Atomically lock a task for execution.
427
+ *
428
+ * Uses a single UPDATE with `AND next_run_at IS NOT NULL` as a lightweight
429
+ * lock: the first process to UPDATE sets next_run_at to the future, preventing
430
+ * concurrent UPDATEs from matching. Also updates last_run_at.
431
+ *
432
+ * Returns true if lock acquired, false if task is already locked, disabled,
433
+ * or not found.
434
+ */
435
+ export async function lockTaskForExecution(taskId: string): Promise<boolean> {
436
+ if (!hasInternalDB()) return false;
437
+ try {
438
+ // First read the cron expression so we can compute the next run in a single UPDATE
439
+ const taskResult = await getScheduledTask(taskId);
440
+ if (!taskResult.ok) {
441
+ log.warn({ taskId }, "lockTaskForExecution: task not found");
442
+ return false;
443
+ }
444
+
445
+ const nextRun = computeNextRun(taskResult.data.cronExpression);
446
+
447
+ // Atomic UPDATE: only succeeds if enabled AND next_run_at IS NOT NULL
448
+ // (prevents double-execution — second process sees next_run_at already set to future)
449
+ const rows = await internalQuery<{ id: string }>(
450
+ `UPDATE scheduled_tasks SET
451
+ last_run_at = now(),
452
+ next_run_at = $1,
453
+ updated_at = now()
454
+ WHERE id = $2 AND enabled = true AND next_run_at IS NOT NULL
455
+ RETURNING id`,
456
+ [nextRun?.toISOString() ?? null, taskId],
457
+ );
458
+
459
+ if (rows.length === 0) return false;
460
+ return true;
461
+ } catch (err) {
462
+ log.error({ err: err instanceof Error ? err.message : String(err), taskId }, "lockTaskForExecution failed");
463
+ return false;
464
+ }
465
+ }
466
+
467
+ // ---------------------------------------------------------------------------
468
+ // Test helpers
469
+ // ---------------------------------------------------------------------------
470
+
471
+ /** Reset module state for testing. */
472
+ export function _resetScheduledTasksForTest(): void {
473
+ // No module-level state to reset — all state is in the DB.
474
+ // This exists as a hook for future caching.
475
+ }
@@ -0,0 +1,192 @@
1
+ /**
2
+ * Unit tests for the delivery dispatcher.
3
+ */
4
+ import { describe, it, expect, beforeEach, afterEach, mock } from "bun:test";
5
+ import type { ScheduledTask } from "@atlas/api/lib/scheduled-tasks";
6
+ import type { AgentQueryResult } from "@atlas/api/lib/agent-query";
7
+
8
+ // Mock formatters
9
+ mock.module("../format-email", () => ({
10
+ formatEmailReport: mock(() => ({ subject: "Subject", body: "<html>body</html>" })),
11
+ }));
12
+ mock.module("../format-slack", () => ({
13
+ formatSlackReport: mock(() => ({ text: "Report", blocks: [] })),
14
+ }));
15
+ mock.module("../format-webhook", () => ({
16
+ formatWebhookPayload: mock(() => ({ taskId: "t", answer: "A" })),
17
+ }));
18
+
19
+ // Mock fetch for delivery
20
+ const originalFetch = globalThis.fetch;
21
+ const mockFetch = mock(() => Promise.resolve(new Response("ok", { status: 200 })));
22
+
23
+ const { deliverResult } = await import("../delivery");
24
+
25
+ function makeTask(overrides: Partial<ScheduledTask> = {}): ScheduledTask {
26
+ return {
27
+ id: "task-123",
28
+ ownerId: "u1",
29
+ name: "Test Report",
30
+ question: "Q?",
31
+ cronExpression: "0 9 * * 1",
32
+ deliveryChannel: "webhook",
33
+ recipients: [],
34
+ connectionId: null,
35
+ approvalMode: "auto",
36
+ enabled: true,
37
+ lastRunAt: null,
38
+ nextRunAt: null,
39
+ createdAt: "2024-01-01T00:00:00Z",
40
+ updatedAt: "2024-01-01T00:00:00Z",
41
+ ...overrides,
42
+ };
43
+ }
44
+
45
+ function makeResult(): AgentQueryResult {
46
+ return {
47
+ answer: "Revenue was $1M",
48
+ sql: ["SELECT SUM(revenue) FROM orders"],
49
+ data: [{ columns: ["total"], rows: [{ total: 1000000 }] }],
50
+ steps: 3,
51
+ usage: { totalTokens: 1500 },
52
+ };
53
+ }
54
+
55
+ describe("delivery dispatcher", () => {
56
+ beforeEach(() => {
57
+ mockFetch.mockReset();
58
+ mockFetch.mockResolvedValue(new Response("ok", { status: 200 }));
59
+ globalThis.fetch = mockFetch as unknown as typeof fetch;
60
+ });
61
+
62
+ afterEach(() => {
63
+ globalThis.fetch = originalFetch;
64
+ });
65
+
66
+ it("returns zero summary when no recipients", async () => {
67
+ const task = makeTask({ recipients: [] });
68
+ const summary = await deliverResult(task, makeResult());
69
+ expect(summary).toEqual({ attempted: 0, succeeded: 0, failed: 0 });
70
+ expect(mockFetch).not.toHaveBeenCalled();
71
+ });
72
+
73
+ it("delivers webhook and returns success summary", async () => {
74
+ const task = makeTask({
75
+ deliveryChannel: "webhook",
76
+ recipients: [{ type: "webhook", url: "https://hook.example.com" }],
77
+ });
78
+ const summary = await deliverResult(task, makeResult());
79
+ expect(summary).toEqual({ attempted: 1, succeeded: 1, failed: 0 });
80
+ expect(mockFetch).toHaveBeenCalledTimes(1);
81
+ const callArgs = mockFetch.mock.calls[0] as unknown as [string, RequestInit];
82
+ expect(callArgs[0]).toBe("https://hook.example.com");
83
+ });
84
+
85
+ it("delivers email via Resend when API key is set", async () => {
86
+ const origKey = process.env.RESEND_API_KEY;
87
+ process.env.RESEND_API_KEY = "re_test_123";
88
+
89
+ const task = makeTask({
90
+ deliveryChannel: "email",
91
+ recipients: [{ type: "email", address: "test@example.com" }],
92
+ });
93
+ const summary = await deliverResult(task, makeResult());
94
+ expect(summary.succeeded).toBe(1);
95
+ expect(mockFetch).toHaveBeenCalledTimes(1);
96
+ const callArgs = mockFetch.mock.calls[0] as unknown as [string, RequestInit];
97
+ expect(callArgs[0]).toBe("https://api.resend.com/emails");
98
+
99
+ if (origKey) process.env.RESEND_API_KEY = origKey;
100
+ else delete process.env.RESEND_API_KEY;
101
+ });
102
+
103
+ it("reports failure when no RESEND_API_KEY", async () => {
104
+ const origKey = process.env.RESEND_API_KEY;
105
+ delete process.env.RESEND_API_KEY;
106
+
107
+ const task = makeTask({
108
+ deliveryChannel: "email",
109
+ recipients: [{ type: "email", address: "test@example.com" }],
110
+ });
111
+ const summary = await deliverResult(task, makeResult());
112
+ expect(summary).toEqual({ attempted: 1, succeeded: 0, failed: 1 });
113
+ expect(mockFetch).not.toHaveBeenCalled();
114
+
115
+ if (origKey) process.env.RESEND_API_KEY = origKey;
116
+ });
117
+
118
+ it("reports failure on webhook delivery error", async () => {
119
+ mockFetch.mockResolvedValueOnce(new Response("error", { status: 500 }));
120
+
121
+ const task = makeTask({
122
+ deliveryChannel: "webhook",
123
+ recipients: [{ type: "webhook", url: "https://hook.example.com" }],
124
+ });
125
+ const summary = await deliverResult(task, makeResult());
126
+ expect(summary).toEqual({ attempted: 1, succeeded: 0, failed: 1 });
127
+ });
128
+
129
+ it("reports failure on fetch network error", async () => {
130
+ mockFetch.mockRejectedValueOnce(new Error("network error"));
131
+
132
+ const task = makeTask({
133
+ deliveryChannel: "webhook",
134
+ recipients: [{ type: "webhook", url: "https://hook.example.com" }],
135
+ });
136
+ const summary = await deliverResult(task, makeResult());
137
+ expect(summary).toEqual({ attempted: 1, succeeded: 0, failed: 1 });
138
+ });
139
+
140
+ it("includes safe custom headers for webhook recipients", async () => {
141
+ const task = makeTask({
142
+ deliveryChannel: "webhook",
143
+ recipients: [{ type: "webhook", url: "https://hook.example.com", headers: { "X-Key": "abc" } }],
144
+ });
145
+ await deliverResult(task, makeResult());
146
+ const callArgs = mockFetch.mock.calls[0] as unknown as [string, RequestInit];
147
+ const headers = callArgs[1].headers as Record<string, string>;
148
+ expect(headers["X-Key"]).toBe("abc");
149
+ });
150
+
151
+ it("blocks sensitive headers in webhook recipients", async () => {
152
+ const task = makeTask({
153
+ deliveryChannel: "webhook",
154
+ recipients: [{ type: "webhook", url: "https://hook.example.com", headers: { "Authorization": "Bearer secret", "X-Safe": "ok" } }],
155
+ });
156
+ await deliverResult(task, makeResult());
157
+ const callArgs = mockFetch.mock.calls[0] as unknown as [string, RequestInit];
158
+ const headers = callArgs[1].headers as Record<string, string>;
159
+ expect(headers["X-Safe"]).toBe("ok");
160
+ expect(headers["Authorization"]).toBeUndefined();
161
+ });
162
+
163
+ it("blocks webhook URLs targeting private/internal addresses", async () => {
164
+ const task = makeTask({
165
+ deliveryChannel: "webhook",
166
+ recipients: [{ type: "webhook", url: "http://169.254.169.254/latest/meta-data/" }],
167
+ });
168
+ const summary = await deliverResult(task, makeResult());
169
+ expect(summary).toEqual({ attempted: 1, succeeded: 0, failed: 1 });
170
+ expect(mockFetch).not.toHaveBeenCalled();
171
+ });
172
+
173
+ it("blocks webhook URLs targeting localhost", async () => {
174
+ const task = makeTask({
175
+ deliveryChannel: "webhook",
176
+ recipients: [{ type: "webhook", url: "http://localhost:3001/api/health" }],
177
+ });
178
+ const summary = await deliverResult(task, makeResult());
179
+ expect(summary.failed).toBe(1);
180
+ expect(mockFetch).not.toHaveBeenCalled();
181
+ });
182
+
183
+ it("blocks webhook URLs targeting private 10.x.x.x range", async () => {
184
+ const task = makeTask({
185
+ deliveryChannel: "webhook",
186
+ recipients: [{ type: "webhook", url: "http://10.0.0.1/internal" }],
187
+ });
188
+ const summary = await deliverResult(task, makeResult());
189
+ expect(summary.failed).toBe(1);
190
+ expect(mockFetch).not.toHaveBeenCalled();
191
+ });
192
+ });