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,64 +0,0 @@
1
- import { afterEach, beforeEach, describe, expect, it } from "vitest";
2
- import { eq } from "drizzle-orm";
3
- import { db } from "@/lib/db";
4
- import { license as licenseTable } from "@/lib/db/schema";
5
- import { licenseManager } from "../manager";
6
-
7
- const LICENSE_ROW_ID = "default";
8
-
9
- /**
10
- * Regression: under Turbopack module instance separation, the singleton's
11
- * in-memory cache could disagree with the DB. If another module instance
12
- * (or another process) updates the license row, getTier() must reflect
13
- * the new value — otherwise gated limits silently fall back to community
14
- * tier rules even on paid tiers.
15
- *
16
- * The fix in manager.ts makes getTier() / getStatus() read from the DB.
17
- * This test simulates the cross-instance scenario by mutating the DB row
18
- * directly without going through licenseManager.activate().
19
- */
20
- describe("LicenseManager — DB-direct tier reads", () => {
21
- beforeEach(() => {
22
- // Reset to a known community baseline.
23
- db.delete(licenseTable).where(eq(licenseTable.id, LICENSE_ROW_ID)).run();
24
- licenseManager.initialize();
25
- });
26
-
27
- afterEach(() => {
28
- db.delete(licenseTable).where(eq(licenseTable.id, LICENSE_ROW_ID)).run();
29
- });
30
-
31
- it("getTier() reflects DB updates that bypassed activate()", () => {
32
- expect(licenseManager.getTier()).toBe("community");
33
-
34
- // Simulate "another module instance wrote scale to the DB".
35
- db.update(licenseTable)
36
- .set({ tier: "scale", status: "active", updatedAt: new Date() })
37
- .where(eq(licenseTable.id, LICENSE_ROW_ID))
38
- .run();
39
-
40
- expect(licenseManager.getTier()).toBe("scale");
41
- expect(licenseManager.getLimit("agentMemories")).toBe(Infinity);
42
- expect(licenseManager.getLimit("activeSchedules")).toBe(Infinity);
43
- expect(licenseManager.getLimit("parallelWorkflows")).toBe(Infinity);
44
- expect(licenseManager.isPremium()).toBe(true);
45
- });
46
-
47
- it("getStatus() also reads from DB, not stale cache", () => {
48
- expect(licenseManager.getStatus().tier).toBe("community");
49
-
50
- db.update(licenseTable)
51
- .set({
52
- tier: "operator",
53
- status: "active",
54
- email: "ops@example.com",
55
- updatedAt: new Date(),
56
- })
57
- .where(eq(licenseTable.id, LICENSE_ROW_ID))
58
- .run();
59
-
60
- const status = licenseManager.getStatus();
61
- expect(status.tier).toBe("operator");
62
- expect(status.email).toBe("ops@example.com");
63
- });
64
- });
@@ -1,79 +0,0 @@
1
- import { describe, expect, it } from "vitest";
2
- import {
3
- TIERS,
4
- TIER_RANK,
5
- TIER_LIMITS,
6
- TIER_LABELS,
7
- TIER_PRICING,
8
- type LicenseTier,
9
- type LimitResource,
10
- } from "../tier-limits";
11
-
12
- describe("tier-limits", () => {
13
- it("defines all four tiers", () => {
14
- expect(TIERS).toEqual(["community", "solo", "operator", "scale"]);
15
- });
16
-
17
- it("ranks tiers in ascending order", () => {
18
- expect(TIER_RANK.community).toBeLessThan(TIER_RANK.solo);
19
- expect(TIER_RANK.solo).toBeLessThan(TIER_RANK.operator);
20
- expect(TIER_RANK.operator).toBeLessThan(TIER_RANK.scale);
21
- });
22
-
23
- it("all limit resources are defined for every tier", () => {
24
- const resources: LimitResource[] = [
25
- "agentMemories",
26
- "contextVersions",
27
- "activeSchedules",
28
- "historyRetentionDays",
29
- "parallelWorkflows",
30
- ];
31
- for (const tier of TIERS) {
32
- for (const resource of resources) {
33
- expect(TIER_LIMITS[tier][resource]).toBeDefined();
34
- expect(typeof TIER_LIMITS[tier][resource]).toBe("number");
35
- }
36
- }
37
- });
38
-
39
- it("higher tiers have equal or greater limits", () => {
40
- const tierOrder: LicenseTier[] = ["community", "solo", "operator", "scale"];
41
- for (let i = 1; i < tierOrder.length; i++) {
42
- const current = tierOrder[i];
43
- const previous = tierOrder[i - 1];
44
- for (const resource of Object.keys(TIER_LIMITS[current]) as LimitResource[]) {
45
- expect(TIER_LIMITS[current][resource]).toBeGreaterThanOrEqual(
46
- TIER_LIMITS[previous][resource]
47
- );
48
- }
49
- }
50
- });
51
-
52
- it("scale tier has Infinity for all resources", () => {
53
- for (const value of Object.values(TIER_LIMITS.scale)) {
54
- expect(value).toBe(Infinity);
55
- }
56
- });
57
-
58
- it("community tier has finite limits", () => {
59
- for (const value of Object.values(TIER_LIMITS.community)) {
60
- expect(Number.isFinite(value)).toBe(true);
61
- }
62
- });
63
-
64
- it("labels exist for all tiers", () => {
65
- for (const tier of TIERS) {
66
- expect(TIER_LABELS[tier]).toBeTruthy();
67
- }
68
- });
69
-
70
- it("pricing exists for all tiers with annual < 12x monthly", () => {
71
- for (const tier of TIERS) {
72
- const { monthly, annual } = TIER_PRICING[tier];
73
- expect(monthly).toBeGreaterThanOrEqual(0);
74
- if (monthly > 0) {
75
- expect(annual).toBeLessThan(monthly * 12);
76
- }
77
- }
78
- });
79
- });
@@ -1,60 +0,0 @@
1
- /**
2
- * Cloud license validation via Supabase Edge Function.
3
- *
4
- * Calls the validate-license Edge Function to check if the user's
5
- * email has an active subscription. Uses the same Supabase client
6
- * defaults as supabase-client.ts (production backend by default).
7
- */
8
-
9
- import { isCloudConfigured, getSupabaseUrl, getSupabaseAnonKey } from "@/lib/cloud/supabase-client";
10
- import type { LicenseTier } from "./tier-limits";
11
-
12
- export interface CloudValidationResult {
13
- valid: boolean;
14
- tier: LicenseTier;
15
- expiresAt?: Date;
16
- error?: string;
17
- }
18
-
19
- /**
20
- * Validate a license against the cloud backend.
21
- * Returns { valid: false } if cloud is disabled or email is empty.
22
- */
23
- export async function validateLicenseWithCloud(
24
- email: string
25
- ): Promise<CloudValidationResult> {
26
- if (!isCloudConfigured() || !email) {
27
- return { valid: false, tier: "community", error: "Cloud disabled or no email" };
28
- }
29
-
30
- const supabaseUrl = getSupabaseUrl();
31
- const anonKey = getSupabaseAnonKey();
32
-
33
- try {
34
- const response = await fetch(`${supabaseUrl}/functions/v1/validate-license`, {
35
- method: "POST",
36
- headers: {
37
- "Content-Type": "application/json",
38
- Authorization: `Bearer ${anonKey}`,
39
- },
40
- body: JSON.stringify({ email }),
41
- });
42
-
43
- if (!response.ok) {
44
- return { valid: false, tier: "community", error: `HTTP ${response.status}` };
45
- }
46
-
47
- const data = await response.json();
48
- return {
49
- valid: data.valid === true,
50
- tier: data.tier ?? "community",
51
- expiresAt: data.expiresAt ? new Date(data.expiresAt) : undefined,
52
- };
53
- } catch (err) {
54
- return {
55
- valid: false,
56
- tier: "community",
57
- error: err instanceof Error ? err.message : "Unknown error",
58
- };
59
- }
60
- }
@@ -1,44 +0,0 @@
1
- /**
2
- * Feature→tier gating map.
3
- *
4
- * Each feature has a minimum tier required to access it.
5
- * Use LicenseManager.isFeatureAllowed(feature) to check access.
6
- */
7
-
8
- import { type LicenseTier, TIER_RANK } from "./tier-limits";
9
-
10
- export const LICENSE_FEATURES = [
11
- "analytics",
12
- "cloud-sync",
13
- "marketplace-browse",
14
- "marketplace-import",
15
- "marketplace-publish",
16
- "marketplace-featured",
17
- "telemetry-benchmarks",
18
- "advanced-history",
19
- "priority-support",
20
- ] as const;
21
-
22
- export type LicenseFeature = (typeof LICENSE_FEATURES)[number];
23
-
24
- /** Minimum tier required to access each feature */
25
- const FEATURE_MIN_TIER: Record<LicenseFeature, LicenseTier> = {
26
- "analytics": "operator",
27
- "cloud-sync": "operator",
28
- "marketplace-browse": "community",
29
- "marketplace-import": "solo",
30
- "marketplace-publish": "operator",
31
- "marketplace-featured": "scale",
32
- "telemetry-benchmarks": "solo",
33
- "advanced-history": "solo",
34
- "priority-support": "scale",
35
- } as const;
36
-
37
- /**
38
- * Check if a tier can access a feature.
39
- * Pure function — no side effects or DB access.
40
- */
41
- export function canAccessFeature(tier: LicenseTier, feature: LicenseFeature): boolean {
42
- const minTier = FEATURE_MIN_TIER[feature];
43
- return TIER_RANK[tier] >= TIER_RANK[minTier];
44
- }
@@ -1,101 +0,0 @@
1
- /**
2
- * License key format: STAG-XXXX-XXXX-XXXX-XXXX
3
- *
4
- * - 16 characters from an unambiguous alphabet (no 0/O, 1/I/L)
5
- * - Last 2 characters are a CRC-16 checksum
6
- * - Keys are always uppercase
7
- */
8
-
9
- // Unambiguous character set (no 0, O, 1, I, L)
10
- const ALPHABET = "ABCDEFGHJKMNPQRSTUVWXYZ23456789";
11
-
12
- /**
13
- * Compute a simple CRC-16 checksum for a string.
14
- */
15
- function crc16(input: string): number {
16
- let crc = 0xffff;
17
- for (let i = 0; i < input.length; i++) {
18
- crc ^= input.charCodeAt(i) << 8;
19
- for (let j = 0; j < 8; j++) {
20
- if (crc & 0x8000) {
21
- crc = ((crc << 1) ^ 0x1021) & 0xffff;
22
- } else {
23
- crc = (crc << 1) & 0xffff;
24
- }
25
- }
26
- }
27
- return crc;
28
- }
29
-
30
- /**
31
- * Generate a license key: STAG-XXXX-XXXX-XXXX-XXXX
32
- * The last 2 characters encode the CRC-16 checksum.
33
- */
34
- export function generateLicenseKey(): string {
35
- const chars: string[] = [];
36
- for (let i = 0; i < 14; i++) {
37
- chars.push(ALPHABET[Math.floor(Math.random() * ALPHABET.length)]);
38
- }
39
-
40
- const payload = chars.join("");
41
- const checksum = crc16(payload);
42
-
43
- // Encode checksum as 2 characters from alphabet
44
- const c1 = ALPHABET[checksum % ALPHABET.length];
45
- const c2 = ALPHABET[Math.floor(checksum / ALPHABET.length) % ALPHABET.length];
46
- chars.push(c1, c2);
47
-
48
- // Format as STAG-XXXX-XXXX-XXXX-XXXX
49
- const raw = chars.join("");
50
- return `STAG-${raw.slice(0, 4)}-${raw.slice(4, 8)}-${raw.slice(8, 12)}-${raw.slice(12, 16)}`;
51
- }
52
-
53
- /**
54
- * Validate a license key format and checksum.
55
- * Returns true if the key is well-formed and the checksum matches.
56
- */
57
- export function validateLicenseKey(key: string): { valid: boolean; error?: string } {
58
- const normalized = key.toUpperCase().trim();
59
-
60
- // Check format
61
- const pattern = /^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}$/;
62
- if (!pattern.test(normalized)) {
63
- return { valid: false, error: "Invalid format (expected STAG-XXXX-XXXX-XXXX-XXXX)" };
64
- }
65
-
66
- // Extract raw characters (remove STAG- prefix and dashes)
67
- const raw = normalized.replace(/^STAG-/, "").replace(/-/g, "");
68
- if (raw.length !== 16) {
69
- return { valid: false, error: "Invalid key length" };
70
- }
71
-
72
- // Verify checksum
73
- const payload = raw.slice(0, 14);
74
- const checksum = crc16(payload);
75
- const c1 = ALPHABET[checksum % ALPHABET.length];
76
- const c2 = ALPHABET[Math.floor(checksum / ALPHABET.length) % ALPHABET.length];
77
-
78
- if (raw[14] !== c1 || raw[15] !== c2) {
79
- return { valid: false, error: "Invalid checksum" };
80
- }
81
-
82
- return { valid: true };
83
- }
84
-
85
- /**
86
- * Auto-format a partial key input with dashes.
87
- * Converts lowercase and strips invalid characters.
88
- */
89
- export function formatKeyInput(input: string): string {
90
- // Strip everything except alphanumeric
91
- const clean = input.toUpperCase().replace(/[^A-HJ-NP-Z2-9]/g, "");
92
-
93
- // Add STAG- prefix if not present
94
- const parts: string[] = [];
95
- for (let i = 0; i < clean.length && i < 16; i += 4) {
96
- parts.push(clean.slice(i, i + 4));
97
- }
98
-
99
- if (parts.length === 0) return "";
100
- return "STAG-" + parts.join("-");
101
- }
@@ -1,111 +0,0 @@
1
- /**
2
- * Shared limit enforcement helpers.
3
- *
4
- * Used by API routes and internal write sites to check tier limits
5
- * before allowing operations. Returns a structured result for
6
- * consistent 402 response formatting.
7
- */
8
-
9
- import { licenseManager } from "./manager";
10
- import { TIER_LIMITS, type LimitResource, TIER_LABELS } from "./tier-limits";
11
- import { createTierLimitNotification } from "./notifications";
12
- import { sendMemoryWarning } from "@/lib/billing/email";
13
- import { trackConversionEvent } from "@/lib/telemetry/conversion-events";
14
-
15
- export interface LimitCheckResult {
16
- allowed: boolean;
17
- current: number;
18
- limit: number;
19
- tier: string;
20
- requiredTier: string;
21
- }
22
-
23
- /**
24
- * Check if an operation is within the current tier's limit.
25
- *
26
- * @param resource - The limit resource key
27
- * @param currentCount - Current usage count
28
- * @returns Result with allowed flag and metadata for 402 responses
29
- */
30
- export function checkLimit(
31
- resource: LimitResource,
32
- currentCount: number
33
- ): LimitCheckResult {
34
- const tier = licenseManager.getTier();
35
- const limit = TIER_LIMITS[tier][resource];
36
-
37
- if (currentCount < limit) {
38
- return {
39
- allowed: true,
40
- current: currentCount,
41
- limit: limit === Infinity ? -1 : limit,
42
- tier,
43
- requiredTier: tier,
44
- };
45
- }
46
-
47
- // Find the next tier that allows more
48
- const tiers = ["community", "solo", "operator", "scale"] as const;
49
- const tierIndex = tiers.indexOf(tier);
50
- let requiredTier = "scale";
51
- for (let i = tierIndex + 1; i < tiers.length; i++) {
52
- if (TIER_LIMITS[tiers[i]][resource] > currentCount) {
53
- requiredTier = tiers[i];
54
- break;
55
- }
56
- }
57
-
58
- return {
59
- allowed: false,
60
- current: currentCount,
61
- limit: limit === Infinity ? -1 : limit,
62
- tier,
63
- requiredTier,
64
- };
65
- }
66
-
67
- /**
68
- * Check a limit and emit a tier_limit notification if blocked.
69
- * Convenience wrapper that combines checkLimit + notification.
70
- */
71
- export async function checkLimitAndNotify(
72
- resource: LimitResource,
73
- currentCount: number,
74
- taskId?: string
75
- ): Promise<LimitCheckResult> {
76
- const result = checkLimit(resource, currentCount);
77
- if (!result.allowed) {
78
- await createTierLimitNotification(resource, currentCount, result.limit, taskId);
79
- trackConversionEvent("limit_hit", resource, { current: currentCount, limit: result.limit });
80
- }
81
-
82
- // Send email warning when approaching memory limit (75%+)
83
- if (resource === "agentMemories" && result.allowed && result.limit > 0) {
84
- const ratio = currentCount / result.limit;
85
- if (ratio >= 0.75) {
86
- const email = licenseManager.getStatus().email;
87
- if (email) {
88
- sendMemoryWarning(email, "agent", currentCount, result.limit).catch(() => {});
89
- }
90
- }
91
- }
92
-
93
- return result;
94
- }
95
-
96
- /**
97
- * Build a standard 402 response body for limit violations.
98
- */
99
- export function buildLimitErrorBody(
100
- resource: LimitResource,
101
- result: LimitCheckResult
102
- ): Record<string, unknown> {
103
- return {
104
- error: `${TIER_LABELS[result.tier as keyof typeof TIER_LABELS] ?? result.tier} tier limit reached for ${resource}`,
105
- upgradeUrl: "/settings",
106
- requiredTier: result.requiredTier,
107
- limitType: resource,
108
- current: result.current,
109
- max: result.limit,
110
- };
111
- }
@@ -1,51 +0,0 @@
1
- /**
2
- * Count queries for tier limit enforcement.
3
- *
4
- * Each function returns the current count for a specific resource,
5
- * used with checkLimit() to determine if an operation is allowed.
6
- */
7
-
8
- import { db } from "@/lib/db";
9
- import { agentMemory, learnedContext, schedules } from "@/lib/db/schema";
10
- import { eq, and, sql } from "drizzle-orm";
11
-
12
- /**
13
- * Count active (non-archived) memories for a profile.
14
- */
15
- export function getMemoryCount(profileId: string): number {
16
- const result = db
17
- .select({ count: sql<number>`count(*)` })
18
- .from(agentMemory)
19
- .where(
20
- and(
21
- eq(agentMemory.profileId, profileId),
22
- eq(agentMemory.status, "active")
23
- )
24
- )
25
- .get();
26
- return result?.count ?? 0;
27
- }
28
-
29
- /**
30
- * Count learned context versions for a profile.
31
- */
32
- export function getContextVersionCount(profileId: string): number {
33
- const result = db
34
- .select({ count: sql<number>`count(*)` })
35
- .from(learnedContext)
36
- .where(eq(learnedContext.profileId, profileId))
37
- .get();
38
- return result?.count ?? 0;
39
- }
40
-
41
- /**
42
- * Count active schedules (both scheduled and heartbeat types).
43
- */
44
- export function getActiveScheduleCount(): number {
45
- const result = db
46
- .select({ count: sql<number>`count(*)` })
47
- .from(schedules)
48
- .where(eq(schedules.status, "active"))
49
- .get();
50
- return result?.count ?? 0;
51
- }