@poolzin/pool-bot 2026.2.0 → 2026.2.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (258) hide show
  1. package/CHANGELOG.md +118 -0
  2. package/README-header.png +0 -0
  3. package/dist/agents/bash-tools.exec.js +76 -25
  4. package/dist/agents/cli-runner/helpers.js +9 -11
  5. package/dist/agents/context.js +1 -1
  6. package/dist/agents/identity.js +47 -7
  7. package/dist/agents/memory-search.js +25 -8
  8. package/dist/agents/model-catalog.js +1 -1
  9. package/dist/agents/model-selection.js +21 -0
  10. package/dist/agents/pi-embedded-block-chunker.js +117 -42
  11. package/dist/agents/pi-embedded-helpers/errors.js +183 -78
  12. package/dist/agents/pi-embedded-helpers.js +1 -1
  13. package/dist/agents/pi-embedded-runner/compact.js +8 -10
  14. package/dist/agents/pi-embedded-runner/model.js +62 -3
  15. package/dist/agents/pi-embedded-runner/run/attempt.js +21 -11
  16. package/dist/agents/pi-embedded-runner/run.js +199 -46
  17. package/dist/agents/pi-embedded-runner/system-prompt.js +10 -2
  18. package/dist/agents/pi-embedded-subscribe.js +118 -29
  19. package/dist/agents/pi-tools.js +10 -5
  20. package/dist/agents/poolbot-tools.js +15 -10
  21. package/dist/agents/sandbox-paths.js +31 -0
  22. package/dist/agents/session-tool-result-guard.js +94 -15
  23. package/dist/agents/shell-utils.js +51 -0
  24. package/dist/agents/skills/bundled-context.js +23 -0
  25. package/dist/agents/skills/bundled-dir.js +41 -7
  26. package/dist/agents/skills-install.js +60 -23
  27. package/dist/agents/subagent-announce.js +79 -34
  28. package/dist/agents/tool-policy.conformance.js +14 -0
  29. package/dist/agents/tool-policy.js +24 -0
  30. package/dist/agents/tools/cron-tool.js +166 -19
  31. package/dist/agents/tools/discord-actions-presence.js +78 -0
  32. package/dist/agents/tools/image-tool.js +1 -1
  33. package/dist/agents/tools/message-tool.js +56 -2
  34. package/dist/agents/tools/sessions-history-tool.js +69 -1
  35. package/dist/agents/tools/web-search.js +211 -42
  36. package/dist/agents/usage.js +23 -1
  37. package/dist/agents/workspace-run.js +67 -0
  38. package/dist/agents/workspace-templates.js +44 -0
  39. package/dist/auto-reply/command-auth.js +121 -6
  40. package/dist/auto-reply/envelope.js +74 -82
  41. package/dist/auto-reply/reply/commands-compact.js +1 -0
  42. package/dist/auto-reply/reply/commands-context-report.js +1 -0
  43. package/dist/auto-reply/reply/commands-context.js +1 -0
  44. package/dist/auto-reply/reply/commands-models.js +107 -60
  45. package/dist/auto-reply/reply/commands-ptt.js +171 -0
  46. package/dist/auto-reply/reply/get-reply-run.js +2 -1
  47. package/dist/auto-reply/reply/inbound-context.js +5 -1
  48. package/dist/auto-reply/reply/mentions.js +1 -1
  49. package/dist/auto-reply/reply/model-selection.js +3 -3
  50. package/dist/auto-reply/thinking.js +88 -43
  51. package/dist/browser/bridge-server.js +13 -0
  52. package/dist/browser/cdp.helpers.js +38 -24
  53. package/dist/browser/client-fetch.js +50 -7
  54. package/dist/browser/config.js +1 -10
  55. package/dist/browser/extension-relay.js +101 -40
  56. package/dist/browser/pw-ai.js +1 -1
  57. package/dist/browser/pw-session.js +143 -8
  58. package/dist/browser/pw-tools-core.interactions.js +125 -27
  59. package/dist/browser/pw-tools-core.responses.js +1 -1
  60. package/dist/browser/pw-tools-core.state.js +1 -1
  61. package/dist/browser/routes/agent.act.js +86 -41
  62. package/dist/browser/routes/dispatcher.js +4 -4
  63. package/dist/browser/screenshot.js +1 -1
  64. package/dist/browser/server.js +13 -0
  65. package/dist/build-info.json +3 -3
  66. package/dist/canvas-host/a2ui/index.html +28 -28
  67. package/dist/channels/reply-prefix.js +8 -1
  68. package/dist/cli/cron-cli/register.cron-add.js +61 -40
  69. package/dist/cli/cron-cli/register.cron-edit.js +60 -34
  70. package/dist/cli/cron-cli/shared.js +56 -41
  71. package/dist/cli/dns-cli.js +26 -14
  72. package/dist/cli/gateway-cli/register.js +37 -19
  73. package/dist/cli/memory-cli.js +5 -5
  74. package/dist/cli/parse-bytes.js +37 -0
  75. package/dist/cli/update-cli.js +173 -52
  76. package/dist/commands/agent.js +1 -0
  77. package/dist/commands/auth-choice.apply.oauth.js +1 -1
  78. package/dist/commands/doctor-config-flow.js +61 -5
  79. package/dist/commands/doctor-state-migrations.js +1 -1
  80. package/dist/commands/health.js +1 -1
  81. package/dist/commands/model-allowlist.js +29 -0
  82. package/dist/commands/model-picker.js +2 -1
  83. package/dist/commands/models/list.registry.js +1 -1
  84. package/dist/commands/models/list.status-command.js +43 -23
  85. package/dist/commands/models/shared.js +15 -0
  86. package/dist/commands/onboard-custom.js +384 -0
  87. package/dist/commands/onboard-non-interactive/local/auth-choice-inference.js +35 -0
  88. package/dist/commands/onboard-non-interactive/local/auth-choice.js +6 -3
  89. package/dist/commands/onboard-skills.js +63 -38
  90. package/dist/commands/openai-model-default.js +41 -0
  91. package/dist/compat/legacy-names.js +2 -0
  92. package/dist/config/defaults.js +3 -2
  93. package/dist/config/paths.js +136 -35
  94. package/dist/config/plugin-auto-enable.js +21 -5
  95. package/dist/config/redact-snapshot.js +153 -0
  96. package/dist/config/schema.field-metadata.js +590 -0
  97. package/dist/config/schema.js +2 -2
  98. package/dist/config/sessions/store.js +291 -23
  99. package/dist/config/zod-schema.agent-defaults.js +3 -0
  100. package/dist/config/zod-schema.agent-runtime.js +13 -2
  101. package/dist/config/zod-schema.providers-core.js +142 -0
  102. package/dist/config/zod-schema.session.js +3 -0
  103. package/dist/control-ui/assets/{index-CIRDm-Lu.css → index-CSfXd2LO.css} +1 -1
  104. package/dist/control-ui/assets/{index-CmNMuoem.js → index-HRr1grwl.js} +446 -413
  105. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -0
  106. package/dist/control-ui/index.html +4 -4
  107. package/dist/cron/delivery.js +57 -0
  108. package/dist/cron/isolated-agent/delivery-target.js +18 -3
  109. package/dist/cron/isolated-agent/helpers.js +22 -5
  110. package/dist/cron/isolated-agent/run.js +172 -63
  111. package/dist/cron/isolated-agent/session.js +2 -0
  112. package/dist/cron/normalize.js +356 -28
  113. package/dist/cron/parse.js +10 -5
  114. package/dist/cron/run-log.js +35 -10
  115. package/dist/cron/schedule.js +41 -6
  116. package/dist/cron/service/jobs.js +208 -35
  117. package/dist/cron/service/ops.js +72 -16
  118. package/dist/cron/service/state.js +2 -0
  119. package/dist/cron/service/store.js +386 -14
  120. package/dist/cron/service/timer.js +390 -147
  121. package/dist/cron/session-reaper.js +86 -0
  122. package/dist/cron/store.js +23 -8
  123. package/dist/cron/validate-timestamp.js +43 -0
  124. package/dist/discord/monitor/agent-components.js +438 -0
  125. package/dist/discord/monitor/allow-list.js +28 -5
  126. package/dist/discord/monitor/gateway-registry.js +29 -0
  127. package/dist/discord/monitor/native-command.js +44 -23
  128. package/dist/discord/monitor/sender-identity.js +45 -0
  129. package/dist/discord/pluralkit.js +27 -0
  130. package/dist/discord/send.outbound.js +92 -5
  131. package/dist/discord/send.shared.js +60 -23
  132. package/dist/discord/targets.js +84 -1
  133. package/dist/entry.js +15 -9
  134. package/dist/extensionAPI.js +8 -0
  135. package/dist/gateway/control-ui.js +8 -1
  136. package/dist/gateway/hooks-mapping.js +3 -0
  137. package/dist/gateway/hooks.js +65 -0
  138. package/dist/gateway/net.js +96 -31
  139. package/dist/gateway/node-command-policy.js +50 -15
  140. package/dist/gateway/origin-check.js +56 -0
  141. package/dist/gateway/protocol/client-info.js +9 -0
  142. package/dist/gateway/protocol/index.js +9 -2
  143. package/dist/gateway/protocol/schema/agents-models-skills.js +71 -1
  144. package/dist/gateway/protocol/schema/cron.js +22 -10
  145. package/dist/gateway/protocol/schema/protocol-schemas.js +16 -2
  146. package/dist/gateway/protocol/schema/sessions.js +12 -0
  147. package/dist/gateway/server/hooks.js +1 -1
  148. package/dist/gateway/server-broadcast.js +26 -9
  149. package/dist/gateway/server-chat.js +112 -23
  150. package/dist/gateway/server-discovery-runtime.js +10 -2
  151. package/dist/gateway/server-http.js +109 -11
  152. package/dist/gateway/server-methods/agent-timestamp.js +60 -0
  153. package/dist/gateway/server-methods/agents.js +321 -2
  154. package/dist/gateway/server-methods/usage.js +559 -16
  155. package/dist/gateway/server-runtime-state.js +22 -8
  156. package/dist/gateway/server-startup-memory.js +16 -0
  157. package/dist/gateway/server.impl.js +5 -1
  158. package/dist/gateway/session-utils.fs.js +23 -25
  159. package/dist/gateway/session-utils.js +20 -10
  160. package/dist/gateway/sessions-patch.js +7 -22
  161. package/dist/gateway/test-helpers.mocks.js +11 -7
  162. package/dist/gateway/test-helpers.server.js +35 -2
  163. package/dist/imessage/constants.js +2 -0
  164. package/dist/imessage/monitor/deliver.js +4 -1
  165. package/dist/imessage/monitor/monitor-provider.js +51 -1
  166. package/dist/infra/bonjour-discovery.js +131 -70
  167. package/dist/infra/control-ui-assets.js +134 -12
  168. package/dist/infra/errors.js +12 -0
  169. package/dist/infra/exec-approvals.js +266 -57
  170. package/dist/infra/format-time/format-datetime.js +79 -0
  171. package/dist/infra/format-time/format-duration.js +81 -0
  172. package/dist/infra/format-time/format-relative.js +80 -0
  173. package/dist/infra/heartbeat-runner.js +140 -49
  174. package/dist/infra/home-dir.js +54 -0
  175. package/dist/infra/net/fetch-guard.js +122 -0
  176. package/dist/infra/net/ssrf.js +65 -29
  177. package/dist/infra/outbound/abort.js +14 -0
  178. package/dist/infra/outbound/message-action-runner.js +77 -13
  179. package/dist/infra/outbound/outbound-session.js +143 -37
  180. package/dist/infra/poolbot-root.js +43 -1
  181. package/dist/infra/session-cost-usage.js +631 -41
  182. package/dist/infra/state-migrations.js +317 -47
  183. package/dist/infra/update-global.js +35 -0
  184. package/dist/infra/update-runner.js +149 -43
  185. package/dist/infra/warning-filter.js +65 -0
  186. package/dist/infra/widearea-dns.js +30 -9
  187. package/dist/logging/redact-identifier.js +12 -0
  188. package/dist/media/fetch.js +81 -58
  189. package/dist/media/store.js +2 -0
  190. package/dist/media-understanding/apply.js +403 -3
  191. package/dist/media-understanding/attachments.js +38 -27
  192. package/dist/media-understanding/defaults.js +16 -0
  193. package/dist/media-understanding/providers/deepgram/audio.js +22 -14
  194. package/dist/media-understanding/providers/google/audio.js +24 -17
  195. package/dist/media-understanding/providers/google/video.js +24 -17
  196. package/dist/media-understanding/providers/image.js +3 -3
  197. package/dist/media-understanding/providers/index.js +4 -1
  198. package/dist/media-understanding/providers/openai/audio.js +22 -14
  199. package/dist/media-understanding/providers/shared.js +16 -11
  200. package/dist/media-understanding/providers/zai/index.js +6 -0
  201. package/dist/media-understanding/runner.js +158 -90
  202. package/dist/memory/batch-voyage.js +277 -0
  203. package/dist/memory/embeddings-voyage.js +75 -0
  204. package/dist/memory/embeddings.js +28 -16
  205. package/dist/memory/internal.js +101 -18
  206. package/dist/memory/manager.js +154 -48
  207. package/dist/memory/search-manager.js +173 -0
  208. package/dist/memory/session-files.js +9 -3
  209. package/dist/node-host/runner.js +34 -24
  210. package/dist/node-host/with-timeout.js +27 -0
  211. package/dist/plugins/commands.js +5 -1
  212. package/dist/plugins/config-state.js +86 -7
  213. package/dist/plugins/source-display.js +51 -0
  214. package/dist/process/exec.js +20 -2
  215. package/dist/routing/resolve-route.js +12 -0
  216. package/dist/routing/session-key.js +15 -0
  217. package/dist/runtime.js +2 -0
  218. package/dist/security/audit-extra.async.js +601 -0
  219. package/dist/security/audit-extra.js +2 -830
  220. package/dist/security/audit-extra.sync.js +505 -0
  221. package/dist/security/channel-metadata.js +34 -0
  222. package/dist/security/external-content.js +88 -6
  223. package/dist/security/skill-scanner.js +330 -0
  224. package/dist/sessions/session-key-utils.js +7 -0
  225. package/dist/signal/monitor/event-handler.js +80 -1
  226. package/dist/slack/monitor/media.js +85 -15
  227. package/dist/tailscale/detect.js +1 -2
  228. package/dist/telegram/bot/helpers.js +109 -28
  229. package/dist/telegram/bot-handlers.js +144 -3
  230. package/dist/telegram/bot-message-context.js +37 -10
  231. package/dist/telegram/bot-message-dispatch.js +54 -17
  232. package/dist/telegram/bot-native-commands.js +86 -29
  233. package/dist/telegram/bot.js +30 -29
  234. package/dist/telegram/model-buttons.js +163 -0
  235. package/dist/telegram/monitor.js +110 -85
  236. package/dist/telegram/send.js +129 -47
  237. package/dist/terminal/restore.js +45 -0
  238. package/dist/test-helpers/state-dir-env.js +16 -0
  239. package/dist/tts/tts.js +12 -6
  240. package/dist/tui/tui-session-actions.js +166 -54
  241. package/dist/utils/fetch-timeout.js +20 -0
  242. package/dist/utils/normalize-secret-input.js +19 -0
  243. package/dist/utils/transcript-tools.js +58 -0
  244. package/dist/utils.js +45 -14
  245. package/dist/version.js +42 -5
  246. package/dist/wizard/clack-prompter.js +9 -6
  247. package/extensions/googlechat/node_modules/.bin/poolbot +21 -0
  248. package/extensions/googlechat/package.json +2 -2
  249. package/extensions/line/node_modules/.bin/poolbot +21 -0
  250. package/extensions/line/package.json +1 -1
  251. package/extensions/matrix/node_modules/.bin/poolbot +21 -0
  252. package/extensions/matrix/package.json +1 -1
  253. package/extensions/memory-core/node_modules/.bin/poolbot +21 -0
  254. package/extensions/memory-core/package.json +4 -1
  255. package/extensions/twitch/node_modules/.bin/poolbot +21 -0
  256. package/extensions/twitch/package.json +1 -1
  257. package/package.json +183 -24
  258. package/dist/control-ui/assets/index-CmNMuoem.js.map +0 -1
