@cursor/july 0.1.39 → 0.1.41

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 (259) hide show
  1. package/AGENTS.md +4 -3
  2. package/dist/bin/agent-serve.js +28 -9
  3. package/dist/channels/slack/api.d.ts.map +1 -1
  4. package/dist/channels/slack/api.js +18 -8
  5. package/dist/channels/slack/attachments.d.ts +82 -0
  6. package/dist/channels/slack/attachments.d.ts.map +1 -0
  7. package/dist/channels/slack/attachments.js +520 -0
  8. package/dist/channels/slack/cursor-account.d.ts +7 -1
  9. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  10. package/dist/channels/slack/cursor-account.js +2 -2
  11. package/dist/channels/slack/defaults.d.ts.map +1 -1
  12. package/dist/channels/slack/defaults.js +26 -17
  13. package/dist/channels/slack/dispatch.d.ts +4 -3
  14. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  15. package/dist/channels/slack/dispatch.js +84 -21
  16. package/dist/channels/slack/inbound.d.ts +6 -0
  17. package/dist/channels/slack/inbound.d.ts.map +1 -1
  18. package/dist/channels/slack/inbound.js +48 -2
  19. package/dist/channels/slack/index.d.ts +1 -0
  20. package/dist/channels/slack/index.d.ts.map +1 -1
  21. package/dist/channels/slack/index.js +1 -0
  22. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  23. package/dist/channels/slack/live-delivery.js +16 -4
  24. package/dist/channels/slack/manifest.d.ts.map +1 -1
  25. package/dist/channels/slack/manifest.js +1 -0
  26. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  27. package/dist/channels/slack/slack-channel.js +7 -4
  28. package/dist/channels/slack/types.d.ts +49 -1
  29. package/dist/channels/slack/types.d.ts.map +1 -1
  30. package/dist/docs/404.html +2 -2
  31. package/dist/docs/ab.html +6 -6
  32. package/dist/docs/assets/{ab.md.6cLOW7--.js → ab.md.hdxsvX5M.js} +2 -2
  33. package/dist/docs/assets/{app.BQ8Hihdf.js → app.mIw24pb1.js} +1 -1
  34. package/dist/docs/assets/{building-with-agents.md.txrcGU2B.js → building-with-agents.md.PeZaZA1P.js} +1 -1
  35. package/dist/docs/assets/chunks/@localSearchIndexroot.BA7V8dEJ.js +1 -0
  36. package/dist/docs/assets/chunks/{VPLocalSearchBox.BBCr8Yuy.js → VPLocalSearchBox.hOzpPwrG.js} +1 -1
  37. package/dist/docs/assets/chunks/{theme.CK_NiGC-.js → theme.BtFhUYmh.js} +2 -2
  38. package/dist/docs/assets/{deployment.md.CuK5SNjN.js → deployment.md.BHVjVqRa.js} +4 -4
  39. package/dist/docs/assets/{deployment.md.CuK5SNjN.lean.js → deployment.md.BHVjVqRa.lean.js} +1 -1
  40. package/dist/docs/assets/{evals.md.BQXI3rXy.js → evals.md.qeoGgPw0.js} +3 -3
  41. package/dist/docs/assets/example-agents_index.md.xmJ87d_3.js +2 -0
  42. package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.js → guides_webhooks.md.DB-r_er9.js} +47 -4
  43. package/dist/docs/assets/{guides_webhooks.md.BERuBSJW.lean.js → guides_webhooks.md.DB-r_er9.lean.js} +1 -1
  44. package/dist/docs/assets/index.md.BKpbkCa6.js +24 -0
  45. package/dist/docs/assets/{index.md.CmhptOmN.lean.js → index.md.BKpbkCa6.lean.js} +1 -1
  46. package/dist/docs/assets/{quickstart.md.C_b6ESpD.js → quickstart.md.rIbfahJM.js} +4 -4
  47. package/dist/docs/assets/{reference_cli.md.Byvrg8eu.js → reference_cli.md.DqEqQpGn.js} +5 -5
  48. package/dist/docs/assets/{reference_cli.md.Byvrg8eu.lean.js → reference_cli.md.DqEqQpGn.lean.js} +1 -1
  49. package/dist/docs/assets/{reference_hooks.md.BGDw4VLm.js → reference_hooks.md.BHeG-vjK.js} +2 -2
  50. package/dist/docs/assets/{reference_hooks.md.BGDw4VLm.lean.js → reference_hooks.md.BHeG-vjK.lean.js} +1 -1
  51. package/dist/docs/assets/{storage.md.mQDtIULc.js → storage.md.DcGSnIxX.js} +10 -4
  52. package/dist/docs/assets/{storage.md.mQDtIULc.lean.js → storage.md.DcGSnIxX.lean.js} +1 -1
  53. package/dist/docs/assets/troubleshooting.md.CkgwmVI0.js +1 -0
  54. package/dist/docs/assets/{troubleshooting.md.B5RVX_tL.lean.js → troubleshooting.md.CkgwmVI0.lean.js} +1 -1
  55. package/dist/docs/building-with-agents.html +5 -5
  56. package/dist/docs/concepts.html +3 -3
  57. package/dist/docs/deployment.html +6 -6
  58. package/dist/docs/evals.html +7 -7
  59. package/dist/docs/example-agents/approval-buddy.html +3 -3
  60. package/dist/docs/example-agents/benny.html +3 -3
  61. package/dist/docs/example-agents/bugbot.html +3 -3
  62. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  63. package/dist/docs/example-agents/codeowners-review.html +3 -3
  64. package/dist/docs/example-agents/concierge.html +3 -3
  65. package/dist/docs/example-agents/fsd.html +3 -3
  66. package/dist/docs/example-agents/index.html +5 -5
  67. package/dist/docs/example-agents/knowledge-base.html +3 -3
  68. package/dist/docs/example-agents/oncall.html +3 -3
  69. package/dist/docs/example-agents/security-reviewer.html +3 -3
  70. package/dist/docs/example-agents/slack-agent.html +3 -3
  71. package/dist/docs/example-agents/weather-agent.html +3 -3
  72. package/dist/docs/guides/agent-to-agent.html +3 -3
  73. package/dist/docs/guides/cloud-runtime.html +3 -3
  74. package/dist/docs/guides/github.html +3 -3
  75. package/dist/docs/guides/human-in-the-loop.html +3 -3
  76. package/dist/docs/guides/mcp-oauth.html +3 -3
  77. package/dist/docs/guides/slack.html +3 -3
  78. package/dist/docs/guides/webhooks.html +49 -6
  79. package/dist/docs/hashmap.json +1 -1
  80. package/dist/docs/hillclimbing.html +3 -3
  81. package/dist/docs/index.html +7 -7
  82. package/dist/docs/quickstart.html +8 -8
  83. package/dist/docs/reference/agent-config.html +3 -3
  84. package/dist/docs/reference/artifacts.html +3 -3
  85. package/dist/docs/reference/channels.html +3 -3
  86. package/dist/docs/reference/cli.html +8 -8
  87. package/dist/docs/reference/connections.html +3 -3
  88. package/dist/docs/reference/hooks.html +5 -5
  89. package/dist/docs/reference/http-api.html +3 -3
  90. package/dist/docs/reference/instructions.html +3 -3
  91. package/dist/docs/reference/playground.html +3 -3
  92. package/dist/docs/reference/project-layout.html +3 -3
  93. package/dist/docs/reference/prompt.html +3 -3
  94. package/dist/docs/reference/schedules.html +3 -3
  95. package/dist/docs/reference/sessions.html +3 -3
  96. package/dist/docs/reference/skills.html +3 -3
  97. package/dist/docs/reference/subagents.html +3 -3
  98. package/dist/docs/reference/tools.html +3 -3
  99. package/dist/docs/scaffolding-agents.html +3 -3
  100. package/dist/docs/storage.html +14 -8
  101. package/dist/docs/troubleshooting.html +5 -5
  102. package/dist/index.d.ts +4 -2
  103. package/dist/index.d.ts.map +1 -1
  104. package/dist/index.js +3 -1
  105. package/dist/internal/cli-ax.d.ts.map +1 -1
  106. package/dist/internal/cli-ax.js +22 -2
  107. package/dist/internal/cli-deploy.d.ts +6 -0
  108. package/dist/internal/cli-deploy.d.ts.map +1 -1
  109. package/dist/internal/cli-deploy.js +14 -4
  110. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  111. package/dist/internal/cli-mcp-oauth.js +3 -1
  112. package/dist/internal/cli-mcp.js +4 -4
  113. package/dist/internal/cursor-event-relay.d.ts +45 -0
  114. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  115. package/dist/internal/cursor-event-relay.js +96 -2
  116. package/dist/internal/deploy-client.js +1 -1
  117. package/dist/internal/discovery.d.ts.map +1 -1
  118. package/dist/internal/discovery.js +35 -7
  119. package/dist/internal/distribution.d.ts.map +1 -1
  120. package/dist/internal/distribution.js +3 -1
  121. package/dist/internal/durable-session-store.d.ts +48 -0
  122. package/dist/internal/durable-session-store.d.ts.map +1 -0
  123. package/dist/internal/durable-session-store.js +86 -0
  124. package/dist/internal/hosting.d.ts +1 -1
  125. package/dist/internal/hosting.js +2 -2
  126. package/dist/internal/init-project.d.ts +34 -0
  127. package/dist/internal/init-project.d.ts.map +1 -1
  128. package/dist/internal/init-project.js +121 -2
  129. package/dist/internal/ledger-serving.d.ts +101 -0
  130. package/dist/internal/ledger-serving.d.ts.map +1 -0
  131. package/dist/internal/ledger-serving.js +174 -0
  132. package/dist/internal/listen-port.d.ts +22 -0
  133. package/dist/internal/listen-port.d.ts.map +1 -0
  134. package/dist/internal/listen-port.js +77 -0
  135. package/dist/internal/mcp-oauth.d.ts +12 -1
  136. package/dist/internal/mcp-oauth.d.ts.map +1 -1
  137. package/dist/internal/mcp-oauth.js +25 -3
  138. package/dist/internal/playground/static.d.ts +21 -4
  139. package/dist/internal/playground/static.d.ts.map +1 -1
  140. package/dist/internal/playground/static.js +82 -18
  141. package/dist/internal/sdk-runner.d.ts.map +1 -1
  142. package/dist/internal/sdk-runner.js +10 -1
  143. package/dist/internal/server.d.ts.map +1 -1
  144. package/dist/internal/server.js +67 -49
  145. package/dist/internal/session-engine.d.ts +57 -1
  146. package/dist/internal/session-engine.d.ts.map +1 -1
  147. package/dist/internal/session-engine.js +117 -28
  148. package/dist/internal/slack-provision-client.js +1 -1
  149. package/dist/internal/workspace.d.ts +6 -2
  150. package/dist/internal/workspace.d.ts.map +1 -1
  151. package/dist/internal/workspace.js +10 -2
  152. package/dist/kv/memory-kv.d.ts +23 -0
  153. package/dist/kv/memory-kv.d.ts.map +1 -0
  154. package/dist/kv/memory-kv.js +103 -0
  155. package/dist/kv.d.ts +22 -0
  156. package/dist/kv.d.ts.map +1 -0
  157. package/dist/kv.js +20 -0
  158. package/dist/ledger/session-ledger.d.ts +204 -0
  159. package/dist/ledger/session-ledger.d.ts.map +1 -0
  160. package/dist/ledger/session-ledger.js +382 -0
  161. package/dist/ledger.d.ts +11 -0
  162. package/dist/ledger.d.ts.map +1 -0
  163. package/dist/ledger.js +9 -0
  164. package/dist/playground/assets/index-B3HBneZZ.js +85 -0
  165. package/dist/playground/assets/index-Bqk9nplh.css +1 -0
  166. package/dist/playground/index.html +2 -2
  167. package/dist/storage-backends/cursor-hosted.d.ts +25 -3
  168. package/dist/storage-backends/cursor-hosted.d.ts.map +1 -1
  169. package/dist/storage-backends/cursor-hosted.js +50 -2
  170. package/dist/storage-backends/file-kv.d.ts +5 -2
  171. package/dist/storage-backends/file-kv.d.ts.map +1 -1
  172. package/dist/storage-backends/file-kv.js +107 -7
  173. package/dist/storage.d.ts +60 -5
  174. package/dist/storage.d.ts.map +1 -1
  175. package/dist/storage.js +28 -4
  176. package/dist/types.d.ts +123 -3
  177. package/dist/types.d.ts.map +1 -1
  178. package/dist/types.js +14 -0
  179. package/docs/README.md +10 -7
  180. package/docs/ab.md +2 -2
  181. package/docs/building-with-agents.md +1 -1
  182. package/docs/deployment.md +34 -11
  183. package/docs/evals.md +3 -3
  184. package/docs/example-agents/index.md +1 -1
  185. package/docs/guides/webhooks.md +87 -0
  186. package/docs/quickstart.md +7 -8
  187. package/docs/reference/cli.md +15 -6
  188. package/docs/reference/hooks.md +8 -2
  189. package/docs/storage.md +67 -7
  190. package/docs/troubleshooting.md +10 -2
  191. package/package.json +20 -7
  192. package/skills/create-agent/SKILL.md +1 -1
  193. package/src/bin/agent-serve.ts +27 -11
  194. package/src/channels/slack/api.ts +19 -14
  195. package/src/channels/slack/attachments.ts +635 -0
  196. package/src/channels/slack/cursor-account.ts +8 -0
  197. package/src/channels/slack/defaults.ts +34 -18
  198. package/src/channels/slack/dispatch.ts +121 -6
  199. package/src/channels/slack/inbound.ts +60 -0
  200. package/src/channels/slack/index.ts +1 -0
  201. package/src/channels/slack/live-delivery.ts +17 -3
  202. package/src/channels/slack/manifest.ts +1 -0
  203. package/src/channels/slack/slack-channel.ts +7 -0
  204. package/src/channels/slack/types.ts +47 -1
  205. package/src/index.ts +9 -0
  206. package/src/internal/cli-ax.ts +25 -1
  207. package/src/internal/cli-deploy.ts +22 -4
  208. package/src/internal/cli-mcp-oauth.ts +3 -1
  209. package/src/internal/cli-mcp.ts +4 -4
  210. package/src/internal/cursor-event-relay.ts +168 -0
  211. package/src/internal/deploy-client.ts +1 -1
  212. package/src/internal/discovery.ts +42 -5
  213. package/src/internal/distribution.ts +3 -1
  214. package/src/internal/durable-session-store.ts +95 -0
  215. package/src/internal/hosting.ts +2 -2
  216. package/src/internal/init-project.ts +151 -2
  217. package/src/internal/ledger-serving.ts +240 -0
  218. package/src/internal/listen-port.ts +85 -0
  219. package/src/internal/mcp-oauth.ts +29 -4
  220. package/src/internal/playground/static.ts +111 -20
  221. package/src/internal/sdk-runner.ts +11 -1
  222. package/src/internal/server.ts +76 -62
  223. package/src/internal/session-engine.ts +192 -16
  224. package/src/internal/slack-provision-client.ts +1 -1
  225. package/src/internal/workspace.ts +14 -3
  226. package/src/kv/memory-kv.ts +99 -0
  227. package/src/kv.ts +27 -0
  228. package/src/ledger/session-ledger.ts +535 -0
  229. package/src/ledger.ts +29 -0
  230. package/src/storage-backends/cursor-hosted.ts +108 -3
  231. package/src/storage-backends/file-kv.ts +128 -12
  232. package/src/storage.ts +90 -6
  233. package/src/types.ts +130 -4
  234. package/dist/channels/slack/post-update-delivery.d.ts +0 -85
  235. package/dist/channels/slack/post-update-delivery.d.ts.map +0 -1
  236. package/dist/docs/assets/chunks/@localSearchIndexroot.B-VTH4As.js +0 -1
  237. package/dist/docs/assets/example-agents_index.md.DgGBwckv.js +0 -2
  238. package/dist/docs/assets/index.md.CmhptOmN.js +0 -24
  239. package/dist/docs/assets/troubleshooting.md.B5RVX_tL.js +0 -1
  240. package/dist/internal/json-dir-store.d.ts +0 -32
  241. package/dist/internal/json-dir-store.d.ts.map +0 -1
  242. package/dist/internal/persistence-coordinator.d.ts +0 -127
  243. package/dist/internal/persistence-coordinator.d.ts.map +0 -1
  244. package/dist/multi-tenant.d.ts +0 -80
  245. package/dist/multi-tenant.d.ts.map +0 -1
  246. package/dist/multi-tenant.js +0 -69
  247. package/dist/persistence.d.ts +0 -184
  248. package/dist/persistence.d.ts.map +0 -1
  249. package/dist/playground/assets/index-BRhBbnd4.css +0 -1
  250. package/dist/playground/assets/index-_2WKw9a-.js +0 -85
  251. package/dist/storage-backends/postgres-kv.d.ts +0 -82
  252. package/dist/storage-backends/postgres-kv.d.ts.map +0 -1
  253. package/dist/storage-backends/postgres-kv.js +0 -185
  254. package/src/storage-backends/postgres-kv.ts +0 -253
  255. /package/dist/docs/assets/{ab.md.6cLOW7--.lean.js → ab.md.hdxsvX5M.lean.js} +0 -0
  256. /package/dist/docs/assets/{building-with-agents.md.txrcGU2B.lean.js → building-with-agents.md.PeZaZA1P.lean.js} +0 -0
  257. /package/dist/docs/assets/{evals.md.BQXI3rXy.lean.js → evals.md.qeoGgPw0.lean.js} +0 -0
  258. /package/dist/docs/assets/{example-agents_index.md.DgGBwckv.lean.js → example-agents_index.md.xmJ87d_3.lean.js} +0 -0
  259. /package/dist/docs/assets/{quickstart.md.C_b6ESpD.lean.js → quickstart.md.rIbfahJM.lean.js} +0 -0
