@cursor/july 0.1.49 → 0.1.52

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 (126) hide show
  1. package/AGENTS.md +3 -3
  2. package/README.md +7 -14
  3. package/dist/bin/agent-serve.js +22 -8
  4. package/dist/channels/slack/init.d.ts +2 -3
  5. package/dist/channels/slack/init.d.ts.map +1 -1
  6. package/dist/channels/slack/init.js +8 -43
  7. package/dist/channels/slack/manifest.d.ts +3 -3
  8. package/dist/channels/slack/manifest.js +3 -3
  9. package/dist/channels/slack/setup.d.ts +2 -11
  10. package/dist/channels/slack/setup.d.ts.map +1 -1
  11. package/dist/channels/slack/setup.js +26 -94
  12. package/dist/channels/slack/types.d.ts +3 -4
  13. package/dist/channels/slack/types.d.ts.map +1 -1
  14. package/dist/docs/404.html +2 -2
  15. package/dist/docs/ab.html +3 -3
  16. package/dist/docs/assets/{app.BpWAgjaR.js → app.Cbeq6FOy.js} +1 -1
  17. package/dist/docs/assets/chunks/@localSearchIndexroot.TzAgoWLl.js +1 -0
  18. package/dist/docs/assets/chunks/{VPLocalSearchBox.CnAktZtG.js → VPLocalSearchBox.Bmb8QjCI.js} +1 -1
  19. package/dist/docs/assets/chunks/{theme.B5IvUxOB.js → theme.BLNMeCRL.js} +2 -2
  20. package/dist/docs/assets/{deployment.md.Cdp4rTE4.js → deployment.md.B8kW-h7P.js} +1 -1
  21. package/dist/docs/assets/{example-agents_benny.md.l7JTmm8X.js → example-agents_benny.md.B-LIDGja.js} +1 -1
  22. package/dist/docs/assets/{example-agents_oncall.md.ZE0n6ZFN.js → example-agents_oncall.md.CBmyuAKc.js} +2 -2
  23. package/dist/docs/assets/{guides_human-in-the-loop.md.BWvT7UqY.js → guides_human-in-the-loop.md.Cf8kIIqC.js} +2 -2
  24. package/dist/docs/assets/{guides_human-in-the-loop.md.BWvT7UqY.lean.js → guides_human-in-the-loop.md.Cf8kIIqC.lean.js} +1 -1
  25. package/dist/docs/assets/{guides_slack.md.zriQpU_9.js → guides_slack.md.D2nTRu9M.js} +7 -12
  26. package/dist/docs/assets/guides_slack.md.D2nTRu9M.lean.js +1 -0
  27. package/dist/docs/assets/{reference_cli.md.BDmjPPAm.js → reference_cli.md.CGKfz_Yj.js} +3 -7
  28. package/dist/docs/assets/{reference_cli.md.BDmjPPAm.lean.js → reference_cli.md.CGKfz_Yj.lean.js} +1 -1
  29. package/dist/docs/assets/{troubleshooting.md.CkgwmVI0.js → troubleshooting.md.C6n3wCr6.js} +1 -1
  30. package/dist/docs/building-with-agents.html +3 -3
  31. package/dist/docs/concepts.html +3 -3
  32. package/dist/docs/deployment.html +5 -5
  33. package/dist/docs/evals.html +3 -3
  34. package/dist/docs/example-agents/approval-buddy.html +3 -3
  35. package/dist/docs/example-agents/benny.html +5 -5
  36. package/dist/docs/example-agents/bugbot.html +3 -3
  37. package/dist/docs/example-agents/codebase-wiki.html +3 -3
  38. package/dist/docs/example-agents/codeowners-review.html +3 -3
  39. package/dist/docs/example-agents/concierge.html +3 -3
  40. package/dist/docs/example-agents/fsd.html +3 -3
  41. package/dist/docs/example-agents/index.html +3 -3
  42. package/dist/docs/example-agents/knowledge-base.html +3 -3
  43. package/dist/docs/example-agents/oncall.html +6 -6
  44. package/dist/docs/example-agents/security-reviewer.html +3 -3
  45. package/dist/docs/example-agents/slack-agent.html +3 -3
  46. package/dist/docs/example-agents/weather-agent.html +3 -3
  47. package/dist/docs/guides/agent-to-agent.html +3 -3
  48. package/dist/docs/guides/cloud-runtime.html +3 -3
  49. package/dist/docs/guides/github.html +3 -3
  50. package/dist/docs/guides/human-in-the-loop.html +5 -5
  51. package/dist/docs/guides/mcp-oauth.html +3 -3
  52. package/dist/docs/guides/opentelemetry.html +3 -3
  53. package/dist/docs/guides/slack.html +11 -16
  54. package/dist/docs/guides/webhooks.html +3 -3
  55. package/dist/docs/hashmap.json +1 -1
  56. package/dist/docs/hillclimbing.html +3 -3
  57. package/dist/docs/index.html +3 -3
  58. package/dist/docs/quickstart.html +3 -3
  59. package/dist/docs/reference/agent-config.html +3 -3
  60. package/dist/docs/reference/artifacts.html +3 -3
  61. package/dist/docs/reference/channels.html +3 -3
  62. package/dist/docs/reference/cli.html +6 -10
  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/templates/triage.html +3 -3
  78. package/dist/docs/troubleshooting.html +5 -5
  79. package/dist/internal/cli-ax.js +3 -3
  80. package/dist/internal/cli-cursor.d.ts +18 -0
  81. package/dist/internal/cli-cursor.d.ts.map +1 -1
  82. package/dist/internal/cli-cursor.js +25 -0
  83. package/dist/internal/cli-slack.d.ts +3 -4
  84. package/dist/internal/cli-slack.d.ts.map +1 -1
  85. package/dist/internal/cli-slack.js +126 -322
  86. package/dist/internal/convert-automation/convert-workflow.js +2 -2
  87. package/dist/internal/event-log.d.ts +37 -17
  88. package/dist/internal/event-log.d.ts.map +1 -1
  89. package/dist/internal/event-log.js +189 -55
  90. package/dist/internal/session-engine.d.ts.map +1 -1
  91. package/dist/internal/session-engine.js +2 -2
  92. package/dist/internal/slack-provision-client.d.ts +37 -2
  93. package/dist/internal/slack-provision-client.d.ts.map +1 -1
  94. package/dist/internal/slack-provision-client.js +46 -0
  95. package/dist/playground/assets/{index-Dq_HqyU-.js → index-BKMz70wx.js} +39 -39
  96. package/dist/playground/assets/index-BmMrulSc.css +1 -0
  97. package/dist/playground/index.html +2 -2
  98. package/docs/deployment.md +1 -1
  99. package/docs/example-agents/benny.md +2 -3
  100. package/docs/example-agents/oncall.md +4 -5
  101. package/docs/guides/human-in-the-loop.md +1 -1
  102. package/docs/guides/slack.md +51 -112
  103. package/docs/reference/cli.md +14 -37
  104. package/docs/troubleshooting.md +2 -2
  105. package/package.json +1 -1
  106. package/skills/create-agent/SKILL.md +4 -5
  107. package/skills/setup-slack/SKILL.md +73 -131
  108. package/src/bin/agent-serve.ts +26 -9
  109. package/src/channels/slack/init.ts +10 -71
  110. package/src/channels/slack/manifest.ts +3 -3
  111. package/src/channels/slack/setup.ts +25 -103
  112. package/src/channels/slack/types.ts +3 -4
  113. package/src/internal/cli-ax.ts +3 -3
  114. package/src/internal/cli-cursor.ts +34 -0
  115. package/src/internal/cli-slack.ts +136 -389
  116. package/src/internal/convert-automation/convert-workflow.ts +2 -2
  117. package/src/internal/event-log.ts +167 -52
  118. package/src/internal/session-engine.ts +4 -2
  119. package/src/internal/slack-provision-client.ts +83 -2
  120. package/dist/docs/assets/chunks/@localSearchIndexroot.D00d772K.js +0 -1
  121. package/dist/docs/assets/guides_slack.md.zriQpU_9.lean.js +0 -1
  122. package/dist/playground/assets/index-Dox74yWJ.css +0 -1
  123. /package/dist/docs/assets/{deployment.md.Cdp4rTE4.lean.js → deployment.md.B8kW-h7P.lean.js} +0 -0
  124. /package/dist/docs/assets/{example-agents_benny.md.l7JTmm8X.lean.js → example-agents_benny.md.B-LIDGja.lean.js} +0 -0
  125. /package/dist/docs/assets/{example-agents_oncall.md.ZE0n6ZFN.lean.js → example-agents_oncall.md.CBmyuAKc.lean.js} +0 -0
  126. /package/dist/docs/assets/{troubleshooting.md.CkgwmVI0.lean.js → troubleshooting.md.C6n3wCr6.lean.js} +0 -0
