@phnx-labs/agents-cli 1.22.114 → 1.22.115

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 (176) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +199 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +2 -7
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/focus.js +1 -1
  19. package/dist/commands/go.d.ts +0 -17
  20. package/dist/commands/go.js +2 -19
  21. package/dist/commands/logs.js +1 -1
  22. package/dist/commands/mcp.js +8 -83
  23. package/dist/commands/memory.js +4 -47
  24. package/dist/commands/message.js +4 -4
  25. package/dist/commands/plugins.js +1 -93
  26. package/dist/commands/repo.js +0 -44
  27. package/dist/commands/resume.js +4 -1
  28. package/dist/commands/secrets-passthrough.js +2 -2
  29. package/dist/commands/send.d.ts +4 -4
  30. package/dist/commands/send.js +6 -51
  31. package/dist/commands/sessions-backup-setup.js +1 -1
  32. package/dist/commands/sessions-resume.js +0 -1
  33. package/dist/commands/sessions-share.d.ts +5 -7
  34. package/dist/commands/sessions-share.js +98 -49
  35. package/dist/commands/sessions.js +1 -8
  36. package/dist/commands/setup-browser.js +18 -2
  37. package/dist/commands/setup-computer.js +20 -6
  38. package/dist/commands/setup-secrets.js +24 -4
  39. package/dist/commands/setup-terminal.d.ts +3 -0
  40. package/dist/commands/setup-terminal.js +22 -0
  41. package/dist/commands/setup.d.ts +1 -1
  42. package/dist/commands/setup.js +20 -10
  43. package/dist/commands/skills.js +0 -8
  44. package/dist/commands/ssh.d.ts +6 -0
  45. package/dist/commands/ssh.js +92 -233
  46. package/dist/commands/sync.js +14 -5
  47. package/dist/commands/teams.js +1 -1
  48. package/dist/commands/traces.js +1 -1
  49. package/dist/lib/accounts/add.d.ts +0 -5
  50. package/dist/lib/accounts/add.js +1 -7
  51. package/dist/lib/artifacts-client.d.ts +20 -0
  52. package/dist/lib/artifacts-client.js +46 -0
  53. package/dist/lib/auth-mint.js +2 -2
  54. package/dist/lib/browser/runtime-state.d.ts +55 -0
  55. package/dist/lib/browser/runtime-state.js +99 -18
  56. package/dist/lib/browser/service.js +21 -1
  57. package/dist/lib/cli-resources.js +3 -1
  58. package/dist/lib/cloud/dispatch.js +1 -1
  59. package/dist/lib/cloudflare/creds.d.ts +10 -0
  60. package/dist/lib/cloudflare/creds.js +46 -0
  61. package/dist/lib/cloudflare/provision.d.ts +35 -0
  62. package/dist/lib/cloudflare/provision.js +144 -0
  63. package/dist/lib/computer/sessions-list.d.ts +55 -0
  64. package/dist/lib/computer/sessions-list.js +168 -1
  65. package/dist/lib/daemon/daemon.js +8 -1
  66. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  67. package/dist/lib/daemon/feed-stream-service.js +40 -0
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/devices/connect.d.ts +49 -5
  71. package/dist/lib/devices/connect.js +169 -21
  72. package/dist/lib/feed/envelope.d.ts +79 -0
  73. package/dist/lib/feed/envelope.js +23 -0
  74. package/dist/lib/feed/events.d.ts +6 -0
  75. package/dist/lib/feed/events.js +8 -0
  76. package/dist/lib/feed/hub-server.d.ts +86 -0
  77. package/dist/lib/feed/hub-server.js +334 -0
  78. package/dist/lib/feed/hub.d.ts +95 -0
  79. package/dist/lib/feed/hub.js +255 -0
  80. package/dist/lib/feed/tool-activity.d.ts +108 -0
  81. package/dist/lib/feed/tool-activity.js +313 -0
  82. package/dist/lib/feed/tools.d.ts +198 -0
  83. package/dist/lib/feed/tools.js +265 -0
  84. package/dist/lib/feed/watch.d.ts +50 -50
  85. package/dist/lib/feed/watch.js +147 -16
  86. package/dist/lib/format.d.ts +1 -1
  87. package/dist/lib/format.js +1 -1
  88. package/dist/lib/git.d.ts +0 -16
  89. package/dist/lib/git.js +0 -58
  90. package/dist/lib/helper-versions.js +1 -1
  91. package/dist/lib/hosts/remote-cmd.d.ts +51 -1
  92. package/dist/lib/hosts/remote-cmd.js +125 -8
  93. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  94. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  95. package/dist/lib/mcp.js +17 -11
  96. package/dist/lib/probe.d.ts +4 -1
  97. package/dist/lib/probe.js +5 -2
  98. package/dist/lib/pwsh.d.ts +33 -0
  99. package/dist/lib/pwsh.js +56 -0
  100. package/dist/lib/redact.d.ts +8 -0
  101. package/dist/lib/redact.js +11 -0
  102. package/dist/lib/refresh.d.ts +6 -2
  103. package/dist/lib/refresh.js +92 -72
  104. package/dist/lib/secrets-cli.d.ts +11 -0
  105. package/dist/lib/secrets-cli.js +30 -0
  106. package/dist/lib/secrets-client.js +3 -2
  107. package/dist/lib/session/detached.d.ts +7 -0
  108. package/dist/lib/session/detached.js +29 -0
  109. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  110. package/dist/lib/session/remote/peer-stream.js +33 -6
  111. package/dist/lib/session/sync/backend.d.ts +3 -3
  112. package/dist/lib/session/sync/backend.js +3 -3
  113. package/dist/lib/session/sync/provision.d.ts +1 -1
  114. package/dist/lib/session/sync/provision.js +2 -2
  115. package/dist/lib/sessions-client.js +0 -3
  116. package/dist/lib/setup-tool-install.d.ts +3 -0
  117. package/dist/lib/setup-tool-install.js +26 -0
  118. package/dist/lib/setup-tool-status.d.ts +22 -0
  119. package/dist/lib/setup-tool-status.js +215 -0
  120. package/dist/lib/share-runtime.d.ts +11 -0
  121. package/dist/lib/share-runtime.js +63 -0
  122. package/dist/lib/smart-launch.d.ts +1 -5
  123. package/dist/lib/smart-launch.js +3 -11
  124. package/dist/lib/ssh-exec.d.ts +44 -0
  125. package/dist/lib/ssh-exec.js +119 -0
  126. package/dist/lib/startup/command-registry.d.ts +6 -4
  127. package/dist/lib/startup/command-registry.js +10 -7
  128. package/dist/lib/state.js +2 -2
  129. package/dist/lib/storage/selection.d.ts +2 -2
  130. package/dist/lib/storage/selection.js +2 -2
  131. package/dist/lib/sync-umbrella.d.ts +5 -0
  132. package/dist/lib/sync-umbrella.js +18 -10
  133. package/dist/lib/traces/backend.d.ts +1 -2
  134. package/dist/lib/traces/backend.js +1 -2
  135. package/dist/lib/traces/provision.d.ts +1 -1
  136. package/dist/lib/traces/provision.js +2 -2
  137. package/dist/lib/types.d.ts +8 -6
  138. package/package.json +2 -3
  139. package/dist/commands/artifacts-setup.d.ts +0 -53
  140. package/dist/commands/artifacts-setup.js +0 -161
  141. package/dist/commands/artifacts.d.ts +0 -18
  142. package/dist/commands/artifacts.js +0 -58
  143. package/dist/commands/attach.d.ts +0 -12
  144. package/dist/commands/attach.js +0 -86
  145. package/dist/commands/auth-mint.d.ts +0 -12
  146. package/dist/commands/auth-mint.js +0 -108
  147. package/dist/commands/reconnect.d.ts +0 -46
  148. package/dist/commands/reconnect.js +0 -115
  149. package/dist/commands/share.d.ts +0 -293
  150. package/dist/commands/share.js +0 -1424
  151. package/dist/lib/share/analytics.d.ts +0 -13
  152. package/dist/lib/share/analytics.js +0 -45
  153. package/dist/lib/share/backend.d.ts +0 -120
  154. package/dist/lib/share/backend.js +0 -176
  155. package/dist/lib/share/capture.d.ts +0 -31
  156. package/dist/lib/share/capture.js +0 -174
  157. package/dist/lib/share/config.d.ts +0 -72
  158. package/dist/lib/share/config.js +0 -211
  159. package/dist/lib/share/delete.d.ts +0 -123
  160. package/dist/lib/share/delete.js +0 -173
  161. package/dist/lib/share/html.d.ts +0 -20
  162. package/dist/lib/share/html.js +0 -88
  163. package/dist/lib/share/http-error.d.ts +0 -53
  164. package/dist/lib/share/http-error.js +0 -65
  165. package/dist/lib/share/og.d.ts +0 -26
  166. package/dist/lib/share/og.js +0 -84
  167. package/dist/lib/share/provision.d.ts +0 -127
  168. package/dist/lib/share/provision.js +0 -285
  169. package/dist/lib/share/publish.d.ts +0 -379
  170. package/dist/lib/share/publish.js +0 -818
  171. package/dist/lib/share/worker-template.d.ts +0 -27
  172. package/dist/lib/share/worker-template.js +0 -2424
  173. package/dist/lib/storage/index.d.ts +0 -14
  174. package/dist/lib/storage/index.js +0 -14
  175. package/dist/lib/storage/visibility.d.ts +0 -82
  176. package/dist/lib/storage/visibility.js +0 -99
