@cloudgrid-io/mcp 0.20.27 → 0.20.29

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.27",
3
+ "version": "0.20.29",
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": {
@@ -51,12 +51,16 @@
51
51
  "build:mcpb": "node scripts/build-mcpb.mjs",
52
52
  "test:widgets": "node test/apps-widgets.test.mjs",
53
53
  "test:fork": "node test/fork.test.mjs",
54
- "test:set-sharing": "node test/set-sharing.test.mjs"
54
+ "test:set-sharing": "node test/set-sharing.test.mjs",
55
+ "test:check-deploy": "node test/check-deploy.test.mjs",
56
+ "test:staleness": "node test/staleness.test.mjs",
57
+ "test:proxy": "node test/proxy.test.mjs"
55
58
  },
56
59
  "dependencies": {
57
60
  "@modelcontextprotocol/sdk": "^1.0.0",
58
61
  "express": "^4.22.2",
59
62
  "fflate": "^0.8.3",
63
+ "undici": "^8.7.0",
60
64
  "zod": "^3.23.0"
61
65
  },
62
66
  "license": "Apache-2.0",
package/src/auth.js CHANGED
@@ -45,7 +45,17 @@ export function buildLoginUrl(code) {
45
45
  // A 404 means the session for this code does not exist yet — the user has not
46
46
  // opened the sign-in URL — so it is reported as 'not_started', not an error.
47
47
  export async function pollStatusOnce(code) {
48
- const res = await fetch(`${API_BASE}/auth/status?code=${encodeURIComponent(code)}`);
48
+ let res;
49
+ try {
50
+ res = await fetch(`${API_BASE}/auth/status?code=${encodeURIComponent(code)}`);
51
+ } catch (err) {
52
+ const cause = err?.cause?.code || err?.cause?.message || err?.cause || err.message;
53
+ throw new Error(
54
+ `Can't reach api.cloudgrid.io (${cause}). ` +
55
+ `If you are behind a proxy, set HTTPS_PROXY; or use the hosted connector ` +
56
+ `https://mcp-connected.cloudgrid.io/mcp.`,
57
+ );
58
+ }
49
59
  if (res.status === 404) {
50
60
  return { status: "not_started" };
51
61
  }
package/src/index.js CHANGED
@@ -5,6 +5,9 @@
5
5
  // Desktop). Full toolset, including the CLI-wrapping tools. Identity comes from
6
6
  // the shared ~/.cloudgrid/credentials file, so it interoperates with the CLI.
7
7
 
8
+ import { installProxy } from "./proxy.js";
9
+ installProxy();
10
+
8
11
  import { readFileSync } from "node:fs";
9
12
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
10
13
  import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
@@ -19,6 +22,7 @@ import { randomUUID } from "node:crypto";
19
22
  import { createSessionLogger } from "./session-logger.js";
20
23
  import { createSink } from "./log-sink.js";
21
24
  import { INSTRUCTIONS_LOCAL } from "./playbook.js";
25
+ import { checkForNewerVersion } from "./staleness.js";
22
26
 
23
27
  const { version } = JSON.parse(readFileSync(new URL("../package.json", import.meta.url)));
24
28
 
@@ -32,6 +36,24 @@ const ctx = {
32
36
  savedLocationNote: () => `Credentials saved to ${credentialsPath()}.`,
33
37
  };
34
38
 
39
+ // Boot-time staleness self-check (local edition only — hosted auto-updates on
40
+ // cutover). Fire-and-forget with a short timeout: offline or slow → stays null.
41
+ // Surfaced in grid_start's live context so the model relays the reinstall note
42
+ // in-session; also logged to stderr for the Desktop MCP log.
43
+ ctx.staleness = null;
44
+ checkForNewerVersion(version)
45
+ .then((info) => {
46
+ if (info?.behind) {
47
+ ctx.staleness = info;
48
+ console.error(
49
+ `cloudgrid-mcp: this MCP is v${info.current}; latest is v${info.latest}. ` +
50
+ `The .mcpb Desktop extension never auto-updates — reinstall the latest from ` +
51
+ `https://github.com/cloudgrid-io/mcp/releases/latest`,
52
+ );
53
+ }
54
+ })
55
+ .catch(() => {});
56
+
35
57
  // QA session log (dark by default: no CLOUDGRID_QA_SLACK_WEBHOOK → null → no-op).
36
58
  // The stdio process IS the session. user_request is forwarded by Claude Code via
37
59
  // CLOUDGRID_USER_REQUEST when present.
package/src/proxy.js ADDED
@@ -0,0 +1,63 @@
1
+ // Corporate proxy support for Node's global fetch (undici).
2
+ //
3
+ // Node's built-in fetch ignores HTTPS_PROXY / HTTP_PROXY. This module reads
4
+ // the standard proxy env vars and, when set, installs an undici ProxyAgent as
5
+ // the global dispatcher so every subsequent fetch() honors the proxy.
6
+ //
7
+ // Fully guarded: if undici is unavailable or the env is unset, this is a
8
+ // silent no-op. Never throws at startup.
9
+
10
+ import { createRequire } from "node:module";
11
+
12
+ export function resolveProxy() {
13
+ const proxyUrl =
14
+ process.env.HTTPS_PROXY ||
15
+ process.env.https_proxy ||
16
+ process.env.HTTP_PROXY ||
17
+ process.env.http_proxy ||
18
+ null;
19
+
20
+ if (!proxyUrl) return null;
21
+
22
+ const raw =
23
+ process.env.NO_PROXY ||
24
+ process.env.no_proxy ||
25
+ "";
26
+
27
+ const noProxy = raw
28
+ .split(",")
29
+ .map((s) => s.trim())
30
+ .filter(Boolean);
31
+
32
+ return { proxyUrl, noProxy };
33
+ }
34
+
35
+ export function shouldBypassProxy(hostname, noProxyList) {
36
+ if (!noProxyList || noProxyList.length === 0) return false;
37
+ for (const entry of noProxyList) {
38
+ if (entry === "*") return true;
39
+ if (hostname === entry) return true;
40
+ if (entry.startsWith(".") && hostname.endsWith(entry)) return true;
41
+ }
42
+ return false;
43
+ }
44
+
45
+ export function installProxy() {
46
+ try {
47
+ const cfg = resolveProxy();
48
+ if (!cfg) return;
49
+
50
+ const require = createRequire(import.meta.url);
51
+ const { ProxyAgent, setGlobalDispatcher } = require("undici");
52
+
53
+ setGlobalDispatcher(
54
+ new ProxyAgent({
55
+ uri: cfg.proxyUrl,
56
+ noProxy: cfg.noProxy.join(","),
57
+ }),
58
+ );
59
+ } catch {
60
+ // Silent: undici unavailable or ProxyAgent unsupported — fall through to
61
+ // default (unproxied) fetch. Never break startup.
62
+ }
63
+ }
@@ -0,0 +1,71 @@
1
+ // Boot-time staleness self-check for the LOCAL edition.
2
+ //
3
+ // The .mcpb Desktop extension is a frozen snapshot: it never auto-updates, so
4
+ // an installed bundle silently rots (a pre-0.20.17 bundle crashed on boot for a
5
+ // week; stale installs pinned old CLIs that the API rejects). The hosted
6
+ // edition auto-updates on cutover and never needs this.
7
+ //
8
+ // The check is strictly best-effort: one fetch to the npm registry with a short
9
+ // timeout, fire-and-forget at boot. Offline, slow, or erroring → null, silently.
10
+ // The result is surfaced in TWO places: stderr (Desktop MCP logs) and the
11
+ // grid_start live context, where the model can relay it to the user in-session
12
+ // — turning silent rot into a one-line user action.
13
+
14
+ const REGISTRY_LATEST_URL = "https://registry.npmjs.org/@cloudgrid-io/mcp/latest";
15
+ const CHECK_TIMEOUT_MS = 2500;
16
+
17
+ // Compare two semver strings (no prerelease handling — releases are plain
18
+ // X.Y.Z). Returns -1 / 0 / 1 for a < b / a == b / a > b; null if unparsable.
19
+ export function compareSemver(a, b) {
20
+ const pa = String(a ?? "").split(".").map((n) => Number.parseInt(n, 10));
21
+ const pb = String(b ?? "").split(".").map((n) => Number.parseInt(n, 10));
22
+ if (pa.length !== 3 || pb.length !== 3 || pa.some(Number.isNaN) || pb.some(Number.isNaN)) return null;
23
+ for (let i = 0; i < 3; i++) {
24
+ if (pa[i] < pb[i]) return -1;
25
+ if (pa[i] > pb[i]) return 1;
26
+ }
27
+ return 0;
28
+ }
29
+
30
+ /**
31
+ * One best-effort registry check. Resolves { current, latest, behind } when the
32
+ * registry answered and `current` is older than `latest`; resolves null in every
33
+ * other case (up to date, ahead, offline, timeout, bad payload). Never throws.
34
+ */
35
+ export async function checkForNewerVersion(current, { fetchImpl = fetch, timeoutMs = CHECK_TIMEOUT_MS } = {}) {
36
+ try {
37
+ const ac = new AbortController();
38
+ const timer = setTimeout(() => ac.abort(), timeoutMs);
39
+ let res;
40
+ try {
41
+ res = await fetchImpl(REGISTRY_LATEST_URL, { signal: ac.signal });
42
+ } finally {
43
+ clearTimeout(timer);
44
+ }
45
+ if (!res?.ok) return null;
46
+ const data = await res.json();
47
+ const latest = data?.version;
48
+ if (typeof latest !== "string") return null;
49
+ if (compareSemver(current, latest) === -1) {
50
+ return { current, latest, behind: true };
51
+ }
52
+ return null;
53
+ } catch {
54
+ return null;
55
+ }
56
+ }
57
+
58
+ /**
59
+ * The one-line, model-relayable note grid_start appends when the local MCP is
60
+ * stale. Kept short and §23-plain; names the .mcpb non-auto-update property so
61
+ * the model explains WHY a reinstall is needed.
62
+ */
63
+ export function stalenessNote(staleness) {
64
+ if (!staleness?.behind) return null;
65
+ return (
66
+ `Note: this local CloudGrid MCP is v${staleness.current}; the latest is v${staleness.latest}. ` +
67
+ `If it is installed as the Claude Desktop extension (.mcpb), it never auto-updates — ` +
68
+ `tell the user to reinstall the latest cloudgrid.mcpb from https://github.com/cloudgrid-io/mcp/releases/latest ` +
69
+ `(npx installs pick up the latest on their next cold start).`
70
+ );
71
+ }
@@ -1401,6 +1401,10 @@ export async function runPlug(ctx, input, deps = {}) {
1401
1401
  // Kind lets grid_visibility route to the right visibility surface
1402
1402
  // (inspirations vs runtime entities) without a re-detect round-trip.
1403
1403
  kind: data.detection?.kind ?? data.kind ?? null,
1404
+ // poll_url lets grid_check_deploy default to this session's build with
1405
+ // no arguments ("is it live yet?" needs no ids).
1406
+ poll_url: data.poll_url ?? null,
1407
+ grid: data.grid ?? null,
1404
1408
  owner_token: useAnonWire ? (freshOwnerToken ?? ownerToken ?? null) : null,
1405
1409
  };
1406
1410
  }
@@ -1414,6 +1418,32 @@ export async function runPlug(ctx, input, deps = {}) {
1414
1418
  ctx.state.lastAnonClaim = null;
1415
1419
  }
1416
1420
 
1421
+ // Confirm-before-claiming-live: a runtime build is async (status "building" +
1422
+ // poll_url). Rather than returning immediately — which trained models to
1423
+ // promise URLs that then 502'd — poll the deploy trace server-side for a short
1424
+ // budget. Fast builds come back "live" in this same tool call; a build that
1425
+ // outlives the budget returns "building" with explicit do-not-claim-live copy;
1426
+ // a failed build surfaces the platform's user-language error instead of a URL.
1427
+ if ((data.status === "building" || data.poll_url) && data.poll_url && !useAnonWire) {
1428
+ const verdict = await pollDeployTrace(ctx, {
1429
+ pollUrl: data.poll_url,
1430
+ grid: data.grid,
1431
+ budgetMs: ctx.deployPollBudgetMs ?? DEPLOY_POLL_BUDGET_MS,
1432
+ intervalMs: ctx.deployPollIntervalMs ?? DEPLOY_POLL_INTERVAL_MS,
1433
+ });
1434
+ if (verdict.status === "success") {
1435
+ data.status = "live";
1436
+ data.poll_url = undefined;
1437
+ } else if (verdict.status === "failed") {
1438
+ const msg = verdict.error || "The build failed.";
1439
+ throw new Error(
1440
+ `Deploy failed (trace ${data.trace_id ?? "n/a"}): ${msg} The URL is NOT live — do not give it to the user as working.`,
1441
+ );
1442
+ }
1443
+ // Anything else (still building / poll unreachable): fall through to the
1444
+ // building wording below — the do-not-claim-live path.
1445
+ }
1446
+
1417
1447
  const structured = {
1418
1448
  ...(data.entity_id ? { entity_id: data.entity_id } : {}),
1419
1449
  ...(data.slug ? { slug: data.slug } : {}),
@@ -1456,10 +1486,16 @@ export async function runPlug(ctx, input, deps = {}) {
1456
1486
  ? `Building (async): ${url} — the update is deploying, not live yet.`
1457
1487
  : `Building (async): ${url} — the deploy is in progress, not live yet.`,
1458
1488
  );
1489
+ // Point at a tool that exists on THIS edition. grid_status is CLI-wrapping,
1490
+ // local-only — telling a hosted (ChatGPT/claude.ai) model to "run
1491
+ // grid_status" dead-ends (observed live: a hosted session had no status
1492
+ // tool and blind-polled the public URL into a 502). grid_check_deploy is
1493
+ // direct-API and registered on both editions.
1494
+ const checkHint = ctx.edition === "web" ? "grid_check_deploy" : "grid_check_deploy (or grid_status)";
1459
1495
  lines.push(
1460
1496
  data.poll_url
1461
- ? `Poll ${data.poll_url} or run grid_status until it is ready (trace ${data.trace_id ?? "n/a"}). Do not report it as live until then.`
1462
- : "Run grid_status until it is ready. Do not report it as live until then.",
1497
+ ? `Call ${checkHint} until status is "success" (trace ${data.trace_id ?? "n/a"}). Do NOT tell the user it is live until then.`
1498
+ : `Call ${checkHint} until it is ready. Do NOT tell the user it is live until then.`,
1463
1499
  );
1464
1500
  } else if (isEdit) {
1465
1501
  lines.push(`Updated in place: ${url}`);
@@ -1724,6 +1760,91 @@ export async function runVisibility(ctx, { target, visibility, kind, org }) {
1724
1760
  throw new Error(`Visibility change failed (HTTP ${last?.res?.status}): ${msg}${hint ? ` ${hint}` : ""}`);
1725
1761
  }
1726
1762
 
1763
+ // ── Deploy-trace polling (confirm-before-claiming-live) ─────────────────────
1764
+ // A runtime build is async: POST /api/v2/plug answers 202 { status: "building",
1765
+ // poll_url: "/deploys/<trace_id>" } and GET {API}/deploys/<trace_id> (org-authed)
1766
+ // reports { status, error } until a terminal "success" | "failed". Budget below
1767
+ // is deliberately under typical host tool timeouts; slower builds fall back to
1768
+ // the explicit do-not-claim-live wording + grid_check_deploy.
1769
+ const DEPLOY_POLL_BUDGET_MS = 45_000;
1770
+ const DEPLOY_POLL_INTERVAL_MS = 3_000;
1771
+ const sleepMs = (ms) => new Promise((r) => setTimeout(r, ms));
1772
+
1773
+ // GET one deploy-trace snapshot. Returns { status, error } — status is the
1774
+ // server's word ("queued"/"building"/"success"/"failed"), error is the
1775
+ // user-language message when failed. Throws only on a non-OK HTTP response.
1776
+ async function fetchDeployTrace(ctx, { pollUrl, grid }) {
1777
+ const token = await ctx.getToken();
1778
+ const headers = {};
1779
+ if (token) headers.Authorization = `Bearer ${token}`;
1780
+ const orgSlug = grid || (await ctx.getActiveGrid());
1781
+ if (orgSlug) {
1782
+ headers["X-CloudGrid-Grid"] = orgSlug;
1783
+ headers["X-CloudGrid-Org"] = orgSlug;
1784
+ }
1785
+ const path = pollUrl.startsWith("http") ? pollUrl : `${API_BASE}${pollUrl.startsWith("/") ? "" : "/"}${pollUrl}`;
1786
+ const res = await fetch(path, { headers });
1787
+ const raw = await res.text();
1788
+ let data = null;
1789
+ try { data = JSON.parse(raw); } catch { /* handled below */ }
1790
+ if (!res.ok) {
1791
+ throw new Error(`Deploy status check failed (HTTP ${res.status}): ${data?.error?.message || data?.error || raw || res.status}`);
1792
+ }
1793
+ return {
1794
+ status: data?.status ?? "unknown",
1795
+ error: data?.error?.message_user || data?.error?.message || null,
1796
+ };
1797
+ }
1798
+
1799
+ // Poll until terminal or the budget runs out. Never throws — an unreachable
1800
+ // poll endpoint degrades to { status: "unknown" } and the caller keeps the
1801
+ // do-not-claim-live wording (the safe direction).
1802
+ export async function pollDeployTrace(ctx, { pollUrl, grid, budgetMs = DEPLOY_POLL_BUDGET_MS, intervalMs = DEPLOY_POLL_INTERVAL_MS }) {
1803
+ const start = Date.now();
1804
+ let last = { status: "unknown", error: null };
1805
+ for (;;) {
1806
+ try {
1807
+ last = await fetchDeployTrace(ctx, { pollUrl, grid });
1808
+ } catch {
1809
+ // transient/unauthorized/network — keep last, retry within budget
1810
+ }
1811
+ if (last.status === "success" || last.status === "failed") return last;
1812
+ if (Date.now() - start + intervalMs > budgetMs) return last;
1813
+ await sleepMs(intervalMs);
1814
+ }
1815
+ }
1816
+
1817
+ // grid_check_deploy — one authed status check for an async build. Direct API,
1818
+ // both editions: this is the status verb hosted (ChatGPT/claude.ai) sessions
1819
+ // were missing — they had no way to confirm a runtime build came live and
1820
+ // blind-polled the public URL into 502s.
1821
+ export async function runCheckDeploy(ctx, { poll_url, grid } = {}) {
1822
+ const pollUrl = poll_url || ctx.state.lastDrop?.poll_url;
1823
+ if (!pollUrl) {
1824
+ throw new Error(
1825
+ "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 poll_url.)",
1826
+ );
1827
+ }
1828
+ const verdict = await fetchDeployTrace(ctx, { pollUrl, grid: grid || ctx.state.lastDrop?.grid });
1829
+ const url = ctx.state.lastDrop?.url;
1830
+ if (verdict.status === "success") {
1831
+ return {
1832
+ text: `Live${url ? `: ${url}` : ""} — the build finished. Give the user the URL.`,
1833
+ structured: { status: "success", live: true, ...(url ? { url } : {}) },
1834
+ };
1835
+ }
1836
+ if (verdict.status === "failed") {
1837
+ return {
1838
+ 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.`,
1839
+ structured: { status: "failed", live: false, ...(verdict.error ? { error: verdict.error } : {}) },
1840
+ };
1841
+ }
1842
+ return {
1843
+ text: `Still ${verdict.status === "unknown" ? "building (status unavailable)" : verdict.status} — NOT live yet. Do not tell the user it is live. Wait ~15s and call grid_check_deploy again.`,
1844
+ structured: { status: verdict.status, live: false },
1845
+ };
1846
+ }
1847
+
1727
1848
  // Max HTML we'll return inline from runSource. Past this, we return the first
1728
1849
  // slice with truncated:true — a re-plug needs the complete document, so a
1729
1850
  // truncated body is a signal the drop is likely multi-file rather than a single
@@ -26,7 +26,9 @@ import {
26
26
  runDownload,
27
27
  runVisibility,
28
28
  runSource,
29
+ runCheckDeploy,
29
30
  } from "./deploy.js";
31
+ import { stalenessNote } from "../staleness.js";
30
32
 
31
33
  // Build the argv for grid_create_project → `grid new` (CLI >= 0.15.14; `init`
32
34
  // is a deprecated alias we no longer author). Modern shape: `new <slug>` with
@@ -643,6 +645,36 @@ export function registerTools(server, ctx) {
643
645
  },
644
646
  );
645
647
 
648
+ // Deploy-status check — both editions. The status verb hosted sessions were
649
+ // missing: grid_status wraps the CLI (local-only), so a ChatGPT/claude.ai
650
+ // session had NO way to confirm an async runtime build came live and
651
+ // blind-polled the public URL into 502s. Direct API, no CLI.
652
+ reg(
653
+ "grid_check_deploy",
654
+ {
655
+ description:
656
+ "Check whether an async runtime-app build has finished and the app is live. Call this after grid_deploy returns status \"building\" — repeat every ~15s until it reports success or failed, and do NOT tell the user the app is live until it does. Defaults to this session's last deploy; pass poll_url from a grid_deploy result to check another. Requires sign-in (builds are owned). Calls the API directly — works on every edition, including hosted.",
657
+ inputSchema: {
658
+ poll_url: z.string().optional().describe("The poll_url from a grid_deploy result. Defaults to this session's last deploy."),
659
+ grid: z.string().optional().describe("Grid of the entity. Defaults to the deploy's grid, then the active grid."),
660
+ },
661
+ outputSchema: {
662
+ status: z.string().describe("The build status: success | failed | building | queued | unknown."),
663
+ live: z.boolean().describe("True only when the build finished successfully and the URL serves."),
664
+ url: z.string().optional().describe("The live URL, when known."),
665
+ error: z.string().optional().describe("User-language failure reason, when the build failed."),
666
+ },
667
+ annotations: { readOnlyHint: true, destructiveHint: false, openWorldHint: true },
668
+ },
669
+ async (input) => {
670
+ try {
671
+ return okResult(await runCheckDeploy(ctx, input || {}));
672
+ } catch (err) {
673
+ return fail(err.message);
674
+ }
675
+ },
676
+ );
677
+
646
678
  // Org listing — both editions.
647
679
  reg(
648
680
  "grid_list_grids",
@@ -724,6 +756,13 @@ export function registerTools(server, ctx) {
724
756
  .object({
725
757
  active_grid: z.string().nullable().describe("The user's active grid/org slug, or null."),
726
758
  signed_in: z.boolean().describe("Whether the current session is signed in."),
759
+ update_available: z
760
+ .object({
761
+ current: z.string().describe("This MCP's version."),
762
+ latest: z.string().describe("Latest published version."),
763
+ })
764
+ .optional()
765
+ .describe("Present when this local MCP is behind the latest release — relay the reinstall note to the user."),
727
766
  })
728
767
  .optional()
729
768
  .describe("Live context from the current session."),
@@ -744,17 +783,28 @@ export function registerTools(server, ctx) {
744
783
  } catch {
745
784
  activeGrid = null;
746
785
  }
786
+ // Staleness (local edition): populated fire-and-forget at boot. The
787
+ // .mcpb never auto-updates, so this is where silent rot becomes a
788
+ // one-line user action — the model relays the note in-session.
789
+ const stale = stalenessNote(ctx.staleness);
747
790
  const structured = {
748
791
  playbook: PLAYBOOK,
749
792
  workflows,
750
- context: { active_grid: activeGrid, signed_in: signedIn },
793
+ context: {
794
+ active_grid: activeGrid,
795
+ signed_in: signedIn,
796
+ ...(ctx.staleness?.behind
797
+ ? { update_available: { current: ctx.staleness.current, latest: ctx.staleness.latest } }
798
+ : {}),
799
+ },
751
800
  };
752
801
  const wfLines = workflows.length
753
802
  ? workflows.map((w) => ` - ${w.name}: ${w.when || w.summary}`).join("\n")
754
803
  : " (none available)";
755
804
  const text =
756
805
  `${PLAYBOOK}\n\nAvailable workflows:\n${wfLines}\n\n` +
757
- `Next: match the intent to a workflow and call grid_get_template({kind:"workflow", name}).`;
806
+ `Next: match the intent to a workflow and call grid_get_template({kind:"workflow", name}).` +
807
+ (stale ? `\n\n${stale}` : "");
758
808
  return okResult({ text, structured });
759
809
  },
760
810
  );
package/src/tools.js CHANGED
@@ -36,6 +36,8 @@ export {
36
36
  runFork,
37
37
  runSource,
38
38
  runVisibility,
39
+ runCheckDeploy,
40
+ pollDeployTrace,
39
41
  } from "./tools/deploy.js";
40
42
  export { registerTools, buildCreateProjectArgs } from "./tools/register.js";
41
43
  export { decodeJwt } from "./auth.js";
package/src/web.js CHANGED
@@ -14,6 +14,9 @@
14
14
  //
15
15
  // Run: PORT=8080 node src/web.js Health: GET /healthz
16
16
 
17
+ import { installProxy } from "./proxy.js";
18
+ installProxy();
19
+
17
20
  import { readFileSync } from "node:fs";
18
21
  import { randomUUID } from "node:crypto";
19
22
  import express from "express";