@cursor/july 0.1.87 → 0.1.88

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.
Files changed (49) hide show
  1. package/dist/bin/agent-serve.js +7 -0
  2. package/dist/internal/advertise-tools.d.ts.map +1 -1
  3. package/dist/internal/advertise-tools.js +4 -2
  4. package/dist/internal/cli-docs.js +11 -0
  5. package/dist/internal/cloud-merge.d.ts +3 -1
  6. package/dist/internal/cloud-merge.d.ts.map +1 -1
  7. package/dist/internal/cloud-merge.js +10 -2
  8. package/dist/internal/cursor/backend-client.d.ts +4 -0
  9. package/dist/internal/cursor/backend-client.d.ts.map +1 -1
  10. package/dist/internal/cursor/backend-client.js +4 -0
  11. package/dist/internal/discovery.d.ts.map +1 -1
  12. package/dist/internal/discovery.js +87 -11
  13. package/dist/internal/docs-site.d.ts +13 -2
  14. package/dist/internal/docs-site.d.ts.map +1 -1
  15. package/dist/internal/docs-site.js +76 -13
  16. package/dist/internal/grokbot/runner.d.ts +61 -0
  17. package/dist/internal/grokbot/runner.d.ts.map +1 -0
  18. package/dist/internal/grokbot/runner.js +278 -0
  19. package/dist/internal/mcp-endpoint.js +4 -2
  20. package/dist/internal/mcp-host.d.ts +14 -1
  21. package/dist/internal/mcp-host.d.ts.map +1 -1
  22. package/dist/internal/mcp-host.js +41 -2
  23. package/dist/internal/runtime-dispatch-runner.d.ts +30 -0
  24. package/dist/internal/runtime-dispatch-runner.d.ts.map +1 -0
  25. package/dist/internal/runtime-dispatch-runner.js +60 -0
  26. package/dist/internal/sdk-runner.d.ts.map +1 -1
  27. package/dist/internal/sdk-runner.js +7 -0
  28. package/dist/internal/server.d.ts.map +1 -1
  29. package/dist/internal/server.js +24 -1
  30. package/dist/internal/session-engine.d.ts.map +1 -1
  31. package/dist/internal/session-engine.js +36 -18
  32. package/dist/types.d.ts +64 -2
  33. package/dist/types.d.ts.map +1 -1
  34. package/package.json +1 -1
  35. package/src/bin/agent-serve.ts +8 -0
  36. package/src/internal/advertise-tools.ts +6 -0
  37. package/src/internal/cli-docs.ts +11 -0
  38. package/src/internal/cloud-merge.ts +12 -2
  39. package/src/internal/cursor/backend-client.ts +4 -0
  40. package/src/internal/discovery.ts +133 -13
  41. package/src/internal/docs-site.ts +83 -13
  42. package/src/internal/grokbot/runner.ts +361 -0
  43. package/src/internal/mcp-endpoint.ts +4 -0
  44. package/src/internal/mcp-host.ts +48 -0
  45. package/src/internal/runtime-dispatch-runner.ts +63 -0
  46. package/src/internal/sdk-runner.ts +9 -0
  47. package/src/internal/server.ts +32 -4
  48. package/src/internal/session-engine.ts +40 -15
  49. package/src/types.ts +65 -2
@@ -436,7 +436,7 @@ async function loadAgent(
436
436
  );
437
437
  const seedFiles = await loadSeedFiles(ctx, agentDir);
438
438
  const subagents = await loadSubagents(ctx, join(agentDir, "subagents"));
439
- const runtime = config?.runtime === "cloud" ? "cloud" : "local";
439
+ const runtime = resolveRuntime(ctx, agentDir, config?.runtime);
440
440
  // Keep authored `cloud` even when default runtime is local — per-session
441
441
  // `send({ cloud })` merges it under cloudOverride for handoff sessions.
442
442
  const cloud = config?.cloud;
