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,51 @@
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
+ }
@@ -0,0 +1,290 @@
1
+ /**
2
+ * LicenseManager — singleton for local license enforcement.
3
+ *
4
+ * Mirrors the budget-guardrails.ts pattern: process-memory cache for
5
+ * synchronous access, daily cloud validation, offline grace period.
6
+ *
7
+ * Usage:
8
+ * import { licenseManager } from "@/lib/license/manager";
9
+ * const tier = licenseManager.getTier();
10
+ * if (!licenseManager.isFeatureAllowed("cloud-sync")) { ... }
11
+ */
12
+
13
+ import { db } from "@/lib/db";
14
+ import { license as licenseTable } from "@/lib/db/schema";
15
+ import { eq } from "drizzle-orm";
16
+ import { type LicenseTier, TIER_LIMITS, type LimitResource } from "./tier-limits";
17
+ import { canAccessFeature, type LicenseFeature } from "./features";
18
+
19
+ const LICENSE_ROW_ID = "default";
20
+ const GRACE_PERIOD_MS = 7 * 24 * 60 * 60 * 1000; // 7 days
21
+ const VALIDATION_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
22
+
23
+ interface CachedLicense {
24
+ tier: LicenseTier;
25
+ status: "active" | "inactive" | "grace";
26
+ email: string | null;
27
+ activatedAt: Date | null;
28
+ expiresAt: Date | null;
29
+ lastValidatedAt: Date | null;
30
+ gracePeriodExpiresAt: Date | null;
31
+ }
32
+
33
+ class LicenseManager {
34
+ private cache: CachedLicense | null = null;
35
+ private validationTimer: ReturnType<typeof setInterval> | null = null;
36
+
37
+ /**
38
+ * Initialize from DB. Call once at app boot (instrumentation.ts).
39
+ * Creates the default license row if it doesn't exist.
40
+ */
41
+ initialize(): void {
42
+ const rows = db
43
+ .select()
44
+ .from(licenseTable)
45
+ .where(eq(licenseTable.id, LICENSE_ROW_ID))
46
+ .all();
47
+
48
+ if (rows.length === 0) {
49
+ const now = new Date();
50
+ db.insert(licenseTable)
51
+ .values({
52
+ id: LICENSE_ROW_ID,
53
+ tier: "community",
54
+ status: "active",
55
+ createdAt: now,
56
+ updatedAt: now,
57
+ })
58
+ .run();
59
+
60
+ this.cache = {
61
+ tier: "community",
62
+ status: "active",
63
+ email: null,
64
+ activatedAt: null,
65
+ expiresAt: null,
66
+ lastValidatedAt: null,
67
+ gracePeriodExpiresAt: null,
68
+ };
69
+ } else {
70
+ const row = rows[0];
71
+ this.cache = this.rowToCache(row);
72
+ }
73
+
74
+ // Check grace period expiry
75
+ this.checkGracePeriod();
76
+ }
77
+
78
+ /**
79
+ * Start the daily validation timer.
80
+ * Separated from initialize() so tests can skip the timer.
81
+ */
82
+ startValidationTimer(): void {
83
+ if (this.validationTimer) return;
84
+ this.validationTimer = setInterval(() => {
85
+ this.validateAndRefresh();
86
+ }, VALIDATION_INTERVAL_MS);
87
+ }
88
+
89
+ stopValidationTimer(): void {
90
+ if (this.validationTimer) {
91
+ clearInterval(this.validationTimer);
92
+ this.validationTimer = null;
93
+ }
94
+ }
95
+
96
+ /** Current tier — synchronous, zero-latency (reads from cache) */
97
+ getTier(): LicenseTier {
98
+ return this.cache?.tier ?? "community";
99
+ }
100
+
101
+ /**
102
+ * Read tier directly from DB — bypasses in-memory cache.
103
+ * Use this in Server Components where the singleton cache may be stale
104
+ * due to Turbopack module instance separation.
105
+ */
106
+ getTierFromDb(): LicenseTier {
107
+ const row = db
108
+ .select({ tier: licenseTable.tier })
109
+ .from(licenseTable)
110
+ .where(eq(licenseTable.id, LICENSE_ROW_ID))
111
+ .get();
112
+ return (row?.tier as LicenseTier) ?? "community";
113
+ }
114
+
115
+ /** True if tier is solo or above */
116
+ isPremium(): boolean {
117
+ const tier = this.getTier();
118
+ return tier !== "community";
119
+ }
120
+
121
+ /** Check if a specific feature is allowed for the current tier */
122
+ isFeatureAllowed(feature: LicenseFeature): boolean {
123
+ return canAccessFeature(this.getTier(), feature);
124
+ }
125
+
126
+ /** Get the limit value for a resource at the current tier */
127
+ getLimit(resource: LimitResource): number {
128
+ return TIER_LIMITS[this.getTier()][resource];
129
+ }
130
+
131
+ /** Get the full cached license state */
132
+ getStatus(): CachedLicense & { tier: LicenseTier } {
133
+ return {
134
+ tier: this.getTier(),
135
+ status: this.cache?.status ?? "inactive",
136
+ email: this.cache?.email ?? null,
137
+ activatedAt: this.cache?.activatedAt ?? null,
138
+ expiresAt: this.cache?.expiresAt ?? null,
139
+ lastValidatedAt: this.cache?.lastValidatedAt ?? null,
140
+ gracePeriodExpiresAt: this.cache?.gracePeriodExpiresAt ?? null,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * Activate a license locally. Called after successful cloud validation.
146
+ */
147
+ activate(params: {
148
+ tier: LicenseTier;
149
+ email: string;
150
+ expiresAt?: Date;
151
+ encryptedToken?: string;
152
+ }): void {
153
+ const now = new Date();
154
+ db.update(licenseTable)
155
+ .set({
156
+ tier: params.tier,
157
+ status: "active",
158
+ email: params.email,
159
+ activatedAt: now,
160
+ expiresAt: params.expiresAt ?? null,
161
+ lastValidatedAt: now,
162
+ gracePeriodExpiresAt: null,
163
+ encryptedToken: params.encryptedToken ?? null,
164
+ updatedAt: now,
165
+ })
166
+ .where(eq(licenseTable.id, LICENSE_ROW_ID))
167
+ .run();
168
+
169
+ this.refreshCache();
170
+ }
171
+
172
+ /**
173
+ * Deactivate — revert to community tier.
174
+ */
175
+ deactivate(): void {
176
+ const now = new Date();
177
+ db.update(licenseTable)
178
+ .set({
179
+ tier: "community",
180
+ status: "inactive",
181
+ email: null,
182
+ activatedAt: null,
183
+ expiresAt: null,
184
+ lastValidatedAt: null,
185
+ gracePeriodExpiresAt: null,
186
+ encryptedToken: null,
187
+ updatedAt: now,
188
+ })
189
+ .where(eq(licenseTable.id, LICENSE_ROW_ID))
190
+ .run();
191
+
192
+ this.refreshCache();
193
+ }
194
+
195
+ /**
196
+ * Daily validation against cloud.
197
+ * On success: update lastValidatedAt, clear grace period.
198
+ * On failure: enter grace period if not already in one.
199
+ * Never throws — silently uses cached state on error.
200
+ */
201
+ async validateAndRefresh(): Promise<void> {
202
+ try {
203
+ // Skip validation for community tier — no license to validate
204
+ if (this.getTier() === "community") return;
205
+
206
+ // Dynamic import to avoid circular deps and allow Supabase to be optional
207
+ const { validateLicenseWithCloud } = await import("./cloud-validation");
208
+ const result = await validateLicenseWithCloud(this.cache?.email ?? "");
209
+
210
+ if (result.valid) {
211
+ const now = new Date();
212
+ db.update(licenseTable)
213
+ .set({
214
+ tier: result.tier,
215
+ status: "active",
216
+ lastValidatedAt: now,
217
+ gracePeriodExpiresAt: null,
218
+ expiresAt: result.expiresAt ?? null,
219
+ updatedAt: now,
220
+ })
221
+ .where(eq(licenseTable.id, LICENSE_ROW_ID))
222
+ .run();
223
+ } else {
224
+ this.enterGracePeriod();
225
+ }
226
+ } catch {
227
+ // Network failure — enter grace period if not already in one
228
+ if (this.cache?.status === "active" && this.cache?.lastValidatedAt) {
229
+ this.enterGracePeriod();
230
+ }
231
+ }
232
+
233
+ this.refreshCache();
234
+ }
235
+
236
+ private enterGracePeriod(): void {
237
+ if (this.cache?.status === "grace") return; // Already in grace
238
+
239
+ const now = new Date();
240
+ const graceExpiry = new Date(now.getTime() + GRACE_PERIOD_MS);
241
+
242
+ db.update(licenseTable)
243
+ .set({
244
+ status: "grace",
245
+ gracePeriodExpiresAt: graceExpiry,
246
+ updatedAt: now,
247
+ })
248
+ .where(eq(licenseTable.id, LICENSE_ROW_ID))
249
+ .run();
250
+ }
251
+
252
+ private checkGracePeriod(): void {
253
+ if (this.cache?.status !== "grace") return;
254
+ if (!this.cache.gracePeriodExpiresAt) return;
255
+
256
+ if (new Date() > this.cache.gracePeriodExpiresAt) {
257
+ // Grace period expired — degrade to community
258
+ this.deactivate();
259
+ }
260
+ }
261
+
262
+ private refreshCache(): void {
263
+ const rows = db
264
+ .select()
265
+ .from(licenseTable)
266
+ .where(eq(licenseTable.id, LICENSE_ROW_ID))
267
+ .all();
268
+
269
+ if (rows.length > 0) {
270
+ this.cache = this.rowToCache(rows[0]);
271
+ this.checkGracePeriod();
272
+ }
273
+ }
274
+
275
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
276
+ private rowToCache(row: any): CachedLicense {
277
+ return {
278
+ tier: row.tier as LicenseTier,
279
+ status: row.status as CachedLicense["status"],
280
+ email: row.email,
281
+ activatedAt: row.activatedAt,
282
+ expiresAt: row.expiresAt,
283
+ lastValidatedAt: row.lastValidatedAt,
284
+ gracePeriodExpiresAt: row.gracePeriodExpiresAt,
285
+ };
286
+ }
287
+ }
288
+
289
+ /** Singleton instance */
290
+ export const licenseManager = new LicenseManager();
@@ -0,0 +1,59 @@
1
+ /**
2
+ * License tier limit notifications.
3
+ * Mirrors the budget_alert notification pattern.
4
+ */
5
+
6
+ import { db } from "@/lib/db";
7
+ import { notifications } from "@/lib/db/schema";
8
+ import type { LimitResource } from "./tier-limits";
9
+ import { licenseManager } from "./manager";
10
+
11
+ export class TierLimitExceededError extends Error {
12
+ public readonly resource: LimitResource;
13
+ public readonly current: number;
14
+ public readonly limit: number;
15
+ public readonly tier: string;
16
+
17
+ constructor(resource: LimitResource, current: number, limit: number) {
18
+ const tier = licenseManager.getTier();
19
+ super(
20
+ `Tier limit exceeded: ${resource} (${current}/${limit}) on ${tier} tier`
21
+ );
22
+ this.name = "TierLimitExceededError";
23
+ this.resource = resource;
24
+ this.current = current;
25
+ this.limit = limit;
26
+ this.tier = tier;
27
+ }
28
+ }
29
+
30
+ const RESOURCE_LABELS: Record<LimitResource, string> = {
31
+ agentMemories: "Agent Memories",
32
+ contextVersions: "Context Versions",
33
+ activeSchedules: "Active Schedules",
34
+ historyRetentionDays: "History Retention",
35
+ parallelWorkflows: "Parallel Workflows",
36
+ };
37
+
38
+ /**
39
+ * Create a tier_limit notification to surface limit hits in the Inbox.
40
+ */
41
+ export async function createTierLimitNotification(
42
+ resource: LimitResource,
43
+ current: number,
44
+ limit: number,
45
+ taskId?: string
46
+ ): Promise<void> {
47
+ const tier = licenseManager.getTier();
48
+ const label = RESOURCE_LABELS[resource];
49
+
50
+ await db.insert(notifications).values({
51
+ id: crypto.randomUUID(),
52
+ taskId: taskId ?? null,
53
+ type: "tier_limit",
54
+ title: `${label} limit reached`,
55
+ body: `You've reached the ${tier} tier limit of ${limit} ${label.toLowerCase()}. Current usage: ${current}. Upgrade to unlock higher limits.`,
56
+ read: false,
57
+ createdAt: new Date(),
58
+ });
59
+ }
@@ -0,0 +1,71 @@
1
+ /**
2
+ * Tier limit constants for the PLG monetization system.
3
+ *
4
+ * Each tier includes all capabilities of lower tiers.
5
+ * "Unlimited" is represented as Infinity for easy comparison.
6
+ */
7
+
8
+ export const TIERS = ["community", "solo", "operator", "scale"] as const;
9
+ export type LicenseTier = (typeof TIERS)[number];
10
+
11
+ /** Numeric rank for tier comparison — higher = more capable */
12
+ export const TIER_RANK: Record<LicenseTier, number> = {
13
+ community: 0,
14
+ solo: 1,
15
+ operator: 2,
16
+ scale: 3,
17
+ } as const;
18
+
19
+ export type LimitResource =
20
+ | "agentMemories"
21
+ | "contextVersions"
22
+ | "activeSchedules"
23
+ | "historyRetentionDays"
24
+ | "parallelWorkflows";
25
+
26
+ export const TIER_LIMITS: Record<LicenseTier, Record<LimitResource, number>> = {
27
+ community: {
28
+ agentMemories: 50,
29
+ contextVersions: 10,
30
+ activeSchedules: 5,
31
+ historyRetentionDays: 30,
32
+ parallelWorkflows: 3,
33
+ },
34
+ solo: {
35
+ agentMemories: 200,
36
+ contextVersions: 50,
37
+ activeSchedules: 20,
38
+ historyRetentionDays: 180,
39
+ parallelWorkflows: 5,
40
+ },
41
+ operator: {
42
+ agentMemories: 500,
43
+ contextVersions: 100,
44
+ activeSchedules: 50,
45
+ historyRetentionDays: 365,
46
+ parallelWorkflows: 10,
47
+ },
48
+ scale: {
49
+ agentMemories: Infinity,
50
+ contextVersions: Infinity,
51
+ activeSchedules: Infinity,
52
+ historyRetentionDays: Infinity,
53
+ parallelWorkflows: Infinity,
54
+ },
55
+ } as const;
56
+
57
+ /** Human-friendly tier labels for UI display */
58
+ export const TIER_LABELS: Record<LicenseTier, string> = {
59
+ community: "Community",
60
+ solo: "Solo",
61
+ operator: "Operator",
62
+ scale: "Scale",
63
+ } as const;
64
+
65
+ /** Monthly pricing in USD (0 = free) */
66
+ export const TIER_PRICING: Record<LicenseTier, { monthly: number; annual: number }> = {
67
+ community: { monthly: 0, annual: 0 },
68
+ solo: { monthly: 19, annual: 190 },
69
+ operator: { monthly: 49, annual: 490 },
70
+ scale: { monthly: 99, annual: 990 },
71
+ } as const;
@@ -0,0 +1,107 @@
1
+ /**
2
+ * Marketplace client — Supabase CRUD for blueprints.
3
+ * All reads go through the marketplace-catalog Edge Function.
4
+ * Writes go directly to Supabase with RLS.
5
+ */
6
+
7
+ import { getSupabaseClient, isCloudConfigured } from "@/lib/cloud/supabase-client";
8
+
9
+ export interface MarketplaceBlueprint {
10
+ id: string;
11
+ title: string;
12
+ description: string | null;
13
+ category: string;
14
+ price_cents: number;
15
+ success_rate: number;
16
+ install_count: number;
17
+ tags: string[];
18
+ created_at: string;
19
+ }
20
+
21
+ export interface MarketplaceCatalogResult {
22
+ blueprints: MarketplaceBlueprint[];
23
+ total: number;
24
+ page: number;
25
+ limit: number;
26
+ }
27
+
28
+ /**
29
+ * Browse published blueprints (available to all tiers).
30
+ */
31
+ export async function browseBlueprints(
32
+ page: number = 1,
33
+ category?: string
34
+ ): Promise<MarketplaceCatalogResult> {
35
+ if (!isCloudConfigured()) {
36
+ return { blueprints: [], total: 0, page, limit: 20 };
37
+ }
38
+
39
+ const supabaseUrl = process.env.NEXT_PUBLIC_SUPABASE_URL!;
40
+ const anonKey = process.env.NEXT_PUBLIC_SUPABASE_ANON_KEY!;
41
+
42
+ const params = new URLSearchParams({ page: String(page) });
43
+ if (category) params.set("category", category);
44
+
45
+ try {
46
+ const res = await fetch(
47
+ `${supabaseUrl}/functions/v1/marketplace-catalog?${params}`,
48
+ { headers: { Authorization: `Bearer ${anonKey}` } }
49
+ );
50
+ if (!res.ok) return { blueprints: [], total: 0, page, limit: 20 };
51
+ return await res.json();
52
+ } catch {
53
+ return { blueprints: [], total: 0, page, limit: 20 };
54
+ }
55
+ }
56
+
57
+ /**
58
+ * Import a blueprint into local workflows (Solo+ tier).
59
+ */
60
+ export async function importBlueprint(
61
+ blueprintId: string
62
+ ): Promise<{ success: boolean; content?: string; error?: string }> {
63
+ const supabase = getSupabaseClient();
64
+ if (!supabase) return { success: false, error: "Cloud not configured" };
65
+
66
+ const { data, error } = await supabase
67
+ .from("blueprints")
68
+ .select("content")
69
+ .eq("id", blueprintId)
70
+ .single();
71
+
72
+ if (error || !data) {
73
+ return { success: false, error: error?.message ?? "Blueprint not found" };
74
+ }
75
+
76
+ return { success: true, content: data.content };
77
+ }
78
+
79
+ /**
80
+ * Publish a local workflow as a marketplace blueprint (Operator+ tier).
81
+ */
82
+ export async function publishBlueprint(params: {
83
+ title: string;
84
+ description: string;
85
+ category: string;
86
+ content: string;
87
+ tags: string[];
88
+ }): Promise<{ success: boolean; id?: string; error?: string }> {
89
+ const supabase = getSupabaseClient();
90
+ if (!supabase) return { success: false, error: "Cloud not configured" };
91
+
92
+ const { data, error } = await supabase
93
+ .from("blueprints")
94
+ .insert({
95
+ title: params.title,
96
+ description: params.description,
97
+ category: params.category,
98
+ content: params.content,
99
+ tags: params.tags,
100
+ status: "published",
101
+ })
102
+ .select("id")
103
+ .single();
104
+
105
+ if (error) return { success: false, error: error.message };
106
+ return { success: true, id: data?.id };
107
+ }