@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,12 +1,16 @@
1
1
  import { createServer as createHttpServer, } from "node:http";
2
2
  import { createServer as createHttpsServer } from "node:https";
3
- import { handleA2uiHttpRequest } from "../canvas-host/a2ui.js";
3
+ import { A2UI_PATH, CANVAS_HOST_PATH, CANVAS_WS_PATH, handleA2uiHttpRequest, } from "../canvas-host/a2ui.js";
4
4
  import { loadConfig } from "../config/config.js";
5
- import { handleSlackHttpRequest } from "../slack/http/index.js";
6
5
  import { resolveAgentAvatar } from "../agents/identity-avatar.js";
7
- import { handleControlUiAvatarRequest, handleControlUiHttpRequest } from "./control-ui.js";
8
- import { extractHookToken, getHookChannelError, normalizeAgentPayload, normalizeHookHeaders, normalizeWakePayload, readJsonBody, resolveHookChannel, resolveHookDeliver, } from "./hooks.js";
6
+ import { handleSlackHttpRequest } from "../slack/http/index.js";
7
+ import { authorizeGatewayConnect, isLocalDirectRequest } from "./auth.js";
8
+ import { handleControlUiAvatarRequest, handleControlUiHttpRequest, } from "./control-ui.js";
9
+ import { extractHookToken, getHookAgentPolicyError, getHookChannelError, isHookAgentAllowed, normalizeAgentPayload, normalizeHookHeaders, normalizeWakePayload, readJsonBody, resolveHookTargetAgentId, resolveHookChannel, resolveHookDeliver, } from "./hooks.js";
9
10
  import { applyHookMappings } from "./hooks-mapping.js";
11
+ import { sendUnauthorized } from "./http-common.js";
12
+ import { getBearerToken, getHeader } from "./http-utils.js";
13
+ import { resolveGatewayClientIp } from "./net.js";
10
14
  import { handleOpenAiHttpRequest } from "./openai-http.js";
11
15
  import { handleOpenResponsesHttpRequest } from "./openresponses-http.js";
12
16
  import { handleToolsInvokeHttpRequest } from "./tools-invoke-http.js";
@@ -15,6 +19,49 @@ function sendJson(res, status, body) {
15
19
  res.setHeader("Content-Type", "application/json; charset=utf-8");
16
20
  res.end(JSON.stringify(body));
17
21
  }
