@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
@@ -0,0 +1,79 @@
1
+ /**
2
+ * Centralized date/time formatting utilities.
3
+ *
4
+ * All formatters are timezone-aware, using Intl.DateTimeFormat.
5
+ * Consolidates duplicated formatUtcTimestamp / formatZonedTimestamp / resolveExplicitTimezone
6
+ * that previously lived in envelope.ts and session-updates.ts.
7
+ */
8
+ /**
9
+ * Validate an IANA timezone string. Returns the string if valid, undefined otherwise.
10
+ */
11
+ export function resolveTimezone(value) {
12
+ try {
13
+ new Intl.DateTimeFormat("en-US", { timeZone: value }).format(new Date());
14
+ return value;
15
+ }
16
+ catch {
17
+ return undefined;
18
+ }
19
+ }
20
+ /**
21
+ * Format a Date as a UTC timestamp string.
22
+ *
23
+ * Without seconds: `2024-01-15T14:30Z`
24
+ * With seconds: `2024-01-15T14:30:05Z`
25
+ */
26
+ export function formatUtcTimestamp(date, options) {
27
+ const yyyy = String(date.getUTCFullYear()).padStart(4, "0");
28
+ const mm = String(date.getUTCMonth() + 1).padStart(2, "0");
29
+ const dd = String(date.getUTCDate()).padStart(2, "0");
30
+ const hh = String(date.getUTCHours()).padStart(2, "0");
31
+ const min = String(date.getUTCMinutes()).padStart(2, "0");
32
+ if (!options?.displaySeconds) {
33
+ return `${yyyy}-${mm}-${dd}T${hh}:${min}Z`;
34
+ }
35
+ const sec = String(date.getUTCSeconds()).padStart(2, "0");
36
+ return `${yyyy}-${mm}-${dd}T${hh}:${min}:${sec}Z`;
37
+ }
38
+ /**
39
+ * Format a Date with timezone display using Intl.DateTimeFormat.
40
+ *
41
+ * Without seconds: `2024-01-15 14:30 EST`
42
+ * With seconds: `2024-01-15 14:30:05 EST`
43
+ *
44
+ * Returns undefined if Intl formatting fails.
45
+ */
46
+ export function formatZonedTimestamp(date, options) {
47
+ const intlOptions = {
48
+ timeZone: options?.timeZone,
49
+ year: "numeric",
50
+ month: "2-digit",
51
+ day: "2-digit",
52
+ hour: "2-digit",
53
+ minute: "2-digit",
54
+ hourCycle: "h23",
55
+ timeZoneName: "short",
56
+ };
57
+ if (options?.displaySeconds) {
58
+ intlOptions.second = "2-digit";
59
+ }
60
+ const parts = new Intl.DateTimeFormat("en-US", intlOptions).formatToParts(date);
61
+ const pick = (type) => parts.find((part) => part.type === type)?.value;
62
+ const yyyy = pick("year");
63
+ const mm = pick("month");
64
+ const dd = pick("day");
65
+ const hh = pick("hour");
66
+ const min = pick("minute");
67
+ const sec = options?.displaySeconds ? pick("second") : undefined;
68
+ const tz = [...parts]
69
+ .toReversed()
70
+ .find((part) => part.type === "timeZoneName")
71
+ ?.value?.trim();
72
+ if (!yyyy || !mm || !dd || !hh || !min) {
73
+ return undefined;
74
+ }
75
+ if (options?.displaySeconds && sec) {
76
+ return `${yyyy}-${mm}-${dd} ${hh}:${min}:${sec}${tz ? ` ${tz}` : ""}`;
77
+ }
78
+ return `${yyyy}-${mm}-${dd} ${hh}:${min}${tz ? ` ${tz}` : ""}`;
79
+ }
@@ -0,0 +1,81 @@
1
+ export function formatDurationSeconds(ms, options = {}) {
2
+ if (!Number.isFinite(ms)) {
3
+ return "unknown";
4
+ }
5
+ const decimals = options.decimals ?? 1;
6
+ const unit = options.unit ?? "s";
7
+ const seconds = Math.max(0, ms) / 1000;
8
+ const fixed = seconds.toFixed(Math.max(0, decimals));
9
+ const trimmed = fixed.replace(/\.0+$/, "").replace(/(\.\d*[1-9])0+$/, "$1");
10
+ return unit === "seconds" ? `${trimmed} seconds` : `${trimmed}s`;
11
+ }
12
+ /** Precise decimal-seconds output: "500ms" or "1.23s". Input is milliseconds. */
13
+ export function formatDurationPrecise(ms, options = {}) {
14
+ if (!Number.isFinite(ms)) {
15
+ return "unknown";
16
+ }
17
+ if (ms < 1000) {
18
+ return `${ms}ms`;
19
+ }
20
+ return formatDurationSeconds(ms, {
21
+ decimals: options.decimals ?? 2,
22
+ unit: options.unit ?? "s",
23
+ });
24
+ }
25
+ /**
26
+ * Compact compound duration: "500ms", "45s", "2m5s", "1h30m".
27
+ * With `spaced`: "45s", "2m 5s", "1h 30m".
28
+ * Omits trailing zero components: "1m" not "1m 0s", "2h" not "2h 0m".
29
+ * Returns undefined for null/undefined/non-finite/non-positive input.
30
+ */
31
+ export function formatDurationCompact(ms, options) {
32
+ if (ms == null || !Number.isFinite(ms) || ms <= 0) {
33
+ return undefined;
34
+ }
35
+ if (ms < 1000) {
36
+ return `${Math.round(ms)}ms`;
37
+ }
38
+ const sep = options?.spaced ? " " : "";
39
+ const totalSeconds = Math.round(ms / 1000);
40
+ const hours = Math.floor(totalSeconds / 3600);
41
+ const minutes = Math.floor((totalSeconds % 3600) / 60);
42
+ const seconds = totalSeconds % 60;
43
+ if (hours >= 24) {
44
+ const days = Math.floor(hours / 24);
45
+ const remainingHours = hours % 24;
46
+ return remainingHours > 0 ? `${days}d${sep}${remainingHours}h` : `${days}d`;
47
+ }
48
+ if (hours > 0) {
49
+ return minutes > 0 ? `${hours}h${sep}${minutes}m` : `${hours}h`;
50
+ }
51
+ if (minutes > 0) {
52
+ return seconds > 0 ? `${minutes}m${sep}${seconds}s` : `${minutes}m`;
53
+ }
54
+ return `${seconds}s`;
55
+ }
56
+ /**
57
+ * Rounded single-unit duration for display: "500ms", "5s", "3m", "2h", "5d".
58
+ * Returns fallback string for null/undefined/non-finite input.
59
+ */
60
+ export function formatDurationHuman(ms, fallback = "n/a") {
61
+ if (ms == null || !Number.isFinite(ms) || ms < 0) {
62
+ return fallback;
63
+ }
64
+ if (ms < 1000) {
65
+ return `${Math.round(ms)}ms`;
66
+ }
67
+ const sec = Math.round(ms / 1000);
68
+ if (sec < 60) {
69
+ return `${sec}s`;
70
+ }
71
+ const min = Math.round(sec / 60);
72
+ if (min < 60) {
73
+ return `${min}m`;
74
+ }
75
+ const hr = Math.round(min / 60);
76
+ if (hr < 24) {
77
+ return `${hr}h`;
78
+ }
79
+ const day = Math.round(hr / 24);
80
+ return `${day}d`;
81
+ }
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Centralized relative-time formatting utilities.
3
+ *
4
+ * Consolidates 7+ scattered implementations (formatAge, formatAgeShort, formatAgo,
5
+ * formatRelativeTime, formatElapsedTime) into two functions:
6
+ *
7
+ * - `formatTimeAgo(durationMs)` -- format a duration as "5m ago" / "5m" (for known elapsed time)
8
+ * - `formatRelativeTimestamp(epochMs)` -- format an epoch timestamp relative to now (handles future)
9
+ */
10
+ /**
11
+ * Format a duration (in ms) as a human-readable relative time.
12
+ *
13
+ * Input: how many milliseconds ago something happened.
14
+ *
15
+ * With suffix (default): "just now", "5m ago", "3h ago", "2d ago"
16
+ * Without suffix: "0s", "5m", "3h", "2d"
17
+ */
18
+ export function formatTimeAgo(durationMs, options) {
19
+ const suffix = options?.suffix !== false;
20
+ const fallback = options?.fallback ?? "unknown";
21
+ if (durationMs == null || !Number.isFinite(durationMs) || durationMs < 0) {
22
+ return fallback;
23
+ }
24
+ const totalSeconds = Math.round(durationMs / 1000);
25
+ const minutes = Math.round(totalSeconds / 60);
26
+ if (minutes < 1) {
27
+ return suffix ? "just now" : `${totalSeconds}s`;
28
+ }
29
+ if (minutes < 60) {
30
+ return suffix ? `${minutes}m ago` : `${minutes}m`;
31
+ }
32
+ const hours = Math.round(minutes / 60);
33
+ if (hours < 48) {
34
+ return suffix ? `${hours}h ago` : `${hours}h`;
35
+ }
36
+ const days = Math.round(hours / 24);
37
+ return suffix ? `${days}d ago` : `${days}d`;
38
+ }
39
+ /**
40
+ * Format an epoch timestamp relative to now.
41
+ *
42
+ * Handles both past ("5m ago") and future ("in 5m") timestamps.
43
+ * Optionally falls back to a short date for timestamps older than 7 days.
44
+ */
45
+ export function formatRelativeTimestamp(timestampMs, options) {
46
+ const fallback = options?.fallback ?? "n/a";
47
+ if (timestampMs == null || !Number.isFinite(timestampMs)) {
48
+ return fallback;
49
+ }
50
+ const diff = Date.now() - timestampMs;
51
+ const absDiff = Math.abs(diff);
52
+ const isPast = diff >= 0;
53
+ const sec = Math.round(absDiff / 1000);
54
+ if (sec < 60) {
55
+ return isPast ? "just now" : "in <1m";
56
+ }
57
+ const min = Math.round(sec / 60);
58
+ if (min < 60) {
59
+ return isPast ? `${min}m ago` : `in ${min}m`;
60
+ }
61
+ const hr = Math.round(min / 60);
62
+ if (hr < 48) {
63
+ return isPast ? `${hr}h ago` : `in ${hr}h`;
64
+ }
65
+ const day = Math.round(hr / 24);
66
+ if (!options?.dateFallback || day <= 7) {
67
+ return isPast ? `${day}d ago` : `in ${day}d`;
68
+ }
69
+ // Fall back to short date display for old timestamps
70
+ try {
71
+ return new Intl.DateTimeFormat("en-US", {
72
+ month: "short",
73
+ day: "numeric",
74
+ ...(options.timezone ? { timeZone: options.timezone } : {}),
75
+ }).format(new Date(timestampMs));
76
+ }
77
+ catch {
78
+ return `${day}d ago`;
79
+ }
80
+ }