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.
- package/LICENSE +191 -0
- package/README.md +399 -0
- package/components.json +21 -0
- package/dist/cli.js +171 -0
- package/drizzle.config.ts +12 -0
- package/next.config.mjs +15 -0
- package/package.json +114 -0
- package/postcss.config.mjs +8 -0
- package/public/icon-512.png +0 -0
- package/public/icon.svg +13 -0
- package/public/readme/home-workspace.png +0 -0
- package/public/readme/inbox-approvals.png +0 -0
- package/public/readme/workflow-blueprints.png +0 -0
- package/public/stagent-s-128.png +0 -0
- package/public/stagent-s-64.png +0 -0
- package/src/app/api/blueprints/[id]/instantiate/route.ts +27 -0
- package/src/app/api/blueprints/[id]/route.ts +39 -0
- package/src/app/api/blueprints/import/route.ts +68 -0
- package/src/app/api/blueprints/route.ts +29 -0
- package/src/app/api/command-palette/recent/route.ts +31 -0
- package/src/app/api/data/clear/route.ts +22 -0
- package/src/app/api/data/seed/route.ts +22 -0
- package/src/app/api/documents/[id]/file/route.ts +44 -0
- package/src/app/api/documents/[id]/route.ts +123 -0
- package/src/app/api/documents/route.ts +59 -0
- package/src/app/api/logs/stream/route.ts +101 -0
- package/src/app/api/notifications/[id]/route.ts +36 -0
- package/src/app/api/notifications/mark-all-read/route.ts +13 -0
- package/src/app/api/notifications/pending-approvals/route.ts +10 -0
- package/src/app/api/notifications/pending-approvals/stream/route.ts +101 -0
- package/src/app/api/notifications/route.ts +34 -0
- package/src/app/api/permissions/route.ts +46 -0
- package/src/app/api/profiles/[id]/route.ts +79 -0
- package/src/app/api/profiles/[id]/test/route.ts +42 -0
- package/src/app/api/profiles/import/route.ts +108 -0
- package/src/app/api/profiles/route.ts +50 -0
- package/src/app/api/projects/[id]/route.ts +72 -0
- package/src/app/api/projects/route.ts +53 -0
- package/src/app/api/schedules/[id]/route.ts +185 -0
- package/src/app/api/schedules/route.ts +117 -0
- package/src/app/api/settings/budgets/route.ts +24 -0
- package/src/app/api/settings/openai/route.ts +24 -0
- package/src/app/api/settings/route.ts +21 -0
- package/src/app/api/settings/test/route.ts +26 -0
- package/src/app/api/tasks/[id]/cancel/route.ts +21 -0
- package/src/app/api/tasks/[id]/execute/route.ts +90 -0
- package/src/app/api/tasks/[id]/logs/route.ts +95 -0
- package/src/app/api/tasks/[id]/output/route.ts +47 -0
- package/src/app/api/tasks/[id]/respond/route.ts +64 -0
- package/src/app/api/tasks/[id]/resume/route.ts +76 -0
- package/src/app/api/tasks/[id]/route.ts +77 -0
- package/src/app/api/tasks/assist/route.ts +35 -0
- package/src/app/api/tasks/route.ts +82 -0
- package/src/app/api/uploads/[id]/route.ts +81 -0
- package/src/app/api/uploads/cleanup/route.ts +7 -0
- package/src/app/api/uploads/route.ts +66 -0
- package/src/app/api/workflows/[id]/execute/route.ts +82 -0
- package/src/app/api/workflows/[id]/route.ts +133 -0
- package/src/app/api/workflows/[id]/status/route.ts +54 -0
- package/src/app/api/workflows/[id]/steps/[stepId]/retry/route.ts +22 -0
- package/src/app/api/workflows/route.ts +61 -0
- package/src/app/apple-icon.tsx +31 -0
- package/src/app/costs/page.tsx +256 -0
- package/src/app/dashboard/page.tsx +44 -0
- package/src/app/documents/[id]/page.tsx +46 -0
- package/src/app/documents/page.tsx +45 -0
- package/src/app/error.tsx +26 -0
- package/src/app/global-error.tsx +23 -0
- package/src/app/globals.css +733 -0
- package/src/app/icon.tsx +30 -0
- package/src/app/inbox/loading.tsx +15 -0
- package/src/app/inbox/page.tsx +35 -0
- package/src/app/layout.tsx +78 -0
- package/src/app/manifest.ts +32 -0
- package/src/app/monitor/page.tsx +37 -0
- package/src/app/page.tsx +162 -0
- package/src/app/profiles/[id]/edit/page.tsx +39 -0
- package/src/app/profiles/[id]/page.tsx +33 -0
- package/src/app/profiles/new/page.tsx +22 -0
- package/src/app/profiles/page.tsx +19 -0
- package/src/app/projects/[id]/page.tsx +134 -0
- package/src/app/projects/loading.tsx +17 -0
- package/src/app/projects/page.tsx +32 -0
- package/src/app/schedules/[id]/page.tsx +47 -0
- package/src/app/schedules/page.tsx +18 -0
- package/src/app/settings/loading.tsx +24 -0
- package/src/app/settings/page.tsx +27 -0
- package/src/app/tasks/[id]/page.tsx +45 -0
- package/src/app/tasks/new/page.tsx +27 -0
- package/src/app/workflows/[id]/edit/page.tsx +66 -0
- package/src/app/workflows/[id]/page.tsx +37 -0
- package/src/app/workflows/blueprints/[id]/page.tsx +40 -0
- package/src/app/workflows/blueprints/new/page.tsx +20 -0
- package/src/app/workflows/blueprints/page.tsx +11 -0
- package/src/app/workflows/new/page.tsx +36 -0
- package/src/app/workflows/page.tsx +18 -0
- package/src/components/charts/donut-ring.tsx +64 -0
- package/src/components/charts/mini-bar.tsx +75 -0
- package/src/components/charts/sparkline.tsx +107 -0
- package/src/components/costs/cost-dashboard.tsx +877 -0
- package/src/components/costs/cost-filters.tsx +179 -0
- package/src/components/dashboard/activity-feed.tsx +95 -0
- package/src/components/dashboard/greeting.tsx +30 -0
- package/src/components/dashboard/priority-queue.tsx +79 -0
- package/src/components/dashboard/quick-actions.tsx +62 -0
- package/src/components/dashboard/recent-projects.tsx +79 -0
- package/src/components/dashboard/stats-cards.tsx +114 -0
- package/src/components/documents/document-browser.tsx +235 -0
- package/src/components/documents/document-detail-view.tsx +367 -0
- package/src/components/documents/document-grid.tsx +78 -0
- package/src/components/documents/document-preview.tsx +68 -0
- package/src/components/documents/document-table.tsx +119 -0
- package/src/components/documents/document-upload-dialog.tsx +153 -0
- package/src/components/documents/types.ts +6 -0
- package/src/components/documents/utils.ts +57 -0
- package/src/components/monitoring/connection-indicator.tsx +14 -0
- package/src/components/monitoring/log-entry.tsx +79 -0
- package/src/components/monitoring/log-filters.tsx +57 -0
- package/src/components/monitoring/log-stream.tsx +144 -0
- package/src/components/monitoring/monitor-overview-wrapper.tsx +64 -0
- package/src/components/monitoring/monitor-overview.tsx +119 -0
- package/src/components/notifications/failure-action.tsx +38 -0
- package/src/components/notifications/inbox-list.tsx +165 -0
- package/src/components/notifications/message-response.tsx +196 -0
- package/src/components/notifications/notification-item.tsx +250 -0
- package/src/components/notifications/pending-approval-host.tsx +478 -0
- package/src/components/notifications/permission-action.tsx +37 -0
- package/src/components/notifications/permission-response-actions.tsx +126 -0
- package/src/components/notifications/unread-badge.tsx +35 -0
- package/src/components/profiles/profile-browser.tsx +117 -0
- package/src/components/profiles/profile-card.tsx +78 -0
- package/src/components/profiles/profile-detail-view.tsx +564 -0
- package/src/components/profiles/profile-form-view.tsx +480 -0
- package/src/components/profiles/profile-import-dialog.tsx +113 -0
- package/src/components/projects/project-card.tsx +58 -0
- package/src/components/projects/project-create-dialog.tsx +140 -0
- package/src/components/projects/project-detail.tsx +68 -0
- package/src/components/projects/project-edit-dialog.tsx +219 -0
- package/src/components/projects/project-list.tsx +108 -0
- package/src/components/schedules/schedule-create-dialog.tsx +403 -0
- package/src/components/schedules/schedule-detail-view.tsx +274 -0
- package/src/components/schedules/schedule-list.tsx +242 -0
- package/src/components/schedules/schedule-status-badge.tsx +16 -0
- package/src/components/settings/api-key-form.tsx +141 -0
- package/src/components/settings/auth-config-section.tsx +141 -0
- package/src/components/settings/auth-method-selector.tsx +67 -0
- package/src/components/settings/auth-status-badge.tsx +40 -0
- package/src/components/settings/auth-status-dot.tsx +59 -0
- package/src/components/settings/budget-guardrails-section.tsx +842 -0
- package/src/components/settings/data-management-section.tsx +141 -0
- package/src/components/settings/openai-runtime-section.tsx +104 -0
- package/src/components/settings/permissions-section.tsx +91 -0
- package/src/components/shared/app-sidebar.tsx +123 -0
- package/src/components/shared/card-skeleton.tsx +42 -0
- package/src/components/shared/command-palette.tsx +250 -0
- package/src/components/shared/confirm-dialog.tsx +52 -0
- package/src/components/shared/empty-state.tsx +24 -0
- package/src/components/shared/error-state.tsx +32 -0
- package/src/components/shared/form-section-card.tsx +33 -0
- package/src/components/shared/section-heading.tsx +14 -0
- package/src/components/shared/stagent-logo.tsx +21 -0
- package/src/components/shared/theme-toggle.tsx +46 -0
- package/src/components/tasks/ai-assist-panel.tsx +210 -0
- package/src/components/tasks/content-preview.tsx +89 -0
- package/src/components/tasks/empty-board.tsx +12 -0
- package/src/components/tasks/file-upload.tsx +120 -0
- package/src/components/tasks/kanban-board.tsx +275 -0
- package/src/components/tasks/kanban-column.tsx +75 -0
- package/src/components/tasks/skeleton-board.tsx +21 -0
- package/src/components/tasks/task-attachments.tsx +114 -0
- package/src/components/tasks/task-card.tsx +101 -0
- package/src/components/tasks/task-create-panel.tsx +360 -0
- package/src/components/tasks/task-detail-view.tsx +356 -0
- package/src/components/ui/alert-dialog.tsx +196 -0
- package/src/components/ui/badge.tsx +50 -0
- package/src/components/ui/button.tsx +71 -0
- package/src/components/ui/card.tsx +92 -0
- package/src/components/ui/checkbox.tsx +32 -0
- package/src/components/ui/command.tsx +184 -0
- package/src/components/ui/dialog.tsx +158 -0
- package/src/components/ui/dropdown-menu.tsx +257 -0
- package/src/components/ui/form.tsx +167 -0
- package/src/components/ui/input.tsx +21 -0
- package/src/components/ui/label.tsx +24 -0
- package/src/components/ui/popover.tsx +89 -0
- package/src/components/ui/progress.tsx +31 -0
- package/src/components/ui/radio-group.tsx +45 -0
- package/src/components/ui/scroll-area.tsx +58 -0
- package/src/components/ui/select.tsx +190 -0
- package/src/components/ui/separator.tsx +28 -0
- package/src/components/ui/sheet.tsx +143 -0
- package/src/components/ui/sidebar.tsx +726 -0
- package/src/components/ui/skeleton.tsx +13 -0
- package/src/components/ui/slider.tsx +63 -0
- package/src/components/ui/sonner.tsx +36 -0
- package/src/components/ui/switch.tsx +35 -0
- package/src/components/ui/table.tsx +116 -0
- package/src/components/ui/tabs.tsx +91 -0
- package/src/components/ui/textarea.tsx +18 -0
- package/src/components/ui/tooltip.tsx +57 -0
- package/src/components/workflows/blueprint-editor.tsx +109 -0
- package/src/components/workflows/blueprint-gallery.tsx +155 -0
- package/src/components/workflows/blueprint-preview.tsx +240 -0
- package/src/components/workflows/loop-status-view.tsx +272 -0
- package/src/components/workflows/swarm-dashboard.tsx +185 -0
- package/src/components/workflows/workflow-form-view.tsx +1376 -0
- package/src/components/workflows/workflow-list.tsx +230 -0
- package/src/components/workflows/workflow-status-view.tsx +477 -0
- package/src/hooks/use-mobile.ts +19 -0
- package/src/instrumentation.ts +7 -0
- package/src/lib/agents/claude-agent.ts +737 -0
- package/src/lib/agents/execution-manager.ts +27 -0
- package/src/lib/agents/profiles/assignment-validation.ts +75 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/SKILL.md +21 -0
- package/src/lib/agents/profiles/builtins/code-reviewer/profile.yaml +28 -0
- package/src/lib/agents/profiles/builtins/data-analyst/SKILL.md +25 -0
- package/src/lib/agents/profiles/builtins/data-analyst/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/devops-engineer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/document-writer/SKILL.md +16 -0
- package/src/lib/agents/profiles/builtins/document-writer/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/general/SKILL.md +13 -0
- package/src/lib/agents/profiles/builtins/general/profile.yaml +18 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/health-fitness-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/learning-coach/SKILL.md +35 -0
- package/src/lib/agents/profiles/builtins/learning-coach/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/SKILL.md +26 -0
- package/src/lib/agents/profiles/builtins/project-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/researcher/SKILL.md +15 -0
- package/src/lib/agents/profiles/builtins/researcher/profile.yaml +27 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/SKILL.md +34 -0
- package/src/lib/agents/profiles/builtins/shopping-assistant/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/technical-writer/SKILL.md +31 -0
- package/src/lib/agents/profiles/builtins/technical-writer/profile.yaml +29 -0
- package/src/lib/agents/profiles/builtins/travel-planner/SKILL.md +23 -0
- package/src/lib/agents/profiles/builtins/travel-planner/profile.yaml +26 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/SKILL.md +24 -0
- package/src/lib/agents/profiles/builtins/wealth-manager/profile.yaml +26 -0
- package/src/lib/agents/profiles/compatibility.ts +109 -0
- package/src/lib/agents/profiles/registry.ts +293 -0
- package/src/lib/agents/profiles/test-runner.ts +18 -0
- package/src/lib/agents/profiles/test-types.ts +20 -0
- package/src/lib/agents/profiles/types.ts +43 -0
- package/src/lib/agents/router.ts +56 -0
- package/src/lib/agents/runtime/catalog.ts +85 -0
- package/src/lib/agents/runtime/claude-sdk.ts +12 -0
- package/src/lib/agents/runtime/claude.ts +370 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +289 -0
- package/src/lib/agents/runtime/index.ts +167 -0
- package/src/lib/agents/runtime/openai-codex.ts +1089 -0
- package/src/lib/agents/runtime/task-assist-types.ts +8 -0
- package/src/lib/agents/runtime/types.ts +30 -0
- package/src/lib/constants/settings.ts +13 -0
- package/src/lib/constants/status-colors.ts +44 -0
- package/src/lib/constants/task-status.ts +49 -0
- package/src/lib/data/clear.ts +63 -0
- package/src/lib/data/seed-data/documents.ts +715 -0
- package/src/lib/data/seed-data/logs.ts +195 -0
- package/src/lib/data/seed-data/notifications.ts +141 -0
- package/src/lib/data/seed-data/profiles.ts +175 -0
- package/src/lib/data/seed-data/projects.ts +61 -0
- package/src/lib/data/seed-data/schedules.ts +108 -0
- package/src/lib/data/seed-data/tasks.ts +341 -0
- package/src/lib/data/seed-data/usage-ledger.ts +130 -0
- package/src/lib/data/seed-data/workflows.ts +213 -0
- package/src/lib/data/seed.ts +129 -0
- package/src/lib/db/index.ts +221 -0
- package/src/lib/db/migrations/0000_aromatic_gargoyle.sql +59 -0
- package/src/lib/db/migrations/0001_first_iron_patriot.sql +6 -0
- package/src/lib/db/migrations/0002_add_resume_count.sql +1 -0
- package/src/lib/db/migrations/0003_add_settings.sql +5 -0
- package/src/lib/db/migrations/0004_add_documents.sql +20 -0
- package/src/lib/db/migrations/0005_add_document_preprocessing.sql +4 -0
- package/src/lib/db/migrations/0006_add_agent_profile.sql +2 -0
- package/src/lib/db/migrations/0007_add_usage_metering_ledger.sql +30 -0
- package/src/lib/db/migrations/0008_add_document_version.sql +1 -0
- package/src/lib/db/migrations/meta/0000_snapshot.json +416 -0
- package/src/lib/db/migrations/meta/0001_snapshot.json +461 -0
- package/src/lib/db/migrations/meta/0002_snapshot.json +469 -0
- package/src/lib/db/migrations/meta/_journal.json +27 -0
- package/src/lib/db/schema.ts +227 -0
- package/src/lib/documents/cleanup.ts +50 -0
- package/src/lib/documents/context-builder.ts +75 -0
- package/src/lib/documents/output-scanner.ts +166 -0
- package/src/lib/documents/processor.ts +120 -0
- package/src/lib/documents/processors/image.ts +21 -0
- package/src/lib/documents/processors/office.ts +36 -0
- package/src/lib/documents/processors/pdf.ts +12 -0
- package/src/lib/documents/processors/spreadsheet.ts +18 -0
- package/src/lib/documents/processors/text.ts +8 -0
- package/src/lib/documents/registry.ts +25 -0
- package/src/lib/notifications/actionable.ts +108 -0
- package/src/lib/notifications/permissions.ts +169 -0
- package/src/lib/queries/chart-data.ts +184 -0
- package/src/lib/schedules/interval-parser.ts +110 -0
- package/src/lib/schedules/scheduler.ts +220 -0
- package/src/lib/settings/auth.ts +98 -0
- package/src/lib/settings/budget-guardrails.ts +590 -0
- package/src/lib/settings/helpers.ts +23 -0
- package/src/lib/settings/openai-auth.ts +80 -0
- package/src/lib/settings/permissions.ts +102 -0
- package/src/lib/usage/ledger.ts +489 -0
- package/src/lib/usage/pricing.ts +68 -0
- package/src/lib/utils/crypto.ts +90 -0
- package/src/lib/utils/format-timestamp.ts +46 -0
- package/src/lib/utils/session-cleanup.ts +26 -0
- package/src/lib/utils/stagent-paths.ts +18 -0
- package/src/lib/utils.ts +6 -0
- package/src/lib/validators/blueprint.ts +43 -0
- package/src/lib/validators/profile.ts +64 -0
- package/src/lib/validators/project.ts +17 -0
- package/src/lib/validators/settings.ts +57 -0
- package/src/lib/validators/task.ts +30 -0
- package/src/lib/workflows/blueprints/builtins/code-review-pipeline.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/documentation-generation.yaml +62 -0
- package/src/lib/workflows/blueprints/builtins/investment-research.yaml +81 -0
- package/src/lib/workflows/blueprints/builtins/meal-planning.yaml +73 -0
- package/src/lib/workflows/blueprints/builtins/product-research.yaml +72 -0
- package/src/lib/workflows/blueprints/builtins/research-report.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/sprint-planning.yaml +77 -0
- package/src/lib/workflows/blueprints/builtins/travel-planning.yaml +80 -0
- package/src/lib/workflows/blueprints/instantiator.ts +131 -0
- package/src/lib/workflows/blueprints/registry.ts +128 -0
- package/src/lib/workflows/blueprints/template.ts +58 -0
- package/src/lib/workflows/blueprints/types.ts +38 -0
- package/src/lib/workflows/definition-validation.ts +121 -0
- package/src/lib/workflows/engine.ts +1113 -0
- package/src/lib/workflows/loop-executor.ts +270 -0
- package/src/lib/workflows/parallel.ts +55 -0
- package/src/lib/workflows/swarm.ts +97 -0
- package/src/lib/workflows/types.ts +112 -0
- 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
|
+
}
|