stagent 0.1.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 (333) hide show
  1. package/LICENSE +191 -0
  2. package/README.md +399 -0
  3. package/components.json +21 -0
  4. package/dist/cli.js +171 -0
  5. package/drizzle.config.ts +12 -0
  6. package/next.config.mjs +15 -0
  7. package/package.json +114 -0
  8. package/postcss.config.mjs +8 -0
  9. package/public/icon-512.png +0 -0
  10. package/public/icon.svg +13 -0
  11. package/public/readme/home-workspace.png +0 -0
  12. package/public/readme/inbox-approvals.png +0 -0
  13. package/public/readme/workflow-blueprints.png +0 -0
  14. package/public/stagent-s-128.png +0 -0
  15. package/public/stagent-s-64.png +0 -0
  16. package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
  17. package/src/app/api/blueprints/[id]/route.ts +39 -0
  18. package/src/app/api/blueprints/import/route.ts +68 -0
  19. package/src/app/api/blueprints/route.ts +29 -0
  20. package/src/app/api/command-palette/recent/route.ts +31 -0
  21. package/src/app/api/data/clear/route.ts +22 -0
  22. package/src/app/api/data/seed/route.ts +22 -0
  23. package/src/app/api/documents/[id]/file/route.ts +44 -0
  24. package/src/app/api/documents/[id]/route.ts +123 -0
  25. package/src/app/api/documents/route.ts +59 -0
  26. package/src/app/api/logs/stream/route.ts +101 -0
  27. package/src/app/api/notifications/[id]/route.ts +36 -0
  28. package/src/app/api/notifications/mark-all-read/route.ts +13 -0
  29. package/src/app/api/notifications/pending-approvals/route.ts +10 -0
  30. package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
  31. package/src/app/api/notifications/route.ts +34 -0
  32. package/src/app/api/permissions/route.ts +46 -0
  33. package/src/app/api/profiles/[id]/route.ts +79 -0
  34. package/src/app/api/profiles/[id]/test/route.ts +42 -0
  35. package/src/app/api/profiles/import/route.ts +108 -0
  36. package/src/app/api/profiles/route.ts +50 -0
  37. package/src/app/api/projects/[id]/route.ts +72 -0
  38. package/src/app/api/projects/route.ts +53 -0
  39. package/src/app/api/schedules/[id]/route.ts +185 -0
  40. package/src/app/api/schedules/route.ts +117 -0
  41. package/src/app/api/settings/budgets/route.ts +24 -0
  42. package/src/app/api/settings/openai/route.ts +24 -0
  43. package/src/app/api/settings/route.ts +21 -0
  44. package/src/app/api/settings/test/route.ts +26 -0
  45. package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +90 -0
  47. package/src/app/api/tasks/[id]/logs/route.ts +95 -0
  48. package/src/app/api/tasks/[id]/output/route.ts +47 -0
  49. package/src/app/api/tasks/[id]/respond/route.ts +64 -0
  50. package/src/app/api/tasks/[id]/resume/route.ts +76 -0
  51. package/src/app/api/tasks/[id]/route.ts +77 -0
  52. package/src/app/api/tasks/assist/route.ts +35 -0
  53. package/src/app/api/tasks/route.ts +82 -0
  54. package/src/app/api/uploads/[id]/route.ts +81 -0
  55. package/src/app/api/uploads/cleanup/route.ts +7 -0
  56. package/src/app/api/uploads/route.ts +66 -0
  57. package/src/app/api/workflows/[id]/execute/route.ts +82 -0
  58. package/src/app/api/workflows/[id]/route.ts +133 -0
  59. package/src/app/api/workflows/[id]/status/route.ts +54 -0
  60. package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
  61. package/src/app/api/workflows/route.ts +61 -0
  62. package/src/app/apple-icon.tsx +31 -0
  63. package/src/app/costs/page.tsx +256 -0
  64. package/src/app/dashboard/page.tsx +44 -0
  65. package/src/app/documents/[id]/page.tsx +46 -0
  66. package/src/app/documents/page.tsx +45 -0
  67. package/src/app/error.tsx +26 -0
  68. package/src/app/global-error.tsx +23 -0
  69. package/src/app/globals.css +733 -0
  70. package/src/app/icon.tsx +30 -0
  71. package/src/app/inbox/loading.tsx +15 -0
  72. package/src/app/inbox/page.tsx +35 -0
  73. package/src/app/layout.tsx +78 -0
  74. package/src/app/manifest.ts +32 -0
  75. package/src/app/monitor/page.tsx +37 -0
  76. package/src/app/page.tsx +162 -0
  77. package/src/app/profiles/[id]/edit/page.tsx +39 -0
  78. package/src/app/profiles/[id]/page.tsx +33 -0
  79. package/src/app/profiles/new/page.tsx +22 -0
  80. package/src/app/profiles/page.tsx +19 -0
  81. package/src/app/projects/[id]/page.tsx +134 -0
  82. package/src/app/projects/loading.tsx +17 -0
  83. package/src/app/projects/page.tsx +32 -0
  84. package/src/app/schedules/[id]/page.tsx +47 -0
  85. package/src/app/schedules/page.tsx +18 -0
  86. package/src/app/settings/loading.tsx +24 -0
  87. package/src/app/settings/page.tsx +27 -0
  88. package/src/app/tasks/[id]/page.tsx +45 -0
  89. package/src/app/tasks/new/page.tsx +27 -0
  90. package/src/app/workflows/[id]/edit/page.tsx +66 -0
  91. package/src/app/workflows/[id]/page.tsx +37 -0
  92. package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
  93. package/src/app/workflows/blueprints/new/page.tsx +20 -0
  94. package/src/app/workflows/blueprints/page.tsx +11 -0
  95. package/src/app/workflows/new/page.tsx +36 -0
  96. package/src/app/workflows/page.tsx +18 -0
  97. package/src/components/charts/donut-ring.tsx +64 -0
  98. package/src/components/charts/mini-bar.tsx +75 -0
  99. package/src/components/charts/sparkline.tsx +107 -0
  100. package/src/components/costs/cost-dashboard.tsx +877 -0
  101. package/src/components/costs/cost-filters.tsx +179 -0
  102. package/src/components/dashboard/activity-feed.tsx +95 -0
  103. package/src/components/dashboard/greeting.tsx +30 -0
  104. package/src/components/dashboard/priority-queue.tsx +79 -0
  105. package/src/components/dashboard/quick-actions.tsx +62 -0
  106. package/src/components/dashboard/recent-projects.tsx +79 -0
  107. package/src/components/dashboard/stats-cards.tsx +114 -0
  108. package/src/components/documents/document-browser.tsx +235 -0
  109. package/src/components/documents/document-detail-view.tsx +367 -0
  110. package/src/components/documents/document-grid.tsx +78 -0
  111. package/src/components/documents/document-preview.tsx +68 -0
  112. package/src/components/documents/document-table.tsx +119 -0
  113. package/src/components/documents/document-upload-dialog.tsx +153 -0
  114. package/src/components/documents/types.ts +6 -0
  115. package/src/components/documents/utils.ts +57 -0
  116. package/src/components/monitoring/connection-indicator.tsx +14 -0
  117. package/src/components/monitoring/log-entry.tsx +79 -0
  118. package/src/components/monitoring/log-filters.tsx +57 -0
  119. package/src/components/monitoring/log-stream.tsx +144 -0
  120. package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
  121. package/src/components/monitoring/monitor-overview.tsx +119 -0
  122. package/src/components/notifications/failure-action.tsx +38 -0
  123. package/src/components/notifications/inbox-list.tsx +165 -0
  124. package/src/components/notifications/message-response.tsx +196 -0
  125. package/src/components/notifications/notification-item.tsx +250 -0
  126. package/src/components/notifications/pending-approval-host.tsx +478 -0
  127. package/src/components/notifications/permission-action.tsx +37 -0
  128. package/src/components/notifications/permission-response-actions.tsx +126 -0
  129. package/src/components/notifications/unread-badge.tsx +35 -0
  130. package/src/components/profiles/profile-browser.tsx +117 -0
  131. package/src/components/profiles/profile-card.tsx +78 -0
  132. package/src/components/profiles/profile-detail-view.tsx +564 -0
  133. package/src/components/profiles/profile-form-view.tsx +480 -0
  134. package/src/components/profiles/profile-import-dialog.tsx +113 -0
  135. package/src/components/projects/project-card.tsx +58 -0
  136. package/src/components/projects/project-create-dialog.tsx +140 -0
  137. package/src/components/projects/project-detail.tsx +68 -0
  138. package/src/components/projects/project-edit-dialog.tsx +219 -0
  139. package/src/components/projects/project-list.tsx +108 -0
  140. package/src/components/schedules/schedule-create-dialog.tsx +403 -0
  141. package/src/components/schedules/schedule-detail-view.tsx +274 -0
  142. package/src/components/schedules/schedule-list.tsx +242 -0
  143. package/src/components/schedules/schedule-status-badge.tsx +16 -0
  144. package/src/components/settings/api-key-form.tsx +141 -0
  145. package/src/components/settings/auth-config-section.tsx +141 -0
  146. package/src/components/settings/auth-method-selector.tsx +67 -0
  147. package/src/components/settings/auth-status-badge.tsx +40 -0
  148. package/src/components/settings/auth-status-dot.tsx +59 -0
  149. package/src/components/settings/budget-guardrails-section.tsx +842 -0
  150. package/src/components/settings/data-management-section.tsx +141 -0
  151. package/src/components/settings/openai-runtime-section.tsx +104 -0
  152. package/src/components/settings/permissions-section.tsx +91 -0
  153. package/src/components/shared/app-sidebar.tsx +123 -0
  154. package/src/components/shared/card-skeleton.tsx +42 -0
  155. package/src/components/shared/command-palette.tsx +250 -0
  156. package/src/components/shared/confirm-dialog.tsx +52 -0
  157. package/src/components/shared/empty-state.tsx +24 -0
  158. package/src/components/shared/error-state.tsx +32 -0
  159. package/src/components/shared/form-section-card.tsx +33 -0
  160. package/src/components/shared/section-heading.tsx +14 -0
  161. package/src/components/shared/stagent-logo.tsx +21 -0
  162. package/src/components/shared/theme-toggle.tsx +46 -0
  163. package/src/components/tasks/ai-assist-panel.tsx +210 -0
  164. package/src/components/tasks/content-preview.tsx +89 -0
  165. package/src/components/tasks/empty-board.tsx +12 -0
  166. package/src/components/tasks/file-upload.tsx +120 -0
  167. package/src/components/tasks/kanban-board.tsx +275 -0
  168. package/src/components/tasks/kanban-column.tsx +75 -0
  169. package/src/components/tasks/skeleton-board.tsx +21 -0
  170. package/src/components/tasks/task-attachments.tsx +114 -0
  171. package/src/components/tasks/task-card.tsx +101 -0
  172. package/src/components/tasks/task-create-panel.tsx +360 -0
  173. package/src/components/tasks/task-detail-view.tsx +356 -0
  174. package/src/components/ui/alert-dialog.tsx +196 -0
  175. package/src/components/ui/badge.tsx +50 -0
  176. package/src/components/ui/button.tsx +71 -0
  177. package/src/components/ui/card.tsx +92 -0
  178. package/src/components/ui/checkbox.tsx +32 -0
  179. package/src/components/ui/command.tsx +184 -0
  180. package/src/components/ui/dialog.tsx +158 -0
  181. package/src/components/ui/dropdown-menu.tsx +257 -0
  182. package/src/components/ui/form.tsx +167 -0
  183. package/src/components/ui/input.tsx +21 -0
  184. package/src/components/ui/label.tsx +24 -0
  185. package/src/components/ui/popover.tsx +89 -0
  186. package/src/components/ui/progress.tsx +31 -0
  187. package/src/components/ui/radio-group.tsx +45 -0
  188. package/src/components/ui/scroll-area.tsx +58 -0
  189. package/src/components/ui/select.tsx +190 -0
  190. package/src/components/ui/separator.tsx +28 -0
  191. package/src/components/ui/sheet.tsx +143 -0
  192. package/src/components/ui/sidebar.tsx +726 -0
  193. package/src/components/ui/skeleton.tsx +13 -0
  194. package/src/components/ui/slider.tsx +63 -0
  195. package/src/components/ui/sonner.tsx +36 -0
  196. package/src/components/ui/switch.tsx +35 -0
  197. package/src/components/ui/table.tsx +116 -0
  198. package/src/components/ui/tabs.tsx +91 -0
  199. package/src/components/ui/textarea.tsx +18 -0
  200. package/src/components/ui/tooltip.tsx +57 -0
  201. package/src/components/workflows/blueprint-editor.tsx +109 -0
  202. package/src/components/workflows/blueprint-gallery.tsx +155 -0
  203. package/src/components/workflows/blueprint-preview.tsx +240 -0
  204. package/src/components/workflows/loop-status-view.tsx +272 -0
  205. package/src/components/workflows/swarm-dashboard.tsx +185 -0
  206. package/src/components/workflows/workflow-form-view.tsx +1376 -0
  207. package/src/components/workflows/workflow-list.tsx +230 -0
  208. package/src/components/workflows/workflow-status-view.tsx +477 -0
  209. package/src/hooks/use-mobile.ts +19 -0
  210. package/src/instrumentation.ts +7 -0
  211. package/src/lib/agents/claude-agent.ts +737 -0
  212. package/src/lib/agents/execution-manager.ts +27 -0
  213. package/src/lib/agents/profiles/assignment-validation.ts +75 -0
  214. package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
  215. package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
  216. package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
  217. package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
  218. package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
  219. package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
  220. package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
  221. package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
  222. package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
  223. package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
  224. package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
  225. package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
  226. package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
  227. package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
  228. package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
  229. package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
  230. package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
  231. package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
  232. package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
  233. package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
  234. package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
  235. package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
  236. package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
  237. package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
  238. package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
  239. package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
  240. package/src/lib/agents/profiles/compatibility.ts +109 -0
  241. package/src/lib/agents/profiles/registry.ts +293 -0
  242. package/src/lib/agents/profiles/test-runner.ts +18 -0
  243. package/src/lib/agents/profiles/test-types.ts +20 -0
  244. package/src/lib/agents/profiles/types.ts +43 -0
  245. package/src/lib/agents/router.ts +56 -0
  246. package/src/lib/agents/runtime/catalog.ts +85 -0
  247. package/src/lib/agents/runtime/claude-sdk.ts +12 -0
  248. package/src/lib/agents/runtime/claude.ts +370 -0
  249. package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
  250. package/src/lib/agents/runtime/index.ts +167 -0
  251. package/src/lib/agents/runtime/openai-codex.ts +1089 -0
  252. package/src/lib/agents/runtime/task-assist-types.ts +8 -0
  253. package/src/lib/agents/runtime/types.ts +30 -0
  254. package/src/lib/constants/settings.ts +13 -0
  255. package/src/lib/constants/status-colors.ts +44 -0
  256. package/src/lib/constants/task-status.ts +49 -0
  257. package/src/lib/data/clear.ts +63 -0
  258. package/src/lib/data/seed-data/documents.ts +715 -0
  259. package/src/lib/data/seed-data/logs.ts +195 -0
  260. package/src/lib/data/seed-data/notifications.ts +141 -0
  261. package/src/lib/data/seed-data/profiles.ts +175 -0
  262. package/src/lib/data/seed-data/projects.ts +61 -0
  263. package/src/lib/data/seed-data/schedules.ts +108 -0
  264. package/src/lib/data/seed-data/tasks.ts +341 -0
  265. package/src/lib/data/seed-data/usage-ledger.ts +130 -0
  266. package/src/lib/data/seed-data/workflows.ts +213 -0
  267. package/src/lib/data/seed.ts +129 -0
  268. package/src/lib/db/index.ts +221 -0
  269. package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
  270. package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
  271. package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
  272. package/src/lib/db/migrations/0003_add_settings.sql +5 -0
  273. package/src/lib/db/migrations/0004_add_documents.sql +20 -0
  274. package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
  275. package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
  276. package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
  277. package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
  278. package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
  279. package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
  280. package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
  281. package/src/lib/db/migrations/meta/_journal.json +27 -0
  282. package/src/lib/db/schema.ts +227 -0
  283. package/src/lib/documents/cleanup.ts +50 -0
  284. package/src/lib/documents/context-builder.ts +75 -0
  285. package/src/lib/documents/output-scanner.ts +166 -0
  286. package/src/lib/documents/processor.ts +120 -0
  287. package/src/lib/documents/processors/image.ts +21 -0
  288. package/src/lib/documents/processors/office.ts +36 -0
  289. package/src/lib/documents/processors/pdf.ts +12 -0
  290. package/src/lib/documents/processors/spreadsheet.ts +18 -0
  291. package/src/lib/documents/processors/text.ts +8 -0
  292. package/src/lib/documents/registry.ts +25 -0
  293. package/src/lib/notifications/actionable.ts +108 -0
  294. package/src/lib/notifications/permissions.ts +169 -0
  295. package/src/lib/queries/chart-data.ts +184 -0
  296. package/src/lib/schedules/interval-parser.ts +110 -0
  297. package/src/lib/schedules/scheduler.ts +220 -0
  298. package/src/lib/settings/auth.ts +98 -0
  299. package/src/lib/settings/budget-guardrails.ts +590 -0
  300. package/src/lib/settings/helpers.ts +23 -0
  301. package/src/lib/settings/openai-auth.ts +80 -0
  302. package/src/lib/settings/permissions.ts +102 -0
  303. package/src/lib/usage/ledger.ts +489 -0
  304. package/src/lib/usage/pricing.ts +68 -0
  305. package/src/lib/utils/crypto.ts +90 -0
  306. package/src/lib/utils/format-timestamp.ts +46 -0
  307. package/src/lib/utils/session-cleanup.ts +26 -0
  308. package/src/lib/utils/stagent-paths.ts +18 -0
  309. package/src/lib/utils.ts +6 -0
  310. package/src/lib/validators/blueprint.ts +43 -0
  311. package/src/lib/validators/profile.ts +64 -0
  312. package/src/lib/validators/project.ts +17 -0
  313. package/src/lib/validators/settings.ts +57 -0
  314. package/src/lib/validators/task.ts +30 -0
  315. package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
  316. package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
  317. package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
  318. package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
  319. package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
  320. package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
  321. package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
  322. package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
  323. package/src/lib/workflows/blueprints/instantiator.ts +131 -0
  324. package/src/lib/workflows/blueprints/registry.ts +128 -0
  325. package/src/lib/workflows/blueprints/template.ts +58 -0
  326. package/src/lib/workflows/blueprints/types.ts +38 -0
  327. package/src/lib/workflows/definition-validation.ts +121 -0
  328. package/src/lib/workflows/engine.ts +1113 -0
  329. package/src/lib/workflows/loop-executor.ts +270 -0
  330. package/src/lib/workflows/parallel.ts +55 -0
  331. package/src/lib/workflows/swarm.ts +97 -0
  332. package/src/lib/workflows/types.ts +112 -0
  333. package/tsconfig.json +41 -0
