@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.
- package/README.md +3 -3
- 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 +8 -11
- package/dist/channels/feishu/context-buffer.js +6 -67
- package/dist/channels/feishu/feishu-api.d.ts +11 -3
- package/dist/channels/feishu/feishu-api.js +20 -2
- package/dist/channels/feishu/feishu.d.ts +28 -16
- package/dist/channels/feishu/feishu.js +210 -188
- package/dist/channels/feishu/invoke-turn.d.ts +7 -20
- package/dist/channels/feishu/invoke-turn.js +10 -62
- package/dist/channels/feishu/model.d.ts +2 -1
- package/dist/channels/feishu/normalize.d.ts +2 -1
- package/dist/channels/feishu/owned-threads.d.ts +2 -6
- package/dist/channels/feishu/parse.d.ts +1 -1
- package/dist/channels/feishu/preview.d.ts +4 -7
- package/dist/channels/feishu/preview.js +24 -140
- package/dist/channels/feishu/register-app.d.ts +2 -1
- package/dist/channels/feishu/register-webhook.d.ts +2 -1
- 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/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 +4 -0
- package/dist/cli/commands/add.js +152 -20
- package/dist/cli/commands/attach.d.ts +82 -0
- package/dist/cli/commands/attach.js +559 -0
- package/dist/cli/commands/chat.d.ts +1 -0
- package/dist/cli/commands/chat.js +6 -1
- package/dist/cli/commands/deploy.d.ts +2 -0
- package/dist/cli/commands/deploy.js +78 -22
- package/dist/cli/commands/dev.js +11 -5
- package/dist/cli/commands/fire.js +3 -3
- package/dist/cli/commands/info.js +2 -2
- package/dist/cli/commands/invoke.js +2 -3
- package/dist/cli/commands/login.js +14 -25
- package/dist/cli/commands/models.js +1 -1
- package/dist/cli/commands/start.js +15 -8
- package/dist/cli/commands/tool.js +2 -1
- package/dist/cli/fail.d.ts +0 -2
- package/dist/cli/fail.js +1 -1
- 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 +3 -2
- package/dist/cli/models-view.d.ts +21 -0
- package/dist/cli/models-view.js +66 -0
- package/dist/cli/program.js +79 -21
- package/dist/cli/serve.d.ts +38 -18
- package/dist/cli/serve.js +197 -39
- package/dist/cli/shared.d.ts +26 -6
- package/dist/cli/shared.js +163 -33
- 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 +2 -2
- package/dist/deploy/docker/plan.js +8 -6
- 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 +53 -9
- 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 +2 -1
- 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/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 -261
- package/dist/engines/pi/config.d.ts +14 -9
- package/dist/engines/pi/config.js +33 -18
- package/dist/engines/pi/create.d.ts +22 -2
- package/dist/engines/pi/create.js +20 -14
- package/dist/engines/pi/harness.d.ts +49 -0
- package/dist/engines/pi/harness.js +90 -2
- package/dist/engines/pi/invoke.d.ts +55 -2
- package/dist/engines/pi/invoke.js +304 -19
- package/dist/engines/pi/login.d.ts +11 -0
- package/dist/engines/pi/login.js +16 -4
- package/dist/engines/pi/models.d.ts +56 -10
- package/dist/engines/pi/models.js +61 -23
- 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 +18 -11
- package/dist/engines/pi/tool-context.js +3 -9
- package/dist/engines/pi/tool.d.ts +6 -8
- package/dist/engines/pi/tool.js +6 -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 +52 -0
- package/dist/engines/pi/workspace.js +72 -15
- 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 +5 -4
- package/dist/pi.js +2 -1
- package/dist/scaffold/add-channel.d.ts +19 -6
- package/dist/scaffold/add-channel.js +126 -17
- package/dist/scaffold/templates/fastagent.config.mjs +4 -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 +19 -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,171 @@
|
|
|
1
|
+
import { defineTool, z } from "@fastagent-sh/fastagent";
|
|
2
|
+
import { open, stat } from "node:fs/promises";
|
|
3
|
+
import { basename } from "node:path";
|
|
4
|
+
|
|
5
|
+
// Send a message or upload a local file to Slack. In a CHAT turn the channel delivers the reply
|
|
6
|
+
// itself — do NOT call this to answer a normal chat turn (that posts the message twice). This tool is
|
|
7
|
+
// for file uploads, and for turns NO channel is carrying: a scheduled turn (schedules/<name>.ts) or a
|
|
8
|
+
// self-scheduled wake-up, whose plain reply is not delivered anywhere. The channelId/threadTs come
|
|
9
|
+
// from the [slack: …] context line in a chat turn; a scheduled/woken turn has no such line, so its
|
|
10
|
+
// prompt must name the target channel id. tools/ is auto-discovered.
|
|
11
|
+
|
|
12
|
+
const API = "https://slack.com/api";
|
|
13
|
+
const MAX_TEXT = 10_000;
|
|
14
|
+
const RETRIES = 3;
|
|
15
|
+
const MAX_RETRY_AFTER_S = 30;
|
|
16
|
+
|
|
17
|
+
const wait = (ms: number): Promise<void> => new Promise((resolve) => setTimeout(resolve, ms));
|
|
18
|
+
|
|
19
|
+
function splitText(text: string): string[] {
|
|
20
|
+
const points = Array.from(text);
|
|
21
|
+
const chunks: string[] = [];
|
|
22
|
+
let offset = 0;
|
|
23
|
+
while (offset < points.length) {
|
|
24
|
+
let end = Math.min(points.length, offset + MAX_TEXT);
|
|
25
|
+
if (end < points.length) {
|
|
26
|
+
const block = points.slice(offset, end).join("");
|
|
27
|
+
const newline = block.lastIndexOf("\n");
|
|
28
|
+
if (newline > 0) end = offset + Array.from(block.slice(0, newline)).length;
|
|
29
|
+
}
|
|
30
|
+
chunks.push(points.slice(offset, end).join(""));
|
|
31
|
+
offset = end;
|
|
32
|
+
if (points[offset] === "\n") offset++;
|
|
33
|
+
}
|
|
34
|
+
return chunks.length ? chunks : [""];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function callSlack<T extends { ok?: boolean; error?: string }>(
|
|
38
|
+
token: string,
|
|
39
|
+
method: string,
|
|
40
|
+
body: Record<string, unknown>,
|
|
41
|
+
): Promise<T> {
|
|
42
|
+
for (let attempt = 0; ; attempt++) {
|
|
43
|
+
let response: Response;
|
|
44
|
+
let raw: string;
|
|
45
|
+
try {
|
|
46
|
+
response = await fetch(`${API}/${method}`, {
|
|
47
|
+
method: "POST",
|
|
48
|
+
headers: { authorization: `Bearer ${token}`, "content-type": "application/json; charset=utf-8" },
|
|
49
|
+
body: JSON.stringify(body),
|
|
50
|
+
signal: AbortSignal.timeout(120_000),
|
|
51
|
+
});
|
|
52
|
+
raw = await response.text();
|
|
53
|
+
} catch (error) {
|
|
54
|
+
throw new Error(`slack ${method}: ${String(error)}`, { cause: error });
|
|
55
|
+
}
|
|
56
|
+
let data: T;
|
|
57
|
+
try {
|
|
58
|
+
data = JSON.parse(raw) as T;
|
|
59
|
+
} catch {
|
|
60
|
+
data = {} as T;
|
|
61
|
+
}
|
|
62
|
+
if (response.ok && data.ok === true) return data;
|
|
63
|
+
if ((response.status === 429 || data.error === "ratelimited") && attempt < RETRIES) {
|
|
64
|
+
const retryAfter = Number(response.headers.get("retry-after") ?? attempt + 1);
|
|
65
|
+
if (Number.isFinite(retryAfter) && retryAfter <= MAX_RETRY_AFTER_S) {
|
|
66
|
+
await wait(Math.max(1, retryAfter) * 1000);
|
|
67
|
+
continue;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
throw new Error(`slack ${method} failed: ${response.status} ${data.error ?? "unexpected response"}`);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function trustedUploadUrl(value: string): URL {
|
|
75
|
+
const url = new URL(value);
|
|
76
|
+
const host = url.hostname.toLowerCase();
|
|
77
|
+
const slackHost =
|
|
78
|
+
host === "slack-files.com" ||
|
|
79
|
+
host.endsWith(".slack-files.com") ||
|
|
80
|
+
host === "slack.com" ||
|
|
81
|
+
host.endsWith(".slack.com") ||
|
|
82
|
+
host === "slack-edge.com" ||
|
|
83
|
+
host.endsWith(".slack-edge.com");
|
|
84
|
+
if (url.protocol !== "https:" || !slackHost) {
|
|
85
|
+
throw new Error(`Slack returned an untrusted upload URL host: ${host}`);
|
|
86
|
+
}
|
|
87
|
+
return url;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export default defineTool({
|
|
91
|
+
description:
|
|
92
|
+
"Upload one local file to Slack (`path`), or send a message (`text`) for a turn NO channel is " +
|
|
93
|
+
"carrying — a scheduled or self-scheduled (wake) turn. In a normal chat turn the channel already " +
|
|
94
|
+
"delivers your reply, so do NOT call this to answer (it would post the message twice). Pass exactly " +
|
|
95
|
+
"one of `text`/`path`. channelId/threadTs come from the [slack: …] context line in a chat turn; a " +
|
|
96
|
+
"scheduled/woken turn has no context line, so name the destination in your instruction.",
|
|
97
|
+
input: z.object({
|
|
98
|
+
channelId: z.string().describe("target Slack channel ID"),
|
|
99
|
+
text: z.string().optional().describe("standard Markdown message text"),
|
|
100
|
+
path: z.string().optional().describe("absolute path of a local file to upload"),
|
|
101
|
+
title: z.string().optional().describe("file title (file mode only)"),
|
|
102
|
+
initialComment: z.string().optional().describe("message posted with the file (file mode only)"),
|
|
103
|
+
threadTs: z.string().optional().describe("thread parent timestamp, if replying in a thread"),
|
|
104
|
+
}),
|
|
105
|
+
async execute({ channelId, text, path, title, initialComment, threadTs }) {
|
|
106
|
+
const token = process.env.SLACK_BOT_TOKEN;
|
|
107
|
+
if (!token) throw new Error("SLACK_BOT_TOKEN is not set");
|
|
108
|
+
if ((text === undefined) === (path === undefined)) throw new Error("pass exactly one of `text` or `path`");
|
|
109
|
+
|
|
110
|
+
if (text !== undefined) {
|
|
111
|
+
if (title !== undefined || initialComment !== undefined) {
|
|
112
|
+
throw new Error("`title`/`initialComment` are file-mode only");
|
|
113
|
+
}
|
|
114
|
+
const chunks = splitText(text);
|
|
115
|
+
for (const chunk of chunks) {
|
|
116
|
+
await callSlack(token, "chat.postMessage", {
|
|
117
|
+
channel: channelId,
|
|
118
|
+
markdown_text: chunk,
|
|
119
|
+
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
120
|
+
unfurl_links: false,
|
|
121
|
+
unfurl_media: false,
|
|
122
|
+
});
|
|
123
|
+
}
|
|
124
|
+
return chunks.length === 1
|
|
125
|
+
? `sent message to Slack channel ${channelId}`
|
|
126
|
+
: `sent ${chunks.length} messages to Slack channel ${channelId}`;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const filePath = path as string;
|
|
130
|
+
const info = await stat(filePath);
|
|
131
|
+
if (!info.isFile()) throw new Error(`${filePath} is not a regular file`);
|
|
132
|
+
const filename = basename(filePath);
|
|
133
|
+
const upload = await callSlack<{ ok?: boolean; error?: string; upload_url?: string; file_id?: string }>(
|
|
134
|
+
token,
|
|
135
|
+
"files.getUploadURLExternal",
|
|
136
|
+
{ filename, length: info.size },
|
|
137
|
+
);
|
|
138
|
+
if (!upload.upload_url || !upload.file_id) throw new Error("Slack upload URL response carried no upload_url/file_id");
|
|
139
|
+
|
|
140
|
+
let byteResponse: Response;
|
|
141
|
+
const handle = await open(filePath, "r");
|
|
142
|
+
try {
|
|
143
|
+
byteResponse = await fetch(trustedUploadUrl(upload.upload_url), {
|
|
144
|
+
method: "POST",
|
|
145
|
+
headers: { "content-type": "application/octet-stream" },
|
|
146
|
+
body: handle.readableWebStream(),
|
|
147
|
+
signal: AbortSignal.timeout(120_000),
|
|
148
|
+
// Required by Node fetch for a streaming request body; not part of the browser RequestInit type.
|
|
149
|
+
duplex: "half",
|
|
150
|
+
} as RequestInit & { duplex: "half" });
|
|
151
|
+
} catch (error) {
|
|
152
|
+
throw new Error(`Slack file byte upload failed: ${String(error)}`, { cause: error });
|
|
153
|
+
} finally {
|
|
154
|
+
await handle.close().catch(() => {});
|
|
155
|
+
}
|
|
156
|
+
if (!byteResponse.ok) {
|
|
157
|
+
throw new Error(`Slack file byte upload failed: ${byteResponse.status} ${await byteResponse.text()}`);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// At-least-once: if Slack commits this call but the response is lost, a tool retry may post a
|
|
161
|
+
// duplicate. Retrying automatically here would hide that ambiguity, so only explicit agent/user
|
|
162
|
+
// retry crosses this final side-effect boundary.
|
|
163
|
+
await callSlack(token, "files.completeUploadExternal", {
|
|
164
|
+
files: [{ id: upload.file_id, title: title ?? filename }],
|
|
165
|
+
channel_id: channelId,
|
|
166
|
+
...(initialComment ? { initial_comment: initialComment } : {}),
|
|
167
|
+
...(threadTs ? { thread_ts: threadTs } : {}),
|
|
168
|
+
});
|
|
169
|
+
return `uploaded ${filename} to Slack channel ${channelId}`;
|
|
170
|
+
},
|
|
171
|
+
});
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface SlackSetupServer {
|
|
2
|
+
port: number;
|
|
3
|
+
requestPath: string;
|
|
4
|
+
redirectPath: string;
|
|
5
|
+
waitForOAuth(timeoutMs?: number): Promise<{
|
|
6
|
+
code?: string;
|
|
7
|
+
state?: string;
|
|
8
|
+
error?: string;
|
|
9
|
+
}>;
|
|
10
|
+
close(): Promise<void>;
|
|
11
|
+
}
|
|
12
|
+
/**
|
|
13
|
+
* Temporary onboarding-only responder. Its unguessable paths live for one command: one path echoes only
|
|
14
|
+
* Slack's URL-verification challenge, while the other captures one OAuth redirect. It never accepts
|
|
15
|
+
* event callbacks or runs Agent work.
|
|
16
|
+
*/
|
|
17
|
+
export declare function startSlackSetupServer(): Promise<SlackSetupServer>;
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { createServer } from "node:http";
|
|
3
|
+
/**
|
|
4
|
+
* Temporary onboarding-only responder. Its unguessable paths live for one command: one path echoes only
|
|
5
|
+
* Slack's URL-verification challenge, while the other captures one OAuth redirect. It never accepts
|
|
6
|
+
* event callbacks or runs Agent work.
|
|
7
|
+
*/
|
|
8
|
+
export async function startSlackSetupServer() {
|
|
9
|
+
const nonce = randomBytes(24).toString("hex");
|
|
10
|
+
const requestPath = `/slack/setup/${nonce}`;
|
|
11
|
+
const redirectPath = `/slack/oauth/${nonce}`;
|
|
12
|
+
let settleOAuth;
|
|
13
|
+
const oauth = new Promise((resolve) => {
|
|
14
|
+
settleOAuth = resolve;
|
|
15
|
+
});
|
|
16
|
+
const server = createServer((request, response) => {
|
|
17
|
+
const url = new URL(request.url ?? "/", "http://127.0.0.1");
|
|
18
|
+
if (request.method === "GET" && url.pathname === "/health") {
|
|
19
|
+
response.end("ok\n");
|
|
20
|
+
return;
|
|
21
|
+
}
|
|
22
|
+
if (request.method === "GET" && url.pathname === redirectPath) {
|
|
23
|
+
settleOAuth?.({
|
|
24
|
+
code: url.searchParams.get("code") ?? undefined,
|
|
25
|
+
state: url.searchParams.get("state") ?? undefined,
|
|
26
|
+
error: url.searchParams.get("error") ?? undefined,
|
|
27
|
+
});
|
|
28
|
+
settleOAuth = undefined;
|
|
29
|
+
response.statusCode = 200;
|
|
30
|
+
response.setHeader("content-type", "text/html; charset=utf-8");
|
|
31
|
+
response.end("<!doctype html><title>FastAgent Slack setup</title><p>Slack authorization received. Return to your terminal.</p>");
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
if (request.method === "POST" && url.pathname === requestPath) {
|
|
35
|
+
let body = "";
|
|
36
|
+
let tooLarge = false;
|
|
37
|
+
request.on("data", (chunk) => {
|
|
38
|
+
if (tooLarge)
|
|
39
|
+
return;
|
|
40
|
+
body += String(chunk);
|
|
41
|
+
if (Buffer.byteLength(body) > 64 * 1024)
|
|
42
|
+
tooLarge = true;
|
|
43
|
+
});
|
|
44
|
+
request.on("end", () => {
|
|
45
|
+
if (tooLarge) {
|
|
46
|
+
response.statusCode = 413;
|
|
47
|
+
response.end("payload too large\n");
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
try {
|
|
51
|
+
const payload = JSON.parse(body);
|
|
52
|
+
if (payload.type === "url_verification" && typeof payload.challenge === "string") {
|
|
53
|
+
response.setHeader("content-type", "application/json");
|
|
54
|
+
response.end(JSON.stringify({ challenge: payload.challenge }));
|
|
55
|
+
return;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch {
|
|
59
|
+
// Fall through: setup never acknowledges normal events or malformed traffic.
|
|
60
|
+
}
|
|
61
|
+
response.statusCode = 404;
|
|
62
|
+
response.end("not found\n");
|
|
63
|
+
});
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
response.statusCode = 404;
|
|
67
|
+
response.end("not found\n");
|
|
68
|
+
});
|
|
69
|
+
await new Promise((resolve, reject) => {
|
|
70
|
+
server.once("error", reject);
|
|
71
|
+
server.listen(0, "127.0.0.1", () => {
|
|
72
|
+
server.off("error", reject);
|
|
73
|
+
resolve();
|
|
74
|
+
});
|
|
75
|
+
});
|
|
76
|
+
const address = server.address();
|
|
77
|
+
if (!address || typeof address === "string") {
|
|
78
|
+
server.close();
|
|
79
|
+
throw new Error("Slack setup server did not receive a local TCP port");
|
|
80
|
+
}
|
|
81
|
+
return {
|
|
82
|
+
port: address.port,
|
|
83
|
+
requestPath,
|
|
84
|
+
redirectPath,
|
|
85
|
+
async waitForOAuth(timeoutMs = 10 * 60_000) {
|
|
86
|
+
let timer;
|
|
87
|
+
const timeout = new Promise((_, reject) => {
|
|
88
|
+
timer = setTimeout(() => reject(new Error("Slack OAuth timed out — re-run `fastagent add slack`")), timeoutMs);
|
|
89
|
+
timer.unref();
|
|
90
|
+
});
|
|
91
|
+
try {
|
|
92
|
+
return await Promise.race([oauth, timeout]);
|
|
93
|
+
}
|
|
94
|
+
finally {
|
|
95
|
+
if (timer)
|
|
96
|
+
clearTimeout(timer);
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
close: () => new Promise((resolve, reject) => {
|
|
100
|
+
server.close((error) => (error ? reject(error) : resolve()));
|
|
101
|
+
}),
|
|
102
|
+
};
|
|
103
|
+
}
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import type { ImageRef } from "../../agent.ts";
|
|
2
|
+
import type { SlackFile } from "./model.ts";
|
|
3
|
+
export interface SlackTarget {
|
|
4
|
+
channelId: string;
|
|
5
|
+
threadTs?: string;
|
|
6
|
+
/** Required by Slack when a native stream replies in a channel rather than a DM. */
|
|
7
|
+
recipientUserId?: string;
|
|
8
|
+
recipientTeamId?: string;
|
|
9
|
+
}
|
|
10
|
+
interface SlackMarkdownTextChunk {
|
|
11
|
+
type: "markdown_text";
|
|
12
|
+
text: string;
|
|
13
|
+
}
|
|
14
|
+
interface SlackTaskUpdateChunk {
|
|
15
|
+
type: "task_update";
|
|
16
|
+
id: string;
|
|
17
|
+
title: string;
|
|
18
|
+
status: "pending" | "in_progress" | "complete" | "error";
|
|
19
|
+
}
|
|
20
|
+
export type SlackStreamChunk = SlackMarkdownTextChunk | SlackTaskUpdateChunk;
|
|
21
|
+
interface SlackStreamContent {
|
|
22
|
+
markdownText?: string;
|
|
23
|
+
chunks?: SlackStreamChunk[];
|
|
24
|
+
}
|
|
25
|
+
/** How Slack lays out task cards in a native stream (chat.startStream `task_display_mode`): `timeline`
|
|
26
|
+
* lists steps sequentially, `plan` groups them under one heading, `dense` collapses consecutive tool
|
|
27
|
+
* calls into one summarized card. */
|
|
28
|
+
export type SlackTaskDisplayMode = "timeline" | "plan" | "dense";
|
|
29
|
+
export interface DownloadedSlackFile {
|
|
30
|
+
path: string;
|
|
31
|
+
name: string;
|
|
32
|
+
size: number;
|
|
33
|
+
}
|
|
34
|
+
export declare class SlackApiError extends Error {
|
|
35
|
+
readonly method: string;
|
|
36
|
+
readonly status: number;
|
|
37
|
+
readonly slackError?: string;
|
|
38
|
+
constructor(method: string, status: number, description: string, slackError?: string, options?: {
|
|
39
|
+
cause?: unknown;
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
/** A definitive capability rejection is safe to route through the compatibility renderer. Network,
|
|
43
|
+
* internal, and timeout failures are ambiguous: Slack may already have created the stream. */
|
|
44
|
+
export declare function isSlackNativeUnavailable(error: unknown): boolean;
|
|
45
|
+
export interface SlackApiOptions {
|
|
46
|
+
botToken: string | (() => Promise<string>);
|
|
47
|
+
baseUrl?: string;
|
|
48
|
+
}
|
|
49
|
+
export interface SlackApi {
|
|
50
|
+
authTest(): Promise<{
|
|
51
|
+
teamId?: string;
|
|
52
|
+
userId?: string;
|
|
53
|
+
botId?: string;
|
|
54
|
+
}>;
|
|
55
|
+
postMessage(target: SlackTarget, text: string): Promise<string>;
|
|
56
|
+
postMarkdown(target: SlackTarget, markdown: string): Promise<string>;
|
|
57
|
+
updateMessage(channelId: string, ts: string, text: string): Promise<void>;
|
|
58
|
+
updateMarkdown(channelId: string, ts: string, markdown: string): Promise<void>;
|
|
59
|
+
deleteMessage(channelId: string, ts: string): Promise<void>;
|
|
60
|
+
sendMarkdown(target: SlackTarget, markdown: string): Promise<string | undefined>;
|
|
61
|
+
startStream(target: SlackTarget, content?: SlackStreamContent, taskDisplayMode?: SlackTaskDisplayMode): Promise<string>;
|
|
62
|
+
appendStream(channelId: string, ts: string, content: SlackStreamContent): Promise<void>;
|
|
63
|
+
stopStream(channelId: string, ts: string, content?: SlackStreamContent): Promise<void>;
|
|
64
|
+
setThreadStatus(target: SlackTarget, status: string): Promise<void>;
|
|
65
|
+
setThreadTitle(target: SlackTarget, title: string): Promise<void>;
|
|
66
|
+
addReaction(channelId: string, timestamp: string, emoji: string): Promise<void>;
|
|
67
|
+
removeReaction(channelId: string, timestamp: string, emoji: string): Promise<void>;
|
|
68
|
+
fileInfo(fileId: string): Promise<SlackFile>;
|
|
69
|
+
fetchImage(file: SlackFile): Promise<ImageRef>;
|
|
70
|
+
fetchFile(file: SlackFile, channelId: string, filesDir: string): Promise<DownloadedSlackFile>;
|
|
71
|
+
}
|
|
72
|
+
/** Split a Slack text/Markdown field at a code-point-safe boundary, preferring a newline. */
|
|
73
|
+
export declare function chunkSlackText(text: string, maxPoints?: number): string[];
|
|
74
|
+
/** Split standard Markdown while balancing fenced code blocks across separately posted messages. */
|
|
75
|
+
export declare function chunkSlackMarkdown(markdown: string, maxPoints?: number): string[];
|
|
76
|
+
export declare function createSlackApi({ botToken, baseUrl }: SlackApiOptions): SlackApi;
|
|
77
|
+
export {};
|