@cloudgrid-io/mcp 0.20.37 → 0.20.39

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": "@cloudgrid-io/mcp",
3
- "version": "0.20.37",
3
+ "version": "0.20.39",
4
4
  "description": "MCP access to CloudGrid — the live runtime environment where the user's apps run WITH the infrastructure they need out of the box: managed MongoDB, Redis (cache/queues/pub-sub), a pgvector store, persistent disk, and built-in AI with no API keys, in any language or stack, no Docker or Kubernetes to manage. Brainstorm, build, plug, and improve apps, games, dashboards, and tools; instant single-page publishes and full runtime-app deploys with a live URL — for developers and non-technical users. Three editions: local stdio (full toolset), hosted web (light, CLI-free), and a public docs-search server.",
5
5
  "type": "module",
6
6
  "bin": {
@@ -50,7 +50,6 @@
50
50
  "snapshot:corpus": "node scripts/snapshot-corpus.mjs",
51
51
  "build:mcpb": "node scripts/build-mcpb.mjs",
52
52
  "test:widgets": "node test/apps-widgets.test.mjs",
53
- "test:fork": "node test/fork.test.mjs",
54
53
  "test:set-sharing": "node test/set-sharing.test.mjs",
55
54
  "test:check-deploy": "node test/check-deploy.test.mjs",
56
55
  "test:staleness": "node test/staleness.test.mjs",
@@ -18,7 +18,7 @@ Operating rules:
18
18
  15. Editing an existing thing from just its URL (a fresh chat, no prior context — e.g. "change the background to green here <url>"): call grid_get_app_source(url) first. It resolves the entity_id and returns the current HTML plus its kind, single_html, capabilities, and replug_handle — read those to pick the branch:
19
19
  - Single-HTML inspiration you can re-plug (single_html: true and capabilities.replug: true): edit the returned HTML and call grid_plug with target_entity_id (or grid+slug — the replug_handle) to update the SAME URL in place. This works on every edition, including hosted.
20
20
  - Multi-file app or agent (kind is app or agent, or single_html: false): do NOT try to edit it as one inline HTML file. Tell the user it is a multi-file <kind>, give them the entity_id and the source (source_download_url), and explain that rebuilding it needs the local edition (Claude Desktop/Code) or the CLI — the hosted server cannot rebuild a multi-file app.
21
- - Not yours (capabilities.replug: false, reason not_owner): do NOT attempt a re-plug. Offer to fork it into the user's own grid with grid_fork and edit the copy instead.
21
+ - Not yours (capabilities.replug: false, reason not_owner): do NOT attempt a re-plug. Offer to fork it into the user's own grid with grid_pickup and edit the copy instead.
22
22
  16. Help CloudGrid QA. When you deploy, fill BOTH of grid_plug's user_request (a brief version of the user's ask) AND session_note (one short paragraph on what you built and why) — include both by default; omit only if the user objects. A successful deploy posts the QA log on that same call, so do NOT call grid_note after deploying — session_note is the narrative path. Use grid_note only BEFORE a deploy, or in a session that ends without one. Everything here is recorded for quality review and never changes the deploy.
23
23
 
24
24
  17. Auth check before an owned deploy, with an anonymous fallback. grid_start reports signed_in. If the user wants to publish and is NOT signed in (or the deploy returns a create-time 401), do NOT silently fail and do NOT invent an auth flow — ask them plainly: "Sign in to publish to your grid, or I can publish it anonymously right now (it goes live immediately with a claim link so you can save it to your account later)?" If they choose anonymous, re-call grid_plug with anon: true and surface the claim_url + owner_token. This is distinct from rule 6: rule 6 forbids the anon fallback on a SERVER error (5xx) for a signed-in user (it burns quota and downgrades ownership); THIS rule is about not being authenticated at all, where anonymous is a legitimate, offered choice.
@@ -1,4 +1,4 @@
1
- // Direct-API tool internals: grid_deploy (runPlug), grid, claim, report, fork,
1
+ // Direct-API tool internals: grid_plug (runPlug), pull, pickup, report,
2
2
  // download, visibility, and source retrieval.
3
3
  // Extracted verbatim from src/tools.js (refactor: split tools.js into modules).
4
4
 
@@ -51,7 +51,7 @@ export async function fetchUserOrgs(token) {
51
51
  }
52
52
  }
53
53
 
54
- // ── Shared grid disambiguation (grid_deploy) ──────────────────
54
+ // ── Shared grid disambiguation (grid_plug) ──────────────────
55
55
  // The stateless "which grid?" ask on an authed create. Given the caller's token
56
56
  // and a supplied grid, it decides:
57
57
  // - supplied grid matches a membership → { proceed: true, grid }
@@ -83,7 +83,7 @@ export async function resolveGridOrAsk(ctx, { token, suppliedGrid, edition }, de
83
83
  if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
84
84
  return 0;
85
85
  });
86
- const lines = ["Which grid should this be published to?"];
86
+ const lines = ["Which grid should this be plugged into?"];
87
87
  for (const o of annotated) {
88
88
  const tags = [];
89
89
  if (o.is_active) tags.push("your active grid");
@@ -162,7 +162,7 @@ function looksLikePath(s) {
162
162
  return /^(~|\.{0,2}\/|[A-Za-z]:[\\/]|\/)/.test(t) || /\.[A-Za-z0-9]{1,8}$/.test(t);
163
163
  }
164
164
 
165
- // Normalize an inline `html` string — grid_deploy's ergonomic single-file publish
165
+ // Normalize an inline `html` string — grid_plug's ergonomic single-file publish
166
166
  // path — into ONE index.html artifact, reusing the same hardening the old drop
167
167
  // verb used (decodeIfBase64Html, the @-path/file-path rejection, the base64
168
168
  // guard, and the small-fragment wrap). Returns { path, buffer, type }. Throws on
@@ -512,7 +512,13 @@ export async function runReport(
512
512
  });
513
513
  }
514
514
 
