@phnx-labs/agents-cli 1.22.117 → 1.22.119

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 (271) hide show
  1. package/CHANGELOG.md +301 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/accounts.js +7 -4
  4. package/dist/commands/browser.d.ts +52 -70
  5. package/dist/commands/browser.js +192 -3562
  6. package/dist/commands/daemon.d.ts +39 -2
  7. package/dist/commands/daemon.js +72 -73
  8. package/dist/commands/events.js +1 -1
  9. package/dist/commands/exec.js +53 -1
  10. package/dist/commands/projects.js +92 -7
  11. package/dist/commands/repo.js +13 -49
  12. package/dist/commands/route.js +5 -5
  13. package/dist/commands/routines.js +39 -12
  14. package/dist/commands/sessions-render.d.ts +1 -1
  15. package/dist/commands/sessions-render.js +0 -1
  16. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  17. package/dist/commands/sessions.test-fixture.js +0 -37
  18. package/dist/commands/setup-browser.d.ts +17 -12
  19. package/dist/commands/setup-browser.js +52 -87
  20. package/dist/commands/setup-preferences.d.ts +0 -17
  21. package/dist/commands/setup-preferences.js +18 -49
  22. package/dist/commands/setup.js +9 -9
  23. package/dist/commands/ssh.js +10 -1
  24. package/dist/commands/sync.js +6 -44
  25. package/dist/commands/teams.js +0 -1
  26. package/dist/commands/update.d.ts +19 -0
  27. package/dist/commands/update.js +34 -6
  28. package/dist/commands/view.d.ts +12 -1
  29. package/dist/commands/view.js +47 -2
  30. package/dist/index.js +107 -26
  31. package/dist/lib/account-capabilities.js +0 -2
  32. package/dist/lib/account-catalog.d.ts +38 -0
  33. package/dist/lib/account-catalog.js +110 -26
  34. package/dist/lib/account-provider-registry.js +1 -1
  35. package/dist/lib/account-registry.d.ts +1 -1
  36. package/dist/lib/account-registry.js +36 -2
  37. package/dist/lib/accounting/rotate.js +22 -8
  38. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  39. package/dist/lib/accounting/usage-ingest.js +115 -45
  40. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  41. package/dist/lib/accounting/usage-sync.js +227 -66
  42. package/dist/lib/accounting/usage.d.ts +16 -3
  43. package/dist/lib/accounting/usage.js +26 -8
  44. package/dist/lib/accounts/add.d.ts +3 -1
  45. package/dist/lib/accounts/add.js +24 -13
  46. package/dist/lib/accounts/slots.d.ts +8 -0
  47. package/dist/lib/accounts/slots.js +17 -0
  48. package/dist/lib/add-dir.js +0 -1
  49. package/dist/lib/agent-cli-commands.js +0 -1
  50. package/dist/lib/agent-spec/agents.d.ts +6 -7
  51. package/dist/lib/agent-spec/agents.js +7 -60
  52. package/dist/lib/auth-health.d.ts +69 -1
  53. package/dist/lib/auth-health.js +133 -4
  54. package/dist/lib/browser/context.d.ts +82 -0
  55. package/dist/lib/browser/context.js +88 -0
  56. package/dist/lib/browser/paths.d.ts +25 -0
  57. package/dist/lib/browser/paths.js +51 -0
  58. package/dist/lib/browser/record.d.ts +38 -0
  59. package/dist/lib/browser/record.js +57 -0
  60. package/dist/lib/browser/sessions-list.js +1 -2
  61. package/dist/lib/browser-client.d.ts +158 -0
  62. package/dist/lib/browser-client.js +234 -0
  63. package/dist/lib/channels/owner-sink.d.ts +1 -18
  64. package/dist/lib/channels/owner-sink.js +29 -82
  65. package/dist/lib/channels/providers/rush.d.ts +7 -7
  66. package/dist/lib/channels/providers/rush.js +127 -57
  67. package/dist/lib/claude-account-token.d.ts +7 -1
  68. package/dist/lib/claude-account-token.js +26 -4
  69. package/dist/lib/claude-statusline.d.ts +1 -1
  70. package/dist/lib/claude-statusline.js +15 -3
  71. package/dist/lib/commands.js +1 -1
  72. package/dist/lib/crabbox/lease.js +1 -1
  73. package/dist/lib/crabbox/runtimes.js +1 -3
  74. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  75. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  76. package/dist/lib/daemon/auth-sync-service.js +37 -43
  77. package/dist/lib/daemon/daemon.d.ts +25 -14
  78. package/dist/lib/daemon/daemon.js +92 -154
  79. package/dist/lib/daemon/runner.d.ts +2 -2
  80. package/dist/lib/daemon/runner.js +32 -9
  81. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  82. package/dist/lib/daemon/self-update-service.js +40 -36
  83. package/dist/lib/daemon/service.d.ts +12 -8
  84. package/dist/lib/daemon/supervisor.d.ts +65 -54
  85. package/dist/lib/daemon/supervisor.js +136 -171
  86. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  87. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  88. package/dist/lib/daemon/usage-sync-service.js +46 -78
  89. package/dist/lib/daemon-health.d.ts +22 -5
  90. package/dist/lib/daemon-health.js +47 -3
  91. package/dist/lib/daemon-services.d.ts +1 -1
  92. package/dist/lib/daemon-services.js +0 -10
  93. package/dist/lib/daemon-ticks.js +4 -2
  94. package/dist/lib/devices/doctor-findings.js +8 -11
  95. package/dist/lib/devices/harness-inventory.js +14 -5
  96. package/dist/lib/exec-account-home.js +5 -0
  97. package/dist/lib/exec.js +10 -13
  98. package/dist/lib/feed/events.js +0 -1
  99. package/dist/lib/feed/tool-activity.js +5 -2
  100. package/dist/lib/fleet-shared-state.d.ts +45 -0
  101. package/dist/lib/fleet-shared-state.js +78 -9
  102. package/dist/lib/fs-atomic.js +25 -0
  103. package/dist/lib/github/pr-mergeable.d.ts +4 -2
  104. package/dist/lib/github/pr-mergeable.js +5 -3
  105. package/dist/lib/github/project-prs.d.ts +58 -3
  106. package/dist/lib/github/project-prs.js +205 -7
  107. package/dist/lib/harness/adapters/cursor.js +14 -4
  108. package/dist/lib/harness-auth-capabilities.d.ts +4 -3
  109. package/dist/lib/harness-auth-capabilities.js +1 -2
  110. package/dist/lib/helper-versions.js +1 -1
  111. package/dist/lib/hooks/cache.d.ts +7 -0
  112. package/dist/lib/hooks/cache.js +27 -2
  113. package/dist/lib/hooks/install.js +7 -1
  114. package/dist/lib/hosts/ready.d.ts +38 -8
  115. package/dist/lib/hosts/ready.js +62 -14
  116. package/dist/lib/installations/active-check.d.ts +27 -0
  117. package/dist/lib/installations/active-check.js +61 -0
  118. package/dist/lib/installations/migrate.js +6 -3
  119. package/dist/lib/installations/shims.d.ts +1 -1
  120. package/dist/lib/installations/shims.js +32 -12
  121. package/dist/lib/installations/store.d.ts +9 -0
  122. package/dist/lib/installations/store.js +24 -0
  123. package/dist/lib/installations/update.js +21 -15
  124. package/dist/lib/installations/versions.js +15 -3
  125. package/dist/lib/manifest.js +1 -1
  126. package/dist/lib/models.js +0 -2
  127. package/dist/lib/open-url.js +28 -54
  128. package/dist/lib/profiles.js +0 -2
  129. package/dist/lib/projects.d.ts +16 -0
  130. package/dist/lib/projects.js +31 -0
  131. package/dist/lib/refresh.js +1 -1
  132. package/dist/lib/routine-readiness.d.ts +35 -0
  133. package/dist/lib/routine-readiness.js +74 -16
  134. package/dist/lib/rules/compile.js +6 -0
  135. package/dist/lib/sandbox.js +0 -2
  136. package/dist/lib/secrets-policy.d.ts +89 -11
  137. package/dist/lib/secrets-policy.js +175 -28
  138. package/dist/lib/session/active.d.ts +17 -6
  139. package/dist/lib/session/active.js +105 -49
  140. package/dist/lib/session/db.d.ts +1 -1
  141. package/dist/lib/session/db.js +65 -18
  142. package/dist/lib/session/discover.js +1 -283
  143. package/dist/lib/session/mirror.d.ts +4 -3
  144. package/dist/lib/session/mirror.js +4 -3
  145. package/dist/lib/session/parse.d.ts +0 -2
  146. package/dist/lib/session/parse.js +1 -138
  147. package/dist/lib/session/pid-registry.d.ts +6 -3
  148. package/dist/lib/session/pid-registry.js +18 -8
  149. package/dist/lib/session/remote/watch.d.ts +1 -1
  150. package/dist/lib/session/remote/watch.js +7 -3
  151. package/dist/lib/session/throughput.d.ts +1 -3
  152. package/dist/lib/session/throughput.js +0 -20
  153. package/dist/lib/session/types.d.ts +2 -2
  154. package/dist/lib/session/types.js +2 -2
  155. package/dist/lib/sessions-client.d.ts +60 -1
  156. package/dist/lib/sessions-client.js +227 -4
  157. package/dist/lib/setup-tool-status.js +5 -10
  158. package/dist/lib/signin-badge.d.ts +3 -2
  159. package/dist/lib/signin-badge.js +6 -3
  160. package/dist/lib/smart-launch.js +4 -1
  161. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  162. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  163. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  164. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  165. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  166. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  167. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  168. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  169. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  170. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  171. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  172. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  173. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  174. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  175. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  176. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  177. package/dist/lib/staleness/writers/sources.js +5 -0
  178. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  179. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  180. package/dist/lib/state.d.ts +6 -4
  181. package/dist/lib/state.js +7 -17
  182. package/dist/lib/teams/parsers.d.ts +1 -1
  183. package/dist/lib/teams/parsers.js +1 -136
  184. package/dist/lib/teams/placement-probe.js +50 -15
  185. package/dist/lib/teams/scheduler.d.ts +7 -0
  186. package/dist/lib/types.d.ts +16 -1
  187. package/dist/lib/types.js +1 -1
  188. package/dist/lib/view-types.d.ts +32 -0
  189. package/dist/lib/watchdog/read.js +0 -1
  190. package/dist/lib/watchdog/rotate.js +1 -2
  191. package/dist/lib/watchdog/runner.js +2 -2
  192. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  193. package/dist/lib/watchdog/watchdogTail.js +0 -31
  194. package/package.json +1 -1
  195. package/scripts/postinstall.js +8 -8
  196. package/dist/browser.d.ts +0 -2
  197. package/dist/browser.js +0 -17
  198. package/dist/commands/browser-picker.d.ts +0 -18
  199. package/dist/commands/browser-picker.js +0 -97
  200. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  201. package/dist/lib/browser/arc-discovery.js +0 -197
  202. package/dist/lib/browser/arc-dom.d.ts +0 -14
  203. package/dist/lib/browser/arc-dom.js +0 -121
  204. package/dist/lib/browser/caller-identity.d.ts +0 -40
  205. package/dist/lib/browser/caller-identity.js +0 -175
  206. package/dist/lib/browser/cdp.d.ts +0 -51
  207. package/dist/lib/browser/cdp.js +0 -254
  208. package/dist/lib/browser/chrome.d.ts +0 -141
  209. package/dist/lib/browser/chrome.js +0 -704
  210. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  211. package/dist/lib/browser/chromium-discovery.js +0 -96
  212. package/dist/lib/browser/devices.d.ts +0 -34
  213. package/dist/lib/browser/devices.js +0 -61
  214. package/dist/lib/browser/domain-skills.d.ts +0 -71
  215. package/dist/lib/browser/domain-skills.js +0 -195
  216. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  217. package/dist/lib/browser/drivers/arc.js +0 -276
  218. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  219. package/dist/lib/browser/drivers/firefox.js +0 -377
  220. package/dist/lib/browser/drivers/local.d.ts +0 -55
  221. package/dist/lib/browser/drivers/local.js +0 -266
  222. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  223. package/dist/lib/browser/drivers/ssh.js +0 -467
  224. package/dist/lib/browser/editor.d.ts +0 -3
  225. package/dist/lib/browser/editor.js +0 -50
  226. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  227. package/dist/lib/browser/ffmpeg.js +0 -184
  228. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  229. package/dist/lib/browser/firefox-discovery.js +0 -162
  230. package/dist/lib/browser/har.d.ts +0 -85
  231. package/dist/lib/browser/har.js +0 -77
  232. package/dist/lib/browser/hygiene.d.ts +0 -97
  233. package/dist/lib/browser/hygiene.js +0 -153
  234. package/dist/lib/browser/index.d.ts +0 -5
  235. package/dist/lib/browser/index.js +0 -5
  236. package/dist/lib/browser/input.d.ts +0 -7
  237. package/dist/lib/browser/input.js +0 -92
  238. package/dist/lib/browser/ipc.d.ts +0 -204
  239. package/dist/lib/browser/ipc.js +0 -1453
  240. package/dist/lib/browser/login-detection.d.ts +0 -87
  241. package/dist/lib/browser/login-detection.js +0 -274
  242. package/dist/lib/browser/profiles.d.ts +0 -422
  243. package/dist/lib/browser/profiles.js +0 -1157
  244. package/dist/lib/browser/refs.d.ts +0 -89
  245. package/dist/lib/browser/refs.js +0 -191
  246. package/dist/lib/browser/registry.d.ts +0 -70
  247. package/dist/lib/browser/registry.js +0 -252
  248. package/dist/lib/browser/remote-control.d.ts +0 -59
  249. package/dist/lib/browser/remote-control.js +0 -85
  250. package/dist/lib/browser/resolve-target.d.ts +0 -66
  251. package/dist/lib/browser/resolve-target.js +0 -264
  252. package/dist/lib/browser/runtime-state.d.ts +0 -291
  253. package/dist/lib/browser/runtime-state.js +0 -584
  254. package/dist/lib/browser/secret-ref.d.ts +0 -10
  255. package/dist/lib/browser/secret-ref.js +0 -14
  256. package/dist/lib/browser/service.d.ts +0 -812
  257. package/dist/lib/browser/service.js +0 -4693
  258. package/dist/lib/browser/stream.d.ts +0 -17
  259. package/dist/lib/browser/stream.js +0 -72
  260. package/dist/lib/browser/task-index.d.ts +0 -78
  261. package/dist/lib/browser/task-index.js +0 -199
  262. package/dist/lib/browser/types.d.ts +0 -647
  263. package/dist/lib/browser/types.js +0 -61
  264. package/dist/lib/browser/upload.d.ts +0 -29
  265. package/dist/lib/browser/upload.js +0 -298
  266. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  267. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  268. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  269. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  270. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  271. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -2,99 +2,46 @@
