@cursor/july 0.1.33 → 0.1.34

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 (164) hide show
  1. package/README.md +1 -1
  2. package/dist/artifacts.d.ts.map +1 -1
  3. package/dist/artifacts.js +0 -5
  4. package/dist/bin/agent-serve.d.ts +1 -0
  5. package/dist/bin/agent-serve.d.ts.map +1 -1
  6. package/dist/bin/agent-serve.js +32 -2
  7. package/dist/channels/github/types.d.ts +10 -7
  8. package/dist/channels/github/types.d.ts.map +1 -1
  9. package/dist/docs/404.html +2 -2
  10. package/dist/docs/ab.html +3 -3
  11. package/dist/docs/assets/{app.CgZqLyWL.js → app.FPupl4SP.js} +1 -1
  12. package/dist/docs/assets/chunks/@localSearchIndexroot.WoYunhnT.js +1 -0
  13. package/dist/docs/assets/chunks/{VPLocalSearchBox.C4KigoQn.js → VPLocalSearchBox.Cd182Cu0.js} +1 -1
  14. package/dist/docs/assets/chunks/{theme.C_Ty4yT7.js → theme.BEM3Okcd.js} +2 -2
  15. package/dist/docs/assets/{example-agents_weather-agent.md.Dmrcphhl.js → example-agents_weather-agent.md.BADkPqxQ.js} +6 -4
  16. package/dist/docs/assets/example-agents_weather-agent.md.BADkPqxQ.lean.js +1 -0
  17. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.js +9 -0
  18. package/dist/docs/assets/guides_cloud-runtime.md.CDJGvVC4.lean.js +1 -0
  19. package/dist/docs/building-with-agents.html +3 -3
  20. package/dist/docs/concepts.html +3 -3
  21. package/dist/docs/deployment.html +3 -3
  22. package/dist/docs/evals.html +3 -3
  23. package/dist/docs/example-agents/approval-buddy.html +3 -3
  24. package/dist/docs/example-agents/benny.html +3 -3
  25. package/dist/docs/example-agents/bugbot.html +3 -3
  26. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  27. package/dist/docs/example-agents/codeowners-review.html +3 -3
  28. package/dist/docs/example-agents/concierge.html +3 -3
  29. package/dist/docs/example-agents/fsd.html +3 -3
  30. package/dist/docs/example-agents/index.html +3 -3
  31. package/dist/docs/example-agents/knowledge-base.html +3 -3
  32. package/dist/docs/example-agents/oncall.html +3 -3
  33. package/dist/docs/example-agents/security-reviewer.html +3 -3
  34. package/dist/docs/example-agents/slack-agent.html +3 -3
  35. package/dist/docs/example-agents/weather-agent.html +10 -8
  36. package/dist/docs/guides/agent-to-agent.html +3 -3
  37. package/dist/docs/guides/cloud-runtime.html +5 -5
  38. package/dist/docs/guides/github.html +3 -3
  39. package/dist/docs/guides/human-in-the-loop.html +3 -3
  40. package/dist/docs/guides/mcp-oauth.html +3 -3
  41. package/dist/docs/guides/slack.html +3 -3
  42. package/dist/docs/guides/webhooks.html +3 -3
  43. package/dist/docs/hashmap.json +1 -1
  44. package/dist/docs/hillclimbing.html +3 -3
  45. package/dist/docs/index.html +3 -3
  46. package/dist/docs/quickstart.html +3 -3
  47. package/dist/docs/reference/agent-config.html +3 -3
  48. package/dist/docs/reference/channels.html +3 -3
  49. package/dist/docs/reference/cli.html +3 -3
  50. package/dist/docs/reference/connections.html +3 -3
  51. package/dist/docs/reference/hooks.html +3 -3
  52. package/dist/docs/reference/http-api.html +3 -3
  53. package/dist/docs/reference/instructions.html +3 -3
  54. package/dist/docs/reference/playground.html +3 -3
  55. package/dist/docs/reference/project-layout.html +3 -3
  56. package/dist/docs/reference/prompt.html +3 -3
  57. package/dist/docs/reference/schedules.html +3 -3
  58. package/dist/docs/reference/sessions.html +3 -3
  59. package/dist/docs/reference/skills.html +3 -3
  60. package/dist/docs/reference/subagents.html +3 -3
  61. package/dist/docs/reference/tools.html +3 -3
  62. package/dist/docs/scaffolding-agents.html +3 -3
  63. package/dist/docs/storage.html +3 -3
  64. package/dist/docs/troubleshooting.html +3 -3
  65. package/dist/files-backends/agent-store-presigned-url.d.ts +3 -11
  66. package/dist/files.d.ts +20 -0
  67. package/dist/files.d.ts.map +1 -1
  68. package/dist/files.js +20 -0
  69. package/dist/index.d.ts +2 -2
  70. package/dist/index.d.ts.map +1 -1
  71. package/dist/index.js +2 -2
  72. package/dist/internal/artifacts-store.d.ts +14 -6
  73. package/dist/internal/artifacts-store.d.ts.map +1 -1
  74. package/dist/internal/artifacts-store.js +160 -16
  75. package/dist/internal/cli-convert-automation.d.ts +39 -0
  76. package/dist/internal/cli-convert-automation.d.ts.map +1 -0
  77. package/dist/internal/cli-convert-automation.js +280 -0
  78. package/dist/internal/cli-deploy.d.ts +2 -0
  79. package/dist/internal/cli-deploy.d.ts.map +1 -1
  80. package/dist/internal/cli-deploy.js +1 -1
  81. package/dist/internal/cli-mcp-oauth.d.ts +23 -0
  82. package/dist/internal/cli-mcp-oauth.d.ts.map +1 -1
  83. package/dist/internal/cli-mcp-oauth.js +57 -33
  84. package/dist/internal/convert-automation/convert-workflow.d.ts +10 -0
  85. package/dist/internal/convert-automation/convert-workflow.d.ts.map +1 -0
  86. package/dist/internal/convert-automation/convert-workflow.js +998 -0
  87. package/dist/internal/convert-automation/index.d.ts +3 -0
  88. package/dist/internal/convert-automation/index.d.ts.map +1 -0
  89. package/dist/internal/convert-automation/index.js +1 -0
  90. package/dist/internal/convert-automation/slug.d.ts +11 -0
  91. package/dist/internal/convert-automation/slug.d.ts.map +1 -0
  92. package/dist/internal/convert-automation/slug.js +36 -0
  93. package/dist/internal/convert-automation/types.d.ts +191 -0
  94. package/dist/internal/convert-automation/types.d.ts.map +1 -0
  95. package/dist/internal/convert-automation/types.js +13 -0
  96. package/dist/internal/deploy-client.d.ts +31 -0
  97. package/dist/internal/deploy-client.d.ts.map +1 -1
  98. package/dist/internal/deploy-client.js +48 -0
  99. package/dist/internal/discovery.d.ts.map +1 -1
  100. package/dist/internal/discovery.js +3 -32
  101. package/dist/internal/init-project.d.ts +1 -0
  102. package/dist/internal/init-project.d.ts.map +1 -1
  103. package/dist/internal/init-project.js +1 -1
  104. package/dist/internal/mcp-endpoint.d.ts +6 -0
  105. package/dist/internal/mcp-endpoint.d.ts.map +1 -1
  106. package/dist/internal/mcp-endpoint.js +168 -0
  107. package/dist/internal/resolved-connections.d.ts +15 -0
  108. package/dist/internal/resolved-connections.d.ts.map +1 -1
  109. package/dist/internal/resolved-connections.js +12 -5
  110. package/dist/internal/server.d.ts.map +1 -1
  111. package/dist/internal/server.js +75 -7
  112. package/dist/internal/session-engine.d.ts +56 -3
  113. package/dist/internal/session-engine.d.ts.map +1 -1
  114. package/dist/internal/session-engine.js +252 -83
  115. package/dist/internal/storage-coordinator.d.ts +0 -15
  116. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  117. package/dist/internal/storage-coordinator.js +0 -52
  118. package/dist/internal/workspace.d.ts +10 -5
  119. package/dist/internal/workspace.d.ts.map +1 -1
  120. package/dist/internal/workspace.js +13 -0
  121. package/dist/memory.d.ts +22 -6
  122. package/dist/memory.d.ts.map +1 -1
  123. package/dist/memory.js +100 -8
  124. package/dist/playground/assets/{index-CVlCAcUx.js → index-CDDWw0YX.js} +46 -46
  125. package/dist/playground/assets/index-MVuNTd8v.css +1 -0
  126. package/dist/playground/index.html +2 -2
  127. package/dist/storage.d.ts +0 -4
  128. package/dist/storage.d.ts.map +1 -1
  129. package/dist/storage.js +0 -4
  130. package/dist/types.d.ts +18 -0
  131. package/dist/types.d.ts.map +1 -1
  132. package/docs/example-agents/weather-agent.md +29 -11
  133. package/docs/guides/cloud-runtime.md +6 -4
  134. package/package.json +3 -1
  135. package/src/artifacts.ts +0 -6
  136. package/src/bin/agent-serve.ts +36 -2
  137. package/src/channels/github/types.ts +10 -7
  138. package/src/files.ts +27 -0
  139. package/src/index.ts +6 -1
  140. package/src/internal/artifacts-store.ts +185 -22
  141. package/src/internal/cli-convert-automation.ts +352 -0
  142. package/src/internal/cli-deploy.ts +1 -1
  143. package/src/internal/cli-mcp-oauth.ts +69 -31
  144. package/src/internal/convert-automation/convert-workflow.ts +1422 -0
  145. package/src/internal/convert-automation/index.ts +10 -0
  146. package/src/internal/convert-automation/slug.ts +38 -0
  147. package/src/internal/convert-automation/types.ts +199 -0
  148. package/src/internal/deploy-client.ts +78 -0
  149. package/src/internal/discovery.ts +3 -37
  150. package/src/internal/init-project.ts +1 -1
  151. package/src/internal/mcp-endpoint.ts +243 -0
  152. package/src/internal/resolved-connections.ts +32 -6
  153. package/src/internal/server.ts +99 -1
  154. package/src/internal/session-engine.ts +329 -99
  155. package/src/internal/storage-coordinator.ts +0 -62
  156. package/src/internal/workspace.ts +19 -5
  157. package/src/memory.ts +126 -10
  158. package/src/storage.ts +0 -6
  159. package/src/types.ts +15 -0
  160. package/dist/docs/assets/chunks/@localSearchIndexroot.B1uHGxBZ.js +0 -1
  161. package/dist/docs/assets/example-agents_weather-agent.md.Dmrcphhl.lean.js +0 -1
  162. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.js +0 -9
  163. package/dist/docs/assets/guides_cloud-runtime.md.BZ2GA7Es.lean.js +0 -1
  164. package/dist/playground/assets/index-BRhBbnd4.css +0 -1
