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,220 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Poll-based scheduler engine.
|
|
3
|
+
*
|
|
4
|
+
* Runs on a configurable interval (default 60s), checking for schedules whose
|
|
5
|
+
* `nextFireAt` has passed. For each due schedule it creates a child task and
|
|
6
|
+
* fires it via the provider runtime pipeline.
|
|
7
|
+
*
|
|
8
|
+
* Lifecycle:
|
|
9
|
+
* - `startScheduler()` — call once at server boot (idempotent)
|
|
10
|
+
* - `stopScheduler()` — call on graceful shutdown
|
|
11
|
+
* - `tickScheduler()` — exposed for testing; runs one poll cycle
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { db } from "@/lib/db";
|
|
15
|
+
import { schedules, tasks } from "@/lib/db/schema";
|
|
16
|
+
import { eq, and, lte, like, inArray, sql } from "drizzle-orm";
|
|
17
|
+
import { computeNextFireTime } from "./interval-parser";
|
|
18
|
+
import { executeTaskWithRuntime } from "@/lib/agents/runtime";
|
|
19
|
+
|
|
20
|
+
const POLL_INTERVAL_MS = 60_000; // 60 seconds
|
|
21
|
+
|
|
22
|
+
let intervalHandle: ReturnType<typeof setInterval> | null = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start the scheduler singleton. Safe to call multiple times — subsequent
|
|
26
|
+
* calls are no-ops if already running.
|
|
27
|
+
*/
|
|
28
|
+
export function startScheduler(): void {
|
|
29
|
+
if (intervalHandle !== null) return;
|
|
30
|
+
|
|
31
|
+
// Bootstrap: recompute nextFireAt for any active schedules that are missing it
|
|
32
|
+
bootstrapNextFireTimes();
|
|
33
|
+
|
|
34
|
+
intervalHandle = setInterval(() => {
|
|
35
|
+
tickScheduler().catch((err) => {
|
|
36
|
+
console.error("[scheduler] tick error:", err);
|
|
37
|
+
});
|
|
38
|
+
}, POLL_INTERVAL_MS);
|
|
39
|
+
|
|
40
|
+
console.log("[scheduler] started — polling every 60s");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Stop the scheduler.
|
|
45
|
+
*/
|
|
46
|
+
export function stopScheduler(): void {
|
|
47
|
+
if (intervalHandle !== null) {
|
|
48
|
+
clearInterval(intervalHandle);
|
|
49
|
+
intervalHandle = null;
|
|
50
|
+
console.log("[scheduler] stopped");
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Run one poll cycle: find due schedules and fire them.
|
|
56
|
+
*/
|
|
57
|
+
export async function tickScheduler(): Promise<void> {
|
|
58
|
+
const now = new Date();
|
|
59
|
+
|
|
60
|
+
const dueSchedules = await db
|
|
61
|
+
.select()
|
|
62
|
+
.from(schedules)
|
|
63
|
+
.where(
|
|
64
|
+
and(
|
|
65
|
+
eq(schedules.status, "active"),
|
|
66
|
+
lte(schedules.nextFireAt, now)
|
|
67
|
+
)
|
|
68
|
+
);
|
|
69
|
+
|
|
70
|
+
for (const schedule of dueSchedules) {
|
|
71
|
+
try {
|
|
72
|
+
// Atomic claim: attempt to update nextFireAt to null as a lock.
|
|
73
|
+
// Only the first tick to succeed (.changes > 0) proceeds with firing.
|
|
74
|
+
const claimResult = db
|
|
75
|
+
.update(schedules)
|
|
76
|
+
.set({ nextFireAt: null, updatedAt: now })
|
|
77
|
+
.where(
|
|
78
|
+
and(
|
|
79
|
+
eq(schedules.id, schedule.id),
|
|
80
|
+
eq(schedules.status, "active"),
|
|
81
|
+
lte(schedules.nextFireAt, now)
|
|
82
|
+
)
|
|
83
|
+
)
|
|
84
|
+
.run();
|
|
85
|
+
|
|
86
|
+
if (claimResult.changes === 0) {
|
|
87
|
+
// Another tick already claimed this schedule
|
|
88
|
+
continue;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
await fireSchedule(schedule, now);
|
|
92
|
+
} catch (err) {
|
|
93
|
+
console.error(`[scheduler] failed to fire schedule ${schedule.id}:`, err);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
async function fireSchedule(
|
|
99
|
+
schedule: typeof schedules.$inferSelect,
|
|
100
|
+
now: Date
|
|
101
|
+
): Promise<void> {
|
|
102
|
+
// Concurrency guard: skip if a child task from this schedule is still running.
|
|
103
|
+
// Escape SQL LIKE metacharacters (%, _) in schedule name to prevent false matches.
|
|
104
|
+
const escapedName = schedule.name
|
|
105
|
+
.replace(/\\/g, "\\\\")
|
|
106
|
+
.replace(/%/g, "\\%")
|
|
107
|
+
.replace(/_/g, "\\_");
|
|
108
|
+
const runningChildren = await db
|
|
109
|
+
.select({ id: tasks.id })
|
|
110
|
+
.from(tasks)
|
|
111
|
+
.where(
|
|
112
|
+
and(
|
|
113
|
+
sql`${tasks.title} LIKE ${`${escapedName} — firing #%`} ESCAPE '\\'`,
|
|
114
|
+
inArray(tasks.status, ["queued", "running"])
|
|
115
|
+
)
|
|
116
|
+
);
|
|
117
|
+
if (runningChildren.length > 0) {
|
|
118
|
+
console.log(`[scheduler] skipping ${schedule.id} — previous firing still running`);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Check expiry
|
|
123
|
+
if (schedule.expiresAt && schedule.expiresAt <= now) {
|
|
124
|
+
await db
|
|
125
|
+
.update(schedules)
|
|
126
|
+
.set({ status: "expired", updatedAt: now })
|
|
127
|
+
.where(eq(schedules.id, schedule.id));
|
|
128
|
+
return;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
// Check max firings
|
|
132
|
+
if (schedule.maxFirings && schedule.firingCount >= schedule.maxFirings) {
|
|
133
|
+
await db
|
|
134
|
+
.update(schedules)
|
|
135
|
+
.set({ status: "expired", updatedAt: now })
|
|
136
|
+
.where(eq(schedules.id, schedule.id));
|
|
137
|
+
return;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Create child task
|
|
141
|
+
const taskId = crypto.randomUUID();
|
|
142
|
+
const firingNumber = schedule.firingCount + 1;
|
|
143
|
+
|
|
144
|
+
await db.insert(tasks).values({
|
|
145
|
+
id: taskId,
|
|
146
|
+
projectId: schedule.projectId,
|
|
147
|
+
workflowId: null,
|
|
148
|
+
scheduleId: schedule.id,
|
|
149
|
+
title: `${schedule.name} — firing #${firingNumber}`,
|
|
150
|
+
description: schedule.prompt,
|
|
151
|
+
status: "queued",
|
|
152
|
+
assignedAgent: schedule.assignedAgent,
|
|
153
|
+
agentProfile: schedule.agentProfile,
|
|
154
|
+
priority: 2,
|
|
155
|
+
createdAt: now,
|
|
156
|
+
updatedAt: now,
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
// Update schedule counters
|
|
160
|
+
const isOneShot = !schedule.recurs;
|
|
161
|
+
const reachedMax =
|
|
162
|
+
schedule.maxFirings !== null && firingNumber >= schedule.maxFirings;
|
|
163
|
+
|
|
164
|
+
const nextStatus = isOneShot
|
|
165
|
+
? "completed"
|
|
166
|
+
: reachedMax
|
|
167
|
+
? "expired"
|
|
168
|
+
: "active";
|
|
169
|
+
|
|
170
|
+
const nextFireAt =
|
|
171
|
+
nextStatus === "active"
|
|
172
|
+
? computeNextFireTime(schedule.cronExpression, now)
|
|
173
|
+
: null;
|
|
174
|
+
|
|
175
|
+
await db
|
|
176
|
+
.update(schedules)
|
|
177
|
+
.set({
|
|
178
|
+
firingCount: firingNumber,
|
|
179
|
+
lastFiredAt: now,
|
|
180
|
+
nextFireAt,
|
|
181
|
+
status: nextStatus,
|
|
182
|
+
updatedAt: now,
|
|
183
|
+
})
|
|
184
|
+
.where(eq(schedules.id, schedule.id));
|
|
185
|
+
|
|
186
|
+
// Fire-and-forget task execution
|
|
187
|
+
executeTaskWithRuntime(taskId).catch((err) => {
|
|
188
|
+
console.error(
|
|
189
|
+
`[scheduler] task execution failed for schedule ${schedule.id}, task ${taskId}:`,
|
|
190
|
+
err
|
|
191
|
+
);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
console.log(
|
|
195
|
+
`[scheduler] fired schedule "${schedule.name}" → task ${taskId} (firing #${firingNumber})`
|
|
196
|
+
);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Recompute nextFireAt for active schedules that have it set to null.
|
|
201
|
+
* Called once at startup to recover from unclean shutdowns.
|
|
202
|
+
*/
|
|
203
|
+
function bootstrapNextFireTimes(): void {
|
|
204
|
+
const activeSchedules = db
|
|
205
|
+
.select()
|
|
206
|
+
.from(schedules)
|
|
207
|
+
.where(eq(schedules.status, "active"))
|
|
208
|
+
.all();
|
|
209
|
+
|
|
210
|
+
const now = new Date();
|
|
211
|
+
for (const schedule of activeSchedules) {
|
|
212
|
+
if (!schedule.nextFireAt) {
|
|
213
|
+
const nextFire = computeNextFireTime(schedule.cronExpression, now);
|
|
214
|
+
db.update(schedules)
|
|
215
|
+
.set({ nextFireAt: nextFire, updatedAt: now })
|
|
216
|
+
.where(eq(schedules.id, schedule.id))
|
|
217
|
+
.run();
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import { db } from "@/lib/db";
|
|
2
|
+
import { settings } from "@/lib/db/schema";
|
|
3
|
+
import { eq } from "drizzle-orm";
|
|
4
|
+
import { encrypt, decrypt } from "@/lib/utils/crypto";
|
|
5
|
+
import { SETTINGS_KEYS, type AuthMethod, type ApiKeySource } from "@/lib/constants/settings";
|
|
6
|
+
import type { UpdateAuthSettingsInput } from "@/lib/validators/settings";
|
|
7
|
+
import { getSetting, setSetting } from "./helpers";
|
|
8
|
+
|
|
9
|
+
export interface AuthSettings {
|
|
10
|
+
method: AuthMethod;
|
|
11
|
+
hasKey: boolean;
|
|
12
|
+
apiKeySource: ApiKeySource;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Get current auth settings. Never returns the raw API key.
|
|
17
|
+
*/
|
|
18
|
+
export async function getAuthSettings(): Promise<AuthSettings> {
|
|
19
|
+
const method = ((await getSetting(SETTINGS_KEYS.AUTH_METHOD)) as AuthMethod) ?? "oauth";
|
|
20
|
+
const encryptedKey = await getSetting(SETTINGS_KEYS.AUTH_API_KEY);
|
|
21
|
+
const storedSource = (await getSetting(SETTINGS_KEYS.AUTH_API_KEY_SOURCE)) as ApiKeySource | null;
|
|
22
|
+
|
|
23
|
+
const hasDbKey = encryptedKey !== null;
|
|
24
|
+
const hasEnvKey = !!process.env.ANTHROPIC_API_KEY;
|
|
25
|
+
|
|
26
|
+
let apiKeySource: ApiKeySource;
|
|
27
|
+
if (storedSource) {
|
|
28
|
+
apiKeySource = storedSource;
|
|
29
|
+
} else if (hasDbKey) {
|
|
30
|
+
apiKeySource = "db";
|
|
31
|
+
} else if (hasEnvKey) {
|
|
32
|
+
apiKeySource = "env";
|
|
33
|
+
} else if (method === "oauth") {
|
|
34
|
+
apiKeySource = "oauth";
|
|
35
|
+
} else {
|
|
36
|
+
apiKeySource = "unknown";
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return {
|
|
40
|
+
method,
|
|
41
|
+
hasKey: hasDbKey || hasEnvKey,
|
|
42
|
+
apiKeySource,
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Save auth settings. Encrypts API key before storing.
|
|
48
|
+
*/
|
|
49
|
+
export async function setAuthSettings(input: UpdateAuthSettingsInput): Promise<void> {
|
|
50
|
+
await setSetting(SETTINGS_KEYS.AUTH_METHOD, input.method);
|
|
51
|
+
|
|
52
|
+
if (input.apiKey) {
|
|
53
|
+
await setSetting(SETTINGS_KEYS.AUTH_API_KEY, encrypt(input.apiKey));
|
|
54
|
+
await setSetting(SETTINGS_KEYS.AUTH_API_KEY_SOURCE, "db");
|
|
55
|
+
} else if (input.method === "oauth") {
|
|
56
|
+
// Clear stored key when switching to OAuth
|
|
57
|
+
const existingKey = await getSetting(SETTINGS_KEYS.AUTH_API_KEY);
|
|
58
|
+
if (existingKey !== null) {
|
|
59
|
+
await db.delete(settings)
|
|
60
|
+
.where(eq(settings.key, SETTINGS_KEYS.AUTH_API_KEY));
|
|
61
|
+
}
|
|
62
|
+
await setSetting(SETTINGS_KEYS.AUTH_API_KEY_SOURCE, "oauth");
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Get the environment variables to pass to the Agent SDK.
|
|
68
|
+
* Priority: DB-stored key > process.env > undefined (SDK falls back to OAuth).
|
|
69
|
+
*/
|
|
70
|
+
export async function getAuthEnv(): Promise<Record<string, string> | undefined> {
|
|
71
|
+
const method = ((await getSetting(SETTINGS_KEYS.AUTH_METHOD)) as AuthMethod) ?? "oauth";
|
|
72
|
+
|
|
73
|
+
// If OAuth is selected, don't inject any key — let SDK handle it
|
|
74
|
+
if (method === "oauth") {
|
|
75
|
+
return undefined;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// Try DB-stored key first
|
|
79
|
+
const encryptedKey = await getSetting(SETTINGS_KEYS.AUTH_API_KEY);
|
|
80
|
+
if (encryptedKey) {
|
|
81
|
+
try {
|
|
82
|
+
const key = decrypt(encryptedKey);
|
|
83
|
+
return { ANTHROPIC_API_KEY: key };
|
|
84
|
+
} catch {
|
|
85
|
+
// If decryption fails, fall through to env
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Fall back to env var (already in process.env, no need to inject)
|
|
90
|
+
return undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Update the last-known API key source after SDK initialization.
|
|
95
|
+
*/
|
|
96
|
+
export async function updateAuthStatus(source: ApiKeySource): Promise<void> {
|
|
97
|
+
await setSetting(SETTINGS_KEYS.AUTH_API_KEY_SOURCE, source);
|
|
98
|
+
}
|