@seldonframe/mcp 1.55.0 → 1.57.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@seldonframe/mcp",
3
- "version": "1.55.0",
3
+ "version": "1.57.0",
4
4
  "mcpName": "io.github.seldonframe/seldonframe-mcp",
5
5
  "description": "Open-source GoHighLevel alternative for agencies. 146+ MCP tools that let Claude Code spin up white-labeled client workspaces — CRM, booking, intake forms, landing pages, AI chatbot, and pre-wired agent archetypes (speed-to-lead, missed-call-text-back, review-requester) — in minutes. AGPL-3.0.",
6
6
  "license": "AGPL-3.0-or-later",
@@ -1,108 +1,157 @@
1
- // v1.55.0 — Standalone builder for the finalize_workspace operator summary.
2
- // Extracted from tools.js so we can unit-test the string-building logic
3
- // in isolation (no MCP server boot, no HTTP shims, no snapshot fetcher).
4
- //
5
- // The handler in tools.js fetches the snapshot, computes duration, and
6
- // passes the result here. This file just builds the string.
7
-
8
- /**
9
- * @param {object} args
10
- * @param {object} args.snapshot — workspace snapshot from
11
- * /api/v1/workspace/<id>/snapshot. Must include: workspace.name,
12
- * public_urls.{home,book,intake}, chatbot (or null), ops_stack,
13
- * available_automations, tier.
14
- * @param {number} args.durationSec — total workspace creation time.
15
- * @param {string|null} args.aestheticArchetype — workspace's classified
16
- * archetype (from snapshot.theme.aestheticArchetype). Used in the
17
- * closing landing-page nudge.
18
- * @returns {string} the formatted operator summary
19
- */
20
- export function buildFinalizeSummary({ snapshot, durationSec, aestheticArchetype }) {
21
- const ws = snapshot.workspace ?? {};
22
- const businessName = ws.name ?? "Your workspace";
23
- const chatbot = snapshot.chatbot ?? null;
24
- const opsStack = snapshot.ops_stack ?? {};
25
- const automations = snapshot.available_automations ?? [];
26
- const tier = snapshot.tier ?? {};
27
- const tierLabel = tier.current_tier_label ?? "Free";
28
- const isPaid = tier.current_tier === "growth" || tier.current_tier === "scale";
29
-
30
- // Extract client domain from the operator's input (the URL they scraped).
31
- // We store it on workspace.settings.source_url upstream; fall back to
32
- // the public preview URL host if unavailable.
33
- const clientDomain =
34
- (ws.settings && typeof ws.settings.source_url === "string"
35
- ? new URL(ws.settings.source_url).host
36
- : null) ?? "your client's site";
37
-
38
- const lines = [];
39
-
40
- // Header
41
- lines.push(`✅ Client ops stack ready for ${businessName}. (${durationSec} seconds)`);
42
- lines.push("");
43
-
44
- // Chatbot embed snippet (the magic moment — paste on client's existing site)
45
- if (chatbot && chatbot.embed_snippet) {
46
- lines.push(`📞 AI receptionist — paste before </body> on ${clientDomain} to go live:`);
47
- lines.push(chatbot.embed_snippet);
48
- lines.push("");
49
- lines.push(`🤖 Demo for your client: ${chatbot.preview_url ?? snapshot.public_urls?.home ?? ""}`);
50
- lines.push(` (Chatbot live in TEST mode — share so your client can try it before pasting)`);
51
- } else {
52
- lines.push(`🤖 AI chatbot — scaffold pending. Retry:`);
53
- lines.push(` create_agent({ archetype: "website-chatbot", channel: "web_chat" })`);
54
- }
55
- lines.push("");
56
-
57
- // Ops stack URLs
58
- lines.push(`📅 Booking: ${opsStack.booking_url ?? snapshot.public_urls?.book ?? ""}`);
59
- lines.push(`📝 Intake: ${opsStack.intake_url ?? snapshot.public_urls?.intake ?? ""}`);
60
- lines.push(`🔧 Admin: ${opsStack.admin_url ?? ""}`);
61
- lines.push("");
62
-
63
- // 7-automation callout
64
- if (automations.length > 0) {
65
- lines.push(`⚡ ${automations.length} more automations ready to deploy for this client:`);
66
- const descriptions = {
67
- "speed-to-lead": "text the lead within 30 sec of intake submission",
68
- "missed-call-text-back": "auto-SMS when their phone goes unanswered",
69
- "review-requester": "ask for a 5★ after every completed booking",
70
- "appointment-confirm-sms": "reduce no-shows automatically",
71
- "weather-aware-booking": "reschedule outdoor jobs when rain is forecast",
72
- "daily-digest": "morning summary of yesterday's activity",
73
- "win-back": "re-engage cancelled subscribers with a time-limited code",
74
- };
75
- for (const a of automations) {
76
- const desc = descriptions[a.id] ?? "";
77
- lines.push(` • ${a.name}${desc ? " — " + desc : ""}`);
78
- }
79
- lines.push(` Activate any: ${opsStack.automations_url ?? ""}`);
80
- lines.push(
81
- ` (Need API keys for SMS/email? Just ask — Claude will walk you through`,
82
- );
83
- lines.push(` Twilio / Resend / Stripe setup when an automation needs one.)`);
84
- lines.push("");
85
- }
86
-
87
- // Tier + client portal
88
- const tierUpsell = isPaid
89
- ? "white-label + reseller pricing on Scale ($99/mo)"
90
- : "Upgrade $9/mo for unlimited workspaces";
91
- const clientPortalUrl = tier.client_portal_url ?? "";
92
- lines.push(
93
- `💼 Tier: ${tierLabel} · ${tierUpsell}` +
94
- (clientPortalUrl ? ` · Client portal: ${clientPortalUrl}` : ""),
95
- );
96
- lines.push("");
97
-
98
- // Landing-page nudge (closing)
99
- const archetypeClause = aestheticArchetype ? ` in ${aestheticArchetype} style` : "";
100
- lines.push(
101
- `Want a landing page too? Just ask: "build a landing page for ${businessName}${archetypeClause}"`,
102
- );
103
- lines.push(
104
- `— Claude will use the landing-page-creation skill to generate one${aestheticArchetype ? " with the archetype voice" : ""}.`,
105
- );
106
-
107
- return lines.join("\n");
108
- }
1
+ // v1.55.0 — Standalone builder for the finalize_workspace operator summary.
2
+ // Extracted from tools.js so we can unit-test the string-building logic
3
+ // in isolation (no MCP server boot, no HTTP shims, no snapshot fetcher).
4
+ //
5
+ // The handler in tools.js fetches the snapshot, computes duration, and
6
+ // passes the result here. This file just builds the string.
7
+
8
+ /**
9
+ * @param {object} args
10
+ * @param {object} args.snapshot — workspace snapshot from
11
+ * /api/v1/workspace/<id>/snapshot. Must include: workspace.name,
12
+ * public_urls.{home,book,intake}, chatbot (or null), ops_stack,
13
+ * available_automations, tier.
14
+ * @param {number} args.durationSec — total workspace creation time.
15
+ * @param {string|null} args.aestheticArchetype — workspace's classified
16
+ * archetype (from snapshot.theme.aestheticArchetype). Used in the
17
+ * closing landing-page nudge.
18
+ * @returns {string} the formatted operator summary
19
+ */
20
+ export function buildFinalizeSummary({ snapshot, durationSec, aestheticArchetype }) {
21
+ const ws = snapshot.workspace ?? {};
22
+ const businessName = ws.name ?? "Your workspace";
23
+ const chatbot = snapshot.chatbot ?? null;
24
+ const opsStack = snapshot.ops_stack ?? {};
25
+ const automations = snapshot.available_automations ?? [];
26
+ const tier = snapshot.tier ?? {};
27
+ const tierLabel = tier.current_tier_label ?? "Free";
28
+ const isPaid = tier.current_tier === "growth" || tier.current_tier === "scale";
29
+
30
+ // Extract client domain from the operator's input (the URL they scraped).
31
+ // We store it on workspace.settings.source_url upstream; fall back to
32
+ // the public preview URL host if unavailable.
33
+ const clientDomain =
34
+ (ws.settings && typeof ws.settings.source_url === "string"
35
+ ? new URL(ws.settings.source_url).host
36
+ : null) ?? "your client's site";
37
+
38
+ const lines = [];
39
+
40
+ // Header
41
+ lines.push(`✅ Client ops stack ready for ${businessName}. (${durationSec} seconds)`);
42
+ lines.push("");
43
+
44
+ // Chatbot embed snippet (the magic moment — paste on client's existing site)
45
+ if (chatbot && chatbot.embed_snippet) {
46
+ lines.push(`📞 AI receptionist — paste before </body> on ${clientDomain} to go live:`);
47
+ lines.push(chatbot.embed_snippet);
48
+ lines.push("");
49
+ lines.push(`🤖 Demo for your client: ${chatbot.preview_url ?? snapshot.public_urls?.home ?? ""}`);
50
+ lines.push(` (Chatbot is live and answering on your landing — share so your client can try it before pasting)`);
51
+ } else {
52
+ lines.push(`🤖 AI chatbot — scaffold pending. Retry:`);
53
+ lines.push(` create_agent({ archetype: "website-chatbot", channel: "web_chat" })`);
54
+ }
55
+ lines.push("");
56
+
57
+ // Ops stack URLs
58
+ lines.push(`📅 Booking: ${opsStack.booking_url ?? snapshot.public_urls?.book ?? ""}`);
59
+ lines.push(`📝 Intake: ${opsStack.intake_url ?? snapshot.public_urls?.intake ?? ""}`);
60
+ lines.push(`🔧 Admin: ${opsStack.admin_url ?? ""}`);
61
+ lines.push("");
62
+
63
+ // 7-automation callout
64
+ if (automations.length > 0) {
65
+ lines.push(`⚡ ${automations.length} more automations ready to deploy for this client:`);
66
+ const descriptions = {
67
+ "speed-to-lead": "text the lead within 30 sec of intake submission",
68
+ "missed-call-text-back": "auto-SMS when their phone goes unanswered",
69
+ "review-requester": "ask for a 5★ after every completed booking",
70
+ "appointment-confirm-sms": "reduce no-shows automatically",
71
+ "weather-aware-booking": "reschedule outdoor jobs when rain is forecast",
72
+ "daily-digest": "morning summary of yesterday's activity",
73
+ "win-back": "re-engage cancelled subscribers with a time-limited code",
74
+ };
75
+ for (const a of automations) {
76
+ const desc = descriptions[a.id] ?? "";
77
+ lines.push(` • ${a.name}${desc ? " — " + desc : ""}`);
78
+ }
79
+ lines.push(` Activate any: ${opsStack.automations_url ?? ""}`);
80
+ lines.push(
81
+ ` (Need API keys for SMS/email? Just ask — Claude will walk you through`,
82
+ );
83
+ lines.push(` Twilio / Resend / Stripe setup when an automation needs one.)`);
84
+ lines.push("");
85
+ }
86
+
87
+ // Tier + client portal
88
+ const tierUpsell = isPaid
89
+ ? "white-label + reseller pricing on Scale ($99/mo)"
90
+ : "Upgrade $9/mo for unlimited workspaces";
91
+ const clientPortalUrl = tier.client_portal_url ?? "";
92
+ lines.push(
93
+ `💼 Tier: ${tierLabel} · ${tierUpsell}` +
94
+ (clientPortalUrl ? ` · Client portal: ${clientPortalUrl}` : ""),
95
+ );
96
+ lines.push("");
97
+
98
+ // v1.55.x — derive appHost for the LLM-settings + client-portal links.
99
+ // SELDONFRAME_APP_BASE wins when set (white-label/staging); falls back
100
+ // to https://app.seldonframe.com. Trailing slash stripped to match the
101
+ // pattern used by v2/complete when building ops_stack.automations_url.
102
+ const appHost = (
103
+ process.env.SELDONFRAME_APP_BASE ?? "https://app.seldonframe.com"
104
+ ).replace(/\/$/, "");
105
+
106
+ // LLM key clarity — operators kept asking "which key is the chatbot
107
+ // using?" + had no recovery path when llm_credit_exhausted fired.
108
+ lines.push(
109
+ `🔑 Chatbot LLM key: uses your Claude Code key by default. Change at ${appHost}/settings/integrations/llm.`,
110
+ );
111
+ lines.push(
112
+ ` (If you see "llm_credit_exhausted", top up at console.anthropic.com/settings/billing.)`,
113
+ );
114
+ lines.push("");
115
+
116
+ // v1.55.x — Client portal demo callout. Closes the demo loop:
117
+ // chatbot (above) → booking → CRM → portal demo for the client.
118
+ // Slug is required; skip gracefully if absent (shouldn't happen in
119
+ // practice but the snapshot can theoretically lack it).
120
+ //
121
+ // The /demo URL is a one-click deep link: workspace creation seeds
122
+ // a "Demo Customer" contact + sample upcoming appointment + welcome
123
+ // message, and visiting /customer/<slug>/demo auto-establishes a
124
+ // portal session for that contact — no email, no magic link. The
125
+ // operator can paste this URL to a prospect and they land directly
126
+ // in a populated portal instead of a magic-link form with empty
127
+ // tabs. Demo data is filtered out of operator-facing CRM / pipeline
128
+ // / analytics by tag, so it stays isolated from real customer data.
129
+ const slug = ws.slug ?? "";
130
+ if (slug) {
131
+ const portalUrl = `${appHost}/customer/${slug}/demo`;
132
+ lines.push(`🎬 Demo the client portal (one-click, no login):`);
133
+ lines.push(` ${portalUrl}`);
134
+ lines.push(
135
+ ` Share this link with the prospect — opens directly as "Demo Customer" with a`,
136
+ );
137
+ lines.push(
138
+ ` sample appointment and welcome message. Demo data stays isolated from your`,
139
+ );
140
+ lines.push(` real CRM and pipelines.`);
141
+ lines.push(
142
+ ` Free tier shows the portal with SeldonFrame branding. Growth ($29/mo) unlocks custom domain + the agency's logo.`,
143
+ );
144
+ lines.push("");
145
+ }
146
+
147
+ // Landing-page nudge (closing)
148
+ const archetypeClause = aestheticArchetype ? ` in ${aestheticArchetype} style` : "";
149
+ lines.push(
150
+ `Want a landing page too? Just ask: "build a landing page for ${businessName}${archetypeClause}"`,
151
+ );
152
+ lines.push(
153
+ `— Claude will use the landing-page-creation skill to generate one${aestheticArchetype ? " with the archetype voice" : ""}.`,
154
+ );
155
+
156
+ return lines.join("\n");
157
+ }
package/src/tools.js CHANGED
@@ -4777,6 +4777,78 @@ export const TOOLS = [
4777
4777
  },
