@poolzin/pool-bot 2026.2.24 → 2026.2.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (191) hide show
  1. package/CHANGELOG.md +21 -0
  2. package/dist/acp/client.js +207 -18
  3. package/dist/acp/secret-file.js +22 -0
  4. package/dist/agents/agent-scope.js +10 -0
  5. package/dist/agents/bash-process-registry.test-helpers.js +29 -0
  6. package/dist/agents/bash-tools.exec-approval-request.js +20 -0
  7. package/dist/agents/bash-tools.exec-host-gateway.js +230 -0
  8. package/dist/agents/bash-tools.exec-host-node.js +235 -0
  9. package/dist/agents/bash-tools.exec-types.js +1 -0
  10. package/dist/agents/bash-tools.process.js +224 -218
  11. package/dist/agents/content-blocks.js +16 -0
  12. package/dist/agents/model-fallback.js +96 -101
  13. package/dist/agents/models-config.providers.js +299 -182
  14. package/dist/agents/pi-embedded-payloads.js +1 -0
  15. package/dist/agents/pi-embedded-runner/run.overflow-compaction.fixture.js +34 -0
  16. package/dist/agents/skills.test-helpers.js +13 -0
  17. package/dist/agents/stable-stringify.js +12 -0
  18. package/dist/agents/subagent-registry.mocks.shared.js +12 -0
  19. package/dist/agents/test-helpers/assistant-message-fixtures.js +29 -0
  20. package/dist/agents/test-helpers/pi-tools-sandbox-context.js +27 -0
  21. package/dist/agents/tool-policy-shared.js +108 -0
  22. package/dist/agents/tools/browser-tool.js +160 -54
  23. package/dist/agents/tools/cron-tool.test-helpers.js +12 -0
  24. package/dist/agents/tools/discord-actions-moderation-shared.js +27 -0
  25. package/dist/agents/tools/image-tool.js +214 -99
  26. package/dist/agents/tools/sessions-history-tool.js +140 -108
  27. package/dist/agents/workspace.js +222 -46
  28. package/dist/auto-reply/commands-registry.js +15 -18
  29. package/dist/auto-reply/fallback-state.js +114 -0
  30. package/dist/auto-reply/model-runtime.js +68 -0
  31. package/dist/auto-reply/reply/agent-runner-execution.js +36 -4
  32. package/dist/auto-reply/reply/agent-runner.js +165 -39
  33. package/dist/auto-reply/reply/commands-setunset-standard.js +13 -0
  34. package/dist/browser/config.js +26 -0
  35. package/dist/browser/navigation-guard.js +31 -0
  36. package/dist/browser/routes/agent.act.js +431 -424
  37. package/dist/browser/routes/agent.shared.js +47 -3
  38. package/dist/browser/routes/agent.snapshot.js +122 -116
  39. package/dist/browser/routes/agent.storage.js +303 -297
  40. package/dist/browser/routes/tabs.js +154 -100
  41. package/dist/browser/server-lifecycle.js +37 -0
  42. package/dist/build-info.json +3 -3
  43. package/dist/channels/allow-from.js +25 -0
  44. package/dist/channels/plugins/account-action-gate.js +13 -0
  45. package/dist/channels/plugins/message-actions.js +10 -0
  46. package/dist/channels/telegram/api.js +18 -0
  47. package/dist/cli/argv.js +84 -21
  48. package/dist/cli/banner.js +2 -1
  49. package/dist/cli/exec-approvals-cli.js +92 -124
  50. package/dist/cli/memory-cli.js +158 -61
  51. package/dist/cli/nodes-cli/register.push.js +63 -0
  52. package/dist/cli/nodes-media-utils.js +21 -0
  53. package/dist/cli/plugins-cli.js +245 -61
  54. package/dist/cli/program/build-program.js +3 -1
  55. package/dist/cli/program/command-registry.js +223 -136
  56. package/dist/cli/program/help.js +43 -12
  57. package/dist/cli/route.js +1 -1
  58. package/dist/cli/test-runtime-capture.js +24 -0
  59. package/dist/commands/agent.js +163 -87
  60. package/dist/commands/channels.mock-harness.js +23 -0
  61. package/dist/commands/daemon-install-runtime-warning.js +11 -0
  62. package/dist/commands/sessions.test-helpers.js +61 -0
  63. package/dist/config/commands.js +3 -0
  64. package/dist/config/config.js +1 -1
  65. package/dist/config/env-substitution.js +62 -34
  66. package/dist/config/env-vars.js +9 -0
  67. package/dist/config/io.js +571 -171
  68. package/dist/config/merge-patch.js +50 -4
  69. package/dist/config/redact-snapshot.js +404 -76
  70. package/dist/config/schema.js +58 -570
  71. package/dist/config/validation.js +140 -85
  72. package/dist/config/zod-schema.hooks.js +40 -11
  73. package/dist/config/zod-schema.installs.js +20 -0
  74. package/dist/config/zod-schema.js +8 -7
  75. package/dist/daemon/cmd-argv.js +21 -0
  76. package/dist/daemon/cmd-set.js +58 -0
  77. package/dist/daemon/service-types.js +1 -0
  78. package/dist/discord/monitor/exec-approvals.js +357 -162
  79. package/dist/gateway/auth.js +38 -3
  80. package/dist/gateway/call.js +149 -68
  81. package/dist/gateway/canvas-capability.js +75 -0
  82. package/dist/gateway/control-plane-audit.js +28 -0
  83. package/dist/gateway/control-plane-rate-limit.js +53 -0
  84. package/dist/gateway/events.js +1 -0
  85. package/dist/gateway/hooks.js +109 -54
  86. package/dist/gateway/http-common.js +22 -0
  87. package/dist/gateway/method-scopes.js +169 -0
  88. package/dist/gateway/net.js +23 -0
  89. package/dist/gateway/openresponses-http.js +120 -110
  90. package/dist/gateway/probe-auth.js +2 -0
  91. package/dist/gateway/protocol/index.js +3 -2
  92. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  93. package/dist/gateway/protocol/schema/push.js +18 -0
  94. package/dist/gateway/protocol/schema.js +1 -0
  95. package/dist/gateway/server-http.js +236 -52
  96. package/dist/gateway/server-methods/agent.js +162 -24
  97. package/dist/gateway/server-methods/chat.js +461 -130
  98. package/dist/gateway/server-methods/config.js +193 -150
  99. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  100. package/dist/gateway/server-methods/nodes.js +251 -69
  101. package/dist/gateway/server-methods/push.js +53 -0
  102. package/dist/gateway/server-reload-handlers.js +2 -3
  103. package/dist/gateway/server-runtime-config.js +5 -0
  104. package/dist/gateway/server-runtime-state.js +2 -0
  105. package/dist/gateway/server-ws-runtime.js +1 -0
  106. package/dist/gateway/server.impl.js +296 -139
  107. package/dist/gateway/session-preview.test-helpers.js +11 -0
  108. package/dist/gateway/startup-auth.js +126 -0
  109. package/dist/gateway/test-helpers.agent-results.js +15 -0
  110. package/dist/gateway/test-helpers.mocks.js +37 -14
  111. package/dist/gateway/test-helpers.server.js +161 -77
  112. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  113. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  114. package/dist/infra/archive-path.js +49 -0
  115. package/dist/infra/device-pairing.js +148 -167
  116. package/dist/infra/exec-approvals-allowlist.js +19 -70
  117. package/dist/infra/exec-approvals-analysis.js +44 -17
  118. package/dist/infra/exec-safe-bin-policy.js +269 -0
  119. package/dist/infra/fixed-window-rate-limit.js +33 -0
  120. package/dist/infra/git-root.js +61 -0
  121. package/dist/infra/heartbeat-active-hours.js +2 -2
  122. package/dist/infra/heartbeat-reason.js +40 -0
  123. package/dist/infra/heartbeat-runner.js +72 -32
  124. package/dist/infra/install-source-utils.js +91 -7
  125. package/dist/infra/node-pairing.js +50 -105
  126. package/dist/infra/npm-integrity.js +45 -0
  127. package/dist/infra/npm-pack-install.js +40 -0
  128. package/dist/infra/outbound/channel-adapters.js +20 -7
  129. package/dist/infra/outbound/message-action-runner.js +107 -327
  130. package/dist/infra/outbound/message.js +59 -36
  131. package/dist/infra/outbound/outbound-policy.js +52 -25
  132. package/dist/infra/outbound/outbound-send-service.js +58 -71
  133. package/dist/infra/pairing-files.js +10 -0
  134. package/dist/infra/plain-object.js +9 -0
  135. package/dist/infra/push-apns.js +365 -0
  136. package/dist/infra/restart-sentinel.js +16 -1
  137. package/dist/infra/restart.js +229 -26
  138. package/dist/infra/scp-host.js +54 -0
  139. package/dist/infra/update-startup.js +86 -9
  140. package/dist/media/inbound-path-policy.js +114 -0
  141. package/dist/media/input-files.js +16 -0
  142. package/dist/memory/test-manager.js +8 -0
  143. package/dist/plugin-sdk/temp-path.js +47 -0
  144. package/dist/plugins/discovery.js +217 -23
  145. package/dist/plugins/hook-runner-global.js +16 -0
  146. package/dist/plugins/loader.js +192 -26
  147. package/dist/plugins/logger.js +8 -0
  148. package/dist/plugins/manifest-registry.js +3 -0
  149. package/dist/plugins/path-safety.js +34 -0
  150. package/dist/plugins/registry.js +5 -2
  151. package/dist/plugins/runtime/index.js +271 -206
  152. package/dist/providers/github-copilot-models.js +4 -1
  153. package/dist/security/audit-channel.js +8 -19
  154. package/dist/security/audit-extra.async.js +354 -182
  155. package/dist/security/audit-extra.js +11 -1
  156. package/dist/security/audit-extra.sync.js +340 -33
  157. package/dist/security/audit-fs.js +31 -13
  158. package/dist/security/audit.js +145 -371
  159. package/dist/security/dm-policy-shared.js +24 -0
  160. package/dist/security/external-content.js +20 -8
  161. package/dist/security/fix.js +49 -85
  162. package/dist/security/scan-paths.js +20 -0
  163. package/dist/security/secret-equal.js +3 -7
  164. package/dist/security/windows-acl.js +30 -15
  165. package/dist/shared/node-list-parse.js +13 -0
  166. package/dist/shared/operator-scope-compat.js +37 -0
  167. package/dist/shared/text-chunking.js +29 -0
  168. package/dist/slack/blocks.test-helpers.js +31 -0
  169. package/dist/slack/monitor/mrkdwn.js +8 -0
  170. package/dist/telegram/bot-message-dispatch.js +366 -164
  171. package/dist/telegram/draft-stream.js +30 -7
  172. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  173. package/dist/terminal/prompt-select-styled.js +9 -0
  174. package/dist/test-utils/command-runner.js +6 -0
  175. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  176. package/dist/test-utils/model-auth-mock.js +12 -0
  177. package/dist/test-utils/provider-usage-fetch.js +14 -0
  178. package/dist/test-utils/temp-home.js +33 -0
  179. package/dist/tui/components/chat-log.js +9 -0
  180. package/dist/tui/tui-command-handlers.js +36 -27
  181. package/dist/tui/tui-event-handlers.js +122 -32
  182. package/dist/tui/tui.js +181 -45
  183. package/dist/utils/mask-api-key.js +10 -0
  184. package/dist/utils/run-with-concurrency.js +39 -0
  185. package/dist/web/media.js +4 -0
  186. package/docs/tools/slash-commands.md +5 -1
  187. package/extensions/feishu/src/external-keys.ts +19 -0
  188. package/extensions/lobster/src/windows-spawn.ts +193 -0
  189. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  190. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  191. package/package.json +1 -1
