@cursor/july 0.1.19 → 0.1.21
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 +10 -0
- package/dist/bin/agent-serve.js +0 -0
- package/dist/channels/github/instrument.d.ts +20 -0
- package/dist/channels/github/instrument.d.ts.map +1 -0
- package/dist/channels/slack/api.d.ts +2 -0
- package/dist/channels/slack/api.d.ts.map +1 -1
- package/dist/channels/slack/api.js +3 -0
- package/dist/channels/slack/bot-mentions.d.ts +106 -0
- package/dist/channels/slack/bot-mentions.d.ts.map +1 -0
- package/dist/channels/slack/bot-mentions.js +243 -0
- package/dist/channels/slack/dispatch.d.ts +6 -0
- package/dist/channels/slack/dispatch.d.ts.map +1 -1
- package/dist/channels/slack/dispatch.js +24 -3
- package/dist/channels/slack/inbound.d.ts +10 -1
- package/dist/channels/slack/inbound.d.ts.map +1 -1
- package/dist/channels/slack/inbound.js +12 -4
- package/dist/channels/slack/index.d.ts +1 -0
- package/dist/channels/slack/index.d.ts.map +1 -1
- package/dist/channels/slack/index.js +1 -0
- package/dist/channels/slack/slack-channel.d.ts.map +1 -1
- package/dist/channels/slack/slack-channel.js +61 -22
- package/dist/channels/slack/types.d.ts +58 -0
- package/dist/channels/slack/types.d.ts.map +1 -1
- package/dist/docs/404.html +1 -1
- package/dist/docs/ab.html +2 -2
- package/dist/docs/assets/{app.CrsWMchO.js → app.jDxLzWv4.js} +1 -1
- package/dist/docs/assets/chunks/@localSearchIndexroot.DoJHJjqF.js +1 -0
- package/dist/docs/assets/chunks/{VPLocalSearchBox.D1JqzSh8.js → VPLocalSearchBox.Y6bDR1-a.js} +1 -1
- package/dist/docs/assets/chunks/{theme.DaBvZYwl.js → theme.CLazCWlJ.js} +2 -2
- package/dist/docs/building-with-agents.html +2 -2
- package/dist/docs/concepts.html +2 -2
- package/dist/docs/deployment.html +2 -2
- package/dist/docs/evals.html +2 -2
- package/dist/docs/example-agents/approval-buddy.html +2 -2
- package/dist/docs/example-agents/benny.html +2 -2
- package/dist/docs/example-agents/bugbot.html +2 -2
- package/dist/docs/example-agents/codebase-wiki.html +2 -2
- package/dist/docs/example-agents/codeowners-review.html +2 -2
- package/dist/docs/example-agents/concierge.html +2 -2
- package/dist/docs/example-agents/fsd.html +2 -2
- package/dist/docs/example-agents/index.html +2 -2
- package/dist/docs/example-agents/knowledge-base.html +2 -2
- package/dist/docs/example-agents/oncall.html +2 -2
- package/dist/docs/example-agents/security-reviewer.html +2 -2
- package/dist/docs/example-agents/slack-agent.html +2 -2
- package/dist/docs/example-agents/weather-agent.html +2 -2
- package/dist/docs/guides/agent-to-agent.html +2 -2
- package/dist/docs/guides/cloud-runtime.html +2 -2
- package/dist/docs/guides/github.html +2 -2
- package/dist/docs/guides/human-in-the-loop.html +2 -2
- package/dist/docs/guides/mcp-oauth.html +2 -2
- package/dist/docs/guides/slack.html +2 -2
- package/dist/docs/guides/webhooks.html +2 -2
- package/dist/docs/hillclimbing.html +2 -2
- package/dist/docs/index.html +2 -2
- package/dist/docs/quickstart.html +2 -2
- package/dist/docs/reference/agent-config.html +2 -2
- package/dist/docs/reference/channels.html +2 -2
- package/dist/docs/reference/cli.html +2 -2
- package/dist/docs/reference/connections.html +2 -2
- package/dist/docs/reference/hooks.html +2 -2
- package/dist/docs/reference/http-api.html +2 -2
- package/dist/docs/reference/instructions.html +2 -2
- package/dist/docs/reference/playground.html +2 -2
- package/dist/docs/reference/project-layout.html +2 -2
- package/dist/docs/reference/prompt.html +2 -2
- package/dist/docs/reference/schedules.html +2 -2
- package/dist/docs/reference/sessions.html +2 -2
- package/dist/docs/reference/skills.html +2 -2
- package/dist/docs/reference/subagents.html +2 -2
- package/dist/docs/reference/tools.html +2 -2
- package/dist/docs/scaffolding-agents.html +2 -2
- package/dist/docs/storage.html +2 -2
- package/dist/docs/troubleshooting.html +2 -2
- package/dist/internal/json-dir-store.d.ts +32 -0
- package/dist/internal/json-dir-store.d.ts.map +1 -0
- package/dist/internal/json-dir-store.js +100 -0
- package/dist/internal/resolved-connections.d.ts +1 -1
- package/dist/internal/resolved-connections.d.ts.map +1 -1
- package/dist/internal/resolved-connections.js +10 -0
- package/dist/internal/session-engine.d.ts +4 -1
- package/dist/internal/session-engine.d.ts.map +1 -1
- package/dist/internal/session-engine.js +13 -2
- package/dist/playground/assets/index-CjOQ4hN9.css +1 -0
- package/dist/playground/assets/{index-C2SU2xV5.js → index-dshZQJCp.js} +46 -46
- package/dist/playground/index.html +2 -2
- package/dist/types.d.ts +7 -0
- package/dist/types.d.ts.map +1 -1
- package/dist/types.js +9 -0
- package/package.json +25 -25
- package/skills/create-agent/SKILL.md +36 -4
- package/skills/evals/SKILL.md +3 -0
- package/skills/framework-map/SKILL.md +14 -0
- package/skills/github/SKILL.md +6 -1
- package/skills/hillclimb/SKILL.md +28 -7
- package/src/bin/agent-serve.version.test.ts +62 -0
- package/src/channels/github/api.test.ts +64 -0
- package/src/channels/github/auth.test.ts +105 -0
- package/src/channels/github/cursor-account.test.ts +204 -0
- package/src/channels/github/forward.test.ts +457 -0
- package/src/channels/github/github.test.ts +937 -0
- package/src/channels/github/replay.test.ts +179 -0
- package/src/channels/slack/api.post-message.test.ts +148 -0
- package/src/channels/slack/api.ts +5 -0
- package/src/channels/slack/approvals.test.ts +328 -0
- package/src/channels/slack/block-actions.test.ts +452 -0
- package/src/channels/slack/bot-mentions.test.ts +217 -0
- package/src/channels/slack/bot-mentions.ts +315 -0
- package/src/channels/slack/channel-watch.test.ts +363 -0
- package/src/channels/slack/cursor-account.test.ts +253 -0
- package/src/channels/slack/defaults.final-post.test.ts +182 -0
- package/src/channels/slack/dispatch.test.ts +795 -0
- package/src/channels/slack/dispatch.ts +27 -1
- package/src/channels/slack/eval-directive.test.ts +273 -0
- package/src/channels/slack/inbound.ts +25 -4
- package/src/channels/slack/index.ts +1 -0
- package/src/channels/slack/message-body.test.ts +54 -0
- package/src/channels/slack/nudge-store.test.ts +143 -0
- package/src/channels/slack/slack-channel.ts +66 -8
- package/src/channels/slack/slack.test.ts +391 -0
- package/src/channels/slack/stop.test.ts +23 -0
- package/src/channels/slack/thread-context.test.ts +202 -0
- package/src/channels/slack/types.ts +60 -0
- package/src/evals/assertions.test.ts +580 -0
- package/src/evals/expect.test.ts +144 -0
- package/src/evals/judge.test.ts +181 -0
- package/src/evals/loaders.test.ts +132 -0
- package/src/evals/matchers.test.ts +95 -0
- package/src/evals/reporters.test.ts +303 -0
- package/src/evals/run-facts.test.ts +259 -0
- package/src/internal/ab-snapshot.test.ts +325 -0
- package/src/internal/approval-gate.test.ts +49 -0
- package/src/internal/approvals.integration.test.ts +383 -0
- package/src/internal/authored-loaders.test.ts +31 -0
- package/src/internal/builtin-tools/reminders.test.ts +201 -0
- package/src/internal/channel-route-schema.test.ts +294 -0
- package/src/internal/chat-attach.test.ts +262 -0
- package/src/internal/cli-deploy.test.ts +1991 -0
- package/src/internal/cli-mcp.test.ts +789 -0
- package/src/internal/cli-skills.test.ts +133 -0
- package/src/internal/cli-slack.test.ts +1647 -0
- package/src/internal/cloud-merge.test.ts +74 -0
- package/src/internal/cron.test.ts +22 -0
- package/src/internal/cursor/account-mcp.test.ts +807 -0
- package/src/internal/cursor/backend-client.test.ts +591 -0
- package/src/internal/cursor/credentials.test.ts +351 -0
- package/src/internal/cursor/github-credentials.test.ts +136 -0
- package/src/internal/cursor-account-mcp-auth.test.ts +310 -0
- package/src/internal/cursor-account.integration.test.ts +441 -0
- package/src/internal/cursor-event-relay.test.ts +746 -0
- package/src/internal/cursor-github-credentials.integration.test.ts +271 -0
- package/src/internal/cursor-slack-relay.test.ts +525 -0
- package/src/internal/deploy-source.test.ts +111 -0
- package/src/internal/discovery.builtin-tools.test.ts +94 -0
- package/src/internal/discovery.concurrency.test.ts +60 -0
- package/src/internal/discovery.cursor-account.test.ts +133 -0
- package/src/internal/discovery.cwd.test.ts +83 -0
- package/src/internal/discovery.hosting.test.ts +80 -0
- package/src/internal/discovery.identity.test.ts +44 -0
- package/src/internal/docs-site.test.ts +66 -0
- package/src/internal/duration.test.ts +29 -0
- package/src/internal/eval-judge-model.test.ts +187 -0
- package/src/internal/eval-run-store.cancel.test.ts +142 -0
- package/src/internal/eval-run-store.storage.test.ts +211 -0
- package/src/internal/eval-runner.http.test.ts +403 -0
- package/src/internal/eval-runner.run.test.ts +928 -0
- package/src/internal/evals-client.test.ts +307 -0
- package/src/internal/event-mapper.test.ts +243 -0
- package/src/internal/github-fanout.test.ts +213 -0
- package/src/internal/handleAgentServeTrigger.test.ts +179 -0
- package/src/internal/host-kv.test.ts +82 -0
- package/src/internal/host-platforms.test.ts +126 -0
- package/src/internal/http-channel.test.ts +402 -0
- package/src/internal/init-project.test.ts +269 -0
- package/src/internal/install-cursor-skills.test.ts +262 -0
- package/src/internal/local-env.test.ts +100 -0
- package/src/internal/log-ring.test.ts +31 -0
- package/src/internal/logs-client.test.ts +350 -0
- package/src/internal/mcp-endpoint.test.ts +436 -0
- package/src/internal/mcp-host.test.ts +298 -0
- package/src/internal/mcp-oauth.test.ts +148 -0
- package/src/internal/net.test.ts +17 -0
- package/src/internal/peer-connections.test.ts +128 -0
- package/src/internal/peer-mcp.integration.test.ts +289 -0
- package/src/internal/playground/toolchain.test.ts +53 -0
- package/src/internal/playground-cli.test.ts +187 -0
- package/src/internal/playground-proxy.test.ts +376 -0
- package/src/internal/prompt-context.integration.test.ts +232 -0
- package/src/internal/prompt-context.test.ts +127 -0
- package/src/internal/reminder-runner.test.ts +390 -0
- package/src/internal/reminder-store.test.ts +53 -0
- package/src/internal/request-headers.test.ts +27 -0
- package/src/internal/resolve-prod-target.test.ts +787 -0
- package/src/internal/resolved-connections.test.ts +295 -0
- package/src/internal/resolved-connections.ts +18 -1
- package/src/internal/router.test.ts +57 -0
- package/src/internal/sdk-runner.test.ts +290 -0
- package/src/internal/session-engine.coalesce.test.ts +169 -0
- package/src/internal/session-engine.concurrency.test.ts +250 -0
- package/src/internal/session-engine.host-oauth-mcp.test.ts +110 -0
- package/src/internal/session-engine.interrupt.test.ts +577 -0
- package/src/internal/session-engine.storage.test.ts +547 -0
- package/src/internal/session-engine.ts +15 -1
- package/src/internal/session-urls.test.ts +28 -0
- package/src/internal/sessions-client.test.ts +518 -0
- package/src/internal/storage-coordinator.test.ts +517 -0
- package/src/internal/tool-call.test.ts +458 -0
- package/src/internal/tool-result.test.ts +52 -0
- package/src/internal/trajectory.approvals.test.ts +83 -0
- package/src/internal/trajectory.subagents.test.ts +198 -0
- package/src/internal/turn-governor.test.ts +137 -0
- package/src/internal/update-check.test.ts +485 -0
- package/src/internal/workspace.test.ts +81 -0
- package/src/storage-backends/cursor-hosted.test.ts +121 -0
- package/src/types.ts +12 -0
- package/dist/docs/assets/chunks/@localSearchIndexroot.BnHRjfoe.js +0 -1
- package/dist/playground/assets/index-CidizGZv.css +0 -1
|
@@ -17,6 +17,11 @@ import type {
|
|
|
17
17
|
SendMessageFn,
|
|
18
18
|
} from "../../types.js";
|
|
19
19
|
import { slackContinuationKey } from "./api.js";
|
|
20
|
+
import {
|
|
21
|
+
normalizeBotMentionAllowlist,
|
|
22
|
+
SLACK_BOT_MENTION_DEFAULTS,
|
|
23
|
+
SlackBotMentionGate,
|
|
24
|
+
} from "./bot-mentions.js";
|
|
20
25
|
import {
|
|
21
26
|
type ChannelPostWatcher,
|
|
22
27
|
createChannelPostWatcher,
|
|
@@ -94,6 +99,14 @@ export function slackChannel<const Name extends string = string>(
|
|
|
94
99
|
"slackChannel({ cursorAccount: true }) does not support engagement.channelPosts — agents must be explicitly addressed (@mention, DM, or claimed-thread reply). Channel watching is Socket Mode only."
|
|
95
100
|
);
|
|
96
101
|
}
|
|
102
|
+
// Bot-mention wake needs the app's own bot identity (auth.test) and
|
|
103
|
+
// Socket Mode event delivery; the Cursor relay drops bot-authored
|
|
104
|
+
// mentions before they reach the agent.
|
|
105
|
+
if (options.engagement?.botMentions !== undefined) {
|
|
106
|
+
throw new Error(
|
|
107
|
+
"slackChannel({ cursorAccount: true }) does not support engagement.botMentions — bot-to-bot mention wake is Socket Mode only (author a dedicated Slack app channel)."
|
|
108
|
+
);
|
|
109
|
+
}
|
|
97
110
|
// Mentions/DMs are always routed by the backend. Disabling them
|
|
98
111
|
// client-side would black-hole events the webhook already stole from
|
|
99
112
|
// native Cursor handling — reject at construction instead.
|
|
@@ -156,6 +169,10 @@ export function slackChannel<const Name extends string = string>(
|
|
|
156
169
|
);
|
|
157
170
|
}
|
|
158
171
|
}
|
|
172
|
+
const botMentions = engagement.botMentions;
|
|
173
|
+
if (botMentions !== undefined) {
|
|
174
|
+
normalizeBotMentionAllowlist(botMentions.allowFrom);
|
|
175
|
+
}
|
|
159
176
|
const eventIds = createEventIdDedupe();
|
|
160
177
|
const toolApprovals = options.toolApprovals === true;
|
|
161
178
|
const interactivity = options.interactivity === true;
|
|
@@ -192,6 +209,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
192
209
|
|
|
193
210
|
let socketHandle: SlackSocketModeHandle | null = null;
|
|
194
211
|
let channelWatch: ChannelPostWatcher | undefined;
|
|
212
|
+
let botMentionGate: SlackBotMentionGate | undefined;
|
|
195
213
|
|
|
196
214
|
function inboundDeps(args: {
|
|
197
215
|
send: SendMessageFn;
|
|
@@ -232,6 +250,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
232
250
|
? {}
|
|
233
251
|
: { interruptContinuation: args.interruptContinuation }),
|
|
234
252
|
...(channelWatch === undefined ? {} : { channelWatch }),
|
|
253
|
+
...(botMentionGate === undefined ? {} : { botMentionGate }),
|
|
235
254
|
};
|
|
236
255
|
}
|
|
237
256
|
|
|
@@ -305,22 +324,60 @@ export function slackChannel<const Name extends string = string>(
|
|
|
305
324
|
return;
|
|
306
325
|
}
|
|
307
326
|
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
327
|
+
let botUserId: string | undefined;
|
|
328
|
+
let botId: string | undefined;
|
|
329
|
+
let webClient: InstanceType<
|
|
330
|
+
typeof import("@slack/web-api").WebClient
|
|
331
|
+
> | null = null;
|
|
332
|
+
if (channelPosts !== undefined || botMentions !== undefined) {
|
|
312
333
|
try {
|
|
313
334
|
const { WebClient } = await import("@slack/web-api");
|
|
314
|
-
|
|
315
|
-
const identity = await
|
|
335
|
+
webClient = new WebClient(botToken);
|
|
336
|
+
const identity = await webClient.auth.test();
|
|
316
337
|
botUserId =
|
|
317
338
|
typeof identity.user_id === "string" ? identity.user_id : undefined;
|
|
318
339
|
botId =
|
|
319
340
|
typeof identity.bot_id === "string" ? identity.bot_id : undefined;
|
|
341
|
+
} catch (error) {
|
|
342
|
+
getLog().error("bot identity lookup degraded", {
|
|
343
|
+
error: error instanceof Error ? error.message : String(error),
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
if (botMentions !== undefined) {
|
|
349
|
+
if (botUserId === undefined && botId === undefined) {
|
|
350
|
+
getLog().error("bot mentions disabled", {
|
|
351
|
+
reason:
|
|
352
|
+
"engagement.botMentions requires bot identity from auth.test (loop safety)",
|
|
353
|
+
});
|
|
354
|
+
} else {
|
|
355
|
+
botMentionGate = new SlackBotMentionGate(
|
|
356
|
+
botMentions,
|
|
357
|
+
{ botUserId, botId },
|
|
358
|
+
{ log: getLog() }
|
|
359
|
+
);
|
|
360
|
+
getLog().info("bot mentions enabled", {
|
|
361
|
+
allow_from: botMentions.allowFrom.join(","),
|
|
362
|
+
max_depth:
|
|
363
|
+
botMentions.maxDepth ?? SLACK_BOT_MENTION_DEFAULTS.maxDepth,
|
|
364
|
+
max_per_thread_per_hour:
|
|
365
|
+
botMentions.maxPerThreadPerHour ??
|
|
366
|
+
SLACK_BOT_MENTION_DEFAULTS.maxPerThreadPerHour,
|
|
367
|
+
max_per_thread_total:
|
|
368
|
+
botMentions.maxPerThreadTotal ??
|
|
369
|
+
SLACK_BOT_MENTION_DEFAULTS.maxPerThreadTotal,
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
if (channelPosts !== undefined) {
|
|
375
|
+
const allowlist = new SlackChannelAllowlist(channelPosts.allow);
|
|
376
|
+
try {
|
|
320
377
|
const names = allowlist.unresolvedNames();
|
|
321
|
-
if (names.length > 0) {
|
|
378
|
+
if (names.length > 0 && webClient !== null) {
|
|
322
379
|
allowlist.applyResolvedNames(
|
|
323
|
-
await resolveChannelNames(
|
|
380
|
+
await resolveChannelNames(webClient, names, getLog())
|
|
324
381
|
);
|
|
325
382
|
}
|
|
326
383
|
} catch (error) {
|
|
@@ -472,6 +529,7 @@ export function slackChannel<const Name extends string = string>(
|
|
|
472
529
|
getLog().info("channel stop");
|
|
473
530
|
channelWatch?.stop();
|
|
474
531
|
channelWatch = undefined;
|
|
532
|
+
botMentionGate = undefined;
|
|
475
533
|
await socketHandle?.stop();
|
|
476
534
|
socketHandle = null;
|
|
477
535
|
await options.onStop?.();
|
|
@@ -0,0 +1,391 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { SLACK_LOADING_MESSAGE_MAX_LEN } from "./constants.js";
|
|
3
|
+
import {
|
|
4
|
+
buildSlackManifest,
|
|
5
|
+
computeStreamAppend,
|
|
6
|
+
createSlackLogger,
|
|
7
|
+
credentialsFromEnv,
|
|
8
|
+
envPrefixFromName,
|
|
9
|
+
extractGithubPrRef,
|
|
10
|
+
parseAppMentionEvent,
|
|
11
|
+
parseDirectMessageEvent,
|
|
12
|
+
SLACK_EXTERNAL_APP_MANAGEMENT_URL,
|
|
13
|
+
slackChannel,
|
|
14
|
+
slackContinuationKey,
|
|
15
|
+
slackEnvKeys,
|
|
16
|
+
slackTextToMarkdown,
|
|
17
|
+
slackThreadPermalink,
|
|
18
|
+
truncateTypingStatus,
|
|
19
|
+
tryResolveAppToken,
|
|
20
|
+
tryResolveBotToken,
|
|
21
|
+
} from "./index.js";
|
|
22
|
+
|
|
23
|
+
describe("slackContinuationKey", () => {
|
|
24
|
+
it("joins channel and thread", () => {
|
|
25
|
+
expect(slackContinuationKey("C123", "1719.000")).toBe("C123:1719.000");
|
|
26
|
+
});
|
|
27
|
+
});
|
|
28
|
+
|
|
29
|
+
describe("slackThreadPermalink", () => {
|
|
30
|
+
it("prefers the client thread deep link when team is known", () => {
|
|
31
|
+
expect(
|
|
32
|
+
slackThreadPermalink({
|
|
33
|
+
teamId: "T1",
|
|
34
|
+
channelId: "C9",
|
|
35
|
+
threadTs: "1783451265.725729",
|
|
36
|
+
})
|
|
37
|
+
).toBe("https://app.slack.com/client/T1/C9/thread/C9-1783451265.725729");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("falls back to archives permalink without team", () => {
|
|
41
|
+
expect(
|
|
42
|
+
slackThreadPermalink({
|
|
43
|
+
channelId: "C9",
|
|
44
|
+
threadTs: "1783451265.725729",
|
|
45
|
+
})
|
|
46
|
+
).toBe(
|
|
47
|
+
"https://app.slack.com/archives/C9/p1783451265725729?thread_ts=1783451265.725729"
|
|
48
|
+
);
|
|
49
|
+
});
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe("createSlackLogger", () => {
|
|
53
|
+
it("formats levels and fields", () => {
|
|
54
|
+
const lines: string[] = [];
|
|
55
|
+
const log = createSlackLogger((line) => lines.push(line));
|
|
56
|
+
log.info("inbound", { kind: "app_mention", channel: "C9" });
|
|
57
|
+
log.warn("chat.startStream unavailable", { error: "missing_scope" });
|
|
58
|
+
expect(lines[0]).toBe(
|
|
59
|
+
"[agentkit/slack] inbound kind=app_mention channel=C9"
|
|
60
|
+
);
|
|
61
|
+
expect(lines[1]).toBe(
|
|
62
|
+
"[agentkit/slack] warn chat.startStream unavailable error=missing_scope"
|
|
63
|
+
);
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
describe("inbound parsers", () => {
|
|
68
|
+
it("parses app_mention", () => {
|
|
69
|
+
const message = parseAppMentionEvent({
|
|
70
|
+
type: "event_callback",
|
|
71
|
+
team_id: "T1",
|
|
72
|
+
event: {
|
|
73
|
+
type: "app_mention",
|
|
74
|
+
user: "U1",
|
|
75
|
+
text: "<@Ubot> hi there",
|
|
76
|
+
ts: "1.0",
|
|
77
|
+
channel: "C9",
|
|
78
|
+
},
|
|
79
|
+
});
|
|
80
|
+
expect(message).not.toBeNull();
|
|
81
|
+
expect(message?.channelId).toBe("C9");
|
|
82
|
+
expect(message?.threadTs).toBe("1.0");
|
|
83
|
+
expect(message?.markdown).toContain("hi there");
|
|
84
|
+
expect(message?.teamId).toBe("T1");
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
it("parses DMs and drops bot messages", () => {
|
|
88
|
+
const dm = parseDirectMessageEvent({
|
|
89
|
+
type: "event_callback",
|
|
90
|
+
team_id: "T1",
|
|
91
|
+
event: {
|
|
92
|
+
type: "message",
|
|
93
|
+
channel_type: "im",
|
|
94
|
+
user: "U1",
|
|
95
|
+
text: "hello",
|
|
96
|
+
ts: "2.0",
|
|
97
|
+
channel: "D1",
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
expect(dm?.markdown).toBe("hello");
|
|
101
|
+
|
|
102
|
+
const bot = parseDirectMessageEvent({
|
|
103
|
+
type: "event_callback",
|
|
104
|
+
event: {
|
|
105
|
+
type: "message",
|
|
106
|
+
channel_type: "im",
|
|
107
|
+
bot_id: "B1",
|
|
108
|
+
text: "noop",
|
|
109
|
+
ts: "2.0",
|
|
110
|
+
channel: "D1",
|
|
111
|
+
},
|
|
112
|
+
});
|
|
113
|
+
expect(bot).toBeNull();
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it("drops bot-authored app_mention events", () => {
|
|
117
|
+
expect(
|
|
118
|
+
parseAppMentionEvent({
|
|
119
|
+
type: "event_callback",
|
|
120
|
+
event: {
|
|
121
|
+
type: "app_mention",
|
|
122
|
+
bot_id: "B1",
|
|
123
|
+
text: "<@Ubot> loop",
|
|
124
|
+
ts: "1.0",
|
|
125
|
+
channel: "C9",
|
|
126
|
+
},
|
|
127
|
+
})
|
|
128
|
+
).toBeNull();
|
|
129
|
+
});
|
|
130
|
+
});
|
|
131
|
+
|
|
132
|
+
describe("slackTextToMarkdown", () => {
|
|
133
|
+
it("strips user mentions", () => {
|
|
134
|
+
expect(slackTextToMarkdown("<@U123> please help")).toBe(
|
|
135
|
+
"@U123 please help"
|
|
136
|
+
);
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe("buildSlackManifest", () => {
|
|
141
|
+
it("enables Socket Mode without request_url", () => {
|
|
142
|
+
const manifest = buildSlackManifest({
|
|
143
|
+
name: "Weather Bot",
|
|
144
|
+
env: "dev",
|
|
145
|
+
});
|
|
146
|
+
expect(manifest.display_information.name).toBe("Weather Bot (dev)");
|
|
147
|
+
expect(manifest.features.bot_user.display_name).toContain("dev");
|
|
148
|
+
expect(manifest.settings.socket_mode_enabled).toBe(true);
|
|
149
|
+
expect(
|
|
150
|
+
(manifest.settings.event_subscriptions as { request_url?: string })
|
|
151
|
+
.request_url
|
|
152
|
+
).toBeUndefined();
|
|
153
|
+
expect(manifest.oauth_config.scopes.bot).toContain("chat:write");
|
|
154
|
+
expect(manifest.settings.event_subscriptions.bot_events).toContain(
|
|
155
|
+
"app_mention"
|
|
156
|
+
);
|
|
157
|
+
expect(manifest.settings.interactivity.is_enabled).toBe(false);
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it("hides tokens and Slack-side installs, pointing management at Cursor", () => {
|
|
161
|
+
for (const env of ["dev", "prod"] as const) {
|
|
162
|
+
expect(
|
|
163
|
+
buildSlackManifest({ name: "Weather Bot", env }).settings
|
|
164
|
+
.managed_app_settings
|
|
165
|
+
).toEqual({
|
|
166
|
+
is_install_from_slack_disabled: true,
|
|
167
|
+
external_app_management_url: SLACK_EXTERNAL_APP_MANAGEMENT_URL,
|
|
168
|
+
});
|
|
169
|
+
}
|
|
170
|
+
expect(SLACK_EXTERNAL_APP_MANAGEMENT_URL).toBe(
|
|
171
|
+
"https://cursor.com/dashboard/deployed-agents"
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
it("omits managed_app_settings for hand-imported manifests", () => {
|
|
176
|
+
const manifest = buildSlackManifest({
|
|
177
|
+
name: "Weather Bot",
|
|
178
|
+
env: "prod",
|
|
179
|
+
managedByCursor: false,
|
|
180
|
+
});
|
|
181
|
+
expect("managed_app_settings" in manifest.settings).toBe(false);
|
|
182
|
+
});
|
|
183
|
+
|
|
184
|
+
it("pins Benny's Slack app to benny-dev with org deploy", () => {
|
|
185
|
+
for (const env of ["dev", "prod"] as const) {
|
|
186
|
+
const manifest = buildSlackManifest({
|
|
187
|
+
name: "Benny",
|
|
188
|
+
env,
|
|
189
|
+
channelPosts: true,
|
|
190
|
+
});
|
|
191
|
+
expect(manifest.display_information.name).toBe("benny-dev");
|
|
192
|
+
expect(manifest.features.bot_user.display_name).toBe("benny-dev");
|
|
193
|
+
expect(manifest.settings.org_deploy_enabled).toBe(true);
|
|
194
|
+
expect(manifest.settings.socket_mode_enabled).toBe(true);
|
|
195
|
+
expect(manifest.settings.token_rotation_enabled).toBe(false);
|
|
196
|
+
expect(manifest.settings.is_mcp_enabled).toBe(false);
|
|
197
|
+
}
|
|
198
|
+
expect(
|
|
199
|
+
buildSlackManifest({ name: "benny", env: "prod" }).features.bot_user
|
|
200
|
+
.display_name
|
|
201
|
+
).toBe("benny-dev");
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("enables interactivity when opted in", () => {
|
|
205
|
+
const manifest = buildSlackManifest({
|
|
206
|
+
name: "Weather Bot",
|
|
207
|
+
env: "prod",
|
|
208
|
+
interactivity: true,
|
|
209
|
+
});
|
|
210
|
+
expect(manifest.settings.interactivity.is_enabled).toBe(true);
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it("omits channel-post events by default and adds them when opted in", () => {
|
|
214
|
+
const defaults = buildSlackManifest({ name: "Bot", env: "dev" });
|
|
215
|
+
expect(defaults.settings.event_subscriptions.bot_events).not.toContain(
|
|
216
|
+
"message.channels"
|
|
217
|
+
);
|
|
218
|
+
|
|
219
|
+
const watching = buildSlackManifest({
|
|
220
|
+
name: "Bot",
|
|
221
|
+
env: "dev",
|
|
222
|
+
channelPosts: true,
|
|
223
|
+
});
|
|
224
|
+
expect(watching.settings.event_subscriptions.bot_events).toContain(
|
|
225
|
+
"message.channels"
|
|
226
|
+
);
|
|
227
|
+
expect(watching.settings.event_subscriptions.bot_events).toContain(
|
|
228
|
+
"message.groups"
|
|
229
|
+
);
|
|
230
|
+
});
|
|
231
|
+
|
|
232
|
+
it("defaults the description from the name and honors an explicit one", () => {
|
|
233
|
+
const bare = buildSlackManifest({ name: "Bot", env: "prod" });
|
|
234
|
+
expect(bare.display_information.description).toBe(
|
|
235
|
+
"Bot — an AI agent built with Cursor"
|
|
236
|
+
);
|
|
237
|
+
expect(bare.features.assistant_view?.assistant_description).toBe(
|
|
238
|
+
"Bot — an AI agent built with Cursor"
|
|
239
|
+
);
|
|
240
|
+
|
|
241
|
+
const described = buildSlackManifest({
|
|
242
|
+
name: "Bot",
|
|
243
|
+
env: "prod",
|
|
244
|
+
description: "Does things",
|
|
245
|
+
});
|
|
246
|
+
expect(described.display_information.description).toBe("Does things");
|
|
247
|
+
expect(described.features.assistant_view?.assistant_description).toBe(
|
|
248
|
+
"Does things"
|
|
249
|
+
);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it("keeps the default description inside Slack's 140-char bound", () => {
|
|
253
|
+
const manifest = buildSlackManifest({
|
|
254
|
+
name: "x".repeat(200),
|
|
255
|
+
env: "prod",
|
|
256
|
+
});
|
|
257
|
+
expect(manifest.display_information.description).toHaveLength(140);
|
|
258
|
+
expect(manifest.display_information.description).toContain(
|
|
259
|
+
"an AI agent built with Cursor"
|
|
260
|
+
);
|
|
261
|
+
});
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
describe("slackChannel engagement", () => {
|
|
265
|
+
it("rejects an empty channelPosts allowlist", () => {
|
|
266
|
+
expect(() =>
|
|
267
|
+
slackChannel({
|
|
268
|
+
engagement: { channelPosts: { allow: [" "] } },
|
|
269
|
+
})
|
|
270
|
+
).toThrow(/allow must list at least one channel/);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("surfaces watched channels in channel meta", () => {
|
|
274
|
+
const channel = slackChannel({
|
|
275
|
+
engagement: { channelPosts: { allow: ["#issues-glass", "C123"] } },
|
|
276
|
+
});
|
|
277
|
+
expect(channel.meta?.kind).toBe("slack");
|
|
278
|
+
expect(channel.meta?.slackChannelPosts).toEqual(["#issues-glass", "C123"]);
|
|
279
|
+
|
|
280
|
+
const plain = slackChannel();
|
|
281
|
+
expect(plain.meta?.slackChannelPosts).toBeUndefined();
|
|
282
|
+
});
|
|
283
|
+
});
|
|
284
|
+
|
|
285
|
+
describe("socket mode event payload", () => {
|
|
286
|
+
it("parses app_mention from the event body Slack delivers over Socket Mode", () => {
|
|
287
|
+
const message = parseAppMentionEvent({
|
|
288
|
+
type: "event_callback",
|
|
289
|
+
team_id: "T1",
|
|
290
|
+
event_id: "EvSOCKET1",
|
|
291
|
+
event: {
|
|
292
|
+
type: "app_mention",
|
|
293
|
+
user: "U1",
|
|
294
|
+
text: "<@Ubot> via socket hello",
|
|
295
|
+
ts: "9.1",
|
|
296
|
+
channel: "C9",
|
|
297
|
+
},
|
|
298
|
+
});
|
|
299
|
+
expect(message?.markdown).toContain("via socket hello");
|
|
300
|
+
expect(message?.channelId).toBe("C9");
|
|
301
|
+
});
|
|
302
|
+
});
|
|
303
|
+
|
|
304
|
+
describe("computeStreamAppend", () => {
|
|
305
|
+
it("returns full text when nothing was posted", () => {
|
|
306
|
+
expect(computeStreamAppend(null, "Hello")).toBe("Hello");
|
|
307
|
+
expect(computeStreamAppend("", "Hello")).toBe("Hello");
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it("returns the unsent suffix", () => {
|
|
311
|
+
expect(computeStreamAppend("Hello", "Hello world")).toBe(" world");
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
it("returns empty when text does not extend lastPosted", () => {
|
|
315
|
+
expect(computeStreamAppend("Hello", "Hi there")).toBe("");
|
|
316
|
+
});
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
describe("truncateTypingStatus", () => {
|
|
320
|
+
it("keeps loading_messages under Slack's 51-char limit", () => {
|
|
321
|
+
const long = "a".repeat(80);
|
|
322
|
+
const truncated = truncateTypingStatus(long);
|
|
323
|
+
expect(truncated.length).toBe(SLACK_LOADING_MESSAGE_MAX_LEN);
|
|
324
|
+
expect(truncated.endsWith("…")).toBe(true);
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe("env prefixes", () => {
|
|
329
|
+
it("normalizes agent names", () => {
|
|
330
|
+
expect(envPrefixFromName("bugbot-cloud")).toBe("BUGBOT_CLOUD");
|
|
331
|
+
expect(envPrefixFromName("security-reviewer")).toBe("SECURITY_REVIEWER");
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
it("builds per-agent env keys", () => {
|
|
335
|
+
expect(slackEnvKeys("BUGBOT")).toEqual({
|
|
336
|
+
bot: "BUGBOT_SLACK_BOT_TOKEN",
|
|
337
|
+
app: "BUGBOT_SLACK_APP_TOKEN",
|
|
338
|
+
});
|
|
339
|
+
expect(slackEnvKeys()).toEqual({
|
|
340
|
+
bot: "SLACK_BOT_TOKEN",
|
|
341
|
+
app: "SLACK_APP_TOKEN",
|
|
342
|
+
});
|
|
343
|
+
});
|
|
344
|
+
|
|
345
|
+
it("does not fall through to shared tokens for prefixed credentials", async () => {
|
|
346
|
+
const previousBot = process.env.SLACK_BOT_TOKEN;
|
|
347
|
+
const previousApp = process.env.SLACK_APP_TOKEN;
|
|
348
|
+
process.env.SLACK_BOT_TOKEN = "xoxb-shared";
|
|
349
|
+
process.env.SLACK_APP_TOKEN = "xapp-shared";
|
|
350
|
+
delete process.env.BUGBOT_SLACK_BOT_TOKEN;
|
|
351
|
+
delete process.env.BUGBOT_SLACK_APP_TOKEN;
|
|
352
|
+
try {
|
|
353
|
+
const credentials = credentialsFromEnv("BUGBOT");
|
|
354
|
+
expect(await tryResolveBotToken(credentials)).toBeUndefined();
|
|
355
|
+
expect(await tryResolveAppToken(credentials)).toBeUndefined();
|
|
356
|
+
} finally {
|
|
357
|
+
if (previousBot === undefined) {
|
|
358
|
+
delete process.env.SLACK_BOT_TOKEN;
|
|
359
|
+
} else {
|
|
360
|
+
process.env.SLACK_BOT_TOKEN = previousBot;
|
|
361
|
+
}
|
|
362
|
+
if (previousApp === undefined) {
|
|
363
|
+
delete process.env.SLACK_APP_TOKEN;
|
|
364
|
+
} else {
|
|
365
|
+
process.env.SLACK_APP_TOKEN = previousApp;
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
describe("extractGithubPrRef", () => {
|
|
372
|
+
it("parses URLs and owner/repo#N", () => {
|
|
373
|
+
expect(
|
|
374
|
+
extractGithubPrRef("please review https://github.com/acme/widget/pull/42")
|
|
375
|
+
).toEqual({
|
|
376
|
+
owner: "acme",
|
|
377
|
+
repo: "widget",
|
|
378
|
+
number: 42,
|
|
379
|
+
url: "https://github.com/acme/widget/pull/42",
|
|
380
|
+
label: "acme/widget#42",
|
|
381
|
+
});
|
|
382
|
+
expect(extractGithubPrRef("drive acme/widget#7 for me")).toEqual({
|
|
383
|
+
owner: "acme",
|
|
384
|
+
repo: "widget",
|
|
385
|
+
number: 7,
|
|
386
|
+
url: "https://github.com/acme/widget/pull/7",
|
|
387
|
+
label: "acme/widget#7",
|
|
388
|
+
});
|
|
389
|
+
expect(extractGithubPrRef("no pr here")).toBeUndefined();
|
|
390
|
+
});
|
|
391
|
+
});
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import { isWholeMessageStop, stripLeadingSlackMentions } from "./stop.js";
|
|
3
|
+
|
|
4
|
+
describe("isWholeMessageStop", () => {
|
|
5
|
+
it("matches bare stop and @mention stop", () => {
|
|
6
|
+
expect(isWholeMessageStop("stop")).toBe(true);
|
|
7
|
+
expect(isWholeMessageStop("STOP")).toBe(true);
|
|
8
|
+
expect(isWholeMessageStop("<@Ubot> stop")).toBe(true);
|
|
9
|
+
expect(isWholeMessageStop("<@Ubot> stop ")).toBe(true);
|
|
10
|
+
});
|
|
11
|
+
|
|
12
|
+
it("does not match embedded stop", () => {
|
|
13
|
+
expect(isWholeMessageStop("please stop investigating")).toBe(false);
|
|
14
|
+
expect(isWholeMessageStop("<@Ubot> stop the deploy")).toBe(false);
|
|
15
|
+
expect(isWholeMessageStop("")).toBe(false);
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
describe("stripLeadingSlackMentions", () => {
|
|
20
|
+
it("strips repeated leading mentions", () => {
|
|
21
|
+
expect(stripLeadingSlackMentions("<@A> <@B> stop")).toBe("stop");
|
|
22
|
+
});
|
|
23
|
+
});
|