@poolzin/pool-bot 2026.2.0 → 2026.2.1

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 (230) hide show
  1. package/dist/agents/bash-tools.exec.js +76 -25
  2. package/dist/agents/cli-runner/helpers.js +9 -11
  3. package/dist/agents/identity.js +47 -7
  4. package/dist/agents/memory-search.js +25 -8
  5. package/dist/agents/model-selection.js +21 -0
  6. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  7. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  8. package/dist/agents/pi-embedded-helpers.js +1 -1
  9. package/dist/agents/pi-embedded-runner/compact.js +1 -0
  10. package/dist/agents/pi-embedded-runner/model.js +61 -2
  11. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  12. package/dist/agents/pi-embedded-runner/run.js +199 -46
  13. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  14. package/dist/agents/pi-embedded-subscribe.js +118 -29
  15. package/dist/agents/pi-tools.js +10 -5
  16. package/dist/agents/poolbot-tools.js +15 -10
  17. package/dist/agents/sandbox-paths.js +31 -0
  18. package/dist/agents/session-tool-result-guard.js +94 -15
  19. package/dist/agents/shell-utils.js +51 -0
  20. package/dist/agents/skills/bundled-context.js +23 -0
  21. package/dist/agents/skills/bundled-dir.js +41 -7
  22. package/dist/agents/skills-install.js +60 -23
  23. package/dist/agents/subagent-announce.js +79 -34
  24. package/dist/agents/tool-policy.conformance.js +14 -0
  25. package/dist/agents/tool-policy.js +24 -0
  26. package/dist/agents/tools/cron-tool.js +166 -19
  27. package/dist/agents/tools/discord-actions-presence.js +78 -0
  28. package/dist/agents/tools/message-tool.js +56 -2
  29. package/dist/agents/tools/sessions-history-tool.js +69 -1
  30. package/dist/agents/tools/web-search.js +211 -42
  31. package/dist/agents/usage.js +23 -1
  32. package/dist/agents/workspace-run.js +67 -0
  33. package/dist/agents/workspace-templates.js +44 -0
  34. package/dist/auto-reply/command-auth.js +121 -6
  35. package/dist/auto-reply/envelope.js +50 -72
  36. package/dist/auto-reply/reply/commands-compact.js +1 -0
  37. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  38. package/dist/auto-reply/reply/commands-context.js +1 -0
  39. package/dist/auto-reply/reply/commands-models.js +107 -60
  40. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  41. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  42. package/dist/auto-reply/reply/inbound-context.js +5 -1
  43. package/dist/auto-reply/reply/model-selection.js +3 -3
  44. package/dist/auto-reply/thinking.js +88 -43
  45. package/dist/browser/bridge-server.js +13 -0
  46. package/dist/browser/cdp.helpers.js +38 -24
  47. package/dist/browser/client-fetch.js +50 -7
  48. package/dist/browser/config.js +1 -10
  49. package/dist/browser/extension-relay.js +101 -40
  50. package/dist/browser/pw-ai.js +1 -1
  51. package/dist/browser/pw-session.js +143 -8
  52. package/dist/browser/pw-tools-core.interactions.js +125 -27
  53. package/dist/browser/pw-tools-core.responses.js +1 -1
  54. package/dist/browser/pw-tools-core.state.js +1 -1
  55. package/dist/browser/routes/agent.act.js +86 -41
  56. package/dist/browser/routes/dispatcher.js +4 -4
  57. package/dist/browser/screenshot.js +1 -1
  58. package/dist/browser/server.js +13 -0
  59. package/dist/build-info.json +3 -3
  60. package/dist/channels/reply-prefix.js +8 -1
  61. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  62. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  63. package/dist/cli/cron-cli/shared.js +56 -41
  64. package/dist/cli/dns-cli.js +26 -14
  65. package/dist/cli/gateway-cli/register.js +37 -19
  66. package/dist/cli/memory-cli.js +5 -5
  67. package/dist/cli/parse-bytes.js +37 -0
  68. package/dist/cli/update-cli.js +173 -52
  69. package/dist/commands/agent.js +1 -0
  70. package/dist/commands/doctor-config-flow.js +61 -5
  71. package/dist/commands/doctor-state-migrations.js +1 -1
  72. package/dist/commands/health.js +1 -1
  73. package/dist/commands/model-allowlist.js +29 -0
  74. package/dist/commands/model-picker.js +2 -1
  75. package/dist/commands/models/list.status-command.js +43 -23
  76. package/dist/commands/models/shared.js +15 -0
  77. package/dist/commands/onboard-custom.js +384 -0
  78. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  79. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  80. package/dist/commands/onboard-skills.js +63 -38
  81. package/dist/commands/openai-model-default.js +41 -0
  82. package/dist/config/defaults.js +3 -2
  83. package/dist/config/paths.js +136 -35
  84. package/dist/config/plugin-auto-enable.js +21 -5
  85. package/dist/config/redact-snapshot.js +153 -0
  86. package/dist/config/schema.field-metadata.js +590 -0
  87. package/dist/config/schema.js +2 -2
  88. package/dist/config/sessions/store.js +291 -23
  89. package/dist/config/zod-schema.agent-defaults.js +3 -0
  90. package/dist/config/zod-schema.agent-runtime.js +13 -2
  91. package/dist/config/zod-schema.providers-core.js +142 -0
  92. package/dist/config/zod-schema.session.js +3 -0
  93. package/dist/cron/delivery.js +57 -0
  94. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  95. package/dist/cron/isolated-agent/helpers.js +22 -5
  96. package/dist/cron/isolated-agent/run.js +171 -63
  97. package/dist/cron/isolated-agent/session.js +2 -0
  98. package/dist/cron/normalize.js +356 -28
  99. package/dist/cron/parse.js +10 -5
  100. package/dist/cron/run-log.js +35 -10
  101. package/dist/cron/schedule.js +41 -6
  102. package/dist/cron/service/jobs.js +208 -35
  103. package/dist/cron/service/ops.js +72 -16
  104. package/dist/cron/service/state.js +2 -0
  105. package/dist/cron/service/store.js +386 -14
  106. package/dist/cron/service/timer.js +390 -147
  107. package/dist/cron/session-reaper.js +86 -0
  108. package/dist/cron/store.js +23 -8
  109. package/dist/cron/validate-timestamp.js +43 -0
  110. package/dist/discord/monitor/agent-components.js +438 -0
  111. package/dist/discord/monitor/allow-list.js +28 -5
  112. package/dist/discord/monitor/gateway-registry.js +29 -0
  113. package/dist/discord/monitor/native-command.js +44 -23
  114. package/dist/discord/monitor/sender-identity.js +45 -0
  115. package/dist/discord/pluralkit.js +27 -0
  116. package/dist/discord/send.outbound.js +92 -5
  117. package/dist/discord/send.shared.js +60 -23
  118. package/dist/discord/targets.js +84 -1
  119. package/dist/entry.js +15 -9
  120. package/dist/extensionAPI.js +8 -0
  121. package/dist/gateway/control-ui.js +8 -1
  122. package/dist/gateway/hooks-mapping.js +3 -0
  123. package/dist/gateway/hooks.js +65 -0
  124. package/dist/gateway/net.js +96 -31
  125. package/dist/gateway/node-command-policy.js +50 -15
  126. package/dist/gateway/origin-check.js +56 -0
  127. package/dist/gateway/protocol/client-info.js +9 -0
  128. package/dist/gateway/protocol/index.js +9 -2
  129. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  130. package/dist/gateway/protocol/schema/cron.js +22 -10
  131. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  132. package/dist/gateway/protocol/schema/sessions.js +12 -0
  133. package/dist/gateway/server/hooks.js +1 -1
  134. package/dist/gateway/server-broadcast.js +26 -9
  135. package/dist/gateway/server-chat.js +112 -23
  136. package/dist/gateway/server-discovery-runtime.js +10 -2
  137. package/dist/gateway/server-http.js +109 -11
  138. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  139. package/dist/gateway/server-methods/agents.js +321 -2
  140. package/dist/gateway/server-methods/usage.js +559 -16
  141. package/dist/gateway/server-runtime-state.js +22 -8
  142. package/dist/gateway/server-startup-memory.js +16 -0
  143. package/dist/gateway/server.impl.js +5 -1
  144. package/dist/gateway/session-utils.fs.js +23 -25
  145. package/dist/gateway/session-utils.js +20 -10
  146. package/dist/gateway/sessions-patch.js +7 -22
  147. package/dist/gateway/test-helpers.server.js +35 -2
  148. package/dist/imessage/constants.js +2 -0
  149. package/dist/imessage/monitor/deliver.js +4 -1
  150. package/dist/imessage/monitor/monitor-provider.js +51 -1
  151. package/dist/infra/bonjour-discovery.js +131 -70
  152. package/dist/infra/control-ui-assets.js +134 -12
  153. package/dist/infra/errors.js +12 -0
  154. package/dist/infra/exec-approvals.js +266 -57
  155. package/dist/infra/format-time/format-datetime.js +79 -0
  156. package/dist/infra/format-time/format-duration.js +81 -0
  157. package/dist/infra/format-time/format-relative.js +80 -0
  158. package/dist/infra/heartbeat-runner.js +140 -49
  159. package/dist/infra/home-dir.js +54 -0
  160. package/dist/infra/net/fetch-guard.js +122 -0
  161. package/dist/infra/net/ssrf.js +65 -29
  162. package/dist/infra/outbound/abort.js +14 -0
  163. package/dist/infra/outbound/message-action-runner.js +77 -13
  164. package/dist/infra/outbound/outbound-session.js +143 -37
  165. package/dist/infra/poolbot-root.js +43 -1
  166. package/dist/infra/session-cost-usage.js +631 -41
  167. package/dist/infra/state-migrations.js +317 -47
  168. package/dist/infra/update-global.js +35 -0
  169. package/dist/infra/update-runner.js +149 -43
  170. package/dist/infra/warning-filter.js +65 -0
  171. package/dist/infra/widearea-dns.js +30 -9
  172. package/dist/logging/redact-identifier.js +12 -0
  173. package/dist/media/fetch.js +81 -58
  174. package/dist/media-understanding/apply.js +403 -3
  175. package/dist/media-understanding/attachments.js +38 -27
  176. package/dist/media-understanding/defaults.js +16 -0
  177. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  178. package/dist/media-understanding/providers/google/audio.js +24 -17
  179. package/dist/media-understanding/providers/google/video.js +24 -17
  180. package/dist/media-understanding/providers/image.js +2 -2
  181. package/dist/media-understanding/providers/index.js +4 -1
  182. package/dist/media-understanding/providers/openai/audio.js +22 -14
  183. package/dist/media-understanding/providers/shared.js +16 -11
  184. package/dist/media-understanding/providers/zai/index.js +6 -0
  185. package/dist/media-understanding/runner.js +158 -90
  186. package/dist/memory/batch-voyage.js +277 -0
  187. package/dist/memory/embeddings-voyage.js +75 -0
  188. package/dist/memory/embeddings.js +28 -16
  189. package/dist/memory/internal.js +101 -18
  190. package/dist/memory/manager.js +154 -48
  191. package/dist/memory/search-manager.js +173 -0
  192. package/dist/memory/session-files.js +9 -3
  193. package/dist/node-host/runner.js +34 -24
  194. package/dist/node-host/with-timeout.js +27 -0
  195. package/dist/plugins/commands.js +5 -1
  196. package/dist/plugins/config-state.js +86 -7
  197. package/dist/plugins/source-display.js +51 -0
  198. package/dist/process/exec.js +20 -2
  199. package/dist/routing/resolve-route.js +12 -0
  200. package/dist/routing/session-key.js +15 -0
  201. package/dist/runtime.js +2 -0
  202. package/dist/security/audit-extra.async.js +601 -0
  203. package/dist/security/audit-extra.js +2 -830
  204. package/dist/security/audit-extra.sync.js +505 -0
  205. package/dist/security/channel-metadata.js +34 -0
  206. package/dist/security/external-content.js +88 -6
  207. package/dist/security/skill-scanner.js +330 -0
  208. package/dist/sessions/session-key-utils.js +7 -0
  209. package/dist/signal/monitor/event-handler.js +80 -1
  210. package/dist/slack/monitor/media.js +85 -15
  211. package/dist/tailscale/detect.js +1 -2
  212. package/dist/telegram/bot/helpers.js +109 -28
  213. package/dist/telegram/bot-handlers.js +144 -3
  214. package/dist/telegram/bot-message-context.js +37 -10
  215. package/dist/telegram/bot-message-dispatch.js +48 -15
  216. package/dist/telegram/bot-native-commands.js +86 -29
  217. package/dist/telegram/bot.js +30 -29
  218. package/dist/telegram/model-buttons.js +163 -0
  219. package/dist/telegram/monitor.js +110 -85
  220. package/dist/telegram/send.js +129 -47
  221. package/dist/terminal/restore.js +45 -0
  222. package/dist/test-helpers/state-dir-env.js +16 -0
  223. package/dist/tts/tts.js +12 -6
  224. package/dist/tui/tui-session-actions.js +166 -54
  225. package/dist/utils/fetch-timeout.js +20 -0
  226. package/dist/utils/normalize-secret-input.js +19 -0
  227. package/dist/utils/transcript-tools.js +58 -0
  228. package/dist/utils.js +45 -14
  229. package/dist/version.js +42 -5
  230. package/package.json +1 -1