4778
4778
  },
4779
4779
 
4780
+ // ───────────────────────────────────────────────────────────────────────
4781
+ // v1.57.0 — DEPLOY-TO-SELL. The self-serve deployment verb: take a
4782
+ // builder-owned template (or a purchased marketplace listing) and turn
4783
+ // it into a REAL, answering agent — one call, idempotent, resumable
4784
+ // across the human-only connect steps (calendar OAuth / phone number).
4785
+ // This is the money moment: a builder goes from "I built an agent" to
4786
+ // "I'm earning from it" without leaving the IDE except for the one-time
4787
+ // OAuth/Twilio connect screen the wizardUrl points at.
4788
+ // ───────────────────────────────────────────────────────────────────────
4789
+
4790
+ {
4791
+ name: "deploy_agent",
4792
+ description:
4793
+ "USE WHEN USER SAYS: 'deploy this agent', 'go live with my template', 'sell this agent — get it answering', 'launch the receptionist I built', 'turn my template into a real number', 'ship this to a client'. " +
4794
+ "Deploys a self-built template (source.template_id) or an installed marketplace listing (source.listing_slug) into a REAL, answering agent for THIS workspace — the deploy-to-sell / earn moment, not just a build step. Idempotent: calling it again for the same source resumes the same deployment instead of creating a duplicate. " +
4795
+ "RESUMABLE TWO-STEP FLOW — read the `status` field: " +
4796
+ "• status='needs_connect' → NOT live yet. `requirements`/`missing` list what's unmet (calendar OAuth, another connector, a phone number, business info) and `wizardUrl` is a ONE-TIME human-only link (calendar OAuth + Twilio number can't be done by an agent). Relay it verbatim: 'Open this once to connect your calendar / phone number: <wizardUrl>' — then, once the human confirms they've done it, call deploy_agent again with the SAME source (and `phone` if the agent needs a number) to proceed. " +
4797
+ "• status='live' → deployed and answering right now. Tell the operator the number (if any) is live and taking real calls/bookings — this is revenue-generating, not a sandbox. " +
4798
+ "• status='disabled' → the deploy verb isn't enabled for this environment yet. " +
4799
+ "PHONE: only required when the template needs a number and none is attached yet — pass phone={mode:'forward', number:'+1...'} to forward an existing line, or phone={mode:'provision', area_code:'512'} to buy a new Twilio number (requires the workspace's own Twilio creds — surfaced as a `telephony` requirement if missing). " +
4800
+ "Exactly one of source.template_id / source.listing_slug is required — template_id for something you built with create_agent-style tools in THIS workspace, listing_slug for a marketplace agent you're installing to run for a client.",
4801
+ inputSchema: obj(
4802
+ {
4803
+ workspace_id: str("Workspace id (bearer workspace)."),
4804
+ source: {
4805
+ type: "object",
4806
+ description:
4807
+ "Exactly one of template_id (a template this workspace built) or listing_slug (a published marketplace listing to install-and-deploy).",
4808
+ properties: {
4809
+ template_id: str("Id of an agent_templates row owned by this workspace."),
4810
+ listing_slug: str("Slug of a published marketplace listing (kind='agent') to clone in and deploy."),
4811
+ },
4812
+ },
4813
+ phone: {
4814
+ type: "object",
4815
+ description:
4816
+ "Only needed when deploy_agent (or an earlier call) reported a 'telephony' requirement and no number is attached yet. Pass ONE mode.",
4817
+ properties: {
4818
+ mode: {
4819
+ type: "string",
4820
+ enum: ["forward", "provision"],
4821
+ description: "'forward' = point an existing number at the agent; 'provision' = buy a new Twilio number.",
4822
+ },
4823
+ number: str("E.164 phone number to forward, e.g. '+15125550148'. Required when mode='forward'."),
4824
+ area_code: str("3-digit US/NANP area code to provision a new number in, e.g. '512'. Required when mode='provision'."),
4825
+ },
4826
+ },
4827
+ },
4828
+ ["workspace_id", "source"],
4829
+ ),
4830
+ handler: async (args) => {
4831
+ const ws = args.workspace_id;
4832
+ const source = args.source ?? {};
4833
+ const body = {
4834
+ source: {
4835
+ ...(source.template_id ? { templateId: source.template_id } : {}),
4836
+ ...(source.listing_slug ? { listingSlug: source.listing_slug } : {}),
4837
+ },
4838
+ };
4839
+ if (args.phone?.mode === "forward") {
4840
+ body.phone = { mode: "forward", number: args.phone.number };
4841
+ } else if (args.phone?.mode === "provision") {
4842
+ body.phone = { mode: "provision", areaCode: args.phone.area_code };
4843
+ }
4844
+ const result = await api("POST", "/build/deploy", {
4845
+ body,
4846
+ workspace_id: ws,
4847
+ });
4848
+ return result;
4849
+ },
4850
+ },
4851
+
4780
4852
  {
4781
4853
  name: "update_agent_blueprint",
4782
4854
  description:
@@ -5639,6 +5711,109 @@ export const TOOLS = [
5639
5711
  });