@@ -0,0 +1,220 @@
1
+ /**
2
+ * Poll-based scheduler engine.
3
+ *
4
+ * Runs on a configurable interval (default 60s), checking for schedules whose
5
+ * `nextFireAt` has passed. For each due schedule it creates a child task and
6
+ * fires it via the provider runtime pipeline.
7
+ *
8
+ * Lifecycle:
9
+ * - `startScheduler()` — call once at server boot (idempotent)
10
+ * - `stopScheduler()` — call on graceful shutdown
11
+ * - `tickScheduler()` — exposed for testing; runs one poll cycle
12
+ */
13
+
14
+ import { db } from "@/lib/db";
15
+ import { schedules, tasks } from "@/lib/db/schema";
16
+ import { eq, and, lte, like, inArray, sql } from "drizzle-orm";
17
+ import { computeNextFireTime } from "./interval-parser";
18
+ import { executeTaskWithRuntime } from "@/lib/agents/runtime";
19
+
20
+ const POLL_INTERVAL_MS = 60_000; // 60 seconds
21
+
22
+ let intervalHandle: ReturnType<typeof setInterval> | null = null;
23
+
24
+ /**
25
+ * Start the scheduler singleton. Safe to call multiple times — subsequent
26
+ * calls are no-ops if already running.
27
+ */
28
+ export function startScheduler(): void {
29
+ if (intervalHandle !== null) return;
30
+
31
+ // Bootstrap: recompute nextFireAt for any active schedules that are missing it
32
+ bootstrapNextFireTimes();
33
+
34
+ intervalHandle = setInterval(() => {
35
+ tickScheduler().catch((err) => {
36
+ console.error("[scheduler] tick error:", err);
37
+ });
38
+ }, POLL_INTERVAL_MS);
39
+
40
+ console.log("[scheduler] started — polling every 60s");
41
+ }
42
+
43
+ /**
44
+ * Stop the scheduler.
45
+ */
46
+ export function stopScheduler(): void {
47
+ if (intervalHandle !== null) {
48
+ clearInterval(intervalHandle);
49
+ intervalHandle = null;
50
+ console.log("[scheduler] stopped");
51
+ }
52
+ }
53
+
54
+ /**
55
+ * Run one poll cycle: find due schedules and fire them.
56
+ */
57
+ export async function tickScheduler(): Promise<void> {
58
+ const now = new Date();
59
+
60
+ const dueSchedules = await db
61
+ .select()
62
+ .from(schedules)
63
+ .where(
64
+ and(
65
+ eq(schedules.status, "active"),
66
+ lte(schedules.nextFireAt, now)
67
+ )
68
+ );
69
+
70
+ for (const schedule of dueSchedules) {
71
+ try {
72
+ // Atomic claim: attempt to update nextFireAt to null as a lock.
73
+ // Only the first tick to succeed (.changes > 0) proceeds with firing.
74
+ const claimResult = db
75
+ .update(schedules)
76
+ .set({ nextFireAt: null, updatedAt: now })
77
+ .where(
78
+ and(
79
+ eq(schedules.id, schedule.id),
80
+ eq(schedules.status, "active"),
81
+ lte(schedules.nextFireAt, now)
82
+ )
83
+ )
84
+ .run();
85
+
86
+ if (claimResult.changes === 0) {
87
+ // Another tick already claimed this schedule
88
+ continue;
89
+ }
90
+
91
+ await fireSchedule(schedule, now);
92
+ } catch (err) {
93
+ console.error(`[scheduler] failed to fire schedule ${schedule.id}:`, err);
94
+ }
95
+ }
96
+ }
97
+
98
+ async function fireSchedule(
99
+ schedule: typeof schedules.$inferSelect,
100
+ now: Date
101
+ ): Promise<void> {
102
+ // Concurrency guard: skip if a child task from this schedule is still running.
103
+ // Escape SQL LIKE metacharacters (%, _) in schedule name to prevent false matches.
104
+ const escapedName = schedule.name
105
+ .replace(/\\/g, "\\\\")
106
+ .replace(/%/g, "\\%")
107
+ .replace(/_/g, "\\_");
108
+ const runningChildren = await db
109
+ .select({ id: tasks.id })
110
+ .from(tasks)
111
+ .where(
112
+ and(
113
+ sql`${tasks.title} LIKE ${`${escapedName} — firing #%`} ESCAPE '\\'`,
114
+ inArray(tasks.status, ["queued", "running"])
115
+ )
116
+ );
117
+ if (runningChildren.length > 0) {
118
+ console.log(`[scheduler] skipping ${schedule.id} — previous firing still running`);
119
+ return;
120
+ }
121
+
122
+ // Check expiry
123
+ if (schedule.expiresAt && schedule.expiresAt <= now) {
124
+ await db
125
+ .update(schedules)
126
+ .set({ status: "expired", updatedAt: now })
127
+ .where(eq(schedules.id, schedule.id));
128
+ return;
129
+ }
130
+
131
+ // Check max firings
132
+ if (schedule.maxFirings && schedule.firingCount >= schedule.maxFirings) {
133
+ await db
134
+ .update(schedules)
135
+ .set({ status: "expired", updatedAt: now })
136
+ .where(eq(schedules.id, schedule.id));
137
+ return;
138
+ }
139
+
140
+ // Create child task
141
+ const taskId = crypto.randomUUID();
142
+ const firingNumber = schedule.firingCount + 1;
143
+
144
+ await db.insert(tasks).values({
145
+ id: taskId,
146
+ projectId: schedule.projectId,
147
+ workflowId: null,
148
+ scheduleId: schedule.id,
149
+ title: `${schedule.name} — firing #${firingNumber}`,
150
+ description: schedule.prompt,
151
+ status: "queued",
152
+ assignedAgent: schedule.assignedAgent,
153
+ agentProfile: schedule.agentProfile,
154
+ priority: 2,
155
+ createdAt: now,
156
+ updatedAt: now,
157
+ });
158
+
159
+ // Update schedule counters
160
+ const isOneShot = !schedule.recurs;
161
+ const reachedMax =
162
+ schedule.maxFirings !== null && firingNumber >= schedule.maxFirings;
163
+
164
+ const nextStatus = isOneShot
165
+ ? "completed"
166
+ : reachedMax
167
+ ? "expired"
168
+ : "active";
169
+
170
+ const nextFireAt =
171
+ nextStatus === "active"
172
+ ? computeNextFireTime(schedule.cronExpression, now)
173
+ : null;
174
+
175
+ await db
176
+ .update(schedules)
177
+ .set({
178
+ firingCount: firingNumber,
179
+ lastFiredAt: now,
180
+ nextFireAt,
181
+ status: nextStatus,
182
+ updatedAt: now,
183
+ })
184
+ .where(eq(schedules.id, schedule.id));
185
+
186
+ // Fire-and-forget task execution
187
+ executeTaskWithRuntime(taskId).catch((err) => {
188
+ console.error(
189
+ `[scheduler] task execution failed for schedule ${schedule.id}, task ${taskId}:`,
190
+ err
191
+ );
192
+ });
193
+
194
+ console.log(
195
+ `[scheduler] fired schedule "${schedule.name}" → task ${taskId} (firing #${firingNumber})`
196
+ );
197
+ }
198
+
199
+ /**
200
+ * Recompute nextFireAt for active schedules that have it set to null.
201
+ * Called once at startup to recover from unclean shutdowns.
202
+ */
203
+ function bootstrapNextFireTimes(): void {
204
+ const activeSchedules = db
205
+ .select()
206
+ .from(schedules)
207
+ .where(eq(schedules.status, "active"))
208
+ .all();
209
+
210
+ const now = new Date();
211
+ for (const schedule of activeSchedules) {
212
+ if (!schedule.nextFireAt) {
213
+ const nextFire = computeNextFireTime(schedule.cronExpression, now);
214
+ db.update(schedules)
215
+ .set({ nextFireAt: nextFire, updatedAt: now })
216
+ .where(eq(schedules.id, schedule.id))
217
+ .run();
218
+ }
219
+ }
220
+ }
@@ -0,0 +1,98 @@
1
+ import { db } from "@/lib/db";
2
+ import { settings } from "@/lib/db/schema";
3
+ import { eq } from "drizzle-orm";
4
+ import { encrypt, decrypt } from "@/lib/utils/crypto";
5
+ import { SETTINGS_KEYS, type AuthMethod, type ApiKeySource } from "@/lib/constants/settings";
6
+ import type { UpdateAuthSettingsInput } from "@/lib/validators/settings";
7
+ import { getSetting, setSetting } from "./helpers";
8
+
9
+ export interface AuthSettings {
10
+ method: AuthMethod;
11
+ hasKey: boolean;
12
+ apiKeySource: ApiKeySource;
13
+ }
14
+
15
+ /**
16
+ * Get current auth settings. Never returns the raw API key.
17
+ */
18
+ export async function getAuthSettings(): Promise<AuthSettings> {
19
+ const method = ((await getSetting(SETTINGS_KEYS.AUTH_METHOD)) as AuthMethod) ?? "oauth";
20
+ const encryptedKey = await getSetting(SETTINGS_KEYS.AUTH_API_KEY);
21
+ const storedSource = (await getSetting(SETTINGS_KEYS.AUTH_API_KEY_SOURCE)) as ApiKeySource | null;
22
+
23
+ const hasDbKey = encryptedKey !== null;
24
+ const hasEnvKey = !!process.env.ANTHROPIC_API_KEY;
25
+
26
+ let apiKeySource: ApiKeySource;
27
+ if (storedSource) {
28
+ apiKeySource = storedSource;
29
+ } else if (hasDbKey) {
30
+ apiKeySource = "db";
31
+ } else if (hasEnvKey) {
32
+ apiKeySource = "env";
33
+ } else if (method === "oauth") {
34
+ apiKeySource = "oauth";
35
+ } else {
36
+ apiKeySource = "unknown";
37
+ }
38
+
39
+ return {
40
+ method,
41
+ hasKey: hasDbKey || hasEnvKey,
42
+ apiKeySource,
43
+ };
44
+ }
45
+
46
+ /**
47
+ * Save auth settings. Encrypts API key before storing.
48
+ */
49
+ export async function setAuthSettings(input: UpdateAuthSettingsInput): Promise<void> {
50
+ await setSetting(SETTINGS_KEYS.AUTH_METHOD, input.method);
51
+
52
+ if (input.apiKey) {
53
+ await setSetting(SETTINGS_KEYS.AUTH_API_KEY, encrypt(input.apiKey));
54
+ await setSetting(SETTINGS_KEYS.AUTH_API_KEY_SOURCE, "db");
55
+ } else if (input.method === "oauth") {
56
+ // Clear stored key when switching to OAuth
57
+ const existingKey = await getSetting(SETTINGS_KEYS.AUTH_API_KEY);
58
+ if (existingKey !== null) {
59
+ await db.delete(settings)
60
+ .where(eq(settings.key, SETTINGS_KEYS.AUTH_API_KEY));
61
+ }
62
+ await setSetting(SETTINGS_KEYS.AUTH_API_KEY_SOURCE, "oauth");
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Get the environment variables to pass to the Agent SDK.
68
+ * Priority: DB-stored key > process.env > undefined (SDK falls back to OAuth).
69
+ */
70
+ export async function getAuthEnv(): Promise<Record<string, string> | undefined> {
71
+ const method = ((await getSetting(SETTINGS_KEYS.AUTH_METHOD)) as AuthMethod) ?? "oauth";
72
+
73
+ // If OAuth is selected, don't inject any key — let SDK handle it
74
+ if (method === "oauth") {
75
+ return undefined;
76
+ }
77
+
78
+ // Try DB-stored key first
79
+ const encryptedKey = await getSetting(SETTINGS_KEYS.AUTH_API_KEY);
80
+ if (encryptedKey) {
81
+ try {
82
+ const key = decrypt(encryptedKey);
83
+ return { ANTHROPIC_API_KEY: key };
84
+ } catch {
85
+ // If decryption fails, fall through to env
86
+ }
87
+ }
88
+
89
+ // Fall back to env var (already in process.env, no need to inject)
90
+ return undefined;
91
+ }
92
+
93
+ /**
94
+ * Update the last-known API key source after SDK initialization.
95
+ */
96
+ export async function updateAuthStatus(source: ApiKeySource): Promise<void> {
97
+ await setSetting(SETTINGS_KEYS.AUTH_API_KEY_SOURCE, source);
98
+ }