@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 +1 -2
- package/src/corpus/playbook.md +1 -1
- package/src/tools/deploy.js +84 -119
- package/src/tools/register.js +73 -136
- package/src/tools.js +2 -1
- package/src/widgets/org-picker.html +3 -3
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.20.
|
|
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",
|
package/src/corpus/playbook.md
CHANGED
|
@@ -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
|
|
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.
|
package/src/tools/deploy.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
// Direct-API tool internals:
|
|
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 (
|
|
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
|
|
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 —
|
|
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
|
-
|
|
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: "
|
|
597
|
-
structured: {
|
|
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(`
|
|
618
|
+
throw new Error(`Pull failed (HTTP ${res.status}): ${msg}`);
|
|
602
619
|
}
|
|
603
620
|
|
|
604
621
|
ctx.state.lastAnonClaim = null;
|
|
605
|
-
//
|
|
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
|
|
613
|
-
|
|
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
|
-
|
|
618
|
-
|
|
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
|
-
// ──
|
|
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 —
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
1337
|
-
`The build may still be running on CloudGrid — check the
|
|
1338
|
-
`(poll_url / grid_status, or your grid) before
|
|
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
|
|
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
|
|
1532
|
-
: `Building (async): ${url} — the
|
|
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}` : `
|
|
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.
|
|
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
|
-
//
|
|
1636
|
-
//
|
|
1637
|
-
//
|
|
1638
|
-
//
|
|
1639
|
-
//
|
|
1640
|
-
//
|
|
1641
|
-
|
|
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: "
|
|
1688
|
+
verb: "Pickup",
|
|
1701
1689
|
});
|
|
1702
1690
|
const gridSlug = data?.grid_slug ?? data?.org?.slug ?? null;
|
|
1703
1691
|
const lines = [
|
|
1704
|
-
`
|
|
1705
|
-
|
|
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
|
|
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
|
|
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-
|
|
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
|
|
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, ... }
|
package/src/tools/register.js
CHANGED
|
@@ -20,11 +20,9 @@ import {
|
|
|
20
20
|
resolveGridOrAsk,
|
|
21
21
|
detectSourceManifest,
|
|
22
22
|
runReport,
|
|
23
|
-
|
|
23
|
+
runPull,
|
|
24
24
|
runPlug,
|
|
25
|
-
|
|
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
|
|
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
|
-
//
|
|
124
|
-
//
|
|
125
|
-
//
|
|
126
|
-
//
|
|
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
|
|
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
|
|
133
|
-
claim_token: z.string().optional().describe("
|
|
134
|
-
claim_url: z.string().optional().describe("Alternative to claim_token for an anonymous drop — the claim_url
|
|
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
|
-
|
|
138
|
-
|
|
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
|
|
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
|
|
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
|
|
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,
|
|
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
|
|
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
|
|
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
|
|
554
|
-
"
|
|
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
|
-
"
|
|
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
|
|
1138
|
-
target_dir: z.string().optional().describe("Directory to
|
|
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("
|
|
1141
|
-
force: z.boolean().optional().describe("
|
|
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 = ["
|
|
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
|
@@ -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
|
|
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 = "
|
|
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
|
|
82
|
+
if (statusEl) statusEl.textContent = "Failed to plug";
|
|
83
83
|
});
|
|
84
84
|
});
|
|
85
85
|
})(buttons[j]);
|