@phnx-labs/agents-cli 1.22.116 → 1.22.118

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 (254) hide show
  1. package/CHANGELOG.md +247 -0
  2. package/dist/bootstrap.js +4 -11
  3. package/dist/commands/browser.d.ts +52 -70
  4. package/dist/commands/browser.js +192 -3562
  5. package/dist/commands/daemon.d.ts +39 -2
  6. package/dist/commands/daemon.js +72 -73
  7. package/dist/commands/events.js +1 -1
  8. package/dist/commands/exec.d.ts +18 -0
  9. package/dist/commands/exec.js +104 -12
  10. package/dist/commands/repo.js +13 -49
  11. package/dist/commands/route.js +5 -5
  12. package/dist/commands/routines.js +39 -12
  13. package/dist/commands/sessions-render.d.ts +1 -1
  14. package/dist/commands/sessions-render.js +0 -1
  15. package/dist/commands/sessions.test-fixture.d.ts +0 -1
  16. package/dist/commands/sessions.test-fixture.js +0 -37
  17. package/dist/commands/setup-browser.d.ts +17 -12
  18. package/dist/commands/setup-browser.js +52 -87
  19. package/dist/commands/setup-preferences.d.ts +0 -17
  20. package/dist/commands/setup-preferences.js +18 -49
  21. package/dist/commands/setup-term.d.ts +4 -3
  22. package/dist/commands/setup-term.js +5 -4
  23. package/dist/commands/setup.js +11 -11
  24. package/dist/commands/ssh.js +10 -1
  25. package/dist/commands/sync.js +6 -44
  26. package/dist/commands/teams.js +0 -1
  27. package/dist/commands/update.d.ts +19 -0
  28. package/dist/commands/update.js +34 -6
  29. package/dist/commands/view.d.ts +12 -1
  30. package/dist/commands/view.js +47 -2
  31. package/dist/index.js +107 -26
  32. package/dist/lib/account-capabilities.js +0 -2
  33. package/dist/lib/account-catalog.d.ts +38 -0
  34. package/dist/lib/account-catalog.js +110 -26
  35. package/dist/lib/account-provider-registry.js +1 -1
  36. package/dist/lib/account-registry.d.ts +1 -1
  37. package/dist/lib/account-registry.js +36 -2
  38. package/dist/lib/accounting/rotate.js +22 -8
  39. package/dist/lib/accounting/usage-ingest.d.ts +6 -1
  40. package/dist/lib/accounting/usage-ingest.js +115 -45
  41. package/dist/lib/accounting/usage-sync.d.ts +134 -31
  42. package/dist/lib/accounting/usage-sync.js +227 -66
  43. package/dist/lib/accounting/usage.d.ts +16 -3
  44. package/dist/lib/accounting/usage.js +26 -8
  45. package/dist/lib/accounts/slots.d.ts +8 -0
  46. package/dist/lib/accounts/slots.js +17 -0
  47. package/dist/lib/add-dir.js +0 -1
  48. package/dist/lib/agent-cli-commands.js +0 -1
  49. package/dist/lib/agent-spec/agents.d.ts +6 -7
  50. package/dist/lib/agent-spec/agents.js +6 -60
  51. package/dist/lib/auth-health.d.ts +69 -1
  52. package/dist/lib/auth-health.js +133 -4
  53. package/dist/lib/browser/context.d.ts +82 -0
  54. package/dist/lib/browser/context.js +88 -0
  55. package/dist/lib/browser/paths.d.ts +25 -0
  56. package/dist/lib/browser/paths.js +51 -0
  57. package/dist/lib/browser/record.d.ts +38 -0
  58. package/dist/lib/browser/record.js +57 -0
  59. package/dist/lib/browser/sessions-list.js +1 -2
  60. package/dist/lib/browser-client.d.ts +158 -0
  61. package/dist/lib/browser-client.js +234 -0
  62. package/dist/lib/channels/owner-sink.d.ts +1 -18
  63. package/dist/lib/channels/owner-sink.js +29 -82
  64. package/dist/lib/channels/providers/rush.d.ts +7 -7
  65. package/dist/lib/channels/providers/rush.js +127 -57
  66. package/dist/lib/claude-statusline.d.ts +1 -1
  67. package/dist/lib/claude-statusline.js +15 -3
  68. package/dist/lib/commands.js +1 -1
  69. package/dist/lib/crabbox/lease.js +1 -1
  70. package/dist/lib/crabbox/runtimes.js +1 -3
  71. package/dist/lib/daemon/account-state-daemon-service.js +1 -0
  72. package/dist/lib/daemon/auth-sync-service.d.ts +21 -18
  73. package/dist/lib/daemon/auth-sync-service.js +37 -43
  74. package/dist/lib/daemon/daemon.d.ts +25 -14
  75. package/dist/lib/daemon/daemon.js +92 -154
  76. package/dist/lib/daemon/runner.d.ts +2 -2
  77. package/dist/lib/daemon/runner.js +32 -9
  78. package/dist/lib/daemon/self-update-service.d.ts +35 -34
  79. package/dist/lib/daemon/self-update-service.js +40 -36
  80. package/dist/lib/daemon/service.d.ts +12 -8
  81. package/dist/lib/daemon/supervisor.d.ts +65 -54
  82. package/dist/lib/daemon/supervisor.js +136 -171
  83. package/dist/lib/daemon/testdata/process-view-start.js +5 -2
  84. package/dist/lib/daemon/usage-sync-service.d.ts +21 -30
  85. package/dist/lib/daemon/usage-sync-service.js +46 -78
  86. package/dist/lib/daemon-health.d.ts +22 -5
  87. package/dist/lib/daemon-health.js +47 -3
  88. package/dist/lib/daemon-services.d.ts +1 -1
  89. package/dist/lib/daemon-services.js +0 -10
  90. package/dist/lib/daemon-ticks.js +4 -2
  91. package/dist/lib/devices/doctor-findings.js +8 -11
  92. package/dist/lib/devices/harness-inventory.js +14 -5
  93. package/dist/lib/exec-account-home.js +5 -0
  94. package/dist/lib/exec.js +0 -11
  95. package/dist/lib/feed/events.js +0 -1
  96. package/dist/lib/feed/tool-activity.js +5 -2
  97. package/dist/lib/fleet-shared-state.d.ts +45 -0
  98. package/dist/lib/fleet-shared-state.js +78 -9
  99. package/dist/lib/fs-atomic.js +25 -0
  100. package/dist/lib/harness-auth-capabilities.js +0 -1
  101. package/dist/lib/helper-versions.js +1 -1
  102. package/dist/lib/hooks/cache.d.ts +7 -0
  103. package/dist/lib/hooks/cache.js +27 -2
  104. package/dist/lib/hooks/install.js +7 -1
  105. package/dist/lib/hosts/ready.d.ts +38 -8
  106. package/dist/lib/hosts/ready.js +62 -14
  107. package/dist/lib/hosts/remote-cmd.js +1 -0
  108. package/dist/lib/installations/active-check.d.ts +27 -0
  109. package/dist/lib/installations/active-check.js +61 -0
  110. package/dist/lib/installations/migrate.js +6 -3
  111. package/dist/lib/installations/shims.js +9 -7
  112. package/dist/lib/installations/update.js +21 -15
  113. package/dist/lib/installations/versions.js +12 -1
  114. package/dist/lib/manifest.js +1 -1
  115. package/dist/lib/models.js +0 -2
  116. package/dist/lib/open-url.js +28 -54
  117. package/dist/lib/placement.d.ts +2 -0
  118. package/dist/lib/placement.js +5 -0
  119. package/dist/lib/profiles.js +0 -2
  120. package/dist/lib/refresh.js +1 -1
  121. package/dist/lib/routine-readiness.d.ts +35 -0
  122. package/dist/lib/routine-readiness.js +74 -16
  123. package/dist/lib/rules/compile.js +6 -0
  124. package/dist/lib/sandbox.js +0 -2
  125. package/dist/lib/secrets-policy.d.ts +89 -11
  126. package/dist/lib/secrets-policy.js +175 -28
  127. package/dist/lib/session/db.d.ts +1 -1
  128. package/dist/lib/session/db.js +65 -18
  129. package/dist/lib/session/discover.js +1 -283
  130. package/dist/lib/session/mirror.d.ts +4 -3
  131. package/dist/lib/session/mirror.js +4 -3
  132. package/dist/lib/session/parse.d.ts +0 -2
  133. package/dist/lib/session/parse.js +1 -138
  134. package/dist/lib/session/throughput.d.ts +1 -3
  135. package/dist/lib/session/throughput.js +0 -20
  136. package/dist/lib/session/types.d.ts +2 -2
  137. package/dist/lib/session/types.js +2 -2
  138. package/dist/lib/sessions-client.d.ts +60 -1
  139. package/dist/lib/sessions-client.js +227 -4
  140. package/dist/lib/setup-tool-status.js +8 -12
  141. package/dist/lib/signin-badge.d.ts +3 -2
  142. package/dist/lib/signin-badge.js +6 -3
  143. package/dist/lib/smart-launch.js +4 -1
  144. package/dist/lib/staleness/detectors/commands.d.ts +1 -1
  145. package/dist/lib/staleness/detectors/hooks.d.ts +1 -1
  146. package/dist/lib/staleness/detectors/mcp.d.ts +1 -1
  147. package/dist/lib/staleness/detectors/permissions.d.ts +1 -1
  148. package/dist/lib/staleness/detectors/plugins.d.ts +1 -1
  149. package/dist/lib/staleness/detectors/rules.d.ts +1 -1
  150. package/dist/lib/staleness/detectors/skills.d.ts +1 -1
  151. package/dist/lib/staleness/detectors/subagents.d.ts +1 -1
  152. package/dist/lib/staleness/detectors/workflows.d.ts +1 -1
  153. package/dist/lib/staleness/writers/commands.d.ts +1 -1
  154. package/dist/lib/staleness/writers/hooks.d.ts +1 -1
  155. package/dist/lib/staleness/writers/mcp.d.ts +1 -1
  156. package/dist/lib/staleness/writers/permissions.d.ts +1 -1
  157. package/dist/lib/staleness/writers/plugins.d.ts +1 -1
  158. package/dist/lib/staleness/writers/rules.d.ts +1 -1
  159. package/dist/lib/staleness/writers/skills.d.ts +1 -1
  160. package/dist/lib/staleness/writers/sources.js +5 -0
  161. package/dist/lib/staleness/writers/subagents.d.ts +1 -1
  162. package/dist/lib/staleness/writers/workflows.d.ts +1 -1
  163. package/dist/lib/state.d.ts +6 -4
  164. package/dist/lib/state.js +7 -17
  165. package/dist/lib/teams/parsers.d.ts +1 -1
  166. package/dist/lib/teams/parsers.js +1 -136
  167. package/dist/lib/teams/placement-probe.js +50 -15
  168. package/dist/lib/teams/scheduler.d.ts +7 -0
  169. package/dist/lib/types.d.ts +10 -1
  170. package/dist/lib/types.js +1 -1
  171. package/dist/lib/view-types.d.ts +32 -0
  172. package/dist/lib/watchdog/read.js +0 -1
  173. package/dist/lib/watchdog/rotate.js +1 -2
  174. package/dist/lib/watchdog/runner.js +2 -2
  175. package/dist/lib/watchdog/watchdog.d.ts +1 -1
  176. package/dist/lib/watchdog/watchdogTail.js +0 -31
  177. package/package.json +1 -1
  178. package/scripts/postinstall.js +8 -8
  179. package/dist/browser.d.ts +0 -2
  180. package/dist/browser.js +0 -17
  181. package/dist/commands/browser-picker.d.ts +0 -18
  182. package/dist/commands/browser-picker.js +0 -97
  183. package/dist/lib/browser/arc-discovery.d.ts +0 -52
  184. package/dist/lib/browser/arc-discovery.js +0 -197
  185. package/dist/lib/browser/arc-dom.d.ts +0 -14
  186. package/dist/lib/browser/arc-dom.js +0 -121
  187. package/dist/lib/browser/caller-identity.d.ts +0 -40
  188. package/dist/lib/browser/caller-identity.js +0 -175
  189. package/dist/lib/browser/cdp.d.ts +0 -51
  190. package/dist/lib/browser/cdp.js +0 -254
  191. package/dist/lib/browser/chrome.d.ts +0 -141
  192. package/dist/lib/browser/chrome.js +0 -704
  193. package/dist/lib/browser/chromium-discovery.d.ts +0 -28
  194. package/dist/lib/browser/chromium-discovery.js +0 -96
  195. package/dist/lib/browser/devices.d.ts +0 -34
  196. package/dist/lib/browser/devices.js +0 -61
  197. package/dist/lib/browser/domain-skills.d.ts +0 -71
  198. package/dist/lib/browser/domain-skills.js +0 -195
  199. package/dist/lib/browser/drivers/arc.d.ts +0 -40
  200. package/dist/lib/browser/drivers/arc.js +0 -276
  201. package/dist/lib/browser/drivers/firefox.d.ts +0 -99
  202. package/dist/lib/browser/drivers/firefox.js +0 -377
  203. package/dist/lib/browser/drivers/local.d.ts +0 -55
  204. package/dist/lib/browser/drivers/local.js +0 -266
  205. package/dist/lib/browser/drivers/ssh.d.ts +0 -120
  206. package/dist/lib/browser/drivers/ssh.js +0 -467
  207. package/dist/lib/browser/editor.d.ts +0 -3
  208. package/dist/lib/browser/editor.js +0 -50
  209. package/dist/lib/browser/ffmpeg.d.ts +0 -12
  210. package/dist/lib/browser/ffmpeg.js +0 -184
  211. package/dist/lib/browser/firefox-discovery.d.ts +0 -69
  212. package/dist/lib/browser/firefox-discovery.js +0 -162
  213. package/dist/lib/browser/har.d.ts +0 -85
  214. package/dist/lib/browser/har.js +0 -77
  215. package/dist/lib/browser/hygiene.d.ts +0 -97
  216. package/dist/lib/browser/hygiene.js +0 -153
  217. package/dist/lib/browser/index.d.ts +0 -5
  218. package/dist/lib/browser/index.js +0 -5
  219. package/dist/lib/browser/input.d.ts +0 -7
  220. package/dist/lib/browser/input.js +0 -92
  221. package/dist/lib/browser/ipc.d.ts +0 -204
  222. package/dist/lib/browser/ipc.js +0 -1453
  223. package/dist/lib/browser/login-detection.d.ts +0 -87
  224. package/dist/lib/browser/login-detection.js +0 -274
  225. package/dist/lib/browser/profiles.d.ts +0 -422
  226. package/dist/lib/browser/profiles.js +0 -1157
  227. package/dist/lib/browser/refs.d.ts +0 -89
  228. package/dist/lib/browser/refs.js +0 -191
  229. package/dist/lib/browser/registry.d.ts +0 -70
  230. package/dist/lib/browser/registry.js +0 -252
  231. package/dist/lib/browser/remote-control.d.ts +0 -59
  232. package/dist/lib/browser/remote-control.js +0 -85
  233. package/dist/lib/browser/resolve-target.d.ts +0 -66
  234. package/dist/lib/browser/resolve-target.js +0 -264
  235. package/dist/lib/browser/runtime-state.d.ts +0 -291
  236. package/dist/lib/browser/runtime-state.js +0 -584
  237. package/dist/lib/browser/secret-ref.d.ts +0 -10
  238. package/dist/lib/browser/secret-ref.js +0 -14
  239. package/dist/lib/browser/service.d.ts +0 -812
  240. package/dist/lib/browser/service.js +0 -4693
  241. package/dist/lib/browser/stream.d.ts +0 -17
  242. package/dist/lib/browser/stream.js +0 -72
  243. package/dist/lib/browser/task-index.d.ts +0 -78
  244. package/dist/lib/browser/task-index.js +0 -199
  245. package/dist/lib/browser/types.d.ts +0 -647
  246. package/dist/lib/browser/types.js +0 -61
  247. package/dist/lib/browser/upload.d.ts +0 -29
  248. package/dist/lib/browser/upload.js +0 -298
  249. package/dist/lib/daemon/browser-ipc-service.d.ts +0 -20
  250. package/dist/lib/daemon/browser-ipc-service.js +0 -46
  251. package/dist/lib/daemon/browser-task-reap-service.d.ts +0 -14
  252. package/dist/lib/daemon/browser-task-reap-service.js +0 -26
  253. package/dist/lib/fleet-shared-repo-sync.d.ts +0 -55
  254. package/dist/lib/fleet-shared-repo-sync.js +0 -490
