stagent 0.8.0 → 0.9.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 (75) hide show
  1. package/README.md +41 -0
  2. package/dist/cli.js +19 -2
  3. package/package.json +2 -1
  4. package/src/app/analytics/page.tsx +60 -0
  5. package/src/app/api/license/checkout/route.ts +28 -0
  6. package/src/app/api/license/portal/route.ts +26 -0
  7. package/src/app/api/license/route.ts +88 -0
  8. package/src/app/api/license/usage/route.ts +63 -0
  9. package/src/app/api/marketplace/browse/route.ts +15 -0
  10. package/src/app/api/marketplace/import/route.ts +28 -0
  11. package/src/app/api/marketplace/publish/route.ts +40 -0
  12. package/src/app/api/memory/route.ts +11 -0
  13. package/src/app/api/onboarding/email/route.ts +53 -0
  14. package/src/app/api/onboarding/progress/route.ts +60 -0
  15. package/src/app/api/schedules/route.ts +11 -0
  16. package/src/app/api/settings/telemetry/route.ts +14 -0
  17. package/src/app/api/sync/export/route.ts +54 -0
  18. package/src/app/api/sync/restore/route.ts +37 -0
  19. package/src/app/api/sync/sessions/route.ts +24 -0
  20. package/src/app/api/tasks/[id]/execute/route.ts +21 -0
  21. package/src/app/auth/callback/route.ts +79 -0
  22. package/src/app/marketplace/page.tsx +19 -0
  23. package/src/app/page.tsx +6 -2
  24. package/src/app/settings/page.tsx +8 -0
  25. package/src/components/analytics/analytics-dashboard.tsx +200 -0
  26. package/src/components/analytics/analytics-gate-card.tsx +101 -0
  27. package/src/components/marketplace/blueprint-card.tsx +61 -0
  28. package/src/components/marketplace/marketplace-browser.tsx +131 -0
  29. package/src/components/onboarding/activation-checklist.tsx +64 -0
  30. package/src/components/onboarding/donut-ring.tsx +52 -0
  31. package/src/components/onboarding/email-capture-card.tsx +104 -0
  32. package/src/components/settings/activation-form.tsx +95 -0
  33. package/src/components/settings/cloud-account-section.tsx +145 -0
  34. package/src/components/settings/cloud-sync-section.tsx +155 -0
  35. package/src/components/settings/subscription-section.tsx +410 -0
  36. package/src/components/settings/telemetry-section.tsx +80 -0
  37. package/src/components/shared/app-sidebar.tsx +136 -29
  38. package/src/components/shared/premium-gate-overlay.tsx +50 -0
  39. package/src/components/shared/schedule-gate-dialog.tsx +64 -0
  40. package/src/components/shared/upgrade-banner.tsx +112 -0
  41. package/src/hooks/use-snoozed-banners.ts +73 -0
  42. package/src/hooks/use-supabase-auth.ts +79 -0
  43. package/src/instrumentation.ts +34 -0
  44. package/src/lib/agents/__tests__/execution-manager.test.ts +28 -1
  45. package/src/lib/agents/__tests__/learned-context.test.ts +13 -0
  46. package/src/lib/agents/execution-manager.ts +35 -0
  47. package/src/lib/agents/learned-context.ts +13 -0
  48. package/src/lib/analytics/queries.ts +207 -0
  49. package/src/lib/billing/email.ts +54 -0
  50. package/src/lib/billing/products.ts +80 -0
  51. package/src/lib/billing/stripe.ts +101 -0
  52. package/src/lib/cloud/supabase-browser.ts +38 -0
  53. package/src/lib/cloud/supabase-client.ts +49 -0
  54. package/src/lib/constants/settings.ts +18 -0
  55. package/src/lib/data/clear.ts +5 -0
  56. package/src/lib/db/bootstrap.ts +16 -0
  57. package/src/lib/db/schema.ts +24 -0
  58. package/src/lib/license/__tests__/features.test.ts +56 -0
  59. package/src/lib/license/__tests__/key-format.test.ts +88 -0
  60. package/src/lib/license/__tests__/tier-limits.test.ts +79 -0
  61. package/src/lib/license/cloud-validation.ts +64 -0
  62. package/src/lib/license/features.ts +44 -0
  63. package/src/lib/license/key-format.ts +101 -0
  64. package/src/lib/license/limit-check.ts +111 -0
  65. package/src/lib/license/limit-queries.ts +51 -0
  66. package/src/lib/license/manager.ts +290 -0
  67. package/src/lib/license/notifications.ts +59 -0
  68. package/src/lib/license/tier-limits.ts +71 -0
  69. package/src/lib/marketplace/marketplace-client.ts +107 -0
  70. package/src/lib/sync/cloud-sync.ts +237 -0
  71. package/src/lib/telemetry/conversion-events.ts +73 -0
  72. package/src/lib/telemetry/queue.ts +122 -0
  73. package/src/lib/usage/ledger.ts +18 -0
  74. package/src/lib/validators/license.ts +33 -0
  75. package/tsconfig.json +2 -1
