@cursor/july 0.1.27 → 0.1.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.
Files changed (180) hide show
  1. package/dist/bin/agent-serve.js +32 -5
  2. package/dist/channels/slack/auth.d.ts +13 -13
  3. package/dist/channels/slack/auth.d.ts.map +1 -1
  4. package/dist/channels/slack/auth.js +19 -10
  5. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  6. package/dist/channels/slack/cursor-account.js +3 -1
  7. package/dist/channels/slack/defaults.d.ts +7 -4
  8. package/dist/channels/slack/defaults.d.ts.map +1 -1
  9. package/dist/channels/slack/defaults.js +14 -4
  10. package/dist/channels/slack/dispatch.d.ts +6 -0
  11. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  12. package/dist/channels/slack/dispatch.js +59 -21
  13. package/dist/channels/slack/external-policy.d.ts +27 -0
  14. package/dist/channels/slack/external-policy.d.ts.map +1 -0
  15. package/dist/channels/slack/external-policy.js +53 -0
  16. package/dist/channels/slack/index.d.ts +1 -0
  17. package/dist/channels/slack/index.d.ts.map +1 -1
  18. package/dist/channels/slack/index.js +1 -0
  19. package/dist/channels/slack/interactive.d.ts +6 -0
  20. package/dist/channels/slack/interactive.d.ts.map +1 -1
  21. package/dist/channels/slack/interactive.js +42 -15
  22. package/dist/channels/slack/slack-channel.d.ts +19 -1
  23. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  24. package/dist/channels/slack/slack-channel.js +48 -6
  25. package/dist/channels/slack/types.d.ts +10 -0
  26. package/dist/channels/slack/types.d.ts.map +1 -1
  27. package/dist/docs/404.html +2 -2
  28. package/dist/docs/ab.html +3 -3
  29. package/dist/docs/assets/{app.BICLoJYF.js → app.QdunVQKg.js} +1 -1
  30. package/dist/docs/assets/chunks/@localSearchIndexroot.DGe61XHA.js +1 -0
  31. package/dist/docs/assets/chunks/{VPLocalSearchBox.wN4Ko9dT.js → VPLocalSearchBox.B6EpUXYf.js} +1 -1
  32. package/dist/docs/assets/chunks/{theme.dnnTE-Co.js → theme.BOTJVqh7.js} +2 -2
  33. package/dist/docs/assets/{guides_slack.md.D_xFKlnF.js → guides_slack.md.CjmJSvZS.js} +8 -4
  34. package/dist/docs/assets/guides_slack.md.CjmJSvZS.lean.js +1 -0
  35. package/dist/docs/assets/guides_webhooks.md.wSOYas3X.js +183 -0
  36. package/dist/docs/assets/{guides_webhooks.md.B1EswtUu.lean.js → guides_webhooks.md.wSOYas3X.lean.js} +1 -1
  37. package/dist/docs/assets/{index.md.Dfv5ic9t.js → index.md.CH_s5uZe.js} +1 -1
  38. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.js → reference_channels.md.CDhTRfUz.js} +2 -2
  39. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.lean.js → reference_channels.md.CDhTRfUz.lean.js} +1 -1
  40. package/dist/docs/building-with-agents.html +3 -3
  41. package/dist/docs/concepts.html +3 -3
  42. package/dist/docs/deployment.html +3 -3
  43. package/dist/docs/evals.html +3 -3
  44. package/dist/docs/example-agents/approval-buddy.html +3 -3
  45. package/dist/docs/example-agents/benny.html +3 -3
  46. package/dist/docs/example-agents/bugbot.html +3 -3
  47. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  48. package/dist/docs/example-agents/codeowners-review.html +3 -3
  49. package/dist/docs/example-agents/concierge.html +3 -3
  50. package/dist/docs/example-agents/fsd.html +3 -3
  51. package/dist/docs/example-agents/index.html +3 -3
  52. package/dist/docs/example-agents/knowledge-base.html +3 -3
  53. package/dist/docs/example-agents/oncall.html +3 -3
  54. package/dist/docs/example-agents/security-reviewer.html +3 -3
  55. package/dist/docs/example-agents/slack-agent.html +3 -3
  56. package/dist/docs/example-agents/weather-agent.html +3 -3
  57. package/dist/docs/guides/agent-to-agent.html +3 -3
  58. package/dist/docs/guides/cloud-runtime.html +3 -3
  59. package/dist/docs/guides/github.html +3 -3
  60. package/dist/docs/guides/human-in-the-loop.html +3 -3
  61. package/dist/docs/guides/mcp-oauth.html +3 -3
  62. package/dist/docs/guides/slack.html +11 -7
  63. package/dist/docs/guides/webhooks.html +138 -5
  64. package/dist/docs/hashmap.json +1 -1
  65. package/dist/docs/hillclimbing.html +3 -3
  66. package/dist/docs/index.html +5 -5
  67. package/dist/docs/quickstart.html +3 -3
  68. package/dist/docs/reference/agent-config.html +3 -3
  69. package/dist/docs/reference/channels.html +5 -5
  70. package/dist/docs/reference/cli.html +3 -3
  71. package/dist/docs/reference/connections.html +3 -3
  72. package/dist/docs/reference/hooks.html +3 -3
  73. package/dist/docs/reference/http-api.html +3 -3
  74. package/dist/docs/reference/instructions.html +3 -3
  75. package/dist/docs/reference/playground.html +3 -3
  76. package/dist/docs/reference/project-layout.html +3 -3
  77. package/dist/docs/reference/prompt.html +3 -3
  78. package/dist/docs/reference/schedules.html +3 -3
  79. package/dist/docs/reference/sessions.html +3 -3
  80. package/dist/docs/reference/skills.html +3 -3
  81. package/dist/docs/reference/subagents.html +3 -3
  82. package/dist/docs/reference/tools.html +3 -3
  83. package/dist/docs/scaffolding-agents.html +3 -3
  84. package/dist/docs/storage.html +3 -3
  85. package/dist/docs/troubleshooting.html +3 -3
  86. package/dist/internal/cli-ax.d.ts +8 -0
  87. package/dist/internal/cli-ax.d.ts.map +1 -1
  88. package/dist/internal/cli-ax.js +116 -57
  89. package/dist/internal/cloud-turn-cost.d.ts +34 -0
  90. package/dist/internal/cloud-turn-cost.d.ts.map +1 -0
  91. package/dist/internal/cloud-turn-cost.js +106 -0
  92. package/dist/internal/cursor-slack-relay.d.ts +2 -0
  93. package/dist/internal/cursor-slack-relay.d.ts.map +1 -1
  94. package/dist/internal/cursor-slack-relay.js +1 -0
  95. package/dist/internal/deploy-client.d.ts +2 -0
  96. package/dist/internal/deploy-client.d.ts.map +1 -1
  97. package/dist/internal/deploy-client.js +1 -0
  98. package/dist/internal/discovery.d.ts.map +1 -1
  99. package/dist/internal/discovery.js +9 -2
  100. package/dist/internal/event-log.d.ts +6 -0
  101. package/dist/internal/event-log.d.ts.map +1 -1
  102. package/dist/internal/event-log.js +16 -0
  103. package/dist/internal/host-platforms.d.ts +13 -0
  104. package/dist/internal/host-platforms.d.ts.map +1 -1
  105. package/dist/internal/host-platforms.js +25 -16
  106. package/dist/internal/http-channel.d.ts.map +1 -1
  107. package/dist/internal/http-channel.js +3 -1
  108. package/dist/internal/local-control-plane.d.ts +25 -0
  109. package/dist/internal/local-control-plane.d.ts.map +1 -0
  110. package/dist/internal/local-control-plane.js +172 -0
  111. package/dist/internal/platform-schedule-sync.d.ts +17 -0
  112. package/dist/internal/platform-schedule-sync.d.ts.map +1 -0
  113. package/dist/internal/platform-schedule-sync.js +72 -0
  114. package/dist/internal/platform-timers.d.ts +28 -0
  115. package/dist/internal/platform-timers.d.ts.map +1 -0
  116. package/dist/internal/platform-timers.js +120 -0
  117. package/dist/internal/reminder-runner.d.ts +16 -12
  118. package/dist/internal/reminder-runner.d.ts.map +1 -1
  119. package/dist/internal/reminder-runner.js +112 -61
  120. package/dist/internal/reminder-store.d.ts +5 -4
  121. package/dist/internal/reminder-store.d.ts.map +1 -1
  122. package/dist/internal/reminder-store.js +5 -4
  123. package/dist/internal/sdk-runner.d.ts.map +1 -1
  124. package/dist/internal/sdk-runner.js +44 -1
  125. package/dist/internal/server.d.ts.map +1 -1
  126. package/dist/internal/server.js +115 -16
  127. package/dist/internal/session-engine.d.ts +22 -1
  128. package/dist/internal/session-engine.d.ts.map +1 -1
  129. package/dist/internal/session-engine.js +110 -4
  130. package/dist/internal/storage-coordinator.d.ts +4 -15
  131. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  132. package/dist/internal/storage-coordinator.js +4 -30
  133. package/dist/playground/assets/index-CquRB-l0.js +85 -0
  134. package/dist/playground/assets/{index-Bul1WfrB.css → index-Dj0bWpkn.css} +1 -1
  135. package/dist/playground/index.html +2 -2
  136. package/dist/storage.d.ts +0 -3
  137. package/dist/storage.d.ts.map +1 -1
  138. package/dist/storage.js +0 -3
  139. package/dist/types.d.ts +11 -2
  140. package/dist/types.d.ts.map +1 -1
  141. package/docs/README.md +1 -0
  142. package/docs/guides/slack.md +32 -7
  143. package/docs/guides/webhooks.md +183 -0
  144. package/docs/reference/channels.md +2 -1
  145. package/package.json +1 -1
  146. package/skills/setup-slack/SKILL.md +3 -0
  147. package/src/bin/agent-serve.ts +33 -5
  148. package/src/channels/slack/auth.ts +29 -22
  149. package/src/channels/slack/cursor-account.ts +2 -0
  150. package/src/channels/slack/defaults.ts +24 -4
  151. package/src/channels/slack/dispatch.ts +42 -1
  152. package/src/channels/slack/external-policy.ts +62 -0
  153. package/src/channels/slack/index.ts +1 -0
  154. package/src/channels/slack/interactive.ts +42 -10
  155. package/src/channels/slack/slack-channel.ts +66 -3
  156. package/src/channels/slack/types.ts +12 -0
  157. package/src/internal/cli-ax.ts +110 -34
  158. package/src/internal/cloud-turn-cost.ts +158 -0
  159. package/src/internal/cursor-slack-relay.ts +3 -0
  160. package/src/internal/deploy-client.ts +3 -0
  161. package/src/internal/discovery.ts +9 -1
  162. package/src/internal/event-log.ts +17 -0
  163. package/src/internal/host-platforms.ts +40 -18
  164. package/src/internal/http-channel.ts +3 -1
  165. package/src/internal/local-control-plane.ts +188 -0
  166. package/src/internal/platform-schedule-sync.ts +81 -0
  167. package/src/internal/platform-timers.ts +173 -0
  168. package/src/internal/reminder-runner.ts +131 -67
  169. package/src/internal/reminder-store.ts +5 -4
  170. package/src/internal/sdk-runner.ts +49 -1
  171. package/src/internal/server.ts +130 -11
  172. package/src/internal/session-engine.ts +138 -3
  173. package/src/internal/storage-coordinator.ts +4 -41
  174. package/src/storage.ts +0 -5
  175. package/src/types.ts +11 -2
  176. package/dist/docs/assets/chunks/@localSearchIndexroot.CQJytDwU.js +0 -1
  177. package/dist/docs/assets/guides_slack.md.D_xFKlnF.lean.js +0 -1
  178. package/dist/docs/assets/guides_webhooks.md.B1EswtUu.js +0 -50
  179. package/dist/playground/assets/index-BDCeW4uc.js +0 -86
  180. /package/dist/docs/assets/{index.md.Dfv5ic9t.lean.js → index.md.CH_s5uZe.lean.js} +0 -0
