@fastagent-sh/fastagent 0.14.0 → 0.15.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 (209) hide show
  1. package/README.md +3 -3
  2. package/dist/agent.d.ts +20 -0
  3. package/dist/agent.js +9 -0
  4. package/dist/channels/context-buffer.d.ts +30 -0
  5. package/dist/channels/context-buffer.js +98 -0
  6. package/dist/channels/control.d.ts +28 -0
  7. package/dist/channels/control.js +214 -0
  8. package/dist/channels/feishu/card.js +1 -1
  9. package/dist/channels/feishu/context-buffer.d.ts +8 -11
  10. package/dist/channels/feishu/context-buffer.js +6 -67
  11. package/dist/channels/feishu/feishu-api.d.ts +11 -3
  12. package/dist/channels/feishu/feishu-api.js +20 -2
  13. package/dist/channels/feishu/feishu.d.ts +28 -16
  14. package/dist/channels/feishu/feishu.js +210 -188
  15. package/dist/channels/feishu/invoke-turn.d.ts +7 -20
  16. package/dist/channels/feishu/invoke-turn.js +10 -62
  17. package/dist/channels/feishu/model.d.ts +2 -1
  18. package/dist/channels/feishu/normalize.d.ts +2 -1
  19. package/dist/channels/feishu/owned-threads.d.ts +2 -6
  20. package/dist/channels/feishu/parse.d.ts +1 -1
  21. package/dist/channels/feishu/preview.d.ts +4 -7
  22. package/dist/channels/feishu/preview.js +24 -140
  23. package/dist/channels/feishu/register-app.d.ts +2 -1
  24. package/dist/channels/feishu/register-webhook.d.ts +2 -1
  25. package/dist/channels/feishu/setup-mode.d.ts +8 -0
  26. package/dist/channels/feishu/setup-mode.js +2 -0
  27. package/dist/channels/feishu/ws-ingress.d.ts +28 -0
  28. package/dist/channels/feishu/ws-ingress.js +136 -0
  29. package/dist/channels/github/github.js +8 -6
  30. package/dist/channels/http.d.ts +14 -0
  31. package/dist/channels/http.js +35 -2
  32. package/dist/channels/invoke-turn-kit.d.ts +65 -0
  33. package/dist/channels/invoke-turn-kit.js +87 -0
  34. package/dist/channels/lark/lark.d.ts +4 -2
  35. package/dist/channels/lark/lark.js +4 -1
  36. package/dist/channels/lark/onboard.d.ts +8 -4
  37. package/dist/channels/lark/onboard.js +8 -0
  38. package/dist/channels/preview-kit.d.ts +109 -0
  39. package/dist/channels/preview-kit.js +183 -0
  40. package/dist/channels/seen.d.ts +5 -0
  41. package/dist/channels/seen.js +35 -0
  42. package/dist/channels/slack/bot-auth.d.ts +15 -0
  43. package/dist/channels/slack/bot-auth.js +146 -0
  44. package/dist/channels/slack/config-api.d.ts +60 -0
  45. package/dist/channels/slack/config-api.js +149 -0
  46. package/dist/channels/slack/context-buffer.d.ts +24 -0
  47. package/dist/channels/slack/context-buffer.js +37 -0
  48. package/dist/channels/slack/invoke-turn.d.ts +19 -0
  49. package/dist/channels/slack/invoke-turn.js +63 -0
  50. package/dist/channels/slack/manifest.d.ts +49 -0
  51. package/dist/channels/slack/manifest.js +69 -0
  52. package/dist/channels/slack/model.d.ts +67 -0
  53. package/dist/channels/slack/model.js +2 -0
  54. package/dist/channels/slack/onboard.d.ts +41 -0
  55. package/dist/channels/slack/onboard.js +120 -0
  56. package/dist/channels/slack/onboarding-state.d.ts +31 -0
  57. package/dist/channels/slack/onboarding-state.js +69 -0
  58. package/dist/channels/slack/owned-threads.d.ts +6 -0
  59. package/dist/channels/slack/owned-threads.js +43 -0
  60. package/dist/channels/slack/parse.d.ts +23 -0
  61. package/dist/channels/slack/parse.js +81 -0
  62. package/dist/channels/slack/preview.d.ts +24 -0
  63. package/dist/channels/slack/preview.js +359 -0
  64. package/dist/channels/slack/reaction.d.ts +24 -0
  65. package/dist/channels/slack/reaction.js +62 -0
  66. package/dist/channels/slack/register-webhook.d.ts +10 -0
  67. package/dist/channels/slack/register-webhook.js +49 -0
  68. package/dist/channels/slack/scaffold/channel.ts +33 -0
  69. package/dist/channels/slack/scaffold/slack-send.ts +171 -0
  70. package/dist/channels/slack/setup-server.d.ts +17 -0
  71. package/dist/channels/slack/setup-server.js +103 -0
  72. package/dist/channels/slack/slack-api.d.ts +77 -0
  73. package/dist/channels/slack/slack-api.js +415 -0
  74. package/dist/channels/slack/slack.d.ts +58 -0
  75. package/dist/channels/slack/slack.js +451 -0
  76. package/dist/channels/slack/welcomed.d.ts +5 -0
  77. package/dist/channels/slack/welcomed.js +32 -0
  78. package/dist/channels/state.js +3 -3
  79. package/dist/channels/stop-command.d.ts +6 -0
  80. package/dist/channels/stop-command.js +36 -0
  81. package/dist/channels/tasks.d.ts +13 -0
  82. package/dist/channels/tasks.js +10 -0
  83. package/dist/channels/telegram/context-buffer.d.ts +8 -17
  84. package/dist/channels/telegram/context-buffer.js +6 -85
  85. package/dist/channels/telegram/invoke-turn.d.ts +5 -22
  86. package/dist/channels/telegram/invoke-turn.js +11 -58
  87. package/dist/channels/telegram/preview.d.ts +4 -7
  88. package/dist/channels/telegram/preview.js +24 -142
  89. package/dist/channels/telegram/telegram.js +23 -9
  90. package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
  91. package/dist/channels/{feishu/text.js → text.js} +1 -1
  92. package/dist/channels/turn-queue.js +1 -1
  93. package/dist/channels/turn-store.d.ts +1 -1
  94. package/dist/channels/turn-store.js +2 -3
  95. package/dist/cli/add-feishu.d.ts +27 -0
  96. package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
  97. package/dist/cli/add-slack.d.ts +10 -0
  98. package/dist/cli/add-slack.js +204 -0
  99. package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
  100. package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
  101. package/dist/cli/commands/add.d.ts +4 -0
  102. package/dist/cli/commands/add.js +152 -20
  103. package/dist/cli/commands/attach.d.ts +82 -0
  104. package/dist/cli/commands/attach.js +559 -0
  105. package/dist/cli/commands/chat.d.ts +1 -0
  106. package/dist/cli/commands/chat.js +6 -1
  107. package/dist/cli/commands/deploy.d.ts +2 -0
  108. package/dist/cli/commands/deploy.js +78 -22
  109. package/dist/cli/commands/dev.js +11 -5
  110. package/dist/cli/commands/fire.js +3 -3
  111. package/dist/cli/commands/info.js +2 -2
  112. package/dist/cli/commands/invoke.js +2 -3
  113. package/dist/cli/commands/login.js +14 -25
  114. package/dist/cli/commands/models.js +1 -1
  115. package/dist/cli/commands/start.js +15 -8
  116. package/dist/cli/commands/tool.js +2 -1
  117. package/dist/cli/fail.d.ts +0 -2
  118. package/dist/cli/fail.js +1 -1
  119. package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
  120. package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
  121. package/dist/cli/kernel.d.ts +3 -2
  122. package/dist/cli/models-view.d.ts +21 -0
  123. package/dist/cli/models-view.js +66 -0
  124. package/dist/cli/program.js +79 -21
  125. package/dist/cli/serve.d.ts +38 -18
  126. package/dist/cli/serve.js +197 -39
  127. package/dist/cli/shared.d.ts +26 -6
  128. package/dist/cli/shared.js +163 -33
  129. package/dist/collect.d.ts +14 -3
  130. package/dist/collect.js +24 -0
  131. package/dist/core.d.ts +3 -1
  132. package/dist/core.js +2 -0
  133. package/dist/deploy/docker/plan.d.ts +2 -2
  134. package/dist/deploy/docker/plan.js +8 -6
  135. package/dist/deploy/fly/plan.d.ts +3 -1
  136. package/dist/deploy/fly/plan.js +12 -7
  137. package/dist/deploy/fly/run.d.ts +5 -4
  138. package/dist/deploy/fly/run.js +16 -5
  139. package/dist/deploy/preflight.d.ts +10 -4
  140. package/dist/deploy/preflight.js +53 -9
  141. package/dist/deploy/railway/plan.d.ts +3 -1
  142. package/dist/deploy/railway/plan.js +12 -4
  143. package/dist/deploy/railway/run.d.ts +5 -4
  144. package/dist/deploy/railway/run.js +15 -5
  145. package/dist/deploy/runner.d.ts +2 -1
  146. package/dist/deploy/secrets.d.ts +2 -1
  147. package/dist/deploy/secrets.js +23 -3
  148. package/dist/dev-supervisor.d.ts +0 -2
  149. package/dist/dev-supervisor.js +7 -3
  150. package/dist/engines/pi/channel.d.ts +22 -16
  151. package/dist/engines/pi/channel.js +90 -60
  152. package/dist/engines/pi/chat.d.ts +4 -16
  153. package/dist/engines/pi/chat.js +8 -261
  154. package/dist/engines/pi/config.d.ts +14 -9
  155. package/dist/engines/pi/config.js +33 -18
  156. package/dist/engines/pi/create.d.ts +22 -2
  157. package/dist/engines/pi/create.js +20 -14
  158. package/dist/engines/pi/harness.d.ts +49 -0
  159. package/dist/engines/pi/harness.js +90 -2
  160. package/dist/engines/pi/invoke.d.ts +55 -2
  161. package/dist/engines/pi/invoke.js +304 -19
  162. package/dist/engines/pi/login.d.ts +11 -0
  163. package/dist/engines/pi/login.js +16 -4
  164. package/dist/engines/pi/models.d.ts +56 -10
  165. package/dist/engines/pi/models.js +61 -23
  166. package/dist/engines/pi/session-builder.d.ts +16 -0
  167. package/dist/engines/pi/session-builder.js +308 -0
  168. package/dist/engines/pi/session-control.d.ts +50 -0
  169. package/dist/engines/pi/session-control.js +604 -0
  170. package/dist/engines/pi/sessions.d.ts +17 -2
  171. package/dist/engines/pi/sessions.js +9 -0
  172. package/dist/engines/pi/tool-context.d.ts +18 -11
  173. package/dist/engines/pi/tool-context.js +3 -9
  174. package/dist/engines/pi/tool.d.ts +6 -8
  175. package/dist/engines/pi/tool.js +6 -1
  176. package/dist/engines/pi/wake-tool.d.ts +0 -3
  177. package/dist/engines/pi/wake-tool.js +9 -7
  178. package/dist/engines/pi/workspace.d.ts +52 -0
  179. package/dist/engines/pi/workspace.js +72 -15
  180. package/dist/feishu.d.ts +1 -1
  181. package/dist/feishu.js +1 -1
  182. package/dist/host/node.d.ts +23 -6
  183. package/dist/host/node.js +5 -4
  184. package/dist/index.d.ts +1 -0
  185. package/dist/index.js +1 -0
  186. package/dist/lark.d.ts +1 -1
  187. package/dist/lark.js +1 -1
  188. package/dist/observe.js +3 -0
  189. package/dist/pi.d.ts +5 -4
  190. package/dist/pi.js +2 -1
  191. package/dist/scaffold/add-channel.d.ts +19 -6
  192. package/dist/scaffold/add-channel.js +126 -17
  193. package/dist/scaffold/templates/fastagent.config.mjs +4 -3
  194. package/dist/schedule/wakeups.d.ts +0 -3
  195. package/dist/schedule/wakeups.js +1 -1
  196. package/dist/session-remote.d.ts +53 -0
  197. package/dist/session-remote.js +336 -0
  198. package/dist/session.d.ts +265 -0
  199. package/dist/session.js +37 -0
  200. package/dist/slack.d.ts +2 -0
  201. package/dist/slack.js +2 -0
  202. package/dist/tunnel.d.ts +7 -4
  203. package/dist/tunnel.js +21 -10
  204. package/package.json +19 -7
  205. package/dist/channels/feishu/seen.d.ts +0 -5
  206. package/dist/channels/feishu/seen.js +0 -47
  207. package/dist/cli-add-feishu.d.ts +0 -8
  208. package/dist/cli-models.d.ts +0 -11
  209. package/dist/cli-models.js +0 -20