@@ -1,5 +1,6 @@
1
1
  import { listChannelPlugins } from "../../channels/plugins/index.js";
2
2
  import { parseAbsoluteTimeMs } from "../../cron/parse.js";
3
+ import { formatDurationHuman } from "../../infra/format-time/format-duration.js";
3
4
  import { defaultRuntime } from "../../runtime.js";
4
5
  import { colorize, isRich, theme } from "../../terminal/theme.js";
5
6
  import { callGatewayFromCli } from "../gateway-rpc.js";
@@ -7,8 +8,9 @@ export const getCronChannelOptions = () => ["last", ...listChannelPlugins().map(
7
8
  export async function warnIfCronSchedulerDisabled(opts) {
8
9
  try {
9
10
  const res = (await callGatewayFromCli("cron.status", opts, {}));
10
- if (res?.enabled === true)
11
+ if (res?.enabled === true) {
11
12
  return;
13
+ }
12
14
  const store = typeof res?.storePath === "string" ? res.storePath : "";
13
15
  defaultRuntime.error([
14
16
  "warning: cron scheduler is disabled in the Gateway; jobs are saved but will not run automatically.",
@@ -24,14 +26,17 @@ export async function warnIfCronSchedulerDisabled(opts) {
24
26
  }
25
27
  export function parseDurationMs(input) {
26
28
  const raw = input.trim();
27
- if (!raw)
29
+ if (!raw) {
28
30
  return null;
31
+ }
29
32
  const match = raw.match(/^(\d+(?:\.\d+)?)(ms|s|m|h|d)$/i);
30
- if (!match)
33
+ if (!match) {
31
34
  return null;
35
+ }
32
36
  const n = Number.parseFloat(match[1] ?? "");
33
- if (!Number.isFinite(n) || n <= 0)
37
+ if (!Number.isFinite(n) || n <= 0) {
34
38
  return null;
39
+ }
35
40
  const unit = (match[2] ?? "").toLowerCase();
36
41
  const factor = unit === "ms"
37
42
  ? 1
@@ -44,16 +49,19 @@ export function parseDurationMs(input) {
44
49
  : 86_400_000;
45
50
  return Math.floor(n * factor);
46
51
  }
47
- export function parseAtMs(input) {
52
+ export function parseAt(input) {
48
53
  const raw = input.trim();
49
- if (!raw)
54
+ if (!raw) {
50
55
  return null;
56
+ }
51
57
  const absolute = parseAbsoluteTimeMs(raw);
52
- if (absolute)
53
- return absolute;
58
+ if (absolute !== null) {
59
+ return new Date(absolute).toISOString();
60
+ }
54
61
  const dur = parseDurationMs(raw);
55
- if (dur)
56
- return Date.now() + dur;
62
+ if (dur !== null) {
63
+ return new Date(Date.now() + dur).toISOString();
64
+ }
57
65
  return null;
58
66
  }
59
67
  const CRON_ID_PAD = 36;
@@ -66,56 +74,59 @@ const CRON_TARGET_PAD = 9;
66
74
  const CRON_AGENT_PAD = 10;
67
75
  const pad = (value, width) => value.padEnd(width);
68
76
  const truncate = (value, width) => {
69
- if (value.length <= width)
77
+ if (value.length <= width) {
70
78
  return value;
71
- if (width <= 3)
79
+ }
80
+ if (width <= 3) {
72
81
  return value.slice(0, width);
82
+ }
73
83
  return `${value.slice(0, width - 3)}...`;
74
84
  };
75
- const formatIsoMinute = (ms) => {
76
- const d = new Date(ms);
77
- if (Number.isNaN(d.getTime()))
85
+ const formatIsoMinute = (iso) => {
86
+ const parsed = parseAbsoluteTimeMs(iso);
87
+ const d = new Date(parsed ?? NaN);
88
+ if (Number.isNaN(d.getTime())) {
78
89
  return "-";
79
- const iso = d.toISOString();
80
- return `${iso.slice(0, 10)} ${iso.slice(11, 16)}Z`;
81
- };
82
- const formatDuration = (ms) => {
83
- if (ms < 60_000)
84
- return `${Math.max(1, Math.round(ms / 1000))}s`;
85
- if (ms < 3_600_000)
86
- return `${Math.round(ms / 60_000)}m`;
87
- if (ms < 86_400_000)
88
- return `${Math.round(ms / 3_600_000)}h`;
89
- return `${Math.round(ms / 86_400_000)}d`;
90
+ }
91
+ const isoStr = d.toISOString();
92
+ return `${isoStr.slice(0, 10)} ${isoStr.slice(11, 16)}Z`;
90
93
  };
91
94
  const formatSpan = (ms) => {
92
- if (ms < 60_000)
95
+ if (ms < 60_000) {
93
96
  return "<1m";
94
- if (ms < 3_600_000)
97
+ }
98
+ if (ms < 3_600_000) {
95
99
  return `${Math.round(ms / 60_000)}m`;
96
- if (ms < 86_400_000)
100
+ }
101
+ if (ms < 86_400_000) {
97
102
  return `${Math.round(ms / 3_600_000)}h`;
103
+ }
98
104
  return `${Math.round(ms / 86_400_000)}d`;
99
105
  };
100
106
  const formatRelative = (ms, nowMs) => {
101
- if (!ms)
107
+ if (!ms) {
102
108
  return "-";
109
+ }
103
110
  const delta = ms - nowMs;
104
111
  const label = formatSpan(Math.abs(delta));
105
112
  return delta >= 0 ? `in ${label}` : `${label} ago`;
106
113
  };
107
114
  const formatSchedule = (schedule) => {
108
- if (schedule.kind === "at")
109
- return `at ${formatIsoMinute(schedule.atMs)}`;
110
- if (schedule.kind === "every")
111
- return `every ${formatDuration(schedule.everyMs)}`;
115
+ if (schedule.kind === "at") {
116
+ return `at ${formatIsoMinute(schedule.at)}`;
117
+ }
118
+ if (schedule.kind === "every") {
119
+ return `every ${formatDurationHuman(schedule.everyMs)}`;
120
+ }
112
121
  return schedule.tz ? `cron ${schedule.expr} @ ${schedule.tz}` : `cron ${schedule.expr}`;
113
122
  };
114
123
  const formatStatus = (job) => {
115
- if (!job.enabled)
124
+ if (!job.enabled) {
116
125
  return "disabled";
117
- if (job.state.runningAtMs)
126
+ }
127
+ if (job.state.runningAtMs) {
118
128
  return "running";
129
+ }
119
130
  return job.state.lastStatus ?? "idle";
120
131
  };
121
132
  export function printCronList(jobs, runtime = defaultRuntime) {
@@ -144,17 +155,21 @@ export function printCronList(jobs, runtime = defaultRuntime) {
144
155
  const lastLabel = pad(formatRelative(job.state.lastRunAtMs, now), CRON_LAST_PAD);
145
156
  const statusRaw = formatStatus(job);
146
157
  const statusLabel = pad(statusRaw, CRON_STATUS_PAD);
147
- const targetLabel = pad(job.sessionTarget, CRON_TARGET_PAD);
158
+ const targetLabel = pad(job.sessionTarget ?? "-", CRON_TARGET_PAD);
148
159
  const agentLabel = pad(truncate(job.agentId ?? "default", CRON_AGENT_PAD), CRON_AGENT_PAD);
149
160
  const coloredStatus = (() => {
150
- if (statusRaw === "ok")
161
+ if (statusRaw === "ok") {
151
162
  return colorize(rich, theme.success, statusLabel);
152
- if (statusRaw === "error")
163
+ }
164
+ if (statusRaw === "error") {
153
165
  return colorize(rich, theme.error, statusLabel);
154
- if (statusRaw === "running")
166
+ }
167
+ if (statusRaw === "running") {
155
168
  return colorize(rich, theme.warn, statusLabel);
156
- if (statusRaw === "skipped")
169
+ }
170
+ if (statusRaw === "skipped") {
157
171
  return colorize(rich, theme.muted, statusLabel);
172
+ }
158
173
  return colorize(rich, theme.muted, statusLabel);
159
174
  })();
160
175
  const coloredTarget = job.sessionTarget === "isolated"
@@ -3,7 +3,7 @@ import fs from "node:fs";
3
3
  import path from "node:path";
4
4
  import { loadConfig } from "../config/config.js";
5
5
  import { pickPrimaryTailnetIPv4, pickPrimaryTailnetIPv6 } from "../infra/tailnet.js";
6
- import { getWideAreaZonePath, WIDE_AREA_DISCOVERY_DOMAIN } from "../infra/widearea-dns.js";
6
+ import { getWideAreaZonePath, resolveWideAreaDiscoveryDomain } from "../infra/widearea-dns.js";
7
7
  import { defaultRuntime } from "../runtime.js";
8
8
  import { formatDocsLink } from "../terminal/links.js";
9
9
  import { renderTable } from "../terminal/table.js";
@@ -13,8 +13,9 @@ function run(cmd, args, opts) {
13
13
  encoding: "utf-8",
14
14
  stdio: opts?.inherit ? "inherit" : "pipe",
15
15
  });
16
- if (res.error)
16
+ if (res.error) {
17
17
  throw res.error;
18
+ }
18
19
  if (!opts?.allowFailure && res.status !== 0) {
19
20
  const errText = typeof res.stderr === "string" && res.stderr.trim()
20
21
  ? res.stderr.trim()
@@ -39,8 +40,9 @@ function writeFileSudoIfNeeded(filePath, content) {
39
40
  encoding: "utf-8",
40
41
  stdio: ["pipe", "ignore", "inherit"],
41
42
  });
42
- if (res.error)
43
+ if (res.error) {
43
44
  throw res.error;
45
+ }
44
46
  if (res.status !== 0) {
45
47
  throw new Error(`sudo tee ${filePath} failed: exit ${res.status ?? "unknown"}`);
46
48
  }
@@ -59,8 +61,9 @@ function mkdirSudoIfNeeded(dirPath) {
59
61
  run("sudo", ["mkdir", "-p", dirPath], { inherit: true });
60
62
  }
61
63
  function zoneFileNeedsBootstrap(zonePath) {
62
- if (!fs.existsSync(zonePath))
64
+ if (!fs.existsSync(zonePath)) {
63
65
  return true;
66
+ }
64
67
  try {
65
68
  const content = fs.readFileSync(zonePath, "utf-8");
66
69
  return !/\bSOA\b/.test(content) || !/\bNS\b/.test(content);
@@ -72,14 +75,16 @@ function zoneFileNeedsBootstrap(zonePath) {
72
75
  function detectBrewPrefix() {
73
76
  const out = run("brew", ["--prefix"]);
74
77
  const prefix = out.trim();
75
- if (!prefix)
78
+ if (!prefix) {
76
79
  throw new Error("failed to resolve Homebrew prefix");
80
+ }
77
81
  return prefix;
78
82
  }
79
83
  function ensureImportLine(corefilePath, importGlob) {
80
84
  const existing = fs.readFileSync(corefilePath, "utf-8");
81
- if (existing.includes(importGlob))
85
+ if (existing.includes(importGlob)) {
82
86
  return false;
87
+ }
83
88
  const next = `${existing.replace(/\s*$/, "")}\n\nimport ${importGlob}\n`;
84
89
  writeFileSudoIfNeeded(corefilePath, next);
85
90
  return true;
@@ -91,13 +96,20 @@ export function registerDnsCli(program) {
91
96
  .addHelpText("after", () => `\n${theme.muted("Docs:")} ${formatDocsLink("/cli/dns", "docs.molt.bot/cli/dns")}\n`);
92
97
  dns
93
98
  .command("setup")
94
- .description("Set up CoreDNS to serve poolbot.internal for unicast DNS-SD (Wide-Area Bonjour)")
99
+ .description("Set up CoreDNS to serve your discovery domain for unicast DNS-SD (Wide-Area Bonjour)")
100
+ .option("--domain <domain>", "Wide-area discovery domain (e.g. poolbot.internal)")
95
101
  .option("--apply", "Install/update CoreDNS config and (re)start the service (requires sudo)", false)
96
102
  .action(async (opts) => {
97
103
  const cfg = loadConfig();
98
104
  const tailnetIPv4 = pickPrimaryTailnetIPv4();
99
105
  const tailnetIPv6 = pickPrimaryTailnetIPv6();
100
- const zonePath = getWideAreaZonePath();
106
+ const wideAreaDomain = resolveWideAreaDiscoveryDomain({
107
+ configDomain: opts.domain ?? cfg.discovery?.wideArea?.domain,
108
+ });
109
+ if (!wideAreaDomain) {
110
+ throw new Error("No wide-area domain configured. Set discovery.wideArea.domain or pass --domain.");
111
+ }
112
+ const zonePath = getWideAreaZonePath(wideAreaDomain);
101
113
  const tableWidth = Math.max(60, (process.stdout.columns ?? 120) - 1);
102
114
  defaultRuntime.log(theme.heading("DNS setup"));
103
115
  defaultRuntime.log(renderTable({
@@ -107,7 +119,7 @@ export function registerDnsCli(program) {
107
119
  { key: "Value", header: "Value", minWidth: 24, flex: true },
108
120
  ],
109
121
  rows: [
110
- { Key: "Domain", Value: WIDE_AREA_DISCOVERY_DOMAIN },
122
+ { Key: "Domain", Value: wideAreaDomain },
111
123
  { Key: "Zone file", Value: zonePath },
112
124
  {
113
125
  Key: "Tailnet IP",
@@ -119,12 +131,12 @@ export function registerDnsCli(program) {
119
131
  defaultRuntime.log(theme.heading("Recommended ~/.poolbot/poolbot.json:"));
120
132
  defaultRuntime.log(JSON.stringify({
121
133
  gateway: { bind: "auto" },
122
- discovery: { wideArea: { enabled: true } },
134
+ discovery: { wideArea: { enabled: true, domain: wideAreaDomain } },
123
135
  }, null, 2));
124
136
  defaultRuntime.log("");
125
137
  defaultRuntime.log(theme.heading("Tailscale admin (DNS → Nameservers):"));
126
138
  defaultRuntime.log(theme.muted(`- Add nameserver: ${tailnetIPv4 ?? "<this machine's tailnet IPv4>"}`));
127
- defaultRuntime.log(theme.muted("- Restrict to domain (Split DNS): poolbot.internal"));
139
+ defaultRuntime.log(theme.muted(`- Restrict to domain (Split DNS): ${wideAreaDomain.replace(/\.$/, "")}`));
128
140
  if (!opts.apply) {
129
141
  defaultRuntime.log("");
130
142
  defaultRuntime.log(theme.muted("Run with --apply to install CoreDNS and configure it."));
@@ -141,7 +153,7 @@ export function registerDnsCli(program) {
141
153
  const corefilePath = path.join(etcDir, "Corefile");
142
154
  const confDir = path.join(etcDir, "conf.d");
143
155
  const importGlob = path.join(confDir, "*.server");
144
- const serverPath = path.join(confDir, "poolbot.internal.server");
156
+ const serverPath = path.join(confDir, `${wideAreaDomain.replace(/\.$/, "")}.server`);
145
157
  run("brew", ["list", "coredns"], { allowFailure: true });
146
158
  run("brew", ["install", "coredns"], {
147
159
  inherit: true,
@@ -156,7 +168,7 @@ export function registerDnsCli(program) {
156
168
  }
157
169
  const bindArgs = [tailnetIPv4, tailnetIPv6].filter((v) => Boolean(v?.trim()));
158
170
  const server = [
159
- `${WIDE_AREA_DISCOVERY_DOMAIN.replace(/\.$/, "")}:53 {`,
171
+ `${wideAreaDomain.replace(/\.$/, "")}:53 {`,
160
172
  ` bind ${bindArgs.join(" ")}`,
161
173
  ` file ${zonePath} {`,
162
174
  ` reload 10s`,
@@ -176,7 +188,7 @@ export function registerDnsCli(program) {
176
188
  const serial = `${y}${m}${d}01`;
177
189
  const zoneLines = [
178
190
  `; created by poolbot dns setup (will be overwritten by the gateway when wide-area discovery is enabled)`,
179
- `$ORIGIN ${WIDE_AREA_DISCOVERY_DOMAIN}`,
191
+ `$ORIGIN ${wideAreaDomain}`,
180
192
  `$TTL 60`,
181
193
  `@ IN SOA ns1 hostmaster ${serial} 7200 3600 1209600 60`,
182
194
  `@ IN NS ns1`,
@@ -1,41 +1,51 @@
1
1
  import { gatewayStatusCommand } from "../../commands/gateway-status.js";
2
2
  import { formatHealthChannelLines } from "../../commands/health.js";
3
+ import { loadConfig } from "../../config/config.js";
3
4
  import { discoverGatewayBeacons } from "../../infra/bonjour-discovery.js";
4
- import { WIDE_AREA_DISCOVERY_DOMAIN } from "../../infra/widearea-dns.js";
5
+ import { resolveWideAreaDiscoveryDomain } from "../../infra/widearea-dns.js";
5
6
  import { defaultRuntime } from "../../runtime.js";
6
7
  import { formatDocsLink } from "../../terminal/links.js";
7
8
  import { colorize, isRich, theme } from "../../terminal/theme.js";
8
9
  import { formatTokenCount, formatUsd } from "../../utils/usage-format.js";
9
- import { withProgress } from "../progress.js";
10
10
  import { runCommandWithRuntime } from "../cli-utils.js";
11
11
  import { runDaemonInstall, runDaemonRestart, runDaemonStart, runDaemonStatus, runDaemonStop, runDaemonUninstall, } from "../daemon-cli.js";
12
+ import { withProgress } from "../progress.js";
12
13
  import { callGatewayCli, gatewayCallOpts } from "./call.js";
13
14
  import { dedupeBeacons, parseDiscoverTimeoutMs, pickBeaconHost, pickGatewayPort, renderBeaconLines, } from "./discover.js";
14
15
  import { addGatewayRunCommand } from "./run.js";
15
16
  function styleHealthChannelLine(line, rich) {
16
- if (!rich)
17
+ if (!rich) {
17
18
  return line;
19
+ }
18
20
  const colon = line.indexOf(":");
19
- if (colon === -1)
21
+ if (colon === -1) {
20
22
  return line;
23
+ }
21
24
  const label = line.slice(0, colon + 1);
22
25
  const detail = line.slice(colon + 1).trimStart();
23
26
  const normalized = detail.toLowerCase();
24
27
  const applyPrefix = (prefix, color) => `${label} ${color(detail.slice(0, prefix.length))}${detail.slice(prefix.length)}`;
25
- if (normalized.startsWith("failed"))
28
+ if (normalized.startsWith("failed")) {
26
29
  return applyPrefix("failed", theme.error);
27
- if (normalized.startsWith("ok"))
30
+ }
31
+ if (normalized.startsWith("ok")) {
28
32
  return applyPrefix("ok", theme.success);
29
- if (normalized.startsWith("linked"))
33
+ }
34
+ if (normalized.startsWith("linked")) {
30
35
  return applyPrefix("linked", theme.success);
31
- if (normalized.startsWith("configured"))
36
+ }
37
+ if (normalized.startsWith("configured")) {
32
38
  return applyPrefix("configured", theme.success);
33
- if (normalized.startsWith("not linked"))
39
+ }
40
+ if (normalized.startsWith("not linked")) {
34
41
  return applyPrefix("not linked", theme.warn);
35
- if (normalized.startsWith("not configured"))
42
+ }
43
+ if (normalized.startsWith("not configured")) {
36
44
  return applyPrefix("not configured", theme.muted);
37
- if (normalized.startsWith("unknown"))
45
+ }
46
+ if (normalized.startsWith("unknown")) {
38
47
  return applyPrefix("unknown", theme.warn);
48
+ }
39
49
  return line;
40
50
  }
41
51
  function runGatewayCommand(action, label) {
@@ -46,12 +56,14 @@ function runGatewayCommand(action, label) {
46
56
  });
47
57
  }
48
58
  function parseDaysOption(raw, fallback = 30) {
49
- if (typeof raw === "number" && Number.isFinite(raw))
59
+ if (typeof raw === "number" && Number.isFinite(raw)) {
50
60
  return Math.max(1, Math.floor(raw));
61
+ }
51
62
  if (typeof raw === "string" && raw.trim() !== "") {
52
63
  const parsed = Number(raw);
53
- if (Number.isFinite(parsed))
64
+ if (Number.isFinite(parsed)) {
54
65
  return Math.max(1, Math.floor(parsed));
66
+ }
55
67
  }
56
68
  return fallback;
57
69
  }
@@ -213,19 +225,24 @@ export function registerGatewayCli(program) {
213
225
  });
214
226
  gateway
215
227
  .command("discover")
216
- .description(`Discover gateways via Bonjour (multicast local. + unicast ${WIDE_AREA_DISCOVERY_DOMAIN})`)
228
+ .description("Discover gateways via Bonjour (local + wide-area if configured)")
217
229
  .option("--timeout <ms>", "Per-command timeout in ms", "2000")
218
230
  .option("--json", "Output JSON", false)
219
231
  .action(async (opts) => {
220
232
  await runGatewayCommand(async () => {
233
+ const cfg = loadConfig();
234
+ const wideAreaDomain = resolveWideAreaDiscoveryDomain({
235
+ configDomain: cfg.discovery?.wideArea?.domain,
236
+ });
221
237
  const timeoutMs = parseDiscoverTimeoutMs(opts.timeout, 2000);
238
+ const domains = ["local.", ...(wideAreaDomain ? [wideAreaDomain] : [])];
222
239
  const beacons = await withProgress({
223
240
  label: "Scanning for gateways…",
224
241
  indeterminate: true,
225
242
  enabled: opts.json !== true,
226
243
  delayMs: 0,
227
- }, async () => await discoverGatewayBeacons({ timeoutMs }));
228
- const deduped = dedupeBeacons(beacons).sort((a, b) => String(a.displayName || a.instanceName).localeCompare(String(b.displayName || b.instanceName)));
244
+ }, async () => await discoverGatewayBeacons({ timeoutMs, wideAreaDomain }));
245
+ const deduped = dedupeBeacons(beacons).toSorted((a, b) => String(a.displayName || a.instanceName).localeCompare(String(b.displayName || b.instanceName)));
229
246
  if (opts.json) {
230
247
  const enriched = deduped.map((b) => {
231
248
  const host = pickBeaconHost(b);
@@ -234,7 +251,7 @@ export function registerGatewayCli(program) {
234
251
  });
235
252
  defaultRuntime.log(JSON.stringify({
236
253
  timeoutMs,
237
- domains: ["local.", WIDE_AREA_DISCOVERY_DOMAIN],
254
+ domains,
238
255
  count: enriched.length,
239
256
  beacons: enriched,
240
257
  }, null, 2));
@@ -242,9 +259,10 @@ export function registerGatewayCli(program) {
242
259
  }
243
260
  const rich = isRich();
244
261
  defaultRuntime.log(colorize(rich, theme.heading, "Gateway Discovery"));
245
- defaultRuntime.log(colorize(rich, theme.muted, `Found ${deduped.length} gateway(s) · domains: local., ${WIDE_AREA_DISCOVERY_DOMAIN}`));
246
- if (deduped.length === 0)
262
+ defaultRuntime.log(colorize(rich, theme.muted, `Found ${deduped.length} gateway(s) · domains: ${domains.join(", ")}`));
263
+ if (deduped.length === 0) {
247
264
  return;
265
+ }
248
266
  for (const beacon of deduped) {
249
267
  for (const line of renderBeaconLines(beacon, rich)) {
250
268
  defaultRuntime.log(line);
@@ -161,7 +161,7 @@ export async function runMemoryStatus(opts) {
161
161
  getManager: () => getMemorySearchManager({ cfg, agentId }),
162
162
  onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."),
163
163
  onCloseError: (err) => defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`),
164
- close: (manager) => manager.close(),
164
+ close: async (manager) => { await manager.close?.(); },
165
165
  run: async (manager) => {
166
166
  const deep = Boolean(opts.deep || opts.index);
167
167
  let embeddingProbe;
@@ -182,7 +182,7 @@ export async function runMemoryStatus(opts) {
182
182
  fallback: opts.verbose ? "line" : undefined,
183
183
  }, async (update, progress) => {
184
184
  try {
185
- await manager.sync({
185
+ await manager.sync?.({
186
186
  reason: "cli",
187
187
  progress: (syncUpdate) => {
188
188
  update({
@@ -387,7 +387,7 @@ export function registerMemoryCli(program) {
387
387
  getManager: () => getMemorySearchManager({ cfg, agentId }),
388
388
  onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."),
389
389
  onCloseError: (err) => defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`),
390
- close: (manager) => manager.close(),
390
+ close: async (manager) => { await manager.close?.(); },
391
391
  run: async (manager) => {
392
392
  try {
393
393
  if (opts.verbose) {
@@ -453,7 +453,7 @@ export function registerMemoryCli(program) {
453
453
  progress.setLabel(buildLabel());
454
454
  }, 1000);
455
455
  try {
456
- await manager.sync({
456
+ await manager.sync?.({
457
457
  reason: "cli",
458
458
  force: opts.force,
459
459
  progress: (syncUpdate) => {
@@ -500,7 +500,7 @@ export function registerMemoryCli(program) {
500
500
  getManager: () => getMemorySearchManager({ cfg, agentId }),
501
501
  onMissing: (error) => defaultRuntime.log(error ?? "Memory search disabled."),
502
502
  onCloseError: (err) => defaultRuntime.error(`Memory manager close failed: ${formatErrorMessage(err)}`),
503
- close: (manager) => manager.close(),
503
+ close: async (manager) => { await manager.close?.(); },
504
504
  run: async (manager) => {
505
505
  let results;
506
506
  try {
@@ -0,0 +1,37 @@
1
+ const UNIT_MULTIPLIERS = {
2
+ b: 1,
3
+ kb: 1024,
4
+ k: 1024,
5
+ mb: 1024 ** 2,
6
+ m: 1024 ** 2,
7
+ gb: 1024 ** 3,
8
+ g: 1024 ** 3,
9
+ tb: 1024 ** 4,
10
+ t: 1024 ** 4,
11
+ };
12
+ export function parseByteSize(raw, opts) {
13
+ const trimmed = String(raw ?? "")
14
+ .trim()
15
+ .toLowerCase();
16
+ if (!trimmed) {
17
+ throw new Error("invalid byte size (empty)");
18
+ }
19
+ const m = /^(\d+(?:\.\d+)?)([a-z]+)?$/.exec(trimmed);
20
+ if (!m) {
21
+ throw new Error(`invalid byte size: ${raw}`);
22
+ }
23
+ const value = Number(m[1]);
24
+ if (!Number.isFinite(value) || value < 0) {
25
+ throw new Error(`invalid byte size: ${raw}`);
26
+ }
27
+ const unit = (m[2] ?? opts?.defaultUnit ?? "b").toLowerCase();
28
+ const multiplier = UNIT_MULTIPLIERS[unit];
29
+ if (!multiplier) {
30
+ throw new Error(`invalid byte size unit: ${raw}`);
31
+ }
32
+ const bytes = Math.round(value * multiplier);
33
+ if (!Number.isFinite(bytes)) {
34
+ throw new Error(`invalid byte size: ${raw}`);
35
+ }
36
+ return bytes;
37
+ }