5640
5712
  },
5641
5713
  },
5714
+
5715
+ // Phase T — natural-language R1 landing-page editor.
5716
+ //
5717
+ // Wraps POST /api/v1/landing/r1/customize. Same handler as the in-app
5718
+ // editor at /clients/<slug>/landing/edit. Auth flows through the workspace
5719
+ // bearer stored in ~/.seldonframe/device.json.
5720
+ //
5721
+ // IMPORTANT: The target workspace MUST already have an R1 landing page
5722
+ // (generated by create_full_workspace or create_workspace_from_url with
5723
+ // the R-framework pipeline). If no R1 page exists, the tool returns
5724
+ // { ok: false, error: "no_landing_exists" }.
5725
+ {
5726
+ name: "customize_landing",
5727
+ description:
5728
+ "Apply a natural-language instruction to the current R1 landing page for a workspace. " +
5729
+ "The LLM rewrites only the parts of the payload touched by the instruction, preserving " +
5730
+ "everything else. Each call creates an immutable version snapshot so the operator can " +
5731
+ "undo any change. Edits go live immediately (no publish step). " +
5732
+ "Examples:\n" +
5733
+ " • customize_landing({ workspace_id: '...', instruction: 'Change the hero tagline to focus on 24/7 emergency service' })\n" +
5734
+ " • customize_landing({ workspace_id: '...', instruction: 'Replace the hero photo with https://example.com/photo.jpg' })\n" +
5735
+ " • customize_landing({ workspace_id: '...', instruction: 'Make the FAQ more local-flavored for Phoenix residents' })\n" +
5736
+ " • customize_landing({ workspace_id: '...', instruction: 'Add a question about financing options to the FAQ' })\n" +
5737
+ "Requires: the workspace must have an R1 landing page (auto-generated during workspace creation). " +
5738
+ "Requires: an Anthropic API key configured for the workspace (BYOK via /settings/integrations or platform fallback). " +
5739
+ "Returns: { ok: true, summary, version_id } on success. " +
5740
+ "Returns: { ok: false, error: 'no_landing_exists' } if the workspace has no R1 landing yet.",
5741
+ inputSchema: obj(
5742
+ {
5743
+ workspace_id: str("UUID of the workspace whose landing page to edit."),
5744
+ instruction: str(
5745
+ "Natural-language description of what to change. Be specific. " +
5746
+ "Include image URLs verbatim if swapping a photo. " +
5747
+ "Example: 'Change the hero tagline to: Fast. Local. Trusted.' or " +
5748
+ "'Replace the hero photo with https://images.unsplash.com/photo-...'",
5749
+ ),
5750
+ },
5751
+ ["workspace_id", "instruction"],
5752
+ ),
5753
+ handler: async (a) => {
5754
+ const ws = wsOrDefault(a.workspace_id);
5755
+ if (typeof a.instruction !== "string" || !a.instruction.trim()) {
5756
+ throw new Error("instruction must be a non-empty string.");
5757
+ }
5758
+ return api("POST", "/landing/r1/customize", {
5759
+ body: { instruction: a.instruction },
5760
+ workspace_id: ws,
5761
+ });
5762
+ },
5763
+ },
5764
+
5765
+ // Phase T — list R1 landing version history.
5766
+ {
5767
+ name: "list_landing_versions",
5768
+ description:
5769
+ "Return the edit history for a workspace's R1 landing page, newest first. " +
5770
+ "Each row has: id, instruction, summary, createdAt. Pass a version id to " +
5771
+ "customize_landing_revert to undo to that point. " +
5772
+ "Example: list_landing_versions({ workspace_id: '...' })",
5773
+ inputSchema: obj(
5774
+ {
5775
+ workspace_id: str("UUID of the workspace."),
5776
+ limit: {
5777
+ type: "number",
5778
+ description: "Optional. Max rows to return (default 20, max 100).",
5779
+ },
5780
+ },
5781
+ ["workspace_id"],
5782
+ ),
5783
+ handler: async (a) => {
5784
+ const ws = wsOrDefault(a.workspace_id);
5785
+ const qs = a.limit ? `?limit=${encodeURIComponent(String(a.limit))}` : "";
5786
+ return api("GET", `/landing/r1/versions${qs}`, { workspace_id: ws });
5787
+ },
5788
+ },
5789
+
5790
+ // Phase T — revert R1 landing to a prior version.
5791
+ {
5792
+ name: "revert_landing",
5793
+ description:
5794
+ "Revert the R1 landing page to a prior version's payload snapshot. " +
5795
+ "Creates a new immutable version row (never deletes history). " +
5796
+ "The page goes live immediately with the reverted content. " +
5797
+ "Get version IDs from list_landing_versions. " +
5798
+ "Example: revert_landing({ workspace_id: '...', version_id: '<uuid>' })",
5799
+ inputSchema: obj(
5800
+ {
5801
+ workspace_id: str("UUID of the workspace."),
5802
+ version_id: str("UUID of the version to revert to (from list_landing_versions)."),
5803
+ },
5804
+ ["workspace_id", "version_id"],
5805
+ ),
5806
+ handler: async (a) => {
5807
+ const ws = wsOrDefault(a.workspace_id);
5808
+ if (typeof a.version_id !== "string" || !a.version_id.trim()) {
5809
+ throw new Error("version_id must be a non-empty string.");
5810
+ }
5811
+ return api("POST", "/landing/r1/revert", {
5812
+ body: { version_id: a.version_id },
5813
+ workspace_id: ws,
5814
+ });
5815
+ },
5816
+ },
5642
5817
  ];
5643
5818
 
5644
5819
  export const TOOL_MAP = Object.fromEntries(TOOLS.map((t) => [t.name, t]));