@phnx-labs/agents-cli 1.22.46 → 1.22.47

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 (167) hide show
  1. package/CHANGELOG.md +279 -0
  2. package/LICENSE +102 -182
  3. package/README.md +31 -18
  4. package/dist/cli/command-registry.d.ts +1 -1
  5. package/dist/cli/command-registry.js +4 -2
  6. package/dist/commands/accounts.js +56 -12
  7. package/dist/commands/auth.js +7 -5
  8. package/dist/commands/browser-sessions-picker.js +9 -7
  9. package/dist/commands/browser.js +266 -15
  10. package/dist/commands/config.js +26 -4
  11. package/dist/commands/cost.d.ts +1 -1
  12. package/dist/commands/detach.d.ts +13 -0
  13. package/dist/commands/detach.js +11 -8
  14. package/dist/commands/exec.js +55 -38
  15. package/dist/commands/feed-watch.d.ts +2 -0
  16. package/dist/commands/feed-watch.js +28 -0
  17. package/dist/commands/feed.js +27 -0
  18. package/dist/commands/feedback.js +3 -19
  19. package/dist/commands/insights.d.ts +1 -1
  20. package/dist/commands/insights.js +2 -2
  21. package/dist/commands/lease.js +3 -3
  22. package/dist/commands/routines.js +1 -1
  23. package/dist/commands/sessions-picker-factory.d.ts +2 -1
  24. package/dist/commands/sessions-picker-factory.js +1 -1
  25. package/dist/commands/sessions-stop.d.ts +24 -0
  26. package/dist/commands/sessions-stop.js +66 -0
  27. package/dist/commands/sessions-trace.d.ts +82 -0
  28. package/dist/commands/sessions-trace.js +387 -0
  29. package/dist/commands/sessions.js +4 -0
  30. package/dist/commands/ssh.d.ts +15 -0
  31. package/dist/commands/ssh.js +194 -16
  32. package/dist/commands/teams.js +34 -2
  33. package/dist/commands/view.d.ts +2 -0
  34. package/dist/commands/view.js +18 -31
  35. package/dist/commands/watchdog.js +9 -14
  36. package/dist/lib/account-capabilities.d.ts +4 -0
  37. package/dist/lib/account-capabilities.js +13 -9
  38. package/dist/lib/account-registry.d.ts +2 -0
  39. package/dist/lib/account-registry.js +30 -2
  40. package/dist/lib/accounting/rotate.d.ts +9 -0
  41. package/dist/lib/accounting/rotate.js +12 -2
  42. package/dist/lib/accounting/usage.d.ts +70 -3
  43. package/dist/lib/accounting/usage.js +235 -63
  44. package/dist/lib/agent-modes.js +29 -1
  45. package/dist/lib/agent-spec/agents.d.ts +4 -3
  46. package/dist/lib/agent-spec/agents.js +14 -6
  47. package/dist/lib/analytics/mix-commands.d.ts +1 -1
  48. package/dist/lib/analytics/mix-commands.js +2 -2
  49. package/dist/lib/auth-health.d.ts +13 -3
  50. package/dist/lib/auth-health.js +50 -8
  51. package/dist/lib/browser/devices.d.ts +19 -0
  52. package/dist/lib/browser/devices.js +23 -0
  53. package/dist/lib/browser/ipc.d.ts +1 -1
  54. package/dist/lib/browser/ipc.js +33 -7
  55. package/dist/lib/browser/profiles.d.ts +106 -1
  56. package/dist/lib/browser/profiles.js +300 -17
  57. package/dist/lib/browser/remote-control.d.ts +26 -2
  58. package/dist/lib/browser/remote-control.js +33 -3
  59. package/dist/lib/browser/runtime-state.d.ts +9 -0
  60. package/dist/lib/browser/runtime-state.js +24 -8
  61. package/dist/lib/browser/service.d.ts +37 -0
  62. package/dist/lib/browser/service.js +78 -20
  63. package/dist/lib/browser/sessions-list.d.ts +1 -3
  64. package/dist/lib/browser/sessions-list.js +3 -16
  65. package/dist/lib/browser/types.d.ts +12 -1
  66. package/dist/lib/codex-policy.d.ts +11 -1
  67. package/dist/lib/codex-policy.js +20 -5
  68. package/dist/lib/config-keys.d.ts +4 -3
  69. package/dist/lib/config-keys.js +17 -7
  70. package/dist/lib/daemon/runner.js +15 -2
  71. package/dist/lib/daemon-ticks.js +2 -2
  72. package/dist/lib/device-config.d.ts +25 -8
  73. package/dist/lib/device-config.js +90 -17
  74. package/dist/lib/devices/config-migration.d.ts +7 -0
  75. package/dist/lib/devices/config-migration.js +67 -3
  76. package/dist/lib/devices/health.d.ts +27 -2
  77. package/dist/lib/devices/health.js +39 -9
  78. package/dist/lib/devices/interactive-host.d.ts +15 -0
  79. package/dist/lib/devices/interactive-host.js +53 -0
  80. package/dist/lib/devices/registry.d.ts +61 -4
  81. package/dist/lib/devices/registry.js +94 -38
  82. package/dist/lib/devices/stats-cache.d.ts +6 -0
  83. package/dist/lib/devices/stats-cache.js +10 -2
  84. package/dist/lib/exec.d.ts +5 -3
  85. package/dist/lib/exec.js +32 -16
  86. package/dist/lib/feed/answer.d.ts +29 -0
  87. package/dist/lib/feed/answer.js +145 -0
  88. package/dist/lib/feed/attention.d.ts +83 -0
  89. package/dist/lib/feed/attention.js +0 -0
  90. package/dist/lib/feed/feed.d.ts +109 -0
  91. package/dist/lib/feed/feed.js +154 -3
  92. package/dist/lib/feed/pr-status.d.ts +14 -0
  93. package/dist/lib/feed/pr-status.js +47 -0
  94. package/dist/lib/feed/watch.d.ts +59 -0
  95. package/dist/lib/feed/watch.js +180 -0
  96. package/dist/lib/fleet/capture.js +6 -0
  97. package/dist/lib/fleet/remote-login.js +8 -3
  98. package/dist/lib/fleet/types.d.ts +17 -0
  99. package/dist/lib/harness/adapter.d.ts +11 -0
  100. package/dist/lib/harness/adapters/claude.js +38 -24
  101. package/dist/lib/harness/adapters/codex.js +2 -4
  102. package/dist/lib/hooks/cache.js +8 -0
  103. package/dist/lib/hooks/match.d.ts +3 -0
  104. package/dist/lib/hooks/match.js +25 -0
  105. package/dist/lib/hosts/passthrough.js +16 -0
  106. package/dist/lib/hosts/registry.js +7 -0
  107. package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
  108. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  109. package/dist/lib/menubar/install-menubar.d.ts +13 -0
  110. package/dist/lib/menubar/install-menubar.js +26 -2
  111. package/dist/lib/open-url.d.ts +60 -2
  112. package/dist/lib/open-url.js +167 -12
  113. package/dist/lib/refresh-coordinator.d.ts +1 -1
  114. package/dist/lib/refresh-coordinator.js +1 -1
  115. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  116. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
  117. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  118. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  119. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
  120. package/dist/lib/session/active.d.ts +67 -0
  121. package/dist/lib/session/active.js +69 -1
  122. package/dist/lib/session/db.js +4 -2
  123. package/dist/lib/session/discover.d.ts +2 -0
  124. package/dist/lib/session/discover.js +12 -6
  125. package/dist/lib/session/prompt.d.ts +35 -0
  126. package/dist/lib/session/prompt.js +56 -0
  127. package/dist/lib/session/remote/watch.d.ts +1 -1
  128. package/dist/lib/session/remote/watch.js +6 -1
  129. package/dist/lib/session/render.js +7 -3
  130. package/dist/lib/session/session-cache.js +5 -1
  131. package/dist/lib/session/trajectory-compare.d.ts +47 -0
  132. package/dist/lib/session/trajectory-compare.js +116 -0
  133. package/dist/lib/session/trajectory-html.d.ts +15 -0
  134. package/dist/lib/session/trajectory-html.js +750 -0
  135. package/dist/lib/session/trajectory-lineage.d.ts +92 -0
  136. package/dist/lib/session/trajectory-lineage.js +195 -0
  137. package/dist/lib/session/trajectory-text.d.ts +37 -0
  138. package/dist/lib/session/trajectory-text.js +277 -0
  139. package/dist/lib/session/trajectory.d.ts +102 -0
  140. package/dist/lib/session/trajectory.js +335 -0
  141. package/dist/lib/startup/command-registry.d.ts +3 -1
  142. package/dist/lib/startup/command-registry.js +5 -2
  143. package/dist/lib/state.d.ts +1 -1
  144. package/dist/lib/state.js +4 -2
  145. package/dist/lib/teams/agents.js +9 -0
  146. package/dist/lib/terminal/inject.d.ts +19 -0
  147. package/dist/lib/terminal/inject.js +19 -9
  148. package/dist/lib/tmux/session.d.ts +46 -13
  149. package/dist/lib/tmux/session.js +173 -27
  150. package/dist/lib/types.d.ts +16 -0
  151. package/dist/lib/usage-backoff.d.ts +2 -1
  152. package/dist/lib/usage-backoff.js +60 -19
  153. package/dist/lib/usage-refresh.d.ts +17 -3
  154. package/dist/lib/usage-refresh.js +57 -11
  155. package/dist/lib/watchdog/log.d.ts +1 -1
  156. package/dist/lib/watchdog/log.js +1 -1
  157. package/dist/lib/watchdog/runner.d.ts +36 -45
  158. package/dist/lib/watchdog/runner.js +150 -119
  159. package/dist/lib/watchdog/service.d.ts +0 -1
  160. package/dist/lib/watchdog/service.js +0 -1
  161. package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
  162. package/dist/lib/watchdog/watchdog-agent.js +59 -0
  163. package/dist/lib/watchdog/watchdog.d.ts +11 -2
  164. package/dist/lib/watchdog/watchdog.js +37 -104
  165. package/package.json +2 -2
  166. package/dist/commands/usage.d.ts +0 -27
  167. package/dist/commands/usage.js +0 -96