@@ -0,0 +1,998 @@
1
+ /**
2
+ * Pure Workflow → agentkit project converter.
3
+ *
4
+ * Maps an exported Cursor automation (proto3 workflow JSON + enriched MCP
5
+ * server list) to project files, warnings, and setup steps. No I/O, no
6
+ * clock — `exportedAt` comes from the caller.
7
+ */
8
+ import { CLI_COMMAND_NAME, PACKAGE_NAME } from "../distribution.js";
9
+ import { pascalCase, SlugRegistry, slugify } from "./slug.js";
10
+ export function convertWorkflow(input) {
11
+ var _a, _b;
12
+ var _c, _d;
13
+ const { automation, workflow, mcpServers, exportedAt } = input;
14
+ const out = {
15
+ files: [],
16
+ warnings: [],
17
+ setupSteps: [],
18
+ mcpConnections: [],
19
+ };
20
+ if (workflow.managedConfig) {
21
+ out.warnings.push("This is a Cursor-managed automation: its behavior lives in managed configuration that is not exported. The exported project will NOT reproduce what this automation does.");
22
+ }
23
+ // Pin the model the prompt actually ran with; the workflow-level model is
24
+ // only a fallback. Silently swapping models changes cost and behavior.
25
+ const promptModel = (_b = (_a = workflow.prompts) === null || _a === void 0 ? void 0 : _a[0]) === null || _b === void 0 ? void 0 : _b.model;
26
+ const pinnedModel = promptModel !== undefined && promptModel !== ""
27
+ ? promptModel
28
+ : workflow.model;
29
+ if (promptModel !== undefined &&
30
+ promptModel !== "" &&
31
+ workflow.model !== undefined &&
32
+ workflow.model !== "" &&
33
+ promptModel !== workflow.model) {
34
+ out.warnings.push(`The automation ran its prompt with model ${JSON.stringify(promptModel)} (pinned in agent/agent.ts); its workflow-level default was ${JSON.stringify(workflow.model)}.`);
35
+ }
36
+ out.files.push({
37
+ path: "agent/instructions.md",
38
+ contents: renderInstructions(automation, workflow, exportedAt, out.warnings),
39
+ });
40
+ out.files.push({
41
+ path: "agent/agent.ts",
42
+ contents: renderAgentTs(automation, workflow, pinnedModel),
43
+ });
44
+ const triggers = (_c = workflow.triggers) !== null && _c !== void 0 ? _c : [];
45
+ const scheduleSlugs = new SlugRegistry();
46
+ const channelSlugs = new SlugRegistry();
47
+ const gitTriggers = [];
48
+ const slackTriggers = [];
49
+ const boilerplate = [];
50
+ for (const trigger of triggers) {
51
+ if (trigger.cron !== undefined) {
52
+ emitSchedule(automation, trigger, scheduleSlugs, out);
53
+ }
54
+ else if (trigger.git !== undefined) {
55
+ gitTriggers.push(trigger);
56
+ }
57
+ else if (isSlackTrigger(trigger)) {
58
+ slackTriggers.push(trigger);
59
+ }
60
+ else {
61
+ const kind = boilerplateKind(trigger);
62
+ if (kind === undefined) {
63
+ out.warnings.push(`Unrecognized trigger (keys: ${Object.keys(trigger).join(", ") || "none"}) was skipped — export it manually.`);
64
+ }
65
+ else {
66
+ boilerplate.push({ trigger, kind });
67
+ }
68
+ }
69
+ }
70
+ if (gitTriggers.length > 0) {
71
+ emitGithubChannel(automation, workflow, gitTriggers, channelSlugs, out);
72
+ }
73
+ if (slackTriggers.length > 0) {
74
+ emitSlackChannel(automation, slackTriggers, channelSlugs, out);
75
+ }
76
+ for (const { trigger, kind } of boilerplate) {
77
+ emitBoilerplateChannel(automation, trigger, kind, channelSlugs, out);
78
+ }
79
+ emitMcpConnections(automation, mcpServers, out);
80
+ const hasSlackAction = ((_d = workflow.actions) !== null && _d !== void 0 ? _d : []).some((action) => action.slack !== undefined || action.readSlack !== undefined);
81
+ const hasSlackConnection = hasSlackAction && emitSlackAccountConnection(automation, out);
82
+ if (workflow.memoryEnabled === true) {
83
+ out.files.push({ path: "agent/hooks/memory.ts", contents: MEMORY_HOOK });
84
+ }
85
+ out.setupSteps.push(...behaviorSteps(workflow, out.warnings, {
86
+ hasSlackChannel: slackTriggers.length > 0,
87
+ hasSlackConnection,
88
+ }));
89
+ out.setupSteps.push(`Disable the source automation before deploying this agent so both do not fire on the same events: ${automation.webUrl}${automation.enabled ? "" : " (it is currently disabled — keep it that way)"}`);
90
+ return out;
91
+ }
92
+ // ============================================================================
93
+ // instructions.md
94
+ // ============================================================================
95
+ function renderInstructions(automation, workflow, exportedAt, warnings) {
96
+ var _a, _b;
97
+ const header = [
98
+ "<!--",
99
+ ` Exported from Cursor automation "${commentSafe(automation.name)}"`,
100
+ ` uuid: ${commentSafe(automation.uuid)}`,
101
+ ` source: ${commentSafe(automation.webUrl)}`,
102
+ ` exported: ${commentSafe(exportedAt)}`,
103
+ "-->",
104
+ ].join("\n");
105
+ const prompts = (_a = workflow.prompts) !== null && _a !== void 0 ? _a : [];
106
+ if (prompts.length === 0) {
107
+ warnings.push("The source automation has no prompts — agent/instructions.md contains a placeholder.");
108
+ return `${header}\n\nTODO: the source automation had no prompt text. Write this agent's instructions.\n`;
109
+ }
110
+ // Prompt chaining was internal-only and never shipped; export the first
111
+ // prompt and warn if a legacy row somehow carries more.
112
+ if (prompts.length > 1) {
113
+ warnings.push(`The source automation has ${prompts.length} prompts (a legacy internal feature); only the first was exported to agent/instructions.md.`);
114
+ }
115
+ const prompt = prompts[0];
116
+ const meta = promptMeta(prompt);
117
+ const heading = meta === "" ? "" : `<!-- ${meta} -->\n\n`;
118
+ return `${header}\n\n${heading}${sanitizePromptText((_b = prompt.prompt) !== null && _b !== void 0 ? _b : "", warnings)}\n`;
119
+ }
120
+ /**
121
+ * Strip Cursor-backend prompt markup that means nothing in agentkit:
122
+ * `@[label](action:...)` action tokens become their label text, and
123
+ * unresolvable `%%INLINECODE<n>%%` storage placeholders are flagged (their
124
+ * content is not in the export).
125
+ */
126
+ function sanitizePromptText(text, warnings) {
127
+ let sanitized = text;
128
+ if (/@\[[^\]]*\]\(action:[^)]*\)/.test(sanitized)) {
129
+ warnings.push("The prompt referenced Cursor automation actions (@[...](action:...)); the tokens were replaced with their label text. Wire the equivalent channel/connection (see setup steps) to restore the behavior.");
130
+ sanitized = sanitized.replace(/@\[([^\]]*)\]\(action:[^)]*\)/g, "$1");
131
+ }
132
+ if (/%%INLINECODE\d+%%/.test(sanitized)) {
133
+ warnings.push("agent/instructions.md contains unresolved %%INLINECODE<n>%% placeholders from Cursor's prompt storage — open the automation in the dashboard and paste the original snippet back in.");
134
+ }
135
+ return sanitized;
136
+ }
137
+ const EFFORT_LABELS = {
138
+ PROMPT_EFFORT_LEVEL_STANDARD: "standard",
139
+ 1: "standard",
140
+ PROMPT_EFFORT_LEVEL_HARD: "hard",
141
+ 10: "hard",
142
+ };
143
+ function promptMeta(prompt) {
144
+ const parts = [];
145
+ const effort = EFFORT_LABELS[String(prompt.effortLevel)];
146
+ if (effort !== undefined && effort !== "standard") {
147
+ parts.push(`effort: ${effort}`);
148
+ }
149
+ if (prompt.model !== undefined && prompt.model !== "") {
150
+ parts.push(`model: ${prompt.model}`);
151
+ }
152
+ return parts.join("; ");
153
+ }
154
+ // ============================================================================
155
+ // agent.ts
156
+ // ============================================================================
157
+ function renderAgentTs(automation, workflow, pinnedModel) {
158
+ const comments = [
159
+ `// Exported from Cursor automation "${commentSafe(automation.name)}"`,
160
+ `// ${commentSafe(automation.webUrl)}`,
161
+ ];
162
+ if (workflow.gitConfig !== undefined) {
163
+ comments.push("// git_config (repo/branch the automation worked in):");
164
+ comments.push(...jsonComment(workflow.gitConfig));
165
+ }
166
+ if (workflow.agentOptions !== undefined) {
167
+ comments.push("// agent_options:");
168
+ comments.push(...jsonComment(workflow.agentOptions));
169
+ }
170
+ const body = pinnedModel !== undefined && pinnedModel !== ""
171
+ ? `export default defineAgent({\n model: ${JSON.stringify(pinnedModel)},\n});`
172
+ : "export default defineAgent({\n // The source automation used Cursor's default model.\n});";
173
+ return `import { defineAgent } from "${PACKAGE_NAME}";\n\n${comments.join("\n")}\n${body}\n`;
174
+ }
175
+ // ============================================================================
176
+ // Schedules (cron triggers)
177
+ // ============================================================================
178
+ function emitSchedule(automation, trigger, slugs, out) {
179
+ var _a;
180
+ const cron = (_a = trigger.cron) === null || _a === void 0 ? void 0 : _a.cron;
181
+ if (cron === undefined || cron === "") {
182
+ out.warnings.push("A cron trigger had no cron expression and was skipped.");
183
+ return;
184
+ }
185
+ const slug = slugs.claim(slugify(automation.name, "schedule"));
186
+ const path = `agent/schedules/${slug}.ts`;
187
+ const lines = [
188
+ `import { defineSchedule } from "${PACKAGE_NAME}/schedules";`,
189
+ "",
190
+ `// Exported from Cursor automation "${commentSafe(automation.name)}" cron trigger.`,
191
+ "// Cron is evaluated in UTC in both systems.",
192
+ "export default defineSchedule({",
193
+ ` cron: ${JSON.stringify(cron)},`,
194
+ " markdown:",
195
+ ' "Scheduled run: follow the instructions in agent/instructions.md.",',
196
+ "});",
197
+ "",
198
+ ];
199
+ out.files.push({ path, contents: lines.join("\n") });
200
+ }
201
+ // ============================================================================
202
+ // GitHub channel (git triggers)
203
+ // ============================================================================
204
+ const GIT_EVENT_WEBHOOKS = {
205
+ pullRequest: ["pull_request"],
206
+ push: ["push"],
207
+ ciCompleted: ["check_suite"],
208
+ issueLabeled: ["issues"],
209
+ label: [],
210
+ issueComment: ["issue_comment"],
211
+ pullRequestReviewComment: ["pull_request_review_comment"],
212
+ pullRequestReview: ["pull_request_review"],
213
+ reviewThread: ["pull_request_review_thread"],
214
+ workflowRun: ["workflow_run"],
215
+ pullRequestReviewRequested: ["pull_request"],
216
+ issueAssigned: ["issues"],
217
+ };
218
+ /**
219
+ * `pr.action`-based dispatch condition for one pull_request trigger,
220
+ * mirroring the backend matcher (`webhookHandler.ts`): OPENED is non-draft
221
+ * opened-or-ready-for-review; UNSPECIFIED is the legacy opened-or-synchronize
222
+ * gated by `only_once`; `ignore_draft_prs` applies per trigger.
223
+ */
224
+ function prConditionFor(prAction, eventObject) {
225
+ const ignoreDrafts = eventObject.ignoreDraftPrs === true;
226
+ switch (prAction) {
227
+ case "GIT_PULL_REQUEST_ACTION_UNSPECIFIED": {
228
+ const base = eventObject.onlyOnce === true
229
+ ? 'pr.action === "opened"'
230
+ : '(pr.action === "opened" || pr.action === "synchronize")';
231
+ return `(${base}${ignoreDrafts ? " && !pr.draft" : ""})`;
232
+ }
233
+ case "GIT_PULL_REQUEST_ACTION_OPENED":
234
+ return '((pr.action === "opened" || pr.action === "ready_for_review") && !pr.draft)';
235
+ case "GIT_PULL_REQUEST_ACTION_PUSHED":
236
+ return ignoreDrafts
237
+ ? '(pr.action === "synchronize" && !pr.draft)'
238
+ : '(pr.action === "synchronize")';
239
+ case "GIT_PULL_REQUEST_ACTION_MERGED":
240
+ return '(pr.action === "closed" && pr.merged)';
241
+ case "GIT_PULL_REQUEST_ACTION_DRAFT_OPENED":
242
+ return '(pr.action === "opened" && pr.draft)';
243
+ case "GIT_PULL_REQUEST_ACTION_LABELED":
244
+ return '(pr.action === "labeled")';
245
+ case "GIT_PULL_REQUEST_ACTION_UNLABELED":
246
+ return '(pr.action === "unlabeled")';
247
+ default:
248
+ return undefined;
249
+ }
250
+ }
251
+ const PR_ACTION_NUMBERS = {
252
+ 0: "GIT_PULL_REQUEST_ACTION_UNSPECIFIED",
253
+ 1: "GIT_PULL_REQUEST_ACTION_OPENED",
254
+ 2: "GIT_PULL_REQUEST_ACTION_PUSHED",
255
+ 3: "GIT_PULL_REQUEST_ACTION_MERGED",
256
+ 4: "GIT_PULL_REQUEST_ACTION_COMMENTED",
257
+ 5: "GIT_PULL_REQUEST_ACTION_DRAFT_OPENED",
258
+ 6: "GIT_PULL_REQUEST_ACTION_LABELED",
259
+ 7: "GIT_PULL_REQUEST_ACTION_UNLABELED",
260
+ };
261
+ function emitGithubChannel(automation, workflow, triggers, slugs, out) {
262
+ var _a, _b;
263
+ var _c, _d, _e, _f, _g, _h;
264
+ const slug = slugs.claim("github");
265
+ const path = `agent/channels/${slug}.ts`;
266
+ const webhookEvents = [];
267
+ const repos = [];
268
+ let hasOrgs = false;
269
+ // Mechanical filters recovered from the triggers, turned into real hooks
270
+ // below so the exported channel dispatches on the same events the
271
+ // automation did instead of shipping inert.
272
+ const prConditions = [];
273
+ let hasCommentedAction = false;
274
+ let hasIssueCommentTrigger = false;
275
+ // COMMENTED / review-comment triggers whose backend gates (PR-author
276
+ // provenance via user_allowlist, commenter_allowlist, comment_contains)
277
+ // cannot be reproduced by an onComment hook.
278
+ let commentGatesLost = false;
279
+ let hasUnknownPrAction = false;
280
+ const pushBranches = [];
281
+ let hasCheckSuite = false;
282
+ let hasWorkflowRun = false;
283
+ const issueActions = [];
284
+ const userAllowlist = [];
285
+ for (const trigger of triggers) {
286
+ const git = trigger.git;
287
+ userAllowlist.push(...((_c = git.userAllowlist) !== null && _c !== void 0 ? _c : []).filter((user) => user !== ""));
288
+ for (const [eventKey, events] of Object.entries(GIT_EVENT_WEBHOOKS)) {
289
+ const event = git[eventKey];
290
+ if (event === undefined || typeof event !== "object") {
291
+ continue;
292
+ }
293
+ webhookEvents.push(...events);
294
+ const eventObject = event;
295
+ if (eventKey === "pullRequest") {
296
+ const raw = String((_d = eventObject.prAction) !== null && _d !== void 0 ? _d : "");
297
+ const prAction = (_e = PR_ACTION_NUMBERS[raw]) !== null && _e !== void 0 ? _e : (raw === "" ? "GIT_PULL_REQUEST_ACTION_UNSPECIFIED" : raw);
298
+ if (prAction === "GIT_PULL_REQUEST_ACTION_COMMENTED") {
299
+ webhookEvents.push("issue_comment");
300
+ hasCommentedAction = true;
301
+ if (((_f = git.userAllowlist) !== null && _f !== void 0 ? _f : []).length > 0 ||
302
+ (Array.isArray(eventObject.commenterAllowlist) &&
303
+ eventObject.commenterAllowlist.length > 0) ||
304
+ (typeof eventObject.commentContains === "string" &&
305
+ eventObject.commentContains !== "")) {
306
+ commentGatesLost = true;
307
+ }
308
+ }
309
+ else {
310
+ const condition = prConditionFor(prAction, eventObject);
311
+ if (condition === undefined) {
312
+ hasUnknownPrAction = true;
313
+ out.warnings.push(`A PR trigger${repoLabel(eventObject)} used unrecognized pr_action ${JSON.stringify(raw)} — add its dispatch condition to onPullRequest in ${path} (exported fail-closed).`);
314
+ }
315
+ else {
316
+ prConditions.push(condition);
317
+ }
318
+ }
319
+ }
320
+ if (eventKey === "pullRequestReviewRequested") {
321
+ prConditions.push('(pr.action === "review_requested")');
322
+ }
323
+ if (eventKey === "issueComment") {
324
+ hasIssueCommentTrigger = true;
325
+ // A separate commenter_allowlist means BOTH lists must pass
326
+ // (intersection), and comment_contains filters the body — neither
327
+ // fits the single exported commenter gate.
328
+ if ((Array.isArray(eventObject.commenterAllowlist) &&
329
+ eventObject.commenterAllowlist.length > 0) ||
330
+ (typeof eventObject.commentContains === "string" &&
331
+ eventObject.commentContains !== "")) {
332
+ commentGatesLost = true;
333
+ }
334
+ }
335
+ if (eventKey === "pullRequestReviewComment") {
336
+ // user_allowlist gates the PR author and commenter_allowlist the
337
+ // commenter here — neither identity is visible to onComment — and
338
+ // comment_contains filters the body.
339
+ if (((_g = git.userAllowlist) !== null && _g !== void 0 ? _g : []).length > 0 ||
340
+ (Array.isArray(eventObject.commenterAllowlist) &&
341
+ eventObject.commenterAllowlist.length > 0) ||
342
+ (typeof eventObject.commentContains === "string" &&
343
+ eventObject.commentContains !== "")) {
344
+ commentGatesLost = true;
345
+ }
346
+ }
347
+ if (eventKey === "push") {
348
+ if (typeof eventObject.branch === "string" &&
349
+ eventObject.branch !== "") {
350
+ pushBranches.push(eventObject.branch);
351
+ }
352
+ else {
353
+ // The backend matcher rejects push triggers without a branch —
354
+ // they never fired, so exporting a dispatch would invent runs.
355
+ out.warnings.push(`A push trigger${repoLabel(eventObject)} had no branch filter and never fired in the backend — exported without a dispatch.`);
356
+ }
357
+ }
358
+ if (eventKey === "ciCompleted") {
359
+ hasCheckSuite = true;
360
+ }
361
+ if (eventKey === "workflowRun") {
362
+ hasWorkflowRun = true;
363
+ }
364
+ if (eventKey === "issueLabeled") {
365
+ if (typeof eventObject.labelName === "string" &&
366
+ eventObject.labelName !== "") {
367
+ // The label-name filter is not visible to the typed hooks, so a
368
+ // dispatch would fire on EVERY label change.
369
+ out.warnings.push(`An issue-label trigger filtered on label ${JSON.stringify(eventObject.labelName)}, which the exported hooks cannot check — exported without a dispatch; re-add the filter in an onEvent hook reading the raw payload.`);
370
+ }
371
+ else {
372
+ // `unlabeled: true` flips the legacy trigger to label removal.
373
+ issueActions.push(eventObject.unlabeled === true ? "unlabeled" : "labeled");
374
+ }
375
+ }
376
+ if (eventKey === "issueAssigned") {
377
+ issueActions.push("assigned");
378
+ }
379
+ // Review events export fail-closed: the backend gated review/thread
380
+ // state and PR-author provenance, none of which the export can
381
+ // reproduce. Without an onEvent hook the SDK drops the deliveries.
382
+ if (eventKey === "pullRequestReview" || eventKey === "reviewThread") {
383
+ out.warnings.push(`A ${eventKey === "pullRequestReview" ? "pull_request_review" : "review_thread"} trigger exported without a dispatch: the backend's review-state and PR-author provenance gates cannot be exported. Add an onEvent hook in ${path} to re-enable it with your own filters.`);
384
+ }
385
+ if (typeof eventObject.repo === "string" && eventObject.repo !== "") {
386
+ repos.push(eventObject.repo);
387
+ }
388
+ if (Array.isArray(eventObject.repos)) {
389
+ repos.push(...eventObject.repos.filter((repo) => typeof repo === "string" && repo !== ""));
390
+ }
391
+ if (Array.isArray(eventObject.orgs) && eventObject.orgs.length > 0) {
392
+ hasOrgs = true;
393
+ }
394
+ }
395
+ const label = git.label;
396
+ if (label !== undefined) {
397
+ const labelObject = label;
398
+ // The backend fires only for the configured direction(s); neither
399
+ // flag set means the trigger never fired.
400
+ let labelActions = [
401
+ ...(labelObject.onAdded === true ? ["labeled"] : []),
402
+ ...(labelObject.onRemoved === true ? ["unlabeled"] : []),
403
+ ];
404
+ if (labelActions.length === 0) {
405
+ out.warnings.push("A label trigger had neither on_added nor on_removed set and never fired in the backend — exported without a dispatch.");
406
+ }
407
+ if (typeof labelObject.labelName === "string" &&
408
+ labelObject.labelName !== "") {
409
+ // Label names are not visible to the typed hooks; dispatching would
410
+ // fire on EVERY label change instead of the filtered one.
411
+ out.warnings.push(`A label trigger filtered on label ${JSON.stringify(labelObject.labelName)}, which the exported hooks cannot check — exported without a dispatch; re-add the filter in a hook reading the raw payload.`);
412
+ labelActions = [];
413
+ }
414
+ if (label.pullRequests === true || label.issues !== true) {
415
+ webhookEvents.push("pull_request");
416
+ prConditions.push(...labelActions.map((action) => `(pr.action === ${JSON.stringify(action)})`));
417
+ }
418
+ if (label.issues === true) {
419
+ webhookEvents.push("issues");
420
+ issueActions.push(...labelActions);
421
+ }
422
+ }
423
+ }
424
+ if (((_a = workflow.gitConfig) === null || _a === void 0 ? void 0 : _a.repo) !== undefined &&
425
+ workflow.gitConfig.repo !== "") {
426
+ repos.push(workflow.gitConfig.repo);
427
+ }
428
+ repos.push(...((_h = (_b = workflow.gitConfig) === null || _b === void 0 ? void 0 : _b.repos) !== null && _h !== void 0 ? _h : []));
429
+ const uniqueEvents = dedupe(webhookEvents);
430
+ const uniqueRepos = dedupe(dedupe(repos.filter((repo) => repo !== "")).flatMap((repo) => {
431
+ const normalized = normalizeRepoIdentifier(repo, out);
432
+ return normalized === undefined ? [] : [normalized];
433
+ }));
434
+ if (hasOrgs) {
435
+ out.warnings.push("A git trigger watched whole GitHub orgs; the GitHub channel's cursorAccount scope needs explicit repos. List them in agent/channels/github.ts.");
436
+ }
437
+ out.setupSteps.push(`Review the generated hook filters in ${path} — mechanical trigger filters (PR action, push branch, issue action, user allowlist) were exported, but bespoke conditions Cursor's backend enforced (comment matching, CI conclusions, label names) must be re-added by hand.`);
438
+ if (hasCheckSuite || hasWorkflowRun) {
439
+ out.warnings.push(`A CI trigger (ci_completed / workflow_run) exported FAIL-CLOSED: its backend filters (conclusion, branch, actor allowlist) cannot be exported, so the generated hook in ${path} returns null until you re-add them and dispatch.`);
440
+ }
441
+ if (uniqueRepos.length === 0) {
442
+ out.setupSteps.push(`No repositories could be derived for the GitHub channel — set githubChannel({ cursorAccount: { repos: [...] } }) or GitHub App credentials in ${path}.`);
443
+ }
444
+ const configLines = [];
445
+ if (uniqueRepos.length > 0) {
446
+ configLines.push(" cursorAccount: {");
447
+ configLines.push(` repos: [${uniqueRepos.map((repo) => JSON.stringify(repo)).join(", ")}],`);
448
+ configLines.push(" },");
449
+ }
450
+ configLines.push(` webhookEvents: [${uniqueEvents.map((event) => JSON.stringify(event)).join(", ")}],`);
451
+ const uniqueAllowlist = dedupe(userAllowlist.map((user) => user.toLowerCase()));
452
+ if (triggers.length > 1 && uniqueAllowlist.length > 0) {
453
+ out.warnings.push(`user_allowlist applies per trigger in the backend; the export unions every trigger's allowlist across all hooks in ${path} — tighten per hook if the triggers differed.`);
454
+ }
455
+ // The cursorAccount SCM-event relay delivers PR/comment snapshots only —
456
+ // no push ref, no issue payload, no CI nodes, and no sender for
457
+ // non-comment events — so hooks needing those fields only work with a
458
+ // real GitHub App webhook or `github forward`.
459
+ const relayLimited = [
460
+ ...(dedupe(pushBranches).length > 0 ? ["push branch filters"] : []),
461
+ ...(dedupe(issueActions).length > 0 ? ["issue triggers"] : []),
462
+ ...(hasCheckSuite || hasWorkflowRun ? ["CI triggers"] : []),
463
+ ];
464
+ if (uniqueRepos.length > 0 && relayLimited.length > 0) {
465
+ out.warnings.push(`cursorAccount event delivery carries PR/comment data only, so ${relayLimited.join(", ")} will NOT fire through it — deliver real GitHub webhooks to the deployed agent (GitHub App or \`github forward\`) for those triggers.`);
466
+ }
467
+ const hasCommentEvents = uniqueEvents.includes("issue_comment") ||
468
+ uniqueEvents.includes("pull_request_review_comment");
469
+ const commentGate = !hasCommentEvents
470
+ ? "none"
471
+ : commentGatesLost
472
+ ? "failClosed"
473
+ : hasIssueCommentTrigger && uniqueAllowlist.length > 0
474
+ ? "commenterAllowlist"
475
+ : "mentionDefault";
476
+ if (commentGate === "failClosed") {
477
+ out.warnings.push(`A comment trigger's backend gates (PR-author provenance via user_allowlist, commenter_allowlist, comment_contains) cannot be exported — comments dispatch NOTHING until you re-add them in onComment in ${path}.`);
478
+ }
479
+ const { constLines, hookLines, usesAuth } = renderGithubHooks({
480
+ prConditions: dedupe(prConditions),
481
+ hasUnknownPrAction,
482
+ pushBranches: dedupe(pushBranches),
483
+ hasCheckSuite,
484
+ hasWorkflowRun,
485
+ issueActions: dedupe(issueActions),
486
+ userAllowlist: uniqueAllowlist,
487
+ hasPullRequestEvents: uniqueEvents.includes("pull_request"),
488
+ commentGate,
489
+ hasCommentedAction,
490
+ });
491
+ const imports = usesAuth
492
+ ? "import { defaultGitHubAuth, githubChannel }"
493
+ : "import { githubChannel }";
494
+ const lines = [
495
+ `${imports} from "${PACKAGE_NAME}/channels/github";`,
496
+ "",
497
+ `// Exported from Cursor automation "${commentSafe(automation.name)}" git trigger(s).`,
498
+ "// Original trigger config:",
499
+ ...jsonComment(triggers.map((trigger) => trigger.git)),
500
+ "//",
501
+ "// The hooks below reproduce the triggers' mechanical filters. Cursor's",
502
+ "// backend also enforced any bespoke conditions (comment matching, CI",
503
+ "// conclusions, label names) — re-add those before relying on this.",
504
+ ...constLines,
505
+ "export default githubChannel({",
506
+ ...configLines,
507
+ ...hookLines,
508
+ "});",
509
+ "",
510
+ ];
511
+ out.files.push({ path, contents: lines.join("\n") });
512
+ }
513
+ /** Render the dispatch hooks for {@link emitGithubChannel}. */
514
+ function renderGithubHooks(args) {
515
+ const lines = [];
516
+ const hasAllowlist = args.userAllowlist.length > 0;
517
+ const hasPush = args.pushBranches.length > 0;
518
+ let allowlistUsed = false;
519
+ let usesAuth = false;
520
+ if (args.prConditions.length > 0) {
521
+ lines.push(" onPullRequest: (ctx, pr) => {");
522
+ if (hasAllowlist) {
523
+ allowlistUsed = true;
524
+ lines.push(" if (", " pr.authorLogin === undefined ||", " !ALLOWED_USERS.includes(pr.authorLogin.toLowerCase())", " ) {", " return null;", " }");
525
+ }
526
+ lines.push(" return (", ...args.prConditions.map((condition, index) => ` ${condition}${index < args.prConditions.length - 1 ? " ||" : ""}`), " )", " ? { auth: defaultGitHubAuth(ctx) }", " : null;", " },");
527
+ usesAuth = true;
528
+ }
529
+ else if (args.hasPullRequestEvents) {
530
+ // Without this fail-safe the SDK's default gate would dispatch on
531
+ // opened/reopened/synchronize/ready_for_review — broader than any
532
+ // exported trigger. COMMENTED triggers get comments via the comment
533
+ // gate; unrecognized/label-filtered triggers need their condition
534
+ // re-added here.
535
+ lines.push(" onPullRequest: () => null,");
536
+ }
537
+ if (hasPush) {
538
+ lines.push(" onEvent: (ctx, event) => {");
539
+ if (hasAllowlist) {
540
+ allowlistUsed = true;
541
+ lines.push(" // The backend gated push on the user allowlist (actor).", " if (", " ctx.senderLogin === undefined ||", " !ALLOWED_USERS.includes(ctx.senderLogin.toLowerCase())", " ) {", " return null;", " }");
542
+ }
543
+ const refs = args.pushBranches.map((branch) => JSON.stringify(`refs/heads/${branch}`));
544
+ lines.push(' if (event.eventName === "push") {', " // Branch filter from the original trigger.", " const ref = (event.payload as { ref?: string }).ref;", refs.length === 1
545
+ ? ` if (ref !== ${refs[0]}) {`
546
+ : ` if (ref === undefined || ![${refs.join(", ")}].includes(ref)) {`, " return null;", " }", " return { auth: defaultGitHubAuth(ctx) };", " }");
547
+ usesAuth = true;
548
+ lines.push(" return null;", " },");
549
+ }
550
+ // check_suite / workflow_run only dispatch through their typed hooks —
551
+ // the onEvent catch-all never sees them. They export FAIL-CLOSED: the
552
+ // backend filters they relied on (CI conclusion/branch, and for
553
+ // workflow_run the actor AND triggering-actor allowlist) cannot be
554
+ // reproduced mechanically, and dispatching unfiltered would let any CI
555
+ // delivery start an agent turn.
556
+ if (args.hasCheckSuite) {
557
+ lines.push(" onCheckSuite: () => {", " // Fail closed: re-add the original ci_completed filters —", ' // `suite.action === "completed"`, conclusion/branch, user', " // allowlist (see the trigger config above) — then dispatch with", " // `(ctx, suite) => ({ auth: defaultGitHubAuth(ctx) })`.", " return null;", " },");
558
+ }
559
+ if (args.hasWorkflowRun) {
560
+ lines.push(" onWorkflowRun: () => {", " // Fail closed: re-add the original workflow_run filters — the", ' // backend required `run.action === "completed"`, a push-origin', " // same-repo (non-fork) run, and gated the user allowlist on the", " // run's actor AND triggering actor — then dispatch with", " // `(ctx, run) => ({ auth: defaultGitHubAuth(ctx) })`.", " return null;", " },");
561
+ }
562
+ if (args.issueActions.length > 0) {
563
+ const condition = args.issueActions.length === 1
564
+ ? `issue.action === ${JSON.stringify(args.issueActions[0])}`
565
+ : `[${args.issueActions.map((action) => JSON.stringify(action)).join(", ")}].includes(issue.action)`;
566
+ lines.push(" onIssue: (ctx, issue) => {");
567
+ if (hasAllowlist) {
568
+ allowlistUsed = true;
569
+ lines.push(" // The backend allowlisted the ACTOR (who labeled/assigned),", " // not the issue author.", " if (", " ctx.senderLogin === undefined ||", " !ALLOWED_USERS.includes(ctx.senderLogin.toLowerCase())", " ) {", " return null;", " }");
570
+ }
571
+ lines.push(` return ${condition} ? { auth: defaultGitHubAuth(ctx) } : null;`, " },");
572
+ usesAuth = true;
573
+ }
574
+ if (args.commentGate === "failClosed") {
575
+ lines.push(" onComment: () => {", " // Fail closed: the trigger's backend comment gates (PR-author", " // provenance via user_allowlist, commenter_allowlist,", " // comment_contains — see the trigger config above) cannot be", " // exported. Re-add them here (PR author needs a ctx.github", " // lookup), then return { auth: defaultGitHubAuth(ctx) }.", " return null;", " },");
576
+ }
577
+ else if (args.commentGate === "commenterAllowlist" && hasAllowlist) {
578
+ // issue_comment triggers gate the COMMENTER on user_allowlist — the one
579
+ // comment gate that is faithfully exportable.
580
+ allowlistUsed = true;
581
+ usesAuth = true;
582
+ lines.push(" // Replaces the default @mention gate: the original fired on ALL", " // comments by allowlisted commenters.", " onComment: (ctx, comment) => {", " const author = comment.author?.login;", " if (", " author === undefined ||", " !ALLOWED_USERS.includes(author.toLowerCase())", " ) {", " return null;", " }", " return { auth: defaultGitHubAuth(ctx) };", " },");
583
+ }
584
+ else if (args.commentGate === "mentionDefault" && lines.length > 0) {
585
+ lines.push(" // Comments dispatch via the default @mention gate; the original", " // fired on ALL comments — declare onComment to widen it.");
586
+ }
587
+ const constLines = allowlistUsed
588
+ ? [
589
+ "// user_allowlist from the original trigger(s), lowercased — GitHub",
590
+ "// logins are case-insensitive and the backend compared them that way.",
591
+ `const ALLOWED_USERS = [${args.userAllowlist.map((user) => JSON.stringify(user)).join(", ")}];`,
592
+ "",
593
+ ]
594
+ : [];
595
+ return { constLines, hookLines: lines, usesAuth };
596
+ }
597
+ function repoLabel(eventObject) {
598
+ const repos = dedupe([
599
+ typeof eventObject.repo === "string" ? eventObject.repo : "",
600
+ ...(Array.isArray(eventObject.repos)
601
+ ? eventObject.repos.filter((repo) => typeof repo === "string")
602
+ : []),
603
+ ].filter((repo) => repo !== ""));
604
+ return repos.length > 0 ? ` (${repos.join(", ")})` : "";
605
+ }
606
+ /**
607
+ * Normalize a trigger/git_config repo into a GitHub `owner/repo` identifier
608
+ * for `cursorAccount: { repos: [...] }`. github.com URLs (https or ssh) are
609
+ * reduced to `owner/repo`; non-GitHub URLs are excluded with a warning;
610
+ * plain strings pass through.
611
+ */
612
+ function normalizeRepoIdentifier(raw, out) {
613
+ let candidate = raw;
614
+ if (raw.startsWith("git@") && !raw.includes("://")) {
615
+ // git@host:path SSH shorthand → a parseable ssh:// URL.
616
+ candidate = `ssh://${raw.replace(":", "/")}`;
617
+ }
618
+ if (!candidate.includes("://")) {
619
+ return raw;
620
+ }
621
+ if (!URL.canParse(candidate)) {
622
+ out.warnings.push(`Repo ${JSON.stringify(raw)} could not be parsed as a URL — excluded from the GitHub channel's cursorAccount repos; list it manually as owner/repo if it is GitHub-hosted.`);
623
+ return undefined;
624
+ }
625
+ const url = new URL(candidate);
626
+ const host = url.hostname.toLowerCase();
627
+ if (host !== "github.com" && host !== "www.github.com") {
628
+ out.warnings.push(`Repo ${JSON.stringify(raw)} is not GitHub-hosted (host: ${url.hostname}) — excluded from the GitHub channel's cursorAccount repos; cursorAccount can only watch GitHub repositories.`);
629
+ // Losing a trigger repo silently halves coverage for dual-host setups,
630
+ // so surface it as a required setup step, not just a warning.
631
+ out.setupSteps.push(`Trigger repo ${JSON.stringify(raw)} is not on GitHub, so its events will NOT reach this agent. Re-deliver them yourself (e.g. a webhook to the deployed agent's /v1/channels/github route) or drop that trigger.`);
632
+ return undefined;
633
+ }
634
+ const segments = url.pathname.split("/").filter((s) => s !== "");
635
+ if (segments.length < 2) {
636
+ out.warnings.push(`Repo URL ${JSON.stringify(raw)} has no owner/repo path — excluded from the GitHub channel's cursorAccount repos.`);
637
+ return undefined;
638
+ }
639
+ return `${segments[0]}/${segments[1].replace(/\.git$/, "")}`;
640
+ }
641
+ // ============================================================================
642
+ // Slack channel
643
+ // ============================================================================
644
+ function isSlackTrigger(trigger) {
645
+ return (trigger.slackMention !== undefined ||
646
+ trigger.slackTrigger !== undefined ||
647
+ trigger.slackReactionAdded !== undefined ||
648
+ trigger.slackAnyReactionAdded !== undefined ||
649
+ trigger.slackChannelCreated !== undefined);
650
+ }
651
+ function emitSlackChannel(automation, triggers, slugs, out) {
652
+ var _a;
653
+ const slug = slugs.claim("slack");
654
+ const path = `agent/channels/${slug}.ts`;
655
+ const watches = triggers.filter((t) => t.slackTrigger !== undefined);
656
+ const unsupported = triggers.filter((t) => t.slackReactionAdded !== undefined ||
657
+ t.slackAnyReactionAdded !== undefined ||
658
+ t.slackChannelCreated !== undefined);
659
+ const socketMode = watches.length > 0 || unsupported.length > 0;
660
+ for (const trigger of unsupported) {
661
+ const kind = trigger.slackChannelCreated !== undefined
662
+ ? "channel-created"
663
+ : "reaction-added";
664
+ out.warnings.push(`Slack ${kind} trigger has no agentkit equivalent — its config is inlined as a comment in ${path}; handle the Slack event yourself on a dedicated Socket Mode app.`);
665
+ }
666
+ let reactionDropped = false;
667
+ for (const watch of watches) {
668
+ const config = watch.slackTrigger;
669
+ if (config.messageContains !== undefined && config.messageContains !== "") {
670
+ out.warnings.push(`Slack channel-watch message filter (${JSON.stringify(config.messageContains)}) was enforced by Cursor's backend — reimplement it (e.g. onChannelPost) in ${path}.`);
671
+ }
672
+ // The backend defaults a missing/UNSPECIFIED mode to ON (checkmark
673
+ // reaction); only explicit OFF means no reaction was added.
674
+ const reactionMode = String((_a = config.slackCompletionReactionMode) !== null && _a !== void 0 ? _a : "");
675
+ if (reactionMode !== "SLACK_COMPLETION_REACTION_MODE_OFF" &&
676
+ reactionMode !== "2") {
677
+ reactionDropped = true;
678
+ }
679
+ }
680
+ if (reactionDropped) {
681
+ out.warnings.push("The Slack trigger marked triggering messages with a completion reaction; agentkit has no completion-reaction option, so that signal is lost unless agent/instructions.md tells the agent to add the reaction itself via Slack tools.");
682
+ }
683
+ if (socketMode) {
684
+ out.setupSteps.push(`Slack channel-watch / reaction / channel-created triggers need a dedicated Socket Mode Slack app (not the Cursor connection): run \`${CLI_COMMAND_NAME} slack setup\`, then \`${CLI_COMMAND_NAME} slack init --channel-posts\`, and set the generated tokens.`);
685
+ }
686
+ const header = [
687
+ `import { slackChannel } from "${PACKAGE_NAME}/channels/slack";`,
688
+ "",
689
+ `// Exported from Cursor automation "${commentSafe(automation.name)}" Slack trigger(s).`,
690
+ "// Original trigger config:",
691
+ ...jsonComment(triggers),
692
+ ];
693
+ let body;
694
+ if (socketMode) {
695
+ const allow = dedupe(watches.flatMap((watch) => slackChannelList(watch.slackTrigger)));
696
+ const anyThreadReplies = watches.some((watch) => watch.slackTrigger.topLevelOnly === false);
697
+ // "Authenticated Cursor users" watches require a linked Cursor account
698
+ // on the owning team, which bots never have — so the backend blocked
699
+ // bot posts for them, and the sender gate itself has no SDK equivalent.
700
+ // The SDK has ONE bot-post setting per channel, so mixed watches must
701
+ // pick a side: keep bots when any watch allowed everyone (alert /
702
+ // scorebot watches depend on them), and say so loudly.
703
+ const blocksUnauthenticated = watches.some((watch) => watch.slackTrigger
704
+ .blockUnauthenticatedSlackUsers === true);
705
+ const includeBotPosts = watches.length > 0 &&
706
+ watches.some((watch) => watch.slackTrigger
707
+ .blockUnauthenticatedSlackUsers !== true);
708
+ if (blocksUnauthenticated) {
709
+ out.warnings.push(includeBotPosts
710
+ ? "Some Slack watches only fired for authenticated Cursor users while others allowed anyone; agentkit has one bot-post setting per channel, so the export keeps bot posts ENABLED (the open watches depend on them) and the authenticated-only watches lose both their sender gate and their bot exclusion. Split the channels or add an onChannelPost gate."
711
+ : "The Slack trigger only fired for authenticated Cursor users (linked accounts on the owning team); agentkit has no equivalent sender gate, so the exported watch dispatches for ANY channel member (bot posts stay excluded). Add an onChannelPost gate in the channel if that matters.");
712
+ }
713
+ const engagement = [];
714
+ if (allow.length > 0) {
715
+ engagement.push(" engagement: {");
716
+ engagement.push(" channelPosts: {");
717
+ engagement.push(` allow: [${allow.map((channel) => JSON.stringify(channel)).join(", ")}],`);
718
+ if (anyThreadReplies) {
719
+ engagement.push(' posts: "all",');
720
+ }
721
+ if (includeBotPosts) {
722
+ engagement.push(" // Cursor's channel-watch dispatched on bot-authored posts", " // (alerts, scorebots, ...); the SDK drops them by default and", " // always ignores this agent's own posts.", " includeBotPosts: true,");
723
+ }
724
+ engagement.push(" },");
725
+ engagement.push(" },");
726
+ }
727
+ body = [
728
+ "//",
729
+ "// This channel needs a dedicated Socket Mode Slack app (not the Cursor",
730
+ `// connection): run \`${CLI_COMMAND_NAME} slack setup\` and \`${CLI_COMMAND_NAME} slack init\`.`,
731
+ "export default slackChannel({",
732
+ ...engagement,
733
+ "});",
734
+ ];
735
+ }
736
+ else {
737
+ const agentName = pascalCase(automation.name, "ExportedAgent");
738
+ body = [
739
+ "//",
740
+ `// Mention this agent in Slack with \`@Cursor ${agentName} ...\`.`,
741
+ "export default slackChannel({",
742
+ " cursorAccount: true,",
743
+ ` agentName: ${JSON.stringify(agentName)},`,
744
+ "});",
745
+ ];
746
+ }
747
+ out.files.push({ path, contents: `${[...header, ...body].join("\n")}\n` });
748
+ }
749
+ function slackChannelList(trigger) {
750
+ var _a;
751
+ return [...((_a = trigger === null || trigger === void 0 ? void 0 : trigger.channels) !== null && _a !== void 0 ? _a : []), trigger === null || trigger === void 0 ? void 0 : trigger.channel].filter((channel) => typeof channel === "string" && channel !== "");
752
+ }
753
+ const BOILERPLATE_KINDS = [
754
+ { key: "linear", slug: "linear", label: "Linear" },
755
+ { key: "pagerduty", slug: "pagerduty", label: "PagerDuty" },
756
+ { key: "sentry", slug: "sentry", label: "Sentry" },
757
+ { key: "webhook", slug: "webhook", label: "webhook" },
758
+ {
759
+ key: "microsoftTeamsTrigger",
760
+ slug: "teams",
761
+ label: "Microsoft Teams message",
762
+ },
763
+ {
764
+ key: "microsoftTeamsChannelCreated",
765
+ slug: "teams-channel-created",
766
+ label: "Microsoft Teams channel-created",
767
+ },
768
+ ];
769
+ function boilerplateKind(trigger) {
770
+ return BOILERPLATE_KINDS.find((kind) => trigger[kind.key] !== undefined);
771
+ }
772
+ function emitBoilerplateChannel(automation, trigger, kind, slugs, out) {
773
+ const slug = slugs.claim(kind.slug);
774
+ const path = `agent/channels/${slug}.ts`;
775
+ out.warnings.push(`${kind.label} trigger exported as a boilerplate webhook channel (${path}) — Cursor-managed webhook delivery and event filtering do not carry over.`);
776
+ out.setupSteps.push(`Re-register the ${kind.label} webhook against the deployed agent URL (/v1/channels/${slug}) and reimplement event filtering in ${path} — Cursor's backend did both before. The generated route accepts ANY unauthenticated POST and starts a run from it: add auth (e.g. bearerAuth or provider signature verification) before exposing it.`);
777
+ const lines = [
778
+ `import { defineChannel, POST } from "${PACKAGE_NAME}/channels";`,
779
+ 'import { z } from "zod";',
780
+ "",
781
+ `// Exported from Cursor automation "${commentSafe(automation.name)}" ${kind.label} trigger.`,
782
+ "// Original trigger config:",
783
+ ...jsonComment(trigger[kind.key]),
784
+ "//",
785
+ `// TODO: Cursor's backend previously received ${kind.label} webhooks and`,
786
+ "// applied the filtering above. Re-register the provider webhook against",
787
+ `// the deployed agent URL (/v1/channels/${slug}) and reimplement the event`,
788
+ "// filtering in this handler before dispatching.",
789
+ "export default defineChannel({",
790
+ " routes: [",
791
+ ' POST("/", {',
792
+ " bodySchema: z.unknown(),",
793
+ " handler: async (_req, { send, body }) => {",
794
+ " const session = await send(",
795
+ " [",
796
+ ` "Handle this ${kind.label} event:",`,
797
+ ' "",',
798
+ " JSON.stringify(body, null, 2),",
799
+ ' ].join("\\n")',
800
+ " );",
801
+ " return Response.json({ sessionId: session.id });",
802
+ " },",
803
+ " }),",
804
+ " ],",
805
+ "});",
806
+ "",
807
+ ];
808
+ out.files.push({ path, contents: lines.join("\n") });
809
+ }
810
+ // ============================================================================
811
+ // MCP connections
812
+ // ============================================================================
813
+ function emitMcpConnections(automation, mcpServers, out) {
814
+ const slugs = new SlugRegistry();
815
+ for (const server of mcpServers) {
816
+ const slug = slugs.claim(slugify(server.name, "mcp-server"));
817
+ if (server.transportType !== "stdio") {
818
+ out.mcpConnections.push({ name: slug, kind: "cursorAccount" });
819
+ const lines = [
820
+ `import { defineConnection } from "${PACKAGE_NAME}/connections";`,
821
+ "",
822
+ `// MCP server "${commentSafe(server.name)}" from Cursor automation "${commentSafe(automation.name)}".`,
823
+ "// Resolved at runtime through your Cursor account: tools list and",
824
+ "// execute on the Cursor backend with the account's stored credentials,",
825
+ "// so no URL or tokens live in this project. Sign this host in",
826
+ `// (\`${CLI_COMMAND_NAME} login\`) and manage the server's authorization`,
827
+ "// in Cursor (dashboard -> MCP).",
828
+ "export default defineConnection({",
829
+ " cursorAccount: true,",
830
+ ` servers: [${JSON.stringify(server.name)}],`,
831
+ "});",
832
+ "",
833
+ ];
834
+ out.files.push({
835
+ path: `agent/mcp-connections/${slug}.ts`,
836
+ contents: lines.join("\n"),
837
+ });
838
+ continue;
839
+ }
840
+ out.mcpConnections.push({ name: slug, kind: "stdioStub" });
841
+ const reason = "it is a stdio server (its command, args, and env stay on Cursor's server). Run the server yourself and point a connection at it.";
842
+ // A comment-only .ts in mcp-connections/ fails discovery (a connection
843
+ // module must default-export defineConnection), so the stub is a .md the
844
+ // framework ignores.
845
+ const stubPath = `agent/mcp-connections/${slug}.todo.md`;
846
+ out.warnings.push(`MCP server "${server.name}" exported as a stub (${stubPath}): ${reason}`);
847
+ const lines = [
848
+ `# TODO: MCP server "${commentSafe(server.name)}"`,
849
+ "",
850
+ `The source automation used MCP server "${commentSafe(server.name)}", which could not`,
851
+ "be exported as a working connection:",
852
+ "",
853
+ `- ${reason}`,
854
+ "",
855
+ "Credentials are never exported. Once the server is reachable over",
856
+ `HTTP, create \`agent/mcp-connections/${slug}.ts\`:`,
857
+ "",
858
+ "```ts",
859
+ `import { defineConnection } from "${PACKAGE_NAME}/connections";`,
860
+ "",
861
+ "export default defineConnection({",
862
+ ' url: "https://example.com/mcp",',
863
+ ' oauth: true, // or headers: { authorization: "Bearer <token>" }',
864
+ "});",
865
+ "```",
866
+ "",
867
+ "This .md file is ignored by the framework; delete it once resolved.",
868
+ "",
869
+ ];
870
+ out.files.push({ path: stubPath, contents: lines.join("\n") });
871
+ }
872
+ }
873
+ /**
874
+ * The automation posted to / read Slack via Cursor's built-in Slack action;
875
+ * restore that access with a Cursor-account Slack connection so the exported
876
+ * agent has Slack tools without any token plumbing. Returns whether the
877
+ * project ends up with `agent/mcp-connections/slack.ts` — an MCP server named
878
+ * "slack" may already have produced an equivalent connection, which counts.
879
+ */
880
+ function emitSlackAccountConnection(automation, out) {
881
+ const path = "agent/mcp-connections/slack.ts";
882
+ if (out.files.some((file) => file.path === path)) {
883
+ return true;
884
+ }
885
+ const lines = [
886
+ `import { defineConnection } from "${PACKAGE_NAME}/connections";`,
887
+ "",
888
+ `// Cursor automation "${commentSafe(automation.name)}" used the built-in Slack action.`,
889
+ "// This connection restores Slack access through your Cursor account's",
890
+ "// Slack connector (dashboard -> MCP): tools execute on the Cursor",
891
+ "// backend with the account's stored credentials.",
892
+ "export default defineConnection({",
893
+ " cursorAccount: true,",
894
+ ' servers: ["Slack"],',
895
+ "});",
896
+ "",
897
+ ];
898
+ out.files.push({ path, contents: lines.join("\n") });
899
+ if (!out.mcpConnections.some((connection) => connection.name === "slack")) {
900
+ out.mcpConnections.push({ name: "slack", kind: "cursorAccount" });
901
+ }
902
+ return true;
903
+ }
904
+ // ============================================================================
905
+ // Built-in actions → behaviors to verify
906
+ // ============================================================================
907
+ const ACTION_STEPS = {
908
+ gitPr: 'Verify: git_pr — the automation auto-opened PRs. Have the agent push and open PRs itself; githubChannel needs permissions: "contents-write" to push code.',
909
+ prComment: 'Verify: pr_comment — githubChannel\'s default permissions ("pr-write") covers PR comments and reviews; guide comment behavior in agent/instructions.md.',
910
+ manageCheckRun: "Verify: manage_check_run — no built-in equivalent; create/update check runs via ctx.github in a github channel hook.",
911
+ requestReviewers: "Verify: request_reviewers — no built-in equivalent; request reviewers via ctx.github in a github channel hook.",
912
+ approvePr: 'Verify: approve_pr — PR approval needs githubChannel "pr-write" plus explicit approval guidance in agent/instructions.md.',
913
+ resolveReviewThreads: "Verify: resolve_review_threads — no built-in equivalent; resolve threads via the GitHub GraphQL API in a hook or tool.",
914
+ microsoftTeams: "Verify: microsoft_teams post — agentkit has no Teams channel; wire the Microsoft Teams API yourself.",
915
+ readMicrosoftTeams: "Verify: read_microsoft_teams — agentkit has no Teams tools; wire the Microsoft Teams API yourself.",
916
+ };
917
+ function slackActionStep(action, { hasSlackChannel, hasSlackConnection }) {
918
+ const channels = dedupe(slackChannelList(action));
919
+ const target = action.generalized === true
920
+ ? "any channel (generalized)"
921
+ : channels.length > 0
922
+ ? channels.join(", ")
923
+ : "unspecified channel";
924
+ if (hasSlackChannel) {
925
+ return `Verify: slack post (target: ${target}) — the Slack channel replies in the triggering thread; posting to other channels needs Slack tools or a Cursor account MCP connection.`;
926
+ }
927
+ if (hasSlackConnection) {
928
+ return `Verify: slack post (target: ${target}) — agent/mcp-connections/slack.ts restores Slack through your Cursor account's Slack connector; confirm it is connected in Cursor (dashboard -> MCP) and that agent/instructions.md names the target channel.`;
929
+ }
930
+ return `Verify: slack post (target: ${target}) — the automation posted to Slack, but the exported project has no Slack integration; wire a Slack channel or a Cursor account MCP connection (defineConnection({ cursorAccount: true, servers: ["Slack"] })) to restore it.`;
931
+ }
932
+ function behaviorSteps(workflow, warnings, slack) {
933
+ var _a;
934
+ var _b, _c;
935
+ const steps = [];
936
+ for (const action of (_b = workflow.actions) !== null && _b !== void 0 ? _b : []) {
937
+ if (action.mcp !== undefined) {
938
+ continue; // Covered by the exported mcpServers list.
939
+ }
940
+ if (action.slack !== undefined) {
941
+ steps.push(slackActionStep(action.slack, slack));
942
+ continue;
943
+ }
944
+ if (action.readSlack !== undefined) {
945
+ steps.push(slack.hasSlackConnection
946
+ ? "Verify: read_slack — Slack read access is available through the generated agent/mcp-connections/slack.ts (Cursor-account Slack connector)."
947
+ : 'Verify: read_slack — expose Slack read access via a Cursor account MCP connection (defineConnection({ cursorAccount: true, servers: ["Slack"] })).');
948
+ continue;
949
+ }
950
+ const step = (_a = Object.entries(ACTION_STEPS).find(([key]) => action[key] !== undefined)) === null || _a === void 0 ? void 0 : _a[1];
951
+ if (step === undefined) {
952
+ warnings.push(`Unrecognized action (keys: ${Object.keys(action).join(", ") || "none"}) was skipped — verify its behavior manually.`);
953
+ continue;
954
+ }
955
+ steps.push(step);
956
+ }
957
+ const disabled = (_c = workflow.disabledDefaultTools) !== null && _c !== void 0 ? _c : [];
958
+ if (disabled.length > 0) {
959
+ steps.push(`Verify: disabled_default_tools (${disabled.join(", ")}) — agentkit has no default automation tools; enforce the equivalent restriction in agent/instructions.md.`);
960
+ }
961
+ return steps;
962
+ }
963
+ // ============================================================================
964
+ // Shared helpers
965
+ // ============================================================================
966
+ /**
967
+ * Neutralize comment-terminator sequences in server-provided text embedded
968
+ * in generated comments: JS line terminators (\r, \n, U+2028, U+2029) end a
969
+ * `//` comment, `-->` ends an HTML comment, and `*` + `/` ends a block
970
+ * comment. Without this, a crafted automation or MCP server name injects
971
+ * live code into the generated project.
972
+ */
973
+ function commentSafe(text) {
974
+ return text
975
+ .replace(/[\r\n\u2028\u2029]+/g, " ")
976
+ .replace(/-->/g, "-- >")
977
+ .replace(/\*\//g, "* /");
978
+ }
979
+ function jsonComment(value) {
980
+ // JSON.stringify leaves U+2028/U+2029 raw; both terminate a `//` comment.
981
+ return JSON.stringify(value, null, 2)
982
+ .replace(/\u2028/g, "\\u2028")
983
+ .replace(/\u2029/g, "\\u2029")
984
+ .split("\n")
985
+ .map((line) => `// ${line}`);
986
+ }
987
+ function dedupe(values) {
988
+ return [...new Set(values)];
989
+ }
990
+ /** Memory hook contents — identical to the `initProject` scaffold. */
991
+ const MEMORY_HOOK = `// Journals every turn (timestamp, session, user message, result) so
992
+ // future sessions can recall past work. The journal is shared across every
993
+ // session — and every user — of this agent. Pass your own MemoryBackend to
994
+ // change where memories live, or delete this file to opt out.
995
+ import { memoryHook } from "${PACKAGE_NAME}/memory";
996
+
997
+ export default memoryHook();
998
+ `;