@@ -999,7 +999,7 @@ function emitSlackChannel(
999
999
  }
1000
1000
  if (socketMode) {
1001
1001
  out.setupSteps.push(
1002
- `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.`
1002
+ `Slack channel-watch / reaction / channel-created triggers need a dedicated Socket Mode Slack app (not the Cursor connection): run \`${CLI_COMMAND_NAME} slack create --channel-posts\`, then finish in the dashboard wizard.`
1003
1003
  );
1004
1004
  }
1005
1005
 
@@ -1069,7 +1069,7 @@ function emitSlackChannel(
1069
1069
  body = [
1070
1070
  "//",
1071
1071
  "// This channel needs a dedicated Socket Mode Slack app (not the Cursor",
1072
- `// connection): run \`${CLI_COMMAND_NAME} slack setup\` and \`${CLI_COMMAND_NAME} slack init\`.`,
1072
+ `// connection): run \`${CLI_COMMAND_NAME} slack create --channel-posts\`.`,
1073
1073
  "export default slackChannel({",
1074
1074
  ...engagement,
1075
1075
  "});",
@@ -1,20 +1,83 @@
1
1
  /**
2
- * Durable, replayable per-session event log. Events are held in memory for
3
- * live fan-out and appended to `events.ndjson` for durability; on process
4
- * restart the file is hydrated back so streams can replay from any
5
- * `startIndex`.
2
+ * Durable, replayable per-session event log. `events.ndjson` is the source
3
+ * of truth. The process keeps `nextIndex`, live subscribers, and a short
4
+ * unflushed tail not a cache of the full transcript. Replay reads the
5
+ * file on demand; live streams subscribe first, then catch up by index.
6
+ *
7
+ * Hosted engines mint thousands of sessions (one per PR under
8
+ * agentic-owners). Retaining every streamed tool output in RAM for the
9
+ * process lifetime is what OOM'd those engines at the ~4 GB heap cap.
6
10
  */
7
11
 
8
- import { appendFile, mkdir, readFile } from "node:fs/promises";
12
+ import { appendFile, type FileHandle, mkdir, open } from "node:fs/promises";
9
13
  import { dirname } from "node:path";
14
+ import { createInterface } from "node:readline";
10
15
  import type { SessionEvent, SessionEventPayload } from "../types.js";
11
16
 
12
17
  export type AppendableEvent = SessionEventPayload & { turnId?: string };
13
18
 
19
+ function isEnoent(error: unknown): boolean {
20
+ return (
21
+ typeof error === "object" &&
22
+ error !== null &&
23
+ "code" in error &&
24
+ (error as { code: unknown }).code === "ENOENT"
25
+ );
26
+ }
27
+
28
+ function parseEventLine(line: string): SessionEvent | undefined {
29
+ const trimmed = line.trim();
30
+ if (trimmed === "") {
31
+ return undefined;
32
+ }
33
+ try {
34
+ return JSON.parse(trimmed) as SessionEvent;
35
+ } catch {
36
+ // Skip torn trailing writes from a crashed process.
37
+ return undefined;
38
+ }
39
+ }
40
+
41
+ async function readNdjsonEvents(
42
+ filePath: string,
43
+ onEvent: (event: SessionEvent) => void
44
+ ): Promise<void> {
45
+ let fh: FileHandle;
46
+ try {
47
+ fh = await open(filePath, "r");
48
+ } catch (error) {
49
+ if (isEnoent(error)) {
50
+ return;
51
+ }
52
+ throw error;
53
+ }
54
+ const stream = fh.createReadStream({ encoding: "utf8" });
55
+ const rl = createInterface({ input: stream, crlfDelay: Infinity });
56
+ try {
57
+ for await (const line of rl) {
58
+ const event = parseEventLine(line);
59
+ if (event !== undefined) {
60
+ onEvent(event);
61
+ }
62
+ }
63
+ } finally {
64
+ rl.close();
65
+ stream.destroy();
66
+ await fh.close().catch(() => {});
67
+ }
68
+ }
69
+
14
70
  export class SessionEventLog {
15
- private readonly events: SessionEvent[] = [];
71
+ private nextIndex = 0;
16
72
  private readonly listeners = new Set<(event: SessionEvent) => void>();
17
73
  private persistChain: Promise<void> = Promise.resolve();
74
+ /** Appended events whose `appendFile` has not finished. */
75
+ private readonly inflight: SessionEvent[] = [];
76
+ /**
77
+ * In-process patches (cloud cost backfill) that are not rewritten to
78
+ * `events.ndjson`. Applied on read.
79
+ */
80
+ private readonly replacements = new Map<number, SessionEvent>();
18
81
 
19
82
  private constructor(
20
83
  private readonly sessionId: string,
@@ -32,59 +95,52 @@ export class SessionEventLog {
32
95
  args.filePath,
33
96
  args.onPersistError ?? (() => {})
34
97
  );
35
- await log.hydrate();
98
+ await log.readNextIndex();
36
99
  return log;
37
100
  }
38
101
 
39
- private async hydrate(): Promise<void> {
40
- let raw: string;
41
- try {
42
- raw = await readFile(this.filePath, "utf8");
43
- } catch {
44
- return;
45
- }
46
- for (const line of raw.split("\n")) {
47
- if (line.trim() === "") {
48
- continue;
102
+ private async readNextIndex(): Promise<void> {
103
+ let next = 0;
104
+ await readNdjsonEvents(this.filePath, (event) => {
105
+ if (typeof event.index === "number" && event.index >= next) {
106
+ next = event.index + 1;
49
107
  }
50
- try {
51
- this.events.push(JSON.parse(line) as SessionEvent);
52
- } catch {
53
- // Skip torn trailing writes from a crashed process.
54
- }
55
- }
108
+ });
109
+ this.nextIndex = next;
56
110
  }
57
111
 
58
112
  get count(): number {
59
- return this.events.length;
113
+ return this.nextIndex;
60
114
  }
61
115
 
62
116
  /**
63
- * In-process residency vs recorded length. Today those are the same
64
- * (`events` is the cache). A later change can drop the cache and keep
65
- * `eventsTotal` as the durable count while `eventsRetained` shrinks to
66
- * the unflushed tail.
117
+ * In-process residency vs recorded length. `eventsTotal` is the durable
118
+ * count (`nextIndex`). `eventsRetained` is the unflushed tail the
119
+ * transcript is not cached after `appendFile` lands.
67
120
  */
68
121
  memoryStats(): { eventsRetained: number; eventsTotal: number } {
69
- const n = this.events.length;
70
- return { eventsRetained: n, eventsTotal: n };
122
+ return {
123
+ eventsRetained: this.inflight.length,
124
+ eventsTotal: this.nextIndex,
125
+ };
71
126
  }
72
127
 
73
128
  /**
74
- * Assign the next index, record the event, and fan out to live
75
- * subscribers. Ordering is synchronous; file persistence is chained in
76
- * the background and awaited by {@link flush}.
129
+ * Assign the next index and fan out to live subscribers. Ordering is
130
+ * synchronous; file persistence is chained in the background and awaited
131
+ * by {@link flush}. The event is not retained after the write lands.
77
132
  */
78
133
  append(payload: AppendableEvent): SessionEvent {
79
134
  const { turnId, ...rest } = payload;
80
135
  const event = {
81
- index: this.events.length,
136
+ index: this.nextIndex,
82
137
  sessionId: this.sessionId,
83
138
  ...(turnId === undefined ? {} : { turnId }),
84
139
  at: new Date().toISOString(),
85
140
  ...rest,
86
141
  } as SessionEvent;
87
- this.events.push(event);
142
+ this.nextIndex += 1;
143
+ this.inflight.push(event);
88
144
  for (const listener of [...this.listeners]) {
89
145
  try {
90
146
  listener(event);
@@ -93,35 +149,67 @@ export class SessionEventLog {
93
149
  }
94
150
  }
95
151
  const line = `${JSON.stringify(event)}\n`;
152
+ const index = event.index;
96
153
  this.persistChain = this.persistChain
97
154
  .then(() => appendFile(this.filePath, line, "utf8"))
155
+ .then(() => {
156
+ this.dropInflight(index);
157
+ })
98
158
  .catch((error) => this.onPersistError(error));
99
159
  return event;
100
160
  }
101
161
 
162
+ private dropInflight(index: number): void {
163
+ if (this.inflight[0]?.index === index) {
164
+ this.inflight.shift();
165
+ return;
166
+ }
167
+ const i = this.inflight.findIndex((event) => event.index === index);
168
+ if (i >= 0) {
169
+ this.inflight.splice(i, 1);
170
+ }
171
+ }
172
+
102
173
  /** Wait for pending file writes (used on close). */
103
174
  async flush(): Promise<void> {
104
175
  await this.persistChain;
105
176
  }
106
177
 
107
- snapshot(startIndex = 0): SessionEvent[] {
108
- return startIndex <= 0 ? [...this.events] : this.events.slice(startIndex);
178
+ /**
179
+ * Read recorded events from `startIndex`. Does not retain the result on
180
+ * the log; callers own the returned array.
181
+ */
182
+ async snapshot(startIndex = 0): Promise<SessionEvent[]> {
183
+ const byIndex = new Map<number, SessionEvent>();
184
+ await readNdjsonEvents(this.filePath, (event) => {
185
+ if (event.index >= startIndex) {
186
+ byIndex.set(event.index, event);
187
+ }
188
+ });
189
+ for (const event of this.inflight) {
190
+ if (event.index >= startIndex) {
191
+ byIndex.set(event.index, event);
192
+ }
193
+ }
194
+ for (const [index, event] of this.replacements) {
195
+ if (index >= startIndex) {
196
+ byIndex.set(index, event);
197
+ }
198
+ }
199
+ return [...byIndex.values()].sort((a, b) => a.index - b.index);
109
200
  }
110
201
 
111
202
  /**
112
- * Replace one already-recorded event in memory. Used to backfill usage/cost
113
- * onto historical `turn.completed` rows without rewriting `events.ndjson`.
114
- * Live subscribers are not notified — patch before opening a replay stream.
203
+ * Replace one already-recorded event in this process. Used to backfill
204
+ * usage/cost onto historical `turn.completed` rows without rewriting
205
+ * `events.ndjson`. Live subscribers are not notified — patch before
206
+ * opening a replay stream.
115
207
  */
116
208
  replace(index: number, event: SessionEvent): boolean {
117
- if (index < 0 || index >= this.events.length) {
209
+ if (index < 0 || index >= this.nextIndex || event.index !== index) {
118
210
  return false;
119
211
  }
120
- const current = this.events[index];
121
- if (current === undefined || current.index !== index) {
122
- return false;
123
- }
124
- this.events[index] = event;
212
+ this.replacements.set(index, event);
125
213
  return true;
126
214
  }
127
215
 
@@ -135,12 +223,16 @@ export class SessionEventLog {
135
223
  /**
136
224
  * NDJSON stream: replays recorded events from `startIndex`, then follows
137
225
  * live events until the consumer cancels.
226
+ *
227
+ * Subscribe first (buffer live events), then read the file, then drain
228
+ * the buffer by index so an async file read cannot drop or duplicate
229
+ * events that arrive during catch-up.
138
230
  */
139
231
  stream(startIndex = 0): ReadableStream<Uint8Array> {
140
232
  const encoder = new TextEncoder();
141
233
  let unsubscribe: (() => void) | undefined;
142
234
  return new ReadableStream<Uint8Array>({
143
- start: (controller) => {
235
+ start: async (controller) => {
144
236
  const push = (event: SessionEvent): void => {
145
237
  try {
146
238
  controller.enqueue(encoder.encode(`${JSON.stringify(event)}\n`));
@@ -148,15 +240,38 @@ export class SessionEventLog {
148
240
  unsubscribe?.();
149
241
  }
150
242
  };
151
- // Synchronous snapshot + subscribe: no events can interleave.
152
- for (const event of this.snapshot(startIndex)) {
153
- push(event);
154
- }
243
+ let following = false;
244
+ const buffered: SessionEvent[] = [];
155
245
  unsubscribe = this.subscribe((event) => {
156
- if (event.index >= startIndex) {
246
+ if (event.index < startIndex) {
247
+ return;
248
+ }
249
+ if (following) {
157
250
  push(event);
251
+ } else {
252
+ buffered.push(event);
158
253
  }
159
254
  });
255
+ try {
256
+ const prefix = await this.snapshot(startIndex);
257
+ let maxSeen = startIndex - 1;
258
+ for (const event of prefix) {
259
+ push(event);
260
+ if (event.index > maxSeen) {
261
+ maxSeen = event.index;
262
+ }
263
+ }
264
+ following = true;
265
+ for (const event of buffered) {
266
+ if (event.index > maxSeen) {
267
+ push(event);
268
+ maxSeen = event.index;
269
+ }
270
+ }
271
+ } catch (error) {
272
+ unsubscribe?.();
273
+ controller.error(error);
274
+ }
160
275
  },
161
276
  cancel: () => {
162
277
  unsubscribe?.();
@@ -3014,7 +3014,7 @@ export class SessionEngine {
3014
3014
 
3015
3015
  private async runCloudTurnCostBackfill(sessionId: string): Promise<void> {
3016
3016
  const log = await this.logs.get(sessionId);
3017
- const events = log.snapshot(0);
3017
+ const events = await log.snapshot(0);
3018
3018
  if (!events.some((event) => isUnpricedCompletedTurn(event))) {
3019
3019
  if (
3020
3020
  !this.isTurnRunning(sessionId) &&
@@ -3049,7 +3049,9 @@ export class SessionEngine {
3049
3049
  }
3050
3050
  log.replace(i, next);
3051
3051
  }
3052
- if (!log.snapshot(0).some((event) => isUnpricedCompletedTurn(event))) {
3052
+ if (
3053
+ !(await log.snapshot(0)).some((event) => isUnpricedCompletedTurn(event))
3054
+ ) {
3053
3055
  this.cloudCostBackfillDone.add(sessionId);
3054
3056
  }
3055
3057
  }
@@ -55,8 +55,8 @@ export type SlackAppIconInput =
55
55
  * Outcome of `POST /apps`. Credentials are returned exactly once and never
56
56
  * persisted server-side — the caller owns storing them. `appToken` can be
57
57
  * absent when Slack's install response omitted the app-level token.
58
- * `oauthAuthorizeUrl` is only minted on a fresh app create, so an update
59
- * that lands in admin approval omits it.
58
+ * `oauthAuthorizeUrl` is Slack's request-approval / reminder page. Fresh
59
+ * creates mint it; updates return it when Slack (or a persisted row) has one.
60
60
  */
61
61
  export type SlackAppProvisionResult =
62
62
  | {
@@ -176,6 +176,66 @@ const setIconResponseSchema = z
176
176
  })
177
177
  .passthrough();
178
178
 
179
+ const mintWizardResponseSchema = z
180
+ .object({
181
+ wizardId: z.string().min(1),
182
+ url: z.string().min(1),
183
+ })
184
+ .passthrough();
185
+
186
+ const wizardPollResponseSchema = z
187
+ .object({
188
+ wizardId: z.string(),
189
+ teamId: z.number(),
190
+ slug: z.string(),
191
+ env: z.string(),
192
+ status: z.string(),
193
+ appId: z.string().optional(),
194
+ oauthAuthorizeUrl: z.string().optional(),
195
+ error: z.string().optional(),
196
+ localEnv: z
197
+ .object({
198
+ botToken: z.string().min(1),
199
+ appToken: z.string().optional(),
200
+ })
201
+ .optional(),
202
+ localEnvClaimed: z.boolean().optional(),
203
+ })
204
+ .passthrough();
205
+
206
+ export interface SlackWizardMintResult {
207
+ wizardId: string;
208
+ url: string;
209
+ }
210
+
211
+ export interface SlackWizardPollResult {
212
+ wizardId: string;
213
+ teamId: number;
214
+ slug: string;
215
+ env: string;
216
+ status: string;
217
+ appId?: string;
218
+ oauthAuthorizeUrl?: string;
219
+ error?: string;
220
+ localEnv?: { botToken: string; appToken?: string };
221
+ localEnvClaimed?: boolean;
222
+ }
223
+
224
+ export interface SlackWizardMintInput {
225
+ teamId: number;
226
+ slug: string;
227
+ env: SlackAppEnv;
228
+ envPrefix?: string;
229
+ name?: string;
230
+ gitRepoUrl?: string;
231
+ gitRef?: string;
232
+ agentPath?: string;
233
+ channelPosts?: boolean;
234
+ slackTeamId?: string;
235
+ iconUrl?: string;
236
+ force?: boolean;
237
+ }
238
+
179
239
  // ============================================================================
180
240
  // Client
181
241
  // ============================================================================
@@ -224,6 +284,27 @@ export class AgentServeSlackClient {
224
284
  return connectResponseSchema.parse(raw).url;
225
285
  }
226
286
 
287
+ async mintWizard(
288
+ input: SlackWizardMintInput
289
+ ): Promise<SlackWizardMintResult> {
290
+ const raw = await this.request({
291
+ method: "POST",
292
+ path: "/internal/agent-serve/slack/wizard",
293
+ body: input,
294
+ context: { verb: "start Slack setup" },
295
+ });
296
+ return mintWizardResponseSchema.parse(raw);
297
+ }
298
+
299
+ async pollWizard(wizardId: string): Promise<SlackWizardPollResult> {
300
+ const raw = await this.request({
301
+ method: "GET",
302
+ path: `/internal/agent-serve/slack/wizard/${encodeURIComponent(wizardId)}`,
303
+ context: { verb: "poll Slack setup" },
304
+ });
305
+ return wizardPollResponseSchema.parse(raw);
306
+ }
307
+
227
308
  /**
228
309
  * Provision (create or update) and install one managed Slack app for
229
310
  * (slackTeamId, slug, env). Expected failures throw