@@ -298,7 +298,7 @@ export const AGENTS = {
298
298
  cloudProvider: 'codex',
299
299
  // Subagents: multi-agent plumbing since 0.117.0; custom agents as
300
300
  // ~/.codex/agents/*.toml (name, description, developer_instructions).
301
- capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: { since: '0.138.0' }, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: { since: '0.117.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'skip'], interactiveRepl: true },
301
+ capabilities: { hooks: { since: '0.116.0' }, mcp: true, mcpHttp: true, mcpHeaders: false, allowlist: { since: '0.138.0' }, skills: true, commands: { until: '0.117.0' }, plugins: { since: '0.128.0' }, subagents: { since: '0.117.0' }, rules: { file: 'AGENTS.md' }, workflows: false, memory: true, modes: ['plan', 'edit', 'auto', 'skip'], interactiveRepl: true },
302
302
  },
303
303
  gemini: {
304
304
  id: 'gemini',
@@ -3050,15 +3050,23 @@ export function isAgentName(input) {
3050
3050
  return resolveAgentName(input) !== null;
3051
3051
  }
3052
3052
  /**
3053
- * Split a CLI-provided `<agent>[@<version>]` spec into its bare agent id and
3054
- * optional exact version token, the same way `agents run` does
3055
- * (commands/exec.ts parses its positional with a plain `split('@')`) — NOT the
3053
+ * Split a CLI-provided `<agent>[@<version>][#<label>]` spec into its bare agent
3054
+ * id, optional exact version token, and optional account label, the same way
3055
+ * `agents run` does — NOT the
3056
3056
  * agent-spec qualifier engine (`@latest`/`@all`), which enumerates installed
3057
3057
  * versions for read/diagnostic commands, not a single launch target. Returns an
3058
3058
  * error message rather than throwing so callers decide exit-vs-continue.
3059
3059
  */
3060
3060
  export function parseAgentVersionSpec(raw) {
3061
- const parts = raw.split('@');
3061
+ const labelParts = raw.split('#');
3062
+ if (labelParts.length > 2) {
3063
+ return { error: `Invalid agent spec '${raw}': at most one '#label' is allowed` };
3064
+ }
3065
+ const [versionSpec, rawLabel] = labelParts;
3066
+ if (rawLabel !== undefined && (rawLabel === '' || !/^[a-zA-Z0-9][a-zA-Z0-9@._+-]*$/.test(rawLabel))) {
3067
+ return { error: `Invalid account label '${rawLabel}' in '${raw}'` };
3068
+ }
3069
+ const parts = versionSpec.split('@');
3062
3070
  if (parts.length > 2) {
3063
3071
  return { error: `Invalid agent spec '${raw}': at most one '@version' is allowed` };
3064
3072
  }
@@ -3070,7 +3078,7 @@ export function parseAgentVersionSpec(raw) {
3070
3078
  if (rawVersion !== undefined && (rawVersion === '' || !VERSION_RE.test(rawVersion))) {
3071
3079
  return { error: `Invalid version '${rawVersion}' in '${raw}'` };
3072
3080
  }
3073
- return { agent, ...(rawVersion ? { version: rawVersion } : {}) };
3081
+ return { agent, ...(rawVersion ? { version: rawVersion } : {}), ...(rawLabel ? { label: rawLabel } : {}) };
3074
3082
  }
3075
3083
  /**
3076
3084
  * Build the deprecation notice lines for an agent, or null if it isn't
@@ -5,7 +5,7 @@
5
5
  * peer of `agents insights` with overlapping "analytics" meaning, so agents and
6
6
  * humans kept picking the wrong verb. The cheap counter path (sessions index +
7
7
  * usage.db) still exists — it is now `agents insights mix` and the recipe
8
- * subcommands below. Latency stays on `agents perf`; quota on `agents usage`.
8
+ * subcommands below. Latency stays on `agents perf`; quota on `agents view`.
9
9
  *
10
10
  * Former top-level `agents trends` is gone. The nested spelling
11
11
  * `agents insights trends` is an alias of `agents insights mix`.
@@ -5,7 +5,7 @@
5
5
  * peer of `agents insights` with overlapping "analytics" meaning, so agents and
6
6
  * humans kept picking the wrong verb. The cheap counter path (sessions index +
7
7
  * usage.db) still exists — it is now `agents insights mix` and the recipe
8
- * subcommands below. Latency stays on `agents perf`; quota on `agents usage`.
8
+ * subcommands below. Latency stays on `agents perf`; quota on `agents view`.
9
9
  *
10
10
  * Former top-level `agents trends` is gone. The nested spelling
11
11
  * `agents insights trends` is an alias of `agents insights mix`.
@@ -97,7 +97,7 @@ export function registerMixCommands(parent) {
97
97
  Session recipes read sessions.db; resource recipes read ~/.agents/.history/analytics/usage.db.
98
98
  Empty recipes are skipped on the default mix board.
99
99
  This is the cheap counter path. Behavioural report (transcript content, account split)
100
- is bare \`agents insights\`. Latency is \`agents perf\`; quota is \`agents usage\`.
100
+ is bare \`agents insights\`. Latency is \`agents perf\`; quota is \`agents view\`.
101
101
  Skill/slash-command popularity is \`agents sessions stats\`.
102
102
  `,
103
103
  });
@@ -148,13 +148,21 @@ export declare function mergeAuthHealthEntries(current: Record<string, AuthHealt
148
148
  export declare function writeAuthHealthEntries(entries: Record<string, AuthHealth>): void;
149
149
  /**
150
150
  * Complete a live auth probe for one (agent, home). For claude/kimi/droid this
151
- * hits the provider; for everyone else it reports a best-effort local verdict
152
- * (`unverified` when a credential is present, `unconfigured` otherwise) never
153
- * masquerading as `live`.
151
+ * hits the provider unless the account's usage cache already holds a fresh
152
+ * successful fetch, which is the same authenticated request and proves the
153
+ * token live without spending a second one (RUSH-3036). For everyone else it
154
+ * reports a best-effort local verdict (`unverified` when a credential is
155
+ * present, `unconfigured` otherwise) — never masquerading as `live`.
154
156
  */
155
157
  export declare function probeAuthHealth(agent: AgentId, home: string | undefined, opts?: {
156
158
  cliVersion?: string | null;
157
159
  info?: AccountInfo | null;
160
+ /**
161
+ * Skip the derived-from-usage shortcut and fire a real network probe
162
+ * (RUSH-3036). Set by `agents devices ping [--strict]`, whose contract is
163
+ * a genuinely live request that surfaces `revoked` immediately.
164
+ */
165
+ forceLive?: boolean;
158
166
  }): Promise<AuthHealth>;
159
167
  /** One probed install on a host. */
160
168
  export interface AuthProbeRow {
@@ -215,6 +223,8 @@ export declare function groupFleetAuthInstalls<T extends FleetAuthInstall>(insta
215
223
  export declare function probeLocalFleetAuth(opts?: {
216
224
  cliVersion?: string | null;
217
225
  agents?: readonly AgentId[];
226
+ /** Fire real network probes even when fresh usage evidence exists (RUSH-3036) — the `devices ping [--strict]` contract. */
227
+ forceLive?: boolean;
218
228
  }): Promise<AuthProbeRow[]>;
219
229
  /** Persist a host's probed rows into the cache (keyed by host+agent+version). */
220
230
  export declare function writeFleetAuthRows(host: string, rows: AuthProbeRow[]): void;
@@ -20,7 +20,7 @@ import * as fs from 'fs';
20
20
  import * as path from 'path';
21
21
  import { ALL_AGENT_IDS, getAccountInfo } from './agents.js';
22
22
  import { getCacheDir } from './state.js';
23
- import { probeClaudeStatus, probeDroidStatus, probeKimiStatus, USAGE_HEADLESS_SCOPE_MARKER, } from './accounting/usage.js';
23
+ import { probeClaudeStatus, probeDroidStatus, probeKimiStatus, USAGE_HEADLESS_SCOPE_MARKER, readClaudeUsageCache, } from './accounting/usage.js';
24
24
  import { getVersionHomePath, listInstalledVersions } from './installations/versions.js';
25
25
  import { atomicWriteFileSync, ensureLockTarget, withFileLock } from './fs-atomic.js';
26
26
  /** Agents with a live network probe wired up today. The rest are best-effort. */
@@ -294,22 +294,64 @@ export function writeAuthHealthEntries(entries) {
294
294
  // ---------------------------------------------------------------------------
295
295
  // The probe (writer side)
296
296
  // ---------------------------------------------------------------------------
297
+ /**
298
+ * A usage snapshot this recent is live proof the account's shared setup-token
299
+ * works: the snapshot only exists because an authenticated `/oauth/usage`
300
+ * request succeeded. Matches the periodic tick's own probe window
301
+ * (AUTH_PROBE_MAX_AGE_MS in daemon-ticks.ts — not imported to avoid a cycle;
302
+ * a drift here only widens/narrows evidence freshness, never correctness).
303
+ */
304
+ const FRESH_USAGE_VERDICT_MAX_AGE_MS = 20 * 60_000;
305
+ /**
306
+ * A `live` verdict derived from the account's usage cache instead of a second
307
+ * network request (RUSH-3036). The auth probe and the usage fetch hit the SAME
308
+ * rate-limited endpoint with the SAME fleet-shared setup-token, so a fresh
309
+ * successful usage snapshot already proves everything the probe would: paying a
310
+ * second request per account per box was half the fleet's endpoint load.
311
+ *
312
+ * Two guards keep the evidence honest (both review findings on the first cut):
313
+ * the caller must assert this box holds a LOCAL credential for the account
314
+ * (`signedIn`) — a fleet-imported snapshot proves the shared token works, not
315
+ * that THIS box can authenticate, so an unsigned home never derives `live`;
316
+ * and a `forceLive` caller (`agents devices ping --strict`) skips derivation
317
+ * entirely, because its contract is a real request that can surface `revoked`
318
+ * within seconds, not minutes. Returns null when there is no admissible fresh
319
+ * evidence — the caller then live-probes as before.
320
+ */
321
+ function verdictFromFreshUsage(usageKey, signedIn, now) {
322
+ if (!usageKey || !signedIn)
323
+ return null;
324
+ const snapshot = readClaudeUsageCache(usageKey);
325
+ const capturedAt = snapshot?.capturedAt?.getTime();
326
+ if (!capturedAt || now - capturedAt >= FRESH_USAGE_VERDICT_MAX_AGE_MS)
327
+ return null;
328
+ const ageMin = Math.max(1, Math.round((now - capturedAt) / 60_000));
329
+ return { verdict: 'live', checkedAt: now, detail: `token proven live by a usage fetch ${ageMin}m ago` };
330
+ }
297
331
  /**
298
332
  * Complete a live auth probe for one (agent, home). For claude/kimi/droid this
299
- * hits the provider; for everyone else it reports a best-effort local verdict
300
- * (`unverified` when a credential is present, `unconfigured` otherwise) never
301
- * masquerading as `live`.
333
+ * hits the provider unless the account's usage cache already holds a fresh
334
+ * successful fetch, which is the same authenticated request and proves the
335
+ * token live without spending a second one (RUSH-3036). For everyone else it
336
+ * reports a best-effort local verdict (`unverified` when a credential is
337
+ * present, `unconfigured` otherwise) — never masquerading as `live`.
302
338
  */
303
339
  export async function probeAuthHealth(agent, home, opts) {
304
340
  const checkedAt = Date.now();
305
341
  if (LIVE_PROBE_AGENTS.has(agent)) {
342
+ const usageScope = opts?.info?.usageKey ?? null;
343
+ if (opts?.forceLive !== true) {
344
+ const derived = verdictFromFreshUsage(usageScope, opts?.info?.signedIn === true, checkedAt);
345
+ if (derived)
346
+ return derived;
347
+ }
306
348
  let probe;
307
349
  if (agent === 'claude')
308
- probe = await probeClaudeStatus(home, opts?.cliVersion);
350
+ probe = await probeClaudeStatus(home, opts?.cliVersion, usageScope);
309
351
  else if (agent === 'kimi')
310
- probe = await probeKimiStatus(home);
352
+ probe = await probeKimiStatus(home, usageScope);
311
353
  else
312
- probe = await probeDroidStatus(home);
354
+ probe = await probeDroidStatus(home, usageScope);
313
355
  return { verdict: verdictFromProbe(probe), checkedAt, detail: probeDetail(probe) };
314
356
  }
315
357
  const info = opts?.info !== undefined ? opts.info : await getAccountInfo(agent, home).catch(() => null);
@@ -384,7 +426,7 @@ export async function probeLocalFleetAuth(opts) {
384
426
  // accounts, so no same-account concurrency is left to trip the throttle.
385
427
  const perGroup = await Promise.all(groupFleetAuthInstalls(installs, (inst) => LIVE_PROBE_AGENTS.has(inst.agent)).map(async (group) => {
386
428
  const rep = group.probe;
387
- const health = await probeAuthHealth(rep.agent, rep.home, { cliVersion: opts?.cliVersion, info: rep.info });
429
+ const health = await probeAuthHealth(rep.agent, rep.home, { cliVersion: opts?.cliVersion, info: rep.info, forceLive: opts?.forceLive });
388
430
  health.account = authAccountLabel(rep.info);
389
431
  if (health.verdict === 'unconfigured')
390
432
  return [];
@@ -13,3 +13,22 @@ export declare const DEFAULT_VIEWPORT: {
13
13
  export declare const DEVICES: Record<string, DeviceDescriptor>;
14
14
  export declare function getDevice(name: string): DeviceDescriptor | undefined;
15
15
  export declare function listDevices(): string[];
16
+ /**
17
+ * Parse a `--window WxH` value. Returns null when the value is malformed, so
18
+ * callers emit their own actionable error.
19
+ *
20
+ * Shared by `profiles create` and `profiles edit` — one regex, one test, no
21
+ * drift between the two surfaces.
22
+ */
23
+ export declare function parseWindowSize(raw: string): {
24
+ width: number;
25
+ height: number;
26
+ } | null;
27
+ /**
28
+ * Parse a `--position X,Y` value. Negative coordinates are valid — a window may
29
+ * sit on a display left of or above the primary one.
30
+ */
31
+ export declare function parseWindowPosition(raw: string): {
32
+ x: number;
33
+ y: number;
34
+ } | null;
@@ -36,3 +36,26 @@ export function getDevice(name) {
36
36
  export function listDevices() {
37
37
  return Object.keys(DEVICES);
38
38
  }
39
+ /**
40
+ * Parse a `--window WxH` value. Returns null when the value is malformed, so
41
+ * callers emit their own actionable error.
42
+ *
43
+ * Shared by `profiles create` and `profiles edit` — one regex, one test, no
44
+ * drift between the two surfaces.
45
+ */
46
+ export function parseWindowSize(raw) {
47
+ const m = String(raw).match(/^(\d+)x(\d+)$/);
48
+ if (!m)
49
+ return null;
50
+ return { width: parseInt(m[1], 10), height: parseInt(m[2], 10) };
51
+ }
52
+ /**
53
+ * Parse a `--position X,Y` value. Negative coordinates are valid — a window may
54
+ * sit on a display left of or above the primary one.
55
+ */
56
+ export function parseWindowPosition(raw) {
57
+ const m = String(raw).match(/^(-?\d+),(-?\d+)$/);
58
+ if (!m)
59
+ return null;
60
+ return { x: parseInt(m[1], 10), y: parseInt(m[2], 10) };
61
+ }
@@ -96,7 +96,7 @@ export declare function sendIPCRequest(request: IPCRequest, opts?: IPCRequestOpt
96
96
  * Fill actor / launchId / sessionId from the calling process when the request
97
97
  * left them blank. Explicit values on the request always win.
98
98
  */
99
- export declare function stampCallerIdentity(request: IPCRequest): IPCRequest;
99
+ export declare function stampCallerIdentity(request: IPCRequest, env?: NodeJS.ProcessEnv): IPCRequest;
100
100
  /**
101
101
  * Open one connection that can serve many browser operations. This uses the
102
102
  * same daemon readiness and version-reconciliation path as sendIPCRequest;
@@ -8,6 +8,7 @@ import { startDaemon, stopDaemon } from '../daemon/daemon.js';
8
8
  import { getCliVersion } from '../version.js';
9
9
  import { compareVersions } from '../agent-spec/primitives.js';
10
10
  import { getDaemonLogPath } from '../daemon/daemon.js';
11
+ import { isFleetRemoteInvocation } from './remote-control.js';
11
12
  import { resolveCallerIdentity } from './caller-identity.js';
12
13
  import { actionable } from './service.js';
13
14
  /**
@@ -382,6 +383,7 @@ export class BrowserIPCServer {
382
383
  actor: request.actor,
383
384
  launchId: request.launchId,
384
385
  sessionId: request.sessionId,
386
+ fleetRemote: request.fleetRemote,
385
387
  createIfMissing,
386
388
  title: request.title,
387
389
  url: request.url,
@@ -421,6 +423,22 @@ export class BrowserIPCServer {
421
423
  case 'version': {
422
424
  return { ok: true, version: getCliVersion() };
423
425
  }
426
+ case 'show': {
427
+ // Task-less by design — see BrowserService.showUrl. Absent from both
428
+ // PAGE_CREATE_VERBS and PAGE_RESOLVE_VERBS, so bindTask never ran above
429
+ // and this request carries no task.
430
+ if (!request.url) {
431
+ return { ok: false, error: actionable('URL required.', 'Next: agents browser navigate <url>') };
432
+ }
433
+ if (!request.profile) {
434
+ return { ok: false, error: actionable('Profile required.', 'Next: agents browser profiles list') };
435
+ }
436
+ const shown = await this.service.showUrl(request.profile, request.url, {
437
+ fleetRemote: request.fleetRemote,
438
+ actor: request.actor,
439
+ });
440
+ return { ok: true, tabId: shown.tabId };
441
+ }
424
442
  case 'start': {
425
443
  if (!request.profile) {
426
444
  return { ok: false, error: actionable('Profile required.', 'Next: agents browser profiles list') };
@@ -434,6 +452,7 @@ export class BrowserIPCServer {
434
452
  actor: request.actor,
435
453
  launchId: request.launchId,
436
454
  sessionId: request.sessionId,
455
+ fleetRemote: request.fleetRemote,
437
456
  title: request.title,
438
457
  });
439
458
  return {
@@ -855,15 +874,22 @@ export async function sendIPCRequest(request, opts = {}) {
855
874
  * Fill actor / launchId / sessionId from the calling process when the request
856
875
  * left them blank. Explicit values on the request always win.
857
876
  */
858
- export function stampCallerIdentity(request) {
859
- if (request.actor && request.launchId && request.sessionId)
860
- return request;
877
+ export function stampCallerIdentity(request, env = process.env) {
878
+ // The consent marker is stamped FIRST, above the identity early-return: a
879
+ // request that already carries a full identity (every streamed request does)
880
+ // would otherwise skip stamping entirely and reach the daemon unmarked.
881
+ // OR-ed, never overwritten — a client may assert the marker but must not be
882
+ // able to clear one this environment sets.
883
+ const fleetRemote = isFleetRemoteInvocation(env) || request.fleetRemote === true;
884
+ const marked = request.fleetRemote === fleetRemote ? request : { ...request, fleetRemote };
885
+ if (marked.actor && marked.launchId && marked.sessionId)
886
+ return marked;
861
887
  const id = resolveCallerIdentity();
862
888
  return {
863
- ...request,
864
- actor: request.actor ?? id.actor,
865
- launchId: request.launchId ?? id.launchId,
866
- sessionId: request.sessionId ?? id.sessionId,
889
+ ...marked,
890
+ actor: marked.actor ?? id.actor,
891
+ launchId: marked.launchId ?? id.launchId,
892
+ sessionId: marked.sessionId ?? id.sessionId,
867
893
  };
868
894
  }
869
895
  /**
@@ -1,4 +1,4 @@
1
- import type { BrowserProfile } from './types.js';
1
+ import type { BrowserProfile, ProfileName } from './types.js';
2
2
  export type { BrowserProfile } from './types.js';
3
3
  /**
4
4
  * Name of the profile `ensureDefaultBrowserProfile` auto-detects and pins.
@@ -167,6 +167,20 @@ export declare function effectiveLocalPort(profile: BrowserProfile): number | un
167
167
  * skip remote profiles in this scan.
168
168
  */
169
169
  export declare function findFreeProfilePort(): Promise<number>;
170
+ /**
171
+ * Refuse a profile whose LOCAL port another profile already owns.
172
+ *
173
+ * Every CDP/SSH profile ends up listening on (or tunneling to) the same LOCAL
174
+ * port number as the one configured in the endpoint URL — SSH profiles reuse
175
+ * `?port=N` locally, so we no longer scope by host. Two profiles that would
176
+ * need the same local port can't both run at the same time.
177
+ *
178
+ * `opts.ignore` is the profile's own name on an edit. Without it every edit
179
+ * collides with itself, since the stored copy still owns the port being kept.
180
+ */
181
+ export declare function assertLocalPortFree(profile: BrowserProfile, opts?: {
182
+ ignore?: string;
183
+ }): void;
170
184
  /**
171
185
  * Create a profile. **It lands in this machine's own store by default** — pass
172
186
  * `{ fleet: true }` (the CLI's `--fleet`) to put it in the synced `agents.yaml`
@@ -188,6 +202,97 @@ export declare function createProfile(profile: BrowserProfile, opts?: {
188
202
  fleet?: boolean;
189
203
  }): Promise<void>;
190
204
  export declare function updateProfile(profile: BrowserProfile): Promise<void>;
205
+ /**
206
+ * Flag a fleet-synced profile whose contents only make sense on ONE machine.
207
+ *
208
+ * A `cdp://` endpoint on loopback always connects on the machine EVALUATING the
209
+ * profile, so a fleet-scope copy does not mean "that browser" — it means "port N
210
+ * on whichever box ran the command". The name then silently resolves to a
211
+ * different, usually logged-out browser on every other machine, which is the
212
+ * worst possible failure for a profile that carries live logins.
213
+ *
214
+ * Reported by `profiles doctor` (a failing `scope` check) and noted in the
215
+ * `kept` reason `profiles prune` prints for a fleet profile. The repair is
216
+ * {@link moveProfileScope}, never an automatic rewrite of the synced doc.
217
+ */
218
+ export declare function misfiledFleetProfile(profile: BrowserProfile, scope: ProfileScope): {
219
+ misfiled: false;
220
+ } | {
221
+ misfiled: true;
222
+ why: string;
223
+ };
224
+ /** Fields an existing profile may be edited in place.
225
+ *
226
+ * Deliberately a SUBSET of {@link BrowserProfile}. `name` and `browser` are
227
+ * identity, not settings: both key the on-disk runtime dir
228
+ * ({@link getProfileRuntimeDir}) and any live `<name>@<endpoint>` connection, so
229
+ * changing either orphans the cached browser data. Delete and recreate instead.
230
+ */
231
+ export type EditableProfileFields = Partial<Pick<BrowserProfile, 'description' | 'binary' | 'electron' | 'targetFilter' | 'endpoints' | 'chrome' | 'secrets' | 'viewport'>>;
232
+ export interface EditProfileResult {
233
+ profile: BrowserProfile;
234
+ scope: ProfileScope;
235
+ /** Field names that actually changed. Empty means the edit was a no-op. */
236
+ changed: string[];
237
+ /**
238
+ * Set when the write landed machine-locally while a stale fleet copy remains.
239
+ * Only reachable for {@link isMachineLocalProfile} names that an older version
240
+ * wrote into the fleet store: {@link updateProfile} pins those local, so this
241
+ * box sees the edit and every other box keeps the old value. Callers must say
242
+ * so rather than reporting a clean success.
243
+ */
244
+ fleetCopyLeftStale?: boolean;
245
+ }
246
+ /**
247
+ * Merge `patch` onto the stored profile and persist it in the store it already
248
+ * lives in (see {@link updateProfile} for the scope rules).
249
+ *
250
+ * Runs {@link createProfile}'s validations against the MERGED record, not just
251
+ * the patched fields — a binary edit re-resolves the browser path, a
252
+ * targetFilter edit re-checks the electron gate, and the local-port scan runs
253
+ * with this profile excluded so an unchanged port is not a self-collision.
254
+ */
255
+ export declare function editProfile(name: string, patch: EditableProfileFields): Promise<EditProfileResult>;
256
+ /**
257
+ * Throw if `name` cannot be used for a NEW profile.
258
+ *
259
+ * Shared by `profiles create` and {@link renameProfile} so the two cannot drift
260
+ * — the shape rule used to live inline in the command, which is how `rename`
261
+ * would have accepted a name `create` rejects.
262
+ *
263
+ * `default` is refused because it is the reserved ALIAS meaning "whatever
264
+ * profile this machine is configured to use" (RUSH-2709). A literal profile by
265
+ * that name makes `--profile default` mean two different things.
266
+ */
267
+ export declare function assertRegistrableProfileName(name: string): void;
268
+ /**
269
+ * Rename a profile, taking its on-disk state with it.
270
+ *
271
+ * `editProfile` deliberately refuses a name change, and this is why: the name
272
+ * keys the runtime dir ({@link getProfileRuntimeDir}), every fork/endpoint dir
273
+ * derived from it, and the `browser.profile` pointer. Delete-and-recreate — the
274
+ * only route before this — silently abandons the browser's `--user-data-dir`,
275
+ * which is where a profile's logins live. On a real agent browser that is
276
+ * gigabytes of session state and every account it has ever signed into.
277
+ *
278
+ * Refuses when the profile is in use: moving a `--user-data-dir` out from under
279
+ * a running browser corrupts it.
280
+ */
281
+ export declare function renameProfile(from: ProfileName, to: ProfileName): Promise<{
282
+ scope: ProfileScope;
283
+ movedDirs: string[];
284
+ repointedDefault: boolean;
285
+ repointedViewer: boolean;
286
+ /** Peers still pinning the OLD name, and which key each used. */
287
+ stalePins: Array<{
288
+ device: string;
289
+ key: 'browser.profile' | 'browser.viewer';
290
+ }>;
291
+ }>;
292
+ export declare function moveProfileScope(name: string, to: ProfileScope): Promise<{
293
+ from: ProfileScope;
294
+ to: ProfileScope;
295
+ }>;
191
296
  export declare function deleteProfile(name: string): Promise<void>;
192
297
  /**
193
298
  * Pad `text` to exactly `width` visible characters, truncating with an ellipsis