@fastagent-sh/fastagent 0.13.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.
- package/README.md +5 -5
- package/dist/agent.d.ts +20 -0
- package/dist/agent.js +9 -0
- package/dist/channels/context-buffer.d.ts +30 -0
- package/dist/channels/context-buffer.js +98 -0
- package/dist/channels/control.d.ts +28 -0
- package/dist/channels/control.js +214 -0
- package/dist/channels/feishu/card.js +1 -1
- package/dist/channels/feishu/context-buffer.d.ts +43 -0
- package/dist/channels/feishu/context-buffer.js +72 -0
- package/dist/channels/feishu/crypto.d.ts +4 -2
- package/dist/channels/feishu/crypto.js +4 -2
- package/dist/channels/feishu/feishu-api.d.ts +15 -7
- package/dist/channels/feishu/feishu-api.js +22 -4
- package/dist/channels/feishu/feishu.d.ts +38 -16
- package/dist/channels/feishu/feishu.js +286 -151
- package/dist/channels/feishu/invoke-turn.d.ts +24 -31
- package/dist/channels/feishu/invoke-turn.js +61 -62
- package/dist/channels/feishu/model.d.ts +98 -0
- package/dist/channels/feishu/model.js +9 -0
- package/dist/channels/feishu/normalize.d.ts +23 -0
- package/dist/channels/feishu/normalize.js +132 -0
- package/dist/channels/feishu/owned-threads.d.ts +7 -0
- package/dist/channels/feishu/owned-threads.js +47 -0
- package/dist/channels/feishu/parse.d.ts +21 -103
- package/dist/channels/feishu/parse.js +35 -145
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +26 -142
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- package/dist/channels/feishu/scaffold/channel.ts +10 -3
- package/dist/channels/feishu/setup-mode.d.ts +8 -0
- package/dist/channels/feishu/setup-mode.js +2 -0
- package/dist/channels/feishu/ws-ingress.d.ts +28 -0
- package/dist/channels/feishu/ws-ingress.js +136 -0
- package/dist/channels/github/github.js +8 -6
- package/dist/channels/http.d.ts +14 -0
- package/dist/channels/http.js +35 -2
- package/dist/channels/invoke-turn-kit.d.ts +65 -0
- package/dist/channels/invoke-turn-kit.js +87 -0
- package/dist/channels/lark/lark.d.ts +4 -2
- package/dist/channels/lark/lark.js +4 -1
- package/dist/channels/lark/onboard.d.ts +8 -4
- package/dist/channels/lark/onboard.js +8 -0
- package/dist/channels/lark/scaffold/channel.ts +10 -3
- package/dist/channels/preview-kit.d.ts +109 -0
- package/dist/channels/preview-kit.js +183 -0
- package/dist/channels/seen.d.ts +5 -0
- package/dist/channels/seen.js +35 -0
- package/dist/channels/slack/bot-auth.d.ts +15 -0
- package/dist/channels/slack/bot-auth.js +146 -0
- package/dist/channels/slack/config-api.d.ts +60 -0
- package/dist/channels/slack/config-api.js +149 -0
- package/dist/channels/slack/context-buffer.d.ts +24 -0
- package/dist/channels/slack/context-buffer.js +37 -0
- package/dist/channels/slack/invoke-turn.d.ts +19 -0
- package/dist/channels/slack/invoke-turn.js +63 -0
- package/dist/channels/slack/manifest.d.ts +49 -0
- package/dist/channels/slack/manifest.js +69 -0
- package/dist/channels/slack/model.d.ts +67 -0
- package/dist/channels/slack/model.js +2 -0
- package/dist/channels/slack/onboard.d.ts +41 -0
- package/dist/channels/slack/onboard.js +120 -0
- package/dist/channels/slack/onboarding-state.d.ts +31 -0
- package/dist/channels/slack/onboarding-state.js +69 -0
- package/dist/channels/slack/owned-threads.d.ts +6 -0
- package/dist/channels/slack/owned-threads.js +43 -0
- package/dist/channels/slack/parse.d.ts +23 -0
- package/dist/channels/slack/parse.js +81 -0
- package/dist/channels/slack/preview.d.ts +24 -0
- package/dist/channels/slack/preview.js +359 -0
- package/dist/channels/slack/reaction.d.ts +24 -0
- package/dist/channels/slack/reaction.js +62 -0
- package/dist/channels/slack/register-webhook.d.ts +10 -0
- package/dist/channels/slack/register-webhook.js +49 -0
- package/dist/channels/slack/scaffold/channel.ts +33 -0
- package/dist/channels/slack/scaffold/slack-send.ts +171 -0
- package/dist/channels/slack/setup-server.d.ts +17 -0
- package/dist/channels/slack/setup-server.js +103 -0
- package/dist/channels/slack/slack-api.d.ts +77 -0
- package/dist/channels/slack/slack-api.js +415 -0
- package/dist/channels/slack/slack.d.ts +58 -0
- package/dist/channels/slack/slack.js +451 -0
- package/dist/channels/slack/welcomed.d.ts +5 -0
- package/dist/channels/slack/welcomed.js +32 -0
- package/dist/channels/state.js +3 -3
- package/dist/channels/stop-command.d.ts +6 -0
- package/dist/channels/stop-command.js +36 -0
- package/dist/channels/tasks.d.ts +13 -0
- package/dist/channels/tasks.js +10 -0
- package/dist/channels/telegram/context-buffer.d.ts +8 -17
- package/dist/channels/telegram/context-buffer.js +6 -85
- package/dist/channels/telegram/invoke-turn.d.ts +5 -22
- package/dist/channels/telegram/invoke-turn.js +11 -58
- package/dist/channels/telegram/preview.d.ts +4 -7
- package/dist/channels/telegram/preview.js +24 -142
- package/dist/channels/telegram/telegram.js +23 -9
- package/dist/channels/{feishu/text.d.ts → text.d.ts} +1 -1
- package/dist/channels/{feishu/text.js → text.js} +1 -1
- package/dist/channels/turn-queue.js +1 -1
- package/dist/channels/turn-store.d.ts +1 -1
- package/dist/channels/turn-store.js +2 -3
- package/dist/cli/add-feishu.d.ts +27 -0
- package/dist/{cli-add-feishu.js → cli/add-feishu.js} +167 -44
- package/dist/cli/add-slack.d.ts +10 -0
- package/dist/cli/add-slack.js +204 -0
- package/dist/{cli-auth.d.ts → cli/auth-view.d.ts} +1 -1
- package/dist/{cli-auth.js → cli/auth-view.js} +1 -1
- package/dist/cli/commands/add.d.ts +13 -0
- package/dist/cli/commands/add.js +274 -0
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +559 -0
- package/dist/cli/commands/chat.d.ts +4 -0
- package/dist/cli/commands/chat.js +21 -0
- package/dist/cli/commands/deploy.d.ts +15 -0
- package/dist/cli/commands/deploy.js +394 -0
- package/dist/cli/commands/dev.d.ts +11 -0
- package/dist/cli/commands/dev.js +82 -0
- package/dist/cli/commands/fire.d.ts +7 -0
- package/dist/cli/commands/fire.js +45 -0
- package/dist/cli/commands/info.d.ts +7 -0
- package/dist/cli/commands/info.js +108 -0
- package/dist/cli/commands/init.d.ts +8 -0
- package/dist/cli/commands/init.js +81 -0
- package/dist/cli/commands/invoke.d.ts +7 -0
- package/dist/cli/commands/invoke.js +28 -0
- package/dist/cli/commands/login.d.ts +6 -0
- package/dist/cli/commands/login.js +52 -0
- package/dist/cli/commands/models.d.ts +1 -0
- package/dist/cli/commands/models.js +15 -0
- package/dist/cli/commands/schedule.d.ts +12 -0
- package/dist/cli/commands/schedule.js +89 -0
- package/dist/cli/commands/start.d.ts +10 -0
- package/dist/cli/commands/start.js +97 -0
- package/dist/cli/commands/tool.d.ts +1 -0
- package/dist/cli/commands/tool.js +38 -0
- package/dist/cli/fail.d.ts +17 -0
- package/dist/cli/fail.js +32 -0
- package/dist/{invoke-stream.d.ts → cli/invoke-stream.d.ts} +1 -1
- package/dist/{invoke-stream.js → cli/invoke-stream.js} +4 -0
- package/dist/cli/kernel.d.ts +90 -0
- package/dist/cli/kernel.js +190 -0
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.d.ts +11 -0
- package/dist/cli/program.js +479 -0
- package/dist/cli/serve.d.ts +48 -0
- package/dist/cli/serve.js +248 -0
- package/dist/cli/shared.d.ts +44 -0
- package/dist/cli/shared.js +246 -0
- package/dist/cli.js +8 -1329
- package/dist/collect.d.ts +14 -3
- package/dist/collect.js +24 -0
- package/dist/core.d.ts +3 -1
- package/dist/core.js +2 -0
- package/dist/deploy/docker/plan.d.ts +45 -0
- package/dist/deploy/docker/plan.js +141 -0
- package/dist/deploy/docker/run.d.ts +40 -0
- package/dist/deploy/docker/run.js +126 -0
- package/dist/deploy/fly/plan.d.ts +3 -1
- package/dist/deploy/fly/plan.js +12 -7
- package/dist/deploy/fly/run.d.ts +5 -4
- package/dist/deploy/fly/run.js +16 -5
- package/dist/deploy/preflight.d.ts +10 -4
- package/dist/deploy/preflight.js +57 -12
- package/dist/deploy/railway/plan.d.ts +3 -1
- package/dist/deploy/railway/plan.js +12 -4
- package/dist/deploy/railway/run.d.ts +5 -4
- package/dist/deploy/railway/run.js +15 -5
- package/dist/deploy/runner.d.ts +6 -2
- package/dist/deploy/runner.js +1 -0
- package/dist/deploy/secrets.d.ts +2 -1
- package/dist/deploy/secrets.js +23 -3
- package/dist/dev-supervisor.d.ts +0 -2
- package/dist/dev-supervisor.js +7 -3
- package/dist/engines/pi/auth.js +160 -46
- package/dist/engines/pi/channel.d.ts +22 -16
- package/dist/engines/pi/channel.js +90 -60
- package/dist/engines/pi/chat.d.ts +4 -16
- package/dist/engines/pi/chat.js +8 -188
- package/dist/engines/pi/config.d.ts +23 -9
- package/dist/engines/pi/config.js +35 -5
- package/dist/engines/pi/create.d.ts +36 -7
- package/dist/engines/pi/create.js +63 -22
- package/dist/engines/pi/harness.d.ts +65 -1
- package/dist/engines/pi/harness.js +166 -2
- package/dist/engines/pi/invoke.d.ts +56 -3
- package/dist/engines/pi/invoke.js +340 -20
- package/dist/engines/pi/login.d.ts +11 -0
- package/dist/engines/pi/login.js +17 -5
- package/dist/engines/pi/models.d.ts +56 -10
- package/dist/engines/pi/models.js +61 -23
- package/dist/engines/pi/search-tools.d.ts +10 -0
- package/dist/engines/pi/search-tools.js +138 -0
- package/dist/engines/pi/session-builder.d.ts +16 -0
- package/dist/engines/pi/session-builder.js +308 -0
- package/dist/engines/pi/session-control.d.ts +50 -0
- package/dist/engines/pi/session-control.js +604 -0
- package/dist/engines/pi/sessions.d.ts +17 -2
- package/dist/engines/pi/sessions.js +9 -0
- package/dist/engines/pi/tool-context.d.ts +46 -11
- package/dist/engines/pi/tool-context.js +11 -9
- package/dist/engines/pi/tool.d.ts +35 -6
- package/dist/engines/pi/tool.js +47 -1
- package/dist/engines/pi/wake-tool.d.ts +0 -3
- package/dist/engines/pi/wake-tool.js +9 -7
- package/dist/engines/pi/workspace.d.ts +56 -1
- package/dist/engines/pi/workspace.js +75 -16
- package/dist/feishu.d.ts +1 -1
- package/dist/feishu.js +1 -1
- package/dist/host/node.d.ts +23 -6
- package/dist/host/node.js +5 -4
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -0
- package/dist/lark.d.ts +1 -1
- package/dist/lark.js +1 -1
- package/dist/observe.js +3 -0
- package/dist/pi.d.ts +6 -4
- package/dist/pi.js +2 -1
- package/dist/scaffold/add-channel.d.ts +19 -6
- package/dist/scaffold/add-channel.js +127 -16
- package/dist/scaffold/templates/fastagent.config.mjs +5 -3
- package/dist/schedule/wakeups.d.ts +0 -3
- package/dist/schedule/wakeups.js +1 -1
- package/dist/session-remote.d.ts +53 -0
- package/dist/session-remote.js +336 -0
- package/dist/session.d.ts +265 -0
- package/dist/session.js +37 -0
- package/dist/slack.d.ts +2 -0
- package/dist/slack.js +2 -0
- package/dist/tunnel.d.ts +7 -4
- package/dist/tunnel.js +21 -10
- package/package.json +22 -7
- package/dist/channels/feishu/seen.d.ts +0 -5
- package/dist/channels/feishu/seen.js +0 -47
- package/dist/cli-add-feishu.d.ts +0 -8
- package/dist/cli-models.d.ts +0 -11
- package/dist/cli-models.js +0 -20
|
@@ -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
|
+
}
|
|
@@ -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,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
|
+
}
|