@@ -43,6 +43,7 @@ import {
43
43
  buildDefaultEvents,
44
44
  createDefaultOnChannelPost,
45
45
  createDefaultOnInbound,
46
+ resolveSlackBlockExternals,
46
47
  } from "./defaults.js";
47
48
  import {
48
49
  dispatchInboundFromEventBody,
@@ -66,9 +67,53 @@ import {
66
67
  assertSlackCursorAgentName,
67
68
  type SlackChannelOptions,
68
69
  type SlackChannelState,
70
+ type SlackCredentials,
69
71
  type SlackCursorAgentName,
70
72
  } from "./types.js";
71
73
 
74
+ /** Options used to construct a slack channel — for host-level credential binding. */
75
+ const slackChannelOptionsByDefinition = new WeakMap<
76
+ object,
77
+ SlackChannelOptions
78
+ >();
79
+
80
+ /**
81
+ * Recover the {@link SlackChannelOptions} used to build a channel definition,
82
+ * if it was created via {@link slackChannel}.
83
+ */
84
+ export function getSlackChannelOptions(
85
+ definition: object
86
+ ): SlackChannelOptions | undefined {
87
+ return slackChannelOptionsByDefinition.get(definition);
88
+ }
89
+
90
+ /** Dedicated Socket Mode credentials for `ctx.host.slack.getClient()`. */
91
+ export interface SlackChannelHostBinding {
92
+ credentials: SlackCredentials;
93
+ envPrefix?: string;
94
+ }
95
+
96
+ /**
97
+ * First dedicated (Socket Mode) Slack channel on a project. Skips
98
+ * `cursorAccount` channels — those have no local bot token for
99
+ * {@link HostContext.slack.getClient}.
100
+ */
101
+ export function resolveDedicatedSlackHostBinding(
102
+ channels: ReadonlyArray<{ definition: object }>
103
+ ): SlackChannelHostBinding | undefined {
104
+ for (const channel of channels) {
105
+ const options = slackChannelOptionsByDefinition.get(channel.definition);
106
+ if (options === undefined || options.cursorAccount === true) {
107
+ continue;
108
+ }
109
+ return {
110
+ credentials: options.credentials ?? credentialsFromEnv(options.envPrefix),
111
+ envPrefix: options.envPrefix,
112
+ };
113
+ }
114
+ return undefined;
115
+ }
116
+
72
117
  /**
73
118
  * Create a Slack channel. Author as `agent/channels/slack.ts` default export.
74
119
  *
@@ -152,11 +197,14 @@ export function slackChannel<const Name extends string = string>(
152
197
  : (options.credentials ?? credentialsFromEnv(options.envPrefix));
153
198
  const envKeys = slackEnvKeys(options.envPrefix);
154
199
  const loadingMessages = options.loadingMessages;
155
- const defaultInbound = createDefaultOnInbound(loadingMessages);
200
+ const policy = options.policy;
201
+ const blockExternals = resolveSlackBlockExternals(policy);
202
+ const defaultInbound = createDefaultOnInbound(loadingMessages, policy);
156
203
  const onAppMention = options.onAppMention ?? defaultInbound;
157
204
  const onDirectMessage = options.onDirectMessage ?? defaultInbound;
158
205
  const onChannelPost =
159
- options.onChannelPost ?? createDefaultOnChannelPost(loadingMessages);
206
+ options.onChannelPost ??
207
+ createDefaultOnChannelPost(loadingMessages, policy);
160
208
  const engagement = options.engagement ?? {};
161
209
  const mentionsEnabled = engagement.mentions !== false;
162
210
  const directMessagesEnabled = engagement.directMessages !== false;
@@ -237,6 +285,8 @@ export function slackChannel<const Name extends string = string>(
237
285
  mentionsEnabled,
238
286
  directMessagesEnabled,
239
287
  evalCommandsEnabled,
288
+ // cursorAccount: backend router already refuses externals.
289
+ blockExternals: cursorAccount ? false : blockExternals,
240
290
  ...(args.hasContinuationSession === undefined
241
291
  ? {}
242
292
  : { hasContinuationSession: args.hasContinuationSession }),
@@ -329,10 +379,15 @@ export function slackChannel<const Name extends string = string>(
329
379
 
330
380
  let botUserId: string | undefined;
331
381
  let botId: string | undefined;
382
+ let installTeamId: string | undefined;
332
383
  let webClient: InstanceType<
333
384
  typeof import("@slack/web-api").WebClient
334
385
  > | null = null;
335
- if (channelPosts !== undefined || botMentions !== undefined) {
386
+ if (
387
+ channelPosts !== undefined ||
388
+ botMentions !== undefined ||
389
+ routeInteractive
390
+ ) {
336
391
  try {
337
392
  const { WebClient } = await import("@slack/web-api");
338
393
  webClient = new WebClient(botToken);
@@ -341,6 +396,8 @@ export function slackChannel<const Name extends string = string>(
341
396
  typeof identity.user_id === "string" ? identity.user_id : undefined;
342
397
  botId =
343
398
  typeof identity.bot_id === "string" ? identity.bot_id : undefined;
399
+ installTeamId =
400
+ typeof identity.team_id === "string" ? identity.team_id : undefined;
344
401
  } catch (error) {
345
402
  getLog().error("bot identity lookup degraded", {
346
403
  error: error instanceof Error ? error.message : String(error),
@@ -416,6 +473,7 @@ export function slackChannel<const Name extends string = string>(
416
473
  artifacts: args.artifacts,
417
474
  log: getLog(),
418
475
  evalCommandsEnabled,
476
+ blockExternals,
419
477
  ...(args.hasContinuationSession === undefined
420
478
  ? {}
421
479
  : {
@@ -491,6 +549,7 @@ export function slackChannel<const Name extends string = string>(
491
549
  resolveApproval: args.resolveApproval,
492
550
  waitUntil: args.waitUntil,
493
551
  log: getLog(),
552
+ blockExternals,
494
553
  }
495
554
  );
496
555
  if (handledAsApproval || onBlockAction === undefined) {
@@ -503,6 +562,8 @@ export function slackChannel<const Name extends string = string>(
503
562
  host: args.host,
504
563
  artifacts: args.artifacts,
505
564
  log: getLog(),
565
+ blockExternals,
566
+ installTeamId,
506
567
  hasContinuationSession: args.hasContinuationSession,
507
568
  isContinuationBusy: args.isContinuationBusy,
508
569
  getContinuationLastBotMessageTs:
@@ -542,6 +603,8 @@ export function slackChannel<const Name extends string = string>(
542
603
  },
543
604
  });
544
605
 
606
+ slackChannelOptionsByDefinition.set(definition, options);
607
+
545
608
  if (accountTransport !== undefined) {
546
609
  // Serve-time seam: the host discovers cursor-account channels through
547
610
  // this registry, binds the outbound transport, and dispatches relayed
@@ -432,8 +432,20 @@ export function assertSlackCursorAgentName(name: string): void {
432
432
  }
433
433
  }
434
434
 
435
+ /** `"thread"` = anyone in the thread (default); `"author"` = session starter only. */
436
+ export type SlackRespondTo = "thread" | "author";
437
+
438
+ export interface SlackAccessPolicy {
439
+ /** Defaults to `"thread"`. Applied by the default inbound handlers. */
440
+ respondTo?: SlackRespondTo;
441
+ /** Refuse Slack Connect / guest / other-workspace senders. Defaults to `true`. */
442
+ blockExternals?: boolean;
443
+ }
444
+
435
445
  export interface SlackChannelOptions {
436
446
  credentials?: SlackCredentials;
447
+ /** Defaults to `{ respondTo: "thread", blockExternals: true }`. */
448
+ policy?: SlackAccessPolicy;
437
449
  /**
438
450
  * Per-agent env prefix for tokens. `envPrefix: "MY_AGENT"` reads
439
451
  * `MY_AGENT_SLACK_BOT_TOKEN` + `MY_AGENT_SLACK_APP_TOKEN`. Prefer this over
@@ -9,7 +9,7 @@
9
9
 
10
10
  import { mkdir, mkdtemp, writeFile } from "node:fs/promises";
11
11
  import { tmpdir } from "node:os";
12
- import { join, resolve } from "node:path";
12
+ import { dirname, join, resolve } from "node:path";
13
13
  import { Artifacts, JUnit } from "../evals/reporters.js";
14
14
  import type {
15
15
  EvalAssertionResult,
@@ -122,6 +122,12 @@ export interface AxCliOptions {
122
122
  skipReport?: boolean;
123
123
  /** `eval`: override `evals.config.ts` `maxConcurrency`. */
124
124
  maxConcurrency?: number;
125
+ /**
126
+ * `eval` / `eval status <runId>`: also write the full results JSON to this
127
+ * path. `session`: write the raw NDJSON trace (same format as `run
128
+ * --events` and the playground download) to this path instead of printing.
129
+ */
130
+ out?: string;
125
131
  /** Scaffold target directory for `init`. */
126
132
  initTarget?: string;
127
133
  /** Tool name for `call`. */
@@ -794,6 +800,18 @@ export async function cmdEval(options: AxCliOptions): Promise<number> {
794
800
  return 2;
795
801
  }
796
802
  }
803
+ if (trimNonEmpty(options.out) !== undefined) {
804
+ if (options.list === true) {
805
+ process.stderr.write("--out and --list are mutually exclusive\n");
806
+ return 2;
807
+ }
808
+ if (options.noWait === true) {
809
+ process.stderr.write(
810
+ `--out needs the finished results; drop --no-wait, or poll later with ${CLI_COMMAND_NAME} eval status <evalId> --out <path>\n`
811
+ );
812
+ return 2;
813
+ }
814
+ }
797
815
 
798
816
  const filterIds =
799
817
  options.evalIds === undefined || options.evalIds.length === 0
@@ -888,7 +906,12 @@ export async function cmdEval(options: AxCliOptions): Promise<number> {
888
906
  process.stderr.write("No matching evals found.\n");
889
907
  return 2;
890
908
  }
891
- return printEvalResults(results, options, undefined, artifactsDir);
909
+ return await printEvalResults(
910
+ results,
911
+ options,
912
+ undefined,
913
+ artifactsDir
914
+ );
892
915
  } finally {
893
916
  progress?.close();
894
917
  }