@@ -1,8 +1,10 @@
1
1
  import { Type } from "@sinclair/typebox";
2
2
  import { formatCliCommand } from "../../cli/command-format.js";
3
+ import { wrapWebContent } from "../../security/external-content.js";
4
+ import { normalizeSecretInput } from "../../utils/normalize-secret-input.js";
3
5
  import { jsonResult, readNumberParam, readStringParam } from "./common.js";
4
6
  import { DEFAULT_CACHE_TTL_MINUTES, DEFAULT_TIMEOUT_SECONDS, normalizeCacheKey, readCache, readResponseText, resolveCacheTtlMs, resolveTimeoutSeconds, withTimeout, writeCache, } from "./web-shared.js";
5
- const SEARCH_PROVIDERS = ["brave", "perplexity"];
7
+ const SEARCH_PROVIDERS = ["brave", "perplexity", "grok"];
6
8
  const DEFAULT_SEARCH_COUNT = 5;
7
9
  const MAX_SEARCH_COUNT = 10;
8
10
  const BRAVE_SEARCH_ENDPOINT = "https://api.search.brave.com/res/v1/web/search";
@@ -11,6 +13,8 @@ const PERPLEXITY_DIRECT_BASE_URL = "https://api.perplexity.ai";
11
13
  const DEFAULT_PERPLEXITY_MODEL = "perplexity/sonar-pro";
