@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
@@ -3,16 +3,18 @@ import fs from "node:fs/promises";
3
3
  import os from "node:os";
4
4
  import path from "node:path";
5
5
  import { fileURLToPath } from "node:url";
6
+ import { logVerbose, shouldLogVerbose } from "../globals.js";
7
+ import { isAbortError } from "../infra/unhandled-rejections.js";
6
8
  import { fetchRemoteMedia, MediaFetchError } from "../media/fetch.js";
7
9
  import { detectMime, getFileExtension, isAudioFileName, kindFromMime } from "../media/mime.js";
8
- import { logVerbose, shouldLogVerbose } from "../globals.js";
9
- import { fetchWithTimeout } from "./providers/shared.js";
10
10
  import { MediaUnderstandingSkipError } from "./errors.js";
11
+ import { fetchWithTimeout } from "./providers/shared.js";
11
12
  const DEFAULT_MAX_ATTACHMENTS = 1;
12
13
  function normalizeAttachmentPath(raw) {
13
14
  const value = raw?.trim();
14
- if (!value)
15
+ if (!value) {
15
16
  return undefined;
17
+ }
16
18
  if (value.startsWith("file://")) {
17
19
  try {
18
20
  return fileURLToPath(value);
@@ -30,8 +32,9 @@ export function normalizeAttachments(ctx) {
30
32
  const resolveMime = (count, index) => {
31
33
  const typeHint = typesFromArray?.[index];
32
34
  const trimmed = typeof typeHint === "string" ? typeHint.trim() : "";
33
- if (trimmed)
35
+ if (trimmed) {
34
36
  return trimmed;
37
+ }
35
38
  return count === 1 ? ctx.MediaType : undefined;
36
39
  };
37
40
  if (pathsFromArray && pathsFromArray.length > 0) {
@@ -59,8 +62,9 @@ export function normalizeAttachments(ctx) {
59
62
  }
60
63
  const pathValue = ctx.MediaPath?.trim();
61
64
  const url = ctx.MediaUrl?.trim();
62
- if (!pathValue && !url)
65
+ if (!pathValue && !url) {
63
66
  return [];
67
+ }
64
68
  return [
65
69
  {
66
70
  path: pathValue || undefined,
@@ -72,15 +76,19 @@ export function normalizeAttachments(ctx) {
72
76
  }
73
77
  export function resolveAttachmentKind(attachment) {
74
78
  const kind = kindFromMime(attachment.mime);
75
- if (kind === "image" || kind === "audio" || kind === "video")
79
+ if (kind === "image" || kind === "audio" || kind === "video") {
76
80
  return kind;
81
+ }
77
82
  const ext = getFileExtension(attachment.path ?? attachment.url);
78
- if (!ext)
83
+ if (!ext) {
79
84
  return "unknown";
80
- if ([".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"].includes(ext))
85
+ }
86
+ if ([".mp4", ".mov", ".mkv", ".webm", ".avi", ".m4v"].includes(ext)) {
81
87
  return "video";
82
- if (isAudioFileName(attachment.path ?? attachment.url))
88
+ }
89
+ if (isAudioFileName(attachment.path ?? attachment.url)) {
83
90
  return "audio";
91
+ }
84
92
  if ([".png", ".jpg", ".jpeg", ".webp", ".gif", ".bmp", ".tiff", ".tif"].includes(ext)) {
85
93
  return "image";
86
94
  }
@@ -95,25 +103,22 @@ export function isAudioAttachment(attachment) {
95
103
  export function isImageAttachment(attachment) {
96
104
  return resolveAttachmentKind(attachment) === "image";
97
105
  }
98
- function isAbortError(err) {
99
- if (!err)
100
- return false;
101
- if (err instanceof Error && err.name === "AbortError")
102
- return true;
103
- return false;
104
- }
105
106
  function resolveRequestUrl(input) {
106
- if (typeof input === "string")
107
+ if (typeof input === "string") {
107
108
  return input;
108
- if (input instanceof URL)
109
+ }
110
+ if (input instanceof URL) {
109
111
  return input.toString();
112
+ }
110
113
  return input.url;
111
114
  }
112
115
  function orderAttachments(attachments, prefer) {
113
- if (!prefer || prefer === "first")
116
+ if (!prefer || prefer === "first") {
114
117
  return attachments;
115
- if (prefer === "last")
116
- return [...attachments].reverse();
118
+ }
119
+ if (prefer === "last") {
120
+ return [...attachments].toReversed();
121
+ }
117
122
  if (prefer === "path") {
118
123
  const withPath = attachments.filter((item) => item.path);
119
124
  const withoutPath = attachments.filter((item) => !item.path);
@@ -129,14 +134,17 @@ function orderAttachments(attachments, prefer) {
129
134
  export function selectAttachments(params) {
130
135
  const { capability, attachments, policy } = params;
131
136
  const matches = attachments.filter((item) => {
132
- if (capability === "image")
137
+ if (capability === "image") {
133
138
  return isImageAttachment(item);
134
- if (capability === "audio")
139
+ }
140
+ if (capability === "audio") {
135
141
  return isAudioAttachment(item);
142
+ }
136
143
  return isVideoAttachment(item);
137
144
  });
138
- if (matches.length === 0)
145
+ if (matches.length === 0) {
139
146
  return [];
147
+ }
140
148
  const ordered = orderAttachments(matches, policy?.prefer);
141
149
  const mode = policy?.mode ?? "first";
142
150
  const maxAttachments = policy?.maxAttachments ?? DEFAULT_MAX_ATTACHMENTS;
@@ -291,15 +299,18 @@ export class MediaAttachmentCache {
291
299
  }
292
300
  resolveLocalPath(attachment) {
293
301
  const rawPath = normalizeAttachmentPath(attachment.path);
294
- if (!rawPath)
302
+ if (!rawPath) {
295
303
  return undefined;
304
+ }
296
305
  return path.isAbsolute(rawPath) ? rawPath : path.resolve(rawPath);
297
306
  }
298
307
  async ensureLocalStat(entry) {
299
- if (!entry.resolvedPath)
308
+ if (!entry.resolvedPath) {
300
309
  return undefined;
301
- if (entry.statSize !== undefined)
310
+ }
311
+ if (entry.statSize !== undefined) {
302
312
  return entry.statSize;
313
+ }
303
314
  try {
304
315
  const stat = await fs.stat(entry.resolvedPath);
305
316
  if (!stat.isFile()) {
@@ -26,5 +26,21 @@ export const DEFAULT_AUDIO_MODELS = {
26
26
  openai: "gpt-4o-mini-transcribe",
27
27
  deepgram: "nova-3",
28
28
  };
29
+ export const AUTO_AUDIO_KEY_PROVIDERS = ["openai", "groq", "deepgram", "google"];
30
+ export const AUTO_IMAGE_KEY_PROVIDERS = [
31
+ "openai",
32
+ "anthropic",
33
+ "google",
34
+ "minimax",
35
+ "zai",
36
+ ];
37
+ export const AUTO_VIDEO_KEY_PROVIDERS = ["google"];
38
+ export const DEFAULT_IMAGE_MODELS = {
39
+ openai: "gpt-5-mini",
40
+ anthropic: "claude-opus-4-6",
41
+ google: "gemini-3-flash-preview",
42
+ minimax: "MiniMax-VL-01",
43
+ zai: "glm-4.6v",
44
+ };
29
45
  export const CLI_OUTPUT_MAX_BUFFER = 5 * MB;
30
46
  export const DEFAULT_MEDIA_CONCURRENCY = 2;
@@ -1,4 +1,4 @@
1
- import { fetchWithTimeout, normalizeBaseUrl, readErrorResponse } from "../shared.js";
1
+ import { fetchWithTimeoutGuarded, normalizeBaseUrl, readErrorResponse } from "../shared.js";
2
2
  export const DEFAULT_DEEPGRAM_AUDIO_BASE_URL = "https://api.deepgram.com/v1";
3
3
  export const DEFAULT_DEEPGRAM_AUDIO_MODEL = "nova-3";
4
4
  function resolveModel(model) {
@@ -8,15 +8,18 @@ function resolveModel(model) {
8
8
  export async function transcribeDeepgramAudio(params) {
9
9
  const fetchFn = params.fetchFn ?? fetch;
10
10
  const baseUrl = normalizeBaseUrl(params.baseUrl, DEFAULT_DEEPGRAM_AUDIO_BASE_URL);
11
+ const allowPrivate = Boolean(params.baseUrl?.trim());
11
12
  const model = resolveModel(params.model);
12
13
  const url = new URL(`${baseUrl}/listen`);
13
14
  url.searchParams.set("model", model);
14
- if (params.language?.trim())
15
+ if (params.language?.trim()) {
15
16
  url.searchParams.set("language", params.language.trim());
17
+ }
16
18
  if (params.query) {
17
19
  for (const [key, value] of Object.entries(params.query)) {
18
- if (value === undefined)
20
+ if (value === undefined) {
19
21
  continue;
22
+ }
20
23
  url.searchParams.set(key, String(value));
21
24
  }
22
25
  }
@@ -28,20 +31,25 @@ export async function transcribeDeepgramAudio(params) {
28
31
  headers.set("content-type", params.mime ?? "application/octet-stream");
29
32
  }
30
33
  const body = new Uint8Array(params.buffer);
31
- const res = await fetchWithTimeout(url.toString(), {
34
+ const { response: res, release } = await fetchWithTimeoutGuarded(url.toString(), {
32
35
  method: "POST",
33
36
  headers,
34
37
  body,
35
- }, params.timeoutMs, fetchFn);
36
- if (!res.ok) {
37
- const detail = await readErrorResponse(res);
38
- const suffix = detail ? `: ${detail}` : "";
39
- throw new Error(`Audio transcription failed (HTTP ${res.status})${suffix}`);
38
+ }, params.timeoutMs, fetchFn, allowPrivate ? { ssrfPolicy: { allowPrivateNetwork: true } } : undefined);
39
+ try {
40
+ if (!res.ok) {
41
+ const detail = await readErrorResponse(res);
42
+ const suffix = detail ? `: ${detail}` : "";
43
+ throw new Error(`Audio transcription failed (HTTP ${res.status})${suffix}`);
44
+ }
45
+ const payload = (await res.json());
46
+ const transcript = payload.results?.channels?.[0]?.alternatives?.[0]?.transcript?.trim();
47
+ if (!transcript) {
48
+ throw new Error("Audio transcription response missing transcript");
49
+ }
50
+ return { text: transcript, model };
40
51
  }
41
- const payload = (await res.json());
42
- const transcript = payload.results?.channels?.[0]?.alternatives?.[0]?.transcript?.trim();
43
- if (!transcript) {
44
- throw new Error("Audio transcription response missing transcript");
52
+ finally {
53
+ await release();
45
54
  }
46
- return { text: transcript, model };
47
55
  }
@@ -1,12 +1,13 @@
1
1
  import { normalizeGoogleModelId } from "../../../agents/models-config.providers.js";
2
- import { fetchWithTimeout, normalizeBaseUrl, readErrorResponse } from "../shared.js";
2
+ import { fetchWithTimeoutGuarded, normalizeBaseUrl, readErrorResponse } from "../shared.js";
3
3
  export const DEFAULT_GOOGLE_AUDIO_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
4
4
  const DEFAULT_GOOGLE_AUDIO_MODEL = "gemini-3-flash-preview";
5
5
  const DEFAULT_GOOGLE_AUDIO_PROMPT = "Transcribe the audio.";
6
6
  function resolveModel(model) {
7
7
  const trimmed = model?.trim();
8
- if (!trimmed)
8
+ if (!trimmed) {
9
9
  return DEFAULT_GOOGLE_AUDIO_MODEL;
10
+ }
10
11
  return normalizeGoogleModelId(trimmed);
11
12
  }
12
13
  function resolvePrompt(prompt) {
@@ -16,6 +17,7 @@ function resolvePrompt(prompt) {
16
17
  export async function transcribeGeminiAudio(params) {
17
18
  const fetchFn = params.fetchFn ?? fetch;
18
19
  const baseUrl = normalizeBaseUrl(params.baseUrl, DEFAULT_GOOGLE_AUDIO_BASE_URL);
20
+ const allowPrivate = Boolean(params.baseUrl?.trim());
19
21
  const model = resolveModel(params.model);
20
22
  const url = `${baseUrl}/models/${model}:generateContent`;
21
23
  const headers = new Headers(params.headers);
@@ -41,24 +43,29 @@ export async function transcribeGeminiAudio(params) {
41
43
  },
42
44
  ],
43
45
  };
44
- const res = await fetchWithTimeout(url, {
46
+ const { response: res, release } = await fetchWithTimeoutGuarded(url, {
45
47
  method: "POST",
46
48
  headers,
47
49
  body: JSON.stringify(body),
48
- }, params.timeoutMs, fetchFn);
49
- if (!res.ok) {
50
- const detail = await readErrorResponse(res);
51
- const suffix = detail ? `: ${detail}` : "";
52
- throw new Error(`Audio transcription failed (HTTP ${res.status})${suffix}`);
50
+ }, params.timeoutMs, fetchFn, allowPrivate ? { ssrfPolicy: { allowPrivateNetwork: true } } : undefined);
51
+ try {
52
+ if (!res.ok) {
53
+ const detail = await readErrorResponse(res);
54
+ const suffix = detail ? `: ${detail}` : "";
55
+ throw new Error(`Audio transcription failed (HTTP ${res.status})${suffix}`);
56
+ }
57
+ const payload = (await res.json());
58
+ const parts = payload.candidates?.[0]?.content?.parts ?? [];
59
+ const text = parts
60
+ .map((part) => part?.text?.trim())
61
+ .filter(Boolean)
62
+ .join("\n");
63
+ if (!text) {
64
+ throw new Error("Audio transcription response missing text");
65
+ }
66
+ return { text, model };
53
67
  }
54
- const payload = (await res.json());
55
- const parts = payload.candidates?.[0]?.content?.parts ?? [];
56
- const text = parts
57
- .map((part) => part?.text?.trim())
58
- .filter(Boolean)
59
- .join("\n");
60
- if (!text) {
61
- throw new Error("Audio transcription response missing text");
68
+ finally {
69
+ await release();
62
70
  }
63
- return { text, model };
64
71
  }
@@ -1,12 +1,13 @@
1
1
  import { normalizeGoogleModelId } from "../../../agents/models-config.providers.js";
2
- import { fetchWithTimeout, normalizeBaseUrl, readErrorResponse } from "../shared.js";
2
+ import { fetchWithTimeoutGuarded, normalizeBaseUrl, readErrorResponse } from "../shared.js";
3
3
  export const DEFAULT_GOOGLE_VIDEO_BASE_URL = "https://generativelanguage.googleapis.com/v1beta";
4
4
  const DEFAULT_GOOGLE_VIDEO_MODEL = "gemini-3-flash-preview";
5
5
  const DEFAULT_GOOGLE_VIDEO_PROMPT = "Describe the video.";
6
6
  function resolveModel(model) {
7
7
  const trimmed = model?.trim();
8
- if (!trimmed)
8
+ if (!trimmed) {
9
9
  return DEFAULT_GOOGLE_VIDEO_MODEL;
10
+ }
10
11
  return normalizeGoogleModelId(trimmed);
11
12
  }
12
13
  function resolvePrompt(prompt) {
@@ -16,6 +17,7 @@ function resolvePrompt(prompt) {
16
17
  export async function describeGeminiVideo(params) {
17
18
  const fetchFn = params.fetchFn ?? fetch;
18
19
  const baseUrl = normalizeBaseUrl(params.baseUrl, DEFAULT_GOOGLE_VIDEO_BASE_URL);
20
+ const allowPrivate = Boolean(params.baseUrl?.trim());
19
21
  const model = resolveModel(params.model);
20
22
  const url = `${baseUrl}/models/${model}:generateContent`;
21
23
  const headers = new Headers(params.headers);
@@ -41,24 +43,29 @@ export async function describeGeminiVideo(params) {
41
43
  },
42
44
  ],
43
45
  };
44
- const res = await fetchWithTimeout(url, {
46
+ const { response: res, release } = await fetchWithTimeoutGuarded(url, {
45
47
  method: "POST",
46
48
  headers,
47
49
  body: JSON.stringify(body),
48
- }, params.timeoutMs, fetchFn);
49
- if (!res.ok) {
50
- const detail = await readErrorResponse(res);
51
- const suffix = detail ? `: ${detail}` : "";
52
- throw new Error(`Video description failed (HTTP ${res.status})${suffix}`);
50
+ }, params.timeoutMs, fetchFn, allowPrivate ? { ssrfPolicy: { allowPrivateNetwork: true } } : undefined);
51
+ try {
52
+ if (!res.ok) {
53
+ const detail = await readErrorResponse(res);
54
+ const suffix = detail ? `: ${detail}` : "";
55
+ throw new Error(`Video description failed (HTTP ${res.status})${suffix}`);
56
+ }
57
+ const payload = (await res.json());
58
+ const parts = payload.candidates?.[0]?.content?.parts ?? [];
59
+ const text = parts
60
+ .map((part) => part?.text?.trim())
61
+ .filter(Boolean)
62
+ .join("\n");
63
+ if (!text) {
64
+ throw new Error("Video description response missing text");
65
+ }
66
+ return { text, model };
53
67
  }
54
- const payload = (await res.json());
55
- const parts = payload.candidates?.[0]?.content?.parts ?? [];
56
- const text = parts
57
- .map((part) => part?.text?.trim())
58
- .filter(Boolean)
59
- .join("\n");
60
- if (!text) {
61
- throw new Error("Video description response missing text");
68
+ finally {
69
+ await release();
62
70
  }
63
- return { text, model };
64
71
  }
@@ -1,5 +1,5 @@
1
1
  import { complete } from "@mariozechner/pi-ai";
2
- import { discoverAuthStorage, discoverModels } from "@mariozechner/pi-coding-agent";
2
+ import { discoverAuthStorage, discoverModels } from "../../agents/pi-model-discovery.js";
3
3
  import { getApiKeyForModel, requireApiKey } from "../../agents/model-auth.js";
4
4
  import { ensurePoolbotModelsJson } from "../../agents/models-config.js";
5
5
  import { minimaxUnderstandImage } from "../../agents/minimax-vlm.js";
@@ -46,10 +46,10 @@ export async function describeImageWithModel(params) {
46
46
  },
47
47
  ],
48
48
  };
49
- const message = (await complete(model, context, {
49
+ const message = await complete(model, context, {
50
50
  apiKey,
51
51
  maxTokens: params.maxTokens ?? 512,
52
- }));
52
+ });
53
53
  const text = coerceImageAssistantText({
54
54
  message,
55
55
  provider: model.provider,
@@ -5,18 +5,21 @@ import { googleProvider } from "./google/index.js";
5
5
  import { groqProvider } from "./groq/index.js";
6
6
  import { minimaxProvider } from "./minimax/index.js";
7
7
  import { openaiProvider } from "./openai/index.js";
8
+ import { zaiProvider } from "./zai/index.js";
8
9
  const PROVIDERS = [
9
10
  groqProvider,
10
11
  openaiProvider,
11
12
  googleProvider,
12
13
  anthropicProvider,
13
14
  minimaxProvider,
15
+ zaiProvider,
14
16
  deepgramProvider,
15
17
  ];
16
18
  export function normalizeMediaProviderId(id) {
17
19
  const normalized = normalizeProviderId(id);
18
- if (normalized === "gemini")
20
+ if (normalized === "gemini") {
19
21
  return "google";
22
+ }
20
23
  return normalized;
21
24
  }
22
25
  export function buildMediaUnderstandingRegistry(overrides) {
@@ -1,5 +1,5 @@
1
1
  import path from "node:path";
2
- import { fetchWithTimeout, normalizeBaseUrl, readErrorResponse } from "../shared.js";
2
+ import { fetchWithTimeoutGuarded, normalizeBaseUrl, readErrorResponse } from "../shared.js";
3
3
  export const DEFAULT_OPENAI_AUDIO_BASE_URL = "https://api.openai.com/v1";
4
4
  const DEFAULT_OPENAI_AUDIO_MODEL = "gpt-4o-mini-transcribe";
5
5
  function resolveModel(model) {
@@ -9,6 +9,7 @@ function resolveModel(model) {
9
9
  export async function transcribeOpenAiCompatibleAudio(params) {
10
10
  const fetchFn = params.fetchFn ?? fetch;
11
11
  const baseUrl = normalizeBaseUrl(params.baseUrl, DEFAULT_OPENAI_AUDIO_BASE_URL);
12
+ const allowPrivate = Boolean(params.baseUrl?.trim());
12
13
  const url = `${baseUrl}/audio/transcriptions`;
13
14
  const model = resolveModel(params.model);
14
15
  const form = new FormData();
@@ -19,28 +20,35 @@ export async function transcribeOpenAiCompatibleAudio(params) {
19
20
  });
20
21
  form.append("file", blob, fileName);
21
22
  form.append("model", model);
22
- if (params.language?.trim())
23
+ if (params.language?.trim()) {
23
24
  form.append("language", params.language.trim());
24
- if (params.prompt?.trim())
25
+ }
26
+ if (params.prompt?.trim()) {
25
27
  form.append("prompt", params.prompt.trim());
28
+ }
26
29
  const headers = new Headers(params.headers);
27
30
  if (!headers.has("authorization")) {
28
31
  headers.set("authorization", `Bearer ${params.apiKey}`);
29
32
  }
30
- const res = await fetchWithTimeout(url, {
33
+ const { response: res, release } = await fetchWithTimeoutGuarded(url, {
31
34
  method: "POST",
32
35
  headers,
33
36
  body: form,
34
- }, params.timeoutMs, fetchFn);
35
- if (!res.ok) {
36
- const detail = await readErrorResponse(res);
37
- const suffix = detail ? `: ${detail}` : "";
38
- throw new Error(`Audio transcription failed (HTTP ${res.status})${suffix}`);
37
+ }, params.timeoutMs, fetchFn, allowPrivate ? { ssrfPolicy: { allowPrivateNetwork: true } } : undefined);
38
+ try {
39
+ if (!res.ok) {
40
+ const detail = await readErrorResponse(res);
41
+ const suffix = detail ? `: ${detail}` : "";
42
+ throw new Error(`Audio transcription failed (HTTP ${res.status})${suffix}`);
43
+ }
44
+ const payload = (await res.json());
45
+ const text = payload.text?.trim();
46
+ if (!text) {
47
+ throw new Error("Audio transcription response missing text");
48
+ }
49
+ return { text, model };
39
50
  }
40
- const payload = (await res.json());
41
- const text = payload.text?.trim();
42
- if (!text) {
43
- throw new Error("Audio transcription response missing text");
51
+ finally {
52
+ await release();
44
53
  }
45
- return { text, model };
46
54
  }
@@ -1,26 +1,31 @@
1
+ import { fetchWithSsrFGuard } from "../../infra/net/fetch-guard.js";
2
+ export { fetchWithTimeout } from "../../utils/fetch-timeout.js";
1
3
  const MAX_ERROR_CHARS = 300;
2
4
  export function normalizeBaseUrl(baseUrl, fallback) {
3
5
  const raw = baseUrl?.trim() || fallback;
4
6
  return raw.replace(/\/+$/, "");
5
7
  }
6
- export async function fetchWithTimeout(url, init, timeoutMs, fetchFn) {
7
- const controller = new AbortController();
8
- const timer = setTimeout(() => controller.abort(), Math.max(1, timeoutMs));
9
- try {
10
- return await fetchFn(url, { ...init, signal: controller.signal });
11
- }
12
- finally {
13
- clearTimeout(timer);
14
- }
8
+ export async function fetchWithTimeoutGuarded(url, init, timeoutMs, fetchFn, options) {
9
+ return await fetchWithSsrFGuard({
10
+ url,
11
+ fetchImpl: fetchFn,
12
+ init,
13
+ timeoutMs,
14
+ policy: options?.ssrfPolicy,
15
+ lookupFn: options?.lookupFn,
16
+ pinDns: options?.pinDns,
17
+ });
15
18
  }
16
19
  export async function readErrorResponse(res) {
17
20
  try {
18
21
  const text = await res.text();
19
22
  const collapsed = text.replace(/\s+/g, " ").trim();
20
- if (!collapsed)
23
+ if (!collapsed) {
21
24
  return undefined;
22
- if (collapsed.length <= MAX_ERROR_CHARS)
25
+ }
26
+ if (collapsed.length <= MAX_ERROR_CHARS) {
23
27
  return collapsed;
28
+ }
24
29
  return `${collapsed.slice(0, MAX_ERROR_CHARS)}…`;
25
30
  }
26
31
  catch {
@@ -0,0 +1,6 @@
1
+ import { describeImageWithModel } from "../image.js";
2
+ export const zaiProvider = {
3
+ id: "zai",
4
+ capabilities: ["image"],
5
+ describeImage: describeImageWithModel,
6
+ };