@poolzin/pool-bot 2026.2.20 → 2026.2.22

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 (388) hide show
  1. package/CHANGELOG.md +25 -0
  2. package/dist/agents/api-key-rotation.js +47 -0
  3. package/dist/agents/apply-patch-update.js +19 -9
  4. package/dist/agents/apply-patch.js +72 -47
  5. package/dist/agents/bash-tools.exec.js +141 -559
  6. package/dist/agents/cli-backends.js +49 -6
  7. package/dist/agents/cli-runner/helpers.js +69 -152
  8. package/dist/agents/cli-runner.js +70 -19
  9. package/dist/agents/identity.js +20 -1
  10. package/dist/agents/image-sanitization.js +9 -0
  11. package/dist/agents/live-auth-keys.js +123 -26
  12. package/dist/agents/live-model-filter.js +13 -4
  13. package/dist/agents/model-auth.js +12 -0
  14. package/dist/agents/model-catalog.js +40 -9
  15. package/dist/agents/model-fallback.js +24 -0
  16. package/dist/agents/model-forward-compat.js +60 -23
  17. package/dist/agents/model-selection.js +134 -41
  18. package/dist/agents/pi-auth-json.js +2 -2
  19. package/dist/agents/pi-embedded-helpers/bootstrap.js +65 -15
  20. package/dist/agents/pi-embedded-helpers/errors.js +140 -15
  21. package/dist/agents/pi-embedded-helpers/images.js +22 -12
  22. package/dist/agents/pi-embedded-helpers.js +2 -2
  23. package/dist/agents/pi-embedded-runner/abort.js +10 -3
  24. package/dist/agents/pi-embedded-runner/compact.js +230 -32
  25. package/dist/agents/pi-embedded-runner/extra-params.js +203 -12
  26. package/dist/agents/pi-embedded-runner/google.js +109 -19
  27. package/dist/agents/pi-embedded-runner/history.js +35 -17
  28. package/dist/agents/pi-embedded-runner/run/attempt.js +386 -80
  29. package/dist/agents/pi-embedded-runner/run/images.js +81 -55
  30. package/dist/agents/pi-embedded-runner/run/payloads.js +89 -39
  31. package/dist/agents/pi-embedded-runner/run.js +193 -25
  32. package/dist/agents/pi-embedded-runner/run.overflow-compaction.mocks.shared.js +2 -2
  33. package/dist/agents/pi-embedded-runner/runs.js +17 -8
  34. package/dist/agents/pi-embedded-runner/tool-result-context-guard.js +262 -0
  35. package/dist/agents/pi-embedded-runner.js +1 -1
  36. package/dist/agents/pi-embedded-subscribe.handlers.tools.js +180 -10
  37. package/dist/agents/pi-embedded-subscribe.js +37 -0
  38. package/dist/agents/pi-embedded-subscribe.tools.js +127 -30
  39. package/dist/agents/pi-model-discovery.js +9 -2
  40. package/dist/agents/pi-tool-definition-adapter.js +60 -8
  41. package/dist/agents/pi-tools.before-tool-call.js +1 -1
  42. package/dist/agents/pi-tools.js +113 -94
  43. package/dist/agents/pi-tools.read.js +337 -38
  44. package/dist/agents/poolbot-tools.js +14 -5
  45. package/dist/agents/provider/config-loader.js +76 -0
  46. package/dist/agents/provider/index.js +15 -0
  47. package/dist/agents/provider/integration.js +136 -0
  48. package/dist/agents/provider/models-dev.js +129 -0
  49. package/dist/agents/provider/rate-limits.js +458 -0
  50. package/dist/agents/provider/request-monitor.js +449 -0
  51. package/dist/agents/provider/session-binding.js +376 -0
  52. package/dist/agents/provider/token-pool.js +541 -0
  53. package/dist/agents/sandbox/docker.js +10 -5
  54. package/dist/agents/sandbox/registry.js +96 -46
  55. package/dist/agents/sandbox/sanitize-env-vars.js +82 -0
  56. package/dist/agents/sandbox-paths.js +43 -10
  57. package/dist/agents/session-tool-result-guard-wrapper.js +23 -11
  58. package/dist/agents/session-tool-result-guard.js +39 -39
  59. package/dist/agents/session-transcript-repair.js +36 -33
  60. package/dist/agents/session-write-lock.js +62 -44
  61. package/dist/agents/skills/frontmatter.js +49 -88
  62. package/dist/agents/skills/workspace.js +335 -28
  63. package/dist/agents/subagent-announce.js +508 -174
  64. package/dist/agents/subagent-registry.js +45 -4
  65. package/dist/agents/subagent-spawn.js +16 -33
  66. package/dist/agents/system-prompt-report.js +27 -10
  67. package/dist/agents/system-prompt.js +26 -32
  68. package/dist/agents/tool-call-id.js +69 -17
  69. package/dist/agents/tool-display-common.js +1 -1
  70. package/dist/agents/tool-images.js +64 -31
  71. package/dist/agents/tools/canvas-tool.js +17 -11
  72. package/dist/agents/tools/common.js +37 -19
  73. package/dist/agents/tools/cron-tool.js +40 -38
  74. package/dist/agents/tools/gateway.js +70 -2
  75. package/dist/agents/tools/message-tool.js +181 -40
  76. package/dist/agents/tools/nodes-tool.js +128 -36
  77. package/dist/agents/tools/nodes-utils.js +12 -38
  78. package/dist/agents/tools/session-status-tool.js +24 -71
  79. package/dist/agents/tools/sessions-helpers.js +38 -210
  80. package/dist/agents/tools/sessions-spawn-tool.js +28 -198
  81. package/dist/agents/tools/telegram-actions.js +58 -7
  82. package/dist/agents/tools/web-fetch-utils.js +112 -7
  83. package/dist/agents/tools/web-fetch.js +279 -175
  84. package/dist/agents/tools/web-shared.js +71 -8
  85. package/dist/agents/usage.js +25 -16
  86. package/dist/auto-reply/commands-registry.data.js +85 -11
  87. package/dist/auto-reply/dispatch.js +40 -21
  88. package/dist/auto-reply/reply/abort.js +102 -33
  89. package/dist/auto-reply/reply/commands-core.js +82 -33
  90. package/dist/auto-reply/reply/commands-export-session.js +1 -1
  91. package/dist/auto-reply/reply/commands-info.js +41 -12
  92. package/dist/auto-reply/reply/commands-subagents.js +352 -100
  93. package/dist/auto-reply/reply/commands-system-prompt.js +2 -2
  94. package/dist/auto-reply/reply/dispatch-from-config.js +100 -29
  95. package/dist/auto-reply/reply/elevated-unavailable.js +1 -1
  96. package/dist/auto-reply/reply/inbound-meta.js +12 -1
  97. package/dist/auto-reply/reply/mentions.js +18 -11
  98. package/dist/auto-reply/reply/normalize-reply.js +17 -8
  99. package/dist/auto-reply/reply/reply-dispatcher.js +62 -10
  100. package/dist/auto-reply/reply/session.js +102 -21
  101. package/dist/auto-reply/reply/streaming-directives.js +16 -5
  102. package/dist/auto-reply/status.js +73 -50
  103. package/dist/browser/extension-relay.js +3 -3
  104. package/dist/browser/http-auth.js +1 -1
  105. package/dist/browser/paths.js +2 -2
  106. package/dist/build-info.json +3 -3
  107. package/dist/channels/allowlist-match.js +20 -0
  108. package/dist/channels/allowlists/resolve-utils.js +65 -2
  109. package/dist/channels/chat-type.js +8 -4
  110. package/dist/channels/dock.js +127 -35
  111. package/dist/channels/draft-stream-loop.js +6 -2
  112. package/dist/channels/plugins/actions/telegram.js +42 -18
  113. package/dist/channels/plugins/allowlist-match.js +1 -1
  114. package/dist/channels/plugins/group-mentions.js +51 -41
  115. package/dist/channels/plugins/message-action-names.js +2 -0
  116. package/dist/channels/plugins/message-actions.js +24 -5
  117. package/dist/channels/plugins/normalize/discord.js +26 -4
  118. package/dist/channels/plugins/normalize/signal.js +35 -22
  119. package/dist/channels/plugins/onboarding/helpers.js +8 -26
  120. package/dist/channels/plugins/outbound/imessage.js +15 -14
  121. package/dist/channels/registry.js +20 -7
  122. package/dist/cli/acp-cli.js +7 -5
  123. package/dist/cli/browser-cli-extension.js +25 -12
  124. package/dist/cli/browser-cli-state.cookies-storage.js +25 -6
  125. package/dist/cli/browser-cli-state.js +101 -145
  126. package/dist/cli/command-options.js +28 -0
  127. package/dist/cli/completion-cli.js +6 -6
  128. package/dist/cli/cron-cli/register.cron-add.js +25 -1
  129. package/dist/cli/cron-cli/register.cron-edit.js +44 -0
  130. package/dist/cli/cron-cli/shared.js +7 -1
  131. package/dist/cli/daemon-cli/lifecycle-core.js +23 -21
  132. package/dist/cli/daemon-cli/lifecycle.js +23 -247
  133. package/dist/cli/daemon-cli/register-service-commands.js +25 -4
  134. package/dist/cli/daemon-cli.js +1 -0
  135. package/dist/cli/devices-cli.js +33 -20
  136. package/dist/cli/gateway-cli/register.js +37 -105
  137. package/dist/cli/gateway-cli/run.js +49 -11
  138. package/dist/cli/nodes-camera.js +59 -4
  139. package/dist/cli/nodes-cli/register.camera.js +27 -24
  140. package/dist/cli/nodes-cli/rpc.js +21 -38
  141. package/dist/cli/qr-cli.js +2 -2
  142. package/dist/cli/skills-cli.format.js +2 -2
  143. package/dist/cli/update-cli/progress.js +2 -2
  144. package/dist/cli/update-cli/restart-helper.js +28 -7
  145. package/dist/cli/update-cli/shared.js +7 -7
  146. package/dist/cli/update-cli/status.js +1 -1
  147. package/dist/cli/update-cli/update-command.js +14 -8
  148. package/dist/cli/update-cli/wizard.js +2 -2
  149. package/dist/cli/update-cli.js +21 -1027
  150. package/dist/commands/auth-choice.apply.anthropic.js +10 -2
  151. package/dist/commands/channels/add-mutators.js +3 -35
  152. package/dist/commands/channels/add.js +39 -51
  153. package/dist/commands/config-validation.js +1 -1
  154. package/dist/commands/configure.gateway-auth.js +52 -15
  155. package/dist/commands/configure.gateway.js +84 -40
  156. package/dist/commands/doctor-completion.js +3 -3
  157. package/dist/commands/doctor-config-flow.js +536 -16
  158. package/dist/commands/doctor-gateway-services.js +103 -79
  159. package/dist/commands/doctor-memory-search.js +9 -9
  160. package/dist/commands/doctor-platform-notes.js +57 -30
  161. package/dist/commands/doctor-prompter.js +26 -15
  162. package/dist/commands/doctor-session-locks.js +1 -1
  163. package/dist/commands/doctor.js +21 -9
  164. package/dist/commands/model-picker.js +120 -95
  165. package/dist/commands/models/set.js +2 -21
  166. package/dist/commands/models/shared.js +65 -37
  167. package/dist/commands/onboard-helpers.js +81 -39
  168. package/dist/commands/openai-codex-oauth.js +1 -1
  169. package/dist/commands/sessions.js +52 -53
  170. package/dist/commands/status.summary.js +52 -34
  171. package/dist/commands/test-wizard-helpers.js +2 -2
  172. package/dist/config/defaults.js +79 -42
  173. package/dist/config/group-policy.js +50 -18
  174. package/dist/config/includes.js +37 -10
  175. package/dist/config/schema.help.js +5 -4
  176. package/dist/config/schema.hints.js +2 -2
  177. package/dist/config/schema.labels.js +1 -0
  178. package/dist/config/sessions/group.js +12 -11
  179. package/dist/config/sessions/paths.js +137 -11
  180. package/dist/config/sessions/store.js +185 -65
  181. package/dist/config/sessions/types.js +15 -1
  182. package/dist/config/sessions.js +1 -0
  183. package/dist/config/telegram-custom-commands.js +3 -2
  184. package/dist/config/types.js +2 -0
  185. package/dist/config/zod-schema.agent-defaults.js +6 -27
  186. package/dist/config/zod-schema.agent-runtime.js +171 -79
  187. package/dist/config/zod-schema.providers-core.js +138 -65
  188. package/dist/config/zod-schema.session.js +49 -22
  189. package/dist/control-ui/assets/index-HRr1grwl.js.map +1 -1
  190. package/dist/cron/isolated-agent/run.js +224 -57
  191. package/dist/cron/normalize.js +48 -45
  192. package/dist/cron/run-log.js +14 -0
  193. package/dist/cron/service/jobs.js +190 -28
  194. package/dist/cron/service/normalize.js +29 -11
  195. package/dist/cron/service/store.js +30 -44
  196. package/dist/cron/service/timer.js +182 -96
  197. package/dist/cron/service.js +3 -0
  198. package/dist/cron/stagger.js +37 -0
  199. package/dist/daemon/inspect.js +132 -92
  200. package/dist/daemon/runtime-paths.js +25 -4
  201. package/dist/daemon/service-audit.js +47 -16
  202. package/dist/discord/accounts.js +23 -20
  203. package/dist/discord/monitor/agent-components.js +1115 -219
  204. package/dist/discord/monitor/allow-list.js +114 -34
  205. package/dist/discord/monitor/listeners.js +204 -97
  206. package/dist/discord/monitor/message-handler.js +21 -10
  207. package/dist/discord/monitor/message-handler.preflight.js +195 -101
  208. package/dist/discord/monitor/message-handler.process.js +384 -123
  209. package/dist/discord/monitor/message-utils.js +86 -23
  210. package/dist/discord/monitor/native-command.js +77 -57
  211. package/dist/discord/monitor/provider.js +122 -117
  212. package/dist/discord/monitor/reply-context.js +20 -16
  213. package/dist/discord/monitor/reply-delivery.js +40 -8
  214. package/dist/discord/monitor/rest-fetch.js +22 -0
  215. package/dist/discord/monitor/threading.js +117 -24
  216. package/dist/discord/send.js +2 -1
  217. package/dist/discord/send.outbound.js +124 -11
  218. package/dist/discord/send.shared.js +112 -72
  219. package/dist/discord/voice-message.js +3 -3
  220. package/dist/gateway/auth.js +119 -44
  221. package/dist/gateway/call.js +76 -34
  222. package/dist/gateway/channel-health-monitor.js +57 -50
  223. package/dist/gateway/client.js +63 -29
  224. package/dist/gateway/control-ui-contract.js +1 -1
  225. package/dist/gateway/gateway-config-prompts.shared.js +2 -2
  226. package/dist/gateway/net.js +109 -1
  227. package/dist/gateway/protocol/index.js +5 -8
  228. package/dist/gateway/protocol/schema/agent.js +19 -1
  229. package/dist/gateway/protocol/schema/channels.js +21 -0
  230. package/dist/gateway/protocol/schema/cron.js +43 -30
  231. package/dist/gateway/protocol/schema/protocol-schemas.js +6 -11
  232. package/dist/gateway/protocol/schema/sessions.js +5 -1
  233. package/dist/gateway/protocol/schema.js +0 -1
  234. package/dist/gateway/server/presence-events.js +12 -0
  235. package/dist/gateway/server/ws-connection/message-handler.js +203 -212
  236. package/dist/gateway/server/ws-connection.js +58 -21
  237. package/dist/gateway/server-broadcast.js +18 -13
  238. package/dist/gateway/server-cron.js +177 -10
  239. package/dist/gateway/server-methods/agent-job.js +131 -38
  240. package/dist/gateway/server-methods/send.js +60 -14
  241. package/dist/gateway/server-methods/sessions.js +160 -96
  242. package/dist/gateway/server-methods/system.js +5 -7
  243. package/dist/gateway/server-methods-list.js +8 -0
  244. package/dist/gateway/server-methods.js +24 -8
  245. package/dist/gateway/server-node-events.js +278 -68
  246. package/dist/gateway/session-utils.fs.js +316 -75
  247. package/dist/gateway/session-utils.js +224 -70
  248. package/dist/gateway/sessions-patch.js +63 -20
  249. package/dist/gateway/test-temp-config.js +1 -1
  250. package/dist/gateway/tools-invoke-http.js +118 -70
  251. package/dist/gateway/ws-log.js +135 -107
  252. package/dist/hooks/frontmatter.js +36 -82
  253. package/dist/hooks/install.js +149 -139
  254. package/dist/hooks/internal-hooks.js +29 -4
  255. package/dist/hooks/plugin-hooks.js +2 -1
  256. package/dist/imessage/monitor/deliver.js +10 -4
  257. package/dist/imessage/monitor/monitor-provider.js +138 -375
  258. package/dist/imessage/monitor/runtime.js +4 -8
  259. package/dist/imessage/send.js +65 -19
  260. package/dist/infra/exec-approvals-allowlist.js +7 -0
  261. package/dist/infra/exec-approvals.js +35 -920
  262. package/dist/infra/exec-safe-bin-trust.js +64 -0
  263. package/dist/infra/heartbeat-runner.js +207 -134
  264. package/dist/infra/heartbeat-wake.js +183 -22
  265. package/dist/infra/install-source-utils.js +47 -0
  266. package/dist/infra/net/ssrf.js +170 -36
  267. package/dist/infra/outbound/deliver.js +224 -58
  268. package/dist/infra/outbound/message-action-spec.js +12 -5
  269. package/dist/infra/outbound/outbound-session.js +27 -25
  270. package/dist/infra/poolbot-root.js +32 -22
  271. package/dist/infra/ports.js +14 -11
  272. package/dist/infra/skills-remote.js +48 -37
  273. package/dist/infra/system-events.js +25 -11
  274. package/dist/infra/system-presence.js +26 -33
  275. package/dist/infra/tmp-poolbot-dir.js +81 -2
  276. package/dist/infra/wsl.js +37 -1
  277. package/dist/line/bot-message-context.js +163 -191
  278. package/dist/logging/subsystem.js +59 -22
  279. package/dist/markdown/ir.js +124 -50
  280. package/dist/media/store.js +1 -1
  281. package/dist/media-understanding/runner.entries.js +42 -25
  282. package/dist/media-understanding/runner.js +53 -488
  283. package/dist/memory/embeddings-gemini.js +53 -38
  284. package/dist/memory/manager-embedding-ops.js +48 -69
  285. package/dist/pairing/pairing-store.js +178 -119
  286. package/dist/plugin-sdk/index.js +34 -6
  287. package/dist/plugins/hooks.js +135 -14
  288. package/dist/plugins/install.js +190 -152
  289. package/dist/polls.js +11 -0
  290. package/dist/routing/resolve-route.js +190 -56
  291. package/dist/routing/session-key.js +38 -22
  292. package/dist/runtime.js +35 -9
  293. package/dist/security/audit-channel.js +1 -1
  294. package/dist/sessions/session-key-utils.js +29 -11
  295. package/dist/shared/frontmatter.js +5 -5
  296. package/dist/shared/node-list-types.js +1 -0
  297. package/dist/shared/string-normalization.js +15 -0
  298. package/dist/signal/monitor/event-handler.js +68 -36
  299. package/dist/signal/send.js +29 -37
  300. package/dist/slack/monitor/allow-list.js +10 -11
  301. package/dist/slack/monitor/commands.js +14 -3
  302. package/dist/slack/monitor/events/interactions.js +4 -4
  303. package/dist/slack/monitor/media.js +224 -16
  304. package/dist/slack/monitor/message-handler/dispatch.js +247 -13
  305. package/dist/slack/monitor/message-handler/prepare.js +128 -45
  306. package/dist/slack/monitor/slash.js +357 -144
  307. package/dist/slack/streaming.js +77 -0
  308. package/dist/telegram/accounts.js +40 -13
  309. package/dist/telegram/allowed-updates.js +3 -0
  310. package/dist/telegram/bot/delivery.js +129 -66
  311. package/dist/telegram/bot/helpers.js +136 -122
  312. package/dist/telegram/bot-handlers.js +600 -339
  313. package/dist/telegram/bot-message-context.js +115 -73
  314. package/dist/telegram/bot-message-dispatch.js +235 -104
  315. package/dist/telegram/bot-native-command-menu.js +3 -1
  316. package/dist/telegram/bot-native-commands.js +213 -193
  317. package/dist/telegram/bot.js +24 -132
  318. package/dist/telegram/draft-stream.js +84 -75
  319. package/dist/telegram/format.js +150 -6
  320. package/dist/telegram/send.js +415 -255
  321. package/dist/telegram/targets.js +21 -2
  322. package/dist/telegram/update-offset-store.js +19 -3
  323. package/dist/terminal/restore.js +5 -2
  324. package/dist/test-utils/fetch-mock.js +5 -0
  325. package/dist/version.js +18 -5
  326. package/dist/web/auto-reply/monitor/broadcast.js +7 -3
  327. package/dist/web/auto-reply/monitor/on-message.js +6 -3
  328. package/dist/web/inbound/media.js +34 -8
  329. package/dist/web/inbound/monitor.js +34 -17
  330. package/dist/web/inbound/send-api.js +18 -17
  331. package/dist/web/outbound.js +12 -5
  332. package/dist/wizard/clack-prompter.js +40 -7
  333. package/extensions/bluebubbles/package.json +1 -1
  334. package/extensions/copilot-proxy/package.json +1 -1
  335. package/extensions/diagnostics-otel/package.json +1 -1
  336. package/extensions/discord/package.json +1 -1
  337. package/extensions/feishu/package.json +1 -1
  338. package/extensions/google-antigravity-auth/package.json +1 -1
  339. package/extensions/google-gemini-cli-auth/package.json +1 -1
  340. package/extensions/googlechat/package.json +1 -1
  341. package/extensions/imessage/package.json +1 -1
  342. package/extensions/irc/package.json +1 -1
  343. package/extensions/line/package.json +1 -1
  344. package/extensions/llm-task/package.json +1 -1
  345. package/extensions/lobster/package.json +1 -1
  346. package/extensions/matrix/CHANGELOG.md +5 -0
  347. package/extensions/matrix/package.json +1 -1
  348. package/extensions/mattermost/package.json +1 -1
  349. package/extensions/memory-core/package.json +1 -1
  350. package/extensions/memory-lancedb/package.json +1 -1
  351. package/extensions/minimax-portal-auth/package.json +1 -1
  352. package/extensions/msteams/CHANGELOG.md +5 -0
  353. package/extensions/msteams/package.json +1 -1
  354. package/extensions/nextcloud-talk/package.json +1 -1
  355. package/extensions/nostr/CHANGELOG.md +5 -0
  356. package/extensions/nostr/package.json +1 -1
  357. package/extensions/open-prose/package.json +1 -1
  358. package/extensions/openai-codex-auth/package.json +1 -1
  359. package/extensions/signal/package.json +1 -1
  360. package/extensions/slack/package.json +1 -1
  361. package/extensions/telegram/package.json +1 -1
  362. package/extensions/tlon/package.json +1 -1
  363. package/extensions/twitch/CHANGELOG.md +5 -0
  364. package/extensions/twitch/package.json +1 -1
  365. package/extensions/voice-call/CHANGELOG.md +5 -0
  366. package/extensions/voice-call/package.json +1 -1
  367. package/extensions/whatsapp/package.json +1 -1
  368. package/extensions/zalo/CHANGELOG.md +5 -0
  369. package/extensions/zalo/package.json +1 -1
  370. package/extensions/zalouser/CHANGELOG.md +5 -0
  371. package/extensions/zalouser/package.json +1 -1
  372. package/package.json +1 -1
  373. package/skills/apple-reminders/SKILL.md +100 -49
  374. package/skills/coding-agent/SKILL.md +34 -28
  375. package/skills/github/SKILL.md +131 -16
  376. package/skills/imsg/SKILL.md +112 -15
  377. package/skills/openhue/SKILL.md +101 -19
  378. package/skills/plcode-controller/SKILL.md +156 -0
  379. package/skills/plcode-controller/assets/operator-prompts.md +65 -0
  380. package/skills/plcode-controller/references/command-cheatsheet.md +53 -0
  381. package/skills/plcode-controller/references/failure-handling.md +60 -0
  382. package/skills/plcode-controller/references/model-selection.md +57 -0
  383. package/skills/plcode-controller/references/plan-vs-build.md +52 -0
  384. package/skills/plcode-controller/references/question-handling.md +40 -0
  385. package/skills/plcode-controller/references/session-management.md +63 -0
  386. package/skills/plcode-controller/references/workflow.md +35 -0
  387. package/skills/tmux/SKILL.md +111 -79
  388. package/skills/weather/SKILL.md +88 -25
