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,54 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { licenseManager } from "@/lib/license/manager";
3
+ import { exportAndUpload } from "@/lib/sync/cloud-sync";
4
+ import { getSettingSync } from "@/lib/settings/helpers";
5
+ import { SETTINGS_KEYS } from "@/lib/constants/settings";
6
+
7
+ /**
8
+ * POST /api/sync/export
9
+ * Export and encrypt the database, upload to Supabase Storage.
10
+ * Requires Operator+ tier and an authenticated Supabase session.
11
+ * Body: { accessToken: string } — the user's Supabase JWT
12
+ */
13
+ export async function POST(req: NextRequest) {
14
+ if (!licenseManager.isFeatureAllowed("cloud-sync")) {
15
+ return NextResponse.json(
16
+ { error: "Cloud sync requires Operator tier or above" },
17
+ { status: 402 }
18
+ );
19
+ }
20
+
21
+ const email = licenseManager.getStatus().email;
22
+ if (!email) {
23
+ return NextResponse.json(
24
+ { error: "Sign in with your email first (Settings → Cloud Account)" },
25
+ { status: 400 }
26
+ );
27
+ }
28
+
29
+ // Get the user's access token for authenticated Storage uploads
30
+ const body = await req.json().catch(() => ({}));
31
+ const accessToken = body.accessToken as string | undefined;
32
+
33
+ let deviceId = getSettingSync(SETTINGS_KEYS.DEVICE_ID);
34
+ if (!deviceId) {
35
+ deviceId = crypto.randomUUID();
36
+ const { setSetting } = await import("@/lib/settings/helpers");
37
+ await setSetting(SETTINGS_KEYS.DEVICE_ID, deviceId);
38
+ }
39
+
40
+ const result = await exportAndUpload(email, deviceId, accessToken);
41
+
42
+ if (!result.success) {
43
+ return NextResponse.json({ error: result.error }, { status: 500 });
44
+ }
45
+
46
+ const { setSetting } = await import("@/lib/settings/helpers");
47
+ await setSetting(SETTINGS_KEYS.LAST_SYNC_AT, new Date().toISOString());
48
+
49
+ return NextResponse.json({
50
+ success: true,
51
+ blobPath: result.blobPath,
52
+ sizeBytes: result.sizeBytes,
53
+ });
54
+ }
@@ -0,0 +1,37 @@
1
+ import { NextResponse } from "next/server";
2
+ import { licenseManager } from "@/lib/license/manager";
3
+ import { downloadAndRestore } from "@/lib/sync/cloud-sync";
4
+
5
+ /**
6
+ * POST /api/sync/restore
7
+ * Download the latest snapshot, decrypt, and restore.
8
+ * Requires Operator+ tier. Creates a safety backup first.
9
+ */
10
+ export async function POST() {
11
+ if (!licenseManager.isFeatureAllowed("cloud-sync")) {
12
+ return NextResponse.json(
13
+ { error: "Cloud sync requires Operator tier or above" },
14
+ { status: 402 }
15
+ );
16
+ }
17
+
18
+ const email = licenseManager.getStatus().email;
19
+ if (!email) {
20
+ return NextResponse.json(
21
+ { error: "No license email — activate your license first" },
22
+ { status: 400 }
23
+ );
24
+ }
25
+
26
+ const result = await downloadAndRestore(email);
27
+
28
+ if (!result.success) {
29
+ return NextResponse.json({ error: result.error }, { status: 500 });
30
+ }
31
+
32
+ return NextResponse.json({
33
+ success: true,
34
+ sizeBytes: result.sizeBytes,
35
+ message: "Database restored. Restart the app to apply changes.",
36
+ });
37
+ }
@@ -0,0 +1,24 @@
1
+ import { NextResponse } from "next/server";
2
+ import { licenseManager } from "@/lib/license/manager";
3
+ import { listSyncSessions } from "@/lib/sync/cloud-sync";
4
+
5
+ /**
6
+ * GET /api/sync/sessions
7
+ * List recent sync sessions for the current user.
8
+ */
9
+ export async function GET() {
10
+ if (!licenseManager.isFeatureAllowed("cloud-sync")) {
11
+ return NextResponse.json(
12
+ { error: "Cloud sync requires Operator tier or above" },
13
+ { status: 402 }
14
+ );
15
+ }
16
+
17
+ const email = licenseManager.getStatus().email;
18
+ if (!email) {
19
+ return NextResponse.json({ sessions: [] });
20
+ }
21
+
22
+ const sessions = await listSyncSessions(email);
23
+ return NextResponse.json({ sessions });
24
+ }
@@ -10,6 +10,8 @@ import {
10
10
  enforceTaskBudgetGuardrails,
11
11
  } from "@/lib/settings/budget-guardrails";
