@cursor/july 0.1.27 → 0.1.28

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 (122) hide show
  1. package/dist/bin/agent-serve.js +12 -3
  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.map +1 -1
  23. package/dist/channels/slack/slack-channel.js +19 -6
  24. package/dist/channels/slack/types.d.ts +10 -0
  25. package/dist/channels/slack/types.d.ts.map +1 -1
  26. package/dist/docs/404.html +2 -2
  27. package/dist/docs/ab.html +3 -3
  28. package/dist/docs/assets/{app.BICLoJYF.js → app.CqjrYaT2.js} +1 -1
  29. package/dist/docs/assets/chunks/@localSearchIndexroot.VWQoPFJ1.js +1 -0
  30. package/dist/docs/assets/chunks/{VPLocalSearchBox.wN4Ko9dT.js → VPLocalSearchBox.ag-MwUel.js} +1 -1
  31. package/dist/docs/assets/chunks/{theme.dnnTE-Co.js → theme.BS-DJWat.js} +2 -2
  32. package/dist/docs/assets/{guides_slack.md.D_xFKlnF.js → guides_slack.md.CjmJSvZS.js} +8 -4
  33. package/dist/docs/assets/guides_slack.md.CjmJSvZS.lean.js +1 -0
  34. package/dist/docs/assets/guides_webhooks.md.wSOYas3X.js +183 -0
  35. package/dist/docs/assets/{guides_webhooks.md.B1EswtUu.lean.js → guides_webhooks.md.wSOYas3X.lean.js} +1 -1
  36. package/dist/docs/assets/{index.md.Dfv5ic9t.js → index.md.CH_s5uZe.js} +1 -1
  37. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.js → reference_channels.md.CDhTRfUz.js} +2 -2
  38. package/dist/docs/assets/{reference_channels.md.DdmiKgqf.lean.js → reference_channels.md.CDhTRfUz.lean.js} +1 -1
  39. package/dist/docs/building-with-agents.html +3 -3
  40. package/dist/docs/concepts.html +3 -3
  41. package/dist/docs/deployment.html +3 -3
  42. package/dist/docs/evals.html +3 -3
  43. package/dist/docs/example-agents/approval-buddy.html +3 -3
  44. package/dist/docs/example-agents/benny.html +3 -3
  45. package/dist/docs/example-agents/bugbot.html +3 -3
  46. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  47. package/dist/docs/example-agents/codeowners-review.html +3 -3
  48. package/dist/docs/example-agents/concierge.html +3 -3
  49. package/dist/docs/example-agents/fsd.html +3 -3
  50. package/dist/docs/example-agents/index.html +3 -3
  51. package/dist/docs/example-agents/knowledge-base.html +3 -3
  52. package/dist/docs/example-agents/oncall.html +3 -3
  53. package/dist/docs/example-agents/security-reviewer.html +3 -3
  54. package/dist/docs/example-agents/slack-agent.html +3 -3
  55. package/dist/docs/example-agents/weather-agent.html +3 -3
  56. package/dist/docs/guides/agent-to-agent.html +3 -3
  57. package/dist/docs/guides/cloud-runtime.html +3 -3
  58. package/dist/docs/guides/github.html +3 -3
  59. package/dist/docs/guides/human-in-the-loop.html +3 -3
  60. package/dist/docs/guides/mcp-oauth.html +3 -3
  61. package/dist/docs/guides/slack.html +11 -7
  62. package/dist/docs/guides/webhooks.html +138 -5
  63. package/dist/docs/hashmap.json +1 -1
  64. package/dist/docs/hillclimbing.html +3 -3
  65. package/dist/docs/index.html +5 -5
  66. package/dist/docs/quickstart.html +3 -3
  67. package/dist/docs/reference/agent-config.html +3 -3
  68. package/dist/docs/reference/channels.html +5 -5
  69. package/dist/docs/reference/cli.html +3 -3
  70. package/dist/docs/reference/connections.html +3 -3
  71. package/dist/docs/reference/hooks.html +3 -3
  72. package/dist/docs/reference/http-api.html +3 -3
  73. package/dist/docs/reference/instructions.html +3 -3
  74. package/dist/docs/reference/playground.html +3 -3
  75. package/dist/docs/reference/project-layout.html +3 -3
  76. package/dist/docs/reference/prompt.html +3 -3
  77. package/dist/docs/reference/schedules.html +3 -3
  78. package/dist/docs/reference/sessions.html +3 -3
  79. package/dist/docs/reference/skills.html +3 -3
  80. package/dist/docs/reference/subagents.html +3 -3
  81. package/dist/docs/reference/tools.html +3 -3
  82. package/dist/docs/scaffolding-agents.html +3 -3
  83. package/dist/docs/storage.html +3 -3
  84. package/dist/docs/troubleshooting.html +3 -3
  85. package/dist/internal/cli-ax.d.ts +8 -0
  86. package/dist/internal/cli-ax.d.ts.map +1 -1
  87. package/dist/internal/cli-ax.js +116 -57
  88. package/dist/internal/cursor-slack-relay.d.ts +2 -0
  89. package/dist/internal/cursor-slack-relay.d.ts.map +1 -1
  90. package/dist/internal/cursor-slack-relay.js +1 -0
  91. package/dist/internal/deploy-client.d.ts +2 -0
  92. package/dist/internal/deploy-client.d.ts.map +1 -1
  93. package/dist/internal/deploy-client.js +1 -0
  94. package/dist/internal/discovery.js +8 -1
  95. package/dist/internal/server.js +10 -5
  96. package/dist/playground/assets/{index-BDCeW4uc.js → index-CkN74U_L.js} +48 -47
  97. package/dist/playground/index.html +1 -1
  98. package/docs/README.md +1 -0
  99. package/docs/guides/slack.md +32 -7
  100. package/docs/guides/webhooks.md +183 -0
  101. package/docs/reference/channels.md +2 -1
  102. package/package.json +1 -1
  103. package/skills/setup-slack/SKILL.md +3 -0
  104. package/src/bin/agent-serve.ts +12 -3
  105. package/src/channels/slack/auth.ts +29 -22
  106. package/src/channels/slack/cursor-account.ts +2 -0
  107. package/src/channels/slack/defaults.ts +24 -4
  108. package/src/channels/slack/dispatch.ts +42 -1
  109. package/src/channels/slack/external-policy.ts +62 -0
  110. package/src/channels/slack/index.ts +1 -0
  111. package/src/channels/slack/interactive.ts +42 -10
  112. package/src/channels/slack/slack-channel.ts +20 -3
  113. package/src/channels/slack/types.ts +12 -0
  114. package/src/internal/cli-ax.ts +110 -34
  115. package/src/internal/cursor-slack-relay.ts +3 -0
  116. package/src/internal/deploy-client.ts +3 -0
  117. package/src/internal/discovery.ts +8 -1
  118. package/src/internal/server.ts +9 -5
  119. package/dist/docs/assets/chunks/@localSearchIndexroot.CQJytDwU.js +0 -1
  120. package/dist/docs/assets/guides_slack.md.D_xFKlnF.lean.js +0 -1
  121. package/dist/docs/assets/guides_webhooks.md.B1EswtUu.js +0 -50
  122. /package/dist/docs/assets/{index.md.Dfv5ic9t.lean.js → index.md.CH_s5uZe.lean.js} +0 -0