@@ -59,8 +59,10 @@ export function registerCronAddCommand(cron) {
59
59
  .option("--wake <mode>", "Wake mode (now|next-heartbeat)", "now")
60
60
  .option("--at <when>", "Run once at time (ISO) or +duration (e.g. 20m)")
61
61
  .option("--every <duration>", "Run every duration (e.g. 10m, 1h)")
62
- .option("--cron <expr>", "Cron expression (5-field)")
62
+ .option("--cron <expr>", "Cron expression (5-field or 6-field with seconds)")
63
63
  .option("--tz <iana>", "Timezone for cron expressions (IANA)", "")
64
+ .option("--stagger <duration>", "Cron stagger window (e.g. 30s, 5m)")
65
+ .option("--exact", "Disable cron staggering (set stagger to 0)", false)
64
66
  .option("--system-event <text>", "System event payload (main session)")
65
67
  .option("--message <text>", "Agent message payload")
66
68
  .option("--thinking <level>", "Thinking level for agent jobs (off|minimal|low|medium|high)")
@@ -75,6 +77,11 @@ export function registerCronAddCommand(cron) {
75
77
  .option("--json", "Output JSON", false)
76
78
  .action(async (opts, cmd) => {
77
79
  try {
80
+ const staggerRaw = typeof opts.stagger === "string" ? opts.stagger.trim() : "";
81
+ const useExact = Boolean(opts.exact);
82
+ if (staggerRaw && useExact) {
83
+ throw new Error("Choose either --stagger or --exact, not both");
84
+ }
78
85
  const schedule = (() => {
79
86
  const at = typeof opts.at === "string" ? opts.at : "";
80
87
  const every = typeof opts.every === "string" ? opts.every : "";
@@ -83,6 +90,9 @@ export function registerCronAddCommand(cron) {
83
90
  if (chosen !== 1) {
84
91
  throw new Error("Choose exactly one schedule: --at, --every, or --cron");
85
92
  }
93
+ if ((useExact || staggerRaw) && !cronExpr) {
94
+ throw new Error("--stagger/--exact are only valid with --cron");
95
+ }
86
96
  if (at) {
87
97
  const atIso = parseAt(at);
88
98
  if (!atIso) {
@@ -97,10 +107,24 @@ export function registerCronAddCommand(cron) {
97
107
  }
98
108
  return { kind: "every", everyMs };
99
109
  }
110
+ const staggerMs = (() => {
111
+ if (useExact) {
112
+ return 0;
113
+ }
114
+ if (!staggerRaw) {
115
+ return undefined;
116
+ }
117
+ const parsed = parseDurationMs(staggerRaw);
118
+ if (!parsed) {
119
+ throw new Error("Invalid --stagger; use e.g. 30s, 1m, 5m");
120
+ }
121
+ return parsed;
122
+ })();
100
123
  return {
101
124
  kind: "cron",
102
125
  expr: cronExpr,
103
126
  tz: typeof opts.tz === "string" && opts.tz.trim() ? opts.tz.trim() : undefined,
127
+ staggerMs,
104
128
  };
105
129
  })();
106
130
  const wakeModeRaw = typeof opts.wake === "string" ? opts.wake : "now";
@@ -27,6 +27,8 @@ export function registerCronEditCommand(cron) {
27
27
  .option("--every <duration>", "Set interval duration like 10m")
28
28
  .option("--cron <expr>", "Set cron expression")
29
29
  .option("--tz <iana>", "Timezone for cron expressions (IANA)")
30
+ .option("--stagger <duration>", "Cron stagger window (e.g. 30s, 5m)")
31
+ .option("--exact", "Disable cron staggering (set stagger to 0)")
30
32
  .option("--system-event <text>", "Set systemEvent payload")
31
33
  .option("--message <text>", "Set agentTurn payload message")
32
34
  .option("--thinking <level>", "Thinking level for agent jobs")
@@ -50,6 +52,24 @@ export function registerCronEditCommand(cron) {
50
52
  if (opts.announce && typeof opts.deliver === "boolean") {
51
53
  throw new Error("Choose --announce or --no-deliver (not multiple).");
52
54
  }
55
+ const staggerRaw = typeof opts.stagger === "string" ? opts.stagger.trim() : "";
56
+ const useExact = Boolean(opts.exact);
57
+ if (staggerRaw && useExact) {
58
+ throw new Error("Choose either --stagger or --exact, not both");
59
+ }
60
+ const requestedStaggerMs = (() => {
61
+ if (useExact) {
62
+ return 0;
63
+ }
64
+ if (!staggerRaw) {
65
+ return undefined;
66
+ }
67
+ const parsed = parseDurationMs(staggerRaw);
68
+ if (!parsed) {
69
+ throw new Error("Invalid --stagger; use e.g. 30s, 1m, 5m");
70
+ }
71
+ return parsed;
72
+ })();
53
73
  const patch = {};
54
74
  if (typeof opts.name === "string") {
55
75
  patch.name = opts.name;
@@ -94,6 +114,10 @@ export function registerCronEditCommand(cron) {
94
114
  if (scheduleChosen > 1) {
95
115
  throw new Error("Choose at most one schedule change");
96
116
  }
117
+ if ((requestedStaggerMs !== undefined || typeof opts.tz === "string") &&
118
+ (opts.at || opts.every)) {
119
+ throw new Error("--stagger/--exact/--tz are only valid for cron schedules");
120
+ }
97
121
  if (opts.at) {
98
122
  const atIso = parseAt(String(opts.at));
99
123
  if (!atIso) {
@@ -113,6 +137,26 @@ export function registerCronEditCommand(cron) {
113
137
  kind: "cron",
114
138
  expr: String(opts.cron),
115
139
  tz: typeof opts.tz === "string" && opts.tz.trim() ? opts.tz.trim() : undefined,
140
+ staggerMs: requestedStaggerMs,
141
+ };
142
+ }
143
+ else if (requestedStaggerMs !== undefined || typeof opts.tz === "string") {
144
+ const listed = (await callGatewayFromCli("cron.list", opts, {
145
+ includeDisabled: true,
146
+ }));
147
+ const existing = (listed?.jobs ?? []).find((job) => job.id === id);
148
+ if (!existing) {
149
+ throw new Error(`unknown cron job id: ${id}`);
150
+ }
151
+ if (existing.schedule.kind !== "cron") {
152
+ throw new Error("Current job is not a cron schedule; use --cron to convert first");
153
+ }
154
+ const tz = typeof opts.tz === "string" ? opts.tz.trim() || undefined : existing.schedule.tz;
155
+ patch.schedule = {
156
+ kind: "cron",
157
+ expr: existing.schedule.expr,
158
+ tz,
159
+ staggerMs: requestedStaggerMs !== undefined ? requestedStaggerMs : existing.schedule.staggerMs,
116
160
  };
117
161
  }
118
162
  const hasSystemEventPatch = typeof opts.systemEvent === "string";
@@ -1,5 +1,6 @@
1
1
  import { listChannelPlugins } from "../../channels/plugins/index.js";
2
2
  import { parseAbsoluteTimeMs } from "../../cron/parse.js";
3
+ import { resolveCronStaggerMs } from "../../cron/stagger.js";
3
4
  import { formatDurationHuman } from "../../infra/format-time/format-duration.js";
4
5
  import { defaultRuntime } from "../../runtime.js";
5
6
  import { colorize, isRich, theme } from "../../terminal/theme.js";
@@ -118,7 +119,12 @@ const formatSchedule = (schedule) => {
118
119
  if (schedule.kind === "every") {
119
120
  return `every ${formatDurationHuman(schedule.everyMs)}`;
120
121
  }
121
- return schedule.tz ? `cron ${schedule.expr} @ ${schedule.tz}` : `cron ${schedule.expr}`;
122
+ const base = schedule.tz ? `cron ${schedule.expr} @ ${schedule.tz}` : `cron ${schedule.expr}`;
123
+ const staggerMs = resolveCronStaggerMs(schedule);
124
+ if (staggerMs <= 0) {
125
+ return `${base} (exact)`;
126
+ }
127
+ return `${base} (stagger ${formatDurationHuman(staggerMs)})`;
122
128
  };
123
129
  const formatStatus = (job) => {
124
130
  if (!job.enabled) {
@@ -205,31 +205,33 @@ export async function runServiceRestart(params) {
205
205
  });
206
206
  return false;
207
207
  }
208
- // Check for token drift before restart (service token vs config token)
209
208
  const warnings = [];
210
- try {
211
- const command = await params.service.readCommand(process.env);
212
- const serviceToken = command?.environment?.POOLBOT_GATEWAY_TOKEN;
213
- const cfg = loadConfig();
214
- const configToken = cfg.gateway?.auth?.token ||
215
- process.env.POOLBOT_GATEWAY_TOKEN ||
216
- process.env.CLAWDBOT_GATEWAY_TOKEN;
217
- const driftIssue = checkTokenDrift({ serviceToken, configToken });
218
- if (driftIssue) {
219
- const warning = driftIssue.detail
220
- ? `${driftIssue.message} ${driftIssue.detail}`
221
- : driftIssue.message;
222
- warnings.push(warning);
223
- if (!json) {
224
- defaultRuntime.log(`\n⚠️ ${driftIssue.message}`);
225
- if (driftIssue.detail) {
226
- defaultRuntime.log(` ${driftIssue.detail}\n`);
209
+ if (params.checkTokenDrift) {
210
+ // Check for token drift before restart (service token vs config token)
211
+ try {
212
+ const command = await params.service.readCommand(process.env);
213
+ const serviceToken = command?.environment?.POOLBOT_GATEWAY_TOKEN;
214
+ const cfg = loadConfig();
215
+ const configToken = cfg.gateway?.auth?.token ||
216
+ process.env.POOLBOT_GATEWAY_TOKEN ||
217
+ process.env.CLAWDBOT_GATEWAY_TOKEN;
218
+ const driftIssue = checkTokenDrift({ serviceToken, configToken });
219
+ if (driftIssue) {
220
+ const warning = driftIssue.detail
221
+ ? `${driftIssue.message} ${driftIssue.detail}`
222
+ : driftIssue.message;
223
+ warnings.push(warning);
224
+ if (!json) {
225
+ defaultRuntime.log(`\n⚠️ ${driftIssue.message}`);
226
+ if (driftIssue.detail) {
227
+ defaultRuntime.log(` ${driftIssue.detail}\n`);
228
+ }
227
229
  }
228
230
  }
229
231
  }
230
- }
231
- catch {
232
- // Non-fatal: token drift check is best-effort
232
+ catch {
233
+ // Non-fatal: token drift check is best-effort
234
+ }
233
235
  }
234
236
  try {
235
237
  await params.service.restart({ env: process.env, stdout });
@@ -1,192 +1,28 @@
1
- import { resolveIsNixMode } from "../../config/paths.js";
2
1
  import { resolveGatewayService } from "../../daemon/service.js";
3
- import { isSystemdUserServiceAvailable } from "../../daemon/systemd.js";
4
- import { renderSystemdUnavailableHints } from "../../daemon/systemd-hints.js";
5
- import { isWSL } from "../../infra/wsl.js";
6
- import { defaultRuntime } from "../../runtime.js";
7
- import { buildDaemonServiceSnapshot, createNullWriter, emitDaemonActionJson } from "./response.js";
2
+ import { runServiceRestart, runServiceStart, runServiceStop, runServiceUninstall, } from "./lifecycle-core.js";
8
3
  import { renderGatewayServiceStartHints } from "./shared.js";
9
4
  export async function runDaemonUninstall(opts = {}) {
10
- const json = Boolean(opts.json);
11
- const stdout = json ? createNullWriter() : process.stdout;
12
- const emit = (payload) => {
13
- if (!json)
14
- return;
15
- emitDaemonActionJson({ action: "uninstall", ...payload });
16
- };
17
- const fail = (message) => {
18
- if (json)
19
- emit({ ok: false, error: message });
20
- else
21
- defaultRuntime.error(message);
22
- defaultRuntime.exit(1);
23
- };
24
- if (resolveIsNixMode(process.env)) {
25
- fail("Nix mode detected; service uninstall is disabled.");
26
- return;
27
- }
28
- const service = resolveGatewayService();
29
- let loaded = false;
30
- try {
31
- loaded = await service.isLoaded({ env: process.env });
32
- }
33
- catch {
34
- loaded = false;
35
- }
36
- if (loaded) {
37
- try {
38
- await service.stop({ env: process.env, stdout });
39
- }
40
- catch {
41
- // Best-effort stop; final loaded check gates success.
42
- }
43
- }
44
- try {
45
- await service.uninstall({ env: process.env, stdout });
46
- }
47
- catch (err) {
48
- fail(`Gateway uninstall failed: ${String(err)}`);
49
- return;
50
- }
51
- loaded = false;
52
- try {
53
- loaded = await service.isLoaded({ env: process.env });
54
- }
55
- catch {
56
- loaded = false;
57
- }
58
- if (loaded) {
59
- fail("Gateway service still loaded after uninstall.");
60
- return;
61
- }
62
- emit({
63
- ok: true,
64
- result: "uninstalled",
65
- service: buildDaemonServiceSnapshot(service, loaded),
5
+ return await runServiceUninstall({
6
+ serviceNoun: "Gateway",
7
+ service: resolveGatewayService(),
8
+ opts,
9
+ stopBeforeUninstall: true,
10
+ assertNotLoadedAfterUninstall: true,
66
11
  });
67
12
  }
68
13
  export async function runDaemonStart(opts = {}) {
69
- const json = Boolean(opts.json);
70
- const stdout = json ? createNullWriter() : process.stdout;
71
- const emit = (payload) => {
72
- if (!json)
73
- return;
74
- emitDaemonActionJson({ action: "start", ...payload });
75
- };
76
- const fail = (message, hints) => {
77
- if (json)
78
- emit({ ok: false, error: message, hints });
79
- else
80
- defaultRuntime.error(message);
81
- defaultRuntime.exit(1);
82
- };
83
- const service = resolveGatewayService();
84
- let loaded = false;
85
- try {
86
- loaded = await service.isLoaded({ env: process.env });
87
- }
88
- catch (err) {
89
- fail(`Gateway service check failed: ${String(err)}`);
90
- return;
91
- }
92
- if (!loaded) {
93
- let hints = renderGatewayServiceStartHints();
94
- if (process.platform === "linux") {
95
- const systemdAvailable = await isSystemdUserServiceAvailable().catch(() => false);
96
- if (!systemdAvailable) {
97
- hints = [...hints, ...renderSystemdUnavailableHints({ wsl: await isWSL() })];
98
- }
99
- }
100
- emit({
101
- ok: true,
102
- result: "not-loaded",
103
- message: `Gateway service ${service.notLoadedText}.`,
104
- hints,
105
- service: buildDaemonServiceSnapshot(service, loaded),
106
- });
107
- if (!json) {
108
- defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
109
- for (const hint of hints) {
110
- defaultRuntime.log(`Start with: ${hint}`);
111
- }
112
- }
113
- return;
114
- }
115
- try {
116
- await service.restart({ env: process.env, stdout });
117
- }
118
- catch (err) {
119
- const hints = renderGatewayServiceStartHints();
120
- fail(`Gateway start failed: ${String(err)}`, hints);
121
- return;
122
- }
123
- let started = true;
124
- try {
125
- started = await service.isLoaded({ env: process.env });
126
- }
127
- catch {
128
- started = true;
129
- }
130
- emit({
131
- ok: true,
132
- result: "started",
133
- service: buildDaemonServiceSnapshot(service, started),
14
+ return await runServiceStart({
15
+ serviceNoun: "Gateway",
16
+ service: resolveGatewayService(),
17
+ renderStartHints: renderGatewayServiceStartHints,
18
+ opts,
134
19
  });
135
20
  }
136
21
  export async function runDaemonStop(opts = {}) {
137
- const json = Boolean(opts.json);
138
- const stdout = json ? createNullWriter() : process.stdout;
139
- const emit = (payload) => {
140
- if (!json)
141
- return;
142
- emitDaemonActionJson({ action: "stop", ...payload });
143
- };
144
- const fail = (message) => {
145
- if (json)
146
- emit({ ok: false, error: message });
147
- else
148
- defaultRuntime.error(message);
149
- defaultRuntime.exit(1);
150
- };
151
- const service = resolveGatewayService();
152
- let loaded = false;
153
- try {
154
- loaded = await service.isLoaded({ env: process.env });
155
- }
156
- catch (err) {
157
- fail(`Gateway service check failed: ${String(err)}`);
158
- return;
159
- }
160
- if (!loaded) {
161
- emit({
162
- ok: true,
163
- result: "not-loaded",
164
- message: `Gateway service ${service.notLoadedText}.`,
165
- service: buildDaemonServiceSnapshot(service, loaded),
166
- });
167
- if (!json) {
168
- defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
169
- }
170
- return;
171
- }
172
- try {
173
- await service.stop({ env: process.env, stdout });
174
- }
175
- catch (err) {
176
- fail(`Gateway stop failed: ${String(err)}`);
177
- return;
178
- }
179
- let stopped = false;
180
- try {
181
- stopped = await service.isLoaded({ env: process.env });
182
- }
183
- catch {
184
- stopped = false;
185
- }
186
- emit({
187
- ok: true,
188
- result: "stopped",
189
- service: buildDaemonServiceSnapshot(service, stopped),
22
+ return await runServiceStop({
23
+ serviceNoun: "Gateway",
24
+ service: resolveGatewayService(),
25
+ opts,
190
26
  });
191
27
  }
192
28
  /**
@@ -195,71 +31,11 @@ export async function runDaemonStop(opts = {}) {
195
31
  * Throws/exits on check or restart failures.
196
32
  */
197
33
  export async function runDaemonRestart(opts = {}) {
198
- const json = Boolean(opts.json);
199
- const stdout = json ? createNullWriter() : process.stdout;
200
- const emit = (payload) => {
201
- if (!json)
202
- return;
203
- emitDaemonActionJson({ action: "restart", ...payload });
204
- };
205
- const fail = (message, hints) => {
206
- if (json)
207
- emit({ ok: false, error: message, hints });
208
- else
209
- defaultRuntime.error(message);
210
- defaultRuntime.exit(1);
211
- };
212
- const service = resolveGatewayService();
213
- let loaded = false;
214
- try {
215
- loaded = await service.isLoaded({ env: process.env });
216
- }
217
- catch (err) {
218
- fail(`Gateway service check failed: ${String(err)}`);
219
- return false;
220
- }
221
- if (!loaded) {
222
- let hints = renderGatewayServiceStartHints();
223
- if (process.platform === "linux") {
224
- const systemdAvailable = await isSystemdUserServiceAvailable().catch(() => false);
225
- if (!systemdAvailable) {
226
- hints = [...hints, ...renderSystemdUnavailableHints({ wsl: await isWSL() })];
227
- }
228
- }
229
- emit({
230
- ok: true,
231
- result: "not-loaded",
232
- message: `Gateway service ${service.notLoadedText}.`,
233
- hints,
234
- service: buildDaemonServiceSnapshot(service, loaded),
235
- });
236
- if (!json) {
237
- defaultRuntime.log(`Gateway service ${service.notLoadedText}.`);
238
- for (const hint of hints) {
239
- defaultRuntime.log(`Start with: ${hint}`);
240
- }
241
- }
242
- return false;
243
- }
244
- try {
245
- await service.restart({ env: process.env, stdout });
246
- let restarted = true;
247
- try {
248
- restarted = await service.isLoaded({ env: process.env });
249
- }
250
- catch {
251
- restarted = true;
252
- }
253
- emit({
254
- ok: true,
255
- result: "restarted",
256
- service: buildDaemonServiceSnapshot(service, restarted),
257
- });
258
- return true;
259
- }
260
- catch (err) {
261
- const hints = renderGatewayServiceStartHints();
262
- fail(`Gateway restart failed: ${String(err)}`, hints);
263
- return false;
264
- }
34
+ return await runServiceRestart({
35
+ serviceNoun: "Gateway",
36
+ service: resolveGatewayService(),
37
+ renderStartHints: renderGatewayServiceStartHints,
38
+ opts,
39
+ checkTokenDrift: true,
40
+ });
265
41
  }
@@ -1,4 +1,25 @@
1
+ import { inheritOptionFromParent } from "../command-options.js";
1
2
  import { runDaemonInstall, runDaemonRestart, runDaemonStart, runDaemonStatus, runDaemonStop, runDaemonUninstall, } from "./runners.js";
3
+ function resolveInstallOptions(cmdOpts, command) {
4
+ const parentForce = inheritOptionFromParent(command, "force");
5
+ const parentPort = inheritOptionFromParent(command, "port");
6
+ const parentToken = inheritOptionFromParent(command, "token");
7
+ return {
8
+ ...cmdOpts,
9
+ force: Boolean(cmdOpts.force || parentForce),
10
+ port: cmdOpts.port ?? parentPort,
11
+ token: cmdOpts.token ?? parentToken,
12
+ };
13
+ }
14
+ function resolveRpcOptions(cmdOpts, command) {
15
+ const parentToken = inheritOptionFromParent(command, "token");
16
+ const parentPassword = inheritOptionFromParent(command, "password");
17
+ return {
18
+ ...cmdOpts,
19
+ token: cmdOpts.token ?? parentToken,
20
+ password: cmdOpts.password ?? parentPassword,
21
+ };
22
+ }
2
23
  export function addGatewayServiceCommands(parent, opts) {
3
24
  parent
4
25
  .command("status")
@@ -10,9 +31,9 @@ export function addGatewayServiceCommands(parent, opts) {
10
31
  .option("--no-probe", "Skip RPC probe")
11
32
  .option("--deep", "Scan system-level services", false)
12
33
  .option("--json", "Output JSON", false)
13
- .action(async (cmdOpts) => {
34
+ .action(async (cmdOpts, command) => {
14
35
  await runDaemonStatus({
15
- rpc: cmdOpts,
36
+ rpc: resolveRpcOptions(cmdOpts, command),
16
37
  probe: Boolean(cmdOpts.probe),
17
38
  deep: Boolean(cmdOpts.deep),
18
39
  json: Boolean(cmdOpts.json),
@@ -26,8 +47,8 @@ export function addGatewayServiceCommands(parent, opts) {
26
47
  .option("--token <token>", "Gateway token (token auth)")
27
48
  .option("--force", "Reinstall/overwrite if already installed", false)
28
49
  .option("--json", "Output JSON", false)
29
- .action(async (cmdOpts) => {
30
- await runDaemonInstall(cmdOpts);
50
+ .action(async (cmdOpts, command) => {
51
+ await runDaemonInstall(resolveInstallOptions(cmdOpts, command));
31
52
  });
32
53
  parent
33
54
  .command("uninstall")
@@ -1,2 +1,3 @@
1
1
  export { registerDaemonCli } from "./daemon-cli/register.js";
2
+ export { addGatewayServiceCommands } from "./daemon-cli/register-service-commands.js";
2
3
  export { runDaemonInstall, runDaemonRestart, runDaemonStart, runDaemonStatus, runDaemonStop, runDaemonUninstall, } from "./daemon-cli/runners.js";
@@ -1,22 +1,10 @@
1
1
  import { callGateway } from "../gateway/call.js";
2
- import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
2
+ import { formatTimeAgo } from "../infra/format-time/format-relative.js";
3
3
  import { defaultRuntime } from "../runtime.js";
4
4
  import { renderTable } from "../terminal/table.js";
5
5
  import { theme } from "../terminal/theme.js";
6
+ import { GATEWAY_CLIENT_MODES, GATEWAY_CLIENT_NAMES } from "../utils/message-channel.js";
6
7
  import { withProgress } from "./progress.js";
7
- function formatAge(msAgo) {
8
- const s = Math.max(0, Math.floor(msAgo / 1000));
9
- if (s < 60)
10
- return `${s}s`;
11
- const m = Math.floor(s / 60);
12
- if (m < 60)
13
- return `${m}m`;
14
- const h = Math.floor(m / 60);
15
- if (h < 24)
16
- return `${h}h`;
17
- const d = Math.floor(h / 24);
18
- return `${d}d`;
19
- }
20
8
  const devicesCallOpts = (cmd, defaults) => cmd
21
9
  .option("--url <url>", "Gateway WebSocket URL (defaults to gateway.remote.url when configured)")
22
10
  .option("--token <token>", "Gateway token (if required)")
@@ -44,12 +32,23 @@ function parseDevicePairingList(value) {
44
32
  paired: Array.isArray(obj.paired) ? obj.paired : [],
45
33
  };
46
34
  }
35
+ function selectLatestPendingRequest(pending) {
36
+ if (!pending?.length) {
37
+ return null;
38
+ }
39
+ return pending.reduce((latest, current) => {
40
+ const latestTs = typeof latest.ts === "number" ? latest.ts : 0;
41
+ const currentTs = typeof current.ts === "number" ? current.ts : 0;
42
+ return currentTs > latestTs ? current : latest;
43
+ });
44
+ }
47
45
  function formatTokenSummary(tokens) {
48
- if (!tokens || tokens.length === 0)
46
+ if (!tokens || tokens.length === 0) {
49
47
  return "none";
48
+ }
50
49
  const parts = tokens
51
50
  .map((t) => `${t.role}${t.revokedAtMs ? " (revoked)" : ""}`)
52
- .sort((a, b) => a.localeCompare(b));
51
+ .toSorted((a, b) => a.localeCompare(b));
53
52
  return parts.join(", ");
54
53
  }
55
54
  export function registerDevicesCli(program) {
@@ -82,7 +81,7 @@ export function registerDevicesCli(program) {
82
81
  Device: req.displayName || req.deviceId,
83
82
  Role: req.role ?? "",
84
83
  IP: req.remoteIp ?? "",
85
- Age: typeof req.ts === "number" ? `${formatAge(Date.now() - req.ts)} ago` : "",
84
+ Age: typeof req.ts === "number" ? formatTimeAgo(Date.now() - req.ts) : "",
86
85
  Flags: req.isRepair ? "repair" : "",
87
86
  })),
88
87
  }).trimEnd());
@@ -115,15 +114,29 @@ export function registerDevicesCli(program) {
115
114
  devicesCallOpts(devices
116
115
  .command("approve")
117
116
  .description("Approve a pending device pairing request")
118
- .argument("<requestId>", "Pending request id")
117
+ .argument("[requestId]", "Pending request id")
118
+ .option("--latest", "Approve the most recent pending request", false)
119
119
  .action(async (requestId, opts) => {
120
- const result = await callGatewayCli("device.pair.approve", opts, { requestId });
120
+ let resolvedRequestId = requestId?.trim();
121
+ if (!resolvedRequestId || opts.latest) {
122
+ const listResult = await callGatewayCli("device.pair.list", opts, {});
123
+ const latest = selectLatestPendingRequest(parseDevicePairingList(listResult).pending);
124
+ resolvedRequestId = latest?.requestId?.trim();
125
+ }
126
+ if (!resolvedRequestId) {
127
+ defaultRuntime.error("No pending device pairing requests to approve");
128
+ defaultRuntime.exit(1);
129
+ return;
130
+ }
131
+ const result = await callGatewayCli("device.pair.approve", opts, {
132
+ requestId: resolvedRequestId,
133
+ });
121
134
  if (opts.json) {
122
135
  defaultRuntime.log(JSON.stringify(result, null, 2));
123
136
  return;
124
137
  }
125
138
  const deviceId = result?.device?.deviceId;
126
- defaultRuntime.log(`${theme.success("Approved")} ${theme.command(deviceId ?? "ok")}`);
139
+ defaultRuntime.log(`${theme.success("Approved")} ${theme.command(deviceId ?? "ok")} ${theme.muted(`(${resolvedRequestId})`)}`);
127
140
  }));
128
141
  devicesCallOpts(devices
129
142
  .command("reject")