@@ -0,0 +1,155 @@
1
+ "use client";
2
+
3
+ import { useState, useEffect } from "react";
4
+ import { Cloud, Upload, Download } from "lucide-react";
5
+ import { getSupabaseBrowserClient } from "@/lib/cloud/supabase-browser";
6
+ import { toast } from "sonner";
7
+ import {
8
+ Card,
9
+ CardContent,
10
+ CardDescription,
11
+ CardHeader,
12
+ CardTitle,
13
+ } from "@/components/ui/card";
14
+ import { Button } from "@/components/ui/button";
15
+ import { Badge } from "@/components/ui/badge";
16
+
17
+ interface SyncSession {
18
+ id: string;
19
+ device_name: string;
20
+ sync_type: string;
21
+ blob_size_bytes: number;
22
+ created_at: string;
23
+ }
24
+
25
+ export function CloudSyncSection() {
26
+ const [sessions, setSessions] = useState<SyncSession[]>([]);
27
+ const [exporting, setExporting] = useState(false);
28
+ const [restoring, setRestoring] = useState(false);
29
+
30
+ useEffect(() => {
31
+ fetch("/api/sync/sessions")
32
+ .then((r) => (r.ok ? r.json() : { sessions: [] }))
33
+ .then((d) => setSessions(d.sessions ?? []))
34
+ .catch(() => {});
35
+ }, []);
36
+
37
+ async function handleExport() {
38
+ setExporting(true);
39
+ try {
40
+ // Get the user's access token for authenticated Storage uploads
41
+ const supabase = getSupabaseBrowserClient();
42
+ const { data: { session } } = await supabase.auth.getSession();
43
+ const accessToken = session?.access_token;
44
+
45
+ const res = await fetch("/api/sync/export", {
46
+ method: "POST",
47
+ headers: { "Content-Type": "application/json" },
48
+ body: JSON.stringify({ accessToken }),
49
+ });
50
+ const data = await res.json();
51
+ if (res.ok) {
52
+ toast.success(`Backup uploaded (${formatBytes(data.sizeBytes)})`);
53
+ // Refresh sessions
54
+ const sessRes = await fetch("/api/sync/sessions");
55
+ if (sessRes.ok) setSessions((await sessRes.json()).sessions ?? []);
56
+ } else {
57
+ toast.error(data.error ?? "Export failed");
58
+ }
59
+ } catch {
60
+ toast.error("Failed to export");
61
+ } finally {
62
+ setExporting(false);
63
+ }
64
+ }
65
+
66
+ async function handleRestore() {
67
+ if (!confirm("This will replace your local database with the latest cloud backup. A safety backup will be created first. Continue?")) {
68
+ return;
69
+ }
70
+ setRestoring(true);
71
+ try {
72
+ const res = await fetch("/api/sync/restore", { method: "POST" });
73
+ const data = await res.json();
74
+ if (res.ok) {
75
+ toast.success("Database restored. Restart the app to apply changes.");
76
+ } else {
77
+ toast.error(data.error ?? "Restore failed");
78
+ }
79
+ } catch {
80
+ toast.error("Failed to restore");
81
+ } finally {
82
+ setRestoring(false);
83
+ }
84
+ }
85
+
86
+ return (
87
+ <Card>
88
+ <CardHeader>
89
+ <CardTitle className="flex items-center gap-2">
90
+ <Cloud className="h-5 w-5" />
91
+ Cloud Sync
92
+ </CardTitle>
93
+ <CardDescription>
94
+ Encrypted database backup and restore via Supabase Storage
95
+ </CardDescription>
96
+ </CardHeader>
97
+ <CardContent className="space-y-4">
98
+ <div className="flex gap-3">
99
+ <Button
100
+ variant="outline"
101
+ size="sm"
102
+ onClick={handleExport}
103
+ disabled={exporting || restoring}
104
+ >
105
+ <Upload className="h-3.5 w-3.5 mr-1.5" />
106
+ {exporting ? "Exporting..." : "Backup Now"}
107
+ </Button>
108
+ <Button
109
+ variant="outline"
110
+ size="sm"
111
+ onClick={handleRestore}
112
+ disabled={exporting || restoring || sessions.length === 0}
113
+ >
114
+ <Download className="h-3.5 w-3.5 mr-1.5" />
115
+ {restoring ? "Restoring..." : "Restore Latest"}
116
+ </Button>
117
+ </div>
118
+
119
+ {sessions.length > 0 && (
120
+ <div className="space-y-2">
121
+ <h4 className="text-xs font-medium text-muted-foreground">Recent Syncs</h4>
122
+ <div className="space-y-1">
123
+ {sessions.slice(0, 5).map((s) => (
124
+ <div key={s.id} className="flex items-center justify-between text-xs py-1">
125
+ <div className="flex items-center gap-2">
126
+ <Badge variant={s.sync_type === "backup" ? "secondary" : "outline"} className="text-[10px]">
127
+ {s.sync_type}
128
+ </Badge>
129
+ <span className="text-muted-foreground">{s.device_name}</span>
130
+ </div>
131
+ <div className="flex items-center gap-2 text-muted-foreground">
132
+ <span>{formatBytes(s.blob_size_bytes)}</span>
133
+ <span>{new Date(s.created_at).toLocaleDateString()}</span>
134
+ </div>
135
+ </div>
136
+ ))}
137
+ </div>
138
+ </div>
139
+ )}
140
+
141
+ {sessions.length === 0 && (
142
+ <p className="text-xs text-muted-foreground">
143
+ No backups yet. Click "Backup Now" to create your first encrypted cloud backup.
144
+ </p>
145
+ )}
146
+ </CardContent>
147
+ </Card>
148
+ );
149
+ }
150
+
151
+ function formatBytes(bytes: number): string {
152
+ if (bytes < 1024) return `${bytes} B`;
153
+ if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
154
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
155
+ }
@@ -0,0 +1,410 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState, useCallback } from "react";
4
+ import { Crown, BarChart3, Brain, Calendar, Zap, ExternalLink, TrendingUp, Cloud, Store } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Button } from "@/components/ui/button";
14
+ import { Badge } from "@/components/ui/badge";
15
+ import { Progress } from "@/components/ui/progress";
16
+ import { TIER_LABELS, TIER_PRICING, type LicenseTier } from "@/lib/license/tier-limits";
17
+ import { ActivationForm } from "./activation-form";
18
+
19
+ interface LicenseStatus {
20
+ tier: LicenseTier;
21
+ status: string;
22
+ email: string | null;
23
+ isPremium: boolean;
24
+ activatedAt: string | null;
25
+ expiresAt: string | null;
26
+ limits: Record<string, number>;
27
+ features: Record<string, boolean>;
28
+ }
29
+
30
+ interface UsageData {
31
+ agentMemories: { current: number; limit: number };
32
+ contextVersions: { current: number; limit: number };
33
+ activeSchedules: { current: number; limit: number };
34
+ parallelWorkflows: { current: number; limit: number };
35
+ }
36
+
37
+ const TIER_BADGE_VARIANT: Record<LicenseTier, "secondary" | "default" | "destructive" | "outline"> = {
38
+ community: "secondary",
39
+ solo: "default",
40
+ operator: "default",
41
+ scale: "outline",
42
+ };
43
+
44
+ const USAGE_HINTS: Record<string, { sub: string; atLimit: string }> = {
45
+ agentMemories: {
46
+ sub: "Cumulative per profile. New writes blocked at limit — existing memories preserved.",
47
+ atLimit: "At capacity — archive old memories or upgrade to continue learning.",
48
+ },
49
+ contextVersions: {
50
+ sub: "Cumulative per profile. New proposals blocked at limit — existing versions preserved.",
51
+ atLimit: "At capacity — upgrade to unlock more self-improvement cycles.",
52
+ },
53
+ activeSchedules: {
54
+ sub: "Concurrent active schedules. Pausing a schedule frees a slot.",
55
+ atLimit: "All slots used — pause an existing schedule or upgrade for more.",
56
+ },
57
+ parallelWorkflows: {
58
+ sub: "Concurrent running workflows. Slots free when tasks complete.",
59
+ atLimit: "All slots busy — wait for a task to finish or upgrade.",
60
+ },
61
+ };
62
+
63
+ function UsageCard({
64
+ icon: Icon,
65
+ label,
66
+ current,
67
+ limit,
68
+ resourceKey,
69
+ }: {
70
+ icon: typeof Brain;
71
+ label: string;
72
+ current: number;
73
+ limit: number;
74
+ resourceKey: string;
75
+ }) {
76
+ const isUnlimited = limit === -1;
77
+ const ratio = isUnlimited ? 0 : (current / limit) * 100;
78
+ const isWarning = !isUnlimited && ratio >= 80;
79
+ const isBlocked = !isUnlimited && ratio >= 100;
80
+ const hints = USAGE_HINTS[resourceKey];
81
+
82
+ return (
83
+ <div className="surface-card-muted rounded-lg p-3 space-y-2">
84
+ <div className="flex items-center justify-between">
85
+ <div className="flex items-center gap-2">
86
+ <Icon className="h-3.5 w-3.5 text-muted-foreground" />
87
+ <span className="text-xs font-medium">{label}</span>
88
+ </div>
89
+ <span className={`text-xs ${isBlocked ? "text-destructive font-medium" : "text-muted-foreground"}`}>
90
+ {current} / {isUnlimited ? "∞" : limit}
91
+ </span>
92
+ </div>
93
+ {!isUnlimited && (
94
+ <Progress
95
+ value={Math.min(ratio, 100)}
96
+ className={`h-1.5 ${isBlocked ? "[&>div]:bg-destructive" : isWarning ? "[&>div]:bg-amber-500" : ""}`}
97
+ />
98
+ )}
99
+ {hints && (
100
+ <p className="text-[10px] leading-relaxed text-muted-foreground">
101
+ {isBlocked ? hints.atLimit : hints.sub}
102
+ </p>
103
+ )}
104
+ </div>
105
+ );
106
+ }
107
+
108
+ function TierBenefit({ icon: Icon, text }: { icon: typeof Brain; text: string }) {
109
+ return (
110
+ <li className="flex items-center gap-2">
111
+ <Icon className="h-3 w-3 text-primary shrink-0" />
112
+ <span className="text-[11px] text-muted-foreground">{text}</span>
113
+ </li>
114
+ );
115
+ }
116
+
117
+ export function SubscriptionSection() {
118
+ const [license, setLicense] = useState<LicenseStatus | null>(null);
119
+ const [usage, setUsage] = useState<UsageData | null>(null);
120
+ const [loading, setLoading] = useState(true);
121
+ const [checkoutLoading, setCheckoutLoading] = useState(false);
122
+ const [billingPeriod, setBillingPeriod] = useState<"monthly" | "annual">("monthly");
123
+
124
+ const fetchStatus = useCallback(async () => {
125
+ try {
126
+ const [licRes, usageRes] = await Promise.all([
127
+ fetch("/api/license"),
128
+ fetch("/api/license/usage"),
129
+ ]);
130
+ if (licRes.ok) setLicense(await licRes.json());
131
+ if (usageRes.ok) setUsage(await usageRes.json());
132
+ } catch {
133
+ // Fail silently
134
+ } finally {
135
+ setLoading(false);
136
+ }
137
+ }, []);
138
+
139
+ useEffect(() => {
140
+ fetchStatus();
141
+
142
+ // Check for post-purchase success
143
+ const params = new URLSearchParams(window.location.search);
144
+ if (params.get("success") === "true") {
145
+ toast.success("Subscription activated! Premium features are now available.");
146
+ // Clean up URL
147
+ const url = new URL(window.location.href);
148
+ url.searchParams.delete("success");
149
+ window.history.replaceState({}, "", url.toString());
150
+ // Refresh status after a short delay to pick up the new tier
151
+ setTimeout(fetchStatus, 2000);
152
+ }
153
+ }, [fetchStatus]);
154
+
155
+ if (loading) {
156
+ return (
157
+ <Card>
158
+ <CardHeader>
159
+ <CardTitle className="flex items-center gap-2">
160
+ <Crown className="h-5 w-5" />
161
+ Subscription
162
+ </CardTitle>
163
+ </CardHeader>
164
+ <CardContent>
165
+ <div className="animate-pulse space-y-3">
166
+ <div className="h-4 bg-muted rounded w-1/3" />
167
+ <div className="h-20 bg-muted rounded" />
168
+ </div>
169
+ </CardContent>
170
+ </Card>
171
+ );
172
+ }
173
+
174
+ const tier = license?.tier ?? "community";
175
+ const isPremium = license?.isPremium ?? false;
176
+
177
+ async function handleUpgrade(targetTier: string) {
178
+ setCheckoutLoading(true);
179
+ try {
180
+ const res = await fetch("/api/license/checkout", {
181
+ method: "POST",
182
+ headers: { "Content-Type": "application/json" },
183
+ body: JSON.stringify({ tier: targetTier, billingPeriod }),
184
+ });
185
+ const data = await res.json();
186
+ if (data.url) {
187
+ window.location.href = data.url;
188
+ } else {
189
+ toast.error(data.error ?? "Failed to start checkout");
190
+ }
191
+ } catch {
192
+ toast.error("Failed to connect to billing service");
193
+ } finally {
194
+ setCheckoutLoading(false);
195
+ }
196
+ }
197
+
198
+ return (
199
+ <Card>
200
+ <CardHeader>
201
+ <CardTitle className="flex items-center gap-2">
202
+ <Crown className="h-5 w-5" />
203
+ Subscription
204
+ </CardTitle>
205
+ <CardDescription>
206
+ Manage your plan and usage limits
207
+ </CardDescription>
208
+ </CardHeader>
209
+ <CardContent className="space-y-6">
210
+ {/* Current Plan */}
211
+ <div className="flex items-center justify-between">
212
+ <div className="flex items-center gap-3">
213
+ <Badge variant={TIER_BADGE_VARIANT[tier]}>
214
+ {TIER_LABELS[tier]}
215
+ </Badge>
216
+ {!isPremium && (
217
+ <span className="text-sm text-muted-foreground">Free forever</span>
218
+ )}
219
+ {license?.expiresAt && (
220
+ <span className="text-sm text-muted-foreground">
221
+ Renews {new Date(license.expiresAt).toLocaleDateString()}
222
+ </span>
223
+ )}
224
+ </div>
225
+ {isPremium && license?.email && (
226
+ <Button variant="outline" size="sm" asChild>
227
+ <a href={`/api/license/portal?email=${encodeURIComponent(license.email)}`} target="_blank" rel="noopener">
228
+ Manage Billing <ExternalLink className="ml-1 h-3 w-3" />
229
+ </a>
230
+ </Button>
231
+ )}
232
+ </div>
233
+
234
+ {/* Usage Summary */}
235
+ {usage && (
236
+ <div className="grid grid-cols-2 gap-3">
237
+ <UsageCard icon={Brain} label="Agent Memories" current={usage.agentMemories.current} limit={usage.agentMemories.limit} resourceKey="agentMemories" />
238
+ <UsageCard icon={BarChart3} label="Context Versions" current={usage.contextVersions.current} limit={usage.contextVersions.limit} resourceKey="contextVersions" />
239
+ <UsageCard icon={Calendar} label="Active Schedules" current={usage.activeSchedules.current} limit={usage.activeSchedules.limit} resourceKey="activeSchedules" />
240
+ <UsageCard icon={Zap} label="Parallel Workflows" current={usage.parallelWorkflows.current} limit={usage.parallelWorkflows.limit} resourceKey="parallelWorkflows" />
241
+ </div>
242
+ )}
243
+
244
+ {/* Tier Comparison */}
245
+ {!isPremium && (
246
+ <div className="space-y-3">
247
+ <div className="flex items-center justify-between">
248
+ <h4 className="text-sm font-medium">Upgrade your plan</h4>
249
+ <div className="flex items-center gap-1 rounded-lg border p-0.5">
250
+ <button
251
+ type="button"
252
+ className={`px-3 py-1 text-xs rounded-md transition-colors ${billingPeriod === "monthly" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"}`}
253
+ onClick={() => setBillingPeriod("monthly")}
254
+ >
255
+ Monthly
256
+ </button>
257
+ <button
258
+ type="button"
259
+ className={`px-3 py-1 text-xs rounded-md transition-colors ${billingPeriod === "annual" ? "bg-primary text-primary-foreground" : "text-muted-foreground hover:text-foreground"}`}
260
+ onClick={() => setBillingPeriod("annual")}
261
+ >
262
+ Annual
263
+ </button>
264
+ </div>
265
+ </div>
266
+ <div className="grid grid-cols-3 gap-3">
267
+ {/* Solo */}
268
+ <div className={`surface-card-muted rounded-lg p-4 flex flex-col ${tier === "solo" ? "ring-1 ring-primary" : ""}`}>
269
+ <div className="space-y-3 flex-1">
270
+ <div>
271
+ <div className="flex items-center justify-between">
272
+ <span className="text-sm font-semibold">Solo</span>
273
+ {tier === "solo" && <Badge variant="outline" className="text-[10px]">Current Plan</Badge>}
274
+ </div>
275
+ <p className="text-lg font-bold mt-1">
276
+ ${TIER_PRICING.solo[billingPeriod]}
277
+ <span className="text-xs font-normal text-muted-foreground">
278
+ {billingPeriod === "monthly" ? "/mo" : "/yr"}
279
+ </span>
280
+ </p>
281
+ {billingPeriod === "annual" && (
282
+ <p className="text-[10px] text-primary font-medium mt-0.5">
283
+ Save ${TIER_PRICING.solo.monthly * 12 - TIER_PRICING.solo.annual}/yr
284
+ </p>
285
+ )}
286
+ </div>
287
+ <p className="text-[11px] leading-relaxed text-muted-foreground">
288
+ For power users who need room to grow. 4x the memory, longer history, and marketplace access.
289
+ </p>
290
+ <ul className="space-y-1">
291
+ <TierBenefit icon={Brain} text="200 memories per profile" />
292
+ <TierBenefit icon={Calendar} text="20 active schedules" />
293
+ <TierBenefit icon={Store} text="Import marketplace blueprints" />
294
+ </ul>
295
+ </div>
296
+ <Button
297
+ size="sm"
298
+ variant="outline"
299
+ className="w-full mt-4"
300
+ disabled={checkoutLoading || tier === "solo"}
301
+ onClick={() => handleUpgrade("solo")}
302
+ >
303
+ {tier === "solo" ? "Current Plan" : checkoutLoading ? "Loading..." : "Get Solo"}
304
+ </Button>
305
+ </div>
306
+
307
+ {/* Operator */}
308
+ <div className={`surface-card-muted rounded-lg p-4 flex flex-col ${tier === "operator" ? "ring-1 ring-primary" : "ring-1 ring-primary/30"}`}>
309
+ <div className="space-y-3 flex-1">
310
+ <div className="flex items-center justify-between">
311
+ <span className="text-sm font-semibold">Operator</span>
312
+ <div className="flex items-center gap-1.5">
313
+ {tier === "operator" && <Badge variant="outline" className="text-[10px]">Current Plan</Badge>}
314
+ <Badge variant="secondary" className="text-[10px]">Popular</Badge>
315
+ </div>
316
+ </div>
317
+ <p className="text-lg font-bold">
318
+ ${TIER_PRICING.operator[billingPeriod]}
319
+ <span className="text-xs font-normal text-muted-foreground">
320
+ {billingPeriod === "monthly" ? "/mo" : "/yr"}
321
+ </span>
322
+ </p>
323
+ {billingPeriod === "annual" && (
324
+ <p className="text-[10px] text-primary font-medium">
325
+ Save ${TIER_PRICING.operator.monthly * 12 - TIER_PRICING.operator.annual}/yr
326
+ </p>
327
+ )}
328
+ <p className="text-[11px] leading-relaxed text-muted-foreground">
329
+ For professionals who run AI at scale. Full analytics, cloud sync, and marketplace publishing.
330
+ </p>
331
+ <ul className="space-y-1">
332
+ <TierBenefit icon={TrendingUp} text="ROI analytics dashboard" />
333
+ <TierBenefit icon={Cloud} text="Encrypted cloud sync" />
334
+ <TierBenefit icon={Store} text="Publish to marketplace" />
335
+ <TierBenefit icon={Brain} text="500 memories per profile" />
336
+ </ul>
337
+ </div>
338
+ <Button
339
+ size="sm"
340
+ className="w-full mt-4"
341
+ disabled={checkoutLoading || tier === "operator"}
342
+ onClick={() => handleUpgrade("operator")}
343
+ >
344
+ {tier === "operator" ? "Current Plan" : checkoutLoading ? "Loading..." : "Get Operator"}
345
+ </Button>
346
+ </div>
347
+
348
+ {/* Scale */}
349
+ <div className={`surface-card-muted rounded-lg p-4 flex flex-col ${tier === "scale" ? "ring-1 ring-primary" : ""}`}>
350
+ <div className="space-y-3 flex-1">
351
+ <div>
352
+ <div className="flex items-center justify-between">
353
+ <span className="text-sm font-semibold">Scale</span>
354
+ {tier === "scale" && <Badge variant="outline" className="text-[10px]">Current Plan</Badge>}
355
+ </div>
356
+ <p className="text-lg font-bold mt-1">
357
+ ${TIER_PRICING.scale[billingPeriod]}
358
+ <span className="text-xs font-normal text-muted-foreground">
359
+ {billingPeriod === "monthly" ? "/mo" : "/yr"}
360
+ </span>
361
+ </p>
362
+ {billingPeriod === "annual" && (
363
+ <p className="text-[10px] text-primary font-medium mt-0.5">
364
+ Save ${TIER_PRICING.scale.monthly * 12 - TIER_PRICING.scale.annual}/yr
365
+ </p>
366
+ )}
367
+ </div>
368
+ <p className="text-[11px] leading-relaxed text-muted-foreground">
369
+ No limits, no compromises. Unlimited everything with featured marketplace placement.
370
+ </p>
371
+ <ul className="space-y-1">
372
+ <TierBenefit icon={Zap} text="Unlimited memories & schedules" />
373
+ <TierBenefit icon={Store} text="Featured marketplace listings" />
374
+ <TierBenefit icon={Calendar} text="Unlimited history retention" />
375
+ </ul>
376
+ </div>
377
+ <Button
378
+ size="sm"
379
+ variant="outline"
380
+ className="w-full mt-4"
381
+ disabled={checkoutLoading || tier === "scale"}
382
+ onClick={() => handleUpgrade("scale")}
383
+ >
384
+ {tier === "scale" ? "Current Plan" : checkoutLoading ? "Loading..." : "Get Scale"}
385
+ </Button>
386
+ </div>
387
+ </div>
388
+ </div>
389
+ )}
390
+
391
+ {/* License Key Activation (fallback for manual entry) */}
392
+ {!isPremium && (
393
+ <ActivationForm onActivated={fetchStatus} />
394
+ )}
395
+
396
+ {/* License Info */}
397
+ {license?.email && (
398
+ <p className="text-xs text-muted-foreground">
399
+ Licensed to {license.email}
400
+ {license.status === "grace" && (
401
+ <span className="text-amber-500 ml-2">
402
+ (Offline grace period — reconnect to validate)
403
+ </span>
404
+ )}
405
+ </p>
406
+ )}
407
+ </CardContent>
408
+ </Card>
409
+ );
410
+ }
@@ -0,0 +1,80 @@
1
+ "use client";
2
+
3
+ import { useEffect, useState } from "react";
4
+ import { BarChart3, Shield } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import {
7
+ Card,
8
+ CardContent,
9
+ CardDescription,
10
+ CardHeader,
11
+ CardTitle,
12
+ } from "@/components/ui/card";
13
+ import { Switch } from "@/components/ui/switch";
14
+
15
+ export function TelemetrySection() {
16
+ const [enabled, setEnabled] = useState(false);
17
+ const [loading, setLoading] = useState(true);
18
+
19
+ useEffect(() => {
20
+ fetch("/api/settings/telemetry")
21
+ .then((r) => (r.ok ? r.json() : { enabled: false }))
22
+ .then((d) => setEnabled(d.enabled === true))
23
+ .catch(() => {})
24
+ .finally(() => setLoading(false));
25
+ }, []);
26
+
27
+ async function handleToggle(checked: boolean) {
28
+ setEnabled(checked);
29
+ try {
30
+ const res = await fetch("/api/settings/telemetry", {
31
+ method: "POST",
32
+ headers: { "Content-Type": "application/json" },
33
+ body: JSON.stringify({ enabled: checked }),
34
+ });
35
+ if (res.ok) {
36
+ toast.success(checked ? "Telemetry enabled — thank you!" : "Telemetry disabled");
37
+ } else {
38
+ setEnabled(!checked); // Revert on failure
39
+ }
40
+ } catch {
41
+ setEnabled(!checked);
42
+ }
43
+ }
44
+
45
+ return (
46
+ <Card>
47
+ <CardHeader>
48
+ <CardTitle className="flex items-center gap-2">
49
+ <BarChart3 className="h-5 w-5" />
50
+ Anonymous Telemetry
51
+ </CardTitle>
52
+ <CardDescription>
53
+ Help improve Stagent by sharing anonymized usage data
54
+ </CardDescription>
55
+ </CardHeader>
56
+ <CardContent className="space-y-4">
57
+ <div className="flex items-center justify-between">
58
+ <div className="space-y-1">
59
+ <p className="text-sm font-medium">Share usage data</p>
60
+ <p className="text-xs text-muted-foreground">
61
+ Activity types, model usage, and outcome rates. No task content, project names, or personal data.
62
+ </p>
63
+ </div>
64
+ <Switch
65
+ checked={enabled}
66
+ onCheckedChange={handleToggle}
67
+ disabled={loading}
68
+ />
69
+ </div>
70
+ <div className="flex items-start gap-2 text-xs text-muted-foreground">
71
+ <Shield className="h-3.5 w-3.5 shrink-0 mt-0.5" />
72
+ <span>
73
+ Data is opt-in, anonymized, and never includes task descriptions, file contents, or email addresses.
74
+ You can disable this at any time.
75
+ </span>
76
+ </div>
77
+ </CardContent>
78
+ </Card>
79
+ );
80
+ }