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.
- package/README.md +41 -0
- package/dist/cli.js +19 -2
- package/package.json +2 -1
- package/src/app/analytics/page.tsx +60 -0
- package/src/app/api/license/checkout/route.ts +28 -0
- package/src/app/api/license/portal/route.ts +26 -0
- package/src/app/api/license/route.ts +88 -0
- package/src/app/api/license/usage/route.ts +63 -0
- package/src/app/api/marketplace/browse/route.ts +15 -0
- package/src/app/api/marketplace/import/route.ts +28 -0
- package/src/app/api/marketplace/publish/route.ts +40 -0
- package/src/app/api/memory/route.ts +11 -0
- package/src/app/api/onboarding/email/route.ts +53 -0
- package/src/app/api/onboarding/progress/route.ts +60 -0
- package/src/app/api/schedules/route.ts +11 -0
- package/src/app/api/settings/telemetry/route.ts +14 -0
- package/src/app/api/sync/export/route.ts +54 -0
- package/src/app/api/sync/restore/route.ts +37 -0
- package/src/app/api/sync/sessions/route.ts +24 -0
- package/src/app/api/tasks/[id]/execute/route.ts +21 -0
- package/src/app/auth/callback/route.ts +79 -0
- package/src/app/marketplace/page.tsx +19 -0
- package/src/app/page.tsx +6 -2
- package/src/app/settings/page.tsx +8 -0
- package/src/components/analytics/analytics-dashboard.tsx +200 -0
- package/src/components/analytics/analytics-gate-card.tsx +101 -0
- package/src/components/marketplace/blueprint-card.tsx +61 -0
- package/src/components/marketplace/marketplace-browser.tsx +131 -0
- package/src/components/onboarding/activation-checklist.tsx +64 -0
- package/src/components/onboarding/donut-ring.tsx +52 -0
- package/src/components/onboarding/email-capture-card.tsx +104 -0
- package/src/components/settings/activation-form.tsx +95 -0
- package/src/components/settings/cloud-account-section.tsx +145 -0
- package/src/components/settings/cloud-sync-section.tsx +155 -0
- package/src/components/settings/subscription-section.tsx +410 -0
- package/src/components/settings/telemetry-section.tsx +80 -0
- package/src/components/shared/app-sidebar.tsx +136 -29
- package/src/components/shared/premium-gate-overlay.tsx +50 -0
- package/src/components/shared/schedule-gate-dialog.tsx +64 -0
- package/src/components/shared/upgrade-banner.tsx +112 -0
- package/src/hooks/use-snoozed-banners.ts +73 -0
- package/src/hooks/use-supabase-auth.ts +79 -0
- package/src/instrumentation.ts +34 -0
- package/src/lib/agents/__tests__/execution-manager.test.ts +28 -1
- package/src/lib/agents/__tests__/learned-context.test.ts +13 -0
- package/src/lib/agents/execution-manager.ts +35 -0
- package/src/lib/agents/learned-context.ts +13 -0
- package/src/lib/analytics/queries.ts +207 -0
- package/src/lib/billing/email.ts +54 -0
- package/src/lib/billing/products.ts +80 -0
- package/src/lib/billing/stripe.ts +101 -0
- package/src/lib/cloud/supabase-browser.ts +38 -0
- package/src/lib/cloud/supabase-client.ts +49 -0
- package/src/lib/constants/settings.ts +18 -0
- package/src/lib/data/clear.ts +5 -0
- package/src/lib/db/bootstrap.ts +16 -0
- package/src/lib/db/schema.ts +24 -0
- package/src/lib/license/__tests__/features.test.ts +56 -0
- package/src/lib/license/__tests__/key-format.test.ts +88 -0
- package/src/lib/license/__tests__/tier-limits.test.ts +79 -0
- package/src/lib/license/cloud-validation.ts +64 -0
- package/src/lib/license/features.ts +44 -0
- package/src/lib/license/key-format.ts +101 -0
- package/src/lib/license/limit-check.ts +111 -0
- package/src/lib/license/limit-queries.ts +51 -0
- package/src/lib/license/manager.ts +290 -0
- package/src/lib/license/notifications.ts +59 -0
- package/src/lib/license/tier-limits.ts +71 -0
- package/src/lib/marketplace/marketplace-client.ts +107 -0
- package/src/lib/sync/cloud-sync.ts +237 -0
- package/src/lib/telemetry/conversion-events.ts +73 -0
- package/src/lib/telemetry/queue.ts +122 -0
- package/src/lib/usage/ledger.ts +18 -0
- package/src/lib/validators/license.ts +33 -0
- package/tsconfig.json +2 -1
package/README.md
CHANGED
|
@@ -39,6 +39,47 @@ The AI agent stack is broken for business operators. You can spin up an agent in
|
|
|
39
39
|
|
|
40
40
|
---
|
|
41
41
|
|
|
42
|
+
## Community & Premium Editions
|
|
43
|
+
|
|
44
|
+
Stagent is **free and fully functional** as a local-first tool. The Community Edition (Apache 2.0) includes everything you need to run AI agents, workflows, and schedules on your machine. Premium tiers add cloud sync, expanded limits, marketplace access, and analytics for power users and teams.
|
|
45
|
+
|
|
46
|
+
### Feature Comparison
|
|
47
|
+
|
|
48
|
+
| Capability | Community (Free) | Premium |
|
|
49
|
+
|---|---|---|
|
|
50
|
+
| Local tasks & workflows | Unlimited | Unlimited |
|
|
51
|
+
| Agent profiles | All built-in | All built-in |
|
|
52
|
+
| Human-in-the-loop approval | Full | Full |
|
|
53
|
+
| Agent memories per profile | 50 | 200 - Unlimited |
|
|
54
|
+
| Learned context versions | 10 | 50 - Unlimited |
|
|
55
|
+
| Active schedules | 5 | 20 - Unlimited |
|
|
56
|
+
| History retention | 30 days | 180 days - Unlimited |
|
|
57
|
+
| Cloud sync & backup | — | Solo and above |
|
|
58
|
+
| Marketplace blueprints | Browse only | Import & Publish |
|
|
59
|
+
| Outcome analytics | — | Operator and above |
|
|
60
|
+
|
|
61
|
+
### Soft Limits
|
|
62
|
+
|
|
63
|
+
| Resource | Community Limit | At Limit |
|
|
64
|
+
|---|---|---|
|
|
65
|
+
| Agent memories | 50 per profile | New writes blocked, existing preserved |
|
|
66
|
+
| Context versions | 10 per profile | New proposals blocked |
|
|
67
|
+
| Active schedules | 5 | New schedule creation returns 402 |
|
|
68
|
+
| Task history | 30 days | Older entries pruned daily |
|
|
69
|
+
|
|
70
|
+
### Pricing
|
|
71
|
+
|
|
72
|
+
| Tier | Price | Best For |
|
|
73
|
+
|---|---|---|
|
|
74
|
+
| Community | Free forever | Individual developers, evaluation |
|
|
75
|
+
| Solo | $19/mo | Power users, expanded limits |
|
|
76
|
+
| Operator | $49/mo | Professionals, analytics, cloud sync |
|
|
77
|
+
| Scale | $99/mo | Teams, marketplace publishing, unlimited |
|
|
78
|
+
|
|
79
|
+
[Get Premium →](https://stagent.io/pricing)
|
|
80
|
+
|
|
81
|
+
---
|
|
82
|
+
|
|
42
83
|
## Runtime Bridge
|
|
43
84
|
|
|
44
85
|
Run the same business process on different AI providers without changing a line of configuration. Stagent's shared runtime registry routes tasks, schedules, and workflow steps through **Claude Code** (Anthropic Claude Agent SDK) and **OpenAI Codex App Server**, landing everything in the same inbox, monitoring, and cost surfaces. Switching providers is a settings change, not a rewrite.
|
package/dist/cli.js
CHANGED
|
@@ -129,7 +129,8 @@ var STAGENT_TABLES = [
|
|
|
129
129
|
"schedule_table_inputs",
|
|
130
130
|
"user_table_triggers",
|
|
131
131
|
"user_table_row_history",
|
|
132
|
-
"snapshots"
|
|
132
|
+
"snapshots",
|
|
133
|
+
"license"
|
|
133
134
|
];
|
|
134
135
|
function bootstrapStagentDatabase(sqlite2) {
|
|
135
136
|
sqlite2.exec(`
|
|
@@ -889,6 +890,21 @@ function bootstrapStagentDatabase(sqlite2) {
|
|
|
889
890
|
|
|
890
891
|
CREATE INDEX IF NOT EXISTS idx_snapshots_type ON snapshots(type);
|
|
891
892
|
CREATE INDEX IF NOT EXISTS idx_snapshots_created_at ON snapshots(created_at);
|
|
893
|
+
|
|
894
|
+
CREATE TABLE IF NOT EXISTS license (
|
|
895
|
+
id TEXT PRIMARY KEY NOT NULL,
|
|
896
|
+
supabase_user_id TEXT,
|
|
897
|
+
tier TEXT DEFAULT 'community' NOT NULL,
|
|
898
|
+
status TEXT DEFAULT 'inactive' NOT NULL,
|
|
899
|
+
email TEXT,
|
|
900
|
+
activated_at INTEGER,
|
|
901
|
+
expires_at INTEGER,
|
|
902
|
+
last_validated_at INTEGER,
|
|
903
|
+
grace_period_expires_at INTEGER,
|
|
904
|
+
encrypted_token TEXT,
|
|
905
|
+
created_at INTEGER NOT NULL,
|
|
906
|
+
updated_at INTEGER NOT NULL
|
|
907
|
+
);
|
|
892
908
|
`);
|
|
893
909
|
}
|
|
894
910
|
function hasLegacyStagentTables(sqlite2) {
|
|
@@ -1060,11 +1076,12 @@ async function main() {
|
|
|
1060
1076
|
port: actualPort
|
|
1061
1077
|
});
|
|
1062
1078
|
const sidecarUrl = buildSidecarUrl(actualPort);
|
|
1063
|
-
console.log(`Stagent ${pkg.version}`);
|
|
1079
|
+
console.log(`Stagent ${pkg.version} \u2014 Community Edition`);
|
|
1064
1080
|
console.log(`Data dir: ${DATA_DIR}`);
|
|
1065
1081
|
console.log(`Mode: ${isPrebuilt ? "production" : "development"}`);
|
|
1066
1082
|
console.log(`Next entry: ${nextEntrypoint}`);
|
|
1067
1083
|
console.log(`Starting Stagent on ${sidecarUrl}`);
|
|
1084
|
+
console.log(`Upgrade to Premium for cloud sync, expanded limits, and analytics \u2192 stagent.io/pricing`);
|
|
1068
1085
|
const child = spawn(process.execPath, [nextEntrypoint, ...nextArgs], {
|
|
1069
1086
|
cwd: effectiveCwd,
|
|
1070
1087
|
stdio: "inherit",
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "stagent",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "AI Business Operating System — run your business with AI agents. Local-first, multi-provider, governed.",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"ai",
|
|
@@ -67,6 +67,7 @@
|
|
|
67
67
|
"@dnd-kit/sortable": "^10.0.0",
|
|
68
68
|
"@dnd-kit/utilities": "^3.2.2",
|
|
69
69
|
"@hookform/resolvers": "^5.2.2",
|
|
70
|
+
"@supabase/supabase-js": "^2.101.1",
|
|
70
71
|
"@tailwindcss/postcss": "^4",
|
|
71
72
|
"@tailwindcss/typography": "^0.5",
|
|
72
73
|
"@tanstack/react-table": "^8.21.3",
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { Suspense } from "react";
|
|
2
|
+
import { PageShell } from "@/components/shared/page-shell";
|
|
3
|
+
import { AnalyticsDashboard } from "@/components/analytics/analytics-dashboard";
|
|
4
|
+
import { AnalyticsGateCard } from "@/components/analytics/analytics-gate-card";
|
|
5
|
+
import { licenseManager } from "@/lib/license/manager";
|
|
6
|
+
import {
|
|
7
|
+
getOutcomeCounts,
|
|
8
|
+
getSuccessRateTrend,
|
|
9
|
+
getCostPerOutcomeTrend,
|
|
10
|
+
getProfileLeaderboard,
|
|
11
|
+
getEstimatedHoursSaved,
|
|
12
|
+
} from "@/lib/analytics/queries";
|
|
13
|
+
|
|
14
|
+
export const dynamic = "force-dynamic";
|
|
15
|
+
|
|
16
|
+
function AnalyticsContent() {
|
|
17
|
+
const tier = licenseManager.getTierFromDb();
|
|
18
|
+
const isAllowed = tier !== "community";
|
|
19
|
+
|
|
20
|
+
const outcomes = getOutcomeCounts(30);
|
|
21
|
+
const successTrend = getSuccessRateTrend(30);
|
|
22
|
+
const costTrend = getCostPerOutcomeTrend(30);
|
|
23
|
+
const leaderboard = getProfileLeaderboard(30);
|
|
24
|
+
const hoursSaved = getEstimatedHoursSaved(30);
|
|
25
|
+
|
|
26
|
+
const dashboard = (
|
|
27
|
+
<AnalyticsDashboard
|
|
28
|
+
outcomes={outcomes}
|
|
29
|
+
successTrend={successTrend}
|
|
30
|
+
costTrend={costTrend}
|
|
31
|
+
leaderboard={leaderboard}
|
|
32
|
+
hoursSaved={hoursSaved}
|
|
33
|
+
/>
|
|
34
|
+
);
|
|
35
|
+
|
|
36
|
+
if (isAllowed) return dashboard;
|
|
37
|
+
|
|
38
|
+
return (
|
|
39
|
+
<div className="relative">
|
|
40
|
+
{/* Blurred dashboard preview */}
|
|
41
|
+
<div className="opacity-20 pointer-events-none select-none blur-[2px]" aria-hidden>
|
|
42
|
+
{dashboard}
|
|
43
|
+
</div>
|
|
44
|
+
{/* Upgrade CTA */}
|
|
45
|
+
<div className="absolute inset-0 flex items-start justify-center pt-16">
|
|
46
|
+
<AnalyticsGateCard />
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export default function AnalyticsPage() {
|
|
53
|
+
return (
|
|
54
|
+
<PageShell title="Analytics" description="Agent performance and ROI insights">
|
|
55
|
+
<Suspense fallback={<div className="animate-pulse h-64 bg-muted rounded-lg" />}>
|
|
56
|
+
<AnalyticsContent />
|
|
57
|
+
</Suspense>
|
|
58
|
+
</PageShell>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { createCheckoutSession } from "@/lib/billing/stripe";
|
|
3
|
+
import { TIERS } from "@/lib/license/tier-limits";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /api/license/checkout
|
|
7
|
+
* Creates a Stripe Checkout Session and returns the URL.
|
|
8
|
+
* Body: { tier: "solo"|"operator"|"scale", billingPeriod?: "monthly"|"annual" }
|
|
9
|
+
*/
|
|
10
|
+
export async function POST(req: NextRequest) {
|
|
11
|
+
const body = await req.json();
|
|
12
|
+
const { tier, billingPeriod } = body;
|
|
13
|
+
|
|
14
|
+
if (!tier || tier === "community" || !TIERS.includes(tier)) {
|
|
15
|
+
return NextResponse.json(
|
|
16
|
+
{ error: "Valid paid tier required (solo, operator, scale)" },
|
|
17
|
+
{ status: 400 }
|
|
18
|
+
);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const result = await createCheckoutSession(tier, billingPeriod ?? "monthly");
|
|
22
|
+
|
|
23
|
+
if ("error" in result) {
|
|
24
|
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return NextResponse.json({ url: result.url });
|
|
28
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { createPortalSession } from "@/lib/billing/stripe";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/license/portal
|
|
6
|
+
* Creates a Stripe Customer Portal session and redirects.
|
|
7
|
+
* Query: ?email=user@example.com
|
|
8
|
+
*/
|
|
9
|
+
export async function GET(req: NextRequest) {
|
|
10
|
+
const email = req.nextUrl.searchParams.get("email");
|
|
11
|
+
|
|
12
|
+
if (!email) {
|
|
13
|
+
return NextResponse.json(
|
|
14
|
+
{ error: "email query parameter required" },
|
|
15
|
+
{ status: 400 }
|
|
16
|
+
);
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const result = await createPortalSession(email);
|
|
20
|
+
|
|
21
|
+
if ("error" in result) {
|
|
22
|
+
return NextResponse.json({ error: result.error }, { status: 502 });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
return NextResponse.redirect(result.url);
|
|
26
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { licenseManager } from "@/lib/license/manager";
|
|
3
|
+
import { TIER_LIMITS, type LimitResource } from "@/lib/license/tier-limits";
|
|
4
|
+
import { LICENSE_FEATURES, canAccessFeature } from "@/lib/license/features";
|
|
5
|
+
import { activateLicenseSchema } from "@/lib/validators/license";
|
|
6
|
+
import type { LicenseTier } from "@/lib/license/tier-limits";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* GET /api/license — current license status, feature flags, and limits.
|
|
10
|
+
*/
|
|
11
|
+
export async function GET() {
|
|
12
|
+
const status = licenseManager.getStatus();
|
|
13
|
+
const tier = status.tier;
|
|
14
|
+
|
|
15
|
+
// Build feature access map
|
|
16
|
+
const features: Record<string, boolean> = {};
|
|
17
|
+
for (const feature of LICENSE_FEATURES) {
|
|
18
|
+
features[feature] = canAccessFeature(tier, feature);
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// Build limit map
|
|
22
|
+
const limits: Record<string, number> = {};
|
|
23
|
+
for (const key of Object.keys(TIER_LIMITS[tier]) as LimitResource[]) {
|
|
24
|
+
const val = TIER_LIMITS[tier][key];
|
|
25
|
+
limits[key] = val === Infinity ? -1 : val; // -1 = unlimited for JSON
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return NextResponse.json({
|
|
29
|
+
tier: status.tier,
|
|
30
|
+
status: status.status,
|
|
31
|
+
email: status.email,
|
|
32
|
+
activatedAt: status.activatedAt?.toISOString() ?? null,
|
|
33
|
+
expiresAt: status.expiresAt?.toISOString() ?? null,
|
|
34
|
+
lastValidatedAt: status.lastValidatedAt?.toISOString() ?? null,
|
|
35
|
+
gracePeriodExpiresAt: status.gracePeriodExpiresAt?.toISOString() ?? null,
|
|
36
|
+
isPremium: licenseManager.isPremium(),
|
|
37
|
+
features,
|
|
38
|
+
limits,
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* POST /api/license — activate a license.
|
|
44
|
+
* Accepts either a license key or direct tier/email/token (from Stripe webhook flow).
|
|
45
|
+
*/
|
|
46
|
+
export async function POST(req: NextRequest) {
|
|
47
|
+
const body = await req.json();
|
|
48
|
+
const parsed = activateLicenseSchema.safeParse(body);
|
|
49
|
+
|
|
50
|
+
if (!parsed.success) {
|
|
51
|
+
return NextResponse.json(
|
|
52
|
+
{ error: "Invalid request", details: parsed.error.flatten() },
|
|
53
|
+
{ status: 400 }
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
const { tier, email, token } = parsed.data;
|
|
58
|
+
|
|
59
|
+
if (!tier || !email) {
|
|
60
|
+
return NextResponse.json(
|
|
61
|
+
{ error: "tier and email are required for activation" },
|
|
62
|
+
{ status: 400 }
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
licenseManager.activate({
|
|
67
|
+
tier: tier as LicenseTier,
|
|
68
|
+
email,
|
|
69
|
+
encryptedToken: token,
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
return NextResponse.json({
|
|
73
|
+
success: true,
|
|
74
|
+
tier: licenseManager.getTier(),
|
|
75
|
+
status: licenseManager.getStatus().status,
|
|
76
|
+
});
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* DELETE /api/license — deactivate and revert to community.
|
|
81
|
+
*/
|
|
82
|
+
export async function DELETE() {
|
|
83
|
+
licenseManager.deactivate();
|
|
84
|
+
return NextResponse.json({
|
|
85
|
+
success: true,
|
|
86
|
+
tier: "community",
|
|
87
|
+
});
|
|
88
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { licenseManager } from "@/lib/license/manager";
|
|
3
|
+
import { getContextVersionCount, getActiveScheduleCount } from "@/lib/license/limit-queries";
|
|
4
|
+
import { getAllExecutions } from "@/lib/agents/execution-manager";
|
|
5
|
+
import { db } from "@/lib/db";
|
|
6
|
+
import { agentMemory } from "@/lib/db/schema";
|
|
7
|
+
import { eq, sql, desc } from "drizzle-orm";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* GET /api/license/usage
|
|
11
|
+
* Returns current usage counts and limits for the subscription UI.
|
|
12
|
+
*/
|
|
13
|
+
export async function GET() {
|
|
14
|
+
// Find the profile with the most memories for the usage display
|
|
15
|
+
const topProfile = db
|
|
16
|
+
.select({
|
|
17
|
+
profileId: agentMemory.profileId,
|
|
18
|
+
count: sql<number>`count(*)`,
|
|
19
|
+
})
|
|
20
|
+
.from(agentMemory)
|
|
21
|
+
.where(eq(agentMemory.status, "active"))
|
|
22
|
+
.groupBy(agentMemory.profileId)
|
|
23
|
+
.orderBy(desc(sql`count(*)`))
|
|
24
|
+
.limit(1)
|
|
25
|
+
.get();
|
|
26
|
+
|
|
27
|
+
const memoryCount = topProfile?.count ?? 0;
|
|
28
|
+
|
|
29
|
+
// Find the profile with the most context versions
|
|
30
|
+
// Use the same top profile for consistency
|
|
31
|
+
const contextCount = topProfile?.profileId
|
|
32
|
+
? getContextVersionCount(topProfile.profileId)
|
|
33
|
+
: 0;
|
|
34
|
+
|
|
35
|
+
const scheduleCount = getActiveScheduleCount();
|
|
36
|
+
const parallelCount = getAllExecutions().size;
|
|
37
|
+
|
|
38
|
+
const limits = {
|
|
39
|
+
agentMemories: licenseManager.getLimit("agentMemories"),
|
|
40
|
+
contextVersions: licenseManager.getLimit("contextVersions"),
|
|
41
|
+
activeSchedules: licenseManager.getLimit("activeSchedules"),
|
|
42
|
+
parallelWorkflows: licenseManager.getLimit("parallelWorkflows"),
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return NextResponse.json({
|
|
46
|
+
agentMemories: {
|
|
47
|
+
current: memoryCount,
|
|
48
|
+
limit: limits.agentMemories === Infinity ? -1 : limits.agentMemories,
|
|
49
|
+
},
|
|
50
|
+
contextVersions: {
|
|
51
|
+
current: contextCount,
|
|
52
|
+
limit: limits.contextVersions === Infinity ? -1 : limits.contextVersions,
|
|
53
|
+
},
|
|
54
|
+
activeSchedules: {
|
|
55
|
+
current: scheduleCount,
|
|
56
|
+
limit: limits.activeSchedules === Infinity ? -1 : limits.activeSchedules,
|
|
57
|
+
},
|
|
58
|
+
parallelWorkflows: {
|
|
59
|
+
current: parallelCount,
|
|
60
|
+
limit: limits.parallelWorkflows === Infinity ? -1 : limits.parallelWorkflows,
|
|
61
|
+
},
|
|
62
|
+
});
|
|
63
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { browseBlueprints } from "@/lib/marketplace/marketplace-client";
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GET /api/marketplace/browse?page=1&category=general
|
|
6
|
+
* Browse published marketplace blueprints. Available to all tiers.
|
|
7
|
+
*/
|
|
8
|
+
export async function GET(req: NextRequest) {
|
|
9
|
+
const { searchParams } = req.nextUrl;
|
|
10
|
+
const page = parseInt(searchParams.get("page") ?? "1", 10);
|
|
11
|
+
const category = searchParams.get("category") ?? undefined;
|
|
12
|
+
|
|
13
|
+
const result = await browseBlueprints(page, category);
|
|
14
|
+
return NextResponse.json(result);
|
|
15
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { licenseManager } from "@/lib/license/manager";
|
|
3
|
+
import { importBlueprint } from "@/lib/marketplace/marketplace-client";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /api/marketplace/import
|
|
7
|
+
* Import a blueprint from the marketplace. Requires Solo+ tier.
|
|
8
|
+
*/
|
|
9
|
+
export async function POST(req: NextRequest) {
|
|
10
|
+
if (!licenseManager.isFeatureAllowed("marketplace-import")) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: "Blueprint import requires Solo tier or above" },
|
|
13
|
+
{ status: 402 }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const { blueprintId } = await req.json();
|
|
18
|
+
if (!blueprintId) {
|
|
19
|
+
return NextResponse.json({ error: "blueprintId required" }, { status: 400 });
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const result = await importBlueprint(blueprintId);
|
|
23
|
+
if (!result.success) {
|
|
24
|
+
return NextResponse.json({ error: result.error }, { status: 500 });
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
return NextResponse.json({ content: result.content });
|
|
28
|
+
}
|
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { licenseManager } from "@/lib/license/manager";
|
|
3
|
+
import { publishBlueprint } from "@/lib/marketplace/marketplace-client";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* POST /api/marketplace/publish
|
|
7
|
+
* Publish a workflow blueprint to the marketplace. Requires Operator+ tier.
|
|
8
|
+
*/
|
|
9
|
+
export async function POST(req: NextRequest) {
|
|
10
|
+
if (!licenseManager.isFeatureAllowed("marketplace-publish")) {
|
|
11
|
+
return NextResponse.json(
|
|
12
|
+
{ error: "Blueprint publishing requires Operator tier or above" },
|
|
13
|
+
{ status: 402 }
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
const body = await req.json();
|
|
18
|
+
const { title, description, category, content, tags } = body;
|
|
19
|
+
|
|
20
|
+
if (!title || !content) {
|
|
21
|
+
return NextResponse.json(
|
|
22
|
+
{ error: "title and content are required" },
|
|
23
|
+
{ status: 400 }
|
|
24
|
+
);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
const result = await publishBlueprint({
|
|
28
|
+
title,
|
|
29
|
+
description: description ?? "",
|
|
30
|
+
category: category ?? "general",
|
|
31
|
+
content,
|
|
32
|
+
tags: tags ?? [],
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
if (!result.success) {
|
|
36
|
+
return NextResponse.json({ error: result.error }, { status: 500 });
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return NextResponse.json({ id: result.id }, { status: 201 });
|
|
40
|
+
}
|
|
@@ -3,6 +3,9 @@ import { db } from "@/lib/db";
|
|
|
3
3
|
import { agentMemory } from "@/lib/db/schema";
|
|
4
4
|
import { and, eq, desc } from "drizzle-orm";
|
|
5
5
|
import { randomUUID } from "crypto";
|
|
6
|
+
import { checkLimit, buildLimitErrorBody } from "@/lib/license/limit-check";
|
|
7
|
+
import { getMemoryCount } from "@/lib/license/limit-queries";
|
|
8
|
+
import { createTierLimitNotification } from "@/lib/license/notifications";
|
|
6
9
|
|
|
7
10
|
/**
|
|
8
11
|
* GET /api/memory?profileId=xxx&category=fact&status=active
|
|
@@ -75,6 +78,14 @@ export async function POST(req: NextRequest) {
|
|
|
75
78
|
);
|
|
76
79
|
}
|
|
77
80
|
|
|
81
|
+
// Tier limit check — memory cap per profile
|
|
82
|
+
const currentCount = getMemoryCount(profileId);
|
|
83
|
+
const limitResult = checkLimit("agentMemories", currentCount);
|
|
84
|
+
if (!limitResult.allowed) {
|
|
85
|
+
createTierLimitNotification("agentMemories", currentCount, limitResult.limit).catch(() => {});
|
|
86
|
+
return NextResponse.json(buildLimitErrorBody("agentMemories", limitResult), { status: 402 });
|
|
87
|
+
}
|
|
88
|
+
|
|
78
89
|
const now = new Date();
|
|
79
90
|
const id = randomUUID();
|
|
80
91
|
// Convert 0-1 confidence to 0-1000, default 700
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { z } from "zod";
|
|
3
|
+
import { getSupabaseClient } from "@/lib/cloud/supabase-client";
|
|
4
|
+
|
|
5
|
+
const emailSchema = z.object({
|
|
6
|
+
email: z.string().email(),
|
|
7
|
+
});
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* POST /api/onboarding/email
|
|
11
|
+
* Creates a Supabase Auth user via magic link.
|
|
12
|
+
* Fire-and-forget — does not block any local functionality.
|
|
13
|
+
*/
|
|
14
|
+
export async function POST(req: NextRequest) {
|
|
15
|
+
const body = await req.json();
|
|
16
|
+
const parsed = emailSchema.safeParse(body);
|
|
17
|
+
|
|
18
|
+
if (!parsed.success) {
|
|
19
|
+
return NextResponse.json(
|
|
20
|
+
{ error: "Valid email required" },
|
|
21
|
+
{ status: 400 }
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const supabase = getSupabaseClient();
|
|
26
|
+
if (!supabase) {
|
|
27
|
+
// Cloud not configured — silently succeed
|
|
28
|
+
return NextResponse.json({ ok: true });
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const { error } = await supabase.auth.signInWithOtp({
|
|
33
|
+
email: parsed.data.email,
|
|
34
|
+
options: {
|
|
35
|
+
emailRedirectTo: `${process.env.NEXT_PUBLIC_SUPABASE_URL ?? "http://localhost:3000"}/auth/callback`,
|
|
36
|
+
},
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
if (error) {
|
|
40
|
+
return NextResponse.json(
|
|
41
|
+
{ error: error.message },
|
|
42
|
+
{ status: 502 }
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
return NextResponse.json({ ok: true });
|
|
47
|
+
} catch {
|
|
48
|
+
return NextResponse.json(
|
|
49
|
+
{ error: "Failed to connect to cloud service" },
|
|
50
|
+
{ status: 502 }
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { NextResponse } from "next/server";
|
|
2
|
+
import { db } from "@/lib/db";
|
|
3
|
+
import { tasks, projects, schedules, settings } from "@/lib/db/schema";
|
|
4
|
+
import { eq, sql } from "drizzle-orm";
|
|
5
|
+
|
|
6
|
+
interface Milestone {
|
|
7
|
+
id: string;
|
|
8
|
+
label: string;
|
|
9
|
+
completed: boolean;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* GET /api/onboarding/progress
|
|
14
|
+
* Returns 6 milestones computed from existing DB data.
|
|
15
|
+
*/
|
|
16
|
+
export async function GET() {
|
|
17
|
+
const taskCount = db
|
|
18
|
+
.select({ count: sql<number>`count(*)` })
|
|
19
|
+
.from(tasks)
|
|
20
|
+
.get()?.count ?? 0;
|
|
21
|
+
|
|
22
|
+
const completedTaskCount = db
|
|
23
|
+
.select({ count: sql<number>`count(*)` })
|
|
24
|
+
.from(tasks)
|
|
25
|
+
.where(eq(tasks.status, "completed"))
|
|
26
|
+
.get()?.count ?? 0;
|
|
27
|
+
|
|
28
|
+
const projectCount = db
|
|
29
|
+
.select({ count: sql<number>`count(*)` })
|
|
30
|
+
.from(projects)
|
|
31
|
+
.get()?.count ?? 0;
|
|
32
|
+
|
|
33
|
+
const scheduleCount = db
|
|
34
|
+
.select({ count: sql<number>`count(*)` })
|
|
35
|
+
.from(schedules)
|
|
36
|
+
.get()?.count ?? 0;
|
|
37
|
+
|
|
38
|
+
const hasBudgetConfig = db
|
|
39
|
+
.select({ count: sql<number>`count(*)` })
|
|
40
|
+
.from(settings)
|
|
41
|
+
.where(eq(settings.key, "usage.budgetPolicy"))
|
|
42
|
+
.get()?.count ?? 0;
|
|
43
|
+
|
|
44
|
+
const milestones: Milestone[] = [
|
|
45
|
+
{ id: "create-task", label: "Create a task", completed: taskCount > 0 },
|
|
46
|
+
{ id: "complete-task", label: "Run a task to completion", completed: completedTaskCount > 0 },
|
|
47
|
+
{ id: "create-project", label: "Create a project", completed: projectCount > 0 },
|
|
48
|
+
{ id: "create-schedule", label: "Schedule a workflow", completed: scheduleCount > 0 },
|
|
49
|
+
{ id: "run-three", label: "Run 3 tasks", completed: completedTaskCount >= 3 },
|
|
50
|
+
{ id: "configure-budget", label: "Configure a budget", completed: hasBudgetConfig > 0 },
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
const completedCount = milestones.filter((m) => m.completed).length;
|
|
54
|
+
|
|
55
|
+
return NextResponse.json({
|
|
56
|
+
milestones,
|
|
57
|
+
completedCount,
|
|
58
|
+
totalCount: milestones.length,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
@@ -6,6 +6,9 @@ import { parseInterval, computeNextFireTime } from "@/lib/schedules/interval-par
|
|
|
6
6
|
import { parseNaturalLanguage } from "@/lib/schedules/nlp-parser";
|
|
7
7
|
import { resolveAgentRuntime } from "@/lib/agents/runtime/catalog";
|
|
8
8
|
import { validateRuntimeProfileAssignment } from "@/lib/agents/profiles/assignment-validation";
|
|
9
|
+
import { checkLimit, buildLimitErrorBody } from "@/lib/license/limit-check";
|
|
10
|
+
import { getActiveScheduleCount } from "@/lib/license/limit-queries";
|
|
11
|
+
import { createTierLimitNotification } from "@/lib/license/notifications";
|
|
9
12
|
|
|
10
13
|
export async function GET() {
|
|
11
14
|
const result = await db
|
|
@@ -127,6 +130,14 @@ export async function POST(req: NextRequest) {
|
|
|
127
130
|
return NextResponse.json({ error: compatibilityError }, { status: 400 });
|
|
128
131
|
}
|
|
129
132
|
|
|
133
|
+
// Tier limit check — active schedule cap
|
|
134
|
+
const activeCount = getActiveScheduleCount();
|
|
135
|
+
const limitResult = checkLimit("activeSchedules", activeCount);
|
|
136
|
+
if (!limitResult.allowed) {
|
|
137
|
+
createTierLimitNotification("activeSchedules", activeCount, limitResult.limit).catch(() => {});
|
|
138
|
+
return NextResponse.json(buildLimitErrorBody("activeSchedules", limitResult), { status: 402 });
|
|
139
|
+
}
|
|
140
|
+
|
|
130
141
|
const id = crypto.randomUUID();
|
|
131
142
|
const now = new Date();
|
|
132
143
|
const nextFireAt = computeNextFireTime(cronExpression, now);
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import { NextRequest, NextResponse } from "next/server";
|
|
2
|
+
import { getSetting, setSetting } from "@/lib/settings/helpers";
|
|
3
|
+
import { SETTINGS_KEYS } from "@/lib/constants/settings";
|
|
4
|
+
|
|
5
|
+
export async function GET() {
|
|
6
|
+
const enabled = await getSetting(SETTINGS_KEYS.TELEMETRY_ENABLED);
|
|
7
|
+
return NextResponse.json({ enabled: enabled === "true" });
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export async function POST(req: NextRequest) {
|
|
11
|
+
const { enabled } = await req.json();
|
|
12
|
+
await setSetting(SETTINGS_KEYS.TELEMETRY_ENABLED, String(!!enabled));
|
|
13
|
+
return NextResponse.json({ enabled: !!enabled });
|
|
14
|
+
}
|