@@ -944,7 +967,7 @@ export async function cmdEval(options: AxCliOptions): Promise<number> {
944
967
  process.stderr.write("No matching evals found.\n");
945
968
  return 2;
946
969
  }
947
- return printEvalResults(results, options, {
970
+ return await printEvalResults(results, options, {
948
971
  runId: snapshot.runId,
949
972
  status: snapshot.status,
950
973
  error: snapshot.error,
@@ -1049,8 +1072,8 @@ async function printEvalAccepted(
1049
1072
  agentUrl
1050
1073
  );
1051
1074
  if (options.json) {
1052
- console.log(
1053
- JSON.stringify(
1075
+ process.stdout.write(
1076
+ `${JSON.stringify(
1054
1077
  {
1055
1078
  ok: true,
1056
1079
  accepted: true,
@@ -1064,7 +1087,7 @@ async function printEvalAccepted(
1064
1087
  },
1065
1088
  null,
1066
1089
  2
1067
- )
1090
+ )}\n`
1068
1091
  );
1069
1092
  } else {
1070
1093
  const durable = started.config.durableRuns === true ? " (persisted)" : "";
@@ -1168,7 +1191,7 @@ export async function cmdEvalCancel(
1168
1191
  fetchImpl: options.fetchImpl,
1169
1192
  });
1170
1193
  if (options.json) {
1171
- console.log(JSON.stringify(snap, null, 2));
1194
+ process.stdout.write(`${JSON.stringify(snap, null, 2)}\n`);
1172
1195
  } else {
1173
1196
  process.stdout.write(
1174
1197
  `Cancelled Eval ID ${snap.runId} (${snap.summary.done}/${snap.summary.total} cases finalized)\n`
@@ -1214,6 +1237,13 @@ export async function cmdEvalStatus(
1214
1237
  process.stderr.write("eval status does not accept --list or --no-wait\n");
1215
1238
  return 2;
1216
1239
  }
1240
+ if (
1241
+ trimNonEmpty(options.out) !== undefined &&
1242
+ (runId === undefined || runId === "")
1243
+ ) {
1244
+ process.stderr.write("eval status --out requires an eval id\n");
1245
+ return 2;
1246
+ }
1217
1247
 
1218
1248
  const target = await resolveEvalTarget(options);
1219
1249
  if (typeof target === "number") {
@@ -1232,7 +1262,9 @@ export async function cmdEvalStatus(
1232
1262
  fetchImpl: options.fetchImpl,
1233
1263
  });
1234
1264
  if (options.json) {
1235
- console.log(JSON.stringify({ runs, activeRunId }, null, 2));
1265
+ process.stdout.write(
1266
+ `${JSON.stringify({ runs, activeRunId }, null, 2)}\n`
1267
+ );
1236
1268
  } else if (runs.length === 0) {
1237
1269
  process.stdout.write("No eval runs on this agent.\n");
1238
1270
  } else {
@@ -1252,8 +1284,12 @@ export async function cmdEvalStatus(
1252
1284
  auth,
1253
1285
  fetchImpl: options.fetchImpl,
1254
1286
  });
1287
+ const outPath = await writeJsonOutFile(options.out, snap);
1255
1288
  if (options.json) {
1256
- console.log(JSON.stringify(snap, null, 2));
1289
+ process.stdout.write(`${JSON.stringify(snap, null, 2)}\n`);
1290
+ if (outPath !== undefined) {
1291
+ process.stderr.write(`results: ${outPath}\n`);
1292
+ }
1257
1293
  } else {
1258
1294
  process.stdout.write(`${formatEvalRunSummaryLine(snap)}\n`);
1259
1295
  if (snap.error !== undefined && snap.error !== "") {
@@ -1271,6 +1307,9 @@ export async function cmdEvalStatus(
1271
1307
  process.stdout.write(` error: ${c.error}\n`);
1272
1308
  }
1273
1309
  }
1310
+ if (outPath !== undefined) {
1311
+ process.stdout.write(`results: ${outPath}\n`);
1312
+ }
1274
1313
  }
1275
1314
  if (snap.status === "running") {
1276
1315
  return 3;
@@ -1333,7 +1372,7 @@ function printListedEvals(
1333
1372
  emptyMessage: string
1334
1373
  ): number {
1335
1374
  if (options.json) {
1336
- console.log(JSON.stringify(listed, null, 2));
1375
+ process.stdout.write(`${JSON.stringify(listed, null, 2)}\n`);
1337
1376
  } else if (listed.length === 0) {
1338
1377
  process.stdout.write(`${emptyMessage}\n`);
1339
1378
  } else {
@@ -1438,7 +1477,7 @@ const EVAL_VERDICT_LABEL: Record<EvalVerdict, string> = {
1438
1477
  skipped: "SKIP",
1439
1478
  };
1440
1479
 
1441
- function printEvalResults(
1480
+ async function printEvalResults(
1442
1481
  results: EvalRunResult[],
1443
1482
  options: AxCliOptions,
1444
1483
  batch?: {
@@ -1447,7 +1486,7 @@ function printEvalResults(
1447
1486
  error?: string;
1448
1487
  },
1449
1488
  artifactsDir?: string
1450
- ): number {
1489
+ ): Promise<number> {
1451
1490
  const strict = options.strict === true;
1452
1491
  const counts = { passed: 0, failed: 0, scored: 0, skipped: 0 };
1453
1492
  for (const result of results) {
@@ -1462,28 +1501,29 @@ function printEvalResults(
1462
1501
  process.stderr.write(`eval run error: ${batch.error}\n`);
1463
1502
  }
1464
1503
 
1504
+ const payload = {
1505
+ ok,
1506
+ passed: counts.passed,
1507
+ failed: counts.failed,
1508
+ scored: counts.scored,
1509
+ skipped: counts.skipped,
1510
+ strict,
1511
+ results,
1512
+ ...(artifactsDir === undefined ? {} : { artifactsDir }),
1513
+ ...(batch?.runId === undefined ? {} : { runId: batch.runId }),
1514
+ ...(batch?.status === undefined ? {} : { status: batch.status }),
1515
+ ...(batch?.error === undefined || batch.error === ""
1516
+ ? {}
1517
+ : { error: batch.error }),
1518
+ };
1519
+ const outPath = await writeJsonOutFile(options.out, payload);
1520
+
1465
1521
  if (options.json) {
1466
- console.log(
1467
- JSON.stringify(
1468
- {
1469
- ok,
1470
- passed: counts.passed,
1471
- failed: counts.failed,
1472
- scored: counts.scored,
1473
- skipped: counts.skipped,
1474
- strict,
1475
- results,
1476
- ...(artifactsDir === undefined ? {} : { artifactsDir }),
1477
- ...(batch?.runId === undefined ? {} : { runId: batch.runId }),
1478
- ...(batch?.status === undefined ? {} : { status: batch.status }),
1479
- ...(batch?.error === undefined || batch.error === ""
1480
- ? {}
1481
- : { error: batch.error }),
1482
- },
1483
- null,
1484
- 2
1485
- )
1486
- );
1522
+ process.stdout.write(`${JSON.stringify(payload, null, 2)}\n`);
1523
+ if (outPath !== undefined) {
1524
+ // stdout stays pure JSON; report the file on stderr.
1525
+ process.stderr.write(`results: ${outPath}\n`);
1526
+ }
1487
1527
  } else {
1488
1528
  for (const r of results) {
1489
1529
  const verdict = evalVerdictOf(r);
@@ -1532,10 +1572,31 @@ function printEvalResults(
1532
1572
  if (artifactsDir !== undefined) {
1533
1573
  process.stdout.write(`artifacts: ${artifactsDir}\n`);
1534
1574
  }
1575
+ if (outPath !== undefined) {
1576
+ process.stdout.write(`results: ${outPath}\n`);
1577
+ }
1535
1578
  }
1536
1579
  return ok ? 0 : 1;
1537
1580
  }
1538
1581
 
1582
+ /**
1583
+ * `--out <path>`: write a JSON payload (results / run snapshot), creating
1584
+ * parent directories. Returns the resolved path, or undefined when unset.
1585
+ */
1586
+ async function writeJsonOutFile(
1587
+ out: string | undefined,
1588
+ payload: unknown
1589
+ ): Promise<string | undefined> {
1590
+ const trimmed = trimNonEmpty(out);
1591
+ if (trimmed === undefined) {
1592
+ return undefined;
1593
+ }
1594
+ const target = resolve(trimmed);
1595
+ await mkdir(dirname(target), { recursive: true });
1596
+ await writeFile(target, `${JSON.stringify(payload, null, 2)}\n`, "utf8");
1597
+ return target;
1598
+ }
1599
+
1539
1600
  /**
1540
1601
  * One assertion line. Soft assertions read `~` rather than `✗` when they miss,
1541
1602
  * so a tracked score is visibly different from a failed gate.
@@ -1721,6 +1782,8 @@ export async function cmdSessions(options: AxCliOptions): Promise<number> {
1721
1782
  /**
1722
1783
  * `session <id>` — view a full session (trajectory text by default).
1723
1784
  * `--json` → trajectory JSON; `--events` (`rawEvents`) → raw event list.
1785
+ * `--out <path>` → write the raw NDJSON trace to a file instead of printing
1786
+ * (same format as `run --events` and the playground download).
1724
1787
  * Interactive reattach: `resume [id]` / `chat --resume` (or `chat --session`).
1725
1788
  */
1726
1789
  export async function cmdSession(
@@ -1729,7 +1792,7 @@ export async function cmdSession(
1729
1792
  ): Promise<number> {
1730
1793
  if (sessionId === undefined || sessionId.trim() === "") {
1731
1794
  process.stderr.write(
1732
- `Usage: ${CLI_COMMAND_NAME} session <sessionId> [--url | --prod] [--json | --text | --events]\n`
1795
+ `Usage: ${CLI_COMMAND_NAME} session <sessionId> [--url | --prod] [--json | --text | --events] [--out <file.ndjson>]\n`
1733
1796
  );
1734
1797
  return 2;
1735
1798
  }
@@ -1754,6 +1817,19 @@ export async function cmdSession(
1754
1817
  fetchImpl: options.fetchImpl,
1755
1818
  });
1756
1819
 
1820
+ const outPath = trimNonEmpty(options.out);
1821
+ if (outPath !== undefined) {
1822
+ const targetPath = resolve(outPath);
1823
+ await mkdir(dirname(targetPath), { recursive: true });
1824
+ await writeFile(
1825
+ targetPath,
1826
+ `${events.map((e) => JSON.stringify(e)).join("\n")}\n`,
1827
+ "utf8"
1828
+ );
1829
+ process.stdout.write(`trace: ${targetPath} (${events.length} events)\n`);
1830
+ return 0;
1831
+ }
1832
+
1757
1833
  if (options.rawEvents === true) {
1758
1834
  process.stdout.write(
1759
1835
  `${JSON.stringify({ sessionId, events }, null, 2)}\n`
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Recover token usage / estimated cost for cloud turns that finished without
3
+ * either field on `turn.completed`.
4
+ *
5
+ * Cloud `run.wait()` often drops streamed usage after SSE reconnect, and
6
+ * older engines never persisted cost. The playground Trace tab only renders
7
+ * what is on the event, so historical sessions stay blank unless we backfill
8
+ * from `Agent.getUsage` when the stream is read.
9
+ */
10
+
11
+ import type {
12
+ ModelSetting,
13
+ SessionEvent,
14
+ TurnCost,
15
+ TurnUsage,
16
+ } from "../types.js";
17
+ import { estimateTurnCost } from "./model-pricing.js";
18
+
19
+ export interface CloudRunUsage {
20
+ runId: string;
21
+ usage: TurnUsage;
22
+ }
23
+
24
+ export interface CloudAgentUsage {
25
+ usage: TurnUsage;
26
+ runs: CloudRunUsage[];
27
+ }
28
+
29
+ export function usageTokenSum(usage: TurnUsage | undefined): number {
30
+ if (usage === undefined) {
31
+ return 0;
32
+ }
33
+ return (
34
+ usage.inputTokens +
35
+ usage.outputTokens +
36
+ usage.cacheReadTokens +
37
+ usage.cacheWriteTokens
38
+ );
39
+ }
40
+
41
+ export function isUnpricedCompletedTurn(event: SessionEvent): boolean {
42
+ return (
43
+ event.type === "turn.completed" &&
44
+ usageTokenSum(event.data.usage) <= 0 &&
45
+ event.data.cost === undefined
46
+ );
47
+ }
48
+
49
+ export function cloudSdkAgentIdFromEvents(
50
+ events: ReadonlyArray<SessionEvent>
51
+ ): string | undefined {
52
+ let sdkAgentId: string | undefined;
53
+ for (const event of events) {
54
+ if (event.type !== "agent.bound") {
55
+ continue;
56
+ }
57
+ const id = event.data.sdkAgentId.trim();
58
+ if (id.startsWith("bc-")) {
59
+ sdkAgentId = id;
60
+ }
61
+ }
62
+ return sdkAgentId;
63
+ }
64
+
65
+ /**
66
+ * Copy usage/cost onto unpriced `turn.completed` events. Returns undefined
67
+ * when the snapshot cannot be attributed safely (multiple unmatched turns).
68
+ */
69
+ export function applyCloudUsageToEvents(
70
+ events: ReadonlyArray<SessionEvent>,
71
+ billed: CloudAgentUsage,
72
+ model: ModelSetting
73
+ ): SessionEvent[] | undefined {
74
+ const completedIndexes: number[] = [];
75
+ const unpricedIndexes: number[] = [];
76
+ for (let i = 0; i < events.length; i++) {
77
+ const event = events[i];
78
+ if (event === undefined || event.type !== "turn.completed") {
79
+ continue;
80
+ }
81
+ completedIndexes.push(i);
82
+ if (isUnpricedCompletedTurn(event)) {
83
+ unpricedIndexes.push(i);
84
+ }
85
+ }
86
+ if (unpricedIndexes.length === 0) {
87
+ return [...events];
88
+ }
89
+
90
+ const patches = new Map<
91
+ number,
92
+ { usage: TurnUsage; cost: TurnCost | undefined }
93
+ >();
94
+ if (
95
+ unpricedIndexes.length === 1 &&
96
+ completedIndexes.length === 1 &&
97
+ usageTokenSum(billed.usage) > 0
98
+ ) {
99
+ const index = unpricedIndexes[0];
100
+ if (index !== undefined) {
101
+ patches.set(index, {
102
+ usage: billed.usage,
103
+ cost: estimateTurnCost(model, billed.usage),
104
+ });
105
+ }
106
+ } else if (
107
+ billed.runs.length === unpricedIndexes.length &&
108
+ billed.runs.every((run) => usageTokenSum(run.usage) > 0)
109
+ ) {
110
+ for (let i = 0; i < unpricedIndexes.length; i++) {
111
+ const index = unpricedIndexes[i];
112
+ const run = billed.runs[i];
113
+ if (index === undefined || run === undefined) {
114
+ return undefined;
115
+ }
116
+ patches.set(index, {
117
+ usage: run.usage,
118
+ cost: estimateTurnCost(model, run.usage),
119
+ });
120
+ }
121
+ } else {
122
+ return undefined;
123
+ }
124
+
125
+ return events.map((event, index) => {
126
+ const patch = patches.get(index);
127
+ if (patch === undefined || event.type !== "turn.completed") {
128
+ return event;
129
+ }
130
+ return {
131
+ ...event,
132
+ data: {
133
+ ...event.data,
134
+ usage: patch.usage,
135
+ ...(patch.cost === undefined ? {} : { cost: patch.cost }),
136
+ },
137
+ };
138
+ });
139
+ }
140
+
141
+ export function mapSdkTokenUsage(usage: {
142
+ inputTokens: number;
143
+ outputTokens: number;
144
+ cacheReadTokens: number;
145
+ cacheWriteTokens: number;
146
+ reasoningTokens?: number;
147
+ }): TurnUsage {
148
+ const mapped: TurnUsage = {
149
+ inputTokens: usage.inputTokens,
150
+ outputTokens: usage.outputTokens,
151
+ cacheReadTokens: usage.cacheReadTokens,
152
+ cacheWriteTokens: usage.cacheWriteTokens,
153
+ };
154
+ if (usage.reasoningTokens !== undefined) {
155
+ mapped.reasoningTokens = usage.reasoningTokens;
156
+ }
157
+ return mapped;
158
+ }
@@ -23,6 +23,8 @@ export interface CursorSlackAgentRegistration {
23
23
  name: string;
24
24
  iconUrl?: string;
25
25
  iconEmoji?: string;
26
+ /** Opt out of the default external-sender block. */
27
+ blockExternals?: boolean;
26
28
  }
27
29
 
28
30
  interface RegistrationResponse {
@@ -160,6 +162,7 @@ export class CursorSlackRelay {
160
162
  name: agent.name,
161
163
  iconUrl: agent.iconUrl,
162
164
  iconEmoji: agent.iconEmoji,
165
+ blockExternals: agent.blockExternals === false ? false : undefined,
163
166
  })),
164
167
  };
165
168
  },
@@ -148,6 +148,8 @@ export interface Deployment {
148
148
  engineGeneration?: number | null;
149
149
  publicId?: string;
150
150
  engineUrl?: string | null;
151
+ /** Anyrun pod id for the active engine, when coordinates are known. */
152
+ enginePodId?: string | null;
151
153
  lastError?: string | null;
152
154
  engineAlias: string;
153
155
  desiredUpdatedAt?: string;
@@ -259,6 +261,7 @@ const deploymentSchema = z.object({
259
261
  engineGeneration: z.number().nullable().optional(),
260
262
  publicId: z.string().optional(),
261
263
  engineUrl: z.string().nullable().optional(),
264
+ enginePodId: z.string().nullable().optional(),
262
265
  lastError: z.string().nullable().optional(),
263
266
  engineAlias: z.string(),
264
267
  desiredUpdatedAt: z.string().optional(),
@@ -1753,6 +1753,7 @@ export function projectInfo(project: AgentProject): AgentProjectInfo {
1753
1753
  ];
1754
1754
  return {
1755
1755
  name: project.name,
1756
+ description: agent.description,
1756
1757
  model: agent.model,
1757
1758
  runtime: agent.runtime,
1758
1759
  cloud:
@@ -2000,7 +2001,14 @@ function isModuleFile(fileName: string): boolean {
2000
2001
  if (fileName.endsWith(".d.ts") || fileName.startsWith(".")) {
2001
2002
  return false;
2002
2003
  }
2003
- return MODULE_EXTENSIONS.has(extname(fileName));
2004
+ if (!MODULE_EXTENSIONS.has(extname(fileName))) {
2005
+ return false;
2006
+ }
2007
+ // Colocated unit tests (Vitest/Jest convention) are not definitions.
2008
+ // Without this, `channels/slack.test.ts` is scanned as a channel named
2009
+ // "slack.test", fails NAME_PATTERN, and the whole agent refuses to serve.
2010
+ const stem = moduleStem(fileName);
2011
+ return !stem.endsWith(".test") && !stem.endsWith(".spec");
2004
2012
  }
2005
2013
 
2006
2014
  function moduleStem(fileName: string): string {
@@ -97,6 +97,23 @@ export class SessionEventLog {
97
97
  return startIndex <= 0 ? [...this.events] : this.events.slice(startIndex);
98
98
  }
99
99
 
100
+ /**
101
+ * Replace one already-recorded event in memory. Used to backfill usage/cost
102
+ * onto historical `turn.completed` rows without rewriting `events.ndjson`.
103
+ * Live subscribers are not notified — patch before opening a replay stream.
104
+ */
105
+ replace(index: number, event: SessionEvent): boolean {
106
+ if (index < 0 || index >= this.events.length) {
107
+ return false;
108
+ }
109
+ const current = this.events[index];
110
+ if (current === undefined || current.index !== index) {
111
+ return false;
112
+ }
113
+ this.events[index] = event;
114
+ return true;
115
+ }
116
+
100
117
  subscribe(listener: (event: SessionEvent) => void): () => void {
101
118
  this.listeners.add(listener);
102
119
  return () => {