@@ -460,29 +460,30 @@ async function loadAgent(
460
460
  authoredAllowlist === undefined || authoredAllowlist.includes("mcp")
461
461
  ? authoredAllowlist
462
462
  : [...authoredAllowlist, "mcp" as const];
463
- // The allowlist states runtime-agnostic intent, but the SDK cannot
464
- // enforce tool restrictions on cloud agents yet a cloud-runtime agent
465
- // that declares one must fail at serve start, never run unrestricted.
466
- // (Hybrid per-send cloud sessions fail closed in the session engine.)
467
- if (allowedTools !== undefined && runtime === "cloud") {
463
+ // The allowlist states runtime-agnostic intent, but only the local
464
+ // harness can enforce it (the Cursor SDK rejects tool restrictions on
465
+ // cloud agents, and the Grok Bot harness has its own fixed toolset) — a
466
+ // non-local agent that declares one must fail at serve start, never run
467
+ // unrestricted. (Hybrid per-send cloud sessions fail closed in the
468
+ // session engine.)
469
+ if (allowedTools !== undefined && runtime !== "local") {
468
470
  ctx.diagnostics.push({
469
471
  severity: "error",
470
472
  path: displayPath(ctx, join(agentDir, "agent.ts")),
471
- message:
472
- 'tools cannot be enforced on runtime: "cloud" yet (the Cursor SDK rejects tool restrictions on cloud agents). Remove `tools` or run the agent on the local runtime.',
473
+ message: `tools cannot be enforced on runtime: "${runtime}". Remove \`tools\` or run the agent on the local runtime.`,
473
474
  });
474
475
  }
475
476
  // Per-session connection features (defineConnection auth/advertiseTools)
476
477
  // only run on local turns: advertised tools are in-process server tools,
477
478
  // and per-session headers cannot reach a cloud VM's MCP config. A
478
- // cloud-runtime agent that declares them must fail at serve start —
479
- // never run without the connection's tools or identity. (Hybrid
480
- // per-send cloud sessions fail closed in the session engine.)
479
+ // non-local agent that declares them must fail at serve start — never
480
+ // run without the connection's tools or identity. (Hybrid per-send
481
+ // cloud sessions fail closed in the session engine.)
481
482
  const perSessionConnection = connections.find(
482
483
  (connection) =>
483
484
  connection.advertiseTools === true || connection.auth !== undefined
484
485
  );
485
- if (perSessionConnection !== undefined && runtime === "cloud") {
486
+ if (perSessionConnection !== undefined && runtime !== "local") {
486
487
  ctx.diagnostics.push({
487
488
  severity: "error",
488
489
  path: displayPath(ctx, join(agentDir, "agent.ts")),
@@ -532,9 +533,22 @@ async function loadAgent(
532
533
  hasCloudOptions: cloud !== undefined,
533
534
  });
534
535
  }
535
-
536
536
  appendBuiltinTools(ctx, agentDir, tools, config?.builtinTools);
537
537
 
538
+ // After appendBuiltinTools so opted-in built-ins (server-executed) count.
539
+ if (runtime === "grokbot") {
540
+ checkGrokBotRuntimeLimitations(ctx, agentDir, {
541
+ serverTools: tools.filter((tool) => tool.execution === "server").length,
542
+ agentTools: tools.filter((tool) => tool.execution === "agent").length,
543
+ skills: skills.length,
544
+ seedFiles: seedFiles.length,
545
+ connections: connections.length,
546
+ subagents: subagents.length,
547
+ hasCloudOptions: cloud !== undefined,
548
+ hasLocalSandbox: localSandbox,
549
+ });
550
+ }
551
+
538
552
  return {
539
553
  name,
540
554
  description: config?.description,
@@ -1575,6 +1589,112 @@ function appendBuiltinTools(
1575
1589
  }
1576
1590
  }
1577
1591
 
1592
+ /**
1593
+ * Validate the authored runtime. An unrecognized value is an error
1594
+ * diagnostic and falls back to local — a typo must not silently pick a
1595
+ * different harness for every turn.
1596
+ */
1597
+ function resolveRuntime(
1598
+ ctx: DiscoveryContext,
1599
+ agentDir: string,
1600
+ runtime: AgentRuntime | undefined
1601
+ ): AgentRuntime {
1602
+ if (
1603
+ runtime === undefined ||
1604
+ runtime === "local" ||
1605
+ runtime === "cloud" ||
1606
+ runtime === "grokbot"
1607
+ ) {
1608
+ return runtime ?? "local";
1609
+ }
1610
+ ctx.diagnostics.push({
1611
+ severity: "error",
1612
+ path: displayPath(ctx, join(agentDir, "agent.ts")),
1613
+ message: `runtime must be "local", "cloud", or "grokbot"; got ${JSON.stringify(runtime)}.`,
1614
+ });
1615
+ return "local";
1616
+ }
1617
+
1618
+ /**
1619
+ * Grok Bot turns run on the Sand harness with its own toolset: authored
1620
+ * server tools, MCP connections, and subagents have no path onto that
1621
+ * toolset yet, so an agent declaring them must fail at serve start rather
1622
+ * than run with capabilities silently missing.
1623
+ */
1624
+ function checkGrokBotRuntimeLimitations(
1625
+ ctx: DiscoveryContext,
1626
+ agentDir: string,
1627
+ counts: {
1628
+ serverTools: number;
1629
+ agentTools: number;
1630
+ skills: number;
1631
+ seedFiles: number;
1632
+ connections: number;
1633
+ subagents: number;
1634
+ hasCloudOptions: boolean;
1635
+ hasLocalSandbox: boolean;
1636
+ }
1637
+ ): void {
1638
+ const agentPath = displayPath(ctx, join(agentDir, "agent.ts"));
1639
+ const error = (path: string, message: string): void => {
1640
+ ctx.diagnostics.push({ severity: "error", path, message });
1641
+ };
1642
+ const warn = (path: string, message: string): void => {
1643
+ ctx.diagnostics.push({ severity: "warning", path, message });
1644
+ };
1645
+ if (counts.serverTools > 0) {
1646
+ error(
1647
+ displayPath(ctx, join(agentDir, "tools")),
1648
+ 'Server tools (execution: "server", including opted-in builtinTools) are not reachable from grokbot turns yet. Remove them or run the agent on the local runtime.'
1649
+ );
1650
+ }
1651
+ if (counts.connections > 0) {
1652
+ error(
1653
+ displayPath(ctx, join(agentDir, "mcp-connections")),
1654
+ "MCP connections are not wired into grokbot turns yet. Remove them or run the agent on the local runtime."
1655
+ );
1656
+ }
1657
+ if (counts.subagents > 0) {
1658
+ error(
1659
+ displayPath(ctx, join(agentDir, "subagents")),
1660
+ "Authored subagents are not available on grokbot turns (the Grok Bot harness runs its own subagent set). Remove them or run the agent on the local runtime."
1661
+ );
1662
+ }
1663
+ // The remaining declarations mirror the cloud runtime's warnings: the
1664
+ // turn runs on a hosted machine (the account's box), so local-workspace
1665
+ // materialization never reaches it.
1666
+ if (counts.agentTools > 0) {
1667
+ warn(
1668
+ displayPath(ctx, join(agentDir, "tools")),
1669
+ 'Agent tools (execution: "agent") are described in the hosted session\'s instructions (with script bodies); they are not written onto the box filesystem automatically.'
1670
+ );
1671
+ }
1672
+ if (counts.skills > 0) {
1673
+ warn(
1674
+ displayPath(ctx, join(agentDir, "skills")),
1675
+ "Authored skills are materialized into the local session workspace only; the hosted Grok Bot box does not see them."
1676
+ );
1677
+ }
1678
+ if (counts.seedFiles > 0) {
1679
+ warn(
1680
+ displayPath(ctx, join(agentDir, "sandbox")),
1681
+ "sandbox/workspace seed files apply to the local session workspace only; grokbot turns run on the account's hosted box."
1682
+ );
1683
+ }
1684
+ if (counts.hasCloudOptions) {
1685
+ warn(
1686
+ agentPath,
1687
+ 'The `cloud` block is unused on runtime: "grokbot", and grokbot sessions refuse per-session cloud attach.'
1688
+ );
1689
+ }
1690
+ if (counts.hasLocalSandbox) {
1691
+ warn(
1692
+ agentPath,
1693
+ 'local.sandbox does not apply to runtime: "grokbot"; the box is the account\'s Cursor-hosted computer.'
1694
+ );
1695
+ }
1696
+ }
1697
+
1578
1698
  function warnCloudRuntimeLimitations(
1579
1699
  ctx: DiscoveryContext,
1580
1700
  agentDir: string,
@@ -6,10 +6,16 @@
6
6
  * resolve under the mount). `serve` auto-builds it when the local
7
7
  * vitepress toolchain is present (source checkouts); published installs
8
8
  * ship the prebuilt site inside `dist/**`.
9
+ *
10
+ * Set `AGENT_SERVE_DOCS_DIST` to an overlay directory that contains a built
11
+ * site (`index.html`). When that path exists, `/docs` is served from it so a
12
+ * long-lived host (corp ECS) can pick up the published `@cursor/july` docs
13
+ * without rebuilding the process image. Requests re-read the path, so an
14
+ * atomic symlink swap is visible without a restart.
9
15
  */
10
16
 
11
17
  import { spawn } from "node:child_process";
12
- import { existsSync, readFileSync, statSync } from "node:fs";
18
+ import { existsSync, readFileSync, realpathSync, statSync } from "node:fs";
13
19
  import { createRequire } from "node:module";
14
20
  import { dirname, extname, join, normalize, sep } from "node:path";
15
21
  import { fileURLToPath } from "node:url";
@@ -21,14 +27,69 @@ const DOCS_DIST_DIR = fileURLToPath(
21
27
  new URL("../../dist/docs/", import.meta.url)
22
28
  );
23
29
 
24
- /** Absolute path of the built VitePress site shipped in this install. */
30
+ const DOCS_OVERLAY_ENV = "AGENT_SERVE_DOCS_DIST";
31
+
32
+ function realDir(dir: string): string | undefined {
33
+ try {
34
+ return realpathSync(dir);
35
+ } catch {
36
+ return undefined;
37
+ }
38
+ }
39
+
40
+ function isInsideRoot(fileReal: string, rootReal: string): boolean {
41
+ const prefix = rootReal.endsWith(sep) ? rootReal : `${rootReal}${sep}`;
42
+ return fileReal === rootReal || fileReal.startsWith(prefix);
43
+ }
44
+
45
+ /**
46
+ * Resolve `candidate` to a regular file whose real path stays under
47
+ * `rootReal`. Follows overlay / site-root symlinks, but refuses a planted
48
+ * symlink (or hard-linked path) that escapes the docs tree.
49
+ */
50
+ function containedFile(
51
+ candidate: string,
52
+ rootReal: string
53
+ ): string | undefined {
54
+ try {
55
+ const real = realpathSync(candidate);
56
+ if (!isInsideRoot(real, rootReal) || !statSync(real).isFile()) {
57
+ return undefined;
58
+ }
59
+ return real;
60
+ } catch {
61
+ return undefined;
62
+ }
63
+ }
64
+
65
+ function hasDocsIndex(distDir: string): boolean {
66
+ const root = realDir(distDir);
67
+ if (root === undefined) {
68
+ return false;
69
+ }
70
+ return containedFile(join(distDir, "index.html"), root) !== undefined;
71
+ }
72
+
73
+ /** Absolute path of the VitePress site shipped in this install (ignores overlay). */
74
+ export function shippedDocsDistDir(): string {
75
+ return DOCS_DIST_DIR;
76
+ }
77
+
78
+ /**
79
+ * Directory `/docs` is served from. Prefers `AGENT_SERVE_DOCS_DIST` when that
80
+ * overlay has a built site; otherwise the install's `dist/docs/`.
81
+ */
25
82
  export function docsDistDir(): string {
83
+ const overlay = process.env[DOCS_OVERLAY_ENV]?.trim();
84
+ if (overlay !== undefined && overlay !== "" && hasDocsIndex(overlay)) {
85
+ return overlay;
86
+ }
26
87
  return DOCS_DIST_DIR;
27
88
  }
28
89
 
29
- /** Whether `pnpm run docs:build` has produced the docs site. */
30
- export function docsSiteExists(distDir: string = DOCS_DIST_DIR): boolean {
31
- return existsSync(join(distDir, "index.html"));
90
+ /** Whether a built docs site is available at `distDir` (overlay or shipped). */
91
+ export function docsSiteExists(distDir: string = docsDistDir()): boolean {
92
+ return hasDocsIndex(distDir);
32
93
  }
33
94
 
34
95
  /**
@@ -39,7 +100,7 @@ export function docsSiteExists(distDir: string = DOCS_DIST_DIR): boolean {
39
100
  */
40
101
  export function resolveDocsFile(
41
102
  requestPath: string,
42
- distDir: string = DOCS_DIST_DIR
103
+ distDir: string = docsDistDir()
43
104
  ): string | undefined {
44
105
  const decoded = requestPath
45
106
  .split("/")
@@ -49,14 +110,19 @@ export function resolveDocsFile(
49
110
  if (normalized.startsWith("..") || normalized.includes(`..${sep}`)) {
50
111
  return undefined;
51
112
  }
113
+ const rootReal = realDir(distDir);
114
+ if (rootReal === undefined) {
115
+ return undefined;
116
+ }
52
117
  const root = distDir.endsWith(sep) ? distDir : `${distDir}${sep}`;
53
118
  const base = join(root, normalized);
54
119
  if (base !== root.slice(0, -1) && !base.startsWith(root)) {
55
120
  return undefined;
56
121
  }
57
122
  for (const candidate of [base, join(base, "index.html"), `${base}.html`]) {
58
- if (existsSync(candidate) && statSync(candidate).isFile()) {
59
- return candidate;
123
+ const file = containedFile(candidate, rootReal);
124
+ if (file !== undefined) {
125
+ return file;
60
126
  }
61
127
  }
62
128
  return undefined;
@@ -65,7 +131,7 @@ export function resolveDocsFile(
65
131
  /** Serve one path from the built docs site. */
66
132
  export function renderDocsFile(
67
133
  requestPath: string,
68
- distDir: string = DOCS_DIST_DIR
134
+ distDir: string = docsDistDir()
69
135
  ): Response {
70
136
  if (!docsSiteExists(distDir)) {
71
137
  return new Response(fallbackHtml(), {
@@ -75,9 +141,13 @@ export function renderDocsFile(
75
141
  }
76
142
  const filePath = resolveDocsFile(requestPath, distDir);
77
143
  if (filePath === undefined) {
78
- const notFound = join(distDir, "404.html");
144
+ const root = realDir(distDir);
145
+ const notFound =
146
+ root === undefined
147
+ ? undefined
148
+ : containedFile(join(distDir, "404.html"), root);
79
149
  return new Response(
80
- existsSync(notFound) ? readFileSync(notFound, "utf8") : "Not found",
150
+ notFound !== undefined ? readFileSync(notFound, "utf8") : "Not found",
81
151
  {
82
152
  status: 404,
83
153
  headers: { "content-type": "text/html; charset=utf-8" },
@@ -110,7 +180,7 @@ export function renderDocsFile(
110
180
  export async function ensureDocsSiteBundle(
111
181
  logger: (line: string) => void = console.log
112
182
  ): Promise<void> {
113
- if (docsSiteExists()) {
183
+ if (docsSiteExists(DOCS_DIST_DIR)) {
114
184
  return;
115
185
  }
116
186
  const toolchain = resolveDocsToolchain();
@@ -133,7 +203,7 @@ export async function ensureDocsSiteBundle(
133
203
  if (code !== 0) {
134
204
  throw new Error(`docs vitepress build failed (exit ${code})`);
135
205
  }
136
- if (!docsSiteExists()) {
206
+ if (!docsSiteExists(DOCS_DIST_DIR)) {
137
207
  throw new Error(
138
208
  "docs vitepress build finished but dist/docs/index.html is missing"
139
209
  );