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,21 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { tasks } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { cancelTaskWithRuntime } from "@/lib/agents/runtime";
6
+
7
+ export async function POST(
8
+ _req: NextRequest,
9
+ { params }: { params: Promise<{ id: string }> }
10
+ ) {
11
+ const { id } = await params;
12
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, id));
13
+
14
+ if (!task) {
15
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
16
+ }
17
+
18
+ await cancelTaskWithRuntime(id, task.assignedAgent);
19
+
20
+ return NextResponse.json({ success: true });
21
+ }
@@ -0,0 +1,90 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { tasks } from "@/lib/db/schema";
4
+ import { eq, and } from "drizzle-orm";
5
+ import { executeTaskWithAgent, classifyTaskProfile } from "@/lib/agents/router";
6
+ import { DEFAULT_AGENT_RUNTIME } from "@/lib/agents/runtime/catalog";
7
+ import { validateRuntimeProfileAssignment } from "@/lib/agents/profiles/assignment-validation";
8
+ import {
9
+ BudgetLimitExceededError,
10
+ enforceTaskBudgetGuardrails,
11
+ } from "@/lib/settings/budget-guardrails";
12
+
13
+ export async function POST(
14
+ _req: NextRequest,
15
+ { params }: { params: Promise<{ id: string }> }
16
+ ) {
17
+ const { id } = await params;
18
+
19
+ try {
20
+ await enforceTaskBudgetGuardrails(id);
21
+ } catch (error) {
22
+ if (error instanceof BudgetLimitExceededError) {
23
+ return NextResponse.json({ error: error.message }, { status: 429 });
24
+ }
25
+ throw error;
26
+ }
27
+
28
+ // Atomic check-and-claim: only one request can transition queued → running
29
+ const claimed = db
30
+ .update(tasks)
31
+ .set({ status: "running", updatedAt: new Date() })
32
+ .where(and(eq(tasks.id, id), eq(tasks.status, "queued")))
33
+ .returning()
34
+ .all();
35
+
36
+ if (claimed.length === 0) {
37
+ // Either not found or not in queued status
38
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, id));
39
+ if (!task) {
40
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
41
+ }
42
+ return NextResponse.json(
43
+ { error: `Task must be queued to execute, current status: ${task.status}` },
44
+ { status: 400 }
45
+ );
46
+ }
47
+
48
+ const task = claimed[0];
49
+
50
+ // Auto-classify profile if none was set
51
+ if (!task.agentProfile) {
52
+ const autoProfile = classifyTaskProfile(
53
+ task.title,
54
+ task.description,
55
+ task.assignedAgent ?? DEFAULT_AGENT_RUNTIME
56
+ );
57
+ db.update(tasks)
58
+ .set({ agentProfile: autoProfile, updatedAt: new Date() })
59
+ .where(eq(tasks.id, id))
60
+ .run();
61
+ }
62
+
63
+ const compatibilityError = validateRuntimeProfileAssignment({
64
+ profileId: task.agentProfile ?? classifyTaskProfile(
65
+ task.title,
66
+ task.description,
67
+ task.assignedAgent ?? DEFAULT_AGENT_RUNTIME
68
+ ),
69
+ runtimeId: task.assignedAgent,
70
+ context: "Task profile",
71
+ });
72
+ if (compatibilityError) {
73
+ db.update(tasks)
74
+ .set({
75
+ status: "failed",
76
+ result: compatibilityError,
77
+ updatedAt: new Date(),
78
+ })
79
+ .where(eq(tasks.id, id))
80
+ .run();
81
+ return NextResponse.json({ error: compatibilityError }, { status: 400 });
82
+ }
83
+
84
+ // Fire-and-forget — task already marked as running
85
+ executeTaskWithAgent(id, task.assignedAgent ?? DEFAULT_AGENT_RUNTIME).catch(
86
+ (err) => console.error(`Task ${id} execution error:`, err)
87
+ );
88
+
89
+ return NextResponse.json({ message: "Execution started" }, { status: 202 });
90
+ }
@@ -0,0 +1,95 @@
1
+ import { NextRequest } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { agentLogs } from "@/lib/db/schema";
4
+ import { eq, and, gt } from "drizzle-orm";
5
+
6
+ export async function GET(
7
+ req: NextRequest,
8
+ { params }: { params: Promise<{ id: string }> }
9
+ ) {
10
+ const { id: taskId } = await params;
11
+
12
+ const encoder = new TextEncoder();
13
+ let lastTimestamp = new Date(0);
14
+ let closed = false;
15
+
16
+ const stream = new ReadableStream({
17
+ async start(controller) {
18
+ const send = (data: string) => {
19
+ if (closed) return;
20
+ try {
21
+ controller.enqueue(encoder.encode(`data: ${data}\n\n`));
22
+ } catch {
23
+ closed = true;
24
+ }
25
+ };
26
+
27
+ const poll = async () => {
28
+ while (!closed) {
29
+ try {
30
+ const logs = await db
31
+ .select()
32
+ .from(agentLogs)
33
+ .where(
34
+ and(
35
+ eq(agentLogs.taskId, taskId),
36
+ gt(agentLogs.timestamp, lastTimestamp)
37
+ )
38
+ )
39
+ .orderBy(agentLogs.timestamp);
40
+
41
+ for (const log of logs) {
42
+ send(JSON.stringify(log));
43
+ if (log.timestamp > lastTimestamp) {
44
+ lastTimestamp = log.timestamp;
45
+ }
46
+ }
47
+ } catch (err) {
48
+ console.error(`[SSE /tasks/${taskId}/logs] DB poll error:`, err);
49
+ send(JSON.stringify({ type: "error", message: "Database query failed" }));
50
+ // Back off on error before retrying
51
+ await new Promise((resolve) => setTimeout(resolve, 2000));
52
+ continue;
53
+ }
54
+
55
+ await new Promise((resolve) => setTimeout(resolve, 500));
56
+ }
57
+ };
58
+
59
+ // Keepalive
60
+ const keepalive = setInterval(() => {
61
+ if (closed) {
62
+ clearInterval(keepalive);
63
+ return;
64
+ }
65
+ try {
66
+ controller.enqueue(encoder.encode(": keepalive\n\n"));
67
+ } catch {
68
+ closed = true;
69
+ clearInterval(keepalive);
70
+ }
71
+ }, 15_000);
72
+
73
+ // Clean up on abort
74
+ req.signal.addEventListener("abort", () => {
75
+ closed = true;
76
+ clearInterval(keepalive);
77
+ try {
78
+ controller.close();
79
+ } catch {
80
+ // Already closed
81
+ }
82
+ });
83
+
84
+ poll();
85
+ },
86
+ });
87
+
88
+ return new Response(stream, {
89
+ headers: {
90
+ "Content-Type": "text/event-stream",
91
+ "Cache-Control": "no-cache",
92
+ Connection: "keep-alive",
93
+ },
94
+ });
95
+ }
@@ -0,0 +1,47 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { tasks } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+
6
+ export async function GET(
7
+ _req: NextRequest,
8
+ { params }: { params: Promise<{ id: string }> }
9
+ ) {
10
+ const { id } = await params;
11
+
12
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, id));
13
+
14
+ if (!task) {
15
+ return NextResponse.json({ error: "Task not found" }, { status: 404 });
16
+ }
17
+
18
+ // Detect content type from the result
19
+ const result = task.result ?? "";
20
+ let contentType: "text" | "markdown" | "code" | "json" | "unknown" = "text";
21
+
22
+ if (result.startsWith("{") || result.startsWith("[")) {
23
+ try {
24
+ JSON.parse(result);
25
+ contentType = "json";
26
+ } catch {
27
+ contentType = "text";
28
+ }
29
+ } else if (result.includes("```") || result.includes("# ") || result.includes("**")) {
30
+ contentType = "markdown";
31
+ } else if (
32
+ result.includes("function ") ||
33
+ result.includes("const ") ||
34
+ result.includes("import ") ||
35
+ result.includes("def ") ||
36
+ result.includes("class ")
37
+ ) {
38
+ contentType = "code";
39
+ }
40
+
41
+ return NextResponse.json({
42
+ taskId: id,
43
+ status: task.status,
44
+ result,
45
+ contentType,
46
+ });
47
+ }
@@ -0,0 +1,64 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { notifications } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { z } from "zod";
6
+
7
+ const respondSchema = z.object({
8
+ notificationId: z.string().min(1),
9
+ behavior: z.enum(["allow", "deny"]),
10
+ message: z.string().optional(),
11
+ updatedInput: z.unknown().optional(),
12
+ alwaysAllow: z.boolean().optional(),
13
+ permissionPattern: z.string().optional(),
14
+ });
15
+
16
+ export async function POST(
17
+ req: NextRequest,
18
+ { params }: { params: Promise<{ id: string }> }
19
+ ) {
20
+ const { id } = await params;
21
+ const body = await req.json();
22
+ const parsed = respondSchema.safeParse(body);
23
+
24
+ if (!parsed.success) {
25
+ return NextResponse.json(
26
+ { error: "notificationId (string) and behavior ('allow' | 'deny') are required" },
27
+ { status: 400 }
28
+ );
29
+ }
30
+
31
+ const { notificationId, behavior, message, updatedInput, alwaysAllow, permissionPattern } = parsed.data;
32
+
33
+ const [notification] = await db
34
+ .select()
35
+ .from(notifications)
36
+ .where(eq(notifications.id, notificationId));
37
+
38
+ if (!notification) {
39
+ return NextResponse.json({ error: "Notification not found" }, { status: 404 });
40
+ }
41
+
42
+ if (notification.response) {
43
+ return NextResponse.json({ error: "Already responded" }, { status: 409 });
44
+ }
45
+
46
+ // Write response — the polling loop in claude-agent.ts will detect this
47
+ const responseData = { behavior, message, updatedInput, alwaysAllow };
48
+ await db
49
+ .update(notifications)
50
+ .set({
51
+ response: JSON.stringify(responseData),
52
+ respondedAt: new Date(),
53
+ read: true,
54
+ })
55
+ .where(eq(notifications.id, notificationId));
56
+
57
+ // Save "Always Allow" permission if requested
58
+ if (behavior === "allow" && alwaysAllow && permissionPattern) {
59
+ const { addAllowedPermission } = await import("@/lib/settings/permissions");
60
+ await addAllowedPermission(permissionPattern);
61
+ }
62
+
63
+ return NextResponse.json({ success: true });
64
+ }
@@ -0,0 +1,76 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { tasks } from "@/lib/db/schema";
4
+ import { eq, and, inArray } from "drizzle-orm";
5
+ import { resumeTaskWithAgent } from "@/lib/agents/router";
6
+ import { MAX_RESUME_COUNT } from "@/lib/constants/task-status";
7
+ import { DEFAULT_AGENT_RUNTIME } from "@/lib/agents/runtime/catalog";
8
+ import {
9
+ BudgetLimitExceededError,
10
+ enforceTaskBudgetGuardrails,
11
+ } from "@/lib/settings/budget-guardrails";
12
+
13
+ export async function POST(
14
+ _req: NextRequest,
15
+ { params }: { params: Promise<{ id: string }> }
16
+ ) {
17
+ const { id } = await params;
18
+
19
+ // Check task exists and has a session
20
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, id));
21
+ if (!task) {
22
+ return NextResponse.json({ error: "Not found" }, { status: 404 });
23
+ }
24
+
25
+ if (!task.sessionId) {
26
+ return NextResponse.json(
27
+ { error: "No session to resume — use Retry instead" },
28
+ { status: 400 }
29
+ );
30
+ }
31
+
32
+ if (task.resumeCount >= MAX_RESUME_COUNT) {
33
+ return NextResponse.json(
34
+ { error: "Resume limit reached. Re-queue for fresh start." },
35
+ { status: 400 }
36
+ );
37
+ }
38
+
39
+ try {
40
+ await enforceTaskBudgetGuardrails(id, { isResume: true });
41
+ } catch (error) {
42
+ if (error instanceof BudgetLimitExceededError) {
43
+ return NextResponse.json({ error: error.message }, { status: 429 });
44
+ }
45
+ throw error;
46
+ }
47
+
48
+ // Atomic claim: failed/cancelled → running
49
+ const claimed = db
50
+ .update(tasks)
51
+ .set({ status: "running", updatedAt: new Date() })
52
+ .where(
53
+ and(
54
+ eq(tasks.id, id),
55
+ inArray(tasks.status, ["failed", "cancelled"])
56
+ )
57
+ )
58
+ .returning()
59
+ .all();
60
+
61
+ if (claimed.length === 0) {
62
+ return NextResponse.json(
63
+ {
64
+ error: `Task must be failed or cancelled to resume, current status: ${task.status}`,
65
+ },
66
+ { status: 400 }
67
+ );
68
+ }
69
+
70
+ // Fire-and-forget
71
+ resumeTaskWithAgent(id, task.assignedAgent ?? DEFAULT_AGENT_RUNTIME).catch((err) =>
72
+ console.error(`Task ${id} resume error:`, err)
73
+ );
74
+
75
+ return NextResponse.json({ message: "Resume started" }, { status: 202 });
76
+ }
@@ -0,0 +1,77 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { tasks } from "@/lib/db/schema";
4
+ import { eq } from "drizzle-orm";
5
+ import { updateTaskSchema } from "@/lib/validators/task";
6
+ import { isValidTransition, type TaskStatus } from "@/lib/constants/task-status";
7
+ import { validateRuntimeProfileAssignment } from "@/lib/agents/profiles/assignment-validation";
8
+
9
+ export async function GET(
10
+ _req: NextRequest,
11
+ { params }: { params: Promise<{ id: string }> }
12
+ ) {
13
+ const { id } = await params;
14
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, id));
15
+ if (!task) return NextResponse.json({ error: "Not found" }, { status: 404 });
16
+ return NextResponse.json(task);
17
+ }
18
+
19
+ export async function PATCH(
20
+ req: NextRequest,
21
+ { params }: { params: Promise<{ id: string }> }
22
+ ) {
23
+ const { id } = await params;
24
+ const body = await req.json();
25
+ const parsed = updateTaskSchema.safeParse(body);
26
+ if (!parsed.success) {
27
+ return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
28
+ }
29
+
30
+ const [existing] = await db.select().from(tasks).where(eq(tasks.id, id));
31
+ if (!existing) return NextResponse.json({ error: "Not found" }, { status: 404 });
32
+
33
+ const compatibilityError = validateRuntimeProfileAssignment({
34
+ profileId:
35
+ parsed.data.agentProfile !== undefined
36
+ ? parsed.data.agentProfile
37
+ : existing.agentProfile,
38
+ runtimeId:
39
+ parsed.data.assignedAgent !== undefined
40
+ ? parsed.data.assignedAgent
41
+ : existing.assignedAgent,
42
+ context: "Task profile",
43
+ });
44
+ if (compatibilityError) {
45
+ return NextResponse.json({ error: compatibilityError }, { status: 400 });
46
+ }
47
+
48
+ // Validate status transitions
49
+ if (parsed.data.status && parsed.data.status !== existing.status) {
50
+ if (!isValidTransition(existing.status as TaskStatus, parsed.data.status as TaskStatus)) {
51
+ return NextResponse.json(
52
+ { error: `Invalid transition from ${existing.status} to ${parsed.data.status}` },
53
+ { status: 400 }
54
+ );
55
+ }
56
+ }
57
+
58
+ await db
59
+ .update(tasks)
60
+ .set({ ...parsed.data, updatedAt: new Date() })
61
+ .where(eq(tasks.id, id));
62
+
63
+ const [updated] = await db.select().from(tasks).where(eq(tasks.id, id));
64
+ return NextResponse.json(updated);
65
+ }
66
+
67
+ export async function DELETE(
68
+ _req: NextRequest,
69
+ { params }: { params: Promise<{ id: string }> }
70
+ ) {
71
+ const { id } = await params;
72
+ const [existing] = await db.select().from(tasks).where(eq(tasks.id, id));
73
+ if (!existing) return NextResponse.json({ error: "Not found" }, { status: 404 });
74
+
75
+ await db.delete(tasks).where(eq(tasks.id, id));
76
+ return NextResponse.json({ success: true });
77
+ }
@@ -0,0 +1,35 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { runTaskAssistWithRuntime } from "@/lib/agents/runtime";
3
+ import type { TaskAssistResponse } from "@/lib/agents/runtime/task-assist-types";
4
+ import { BudgetLimitExceededError } from "@/lib/settings/budget-guardrails";
5
+
6
+ export async function POST(req: NextRequest) {
7
+ const body = await req.json();
8
+ const { title, description, assignedAgent } = body as {
9
+ title?: string;
10
+ description?: string;
11
+ assignedAgent?: string;
12
+ };
13
+
14
+ if (!title?.trim() && !description?.trim()) {
15
+ return NextResponse.json(
16
+ { error: "Provide at least a title or description" },
17
+ { status: 400 }
18
+ );
19
+ }
20
+
21
+ try {
22
+ const parsed: TaskAssistResponse = await runTaskAssistWithRuntime(
23
+ { title, description },
24
+ assignedAgent
25
+ );
26
+ return NextResponse.json(parsed);
27
+ } catch (error) {
28
+ if (error instanceof BudgetLimitExceededError) {
29
+ return NextResponse.json({ error: error.message }, { status: 429 });
30
+ }
31
+ const message =
32
+ error instanceof Error ? error.message : "AI assist failed";
33
+ return NextResponse.json({ error: message }, { status: 500 });
34
+ }
35
+ }
@@ -0,0 +1,82 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { db } from "@/lib/db";
3
+ import { tasks, documents } from "@/lib/db/schema";
4
+ import { eq, and, desc } from "drizzle-orm";
5
+ import { createTaskSchema } from "@/lib/validators/task";
6
+ import { processDocument } from "@/lib/documents/processor";
7
+ import { validateRuntimeProfileAssignment } from "@/lib/agents/profiles/assignment-validation";
8
+
9
+ export async function GET(req: NextRequest) {
10
+ const url = new URL(req.url);
11
+ const projectId = url.searchParams.get("projectId");
12
+ const status = url.searchParams.get("status");
13
+
14
+ const conditions = [];
15
+ if (projectId) conditions.push(eq(tasks.projectId, projectId));
16
+ if (status) conditions.push(eq(tasks.status, status as typeof tasks.status.enumValues[number]));
17
+
18
+ const result = await db
19
+ .select()
20
+ .from(tasks)
21
+ .where(conditions.length > 0 ? and(...conditions) : undefined)
22
+ .orderBy(tasks.priority, desc(tasks.createdAt));
23
+
24
+ return NextResponse.json(result);
25
+ }
26
+
27
+ export async function POST(req: NextRequest) {
28
+ const body = await req.json();
29
+ const parsed = createTaskSchema.safeParse(body);
30
+ if (!parsed.success) {
31
+ return NextResponse.json({ error: parsed.error.flatten() }, { status: 400 });
32
+ }
33
+
34
+ const compatibilityError = validateRuntimeProfileAssignment({
35
+ profileId: parsed.data.agentProfile,
36
+ runtimeId: parsed.data.assignedAgent,
37
+ context: "Task profile",
38
+ });
39
+ if (compatibilityError) {
40
+ return NextResponse.json({ error: compatibilityError }, { status: 400 });
41
+ }
42
+
43
+ const now = new Date();
44
+ const id = crypto.randomUUID();
45
+
46
+ await db.insert(tasks).values({
47
+ id,
48
+ title: parsed.data.title,
49
+ description: parsed.data.description ?? null,
50
+ projectId: parsed.data.projectId ?? null,
51
+ priority: parsed.data.priority,
52
+ assignedAgent: parsed.data.assignedAgent ?? null,
53
+ agentProfile: parsed.data.agentProfile ?? null,
54
+ status: "planned",
55
+ createdAt: now,
56
+ updatedAt: now,
57
+ });
58
+
59
+ // Link already-uploaded documents to this task
60
+ if (parsed.data.fileIds && parsed.data.fileIds.length > 0) {
61
+ try {
62
+ for (const fileId of parsed.data.fileIds) {
63
+ // Update existing document record (created by /api/uploads) to link to this task
64
+ await db.update(documents)
65
+ .set({
66
+ taskId: id,
67
+ projectId: parsed.data.projectId ?? null,
68
+ updatedAt: now,
69
+ })
70
+ .where(eq(documents.id, fileId));
71
+
72
+ // Trigger processing if not already done (fire-and-forget)
73
+ processDocument(fileId).catch(() => {});
74
+ }
75
+ } catch {
76
+ // File association is best-effort — don't fail task creation
77
+ }
78
+ }
79
+
80
+ const [task] = await db.select().from(tasks).where(eq(tasks.id, id));
81
+ return NextResponse.json(task, { status: 201 });
82
+ }
@@ -0,0 +1,81 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { readFile, readdir, unlink } from "fs/promises";
3
+ import { join } from "path";
4
+ import { db } from "@/lib/db";
5
+ import { documents } from "@/lib/db/schema";
6
+ import { getStagentUploadsDir } from "@/lib/utils/stagent-paths";
7
+ import { eq } from "drizzle-orm";
8
+
9
+ const UPLOAD_DIR = getStagentUploadsDir();
10
+
11
+ const MIME_TYPES: Record<string, string> = {
12
+ txt: "text/plain",
13
+ md: "text/markdown",
14
+ json: "application/json",
15
+ js: "text/javascript",
16
+ ts: "text/typescript",
17
+ py: "text/x-python",
18
+ html: "text/html",
19
+ css: "text/css",
20
+ png: "image/png",
21
+ jpg: "image/jpeg",
22
+ jpeg: "image/jpeg",
23
+ gif: "image/gif",
24
+ svg: "image/svg+xml",
25
+ pdf: "application/pdf",
26
+ };
27
+
28
+ export async function GET(
29
+ _req: NextRequest,
30
+ { params }: { params: Promise<{ id: string }> }
31
+ ) {
32
+ const { id } = await params;
33
+
34
+ try {
35
+ const files = await readdir(UPLOAD_DIR);
36
+ const match = files.find((f) => f.startsWith(id));
37
+ if (!match) {
38
+ return NextResponse.json({ error: "File not found" }, { status: 404 });
39
+ }
40
+
41
+ const filepath = join(UPLOAD_DIR, match);
42
+ const data = await readFile(filepath);
43
+ const ext = match.split(".").pop()?.toLowerCase() ?? "";
44
+ const contentType = MIME_TYPES[ext] ?? "application/octet-stream";
45
+
46
+ // Force download for all files to prevent stored XSS via inline rendering
47
+ // of attacker-controlled HTML/SVG/JS content
48
+ return new NextResponse(data, {
49
+ headers: {
50
+ "Content-Type": contentType,
51
+ "Content-Disposition": `attachment; filename="${match}"`,
52
+ "X-Content-Type-Options": "nosniff",
53
+ },
54
+ });
55
+ } catch {
56
+ return NextResponse.json({ error: "File not found" }, { status: 404 });
57
+ }
58
+ }
59
+
60
+ export async function DELETE(
61
+ _req: NextRequest,
62
+ { params }: { params: Promise<{ id: string }> }
63
+ ) {
64
+ const { id } = await params;
65
+
66
+ try {
67
+ const files = await readdir(UPLOAD_DIR);
68
+ const match = files.find((f) => f.startsWith(id));
69
+ if (!match) {
70
+ return NextResponse.json({ error: "File not found" }, { status: 404 });
71
+ }
72
+
73
+ const filepath = join(UPLOAD_DIR, match);
74
+ await unlink(filepath);
75
+ await db.delete(documents).where(eq(documents.id, id));
76
+
77
+ return new NextResponse(null, { status: 204 });
78
+ } catch {
79
+ return NextResponse.json({ error: "Failed to delete file" }, { status: 500 });
80
+ }
81
+ }
@@ -0,0 +1,7 @@
1
+ import { NextResponse } from "next/server";
2
+ import { cleanupOrphanedUploads } from "@/lib/documents/cleanup";
3
+
4
+ export async function POST() {
5
+ const result = await cleanupOrphanedUploads();
6
+ return NextResponse.json(result);
7
+ }