@cursor/july 0.1.17 → 0.1.18

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 (135) hide show
  1. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  2. package/dist/channels/slack/cursor-account.js +1 -1
  3. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  4. package/dist/channels/slack/dispatch.js +4 -1
  5. package/dist/channels/slack/inbound.d.ts.map +1 -1
  6. package/dist/channels/slack/inbound.js +6 -1
  7. package/dist/channels/slack/index.d.ts +1 -0
  8. package/dist/channels/slack/index.d.ts.map +1 -1
  9. package/dist/channels/slack/index.js +1 -0
  10. package/dist/channels/slack/init.d.ts.map +1 -1
  11. package/dist/channels/slack/init.js +4 -1
  12. package/dist/channels/slack/interactive.d.ts.map +1 -1
  13. package/dist/channels/slack/interactive.js +1 -0
  14. package/dist/channels/slack/manifest.d.ts +10 -0
  15. package/dist/channels/slack/manifest.d.ts.map +1 -1
  16. package/dist/channels/slack/manifest.js +33 -14
  17. package/dist/channels/slack/message-body.d.ts +13 -0
  18. package/dist/channels/slack/message-body.d.ts.map +1 -0
  19. package/dist/channels/slack/message-body.js +82 -0
  20. package/dist/channels/slack/thread-context.d.ts +0 -2
  21. package/dist/channels/slack/thread-context.d.ts.map +1 -1
  22. package/dist/channels/slack/thread-context.js +1 -65
  23. package/dist/channels/slack/types.d.ts +16 -1
  24. package/dist/channels/slack/types.d.ts.map +1 -1
  25. package/dist/docs/404.html +2 -2
  26. package/dist/docs/ab.html +3 -3
  27. package/dist/docs/assets/{app.BbEXr2KO.js → app.BR5EdVSq.js} +1 -1
  28. package/dist/docs/assets/chunks/@localSearchIndexroot.BOzkeUi8.js +1 -0
  29. package/dist/docs/assets/chunks/{VPLocalSearchBox.DBEi4HDp.js → VPLocalSearchBox.CNCzfUKC.js} +1 -1
  30. package/dist/docs/assets/chunks/{theme.DGfptcHw.js → theme.CwKvMSWv.js} +2 -2
  31. package/dist/docs/assets/{example-agents_security-reviewer.md.BZSNycMf.js → example-agents_security-reviewer.md.Dhj_m7_B.js} +1 -1
  32. package/dist/docs/building-with-agents.html +3 -3
  33. package/dist/docs/concepts.html +3 -3
  34. package/dist/docs/deployment.html +3 -3
  35. package/dist/docs/evals.html +3 -3
  36. package/dist/docs/example-agents/approval-buddy.html +3 -3
  37. package/dist/docs/example-agents/benny.html +3 -3
  38. package/dist/docs/example-agents/bugbot.html +3 -3
  39. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  40. package/dist/docs/example-agents/codeowners-review.html +3 -3
  41. package/dist/docs/example-agents/concierge.html +3 -3
  42. package/dist/docs/example-agents/fsd.html +3 -3
  43. package/dist/docs/example-agents/index.html +3 -3
  44. package/dist/docs/example-agents/knowledge-base.html +3 -3
  45. package/dist/docs/example-agents/oncall.html +3 -3
  46. package/dist/docs/example-agents/security-reviewer.html +5 -5
  47. package/dist/docs/example-agents/slack-agent.html +3 -3
  48. package/dist/docs/example-agents/weather-agent.html +3 -3
  49. package/dist/docs/guides/agent-to-agent.html +3 -3
  50. package/dist/docs/guides/cloud-runtime.html +3 -3
  51. package/dist/docs/guides/github.html +3 -3
  52. package/dist/docs/guides/human-in-the-loop.html +3 -3
  53. package/dist/docs/guides/mcp-oauth.html +3 -3
  54. package/dist/docs/guides/slack.html +3 -3
  55. package/dist/docs/guides/webhooks.html +3 -3
  56. package/dist/docs/hashmap.json +1 -1
  57. package/dist/docs/hillclimbing.html +3 -3
  58. package/dist/docs/index.html +3 -3
  59. package/dist/docs/quickstart.html +3 -3
  60. package/dist/docs/reference/agent-config.html +3 -3
  61. package/dist/docs/reference/channels.html +3 -3
  62. package/dist/docs/reference/cli.html +3 -3
  63. package/dist/docs/reference/connections.html +3 -3
  64. package/dist/docs/reference/hooks.html +3 -3
  65. package/dist/docs/reference/http-api.html +3 -3
  66. package/dist/docs/reference/instructions.html +3 -3
  67. package/dist/docs/reference/playground.html +3 -3
  68. package/dist/docs/reference/project-layout.html +3 -3
  69. package/dist/docs/reference/prompt.html +3 -3
  70. package/dist/docs/reference/schedules.html +3 -3
  71. package/dist/docs/reference/sessions.html +3 -3
  72. package/dist/docs/reference/skills.html +3 -3
  73. package/dist/docs/reference/subagents.html +3 -3
  74. package/dist/docs/reference/tools.html +3 -3
  75. package/dist/docs/scaffolding-agents.html +3 -3
  76. package/dist/docs/storage.html +3 -3
  77. package/dist/docs/troubleshooting.html +3 -3
  78. package/dist/internal/builtin-tools/index.d.ts +10 -0
  79. package/dist/internal/builtin-tools/index.d.ts.map +1 -0
  80. package/dist/internal/builtin-tools/index.js +27 -0
  81. package/dist/internal/builtin-tools/reminders.d.ts +9 -0
  82. package/dist/internal/builtin-tools/reminders.d.ts.map +1 -0
  83. package/dist/internal/builtin-tools/reminders.js +138 -0
  84. package/dist/internal/cli-slack.d.ts.map +1 -1
  85. package/dist/internal/cli-slack.js +36 -15
  86. package/dist/internal/cursor-event-relay.d.ts +30 -0
  87. package/dist/internal/cursor-event-relay.d.ts.map +1 -1
  88. package/dist/internal/cursor-event-relay.js +37 -0
  89. package/dist/internal/discovery.d.ts.map +1 -1
  90. package/dist/internal/discovery.js +41 -0
  91. package/dist/internal/reminder-runner.d.ts +7 -0
  92. package/dist/internal/reminder-runner.d.ts.map +1 -1
  93. package/dist/internal/reminder-runner.js +21 -0
  94. package/dist/internal/sdk-runner.d.ts +15 -0
  95. package/dist/internal/sdk-runner.d.ts.map +1 -1
  96. package/dist/internal/sdk-runner.js +113 -13
  97. package/dist/internal/server.d.ts.map +1 -1
  98. package/dist/internal/server.js +1 -0
  99. package/dist/internal/session-engine.d.ts.map +1 -1
  100. package/dist/internal/session-engine.js +9 -0
  101. package/dist/internal/slack-provision-client.d.ts +6 -3
  102. package/dist/internal/slack-provision-client.d.ts.map +1 -1
  103. package/dist/internal/slack-provision-client.js +5 -3
  104. package/dist/playground/assets/{index-DNqirsrK.js → index-DRjS5klQ.js} +42 -42
  105. package/dist/playground/assets/index-TCDs-UhZ.css +1 -0
  106. package/dist/playground/index.html +2 -2
  107. package/dist/types.d.ts +28 -0
  108. package/dist/types.d.ts.map +1 -1
  109. package/docs/example-agents/security-reviewer.md +1 -1
  110. package/package.json +1 -1
  111. package/skills/setup-slack/SKILL.md +4 -0
  112. package/src/channels/slack/cursor-account.ts +1 -0
  113. package/src/channels/slack/dispatch.ts +3 -1
  114. package/src/channels/slack/inbound.ts +6 -1
  115. package/src/channels/slack/index.ts +1 -0
  116. package/src/channels/slack/init.ts +3 -0
  117. package/src/channels/slack/interactive.ts +1 -0
  118. package/src/channels/slack/manifest.ts +34 -6
  119. package/src/channels/slack/message-body.ts +90 -0
  120. package/src/channels/slack/thread-context.ts +1 -69
  121. package/src/channels/slack/types.ts +16 -1
  122. package/src/internal/builtin-tools/index.ts +40 -0
  123. package/src/internal/builtin-tools/reminders.ts +158 -0
  124. package/src/internal/cli-slack.ts +38 -14
  125. package/src/internal/cursor-event-relay.ts +67 -0
  126. package/src/internal/discovery.ts +52 -0
  127. package/src/internal/reminder-runner.ts +30 -0
  128. package/src/internal/sdk-runner.ts +131 -13
  129. package/src/internal/server.ts +2 -0
  130. package/src/internal/session-engine.ts +14 -0
  131. package/src/internal/slack-provision-client.ts +7 -3
  132. package/src/types.ts +33 -0
  133. package/dist/docs/assets/chunks/@localSearchIndexroot.DCBsrfwP.js +0 -1
  134. package/dist/playground/assets/index-CidizGZv.css +0 -1
  135. /package/dist/docs/assets/{example-agents_security-reviewer.md.BZSNycMf.lean.js → example-agents_security-reviewer.md.Dhj_m7_B.lean.js} +0 -0