@@ -3,6 +3,7 @@
3
3
  */
4
4
 
5
5
  import { spawn } from "node:child_process";
6
+ import { existsSync } from "node:fs";
6
7
  import { mkdir, writeFile } from "node:fs/promises";
7
8
  import { basename, dirname, join, relative, resolve } from "node:path";
8
9
  import type { ModelSetting } from "../types.js";
@@ -104,6 +105,7 @@ export async function initProject(
104
105
  description: `agent project: ${name}`,
105
106
  scripts: {
106
107
  check: "tsc --noEmit",
108
+ dev: "agent-sdk dev",
107
109
  },
108
110
  dependencies: {
109
111
  [PACKAGE_NAME]: `^${packageVersion()}`,
@@ -256,6 +258,141 @@ export async function installScaffoldDependencies(
256
258
  };
257
259
  }
258
260
 
261
+ export interface LinkCliOntoPathOptions {
262
+ stdio?: "inherit" | "pipe";
263
+ runner?: (args: {
264
+ command: string;
265
+ args: string[];
266
+ cwd: string;
267
+ stdio: "inherit" | "pipe";
268
+ }) => Promise<{ exitCode: number; stderr: string }>;
269
+ /** Test seam — absolute path of the linked CLI when present. */
270
+ whichImpl?: (
271
+ command: string
272
+ ) => string | undefined | Promise<string | undefined>;
273
+ }
274
+
275
+ export interface LinkCliOntoPathResult {
276
+ ok: boolean;
277
+ exitCode: number;
278
+ stderr: string;
279
+ /** Absolute path to the linked `agent-sdk` when it resolves on PATH. */
280
+ resolvedPath?: string;
281
+ }
282
+
283
+ /**
284
+ * Put `agent-sdk` on the user PATH by installing the already-downloaded
285
+ * project copy into the npm global prefix. `npx @cursor/july init` only
286
+ * runs the package; this is the install that makes bare `agent-sdk …`
287
+ * work afterward.
288
+ */
289
+ export async function linkCliOntoPath(
290
+ rootDir: string,
291
+ options: LinkCliOntoPathOptions = {}
292
+ ): Promise<LinkCliOntoPathResult> {
293
+ const packageDir = join(rootDir, "node_modules", PACKAGE_NAME);
294
+ if (!existsSync(packageDir)) {
295
+ return {
296
+ ok: false,
297
+ exitCode: 1,
298
+ stderr: `missing ${PACKAGE_NAME} under ${packageDir} (run npm install first)`,
299
+ };
300
+ }
301
+ const stdio = options.stdio ?? "inherit";
302
+ const command = process.platform === "win32" ? "npm.cmd" : "npm";
303
+ const args = ["install", "--global", "--no-fund", "--no-audit", packageDir];
304
+ const runner = options.runner ?? defaultNpmInstallRunner;
305
+ const result = await runner({ command, args, cwd: rootDir, stdio });
306
+ if (result.exitCode !== 0) {
307
+ return {
308
+ ok: false,
309
+ exitCode: result.exitCode,
310
+ stderr: result.stderr,
311
+ };
312
+ }
313
+ const whichImpl = options.whichImpl ?? resolveLinkedCli;
314
+ const resolvedPath = await whichImpl(CLI_COMMAND_NAME);
315
+ return {
316
+ ok: resolvedPath !== undefined,
317
+ exitCode: 0,
318
+ stderr:
319
+ resolvedPath === undefined
320
+ ? `${CLI_COMMAND_NAME} linked into the npm global prefix but is not on PATH; open a new shell or add that bin directory to PATH`
321
+ : "",
322
+ resolvedPath,
323
+ };
324
+ }
325
+
326
+ /** Prefer the npm-global bin; ignore an older `agent-sdk` earlier on PATH. */
327
+ async function resolveLinkedCli(
328
+ commandName: string
329
+ ): Promise<string | undefined> {
330
+ const prefix = await npmGlobalPrefix();
331
+ if (prefix === undefined) {
332
+ return whichOnPath(commandName);
333
+ }
334
+ const binDir = process.platform === "win32" ? prefix : join(prefix, "bin");
335
+ const exts =
336
+ process.platform === "win32"
337
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean)
338
+ : [""];
339
+ for (const ext of exts) {
340
+ const candidate = join(binDir, `${commandName}${ext}`);
341
+ if (!existsSync(candidate)) {
342
+ continue;
343
+ }
344
+ // Only claim PATH success when this linked bin is the first hit.
345
+ const onPath = whichOnPath(commandName);
346
+ return onPath === candidate ? candidate : undefined;
347
+ }
348
+ return undefined;
349
+ }
350
+
351
+ async function npmGlobalPrefix(): Promise<string | undefined> {
352
+ const command = process.platform === "win32" ? "npm.cmd" : "npm";
353
+ return await new Promise((resolvePromise) => {
354
+ const child = spawn(command, ["prefix", "-g"], {
355
+ stdio: ["ignore", "pipe", "ignore"],
356
+ env: process.env,
357
+ });
358
+ let out = "";
359
+ child.stdout?.setEncoding("utf8");
360
+ child.stdout?.on("data", (chunk: string) => {
361
+ out += chunk;
362
+ });
363
+ child.on("error", () => resolvePromise(undefined));
364
+ child.on("close", (code) => {
365
+ if (code !== 0) {
366
+ resolvePromise(undefined);
367
+ return;
368
+ }
369
+ const prefix = out.trim();
370
+ resolvePromise(prefix === "" ? undefined : prefix);
371
+ });
372
+ });
373
+ }
374
+
375
+ function whichOnPath(commandName: string): string | undefined {
376
+ const pathEnv = process.env.PATH ?? "";
377
+ const sep = process.platform === "win32" ? ";" : ":";
378
+ const exts =
379
+ process.platform === "win32"
380
+ ? (process.env.PATHEXT ?? ".EXE;.CMD;.BAT").split(";").filter(Boolean)
381
+ : [""];
382
+ for (const dir of pathEnv.split(sep)) {
383
+ if (dir === "") {
384
+ continue;
385
+ }
386
+ for (const ext of exts) {
387
+ const candidate = join(dir, `${commandName}${ext}`);
388
+ if (existsSync(candidate)) {
389
+ return candidate;
390
+ }
391
+ }
392
+ }
393
+ return undefined;
394
+ }
395
+
259
396
  async function defaultNpmInstallRunner(args: {
260
397
  command: string;
261
398
  args: string[];
@@ -323,6 +460,11 @@ export interface InitNextStepsOptions {
323
460
  * `login` (used for `--json` and when an interactive login attempt failed).
324
461
  */
325
462
  signedIn: boolean;
463
+ /**
464
+ * True when `agent-sdk` resolves on PATH after init linked the CLI.
465
+ * When false, next steps include a PATH export for `node_modules/.bin`.
466
+ */
467
+ cliOnPath: boolean;
326
468
  /** Override TTY color detection (tests / `--json` callers). */
327
469
  color?: boolean;
328
470
  }
@@ -415,6 +557,12 @@ function initNextStepEntries(options: InitNextStepsOptions): Array<{
415
557
  if (cd !== undefined) {
416
558
  steps.push({ command: cd });
417
559
  }
560
+ if (!options.cliOnPath) {
561
+ steps.push({
562
+ command: 'export PATH="$PWD/node_modules/.bin:$PATH"',
563
+ note: "use the project-local agent-sdk bin",
564
+ });
565
+ }
418
566
  if (!options.signedIn) {
419
567
  steps.push({
420
568
  command: `${CLI_COMMAND_NAME} login`,
@@ -436,6 +584,7 @@ export function initNextCommands(options: {
436
584
  rootDir: string;
437
585
  cwd: string;
438
586
  signedIn: boolean;
587
+ cliOnPath: boolean;
439
588
  }): string[] {
440
589
  return initNextStepEntries(options).map((step) => step.command);
441
590
  }
@@ -448,10 +597,10 @@ export function printInitGuide(): void {
448
597
  process.stdout.write(`${CLI_COMMAND_NAME} — coding agent setup
449
598
 
450
599
  Scaffold a project, then open the playground:
451
- ${CLI_COMMAND_NAME} init ./my-agent # installs deps; signs you in when needed
600
+ npx ${PACKAGE_NAME} init ./my-agent # installs deps + puts agent-sdk on PATH
452
601
  cd ./my-agent && ${CLI_COMMAND_NAME} dev
453
602
 
454
- Recommended loop once the project exists:
603
+ Recommended loop once the project exists (from the project directory):
455
604
  1. Edit agent/ + tools under agent/tools/
456
605
  2. ${CLI_COMMAND_NAME} info --dir . --json
457
606
  3. ${CLI_COMMAND_NAME} run --dir . --message "..."
@@ -0,0 +1,240 @@
1
+ /**
2
+ * Ledger-mode serving seam (`serving: "ledger"` on `defineAgent`):
3
+ * coordination only, no inbox. Every engine `send` acquires the session
4
+ * lease before its turn runs and commits (head+1, lease released) after
5
+ * it completes, so cross-pod mutual exclusion, generation fencing, and
6
+ * the committed head sequence are real.
7
+ *
8
+ * **Durability trade-off (deliberate):** there is no durable mailbox.
9
+ * An event that arrives while the lease is held does NOT park in the
10
+ * store — the delivering pod waits *in-process*: local concurrent sends
11
+ * serialize on a per-session promise chain (no claim spinning), and a
12
+ * lease held by another pod is waited out with bounded claim retries. A
13
+ * crash therefore loses the in-flight event exactly as resident serving
14
+ * does. A dead pod's lease simply expires (~30s TTL); the next delivery
15
+ * claims at generation+1 — no janitor is needed because nothing is
16
+ * parked.
17
+ *
18
+ * Re-entrancy caveat (same shape as resident `waitForCompletion`): a
19
+ * send issued from *inside* a running turn against the same session
20
+ * chains behind that turn's serve, so awaiting its resolution from the
21
+ * turn would deadlock. Fire-and-forget such sends.
22
+ */
23
+
24
+ import { randomBytes, randomUUID } from "node:crypto";
25
+ import type { SessionLedger } from "../ledger/session-ledger.js";
26
+ import {
27
+ HEARTBEAT_INTERVAL_MS,
28
+ LEASE_TTL_MS,
29
+ LedgerFencedError,
30
+ } from "../ledger/session-ledger.js";
31
+ import type { ChannelSession, SendMessageOptions } from "../types.js";
32
+ import { newContinuationKey } from "./ids.js";
33
+
34
+ /** Wait between claim retries while another holder has the lease. */
35
+ export const CLAIM_RETRY_DELAY_MS = 250;
36
+ /**
37
+ * Give up waiting for the lease after this long — two full lease TTLs,
38
+ * so a dead holder's expiry (~30s) is always survived with margin.
39
+ */
40
+ export const CLAIM_WAIT_TIMEOUT_MS: number = 2 * LEASE_TTL_MS;
41
+
42
+ /** The `send` options a ledger delivery carries to the resident turn. */
43
+ export type LedgerDeliverOptions = SendMessageOptions & {
44
+ mode?: "chat" | "task";
45
+ };
46
+
47
+ /** One delivery handed back to the engine's resident path. */
48
+ export interface LedgerTurnRequest {
49
+ channelId: string;
50
+ continuationToken: string;
51
+ message: string;
52
+ options: LedgerDeliverOptions;
53
+ }
54
+
55
+ export interface LedgerServingOptions {
56
+ ledger: SessionLedger;
57
+ /**
58
+ * Run one delivery as a resident-path turn (the engine's `send` with
59
+ * the internal ledger-delivery flag). Must resolve once the turn is
60
+ * admitted; the pump awaits `waitForCompletion` itself.
61
+ */
62
+ deliverTurn: (request: LedgerTurnRequest) => Promise<ChannelSession>;
63
+ logger?: (line: string) => void;
64
+ /** Lease heartbeat cadence while the turn runs (default 10s). */
65
+ heartbeatIntervalMs?: number;
66
+ /** Claim retry cadence while the lease is held (default 250ms). */
67
+ claimRetryDelayMs?: number;
68
+ /** Give-up bound for lease waiting (default 2× lease TTL). */
69
+ claimWaitTimeoutMs?: number;
70
+ clock?: () => number;
71
+ /** Injectable wait (tests use short real delays; keep them tiny). */
72
+ sleep?: (ms: number) => Promise<void>;
73
+ }
74
+
75
+ export class LedgerServing {
76
+ private readonly ledger: SessionLedger;
77
+ private readonly deliverTurn: LedgerServingOptions["deliverTurn"];
78
+ private readonly logger: (line: string) => void;
79
+ private readonly heartbeatIntervalMs: number;
80
+ private readonly claimRetryDelayMs: number;
81
+ private readonly claimWaitTimeoutMs: number;
82
+ private readonly clock: () => number;
83
+ private readonly sleep: (ms: number) => Promise<void>;
84
+ /**
85
+ * Per-ledger-session in-process serialization: local concurrent sends
86
+ * queue here instead of spinning claim retries against our own lease.
87
+ */
88
+ private readonly chains = new Map<string, Promise<unknown>>();
89
+
90
+ constructor(options: LedgerServingOptions) {
91
+ this.ledger = options.ledger;
92
+ this.deliverTurn = options.deliverTurn;
93
+ this.logger = options.logger ?? (() => {});
94
+ this.heartbeatIntervalMs =
95
+ options.heartbeatIntervalMs ?? HEARTBEAT_INTERVAL_MS;
96
+ this.claimRetryDelayMs = options.claimRetryDelayMs ?? CLAIM_RETRY_DELAY_MS;
97
+ this.claimWaitTimeoutMs =
98
+ options.claimWaitTimeoutMs ?? CLAIM_WAIT_TIMEOUT_MS;
99
+ this.clock = options.clock ?? Date.now;
100
+ this.sleep =
101
+ options.sleep ??
102
+ ((ms) => new Promise((resolve) => setTimeout(resolve, ms)));
103
+ }
104
+
105
+ /** Settle every queued and in-flight delivery. */
106
+ async close(): Promise<void> {
107
+ await Promise.allSettled([...this.chains.values()]);
108
+ }
109
+
110
+ /**
111
+ * Ledger-mode `send`: resolve the session identity, wait for the
112
+ * lease (in-process queue first, then cross-pod claim retries), run
113
+ * the turn, commit. Resolves with the engine session handle after the
114
+ * commit released the lease.
115
+ */
116
+ async deliver(
117
+ channelId: string,
118
+ message: string,
119
+ options: LedgerDeliverOptions
120
+ ): Promise<ChannelSession> {
121
+ const continuationToken = options.continuationToken ?? newContinuationKey();
122
+ const ensured = await this.ledger.ensureSession(
123
+ `${channelId}/${continuationToken}`
124
+ );
125
+ return this.enqueue(ensured.session.sessionId, () =>
126
+ this.serveOne(ensured.session.sessionId, {
127
+ channelId,
128
+ continuationToken,
129
+ message,
130
+ options,
131
+ })
132
+ );
133
+ }
134
+
135
+ /** Chain a delivery behind the session's in-flight ones. */
136
+ private enqueue(
137
+ ledgerSessionId: string,
138
+ run: () => Promise<ChannelSession>
139
+ ): Promise<ChannelSession> {
140
+ const prior = this.chains.get(ledgerSessionId) ?? Promise.resolve();
141
+ const next = prior.then(
142
+ () => run(),
143
+ () => run()
144
+ );
145
+ this.chains.set(ledgerSessionId, next);
146
+ void next
147
+ .catch(() => {})
148
+ .finally(() => {
149
+ if (this.chains.get(ledgerSessionId) === next) {
150
+ this.chains.delete(ledgerSessionId);
151
+ }
152
+ });
153
+ return next;
154
+ }
155
+
156
+ /** claim (waiting) → resident turn → commit-release. */
157
+ private async serveOne(
158
+ ledgerSessionId: string,
159
+ request: LedgerTurnRequest
160
+ ): Promise<ChannelSession> {
161
+ const lease = await this.claimWithWait(ledgerSessionId);
162
+ let fenced = false;
163
+ const heartbeat = setInterval(() => {
164
+ this.ledger.heartbeat(ledgerSessionId, lease.generation).then(
165
+ (result) => {
166
+ if (!result.ok) {
167
+ fenced = true;
168
+ }
169
+ },
170
+ () => {
171
+ // Transient KV failure: the next beat retries within the TTL.
172
+ }
173
+ );
174
+ }, this.heartbeatIntervalMs);
175
+ heartbeat.unref?.();
176
+ try {
177
+ let handle: ChannelSession;
178
+ try {
179
+ handle = await this.deliverTurn(request);
180
+ await handle.waitForCompletion();
181
+ } catch (error) {
182
+ // The turn never committed; free the session for the next
183
+ // delivery instead of making it wait out the TTL. A fenced
184
+ // holder's release is a no-op.
185
+ await this.ledger
186
+ .release(ledgerSessionId, lease.generation)
187
+ .catch(() => {});
188
+ throw error;
189
+ }
190
+ if (fenced) {
191
+ // A new holder took over mid-turn (our lease lapsed). Discard:
192
+ // committing would fence anyway, and warm state is now stale.
193
+ this.logger(
194
+ `[ledger] session ${ledgerSessionId} fenced mid-turn (generation ${lease.generation}); discarding without commit`
195
+ );
196
+ throw new LedgerFencedError(
197
+ `deliver(${ledgerSessionId}): fenced mid-turn at generation ${lease.generation}`
198
+ );
199
+ }
200
+ await this.ledger.commit(ledgerSessionId, {
201
+ generation: lease.generation,
202
+ expectedHead: lease.head,
203
+ commitId: randomUUID(),
204
+ });
205
+ return handle;
206
+ } finally {
207
+ clearInterval(heartbeat);
208
+ }
209
+ }
210
+
211
+ /**
212
+ * Claim the lease, waiting out another holder with bounded retries.
213
+ * The common waits are short: our own pod's deliveries never reach
214
+ * here concurrently (the in-process chain serializes them), so a held
215
+ * lease means another pod is serving — usually for one turn — or died
216
+ * (expiry unblocks us at ~30s).
217
+ */
218
+ private async claimWithWait(
219
+ ledgerSessionId: string
220
+ ): Promise<{ generation: number; head: number }> {
221
+ const start = this.clock();
222
+ for (;;) {
223
+ const claim = await this.ledger.claim(ledgerSessionId);
224
+ if (claim.claimed) {
225
+ return { generation: claim.lease.generation, head: claim.head };
226
+ }
227
+ if (this.clock() - start >= this.claimWaitTimeoutMs) {
228
+ throw new Error(
229
+ `ledger serving: session ${ledgerSessionId} is still leased by ${claim.holder} after ${this.claimWaitTimeoutMs}ms; giving up on this delivery`
230
+ );
231
+ }
232
+ await this.sleep(this.claimRetryDelayMs);
233
+ }
234
+ }
235
+ }
236
+
237
+ /** Mint the lease holder id for this process. */
238
+ export function newLedgerPodId(): string {
239
+ return `pod_${randomBytes(6).toString("hex")}`;
240
+ }
@@ -0,0 +1,85 @@
1
+ /**
2
+ * Bind an HTTP server to a port, with a clear next-port path when 3000 (or
3
+ * whatever was requested) is already taken — a common onboarding failure.
4
+ */
5
+
6
+ import { once } from "node:events";
7
+ import type { Server } from "node:http";
8
+ import { CLI_COMMAND_NAME } from "./distribution.js";
9
+
10
+ /** Default listen port for `serve` / `dev` when `--port` is omitted. */
11
+ export const DEFAULT_SERVE_PORT = 3000;
12
+
13
+ /** How many ports above the default to try when the default is taken. */
14
+ const DEFAULT_PORT_FALLBACK_SPAN = 20;
15
+
16
+ export function isAddrInUse(error: unknown): boolean {
17
+ return (
18
+ typeof error === "object" &&
19
+ error !== null &&
20
+ "code" in error &&
21
+ (error as { code?: unknown }).code === "EADDRINUSE"
22
+ );
23
+ }
24
+
25
+ export function formatPortInUseHint(port: number): string {
26
+ const next = port === 0 ? 3001 : port + 1;
27
+ return (
28
+ `port ${port} is already in use. ` +
29
+ `Try \`${CLI_COMMAND_NAME} dev --port ${next}\` or \`--port 0\` for any free port.`
30
+ );
31
+ }
32
+
33
+ /**
34
+ * `server.listen` with fallback when the caller used the default port.
35
+ * Explicit `--port N` (including `0`) fails immediately with a next-port hint.
36
+ */
37
+ export async function listenHttpServer(args: {
38
+ server: Server;
39
+ host: string;
40
+ port: number;
41
+ /** True when the caller passed `port` (including 0); skip fallback. */
42
+ portWasExplicit: boolean;
43
+ logger: (line: string) => void;
44
+ }): Promise<number> {
45
+ const { server, host, logger } = args;
46
+ const startPort = args.port;
47
+ if (args.portWasExplicit || startPort === 0) {
48
+ try {
49
+ server.listen(startPort, host);
50
+ await once(server, "listening");
51
+ } catch (error) {
52
+ if (isAddrInUse(error)) {
53
+ throw new Error(formatPortInUseHint(startPort));
54
+ }
55
+ throw error;
56
+ }
57
+ const address = server.address();
58
+ if (address === null || typeof address === "string") {
59
+ throw new Error("server listening without an address");
60
+ }
61
+ return address.port;
62
+ }
63
+
64
+ const last = startPort + DEFAULT_PORT_FALLBACK_SPAN;
65
+ for (let port = startPort; port <= last; port++) {
66
+ try {
67
+ server.listen(port, host);
68
+ await once(server, "listening");
69
+ if (port !== startPort) {
70
+ logger(
71
+ `[agent-sdk] port ${startPort} in use; listening on ${port} instead (pass --port to pick)`
72
+ );
73
+ }
74
+ return port;
75
+ } catch (error) {
76
+ if (!isAddrInUse(error)) {
77
+ throw error;
78
+ }
79
+ if (port === last) {
80
+ throw new Error(formatPortInUseHint(port));
81
+ }
82
+ }
83
+ }
84
+ throw new Error(formatPortInUseHint(last));
85
+ }
@@ -385,14 +385,28 @@ export type OAuthCallbackResult = {
385
385
  state?: string;
386
386
  };
387
387
 
388
- export async function waitForMcpOAuthCallback(args?: {
388
+ /**
389
+ * Deliberately not `@anysphere/mcp-core/oauth/loopback`, which is the canonical
390
+ * implementation every other Cursor surface shares. `@cursor/july` publishes to
391
+ * public npm with no private workspace dependencies, and its OAuth state is a
392
+ * bare UUID rather than the payload that module's validator expects.
393
+ *
394
+ * Nothing here reads Statsig, so this listener is outside
395
+ * `unified_mcp_loopback`: turning that gate off restores the other surfaces'
396
+ * original servers but leaves this one, and its state check, in place.
397
+ */
398
+ export async function waitForMcpOAuthCallback(args: {
399
+ authorizationUrl: string;
389
400
  port?: number;
390
401
  path?: string;
391
402
  timeoutMs?: number;
392
403
  }): Promise<OAuthCallbackResult> {
393
- const port = args?.port ?? MCP_OAUTH_CALLBACK_PORT;
394
- const path = args?.path ?? MCP_OAUTH_CALLBACK_PATH;
395
- const timeoutMs = args?.timeoutMs ?? 5 * 60 * 1000;
404
+ const port = args.port ?? MCP_OAUTH_CALLBACK_PORT;
405
+ const path = args.path ?? MCP_OAUTH_CALLBACK_PATH;
406
+ const timeoutMs = args.timeoutMs ?? 5 * 60 * 1000;
407
+ const expectedState = new URL(args.authorizationUrl).searchParams.get(
408
+ "state"
409
+ );
396
410
 
397
411
  return new Promise((resolve, reject) => {
398
412
  let done = false;
@@ -428,6 +442,17 @@ export async function waitForMcpOAuthCallback(args?: {
428
442
  const code = url.searchParams.get("code");
429
443
  const oauthError = url.searchParams.get("error");
430
444
  const state = url.searchParams.get("state") ?? undefined;
445
+ // Without this, any page that can reach the loopback port during a login
446
+ // can inject its own authorization code into the exchange. Ignore what is
447
+ // not ours rather than failing the login: treating a stray request as
448
+ // fatal would let any page cancel a login, and free the port for someone
449
+ // else to bind before the real callback arrives.
450
+ if (state !== (expectedState ?? undefined)) {
451
+ res
452
+ .writeHead(404, { "content-type": "text/html; charset=utf-8" })
453
+ .end("<!doctype html><html><body><h1>Not found</h1></body></html>");
454
+ return;
455
+ }
431
456
  if (code) {
432
457
  res
433
458
  .writeHead(200, { "content-type": "text/html; charset=utf-8" })