@cursor/july 0.1.6 → 0.1.8

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 (179) hide show
  1. package/dist/bin/agent-serve.js +73 -19
  2. package/dist/channels/slack/channel-watch.d.ts +6 -1
  3. package/dist/channels/slack/channel-watch.d.ts.map +1 -1
  4. package/dist/channels/slack/channel-watch.js +22 -1
  5. package/dist/channels/slack/cursor-account.d.ts +4 -0
  6. package/dist/channels/slack/cursor-account.d.ts.map +1 -1
  7. package/dist/channels/slack/cursor-account.js +12 -2
  8. package/dist/channels/slack/defaults.d.ts +10 -5
  9. package/dist/channels/slack/defaults.d.ts.map +1 -1
  10. package/dist/channels/slack/defaults.js +39 -17
  11. package/dist/channels/slack/dispatch.d.ts +18 -0
  12. package/dist/channels/slack/dispatch.d.ts.map +1 -1
  13. package/dist/channels/slack/dispatch.js +141 -38
  14. package/dist/channels/slack/inbound.d.ts +13 -3
  15. package/dist/channels/slack/inbound.d.ts.map +1 -1
  16. package/dist/channels/slack/inbound.js +17 -9
  17. package/dist/channels/slack/index.d.ts +4 -0
  18. package/dist/channels/slack/index.d.ts.map +1 -1
  19. package/dist/channels/slack/index.js +4 -0
  20. package/dist/channels/slack/live-delivery.d.ts.map +1 -1
  21. package/dist/channels/slack/live-delivery.js +1 -0
  22. package/dist/channels/slack/manifest.d.ts.map +1 -1
  23. package/dist/channels/slack/manifest.js +3 -0
  24. package/dist/channels/slack/nudge-carrier.d.ts +19 -0
  25. package/dist/channels/slack/nudge-carrier.d.ts.map +1 -0
  26. package/dist/channels/slack/nudge-carrier.js +166 -0
  27. package/dist/channels/slack/nudge-store.d.ts +61 -0
  28. package/dist/channels/slack/nudge-store.d.ts.map +1 -0
  29. package/dist/channels/slack/nudge-store.js +266 -0
  30. package/dist/channels/slack/slack-channel.d.ts.map +1 -1
  31. package/dist/channels/slack/slack-channel.js +74 -25
  32. package/dist/channels/slack/stop.d.ts +14 -0
  33. package/dist/channels/slack/stop.d.ts.map +1 -0
  34. package/dist/channels/slack/stop.js +22 -0
  35. package/dist/channels/slack/thread-context.d.ts +81 -0
  36. package/dist/channels/slack/thread-context.d.ts.map +1 -0
  37. package/dist/channels/slack/thread-context.js +470 -0
  38. package/dist/channels/slack/types.d.ts +27 -0
  39. package/dist/channels/slack/types.d.ts.map +1 -1
  40. package/dist/docs/404.html +2 -2
  41. package/dist/docs/ab.html +4 -4
  42. package/dist/docs/assets/{app.DMboGKUD.js → app.BP8mAVMW.js} +1 -1
  43. package/dist/docs/assets/chunks/@localSearchIndexroot.DevOabKJ.js +1 -0
  44. package/dist/docs/assets/chunks/{VPLocalSearchBox.DkEqEnJU.js → VPLocalSearchBox.1FXIZbz8.js} +1 -1
  45. package/dist/docs/assets/chunks/{theme.ASugdMNm.js → theme.Dm1R02v9.js} +2 -2
  46. package/dist/docs/assets/example-agents_index.md.CwtWu5ix.js +2 -0
  47. package/dist/docs/assets/{example-agents_index.md.CRqJlnIf.lean.js → example-agents_index.md.CwtWu5ix.lean.js} +1 -1
  48. package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.js +10 -0
  49. package/dist/docs/assets/example-agents_oncall.md.BG_sUMly.lean.js +1 -0
  50. package/dist/docs/assets/{guides_slack.md.CCwqHvSV.js → guides_slack.md.FWale6Ip.js} +4 -3
  51. package/dist/docs/assets/{guides_slack.md.CCwqHvSV.lean.js → guides_slack.md.FWale6Ip.lean.js} +1 -1
  52. package/dist/docs/assets/{reference_sessions.md.DdjWabnN.js → reference_sessions.md.DLd6mvbv.js} +2 -2
  53. package/dist/docs/assets/{reference_sessions.md.DdjWabnN.lean.js → reference_sessions.md.DLd6mvbv.lean.js} +1 -1
  54. package/dist/docs/assets/{storage.md.CVnInNiN.js → storage.md.Cu3MwM2a.js} +3 -2
  55. package/dist/docs/assets/{storage.md.CVnInNiN.lean.js → storage.md.Cu3MwM2a.lean.js} +1 -1
  56. package/dist/docs/building-with-agents.html +4 -4
  57. package/dist/docs/concepts.html +4 -4
  58. package/dist/docs/deployment.html +4 -4
  59. package/dist/docs/evals.html +4 -4
  60. package/dist/docs/example-agents/approval-buddy.html +4 -4
  61. package/dist/docs/example-agents/benny.html +5 -5
  62. package/dist/docs/example-agents/bugbot.html +5 -5
  63. package/dist/docs/example-agents/codebase-wiki.html +4 -4
  64. package/dist/docs/example-agents/codeowners-review.html +4 -4
  65. package/dist/docs/example-agents/concierge.html +4 -4
  66. package/dist/docs/example-agents/fsd.html +4 -4
  67. package/dist/docs/example-agents/index.html +6 -6
  68. package/dist/docs/example-agents/knowledge-base.html +4 -4
  69. package/dist/docs/example-agents/oncall.html +34 -0
  70. package/dist/docs/example-agents/security-reviewer.html +4 -4
  71. package/dist/docs/example-agents/slack-agent.html +4 -4
  72. package/dist/docs/example-agents/weather-agent.html +4 -4
  73. package/dist/docs/guides/agent-to-agent.html +4 -4
  74. package/dist/docs/guides/cloud-runtime.html +4 -4
  75. package/dist/docs/guides/github.html +4 -4
  76. package/dist/docs/guides/human-in-the-loop.html +4 -4
  77. package/dist/docs/guides/slack.html +7 -6
  78. package/dist/docs/guides/webhooks.html +4 -4
  79. package/dist/docs/hashmap.json +1 -1
  80. package/dist/docs/hillclimbing.html +4 -4
  81. package/dist/docs/index.html +4 -4
  82. package/dist/docs/quickstart.html +4 -4
  83. package/dist/docs/reference/agent-config.html +4 -4
  84. package/dist/docs/reference/channels.html +4 -4
  85. package/dist/docs/reference/cli.html +4 -4
  86. package/dist/docs/reference/connections.html +4 -4
  87. package/dist/docs/reference/hooks.html +4 -4
  88. package/dist/docs/reference/http-api.html +4 -4
  89. package/dist/docs/reference/instructions.html +4 -4
  90. package/dist/docs/reference/playground.html +4 -4
  91. package/dist/docs/reference/project-layout.html +4 -4
  92. package/dist/docs/reference/schedules.html +4 -4
  93. package/dist/docs/reference/sessions.html +5 -5
  94. package/dist/docs/reference/skills.html +4 -4
  95. package/dist/docs/reference/subagents.html +4 -4
  96. package/dist/docs/reference/tools.html +4 -4
  97. package/dist/docs/scaffolding-agents.html +4 -4
  98. package/dist/docs/storage.html +7 -6
  99. package/dist/docs/troubleshooting.html +4 -4
  100. package/dist/evals.d.ts +2 -2
  101. package/dist/internal/cli-ax.d.ts +23 -10
  102. package/dist/internal/cli-ax.d.ts.map +1 -1
  103. package/dist/internal/cli-ax.js +358 -55
  104. package/dist/internal/cli-mcp.d.ts +64 -0
  105. package/dist/internal/cli-mcp.d.ts.map +1 -0
  106. package/dist/internal/cli-mcp.js +570 -0
  107. package/dist/internal/distribution.d.ts.map +1 -1
  108. package/dist/internal/distribution.js +2 -0
  109. package/dist/internal/eval-run-store.d.ts +2 -0
  110. package/dist/internal/eval-run-store.d.ts.map +1 -1
  111. package/dist/internal/eval-run-store.js +4 -4
  112. package/dist/internal/evals-client.d.ts +96 -0
  113. package/dist/internal/evals-client.d.ts.map +1 -0
  114. package/dist/internal/evals-client.js +262 -0
  115. package/dist/internal/host-kv.d.ts +17 -0
  116. package/dist/internal/host-kv.d.ts.map +1 -0
  117. package/dist/internal/host-kv.js +77 -0
  118. package/dist/internal/init-project.js +3 -3
  119. package/dist/internal/server.d.ts.map +1 -1
  120. package/dist/internal/server.js +19 -2
  121. package/dist/internal/session-engine.d.ts +42 -7
  122. package/dist/internal/session-engine.d.ts.map +1 -1
  123. package/dist/internal/session-engine.js +247 -51
  124. package/dist/internal/storage-coordinator.d.ts +18 -1
  125. package/dist/internal/storage-coordinator.d.ts.map +1 -1
  126. package/dist/internal/storage-coordinator.js +43 -0
  127. package/dist/playground/assets/index-Bpr4Th4M.js +319 -0
  128. package/dist/playground/index.html +1 -1
  129. package/dist/storage-backends/file-kv.d.ts +42 -0
  130. package/dist/storage-backends/file-kv.d.ts.map +1 -0
  131. package/dist/storage-backends/file-kv.js +190 -0
  132. package/dist/storage-backends/postgres-kv.d.ts +77 -0
  133. package/dist/storage-backends/postgres-kv.d.ts.map +1 -0
  134. package/dist/storage-backends/postgres-kv.js +180 -0
  135. package/dist/storage.d.ts +10 -1
  136. package/dist/storage.d.ts.map +1 -1
  137. package/dist/storage.js +10 -1
  138. package/dist/types.d.ts +79 -0
  139. package/dist/types.d.ts.map +1 -1
  140. package/docs/example-agents/index.md +14 -9
  141. package/docs/example-agents/oncall.md +216 -0
  142. package/docs/guides/slack.md +11 -2
  143. package/docs/reference/sessions.md +13 -4
  144. package/docs/storage.md +27 -0
  145. package/package.json +15 -1
  146. package/skills/framework-map/SKILL.md +6 -4
  147. package/src/bin/agent-serve.ts +79 -18
  148. package/src/channels/slack/channel-watch.ts +28 -1
  149. package/src/channels/slack/cursor-account.ts +20 -0
  150. package/src/channels/slack/defaults.ts +51 -20
  151. package/src/channels/slack/dispatch.ts +215 -40
  152. package/src/channels/slack/inbound.ts +30 -10
  153. package/src/channels/slack/index.ts +4 -0
  154. package/src/channels/slack/live-delivery.ts +1 -0
  155. package/src/channels/slack/manifest.ts +3 -0
  156. package/src/channels/slack/nudge-carrier.ts +182 -0
  157. package/src/channels/slack/nudge-store.ts +308 -0
  158. package/src/channels/slack/slack-channel.ts +106 -31
  159. package/src/channels/slack/stop.ts +25 -0
  160. package/src/channels/slack/thread-context.ts +572 -0
  161. package/src/channels/slack/types.ts +27 -0
  162. package/src/evals.ts +2 -2
  163. package/src/internal/cli-ax.ts +466 -75
  164. package/src/internal/cli-mcp.ts +708 -0
  165. package/src/internal/distribution.ts +2 -0
  166. package/src/internal/eval-run-store.ts +5 -0
  167. package/src/internal/evals-client.ts +431 -0
  168. package/src/internal/host-kv.ts +85 -0
  169. package/src/internal/init-project.ts +3 -3
  170. package/src/internal/server.ts +43 -0
  171. package/src/internal/session-engine.ts +351 -50
  172. package/src/internal/storage-coordinator.ts +41 -0
  173. package/src/storage-backends/file-kv.ts +221 -0
  174. package/src/storage-backends/postgres-kv.ts +248 -0
  175. package/src/storage.ts +11 -1
  176. package/src/types.ts +83 -0
  177. package/dist/docs/assets/chunks/@localSearchIndexroot.O6eP7QZr.js +0 -1
  178. package/dist/docs/assets/example-agents_index.md.CRqJlnIf.js +0 -2
  179. package/dist/playground/assets/index-mux9aj5P.js +0 -319
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@cursor/july",
3
- "version": "0.1.6",
3
+ "version": "0.1.8",
4
4
  "description": "(early alpha) Filesystem-first framework for defining Cursor agents as markdown and TypeScript and serving them over channels with the Cursor SDK.",