22
+ function isCanvasPath(pathname) {
23
+ return (pathname === A2UI_PATH ||
24
+ pathname.startsWith(`${A2UI_PATH}/`) ||
25
+ pathname === CANVAS_HOST_PATH ||
26
+ pathname.startsWith(`${CANVAS_HOST_PATH}/`) ||
27
+ pathname === CANVAS_WS_PATH);
28
+ }
29
+ function hasAuthorizedWsClientForIp(clients, clientIp) {
30
+ for (const client of clients) {
31
+ if (client.clientIp && client.clientIp === clientIp) {
32
+ return true;
33
+ }
34
+ }
35
+ return false;
36
+ }
37
+ async function authorizeCanvasRequest(params) {
38
+ const { req, auth, trustedProxies, clients } = params;
39
+ if (isLocalDirectRequest(req, trustedProxies)) {
40
+ return true;
41
+ }
42
+ const token = getBearerToken(req);
43
+ if (token) {
44
+ const authResult = await authorizeGatewayConnect({
45
+ auth: { ...auth, allowTailscale: false },
46
+ connectAuth: { token, password: token },
47
+ req,
48
+ trustedProxies,
49
+ });
50
+ if (authResult.ok) {
51
+ return true;
52
+ }
53
+ }
54
+ const clientIp = resolveGatewayClientIp({
55
+ remoteAddr: req.socket?.remoteAddress ?? "",
56
+ forwardedFor: getHeader(req, "x-forwarded-for"),
57
+ realIp: getHeader(req, "x-real-ip"),
58
+ trustedProxies,
59
+ });
60
+ if (!clientIp) {
61
+ return false;
62
+ }
63
+ return hasAuthorizedWsClientForIp(clients, clientIp);
64
+ }
18
65
  export function createHooksRequestHandler(opts) {
19
66
  const { getHooksConfig, bindHost, port, logHooks, dispatchAgentHook, dispatchWakeHook } = opts;
20
67
  return async (req, res) => {
@@ -26,6 +73,8 @@ export function createHooksRequestHandler(opts) {
26
73
  if (url.pathname !== basePath && !url.pathname.startsWith(`${basePath}/`)) {
27
74
  return false;
28
75
  }
76
+ // pool-bot keeps the deprecation-warning approach for query-param tokens
77
+ // (upstream hard-rejects; we warn and allow for now)
29
78
  const { token, fromQuery } = extractHookToken(req, url);
30
79
  if (!token || token !== hooksConfig.token) {
31
80
  res.statusCode = 401;
@@ -76,7 +125,14 @@ export function createHooksRequestHandler(opts) {
76
125
  sendJson(res, 400, { ok: false, error: normalized.error });
77
126
  return true;
78
127
  }
79
- const runId = dispatchAgentHook(normalized.value);
128
+ if (!isHookAgentAllowed(hooksConfig, normalized.value.agentId)) {
129
+ sendJson(res, 400, { ok: false, error: getHookAgentPolicyError() });
130
+ return true;
131
+ }
132
+ const runId = dispatchAgentHook({
133
+ ...normalized.value,
134
+ agentId: resolveHookTargetAgentId(hooksConfig, normalized.value.agentId),
135
+ });
80
136
  sendJson(res, 202, { ok: true, runId });
81
137
  return true;
82
138
  }
@@ -111,9 +167,14 @@ export function createHooksRequestHandler(opts) {
111
167
  sendJson(res, 400, { ok: false, error: getHookChannelError() });
112
168
  return true;
113
169
  }
170
+ if (!isHookAgentAllowed(hooksConfig, mapped.action.agentId)) {
171
+ sendJson(res, 400, { ok: false, error: getHookAgentPolicyError() });
172
+ return true;
173
+ }
114
174
  const runId = dispatchAgentHook({
115
175
  message: mapped.action.message,
116
176
  name: mapped.action.name ?? "Hook",
177
+ agentId: resolveHookTargetAgentId(hooksConfig, mapped.action.agentId),
117
178
  wakeMode: mapped.action.wakeMode,
118
179
  sessionKey: mapped.action.sessionKey ?? "",
119
180
  deliver: resolveHookDeliver(mapped.action.deliver),
@@ -141,7 +202,7 @@ export function createHooksRequestHandler(opts) {
141
202
  };
142
203
  }
143
204
  export function createGatewayHttpServer(opts) {
144
- const { canvasHost, controlUiEnabled, controlUiBasePath, openAiChatCompletionsEnabled, openResponsesEnabled, openResponsesConfig, handleHooksRequest, handlePluginRequest, resolvedAuth, } = opts;
205
+ const { canvasHost, clients, controlUiEnabled, controlUiBasePath, controlUiRoot, openAiChatCompletionsEnabled, openResponsesEnabled, openResponsesConfig, handleHooksRequest, handlePluginRequest, resolvedAuth, } = opts;
145
206
  const httpServer = opts.tlsOptions
146
207
  ? createHttpsServer(opts.tlsOptions, (req, res) => {
147
208
  void handleRequest(req, res);
@@ -183,6 +244,19 @@ export function createGatewayHttpServer(opts) {
183
244
  return;
184
245
  }
185
246
  if (canvasHost) {
247
+ const url = new URL(req.url ?? "/", "http://localhost");
248
+ if (isCanvasPath(url.pathname)) {
249
+ const ok = await authorizeCanvasRequest({
250
+ req,
251
+ auth: resolvedAuth,
252
+ trustedProxies,
253
+ clients,
254
+ });
255
+ if (!ok) {
256
+ sendUnauthorized(res);
257
+ return;
258
+ }
259
+ }
186
260
  if (await handleA2uiHttpRequest(req, res))
187
261
  return;
188
262
  if (await canvasHost.handleHttpRequest(req, res))
@@ -197,6 +271,7 @@ export function createGatewayHttpServer(opts) {
197
271
  if (handleControlUiHttpRequest(req, res, {
198
272
  basePath: controlUiBasePath,
199
273
  config: configSnapshot,
274
+ root: controlUiRoot,
200
275
  }))
201
276
  return;
202
277
  }
@@ -213,12 +288,35 @@ export function createGatewayHttpServer(opts) {
213
288
  return httpServer;
214
289
  }
215
290
  export function attachGatewayUpgradeHandler(opts) {
216
- const { httpServer, wss, canvasHost } = opts;
291
+ const { httpServer, wss, canvasHost, clients, resolvedAuth } = opts;
217
292
  httpServer.on("upgrade", (req, socket, head) => {
218
- if (canvasHost?.handleUpgrade(req, socket, head))
219
- return;
220
- wss.handleUpgrade(req, socket, head, (ws) => {
221
- wss.emit("connection", ws, req);
293
+ void (async () => {
294
+ if (canvasHost) {
295
+ const url = new URL(req.url ?? "/", "http://localhost");
296
+ if (url.pathname === CANVAS_WS_PATH) {
297
+ const configSnapshot = loadConfig();
298
+ const trustedProxies = configSnapshot.gateway?.trustedProxies ?? [];
299
+ const ok = await authorizeCanvasRequest({
300
+ req,
301
+ auth: resolvedAuth,
302
+ trustedProxies,
303
+ clients,
304
+ });
305
+ if (!ok) {
306
+ socket.write("HTTP/1.1 401 Unauthorized\r\nConnection: close\r\n\r\n");
307
+ socket.destroy();
308
+ return;
309
+ }
310
+ }
311
+ if (canvasHost.handleUpgrade(req, socket, head)) {
312
+ return;
313
+ }
314
+ }
315
+ wss.handleUpgrade(req, socket, head, (ws) => {
316
+ wss.emit("connection", ws, req);
317
+ });
318
+ })().catch(() => {
319
+ socket.destroy();
222
320
  });
223
321
  });
224
322
  }
@@ -0,0 +1,60 @@
1
+ import { resolveUserTimezone } from "../../agents/date-time.js";
2
+ import { formatZonedTimestamp } from "../../infra/format-time/format-datetime.js";
3
+ /**
4
+ * Cron jobs inject "Current time: ..." into their messages.
5
+ * Skip injection for those.
6
+ */
7
+ const CRON_TIME_PATTERN = /Current time: /;
8
+ /**
9
+ * Matches a leading `[... YYYY-MM-DD HH:MM ...]` envelope -- either from
10
+ * channel plugins or from a previous injection. Uses the same YYYY-MM-DD
11
+ * HH:MM format as {@link formatZonedTimestamp}, so detection stays in sync
12
+ * with the formatting.
13
+ */
14
+ const TIMESTAMP_ENVELOPE_PATTERN = /^\[.*\d{4}-\d{2}-\d{2} \d{2}:\d{2}/;
15
+ /**
16
+ * Injects a compact timestamp prefix into a message if one isn't already
17
+ * present. Uses the same `YYYY-MM-DD HH:MM TZ` format as channel envelope
18
+ * timestamps ({@link formatZonedTimestamp}), keeping token cost low (~7
19
+ * tokens) and format consistent across all agent contexts.
20
+ *
21
+ * Used by the gateway `agent` and `chat.send` handlers to give TUI, web,
22
+ * spawned subagents, `sessions_send`, and heartbeat wake events date/time
23
+ * awareness -- without modifying the system prompt (which is cached).
24
+ *
25
+ * Channel messages (Discord, Telegram, etc.) already have timestamps via
26
+ * envelope formatting and take a separate code path -- they never reach
27
+ * these handlers, so there is no double-stamping risk. The detection
28
+ * pattern is a safety net for edge cases.
29
+ */
30
+ export function injectTimestamp(message, opts) {
31
+ if (!message.trim()) {
32
+ return message;
33
+ }
34
+ // Already has an envelope or injected timestamp
35
+ if (TIMESTAMP_ENVELOPE_PATTERN.test(message)) {
36
+ return message;
37
+ }
38
+ // Already has a cron-injected timestamp
39
+ if (CRON_TIME_PATTERN.test(message)) {
40
+ return message;
41
+ }
42
+ const now = opts?.now ?? new Date();
43
+ const timezone = opts?.timezone ?? "UTC";
44
+ const formatted = formatZonedTimestamp(now, { timeZone: timezone });
45
+ if (!formatted) {
46
+ return message;
47
+ }
48
+ // 3-letter DOW: small models (8B) can't reliably derive day-of-week from
49
+ // a date, and may treat a bare "Wed" as a typo. Costs ~1 token.
50
+ const dow = new Intl.DateTimeFormat("en-US", { timeZone: timezone, weekday: "short" }).format(now);
51
+ return `[${dow} ${formatted}] ${message}`;
52
+ }
53
+ /**
54
+ * Build TimestampInjectionOptions from a PoolBotConfig.
55
+ */
56
+ export function timestampOptsFromConfig(cfg) {
57
+ return {
58
+ timezone: resolveUserTimezone(cfg.agents?.defaults?.userTimezone),
59
+ };
60
+ }
@@ -1,6 +1,119 @@
1
- import { loadConfig } from "../../config/config.js";
2
- import { ErrorCodes, errorShape, formatValidationErrors, validateAgentsListParams, } from "../protocol/index.js";
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+ import { listAgentIds, resolveAgentDir, resolveAgentWorkspaceDir, } from "../../agents/agent-scope.js";
4
+ import { DEFAULT_AGENTS_FILENAME, DEFAULT_BOOTSTRAP_FILENAME, DEFAULT_HEARTBEAT_FILENAME, DEFAULT_IDENTITY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME, DEFAULT_MEMORY_FILENAME, DEFAULT_SOUL_FILENAME, DEFAULT_TOOLS_FILENAME, DEFAULT_USER_FILENAME, ensureAgentWorkspace, } from "../../agents/workspace.js";
5
+ import { movePathToTrash } from "../../browser/trash.js";
6
+ import { applyAgentConfig, findAgentEntryIndex, listAgentEntries, pruneAgentConfig, } from "../../commands/agents.config.js";
7
+ import { loadConfig, writeConfigFile } from "../../config/config.js";
8
+ import { resolveSessionTranscriptsDirForAgent } from "../../config/sessions/paths.js";
9
+ import { DEFAULT_AGENT_ID, normalizeAgentId } from "../../routing/session-key.js";
10
+ import { resolveUserPath } from "../../utils.js";
11
+ import { ErrorCodes, errorShape, formatValidationErrors, validateAgentsCreateParams, validateAgentsDeleteParams, validateAgentsFilesGetParams, validateAgentsFilesListParams, validateAgentsFilesSetParams, validateAgentsListParams, validateAgentsUpdateParams, } from "../protocol/index.js";
3
12
  import { listAgentsForGateway } from "../session-utils.js";
13
+ const BOOTSTRAP_FILE_NAMES = [
14
+ DEFAULT_AGENTS_FILENAME,
15
+ DEFAULT_SOUL_FILENAME,
16
+ DEFAULT_TOOLS_FILENAME,
17
+ DEFAULT_IDENTITY_FILENAME,
18
+ DEFAULT_USER_FILENAME,
19
+ DEFAULT_HEARTBEAT_FILENAME,
20
+ DEFAULT_BOOTSTRAP_FILENAME,
21
+ ];
22
+ const MEMORY_FILE_NAMES = [DEFAULT_MEMORY_FILENAME, DEFAULT_MEMORY_ALT_FILENAME];
23
+ const ALLOWED_FILE_NAMES = new Set([...BOOTSTRAP_FILE_NAMES, ...MEMORY_FILE_NAMES]);
24
+ async function statFile(filePath) {
25
+ try {
26
+ const stat = await fs.stat(filePath);
27
+ if (!stat.isFile()) {
28
+ return null;
29
+ }
30
+ return {
31
+ size: stat.size,
32
+ updatedAtMs: Math.floor(stat.mtimeMs),
33
+ };
34
+ }
35
+ catch {
36
+ return null;
37
+ }
38
+ }
39
+ async function listAgentFiles(workspaceDir) {
40
+ const files = [];
41
+ for (const name of BOOTSTRAP_FILE_NAMES) {
42
+ const filePath = path.join(workspaceDir, name);
43
+ const meta = await statFile(filePath);
44
+ if (meta) {
45
+ files.push({
46
+ name,
47
+ path: filePath,
48
+ missing: false,
49
+ size: meta.size,
50
+ updatedAtMs: meta.updatedAtMs,
51
+ });
52
+ }
53
+ else {
54
+ files.push({ name, path: filePath, missing: true });
55
+ }
56
+ }
57
+ const primaryMemoryPath = path.join(workspaceDir, DEFAULT_MEMORY_FILENAME);
58
+ const primaryMeta = await statFile(primaryMemoryPath);
59
+ if (primaryMeta) {
60
+ files.push({
61
+ name: DEFAULT_MEMORY_FILENAME,
62
+ path: primaryMemoryPath,
63
+ missing: false,
64
+ size: primaryMeta.size,
65
+ updatedAtMs: primaryMeta.updatedAtMs,
66
+ });
67
+ }
68
+ else {
69
+ const altMemoryPath = path.join(workspaceDir, DEFAULT_MEMORY_ALT_FILENAME);
70
+ const altMeta = await statFile(altMemoryPath);
71
+ if (altMeta) {
72
+ files.push({
73
+ name: DEFAULT_MEMORY_ALT_FILENAME,
74
+ path: altMemoryPath,
75
+ missing: false,
76
+ size: altMeta.size,
77
+ updatedAtMs: altMeta.updatedAtMs,
78
+ });
79
+ }
80
+ else {
81
+ files.push({ name: DEFAULT_MEMORY_FILENAME, path: primaryMemoryPath, missing: true });
82
+ }
83
+ }
84
+ return files;
85
+ }
86
+ function resolveAgentIdOrError(agentIdRaw, cfg) {
87
+ const agentId = normalizeAgentId(agentIdRaw);
88
+ const allowed = new Set(listAgentIds(cfg));
89
+ if (!allowed.has(agentId)) {
90
+ return null;
91
+ }
92
+ return agentId;
93
+ }
94
+ function sanitizeIdentityLine(value) {
95
+ return value.replace(/\s+/g, " ").trim();
96
+ }
97
+ function resolveOptionalStringParam(value) {
98
+ return typeof value === "string" && value.trim() ? value.trim() : undefined;
99
+ }
100
+ async function moveToTrashBestEffort(pathname) {
101
+ if (!pathname) {
102
+ return;
103
+ }
104
+ try {
105
+ await fs.access(pathname);
106
+ }
107
+ catch {
108
+ return;
109
+ }
110
+ try {
111
+ await movePathToTrash(pathname);
112
+ }
113
+ catch {
114
+ // Best-effort: path may already be gone or trash unavailable.
115
+ }
116
+ }
4
117
  export const agentsHandlers = {
5
118
  "agents.list": ({ params, respond }) => {
6
119
  if (!validateAgentsListParams(params)) {
@@ -11,4 +124,210 @@ export const agentsHandlers = {
11
124
  const result = listAgentsForGateway(cfg);
12
125
  respond(true, result, undefined);
13
126
  },
127
+ "agents.create": async ({ params, respond }) => {
128
+ if (!validateAgentsCreateParams(params)) {
129
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid agents.create params: ${formatValidationErrors(validateAgentsCreateParams.errors)}`));
130
+ return;
131
+ }
132
+ const cfg = loadConfig();
133
+ const rawName = String(params.name ?? "").trim();
134
+ const agentId = normalizeAgentId(rawName);
135
+ if (agentId === DEFAULT_AGENT_ID) {
136
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `"${DEFAULT_AGENT_ID}" is reserved`));
137
+ return;
138
+ }
139
+ if (findAgentEntryIndex(listAgentEntries(cfg), agentId) >= 0) {
140
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `agent "${agentId}" already exists`));
141
+ return;
142
+ }
143
+ const workspaceDir = resolveUserPath(String(params.workspace ?? "").trim());
144
+ // Resolve agentDir against the config we're about to persist (vs the pre-write config),
145
+ // so subsequent resolutions can't disagree about the agent's directory.
146
+ let nextConfig = applyAgentConfig(cfg, {
147
+ agentId,
148
+ name: rawName,
149
+ workspace: workspaceDir,
150
+ });
151
+ const agentDir = resolveAgentDir(nextConfig, agentId);
152
+ nextConfig = applyAgentConfig(nextConfig, { agentId, agentDir });
153
+ // Ensure workspace & transcripts exist BEFORE writing config so a failure
154
+ // here does not leave a broken config entry behind.
155
+ const skipBootstrap = Boolean(nextConfig.agents?.defaults?.skipBootstrap);
156
+ await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap });
157
+ await fs.mkdir(resolveSessionTranscriptsDirForAgent(agentId), { recursive: true });
158
+ await writeConfigFile(nextConfig);
159
+ // Always write Name to IDENTITY.md; optionally include emoji/avatar.
160
+ const safeName = sanitizeIdentityLine(rawName);
161
+ const emoji = resolveOptionalStringParam(params.emoji);
162
+ const avatar = resolveOptionalStringParam(params.avatar);
163
+ const identityPath = path.join(workspaceDir, DEFAULT_IDENTITY_FILENAME);
164
+ const lines = [
165
+ "",
166
+ `- Name: ${safeName}`,
167
+ ...(emoji ? [`- Emoji: ${sanitizeIdentityLine(emoji)}`] : []),
168
+ ...(avatar ? [`- Avatar: ${sanitizeIdentityLine(avatar)}`] : []),
169
+ "",
170
+ ];
171
+ await fs.appendFile(identityPath, lines.join("\n"), "utf-8");
172
+ respond(true, { ok: true, agentId, name: rawName, workspace: workspaceDir }, undefined);
173
+ },
174
+ "agents.update": async ({ params, respond }) => {
175
+ if (!validateAgentsUpdateParams(params)) {
176
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid agents.update params: ${formatValidationErrors(validateAgentsUpdateParams.errors)}`));
177
+ return;
178
+ }
179
+ const cfg = loadConfig();
180
+ const agentId = normalizeAgentId(String(params.agentId ?? ""));
181
+ if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0) {
182
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `agent "${agentId}" not found`));
183
+ return;
184
+ }
185
+ const workspaceDir = typeof params.workspace === "string" && params.workspace.trim()
186
+ ? resolveUserPath(params.workspace.trim())
187
+ : undefined;
188
+ const model = resolveOptionalStringParam(params.model);
189
+ const avatar = resolveOptionalStringParam(params.avatar);
190
+ const nextConfig = applyAgentConfig(cfg, {
191
+ agentId,
192
+ ...(typeof params.name === "string" && params.name.trim()
193
+ ? { name: params.name.trim() }
194
+ : {}),
195
+ ...(workspaceDir ? { workspace: workspaceDir } : {}),
196
+ ...(model ? { model } : {}),
197
+ });
198
+ await writeConfigFile(nextConfig);
199
+ if (workspaceDir) {
200
+ const skipBootstrap = Boolean(nextConfig.agents?.defaults?.skipBootstrap);
201
+ await ensureAgentWorkspace({ dir: workspaceDir, ensureBootstrapFiles: !skipBootstrap });
202
+ }
203
+ if (avatar) {
204
+ const workspace = workspaceDir ?? resolveAgentWorkspaceDir(nextConfig, agentId);
205
+ await fs.mkdir(workspace, { recursive: true });
206
+ const identityPath = path.join(workspace, DEFAULT_IDENTITY_FILENAME);
207
+ await fs.appendFile(identityPath, `\n- Avatar: ${sanitizeIdentityLine(avatar)}\n`, "utf-8");
208
+ }
209
+ respond(true, { ok: true, agentId }, undefined);
210
+ },
211
+ "agents.delete": async ({ params, respond }) => {
212
+ if (!validateAgentsDeleteParams(params)) {
213
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid agents.delete params: ${formatValidationErrors(validateAgentsDeleteParams.errors)}`));
214
+ return;
215
+ }
216
+ const cfg = loadConfig();
217
+ const agentId = normalizeAgentId(String(params.agentId ?? ""));
218
+ if (agentId === DEFAULT_AGENT_ID) {
219
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `"${DEFAULT_AGENT_ID}" cannot be deleted`));
220
+ return;
221
+ }
222
+ if (findAgentEntryIndex(listAgentEntries(cfg), agentId) < 0) {
223
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `agent "${agentId}" not found`));
224
+ return;
225
+ }
226
+ const deleteFiles = typeof params.deleteFiles === "boolean" ? params.deleteFiles : true;
227
+ const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
228
+ const agentDir = resolveAgentDir(cfg, agentId);
229
+ const sessionsDir = resolveSessionTranscriptsDirForAgent(agentId);
230
+ const result = pruneAgentConfig(cfg, agentId);
231
+ await writeConfigFile(result.config);
232
+ if (deleteFiles) {
233
+ await Promise.all([
234
+ moveToTrashBestEffort(workspaceDir),
235
+ moveToTrashBestEffort(agentDir),
236
+ moveToTrashBestEffort(sessionsDir),
237
+ ]);
238
+ }
239
+ respond(true, { ok: true, agentId, removedBindings: result.removedBindings }, undefined);
240
+ },
241
+ "agents.files.list": async ({ params, respond }) => {
242
+ if (!validateAgentsFilesListParams(params)) {
243
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid agents.files.list params: ${formatValidationErrors(validateAgentsFilesListParams.errors)}`));
244
+ return;
245
+ }
246
+ const cfg = loadConfig();
247
+ const agentId = resolveAgentIdOrError(String(params.agentId ?? ""), cfg);
248
+ if (!agentId) {
249
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown agent id"));
250
+ return;
251
+ }
252
+ const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
253
+ const files = await listAgentFiles(workspaceDir);
254
+ respond(true, { agentId, workspace: workspaceDir, files }, undefined);
255
+ },
256
+ "agents.files.get": async ({ params, respond }) => {
257
+ if (!validateAgentsFilesGetParams(params)) {
258
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid agents.files.get params: ${formatValidationErrors(validateAgentsFilesGetParams.errors)}`));
259
+ return;
260
+ }
261
+ const cfg = loadConfig();
262
+ const agentId = resolveAgentIdOrError(String(params.agentId ?? ""), cfg);
263
+ if (!agentId) {
264
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown agent id"));
265
+ return;
266
+ }
267
+ const name = String(params.name ?? "").trim();
268
+ if (!ALLOWED_FILE_NAMES.has(name)) {
269
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `unsupported file "${name}"`));
270
+ return;
271
+ }
272
+ const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
273
+ const filePath = path.join(workspaceDir, name);
274
+ const meta = await statFile(filePath);
275
+ if (!meta) {
276
+ respond(true, {
277
+ agentId,
278
+ workspace: workspaceDir,
279
+ file: { name, path: filePath, missing: true },
280
+ }, undefined);
281
+ return;
282
+ }
283
+ const content = await fs.readFile(filePath, "utf-8");
284
+ respond(true, {
285
+ agentId,
286
+ workspace: workspaceDir,
287
+ file: {
288
+ name,
289
+ path: filePath,
290
+ missing: false,
291
+ size: meta.size,
292
+ updatedAtMs: meta.updatedAtMs,
293
+ content,
294
+ },
295
+ }, undefined);
296
+ },
297
+ "agents.files.set": async ({ params, respond }) => {
298
+ if (!validateAgentsFilesSetParams(params)) {
299
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `invalid agents.files.set params: ${formatValidationErrors(validateAgentsFilesSetParams.errors)}`));
300
+ return;
301
+ }
302
+ const cfg = loadConfig();
303
+ const agentId = resolveAgentIdOrError(String(params.agentId ?? ""), cfg);
304
+ if (!agentId) {
305
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, "unknown agent id"));
306
+ return;
307
+ }
308
+ const name = String(params.name ?? "").trim();
309
+ if (!ALLOWED_FILE_NAMES.has(name)) {
310
+ respond(false, undefined, errorShape(ErrorCodes.INVALID_REQUEST, `unsupported file "${name}"`));
311
+ return;
312
+ }
313
+ const workspaceDir = resolveAgentWorkspaceDir(cfg, agentId);
314
+ await fs.mkdir(workspaceDir, { recursive: true });
315
+ const filePath = path.join(workspaceDir, name);
316
+ const content = String(params.content ?? "");
317
+ await fs.writeFile(filePath, content, "utf-8");
318
+ const meta = await statFile(filePath);
319
+ respond(true, {
320
+ ok: true,
321
+ agentId,
322
+ workspace: workspaceDir,
323
+ file: {
324
+ name,
325
+ path: filePath,
326
+ missing: false,
327
+ size: meta?.size,
328
+ updatedAtMs: meta?.updatedAtMs,
329
+ content,
330
+ },
331
+ }, undefined);
332
+ },
14
333
  };