@fastagent-sh/fastagent 0.14.0 → 0.16.0

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 (277) hide show
  1. package/README.md +16 -6
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/agentcore-limits.d.ts +7 -0
  5. package/dist/channels/agentcore-limits.js +9 -0
  6. package/dist/channels/agentcore-state.d.ts +83 -0
  7. package/dist/channels/agentcore-state.js +258 -0
  8. package/dist/channels/agentcore.d.ts +98 -0
  9. package/dist/channels/agentcore.js +260 -0
  10. package/dist/channels/busy.d.ts +23 -0
  11. package/dist/channels/busy.js +53 -0
  12. package/dist/channels/context-buffer.d.ts +35 -0
  13. package/dist/channels/context-buffer.js +103 -0
  14. package/dist/channels/control.d.ts +28 -0
  15. package/dist/channels/control.js +214 -0
  16. package/dist/channels/feishu/card.js +1 -1
  17. package/dist/channels/feishu/context-buffer.d.ts +15 -13
  18. package/dist/channels/feishu/context-buffer.js +62 -69
  19. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  20. package/dist/channels/feishu/feishu-api.js +24 -3
  21. package/dist/channels/feishu/feishu.d.ts +27 -27
  22. package/dist/channels/feishu/feishu.js +318 -207
  23. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  24. package/dist/channels/feishu/invoke-turn.js +44 -79
  25. package/dist/channels/feishu/model.d.ts +2 -2
  26. package/dist/channels/feishu/normalize.d.ts +2 -1
  27. package/dist/channels/feishu/normalize.js +0 -1
  28. package/dist/channels/feishu/parse.d.ts +22 -8
  29. package/dist/channels/feishu/parse.js +24 -7
  30. package/dist/channels/feishu/preview.d.ts +4 -7
  31. package/dist/channels/feishu/preview.js +27 -142
  32. package/dist/channels/feishu/register-app.d.ts +2 -1
  33. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  34. package/dist/channels/feishu/scaffold/channel.ts +9 -8
  35. package/dist/channels/feishu/scaffold/feishu-send.ts +6 -4
  36. package/dist/channels/feishu/setup-mode.d.ts +37 -0
  37. package/dist/channels/feishu/setup-mode.js +28 -0
  38. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  39. package/dist/channels/feishu/ws-ingress.js +136 -0
  40. package/dist/channels/github/github.js +16 -7
  41. package/dist/channels/http.d.ts +14 -0
  42. package/dist/channels/http.js +36 -3
  43. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  44. package/dist/channels/invoke-turn-kit.js +87 -0
  45. package/dist/channels/lark/lark.d.ts +4 -2
  46. package/dist/channels/lark/lark.js +4 -1
  47. package/dist/channels/lark/onboard.d.ts +8 -4
  48. package/dist/channels/lark/onboard.js +8 -0
  49. package/dist/channels/lark/scaffold/channel.ts +9 -8
  50. package/dist/channels/lark/scaffold/lark-send.ts +6 -4
  51. package/dist/channels/preview-kit.d.ts +115 -0
  52. package/dist/channels/preview-kit.js +184 -0
  53. package/dist/channels/seen.d.ts +5 -0
  54. package/dist/channels/seen.js +35 -0
  55. package/dist/channels/slack/bot-auth.d.ts +15 -0
  56. package/dist/channels/slack/bot-auth.js +146 -0
  57. package/dist/channels/slack/config-api.d.ts +60 -0
  58. package/dist/channels/slack/config-api.js +149 -0
  59. package/dist/channels/slack/context-buffer.d.ts +24 -0
  60. package/dist/channels/slack/context-buffer.js +37 -0
  61. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  62. package/dist/channels/slack/invoke-turn.js +63 -0
  63. package/dist/channels/slack/manifest.d.ts +49 -0
  64. package/dist/channels/slack/manifest.js +69 -0
  65. package/dist/channels/slack/model.d.ts +67 -0
  66. package/dist/channels/slack/model.js +2 -0
  67. package/dist/channels/slack/onboard.d.ts +41 -0
  68. package/dist/channels/slack/onboard.js +120 -0
  69. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  70. package/dist/channels/slack/onboarding-state.js +69 -0
  71. package/dist/channels/slack/parse.d.ts +38 -0
  72. package/dist/channels/slack/parse.js +124 -0
  73. package/dist/channels/slack/preview.d.ts +23 -0
  74. package/dist/channels/slack/preview.js +403 -0
  75. package/dist/channels/slack/reaction.d.ts +24 -0
  76. package/dist/channels/slack/reaction.js +62 -0
  77. package/dist/channels/slack/register-webhook.d.ts +10 -0
  78. package/dist/channels/slack/register-webhook.js +49 -0
  79. package/dist/channels/slack/scaffold/channel.ts +33 -0
  80. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  81. package/dist/channels/slack/setup-server.d.ts +17 -0
  82. package/dist/channels/slack/setup-server.js +103 -0
  83. package/dist/channels/slack/slack-api.d.ts +57 -0
  84. package/dist/channels/slack/slack-api.js +399 -0
  85. package/dist/channels/slack/slack.d.ts +51 -0
  86. package/dist/channels/slack/slack.js +496 -0
  87. package/dist/channels/slack/welcomed.d.ts +5 -0
  88. package/dist/channels/slack/welcomed.js +32 -0
  89. package/dist/channels/state.d.ts +11 -4
  90. package/dist/channels/state.js +22 -15
  91. package/dist/channels/stop-command.d.ts +6 -0
  92. package/dist/channels/stop-command.js +36 -0
  93. package/dist/channels/tasks.d.ts +7 -0
  94. package/dist/channels/tasks.js +25 -0
  95. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  96. package/dist/channels/telegram/context-buffer.js +6 -85
  97. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  98. package/dist/channels/telegram/invoke-turn.js +11 -58
  99. package/dist/channels/telegram/parse.d.ts +0 -7
  100. package/dist/channels/telegram/parse.js +4 -2
  101. package/dist/channels/telegram/preview.d.ts +4 -7
  102. package/dist/channels/telegram/preview.js +24 -142
  103. package/dist/channels/telegram/scaffold/telegram-send.ts +6 -3
  104. package/dist/channels/telegram/telegram.js +24 -10
  105. package/dist/channels/{feishu/text.d.ts → text.d.ts} +15 -1
  106. package/dist/channels/{feishu/text.js → text.js} +15 -1
  107. package/dist/channels/thread-participants.d.ts +21 -0
  108. package/dist/channels/thread-participants.js +132 -0
  109. package/dist/channels/turn-queue.js +8 -1
  110. package/dist/channels/turn-store.d.ts +1 -1
  111. package/dist/channels/turn-store.js +2 -3
  112. package/dist/cli/add-feishu.d.ts +30 -0
  113. package/dist/cli/add-feishu.js +366 -0
  114. package/dist/cli/add-slack.d.ts +11 -0
  115. package/dist/cli/add-slack.js +199 -0
  116. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  117. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  118. package/dist/cli/commands/add.d.ts +4 -0
  119. package/dist/cli/commands/add.js +191 -60
  120. package/dist/cli/commands/attach.d.ts +82 -0
  121. package/dist/cli/commands/attach.js +563 -0
  122. package/dist/cli/commands/chat.d.ts +1 -0
  123. package/dist/cli/commands/chat.js +12 -7
  124. package/dist/cli/commands/deploy.d.ts +3 -1
  125. package/dist/cli/commands/deploy.js +384 -82
  126. package/dist/cli/commands/dev.js +34 -26
  127. package/dist/cli/commands/fire.js +17 -18
  128. package/dist/cli/commands/info.js +36 -29
  129. package/dist/cli/commands/init.d.ts +1 -1
  130. package/dist/cli/commands/init.js +65 -53
  131. package/dist/cli/commands/invoke.js +11 -9
  132. package/dist/cli/commands/login.js +48 -45
  133. package/dist/cli/commands/models.js +1 -1
  134. package/dist/cli/commands/schedule.js +6 -8
  135. package/dist/cli/commands/start.js +102 -39
  136. package/dist/cli/commands/tool.js +23 -18
  137. package/dist/cli/fail.d.ts +17 -2
  138. package/dist/cli/fail.js +25 -1
  139. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  140. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  141. package/dist/cli/kernel.d.ts +3 -2
  142. package/dist/cli/models-view.d.ts +21 -0
  143. package/dist/cli/models-view.js +66 -0
  144. package/dist/cli/program.js +135 -56
  145. package/dist/cli/serve.d.ts +60 -20
  146. package/dist/cli/serve.js +252 -47
  147. package/dist/cli/shared.d.ts +40 -7
  148. package/dist/cli/shared.js +180 -38
  149. package/dist/collect.d.ts +14 -3
  150. package/dist/collect.js +24 -0
  151. package/dist/core.d.ts +3 -1
  152. package/dist/core.js +2 -0
  153. package/dist/deploy/agentcore/plan.d.ts +117 -0
  154. package/dist/deploy/agentcore/plan.js +721 -0
  155. package/dist/deploy/agentcore/run.d.ts +73 -0
  156. package/dist/deploy/agentcore/run.js +412 -0
  157. package/dist/deploy/agentcore/zip.d.ts +17 -0
  158. package/dist/deploy/agentcore/zip.js +68 -0
  159. package/dist/deploy/container.d.ts +26 -25
  160. package/dist/deploy/container.js +93 -89
  161. package/dist/deploy/docker/plan.d.ts +3 -3
  162. package/dist/deploy/docker/plan.js +20 -23
  163. package/dist/deploy/fly/plan.d.ts +5 -1
  164. package/dist/deploy/fly/plan.js +39 -26
  165. package/dist/deploy/fly/run.d.ts +17 -5
  166. package/dist/deploy/fly/run.js +52 -7
  167. package/dist/deploy/preflight.d.ts +20 -8
  168. package/dist/deploy/preflight.js +267 -71
  169. package/dist/deploy/railway/plan.d.ts +10 -1
  170. package/dist/deploy/railway/plan.js +53 -20
  171. package/dist/deploy/railway/run.d.ts +13 -5
  172. package/dist/deploy/railway/run.js +22 -7
  173. package/dist/deploy/runner.d.ts +7 -3
  174. package/dist/deploy/runner.js +9 -3
  175. package/dist/deploy/secrets.d.ts +2 -1
  176. package/dist/deploy/secrets.js +23 -3
  177. package/dist/dev-supervisor.d.ts +11 -10
  178. package/dist/dev-supervisor.js +56 -50
  179. package/dist/engines/pi/auth.d.ts +8 -7
  180. package/dist/engines/pi/auth.js +12 -10
  181. package/dist/engines/pi/channel.d.ts +22 -16
  182. package/dist/engines/pi/channel.js +92 -62
  183. package/dist/engines/pi/chat.d.ts +4 -16
  184. package/dist/engines/pi/chat.js +8 -261
  185. package/dist/engines/pi/config.d.ts +20 -55
  186. package/dist/engines/pi/config.js +49 -121
  187. package/dist/engines/pi/create.d.ts +30 -10
  188. package/dist/engines/pi/create.js +33 -26
  189. package/dist/engines/pi/definition.d.ts +7 -26
  190. package/dist/engines/pi/definition.js +8 -54
  191. package/dist/engines/pi/harness.d.ts +49 -0
  192. package/dist/engines/pi/harness.js +90 -2
  193. package/dist/engines/pi/invoke.d.ts +55 -2
  194. package/dist/engines/pi/invoke.js +304 -19
  195. package/dist/engines/pi/login.d.ts +12 -1
  196. package/dist/engines/pi/login.js +16 -4
  197. package/dist/engines/pi/models.d.ts +59 -13
  198. package/dist/engines/pi/models.js +62 -24
  199. package/dist/engines/pi/open.d.ts +114 -0
  200. package/dist/engines/pi/open.js +127 -0
  201. package/dist/engines/pi/session-builder.d.ts +16 -0
  202. package/dist/engines/pi/session-builder.js +308 -0
  203. package/dist/engines/pi/session-control.d.ts +50 -0
  204. package/dist/engines/pi/session-control.js +604 -0
  205. package/dist/engines/pi/sessions.d.ts +17 -2
  206. package/dist/engines/pi/sessions.js +9 -0
  207. package/dist/engines/pi/tool-context.d.ts +18 -11
  208. package/dist/engines/pi/tool-context.js +3 -9
  209. package/dist/engines/pi/tool.d.ts +6 -8
  210. package/dist/engines/pi/tool.js +10 -1
  211. package/dist/engines/pi/wake-tool.d.ts +0 -3
  212. package/dist/engines/pi/wake-tool.js +9 -7
  213. package/dist/env.d.ts +16 -4
  214. package/dist/env.js +43 -5
  215. package/dist/feishu.d.ts +1 -1
  216. package/dist/feishu.js +1 -1
  217. package/dist/host/node.d.ts +25 -8
  218. package/dist/host/node.js +5 -4
  219. package/dist/index.d.ts +1 -0
  220. package/dist/index.js +1 -0
  221. package/dist/lark.d.ts +1 -1
  222. package/dist/lark.js +1 -1
  223. package/dist/loader.d.ts +2 -2
  224. package/dist/loader.js +3 -3
  225. package/dist/log.d.ts +1 -1
  226. package/dist/log.js +1 -1
  227. package/dist/observe.js +3 -0
  228. package/dist/paths.d.ts +138 -0
  229. package/dist/paths.js +326 -0
  230. package/dist/pi.d.ts +6 -5
  231. package/dist/pi.js +4 -3
  232. package/dist/runtime.d.ts +7 -5
  233. package/dist/runtime.js +2 -2
  234. package/dist/scaffold/add-channel.d.ts +26 -9
  235. package/dist/scaffold/add-channel.js +175 -40
  236. package/dist/scaffold/init.d.ts +32 -41
  237. package/dist/scaffold/init.js +161 -185
  238. package/dist/scaffold/templates/env.example +15 -6
  239. package/dist/scaffold/templates/fastagent.config.mjs +5 -4
  240. package/dist/scaffold/templates/gitignore +14 -6
  241. package/dist/scaffold/templates/persona.md +4 -2
  242. package/dist/scaffold/templates/secrets.gitignore +5 -0
  243. package/dist/scaffold/templates.d.ts +1 -7
  244. package/dist/scaffold/templates.js +3 -25
  245. package/dist/scaffold/vendor-skill.d.ts +2 -2
  246. package/dist/scaffold/vendor-skill.js +13 -13
  247. package/dist/schedule/discover.js +4 -4
  248. package/dist/schedule/scheduler.d.ts +40 -1
  249. package/dist/schedule/scheduler.js +89 -56
  250. package/dist/schedule/state.js +1 -1
  251. package/dist/schedule/wake-alarm.d.ts +47 -0
  252. package/dist/schedule/wake-alarm.js +136 -0
  253. package/dist/schedule/wakeups.d.ts +1 -3
  254. package/dist/schedule/wakeups.js +19 -1
  255. package/dist/session-remote.d.ts +53 -0
  256. package/dist/session-remote.js +336 -0
  257. package/dist/session.d.ts +265 -0
  258. package/dist/session.js +37 -0
  259. package/dist/slack.d.ts +2 -0
  260. package/dist/slack.js +2 -0
  261. package/dist/tunnel.d.ts +7 -4
  262. package/dist/tunnel.js +23 -12
  263. package/package.json +22 -7
  264. package/dist/channels/feishu/owned-threads.d.ts +0 -11
  265. package/dist/channels/feishu/owned-threads.js +0 -47
  266. package/dist/channels/feishu/seen.d.ts +0 -5
  267. package/dist/channels/feishu/seen.js +0 -47
  268. package/dist/cli-add-feishu.d.ts +0 -8
  269. package/dist/cli-add-feishu.js +0 -223
  270. package/dist/cli-models.d.ts +0 -11
  271. package/dist/cli-models.js +0 -20
  272. package/dist/engines/pi/workspace.d.ts +0 -56
  273. package/dist/engines/pi/workspace.js +0 -72
  274. package/dist/scaffold/templates/gitignore.agentdir-root +0 -5
  275. package/dist/scaffold/templates/gitignore.kit +0 -2
  276. package/dist/workspace.d.ts +0 -9
  277. package/dist/workspace.js +0 -45
