@poolzin/pool-bot 2026.2.23 → 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 (235) hide show
  1. package/CHANGELOG.md +29 -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/onboard-helpers.js +4 -4
  63. package/dist/commands/sessions.test-helpers.js +61 -0
  64. package/dist/compat/legacy-names.js +2 -2
  65. package/dist/config/commands.js +3 -0
  66. package/dist/config/config.js +1 -1
  67. package/dist/config/env-substitution.js +62 -34
  68. package/dist/config/env-vars.js +9 -0
  69. package/dist/config/io.js +571 -171
  70. package/dist/config/merge-patch.js +50 -4
  71. package/dist/config/redact-snapshot.js +404 -76
  72. package/dist/config/schema.js +58 -570
  73. package/dist/config/validation.js +140 -85
  74. package/dist/config/zod-schema.hooks.js +40 -11
  75. package/dist/config/zod-schema.installs.js +20 -0
  76. package/dist/config/zod-schema.js +8 -7
  77. package/dist/control-ui/assets/{index-HRr1grwl.js → index-Dvkl4Xlx.js} +2 -1
  78. package/dist/control-ui/assets/{index-HRr1grwl.js.map → index-Dvkl4Xlx.js.map} +1 -1
  79. package/dist/control-ui/index.html +1 -1
  80. package/dist/daemon/cmd-argv.js +21 -0
  81. package/dist/daemon/cmd-set.js +58 -0
  82. package/dist/daemon/service-types.js +1 -0
  83. package/dist/discord/monitor/exec-approvals.js +357 -162
  84. package/dist/gateway/auth.js +38 -3
  85. package/dist/gateway/call.js +149 -68
  86. package/dist/gateway/canvas-capability.js +75 -0
  87. package/dist/gateway/control-plane-audit.js +28 -0
  88. package/dist/gateway/control-plane-rate-limit.js +53 -0
  89. package/dist/gateway/events.js +1 -0
  90. package/dist/gateway/hooks.js +109 -54
  91. package/dist/gateway/http-common.js +22 -0
  92. package/dist/gateway/method-scopes.js +169 -0
  93. package/dist/gateway/net.js +23 -0
  94. package/dist/gateway/openresponses-http.js +120 -110
  95. package/dist/gateway/probe-auth.js +2 -0
  96. package/dist/gateway/protocol/index.js +3 -2
  97. package/dist/gateway/protocol/schema/protocol-schemas.js +2 -0
  98. package/dist/gateway/protocol/schema/push.js +18 -0
  99. package/dist/gateway/protocol/schema.js +1 -0
  100. package/dist/gateway/server-http.js +236 -52
  101. package/dist/gateway/server-methods/agent.js +162 -24
  102. package/dist/gateway/server-methods/chat.js +461 -130
  103. package/dist/gateway/server-methods/config.js +193 -150
  104. package/dist/gateway/server-methods/nodes.helpers.js +12 -0
  105. package/dist/gateway/server-methods/nodes.js +251 -69
  106. package/dist/gateway/server-methods/push.js +53 -0
  107. package/dist/gateway/server-reload-handlers.js +2 -3
  108. package/dist/gateway/server-runtime-config.js +5 -0
  109. package/dist/gateway/server-runtime-state.js +2 -0
  110. package/dist/gateway/server-ws-runtime.js +1 -0
  111. package/dist/gateway/server.impl.js +296 -139
  112. package/dist/gateway/session-preview.test-helpers.js +11 -0
  113. package/dist/gateway/startup-auth.js +126 -0
  114. package/dist/gateway/test-helpers.agent-results.js +15 -0
  115. package/dist/gateway/test-helpers.mocks.js +37 -14
  116. package/dist/gateway/test-helpers.server.js +161 -77
  117. package/dist/hooks/bundled/session-memory/handler.js +165 -34
  118. package/dist/hooks/gmail-watcher-lifecycle.js +23 -0
  119. package/dist/infra/archive-path.js +49 -0
  120. package/dist/infra/device-pairing.js +148 -167
  121. package/dist/infra/exec-approvals-allowlist.js +19 -70
  122. package/dist/infra/exec-approvals-analysis.js +44 -17
  123. package/dist/infra/exec-safe-bin-policy.js +269 -0
  124. package/dist/infra/fixed-window-rate-limit.js +33 -0
  125. package/dist/infra/git-root.js +61 -0
  126. package/dist/infra/heartbeat-active-hours.js +2 -2
  127. package/dist/infra/heartbeat-reason.js +40 -0
  128. package/dist/infra/heartbeat-runner.js +72 -32
  129. package/dist/infra/install-source-utils.js +91 -7
  130. package/dist/infra/node-pairing.js +50 -105
  131. package/dist/infra/npm-integrity.js +45 -0
  132. package/dist/infra/npm-pack-install.js +40 -0
  133. package/dist/infra/outbound/channel-adapters.js +20 -7
  134. package/dist/infra/outbound/message-action-runner.js +107 -327
  135. package/dist/infra/outbound/message.js +59 -36
  136. package/dist/infra/outbound/outbound-policy.js +52 -25
  137. package/dist/infra/outbound/outbound-send-service.js +58 -71
  138. package/dist/infra/pairing-files.js +10 -0
  139. package/dist/infra/plain-object.js +9 -0
  140. package/dist/infra/push-apns.js +365 -0
  141. package/dist/infra/restart-sentinel.js +16 -1
  142. package/dist/infra/restart.js +229 -26
  143. package/dist/infra/scp-host.js +54 -0
  144. package/dist/infra/update-startup.js +86 -9
  145. package/dist/media/inbound-path-policy.js +114 -0
  146. package/dist/media/input-files.js +16 -0
  147. package/dist/memory/test-manager.js +8 -0
  148. package/dist/plugin-sdk/temp-path.js +47 -0
  149. package/dist/plugins/discovery.js +217 -23
  150. package/dist/plugins/hook-runner-global.js +16 -0
  151. package/dist/plugins/loader.js +192 -26
  152. package/dist/plugins/logger.js +8 -0
  153. package/dist/plugins/manifest-registry.js +3 -0
  154. package/dist/plugins/path-safety.js +34 -0
  155. package/dist/plugins/registry.js +5 -2
  156. package/dist/plugins/runtime/index.js +271 -206
  157. package/dist/providers/github-copilot-models.js +4 -1
  158. package/dist/security/audit-channel.js +8 -19
  159. package/dist/security/audit-extra.async.js +354 -182
  160. package/dist/security/audit-extra.js +11 -1
  161. package/dist/security/audit-extra.sync.js +340 -33
  162. package/dist/security/audit-fs.js +31 -13
  163. package/dist/security/audit.js +145 -371
  164. package/dist/security/dm-policy-shared.js +24 -0
  165. package/dist/security/external-content.js +20 -8
  166. package/dist/security/fix.js +49 -85
  167. package/dist/security/scan-paths.js +20 -0
  168. package/dist/security/secret-equal.js +3 -7
  169. package/dist/security/windows-acl.js +30 -15
  170. package/dist/shared/node-list-parse.js +13 -0
  171. package/dist/shared/operator-scope-compat.js +37 -0
  172. package/dist/shared/text-chunking.js +29 -0
  173. package/dist/slack/blocks.test-helpers.js +31 -0
  174. package/dist/slack/monitor/mrkdwn.js +8 -0
  175. package/dist/telegram/bot-message-dispatch.js +366 -164
  176. package/dist/telegram/draft-stream.js +30 -7
  177. package/dist/telegram/reasoning-lane-coordinator.js +128 -0
  178. package/dist/terminal/prompt-select-styled.js +9 -0
  179. package/dist/test-utils/command-runner.js +6 -0
  180. package/dist/test-utils/internal-hook-event-payload.js +10 -0
  181. package/dist/test-utils/model-auth-mock.js +12 -0
  182. package/dist/test-utils/provider-usage-fetch.js +14 -0
  183. package/dist/test-utils/temp-home.js +33 -0
  184. package/dist/tui/components/chat-log.js +9 -0
  185. package/dist/tui/tui-command-handlers.js +36 -27
  186. package/dist/tui/tui-event-handlers.js +122 -32
  187. package/dist/tui/tui.js +181 -45
  188. package/dist/utils/mask-api-key.js +10 -0
  189. package/dist/utils/run-with-concurrency.js +39 -0
  190. package/dist/web/media.js +4 -0
  191. package/docs/tools/slash-commands.md +5 -1
  192. package/extensions/bluebubbles/package.json +1 -1
  193. package/extensions/copilot-proxy/package.json +1 -1
  194. package/extensions/diagnostics-otel/package.json +1 -1
  195. package/extensions/discord/package.json +1 -1
  196. package/extensions/feishu/package.json +1 -1
  197. package/extensions/feishu/src/external-keys.ts +19 -0
  198. package/extensions/google-antigravity-auth/package.json +1 -1
  199. package/extensions/google-gemini-cli-auth/package.json +1 -1
  200. package/extensions/googlechat/package.json +1 -1
  201. package/extensions/imessage/package.json +1 -1
  202. package/extensions/irc/package.json +1 -1
  203. package/extensions/line/package.json +1 -1
  204. package/extensions/llm-task/package.json +1 -1
  205. package/extensions/lobster/package.json +1 -1
  206. package/extensions/lobster/src/windows-spawn.ts +193 -0
  207. package/extensions/matrix/CHANGELOG.md +5 -0
  208. package/extensions/matrix/package.json +1 -1
  209. package/extensions/matrix/src/matrix/actions/limits.ts +6 -0
  210. package/extensions/mattermost/package.json +1 -1
  211. package/extensions/mattermost/src/mattermost/reactions.test-helpers.ts +83 -0
  212. package/extensions/memory-core/package.json +1 -1
  213. package/extensions/memory-lancedb/package.json +1 -1
  214. package/extensions/minimax-portal-auth/package.json +1 -1
  215. package/extensions/msteams/CHANGELOG.md +5 -0
  216. package/extensions/msteams/package.json +1 -1
  217. package/extensions/nextcloud-talk/package.json +1 -1
  218. package/extensions/nostr/CHANGELOG.md +5 -0
  219. package/extensions/nostr/package.json +1 -1
  220. package/extensions/open-prose/package.json +1 -1
  221. package/extensions/openai-codex-auth/package.json +1 -1
  222. package/extensions/signal/package.json +1 -1
  223. package/extensions/slack/package.json +1 -1
  224. package/extensions/telegram/package.json +1 -1
  225. package/extensions/tlon/package.json +1 -1
  226. package/extensions/twitch/CHANGELOG.md +5 -0
  227. package/extensions/twitch/package.json +1 -1
  228. package/extensions/voice-call/CHANGELOG.md +5 -0
  229. package/extensions/voice-call/package.json +1 -1
  230. package/extensions/whatsapp/package.json +1 -1
  231. package/extensions/zalo/CHANGELOG.md +5 -0
  232. package/extensions/zalo/package.json +1 -1
  233. package/extensions/zalouser/CHANGELOG.md +5 -0
  234. package/extensions/zalouser/package.json +1 -1
  235. package/package.json +1 -1