@@ -5,11 +5,29 @@ export declare const PEER_BACKOFF_BASE_MS = 2000;
5
5
  export declare const PEER_BACKOFF_CAP_MS = 60000;
6
6
  /** Consecutive failed spawns before the peer is parked. */
7
7
  export declare const PEER_PARK_AFTER_FAILURES = 3;
8
+ /**
9
+ * Consecutive failed spawns before the peer is RETIRED — the capped 60 s ladder
10
+ * gives way to {@link PEER_RETIRED_RECHECK_MS}.
11
+ *
12
+ * The cap alone bounds the delay but not the total work: a box that is off for a
13
+ * weekend was dialed every 60 s for two days, ~2,880 ssh children per watcher
14
+ * per peer, each one already known to fail. Ten consecutive failures is well
15
+ * past any transient network event, so past that point the peer is treated as
16
+ * genuinely absent and re-dialed on the slow cadence instead.
17
+ */
18
+ export declare const PEER_RETIRE_AFTER_FAILURES = 10;
19
+ /**
20
+ * Re-dial cadence for a retired peer. Still bounded rather than never, because a
21
+ * box can come back without anything touching the device registry — waiting
22
+ * only on a registry change would leave it unreachable until an operator acted.
23
+ */
24
+ export declare const PEER_RETIRED_RECHECK_MS: number;
8
25
  /**
9
26
  * Reconnect delay for `failures` consecutive failed spawns: 0 for a healthy
10
- * peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS}.
27
+ * peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS},
28
+ * and {@link PEER_RETIRED_RECHECK_MS} once the peer is retired.
11
29
  */