@@ -0,0 +1,399 @@
1
+ /** Slack Web API transport: one JSON pipeline plus authenticated, capped private-file downloads. */
2
+ import { mkdir, writeFile } from "node:fs/promises";
3
+ import { join } from "node:path";
4
+ import { codePointPrefix } from "../text.js";
5
+ const API_TIMEOUT_MS = 30_000;
6
+ const DOWNLOAD_TIMEOUT_MS = 120_000;
7
+ const MAX_DOWNLOAD_BYTES = 20 * 1024 * 1024;
8
+ const RETRIES = 3;
9
+ const MAX_RETRY_AFTER_S = 30;
10
+ const MAX_REDIRECTS = 3;
11
+ /** Slack's standard-Markdown fields cap each call at 12,000 characters. Keep headroom for
12
+ * code-fence balancing and future server-side transformations. */
13
+ const SLACK_MAX_MARKDOWN = 10_000;
14
+ export class SlackApiError extends Error {
15
+ method;
16
+ status;
17
+ slackError;
18
+ constructor(method, status, description, slackError, options) {
19
+ super(status === 0 ? `slack ${method}: ${description}` : `slack ${method} failed: ${status} ${description}`, options);
20
+ this.name = "SlackApiError";
21
+ this.method = method;
22
+ this.status = status;
23
+ this.slackError = slackError;
24
+ }
25
+ }
26
+ const NATIVE_UNAVAILABLE_ERRORS = new Set([
27
+ "channel_type_not_supported",
28
+ "messages_tab_disabled",
29
+ "method_deprecated",
30
+ "missing_scope",
31
+ "no_permission",
32
+ "not_allowed_token_type",
33
+ ]);
34
+ /** A definitive capability rejection is safe to route through the compatibility renderer. Network,
35
+ * internal, and timeout failures are ambiguous: Slack may already have created the stream. */
36
+ export function isSlackNativeUnavailable(error) {
37
+ return error instanceof SlackApiError && !!error.slackError && NATIVE_UNAVAILABLE_ERRORS.has(error.slackError);
38
+ }
39
+ const wait = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
40
+ /** Split a Slack text/Markdown field at a code-point-safe boundary, preferring a newline. */
41
+ export function chunkSlackText(text, maxPoints = SLACK_MAX_MARKDOWN) {
42
+ if (!Number.isSafeInteger(maxPoints) || maxPoints <= 0)
43
+ throw new RangeError("maxPoints must be a positive integer");
44
+ const points = Array.from(text);
45
+ if (points.length <= maxPoints)
46
+ return [text];
47
+ const chunks = [];
48
+ let offset = 0;
49
+ while (offset < points.length) {
50
+ let end = Math.min(points.length, offset + maxPoints);
51
+ if (end < points.length) {
52
+ const window = points.slice(offset, end).join("");
53
+ const paragraph = window.lastIndexOf("\n\n");
54
+ const newline = window.lastIndexOf("\n");
55
+ const boundary = paragraph >= 0 ? paragraph + 2 : newline >= 0 ? newline + 1 : -1;
56
+ if (boundary > 0)
57
+ end = offset + Array.from(window.slice(0, boundary)).length;
58
+ }
59
+ chunks.push(points.slice(offset, end).join(""));
60
+ offset = end;
61
+ }
62
+ return chunks.length ? chunks : [""];
63
+ }
64
+ /** Split standard Markdown while balancing fenced code blocks across separately posted messages. */
65
+ export function chunkSlackMarkdown(markdown, maxPoints = SLACK_MAX_MARKDOWN) {
66
+ if (!Number.isSafeInteger(maxPoints) || maxPoints < 256) {
67
+ throw new RangeError("Markdown chunk size must be an integer of at least 256 code points");
68
+ }
69
+ if (Array.from(markdown).length <= maxPoints)
70
+ return [markdown];
71
+ const rawChunks = chunkSlackText(markdown, maxPoints - 128);
72
+ let fence;
73
+ const output = [];
74
+ for (const raw of rawChunks) {
75
+ const entering = fence;
76
+ for (const line of raw.split("\n")) {
77
+ const match = line.trim().match(/^(`{3,}|~{3,})(.*)$/);
78
+ if (!match)
79
+ continue;
80
+ const marker = match[1] ?? "```";
81
+ // Pathological fences longer than our reserved balancing headroom are left untouched rather than
82
+ // making a generated chunk exceed Slack's API limit.
83
+ if (marker.length > 64)
84
+ continue;
85
+ if (!fence) {
86
+ fence = { marker, opening: `${marker}${(match[2] ?? "").slice(0, 64)}` };
87
+ }
88
+ else if (marker[0] === fence.marker[0] && marker.length >= fence.marker.length) {
89
+ fence = undefined;
90
+ }
91
+ }
92
+ const prefix = entering ? `${entering.opening}\n` : "";
93
+ const suffix = fence ? `\n${fence.marker}` : "";
94
+ output.push(`${prefix}${raw}${suffix}`);
95
+ }
96
+ return output;
97
+ }
98
+ function safeFileName(file) {
99
+ const raw = file.name ?? file.title ?? file.id ?? "file";
100
+ const safe = raw.replace(/[/\\]/g, "_").replace(/^\.+/, "_") || "file";
101
+ return `${file.id ?? "slack"}-${safe}`;
102
+ }
103
+ function fileDownloadUrl(file) {
104
+ if (file.file_access === "check_file_info")
105
+ throw new Error(`Slack file ${file.id ?? "(unknown)"} is not ready`);
106
+ if (file.file_access === "access_denied")
107
+ throw new Error(`Slack file ${file.id ?? "(unknown)"} is not accessible`);
108
+ const url = file.url_private_download ?? file.url_private;
109
+ if (!url) {
110
+ const kind = file.is_external ? `external (${file.external_type ?? "unknown"})` : (file.mode ?? "unknown");
111
+ throw new Error(`Slack file ${file.id ?? "(unknown)"} has no downloadable bytes (${kind})`);
112
+ }
113
+ if ((file.size ?? 0) > MAX_DOWNLOAD_BYTES)
114
+ throw new Error("Slack file is too large (max 20 MB)");
115
+ return url;
116
+ }
117
+ async function readBytesCapped(response) {
118
+ const declared = Number(response.headers.get("content-length") ?? "0");
119
+ if (Number.isFinite(declared) && declared > MAX_DOWNLOAD_BYTES)
120
+ throw new Error("Slack file is too large (max 20 MB)");
121
+ if (!response.body)
122
+ return Buffer.alloc(0);
123
+ const reader = response.body.getReader();
124
+ const chunks = [];
125
+ let total = 0;
126
+ for (;;) {
127
+ const { done, value } = await reader.read();
128
+ if (done)
129
+ break;
130
+ if (!value)
131
+ continue;
132
+ total += value.byteLength;
133
+ if (total > MAX_DOWNLOAD_BYTES) {
134
+ await reader.cancel("download exceeds cap").catch(() => { });
135
+ throw new Error("Slack file is too large (max 20 MB)");
136
+ }
137
+ chunks.push(value);
138
+ }
139
+ return Buffer.concat(chunks.map((chunk) => Buffer.from(chunk)), total);
140
+ }
141
+ export function createSlackApi({ botToken, baseUrl = "https://slack.com/api" }) {
142
+ const apiBase = baseUrl.replace(/\/$/, "");
143
+ const configuredOrigin = new URL(apiBase).origin;
144
+ const currentToken = typeof botToken === "string" ? async () => botToken : botToken;
145
+ const trustedDownloadUrl = (value) => {
146
+ const url = new URL(value);
147
+ const host = url.hostname.toLowerCase();
148
+ const slackHost = host === "slack-files.com" ||
149
+ host.endsWith(".slack-files.com") ||
150
+ host === "slack.com" ||
151
+ host.endsWith(".slack.com") ||
152
+ host === "slack-edge.com" ||
153
+ host.endsWith(".slack-edge.com");
154
+ if (!slackHost && url.origin !== configuredOrigin)
155
+ throw new Error(`refusing non-Slack file URL host ${host}`);
156
+ if (url.protocol !== "https:" && url.origin !== configuredOrigin)
157
+ throw new Error("refusing non-HTTPS Slack file URL");
158
+ return url;
159
+ };
160
+ const call = async (method, body, httpMethod = "POST") => {
161
+ const url = new URL(`${apiBase}/${method}`);
162
+ if (httpMethod === "GET") {
163
+ for (const [name, value] of Object.entries(body)) {
164
+ if (value !== undefined)
165
+ url.searchParams.set(name, String(value));
166
+ }
167
+ }
168
+ for (let attempt = 0;; attempt++) {
169
+ let response;
170
+ let raw;
171
+ try {
172
+ const token = await currentToken();
173
+ response = await fetch(url, {
174
+ method: httpMethod,
175
+ headers: {
176
+ authorization: `Bearer ${token}`,
177
+ ...(httpMethod === "POST" ? { "content-type": "application/json; charset=utf-8" } : {}),
178
+ },
179
+ ...(httpMethod === "POST" ? { body: JSON.stringify(body) } : {}),
180
+ signal: AbortSignal.timeout(API_TIMEOUT_MS),
181
+ });
182
+ raw = await response.text();
183
+ }
184
+ catch (error) {
185
+ throw new SlackApiError(method, 0, String(error), undefined, { cause: error });
186
+ }
187
+ let data;
188
+ try {
189
+ data = JSON.parse(raw);
190
+ }
191
+ catch {
192
+ data = {};
193
+ }
194
+ if (response.ok && data.ok === true)
195
+ return data;
196
+ const rateLimited = response.status === 429 || data.error === "ratelimited";
197
+ if (rateLimited && attempt < RETRIES) {
198
+ const retryAfter = Number(response.headers.get("retry-after") ?? attempt + 1);
199
+ if (Number.isFinite(retryAfter) && retryAfter <= MAX_RETRY_AFTER_S) {
200
+ await wait(Math.max(1, retryAfter) * 1000);
201
+ continue;
202
+ }
203
+ }
204
+ const detail = [
205
+ data.error ?? "response was not the expected Slack JSON",
206
+ data.needed ? `needed ${data.needed}` : undefined,
207
+ ...(data.response_metadata?.messages ?? []),
208
+ ]
209
+ .filter(Boolean)
210
+ .join("; ");
211
+ throw new SlackApiError(method, response.status, detail, data.error);
212
+ }
213
+ };
214
+ const download = async (file) => {
215
+ let url = trustedDownloadUrl(fileDownloadUrl(file));
216
+ for (let redirect = 0;; redirect++) {
217
+ let response;
218
+ try {
219
+ const token = await currentToken();
220
+ response = await fetch(url, {
221
+ headers: { authorization: `Bearer ${token}` },
222
+ redirect: "manual",
223
+ signal: AbortSignal.timeout(DOWNLOAD_TIMEOUT_MS),
224
+ });
225
+ }
226
+ catch (error) {
227
+ throw new SlackApiError("file download", 0, String(error), undefined, { cause: error });
228
+ }
229
+ if (response.status >= 300 && response.status < 400) {
230
+ const location = response.headers.get("location");
231
+ if (!location || redirect >= MAX_REDIRECTS) {
232
+ throw new SlackApiError("file download", response.status, "invalid or excessive redirect");
233
+ }
234
+ await response.body?.cancel().catch(() => { });
235
+ url = trustedDownloadUrl(new URL(location, url).toString());
236
+ continue;
237
+ }
238
+ if (!response.ok) {
239
+ const detail = codePointPrefix(await response.text().catch(() => ""), 300) || "file bytes were rejected";
240
+ throw new SlackApiError("file download", response.status, detail);
241
+ }
242
+ return { bytes: await readBytesCapped(response), contentType: response.headers.get("content-type") ?? undefined };
243
+ }
244
+ };
245
+ const api = {
246
+ async authTest() {
247
+ const data = await call("auth.test", {});
248
+ return { teamId: data.team_id, userId: data.user_id, botId: data.bot_id };
249
+ },
250
+ async postMessage(target, text) {
251
+ const data = await call("chat.postMessage", {
252
+ channel: target.channelId,
253
+ text,
254
+ ...(target.threadTs ? { thread_ts: target.threadTs } : {}),
255
+ unfurl_links: false,
256
+ unfurl_media: false,
257
+ });
258
+ if (!data.ts)
259
+ throw new SlackApiError("chat.postMessage", 200, "response carried no ts");
260
+ return data.ts;
261
+ },
262
+ async postMarkdown(target, markdown) {
263
+ const data = await call("chat.postMessage", {
264
+ channel: target.channelId,
265
+ markdown_text: markdown,
266
+ ...(target.threadTs ? { thread_ts: target.threadTs } : {}),
267
+ unfurl_links: false,
268
+ unfurl_media: false,
269
+ });
270
+ if (!data.ts)
271
+ throw new SlackApiError("chat.postMessage", 200, "response carried no ts");
272
+ return data.ts;
273
+ },
274
+ async updateMessage(channelId, ts, text) {
275
+ try {
276
+ await call("chat.update", { channel: channelId, ts, text });
277
+ }
278
+ catch (error) {
279
+ if (error instanceof SlackApiError && error.slackError === "message_not_modified")
280
+ return;
281
+ throw error;
282
+ }
283
+ },
284
+ async updateMarkdown(channelId, ts, markdown) {
285
+ try {
286
+ await call("chat.update", { channel: channelId, ts, markdown_text: markdown });
287
+ }
288
+ catch (error) {
289
+ if (error instanceof SlackApiError && error.slackError === "message_not_modified")
290
+ return;
291
+ throw error;
292
+ }
293
+ },
294
+ async deleteMessage(channelId, ts) {
295
+ await call("chat.delete", { channel: channelId, ts });
296
+ },
297
+ async sendMarkdown(target, markdown) {
298
+ let first;
299
+ for (const chunk of chunkSlackMarkdown(markdown)) {
300
+ const ts = await api.postMarkdown(target, chunk);
301
+ first ??= ts;
302
+ }
303
+ return first;
304
+ },
305
+ async startStream(target, markdown) {
306
+ if (!target.threadTs)
307
+ throw new Error("Slack native streams require a parent thread timestamp");
308
+ const channelRecipient = target.channelId.startsWith("D")
309
+ ? {}
310
+ : {
311
+ recipient_user_id: target.recipientUserId,
312
+ recipient_team_id: target.recipientTeamId,
313
+ };
314
+ if (!target.channelId.startsWith("D") && (!target.recipientUserId || !target.recipientTeamId)) {
315
+ throw new Error("Slack native channel streams require recipient user and team IDs");
316
+ }
317
+ const data = await call("chat.startStream", {
318
+ channel: target.channelId,
319
+ thread_ts: target.threadTs,
320
+ ...channelRecipient,
321
+ ...(markdown ? { markdown_text: markdown } : {}),
322
+ });
323
+ if (!data.ts)
324
+ throw new SlackApiError("chat.startStream", 200, "response carried no ts");
325
+ return data.ts;
326
+ },
327
+ async appendStream(channelId, ts, markdown) {
328
+ await call("chat.appendStream", { channel: channelId, ts, markdown_text: markdown });
329
+ },
330
+ async stopStream(channelId, ts, markdown) {
331
+ await call("chat.stopStream", {
332
+ channel: channelId,
333
+ ts,
334
+ ...(markdown ? { markdown_text: markdown } : {}),
335
+ });
336
+ },
337
+ async setThreadStatus(target, status) {
338
+ if (!target.threadTs)
339
+ return;
340
+ await call("assistant.threads.setStatus", {
341
+ channel_id: target.channelId,
342
+ thread_ts: target.threadTs,
343
+ status,
344
+ });
345
+ },
346
+ async setThreadTitle(target, title) {
347
+ if (!target.threadTs)
348
+ return;
349
+ await call("assistant.threads.setTitle", {
350
+ channel_id: target.channelId,
351
+ thread_ts: target.threadTs,
352
+ title,
353
+ });
354
+ },
355
+ async addReaction(channelId, timestamp, emoji) {
356
+ try {
357
+ await call("reactions.add", { channel: channelId, timestamp, name: emoji });
358
+ }
359
+ catch (error) {
360
+ if (error instanceof SlackApiError && error.slackError === "already_reacted")
361
+ return;
362
+ throw error;
363
+ }
364
+ },
365
+ async removeReaction(channelId, timestamp, emoji) {
366
+ try {
367
+ await call("reactions.remove", { channel: channelId, timestamp, name: emoji });
368
+ }
369
+ catch (error) {
370
+ if (error instanceof SlackApiError && error.slackError === "no_reaction")
371
+ return;
372
+ throw error;
373
+ }
374
+ },
375
+ async fileInfo(fileId) {
376
+ const data = await call("files.info", { file: fileId }, "GET");
377
+ if (!data.file?.id)
378
+ throw new SlackApiError("files.info", 200, `response carried no file for ${fileId}`);
379
+ return data.file;
380
+ },
381
+ async fetchImage(file) {
382
+ const { bytes, contentType } = await download(file);
383
+ const mime = (file.mimetype ?? contentType?.split(";")[0]?.trim() ?? "").toLowerCase();
384
+ if (!mime.startsWith("image/"))
385
+ throw new Error(`Slack file ${file.id ?? "(unknown)"} is not an image (${mime || "unknown type"})`);
386
+ return { mimeType: mime, data: bytes.toString("base64") };
387
+ },
388
+ async fetchFile(file, channelId, filesDir) {
389
+ const { bytes } = await download(file);
390
+ const dir = join(filesDir, channelId);
391
+ await mkdir(dir, { recursive: true });
392
+ const name = safeFileName(file);
393
+ const path = join(dir, name);
394
+ await writeFile(path, bytes);
395
+ return { path, name, size: bytes.byteLength };
396
+ },
397
+ };
398
+ return api;
399
+ }
@@ -0,0 +1,51 @@
1
+ import type { ChannelModule } from "../../host/node.ts";
2
+ import { type SlackEventEnvelope, type SlackFile, type SlackMessageEvent, type SlackRoute, defaultSlackRoute, slackEnvelope } from "./parse.ts";
3
+ import { type SlackFailure, type SlackRendering } from "./preview.ts";
4
+ export { defaultSlackRoute, slackEnvelope };
5
+ export type { SlackEventEnvelope, SlackFailure, SlackFile, SlackMessageEvent, SlackRendering, SlackRoute };
6
+ export interface SlackChannelOptions {
7
+ /** Bot User OAuth Token (`xoxb-…`) used for replies and files. */
8
+ botToken: string;
9
+ /** App signing secret used to verify the raw Events API request body. */
10
+ signingSecret: string;
11
+ /** Rotating OAuth credentials. Omit all four only for a manually managed long-lived bot token. */
12
+ botRefreshToken?: string;
13
+ clientId?: string;
14
+ clientSecret?: string;
15
+ botTokenExpiresAt?: number;
16
+ /** `context` (default) subscribes to group message streams, which is what lets the channel HEAR a
17
+ * thread: bare replies are then admitted by the participation rule (design/participant-model.md §3),
18
+ * and other discussion is buffered. `mentions` answers only app_mention plus DMs for an explicit
19
+ * least-privilege setup. */
20
+ groupBehavior?: "context" | "mentions";
21
+ /** `native` (default) uses Slack Agent streams for threaded replies. Its inline tool traces carry a
22
+ * bounded summary of each call's first argument and cannot be retracted, so they stay in the
23
+ * delivered message beside the answer. `classic` retains the compatibility renderer based on one
24
+ * rate-limited edited message, which settles into the answer alone. A top-level target necessarily
25
+ * uses the classic renderer, because Slack streams require a parent user message; a custom route
26
+ * reaches one either by returning `threadTs: null` or by redirecting to another channel without
27
+ * naming a thread. */
28
+ rendering?: SlackRendering;
29
+ /** Optional footer for successful Agent replies. Omitted or `false` sends no repetitive disclaimer. */
30
+ aiDisclaimer?: string | false;
31
+ /** First-run direct-message welcome, sent once when a user first opens the DM (`app_home_opened`,
32
+ * `tab: "messages"`). A string customizes it; `false` disables it. Plain Markdown only — no interactive
33
+ * buttons until an interactivity endpoint exists. Defaults to a generic greeting. */
34
+ welcome?: string | false;
35
+ /** Lightweight emoji ack on the user's triggering message: 👀 while working, ✅ when done. `false`
36
+ * disables it; an object overrides either emoji name. Requires the `reactions:write` scope; a missing
37
+ * scope degrades to no ack. */
38
+ reactionAck?: false | {
39
+ processing?: string;
40
+ completed?: string;
41
+ };
42
+ /** Custom route policy. Providing it disables the default participant-model thread/context admission policy. */
43
+ route?: (envelope: SlackEventEnvelope) => SlackRoute | null;
44
+ /** Customer-facing failure formatter; full details always remain in operator logs. */
45
+ onError?: (failure: SlackFailure) => string | undefined;
46
+ /** Slack Web API base override for tests or an operator-controlled gateway. */
47
+ apiBaseUrl?: string;
48
+ }
49
+ /** Verify Slack's v0 HMAC over the exact raw body and reject timestamps outside the replay window. */
50
+ export declare function verifySlackSignature(signingSecret: string, timestamp: string, signature: string, rawBody: string, nowMs?: number): boolean;
51
+ export declare function slackChannel(options: SlackChannelOptions): ChannelModule;