@@ -1,10 +1,8 @@
1
- import fs from "node:fs/promises";
2
1
  import path from "node:path";
3
- import { complete, } from "@mariozechner/pi-ai";
4
- import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
2
+ import { complete } from "@mariozechner/pi-ai";
5
3
  import { Type } from "@sinclair/typebox";
6
4
  import { resolveUserPath } from "../../utils.js";
7
- import { loadWebMedia } from "../../web/media.js";
5
+ import { getDefaultLocalRoots, loadWebMedia } from "../../web/media.js";
8
6
  import { ensureAuthProfileStore, listProfilesForProvider } from "../auth-profiles.js";
9
7
  import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../defaults.js";
10
8
  import { minimaxUnderstandImage } from "../minimax-vlm.js";
@@ -12,13 +10,26 @@ import { getApiKeyForModel, requireApiKey, resolveEnvApiKey } from "../model-aut
12
10
  import { runWithImageModelFallback } from "../model-fallback.js";
13
11
  import { resolveConfiguredModelRef } from "../model-selection.js";
14
12
  import { ensurePoolbotModelsJson } from "../models-config.js";
15
- import { assertSandboxPath } from "../sandbox-paths.js";
13
+ import { discoverAuthStorage, discoverModels } from "../pi-model-discovery.js";
14
+ import { normalizeWorkspaceDir } from "../workspace-dir.js";
16
15
  import { coerceImageAssistantText, coerceImageModelConfig, decodeDataUrl, resolveProviderVisionModelFromConfig, } from "./image-tool.helpers.js";