@@ -33,6 +33,7 @@ export * from "./dispatch.js";
33
33
  export * from "./doctor.js";
34
34
  export * from "./eval-directive.js";
35
35
  export * from "./event-id-dedupe.js";
36
+ export * from "./external-policy.js";
36
37
  export * from "./inbound.js";
37
38
  export * from "./init.js";
38
39
  export * from "./interactive.js";
@@ -20,6 +20,7 @@ import { updateMessage } from "./api.js";
20
20
  import { decisionFromActionId, parseApprovalActionValue } from "./approvals.js";
21
21
  import { buildSlackInteractiveApprovalAuth } from "./auth.js";
22
22
  import { dispatchInboundMessage } from "./dispatch.js";
23
+ import { isExternalSlackSender } from "./external-policy.js";
23
24
  import { defaultSlackLog, type SlackLog } from "./log.js";
24
25
  import type {
25
26
  SlackBlockAction,
@@ -41,6 +42,8 @@ export interface SlackInteractiveDispatchDeps {
41
42
  ) => Promise<PendingApproval>;
42
43
  waitUntil: (promise: Promise<unknown>) => void;
43
44
  log?: SlackLog;
45
+ /** Defaults to true. */
46
+ blockExternals?: boolean;
44
47
  }
45
48
 
46
49
  /**
@@ -116,11 +119,35 @@ export async function dispatchInteractiveFromPayload(
116
119
  }
117
120
 
118
121
  const teamId = attrs.team_id;
122
+ if (deps.blockExternals !== false && parsed.userId !== "") {
123
+ if (teamId === undefined || teamId === "") {
124
+ log.info("interactive approval dropped (missing team)", {
125
+ session: value.sessionId,
126
+ user: parsed.userId,
127
+ });
128
+ return true;
129
+ }
130
+ if (
131
+ await isExternalSlackSender({
132
+ botToken: deps.credentials?.botToken,
133
+ slackTeamId: teamId,
134
+ userId: parsed.userId,
135
+ })
136
+ ) {
137
+ log.info("interactive approval dropped (external sender)", {
138
+ session: value.sessionId,
139
+ user: parsed.userId,
140
+ team: teamId,
141
+ });
142
+ return true;
143
+ }
144
+ }
145
+
119
146
  const clickingAuth = buildSlackInteractiveApprovalAuth({
120
147
  sessionAuth,
121
148
  channelId: parsed.channelId || sessionChannel,
122
149
  threadTs: parsed.threadTs || sessionThread,
123
- ...(teamId === undefined || teamId === "" ? {} : { teamId }),
150
+ teamId: teamId === undefined || teamId === "" ? undefined : teamId,
124
151
  clickingUserId: parsed.userId,
125
152
  });
126
153
 
@@ -204,12 +231,9 @@ export function parseSlackBlockAction(
204
231
  : typeof user.name === "string"
205
232
  ? user.name
206
233
  : undefined;
207
- const teamId =
208
- typeof team.id === "string"
209
- ? team.id
210
- : typeof user.team_id === "string"
211
- ? user.team_id
212
- : undefined;
234
+ // Payload `team.id` only — never `user.team_id` (Connect home team).
235
+ // Membership gates must baseline the install workspace separately.
236
+ const teamId = typeof team.id === "string" ? team.id : undefined;
213
237
  const label =
214
238
  typeof action.text === "object" &&
215
239
  action.text !== null &&
@@ -278,6 +302,10 @@ export interface SlackBlockActionDispatchDeps extends SlackContinuationProbes {
278
302
  host: HostContext;
279
303
  artifacts: ArtifactsApi;
280
304
  log?: SlackLog;
305
+ /** Defaults to true. */
306
+ blockExternals?: boolean;
307
+ /** Install workspace from `auth.test`; baseline for the external gate. */
308
+ installTeamId?: string;
281
309
  }
