@phnx-labs/agents-cli 1.22.114 → 1.22.115
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +110 -0
- package/README.md +22 -102
- package/dist/browser.js +0 -0
- package/dist/cli/command-registry.js +0 -7
- package/dist/commands/accounts.d.ts +3 -77
- package/dist/commands/accounts.js +13 -366
- package/dist/commands/audit.js +1 -1
- package/dist/commands/auth.js +0 -2
- package/dist/commands/browser.d.ts +53 -0
- package/dist/commands/browser.js +199 -9
- package/dist/commands/doctor.d.ts +2 -3
- package/dist/commands/doctor.js +0 -11
- package/dist/commands/events.d.ts +1 -3
- package/dist/commands/events.js +4 -7
- package/dist/commands/exec.d.ts +0 -1
- package/dist/commands/exec.js +2 -7
- package/dist/commands/feed-watch.js +52 -8
- package/dist/commands/focus.js +1 -1
- package/dist/commands/go.d.ts +0 -17
- package/dist/commands/go.js +2 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mcp.js +8 -83
- package/dist/commands/memory.js +4 -47
- package/dist/commands/message.js +4 -4
- package/dist/commands/plugins.js +1 -93
- package/dist/commands/repo.js +0 -44
- package/dist/commands/resume.js +4 -1
- package/dist/commands/secrets-passthrough.js +2 -2
- package/dist/commands/send.d.ts +4 -4
- package/dist/commands/send.js +6 -51
- package/dist/commands/sessions-backup-setup.js +1 -1
- package/dist/commands/sessions-resume.js +0 -1
- package/dist/commands/sessions-share.d.ts +5 -7
- package/dist/commands/sessions-share.js +98 -49
- package/dist/commands/sessions.js +1 -8
- package/dist/commands/setup-browser.js +18 -2
- package/dist/commands/setup-computer.js +20 -6
- package/dist/commands/setup-secrets.js +24 -4
- package/dist/commands/setup-terminal.d.ts +3 -0
- package/dist/commands/setup-terminal.js +22 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +20 -10
- package/dist/commands/skills.js +0 -8
- package/dist/commands/ssh.d.ts +6 -0
- package/dist/commands/ssh.js +92 -233
- package/dist/commands/sync.js +14 -5
- package/dist/commands/teams.js +1 -1
- package/dist/commands/traces.js +1 -1
- package/dist/lib/accounts/add.d.ts +0 -5
- package/dist/lib/accounts/add.js +1 -7
- package/dist/lib/artifacts-client.d.ts +20 -0
- package/dist/lib/artifacts-client.js +46 -0
- package/dist/lib/auth-mint.js +2 -2
- package/dist/lib/browser/runtime-state.d.ts +55 -0
- package/dist/lib/browser/runtime-state.js +99 -18
- package/dist/lib/browser/service.js +21 -1
- package/dist/lib/cli-resources.js +3 -1
- package/dist/lib/cloud/dispatch.js +1 -1
- package/dist/lib/cloudflare/creds.d.ts +10 -0
- package/dist/lib/cloudflare/creds.js +46 -0
- package/dist/lib/cloudflare/provision.d.ts +35 -0
- package/dist/lib/cloudflare/provision.js +144 -0
- package/dist/lib/computer/sessions-list.d.ts +55 -0
- package/dist/lib/computer/sessions-list.js +168 -1
- package/dist/lib/daemon/daemon.js +8 -1
- package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
- package/dist/lib/daemon/feed-stream-service.js +40 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/devices/connect.d.ts +49 -5
- package/dist/lib/devices/connect.js +169 -21
- package/dist/lib/feed/envelope.d.ts +79 -0
- package/dist/lib/feed/envelope.js +23 -0
- package/dist/lib/feed/events.d.ts +6 -0
- package/dist/lib/feed/events.js +8 -0
- package/dist/lib/feed/hub-server.d.ts +86 -0
- package/dist/lib/feed/hub-server.js +334 -0
- package/dist/lib/feed/hub.d.ts +95 -0
- package/dist/lib/feed/hub.js +255 -0
- package/dist/lib/feed/tool-activity.d.ts +108 -0
- package/dist/lib/feed/tool-activity.js +313 -0
- package/dist/lib/feed/tools.d.ts +198 -0
- package/dist/lib/feed/tools.js +265 -0
- package/dist/lib/feed/watch.d.ts +50 -50
- package/dist/lib/feed/watch.js +147 -16
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/git.d.ts +0 -16
- package/dist/lib/git.js +0 -58
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hosts/remote-cmd.d.ts +51 -1
- package/dist/lib/hosts/remote-cmd.js +125 -8
- package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
- package/dist/lib/mcp.js +17 -11
- package/dist/lib/probe.d.ts +4 -1
- package/dist/lib/probe.js +5 -2
- package/dist/lib/pwsh.d.ts +33 -0
- package/dist/lib/pwsh.js +56 -0
- package/dist/lib/redact.d.ts +8 -0
- package/dist/lib/redact.js +11 -0
- package/dist/lib/refresh.d.ts +6 -2
- package/dist/lib/refresh.js +92 -72
- package/dist/lib/secrets-cli.d.ts +11 -0
- package/dist/lib/secrets-cli.js +30 -0
- package/dist/lib/secrets-client.js +3 -2
- package/dist/lib/session/detached.d.ts +7 -0
- package/dist/lib/session/detached.js +29 -0
- package/dist/lib/session/remote/peer-stream.d.ts +24 -2
- package/dist/lib/session/remote/peer-stream.js +33 -6
- package/dist/lib/session/sync/backend.d.ts +3 -3
- package/dist/lib/session/sync/backend.js +3 -3
- package/dist/lib/session/sync/provision.d.ts +1 -1
- package/dist/lib/session/sync/provision.js +2 -2
- package/dist/lib/sessions-client.js +0 -3
- package/dist/lib/setup-tool-install.d.ts +3 -0
- package/dist/lib/setup-tool-install.js +26 -0
- package/dist/lib/setup-tool-status.d.ts +22 -0
- package/dist/lib/setup-tool-status.js +215 -0
- package/dist/lib/share-runtime.d.ts +11 -0
- package/dist/lib/share-runtime.js +63 -0
- package/dist/lib/smart-launch.d.ts +1 -5
- package/dist/lib/smart-launch.js +3 -11
- package/dist/lib/ssh-exec.d.ts +44 -0
- package/dist/lib/ssh-exec.js +119 -0
- package/dist/lib/startup/command-registry.d.ts +6 -4
- package/dist/lib/startup/command-registry.js +10 -7
- package/dist/lib/state.js +2 -2
- package/dist/lib/storage/selection.d.ts +2 -2
- package/dist/lib/storage/selection.js +2 -2
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +18 -10
- package/dist/lib/traces/backend.d.ts +1 -2
- package/dist/lib/traces/backend.js +1 -2
- package/dist/lib/traces/provision.d.ts +1 -1
- package/dist/lib/traces/provision.js +2 -2
- package/dist/lib/types.d.ts +8 -6
- package/package.json +2 -3
- package/dist/commands/artifacts-setup.d.ts +0 -53
- package/dist/commands/artifacts-setup.js +0 -161
- package/dist/commands/artifacts.d.ts +0 -18
- package/dist/commands/artifacts.js +0 -58
- package/dist/commands/attach.d.ts +0 -12
- package/dist/commands/attach.js +0 -86
- package/dist/commands/auth-mint.d.ts +0 -12
- package/dist/commands/auth-mint.js +0 -108
- package/dist/commands/reconnect.d.ts +0 -46
- package/dist/commands/reconnect.js +0 -115
- package/dist/commands/share.d.ts +0 -293
- package/dist/commands/share.js +0 -1424
- package/dist/lib/share/analytics.d.ts +0 -13
- package/dist/lib/share/analytics.js +0 -45
- package/dist/lib/share/backend.d.ts +0 -120
- package/dist/lib/share/backend.js +0 -176
- package/dist/lib/share/capture.d.ts +0 -31
- package/dist/lib/share/capture.js +0 -174
- package/dist/lib/share/config.d.ts +0 -72
- package/dist/lib/share/config.js +0 -211
- package/dist/lib/share/delete.d.ts +0 -123
- package/dist/lib/share/delete.js +0 -173
- package/dist/lib/share/html.d.ts +0 -20
- package/dist/lib/share/html.js +0 -88
- package/dist/lib/share/http-error.d.ts +0 -53
- package/dist/lib/share/http-error.js +0 -65
- package/dist/lib/share/og.d.ts +0 -26
- package/dist/lib/share/og.js +0 -84
- package/dist/lib/share/provision.d.ts +0 -127
- package/dist/lib/share/provision.js +0 -285
- package/dist/lib/share/publish.d.ts +0 -379
- package/dist/lib/share/publish.js +0 -818
- package/dist/lib/share/worker-template.d.ts +0 -27
- package/dist/lib/share/worker-template.js +0 -2424
- package/dist/lib/storage/index.d.ts +0 -14
- package/dist/lib/storage/index.js +0 -14
- package/dist/lib/storage/visibility.d.ts +0 -82
- package/dist/lib/storage/visibility.js +0 -99
package/dist/commands/browser.js
CHANGED
|
@@ -25,7 +25,11 @@ import { withActorEnv } from '../lib/hosts/dispatch.js';
|
|
|
25
25
|
import { streamAgentsOnHost, passthroughSshOptions, } from '../lib/hosts/passthrough.js';
|
|
26
26
|
import { buildRemoteAgentsInvocation, HOST_ROUTING_SPECS, stripRoutingFlags, } from '../lib/hosts/remote-cmd.js';
|
|
27
27
|
import { resolveRemoteOsSync } from '../lib/hosts/remote-os.js';
|
|
28
|
-
import { sshExec, SSH_OPTS } from '../lib/ssh-exec.js';
|
|
28
|
+
import { sshExec, SSH_OPTS, sshStreamWithArgs } from '../lib/ssh-exec.js';
|
|
29
|
+
import { getBrowserRuntimeDir } from '../lib/state.js';
|
|
30
|
+
import { buildSshInvocation, writeAskpassShim } from '../lib/devices/connect.js';
|
|
31
|
+
import { resolveDeviceProfile } from '../lib/devices/resolve-profile.js';
|
|
32
|
+
import { resolveDeviceTarget } from '../lib/devices/resolve-target.js';
|
|
29
33
|
import { flagValue } from '../lib/hosts/routing-flag.js';
|
|
30
34
|
import { browserTaskPicker } from './browser-picker.js';
|
|
31
35
|
import { assertRemoteControlAllowed, isFleetRemoteInvocation } from '../lib/browser/remote-control.js';
|
|
@@ -35,7 +39,7 @@ import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
|
35
39
|
import { buildHar } from '../lib/browser/har.js';
|
|
36
40
|
import { getCliVersion } from '../lib/version.js';
|
|
37
41
|
import { runBrowserIPCStream } from '../lib/browser/stream.js';
|
|
38
|
-
import { machineId } from '../lib/machine-id.js';
|
|
42
|
+
import { machineId, normalizeHost } from '../lib/machine-id.js';
|
|
39
43
|
import { isArcRunning } from '../lib/browser/drivers/arc.js';
|
|
40
44
|
import { resolveBrowserTarget } from '../lib/browser/resolve-target.js';
|
|
41
45
|
/**
|
|
@@ -179,6 +183,158 @@ export function remoteStartTaskName(stdout, explicit) {
|
|
|
179
183
|
}
|
|
180
184
|
return trimmed.split('\n').map((line) => line.trim()).find(Boolean);
|
|
181
185
|
}
|
|
186
|
+
/**
|
|
187
|
+
* Fetch an absolute remote file into a private local path, bounded.
|
|
188
|
+
*
|
|
189
|
+
* Bounded for real, not by a pre-flight `stat`: the byte budget is enforced on the
|
|
190
|
+
* stream as it arrives (`sshExecRawStream` + `cat`), so a file that grows between
|
|
191
|
+
* a size check and the copy — or a peer that lies about the size — still cannot
|
|
192
|
+
* spend more than the budget. `scp` could not do this; it would transfer
|
|
193
|
+
* everything before this process got a chance to refuse.
|
|
194
|
+
*
|
|
195
|
+
* The local file is written 0600 inside a 0700 directory, because a capture can
|
|
196
|
+
* show anything that was on the peer's screen and a world-readable temp path
|
|
197
|
+
* would publish it to every local user.
|
|
198
|
+
*/
|
|
199
|
+
/**
|
|
200
|
+
* The command that streams one file's RAW BYTES to stdout on the device's shell.
|
|
201
|
+
*
|
|
202
|
+
* Platform-correct, not one command with a portable-looking name. On Windows
|
|
203
|
+
* `cat` is an alias for `Get-Content`, which is a TEXT reader: it decodes the
|
|
204
|
+
* file, splits it into lines, and re-encodes on output — so a screenshot arrives
|
|
205
|
+
* corrupted rather than merely reordered. The .NET path opens a `FileStream` and
|
|
206
|
+
* copies it into the raw stdout handle, bypassing PowerShell's object/text
|
|
207
|
+
* pipeline entirely, which is the only way binary survives.
|
|
208
|
+
*/
|
|
209
|
+
export function remoteReadFileArgv(remotePath, shell) {
|
|
210
|
+
if (shell === 'powershell') {
|
|
211
|
+
// Single-quoted inside the script with pwsh's own doubling, because this whole
|
|
212
|
+
// script is ONE argv token to the outer powershell.
|
|
213
|
+
const literal = remotePath.replace(/'/g, "''");
|
|
214
|
+
return ['powershell', '-NoProfile', '-Command', [
|
|
215
|
+
'$ErrorActionPreference=' + "'Stop';",
|
|
216
|
+
`$in=[System.IO.File]::OpenRead('${literal}');`,
|
|
217
|
+
'try{$out=[System.Console]::OpenStandardOutput();$in.CopyTo($out);$out.Flush()}finally{$in.Dispose()}',
|
|
218
|
+
].join(' ')];
|
|
219
|
+
}
|
|
220
|
+
// `--` so a path that begins with a dash is a path, not an option.
|
|
221
|
+
return ['cat', '--', remotePath];
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* Largest capture `browser show --device` will pull. A capture is a screenshot,
|
|
225
|
+
* a PDF or a short recording; anything past this is not something a viewer should
|
|
226
|
+
* stream over ssh, and the cap is what makes the transfer BOUNDED rather than
|
|
227
|
+
* "however big the peer's file happens to be".
|
|
228
|
+
*/
|
|
229
|
+
export const REMOTE_VIEW_MAX_BYTES = 64 * 1024 * 1024;
|
|
230
|
+
/** Write a whole buffer, looping until it is all out. */
|
|
231
|
+
function writeFully(handle, chunk) {
|
|
232
|
+
let offset = 0;
|
|
233
|
+
while (offset < chunk.length) {
|
|
234
|
+
// `writeSync` may write FEWER bytes than asked (a signal, a pipe boundary),
|
|
235
|
+
// and ignoring the return value silently truncated the file mid-capture.
|
|
236
|
+
const wrote = fs.writeSync(handle, chunk, offset, chunk.length - offset);
|
|
237
|
+
if (wrote <= 0)
|
|
238
|
+
throw new Error('local write made no progress');
|
|
239
|
+
offset += wrote;
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Fetch an absolute remote file into a private local path, bounded.
|
|
244
|
+
*
|
|
245
|
+
* Auth is the CANONICAL device path, not a hand-rolled ssh: `buildSshInvocation`
|
|
246
|
+
* supplies the askpass shim for a password-auth box, `-i`/`IdentitiesOnly` for an
|
|
247
|
+
* explicit identity file, and the managed known-hosts pinning. Building the ssh
|
|
248
|
+
* args here instead meant a password-auth device could not be reached at all and
|
|
249
|
+
* an explicit `identityFile` was silently ignored.
|
|
250
|
+
*
|
|
251
|
+
* Bounded for real, not by a pre-flight `stat`: the byte budget is enforced on the
|
|
252
|
+
* stream as it arrives, so a file that grows between a size check and the copy — or
|
|
253
|
+
* a peer that misreports its size — still cannot spend more than the budget.
|
|
254
|
+
*
|
|
255
|
+
* The local file is written 0600 inside a 0700 directory, because a capture can
|
|
256
|
+
* show anything that was on the peer's screen and a world-readable temp path
|
|
257
|
+
* would publish it to every local user.
|
|
258
|
+
*/
|
|
259
|
+
export async function fetchRemoteFileForViewing(device, remotePath, localPath, opts = {}) {
|
|
260
|
+
const posixAbsolute = path.posix.isAbsolute(remotePath);
|
|
261
|
+
const windowsAbsolute = /^[A-Za-z]:[\\/]/.test(remotePath) || /^\\\\/.test(remotePath);
|
|
262
|
+
if (!posixAbsolute && !windowsAbsolute) {
|
|
263
|
+
throw new Error(`Remote path must be absolute: got "${remotePath}".\n`
|
|
264
|
+
+ 'This machine cannot resolve the peer\'s working directory, so a relative path\n'
|
|
265
|
+
+ 'would name a different file there than you meant here.');
|
|
266
|
+
}
|
|
267
|
+
// `resolveDeviceTarget` is the SAME resolver `agents ssh` uses, so it yields the
|
|
268
|
+
// full device profile — platform, shell and auth — that `buildSshInvocation`
|
|
269
|
+
// needs. `resolveHost` returns a hosts-registry `Host`, which carries none of
|
|
270
|
+
// that: with it, a password-auth device could not be reached and an explicit
|
|
271
|
+
// identityFile was silently dropped.
|
|
272
|
+
const resolved = await resolveDeviceTarget(device);
|
|
273
|
+
if (!resolved)
|
|
274
|
+
throw new Error(`Unknown device "${device}". Next: agents devices list`);
|
|
275
|
+
const profile = resolveDeviceProfile(resolved);
|
|
276
|
+
const maxBytes = opts.maxBytes ?? REMOTE_VIEW_MAX_BYTES;
|
|
277
|
+
// A POSIX device cannot read a Windows path and vice versa; saying so here beats
|
|
278
|
+
// a confusing shell error from the peer.
|
|
279
|
+
if (profile.shell === 'powershell' && posixAbsolute && !windowsAbsolute) {
|
|
280
|
+
throw new Error(`"${remotePath}" is a POSIX path but ${device} runs PowerShell. Pass the Windows path (e.g. C:\\Users\\…).`);
|
|
281
|
+
}
|
|
282
|
+
if (profile.shell !== 'powershell' && windowsAbsolute) {
|
|
283
|
+
throw new Error(`"${remotePath}" is a Windows path but ${device} is a POSIX host. Pass the absolute POSIX path.`);
|
|
284
|
+
}
|
|
285
|
+
const resolvedLocal = path.resolve(localPath);
|
|
286
|
+
const dir = path.dirname(resolvedLocal);
|
|
287
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
288
|
+
fs.chmodSync(dir, 0o700);
|
|
289
|
+
const handle = fs.openSync(resolvedLocal, 'w', 0o600);
|
|
290
|
+
let written = 0;
|
|
291
|
+
let overflow = false;
|
|
292
|
+
const controller = new AbortController();
|
|
293
|
+
try {
|
|
294
|
+
// `argv: true` is what makes a path containing a space or a quote survive —
|
|
295
|
+
// the same exact-token delivery `agents ssh --argv` exposes.
|
|
296
|
+
const { args, env } = buildSshInvocation(profile, remoteReadFileArgv(remotePath, profile.shell), writeAskpassShim(), {}, { argv: true });
|
|
297
|
+
const result = await sshStreamWithArgs({
|
|
298
|
+
args,
|
|
299
|
+
env,
|
|
300
|
+
timeoutMs: opts.timeoutMs ?? 120_000,
|
|
301
|
+
signal: controller.signal,
|
|
302
|
+
onStdout: (chunk) => {
|
|
303
|
+
if (overflow)
|
|
304
|
+
return;
|
|
305
|
+
if (written + chunk.length > maxBytes) {
|
|
306
|
+
overflow = true;
|
|
307
|
+
controller.abort();
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
writeFully(handle, chunk);
|
|
311
|
+
written += chunk.length;
|
|
312
|
+
},
|
|
313
|
+
});
|
|
314
|
+
if (overflow) {
|
|
315
|
+
throw new Error(`Refusing to pull ${device}:${remotePath}: larger than the ${maxBytes}-byte view budget.`);
|
|
316
|
+
}
|
|
317
|
+
if (result.timedOut)
|
|
318
|
+
throw new Error(`Timed out pulling ${device}:${remotePath}.`);
|
|
319
|
+
if (result.code !== 0) {
|
|
320
|
+
const detail = result.stderr.toString('utf8').trim();
|
|
321
|
+
throw new Error(`Failed to read ${device}:${remotePath}${detail ? `: ${detail}` : ` (ssh exited ${result.code})`}`);
|
|
322
|
+
}
|
|
323
|
+
if (written === 0)
|
|
324
|
+
throw new Error(`${device}:${remotePath} is empty or was not readable.`);
|
|
325
|
+
}
|
|
326
|
+
catch (error) {
|
|
327
|
+
// Never leave a partial file behind for a viewer to open as if it were whole.
|
|
328
|
+
try {
|
|
329
|
+
fs.unlinkSync(resolvedLocal);
|
|
330
|
+
}
|
|
331
|
+
catch { /* nothing to clean */ }
|
|
332
|
+
throw error;
|
|
333
|
+
}
|
|
334
|
+
finally {
|
|
335
|
+
fs.closeSync(handle);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
182
338
|
async function pullRemoteFile(device, remotePath, localPath) {
|
|
183
339
|
const host = await resolveHost(device);
|
|
184
340
|
if (!host) {
|
|
@@ -2071,8 +2227,14 @@ function registerTaskCommands(browser) {
|
|
|
2071
2227
|
});
|
|
2072
2228
|
browser
|
|
2073
2229
|
.command('show <url>')
|
|
2074
|
-
.description('Open a URL for a human to read: goes to browser.viewer (default: browser.profile), and binds no task')
|
|
2230
|
+
.description('Open a URL or local file for a human to read: goes to browser.viewer (default: browser.profile), and binds no task')
|
|
2075
2231
|
.option('--os-browser', 'Use the OS default handler instead of the configured viewer')
|
|
2232
|
+
// `--device` here names WHERE THE FILE IS, not where to open it. `show` is the
|
|
2233
|
+
// one browser verb narrowly exempt from the task-routing refusal
|
|
2234
|
+
// (`REJECT_DEVICE_MESSAGE`) because it binds no task and has nothing to route:
|
|
2235
|
+
// it is a local viewer, so the flag can only mean "the source lives there".
|
|
2236
|
+
// Every other verb still refuses `--device` — a task is bound at `start`.
|
|
2237
|
+
.option('--device <name>', 'fetch the file from this device, then view it locally (absolute path only)')
|
|
2076
2238
|
.option('--json', 'Output machine-readable JSON')
|
|
2077
2239
|
.action(async (url, opts) => {
|
|
2078
2240
|
// The entry point external tools need. `navigate` binds a task, which the
|
|
@@ -2083,20 +2245,48 @@ function registerTaskCommands(browser) {
|
|
|
2083
2245
|
// drive letter (`C:\Users\me\plan.html`), so a real path was treated as a
|
|
2084
2246
|
// URL. A scheme is at least two characters.
|
|
2085
2247
|
const isLocalFile = !/^[a-z][a-z0-9+.-]+:/i.test(url);
|
|
2086
|
-
|
|
2087
|
-
|
|
2248
|
+
let localTarget = isLocalFile ? path.resolve(url) : undefined;
|
|
2249
|
+
let fetched;
|
|
2250
|
+
if (opts.device) {
|
|
2251
|
+
// A URL is already reachable from here, so `--device` with one is a
|
|
2252
|
+
// contradiction rather than a no-op: it says the source is remote while
|
|
2253
|
+
// naming something that is not a file on that peer.
|
|
2254
|
+
if (!isLocalFile) {
|
|
2255
|
+
console.error(`--device names the device holding a FILE, so it cannot be combined with a URL.\n Drop --device to open ${url} here, or pass an absolute path on ${opts.device}.`);
|
|
2256
|
+
process.exit(1);
|
|
2257
|
+
}
|
|
2258
|
+
// The peer's path, verbatim — NOT path.resolve()'d, which would rewrite it
|
|
2259
|
+
// against this machine's cwd and name a different file there.
|
|
2260
|
+
const remotePath = url;
|
|
2261
|
+
const dir = path.join(getBrowserRuntimeDir(), 'remote-views');
|
|
2262
|
+
fetched = path.join(dir, `${normalizeHost(opts.device)}-${Date.now()}-${path.basename(remotePath) || 'capture'}`);
|
|
2263
|
+
try {
|
|
2264
|
+
await fetchRemoteFileForViewing(opts.device, remotePath, fetched);
|
|
2265
|
+
}
|
|
2266
|
+
catch (error) {
|
|
2267
|
+
console.error(error.message);
|
|
2268
|
+
process.exit(1);
|
|
2269
|
+
}
|
|
2270
|
+
localTarget = fetched;
|
|
2271
|
+
}
|
|
2272
|
+
const outcome = localTarget
|
|
2273
|
+
? await showFile(localTarget, { osBrowser: opts.osBrowser })
|
|
2088
2274
|
: await showUrl(url, { osBrowser: opts.osBrowser });
|
|
2275
|
+
// The fetched copy is what a viewer actually opened, so it is what the
|
|
2276
|
+
// caller is told about — pointing at the peer's path would name a file that
|
|
2277
|
+
// is not on this machine.
|
|
2278
|
+
const shown = fetched ?? url;
|
|
2089
2279
|
if (opts.json) {
|
|
2090
|
-
console.log(JSON.stringify(outcome, null, 2));
|
|
2280
|
+
console.log(JSON.stringify({ ...outcome, ...(fetched ? { source: `${opts.device}:${url}`, local: fetched } : {}) }, null, 2));
|
|
2091
2281
|
}
|
|
2092
2282
|
else if (outcome.via === 'none') {
|
|
2093
|
-
console.error(`Could not open a browser — open this yourself:\n ${
|
|
2283
|
+
console.error(`Could not open a browser — open this yourself:\n ${shown}`);
|
|
2094
2284
|
}
|
|
2095
2285
|
else if (outcome.via === 'profile') {
|
|
2096
|
-
console.log(`Shown in ${outcome.profile}: ${
|
|
2286
|
+
console.log(`Shown in ${outcome.profile}: ${shown}`);
|
|
2097
2287
|
}
|
|
2098
2288
|
else {
|
|
2099
|
-
console.log(`Opened in the OS default browser: ${
|
|
2289
|
+
console.log(`Opened in the OS default browser: ${shown}`);
|
|
2100
2290
|
}
|
|
2101
2291
|
if (outcome.via === 'none')
|
|
2102
2292
|
process.exit(1);
|
|
@@ -18,9 +18,8 @@
|
|
|
18
18
|
* Read-only always: doctor diagnoses, it never mutates. To fix the gaps it
|
|
19
19
|
* finds — install missing resources, repair Claude-invalid plugin manifests,
|
|
20
20
|
* refresh stale plugins, reconcile drift, repair hook runtime shims — run
|
|
21
|
-
* `agents sync <agent>@all` (the one fixer
|
|
22
|
-
*
|
|
23
|
-
* deprecated no-op that now errors with a pointer to `agents sync`.
|
|
21
|
+
* `agents sync <agent>@all` (the one fixer). Run `agents prune cleanup` to act
|
|
22
|
+
* on orphan readouts.
|
|
24
23
|
*/
|
|
25
24
|
import type { Command } from 'commander';
|
|
26
25
|
import { type FleetInventory } from '../lib/devices/fleet-divergence.js';
|
package/dist/commands/doctor.js
CHANGED
|
@@ -1251,7 +1251,6 @@ export function registerDoctorCommand(program) {
|
|
|
1251
1251
|
.description('Diagnose CLI availability, sync status, and resource divergence (optionally for a specific agent[@version]).')
|
|
1252
1252
|
.option('--json', 'Output machine-readable JSON')
|
|
1253
1253
|
.option('--diff', 'In target mode, include unified diffs for divergent files')
|
|
1254
|
-
.option('--fix', '(deprecated) doctor is diagnose-only — this errors with a pointer to `agents sync`, the one fixer')
|
|
1255
1254
|
.option('--kind <kinds>', 'Restrict to comma-separated resource kinds (commands,skills,hooks,rules,mcp,permissions,subagents,plugins,workflows,memory)')
|
|
1256
1255
|
.option('--cwd <path>', 'Resolution cwd for project layer detection (default: process.cwd())')
|
|
1257
1256
|
.option('--adopt <agent>', "Take over the agent's native launcher that shadows the shim (symlink it to the version-managed shim; reversible with --release)")
|
|
@@ -1381,16 +1380,6 @@ export function registerDoctorCommand(program) {
|
|
|
1381
1380
|
}
|
|
1382
1381
|
return;
|
|
1383
1382
|
}
|
|
1384
|
-
// `doctor --fix` moved to `agents sync` — doctor is diagnose-only now.
|
|
1385
|
-
// Keep the flag for one release as a signpost: it never heals, it points at
|
|
1386
|
-
// the one fixer and exits non-zero so scripts and muscle memory get a clear
|
|
1387
|
-
// redirect instead of a silent no-op.
|
|
1388
|
-
if (opts.fix) {
|
|
1389
|
-
const where = target ? `agents sync ${target}` : 'agents sync <agent>@all';
|
|
1390
|
-
console.error(chalk.yellow('`agents doctor --fix` has moved to `agents sync`.'));
|
|
1391
|
-
console.error(chalk.gray(` doctor now only diagnoses. To fix, run: ${where}`));
|
|
1392
|
-
process.exit(1);
|
|
1393
|
-
}
|
|
1394
1383
|
if (!target) {
|
|
1395
1384
|
// Singleflight + short-TTL cache for the bare `doctor --json` overview.
|
|
1396
1385
|
// This overview probes every host CLI, every agent's sign-in, and every
|
|
@@ -42,13 +42,11 @@ export interface EventsOptions {
|
|
|
42
42
|
limit?: string;
|
|
43
43
|
json?: boolean;
|
|
44
44
|
follow?: boolean;
|
|
45
|
-
/** @deprecated Prefer --include ops / --exclude activity */
|
|
46
|
-
audit?: boolean;
|
|
47
45
|
include?: string;
|
|
48
46
|
exclude?: string;
|
|
49
47
|
}
|
|
50
48
|
/** Add the one canonical event-reader option surface to a command or alias. */
|
|
51
|
-
export declare function addEventsReadOptions(command: Command
|
|
49
|
+
export declare function addEventsReadOptions(command: Command): Command;
|
|
52
50
|
/** Canonical reader used by `agents events`, `agents events audit`, and `agents logs`. */
|
|
53
51
|
export declare function runEventsCommand(options: EventsOptions, forceAudit?: boolean): Promise<void>;
|
|
54
52
|
/** Shared by `events stats` and the `logs stats` alias. */
|
package/dist/commands/events.js
CHANGED
|
@@ -195,7 +195,7 @@ function registerEmitSubcommand(events) {
|
|
|
195
195
|
});
|
|
196
196
|
}
|
|
197
197
|
/** Add the one canonical event-reader option surface to a command or alias. */
|
|
198
|
-
export function addEventsReadOptions(command
|
|
198
|
+
export function addEventsReadOptions(command) {
|
|
199
199
|
command
|
|
200
200
|
.option('--include <families>', `Only these families (comma-sep): ${EVENT_FAMILIES.join(', ')}`)
|
|
201
201
|
.option('--exclude <families>', `Drop these families (comma-sep): ${EVENT_FAMILIES.join(', ')}`)
|
|
@@ -211,9 +211,6 @@ export function addEventsReadOptions(command, includeAuditFlag = true) {
|
|
|
211
211
|
.option('--limit <n>', 'Max records to show; 0 for no cap (default 50)', '50')
|
|
212
212
|
.option('--json', 'Output raw records as JSON')
|
|
213
213
|
.option('-f, --follow', "Tail today's operational log live");
|
|
214
|
-
if (includeAuditFlag) {
|
|
215
|
-
command.option('--audit', 'Deprecated: operational events only — prefer --include ops or --exclude activity');
|
|
216
|
-
}
|
|
217
214
|
return command;
|
|
218
215
|
}
|
|
219
216
|
/** Canonical reader used by `agents events`, `agents events audit`, and `agents logs`. */
|
|
@@ -242,11 +239,11 @@ export async function runEventsCommand(options, forceAudit = false) {
|
|
|
242
239
|
process.exitCode = 2;
|
|
243
240
|
return;
|
|
244
241
|
}
|
|
245
|
-
//
|
|
246
|
-
// source selection via applyFamilies — never override includeActivity after.
|
|
242
|
+
// forceAudit (`events audit` / `logs`) = ops-only when no family flags. Families
|
|
243
|
+
// own the source selection via applyFamilies — never override includeActivity after.
|
|
247
244
|
const includeActivity = (includeFamilies !== undefined || excludeFamilies !== undefined)
|
|
248
245
|
? true
|
|
249
|
-
: !
|
|
246
|
+
: !forceAudit;
|
|
250
247
|
const fetched = readUnifiedEvents({
|
|
251
248
|
startDate,
|
|
252
249
|
eventTypes: options.event && options.event.length ? options.event : undefined,
|
package/dist/commands/exec.d.ts
CHANGED
package/dist/commands/exec.js
CHANGED
|
@@ -660,8 +660,6 @@ export function registerRunCommand(program) {
|
|
|
660
660
|
// `--on` and `--computer` are hidden aliases of `--device` — same behavior.
|
|
661
661
|
runCmd.addOption(new Option('--on <name>', 'Alias of --device.').hideHelp());
|
|
662
662
|
runCmd.addOption(new Option('--computer <name>', 'Alias of --device.').hideHelp());
|
|
663
|
-
// Deprecated one-release alias: `agents run … --smart` → treat as `--device auto`.
|
|
664
|
-
runCmd.addOption(new Option('--smart', 'Deprecated: use --device auto (affinity host pick).').hideHelp());
|
|
665
663
|
// Internal: the `--device` dispatch forwards this so the REMOTE run prints its
|
|
666
664
|
// resolved session id as a one-line stdout sentinel (hosts/session-marker.ts),
|
|
667
665
|
// letting the launcher relate the remote-created session back to itself for
|
|
@@ -1226,7 +1224,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1226
1224
|
const defaultPlacement = bareInteractiveRunDefaultsToDeviceAuto(options, { prompt, devicePickerRequested }, { tty: isInteractiveTerminal(), json: options.json });
|
|
1227
1225
|
if (defaultPlacement)
|
|
1228
1226
|
options.device = 'auto';
|
|
1229
|
-
// --device auto
|
|
1227
|
+
// --device auto: live fleet pick.
|
|
1230
1228
|
// Harness is always the agent the user typed — never auto-picked.
|
|
1231
1229
|
// Placement failure propagates; an automatic request never becomes local.
|
|
1232
1230
|
{
|
|
@@ -1252,9 +1250,6 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
1252
1250
|
console.error(chalk.gray(`Run here instead: agents run ${runBaseAgentName} --device ${machineId()}`));
|
|
1253
1251
|
process.exit(1);
|
|
1254
1252
|
}
|
|
1255
|
-
if (!options.quiet && result.deprecationSmart) {
|
|
1256
|
-
process.stderr.write(chalk.yellow('[agents] --smart is deprecated; use --device auto\n'));
|
|
1257
|
-
}
|
|
1258
1253
|
if (!options.quiet && result.banner) {
|
|
1259
1254
|
const { hostLabel, deviceHint, acctNote } = result.banner;
|
|
1260
1255
|
process.stderr.write(chalk.gray(`[agents] device=auto → ${hostLabel}` +
|
|
@@ -2064,7 +2059,7 @@ agents run auto --device yosemite-s0 "fix the flaky test" # pin the device
|
|
|
2064
2059
|
import('../lib/run-defaults.js'),
|
|
2065
2060
|
import('../lib/mcp.js'),
|
|
2066
2061
|
import('../lib/capabilities.js'),
|
|
2067
|
-
import('../lib/share
|
|
2062
|
+
import('../lib/share-runtime.js'),
|
|
2068
2063
|
]);
|
|
2069
2064
|
bootMark('run-deps:imported');
|
|
2070
2065
|
const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
|
|
@@ -1,10 +1,51 @@
|
|
|
1
|
-
import { machineId } from '../lib/machine-id.js';
|
|
2
1
|
import { setHelpSections } from '../lib/help.js';
|
|
3
|
-
import {
|
|
2
|
+
import { streamFeedFromHub, waitForHub } from '../lib/feed/hub-server.js';
|
|
3
|
+
import { ensureDaemonStarted } from '../lib/daemon/daemon.js';
|
|
4
|
+
/**
|
|
5
|
+
* Attach to the daemon's shared collector, starting the daemon if it is not up.
|
|
6
|
+
*
|
|
7
|
+
* There is deliberately no in-process fallback: running `watchFleetFeed` here
|
|
8
|
+
* would give this reader its own ssh child per peer, which is exactly the
|
|
9
|
+
* per-caller fan-out the hub exists to collapse (see `lib/feed/hub.ts`). So a
|
|
10
|
+
* missing hub is answered by starting its owner and WAITING for it.
|
|
11
|
+
*
|
|
12
|
+
* The wait is what makes that honest. `ensureDaemonStarted()` returns once the
|
|
13
|
+
* process is spawned, long before it has loaded its services and bound the
|
|
14
|
+
* socket, so retrying immediately raced the bind and reported a daemon that was
|
|
15
|
+
* about to be perfectly healthy as unavailable. The final error is the one from
|
|
16
|
+
* the last real attempt, not the first.
|
|
17
|
+
*/
|
|
18
|
+
async function attachToHub(signal, emit, scope) {
|
|
19
|
+
try {
|
|
20
|
+
await streamFeedFromHub({ signal, emit, scope });
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
catch (first) {
|
|
24
|
+
if (signal.aborted)
|
|
25
|
+
return;
|
|
26
|
+
ensureDaemonStarted();
|
|
27
|
+
if (!await waitForHub()) {
|
|
28
|
+
throw new Error(`the shared feed stream did not come up: ${first.message}\n`
|
|
29
|
+
+ 'Next: agents daemon status, then agents daemon services enable feed-stream');
|
|
30
|
+
}
|
|
31
|
+
try {
|
|
32
|
+
await streamFeedFromHub({ signal, emit, scope });
|
|
33
|
+
}
|
|
34
|
+
catch (second) {
|
|
35
|
+
throw new Error(`the shared feed stream is unavailable: ${second.message}\n`
|
|
36
|
+
+ 'Next: agents daemon status, then agents daemon services enable feed-stream');
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
}
|
|
4
40
|
export function registerFeedWatchCommand(parent) {
|
|
5
|
-
const command = parent.command('watch').description('Stream the canonical agent, attention, and activity projection as NDJSON')
|
|
41
|
+
const command = parent.command('watch').description('Stream the canonical agent, attention, tool, and activity projection as NDJSON')
|
|
6
42
|
.option('--json', 'Emit versioned NDJSON envelopes').option('--local', 'Watch only this machine');
|
|
7
|
-
setHelpSections(command, {
|
|
43
|
+
setHelpSections(command, {
|
|
44
|
+
examples: `agents feed watch --json\nagents feed watch --json --local`,
|
|
45
|
+
notes: 'Order by streamId + sequence. Unavailable scopes retain their last rows until reset on reconnect. '
|
|
46
|
+
+ 'The fleet stream is served by the daemon\'s shared collector, so several readers cost one connection per peer; '
|
|
47
|
+
+ '--local is the per-machine stream the collector itself subscribes to over ssh.',
|
|
48
|
+
});
|
|
8
49
|
command.action(async (_opts, invoked) => {
|
|
9
50
|
const opts = invoked.optsWithGlobals();
|
|
10
51
|
if (!opts.json)
|
|
@@ -15,10 +56,13 @@ export function registerFeedWatchCommand(parent) {
|
|
|
15
56
|
process.once('SIGINT', stop);
|
|
16
57
|
process.once('SIGTERM', stop);
|
|
17
58
|
try {
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
59
|
+
// BOTH scopes go through the daemon's shared collectors. `--local` used to
|
|
60
|
+
// run its own `watchLocalFeed`, so every observing box's ssh subscription
|
|
61
|
+
// built a separate activity cursor set, tool watcher and setup subscription
|
|
62
|
+
// on the SAME peer. The local collector is a distinct hub from the fleet one
|
|
63
|
+
// (`feed-stream-service.ts`) and its watcher dials no peer at all, so a
|
|
64
|
+
// local reader can never trigger a fan-out, recursively or otherwise.
|
|
65
|
+
await attachToHub(controller.signal, emit, opts.local ? 'local' : 'fleet');
|
|
22
66
|
}
|
|
23
67
|
finally {
|
|
24
68
|
process.off('SIGINT', stop);
|
package/dist/commands/focus.js
CHANGED
|
@@ -136,7 +136,7 @@ export function registerFocusCommand(program) {
|
|
|
136
136
|
# Resolve latest on yosemite-s0, then pick from that version's sessions
|
|
137
137
|
agents sessions focus claude@latest --device yosemite-s0
|
|
138
138
|
|
|
139
|
-
# Attach only — refuse if nothing is joinable
|
|
139
|
+
# Attach only — refuse if nothing is joinable
|
|
140
140
|
agents sessions focus a1b2c3d4 --attach-only
|
|
141
141
|
|
|
142
142
|
# Pick from live sessions on this machine only
|
package/dist/commands/go.d.ts
CHANGED
|
@@ -1,23 +1,6 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* `agents sessions go [id]` — DEPRECATED alias for `agents sessions focus --attach-only`.
|
|
3
|
-
*
|
|
4
|
-
* `go` was "attach or refuse" (never fork/resume). `focus --attach-only` is exactly
|
|
5
|
-
* that behavior, so `go` now prints a deprecation notice and delegates to `focusAction`.
|
|
6
|
-
*
|
|
7
|
-
* This file still owns the shared reach engine that `focus` imports:
|
|
8
|
-
* - `gatherLiveTargets` / `pickLiveTarget` / `buildLivePool` — live-session discovery + picker
|
|
9
|
-
* - `jumpTo` — the side-effecting jump: attach the already-running terminal
|
|
10
|
-
* local tmux -> attach (switch-client when already inside tmux)
|
|
11
|
-
* local Ghostty -> focus its tab (Cmd+<n> via System Events; tab # from ghostty-tabs)
|
|
12
|
-
* remote tmux -> ssh -tt + tmux attach (pane->session resolved on the remote)
|
|
13
|
-
* otherwise -> hand off to the `UnreachableFallback` (attach-only refuses; focus resumes)
|
|
14
|
-
* - `refuseFallback` — the attach-only fallback (remote -> login shell; local -> refuse)
|
|
15
|
-
*/
|
|
16
|
-
import type { Command } from 'commander';
|
|
17
1
|
import { type ActiveSession } from '../lib/session/active.js';
|
|
18
2
|
import type { SessionMeta } from '../lib/session/types.js';
|
|
19
3
|
import { type LiveStatusFilter } from './sessions.js';
|
|
20
|
-
export declare function registerGoCommand(program: Command): void;
|
|
21
4
|
/**
|
|
22
5
|
* Scope a live-session pool by device and live status. Pure so the `focus`
|
|
23
6
|
* device/status filters are unit-testable without touching the sweep. `hosts`
|
package/dist/commands/go.js
CHANGED
|
@@ -1,10 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
3
|
-
*
|
|
4
|
-
* `go` was "attach or refuse" (never fork/resume). `focus --attach-only` is exactly
|
|
5
|
-
* that behavior, so `go` now prints a deprecation notice and delegates to `focusAction`.
|
|
6
|
-
*
|
|
7
|
-
* This file still owns the shared reach engine that `focus` imports:
|
|
2
|
+
* The shared live-session reach engine that `sessions focus` / `resume --attach-only`
|
|
3
|
+
* import ("attach or refuse", never fork/resume):
|
|
8
4
|
* - `gatherLiveTargets` / `pickLiveTarget` / `buildLivePool` — live-session discovery + picker
|
|
9
5
|
* - `jumpTo` — the side-effecting jump: attach the already-running terminal
|
|
10
6
|
* local tmux -> attach (switch-client when already inside tmux)
|
|
@@ -26,7 +22,6 @@ import { dedupeByMachineSession, mergeLocalFirst, pickSessionInteractive, matche
|
|
|
26
22
|
import { buildPreview } from './sessions-picker.js';
|
|
27
23
|
import { multiItemPicker } from '../lib/picker.js';
|
|
28
24
|
import { isPromptCancelled } from './utils.js';
|
|
29
|
-
import { focusAction } from './focus.js';
|
|
30
25
|
import { machineId } from '../lib/session/sync/config.js';
|
|
31
26
|
import { attachTmux, runTmux } from '../lib/tmux/binary.js';
|
|
32
27
|
import { ensureSessionHookRepaired, paneExitStatus, teardownIfAgentExited } from '../lib/tmux/session.js';
|
|
@@ -36,18 +31,6 @@ import { connectionEndedNotice } from '../lib/hosts/reconnect.js';
|
|
|
36
31
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
37
32
|
import { addressabilityRecoveryHint } from '../lib/terminal/resolve.js';
|
|
38
33
|
const execFileAsync = promisify(execFile);
|
|
39
|
-
export function registerGoCommand(program) {
|
|
40
|
-
program
|
|
41
|
-
.command('go', { hidden: true })
|
|
42
|
-
.argument('[id]', 'Short/full session id to jump to; omit for an interactive picker')
|
|
43
|
-
.option('--local', 'Only this machine (skip the cross-host sweep)')
|
|
44
|
-
.description('Deprecated — use `agents sessions resume --attach-only` instead.')
|
|
45
|
-
.action(async (id, opts) => {
|
|
46
|
-
console.warn(chalk.yellow('`agents sessions go` is deprecated — use `agents sessions resume --attach-only` instead:'));
|
|
47
|
-
console.warn(chalk.gray(` agents sessions resume ${id ?? '<id>'} --attach-only`));
|
|
48
|
-
await focusAction(id, { local: opts.local, attachOnly: true });
|
|
49
|
-
});
|
|
50
|
-
}
|
|
51
34
|
/**
|
|
52
35
|
* Scope a live-session pool by device and live status. Pure so the `focus`
|
|
53
36
|
* device/status filters are unit-testable without touching the sweep. `hosts`
|
package/dist/commands/logs.js
CHANGED
|
@@ -263,7 +263,7 @@ export function registerLogsCommand(program) {
|
|
|
263
263
|
.action((id, opts) => runLogs(id, opts));
|
|
264
264
|
addEventsReadOptions(logsCmd
|
|
265
265
|
.command('audit')
|
|
266
|
-
.description('Alias for `agents events
|
|
266
|
+
.description('Alias for `agents events audit`'))
|
|
267
267
|
.addHelpText('after', `
|
|
268
268
|
Examples:
|
|
269
269
|
agents logs audit Recent activity across everything
|