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,12 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import type { ProcessorResult } from "../registry";
|
|
3
|
+
|
|
4
|
+
/** Extract text from PDF files using pdf-parse v2 */
|
|
5
|
+
export async function processPdf(filePath: string): Promise<ProcessorResult> {
|
|
6
|
+
const { PDFParse } = await import("pdf-parse");
|
|
7
|
+
const buffer = await readFile(filePath);
|
|
8
|
+
const parser = new PDFParse({ data: new Uint8Array(buffer) });
|
|
9
|
+
const result = await parser.getText();
|
|
10
|
+
await parser.destroy();
|
|
11
|
+
return { extractedText: result.text };
|
|
12
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import type { ProcessorResult } from "../registry";
|
|
3
|
+
|
|
4
|
+
/** Parse XLSX/CSV to a text table representation */
|
|
5
|
+
export async function processSpreadsheet(filePath: string): Promise<ProcessorResult> {
|
|
6
|
+
const XLSX = await import("xlsx");
|
|
7
|
+
const buffer = await readFile(filePath);
|
|
8
|
+
const workbook = XLSX.read(buffer, { type: "buffer" });
|
|
9
|
+
|
|
10
|
+
const sheets: string[] = [];
|
|
11
|
+
for (const sheetName of workbook.SheetNames) {
|
|
12
|
+
const sheet = workbook.Sheets[sheetName];
|
|
13
|
+
const csv = XLSX.utils.sheet_to_csv(sheet);
|
|
14
|
+
sheets.push(`--- Sheet: ${sheetName} ---\n${csv}`);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
return { extractedText: sheets.join("\n\n") };
|
|
18
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
import { readFile } from "fs/promises";
|
|
2
|
+
import type { ProcessorResult } from "../registry";
|
|
3
|
+
|
|
4
|
+
/** Read plain text, markdown, code, and config files directly */
|
|
5
|
+
export async function processText(filePath: string): Promise<ProcessorResult> {
|
|
6
|
+
const content = await readFile(filePath, "utf-8");
|
|
7
|
+
return { extractedText: content };
|
|
8
|
+
}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Extensible document processor registry.
|
|
3
|
+
* Maps MIME types to processor functions for text extraction.
|
|
4
|
+
*/
|
|
5
|
+
|
|
6
|
+
export interface ProcessorResult {
|
|
7
|
+
extractedText: string;
|
|
8
|
+
processedPath?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export type Processor = (filePath: string) => Promise<ProcessorResult>;
|
|
12
|
+
|
|
13
|
+
const registry = new Map<string, Processor>();
|
|
14
|
+
|
|
15
|
+
export function registerProcessor(mimeType: string, processor: Processor): void {
|
|
16
|
+
registry.set(mimeType, processor);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function getProcessor(mimeType: string): Processor | undefined {
|
|
20
|
+
return registry.get(mimeType);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function hasProcessor(mimeType: string): boolean {
|
|
24
|
+
return registry.has(mimeType);
|
|
25
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { and, desc, eq, isNull } from "drizzle-orm";
|
|
2
|
+
|
|
3
|
+
import { db } from "@/lib/db";
|
|
4
|
+
import { notifications, tasks, workflows } from "@/lib/db/schema";
|
|
5
|
+
import {
|
|
6
|
+
buildPermissionSummary,
|
|
7
|
+
getPermissionKindLabel,
|
|
8
|
+
parseNotificationToolInput,
|
|
9
|
+
type PermissionToolInput,
|
|
10
|
+
} from "@/lib/notifications/permissions";
|
|
11
|
+
|
|
12
|
+
export const APPROVAL_ACTION_IDS = [
|
|
13
|
+
"allow_once",
|
|
14
|
+
"always_allow",
|
|
15
|
+
"deny",
|
|
16
|
+
"open_inbox",
|
|
17
|
+
] as const;
|
|
18
|
+
|
|
19
|
+
export type ApprovalActionId = (typeof APPROVAL_ACTION_IDS)[number];
|
|
20
|
+
export type NotificationChannelId = "in_app" | "browser" | "tauri";
|
|
21
|
+
|
|
22
|
+
export interface ActionableNotificationPayload {
|
|
23
|
+
notificationId: string;
|
|
24
|
+
taskId: string | null;
|
|
25
|
+
workflowId: string | null;
|
|
26
|
+
toolName: string | null;
|
|
27
|
+
permissionLabel: string;
|
|
28
|
+
compactSummary: string;
|
|
29
|
+
deepLink: string;
|
|
30
|
+
supportedActionIds: ApprovalActionId[];
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface PendingApprovalPayload extends ActionableNotificationPayload {
|
|
34
|
+
channel: "in_app";
|
|
35
|
+
title: string;
|
|
36
|
+
body: string | null;
|
|
37
|
+
taskTitle: string | null;
|
|
38
|
+
workflowName: string | null;
|
|
39
|
+
toolInput: PermissionToolInput | null;
|
|
40
|
+
createdAt: string;
|
|
41
|
+
read: boolean;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
export interface ActionableNotificationChannelAdapter {
|
|
45
|
+
channelId: NotificationChannelId;
|
|
46
|
+
present(payload: ActionableNotificationPayload): void | Promise<void>;
|
|
47
|
+
dismiss?(notificationId: string): void | Promise<void>;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function buildDeepLink(taskId: string | null, workflowId: string | null): string {
|
|
51
|
+
if (taskId) return `/tasks/${taskId}`;
|
|
52
|
+
if (workflowId) return `/workflows/${workflowId}`;
|
|
53
|
+
return "/inbox";
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
export async function listPendingApprovalPayloads(
|
|
57
|
+
limit = 20
|
|
58
|
+
): Promise<PendingApprovalPayload[]> {
|
|
59
|
+
const rows = await db
|
|
60
|
+
.select({
|
|
61
|
+
notificationId: notifications.id,
|
|
62
|
+
taskId: notifications.taskId,
|
|
63
|
+
type: notifications.type,
|
|
64
|
+
title: notifications.title,
|
|
65
|
+
body: notifications.body,
|
|
66
|
+
read: notifications.read,
|
|
67
|
+
toolName: notifications.toolName,
|
|
68
|
+
toolInput: notifications.toolInput,
|
|
69
|
+
createdAt: notifications.createdAt,
|
|
70
|
+
taskTitle: tasks.title,
|
|
71
|
+
workflowId: tasks.workflowId,
|
|
72
|
+
workflowName: workflows.name,
|
|
73
|
+
})
|
|
74
|
+
.from(notifications)
|
|
75
|
+
.leftJoin(tasks, eq(tasks.id, notifications.taskId))
|
|
76
|
+
.leftJoin(workflows, eq(workflows.id, tasks.workflowId))
|
|
77
|
+
.where(
|
|
78
|
+
and(
|
|
79
|
+
eq(notifications.type, "permission_required"),
|
|
80
|
+
isNull(notifications.response)
|
|
81
|
+
)
|
|
82
|
+
)
|
|
83
|
+
.orderBy(desc(notifications.createdAt))
|
|
84
|
+
.limit(limit);
|
|
85
|
+
|
|
86
|
+
return rows.map((row) => {
|
|
87
|
+
const parsedInput = parseNotificationToolInput(row.toolInput);
|
|
88
|
+
|
|
89
|
+
return {
|
|
90
|
+
channel: "in_app",
|
|
91
|
+
notificationId: row.notificationId,
|
|
92
|
+
taskId: row.taskId,
|
|
93
|
+
workflowId: row.workflowId,
|
|
94
|
+
toolName: row.toolName,
|
|
95
|
+
permissionLabel: getPermissionKindLabel(row.toolName),
|
|
96
|
+
compactSummary: buildPermissionSummary(row.toolName, parsedInput),
|
|
97
|
+
deepLink: buildDeepLink(row.taskId, row.workflowId),
|
|
98
|
+
supportedActionIds: [...APPROVAL_ACTION_IDS],
|
|
99
|
+
title: row.title,
|
|
100
|
+
body: row.body,
|
|
101
|
+
taskTitle: row.taskTitle,
|
|
102
|
+
workflowName: row.workflowName,
|
|
103
|
+
toolInput: parsedInput as PermissionToolInput | null,
|
|
104
|
+
createdAt: row.createdAt.toISOString(),
|
|
105
|
+
read: row.read,
|
|
106
|
+
};
|
|
107
|
+
});
|
|
108
|
+
}
|
|
@@ -0,0 +1,169 @@
|
|
|
1
|
+
export type PermissionToolInput = Record<string, unknown>;
|
|
2
|
+
|
|
3
|
+
function truncate(value: string, max = 120): string {
|
|
4
|
+
if (value.length <= max) return value;
|
|
5
|
+
return `${value.slice(0, max - 1)}…`;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function toDisplayString(value: unknown): string {
|
|
9
|
+
if (typeof value === "string") return value;
|
|
10
|
+
if (typeof value === "number" || typeof value === "boolean") {
|
|
11
|
+
return String(value);
|
|
12
|
+
}
|
|
13
|
+
return JSON.stringify(value);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function parseNotificationToolInput(
|
|
17
|
+
toolInput: string | null
|
|
18
|
+
): PermissionToolInput | null {
|
|
19
|
+
if (!toolInput) return null;
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
const parsed = JSON.parse(toolInput);
|
|
23
|
+
return parsed && typeof parsed === "object"
|
|
24
|
+
? (parsed as PermissionToolInput)
|
|
25
|
+
: null;
|
|
26
|
+
} catch {
|
|
27
|
+
return null;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
export function getPermissionKindLabel(toolName: string | null): string {
|
|
32
|
+
if (!toolName) return "Tool";
|
|
33
|
+
|
|
34
|
+
if (toolName.startsWith("mcp__")) {
|
|
35
|
+
const [, server = "server", tool = "tool"] = toolName.split("__");
|
|
36
|
+
return `MCP ${server}/${tool}`;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return toolName;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function buildPermissionPattern(
|
|
43
|
+
toolName: string,
|
|
44
|
+
input: PermissionToolInput
|
|
45
|
+
): string {
|
|
46
|
+
if (toolName === "Bash" && typeof input.command === "string") {
|
|
47
|
+
const firstWord = input.command.trim().split(/\s+/)[0];
|
|
48
|
+
return `Bash(command:${firstWord} *)`;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
if (toolName === "bash" && typeof input.cmd === "string") {
|
|
52
|
+
const firstWord = input.cmd.trim().split(/\s+/)[0];
|
|
53
|
+
return `bash(command:${firstWord} *)`;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return toolName;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function buildPermissionSummary(
|
|
60
|
+
toolName: string | null,
|
|
61
|
+
input: PermissionToolInput | null
|
|
62
|
+
): string {
|
|
63
|
+
if (!input) return "Review the requested tool invocation.";
|
|
64
|
+
|
|
65
|
+
if (toolName === "Bash" || toolName === "bash") {
|
|
66
|
+
const command = input.command ?? input.cmd;
|
|
67
|
+
if (typeof command === "string" && command.trim().length > 0) {
|
|
68
|
+
return truncate(command.trim());
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (
|
|
73
|
+
toolName === "Read" ||
|
|
74
|
+
toolName === "Write" ||
|
|
75
|
+
toolName === "Edit" ||
|
|
76
|
+
toolName === "read" ||
|
|
77
|
+
toolName === "write" ||
|
|
78
|
+
toolName === "edit"
|
|
79
|
+
) {
|
|
80
|
+
const path = input.file_path ?? input.path;
|
|
81
|
+
if (typeof path === "string" && path.trim().length > 0) {
|
|
82
|
+
return truncate(path.trim());
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (toolName?.startsWith("mcp__")) {
|
|
87
|
+
const entries = Object.entries(input);
|
|
88
|
+
const firstString = entries.find(([, value]) => typeof value === "string");
|
|
89
|
+
if (firstString && typeof firstString[1] === "string") {
|
|
90
|
+
return truncate(firstString[1]);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (entries.length > 0) {
|
|
94
|
+
const [key, value] = entries[0];
|
|
95
|
+
return truncate(`${key}: ${toDisplayString(value)}`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const entries = Object.entries(input);
|
|
100
|
+
const firstString = entries.find(([, value]) => typeof value === "string");
|
|
101
|
+
if (firstString && typeof firstString[1] === "string") {
|
|
102
|
+
return truncate(firstString[1]);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (entries.length > 0) {
|
|
106
|
+
const [key, value] = entries[0];
|
|
107
|
+
return truncate(`${key}: ${toDisplayString(value)}`);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
return "Review the requested tool invocation.";
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export function getPermissionDetailEntries(
|
|
114
|
+
toolName: string | null,
|
|
115
|
+
input: PermissionToolInput | null
|
|
116
|
+
): Array<{ label: string; value: string }> {
|
|
117
|
+
if (!input) return [];
|
|
118
|
+
|
|
119
|
+
if (toolName === "Bash" || toolName === "bash") {
|
|
120
|
+
const command = input.command ?? input.cmd;
|
|
121
|
+
if (typeof command === "string") {
|
|
122
|
+
return [{ label: "Command", value: command }];
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
if (
|
|
127
|
+
toolName === "Read" ||
|
|
128
|
+
toolName === "Write" ||
|
|
129
|
+
toolName === "Edit" ||
|
|
130
|
+
toolName === "read" ||
|
|
131
|
+
toolName === "write" ||
|
|
132
|
+
toolName === "edit"
|
|
133
|
+
) {
|
|
134
|
+
const path = input.file_path ?? input.path;
|
|
135
|
+
if (typeof path === "string") {
|
|
136
|
+
return [{ label: "Path", value: path }];
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return Object.entries(input)
|
|
141
|
+
.slice(0, 6)
|
|
142
|
+
.map(([key, value]) => ({
|
|
143
|
+
label: key,
|
|
144
|
+
value: toDisplayString(value),
|
|
145
|
+
}));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getPermissionResponseLabel(response: string | null): string | null {
|
|
149
|
+
if (!response) return null;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
const parsed = JSON.parse(response) as {
|
|
153
|
+
behavior?: "allow" | "deny";
|
|
154
|
+
alwaysAllow?: boolean;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
if (parsed.behavior === "allow") {
|
|
158
|
+
return parsed.alwaysAllow ? "Always allowed" : "Allowed";
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
if (parsed.behavior === "deny") {
|
|
162
|
+
return "Denied";
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
return null;
|
|
166
|
+
} catch {
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
import { db } from "@/lib/db";
|
|
2
|
+
import { tasks, projects, agentLogs, notifications } from "@/lib/db/schema";
|
|
3
|
+
import { sql, eq, and, gte } from "drizzle-orm";
|
|
4
|
+
|
|
5
|
+
/** Helper: generate array of date strings (YYYY-MM-DD) for the last N days */
|
|
6
|
+
function lastNDays(days: number): string[] {
|
|
7
|
+
const result: string[] = [];
|
|
8
|
+
const now = new Date();
|
|
9
|
+
for (let i = days - 1; i >= 0; i--) {
|
|
10
|
+
const d = new Date(now);
|
|
11
|
+
d.setDate(d.getDate() - i);
|
|
12
|
+
result.push(d.toISOString().slice(0, 10));
|
|
13
|
+
}
|
|
14
|
+
return result;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/** Helper: merge query results with gap-filled date array */
|
|
18
|
+
function gapFill(
|
|
19
|
+
dates: string[],
|
|
20
|
+
rows: { date: string; count: number }[]
|
|
21
|
+
): number[] {
|
|
22
|
+
const map = new Map(rows.map((r) => [r.date, r.count]));
|
|
23
|
+
return dates.map((d) => map.get(d) ?? 0);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/** Unix timestamp for N days ago at midnight */
|
|
27
|
+
function daysAgoTimestamp(days: number): Date {
|
|
28
|
+
const d = new Date();
|
|
29
|
+
d.setDate(d.getDate() - days);
|
|
30
|
+
d.setHours(0, 0, 0, 0);
|
|
31
|
+
return d;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* 7-day task completion counts (one number per day).
|
|
36
|
+
*/
|
|
37
|
+
export async function getCompletionsByDay(days = 7): Promise<number[]> {
|
|
38
|
+
const dates = lastNDays(days);
|
|
39
|
+
const since = daysAgoTimestamp(days);
|
|
40
|
+
|
|
41
|
+
const rows = await db
|
|
42
|
+
.select({
|
|
43
|
+
date: sql<string>`strftime('%Y-%m-%d', ${tasks.updatedAt} , 'unixepoch')`,
|
|
44
|
+
count: sql<number>`count(*)`,
|
|
45
|
+
})
|
|
46
|
+
.from(tasks)
|
|
47
|
+
.where(and(eq(tasks.status, "completed"), gte(tasks.updatedAt, since)))
|
|
48
|
+
.groupBy(sql`strftime('%Y-%m-%d', ${tasks.updatedAt} , 'unixepoch')`);
|
|
49
|
+
|
|
50
|
+
return gapFill(dates, rows);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* 7-day task creation counts.
|
|
55
|
+
*/
|
|
56
|
+
export async function getTaskCreationsByDay(days = 7): Promise<number[]> {
|
|
57
|
+
const dates = lastNDays(days);
|
|
58
|
+
const since = daysAgoTimestamp(days);
|
|
59
|
+
|
|
60
|
+
const rows = await db
|
|
61
|
+
.select({
|
|
62
|
+
date: sql<string>`strftime('%Y-%m-%d', ${tasks.createdAt} , 'unixepoch')`,
|
|
63
|
+
count: sql<number>`count(*)`,
|
|
64
|
+
})
|
|
65
|
+
.from(tasks)
|
|
66
|
+
.where(gte(tasks.createdAt, since))
|
|
67
|
+
.groupBy(sql`strftime('%Y-%m-%d', ${tasks.createdAt} , 'unixepoch')`);
|
|
68
|
+
|
|
69
|
+
return gapFill(dates, rows);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* 7-day count of distinct active projects with task activity each day.
|
|
74
|
+
* Shows how many active projects had task updates per day.
|
|
75
|
+
*/
|
|
76
|
+
export async function getActiveProjectActivityByDay(days = 7): Promise<number[]> {
|
|
77
|
+
const dates = lastNDays(days);
|
|
78
|
+
const since = daysAgoTimestamp(days);
|
|
79
|
+
|
|
80
|
+
const rows = await db
|
|
81
|
+
.select({
|
|
82
|
+
date: sql<string>`strftime('%Y-%m-%d', ${tasks.updatedAt} , 'unixepoch')`,
|
|
83
|
+
count: sql<number>`count(distinct ${tasks.projectId})`,
|
|
84
|
+
})
|
|
85
|
+
.from(tasks)
|
|
86
|
+
.innerJoin(projects, eq(tasks.projectId, projects.id))
|
|
87
|
+
.where(and(eq(projects.status, "active"), gte(tasks.updatedAt, since)))
|
|
88
|
+
.groupBy(sql`strftime('%Y-%m-%d', ${tasks.updatedAt} , 'unixepoch')`);
|
|
89
|
+
|
|
90
|
+
return gapFill(dates, rows);
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* 24-hour agent activity (log counts per hour for the last 24h).
|
|
95
|
+
* Returns 24 numbers (index 0 = 24h ago, index 23 = current hour).
|
|
96
|
+
*/
|
|
97
|
+
export async function getAgentActivityByHour(): Promise<number[]> {
|
|
98
|
+
const since = new Date(Date.now() - 24 * 60 * 60 * 1000);
|
|
99
|
+
|
|
100
|
+
const rows = await db
|
|
101
|
+
.select({
|
|
102
|
+
hour: sql<number>`cast(strftime('%H', ${agentLogs.timestamp} , 'unixepoch') as integer)`,
|
|
103
|
+
count: sql<number>`count(*)`,
|
|
104
|
+
})
|
|
105
|
+
.from(agentLogs)
|
|
106
|
+
.where(gte(agentLogs.timestamp, since))
|
|
107
|
+
.groupBy(sql`strftime('%H', ${agentLogs.timestamp} , 'unixepoch')`);
|
|
108
|
+
|
|
109
|
+
// Build 24-slot array from current hour backward
|
|
110
|
+
const nowHour = new Date().getHours();
|
|
111
|
+
const hourMap = new Map(rows.map((r) => [r.hour, r.count]));
|
|
112
|
+
const result: number[] = [];
|
|
113
|
+
for (let i = 0; i < 24; i++) {
|
|
114
|
+
const h = (nowHour - 23 + i + 24) % 24;
|
|
115
|
+
result.push(hourMap.get(h) ?? 0);
|
|
116
|
+
}
|
|
117
|
+
return result;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* 7-day notification counts.
|
|
122
|
+
*/
|
|
123
|
+
export async function getNotificationsByDay(days = 7): Promise<number[]> {
|
|
124
|
+
const dates = lastNDays(days);
|
|
125
|
+
const since = daysAgoTimestamp(days);
|
|
126
|
+
|
|
127
|
+
const rows = await db
|
|
128
|
+
.select({
|
|
129
|
+
date: sql<string>`strftime('%Y-%m-%d', ${notifications.createdAt} , 'unixepoch')`,
|
|
130
|
+
count: sql<number>`count(*)`,
|
|
131
|
+
})
|
|
132
|
+
.from(notifications)
|
|
133
|
+
.where(gte(notifications.createdAt, since))
|
|
134
|
+
.groupBy(sql`strftime('%Y-%m-%d', ${notifications.createdAt} , 'unixepoch')`);
|
|
135
|
+
|
|
136
|
+
return gapFill(dates, rows);
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* N-day completion trend for a specific project.
|
|
141
|
+
*/
|
|
142
|
+
export async function getProjectCompletionTrend(
|
|
143
|
+
projectId: string,
|
|
144
|
+
days = 14
|
|
145
|
+
): Promise<number[]> {
|
|
146
|
+
const dates = lastNDays(days);
|
|
147
|
+
const since = daysAgoTimestamp(days);
|
|
148
|
+
|
|
149
|
+
const rows = await db
|
|
150
|
+
.select({
|
|
151
|
+
date: sql<string>`strftime('%Y-%m-%d', ${tasks.updatedAt} , 'unixepoch')`,
|
|
152
|
+
count: sql<number>`count(*)`,
|
|
153
|
+
})
|
|
154
|
+
.from(tasks)
|
|
155
|
+
.where(
|
|
156
|
+
and(
|
|
157
|
+
eq(tasks.projectId, projectId),
|
|
158
|
+
eq(tasks.status, "completed"),
|
|
159
|
+
gte(tasks.updatedAt, since)
|
|
160
|
+
)
|
|
161
|
+
)
|
|
162
|
+
.groupBy(sql`strftime('%Y-%m-%d', ${tasks.updatedAt} , 'unixepoch')`);
|
|
163
|
+
|
|
164
|
+
return gapFill(dates, rows);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
/**
|
|
168
|
+
* Status distribution for a project.
|
|
169
|
+
* Returns { status: string, count: number }[] for stacked bar segments.
|
|
170
|
+
*/
|
|
171
|
+
export async function getProjectStatusDistribution(
|
|
172
|
+
projectId: string
|
|
173
|
+
): Promise<{ status: string; count: number }[]> {
|
|
174
|
+
const rows = await db
|
|
175
|
+
.select({
|
|
176
|
+
status: tasks.status,
|
|
177
|
+
count: sql<number>`count(*)`,
|
|
178
|
+
})
|
|
179
|
+
.from(tasks)
|
|
180
|
+
.where(eq(tasks.projectId, projectId))
|
|
181
|
+
.groupBy(tasks.status);
|
|
182
|
+
|
|
183
|
+
return rows;
|
|
184
|
+
}
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Parse human-friendly interval strings into 5-field cron expressions.
|
|
3
|
+
*
|
|
4
|
+
* Supported formats:
|
|
5
|
+
* - `5m` → every 5 minutes → `*/5 * * * *`
|
|
6
|
+
* - `2h` → every 2 hours → `0 */2 * * *`
|
|
7
|
+
* - `1d` → daily at 9am → `0 9 * * *`
|
|
8
|
+
* - `30s` → not supported (sub-minute precision is not allowed)
|
|
9
|
+
* - Raw cron expressions (5 fields) are returned as-is after validation
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { CronExpressionParser } from "cron-parser";
|
|
13
|
+
|
|
14
|
+
const INTERVAL_RE = /^(\d+)\s*(m|min|mins|minutes?|h|hr|hrs|hours?|d|day|days)$/i;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Convert a human-friendly interval string or raw cron expression into a
|
|
18
|
+
* validated 5-field cron expression.
|
|
19
|
+
*
|
|
20
|
+
* @returns The cron expression string
|
|
21
|
+
* @throws If the input cannot be parsed
|
|
22
|
+
*/
|
|
23
|
+
export function parseInterval(input: string): string {
|
|
24
|
+
const trimmed = input.trim();
|
|
25
|
+
if (!trimmed) throw new Error("Interval cannot be empty");
|
|
26
|
+
|
|
27
|
+
// Try as human-friendly shorthand first
|
|
28
|
+
const match = trimmed.match(INTERVAL_RE);
|
|
29
|
+
if (match) {
|
|
30
|
+
const value = parseInt(match[1], 10);
|
|
31
|
+
const unit = match[2].toLowerCase().charAt(0); // m, h, or d
|
|
32
|
+
|
|
33
|
+
if (value <= 0) throw new Error("Interval value must be positive");
|
|
34
|
+
|
|
35
|
+
switch (unit) {
|
|
36
|
+
case "m":
|
|
37
|
+
if (value > 59) throw new Error("Minute interval must be 1-59");
|
|
38
|
+
return value === 1 ? "* * * * *" : `*/${value} * * * *`;
|
|
39
|
+
case "h":
|
|
40
|
+
if (value > 23) throw new Error("Hour interval must be 1-23");
|
|
41
|
+
return value === 1 ? "0 * * * *" : `0 */${value} * * *`;
|
|
42
|
+
case "d":
|
|
43
|
+
if (value === 1) return "0 9 * * *"; // daily at 9am
|
|
44
|
+
if (value > 31) throw new Error("Day interval must be 1-31");
|
|
45
|
+
return `0 9 */${value} * *`;
|
|
46
|
+
default:
|
|
47
|
+
throw new Error(`Unknown unit: ${unit}`);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// Try as raw cron expression (5 fields)
|
|
52
|
+
const fields = trimmed.split(/\s+/);
|
|
53
|
+
if (fields.length === 5) {
|
|
54
|
+
// Validate by parsing — cron-parser will throw if invalid
|
|
55
|
+
CronExpressionParser.parse(trimmed);
|
|
56
|
+
return trimmed;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
throw new Error(
|
|
60
|
+
`Cannot parse interval "${trimmed}". Use formats like 5m, 2h, 1d, or a 5-field cron expression.`
|
|
61
|
+
);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Compute the next fire time from a cron expression.
|
|
66
|
+
*
|
|
67
|
+
* @param cronExpression A valid 5-field cron expression
|
|
68
|
+
* @param from Base date to compute from (defaults to now)
|
|
69
|
+
* @returns The next fire Date
|
|
70
|
+
*/
|
|
71
|
+
export function computeNextFireTime(cronExpression: string, from?: Date): Date {
|
|
72
|
+
const expr = CronExpressionParser.parse(cronExpression, {
|
|
73
|
+
currentDate: from ?? new Date(),
|
|
74
|
+
});
|
|
75
|
+
return expr.next().toDate();
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Generate a human-readable description of a cron expression.
|
|
80
|
+
*/
|
|
81
|
+
export function describeCron(cronExpression: string): string {
|
|
82
|
+
const fields = cronExpression.split(/\s+/);
|
|
83
|
+
if (fields.length !== 5) return cronExpression;
|
|
84
|
+
|
|
85
|
+
const [minute, hour, dom, , dow] = fields;
|
|
86
|
+
|
|
87
|
+
// Common patterns
|
|
88
|
+
if (minute === "*" && hour === "*" && dom === "*" && dow === "*") {
|
|
89
|
+
return "Every minute";
|
|
90
|
+
}
|
|
91
|
+
if (minute.startsWith("*/") && hour === "*" && dom === "*" && dow === "*") {
|
|
92
|
+
const mins = minute.slice(2);
|
|
93
|
+
return `Every ${mins} minutes`;
|
|
94
|
+
}
|
|
95
|
+
if (minute === "0" && hour === "*" && dom === "*" && dow === "*") {
|
|
96
|
+
return "Every hour";
|
|
97
|
+
}
|
|
98
|
+
if (minute === "0" && hour.startsWith("*/") && dom === "*" && dow === "*") {
|
|
99
|
+
const hrs = hour.slice(2);
|
|
100
|
+
return `Every ${hrs} hours`;
|
|
101
|
+
}
|
|
102
|
+
if (minute === "0" && hour === "9" && dom === "*" && dow === "*") {
|
|
103
|
+
return "Daily at 9:00 AM";
|
|
104
|
+
}
|
|
105
|
+
if (minute === "0" && hour === "9" && dom === "*" && dow === "1-5") {
|
|
106
|
+
return "Weekdays at 9:00 AM";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return cronExpression;
|
|
110
|
+
}
|