@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,67 @@
1
+ /** Minimal Slack Events/Web API shapes used by the first-party channel. Unknown fields remain ignored. */
2
+ export interface SlackFile {
3
+ id?: string;
4
+ name?: string;
5
+ title?: string;
6
+ mimetype?: string;
7
+ filetype?: string;
8
+ pretty_type?: string;
9
+ size?: number;
10
+ mode?: string;
11
+ is_external?: boolean;
12
+ external_type?: string;
13
+ file_access?: string;
14
+ url_private?: string;
15
+ url_private_download?: string;
16
+ }
17
+ interface SlackAppContextEntity {
18
+ type?: string;
19
+ value?: string;
20
+ team_id?: string;
21
+ }
22
+ export interface SlackMessageEvent {
23
+ type?: string;
24
+ subtype?: string;
25
+ hidden?: boolean;
26
+ user?: string;
27
+ bot_id?: string;
28
+ text?: string;
29
+ ts?: string;
30
+ event_ts?: string;
31
+ thread_ts?: string;
32
+ channel?: string;
33
+ channel_type?: string;
34
+ /** `app_home_opened` only: which App Home tab was opened (`messages` signals a DM open). */
35
+ tab?: string;
36
+ team?: string;
37
+ files?: SlackFile[];
38
+ /** Included on message.im when the Agent messaging experience is enabled. */
39
+ app_context?: {
40
+ entities?: SlackAppContextEntity[];
41
+ };
42
+ }
43
+ export interface SlackEventEnvelope {
44
+ type?: string;
45
+ challenge?: string;
46
+ team_id?: string;
47
+ context_team_id?: string;
48
+ enterprise_id?: string;
49
+ api_app_id?: string;
50
+ event_id?: string;
51
+ event_time?: number;
52
+ event?: SlackMessageEvent;
53
+ authorizations?: Array<{
54
+ team_id?: string;
55
+ enterprise_id?: string;
56
+ user_id?: string;
57
+ is_bot?: boolean;
58
+ }>;
59
+ }
60
+ /** A routed Slack turn. `threadTs:null` explicitly sends at channel top level. */
61
+ export interface SlackRoute {
62
+ session?: string;
63
+ channelId?: string;
64
+ threadTs?: string | null;
65
+ text?: string;
66
+ }
67
+ export {};
@@ -0,0 +1,2 @@
1
+ /** Minimal Slack Events/Web API shapes used by the first-party channel. Unknown fields remain ignored. */
2
+ export {};
@@ -0,0 +1,41 @@
1
+ import { createSlackApp, exchangeSlackOAuthCode, updateSlackAppManifest } from "./config-api.ts";
2
+ import { type SlackGroupBehavior } from "./manifest.ts";
3
+ import { type SlackOnboardingState } from "./onboarding-state.ts";
4
+ export interface SlackOnboardIO {
5
+ note(message: string): void;
6
+ openUrl(url: string): void;
7
+ /** Wait for the one OAuth redirect. Implementations must validate only the path; core validates state. */
8
+ waitForOAuth(): Promise<{
9
+ code?: string;
10
+ state?: string;
11
+ error?: string;
12
+ }>;
13
+ /** Stage runtime-only credentials into the gitignored .env. */
14
+ writeRuntimeSecrets(values: {
15
+ botToken?: string;
16
+ botRefreshToken?: string;
17
+ botTokenExpiresAt?: number;
18
+ clientId?: string;
19
+ clientSecret?: string;
20
+ signingSecret?: string;
21
+ }): Promise<void>;
22
+ }
23
+ export interface SlackOnboardInput {
24
+ stateRoot: string;
25
+ state: SlackOnboardingState;
26
+ requestUrl: string;
27
+ redirectUrl: string;
28
+ }
29
+ /** Create/resume one internal Slack app and complete its workspace OAuth installation. */
30
+ export declare function onboardSlackApp(input: SlackOnboardInput, io: SlackOnboardIO, deps?: {
31
+ createApp?: typeof createSlackApp;
32
+ updateManifest?: typeof updateSlackAppManifest;
33
+ exchangeCode?: typeof exchangeSlackOAuthCode;
34
+ }): Promise<SlackOnboardingState>;
35
+ export declare function newSlackOnboardingState(input: {
36
+ appName: string;
37
+ groupBehavior: SlackGroupBehavior;
38
+ configToken: string;
39
+ configRefreshToken: string;
40
+ now?: number;
41
+ }): SlackOnboardingState;
@@ -0,0 +1,120 @@
1
+ import { randomBytes } from "node:crypto";
2
+ import { createSlackApp, exchangeSlackOAuthCode, SlackConfigApiError, updateSlackAppManifest } from "./config-api.js";
3
+ import { buildSlackManifest, slackBotScopes } from "./manifest.js";
4
+ import { currentSlackConfigToken, writeSlackOnboardingState } from "./onboarding-state.js";
5
+ /** Create/resume one internal Slack app and complete its workspace OAuth installation. */
6
+ export async function onboardSlackApp(input, io, deps = {}) {
7
+ let state = input.state;
8
+ const current = await currentSlackConfigToken(input.stateRoot, state);
9
+ state = current.state;
10
+ const manifest = buildSlackManifest({
11
+ name: state.appName,
12
+ groupBehavior: state.groupBehavior,
13
+ requestUrl: input.requestUrl,
14
+ redirectUrl: input.redirectUrl,
15
+ });
16
+ if (!state.appId) {
17
+ if (state.createAttemptedAt) {
18
+ throw new Error(`a Slack app creation request started at ${state.createAttemptedAt}, but no app ID was returned. ` +
19
+ "Inspect https://api.slack.com/apps before retrying; if an app exists, delete that incomplete app first, then remove the local Slack onboarding state and re-run");
20
+ }
21
+ io.note("Creating the internal Slack app from its FastAgent manifest…");
22
+ // Record BEFORE the non-idempotent API call. A transport/internal failure may have created the app;
23
+ // refusing a blind retry is safer than silently producing duplicates.
24
+ state = { ...state, createAttemptedAt: new Date().toISOString() };
25
+ await writeSlackOnboardingState(input.stateRoot, state);
26
+ let created;
27
+ try {
28
+ created = await (deps.createApp ?? createSlackApp)(current.token, manifest);
29
+ }
30
+ catch (error) {
31
+ const ambiguous = !(error instanceof SlackConfigApiError) ||
32
+ ["fatal_error", "internal_error", "request_timeout", "service_unavailable", "failed_creating_app"].includes(error.code);
33
+ if (!ambiguous) {
34
+ state = { ...state, createAttemptedAt: undefined };
35
+ await writeSlackOnboardingState(input.stateRoot, state);
36
+ }
37
+ throw error;
38
+ }
39
+ state = {
40
+ ...state,
41
+ appId: created.appId,
42
+ createAttemptedAt: undefined,
43
+ clientId: created.clientId,
44
+ clientSecret: created.clientSecret,
45
+ signingSecret: created.signingSecret,
46
+ };
47
+ // Irreversible boundary first: a cancellation or .env write failure can resume without creating a duplicate.
48
+ await writeSlackOnboardingState(input.stateRoot, state);
49
+ io.note(`Created Slack app ${created.appId}; credentials captured locally.`);
50
+ }
51
+ else {
52
+ if (!state.clientId || !state.clientSecret) {
53
+ throw new Error(`Slack app ${state.appId} exists but OAuth client credentials are no longer available — ` +
54
+ "the app appears already installed; set SLACK_BOT_TOKEN/SLACK_SIGNING_SECRET in .env or remove the app and onboarding state to start over");
55
+ }
56
+ io.note(`Resuming Slack app ${state.appId}; refreshing its temporary setup URLs…`);
57
+ await (deps.updateManifest ?? updateSlackAppManifest)(current.token, state.appId, manifest);
58
+ }
59
+ if (state.signingSecret) {
60
+ await io.writeRuntimeSecrets({ signingSecret: state.signingSecret });
61
+ state = { ...state, signingSecret: undefined };
62
+ await writeSlackOnboardingState(input.stateRoot, state);
63
+ }
64
+ if (!state.appId || !state.clientId || !state.clientSecret) {
65
+ throw new Error("Slack onboarding state lost app OAuth credentials before installation");
66
+ }
67
+ const oauthState = randomBytes(24).toString("hex");
68
+ const authorize = new URL("https://slack.com/oauth/v2/authorize");
69
+ authorize.searchParams.set("client_id", state.clientId);
70
+ authorize.searchParams.set("scope", slackBotScopes(state.groupBehavior).join(","));
71
+ authorize.searchParams.set("redirect_uri", input.redirectUrl);
72
+ authorize.searchParams.set("state", oauthState);
73
+ io.note(`Approve the internal app installation in Slack: ${authorize}`);
74
+ io.openUrl(authorize.toString());
75
+ const callback = await io.waitForOAuth();
76
+ if (callback.error)
77
+ throw new Error("Slack OAuth installation was not approved");
78
+ if (!callback.code)
79
+ throw new Error("Slack OAuth callback carried no authorization code");
80
+ if (!callback.state || callback.state !== oauthState)
81
+ throw new Error("Slack OAuth callback state mismatch");
82
+ const oauth = await (deps.exchangeCode ?? exchangeSlackOAuthCode)({
83
+ clientId: state.clientId,
84
+ clientSecret: state.clientSecret,
85
+ code: callback.code,
86
+ redirectUrl: input.redirectUrl,
87
+ });
88
+ if (oauth.appId !== state.appId)
89
+ throw new Error("Slack OAuth installed a different app than the manifest app");
90
+ if (slackBotScopes(state.groupBehavior).some((scope) => !oauth.scopes.includes(scope))) {
91
+ throw new Error("Slack OAuth completed without all required bot scopes; re-run fastagent add slack to reinstall");
92
+ }
93
+ await io.writeRuntimeSecrets({
94
+ botToken: oauth.botToken,
95
+ botRefreshToken: oauth.botRefreshToken,
96
+ botTokenExpiresAt: oauth.botTokenExpiresAt,
97
+ clientId: state.clientId,
98
+ clientSecret: state.clientSecret,
99
+ });
100
+ state = {
101
+ ...state,
102
+ clientSecret: undefined,
103
+ teamId: oauth.teamId,
104
+ teamName: oauth.teamName,
105
+ installedAt: new Date().toISOString(),
106
+ };
107
+ await writeSlackOnboardingState(input.stateRoot, state);
108
+ return state;
109
+ }
110
+ export function newSlackOnboardingState(input) {
111
+ return {
112
+ version: 1,
113
+ appName: input.appName,
114
+ groupBehavior: input.groupBehavior,
115
+ configToken: input.configToken,
116
+ configRefreshToken: input.configRefreshToken,
117
+ // Slack config access tokens expire in 12 hours; use 11h so registration rotates before the edge.
118
+ configTokenExpiresAt: (input.now ?? Date.now()) + 11 * 60 * 60_000,
119
+ };
120
+ }
@@ -0,0 +1,31 @@
1
+ import type { SlackGroupBehavior } from "./manifest.ts";
2
+ export interface SlackOnboardingState {
3
+ version: 1;
4
+ appName: string;
5
+ groupBehavior: SlackGroupBehavior;
6
+ appId?: string;
7
+ /** Set before apps.manifest.create; without an appId it blocks blind duplicate-creation retries. */
8
+ createAttemptedAt?: string;
9
+ clientId?: string;
10
+ /** Kept only while OAuth installation is unfinished, then deleted. */
11
+ clientSecret?: string;
12
+ /** Kept only until it has been staged into .env, then deleted. */
13
+ signingSecret?: string;
14
+ configToken: string;
15
+ configRefreshToken: string;
16
+ configTokenExpiresAt: number;
17
+ teamId?: string;
18
+ teamName?: string;
19
+ installedAt?: string;
20
+ }
21
+ export declare function readSlackOnboardingState(stateRoot: string): Promise<SlackOnboardingState | undefined>;
22
+ /** Atomic replacement with owner-only permissions: this file carries a workspace-wide config refresh token. */
23
+ export declare function writeSlackOnboardingState(stateRoot: string, state: SlackOnboardingState): Promise<void>;
24
+ export declare function currentSlackConfigToken(stateRoot: string, state: SlackOnboardingState, options?: {
25
+ now?: number;
26
+ apiBaseUrl?: string;
27
+ fetch?: typeof fetch;
28
+ }): Promise<{
29
+ token: string;
30
+ state: SlackOnboardingState;
31
+ }>;
@@ -0,0 +1,69 @@
1
+ import { chmod, mkdir, readFile, rename, rm, writeFile } from "node:fs/promises";
2
+ import { dirname, join } from "node:path";
3
+ import { rotateSlackConfigToken } from "./config-api.js";
4
+ function slackOnboardingStatePath(stateRoot) {
5
+ return join(stateRoot, "channels", "slack", "onboarding.json");
6
+ }
7
+ function validState(value) {
8
+ const state = value;
9
+ return (typeof state === "object" &&
10
+ state !== null &&
11
+ state.version === 1 &&
12
+ typeof state.appName === "string" &&
13
+ (state.groupBehavior === "context" || state.groupBehavior === "mentions") &&
14
+ typeof state.configToken === "string" &&
15
+ typeof state.configRefreshToken === "string" &&
16
+ typeof state.configTokenExpiresAt === "number");
17
+ }
18
+ export async function readSlackOnboardingState(stateRoot) {
19
+ const file = slackOnboardingStatePath(stateRoot);
20
+ let raw;
21
+ try {
22
+ raw = await readFile(file, "utf8");
23
+ }
24
+ catch (error) {
25
+ if (error.code === "ENOENT")
26
+ return undefined;
27
+ throw new Error(`cannot read Slack onboarding state ${file}: ${error.message}`);
28
+ }
29
+ try {
30
+ const value = JSON.parse(raw);
31
+ if (!validState(value))
32
+ throw new Error("unexpected shape/version");
33
+ return value;
34
+ }
35
+ catch (error) {
36
+ throw new Error(`invalid Slack onboarding state ${file}: ${error.message}`);
37
+ }
38
+ }
39
+ /** Atomic replacement with owner-only permissions: this file carries a workspace-wide config refresh token. */
40
+ export async function writeSlackOnboardingState(stateRoot, state) {
41
+ const file = slackOnboardingStatePath(stateRoot);
42
+ await mkdir(dirname(file), { recursive: true });
43
+ const temp = `${file}.${process.pid}.${Date.now()}.tmp`;
44
+ try {
45
+ await writeFile(temp, `${JSON.stringify(state, null, 2)}\n`, { mode: 0o600 });
46
+ await chmod(temp, 0o600);
47
+ await rename(temp, file);
48
+ await chmod(file, 0o600);
49
+ }
50
+ catch (error) {
51
+ await rm(temp, { force: true }).catch(() => { });
52
+ throw error;
53
+ }
54
+ }
55
+ export async function currentSlackConfigToken(stateRoot, state, options = {}) {
56
+ const now = options.now ?? Date.now();
57
+ if (state.configTokenExpiresAt > now + 5 * 60_000)
58
+ return { token: state.configToken, state };
59
+ const rotated = await rotateSlackConfigToken(state.configRefreshToken, options);
60
+ const next = {
61
+ ...state,
62
+ configToken: rotated.token,
63
+ configRefreshToken: rotated.refreshToken,
64
+ configTokenExpiresAt: rotated.expiresAt,
65
+ teamId: state.teamId ?? rotated.teamId,
66
+ };
67
+ await writeSlackOnboardingState(stateRoot, next);
68
+ return { token: next.configToken, state: next };
69
+ }
@@ -0,0 +1,6 @@
1
+ interface OwnedSlackThreads {
2
+ has(teamId: string, channelId: string, rootTs: string): boolean;
3
+ add(teamId: string, channelId: string, rootTs: string): void;
4
+ }
5
+ export declare function createOwnedSlackThreads(path: string, label?: string, now?: () => number): OwnedSlackThreads;
6
+ export {};
@@ -0,0 +1,43 @@
1
+ /** Durable roots of Slack group threads entered by an explicit summon. */
2
+ import { log } from "../../log.js";
3
+ import { loadStateFile, saveStateFile } from "../state.js";
4
+ function key(teamId, channelId, rootTs) {
5
+ return `${teamId}:${channelId}:${rootTs}`;
6
+ }
7
+ function isRecord(value) {
8
+ const record = value;
9
+ return (typeof record?.teamId === "string" &&
10
+ typeof record.channelId === "string" &&
11
+ typeof record.rootTs === "string" &&
12
+ typeof record.createdAt === "number" &&
13
+ Number.isFinite(record.createdAt));
14
+ }
15
+ export function createOwnedSlackThreads(path, label = "[slack]", now = Date.now) {
16
+ const raw = loadStateFile(path);
17
+ let records = new Map();
18
+ if (raw !== undefined) {
19
+ if (typeof raw === "object" &&
20
+ raw !== null &&
21
+ !Array.isArray(raw) &&
22
+ Object.entries(raw).every(([id, record]) => isRecord(record) && key(record.teamId, record.channelId, record.rootTs) === id)) {
23
+ records = new Map(Object.entries(raw));
24
+ }
25
+ else {
26
+ log.warn(`${label} unexpected shape in ${path} — starting with no managed Slack threads`);
27
+ }
28
+ }
29
+ return {
30
+ has(teamId, channelId, rootTs) {
31
+ return records.has(key(teamId, channelId, rootTs));
32
+ },
33
+ add(teamId, channelId, rootTs) {
34
+ const id = key(teamId, channelId, rootTs);
35
+ if (records.has(id))
36
+ return;
37
+ const next = new Map(records);
38
+ next.set(id, { teamId, channelId, rootTs, createdAt: now() });
39
+ saveStateFile(path, Object.fromEntries(next));
40
+ records = next;
41
+ },
42
+ };
43
+ }
@@ -0,0 +1,23 @@
1
+ import type { SlackEventEnvelope, SlackMessageEvent, SlackRoute } from "./model.ts";
2
+ export type { SlackEventEnvelope, SlackFile, SlackMessageEvent, SlackRoute } from "./model.ts";
3
+ /** Slack message events whose content represents a new human message rather than a mutation/service event. */
4
+ export declare function isSlackHumanMessage(event: SlackMessageEvent | undefined): event is SlackMessageEvent & {
5
+ channel: string;
6
+ ts: string;
7
+ user: string;
8
+ };
9
+ export declare function isSlackDirectMessage(event: SlackMessageEvent): boolean;
10
+ export declare function isSlackGroupMessage(event: SlackMessageEvent): boolean;
11
+ /** Stable install identity. Events API normally supplies team_id; the fallbacks cover Grid envelopes. */
12
+ export declare function slackTeamId(envelope: SlackEventEnvelope): string | undefined;
13
+ export declare function slackFileIds(event: SlackMessageEvent): string[];
14
+ /** Text plus a structural marker for file-only or captioned file shares. */
15
+ export declare function slackMessageText(event: SlackMessageEvent): string;
16
+ export declare function slackSenderLabel(event: SlackMessageEvent): string;
17
+ /** Main-channel discussion and each concrete thread are independent context buckets. */
18
+ export declare function slackPlaceKey(teamId: string, event: Pick<SlackMessageEvent, "channel" | "thread_ts">): string;
19
+ export declare function slackBufferText(text: string): string;
20
+ /** Canonical prompt envelope. The channel/thread ids also give slack-send an explicit delivery target. */
21
+ export declare function slackEnvelope(envelope: SlackEventEnvelope): string;
22
+ /** Default explicit-summon policy: DMs and app_mention events only. */
23
+ export declare function defaultSlackRoute(envelope: SlackEventEnvelope): SlackRoute | null;
@@ -0,0 +1,81 @@
1
+ import { codePointPrefix } from "../text.js";
2
+ const HUMAN_MESSAGE_SUBTYPES = new Set(["file_share", "thread_broadcast"]);
3
+ /** Slack message events whose content represents a new human message rather than a mutation/service event. */
4
+ export function isSlackHumanMessage(event) {
5
+ if (!event || (event.type !== "message" && event.type !== "app_mention"))
6
+ return false;
7
+ if (!event.channel || !event.ts || !event.user || event.bot_id || event.hidden)
8
+ return false;
9
+ return event.subtype === undefined || HUMAN_MESSAGE_SUBTYPES.has(event.subtype);
10
+ }
11
+ export function isSlackDirectMessage(event) {
12
+ return event.channel_type === "im";
13
+ }
14
+ export function isSlackGroupMessage(event) {
15
+ return !isSlackDirectMessage(event) && (event.type === "app_mention" || event.channel_type !== undefined);
16
+ }
17
+ /** Stable install identity. Events API normally supplies team_id; the fallbacks cover Grid envelopes. */
18
+ export function slackTeamId(envelope) {
19
+ return (envelope.team_id ??
20
+ envelope.event?.team ??
21
+ envelope.authorizations?.find((authorization) => authorization.team_id)?.team_id ??
22
+ envelope.context_team_id ??
23
+ envelope.enterprise_id ??
24
+ envelope.authorizations?.find((authorization) => authorization.enterprise_id)?.enterprise_id);
25
+ }
26
+ export function slackFileIds(event) {
27
+ return [...new Set((event.files ?? []).flatMap((file) => (typeof file.id === "string" ? [file.id] : [])))];
28
+ }
29
+ /** Text plus a structural marker for file-only or captioned file shares. */
30
+ export function slackMessageText(event) {
31
+ const text = event.text?.trim() ?? "";
32
+ const files = slackFileIds(event).length;
33
+ if (files === 0)
34
+ return text;
35
+ const marker = `[${files} attached file${files === 1 ? "" : "s"}]`;
36
+ return text ? `${text}\n${marker}` : marker;
37
+ }
38
+ export function slackSenderLabel(event) {
39
+ return `user ${event.user ?? "unknown"}`;
40
+ }
41
+ /** Main-channel discussion and each concrete thread are independent context buckets. */
42
+ export function slackPlaceKey(teamId, event) {
43
+ const base = `${teamId}:${event.channel ?? "unknown-channel"}`;
44
+ return event.thread_ts ? `${base}:root:${event.thread_ts}` : base;
45
+ }
46
+ export function slackBufferText(text) {
47
+ return codePointPrefix(text.replace(/\s+/g, " ").trim(), 280);
48
+ }
49
+ /** Canonical prompt envelope. The channel/thread ids also give slack-send an explicit delivery target. */
50
+ export function slackEnvelope(envelope) {
51
+ const event = envelope.event;
52
+ if (!event?.channel)
53
+ return "";
54
+ const team = slackTeamId(envelope) ?? "unknown-team";
55
+ const direct = isSlackDirectMessage(event);
56
+ const meta = [
57
+ `team ${team}`,
58
+ `channel ${event.channel} (${direct ? "direct" : "group"})`,
59
+ event.thread_ts ? `thread ${event.thread_ts}` : undefined,
60
+ event.ts ? `msg ${event.ts}` : undefined,
61
+ event.user ? `from user ${event.user}` : undefined,
62
+ ]
63
+ .filter(Boolean)
64
+ .join(", ");
65
+ const activeContext = (event.app_context?.entities ?? [])
66
+ .filter((entity) => entity.type && entity.value)
67
+ .slice(0, 5)
68
+ .map((entity) => `${codePointPrefix(String(entity.type).replace(/\s+/g, " "), 80)}=` +
69
+ codePointPrefix(String(entity.value).replace(/\s+/g, " "), 200))
70
+ .join(", ");
71
+ const scope = direct ? "" : "\n[group chat — multiple people; recent discussion is sender-prefixed]";
72
+ const context = activeContext ? `\n[Slack context currently open for the user: ${activeContext}]` : "";
73
+ return `[slack: ${meta}]${scope}${context}\n${slackMessageText(event)}`;
74
+ }
75
+ /** Default explicit-summon policy: DMs and app_mention events only. */
76
+ export function defaultSlackRoute(envelope) {
77
+ const event = envelope.event;
78
+ if (!isSlackHumanMessage(event))
79
+ return null;
80
+ return isSlackDirectMessage(event) || event.type === "app_mention" ? {} : null;
81
+ }
@@ -0,0 +1,24 @@
1
+ /** Slack reply rendering: native Agent streams first, rate-safe edited-message compatibility second. */
2
+ import type { AgentEvent } from "../../agent.ts";
3
+ import { type ChannelFailure, defaultErrorMessage } from "../preview-kit.ts";
4
+ import { type SlackApi, type SlackTarget, type SlackTaskDisplayMode } from "./slack-api.ts";
5
+ export type SlackFailure = ChannelFailure;
6
+ export type SlackRendering = "native" | "classic";
7
+ export { defaultErrorMessage };
8
+ /** LLM output uses standard Markdown, never Slack's notification control syntax. Keep explicit sends in
9
+ * the slack-send tool, where the Agent has to choose a side-effecting delivery action deliberately.
10
+ * The inner class excludes BOTH `<` and `>` on purpose: bounding each run by the next delimiter keeps
11
+ * the scan linear even on adversarial input like `(<!)^n` (a `>`-only bound would still be polynomial
12
+ * across the many `<` start positions), while still neutralizing every `<@…>` / `<!…>` control sequence
13
+ * (real Slack controls never contain a `<`). */
14
+ export declare function sanitizeSlackMarkdown(markdown: string): string;
15
+ /** Settle a queue/drop/defer notice. These are authored plain strings, so the basic text API is enough. */
16
+ export declare function settleSlackPreview(api: SlackApi, target: SlackTarget, previewTs: string | undefined, text: string): Promise<void>;
17
+ export declare function streamSlackReply(events: AsyncIterable<AgentEvent>, api: SlackApi, target: SlackTarget, formatError: (failure: SlackFailure) => string | undefined, options?: {
18
+ rendering?: SlackRendering;
19
+ initialPreviewTs?: string;
20
+ threadTitle?: string;
21
+ disclaimer?: string | false;
22
+ taskDisplay?: SlackTaskDisplayMode;
23
+ label?: string;
24
+ }): Promise<void>;