@cursor/july 0.1.62 → 0.1.68

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 (180) hide show
  1. package/dist/bin/agent-serve.js +8 -0
  2. package/dist/channels/slack/channel-watch.d.ts +2 -1
  3. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  4. package/dist/channels/slack/channel-watch.js +7 -3
  5. package/dist/channels/slack/dispatch.d.ts +9 -0
  6. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  7. package/dist/channels/slack/dispatch.js +41 -21
  8. package/dist/channels/slack/manifest.d.ts.map +1 -1
  9. package/dist/channels/slack/manifest.js +1 -0
  10. package/dist/channels/slack/nudge-store.d.ts +16 -0
  11. package/dist/channels/slack/nudge-store.d.ts.map +1 -1
  12. package/dist/channels/slack/nudge-store.js +53 -0
  13. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  14. package/dist/channels/slack/slack-channel.js +4 -1
  15. package/dist/channels/slack/types.d.ts +22 -5
  16. package/dist/channels/slack/types.d.ts.map +1 -1
  17. package/dist/docs/404.html +2 -2
  18. package/dist/docs/ab.html +3 -3
  19. package/dist/docs/assets/{app.B-biogkN.js → app.C5XUTCw9.js} +1 -1
  20. package/dist/docs/assets/chunks/@localSearchIndexroot.DvUR3lv9.js +1 -0
  21. package/dist/docs/assets/chunks/{VPLocalSearchBox.DZqKtFZS.js → VPLocalSearchBox.CGDTAYHY.js} +1 -1
  22. package/dist/docs/assets/chunks/{theme.BwC5zP2r.js → theme.lPixh250.js} +2 -2
  23. package/dist/docs/assets/{reference_connections.md.CSQiNzre.js → reference_connections.md.Bwswbm6H.js} +13 -3
  24. package/dist/docs/assets/{reference_connections.md.CSQiNzre.lean.js → reference_connections.md.Bwswbm6H.lean.js} +1 -1
  25. package/dist/docs/assets/{reference_tools.md.kJfJAP2m.js → reference_tools.md.Wmvb7Lob.js} +2 -2
  26. package/dist/docs/assets/{reference_tools.md.kJfJAP2m.lean.js → reference_tools.md.Wmvb7Lob.lean.js} +1 -1
  27. package/dist/docs/building-with-agents.html +3 -3
  28. package/dist/docs/concepts.html +3 -3
  29. package/dist/docs/deployment.html +3 -3
  30. package/dist/docs/evals.html +3 -3
  31. package/dist/docs/example-agents/approval-buddy.html +3 -3
  32. package/dist/docs/example-agents/benny.html +3 -3
  33. package/dist/docs/example-agents/bugbot.html +3 -3
  34. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  35. package/dist/docs/example-agents/codeowners-review.html +3 -3
  36. package/dist/docs/example-agents/concierge.html +3 -3
  37. package/dist/docs/example-agents/fsd.html +3 -3
  38. package/dist/docs/example-agents/index.html +3 -3
  39. package/dist/docs/example-agents/knowledge-base.html +3 -3
  40. package/dist/docs/example-agents/oncall.html +3 -3
  41. package/dist/docs/example-agents/security-reviewer.html +3 -3
  42. package/dist/docs/example-agents/slack-agent.html +3 -3
  43. package/dist/docs/example-agents/weather-agent.html +3 -3
  44. package/dist/docs/guides/agent-to-agent.html +3 -3
  45. package/dist/docs/guides/cloud-runtime.html +3 -3
  46. package/dist/docs/guides/convert-automation.html +3 -3
  47. package/dist/docs/guides/github.html +3 -3
  48. package/dist/docs/guides/human-in-the-loop.html +3 -3
  49. package/dist/docs/guides/mcp-oauth.html +3 -3
  50. package/dist/docs/guides/opentelemetry.html +3 -3
  51. package/dist/docs/guides/slack.html +3 -3
  52. package/dist/docs/guides/webhooks.html +3 -3
  53. package/dist/docs/hashmap.json +1 -1
  54. package/dist/docs/hillclimbing.html +3 -3
  55. package/dist/docs/index.html +3 -3
  56. package/dist/docs/quickstart.html +3 -3
  57. package/dist/docs/reference/agent-config.html +3 -3
  58. package/dist/docs/reference/artifacts.html +3 -3
  59. package/dist/docs/reference/channels.html +3 -3
  60. package/dist/docs/reference/cli.html +3 -3
  61. package/dist/docs/reference/connections.html +15 -5
  62. package/dist/docs/reference/hooks.html +3 -3
  63. package/dist/docs/reference/http-api.html +3 -3
  64. package/dist/docs/reference/instructions.html +3 -3
  65. package/dist/docs/reference/playground.html +3 -3
  66. package/dist/docs/reference/project-layout.html +3 -3
  67. package/dist/docs/reference/prompt.html +3 -3
  68. package/dist/docs/reference/schedules.html +3 -3
  69. package/dist/docs/reference/sessions.html +3 -3
  70. package/dist/docs/reference/skills.html +3 -3
  71. package/dist/docs/reference/subagents.html +3 -3
  72. package/dist/docs/reference/tools.html +5 -5
  73. package/dist/docs/scaffolding-agents.html +3 -3
  74. package/dist/docs/storage.html +3 -3
  75. package/dist/docs/templates/agentic-owners.html +3 -3
  76. package/dist/docs/templates/demo.html +3 -3
  77. package/dist/docs/templates/pr-autofixer.html +3 -3
  78. package/dist/docs/templates/security-reviewer.html +3 -3
  79. package/dist/docs/templates/triage.html +3 -3
  80. package/dist/docs/troubleshooting.html +3 -3
  81. package/dist/files-backends/agent-store-presigned-url.js +1 -1
  82. package/dist/index.d.ts +2 -2
  83. package/dist/index.d.ts.map +1 -1
  84. package/dist/index.js +1 -1
  85. package/dist/internal/advertise-tools.d.ts +74 -0
  86. package/dist/internal/advertise-tools.d.ts.map +1 -0
  87. package/dist/internal/advertise-tools.js +152 -0
  88. package/dist/internal/cli-deploy.d.ts.map +1 -1
  89. package/dist/internal/cli-deploy.js +11 -3
  90. package/dist/internal/cli-manifest.d.ts +19 -0
  91. package/dist/internal/cli-manifest.d.ts.map +1 -0
  92. package/dist/internal/cli-manifest.js +49 -0
  93. package/dist/internal/cli-slack.d.ts.map +1 -1
  94. package/dist/internal/cli-slack.js +9 -1
  95. package/dist/internal/cursor-agent-template.d.ts +9 -0
  96. package/dist/internal/cursor-agent-template.d.ts.map +1 -0
  97. package/dist/internal/cursor-agent-template.js +37 -0
  98. package/dist/internal/deploy-client.d.ts +8 -0
  99. package/dist/internal/deploy-client.d.ts.map +1 -1
  100. package/dist/internal/deploy-client.js +21 -3
  101. package/dist/internal/deploy-manifest.d.ts +85 -0
  102. package/dist/internal/deploy-manifest.d.ts.map +1 -0
  103. package/dist/internal/deploy-manifest.js +106 -0
  104. package/dist/internal/deployment-scm-event-stream.d.ts +113 -0
  105. package/dist/internal/deployment-scm-event-stream.d.ts.map +1 -0
  106. package/dist/internal/deployment-scm-event-stream.js +460 -0
  107. package/dist/internal/discovery.d.ts.map +1 -1
  108. package/dist/internal/discovery.js +69 -0
  109. package/dist/internal/init-project.d.ts.map +1 -1
  110. package/dist/internal/init-project.js +5 -17
  111. package/dist/internal/mcp-host.d.ts +18 -0
  112. package/dist/internal/mcp-host.d.ts.map +1 -1
  113. package/dist/internal/mcp-host.js +41 -5
  114. package/dist/internal/sdk-runner.d.ts +1 -1
  115. package/dist/internal/sdk-runner.d.ts.map +1 -1
  116. package/dist/internal/sdk-runner.js +36 -13
  117. package/dist/internal/server.d.ts.map +1 -1
  118. package/dist/internal/server.js +18 -0
  119. package/dist/internal/session-engine.d.ts +66 -0
  120. package/dist/internal/session-engine.d.ts.map +1 -1
  121. package/dist/internal/session-engine.js +430 -110
  122. package/dist/internal/shallow-clone.d.ts +115 -0
  123. package/dist/internal/shallow-clone.d.ts.map +1 -0
  124. package/dist/internal/shallow-clone.js +292 -0
  125. package/dist/internal/slack-provision-client.d.ts +2 -0
  126. package/dist/internal/slack-provision-client.d.ts.map +1 -1
  127. package/dist/internal/slack-provision-client.js +1 -0
  128. package/dist/internal/turn-lease-store.d.ts +44 -0
  129. package/dist/internal/turn-lease-store.d.ts.map +1 -0
  130. package/dist/internal/turn-lease-store.js +139 -0
  131. package/dist/internal/workspace.d.ts +12 -0
  132. package/dist/internal/workspace.d.ts.map +1 -1
  133. package/dist/internal/workspace.js +28 -0
  134. package/dist/playground/assets/index-CoL7xCJl.css +1 -0
  135. package/dist/playground/assets/index-yxs1heea.js +85 -0
  136. package/dist/playground/index.html +2 -2
  137. package/dist/storage-protocol.d.ts +122 -0
  138. package/dist/storage-protocol.d.ts.map +1 -0
  139. package/dist/storage-protocol.js +173 -0
  140. package/dist/storage.d.ts +2 -72
  141. package/dist/storage.d.ts.map +1 -1
  142. package/dist/storage.js +2 -82
  143. package/dist/types.d.ts +113 -3
  144. package/dist/types.d.ts.map +1 -1
  145. package/docs/reference/connections.md +80 -0
  146. package/docs/reference/tools.md +14 -0
  147. package/package.json +4 -2
  148. package/src/bin/agent-serve.ts +9 -0
  149. package/src/channels/slack/channel-watch.ts +6 -3
  150. package/src/channels/slack/dispatch.ts +40 -3
  151. package/src/channels/slack/manifest.ts +1 -0
  152. package/src/channels/slack/nudge-store.ts +56 -0
  153. package/src/channels/slack/slack-channel.ts +3 -0
  154. package/src/channels/slack/types.ts +22 -5
  155. package/src/index.ts +5 -0
  156. package/src/internal/advertise-tools.ts +229 -0
  157. package/src/internal/cli-deploy.ts +22 -1
  158. package/src/internal/cli-manifest.ts +63 -0
  159. package/src/internal/cli-slack.ts +10 -1
  160. package/src/internal/cursor-agent-template.ts +46 -0
  161. package/src/internal/deploy-client.ts +35 -6
  162. package/src/internal/deploy-manifest.ts +167 -0
  163. package/src/internal/deployment-scm-event-stream.ts +577 -0
  164. package/src/internal/discovery.ts +85 -0
  165. package/src/internal/init-project.ts +11 -20
  166. package/src/internal/mcp-host.ts +66 -9
  167. package/src/internal/sdk-runner.ts +50 -11
  168. package/src/internal/server.ts +19 -0
  169. package/src/internal/session-engine.ts +385 -19
  170. package/src/internal/shallow-clone.ts +407 -0
  171. package/src/internal/slack-provision-client.ts +3 -0
  172. package/src/internal/turn-lease-store.ts +147 -0
  173. package/src/internal/workspace.ts +27 -0
  174. package/src/storage-protocol.ts +247 -0
  175. package/src/storage.ts +16 -110
  176. package/src/types.ts +117 -3
  177. package/templates/pr-autofixer/agent/lib/drive-schemas.ts +4 -1
  178. package/dist/docs/assets/chunks/@localSearchIndexroot.5ozYNf74.js +0 -1
  179. package/dist/playground/assets/index-B4rcOgeP.css +0 -1
  180. package/dist/playground/assets/index-DXcY42Jv.js +0 -85
