@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,14 +1,15 @@
1
1
  import { runCommandWithTimeout } from "../process/exec.js";
2
- import { WIDE_AREA_DISCOVERY_DOMAIN } from "./widearea-dns.js";
2
+ import { resolveWideAreaDiscoveryDomain } from "./widearea-dns.js";
3
3
  const DEFAULT_TIMEOUT_MS = 2000;
4
- const DEFAULT_DOMAINS = ["local.", WIDE_AREA_DISCOVERY_DOMAIN];
4
+ const GATEWAY_SERVICE_TYPE = "_poolbot-gw._tcp";
5
5
  function decodeDnsSdEscapes(value) {
6
6
  let decoded = false;
7
7
  const bytes = [];
8
8
  let pending = "";
9
9
  const flush = () => {
10
- if (!pending)
10
+ if (!pending) {
11
11
  return;
12
+ }
12
13
  bytes.push(...Buffer.from(pending, "utf8"));
13
14
  pending = "";
14
15
  };
@@ -31,18 +32,21 @@ function decodeDnsSdEscapes(value) {
31
32
  }
32
33
  pending += ch;
33
34
  }
34
- if (!decoded)
35
+ if (!decoded) {
35
36
  return value;
37
+ }
36
38
  flush();
37
39
  return Buffer.from(bytes).toString("utf8");
38
40
  }
