@phnx-labs/agents-cli 1.22.45 → 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.
- package/CHANGELOG.md +303 -0
- package/LICENSE +102 -182
- package/README.md +54 -17
- package/dist/cli/command-registry.d.ts +2 -1
- package/dist/cli/command-registry.js +6 -2
- package/dist/commands/accounts.js +56 -12
- package/dist/commands/auth.d.ts +2 -0
- package/dist/commands/auth.js +229 -0
- package/dist/commands/browser-sessions-picker.js +9 -7
- package/dist/commands/browser.js +266 -15
- package/dist/commands/config.js +26 -4
- package/dist/commands/cost.d.ts +1 -1
- package/dist/commands/detach.d.ts +13 -0
- package/dist/commands/detach.js +11 -8
- package/dist/commands/exec.js +55 -38
- package/dist/commands/feed-watch.d.ts +2 -0
- package/dist/commands/feed-watch.js +28 -0
- package/dist/commands/feed.js +27 -0
- package/dist/commands/feedback.js +3 -19
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +2 -2
- package/dist/commands/lease.js +3 -3
- package/dist/commands/routines.js +1 -1
- package/dist/commands/sessions-picker-factory.d.ts +2 -1
- package/dist/commands/sessions-picker-factory.js +1 -1
- package/dist/commands/sessions-stop.d.ts +24 -0
- package/dist/commands/sessions-stop.js +66 -0
- package/dist/commands/sessions-trace.d.ts +82 -0
- package/dist/commands/sessions-trace.js +387 -0
- package/dist/commands/sessions.js +4 -0
- package/dist/commands/ssh.d.ts +15 -0
- package/dist/commands/ssh.js +194 -16
- package/dist/commands/teams.js +34 -2
- package/dist/commands/view.d.ts +2 -0
- package/dist/commands/view.js +19 -32
- package/dist/commands/watchdog.js +9 -14
- package/dist/lib/account-capabilities.d.ts +4 -0
- package/dist/lib/account-capabilities.js +13 -9
- package/dist/lib/account-registry.d.ts +2 -0
- package/dist/lib/account-registry.js +30 -2
- package/dist/lib/accounting/rotate.d.ts +9 -0
- package/dist/lib/accounting/rotate.js +12 -2
- package/dist/lib/accounting/usage.d.ts +92 -6
- package/dist/lib/accounting/usage.js +329 -75
- package/dist/lib/agent-modes.js +29 -1
- package/dist/lib/agent-spec/agents.d.ts +4 -3
- package/dist/lib/agent-spec/agents.js +20 -7
- package/dist/lib/analytics/mix-commands.d.ts +1 -1
- package/dist/lib/analytics/mix-commands.js +2 -2
- package/dist/lib/auth-health.d.ts +13 -3
- package/dist/lib/auth-health.js +50 -8
- package/dist/lib/browser/devices.d.ts +19 -0
- package/dist/lib/browser/devices.js +23 -0
- package/dist/lib/browser/ipc.d.ts +1 -1
- package/dist/lib/browser/ipc.js +33 -7
- package/dist/lib/browser/profiles.d.ts +106 -1
- package/dist/lib/browser/profiles.js +300 -17
- package/dist/lib/browser/remote-control.d.ts +26 -2
- package/dist/lib/browser/remote-control.js +33 -3
- package/dist/lib/browser/runtime-state.d.ts +9 -0
- package/dist/lib/browser/runtime-state.js +24 -8
- package/dist/lib/browser/service.d.ts +37 -0
- package/dist/lib/browser/service.js +78 -20
- package/dist/lib/browser/sessions-list.d.ts +1 -3
- package/dist/lib/browser/sessions-list.js +3 -16
- package/dist/lib/browser/types.d.ts +12 -1
- package/dist/lib/cli-resources.js +17 -15
- package/dist/lib/codex-policy.d.ts +11 -1
- package/dist/lib/codex-policy.js +20 -5
- package/dist/lib/config-keys.d.ts +4 -3
- package/dist/lib/config-keys.js +17 -7
- package/dist/lib/daemon/runner.js +15 -2
- package/dist/lib/daemon-ticks.js +2 -2
- package/dist/lib/device-config.d.ts +25 -8
- package/dist/lib/device-config.js +90 -17
- package/dist/lib/devices/config-migration.d.ts +7 -0
- package/dist/lib/devices/config-migration.js +67 -3
- package/dist/lib/devices/harness-inventory.js +20 -3
- package/dist/lib/devices/health.d.ts +27 -2
- package/dist/lib/devices/health.js +39 -9
- package/dist/lib/devices/interactive-host.d.ts +15 -0
- package/dist/lib/devices/interactive-host.js +53 -0
- package/dist/lib/devices/registry.d.ts +61 -4
- package/dist/lib/devices/registry.js +94 -38
- package/dist/lib/devices/stats-cache.d.ts +6 -0
- package/dist/lib/devices/stats-cache.js +10 -2
- package/dist/lib/exec.d.ts +25 -3
- package/dist/lib/exec.js +75 -22
- package/dist/lib/feed/answer.d.ts +29 -0
- package/dist/lib/feed/answer.js +145 -0
- package/dist/lib/feed/attention.d.ts +83 -0
- package/dist/lib/feed/attention.js +0 -0
- package/dist/lib/feed/feed.d.ts +109 -0
- package/dist/lib/feed/feed.js +154 -3
- package/dist/lib/feed/pr-status.d.ts +14 -0
- package/dist/lib/feed/pr-status.js +47 -0
- package/dist/lib/feed/watch.d.ts +59 -0
- package/dist/lib/feed/watch.js +180 -0
- package/dist/lib/fleet/capture.js +6 -0
- package/dist/lib/fleet/remote-login.js +8 -3
- package/dist/lib/fleet/types.d.ts +17 -0
- package/dist/lib/harness/adapter.d.ts +11 -0
- package/dist/lib/harness/adapters/claude.js +38 -24
- package/dist/lib/harness/adapters/codex.js +2 -4
- package/dist/lib/hooks/cache.js +8 -0
- package/dist/lib/hooks/match.d.ts +3 -0
- package/dist/lib/hooks/match.js +25 -0
- package/dist/lib/hosts/passthrough.js +16 -0
- package/dist/lib/hosts/registry.js +7 -0
- package/dist/lib/identity/client.d.ts +53 -0
- package/dist/lib/identity/client.js +106 -0
- package/dist/lib/identity/index.d.ts +115 -0
- package/dist/lib/identity/index.js +82 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.d.ts +13 -0
- package/dist/lib/menubar/install-menubar.js +26 -2
- package/dist/lib/open-url.d.ts +60 -2
- package/dist/lib/open-url.js +167 -12
- package/dist/lib/probe.d.ts +8 -0
- package/dist/lib/probe.js +105 -0
- package/dist/lib/refresh-coordinator.d.ts +1 -1
- package/dist/lib/refresh-coordinator.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +2 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +13 -1
- package/dist/lib/session/active.d.ts +67 -0
- package/dist/lib/session/active.js +69 -1
- package/dist/lib/session/db.js +4 -2
- package/dist/lib/session/discover.d.ts +2 -0
- package/dist/lib/session/discover.js +12 -6
- package/dist/lib/session/prompt.d.ts +35 -0
- package/dist/lib/session/prompt.js +56 -0
- package/dist/lib/session/remote/watch.d.ts +1 -1
- package/dist/lib/session/remote/watch.js +6 -1
- package/dist/lib/session/render.js +7 -3
- package/dist/lib/session/session-cache.js +5 -1
- package/dist/lib/session/trajectory-compare.d.ts +47 -0
- package/dist/lib/session/trajectory-compare.js +116 -0
- package/dist/lib/session/trajectory-html.d.ts +15 -0
- package/dist/lib/session/trajectory-html.js +750 -0
- package/dist/lib/session/trajectory-lineage.d.ts +92 -0
- package/dist/lib/session/trajectory-lineage.js +195 -0
- package/dist/lib/session/trajectory-text.d.ts +37 -0
- package/dist/lib/session/trajectory-text.js +277 -0
- package/dist/lib/session/trajectory.d.ts +102 -0
- package/dist/lib/session/trajectory.js +335 -0
- package/dist/lib/startup/command-registry.d.ts +5 -2
- package/dist/lib/startup/command-registry.js +8 -5
- package/dist/lib/state.d.ts +1 -1
- package/dist/lib/state.js +4 -2
- package/dist/lib/teams/agents.js +9 -0
- package/dist/lib/terminal/inject.d.ts +19 -0
- package/dist/lib/terminal/inject.js +19 -9
- package/dist/lib/tmux/session.d.ts +46 -13
- package/dist/lib/tmux/session.js +173 -27
- package/dist/lib/types.d.ts +16 -0
- package/dist/lib/usage-backoff.d.ts +2 -1
- package/dist/lib/usage-backoff.js +60 -19
- package/dist/lib/usage-refresh.d.ts +17 -3
- package/dist/lib/usage-refresh.js +57 -11
- package/dist/lib/view-types.d.ts +2 -2
- package/dist/lib/watchdog/log.d.ts +1 -1
- package/dist/lib/watchdog/log.js +1 -1
- package/dist/lib/watchdog/runner.d.ts +36 -45
- package/dist/lib/watchdog/runner.js +150 -119
- package/dist/lib/watchdog/service.d.ts +0 -1
- package/dist/lib/watchdog/service.js +0 -1
- package/dist/lib/watchdog/watchdog-agent.d.ts +38 -0
- package/dist/lib/watchdog/watchdog-agent.js +59 -0
- package/dist/lib/watchdog/watchdog.d.ts +11 -2
- package/dist/lib/watchdog/watchdog.js +37 -104
- package/package.json +2 -2
- package/dist/commands/usage.d.ts +0 -27
- package/dist/commands/usage.js +0 -96
package/dist/commands/browser.js
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
import chalk from 'chalk';
|
|
2
2
|
import * as fs from 'fs';
|
|
3
3
|
import * as path from 'path';
|
|
4
|
-
import { listProfiles, getProfile, createProfile, deleteProfile, getConfiguredDefaultProfileName, resolveProfileRef, resolveProfileRefForStart, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, listProfilesWithScope, formatProfilesTable, } from '../lib/browser/profiles.js';
|
|
4
|
+
import { listProfiles, getProfile, createProfile, deleteProfile, getConfiguredDefaultProfileName, resolveProfileRef, resolveProfileRefForStart, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, listProfilesWithScope, formatProfilesTable, editProfile, moveProfileScope, renameProfile, assertRegistrableProfileName, misfiledFleetProfile, } from '../lib/browser/profiles.js';
|
|
5
5
|
import { resolveActor } from '../lib/actor.js';
|
|
6
6
|
import { loginsForProfile, profilesLoggedInto, serviceForUrl, loginsWithAccountsForProfile, accountsForProfile, credKeysForService, AUTH_SIGNATURES, } from '../lib/browser/login-detection.js';
|
|
7
7
|
import { parseSecretRef } from '../lib/browser/secret-ref.js';
|
|
8
8
|
import { readAndResolveBundleEnv, bundleExists, readBundle, describeBundle } from '../lib/secrets/bundles.js';
|
|
9
9
|
import { findBrowserPath, getPortOccupant, isLauncherScript, listInstalledBrowsers } from '../lib/browser/chrome.js';
|
|
10
10
|
import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, buildProfilePrunePlan, pruneProfiles, PRUNE_REASON_TEXT, } from '../lib/browser/runtime-state.js';
|
|
11
|
-
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
11
|
+
import { DEFAULT_VIEWPORT, parseWindowSize, parseWindowPosition } from '../lib/browser/devices.js';
|
|
12
12
|
import { runBrowserSessionsCommand } from './browser-sessions-picker.js';
|
|
13
13
|
import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
|
|
14
14
|
import { parseTargetFilter } from '../lib/browser/service.js';
|
|
@@ -330,8 +330,11 @@ function registerProfilesCommands(browser) {
|
|
|
330
330
|
.option('--electron', 'Treat this profile as an Electron desktop app: never call Target.createTarget; bind to the visible window using --target-filter or the skip-invisible heuristic')
|
|
331
331
|
.option('--target-filter <expr>', 'Pick the visible CDP page target when the app exposes more than one. Format: url:<substring> or title:<substring>')
|
|
332
332
|
.action(async (name, opts) => {
|
|
333
|
-
|
|
334
|
-
|
|
333
|
+
try {
|
|
334
|
+
assertRegistrableProfileName(name);
|
|
335
|
+
}
|
|
336
|
+
catch (err) {
|
|
337
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
335
338
|
process.exit(1);
|
|
336
339
|
}
|
|
337
340
|
if (!VALID_BROWSERS.includes(opts.browser)) {
|
|
@@ -370,22 +373,22 @@ function registerProfilesCommands(browser) {
|
|
|
370
373
|
height: DEFAULT_VIEWPORT.height,
|
|
371
374
|
};
|
|
372
375
|
if (opts.window) {
|
|
373
|
-
const
|
|
374
|
-
if (!
|
|
376
|
+
const size = parseWindowSize(String(opts.window));
|
|
377
|
+
if (!size) {
|
|
375
378
|
console.error(`--window must be WxH, e.g. ${DEFAULT_VIEWPORT.width}x${DEFAULT_VIEWPORT.height}`);
|
|
376
379
|
process.exit(1);
|
|
377
380
|
}
|
|
378
|
-
viewport.width =
|
|
379
|
-
viewport.height =
|
|
381
|
+
viewport.width = size.width;
|
|
382
|
+
viewport.height = size.height;
|
|
380
383
|
}
|
|
381
384
|
if (opts.position) {
|
|
382
|
-
const
|
|
383
|
-
if (!
|
|
385
|
+
const pos = parseWindowPosition(String(opts.position));
|
|
386
|
+
if (!pos) {
|
|
384
387
|
console.error('--position must be X,Y, e.g. 80,80');
|
|
385
388
|
process.exit(1);
|
|
386
389
|
}
|
|
387
|
-
viewport.x =
|
|
388
|
-
viewport.y =
|
|
390
|
+
viewport.x = pos.x;
|
|
391
|
+
viewport.y = pos.y;
|
|
389
392
|
}
|
|
390
393
|
const profile = {
|
|
391
394
|
name,
|
|
@@ -407,6 +410,193 @@ function registerProfilesCommands(browser) {
|
|
|
407
410
|
console.error(`warning: secrets bundle "${opts.secrets}" does not exist yet. Create it with: agents secrets create ${opts.secrets}`);
|
|
408
411
|
}
|
|
409
412
|
});
|
|
413
|
+
profiles
|
|
414
|
+
.command('edit <name>')
|
|
415
|
+
.description('Edit an existing profile in place (stays in the store it already lives in)')
|
|
416
|
+
// Declared only so the action can explain WHY the browser type is not
|
|
417
|
+
// editable. Without it commander rejects `-b` as an unknown option before
|
|
418
|
+
// the action runs, and the delete-and-recreate guidance never reaches anyone.
|
|
419
|
+
.option('-b, --browser <type>', 'Not editable — see the error for the delete-and-recreate path')
|
|
420
|
+
.option('-d, --description <text>', "Profile description (pass '' to clear)")
|
|
421
|
+
.option('-e, --endpoint <url>', 'Replace the endpoint list (repeatable)', collect, [])
|
|
422
|
+
.option('-s, --secrets <bundle>', 'Secrets bundle to inject')
|
|
423
|
+
.option('--headless', 'Run in headless mode')
|
|
424
|
+
.option('--no-headless', 'Run headed')
|
|
425
|
+
.option('--window <WxH>', 'Window size in CSS pixels')
|
|
426
|
+
.option('--position <X,Y>', 'Window position on screen, e.g. 80,80')
|
|
427
|
+
.option('--binary <path>', 'Absolute path to the browser/app binary')
|
|
428
|
+
.option('--electron', 'Treat this profile as an Electron desktop app')
|
|
429
|
+
.option('--no-electron', 'Stop treating it as an Electron app')
|
|
430
|
+
.option('--target-filter <expr>', "url:<substring> or title:<substring>; requires --electron (pass '' to clear)")
|
|
431
|
+
.option('--json', 'Output machine-readable JSON')
|
|
432
|
+
.action(async (name, opts) => {
|
|
433
|
+
// The browser type and the name are identity, not settings: both key the
|
|
434
|
+
// on-disk runtime dir and any live `<name>@<endpoint>` connection, so
|
|
435
|
+
// changing either orphans the cached browser data (and its logins).
|
|
436
|
+
if (opts.browser) {
|
|
437
|
+
console.error('browser type is not editable — it keys the on-disk profile cache. ' +
|
|
438
|
+
`Delete and recreate instead: agents browser profiles delete ${name} && agents browser profiles create ${name} -b <type>`);
|
|
439
|
+
process.exit(1);
|
|
440
|
+
}
|
|
441
|
+
if (opts.targetFilter) {
|
|
442
|
+
const parsed = parseTargetFilter(String(opts.targetFilter));
|
|
443
|
+
if (!parsed) {
|
|
444
|
+
console.error('--target-filter must be url:<substring> or title:<substring> (non-empty value, no leading whitespace)');
|
|
445
|
+
process.exit(1);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
const patch = {};
|
|
449
|
+
if (opts.description !== undefined)
|
|
450
|
+
patch.description = opts.description || undefined;
|
|
451
|
+
if (opts.endpoint && opts.endpoint.length > 0)
|
|
452
|
+
patch.endpoints = opts.endpoint;
|
|
453
|
+
if (opts.secrets !== undefined)
|
|
454
|
+
patch.secrets = opts.secrets;
|
|
455
|
+
if (opts.binary !== undefined)
|
|
456
|
+
patch.binary = opts.binary;
|
|
457
|
+
if (opts.targetFilter !== undefined)
|
|
458
|
+
patch.targetFilter = opts.targetFilter || undefined;
|
|
459
|
+
// commander maps --electron/--no-electron and --headless/--no-headless onto
|
|
460
|
+
// one boolean each, and leaves it undefined when neither was passed.
|
|
461
|
+
if (opts.electron !== undefined)
|
|
462
|
+
patch.electron = opts.electron || undefined;
|
|
463
|
+
if (opts.headless !== undefined || opts.window || opts.position) {
|
|
464
|
+
const current = await getProfile(name);
|
|
465
|
+
if (!current) {
|
|
466
|
+
console.error(`Profile "${name}" does not exist`);
|
|
467
|
+
process.exit(1);
|
|
468
|
+
}
|
|
469
|
+
if (opts.headless !== undefined) {
|
|
470
|
+
const chrome = { ...current.chrome };
|
|
471
|
+
if (opts.headless)
|
|
472
|
+
chrome.headless = true;
|
|
473
|
+
else
|
|
474
|
+
delete chrome.headless;
|
|
475
|
+
// `{ headless: undefined }` is truthy, so profileToConfig would persist
|
|
476
|
+
// a bare `chrome: {}` into the YAML and the change-detector would report
|
|
477
|
+
// a phantom edit on an already-headed profile. Drop the key entirely.
|
|
478
|
+
patch.chrome = Object.keys(chrome).length > 0 ? chrome : undefined;
|
|
479
|
+
}
|
|
480
|
+
if (opts.window || opts.position) {
|
|
481
|
+
const viewport = { ...(current.viewport ?? { width: DEFAULT_VIEWPORT.width, height: DEFAULT_VIEWPORT.height }) };
|
|
482
|
+
if (opts.window) {
|
|
483
|
+
const size = parseWindowSize(String(opts.window));
|
|
484
|
+
if (!size) {
|
|
485
|
+
console.error('--window must be WxH, e.g. 1512x982');
|
|
486
|
+
process.exit(1);
|
|
487
|
+
}
|
|
488
|
+
viewport.width = size.width;
|
|
489
|
+
viewport.height = size.height;
|
|
490
|
+
}
|
|
491
|
+
if (opts.position) {
|
|
492
|
+
const pos = parseWindowPosition(String(opts.position));
|
|
493
|
+
if (!pos) {
|
|
494
|
+
console.error('--position must be X,Y, e.g. 80,80');
|
|
495
|
+
process.exit(1);
|
|
496
|
+
}
|
|
497
|
+
viewport.x = pos.x;
|
|
498
|
+
viewport.y = pos.y;
|
|
499
|
+
}
|
|
500
|
+
patch.viewport = viewport;
|
|
501
|
+
}
|
|
502
|
+
}
|
|
503
|
+
if (Object.keys(patch).length === 0) {
|
|
504
|
+
console.error('Nothing to edit. Pass at least one field, e.g. -d "<description>".');
|
|
505
|
+
process.exit(1);
|
|
506
|
+
}
|
|
507
|
+
let result;
|
|
508
|
+
try {
|
|
509
|
+
result = await editProfile(name, patch);
|
|
510
|
+
}
|
|
511
|
+
catch (err) {
|
|
512
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
513
|
+
process.exit(1);
|
|
514
|
+
}
|
|
515
|
+
if (opts.json) {
|
|
516
|
+
console.log(JSON.stringify({ ...result.profile, scope: result.scope, changed: result.changed }, null, 2));
|
|
517
|
+
}
|
|
518
|
+
else if (result.changed.length === 0) {
|
|
519
|
+
console.log(`No change: ${name} already had those values.`);
|
|
520
|
+
}
|
|
521
|
+
else {
|
|
522
|
+
console.log(`Updated ${name} (${result.scope}): ${result.changed.join(', ')}`);
|
|
523
|
+
}
|
|
524
|
+
if (result.fleetCopyLeftStale) {
|
|
525
|
+
console.error(`warning: "${name}" is machine-local by rule, so the edit was written to this machine only. ` +
|
|
526
|
+
`The fleet-synced copy still holds the old values on every other box.`);
|
|
527
|
+
}
|
|
528
|
+
if (patch.secrets && !bundleExists(patch.secrets)) {
|
|
529
|
+
console.error(`warning: secrets bundle "${patch.secrets}" does not exist yet. Create it with: agents secrets create ${patch.secrets}`);
|
|
530
|
+
}
|
|
531
|
+
});
|
|
532
|
+
profiles
|
|
533
|
+
.command('rename <from> <to>')
|
|
534
|
+
.description("Rename a profile, moving its browser data with it (logins survive)")
|
|
535
|
+
.option('--json', 'Output machine-readable JSON')
|
|
536
|
+
.action(async (from, to, opts) => {
|
|
537
|
+
let res;
|
|
538
|
+
try {
|
|
539
|
+
res = await renameProfile(from, to);
|
|
540
|
+
}
|
|
541
|
+
catch (err) {
|
|
542
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
543
|
+
process.exit(1);
|
|
544
|
+
}
|
|
545
|
+
if (opts.json) {
|
|
546
|
+
console.log(JSON.stringify({ from, to, ...res }, null, 2));
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
console.log(`Renamed ${from} -> ${to} (${res.scope})`);
|
|
550
|
+
if (res.movedDirs.length > 0) {
|
|
551
|
+
console.log(` moved ${res.movedDirs.length} browser data dir${res.movedDirs.length === 1 ? '' : 's'} — logins preserved`);
|
|
552
|
+
}
|
|
553
|
+
if (res.repointedDefault) {
|
|
554
|
+
console.log(` browser.profile now points at ${to}`);
|
|
555
|
+
}
|
|
556
|
+
if (res.repointedViewer) {
|
|
557
|
+
console.log(` browser.viewer now points at ${to}`);
|
|
558
|
+
}
|
|
559
|
+
if (res.stalePins.length > 0) {
|
|
560
|
+
const devices = [...new Set(res.stalePins.map((p) => p.device))];
|
|
561
|
+
const one = devices.length === 1;
|
|
562
|
+
console.error(`warning: ${devices.join(', ')} still ${one ? 'pins' : 'pin'} "${from}" in ` +
|
|
563
|
+
`${one ? 'its' : 'their'} own device config. Fix with:`);
|
|
564
|
+
for (const pin of res.stalePins) {
|
|
565
|
+
console.error(` agents config set ${pin.key} ${to} --device ${pin.device}`);
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
});
|
|
569
|
+
profiles
|
|
570
|
+
.command('scope <name> <scope>')
|
|
571
|
+
.description('Move a profile between the fleet-synced store and this machine (local|fleet)')
|
|
572
|
+
.option('--json', 'Output machine-readable JSON')
|
|
573
|
+
.action(async (name, scope, opts) => {
|
|
574
|
+
if (scope !== 'local' && scope !== 'fleet') {
|
|
575
|
+
console.error('scope must be `local` or `fleet`');
|
|
576
|
+
process.exit(1);
|
|
577
|
+
}
|
|
578
|
+
let moved;
|
|
579
|
+
try {
|
|
580
|
+
moved = await moveProfileScope(name, scope);
|
|
581
|
+
}
|
|
582
|
+
catch (err) {
|
|
583
|
+
console.error(err instanceof Error ? err.message : String(err));
|
|
584
|
+
process.exit(1);
|
|
585
|
+
}
|
|
586
|
+
if (opts.json) {
|
|
587
|
+
console.log(JSON.stringify({ profile: name, ...moved }, null, 2));
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
590
|
+
if (moved.from === moved.to) {
|
|
591
|
+
console.log(`${name} is already ${scope}.`);
|
|
592
|
+
return;
|
|
593
|
+
}
|
|
594
|
+
console.log(`Moved ${name}: ${moved.from} -> ${moved.to}`);
|
|
595
|
+
if (scope === 'local') {
|
|
596
|
+
console.error(`note: ${name} is no longer visible on any other machine. ` +
|
|
597
|
+
`Run this ON the machine that owns the browser — the surviving copy is this one.`);
|
|
598
|
+
}
|
|
599
|
+
});
|
|
410
600
|
profiles
|
|
411
601
|
.command('show <name>')
|
|
412
602
|
.description('Show profile details')
|
|
@@ -531,12 +721,29 @@ function registerProfilesCommands(browser) {
|
|
|
531
721
|
console.log(JSON.stringify({ dryRun: !!opts.dryRun, ...plan }, null, 2));
|
|
532
722
|
return;
|
|
533
723
|
}
|
|
724
|
+
// A misfiled fleet profile is never a prune candidate (deleting a fleet
|
|
725
|
+
// entry deletes it everywhere), so without this it would only ever surface
|
|
726
|
+
// in --json. It is the one kept-reason a user has to act on.
|
|
727
|
+
const misfiled = plan.kept.filter((k) => k.misfiled);
|
|
728
|
+
const reportMisfiled = () => {
|
|
729
|
+
if (misfiled.length === 0)
|
|
730
|
+
return;
|
|
731
|
+
console.log('');
|
|
732
|
+
console.log(`${misfiled.length} fleet profile${misfiled.length === 1 ? '' : 's'} ${misfiled.length === 1 ? 'is' : 'are'} misfiled — ` +
|
|
733
|
+
`fleet-synced but bound to a local port, so the name means a different browser on every machine:`);
|
|
734
|
+
for (const k of misfiled) {
|
|
735
|
+
console.log(` ${k.name} — agents browser profiles scope ${k.name} local`);
|
|
736
|
+
}
|
|
737
|
+
console.log('Run that on the machine that owns each browser. Nothing was moved for you.');
|
|
738
|
+
console.log('(Prune never deletes these — removing a fleet entry removes it everywhere.)');
|
|
739
|
+
};
|
|
534
740
|
if (plan.candidates.length === 0) {
|
|
535
741
|
console.log('Nothing to prune — every profile is in use, healthy, or protected.');
|
|
536
742
|
if (plan.kept.length > 0) {
|
|
537
743
|
for (const k of plan.kept)
|
|
538
744
|
console.log(` kept ${k.name} (${k.scope}) — ${k.why}`);
|
|
539
745
|
}
|
|
746
|
+
reportMisfiled();
|
|
540
747
|
return;
|
|
541
748
|
}
|
|
542
749
|
const verb = opts.dryRun ? 'Would remove' : 'Removed';
|
|
@@ -547,6 +754,7 @@ function registerProfilesCommands(browser) {
|
|
|
547
754
|
: '';
|
|
548
755
|
console.log(` ${c.name} (${c.scope}) — ${PRUNE_REASON_TEXT[c.reason]}${cache}`);
|
|
549
756
|
}
|
|
757
|
+
reportMisfiled();
|
|
550
758
|
if (opts.dryRun) {
|
|
551
759
|
console.log('');
|
|
552
760
|
console.log('Nothing was changed. Re-run without --dry-run to apply.');
|
|
@@ -594,6 +802,15 @@ function registerProfilesCommands(browser) {
|
|
|
594
802
|
process.exit(1);
|
|
595
803
|
}
|
|
596
804
|
const checks = [];
|
|
805
|
+
// 0. Store scope. A fleet-synced profile whose endpoint is loopback cdp://
|
|
806
|
+
// means a DIFFERENT browser on every machine — the name lies everywhere
|
|
807
|
+
// but here. Checked first because it invalidates the reading of every
|
|
808
|
+
// check below on any other box.
|
|
809
|
+
const scoped = (await listProfilesWithScope()).find((p) => p.profile.name === name);
|
|
810
|
+
const misfiled = scoped ? misfiledFleetProfile(profile, scoped.scope) : { misfiled: false };
|
|
811
|
+
checks.push(misfiled.misfiled
|
|
812
|
+
? { label: 'scope', ok: false, detail: misfiled.why }
|
|
813
|
+
: { label: 'scope', ok: true, detail: scoped ? `stored ${scoped.scope}` : 'stored local' });
|
|
597
814
|
// 1. Binary exists for declared browser type, and is a real executable we
|
|
598
815
|
// can drive — not a distro launcher script. findBrowserPath already
|
|
599
816
|
// unwraps the known Chromium wrappers to their ELF; if it still hands
|
|
@@ -836,9 +1053,11 @@ function registerTaskCommands(browser) {
|
|
|
836
1053
|
.option('--duration <sec>', 'Recording duration cap in seconds (with --record; default 60)', (v) => parseInt(v, 10))
|
|
837
1054
|
.option('--max-mb <mb>', 'Recording size cap in MB (with --record; default 25)', (v) => parseInt(v, 10))
|
|
838
1055
|
.action(async (opts) => {
|
|
839
|
-
//
|
|
840
|
-
//
|
|
841
|
-
//
|
|
1056
|
+
// Fast-fail copy of the consent gate, so a refused start never resolves or
|
|
1057
|
+
// auto-creates a profile. The AUTHORITATIVE gate is in the daemon
|
|
1058
|
+
// (BrowserService.start / resolveOrCreateTask, via
|
|
1059
|
+
// assertRemoteControlAllowedForRequest) — it has to be, because the page
|
|
1060
|
+
// verbs create a browser implicitly and never reach this command.
|
|
842
1061
|
try {
|
|
843
1062
|
assertRemoteControlAllowed();
|
|
844
1063
|
}
|
|
@@ -1053,6 +1272,38 @@ function registerTaskCommands(browser) {
|
|
|
1053
1272
|
console.log(` ${c.task} (${c.profile}, ${c.reason})`);
|
|
1054
1273
|
}
|
|
1055
1274
|
});
|
|
1275
|
+
browser
|
|
1276
|
+
.command('show <url>')
|
|
1277
|
+
.description('Open a URL for a human to read: goes to browser.viewer (default: browser.profile), and binds no task')
|
|
1278
|
+
.option('--os-browser', 'Use the OS default handler instead of the configured viewer')
|
|
1279
|
+
.option('--json', 'Output machine-readable JSON')
|
|
1280
|
+
.action(async (url, opts) => {
|
|
1281
|
+
// The entry point external tools need. `navigate` binds a task, which the
|
|
1282
|
+
// abandoned-task reaper closes when the calling session ends — wrong for a
|
|
1283
|
+
// page a person is reading. This does not.
|
|
1284
|
+
const { showUrl, showFile } = await import('../lib/open-url.js');
|
|
1285
|
+
// `+` not `*` on the scheme body: `[a-z][a-z0-9+.-]*:` matches a Windows
|
|
1286
|
+
// drive letter (`C:\Users\me\plan.html`), so a real path was treated as a
|
|
1287
|
+
// URL. A scheme is at least two characters.
|
|
1288
|
+
const isLocalFile = !/^[a-z][a-z0-9+.-]+:/i.test(url);
|
|
1289
|
+
const outcome = isLocalFile
|
|
1290
|
+
? await showFile(path.resolve(url), { osBrowser: opts.osBrowser })
|
|
1291
|
+
: await showUrl(url, { osBrowser: opts.osBrowser });
|
|
1292
|
+
if (opts.json) {
|
|
1293
|
+
console.log(JSON.stringify(outcome, null, 2));
|
|
1294
|
+
}
|
|
1295
|
+
else if (outcome.via === 'none') {
|
|
1296
|
+
console.error(`Could not open a browser — open this yourself:\n ${url}`);
|
|
1297
|
+
}
|
|
1298
|
+
else if (outcome.via === 'profile') {
|
|
1299
|
+
console.log(`Shown in ${outcome.profile}: ${url}`);
|
|
1300
|
+
}
|
|
1301
|
+
else {
|
|
1302
|
+
console.log(`Opened in the OS default browser: ${url}`);
|
|
1303
|
+
}
|
|
1304
|
+
if (outcome.via === 'none')
|
|
1305
|
+
process.exit(1);
|
|
1306
|
+
});
|
|
1056
1307
|
browser
|
|
1057
1308
|
.command('navigate [url]')
|
|
1058
1309
|
.alias('goto')
|
package/dist/commands/config.js
CHANGED
|
@@ -72,6 +72,7 @@ function parseValue(key, parsed, raw) {
|
|
|
72
72
|
case 'notes':
|
|
73
73
|
return raw.trim();
|
|
74
74
|
case 'browser.profile':
|
|
75
|
+
case 'browser.viewer':
|
|
75
76
|
return raw.trim();
|
|
76
77
|
}
|
|
77
78
|
// A device property with no arm above used to fall out of the switch and
|
|
@@ -121,7 +122,7 @@ function setConfig(parsed, value) {
|
|
|
121
122
|
// (same store `agents devices config` / getConfigValue use). Bare
|
|
122
123
|
// browser.profile targets this machine; devices.<name>.browser.profile
|
|
123
124
|
// targets a peer.
|
|
124
|
-
setConfigValue('browser.profile', value, parsed.device ? { device: parsed.device } : undefined);
|
|
125
|
+
setConfigValue(parsed.property === 'viewer' ? 'browser.viewer' : 'browser.profile', value, parsed.device ? { device: parsed.device } : undefined);
|
|
125
126
|
return;
|
|
126
127
|
}
|
|
127
128
|
case 'project':
|
|
@@ -167,8 +168,12 @@ function unsetConfig(parsed) {
|
|
|
167
168
|
}
|
|
168
169
|
case 'browser': {
|
|
169
170
|
const target = parsed.device ? { device: parsed.device } : undefined;
|
|
170
|
-
|
|
171
|
-
|
|
171
|
+
// Must follow parsed.property. Hardcoding 'browser.profile' here meant
|
|
172
|
+
// `config unset browser.viewer` deleted the user's browser.profile while
|
|
173
|
+
// printing success, and left browserViewer in place.
|
|
174
|
+
const name = parsed.property === 'viewer' ? 'browser.viewer' : 'browser.profile';
|
|
175
|
+
const had = getConfigValue(name, target).value !== undefined;
|
|
176
|
+
unsetConfigValue(name, target);
|
|
172
177
|
return had;
|
|
173
178
|
}
|
|
174
179
|
case 'project': {
|
|
@@ -204,7 +209,7 @@ function getConfig(parsed) {
|
|
|
204
209
|
case 'auto':
|
|
205
210
|
return getConfigValue('auto.pool').value;
|
|
206
211
|
case 'browser': {
|
|
207
|
-
return getConfigValue('browser.profile', parsed.device ? { device: parsed.device } : undefined).value;
|
|
212
|
+
return getConfigValue(parsed.property === 'viewer' ? 'browser.viewer' : 'browser.profile', parsed.device ? { device: parsed.device } : undefined).value;
|
|
208
213
|
}
|
|
209
214
|
case 'project':
|
|
210
215
|
return getProjectRoot();
|
|
@@ -274,6 +279,11 @@ function* listCentralConfigEntries() {
|
|
|
274
279
|
const key = 'browser.profile';
|
|
275
280
|
yield { key, value: browserProfile, hint: configKeyStorageHint(parseConfigKey(key)) };
|
|
276
281
|
}
|
|
282
|
+
const browserViewer = getConfigValue('browser.viewer').value;
|
|
283
|
+
if (browserViewer !== undefined) {
|
|
284
|
+
const key = 'browser.viewer';
|
|
285
|
+
yield { key, value: browserViewer, hint: configKeyStorageHint(parseConfigKey(key)) };
|
|
286
|
+
}
|
|
277
287
|
}
|
|
278
288
|
/** Collect device-scope config entries. */
|
|
279
289
|
function* listDeviceConfigEntries(device) {
|
|
@@ -314,7 +324,19 @@ function* listDeviceConfigEntries(device) {
|
|
|
314
324
|
continue;
|
|
315
325
|
key = `${prefix}browser.profile`;
|
|
316
326
|
break;
|
|
327
|
+
case 'browser.viewer':
|
|
328
|
+
// Same duplication rule as browser.profile above.
|
|
329
|
+
if (device === machineId())
|
|
330
|
+
continue;
|
|
331
|
+
key = `${prefix}browser.viewer`;
|
|
332
|
+
break;
|
|
317
333
|
default:
|
|
334
|
+
// A `default: continue` here silently drops any device property with no
|
|
335
|
+
// arm — which is how browser.viewer was invisible to `config list` after
|
|
336
|
+
// being added everywhere else. This is the fourth switch enumerating
|
|
337
|
+
// DeviceConfigProperty; unlike parseValue's it cannot use a `never`
|
|
338
|
+
// binding (it must keep skipping properties that are deliberately not
|
|
339
|
+
// listed), so the completeness test in config.test.ts is the guard.
|
|
318
340
|
continue;
|
|
319
341
|
}
|
|
320
342
|
yield { key, value: entry.value, hint: configKeyStorageHint(parseConfigKey(key)) };
|
package/dist/commands/cost.d.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* This is the read side of issue #323. Cost and duration are computed and
|
|
6
6
|
* persisted at scan time (see src/lib/session/discover.ts + db.ts); this
|
|
7
7
|
* command only queries and renders them — a pure SQLite/CLI win, no server,
|
|
8
|
-
* no telemetry. Distinct from `agents
|
|
8
|
+
* no telemetry. Distinct from `agents view`, which reports live rate-limit /
|
|
9
9
|
* quota status per agent and is left untouched.
|
|
10
10
|
*/
|
|
11
11
|
import type { Command } from 'commander';
|
|
@@ -1,5 +1,18 @@
|
|
|
1
1
|
import type { Command } from 'commander';
|
|
2
|
+
import type { ActiveSession } from '../lib/session/active.js';
|
|
2
3
|
export declare function registerDetachCommand(program: Command): void;
|
|
4
|
+
/**
|
|
5
|
+
* Stop a live agent's interactive process and tear down its mux. tmux-hosted
|
|
6
|
+
* sessions are killed by session (closes the pane, ends the agent, reaps its
|
|
7
|
+
* helper processes); a plain process is SIGTERM'd (then SIGKILLed) by pid. Either
|
|
8
|
+
* way we wait for the process to actually exit before returning.
|
|
9
|
+
*
|
|
10
|
+
* Shared by two callers: `detach` (stop the terminal so the headless resume it
|
|
11
|
+
* spawns next doesn't fight the old process over the same transcript) and `stop`
|
|
12
|
+
* (end the session outright — no resume). `socket` defaults to the shared agents
|
|
13
|
+
* socket; it is a parameter only so a test can drive an isolated tmux server.
|
|
14
|
+
*/
|
|
15
|
+
export declare function stopInteractive(s: ActiveSession, socket?: string): Promise<void>;
|
|
3
16
|
export declare function detachAction(id: string, opts?: {
|
|
4
17
|
local?: boolean;
|
|
5
18
|
}): Promise<void>;
|
package/dist/commands/detach.js
CHANGED
|
@@ -34,17 +34,20 @@ export function registerDetachCommand(program) {
|
|
|
34
34
|
});
|
|
35
35
|
}
|
|
36
36
|
/**
|
|
37
|
-
* Stop
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
* wait for the process to actually exit before returning
|
|
41
|
-
*
|
|
42
|
-
*
|
|
37
|
+
* Stop a live agent's interactive process and tear down its mux. tmux-hosted
|
|
38
|
+
* sessions are killed by session (closes the pane, ends the agent, reaps its
|
|
39
|
+
* helper processes); a plain process is SIGTERM'd (then SIGKILLed) by pid. Either
|
|
40
|
+
* way we wait for the process to actually exit before returning.
|
|
41
|
+
*
|
|
42
|
+
* Shared by two callers: `detach` (stop the terminal so the headless resume it
|
|
43
|
+
* spawns next doesn't fight the old process over the same transcript) and `stop`
|
|
44
|
+
* (end the session outright — no resume). `socket` defaults to the shared agents
|
|
45
|
+
* socket; it is a parameter only so a test can drive an isolated tmux server.
|
|
43
46
|
*/
|
|
44
|
-
async function stopInteractive(s) {
|
|
47
|
+
export async function stopInteractive(s, socket = getDefaultSocketPath()) {
|
|
45
48
|
if (s.tmuxTarget) {
|
|
46
49
|
const name = s.tmuxTarget.split(':')[0];
|
|
47
|
-
await killSession(name,
|
|
50
|
+
await killSession(name, socket);
|
|
48
51
|
}
|
|
49
52
|
else if (s.pid && s.pid > 0) {
|
|
50
53
|
try {
|