@cloudgrid-io/mcp 0.7.2 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +9 -2
- package/package.json +2 -1
- package/src/auth.js +8 -4
- package/src/index.js +2 -2
- package/src/tools.js +216 -94
- package/src/web.js +3 -3
- package/src/widgets/org-picker.html +10 -7
package/README.md
CHANGED
|
@@ -15,11 +15,18 @@ drop, claim, and login tools.
|
|
|
15
15
|
|
|
16
16
|
## CLI compatibility
|
|
17
17
|
|
|
18
|
-
MCP 0.
|
|
19
|
-
`@cloudgrid-io/cli@~0.
|
|
18
|
+
MCP 0.8.0 is tested against CLI 0.12. The lazy-npx fallback pins
|
|
19
|
+
`@cloudgrid-io/cli@~0.12` so a future CLI major with renamed verbs cannot
|
|
20
20
|
silently break a released MCP. A CI drift guard (`npm run test:drift-guard`)
|
|
21
21
|
asserts every wrapped verb exists in the CLI help.
|
|
22
22
|
|
|
23
|
+
### `GRID_AUTH_STALE` is out of scope
|
|
24
|
+
|
|
25
|
+
Strict-OIDC (org-owned) grids can return `GRID_AUTH_STALE` when the caller's
|
|
26
|
+
session predates an SSO policy change. The MCP does not target such grids and
|
|
27
|
+
does not handle this code: if a `GRID_AUTH_STALE` response is ever seen, point
|
|
28
|
+
the user at Console SSO to re-authenticate — the MCP will not silently retry.
|
|
29
|
+
|
|
23
30
|
## Prerequisite
|
|
24
31
|
|
|
25
32
|
Install and log in to the CLI:
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@cloudgrid-io/mcp",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0",
|
|
4
4
|
"description": "MCP server for CloudGrid. Three editions: a local stdio server (full toolset), a hosted web server (light, CLI-free toolset), and a public docs server (search_cloudgrid_documentation) over MCP Streamable HTTP.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -18,6 +18,7 @@
|
|
|
18
18
|
"smoke": "node test/smoke.mjs",
|
|
19
19
|
"test:plug-wire": "node test/plug-wire.test.mjs",
|
|
20
20
|
"test:self-healing": "node test/self-healing.test.mjs",
|
|
21
|
+
"test:grid-picker": "node test/grid-picker.test.mjs",
|
|
21
22
|
"test:drift-guard": "node test/drift-guard.mjs",
|
|
22
23
|
"smoke:drop": "node test/smoke-drop.mjs",
|
|
23
24
|
"smoke:login": "node test/smoke-login.mjs",
|
package/src/auth.js
CHANGED
|
@@ -66,12 +66,16 @@ export async function readCredentials() {
|
|
|
66
66
|
}
|
|
67
67
|
}
|
|
68
68
|
|
|
69
|
-
// The CLI's active
|
|
70
|
-
// line parse — no YAML dependency for one field.
|
|
71
|
-
|
|
69
|
+
// The CLI's active grid slug from ~/.cloudgrid/config.yaml, or null. A minimal
|
|
70
|
+
// line parse — no YAML dependency for one field. Accepts the grid-native
|
|
71
|
+
// `active_grid_slug` key and falls back to the legacy `active_org_slug` (CLI 0.12
|
|
72
|
+
// still writes the org key; this is forward-compatible with a future rename).
|
|
73
|
+
export async function readActiveGridSlug() {
|
|
72
74
|
try {
|
|
73
75
|
const raw = await readFile(join(cloudgridHome(), "config.yaml"), "utf8");
|
|
74
|
-
const m =
|
|
76
|
+
const m =
|
|
77
|
+
raw.match(/^\s*active_grid_slug:\s*(\S+)\s*$/m) ||
|
|
78
|
+
raw.match(/^\s*active_org_slug:\s*(\S+)\s*$/m);
|
|
75
79
|
return m ? m[1] : null;
|
|
76
80
|
} catch {
|
|
77
81
|
return null;
|
package/src/index.js
CHANGED
|
@@ -11,7 +11,7 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
|
|
|
11
11
|
import { registerTools } from "./tools.js";
|
|
12
12
|
import {
|
|
13
13
|
readCredentials,
|
|
14
|
-
|
|
14
|
+
readActiveGridSlug,
|
|
15
15
|
writeCredentials,
|
|
16
16
|
credentialsPath,
|
|
17
17
|
} from "./auth.js";
|
|
@@ -23,7 +23,7 @@ const ctx = {
|
|
|
23
23
|
state: { pendingLoginCode: null, lastAnonClaim: null, lastDrop: null, anonCookie: null },
|
|
24
24
|
canOpenBrowser: true,
|
|
25
25
|
getToken: async () => (await readCredentials())?.jwt ?? null,
|
|
26
|
-
|
|
26
|
+
getActiveGrid: async () => await readActiveGridSlug(),
|
|
27
27
|
saveToken: async (jwt) => await writeCredentials(jwt),
|
|
28
28
|
savedLocationNote: () => `Credentials saved to ${credentialsPath()}.`,
|
|
29
29
|
};
|
package/src/tools.js
CHANGED
|
@@ -30,9 +30,11 @@ const CONSOLE_URL = "https://console.cloudgrid.io/";
|
|
|
30
30
|
|
|
31
31
|
// ── Widget resources (ChatGPT Apps SDK, web edition only) ────────────────────
|
|
32
32
|
const LIVE_RESULT_URI = "ui://cloudgrid/live-result.html";
|
|
33
|
-
|
|
33
|
+
// URI/resource-name/filename stay `org-picker` — that's the stable contract the
|
|
34
|
+
// web card is registered under; only the JS identifier moves toward grid.
|
|
35
|
+
const GRID_PICKER_URI = "ui://cloudgrid/org-picker.html";
|
|
34
36
|
const LIVE_RESULT_HTML = readFileSync(new URL("./widgets/live-result.html", import.meta.url), "utf-8");
|
|
35
|
-
const
|
|
37
|
+
const GRID_PICKER_HTML = readFileSync(new URL("./widgets/org-picker.html", import.meta.url), "utf-8");
|
|
36
38
|
const WIDGET_CSP = {
|
|
37
39
|
connectDomains: ["https://*.cloudgrid.io"],
|
|
38
40
|
resourceDomains: ["https://*.cloudgrid.io"],
|
|
@@ -66,6 +68,7 @@ Operating rules:
|
|
|
66
68
|
4. Always return the live share URL at the end — that is the deliverable.
|
|
67
69
|
5. Minimize questions. Use sensible defaults and build; don't front-load setup questions.
|
|
68
70
|
6. If a signed-in publish fails with a server error, do not fall back to anonymous publishing (it burns the anonymous quota and downgrades ownership); surface the error, use the CLI fallback if offered, or ask the user.
|
|
71
|
+
7. When signed in and the user has more than one grid, do not assume a target — the publish tools will ask; relay the choice to the user and pass the chosen grid.
|
|
69
72
|
|
|
70
73
|
Deploy is edition-dependent: on the hosted MCP call the drop tool with the HTML; on local MCP / CLI write the file and run the plug tool. An HTML page deploys synchronously, so you get a URL right away.`;
|
|
71
74
|
|
|
@@ -193,11 +196,11 @@ function resolveCwd(cwd) {
|
|
|
193
196
|
}
|
|
194
197
|
|
|
195
198
|
// Pin the CLI version for the lazy npx fallback so behaviour is reproducible.
|
|
196
|
-
// MCP 0.
|
|
197
|
-
const CLI_NPX_PKG = "@cloudgrid-io/cli@~0.
|
|
199
|
+
// MCP 0.8.0 is tested against CLI 0.12.
|
|
200
|
+
const CLI_NPX_PKG = "@cloudgrid-io/cli@~0.12";
|
|
198
201
|
|
|
199
202
|
// Minimum CLI version that supports the verbs and flags the MCP passes.
|
|
200
|
-
const MIN_CLI_VERSION = "0.
|
|
203
|
+
const MIN_CLI_VERSION = "0.12.0";
|
|
201
204
|
|
|
202
205
|
// Verb map for the drift guard: each CLI-wrapping tool's top-level verb(s).
|
|
203
206
|
// The drift-guard test imports this and asserts every verb exists in `cloudgrid --help`.
|
|
@@ -655,8 +658,16 @@ async function fetchUserOrgs(token) {
|
|
|
655
658
|
});
|
|
656
659
|
if (!res.ok) return [];
|
|
657
660
|
const data = await res.json();
|
|
658
|
-
|
|
659
|
-
|
|
661
|
+
// 0.8.0: read the grid-native `data.grids` (dual-emitted alongside the legacy
|
|
662
|
+
// `data.orgs`, same array/order). Fall back to `data.orgs`/bare-array during soak.
|
|
663
|
+
const grids = Array.isArray(data?.grids)
|
|
664
|
+
? data.grids
|
|
665
|
+
: Array.isArray(data?.orgs)
|
|
666
|
+
? data.orgs
|
|
667
|
+
: Array.isArray(data)
|
|
668
|
+
? data
|
|
669
|
+
: [];
|
|
670
|
+
return grids.map((o) => ({
|
|
660
671
|
slug: o.slug ?? "",
|
|
661
672
|
name: o.name ?? o.slug ?? "",
|
|
662
673
|
role: o.role ?? "member",
|
|
@@ -667,6 +678,67 @@ async function fetchUserOrgs(token) {
|
|
|
667
678
|
}
|
|
668
679
|
}
|
|
669
680
|
|
|
681
|
+
// ── Shared grid disambiguation (gridctl_drop + gridctl_plug) ──────────────────
|
|
682
|
+
// The stateless "which grid?" ask, used identically by both publish verbs so
|
|
683
|
+
// they never drift. Given the caller's token and a supplied grid, it decides:
|
|
684
|
+
// - supplied grid matches a membership → { proceed: true, grid }
|
|
685
|
+
// - >1 grid and none supplied → { picker } (a ready-to-return result)
|
|
686
|
+
// - exactly one grid → { single: annotatedOrg } — the caller
|
|
687
|
+
// decides how to treat a not-ready single grid (drop blocks; plug warns)
|
|
688
|
+
// - no orgs / listing failed → { proceed: true } (fall through)
|
|
689
|
+
// User-facing text says "grid" (Gilad's org→grid rename); the structured payload
|
|
690
|
+
// carries `needs_grid` AND the `needs_org`/`orgs`/`org`-slug fields the existing
|
|
691
|
+
// org-picker web widget reads, so the web card keeps working. Stateless — no
|
|
692
|
+
// dependence on prior-call state (ChatGPT Apps SDK reconnects every call).
|
|
693
|
+
export async function resolveGridOrAsk(ctx, { token, suppliedGrid, edition }, deps = {}) {
|
|
694
|
+
const listGrids = deps.fetchUserOrgs || fetchUserOrgs;
|
|
695
|
+
const grids = await listGrids(token);
|
|
696
|
+
const activeGrid = await ctx.getActiveGrid();
|
|
697
|
+
const matched = suppliedGrid && grids.find((o) => o.slug === suppliedGrid);
|
|
698
|
+
if (matched) {
|
|
699
|
+
// Supplied grid matches — proceed. The agent should already have checked
|
|
700
|
+
// render_ready and warned the user; we don't block here.
|
|
701
|
+
return { proceed: true, grid: suppliedGrid };
|
|
702
|
+
}
|
|
703
|
+
if (grids.length > 1) {
|
|
704
|
+
// No valid grid supplied and multiple grids — ask once. Mark the active
|
|
705
|
+
// grid so the agent can offer it as the default.
|
|
706
|
+
const annotated = grids.map((o) => ({ ...o, is_active: o.slug === activeGrid }));
|
|
707
|
+
// Sort: active grid first, then ready grids, then not-ready grids.
|
|
708
|
+
annotated.sort((a, b) => {
|
|
709
|
+
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
710
|
+
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
711
|
+
return 0;
|
|
712
|
+
});
|
|
713
|
+
const lines = ["Which grid should this be published to?"];
|
|
714
|
+
for (const o of annotated) {
|
|
715
|
+
const tags = [];
|
|
716
|
+
if (o.is_active) tags.push("your active grid");
|
|
717
|
+
if (!o.render_ready) tags.push("not set up yet");
|
|
718
|
+
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
719
|
+
lines.push(` ${o.slug} — ${o.name} (${o.role})${suffix}`);
|
|
720
|
+
}
|
|
721
|
+
lines.push("Pass the grid slug in the `grid` parameter.");
|
|
722
|
+
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
723
|
+
if (readyCount === 0) {
|
|
724
|
+
lines.push("Note: none of your grids are fully set up yet. You can use anonymous: true as a fallback.");
|
|
725
|
+
}
|
|
726
|
+
return {
|
|
727
|
+
picker: {
|
|
728
|
+
text: lines.join("\n"),
|
|
729
|
+
// `needs_grid` is the new field; `needs_org`/`orgs` are kept as aliases
|
|
730
|
+
// so the existing org-picker.html web widget (reads data.orgs) still works.
|
|
731
|
+
structured: { needs_grid: true, needs_org: true, grids: annotated, orgs: annotated },
|
|
732
|
+
...(edition === "web" ? { meta: { "openai/outputTemplate": GRID_PICKER_URI } } : {}),
|
|
733
|
+
},
|
|
734
|
+
};
|
|
735
|
+
}
|
|
736
|
+
if (grids.length === 1) {
|
|
737
|
+
return { single: { ...grids[0], is_active: grids[0].slug === activeGrid } };
|
|
738
|
+
}
|
|
739
|
+
return { proceed: true };
|
|
740
|
+
}
|
|
741
|
+
|
|
670
742
|
// After an authenticated web drop, upgrade visibility to "link" so the artifact
|
|
671
743
|
// is shareable and its preview renders without a sign-in wall. Best-effort — a
|
|
672
744
|
// failure here does not fail the drop; the user can always call gridctl_visibility.
|
|
@@ -675,7 +747,12 @@ async function upgradeVisibilityToLink(ctx, entityId, orgSlug) {
|
|
|
675
747
|
if (!token || !entityId) return false;
|
|
676
748
|
try {
|
|
677
749
|
const hdrs = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
678
|
-
|
|
750
|
+
// Send the grid-native header; keep X-CloudGrid-Org in parallel (same value)
|
|
751
|
+
// during the org→grid soak. Never send conflicting values → 400 GRID_HEADER_CONFLICT.
|
|
752
|
+
if (orgSlug) {
|
|
753
|
+
hdrs["X-CloudGrid-Grid"] = orgSlug;
|
|
754
|
+
hdrs["X-CloudGrid-Org"] = orgSlug;
|
|
755
|
+
}
|
|
679
756
|
const res = await fetch(`${API_BASE}/api/v2/inspirations/${encodeURIComponent(entityId)}`, {
|
|
680
757
|
method: "PATCH",
|
|
681
758
|
headers: hdrs,
|
|
@@ -733,9 +810,11 @@ export function resolvePlugUrl(data) {
|
|
|
733
810
|
|
|
734
811
|
export async function runDrop(
|
|
735
812
|
ctx,
|
|
736
|
-
{ html, path: filePath, filename, anonymous, org, fresh, entity_id, owner_token },
|
|
813
|
+
{ html, path: filePath, filename, anonymous, org, grid, fresh, entity_id, owner_token },
|
|
737
814
|
deps = {},
|
|
738
815
|
) {
|
|
816
|
+
// `grid` is an accepted alias for `org` (Gilad's org→grid rename); `org` still works.
|
|
817
|
+
const targetGrid = org ?? grid;
|
|
739
818
|
// Defensive: the web edition schema excludes `path`, but if a model still
|
|
740
819
|
// passes one (e.g. from a cached tool description), reject early with a
|
|
741
820
|
// clear explanation.
|
|
@@ -779,7 +858,7 @@ export async function runDrop(
|
|
|
779
858
|
if (anonymous !== true) {
|
|
780
859
|
authToken = await ctx.getToken();
|
|
781
860
|
if (authToken) {
|
|
782
|
-
orgSlug =
|
|
861
|
+
orgSlug = targetGrid || (await ctx.getActiveGrid());
|
|
783
862
|
}
|
|
784
863
|
}
|
|
785
864
|
|
|
@@ -833,7 +912,11 @@ export async function runDrop(
|
|
|
833
912
|
const headers = {};
|
|
834
913
|
if (!isAnonymousWire) {
|
|
835
914
|
headers["Authorization"] = `Bearer ${authToken}`;
|
|
836
|
-
|
|
915
|
+
// Grid-native header + X-CloudGrid-Org alias (same slug) during the soak.
|
|
916
|
+
if (orgSlug) {
|
|
917
|
+
headers["X-CloudGrid-Grid"] = orgSlug;
|
|
918
|
+
headers["X-CloudGrid-Org"] = orgSlug;
|
|
919
|
+
}
|
|
837
920
|
}
|
|
838
921
|
|
|
839
922
|
// Hosted server: attach the trusted-server credential on EVERY anonymous call
|
|
@@ -1097,8 +1180,12 @@ async function runClaim(ctx, { claim_token, claim_url, entity_id }) {
|
|
|
1097
1180
|
// The claim re-homes the inspiration into the caller's grid; the platform
|
|
1098
1181
|
// resolves the destination from the active-org context, so send the org
|
|
1099
1182
|
// header (the same header every other authed write uses).
|
|
1100
|
-
const orgSlug = await ctx.
|
|
1101
|
-
|
|
1183
|
+
const orgSlug = await ctx.getActiveGrid();
|
|
1184
|
+
// Grid-native header + X-CloudGrid-Org alias (same slug) during the soak.
|
|
1185
|
+
if (orgSlug) {
|
|
1186
|
+
headers["X-CloudGrid-Grid"] = orgSlug;
|
|
1187
|
+
headers["X-CloudGrid-Org"] = orgSlug;
|
|
1188
|
+
}
|
|
1102
1189
|
// Replay the anon-session cookie so a cookie-class caller can claim what it
|
|
1103
1190
|
// dropped, even without a claim token.
|
|
1104
1191
|
if (ctx.state.anonCookie) headers["Cookie"] = ctx.state.anonCookie;
|
|
@@ -1478,8 +1565,12 @@ export async function runPlug(ctx, input, deps = {}) {
|
|
|
1478
1565
|
// the caller's membership from this header and requires it to MATCH the
|
|
1479
1566
|
// entity's grid, so pass `grid` here too when the target lives outside the
|
|
1480
1567
|
// active grid.
|
|
1481
|
-
const orgSlug = grid || (await ctx.
|
|
1482
|
-
|
|
1568
|
+
const orgSlug = grid || (await ctx.getActiveGrid());
|
|
1569
|
+
// Grid-native header + X-CloudGrid-Org alias (same slug) during the soak.
|
|
1570
|
+
if (orgSlug) {
|
|
1571
|
+
headers["X-CloudGrid-Grid"] = orgSlug;
|
|
1572
|
+
headers["X-CloudGrid-Org"] = orgSlug;
|
|
1573
|
+
}
|
|
1483
1574
|
}
|
|
1484
1575
|
if (useAnonWire && ctx.trustedServer?.secret && ctx.trustedServer?.endUserId) {
|
|
1485
1576
|
headers["X-CloudGrid-Trusted-Server-Auth"] = ctx.trustedServer.secret;
|
|
@@ -1640,8 +1731,12 @@ async function authedApiCall(ctx, { method, pathName, body, verb }) {
|
|
|
1640
1731
|
throw new Error(`${verb} requires sign-in. Run gridctl_login first.`);
|
|
1641
1732
|
}
|
|
1642
1733
|
const headers = { Authorization: `Bearer ${token}` };
|
|
1643
|
-
const orgSlug = await ctx.
|
|
1644
|
-
|
|
1734
|
+
const orgSlug = await ctx.getActiveGrid();
|
|
1735
|
+
// Grid-native header + X-CloudGrid-Org alias (same slug) during the soak.
|
|
1736
|
+
if (orgSlug) {
|
|
1737
|
+
headers["X-CloudGrid-Grid"] = orgSlug;
|
|
1738
|
+
headers["X-CloudGrid-Org"] = orgSlug;
|
|
1739
|
+
}
|
|
1645
1740
|
if (body !== undefined) headers["Content-Type"] = "application/json";
|
|
1646
1741
|
let res;
|
|
1647
1742
|
try {
|
|
@@ -1736,8 +1831,12 @@ async function runVisibility(ctx, { target, visibility, org }) {
|
|
|
1736
1831
|
throw new Error("No target. Pass the entity id, or drop something first in this session.");
|
|
1737
1832
|
}
|
|
1738
1833
|
const headers = { Authorization: `Bearer ${token}`, "Content-Type": "application/json" };
|
|
1739
|
-
const orgSlug = org || (await ctx.
|
|
1740
|
-
|
|
1834
|
+
const orgSlug = org || (await ctx.getActiveGrid());
|
|
1835
|
+
// Grid-native header + X-CloudGrid-Org alias (same slug) during the soak.
|
|
1836
|
+
if (orgSlug) {
|
|
1837
|
+
headers["X-CloudGrid-Grid"] = orgSlug;
|
|
1838
|
+
headers["X-CloudGrid-Org"] = orgSlug;
|
|
1839
|
+
}
|
|
1741
1840
|
let res;
|
|
1742
1841
|
try {
|
|
1743
1842
|
res = await fetch(`${API_BASE}/api/v2/inspirations/${encodeURIComponent(id)}`, {
|
|
@@ -1815,14 +1914,14 @@ export function registerTools(server, ctx) {
|
|
|
1815
1914
|
}],
|
|
1816
1915
|
}));
|
|
1817
1916
|
|
|
1818
|
-
server.registerResource("cloudgrid-org-picker",
|
|
1917
|
+
server.registerResource("cloudgrid-org-picker", GRID_PICKER_URI, {
|
|
1819
1918
|
description: "Org picker card — lets the user choose which organization to publish into.",
|
|
1820
1919
|
mimeType: "text/html;profile=mcp-app",
|
|
1821
1920
|
}, async () => ({
|
|
1822
1921
|
contents: [{
|
|
1823
|
-
uri:
|
|
1922
|
+
uri: GRID_PICKER_URI,
|
|
1824
1923
|
mimeType: "text/html;profile=mcp-app",
|
|
1825
|
-
text:
|
|
1924
|
+
text: GRID_PICKER_HTML,
|
|
1826
1925
|
_meta: { ui: { csp: WIDGET_CSP } },
|
|
1827
1926
|
}],
|
|
1828
1927
|
}));
|
|
@@ -1847,7 +1946,8 @@ export function registerTools(server, ctx) {
|
|
|
1847
1946
|
),
|
|
1848
1947
|
filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
|
|
1849
1948
|
anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
|
|
1850
|
-
|
|
1949
|
+
grid: z.string().optional().describe("Leave unset; the tool will ask the user which grid to publish into. Only set this after the user picks from the list the tool returns."),
|
|
1950
|
+
org: z.string().optional().describe("Deprecated alias for `grid` (kept for compatibility). Prefer `grid`."),
|
|
1851
1951
|
...dropReplugParams,
|
|
1852
1952
|
}
|
|
1853
1953
|
: {
|
|
@@ -1855,7 +1955,8 @@ export function registerTools(server, ctx) {
|
|
|
1855
1955
|
path: z.string().optional().describe("Path to a local file to upload instead of inline HTML."),
|
|
1856
1956
|
filename: z.string().optional().describe("Filename to present. Defaults to index.html for inline HTML."),
|
|
1857
1957
|
anonymous: z.boolean().optional().describe("Force an anonymous drop even if the user is signed in."),
|
|
1858
|
-
|
|
1958
|
+
grid: z.string().optional().describe("Leave unset; the tool will ask the user which grid to publish into. Only set this after the user picks from the list the tool returns."),
|
|
1959
|
+
org: z.string().optional().describe("Deprecated alias for `grid` (kept for compatibility). Prefer `grid`."),
|
|
1859
1960
|
...dropReplugParams,
|
|
1860
1961
|
};
|
|
1861
1962
|
|
|
@@ -1877,13 +1978,20 @@ export function registerTools(server, ctx) {
|
|
|
1877
1978
|
value: z.string().describe("Visibility value to pass to gridctl_visibility."),
|
|
1878
1979
|
label: z.string().describe("Human-readable label."),
|
|
1879
1980
|
})).optional().describe("Available visibility levels."),
|
|
1880
|
-
|
|
1981
|
+
needs_grid: z.boolean().optional().describe("True when the user must choose a grid before dropping."),
|
|
1982
|
+
needs_org: z.boolean().optional().describe("Deprecated alias of needs_grid (kept so the org-picker widget keeps working)."),
|
|
1983
|
+
grids: z.array(z.object({
|
|
1984
|
+
slug: z.string().describe("Grid slug to pass as the grid parameter."),
|
|
1985
|
+
name: z.string().describe("Human-readable grid name."),
|
|
1986
|
+
role: z.string().describe("User's role in the grid."),
|
|
1987
|
+
is_active: z.boolean().optional().describe("True if this is the user's currently active grid."),
|
|
1988
|
+
})).optional().describe("The user's grids, when a grid choice is needed."),
|
|
1881
1989
|
orgs: z.array(z.object({
|
|
1882
|
-
slug: z.string().describe("
|
|
1883
|
-
name: z.string().describe("Human-readable
|
|
1884
|
-
role: z.string().describe("User's role in the
|
|
1885
|
-
is_active: z.boolean().optional().describe("True if this is the user's currently active
|
|
1886
|
-
})).optional().describe("
|
|
1990
|
+
slug: z.string().describe("Grid slug to pass as the grid parameter."),
|
|
1991
|
+
name: z.string().describe("Human-readable grid name."),
|
|
1992
|
+
role: z.string().describe("User's role in the grid."),
|
|
1993
|
+
is_active: z.boolean().optional().describe("True if this is the user's currently active grid."),
|
|
1994
|
+
})).optional().describe("Deprecated alias of grids (kept so the org-picker widget keeps working)."),
|
|
1887
1995
|
needs_sign_in: z.boolean().optional().describe("True when sign-in is needed before dropping."),
|
|
1888
1996
|
login_url: z.string().optional().describe("Sign-in URL when authentication is needed."),
|
|
1889
1997
|
},
|
|
@@ -1918,63 +2026,37 @@ export function registerTools(server, ctx) {
|
|
|
1918
2026
|
}
|
|
1919
2027
|
}
|
|
1920
2028
|
|
|
1921
|
-
// Stateless
|
|
2029
|
+
// Stateless grid disambiguation — both editions, when authenticated.
|
|
1922
2030
|
// No dependency on prior-call state so it works even when the client
|
|
1923
|
-
// reconnects on every tool call (ChatGPT Apps SDK behaviour).
|
|
2031
|
+
// reconnects on every tool call (ChatGPT Apps SDK behaviour). `grid` is
|
|
2032
|
+
// accepted as an alias for `org` (Gilad's rename); `org` still works.
|
|
1924
2033
|
if (input?.anonymous !== true) {
|
|
1925
2034
|
const token = await ctx.getToken();
|
|
1926
2035
|
if (token) {
|
|
1927
|
-
const
|
|
1928
|
-
const
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
}));
|
|
1943
|
-
// Sort: active org first, then ready orgs, then not-ready orgs.
|
|
1944
|
-
annotated.sort((a, b) => {
|
|
1945
|
-
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
1946
|
-
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
1947
|
-
return 0;
|
|
1948
|
-
});
|
|
1949
|
-
const lines = ["Which org should this be published to?"];
|
|
1950
|
-
for (const o of annotated) {
|
|
1951
|
-
const tags = [];
|
|
1952
|
-
if (o.is_active) tags.push("your active org");
|
|
1953
|
-
if (!o.render_ready) tags.push("not set up yet");
|
|
1954
|
-
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
1955
|
-
lines.push(` ${o.slug} — ${o.name} (${o.role})${suffix}`);
|
|
1956
|
-
}
|
|
1957
|
-
lines.push("Pass the org slug in the org parameter to publish.");
|
|
1958
|
-
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
1959
|
-
if (readyCount === 0) {
|
|
1960
|
-
lines.push("Note: none of your orgs are fully set up yet. You can use anonymous: true as a fallback.");
|
|
1961
|
-
}
|
|
1962
|
-
return okResult({
|
|
1963
|
-
text: lines.join("\n"),
|
|
1964
|
-
structured: { needs_org: true, orgs: annotated },
|
|
1965
|
-
...(ctx.edition === "web" ? { meta: { "openai/outputTemplate": ORG_PICKER_URI } } : {}),
|
|
1966
|
-
});
|
|
1967
|
-
} else if (orgs.length === 1) {
|
|
1968
|
-
if (!orgs[0].render_ready) {
|
|
1969
|
-
// Single org but not set up — tell the agent so it can warn
|
|
1970
|
-
// the user and offer anonymous drop as fallback.
|
|
1971
|
-
const annotated = [{ ...orgs[0], is_active: orgs[0].slug === activeOrg, render_ready: false }];
|
|
2036
|
+
const suppliedGrid = input?.grid || input?.org;
|
|
2037
|
+
const decision = await resolveGridOrAsk(ctx, {
|
|
2038
|
+
token,
|
|
2039
|
+
suppliedGrid,
|
|
2040
|
+
edition: ctx.edition,
|
|
2041
|
+
});
|
|
2042
|
+
if (decision.picker) {
|
|
2043
|
+
return okResult(decision.picker);
|
|
2044
|
+
}
|
|
2045
|
+
if (decision.single) {
|
|
2046
|
+
if (!decision.single.render_ready) {
|
|
2047
|
+
// Single grid but not set up — tell the agent so it can warn
|
|
2048
|
+
// the user and offer anonymous drop as fallback. (drop-specific:
|
|
2049
|
+
// drop blocks here; plug proceeds with a warning.)
|
|
2050
|
+
const annotated = [{ ...decision.single, render_ready: false }];
|
|
1972
2051
|
return okResult({
|
|
1973
|
-
text: `Your only
|
|
1974
|
-
structured: { needs_org: true, org_not_ready: true, orgs: annotated },
|
|
2052
|
+
text: `Your only grid "${decision.single.slug}" isn't fully set up yet — pages published there may not load. You can drop anonymously (set anonymous: true) or wait until provisioning completes, then re-run.`,
|
|
2053
|
+
structured: { needs_grid: true, needs_org: true, grid_not_ready: true, org_not_ready: true, grids: annotated, orgs: annotated },
|
|
1975
2054
|
});
|
|
1976
2055
|
}
|
|
1977
|
-
input = { ...(input || {}), org:
|
|
2056
|
+
input = { ...(input || {}), org: decision.single.slug };
|
|
2057
|
+
} else if (decision.grid) {
|
|
2058
|
+
// Supplied grid matched — normalize onto `org` for runDrop.
|
|
2059
|
+
input = { ...(input || {}), org: decision.grid };
|
|
1978
2060
|
}
|
|
1979
2061
|
}
|
|
1980
2062
|
}
|
|
@@ -2090,6 +2172,42 @@ export function registerTools(server, ctx) {
|
|
|
2090
2172
|
},
|
|
2091
2173
|
async (input) => {
|
|
2092
2174
|
try {
|
|
2175
|
+
// Grid-picker parity with gridctl_drop: a signed-in user with >1 grid is
|
|
2176
|
+
// ASKED which grid to publish to on every CREATE. Only for authed creates
|
|
2177
|
+
// (no target_entity_id, not anon). Edits NEVER ask — the grid is fixed by
|
|
2178
|
+
// the entity. Anon proceeds as a Guest-Grid drop. Explicit valid grid
|
|
2179
|
+
// proceeds. A single grid proceeds (with a warning if it isn't set up yet).
|
|
2180
|
+
const isEdit =
|
|
2181
|
+
typeof input?.target_entity_id === "string" && input.target_entity_id.length > 0;
|
|
2182
|
+
if (input?.anon !== true && !isEdit) {
|
|
2183
|
+
const token = await ctx.getToken();
|
|
2184
|
+
if (token) {
|
|
2185
|
+
const decision = await resolveGridOrAsk(ctx, {
|
|
2186
|
+
token,
|
|
2187
|
+
suppliedGrid: input?.grid,
|
|
2188
|
+
edition: ctx.edition,
|
|
2189
|
+
});
|
|
2190
|
+
if (decision.picker) {
|
|
2191
|
+
// Do NOT silently default to the active grid — surface the ask.
|
|
2192
|
+
return okResult(decision.picker);
|
|
2193
|
+
}
|
|
2194
|
+
if (decision.single) {
|
|
2195
|
+
// Proceed into the single grid; warn (don't block) if not set up yet.
|
|
2196
|
+
input = { ...(input || {}), grid: decision.single.slug };
|
|
2197
|
+
if (decision.single.render_ready === false) {
|
|
2198
|
+
const res = await runPlug(ctx, input || {});
|
|
2199
|
+
return okResult({
|
|
2200
|
+
...res,
|
|
2201
|
+
text:
|
|
2202
|
+
`Warning: your only grid "${decision.single.slug}" isn't fully set up yet — the page may not load until provisioning completes.\n` +
|
|
2203
|
+
res.text,
|
|
2204
|
+
});
|
|
2205
|
+
}
|
|
2206
|
+
} else if (decision.grid) {
|
|
2207
|
+
input = { ...(input || {}), grid: decision.grid };
|
|
2208
|
+
}
|
|
2209
|
+
}
|
|
2210
|
+
}
|
|
2093
2211
|
return okResult(await runPlug(ctx, input || {}));
|
|
2094
2212
|
} catch (err) {
|
|
2095
2213
|
return fail(err.message);
|
|
@@ -2283,16 +2401,17 @@ export function registerTools(server, ctx) {
|
|
|
2283
2401
|
if (!token) {
|
|
2284
2402
|
return fail("You are not signed in. Run gridctl_login first.");
|
|
2285
2403
|
}
|
|
2286
|
-
const
|
|
2287
|
-
if (
|
|
2288
|
-
|
|
2404
|
+
const grids = await fetchUserOrgs(token);
|
|
2405
|
+
if (grids.length === 0) {
|
|
2406
|
+
// Structured output stays `orgs` (its declared schema); user text says grid.
|
|
2407
|
+
return okResult({ text: "No grids found.", structured: { orgs: [] } });
|
|
2289
2408
|
}
|
|
2290
|
-
const
|
|
2291
|
-
const annotated =
|
|
2409
|
+
const activeGrid = await ctx.getActiveGrid();
|
|
2410
|
+
const annotated = grids.map((o) => ({
|
|
2292
2411
|
...o,
|
|
2293
|
-
is_active: o.slug ===
|
|
2412
|
+
is_active: o.slug === activeGrid,
|
|
2294
2413
|
}));
|
|
2295
|
-
// Sort: active
|
|
2414
|
+
// Sort: active grid first, then ready grids, then not-ready grids.
|
|
2296
2415
|
annotated.sort((a, b) => {
|
|
2297
2416
|
if (a.is_active !== b.is_active) return b.is_active ? 1 : -1;
|
|
2298
2417
|
if (a.render_ready !== b.render_ready) return b.render_ready ? 1 : -1;
|
|
@@ -2300,15 +2419,16 @@ export function registerTools(server, ctx) {
|
|
|
2300
2419
|
});
|
|
2301
2420
|
const lines = annotated.map((o) => {
|
|
2302
2421
|
const tags = [];
|
|
2303
|
-
if (o.is_active) tags.push("your active
|
|
2422
|
+
if (o.is_active) tags.push("your active grid");
|
|
2304
2423
|
if (!o.render_ready) tags.push("not set up yet");
|
|
2305
2424
|
const suffix = tags.length ? ` (${tags.join(", ")})` : "";
|
|
2306
2425
|
return `${o.slug} — ${o.name} (${o.role})${suffix}`;
|
|
2307
2426
|
});
|
|
2308
2427
|
const readyCount = annotated.filter((o) => o.render_ready).length;
|
|
2309
2428
|
if (readyCount === 0 && annotated.length > 0) {
|
|
2310
|
-
lines.push("\nNone of your
|
|
2429
|
+
lines.push("\nNone of your grids are fully set up yet. You can use an anonymous drop as a fallback, or wait until provisioning completes.");
|
|
2311
2430
|
}
|
|
2431
|
+
// Structured output stays `orgs` (its declared schema); user text says grid.
|
|
2312
2432
|
return okResult({ text: lines.join("\n"), structured: { orgs: annotated } });
|
|
2313
2433
|
},
|
|
2314
2434
|
);
|
|
@@ -2356,7 +2476,7 @@ export function registerTools(server, ctx) {
|
|
|
2356
2476
|
signedIn = false;
|
|
2357
2477
|
}
|
|
2358
2478
|
try {
|
|
2359
|
-
activeGrid = (await ctx.
|
|
2479
|
+
activeGrid = (await ctx.getActiveGrid()) ?? null;
|
|
2360
2480
|
} catch {
|
|
2361
2481
|
activeGrid = null;
|
|
2362
2482
|
}
|
|
@@ -2429,7 +2549,8 @@ export function registerTools(server, ctx) {
|
|
|
2429
2549
|
if (type) args.push("--type", type);
|
|
2430
2550
|
if (description) args.push("--description", description);
|
|
2431
2551
|
if (dir) args.push("--dir", dir);
|
|
2432
|
-
|
|
2552
|
+
// CLI 0.12 dropped `--org` in favour of `--grid` (same slug).
|
|
2553
|
+
if (org) args.push("--grid", org);
|
|
2433
2554
|
return args;
|
|
2434
2555
|
}, { cwdParam: true }),
|
|
2435
2556
|
);
|
|
@@ -2480,7 +2601,8 @@ export function registerTools(server, ctx) {
|
|
|
2480
2601
|
const args = ["feedback", "list"];
|
|
2481
2602
|
if (since) args.push("--since", since);
|
|
2482
2603
|
if (limit) args.push("--limit", String(limit));
|
|
2483
|
-
|
|
2604
|
+
// CLI 0.12 dropped `--org` in favour of `--grid` (same slug).
|
|
2605
|
+
if (org) args.push("--grid", org);
|
|
2484
2606
|
return args;
|
|
2485
2607
|
}),
|
|
2486
2608
|
);
|
|
@@ -2587,8 +2709,8 @@ export function registerTools(server, ctx) {
|
|
|
2587
2709
|
cliTool(({ name, target_dir, grid, version, force, no_bind }) => {
|
|
2588
2710
|
const args = ["pickup", name];
|
|
2589
2711
|
if (target_dir) args.push(target_dir);
|
|
2590
|
-
//
|
|
2591
|
-
if (grid) args.push("--
|
|
2712
|
+
// CLI 0.12's pickup exposes `--grid <slug>` (was legacy `--org`).
|
|
2713
|
+
if (grid) args.push("--grid", grid);
|
|
2592
2714
|
if (version) args.push("--version", version);
|
|
2593
2715
|
if (force) args.push("--force");
|
|
2594
2716
|
if (no_bind) args.push("--no-bind");
|
package/src/web.js
CHANGED
|
@@ -59,9 +59,9 @@ function makeWebContext(sessionId) {
|
|
|
59
59
|
canOpenBrowser: false,
|
|
60
60
|
// Transport OAuth wins; the in-tool login flow is the fallback.
|
|
61
61
|
getToken: async () => sessionAuth[sessionId] ?? sessionToken,
|
|
62
|
-
// No local config on a shared host. The user passes `
|
|
63
|
-
// the list of
|
|
64
|
-
|
|
62
|
+
// No local config on a shared host. The user passes `grid`, or the API returns
|
|
63
|
+
// the list of grids to choose from.
|
|
64
|
+
getActiveGrid: async () => null,
|
|
65
65
|
saveToken: async (jwt) => {
|
|
66
66
|
sessionToken = jwt;
|
|
67
67
|
return decodeJwt(jwt);
|
|
@@ -34,18 +34,20 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
|
|
|
34
34
|
}
|
|
35
35
|
|
|
36
36
|
function render(data) {
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
// `grids` is the current field; fall back to `orgs` for older payloads.
|
|
38
|
+
var items = (data && (data.grids || data.orgs)) || null;
|
|
39
|
+
if (!items || items.length === 0) {
|
|
40
|
+
root.innerHTML = '<div class="card"><div class="title">No grids found</div></div>';
|
|
39
41
|
return;
|
|
40
42
|
}
|
|
41
43
|
|
|
42
44
|
var html =
|
|
43
45
|
'<div class="card">' +
|
|
44
|
-
'<div class="title">Choose
|
|
45
|
-
'<div class="subtitle">Which
|
|
46
|
+
'<div class="title">Choose a grid</div>' +
|
|
47
|
+
'<div class="subtitle">Which grid should this be published to?</div>' +
|
|
46
48
|
'<div class="orgs">';
|
|
47
|
-
for (var i = 0; i <
|
|
48
|
-
var org =
|
|
49
|
+
for (var i = 0; i < items.length; i++) {
|
|
50
|
+
var org = items[i];
|
|
49
51
|
var notReady = org.render_ready === false;
|
|
50
52
|
html +=
|
|
51
53
|
'<button class="org-btn' + (notReady ? ' not-ready' : '') + '" data-slug="' + esc(org.slug) + '">' +
|
|
@@ -71,7 +73,8 @@ body{font-family:-apple-system,BlinkMacSystemFont,"Segoe UI",Roboto,sans-serif;b
|
|
|
71
73
|
if (input.html) args.html = input.html;
|
|
72
74
|
if (input.filename) args.filename = input.filename;
|
|
73
75
|
if (input.fresh !== undefined) args.fresh = input.fresh;
|
|
74
|
-
args.
|
|
76
|
+
args.grid = slug;
|
|
77
|
+
args.org = slug; // alias kept for compatibility
|
|
75
78
|
window.openai.callTool("cloudgrid_drop", args).then(function () {
|
|
76
79
|
// The host will render the new result with the live-result card.
|
|
77
80
|
}).catch(function () {
|