@phnx-labs/agents-cli 1.22.51 → 1.22.53
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 +238 -0
- package/README.md +1 -1
- package/dist/commands/accounts.js +1 -1
- package/dist/commands/attach.js +7 -0
- package/dist/commands/browser.js +118 -56
- package/dist/commands/daemon.d.ts +2 -0
- package/dist/commands/daemon.js +8 -4
- package/dist/commands/detach.js +1 -1
- package/dist/commands/exec.js +16 -9
- package/dist/commands/fleet-capture.js +7 -0
- package/dist/commands/focus.d.ts +1 -10
- package/dist/commands/focus.js +16 -79
- package/dist/commands/go.d.ts +26 -0
- package/dist/commands/go.js +65 -6
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/repo.js +31 -3
- package/dist/commands/sessions-inject.js +8 -3
- package/dist/commands/sessions-picker.js +2 -1
- package/dist/commands/sessions-resume.d.ts +1 -0
- package/dist/commands/sessions-resume.js +13 -2
- package/dist/commands/sessions-stop.js +1 -1
- package/dist/commands/sessions.d.ts +23 -13
- package/dist/commands/sessions.js +69 -39
- package/dist/commands/setup-browser.d.ts +5 -2
- package/dist/commands/setup-browser.js +14 -29
- package/dist/commands/setup-preferences.d.ts +22 -3
- package/dist/commands/setup-preferences.js +25 -8
- package/dist/commands/share.js +12 -8
- package/dist/commands/ssh.js +35 -12
- package/dist/commands/status.js +5 -0
- package/dist/commands/sync.js +102 -2
- package/dist/commands/tmux.d.ts +8 -1
- package/dist/commands/tmux.js +167 -17
- package/dist/lib/account-registry.d.ts +15 -5
- package/dist/lib/account-registry.js +150 -50
- package/dist/lib/answer-router.js +2 -1
- package/dist/lib/browser/ipc.d.ts +44 -0
- package/dist/lib/browser/ipc.js +120 -8
- package/dist/lib/browser/profiles.d.ts +57 -17
- package/dist/lib/browser/profiles.js +77 -53
- package/dist/lib/browser/registry.d.ts +44 -14
- package/dist/lib/browser/registry.js +141 -45
- package/dist/lib/browser/runtime-state.d.ts +4 -2
- package/dist/lib/browser/runtime-state.js +4 -2
- package/dist/lib/browser/service.js +4 -3
- package/dist/lib/channels/owner-forward.d.ts +88 -0
- package/dist/lib/channels/owner-forward.js +116 -0
- package/dist/lib/channels/owner-sink.js +7 -0
- package/dist/lib/daemon/runner.js +10 -2
- package/dist/lib/device-config.js +3 -2
- package/dist/lib/devices/config-migration.js +147 -1
- package/dist/lib/devices/device-docs.d.ts +35 -0
- package/dist/lib/devices/device-docs.js +163 -0
- package/dist/lib/devices/discovery-policy.d.ts +14 -2
- package/dist/lib/devices/discovery-policy.js +31 -21
- package/dist/lib/devices/registry.d.ts +11 -5
- package/dist/lib/devices/registry.js +46 -18
- package/dist/lib/exec.d.ts +66 -28
- package/dist/lib/exec.js +71 -26
- package/dist/lib/feed/feed.d.ts +10 -2
- package/dist/lib/feed/feed.js +12 -1
- package/dist/lib/feed-broadcast.js +15 -1
- package/dist/lib/git.d.ts +93 -0
- package/dist/lib/git.js +232 -0
- package/dist/lib/hosts/dispatch.d.ts +4 -3
- package/dist/lib/hosts/dispatch.js +12 -8
- package/dist/lib/hosts/providers/local.d.ts +9 -3
- package/dist/lib/hosts/providers/local.js +23 -12
- package/dist/lib/hosts/reconnect.d.ts +7 -4
- package/dist/lib/hosts/reconnect.js +29 -25
- package/dist/lib/hosts/registry.js +4 -1
- package/dist/lib/hosts/remote-os.js +3 -1
- package/dist/lib/monitors/remote.d.ts +18 -1
- package/dist/lib/monitors/remote.js +15 -2
- package/dist/lib/notify.d.ts +7 -0
- package/dist/lib/notify.js +15 -1
- package/dist/lib/session/active.d.ts +10 -1
- package/dist/lib/session/active.js +7 -1
- package/dist/lib/session/actor-sidecar.d.ts +7 -0
- package/dist/lib/session/actor-sidecar.js +2 -0
- package/dist/lib/session/db.d.ts +1 -1
- package/dist/lib/session/db.js +39 -3
- package/dist/lib/session/discover.js +7 -12
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/local-tmux-attach.d.ts +69 -0
- package/dist/lib/session/local-tmux-attach.js +164 -0
- package/dist/lib/session/pid-registry.d.ts +7 -0
- package/dist/lib/session/prompt.d.ts +15 -0
- package/dist/lib/session/prompt.js +21 -0
- package/dist/lib/session/remote-active.d.ts +8 -0
- package/dist/lib/session/remote-active.js +1 -0
- package/dist/lib/session/types.d.ts +17 -0
- package/dist/lib/session/types.js +10 -0
- package/dist/lib/share/publish.d.ts +8 -11
- package/dist/lib/share/publish.js +16 -20
- package/dist/lib/share/worker-template.js +104 -12
- package/dist/lib/state.d.ts +8 -0
- package/dist/lib/state.js +143 -11
- package/dist/lib/sync-status.d.ts +17 -0
- package/dist/lib/sync-status.js +21 -2
- package/dist/lib/terminal/resolve.d.ts +7 -0
- package/dist/lib/terminal/resolve.js +41 -2
- package/dist/lib/tmux/index.d.ts +1 -1
- package/dist/lib/tmux/index.js +1 -1
- package/dist/lib/tmux/session.d.ts +10 -0
- package/dist/lib/tmux/session.js +29 -0
- package/dist/lib/traces/insights.d.ts +67 -0
- package/dist/lib/traces/insights.js +178 -0
- package/dist/lib/traces/phenotype.d.ts +67 -0
- package/dist/lib/traces/phenotype.js +437 -0
- package/dist/lib/traces/segments.d.ts +133 -0
- package/dist/lib/traces/segments.js +301 -0
- package/dist/lib/traces/sync.d.ts +33 -0
- package/dist/lib/traces/sync.js +11 -2
- package/dist/lib/types.d.ts +47 -1
- package/dist/lib/watchdog/runner.js +18 -4
- package/package.json +1 -1
|
@@ -826,27 +826,37 @@ export type MetadataResolveOutcome = {
|
|
|
826
826
|
};
|
|
827
827
|
/**
|
|
828
828
|
* A match unique enough to resolve on the FIRST peer that returns it and cancel
|
|
829
|
-
* the rest of the fleet sweep: a full session UUID
|
|
829
|
+
* the rest of the fleet sweep: a full session UUID (globally unique), a live
|
|
830
|
+
* tmux alias, or a bare 8-hex short id — all three name at most one session PER
|
|
831
|
+
* ANSWERING peer, so the first reachable hit is enough (PHNX-3292: the alias/
|
|
832
|
+
* short-id case is exactly what `agents tmux ls` prints, and it used to stay
|
|
833
|
+
* all-settle right alongside a genuine keyword search).
|
|
830
834
|
*
|
|
831
|
-
*
|
|
832
|
-
*
|
|
833
|
-
*
|
|
834
|
-
*
|
|
835
|
-
*
|
|
836
|
-
*
|
|
837
|
-
*
|
|
835
|
+
* A session **label** is deliberately NOT definitive here even on an exact
|
|
836
|
+
* match: labels are free-form and can collide, so a distinct session may carry
|
|
837
|
+
* the same label on a peer that has not answered yet — early-exiting on the
|
|
838
|
+
* first would silently, and nondeterministically, resume the wrong session
|
|
839
|
+
* (whichever peer replied first). Labels therefore stay all-settle so a
|
|
840
|
+
* cross-machine label conflict surfaces as an ambiguity, and a short-id PREFIX
|
|
841
|
+
* *shorter* than the full 8-hex width stays all-settle for the same reason
|
|
842
|
+
* (RUSH-2203).
|
|
838
843
|
*
|
|
839
844
|
* This is about cancelling a sweep that is still IN FLIGHT, where a peer that
|
|
840
845
|
* has not answered yet is still expected to. It is deliberately stricter than
|
|
841
846
|
* `isUniqueEnoughSelector`, which decides what to do once the sweep is OVER and
|
|
842
|
-
* a peer has definitively not answered — see SES-9a.
|
|
847
|
+
* a peer has definitively not answered — see SES-9a. A collision between two
|
|
848
|
+
* peers that BOTH answer before the abort lands is still caught by the ordinary
|
|
849
|
+
* uniqueness gate in `metadataResolveOutcome`/`fleetCandidatesByQuery`; only a
|
|
850
|
+
* peer that has not answered yet, at the moment of the FIRST hit, is the
|
|
851
|
+
* accepted risk (documented in the PHNX-3292 plan).
|
|
843
852
|
*/
|
|
844
853
|
export declare function isDefinitiveMatch(session: SessionMeta, selector: string): boolean;
|
|
845
854
|
/**
|
|
846
|
-
* Whether a selector may enable early-exit on the cancellable fan-out
|
|
847
|
-
*
|
|
848
|
-
*
|
|
849
|
-
*
|
|
855
|
+
* Whether a selector may enable early-exit on the cancellable fan-out: a full
|
|
856
|
+
* UUID, a tmux alias, or an exact 8-hex short id — every shape that is globally
|
|
857
|
+
* unique enough for the first hit to be the only hit worth waiting for. Labels,
|
|
858
|
+
* keywords, and short-id prefixes narrower than 8 hex stay all-settle: their
|
|
859
|
+
* uniqueness (or conflict) is only knowable once every peer has answered. See
|
|
850
860
|
* {@link isDefinitiveMatch}.
|
|
851
861
|
*/
|
|
852
862
|
export declare function selectorAllowsEarlyExit(selector: string): boolean;
|
|
@@ -18,10 +18,10 @@ import { sanitizeForTerminal, redactSecrets } from '../lib/redact.js';
|
|
|
18
18
|
import { resolveProjectKey } from '../lib/project-key.js';
|
|
19
19
|
import { listProjectDefs, resolveProjectNameForCwd } from '../lib/projects.js';
|
|
20
20
|
import ora from 'ora';
|
|
21
|
-
import { SESSION_AGENTS } from '../lib/session/types.js';
|
|
21
|
+
import { SESSION_AGENTS, isAgentTmuxAlias, sessionDisplayAgent } from '../lib/session/types.js';
|
|
22
22
|
import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
|
|
23
23
|
import { looksLikePath, toComparablePath, homeDir, needsWindowsShell, composeWin32CommandLine } from '../lib/platform/index.js';
|
|
24
|
-
import { getActiveSessions, describeActiveDiscoveryHealth, sessionProcessIsLocal, backfillActiveRowsFromIndex, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson } from '../lib/session/active.js';
|
|
24
|
+
import { getActiveSessions, describeActiveDiscoveryHealth, sessionProcessIsLocal, backfillActiveRowsFromIndex, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson, shortIdFromName } from '../lib/session/active.js';
|
|
25
25
|
export { activeSessionProjectKey, backfillActiveRowsFromIndex, backfillActiveRowsFromMeta, isRunningLiveSession, serializeActiveSessionsForJson, serializeSessionsJson } from '../lib/session/active.js';
|
|
26
26
|
import { enumerateGhosttyTabs, assignGhosttyTabs } from '../lib/session/ghostty-tabs.js';
|
|
27
27
|
import { mapPanesToTargets, listClients } from '../lib/tmux/session.js';
|
|
@@ -233,9 +233,10 @@ async function renderArtifactsForSession(session, listAll, name) {
|
|
|
233
233
|
console.log(chalk.gray('No file-write artifacts found in this session.'));
|
|
234
234
|
return;
|
|
235
235
|
}
|
|
236
|
-
const
|
|
236
|
+
const shown = sessionDisplayAgent(session);
|
|
237
|
+
const agentColor = colorAgent(shown);
|
|
237
238
|
console.log('');
|
|
238
|
-
console.log(agentColor(
|
|
239
|
+
console.log(agentColor(shown) +
|
|
239
240
|
chalk.gray(` · ${session.shortId} · ${formatRelativeTime(session.timestamp)}`));
|
|
240
241
|
console.log(chalk.gray('─'.repeat(72)));
|
|
241
242
|
for (const a of artifacts) {
|
|
@@ -726,7 +727,8 @@ function fitCell(content, room) {
|
|
|
726
727
|
*/
|
|
727
728
|
export function renderActiveRowLines(s, indent, termW) {
|
|
728
729
|
const idCol = chalk.dim(padToWidth((s.sessionId?.slice(0, 8)) ?? '-', ROW_ID_W));
|
|
729
|
-
const
|
|
730
|
+
const shownKind = sessionDisplayAgent({ agent: s.kind, harness: s.harness });
|
|
731
|
+
const kindCol = colorAgent(shownKind)(padToWidth(truncateToWidth(shownKind, ROW_AGENT_W), ROW_AGENT_W + 1));
|
|
730
732
|
const versionCol = chalk.gray(padToWidth(truncateToWidth(s.version ?? '', ROW_VERSION_W), ROW_VERSION_W + 1));
|
|
731
733
|
const statusCol = statusColor(s.status)(padToWidth(truncateToWidth(activityLabel(s), ROW_STATUS_W - 1), ROW_STATUS_W));
|
|
732
734
|
const ownerCol = chalk.cyan(padToWidth(truncateToWidth(ownerLabel(s), ROW_OWNER_W - 1), ROW_OWNER_W));
|
|
@@ -1010,6 +1012,7 @@ export function serializeResolvedSessionsJson(sessions) {
|
|
|
1010
1012
|
id: session.id,
|
|
1011
1013
|
shortId: session.shortId,
|
|
1012
1014
|
agent: session.agent,
|
|
1015
|
+
harness: session.harness,
|
|
1013
1016
|
origin: session.origin,
|
|
1014
1017
|
timestamp: session.timestamp,
|
|
1015
1018
|
lastActivity: session.lastActivity,
|
|
@@ -1805,7 +1808,8 @@ function runOutcomeDetail(meta) {
|
|
|
1805
1808
|
/** Compact per-session metadata line under a linked session row. */
|
|
1806
1809
|
function linkedSessionMeta(session) {
|
|
1807
1810
|
const parts = [];
|
|
1808
|
-
|
|
1811
|
+
const shown = sessionDisplayAgent(session);
|
|
1812
|
+
parts.push(session.version ? `${shown} v${session.version}` : shown);
|
|
1809
1813
|
if (session.account)
|
|
1810
1814
|
parts.push(session.account);
|
|
1811
1815
|
if (session.model)
|
|
@@ -2996,7 +3000,8 @@ function timeCell(age, topicSlack) {
|
|
|
2996
3000
|
* when `showTicket` — otherwise a listing with no refs would waste a column of
|
|
2997
3001
|
* dashes and needlessly truncate the topic. Worktree stays a trailing badge. */
|
|
2998
3002
|
export function flatSessionRow(session, live, showTicket = false, cols = {}, bookmarked = false) {
|
|
2999
|
-
const
|
|
3003
|
+
const shown = sessionDisplayAgent(session);
|
|
3004
|
+
const agentColor = colorAgent(shown);
|
|
3000
3005
|
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
3001
3006
|
const project = session.project || '-';
|
|
3002
3007
|
const tag = originTag(session) || teamTag(session);
|
|
@@ -3045,7 +3050,7 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, boo
|
|
|
3045
3050
|
const topicW = Math.max(MIN_TOPIC_W, width - fixedW - modelW - when.extraW);
|
|
3046
3051
|
return (bookmarkCell +
|
|
3047
3052
|
chalk.white(padToWidth(truncateToWidth(session.shortId, 9), 10)) +
|
|
3048
|
-
agentColor(padToWidth(truncateToWidth(
|
|
3053
|
+
agentColor(padToWidth(truncateToWidth(shown, 8), 9)) +
|
|
3049
3054
|
chalk.yellow(padToWidth(truncateToWidth(session.version || '-', 7), 8)) +
|
|
3050
3055
|
(modelW ? chalk.yellow(padToWidth(truncateToWidth(modelLabel(session.model), modelW - 1), modelW)) : '') +
|
|
3051
3056
|
machineCell +
|
|
@@ -3060,7 +3065,8 @@ export function flatSessionRow(session, live, showTicket = false, cols = {}, boo
|
|
|
3060
3065
|
}
|
|
3061
3066
|
/** One tree-mode row (grouped under a dir header): id · agent · badges · topic · time. No version/project column. */
|
|
3062
3067
|
function treeSessionRow(session, live) {
|
|
3063
|
-
const
|
|
3068
|
+
const shown = sessionDisplayAgent(session);
|
|
3069
|
+
const agentColor = colorAgent(shown);
|
|
3064
3070
|
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
3065
3071
|
const tag = originTag(session) || teamTag(session);
|
|
3066
3072
|
const label = session.label;
|
|
@@ -3082,7 +3088,7 @@ function treeSessionRow(session, live) {
|
|
|
3082
3088
|
const topicW = Math.max(12, baseTopicW - when.extraW);
|
|
3083
3089
|
return (' ' +
|
|
3084
3090
|
chalk.dim(padToWidth(session.shortId, 9)) +
|
|
3085
|
-
agentColor(padToWidth(truncateToWidth(
|
|
3091
|
+
agentColor(padToWidth(truncateToWidth(shown, 7), 8)) +
|
|
3086
3092
|
(badges ? badges + ' ' : '') +
|
|
3087
3093
|
(glyph ? glyph + ' ' : '') +
|
|
3088
3094
|
statusCell +
|
|
@@ -3246,7 +3252,8 @@ const TEAM_HANDLE_W = 16;
|
|
|
3246
3252
|
* identity the `--teams` view exists to show (RUSH-1997).
|
|
3247
3253
|
*/
|
|
3248
3254
|
function teamMemberRow(session, live) {
|
|
3249
|
-
const
|
|
3255
|
+
const shown = sessionDisplayAgent(session);
|
|
3256
|
+
const agentColor = colorAgent(shown);
|
|
3250
3257
|
const origin = session.teamOrigin;
|
|
3251
3258
|
const age = sessionAgeParts(session.timestamp, session.lastActivity);
|
|
3252
3259
|
const handle = safeTeamText(origin?.handle) ?? session.shortId;
|
|
@@ -3267,7 +3274,7 @@ function teamMemberRow(session, live) {
|
|
|
3267
3274
|
const topicW = Math.max(12, baseTopicW - when.extraW);
|
|
3268
3275
|
return (' ' +
|
|
3269
3276
|
chalk.dim(padToWidth(session.shortId, 9)) +
|
|
3270
|
-
agentColor(padToWidth(truncateToWidth(
|
|
3277
|
+
agentColor(padToWidth(truncateToWidth(shown, 7), 8)) +
|
|
3271
3278
|
chalk.yellow(padToWidth(truncateToWidth(mode || '-', TEAM_MODE_W), TEAM_MODE_W + 1)) +
|
|
3272
3279
|
(badges ? badges + ' ' : '') +
|
|
3273
3280
|
(glyph ? glyph + ' ' : '') +
|
|
@@ -3444,13 +3451,14 @@ function renderArchivedSession(session, mode, options = {}) {
|
|
|
3444
3451
|
}, null, 2));
|
|
3445
3452
|
return;
|
|
3446
3453
|
}
|
|
3447
|
-
const
|
|
3454
|
+
const shown = sessionDisplayAgent(session);
|
|
3455
|
+
const agentColor = colorAgent(shown);
|
|
3448
3456
|
const absTime = formatAbsoluteTime(session.timestamp);
|
|
3449
3457
|
const title = session.label || session.topic;
|
|
3450
3458
|
console.log('');
|
|
3451
3459
|
if (title)
|
|
3452
3460
|
console.log(chalk.bold.white(title));
|
|
3453
|
-
console.log(agentColor(
|
|
3461
|
+
console.log(agentColor(shown) +
|
|
3454
3462
|
(session.version ? chalk.yellow(` ${session.version}`) : '') +
|
|
3455
3463
|
(session.project ? chalk.cyan(` ${session.project}`) : '') +
|
|
3456
3464
|
chalk.gray(` ${absTime} (${formatRelativeTime(session.timestamp)})`) +
|
|
@@ -3481,7 +3489,7 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3481
3489
|
console.log(chalk.yellow('Session transcript not available (file no longer exists).'));
|
|
3482
3490
|
console.log(chalk.gray(`Path: ${session.filePath}`));
|
|
3483
3491
|
if (session.version)
|
|
3484
|
-
console.log(chalk.gray(`Version: ${session
|
|
3492
|
+
console.log(chalk.gray(`Version: ${sessionDisplayAgent(session)} ${session.version}`));
|
|
3485
3493
|
if (session.project)
|
|
3486
3494
|
console.log(chalk.gray(`Project: ${session.project}`));
|
|
3487
3495
|
if (session.account)
|
|
@@ -3489,11 +3497,12 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3489
3497
|
console.log(chalk.gray(`Time: ${session.timestamp}`));
|
|
3490
3498
|
return;
|
|
3491
3499
|
}
|
|
3492
|
-
const spinner = ora(`Parsing ${session
|
|
3500
|
+
const spinner = ora(`Parsing ${sessionDisplayAgent(session)} session...`).start();
|
|
3493
3501
|
const parsedEvents = parseSession(session.filePath, session.agent);
|
|
3494
3502
|
spinner.stop();
|
|
3495
3503
|
let events = filterEvents(parsedEvents, filters);
|
|
3496
|
-
const
|
|
3504
|
+
const shown = sessionDisplayAgent(session);
|
|
3505
|
+
const agentColor = colorAgent(shown);
|
|
3497
3506
|
console.log('');
|
|
3498
3507
|
if (mode === 'summary') {
|
|
3499
3508
|
const stats = computeSummaryStats(events);
|
|
@@ -3507,7 +3516,7 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3507
3516
|
const badges = signalBadges(metaSignals(session));
|
|
3508
3517
|
console.log(chalk.bold.white(title) + (badges ? ' ' + badges : ''));
|
|
3509
3518
|
}
|
|
3510
|
-
console.log(agentColor(
|
|
3519
|
+
console.log(agentColor(shown) +
|
|
3511
3520
|
(session.version ? chalk.yellow(` ${session.version}`) : '') +
|
|
3512
3521
|
modelStr +
|
|
3513
3522
|
(session.project ? chalk.cyan(` ${session.project}`) + branchStr : branchStr) +
|
|
@@ -3521,7 +3530,7 @@ async function renderSession(session, mode, filters, options = {}) {
|
|
|
3521
3530
|
return;
|
|
3522
3531
|
}
|
|
3523
3532
|
if (mode === 'markdown') {
|
|
3524
|
-
console.log(agentColor(
|
|
3533
|
+
console.log(agentColor(shown) +
|
|
3525
3534
|
(session.version ? chalk.yellow(` ${session.version}`) : '') +
|
|
3526
3535
|
(session.project ? chalk.cyan(` ${session.project}`) : '') +
|
|
3527
3536
|
chalk.gray(` ${formatRelativeTime(session.timestamp)}`) +
|
|
@@ -3662,7 +3671,8 @@ export function liveHostLabel(a) {
|
|
|
3662
3671
|
return viewer && viewer !== a.host ? `${a.host}→${viewer}` : a.host;
|
|
3663
3672
|
}
|
|
3664
3673
|
export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarked = false, live) {
|
|
3665
|
-
const
|
|
3674
|
+
const shown = sessionDisplayAgent(s);
|
|
3675
|
+
const agentColor = colorAgent(shown);
|
|
3666
3676
|
const age = sessionAgeParts(s.timestamp, s.lastActivity);
|
|
3667
3677
|
const project = s.project || '-';
|
|
3668
3678
|
// SSH-launch origin (live rows only): mirrors the flat listing's `ssh←<device>`
|
|
@@ -3727,7 +3737,7 @@ export function formatPickerLabel(s, query, cols = {}, ssh, host = '', bookmarke
|
|
|
3727
3737
|
// live row with no session id is named by its pid or cloud task, which can
|
|
3728
3738
|
// run past the column and shunt every later column out of alignment.
|
|
3729
3739
|
chalk.white(padRight(truncate(s.shortId, 9), 10)) +
|
|
3730
|
-
agentColor(padRight(truncate(
|
|
3740
|
+
agentColor(padRight(truncate(shown, 8), 9)) +
|
|
3731
3741
|
chalk.yellow(padRight(truncate(versionStr, 7), 8)) +
|
|
3732
3742
|
machineCell +
|
|
3733
3743
|
hostCell +
|
|
@@ -4579,35 +4589,53 @@ function modeFlag(mode) {
|
|
|
4579
4589
|
return undefined; // summary — the peer's default render
|
|
4580
4590
|
}
|
|
4581
4591
|
const FULL_SESSION_ID_RE = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
|
|
4592
|
+
/** Width of `SessionMeta.shortId` / the hex a tmux alias embeds (`deriveShortId`). */
|
|
4593
|
+
const SHORT_SESSION_ID_RE = /^[0-9a-f]{8}$/i;
|
|
4582
4594
|
/**
|
|
4583
4595
|
* A match unique enough to resolve on the FIRST peer that returns it and cancel
|
|
4584
|
-
* the rest of the fleet sweep: a full session UUID
|
|
4596
|
+
* the rest of the fleet sweep: a full session UUID (globally unique), a live
|
|
4597
|
+
* tmux alias, or a bare 8-hex short id — all three name at most one session PER
|
|
4598
|
+
* ANSWERING peer, so the first reachable hit is enough (PHNX-3292: the alias/
|
|
4599
|
+
* short-id case is exactly what `agents tmux ls` prints, and it used to stay
|
|
4600
|
+
* all-settle right alongside a genuine keyword search).
|
|
4585
4601
|
*
|
|
4586
|
-
*
|
|
4587
|
-
*
|
|
4588
|
-
*
|
|
4589
|
-
*
|
|
4590
|
-
*
|
|
4591
|
-
*
|
|
4592
|
-
*
|
|
4602
|
+
* A session **label** is deliberately NOT definitive here even on an exact
|
|
4603
|
+
* match: labels are free-form and can collide, so a distinct session may carry
|
|
4604
|
+
* the same label on a peer that has not answered yet — early-exiting on the
|
|
4605
|
+
* first would silently, and nondeterministically, resume the wrong session
|
|
4606
|
+
* (whichever peer replied first). Labels therefore stay all-settle so a
|
|
4607
|
+
* cross-machine label conflict surfaces as an ambiguity, and a short-id PREFIX
|
|
4608
|
+
* *shorter* than the full 8-hex width stays all-settle for the same reason
|
|
4609
|
+
* (RUSH-2203).
|
|
4593
4610
|
*
|
|
4594
4611
|
* This is about cancelling a sweep that is still IN FLIGHT, where a peer that
|
|
4595
4612
|
* has not answered yet is still expected to. It is deliberately stricter than
|
|
4596
4613
|
* `isUniqueEnoughSelector`, which decides what to do once the sweep is OVER and
|
|
4597
|
-
* a peer has definitively not answered — see SES-9a.
|
|
4614
|
+
* a peer has definitively not answered — see SES-9a. A collision between two
|
|
4615
|
+
* peers that BOTH answer before the abort lands is still caught by the ordinary
|
|
4616
|
+
* uniqueness gate in `metadataResolveOutcome`/`fleetCandidatesByQuery`; only a
|
|
4617
|
+
* peer that has not answered yet, at the moment of the FIRST hit, is the
|
|
4618
|
+
* accepted risk (documented in the PHNX-3292 plan).
|
|
4598
4619
|
*/
|
|
4599
4620
|
export function isDefinitiveMatch(session, selector) {
|
|
4600
|
-
|
|
4621
|
+
const trimmed = selector.trim();
|
|
4622
|
+
if (FULL_SESSION_ID_RE.test(trimmed)) {
|
|
4623
|
+
return session.id.toLowerCase() === trimmed.toLowerCase();
|
|
4624
|
+
}
|
|
4625
|
+
const shortId = shortIdFromName(trimmed) ?? (SHORT_SESSION_ID_RE.test(trimmed) ? trimmed : undefined);
|
|
4626
|
+
return !!shortId && session.shortId.toLowerCase() === shortId.toLowerCase();
|
|
4601
4627
|
}
|
|
4602
4628
|
/**
|
|
4603
|
-
* Whether a selector may enable early-exit on the cancellable fan-out
|
|
4604
|
-
*
|
|
4605
|
-
*
|
|
4606
|
-
*
|
|
4629
|
+
* Whether a selector may enable early-exit on the cancellable fan-out: a full
|
|
4630
|
+
* UUID, a tmux alias, or an exact 8-hex short id — every shape that is globally
|
|
4631
|
+
* unique enough for the first hit to be the only hit worth waiting for. Labels,
|
|
4632
|
+
* keywords, and short-id prefixes narrower than 8 hex stay all-settle: their
|
|
4633
|
+
* uniqueness (or conflict) is only knowable once every peer has answered. See
|
|
4607
4634
|
* {@link isDefinitiveMatch}.
|
|
4608
4635
|
*/
|
|
4609
4636
|
export function selectorAllowsEarlyExit(selector) {
|
|
4610
|
-
|
|
4637
|
+
const trimmed = selector.trim();
|
|
4638
|
+
return FULL_SESSION_ID_RE.test(trimmed) || isAgentTmuxAlias(trimmed) || SHORT_SESSION_ID_RE.test(trimmed);
|
|
4611
4639
|
}
|
|
4612
4640
|
/** Resolve a fleet sweep through the same canonical full-id / prefix resolver as
|
|
4613
4641
|
* local lookups, then group copies by logical session id. Synced mirrors of one
|
|
@@ -4725,9 +4753,11 @@ export function metadataResolveOutcome(localMatches, remote, selector) {
|
|
|
4725
4753
|
// 6-char word like `facade` or `decade` is not treated as an identifier.
|
|
4726
4754
|
// A label stays fail-closed at any length: labels are free-form and collide by
|
|
4727
4755
|
// design, so one hit there really is a guess.
|
|
4728
|
-
// (RUSH-2203's early-exit
|
|
4729
|
-
//
|
|
4730
|
-
//
|
|
4756
|
+
// (RUSH-2203's early-exit — see isDefinitiveMatch — cancels the sweep
|
|
4757
|
+
// mid-flight, where a peer that has not answered YET is still expected to
|
|
4758
|
+
// answer, so it stays narrower than this post-sweep gate even though
|
|
4759
|
+
// PHNX-3292 widened it past UUID to alias/8-hex too. Here the sweep is
|
|
4760
|
+
// already over.)
|
|
4731
4761
|
if (isUniqueEnoughSelector(selector) && candidates.length === 1) {
|
|
4732
4762
|
return { kind: 'resolved', session: candidates[0].hits[0].session };
|
|
4733
4763
|
}
|
|
@@ -10,8 +10,11 @@
|
|
|
10
10
|
import type { Command } from 'commander';
|
|
11
11
|
/**
|
|
12
12
|
* Interactive browser setup. Returns true if a usable default profile exists
|
|
13
|
-
* afterwards, false if
|
|
14
|
-
*
|
|
13
|
+
* afterwards, false if none exists / the user backed out. Non-interactively it
|
|
14
|
+
* only RECOGNIZES an existing profile — it never creates one (PHNX-3296), since
|
|
15
|
+
* silently minting a logged-out `auto-chrome` on a headless box is the exact bug
|
|
16
|
+
* that removed; a headless box gets its browser from the fleet hub. Never throws
|
|
17
|
+
* on cancel — the `agents setup` hub relies on that.
|
|
15
18
|
*/
|
|
16
19
|
export declare function runBrowserWizard(): Promise<boolean>;
|
|
17
20
|
/** Register `agents setup browser` under the parent `setup` command. */
|
|
@@ -9,7 +9,7 @@
|
|
|
9
9
|
*/
|
|
10
10
|
import chalk from 'chalk';
|
|
11
11
|
import { setConfigValue } from '../lib/device-config.js';
|
|
12
|
-
import {
|
|
12
|
+
import { listInstalledBrowsers } from '../lib/browser/chrome.js';
|
|
13
13
|
import { DEFAULT_BROWSER_PROFILE_NAME, getAutoDetectedProfile, createProfile, findFreeProfilePort, getConfiguredDefaultProfileName, } from '../lib/browser/profiles.js';
|
|
14
14
|
import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
|
|
15
15
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -18,25 +18,27 @@ const INSTALL_HINT = 'Install one of: Google Chrome, Brave, Microsoft Edge, Chro
|
|
|
18
18
|
'(Safari and Firefox are not supported — agents browser drives over the Chrome DevTools Protocol.)';
|
|
19
19
|
/**
|
|
20
20
|
* Interactive browser setup. Returns true if a usable default profile exists
|
|
21
|
-
* afterwards, false if
|
|
22
|
-
*
|
|
21
|
+
* afterwards, false if none exists / the user backed out. Non-interactively it
|
|
22
|
+
* only RECOGNIZES an existing profile — it never creates one (PHNX-3296), since
|
|
23
|
+
* silently minting a logged-out `auto-chrome` on a headless box is the exact bug
|
|
24
|
+
* that removed; a headless box gets its browser from the fleet hub. Never throws
|
|
25
|
+
* on cancel — the `agents setup` hub relies on that.
|
|
23
26
|
*/
|
|
24
27
|
export async function runBrowserWizard() {
|
|
25
28
|
if (!isInteractiveTerminal()) {
|
|
26
|
-
// Non-interactive
|
|
27
|
-
//
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
}
|
|
29
|
+
// Non-interactive / headless: recognize an existing default, but NEVER mint
|
|
30
|
+
// one. Silently auto-detecting an installed browser and creating an
|
|
31
|
+
// `auto-chrome` here is exactly the bug PHNX-3296 removed — a headless box
|
|
32
|
+
// gets its browser from the fleet hub (`browser.device`), or from an
|
|
33
|
+
// interactive pick later.
|
|
32
34
|
const existing = await getAutoDetectedProfile();
|
|
33
35
|
if (existing) {
|
|
34
36
|
console.log(chalk.dim(`Browser profile "${existing.name}" already exists.`));
|
|
35
37
|
return true;
|
|
36
38
|
}
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return
|
|
39
|
+
console.log(chalk.dim('No default browser profile on this machine. Re-run `agents setup browser` in an ' +
|
|
40
|
+
'interactive terminal to pick one, or use the fleet hub: agents config set browser.device <host>.'));
|
|
41
|
+
return false;
|
|
40
42
|
}
|
|
41
43
|
const installed = listInstalledBrowsers();
|
|
42
44
|
if (installed.length === 0) {
|
|
@@ -86,23 +88,6 @@ export async function runBrowserWizard() {
|
|
|
86
88
|
printOnboardingNextStep(profile.name);
|
|
87
89
|
return true;
|
|
88
90
|
}
|
|
89
|
-
/** Build + persist a `default` profile pinned to the first installed browser. */
|
|
90
|
-
async function createAutoDefault() {
|
|
91
|
-
const detected = findFirstInstalledBrowser();
|
|
92
|
-
if (!detected)
|
|
93
|
-
throw new Error('No supported browser found.');
|
|
94
|
-
const freePort = await findFreeProfilePort();
|
|
95
|
-
const profile = {
|
|
96
|
-
name: DEFAULT_BROWSER_PROFILE_NAME,
|
|
97
|
-
description: `Auto-detected ${detected.browserType} profile`,
|
|
98
|
-
browser: detected.browserType,
|
|
99
|
-
binary: detected.binary,
|
|
100
|
-
endpoints: [`cdp://127.0.0.1:${freePort}`],
|
|
101
|
-
viewport: { width: DEFAULT_VIEWPORT.width, height: DEFAULT_VIEWPORT.height },
|
|
102
|
-
};
|
|
103
|
-
await createProfile(profile);
|
|
104
|
-
return profile;
|
|
105
|
-
}
|
|
106
91
|
/** Offer to make `name` this machine's default browser profile (device-local). */
|
|
107
92
|
async function maybeSetDeviceDefault(name, confirm) {
|
|
108
93
|
const current = getConfiguredDefaultProfileName();
|
|
@@ -40,10 +40,29 @@ export declare function maybePickInteractiveHost(): Promise<boolean>;
|
|
|
40
40
|
* Offer to pin this machine's default browser profile to a chosen browser.
|
|
41
41
|
* Only asked when there is nothing to preserve: no configured device default
|
|
42
42
|
* AND no existing `default` profile (an existing profile is the user's earlier
|
|
43
|
-
* choice — never re-pinned behind their back).
|
|
44
|
-
*
|
|
43
|
+
* choice — never re-pinned behind their back). The list is the installed
|
|
44
|
+
* Chromium-family browsers plus a "None — this box uses the fleet hub" opt-out.
|
|
45
|
+
*
|
|
46
|
+
* This is the ONE place a default browser is chosen for a machine (PHNX-3296):
|
|
47
|
+
* a bare `agents browser start` no longer auto-detects and mints one, so picking
|
|
48
|
+
* here (or `agents browser use` / `agents setup browser` later) is how a box
|
|
49
|
+
* gets a local default at all. Picking "None" leaves it to the fleet hub
|
|
50
|
+
* (`browser.device`). Returns true when a profile was created and set as the
|
|
51
|
+
* device default. Silent no-op non-TTY — a headless box relies on the hub.
|
|
45
52
|
*/
|
|
46
|
-
export declare function maybePickBrowserProfile(
|
|
53
|
+
export declare function maybePickBrowserProfile(deps?: {
|
|
54
|
+
/** Force interactivity in a test; defaults to a real TTY probe. */
|
|
55
|
+
interactive?: boolean;
|
|
56
|
+
/** Inject the prompt so the pick is testable without a TTY. */
|
|
57
|
+
select?: (config: {
|
|
58
|
+
message: string;
|
|
59
|
+
default?: string;
|
|
60
|
+
choices: Array<{
|
|
61
|
+
name: string;
|
|
62
|
+
value: string;
|
|
63
|
+
}>;
|
|
64
|
+
}) => Promise<string>;
|
|
65
|
+
}): Promise<boolean>;
|
|
47
66
|
/**
|
|
48
67
|
* The bare-`agents setup` preferences step: interactive host, then browser.
|
|
49
68
|
* Runs AFTER the capability hub so it never delays the bootstrap. Never
|
|
@@ -80,11 +80,19 @@ export async function maybePickInteractiveHost() {
|
|
|
80
80
|
* Offer to pin this machine's default browser profile to a chosen browser.
|
|
81
81
|
* Only asked when there is nothing to preserve: no configured device default
|
|
82
82
|
* AND no existing `default` profile (an existing profile is the user's earlier
|
|
83
|
-
* choice — never re-pinned behind their back).
|
|
84
|
-
*
|
|
83
|
+
* choice — never re-pinned behind their back). The list is the installed
|
|
84
|
+
* Chromium-family browsers plus a "None — this box uses the fleet hub" opt-out.
|
|
85
|
+
*
|
|
86
|
+
* This is the ONE place a default browser is chosen for a machine (PHNX-3296):
|
|
87
|
+
* a bare `agents browser start` no longer auto-detects and mints one, so picking
|
|
88
|
+
* here (or `agents browser use` / `agents setup browser` later) is how a box
|
|
89
|
+
* gets a local default at all. Picking "None" leaves it to the fleet hub
|
|
90
|
+
* (`browser.device`). Returns true when a profile was created and set as the
|
|
91
|
+
* device default. Silent no-op non-TTY — a headless box relies on the hub.
|
|
85
92
|
*/
|
|
86
|
-
export async function maybePickBrowserProfile() {
|
|
87
|
-
|
|
93
|
+
export async function maybePickBrowserProfile(deps = {}) {
|
|
94
|
+
const interactive = deps.interactive ?? isInteractiveTerminal();
|
|
95
|
+
if (!interactive)
|
|
88
96
|
return false;
|
|
89
97
|
if (getConfiguredDefaultProfileName())
|
|
90
98
|
return false;
|
|
@@ -93,15 +101,24 @@ export async function maybePickBrowserProfile() {
|
|
|
93
101
|
const installed = listInstalledBrowsers();
|
|
94
102
|
if (installed.length === 0)
|
|
95
103
|
return false;
|
|
96
|
-
const
|
|
97
|
-
const picked = await select({
|
|
104
|
+
const config = {
|
|
98
105
|
message: 'Which browser should agents drive on THIS machine?',
|
|
99
106
|
default: defaultBrowserChoice(installed) ?? SKIP,
|
|
100
107
|
choices: [
|
|
101
108
|
...installed.map((b) => ({ name: `${b.browserType} ${chalk.dim(b.binary)}`, value: b.browserType })),
|
|
102
|
-
{ name: `
|
|
109
|
+
{ name: `None ${chalk.dim('— this box uses the fleet hub (agents config set browser.device <host>)')}`, value: SKIP },
|
|
103
110
|
],
|
|
104
|
-
}
|
|
111
|
+
};
|
|
112
|
+
// The prompt is the only non-testable boundary here, so it is injectable —
|
|
113
|
+
// everything after (createProfile, setConfigValue) runs for real.
|
|
114
|
+
let picked;
|
|
115
|
+
if (deps.select) {
|
|
116
|
+
picked = await deps.select(config);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
119
|
+
const { select } = await import('@inquirer/prompts');
|
|
120
|
+
picked = await select(config);
|
|
121
|
+
}
|
|
105
122
|
if (picked === SKIP)
|
|
106
123
|
return false;
|
|
107
124
|
const chosen = installed.find((b) => b.browserType === picked) ?? installed[0];
|
package/dist/commands/share.js
CHANGED
|
@@ -22,6 +22,8 @@ export function formatSharePublishResult(result, json = false) {
|
|
|
22
22
|
if (json)
|
|
23
23
|
return JSON.stringify(result, null, 2);
|
|
24
24
|
const lines = [chalk.green(result.url)];
|
|
25
|
+
if (result.slug)
|
|
26
|
+
lines.push(chalk.dim(` slug: ${result.slug}`));
|
|
25
27
|
if (result.label) {
|
|
26
28
|
const hint = result.labelSource === 'derived' ? chalk.dim(' (derived — pass --label to set one)') : '';
|
|
27
29
|
lines.push(chalk.dim(` "${result.label}"`) + hint);
|
|
@@ -436,7 +438,7 @@ export function registerShareCommands(artifactsCmd) {
|
|
|
436
438
|
.command('share')
|
|
437
439
|
.description('Publish an HTML file to a shareable link — managed if signed in, otherwise your Cloudflare R2.')
|
|
438
440
|
.argument('[file]', 'file to publish (HTML or any static asset)')
|
|
439
|
-
.option('--slug <slug>', '
|
|
441
|
+
.option('--slug <slug>', 'URL slug override (default: stable slug of the artifact title, then filename)')
|
|
440
442
|
.option('--github-user <user>', 'GitHub username for the share namespace (default: resolved from gh/git config; ignored on the managed endpoint)')
|
|
441
443
|
.option('--expire <spec>', "auto-expire (default 30d). e.g. 12h, 30d, 2026-08-01, or 'never'")
|
|
442
444
|
.addOption(new Option('--visibility <level>', 'public | unlisted | me | org (default public). unlisted is a capability URL; me/org require a Phoenix session and are hidden from the gallery')
|
|
@@ -497,17 +499,17 @@ export function registerShareCommands(artifactsCmd) {
|
|
|
497
499
|
# Hide from the public gallery (direct URL still works, noindex) and expire sooner
|
|
498
500
|
agents artifacts share ./out/report.html --visibility unlisted --expire 12h
|
|
499
501
|
|
|
500
|
-
# Permanent public page (
|
|
501
|
-
agents artifacts share ./out/landing.html --
|
|
502
|
+
# Permanent public page (the slug is derived from its title)
|
|
503
|
+
agents artifacts share ./out/landing.html --expire never
|
|
502
504
|
|
|
503
|
-
#
|
|
505
|
+
# Optional slug override
|
|
504
506
|
agents artifacts share ./out/report.html --slug q3-report --expire 7d
|
|
505
507
|
|
|
506
508
|
# Human title + structured metadata, shown in the gallery and share list
|
|
507
509
|
agents artifacts share ./out/plan.html --label "Q3 fleet plan" --meta kind=plan --meta ticket=RUSH-2683
|
|
508
510
|
|
|
509
|
-
# Republish
|
|
510
|
-
agents artifacts share ./out/plan.html --
|
|
511
|
+
# Republish the same title to the same URL without retaining a revision
|
|
512
|
+
agents artifacts share ./out/plan.html --no-revision
|
|
511
513
|
${SHARE_DELETE_EXAMPLES}
|
|
512
514
|
# One-time setup (or join an existing endpoint)
|
|
513
515
|
agents artifacts setup
|
|
@@ -519,8 +521,10 @@ ${SHARE_DELETE_EXAMPLES}
|
|
|
519
521
|
notes: `
|
|
520
522
|
Signed-in users publish to the managed endpoint (share.agents-cli.sh/<handle>/…)
|
|
521
523
|
with the Phoenix session — no Cloudflare account, bucket, or write token. The
|
|
522
|
-
handle is the local-part of the signed-in email
|
|
523
|
-
|
|
524
|
+
handle is the local-part of the signed-in email. The default slug is derived
|
|
525
|
+
deterministically from the HTML <title> or Markdown frontmatter title, then
|
|
526
|
+
the filename, so republishing the same title updates the same URL. --slug is
|
|
527
|
+
an optional override. Without a session, the existing BYO Cloudflare path still
|
|
524
528
|
applies (setup / join). HTML is stored as one object: local images are inlined
|
|
525
529
|
and Chrome-saved file:// TOC links become in-page hashes.
|
|
526
530
|
|