agents 0.22.0 → 0.23.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 +20 -1
- package/dist/{agent-routing-CnkaHb-v.d.ts → agent-routing-DE5zmCQ8.d.ts} +1038 -394
- package/dist/agent-routing.d.ts +1 -1
- package/dist/agent-tool-types.d.ts +26 -26
- package/dist/{agent-tools-C0XJqWzB.d.ts → agent-tools-DtXMTDGM.d.ts} +7 -7
- package/dist/agent-tools.d.ts +21 -21
- package/dist/browser/ai.js +4 -1
- package/dist/browser/ai.js.map +1 -1
- package/dist/browser/tanstack-ai.js.map +1 -1
- package/dist/callable-decorator-DP__HhBA.d.ts +72 -0
- package/dist/callable-decorator.d.ts +18 -0
- package/dist/callable-decorator.js +71 -0
- package/dist/callable-decorator.js.map +1 -0
- package/dist/capability-BjSKYpzg.js.map +1 -1
- package/dist/capability-runner-Be_-PLR1.d.ts +459 -0
- package/dist/channel-Bnm4S7T2.d.ts +491 -0
- package/dist/channels/ai-sdk.d.ts +46 -0
- package/dist/channels/ai-sdk.js +120 -0
- package/dist/channels/ai-sdk.js.map +1 -0
- package/dist/channels/email.d.ts +95 -0
- package/dist/channels/email.js +323 -0
- package/dist/channels/email.js.map +1 -0
- package/dist/channels/index.d.ts +233 -0
- package/dist/channels/index.js +608 -0
- package/dist/channels/index.js.map +1 -0
- package/dist/channels/slack.d.ts +140 -0
- package/dist/channels/slack.js +614 -0
- package/dist/channels/slack.js.map +1 -0
- package/dist/channels/tanstack-ai.d.ts +39 -0
- package/dist/channels/tanstack-ai.js +17 -0
- package/dist/channels/tanstack-ai.js.map +1 -0
- package/dist/channels/telegram.d.ts +106 -0
- package/dist/channels/telegram.js +427 -0
- package/dist/channels/telegram.js.map +1 -0
- package/dist/channels/voice.d.ts +45 -0
- package/dist/channels/voice.js +122 -0
- package/dist/channels/voice.js.map +1 -0
- package/dist/chat/index.d.ts +2325 -2019
- package/dist/chat/index.js +888 -518
- package/dist/chat/index.js.map +1 -1
- package/dist/chat-sdk/index.d.ts +7 -7
- package/dist/chat-sdk/index.js +1 -1
- package/dist/client.d.ts +1 -1
- package/dist/context/index.d.ts +216 -0
- package/dist/context/index.js +454 -0
- package/dist/context/index.js.map +1 -0
- package/dist/{current-agent-CuMErtly.d.ts → current-agent-Da_C9a3b.d.ts} +90 -107
- package/dist/current-agent-DhoDkSnH.js.map +1 -1
- package/dist/{diagnostics-CaBjfz4J.js → diagnostics-BzvaX2UT.js} +5 -1
- package/dist/diagnostics-BzvaX2UT.js.map +1 -0
- package/dist/diagnostics-C4jcz3VK.js +360 -0
- package/dist/diagnostics-C4jcz3VK.js.map +1 -0
- package/dist/index-BB0kqhIz.d.ts +101 -0
- package/dist/index-BVVgDSdq.d.ts +1 -0
- package/dist/index-XDkuQ7zm.d.ts +89 -0
- package/dist/{index-DcSAZKsB.d.ts → index-YSKgfgg9.d.ts} +3 -1
- package/dist/index.d.ts +91 -82
- package/dist/index.js +3 -2
- package/dist/ingress-BfetZbMO.js +83 -0
- package/dist/ingress-BfetZbMO.js.map +1 -0
- package/dist/internal-CYlgHl1l.js +59 -0
- package/dist/internal-CYlgHl1l.js.map +1 -0
- package/dist/internal_context.d.ts +1 -1
- package/dist/lifecycle/index.d.ts +35 -17
- package/dist/lifecycle/index.js +1 -1
- package/dist/lifecycle-CMRGjZdw.js +1299 -0
- package/dist/lifecycle-CMRGjZdw.js.map +1 -0
- package/dist/mcp/client/index.d.ts +20 -20
- package/dist/mcp/index.d.ts +35 -35
- package/dist/mcp/index.js +1 -1
- package/dist/observability/index.d.ts +1 -1
- package/dist/observability/index.js +1 -1
- package/dist/react.d.ts +4 -4
- package/dist/{retries-CAvxtG9d.d.ts → retries-D9Ds-1lz.d.ts} +17 -6
- package/dist/retries.d.ts +8 -6
- package/dist/retries.js +13 -1
- package/dist/retries.js.map +1 -1
- package/dist/routing/index.d.ts +137 -0
- package/dist/routing/index.js +244 -0
- package/dist/routing/index.js.map +1 -0
- package/dist/sanitize-D9TujEK8.js +79 -0
- package/dist/sanitize-D9TujEK8.js.map +1 -0
- package/dist/scheduler-DD9NdYbF.js +665 -0
- package/dist/scheduler-DD9NdYbF.js.map +1 -0
- package/dist/{scheduler-DQoTGoAW.d.ts → scheduler-Dwh85ZGl.d.ts} +21 -22
- package/dist/schedules/index.d.ts +1 -1
- package/dist/schedules/index.js +1 -1
- package/dist/sentence-chunker-BAidJ4DA.d.ts +68 -0
- package/dist/serializable.d.ts +1 -1
- package/dist/sessions/index.d.ts +441 -0
- package/dist/sessions/index.js +2063 -0
- package/dist/sessions/index.js.map +1 -0
- package/dist/skills/index.d.ts +99 -0
- package/dist/skills/index.js +254 -5
- package/dist/skills/index.js.map +1 -1
- package/dist/{src-5W6JNKVb.js → src-DlSHshb2.js} +1460 -1110
- package/dist/src-DlSHshb2.js.map +1 -0
- package/dist/streams/index.d.ts +120 -0
- package/dist/streams/index.js +107 -0
- package/dist/streams/index.js.map +1 -0
- package/dist/streams-D6tJ0NN9.d.ts +370 -0
- package/dist/streams-DZKgAj9b.js +709 -0
- package/dist/streams-DZKgAj9b.js.map +1 -0
- package/dist/sub-routing.d.ts +12 -12
- package/dist/surface-bZZJqBka.js +17 -0
- package/dist/surface-bZZJqBka.js.map +1 -0
- package/dist/tasks/index.d.ts +64 -0
- package/dist/tasks/index.js +2 -0
- package/dist/tasks-BRJ5zgya.d.ts +517 -0
- package/dist/tasks-ylZgBjhj.js +1656 -0
- package/dist/tasks-ylZgBjhj.js.map +1 -0
- package/dist/text-segment-joiner-BtAFQSA_.js +57 -0
- package/dist/text-segment-joiner-BtAFQSA_.js.map +1 -0
- package/dist/text-stream-CpdiKrJB.js +272 -0
- package/dist/text-stream-CpdiKrJB.js.map +1 -0
- package/dist/tokens-nHAKcN6M.js +52 -0
- package/dist/tokens-nHAKcN6M.js.map +1 -0
- package/dist/tool-schema-CBjGPrsQ.js +31 -0
- package/dist/tool-schema-CBjGPrsQ.js.map +1 -0
- package/dist/types-B7LojTe4.d.ts +202 -0
- package/dist/types-_Faxb570.d.ts +439 -0
- package/dist/voice/client.d.ts +226 -0
- package/dist/voice/client.js +932 -0
- package/dist/voice/client.js.map +1 -0
- package/dist/voice/errors.d.ts +43 -0
- package/dist/voice/errors.js +41 -0
- package/dist/voice/errors.js.map +1 -0
- package/dist/voice/index.d.ts +271 -0
- package/dist/voice/index.js +1812 -0
- package/dist/voice/index.js.map +1 -0
- package/dist/voice/react.d.ts +167 -0
- package/dist/voice/react.js +234 -0
- package/dist/voice/react.js.map +1 -0
- package/dist/voice/sfu.d.ts +71 -0
- package/dist/voice/sfu.js +157 -0
- package/dist/voice/sfu.js.map +1 -0
- package/dist/voice/text.d.ts +6 -0
- package/dist/voice/text.js +2 -0
- package/dist/voice/types.d.ts +58 -0
- package/dist/voice/types.js +18 -0
- package/dist/voice/types.js.map +1 -0
- package/dist/voice/workers-ai.d.ts +136 -0
- package/dist/voice/workers-ai.js +568 -0
- package/dist/voice/workers-ai.js.map +1 -0
- package/dist/websockets/index.d.ts +192 -0
- package/dist/websockets/index.js +2 -0
- package/dist/websockets-DUfRHPRq.js +502 -0
- package/dist/websockets-DUfRHPRq.js.map +1 -0
- package/dist/workflow-types.d.ts +25 -25
- package/dist/workflows.d.ts +21 -21
- package/dist/workflows.js +1 -1
- package/docs/agent-class.md +2 -2
- package/docs/agent-tools.md +2 -1
- package/docs/channels.md +323 -0
- package/docs/chat-agents.md +6 -13
- package/docs/context.md +131 -0
- package/docs/index.md +15 -12
- package/docs/lifecycle.md +102 -55
- package/docs/long-running-agents.md +2 -2
- package/docs/mcp-servers.md +5 -1
- package/docs/resumable-streaming.md +1 -1
- package/docs/routing.md +105 -0
- package/docs/sessions.md +237 -871
- package/docs/streams.md +213 -0
- package/docs/sub-agents.md +184 -124
- package/docs/tasks.md +246 -0
- package/docs/voice.md +745 -0
- package/package.json +115 -13
- package/dist/capability-runner-CvHGZqUu.d.ts +0 -150
- package/dist/compaction-helpers-iiKMr2TQ.js +0 -340
- package/dist/compaction-helpers-iiKMr2TQ.js.map +0 -1
- package/dist/compaction-helpers-wUz6M3us.d.ts +0 -621
- package/dist/diagnostics-CaBjfz4J.js.map +0 -1
- package/dist/durable-object-lifecycle-D6nNQJJd.js +0 -862
- package/dist/durable-object-lifecycle-D6nNQJJd.js.map +0 -1
- package/dist/experimental/memory/session/index.d.ts +0 -671
- package/dist/experimental/memory/session/index.js +0 -2379
- package/dist/experimental/memory/session/index.js.map +0 -1
- package/dist/experimental/memory/utils/index.d.ts +0 -96
- package/dist/experimental/memory/utils/index.js +0 -79
- package/dist/experimental/memory/utils/index.js.map +0 -1
- package/dist/scheduler-CR9RHGos.js +0 -857
- package/dist/scheduler-CR9RHGos.js.map +0 -1
- package/dist/src-5W6JNKVb.js.map +0 -1
- package/dist/tool-output-truncation-CNnnGZQ3.js +0 -98
- package/dist/tool-output-truncation-CNnnGZQ3.js.map +0 -1
|
@@ -0,0 +1,614 @@
|
|
|
1
|
+
import { t as isChannelMessageSurface } from "../surface-bZZJqBka.js";
|
|
2
|
+
import { a as isRecord, i as encodeUtf8, n as defaultText, o as renderInput, r as emptyIngressResponse, s as uncertain } from "../internal-CYlgHl1l.js";
|
|
3
|
+
import { i as createPacer, n as collectText, r as consumeChunks, t as matchesPath } from "../ingress-BfetZbMO.js";
|
|
4
|
+
//#region src/channels/adapters/slack.ts
|
|
5
|
+
const DEFAULT_SLACK_WEBHOOK_PATH = "/webhooks/slack";
|
|
6
|
+
const DEFAULT_MAX_SKEW_SECONDS = 300;
|
|
7
|
+
const APPROVE_ACTION_ID = "cloudflare_channels_approve_v1";
|
|
8
|
+
const REJECT_ACTION_ID = "cloudflare_channels_reject_v1";
|
|
9
|
+
const DEFAULT_STREAM_INTERVAL_MS = 500;
|
|
10
|
+
const SLACK_APPEND_LIMIT = 12e3;
|
|
11
|
+
const SLACK_TASK_FIELD_LIMIT = 256;
|
|
12
|
+
const SLACK_CONTEXT_LIMIT = 3e3;
|
|
13
|
+
const AMBIGUOUS_SLACK_ERRORS = /* @__PURE__ */ new Set([
|
|
14
|
+
"fatal_error",
|
|
15
|
+
"internal_error",
|
|
16
|
+
"request_timeout",
|
|
17
|
+
"service_unavailable"
|
|
18
|
+
]);
|
|
19
|
+
function approvalText(request) {
|
|
20
|
+
return [
|
|
21
|
+
...request.title ? [request.title] : [],
|
|
22
|
+
request.summary,
|
|
23
|
+
`Input:\n\`\`\`\n${renderInput(request.input)}\n\`\`\``
|
|
24
|
+
].join("\n\n");
|
|
25
|
+
}
|
|
26
|
+
function channelIdentity(teamId, channelId) {
|
|
27
|
+
return `slack:${teamId}:channel:${channelId}`;
|
|
28
|
+
}
|
|
29
|
+
function messageIdentity(teamId, channelId, timestamp) {
|
|
30
|
+
return `${channelIdentity(teamId, channelId)}:message:${timestamp}`;
|
|
31
|
+
}
|
|
32
|
+
function outboundReference(channelId, timestamp) {
|
|
33
|
+
return `slack:channel:${channelId}:message:${timestamp}`;
|
|
34
|
+
}
|
|
35
|
+
function replySurfaceLabel(channelId, threadTimestamp) {
|
|
36
|
+
return threadTimestamp ? `Slack · ${channelId} · thread ${threadTimestamp}` : `Slack · ${channelId}`;
|
|
37
|
+
}
|
|
38
|
+
function threadIdentity(teamId, channelId, timestamp, threadTimestamp, isDirectMessage) {
|
|
39
|
+
const channel = channelIdentity(teamId, channelId);
|
|
40
|
+
if (isDirectMessage && threadTimestamp === void 0) return channel;
|
|
41
|
+
return `${channel}:thread:${threadTimestamp ?? timestamp}`;
|
|
42
|
+
}
|
|
43
|
+
function slackTimestamp(timestamp) {
|
|
44
|
+
const milliseconds = Number(timestamp) * 1e3;
|
|
45
|
+
if (!Number.isFinite(milliseconds)) return void 0;
|
|
46
|
+
try {
|
|
47
|
+
return new Date(milliseconds).toISOString();
|
|
48
|
+
} catch {
|
|
49
|
+
return;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
function selfUserIds(payload, configuredBotUserId) {
|
|
53
|
+
const ids = /* @__PURE__ */ new Set();
|
|
54
|
+
if (configuredBotUserId) ids.add(configuredBotUserId);
|
|
55
|
+
if (Array.isArray(payload.authorizations)) {
|
|
56
|
+
for (const authorization of payload.authorizations) if (authorization?.is_bot === true && typeof authorization.user_id === "string") ids.add(authorization.user_id);
|
|
57
|
+
}
|
|
58
|
+
return ids;
|
|
59
|
+
}
|
|
60
|
+
function normalizedEvent(payload, configuredBotUserId) {
|
|
61
|
+
const event = payload.event;
|
|
62
|
+
if (!event || !isRecord(event)) return void 0;
|
|
63
|
+
if (event.type !== "app_mention" && event.type !== "message") return;
|
|
64
|
+
const isMention = event.type === "app_mention";
|
|
65
|
+
const isDirectMessage = event.type === "message" && event.channel_type === "im";
|
|
66
|
+
if (event.subtype !== void 0 || typeof event.bot_id === "string" || typeof event.app_id === "string") return;
|
|
67
|
+
const teamId = payload.team_id;
|
|
68
|
+
const eventId = payload.event_id;
|
|
69
|
+
const channelId = event.channel;
|
|
70
|
+
const actorId = event.user;
|
|
71
|
+
const text = event.text;
|
|
72
|
+
const timestamp = event.ts;
|
|
73
|
+
if (typeof teamId !== "string" || typeof eventId !== "string" || typeof channelId !== "string" || typeof actorId !== "string" || typeof text !== "string" || typeof timestamp !== "string") return;
|
|
74
|
+
if (selfUserIds(payload, configuredBotUserId).has(actorId)) return;
|
|
75
|
+
const threadTimestamp = typeof event.thread_ts === "string" ? event.thread_ts : void 0;
|
|
76
|
+
const sentAt = slackTimestamp(timestamp);
|
|
77
|
+
return {
|
|
78
|
+
type: "message",
|
|
79
|
+
eventId: `slack:${teamId}:event:${eventId}`,
|
|
80
|
+
thread: {
|
|
81
|
+
id: threadIdentity(teamId, channelId, timestamp, threadTimestamp, isDirectMessage),
|
|
82
|
+
isDirectMessage
|
|
83
|
+
},
|
|
84
|
+
replySurface: {
|
|
85
|
+
version: 1,
|
|
86
|
+
address: {
|
|
87
|
+
teamId,
|
|
88
|
+
channelId,
|
|
89
|
+
...(threadTimestamp || !isDirectMessage) && { threadTs: threadTimestamp ?? timestamp },
|
|
90
|
+
recipientUserId: actorId,
|
|
91
|
+
recipientTeamId: teamId
|
|
92
|
+
},
|
|
93
|
+
label: replySurfaceLabel(channelId, threadTimestamp ?? (isDirectMessage ? void 0 : timestamp))
|
|
94
|
+
},
|
|
95
|
+
actor: {
|
|
96
|
+
id: `slack:${teamId}:user:${actorId}`,
|
|
97
|
+
identity: {
|
|
98
|
+
scope: teamId,
|
|
99
|
+
subject: actorId
|
|
100
|
+
},
|
|
101
|
+
isBot: false,
|
|
102
|
+
isSelf: false
|
|
103
|
+
},
|
|
104
|
+
message: {
|
|
105
|
+
id: messageIdentity(teamId, channelId, timestamp),
|
|
106
|
+
text,
|
|
107
|
+
markdown: text,
|
|
108
|
+
...isMention && { isMention: true },
|
|
109
|
+
...threadTimestamp && threadTimestamp !== timestamp && { reply: { id: messageIdentity(teamId, channelId, threadTimestamp) } },
|
|
110
|
+
...sentAt && { metadata: { sentAt } }
|
|
111
|
+
}
|
|
112
|
+
};
|
|
113
|
+
}
|
|
114
|
+
function parseApprovalValue(value) {
|
|
115
|
+
let parsed;
|
|
116
|
+
try {
|
|
117
|
+
parsed = JSON.parse(value);
|
|
118
|
+
} catch {
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
if (!isRecord(parsed) || parsed.v !== 1 || typeof parsed.interactionId !== "string" || !parsed.interactionId || parsed.decision !== "approve" && parsed.decision !== "reject") return;
|
|
122
|
+
return parsed;
|
|
123
|
+
}
|
|
124
|
+
async function sha256Hex(value) {
|
|
125
|
+
const digest = await crypto.subtle.digest("SHA-256", encodeUtf8(value));
|
|
126
|
+
return Array.from(new Uint8Array(digest), (byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
127
|
+
}
|
|
128
|
+
async function normalizedInteractions(payload, rawBody) {
|
|
129
|
+
const teamId = payload.team?.id;
|
|
130
|
+
const actorId = payload.user?.id;
|
|
131
|
+
const channelId = typeof payload.channel?.id === "string" ? payload.channel.id : payload.container?.channel_id;
|
|
132
|
+
const timestamp = typeof payload.message?.ts === "string" ? payload.message.ts : payload.container?.message_ts;
|
|
133
|
+
if (typeof teamId !== "string" || typeof actorId !== "string" || typeof channelId !== "string" || typeof timestamp !== "string" || !Array.isArray(payload.actions)) return [];
|
|
134
|
+
const threadTimestamp = typeof payload.message?.thread_ts === "string" ? payload.message.thread_ts : typeof payload.container?.thread_ts === "string" ? payload.container.thread_ts : void 0;
|
|
135
|
+
const isDirectMessage = channelId.startsWith("D");
|
|
136
|
+
const bodyHash = await sha256Hex(rawBody);
|
|
137
|
+
const events = [];
|
|
138
|
+
for (const [index, action] of payload.actions.entries()) {
|
|
139
|
+
if (!action || typeof action.value !== "string") continue;
|
|
140
|
+
const expectedDecision = action.action_id === APPROVE_ACTION_ID ? "approve" : action.action_id === REJECT_ACTION_ID ? "reject" : void 0;
|
|
141
|
+
if (!expectedDecision) continue;
|
|
142
|
+
const approval = parseApprovalValue(action.value);
|
|
143
|
+
if (!approval || approval.decision !== expectedDecision) continue;
|
|
144
|
+
const stableEventId = `slack:${teamId}:interaction:sha256:${bodyHash}:action:${index}`;
|
|
145
|
+
const actionTimestamp = typeof action.action_ts === "string" ? action.action_ts : void 0;
|
|
146
|
+
events.push({
|
|
147
|
+
type: "approval-response",
|
|
148
|
+
eventId: stableEventId,
|
|
149
|
+
thread: {
|
|
150
|
+
id: threadIdentity(teamId, channelId, timestamp, threadTimestamp, isDirectMessage),
|
|
151
|
+
isDirectMessage
|
|
152
|
+
},
|
|
153
|
+
replySurface: {
|
|
154
|
+
version: 1,
|
|
155
|
+
address: {
|
|
156
|
+
teamId,
|
|
157
|
+
channelId,
|
|
158
|
+
threadTs: threadTimestamp ?? timestamp,
|
|
159
|
+
...!isDirectMessage && {
|
|
160
|
+
recipientUserId: actorId,
|
|
161
|
+
recipientTeamId: teamId
|
|
162
|
+
}
|
|
163
|
+
},
|
|
164
|
+
label: replySurfaceLabel(channelId, threadTimestamp ?? timestamp)
|
|
165
|
+
},
|
|
166
|
+
actor: {
|
|
167
|
+
id: `slack:${teamId}:user:${actorId}`,
|
|
168
|
+
identity: {
|
|
169
|
+
scope: teamId,
|
|
170
|
+
subject: actorId
|
|
171
|
+
},
|
|
172
|
+
...typeof payload.user?.username === "string" && { username: payload.user.username },
|
|
173
|
+
isBot: false
|
|
174
|
+
},
|
|
175
|
+
interactionId: approval.interactionId,
|
|
176
|
+
decision: approval.decision,
|
|
177
|
+
reference: actionTimestamp ? `${channelIdentity(teamId, channelId)}:action:${actionTimestamp}` : stableEventId
|
|
178
|
+
});
|
|
179
|
+
}
|
|
180
|
+
return events;
|
|
181
|
+
}
|
|
182
|
+
function bytesFromHex(hex) {
|
|
183
|
+
if (!/^[a-f0-9]{64}$/.test(hex)) return void 0;
|
|
184
|
+
const bytes = new Uint8Array(hex.length / 2);
|
|
185
|
+
for (let index = 0; index < bytes.length; index += 1) bytes[index] = Number.parseInt(hex.slice(index * 2, index * 2 + 2), 16);
|
|
186
|
+
return bytes;
|
|
187
|
+
}
|
|
188
|
+
async function hasValidSignature(request, rawBody, signingSecret, maxSkewSeconds) {
|
|
189
|
+
const timestamp = request.headers.get("x-slack-request-timestamp");
|
|
190
|
+
const signature = request.headers.get("x-slack-signature");
|
|
191
|
+
if (!timestamp || !signature || !/^\d+$/.test(timestamp)) return false;
|
|
192
|
+
const timestampSeconds = Number(timestamp);
|
|
193
|
+
if (!Number.isSafeInteger(timestampSeconds) || Math.abs(Math.floor(Date.now() / 1e3) - timestampSeconds) > maxSkewSeconds) return false;
|
|
194
|
+
const match = signature.match(/^v0=([a-f0-9]{64})$/);
|
|
195
|
+
const signatureBytes = match ? bytesFromHex(match[1]) : void 0;
|
|
196
|
+
if (!signatureBytes) return false;
|
|
197
|
+
const key = await crypto.subtle.importKey("raw", encodeUtf8(signingSecret), {
|
|
198
|
+
name: "HMAC",
|
|
199
|
+
hash: "SHA-256"
|
|
200
|
+
}, false, ["verify"]);
|
|
201
|
+
return crypto.subtle.verify("HMAC", key, signatureBytes, encodeUtf8(`v0:${timestamp}:${rawBody}`));
|
|
202
|
+
}
|
|
203
|
+
function parseJsonPayload(rawBody) {
|
|
204
|
+
let parsed;
|
|
205
|
+
try {
|
|
206
|
+
parsed = JSON.parse(rawBody);
|
|
207
|
+
} catch {
|
|
208
|
+
return;
|
|
209
|
+
}
|
|
210
|
+
if (!isRecord(parsed)) return void 0;
|
|
211
|
+
if (parsed.type !== "url_verification" && parsed.type !== "event_callback") return;
|
|
212
|
+
return parsed;
|
|
213
|
+
}
|
|
214
|
+
function parseInteractionPayload(rawBody) {
|
|
215
|
+
const encodedPayload = new URLSearchParams(rawBody).get("payload");
|
|
216
|
+
if (!encodedPayload) return void 0;
|
|
217
|
+
let parsed;
|
|
218
|
+
try {
|
|
219
|
+
parsed = JSON.parse(encodedPayload);
|
|
220
|
+
} catch {
|
|
221
|
+
return;
|
|
222
|
+
}
|
|
223
|
+
return isRecord(parsed) && parsed.type === "block_actions" ? parsed : void 0;
|
|
224
|
+
}
|
|
225
|
+
/** Create dependency-free, request-signed Slack HTTP ingress. */
|
|
226
|
+
function slackWebhook(options) {
|
|
227
|
+
if (!options.signingSecret.trim()) throw new Error("signingSecret is required to create Slack webhook ingress");
|
|
228
|
+
const maxSkewSeconds = options.maxSkewSeconds ?? DEFAULT_MAX_SKEW_SECONDS;
|
|
229
|
+
if (!Number.isInteger(maxSkewSeconds) || maxSkewSeconds < 0) throw new Error("maxSkewSeconds must be a non-negative integer");
|
|
230
|
+
const path = options.path ?? DEFAULT_SLACK_WEBHOOK_PATH;
|
|
231
|
+
return { async receive(request) {
|
|
232
|
+
if (!matchesPath(request, path)) return null;
|
|
233
|
+
if (request.method !== "POST") return emptyIngressResponse(405);
|
|
234
|
+
const rawBody = await request.text();
|
|
235
|
+
if (!await hasValidSignature(request, rawBody, options.signingSecret, maxSkewSeconds)) return emptyIngressResponse(401);
|
|
236
|
+
if ((request.headers.get("content-type")?.split(";", 1)[0]?.trim() ?? "") === "application/x-www-form-urlencoded") {
|
|
237
|
+
const payload = parseInteractionPayload(rawBody);
|
|
238
|
+
if (!payload) return emptyIngressResponse(400);
|
|
239
|
+
return {
|
|
240
|
+
events: (await normalizedInteractions(payload, rawBody)).map((event) => ({
|
|
241
|
+
event,
|
|
242
|
+
raw: payload
|
|
243
|
+
})),
|
|
244
|
+
response: new Response(null, { status: 200 })
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
const payload = parseJsonPayload(rawBody);
|
|
248
|
+
if (!payload) return emptyIngressResponse(400);
|
|
249
|
+
if (payload.type === "url_verification") return typeof payload.challenge === "string" ? {
|
|
250
|
+
events: [],
|
|
251
|
+
response: Response.json({ challenge: payload.challenge })
|
|
252
|
+
} : emptyIngressResponse(400);
|
|
253
|
+
const event = normalizedEvent(payload, options.botUserId);
|
|
254
|
+
return {
|
|
255
|
+
events: event ? [{
|
|
256
|
+
event,
|
|
257
|
+
raw: payload
|
|
258
|
+
}] : [],
|
|
259
|
+
response: new Response(null, { status: 200 })
|
|
260
|
+
};
|
|
261
|
+
} };
|
|
262
|
+
}
|
|
263
|
+
function failed(code, message, retryable) {
|
|
264
|
+
return {
|
|
265
|
+
status: "failed",
|
|
266
|
+
retryable,
|
|
267
|
+
error: {
|
|
268
|
+
code,
|
|
269
|
+
message
|
|
270
|
+
}
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
function slackErrorCode(error) {
|
|
274
|
+
return `SLACK_API_ERROR_${error.toUpperCase().replace(/[^A-Z0-9]+/g, "_")}`;
|
|
275
|
+
}
|
|
276
|
+
function classifyApiFailure(response, payload) {
|
|
277
|
+
if (payload.ok === false) {
|
|
278
|
+
const error = typeof payload.error === "string" ? payload.error : "unknown_error";
|
|
279
|
+
const code = slackErrorCode(error);
|
|
280
|
+
const message = `Slack rejected the message: ${error}`;
|
|
281
|
+
if (response.status >= 500 || AMBIGUOUS_SLACK_ERRORS.has(error)) return uncertain(code, message);
|
|
282
|
+
return failed(code, message, response.status === 429 || error === "ratelimited");
|
|
283
|
+
}
|
|
284
|
+
if (response.status === 429) return failed("SLACK_HTTP_ERROR_429", "Slack rate limited the message", true);
|
|
285
|
+
if (response.status >= 400 && response.status < 500) return failed(`SLACK_HTTP_ERROR_${response.status}`, `Slack rejected the message with HTTP ${response.status}`, false);
|
|
286
|
+
return uncertain("SLACK_DELIVERY_ERROR", "Slack returned an invalid delivery response");
|
|
287
|
+
}
|
|
288
|
+
function asApiResponse(value) {
|
|
289
|
+
return isRecord(value) ? value : void 0;
|
|
290
|
+
}
|
|
291
|
+
function approvalValue(interactionId, decision) {
|
|
292
|
+
return JSON.stringify({
|
|
293
|
+
v: 1,
|
|
294
|
+
interactionId,
|
|
295
|
+
decision
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
function optionalId(value) {
|
|
299
|
+
return typeof value === "string" && value.length > 0 ? value : void 0;
|
|
300
|
+
}
|
|
301
|
+
function slackTarget(surface) {
|
|
302
|
+
if (!isChannelMessageSurface(surface) || surface.version !== 1 || !isRecord(surface.address)) return;
|
|
303
|
+
const address = surface.address;
|
|
304
|
+
if ("userId" in address) return typeof address.teamId === "string" && address.teamId.length > 0 && typeof address.userId === "string" && address.userId.length > 0 ? {
|
|
305
|
+
teamId: address.teamId,
|
|
306
|
+
userId: address.userId
|
|
307
|
+
} : void 0;
|
|
308
|
+
if (typeof address.channelId !== "string" || address.channelId.length === 0) return;
|
|
309
|
+
if ("threadTs" in address && address.threadTs !== void 0 && (typeof address.threadTs !== "string" || address.threadTs.length === 0)) return;
|
|
310
|
+
const recipientUserId = optionalId(address.recipientUserId);
|
|
311
|
+
const recipientTeamId = optionalId(address.recipientTeamId);
|
|
312
|
+
return {
|
|
313
|
+
channelId: address.channelId,
|
|
314
|
+
...typeof address.threadTs === "string" && { threadTs: address.threadTs },
|
|
315
|
+
...recipientUserId && recipientTeamId && {
|
|
316
|
+
recipientUserId,
|
|
317
|
+
recipientTeamId
|
|
318
|
+
}
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
const SLACK_TASK_STATUS = {
|
|
322
|
+
started: "in_progress",
|
|
323
|
+
completed: "complete",
|
|
324
|
+
failed: "error"
|
|
325
|
+
};
|
|
326
|
+
function splitText(text, limit) {
|
|
327
|
+
const pieces = [];
|
|
328
|
+
let piece = "";
|
|
329
|
+
let length = 0;
|
|
330
|
+
for (const character of text) {
|
|
331
|
+
if (length === limit) {
|
|
332
|
+
pieces.push(piece);
|
|
333
|
+
piece = "";
|
|
334
|
+
length = 0;
|
|
335
|
+
}
|
|
336
|
+
piece += character;
|
|
337
|
+
length += 1;
|
|
338
|
+
}
|
|
339
|
+
if (piece.length > 0) pieces.push(piece);
|
|
340
|
+
return pieces;
|
|
341
|
+
}
|
|
342
|
+
function clamp(value, limit) {
|
|
343
|
+
const characters = [...value];
|
|
344
|
+
return characters.length <= limit ? value : `${characters.slice(0, limit - 1).join("")}\u2026`;
|
|
345
|
+
}
|
|
346
|
+
function escapeMrkdwn(value) {
|
|
347
|
+
return value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">");
|
|
348
|
+
}
|
|
349
|
+
function escapeLinkUrl(value) {
|
|
350
|
+
return escapeMrkdwn(value.replaceAll("|", "%7C"));
|
|
351
|
+
}
|
|
352
|
+
function escapeLinkLabel(value) {
|
|
353
|
+
return escapeMrkdwn(value).replaceAll("|", "|");
|
|
354
|
+
}
|
|
355
|
+
/**
|
|
356
|
+
* Render a neutral chunk as Slack stream content.
|
|
357
|
+
*
|
|
358
|
+
* `reasoning` has no Slack rendering and is dropped. A `source` is collected
|
|
359
|
+
* rather than appended, so it can be rendered once beneath the finished
|
|
360
|
+
* message instead of interrupting the text.
|
|
361
|
+
*/
|
|
362
|
+
function toStreamChunks(chunk, sources) {
|
|
363
|
+
switch (chunk.type) {
|
|
364
|
+
case "text": return splitText(chunk.text, SLACK_APPEND_LIMIT).map((text) => ({
|
|
365
|
+
type: "markdown_text",
|
|
366
|
+
text
|
|
367
|
+
}));
|
|
368
|
+
case "tool": return [{
|
|
369
|
+
type: "task_update",
|
|
370
|
+
id: clamp(chunk.id ?? chunk.name, SLACK_TASK_FIELD_LIMIT),
|
|
371
|
+
title: clamp(chunk.title ?? chunk.name, SLACK_TASK_FIELD_LIMIT),
|
|
372
|
+
status: SLACK_TASK_STATUS[chunk.status],
|
|
373
|
+
...chunk.detail !== void 0 && { details: clamp(chunk.detail, SLACK_TASK_FIELD_LIMIT) }
|
|
374
|
+
}];
|
|
375
|
+
case "source":
|
|
376
|
+
sources.push(chunk);
|
|
377
|
+
return [];
|
|
378
|
+
case "reasoning": return [];
|
|
379
|
+
}
|
|
380
|
+
}
|
|
381
|
+
function sourcesBlock(sources) {
|
|
382
|
+
return {
|
|
383
|
+
type: "context",
|
|
384
|
+
elements: [{
|
|
385
|
+
type: "mrkdwn",
|
|
386
|
+
text: clamp(sources.map((source) => `<${escapeLinkUrl(source.url)}|${escapeLinkLabel(source.title ?? source.url)}>`).join(" · "), SLACK_CONTEXT_LIMIT)
|
|
387
|
+
}]
|
|
388
|
+
};
|
|
389
|
+
}
|
|
390
|
+
/** Create a configured Slack Web API Channel. */
|
|
391
|
+
function slack(options) {
|
|
392
|
+
if (!options.botToken.trim()) throw new Error("botToken is required to create a Slack channel");
|
|
393
|
+
const fetch = options.fetch ?? globalThis.fetch;
|
|
394
|
+
const apiBaseUrl = (options.apiBaseUrl ?? "https://slack.com/api").replace(/\/$/, "");
|
|
395
|
+
const toText = options.toText ?? defaultText;
|
|
396
|
+
const streamIntervalMs = options.streamIntervalMs ?? DEFAULT_STREAM_INTERVAL_MS;
|
|
397
|
+
if (!Number.isInteger(streamIntervalMs) || streamIntervalMs < 0) throw new Error("streamIntervalMs must be a non-negative integer");
|
|
398
|
+
const ingress = options.webhook ? slackWebhook(options.webhook) : void 0;
|
|
399
|
+
async function resolveTarget(target) {
|
|
400
|
+
if (!("userId" in target)) return target;
|
|
401
|
+
let response;
|
|
402
|
+
try {
|
|
403
|
+
response = await fetch(`${apiBaseUrl}/conversations.open`, {
|
|
404
|
+
method: "POST",
|
|
405
|
+
headers: {
|
|
406
|
+
authorization: `Bearer ${options.botToken}`,
|
|
407
|
+
"content-type": "application/json; charset=utf-8"
|
|
408
|
+
},
|
|
409
|
+
body: JSON.stringify({ users: target.userId })
|
|
410
|
+
});
|
|
411
|
+
} catch {
|
|
412
|
+
return failed("SLACK_CONTACT_RESOLUTION_FAILED", "Slack could not resolve a direct-message destination", true);
|
|
413
|
+
}
|
|
414
|
+
let payload;
|
|
415
|
+
try {
|
|
416
|
+
payload = await response.json();
|
|
417
|
+
} catch {
|
|
418
|
+
return failed("SLACK_CONTACT_RESOLUTION_FAILED", "Slack returned an invalid direct-message destination", response.status === 429 || response.status >= 500);
|
|
419
|
+
}
|
|
420
|
+
const result = isRecord(payload) ? payload : void 0;
|
|
421
|
+
if (result?.ok === true && typeof result.channel?.id === "string") return {
|
|
422
|
+
channelId: result.channel.id,
|
|
423
|
+
recipientUserId: target.userId,
|
|
424
|
+
recipientTeamId: target.teamId
|
|
425
|
+
};
|
|
426
|
+
const error = typeof result?.error === "string" ? result.error : "unknown";
|
|
427
|
+
return failed(slackErrorCode(error), `Slack could not open a direct message: ${error}`, response.status === 429 || response.status >= 500 || error === "ratelimited");
|
|
428
|
+
}
|
|
429
|
+
async function postMessage(destination, text, blocks) {
|
|
430
|
+
const unresolvedTarget = slackTarget(destination);
|
|
431
|
+
if (!unresolvedTarget) return failed("SLACK_SURFACE_INVALID", `Slack cannot parse the address for Channel "${destination.channelKey}"`, false);
|
|
432
|
+
const target = await resolveTarget(unresolvedTarget);
|
|
433
|
+
if ("status" in target) return target;
|
|
434
|
+
const sent = await callSlack("chat.postMessage", {
|
|
435
|
+
channel: target.channelId,
|
|
436
|
+
text,
|
|
437
|
+
mrkdwn: true,
|
|
438
|
+
...target.threadTs && { thread_ts: target.threadTs },
|
|
439
|
+
...blocks && { blocks }
|
|
440
|
+
});
|
|
441
|
+
return "status" in sent ? sent : {
|
|
442
|
+
status: "delivered",
|
|
443
|
+
reference: outboundReference(sent.channelId, sent.ts)
|
|
444
|
+
};
|
|
445
|
+
}
|
|
446
|
+
/**
|
|
447
|
+
* One transport path for every Slack method this Channel calls.
|
|
448
|
+
*
|
|
449
|
+
* Each of them answers with the conversation and timestamp on success, and
|
|
450
|
+
* every failure mode is classified the same way, so posting and streaming
|
|
451
|
+
* cannot drift apart.
|
|
452
|
+
*/
|
|
453
|
+
async function callSlack(method, body) {
|
|
454
|
+
let response;
|
|
455
|
+
try {
|
|
456
|
+
response = await fetch(`${apiBaseUrl}/${method}`, {
|
|
457
|
+
method: "POST",
|
|
458
|
+
headers: {
|
|
459
|
+
authorization: `Bearer ${options.botToken}`,
|
|
460
|
+
"content-type": "application/json; charset=utf-8"
|
|
461
|
+
},
|
|
462
|
+
body: JSON.stringify(body)
|
|
463
|
+
});
|
|
464
|
+
} catch {
|
|
465
|
+
return uncertain("SLACK_DELIVERY_ERROR", "Slack delivery failed with an unknown outcome");
|
|
466
|
+
}
|
|
467
|
+
let payload;
|
|
468
|
+
try {
|
|
469
|
+
payload = await response.json();
|
|
470
|
+
} catch {
|
|
471
|
+
payload = void 0;
|
|
472
|
+
}
|
|
473
|
+
const apiResponse = asApiResponse(payload);
|
|
474
|
+
if (apiResponse?.ok === true && typeof apiResponse.channel === "string" && typeof apiResponse.ts === "string") return {
|
|
475
|
+
channelId: apiResponse.channel,
|
|
476
|
+
ts: apiResponse.ts
|
|
477
|
+
};
|
|
478
|
+
return apiResponse?.ok === true ? uncertain("SLACK_DELIVERY_ERROR", "Slack returned an invalid delivery response") : classifyApiFailure(response, apiResponse ?? {});
|
|
479
|
+
}
|
|
480
|
+
async function streamMessage(destination, chunks, streamOptions) {
|
|
481
|
+
const unresolvedTarget = slackTarget(destination);
|
|
482
|
+
if (!unresolvedTarget) {
|
|
483
|
+
await chunks.cancel().catch(() => {});
|
|
484
|
+
return failed("SLACK_SURFACE_INVALID", `Slack cannot parse the address for Channel "${destination.channelKey}"`, false);
|
|
485
|
+
}
|
|
486
|
+
if (!("userId" in unresolvedTarget) && !unresolvedTarget.threadTs && !unresolvedTarget.recipientUserId) {
|
|
487
|
+
const collected = await collectText(chunks);
|
|
488
|
+
if (collected.interrupted && collected.text.length === 0) return failed("SLACK_STREAM_INTERRUPTED", "The stream ended before producing any content to deliver", false);
|
|
489
|
+
const delivered = await postMessage(destination, toText({
|
|
490
|
+
...streamOptions.title && { title: streamOptions.title },
|
|
491
|
+
markdown: collected.text
|
|
492
|
+
}));
|
|
493
|
+
if (!collected.interrupted || delivered.status !== "delivered") return delivered;
|
|
494
|
+
return uncertain("SLACK_STREAM_INTERRUPTED", "An incomplete answer was delivered because the stream ended early", delivered.reference);
|
|
495
|
+
}
|
|
496
|
+
const target = await resolveTarget(unresolvedTarget);
|
|
497
|
+
if ("status" in target) {
|
|
498
|
+
await chunks.cancel().catch(() => {});
|
|
499
|
+
return target;
|
|
500
|
+
}
|
|
501
|
+
const started = await callSlack("chat.startStream", {
|
|
502
|
+
channel: target.channelId,
|
|
503
|
+
...target.threadTs && { thread_ts: target.threadTs },
|
|
504
|
+
...target.recipientUserId && {
|
|
505
|
+
recipient_user_id: target.recipientUserId,
|
|
506
|
+
recipient_team_id: target.recipientTeamId
|
|
507
|
+
},
|
|
508
|
+
...streamOptions.title && { chunks: splitText(`${streamOptions.title}\n\n`, SLACK_APPEND_LIMIT).map((text) => ({
|
|
509
|
+
type: "markdown_text",
|
|
510
|
+
text
|
|
511
|
+
})) }
|
|
512
|
+
});
|
|
513
|
+
if ("status" in started) {
|
|
514
|
+
await chunks.cancel().catch(() => {});
|
|
515
|
+
return started;
|
|
516
|
+
}
|
|
517
|
+
const reference = outboundReference(started.channelId, started.ts);
|
|
518
|
+
const sources = [];
|
|
519
|
+
const shouldFlush = createPacer(streamIntervalMs);
|
|
520
|
+
let pending = [];
|
|
521
|
+
let appendFailure;
|
|
522
|
+
return consumeChunks(chunks, {
|
|
523
|
+
async onChunk(chunk) {
|
|
524
|
+
pending.push(...toStreamChunks(chunk, sources));
|
|
525
|
+
if (pending.length === 0 || !shouldFlush()) return;
|
|
526
|
+
const appended = await callSlack("chat.appendStream", {
|
|
527
|
+
channel: started.channelId,
|
|
528
|
+
ts: started.ts,
|
|
529
|
+
chunks: pending
|
|
530
|
+
});
|
|
531
|
+
pending = [];
|
|
532
|
+
if ("status" in appended) {
|
|
533
|
+
appendFailure = appended;
|
|
534
|
+
throw new Error(appended.error.message);
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
async onFinish(outcome) {
|
|
538
|
+
const stopped = await callSlack("chat.stopStream", {
|
|
539
|
+
channel: started.channelId,
|
|
540
|
+
ts: started.ts,
|
|
541
|
+
...pending.length > 0 && !appendFailure && { chunks: pending },
|
|
542
|
+
...sources.length > 0 && { blocks: [sourcesBlock(sources)] }
|
|
543
|
+
});
|
|
544
|
+
if ("status" in stopped) return uncertain(stopped.error.code, `Slack could not stop the stream: ${stopped.error.message}`, reference);
|
|
545
|
+
if (appendFailure) return uncertain(appendFailure.error.code, appendFailure.error.message, reference);
|
|
546
|
+
if (outcome.interrupted) return uncertain("SLACK_STREAM_INTERRUPTED", "The answer ended early, so the Slack message is incomplete", reference);
|
|
547
|
+
return {
|
|
548
|
+
status: "delivered",
|
|
549
|
+
reference
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
});
|
|
553
|
+
}
|
|
554
|
+
return {
|
|
555
|
+
...options.route && { route: options.route },
|
|
556
|
+
...ingress && { ingress },
|
|
557
|
+
contactSurface(identity) {
|
|
558
|
+
if (identity.scope === void 0) return null;
|
|
559
|
+
return {
|
|
560
|
+
version: 1,
|
|
561
|
+
address: {
|
|
562
|
+
teamId: identity.scope,
|
|
563
|
+
userId: identity.subject
|
|
564
|
+
},
|
|
565
|
+
label: `Slack · user ${identity.subject}`
|
|
566
|
+
};
|
|
567
|
+
},
|
|
568
|
+
deliver(destination, message) {
|
|
569
|
+
return postMessage(destination, toText(message));
|
|
570
|
+
},
|
|
571
|
+
stream(destination, chunks, streamOptions) {
|
|
572
|
+
return streamMessage(destination, chunks, streamOptions);
|
|
573
|
+
},
|
|
574
|
+
requestApproval(destination, { interactionId, request }) {
|
|
575
|
+
if (interactionId.length === 0) return Promise.resolve(failed("SLACK_INTERACTION_ID_REQUIRED", "Slack approval requests require a non-empty interaction id", false));
|
|
576
|
+
const text = approvalText(request);
|
|
577
|
+
const approveValue = approvalValue(interactionId, "approve");
|
|
578
|
+
const rejectValue = approvalValue(interactionId, "reject");
|
|
579
|
+
if (text.length > 3e3 || approveValue.length > 2e3 || rejectValue.length > 2e3) return Promise.resolve(failed("SLACK_APPROVAL_TOO_LONG", "Slack approval content exceeds Block Kit limits", false));
|
|
580
|
+
return postMessage(destination, text, [{
|
|
581
|
+
type: "section",
|
|
582
|
+
text: {
|
|
583
|
+
type: "mrkdwn",
|
|
584
|
+
text
|
|
585
|
+
}
|
|
586
|
+
}, {
|
|
587
|
+
type: "actions",
|
|
588
|
+
elements: [{
|
|
589
|
+
type: "button",
|
|
590
|
+
action_id: APPROVE_ACTION_ID,
|
|
591
|
+
text: {
|
|
592
|
+
type: "plain_text",
|
|
593
|
+
text: "Approve"
|
|
594
|
+
},
|
|
595
|
+
style: "primary",
|
|
596
|
+
value: approveValue
|
|
597
|
+
}, {
|
|
598
|
+
type: "button",
|
|
599
|
+
action_id: REJECT_ACTION_ID,
|
|
600
|
+
text: {
|
|
601
|
+
type: "plain_text",
|
|
602
|
+
text: "Reject"
|
|
603
|
+
},
|
|
604
|
+
style: "danger",
|
|
605
|
+
value: rejectValue
|
|
606
|
+
}]
|
|
607
|
+
}]);
|
|
608
|
+
}
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
//#endregion
|
|
612
|
+
export { slack, slackWebhook };
|
|
613
|
+
|
|
614
|
+
//# sourceMappingURL=slack.js.map
|