@poolzin/pool-bot 2026.2.0 → 2026.2.2

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 (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
@@ -1,4 +1,5 @@
1
1
  import path from "node:path";
2
+ import { fetchWithSsrFGuard } from "../infra/net/fetch-guard.js";
2
3
  import { detectMime, extensionForMime } from "./mime.js";
3
4
  export class MediaFetchError extends Error {
4
5
  code;
@@ -12,8 +13,9 @@ function stripQuotes(value) {
12
13
  return value.replace(/^["']|["']$/g, "");
13
14
  }
14
15
  function parseContentDispositionFileName(header) {
15
- if (!header)
16
+ if (!header) {
16
17
  return undefined;
18
+ }
17
19
  const starMatch = /filename\*\s*=\s*([^;]+)/i.exec(header);
18
20
  if (starMatch?.[1]) {
19
21
  const cleaned = stripQuotes(starMatch[1].trim());
@@ -26,20 +28,24 @@ function parseContentDispositionFileName(header) {
26
28
  }
27
29
  }
28
30
  const match = /filename\s*=\s*([^;]+)/i.exec(header);
29
- if (match?.[1])
31
+ if (match?.[1]) {
30
32
  return path.basename(stripQuotes(match[1].trim()));
33
+ }
31
34
  return undefined;
32
35
  }
33
36
  async function readErrorBodySnippet(res, maxChars = 200) {
34
37
  try {
35
38
  const text = await res.text();
36
- if (!text)
39
+ if (!text) {
37
40
  return undefined;
41
+ }
38
42
  const collapsed = text.replace(/\s+/g, " ").trim();
39
- if (!collapsed)
43
+ if (!collapsed) {
40
44
  return undefined;
41
- if (collapsed.length <= maxChars)
45
+ }
46
+ if (collapsed.length <= maxChars) {
42
47
  return collapsed;
48
+ }
43
49
  return `${collapsed.slice(0, maxChars)}…`;
44
50
  }
45
51
  catch {
@@ -47,69 +53,85 @@ async function readErrorBodySnippet(res, maxChars = 200) {
47
53
  }
48
54
  }
49
55
  export async function fetchRemoteMedia(options) {
50
- const { url, fetchImpl, filePathHint, maxBytes } = options;
51
- const fetcher = fetchImpl ?? globalThis.fetch;
52
- if (!fetcher) {
53
- throw new Error("fetch is not available");
54
- }
56
+ const { url, fetchImpl, filePathHint, maxBytes, maxRedirects, ssrfPolicy, lookupFn } = options;
55
57
  let res;
58
+ let finalUrl = url;
59
+ let release = null;
56
60
  try {
57
- res = await fetcher(url);
61
+ const result = await fetchWithSsrFGuard({
62
+ url,
63
+ fetchImpl,
64
+ maxRedirects,
65
+ policy: ssrfPolicy,
66
+ lookupFn,
67
+ });
68
+ res = result.response;
69
+ finalUrl = result.finalUrl;
70
+ release = result.release;
58
71
  }
59
72
  catch (err) {
60
73
  throw new MediaFetchError("fetch_failed", `Failed to fetch media from ${url}: ${String(err)}`);
61
74
  }
62
- if (!res.ok) {
63
- const statusText = res.statusText ? ` ${res.statusText}` : "";
64
- const redirected = res.url && res.url !== url ? ` (redirected to ${res.url})` : "";
65
- let detail = `HTTP ${res.status}${statusText}`;
66
- if (!res.body) {
67
- detail = `HTTP ${res.status}${statusText}; empty response body`;
75
+ try {
76
+ if (!res.ok) {
77
+ const statusText = res.statusText ? ` ${res.statusText}` : "";
78
+ const redirected = finalUrl !== url ? ` (redirected to ${finalUrl})` : "";
79
+ let detail = `HTTP ${res.status}${statusText}`;
80
+ if (!res.body) {
81
+ detail = `HTTP ${res.status}${statusText}; empty response body`;
82
+ }
83
+ else {
84
+ const snippet = await readErrorBodySnippet(res);
85
+ if (snippet) {
86
+ detail += `; body: ${snippet}`;
87
+ }
88
+ }
89
+ throw new MediaFetchError("http_error", `Failed to fetch media from ${url}${redirected}: ${detail}`);
68
90
  }
69
- else {
70
- const snippet = await readErrorBodySnippet(res);
71
- if (snippet)
72
- detail += `; body: ${snippet}`;
91
+ const contentLength = res.headers.get("content-length");
92
+ if (maxBytes && contentLength) {
93
+ const length = Number(contentLength);
94
+ if (Number.isFinite(length) && length > maxBytes) {
95
+ throw new MediaFetchError("max_bytes", `Failed to fetch media from ${url}: content length ${length} exceeds maxBytes ${maxBytes}`);
96
+ }
73
97
  }
74
- throw new MediaFetchError("http_error", `Failed to fetch media from ${url}${redirected}: ${detail}`);
75
- }
76
- const contentLength = res.headers.get("content-length");
77
- if (maxBytes && contentLength) {
78
- const length = Number(contentLength);
79
- if (Number.isFinite(length) && length > maxBytes) {
80
- throw new MediaFetchError("max_bytes", `Failed to fetch media from ${url}: content length ${length} exceeds maxBytes ${maxBytes}`);
98
+ const buffer = maxBytes
99
+ ? await readResponseWithLimit(res, maxBytes)
100
+ : Buffer.from(await res.arrayBuffer());
101
+ let fileNameFromUrl;
102
+ try {
103
+ const parsed = new URL(finalUrl);
104
+ const base = path.basename(parsed.pathname);
105
+ fileNameFromUrl = base || undefined;
81
106
  }
107
+ catch {
108
+ // ignore parse errors; leave undefined
109
+ }
110
+ const headerFileName = parseContentDispositionFileName(res.headers.get("content-disposition"));
111
+ let fileName = headerFileName || fileNameFromUrl || (filePathHint ? path.basename(filePathHint) : undefined);
112
+ const filePathForMime = headerFileName && path.extname(headerFileName) ? headerFileName : (filePathHint ?? finalUrl);
113
+ const contentType = await detectMime({
114
+ buffer,
115
+ headerMime: res.headers.get("content-type"),
116
+ filePath: filePathForMime,
117
+ });
118
+ if (fileName && !path.extname(fileName) && contentType) {
119
+ const ext = extensionForMime(contentType);
120
+ if (ext) {
121
+ fileName = `${fileName}${ext}`;
122
+ }
123
+ }
124
+ return {
125
+ buffer,
126
+ contentType: contentType ?? undefined,
127
+ fileName,
128
+ };
82
129
  }
83
- const buffer = maxBytes
84
- ? await readResponseWithLimit(res, maxBytes)
85
- : Buffer.from(await res.arrayBuffer());
86
- let fileNameFromUrl;
87
- try {
88
- const parsed = new URL(url);
89
- const base = path.basename(parsed.pathname);
90
- fileNameFromUrl = base || undefined;
91
- }
92
- catch {
93
- // ignore parse errors; leave undefined
94
- }
95
- const headerFileName = parseContentDispositionFileName(res.headers.get("content-disposition"));
96
- let fileName = headerFileName || fileNameFromUrl || (filePathHint ? path.basename(filePathHint) : undefined);
97
- const filePathForMime = headerFileName && path.extname(headerFileName) ? headerFileName : (filePathHint ?? url);
98
- const contentType = await detectMime({
99
- buffer,
100
- headerMime: res.headers.get("content-type"),
101
- filePath: filePathForMime,
102
- });
103
- if (fileName && !path.extname(fileName) && contentType) {
104
- const ext = extensionForMime(contentType);
105
- if (ext)
106
- fileName = `${fileName}${ext}`;
130
+ finally {
131
+ if (release) {
132
+ await release();
133
+ }
107
134
  }
108
- return {
109
- buffer,
110
- contentType: contentType ?? undefined,
111
- fileName,
112
- };
113
135
  }
114
136
  async function readResponseWithLimit(res, maxBytes) {
115
137
  const body = res.body;
@@ -126,8 +148,9 @@ async function readResponseWithLimit(res, maxBytes) {
126
148
  try {
127
149
  while (true) {
128
150
  const { done, value } = await reader.read();
129
- if (done)
151
+ if (done) {
130
152
  break;
153
+ }
131
154
  if (value?.length) {
132
155
  total += value.length;
133
156
  if (total > maxBytes) {
@@ -3,6 +3,7 @@ import { createWriteStream } from "node:fs";
3
3
  import fs from "node:fs/promises";
4
4
  import { request as httpRequest } from "node:http";
5
5
  import { request as httpsRequest } from "node:https";
6
+ import os from "node:os";
6
7
  import path from "node:path";
7
8
  import { pipeline } from "node:stream/promises";
8
9
  import { resolveConfigDir } from "../utils.js";
@@ -24,6 +25,7 @@ const DEFAULT_TTL_MS = 2 * 60 * 1000; // 2 minutes
24
25
  const SAFE_PATHS = [
25
26
  "/tmp",
26
27
  "/var/tmp",
28
+ os.tmpdir(), // OS-specific temp directory (e.g. /private/var/folders/... on macOS)
27
29
  process.cwd(), // Current working directory
28
30
  resolveConfigDir(), // PoolBot config directory
29
31
  ];
@@ -1,9 +1,390 @@
1
+ import path from "node:path";
1
2
  import { finalizeInboundContext } from "../auto-reply/reply/inbound-context.js";
2
- import { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody, } from "./format.js";
3
+ import { logVerbose, shouldLogVerbose } from "../globals.js";
4
+ import { DEFAULT_INPUT_FILE_MAX_BYTES, DEFAULT_INPUT_FILE_MAX_CHARS, DEFAULT_INPUT_FILE_MIMES, DEFAULT_INPUT_MAX_REDIRECTS, DEFAULT_INPUT_PDF_MAX_PAGES, DEFAULT_INPUT_PDF_MAX_PIXELS, DEFAULT_INPUT_PDF_MIN_TEXT_CHARS, DEFAULT_INPUT_TIMEOUT_MS, extractFileContentFromSource, normalizeMimeList, normalizeMimeType, } from "../media/input-files.js";
5
+ import { resolveAttachmentKind } from "./attachments.js";
3
6
  import { runWithConcurrency } from "./concurrency.js";
7
+ import { extractMediaUserText, formatAudioTranscripts, formatMediaUnderstandingBody, } from "./format.js";
4
8
  import { resolveConcurrency } from "./resolve.js";
5
9
  import { buildProviderRegistry, createMediaAttachmentCache, normalizeMediaAttachments, runCapability, } from "./runner.js";
6
10
  const CAPABILITY_ORDER = ["image", "audio", "video"];
11
+ const EXTRA_TEXT_MIMES = [
12
+ "application/xml",
13
+ "text/xml",
14
+ "application/x-yaml",
15
+ "text/yaml",
16
+ "application/yaml",
17
+ "application/javascript",
18
+ "text/javascript",
19
+ "text/tab-separated-values",
20
+ ];
21
+ const TEXT_EXT_MIME = new Map([
22
+ [".csv", "text/csv"],
23
+ [".tsv", "text/tab-separated-values"],
24
+ [".txt", "text/plain"],
25
+ [".md", "text/markdown"],
26
+ [".log", "text/plain"],
27
+ [".ini", "text/plain"],
28
+ [".cfg", "text/plain"],
29
+ [".conf", "text/plain"],
30
+ [".env", "text/plain"],
31
+ [".json", "application/json"],
32
+ [".yaml", "text/yaml"],
33
+ [".yml", "text/yaml"],
34
+ [".xml", "application/xml"],
35
+ ]);
36
+ const XML_ESCAPE_MAP = {
37
+ "<": "&lt;",
38
+ ">": "&gt;",
39
+ "&": "&amp;",
40
+ '"': "&quot;",
41
+ "'": "&apos;",
42
+ };
43
+ /**
44
+ * Escapes special XML characters in attribute values to prevent injection.
45
+ */
46
+ function xmlEscapeAttr(value) {
47
+ return value.replace(/[<>&"']/g, (char) => XML_ESCAPE_MAP[char] ?? char);
48
+ }
49
+ function escapeFileBlockContent(value) {
50
+ return value.replace(/<\s*\/\s*file\s*>/gi, "&lt;/file&gt;").replace(/<\s*file\b/gi, "&lt;file");
51
+ }
52
+ function sanitizeMimeType(value) {
53
+ if (!value) {
54
+ return undefined;
55
+ }
56
+ const trimmed = value.trim().toLowerCase();
57
+ if (!trimmed) {
58
+ return undefined;
59
+ }
60
+ const match = trimmed.match(/^([a-z0-9!#$&^_.+-]+\/[a-z0-9!#$&^_.+-]+)/);
61
+ return match?.[1];
62
+ }
63
+ function resolveFileLimits(cfg) {
64
+ const files = cfg.gateway?.http?.endpoints?.responses?.files;
65
+ const allowedMimesConfigured = Boolean(files?.allowedMimes && files.allowedMimes.length > 0);
66
+ return {
67
+ allowUrl: files?.allowUrl ?? true,
68
+ allowedMimes: normalizeMimeList(files?.allowedMimes, DEFAULT_INPUT_FILE_MIMES),
69
+ allowedMimesConfigured,
70
+ maxBytes: files?.maxBytes ?? DEFAULT_INPUT_FILE_MAX_BYTES,
71
+ maxChars: files?.maxChars ?? DEFAULT_INPUT_FILE_MAX_CHARS,
72
+ maxRedirects: files?.maxRedirects ?? DEFAULT_INPUT_MAX_REDIRECTS,
73
+ timeoutMs: files?.timeoutMs ?? DEFAULT_INPUT_TIMEOUT_MS,
74
+ pdf: {
75
+ maxPages: files?.pdf?.maxPages ?? DEFAULT_INPUT_PDF_MAX_PAGES,
76
+ maxPixels: files?.pdf?.maxPixels ?? DEFAULT_INPUT_PDF_MAX_PIXELS,
77
+ minTextChars: files?.pdf?.minTextChars ?? DEFAULT_INPUT_PDF_MIN_TEXT_CHARS,
78
+ },
79
+ };
80
+ }
81
+ function appendFileBlocks(body, blocks) {
82
+ if (!blocks || blocks.length === 0) {
83
+ return body ?? "";
84
+ }
85
+ const base = typeof body === "string" ? body.trim() : "";
86
+ const suffix = blocks.join("\n\n").trim();
87
+ if (!base) {
88
+ return suffix;
89
+ }
90
+ return `${base}\n\n${suffix}`.trim();
91
+ }
92
+ function resolveUtf16Charset(buffer) {
93
+ if (!buffer || buffer.length < 2) {
94
+ return undefined;
95
+ }
96
+ const b0 = buffer[0];
97
+ const b1 = buffer[1];
98
+ if (b0 === 0xff && b1 === 0xfe) {
99
+ return "utf-16le";
100
+ }
101
+ if (b0 === 0xfe && b1 === 0xff) {
102
+ return "utf-16be";
103
+ }
104
+ const sampleLen = Math.min(buffer.length, 2048);
105
+ let zeroEven = 0;
106
+ let zeroOdd = 0;
107
+ for (let i = 0; i < sampleLen; i += 1) {
108
+ if (buffer[i] !== 0) {
109
+ continue;
110
+ }
111
+ if (i % 2 === 0) {
112
+ zeroEven += 1;
113
+ }
114
+ else {
115
+ zeroOdd += 1;
116
+ }
117
+ }
118
+ const zeroCount = zeroEven + zeroOdd;
119
+ if (zeroCount / sampleLen > 0.2) {
120
+ return zeroOdd >= zeroEven ? "utf-16le" : "utf-16be";
121
+ }
122
+ return undefined;
123
+ }
124
+ const WORDISH_CHAR = /[\p{L}\p{N}]/u;
125
+ const CP1252_MAP = [
126
+ "\u20ac",
127
+ undefined,
128
+ "\u201a",
129
+ "\u0192",
130
+ "\u201e",
131
+ "\u2026",
132
+ "\u2020",
133
+ "\u2021",
134
+ "\u02c6",
135
+ "\u2030",
136
+ "\u0160",
137
+ "\u2039",
138
+ "\u0152",
139
+ undefined,
140
+ "\u017d",
141
+ undefined,
142
+ undefined,
143
+ "\u2018",
144
+ "\u2019",
145
+ "\u201c",
146
+ "\u201d",
147
+ "\u2022",
148
+ "\u2013",
149
+ "\u2014",
150
+ "\u02dc",
151
+ "\u2122",
152
+ "\u0161",
153
+ "\u203a",
154
+ "\u0153",
155
+ undefined,
156
+ "\u017e",
157
+ "\u0178",
158
+ ];
159
+ function decodeLegacyText(buffer) {
160
+ let output = "";
161
+ for (const byte of buffer) {
162
+ if (byte >= 0x80 && byte <= 0x9f) {
163
+ const mapped = CP1252_MAP[byte - 0x80];
164
+ output += mapped ?? String.fromCharCode(byte);
165
+ continue;
166
+ }
167
+ output += String.fromCharCode(byte);
168
+ }
169
+ return output;
170
+ }
171
+ function getTextStats(text) {
172
+ if (!text) {
173
+ return { printableRatio: 0, wordishRatio: 0 };
174
+ }
175
+ let printable = 0;
176
+ let control = 0;
177
+ let wordish = 0;
178
+ for (const char of text) {
179
+ const code = char.codePointAt(0) ?? 0;
180
+ if (code === 9 || code === 10 || code === 13 || code === 32) {
181
+ printable += 1;
182
+ wordish += 1;
183
+ continue;
184
+ }
185
+ if (code < 32 || (code >= 0x7f && code <= 0x9f)) {
186
+ control += 1;
187
+ continue;
188
+ }
189
+ printable += 1;
190
+ if (WORDISH_CHAR.test(char)) {
191
+ wordish += 1;
192
+ }
193
+ }
194
+ const total = printable + control;
195
+ if (total === 0) {
196
+ return { printableRatio: 0, wordishRatio: 0 };
197
+ }
198
+ return { printableRatio: printable / total, wordishRatio: wordish / total };
199
+ }
200
+ function isMostlyPrintable(text) {
201
+ return getTextStats(text).printableRatio > 0.85;
202
+ }
203
+ function looksLikeLegacyTextBytes(buffer) {
204
+ if (buffer.length === 0) {
205
+ return false;
206
+ }
207
+ const text = decodeLegacyText(buffer);
208
+ const { printableRatio, wordishRatio } = getTextStats(text);
209
+ return printableRatio > 0.95 && wordishRatio > 0.3;
210
+ }
211
+ function looksLikeUtf8Text(buffer) {
212
+ if (!buffer || buffer.length === 0) {
213
+ return false;
214
+ }
215
+ const sample = buffer.subarray(0, Math.min(buffer.length, 4096));
216
+ try {
217
+ const text = new TextDecoder("utf-8", { fatal: true }).decode(sample);
218
+ return isMostlyPrintable(text);
219
+ }
220
+ catch {
221
+ return looksLikeLegacyTextBytes(sample);
222
+ }
223
+ }
224
+ function decodeTextSample(buffer) {
225
+ if (!buffer || buffer.length === 0) {
226
+ return "";
227
+ }
228
+ const sample = buffer.subarray(0, Math.min(buffer.length, 8192));
229
+ const utf16Charset = resolveUtf16Charset(sample);
230
+ if (utf16Charset === "utf-16be") {
231
+ const swapped = Buffer.alloc(sample.length);
232
+ for (let i = 0; i + 1 < sample.length; i += 2) {
233
+ swapped[i] = sample[i + 1];
234
+ swapped[i + 1] = sample[i];
235
+ }
236
+ return new TextDecoder("utf-16le").decode(swapped);
237
+ }
238
+ if (utf16Charset === "utf-16le") {
239
+ return new TextDecoder("utf-16le").decode(sample);
240
+ }
241
+ return new TextDecoder("utf-8").decode(sample);
242
+ }
243
+ function guessDelimitedMime(text) {
244
+ if (!text) {
245
+ return undefined;
246
+ }
247
+ const line = text.split(/\r?\n/)[0] ?? "";
248
+ const tabs = (line.match(/\t/g) ?? []).length;
249
+ const commas = (line.match(/,/g) ?? []).length;
250
+ if (commas > 0) {
251
+ return "text/csv";
252
+ }
253
+ if (tabs > 0) {
254
+ return "text/tab-separated-values";
255
+ }
256
+ return undefined;
257
+ }
258
+ function resolveTextMimeFromName(name) {
259
+ if (!name) {
260
+ return undefined;
261
+ }
262
+ const ext = path.extname(name).toLowerCase();
263
+ return TEXT_EXT_MIME.get(ext);
264
+ }
265
+ function isBinaryMediaMime(mime) {
266
+ if (!mime) {
267
+ return false;
268
+ }
269
+ return mime.startsWith("image/") || mime.startsWith("audio/") || mime.startsWith("video/");
270
+ }
271
+ async function extractFileBlocks(params) {
272
+ const { attachments, cache, limits, skipAttachmentIndexes } = params;
273
+ if (!attachments || attachments.length === 0) {
274
+ return [];
275
+ }
276
+ const blocks = [];
277
+ for (const attachment of attachments) {
278
+ if (!attachment) {
279
+ continue;
280
+ }
281
+ if (skipAttachmentIndexes?.has(attachment.index)) {
282
+ continue;
283
+ }
284
+ const forcedTextMime = resolveTextMimeFromName(attachment.path ?? attachment.url ?? "");
285
+ const kind = forcedTextMime ? "document" : resolveAttachmentKind(attachment);
286
+ if (!forcedTextMime && (kind === "image" || kind === "video" || kind === "audio")) {
287
+ continue;
288
+ }
289
+ if (!limits.allowUrl && attachment.url && !attachment.path) {
290
+ if (shouldLogVerbose()) {
291
+ logVerbose(`media: file attachment skipped (url disabled) index=${attachment.index}`);
292
+ }
293
+ continue;
294
+ }
295
+ let bufferResult;
296
+ try {
297
+ bufferResult = await cache.getBuffer({
298
+ attachmentIndex: attachment.index,
299
+ maxBytes: limits.maxBytes,
300
+ timeoutMs: limits.timeoutMs,
301
+ });
302
+ }
303
+ catch (err) {
304
+ if (shouldLogVerbose()) {
305
+ logVerbose(`media: file attachment skipped (buffer): ${String(err)}`);
306
+ }
307
+ continue;
308
+ }
309
+ const nameHint = bufferResult?.fileName ?? attachment.path ?? attachment.url;
310
+ const forcedTextMimeResolved = forcedTextMime ?? resolveTextMimeFromName(nameHint ?? "");
311
+ const rawMime = bufferResult?.mime ?? attachment.mime;
312
+ const normalizedRawMime = normalizeMimeType(rawMime);
313
+ if (!forcedTextMimeResolved && isBinaryMediaMime(normalizedRawMime)) {
314
+ continue;
315
+ }
316
+ const utf16Charset = resolveUtf16Charset(bufferResult?.buffer);
317
+ const textSample = decodeTextSample(bufferResult?.buffer);
318
+ const textLike = Boolean(utf16Charset) || looksLikeUtf8Text(bufferResult?.buffer);
319
+ const guessedDelimited = textLike ? guessDelimitedMime(textSample) : undefined;
320
+ const textHint = forcedTextMimeResolved ?? guessedDelimited ?? (textLike ? "text/plain" : undefined);
321
+ const mimeType = sanitizeMimeType(textHint ?? normalizeMimeType(rawMime));
322
+ // Log when MIME type is overridden from non-text to text for auditability
323
+ if (textHint && rawMime && !rawMime.startsWith("text/")) {
324
+ logVerbose(`media: MIME override from "${rawMime}" to "${textHint}" for index=${attachment.index}`);
325
+ }
326
+ if (!mimeType) {
327
+ if (shouldLogVerbose()) {
328
+ logVerbose(`media: file attachment skipped (unknown mime) index=${attachment.index}`);
329
+ }
330
+ continue;
331
+ }
332
+ const allowedMimes = new Set(limits.allowedMimes);
333
+ if (!limits.allowedMimesConfigured) {
334
+ for (const extra of EXTRA_TEXT_MIMES) {
335
+ allowedMimes.add(extra);
336
+ }
337
+ if (mimeType.startsWith("text/")) {
338
+ allowedMimes.add(mimeType);
339
+ }
340
+ }
341
+ if (!allowedMimes.has(mimeType)) {
342
+ if (shouldLogVerbose()) {
343
+ logVerbose(`media: file attachment skipped (unsupported mime ${mimeType}) index=${attachment.index}`);
344
+ }
345
+ continue;
346
+ }
347
+ let extracted;
348
+ try {
349
+ const mediaType = utf16Charset ? `${mimeType}; charset=${utf16Charset}` : mimeType;
350
+ const { allowedMimesConfigured: _allowedMimesConfigured, ...baseLimits } = limits;
351
+ extracted = await extractFileContentFromSource({
352
+ source: {
353
+ type: "base64",
354
+ data: bufferResult.buffer.toString("base64"),
355
+ mediaType,
356
+ filename: bufferResult.fileName,
357
+ },
358
+ limits: {
359
+ ...baseLimits,
360
+ allowedMimes,
361
+ },
362
+ });
363
+ }
364
+ catch (err) {
365
+ if (shouldLogVerbose()) {
366
+ logVerbose(`media: file attachment skipped (extract): ${String(err)}`);
367
+ }
368
+ continue;
369
+ }
370
+ const text = extracted?.text?.trim() ?? "";
371
+ let blockText = text;
372
+ if (!blockText) {
373
+ if (extracted?.images && extracted.images.length > 0) {
374
+ blockText = "[PDF content rendered to images; images not forwarded to model]";
375
+ }
376
+ else {
377
+ blockText = "[No extractable text]";
378
+ }
379
+ }
380
+ const safeName = (bufferResult.fileName ?? `file-${attachment.index + 1}`)
381
+ .replace(/[\r\n\t]+/g, " ")
382
+ .trim();
383
+ // Escape XML special characters in attributes to prevent injection
384
+ blocks.push(`<file name="${xmlEscapeAttr(safeName)}" mime="${xmlEscapeAttr(mimeType)}">\n${escapeFileBlockContent(blockText)}\n</file>`);
385
+ }
386
+ return blocks;
387
+ }
7
388
  export async function applyMediaUnderstanding(params) {
8
389
  const { ctx, cfg } = params;
9
390
  const commandCandidates = [ctx.CommandBody, ctx.RawBody, ctx.Body];
@@ -32,8 +413,9 @@ export async function applyMediaUnderstanding(params) {
32
413
  const outputs = [];
33
414
  const decisions = [];
34
415
  for (const entry of results) {
35
- if (!entry)
416
+ if (!entry) {
36
417
  continue;
418
+ }
37
419
  for (const output of entry.outputs) {
38
420
  outputs.push(output);
39
421
  }
@@ -62,7 +444,24 @@ export async function applyMediaUnderstanding(params) {
62
444
  ctx.RawBody = originalUserText;
63
445
  }
64
446
  ctx.MediaUnderstanding = [...(ctx.MediaUnderstanding ?? []), ...outputs];
65
- finalizeInboundContext(ctx, { forceBodyForAgent: true, forceBodyForCommands: true });
447
+ }
448
+ const audioAttachmentIndexes = new Set(outputs
449
+ .filter((output) => output.kind === "audio.transcription")
450
+ .map((output) => output.attachmentIndex));
451
+ const fileBlocks = await extractFileBlocks({
452
+ attachments,
453
+ cache,
454
+ limits: resolveFileLimits(cfg),
455
+ skipAttachmentIndexes: audioAttachmentIndexes.size > 0 ? audioAttachmentIndexes : undefined,
456
+ });
457
+ if (fileBlocks.length > 0) {
458
+ ctx.Body = appendFileBlocks(ctx.Body, fileBlocks);
459
+ }
460
+ if (outputs.length > 0 || fileBlocks.length > 0) {
461
+ finalizeInboundContext(ctx, {
462
+ forceBodyForAgent: true,
463
+ forceBodyForCommands: outputs.length > 0 || fileBlocks.length > 0,
464
+ });
66
465
  }
67
466
  return {
68
467
  outputs,
@@ -70,6 +469,7 @@ export async function applyMediaUnderstanding(params) {
70
469
  appliedImage: outputs.some((output) => output.kind === "image.description"),
71
470
  appliedAudio: outputs.some((output) => output.kind === "audio.transcription"),
72
471
  appliedVideo: outputs.some((output) => output.kind === "video.description"),
472
+ appliedFile: fileBlocks.length > 0,
73
473
  };
74
474
  }
75
475
  finally {