@cursor/july 0.1.49 → 0.1.51
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.
- package/AGENTS.md +3 -3
- package/README.md +7 -14
- package/dist/bin/agent-serve.js +22 -8
- package/dist/channels/slack/init.d.ts +2 -3
- package/dist/channels/slack/init.d.ts.map +1 -1
- package/dist/channels/slack/init.js +8 -43
- package/dist/channels/slack/manifest.d.ts +3 -3
- package/dist/channels/slack/manifest.js +3 -3
- package/dist/channels/slack/setup.d.ts +2 -11
- package/dist/channels/slack/setup.d.ts.map +1 -1
- package/dist/channels/slack/setup.js +26 -94
- package/dist/channels/slack/types.d.ts +3 -4
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +2 -2
- package/dist/docs/ab.html +3 -3
- package/dist/docs/assets/{app.BpWAgjaR.js → app.C51TX1Bw.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.D2DpXdZN.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.CnAktZtG.js → VPLocalSearchBox.BB9HCJPq.js} +1 -1
- package/dist/docs/assets/chunks/{theme.B5IvUxOB.js → theme.Dp-0SZcu.js} +2 -2
- package/dist/docs/assets/{deployment.md.Cdp4rTE4.js → deployment.md.B8kW-h7P.js} +1 -1
- package/dist/docs/assets/{example-agents_benny.md.l7JTmm8X.js → example-agents_benny.md.B-LIDGja.js} +1 -1
- package/dist/docs/assets/{example-agents_oncall.md.ZE0n6ZFN.js → example-agents_oncall.md.CBmyuAKc.js} +2 -2
- package/dist/docs/assets/{guides_human-in-the-loop.md.BWvT7UqY.js → guides_human-in-the-loop.md.Cf8kIIqC.js} +2 -2
- package/dist/docs/assets/{guides_human-in-the-loop.md.BWvT7UqY.lean.js → guides_human-in-the-loop.md.Cf8kIIqC.lean.js} +1 -1
- package/dist/docs/assets/{guides_slack.md.zriQpU_9.js → guides_slack.md.D2nTRu9M.js} +7 -12
- package/dist/docs/assets/guides_slack.md.D2nTRu9M.lean.js +1 -0
- package/dist/docs/assets/{reference_cli.md.BDmjPPAm.js → reference_cli.md.CGKfz_Yj.js} +3 -7
- package/dist/docs/assets/{reference_cli.md.BDmjPPAm.lean.js → reference_cli.md.CGKfz_Yj.lean.js} +1 -1
- package/dist/docs/assets/{troubleshooting.md.CkgwmVI0.js → troubleshooting.md.C6n3wCr6.js} +1 -1
- package/dist/docs/building-with-agents.html +3 -3
- package/dist/docs/concepts.html +3 -3
- package/dist/docs/deployment.html +5 -5
- package/dist/docs/evals.html +3 -3
- package/dist/docs/example-agents/approval-buddy.html +3 -3
- package/dist/docs/example-agents/benny.html +5 -5
- package/dist/docs/example-agents/bugbot.html +3 -3
- package/dist/docs/example-agents/codebase-wiki.html +3 -3
- package/dist/docs/example-agents/codeowners-review.html +3 -3
- package/dist/docs/example-agents/concierge.html +3 -3
- package/dist/docs/example-agents/fsd.html +3 -3
- package/dist/docs/example-agents/index.html +3 -3
- package/dist/docs/example-agents/knowledge-base.html +3 -3
- package/dist/docs/example-agents/oncall.html +6 -6
- package/dist/docs/example-agents/security-reviewer.html +3 -3
- package/dist/docs/example-agents/slack-agent.html +3 -3
- package/dist/docs/example-agents/weather-agent.html +3 -3
- package/dist/docs/guides/agent-to-agent.html +3 -3
- package/dist/docs/guides/cloud-runtime.html +3 -3
- package/dist/docs/guides/github.html +3 -3
- package/dist/docs/guides/human-in-the-loop.html +5 -5
- package/dist/docs/guides/mcp-oauth.html +3 -3
- package/dist/docs/guides/opentelemetry.html +3 -3
- package/dist/docs/guides/slack.html +11 -16
- package/dist/docs/guides/webhooks.html +3 -3
- package/dist/docs/hashmap.json +1 -1
- package/dist/docs/hillclimbing.html +3 -3
- package/dist/docs/index.html +3 -3
- package/dist/docs/quickstart.html +3 -3
- package/dist/docs/reference/agent-config.html +3 -3
- package/dist/docs/reference/artifacts.html +3 -3
- package/dist/docs/reference/channels.html +3 -3
- package/dist/docs/reference/cli.html +6 -10
- package/dist/docs/reference/connections.html +3 -3
- package/dist/docs/reference/hooks.html +3 -3
- package/dist/docs/reference/http-api.html +3 -3
- package/dist/docs/reference/instructions.html +3 -3
- package/dist/docs/reference/playground.html +3 -3
- package/dist/docs/reference/project-layout.html +3 -3
- package/dist/docs/reference/prompt.html +3 -3
- package/dist/docs/reference/schedules.html +3 -3
- package/dist/docs/reference/sessions.html +3 -3
- package/dist/docs/reference/skills.html +3 -3
- package/dist/docs/reference/subagents.html +3 -3
- package/dist/docs/reference/tools.html +3 -3
- package/dist/docs/scaffolding-agents.html +3 -3
- package/dist/docs/storage.html +3 -3
- package/dist/docs/templates/triage.html +3 -3
- package/dist/docs/troubleshooting.html +5 -5
- package/dist/internal/cli-ax.js +3 -3
- package/dist/internal/cli-cursor.d.ts +18 -0
- package/dist/internal/cli-cursor.d.ts.map +1 -1
- package/dist/internal/cli-cursor.js +25 -0
- package/dist/internal/cli-slack.d.ts +3 -4
- package/dist/internal/cli-slack.d.ts.map +1 -1
- package/dist/internal/cli-slack.js +126 -322
- package/dist/internal/convert-automation/convert-workflow.js +2 -2
- package/dist/internal/slack-provision-client.d.ts +37 -2
- package/dist/internal/slack-provision-client.d.ts.map +1 -1
- package/dist/internal/slack-provision-client.js +46 -0
- package/dist/playground/assets/{index-Dq_HqyU-.js → index-BKMz70wx.js} +39 -39
- package/dist/playground/assets/index-BmMrulSc.css +1 -0
- package/dist/playground/index.html +2 -2
- package/docs/deployment.md +1 -1
- package/docs/example-agents/benny.md +2 -3
- package/docs/example-agents/oncall.md +4 -5
- package/docs/guides/human-in-the-loop.md +1 -1
- package/docs/guides/slack.md +51 -112
- package/docs/reference/cli.md +14 -37
- package/docs/troubleshooting.md +2 -2
- package/package.json +1 -1
- package/skills/create-agent/SKILL.md +4 -5
- package/skills/setup-slack/SKILL.md +73 -131
- package/src/bin/agent-serve.ts +26 -9
- package/src/channels/slack/init.ts +10 -71
- package/src/channels/slack/manifest.ts +3 -3
- package/src/channels/slack/setup.ts +25 -103
- package/src/channels/slack/types.ts +3 -4
- package/src/internal/cli-ax.ts +3 -3
- package/src/internal/cli-cursor.ts +34 -0
- package/src/internal/cli-slack.ts +136 -389
- package/src/internal/convert-automation/convert-workflow.ts +2 -2
- package/src/internal/slack-provision-client.ts +83 -2
- package/dist/docs/assets/chunks/@localSearchIndexroot.D00d772K.js +0 -1
- package/dist/docs/assets/guides_slack.md.zriQpU_9.lean.js +0 -1
- package/dist/playground/assets/index-Dox74yWJ.css +0 -1
- /package/dist/docs/assets/{deployment.md.Cdp4rTE4.lean.js → deployment.md.B8kW-h7P.lean.js} +0 -0
- /package/dist/docs/assets/{example-agents_benny.md.l7JTmm8X.lean.js → example-agents_benny.md.B-LIDGja.lean.js} +0 -0
- /package/dist/docs/assets/{example-agents_oncall.md.ZE0n6ZFN.lean.js → example-agents_oncall.md.CBmyuAKc.lean.js} +0 -0
- /package/dist/docs/assets/{troubleshooting.md.CkgwmVI0.lean.js → troubleshooting.md.C6n3wCr6.lean.js} +0 -0
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `agent-serve slack …` CLI —
|
|
3
|
-
*
|
|
4
|
-
* "Add to Slack" API (`slack-provision-client.ts`).
|
|
2
|
+
* `agent-serve slack …` CLI — doctor, destroy/icon, and `create` as a
|
|
3
|
+
* dashboard-wizard launcher (`slack-provision-client.ts`).
|
|
5
4
|
*/
|
|
6
5
|
|
|
7
6
|
import { access, chmod, mkdir, readFile, writeFile } from "node:fs/promises";
|
|
8
7
|
import { basename, extname, join, resolve as resolvePath } from "node:path";
|
|
9
8
|
import {
|
|
10
|
-
buildSlackManifest,
|
|
11
9
|
buildSlackManifestPair,
|
|
12
10
|
envPrefixFromName,
|
|
13
11
|
formatSlackSetupGuide,
|
|
@@ -19,6 +17,7 @@ import {
|
|
|
19
17
|
import { resolveDeployAuth } from "./cli-deploy.js";
|
|
20
18
|
import { cursorExternalApiUrl } from "./cursor/credentials.js";
|
|
21
19
|
import { slugFromDirectoryName } from "./deploy-client.js";
|
|
20
|
+
import { inferDeployGitSource } from "./deploy-source.js";
|
|
22
21
|
import { describeError } from "./describe-error.js";
|
|
23
22
|
import { CLI_COMMAND_NAME as CLI } from "./distribution.js";
|
|
24
23
|
import { loadAgentLocalEnv } from "./local-env.js";
|
|
@@ -28,11 +27,11 @@ import {
|
|
|
28
27
|
type SlackAppEnv,
|
|
29
28
|
type SlackAppIconContentType,
|
|
30
29
|
type SlackAppIconInput,
|
|
31
|
-
type SlackAppProvisionResult,
|
|
32
|
-
type SlackConnectedWorkspace,
|
|
33
30
|
type SlackManagedAppInfo,
|
|
34
31
|
SlackProvisionApiError,
|
|
35
32
|
type SlackProvisionStatus,
|
|
33
|
+
type SlackWizardMintResult,
|
|
34
|
+
type SlackWizardPollResult,
|
|
36
35
|
} from "./slack-provision-client.js";
|
|
37
36
|
|
|
38
37
|
export interface SlackCliOptions {
|
|
@@ -57,7 +56,7 @@ export interface SlackCliOptions {
|
|
|
57
56
|
apiKey?: string;
|
|
58
57
|
/** create: app icon — an https image URL or a local png/jpg/gif ≤512KB. */
|
|
59
58
|
icon?: string;
|
|
60
|
-
/** create:
|
|
59
|
+
/** create: accepted for compatibility; live-app updates always overwrite. */
|
|
61
60
|
force?: boolean;
|
|
62
61
|
/** `slack icon <source>` positional (same URL-or-file forms as --icon). */
|
|
63
62
|
iconSource?: string;
|
|
@@ -82,33 +81,26 @@ Usage:
|
|
|
82
81
|
[--team <id>] [--json]
|
|
83
82
|
${CLI} slack icon <URL-or-FILE> [--dir .] [--slack-team T0123ABCD]
|
|
84
83
|
[--prod] [--team <id>] [--json]
|
|
85
|
-
${CLI} slack init [--dir .] [--name NAME] [--prefix PREFIX | --no-prefix]
|
|
86
|
-
[--channel-posts]
|
|
87
|
-
${CLI} slack manifest --env dev|prod|both [--dir .] [--name NAME]
|
|
88
|
-
[--print]
|
|
89
84
|
${CLI} slack doctor [--dir .] [--prefix PREFIX | --no-prefix] [--json]
|
|
90
85
|
|
|
91
|
-
create/destroy/icon —
|
|
92
|
-
create
|
|
93
|
-
consent
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
\`${CLI} login\` (or CURSOR_API_KEY) and the feature gate.
|
|
98
|
-
--prod provisions the prod app (default dev); --slack-team picks
|
|
99
|
-
the workspace when several are connected. --icon sets the app's
|
|
100
|
-
icon (best-effort: a bad icon warns, never fails the create).
|
|
101
|
-
A re-run refuses to overwrite manifest edits made by hand at
|
|
102
|
-
api.slack.com (manifest_drift); --force overwrites them.
|
|
86
|
+
create/destroy/icon — dedicated Socket Mode app via the Cursor dashboard:
|
|
87
|
+
create Opens the signed-in dashboard wizard. Finish name, workspace,
|
|
88
|
+
and Slack consent there. The CLI writes the token pair into
|
|
89
|
+
<dir>/.env.local and runs doctor. Requires \`${CLI} login\`
|
|
90
|
+
(or CURSOR_API_KEY). --prod provisions the prod app (default
|
|
91
|
+
dev). --name / --icon / --channel-posts prefill the wizard.
|
|
103
92
|
destroy Delete the provisioned Slack app for this agent (+env). Tokens
|
|
104
93
|
already in .env.local are left in place (they just stop working).
|
|
105
94
|
icon Set the provisioned app's icon: an https image URL or a local
|
|
106
95
|
png/jpg/gif file (at most 512KB).
|
|
107
96
|
|
|
97
|
+
\`slack init\` is removed. Use \`slack create\`. Do not paste a manifest
|
|
98
|
+
at api.slack.com.
|
|
99
|
+
|
|
108
100
|
Env tokens:
|
|
109
101
|
single agent: SLACK_BOT_TOKEN + SLACK_APP_TOKEN
|
|
110
102
|
multi-agent: <PREFIX>_SLACK_BOT_TOKEN + <PREFIX>_SLACK_APP_TOKEN
|
|
111
|
-
(
|
|
103
|
+
(create default PREFIX from the directory name;
|
|
112
104
|
doctor defaults to it too when those keys are set)
|
|
113
105
|
|
|
114
106
|
See also: skills/setup-slack/SKILL.md
|
|
@@ -148,48 +140,15 @@ export async function cmdSlack(
|
|
|
148
140
|
}
|
|
149
141
|
|
|
150
142
|
async function cmdSlackInit(options: SlackCliOptions): Promise<number> {
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
: { envPrefix: options.prefix }),
|
|
161
|
-
...(options.channelPosts === true ? { channelPosts: true } : {}),
|
|
162
|
-
});
|
|
163
|
-
process.stdout.write(
|
|
164
|
-
`${JSON.stringify(
|
|
165
|
-
{
|
|
166
|
-
ok: true,
|
|
167
|
-
directory: result.rootDir,
|
|
168
|
-
created: result.files,
|
|
169
|
-
envPrefix: result.envPrefix ?? null,
|
|
170
|
-
envKeys: result.envKeys,
|
|
171
|
-
next: [
|
|
172
|
-
"Open https://api.slack.com/apps → Create from manifest",
|
|
173
|
-
"Import .agent-serve/slack/manifest.dev.json (then prod)",
|
|
174
|
-
`Install apps; set ${result.envKeys.bot} + ${result.envKeys.app}`,
|
|
175
|
-
result.envPrefix === undefined
|
|
176
|
-
? "agent-serve slack doctor"
|
|
177
|
-
: `agent-serve slack doctor --prefix ${result.envPrefix}`,
|
|
178
|
-
"agent-serve serve --dir . --dev",
|
|
179
|
-
"agent-serve slack setup",
|
|
180
|
-
],
|
|
181
|
-
},
|
|
182
|
-
null,
|
|
183
|
-
2
|
|
184
|
-
)}\n`
|
|
185
|
-
);
|
|
186
|
-
return 0;
|
|
187
|
-
} catch (error) {
|
|
188
|
-
process.stderr.write(
|
|
189
|
-
`${error instanceof Error ? error.message : String(error)}\n`
|
|
190
|
-
);
|
|
191
|
-
return 1;
|
|
192
|
-
}
|
|
143
|
+
errStream(options)(
|
|
144
|
+
`\`slack init\` is removed. Socket Mode apps are created with:
|
|
145
|
+
|
|
146
|
+
${CLI} slack create --dir .
|
|
147
|
+
|
|
148
|
+
That opens the Cursor dashboard wizard. Do not paste a manifest at api.slack.com.
|
|
149
|
+
`
|
|
150
|
+
);
|
|
151
|
+
return 1;
|
|
193
152
|
}
|
|
194
153
|
|
|
195
154
|
async function runManifest(options: SlackCliOptions): Promise<number> {
|
|
@@ -198,10 +157,8 @@ async function runManifest(options: SlackCliOptions): Promise<number> {
|
|
|
198
157
|
const name = options.name ?? basename(dir);
|
|
199
158
|
const pair = buildSlackManifestPair({
|
|
200
159
|
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
160
|
managedByCursor: false,
|
|
204
|
-
|
|
161
|
+
channelPosts: options.channelPosts === true ? true : undefined,
|
|
205
162
|
});
|
|
206
163
|
|
|
207
164
|
if (options.print) {
|
|
@@ -276,8 +233,8 @@ function resolveDoctorEnvPrefix(
|
|
|
276
233
|
// create / destroy — Cursor "Add to Slack" managed provisioning
|
|
277
234
|
// ============================================================================
|
|
278
235
|
|
|
279
|
-
const
|
|
280
|
-
const
|
|
236
|
+
const DEFAULT_WIZARD_POLL_INTERVAL_MS = 3_000;
|
|
237
|
+
const DEFAULT_WIZARD_POLL_TIMEOUT_MS = 60 * 60 * 1000;
|
|
281
238
|
|
|
282
239
|
interface SlackProvisionContext {
|
|
283
240
|
client: AgentServeSlackClient;
|
|
@@ -402,41 +359,30 @@ function toSlackAppIconInput(source: SlackIconSource): SlackAppIconInput {
|
|
|
402
359
|
}
|
|
403
360
|
|
|
404
361
|
/**
|
|
405
|
-
*
|
|
406
|
-
*
|
|
362
|
+
* File icons post after the wizard finishes. URL icons ride the mint
|
|
363
|
+
* payload. Best-effort: failures warn, never fail the create.
|
|
407
364
|
*/
|
|
408
|
-
async function
|
|
365
|
+
async function applySlackCreateFileIcon(
|
|
409
366
|
ctx: SlackProvisionContext,
|
|
410
367
|
source: SlackIconSource | undefined,
|
|
411
|
-
|
|
368
|
+
appId: string
|
|
412
369
|
): Promise<"set" | "failed" | undefined> {
|
|
413
|
-
if (source === undefined) {
|
|
370
|
+
if (source === undefined || source.kind === "url") {
|
|
414
371
|
return undefined;
|
|
415
372
|
}
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
try {
|
|
423
|
-
await ctx.client.setAppIcon({
|
|
424
|
-
teamId: ctx.teamId,
|
|
425
|
-
slackAppId: result.appId,
|
|
426
|
-
icon: toSlackAppIconInput(source),
|
|
427
|
-
});
|
|
428
|
-
} catch (error) {
|
|
429
|
-
failure = describeError(error);
|
|
430
|
-
}
|
|
431
|
-
}
|
|
432
|
-
if (failure === undefined) {
|
|
373
|
+
try {
|
|
374
|
+
await ctx.client.setAppIcon({
|
|
375
|
+
teamId: ctx.teamId,
|
|
376
|
+
slackAppId: appId,
|
|
377
|
+
icon: toSlackAppIconInput(source),
|
|
378
|
+
});
|
|
433
379
|
return "set";
|
|
380
|
+
} catch (error) {
|
|
381
|
+
ctx.err(
|
|
382
|
+
`warning: setting the Slack app icon failed: ${describeError(error)}\nRetry with \`${CLI} slack icon ${source.path}\`.\n`
|
|
383
|
+
);
|
|
384
|
+
return "failed";
|
|
434
385
|
}
|
|
435
|
-
const retryWith = source.kind === "url" ? source.url : source.path;
|
|
436
|
-
ctx.err(
|
|
437
|
-
`warning: setting the Slack app icon failed: ${failure}\nRetry with \`${CLI} slack icon ${retryWith}\`.\n`
|
|
438
|
-
);
|
|
439
|
-
return "failed";
|
|
440
386
|
}
|
|
441
387
|
|
|
442
388
|
/**
|
|
@@ -473,123 +419,12 @@ async function prepareProvision(
|
|
|
473
419
|
err,
|
|
474
420
|
openBrowserImpl: options.openBrowserImpl ?? openBrowser,
|
|
475
421
|
pollIntervalMs:
|
|
476
|
-
options.connectPollIntervalMs ??
|
|
422
|
+
options.connectPollIntervalMs ?? DEFAULT_WIZARD_POLL_INTERVAL_MS,
|
|
477
423
|
pollTimeoutMs:
|
|
478
|
-
options.connectPollTimeoutMs ??
|
|
424
|
+
options.connectPollTimeoutMs ?? DEFAULT_WIZARD_POLL_TIMEOUT_MS,
|
|
479
425
|
};
|
|
480
426
|
}
|
|
481
427
|
|
|
482
|
-
/** What the connect poll waits for in `GET /status`. */
|
|
483
|
-
interface ConnectWaitTarget {
|
|
484
|
-
/** Wait for this workspace to be connected (else: any workspace). */
|
|
485
|
-
slackTeamId?: string;
|
|
486
|
-
/**
|
|
487
|
-
* Wait for connectedAt to move past this value — a fresh grant. Used
|
|
488
|
-
* whenever the workspace row already exists and only the new approval
|
|
489
|
-
* bumps its timestamp.
|
|
490
|
-
*/
|
|
491
|
-
notConnectedAt?: string;
|
|
492
|
-
}
|
|
493
|
-
|
|
494
|
-
/**
|
|
495
|
-
* Slack's team-scoped admin page listing pending app-install requests.
|
|
496
|
-
* Provisioned apps have no OAuth flow (no redirect URLs, install-from-Slack
|
|
497
|
-
* disabled), so the oauth_authorize_url Slack returns from
|
|
498
|
-
* apps.manifest.create dead-ends; approval happens here instead.
|
|
499
|
-
*/
|
|
500
|
-
function slackAppRequestsUrl(slackTeamId: string): string {
|
|
501
|
-
return `https://app.slack.com/apps-manage/${encodeURIComponent(slackTeamId)}/integrations/requests`;
|
|
502
|
-
}
|
|
503
|
-
|
|
504
|
-
/** "Name (T0123ABCD)", or the bare id when the backend has no name. */
|
|
505
|
-
function formatWorkspace(workspace: SlackConnectedWorkspace): string {
|
|
506
|
-
const name = workspace.slackTeamName ?? "";
|
|
507
|
-
return name === ""
|
|
508
|
-
? workspace.slackTeamId
|
|
509
|
-
: `${name} (${workspace.slackTeamId})`;
|
|
510
|
-
}
|
|
511
|
-
|
|
512
|
-
function matchConnectedWorkspace(
|
|
513
|
-
workspaces: SlackConnectedWorkspace[],
|
|
514
|
-
wait: ConnectWaitTarget
|
|
515
|
-
): SlackConnectedWorkspace | undefined {
|
|
516
|
-
const candidates =
|
|
517
|
-
wait.slackTeamId === undefined
|
|
518
|
-
? workspaces
|
|
519
|
-
: workspaces.filter(
|
|
520
|
-
(workspace) => workspace.slackTeamId === wait.slackTeamId
|
|
521
|
-
);
|
|
522
|
-
if (wait.notConnectedAt === undefined) {
|
|
523
|
-
return candidates[0];
|
|
524
|
-
}
|
|
525
|
-
return candidates.find(
|
|
526
|
-
(workspace) => workspace.connectedAt !== wait.notConnectedAt
|
|
527
|
-
);
|
|
528
|
-
}
|
|
529
|
-
|
|
530
|
-
/**
|
|
531
|
-
* Mint the browser consent URL, open it, and poll `GET /status` until the
|
|
532
|
-
* awaited workspace shows up (or the poll times out). Returns the
|
|
533
|
-
* connected workspace, or an exit code with the error already rendered.
|
|
534
|
-
*/
|
|
535
|
-
async function runConnectFlow(
|
|
536
|
-
ctx: SlackProvisionContext,
|
|
537
|
-
wait: ConnectWaitTarget
|
|
538
|
-
): Promise<SlackConnectedWorkspace | number> {
|
|
539
|
-
let url: string;
|
|
540
|
-
try {
|
|
541
|
-
url = await ctx.client.connect(ctx.teamId);
|
|
542
|
-
} catch (error) {
|
|
543
|
-
return renderSlackApiError(error, ctx.err);
|
|
544
|
-
}
|
|
545
|
-
ctx.err(
|
|
546
|
-
`\nConnect your Slack workspace to Cursor — approve in the browser:\n\n ${url}\n\n` +
|
|
547
|
-
`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` +
|
|
548
|
-
`(waiting for the Slack consent to complete, up to ${Math.round(ctx.pollTimeoutMs / 60_000)} minutes)\n`
|
|
549
|
-
);
|
|
550
|
-
ctx.openBrowserImpl(url);
|
|
551
|
-
const deadline = Date.now() + ctx.pollTimeoutMs;
|
|
552
|
-
let lastPollFailure: string | undefined;
|
|
553
|
-
for (;;) {
|
|
554
|
-
let workspace: SlackConnectedWorkspace | undefined;
|
|
555
|
-
let sleepMs = ctx.pollIntervalMs;
|
|
556
|
-
try {
|
|
557
|
-
const status = await ctx.client.status(ctx.teamId);
|
|
558
|
-
workspace = matchConnectedWorkspace(status.workspaces, wait);
|
|
559
|
-
lastPollFailure = undefined;
|
|
560
|
-
} catch (error) {
|
|
561
|
-
// Transient poll failures must not abort a minted consent flow; only
|
|
562
|
-
// a still-failing poll at the deadline surfaces the error.
|
|
563
|
-
if (Date.now() >= deadline) {
|
|
564
|
-
return renderSlackApiError(error, ctx.err);
|
|
565
|
-
}
|
|
566
|
-
const failure = describeError(error);
|
|
567
|
-
if (failure !== lastPollFailure) {
|
|
568
|
-
lastPollFailure = failure;
|
|
569
|
-
ctx.err(`(status poll failed, retrying: ${failure})\n`);
|
|
570
|
-
}
|
|
571
|
-
if (
|
|
572
|
-
error instanceof SlackProvisionApiError &&
|
|
573
|
-
error.retryAfterSeconds !== undefined
|
|
574
|
-
) {
|
|
575
|
-
// Respect backend backpressure instead of hammering at the poll rate.
|
|
576
|
-
sleepMs = Math.max(sleepMs, error.retryAfterSeconds * 1000);
|
|
577
|
-
}
|
|
578
|
-
}
|
|
579
|
-
if (workspace !== undefined) {
|
|
580
|
-
ctx.err(`Connected Slack workspace ${workspace.slackTeamId}.\n`);
|
|
581
|
-
return workspace;
|
|
582
|
-
}
|
|
583
|
-
if (Date.now() >= deadline) {
|
|
584
|
-
ctx.err(
|
|
585
|
-
`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`
|
|
586
|
-
);
|
|
587
|
-
return 1;
|
|
588
|
-
}
|
|
589
|
-
await sleep(sleepMs);
|
|
590
|
-
}
|
|
591
|
-
}
|
|
592
|
-
|
|
593
428
|
export async function cmdSlackCreate(
|
|
594
429
|
options: SlackCliOptions
|
|
595
430
|
): Promise<number> {
|
|
@@ -597,7 +432,6 @@ export async function cmdSlackCreate(
|
|
|
597
432
|
if (typeof target === "number") {
|
|
598
433
|
return target;
|
|
599
434
|
}
|
|
600
|
-
// Resolved before any network I/O so a bad path/type/size fails fast.
|
|
601
435
|
let iconSource: SlackIconSource | undefined;
|
|
602
436
|
if (options.icon !== undefined) {
|
|
603
437
|
const resolved = await resolveSlackIconSource(
|
|
@@ -614,43 +448,6 @@ export async function cmdSlackCreate(
|
|
|
614
448
|
return ctx;
|
|
615
449
|
}
|
|
616
450
|
|
|
617
|
-
// Workspace: --slack-team wins; one → use it; none → connect; several → ask.
|
|
618
|
-
let status: SlackProvisionStatus;
|
|
619
|
-
try {
|
|
620
|
-
status = await ctx.client.status(ctx.teamId);
|
|
621
|
-
} catch (error) {
|
|
622
|
-
return renderSlackApiError(error, ctx.err);
|
|
623
|
-
}
|
|
624
|
-
let workspace: SlackConnectedWorkspace | undefined;
|
|
625
|
-
if (options.slackTeam !== undefined) {
|
|
626
|
-
workspace = status.workspaces.find(
|
|
627
|
-
(candidate) => candidate.slackTeamId === options.slackTeam
|
|
628
|
-
);
|
|
629
|
-
} else if (status.workspaces.length === 1) {
|
|
630
|
-
workspace = status.workspaces[0];
|
|
631
|
-
} else if (status.workspaces.length > 1) {
|
|
632
|
-
ctx.err(
|
|
633
|
-
`Multiple Slack workspaces are connected:\n` +
|
|
634
|
-
status.workspaces
|
|
635
|
-
.map(
|
|
636
|
-
(candidate) =>
|
|
637
|
-
` - ${formatWorkspace(candidate)} (Slack user ${candidate.slackUserId}, connected ${candidate.connectedAt})\n`
|
|
638
|
-
)
|
|
639
|
-
.join("") +
|
|
640
|
-
`Pass --slack-team <id> to pick one.\n`
|
|
641
|
-
);
|
|
642
|
-
return 1;
|
|
643
|
-
}
|
|
644
|
-
if (workspace === undefined) {
|
|
645
|
-
const connected = await runConnectFlow(ctx, {
|
|
646
|
-
slackTeamId: options.slackTeam,
|
|
647
|
-
});
|
|
648
|
-
if (typeof connected === "number") {
|
|
649
|
-
return connected;
|
|
650
|
-
}
|
|
651
|
-
workspace = connected;
|
|
652
|
-
}
|
|
653
|
-
|
|
654
451
|
let wrote: string[];
|
|
655
452
|
try {
|
|
656
453
|
wrote = await scaffoldSlackChannelFiles(options, target);
|
|
@@ -661,106 +458,93 @@ export async function cmdSlackCreate(
|
|
|
661
458
|
if (wrote.length > 0 && !ctx.json) {
|
|
662
459
|
ctx.err(`Scaffolded the Slack channel: ${wrote.join(", ")}\n`);
|
|
663
460
|
}
|
|
664
|
-
// A re-run rebuilds this manifest from flags, so backend-side
|
|
665
|
-
// customizations reset to it (documented in the skill).
|
|
666
|
-
const manifest = buildSlackManifest({
|
|
667
|
-
name: target.name,
|
|
668
|
-
env: target.env,
|
|
669
|
-
channelPosts: options.channelPosts === true,
|
|
670
|
-
});
|
|
671
|
-
const manifestJson = JSON.stringify(manifest);
|
|
672
461
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
462
|
+
const source = await inferDeployGitSource(target.dir);
|
|
463
|
+
let minted: SlackWizardMintResult;
|
|
464
|
+
try {
|
|
465
|
+
minted = await ctx.client.mintWizard({
|
|
466
|
+
teamId: ctx.teamId,
|
|
467
|
+
slug: target.slug,
|
|
468
|
+
env: target.env,
|
|
469
|
+
envPrefix: target.envPrefix,
|
|
470
|
+
name: options.name ?? target.name,
|
|
471
|
+
gitRepoUrl: source.gitRepoUrl,
|
|
472
|
+
gitRef: source.gitRef,
|
|
473
|
+
agentPath: source.agentPath,
|
|
474
|
+
channelPosts: options.channelPosts === true ? true : undefined,
|
|
475
|
+
slackTeamId: options.slackTeam,
|
|
476
|
+
iconUrl: iconSource?.kind === "url" ? iconSource.url : undefined,
|
|
477
|
+
force: options.force === true ? true : undefined,
|
|
478
|
+
});
|
|
479
|
+
} catch (error) {
|
|
480
|
+
return renderSlackApiError(error, ctx.err);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
ctx.err(
|
|
484
|
+
`Finish Slack setup in the browser:\n\n ${minted.url}\n\n` +
|
|
485
|
+
`Sign in as the same Cursor account that ran this command. ` +
|
|
486
|
+
`The CLI waits here (up to ${Math.round(ctx.pollTimeoutMs / 60_000)} minutes).\n`
|
|
487
|
+
);
|
|
488
|
+
ctx.openBrowserImpl(minted.url);
|
|
489
|
+
|
|
490
|
+
const deadline = Date.now() + ctx.pollTimeoutMs;
|
|
491
|
+
let sawPendingAdmin = false;
|
|
492
|
+
let poll: SlackWizardPollResult | undefined;
|
|
677
493
|
for (;;) {
|
|
678
494
|
try {
|
|
679
|
-
|
|
680
|
-
teamId: ctx.teamId,
|
|
681
|
-
slackTeamId: workspace.slackTeamId,
|
|
682
|
-
slug: target.slug,
|
|
683
|
-
env: target.env,
|
|
684
|
-
manifest: manifestJson,
|
|
685
|
-
icon: iconSource?.kind === "url" ? { url: iconSource.url } : undefined,
|
|
686
|
-
force: options.force === true ? true : undefined,
|
|
687
|
-
});
|
|
688
|
-
break;
|
|
495
|
+
poll = await ctx.client.pollWizard(minted.wizardId);
|
|
689
496
|
} catch (error) {
|
|
497
|
+
if (Date.now() >= deadline) {
|
|
498
|
+
return renderSlackApiError(error, ctx.err);
|
|
499
|
+
}
|
|
500
|
+
await sleep(ctx.pollIntervalMs);
|
|
501
|
+
continue;
|
|
502
|
+
}
|
|
503
|
+
if (poll.status === "complete") {
|
|
504
|
+
break;
|
|
505
|
+
}
|
|
506
|
+
if (poll.status === "failed") {
|
|
507
|
+
ctx.err(`${poll.error ?? "Slack setup failed"}\n`);
|
|
508
|
+
return 1;
|
|
509
|
+
}
|
|
510
|
+
if (poll.status === "pending_admin_approval" && !sawPendingAdmin) {
|
|
511
|
+
sawPendingAdmin = true;
|
|
512
|
+
const appSuffix = poll.appId === undefined ? "" : ` (${poll.appId})`;
|
|
513
|
+
ctx.err(
|
|
514
|
+
`Waiting for a Slack admin to approve the app${appSuffix}. Keep this command running.\n` +
|
|
515
|
+
`Managed install does not file the request. Open Slack to submit it; that same page lets you send a reminder to workspace owners.\n`
|
|
516
|
+
);
|
|
690
517
|
if (
|
|
691
|
-
|
|
692
|
-
|
|
693
|
-
error.code === "insufficient_scopes" ||
|
|
694
|
-
error.code === "manager_reauth_required") &&
|
|
695
|
-
!connectRetried
|
|
518
|
+
poll.oauthAuthorizeUrl !== undefined &&
|
|
519
|
+
poll.oauthAuthorizeUrl.length > 0
|
|
696
520
|
) {
|
|
697
|
-
|
|
698
|
-
// All three codes arrive holding a row that GET /status already
|
|
699
|
-
// lists, so the poll waits for a fresh grant instead of re-matching
|
|
700
|
-
// that row.
|
|
701
|
-
const wait: ConnectWaitTarget = {
|
|
702
|
-
slackTeamId: workspace.slackTeamId,
|
|
703
|
-
notConnectedAt: workspace.connectedAt,
|
|
704
|
-
};
|
|
705
|
-
if (error.code === "insufficient_scopes") {
|
|
706
|
-
ctx.err(
|
|
707
|
-
`The Slack connection is missing scopes provisioning needs; approving the consent link again grants them.\n`
|
|
708
|
-
);
|
|
709
|
-
} else if (error.code === "manager_reauth_required") {
|
|
710
|
-
ctx.err(
|
|
711
|
-
`Slack revoked Cursor's authorization for workspace ${workspace.slackTeamId}; re-approving the consent link restores it.\n`
|
|
712
|
-
);
|
|
713
|
-
} else {
|
|
714
|
-
ctx.err(
|
|
715
|
-
`The Slack workspace is not connected to Cursor; starting the connect flow.\n`
|
|
716
|
-
);
|
|
717
|
-
}
|
|
718
|
-
const connected = await runConnectFlow(ctx, wait);
|
|
719
|
-
if (typeof connected === "number") {
|
|
720
|
-
return connected;
|
|
721
|
-
}
|
|
722
|
-
workspace = connected;
|
|
723
|
-
continue;
|
|
521
|
+
ctx.err(` ${poll.oauthAuthorizeUrl}\n`);
|
|
724
522
|
}
|
|
725
|
-
|
|
523
|
+
ctx.err("After an admin approves, click Retry in the dashboard.\n");
|
|
726
524
|
}
|
|
727
|
-
|
|
728
|
-
|
|
729
|
-
const iconStatus = await applySlackCreateIcon(ctx, iconSource, result);
|
|
730
|
-
|
|
731
|
-
if (result.status === "pending_admin_approval") {
|
|
732
|
-
const approveUrl = slackAppRequestsUrl(workspace.slackTeamId);
|
|
733
|
-
if (ctx.json) {
|
|
734
|
-
ctx.out(
|
|
735
|
-
`${JSON.stringify(
|
|
736
|
-
{
|
|
737
|
-
ok: false,
|
|
738
|
-
appId: result.appId,
|
|
739
|
-
status: result.status,
|
|
740
|
-
slackTeamId: workspace.slackTeamId,
|
|
741
|
-
envKeys: target.envKeys,
|
|
742
|
-
wrote,
|
|
743
|
-
approveUrl,
|
|
744
|
-
iconStatus,
|
|
745
|
-
},
|
|
746
|
-
null,
|
|
747
|
-
2
|
|
748
|
-
)}\n`
|
|
749
|
-
);
|
|
750
|
-
} else {
|
|
525
|
+
if (Date.now() >= deadline) {
|
|
751
526
|
ctx.err(
|
|
752
|
-
`
|
|
753
|
-
`\n A workspace admin can approve it here:\n\n ${approveUrl}\n\n` +
|
|
754
|
-
`Once an admin approves the app, re-run \`${CLI} slack create\` to finish the install and mint the tokens.\n`
|
|
527
|
+
`Timed out waiting for Slack setup. Finish it in the browser, then re-run \`${CLI} slack create\`.\n ${minted.url}\n`
|
|
755
528
|
);
|
|
529
|
+
return 1;
|
|
756
530
|
}
|
|
531
|
+
await sleep(ctx.pollIntervalMs);
|
|
532
|
+
}
|
|
533
|
+
|
|
534
|
+
if (poll === undefined || poll.localEnv === undefined) {
|
|
535
|
+
ctx.err(
|
|
536
|
+
poll.localEnvClaimed === true
|
|
537
|
+
? "Tokens were already claimed by another poll; re-run create to rotate them.\n"
|
|
538
|
+
: "Slack setup finished but no tokens were returned; re-run create.\n"
|
|
539
|
+
);
|
|
757
540
|
return 1;
|
|
758
541
|
}
|
|
759
542
|
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
543
|
+
const envEntries = [
|
|
544
|
+
{ key: target.envKeys.bot, value: poll.localEnv.botToken },
|
|
545
|
+
];
|
|
546
|
+
if (poll.localEnv.appToken !== undefined) {
|
|
547
|
+
envEntries.push({ key: target.envKeys.app, value: poll.localEnv.appToken });
|
|
764
548
|
}
|
|
765
549
|
const envFile = join(target.dir, ".env.local");
|
|
766
550
|
try {
|
|
@@ -776,20 +560,24 @@ export async function cmdSlackCreate(
|
|
|
776
560
|
for (const { key, value } of envEntries) {
|
|
777
561
|
process.env[key] = value;
|
|
778
562
|
}
|
|
779
|
-
if (
|
|
563
|
+
if (poll.localEnv.appToken === undefined) {
|
|
780
564
|
ctx.err(
|
|
781
|
-
`warning: Slack did not return an app-level token, so ${target.envKeys.app} was not written
|
|
565
|
+
`warning: Slack did not return an app-level token, so ${target.envKeys.app} was not written; re-run \`${CLI} slack create\` to recover it.\n`
|
|
782
566
|
);
|
|
783
567
|
}
|
|
784
568
|
|
|
569
|
+
let iconStatus: "set" | "failed" | undefined;
|
|
570
|
+
if (poll.appId !== undefined) {
|
|
571
|
+
iconStatus = await applySlackCreateFileIcon(ctx, iconSource, poll.appId);
|
|
572
|
+
}
|
|
573
|
+
|
|
785
574
|
if (ctx.json) {
|
|
786
575
|
ctx.out(
|
|
787
576
|
`${JSON.stringify(
|
|
788
577
|
{
|
|
789
578
|
ok: true,
|
|
790
|
-
appId:
|
|
791
|
-
status:
|
|
792
|
-
slackTeamId: workspace.slackTeamId,
|
|
579
|
+
appId: poll.appId,
|
|
580
|
+
status: poll.status,
|
|
793
581
|
envKeys: target.envKeys,
|
|
794
582
|
wrote,
|
|
795
583
|
iconStatus,
|
|
@@ -802,8 +590,9 @@ export async function cmdSlackCreate(
|
|
|
802
590
|
}
|
|
803
591
|
|
|
804
592
|
ctx.out(
|
|
805
|
-
`
|
|
806
|
-
|
|
593
|
+
`Slack is connected for ${target.slug}` +
|
|
594
|
+
(poll.appId === undefined ? "" : ` (app ${poll.appId})`) +
|
|
595
|
+
`.\n wrote ${envEntries.map((entry) => entry.key).join(" + ")} to ${envFile}\n\n`
|
|
807
596
|
);
|
|
808
597
|
|
|
809
598
|
const doctor = options.doctorImpl ?? runSlackDoctor;
|
|
@@ -811,11 +600,10 @@ export async function cmdSlackCreate(
|
|
|
811
600
|
target.envPrefix === undefined ? {} : { envPrefix: target.envPrefix }
|
|
812
601
|
);
|
|
813
602
|
ctx.out(renderDoctorReport(doctorResult));
|
|
814
|
-
|
|
815
603
|
ctx.out(
|
|
816
604
|
`\nNext steps:\n` +
|
|
817
|
-
` 1. ${CLI}
|
|
818
|
-
` 2.
|
|
605
|
+
` 1. ${CLI} serve --dir ${options.dir} --dev\n` +
|
|
606
|
+
` 2. Invite the bot in Slack and @mention it.\n`
|
|
819
607
|
);
|
|
820
608
|
return 0;
|
|
821
609
|
}
|
|
@@ -988,10 +776,8 @@ function formatAppList(
|
|
|
988
776
|
}
|
|
989
777
|
|
|
990
778
|
/**
|
|
991
|
-
* Scaffold
|
|
992
|
-
*
|
|
993
|
-
* EEXIST means "already scaffolded" (provisioning builds its manifest in
|
|
994
|
-
* memory and does not need the on-disk copies).
|
|
779
|
+
* Scaffold `agent/channels/slack.ts` when absent. Existing files are
|
|
780
|
+
* never overwritten: initSlackChannel writes with `wx`.
|
|
995
781
|
*/
|
|
996
782
|
async function scaffoldSlackChannelFiles(
|
|
997
783
|
options: SlackCliOptions,
|
|
@@ -1109,43 +895,6 @@ function renderDoctorReport(result: SlackDoctorResult): string {
|
|
|
1109
895
|
return text;
|
|
1110
896
|
}
|
|
1111
897
|
|
|
1112
|
-
/**
|
|
1113
|
-
* Actionable rendering for `POST /apps` failures the create flow does not
|
|
1114
|
-
* retry itself (the connect-and-retry codes are handled by the caller).
|
|
1115
|
-
*/
|
|
1116
|
-
function renderCreateProvisionError(
|
|
1117
|
-
error: unknown,
|
|
1118
|
-
err: (text: string) => void
|
|
1119
|
-
): number {
|
|
1120
|
-
if (!(error instanceof SlackProvisionApiError)) {
|
|
1121
|
-
throw error;
|
|
1122
|
-
}
|
|
1123
|
-
switch (error.code) {
|
|
1124
|
-
case "app_conflict":
|
|
1125
|
-
err(
|
|
1126
|
-
`${error.message}\nA concurrent \`${CLI} slack create\` for this agent won the race; re-run create to pick up that app.\n`
|
|
1127
|
-
);
|
|
1128
|
-
return 1;
|
|
1129
|
-
case "ratelimited":
|
|
1130
|
-
err(`${error.message}\nRe-run \`${CLI} slack create\` after the wait.\n`);
|
|
1131
|
-
return 1;
|
|
1132
|
-
case "invalid_app_id":
|
|
1133
|
-
err(
|
|
1134
|
-
`${error.message}\nThe provisioned Slack app seems to have been deleted outside Cursor. Run \`${CLI} slack destroy\` to drop the stale record, then re-run \`${CLI} slack create\`.\n`
|
|
1135
|
-
);
|
|
1136
|
-
return 1;
|
|
1137
|
-
case "manifest_drift":
|
|
1138
|
-
err(
|
|
1139
|
-
`${error.message}\n` +
|
|
1140
|
-
(error.changedPaths ?? []).map((path) => ` - ${path}\n`).join("") +
|
|
1141
|
-
`Re-run \`${CLI} slack create --force\` to overwrite those edits.\n`
|
|
1142
|
-
);
|
|
1143
|
-
return 1;
|
|
1144
|
-
default:
|
|
1145
|
-
return renderSlackApiError(error, err);
|
|
1146
|
-
}
|
|
1147
|
-
}
|
|
1148
|
-
|
|
1149
898
|
function renderSlackApiError(
|
|
1150
899
|
error: unknown,
|
|
1151
900
|
err: (text: string) => void
|
|
@@ -1155,9 +904,7 @@ function renderSlackApiError(
|
|
|
1155
904
|
}
|
|
1156
905
|
err(
|
|
1157
906
|
error.code === "manager_reauth_required"
|
|
1158
|
-
?
|
|
1159
|
-
// flow before provisioning.
|
|
1160
|
-
`${error.message}\nRe-approve the Slack consent by re-running \`${CLI} slack create\`, which reconnects the workspace first.\n`
|
|
907
|
+
? `${error.message}\nRe-run \`${CLI} slack create\` and finish Slack consent in the dashboard.\n`
|
|
1161
908
|
: `${error.message}\n`
|
|
1162
909
|
);
|
|
1163
910
|
return 1;
|