515
- export async function runClaim(ctx, { claim_token, claim_url, entity_id }) {
515
+ // grid_pull — continue/edit an existing entity IN PLACE (POST /entities/:id/pickup).
516
+ // Like `git clone` of the SAME entity: your next grid_plug (target_entity_id)
517
+ // updates it. Needs push access — you must OWN it or be a COLLABORATOR. A
518
+ // view-only caller is told they can't edit/plug it (make a copy with grid_pickup,
519
+ // or request collaborator access with the CLI `grid collab`). A claim_token also
520
+ // claims an anonymous drop into your account (ownership transfer).
521
+ export async function runPull(ctx, { claim_token, claim_url, entity_id }) {
516
522
  const token = await ctx.getToken();
517
523
  if (!token) {
518
524
  throw new Error("You are not signed in. Run grid_login first, then claim.");
@@ -593,35 +599,67 @@ export async function runClaim(ctx, { claim_token, claim_url, entity_id }) {
593
599
  if (res.status === 409) {
594
600
  ctx.state.lastAnonClaim = null;
595
601
  return {
596
- text: "Nothing to claim — it was already claimed.",
597
- structured: { claimed: 0, urls: [] },
602
+ text: "Already yours — nothing to pull.",
603
+ structured: { owner_is_you: true, can_edit: true },
604
+ };
605
+ }
606
+ const code = data?.error?.code || null;
607
+ // No push access → not an error to throw; tell the user their options.
608
+ if (res.status === 403 || code === "NOT_ALLOWLISTED" || code === "PICKUP_DISABLED" || code === "FORBIDDEN_ROLE") {
609
+ return {
610
+ text:
611
+ `You don't have push access to this entity, so you can't edit or plug it. Two options: ` +
612
+ `make your own separate copy with grid_pickup, or ask the owner for collaborator access with the CLI ` +
613
+ `\`grid collab <entity>\` (rolling out soon) and pull again once granted.`,
614
+ structured: { can_edit: false, owner_is_you: false, access: "view_only" },
598
615
  };
599
616
  }
600
617
  const msg = data?.error?.message || data?.message || raw || `HTTP ${res.status}`;
601
- throw new Error(`Claim failed (HTTP ${res.status}): ${msg}`);
618
+ throw new Error(`Pull failed (HTTP ${res.status}): ${msg}`);
602
619
  }
603
620
 
604
621
  ctx.state.lastAnonClaim = null;
605
- // The entity is authed-owned now — the anon owner token is dead weight (a
606
- // claimed drop can no longer be edited anonymously). Future re-drops of it
607
- // must ride the authed wire.
622
+ // A claimed anon drop is authed-owned now — its anon owner token is dead weight.
608
623
  if (ctx.state.lastDrop?.entity_id === targetId) {
609
624
  ctx.state.lastDrop.owner_token = null;
610
625
  }
611
626
  const url = data?.url || data?.redirect_url || ctx.state.lastDrop?.url || null;
612
- const lines = ["Claimed 1, now yours:"];
613
- lines.push(`${url ?? ""}${data?.new_expires_at ? ` (expires ${data.new_expires_at})` : ""}`.trim());
627
+ const slug = data?.slug || targetId;
628
+ const where = data?.grid ? ` in ${data.grid}` : "";
629
+ // A claim_token transfers ownership to you; otherwise read the pickup contract.
630
+ const ownerIsYou = data?.owner?.is_you === true || Boolean(claimToken);
631
+ const canReplug = data?.capabilities?.replug === true;
632
+ let head, canEdit;
633
+ if (ownerIsYou) {
634
+ head = `${slug}${where} is yours — your next grid_plug (with target_entity_id) updates it in place.`;
635
+ canEdit = true;
636
+ } else if (canReplug) {
637
+ head = `You have collaborator push access to ${slug}${where} — your next grid_plug (target_entity_id) updates the SHARED entity; the team sees the new version and can roll it back.`;
638
+ canEdit = true;
639
+ } else {
640
+ head = `You can view ${slug}${where} but do NOT have push access — you can't edit or plug it. Make your own copy with grid_pickup, or request collaborator access with the CLI \`grid collab ${slug}\` (rolling out soon), then pull again.`;
641
+ canEdit = false;
642
+ }
643
+ const lines = [head];
644
+ if (url) lines.push(`URL: ${url}`);
645
+ if (canEdit && data?.entity_id) {
646
+ lines.push(`Re-plug handle: entity_id=${data.entity_id} — pass it as grid_plug's target_entity_id to update in place.`);
647
+ }
614
648
  return {
615
649
  text: lines.join("\n"),
616
650
  structured: {
617
- claimed: 1,
618
- urls: url ? [url] : [],
651
+ ...(data?.entity_id ? { entity_id: data.entity_id } : {}),
652
+ ...(slug ? { slug } : {}),
653
+ grid: data?.grid ?? null,
654
+ ...(url ? { url } : {}),
655
+ owner_is_you: ownerIsYou,
656
+ can_edit: canEdit,
619
657
  },
620
658
  };
621
659
  }
622
660
 
623
661
 
624
- // ── grid_deploy — the unified create/re-plug verb (spec v2 §3) ──────────────
662
+ // ── grid_plug — the unified create/re-plug verb (spec v2 §3) ──────────────
625
663
 
626
664
  // Total upload budget mirrors the server's multipart cap (100 MB).
627
665
  const PLUG_MAX_TOTAL_BYTES = 100 * 1024 * 1024;
@@ -749,7 +787,7 @@ async function expandZipToProject(zipPath, inlineHtml) {
749
787
  const hasInlineHtml = typeof inlineHtml === "string" && inlineHtml.length > 0;
750
788
  if (hasInlineHtml && hasManifest) {
751
789
  throw new Error(
752
- "The zip already contains a cloudgrid.yaml project — deploy it as-is (drop the `html` param), " +
790
+ "The zip already contains a cloudgrid.yaml project — plug it as-is (drop the `html` param), " +
753
791
  "or re-plug the entity and edit its files instead.",
754
792
  );
755
793
  }
@@ -857,7 +895,7 @@ async function plugZipProjectViaCli(ctx, { projectDir, name }, input, deps = {})
857
895
  const url = parseCliPlugUrl(stdout);
858
896
  if (!url) {
859
897
  throw new Error(
860
- `The zip project deployed via the CLI but no live URL was found in its output.\n${stdout.slice(0, 500)}`,
898
+ `The zip project was plugged via the CLI but no live URL was found in its output.\n${stdout.slice(0, 500)}`,
861
899
  );
862
900
  }
863
901
  return {
@@ -958,16 +996,16 @@ export function errorGuidance({ status, code, edition, isEdit, isAnon, signedIn
958
996
  }
959
997
  // 409 EDIT_REJECTED — an in-place re-plug the server won't take.
960
998
  if (status === 409) {
961
- return "The entity cannot be updated right now (a deploy is in progress, or it is archived/expired/claimed). An explicit re-plug never silently creates; retry later, or omit target_entity_id to create a new entity.";
999
+ return "The entity cannot be updated right now (a plug is in progress, or it is archived/expired/claimed). An explicit re-plug never silently creates; retry later, or omit target_entity_id to create a new entity.";
962
1000
  }
963
1001
  // 401 on an edit — the credential didn't authorize this entity.
964
1002
  if (status === 401) {
965
1003
  return isEdit
966
1004
  ? "That did not authorize this entity (wrong entity, expired, or already claimed). Sign in if you own it (grid_login), pass its owner_token for an anonymously-created drop, or omit target_entity_id to create a new entity."
967
- : "Not signed in. Ask the user: sign in (grid_login) to publish to their grid, OR publish anonymously now (re-call grid_deploy with anon: true) - it goes live immediately with a claim_url + owner_token to claim into their account later. Do not silently fail; offer both.";
1005
+ : "Not signed in. Ask the user: sign in (grid_login) to publish to their grid, OR publish anonymously now (re-call grid_plug with anon: true) - it goes live immediately with a claim_url + owner_token to claim into their account later. Do not silently fail; offer both.";
968
1006
  }
969
1007
  if (status === 403) {
970
- return "You lack the role to plug this target. To re-plug someone else's entity, pick it up first (grid_edit_existing_app / grid_claim_anonymous_deploy).";
1008
+ return "You lack the role to plug this target. To re-plug someone else's entity, pull it first (grid_pull), or make your own copy (grid_pickup).";
971
1009
  }
972
1010
  // ── Consent-gated report offer (Task 34) ──────────────────────────────────
973
1011
  // GENUINE bugs only: a build/deploy failure, any 5xx, INTERNAL_ERROR, or an
@@ -1114,11 +1152,11 @@ export async function runPlug(ctx, input, deps = {}) {
1114
1152
  if (target_entity_id || (slug && grid)) {
1115
1153
  throw new Error(
1116
1154
  "Re-plugging an existing entity from a zip is not supported yet — pick up the app " +
1117
- "(grid_edit_existing_app) and re-plug the folder, or deploy the zip as a new entity.",
1155
+ "(grid_edit_existing_app) and re-plug the folder, or plug the zip as a new entity.",
1118
1156
  );
1119
1157
  }
1120
1158
  if (anon) {
1121
- throw new Error("A zip deploy creates a static app and needs sign-in — it cannot be anonymous.");
1159
+ throw new Error("A zip plug creates a static app and needs sign-in — it cannot be anonymous.");
1122
1160
  }
1123
1161
  const expanded = await expandZipToProject(srcPath, hasHtml ? html : null);
1124
1162
  if (expanded.singleHtml) {
@@ -1333,9 +1371,9 @@ export async function runPlug(ctx, input, deps = {}) {
1333
1371
  } catch (err) {
1334
1372
  if (err?.name === "AbortError" || err?.name === "TimeoutError") {
1335
1373
  throw new Error(
1336
- `The deploy request timed out after ${Math.round(uploadTimeoutMs / 1000)}s. ` +
1337
- `The build may still be running on CloudGrid — check the deploy status ` +
1338
- `(poll_url / grid_status, or your grid) before deploying again, so you don't create a duplicate.`,
1374
+ `The plug request timed out after ${Math.round(uploadTimeoutMs / 1000)}s. ` +
1375
+ `The build may still be running on CloudGrid — check the build status ` +
1376
+ `(poll_url / grid_status, or your grid) before plugging again, so you don't create a duplicate.`,
1339
1377
  );
1340
1378
  }
1341
1379
  throw new Error(`Could not reach CloudGrid at ${API_BASE}: ${err.message}`);
@@ -1520,7 +1558,7 @@ export async function runPlug(ctx, input, deps = {}) {
1520
1558
  : "npx -y @cloudgrid-io/cli plug";
1521
1559
  cliSteer =
1522
1560
  "Heads-up: this went in INLINE. For a multi-file app the disk-based CLI is more reliable (lockfiles/binaries can't truncate), but it must run where the CLI is already signed in — a terminal or Claude Code that has done `grid login` once. Do NOT try `grid login` inside a chat sandbox: its login poll is long-lived and the sandbox is ephemeral, so it won't stick. " +
1523
- `If you had to leave any files out to make this inline deploy safe, tell the user — offer the complete deploy from Claude Code or a terminal (\`${replug}\`), or confirm they accept the reduced version. Do not drop files silently.`;
1561
+ `If you had to leave any files out to make this inline plug safe, tell the user — offer the complete plug from Claude Code or a terminal (\`${replug}\`), or confirm they accept the reduced version. Do not drop files silently.`;
1524
1562
  }
1525
1563
  }
1526
1564
 
@@ -1528,8 +1566,8 @@ export async function runPlug(ctx, input, deps = {}) {
1528
1566
  if (isBuilding) {
1529
1567
  lines.push(
1530
1568
  isEdit
1531
- ? `Building (async): ${url} — the update is deploying, not live yet.`
1532
- : `Building (async): ${url} — the deploy is in progress, not live yet.`,
1569
+ ? `Building (async): ${url} — the update is plugging, not live yet.`
1570
+ : `Building (async): ${url} — the plug is in progress, not live yet.`,
1533
1571
  );
1534
1572
  // Point at a tool that exists on THIS edition. grid_status is CLI-wrapping,
1535
1573
  // local-only — telling a hosted (ChatGPT/claude.ai) model to "run
@@ -1546,7 +1584,7 @@ export async function runPlug(ctx, input, deps = {}) {
1546
1584
  lines.push(`Updated in place: ${url}`);
1547
1585
  } else if (isInspirationCreate) {
1548
1586
  // Authed inspiration create — owned by the caller. Wording mirrors the drop verb.
1549
- lines.push(ctx.edition === "web" ? `Your app is live: ${url}` : `Published to your grid: ${url}`);
1587
+ lines.push(ctx.edition === "web" ? `Your app is live: ${url}` : `Plugged into your grid: ${url}`);
1550
1588
  if (ctx.edition !== "web") lines.push("Owned by you.");
1551
1589
  } else {
1552
1590
  lines.push(`Live: ${url}`);
@@ -1622,7 +1660,7 @@ async function authedApiCall(ctx, { method, pathName, body, verb }) {
1622
1660
  const msg = data?.error?.message || data?.message || raw || `HTTP ${res.status}`;
1623
1661
  const codeStr = data?.error?.code || null;
1624
1662
  const err = new Error(`${verb} failed (HTTP ${res.status}${codeStr ? ` ${codeStr}` : ""}): ${msg}`);
1625
- // Expose the structured status/code so callers can branch (e.g. runFork's
1663
+ // Expose the structured status/code so callers can branch (e.g. runPickup's
1626
1664
  // NOT_A_RUNTIME → inspiration-route fallback). Additive: existing callers
1627
1665
  // only read `.message`.
1628
1666
  err.status = res.status;
@@ -1632,63 +1670,13 @@ async function authedApiCall(ctx, { method, pathName, body, verb }) {
1632
1670
  return data;
1633
1671
  }
1634
1672
 
1635
- // Fork: start a NEW entity from an existing source, copy-on-write with lineage.
1636
- // Kind-aware (one fork verb, mirrors the CLI `fork` command): the caller may
1637
- // not know the source's kind, so try the runtime route first and, when the
1638
- // server reports the target is an inspiration (`400 NOT_A_RUNTIME`, emitted by
1639
- // requireEntityAccess before the runtime handler runs), retry the inspiration
1640
- // route. The inspiration remix lands in the caller's active grid, so no
1641
- // `into_org_slug`; `source_version_id` has no meaning for inspirations.
1642
- export async function runFork(ctx, { id, into_org_slug, name, source_version_id }) {
1643
- let data;
1644
- try {
1645
- data = await authedApiCall(ctx, {
1646
- method: "POST",
1647
- pathName: `/api/v2/runtimes/${encodeURIComponent(id)}/fork`,
1648
- body: {
1649
- ...(into_org_slug ? { into_org_slug } : {}),
1650
- ...(name ? { name } : {}),
1651
- ...(source_version_id ? { source_version_id } : {}),
1652
- },
1653
- verb: "Fork",
1654
- });
1655
- } catch (err) {
1656
- if (err && err.code === "NOT_A_RUNTIME") {
1657
- data = await authedApiCall(ctx, {
1658
- method: "POST",
1659
- pathName: `/api/v2/inspirations/${encodeURIComponent(id)}/fork`,
1660
- body: { ...(name ? { name } : {}) },
1661
- verb: "Fork",
1662
- });
1663
- } else {
1664
- throw err;
1665
- }
1666
- }
1667
- const gridSlug = data?.grid_slug ?? data?.org?.slug ?? null;
1668
- const lines = [
1669
- `Forked: ${data?.name ?? id} (entity_id=${data?.entity_id ?? "?"})${gridSlug ? ` in grid ${gridSlug}` : ""}`,
1670
- `Lineage: forked_from=${data?.forked_from ?? "?"}${data?.forked_from_version_id ? ` @ ${data.forked_from_version_id}` : ""}`,
1671
- ];
1672
- return {
1673
- text: lines.join("\n"),
1674
- structured: {
1675
- entity_id: data?.entity_id ?? null,
1676
- name: data?.name ?? null,
1677
- kind: data?.kind ?? null,
1678
- grid_slug: gridSlug,
1679
- forked_from: data?.forked_from ?? null,
1680
- forked_from_version_id: data?.forked_from_version_id ?? null,
1681
- current_version_id: data?.current_version_id ?? null,
1682
- },
1683
- };
1684
- }
1685
-
1686
- // Remix: cross-grid "make my own copy". Unlike fork (same-grid, byte-carrying),
1687
- // remix mints a NEW entity in ANY grid the caller can build in, keeps lineage
1688
- // back to the source, and strips the source's secrets/connection credentials —
1689
- // the remixer supplies their own before plugging. Hits POST
1690
- // /api/v2/runtimes/:id/remix.
1691
- export async function runRemix(ctx, { id, into_org_slug, name, source_version_id }) {
1673
+ // grid_pickup — "make your own copy" (like a git fork). Mints a NEW entity in a
1674
+ // grid you can build in, keeps lineage back to the source, and strips the
1675
+ // source's secrets/connection credentials — you set your own before plugging.
1676
+ // Plugging your copy creates/updates YOUR entity; the original is untouched.
1677
+ // Hits POST /api/v2/runtimes/:id/remix (the copy route). Mirrors the CLI's
1678
+ // `grid pickup`. To edit the ORIGINAL in place, that's grid_pull, not this.
1679
+ export async function runPickup(ctx, { id, into_org_slug, name, source_version_id }) {
1692
1680
  const data = await authedApiCall(ctx, {
1693
1681
  method: "POST",
1694
1682
  pathName: `/api/v2/runtimes/${encodeURIComponent(id)}/remix`,
@@ -1697,12 +1685,13 @@ export async function runRemix(ctx, { id, into_org_slug, name, source_version_id
1697
1685
  ...(name ? { name } : {}),
1698
1686
  ...(source_version_id ? { source_version_id } : {}),
1699
1687
  },
1700
- verb: "Remix",
1688
+ verb: "Pickup",
1701
1689
  });
1702
1690
  const gridSlug = data?.grid_slug ?? data?.org?.slug ?? null;
1703
1691
  const lines = [
1704
- `Remixed: ${data?.name ?? id} (entity_id=${data?.entity_id ?? "?"})${gridSlug ? ` in grid ${gridSlug}` : ""}`,
1705
- `Lineage: forked_from=${data?.forked_from ?? "?"}. Secrets were not copied — set your own before you plug.`,
1692
+ `Picked up a copy: ${data?.name ?? id} (entity_id=${data?.entity_id ?? "?"})${gridSlug ? ` in grid ${gridSlug}` : ""}.`,
1693
+ "This is a NEW entity — plug it (with this entity_id as target_entity_id) to create/update YOUR copy; the original is untouched.",
1694
+ `Lineage kept (forked_from=${data?.forked_from ?? "?"}). The source's secrets were NOT copied — set your own before you plug.`,
1706
1695
  ];
1707
1696
  return {
1708
1697
  text: lines.join("\n"),
@@ -1718,30 +1707,6 @@ export async function runRemix(ctx, { id, into_org_slug, name, source_version_id
1718
1707
  };
1719
1708
  }
1720
1709
 
1721
- // Download: signed, time-limited (15-minute) source-bundle URLs. No entity is
1722
- // created and no registry state changes.
1723
- export async function runDownload(ctx, { id, version }) {
1724
- const qs = version ? `?version=${encodeURIComponent(version)}` : "";
1725
- const data = await authedApiCall(ctx, {
1726
- method: "GET",
1727
- pathName: `/api/v2/runtimes/${encodeURIComponent(id)}/source${qs}`,
1728
- verb: "Download",
1729
- });
1730
- const services = data?.services && typeof data.services === "object" ? data.services : {};
1731
- const lines = [`Source bundle URLs for ${data?.name ?? id} (valid ~15 minutes):`];
1732
- for (const [svc, u] of Object.entries(services)) lines.push(` ${svc}: ${u}`);
1733
- if (Object.keys(services).length === 0) lines.push(" (no services returned)");
1734
- return {
1735
- text: lines.join("\n"),
1736
- structured: {
1737
- entity_id: data?.entity_id ?? null,
1738
- name: data?.name ?? null,
1739
- services,
1740
- domain: data?.domain ?? null,
1741
- },
1742
- };
1743
- }
1744
-
1745
1710
  // Change an entity's visibility — inspiration OR runtime app/agent. Authed,
1746
1711
  // direct API — works on the hosted edition where the CLI-wrapping share tool is
1747
1712
  // unavailable. Kind-aware routing (see below). Defaults to the drop made in this
@@ -1805,7 +1770,7 @@ export async function runVisibility(ctx, { target, visibility, kind, org }) {
1805
1770
  // Route order. `space` is inspiration-only; a known runtime kind goes straight
1806
1771
  // to the entities route; a known inspiration goes straight to the inspiration
1807
1772
  // route; an unknown kind tries the runtime route first and falls back to the
1808
- // inspiration route on a not-found (mirrors runFork).
1773
+ // inspiration route on a not-found (mirrors runPickup).
1809
1774
  let order;
1810
1775
  if (vis === "space") order = ["inspiration"];
1811
1776
  else if (isRuntimeKind) order = ["runtime"];
@@ -1975,7 +1940,7 @@ export async function runCheckDeploy(ctx, { poll_url, grid } = {}) {
1975
1940
  });
1976
1941
  if (!target) {
1977
1942
  throw new Error(
1978
- "No build to check. Pass poll_url from a grid_deploy result, or deploy something first in this session. (Instant inspiration deploys are live on return and have no build to poll.)",
1943
+ "No build to check. Pass poll_url from a grid_plug result, or plug something first in this session. (Instant inspiration plugs are live on return and have no build to poll.)",
1979
1944
  );
1980
1945
  }
1981
1946
  const verdict = await fetchDeployTrace(ctx, { pollUrl: target, grid: gridSlug });
@@ -1988,7 +1953,7 @@ export async function runCheckDeploy(ctx, { poll_url, grid } = {}) {
1988
1953
  }
1989
1954
  if (verdict.status === "failed") {
1990
1955
  return {
1991
- text: `The build FAILED: ${verdict.error || "no reason reported"}. The URL is not live — do not give it to the user as working. Fix the app (or re-deploy) and try again.` +
1956
+ text: `The build FAILED: ${verdict.error || "no reason reported"}. The URL is not live — do not give it to the user as working. Fix the app (or re-plug) and try again.` +
1992
1957
  formatFailureDetail(verdict),
1993
1958
  structured: {
1994
1959
  status: "failed",
@@ -2087,7 +2052,7 @@ async function readInspirationSourceViaApi(ctx, { entityId, slug, grid }) {
2087
2052
 
2088
2053
  // Resolve a public URL / grid+slug to a REAL entity_id (+ edition metadata) via
2089
2054
  // the deployed pickup contract (POST /api/v2/entities/:target/pickup — the same
2090
- // endpoint runClaim uses). Used by runSource when a bare URL arrives with no
2055
+ // endpoint runPull uses). Used by runSource when a bare URL arrives with no
2091
2056
  // session entity_id (a fresh chat): the contract returns
2092
2057
  // { entity_id, slug, grid, kind, single_html, capabilities, replug_handle,
2093
2058
  // source_download_url, ... }
@@ -20,11 +20,9 @@ import {
20
20
  resolveGridOrAsk,
21
21
  detectSourceManifest,
22
22
  runReport,
23
- runClaim,
23
+ runPull,
24
24
  runPlug,
25
- runFork,
26
- runRemix,
27
- runDownload,
25
+ runPickup,
28
26
  runVisibility,
29
27
  runSource,
30
28
  runCheckDeploy,
@@ -106,7 +104,7 @@ export function registerTools(server, ctx) {
106
104
  }));
107
105
 
108
106
  server.registerResource("cloudgrid-org-picker", GRID_PICKER_URI, {
109
- description: "Grid picker card — lets the user choose which grid to publish into.",
107
+ description: "Grid picker card — lets the user choose which grid to plug into.",
110
108
  mimeType: "text/html;profile=mcp-app",
111
109
  }, async () => ({
112
110
  contents: [{
@@ -120,28 +118,68 @@ export function registerTools(server, ctx) {
120
118
 
121
119
  // ── Direct-API tools (both editions) ──────────────────────────────────────
122
120
 
123
- // Pickup — both editions. Adopts an entity into the signed-in account's active
124
- // grid (ownership transfer via POST /api/v2/entities/:id/pickup). Pass an
125
- // anonymous drop's owner token as `claim_token` to take over something you
126
- // dropped anonymously — that path replaces the old standalone "claim" verb.
121
+ // ── Copy / adopt verbs (both editions) — mirror the CLI: ──────────────────
122
+ // grid_pickup = make YOUR OWN COPY (git-fork style) → POST /runtimes/:id/remix
123
+ // grid_pull = continue/edit the SAME entity in place (push access needed)
124
+ // → POST /entities/:id/pickup
125
+ // (The CLI verbs `download`, `fork`, `remix`, `link` were retired; no MCP tool
126
+ // targets them.)
127
+
128
+ // grid_pickup — make your own copy of any app you can see.
127
129
  reg(
128
130
  "grid_pickup",
129
131
  {
130
- description: "Pick up (adopt) an entity into the signed-in account's active grid — an ownership transfer, keeping the same public URL. Use it to take over an entity you have access to, or to keep an anonymous drop you created: pass the drop's `claim_token` and it becomes owned and stops expiring on the anonymous schedule. Requires sign-in (grid_login). Calls the API directly, so it works on every edition. (This is the direct-API adopt; on the local edition grid_edit_existing_app additionally downloads the source and links the folder for editing.)",
132
+ description: "Pick up an app: make your OWN COPY of any app you can see (like a git fork) into a grid you can build in. It mints a NEW entity with lineage back to the source and WITHOUT the source's secrets (set your own before you plug). Plugging your copy creates/updates YOUR entity — the original is never touched. Requires sign-in. To edit the ORIGINAL entity in place (as its owner or a collaborator), use grid_pull instead.",
133
+ inputSchema: {
134
+ id: z.string().describe("The source app to copy: a canonical UUID or <grid-slug>/<entity-slug>."),
135
+ into_org_slug: z.string().optional().describe("Grid to create your copy in. Required only when you belong to more than one grid."),
136
+ name: z.string().optional().describe("Slug for your copy. Omit to derive one from the source."),
137
+ source_version_id: z.string().optional().describe("Copy an older version instead of HEAD, e.g. v_a1b2c3d."),
138
+ },
139
+ outputSchema: {
140
+ entity_id: z.string().nullable().describe("Your copy's entity id — pass as grid_plug's target_entity_id to update it."),
141
+ name: z.string().nullable().describe("Your copy's slug."),
142
+ kind: z.string().nullable().describe("app | agent."),
143
+ grid_slug: z.string().nullable().describe("The grid your copy landed in."),
144
+ forked_from: z.string().nullable().describe("Source entity_id (lineage)."),
145
+ forked_from_version_id: z.string().nullable().describe("Source version, when a specific one was copied."),
146
+ current_version_id: z.string().nullable().describe("Your copy's current version id."),
147
+ },
148
+ annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
149
+ },
150
+ async (input) => {
151
+ try {
152
+ if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
153
+ return okResult(await runPickup(ctx, input));
154
+ } catch (err) {
155
+ return fail(err.message);
156
+ }
157
+ },
158
+ );
159
+
160
+ // grid_pull — continue/edit the SAME entity in place (push access required).
161
+ reg(
162
+ "grid_pull",
163
+ {
164
+ description: "Pull an app to continue/edit it IN PLACE — like `git clone` of the SAME entity: your next grid_plug (with its target_entity_id) updates that entity, and the team sees the new version. Requires PUSH ACCESS: you must own it or be a collaborator. If you can only view it, you CANNOT edit or plug it — say so and tell the user they can make their own copy with grid_pickup, or request collaborator access with the CLI `grid collab <entity>` (rolling out soon). Passing an anonymous drop's `claim_token` claims it into your account. Requires sign-in. Calls the API directly (both editions).",
131
165
  inputSchema: {
132
- entity_id: z.string().optional().describe("The entity id to pick up. Defaults to this session's last anonymous drop."),
133
- claim_token: z.string().optional().describe("For an anonymous drop: its owner token (the owner_token in the drop result, also embedded in claim_url). Passing it takes over that drop."),
134
- claim_url: z.string().optional().describe("Alternative to claim_token for an anonymous drop — the claim_url from the drop result; the token is read from it."),
166
+ entity_id: z.string().optional().describe("The entity id to pull. Defaults to this session's last anonymous drop."),
167
+ claim_token: z.string().optional().describe("Anonymous drop only: its owner token — claims that drop into your account (ownership transfer)."),
168
+ claim_url: z.string().optional().describe("Alternative to claim_token for an anonymous drop — the claim_url; the token is read from it."),
135
169
  },
136
170
  outputSchema: {
137
- claimed: z.number().describe("Number of entities picked up."),
138
- urls: z.array(z.string()).describe("Public URLs of the picked-up entities."),
171
+ entity_id: z.string().optional().describe("The entity you pulled."),
172
+ slug: z.string().optional().describe("Its slug."),
173
+ grid: z.string().nullable().optional().describe("Its grid."),
174
+ url: z.string().optional().describe("Its public URL."),
175
+ owner_is_you: z.boolean().optional().describe("True if you own it."),
176
+ can_edit: z.boolean().optional().describe("True if you can plug/update it (owner or collaborator). False = view-only."),
139
177
  },
140
178
  annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
141
179
  },
142
180
  async (input) => {
143
181
  try {
144
- return okResult(await runClaim(ctx, input || {}));
182
+ return okResult(await runPull(ctx, input || {}));
145
183
  } catch (err) {
146
184
  return fail(err.message);
147
185
  }
@@ -156,7 +194,7 @@ export function registerTools(server, ctx) {
156
194
  // acknowledgement.
157
195
  regTool(
158
196
  "grid_note",
159
- "Optionally leave a one-paragraph summary of what you built this session and why. Call it BEFORE a deploy, or in a session that ends without one — a successful deploy has already posted the QA log, so pass grid_plug's session_note instead. Recorded for CloudGrid QA. No side effects.",
197
+ "Optionally leave a one-paragraph summary of what you built this session and why. Call it BEFORE a plug, or in a session that ends without one — a successful plug has already posted the QA log, so pass grid_plug's session_note instead. Recorded for CloudGrid QA. No side effects.",
160
198
  { summary: z.string().describe("A short plain-language summary of what was built and why.") },
161
199
  { readOnlyHint: true, destructiveHint: false, openWorldHint: false },
162
200
  async (input) => {
@@ -191,7 +229,7 @@ export function registerTools(server, ctx) {
191
229
  path: z.string().optional().describe(
192
230
  "Local edition: path to the entity folder, a single file, or a .zip ARCHIVE to upload. " +
193
231
  "A folder is read recursively, honoring .gitignore/.cloudgridignore (plus .git/node_modules " +
194
- "always skipped). A .zip is extracted and deployed: with its own cloudgrid.yaml it deploys " +
232
+ "always skipped). A .zip is extracted and plugged: with its own cloudgrid.yaml it plugs " +
195
233
  "as that project; otherwise it becomes a static app (all files served). Use this for " +
196
234
  "\"deploy this zip\" / \"gallery from these zipped images\" requests. Mutually exclusive with " +
197
235
  "`html` and `artifact_files` — EXCEPT `html` + a .zip of assets: the html becomes index.html " +
@@ -213,7 +251,7 @@ export function registerTools(server, ctx) {
213
251
  "On re-plug, a name: change is a warning only; it never renames the entity or moves the URL.",
214
252
  ),
215
253
  target_entity_id: z.string().optional().describe(
216
- "Present → RE-PLUG: update this exact entity in place (same entity_id, slug, URL, deploy history). " +
254
+ "Present → RE-PLUG: update this exact entity in place (same entity_id, slug, URL, plug history). " +
217
255
  "Absent → CREATE a new entity. This is the durable handle a previous plug returned — persist it. " +
218
256
  "Re-plugging an anonymously-created drop needs its owner_token instead of sign-in.",
219
257
  ),
@@ -241,7 +279,7 @@ export function registerTools(server, ctx) {
241
279
  "target_entity_id). Re-minted on every anonymous edit; always persist the newest one from the result.",
242
280
  ),
243
281
  confirm_new_app: z.boolean().optional().describe(
244
- "Set true to confirm deploying a source that already contains a cloudgrid.yaml as a NEW runtime app. " +
282
+ "Set true to confirm plugging a source that already contains a cloudgrid.yaml as a NEW runtime app. " +
245
283
  "On a create, if the source has a cloudgrid.yaml and this is not set, grid_plug returns needs_confirmation " +
246
284
  "so you can ask the user first (or use target_entity_id to re-plug an existing entity).",
247
285
  ),
@@ -250,7 +288,7 @@ export function registerTools(server, ctx) {
250
288
  "include it by default; omit only if the user asked not to share it.",
251
289
  ),
252
290
  session_note: z.string().optional().describe(
253
- "One short paragraph on what you built and why. Recorded for CloudGrid QA alongside the deploy.",
291
+ "One short paragraph on what you built and why. Recorded for CloudGrid QA alongside the plug.",
254
292
  ),
255
293
  };
256
294
 
@@ -432,111 +470,6 @@ export function registerTools(server, ctx) {
432
470
  // "publish"/"ship"/"make live" live in grid_plug's description, so
433
471
  // deploy-intent routing lands there without a second listed name.
434
472
 
435
- // ── grid_fork / grid_remix / grid_download — direct-API copy/source verbs ──────
436
- // fork = same-grid byte-carrying copy; remix = cross-grid copy with secrets
437
- // stripped; download = signed source-bundle URLs. All direct-API (both editions).
438
- reg(
439
- "grid_fork",
440
- {
441
- description:
442
- "Fork an entity: start a NEW entity from an existing one (copy-on-write, lineage recorded), carrying " +
443
- "the source bytes. Lands in the source's home grid by default; cross-grid only for system templates " +
444
- "or forkable:'public' sources — for a general cross-grid copy use grid_remix instead. Requires sign-in. " +
445
- "Kind-aware: forks a runtime (app/agent) OR an inspiration — tries POST /api/v2/runtimes/:id/fork and " +
446
- "falls back to /api/v2/inspirations/:id/fork.",
447
- inputSchema: {
448
- id: z.string().describe("The source entity: a canonical UUID or <grid-slug>/<entity-slug>."),
449
- into_org_slug: z.string().optional().describe("Destination grid slug. Required only when you belong to more than one grid."),
450
- name: z.string().optional().describe("Slug for the new entity. Omit to derive one from the source."),
451
- source_version_id: z.string().optional().describe("Fork an older version instead of HEAD, e.g. v_a1b2c3d."),
452
- },
453
- outputSchema: {
454
- entity_id: z.string().nullable().describe("The new entity's id."),
455
- name: z.string().nullable().describe("The new entity's slug."),
456
- kind: z.string().nullable().describe("app | agent | inspiration."),
457
- grid_slug: z.string().nullable().describe("The grid the fork landed in."),
458
- forked_from: z.string().nullable().describe("Source entity_id."),
459
- forked_from_version_id: z.string().nullable().describe("Source version, when a specific one was forked."),
460
- current_version_id: z.string().nullable().describe("The fork's current version id."),
461
- },
462
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
463
- },
464
- async (input) => {
465
- try {
466
- if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
467
- return okResult(await runFork(ctx, input));
468
- } catch (err) {
469
- return fail(err.message);
470
- }
471
- },
472
- );
473
-
474
- reg(
475
- "grid_remix",
476
- {
477
- description:
478
- "Remix an app: make your own copy of any app you can SEE (link/public/authenticated, or a grid you " +
479
- "belong to) into ANY grid you can build in — cross-grid is allowed. Mints a NEW entity with lineage " +
480
- "back to the source, WITHOUT the source's secrets or connection credentials (set your own before you " +
481
- "plug). Distinct from grid_fork, which is same-grid and byte-carrying. Requires sign-in. Calls POST " +
482
- "/api/v2/runtimes/:id/remix.",
483
- inputSchema: {
484
- id: z.string().describe("The source app: a canonical UUID or <grid-slug>/<entity-slug>."),
485
- into_org_slug: z.string().optional().describe("Grid to create your copy in. Required only when you belong to more than one grid."),
486
- name: z.string().optional().describe("Slug for your copy. Omit to derive one from the source."),
487
- source_version_id: z.string().optional().describe("Remix an older version instead of HEAD, e.g. v_a1b2c3d."),
488
- },
489
- outputSchema: {
490
- entity_id: z.string().nullable().describe("Your copy's entity id."),
491
- name: z.string().nullable().describe("Your copy's slug."),
492
- kind: z.string().nullable().describe("app | agent."),
493
- grid_slug: z.string().nullable().describe("The grid your copy landed in."),
494
- forked_from: z.string().nullable().describe("Source entity_id."),
495
- forked_from_version_id: z.string().nullable().describe("Source version, when a specific one was remixed."),
496
- current_version_id: z.string().nullable().describe("Your copy's current version id."),
497
- },
498
- annotations: { readOnlyHint: false, destructiveHint: false, openWorldHint: true },
499
- },
500
- async (input) => {
501
- try {
502
- if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
503
- return okResult(await runRemix(ctx, input));
504
- } catch (err) {
505
- return fail(err.message);
506
- }
507
- },
508
- );
509
-
510
- reg(
511
- "grid_download",
512
- {
513
- description:
514
- "Download an entity's source: the bundle last deployed for a runtime, as one signed, time-limited " +
515
- "(15-minute) read URL per service tarball. No entity is created and no registry state changes (unlike " +
516
- "grid_pickup/grid_fork/grid_remix, which adopt or copy). Requires sign-in. Calls GET " +
517
- "/api/v2/runtimes/:id/source directly.",
518
- inputSchema: {
519
- id: z.string().describe("The runtime to download: a canonical UUID or <grid-slug>/<entity-slug>."),
520
- version: z.string().optional().describe("Download an older version's bundle instead of HEAD, e.g. v_a1b2c3d."),
521
- },
522
- outputSchema: {
523
- entity_id: z.string().nullable().describe("The runtime's entity id."),
524
- name: z.string().nullable().describe("The runtime's slug."),
525
- services: z.record(z.string()).describe("Service name → signed read URL (valid ~15 minutes)."),
526
- domain: z.string().nullable().optional().describe("The runtime's domain, if any."),
527
- },
528
- annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
529
- },
530
- async (input) => {
531
- try {
532
- if (!input?.id) return fail("`id` is required (a canonical UUID or <grid-slug>/<entity-slug>).");
533
- return okResult(await runDownload(ctx, input));
534
- } catch (err) {
535
- return fail(err.message);
536
- }
537
- },
538
- );
539
-
540
473
  // Source — both editions. Fetches a drop's current deployed HTML inline so an
541
474
  // agent that lost the content can edit it and re-plug in place.
542
475
  reg(
@@ -550,9 +483,9 @@ export function registerTools(server, ctx) {
550
483
  "entity_id via the pickup contract and also returns the entity's kind, single_html, capabilities " +
551
484
  "(replug/fork), and replug_handle — read those to decide whether to edit in place (single-HTML + " +
552
485
  "capabilities.replug), fall back for a multi-file app/agent (use source_download_url + the local " +
553
- "edition/CLI), or offer a fork when it isn't yours. For multi-file or runtime (app/agent) source " +
554
- "bundles, use grid_download (signed tarball URLs). Reads the HTML from the API server-side; " +
555
- "read-only, creates nothing.",
486
+ "edition/CLI), or offer a copy (grid_pickup) when it isn't yours. For a multi-file or runtime " +
487
+ "(app/agent) source, use source_download_url, or grid_pull to get the files and link the folder. " +
488
+ "Reads the HTML from the API server-side; read-only, creates nothing.",
556
489
  inputSchema: {
557
490
  entity_id: z.string().optional().describe("The drop's durable id. Defaults to this session's last drop."),
558
491
  url: z.string().optional().describe("The public URL of the drop (e.g. https://<grid>.cloudgrid.io/<slug>). Defaults to this session's last drop URL."),
@@ -1130,23 +1063,27 @@ export function registerTools(server, ctx) {
1130
1063
  cliTool(({ grid }) => ["describe", "grid", grid, "--json"]),
1131
1064
  );
1132
1065
 
1066
+ // Local (CLI-wrapping) counterpart of the direct-API grid_pull: downloads the
1067
+ // source + cloudgrid.yaml and links the folder, so the next `grid plug` updates
1068
+ // the SAME entity in place. Wraps `grid pull` (NOT `grid pickup` — pickup now
1069
+ // makes a separate copy). Push access required; a view-only entity can't be
1070
+ // pulled — use grid_pickup to copy it, or `grid collab` to request access.
1133
1071
  regTool(
1134
1072
  "grid_edit_existing_app",
1135
- "Download an entity's source + cloudgrid.yaml and link the folder to it. Overwrites with --force. Wraps `grid pickup`.",
1073
+ "Continue/edit an EXISTING entity locally: download its source + cloudgrid.yaml and link the folder so your next `grid plug` updates it IN PLACE. Requires push access (owner or collaborator). Wraps `grid pull`. To make your own separate copy instead, use grid_pickup.",
1136
1074
  {
1137
- name: z.string().describe("Entity slug or id to pick up."),
1138
- target_dir: z.string().optional().describe("Directory to pick up into (relative to cwd). Defaults to the entity name."),
1075
+ name: z.string().describe("Entity slug or id to pull."),
1076
+ target_dir: z.string().optional().describe("Directory to pull into (relative to cwd). Defaults to the entity name."),
1139
1077
  grid: z.string().optional().describe("Grid to resolve the entity in. Defaults to the active grid."),
1140
- version: z.string().optional().describe("Pick up an older version's source instead of HEAD."),
1141
- force: z.boolean().optional().describe("Pick up into a non-empty directory."),
1078
+ version: z.string().optional().describe("Pull an older version's source instead of HEAD."),
1079
+ force: z.boolean().optional().describe("Pull into a non-empty directory."),
1142
1080
  no_bind: z.boolean().optional().describe("Download source only — skip cloudgrid.yaml and the link."),
1143
1081
  cwd: z.string().optional().describe("Working directory the CLI runs in. The download lands here; pass an explicit, writable directory."),
1144
1082
  },
1145
1083
  { readOnlyHint: false, destructiveHint: true, openWorldHint: true },
1146
1084
  cliTool(({ name, target_dir, grid, version, force, no_bind }) => {
1147
- const args = ["pickup", name];
1085
+ const args = ["pull", name];
1148
1086
  if (target_dir) args.push(target_dir);
1149
- // CLI 0.12's pickup exposes `--grid <slug>` (was legacy `--org`).
1150
1087
  if (grid) args.push("--grid", grid);
1151
1088
  if (version) args.push("--version", version);
1152
1089
  if (force) args.push("--force");
package/src/tools.js CHANGED
@@ -33,7 +33,8 @@ export {
33
33
  parseCliPlugUrl,
34
34
  plugViaCliFallback,
35
35
  runPlug,
36
- runFork,
36
+ runPickup,
37
+ runPull,
37
38
  runSource,
38
39
  runVisibility,
39
40
  runCheckDeploy,
@@ -44,7 +44,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
44
44
  var html =
45
45
  '<div class="card">' +
46
46
  '<div class="title">Choose a grid</div>' +
47
- '<div class="subtitle">Which grid should this be published to?</div>' +
47
+ '<div class="subtitle">Which grid should this be plugged into?</div>' +
48
48
  '<div class="orgs">';
49
49
  for (var i = 0; i < items.length; i++) {
50
50
  var org = items[i];
@@ -66,7 +66,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
66
66
  var all = document.querySelectorAll(".org-btn");
67
67
  for (var k = 0; k < all.length; k++) all[k].classList.add("loading");
68
68
  var statusEl = document.getElementById("status");
69
- if (statusEl) statusEl.textContent = "Publishing\u2026";
69
+ if (statusEl) statusEl.textContent = "Plugging\u2026";
70
70
  if (!window.openai || !window.openai.callTool) return;
71
71
  var input = (window.openai && window.openai.toolInput) || {};
72
72
  var args = {};
@@ -79,7 +79,7 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
79
79
  // The host will render the new result with the live-result card.
80
80
  }).catch(function () {
81
81
  for (var m = 0; m < all.length; m++) all[m].classList.remove("loading");
82
- if (statusEl) statusEl.textContent = "Failed to publish";
82
+ if (statusEl) statusEl.textContent = "Failed to plug";
83
83
  });
84
84
  });
85
85
  })(buttons[j]);