@phnx-labs/agents-cli 1.22.47 → 1.22.49

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 (249) hide show
  1. package/CHANGELOG.md +401 -17
  2. package/README.md +66 -32
  3. package/dist/bootstrap.js +4 -2
  4. package/dist/cli/command-registry.d.ts +1 -0
  5. package/dist/cli/command-registry.js +2 -0
  6. package/dist/commands/accounts.d.ts +58 -0
  7. package/dist/commands/accounts.js +206 -32
  8. package/dist/commands/artifacts.js +7 -6
  9. package/dist/commands/attach.js +1 -1
  10. package/dist/commands/browser.js +501 -103
  11. package/dist/commands/config.d.ts +0 -1
  12. package/dist/commands/config.js +0 -20
  13. package/dist/commands/daemon.d.ts +21 -0
  14. package/dist/commands/daemon.js +147 -12
  15. package/dist/commands/exec.js +52 -15
  16. package/dist/commands/focus.d.ts +14 -0
  17. package/dist/commands/focus.js +48 -5
  18. package/dist/commands/go.d.ts +3 -0
  19. package/dist/commands/go.js +16 -3
  20. package/dist/commands/inspect.js +2 -2
  21. package/dist/commands/lease.d.ts +1 -1
  22. package/dist/commands/lease.js +4 -3
  23. package/dist/commands/mailboxes.js +4 -3
  24. package/dist/commands/menubar.js +11 -4
  25. package/dist/commands/projects.d.ts +16 -1
  26. package/dist/commands/projects.js +58 -64
  27. package/dist/commands/resume.js +1 -1
  28. package/dist/commands/route.js +4 -4
  29. package/dist/commands/routines.js +3 -2
  30. package/dist/commands/run-broadcast.d.ts +1 -1
  31. package/dist/commands/run-broadcast.js +1 -1
  32. package/dist/commands/sessions-picker.d.ts +22 -0
  33. package/dist/commands/sessions-picker.js +48 -9
  34. package/dist/commands/sessions-trace.js +1 -1
  35. package/dist/commands/sessions.js +1 -0
  36. package/dist/commands/setup-secrets.js +23 -0
  37. package/dist/commands/share.d.ts +20 -5
  38. package/dist/commands/share.js +152 -52
  39. package/dist/commands/ssh.js +54 -7
  40. package/dist/commands/traces.d.ts +2 -0
  41. package/dist/commands/traces.js +184 -0
  42. package/dist/commands/view.d.ts +1 -1
  43. package/dist/commands/view.js +17 -34
  44. package/dist/index.d.ts +1 -0
  45. package/dist/index.js +5 -0
  46. package/dist/lib/accounting/usage.d.ts +14 -3
  47. package/dist/lib/accounting/usage.js +64 -18
  48. package/dist/lib/agent-spec/agents.js +11 -9
  49. package/dist/lib/browser/drivers/ssh.d.ts +16 -2
  50. package/dist/lib/browser/drivers/ssh.js +36 -9
  51. package/dist/lib/browser/ipc.js +8 -3
  52. package/dist/lib/browser/profiles.d.ts +35 -101
  53. package/dist/lib/browser/profiles.js +130 -301
  54. package/dist/lib/browser/registry.d.ts +39 -0
  55. package/dist/lib/browser/registry.js +149 -0
  56. package/dist/lib/browser/remote-control.d.ts +8 -6
  57. package/dist/lib/browser/remote-control.js +8 -6
  58. package/dist/lib/browser/resolve-target.d.ts +68 -0
  59. package/dist/lib/browser/resolve-target.js +250 -0
  60. package/dist/lib/browser/runtime-state.d.ts +40 -16
  61. package/dist/lib/browser/runtime-state.js +92 -25
  62. package/dist/lib/browser/service.d.ts +28 -9
  63. package/dist/lib/browser/service.js +139 -62
  64. package/dist/lib/browser/task-index.d.ts +70 -0
  65. package/dist/lib/browser/task-index.js +180 -0
  66. package/dist/lib/byok-usage.js +2 -2
  67. package/dist/lib/claude-statusline.d.ts +29 -0
  68. package/dist/lib/claude-statusline.js +139 -0
  69. package/dist/lib/cloud/host.d.ts +1 -1
  70. package/dist/lib/cloud/host.js +1 -1
  71. package/dist/lib/cloud/rush.js +1 -1
  72. package/dist/lib/computer/computer-rpc.js +2 -2
  73. package/dist/lib/computer/download.d.ts +15 -15
  74. package/dist/lib/computer/download.js +35 -98
  75. package/dist/lib/computer/ssh-tunnel.d.ts +0 -3
  76. package/dist/lib/computer/ssh-tunnel.js +12 -22
  77. package/dist/lib/config-keys.d.ts +2 -8
  78. package/dist/lib/config-keys.js +2 -13
  79. package/dist/lib/crabbox/cli.d.ts +1 -1
  80. package/dist/lib/crabbox/cli.js +2 -2
  81. package/dist/lib/crabbox/lease.d.ts +9 -0
  82. package/dist/lib/crabbox/lease.js +70 -15
  83. package/dist/lib/crabbox/runtimes.js +1 -1
  84. package/dist/lib/daemon/account-state-daemon-service.d.ts +16 -0
  85. package/dist/lib/daemon/account-state-daemon-service.js +26 -0
  86. package/dist/lib/daemon/browser-ipc-service.d.ts +20 -0
  87. package/dist/lib/daemon/browser-ipc-service.js +46 -0
  88. package/dist/lib/daemon/daemon.d.ts +3 -0
  89. package/dist/lib/daemon/daemon.js +212 -444
  90. package/dist/lib/daemon/device-probe-service.d.ts +26 -0
  91. package/dist/lib/daemon/device-probe-service.js +47 -0
  92. package/dist/lib/daemon/keychain-reap-service.d.ts +17 -0
  93. package/dist/lib/daemon/keychain-reap-service.js +32 -0
  94. package/dist/lib/daemon/monitor-engine-service.d.ts +22 -0
  95. package/dist/lib/daemon/monitor-engine-service.js +29 -0
  96. package/dist/lib/daemon/secrets-broker-service.d.ts +20 -0
  97. package/dist/lib/daemon/secrets-broker-service.js +66 -0
  98. package/dist/lib/daemon/self-heal-service.d.ts +27 -0
  99. package/dist/lib/daemon/self-heal-service.js +48 -0
  100. package/dist/lib/daemon/service.d.ts +102 -0
  101. package/dist/lib/daemon/service.js +98 -0
  102. package/dist/lib/daemon/session-index-service.d.ts +22 -0
  103. package/dist/lib/daemon/session-index-service.js +40 -0
  104. package/dist/lib/daemon/state-dir-check-service.d.ts +43 -0
  105. package/dist/lib/daemon/state-dir-check-service.js +63 -0
  106. package/dist/lib/daemon/supervisor.d.ts +87 -0
  107. package/dist/lib/daemon/supervisor.js +298 -0
  108. package/dist/lib/daemon/watchdog-service.d.ts +19 -0
  109. package/dist/lib/daemon/watchdog-service.js +40 -0
  110. package/dist/lib/daemon-health.d.ts +15 -0
  111. package/dist/lib/daemon-health.js +29 -5
  112. package/dist/lib/daemon-services.d.ts +8 -1
  113. package/dist/lib/daemon-services.js +55 -0
  114. package/dist/lib/daemon-ticks.d.ts +13 -2
  115. package/dist/lib/daemon-ticks.js +20 -18
  116. package/dist/lib/device-config.d.ts +0 -2
  117. package/dist/lib/device-config.js +1 -23
  118. package/dist/lib/devices/registry.d.ts +1 -1
  119. package/dist/lib/devices/registry.js +1 -1
  120. package/dist/lib/devices/stats-cache.d.ts +20 -0
  121. package/dist/lib/devices/stats-cache.js +49 -5
  122. package/dist/lib/devices/worker-pick.d.ts +48 -0
  123. package/dist/lib/devices/worker-pick.js +113 -0
  124. package/dist/lib/exec.d.ts +67 -13
  125. package/dist/lib/exec.js +85 -29
  126. package/dist/lib/harness/adapters/cursor.js +8 -11
  127. package/dist/lib/helper-download.d.ts +108 -0
  128. package/dist/lib/helper-download.js +194 -0
  129. package/dist/lib/helper-versions.d.ts +52 -0
  130. package/dist/lib/helper-versions.js +58 -0
  131. package/dist/lib/hooks/cache.js +11 -0
  132. package/dist/lib/hooks/install.js +2 -2
  133. package/dist/lib/hosts/credentials.d.ts +1 -1
  134. package/dist/lib/hosts/credentials.js +1 -1
  135. package/dist/lib/hosts/dispatch.d.ts +7 -1
  136. package/dist/lib/hosts/dispatch.js +24 -5
  137. package/dist/lib/hosts/passthrough.d.ts +8 -15
  138. package/dist/lib/hosts/passthrough.js +19 -16
  139. package/dist/lib/hosts/providers/devices.d.ts +1 -1
  140. package/dist/lib/hosts/providers/devices.js +1 -1
  141. package/dist/lib/hosts/reconnect.d.ts +179 -21
  142. package/dist/lib/hosts/reconnect.js +266 -45
  143. package/dist/lib/installations/migrate.d.ts +4 -5
  144. package/dist/lib/installations/migrate.js +6 -7
  145. package/dist/lib/installations/shims.d.ts +1 -1
  146. package/dist/lib/installations/shims.js +23 -13
  147. package/dist/lib/installations/versions.js +25 -2
  148. package/dist/lib/linear-autoclose.d.ts +1 -1
  149. package/dist/lib/linear-autoclose.js +1 -1
  150. package/dist/lib/loop.d.ts +2 -2
  151. package/dist/lib/loop.js +1 -1
  152. package/dist/lib/menubar/download-menubar.d.ts +47 -0
  153. package/dist/lib/menubar/download-menubar.js +60 -0
  154. package/dist/lib/menubar/install-menubar.d.ts +31 -7
  155. package/dist/lib/menubar/install-menubar.js +79 -21
  156. package/dist/lib/menubar/notify-desktop.d.ts +3 -3
  157. package/dist/lib/menubar/notify-desktop.js +6 -6
  158. package/dist/lib/model-tiers.d.ts +1 -1
  159. package/dist/lib/project-probe.d.ts +1 -1
  160. package/dist/lib/project-probe.js +1 -1
  161. package/dist/lib/project-pull.d.ts +1 -1
  162. package/dist/lib/project-pull.js +2 -2
  163. package/dist/lib/projects.d.ts +1 -1
  164. package/dist/lib/projects.js +1 -1
  165. package/dist/lib/sandbox.js +6 -7
  166. package/dist/lib/secrets/download-keychain.d.ts +47 -0
  167. package/dist/lib/secrets/download-keychain.js +70 -0
  168. package/dist/lib/secrets/install-helper.d.ts +20 -1
  169. package/dist/lib/secrets/install-helper.js +44 -3
  170. package/dist/lib/session/active.js +17 -1
  171. package/dist/lib/session/db.d.ts +11 -1
  172. package/dist/lib/session/db.js +138 -6
  173. package/dist/lib/session/discover.d.ts +21 -1
  174. package/dist/lib/session/discover.js +33 -5
  175. package/dist/lib/session/highlights.d.ts +49 -1
  176. package/dist/lib/session/highlights.js +95 -0
  177. package/dist/lib/session/host-link.d.ts +16 -2
  178. package/dist/lib/session/host-link.js +7 -1
  179. package/dist/lib/session/parse.js +33 -1
  180. package/dist/lib/session/remote/remote-list.d.ts +9 -0
  181. package/dist/lib/session/remote/remote-list.js +19 -2
  182. package/dist/lib/session/remote/watch.js +11 -2
  183. package/dist/lib/session/session-cache.d.ts +53 -0
  184. package/dist/lib/session/session-cache.js +88 -0
  185. package/dist/lib/session/state.js +1 -1
  186. package/dist/lib/session/tool-calls.d.ts +2 -0
  187. package/dist/lib/session/tool-calls.js +1 -1
  188. package/dist/lib/session/trajectory-html.d.ts +5 -1
  189. package/dist/lib/session/trajectory-html.js +42 -22
  190. package/dist/lib/session/trajectory-text.js +9 -2
  191. package/dist/lib/session/trajectory.js +53 -4
  192. package/dist/lib/session/types.d.ts +15 -0
  193. package/dist/lib/sha256-asset.d.ts +26 -0
  194. package/dist/lib/sha256-asset.js +41 -0
  195. package/dist/lib/share/backend.d.ts +95 -0
  196. package/dist/lib/share/backend.js +139 -0
  197. package/dist/lib/share/delete.d.ts +6 -2
  198. package/dist/lib/share/delete.js +8 -9
  199. package/dist/lib/share/html.d.ts +22 -0
  200. package/dist/lib/share/html.js +88 -0
  201. package/dist/lib/share/provision.d.ts +18 -4
  202. package/dist/lib/share/provision.js +38 -4
  203. package/dist/lib/share/publish.d.ts +39 -15
  204. package/dist/lib/share/publish.js +55 -26
  205. package/dist/lib/share/worker-template.js +171 -11
  206. package/dist/lib/sqlite.js +16 -3
  207. package/dist/lib/ssh-exec.d.ts +65 -0
  208. package/dist/lib/ssh-exec.js +120 -4
  209. package/dist/lib/startup/command-registry.js +1 -1
  210. package/dist/lib/startup/dev-build.js +3 -3
  211. package/dist/lib/state.d.ts +1 -1
  212. package/dist/lib/state.js +7 -3
  213. package/dist/lib/subagents-registry.js +1 -1
  214. package/dist/lib/terminal/index.d.ts +1 -1
  215. package/dist/lib/terminal/types.d.ts +1 -1
  216. package/dist/lib/terminal/types.js +1 -1
  217. package/dist/lib/tmux/orphan-reap.js +1 -1
  218. package/dist/lib/tmux/session.js +31 -0
  219. package/dist/lib/traces/backend.d.ts +29 -0
  220. package/dist/lib/traces/backend.js +46 -0
  221. package/dist/lib/traces/classify.d.ts +52 -0
  222. package/dist/lib/traces/classify.js +72 -0
  223. package/dist/lib/traces/config.d.ts +10 -0
  224. package/dist/lib/traces/config.js +2 -0
  225. package/dist/lib/traces/provision.d.ts +19 -0
  226. package/dist/lib/traces/provision.js +30 -0
  227. package/dist/lib/traces/sync.d.ts +150 -0
  228. package/dist/lib/traces/sync.js +447 -0
  229. package/dist/lib/traces/worker-template.d.ts +4 -0
  230. package/dist/lib/traces/worker-template.js +159 -0
  231. package/dist/lib/types.d.ts +18 -19
  232. package/dist/lib/types.js +14 -0
  233. package/dist/lib/usage-refresh.d.ts +1 -1
  234. package/dist/lib/usage-refresh.js +11 -8
  235. package/dist/lib/workflows.d.ts +1 -1
  236. package/package.json +4 -6
  237. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  238. package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +0 -24
  239. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  240. package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
  241. package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +0 -128
  242. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  243. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -26
  244. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  245. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  246. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +0 -135
  247. package/dist/lib/secrets/Agents CLI.app/Contents/embedded.provisionprofile +0 -0
  248. package/dist/lib/usage-fleet.d.ts +0 -32
  249. package/dist/lib/usage-fleet.js +0 -125
