stagent 0.9.5 → 0.10.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 (277) hide show
  1. package/README.md +5 -42
  2. package/dist/cli.js +42 -18
  3. package/docs/.coverage-gaps.json +13 -55
  4. package/docs/.last-generated +1 -1
  5. package/docs/features/provider-runtimes.md +4 -0
  6. package/docs/features/schedules.md +32 -4
  7. package/docs/features/settings.md +28 -5
  8. package/docs/features/tables.md +9 -2
  9. package/docs/features/workflows.md +10 -4
  10. package/docs/journeys/developer.md +15 -1
  11. package/docs/journeys/personal-use.md +21 -4
  12. package/docs/superpowers/plans/2026-04-07-instance-bootstrap.md +1691 -0
  13. package/docs/superpowers/plans/2026-04-08-schedule-orchestration.md +2983 -0
  14. package/docs/superpowers/plans/2026-04-11-schedule-maxturns-api-control.md +551 -0
  15. package/docs/superpowers/plans/2026-04-11-task-create-profile-validation.md +864 -0
  16. package/docs/superpowers/plans/2026-04-11-task-runtime-stagent-mcp-injection.md +739 -0
  17. package/docs/superpowers/specs/2026-04-08-chat-sse-resilience-hotfix-design.md +201 -0
  18. package/docs/superpowers/specs/2026-04-08-schedule-orchestration-design.md +371 -0
  19. package/docs/superpowers/specs/2026-04-08-swarm-visibility-design.md +213 -0
  20. package/package.json +3 -2
  21. package/src/__tests__/instrumentation-smoke.test.ts +15 -0
  22. package/src/app/analytics/page.tsx +1 -21
  23. package/src/app/api/chat/conversations/[id]/messages/route.ts +22 -1
  24. package/src/app/api/diagnostics/chat-streams/route.ts +65 -0
  25. package/src/app/api/instance/config/route.ts +41 -0
  26. package/src/app/api/instance/init/route.ts +34 -0
  27. package/src/app/api/instance/upgrade/check/route.ts +26 -0
  28. package/src/app/api/instance/upgrade/route.ts +96 -0
  29. package/src/app/api/instance/upgrade/status/route.ts +35 -0
  30. package/src/app/api/memory/route.ts +0 -11
  31. package/src/app/api/notifications/route.ts +4 -2
  32. package/src/app/api/projects/[id]/route.ts +5 -155
  33. package/src/app/api/projects/__tests__/delete-project.test.ts +10 -19
  34. package/src/app/api/schedules/[id]/execute/route.ts +111 -0
  35. package/src/app/api/schedules/[id]/route.ts +9 -1
  36. package/src/app/api/schedules/__tests__/execute-route.test.ts +118 -0
  37. package/src/app/api/schedules/route.ts +3 -12
  38. package/src/app/api/settings/openai/login/route.ts +22 -0
  39. package/src/app/api/settings/openai/logout/route.ts +7 -0
  40. package/src/app/api/settings/openai/route.ts +21 -1
  41. package/src/app/api/settings/providers/route.ts +35 -8
  42. package/src/app/api/tables/[id]/enrich/__tests__/route.test.ts +153 -0
  43. package/src/app/api/tables/[id]/enrich/plan/route.ts +98 -0
  44. package/src/app/api/tables/[id]/enrich/route.ts +147 -0
  45. package/src/app/api/tables/[id]/enrich/runs/route.ts +25 -0
  46. package/src/app/api/tasks/[id]/execute/route.ts +0 -21
  47. package/src/app/api/workflows/[id]/resume/route.ts +59 -0
  48. package/src/app/api/workflows/[id]/status/route.ts +22 -8
  49. package/src/app/api/workspace/context/route.ts +2 -0
  50. package/src/app/api/workspace/fix-data-dir/route.ts +81 -0
  51. package/src/app/chat/page.tsx +11 -0
  52. package/src/app/inbox/page.tsx +12 -5
  53. package/src/app/layout.tsx +42 -21
  54. package/src/app/page.tsx +0 -2
  55. package/src/app/settings/page.tsx +6 -9
  56. package/src/components/chat/__tests__/chat-session-provider.test.tsx +408 -0
  57. package/src/components/chat/chat-command-popover.tsx +2 -2
  58. package/src/components/chat/chat-input.tsx +2 -3
  59. package/src/components/chat/chat-session-provider.tsx +720 -0
  60. package/src/components/chat/chat-shell.tsx +92 -401
  61. package/src/components/instance/__tests__/instance-section.test.tsx +125 -0
  62. package/src/components/instance/instance-section.tsx +382 -0
  63. package/src/components/instance/upgrade-badge.tsx +219 -0
  64. package/src/components/notifications/__tests__/batch-proposal-review.test.tsx +95 -0
  65. package/src/components/notifications/__tests__/notification-item.test.tsx +106 -0
  66. package/src/components/notifications/batch-proposal-review.tsx +20 -5
  67. package/src/components/notifications/inbox-list.tsx +11 -2
  68. package/src/components/notifications/notification-item.tsx +56 -2
  69. package/src/components/notifications/pending-approval-host.tsx +56 -37
  70. package/src/components/schedules/schedule-create-sheet.tsx +19 -1
  71. package/src/components/schedules/schedule-edit-sheet.tsx +20 -1
  72. package/src/components/schedules/schedule-form.tsx +31 -0
  73. package/src/components/settings/__tests__/providers-runtimes-section.test.tsx +149 -0
  74. package/src/components/settings/auth-method-selector.tsx +19 -4
  75. package/src/components/settings/auth-status-badge.tsx +28 -3
  76. package/src/components/settings/openai-chatgpt-auth-control.tsx +278 -0
  77. package/src/components/settings/openai-runtime-section.tsx +7 -1
  78. package/src/components/settings/providers-runtimes-section.tsx +138 -19
  79. package/src/components/shared/app-sidebar.tsx +4 -3
  80. package/src/components/shared/command-palette.tsx +4 -5
  81. package/src/components/shared/theme-toggle.tsx +5 -24
  82. package/src/components/shared/workspace-indicator.tsx +61 -2
  83. package/src/components/tables/__tests__/table-enrichment-sheet.test.tsx +130 -0
  84. package/src/components/tables/table-create-sheet.tsx +4 -0
  85. package/src/components/tables/table-enrichment-runs.tsx +103 -0
  86. package/src/components/tables/table-enrichment-sheet.tsx +538 -0
  87. package/src/components/tables/table-spreadsheet.tsx +29 -5
  88. package/src/components/tables/table-toolbar.tsx +10 -1
  89. package/src/components/tasks/kanban-board.tsx +1 -0
  90. package/src/components/tasks/kanban-column.tsx +53 -14
  91. package/src/components/tasks/task-bento-grid.tsx +19 -0
  92. package/src/components/tasks/task-card.tsx +26 -3
  93. package/src/components/tasks/task-chip-bar.tsx +24 -0
  94. package/src/components/tasks/task-result-renderer.tsx +1 -1
  95. package/src/components/workflows/delay-step-body.tsx +109 -0
  96. package/src/components/workflows/hooks/use-workflow-status.ts +50 -0
  97. package/src/components/workflows/loop-status-view.tsx +1 -1
  98. package/src/components/workflows/shared/step-result.tsx +78 -0
  99. package/src/components/workflows/shared/workflow-header.tsx +141 -0
  100. package/src/components/workflows/shared/workflow-loading-skeleton.tsx +36 -0
  101. package/src/components/workflows/swarm-dashboard.tsx +2 -15
  102. package/src/components/workflows/views/loop-pattern-view.tsx +137 -0
  103. package/src/components/workflows/views/sequence-pattern-view.tsx +511 -0
  104. package/src/components/workflows/workflow-form-view.tsx +133 -16
  105. package/src/components/workflows/workflow-status-view.tsx +30 -740
  106. package/src/instrumentation-node.ts +94 -0
  107. package/src/instrumentation.ts +4 -48
  108. package/src/lib/agents/__tests__/claude-agent.test.ts +199 -0
  109. package/src/lib/agents/__tests__/execution-manager.test.ts +1 -27
  110. package/src/lib/agents/__tests__/failure-reason.test.ts +68 -0
  111. package/src/lib/agents/__tests__/learned-context.test.ts +0 -11
  112. package/src/lib/agents/__tests__/learning-session.test.ts +158 -0
  113. package/src/lib/agents/__tests__/pattern-extractor.test.ts +48 -0
  114. package/src/lib/agents/claude-agent.ts +155 -18
  115. package/src/lib/agents/execution-manager.ts +0 -35
  116. package/src/lib/agents/learned-context.ts +0 -12
  117. package/src/lib/agents/learning-session.ts +18 -5
  118. package/src/lib/agents/profiles/__tests__/registry.test.ts +6 -4
  119. package/src/lib/agents/profiles/builtins/upgrade-assistant/SKILL.md +70 -0
  120. package/src/lib/agents/profiles/builtins/upgrade-assistant/profile.yaml +32 -0
  121. package/src/lib/agents/runtime/__tests__/openai-codex-auth.test.ts +118 -0
  122. package/src/lib/agents/runtime/codex-app-server-client.ts +11 -5
  123. package/src/lib/agents/runtime/openai-codex-auth.ts +389 -0
  124. package/src/lib/agents/runtime/openai-codex.ts +29 -60
  125. package/src/lib/agents/runtime/types.ts +8 -0
  126. package/src/lib/book/chapter-mapping.ts +11 -0
  127. package/src/lib/book/content.ts +10 -0
  128. package/src/lib/chat/__tests__/active-streams.test.ts +49 -0
  129. package/src/lib/chat/__tests__/finalize-safety-net.test.ts +139 -0
  130. package/src/lib/chat/__tests__/reconcile.test.ts +137 -0
  131. package/src/lib/chat/__tests__/stream-telemetry.test.ts +151 -0
  132. package/src/lib/chat/active-streams.ts +27 -0
  133. package/src/lib/chat/codex-engine.ts +16 -17
  134. package/src/lib/chat/context-builder.ts +5 -3
  135. package/src/lib/chat/engine.ts +50 -3
  136. package/src/lib/chat/reconcile.ts +117 -0
  137. package/src/lib/chat/stagent-tools.ts +1 -0
  138. package/src/lib/chat/stream-telemetry.ts +132 -0
  139. package/src/lib/chat/suggested-prompts.ts +28 -1
  140. package/src/lib/chat/system-prompt.ts +26 -1
  141. package/src/lib/chat/tool-catalog.ts +2 -1
  142. package/src/lib/chat/tools/__tests__/enrich-table-tool.test.ts +127 -0
  143. package/src/lib/chat/tools/__tests__/schedule-tools.test.ts +261 -0
  144. package/src/lib/chat/tools/__tests__/task-tools.test.ts +352 -0
  145. package/src/lib/chat/tools/__tests__/workflow-tools-dedup.test.ts +217 -0
  146. package/src/lib/chat/tools/document-tools.ts +29 -13
  147. package/src/lib/chat/tools/helpers.ts +39 -0
  148. package/src/lib/chat/tools/notification-tools.ts +9 -5
  149. package/src/lib/chat/tools/project-tools.ts +33 -0
  150. package/src/lib/chat/tools/schedule-tools.ts +44 -11
  151. package/src/lib/chat/tools/table-tools.ts +71 -0
  152. package/src/lib/chat/tools/task-tools.ts +84 -20
  153. package/src/lib/chat/tools/workflow-tools.ts +234 -32
  154. package/src/lib/constants/settings.ts +8 -18
  155. package/src/lib/data/__tests__/clear.test.ts +56 -2
  156. package/src/lib/data/clear.ts +20 -15
  157. package/src/lib/data/delete-project.ts +171 -0
  158. package/src/lib/db/__tests__/bootstrap.test.ts +1 -1
  159. package/src/lib/db/bootstrap.ts +45 -16
  160. package/src/lib/db/index.ts +5 -0
  161. package/src/lib/db/migrations/0009_add_app_instances.sql +25 -0
  162. package/src/lib/db/migrations/0024_add_workflow_resume_at.sql +10 -0
  163. package/src/lib/db/migrations/0025_drop_app_instances.sql +3 -0
  164. package/src/lib/db/migrations/0026_drop_license.sql +3 -0
  165. package/src/lib/db/migrations/meta/_journal.json +21 -0
  166. package/src/lib/db/schema.ts +68 -23
  167. package/src/lib/environment/workspace-context.ts +13 -1
  168. package/src/lib/import/dedup.ts +4 -54
  169. package/src/lib/instance/__tests__/bootstrap.test.ts +362 -0
  170. package/src/lib/instance/__tests__/detect.test.ts +115 -0
  171. package/src/lib/instance/__tests__/fingerprint.test.ts +48 -0
  172. package/src/lib/instance/__tests__/git-ops.test.ts +95 -0
  173. package/src/lib/instance/__tests__/settings.test.ts +83 -0
  174. package/src/lib/instance/__tests__/upgrade-poller.test.ts +131 -0
  175. package/src/lib/instance/bootstrap.ts +270 -0
  176. package/src/lib/instance/detect.ts +49 -0
  177. package/src/lib/instance/fingerprint.ts +78 -0
  178. package/src/lib/instance/git-ops.ts +95 -0
  179. package/src/lib/instance/settings.ts +61 -0
  180. package/src/lib/instance/types.ts +77 -0
  181. package/src/lib/instance/upgrade-poller.ts +153 -0
  182. package/src/lib/notifications/__tests__/visibility.test.ts +51 -0
  183. package/src/lib/notifications/visibility.ts +33 -0
  184. package/src/lib/schedules/__tests__/collision-check.test.ts +93 -0
  185. package/src/lib/schedules/__tests__/config.test.ts +62 -0
  186. package/src/lib/schedules/__tests__/firing-metrics.test.ts +99 -0
  187. package/src/lib/schedules/__tests__/integration.test.ts +82 -0
  188. package/src/lib/schedules/__tests__/slot-claim.test.ts +242 -0
  189. package/src/lib/schedules/__tests__/tick-scheduler.test.ts +102 -0
  190. package/src/lib/schedules/__tests__/turn-budget.test.ts +228 -0
  191. package/src/lib/schedules/collision-check.ts +105 -0
  192. package/src/lib/schedules/config.ts +53 -0
  193. package/src/lib/schedules/scheduler.ts +232 -13
  194. package/src/lib/schedules/slot-claim.ts +105 -0
  195. package/src/lib/settings/__tests__/openai-auth.test.ts +101 -0
  196. package/src/lib/settings/__tests__/openai-login-manager.test.ts +64 -0
  197. package/src/lib/settings/__tests__/runtime-setup.test.ts +33 -0
  198. package/src/lib/settings/openai-auth.ts +105 -10
  199. package/src/lib/settings/openai-login-manager.ts +260 -0
  200. package/src/lib/settings/runtime-setup.ts +14 -4
  201. package/src/lib/tables/__tests__/enrichment-planner.test.ts +124 -0
  202. package/src/lib/tables/__tests__/enrichment.test.ts +147 -0
  203. package/src/lib/tables/enrichment-planner.ts +454 -0
  204. package/src/lib/tables/enrichment.ts +328 -0
  205. package/src/lib/tables/query-builder.ts +5 -2
  206. package/src/lib/tables/trigger-evaluator.ts +3 -2
  207. package/src/lib/theme.ts +71 -0
  208. package/src/lib/usage/ledger.ts +2 -18
  209. package/src/lib/util/__tests__/similarity.test.ts +106 -0
  210. package/src/lib/util/similarity.ts +77 -0
  211. package/src/lib/utils/format-timestamp.ts +24 -0
  212. package/src/lib/utils/stagent-paths.ts +12 -0
  213. package/src/lib/validators/__tests__/blueprint.test.ts +172 -0
  214. package/src/lib/validators/__tests__/settings.test.ts +10 -0
  215. package/src/lib/validators/blueprint.ts +70 -9
  216. package/src/lib/validators/profile.ts +2 -2
  217. package/src/lib/validators/settings.ts +3 -1
  218. package/src/lib/workflows/__tests__/delay.test.ts +196 -0
  219. package/src/lib/workflows/__tests__/engine.test.ts +8 -0
  220. package/src/lib/workflows/__tests__/loop-executor.test.ts +54 -0
  221. package/src/lib/workflows/__tests__/post-action.test.ts +108 -0
  222. package/src/lib/workflows/blueprints/instantiator.ts +22 -1
  223. package/src/lib/workflows/blueprints/types.ts +10 -2
  224. package/src/lib/workflows/delay.ts +106 -0
  225. package/src/lib/workflows/engine.ts +207 -4
  226. package/src/lib/workflows/loop-executor.ts +349 -24
  227. package/src/lib/workflows/post-action.ts +91 -0
  228. package/src/lib/workflows/types.ts +166 -1
  229. package/src/app/api/license/checkout/route.ts +0 -28
  230. package/src/app/api/license/portal/route.ts +0 -26
  231. package/src/app/api/license/route.ts +0 -89
  232. package/src/app/api/license/usage/route.ts +0 -63
  233. package/src/app/api/marketplace/browse/route.ts +0 -15
  234. package/src/app/api/marketplace/import/route.ts +0 -28
  235. package/src/app/api/marketplace/publish/route.ts +0 -40
  236. package/src/app/api/onboarding/email/route.ts +0 -53
  237. package/src/app/api/settings/telemetry/route.ts +0 -14
  238. package/src/app/api/sync/export/route.ts +0 -54
  239. package/src/app/api/sync/restore/route.ts +0 -37
  240. package/src/app/api/sync/sessions/route.ts +0 -24
  241. package/src/app/auth/callback/route.ts +0 -73
  242. package/src/app/marketplace/page.tsx +0 -19
  243. package/src/components/analytics/analytics-gate-card.tsx +0 -101
  244. package/src/components/marketplace/blueprint-card.tsx +0 -61
  245. package/src/components/marketplace/marketplace-browser.tsx +0 -131
  246. package/src/components/onboarding/email-capture-card.tsx +0 -104
  247. package/src/components/settings/activation-form.tsx +0 -95
  248. package/src/components/settings/cloud-account-section.tsx +0 -147
  249. package/src/components/settings/cloud-sync-section.tsx +0 -155
  250. package/src/components/settings/subscription-section.tsx +0 -410
  251. package/src/components/settings/telemetry-section.tsx +0 -80
  252. package/src/components/shared/premium-gate-overlay.tsx +0 -50
  253. package/src/components/shared/schedule-gate-dialog.tsx +0 -64
  254. package/src/components/shared/upgrade-banner.tsx +0 -112
  255. package/src/hooks/use-supabase-auth.ts +0 -79
  256. package/src/lib/billing/email.ts +0 -54
  257. package/src/lib/billing/products.ts +0 -80
  258. package/src/lib/billing/stripe.ts +0 -101
  259. package/src/lib/cloud/supabase-browser.ts +0 -32
  260. package/src/lib/cloud/supabase-client.ts +0 -56
  261. package/src/lib/license/__tests__/features.test.ts +0 -56
  262. package/src/lib/license/__tests__/key-format.test.ts +0 -88
  263. package/src/lib/license/__tests__/manager.test.ts +0 -64
  264. package/src/lib/license/__tests__/tier-limits.test.ts +0 -79
  265. package/src/lib/license/cloud-validation.ts +0 -60
  266. package/src/lib/license/features.ts +0 -44
  267. package/src/lib/license/key-format.ts +0 -101
  268. package/src/lib/license/limit-check.ts +0 -111
  269. package/src/lib/license/limit-queries.ts +0 -51
  270. package/src/lib/license/manager.ts +0 -345
  271. package/src/lib/license/notifications.ts +0 -59
  272. package/src/lib/license/tier-limits.ts +0 -71
  273. package/src/lib/marketplace/marketplace-client.ts +0 -107
  274. package/src/lib/sync/cloud-sync.ts +0 -235
  275. package/src/lib/telemetry/conversion-events.ts +0 -71
  276. package/src/lib/telemetry/queue.ts +0 -122
  277. package/src/lib/validators/license.ts +0 -33