2
2
  * Owner-delivery-sink reachability probe (RUSH-2262).
3
3
  *
4
4
  * The feed/notify owner-delivery lane (`agents send --to owner`, `agents feed post
5
- * --level important` / `--blocked`) reaches the owner over the rush-backed owner
6
- * channel (iMessage via `rush message send`). That transport can only deliver
7
- * from a context that BOTH finds the `rush` CLI on PATH and can read its
8
- * keychain-bound session. So a headless Linux fleet box (no rush) or a non-GUI
9
- * SSH session on a mac (login keychain locked) structurally cannot escalate —
10
- * and the failure is silent until a block is filed, surfacing only as the
11
- * after-the-fact `owner failed: …` line. `agents doctor` had no signal for it,
12
- * which is exactly the gap RUSH-2258 / RUSH-2262 flagged.
5
+ * --level important` / `--blocked`) reaches the owner through the channel
6
+ * providers registered for each configured channel in humans.yaml.
13
7
  *
14
- * At runtime the owner-delivery lane no longer strands this failure: when local
15
- * delivery fails on a box that structurally cannot reach the owner,
16
- * `forwardOwnerNotifyToPeer` (owner-forward.ts) hands it to a capable macOS peer
17
- * over SSH (PHNX-3303). This finding still stands as a diagnostic — the forward
18
- * is best-effort and only lands when a reachable mac peer exists, so a box that
19
- * cannot deliver locally is worth surfacing regardless.
8
+ * Transport capabilities differ by channel and platform:
9
+ * - **imessage:** macOS only (osascript → Messages.app). Linux boxes rely on
10
+ * the peer-forward in owner-forward.ts to reach a macOS peer.
11
+ * - **slack:** requires a `SLACK_BOT_TOKEN` in env or the `webhooks` secrets
12
+ * bundle. Platform-independent.
13
+ * - **telegram / discord:** requires the Rush daemon (removed); only
14
+ * openclaw-telegram is available as a Telegram alternative.
20
15
  *
21
- * This probes the SAME transport the lane uses, from the SAME context doctor runs
22
- * in, so `agents doctor` can fail loud when this box cannot reach the owner. It is
23
- * deliberately honest about context: `rush whoami` is what tells a real signed-in
24
- * session apart from a keychain that is present but unreadable HERE. The session
25
- * token is a keychain item, NOT `~/.rush/user.yaml` (RUSH-2262), so this never
26
- * reads that file — checking it is the mistake that made a signed-in box look
27
- * signed out.
28
- *
29
- * `agents send --to owner --dry-run` is NOT this probe: it short-circuits before the
30
- * `which rush` preflight (`providers/rush.ts`), so it reports `ok:true` on a box
31
- * with no rush at all. Resolvability (does the envelope build?) and reachability
32
- * (can this box actually deliver?) are different questions; this answers the
33
- * second.
16
+ * `agents send --to owner --dry-run` is NOT this probe: dry-run short-circuits
17
+ * before the capability check. Resolvability (does the envelope build?) and
18
+ * reachability (can this box actually deliver?) are different questions; this
19
+ * answers the second.
34
20
  */
35
- import { execFile } from 'child_process';
36
- import { promisify } from 'util';
21
+ import { platform } from 'os';
37
22
  import { readOwnerDest } from './send.js';
38
- import { RUSH_CHANNELS } from './providers/rush.js';
39
- const execFileAsync = promisify(execFile);
40
- async function rushOnPath() {
41
- try {
42
- await execFileAsync('which', ['rush'], { timeout: 4000 });
43
- return true;
44
- }
45
- catch {
46
- return false;
47
- }
48
- }
49
- /** Signed-in state from `rush whoami`, read with THIS context's keychain access.
50
- * 'unknown' (a timeout or output we can't classify) is treated as reachable by
51
- * the caller — a slow or unexpected probe must not cry wolf and paint a false
52
- * critical. Only a definitive "not logged in" is reported as signed out. */
53
- async function rushSignedIn() {
54
- const classify = (s) => {
55
- const out = s.toLowerCase();
56
- if (/not (logged in|signed in)/.test(out))
57
- return 'no';
58
- if (/logged in as|session:\s*valid/.test(out))
59
- return 'yes';
60
- return 'unknown';
61
- };
62
- try {
63
- const { stdout, stderr } = await execFileAsync('rush', ['whoami'], { timeout: 5000 });
64
- return classify(`${stdout}\n${stderr}`);
65
- }
66
- catch (err) {
67
- const e = err;
68
- if (e.killed)
69
- return 'unknown'; // timed out — do not conclude signed-out
70
- // A signed-out rush commonly exits non-zero; trust an explicit message only.
71
- return classify(`${e.stdout ?? ''}\n${e.stderr ?? ''}`);
72
- }
73
- }
74
- /**
75
- * Probe whether THIS box can deliver an owner notification right now. Returns
76
- * `configured:false` (and the caller emits no finding) when the fleet has no
77
- * owner channel configured — an un-opted-in box is not broken. When configured,
78
- * reports whether the resolved transport can actually deliver from here.
79
- *
80
- * Only the concrete rush-backed failure the lane hits is reported as unreachable;
81
- * non-rush transports (desktop / mailbox / …) deliver locally and are treated as
82
- * reachable rather than probed, so this never invents a critical it cannot back.
83
- */
23
+ import { RUSH_CHANNELS, resolveSlackToken } from './providers/rush.js';
84
24
  export async function probeOwnerSink(meta) {
85
25
  const dest = readOwnerDest(meta);
86
26
  if (!dest)
87
27
  return { configured: false, reachable: false };
88
28
  const channel = dest.channel;
89
29
  const transport = meta.notify?.transports?.[channel] ?? channel;
90
- if (RUSH_CHANNELS.includes(transport)) {
91
- if (!(await rushOnPath())) {
92
- return { configured: true, reachable: false, channel, transport, reason: 'rush-not-on-path' };
93
- }
94
- if ((await rushSignedIn()) === 'no') {
95
- return { configured: true, reachable: false, channel, transport, reason: 'rush-signed-out' };
30
+ if (!RUSH_CHANNELS.includes(transport)) {
31
+ return { configured: true, reachable: true, channel, transport };
32
+ }
33
+ if (transport === 'imessage') {
34
+ if (platform() !== 'darwin') {
35
+ return { configured: true, reachable: false, channel, transport, reason: 'imessage-not-macos' };
96
36
  }
97
37
  return { configured: true, reachable: true, channel, transport };
98
38
  }
99
- return { configured: true, reachable: true, channel, transport };
39
+ if (transport === 'slack') {
40
+ if (resolveSlackToken()) {
41
+ return { configured: true, reachable: true, channel, transport };
42
+ }
43
+ return { configured: true, reachable: false, channel, transport, reason: 'slack-no-token' };
44
+ }
45
+ // telegram / discord — daemon removed, no direct transport.
46
+ return { configured: true, reachable: false, channel, transport, reason: 'channel-unsupported' };
100
47
  }
@@ -1,9 +1,9 @@
1
- import type { ChannelProvider, SendOptions } from '../registry.js';
2
- type RushChannel = 'telegram' | 'imessage' | 'slack' | 'discord';
1
+ import type { ChannelProvider } from '../registry.js';
2
+ export type RushChannel = 'telegram' | 'imessage' | 'slack' | 'discord';
3
3
  export declare const RUSH_CHANNELS: RushChannel[];
4
- /** Build the `rush send` argv (exported for tests). */
5
- export declare function buildRushSendArgs(channel: RushChannel, text: string, opts: SendOptions): string[];
6
- /** Build the owner-scoped iMessage argv. */
7
- export declare function buildRushOwnerMessageArgs(text: string): string[];
4
+ /** Build the osascript argv for sending an iMessage. */
5
+ export declare function buildImessageOsascriptArgs(text: string, phone: string): string[];
6
+ export declare function resolveSlackToken(): string | undefined;
7
+ /** Build the Slack chat.postMessage payload (exported for tests). */
8
+ export declare function buildSlackPayload(channel: string, text: string, thread?: string): Record<string, string>;
8
9
  export declare const rushProviders: ChannelProvider[];
9
- export {};
@@ -1,70 +1,140 @@
1
1
  /**
2
- * Rush channel providers — telegram / imessage / slack / discord.
2
+ * Channel providers formerly backed by the Rush daemon's live gateways.
3
3
  *
4
- * Addressable channels use `rush send`, which routes through the daemon's live
5
- * gateways. Owner-scoped iMessage uses `rush message send`; it is backed by the
6
- * verified Rush owner account and does not require a daemon channel registration.
4
+ * The desktop-era `rush message send` (iMessage) and `rush send --channel`
5
+ * (Slack/Telegram/Discord) commands were removed with the daemon in the
6
+ * thin-client trim (PHNX-3839/3933). This module now delivers directly:
7
+ *
8
+ * - **imessage:** `osascript` → Messages.app (macOS only; Linux boxes
9
+ * rely on the peer-forward in owner-forward.ts to reach a macOS peer).
10
+ * - **slack:** Slack Web API `chat.postMessage` via `fetch()`, reading
11
+ * `SLACK_BOT_TOKEN` from env or the `webhooks` secrets bundle.
12
+ * - **telegram / discord:** error — no direct transport; openclaw-telegram
13
+ * is available as a separate provider for Telegram.
7
14
  */
8
15
  import { execFile } from 'child_process';
16
+ import { platform } from 'os';
9
17
  import { promisify } from 'util';
10
18
  const execFileAsync = promisify(execFile);
11
19
  export const RUSH_CHANNELS = ['telegram', 'imessage', 'slack', 'discord'];
12
- /** Build the `rush send` argv (exported for tests). */
13
- export function buildRushSendArgs(channel, text, opts) {
14
- const args = ['send', text, '--channel', channel, '--id', opts.target, '--json'];
15
- if (opts.thread)
16
- args.push('--thread', opts.thread);
17
- for (const a of opts.attachments ?? [])
18
- args.push('--attachment', a);
19
- return args;
20
+ // ── iMessage via osascript ──────────────────────────────────────────────
21
+ function escapeAppleScript(s) {
22
+ return s.replace(/\\/g, '\\\\').replace(/"/g, '\\"');
23
+ }
24
+ /** Build the osascript argv for sending an iMessage. */
25
+ export function buildImessageOsascriptArgs(text, phone) {
26
+ const escapedText = escapeAppleScript(text);
27
+ const escapedPhone = escapeAppleScript(phone);
28
+ const script = [
29
+ 'tell application "Messages"',
30
+ ` set targetBuddy to buddy "${escapedPhone}" of (first account whose service type is iMessage)`,
31
+ ` send "${escapedText}" to targetBuddy`,
32
+ 'end tell',
33
+ ].join('\n');
34
+ return ['-e', script];
35
+ }
36
+ async function sendImessage(text, opts) {
37
+ const channel = 'imessage';
38
+ if (opts.dryRun)
39
+ return { ok: true, channel, id: opts.target };
40
+ if (platform() !== 'darwin') {
41
+ return { ok: false, channel, id: opts.target, error: 'iMessage requires macOS (peer-forward handles Linux delivery)' };
42
+ }
43
+ if ((opts.attachments?.length ?? 0) > 0) {
44
+ return { ok: false, channel, id: opts.target, error: 'iMessage attachments not supported via osascript' };
45
+ }
46
+ try {
47
+ await execFileAsync('osascript', buildImessageOsascriptArgs(text, opts.target), { timeout: 10_000 });
48
+ return { ok: true, channel, id: opts.target };
49
+ }
50
+ catch (err) {
51
+ return { ok: false, channel, id: opts.target, error: err.message };
52
+ }
53
+ }
54
+ // ── Slack via Web API ───────────────────────────────────────────────────
55
+ export function resolveSlackToken() {
56
+ // 1. Environment variable (set by `secrets exec` or the daemon).
57
+ if (process.env.SLACK_BOT_TOKEN) {
58
+ return process.env.SLACK_BOT_TOKEN;
59
+ }
60
+ // 2. Secrets bundle — the webhook receiver bundle carries SLACK_BOT_TOKEN
61
+ // when a Slack app is configured for this fleet.
62
+ try {
63
+ // Dynamic import to avoid a hard dependency on the secrets subsystem in
64
+ // contexts where it is unavailable (CI, containers without a secrets store).
65
+ // eslint-disable-next-line @typescript-eslint/no-require-imports
66
+ const { readAndResolveBundleEnvSync } = require('../../secrets-client.js');
67
+ const { env } = readAndResolveBundleEnvSync('webhooks', { caller: 'slack-provider', agentOnly: true });
68
+ if (env.SLACK_BOT_TOKEN) {
69
+ return env.SLACK_BOT_TOKEN;
70
+ }
71
+ }
72
+ catch {
73
+ // Bundle missing, store locked, or secrets CLI not available — not fatal.
74
+ }
75
+ return undefined;
76
+ }
77
+ /** Build the Slack chat.postMessage payload (exported for tests). */
78
+ export function buildSlackPayload(channel, text, thread) {
79
+ const payload = { channel, text };
80
+ if (thread)
81
+ payload.thread_ts = thread;
82
+ return payload;
20
83
  }
21
- /** Build the owner-scoped iMessage argv. */
22
- export function buildRushOwnerMessageArgs(text) {
23
- return ['message', 'send', '--text', text];
84
+ async function sendSlack(text, opts) {
85
+ const channel = 'slack';
86
+ if (opts.dryRun)
87
+ return { ok: true, channel, id: opts.target };
88
+ const token = resolveSlackToken();
89
+ if (!token) {
90
+ return {
91
+ ok: false,
92
+ channel,
93
+ id: opts.target,
94
+ error: 'No SLACK_BOT_TOKEN — set it in env or the webhooks secrets bundle',
95
+ };
96
+ }
97
+ try {
98
+ const payload = buildSlackPayload(opts.target, text, opts.thread);
99
+ const res = await fetch('https://slack.com/api/chat.postMessage', {
100
+ method: 'POST',
101
+ headers: {
102
+ 'Authorization': `Bearer ${token}`,
103
+ 'Content-Type': 'application/json; charset=utf-8',
104
+ },
105
+ body: JSON.stringify(payload),
106
+ });
107
+ const body = await res.json();
108
+ if (!body.ok) {
109
+ return { ok: false, channel, id: opts.target, error: `Slack API: ${body.error ?? 'unknown error'}` };
110
+ }
111
+ return { ok: true, channel, id: opts.target };
112
+ }
113
+ catch (err) {
114
+ return { ok: false, channel, id: opts.target, error: err.message };
115
+ }
24
116
  }
25
- function rushProvider(channel) {
117
+ // ── Unsupported (daemon-era only) ───────────────────────────────────────
118
+ function unsupportedProvider(name) {
26
119
  return {
27
- name: channel,
28
- async send(text, opts) {
29
- if (opts.dryRun) {
30
- return { ok: true, channel, id: opts.target, attachments: opts.attachments };
31
- }
32
- // Preflight: rush must be on PATH (mirrors notify.ts's openclaw check).
33
- try {
34
- await execFileAsync('which', ['rush']);
35
- }
36
- catch {
37
- return { ok: false, channel, id: opts.target, error: 'rush CLI not found on PATH' };
38
- }
39
- try {
40
- if (channel === 'imessage' && opts.ownerScoped) {
41
- if ((opts.attachments?.length ?? 0) > 0) {
42
- return {
43
- ok: false,
44
- channel,
45
- id: opts.target,
46
- error: 'owner-scoped iMessage does not support attachments',
47
- };
48
- }
49
- await execFileAsync('rush', buildRushOwnerMessageArgs(text));
50
- return { ok: true, channel, id: opts.target };
51
- }
52
- const { stdout } = await execFileAsync('rush', buildRushSendArgs(channel, text, opts));
53
- const parsed = JSON.parse(stdout);
54
- return {
55
- ok: parsed.ok === true,
56
- channel,
57
- id: opts.target,
58
- attachments: parsed.attachments,
59
- error: parsed.ok === true ? undefined : 'rush send returned ok=false',
60
- };
61
- }
62
- catch (err) {
63
- // Daemon down surfaces here as the exec error (ErrDaemonDown), as does a
64
- // non-JSON stdout (parse throw). Fail loud — never silently reroute.
65
- return { ok: false, channel, id: opts.target, error: err.message };
66
- }
120
+ name,
121
+ async send(_text, opts) {
122
+ if (opts.dryRun)
123
+ return { ok: true, channel: name, id: opts.target };
124
+ return {
125
+ ok: false,
126
+ channel: name,
127
+ id: opts.target,
128
+ error: `${name} delivery requires the Rush daemon (removed); use openclaw-telegram for Telegram`,
129
+ };
67
130
  },
68
131
  };
69
132
  }
70
- export const rushProviders = RUSH_CHANNELS.map(rushProvider);
133
+ // ── Provider registry ───────────────────────────────────────────────────
134
+ const PROVIDERS = {
135
+ imessage: { name: 'imessage', send: sendImessage },
136
+ slack: { name: 'slack', send: sendSlack },
137
+ telegram: unsupportedProvider('telegram'),
138
+ discord: unsupportedProvider('discord'),
139
+ };
140
+ export const rushProviders = RUSH_CHANNELS.map((ch) => PROVIDERS[ch]);
@@ -103,5 +103,11 @@ export declare function isClaudeWorkerHomeSeeded(home: string): boolean;
103
103
  * at it, so nothing else can complete Claude Code's first-run onboarding (theme
104
104
  * picker, "Let's get started"); without the flag every slot launch re-onboarded.
105
105
  * Everything else in the document is preserved.
106
+ *
107
+ * Without an email only the onboarding flag is seeded: a provider (setup-token)
108
+ * launch runs in the shared version home, whose identity belongs to whatever
109
+ * native login lives there, so it must not be rewritten. A document already
110
+ * carrying the requested state is left untouched, so a launch never rewrites a
111
+ * config Claude Code is using.
106
112
  */
107
- export declare function seedClaudeWorkerHomeIdentity(versionHome: string, email: string): void;
113
+ export declare function seedClaudeWorkerHomeIdentity(versionHome: string, email?: string): void;
@@ -393,15 +393,34 @@ export function isClaudeWorkerHomeSeeded(home) {
393
393
  * at it, so nothing else can complete Claude Code's first-run onboarding (theme
394
394
  * picker, "Let's get started"); without the flag every slot launch re-onboarded.
395
395
  * Everything else in the document is preserved.
396
+ *
397
+ * Without an email only the onboarding flag is seeded: a provider (setup-token)
398
+ * launch runs in the shared version home, whose identity belongs to whatever
399
+ * native login lives there, so it must not be rewritten. A document already
400
+ * carrying the requested state is left untouched, so a launch never rewrites a
401
+ * config Claude Code is using.
396
402
  */
397
403
  export function seedClaudeWorkerHomeIdentity(versionHome, email) {
398
- const trimmed = email.trim();
399
- if (!trimmed)
400
- return;
404
+ const trimmed = email?.trim() || undefined;
405
+ // A version home usually links `.claude/.claude.json -> ../.claude.json`. The
406
+ // temp-write + rename below would replace that link with a regular file and
407
+ // split one config into two, so write through a link to the file it names.
408
+ const targets = new Set();
401
409
  for (const p of [
402
410
  path.join(versionHome, '.claude', '.claude.json'),
403
411
  path.join(versionHome, '.claude.json'),
404
412
  ]) {
413
+ let target = p;
414
+ try {
415
+ if (fs.lstatSync(p).isSymbolicLink())
416
+ target = path.resolve(path.dirname(p), fs.readlinkSync(p));
417
+ }
418
+ catch {
419
+ // Missing: written fresh below.
420
+ }
421
+ targets.add(target);
422
+ }
423
+ for (const p of targets) {
405
424
  let doc = {};
406
425
  try {
407
426
  doc = JSON.parse(fs.readFileSync(p, 'utf-8'));
@@ -416,7 +435,10 @@ export function seedClaudeWorkerHomeIdentity(versionHome, email) {
416
435
  const existing = (doc.oauthAccount && typeof doc.oauthAccount === 'object'
417
436
  ? doc.oauthAccount
418
437
  : {});
419
- doc.oauthAccount = { ...existing, emailAddress: trimmed };
438
+ if (doc.hasCompletedOnboarding === true && (!trimmed || existing.emailAddress === trimmed))
439
+ continue;
440
+ if (trimmed)
441
+ doc.oauthAccount = { ...existing, emailAddress: trimmed };
420
442
  doc.hasCompletedOnboarding = true;
421
443
  fs.mkdirSync(path.dirname(p), { recursive: true });
422
444
  // Temp-write + rename: a reader mid-write never sees a truncated doc.
@@ -30,7 +30,7 @@ interface ClaudeStatusLinePayload {
30
30
  };
31
31
  };
32
32
  }
33
- export declare function ingestClaudeStatusLineUsage(payload: ClaudeStatusLinePayload, identity: ClaudeHomeIdentity | null): boolean;
33
+ export declare function ingestClaudeStatusLineUsage(payload: ClaudeStatusLinePayload, identity: ClaudeHomeIdentity | null, versionHome?: string | null): boolean;
34
34
  /**
35
35
  * Where the running Claude keeps the config this render attributes to: the version
36
36
  * home when the launch shim set CLAUDE_CONFIG_DIR, otherwise the real HOME — the
@@ -9,6 +9,7 @@ import { mergeClaudeUsageCacheWindows } from './accounting/usage.js';
9
9
  import { loadReminders, pickReminderForSession } from './reminders.js';
10
10
  import { readMeta } from './state.js';
11
11
  import { machineId } from './machine-id.js';
12
+ import { recordRunAuthOutcome } from './auth-health.js';
12
13
  export const CLAUDE_STATUSLINE_COMMAND = 'agents __claude-statusline';
13
14
  const DELEGATE_FILE = path.join('.agents', 'claude-statusline-delegate');
14
15
  // The private subcommand this feature runs. It is only ever invoked internally,
@@ -64,7 +65,7 @@ function windowFromNative(key, value) {
64
65
  windowMinutes: key === 'session' ? 300 : 10_080,
65
66
  };
66
67
  }
67
- export function ingestClaudeStatusLineUsage(payload, identity) {
68
+ export function ingestClaudeStatusLineUsage(payload, identity, versionHome) {
68
69
  if (!identity?.usageKey || !payload.rate_limits)
69
70
  return false;
70
71
  const windows = [
@@ -80,8 +81,19 @@ export function ingestClaudeStatusLineUsage(payload, identity) {
80
81
  windows,
81
82
  freshness: { source: 'statusline', poller: machineId() },
82
83
  });
84
+ // A status-line payload with rate_limits IS a real inference response — the
85
+ // token authenticated moments ago. Record that as the per-account auth FACT
86
+ // (`last used ok`), keyed to the account this box actually ran (PHNX-4116).
87
+ recordRunAuthOutcome({
88
+ agent: 'claude',
89
+ accountId: resolveNativeAccount(identity)?.id ?? null,
90
+ home: versionHome ?? null,
91
+ outcome: { ok: true },
92
+ });
93
+ // Keep this box's own state file current so the next placement probe or
94
+ // usage-sync fan-out carries this reading; the file is local, no transport.
83
95
  void import('./accounting/usage-sync.js')
84
- .then((mod) => mod.pushUsageSnapshotNow())
96
+ .then((mod) => mod.publishUsageSnapshotToSharedStore())
85
97
  .catch(() => { });
86
98
  return true;
87
99
  }
@@ -211,7 +223,7 @@ export async function runClaudeStatusLine() {
211
223
  const versionHome = versionHomeFromEnv(process.env);
212
224
  const identity = readClaudeIdentity(claudeHomeFromEnv(process.env));
213
225
  if (versionHome)
214
- ingestClaudeStatusLineUsage(payload, identity);
226
+ ingestClaudeStatusLineUsage(payload, identity, versionHome);
215
227
  process.stdout.write(renderClaudeStatusLine(payload, undefined, versionHome ? renderDelegate(raw, versionHome) : '', resolveReminderPart(payload.session_id), formatAccountPart(identity, resolveNativeAccount(identity))));
216
228
  return 0;
217
229
  }
@@ -571,7 +571,7 @@ function commandContentMatches(agentId, commandName, sourcePath) {
571
571
  * Get the project-scoped commands directory for an agent.
572
572
  * Claude: .claude/commands/
573
573
  * Codex: .codex/prompts/
574
- * Gemini: .gemini/commands/
574
+ * Cursor: .cursor/commands/
575
575
  */
576
576
  function getProjectCommandsDirs(agentId, cwd = process.cwd()) {
577
577
  const agent = AGENTS[agentId];
@@ -145,7 +145,7 @@ export function buildBootstrapScript(opts) {
145
145
  // Runs regardless of --keep-box (it's in the box body, not teardown), so a kept
146
146
  // box still loses the token after the run — minimizing the credential window.
147
147
  const shredPaths = credentialRuntimes.flatMap((id) => {
148
- const paths = { claude: ['.claude.json', CLAUDE_TOKEN_REMOTE], codex: ['.codex/auth.json'], gemini: ['.gemini/google_accounts.json'], grok: ['.grok/auth.json'] }[id];
148
+ const paths = { claude: ['.claude.json', CLAUDE_TOKEN_REMOTE], codex: ['.codex/auth.json'], grok: ['.grok/auth.json'] }[id];
149
149
  return paths ?? [];
150
150
  });
151
151
  if (opts.dispatchProfile)
@@ -22,13 +22,12 @@ import { listInstalledVersions, getVersionHomePath } from '../installations/vers
22
22
  export const LEASE_RUNTIMES = [
23
23
  { id: 'claude', label: 'Claude Code', localCandidates: ['.claude/.claude.json', '.claude.json'], remote: '.claude.json' },
24
24
  { id: 'codex', label: 'Codex CLI', localCandidates: ['.codex/auth.json'], remote: '.codex/auth.json' },
25
- { id: 'gemini', label: 'Gemini CLI', localCandidates: ['.gemini/google_accounts.json'], remote: '.gemini/google_accounts.json' },
26
25
  { id: 'grok', label: 'Grok CLI', localCandidates: ['.grok/auth.json'], remote: '.grok/auth.json' },
27
26
  ];
28
27
  /**
29
28
  * Every runtime whose login this module can serialize (`LEASE_RUNTIMES`) is a
30
29
  * native, rotating OAuth / session credential (Claude OAuth token, codex/grok
31
- * `auth.json`, gemini `google_accounts.json`). The fleet-auth contract forbids
30
+ * `auth.json`). The fleet-auth contract forbids
32
31
  * copying any of them between devices — including to an ephemeral leased box —
33
32
  * because a shared refresh token rotates server-side on the next refresh and
34
33
  * invalidates every other copy (`docs/specifications.md` SING-1b,
@@ -142,7 +141,6 @@ const PROFILE_AUTH_ENV_KEYS_BY_RUNTIME = {
142
141
  'ANTHROPIC_FOUNDRY_API_KEY',
143
142
  ],
144
143
  codex: ['OPENAI_API_KEY'],
145
- gemini: ['GEMINI_API_KEY', 'GOOGLE_API_KEY'],
146
144
  grok: ['XAI_API_KEY', 'GROK_API_KEY'],
147
145
  };
148
146
  /** True when a profile already carries auth for its host runtime via env. */
@@ -175,6 +175,7 @@ function verdictRank(verdict) {
175
175
  case 'rate_limited': return 5;
176
176
  case 'live': return 4;
177
177
  case 'unverified': return 3;
178
+ case 'no_evidence': return 3;
178
179
  case 'error': return 2;
179
180
  case 'unconfigured': return 1;
180
181
  }
@@ -3,25 +3,28 @@
3
3
  *
4
4
  * This tick owns only the NON-git duties of auth sync: it materializes worker
5
5
  * slots from durable keys already on the box, then (on the elected headed
6
- * publisher) provisions peers whose LAST-DELIVERED verdict says `missing` by
7
- * pushing the credential over SSH. The secret never enters Git.
6
+ * publisher) provisions peers missing a reserved key by pushing the credential
7
+ * over SSH. The secret never enters Git.
8
8
  *
9
- * It no longer runs its own `syncFleetSharedStateRepo` (PHNX-4051). The verdict
10
- * this tick's decisions read is published and delivered by the single git
11
- * committer, the usage-sync tick — folding both publishes into one caller is
12
- * what stops the two ticks (30 s apart) from contending for the one shared-repo
13
- * lock and starving the usage snapshot workers depend on. This tick keeps its
14
- * own deadline and circuit breaker, so a hung peer SSH push parks only auth-sync
15
- * and never the usage delivery. The pushes read the peer verdicts the last
16
- * usage-sync exchange wrote into the local checkout; they are idempotent
17
- * (push only when a peer is missing a key), so acting on at-most-one-tick-old
18
- * data converges exactly as the in-tick exchange did. To keep "at-most-one-tick-
19
- * old" true, the pushes are gated on the exchange's freshness marker
20
- * (`readLastSuccessfulExchangeMs`): when the last usage-sync exchange is missing
21
- * or older than one usage-sync tick interval (`USAGE_SYNC_TICK_MS`, the
22
- * producer's cadence — not this service's own `AUTH_SYNC_TICK_MS`), this tick
23
- * skips the pushes and WARNs instead of acting on peer state that may no longer
24
- * hold.
9
+ * It runs no transport of its own (PHNX-4051): the per-account readiness
10
+ * verdicts it reads are published and delivered by the usage-sync tick's SSH
11
+ * exchange, which stores each peer's reply at `devices/<peer>/daemon-state.json`
12
+ * stamped `receivedAt`. This tick keeps its own deadline and circuit breaker, so
13
+ * a hung peer SSH push parks only auth-sync and never the usage delivery.
14
+ *
15
+ * The pushes read each peer's OWN reply file (PHNX-4116 PR 5): a peer reporting
16
+ * a `missing` verdict for an account is pushed that account's key; a peer that
17
+ * has never sent a reply (no file), or whose reply carries no `accounts.rows`
18
+ * field (an older CLI — fail closed), is skipped this tick and logged at INFO.
19
+ * Presence is also gated on a delivered-fingerprint match, so a re-mint
20
+ * (`accounts login` bumps `workerCredential.mintedAt` while the old token still
21
+ * authenticates) re-pushes the rotated key within a tick instead of reading
22
+ * `present` forever. The push is idempotent, so a stale reply is harmless — a
23
+ * stale "has key" is fine, a stale "missing key" costs one redundant push —
24
+ * which is why the per-peer `receivedAt` replaced the old global freshness gate
25
+ * (`readLastSuccessfulExchangeMs`) that skipped EVERY push when the newest
26
+ * exchange across the fleet went stale. The reply file is first-hand and
27
+ * timestamped, so acting on it needs no such fleet-wide gate.
25
28
  */
26
29
  import { BasePeriodicService, type DaemonContext } from './service.js';
27
30
  import type { DaemonServiceId } from '../daemon-services.js';