@@ -1,55 +1,46 @@
1
- import path from "node:path";
2
- import { fileURLToPath } from "node:url";
3
- import { readNumberParam, readStringArrayParam, readStringParam, } from "../../agents/tools/common.js";
4
1
  import { resolveSessionAgentId } from "../../agents/agent-scope.js";
2
+ import { readNumberParam, readStringArrayParam, readStringParam, } from "../../agents/tools/common.js";
5
3
  import { parseReplyDirectives } from "../../auto-reply/reply/reply-directives.js";
6
4
  import { dispatchChannelMessageAction } from "../../channels/plugins/message-actions.js";
7
5
  import { isDeliverableMessageChannel, normalizeMessageChannel, } from "../../utils/message-channel.js";
6
+ import { throwIfAborted } from "./abort.js";
8
7
  import { listConfiguredMessageChannels, resolveMessageChannelSelection, } from "./channel-selection.js";
9
8
  import { applyTargetToParams } from "./channel-target.js";
10
- import { ensureOutboundSessionEntry, resolveOutboundSessionRoute } from "./outbound-session.js";
9
+ import { hydrateSendAttachmentParams, hydrateSetGroupIconParams, normalizeSandboxMediaList, normalizeSandboxMediaParams, parseButtonsParam, parseCardParam, parseComponentsParam, readBooleanParam, resolveSlackAutoThreadId, resolveTelegramAutoThreadId, } from "./message-action-params.js";
10
+ import { actionHasTarget, actionRequiresTarget } from "./message-action-spec.js";
11
11
  import { applyCrossContextDecoration, buildCrossContextDecoration, enforceCrossContextPolicy, shouldApplyCrossContextMarker, } from "./outbound-policy.js";