12
14
  const PERPLEXITY_KEY_PREFIXES = ["pplx-"];
13
15
  const OPENROUTER_KEY_PREFIXES = ["sk-or-"];
16
+ const XAI_API_ENDPOINT = "https://api.x.ai/v1/responses";
17
+ const DEFAULT_GROK_MODEL = "grok-4-1-fast";
14
18
  const SEARCH_CACHE = new Map();
15
19
  const BRAVE_FRESHNESS_SHORTCUTS = new Set(["pd", "pw", "pm", "py"]);
16
20
  const BRAVE_FRESHNESS_RANGE = /^(\d{4}-\d{2}-\d{2})to(\d{4}-\d{2}-\d{2})$/;
@@ -34,22 +38,35 @@ const WebSearchSchema = Type.Object({
34
38
  description: "Filter results by discovery time (Brave only). Values: 'pd' (past 24h), 'pw' (past week), 'pm' (past month), 'py' (past year), or date range 'YYYY-MM-DDtoYYYY-MM-DD'.",
35
39
  })),
36
40
  });
41
+ function extractGrokContent(data) {
42
+ // xAI Responses API format: output[0].content[0].text
43
+ const fromResponses = data.output?.[0]?.content?.[0]?.text;
44
+ if (typeof fromResponses === "string" && fromResponses) {
45
+ return fromResponses;
46
+ }
47
+ return typeof data.output_text === "string" ? data.output_text : undefined;
48
+ }
37
49
  function resolveSearchConfig(cfg) {
38
50
  const search = cfg?.tools?.web?.search;
39
- if (!search || typeof search !== "object")
51
+ if (!search || typeof search !== "object") {
40
52
  return undefined;
53
+ }
41
54
  return search;
42
55
  }
