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.
Files changed (396) hide show
  1. package/README.md +20 -44
  2. package/dist/cli.js +66 -18
  3. package/docs/.coverage-gaps.json +144 -56
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/agent-intelligence.md +12 -2
  6. package/docs/features/chat.md +40 -5
  7. package/docs/features/cost-usage.md +1 -1
  8. package/docs/features/documents.md +5 -2
  9. package/docs/features/inbox-notifications.md +10 -2
  10. package/docs/features/keyboard-navigation.md +12 -3
  11. package/docs/features/provider-runtimes.md +20 -2
  12. package/docs/features/schedules.md +32 -4
  13. package/docs/features/settings.md +28 -5
  14. package/docs/features/shared-components.md +7 -3
  15. package/docs/features/tables.md +11 -2
  16. package/docs/features/tool-permissions.md +6 -2
  17. package/docs/features/workflows.md +14 -4
  18. package/docs/index.md +1 -1
  19. package/docs/journeys/developer.md +39 -2
  20. package/docs/journeys/personal-use.md +32 -8
  21. package/docs/journeys/power-user.md +45 -14
  22. package/docs/journeys/work-use.md +17 -8
  23. package/docs/manifest.json +15 -15
  24. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  25. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  26. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  27. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  28. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  29. package/docs/superpowers/plans/2026-04-14-chat-command-namespace-refactor.md +1390 -0
  30. package/docs/superpowers/plans/2026-04-14-chat-environment-integration.md +1561 -0
  31. package/docs/superpowers/plans/2026-04-14-chat-polish-bundle-v1.md +1219 -0
  32. package/docs/superpowers/plans/2026-04-14-chat-session-persistence-provider-closeout.md +399 -0
  33. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  34. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  35. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  36. package/next.config.mjs +1 -0
  37. package/package.json +3 -2
  38. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  39. package/src/app/analytics/page.tsx +1 -21
  40. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  41. package/src/app/api/chat/conversations/[id]/skills/__tests__/activate.test.ts +141 -0
  42. package/src/app/api/chat/conversations/[id]/skills/activate/route.ts +74 -0
  43. package/src/app/api/chat/conversations/[id]/skills/deactivate/route.ts +33 -0
  44. package/src/app/api/chat/export/route.ts +52 -0
  45. package/src/app/api/chat/files/search/route.ts +50 -0
  46. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  47. package/src/app/api/environment/rescan-if-stale/__tests__/route.test.ts +45 -0
  48. package/src/app/api/environment/rescan-if-stale/route.ts +23 -0
  49. package/src/app/api/environment/skills/route.ts +13 -0
  50. package/src/app/api/instance/config/route.ts +41 -0
  51. package/src/app/api/instance/init/route.ts +34 -0
  52. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  53. package/src/app/api/instance/upgrade/route.ts +96 -0
  54. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  55. package/src/app/api/memory/route.ts +0 -11
  56. package/src/app/api/notifications/route.ts +4 -2
  57. package/src/app/api/projects/[id]/route.ts +5 -155
  58. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  59. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  60. package/src/app/api/schedules/[id]/route.ts +9 -1
  61. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  62. package/src/app/api/schedules/route.ts +3 -12
  63. package/src/app/api/settings/chat/pins/route.ts +94 -0
  64. package/src/app/api/settings/chat/saved-searches/__tests__/route.test.ts +119 -0
  65. package/src/app/api/settings/chat/saved-searches/route.ts +79 -0
  66. package/src/app/api/settings/environment/route.ts +26 -0
  67. package/src/app/api/settings/openai/login/route.ts +22 -0
  68. package/src/app/api/settings/openai/logout/route.ts +7 -0
  69. package/src/app/api/settings/openai/route.ts +21 -1
  70. package/src/app/api/settings/providers/route.ts +35 -8
  71. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  72. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  73. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  74. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  75. package/src/app/api/tasks/[id]/execute/route.ts +52 -33
  76. package/src/app/api/tasks/[id]/respond/route.ts +31 -15
  77. package/src/app/api/tasks/[id]/resume/route.ts +24 -3
  78. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  79. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  80. package/src/app/api/workspace/context/route.ts +2 -0
  81. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  82. package/src/app/chat/page.tsx +11 -0
  83. package/src/app/documents/page.tsx +4 -1
  84. package/src/app/inbox/page.tsx +12 -5
  85. package/src/app/layout.tsx +42 -21
  86. package/src/app/page.tsx +0 -2
  87. package/src/app/settings/page.tsx +8 -9
  88. package/src/components/chat/__tests__/capability-banner.test.tsx +38 -0
  89. package/src/components/chat/__tests__/chat-session-provider.test.tsx +573 -0
  90. package/src/components/chat/__tests__/skill-row.test.tsx +91 -0
  91. package/src/components/chat/capability-banner.tsx +68 -0
  92. package/src/components/chat/chat-command-popover.tsx +670 -49
  93. package/src/components/chat/chat-input.tsx +104 -10
  94. package/src/components/chat/chat-message.tsx +12 -3
  95. package/src/components/chat/chat-session-provider.tsx +790 -0
  96. package/src/components/chat/chat-shell.tsx +151 -401
  97. package/src/components/chat/command-tab-bar.tsx +68 -0
  98. package/src/components/chat/conversation-template-picker.tsx +421 -0
  99. package/src/components/chat/help-dialog.tsx +39 -0
  100. package/src/components/chat/skill-composition-conflict-dialog.tsx +96 -0
  101. package/src/components/chat/skill-row.tsx +147 -0
  102. package/src/components/documents/document-browser.tsx +37 -19
  103. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  104. package/src/components/instance/instance-section.tsx +382 -0
  105. package/src/components/instance/upgrade-badge.tsx +219 -0
  106. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  107. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  108. package/src/components/notifications/__tests__/permission-response-actions.test.tsx +70 -0
  109. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  110. package/src/components/notifications/inbox-list.tsx +11 -2
  111. package/src/components/notifications/notification-item.tsx +56 -2
  112. package/src/components/notifications/pending-approval-host.tsx +56 -37
  113. package/src/components/notifications/permission-response-actions.tsx +155 -1
  114. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  115. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  116. package/src/components/schedules/schedule-form.tsx +31 -0
  117. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  118. package/src/components/settings/auth-method-selector.tsx +19 -4
  119. package/src/components/settings/auth-status-badge.tsx +28 -3
  120. package/src/components/settings/environment-section.tsx +102 -0
  121. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  122. package/src/components/settings/openai-runtime-section.tsx +7 -1
  123. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  124. package/src/components/shared/__tests__/filter-hint.test.tsx +40 -0
  125. package/src/components/shared/__tests__/saved-searches-manager.test.tsx +147 -0
  126. package/src/components/shared/app-sidebar.tsx +4 -3
  127. package/src/components/shared/command-palette.tsx +266 -7
  128. package/src/components/shared/filter-hint.tsx +70 -0
  129. package/src/components/shared/filter-input.tsx +59 -0
  130. package/src/components/shared/saved-searches-manager.tsx +199 -0
  131. package/src/components/shared/theme-toggle.tsx +5 -24
  132. package/src/components/shared/workspace-indicator.tsx +61 -2
  133. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  134. package/src/components/tables/table-create-sheet.tsx +4 -0
  135. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  136. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  137. package/src/components/tables/table-spreadsheet.tsx +29 -5
  138. package/src/components/tables/table-toolbar.tsx +10 -1
  139. package/src/components/tasks/kanban-board.tsx +1 -0
  140. package/src/components/tasks/kanban-column.tsx +53 -14
  141. package/src/components/tasks/task-bento-grid.tsx +31 -2
  142. package/src/components/tasks/task-card.tsx +29 -3
  143. package/src/components/tasks/task-chip-bar.tsx +54 -1
  144. package/src/components/tasks/task-result-renderer.tsx +1 -1
  145. package/src/components/workflows/delay-step-body.tsx +109 -0
  146. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  147. package/src/components/workflows/loop-status-view.tsx +1 -1
  148. package/src/components/workflows/shared/step-result.tsx +78 -0
  149. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  150. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  151. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  152. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  153. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  154. package/src/components/workflows/workflow-form-view.tsx +133 -16
  155. package/src/components/workflows/workflow-status-view.tsx +30 -740
  156. package/src/hooks/__tests__/use-chat-autocomplete-tabs.test.ts +47 -0
  157. package/src/hooks/__tests__/use-saved-searches.test.ts +70 -0
  158. package/src/hooks/use-active-skills.ts +110 -0
  159. package/src/hooks/use-chat-autocomplete.ts +120 -7
  160. package/src/hooks/use-enriched-skills.ts +19 -0
  161. package/src/hooks/use-pinned-entries.ts +104 -0
  162. package/src/hooks/use-recent-user-messages.ts +19 -0
  163. package/src/hooks/use-saved-searches.ts +142 -0
  164. package/src/instrumentation-node.ts +94 -0
  165. package/src/instrumentation.ts +4 -48
  166. package/src/lib/agents/__tests__/claude-agent-sdk-options.test.ts +56 -0
  167. package/src/lib/agents/__tests__/claude-agent.test.ts +212 -0
  168. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  169. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  170. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  171. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  172. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  173. package/src/lib/agents/__tests__/task-dispatch.test.ts +166 -0
  174. package/src/lib/agents/__tests__/tool-permissions.test.ts +60 -0
  175. package/src/lib/agents/claude-agent.ts +217 -21
  176. package/src/lib/agents/execution-manager.ts +0 -35
  177. package/src/lib/agents/handoff/bus.ts +2 -2
  178. package/src/lib/agents/learned-context.ts +0 -12
  179. package/src/lib/agents/learning-session.ts +18 -5
  180. package/src/lib/agents/profiles/__tests__/list-fused-profiles.test.ts +110 -0
  181. package/src/lib/agents/profiles/__tests__/registry.test.ts +53 -4
  182. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +97 -0
  183. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +36 -0
  184. package/src/lib/agents/profiles/list-fused-profiles.ts +104 -0
  185. package/src/lib/agents/profiles/registry.ts +18 -0
  186. package/src/lib/agents/profiles/types.ts +7 -1
  187. package/src/lib/agents/router.ts +3 -6
  188. package/src/lib/agents/runtime/__tests__/catalog.test.ts +130 -0
  189. package/src/lib/agents/runtime/__tests__/execution-target.test.ts +183 -0
  190. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  191. package/src/lib/agents/runtime/anthropic-direct.ts +8 -0
  192. package/src/lib/agents/runtime/catalog.ts +121 -0
  193. package/src/lib/agents/runtime/claude-sdk.ts +32 -0
  194. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  195. package/src/lib/agents/runtime/execution-target.ts +456 -0
  196. package/src/lib/agents/runtime/index.ts +4 -0
  197. package/src/lib/agents/runtime/launch-failure.ts +101 -0
  198. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  199. package/src/lib/agents/runtime/openai-codex.ts +64 -60
  200. package/src/lib/agents/runtime/openai-direct.ts +8 -0
  201. package/src/lib/agents/runtime/types.ts +8 -0
  202. package/src/lib/agents/task-dispatch.ts +220 -0
  203. package/src/lib/agents/tool-permissions.ts +16 -1
  204. package/src/lib/book/chapter-mapping.ts +11 -0
  205. package/src/lib/book/content.ts +10 -0
  206. package/src/lib/chat/__tests__/active-skill-injection.test.ts +261 -0
  207. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  208. package/src/lib/chat/__tests__/clean-filter-input.test.ts +68 -0
  209. package/src/lib/chat/__tests__/command-tabs.test.ts +68 -0
  210. package/src/lib/chat/__tests__/context-builder-files.test.ts +112 -0
  211. package/src/lib/chat/__tests__/dismissals.test.ts +65 -0
  212. package/src/lib/chat/__tests__/engine-sdk-options.test.ts +117 -0
  213. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  214. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  215. package/src/lib/chat/__tests__/skill-conflict.test.ts +35 -0
  216. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  217. package/src/lib/chat/__tests__/types.test.ts +28 -0
  218. package/src/lib/chat/active-skills.ts +31 -0
  219. package/src/lib/chat/active-streams.ts +27 -0
  220. package/src/lib/chat/clean-filter-input.ts +30 -0
  221. package/src/lib/chat/codex-engine.ts +46 -24
  222. package/src/lib/chat/command-tabs.ts +61 -0
  223. package/src/lib/chat/context-builder.ts +146 -4
  224. package/src/lib/chat/dismissals.ts +73 -0
  225. package/src/lib/chat/engine.ts +159 -18
  226. package/src/lib/chat/files/__tests__/search.test.ts +135 -0
  227. package/src/lib/chat/files/expand-mention.ts +76 -0
  228. package/src/lib/chat/files/search.ts +99 -0
  229. package/src/lib/chat/reconcile.ts +117 -0
  230. package/src/lib/chat/skill-composition.ts +210 -0
  231. package/src/lib/chat/skill-conflict.ts +105 -0
  232. package/src/lib/chat/stagent-tools.ts +7 -19
  233. package/src/lib/chat/stream-telemetry.ts +137 -0
  234. package/src/lib/chat/suggested-prompts.ts +28 -1
  235. package/src/lib/chat/system-prompt.ts +48 -1
  236. package/src/lib/chat/tool-catalog.ts +35 -4
  237. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  238. package/src/lib/chat/tools/__tests__/profile-tools.test.ts +51 -0
  239. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  240. package/src/lib/chat/tools/__tests__/settings-tools.test.ts +294 -0
  241. package/src/lib/chat/tools/__tests__/skill-tools.test.ts +474 -0
  242. package/src/lib/chat/tools/__tests__/task-tools.test.ts +399 -0
  243. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +351 -0
  244. package/src/lib/chat/tools/blueprint-tools.ts +190 -0
  245. package/src/lib/chat/tools/document-tools.ts +29 -13
  246. package/src/lib/chat/tools/helpers.ts +41 -0
  247. package/src/lib/chat/tools/notification-tools.ts +9 -5
  248. package/src/lib/chat/tools/profile-tools.ts +120 -23
  249. package/src/lib/chat/tools/project-tools.ts +33 -0
  250. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  251. package/src/lib/chat/tools/skill-tools.ts +183 -0
  252. package/src/lib/chat/tools/table-tools.ts +71 -0
  253. package/src/lib/chat/tools/task-tools.ts +89 -21
  254. package/src/lib/chat/tools/workflow-tools.ts +275 -32
  255. package/src/lib/chat/types.ts +15 -0
  256. package/src/lib/constants/settings.ts +10 -18
  257. package/src/lib/data/__tests__/clear.test.ts +56 -2
  258. package/src/lib/data/clear.ts +17 -16
  259. package/src/lib/data/delete-project.ts +171 -0
  260. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  261. package/src/lib/db/bootstrap.ts +62 -16
  262. package/src/lib/db/index.ts +5 -0
  263. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  264. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  265. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  266. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  267. package/src/lib/db/migrations/meta/_journal.json +21 -0
  268. package/src/lib/db/schema.ts +94 -23
  269. package/src/lib/environment/__tests__/auto-promote.test.ts +132 -0
  270. package/src/lib/environment/__tests__/list-skills-enriched.test.ts +55 -0
  271. package/src/lib/environment/__tests__/skill-enrichment.test.ts +129 -0
  272. package/src/lib/environment/__tests__/skill-recommendations.test.ts +87 -0
  273. package/src/lib/environment/data.ts +9 -0
  274. package/src/lib/environment/list-skills.ts +176 -0
  275. package/src/lib/environment/parsers/__tests__/skill.test.ts +54 -0
  276. package/src/lib/environment/parsers/skill.ts +26 -5
  277. package/src/lib/environment/profile-generator.ts +54 -0
  278. package/src/lib/environment/skill-enrichment.ts +106 -0
  279. package/src/lib/environment/skill-recommendations.ts +66 -0
  280. package/src/lib/environment/workspace-context.ts +13 -1
  281. package/src/lib/filters/__tests__/parse.quoted.test.ts +40 -0
  282. package/src/lib/filters/__tests__/parse.test.ts +135 -0
  283. package/src/lib/filters/parse.ts +86 -0
  284. package/src/lib/import/dedup.ts +4 -54
  285. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  286. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  287. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  288. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  289. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  290. package/src/lib/instance/__tests__/upgrade-poller.test.ts +181 -0
  291. package/src/lib/instance/bootstrap.ts +270 -0
  292. package/src/lib/instance/detect.ts +49 -0
  293. package/src/lib/instance/fingerprint.ts +76 -0
  294. package/src/lib/instance/git-ops.ts +95 -0
  295. package/src/lib/instance/settings.ts +61 -0
  296. package/src/lib/instance/types.ts +77 -0
  297. package/src/lib/instance/upgrade-poller.ts +205 -0
  298. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  299. package/src/lib/notifications/visibility.ts +33 -0
  300. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  301. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  302. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  303. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  304. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  305. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  306. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  307. package/src/lib/schedules/collision-check.ts +105 -0
  308. package/src/lib/schedules/config.ts +53 -0
  309. package/src/lib/schedules/scheduler.ts +236 -17
  310. package/src/lib/schedules/slot-claim.ts +105 -0
  311. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  312. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  313. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  314. package/src/lib/settings/openai-auth.ts +105 -10
  315. package/src/lib/settings/openai-login-manager.ts +260 -0
  316. package/src/lib/settings/runtime-setup.ts +14 -4
  317. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  318. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  319. package/src/lib/tables/enrichment-planner.ts +454 -0
  320. package/src/lib/tables/enrichment.ts +328 -0
  321. package/src/lib/tables/query-builder.ts +5 -2
  322. package/src/lib/tables/trigger-evaluator.ts +3 -2
  323. package/src/lib/theme.ts +71 -0
  324. package/src/lib/usage/ledger.ts +2 -18
  325. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  326. package/src/lib/util/similarity.ts +77 -0
  327. package/src/lib/utils/format-timestamp.ts +24 -0
  328. package/src/lib/utils/stagent-paths.ts +12 -0
  329. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  330. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  331. package/src/lib/validators/blueprint.ts +70 -9
  332. package/src/lib/validators/profile.ts +2 -2
  333. package/src/lib/validators/settings.ts +3 -1
  334. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  335. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  336. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  337. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  338. package/src/lib/workflows/blueprints/__tests__/render-prompt.test.ts +124 -0
  339. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  340. package/src/lib/workflows/blueprints/render-prompt.ts +71 -0
  341. package/src/lib/workflows/blueprints/types.ts +16 -2
  342. package/src/lib/workflows/delay.ts +106 -0
  343. package/src/lib/workflows/engine.ts +212 -7
  344. package/src/lib/workflows/loop-executor.ts +349 -24
  345. package/src/lib/workflows/post-action.ts +91 -0
  346. package/src/lib/workflows/types.ts +166 -1
  347. package/src/test/setup.ts +10 -0
  348. package/src/app/api/license/checkout/route.ts +0 -28
  349. package/src/app/api/license/portal/route.ts +0 -26
  350. package/src/app/api/license/route.ts +0 -89
  351. package/src/app/api/license/usage/route.ts +0 -63
  352. package/src/app/api/marketplace/browse/route.ts +0 -15
  353. package/src/app/api/marketplace/import/route.ts +0 -28
  354. package/src/app/api/marketplace/publish/route.ts +0 -40
  355. package/src/app/api/onboarding/email/route.ts +0 -53
  356. package/src/app/api/settings/telemetry/route.ts +0 -14
  357. package/src/app/api/sync/export/route.ts +0 -54
  358. package/src/app/api/sync/restore/route.ts +0 -37
  359. package/src/app/api/sync/sessions/route.ts +0 -24
  360. package/src/app/auth/callback/route.ts +0 -73
  361. package/src/app/marketplace/page.tsx +0 -19
  362. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  363. package/src/components/marketplace/blueprint-card.tsx +0 -61
  364. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  365. package/src/components/onboarding/email-capture-card.tsx +0 -104
  366. package/src/components/settings/activation-form.tsx +0 -95
  367. package/src/components/settings/cloud-account-section.tsx +0 -147
  368. package/src/components/settings/cloud-sync-section.tsx +0 -155
  369. package/src/components/settings/subscription-section.tsx +0 -410
  370. package/src/components/settings/telemetry-section.tsx +0 -80
  371. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  372. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  373. package/src/components/shared/upgrade-banner.tsx +0 -112
  374. package/src/hooks/use-supabase-auth.ts +0 -79
  375. package/src/lib/billing/email.ts +0 -54
  376. package/src/lib/billing/products.ts +0 -80
  377. package/src/lib/billing/stripe.ts +0 -101
  378. package/src/lib/cloud/supabase-browser.ts +0 -32
  379. package/src/lib/cloud/supabase-client.ts +0 -56
  380. package/src/lib/license/__tests__/features.test.ts +0 -56
  381. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  382. package/src/lib/license/__tests__/manager.test.ts +0 -64
  383. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  384. package/src/lib/license/cloud-validation.ts +0 -60
  385. package/src/lib/license/features.ts +0 -44
  386. package/src/lib/license/key-format.ts +0 -101
  387. package/src/lib/license/limit-check.ts +0 -111
  388. package/src/lib/license/limit-queries.ts +0 -51
  389. package/src/lib/license/manager.ts +0 -345
  390. package/src/lib/license/notifications.ts +0 -59
  391. package/src/lib/license/tier-limits.ts +0 -71
  392. package/src/lib/marketplace/marketplace-client.ts +0 -107
  393. package/src/lib/sync/cloud-sync.ts +0 -235
  394. package/src/lib/telemetry/conversion-events.ts +0 -71
  395. package/src/lib/telemetry/queue.ts +0 -122
  396. 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
- const dashboard = (
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
- controller.close();
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
+ }