5
5
  "license": "SEE LICENSE IN LICENSE.md",
6
6
  "repository": {
@@ -124,6 +124,20 @@
124
124
  "import": "./dist/storage.js",
125
125
  "default": "./dist/storage.js"
126
126
  },
127
+ "./storage/file-kv": {
128
+ "anysphere-source": "./src/storage-backends/file-kv.ts",
129
+ "bun": "./src/storage-backends/file-kv.ts",
130
+ "types": "./dist/storage-backends/file-kv.d.ts",
131
+ "import": "./dist/storage-backends/file-kv.js",
132
+ "default": "./dist/storage-backends/file-kv.js"
133
+ },
134
+ "./storage/postgres-kv": {
135
+ "anysphere-source": "./src/storage-backends/postgres-kv.ts",
136
+ "bun": "./src/storage-backends/postgres-kv.ts",
137
+ "types": "./dist/storage-backends/postgres-kv.d.ts",
138
+ "import": "./dist/storage-backends/postgres-kv.js",
139
+ "default": "./dist/storage-backends/postgres-kv.js"
140
+ },
127
141
  "./package.json": {
128
142
  "anysphere-source": "./package.json",
129
143
  "bun": "./package.json",
@@ -97,10 +97,12 @@ Two handles, two owners:
97
97
  - **sessionId** — runtime-owned handle for streaming/inspection
98
98
  (`GET /v1/session/:id/stream?startIndex=N`, durable NDJSON replay).
99
99
 
100
- A follow-up to a busy session **interrupts the in-flight turn** and waits
101
- for it to settle before the new turn sends. `POST /v1/session/:id/stop`
102
- interrupts without sending. Session-bound deterministic tool calls are
103
- rejected with `409 session_busy` while a turn runs.
100
+ A follow-up to a busy HTTP/MCP session **interrupts the in-flight turn**
101
+ and waits for it to settle before the new turn sends. Slack uses
102
+ **coalesce** instead (enqueue + boundary drain; see
103
+ `docs/reference/sessions.md`). `POST /v1/session/:id/stop` interrupts
104
+ without sending. Session-bound deterministic tool calls are rejected with
105
+ `409 session_busy` while a turn runs.
104
106
 
105
107
  Key built-in routes (each agent, under `/<slug>` in multi-agent mode):
106
108
  `POST /v1/session`, `POST /v1/session/:id`, `GET /v1/session/:id/stream`,
@@ -29,6 +29,7 @@ import {
29
29
  cmdCall,
30
30
  cmdChat,
31
31
  cmdEval,
32
+ cmdEvalStatus,
32
33
  cmdInit,
33
34
  cmdLogs,
34
35
  cmdPlayground,
@@ -88,10 +89,17 @@ Usage:
88
89
  ${CLI} call <tool> [--input <json>] [--dir <path> | --url <baseUrl> | --prod]
89
90
  [--session <id>] [--slug <slug>] [--team <id>]
90
91
  ${CLI} init [dir] [--json]
92
+ ${CLI} mcp --prod [--slug <slug>] [--team <id>] [--dir <path>]
93
+ | --url <baseUrl> [--bearer-token <t>]
94
+ ${CLI} mcp install [--name <serverName>] [--print] [--json]
95
+ (same target flags as mcp)
91
96
  ${CLI} eval [--dir <path>] [evalId...] [--list] [--json]
92
97
  [--tag <tag>]... [--verbose] [--timeout-ms <n>]
93
- [--no-stream] [--local | --url <baseUrl>] [--slug <slug>]
98
+ [--no-stream] [--no-wait]
99
+ [--prod | --url <baseUrl>] [--slug <slug>]
94
100
  [--team <id>]
101
+ ${CLI} eval status [runId] --prod|--url <baseUrl> [--dir <path>] [--json]
102
+ [--slug <slug>] [--team <id>]
95
103
  ${CLI} info [--dir <path>] [--json]
96
104
  ${CLI} validate [--dir <path>]
97
105
  ${CLI} login [--api-key <key>] [--key-name <name>]
@@ -154,12 +162,29 @@ Commands:
154
162
  run login when unsigned, then print next steps. --json is
155
163
  machine-readable and never blocks on login. No directory:
156
164
  print the setup guide.
165
+ mcp Serve this agent's MCP endpoint over stdio for MCP clients
166
+ that spawn local servers (e.g. Cursor). Proxies
167
+ newline-delimited JSON-RPC to <target>/v1/mcp. --prod resolves
168
+ the hosted deployment with the signed-in Cursor account and
169
+ re-mints short-lived engine credentials as they expire;
170
+ --url targets a running server. stdout is reserved for the
171
+ MCP wire. \`mcp install\` writes the matching entry into
172
+ ~/.cursor/mcp.json (--print to preview; --name to override
173
+ the server name).
157
174
  eval Run filesystem evals under evals/**/*.eval.ts (streams on a TTY).
158
- Runs against the team's hosted deployment by default (slug from
159
- --slug or the directory name); pass --local for an ephemeral
160
- local server, or --url for a running one. Pass evalId args to
161
- select: one datapoint (weather/nyc), a whole file (weather
162
- all cases in that file), several of either, or omit to run all.
175
+ Default: ephemeral local harness against evals on disk. Pass
176
+ --prod to kick off the batch on the team's hosted deployment
177
+ (slug from --slug or the directory name) via /v1/dev/evals/runs
178
+ so results appear in the playground and persist when the agent
179
+ declares an evals storage table; or --url for a running server
180
+ (same server-side path). With --prod/--url, pass --no-wait to
181
+ return the runId immediately without polling; check later with
182
+ \`${CLI} eval status <runId> --prod\` (omit runId to list recent
183
+ runs; exit 3 while still running). The status subcommand only
184
+ binds when --prod/--url is set, so an eval id named status still
185
+ runs locally. Pass evalId args to select: one datapoint
186
+ (weather/nyc), a whole file (weather → all cases in that file),
187
+ several of either, or omit to run all.
163
188
  info Print the discovered agent surface (tools, skills, channels, ...).
164
189
  validate Check the project(s) and exit non-zero on error diagnostics.
165
190
  login Sign the host in to Cursor: browser sign-in mints a named,
@@ -194,7 +219,8 @@ Agent loop:
194
219
  ${CLI} validate --dir .
195
220
  ${CLI} info --dir . --json
196
221
  ${CLI} run --dir . --message "..."
197
- ${CLI} eval --local --dir .
222
+ ${CLI} eval --dir .
223
+ ${CLI} eval --prod --dir .
198
224
  ${CLI} dev .
199
225
 
200
226
  Options:
@@ -233,15 +259,14 @@ Options:
233
259
  --prod chat/resume/run/call/eval/logs/sessions/session/playground:
234
260
  use the team's hosted deployment. Slug from --slug or the
235
261
  --dir basename. Mutually exclusive with --url. Requires
236
- \`${CLI} login\`. (eval already defaults to the
237
- deployment; there --prod is just being explicit.)
238
- --local eval: run against an ephemeral local server instead of
239
- the hosted deployment. Mutually exclusive with --url.
262
+ \`${CLI} login\`. For eval, this is how you opt into the
263
+ persisted server-side batch (default is local).
240
264
  --once logs: print current progress / log buffer once and exit
241
265
  (default is to follow).
242
266
  --print playground: print the URL only; do not open a browser.
243
- --no-wait deploy/stop: return right after the request is accepted
244
- instead of polling until a terminal status.
267
+ --no-wait deploy/stop/eval: return right after the request is
268
+ accepted instead of polling until a terminal status.
269
+ For eval, prints the runId (deployment / --url only).
245
270
  --no-playground Disable the bundled web playground at /playground (also skips
246
271
  auto-build / HMR).
247
272
  --no-docs Disable the documentation site at /docs (also skips its
@@ -352,7 +377,6 @@ async function main(argv: string[]): Promise<number> {
352
377
  "no-docs": { type: "boolean", default: false },
353
378
  url: { type: "string" },
354
379
  prod: { type: "boolean", default: false },
355
- local: { type: "boolean", default: false },
356
380
  message: { type: "string", multiple: true },
357
381
  input: { type: "string" },
358
382
  "messages-file": { type: "string" },
@@ -407,8 +431,9 @@ async function main(argv: string[]): Promise<number> {
407
431
  const dir = values.dir ?? ".";
408
432
 
409
433
  // Best-effort, cached, and silent on failure. `update` does its own fresh
410
- // lookup, and --json runs stay quiet for machine consumers.
411
- if (command !== "update" && values.json !== true) {
434
+ // lookup, --json runs stay quiet for machine consumers, and `mcp` skips it
435
+ // so MCP client spawns stay fast (stdout there is the protocol wire).
436
+ if (command !== "update" && command !== "mcp" && values.json !== true) {
412
437
  await warnIfOutdated();
413
438
  }
414
439
 
@@ -475,6 +500,22 @@ async function main(argv: string[]): Promise<number> {
475
500
  });
476
501
  }
477
502
 
503
+ case "mcp": {
504
+ const { cmdMcp } = await import("../internal/cli-mcp.js");
505
+ return cmdMcp(positionals[0], {
506
+ dir,
507
+ slug: values.slug,
508
+ team: values.team,
509
+ apiKey: values["api-key"],
510
+ url: values.url,
511
+ prod: values.prod,
512
+ bearerToken: values["bearer-token"],
513
+ name: values.name,
514
+ print: values.print,
515
+ json: values.json,
516
+ });
517
+ }
518
+
478
519
  case "login": {
479
520
  const { cmdLogin } = await import("../internal/cli-cursor.js");
480
521
  return cmdLogin({
@@ -624,6 +665,26 @@ async function main(argv: string[]): Promise<number> {
624
665
  }
625
666
 
626
667
  case "eval": {
668
+ // Remote status subcommand only when targeting --prod/--url. Otherwise
669
+ // the bare token "status" is a normal eval id (evals/status.eval.ts).
670
+ if (
671
+ positionals[0] === "status" &&
672
+ (values.prod === true || values.url !== undefined)
673
+ ) {
674
+ return cmdEvalStatus(
675
+ positionals[1],
676
+ axFromOptions({
677
+ dir,
678
+ json: values.json,
679
+ url: values.url,
680
+ prod: values.prod,
681
+ team: values.team,
682
+ apiKey: values["api-key"],
683
+ slug: values.slug,
684
+ bearerToken: values["bearer-token"],
685
+ })
686
+ );
687
+ }
627
688
  return cmdEval(
628
689
  axFromOptions({
629
690
  dir,
@@ -634,7 +695,7 @@ async function main(argv: string[]): Promise<number> {
634
695
  tags: values.tag,
635
696
  url: values.url,
636
697
  prod: values.prod,
637
- local: values.local,
698
+ noWait: values["no-wait"],
638
699
  team: values.team,
639
700
  apiKey: values["api-key"],
640
701
  slug: values.slug,
@@ -1052,7 +1113,7 @@ function axFromOptions(
1052
1113
  toolInput: partial.toolInput,
1053
1114
  url: partial.url,
1054
1115
  prod: partial.prod,
1055
- local: partial.local,
1116
+ noWait: partial.noWait,
1056
1117
  team: partial.team,
1057
1118
  apiKey: partial.apiKey,
1058
1119
  fetchImpl: partial.fetchImpl,
@@ -6,7 +6,10 @@
6
6
  * - Top-level posts only by default; thread replies never dispatch unless
7
7
  * `posts: "all"` is configured.
8
8
  * - Posts that @mention the bot are skipped here (the `app_mention` path
9
- * owns them), and bot-authored posts are dropped upstream in parsing.
9
+ * owns them).
10
+ * - Bot-authored posts are dropped unless `includeBotPosts` is configured
11
+ * (alert feeds / webhook integrations); the watching bot's own posts are
12
+ * always dropped either way (loop safety).
10
13
  * - An optional debounce window lets rapid edits settle: edits update the
11
14
  * pending text, deleting the post cancels the dispatch.
12
15
  */
@@ -114,11 +117,14 @@ export function createChannelPostWatcher(args: {
114
117
  allowlist: SlackChannelAllowlist;
115
118
  /** Bot user id (from `auth.test`) so bot mentions route via app_mention. */
116
119
  getBotUserId: () => string | undefined;
120
+ /** This app's own `bot_id` (from `auth.test`) for self-post loop safety. */
121
+ getBotId?: () => string | undefined;
117
122
  dispatch: (message: SlackMessage, eventId?: string) => void;
118
123
  log?: SlackLog;
119
124
  }): ChannelPostWatcher {
120
125
  const log = args.log ?? defaultSlackLog;
121
126
  const includeReplies = args.options.posts === "all";
127
+ const includeBotPosts = args.options.includeBotPosts === true;
122
128
  const debounceMs = args.options.debounceMs ?? 0;
123
129
  const pending = new Map<
124
130
  string,
@@ -185,6 +191,27 @@ export function createChannelPostWatcher(args: {
185
191
  return skip("thread reply (posts: top-level)", message);
186
192
  }
187
193
  const botUserId = args.getBotUserId();
194
+ const author = message.author;
195
+ if (author?.isBot === true) {
196
+ if (!includeBotPosts) {
197
+ return skip("bot-authored post (includeBotPosts off)", message);
198
+ }
199
+ const selfBotId = args.getBotId?.();
200
+ if (selfBotId === undefined && botUserId === undefined) {
201
+ return skip(
202
+ "bot-authored post (bot identity unknown; loop safety)",
203
+ message
204
+ );
205
+ }
206
+ if (
207
+ (selfBotId !== undefined && author.botId === selfBotId) ||
208
+ (botUserId !== undefined &&
209
+ author.userId !== "" &&
210
+ author.userId === botUserId)
211
+ ) {
212
+ return skip("own post (loop safety)", message);
213
+ }
214
+ }
188
215
  if (botUserId !== undefined && message.text.includes(`<@${botUserId}>`)) {
189
216
  return skip("bot mention (app_mention path owns it)", message);
190
217
  }
@@ -114,6 +114,12 @@ export async function dispatchCursorSlackEnvelope(args: {
114
114
  envelope: CursorSlackEnvelope;
115
115
  send: SendMessageFn;
116
116
  host: HostContext;
117
+ hasContinuationSession?: (continuationToken: string) => Promise<boolean>;
118
+ isContinuationBusy?: (continuationToken: string) => Promise<boolean>;
119
+ getContinuationLastBotMessageTs?: (
120
+ continuationToken: string
121
+ ) => Promise<string | undefined>;
122
+ interruptContinuation?: (continuationToken: string) => Promise<boolean>;
117
123
  }): Promise<void> {
118
124
  const { runtime, envelope } = args;
119
125
  const log = runtime.getLog();
@@ -161,6 +167,20 @@ export async function dispatchCursorSlackEnvelope(args: {
161
167
  credentials: runtime.credentials,
162
168
  host: args.host,
163
169
  log,
170
+ ...(args.hasContinuationSession === undefined
171
+ ? {}
172
+ : { hasContinuationSession: args.hasContinuationSession }),
173
+ ...(args.isContinuationBusy === undefined
174
+ ? {}
175
+ : { isContinuationBusy: args.isContinuationBusy }),
176
+ ...(args.getContinuationLastBotMessageTs === undefined
177
+ ? {}
178
+ : {
179
+ getContinuationLastBotMessageTs: args.getContinuationLastBotMessageTs,
180
+ }),
181
+ ...(args.interruptContinuation === undefined
182
+ ? {}
183
+ : { interruptContinuation: args.interruptContinuation }),
164
184
  ...(envelope.eventId === undefined ? {} : { eventId: envelope.eventId }),
165
185
  });
166
186
  }
@@ -26,28 +26,51 @@ import type {
26
26
  SlackSuggestedPrompt,
27
27
  } from "./types.js";
28
28
 
29
- const defaultOnInbound: SlackMessageHandler = async (ctx, message) => {
30
- await ctx.thread.startTyping("Thinking…");
31
- const auth = defaultSlackAuth(message, ctx);
32
- return auth === null ? null : { auth };
33
- };
29
+ function resolveLoadingMessages(
30
+ loadingMessages: readonly string[] | undefined
31
+ ): string[] {
32
+ if (loadingMessages !== undefined && loadingMessages.length > 0) {
33
+ return [...loadingMessages];
34
+ }
35
+ return [...SLACK_DEFAULT_LOADING_MESSAGES];
36
+ }
34
37
 
35
- export const defaultOnAppMention: SlackMessageHandler = defaultOnInbound;
36
- export const defaultOnDirectMessage: SlackMessageHandler = defaultOnInbound;
38
+ /** Default mention/DM inbound handler (optional custom loading rotation). */
39
+ export function createDefaultOnInbound(
40
+ loadingMessages?: readonly string[]
41
+ ): SlackMessageHandler {
42
+ const rotating = resolveLoadingMessages(loadingMessages);
43
+ return async (ctx, message) => {
44
+ await ctx.thread.startTyping("Thinking…", rotating);
45
+ const auth = defaultSlackAuth(message, ctx);
46
+ return auth === null ? null : { auth };
47
+ };
48
+ }
37
49
 
38
50
  /**
39
- * Default watched-channel-post handler: same thread-scoped principal as
40
- * mentions (so a later @mention continues the session), tagged with an
41
- * `engagement: "channel_post"` attribute for auth policies.
51
+ * Default watched-channel-post handler (optional custom loading rotation).
52
+ * Same thread-scoped principal as mentions, tagged with
53
+ * `engagement: "channel_post"` for auth policies.
42
54
  */
43
- export const defaultOnChannelPost: SlackMessageHandler = async (
44
- ctx,
45
- message
46
- ) => {
47
- await ctx.thread.startTyping("Thinking…");
48
- const auth = defaultSlackAuth(message, ctx, { engagement: "channel_post" });
49
- return auth === null ? null : { auth };
50
- };
55
+ export function createDefaultOnChannelPost(
56
+ loadingMessages?: readonly string[]
57
+ ): SlackMessageHandler {
58
+ const rotating = resolveLoadingMessages(loadingMessages);
59
+ return async (ctx, message) => {
60
+ await ctx.thread.startTyping("Thinking…", rotating);
61
+ const auth = defaultSlackAuth(message, ctx, {
62
+ engagement: "channel_post",
63
+ });
64
+ return auth === null ? null : { auth };
65
+ };
66
+ }
67
+
68
+ export const defaultOnAppMention: SlackMessageHandler =
69
+ createDefaultOnInbound();
70
+ export const defaultOnDirectMessage: SlackMessageHandler =
71
+ createDefaultOnInbound();
72
+ export const defaultOnChannelPost: SlackMessageHandler =
73
+ createDefaultOnChannelPost();
51
74
 
52
75
  export interface BuildDefaultEventsOptions {
53
76
  credentials?: SlackCredentials;
@@ -55,6 +78,8 @@ export interface BuildDefaultEventsOptions {
55
78
  thinkingSteps?: boolean;
56
79
  suggestedPrompts?: SlackSuggestedPrompt[];
57
80
  suggestedPromptsTitle?: string;
81
+ /** Rotating assistant status lines; defaults to pack constants. */
82
+ loadingMessages?: readonly string[];
58
83
  /** Resolved per-call so channel onStart can rebind the server logger. */
59
84
  getLog?: () => SlackLog;
60
85
  }
@@ -74,6 +99,7 @@ export function buildDefaultEvents(
74
99
  const thinkingSteps = options.thinkingSteps !== false;
75
100
  const suggestedPrompts = options.suggestedPrompts ?? [];
76
101
  const suggestedPromptsTitle = options.suggestedPromptsTitle;
102
+ const loadingMessages = resolveLoadingMessages(options.loadingMessages);
77
103
  const botToken = credentials?.botToken;
78
104
  const getLog = options.getLog ?? (() => defaultSlackLog);
79
105
  const strategy = createStreamDeliveryStrategy({ botToken, getLog });
@@ -87,6 +113,7 @@ export function buildDefaultEvents(
87
113
  channel.state.streamTs = null;
88
114
  channel.state.streamChannelId = null;
89
115
  channel.state.lastPostedText = null;
116
+ // Keep lastBotMessageTs across turns — warm delta watermark.
90
117
  channel.state.approvalMessageByCallId = {};
91
118
 
92
119
  const state = channel.state;
@@ -106,7 +133,7 @@ export function buildDefaultEvents(
106
133
  channelId: state.channelId,
107
134
  threadTs: state.threadTs,
108
135
  status: "Working…",
109
- loadingMessages: [...SLACK_DEFAULT_LOADING_MESSAGES],
136
+ loadingMessages,
110
137
  });
111
138
 
112
139
  // Retry streaming each turn unless a prior call proved the feature
@@ -178,7 +205,10 @@ export function buildDefaultEvents(
178
205
  channelId: channel.state.channelId,
179
206
  threadTs: channel.state.threadTs,
180
207
  status,
181
- loadingMessages: [status, ...SLACK_DEFAULT_LOADING_MESSAGES],
208
+ loadingMessages: [
209
+ status,
210
+ ...loadingMessages.filter((message) => message !== status),
211
+ ],
182
212
  });
183
213
  channel.state.lastReasoningTypingAtMs = now;
184
214
  channel.state.lastReasoningTypingStatus = status;
@@ -337,6 +367,7 @@ export function buildDefaultEvents(
337
367
  threadTs: state.threadTs,
338
368
  });
339
369
  channel.state.lastPostedText = event.data.text;
370
+ channel.state.lastBotMessageTs = posted.ts;
340
371
  log.info("reply delivered via postMessage", {
341
372
  channel: state.channelId,
342
373
  thread_ts: state.threadTs,