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,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
|
+
}
|