@@ -0,0 +1,146 @@
1
+ /** Rotating Slack bot-token provider backed by owner-only channel state. */
2
+ import { chmodSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from "node:fs";
3
+ import { dirname } from "node:path";
4
+ const REFRESH_EARLY_MS = 5 * 60_000;
5
+ const REQUEST_TIMEOUT_MS = 30_000;
6
+ function isState(value) {
7
+ const state = value;
8
+ return (typeof state === "object" &&
9
+ state !== null &&
10
+ state.version === 1 &&
11
+ typeof state.accessToken === "string" &&
12
+ typeof state.refreshToken === "string" &&
13
+ typeof state.expiresAt === "number");
14
+ }
15
+ function load(path) {
16
+ let raw;
17
+ try {
18
+ raw = readFileSync(path, "utf8");
19
+ }
20
+ catch (error) {
21
+ if (error.code === "ENOENT")
22
+ return undefined;
23
+ throw new Error(`Slack bot auth state ${path} is unreadable: ${String(error)}`, { cause: error });
24
+ }
25
+ try {
26
+ const value = JSON.parse(raw);
27
+ if (!isState(value))
28
+ throw new Error("unexpected shape/version");
29
+ return value;
30
+ }
31
+ catch (error) {
32
+ // A stale env refresh token may already have been consumed. Never hide corrupt rotating state by
33
+ // falling back to it: recovery requires restoring the durable file or reinstalling the app.
34
+ throw new Error(`Slack bot auth state ${path} is invalid: ${String(error)}`, { cause: error });
35
+ }
36
+ }
37
+ function save(path, state) {
38
+ mkdirSync(dirname(path), { recursive: true });
39
+ const temp = `${path}.${process.pid}.${Date.now()}.tmp`;
40
+ try {
41
+ writeFileSync(temp, JSON.stringify(state), { mode: 0o600 });
42
+ chmodSync(temp, 0o600);
43
+ renameSync(temp, path);
44
+ chmodSync(path, 0o600);
45
+ }
46
+ catch (error) {
47
+ rmSync(temp, { force: true });
48
+ throw error;
49
+ }
50
+ }
51
+ async function refreshSlackBotToken(input) {
52
+ const authorization = Buffer.from(`${input.clientId}:${input.clientSecret}`, "utf8").toString("base64");
53
+ let lastError;
54
+ // Slack keeps a short grace window for a just-consumed refresh token. One retry recovers the
55
+ // committed-but-response-lost boundary without creating an unbounded refresh loop.
56
+ for (let attempt = 0; attempt < 2; attempt++) {
57
+ try {
58
+ const response = await input.fetch(`${input.apiBaseUrl}/oauth.v2.access`, {
59
+ method: "POST",
60
+ headers: {
61
+ authorization: `Basic ${authorization}`,
62
+ "content-type": "application/x-www-form-urlencoded",
63
+ },
64
+ body: new URLSearchParams({
65
+ grant_type: "refresh_token",
66
+ refresh_token: input.refreshToken,
67
+ }).toString(),
68
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
69
+ });
70
+ const data = (await response.json().catch(() => ({})));
71
+ if (!response.ok ||
72
+ data.ok !== true ||
73
+ !data.access_token ||
74
+ !data.refresh_token ||
75
+ typeof data.expires_in !== "number") {
76
+ throw new Error(`Slack rejected bot-token refresh (HTTP ${response.status})`);
77
+ }
78
+ return {
79
+ version: 1,
80
+ accessToken: data.access_token,
81
+ refreshToken: data.refresh_token,
82
+ expiresAt: Date.now() + data.expires_in * 1_000,
83
+ };
84
+ }
85
+ catch (error) {
86
+ lastError = error;
87
+ }
88
+ }
89
+ throw new Error("Slack bot-token rotation failed; restore credentials or reinstall the app", { cause: lastError });
90
+ }
91
+ /** Latest local rotating credentials for `deploy --run`. The remote runtime still prefers its own
92
+ * durable volume; this overlay prevents a locally consumed refresh token from being redeployed. */
93
+ export function readSlackBotAuthEnv(statePath) {
94
+ const state = load(statePath);
95
+ return state
96
+ ? {
97
+ SLACK_BOT_TOKEN: state.accessToken,
98
+ SLACK_BOT_REFRESH_TOKEN: state.refreshToken,
99
+ SLACK_BOT_TOKEN_EXPIRES_AT: String(state.expiresAt),
100
+ }
101
+ : {};
102
+ }
103
+ /** Resolve the current bot token, refreshing once per process when it approaches expiry. */
104
+ export function createSlackBotTokenProvider(options) {
105
+ const rotatingFields = [options.botRefreshToken, options.clientId, options.clientSecret, options.botTokenExpiresAt];
106
+ const rotating = rotatingFields.some((value) => value !== undefined);
107
+ if (rotating && rotatingFields.some((value) => value === undefined || value === "")) {
108
+ throw new Error("Slack token rotation requires botRefreshToken, clientId, clientSecret, and botTokenExpiresAt together");
109
+ }
110
+ if (rotating && (!Number.isFinite(options.botTokenExpiresAt) || options.botTokenExpiresAt <= 0)) {
111
+ throw new Error("Slack botTokenExpiresAt must be a positive epoch-millisecond value");
112
+ }
113
+ if (!rotating)
114
+ return async () => options.botToken;
115
+ const configured = {
116
+ version: 1,
117
+ accessToken: options.botToken,
118
+ refreshToken: options.botRefreshToken,
119
+ expiresAt: options.botTokenExpiresAt,
120
+ };
121
+ const persisted = load(options.statePath);
122
+ // Deploy may carry a newer pair from the owner machine onto an existing remote volume; ordinary
123
+ // restarts carry the original stale env and therefore keep the newer persisted pair.
124
+ let state = persisted && persisted.expiresAt >= configured.expiresAt ? persisted : configured;
125
+ let refreshing;
126
+ return async () => {
127
+ if (state.expiresAt > Date.now() + REFRESH_EARLY_MS)
128
+ return state.accessToken;
129
+ refreshing ??= refreshSlackBotToken({
130
+ refreshToken: state.refreshToken,
131
+ clientId: options.clientId,
132
+ clientSecret: options.clientSecret,
133
+ apiBaseUrl: (options.apiBaseUrl ?? "https://slack.com/api").replace(/\/$/, ""),
134
+ fetch: options.fetch ?? globalThis.fetch,
135
+ })
136
+ .then((next) => {
137
+ save(options.statePath, next);
138
+ state = next;
139
+ })
140
+ .finally(() => {
141
+ refreshing = undefined;
142
+ });
143
+ await refreshing;
144
+ return state.accessToken;
145
+ };
146
+ }
@@ -0,0 +1,60 @@
1
+ import type { SlackAppManifest } from "./manifest.ts";
2
+ interface SlackErrorShape {
3
+ ok?: boolean;
4
+ error?: string;
5
+ errors?: {
6
+ message?: string;
7
+ pointer?: string;
8
+ }[];
9
+ }
10
+ export declare class SlackConfigApiError extends Error {
11
+ readonly code: string;
12
+ readonly status: number;
13
+ constructor(method: string, status: number, data: SlackErrorShape, fallback: string);
14
+ }
15
+ export interface SlackAppCredentials {
16
+ appId: string;
17
+ clientId: string;
18
+ clientSecret: string;
19
+ signingSecret: string;
20
+ oauthAuthorizeUrl?: string;
21
+ }
22
+ /** Create is intentionally single-attempt: an ambiguous response may already have created the app. */
23
+ export declare function createSlackApp(configToken: string, manifest: SlackAppManifest, options?: {
24
+ apiBaseUrl?: string;
25
+ fetch?: typeof fetch;
26
+ }): Promise<SlackAppCredentials>;
27
+ export declare function updateSlackAppManifest(configToken: string, appId: string, manifest: SlackAppManifest, options?: {
28
+ apiBaseUrl?: string;
29
+ fetch?: typeof fetch;
30
+ }): Promise<void>;
31
+ export interface RotatedSlackConfigToken {
32
+ token: string;
33
+ refreshToken: string;
34
+ expiresAt: number;
35
+ teamId?: string;
36
+ }
37
+ export declare function rotateSlackConfigToken(refreshToken: string, options?: {
38
+ apiBaseUrl?: string;
39
+ fetch?: typeof fetch;
40
+ }): Promise<RotatedSlackConfigToken>;
41
+ export interface SlackOAuthResult {
42
+ botToken: string;
43
+ botRefreshToken: string;
44
+ botTokenExpiresAt: number;
45
+ appId: string;
46
+ teamId: string;
47
+ teamName?: string;
48
+ botUserId?: string;
49
+ scopes: string[];
50
+ }
51
+ export declare function exchangeSlackOAuthCode(input: {
52
+ clientId: string;
53
+ clientSecret: string;
54
+ code: string;
55
+ redirectUrl: string;
56
+ }, options?: {
57
+ apiBaseUrl?: string;
58
+ fetch?: typeof fetch;
59
+ }): Promise<SlackOAuthResult>;
60
+ export {};
@@ -0,0 +1,149 @@
1
+ const SLACK_API = "https://slack.com/api";
2
+ const REQUEST_TIMEOUT_MS = 30_000;
3
+ export class SlackConfigApiError extends Error {
4
+ code;
5
+ status;
6
+ constructor(method, status, data, fallback) {
7
+ const code = data.error ?? fallback;
8
+ const fields = data.errors
9
+ ?.map((error) => `${error.pointer ?? "manifest"}: ${error.message ?? "invalid"}`)
10
+ .join("; ");
11
+ super(`Slack ${method} failed: ${code}${fields ? ` — ${fields}` : ""}`);
12
+ this.name = "SlackConfigApiError";
13
+ this.code = code;
14
+ this.status = status;
15
+ }
16
+ }
17
+ async function slackJson(method, body, options = {}) {
18
+ const fetchFn = options.fetch ?? globalThis.fetch;
19
+ let response;
20
+ let raw;
21
+ try {
22
+ response = await fetchFn(`${options.apiBaseUrl ?? SLACK_API}/${method}`, {
23
+ method: "POST",
24
+ headers: {
25
+ "content-type": "application/json; charset=utf-8",
26
+ ...(options.token ? { authorization: `Bearer ${options.token}` } : {}),
27
+ },
28
+ body: JSON.stringify(body),
29
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
30
+ });
31
+ raw = await response.text();
32
+ }
33
+ catch (error) {
34
+ throw new Error(`Slack ${method} request failed before receiving a response`, { cause: error });
35
+ }
36
+ let data;
37
+ try {
38
+ data = JSON.parse(raw);
39
+ }
40
+ catch {
41
+ throw new Error(`Slack ${method} failed: HTTP ${response.status} returned non-JSON`);
42
+ }
43
+ if (!response.ok || data.ok !== true) {
44
+ throw new SlackConfigApiError(method, response.status, data, raw.trim().slice(0, 200) || "unknown_error");
45
+ }
46
+ return data;
47
+ }
48
+ /** Create is intentionally single-attempt: an ambiguous response may already have created the app. */
49
+ export async function createSlackApp(configToken, manifest, options = {}) {
50
+ const data = await slackJson("apps.manifest.create", { manifest: JSON.stringify(manifest) }, { ...options, token: configToken });
51
+ const credentials = data.credentials;
52
+ if (!data.app_id || !credentials?.client_id || !credentials.client_secret || !credentials.signing_secret) {
53
+ throw new Error("Slack apps.manifest.create succeeded but returned incomplete app credentials");
54
+ }
55
+ return {
56
+ appId: data.app_id,
57
+ clientId: credentials.client_id,
58
+ clientSecret: credentials.client_secret,
59
+ signingSecret: credentials.signing_secret,
60
+ oauthAuthorizeUrl: data.oauth_authorize_url,
61
+ };
62
+ }
63
+ export async function updateSlackAppManifest(configToken, appId, manifest, options = {}) {
64
+ await slackJson("apps.manifest.update", { app_id: appId, manifest: JSON.stringify(manifest) }, { ...options, token: configToken });
65
+ }
66
+ export async function rotateSlackConfigToken(refreshToken, options = {}) {
67
+ const data = await slackJson("tooling.tokens.rotate", { refresh_token: refreshToken }, options);
68
+ if (!data.token || !data.refresh_token || typeof data.exp !== "number") {
69
+ throw new Error("Slack tooling.tokens.rotate succeeded but returned incomplete token credentials");
70
+ }
71
+ return {
72
+ token: data.token,
73
+ refreshToken: data.refresh_token,
74
+ expiresAt: data.exp * 1000,
75
+ teamId: data.team_id,
76
+ };
77
+ }
78
+ function slackOAuthFailure(status, code) {
79
+ const reason = (() => {
80
+ switch (code) {
81
+ case "bad_client_secret":
82
+ return "Slack rejected the app client secret";
83
+ case "invalid_client_id":
84
+ return "Slack rejected the app client ID";
85
+ case "bad_redirect_uri":
86
+ case "invalid_redirect_uri":
87
+ return "Slack rejected the temporary OAuth redirect URL";
88
+ case "invalid_code":
89
+ return "Slack rejected the authorization code";
90
+ case "code_already_used":
91
+ return "the Slack authorization code was already used";
92
+ case "access_denied":
93
+ return "the Slack installation was not approved";
94
+ default:
95
+ return "Slack rejected the OAuth exchange";
96
+ }
97
+ })();
98
+ return new Error(`${reason} (HTTP ${status}); re-run fastagent add slack to resume installation`);
99
+ }
100
+ export async function exchangeSlackOAuthCode(input, options = {}) {
101
+ const authorization = Buffer.from(`${input.clientId}:${input.clientSecret}`, "utf8").toString("base64");
102
+ const fetchFn = options.fetch ?? globalThis.fetch;
103
+ let response;
104
+ let raw;
105
+ try {
106
+ response = await fetchFn(`${options.apiBaseUrl ?? SLACK_API}/oauth.v2.access`, {
107
+ method: "POST",
108
+ headers: {
109
+ authorization: `Basic ${authorization}`,
110
+ "content-type": "application/x-www-form-urlencoded",
111
+ },
112
+ body: new URLSearchParams({ code: input.code, redirect_uri: input.redirectUrl }).toString(),
113
+ signal: AbortSignal.timeout(REQUEST_TIMEOUT_MS),
114
+ });
115
+ raw = await response.text();
116
+ }
117
+ catch (error) {
118
+ throw new Error("Slack oauth.v2.access request failed before receiving a response", { cause: error });
119
+ }
120
+ let data;
121
+ try {
122
+ data = JSON.parse(raw);
123
+ }
124
+ catch {
125
+ throw new Error(`Slack oauth.v2.access failed: HTTP ${response.status} returned non-JSON`);
126
+ }
127
+ if (!response.ok || data.ok !== true)
128
+ throw slackOAuthFailure(response.status, data.error);
129
+ if (!data.access_token ||
130
+ !data.refresh_token ||
131
+ typeof data.expires_in !== "number" ||
132
+ !data.app_id ||
133
+ !data.team?.id) {
134
+ throw new Error("Slack oauth.v2.access succeeded but returned incomplete rotating bot credentials/app identity");
135
+ }
136
+ return {
137
+ botToken: data.access_token,
138
+ botRefreshToken: data.refresh_token,
139
+ botTokenExpiresAt: Date.now() + data.expires_in * 1_000,
140
+ appId: data.app_id,
141
+ teamId: data.team.id,
142
+ teamName: data.team.name,
143
+ botUserId: data.bot_user_id,
144
+ scopes: (data.scope ?? "")
145
+ .split(",")
146
+ .map((scope) => scope.trim())
147
+ .filter(Boolean),
148
+ };
149
+ }
@@ -0,0 +1,24 @@
1
+ /**
2
+ * Slack's half of the shared context buffer (mechanics + consume protocol: ../context-buffer.ts):
3
+ * the entry shape, its fold-line rendering, and buffered-file selection. Durable, bounded
4
+ * unsummoned Slack discussion folded into the next answered turn in the same place.
5
+ */
6
+ import { type ContextBuffer } from "../context-buffer.ts";
7
+ export interface SlackBufferEntry {
8
+ sender: string;
9
+ body: string;
10
+ messageId: string;
11
+ replyTo?: string;
12
+ fileIds?: string[];
13
+ }
14
+ export interface SlackBufferedFileRef {
15
+ id: string;
16
+ from: string;
17
+ messageId: string;
18
+ }
19
+ export declare function collectSlackBufferedFiles(consumed: SlackBufferEntry[], primaryIds: Set<string>): {
20
+ files: SlackBufferedFileRef[];
21
+ skipped: number;
22
+ };
23
+ export type SlackContextBuffer = ContextBuffer<SlackBufferEntry>;
24
+ export declare function createSlackContextBuffer(path: string, label: string): SlackContextBuffer;
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Slack's half of the shared context buffer (mechanics + consume protocol: ../context-buffer.ts):
3
+ * the entry shape, its fold-line rendering, and buffered-file selection. Durable, bounded
4
+ * unsummoned Slack discussion folded into the next answered turn in the same place.
5
+ */
6
+ import { BUFFER_ATTACH_MAX, createContextBuffer as createGenericContextBuffer, } from "../context-buffer.js";
7
+ function bufferLine(entry) {
8
+ const meta = [`msg ${entry.messageId}`, entry.replyTo ? `thread root ${entry.replyTo}` : undefined]
9
+ .filter(Boolean)
10
+ .join(", ");
11
+ return `${entry.sender} (${meta}): ${entry.body}`;
12
+ }
13
+ export function collectSlackBufferedFiles(consumed, primaryIds) {
14
+ const seen = new Set();
15
+ const files = [];
16
+ for (const entry of consumed) {
17
+ for (const id of entry.fileIds ?? []) {
18
+ if (primaryIds.has(id) || seen.has(id))
19
+ continue;
20
+ seen.add(id);
21
+ files.push({ id, from: entry.sender, messageId: entry.messageId });
22
+ }
23
+ }
24
+ return { files: files.slice(-BUFFER_ATTACH_MAX), skipped: Math.max(0, files.length - BUFFER_ATTACH_MAX) };
25
+ }
26
+ function isEntry(value) {
27
+ const entry = value;
28
+ return (typeof entry?.sender === "string" &&
29
+ typeof entry.body === "string" &&
30
+ typeof entry.messageId === "string" &&
31
+ (entry.replyTo === undefined || typeof entry.replyTo === "string") &&
32
+ (entry.fileIds === undefined ||
33
+ (Array.isArray(entry.fileIds) && entry.fileIds.every((id) => typeof id === "string"))));
34
+ }
35
+ export function createSlackContextBuffer(path, label) {
36
+ return createGenericContextBuffer({ path, label, isEntry, line: bufferLine });
37
+ }
@@ -0,0 +1,19 @@
1
+ /** Resolve Slack file IDs at dequeue, then stream one engine-neutral Agent turn. */
2
+ import type { Agent, AgentEvent } from "../../agent.ts";
3
+ import { type BusyRetry } from "../invoke-turn-kit.ts";
4
+ import type { SlackBufferedFileRef } from "./context-buffer.ts";
5
+ import type { SlackApi } from "./slack-api.ts";
6
+ export interface SlackTurnTransport {
7
+ api: SlackApi;
8
+ channelId: string;
9
+ filesDir: string;
10
+ label: string;
11
+ }
12
+ export interface SlackTurnAttachments {
13
+ primaryFileIds: string[];
14
+ buffered: {
15
+ files: SlackBufferedFileRef[];
16
+ skipped: number;
17
+ };
18
+ }
19
+ export declare function invokeSlackTurn(agent: Agent, session: string, text: string, transport: SlackTurnTransport, attachments: SlackTurnAttachments, onCompleted?: () => void, busyRetry?: BusyRetry): AsyncIterable<AgentEvent>;
@@ -0,0 +1,63 @@
1
+ import { log } from "../../log.js";
2
+ import { DEFAULT_BUSY_RETRY, attachedFilesManifest, attributedFileName, backgroundImagesManifest, missingAttachmentsNote, streamTurnWithBusyRetry, } from "../invoke-turn-kit.js";
3
+ const MARKDOWN_INSTRUCTION = "\n\n(Format your reply as standard Markdown. Slack renders it natively. Do not use HTML or Slack control-mention syntax such as <!here>, <!channel>, or <!everyone>.)";
4
+ async function resolveFile(transport, fileId) {
5
+ const info = await transport.api.fileInfo(fileId);
6
+ return info.mimetype?.toLowerCase().startsWith("image/")
7
+ ? { image: await transport.api.fetchImage(info) }
8
+ : { file: await transport.api.fetchFile(info, transport.channelId, transport.filesDir) };
9
+ }
10
+ async function resolveInputs(transport, attachments) {
11
+ const images = [];
12
+ const files = [];
13
+ for (const id of attachments.primaryFileIds) {
14
+ const resolved = await resolveFile(transport, id);
15
+ if (resolved.image)
16
+ images.push(resolved.image);
17
+ if (resolved.file)
18
+ files.push(resolved.file);
19
+ }
20
+ const backgroundImages = [];
21
+ const backgroundFiles = [];
22
+ let lost = 0;
23
+ const results = await Promise.allSettled(attachments.buffered.files.map(async (ref) => ({ ref, resolved: await resolveFile(transport, ref.id) })));
24
+ for (const result of results) {
25
+ if (result.status === "fulfilled") {
26
+ const { ref, resolved } = result.value;
27
+ if (resolved.image)
28
+ backgroundImages.push({ image: resolved.image, ref });
29
+ if (resolved.file)
30
+ backgroundFiles.push({ file: resolved.file, ref });
31
+ }
32
+ else {
33
+ lost++;
34
+ log.warn(`${transport.label} could not load an earlier (buffered) Slack file: ${String(result.reason)}`);
35
+ }
36
+ }
37
+ const missingNote = missingAttachmentsNote(lost + attachments.buffered.skipped);
38
+ const imageManifest = backgroundImagesManifest(images.length, backgroundImages.map(({ ref }) => ref));
39
+ const allFiles = [
40
+ ...files,
41
+ ...backgroundFiles.map(({ file, ref }) => ({
42
+ ...file,
43
+ name: attributedFileName(file.name, ref.from, ref.messageId),
44
+ })),
45
+ ];
46
+ const allImages = [...images, ...backgroundImages.map(({ image }) => image)];
47
+ return {
48
+ images: allImages.length ? allImages : undefined,
49
+ promptSuffix: `${missingNote}${imageManifest}${attachedFilesManifest(allFiles)}`,
50
+ };
51
+ }
52
+ export async function* invokeSlackTurn(agent, session, text, transport, attachments, onCompleted, busyRetry = DEFAULT_BUSY_RETRY) {
53
+ let resolved;
54
+ try {
55
+ resolved = await resolveInputs(transport, attachments);
56
+ }
57
+ catch (error) {
58
+ yield { type: "failed", details: `could not load Slack attachment: ${String(error)}`, retryable: true };
59
+ return;
60
+ }
61
+ const prompt = { text: `${text}${resolved.promptSuffix}${MARKDOWN_INSTRUCTION}`, images: resolved.images };
62
+ yield* streamTurnWithBusyRetry(agent, session, prompt, { label: transport.label, onCompleted, busyRetry });
63
+ }
@@ -0,0 +1,49 @@
1
+ export type SlackGroupBehavior = "context" | "mentions";
2
+ export interface SlackAppManifest {
3
+ display_information: {
4
+ name: string;
5
+ description: string;
6
+ background_color: string;
7
+ };
8
+ features: {
9
+ app_home: {
10
+ home_tab_enabled: boolean;
11
+ messages_tab_enabled: boolean;
12
+ messages_tab_read_only_enabled: boolean;
13
+ };
14
+ agent_view: {
15
+ agent_description: string;
16
+ suggested_prompts: Array<{
17
+ title: string;
18
+ message: string;
19
+ }>;
20
+ };
21
+ bot_user: {
22
+ display_name: string;
23
+ always_online: boolean;
24
+ };
25
+ };
26
+ oauth_config: {
27
+ scopes: {
28
+ bot: string[];
29
+ };
30
+ redirect_urls?: string[];
31
+ };
32
+ settings: {
33
+ event_subscriptions?: {
34
+ request_url: string;
35
+ bot_events: string[];
36
+ };
37
+ org_deploy_enabled: boolean;
38
+ socket_mode_enabled: boolean;
39
+ token_rotation_enabled: boolean;
40
+ };
41
+ }
42
+ export declare function slackBotScopes(groupBehavior: SlackGroupBehavior): string[];
43
+ export declare function slackBotEvents(groupBehavior: SlackGroupBehavior): string[];
44
+ export declare function buildSlackManifest(input: {
45
+ name: string;
46
+ groupBehavior: SlackGroupBehavior;
47
+ requestUrl?: string;
48
+ redirectUrl?: string;
49
+ }): SlackAppManifest;
@@ -0,0 +1,69 @@
1
+ const SLACK_BASE_BOT_SCOPES = [
2
+ "app_mentions:read",
3
+ "assistant:write",
4
+ "chat:write",
5
+ "files:read",
6
+ "files:write",
7
+ "im:history",
8
+ "reactions:write",
9
+ ];
10
+ const SLACK_CONTEXT_BOT_SCOPES = ["channels:history", "groups:history", "mpim:history"];
11
+ const SLACK_BASE_BOT_EVENTS = ["app_context_changed", "app_home_opened", "app_mention", "message.im"];
12
+ const SLACK_CONTEXT_BOT_EVENTS = ["message.channels", "message.groups", "message.mpim"];
13
+ export function slackBotScopes(groupBehavior) {
14
+ return [...SLACK_BASE_BOT_SCOPES, ...(groupBehavior === "context" ? SLACK_CONTEXT_BOT_SCOPES : [])].sort();
15
+ }
16
+ export function slackBotEvents(groupBehavior) {
17
+ return [...SLACK_BASE_BOT_EVENTS, ...(groupBehavior === "context" ? SLACK_CONTEXT_BOT_EVENTS : [])].sort();
18
+ }
19
+ /** Slack's bot display name is more restrictive than the app name. */
20
+ function slackBotDisplayName(name) {
21
+ const normalized = name
22
+ .toLowerCase()
23
+ .replace(/[^a-z0-9._-]+/g, "-")
24
+ .replace(/^[^a-z0-9]+|[^a-z0-9]+$/g, "")
25
+ .slice(0, 80);
26
+ return normalized || "fastagent";
27
+ }
28
+ export function buildSlackManifest(input) {
29
+ const name = input.name.trim().slice(0, 35) || "FastAgent";
30
+ return {
31
+ display_information: {
32
+ name,
33
+ description: "A FastAgent-powered internal workspace agent.",
34
+ background_color: "#111827",
35
+ },
36
+ features: {
37
+ app_home: {
38
+ home_tab_enabled: false,
39
+ messages_tab_enabled: true,
40
+ messages_tab_read_only_enabled: false,
41
+ },
42
+ agent_view: {
43
+ agent_description: "A workspace agent powered by the files, skills, and tools in its FastAgent definition.",
44
+ suggested_prompts: [
45
+ { title: "What can you do?", message: "What can you help me with?" },
46
+ { title: "Summarize context", message: "Summarize the relevant context and propose next steps." },
47
+ ],
48
+ },
49
+ bot_user: { display_name: slackBotDisplayName(name), always_online: false },
50
+ },
51
+ oauth_config: {
52
+ scopes: { bot: slackBotScopes(input.groupBehavior) },
53
+ ...(input.redirectUrl ? { redirect_urls: [input.redirectUrl] } : {}),
54
+ },
55
+ settings: {
56
+ ...(input.requestUrl
57
+ ? {
58
+ event_subscriptions: {
59
+ request_url: input.requestUrl,
60
+ bot_events: slackBotEvents(input.groupBehavior),
61
+ },
62
+ }
63
+ : {}),
64
+ org_deploy_enabled: false,
65
+ socket_mode_enabled: false,
66
+ token_rotation_enabled: true,
67
+ },
68
+ };
69
+ }