@@ -2,8 +2,11 @@ import * as path from 'path';
2
2
  import * as fs from 'fs';
3
3
  import { getBrowserRuntimeDir as getBrowserRuntimeDirRoot, readMeta, updateMeta, } from '../state.js';
4
4
  import { getConfigValue } from '../device-config.js';
5
+ import { machineId } from '../machine-id.js';
6
+ import { declaringDevices, profileRegistry, } from './registry.js';
5
7
  import { findBrowserPath, findFirstInstalledBrowser, isPortInUse } from './chrome.js';
6
8
  import { DEFAULT_VIEWPORT } from './devices.js';
9
+ export { declaringDevices, profileKind, profileRegistry, } from './registry.js';
7
10
  /**
8
11
  * Name of the profile `ensureDefaultBrowserProfile` auto-detects and pins.
9
12
  *
@@ -29,25 +32,6 @@ export const LEGACY_DEFAULT_BROWSER_PROFILE_NAME = 'default';
29
32
  * wins, so no existing config breaks.
30
33
  */
31
34
  export const DEFAULT_PROFILE_ALIAS = 'default';
32
- /**
33
- * True for a profile whose contents only make sense on THIS machine, so it must
34
- * live in the per-machine doc rather than the fleet-synced agents.yaml.
35
- *
36
- * The auto-detected `default` always qualifies: it is generated by probing local
37
- * browser installs and a free local port, so its `binary:` is an absolute path
38
- * for one OS. Shared fleet-wide it caused a permanent rewrite loop — a macOS box
39
- * wrote `/Applications/...`, a Linux box rewrote `/usr/bin/chromium-browser`,
40
- * each on every `agents browser start`.
41
- *
42
- * As of RUSH-2716 it is not the only one: EVERY newly created profile is
43
- * machine-local unless the caller explicitly asks for the fleet-synced store
44
- * (`createProfile(p, { fleet: true })` / `--fleet`). See {@link createProfile}.
45
- * This predicate is what keeps an existing `default` that an older version wrote
46
- * into central from being written back there by {@link updateProfile}.
47
- */
48
- function isMachineLocalProfile(name) {
49
- return name === DEFAULT_BROWSER_PROFILE_NAME || name === LEGACY_DEFAULT_BROWSER_PROFILE_NAME;
50
- }
51
35
  /**
52
36
  * The device-local configured default profile name (set via
53
37
  * `agents browser use`), or undefined when unset. When set, it
@@ -82,7 +66,7 @@ export function getProfileDownloadsDir(name) {
82
66
  export function getProfileSessionsDir(name, task) {
83
67
  return path.join(getProfileRuntimeDir(name), 'sessions', task);
84
68
  }
85
- function configToProfile(name, config) {
69
+ function configToProfile(name, config, devices = []) {
86
70
  validateRemoteBrowserBinaries(config);
87
71
  return {
88
72
  name,
@@ -98,6 +82,7 @@ function configToProfile(name, config) {
98
82
  viewport: config.viewport,
99
83
  logDir: config.logDir,
100
84
  logHost: config.logHost,
85
+ devices,
101
86
  };
102
87
  }
103
88
  function profileToConfig(profile) {
@@ -128,38 +113,24 @@ function profileToConfig(profile) {
128
113
  config.logHost = profile.logHost;
129
114
  return config;
130
115
  }
131
- /**
132
- * Every profile visible on this machine: the fleet-shared ones plus this
133
- * machine's own. A machine-local entry WINS on a name collision — it was written
134
- * by this box for this box, whereas the shared copy may carry another OS's
135
- * binary path (that mismatch is exactly why `default` moved local).
136
- */
137
- function allProfileConfigs() {
138
- const meta = readMeta();
139
- return { ...meta.browser, ...meta.deviceBrowser };
116
+ function selectedDeclaration(declarations) {
117
+ return declarations.find((declaration) => declaration.device === machineId()) ?? declarations[0];
140
118
  }
141
- export async function listProfiles() {
142
- const configs = allProfileConfigs();
143
- return Object.entries(configs).map(([name, config]) => configToProfile(name, config));
119
+ function localDeclaration(name) {
120
+ return profileRegistry().get(name)?.find((declaration) => declaration.device === machineId());
144
121
  }
145
- /**
146
- * Every visible profile paired with the store it lives in. Same precedence as
147
- * {@link listProfiles} — a machine-local entry wins a name collision, so a name
148
- * present in both maps reports `local`, which is the copy that is actually used.
149
- */
150
- export async function listProfilesWithScope() {
151
- const meta = readMeta();
152
- const local = meta.deviceBrowser ?? {};
153
- return Object.entries(allProfileConfigs()).map(([name, config]) => ({
154
- profile: configToProfile(name, config),
155
- scope: (local[name] ? 'local' : 'fleet'),
156
- }));
122
+ export async function listProfiles() {
123
+ return [...profileRegistry()].map(([name, declarations]) => {
124
+ const selected = selectedDeclaration(declarations);
125
+ return configToProfile(name, selected.config, declarations.map((declaration) => declaration.device));
126
+ });
157
127
  }
158
128
  export async function getProfile(name) {
159
- const config = allProfileConfigs()[name];
160
- if (!config)
129
+ const declarations = profileRegistry().get(name);
130
+ if (!declarations?.length)
161
131
  return null;
162
- return configToProfile(name, config);
132
+ const selected = selectedDeclaration(declarations);
133
+ return configToProfile(name, selected.config, declarations.map((declaration) => declaration.device));
163
134
  }
164
135
  /**
165
136
  * True when `profile` can actually launch or attach on THIS machine.
@@ -183,26 +154,6 @@ export function isProfileLaunchableHere(profile) {
183
154
  return false;
184
155
  }
185
156
  }
186
- /**
187
- * Resolve the profile `agents browser start` uses when no `--profile` is given.
188
- *
189
- * Order: (1) the device-local configured default (`agents browser profiles
190
- * set-default <name>`) when it names an existing profile that can launch here;
191
- * (2) an existing `default` profile that can launch here; (3) auto-pick the
192
- * first installed Chromium-family browser per the platform priority list in
193
- * chrome.ts (macOS: chrome > brave > edge > chromium > comet > arc; Linux: chrome >
194
- * chromium > brave > edge; Windows: edge > chrome > brave > comet) and pin a
195
- * `default` profile to it. Throws an actionable error if none of those binaries
196
- * are installed.
197
- *
198
- * "Can launch here" matters because `agents.yaml` syncs across the fleet: a
199
- * `default` auto-created on macOS carries a `/Applications/...` binary path that
200
- * doesn't exist on a Linux box. Rather than hand that broken profile back (the
201
- * top browser roadblock — "Custom binary not found"), we re-detect and
202
- * regenerate the `default` for this machine. A configured or existing default
203
- * that no longer exists, or can't launch here, warns and falls through — never a
204
- * hard fail.
205
- */
206
157
  /**
207
158
  * The auto-detected profile this machine already carries, under either name —
208
159
  * the current `auto-chrome` or the `default` an older build wrote. Callers that
@@ -210,8 +161,14 @@ export function isProfileLaunchableHere(profile) {
210
161
  * name, or a machine that predates the RUSH-2709 rename grows a second one.
211
162
  */
212
163
  export async function getAutoDetectedProfile() {
213
- return ((await getProfile(DEFAULT_BROWSER_PROFILE_NAME)) ??
214
- (await getProfile(LEGACY_DEFAULT_BROWSER_PROFILE_NAME)));
164
+ const current = machineId();
165
+ const registry = profileRegistry();
166
+ for (const name of [DEFAULT_BROWSER_PROFILE_NAME, LEGACY_DEFAULT_BROWSER_PROFILE_NAME]) {
167
+ const declaration = registry.get(name)?.find((entry) => entry.device === current);
168
+ if (declaration)
169
+ return configToProfile(name, declaration.config, [current]);
170
+ }
171
+ return null;
215
172
  }
216
173
  /**
217
174
  * Resolve what a caller typed after `--profile` to a real, BARE profile name.
@@ -262,15 +219,16 @@ export async function resolveProfileRef(ref) {
262
219
  * `start`'s resolver. Same order as {@link resolveProfileRef} for an explicit
263
220
  * name, but the IMPLICIT path (no `--profile`, or the bare `default` alias with
264
221
  * no profile of that name) goes through {@link ensureDefaultBrowserProfile} —
265
- * which additionally verifies the resolved default can launch on THIS machine,
266
- * warns and falls through when it can't, and regenerates or creates one.
222
+ * which additionally verifies the resolved default can launch on THIS machine.
223
+ * An undeclared configured default is an error. A declared default whose
224
+ * browser isn't installed here warns and falls through to auto-detect.
267
225
  *
268
226
  * `start` is the only command that launches a browser, so it is the only one
269
227
  * that may do those things; routing a filter-only command through this would
270
228
  * warn about, and rewrite, config the user never asked it to touch.
271
229
  *
272
- * Throws when no profile exists and no supported browser is installed
273
- * `ensureDefaultBrowserProfile`'s actionable "install one of …" error.
230
+ * Throws when the configured default is undeclared, or when no profile exists
231
+ * and no supported browser is installed.
274
232
  */
275
233
  export async function resolveProfileRefForStart(ref) {
276
234
  if (ref && ref !== DEFAULT_PROFILE_ALIAS)
@@ -280,25 +238,51 @@ export async function resolveProfileRefForStart(ref) {
280
238
  return ref;
281
239
  return (await ensureDefaultBrowserProfile()).name;
282
240
  }
241
+ /**
242
+ * Resolve the profile a bare `agents browser start` uses.
243
+ *
244
+ * Order: (1) the device-local configured default (`agents browser use <name>`)
245
+ * when it names a profile that exists and can launch here; (2) an existing
246
+ * auto-detected profile that can launch here; (3) auto-pick the first installed
247
+ * Chromium-family browser and pin `auto-chrome` (or repair a stale legacy
248
+ * `default`) to it.
249
+ *
250
+ * Two failure modes at the configured-default step are not the same:
251
+ * - No device declares the name (including a leftover central `browser:`
252
+ * entry that was never claimed) → throw. Auto-creating `auto-chrome` would
253
+ * hand the agent a logged-out browser while `browser.profile` still names
254
+ * the credentialed one.
255
+ * - The name is declared, but its browser/binary is not installed HERE →
256
+ * warn and fall through. That is a missing binary on this box, not a
257
+ * missing identity; auto-detect is the existing repair.
258
+ */
283
259
  export async function ensureDefaultBrowserProfile() {
284
260
  const configured = getConfiguredDefaultProfileName();
285
261
  if (configured && configured !== DEFAULT_PROFILE_ALIAS) {
286
262
  const chosen = await getProfile(configured);
287
263
  if (chosen && isProfileLaunchableHere(chosen))
288
264
  return chosen;
289
- console.warn(chosen
290
- ? `warning: configured default browser profile "${configured}" can't launch on this ` +
291
- `machine (its browser/binary isn't installed here); falling back to auto-detect. ` +
292
- `Fix with: agents browser use <name> (or --unset)`
293
- : `warning: configured default browser profile "${configured}" no longer exists; ` +
294
- `falling back to auto-detect. Fix with: agents browser use <name> (or --unset)`);
265
+ if (!chosen) {
266
+ const central = readMeta().browser ?? {};
267
+ if (central[configured]) {
268
+ throw new Error(`configured default browser profile "${configured}" is not declared by any device. ` +
269
+ `It still lives in the central agents.yaml browser: map. ` +
270
+ `Claim it on the machine that hosts that browser with: agents browser profiles claim ${configured}`);
271
+ }
272
+ throw new Error(`configured default browser profile "${configured}" is not declared by any device. ` +
273
+ `Create it with: agents browser profiles create ${configured} --browser <chrome|comet|chromium|brave|edge|arc|custom> ` +
274
+ `(or, if it is a leftover central profile, claim it with: agents browser profiles claim ${configured}). ` +
275
+ `Or unset the default with: agents browser use --unset`);
276
+ }
277
+ console.warn(`warning: configured default browser profile "${configured}" can't launch on this ` +
278
+ `machine (its browser/binary isn't installed here); falling back to auto-detect. ` +
279
+ `Fix with: agents browser use <name> (or --unset)`);
295
280
  }
296
281
  // Prefer whichever auto-detected profile this machine already carries: the
297
282
  // current `auto-chrome`, else the `default` an older build wrote. Reusing the
298
283
  // legacy one is what keeps a currently-running browser (and its runtime dirs,
299
284
  // keyed `default@endpoint-0`) attached after the rename.
300
- const existing = (await getProfile(DEFAULT_BROWSER_PROFILE_NAME)) ??
301
- (await getProfile(LEGACY_DEFAULT_BROWSER_PROFILE_NAME));
285
+ const existing = await getAutoDetectedProfile();
302
286
  if (existing && isProfileLaunchableHere(existing))
303
287
  return existing;
304
288
  const detected = findFirstInstalledBrowser();
@@ -435,10 +419,13 @@ export function assertLocalPortFree(profile, opts = {}) {
435
419
  const wanted = effectiveLocalPort(profile);
436
420
  if (wanted === undefined)
437
421
  return;
438
- for (const [existingName, existingConfig] of Object.entries(allProfileConfigs())) {
422
+ for (const [existingName, declarations] of profileRegistry()) {
439
423
  if (existingName === opts.ignore)
440
424
  continue;
441
- const existingProfile = configToProfile(existingName, existingConfig);
425
+ const declaration = declarations.find((entry) => entry.device === machineId());
426
+ if (!declaration)
427
+ continue;
428
+ const existingProfile = configToProfile(existingName, declaration.config, [machineId()]);
442
429
  if (effectiveLocalPort(existingProfile) === wanted) {
443
430
  throw new Error(`Local port ${wanted} is already used by profile "${existingName}". ` +
444
431
  `Each profile must own a unique local port (SSH tunnels now bind ` +
@@ -446,28 +433,10 @@ export function assertLocalPortFree(profile, opts = {}) {
446
433
  }
447
434
  }
448
435
  }
449
- /**
450
- * Create a profile. **It lands in this machine's own store by default** — pass
451
- * `{ fleet: true }` (the CLI's `--fleet`) to put it in the synced `agents.yaml`
452
- * where every box will see it.
453
- *
454
- * Local-by-default is the RUSH-2716 fix. A profile is a local browser install
455
- * (`binary:` is an absolute path for one OS) bound to a locally chosen free port,
456
- * so the fleet-synced copy is wrong on every other machine to begin with; and
457
- * because agents mint throwaway profiles freely, every ad-hoc one was syncing to
458
- * every machine as junk that nothing cleaned up (`agents browser profiles prune`
459
- * is the other half of that fix). Opting in with `--fleet` is for a profile that
460
- * really is fleet config — a remote `ssh://` endpoint, or a named profile whose
461
- * shape you want on every box.
462
- *
463
- * Existing profiles are NOT migrated: this only decides where a NEW entry is
464
- * written, so a fleet profile created before this change keeps syncing.
465
- */
466
- export async function createProfile(profile, opts = {}) {
467
- const meta = readMeta();
468
- const existingConfigs = allProfileConfigs();
469
- if (existingConfigs[profile.name]) {
470
- throw new Error(`Profile "${profile.name}" already exists`);
436
+ /** Declare a profile on this device. The same name may be declared by peers. */
437
+ export async function createProfile(profile) {
438
+ if (localDeclaration(profile.name)) {
439
+ throw new Error(`Profile "${profile.name}" already exists on ${machineId()}`);
471
440
  }
472
441
  assertLocalPortFree(profile);
473
442
  // Resolve the browser binary at create time. Fails fast with an actionable
@@ -482,69 +451,23 @@ export async function createProfile(profile, opts = {}) {
482
451
  findBrowserPath(profile.browser, profile.binary);
483
452
  }
484
453
  const config = profileToConfig(profile);
485
- // Machine-local unless the caller explicitly asked for the fleet store. The
486
- // auto `default` is always local regardless of what a caller passes — its
487
- // binary path and port are this box's, and fleet-syncing it is the rewrite
488
- // loop RUSH-2161 fixed.
489
- if (!opts.fleet || isMachineLocalProfile(profile.name)) {
490
- updateMeta((m) => ({ ...m, deviceBrowser: { ...m.deviceBrowser, [profile.name]: config } }));
491
- return;
492
- }
493
- updateMeta((m) => ({ ...m, browser: { ...m.browser, [profile.name]: config } }));
454
+ updateMeta((meta) => ({
455
+ ...meta,
456
+ deviceBrowser: { ...meta.deviceBrowser, [profile.name]: config },
457
+ }));
494
458
  }
495
459
  export async function updateProfile(profile) {
496
- const meta = readMeta();
497
- const local = meta.deviceBrowser?.[profile.name];
498
- if (!local && !meta.browser?.[profile.name]) {
499
- throw new Error(`Profile "${profile.name}" does not exist`);
460
+ const local = localDeclaration(profile.name);
461
+ if (!local) {
462
+ const devices = declaringDevices(profile.name);
463
+ const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
464
+ throw new Error(`Profile "${profile.name}" is not declared on ${machineId()}${where}`);
500
465
  }
501
466
  const config = profileToConfig(profile);
502
- // A profile that already lives machine-locally stays there; so does the auto
503
- // `default`, whose binary path and port are specific to this machine.
504
- if (local || isMachineLocalProfile(profile.name)) {
505
- updateMeta((m) => ({ ...m, deviceBrowser: { ...m.deviceBrowser, [profile.name]: config } }));
506
- return;
507
- }
508
- updateMeta((m) => ({ ...m, browser: { ...m.browser, [profile.name]: config } }));
509
- }
510
- /**
511
- * Flag a fleet-synced profile whose contents only make sense on ONE machine.
512
- *
513
- * A `cdp://` endpoint on loopback always connects on the machine EVALUATING the
514
- * profile, so a fleet-scope copy does not mean "that browser" — it means "port N
515
- * on whichever box ran the command". The name then silently resolves to a
516
- * different, usually logged-out browser on every other machine, which is the
517
- * worst possible failure for a profile that carries live logins.
518
- *
519
- * Reported by `profiles doctor` (a failing `scope` check) and noted in the
520
- * `kept` reason `profiles prune` prints for a fleet profile. The repair is
521
- * {@link moveProfileScope}, never an automatic rewrite of the synced doc.
522
- */
523
- export function misfiledFleetProfile(profile, scope) {
524
- if (scope !== 'fleet')
525
- return { misfiled: false };
526
- // Judge the endpoint the profile would actually CONNECT on, resolving
527
- // `defaultEndpoint` exactly as effectiveLocalPort does. Scanning every preset
528
- // instead would fail a legitimate fleet profile that defaults to `ssh://` and
529
- // merely carries a loopback alternate for local debugging.
530
- const presets = getEndpointPresets(profile);
531
- const chosen = profile.defaultEndpoint && presets[profile.defaultEndpoint]
532
- ? profile.defaultEndpoint
533
- : Object.keys(presets)[0];
534
- if (!chosen)
535
- return { misfiled: false };
536
- const loopback = presets[chosen].target;
537
- if (!loopback.startsWith('cdp://'))
538
- return { misfiled: false };
539
- const parsed = parseEndpointUrl(loopback);
540
- if (!parsed || !isLocalHost(parsed.host))
541
- return { misfiled: false };
542
- return {
543
- misfiled: true,
544
- why: `fleet-synced but "${loopback}" is a loopback endpoint — on every other machine ` +
545
- `this name resolves to that box's own port, not this browser. ` +
546
- `Repair on the machine that owns it: agents browser profiles scope ${profile.name} local`,
547
- };
467
+ updateMeta((meta) => ({
468
+ ...meta,
469
+ deviceBrowser: { ...meta.deviceBrowser, [profile.name]: config },
470
+ }));
548
471
  }
549
472
  /**
550
473
  * Merge `patch` onto the stored profile and persist it in the store it already
@@ -556,13 +479,14 @@ export function misfiledFleetProfile(profile, scope) {
556
479
  * with this profile excluded so an unchanged port is not a self-collision.
557
480
  */
558
481
  export async function editProfile(name, patch) {
559
- const meta = readMeta();
560
- const local = meta.deviceBrowser?.[name];
561
- const fleet = meta.browser?.[name];
562
- if (!local && !fleet) {
563
- throw new Error(`Profile "${name}" does not exist`);
564
- }
565
- const current = configToProfile(name, (local ?? fleet));
482
+ const local = localDeclaration(name);
483
+ if (!local) {
484
+ const devices = declaringDevices(name);
485
+ const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
486
+ throw new Error(`Profile "${name}" is not declared on ${machineId()}${where}`);
487
+ }
488
+ const devices = declaringDevices(name);
489
+ const current = configToProfile(name, local.config, devices);
566
490
  const merged = { ...current, ...patch, name };
567
491
  const changed = Object.keys(patch).filter((k) => JSON.stringify(current[k]) !== JSON.stringify(merged[k]));
568
492
  // A target filter only means anything for an Electron app, and the gate must
@@ -580,27 +504,12 @@ export async function editProfile(name, patch) {
580
504
  findBrowserPath(merged.browser, merged.binary);
581
505
  }
582
506
  await updateProfile(merged);
583
- const landedLocal = !!local || isMachineLocalProfile(name);
584
507
  return {
585
508
  profile: merged,
586
- scope: landedLocal ? 'local' : 'fleet',
509
+ devices,
587
510
  changed,
588
- fleetCopyLeftStale: landedLocal && !local && !!fleet ? true : undefined,
589
511
  };
590
512
  }
591
- /**
592
- * Move an existing profile between the fleet-synced store and this machine's
593
- * device store.
594
- *
595
- * `fleet -> local` is the repair for a profile whose endpoint is machine-bound
596
- * (a loopback `cdp://`) but which was created with `--fleet`: the name resolves
597
- * to a DIFFERENT browser on every box, because `cdp:` always connects on the
598
- * machine evaluating it. Run it ON the machine that owns the browser — the copy
599
- * that survives is the one that works.
600
- *
601
- * Deliberately not automatic. Rewriting the user's synced `agents.yaml` from a
602
- * heuristic is the rewrite loop RUSH-2161 fixed; this is an explicit verb.
603
- */
604
513
  /** Profile names an agent has to be able to type and an fs path can hold. */
605
514
  const PROFILE_NAME_RE = /^[a-z][a-z0-9-]*$/;
606
515
  /**
@@ -647,12 +556,13 @@ export function assertRegistrableProfileName(name) {
647
556
  export async function renameProfile(from, to) {
648
557
  if (from === to)
649
558
  throw new Error(`"${from}" is already its own name.`);
650
- const meta = readMeta();
651
- const local = meta.deviceBrowser?.[from];
652
- const fleet = meta.browser?.[from];
653
- if (!local && !fleet)
654
- throw new Error(`Profile "${from}" does not exist`);
655
- if (allProfileConfigs()[to])
559
+ const local = localDeclaration(from);
560
+ if (!local) {
561
+ const devices = declaringDevices(from);
562
+ const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
563
+ throw new Error(`Profile "${from}" is not declared on ${machineId()}${where}`);
564
+ }
565
+ if (profileRegistry().has(to))
656
566
  throw new Error(`Profile "${to}" already exists`);
657
567
  assertRegistrableProfileName(to);
658
568
  const { isProfileInUse, listProfileCacheDirs } = await import('./runtime-state.js');
@@ -660,26 +570,8 @@ export async function renameProfile(from, to) {
660
570
  throw new Error(`"${from}" is in use (live browser, tunnel, or open task). Renaming would move its ` +
661
571
  `user-data-dir out from under the running browser. Stop it first: agents browser stop --profile ${from}`);
662
572
  }
663
- // A name can exist in BOTH stores (local wins in allProfileConfigs). Rewriting
664
- // only one leaves the other listed under the OLD name with its data dir already
665
- // moved away — the abandoned-`--user-data-dir` state this command exists to
666
- // prevent, produced by this command, and previously reported as a clean
667
- // success. Refuse instead: which copy the user means is genuinely ambiguous,
668
- // and `profiles scope` is the tool for collapsing the duplicate first.
669
- if (local && fleet) {
670
- // `scope <name> fleet`, NOT local: moveProfileScope computes `from` as
671
- // 'local' whenever a local entry exists, so asking for local is a no-op that
672
- // reports success and leaves the duplicate. Sending the user there and then
673
- // to `profiles delete` — which drops the cached runtime dirs by default —
674
- // walked a data-preservation command into a data-destroying one.
675
- throw new Error(`"${from}" exists in BOTH this machine's store and the fleet-synced one. ` +
676
- `Renaming would move its browser data while leaving one copy behind under the old name.\n` +
677
- ` Collapse the duplicate first: agents browser profiles scope ${from} fleet\n` +
678
- ` Then rename. (To drop a copy instead, agents browser profiles delete ${from} --keep-cache ` +
679
- `keeps the browser data.)`);
680
- }
681
- const config = (local ?? fleet);
682
- const scope = local ? 'local' : 'fleet';
573
+ const config = local.config;
574
+ const devices = declaringDevices(from);
683
575
  // Move the on-disk state BEFORE the config, so a crash between the two leaves
684
576
  // a profile whose dirs are already where the new name expects them rather than
685
577
  // a config pointing at dirs that no longer exist.
@@ -703,20 +595,11 @@ export async function renameProfile(from, to) {
703
595
  fs.renameSync(dir, dest);
704
596
  movedDirs.push(dest);
705
597
  }
706
- if (scope === 'local') {
707
- updateMeta((m) => {
708
- const next = { ...m.deviceBrowser };
709
- delete next[from];
710
- return { ...m, deviceBrowser: { ...next, [to]: config } };
711
- });
712
- }
713
- else {
714
- updateMeta((m) => {
715
- const next = { ...m.browser };
716
- delete next[from];
717
- return { ...m, browser: { ...next, [to]: config } };
718
- });
719
- }
598
+ updateMeta((meta) => {
599
+ const next = { ...meta.deviceBrowser };
600
+ delete next[from];
601
+ return { ...meta, deviceBrowser: { ...next, [to]: config } };
602
+ });
720
603
  // Both pointers are separate keys. Leaving `browser.profile` behind falls back
721
604
  // to auto-detect on the next `browser start`; leaving `browser.viewer` behind
722
605
  // sends every artifact back to the OS default handler — which is the exact bug
@@ -732,68 +615,24 @@ export async function renameProfile(from, to) {
732
615
  setConfigValue('browser.viewer', to);
733
616
  repointedViewer = true;
734
617
  }
735
- // A fleet rename changes the name on every machine, but each machine's own
736
- // config doc is separate a peer pinning the old name is left dangling. Not
737
- // rewritten from here: that would be a cross-machine mutation, and on a peer
738
- // the pin may be correct (a LOCAL profile of the same name). Reported instead.
618
+ // A rename changes only this device's declaration. If it was the only
619
+ // declaration, peer pins become stale and are reported rather than rewritten.
739
620
  const { devicesPinningBrowserProfile } = await import('../device-config.js');
740
- const { machineId } = await import('../machine-id.js');
741
- const stalePins = scope === 'fleet'
621
+ const stalePins = devices.length === 1
742
622
  ? devicesPinningBrowserProfile(from).filter((p) => p.device !== machineId())
743
623
  : [];
744
- return { scope, movedDirs, repointedDefault, repointedViewer, stalePins };
745
- }
746
- export async function moveProfileScope(name, to) {
747
- const meta = readMeta();
748
- const local = meta.deviceBrowser?.[name];
749
- const fleet = meta.browser?.[name];
750
- if (!local && !fleet) {
751
- throw new Error(`Profile "${name}" does not exist`);
752
- }
753
- const from = local ? 'local' : 'fleet';
754
- if (from === to)
755
- return { from, to };
756
- if (to === 'fleet' && isMachineLocalProfile(name)) {
757
- throw new Error(`"${name}" is always machine-local — its binary path and port are this box's. ` +
758
- `Fleet-syncing it is the rewrite loop RUSH-2161 fixed.`);
759
- }
760
- const config = (local ?? fleet);
761
- // Write the destination BEFORE dropping the source, so a crash between the two
762
- // leaves a duplicate (harmless — local wins in allProfileConfigs) rather than
763
- // no profile at all.
764
- if (to === 'local') {
765
- updateMeta((m) => ({ ...m, deviceBrowser: { ...m.deviceBrowser, [name]: config } }));
766
- updateMeta((m) => {
767
- const next = { ...m.browser };
768
- delete next[name];
769
- return { ...m, browser: Object.keys(next).length > 0 ? next : undefined };
770
- });
771
- }
772
- else {
773
- updateMeta((m) => ({ ...m, browser: { ...m.browser, [name]: config } }));
774
- updateMeta((m) => {
775
- const next = { ...m.deviceBrowser };
776
- delete next[name];
777
- return { ...m, deviceBrowser: Object.keys(next).length > 0 ? next : undefined };
778
- });
779
- }
780
- return { from, to };
624
+ return { devices, movedDirs, repointedDefault, repointedViewer, stalePins };
781
625
  }
782
626
  export async function deleteProfile(name) {
783
- const meta = readMeta();
784
- const isLocal = !!meta.deviceBrowser?.[name];
785
- if (!isLocal && !meta.browser?.[name]) {
786
- throw new Error(`Profile "${name}" does not exist`);
787
- }
788
- updateMeta((m) => {
789
- if (isLocal) {
790
- const next = { ...m.deviceBrowser };
791
- delete next[name];
792
- return { ...m, deviceBrowser: Object.keys(next).length > 0 ? next : undefined };
793
- }
794
- const next = { ...m.browser };
627
+ if (!localDeclaration(name)) {
628
+ const devices = declaringDevices(name);
629
+ const where = devices.length > 0 ? `; declared on ${devices.join(', ')}` : '';
630
+ throw new Error(`Profile "${name}" is not declared on ${machineId()}${where}`);
631
+ }
632
+ updateMeta((meta) => {
633
+ const next = { ...meta.deviceBrowser };
795
634
  delete next[name];
796
- return { ...m, browser: Object.keys(next).length > 0 ? next : undefined };
635
+ return { ...meta, deviceBrowser: Object.keys(next).length > 0 ? next : undefined };
797
636
  });
798
637
  }
799
638
  /** Widest NAME column we will grow to before truncating instead. */
@@ -828,13 +667,10 @@ export function padColumn(text, width) {
828
667
  * a leading column, explained by a legend line — so the name column carries just
829
668
  * the name and `default` in it always means the profile of that name.
830
669
  *
831
- * The SCOPE column is the RUSH-2716 companion: it says whether an entry lives on
832
- * this machine only (`local`) or in the fleet-synced `agents.yaml` (`fleet`), so
833
- * "why does this box have a profile I never made here?" is answerable at a
834
- * glance.
670
+ * WHERE lists the devices whose own files declare the name.
835
671
  */
836
672
  export function formatProfilesTable(rows, configuredDefault) {
837
- const cells = rows.map(({ profile, scope }) => {
673
+ const cells = rows.map((profile) => {
838
674
  const presets = getEndpointPresets(profile);
839
675
  const endpoints = Object.entries(presets)
840
676
  .map(([name, ep]) => (name.startsWith('endpoint-') ? ep.target : `${name}=${ep.target}`))
@@ -843,42 +679,35 @@ export function formatProfilesTable(rows, configuredDefault) {
843
679
  marker: profile.name === configuredDefault ? '*' : ' ',
844
680
  name: profile.name,
845
681
  browser: profile.browser || '-',
846
- scope,
682
+ where: profile.devices.join(', '),
847
683
  description: profile.description ?? '',
848
684
  endpoints,
849
685
  };
850
686
  });
851
687
  const nameWidth = Math.min(MAX_NAME_COLUMN, Math.max('NAME'.length, ...cells.map((c) => c.name.length)));
852
688
  const browserWidth = Math.max('BROWSER'.length, ...cells.map((c) => c.browser.length));
853
- // 'local' / 'fleet' are both 5 chars, so the header is the widest value.
854
- const scopeWidth = 'SCOPE'.length;
689
+ const whereWidth = Math.min(36, Math.max('WHERE'.length, ...cells.map((c) => c.where.length)));
855
690
  const hasDescriptions = cells.some((c) => c.description);
856
691
  const descWidth = hasDescriptions
857
692
  ? Math.min(36, Math.max('DESCRIPTION'.length, ...cells.map((c) => c.description.length)))
858
693
  : 0;
859
- const row = (marker, name, browser, scope, desc, endpoints) => `${marker} ` +
694
+ const row = (marker, name, browser, where, desc, endpoints) => `${marker} ` +
860
695
  padColumn(name, nameWidth) + ' ' +
861
696
  padColumn(browser, browserWidth) + ' ' +
862
- padColumn(scope, scopeWidth) + ' ' +
697
+ padColumn(where, whereWidth) + ' ' +
863
698
  (hasDescriptions ? padColumn(desc, descWidth) + ' ' : '') +
864
699
  endpoints;
865
- const header = row(' ', 'NAME', 'BROWSER', 'SCOPE', 'DESCRIPTION', 'ENDPOINTS');
700
+ const header = row(' ', 'NAME', 'BROWSER', 'WHERE', 'DESCRIPTION', 'ENDPOINTS');
866
701
  const lines = [header, '-'.repeat(header.length)];
867
702
  for (const c of cells) {
868
- lines.push(row(c.marker, c.name, c.browser, c.scope, c.description, c.endpoints));
703
+ lines.push(row(c.marker, c.name, c.browser, c.where, c.description, c.endpoints));
869
704
  }
870
705
  if (configuredDefault) {
871
706
  lines.push('');
872
707
  lines.push(`* = this machine's default profile (${configuredDefault}) — what a bare \`agents browser start\` uses.`);
873
708
  }
874
- lines.push(hasFleetRow(rows)
875
- ? 'scope: local = this machine only · fleet = synced to every machine (agents.yaml).'
876
- : 'scope: local = this machine only. Create a synced one with: agents browser profiles create <name> --fleet');
877
709
  return lines;
878
710
  }
879
- function hasFleetRow(rows) {
880
- return rows.some((r) => r.scope === 'fleet');
881
- }
882
711
  /**
883
712
  * Resolve a profile's endpoint presets into a normalized map regardless of
884
713
  * whether the YAML uses the legacy `string[]` shape or the new map shape.