@@ -142,6 +142,13 @@ export interface SlackInboundDispatchDeps extends SlackContinuationProbes {
142
142
  * dropped at parse time (loop safety).
143
143
  */
144
144
  botMentionGate?: SlackBotMentionGate;
145
+ /**
146
+ * When false, do not write `assistant.threads.setStatus` on a coalesced
147
+ * follow-up. Defaults to `true`. Channels that report progress their own
148
+ * way turn this off: the chip has no owner to clear it once the default
149
+ * `message.completed` handler is replaced, so it lingers past the turn.
150
+ */
151
+ followUpStatus?: boolean;
145
152
  /**
146
153
  * When false, skip the built-in `eval` directive. Defaults to true when
147
154
  * `host.evals` is present.
@@ -195,6 +202,8 @@ export async function dispatchInboundMessage(args: {
195
202
  log?: SlackLog;
196
203
  eventId?: string;
197
204
  evalCommandsEnabled?: boolean;
205
+ /** See {@link SlackInboundDispatchDeps.followUpStatus}. Defaults to `true`. */
206
+ followUpStatus?: boolean;
198
207
  /**
199
208
  * cursorAccount agent display name — stripped before parsing `eval …`
200
209
  * (`OncallBuddy eval list`).
@@ -477,8 +486,9 @@ export async function dispatchInboundMessage(args: {
477
486
  event_id: args.eventId,
478
487
  }
479
488
  );
480
- // Coalesce admits leave the active turn running; follow-ups drain into one
481
- // boundary turn when it finishes (no mid-turn tool/hook inject).
489
+ // Coalesce admits leave the active turn running: the follow-up is queued
490
+ // for the carrier to inject at the next tool boundary, and anything it
491
+ // does not reach in time drains into the boundary turn instead.
482
492
  if (session.coalesced === true) {
483
493
  log.info("session busy; coalescing follow-up", {
484
494
  kind,
@@ -487,8 +497,32 @@ export async function dispatchInboundMessage(args: {
487
497
  continuation,
488
498
  event_id: args.eventId,
489
499
  });
500
+ // Queue it for mid-turn injection too. The carrier drains this at the
501
+ // running turn's next tool boundary; whatever it does not reach in time
502
+ // still drains into the boundary turn, deduped by sourceTs.
503
+ try {
504
+ await args.host.slackNudges?.append({
505
+ continuationKey: continuation,
506
+ sourceTs: message.ts,
507
+ text: media.userText,
508
+ ...(message.author?.userName === undefined
509
+ ? {}
510
+ : { author: message.author.userName }),
511
+ });
512
+ } catch (nudgeError) {
513
+ log.warn("nudge append failed", {
514
+ error:
515
+ nudgeError instanceof Error
516
+ ? nudgeError.message
517
+ : String(nudgeError),
518
+ continuation,
519
+ event_id: args.eventId,
520
+ });
521
+ }
490
522
  try {
491
- await binding.thread.startTyping(FOLLOWUP_TYPING);
523
+ if (args.followUpStatus !== false) {
524
+ await binding.thread.startTyping(FOLLOWUP_TYPING);
525
+ }
492
526
  } catch (typingError) {
493
527
  log.error(
494
528
  `follow-up typing failed: ${
@@ -581,6 +615,7 @@ export function dispatchInboundFromEventBody(
581
615
  log,
582
616
  evalCommandsEnabled: deps.evalCommandsEnabled,
583
617
  cursorAgentName: deps.cursorAgentName,
618
+ followUpStatus: deps.followUpStatus,
584
619
  blockExternals: deps.blockExternals,
585
620
  ...(deps.hasContinuationSession === undefined
586
621
  ? {}
@@ -623,6 +658,7 @@ export function dispatchInboundFromEventBody(
623
658
  log,
624
659
  evalCommandsEnabled: deps.evalCommandsEnabled,
625
660
  cursorAgentName: deps.cursorAgentName,
661
+ followUpStatus: deps.followUpStatus,
626
662
  blockExternals: deps.blockExternals,
627
663
  ...(deps.hasContinuationSession === undefined
628
664
  ? {}
@@ -659,6 +695,7 @@ export function dispatchInboundFromEventBody(
659
695
  log,
660
696
  evalCommandsEnabled: false,
661
697
  cursorAgentName: deps.cursorAgentName,
698
+ followUpStatus: deps.followUpStatus,
662
699
  blockExternals: deps.blockExternals,
663
700
  ...(deps.hasContinuationSession === undefined
664
701
  ? {}
@@ -69,6 +69,7 @@ const BOT_SCOPES = [
69
69
  "channels:history",
70
70
  "chat:write",
71
71
  "files:read",
72
+ "files:write",
72
73
  "groups:history",
73
74
  "im:history",
74
75
  "im:read",
@@ -202,6 +202,24 @@ export class SlackNudgeStore {
202
202
  return (await this.listPending(continuationKey)).length > 0;
203
203
  }
204
204
 
205
+ /**
206
+ * How many follow-ups have reached the agent on this thread. Monotonic
207
+ * within a continuation, so a caller can spot a delivery by comparing two
208
+ * readings — unlike {@link hasUndelivered}, which races the carrier: both
209
+ * run at the same tool boundary, and a queued nudge drained before the
210
+ * first reading never looks pending at all.
211
+ */
212
+ async deliveredCount(continuationKey: string): Promise<number> {
213
+ if (continuationKey.trim() === "") {
214
+ return 0;
215
+ }
216
+ try {
217
+ return (await readdir(this.deliveredDir(continuationKey))).length;
218
+ } catch {
219
+ return 0;
220
+ }
221
+ }
222
+
205
223
  /**
206
224
  * Atomically move all pending nudges to delivered and return them.
207
225
  * Already-delivered source ts values are dropped (exactly-once).
@@ -238,6 +256,44 @@ export class SlackNudgeStore {
238
256
  return delivered;
239
257
  }
240
258
 
259
+ /**
260
+ * Settle a follow-up the caller is handling itself, so the carrier cannot
261
+ * inject it again. Used by the boundary drain: a follow-up the carrier did
262
+ * not reach becomes the next turn's prompt, and without this its pending
263
+ * record survives into that turn and gets injected on top of the ask it
264
+ * already is.
265
+ */
266
+ async markDelivered(
267
+ continuationKey: string,
268
+ sourceTs: string
269
+ ): Promise<void> {
270
+ const key = continuationKey.trim();
271
+ const ts = sourceTs.trim();
272
+ if (key === "" || ts === "") {
273
+ return;
274
+ }
275
+ const pending = await this.listPending(key);
276
+ const record = pending.find((item) => item.sourceTs === ts);
277
+ await mkdir(this.deliveredDir(key), { recursive: true });
278
+ const dest = this.deliveredPath(key, ts);
279
+ const tmp = `${dest}.${process.pid}.tmp`;
280
+ try {
281
+ await writeFile(
282
+ tmp,
283
+ `${JSON.stringify(record ?? { continuationKey: key, sourceTs: ts })}\n`,
284
+ "utf8"
285
+ );
286
+ await rename(tmp, dest);
287
+ } catch {
288
+ await rm(tmp, { force: true }).catch(() => {});
289
+ }
290
+ if (record !== undefined) {
291
+ await rm(join(this.pendingDir(key), `${record.id}.json`), {
292
+ force: true,
293
+ }).catch(() => {});
294
+ }
295
+ }
296
+
241
297
  async clear(continuationKey: string): Promise<void> {
242
298
  await rm(this.continuationDir(continuationKey), {
243
299
  recursive: true,
@@ -219,6 +219,7 @@ export function slackChannel<const Name extends string = string>(
219
219
  const mentionsEnabled = engagement.mentions !== false;
220
220
  const directMessagesEnabled = engagement.directMessages !== false;
221
221
  const evalCommandsEnabled = options.evalCommands !== false;
222
+ const followUpStatus = options.followUpStatus !== false;
222
223
  const channelPosts = engagement.channelPosts;
223
224
  if (channelPosts !== undefined) {
224
225
  const entries = channelPosts.allow.filter((entry) => entry.trim() !== "");
@@ -296,6 +297,7 @@ export function slackChannel<const Name extends string = string>(
296
297
  mentionsEnabled,
297
298
  directMessagesEnabled,
298
299
  evalCommandsEnabled,
300
+ followUpStatus,
299
301
  // cursorAccount: backend router already refuses externals.
300
302
  blockExternals: cursorAccount ? false : blockExternals,
301
303
  ...(args.hasContinuationSession === undefined
@@ -484,6 +486,7 @@ export function slackChannel<const Name extends string = string>(
484
486
  artifacts: args.artifacts,
485
487
  log: getLog(),
486
488
  evalCommandsEnabled,
489
+ followUpStatus,
487
490
  blockExternals,
488
491
  ...(args.hasContinuationSession === undefined
489
492
  ? {}
@@ -330,11 +330,18 @@ export type SlackBlockActionHandler = (
330
330
  */
331
331
  export interface SlackChannelPostsOptions {
332
332
  /**
333
- * Channels to watch: ids (`C…` / `G…`) or `#names`. Must be non-empty
334
- * (empty is a definition error). `#names` are resolved to ids via
335
- * `conversations.list` when the channel starts; the bot must be a member
336
- * of each watched channel to receive its messages. Socket Mode only —
337
- * `cursorAccount: true` rejects `engagement.channelPosts`.
333
+ * Channels to watch: ids (`C…` / `G…`), `#names`, or `"*"` for every
334
+ * channel the bot is in. Must be non-empty (empty is a definition error).
335
+ * `#names` are resolved to ids via `conversations.list` when the channel
336
+ * starts; the bot must be a member of each watched channel to receive its
337
+ * messages. Socket Mode only — `cursorAccount: true` rejects
338
+ * `engagement.channelPosts`.
339
+ *
340
+ * `"*"` suits agents whose handler decides per message (a thread the agent
341
+ * is already in, say) rather than per channel. Every delivered message then
342
+ * reaches {@link SlackChannelOptions.onChannelPost}, and each one costs a
343
+ * `users.info` external-sender check first unless
344
+ * {@link SlackAccessPolicy.blockExternals} is off.
338
345
  */
339
346
  allow: string[];
340
347
  /**
@@ -571,6 +578,16 @@ export interface SlackChannelOptions {
571
578
  * Defaults to `true`.
572
579
  */
573
580
  thinkingSteps?: boolean;
581
+ /**
582
+ * Write Slack's assistant status chip ("Saw your follow-up…") when a
583
+ * follow-up coalesces into a running turn. Defaults to `true`.
584
+ *
585
+ * Set to `false` on channels that report progress themselves: the chip is
586
+ * cleared by the default `message.completed` handler, so a channel that
587
+ * replaces that handler has nothing to take it down and it outlives the
588
+ * turn.
589
+ */
590
+ followUpStatus?: boolean;
574
591
  /**
575
592
  * Opt in to Slack delivery for human-in-the-loop tool approvals.
576
593
  *
package/src/index.ts CHANGED
@@ -132,6 +132,8 @@ export type {
132
132
  export { defineSchedule } from "./schedules.js";
133
133
  export { defineSkill } from "./skills.js";
134
134
  export type {
135
+ DurableSessionRecord,
136
+ DurableSessionSummary,
135
137
  StorageABTable,
136
138
  StorageCasTable,
137
139
  StorageConfig,
@@ -144,8 +146,11 @@ export type {
144
146
  } from "./storage.js";
145
147
  export {
146
148
  defineStorage,
149
+ isDurableSessionRecordKey,
150
+ parseDurableSessionRecord,
147
151
  STORAGE_KEY_ROOT,
148
152
  storageKeys,
153
+ summarizeDurableSessionRecord,
149
154
  } from "./storage.js";
150
155
  export { defineTool } from "./tools.js";
151
156
  export type * from "./types.js";
@@ -0,0 +1,229 @@
1
+ /**
2
+ * Advertised connection tools (`defineConnection({ advertiseTools: true })`).
3
+ *
4
+ * The session engine calls {@link resolveAdvertisedTools} while building
5
+ * every local turn: for each advertising connection it lists the server's
6
+ * tools and synthesizes a named 1:1 passthrough server tool per entry —
7
+ * the server's own names, descriptions, and live schemas — merged into the
8
+ * authored `agent/tools/` catalog. Because it runs at turn-build time it
9
+ * covers every way a turn starts (dispatcher sends, follow-ups, reminder
10
+ * fires that call the engine directly) and survives process restarts,
11
+ * since nothing is parked in memory between turns.
12
+ *
13
+ * Connections with per-session `auth` are listed and called through a
14
+ * one-off client carrying that session's headers (the registry's cached
15
+ * per-name client would leak one session's identity into another);
16
+ * auth-less connections go through the shared `host.mcp` registry, which
17
+ * also covers serve-resolved transports (Cursor account bridges, peers).
18
+ *
19
+ * Everything here fails closed: a listing failure, an invalid tool name,
20
+ * or a name collision throws, and the engine fails the turn rather than
21
+ * running it with a silently shrunken toolset.
22
+ */
23
+
24
+ import type {
25
+ DiscoveredConnection,
26
+ DiscoveredTool,
27
+ HostMcpCallResult,
28
+ HostMcpToolInfo,
29
+ JsonSchemaObject,
30
+ ResolvedAgent,
31
+ SessionInfo,
32
+ ToolDefinition,
33
+ } from "../types.js";
34
+ import { brandDefinition } from "./brand.js";
35
+ import type { HostMcpOneOff } from "./mcp-host.js";
36
+
37
+ /**
38
+ * What the resolver needs from the host MCP registry: the shared cached
39
+ * clients for auth-less connections, and one-off clients (per-session
40
+ * headers, never cached) for auth'd ones. `McpHost` implements this.
41
+ */
42
+ export interface AdvertiseHostMcp {
43
+ listTools(connectionName: string): Promise<HostMcpToolInfo[]>;
44
+ callTool(
45
+ connectionName: string,
46
+ toolName: string,
47
+ args?: Record<string, unknown>
48
+ ): Promise<HostMcpCallResult>;
49
+ oneOff(connection: DiscoveredConnection): HostMcpOneOff;
50
+ }
51
+
52
+ /**
53
+ * The rule advertised tool names must match — the same shape discovery
54
+ * enforces for authored tool filenames (its `NAME_PATTERN`), since both
55
+ * become the tool name the model sees.
56
+ */
57
+ export const ADVERTISED_TOOL_NAME_PATTERN: RegExp =
58
+ /^[A-Za-z0-9][A-Za-z0-9_-]*$/;
59
+
60
+ const ANY_OBJECT_SCHEMA: JsonSchemaObject = {
61
+ type: "object",
62
+ additionalProperties: true,
63
+ };
64
+
65
+ /** How one advertising connection is listed and called. */
66
+ export interface AdvertisedToolSource {
67
+ listTools(): Promise<HostMcpToolInfo[]>;
68
+ callTool(
69
+ toolName: string,
70
+ args: Record<string, unknown>
71
+ ): Promise<HostMcpCallResult>;
72
+ }
73
+
74
+ /**
75
+ * Resolve the advertised tools for one session. Returns `[]` when no
76
+ * connection advertises; throws (failing the turn) when a listing fails
77
+ * or a definition is invalid.
78
+ */
79
+ export async function resolveAdvertisedTools(args: {
80
+ agent: ResolvedAgent;
81
+ session: SessionInfo;
82
+ hostMcp: AdvertiseHostMcp;
83
+ /** Test seam; defaults to {@link sourceForConnection}. */
84
+ sourceFor?: (
85
+ connection: DiscoveredConnection,
86
+ session: SessionInfo,
87
+ hostMcp: AdvertiseHostMcp
88
+ ) => Promise<AdvertisedToolSource>;
89
+ }): Promise<DiscoveredTool[]> {
90
+ const advertising = args.agent.connections.filter(
91
+ (connection) => connection.advertiseTools === true
92
+ );
93
+ if (advertising.length === 0) {
94
+ return [];
95
+ }
96
+ const sourceFor = args.sourceFor ?? sourceForConnection;
97
+ const claimed = new Set(args.agent.tools.map((tool) => tool.name));
98
+ const resolved: DiscoveredTool[] = [];
99
+ for (const connection of advertising) {
100
+ const source = await sourceFor(connection, args.session, args.hostMcp);
101
+ let listed: HostMcpToolInfo[];
102
+ try {
103
+ listed = await source.listTools();
104
+ } catch (error) {
105
+ throw new Error(
106
+ `Failed to list tools on advertised connection "${connection.name}": ${describeError(error)}`,
107
+ { cause: error }
108
+ );
109
+ }
110
+ for (const info of listed) {
111
+ resolved.push(toDiscoveredTool(connection, info, claimed, source));
112
+ claimed.add(info.name);
113
+ }
114
+ }
115
+ return resolved;
116
+ }
117
+
118
+ /**
119
+ * The production source for one advertising connection: the shared
120
+ * `host.mcp` registry client for auth-less connections (which also covers
121
+ * serve-resolved transports — Cursor account bridges, peers), or a
122
+ * registry one-off with the session's headers substituted when the
123
+ * connection has per-session `auth`. The auth callback is evaluated once
124
+ * per turn, here; the one-off opens a fresh client per operation so one
125
+ * session's identity can never serve another's call.
126
+ */
127
+ export async function sourceForConnection(
128
+ connection: DiscoveredConnection,
129
+ session: SessionInfo,
130
+ hostMcp: AdvertiseHostMcp
131
+ ): Promise<AdvertisedToolSource> {
132
+ if (connection.auth === undefined) {
133
+ return {
134
+ listTools: () => hostMcp.listTools(connection.name),
135
+ callTool: (toolName, callArgs) =>
136
+ hostMcp.callTool(connection.name, toolName, callArgs),
137
+ };
138
+ }
139
+ const transport = connection.transport;
140
+ if (!("url" in transport)) {
141
+ // Discovery rejects auth on non-URL transports; guard for callers that
142
+ // bypass discovery (hand-built projects).
143
+ throw new Error(
144
+ `Connection "${connection.name}" has per-session auth but no URL transport.`
145
+ );
146
+ }
147
+ const evaluated = await connection.auth(session);
148
+ return hostMcp.oneOff({
149
+ ...connection,
150
+ transport: {
151
+ ...transport,
152
+ headers: { ...transport.headers, ...evaluated.headers },
153
+ },
154
+ });
155
+ }
156
+
157
+ function toDiscoveredTool(
158
+ connection: DiscoveredConnection,
159
+ info: HostMcpToolInfo,
160
+ claimed: ReadonlySet<string>,
161
+ source: AdvertisedToolSource
162
+ ): DiscoveredTool {
163
+ if (!ADVERTISED_TOOL_NAME_PATTERN.test(info.name)) {
164
+ throw new Error(
165
+ `Advertised connection "${connection.name}" serves tool name "${info.name}", which must match ${ADVERTISED_TOOL_NAME_PATTERN} (it becomes the tool name the model sees).`
166
+ );
167
+ }
168
+ if (claimed.has(info.name)) {
169
+ throw new Error(
170
+ `Advertised connection "${connection.name}" serves tool "${info.name}", which collides with an authored agent/tools/ tool or another advertised connection.`
171
+ );
172
+ }
173
+ const description =
174
+ info.description === undefined || info.description.trim() === ""
175
+ ? `${info.name} — MCP tool on the "${connection.name}" connection, forwarded 1:1.`
176
+ : info.description;
177
+ const inputSchema = info.inputSchema ?? ANY_OBJECT_SCHEMA;
178
+ const definition: ToolDefinition = brandDefinition("tool", {
179
+ description,
180
+ inputSchema,
181
+ execute: async (input: Record<string, unknown>) =>
182
+ formatMcpToolResult(
183
+ connection.name,
184
+ info.name,
185
+ await source.callTool(info.name, input ?? {})
186
+ ),
187
+ } as unknown as ToolDefinition);
188
+ return {
189
+ name: info.name,
190
+ description,
191
+ inputSchema,
192
+ execution: "server",
193
+ needsApproval: false,
194
+ definition,
195
+ };
196
+ }
197
+
198
+ /**
199
+ * Render an MCP call result for the model: text parts joined, non-text
200
+ * parts noted by type, `isError` surfaced as a thrown error so the turn
201
+ * records a tool error instead of error-shaped prose.
202
+ */
203
+ export function formatMcpToolResult(
204
+ connectionName: string,
205
+ toolName: string,
206
+ result: HostMcpCallResult
207
+ ): string {
208
+ const parts = result.content.map((item) =>
209
+ item.type === "text" && typeof item.text === "string"
210
+ ? item.text
211
+ : `[${item.type} content omitted]`
212
+ );
213
+ const text = parts.join("\n").trim();
214
+ if (result.isError === true) {
215
+ throw new Error(
216
+ text === "" ? `${connectionName} tool ${toolName} failed` : text
217
+ );
218
+ }
219
+ if (text !== "") {
220
+ return text;
221
+ }
222
+ return result.structuredContent !== undefined
223
+ ? JSON.stringify(result.structuredContent, null, 2)
224
+ : "(empty result)";
225
+ }
226
+
227
+ function describeError(error: unknown): string {
228
+ return error instanceof Error ? error.message : String(error);
229
+ }
@@ -22,6 +22,7 @@ import {
22
22
  SIGN_IN_HINT,
23
23
  } from "./cursor/credentials.js";
24
24
  import { authorizeCursorAccountConnectors } from "./cursor-account-mcp-auth.js";
25
+ import { readDeployTemplate } from "./cursor-agent-template.js";
25
26
  import {
26
27
  AgentServeDeployClient,
27
28
  type DeleteAccepted,
@@ -38,6 +39,10 @@ import {
38
39
  TERMINAL_DEPLOY_STATUSES,
39
40
  validateDeploymentSlug,
40
41
  } from "./deploy-client.js";
42
+ import {
43
+ type AgentDeployManifestV1,
44
+ buildAgentDeployManifest,
45
+ } from "./deploy-manifest.js";
41
46
  import { type GitRunner, inferDeployGitSource } from "./deploy-source.js";
42
47
  import {
43
48
  type ChildAgentDir,
@@ -661,11 +666,19 @@ async function deployOne(
661
666
  ...(egressAllowedDomains.length === 0 ? {} : { egressAllowedDomains }),
662
667
  publicRoutePaths:
663
668
  publicRoutePaths.length === 0 ? undefined : publicRoutePaths,
669
+ // Offline metadata extraction (Wd): the CP persists this before any VM
670
+ // starts. Directories without an agent project send no manifest.
671
+ ...(manifest === undefined ? {} : { manifest: manifest.deployManifest }),
664
672
  };
665
673
 
666
674
  let accepted: DeployAccepted;
667
675
  try {
668
- accepted = await ctx.client.deploy({ teamId: ctx.teamId, slug, source });
676
+ accepted = await ctx.client.deploy({
677
+ teamId: ctx.teamId,
678
+ slug,
679
+ source,
680
+ template: readDeployTemplate(dir),
681
+ });
669
682
  } catch (error) {
670
683
  return renderApiError(error, ctx.err);
671
684
  }
@@ -851,6 +864,13 @@ interface DeployManifest {
851
864
  * (only `/v1/channels/…` subtrees can be public); surfaced as a warning.
852
865
  */
853
866
  httpChannelPublicEndpoint: boolean;
867
+ /**
868
+ * The offline deploy manifest (Wd): derived event declarations, channel
869
+ * kinds, egress, routes, and the full `/v1/info` payload. Attached to the
870
+ * deploy request so the control plane stores metadata before any VM
871
+ * starts; servers that predate manifest storage strip the field.
872
+ */
873
+ deployManifest: AgentDeployManifestV1;
854
874
  }
855
875
 
856
876
  /**
@@ -875,6 +895,7 @@ async function readDeployManifest(dir: string): Promise<DeployManifest> {
875
895
  httpChannelPublicEndpoint: (project.httpChannel.auth ?? []).some(
876
896
  isPublicEndpointPolicy
877
897
  ),
898
+ deployManifest: buildAgentDeployManifest(project),
878
899
  };
879
900
  }
880
901
 
@@ -0,0 +1,63 @@
1
+ /**
2
+ * `july manifest` — print the offline deploy manifest (Wd-1 of
3
+ * `docs/agent-serve/agent-sdk-event-ingress-plan.md`).
4
+ *
5
+ * Loads the agent project from disk and emits the statically-derived
6
+ * metadata the control plane stores at deploy time: declared GitHub webhook
7
+ * events, egress domains, public routes, and the full `/v1/info` payload.
8
+ * Runs entirely offline — no VM, no engine, no network.
9
+ */
10
+
11
+ import {
12
+ buildAgentDeployManifest,
13
+ parseAgentDeployManifest,
14
+ } from "./deploy-manifest.js";
15
+ import { isAgentProjectDir, loadAgentProject } from "./discovery.js";
16
+
17
+ export interface CmdManifestOptions {
18
+ dir: string;
19
+ /** Compact single-line JSON (machine consumption); default pretty. */
20
+ json?: boolean;
21
+ /** Test seams. */
22
+ out?: (line: string) => void;
23
+ err?: (line: string) => void;
24
+ }
25
+
26
+ export async function cmdManifest(
27
+ options: CmdManifestOptions
28
+ ): Promise<number> {
29
+ const out = options.out ?? ((line: string) => process.stdout.write(line));
30
+ const err = options.err ?? ((line: string) => process.stderr.write(line));
31
+
32
+ if (!(await isAgentProjectDir(options.dir))) {
33
+ err(
34
+ `No agent project found at ${options.dir} (expected an agent.ts default-exporting defineAgent({ ... })).\n`
35
+ );
36
+ return 1;
37
+ }
38
+
39
+ const project = await loadAgentProject(options.dir);
40
+ for (const diagnostic of project.diagnostics) {
41
+ err(`${diagnostic.severity}: ${diagnostic.message}\n`);
42
+ }
43
+ if (project.diagnostics.some((d) => d.severity === "error")) {
44
+ return 1;
45
+ }
46
+
47
+ const manifest = buildAgentDeployManifest(project);
48
+ // Round-trip through the wire schema so `july manifest` can never emit
49
+ // something the control plane contract would reject.
50
+ if (
51
+ parseAgentDeployManifest(JSON.parse(JSON.stringify(manifest))) === undefined
52
+ ) {
53
+ err("internal error: generated manifest failed schema validation\n");
54
+ return 1;
55
+ }
56
+
57
+ out(
58
+ options.json === true
59
+ ? `${JSON.stringify(manifest)}\n`
60
+ : `${JSON.stringify(manifest, null, 2)}\n`
61
+ );
62
+ return 0;
63
+ }
@@ -402,7 +402,16 @@ function writePendingAdminWait(
402
402
  ? poll.oauthAuthorizeUrl
403
403
  : undefined;
404
404
  ctx.err(`Waiting for a Slack admin to approve the app${appSuffix}.\n\n`);
405
- if (requestUrl !== undefined) {
405
+ if (poll.approvalRequest === "pending") {
406
+ // Already in the admin queue: nothing to file, so no browser hop.
407
+ ctx.err(
408
+ ctx.dim(` Approval request sent; workspace owners were notified.\n\n`)
409
+ );
410
+ if (requestUrl !== undefined) {
411
+ ctx.err(ctx.dim(` Send a reminder from:\n\n`));
412
+ ctx.err(` ${requestUrl}\n\n`);
413
+ }
414
+ } else if (requestUrl !== undefined) {
406
415
  ctx.err(
407
416
  ctx.dim(
408
417
  ` Slack will notify workspace owners once the request is in their queue.\n` +
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { join } from "node:path";
3
+
4
+ const MAX = 64;
5
+ const TEMPLATE_NAMES = [
6
+ "agentic-owners",
7
+ "demo",
8
+ "pr-autofixer",
9
+ "security-reviewer",
10
+ "triage-linear",
11
+ "triage-jira",
12
+ ] as const;
13
+ export type TemplateName = (typeof TEMPLATE_NAMES)[number];
14
+ const NAMES = new Set<string>(TEMPLATE_NAMES);
15
+
16
+ export function canonicalTemplateName(name: string): string {
17
+ return name === "triage" ? "triage-linear" : name;
18
+ }
19
+
20
+ export function isTemplateName(name: string): name is TemplateName {
21
+ return NAMES.has(name);
22
+ }
23
+
24
+ /** Raw scaffold marker. Validation depends on the consuming boundary. */
25
+ export function readCursorAgentTemplate(rootDir: string): string | undefined {
26
+ try {
27
+ const pkg = JSON.parse(
28
+ readFileSync(join(rootDir, "package.json"), "utf8")
29
+ ) as {
30
+ cursorAgentTemplate?: unknown;
31
+ };
32
+ const stamp = pkg.cursorAgentTemplate;
33
+ return typeof stamp === "string" ? stamp : undefined;
34
+ } catch {
35
+ return undefined;
36
+ }
37
+ }
38
+
39
+ export function readDeployTemplate(rootDir: string): string | undefined {
40
+ const stamp = readCursorAgentTemplate(rootDir);
41
+ if (stamp === undefined || stamp.length === 0 || stamp.length > MAX) {
42
+ return undefined;
43
+ }
44
+ const name = canonicalTemplateName(stamp);
45
+ return isTemplateName(name) ? name : undefined;
46
+ }