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,227 @@
1
+ import { sqliteTable, text, integer, index } from "drizzle-orm/sqlite-core";
2
+ import type { InferSelectModel } from "drizzle-orm";
3
+
4
+ export const projects = sqliteTable("projects", {
5
+ id: text("id").primaryKey(),
6
+ name: text("name").notNull(),
7
+ description: text("description"),
8
+ workingDirectory: text("working_directory"),
9
+ status: text("status", { enum: ["active", "paused", "completed"] })
10
+ .default("active")
11
+ .notNull(),
12
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
13
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
14
+ });
15
+
16
+ export const tasks = sqliteTable(
17
+ "tasks",
18
+ {
19
+ id: text("id").primaryKey(),
20
+ projectId: text("project_id").references(() => projects.id),
21
+ workflowId: text("workflow_id").references(() => workflows.id),
22
+ scheduleId: text("schedule_id").references(() => schedules.id),
23
+ title: text("title").notNull(),
24
+ description: text("description"),
25
+ status: text("status", {
26
+ enum: ["planned", "queued", "running", "completed", "failed", "cancelled"],
27
+ })
28
+ .default("planned")
29
+ .notNull(),
30
+ assignedAgent: text("assigned_agent"),
31
+ agentProfile: text("agent_profile"),
32
+ priority: integer("priority").default(2).notNull(),
33
+ result: text("result"),
34
+ sessionId: text("session_id"),
35
+ resumeCount: integer("resume_count").default(0).notNull(),
36
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
37
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
38
+ },
39
+ (table) => [
40
+ index("idx_tasks_status").on(table.status),
41
+ index("idx_tasks_project_id").on(table.projectId),
42
+ index("idx_tasks_workflow_id").on(table.workflowId),
43
+ index("idx_tasks_schedule_id").on(table.scheduleId),
44
+ index("idx_tasks_agent_profile").on(table.agentProfile),
45
+ ]
46
+ );
47
+
48
+ export const workflows = sqliteTable("workflows", {
49
+ id: text("id").primaryKey(),
50
+ projectId: text("project_id").references(() => projects.id),
51
+ name: text("name").notNull(),
52
+ definition: text("definition").notNull(),
53
+ status: text("status", {
54
+ enum: ["draft", "active", "paused", "completed", "failed"],
55
+ })
56
+ .default("draft")
57
+ .notNull(),
58
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
59
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
60
+ });
61
+
62
+ export const agentLogs = sqliteTable(
63
+ "agent_logs",
64
+ {
65
+ id: text("id").primaryKey(),
66
+ taskId: text("task_id").references(() => tasks.id),
67
+ agentType: text("agent_type").notNull(),
68
+ event: text("event").notNull(),
69
+ payload: text("payload"),
70
+ timestamp: integer("timestamp", { mode: "timestamp" }).notNull(),
71
+ },
72
+ (table) => [
73
+ index("idx_agent_logs_task_id").on(table.taskId),
74
+ index("idx_agent_logs_timestamp").on(table.timestamp),
75
+ ]
76
+ );
77
+
78
+ export const notifications = sqliteTable(
79
+ "notifications",
80
+ {
81
+ id: text("id").primaryKey(),
82
+ taskId: text("task_id").references(() => tasks.id),
83
+ type: text("type", {
84
+ enum: [
85
+ "permission_required",
86
+ "task_completed",
87
+ "task_failed",
88
+ "agent_message",
89
+ "budget_alert",
90
+ ],
91
+ }).notNull(),
92
+ title: text("title").notNull(),
93
+ body: text("body"),
94
+ read: integer("read", { mode: "boolean" }).default(false).notNull(),
95
+ toolName: text("tool_name"),
96
+ toolInput: text("tool_input"),
97
+ response: text("response"),
98
+ respondedAt: integer("responded_at", { mode: "timestamp" }),
99
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
100
+ },
101
+ (table) => [
102
+ index("idx_notifications_task_id").on(table.taskId),
103
+ index("idx_notifications_read").on(table.read),
104
+ ]
105
+ );
106
+
107
+ export const documents = sqliteTable(
108
+ "documents",
109
+ {
110
+ id: text("id").primaryKey(),
111
+ taskId: text("task_id").references(() => tasks.id),
112
+ projectId: text("project_id").references(() => projects.id),
113
+ filename: text("filename").notNull(),
114
+ originalName: text("original_name").notNull(),
115
+ mimeType: text("mime_type").notNull(),
116
+ size: integer("size").notNull(),
117
+ storagePath: text("storage_path").notNull(),
118
+ version: integer("version").default(1).notNull(),
119
+ direction: text("direction", { enum: ["input", "output"] })
120
+ .default("input")
121
+ .notNull(),
122
+ category: text("category"),
123
+ status: text("status", {
124
+ enum: ["uploaded", "processing", "ready", "error"],
125
+ })
126
+ .default("uploaded")
127
+ .notNull(),
128
+ extractedText: text("extracted_text"),
129
+ processedPath: text("processed_path"),
130
+ processingError: text("processing_error"),
131
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
132
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
133
+ },
134
+ (table) => [
135
+ index("idx_documents_task_id").on(table.taskId),
136
+ index("idx_documents_project_id").on(table.projectId),
137
+ ]
138
+ );
139
+
140
+ export const schedules = sqliteTable(
141
+ "schedules",
142
+ {
143
+ id: text("id").primaryKey(),
144
+ projectId: text("project_id").references(() => projects.id),
145
+ name: text("name").notNull(),
146
+ prompt: text("prompt").notNull(),
147
+ cronExpression: text("cron_expression").notNull(),
148
+ assignedAgent: text("assigned_agent"),
149
+ agentProfile: text("agent_profile"),
150
+ recurs: integer("recurs", { mode: "boolean" }).default(true).notNull(),
151
+ status: text("status", {
152
+ enum: ["active", "paused", "completed", "expired"],
153
+ })
154
+ .default("active")
155
+ .notNull(),
156
+ maxFirings: integer("max_firings"),
157
+ firingCount: integer("firing_count").default(0).notNull(),
158
+ expiresAt: integer("expires_at", { mode: "timestamp" }),
159
+ lastFiredAt: integer("last_fired_at", { mode: "timestamp" }),
160
+ nextFireAt: integer("next_fire_at", { mode: "timestamp" }),
161
+ createdAt: integer("created_at", { mode: "timestamp" }).notNull(),
162
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
163
+ },
164
+ (table) => [
165
+ index("idx_schedules_status").on(table.status),
166
+ index("idx_schedules_next_fire_at").on(table.nextFireAt),
167
+ index("idx_schedules_project_id").on(table.projectId),
168
+ ]
169
+ );
170
+
171
+ export const settings = sqliteTable("settings", {
172
+ key: text("key").primaryKey(),
173
+ value: text("value").notNull(),
174
+ updatedAt: integer("updated_at", { mode: "timestamp" }).notNull(),
175
+ });
176
+
177
+ export const usageLedger = sqliteTable(
178
+ "usage_ledger",
179
+ {
180
+ id: text("id").primaryKey(),
181
+ taskId: text("task_id").references(() => tasks.id),
182
+ workflowId: text("workflow_id").references(() => workflows.id),
183
+ scheduleId: text("schedule_id").references(() => schedules.id),
184
+ projectId: text("project_id").references(() => projects.id),
185
+ activityType: text("activity_type", {
186
+ enum: [
187
+ "task_run",
188
+ "task_resume",
189
+ "workflow_step",
190
+ "scheduled_firing",
191
+ "task_assist",
192
+ "profile_test",
193
+ ],
194
+ }).notNull(),
195
+ runtimeId: text("runtime_id").notNull(),
196
+ providerId: text("provider_id").notNull(),
197
+ modelId: text("model_id"),
198
+ status: text("status", {
199
+ enum: ["completed", "failed", "cancelled", "blocked", "unknown_pricing"],
200
+ }).notNull(),
201
+ inputTokens: integer("input_tokens"),
202
+ outputTokens: integer("output_tokens"),
203
+ totalTokens: integer("total_tokens"),
204
+ costMicros: integer("cost_micros"),
205
+ pricingVersion: text("pricing_version"),
206
+ startedAt: integer("started_at", { mode: "timestamp" }).notNull(),
207
+ finishedAt: integer("finished_at", { mode: "timestamp" }).notNull(),
208
+ },
209
+ (table) => [
210
+ index("idx_usage_ledger_task_id").on(table.taskId),
211
+ index("idx_usage_ledger_activity_type").on(table.activityType),
212
+ index("idx_usage_ledger_runtime_id").on(table.runtimeId),
213
+ index("idx_usage_ledger_provider_model").on(table.providerId, table.modelId),
214
+ index("idx_usage_ledger_finished_at").on(table.finishedAt),
215
+ ]
216
+ );
217
+
218
+ // Shared types derived from schema — use these in components instead of `as any`
219
+ export type ProjectRow = InferSelectModel<typeof projects>;
220
+ export type TaskRow = InferSelectModel<typeof tasks>;
221
+ export type WorkflowRow = InferSelectModel<typeof workflows>;
222
+ export type AgentLogRow = InferSelectModel<typeof agentLogs>;
223
+ export type NotificationRow = InferSelectModel<typeof notifications>;
224
+ export type DocumentRow = InferSelectModel<typeof documents>;
225
+ export type ScheduleRow = InferSelectModel<typeof schedules>;
226
+ export type SettingsRow = InferSelectModel<typeof settings>;
227
+ export type UsageLedgerRow = InferSelectModel<typeof usageLedger>;
@@ -0,0 +1,50 @@
1
+ import { readdir, stat, unlink } from "fs/promises";
2
+ import { join } from "path";
3
+ import { db } from "@/lib/db";
4
+ import { documents } from "@/lib/db/schema";
5
+ import { getStagentUploadsDir } from "@/lib/utils/stagent-paths";
6
+ import { eq } from "drizzle-orm";
7
+
8
+ const UPLOAD_DIR = getStagentUploadsDir();
9
+ const ORPHAN_AGE_MS = 24 * 60 * 60 * 1000; // 24 hours
10
+
11
+ export async function cleanupOrphanedUploads(): Promise<{
12
+ deleted: string[];
13
+ errors: string[];
14
+ }> {
15
+ const deleted: string[] = [];
16
+ const errors: string[] = [];
17
+
18
+ try {
19
+ const files = await readdir(UPLOAD_DIR);
20
+ const now = Date.now();
21
+
22
+ for (const filename of files) {
23
+ const fileId = filename.split(".")[0];
24
+ const filepath = join(UPLOAD_DIR, filename);
25
+
26
+ try {
27
+ const fileStat = await stat(filepath);
28
+ const ageMs = now - fileStat.mtimeMs;
29
+
30
+ if (ageMs < ORPHAN_AGE_MS) continue;
31
+
32
+ const [doc] = await db
33
+ .select()
34
+ .from(documents)
35
+ .where(eq(documents.id, fileId));
36
+
37
+ if (!doc) {
38
+ await unlink(filepath);
39
+ deleted.push(filename);
40
+ }
41
+ } catch (err) {
42
+ errors.push(`${filename}: ${err instanceof Error ? err.message : "unknown error"}`);
43
+ }
44
+ }
45
+ } catch {
46
+ // Upload directory may not exist yet
47
+ }
48
+
49
+ return { deleted, errors };
50
+ }
@@ -0,0 +1,75 @@
1
+ /**
2
+ * Build document context section for agent prompts.
3
+ * Queries documents linked to a task and formats them for the agent.
4
+ */
5
+
6
+ import { db } from "@/lib/db";
7
+ import { documents } from "@/lib/db/schema";
8
+ import { and, eq } from "drizzle-orm";
9
+ import type { DocumentRow } from "@/lib/db/schema";
10
+
11
+ const MAX_INLINE_TEXT = 10_000;
12
+
13
+ function formatDocument(doc: DocumentRow, index: number): string {
14
+ const header = `[Document ${index + 1}: ${doc.originalName}]`;
15
+ const pathLine = `Path: ${doc.storagePath}`;
16
+
17
+ const isImage = doc.mimeType.startsWith("image/");
18
+
19
+ // Images: path reference only (agent uses Read tool to view)
20
+ if (isImage) {
21
+ const meta = doc.extractedText ? `\n${doc.extractedText}` : "";
22
+ return `${header}\n${pathLine}\nType: ${doc.mimeType} (use Read tool to view)${meta}`;
23
+ }
24
+
25
+ // Processing or failed: path + status note
26
+ if (doc.status === "processing") {
27
+ return `${header}\n${pathLine}\nStatus: still processing — content not yet available`;
28
+ }
29
+
30
+ if (doc.status === "error") {
31
+ return `${header}\n${pathLine}\nStatus: processing failed (${doc.processingError ?? "unknown error"})`;
32
+ }
33
+
34
+ if (doc.status === "uploaded") {
35
+ return `${header}\n${pathLine}\nStatus: not yet processed`;
36
+ }
37
+
38
+ // Ready with extracted text
39
+ if (doc.extractedText) {
40
+ if (doc.extractedText.length < MAX_INLINE_TEXT) {
41
+ return `${header}\n${pathLine}\nContent:\n<document>\n${doc.extractedText}\n</document>`;
42
+ }
43
+ // Large document: truncated + path reference
44
+ const truncated = doc.extractedText.slice(0, MAX_INLINE_TEXT);
45
+ return `${header}\n${pathLine}\nContent (truncated to ${MAX_INLINE_TEXT} chars — use Read tool for full content):\n<document>\n${truncated}\n</document>`;
46
+ }
47
+
48
+ // Ready but no extracted text (unsupported format)
49
+ return `${header}\n${pathLine}\nType: ${doc.mimeType} (use Read tool to access)`;
50
+ }
51
+
52
+ /**
53
+ * Build the document context string for a task's prompt.
54
+ * Returns null if the task has no documents.
55
+ */
56
+ export async function buildDocumentContext(
57
+ taskId: string
58
+ ): Promise<string | null> {
59
+ const docs = await db
60
+ .select()
61
+ .from(documents)
62
+ .where(and(eq(documents.taskId, taskId), eq(documents.direction, "input")));
63
+
64
+ if (docs.length === 0) return null;
65
+
66
+ const sections = docs.map((doc, i) => formatDocument(doc, i));
67
+
68
+ return [
69
+ "--- Attached Documents ---",
70
+ "",
71
+ ...sections,
72
+ "",
73
+ "--- End Attached Documents ---",
74
+ ].join("\n");
75
+ }
@@ -0,0 +1,166 @@
1
+ import { homedir } from "node:os";
2
+ import path from "node:path";
3
+ import fs from "node:fs/promises";
4
+ import { db } from "@/lib/db";
5
+ import { documents, tasks } from "@/lib/db/schema";
6
+ import { and, eq } from "drizzle-orm";
7
+ import { processDocument } from "./processor";
8
+
9
+ const STAGENT_DATA_DIR =
10
+ process.env.STAGENT_DATA_DIR || path.join(homedir(), ".stagent");
11
+ const TASK_OUTPUTS_DIR = path.join(STAGENT_DATA_DIR, "outputs");
12
+ const OUTPUT_ARCHIVE_DIR = path.join(STAGENT_DATA_DIR, "documents", "output");
13
+
14
+ const OUTPUT_MIME_TYPES: Record<string, string> = {
15
+ ".md": "text/markdown",
16
+ ".txt": "text/plain",
17
+ ".json": "application/json",
18
+ ".csv": "text/csv",
19
+ ".html": "text/html",
20
+ };
21
+
22
+ export function getTaskOutputDirectory(taskId: string): string {
23
+ return path.join(TASK_OUTPUTS_DIR, taskId);
24
+ }
25
+
26
+ export async function prepareTaskOutputDirectory(
27
+ taskId: string,
28
+ options: { clearExisting?: boolean } = {}
29
+ ): Promise<string> {
30
+ const outputDir = getTaskOutputDirectory(taskId);
31
+
32
+ if (options.clearExisting) {
33
+ await fs.rm(outputDir, { recursive: true, force: true });
34
+ }
35
+
36
+ await fs.mkdir(outputDir, { recursive: true });
37
+ return outputDir;
38
+ }
39
+
40
+ export function buildTaskOutputInstructions(taskId: string): string {
41
+ const outputDir = getTaskOutputDirectory(taskId);
42
+ return [
43
+ "Generated file outputs:",
44
+ `- Write any final files to ${outputDir}`,
45
+ "- Files in .md, .json, .csv, .txt, and .html are automatically captured after completion",
46
+ "- Keep the final filename stable if you want Stagent to version rerun outputs cleanly",
47
+ ].join("\n");
48
+ }
49
+
50
+ async function listFilesRecursively(rootDir: string): Promise<string[]> {
51
+ const entries = await fs.readdir(rootDir, { withFileTypes: true });
52
+ const files = await Promise.all(
53
+ entries.map(async (entry) => {
54
+ const resolved = path.join(rootDir, entry.name);
55
+ if (entry.isDirectory()) {
56
+ return listFilesRecursively(resolved);
57
+ }
58
+ return [resolved];
59
+ })
60
+ );
61
+
62
+ return files.flat();
63
+ }
64
+
65
+ function resolveOutputMimeType(filename: string): string | null {
66
+ return OUTPUT_MIME_TYPES[path.extname(filename).toLowerCase()] ?? null;
67
+ }
68
+
69
+ function normalizeRelativePath(value: string): string {
70
+ return value.split(path.sep).join("/");
71
+ }
72
+
73
+ function buildArchivedFilename(relativePath: string, version: number): string {
74
+ const parsed = path.parse(relativePath);
75
+ const sanitizedBase = parsed.name.replace(/[^a-zA-Z0-9._-]+/g, "-");
76
+ const nestedPrefix = parsed.dir
77
+ ? `${parsed.dir.replace(/[\\/]+/g, "__")}__`
78
+ : "";
79
+ return `${nestedPrefix}${sanitizedBase || "output"}-v${version}${parsed.ext}`;
80
+ }
81
+
82
+ export async function scanTaskOutputDocuments(taskId: string): Promise<string[]> {
83
+ const outputDir = getTaskOutputDirectory(taskId);
84
+ const [task] = await db
85
+ .select({ id: tasks.id, projectId: tasks.projectId })
86
+ .from(tasks)
87
+ .where(eq(tasks.id, taskId));
88
+
89
+ if (!task) {
90
+ throw new Error(`Task ${taskId} not found`);
91
+ }
92
+
93
+ try {
94
+ await fs.access(outputDir);
95
+ } catch {
96
+ return [];
97
+ }
98
+
99
+ const existingOutputDocs = await db
100
+ .select({
101
+ originalName: documents.originalName,
102
+ version: documents.version,
103
+ })
104
+ .from(documents)
105
+ .where(
106
+ and(eq(documents.taskId, taskId), eq(documents.direction, "output"))
107
+ );
108
+
109
+ const versionMap = new Map<string, number>();
110
+ existingOutputDocs.forEach((doc) => {
111
+ versionMap.set(
112
+ doc.originalName,
113
+ Math.max(versionMap.get(doc.originalName) ?? 0, doc.version)
114
+ );
115
+ });
116
+
117
+ const discoveredFiles = await listFilesRecursively(outputDir);
118
+ const registeredDocumentIds: string[] = [];
119
+
120
+ for (const sourcePath of discoveredFiles) {
121
+ const relativePath = normalizeRelativePath(path.relative(outputDir, sourcePath));
122
+ const mimeType = resolveOutputMimeType(relativePath);
123
+ if (!mimeType) {
124
+ continue;
125
+ }
126
+
127
+ const stats = await fs.stat(sourcePath);
128
+ if (!stats.isFile()) {
129
+ continue;
130
+ }
131
+
132
+ const nextVersion = (versionMap.get(relativePath) ?? 0) + 1;
133
+ versionMap.set(relativePath, nextVersion);
134
+
135
+ const archiveDir = path.join(OUTPUT_ARCHIVE_DIR, taskId);
136
+ await fs.mkdir(archiveDir, { recursive: true });
137
+
138
+ const archivedFilename = buildArchivedFilename(relativePath, nextVersion);
139
+ const archivedPath = path.join(archiveDir, archivedFilename);
140
+ await fs.copyFile(sourcePath, archivedPath);
141
+
142
+ const documentId = crypto.randomUUID();
143
+ const now = new Date();
144
+
145
+ await db.insert(documents).values({
146
+ id: documentId,
147
+ taskId,
148
+ projectId: task.projectId ?? null,
149
+ filename: archivedFilename,
150
+ originalName: relativePath,
151
+ mimeType,
152
+ size: stats.size,
153
+ storagePath: archivedPath,
154
+ version: nextVersion,
155
+ direction: "output",
156
+ status: "uploaded",
157
+ createdAt: now,
158
+ updatedAt: now,
159
+ });
160
+
161
+ await processDocument(documentId);
162
+ registeredDocumentIds.push(documentId);
163
+ }
164
+
165
+ return registeredDocumentIds;
166
+ }
@@ -0,0 +1,120 @@
1
+ /**
2
+ * Document processing pipeline orchestrator.
3
+ * Registers all processors and provides the fire-and-forget processDocument entry point.
4
+ */
5
+
6
+ import { db } from "@/lib/db";
7
+ import { documents } from "@/lib/db/schema";
8
+ import { eq } from "drizzle-orm";
9
+ import { registerProcessor, getProcessor } from "./registry";
10
+ import { processText } from "./processors/text";
11
+ import { processPdf } from "./processors/pdf";
12
+ import { processImage } from "./processors/image";
13
+ import { processDocx, processPptx } from "./processors/office";
14
+ import { processSpreadsheet } from "./processors/spreadsheet";
15
+
16
+ // Register all processors by MIME type
17
+ // Text-based formats
18
+ const textMimeTypes = [
19
+ "text/plain",
20
+ "text/markdown",
21
+ "application/json",
22
+ "text/javascript",
23
+ "text/typescript",
24
+ "text/x-python",
25
+ "text/html",
26
+ "text/css",
27
+ "text/yaml",
28
+ "application/x-yaml",
29
+ ];
30
+ for (const mime of textMimeTypes) {
31
+ registerProcessor(mime, processText);
32
+ }
33
+
34
+ // PDF
35
+ registerProcessor("application/pdf", processPdf);
36
+
37
+ // Images
38
+ const imageMimeTypes = ["image/png", "image/jpeg", "image/gif", "image/webp"];
39
+ for (const mime of imageMimeTypes) {
40
+ registerProcessor(mime, processImage);
41
+ }
42
+
43
+ // Office documents
44
+ registerProcessor(
45
+ "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
46
+ processDocx
47
+ );
48
+ registerProcessor(
49
+ "application/vnd.openxmlformats-officedocument.presentationml.presentation",
50
+ processPptx
51
+ );
52
+
53
+ // Spreadsheets
54
+ registerProcessor(
55
+ "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
56
+ processSpreadsheet
57
+ );
58
+ registerProcessor("text/csv", processSpreadsheet);
59
+
60
+ /**
61
+ * Process a document asynchronously. Updates the document record with
62
+ * extracted text or error. Never throws — errors are captured in DB.
63
+ */
64
+ export async function processDocument(documentId: string): Promise<void> {
65
+ const [doc] = await db
66
+ .select()
67
+ .from(documents)
68
+ .where(eq(documents.id, documentId));
69
+
70
+ if (!doc) return;
71
+
72
+ // Mark as processing
73
+ await db
74
+ .update(documents)
75
+ .set({ status: "processing", updatedAt: new Date() })
76
+ .where(eq(documents.id, documentId));
77
+
78
+ const processor = getProcessor(doc.mimeType);
79
+
80
+ if (!processor) {
81
+ // No processor for this type — mark as ready with empty text
82
+ await db
83
+ .update(documents)
84
+ .set({
85
+ status: "ready",
86
+ extractedText: null,
87
+ processingError: `No processor for MIME type: ${doc.mimeType}`,
88
+ updatedAt: new Date(),
89
+ })
90
+ .where(eq(documents.id, documentId));
91
+ return;
92
+ }
93
+
94
+ try {
95
+ const result = await processor(doc.storagePath);
96
+
97
+ await db
98
+ .update(documents)
99
+ .set({
100
+ status: "ready",
101
+ extractedText: result.extractedText,
102
+ processedPath: result.processedPath ?? null,
103
+ processingError: null,
104
+ updatedAt: new Date(),
105
+ })
106
+ .where(eq(documents.id, documentId));
107
+ } catch (error: unknown) {
108
+ const errorMessage =
109
+ error instanceof Error ? error.message : String(error);
110
+
111
+ await db
112
+ .update(documents)
113
+ .set({
114
+ status: "error",
115
+ processingError: errorMessage,
116
+ updatedAt: new Date(),
117
+ })
118
+ .where(eq(documents.id, documentId));
119
+ }
120
+ }
@@ -0,0 +1,21 @@
1
+ import { readFile } from "fs/promises";
2
+ import type { ProcessorResult } from "../registry";
3
+
4
+ const SUPPORTED_FORMATS = new Set(["png", "jpg", "jpeg", "gif", "webp"]);
5
+
6
+ /** Extract image dimensions metadata — agents use the file path to view images */
7
+ export async function processImage(filePath: string): Promise<ProcessorResult> {
8
+ const { imageSize } = await import("image-size");
9
+ const buffer = await readFile(filePath);
10
+ const dimensions = imageSize(new Uint8Array(buffer));
11
+
12
+ if (dimensions.type && !SUPPORTED_FORMATS.has(dimensions.type)) {
13
+ throw new Error(`Unsupported image format: ${dimensions.type}`);
14
+ }
15
+
16
+ const meta = [
17
+ `Image: ${dimensions.width}x${dimensions.height}`,
18
+ `Format: ${dimensions.type}`,
19
+ ].join("\n");
20
+ return { extractedText: meta };
21
+ }
@@ -0,0 +1,36 @@
1
+ import { readFile } from "fs/promises";
2
+ import type { ProcessorResult } from "../registry";
3
+
4
+ /** Extract text from DOCX files using mammoth */
5
+ export async function processDocx(filePath: string): Promise<ProcessorResult> {
6
+ const mammoth = await import("mammoth");
7
+ const buffer = await readFile(filePath);
8
+ const result = await mammoth.extractRawText({ buffer });
9
+ return { extractedText: result.value };
10
+ }
11
+
12
+ /** Extract text from PPTX files by parsing the XML slide contents */
13
+ export async function processPptx(filePath: string): Promise<ProcessorResult> {
14
+ const JSZip = (await import("jszip")).default;
15
+ const buffer = await readFile(filePath);
16
+ const zip = await JSZip.loadAsync(buffer);
17
+
18
+ const slideTexts: string[] = [];
19
+ const slideFiles = Object.keys(zip.files)
20
+ .filter((name) => name.match(/^ppt\/slides\/slide\d+\.xml$/))
21
+ .sort();
22
+
23
+ for (const slideFile of slideFiles) {
24
+ const xml = await zip.files[slideFile].async("text");
25
+ // Extract text between <a:t> tags
26
+ const texts = xml.match(/<a:t>([^<]*)<\/a:t>/g);
27
+ if (texts) {
28
+ const slideText = texts
29
+ .map((t) => t.replace(/<\/?a:t>/g, ""))
30
+ .join(" ");
31
+ slideTexts.push(slideText);
32
+ }
33
+ }
34
+
35
+ return { extractedText: slideTexts.join("\n\n") };
36
+ }