12
- export declare function peerBackoffDelayMs(failures: number, base?: number, cap?: number): number;
30
+ export declare function peerBackoffDelayMs(failures: number, base?: number, cap?: number, retireAfter?: number, retiredMs?: number): number;
13
31
  interface PeerStreamOptions {
14
32
  /** The device to subscribe to. */
15
33
  device: DeviceProfile;
@@ -29,6 +47,10 @@ interface PeerStreamOptions {
29
47
  backoffCapMs?: number;
30
48
  /** Override the park threshold (tests). */
31
49
  parkAfterFailures?: number;
50
+ /** Override the retire threshold (tests). */
51
+ retireAfterFailures?: number;
52
+ /** Override the retired re-dial cadence (tests). */
53
+ retiredRecheckMs?: number;
32
54
  /** Override the ssh binary (tests). */
33
55
  sshBin?: string;
34
56
  /** Override the parked peer's registry re-check cadence (tests). */
@@ -19,6 +19,9 @@
19
19
  * - a peer that fails {@link PEER_PARK_AFTER_FAILURES} spawns in a row parked —
20
20
  * it stops the reconnect cycle and re-dials only when the device registry
21
21
  * changes or the capped backoff elapses;
22
+ * - a peer that fails {@link PEER_RETIRE_AFTER_FAILURES} in a row RETIRED — the
23
+ * capped 60 s ladder is itself unbounded in total work, so past that point the
24
+ * re-dial drops to {@link PEER_RETIRED_RECHECK_MS};
22
25
  * - abort listeners removed per iteration, so a watcher open for hours does not
23
26
  * accumulate one per reconnect on the caller's AbortSignal.
24
27
  */
@@ -34,17 +37,37 @@ export const PEER_BACKOFF_BASE_MS = 2_000;
34
37
  export const PEER_BACKOFF_CAP_MS = 60_000;
35
38
  /** Consecutive failed spawns before the peer is parked. */
36
39
  export const PEER_PARK_AFTER_FAILURES = 3;
40
+ /**
41
+ * Consecutive failed spawns before the peer is RETIRED — the capped 60 s ladder
42
+ * gives way to {@link PEER_RETIRED_RECHECK_MS}.
43
+ *
44
+ * The cap alone bounds the delay but not the total work: a box that is off for a
45
+ * weekend was dialed every 60 s for two days, ~2,880 ssh children per watcher
46
+ * per peer, each one already known to fail. Ten consecutive failures is well
47
+ * past any transient network event, so past that point the peer is treated as
48
+ * genuinely absent and re-dialed on the slow cadence instead.
49
+ */
50
+ export const PEER_RETIRE_AFTER_FAILURES = 10;
51
+ /**
52
+ * Re-dial cadence for a retired peer. Still bounded rather than never, because a
53
+ * box can come back without anything touching the device registry — waiting
54
+ * only on a registry change would leave it unreachable until an operator acted.
55
+ */
56
+ export const PEER_RETIRED_RECHECK_MS = 15 * 60_000;
37
57
  /** Bytes of a peer's stderr retained for the `unavailable` reason. */
38
58
  const PEER_STDERR_BYTES = 2_048;
39
59
  /** How often a parked peer re-checks the device registry for a refresh. */
40
60
  const PEER_REGISTRY_POLL_MS = 5_000;
41
61
  /**
42
62
  * Reconnect delay for `failures` consecutive failed spawns: 0 for a healthy
43
- * peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS}.
63
+ * peer, then {@link PEER_BACKOFF_BASE_MS} doubling to {@link PEER_BACKOFF_CAP_MS},
64
+ * and {@link PEER_RETIRED_RECHECK_MS} once the peer is retired.
44
65
  */
45
- export function peerBackoffDelayMs(failures, base = PEER_BACKOFF_BASE_MS, cap = PEER_BACKOFF_CAP_MS) {
66
+ export function peerBackoffDelayMs(failures, base = PEER_BACKOFF_BASE_MS, cap = PEER_BACKOFF_CAP_MS, retireAfter = PEER_RETIRE_AFTER_FAILURES, retiredMs = PEER_RETIRED_RECHECK_MS) {
46
67
  if (failures <= 0)
47
68
  return 0;
69
+ if (failures >= retireAfter)
70
+ return retiredMs;
48
71
  return Math.min(cap, base * 2 ** (failures - 1));
49
72
  }
50
73
  /** Bounded tail of a child's stderr, kept for the unavailable reason. */
@@ -133,10 +156,14 @@ export async function streamFromPeer(options) {
133
156
  failures += 1;
134
157
  const exit = code == null ? 'ssh failed' : `ssh exited ${code}`;
135
158
  const parked = failures >= parkAfter;
136
- const delay = peerBackoffDelayMs(failures, options.backoffBaseMs, options.backoffCapMs);
137
- options.onUnavailable(parked
138
- ? `${reasonFor(exit, stderr)} — parked after ${failures} failed connections, retrying in ${Math.round(delay / 1000)}s or on a device refresh`
139
- : reasonFor(exit, stderr));
159
+ const retireAfter = options.retireAfterFailures ?? PEER_RETIRE_AFTER_FAILURES;
160
+ const retired = failures >= retireAfter;
161
+ const delay = peerBackoffDelayMs(failures, options.backoffBaseMs, options.backoffCapMs, retireAfter, options.retiredRecheckMs);
162
+ options.onUnavailable(retired
163
+ ? `${reasonFor(exit, stderr)} — retired after ${failures} failed connections, re-dialing in ${Math.round(delay / 60_000)}min or on a device refresh`
164
+ : parked
165
+ ? `${reasonFor(exit, stderr)} — parked after ${failures} failed connections, retrying in ${Math.round(delay / 1000)}s or on a device refresh`
166
+ : reasonFor(exit, stderr));
140
167
  await parkedWait(options, delay);
141
168
  }
142
169
  }
@@ -18,9 +18,9 @@
18
18
  * MANAGED-FIRST — the mere PRESENCE of an r2.backups bundle is NOT a BYO
19
19
  * override. A signed-in user with a stale r2.backups bundle still backs up to
20
20
  * managed unless they opt out explicitly: `--byo`, `AGENTS_SESSIONS_BACKEND=byo`,
21
- * or a DI write token. This mirrors `lib/share/backend.ts` (a persisted BYO
22
- * config is deliberately not an override) so the product's managed-first
23
- * contract is identical across surfaces.
21
+ * or a DI write token. This mirrors the artifact-share backend contract (a
22
+ * persisted BYO config is deliberately not an override) so the product's
23
+ * managed-first contract is identical across surfaces.
24
24
  */
25
25
  import { type PhoenixSession } from '../../identity/client.js';
26
26
  import { type R2Config } from './config.js';
@@ -18,9 +18,9 @@
18
18
  * MANAGED-FIRST — the mere PRESENCE of an r2.backups bundle is NOT a BYO
19
19
  * override. A signed-in user with a stale r2.backups bundle still backs up to
20
20
  * managed unless they opt out explicitly: `--byo`, `AGENTS_SESSIONS_BACKEND=byo`,
21
- * or a DI write token. This mirrors `lib/share/backend.ts` (a persisted BYO
22
- * config is deliberately not an override) so the product's managed-first
23
- * contract is identical across surfaces.
21
+ * or a DI write token. This mirrors the artifact-share backend contract (a
22
+ * persisted BYO config is deliberately not an override) so the product's
23
+ * managed-first contract is identical across surfaces.
24
24
  */
25
25
  import { readSession } from '../../identity/client.js';
26
26
  import { selectStorageBackendKind } from '../../storage/selection.js';
@@ -1,4 +1,4 @@
1
- import { type CloudflareRequester } from '../../share/provision.js';
1
+ import { type CloudflareRequester } from '../../cloudflare/provision.js';
2
2
  interface ProvisionOptions {
3
3
  request?: CloudflareRequester;
4
4
  }
@@ -1,11 +1,11 @@
1
1
  // Cloudflare orchestration for the managed session-backup store. The generic
2
- // request primitives live in share/provision; only the isolated sessions
2
+ // request primitives live in cloudflare/provision; only the isolated sessions
3
3
  // resource choices and Worker template belong here. Mirrors lib/traces/provision.ts.
4
4
  //
5
5
  // This is the OPERATOR provisioning path — cutting the `agents-sessions` Worker +
6
6
  // bucket + custom domain once. Product traffic never runs it; a signed-in user
7
7
  // talks to the already-deployed Worker through `SessionsHttpClient`.
8
- import { addCustomDomain, createBucket, deployWorker, enableWorkersDev, findZoneId, putWorkerSecret, } from '../../share/provision.js';
8
+ import { addCustomDomain, createBucket, deployWorker, enableWorkersDev, findZoneId, putWorkerSecret, } from '../../cloudflare/provision.js';
9
9
  import { DEFAULT_SESSIONS_BUCKET_NAME, DEFAULT_SESSIONS_DOMAIN, DEFAULT_SESSIONS_WORKER_NAME, } from './managed-config.js';
10
10
  import { renderSessionsWorkerScript } from './worker-template.js';
11
11
  /** Bind the Phoenix identity base URL used to verify every session read and write. */
@@ -70,12 +70,9 @@ const ENGINE_VERBS = new Set([
70
70
  'tail',
71
71
  'watch',
72
72
  'preview',
73
- 'attach',
74
73
  'focus',
75
74
  'render',
76
75
  'stats',
77
- 'go',
78
- 'reconnect',
79
76
  'migrations',
80
77
  'export',
81
78
  ]);
@@ -0,0 +1,3 @@
1
+ import { type SetupTool } from './setup-tool-status.js';
2
+ /** Use the host installer; setup must not install or start a tool from a read. */
3
+ export declare function installSetupTool(tool: Exclude<SetupTool, 'secrets'>): Promise<boolean>;
@@ -0,0 +1,26 @@
1
+ import { installCli, resolveCliManifest } from './cli-resources.js';
2
+ import { getCachedToolSetup, refreshToolSetup } from './setup-tool-status.js';
3
+ const PACKAGES = {
4
+ browser: '@phnx-labs/browser-cli@0.1.3',
5
+ computer: '@phnx-labs/computer-cli@0.1.5',
6
+ };
7
+ /** Use the host installer; setup must not install or start a tool from a read. */
8
+ export async function installSetupTool(tool) {
9
+ if (getCachedToolSetup().find((row) => row.tool === tool)?.installed)
10
+ return true;
11
+ const manifest = resolveCliManifest(tool) ?? {
12
+ name: tool,
13
+ check: { kind: 'which', cmd: tool },
14
+ install: [{ npm: PACKAGES[tool] }],
15
+ source: 'builtin',
16
+ path: '',
17
+ };
18
+ const result = installCli(manifest);
19
+ if (result.error)
20
+ console.error(result.error);
21
+ const rows = await refreshToolSetup(tool);
22
+ const installed = rows[0]?.installed === true;
23
+ if (!installed && !result.error)
24
+ console.error(`${tool} installation did not produce a standalone executable on PATH.`);
25
+ return result.installed && installed;
26
+ }
@@ -0,0 +1,22 @@
1
+ export declare const SETUP_TOOLS: readonly ["browser", "computer", "secrets"];
2
+ export type SetupTool = typeof SETUP_TOOLS[number];
3
+ export interface ToolSetupRow {
4
+ tool: SetupTool;
5
+ installed: boolean | null;
6
+ executable?: string;
7
+ version?: string;
8
+ readiness: 'ready' | 'needs-setup' | 'permission-required' | 'stopped' | 'unsupported' | 'unknown';
9
+ detail: string;
10
+ checkedAtMs: number | null;
11
+ }
12
+ export interface ToolSetupOptions {
13
+ cacheDir?: string;
14
+ }
15
+ export declare function toolSetupCacheDir(options?: ToolSetupOptions): string;
16
+ /** Presence is metadata; health is the last explicit check, never a timer probe. */
17
+ export declare function getCachedToolSetup(options?: ToolSetupOptions): ToolSetupRow[];
18
+ export declare function toolReadiness(tool: SetupTool, status: unknown): Pick<ToolSetupRow, 'readiness' | 'detail'>;
19
+ /** A shared disk lock coalesces overlapping requests from separate CLI clients. */
20
+ export declare function refreshToolSetup(tool?: SetupTool | 'all', options?: ToolSetupOptions): Promise<ToolSetupRow[]>;
21
+ /** File notifications are shared by the daemon collector; no background probes. */
22
+ export declare function subscribeToolSetup(listener: (rows: ToolSetupRow[]) => void, options?: ToolSetupOptions): () => void;
@@ -0,0 +1,215 @@
1
+ import * as fs from 'node:fs';
2
+ import * as path from 'node:path';
3
+ import { findInPath } from './agent-spec/agents.js';
4
+ import { getCacheDir, getDeviceMetaPath, getHelpersDir, getUserAgentsDir } from './state.js';
5
+ import { atomicWriteJsonSync, withFileLockAsync } from './fs-atomic.js';
6
+ import { probeCapture } from './probe.js';
7
+ import { invocation, isStandaloneComputer } from './computer-client.js';
8
+ import { getSocketPath as browserSocketPath } from './browser/ipc.js';
9
+ export const SETUP_TOOLS = ['browser', 'computer', 'secrets'];
10
+ export function toolSetupCacheDir(options = {}) {
11
+ return path.join(options.cacheDir ?? getCacheDir(), 'setup-tools');
12
+ }
13
+ function cachePath(tool, options) {
14
+ return path.join(toolSetupCacheDir(options), `${tool}.json`);
15
+ }
16
+ function setupInputs(tool) {
17
+ const inputs = [path.join(getUserAgentsDir(), 'agents.yaml'), getDeviceMetaPath()];
18
+ if (tool === 'browser')
19
+ inputs.push(browserSocketPath());
20
+ if (tool === 'computer')
21
+ inputs.push(process.env.COMPUTER_HELPER_SOCKET || path.join(getHelpersDir(), 'computer.sock'));
22
+ return inputs;
23
+ }
24
+ function inputStamp(tool) {
25
+ return setupInputs(tool).map((file) => {
26
+ try {
27
+ const stat = fs.statSync(file);
28
+ return `${file}:${stat.ino}:${stat.mtimeMs}:${stat.size}`;
29
+ }
30
+ catch {
31
+ return `${file}:missing`;
32
+ }
33
+ }).join('|');
34
+ }
35
+ function binaryMetadata(tool) {
36
+ const empty = { tool, installed: false, readiness: 'needs-setup', detail: 'CLI is not installed.', checkedAtMs: null };
37
+ try {
38
+ const explicit = process.env[`${tool.toUpperCase()}_BIN`]?.trim();
39
+ const accept = (candidate) => {
40
+ if (tool === 'computer')
41
+ return isStandaloneComputer(candidate);
42
+ if (tool === 'browser') {
43
+ try {
44
+ return !fs.realpathSync(candidate).endsWith(path.join('dist', 'browser.js'));
45
+ }
46
+ catch {
47
+ return false;
48
+ }
49
+ }
50
+ return true;
51
+ };
52
+ let executable = explicit ? (accept(explicit) ? explicit : null) : findInPath(tool, { accept });
53
+ if (!executable)
54
+ return { row: empty, fingerprint: 'missing' };
55
+ if (/\.(cmd|ps1)$/i.test(executable)) {
56
+ const packageDir = path.join(path.dirname(executable), 'node_modules', '@phnx-labs', `${tool}-cli`);
57
+ const pkg = JSON.parse(fs.readFileSync(path.join(packageDir, 'package.json'), 'utf8'));
58
+ const entry = typeof pkg.bin === 'string' ? pkg.bin : pkg.bin?.[tool];
59
+ if (pkg.name !== `@phnx-labs/${tool}-cli` || typeof entry !== 'string')
60
+ throw new Error('unrecognized npm launcher');
61
+ const resolved = path.resolve(packageDir, entry);
62
+ if (!resolved.startsWith(`${path.resolve(packageDir)}${path.sep}`))
63
+ throw new Error('invalid npm entrypoint');
64
+ executable = resolved;
65
+ }
66
+ fs.accessSync(executable, process.platform === 'win32' || /\.[cm]?js$/i.test(executable) ? fs.constants.R_OK : fs.constants.X_OK);
67
+ const real = fs.realpathSync(executable);
68
+ const stat = fs.statSync(real);
69
+ if (!stat.isFile())
70
+ throw new Error('not a file');
71
+ let version;
72
+ let dir = path.dirname(real);
73
+ for (let depth = 0; depth < 5; depth++) {
74
+ try {
75
+ const pkg = JSON.parse(fs.readFileSync(path.join(dir, 'package.json'), 'utf8'));
76
+ if (pkg.name === `@phnx-labs/${tool}-cli` && typeof pkg.version === 'string') {
77
+ version = pkg.version;
78
+ break;
79
+ }
80
+ }
81
+ catch { /* Executables need not be npm packages. */ }
82
+ const parent = path.dirname(dir);
83
+ if (parent === dir)
84
+ break;
85
+ dir = parent;
86
+ }
87
+ return {
88
+ fingerprint: `${real}:${stat.size}:${stat.mtimeMs}:${version ?? ''}|${inputStamp(tool)}`,
89
+ row: { tool, installed: true, executable, version, readiness: 'unknown', detail: 'Installed. Health has not been checked.', checkedAtMs: null },
90
+ };
91
+ }
92
+ catch {
93
+ return { fingerprint: 'unavailable', row: { ...empty, installed: null, readiness: 'unknown', detail: 'Executable could not be inspected.' } };
94
+ }
95
+ }
96
+ function readCache(tool, options) {
97
+ try {
98
+ const cached = JSON.parse(fs.readFileSync(cachePath(tool, options), 'utf8'));
99
+ if (cached.v === 1 && cached.row?.tool === tool && typeof cached.fingerprint === 'string')
100
+ return cached;
101
+ }
102
+ catch { /* No health result yet. */ }
103
+ return null;
104
+ }
105
+ /** Presence is metadata; health is the last explicit check, never a timer probe. */
106
+ export function getCachedToolSetup(options = {}) {
107
+ return SETUP_TOOLS.map((tool) => {
108
+ const { row, fingerprint } = binaryMetadata(tool);
109
+ const cached = readCache(tool, options);
110
+ return cached?.fingerprint === fingerprint ? { ...cached.row, executable: row.executable, version: row.version } : row;
111
+ });
112
+ }
113
+ export function toolReadiness(tool, status) {
114
+ if (!status || typeof status !== 'object')
115
+ return { readiness: 'unknown', detail: 'The CLI returned an unreadable health result.' };
116
+ const value = status;
117
+ if (tool === 'computer') {
118
+ if (value.installed === false)
119
+ return { readiness: 'needs-setup', detail: 'CLI installed; computer helper needs setup.' };
120
+ if (value.running === true && value.trusted === true)
121
+ return { readiness: 'ready', detail: 'Helper is running and Accessibility is granted. Screen Recording is checked when capturing.' };
122
+ if (value.running === true && value.trusted === false)
123
+ return { readiness: 'permission-required', detail: 'Grant Accessibility to Agents Computer in System Settings.' };
124
+ if (value.running === false)
125
+ return { readiness: 'stopped', detail: 'Computer helper is stopped or unreachable.' };
126
+ }
127
+ if (tool === 'browser') {
128
+ if (value.running === true)
129
+ return { readiness: 'ready', detail: 'Browser service is running.' };
130
+ if (value.running === false)
131
+ return { readiness: 'stopped', detail: 'Browser service is stopped.' };
132
+ }
133
+ return { readiness: 'unknown', detail: typeof value.error === 'string' && value.error.trim() ? value.error.slice(0, 500) : 'The CLI did not report a recognized health state.' };
134
+ }
135
+ async function checkTool(row) {
136
+ if (!row.installed || !row.executable)
137
+ return { ...row, checkedAtMs: Date.now() };
138
+ // The standalone has no non-interactive health JSON. Do not list bundles or
139
+ // unlock the broker merely to paint a settings row.
140
+ if (row.tool === 'secrets')
141
+ return { ...row, checkedAtMs: Date.now(), detail: 'Installed. Secret access is checked when used; this check does not unlock secrets.' };
142
+ try {
143
+ const { command, prefix } = invocation(row.executable);
144
+ const { stdout } = await probeCapture(command, [...prefix, 'status', '--json'], 8000, { acceptedExitCodes: [0, 1], maxOutputBytes: 256 * 1024 });
145
+ return { ...row, ...toolReadiness(row.tool, JSON.parse(stdout)), checkedAtMs: Date.now() };
146
+ }
147
+ catch {
148
+ return { ...row, readiness: 'unknown', detail: 'Health check failed or timed out. Check again to retry.', checkedAtMs: Date.now() };
149
+ }
150
+ }
151
+ /** A shared disk lock coalesces overlapping requests from separate CLI clients. */
152
+ export async function refreshToolSetup(tool = 'all', options = {}) {
153
+ const requestedAt = Date.now();
154
+ const dir = toolSetupCacheDir(options);
155
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
156
+ for (const selected of tool === 'all' ? SETUP_TOOLS : [tool]) {
157
+ const file = cachePath(selected, options);
158
+ await withFileLockAsync(file, async () => {
159
+ const metadata = binaryMetadata(selected);
160
+ const cached = readCache(selected, options);
161
+ if (cached?.fingerprint === metadata.fingerprint && (cached.row.checkedAtMs ?? 0) >= requestedAt)
162
+ return;
163
+ const row = await checkTool(metadata.row);
164
+ atomicWriteJsonSync(file, { v: 1, fingerprint: metadata.fingerprint, row });
165
+ }, { realpath: false, acquireTimeoutMs: 20_000 });
166
+ }
167
+ return getCachedToolSetup(options).filter((row) => tool === 'all' || row.tool === tool);
168
+ }
169
+ /** File notifications are shared by the daemon collector; no background probes. */
170
+ export function subscribeToolSetup(listener, options = {}) {
171
+ const dir = toolSetupCacheDir(options);
172
+ fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
173
+ const paths = new Set([dir, ...(process.env.PATH ?? '').split(path.delimiter).filter(Boolean)]);
174
+ for (const tool of SETUP_TOOLS)
175
+ for (const input of setupInputs(tool))
176
+ paths.add(path.dirname(input));
177
+ for (const row of getCachedToolSetup(options)) {
178
+ if (row.executable) {
179
+ paths.add(path.dirname(row.executable));
180
+ try {
181
+ paths.add(path.dirname(fs.realpathSync(row.executable)));
182
+ }
183
+ catch { /* Removed since discovery. */ }
184
+ }
185
+ }
186
+ let previous = JSON.stringify(getCachedToolSetup(options));
187
+ let debounce;
188
+ const changed = () => {
189
+ if (debounce)
190
+ return;
191
+ debounce = setTimeout(() => {
192
+ debounce = undefined;
193
+ const rows = getCachedToolSetup(options);
194
+ const next = JSON.stringify(rows);
195
+ if (next !== previous) {
196
+ previous = next;
197
+ listener(rows);
198
+ }
199
+ }, 150);
200
+ debounce.unref();
201
+ };
202
+ const watchers = [];
203
+ for (const target of paths) {
204
+ try {
205
+ const watcher = fs.watch(target, { persistent: false }, changed);
206
+ watcher.on('error', changed);
207
+ watchers.push(watcher);
208
+ }
209
+ catch { /* Absent PATH entries have no executable to report. */ }
210
+ }
211
+ changed();
212
+ return () => { if (debounce)
213
+ clearTimeout(debounce); for (const watcher of watchers)
214
+ watcher.close(); };
215
+ }
@@ -0,0 +1,11 @@
1
+ /** Best-effort runtime env for spawned agents. Never throws AND never prompts.
2
+ *
3
+ * Auto-injecting the share write token on every `agents run` is a background
4
+ * convenience, NOT a user-initiated secret access — so it MUST NOT raise a Touch
5
+ * ID sheet (SEC-13: an agent launch never pops biometry on its own). The read is
6
+ * always `agentOnly`: it resolves the token only from the injected env or an
7
+ * already-held / no-ACL bundle, and silently returns undefined otherwise (the
8
+ * agent can still publish via its own `artifacts share` credentials). For
9
+ * zero-friction auto-share with no prompt: unlock once (`agents secrets unlock
10
+ * share`) or make it no-ACL (`agents secrets policy share never`). */
11
+ export declare function shareRuntimeEnv(): Record<string, string> | undefined;
@@ -0,0 +1,63 @@
1
+ // Runtime share-token injection for spawned agents.
2
+ //
3
+ // The artifact share ENGINE moved to `@phnx-labs/artifacts-cli` (PHNX-3992);
4
+ // agents-cli no longer publishes artifacts itself. What stays here is the thin
5
+ // glue that lets a dispatched agent inherit the operator's BYO write token so
6
+ // its own `artifacts share` can publish headlessly: `run`/`teams`/`cloud`
7
+ // dispatch call `shareRuntimeEnv()` and forward the result into the agent env.
8
+ // artifacts-cli honors the same `SHARE_WRITE_TOKEN` variable, so the injection
9
+ // contract is unchanged by the extraction.
10
+ //
11
+ // The endpoint config still lives in `agents.yaml` under `share:` (Meta.share)
12
+ // so a previously-provisioned BYO endpoint keeps syncing fleet-wide; the raw
13
+ // write token lives in the keychain-backed `share` secrets bundle, never on disk.
14
+ import { readMeta } from './state.js';
15
+ import { bundleExistsSync as bundleExists, readAndResolveBundleEnvSync as readAndResolveBundleEnv, } from './secrets-client.js';
16
+ const SHARE_BUNDLE = 'share';
17
+ const SHARE_TOKEN_KEY = 'WRITE_TOKEN';
18
+ const SHARE_TOKEN_ENV_KEY = 'SHARE_WRITE_TOKEN';
19
+ /** Trim; empty / whitespace-only strings are absent. */
20
+ function nonempty(v) {
21
+ const t = v?.trim();
22
+ return t ? t : undefined;
23
+ }
24
+ /** Whether a BYO share endpoint is still configured in `agents.yaml`. */
25
+ function hasShareEndpoint() {
26
+ return !!nonempty(readMeta().share?.baseUrl);
27
+ }
28
+ /** The write token as injected ephemerally into fleet/cloud agents. */
29
+ function readWriteTokenEnv(env = process.env) {
30
+ const token = env[SHARE_TOKEN_ENV_KEY]?.trim();
31
+ return token ? token : null;
32
+ }
33
+ /** Best-effort runtime env for spawned agents. Never throws AND never prompts.
34
+ *
35
+ * Auto-injecting the share write token on every `agents run` is a background
36
+ * convenience, NOT a user-initiated secret access — so it MUST NOT raise a Touch
37
+ * ID sheet (SEC-13: an agent launch never pops biometry on its own). The read is
38
+ * always `agentOnly`: it resolves the token only from the injected env or an
39
+ * already-held / no-ACL bundle, and silently returns undefined otherwise (the
40
+ * agent can still publish via its own `artifacts share` credentials). For
41
+ * zero-friction auto-share with no prompt: unlock once (`agents secrets unlock
42
+ * share`) or make it no-ACL (`agents secrets policy share never`). */
43
+ export function shareRuntimeEnv() {
44
+ if (!hasShareEndpoint())
45
+ return undefined;
46
+ const fromEnv = readWriteTokenEnv();
47
+ if (fromEnv)
48
+ return { [SHARE_TOKEN_ENV_KEY]: fromEnv };
49
+ try {
50
+ if (!bundleExists(SHARE_BUNDLE))
51
+ return undefined;
52
+ const { env } = readAndResolveBundleEnv(SHARE_BUNDLE, {
53
+ caller: 'share',
54
+ keys: [SHARE_TOKEN_KEY],
55
+ agentOnly: true, // never raise a Touch ID sheet on an agent launch (SEC-13)
56
+ });
57
+ const token = env[SHARE_TOKEN_KEY];
58
+ return token ? { [SHARE_TOKEN_ENV_KEY]: token } : undefined;
59
+ }
60
+ catch {
61
+ return undefined;
62
+ }
63
+ }
@@ -117,16 +117,12 @@ type DeviceAutoHostOptions = {
117
117
  device?: string;
118
118
  on?: string;
119
119
  computer?: string;
120
- /** @deprecated Hidden alias for `--device auto`. */
121
- smart?: boolean;
122
120
  balanced?: boolean;
123
121
  strategy?: string;
124
122
  };
125
123
  export type DeviceAutoApplyResult = {
126
124
  /** True when automatic placement ran successfully. */
127
125
  attempted: boolean;
128
- /** True when deprecated `--smart` was seen. */
129
- deprecationSmart: boolean;
130
126
  /** Present when affinity resolved without error. */
131
127
  banner?: {
132
128
  hostLabel: string;
@@ -135,7 +131,7 @@ export type DeviceAutoApplyResult = {
135
131
  };
136
132
  };
137
133
  /**
138
- * Apply `--device auto` (and deprecated `--smart`) onto run options.
134
+ * Apply `--device auto` onto run options.
139
135
  * Mutates `options` in place. Placement failures propagate without rewriting
140
136
  * `auto`, so callers fail loud instead of silently launching locally.
141
137
  */
@@ -176,7 +176,7 @@ export async function resolveDeviceAuto(agent, opts = {}) {
176
176
  if (eligiblePool.length === 0) {
177
177
  throw new Error(formatNoHealthyDeviceError(pool, signals, agent));
178
178
  }
179
- // `agents devices prefer <name>` boosts a device in the ranking — resolved
179
+ // `agents devices config <name> auto-launch.preferred on` boosts a device in the ranking — resolved
180
180
  // over the same pool so a fleet default reaches a doc-less box.
181
181
  const preferred = opts.preferred ?? autoLaunchPreferredSet(pool, { roster: pool });
182
182
  const picked = pickBestDevice(eligiblePool, [], { signals, agentLabel: agent, preferred });
@@ -260,21 +260,14 @@ export function isDeviceAuto(value) {
260
260
  }
261
261
  const HOST_SLOTS = ['host', 'device', 'on', 'computer'];
262
262
  /**
263
- * Apply `--device auto` (and deprecated `--smart`) onto run options.
263
+ * Apply `--device auto` onto run options.
264
264
  * Mutates `options` in place. Placement failures propagate without rewriting
265
265
  * `auto`, so callers fail loud instead of silently launching locally.
266
266
  */
267
267
  export async function applyDeviceAutoToOptions(options, deps = {}) {
268
- let deprecationSmart = false;
269
- if (options.smart) {
270
- const anyHost = HOST_SLOTS.some((k) => typeof options[k] === 'string' && options[k].trim() !== '');
271
- if (!anyHost)
272
- options.device = 'auto';
273
- deprecationSmart = true;
274
- }
275
268
  const hasAuto = HOST_SLOTS.some((k) => isDeviceAuto(options[k]));
276
269
  if (!hasAuto) {
277
- return { attempted: false, deprecationSmart };
270
+ return { attempted: false };
278
271
  }
279
272
  const accountPickerRequested = deps.accountPickerRequested ?? false;
280
273
  const resolve = deps.resolve ?? ((accountPicker) => resolveDeviceAuto(deps.agent, { accountPicker }));
@@ -296,7 +289,6 @@ export async function applyDeviceAutoToOptions(options, deps = {}) {
296
289
  const acctNote = accountPickerRequested ? 'accounts=picker' : 'accounts=balanced';
297
290
  return {
298
291
  attempted: true,
299
- deprecationSmart,
300
292
  banner: { hostLabel, deviceHint, acctNote },
301
293
  };
302
294
  }
@@ -173,6 +173,50 @@ interface SshExecRawStreamOptions extends SshExecOptions {
173
173
  * offset-resumed log following where a UTF-8 decode boundary must not shift the
174
174
  * byte cursor.
175
175
  */
176
+ /** Grace between SIGTERM and SIGKILL for a stream that will not stop. */
177
+ export declare const SSH_STREAM_KILL_GRACE_MS = 3000;
178
+ /** Bytes of a stream's stderr retained; the rest is dropped, with a note. */
179
+ export declare const SSH_STREAM_MAX_STDERR: number;
180
+ export interface SshStreamResult {
181
+ code: number | null;
182
+ stderr: Buffer;
183
+ timedOut: boolean;
184
+ /** True when the child had to be SIGKILLed after ignoring SIGTERM. */
185
+ killed: boolean;
186
+ /** True when stderr was truncated at {@link SSH_STREAM_MAX_STDERR}. */
187
+ stderrTruncated: boolean;
188
+ }
189
+ /**
190
+ * Stream one ssh command's stdout, with the args and env a CALLER built.
191
+ *
192
+ * This exists because {@link sshExecRawStream} assembles its own ssh args, which
193
+ * means it cannot carry a device's canonical auth — the askpass shim for a
194
+ * password-auth box, `-i`/`IdentitiesOnly` for an explicit identity file, or the
195
+ * managed known-hosts pinning. A caller that has already built those through
196
+ * `buildSshInvocation` needs a way to run them, and duplicating the transport to
197
+ * get it would be the worse answer. So this takes `args`/`env` verbatim and adds
198
+ * only the lifecycle guarantees:
199
+ *
200
+ * - **SIGTERM then SIGKILL.** A timeout or abort that only sends SIGTERM leaks a
201
+ * child that ignores it — ssh does, mid-handshake — and the promise never
202
+ * settles. After {@link SSH_STREAM_KILL_GRACE_MS} the child is SIGKILLed and
203
+ * `killed` says so.
204
+ * - **Bounded stderr.** A peer that writes endlessly to stderr would otherwise
205
+ * grow this buffer without limit; it is capped and `stderrTruncated` says so.
206
+ * - **`onStdout` cannot break the caller's cleanup.** A throw from the consumer
207
+ * is captured and re-thrown by the awaiting caller, so a write failure ends the
208
+ * transfer instead of escaping as an unhandled error inside a stream event.
209
+ */
210
+ export declare function sshStreamWithArgs(opts: {
211
+ args: string[];
212
+ env?: Record<string, string>;
213
+ onStdout: (chunk: Buffer) => void;
214
+ timeoutMs?: number;
215
+ killGraceMs?: number;
216
+ maxStderrBytes?: number;
217
+ signal?: AbortSignal;
218
+ sshBin?: string;
219
+ }): Promise<SshStreamResult>;
176
220
  export declare function sshExecRawStream(target: string, remoteCmd: string, opts: SshExecRawStreamOptions): Promise<Omit<SshExecRawResult, 'stdout'>>;
177
221
  /** True if `target` is reachable over ssh (a passwordless `true` succeeds quickly). */
178
222
  export declare function sshReachable(target: string, timeoutMs?: number): boolean;