@@ -1,584 +0,0 @@
1
- import * as fs from 'fs';
2
- import * as path from 'path';
3
- import { execFileSync } from 'child_process';
4
- import { getProfileRuntimeDir, getBrowserRuntimeDir, getEndpointPresets, listProfiles, getConfiguredDefaultProfileName, isProfileLaunchableHere, isProfileDeclaredHere, deleteProfile, DEFAULT_BROWSER_PROFILE_NAME, LEGACY_DEFAULT_BROWSER_PROFILE_NAME, } from './profiles.js';
5
- import { keyBelongsToProfile } from './types.js';
6
- import { declaringDevices, profileKind } from './registry.js';
7
- import { machineId } from '../machine-id.js';
8
- /**
9
- * Detect an identity-bearing profile whose stored endpoint is loopback, viewed
10
- * from a machine that is NOT the declaring device.
11
- *
12
- * That is the original `comet-local` bug: five real logins on the declaring
13
- * box, a bare logged-out chromium answering to the same name on a worker
14
- * because `cdp://localhost:N` is evaluated on the machine running the command.
15
- * The daemon now tunnels that shape (`resolve-target.ts`); doctor still flags
16
- * it so a worker does not report a green local binary/port for someone else's
17
- * browser. Kind comes from the registry (exactly one declaring device), never
18
- * from a stored field.
19
- */
20
- export function identityLoopbackMismatch(profile) {
21
- if (profileKind(profile.name) !== 'identity')
22
- return { misfiled: false };
23
- const declaringDevice = declaringDevices(profile.name)[0];
24
- if (!declaringDevice || declaringDevice === machineId())
25
- return { misfiled: false };
26
- const presets = getEndpointPresets(profile);
27
- const chosen = profile.defaultEndpoint && presets[profile.defaultEndpoint]
28
- ? profile.defaultEndpoint
29
- : Object.keys(presets)[0];
30
- if (!chosen)
31
- return { misfiled: false };
32
- const target = presets[chosen].target;
33
- if (!target.startsWith('cdp://'))
34
- return { misfiled: false };
35
- let host;
36
- try {
37
- host = new URL(target).hostname;
38
- }
39
- catch {
40
- return { misfiled: false };
41
- }
42
- if (host !== 'localhost' && host !== '127.0.0.1' && host !== '::1' && host !== '[::1]') {
43
- return { misfiled: false };
44
- }
45
- const here = machineId();
46
- return {
47
- misfiled: true,
48
- why: `identity-bearing profile declared on ${declaringDevice}, but "${target}" is a loopback endpoint — ` +
49
- `on ${here} that address is this device's own browser, not ${declaringDevice}'s. ` +
50
- `Diagnose the real browser on ${declaringDevice}: agents browser profiles doctor ${profile.name}`,
51
- };
52
- }
53
- const PID_FILE = 'pid';
54
- const PORT_FILE = 'port';
55
- const COMMAND_FILE = 'command';
56
- const META_FILE = 'meta.json';
57
- function readNumberFile(p) {
58
- try {
59
- const n = parseInt(fs.readFileSync(p, 'utf-8').trim(), 10);
60
- return Number.isFinite(n) ? n : null;
61
- }
62
- catch {
63
- return null;
64
- }
65
- }
66
- function readStringFile(p) {
67
- try {
68
- return fs.readFileSync(p, 'utf-8').trim() || null;
69
- }
70
- catch {
71
- return null;
72
- }
73
- }
74
- /**
75
- * Save the runtime record atomically. We write the legacy one-value-per-
76
- * file fields plus a JSON meta blob so future code can read either.
77
- * The cache directory may not exist yet (first launch); we create it.
78
- */
79
- export function writeProfileRuntime(profileName, runtime) {
80
- const dir = getProfileRuntimeDir(profileName);
81
- fs.mkdirSync(dir, { recursive: true });
82
- fs.writeFileSync(path.join(dir, PID_FILE), String(runtime.pid));
83
- if (runtime.port !== undefined) {
84
- fs.writeFileSync(path.join(dir, PORT_FILE), String(runtime.port));
85
- }
86
- else {
87
- try {
88
- fs.unlinkSync(path.join(dir, PORT_FILE));
89
- }
90
- catch { /* not present */ }
91
- }
92
- if (runtime.command) {
93
- fs.writeFileSync(path.join(dir, COMMAND_FILE), runtime.command);
94
- }
95
- const meta = {
96
- ...runtime,
97
- daemonPid: runtime.daemonPid ?? process.pid,
98
- spawnedAt: runtime.spawnedAt ?? Date.now(),
99
- };
100
- fs.writeFileSync(path.join(dir, META_FILE), JSON.stringify(meta));
101
- }
102
- /** Read just the JSON meta record. Returns null when absent or malformed. */
103
- export function readProfileRuntimeMeta(profileName) {
104
- const dir = getProfileRuntimeDir(profileName);
105
- try {
106
- const raw = fs.readFileSync(path.join(dir, META_FILE), 'utf-8');
107
- const obj = JSON.parse(raw);
108
- if (typeof obj !== 'object' || obj === null)
109
- return null;
110
- return obj;
111
- }
112
- catch {
113
- return null;
114
- }
115
- }
116
- /**
117
- * Read the runtime triple. Returns null when the files are missing OR when
118
- * the recorded pid no longer points at the same process we launched —
119
- * stale data is auto-cleaned to keep the next caller from acting on it.
120
- */
121
- export function readProfileRuntime(profileName) {
122
- const dir = getProfileRuntimeDir(profileName);
123
- const pid = readNumberFile(path.join(dir, PID_FILE));
124
- const port = readNumberFile(path.join(dir, PORT_FILE));
125
- const command = readStringFile(path.join(dir, COMMAND_FILE)) ?? undefined;
126
- if (pid === null || port === null)
127
- return null;
128
- if (!isProcessAlive(pid, command)) {
129
- clearProfileRuntime(profileName);
130
- return null;
131
- }
132
- return { pid, port, command };
133
- }
134
- /** Remove the pid/port/command/meta files. Leaves chrome-data + tasks.json intact. */
135
- export function clearProfileRuntime(profileName) {
136
- const dir = getProfileRuntimeDir(profileName);
137
- for (const f of [PID_FILE, PORT_FILE, COMMAND_FILE, META_FILE]) {
138
- try {
139
- fs.unlinkSync(path.join(dir, f));
140
- }
141
- catch { /* not present */ }
142
- }
143
- }
144
- /**
145
- * Recursively remove the whole profile cache (chrome-data, tasks.json,
146
- * everything). Used by `profiles remove` so an old profile name doesn't
147
- * leak its history into a freshly-recreated one.
148
- */
149
- export function removeProfileCache(profileName) {
150
- const dir = getProfileRuntimeDir(profileName);
151
- try {
152
- fs.rmSync(dir, { recursive: true, force: true });
153
- }
154
- catch { /* gone */ }
155
- }
156
- /**
157
- * Find every cache directory belonging to a given profile. A runtime dir name
158
- * IS a connection key (`<name>@<endpoint>[.<fork>]`), so a single agents-cli
159
- * profile can have several side by side; this finds them all, plus the legacy
160
- * non-composite dir from older builds.
161
- *
162
- * Membership is decided by {@link keyBelongsToProfile} — the same single rule
163
- * `status`, `stopProfile`, and `findTask` use (RUSH-2709), so a fork dir is no
164
- * longer missed here while being matched there.
165
- */
166
- export function listProfileCacheDirs(profileName) {
167
- const root = getBrowserRuntimeDir();
168
- if (!fs.existsSync(root))
169
- return [];
170
- const matches = [];
171
- for (const entry of fs.readdirSync(root)) {
172
- if (keyBelongsToProfile(entry, profileName))
173
- matches.push(path.join(root, entry));
174
- }
175
- return matches;
176
- }
177
- /**
178
- * `process.kill(pid, 0)` answers "is a process with this id alive?" — but
179
- * pid reuse is real on long-uptime machines, and a stale cache pointing
180
- * at a since-reassigned pid would happily call the imposter ours.
181
- *
182
- * Strategy: if we recorded the executable basename when we launched, ask
183
- * `ps` what command the live pid is running and compare. No command on
184
- * record means we fall back to the existence check (older cache entries
185
- * or `pid:0` for "attached to an externally-launched browser").
186
- */
187
- export function isProcessAlive(pid, expectedCommand) {
188
- if (pid === 0)
189
- return true;
190
- try {
191
- process.kill(pid, 0);
192
- }
193
- catch (err) {
194
- if (err && err.code === 'EPERM') {
195
- // exists but we can't signal it — count it as alive
196
- return !expectedCommand || matchesCommand(pid, expectedCommand);
197
- }
198
- return false;
199
- }
200
- if (!expectedCommand)
201
- return true;
202
- return matchesCommand(pid, expectedCommand);
203
- }
204
- /**
205
- * Read every profile cache directory and produce a structured snapshot.
206
- * Works without the daemon — `agents browser ps` uses this to render a
207
- * complete state view even when the IPC server is down. The caller can
208
- * post-process to detect conflicts (e.g. two profiles with the same port,
209
- * or a port someone else is listening on).
210
- */
211
- export function listAllProfileSnapshots() {
212
- const root = getBrowserRuntimeDir();
213
- if (!fs.existsSync(root))
214
- return [];
215
- const out = [];
216
- for (const name of fs.readdirSync(root).sort()) {
217
- const dir = path.join(root, name);
218
- let stat;
219
- try {
220
- stat = fs.statSync(dir);
221
- }
222
- catch {
223
- continue;
224
- }
225
- if (!stat.isDirectory())
226
- continue;
227
- const meta = readProfileRuntimeMeta(name);
228
- const taskCount = readTaskCount(dir);
229
- const pidAlive = meta ? isProcessAlive(meta.pid, meta.command) : false;
230
- const tunnelAlive = meta?.tunnelPid ? isProcessAlive(meta.tunnelPid, 'ssh') : false;
231
- const daemonAlive = meta?.daemonPid ? isProcessAlive(meta.daemonPid) : false;
232
- out.push({ name, dir, meta, pidAlive, tunnelAlive, daemonAlive, taskCount });
233
- }
234
- return out;
235
- }
236
- function readTaskCount(dir) {
237
- try {
238
- const raw = fs.readFileSync(path.join(dir, 'tasks.json'), 'utf-8');
239
- const obj = JSON.parse(raw);
240
- if (Array.isArray(obj))
241
- return obj.length;
242
- if (obj && typeof obj === 'object')
243
- return Object.keys(obj).length;
244
- return 0;
245
- }
246
- catch {
247
- return 0;
248
- }
249
- }
250
- /**
251
- * Reap browser + tunnel processes spawned by daemons that no longer exist.
252
- * Call once on daemon startup. The idea: every process we spawn records
253
- * its daemonPid in meta.json. If that daemon is dead (crashed, SIGKILL),
254
- * its children were left rootless — kill them now so they don't hijack
255
- * the next session's local ports.
256
- *
257
- * We're conservative: a record with no daemonPid (older builds) is left
258
- * alone — we'd rather leak than wrongly kill a user-owned process that
259
- * happens to share metadata.
260
- */
261
- /**
262
- * Clean up what a DEAD daemon genuinely left behind — which is not the browser.
263
- *
264
- * A live local browser outliving its daemon is the NORMAL case, not an orphan.
265
- * `BrowserService.shutdown` deliberately closes only CDP and leaves the browser
266
- * running, and on macOS the spawn is detached so the daemon's own positive-pid
267
- * `killTree` reaches the daemon alone. This reaper used to read "the recorded
268
- * `daemonPid` has exited" as "everything it recorded is garbage" and SIGTERM the
269
- * browser — so replacing the daemon (a menubar activation, an upgrade, a crash
270
- * restart) killed a browser the user was looking at, and `clearProfileRuntime`
271
- * then removed the `port`/meta that {@link readProfileRuntime} needs, so even a
272
- * survivor could not be re-attached and a second browser would launch beside it.
273
- *
274
- * The three cases are now decided independently:
275
- *
276
- * - **A live local browser/electron is preserved untouched** — not signalled, not
277
- * cleared, and its `daemonPid` deliberately NOT rewritten. Ownership is adopted
278
- * only after an identity-checked attach succeeds (`adoptProfileRuntimeOwner`,
279
- * called from the reuse branch in `BrowserService.connectProfile`), so nothing
280
- * claims a browser it has not proven it can talk to. Re-running this is a no-op.
281
- * - **A dead browser record is cleared**, with no signal sent: there is nothing
282
- * alive to signal, and the stale pid/port would otherwise mislead the next
283
- * attach.
284
- * - **A stale tunnel IS reaped.** A local `ssh -L` cannot outlive its purpose, so
285
- * a dead owner does make it an orphan. This is the one thing the old code was
286
- * right about; it just shared a code path with the browser.
287
- *
288
- * The accepted trade: a genuinely leaked browser from a long-dead daemon is no
289
- * longer force-killed here. Idle/abandoned cleanup has its own owner in
290
- * `browser/hygiene.ts` (session-dead + idle), which the daemon's periodic tick and
291
- * `agents browser prune` both call — that is the right place for it, because it can
292
- * tell "idle and abandoned" from "in use right now" and this function cannot.
293
- */
294
- export function reapOrphanedProcesses() {
295
- const root = getBrowserRuntimeDir();
296
- if (!fs.existsSync(root))
297
- return { reaped: 0, details: [] };
298
- let reaped = 0;
299
- const details = [];
300
- for (const profileName of fs.readdirSync(root)) {
301
- const meta = readProfileRuntimeMeta(profileName);
302
- if (!meta)
303
- continue;
304
- if (!meta.daemonPid)
305
- continue;
306
- if (meta.daemonPid === process.pid)
307
- continue;
308
- // Owning daemon still alive — leave its kids alone.
309
- if (isProcessAlive(meta.daemonPid))
310
- continue;
311
- // A local `ssh -L` whose owner is gone is genuinely orphaned: nothing is
312
- // driving it and it cannot be re-adopted, so it is the one process to signal.
313
- // Matched against the recorded command (or `ssh`) so a recycled pid is not
314
- // mistaken for our tunnel.
315
- if (meta.tunnelPid && meta.tunnelPid !== 0) {
316
- const cmdOk = !meta.command
317
- || matchesCommand(meta.tunnelPid, meta.command)
318
- || matchesCommand(meta.tunnelPid, 'ssh');
319
- if (cmdOk) {
320
- try {
321
- process.kill(meta.tunnelPid, 'SIGTERM');
322
- reaped++;
323
- details.push(`reaped tunnel ${meta.tunnelPid} (profile ${profileName})`);
324
- }
325
- catch { /* already gone */ }
326
- }
327
- }
328
- // A tunnel-kind record has no local browser of its own, so clearing it is the
329
- // whole cleanup. `pid` on such a record is the REMOTE browser (normally 0) and
330
- // is never ours to signal.
331
- if (meta.kind === 'tunnel') {
332
- clearProfileRuntime(profileName);
333
- continue;
334
- }
335
- // `isProcessAlive` re-checks the recorded command, so a recycled pid does not
336
- // read as our browser. A record with no pid at all is stale by definition.
337
- const liveBrowser = !!meta.pid && meta.pid !== 0 && isProcessAlive(meta.pid, meta.command);
338
- if (liveBrowser) {
339
- details.push(`preserved live browser ${meta.pid} (profile ${profileName})`);
340
- continue;
341
- }
342
- // Dead browser: clear the stale record. No signal — there is nothing there.
343
- clearProfileRuntime(profileName);
344
- if (meta.pid)
345
- details.push(`cleared dead browser record ${meta.pid} (profile ${profileName})`);
346
- }
347
- return { reaped, details };
348
- }
349
- /**
350
- * Take ownership of a runtime record after an identity-checked attach succeeded.
351
- *
352
- * Called from the reuse branch in `BrowserService.connectProfile`, which has just
353
- * probed `/json/version` and run `verifyBrowserIdentity` against the profile's
354
- * expected browser family — so by this point the process on the recorded port is
355
- * proven to be ours. Only then may a new service claim it.
356
- *
357
- * Rewrites `daemonPid` ALONE. Every other field — `pid`, `port`, `command`,
358
- * `userDataDir`, `spawnedAt`, `kind`, `tunnelPid` — is the original launch's
359
- * metadata and stays exactly as recorded: this is an ownership handover, not a
360
- * re-registration, and rewriting the rest would lose the provenance the reaper and
361
- * the hygiene pass both read. A record that has since disappeared is left alone
362
- * rather than recreated from a partial view.
363
- */
364
- export function adoptProfileRuntimeOwner(profileName, daemonPid = process.pid) {
365
- const meta = readProfileRuntimeMeta(profileName);
366
- if (!meta)
367
- return false;
368
- if (meta.daemonPid === daemonPid)
369
- return true;
370
- const dir = getProfileRuntimeDir(profileName);
371
- try {
372
- fs.writeFileSync(path.join(dir, META_FILE), JSON.stringify({ ...meta, daemonPid }));
373
- return true;
374
- }
375
- catch {
376
- // Losing the handover is not worth failing an otherwise good attach; the next
377
- // startup simply preserves the browser again and re-attaches.
378
- return false;
379
- }
380
- }
381
- /**
382
- * True when ANY cache dir belonging to this profile still has a live browser or
383
- * SSH tunnel, or an open task. The composite naming (`<name>@<endpoint>`) means
384
- * one profile can own several runtime dirs, so checking only
385
- * `getProfileRuntimeDir(name)` would miss a running composite — and prune must
386
- * never remove a profile that is in use.
387
- *
388
- * A recorded `pid: 0` means "attached to a browser someone else launched", so it
389
- * is not our process to protect; an open task on that dir still counts as in use.
390
- */
391
- export function isProfileInUse(profileName) {
392
- for (const dir of listProfileCacheDirs(profileName)) {
393
- const entry = path.basename(dir);
394
- if (readTaskCount(dir) > 0)
395
- return true;
396
- const meta = readProfileRuntimeMeta(entry);
397
- if (!meta)
398
- continue;
399
- if (meta.pid && isProcessAlive(meta.pid, meta.command))
400
- return true;
401
- if (meta.tunnelPid && isProcessAlive(meta.tunnelPid, 'ssh'))
402
- return true;
403
- }
404
- return false;
405
- }
406
- export const PRUNE_REASON_TEXT = {
407
- 'binary-missing': 'its browser/binary is not installed on this machine',
408
- 'never-used': 'it has never been started (no runtime dir)',
409
- };
410
- /**
411
- * Decide which browser profiles are dead, WITHOUT touching anything.
412
- * `pruneProfiles` executes exactly this plan, so `--dry-run` and the real run
413
- * can never disagree.
414
- *
415
- * Dead means one of {@link PruneReason}: the configured browser cannot launch
416
- * here at all, or the profile has never been started so nothing on this disk
417
- * refers to it. That second reason is what actually reclaims agent-minted
418
- * throwaway profiles (RUSH-2716) — they are created for an installed browser, so
419
- * only "never started" identifies them.
420
- *
421
- * Four guards, and each exists because removing that profile would be wrong
422
- * rather than merely tidy:
423
- * - **in use** — a live browser, SSH tunnel, or open task on any of its dirs.
424
- * - **the configured default** — this machine resolves a bare
425
- * `agents browser start` to it; removing it breaks that command.
426
- * - **the auto-detected profile** (`auto-chrome`, or the `default` an older
427
- * build wrote) — still recognized and resolved by
428
- * `ensureDefaultBrowserProfile` as this machine's default; since PHNX-3296
429
- * it is no longer re-created on demand, so pruning it forces the user
430
- * back through `agents setup` to get a default browser again.
431
- *
432
- * Known limitation: `BrowserProfileConfig` records no creation time, so a
433
- * profile created seconds ago and not yet started is indistinguishable from an
434
- * abandoned one and is reported `never-used`. That is why the verb is explicit
435
- * and `--dry-run` exists.
436
- */
437
- export function planProfilePrune(profiles, opts = {}) {
438
- const plan = { candidates: [], kept: [] };
439
- for (const { name, scope, launchableHere, misfiledWhy } of profiles) {
440
- const keep = (why, misfiled = false) => {
441
- plan.kept.push(misfiled ? { name, scope, why, misfiled } : { name, scope, why });
442
- };
443
- // Checked FIRST. A misfiled identity profile's defining symptom is that the
444
- // browser is absent on whatever box you are standing on — pruning it would
445
- // delete the declaring device's entry (or throw, for a peer declaration).
446
- // The repair is to declare it only on the machine that owns the browser;
447
- // an intentional delete is still available as `profiles remove`.
448
- if (misfiledWhy) {
449
- keep(`MISFILED — ${misfiledWhy}`, true);
450
- continue;
451
- }
452
- if (name === DEFAULT_BROWSER_PROFILE_NAME || name === LEGACY_DEFAULT_BROWSER_PROFILE_NAME) {
453
- keep('the auto-detected default profile');
454
- continue;
455
- }
456
- if (opts.configuredDefault && name === opts.configuredDefault) {
457
- keep("this machine's configured default profile");
458
- continue;
459
- }
460
- if (isProfileInUse(name)) {
461
- keep('in use (live browser, tunnel, or open task)');
462
- continue;
463
- }
464
- const cacheDirs = listProfileCacheDirs(name);
465
- if (!launchableHere) {
466
- plan.candidates.push({ name, scope, reason: 'binary-missing', cacheDirs });
467
- continue;
468
- }
469
- if (cacheDirs.length === 0) {
470
- plan.candidates.push({ name, scope, reason: 'never-used', cacheDirs });
471
- continue;
472
- }
473
- keep('healthy (browser installed, has runtime state)');
474
- }
475
- return plan;
476
- }
477
- /**
478
- * Build the prune plan from live config. Split from {@link planProfilePrune} so
479
- * the decision rules stay unit-testable without a configured machine.
480
- */
481
- export async function buildProfilePrunePlan() {
482
- const current = machineId();
483
- const profiles = await listProfiles();
484
- const declaredHere = [];
485
- const peerKept = [];
486
- for (const profile of profiles) {
487
- const scope = profile.devices.length > 1 ? 'fungible' : 'identity';
488
- const misfiled = identityLoopbackMismatch(profile);
489
- if (profile.arc && !isProfileDeclaredHere(profile.name)) {
490
- peerKept.push({
491
- name: profile.name,
492
- scope,
493
- why: 'read-only native Arc discovery; no agents-cli alias to prune',
494
- });
495
- continue;
496
- }
497
- if (profile.devices.includes(current)) {
498
- declaredHere.push({
499
- name: profile.name,
500
- scope,
501
- launchableHere: isProfileLaunchableHere(profile),
502
- misfiledWhy: misfiled.misfiled ? misfiled.why : undefined,
503
- });
504
- continue;
505
- }
506
- if (misfiled.misfiled) {
507
- peerKept.push({
508
- name: profile.name,
509
- scope,
510
- why: `MISFILED — ${misfiled.why}`,
511
- misfiled: true,
512
- });
513
- }
514
- else {
515
- peerKept.push({
516
- name: profile.name,
517
- scope,
518
- why: `declared on ${profile.devices.join(', ')}, not this device`,
519
- });
520
- }
521
- }
522
- const plan = planProfilePrune(declaredHere, {
523
- configuredDefault: getConfiguredDefaultProfileName(),
524
- });
525
- plan.kept.push(...peerKept);
526
- return plan;
527
- }
528
- /**
529
- * Execute a prune plan: drop each candidate's config entry and wipe its cache
530
- * dirs. Returns the plan that was applied so the caller can report exactly what
531
- * went, matching what `--dry-run` printed.
532
- */
533
- export async function pruneProfiles(plan) {
534
- for (const candidate of plan.candidates) {
535
- await deleteProfile(candidate.name);
536
- for (const dir of candidate.cacheDirs) {
537
- try {
538
- fs.rmSync(dir, { recursive: true, force: true });
539
- }
540
- catch { /* already gone */ }
541
- }
542
- removeProfileCache(candidate.name);
543
- }
544
- return plan;
545
- }
546
- function matchesCommand(pid, expectedCommand) {
547
- const out = liveProcessCommand(pid);
548
- if (!out)
549
- return false;
550
- // Match on the basename only — `/Applications/Comet.app/Contents/MacOS/Comet`
551
- // vs the recorded `Comet`, vs `Google\ Chrome`, vs Windows `chrome.exe`.
552
- // Case-insensitive.
553
- const live = path.basename(out).toLowerCase();
554
- const want = path.basename(expectedCommand).toLowerCase();
555
- return live === want || live.startsWith(want) || want.startsWith(live);
556
- }
557
- /**
558
- * The executable/image name the live `pid` is running, or null if it can't be
559
- * determined. The process-listing API differs per OS: Windows has no `ps`, so
560
- * we query `tasklist` (CSV image name in column 1); POSIX uses `ps -o comm=`.
561
- */
562
- export function liveProcessCommand(pid) {
563
- try {
564
- if (process.platform === 'win32') {
565
- const out = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'], {
566
- encoding: 'utf-8',
567
- stdio: ['ignore', 'pipe', 'ignore'],
568
- windowsHide: true,
569
- }).trim();
570
- // Rows look like: "node.exe","1234","Console","1","12,345 K"
571
- // A no-match prints an "INFO: No tasks..." line that won't match the regex.
572
- const m = out.match(/^"([^"]+)"/);
573
- return m ? m[1] : null;
574
- }
575
- const out = execFileSync('ps', ['-p', String(pid), '-o', 'comm='], {
576
- encoding: 'utf-8',
577
- stdio: ['ignore', 'pipe', 'ignore'],
578
- }).trim();
579
- return out || null;
580
- }
581
- catch {
582
- return null;
583
- }
584
- }
@@ -1,10 +0,0 @@
1
- /**
2
- * Parse a `<bundle>/<KEY>` reference (optionally `secret:<bundle>/<KEY>`) used by
3
- * `agents browser type --secret` to feed a credential from an `agents secrets`
4
- * bundle into a page WITHOUT the value ever crossing stdout or the transcript.
5
- * Returns null on a malformed ref.
6
- */
7
- export declare function parseSecretRef(ref: string): {
8
- bundle: string;
9
- key: string;
10
- } | null;
@@ -1,14 +0,0 @@
1
- /**
2
- * Parse a `<bundle>/<KEY>` reference (optionally `secret:<bundle>/<KEY>`) used by
3
- * `agents browser type --secret` to feed a credential from an `agents secrets`
4
- * bundle into a page WITHOUT the value ever crossing stdout or the transcript.
5
- * Returns null on a malformed ref.
6
- */
7
- export function parseSecretRef(ref) {
8
- const body = ref.startsWith('secret:') ? ref.slice('secret:'.length) : ref;
9
- const slash = body.indexOf('/');
10
- // Need a non-empty bundle before the slash and a non-empty key after it.
11
- if (slash <= 0 || slash >= body.length - 1)
12
- return null;
13
- return { bundle: body.slice(0, slash), key: body.slice(slash + 1) };
14
- }