39
41
  function isTailnetIPv4(address) {
40
42
  const parts = address.split(".");
41
- if (parts.length !== 4)
43
+ if (parts.length !== 4) {
42
44
  return false;
45
+ }
43
46
  const octets = parts.map((p) => Number.parseInt(p, 10));
44
- if (octets.some((n) => !Number.isFinite(n) || n < 0 || n > 255))
47
+ if (octets.some((n) => !Number.isFinite(n) || n < 0 || n > 255)) {
45
48
  return false;
49
+ }
46
50
  // Tailscale IPv4 range: 100.64.0.0/10
47
51
  const [a, b] = octets;
48
52
  return a === 100 && b >= 64 && b <= 127;
@@ -59,8 +63,9 @@ function parseDigTxt(stdout) {
59
63
  const tokens = [];
60
64
  for (const raw of stdout.split("\n")) {
61
65
  const line = raw.trim();
62
- if (!line)
66
+ if (!line) {
63
67
  continue;
68
+ }
64
69
  const matches = Array.from(line.matchAll(/"([^"]*)"/g), (m) => m[1] ?? "");
65
70
  for (const m of matches) {
66
71
  const unescaped = m.replaceAll("\\\\", "\\").replaceAll('\\"', '"').replaceAll("\\n", "\n");
@@ -75,35 +80,43 @@ function parseDigSrv(stdout) {
75
80
  .split("\n")
76
81
  .map((l) => l.trim())
77
82
  .find(Boolean);
78
- if (!line)
83
+ if (!line) {
79
84
  return null;
85
+ }
80
86
  const parts = line.split(/\s+/).filter(Boolean);
81
- if (parts.length < 4)
87
+ if (parts.length < 4) {
82
88
  return null;
89
+ }
83
90
  const port = Number.parseInt(parts[2] ?? "", 10);
84
91
  const hostRaw = parts[3] ?? "";
85
- if (!Number.isFinite(port) || port <= 0)
92
+ if (!Number.isFinite(port) || port <= 0) {
86
93
  return null;
94
+ }
87
95
  const host = hostRaw.replace(/\.$/, "");
88
- if (!host)
96
+ if (!host) {
89
97
  return null;
98
+ }
90
99
  return { host, port };
91
100
  }
92
101
  function parseTailscaleStatusIPv4s(stdout) {
93
102
  const parsed = stdout ? JSON.parse(stdout) : {};
94
103
  const out = [];
95
104
  const addIps = (value) => {
96
- if (!value || typeof value !== "object")
105
+ if (!value || typeof value !== "object") {
97
106
  return;
107
+ }
98
108
  const ips = value.TailscaleIPs;
99
- if (!Array.isArray(ips))
109
+ if (!Array.isArray(ips)) {
100
110
  return;
111
+ }
101
112
  for (const ip of ips) {
102
- if (typeof ip !== "string")
113
+ if (typeof ip !== "string") {
103
114
  continue;
115
+ }
104
116
  const trimmed = ip.trim();
105
- if (trimmed && isTailnetIPv4(trimmed))
117
+ if (trimmed && isTailnetIPv4(trimmed)) {
106
118
  out.push(trimmed);
119
+ }
107
120
  }
108
121
  };
109
122
  addIps(parsed.Self);
@@ -116,8 +129,9 @@ function parseTailscaleStatusIPv4s(stdout) {
116
129
  return [...new Set(out)];
117
130
  }
118
131
  function parseIntOrNull(value) {
119
- if (!value)
132
+ if (!value) {
120
133
  return undefined;
134
+ }
121
135
  const parsed = Number.parseInt(value, 10);
122
136
  return Number.isFinite(parsed) ? parsed : undefined;
123
137
  }
@@ -125,12 +139,14 @@ function parseTxtTokens(tokens) {
125
139
  const txt = {};
126
140
  for (const token of tokens) {
127
141
  const idx = token.indexOf("=");
128
- if (idx <= 0)
142
+ if (idx <= 0) {
129
143
  continue;
144
+ }
130
145
  const key = token.slice(0, idx).trim();
131
146
  const value = decodeDnsSdEscapes(token.slice(idx + 1).trim());
132
- if (!key)
147
+ if (!key) {
133
148
  continue;
149
+ }
134
150
  txt[key] = value;
135
151
  }
136
152
  return txt;
@@ -139,10 +155,12 @@ function parseDnsSdBrowse(stdout) {
139
155
  const instances = new Set();
140
156
  for (const raw of stdout.split("\n")) {
141
157
  const line = raw.trim();
142
- if (!line || !line.includes("_poolbot-gw._tcp"))
158
+ if (!line || !line.includes(GATEWAY_SERVICE_TYPE)) {
143
159
  continue;
144
- if (!line.includes("Add"))
160
+ }
161
+ if (!line.includes("Add")) {
145
162
  continue;
163
+ }
146
164
  const match = line.match(/_poolbot-gw\._tcp\.?\s+(.+)$/);
147
165
  if (match?.[1]) {
148
166
  instances.add(decodeDnsSdEscapes(match[1].trim()));
@@ -156,8 +174,9 @@ function parseDnsSdResolve(stdout, instanceName) {
156
174
  let txt = {};
157
175
  for (const raw of stdout.split("\n")) {
158
176
  const line = raw.trim();
159
- if (!line)
177
+ if (!line) {
160
178
  continue;
179
+ }
161
180
  if (line.includes("can be reached at")) {
162
181
  const match = line.match(/can be reached at\s+([^\s:]+):(\d+)/i);
163
182
  if (match?.[1]) {
@@ -174,49 +193,59 @@ function parseDnsSdResolve(stdout, instanceName) {
174
193
  }
175
194
  }
176
195
  beacon.txt = Object.keys(txt).length ? txt : undefined;
177
- if (txt.displayName)
196
+ if (txt.displayName) {
178
197
  beacon.displayName = decodeDnsSdEscapes(txt.displayName);
179
- if (txt.lanHost)
198
+ }
199
+ if (txt.lanHost) {
180
200
  beacon.lanHost = txt.lanHost;
181
- if (txt.tailnetDns)
201
+ }
202
+ if (txt.tailnetDns) {
182
203
  beacon.tailnetDns = txt.tailnetDns;
183
- if (txt.cliPath)
204
+ }
205
+ if (txt.cliPath) {
184
206
  beacon.cliPath = txt.cliPath;
207
+ }
185
208
  beacon.gatewayPort = parseIntOrNull(txt.gatewayPort);
186
209
  beacon.sshPort = parseIntOrNull(txt.sshPort);
187
210
  if (txt.gatewayTls) {
188
211
  const raw = txt.gatewayTls.trim().toLowerCase();
189
212
  beacon.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
190
213
  }
191
- if (txt.gatewayTlsSha256)
214
+ if (txt.gatewayTlsSha256) {
192
215
  beacon.gatewayTlsFingerprintSha256 = txt.gatewayTlsSha256;
193
- if (txt.role)
216
+ }
217
+ if (txt.role) {
194
218
  beacon.role = txt.role;
195
- if (txt.transport)
219
+ }
220
+ if (txt.transport) {
196
221
  beacon.transport = txt.transport;
197
- if (!beacon.displayName)
222
+ }
223
+ if (!beacon.displayName) {
198
224
  beacon.displayName = decodedInstanceName;
225
+ }
199
226
  return beacon;
200
227
  }
201
228
  async function discoverViaDnsSd(domain, timeoutMs, run) {
202
- const browse = await run(["dns-sd", "-B", "_poolbot-gw._tcp", domain], {
229
+ const browse = await run(["dns-sd", "-B", GATEWAY_SERVICE_TYPE, domain], {
203
230
  timeoutMs,
204
231
  });
205
232
  const instances = parseDnsSdBrowse(browse.stdout);
206
233
  const results = [];
207
234
  for (const instance of instances) {
208
- const resolved = await run(["dns-sd", "-L", instance, "_poolbot-gw._tcp", domain], {
235
+ const resolved = await run(["dns-sd", "-L", instance, GATEWAY_SERVICE_TYPE, domain], {
209
236
  timeoutMs,
210
237
  });
211
238
  const parsed = parseDnsSdResolve(resolved.stdout, instance);
212
- if (parsed)
239
+ if (parsed) {
213
240
  results.push({ ...parsed, domain });
241
+ }
214
242
  }
215
243
  return results;
216
244
  }
217
245
  async function discoverWideAreaViaTailnetDns(domain, timeoutMs, run) {
218
- if (domain !== WIDE_AREA_DISCOVERY_DOMAIN)
246
+ if (!domain || domain === "local.") {
219
247
  return [];
248
+ }
220
249
  const startedAt = Date.now();
221
250
  const remainingMs = () => timeoutMs - (Date.now() - startedAt);
222
251
  const tailscaleCandidates = ["tailscale", "/Applications/Tailscale.app/Contents/MacOS/Tailscale"];
@@ -227,20 +256,23 @@ async function discoverWideAreaViaTailnetDns(domain, timeoutMs, run) {
227
256
  timeoutMs: Math.max(1, Math.min(700, remainingMs())),
228
257
  });
229
258
  ips = parseTailscaleStatusIPv4s(res.stdout);
230
- if (ips.length > 0)
259
+ if (ips.length > 0) {
231
260
  break;
261
+ }
232
262
  }
233
263
  catch {
234
264
  // ignore
235
265
  }
236
266
  }
237
- if (ips.length === 0)
267
+ if (ips.length === 0) {
238
268
  return [];
239
- if (remainingMs() <= 0)
269
+ }
270
+ if (remainingMs() <= 0) {
240
271
  return [];
272
+ }
241
273
  // Keep scans bounded: this is a fallback and should not block long.
242
274
  ips = ips.slice(0, 40);
243
- const probeName = `_poolbot-gw._tcp.${domain.replace(/\.$/, "")}`;
275
+ const probeName = `${GATEWAY_SERVICE_TYPE}.${domain.replace(/\.$/, "")}`;
244
276
  const concurrency = 6;
245
277
  let nextIndex = 0;
246
278
  let nameserver = null;
@@ -248,20 +280,24 @@ async function discoverWideAreaViaTailnetDns(domain, timeoutMs, run) {
248
280
  const worker = async () => {
249
281
  while (nameserver === null) {
250
282
  const budget = remainingMs();
251
- if (budget <= 0)
283
+ if (budget <= 0) {
252
284
  return;
285
+ }
253
286
  const i = nextIndex;
254
287
  nextIndex += 1;
255
- if (i >= ips.length)
288
+ if (i >= ips.length) {
256
289
  return;
290
+ }
257
291
  const ip = ips[i] ?? "";
258
- if (!ip)
292
+ if (!ip) {
259
293
  continue;
294
+ }
260
295
  try {
261
296
  const probe = await run(["dig", "+short", "+time=1", "+tries=1", `@${ip}`, probeName, "PTR"], { timeoutMs: Math.max(1, Math.min(250, budget)) });
262
297
  const lines = parseDigShortLines(probe.stdout);
263
- if (lines.length === 0)
298
+ if (lines.length === 0) {
264
299
  continue;
300
+ }
265
301
  nameserver = ip;
266
302
  ptrs = lines;
267
303
  return;
@@ -272,26 +308,31 @@ async function discoverWideAreaViaTailnetDns(domain, timeoutMs, run) {
272
308
  }
273
309
  };
274
310
  await Promise.all(Array.from({ length: Math.min(concurrency, ips.length) }, () => worker()));
275
- if (!nameserver || ptrs.length === 0)
311
+ if (!nameserver || ptrs.length === 0) {
276
312
  return [];
277
- if (remainingMs() <= 0)
313
+ }
314
+ if (remainingMs() <= 0) {
278
315
  return [];
316
+ }
279
317
  const nameserverArg = `@${String(nameserver)}`;
280
318
  const results = [];
281
319
  for (const ptr of ptrs) {
282
320
  const budget = remainingMs();
283
- if (budget <= 0)
321
+ if (budget <= 0) {
284
322
  break;
323
+ }
285
324
  const ptrName = ptr.trim().replace(/\.$/, "");
286
- if (!ptrName)
325
+ if (!ptrName) {
287
326
  continue;
327
+ }
288
328
  const instanceName = ptrName.replace(/\.?_poolbot-gw\._tcp\..*$/, "");
289
329
  const srv = await run(["dig", "+short", "+time=1", "+tries=1", nameserverArg, ptrName, "SRV"], {
290
330
  timeoutMs: Math.max(1, Math.min(350, budget)),
291
331
  }).catch(() => null);
292
332
  const srvParsed = srv ? parseDigSrv(srv.stdout) : null;
293
- if (!srvParsed)
333
+ if (!srvParsed) {
294
334
  continue;
335
+ }
295
336
  const txtBudget = remainingMs();
296
337
  if (txtBudget <= 0) {
297
338
  results.push({
@@ -324,12 +365,15 @@ async function discoverWideAreaViaTailnetDns(domain, timeoutMs, run) {
324
365
  const raw = txtMap.gatewayTls.trim().toLowerCase();
325
366
  beacon.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
326
367
  }
327
- if (txtMap.gatewayTlsSha256)
368
+ if (txtMap.gatewayTlsSha256) {
328
369
  beacon.gatewayTlsFingerprintSha256 = txtMap.gatewayTlsSha256;
329
- if (txtMap.role)
370
+ }
371
+ if (txtMap.role) {
330
372
  beacon.role = txtMap.role;
331
- if (txtMap.transport)
373
+ }
374
+ if (txtMap.transport) {
332
375
  beacon.transport = txtMap.transport;
376
+ }
333
377
  results.push(beacon);
334
378
  }
335
379
  return results;
@@ -339,12 +383,14 @@ function parseAvahiBrowse(stdout) {
339
383
  let current = null;
340
384
  for (const raw of stdout.split("\n")) {
341
385
  const line = raw.trimEnd();
342
- if (!line)
386
+ if (!line) {
343
387
  continue;
344
- if (line.startsWith("=") && line.includes("_poolbot-gw._tcp")) {
345
- if (current)
388
+ }
389
+ if (line.startsWith("=") && line.includes(GATEWAY_SERVICE_TYPE)) {
390
+ if (current) {
346
391
  results.push(current);
347
- const marker = " _poolbot-gw._tcp";
392
+ }
393
+ const marker = ` ${GATEWAY_SERVICE_TYPE}`;
348
394
  const idx = line.indexOf(marker);
349
395
  const left = idx >= 0 ? line.slice(0, idx).trim() : line;
350
396
  const parts = left.split(/\s+/);
@@ -355,53 +401,64 @@ function parseAvahiBrowse(stdout) {
355
401
  };
356
402
  continue;
357
403
  }
358
- if (!current)
404
+ if (!current) {
359
405
  continue;
406
+ }
360
407
  const trimmed = line.trim();
361
408
  if (trimmed.startsWith("hostname =")) {
362
409
  const match = trimmed.match(/hostname\s*=\s*\[([^\]]+)\]/);
363
- if (match?.[1])
410
+ if (match?.[1]) {
364
411
  current.host = match[1];
412
+ }
365
413
  continue;
366
414
  }
367
415
  if (trimmed.startsWith("port =")) {
368
416
  const match = trimmed.match(/port\s*=\s*\[(\d+)\]/);
369
- if (match?.[1])
417
+ if (match?.[1]) {
370
418
  current.port = parseIntOrNull(match[1]);
419
+ }
371
420
  continue;
372
421
  }
373
422
  if (trimmed.startsWith("txt =")) {
374
423
  const tokens = Array.from(trimmed.matchAll(/"([^"]*)"/g), (m) => m[1]);
375
424
  const txt = parseTxtTokens(tokens);
376
425
  current.txt = Object.keys(txt).length ? txt : undefined;
377
- if (txt.displayName)
426
+ if (txt.displayName) {
378
427
  current.displayName = txt.displayName;
379
- if (txt.lanHost)
428
+ }
429
+ if (txt.lanHost) {
380
430
  current.lanHost = txt.lanHost;
381
- if (txt.tailnetDns)
431
+ }
432
+ if (txt.tailnetDns) {
382
433
  current.tailnetDns = txt.tailnetDns;
383
- if (txt.cliPath)
434
+ }
435
+ if (txt.cliPath) {
384
436
  current.cliPath = txt.cliPath;
437
+ }
385
438
  current.gatewayPort = parseIntOrNull(txt.gatewayPort);
386
439
  current.sshPort = parseIntOrNull(txt.sshPort);
387
440
  if (txt.gatewayTls) {
388
441
  const raw = txt.gatewayTls.trim().toLowerCase();
389
442
  current.gatewayTls = raw === "1" || raw === "true" || raw === "yes";
390
443
  }
391
- if (txt.gatewayTlsSha256)
444
+ if (txt.gatewayTlsSha256) {
392
445
  current.gatewayTlsFingerprintSha256 = txt.gatewayTlsSha256;
393
- if (txt.role)
446
+ }
447
+ if (txt.role) {
394
448
  current.role = txt.role;
395
- if (txt.transport)
449
+ }
450
+ if (txt.transport) {
396
451
  current.transport = txt.transport;
452
+ }
397
453
  }
398
454
  }
399
- if (current)
455
+ if (current) {
400
456
  results.push(current);
457
+ }
401
458
  return results;
402
459
  }
403
460
  async function discoverViaAvahi(domain, timeoutMs, run) {
404
- const args = ["avahi-browse", "-rt", "_poolbot-gw._tcp"];
461
+ const args = ["avahi-browse", "-rt", GATEWAY_SERVICE_TYPE];
405
462
  if (domain && domain !== "local.") {
406
463
  // avahi-browse wants a plain domain (no trailing dot)
407
464
  args.push("-d", domain.replace(/\.$/, ""));
@@ -416,8 +473,10 @@ export async function discoverGatewayBeacons(opts = {}) {
416
473
  const timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
417
474
  const platform = opts.platform ?? process.platform;
418
475
  const run = opts.run ?? runCommandWithTimeout;
476
+ const wideAreaDomain = resolveWideAreaDiscoveryDomain({ configDomain: opts.wideAreaDomain });
419
477
  const domainsRaw = Array.isArray(opts.domains) ? opts.domains : [];
420
- const domains = (domainsRaw.length > 0 ? domainsRaw : [...DEFAULT_DOMAINS])
478
+ const defaultDomains = ["local.", ...(wideAreaDomain ? [wideAreaDomain] : [])];
479
+ const domains = (domainsRaw.length > 0 ? domainsRaw : defaultDomains)
421
480
  .map((d) => String(d).trim())
422
481
  .filter(Boolean)
423
482
  .map((d) => (d.endsWith(".") ? d : `${d}.`));
@@ -425,10 +484,12 @@ export async function discoverGatewayBeacons(opts = {}) {
425
484
  if (platform === "darwin") {
426
485
  const perDomain = await Promise.allSettled(domains.map(async (domain) => await discoverViaDnsSd(domain, timeoutMs, run)));
427
486
  const discovered = perDomain.flatMap((r) => (r.status === "fulfilled" ? r.value : []));
428
- const wantsWideArea = domains.includes(WIDE_AREA_DISCOVERY_DOMAIN);
429
- const hasWideArea = discovered.some((b) => b.domain === WIDE_AREA_DISCOVERY_DOMAIN);
430
- if (wantsWideArea && !hasWideArea) {
431
- const fallback = await discoverWideAreaViaTailnetDns(WIDE_AREA_DISCOVERY_DOMAIN, timeoutMs, run).catch(() => []);
487
+ const wantsWideArea = wideAreaDomain ? domains.includes(wideAreaDomain) : false;
488
+ const hasWideArea = wideAreaDomain
489
+ ? discovered.some((b) => b.domain === wideAreaDomain)
490
+ : false;
491
+ if (wantsWideArea && !hasWideArea && wideAreaDomain) {
492
+ const fallback = await discoverWideAreaViaTailnetDns(wideAreaDomain, timeoutMs, run).catch(() => []);
432
493
  return [...discovered, ...fallback];
433
494
  }
434
495
  return discovered;
@@ -1,17 +1,34 @@
1
1
  import fs from "node:fs";
2
2
  import path from "node:path";
3
+ import { fileURLToPath } from "node:url";
3
4
  import { runCommandWithTimeout } from "../process/exec.js";
4
5
  import { defaultRuntime } from "../runtime.js";
6
+ import { resolvePoolBotPackageRoot, resolvePoolBotPackageRootSync } from "./poolbot-root.js";
7
+ const CONTROL_UI_DIST_PATH_SEGMENTS = ["dist", "control-ui", "index.html"];
8
+ export function resolveControlUiDistIndexPathForRoot(root) {
9
+ return path.join(root, ...CONTROL_UI_DIST_PATH_SEGMENTS);
10
+ }
11
+ export async function resolveControlUiDistIndexHealth(opts = {}) {
12
+ const indexPath = opts.root
13
+ ? resolveControlUiDistIndexPathForRoot(opts.root)
14
+ : await resolveControlUiDistIndexPath(opts.argv1 ?? process.argv[1]);
15
+ return {
16
+ indexPath,
17
+ exists: Boolean(indexPath && fs.existsSync(indexPath)),
18
+ };
19
+ }
5
20
  export function resolveControlUiRepoRoot(argv1 = process.argv[1]) {
6
- if (!argv1)
21
+ if (!argv1) {
7
22
  return null;
23
+ }
8
24
  const normalized = path.resolve(argv1);
9
25
  const parts = normalized.split(path.sep);
10
26
  const srcIndex = parts.lastIndexOf("src");
11
27
  if (srcIndex !== -1) {
12
28
  const root = parts.slice(0, srcIndex).join(path.sep);
13
- if (fs.existsSync(path.join(root, "ui", "vite.config.ts")))
29
+ if (fs.existsSync(path.join(root, "ui", "vite.config.ts"))) {
14
30
  return root;
31
+ }
15
32
  }
16
33
  let dir = path.dirname(normalized);
17
34
  for (let i = 0; i < 8; i++) {
@@ -20,36 +37,141 @@ export function resolveControlUiRepoRoot(argv1 = process.argv[1]) {
20
37
  return dir;
21
38
  }
22
39
  const parent = path.dirname(dir);
23
- if (parent === dir)
40
+ if (parent === dir) {
24
41
  break;
42
+ }
25
43
  dir = parent;
26
44
  }
27
45
  return null;
28
46
  }
29
- export function resolveControlUiDistIndexPath(argv1 = process.argv[1]) {
30
- if (!argv1)
47
+ export async function resolveControlUiDistIndexPath(argv1 = process.argv[1]) {
48
+ if (!argv1) {
31
49
  return null;
50
+ }
32
51
  const normalized = path.resolve(argv1);
52
+ // Case 1: entrypoint is directly inside dist/ (e.g., dist/entry.js)
33
53
  const distDir = path.dirname(normalized);
34
- if (path.basename(distDir) !== "dist")
54
+ if (path.basename(distDir) === "dist") {
55
+ return path.join(distDir, "control-ui", "index.html");
56
+ }
57
+ const packageRoot = await resolvePoolBotPackageRoot({ argv1: normalized });
58
+ if (packageRoot) {
59
+ return path.join(packageRoot, "dist", "control-ui", "index.html");
60
+ }
61
+ // Fallback: traverse up and find package.json with name "poolbot" + dist/control-ui/index.html
62
+ // This handles global installs where path-based resolution might fail.
63
+ let dir = path.dirname(normalized);
64
+ for (let i = 0; i < 8; i++) {
65
+ const pkgJsonPath = path.join(dir, "package.json");
66
+ const indexPath = path.join(dir, "dist", "control-ui", "index.html");
67
+ if (fs.existsSync(pkgJsonPath) && fs.existsSync(indexPath)) {
68
+ try {
69
+ const raw = fs.readFileSync(pkgJsonPath, "utf-8");
70
+ const parsed = JSON.parse(raw);
71
+ if (parsed.name === "poolbot") {
72
+ return indexPath;
73
+ }
74
+ }
75
+ catch {
76
+ // Invalid package.json, continue searching
77
+ }
78
+ }
79
+ const parent = path.dirname(dir);
80
+ if (parent === dir) {
81
+ break;
82
+ }
83
+ dir = parent;
84
+ }
85
+ return null;
86
+ }
87
+ function addCandidate(candidates, value) {
88
+ if (!value) {
89
+ return;
90
+ }
91
+ candidates.add(path.resolve(value));
92
+ }
93
+ export function resolveControlUiRootOverrideSync(rootOverride) {
94
+ const resolved = path.resolve(rootOverride);
95
+ try {
96
+ const stats = fs.statSync(resolved);
97
+ if (stats.isFile()) {
98
+ return path.basename(resolved) === "index.html" ? path.dirname(resolved) : null;
99
+ }
100
+ if (stats.isDirectory()) {
101
+ const indexPath = path.join(resolved, "index.html");
102
+ return fs.existsSync(indexPath) ? resolved : null;
103
+ }
104
+ }
105
+ catch {
35
106
  return null;
36
- return path.join(distDir, "control-ui", "index.html");
107
+ }
108
+ return null;
109
+ }
110
+ export function resolveControlUiRootSync(opts = {}) {
111
+ const candidates = new Set();
112
+ const argv1 = opts.argv1 ?? process.argv[1];
113
+ const cwd = opts.cwd ?? process.cwd();
114
+ const moduleDir = opts.moduleUrl ? path.dirname(fileURLToPath(opts.moduleUrl)) : null;
115
+ const argv1Dir = argv1 ? path.dirname(path.resolve(argv1)) : null;
116
+ const execDir = (() => {
117
+ try {
118
+ const execPath = opts.execPath ?? process.execPath;
119
+ return path.dirname(fs.realpathSync(execPath));
120
+ }
121
+ catch {
122
+ return null;
123
+ }
124
+ })();
125
+ const packageRoot = resolvePoolBotPackageRootSync({
126
+ argv1,
127
+ moduleUrl: opts.moduleUrl,
128
+ cwd,
129
+ });
130
+ // Packaged app: control-ui lives alongside the executable.
131
+ addCandidate(candidates, execDir ? path.join(execDir, "control-ui") : null);
132
+ if (moduleDir) {
133
+ // dist/<bundle>.js -> dist/control-ui
134
+ addCandidate(candidates, path.join(moduleDir, "control-ui"));
135
+ // dist/gateway/control-ui.js -> dist/control-ui
136
+ addCandidate(candidates, path.join(moduleDir, "../control-ui"));
137
+ // src/gateway/control-ui.ts -> dist/control-ui
138
+ addCandidate(candidates, path.join(moduleDir, "../../dist/control-ui"));
139
+ }
140
+ if (argv1Dir) {
141
+ // poolbot.mjs or dist/<bundle>.js
142
+ addCandidate(candidates, path.join(argv1Dir, "dist", "control-ui"));
143
+ addCandidate(candidates, path.join(argv1Dir, "control-ui"));
144
+ }
145
+ if (packageRoot) {
146
+ addCandidate(candidates, path.join(packageRoot, "dist", "control-ui"));
147
+ }
148
+ addCandidate(candidates, path.join(cwd, "dist", "control-ui"));
149
+ for (const dir of candidates) {
150
+ const indexPath = path.join(dir, "index.html");
151
+ if (fs.existsSync(indexPath)) {
152
+ return dir;
153
+ }
154
+ }
155
+ return null;
37
156
  }
38
157
  function summarizeCommandOutput(text) {
39
158
  const lines = text
40
159
  .split(/\r?\n/g)
41
160
  .map((l) => l.trim())
42
161
  .filter(Boolean);
43
- if (!lines.length)
162
+ if (!lines.length) {
44
163
  return undefined;
164
+ }
45
165
  const last = lines.at(-1);
46
- if (!last)
166
+ if (!last) {
47
167
  return undefined;
168
+ }
48
169
  return last.length > 240 ? `${last.slice(0, 239)}…` : last;
49
170
  }
50
171
  export async function ensureControlUiAssetsBuilt(runtime = defaultRuntime, opts) {
51
- const indexFromDist = resolveControlUiDistIndexPath(process.argv[1]);
52
- if (indexFromDist && fs.existsSync(indexFromDist)) {
172
+ const health = await resolveControlUiDistIndexHealth({ argv1: process.argv[1] });
173
+ const indexFromDist = health.indexPath;
174
+ if (health.exists) {
53
175
  return { ok: true, built: false };
54
176
  }
55
177
  const repoRoot = resolveControlUiRepoRoot(process.argv[1]);
@@ -63,7 +185,7 @@ export async function ensureControlUiAssetsBuilt(runtime = defaultRuntime, opts)
63
185
  message: `${hint}. Build them with \`pnpm ui:build\` (auto-installs UI deps).`,
64
186
  };
65
187
  }
66
- const indexPath = path.join(repoRoot, "dist", "control-ui", "index.html");
188
+ const indexPath = resolveControlUiDistIndexPathForRoot(repoRoot);
67
189
  if (fs.existsSync(indexPath)) {
68
190
  return { ok: true, built: false };
69
191
  }
@@ -8,6 +8,18 @@ export function extractErrorCode(err) {
8
8
  return String(code);
9
9
  return undefined;
10
10
  }
11
+ /**
12
+ * Type guard for NodeJS.ErrnoException (any error with a `code` property).
13
+ */
14
+ export function isErrno(err) {
15
+ return Boolean(err && typeof err === "object" && "code" in err);
16
+ }
17
+ /**
18
+ * Check if an error has a specific errno code.
19
+ */
20
+ export function hasErrnoCode(err, code) {
21
+ return isErrno(err) && err.code === code;
22
+ }
11
23
  export function formatErrorMessage(err) {
12
24
  if (err instanceof Error) {
13
25
  return err.message || err.name || "Error";