stagent 0.9.6 → 0.11.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/README.md +20 -44
- package/dist/cli.js +66 -18
- package/docs/.coverage-gaps.json +144 -56
- package/docs/.last-generated +1 -1
- package/docs/features/agent-intelligence.md +12 -2
- package/docs/features/chat.md +40 -5
- package/docs/features/cost-usage.md +1 -1
- package/docs/features/documents.md +5 -2
- package/docs/features/inbox-notifications.md +10 -2
- package/docs/features/keyboard-navigation.md +12 -3
- package/docs/features/provider-runtimes.md +20 -2
- package/docs/features/schedules.md +32 -4
- package/docs/features/settings.md +28 -5
- package/docs/features/shared-components.md +7 -3
- package/docs/features/tables.md +11 -2
- package/docs/features/tool-permissions.md +6 -2
- package/docs/features/workflows.md +14 -4
- package/docs/index.md +1 -1
- package/docs/journeys/developer.md +39 -2
- package/docs/journeys/personal-use.md +32 -8
- package/docs/journeys/power-user.md +45 -14
- package/docs/journeys/work-use.md +17 -8
- package/docs/manifest.json +15 -15
- package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
- package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
- package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
- package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
- package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
- package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
- package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
- package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
- package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
- package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
- package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
- package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
- package/next.config.mjs +1 -0
- package/package.json +3 -2
- package/src/__tests__/instrumentation-smoke.test.ts +15 -0
- package/src/app/analytics/page.tsx +1 -21
- package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
- package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
- package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
- package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
- package/src/app/api/chat/export/route.ts +52 -0
- package/src/app/api/chat/files/search/route.ts +50 -0
- package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
- package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
- package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
- package/src/app/api/environment/skills/route.ts +13 -0
- package/src/app/api/instance/config/route.ts +41 -0
- package/src/app/api/instance/init/route.ts +34 -0
- package/src/app/api/instance/upgrade/check/route.ts +26 -0
- package/src/app/api/instance/upgrade/route.ts +96 -0
- package/src/app/api/instance/upgrade/status/route.ts +35 -0
- package/src/app/api/memory/route.ts +0 -11
- package/src/app/api/notifications/route.ts +4 -2
- package/src/app/api/projects/[id]/route.ts +5 -155
- package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
- package/src/app/api/schedules/[id]/execute/route.ts +111 -0
- package/src/app/api/schedules/[id]/route.ts +9 -1
- package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
- package/src/app/api/schedules/route.ts +3 -12
- package/src/app/api/settings/chat/pins/route.ts +94 -0
- package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
- package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
- package/src/app/api/settings/environment/route.ts +26 -0
- package/src/app/api/settings/openai/login/route.ts +22 -0
- package/src/app/api/settings/openai/logout/route.ts +7 -0
- package/src/app/api/settings/openai/route.ts +21 -1
- package/src/app/api/settings/providers/route.ts +35 -8
- package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
- package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
- package/src/app/api/tables/[id]/enrich/route.ts +147 -0
- package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
- package/src/app/api/tasks/[id]/execute/route.ts +52 -33
- package/src/app/api/tasks/[id]/respond/route.ts +31 -15
- package/src/app/api/tasks/[id]/resume/route.ts +24 -3
- package/src/app/api/workflows/[id]/resume/route.ts +59 -0
- package/src/app/api/workflows/[id]/status/route.ts +22 -8
- package/src/app/api/workspace/context/route.ts +2 -0
- package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
- package/src/app/chat/page.tsx +11 -0
- package/src/app/documents/page.tsx +4 -1
- package/src/app/inbox/page.tsx +12 -5
- package/src/app/layout.tsx +42 -21
- package/src/app/page.tsx +0 -2
- package/src/app/settings/page.tsx +8 -9
- package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
- package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
- package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
- package/src/components/chat/capability-banner.tsx +68 -0
- package/src/components/chat/chat-command-popover.tsx +670 -49
- package/src/components/chat/chat-input.tsx +104 -10
- package/src/components/chat/chat-message.tsx +12 -3
- package/src/components/chat/chat-session-provider.tsx +790 -0
- package/src/components/chat/chat-shell.tsx +151 -401
- package/src/components/chat/command-tab-bar.tsx +68 -0
- package/src/components/chat/conversation-template-picker.tsx +421 -0
- package/src/components/chat/help-dialog.tsx +39 -0
- package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
- package/src/components/chat/skill-row.tsx +147 -0
- package/src/components/documents/document-browser.tsx +37 -19
- package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
- package/src/components/instance/instance-section.tsx +382 -0
- package/src/components/instance/upgrade-badge.tsx +219 -0
- package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
- package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
- package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
- package/src/components/notifications/batch-proposal-review.tsx +20 -5
- package/src/components/notifications/inbox-list.tsx +11 -2
- package/src/components/notifications/notification-item.tsx +56 -2
- package/src/components/notifications/pending-approval-host.tsx +56 -37
- package/src/components/notifications/permission-response-actions.tsx +155 -1
- package/src/components/schedules/schedule-create-sheet.tsx +19 -1
- package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
- package/src/components/schedules/schedule-form.tsx +31 -0
- package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
- package/src/components/settings/auth-method-selector.tsx +19 -4
- package/src/components/settings/auth-status-badge.tsx +28 -3
- package/src/components/settings/environment-section.tsx +102 -0
- package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
- package/src/components/settings/openai-runtime-section.tsx +7 -1
- package/src/components/settings/providers-runtimes-section.tsx +138 -19
- package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
- package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
- package/src/components/shared/app-sidebar.tsx +4 -3
- package/src/components/shared/command-palette.tsx +266 -7
- package/src/components/shared/filter-hint.tsx +70 -0
- package/src/components/shared/filter-input.tsx +59 -0
- package/src/components/shared/saved-searches-manager.tsx +199 -0
- package/src/components/shared/theme-toggle.tsx +5 -24
- package/src/components/shared/workspace-indicator.tsx +61 -2
- package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
- package/src/components/tables/table-create-sheet.tsx +4 -0
- package/src/components/tables/table-enrichment-runs.tsx +103 -0
- package/src/components/tables/table-enrichment-sheet.tsx +538 -0
- package/src/components/tables/table-spreadsheet.tsx +29 -5
- package/src/components/tables/table-toolbar.tsx +10 -1
- package/src/components/tasks/kanban-board.tsx +1 -0
- package/src/components/tasks/kanban-column.tsx +53 -14
- package/src/components/tasks/task-bento-grid.tsx +31 -2
- package/src/components/tasks/task-card.tsx +29 -3
- package/src/components/tasks/task-chip-bar.tsx +54 -1
- package/src/components/tasks/task-result-renderer.tsx +1 -1
- package/src/components/workflows/delay-step-body.tsx +109 -0
- package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
- package/src/components/workflows/loop-status-view.tsx +1 -1
- package/src/components/workflows/shared/step-result.tsx +78 -0
- package/src/components/workflows/shared/workflow-header.tsx +141 -0
- package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
- package/src/components/workflows/swarm-dashboard.tsx +2 -15
- package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
- package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
- package/src/components/workflows/workflow-form-view.tsx +133 -16
- package/src/components/workflows/workflow-status-view.tsx +30 -740
- package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
- package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
- package/src/hooks/use-active-skills.ts +110 -0
- package/src/hooks/use-chat-autocomplete.ts +120 -7
- package/src/hooks/use-enriched-skills.ts +19 -0
- package/src/hooks/use-pinned-entries.ts +104 -0
- package/src/hooks/use-recent-user-messages.ts +19 -0
- package/src/hooks/use-saved-searches.ts +142 -0
- package/src/instrumentation-node.ts +94 -0
- package/src/instrumentation.ts +4 -48
- package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
- package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
- package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
- package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
- package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
- package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
- package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
- package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
- package/src/lib/agents/claude-agent.ts +217 -21
- package/src/lib/agents/execution-manager.ts +0 -35
- package/src/lib/agents/handoff/bus.ts +2 -2
- package/src/lib/agents/learned-context.ts +0 -12
- package/src/lib/agents/learning-session.ts +18 -5
- package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
- package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
- package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
- package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
- package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
- package/src/lib/agents/profiles/registry.ts +18 -0
- package/src/lib/agents/profiles/types.ts +7 -1
- package/src/lib/agents/router.ts +3 -6
- package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
- package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
- package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
- package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
- package/src/lib/agents/runtime/catalog.ts +121 -0
- package/src/lib/agents/runtime/claude-sdk.ts +32 -0
- package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
- package/src/lib/agents/runtime/execution-target.ts +456 -0
- package/src/lib/agents/runtime/index.ts +4 -0
- package/src/lib/agents/runtime/launch-failure.ts +101 -0
- package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
- package/src/lib/agents/runtime/openai-codex.ts +64 -60
- package/src/lib/agents/runtime/openai-direct.ts +8 -0
- package/src/lib/agents/runtime/types.ts +8 -0
- package/src/lib/agents/task-dispatch.ts +220 -0
- package/src/lib/agents/tool-permissions.ts +16 -1
- package/src/lib/book/chapter-mapping.ts +11 -0
- package/src/lib/book/content.ts +10 -0
- package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
- package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
- package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
- package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
- package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
- package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
- package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
- package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
- package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
- package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
- package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
- package/src/lib/chat/__tests__/types.test.ts +28 -0
- package/src/lib/chat/active-skills.ts +31 -0
- package/src/lib/chat/active-streams.ts +27 -0
- package/src/lib/chat/clean-filter-input.ts +30 -0
- package/src/lib/chat/codex-engine.ts +46 -24
- package/src/lib/chat/command-tabs.ts +61 -0
- package/src/lib/chat/context-builder.ts +146 -4
- package/src/lib/chat/dismissals.ts +73 -0
- package/src/lib/chat/engine.ts +159 -18
- package/src/lib/chat/files/__tests__/search.test.ts +135 -0
- package/src/lib/chat/files/expand-mention.ts +76 -0
- package/src/lib/chat/files/search.ts +99 -0
- package/src/lib/chat/reconcile.ts +117 -0
- package/src/lib/chat/skill-composition.ts +210 -0
- package/src/lib/chat/skill-conflict.ts +105 -0
- package/src/lib/chat/stagent-tools.ts +7 -19
- package/src/lib/chat/stream-telemetry.ts +137 -0
- package/src/lib/chat/suggested-prompts.ts +28 -1
- package/src/lib/chat/system-prompt.ts +48 -1
- package/src/lib/chat/tool-catalog.ts +35 -4
- package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
- package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
- package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
- package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
- package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
- package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
- package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
- package/src/lib/chat/tools/blueprint-tools.ts +190 -0
- package/src/lib/chat/tools/document-tools.ts +29 -13
- package/src/lib/chat/tools/helpers.ts +41 -0
- package/src/lib/chat/tools/notification-tools.ts +9 -5
- package/src/lib/chat/tools/profile-tools.ts +120 -23
- package/src/lib/chat/tools/project-tools.ts +33 -0
- package/src/lib/chat/tools/schedule-tools.ts +44 -11
- package/src/lib/chat/tools/skill-tools.ts +183 -0
- package/src/lib/chat/tools/table-tools.ts +71 -0
- package/src/lib/chat/tools/task-tools.ts +89 -21
- package/src/lib/chat/tools/workflow-tools.ts +275 -32
- package/src/lib/chat/types.ts +15 -0
- package/src/lib/constants/settings.ts +10 -18
- package/src/lib/data/__tests__/clear.test.ts +56 -2
- package/src/lib/data/clear.ts +17 -16
- package/src/lib/data/delete-project.ts +171 -0
- package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
- package/src/lib/db/bootstrap.ts +62 -16
- package/src/lib/db/index.ts +5 -0
- package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
- package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
- package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
- package/src/lib/db/migrations/0026_drop_license.sql +3 -0
- package/src/lib/db/migrations/meta/_journal.json +21 -0
- package/src/lib/db/schema.ts +94 -23
- package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
- package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
- package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
- package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
- package/src/lib/environment/data.ts +9 -0
- package/src/lib/environment/list-skills.ts +176 -0
- package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
- package/src/lib/environment/parsers/skill.ts +26 -5
- package/src/lib/environment/profile-generator.ts +54 -0
- package/src/lib/environment/skill-enrichment.ts +106 -0
- package/src/lib/environment/skill-recommendations.ts +66 -0
- package/src/lib/environment/workspace-context.ts +13 -1
- package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
- package/src/lib/filters/__tests__/parse.test.ts +135 -0
- package/src/lib/filters/parse.ts +86 -0
- package/src/lib/import/dedup.ts +4 -54
- package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
- package/src/lib/instance/__tests__/detect.test.ts +115 -0
- package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
- package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
- package/src/lib/instance/__tests__/settings.test.ts +83 -0
- package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
- package/src/lib/instance/bootstrap.ts +270 -0
- package/src/lib/instance/detect.ts +49 -0
- package/src/lib/instance/fingerprint.ts +76 -0
- package/src/lib/instance/git-ops.ts +95 -0
- package/src/lib/instance/settings.ts +61 -0
- package/src/lib/instance/types.ts +77 -0
- package/src/lib/instance/upgrade-poller.ts +205 -0
- package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
- package/src/lib/notifications/visibility.ts +33 -0
- package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
- package/src/lib/schedules/__tests__/config.test.ts +62 -0
- package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
- package/src/lib/schedules/__tests__/integration.test.ts +82 -0
- package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
- package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
- package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
- package/src/lib/schedules/collision-check.ts +105 -0
- package/src/lib/schedules/config.ts +53 -0
- package/src/lib/schedules/scheduler.ts +236 -17
- package/src/lib/schedules/slot-claim.ts +105 -0
- package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
- package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
- package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
- package/src/lib/settings/openai-auth.ts +105 -10
- package/src/lib/settings/openai-login-manager.ts +260 -0
- package/src/lib/settings/runtime-setup.ts +14 -4
- package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
- package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
- package/src/lib/tables/enrichment-planner.ts +454 -0
- package/src/lib/tables/enrichment.ts +328 -0
- package/src/lib/tables/query-builder.ts +5 -2
- package/src/lib/tables/trigger-evaluator.ts +3 -2
- package/src/lib/theme.ts +71 -0
- package/src/lib/usage/ledger.ts +2 -18
- package/src/lib/util/__tests__/similarity.test.ts +106 -0
- package/src/lib/util/similarity.ts +77 -0
- package/src/lib/utils/format-timestamp.ts +24 -0
- package/src/lib/utils/stagent-paths.ts +12 -0
- package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
- package/src/lib/validators/__tests__/settings.test.ts +10 -0
- package/src/lib/validators/blueprint.ts +70 -9
- package/src/lib/validators/profile.ts +2 -2
- package/src/lib/validators/settings.ts +3 -1
- package/src/lib/workflows/__tests__/delay.test.ts +196 -0
- package/src/lib/workflows/__tests__/engine.test.ts +8 -0
- package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
- package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
- package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
- package/src/lib/workflows/blueprints/instantiator.ts +22 -1
- package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
- package/src/lib/workflows/blueprints/types.ts +16 -2
- package/src/lib/workflows/delay.ts +106 -0
- package/src/lib/workflows/engine.ts +212 -7
- package/src/lib/workflows/loop-executor.ts +349 -24
- package/src/lib/workflows/post-action.ts +91 -0
- package/src/lib/workflows/types.ts +166 -1
- package/src/test/setup.ts +10 -0
- package/src/app/api/license/checkout/route.ts +0 -28
- package/src/app/api/license/portal/route.ts +0 -26
- package/src/app/api/license/route.ts +0 -89
- package/src/app/api/license/usage/route.ts +0 -63
- package/src/app/api/marketplace/browse/route.ts +0 -15
- package/src/app/api/marketplace/import/route.ts +0 -28
- package/src/app/api/marketplace/publish/route.ts +0 -40
- package/src/app/api/onboarding/email/route.ts +0 -53
- package/src/app/api/settings/telemetry/route.ts +0 -14
- package/src/app/api/sync/export/route.ts +0 -54
- package/src/app/api/sync/restore/route.ts +0 -37
- package/src/app/api/sync/sessions/route.ts +0 -24
- package/src/app/auth/callback/route.ts +0 -73
- package/src/app/marketplace/page.tsx +0 -19
- package/src/components/analytics/analytics-gate-card.tsx +0 -101
- package/src/components/marketplace/blueprint-card.tsx +0 -61
- package/src/components/marketplace/marketplace-browser.tsx +0 -131
- package/src/components/onboarding/email-capture-card.tsx +0 -104
- package/src/components/settings/activation-form.tsx +0 -95
- package/src/components/settings/cloud-account-section.tsx +0 -147
- package/src/components/settings/cloud-sync-section.tsx +0 -155
- package/src/components/settings/subscription-section.tsx +0 -410
- package/src/components/settings/telemetry-section.tsx +0 -80
- package/src/components/shared/premium-gate-overlay.tsx +0 -50
- package/src/components/shared/schedule-gate-dialog.tsx +0 -64
- package/src/components/shared/upgrade-banner.tsx +0 -112
- package/src/hooks/use-supabase-auth.ts +0 -79
- package/src/lib/billing/email.ts +0 -54
- package/src/lib/billing/products.ts +0 -80
- package/src/lib/billing/stripe.ts +0 -101
- package/src/lib/cloud/supabase-browser.ts +0 -32
- package/src/lib/cloud/supabase-client.ts +0 -56
- package/src/lib/license/__tests__/features.test.ts +0 -56
- package/src/lib/license/__tests__/key-format.test.ts +0 -88
- package/src/lib/license/__tests__/manager.test.ts +0 -64
- package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
- package/src/lib/license/cloud-validation.ts +0 -60
- package/src/lib/license/features.ts +0 -44
- package/src/lib/license/key-format.ts +0 -101
- package/src/lib/license/limit-check.ts +0 -111
- package/src/lib/license/limit-queries.ts +0 -51
- package/src/lib/license/manager.ts +0 -345
- package/src/lib/license/notifications.ts +0 -59
- package/src/lib/license/tier-limits.ts +0 -71
- package/src/lib/marketplace/marketplace-client.ts +0 -107
- package/src/lib/sync/cloud-sync.ts +0 -235
- package/src/lib/telemetry/conversion-events.ts +0 -71
- package/src/lib/telemetry/queue.ts +0 -122
- package/src/lib/validators/license.ts +0 -33
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
resolvePostAction,
|
|
4
|
+
shouldSkipPostActionValue,
|
|
5
|
+
extractPostActionValue,
|
|
6
|
+
} from "../post-action";
|
|
7
|
+
import type { StepPostAction } from "../types";
|
|
8
|
+
|
|
9
|
+
describe("resolvePostAction", () => {
|
|
10
|
+
it("substitutes {{row.id}} with the row's id field", () => {
|
|
11
|
+
const action: StepPostAction = {
|
|
12
|
+
type: "update_row",
|
|
13
|
+
tableId: "tbl_contacts",
|
|
14
|
+
rowId: "{{row.id}}",
|
|
15
|
+
column: "linkedin",
|
|
16
|
+
};
|
|
17
|
+
const row = { id: "row_abc", name: "Alice" };
|
|
18
|
+
|
|
19
|
+
const resolved = resolvePostAction(action, row, "row");
|
|
20
|
+
|
|
21
|
+
expect(resolved.rowId).toBe("row_abc");
|
|
22
|
+
expect(resolved.tableId).toBe("tbl_contacts");
|
|
23
|
+
expect(resolved.column).toBe("linkedin");
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("respects a custom itemVariable name", () => {
|
|
27
|
+
const action: StepPostAction = {
|
|
28
|
+
type: "update_row",
|
|
29
|
+
tableId: "tbl_x",
|
|
30
|
+
rowId: "{{contact.id}}",
|
|
31
|
+
column: "email",
|
|
32
|
+
};
|
|
33
|
+
const row = { id: "c_42" };
|
|
34
|
+
|
|
35
|
+
const resolved = resolvePostAction(action, row, "contact");
|
|
36
|
+
|
|
37
|
+
expect(resolved.rowId).toBe("c_42");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("supports nested field paths like {{row.meta.id}}", () => {
|
|
41
|
+
const action: StepPostAction = {
|
|
42
|
+
type: "update_row",
|
|
43
|
+
tableId: "tbl_x",
|
|
44
|
+
rowId: "{{row.meta.id}}",
|
|
45
|
+
column: "value",
|
|
46
|
+
};
|
|
47
|
+
const row = { meta: { id: "nested_99" } };
|
|
48
|
+
|
|
49
|
+
const resolved = resolvePostAction(action, row, "row");
|
|
50
|
+
|
|
51
|
+
expect(resolved.rowId).toBe("nested_99");
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("leaves rowId untouched when no placeholder is present", () => {
|
|
55
|
+
const action: StepPostAction = {
|
|
56
|
+
type: "update_row",
|
|
57
|
+
tableId: "tbl_x",
|
|
58
|
+
rowId: "literal_row_id",
|
|
59
|
+
column: "value",
|
|
60
|
+
};
|
|
61
|
+
|
|
62
|
+
const resolved = resolvePostAction(action, { id: "ignored" }, "row");
|
|
63
|
+
|
|
64
|
+
expect(resolved.rowId).toBe("literal_row_id");
|
|
65
|
+
});
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
describe("shouldSkipPostActionValue", () => {
|
|
69
|
+
it("skips empty strings", () => {
|
|
70
|
+
expect(shouldSkipPostActionValue("")).toBe(true);
|
|
71
|
+
expect(shouldSkipPostActionValue(" ")).toBe(true);
|
|
72
|
+
expect(shouldSkipPostActionValue("\n\t")).toBe(true);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("skips NOT_FOUND sentinel (case-insensitive)", () => {
|
|
76
|
+
expect(shouldSkipPostActionValue("NOT_FOUND")).toBe(true);
|
|
77
|
+
expect(shouldSkipPostActionValue("not_found")).toBe(true);
|
|
78
|
+
expect(shouldSkipPostActionValue(" NOT_FOUND ")).toBe(true);
|
|
79
|
+
expect(shouldSkipPostActionValue("Not_Found")).toBe(true);
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("does not skip real values", () => {
|
|
83
|
+
expect(shouldSkipPostActionValue("https://linkedin.com/in/alice")).toBe(false);
|
|
84
|
+
expect(shouldSkipPostActionValue("alice@example.com")).toBe(false);
|
|
85
|
+
expect(shouldSkipPostActionValue("0")).toBe(false);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("does not skip values that merely contain NOT_FOUND as a substring", () => {
|
|
89
|
+
// We only skip when the trimmed value IS the sentinel; substrings stay.
|
|
90
|
+
expect(shouldSkipPostActionValue("Status: NOT_FOUND in registry")).toBe(false);
|
|
91
|
+
});
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
describe("extractPostActionValue", () => {
|
|
95
|
+
it("trims whitespace from the agent result", () => {
|
|
96
|
+
expect(extractPostActionValue(" hello ")).toBe("hello");
|
|
97
|
+
expect(extractPostActionValue("\nhttps://example.com\n")).toBe("https://example.com");
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
it("returns the raw string for normal values", () => {
|
|
101
|
+
expect(extractPostActionValue("plain")).toBe("plain");
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("returns empty string for null/undefined-shaped inputs", () => {
|
|
105
|
+
expect(extractPostActionValue(undefined)).toBe("");
|
|
106
|
+
expect(extractPostActionValue("")).toBe("");
|
|
107
|
+
});
|
|
108
|
+
});
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
renderBlueprintPrompt,
|
|
4
|
+
UnresolvedTokenError,
|
|
5
|
+
} from "../render-prompt";
|
|
6
|
+
import type { WorkflowBlueprint } from "../types";
|
|
7
|
+
|
|
8
|
+
function makeBlueprint(
|
|
9
|
+
overrides: Partial<WorkflowBlueprint> = {}
|
|
10
|
+
): WorkflowBlueprint {
|
|
11
|
+
return {
|
|
12
|
+
id: "test-bp",
|
|
13
|
+
name: "Test Blueprint",
|
|
14
|
+
description: "A blueprint for tests",
|
|
15
|
+
version: "1.0.0",
|
|
16
|
+
domain: "work",
|
|
17
|
+
tags: [],
|
|
18
|
+
pattern: "sequence",
|
|
19
|
+
variables: [],
|
|
20
|
+
steps: [],
|
|
21
|
+
...overrides,
|
|
22
|
+
};
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
describe("renderBlueprintPrompt", () => {
|
|
26
|
+
it("uses chatPrompt when present", () => {
|
|
27
|
+
const bp = makeBlueprint({
|
|
28
|
+
chatPrompt: "Research {{topic}} for {{timeframe}}.",
|
|
29
|
+
steps: [
|
|
30
|
+
{ name: "s1", promptTemplate: "fallback should not be used", requiresApproval: false },
|
|
31
|
+
],
|
|
32
|
+
});
|
|
33
|
+
const out = renderBlueprintPrompt(bp, { topic: "AI agents", timeframe: "2026" });
|
|
34
|
+
expect(out.firstMessage).toBe("Research AI agents for 2026.");
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it("falls back to steps[0].promptTemplate when chatPrompt absent", () => {
|
|
38
|
+
const bp = makeBlueprint({
|
|
39
|
+
steps: [
|
|
40
|
+
{
|
|
41
|
+
name: "s1",
|
|
42
|
+
promptTemplate: "Summarize {{topic}} in ≤500 words.",
|
|
43
|
+
requiresApproval: false,
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
});
|
|
47
|
+
const out = renderBlueprintPrompt(bp, { topic: "browser agents" });
|
|
48
|
+
expect(out.firstMessage).toBe("Summarize browser agents in ≤500 words.");
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("renders title with variable substitution", () => {
|
|
52
|
+
const bp = makeBlueprint({
|
|
53
|
+
name: "Research {{topic}}",
|
|
54
|
+
chatPrompt: "go",
|
|
55
|
+
});
|
|
56
|
+
const out = renderBlueprintPrompt(bp, { topic: "SSE streams" });
|
|
57
|
+
expect(out.title).toBe("Research SSE streams");
|
|
58
|
+
});
|
|
59
|
+
|
|
60
|
+
it("returns empty firstMessage when no chatPrompt and no steps", () => {
|
|
61
|
+
const bp = makeBlueprint({ steps: [] });
|
|
62
|
+
const out = renderBlueprintPrompt(bp, {});
|
|
63
|
+
expect(out.firstMessage).toBe("");
|
|
64
|
+
expect(out.title).toBe("Test Blueprint");
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("substitutes missing optional variables with empty string (non-strict)", () => {
|
|
68
|
+
const bp = makeBlueprint({
|
|
69
|
+
chatPrompt: "Topic: {{topic}}. Scope: {{scope}}.",
|
|
70
|
+
});
|
|
71
|
+
const out = renderBlueprintPrompt(bp, { topic: "rate limits" });
|
|
72
|
+
expect(out.firstMessage).toBe("Topic: rate limits. Scope: .");
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("handles {{#if}} conditional blocks", () => {
|
|
76
|
+
const bp = makeBlueprint({
|
|
77
|
+
chatPrompt:
|
|
78
|
+
"Base prompt.{{#if extra}}\n\nExtra: {{extra}}{{/if}}",
|
|
79
|
+
});
|
|
80
|
+
const withExtra = renderBlueprintPrompt(bp, { extra: "deep dive" });
|
|
81
|
+
expect(withExtra.firstMessage).toBe("Base prompt.\n\nExtra: deep dive");
|
|
82
|
+
|
|
83
|
+
const withoutExtra = renderBlueprintPrompt(bp, {});
|
|
84
|
+
expect(withoutExtra.firstMessage).toBe("Base prompt.");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("throws UnresolvedTokenError in strict mode when token is missing", () => {
|
|
88
|
+
const bp = makeBlueprint({
|
|
89
|
+
chatPrompt: "Hello {{name}}, today is {{date}}.",
|
|
90
|
+
});
|
|
91
|
+
expect(() =>
|
|
92
|
+
renderBlueprintPrompt(bp, { name: "Ada" }, { strict: true })
|
|
93
|
+
).toThrow(UnresolvedTokenError);
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
renderBlueprintPrompt(bp, { name: "Ada" }, { strict: true });
|
|
97
|
+
} catch (err) {
|
|
98
|
+
expect(err).toBeInstanceOf(UnresolvedTokenError);
|
|
99
|
+
expect((err as UnresolvedTokenError).tokens).toEqual(["date"]);
|
|
100
|
+
}
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
it("does not throw in strict mode when all tokens resolve", () => {
|
|
104
|
+
const bp = makeBlueprint({
|
|
105
|
+
chatPrompt: "Hello {{name}}.",
|
|
106
|
+
});
|
|
107
|
+
const out = renderBlueprintPrompt(
|
|
108
|
+
bp,
|
|
109
|
+
{ name: "Ada" },
|
|
110
|
+
{ strict: true }
|
|
111
|
+
);
|
|
112
|
+
expect(out.firstMessage).toBe("Hello Ada.");
|
|
113
|
+
});
|
|
114
|
+
|
|
115
|
+
it("strict mode treats empty-string values as resolved (not unresolved)", () => {
|
|
116
|
+
// Empty string is a resolved-to-empty substitution, distinct from an
|
|
117
|
+
// undefined variable which leaves {{token}} in the output string.
|
|
118
|
+
const bp = makeBlueprint({
|
|
119
|
+
chatPrompt: "Name: [{{name}}]",
|
|
120
|
+
});
|
|
121
|
+
const out = renderBlueprintPrompt(bp, { name: "" }, { strict: true });
|
|
122
|
+
expect(out.firstMessage).toBe("Name: []");
|
|
123
|
+
});
|
|
124
|
+
});
|
|
@@ -2,7 +2,7 @@ import { db } from "@/lib/db";
|
|
|
2
2
|
import { workflows } from "@/lib/db/schema";
|
|
3
3
|
import { getBlueprint } from "./registry";
|
|
4
4
|
import { resolveTemplate, evaluateCondition } from "./template";
|
|
5
|
-
import type { BlueprintVariable
|
|
5
|
+
import type { BlueprintVariable } from "./types";
|
|
6
6
|
import type { WorkflowStep } from "../types";
|
|
7
7
|
|
|
8
8
|
interface InstantiateResult {
|
|
@@ -48,6 +48,27 @@ export async function instantiateBlueprint(
|
|
|
48
48
|
continue;
|
|
49
49
|
}
|
|
50
50
|
|
|
51
|
+
// Delay step: a pure time wait with no prompt/profile. Blueprint validation
|
|
52
|
+
// enforces that delayDuration and profileId+promptTemplate are mutually
|
|
53
|
+
// exclusive (XOR), so branching here is safe.
|
|
54
|
+
if (step.delayDuration) {
|
|
55
|
+
resolvedSteps.push({
|
|
56
|
+
id: crypto.randomUUID(),
|
|
57
|
+
name: step.name,
|
|
58
|
+
prompt: "",
|
|
59
|
+
requiresApproval: step.requiresApproval,
|
|
60
|
+
delayDuration: step.delayDuration,
|
|
61
|
+
});
|
|
62
|
+
continue;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// Task step: profileId + promptTemplate must be present (XOR contract).
|
|
66
|
+
if (!step.promptTemplate) {
|
|
67
|
+
throw new Error(
|
|
68
|
+
`Blueprint step "${step.name}" has no promptTemplate — blueprint validation should have caught this.`,
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
51
72
|
const resolvedPrompt = resolveTemplate(step.promptTemplate, resolvedVars);
|
|
52
73
|
|
|
53
74
|
resolvedSteps.push({
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Render a blueprint into a chat-ready first message.
|
|
3
|
+
*
|
|
4
|
+
* Pure template substitution over `blueprint.chatPrompt` with fallback to
|
|
5
|
+
* `blueprint.steps[0].promptTemplate` for the 13 existing built-ins that
|
|
6
|
+
* predate the `chatPrompt` field. Variable resolution is shared with the
|
|
7
|
+
* workflow engine via `resolveTemplate` so behavior stays consistent.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { WorkflowBlueprint } from "./types";
|
|
11
|
+
import { resolveTemplate } from "./template";
|
|
12
|
+
|
|
13
|
+
export interface RenderedBlueprintPrompt {
|
|
14
|
+
/** The seed message to pre-fill into the chat composer. */
|
|
15
|
+
firstMessage: string;
|
|
16
|
+
/** The conversation title, with variable substitution applied. */
|
|
17
|
+
title: string;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface RenderBlueprintPromptOptions {
|
|
21
|
+
/**
|
|
22
|
+
* Throw if any `{{token}}` references an undefined variable. Defaults to
|
|
23
|
+
* false — the underlying `resolveTemplate` substitutes undefined values with
|
|
24
|
+
* empty strings, which is the right behavior for optional blueprint vars.
|
|
25
|
+
* Set to `true` to validate that all referenced tokens were resolved.
|
|
26
|
+
*/
|
|
27
|
+
strict?: boolean;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export class UnresolvedTokenError extends Error {
|
|
31
|
+
constructor(public readonly tokens: string[]) {
|
|
32
|
+
super(`Unresolved template tokens: ${tokens.join(", ")}`);
|
|
33
|
+
this.name = "UnresolvedTokenError";
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Render a blueprint's chat prompt and title with the provided parameters.
|
|
39
|
+
*
|
|
40
|
+
* Falls back to `steps[0].promptTemplate` if `chatPrompt` is absent. If the
|
|
41
|
+
* blueprint has no steps and no `chatPrompt`, returns an empty first message
|
|
42
|
+
* (the picker UI is expected to validate this case upstream).
|
|
43
|
+
*/
|
|
44
|
+
export function renderBlueprintPrompt(
|
|
45
|
+
blueprint: WorkflowBlueprint,
|
|
46
|
+
params: Record<string, unknown>,
|
|
47
|
+
options: RenderBlueprintPromptOptions = {}
|
|
48
|
+
): RenderedBlueprintPrompt {
|
|
49
|
+
const source =
|
|
50
|
+
blueprint.chatPrompt ?? blueprint.steps[0]?.promptTemplate ?? "";
|
|
51
|
+
|
|
52
|
+
if (options.strict) {
|
|
53
|
+
// Collect {{token}} names from the source (before resolveTemplate
|
|
54
|
+
// substitutes undefined with empty string). Skip `#if`/`/if` directives.
|
|
55
|
+
const unresolved = new Set<string>();
|
|
56
|
+
for (const combined of [source, blueprint.name]) {
|
|
57
|
+
const matches = combined.matchAll(/\{\{(\w+)\}\}/g);
|
|
58
|
+
for (const m of matches) {
|
|
59
|
+
if (!(m[1] in params)) unresolved.add(m[1]);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
if (unresolved.size > 0) {
|
|
63
|
+
throw new UnresolvedTokenError([...unresolved]);
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const firstMessage = resolveTemplate(source, params);
|
|
68
|
+
const title = resolveTemplate(blueprint.name, params);
|
|
69
|
+
|
|
70
|
+
return { firstMessage, title };
|
|
71
|
+
}
|
|
@@ -11,10 +11,18 @@ export interface BlueprintVariable {
|
|
|
11
11
|
max?: number;
|
|
12
12
|
}
|
|
13
13
|
|
|
14
|
+
/**
|
|
15
|
+
* A blueprint step is either a task step (profileId + promptTemplate) OR a
|
|
16
|
+
* delay step (delayDuration only). The XOR is enforced at validation time
|
|
17
|
+
* by BlueprintStepSchema in src/lib/validators/blueprint.ts — at the type
|
|
18
|
+
* level, all three fields are optional so either shape is assignable.
|
|
19
|
+
*/
|
|
14
20
|
export interface BlueprintStep {
|
|
15
21
|
name: string;
|
|
16
|
-
profileId
|
|
17
|
-
promptTemplate
|
|
22
|
+
profileId?: string;
|
|
23
|
+
promptTemplate?: string;
|
|
24
|
+
/** If set, this step is a pure time delay. Format: Nm|Nh|Nd|Nw. */
|
|
25
|
+
delayDuration?: string;
|
|
18
26
|
requiresApproval: boolean;
|
|
19
27
|
expectedOutput?: string;
|
|
20
28
|
condition?: string;
|
|
@@ -35,4 +43,10 @@ export interface WorkflowBlueprint {
|
|
|
35
43
|
estimatedDuration?: string;
|
|
36
44
|
difficulty?: "beginner" | "intermediate" | "advanced";
|
|
37
45
|
isBuiltin?: boolean;
|
|
46
|
+
/**
|
|
47
|
+
* Optional chat-composer seed prompt. When present, `chat-conversation-templates`
|
|
48
|
+
* renders this (with variable substitution) into the first user message of a
|
|
49
|
+
* new conversation. When absent, consumers fall back to `steps[0].promptTemplate`.
|
|
50
|
+
*/
|
|
51
|
+
chatPrompt?: string;
|
|
38
52
|
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Duration parser and formatter for workflow step delays.
|
|
3
|
+
*
|
|
4
|
+
* Format: Nm (minutes), Nh (hours), Nd (days), Nw (weeks).
|
|
5
|
+
* Bounds: minimum 1 minute, maximum 30 days.
|
|
6
|
+
* Compound formats (e.g. "3d2h") are not supported.
|
|
7
|
+
*
|
|
8
|
+
* See features/workflow-step-delays.md for the spec.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const MS_PER_MINUTE = 60_000;
|
|
12
|
+
const MS_PER_HOUR = 60 * MS_PER_MINUTE;
|
|
13
|
+
const MS_PER_DAY = 24 * MS_PER_HOUR;
|
|
14
|
+
const MS_PER_WEEK = 7 * MS_PER_DAY;
|
|
15
|
+
|
|
16
|
+
const MIN_DURATION_MS = MS_PER_MINUTE;
|
|
17
|
+
const MAX_DURATION_MS = 30 * MS_PER_DAY;
|
|
18
|
+
|
|
19
|
+
const DURATION_PATTERN = /^(\d+)(m|h|d|w)$/;
|
|
20
|
+
|
|
21
|
+
const UNIT_MS: Record<string, number> = {
|
|
22
|
+
m: MS_PER_MINUTE,
|
|
23
|
+
h: MS_PER_HOUR,
|
|
24
|
+
d: MS_PER_DAY,
|
|
25
|
+
w: MS_PER_WEEK,
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse a duration string into milliseconds.
|
|
30
|
+
*
|
|
31
|
+
* @throws if the format is invalid or the value is outside bounds.
|
|
32
|
+
*/
|
|
33
|
+
export function parseDuration(input: string): number {
|
|
34
|
+
const match = input.match(DURATION_PATTERN);
|
|
35
|
+
if (!match) {
|
|
36
|
+
throw new Error(
|
|
37
|
+
`Invalid duration: "${input}". Use format: 30m, 2h, 3d, 1w`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
const value = Number.parseInt(match[1], 10);
|
|
42
|
+
const unit = match[2];
|
|
43
|
+
const ms = value * UNIT_MS[unit];
|
|
44
|
+
|
|
45
|
+
if (ms < MIN_DURATION_MS) {
|
|
46
|
+
throw new Error(`Duration below minimum: "${input}". Minimum is 1 minute (1m).`);
|
|
47
|
+
}
|
|
48
|
+
if (ms > MAX_DURATION_MS) {
|
|
49
|
+
throw new Error(`Duration above maximum: "${input}". Maximum is 30 days (30d).`);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
return ms;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Result of checking whether a workflow step is a delay step or a task step.
|
|
57
|
+
* The engine branches on this — delay steps pause the workflow, task steps
|
|
58
|
+
* execute normally.
|
|
59
|
+
*/
|
|
60
|
+
export type DelayCheck =
|
|
61
|
+
| { type: "task" }
|
|
62
|
+
| { type: "delay"; resumeAt: number };
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Classify a workflow step as either a delay step or a task step, and compute
|
|
66
|
+
* the resume timestamp for delay steps.
|
|
67
|
+
*
|
|
68
|
+
* Pure function: no I/O, no side effects. The engine calls this inside the
|
|
69
|
+
* sequence executor loop and branches on the result. Invalid duration formats
|
|
70
|
+
* throw — blueprint validation (src/lib/validators/blueprint.ts) should catch
|
|
71
|
+
* these at the workflow-creation boundary, so any invalid duration reaching
|
|
72
|
+
* here is a programming error and must fail loudly.
|
|
73
|
+
*
|
|
74
|
+
* @param step Workflow step (any object with an optional delayDuration field)
|
|
75
|
+
* @param now Current epoch timestamp in milliseconds (injected for testability)
|
|
76
|
+
*/
|
|
77
|
+
export function checkDelayStep(
|
|
78
|
+
step: { delayDuration?: string },
|
|
79
|
+
now: number,
|
|
80
|
+
): DelayCheck {
|
|
81
|
+
if (!step.delayDuration) {
|
|
82
|
+
return { type: "task" };
|
|
83
|
+
}
|
|
84
|
+
return {
|
|
85
|
+
type: "delay",
|
|
86
|
+
resumeAt: now + parseDuration(step.delayDuration),
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Format a millisecond duration back into the canonical string form.
|
|
92
|
+
* Prefers the largest unit that divides cleanly; falls back to minutes
|
|
93
|
+
* when no larger unit divides evenly.
|
|
94
|
+
*/
|
|
95
|
+
export function formatDuration(ms: number): string {
|
|
96
|
+
if (ms >= MS_PER_WEEK && ms % MS_PER_WEEK === 0) {
|
|
97
|
+
return `${ms / MS_PER_WEEK}w`;
|
|
98
|
+
}
|
|
99
|
+
if (ms >= MS_PER_DAY && ms % MS_PER_DAY === 0) {
|
|
100
|
+
return `${ms / MS_PER_DAY}d`;
|
|
101
|
+
}
|
|
102
|
+
if (ms >= MS_PER_HOUR && ms % MS_PER_HOUR === 0) {
|
|
103
|
+
return `${ms / MS_PER_HOUR}h`;
|
|
104
|
+
}
|
|
105
|
+
return `${ms / MS_PER_MINUTE}m`;
|
|
106
|
+
}
|