12
12
  import { executePollAction, executeSendAction } from "./outbound-send-service.js";
13
- import { actionHasTarget, actionRequiresTarget } from "./message-action-spec.js";
13
+ import { ensureOutboundSessionEntry, resolveOutboundSessionRoute } from "./outbound-session.js";
14
14
  import { resolveChannelTarget } from "./target-resolver.js";
15
- import { loadWebMedia } from "../../web/media.js";
16
- import { extensionForMime } from "../../media/mime.js";
17
- import { parseSlackTarget } from "../../slack/targets.js";
18
- import { parseTelegramTarget } from "../../telegram/targets.js";
19
- import { assertMediaNotDataUrl, resolveSandboxedMediaSource } from "../../agents/sandbox-paths.js";
20
- import { throwIfAborted } from "./abort.js";
21
- export function getToolResult(result) {
22
- return "toolResult" in result ? result.toolResult : undefined;
23
- }
24
- function extractToolPayload(result) {
25
- if (result.details !== undefined)
26
- return result.details;
27
- const textBlock = Array.isArray(result.content)
28
- ? result.content.find((block) => block &&
29
- typeof block === "object" &&
30
- block.type === "text" &&
31
- typeof block.text === "string")
15
+ import { extractToolPayload } from "./tool-payload.js";
16
+ function resolveAndApplyOutboundThreadId(params, ctx) {
17
+ const threadId = readStringParam(params, "threadId");
18
+ const slackAutoThreadId = ctx.allowSlackAutoThread && ctx.channel === "slack" && !threadId
19
+ ? resolveSlackAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext })
32
20
  : undefined;