12
12
  import { ensureFreshScan } from "@/lib/environment/auto-scan";
13
+ import { getAllExecutions } from "@/lib/agents/execution-manager";
14
+ import { licenseManager } from "@/lib/license/manager";
13
15
 
14
16
  export async function POST(
15
17
  _req: NextRequest,
@@ -93,6 +95,25 @@ export async function POST(
93
95
  return NextResponse.json({ error: compatibilityError }, { status: 400 });
94
96
  }
95
97
 
98
+ // Pre-check parallel workflow limit before fire-and-forget
99
+ const parallelLimit = licenseManager.getLimit("parallelWorkflows");
100
+ if (Number.isFinite(parallelLimit) && getAllExecutions().size >= parallelLimit) {
101
+ // Revert task to queued since we can't execute it
102
+ db.update(tasks)
103
+ .set({ status: "queued", updatedAt: new Date() })
104
+ .where(eq(tasks.id, id))
105
+ .run();
106
+ return NextResponse.json(
107
+ {
108
+ error: `Parallel workflow limit reached (${getAllExecutions().size}/${parallelLimit}). Wait for a running task to finish or upgrade.`,
109
+ limitType: "parallelWorkflows",
110
+ current: getAllExecutions().size,
111
+ max: parallelLimit,
112
+ },
113
+ { status: 429 }
114
+ );
115
+ }
116
+
96
117
  // Fire-and-forget — task already marked as running
97
118
  executeTaskWithAgent(id, task.assignedAgent ?? DEFAULT_AGENT_RUNTIME).catch(
98
119
  (err) => console.error(`Task ${id} execution error:`, err)
@@ -0,0 +1,79 @@
1
+ import { NextRequest, NextResponse } from "next/server";
2
+ import { createClient } from "@supabase/supabase-js";
3
+ import { licenseManager } from "@/lib/license/manager";
4
+ import { validateLicenseWithCloud } from "@/lib/license/cloud-validation";
5
+ import { sendUpgradeConfirmation } from "@/lib/billing/email";
6
+
7
+ const DEFAULT_SUPABASE_URL = "https://yznantjbmacbllhcyzwc.supabase.co";
8
+ const DEFAULT_SUPABASE_ANON_KEY =
9
+ "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJzdXBhYmFzZSIsInJlZiI6Inl6bmFudGpibWFjYmxsaGN5endjIiwicm9sZSI6ImFub24iLCJpYXQiOjE3NzI1MDg1ODMsImV4cCI6MjA4ODA4NDU4M30.i-P7MXpR1_emBjhUkzbFeSX7fgjgPDv90_wkqF7sW3Y";
10
+
11
+ /**
12
+ * GET /auth/callback
13
+ *
14
+ * Handles the Supabase magic link redirect. Exchanges the auth code
15
+ * for a session, validates the user's license, and redirects to settings.
16
+ *
17
+ * Flow:
18
+ * 1. User clicks magic link in email
19
+ * 2. Supabase redirects here with ?code=...
20
+ * 3. We exchange the code for a session (gets user email)
21
+ * 4. Validate license against cloud (check if they have a paid subscription)
22
+ * 5. Activate the license locally if found
23
+ * 6. Redirect to /settings with success indicator
24
+ */
25
+ export async function GET(req: NextRequest) {
26
+ const { searchParams } = req.nextUrl;
27
+ const code = searchParams.get("code");
28
+
29
+ if (!code) {
30
+ return NextResponse.redirect(new URL("/settings?auth=error", req.url));
31
+ }
32
+
33
+ const url = process.env.NEXT_PUBLIC_SUPABASE_URL || DEFAULT_SUPABASE_URL;
34
+ const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY || DEFAULT_SUPABASE_ANON_KEY;
35
+
36
+ const supabase = createClient(url, anonKey, {
37
+ auth: { persistSession: false },
38
+ });
39
+
40
+ // Exchange the code for a session
41
+ const { data, error } = await supabase.auth.exchangeCodeForSession(code);
42
+
43
+ if (error || !data.session) {
44
+ console.error("[auth/callback] Code exchange failed:", error?.message);
45
+ return NextResponse.redirect(new URL("/settings?auth=error", req.url));
46
+ }
47
+
48
+ const email = data.session.user.email;
49
+ if (!email) {
50
+ return NextResponse.redirect(new URL("/settings?auth=error", req.url));
51
+ }
52
+
53
+ // Validate license against cloud — check if this email has a paid subscription
54
+ const validation = await validateLicenseWithCloud(email);
55
+
56
+ if (validation.valid && validation.tier !== "community") {
57
+ // User has a paid subscription — activate locally
58
+ licenseManager.activate({
59
+ tier: validation.tier,
60
+ email,
61
+ expiresAt: validation.expiresAt,
62
+ });
63
+ // Send upgrade confirmation email (fire-and-forget)
64
+ sendUpgradeConfirmation(email, validation.tier).catch(() => {});
65
+ } else {
66
+ // No paid subscription — still link the email for future activation
67
+ // This sets the email so cloud sync and marketplace work when they upgrade
68
+ const currentTier = licenseManager.getTierFromDb();
69
+ if (currentTier === "community") {
70
+ // Just store the email association without changing tier
71
+ licenseManager.activate({
72
+ tier: "community",
73
+ email,
74
+ });
75
+ }
76
+ }
77
+
78
+ return NextResponse.redirect(new URL("/settings?auth=success", req.url));
79
+ }
@@ -0,0 +1,19 @@
1
+ import { PageShell } from "@/components/shared/page-shell";
2
+ import { MarketplaceBrowser } from "@/components/marketplace/marketplace-browser";
3
+ import { licenseManager } from "@/lib/license/manager";
4
+ import { canAccessFeature } from "@/lib/license/features";
5
+
6
+ export const dynamic = "force-dynamic";
7
+
8
+ export default function MarketplacePage() {
9
+ // Use getTierFromDb() for Server Components (Turbopack module instance separation)
10
+ const tier = licenseManager.getTierFromDb();
11
+ const canImport = canAccessFeature(tier, "marketplace-import");
12
+ const canPublish = canAccessFeature(tier, "marketplace-publish");
13
+
14
+ return (
15
+ <PageShell title="Marketplace" description="Browse and import workflow blueprints">
16
+ <MarketplaceBrowser canImport={canImport} canPublish={canPublish} />
17
+ </PageShell>
18
+ );
19
+ }
package/src/app/page.tsx CHANGED
@@ -12,6 +12,8 @@ import { QuickActions } from "@/components/dashboard/quick-actions";
12
12
  import { RecentProjects } from "@/components/dashboard/recent-projects";
13
13
  import type { RecentProject } from "@/components/dashboard/recent-projects";
14
14
  import { WelcomeLanding } from "@/components/dashboard/welcome-landing";
15
+ import { EmailCaptureCard } from "@/components/onboarding/email-capture-card";
16
+ import { ActivationChecklist } from "@/components/onboarding/activation-checklist";
15
17
  import {
16
18
  getCompletionsByDay,
17
19
  getTaskCreationsByDay,
@@ -104,7 +106,8 @@ export default async function HomePage() {
104
106
  if (isFreshInstance) {
105
107
  return (
106
108
  <div className="bg-background min-h-screen p-4 sm:p-6">
107
- <div className="surface-page-shell min-h-[calc(100dvh-2rem)] rounded-xl p-5 sm:p-6 lg:p-7">
109
+ <div className="surface-page-shell min-h-[calc(100dvh-2rem)] rounded-xl p-5 sm:p-6 lg:p-7 space-y-6">
110
+ <EmailCaptureCard />
108
111
  <WelcomeLanding />
109
112
  </div>
110
113
  </div>
@@ -231,8 +234,9 @@ export default async function HomePage() {
231
234
  <div className="lg:col-span-3">
232
235
  <RecentProjects projects={recentProjectData} />
233
236
  </div>
234
- <div className="lg:col-span-2">
237
+ <div className="lg:col-span-2 space-y-6">
235
238
  <QuickActions />
239
+ <ActivationChecklist />
236
240
  </div>
237
241
  </div>
238
242
  </div>
@@ -1,3 +1,7 @@
1
+ import { SubscriptionSection } from "@/components/settings/subscription-section";
2
+ import { CloudAccountSection } from "@/components/settings/cloud-account-section";
3
+ import { CloudSyncSection } from "@/components/settings/cloud-sync-section";
4
+ import { TelemetrySection } from "@/components/settings/telemetry-section";
1
5
  import { ProvidersAndRuntimesSection } from "@/components/settings/providers-runtimes-section";
2
6
  import { PermissionsSections } from "@/components/settings/permissions-sections";
3
7
  import { DataManagementSection } from "@/components/settings/data-management-section";
@@ -18,6 +22,9 @@ export default function SettingsPage() {
18
22
  return (
19
23
  <PageShell title="Settings" description="Manage your Stagent configuration">
20
24
  <div className="space-y-6">
25
+ <SubscriptionSection />
26
+ <CloudAccountSection />
27
+ <CloudSyncSection />
21
28
  <ProvidersAndRuntimesSection />
22
29
  <OllamaSection />
23
30
  <ChatSettingsSection />
@@ -29,6 +36,7 @@ export default function SettingsPage() {
29
36
  <BudgetGuardrailsSection />
30
37
  <PermissionsSections />
31
38
  <DatabaseSnapshotsSection />
39
+ <TelemetrySection />
32
40
  <DataManagementSection />
33
41
  </div>
34
42
  </PageShell>
@@ -0,0 +1,200 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import {
5
+ Card,
6
+ CardContent,
7
+ CardHeader,
8
+ CardTitle,
9
+ } from "@/components/ui/card";
10
+ import {
11
+ AreaChart,
12
+ Area,
13
+ LineChart,
14
+ Line,
15
+ XAxis,
16
+ YAxis,
17
+ CartesianGrid,
18
+ Tooltip,
19
+ ResponsiveContainer,
20
+ } from "recharts";
21
+ import type {
22
+ OutcomeCount,
23
+ TrendPoint,
24
+ CostTrendPoint,
25
+ ProfileStats,
26
+ } from "@/lib/analytics/queries";
27
+
28
+ interface AnalyticsDashboardProps {
29
+ outcomes: OutcomeCount;
30
+ successTrend: TrendPoint[];
31
+ costTrend: CostTrendPoint[];
32
+ leaderboard: ProfileStats[];
33
+ hoursSaved: number;
34
+ }
35
+
36
+ function StatCard({
37
+ label,
38
+ value,
39
+ sub,
40
+ }: {
41
+ label: string;
42
+ value: string;
43
+ sub?: string;
44
+ }) {
45
+ return (
46
+ <div className="surface-card-muted rounded-lg p-4 text-center">
47
+ <p className="text-2xl font-bold">{value}</p>
48
+ <p className="text-xs text-muted-foreground">{label}</p>
49
+ {sub && <p className="text-[10px] text-muted-foreground mt-1">{sub}</p>}
50
+ </div>
51
+ );
52
+ }
53
+
54
+ export function AnalyticsDashboard({
55
+ outcomes,
56
+ successTrend,
57
+ costTrend,
58
+ leaderboard,
59
+ hoursSaved,
60
+ }: AnalyticsDashboardProps) {
61
+ const [hourlyRate, setHourlyRate] = useState(() => {
62
+ if (typeof window === "undefined") return 75;
63
+ const saved = localStorage.getItem("stagent-hourly-rate");
64
+ return saved ? parseInt(saved, 10) : 75;
65
+ });
66
+
67
+ function updateHourlyRate(rate: number) {
68
+ setHourlyRate(rate);
69
+ if (typeof window !== "undefined") {
70
+ localStorage.setItem("stagent-hourly-rate", String(rate));
71
+ }
72
+ }
73
+
74
+ const valueGenerated = Math.round(hoursSaved * hourlyRate);
75
+ const totalCostUsd = leaderboard.reduce((sum, p) => sum + p.totalCostMicros, 0) / 1_000_000;
76
+
77
+ return (
78
+ <div className="space-y-6">
79
+ {/* ROI Summary Strip */}
80
+ <div className="grid grid-cols-4 gap-3">
81
+ <StatCard label="Tasks Completed" value={String(outcomes.completed)} sub="Last 30 days" />
82
+ <StatCard label="Success Rate" value={`${outcomes.successRate}%`} sub={`${outcomes.failed} failed`} />
83
+ <StatCard label="Hours Saved" value={`${hoursSaved}h`} sub={`~$${valueGenerated} value`} />
84
+ <StatCard label="Total Cost" value={`$${totalCostUsd.toFixed(2)}`} sub="Agent spend" />
85
+ </div>
86
+
87
+ {/* Charts Row */}
88
+ <div className="grid grid-cols-2 gap-4">
89
+ {/* Success Rate Trend */}
90
+ <Card>
91
+ <CardHeader className="pb-2">
92
+ <CardTitle className="text-sm">Task Outcomes (30d)</CardTitle>
93
+ </CardHeader>
94
+ <CardContent>
95
+ <ResponsiveContainer width="100%" height={200}>
96
+ <AreaChart data={successTrend}>
97
+ <CartesianGrid strokeDasharray="3 3" className="stroke-border" />
98
+ <XAxis dataKey="date" tick={{ fontSize: 10 }} tickFormatter={(v) => v.slice(5)} />
99
+ <YAxis tick={{ fontSize: 10 }} />
100
+ <Tooltip />
101
+ <Area type="monotone" dataKey="completed" stackId="1" fill="oklch(0.7 0.15 250)" stroke="oklch(0.6 0.2 250)" fillOpacity={0.3} />
102
+ <Area type="monotone" dataKey="failed" stackId="1" fill="oklch(0.7 0.15 25)" stroke="oklch(0.6 0.2 25)" fillOpacity={0.3} />
103
+ </AreaChart>
104
+ </ResponsiveContainer>
105
+ </CardContent>
106
+ </Card>
107
+
108
+ {/* Cost per Outcome Trend */}
109
+ <Card>
110
+ <CardHeader className="pb-2">
111
+ <CardTitle className="text-sm">Cost per Task (30d)</CardTitle>
112
+ </CardHeader>
113
+ <CardContent>
114
+ <ResponsiveContainer width="100%" height={200}>
115
+ <LineChart data={costTrend}>
116
+ <CartesianGrid strokeDasharray="3 3" className="stroke-border" />
117
+ <XAxis dataKey="date" tick={{ fontSize: 10 }} tickFormatter={(v) => v.slice(5)} />
118
+ <YAxis tick={{ fontSize: 10 }} tickFormatter={(v) => `$${(v / 1_000_000).toFixed(2)}`} />
119
+ <Tooltip formatter={(v: number) => [`$${(v / 1_000_000).toFixed(4)}`, "Avg Cost"]} />
120
+ <Line type="monotone" dataKey="avgCostMicros" stroke="oklch(0.6 0.2 250)" strokeWidth={2} dot={false} />
121
+ </LineChart>
122
+ </ResponsiveContainer>
123
+ </CardContent>
124
+ </Card>
125
+ </div>
126
+
127
+ {/* Profile Leaderboard */}
128
+ <Card>
129
+ <CardHeader className="pb-2">
130
+ <CardTitle className="text-sm">Profile Leaderboard (30d)</CardTitle>
131
+ </CardHeader>
132
+ <CardContent>
133
+ {leaderboard.length === 0 ? (
134
+ <p className="text-sm text-muted-foreground text-center py-6">
135
+ No agent profiles have run tasks yet.
136
+ </p>
137
+ ) : (
138
+ <div className="overflow-x-auto">
139
+ <table className="w-full text-sm">
140
+ <thead>
141
+ <tr className="border-b text-left">
142
+ <th className="pb-2 font-medium">Profile</th>
143
+ <th className="pb-2 font-medium text-right">Completed</th>
144
+ <th className="pb-2 font-medium text-right">Failed</th>
145
+ <th className="pb-2 font-medium text-right">Success</th>
146
+ <th className="pb-2 font-medium text-right">Cost</th>
147
+ <th className="pb-2 font-medium text-right">Avg Duration</th>
148
+ </tr>
149
+ </thead>
150
+ <tbody>
151
+ {leaderboard.map((p) => (
152
+ <tr key={p.profileId} className="border-b border-border/50">
153
+ <td className="py-2 font-mono text-xs">{p.profileId}</td>
154
+ <td className="py-2 text-right">{p.completed}</td>
155
+ <td className="py-2 text-right text-muted-foreground">{p.failed}</td>
156
+ <td className="py-2 text-right">{p.successRate}%</td>
157
+ <td className="py-2 text-right">${(p.totalCostMicros / 1_000_000).toFixed(2)}</td>
158
+ <td className="py-2 text-right text-muted-foreground">
159
+ {p.avgDurationMs > 0 ? `${(p.avgDurationMs / 1000).toFixed(1)}s` : "—"}
160
+ </td>
161
+ </tr>
162
+ ))}
163
+ </tbody>
164
+ </table>
165
+ </div>
166
+ )}
167
+ </CardContent>
168
+ </Card>
169
+
170
+ {/* ROI Calculator */}
171
+ <Card>
172
+ <CardHeader className="pb-2">
173
+ <CardTitle className="text-sm">ROI Calculator</CardTitle>
174
+ </CardHeader>
175
+ <CardContent>
176
+ <div className="flex items-center gap-4">
177
+ <label className="text-xs text-muted-foreground whitespace-nowrap">
178
+ Your hourly rate:
179
+ </label>
180
+ <div className="flex items-center gap-1">
181
+ <span className="text-sm">$</span>
182
+ <input
183
+ type="number"
184
+ value={hourlyRate}
185
+ onChange={(e) => updateHourlyRate(Math.max(1, parseInt(e.target.value, 10) || 1))}
186
+ className="w-20 rounded-md border px-2 py-1 text-sm"
187
+ min={1}
188
+ />
189
+ <span className="text-xs text-muted-foreground">/hr</span>
190
+ </div>
191
+ <div className="text-sm">
192
+ <span className="font-medium">${valueGenerated.toLocaleString()}</span>
193
+ <span className="text-muted-foreground"> estimated value from {hoursSaved}h saved</span>
194
+ </div>
195
+ </div>
196
+ </CardContent>
197
+ </Card>
198
+ </div>
199
+ );
200
+ }
@@ -0,0 +1,101 @@
1
+ "use client";
2
+
3
+ import Link from "next/link";
4
+ import { TrendingUp, Trophy, Coins, Sparkles } from "lucide-react";
5
+ import { Button } from "@/components/ui/button";
6
+ import { Badge } from "@/components/ui/badge";
7
+ import { TIER_PRICING } from "@/lib/license/tier-limits";
8
+
9
+ function ValueProp({
10
+ icon: Icon,
11
+ title,
12
+ description,
13
+ }: {
14
+ icon: typeof TrendingUp;
15
+ title: string;
16
+ description: string;
17
+ }) {
18
+ return (
19
+ <div className="space-y-1.5">
20
+ <div className="flex items-center gap-2">
21
+ <div className="flex items-center justify-center w-7 h-7 rounded-lg border bg-background">
22
+ <Icon className="h-3.5 w-3.5 text-primary" />
23
+ </div>
24
+ <span className="text-xs font-semibold tracking-tight">{title}</span>
25
+ </div>
26
+ <p className="text-[11px] leading-relaxed text-muted-foreground pl-9">
27
+ {description}
28
+ </p>
29
+ </div>
30
+ );
31
+ }
32
+
33
+ /**
34
+ * Analytics-specific upgrade CTA card.
35
+ * Replaces the generic PremiumGateOverlay lock card with
36
+ * benefit-oriented messaging and value props.
37
+ */
38
+ export function AnalyticsGateCard() {
39
+ const price = TIER_PRICING.operator.monthly;
40
+
41
+ return (
42
+ <div className="w-full max-w-lg mx-auto">
43
+ <div className="surface-card rounded-xl border shadow-lg p-8 space-y-6">
44
+ {/* Header */}
45
+ <div className="space-y-2 text-center">
46
+ <div className="flex items-center justify-center gap-2 mb-3">
47
+ <Sparkles className="h-4 w-4 text-primary" />
48
+ <Badge variant="secondary" className="text-[10px] font-medium tracking-wide uppercase">
49
+ Operator Feature
50
+ </Badge>
51
+ </div>
52
+ <h2 className="text-xl font-bold tracking-tight">
53
+ See what your AI agents are worth
54
+ </h2>
55
+ <p className="text-sm text-muted-foreground max-w-xs mx-auto">
56
+ Turn raw execution data into actionable ROI insights
57
+ </p>
58
+ </div>
59
+
60
+ {/* Value Props */}
61
+ <div className="space-y-4 py-2">
62
+ <ValueProp
63
+ icon={TrendingUp}
64
+ title="ROI Tracking"
65
+ description="Know exactly how much time and money your agents save with automated value calculations"
66
+ />
67
+ <ValueProp
68
+ icon={Trophy}
69
+ title="Profile Leaderboard"
70
+ description="See which agent profiles deliver the best results and optimize your team"
71
+ />
72
+ <ValueProp
73
+ icon={Coins}
74
+ title="Cost Efficiency"
75
+ description="Track cost-per-outcome trends to find the sweet spot between quality and spend"
76
+ />
77
+ </div>
78
+
79
+ {/* Pricing + CTA */}
80
+ <div className="space-y-3 pt-2">
81
+ <div className="flex items-baseline justify-center gap-1.5">
82
+ <span className="text-2xl font-bold">${price}</span>
83
+ <span className="text-xs text-muted-foreground">/mo</span>
84
+ <span className="text-xs text-muted-foreground mx-1">·</span>
85
+ <span className="text-xs text-muted-foreground">Operator tier</span>
86
+ </div>
87
+ <Button className="w-full" size="lg" asChild>
88
+ <Link href="/settings?highlight=operator">
89
+ Unlock Analytics
90
+ </Link>
91
+ </Button>
92
+ </div>
93
+
94
+ {/* Social proof / objection handler */}
95
+ <p className="text-[11px] text-center text-muted-foreground">
96
+ Derived from data you already have — zero setup required
97
+ </p>
98
+ </div>
99
+ </div>
100
+ );
101
+ }
@@ -0,0 +1,61 @@
1
+ "use client";
2
+
3
+ import { Download, Star } from "lucide-react";
4
+ import { Badge } from "@/components/ui/badge";
5
+ import { Button } from "@/components/ui/button";
6
+ import type { MarketplaceBlueprint } from "@/lib/marketplace/marketplace-client";
7
+
8
+ interface BlueprintCardProps {
9
+ blueprint: MarketplaceBlueprint;
10
+ canImport: boolean;
11
+ onImport?: (id: string) => void;
12
+ }
13
+
14
+ export function BlueprintCard({ blueprint, canImport, onImport }: BlueprintCardProps) {
15
+ return (
16
+ <div className="surface-card-muted rounded-lg border p-4 space-y-3">
17
+ <div className="flex items-start justify-between gap-2">
18
+ <div className="space-y-1">
19
+ <h3 className="text-sm font-medium">{blueprint.title}</h3>
20
+ {blueprint.description && (
21
+ <p className="text-xs text-muted-foreground line-clamp-2">
22
+ {blueprint.description}
23
+ </p>
24
+ )}
25
+ </div>
26
+ <Badge variant="secondary" className="text-[10px] shrink-0">
27
+ {blueprint.category}
28
+ </Badge>
29
+ </div>
30
+
31
+ <div className="flex items-center justify-between">
32
+ <div className="flex items-center gap-3 text-xs text-muted-foreground">
33
+ <span className="flex items-center gap-1">
34
+ <Download className="h-3 w-3" />
35
+ {blueprint.install_count}
36
+ </span>
37
+ {blueprint.success_rate > 0 && (
38
+ <span className="flex items-center gap-1">
39
+ <Star className="h-3 w-3" />
40
+ {Math.round(blueprint.success_rate * 100)}%
41
+ </span>
42
+ )}
43
+ {blueprint.tags?.length > 0 && (
44
+ <span>{blueprint.tags.slice(0, 2).join(", ")}</span>
45
+ )}
46
+ </div>
47
+
48
+ {canImport && onImport && (
49
+ <Button
50
+ size="sm"
51
+ variant="outline"
52
+ onClick={() => onImport(blueprint.id)}
53
+ >
54
+ <Download className="h-3 w-3 mr-1" />
55
+ Import
56
+ </Button>
57
+ )}
58
+ </div>
59
+ </div>
60
+ );
61
+ }