@@ -0,0 +1,40 @@
1
+ /**
2
+ * Built-in tools (`AgentConfig.builtinTools`): framework-provided
3
+ * model-facing tools, materialized as ordinary server tools at discovery
4
+ * time so turns, direct calls, `info`, and the playground treat them
5
+ * exactly like authored tools.
6
+ */
7
+
8
+ import type {
9
+ BuiltinToolsConfig,
10
+ DiscoveredTool,
11
+ ToolDefinition,
12
+ } from "../../types.js";
13
+ import { toolInputJsonSchema } from "../json-schema.js";
14
+ import { reminderTools } from "./reminders.js";
15
+
16
+ /** Resolve the enabled built-in tools, in stable catalog order. */
17
+ export function builtinTools(
18
+ config: BuiltinToolsConfig | undefined
19
+ ): DiscoveredTool[] {
20
+ const tools: DiscoveredTool[] = [];
21
+ if (config?.reminders === true) {
22
+ tools.push(...toDiscovered(reminderTools));
23
+ }
24
+ return tools;
25
+ }
26
+
27
+ function toDiscovered(
28
+ byName: Record<string, ToolDefinition>
29
+ ): DiscoveredTool[] {
30
+ return Object.entries(byName).map(([name, definition]) => ({
31
+ name,
32
+ description: definition.description,
33
+ inputSchema: toolInputJsonSchema(definition.inputSchema),
34
+ execution: "server",
35
+ needsApproval:
36
+ definition.needsApproval === true ||
37
+ typeof definition.needsApproval === "function",
38
+ definition,
39
+ }));
40
+ }
@@ -0,0 +1,158 @@
1
+ /**
2
+ * Built-in reminder tools (`builtinTools: { reminders: true }`): durable
3
+ * wakes bound to the current conversation over `host.reminders`. The
4
+ * framework's reminder fire prompt tells the model to call
5
+ * `reminders_cancel` by name, so these tool names must not change.
6
+ */
7
+
8
+ import { z } from "zod";
9
+ import { defineTool } from "../../tools.js";
10
+ import type {
11
+ ReminderHostApi,
12
+ ReminderInfo,
13
+ ToolContext,
14
+ ToolDefinition,
15
+ } from "../../types.js";
16
+ import { parseDuration } from "../duration.js";
17
+
18
+ /** Floor for model-armed recurring wakes: each fire spends a model turn. */
19
+ const MIN_EVERY_MS = 60_000;
20
+
21
+ function api(ctx: ToolContext): ReminderHostApi {
22
+ const reminders = ctx.host.reminders;
23
+ if (reminders === undefined) {
24
+ throw new Error("Reminders are disabled on this host.");
25
+ }
26
+ return reminders;
27
+ }
28
+
29
+ function conversation(ctx: ToolContext): {
30
+ channelId: string;
31
+ continuationToken: string;
32
+ } {
33
+ const token = ctx.session.continuationKey;
34
+ if (token == null) {
35
+ throw new Error(
36
+ "This session is not bound to a followable conversation; reminders cannot wake it."
37
+ );
38
+ }
39
+ return { channelId: ctx.session.channelId, continuationToken: token };
40
+ }
41
+
42
+ /** Reminders visible to this conversation (tokens are channel-local). */
43
+ async function listMine(
44
+ ctx: ToolContext,
45
+ status?: ReminderInfo["status"]
46
+ ): Promise<ReminderInfo[]> {
47
+ const conv = conversation(ctx);
48
+ const all = await api(ctx).list({
49
+ status,
50
+ continuationToken: conv.continuationToken,
51
+ });
52
+ return all.filter((r) => r.channelId === conv.channelId);
53
+ }
54
+
55
+ function toResult(r: ReminderInfo) {
56
+ return {
57
+ reminderId: r.id,
58
+ purpose: r.purpose,
59
+ schedule: `${r.scheduleKind} ${r.scheduleValue}`,
60
+ nextFireAt: r.nextFireAt,
61
+ until: r.untilPrompt,
62
+ };
63
+ }
64
+
65
+ const create = defineTool({
66
+ description:
67
+ "Set a reminder for yourself in this conversation. When it fires, your prompt returns here as a follow-up turn. Use it to re-check state that needs time to settle. One-shot (delay) or recurring (every).",
68
+ inputSchema: z.object({
69
+ prompt: z
70
+ .string()
71
+ .min(1)
72
+ .describe(
73
+ "What to do when you wake. Keep it generic ('re-check CI and report only if the status changed') and re-read live state on wake instead of replaying stale details."
74
+ ),
75
+ delay: z
76
+ .string()
77
+ .optional()
78
+ .describe(
79
+ 'One-shot: wake once after this duration, e.g. "20m" or "2h". Provide exactly one of delay or every.'
80
+ ),
81
+ every: z
82
+ .string()
83
+ .optional()
84
+ .describe(
85
+ 'Recurring: wake at this interval, e.g. "30m", until cancelled or the stop condition is met. Provide exactly one of delay or every.'
86
+ ),
87
+ purpose: z
88
+ .string()
89
+ .min(1)
90
+ .max(64)
91
+ .optional()
92
+ .describe(
93
+ 'Short slug naming the reminder, e.g. "ci_recheck". Defaults to "follow_up".'
94
+ ),
95
+ until: z
96
+ .string()
97
+ .optional()
98
+ .describe(
99
+ "Recurring only: plain-language stop condition you will honor by calling reminders_cancel, e.g. 'Cancel once CI is green or the PR is merged.'"
100
+ ),
101
+ }),
102
+ async execute(input, ctx) {
103
+ if ((input.delay !== undefined) === (input.every !== undefined)) {
104
+ throw new Error("Provide exactly one of delay or every.");
105
+ }
106
+ if (
107
+ input.every !== undefined &&
108
+ parseDuration(input.every) < MIN_EVERY_MS
109
+ ) {
110
+ throw new Error('Recurring reminders must use every >= "1m".');
111
+ }
112
+ const conv = conversation(ctx);
113
+ const reminder = await api(ctx).create({
114
+ purpose: input.purpose ?? "follow_up",
115
+ channelId: conv.channelId,
116
+ continuationToken: conv.continuationToken,
117
+ prompt: input.prompt,
118
+ delay: input.delay,
119
+ every: input.every,
120
+ until: input.until,
121
+ });
122
+ return toResult(reminder);
123
+ },
124
+ });
125
+
126
+ const list = defineTool({
127
+ description:
128
+ "List the armed reminders bound to this conversation. Check before arming duplicates.",
129
+ inputSchema: z.object({}),
130
+ async execute(_input, ctx) {
131
+ return { reminders: (await listMine(ctx, "armed")).map(toResult) };
132
+ },
133
+ });
134
+
135
+ const cancel = defineTool({
136
+ description:
137
+ "Cancel one of this conversation's reminders by id, the moment its stop condition is met or it is no longer needed.",
138
+ inputSchema: z.object({
139
+ id: z
140
+ .string()
141
+ .min(1)
142
+ .describe("Reminder id from reminders_create or reminders_list."),
143
+ }),
144
+ async execute(input, ctx) {
145
+ const mine = (await listMine(ctx)).find((r) => r.id === input.id);
146
+ if (mine === undefined) {
147
+ throw new Error(`No reminder "${input.id}" in this conversation.`);
148
+ }
149
+ const cancelled = await api(ctx).cancel(input.id);
150
+ return { reminderId: input.id, status: cancelled?.status ?? "cancelled" };
151
+ },
152
+ });
153
+
154
+ export const reminderTools: Record<string, ToolDefinition> = {
155
+ reminders_create: create,
156
+ reminders_list: list,
157
+ reminders_cancel: cancel,
158
+ };
@@ -198,6 +198,9 @@ async function runManifest(options: SlackCliOptions): Promise<number> {
198
198
  const name = options.name ?? basename(dir);
199
199
  const pair = buildSlackManifestPair({
200
200
  name,
201
+ // These files are pasted into api.slack.com by hand, so they must not
202
+ // carry the manager-token-only managed_app_settings block.
203
+ managedByCursor: false,
201
204
  ...(options.channelPosts === true ? { channelPosts: true } : {}),
202
205
  });
203
206
 
@@ -488,6 +491,14 @@ interface ConnectWaitTarget {
488
491
  notConnectedAt?: string;
489
492
  }
490
493
 
494
+ /** "Name (T0123ABCD)", or the bare id when the backend has no name. */
495
+ function formatWorkspace(workspace: SlackConnectedWorkspace): string {
496
+ const name = workspace.slackTeamName ?? "";
497
+ return name === ""
498
+ ? workspace.slackTeamId
499
+ : `${name} (${workspace.slackTeamId})`;
500
+ }
501
+
491
502
  function matchConnectedWorkspace(
492
503
  workspaces: SlackConnectedWorkspace[],
493
504
  wait: ConnectWaitTarget
@@ -522,7 +533,9 @@ async function runConnectFlow(
522
533
  return renderSlackApiError(error, ctx.err);
523
534
  }
524
535
  ctx.err(
525
- `\nConnect your Slack workspace to Cursor — approve in the browser:\n\n ${url}\n\n(waiting for the Slack consent to complete, up to ${Math.round(ctx.pollTimeoutMs / 60_000)} minutes)\n`
536
+ `\nConnect your Slack workspace to Cursor — approve in the browser:\n\n ${url}\n\n` +
537
+ `After approving in Slack you land on cursor.com, which links the workspace to your Cursor account — sign in there if the browser is not already signed in as you.\n\n` +
538
+ `(waiting for the Slack consent to complete, up to ${Math.round(ctx.pollTimeoutMs / 60_000)} minutes)\n`
526
539
  );
527
540
  ctx.openBrowserImpl(url);
528
541
  const deadline = Date.now() + ctx.pollTimeoutMs;
@@ -559,7 +572,7 @@ async function runConnectFlow(
559
572
  }
560
573
  if (Date.now() >= deadline) {
561
574
  ctx.err(
562
- `Timed out waiting for the Slack workspace connection. The consent link stays valid for ~10 minutes — approve it and re-run \`${CLI} slack create\`.\n`
575
+ `Timed out waiting for the Slack workspace connection. The consent link stays valid for ~10 minutes — approve it, finish the sign-in on cursor.com, then re-run \`${CLI} slack create\`.\n`
563
576
  );
564
577
  return 1;
565
578
  }
@@ -611,7 +624,7 @@ export async function cmdSlackCreate(
611
624
  status.workspaces
612
625
  .map(
613
626
  (candidate) =>
614
- ` - ${candidate.slackTeamId} (Slack user ${candidate.slackUserId}, connected ${candidate.connectedAt})\n`
627
+ ` - ${formatWorkspace(candidate)} (Slack user ${candidate.slackUserId}, connected ${candidate.connectedAt})\n`
615
628
  )
616
629
  .join("") +
617
630
  `Pass --slack-team <id> to pick one.\n`
@@ -647,8 +660,8 @@ export async function cmdSlackCreate(
647
660
  });
648
661
  const manifestJson = JSON.stringify(manifest);
649
662
 
650
- // One connect-and-retry for not_connected / insufficient_scopes (both
651
- // are fixed by a fresh consent grant).
663
+ // One connect-and-retry for not_connected / insufficient_scopes /
664
+ // manager_reauth_required (all fixed by a fresh consent grant).
652
665
  let result: SlackAppProvisionResult;
653
666
  let connectRetried = false;
654
667
  for (;;) {
@@ -667,12 +680,14 @@ export async function cmdSlackCreate(
667
680
  if (
668
681
  error instanceof SlackProvisionApiError &&
669
682
  (error.code === "not_connected" ||
670
- error.code === "insufficient_scopes") &&
683
+ error.code === "insufficient_scopes" ||
684
+ error.code === "manager_reauth_required") &&
671
685
  !connectRetried
672
686
  ) {
673
687
  connectRetried = true;
674
- // Both codes arrive holding a row that GET /status already lists, so
675
- // the poll waits for a fresh grant instead of re-matching that row.
688
+ // All three codes arrive holding a row that GET /status already
689
+ // lists, so the poll waits for a fresh grant instead of re-matching
690
+ // that row.
676
691
  const wait: ConnectWaitTarget = {
677
692
  slackTeamId: workspace.slackTeamId,
678
693
  notConnectedAt: workspace.connectedAt,
@@ -681,6 +696,10 @@ export async function cmdSlackCreate(
681
696
  ctx.err(
682
697
  `The Slack connection is missing scopes provisioning needs; approving the consent link again grants them.\n`
683
698
  );
699
+ } else if (error.code === "manager_reauth_required") {
700
+ ctx.err(
701
+ `Slack revoked Cursor's authorization for workspace ${workspace.slackTeamId}; re-approving the consent link restores it.\n`
702
+ );
684
703
  } else {
685
704
  ctx.err(
686
705
  `The Slack workspace is not connected to Cursor; starting the connect flow.\n`
@@ -1119,8 +1138,7 @@ function renderCreateProvisionError(
1119
1138
  );
1120
1139
  return 1;
1121
1140
  default:
1122
- err(`${error.message}\n`);
1123
- return 1;
1141
+ return renderSlackApiError(error, err);
1124
1142
  }
1125
1143
  }
1126
1144
 
@@ -1128,11 +1146,17 @@ function renderSlackApiError(
1128
1146
  error: unknown,
1129
1147
  err: (text: string) => void
1130
1148
  ): number {
1131
- if (error instanceof SlackProvisionApiError) {
1132
- err(`${error.message}\n`);
1133
- return 1;
1149
+ if (!(error instanceof SlackProvisionApiError)) {
1150
+ throw error;
1134
1151
  }
1135
- throw error;
1152
+ err(
1153
+ error.code === "manager_reauth_required"
1154
+ ? // There is no standalone connect command: create runs the consent
1155
+ // flow before provisioning.
1156
+ `${error.message}\nRe-approve the Slack consent by re-running \`${CLI} slack create\`, which reconnects the workspace first.\n`
1157
+ : `${error.message}\n`
1158
+ );
1159
+ return 1;
1136
1160
  }
1137
1161
 
1138
1162
  async function fileExists(path: string): Promise<boolean> {
@@ -18,6 +18,29 @@ import {
18
18
 
19
19
  export { RelayOffsetStore };
20
20
 
21
+ /**
22
+ * Comment snapshot from scm-events compact metadata. Carries what the GitHub
23
+ * channel's `issue_comment` / `pull_request_review_comment` Zod schemas
24
+ * require so relayed comment wakes parse without re-fetching the comment.
25
+ */
26
+ export type CursorScmCommentMeta = {
27
+ id: number;
28
+ body: string;
29
+ htmlUrl: string;
30
+ authorLogin?: string;
31
+ /**
32
+ * GitHub `user.type` ("User", "Bot", …). Carried onto the stubbed
33
+ * `comment.user` / `sender` so `isGitHubAppBotAuthor` keeps recognizing
34
+ * the agent's own comments on the relay path (self/bot-loop gate).
35
+ */
36
+ authorType?: string;
37
+ /** Review comments only. */
38
+ path?: string;
39
+ line?: number;
40
+ pullRequestReviewId?: number;
41
+ inReplyToId?: number;
42
+ };
43
+
21
44
  /** PR snapshot from scm-events compact metadata (Sand-style; no raw body). */
22
45
  export type CursorScmPullRequestMeta = {
23
46
  number: number;
@@ -53,6 +76,11 @@ export interface CursorScmEvent {
53
76
  * stub a Zod-valid GitHub `pull_request` webhook body.
54
77
  */
55
78
  pullRequest?: CursorScmPullRequestMeta;
79
+ /**
80
+ * Comment snapshot when the backend saw `body.comment`. Enough to stub a
81
+ * Zod-valid `issue_comment` / `pull_request_review_comment` webhook body.
82
+ */
83
+ comment?: CursorScmCommentMeta;
56
84
  /** GitHub-side event time from the payload, when present. */
57
85
  occurredAtMs?: number;
58
86
  }
@@ -114,6 +142,8 @@ export function formatCursorScmEventLog(event: {
114
142
  * Minimal GitHub webhook body from an SCM-events metadata envelope.
115
143
  * When `pullRequest` has title + head/base refs+shas, emits a Zod-valid
116
144
  * `pull_request` object for `onPullRequest`; otherwise identity-only stubs.
145
+ * When `comment` is present, emits a Zod-valid `comment` object so relayed
146
+ * `issue_comment` / `pull_request_review_comment` deliveries dispatch.
117
147
  */
118
148
  export function scmMetadataToGitHubPayload(event: {
119
149
  repo: string;
@@ -121,6 +151,7 @@ export function scmMetadataToGitHubPayload(event: {
121
151
  prNumber?: number;
122
152
  prUrl?: string;
123
153
  pullRequest?: CursorScmPullRequestMeta;
154
+ comment?: CursorScmCommentMeta;
124
155
  }): Record<string, unknown> {
125
156
  const slash = event.repo.indexOf("/");
126
157
  const owner = slash === -1 ? "" : event.repo.slice(0, slash);
@@ -143,6 +174,42 @@ export function scmMetadataToGitHubPayload(event: {
143
174
  };
144
175
  payload.action = event.action;
145
176
 
177
+ const commentMeta = event.comment;
178
+ if (commentMeta !== undefined) {
179
+ const comment: Record<string, unknown> = {
180
+ id: commentMeta.id,
181
+ body: commentMeta.body,
182
+ html_url: commentMeta.htmlUrl,
183
+ };
184
+ if (commentMeta.authorLogin !== undefined) {
185
+ const author: Record<string, unknown> = {
186
+ login: commentMeta.authorLogin,
187
+ };
188
+ if (commentMeta.authorType !== undefined) {
189
+ // Without user.type the isGitHubAppBotAuthor gate fails open and the
190
+ // agent's own relayed comments could wake it again (bot loop).
191
+ author.type = commentMeta.authorType;
192
+ }
193
+ comment.user = author;
194
+ // The comment author is the closest thing the envelope has to the
195
+ // webhook sender; channels use it for self/bot-loop gates.
196
+ payload.sender = { ...author };
197
+ }
198
+ if (commentMeta.path !== undefined) {
199
+ comment.path = commentMeta.path;
200
+ }
201
+ if (commentMeta.line !== undefined) {
202
+ comment.line = commentMeta.line;
203
+ }
204
+ if (commentMeta.pullRequestReviewId !== undefined) {
205
+ comment.pull_request_review_id = commentMeta.pullRequestReviewId;
206
+ }
207
+ if (commentMeta.inReplyToId !== undefined) {
208
+ comment.in_reply_to_id = commentMeta.inReplyToId;
209
+ }
210
+ payload.comment = comment;
211
+ }
212
+
146
213
  const title = prMeta?.title;
147
214
  const headRef = prMeta?.headRef;
148
215
  const headSha = prMeta?.headSha;
@@ -26,6 +26,8 @@ import {
26
26
  type AgentHostingOptions,
27
27
  type AgentProject,
28
28
  type AgentProjectInfo,
29
+ type AgentRuntime,
30
+ type BuiltinToolsConfig,
29
31
  type ChannelDefinition,
30
32
  type ConnectionDefinition,
31
33
  type ConnectionTransport,
@@ -56,6 +58,7 @@ import {
56
58
  import { registerAuthoredModuleLoaders } from "./authored-loaders.js";
57
59
  import { normalizePositiveInt } from "./bounded-int.js";
58
60
  import { getDefinitionKind } from "./brand.js";
61
+ import { builtinTools } from "./builtin-tools/index.js";
59
62
  import { isValidCron } from "./cron.js";
60
63
  import { parseFrontmatter } from "./frontmatter.js";
61
64
  import {
@@ -341,6 +344,13 @@ async function loadAgent(
341
344
  // warn that they exist.
342
345
  if (!options.isRoot) {
343
346
  await warnUnsupportedSubagentSlots(ctx, agentDir);
347
+ if (config?.builtinTools !== undefined) {
348
+ ctx.diagnostics.push({
349
+ severity: "warning",
350
+ path: displayPath(ctx, join(agentDir, "agent.ts")),
351
+ message: "builtinTools is not supported on subagents; ignoring.",
352
+ });
353
+ }
344
354
  return {
345
355
  name,
346
356
  ...(config?.description === undefined
@@ -401,6 +411,8 @@ async function loadAgent(
401
411
  });
402
412
  }
403
413
 
414
+ appendBuiltinTools(ctx, agentDir, tools, config?.builtinTools, runtime);
415
+
404
416
  return {
405
417
  name,
406
418
  description: config?.description,
@@ -1161,6 +1173,46 @@ async function warnUnsupportedSubagentSlots(
1161
1173
  }
1162
1174
  }
1163
1175
 
1176
+ /**
1177
+ * Append enabled built-in tools after authored tools. Authored tools with
1178
+ * the same name win; built-ins are server tools, so cloud runtimes get a
1179
+ * warning instead of tools.
1180
+ */
1181
+ function appendBuiltinTools(
1182
+ ctx: DiscoveryContext,
1183
+ agentDir: string,
1184
+ tools: DiscoveredTool[],
1185
+ config: BuiltinToolsConfig | undefined,
1186
+ runtime: AgentRuntime
1187
+ ): void {
1188
+ const builtin = builtinTools(config);
1189
+ if (builtin.length === 0) {
1190
+ return;
1191
+ }
1192
+ const agentPath = displayPath(ctx, join(agentDir, "agent.ts"));
1193
+ if (runtime === "cloud") {
1194
+ ctx.diagnostics.push({
1195
+ severity: "warning",
1196
+ path: agentPath,
1197
+ message:
1198
+ "builtinTools are in-process server tools and are not available on cloud agents; they are skipped.",
1199
+ });
1200
+ return;
1201
+ }
1202
+ const authored = new Set(tools.map((tool) => tool.name));
1203
+ for (const tool of builtin) {
1204
+ if (authored.has(tool.name)) {
1205
+ ctx.diagnostics.push({
1206
+ severity: "warning",
1207
+ path: agentPath,
1208
+ message: `Authored tool "${tool.name}" shadows the built-in tool of the same name; the authored tool wins.`,
1209
+ });
1210
+ continue;
1211
+ }
1212
+ tools.push(tool);
1213
+ }
1214
+ }
1215
+
1164
1216
  function warnCloudRuntimeLimitations(
1165
1217
  ctx: DiscoveryContext,
1166
1218
  agentDir: string,
@@ -336,6 +336,36 @@ export class ReminderRunner {
336
336
  return n;
337
337
  }
338
338
 
339
+ /**
340
+ * Re-key armed reminders when a channel rotates a session's continuation
341
+ * token (the built-in HTTP channel rotates after every follow-up). Without
342
+ * this, a reminder armed before the rotation misses its session at fire
343
+ * time and disarms as `session_gone`.
344
+ */
345
+ async rekeyContinuation(
346
+ channelId: string,
347
+ oldToken: string,
348
+ newToken: string
349
+ ): Promise<number> {
350
+ const armed = await this.store.listArmed();
351
+ let n = 0;
352
+ for (const record of armed) {
353
+ if (
354
+ record.channelId !== channelId ||
355
+ record.continuationToken !== oldToken
356
+ ) {
357
+ continue;
358
+ }
359
+ await this.persist({
360
+ ...record,
361
+ continuationToken: newToken,
362
+ updatedAt: new Date().toISOString(),
363
+ });
364
+ n += 1;
365
+ }
366
+ return n;
367
+ }
368
+
339
369
  /** Fire one reminder immediately (dev / hillclimb). */
340
370
  async dispatch(reminderId: string): Promise<{
341
371
  reminderId: string;