@@ -1,112 +0,0 @@
1
- "use client";
2
-
3
- import { useEffect } from "react";
4
- import Link from "next/link";
5
- import { AlertTriangle, Lock } from "lucide-react";
6
- import { Button } from "@/components/ui/button";
7
- import { cn } from "@/lib/utils";
8
- import { TIER_LABELS, type LicenseTier } from "@/lib/license/tier-limits";
9
- import type { LimitResource } from "@/lib/license/tier-limits";
10
- import { trackConversionEvent } from "@/lib/telemetry/conversion-events";
11
-
12
- const BANNER_TITLES: Record<LimitResource, string> = {
13
- agentMemories: "Memory limit approaching",
14
- contextVersions: "Context version limit reached",
15
- activeSchedules: "Schedule limit reached",
16
- historyRetentionDays: "History retention limit",
17
- parallelWorkflows: "Parallel workflow limit reached",
18
- };
19
-
20
- function getBannerMessage(
21
- resource: LimitResource,
22
- current: number,
23
- max: number,
24
- requiredTier: string
25
- ): string {
26
- const tierLabel = TIER_LABELS[requiredTier as LicenseTier] ?? requiredTier;
27
- switch (resource) {
28
- case "agentMemories":
29
- return `${current} of ${max} agent memories used. Upgrade to ${tierLabel} for more capacity.`;
30
- case "contextVersions":
31
- return `${current} of ${max} context versions used. Upgrade to ${tierLabel} to unlock more.`;
32
- case "activeSchedules":
33
- return `${current} of ${max} active schedules. Upgrade to ${tierLabel} for more schedules.`;
34
- case "historyRetentionDays":
35
- return `Execution history limited to ${max} days. Upgrade to ${tierLabel} for longer retention.`;
36
- case "parallelWorkflows":
37
- return `${current} of ${max} parallel workflows running. Upgrade to ${tierLabel} for more concurrency.`;
38
- }
39
- }
40
-
41
- interface UpgradeBannerProps {
42
- resource: LimitResource;
43
- current: number;
44
- max: number;
45
- requiredTier: string;
46
- variant: "warning" | "blocked";
47
- onDismiss?: () => void;
48
- onSnooze?: () => void;
49
- className?: string;
50
- }
51
-
52
- export function UpgradeBanner({
53
- resource,
54
- current,
55
- max,
56
- requiredTier,
57
- variant,
58
- onDismiss,
59
- onSnooze,
60
- className,
61
- }: UpgradeBannerProps) {
62
- const Icon = variant === "blocked" ? Lock : AlertTriangle;
63
- const tierLabel = TIER_LABELS[requiredTier as LicenseTier] ?? requiredTier;
64
-
65
- // Track banner impression on mount
66
- useEffect(() => {
67
- trackConversionEvent("banner_impression", resource);
68
- }, [resource]);
69
-
70
- return (
71
- <div
72
- role="alert"
73
- aria-live="polite"
74
- className={cn(
75
- "surface-card-muted rounded-lg border p-4 flex items-start gap-3",
76
- variant === "warning" && "border-amber-500/25",
77
- variant === "blocked" && "border-destructive/25",
78
- className
79
- )}
80
- >
81
- <Icon
82
- className={cn(
83
- "size-4 shrink-0 mt-0.5",
84
- variant === "warning" ? "text-amber-500" : "text-destructive"
85
- )}
86
- />
87
- <div className="flex-1 space-y-1">
88
- <p className="text-sm font-medium">{BANNER_TITLES[resource]}</p>
89
- <p className="text-xs text-muted-foreground">
90
- {getBannerMessage(resource, current, max, requiredTier)}
91
- </p>
92
- <div className="flex items-center gap-2 pt-2">
93
- <Button size="sm" asChild onClick={() => trackConversionEvent("banner_click", resource)}>
94
- <Link href={`/settings?highlight=${requiredTier}`}>
95
- Upgrade to {tierLabel}
96
- </Link>
97
- </Button>
98
- {onSnooze && (
99
- <Button size="sm" variant="ghost" onClick={onSnooze}>
100
- Remind later
101
- </Button>
102
- )}
103
- {onDismiss && (
104
- <Button size="sm" variant="ghost" onClick={onDismiss}>
105
- Dismiss
106
- </Button>
107
- )}
108
- </div>
109
- </div>
110
- </div>
111
- );
112
- }
@@ -1,79 +0,0 @@
1
- "use client";
2
-
3
- import { useState, useEffect, useCallback } from "react";
4
- import { getSupabaseBrowserClient } from "@/lib/cloud/supabase-browser";
5
- import type { Session, User } from "@supabase/supabase-js";
6
-
7
- interface AuthState {
8
- session: Session | null;
9
- user: User | null;
10
- email: string | null;
11
- loading: boolean;
12
- }
13
-
14
- /**
15
- * Hook for Supabase Auth state in the browser.
16
- * Tracks session, provides sign-in/sign-out helpers.
17
- */
18
- export function useSupabaseAuth() {
19
- const [auth, setAuth] = useState<AuthState>({
20
- session: null,
21
- user: null,
22
- email: null,
23
- loading: true,
24
- });
25
-
26
- useEffect(() => {
27
- const supabase = getSupabaseBrowserClient();
28
-
29
- // Get initial session
30
- supabase.auth.getSession().then(({ data: { session } }) => {
31
- setAuth({
32
- session,
33
- user: session?.user ?? null,
34
- email: session?.user?.email ?? null,
35
- loading: false,
36
- });
37
- });
38
-
39
- // Listen for auth state changes (magic link callback, sign-out, etc.)
40
- const {
41
- data: { subscription },
42
- } = supabase.auth.onAuthStateChange((_event, session) => {
43
- setAuth({
44
- session,
45
- user: session?.user ?? null,
46
- email: session?.user?.email ?? null,
47
- loading: false,
48
- });
49
- });
50
-
51
- return () => subscription.unsubscribe();
52
- }, []);
53
-
54
- const signInWithEmail = useCallback(async (email: string) => {
55
- const supabase = getSupabaseBrowserClient();
56
- const redirectTo = typeof window !== "undefined"
57
- ? `${window.location.origin}/auth/callback`
58
- : "http://localhost:3000/auth/callback";
59
-
60
- const { error } = await supabase.auth.signInWithOtp({
61
- email,
62
- options: { emailRedirectTo: redirectTo },
63
- });
64
-
65
- return { error: error?.message ?? null };
66
- }, []);
67
-
68
- const signOut = useCallback(async () => {
69
- const supabase = getSupabaseBrowserClient();
70
- await supabase.auth.signOut();
71
- }, []);
72
-
73
- return {
74
- ...auth,
75
- isSignedIn: !!auth.session,
76
- signInWithEmail,
77
- signOut,
78
- };
79
- }
@@ -1,54 +0,0 @@
1
- /**
2
- * Transactional email helpers via Resend.
3
- *
4
- * All emails are sent through the Supabase `send-email` Edge Function.
5
- * This module provides typed wrappers with no-op behavior when
6
- * the cloud backend is not configured.
7
- */
8
-
9
- import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
10
-
11
- async function sendEmail(
12
- template: string,
13
- to: string,
14
- data: Record<string, unknown>
15
- ): Promise<void> {
16
- if (!isCloudConfigured()) return;
17
-
18
- const supabaseUrl = getSupabaseUrl();
19
- const anonKey = getSupabaseAnonKey();
20
-
21
- try {
22
- await fetch(`${supabaseUrl}/functions/v1/send-email`, {
23
- method: "POST",
24
- headers: {
25
- "Content-Type": "application/json",
26
- Authorization: `Bearer ${anonKey}`,
27
- },
28
- body: JSON.stringify({ template, to, data }),
29
- });
30
- } catch {
31
- // Email sending is non-critical — log and continue
32
- console.warn(`[email] Failed to send ${template} to ${to}`);
33
- }
34
- }
35
-
36
- /** Welcome email for marketing site purchasers with install instructions */
37
- export function sendWelcomeWithInstall(email: string, tier: string): Promise<void> {
38
- return sendEmail("welcome-install", email, { tier });
39
- }
40
-
41
- /** Upgrade confirmation for in-app purchasers */
42
- export function sendUpgradeConfirmation(email: string, tier: string): Promise<void> {
43
- return sendEmail("upgrade-confirmation", email, { tier });
44
- }
45
-
46
- /** Memory cap warning (approaching limit) */
47
- export function sendMemoryWarning(
48
- email: string,
49
- profileName: string,
50
- current: number,
51
- limit: number
52
- ): Promise<void> {
53
- return sendEmail("memory-warning", email, { profileName, current, limit });
54
- }
@@ -1,80 +0,0 @@
1
- /**
2
- * Stripe product and price configuration.
3
- *
4
- * Price IDs are placeholders — replace with actual Stripe dashboard values
5
- * after creating products. The structure supports both monthly and annual
6
- * billing for each tier.
7
- */
8
-
9
- import type { LicenseTier } from "@/lib/license/tier-limits";
10
-
11
- export type BillingInterval = "monthly" | "annual";
12
-
13
- export interface StripeProduct {
14
- tier: Exclude<LicenseTier, "community">;
15
- name: string;
16
- description: string;
17
- prices: Record<BillingInterval, StripePrice>;
18
- paymentLinks: Record<BillingInterval, string>; // Static Stripe Payment Links for marketing site
19
- }
20
-
21
- export interface StripePrice {
22
- id: string; // Stripe Price ID (price_xxx)
23
- amount: number; // In cents
24
- currency: string;
25
- }
26
-
27
- /** Stripe product catalog — live Price IDs and Payment Links. */
28
- export const STRIPE_PRODUCTS: StripeProduct[] = [
29
- {
30
- tier: "solo",
31
- name: "Stagent Solo",
32
- description: "Power users — expanded limits and advanced history",
33
- prices: {
34
- monthly: { id: "price_1TJ2d5RCxnzBPkIX4SnajFok", amount: 1900, currency: "usd" },
35
- annual: { id: "price_1TJ2d5RCxnzBPkIXjjiyc7lb", amount: 19000, currency: "usd" },
36
- },
37
- paymentLinks: {
38
- monthly: "https://buy.stagent.io/fZufZjgKC4q9azrgDzdwc06",
39
- annual: "https://buy.stagent.io/bJe00l1PI7Cl7nf1IFdwc0b",
40
- },
41
- },
42
- {
43
- tier: "operator",
44
- name: "Stagent Operator",
45
- description: "Professionals — analytics, cloud sync, marketplace publishing",
46
- prices: {
47
- monthly: { id: "price_1TJ2e1RCxnzBPkIXZg47cNbO", amount: 4900, currency: "usd" },
48
- annual: { id: "price_1TJ2e1RCxnzBPkIXODs5fZW2", amount: 49000, currency: "usd" },
49
- },
50
- paymentLinks: {
51
- monthly: "https://buy.stagent.io/aFa4gB0LE9Kt22Vevrdwc07",
52
- annual: "https://buy.stagent.io/bJe6oJdyq2i1bDv1IFdwc0a",
53
- },
54
- },
55
- {
56
- tier: "scale",
57
- name: "Stagent Scale",
58
- description: "Teams — unlimited everything, featured marketplace, priority support",
59
- prices: {
60
- monthly: { id: "price_1TJ2evRCxnzBPkIXy9mBqBHB", amount: 9900, currency: "usd" },
61
- annual: { id: "price_1TJ2evRCxnzBPkIXqIRaDxQp", amount: 99000, currency: "usd" },
62
- },
63
- paymentLinks: {
64
- monthly: "https://buy.stagent.io/9B628t2TM5udazr72Zdwc08",
65
- annual: "https://buy.stagent.io/dRmfZjbqicWF5f7873dwc09",
66
- },
67
- },
68
- ];
69
-
70
- /** Map from Stripe Price ID → tier for webhook processing */
71
- export const PRICE_TO_TIER: Record<string, LicenseTier> = Object.fromEntries(
72
- STRIPE_PRODUCTS.flatMap((p) =>
73
- Object.values(p.prices).map((price) => [price.id, p.tier])
74
- )
75
- );
76
-
77
- /** Get the product for a specific tier */
78
- export function getProductForTier(tier: Exclude<LicenseTier, "community">): StripeProduct | undefined {
79
- return STRIPE_PRODUCTS.find((p) => p.tier === tier);
80
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * Stripe billing helpers.
3
- *
4
- * All Stripe API calls go through Supabase Edge Functions —
5
- * no Stripe secret key in the local app. This module calls
6
- * Edge Functions to create Checkout Sessions and Portal Sessions.
7
- */
8
-
9
- import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
10
- import { getProductForTier } from "./products";
11
- import type { LicenseTier } from "@/lib/license/tier-limits";
12
- import type { BillingInterval } from "./products";
13
-
14
- /**
15
- * Create a Stripe Checkout Session for in-app upgrade.
16
- * Returns the checkout URL to redirect the user to.
17
- */
18
- export async function createCheckoutSession(
19
- tier: Exclude<LicenseTier, "community">,
20
- billingPeriod: BillingInterval = "monthly",
21
- returnUrl?: string
22
- ): Promise<{ url: string } | { error: string }> {
23
- if (!isCloudConfigured()) {
24
- return { error: "Cloud backend not configured" };
25
- }
26
-
27
- const product = getProductForTier(tier);
28
- if (!product) {
29
- return { error: `Unknown tier: ${tier}` };
30
- }
31
-
32
- const priceId = product.prices[billingPeriod].id;
33
- const supabaseUrl = getSupabaseUrl();
34
- const anonKey = getSupabaseAnonKey();
35
-
36
- try {
37
- const response = await fetch(
38
- `${supabaseUrl}/functions/v1/create-checkout-session`,
39
- {
40
- method: "POST",
41
- headers: {
42
- "Content-Type": "application/json",
43
- Authorization: `Bearer ${anonKey}`,
44
- },
45
- body: JSON.stringify({
46
- priceId,
47
- returnUrl: returnUrl ?? `${typeof window !== "undefined" ? window.location.origin : "http://localhost:3000"}/settings`,
48
- }),
49
- }
50
- );
51
-
52
- if (!response.ok) {
53
- const data = await response.json().catch(() => ({}));
54
- return { error: data.error ?? `HTTP ${response.status}` };
55
- }
56
-
57
- const data = await response.json();
58
- return { url: data.url };
59
- } catch (err) {
60
- return { error: err instanceof Error ? err.message : "Checkout failed" };
61
- }
62
- }
63
-
64
- /**
65
- * Create a Stripe Customer Portal session for billing management.
66
- * Returns the portal URL to redirect the user to.
67
- */
68
- export async function createPortalSession(
69
- email: string
70
- ): Promise<{ url: string } | { error: string }> {
71
- if (!isCloudConfigured()) {
72
- return { error: "Cloud backend not configured" };
73
- }
74
-
75
- const supabaseUrl = getSupabaseUrl();
76
- const anonKey = getSupabaseAnonKey();
77
-
78
- try {
79
- const response = await fetch(
80
- `${supabaseUrl}/functions/v1/create-portal-session`,
81
- {
82
- method: "POST",
83
- headers: {
84
- "Content-Type": "application/json",
85
- Authorization: `Bearer ${anonKey}`,
86
- },
87
- body: JSON.stringify({ email }),
88
- }
89
- );
90
-
91
- if (!response.ok) {
92
- const data = await response.json().catch(() => ({}));
93
- return { error: data.error ?? `HTTP ${response.status}` };
94
- }
95
-
96
- const data = await response.json();
97
- return { url: data.url };
98
- } catch (err) {
99
- return { error: err instanceof Error ? err.message : "Portal failed" };
100
- }
101
- }
@@ -1,32 +0,0 @@
1
- "use client";
2
-
3
- /**
4
- * Browser-side Supabase client with session persistence.
5
- *
6
- * Used for auth flows (magic link sign-in) and authenticated operations
7
- * (Storage uploads for cloud sync). The server-side client in
8
- * supabase-client.ts is for server components and API routes.
9
- */
10
-
11
- import { createClient, type SupabaseClient } from "@supabase/supabase-js";
12
- import { getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
13
-
14
- let browserClient: SupabaseClient | null = null;
15
-
16
- /**
17
- * Get the browser-side Supabase client with session persistence.
18
- * Sessions are stored in localStorage and auto-refreshed.
19
- */
20
- export function getSupabaseBrowserClient(): SupabaseClient {
21
- if (browserClient) return browserClient;
22
-
23
- browserClient = createClient(getSupabaseUrl(), getSupabaseAnonKey(), {
24
- auth: {
25
- autoRefreshToken: true,
26
- persistSession: true,
27
- storageKey: "stagent-auth",
28
- },
29
- });
30
-
31
- return browserClient;
32
- }
@@ -1,56 +0,0 @@
1
- /**
2
- * Supabase client singleton for cloud features.
3
- *
4
- * Connects to the Stagent cloud backend for license validation,
5
- * marketplace, telemetry, and cloud sync. The anon key is safe to
6
- * embed — Row Level Security policies protect all data.
7
- *
8
- * Env vars override the defaults (for self-hosted or development).
9
- */
10
-
11
- import { createClient, type SupabaseClient } from "@supabase/supabase-js";
12
-
13
- /** Production Stagent cloud backend — safe to embed (anon key, RLS-protected) */
14
- const DEFAULT_SUPABASE_URL = "https://yznantjbmacbllhcyzwc.supabase.co";
15
- const DEFAULT_SUPABASE_ANON_KEY =
16
- "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl6bmFudGpibWFjYmxsaGN5endjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1MDg1ODMsImV4cCI6MjA4ODA4NDU4M30.i-P7MXpR1_emBjhUkzbFeSX7fgjgPDv90_wkqF7sW3Y";
17
-
18
- /** Resolved Supabase URL (env override or production default) */
19
- export function getSupabaseUrl(): string {
20
- return process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL;
21
- }
22
-
23
- /** Resolved Supabase anon key (env override or production default) */
24
- export function getSupabaseAnonKey(): string {
25
- return process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
26
- }
27
-
28
- let client: SupabaseClient | null = null;
29
- let initialized = false;
30
-
31
- /**
32
- * Get the Supabase client. Uses the production Stagent backend by default.
33
- * Override with NEXT_PUBLIC_SUPABASE_URL / NEXT_PUBLIC_SUPABASE_ANON_KEY
34
- * env vars for self-hosted or development setups.
35
- */
36
- export function getSupabaseClient(): SupabaseClient | null {
37
- if (initialized) return client;
38
- initialized = true;
39
-
40
- client = createClient(getSupabaseUrl(), getSupabaseAnonKey(), {
41
- auth: {
42
- autoRefreshToken: true,
43
- persistSession: false, // Server-side — no browser session
44
- },
45
- });
46
-
47
- return client;
48
- }
49
-
50
- /**
51
- * Cloud is always configured — defaults to Stagent production backend.
52
- * Returns false only if explicitly disabled via STAGENT_CLOUD_DISABLED=true.
53
- */
54
- export function isCloudConfigured(): boolean {
55
- return process.env.STAGENT_CLOUD_DISABLED !== "true";
56
- }
@@ -1,56 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { canAccessFeature, LICENSE_FEATURES, type LicenseFeature } from "../features";
3
- import type { LicenseTier } from "../tier-limits";
4
-
5
- describe("features", () => {
6
- it("community can browse marketplace", () => {
7
- expect(canAccessFeature("community", "marketplace-browse")).toBe(true);
8
- });
9
-
10
- it("community cannot access analytics", () => {
11
- expect(canAccessFeature("community", "analytics")).toBe(false);
12
- });
13
-
14
- it("community cannot access cloud sync", () => {
15
- expect(canAccessFeature("community", "cloud-sync")).toBe(false);
16
- });
17
-
18
- it("solo can import from marketplace", () => {
19
- expect(canAccessFeature("solo", "marketplace-import")).toBe(true);
20
- });
21
-
22
- it("solo cannot publish to marketplace", () => {
23
- expect(canAccessFeature("solo", "marketplace-publish")).toBe(false);
24
- });
25
-
26
- it("operator can access analytics and cloud sync", () => {
27
- expect(canAccessFeature("operator", "analytics")).toBe(true);
28
- expect(canAccessFeature("operator", "cloud-sync")).toBe(true);
29
- expect(canAccessFeature("operator", "marketplace-publish")).toBe(true);
30
- });
31
-
32
- it("scale can access everything", () => {
33
- for (const feature of LICENSE_FEATURES) {
34
- expect(canAccessFeature("scale", feature)).toBe(true);
35
- }
36
- });
37
-
38
- it("higher tiers inherit lower tier features", () => {
39
- const tiers: LicenseTier[] = ["community", "solo", "operator", "scale"];
40
- for (const feature of LICENSE_FEATURES) {
41
- let unlocked = false;
42
- for (const tier of tiers) {
43
- if (canAccessFeature(tier, feature)) {
44
- unlocked = true;
45
- }
46
- // Once unlocked, must stay unlocked for all higher tiers
47
- if (unlocked) {
48
- expect(
49
- canAccessFeature(tier, feature),
50
- `${tier} should access ${feature} since a lower tier can`
51
- ).toBe(true);
52
- }
53
- }
54
- }
55
- });
56
- });
@@ -1,88 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import { generateLicenseKey, validateLicenseKey, formatKeyInput } from "../key-format";
3
-
4
- describe("key-format", () => {
5
- describe("generateLicenseKey", () => {
6
- it("generates a key in STAG-XXXX-XXXX-XXXX-XXXX format", () => {
7
- const key = generateLicenseKey();
8
- expect(key).toMatch(/^STAG-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}-[A-HJ-NP-Z2-9]{4}$/);
9
- });
10
-
11
- it("generates keys that pass validation", () => {
12
- for (let i = 0; i < 20; i++) {
13
- const key = generateLicenseKey();
14
- const result = validateLicenseKey(key);
15
- expect(result.valid, `Key ${key} should be valid`).toBe(true);
16
- }
17
- });
18
-
19
- it("generates unique keys", () => {
20
- const keys = new Set<string>();
21
- for (let i = 0; i < 50; i++) {
22
- keys.add(generateLicenseKey());
23
- }
24
- expect(keys.size).toBe(50);
25
- });
26
- });
27
-
28
- describe("validateLicenseKey", () => {
29
- it("accepts a valid generated key", () => {
30
- const key = generateLicenseKey();
31
- expect(validateLicenseKey(key)).toEqual({ valid: true });
32
- });
33
-
34
- it("rejects empty string", () => {
35
- expect(validateLicenseKey("")).toEqual({
36
- valid: false,
37
- error: expect.stringContaining("Invalid format"),
38
- });
39
- });
40
-
41
- it("rejects wrong prefix", () => {
42
- expect(validateLicenseKey("XXXX-ABCD-EFGH-JKMN-PQRS")).toEqual({
43
- valid: false,
44
- error: expect.stringContaining("Invalid format"),
45
- });
46
- });
47
-
48
- it("rejects ambiguous characters (0, O, 1, I, L)", () => {
49
- expect(validateLicenseKey("STAG-0OIL-ABCD-EFGH-JKMN")).toEqual({
50
- valid: false,
51
- error: expect.stringContaining("Invalid format"),
52
- });
53
- });
54
-
55
- it("rejects a key with a bad checksum", () => {
56
- const key = generateLicenseKey();
57
- // Flip the last character
58
- const bad = key.slice(0, -1) + (key.slice(-1) === "A" ? "B" : "A");
59
- const result = validateLicenseKey(bad);
60
- expect(result.valid).toBe(false);
61
- });
62
-
63
- it("accepts lowercase input (case-insensitive)", () => {
64
- const key = generateLicenseKey();
65
- expect(validateLicenseKey(key.toLowerCase())).toEqual({ valid: true });
66
- });
67
- });
68
-
69
- describe("formatKeyInput", () => {
70
- it("adds dashes and STAG prefix", () => {
71
- expect(formatKeyInput("ABCD")).toBe("STAG-ABCD");
72
- expect(formatKeyInput("ABCDEFGH")).toBe("STAG-ABCD-EFGH");
73
- });
74
-
75
- it("strips invalid characters", () => {
76
- expect(formatKeyInput("AB!@CD")).toBe("STAG-ABCD");
77
- });
78
-
79
- it("converts to uppercase", () => {
80
- expect(formatKeyInput("abcd")).toBe("STAG-ABCD");
81
- });
82
-
83
- it("limits to 16 characters", () => {
84
- expect(formatKeyInput("ABCDEFGHJKMNPQRS")).toBe("STAG-ABCD-EFGH-JKMN-PQRS");
85
- expect(formatKeyInput("ABCDEFGHJKMNPQRSTUV")).toBe("STAG-ABCD-EFGH-JKMN-PQRS");
86
- });
87
- });
88
- });