17
16
  const DEFAULT_PROMPT = "Describe the image.";
17
+ const ANTHROPIC_IMAGE_PRIMARY = "anthropic/claude-opus-4-6";
18
+ const ANTHROPIC_IMAGE_FALLBACK = "anthropic/claude-opus-4-5";
19
+ const DEFAULT_MAX_IMAGES = 20;
18
20
  export const __testing = {
19
21
  decodeDataUrl,
20
22
  coerceImageAssistantText,
23
+ resolveImageToolMaxTokens,
21
24
  };
25
+ function resolveImageToolMaxTokens(modelMaxTokens, requestedMaxTokens = 4096) {
26
+ if (typeof modelMaxTokens !== "number" ||
27
+ !Number.isFinite(modelMaxTokens) ||
28
+ modelMaxTokens <= 0) {
29
+ return requestedMaxTokens;
30
+ }
31
+ return Math.min(requestedMaxTokens, modelMaxTokens);
32
+ }
22
33
  function resolveDefaultModelRef(cfg) {
23
34
  if (cfg) {
24
35
  const resolved = resolveConfiguredModelRef({
@@ -31,8 +42,9 @@ function resolveDefaultModelRef(cfg) {
31
42
  return { provider: DEFAULT_PROVIDER, model: DEFAULT_MODEL };
32
43
  }
33
44
  function hasAuthForProvider(params) {
34
- if (resolveEnvApiKey(params.provider)?.apiKey)
45
+ if (resolveEnvApiKey(params.provider)?.apiKey) {
35
46
  return true;
47
+ }
36
48
  const store = ensureAuthProfileStore(params.agentDir, {
37
49
  allowKeychainPrompt: false,
38
50
  });
@@ -67,10 +79,12 @@ export function resolveImageModelConfigForTool(params) {
67
79
  const fallbacks = [];
68
80
  const addFallback = (modelRef) => {
69
81
  const ref = (modelRef ?? "").trim();
70
- if (!ref)
82
+ if (!ref) {
71
83
  return;
72
- if (fallbacks.includes(ref))
84
+ }
85
+ if (fallbacks.includes(ref)) {
73
86
  return;
87
+ }
74
88
  fallbacks.push(ref);
75
89
  };
76
90
  const providerVisionFromConfig = resolveProviderVisionModelFromConfig({
@@ -89,17 +103,22 @@ export function resolveImageModelConfigForTool(params) {
89
103
  else if (providerOk && providerVisionFromConfig) {
90
104
  preferred = providerVisionFromConfig;
91
105
  }
106
+ else if (primary.provider === "zai" && providerOk) {
107
+ preferred = "zai/glm-4.6v";
108
+ }
92
109
  else if (primary.provider === "openai" && openaiOk) {
93
110
  preferred = "openai/gpt-5-mini";
94
111
  }
95
112
  else if (primary.provider === "anthropic" && anthropicOk) {
96
- preferred = "anthropic/claude-opus-4-5";
113
+ preferred = ANTHROPIC_IMAGE_PRIMARY;
97
114
  }
98
115
  if (preferred?.trim()) {
99
- if (openaiOk)
116
+ if (openaiOk) {
100
117
  addFallback("openai/gpt-5-mini");
101
- if (anthropicOk)
102
- addFallback("anthropic/claude-opus-4-5");
118
+ }
119
+ if (anthropicOk) {
120
+ addFallback(ANTHROPIC_IMAGE_FALLBACK);
121
+ }
103
122
  // Don't duplicate primary in fallbacks.
104
123
  const pruned = fallbacks.filter((ref) => ref !== preferred);
105
124
  return {
@@ -109,15 +128,19 @@ export function resolveImageModelConfigForTool(params) {
109
128
  }
110
129
  // Cross-provider fallback when we can't pair with the primary provider.
111
130
  if (openaiOk) {
112
- if (anthropicOk)
113
- addFallback("anthropic/claude-opus-4-5");
131
+ if (anthropicOk) {
132
+ addFallback(ANTHROPIC_IMAGE_FALLBACK);
133
+ }
114
134
  return {
115
135
  primary: "openai/gpt-5-mini",
116
136
  ...(fallbacks.length ? { fallbacks } : {}),
117
137
  };
118
138
  }
119
139
  if (anthropicOk) {
120
- return { primary: "anthropic/claude-opus-4-5" };
140
+ return {
141
+ primary: ANTHROPIC_IMAGE_PRIMARY,
142
+ fallbacks: [ANTHROPIC_IMAGE_FALLBACK],
143
+ };
121
144
  }
122
145
  return null;
123
146
  }
@@ -131,15 +154,16 @@ function pickMaxBytes(cfg, maxBytesMb) {
131
154
  }
132
155
  return undefined;
133
156
  }
134
- function buildImageContext(prompt, base64, mimeType) {
157
+ function buildImageContext(prompt, images) {
158
+ const content = [{ type: "text", text: prompt }];
159
+ for (const img of images) {
160
+ content.push({ type: "image", data: img.base64, mimeType: img.mimeType });
161
+ }
135
162
  return {
136
163
  messages: [
137
164
  {
138
165
  role: "user",
139
- content: [
140
- { type: "text", text: prompt },
141
- { type: "image", data: base64, mimeType },
142
- ],
166
+ content,
143
167
  timestamp: Date.now(),
144
168
  },
145
169
  ],
@@ -149,29 +173,32 @@ async function resolveSandboxedImagePath(params) {
149
173
  const normalize = (p) => (p.startsWith("file://") ? p.slice("file://".length) : p);
150
174
  const filePath = normalize(params.imagePath);
151
175
  try {
152
- const out = await assertSandboxPath({
176
+ const resolved = params.sandbox.bridge.resolvePath({
153
177
  filePath,
154
- cwd: params.sandboxRoot,
155
- root: params.sandboxRoot,
178
+ cwd: params.sandbox.root,
156
179
  });
157
- return { resolved: out.resolved };
180
+ return { resolved: resolved.hostPath };
158
181
  }
159
182
  catch (err) {
160
183
  const name = path.basename(filePath);
161
184
  const candidateRel = path.join("media", "inbound", name);
162
- const candidateAbs = path.join(params.sandboxRoot, candidateRel);
163
185
  try {
164
- await fs.stat(candidateAbs);
186
+ const stat = await params.sandbox.bridge.stat({
187
+ filePath: candidateRel,
188
+ cwd: params.sandbox.root,
189
+ });
190
+ if (!stat) {
191
+ throw err;
192
+ }
165
193
  }
166
194
  catch {
167
195
  throw err;
168
196
  }
169
- const out = await assertSandboxPath({
197
+ const out = params.sandbox.bridge.resolvePath({
170
198
  filePath: candidateRel,
171
- cwd: params.sandboxRoot,
172
- root: params.sandboxRoot,
199
+ cwd: params.sandbox.root,
173
200
  });
174
- return { resolved: out.resolved, rewrittenFrom: filePath };
201
+ return { resolved: out.hostPath, rewrittenFrom: filePath };
175
202
  }
176
203
  }
177
204
  async function runImagePrompt(params) {
@@ -208,8 +235,10 @@ async function runImagePrompt(params) {
208
235
  });
209
236
  const apiKey = requireApiKey(apiKeyInfo, model.provider);
210
237
  authStorage.setRuntimeApiKey(model.provider, apiKey);
211
- const imageDataUrl = `data:${params.mimeType};base64,${params.base64}`;
238
+ // MiniMax VLM only supports a single image; use the first one.
212
239
  if (model.provider === "minimax") {
240
+ const first = params.images[0];
241
+ const imageDataUrl = `data:${first.mimeType};base64,${first.base64}`;
213
242
  const text = await minimaxUnderstandImage({
214
243
  apiKey,
215
244
  prompt: params.prompt,
@@ -218,11 +247,11 @@ async function runImagePrompt(params) {
218
247
  });
219
248
  return { text, provider: model.provider, model: model.id };
220
249
  }
221
- const context = buildImageContext(params.prompt, params.base64, params.mimeType);
222
- const message = (await complete(model, context, {
250
+ const context = buildImageContext(params.prompt, params.images);
251
+ const message = await complete(model, context, {
223
252
  apiKey,
224
- maxTokens: 512,
225
- }));
253
+ maxTokens: resolveImageToolMaxTokens(model.maxTokens),
254
+ });
226
255
  const text = coerceImageAssistantText({
227
256
  message,
228
257
  provider: model.provider,
@@ -255,53 +284,76 @@ export function createImageTool(options) {
255
284
  cfg: options?.config,
256
285
  agentDir,
257
286
  });
258
- if (!imageModelConfig)
287
+ if (!imageModelConfig) {
259
288
  return null;
289
+ }
260
290
  // If model has native vision, images in the prompt are auto-injected
261
291
  // so this tool is only needed when image wasn't provided in the prompt
262
292
  const description = options?.modelHasVision
263
- ? "Analyze an image with a vision model. Only use this tool when the image was NOT already provided in the user's message. Images mentioned in the prompt are automatically visible to you."
264
- : "Analyze an image with the configured image model (agents.defaults.imageModel). Provide a prompt and image path or URL.";
293
+ ? "Analyze one or more images with a vision model. Use image for a single path/URL, or images for multiple (up to 20). Only use this tool when images were NOT already provided in the user's message. Images mentioned in the prompt are automatically visible to you."
294
+ : "Analyze one or more images with the configured image model (agents.defaults.imageModel). Use image for a single path/URL, or images for multiple (up to 20). Provide a prompt describing what to analyze.";
295
+ const localRoots = (() => {
296
+ const roots = getDefaultLocalRoots();
297
+ const workspaceDir = normalizeWorkspaceDir(options?.workspaceDir);
298
+ if (!workspaceDir) {
299
+ return roots;
300
+ }
301
+ return Array.from(new Set([...roots, workspaceDir]));
302
+ })();
265
303
  return {
266
304
  label: "Image",
267
305
  name: "image",
268
306
  description,
269
307
  parameters: Type.Object({
270
308
  prompt: Type.Optional(Type.String()),
271
- image: Type.String(),
309
+ image: Type.Optional(Type.String({ description: "Single image path or URL." })),
310
+ images: Type.Optional(Type.Array(Type.String(), {
311
+ description: "Multiple image paths or URLs (up to maxImages, default 20).",
312
+ })),
272
313
  model: Type.Optional(Type.String()),
273
314
  maxBytesMb: Type.Optional(Type.Number()),
315
+ maxImages: Type.Optional(Type.Number()),
274
316
  }),
275
317
  execute: async (_toolCallId, args) => {
276
318
  const record = args && typeof args === "object" ? args : {};
277
- const imageRawInput = typeof record.image === "string" ? record.image.trim() : "";
278
- const imageRaw = imageRawInput.startsWith("@")
279
- ? imageRawInput.slice(1).trim()
280
- : imageRawInput;
281
- if (!imageRaw)
319
+ // MARK: - Normalize image + images input and dedupe while preserving order
320
+ const imageCandidates = [];
321
+ if (typeof record.image === "string") {
322
+ imageCandidates.push(record.image);
323
+ }
324
+ if (Array.isArray(record.images)) {
325
+ imageCandidates.push(...record.images.filter((v) => typeof v === "string"));
326
+ }
327
+ const seenImages = new Set();
328
+ const imageInputs = [];
329
+ for (const candidate of imageCandidates) {
330
+ const trimmedCandidate = candidate.trim();
331
+ const normalizedForDedupe = trimmedCandidate.startsWith("@")
332
+ ? trimmedCandidate.slice(1).trim()
333
+ : trimmedCandidate;
334
+ if (!normalizedForDedupe || seenImages.has(normalizedForDedupe)) {
335
+ continue;
336
+ }
337
+ seenImages.add(normalizedForDedupe);
338
+ imageInputs.push(trimmedCandidate);
339
+ }
340
+ if (imageInputs.length === 0) {
282
341
  throw new Error("image required");
283
- // The tool accepts file paths, file/data URLs, or http(s) URLs. In some
284
- // agent/model contexts, images can be referenced as pseudo-URIs like
285
- // `image:0` (e.g. "first image in the prompt"). We don't have access to a
286
- // shared image registry here, so fail gracefully instead of attempting to
287
- // `fs.readFile("image:0")` and producing a noisy ENOENT.
288
- const looksLikeWindowsDrivePath = /^[a-zA-Z]:[\\/]/.test(imageRaw);
289
- const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(imageRaw);
290
- const isFileUrl = /^file:/i.test(imageRaw);
291
- const isHttpUrl = /^https?:\/\//i.test(imageRaw);
292
- const isDataUrl = /^data:/i.test(imageRaw);
293
- if (hasScheme && !looksLikeWindowsDrivePath && !isFileUrl && !isHttpUrl && !isDataUrl) {
342
+ }
343
+ // MARK: - Enforce max images cap
344
+ const maxImagesRaw = typeof record.maxImages === "number" ? record.maxImages : undefined;
345
+ const maxImages = typeof maxImagesRaw === "number" && Number.isFinite(maxImagesRaw) && maxImagesRaw > 0
346
+ ? Math.floor(maxImagesRaw)
347
+ : DEFAULT_MAX_IMAGES;
348
+ if (imageInputs.length > maxImages) {
294
349
  return {
295
350
  content: [
296
351
  {
297
352
  type: "text",
298
- text: `Unsupported image reference: ${imageRawInput}. Use a file path, a file:// URL, a data: URL, or an http(s) URL.`,
353
+ text: `Too many images: ${imageInputs.length} provided, maximum is ${maxImages}. Please reduce the number of images.`,
299
354
  },
300
355
  ],
301
- details: {
302
- error: "unsupported_image_reference",
303
- image: imageRawInput,
304
- },
356
+ details: { error: "too_many_images", count: imageInputs.length, max: maxImages },
305
357
  };
306
358
  }
307
359
  const promptRaw = typeof record.prompt === "string" && record.prompt.trim()
@@ -310,58 +362,121 @@ export function createImageTool(options) {
310
362
  const modelOverride = typeof record.model === "string" && record.model.trim() ? record.model.trim() : undefined;
311
363
  const maxBytesMb = typeof record.maxBytesMb === "number" ? record.maxBytesMb : undefined;
312
364
  const maxBytes = pickMaxBytes(options?.config, maxBytesMb);
313
- const sandboxRoot = options?.sandbox?.root?.trim() ?? options?.sandboxRoot?.trim();
314
- const isUrl = isHttpUrl;
315
- if (sandboxRoot && isUrl) {
316
- throw new Error("Sandboxed image tool does not allow remote URLs.");
317
- }
318
- const resolvedImage = (() => {
319
- if (sandboxRoot)
320
- return imageRaw;
321
- if (imageRaw.startsWith("~"))
322
- return resolveUserPath(imageRaw);
323
- return imageRaw;
324
- })();
325
- const resolvedPathInfo = isDataUrl
326
- ? { resolved: "" }
327
- : sandboxRoot
328
- ? await resolveSandboxedImagePath({
329
- sandboxRoot,
330
- imagePath: resolvedImage,
331
- })
332
- : {
333
- resolved: resolvedImage.startsWith("file://")
334
- ? resolvedImage.slice("file://".length)
335
- : resolvedImage,
365
+ const sandboxConfig = options?.sandbox && options?.sandbox.root.trim()
366
+ ? { root: options.sandbox.root.trim(), bridge: options.sandbox.bridge }
367
+ : null;
368
+ // MARK: - Load and resolve each image
369
+ const loadedImages = [];
370
+ for (const imageRawInput of imageInputs) {
371
+ const trimmed = imageRawInput.trim();
372
+ const imageRaw = trimmed.startsWith("@") ? trimmed.slice(1).trim() : trimmed;
373
+ if (!imageRaw) {
374
+ throw new Error("image required (empty string in array)");
375
+ }
376
+ // The tool accepts file paths, file/data URLs, or http(s) URLs. In some
377
+ // agent/model contexts, images can be referenced as pseudo-URIs like
378
+ // `image:0` (e.g. "first image in the prompt"). We don't have access to a
379
+ // shared image registry here, so fail gracefully instead of attempting to
380
+ // `fs.readFile("image:0")` and producing a noisy ENOENT.
381
+ const looksLikeWindowsDrivePath = /^[a-zA-Z]:[\\/]/.test(imageRaw);
382
+ const hasScheme = /^[a-z][a-z0-9+.-]*:/i.test(imageRaw);
383
+ const isFileUrl = /^file:/i.test(imageRaw);
384
+ const isHttpUrl = /^https?:\/\//i.test(imageRaw);
385
+ const isDataUrl = /^data:/i.test(imageRaw);
386
+ if (hasScheme && !looksLikeWindowsDrivePath && !isFileUrl && !isHttpUrl && !isDataUrl) {
387
+ return {
388
+ content: [
389
+ {
390
+ type: "text",
391
+ text: `Unsupported image reference: ${imageRawInput}. Use a file path, a file:// URL, a data: URL, or an http(s) URL.`,
392
+ },
393
+ ],
394
+ details: {
395
+ error: "unsupported_image_reference",
396
+ image: imageRawInput,
397
+ },
336
398
  };
337
- const resolvedPath = isDataUrl ? null : resolvedPathInfo.resolved;
338
- const media = isDataUrl
339
- ? decodeDataUrl(resolvedImage)
340
- : await loadWebMedia(resolvedPath ?? resolvedImage, maxBytes);
341
- if (media.kind !== "image") {
342
- throw new Error(`Unsupported media type: ${media.kind}`);
399
+ }
400
+ if (sandboxConfig && isHttpUrl) {
401
+ throw new Error("Sandboxed image tool does not allow remote URLs.");
402
+ }
403
+ const resolvedImage = (() => {
404
+ if (sandboxConfig) {
405
+ return imageRaw;
406
+ }
407
+ if (imageRaw.startsWith("~")) {
408
+ return resolveUserPath(imageRaw);
409
+ }
410
+ return imageRaw;
411
+ })();
412
+ const resolvedPathInfo = isDataUrl
413
+ ? { resolved: "" }
414
+ : sandboxConfig
415
+ ? await resolveSandboxedImagePath({
416
+ sandbox: sandboxConfig,
417
+ imagePath: resolvedImage,
418
+ })
419
+ : {
420
+ resolved: resolvedImage.startsWith("file://")
421
+ ? resolvedImage.slice("file://".length)
422
+ : resolvedImage,
423
+ };
424
+ const resolvedPath = isDataUrl ? null : resolvedPathInfo.resolved;
425
+ const media = isDataUrl
426
+ ? decodeDataUrl(resolvedImage)
427
+ : sandboxConfig
428
+ ? await loadWebMedia(resolvedPath ?? resolvedImage, {
429
+ maxBytes,
430
+ sandboxValidated: true,
431
+ readFile: (filePath) => sandboxConfig.bridge.readFile({ filePath, cwd: sandboxConfig.root }),
432
+ })
433
+ : await loadWebMedia(resolvedPath ?? resolvedImage, {
434
+ maxBytes,
435
+ localRoots,
436
+ });
437
+ if (media.kind !== "image") {
438
+ throw new Error(`Unsupported media type: ${media.kind}`);
439
+ }
440
+ const mimeType = ("contentType" in media && media.contentType) ||
441
+ ("mimeType" in media && media.mimeType) ||
442
+ "image/png";
443
+ const base64 = media.buffer.toString("base64");
444
+ loadedImages.push({
445
+ base64,
446
+ mimeType,
447
+ resolvedImage,
448
+ ...(resolvedPathInfo.rewrittenFrom
449
+ ? { rewrittenFrom: resolvedPathInfo.rewrittenFrom }
450
+ : {}),
451
+ });
343
452
  }
344
- const mimeType = ("contentType" in media && media.contentType) ||
345
- ("mimeType" in media && media.mimeType) ||
346
- "image/png";
347
- const base64 = media.buffer.toString("base64");
453
+ // MARK: - Run image prompt with all loaded images
348
454
  const result = await runImagePrompt({
349
455
  cfg: options?.config,
350
456
  agentDir,
351
457
  imageModelConfig,
352
458
  modelOverride,
353
459
  prompt: promptRaw,
354
- base64,
355
- mimeType,
460
+ images: loadedImages.map((img) => ({ base64: img.base64, mimeType: img.mimeType })),
356
461
  });
462
+ const imageDetails = loadedImages.length === 1
463
+ ? {
464
+ image: loadedImages[0].resolvedImage,
465
+ ...(loadedImages[0].rewrittenFrom
466
+ ? { rewrittenFrom: loadedImages[0].rewrittenFrom }
467
+ : {}),
468
+ }
469
+ : {
470
+ images: loadedImages.map((img) => ({
471
+ image: img.resolvedImage,
472
+ ...(img.rewrittenFrom ? { rewrittenFrom: img.rewrittenFrom } : {}),
473
+ })),
474
+ };
357
475
  return {
358
476
  content: [{ type: "text", text: result.text }],
359
477
  details: {
360
478
  model: `${result.provider}/${result.model}`,
361
- image: resolvedImage,
362
- ...(resolvedPathInfo.rewrittenFrom
363
- ? { rewrittenFrom: resolvedPathInfo.rewrittenFrom }
364
- : {}),
479
+ ...imageDetails,
365
480
  attempts: result.attempts,
366
481
  },
367
482
  };