@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
@@ -49,6 +49,64 @@ function normalizeAllowFromEntry(params) {
49
49
  });
50
50
  return normalized.filter((entry) => entry.trim().length > 0);
51
51
  }
52
+ function resolveOwnerAllowFromList(params) {
53
+ const raw = params.allowFrom ?? params.cfg.commands?.ownerAllowFrom;
54
+ if (!Array.isArray(raw) || raw.length === 0) {
55
+ return [];
56
+ }
57
+ const filtered = [];
58
+ for (const entry of raw) {
59
+ const trimmed = String(entry ?? "").trim();
60
+ if (!trimmed)
61
+ continue;
62
+ const separatorIndex = trimmed.indexOf(":");
63
+ if (separatorIndex > 0) {
64
+ const prefix = trimmed.slice(0, separatorIndex);
65
+ const channel = normalizeAnyChannelId(prefix);
66
+ if (channel) {
67
+ if (params.providerId && channel !== params.providerId)
68
+ continue;
69
+ const remainder = trimmed.slice(separatorIndex + 1).trim();
70
+ if (remainder)
71
+ filtered.push(remainder);
72
+ continue;
73
+ }
74
+ }
75
+ filtered.push(trimmed);
76
+ }
77
+ return formatAllowFromList({
78
+ dock: params.dock,
79
+ cfg: params.cfg,
80
+ accountId: params.accountId,
81
+ allowFrom: filtered,
82
+ });
83
+ }
84
+ /**
85
+ * Resolves the commands.allowFrom list for a given provider.
86
+ * Returns the provider-specific list if defined, otherwise the "*" global list.
87
+ * Returns null if commands.allowFrom is not configured at all (fall back to channel allowFrom).
88
+ */
89
+ function resolveCommandsAllowFromList(params) {
90
+ const { dock, cfg, accountId, providerId } = params;
91
+ const commandsAllowFrom = cfg.commands?.allowFrom;
92
+ if (!commandsAllowFrom || typeof commandsAllowFrom !== "object") {
93
+ return null; // Not configured, fall back to channel allowFrom
94
+ }
95
+ // Check provider-specific list first, then fall back to global "*"
96
+ const providerKey = providerId ?? "";
97
+ const providerList = commandsAllowFrom[providerKey];
98
+ const globalList = commandsAllowFrom["*"];
99
+ const rawList = Array.isArray(providerList) ? providerList : globalList;
100
+ if (!Array.isArray(rawList)) {
101
+ return null; // No applicable list found
102
+ }
103
+ return formatAllowFromList({
104
+ dock,
105
+ cfg,
106
+ accountId,
107
+ allowFrom: rawList,
108
+ });
109
+ }
52
110
  function resolveSenderCandidates(params) {
53
111
  const { dock, cfg, accountId } = params;
54
112
  const candidates = [];
@@ -83,6 +141,13 @@ export function resolveCommandAuthorization(params) {
83
141
  const dock = providerId ? getChannelDock(providerId) : undefined;
84
142
  const from = (ctx.From ?? "").trim();
85
143
  const to = (ctx.To ?? "").trim();
144
+ // Check if commands.allowFrom is configured (separate command authorization)
145
+ const commandsAllowFromList = resolveCommandsAllowFromList({
146
+ dock,
147
+ cfg,
148
+ accountId: ctx.AccountId,
149
+ providerId,
150
+ });
86
151
  const allowFromRaw = dock?.config?.resolveAllowFrom
87
152
  ? dock.config.resolveAllowFrom({ cfg, accountId: ctx.AccountId })
88
153
  : [];
@@ -92,9 +157,23 @@ export function resolveCommandAuthorization(params) {
92
157
  accountId: ctx.AccountId,
93
158
  allowFrom: Array.isArray(allowFromRaw) ? allowFromRaw : [],
94
159
  });
160
+ const configOwnerAllowFromList = resolveOwnerAllowFromList({
161
+ dock,
162
+ cfg,
163
+ accountId: ctx.AccountId,
164
+ providerId,
165
+ allowFrom: cfg.commands?.ownerAllowFrom,
166
+ });
167
+ const contextOwnerAllowFromList = resolveOwnerAllowFromList({
168
+ dock,
169
+ cfg,
170
+ accountId: ctx.AccountId,
171
+ providerId,
172
+ allowFrom: ctx.OwnerAllowFrom,
173
+ });
95
174
  const allowAll = allowFromList.length === 0 || allowFromList.some((entry) => entry.trim() === "*");
96
- const ownerCandidates = allowAll ? [] : allowFromList.filter((entry) => entry !== "*");
97
- if (!allowAll && ownerCandidates.length === 0 && to) {
175
+ const ownerCandidatesForCommands = allowAll ? [] : allowFromList.filter((entry) => entry !== "*");
176
+ if (!allowAll && ownerCandidatesForCommands.length === 0 && to) {
98
177
  const normalizedTo = normalizeAllowFromEntry({
99
178
  dock,
100
179
  cfg,
@@ -102,9 +181,18 @@ export function resolveCommandAuthorization(params) {
102
181
  value: to,
103
182
  });
104
183
  if (normalizedTo.length > 0)
105
- ownerCandidates.push(...normalizedTo);
184
+ ownerCandidatesForCommands.push(...normalizedTo);
106
185
  }
107
- const ownerList = Array.from(new Set(ownerCandidates));
186
+ const ownerAllowAll = configOwnerAllowFromList.some((entry) => entry.trim() === "*");
187
+ const explicitOwners = configOwnerAllowFromList.filter((entry) => entry !== "*");
188
+ const explicitOverrides = contextOwnerAllowFromList.filter((entry) => entry !== "*");
189
+ const ownerList = Array.from(new Set(explicitOwners.length > 0
190
+ ? explicitOwners
191
+ : ownerAllowAll
192
+ ? []
193
+ : explicitOverrides.length > 0
194
+ ? explicitOverrides
195
+ : ownerCandidatesForCommands));
108
196
  const senderCandidates = resolveSenderCandidates({
109
197
  dock,
110
198
  providerId,
@@ -117,14 +205,41 @@ export function resolveCommandAuthorization(params) {
117
205
  const matchedSender = ownerList.length
118
206
  ? senderCandidates.find((candidate) => ownerList.includes(candidate))
119
207
  : undefined;
208
+ const matchedCommandOwner = ownerCandidatesForCommands.length
209
+ ? senderCandidates.find((candidate) => ownerCandidatesForCommands.includes(candidate))
210
+ : undefined;
120
211
  const senderId = matchedSender ?? senderCandidates[0];
121
212
  const enforceOwner = Boolean(dock?.commands?.enforceOwnerForCommands);
122
- const isOwner = !enforceOwner || allowAll || ownerList.length === 0 || Boolean(matchedSender);
123
- const isAuthorizedSender = commandAuthorized && isOwner;
213
+ const senderIsOwner = Boolean(matchedSender);
214
+ const ownerAllowlistConfigured = ownerAllowAll || explicitOwners.length > 0;
215
+ const requireOwner = enforceOwner || ownerAllowlistConfigured;
216
+ const isOwnerForCommands = !requireOwner
217
+ ? true
218
+ : ownerAllowAll
219
+ ? true
220
+ : ownerAllowlistConfigured
221
+ ? senderIsOwner
222
+ : allowAll || ownerCandidatesForCommands.length === 0 || Boolean(matchedCommandOwner);
223
+ // If commands.allowFrom is configured, use it for command authorization
224
+ // Otherwise, fall back to existing behavior (channel allowFrom + owner checks)
225
+ let isAuthorizedSender;
226
+ if (commandsAllowFromList !== null) {
227
+ // commands.allowFrom is configured - use it for authorization
228
+ const commandsAllowAll = commandsAllowFromList.some((entry) => entry.trim() === "*");
229
+ const matchedCommandsAllowFrom = commandsAllowFromList.length
230
+ ? senderCandidates.find((candidate) => commandsAllowFromList.includes(candidate))
231
+ : undefined;
232
+ isAuthorizedSender = commandsAllowAll || Boolean(matchedCommandsAllowFrom);
233
+ }
234
+ else {
235
+ // Fall back to existing behavior
236
+ isAuthorizedSender = commandAuthorized && isOwnerForCommands;
237
+ }
124
238
  return {
125
239
  providerId,
126
240
  ownerList,
127
241
  senderId: senderId || undefined,
242
+ senderIsOwner,
128
243
  isAuthorizedSender,
129
244
  from: from || undefined,
130
245
  to: to || undefined,
@@ -1,6 +1,16 @@
1
1
  import { resolveUserTimezone } from "../agents/date-time.js";
2
2
  import { normalizeChatType } from "../channels/chat-type.js";
3
3
  import { resolveSenderLabel } from "../channels/sender-label.js";
4
+ import { resolveTimezone, formatUtcTimestamp, formatZonedTimestamp, } from "../infra/format-time/format-datetime.js";
5
+ import { formatTimeAgo } from "../infra/format-time/format-relative.js";
6
+ function sanitizeEnvelopeHeaderPart(value) {
7
+ return value
8
+ .replace(/\r\n|\r|\n/g, " ")
9
+ .replaceAll("[", "(")
10
+ .replaceAll("]", ")")
11
+ .replace(/\s+/g, " ")
12
+ .trim();
13
+ }
4
14
  export function resolveEnvelopeFormatOptions(cfg) {
5
15
  const defaults = cfg?.agents?.defaults;
6
16
  return {
@@ -20,15 +30,6 @@ function normalizeEnvelopeOptions(options) {
20
30
  userTimezone: options?.userTimezone,
21
31
  };
22
32
  }
23
- function resolveExplicitTimezone(value) {
24
- try {
25
- new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
26
- return value;
27
- }
28
- catch {
29
- return undefined;
30
- }
31
- }
32
33
  function resolveEnvelopeTimezone(options) {
33
34
  const trimmed = options.timezone?.trim();
34
35
  if (!trimmed)
@@ -41,42 +42,9 @@ function resolveEnvelopeTimezone(options) {
41
42
  if (lowered === "user") {
42
43
  return { mode: "iana", timeZone: resolveUserTimezone(options.userTimezone) };
43
44
  }
44
- const explicit = resolveExplicitTimezone(trimmed);
45
+ const explicit = resolveTimezone(trimmed);
45
46
  return explicit ? { mode: "iana", timeZone: explicit } : { mode: "utc" };
46
47
  }
47
- function formatUtcTimestamp(date) {
48
- const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
49
- const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
50
- const dd = String(date.getUTCDate()).padStart(2, "0");
51
- const hh = String(date.getUTCHours()).padStart(2, "0");
52
- const min = String(date.getUTCMinutes()).padStart(2, "0");
53
- return `${yyyy}-${mm}-${dd}T${hh}:${min}Z`;
54
- }
55
- function formatZonedTimestamp(date, timeZone) {
56
- const parts = new Intl.DateTimeFormat("en-US", {
57
- timeZone,
58
- year: "numeric",
59
- month: "2-digit",
60
- day: "2-digit",
61
- hour: "2-digit",
62
- minute: "2-digit",
63
- hourCycle: "h23",
64
- timeZoneName: "short",
65
- }).formatToParts(date);
66
- const pick = (type) => parts.find((part) => part.type === type)?.value;
67
- const yyyy = pick("year");
68
- const mm = pick("month");
69
- const dd = pick("day");
70
- const hh = pick("hour");
71
- const min = pick("minute");
72
- const tz = [...parts]
73
- .reverse()
74
- .find((part) => part.type === "timeZoneName")
75
- ?.value?.trim();
76
- if (!yyyy || !mm || !dd || !hh || !min)
77
- return undefined;
78
- return `${yyyy}-${mm}-${dd} ${hh}:${min}${tz ? ` ${tz}` : ""}`;
79
- }
80
48
  function formatTimestamp(ts, options) {
81
49
  if (!ts)
82
50
  return undefined;
@@ -87,48 +55,57 @@ function formatTimestamp(ts, options) {
87
55
  if (Number.isNaN(date.getTime()))
88
56
  return undefined;
89
57
  const zone = resolveEnvelopeTimezone(resolved);
90
- if (zone.mode === "utc")
91
- return formatUtcTimestamp(date);
92
- if (zone.mode === "local")
93
- return formatZonedTimestamp(date);
94
- return formatZonedTimestamp(date, zone.timeZone);
95
- }
96
- function formatElapsedTime(currentMs, previousMs) {
97
- const elapsedMs = currentMs - previousMs;
98
- if (!Number.isFinite(elapsedMs) || elapsedMs < 0)
58
+ let weekday;
59
+ try {
60
+ weekday = new Intl.DateTimeFormat("en-US", {
61
+ weekday: "short",
62
+ ...(zone.mode === "iana" ? { timeZone: zone.timeZone } : {}),
63
+ ...(zone.mode === "utc" ? { timeZone: "UTC" } : {}),
64
+ }).format(date);
65
+ }
66
+ catch {
67
+ weekday = undefined;
68
+ }
69
+ let formatted;
70
+ if (zone.mode === "utc") {
71
+ formatted = formatUtcTimestamp(date);
72
+ }
73
+ else if (zone.mode === "local") {
74
+ formatted = formatZonedTimestamp(date);
75
+ }
76
+ else {
77
+ formatted = formatZonedTimestamp(date, { timeZone: zone.timeZone });
78
+ }
79
+ if (!formatted)
99
80
  return undefined;
100
- const seconds = Math.floor(elapsedMs / 1000);
101
- if (seconds < 60)
102
- return `${seconds}s`;
103
- const minutes = Math.floor(seconds / 60);
104
- if (minutes < 60)
105
- return `${minutes}m`;
106
- const hours = Math.floor(minutes / 60);
107
- if (hours < 24)
108
- return `${hours}h`;
109
- const days = Math.floor(hours / 24);
110
- return `${days}d`;
81
+ return weekday ? `${weekday} ${formatted}` : formatted;
111
82
  }
112
83
  export function formatAgentEnvelope(params) {
113
- const channel = params.channel?.trim() || "Channel";
84
+ const channel = sanitizeEnvelopeHeaderPart(params.channel?.trim() || "Channel");
114
85
  const parts = [channel];
115
86
  const resolved = normalizeEnvelopeOptions(params.envelope);
116
- const elapsed = resolved.includeElapsed && params.timestamp && params.previousTimestamp
117
- ? formatElapsedTime(params.timestamp instanceof Date ? params.timestamp.getTime() : params.timestamp, params.previousTimestamp instanceof Date
87
+ let elapsed;
88
+ if (resolved.includeElapsed && params.timestamp && params.previousTimestamp) {
89
+ const curMs = params.timestamp instanceof Date ? params.timestamp.getTime() : params.timestamp;
90
+ const prevMs = params.previousTimestamp instanceof Date
118
91
  ? params.previousTimestamp.getTime()
119
- : params.previousTimestamp)
120
- : undefined;
92
+ : params.previousTimestamp;
93
+ const elapsedMs = curMs - prevMs;
94
+ if (Number.isFinite(elapsedMs) && elapsedMs >= 0) {
95
+ elapsed = formatTimeAgo(elapsedMs, { suffix: false });
96
+ }
97
+ }
121
98
  if (params.from?.trim()) {
122
- const from = params.from.trim();
99
+ const from = sanitizeEnvelopeHeaderPart(params.from.trim());
123
100
  parts.push(elapsed ? `${from} +${elapsed}` : from);
124
101
  }
125
102
  else if (elapsed) {
126
103
  parts.push(`+${elapsed}`);
127
104
  }
128
105
  if (params.host?.trim())
129
- parts.push(params.host.trim());
106
+ parts.push(sanitizeEnvelopeHeaderPart(params.host.trim()));
130
107
  if (params.ip?.trim())
131
- parts.push(params.ip.trim());
108
+ parts.push(sanitizeEnvelopeHeaderPart(params.ip.trim()));
132
109
  const ts = formatTimestamp(params.timestamp, resolved);
133
110
  if (ts)
134
111
  parts.push(ts);
@@ -138,7 +115,8 @@ export function formatAgentEnvelope(params) {
138
115
  export function formatInboundEnvelope(params) {
139
116
  const chatType = normalizeChatType(params.chatType);
140
117
  const isDirect = !chatType || chatType === "direct";
141
- const resolvedSender = params.senderLabel?.trim() || resolveSenderLabel(params.sender ?? {});
118
+ const resolvedSenderRaw = params.senderLabel?.trim() || resolveSenderLabel(params.sender ?? {});
119
+ const resolvedSender = resolvedSenderRaw ? sanitizeEnvelopeHeaderPart(resolvedSenderRaw) : "";
142
120
  const body = !isDirect && resolvedSender ? `${resolvedSender}: ${params.body}` : params.body;
143
121
  return formatAgentEnvelope({
144
122
  channel: params.channel,
@@ -70,6 +70,7 @@ export const handleCompactCommand = async (params) => {
70
70
  defaultLevel: "off",
71
71
  },
72
72
  customInstructions,
73
+ senderIsOwner: params.command.senderIsOwner,
73
74
  ownerNumbers: params.command.ownerList.length > 0 ? params.command.ownerList : undefined,
74
75
  });
75
76
  const compactLabel = result.ok
@@ -75,6 +75,7 @@ async function resolveContextReport(params) {
75
75
  groupChannel: params.sessionEntry?.groupChannel ?? undefined,
76
76
  groupSpace: params.sessionEntry?.space ?? undefined,
77
77
  spawnedBy: params.sessionEntry?.spawnedBy ?? undefined,
78
+ senderIsOwner: params.command.senderIsOwner,
78
79
  modelProvider: params.provider,
79
80
  modelId: params.model,
80
81
  });
@@ -18,6 +18,7 @@ export function buildCommandContext(params) {
18
18
  channel,
19
19
  channelId: auth.providerId,
20
20
  ownerList: auth.ownerList,
21
+ senderIsOwner: auth.senderIsOwner,
21
22
  isAuthorizedSender: auth.isAuthorizedSender,
22
23
  senderId: auth.senderId,
23
24
  abortKey,
@@ -1,75 +1,28 @@
1
1
  import { loadModelCatalog } from "../../agents/model-catalog.js";
2
2
  import { buildAllowedModelSet, buildModelAliasIndex, normalizeProviderId, resolveConfiguredModelRef, resolveModelRefFromString, } from "../../agents/model-selection.js";
3
3
  import { DEFAULT_MODEL, DEFAULT_PROVIDER } from "../../agents/defaults.js";
4
+ import { buildModelsKeyboard, buildProviderKeyboard, calculateTotalPages, getModelsPageSize, } from "../../telegram/model-buttons.js";
4
5
  const PAGE_SIZE_DEFAULT = 20;
5
6
  const PAGE_SIZE_MAX = 100;
6
- function formatProviderLine(params) {
7
- return `- ${params.provider} (${params.count})`;
8
- }
9
- function parseModelsArgs(raw) {
10
- const trimmed = raw.trim();
11
- if (!trimmed) {
12
- return { page: 1, pageSize: PAGE_SIZE_DEFAULT, all: false };
13
- }
14
- const tokens = trimmed.split(/\s+/g).filter(Boolean);
15
- const provider = tokens[0]?.trim();
16
- let page = 1;
17
- let all = false;
18
- for (const token of tokens.slice(1)) {
19
- const lower = token.toLowerCase();
20
- if (lower === "all" || lower === "--all") {
21
- all = true;
22
- continue;
23
- }
24
- if (lower.startsWith("page=")) {
25
- const value = Number.parseInt(lower.slice("page=".length), 10);
26
- if (Number.isFinite(value) && value > 0)
27
- page = value;
28
- continue;
29
- }
30
- if (/^[0-9]+$/.test(lower)) {
31
- const value = Number.parseInt(lower, 10);
32
- if (Number.isFinite(value) && value > 0)
33
- page = value;
34
- }
35
- }
36
- let pageSize = PAGE_SIZE_DEFAULT;
37
- for (const token of tokens) {
38
- const lower = token.toLowerCase();
39
- if (lower.startsWith("limit=") || lower.startsWith("size=")) {
40
- const rawValue = lower.slice(lower.indexOf("=") + 1);
41
- const value = Number.parseInt(rawValue, 10);
42
- if (Number.isFinite(value) && value > 0)
43
- pageSize = Math.min(PAGE_SIZE_MAX, value);
44
- }
45
- }
46
- return {
47
- provider: provider ? normalizeProviderId(provider) : undefined,
48
- page,
49
- pageSize,
50
- all,
51
- };
52
- }
53
- export async function resolveModelsCommandReply(params) {
54
- const body = params.commandBodyNormalized.trim();
55
- if (!body.startsWith("/models"))
56
- return null;
57
- const argText = body.replace(/^\/models\b/i, "").trim();
58
- const { provider, page, pageSize, all } = parseModelsArgs(argText);
7
+ /**
8
+ * Build provider/model data from config and catalog.
9
+ * Exported for reuse by callback handlers.
10
+ */
11
+ export async function buildModelsProviderData(cfg) {
59
12
  const resolvedDefault = resolveConfiguredModelRef({
60
- cfg: params.cfg,
13
+ cfg,
61
14
  defaultProvider: DEFAULT_PROVIDER,
62
15
  defaultModel: DEFAULT_MODEL,
63
16
  });
64
- const catalog = await loadModelCatalog({ config: params.cfg });
17
+ const catalog = await loadModelCatalog({ config: cfg });
65
18
  const allowed = buildAllowedModelSet({
66
- cfg: params.cfg,
19
+ cfg,
67
20
  catalog,
68
21
  defaultProvider: resolvedDefault.provider,
69
22
  defaultModel: resolvedDefault.model,
70
23
  });
71
24
  const aliasIndex = buildModelAliasIndex({
72
- cfg: params.cfg,
25
+ cfg,
73
26
  defaultProvider: resolvedDefault.provider,
74
27
  });
75
28
  const byProvider = new Map();
@@ -93,7 +46,7 @@ export async function resolveModelsCommandReply(params) {
93
46
  add(resolved.ref.provider, resolved.ref.model);
94
47
  };
95
48
  const addModelConfigEntries = () => {
96
- const modelConfig = params.cfg.agents?.defaults?.model;
49
+ const modelConfig = cfg.agents?.defaults?.model;
97
50
  if (typeof modelConfig === "string") {
98
51
  addRawModelRef(modelConfig);
99
52
  }
@@ -103,7 +56,7 @@ export async function resolveModelsCommandReply(params) {
103
56
  addRawModelRef(fallback);
104
57
  }
105
58
  }
106
- const imageConfig = params.cfg.agents?.defaults?.imageModel;
59
+ const imageConfig = cfg.agents?.defaults?.imageModel;
107
60
  if (typeof imageConfig === "string") {
108
61
  addRawModelRef(imageConfig);
109
62
  }
@@ -118,7 +71,7 @@ export async function resolveModelsCommandReply(params) {
118
71
  add(entry.provider, entry.id);
119
72
  }
120
73
  // Include config-only allowlist keys that aren't in the curated catalog.
121
- for (const raw of Object.keys(params.cfg.agents?.defaults?.models ?? {})) {
74
+ for (const raw of Object.keys(cfg.agents?.defaults?.models ?? {})) {
122
75
  addRawModelRef(raw);
123
76
  }
124
77
  // Ensure configured defaults/fallbacks/image models show up even when the
@@ -126,7 +79,79 @@ export async function resolveModelsCommandReply(params) {
126
79
  add(resolvedDefault.provider, resolvedDefault.model);
127
80
  addModelConfigEntries();
128
81
  const providers = [...byProvider.keys()].sort();
82
+ return { byProvider, providers, resolvedDefault };
83
+ }
84
+ function formatProviderLine(params) {
85
+ return `- ${params.provider} (${params.count})`;
86
+ }
87
+ function parseModelsArgs(raw) {
88
+ const trimmed = raw.trim();
89
+ if (!trimmed) {
90
+ return { page: 1, pageSize: PAGE_SIZE_DEFAULT, all: false };
91
+ }
92
+ const tokens = trimmed.split(/\s+/g).filter(Boolean);
93
+ const provider = tokens[0]?.trim();
94
+ let page = 1;
95
+ let all = false;
96
+ for (const token of tokens.slice(1)) {
97
+ const lower = token.toLowerCase();
98
+ if (lower === "all" || lower === "--all") {
99
+ all = true;
100
+ continue;
101
+ }
102
+ if (lower.startsWith("page=")) {
103
+ const value = Number.parseInt(lower.slice("page=".length), 10);
104
+ if (Number.isFinite(value) && value > 0)
105
+ page = value;
106
+ continue;
107
+ }
108
+ if (/^[0-9]+$/.test(lower)) {
109
+ const value = Number.parseInt(lower, 10);
110
+ if (Number.isFinite(value) && value > 0)
111
+ page = value;
112
+ }
113
+ }
114
+ let pageSize = PAGE_SIZE_DEFAULT;
115
+ for (const token of tokens) {
116
+ const lower = token.toLowerCase();
117
+ if (lower.startsWith("limit=") || lower.startsWith("size=")) {
118
+ const rawValue = lower.slice(lower.indexOf("=") + 1);
119
+ const value = Number.parseInt(rawValue, 10);
120
+ if (Number.isFinite(value) && value > 0)
121
+ pageSize = Math.min(PAGE_SIZE_MAX, value);
122
+ }
123
+ }
124
+ return {
125
+ provider: provider ? normalizeProviderId(provider) : undefined,
126
+ page,
127
+ pageSize,
128
+ all,
129
+ };
130
+ }
131
+ export async function resolveModelsCommandReply(params) {
132
+ const body = params.commandBodyNormalized.trim();
133
+ if (!body.startsWith("/models"))
134
+ return null;
135
+ const argText = body.replace(/^\/models\b/i, "").trim();
136
+ const { provider, page, pageSize, all } = parseModelsArgs(argText);
137
+ const { byProvider, providers } = await buildModelsProviderData(params.cfg);
138
+ const isTelegram = params.surface === "telegram";
139
+ // Provider list (no provider specified)
129
140
  if (!provider) {
141
+ // For Telegram: show buttons if there are providers
142
+ if (isTelegram && providers.length > 0) {
143
+ const providerInfos = providers.map((p) => ({
144
+ id: p,
145
+ count: byProvider.get(p)?.size ?? 0,
146
+ }));
147
+ const buttons = buildProviderKeyboard(providerInfos);
148
+ const text = "Select a provider:";
149
+ return {
150
+ text,
151
+ channelData: { telegram: { buttons } },
152
+ };
153
+ }
154
+ // Text fallback for non-Telegram surfaces
130
155
  const lines = [
131
156
  "Providers:",
132
157
  ...providers.map((p) => formatProviderLine({ provider: p, count: byProvider.get(p)?.size ?? 0 })),
@@ -158,6 +183,26 @@ export async function resolveModelsCommandReply(params) {
158
183
  ];
159
184
  return { text: lines.join("\n") };
160
185
  }
186
+ // For Telegram: use button-based model list with inline keyboard pagination
187
+ if (isTelegram) {
188
+ const telegramPageSize = getModelsPageSize();
189
+ const totalPages = calculateTotalPages(total, telegramPageSize);
190
+ const safePage = Math.max(1, Math.min(page, totalPages));
191
+ const buttons = buildModelsKeyboard({
192
+ provider,
193
+ models,
194
+ currentModel: params.currentModel,
195
+ currentPage: safePage,
196
+ totalPages,
197
+ pageSize: telegramPageSize,
198
+ });
199
+ const text = `Models (${provider}) — ${total} available`;
200
+ return {
201
+ text,
202
+ channelData: { telegram: { buttons } },
203
+ };
204
+ }
205
+ // Text fallback for non-Telegram surfaces
161
206
  const effectivePageSize = all ? total : pageSize;
162
207
  const pageCount = effectivePageSize > 0 ? Math.ceil(total / effectivePageSize) : 1;
163
208
  const safePage = all ? 1 : Math.max(1, Math.min(page, pageCount));
@@ -194,6 +239,8 @@ export const handleModelsCommand = async (params, allowTextCommands) => {
194
239
  const reply = await resolveModelsCommandReply({
195
240
  cfg: params.cfg,
196
241
  commandBodyNormalized: params.command.commandBodyNormalized,
242
+ surface: params.ctx.Surface,
243
+ currentModel: params.model ? `${params.provider}/${params.model}` : undefined,
197
244
  });
198
245
  if (!reply)
199
246
  return null;