33
- const text = textBlock?.text;
34
- if (text) {
35
- try {
36
- return JSON.parse(text);
37
- }
38
- catch {
39
- return text;
40
- }
21
+ const telegramAutoThreadId = ctx.channel === "telegram" && !threadId
22
+ ? resolveTelegramAutoThreadId({ to: ctx.to, toolContext: ctx.toolContext })
23
+ : undefined;
24
+ const resolved = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
25
+ // Write auto-resolved threadId back into params so downstream dispatch
26
+ // (plugin `readStringParam(params, "threadId")`) picks it up.
27
+ if (resolved && !params.threadId) {
28
+ params.threadId = resolved;
41
29
  }
42
- return result.content ?? result;
30
+ return resolved ?? undefined;
31
+ }
32
+ export function getToolResult(result) {
33
+ return "toolResult" in result ? result.toolResult : undefined;
43
34
  }
44
- function applyCrossContextMessageDecoration({ params, message, decoration, preferEmbeds, }) {
35
+ function applyCrossContextMessageDecoration({ params, message, decoration, preferComponents, }) {
45
36
  const applied = applyCrossContextDecoration({
46
37
  message,
47
38
  decoration,
48
- preferEmbeds,
39
+ preferComponents,
49
40
  });
50
41
  params.message = applied.message;
51
- if (applied.embeds?.length) {
52
- params.embeds = applied.embeds;
42
+ if (applied.componentsBuilder) {
43
+ params.components = applied.componentsBuilder;
53
44
  }
54
45
  return applied.message;
55
46
  }
@@ -64,279 +55,16 @@ async function maybeApplyCrossContextMarker(params) {
64
55
  toolContext: params.toolContext,
65
56
  accountId: params.accountId ?? undefined,
66
57
  });
67
- if (!decoration)
58
+ if (!decoration) {
68
59
  return params.message;
60
+ }
69
61
  return applyCrossContextMessageDecoration({
70
62
  params: params.args,
71
63
  message: params.message,
72
64
  decoration,
73
- preferEmbeds: params.preferEmbeds,
65
+ preferComponents: params.preferComponents,
74
66
  });
75
67
  }