282
310
 
283
311
  /**
@@ -285,7 +313,10 @@ export interface SlackBlockActionDispatchDeps extends SlackContinuationProbes {
285
313
  * `action_ts` (unique per click, never a real thread message), so ask-dedupe
286
314
  * claims and `<slack_thread>` exclusions stay per-click.
287
315
  */
288
- function blockActionAsMessage(action: SlackBlockAction): SlackMessage {
316
+ function blockActionAsMessage(
317
+ action: SlackBlockAction,
318
+ installTeamId: string | undefined
319
+ ): SlackMessage {
289
320
  const text = `[clicked: ${action.label ?? action.actionId}]`;
290
321
  return {
291
322
  text,
@@ -294,7 +325,7 @@ function blockActionAsMessage(action: SlackBlockAction): SlackMessage {
294
325
  ts: action.actionTs,
295
326
  threadTs: action.threadTs,
296
327
  channelId: action.channelId,
297
- teamId: action.teamId,
328
+ teamId: installTeamId,
298
329
  author: {
299
330
  userId: action.userId,
300
331
  userName: action.userName,
@@ -344,7 +375,7 @@ export async function dispatchBlockAction(
344
375
 
345
376
  await dispatchInboundMessage({
346
377
  kind: "block_action",
347
- message: blockActionAsMessage(action),
378
+ message: blockActionAsMessage(action, deps.installTeamId),
348
379
  handler: (ctx) => deps.handler(ctx, action),
349
380
  send: deps.send,
350
381
  credentials: deps.credentials,
@@ -353,6 +384,7 @@ export async function dispatchBlockAction(
353
384
  log,
354
385
  // Click text is synthetic — never treat it as a stop/eval directive.
355
386
  evalCommandsEnabled: false,
387
+ blockExternals: deps.blockExternals,
356
388
  hasContinuationSession: deps.hasContinuationSession,
357
389
  isContinuationBusy: deps.isContinuationBusy,
358
390
  getContinuationLastBotMessageTs: deps.getContinuationLastBotMessageTs,
@@ -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,
@@ -152,11 +153,14 @@ export function slackChannel<const Name extends string = string>(
152
153
  : (options.credentials ?? credentialsFromEnv(options.envPrefix));
153
154
  const envKeys = slackEnvKeys(options.envPrefix);
154
155
  const loadingMessages = options.loadingMessages;
155
- const defaultInbound = createDefaultOnInbound(loadingMessages);
156
+ const policy = options.policy;
157
+ const blockExternals = resolveSlackBlockExternals(policy);
158
+ const defaultInbound = createDefaultOnInbound(loadingMessages, policy);
156
159
  const onAppMention = options.onAppMention ?? defaultInbound;
157
160
  const onDirectMessage = options.onDirectMessage ?? defaultInbound;
158
161
  const onChannelPost =
159
- options.onChannelPost ?? createDefaultOnChannelPost(loadingMessages);
162
+ options.onChannelPost ??
163
+ createDefaultOnChannelPost(loadingMessages, policy);
160
164
  const engagement = options.engagement ?? {};
161
165
  const mentionsEnabled = engagement.mentions !== false;
162
166
  const directMessagesEnabled = engagement.directMessages !== false;
@@ -237,6 +241,8 @@ export function slackChannel<const Name extends string = string>(
237
241
  mentionsEnabled,
238
242
  directMessagesEnabled,
239
243
  evalCommandsEnabled,
244
+ // cursorAccount: backend router already refuses externals.
245
+ blockExternals: cursorAccount ? false : blockExternals,
240
246
  ...(args.hasContinuationSession === undefined
241
247
  ? {}
242
248
  : { hasContinuationSession: args.hasContinuationSession }),
@@ -329,10 +335,15 @@ export function slackChannel<const Name extends string = string>(
329
335
 
330
336
  let botUserId: string | undefined;
331
337
  let botId: string | undefined;
338
+ let installTeamId: string | undefined;
332
339
  let webClient: InstanceType<
333
340
  typeof import("@slack/web-api").WebClient
334
341
  > | null = null;
335
- if (channelPosts !== undefined || botMentions !== undefined) {
342
+ if (
343
+ channelPosts !== undefined ||
344
+ botMentions !== undefined ||
345
+ routeInteractive
346
+ ) {
336
347
  try {
337
348
  const { WebClient } = await import("@slack/web-api");
338
349
  webClient = new WebClient(botToken);
@@ -341,6 +352,8 @@ export function slackChannel<const Name extends string = string>(
341
352
  typeof identity.user_id === "string" ? identity.user_id : undefined;
342
353
  botId =
343
354
  typeof identity.bot_id === "string" ? identity.bot_id : undefined;
355
+ installTeamId =
356
+ typeof identity.team_id === "string" ? identity.team_id : undefined;
344
357
  } catch (error) {
345
358
  getLog().error("bot identity lookup degraded", {
346
359
  error: error instanceof Error ? error.message : String(error),
@@ -416,6 +429,7 @@ export function slackChannel<const Name extends string = string>(
416
429
  artifacts: args.artifacts,
417
430
  log: getLog(),
418
431
  evalCommandsEnabled,
432
+ blockExternals,
419
433
  ...(args.hasContinuationSession === undefined
420
434
  ? {}
421
435
  : {
@@ -491,6 +505,7 @@ export function slackChannel<const Name extends string = string>(
491
505
  resolveApproval: args.resolveApproval,
492
506
  waitUntil: args.waitUntil,
493
507
  log: getLog(),
508
+ blockExternals,
494
509
  }
495
510
  );
496
511
  if (handledAsApproval || onBlockAction === undefined) {
@@ -503,6 +518,8 @@ export function slackChannel<const Name extends string = string>(
503
518
  host: args.host,
504
519
  artifacts: args.artifacts,
505
520
  log: getLog(),
521
+ blockExternals,
522
+ installTeamId,
506
523
  hasContinuationSession: args.hasContinuationSession,
507
524
  isContinuationBusy: args.isContinuationBusy,
508
525
  getContinuationLastBotMessageTs:
@@ -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`
@@ -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(),
@@ -2000,7 +2000,14 @@ function isModuleFile(fileName: string): boolean {
2000
2000
  if (fileName.endsWith(".d.ts") || fileName.startsWith(".")) {
2001
2001
  return false;
2002
2002
  }
2003
- return MODULE_EXTENSIONS.has(extname(fileName));
2003
+ if (!MODULE_EXTENSIONS.has(extname(fileName))) {
2004
+ return false;
2005
+ }
2006
+ // Colocated unit tests (Vitest/Jest convention) are not definitions.
2007
+ // Without this, `channels/slack.test.ts` is scanned as a channel named
2008
+ // "slack.test", fails NAME_PATTERN, and the whole agent refuses to serve.
2009
+ const stem = moduleStem(fileName);
2010
+ return !stem.endsWith(".test") && !stem.endsWith(".spec");
2004
2011
  }
2005
2012
 
2006
2013
  function moduleStem(fileName: string): string {
@@ -1747,11 +1747,15 @@ async function startCursorSlackRelay(input: {
1747
1747
 
1748
1748
  // Optional fields may be undefined — the relay JSON-serializes the
1749
1749
  // registration body, which drops them.
1750
- const agents: CursorSlackAgentRegistration[] = targets.map((target) => ({
1751
- name: target.agentName,
1752
- iconUrl: target.accountRuntime.options.agentIcon?.url,
1753
- iconEmoji: target.accountRuntime.options.agentIcon?.emoji,
1754
- }));
1750
+ const agents: CursorSlackAgentRegistration[] = targets.map((target) => {
1751
+ const policy = target.accountRuntime.options.policy;
1752
+ return {
1753
+ name: target.agentName,
1754
+ iconUrl: target.accountRuntime.options.agentIcon?.url,
1755
+ iconEmoji: target.accountRuntime.options.agentIcon?.emoji,
1756
+ blockExternals: policy?.blockExternals === false ? false : undefined,
1757
+ };
1758
+ });
1755
1759
 
1756
1760
  const targetByName = new Map(
1757
1761
  targets.map((target) => [target.agentName.toLowerCase(), target])