43
56
  function resolveSearchEnabled(params) {
44
- if (typeof params.search?.enabled === "boolean")
57
+ if (typeof params.search?.enabled === "boolean") {
45
58
  return params.search.enabled;
46
- if (params.sandboxed)
59
+ }
60
+ if (params.sandboxed) {
47
61
  return true;
62
+ }
48
63
  return true;
49
64
  }
50
65
  function resolveSearchApiKey(search) {
51
- const fromConfig = search && "apiKey" in search && typeof search.apiKey === "string" ? search.apiKey.trim() : "";
52
- const fromEnv = (process.env.BRAVE_API_KEY ?? "").trim();
66
+ const fromConfig = search && "apiKey" in search && typeof search.apiKey === "string"
67
+ ? normalizeSecretInput(search.apiKey)
68
+ : "";
69
+ const fromEnv = normalizeSecretInput(process.env.BRAVE_API_KEY);
53
70
  return fromConfig || fromEnv || undefined;
54
71
  }
55
72
  function missingSearchKeyPayload(provider) {
@@ -60,6 +77,13 @@ function missingSearchKeyPayload(provider) {
60
77
  docs: "https://docs.molt.bot/tools/web",
61
78
  };
62
79
  }
80
+ if (provider === "grok") {
81
+ return {
82
+ error: "missing_xai_api_key",
83
+ message: "web_search (grok) needs an xAI API key. Set XAI_API_KEY in the Gateway environment, or configure tools.web.search.grok.apiKey.",
84
+ docs: "https://docs.molt.bot/tools/web",
85
+ };
86
+ }
63
87
  return {
64
88
  error: "missing_brave_api_key",
65
89
  message: `web_search needs a Brave Search API key. Run \`${formatCliCommand("poolbot configure --section web")}\` to store it, or set BRAVE_API_KEY in the Gateway environment.`,
@@ -70,18 +94,25 @@ function resolveSearchProvider(search) {
70
94
  const raw = search && "provider" in search && typeof search.provider === "string"
71
95
  ? search.provider.trim().toLowerCase()
72
96
  : "";
73
- if (raw === "perplexity")
97
+ if (raw === "perplexity") {
74
98
  return "perplexity";
75
- if (raw === "brave")
99
+ }
100
+ if (raw === "grok") {
101
+ return "grok";
102
+ }
103
+ if (raw === "brave") {
76
104
  return "brave";
105
+ }
77
106
  return "brave";
78
107
  }
79
108
  function resolvePerplexityConfig(search) {
80
- if (!search || typeof search !== "object")
109
+ if (!search || typeof search !== "object") {
81
110
  return {};
111
+ }
82
112
  const perplexity = "perplexity" in search ? search.perplexity : undefined;
83
- if (!perplexity || typeof perplexity !== "object")
113
+ if (!perplexity || typeof perplexity !== "object") {
84
114
  return {};
115
+ }
85
116
  return perplexity;
86
117
  }
87
118
  function resolvePerplexityApiKey(perplexity) {
@@ -100,11 +131,12 @@ function resolvePerplexityApiKey(perplexity) {
100
131
  return { apiKey: undefined, source: "none" };
101
132
  }
102
133
  function normalizeApiKey(key) {
103
- return typeof key === "string" ? key.trim() : "";
134
+ return normalizeSecretInput(key);
104
135
  }
105
136
  function inferPerplexityBaseUrlFromApiKey(apiKey) {
106
- if (!apiKey)
137
+ if (!apiKey) {
107
138
  return undefined;
139
+ }
108
140
  const normalized = apiKey.toLowerCase();
109
141
  if (PERPLEXITY_KEY_PREFIXES.some((prefix) => normalized.startsWith(prefix))) {
110
142
  return "direct";
@@ -118,18 +150,23 @@ function resolvePerplexityBaseUrl(perplexity, apiKeySource = "none", apiKey) {
118
150
  const fromConfig = perplexity && "baseUrl" in perplexity && typeof perplexity.baseUrl === "string"
119
151
  ? perplexity.baseUrl.trim()
120
152
  : "";
121
- if (fromConfig)
153
+ if (fromConfig) {
122
154
  return fromConfig;
123
- if (apiKeySource === "perplexity_env")
155
+ }
156
+ if (apiKeySource === "perplexity_env") {
124
157
  return PERPLEXITY_DIRECT_BASE_URL;
125
- if (apiKeySource === "openrouter_env")
158
+ }
159
+ if (apiKeySource === "openrouter_env") {
126
160
  return DEFAULT_PERPLEXITY_BASE_URL;
161
+ }
127
162
  if (apiKeySource === "config") {
128
163
  const inferred = inferPerplexityBaseUrlFromApiKey(apiKey);
129
- if (inferred === "direct")
164
+ if (inferred === "direct") {
130
165
  return PERPLEXITY_DIRECT_BASE_URL;
131
- if (inferred === "openrouter")
166
+ }
167
+ if (inferred === "openrouter") {
132
168
  return DEFAULT_PERPLEXITY_BASE_URL;
169
+ }
133
170
  }
134
171
  return DEFAULT_PERPLEXITY_BASE_URL;
135
172
  }
@@ -139,42 +176,94 @@ function resolvePerplexityModel(perplexity) {
139
176
  : "";
140
177
  return fromConfig || DEFAULT_PERPLEXITY_MODEL;
141
178
  }
179
+ function isDirectPerplexityBaseUrl(baseUrl) {
180
+ const trimmed = baseUrl.trim();
181
+ if (!trimmed) {
182
+ return false;
183
+ }
184
+ try {
185
+ return new URL(trimmed).hostname.toLowerCase() === "api.perplexity.ai";
186
+ }
187
+ catch {
188
+ return false;
189
+ }
190
+ }
191
+ function resolvePerplexityRequestModel(baseUrl, model) {
192
+ if (!isDirectPerplexityBaseUrl(baseUrl)) {
193
+ return model;
194
+ }
195
+ return model.startsWith("perplexity/") ? model.slice("perplexity/".length) : model;
196
+ }
197
+ function resolveGrokConfig(search) {
198
+ if (!search || typeof search !== "object") {
199
+ return {};
200
+ }
201
+ const grok = "grok" in search ? search.grok : undefined;
202
+ if (!grok || typeof grok !== "object") {
203
+ return {};
204
+ }
205
+ return grok;
206
+ }
207
+ function resolveGrokApiKey(grok) {
208
+ const fromConfig = normalizeApiKey(grok?.apiKey);
209
+ if (fromConfig) {
210
+ return fromConfig;
211
+ }
212
+ const fromEnv = normalizeApiKey(process.env.XAI_API_KEY);
213
+ return fromEnv || undefined;
214
+ }
215
+ function resolveGrokModel(grok) {
216
+ const fromConfig = grok && "model" in grok && typeof grok.model === "string" ? grok.model.trim() : "";
217
+ return fromConfig || DEFAULT_GROK_MODEL;
218
+ }
219
+ function resolveGrokInlineCitations(grok) {
220
+ return grok?.inlineCitations === true;
221
+ }
142
222
  function resolveSearchCount(value, fallback) {
143
223
  const parsed = typeof value === "number" && Number.isFinite(value) ? value : fallback;
144
224
  const clamped = Math.max(1, Math.min(MAX_SEARCH_COUNT, Math.floor(parsed)));
145
225
  return clamped;
146
226
  }
147
227
  function normalizeFreshness(value) {
148
- if (!value)
228
+ if (!value) {
149
229
  return undefined;
230
+ }
150
231
  const trimmed = value.trim();
151
- if (!trimmed)
232
+ if (!trimmed) {
152
233
  return undefined;
234
+ }
153
235
  const lower = trimmed.toLowerCase();
154
- if (BRAVE_FRESHNESS_SHORTCUTS.has(lower))
236
+ if (BRAVE_FRESHNESS_SHORTCUTS.has(lower)) {
155
237
  return lower;
238
+ }
156
239
  const match = trimmed.match(BRAVE_FRESHNESS_RANGE);
157
- if (!match)
240
+ if (!match) {
158
241
  return undefined;
242
+ }
159
243
  const [, start, end] = match;
160
- if (!isValidIsoDate(start) || !isValidIsoDate(end))
244
+ if (!isValidIsoDate(start) || !isValidIsoDate(end)) {
161
245
  return undefined;
162
- if (start > end)
246
+ }
247
+ if (start > end) {
163
248
  return undefined;
249
+ }
164
250
  return `${start}to${end}`;
165
251
  }
166
252
  function isValidIsoDate(value) {
167
- if (!/^\d{4}-\d{2}-\d{2}$/.test(value))
253
+ if (!/^\d{4}-\d{2}-\d{2}$/.test(value)) {
168
254
  return false;
255
+ }
169
256
  const [year, month, day] = value.split("-").map((part) => Number.parseInt(part, 10));
170
- if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day))
257
+ if (!Number.isFinite(year) || !Number.isFinite(month) || !Number.isFinite(day)) {
171
258
  return false;
259
+ }
172
260
  const date = new Date(Date.UTC(year, month - 1, day));
173
261
  return (date.getUTCFullYear() === year && date.getUTCMonth() === month - 1 && date.getUTCDate() === day);
174
262
  }
175
263
  function resolveSiteName(url) {
176
- if (!url)
264
+ if (!url) {
177
265
  return undefined;
266
+ }
178
267
  try {
179
268
  return new URL(url).hostname;
180
269
  }
@@ -183,17 +272,19 @@ function resolveSiteName(url) {
183
272
  }
184
273
  }
185
274
  async function runPerplexitySearch(params) {
186
- const endpoint = `${params.baseUrl.replace(/\/$/, "")}/chat/completions`;
275
+ const baseUrl = params.baseUrl.trim().replace(/\/$/, "");
276
+ const endpoint = `${baseUrl}/chat/completions`;
277
+ const model = resolvePerplexityRequestModel(baseUrl, params.model);
187
278
  const res = await fetch(endpoint, {
188
279
  method: "POST",
189
280
  headers: {
190
281
  "Content-Type": "application/json",
191
282
  Authorization: `Bearer ${params.apiKey}`,
192
283
  "HTTP-Referer": "https://molt.bot",
193
- "X-Title": "Poolbot Web Search",
284
+ "X-Title": "Pool Bot Web Search",
194
285
  },
195
286
  body: JSON.stringify({
196
- model: params.model,
287
+ model,
197
288
  messages: [
198
289
  {
199
290
  role: "user",
@@ -212,13 +303,49 @@ async function runPerplexitySearch(params) {
212
303
  const citations = data.citations ?? [];
213
304
  return { content, citations };
214
305
  }
306
+ async function runGrokSearch(params) {
307
+ const body = {
308
+ model: params.model,
309
+ input: [
310
+ {
311
+ role: "user",
312
+ content: params.query,
313
+ },
314
+ ],
315
+ tools: [{ type: "web_search" }],
316
+ };
317
+ if (params.inlineCitations) {
318
+ body.include = ["inline_citations"];
319
+ }
320
+ const res = await fetch(XAI_API_ENDPOINT, {
321
+ method: "POST",
322
+ headers: {
323
+ "Content-Type": "application/json",
324
+ Authorization: `Bearer ${params.apiKey}`,
325
+ },
326
+ body: JSON.stringify(body),
327
+ signal: withTimeout(undefined, params.timeoutSeconds * 1000),
328
+ });
329
+ if (!res.ok) {
330
+ const detail = await readResponseText(res);
331
+ throw new Error(`xAI API error (${res.status}): ${detail || res.statusText}`);
332
+ }
333
+ const data = (await res.json());
334
+ const content = extractGrokContent(data) ?? "No response";
335
+ const citations = data.citations ?? [];
336
+ const inlineCitations = data.inline_citations;
337
+ return { content, citations, inlineCitations };
338
+ }
215
339
  async function runWebSearch(params) {
216
340
  const cacheKey = normalizeCacheKey(params.provider === "brave"
217
341
  ? `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}:${params.freshness || "default"}`
218
- : `${params.provider}:${params.query}:${params.count}:${params.country || "default"}:${params.search_lang || "default"}:${params.ui_lang || "default"}`);
342
+ : params.provider === "perplexity"
343
+ ? `${params.provider}:${params.query}:${params.perplexityBaseUrl ?? DEFAULT_PERPLEXITY_BASE_URL}:${params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL}`
344
+ : `${params.provider}:${params.query}:${params.grokModel ?? DEFAULT_GROK_MODEL}:${String(params.grokInlineCitations ?? false)}`);
219
345
  const cached = readCache(SEARCH_CACHE, cacheKey);
220
- if (cached)
346
+ if (cached) {
221
347
  return { ...cached.value, cached: true };
348
+ }
222
349
  const start = Date.now();
223
350
  if (params.provider === "perplexity") {
224
351
  const { content, citations } = await runPerplexitySearch({
@@ -233,8 +360,28 @@ async function runWebSearch(params) {
233
360
  provider: params.provider,
234
361
  model: params.perplexityModel ?? DEFAULT_PERPLEXITY_MODEL,
235
362
  tookMs: Date.now() - start,
236
- content,
363
+ content: wrapWebContent(content),
364
+ citations,
365
+ };
366
+ writeCache(SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs);
367
+ return payload;
368
+ }
369
+ if (params.provider === "grok") {
370
+ const { content, citations, inlineCitations } = await runGrokSearch({
371
+ query: params.query,
372
+ apiKey: params.apiKey,
373
+ model: params.grokModel ?? DEFAULT_GROK_MODEL,
374
+ timeoutSeconds: params.timeoutSeconds,
375
+ inlineCitations: params.grokInlineCitations ?? false,
376
+ });
377
+ const payload = {
378
+ query: params.query,
379
+ provider: params.provider,
380
+ model: params.grokModel ?? DEFAULT_GROK_MODEL,
381
+ tookMs: Date.now() - start,
382
+ content: wrapWebContent(content),
237
383
  citations,
384
+ inlineCitations,
238
385
  };
239
386
  writeCache(SEARCH_CACHE, cacheKey, payload, params.cacheTtlMs);
240
387
  return payload;
@@ -271,13 +418,19 @@ async function runWebSearch(params) {
271
418
  }
272
419
  const data = (await res.json());
273
420
  const results = Array.isArray(data.web?.results) ? (data.web?.results ?? []) : [];
274
- const mapped = results.map((entry) => ({
275
- title: entry.title ?? "",
276
- url: entry.url ?? "",
277
- description: entry.description ?? "",
278
- published: entry.age ?? undefined,
279
- siteName: resolveSiteName(entry.url ?? ""),
280
- }));
421
+ const mapped = results.map((entry) => {
422
+ const description = entry.description ?? "";
423
+ const title = entry.title ?? "";
424
+ const url = entry.url ?? "";
425
+ const rawSiteName = resolveSiteName(url);
426
+ return {
427
+ title: title ? wrapWebContent(title, "web_search") : "",
428
+ url, // Keep raw for tool chaining
429
+ description: description ? wrapWebContent(description, "web_search") : "",
430
+ published: entry.age || undefined,
431
+ siteName: rawSiteName || undefined,
432
+ };
433
+ });
281
434
  const payload = {
282
435
  query: params.query,
283
436
  provider: params.provider,
@@ -290,13 +443,17 @@ async function runWebSearch(params) {
290
443
  }
291
444
  export function createWebSearchTool(options) {
292
445
  const search = resolveSearchConfig(options?.config);
293
- if (!resolveSearchEnabled({ search, sandboxed: options?.sandboxed }))
446
+ if (!resolveSearchEnabled({ search, sandboxed: options?.sandboxed })) {
294
447
  return null;
448
+ }
295
449
  const provider = resolveSearchProvider(search);
296
450
  const perplexityConfig = resolvePerplexityConfig(search);
451
+ const grokConfig = resolveGrokConfig(search);
297
452
  const description = provider === "perplexity"
298
453
  ? "Search the web using Perplexity Sonar (direct or via OpenRouter). Returns AI-synthesized answers with citations from real-time web search."
299
- : "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.";
454
+ : provider === "grok"
455
+ ? "Search the web using xAI Grok. Returns AI-synthesized answers with citations from real-time web search."
456
+ : "Search the web using Brave Search API. Supports region-specific and localized search via country and language parameters. Returns titles, URLs, and snippets for fast research.";
300
457
  return {
301
458
  label: "Web Search",
302
459
  name: "web_search",
@@ -304,7 +461,11 @@ export function createWebSearchTool(options) {
304
461
  parameters: WebSearchSchema,
305
462
  execute: async (_toolCallId, args) => {
306
463
  const perplexityAuth = provider === "perplexity" ? resolvePerplexityApiKey(perplexityConfig) : undefined;
307
- const apiKey = provider === "perplexity" ? perplexityAuth?.apiKey : resolveSearchApiKey(search);
464
+ const apiKey = provider === "perplexity"
465
+ ? perplexityAuth?.apiKey
466
+ : provider === "grok"
467
+ ? resolveGrokApiKey(grokConfig)
468
+ : resolveSearchApiKey(search);
308
469
  if (!apiKey) {
309
470
  return jsonResult(missingSearchKeyPayload(provider));
310
471
  }
@@ -343,6 +504,8 @@ export function createWebSearchTool(options) {
343
504
  freshness,
344
505
  perplexityBaseUrl: resolvePerplexityBaseUrl(perplexityConfig, perplexityAuth?.source, perplexityAuth?.apiKey),
345
506
  perplexityModel: resolvePerplexityModel(perplexityConfig),
507
+ grokModel: resolveGrokModel(grokConfig),
508
+ grokInlineCitations: resolveGrokInlineCitations(grokConfig),
346
509
  });
347
510
  return jsonResult(result);
348
511
  },
@@ -351,5 +514,11 @@ export function createWebSearchTool(options) {
351
514
  export const __testing = {
352
515
  inferPerplexityBaseUrlFromApiKey,
353
516
  resolvePerplexityBaseUrl,
517
+ isDirectPerplexityBaseUrl,
518
+ resolvePerplexityRequestModel,
354
519
  normalizeFreshness,
520
+ resolveGrokApiKey,
521
+ resolveGrokModel,
522
+ resolveGrokInlineCitations,
523
+ extractGrokContent,
355
524
  };
@@ -38,11 +38,33 @@ export function normalizeUsage(raw) {
38
38
  };
39
39
  }
40
40
  export function derivePromptTokens(usage) {
41
- if (!usage)
41
+ if (!usage) {
42
42
  return undefined;
43
+ }
43
44
  const input = usage.input ?? 0;
44
45
  const cacheRead = usage.cacheRead ?? 0;
45
46
  const cacheWrite = usage.cacheWrite ?? 0;
46
47
  const sum = input + cacheRead + cacheWrite;
47
48
  return sum > 0 ? sum : undefined;
48
49
  }
50
+ export function deriveSessionTotalTokens(params) {
51
+ const usage = params.usage;
52
+ if (!usage) {
53
+ return undefined;
54
+ }
55
+ const input = usage.input ?? 0;
56
+ const promptTokens = derivePromptTokens({
57
+ input: usage.input,
58
+ cacheRead: usage.cacheRead,
59
+ cacheWrite: usage.cacheWrite,
60
+ });
61
+ let total = promptTokens ?? usage.total ?? input;
62
+ if (!(total > 0)) {
63
+ return undefined;
64
+ }
65
+ const contextTokens = params.contextTokens;
66
+ if (typeof contextTokens === "number" && Number.isFinite(contextTokens) && contextTokens > 0) {
67
+ total = Math.min(total, contextTokens);
68
+ }
69
+ return total;
70
+ }
@@ -0,0 +1,67 @@
1
+ import { redactIdentifier } from "../logging/redact-identifier.js";
2
+ import { classifySessionKeyShape, DEFAULT_AGENT_ID, normalizeAgentId, parseAgentSessionKey, } from "../routing/session-key.js";
3
+ import { resolveUserPath } from "../utils.js";
4
+ import { resolveAgentWorkspaceDir, resolveDefaultAgentId } from "./agent-scope.js";
5
+ function resolveRunAgentId(params) {
6
+ const rawSessionKey = params.sessionKey?.trim() ?? "";
7
+ const shape = classifySessionKeyShape(rawSessionKey);
8
+ if (shape === "malformed_agent") {
9
+ throw new Error("Malformed agent session key; refusing workspace resolution.");
10
+ }
11
+ const explicit = typeof params.agentId === "string" && params.agentId.trim()
12
+ ? normalizeAgentId(params.agentId)
13
+ : undefined;
14
+ if (explicit) {
15
+ return { agentId: explicit, agentIdSource: "explicit" };
16
+ }
17
+ const defaultAgentId = resolveDefaultAgentId(params.config ?? {});
18
+ if (shape === "missing" || shape === "legacy_or_alias") {
19
+ return {
20
+ agentId: defaultAgentId || DEFAULT_AGENT_ID,
21
+ agentIdSource: "default",
22
+ };
23
+ }
24
+ const parsed = parseAgentSessionKey(rawSessionKey);
25
+ if (parsed?.agentId) {
26
+ return {
27
+ agentId: normalizeAgentId(parsed.agentId),
28
+ agentIdSource: "session_key",
29
+ };
30
+ }
31
+ // Defensive fallback, should be unreachable for non-malformed shapes.
32
+ return {
33
+ agentId: defaultAgentId || DEFAULT_AGENT_ID,
34
+ agentIdSource: "default",
35
+ };
36
+ }
37
+ export function redactRunIdentifier(value) {
38
+ return redactIdentifier(value, { len: 12 });
39
+ }
40
+ export function resolveRunWorkspaceDir(params) {
41
+ const requested = params.workspaceDir;
42
+ const { agentId, agentIdSource } = resolveRunAgentId({
43
+ sessionKey: params.sessionKey,
44
+ agentId: params.agentId,
45
+ config: params.config,
46
+ });
47
+ if (typeof requested === "string") {
48
+ const trimmed = requested.trim();
49
+ if (trimmed) {
50
+ return {
51
+ workspaceDir: resolveUserPath(trimmed),
52
+ usedFallback: false,
53
+ agentId,
54
+ agentIdSource,
55
+ };
56
+ }
57
+ }
58
+ const fallbackReason = requested == null ? "missing" : typeof requested === "string" ? "blank" : "invalid_type";
59
+ const fallbackWorkspace = resolveAgentWorkspaceDir(params.config ?? {}, agentId);
60
+ return {
61
+ workspaceDir: resolveUserPath(fallbackWorkspace),
62
+ usedFallback: true,
63
+ fallbackReason,
64
+ agentId,
65
+ agentIdSource,
66
+ };
67
+ }
@@ -0,0 +1,44 @@
1
+ import path from "node:path";
2
+ import { fileURLToPath } from "node:url";
3
+ import { resolvePoolBotPackageRoot } from "../infra/poolbot-root.js";
4
+ import { pathExists } from "../utils.js";
5
+ const FALLBACK_TEMPLATE_DIR = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "../../docs/reference/templates");
6
+ let cachedTemplateDir;
7
+ let resolvingTemplateDir;
8
+ export async function resolveWorkspaceTemplateDir(opts) {
9
+ if (cachedTemplateDir) {
10
+ return cachedTemplateDir;
11
+ }
12
+ if (resolvingTemplateDir) {
13
+ return resolvingTemplateDir;
14
+ }
15
+ resolvingTemplateDir = (async () => {
16
+ const moduleUrl = opts?.moduleUrl ?? import.meta.url;
17
+ const argv1 = opts?.argv1 ?? process.argv[1];
18
+ const cwd = opts?.cwd ?? process.cwd();
19
+ const packageRoot = await resolvePoolBotPackageRoot({ moduleUrl, argv1, cwd });
20
+ const candidates = [
21
+ packageRoot ? path.join(packageRoot, "docs", "reference", "templates") : null,
22
+ cwd ? path.resolve(cwd, "docs", "reference", "templates") : null,
23
+ FALLBACK_TEMPLATE_DIR,
24
+ ].filter(Boolean);
25
+ for (const candidate of candidates) {
26
+ if (await pathExists(candidate)) {
27
+ cachedTemplateDir = candidate;
28
+ return candidate;
29
+ }
30
+ }
31
+ cachedTemplateDir = candidates[0] ?? FALLBACK_TEMPLATE_DIR;
32
+ return cachedTemplateDir;
33
+ })();
34
+ try {
35
+ return await resolvingTemplateDir;
36
+ }
37
+ finally {
38
+ resolvingTemplateDir = undefined;
39
+ }
40
+ }
41
+ export function resetWorkspaceTemplateDirCache() {
42
+ cachedTemplateDir = undefined;
43
+ resolvingTemplateDir = undefined;
44
+ }