76
- function readBooleanParam(params, key) {
77
- const raw = params[key];
78
- if (typeof raw === "boolean")
79
- return raw;
80
- if (typeof raw === "string") {
81
- const trimmed = raw.trim().toLowerCase();
82
- if (trimmed === "true")
83
- return true;
84
- if (trimmed === "false")
85
- return false;
86
- }
87
- return undefined;
88
- }
89
- function resolveSlackAutoThreadId(params) {
90
- const context = params.toolContext;
91
- if (!context?.currentThreadTs || !context.currentChannelId)
92
- return undefined;
93
- // Only mirror auto-threading when Slack would reply in the active thread for this channel.
94
- if (context.replyToMode !== "all" && context.replyToMode !== "first")
95
- return undefined;
96
- const parsedTarget = parseSlackTarget(params.to, { defaultKind: "channel" });
97
- if (!parsedTarget || parsedTarget.kind !== "channel")
98
- return undefined;
99
- if (parsedTarget.id.toLowerCase() !== context.currentChannelId.toLowerCase())
100
- return undefined;
101
- if (context.replyToMode === "first" && context.hasRepliedRef?.value)
102
- return undefined;
103
- return context.currentThreadTs;
104
- }
105
- /**
106
- * Auto-inject Telegram forum topic thread ID when the message tool targets
107
- * the same chat the session originated from.
108
- */
109
- function resolveTelegramAutoThreadId(params) {
110
- const context = params.toolContext;
111
- if (!context?.currentThreadTs || !context.currentChannelId)
112
- return undefined;
113
- const parsedTo = parseTelegramTarget(params.to);
114
- const parsedChannel = parseTelegramTarget(context.currentChannelId);
115
- if (parsedTo.chatId.toLowerCase() !== parsedChannel.chatId.toLowerCase())
116
- return undefined;
117
- return context.currentThreadTs;
118
- }
119
- function resolveAttachmentMaxBytes(params) {
120
- const accountId = typeof params.accountId === "string" ? params.accountId.trim() : "";
121
- const channelCfg = params.cfg.channels?.[params.channel];
122
- const channelObj = channelCfg && typeof channelCfg === "object"
123
- ? channelCfg
124
- : undefined;
125
- const channelMediaMax = typeof channelObj?.mediaMaxMb === "number" ? channelObj.mediaMaxMb : undefined;
126
- const accountsObj = channelObj?.accounts && typeof channelObj.accounts === "object"
127
- ? channelObj.accounts
128
- : undefined;
129
- const accountCfg = accountId && accountsObj ? accountsObj[accountId] : undefined;
130
- const accountMediaMax = accountCfg && typeof accountCfg === "object"
131
- ? accountCfg.mediaMaxMb
132
- : undefined;
133
- const limitMb = (typeof accountMediaMax === "number" ? accountMediaMax : undefined) ??
134
- channelMediaMax ??
135
- params.cfg.agents?.defaults?.mediaMaxMb;
136
- return typeof limitMb === "number" ? limitMb * 1024 * 1024 : undefined;
137
- }
138
- function inferAttachmentFilename(params) {
139
- const mediaHint = params.mediaHint?.trim();
140
- if (mediaHint) {
141
- try {
142
- if (mediaHint.startsWith("file://")) {
143
- const filePath = fileURLToPath(mediaHint);
144
- const base = path.basename(filePath);
145
- if (base)
146
- return base;
147
- }
148
- else if (/^https?:\/\//i.test(mediaHint)) {
149
- const url = new URL(mediaHint);
150
- const base = path.basename(url.pathname);
151
- if (base)
152
- return base;
153
- }
154
- else {
155
- const base = path.basename(mediaHint);
156
- if (base)
157
- return base;
158
- }
159
- }
160
- catch {
161
- // fall through to content-type based default
162
- }
163
- }
164
- const ext = params.contentType ? extensionForMime(params.contentType) : undefined;
165
- return ext ? `attachment${ext}` : "attachment";
166
- }
167
- function normalizeBase64Payload(params) {
168
- if (!params.base64)
169
- return { base64: params.base64, contentType: params.contentType };
170
- const match = /^data:([^;]+);base64,(.*)$/i.exec(params.base64.trim());
171
- if (!match)
172
- return { base64: params.base64, contentType: params.contentType };
173
- const [, mime, payload] = match;
174
- return {
175
- base64: payload,
176
- contentType: params.contentType ?? mime,
177
- };
178
- }
179
- async function normalizeSandboxMediaParams(params) {
180
- const sandboxRoot = params.sandboxRoot?.trim();
181
- const mediaKeys = ["media", "path", "filePath"];
182
- for (const key of mediaKeys) {
183
- const raw = readStringParam(params.args, key, { trim: false });
184
- if (!raw)
185
- continue;
186
- assertMediaNotDataUrl(raw);
187
- if (!sandboxRoot)
188
- continue;
189
- const normalized = await resolveSandboxedMediaSource({ media: raw, sandboxRoot });
190
- if (normalized !== raw) {
191
- params.args[key] = normalized;
192
- }
193
- }
194
- }
195
- async function normalizeSandboxMediaList(params) {
196
- const sandboxRoot = params.sandboxRoot?.trim();
197
- const normalized = [];
198
- const seen = new Set();
199
- for (const value of params.values) {
200
- const raw = value?.trim();
201
- if (!raw)
202
- continue;
203
- assertMediaNotDataUrl(raw);
204
- const resolved = sandboxRoot
205
- ? await resolveSandboxedMediaSource({ media: raw, sandboxRoot })
206
- : raw;
207
- if (seen.has(resolved))
208
- continue;
209
- seen.add(resolved);
210
- normalized.push(resolved);
211
- }
212
- return normalized;
213
- }
214
- async function hydrateSetGroupIconParams(params) {
215
- if (params.action !== "setGroupIcon")
216
- return;
217
- const mediaHint = readStringParam(params.args, "media", { trim: false });
218
- const fileHint = readStringParam(params.args, "path", { trim: false }) ??
219
- readStringParam(params.args, "filePath", { trim: false });
220
- const contentTypeParam = readStringParam(params.args, "contentType") ?? readStringParam(params.args, "mimeType");
221
- const rawBuffer = readStringParam(params.args, "buffer", { trim: false });
222
- const normalized = normalizeBase64Payload({
223
- base64: rawBuffer,
224
- contentType: contentTypeParam ?? undefined,
225
- });
226
- if (normalized.base64 !== rawBuffer && normalized.base64) {
227
- params.args.buffer = normalized.base64;
228
- if (normalized.contentType && !contentTypeParam) {
229
- params.args.contentType = normalized.contentType;
230
- }
231
- }
232
- const filename = readStringParam(params.args, "filename");
233
- const mediaSource = mediaHint ?? fileHint;
234
- if (!params.dryRun && !readStringParam(params.args, "buffer", { trim: false }) && mediaSource) {
235
- const maxBytes = resolveAttachmentMaxBytes({
236
- cfg: params.cfg,
237
- channel: params.channel,
238
- accountId: params.accountId,
239
- });
240
- const media = await loadWebMedia(mediaSource, maxBytes);
241
- params.args.buffer = media.buffer.toString("base64");
242
- if (!contentTypeParam && media.contentType) {
243
- params.args.contentType = media.contentType;
244
- }
245
- if (!filename) {
246
- params.args.filename = inferAttachmentFilename({
247
- mediaHint: media.fileName ?? mediaSource,
248
- contentType: media.contentType ?? contentTypeParam ?? undefined,
249
- });
250
- }
251
- }
252
- else if (!filename) {
253
- params.args.filename = inferAttachmentFilename({
254
- mediaHint: mediaSource,
255
- contentType: contentTypeParam ?? undefined,
256
- });
257
- }
258
- }
259
- async function hydrateSendAttachmentParams(params) {
260
- if (params.action !== "sendAttachment")
261
- return;
262
- const mediaHint = readStringParam(params.args, "media", { trim: false });
263
- const fileHint = readStringParam(params.args, "path", { trim: false }) ??
264
- readStringParam(params.args, "filePath", { trim: false });
265
- const contentTypeParam = readStringParam(params.args, "contentType") ?? readStringParam(params.args, "mimeType");
266
- const caption = readStringParam(params.args, "caption", { allowEmpty: true })?.trim();
267
- const message = readStringParam(params.args, "message", { allowEmpty: true })?.trim();
268
- if (!caption && message)
269
- params.args.caption = message;
270
- const rawBuffer = readStringParam(params.args, "buffer", { trim: false });
271
- const normalized = normalizeBase64Payload({
272
- base64: rawBuffer,
273
- contentType: contentTypeParam ?? undefined,
274
- });
275
- if (normalized.base64 !== rawBuffer && normalized.base64) {
276
- params.args.buffer = normalized.base64;
277
- if (normalized.contentType && !contentTypeParam) {
278
- params.args.contentType = normalized.contentType;
279
- }
280
- }
281
- const filename = readStringParam(params.args, "filename");
282
- const mediaSource = mediaHint ?? fileHint;
283
- if (!params.dryRun && !readStringParam(params.args, "buffer", { trim: false }) && mediaSource) {
284
- const maxBytes = resolveAttachmentMaxBytes({
285
- cfg: params.cfg,
286
- channel: params.channel,
287
- accountId: params.accountId,
288
- });
289
- const media = await loadWebMedia(mediaSource, maxBytes);
290
- params.args.buffer = media.buffer.toString("base64");
291
- if (!contentTypeParam && media.contentType) {
292
- params.args.contentType = media.contentType;
293
- }
294
- if (!filename) {
295
- params.args.filename = inferAttachmentFilename({
296
- mediaHint: media.fileName ?? mediaSource,
297
- contentType: media.contentType ?? contentTypeParam ?? undefined,
298
- });
299
- }
300
- }
301
- else if (!filename) {
302
- params.args.filename = inferAttachmentFilename({
303
- mediaHint: mediaSource,
304
- contentType: contentTypeParam ?? undefined,
305
- });
306
- }
307
- }
308
- function parseButtonsParam(params) {
309
- const raw = params.buttons;
310
- if (typeof raw !== "string")
311
- return;
312
- const trimmed = raw.trim();
313
- if (!trimmed) {
314
- delete params.buttons;
315
- return;
316
- }
317
- try {
318
- params.buttons = JSON.parse(trimmed);
319
- }
320
- catch {
321
- throw new Error("--buttons must be valid JSON");
322
- }
323
- }
324
- function parseCardParam(params) {
325
- const raw = params.card;
326
- if (typeof raw !== "string")
327
- return;
328
- const trimmed = raw.trim();
329
- if (!trimmed) {
330
- delete params.card;
331
- return;
332
- }
333
- try {
334
- params.card = JSON.parse(trimmed);
335
- }
336
- catch {
337
- throw new Error("--card must be valid JSON");
338
- }
339
- }
340
68
  async function resolveChannel(cfg, params) {
341
69
  const channelHint = readStringParam(params, "channel");
342
70
  const selection = await resolveMessageChannelSelection({
@@ -385,8 +113,9 @@ async function resolveActionTarget(params) {
385
113
  return resolvedTarget;
386
114
  }
387
115
  function resolveGateway(input) {
388
- if (!input.gateway)
116
+ if (!input.gateway) {
389
117
  return undefined;
118
+ }
390
119
  return {
391
120
  url: input.gateway.url,
392
121
  token: input.gateway.token,
@@ -426,8 +155,9 @@ async function handleBroadcastAction(input, params) {
426
155
  channel: targetChannel,
427
156
  input: target,
428
157
  });
429
- if (!resolved.ok)
158
+ if (!resolved.ok) {
430
159
  throw resolved.error;
160
+ }
431
161
  const sendResult = await runMessageAction({
432
162
  ...input,
433
163
  action: "send",
@@ -445,8 +175,9 @@ async function handleBroadcastAction(input, params) {
445
175
  });
446
176
  }
447
177
  catch (err) {
448
- if (isAbortError(err))
178
+ if (isAbortError(err)) {
449
179
  throw err;
180
+ }
450
181
  results.push({
451
182
  channel: targetChannel,
452
183
  to: target,
@@ -458,7 +189,7 @@ async function handleBroadcastAction(input, params) {
458
189
  }
459
190
  return {
460
191
  kind: "broadcast",
461
- channel: (targetChannels[0] ?? "discord"),
192
+ channel: targetChannels[0] ?? "discord",
462
193
  action: "broadcast",
463
194
  handledBy: input.dryRun ? "dry-run" : "core",
464
195
  payload: { results },
@@ -475,31 +206,37 @@ async function handleSendAction(ctx) {
475
206
  readStringParam(params, "path", { trim: false }) ??
476
207
  readStringParam(params, "filePath", { trim: false });
477
208
  const hasCard = params.card != null && typeof params.card === "object";
209
+ const hasComponents = params.components != null && typeof params.components === "object";
210
+ const caption = readStringParam(params, "caption", { allowEmpty: true }) ?? "";
478
211
  let message = readStringParam(params, "message", {
479
- required: !mediaHint && !hasCard,
212
+ required: !mediaHint && !hasCard && !hasComponents,
480
213
  allowEmpty: true,
481
214
  }) ?? "";
482
- // Unescape literal \\n sequences that models sometimes emit in tool arguments.
483
215
  if (message.includes("\\n")) {
484
216
  message = message.replaceAll("\\n", "\n");
485
217
  }
218
+ if (!message.trim() && caption.trim()) {
219
+ message = caption;
220
+ }
486
221
  const parsed = parseReplyDirectives(message);
487
222
  const mergedMediaUrls = [];
488
223
  const seenMedia = new Set();
489
224
  const pushMedia = (value) => {
490
225
  const trimmed = value?.trim();
491
- if (!trimmed)
226
+ if (!trimmed) {
492
227
  return;
493
- if (seenMedia.has(trimmed))
228
+ }
229
+ if (seenMedia.has(trimmed)) {
494
230
  return;
231
+ }
495
232
  seenMedia.add(trimmed);
496
233
  mergedMediaUrls.push(trimmed);
497
234
  };
498
235
  pushMedia(mediaHint);
499
- for (const url of parsed.mediaUrls ?? [])
236
+ for (const url of parsed.mediaUrls ?? []) {
500
237
  pushMedia(url);
238
+ }
501
239
  pushMedia(parsed.mediaUrl);
502
- // Sandbox-validate and deduplicate media URLs.
503
240
  const normalizedMediaUrls = await normalizeSandboxMediaList({
504
241
  values: mergedMediaUrls,
505
242
  sandboxRoot: input.sandboxRoot,
@@ -508,8 +245,9 @@ async function handleSendAction(ctx) {
508
245
  mergedMediaUrls.push(...normalizedMediaUrls);
509
246
  message = parsed.text;
510
247
  params.message = message;
511
- if (!params.replyTo && parsed.replyToId)
248
+ if (!params.replyTo && parsed.replyToId) {
512
249
  params.replyTo = parsed.replyToId;
250
+ }
513
251
  if (!params.media) {
514
252
  // Use path/filePath if media not set, then fall back to parsed directives
515
253
  params.media = mergedMediaUrls[0] || undefined;
@@ -523,25 +261,29 @@ async function handleSendAction(ctx) {
523
261
  accountId,
524
262
  args: params,
525
263
  message,
526
- preferEmbeds: true,
264
+ preferComponents: true,
527
265
  });
528
266
  const mediaUrl = readStringParam(params, "media", { trim: false });
267
+ if (channel === "whatsapp") {
268
+ message = message.replace(/^(?:[ \t]*\r?\n)+/, "");
269
+ if (!message.trim()) {
270
+ message = "";
271
+ }
272
+ }
273
+ if (!message.trim() && !mediaUrl && mergedMediaUrls.length === 0 && !hasCard && !hasComponents) {
274
+ throw new Error("send requires text or media");
275
+ }
276
+ params.message = message;
529
277
  const gifPlayback = readBooleanParam(params, "gifPlayback") ?? false;
530
278
  const bestEffort = readBooleanParam(params, "bestEffort");
279
+ const silent = readBooleanParam(params, "silent");
531
280
  const replyToId = readStringParam(params, "replyTo");
532
- const threadId = readStringParam(params, "threadId");
533
- // Slack auto-threading can inject threadTs without explicit params; mirror to that session key.
534
- const slackAutoThreadId = channel === "slack" && !replyToId && !threadId
535
- ? resolveSlackAutoThreadId({ to, toolContext: input.toolContext })
536
- : undefined;
537
- // Telegram forum topic auto-threading
538
- const telegramAutoThreadId = channel === "telegram" && !threadId
539
- ? resolveTelegramAutoThreadId({ to, toolContext: input.toolContext })
540
- : undefined;
541
- const resolvedThreadId = threadId ?? slackAutoThreadId ?? telegramAutoThreadId;
542
- if (resolvedThreadId && !params.threadId) {
543
- params.threadId = resolvedThreadId;
544
- }
281
+ const resolvedThreadId = resolveAndApplyOutboundThreadId(params, {
282
+ channel,
283
+ to,
284
+ toolContext: input.toolContext,
285
+ allowSlackAutoThread: channel === "slack" && !replyToId,
286
+ });
545
287
  const outboundRoute = agentId && !dryRun
546
288
  ? await resolveOutboundSessionRoute({
547
289
  cfg,
@@ -563,6 +305,12 @@ async function handleSendAction(ctx) {
563
305
  route: outboundRoute,
564
306
  });
565
307
  }
308
+ if (outboundRoute && !dryRun) {
309
+ params.__sessionKey = outboundRoute.sessionKey;
310
+ }
311
+ if (agentId) {
312
+ params.__agentId = agentId;
313
+ }
566
314
  const mirrorMediaUrls = mergedMediaUrls.length > 0 ? mergedMediaUrls : mediaUrl ? [mediaUrl] : undefined;
567
315
  throwIfAborted(abortSignal);
568
316
  const send = await executeSendAction({
@@ -570,6 +318,7 @@ async function handleSendAction(ctx) {
570
318
  cfg,
571
319
  channel,
572
320
  params,
321
+ agentId,
573
322
  accountId: accountId ?? undefined,
574
323
  gateway,
575
324
  toolContext: input.toolContext,
@@ -584,6 +333,7 @@ async function handleSendAction(ctx) {
584
333
  }
585
334
  : undefined,
586
335
  abortSignal,
336
+ silent: silent ?? undefined,
587
337
  },
588
338
  to,
589
339
  message,
@@ -591,6 +341,8 @@ async function handleSendAction(ctx) {
591
341
  mediaUrls: mergedMediaUrls.length ? mergedMediaUrls : undefined,
592
342
  gifPlayback,
593
343
  bestEffort: bestEffort ?? undefined,
344
+ replyToId: replyToId ?? undefined,
345
+ threadId: resolvedThreadId ?? undefined,
594
346
  });
595
347
  return {
596
348
  kind: "send",
@@ -616,11 +368,33 @@ async function handlePollAction(ctx) {
616
368
  if (options.length < 2) {
617
369
  throw new Error("pollOption requires at least two values");
618
370
  }
371
+ const silent = readBooleanParam(params, "silent");
619
372
  const allowMultiselect = readBooleanParam(params, "pollMulti") ?? false;
373
+ const pollAnonymous = readBooleanParam(params, "pollAnonymous");
374
+ const pollPublic = readBooleanParam(params, "pollPublic");
375
+ if (pollAnonymous && pollPublic) {
376
+ throw new Error("pollAnonymous and pollPublic are mutually exclusive");
377
+ }
378
+ const isAnonymous = pollAnonymous ? true : pollPublic ? false : undefined;
620
379
  const durationHours = readNumberParam(params, "pollDurationHours", {
621
380
  integer: true,
622
381
  });
382
+ const durationSeconds = readNumberParam(params, "pollDurationSeconds", {
383
+ integer: true,
384
+ });
623
385
  const maxSelections = allowMultiselect ? Math.max(2, options.length) : 1;
386
+ if (durationSeconds !== undefined && channel !== "telegram") {
387
+ throw new Error("pollDurationSeconds is only supported for Telegram polls");
388
+ }
389
+ if (isAnonymous !== undefined && channel !== "telegram") {
390
+ throw new Error("pollAnonymous/pollPublic are only supported for Telegram polls");
391
+ }
392
+ const resolvedThreadId = resolveAndApplyOutboundThreadId(params, {
393
+ channel,
394
+ to,
395
+ toolContext: input.toolContext,
396
+ allowSlackAutoThread: channel === "slack",
397
+ });
624
398
  const base = typeof params.message === "string" ? params.message : "";
625
399
  await maybeApplyCrossContextMarker({
626
400
  cfg,
@@ -631,7 +405,7 @@ async function handlePollAction(ctx) {
631
405
  accountId,
632
406
  args: params,
633
407
  message: base,
634
- preferEmbeds: true,
408
+ preferComponents: false,
635
409
  });
636
410
  const poll = await executePollAction({
637
411
  ctx: {
@@ -642,12 +416,16 @@ async function handlePollAction(ctx) {
642
416
  gateway,
643
417
  toolContext: input.toolContext,
644
418
  dryRun,
419
+ silent: silent ?? undefined,
645
420
  },
646
421
  to,
647
422
  question,
648
423
  options,
649
424
  maxSelections,
425
+ durationSeconds: durationSeconds ?? undefined,
650
426
  durationHours: durationHours ?? undefined,
427
+ threadId: resolvedThreadId ?? undefined,
428
+ isAnonymous,
651
429
  });
652
430
  return {
653
431
  kind: "poll",
@@ -681,6 +459,7 @@ async function handlePluginAction(ctx) {
681
459
  cfg,
682
460
  params,
683
461
  accountId: accountId ?? undefined,
462
+ requesterSenderId: input.requesterSenderId ?? undefined,
684
463
  gateway,
685
464
  toolContext: input.toolContext,
686
465
  dryRun,
@@ -707,6 +486,7 @@ export async function runMessageAction(input) {
707
486
  : undefined);
708
487
  parseButtonsParam(params);
709
488
  parseCardParam(params);
489
+ parseComponentsParam(params);
710
490
  const action = input.action;
711
491
  if (action === "broadcast") {
712
492
  return handleBroadcastAction(input, params);