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
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import { Suspense } from "react";
|
|
2
2
|
import { PageShell } from "@/components/shared/page-shell";
|
|
3
3
|
import { AnalyticsDashboard } from "@/components/analytics/analytics-dashboard";
|
|
4
|
-
import { AnalyticsGateCard } from "@/components/analytics/analytics-gate-card";
|
|
5
|
-
import { licenseManager } from "@/lib/license/manager";
|
|
6
4
|
import {
|
|
7
5
|
getOutcomeCounts,
|
|
8
6
|
getSuccessRateTrend,
|
|
@@ -14,16 +12,13 @@ import {
|
|
|
14
12
|
export const dynamic = "force-dynamic";
|
|
15
13
|
|
|
16
14
|
function AnalyticsContent() {
|
|
17
|
-
const tier = licenseManager.getTierFromDb();
|
|
18
|
-
const isAllowed = tier !== "community";
|
|
19
|
-
|
|
20
15
|
const outcomes = getOutcomeCounts(30);
|
|
21
16
|
const successTrend = getSuccessRateTrend(30);
|
|
22
17
|
const costTrend = getCostPerOutcomeTrend(30);
|
|
23
18
|
const leaderboard = getProfileLeaderboard(30);
|
|
24
19
|
const hoursSaved = getEstimatedHoursSaved(30);
|
|
25
20
|
|
|
26
|
-
|
|
21
|
+
return (
|
|
27
22
|
<AnalyticsDashboard
|
|
28
23
|
outcomes={outcomes}
|
|
29
24
|
successTrend={successTrend}
|
|
@@ -32,21 +27,6 @@ function AnalyticsContent() {
|
|
|
32
27
|
hoursSaved={hoursSaved}
|
|
33
28
|
/>
|
|
34
29
|
);
|
|
35
|
-
|
|
36
|
-
if (isAllowed) return dashboard;
|
|
37
|
-
|
|
38
|
-
return (
|
|
39
|
-
<div className="relative">
|
|
40
|
-
{/* Blurred dashboard preview */}
|
|
41
|
-
<div className="opacity-20 pointer-events-none select-none blur-[2px]" aria-hidden>
|
|
42
|
-
{dashboard}
|
|
43
|
-
</div>
|
|
44
|
-
{/* Upgrade CTA */}
|
|
45
|
-
<div className="absolute inset-0 flex items-start justify-center pt-16">
|
|
46
|
-
<AnalyticsGateCard />
|
|
47
|
-
</div>
|
|
48
|
-
</div>
|
|
49
|
-
);
|
|
50
30
|
}
|
|
51
31
|
|
|
52
32
|
export default function AnalyticsPage() {
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { NextRequest, NextResponse } from "next/server";
|
|
2
2
|
import { getConversation, getMessages } from "@/lib/data/chat";
|
|
3
3
|
import { sendMessage } from "@/lib/chat/engine";
|
|
4
|
+
import { recordTermination } from "@/lib/chat/stream-telemetry";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* GET /api/chat/conversations/[id]/messages?after=xxx&limit=100
|
|
@@ -58,6 +59,7 @@ export async function POST(
|
|
|
58
59
|
|
|
59
60
|
// Bridge the async generator to an SSE ReadableStream
|
|
60
61
|
const encoder = new TextEncoder();
|
|
62
|
+
const streamStartedAt = Date.now();
|
|
61
63
|
const stream = new ReadableStream({
|
|
62
64
|
async start(controller) {
|
|
63
65
|
const keepalive = setInterval(() => {
|
|
@@ -94,9 +96,28 @@ export async function POST(
|
|
|
94
96
|
);
|
|
95
97
|
} finally {
|
|
96
98
|
clearInterval(keepalive);
|
|
97
|
-
|
|
99
|
+
try {
|
|
100
|
+
controller.close();
|
|
101
|
+
} catch {
|
|
102
|
+
// Stream may already be closed by peer; safe to ignore
|
|
103
|
+
}
|
|
98
104
|
}
|
|
99
105
|
},
|
|
106
|
+
// Fires when the client disconnects mid-stream (browser tab closed,
|
|
107
|
+
// user navigated away, AbortController.abort() fired on the fetch).
|
|
108
|
+
// The engine's own `req.signal` abort already records
|
|
109
|
+
// `stream.aborted.signal` in its catch path — this cancel callback
|
|
110
|
+
// only fires when the ReadableStream is torn down independently,
|
|
111
|
+
// so record it as a distinct `stream.aborted.client` code.
|
|
112
|
+
cancel(reason) {
|
|
113
|
+
recordTermination({
|
|
114
|
+
reason: "stream.aborted.client",
|
|
115
|
+
conversationId: id,
|
|
116
|
+
messageId: null,
|
|
117
|
+
durationMs: Date.now() - streamStartedAt,
|
|
118
|
+
error: reason ? String(reason).slice(0, 200) : undefined,
|
|
119
|
+
});
|
|
120
|
+
},
|
|
100
121
|
});
|
|
101
122
|
|
|
102
123
|
return new Response(stream, {
|
|
@@ -0,0 +1,141 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
import { NextRequest } from "next/server";
|
|
3
|
+
|
|
4
|
+
// ---------------------------------------------------------------------------
|
|
5
|
+
// Mocked service module — controls return values for each test.
|
|
6
|
+
// ---------------------------------------------------------------------------
|
|
7
|
+
const mockActivateSkill = vi.fn();
|
|
8
|
+
|
|
9
|
+
vi.mock("@/lib/chat/skill-composition", () => ({
|
|
10
|
+
activateSkill: (...args: unknown[]) => mockActivateSkill(...args),
|
|
11
|
+
}));
|
|
12
|
+
|
|
13
|
+
// Import the route handler AFTER the mock is set up.
|
|
14
|
+
import { POST } from "../activate/route";
|
|
15
|
+
|
|
16
|
+
// ---------------------------------------------------------------------------
|
|
17
|
+
// Helpers
|
|
18
|
+
// ---------------------------------------------------------------------------
|
|
19
|
+
function makeRequest(body: unknown, conversationId = "conv-1"): [NextRequest, { params: Promise<{ id: string }> }] {
|
|
20
|
+
const req = new NextRequest("http://localhost/api/chat/conversations/conv-1/skills/activate", {
|
|
21
|
+
method: "POST",
|
|
22
|
+
headers: { "Content-Type": "application/json" },
|
|
23
|
+
body: JSON.stringify(body),
|
|
24
|
+
});
|
|
25
|
+
return [req, { params: Promise.resolve({ id: conversationId }) }];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
vi.resetAllMocks();
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
// Tests
|
|
34
|
+
// ---------------------------------------------------------------------------
|
|
35
|
+
describe("POST /api/chat/conversations/[id]/skills/activate", () => {
|
|
36
|
+
it("returns 400 for invalid JSON", async () => {
|
|
37
|
+
const req = new NextRequest("http://localhost/...", {
|
|
38
|
+
method: "POST",
|
|
39
|
+
headers: { "Content-Type": "application/json" },
|
|
40
|
+
body: "not-json",
|
|
41
|
+
});
|
|
42
|
+
const res = await POST(req, { params: Promise.resolve({ id: "conv-1" }) });
|
|
43
|
+
expect(res.status).toBe(400);
|
|
44
|
+
const json = await res.json() as Record<string, unknown>;
|
|
45
|
+
expect(typeof json.error).toBe("string");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("returns 400 when skillId is missing", async () => {
|
|
49
|
+
const [req, ctx] = makeRequest({ mode: "replace" });
|
|
50
|
+
const res = await POST(req, ctx);
|
|
51
|
+
expect(res.status).toBe(400);
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
it("returns 200 with activation payload on success (replace mode)", async () => {
|
|
55
|
+
mockActivateSkill.mockResolvedValueOnce({
|
|
56
|
+
kind: "ok",
|
|
57
|
+
activatedSkillId: "my-skill",
|
|
58
|
+
activeSkillIds: ["my-skill"],
|
|
59
|
+
skillName: "My Skill",
|
|
60
|
+
});
|
|
61
|
+
const [req, ctx] = makeRequest({ skillId: "my-skill" });
|
|
62
|
+
const res = await POST(req, ctx);
|
|
63
|
+
expect(res.status).toBe(200);
|
|
64
|
+
const json = await res.json() as Record<string, unknown>;
|
|
65
|
+
expect(json.activatedSkillId).toBe("my-skill");
|
|
66
|
+
expect(json.activeSkillIds).toEqual(["my-skill"]);
|
|
67
|
+
expect(json.skillName).toBe("My Skill");
|
|
68
|
+
expect(mockActivateSkill).toHaveBeenCalledWith({
|
|
69
|
+
conversationId: "conv-1",
|
|
70
|
+
skillId: "my-skill",
|
|
71
|
+
mode: "replace",
|
|
72
|
+
force: false,
|
|
73
|
+
});
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("returns 200 with requiresConfirmation when conflicts detected", async () => {
|
|
77
|
+
mockActivateSkill.mockResolvedValueOnce({
|
|
78
|
+
kind: "conflicts",
|
|
79
|
+
activeSkillIds: ["first"],
|
|
80
|
+
conflicts: [
|
|
81
|
+
{ skillA: "first", skillB: "second", sharedTopic: "tests", excerptA: "Always …", excerptB: "Never …" },
|
|
82
|
+
],
|
|
83
|
+
hint: "Re-call with force=true to add anyway",
|
|
84
|
+
});
|
|
85
|
+
const [req, ctx] = makeRequest({ skillId: "second", mode: "add" });
|
|
86
|
+
const res = await POST(req, ctx);
|
|
87
|
+
expect(res.status).toBe(200);
|
|
88
|
+
const json = await res.json() as Record<string, unknown>;
|
|
89
|
+
expect(json.requiresConfirmation).toBe(true);
|
|
90
|
+
expect(Array.isArray(json.conflicts)).toBe(true);
|
|
91
|
+
expect((json.conflicts as unknown[]).length).toBe(1);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("returns 404 when conversation is not found", async () => {
|
|
95
|
+
mockActivateSkill.mockResolvedValueOnce({
|
|
96
|
+
kind: "error",
|
|
97
|
+
message: "Conversation not found: ghost",
|
|
98
|
+
});
|
|
99
|
+
const [req, ctx] = makeRequest({ skillId: "any-skill" }, "ghost");
|
|
100
|
+
const res = await POST(req, ctx);
|
|
101
|
+
expect(res.status).toBe(404);
|
|
102
|
+
const json = await res.json() as Record<string, unknown>;
|
|
103
|
+
expect((json.error as string)).toContain("Conversation not found");
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it("returns 404 when skill is not found", async () => {
|
|
107
|
+
mockActivateSkill.mockResolvedValueOnce({
|
|
108
|
+
kind: "error",
|
|
109
|
+
message: "Skill not found: no-such-skill",
|
|
110
|
+
});
|
|
111
|
+
const [req, ctx] = makeRequest({ skillId: "no-such-skill" });
|
|
112
|
+
const res = await POST(req, ctx);
|
|
113
|
+
expect(res.status).toBe(404);
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("returns 400 for other logic errors (e.g. max skills reached)", async () => {
|
|
117
|
+
mockActivateSkill.mockResolvedValueOnce({
|
|
118
|
+
kind: "error",
|
|
119
|
+
message: "Max active skills (3) reached on 'claude-code' — deactivate one first",
|
|
120
|
+
});
|
|
121
|
+
const [req, ctx] = makeRequest({ skillId: "any", mode: "add", force: true });
|
|
122
|
+
const res = await POST(req, ctx);
|
|
123
|
+
expect(res.status).toBe(400);
|
|
124
|
+
const json = await res.json() as Record<string, unknown>;
|
|
125
|
+
expect((json.error as string)).toMatch(/max active skills/i);
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it("passes force=true to the service", async () => {
|
|
129
|
+
mockActivateSkill.mockResolvedValueOnce({
|
|
130
|
+
kind: "ok",
|
|
131
|
+
activatedSkillId: "any",
|
|
132
|
+
activeSkillIds: ["first", "any"],
|
|
133
|
+
skillName: "Any",
|
|
134
|
+
});
|
|
135
|
+
const [req, ctx] = makeRequest({ skillId: "any", mode: "add", force: true });
|
|
136
|
+
await POST(req, ctx);
|
|
137
|
+
expect(mockActivateSkill).toHaveBeenCalledWith(
|
|
138
|
+
expect.objectContaining({ force: true, mode: "add" })
|
|
139
|
+
);
|
|
140
|
+
});
|
|
141
|
+
});
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { activateSkill } from "@/lib/chat/skill-composition";
|
|
4
|
+
|
|
5
|
+
const ActivateBody = z.object({
|
|
6
|
+
skillId: z.string().min(1),
|
|
7
|
+
mode: z.enum(["replace", "add"]).optional().default("replace"),
|
|
8
|
+
force: z.boolean().optional().default(false),
|
|
9
|
+
});
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* POST /api/chat/conversations/[id]/skills/activate
|
|
13
|
+
*
|
|
14
|
+
* Thin HTTP wrapper over the activateSkill composition service so the chat UI
|
|
15
|
+
* can reach composition logic without going through MCP.
|
|
16
|
+
*
|
|
17
|
+
* Returns:
|
|
18
|
+
* 200 { activatedSkillId, activeSkillIds, skillName } — success
|
|
19
|
+
* 200 { requiresConfirmation: true, conflicts: [...] } — needs confirm
|
|
20
|
+
* 400 { error: string } — validation / logic error
|
|
21
|
+
* 404 { error: string } — conversation not found
|
|
22
|
+
*
|
|
23
|
+
* See `features/chat-composition-ui-v1.md`.
|
|
24
|
+
*/
|
|
25
|
+
export async function POST(
|
|
26
|
+
req: NextRequest,
|
|
27
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
28
|
+
) {
|
|
29
|
+
const { id: conversationId } = await params;
|
|
30
|
+
|
|
31
|
+
let body: unknown;
|
|
32
|
+
try {
|
|
33
|
+
body = await req.json();
|
|
34
|
+
} catch {
|
|
35
|
+
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
const parsed = ActivateBody.safeParse(body);
|
|
39
|
+
if (!parsed.success) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ error: parsed.error.issues.map((i) => i.message).join("; ") },
|
|
42
|
+
{ status: 400 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const { skillId, mode, force } = parsed.data;
|
|
47
|
+
const result = await activateSkill({ conversationId, skillId, mode, force });
|
|
48
|
+
|
|
49
|
+
if (result.kind === "error") {
|
|
50
|
+
const isNotFound =
|
|
51
|
+
result.message.startsWith("Conversation not found") ||
|
|
52
|
+
result.message.startsWith("Skill not found");
|
|
53
|
+
return NextResponse.json(
|
|
54
|
+
{ error: result.message },
|
|
55
|
+
{ status: isNotFound ? 404 : 400 }
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (result.kind === "conflicts") {
|
|
60
|
+
return NextResponse.json({
|
|
61
|
+
requiresConfirmation: true,
|
|
62
|
+
conflicts: result.conflicts,
|
|
63
|
+
hint: result.hint,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// kind === "ok"
|
|
68
|
+
return NextResponse.json({
|
|
69
|
+
activatedSkillId: result.activatedSkillId,
|
|
70
|
+
activeSkillIds: result.activeSkillIds,
|
|
71
|
+
skillName: result.skillName,
|
|
72
|
+
...(result.note ? { note: result.note } : {}),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { deactivateSkill } from "@/lib/chat/skill-composition";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* POST /api/chat/conversations/[id]/skills/deactivate
|
|
6
|
+
*
|
|
7
|
+
* Clears both activeSkillId and activeSkillIds on the conversation row.
|
|
8
|
+
* Idempotent — safe to call when no skill is active.
|
|
9
|
+
*
|
|
10
|
+
* Returns:
|
|
11
|
+
* 200 { previousSkillId: string | null } — success
|
|
12
|
+
* 404 { error: string } — conversation not found
|
|
13
|
+
*
|
|
14
|
+
* See `features/chat-composition-ui-v1.md`.
|
|
15
|
+
*/
|
|
16
|
+
export async function POST(
|
|
17
|
+
_req: NextRequest,
|
|
18
|
+
{ params }: { params: Promise<{ id: string }> }
|
|
19
|
+
) {
|
|
20
|
+
const { id: conversationId } = await params;
|
|
21
|
+
|
|
22
|
+
const result = await deactivateSkill({ conversationId });
|
|
23
|
+
|
|
24
|
+
if (result.kind === "error") {
|
|
25
|
+
const isNotFound = result.message.startsWith("Conversation not found");
|
|
26
|
+
return NextResponse.json(
|
|
27
|
+
{ error: result.message },
|
|
28
|
+
{ status: isNotFound ? 404 : 400 }
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
return NextResponse.json({ previousSkillId: result.previousSkillId });
|
|
33
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { randomUUID } from "node:crypto";
|
|
3
|
+
import { homedir } from "node:os";
|
|
4
|
+
import path from "node:path";
|
|
5
|
+
import { mkdir, writeFile } from "node:fs/promises";
|
|
6
|
+
import { z } from "zod";
|
|
7
|
+
import { db } from "@/lib/db";
|
|
8
|
+
import { documents } from "@/lib/db/schema";
|
|
9
|
+
|
|
10
|
+
const bodySchema = z.object({
|
|
11
|
+
title: z.string().min(1).max(200),
|
|
12
|
+
markdown: z.string().min(1),
|
|
13
|
+
conversationId: z.string().nullable().optional(),
|
|
14
|
+
});
|
|
15
|
+
|
|
16
|
+
export async function POST(req: NextRequest) {
|
|
17
|
+
const raw = await req.json().catch(() => null);
|
|
18
|
+
const parsed = bodySchema.safeParse(raw);
|
|
19
|
+
if (!parsed.success) {
|
|
20
|
+
return NextResponse.json(
|
|
21
|
+
{ error: "Invalid body", details: parsed.error.issues },
|
|
22
|
+
{ status: 400 }
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
const { title, markdown, conversationId } = parsed.data;
|
|
27
|
+
const id = randomUUID();
|
|
28
|
+
const safeName = title.replace(/[^a-z0-9-_\. ]/gi, "_").slice(0, 80);
|
|
29
|
+
const filename = `${Date.now()}-${safeName}.md`;
|
|
30
|
+
const dir = path.join(homedir(), ".stagent", "uploads", "chat-exports");
|
|
31
|
+
await mkdir(dir, { recursive: true });
|
|
32
|
+
const storagePath = path.join(dir, filename);
|
|
33
|
+
await writeFile(storagePath, markdown, "utf8");
|
|
34
|
+
|
|
35
|
+
const now = new Date();
|
|
36
|
+
await db.insert(documents).values({
|
|
37
|
+
id,
|
|
38
|
+
filename,
|
|
39
|
+
originalName: `${safeName}.md`,
|
|
40
|
+
mimeType: "text/markdown",
|
|
41
|
+
size: Buffer.byteLength(markdown, "utf8"),
|
|
42
|
+
storagePath,
|
|
43
|
+
direction: "output",
|
|
44
|
+
status: "ready",
|
|
45
|
+
source: "chat-export",
|
|
46
|
+
conversationId: conversationId ?? null,
|
|
47
|
+
createdAt: now,
|
|
48
|
+
updatedAt: now,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
return NextResponse.json({ id, filename }, { status: 201 });
|
|
52
|
+
}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { eq } from "drizzle-orm";
|
|
3
|
+
import { db } from "@/lib/db";
|
|
4
|
+
import { projects } from "@/lib/db/schema";
|
|
5
|
+
import { getLaunchCwd } from "@/lib/environment/workspace-context";
|
|
6
|
+
import { searchFiles } from "@/lib/chat/files/search";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/chat/files/search?q=&projectId=&limit=20
|
|
10
|
+
*
|
|
11
|
+
* Returns files under the active project's workingDirectory (if a valid
|
|
12
|
+
* projectId is supplied), else under the stagent launch cwd. The client
|
|
13
|
+
* never supplies cwd directly — that would let a hostile prompt or XSS
|
|
14
|
+
* reach arbitrary paths on disk.
|
|
15
|
+
*
|
|
16
|
+
* Results respect `.gitignore` via `git ls-files --exclude-standard`.
|
|
17
|
+
*/
|
|
18
|
+
export async function GET(req: NextRequest) {
|
|
19
|
+
const { searchParams } = new URL(req.url);
|
|
20
|
+
const q = searchParams.get("q") ?? "";
|
|
21
|
+
|
|
22
|
+
const limitRaw = parseInt(searchParams.get("limit") ?? "20", 10);
|
|
23
|
+
const limit = Number.isFinite(limitRaw)
|
|
24
|
+
? Math.max(1, Math.min(50, limitRaw))
|
|
25
|
+
: 20;
|
|
26
|
+
|
|
27
|
+
const projectId = searchParams.get("projectId");
|
|
28
|
+
|
|
29
|
+
let cwd = getLaunchCwd();
|
|
30
|
+
if (projectId) {
|
|
31
|
+
const project = await db
|
|
32
|
+
.select({ workingDirectory: projects.workingDirectory })
|
|
33
|
+
.from(projects)
|
|
34
|
+
.where(eq(projects.id, projectId))
|
|
35
|
+
.get();
|
|
36
|
+
if (project?.workingDirectory) {
|
|
37
|
+
cwd = project.workingDirectory;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
try {
|
|
42
|
+
const results = searchFiles(cwd, q, limit);
|
|
43
|
+
return NextResponse.json({ results });
|
|
44
|
+
} catch (e) {
|
|
45
|
+
return NextResponse.json(
|
|
46
|
+
{ error: e instanceof Error ? e.message : "file search failed" },
|
|
47
|
+
{ status: 500 }
|
|
48
|
+
);
|
|
49
|
+
}
|
|
50
|
+
}
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
readTerminations,
|
|
4
|
+
countTerminations,
|
|
5
|
+
} from "@/lib/chat/stream-telemetry";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* GET /api/diagnostics/chat-streams
|
|
9
|
+
*
|
|
10
|
+
* Dev-only diagnostics endpoint that reports how chat SSE streams have
|
|
11
|
+
* terminated in the current server process. Returns counts by reason code
|
|
12
|
+
* plus the most recent N events.
|
|
13
|
+
*
|
|
14
|
+
* Query params:
|
|
15
|
+
* ?windowMinutes=N — restrict counts to the last N minutes (default: all)
|
|
16
|
+
* ?limit=N — cap on recent events returned (default: 50, max: 500)
|
|
17
|
+
*
|
|
18
|
+
* Response shape:
|
|
19
|
+
* {
|
|
20
|
+
* windowMinutes: number | null,
|
|
21
|
+
* totalEvents: number,
|
|
22
|
+
* counts: Record<TerminationReason, number>,
|
|
23
|
+
* recent: TerminationEvent[]
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* Gated behind NODE_ENV !== production to match the data/clear and
|
|
27
|
+
* data/seed routes. This is for maintainer inspection, not end users.
|
|
28
|
+
*
|
|
29
|
+
* See src/lib/chat/stream-telemetry.ts for the ring buffer + reason code
|
|
30
|
+
* definitions. See features/chat-stream-resilience-telemetry.md for the
|
|
31
|
+
* motivation (verify-before-building telemetry for a mid-stream refresh
|
|
32
|
+
* bug reported by a sibling repo that doesn't reproduce here).
|
|
33
|
+
*/
|
|
34
|
+
export async function GET(req: NextRequest) {
|
|
35
|
+
if (process.env.NODE_ENV === "production") {
|
|
36
|
+
return NextResponse.json(
|
|
37
|
+
{ error: "Diagnostics disabled in production" },
|
|
38
|
+
{ status: 403 }
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const { searchParams } = req.nextUrl;
|
|
43
|
+
const windowMinutesRaw = searchParams.get("windowMinutes");
|
|
44
|
+
const limitRaw = searchParams.get("limit");
|
|
45
|
+
|
|
46
|
+
const windowMinutes =
|
|
47
|
+
windowMinutesRaw !== null ? Math.max(0, parseInt(windowMinutesRaw, 10) || 0) : null;
|
|
48
|
+
const windowMs = windowMinutes !== null ? windowMinutes * 60 * 1000 : 0;
|
|
49
|
+
|
|
50
|
+
const limit = Math.min(
|
|
51
|
+
500,
|
|
52
|
+
limitRaw !== null ? Math.max(1, parseInt(limitRaw, 10) || 50) : 50
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
const all = readTerminations();
|
|
56
|
+
const recent = all.slice(-limit).reverse(); // newest first
|
|
57
|
+
const counts = countTerminations(windowMs);
|
|
58
|
+
|
|
59
|
+
return NextResponse.json({
|
|
60
|
+
windowMinutes,
|
|
61
|
+
totalEvents: all.length,
|
|
62
|
+
counts,
|
|
63
|
+
recent,
|
|
64
|
+
});
|
|
65
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { describe, it, expect, vi, beforeEach } from "vitest";
|
|
2
|
+
|
|
3
|
+
vi.mock("@/lib/environment/auto-scan", () => ({
|
|
4
|
+
shouldRescan: vi.fn(),
|
|
5
|
+
ensureFreshScan: vi.fn(() => ({ scannedAt: new Date() })),
|
|
6
|
+
}));
|
|
7
|
+
|
|
8
|
+
vi.mock("@/lib/environment/workspace-context", () => ({
|
|
9
|
+
getLaunchCwd: () => "/tmp/project",
|
|
10
|
+
}));
|
|
11
|
+
|
|
12
|
+
import { POST } from "../route";
|
|
13
|
+
import * as autoScan from "@/lib/environment/auto-scan";
|
|
14
|
+
|
|
15
|
+
describe("POST /api/environment/rescan-if-stale", () => {
|
|
16
|
+
beforeEach(() => {
|
|
17
|
+
vi.clearAllMocks();
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("returns scanned:true when shouldRescan=true", async () => {
|
|
21
|
+
(autoScan.shouldRescan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
22
|
+
const res = await POST();
|
|
23
|
+
expect(res.status).toBe(200);
|
|
24
|
+
const json = await res.json();
|
|
25
|
+
expect(json.scanned).toBe(true);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it("returns scanned:false when not stale", async () => {
|
|
29
|
+
(autoScan.shouldRescan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(false);
|
|
30
|
+
const res = await POST();
|
|
31
|
+
const json = await res.json();
|
|
32
|
+
expect(json.scanned).toBe(false);
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("returns scanned:false and logs when ensureFreshScan throws", async () => {
|
|
36
|
+
(autoScan.shouldRescan as unknown as ReturnType<typeof vi.fn>).mockReturnValue(true);
|
|
37
|
+
(autoScan.ensureFreshScan as unknown as ReturnType<typeof vi.fn>).mockImplementation(() => {
|
|
38
|
+
throw new Error("fs error");
|
|
39
|
+
});
|
|
40
|
+
const res = await POST();
|
|
41
|
+
const json = await res.json();
|
|
42
|
+
// ensureFreshScan itself swallows errors — but if it re-threw we must not 500
|
|
43
|
+
expect(res.status).toBe(200);
|
|
44
|
+
});
|
|
45
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { shouldRescan, ensureFreshScan } from "@/lib/environment/auto-scan";
|
|
3
|
+
import { getLaunchCwd } from "@/lib/environment/workspace-context";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Fire-and-forget rescan endpoint for chat session activation.
|
|
7
|
+
* - If last scan is fresh (<5min), returns `{ scanned: false }` without I/O.
|
|
8
|
+
* - Otherwise, runs a scan via `ensureFreshScan` (which catches errors and
|
|
9
|
+
* returns null on failure), returns `{ scanned: true }`.
|
|
10
|
+
* - Never 500s; the chat UI must not be blocked by env issues.
|
|
11
|
+
*/
|
|
12
|
+
export async function POST() {
|
|
13
|
+
if (!shouldRescan()) {
|
|
14
|
+
return NextResponse.json({ scanned: false });
|
|
15
|
+
}
|
|
16
|
+
try {
|
|
17
|
+
ensureFreshScan(getLaunchCwd());
|
|
18
|
+
} catch (err) {
|
|
19
|
+
// ensureFreshScan itself catches internally; this is belt + suspenders.
|
|
20
|
+
console.warn("[rescan-if-stale] unexpected:", err);
|
|
21
|
+
}
|
|
22
|
+
return NextResponse.json({ scanned: true });
|
|
23
|
+
}
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { listSkillsEnriched } from "@/lib/environment/list-skills";
|
|
3
|
+
|
|
4
|
+
export async function GET() {
|
|
5
|
+
try {
|
|
6
|
+
return NextResponse.json(listSkillsEnriched());
|
|
7
|
+
} catch (err) {
|
|
8
|
+
return NextResponse.json(
|
|
9
|
+
{ error: err instanceof Error ? err.message : "scan failed" },
|
|
10
|
+
{ status: 500 }
|
|
11
|
+
);
|
|
12
|
+
}
|
|
13
|
+
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import {
|
|
3
|
+
getInstanceConfig,
|
|
4
|
+
getGuardrails,
|
|
5
|
+
getUpgradeState,
|
|
6
|
+
} from "@/lib/instance/settings";
|
|
7
|
+
import { isDevMode } from "@/lib/instance/detect";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /api/instance/config
|
|
11
|
+
*
|
|
12
|
+
* Returns the full instance state: config, guardrails, and upgrade state
|
|
13
|
+
* in a single response. Used by the Settings → Instance section and by
|
|
14
|
+
* the upgrade pre-flight modal.
|
|
15
|
+
*
|
|
16
|
+
* When running on the canonical dev repo (STAGENT_DEV_MODE=true or the
|
|
17
|
+
* .git/stagent-dev-mode sentinel), returns `{ devMode: true }` with null
|
|
18
|
+
* payloads. This prevents stale instance rows written during prior testing
|
|
19
|
+
* from surfacing in the UI as if the dev repo were a real instance.
|
|
20
|
+
*/
|
|
21
|
+
export async function GET() {
|
|
22
|
+
try {
|
|
23
|
+
if (isDevMode()) {
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
devMode: true,
|
|
26
|
+
config: null,
|
|
27
|
+
guardrails: null,
|
|
28
|
+
upgrade: null,
|
|
29
|
+
});
|
|
30
|
+
}
|
|
31
|
+
return NextResponse.json({
|
|
32
|
+
devMode: false,
|
|
33
|
+
config: getInstanceConfig(),
|
|
34
|
+
guardrails: getGuardrails(),
|
|
35
|
+
upgrade: getUpgradeState(),
|
|
36
|
+
});
|
|
37
|
+
} catch (err) {
|
|
38
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
39
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
40
|
+
}
|
|
41
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { ensureInstance } from "@/lib/instance/bootstrap";
|
|
3
|
+
import {
|
|
4
|
+
getInstanceConfig,
|
|
5
|
+
getGuardrails,
|
|
6
|
+
getUpgradeState,
|
|
7
|
+
} from "@/lib/instance/settings";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/instance/init
|
|
11
|
+
*
|
|
12
|
+
* Idempotent manual re-run of the instance bootstrap. Useful when the
|
|
13
|
+
* initial boot-time run failed (permission error, git not installed),
|
|
14
|
+
* or when the user wants to re-apply guardrails after changing consent
|
|
15
|
+
* via the Settings → Instance UI.
|
|
16
|
+
*
|
|
17
|
+
* Returns the current instance config + guardrails + upgrade state after
|
|
18
|
+
* the re-run so the Settings → Instance section can refresh its display
|
|
19
|
+
* without a second request.
|
|
20
|
+
*/
|
|
21
|
+
export async function POST() {
|
|
22
|
+
try {
|
|
23
|
+
const result = await ensureInstance();
|
|
24
|
+
return NextResponse.json({
|
|
25
|
+
ensureResult: result,
|
|
26
|
+
config: getInstanceConfig(),
|
|
27
|
+
guardrails: getGuardrails(),
|
|
28
|
+
upgrade: getUpgradeState(),
|
|
29
|
+
});
|
|
30
|
+
} catch (err) {
|
|
31
|
+
const message = err instanceof Error ? err.message : String(err);
|
|
32
|
+
return NextResponse.json({ error: message }, { status: 500 });
|
|
33
|
+
}
|
|
34
|
+
}
|