@phnx-labs/agents-cli 1.22.7 → 1.22.9
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 +97 -0
- package/README.md +5 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/browser.js +61 -0
- package/dist/commands/exec.js +89 -21
- package/dist/commands/feed.js +38 -1
- package/dist/commands/harness.d.ts +40 -2
- package/dist/commands/harness.js +316 -20
- package/dist/commands/monitors.js +2 -2
- package/dist/commands/profiles.d.ts +42 -0
- package/dist/commands/profiles.js +91 -3
- package/dist/commands/routines.js +2 -2
- package/dist/commands/run-account-picker.js +2 -0
- package/dist/commands/sessions-picker.js +3 -3
- package/dist/commands/sessions-render.d.ts +12 -0
- package/dist/commands/sessions-render.js +124 -0
- package/dist/commands/sessions.js +28 -1
- package/dist/commands/snapshot.d.ts +11 -0
- package/dist/commands/snapshot.js +107 -0
- package/dist/commands/ssh.js +26 -3
- package/dist/commands/teams.js +14 -2
- package/dist/commands/view.d.ts +7 -0
- package/dist/commands/view.js +1 -1
- package/dist/index.js +11 -1
- package/dist/lib/browser/ipc.js +2 -0
- package/dist/lib/browser/remote-control.d.ts +35 -0
- package/dist/lib/browser/remote-control.js +48 -0
- package/dist/lib/browser/service.d.ts +19 -0
- package/dist/lib/browser/service.js +19 -1
- package/dist/lib/browser/types.d.ts +14 -2
- package/dist/lib/crabbox/cli.d.ts +35 -0
- package/dist/lib/crabbox/cli.js +46 -0
- package/dist/lib/crabbox/config.d.ts +21 -0
- package/dist/lib/crabbox/config.js +43 -0
- package/dist/lib/crabbox/lease.d.ts +15 -5
- package/dist/lib/crabbox/lease.js +57 -17
- package/dist/lib/daemon.js +12 -11
- package/dist/lib/device-config.js +8 -0
- package/dist/lib/devices/resolve-target.d.ts +4 -3
- package/dist/lib/devices/resolve-target.js +4 -3
- package/dist/lib/hosts/passthrough.d.ts +10 -1
- package/dist/lib/hosts/passthrough.js +41 -3
- package/dist/lib/hosts/registry.d.ts +4 -0
- package/dist/lib/hosts/registry.js +16 -0
- package/dist/lib/hosts/remote-cmd.js +2 -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/observe-aliases.d.ts +30 -0
- package/dist/lib/observe-aliases.js +56 -0
- package/dist/lib/placement.d.ts +82 -0
- package/dist/lib/placement.js +188 -0
- package/dist/lib/profiles.d.ts +31 -9
- package/dist/lib/profiles.js +83 -13
- package/dist/lib/redact.js +4 -0
- package/dist/lib/rotate.d.ts +19 -4
- package/dist/lib/rotate.js +24 -1
- package/dist/lib/routines.d.ts +2 -0
- package/dist/lib/runner.d.ts +3 -0
- package/dist/lib/runner.js +90 -7
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/parse.d.ts +5 -1
- package/dist/lib/session/parse.js +23 -13
- package/dist/lib/session/prompt.js +5 -0
- package/dist/lib/session/render.d.ts +3 -0
- package/dist/lib/session/render.js +33 -10
- package/dist/lib/snapshot.d.ts +103 -0
- package/dist/lib/snapshot.js +99 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +17 -1
- package/dist/lib/usage-refresh.d.ts +19 -11
- package/dist/lib/usage-refresh.js +75 -43
- package/dist/lib/usage.d.ts +12 -0
- package/dist/lib/usage.js +37 -6
- package/package.json +1 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import type { Command } from 'commander';
|
|
2
|
+
import type { SessionMeta } from '../lib/session/types.js';
|
|
3
|
+
export declare const MARKDOWN_RENDER_AGENTS: readonly ["claude", "codex", "gemini", "antigravity", "opencode", "grok", "rush", "hermes", "kimi", "droid", "cursor"];
|
|
4
|
+
export type ReasoningMode = 'omit' | 'fold' | 'include';
|
|
5
|
+
/** Build one shareable Markdown document from the canonical preview and event model. */
|
|
6
|
+
export declare function renderSessionMarkdownDocument(session: SessionMeta, options?: {
|
|
7
|
+
redact?: boolean;
|
|
8
|
+
reasoning?: ReasoningMode;
|
|
9
|
+
knownSecrets?: readonly string[];
|
|
10
|
+
maxToolOutputChars?: number;
|
|
11
|
+
}): string;
|
|
12
|
+
export declare function registerSessionsRenderCommand(sessionsCmd: Command): void;
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/** Render selected normalized session transcripts as redacted Markdown documents. */
|
|
2
|
+
import * as fs from 'fs';
|
|
3
|
+
import { stripVTControlCharacters } from 'node:util';
|
|
4
|
+
import chalk from 'chalk';
|
|
5
|
+
import { setHelpSections } from '../lib/help.js';
|
|
6
|
+
import { knownSecretValuesFromEnv, redactSecrets } from '../lib/redact.js';
|
|
7
|
+
import { discoverSessions } from '../lib/session/discover.js';
|
|
8
|
+
import { parseSession } from '../lib/session/parse.js';
|
|
9
|
+
import { extractSessionTopic, isSyntheticUserMessage } from '../lib/session/prompt.js';
|
|
10
|
+
import { renderConversationMarkdown } from '../lib/session/render.js';
|
|
11
|
+
import { buildPreview } from './sessions-picker.js';
|
|
12
|
+
import { parseAgentFilter } from './sessions.js';
|
|
13
|
+
import { selectSessions } from './sessions-export.js';
|
|
14
|
+
export const MARKDOWN_RENDER_AGENTS = [
|
|
15
|
+
'claude',
|
|
16
|
+
'codex',
|
|
17
|
+
'gemini',
|
|
18
|
+
'antigravity',
|
|
19
|
+
'opencode',
|
|
20
|
+
'grok',
|
|
21
|
+
'rush',
|
|
22
|
+
'hermes',
|
|
23
|
+
'kimi',
|
|
24
|
+
'droid',
|
|
25
|
+
'cursor',
|
|
26
|
+
];
|
|
27
|
+
function parseReasoning(value) {
|
|
28
|
+
if (value === 'omit' || value === 'fold' || value === 'include')
|
|
29
|
+
return value;
|
|
30
|
+
throw new Error(`Unknown reasoning mode "${value}". Expected omit, fold, or include.`);
|
|
31
|
+
}
|
|
32
|
+
function quotePreview(preview) {
|
|
33
|
+
return preview.split('\n').map((line) => `> ${line}`).join('\n');
|
|
34
|
+
}
|
|
35
|
+
/** Build one shareable Markdown document from the canonical preview and event model. */
|
|
36
|
+
export function renderSessionMarkdownDocument(session, options = {}) {
|
|
37
|
+
if (!MARKDOWN_RENDER_AGENTS.includes(session.agent)) {
|
|
38
|
+
throw new Error(`Cannot render ${session.agent} session ${session.shortId || session.id}: Markdown rendering supports ${MARKDOWN_RENDER_AGENTS.join(', ')}.`);
|
|
39
|
+
}
|
|
40
|
+
// Preserve normalized tool output here so the Markdown renderer owns the
|
|
41
|
+
// visible cap and can report exactly how much it omitted.
|
|
42
|
+
const events = parseSession(session.filePath, session.agent, { maxToolOutputChars: Infinity });
|
|
43
|
+
if (events.length === 0) {
|
|
44
|
+
throw new Error(`Cannot render ${session.agent} session ${session.shortId || session.id}: transcript produced no normalized events.`);
|
|
45
|
+
}
|
|
46
|
+
const shouldRedact = options.redact !== false;
|
|
47
|
+
const sanitize = (text) => shouldRedact ? redactSecrets(text, options.knownSecrets) : text;
|
|
48
|
+
const preview = sanitize(stripVTControlCharacters(buildPreview(session)));
|
|
49
|
+
const shareEvents = events.filter((event) => !(event.type === 'message' && event._synthetic));
|
|
50
|
+
const firstPrompt = shareEvents.find((event) => event.type === 'message' && event.role === 'user')?.content;
|
|
51
|
+
const title = sanitize(session.label || (firstPrompt ? extractSessionTopic(firstPrompt) : undefined) ||
|
|
52
|
+
(session.topic && !isSyntheticUserMessage(session.topic) ? session.topic : undefined) ||
|
|
53
|
+
`${session.agent} session ${session.shortId || session.id}`);
|
|
54
|
+
const conversation = renderConversationMarkdown(shareEvents, {
|
|
55
|
+
redact: shouldRedact,
|
|
56
|
+
knownSecrets: options.knownSecrets,
|
|
57
|
+
reasoning: options.reasoning ?? 'omit',
|
|
58
|
+
maxToolOutputChars: options.maxToolOutputChars,
|
|
59
|
+
});
|
|
60
|
+
return `# ${title}\n\n## Session preview\n\n${quotePreview(preview)}\n\n## Conversation\n\n${conversation}\n`;
|
|
61
|
+
}
|
|
62
|
+
export function registerSessionsRenderCommand(sessionsCmd) {
|
|
63
|
+
const cmd = sessionsCmd
|
|
64
|
+
.command('render <selectors...>')
|
|
65
|
+
.description('Render one or more sessions as readable, redacted Markdown for review or sharing.')
|
|
66
|
+
.option('--format <format>', 'Output format (md or markdown)', 'md')
|
|
67
|
+
.option('-o, --output <path>', 'Write the rendered document to a file instead of stdout')
|
|
68
|
+
.option('--reasoning <mode>', 'Reasoning visibility: omit, fold, or include', 'omit');
|
|
69
|
+
setHelpSections(cmd, {
|
|
70
|
+
examples: `# Render one redacted session for a confidential gist
|
|
71
|
+
agents sessions render a1b2c3d4 -o session.md
|
|
72
|
+
|
|
73
|
+
# Combine several sessions into one Markdown document
|
|
74
|
+
agents sessions render a1b2c3d4 d4c3b2a1 -o delivery-sessions.md
|
|
75
|
+
|
|
76
|
+
# Keep reasoning locally in collapsible sections and opt out of redaction
|
|
77
|
+
agents sessions render a1b2c3d4 --reasoning fold --no-redact`,
|
|
78
|
+
notes: `Markdown is redacted by default, including credential-shaped values and local home paths.
|
|
79
|
+
The preview at the top is the same preview shown by 'agents sessions'. Tool output is truncated
|
|
80
|
+
with an explicit note. Use --no-redact only for local output that will not be shared.`,
|
|
81
|
+
});
|
|
82
|
+
cmd.action(async (selectors, options, command) => {
|
|
83
|
+
const globals = command.optsWithGlobals();
|
|
84
|
+
if (!['md', 'markdown'].includes(options.format.toLowerCase())) {
|
|
85
|
+
throw new Error(`Unknown format "${options.format}". Expected md or markdown.`);
|
|
86
|
+
}
|
|
87
|
+
const reasoning = parseReasoning(options.reasoning);
|
|
88
|
+
const limit = Math.max(1, Number.parseInt(globals.limit || '100', 10) || 100);
|
|
89
|
+
const agent = parseAgentFilter(globals.agent).agent;
|
|
90
|
+
const sessions = selectSessions(await discoverSessions({
|
|
91
|
+
all: globals.all !== false,
|
|
92
|
+
since: globals.since,
|
|
93
|
+
limit,
|
|
94
|
+
agent: agent ?? undefined,
|
|
95
|
+
}), selectors);
|
|
96
|
+
if (sessions.length === 0) {
|
|
97
|
+
process.stderr.write(chalk.yellow('No sessions matched the selection.\n'));
|
|
98
|
+
process.exitCode = 1;
|
|
99
|
+
return;
|
|
100
|
+
}
|
|
101
|
+
const knownSecrets = globals.redact === false ? undefined : knownSecretValuesFromEnv();
|
|
102
|
+
const rendered = sessions.map((session) => ({
|
|
103
|
+
id: session.id,
|
|
104
|
+
agent: session.agent,
|
|
105
|
+
markdown: renderSessionMarkdownDocument(session, {
|
|
106
|
+
redact: globals.redact !== false,
|
|
107
|
+
reasoning,
|
|
108
|
+
knownSecrets,
|
|
109
|
+
}),
|
|
110
|
+
}));
|
|
111
|
+
const markdown = rendered.map((item) => item.markdown.trimEnd()).join('\n\n---\n\n') + '\n';
|
|
112
|
+
if (options.output)
|
|
113
|
+
fs.writeFileSync(options.output, markdown, { mode: 0o600 });
|
|
114
|
+
if (globals.json) {
|
|
115
|
+
process.stdout.write(JSON.stringify({ redacted: globals.redact !== false, reasoning, sessions: rendered }, null, 2) + '\n');
|
|
116
|
+
}
|
|
117
|
+
else if (!options.output) {
|
|
118
|
+
process.stdout.write(markdown);
|
|
119
|
+
}
|
|
120
|
+
else {
|
|
121
|
+
process.stderr.write(chalk.green(`Rendered ${rendered.length} session${rendered.length === 1 ? '' : 's'} to ${options.output}\n`));
|
|
122
|
+
}
|
|
123
|
+
});
|
|
124
|
+
}
|
|
@@ -56,6 +56,7 @@ import { registerDetachCommand } from './detach.js';
|
|
|
56
56
|
import { registerAttachCommand } from './attach.js';
|
|
57
57
|
import { registerSessionsInjectCommand } from './sessions-inject.js';
|
|
58
58
|
import { registerSessionsExportCommand } from './sessions-export.js';
|
|
59
|
+
import { registerSessionsRenderCommand } from './sessions-render.js';
|
|
59
60
|
import { registerSessionsImportCommand } from './sessions-import.js';
|
|
60
61
|
import { registerSessionsMigrateCommand, registerSessionsMigrationsCommand } from './sessions-migrate.js';
|
|
61
62
|
import { registerSessionsBackfillCommand } from './sessions-backfill.js';
|
|
@@ -3584,7 +3585,7 @@ export function registerSessionsCommands(program) {
|
|
|
3584
3585
|
.option('--query <clause>', 'Search text; repeat with --include tools to require distinct matching calls', collectQueryClause, [])
|
|
3585
3586
|
.option('--resolve <selector>', 'Resolve one full ID, unique prefix, or keyword query to safe session metadata (requires --json; searches the fleet unless --local)')
|
|
3586
3587
|
.addOption(new Option('--resolve-safe-v1 <selector>').hideHelp())
|
|
3587
|
-
.description('Find, browse, and read agent conversation transcripts
|
|
3588
|
+
.description('Find, browse, and read agent conversation transcripts. Live roster: `agents sessions --active` (alias: `agents roster`).')
|
|
3588
3589
|
.option('-a, --agent <agent>', 'Filter by agent type and version (e.g., claude, codex@0.116.0)')
|
|
3589
3590
|
.option('--claude', 'Shorthand for --agent claude')
|
|
3590
3591
|
.option('--codex', 'Shorthand for --agent codex')
|
|
@@ -3735,11 +3736,37 @@ export function registerSessionsCommands(program) {
|
|
|
3735
3736
|
registerAttachCommand(sessionsCmd);
|
|
3736
3737
|
registerSessionsInjectCommand(sessionsCmd);
|
|
3737
3738
|
registerSessionsExportCommand(sessionsCmd);
|
|
3739
|
+
registerSessionsRenderCommand(sessionsCmd);
|
|
3738
3740
|
registerSessionsImportCommand(sessionsCmd);
|
|
3739
3741
|
registerSessionsMigrateCommand(sessionsCmd);
|
|
3740
3742
|
registerSessionsMigrationsCommand(sessionsCmd);
|
|
3741
3743
|
registerSessionsBackfillCommand(sessionsCmd);
|
|
3742
3744
|
registerSessionsStatsCommand(sessionsCmd);
|
|
3745
|
+
// Observe-umbrella alias (Phase 3): roster → sessions --active.
|
|
3746
|
+
registerSessionsObserveAliases(program);
|
|
3747
|
+
}
|
|
3748
|
+
/**
|
|
3749
|
+
* `roster` → sessions --active. Registered with the sessions module so the
|
|
3750
|
+
* lazy loader for `roster` also registers the real `sessions` command for re-parse.
|
|
3751
|
+
*/
|
|
3752
|
+
function registerSessionsObserveAliases(program) {
|
|
3753
|
+
program
|
|
3754
|
+
.command('roster')
|
|
3755
|
+
.description('Live agent roster (alias of `agents sessions --active`). Who is running right now.')
|
|
3756
|
+
.allowUnknownOption()
|
|
3757
|
+
.allowExcessArguments()
|
|
3758
|
+
.action(async () => {
|
|
3759
|
+
const { expandObserveAlias } = await import('../lib/observe-aliases.js');
|
|
3760
|
+
const rest = process.argv.slice(3);
|
|
3761
|
+
const expanded = expandObserveAlias('roster', rest);
|
|
3762
|
+
if (!expanded) {
|
|
3763
|
+
console.error(chalk.red('Unknown observe alias: roster'));
|
|
3764
|
+
process.exit(1);
|
|
3765
|
+
}
|
|
3766
|
+
if (process.stderr.isTTY)
|
|
3767
|
+
process.stderr.write(chalk.gray(`${expanded.note}\n`));
|
|
3768
|
+
await program.parseAsync(['node', 'agents', ...expanded.argv]);
|
|
3769
|
+
});
|
|
3743
3770
|
}
|
|
3744
3771
|
function formatNoSessionsMessage(showAll, project) {
|
|
3745
3772
|
const projectQuery = project?.trim();
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents snapshot` — one-process consumer snapshot for pollers.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the N× `view --json` + `sessions --active --json` (+ optional feed)
|
|
5
|
+
* fork storm with a single command. Does NOT replace `agents status`, which
|
|
6
|
+
* remains the UnifiedSyncStatus sync contract for menubar / Agency drift.
|
|
7
|
+
*
|
|
8
|
+
* JSON shape: {@link FleetSnapshot} in `lib/snapshot.ts` (version: 1).
|
|
9
|
+
*/
|
|
10
|
+
import type { Command } from 'commander';
|
|
11
|
+
export declare function registerSnapshotCommand(program: Command): void;
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `agents snapshot` — one-process consumer snapshot for pollers.
|
|
3
|
+
*
|
|
4
|
+
* Replaces the N× `view --json` + `sessions --active --json` (+ optional feed)
|
|
5
|
+
* fork storm with a single command. Does NOT replace `agents status`, which
|
|
6
|
+
* remains the UnifiedSyncStatus sync contract for menubar / Agency drift.
|
|
7
|
+
*
|
|
8
|
+
* JSON shape: {@link FleetSnapshot} in `lib/snapshot.ts` (version: 1).
|
|
9
|
+
*/
|
|
10
|
+
import chalk from 'chalk';
|
|
11
|
+
import { addHostOption } from '../lib/hosts/option.js';
|
|
12
|
+
import { setHelpSections } from '../lib/help.js';
|
|
13
|
+
import { resolveAgentName, AGENTS } from '../lib/agents.js';
|
|
14
|
+
import { computeSnapshot } from '../lib/snapshot.js';
|
|
15
|
+
import { resolveSurface } from './utils.js';
|
|
16
|
+
function resolveAgentFilter(raw) {
|
|
17
|
+
if (!raw)
|
|
18
|
+
return undefined;
|
|
19
|
+
const name = resolveAgentName(raw.split('@')[0]);
|
|
20
|
+
if (!name) {
|
|
21
|
+
console.error(chalk.red(`Unknown agent "${raw}".`));
|
|
22
|
+
process.exit(1);
|
|
23
|
+
}
|
|
24
|
+
return name;
|
|
25
|
+
}
|
|
26
|
+
/** Compact human summary — the JSON path is the real contract. */
|
|
27
|
+
function renderHuman(snap) {
|
|
28
|
+
console.log(chalk.bold('Fleet snapshot') + chalk.gray(` · ${snap.host} · ${snap.capturedAt}`));
|
|
29
|
+
const invParts = [];
|
|
30
|
+
for (const a of snap.inventory) {
|
|
31
|
+
if (a.versions.length === 0)
|
|
32
|
+
continue;
|
|
33
|
+
const def = a.versions.find((v) => v.isDefault) ?? a.versions[0];
|
|
34
|
+
const label = AGENTS[a.agent]?.name ?? a.agent;
|
|
35
|
+
const signed = def.signedIn ? chalk.green('in') : chalk.gray('out');
|
|
36
|
+
invParts.push(`${label}@${def.version} (${signed})`);
|
|
37
|
+
}
|
|
38
|
+
console.log(` ${'Inventory'.padEnd(12)} ${invParts.length ? invParts.join(chalk.gray(' · ')) : chalk.gray('(none installed)')}`);
|
|
39
|
+
const { running, live, byContext, byAgent } = snap.agents;
|
|
40
|
+
const ctx = Object.entries(byContext)
|
|
41
|
+
.map(([k, n]) => `${k}:${n}`)
|
|
42
|
+
.join(' ');
|
|
43
|
+
const ag = Object.entries(byAgent)
|
|
44
|
+
.map(([k, n]) => `${k}:${n}`)
|
|
45
|
+
.join(' ');
|
|
46
|
+
console.log(` ${'Sessions'.padEnd(12)} ${live} live · ${running} running` +
|
|
47
|
+
(snap.remoteDeviceCount > 0 ? chalk.gray(` · ${snap.remoteDeviceCount} remote device(s)`) : '') +
|
|
48
|
+
(ctx ? chalk.gray(` · ${ctx}`) : '') +
|
|
49
|
+
(ag ? chalk.gray(` · ${ag}`) : ''));
|
|
50
|
+
if (snap.feed) {
|
|
51
|
+
console.log(` ${'Feed'.padEnd(12)} ${snap.feed.openBlocks} open block${snap.feed.openBlocks === 1 ? '' : 's'}`);
|
|
52
|
+
}
|
|
53
|
+
if (snap.sync) {
|
|
54
|
+
const need = snap.sync.agents.filter((a) => a.needsSync).length;
|
|
55
|
+
const sys = snap.sync.system.unknown
|
|
56
|
+
? 'system unknown'
|
|
57
|
+
: snap.sync.system.behind > 0
|
|
58
|
+
? `system ${snap.sync.system.behind} behind`
|
|
59
|
+
: 'system ok';
|
|
60
|
+
console.log(` ${'Sync'.padEnd(12)} ${sys} · ${need} version${need === 1 ? '' : 's'} need sync`);
|
|
61
|
+
}
|
|
62
|
+
console.log(chalk.gray('\n Machine-readable: agents snapshot --json\n' +
|
|
63
|
+
' Sync-only (unchanged): agents status --json'));
|
|
64
|
+
}
|
|
65
|
+
export function registerSnapshotCommand(program) {
|
|
66
|
+
const cmd = addHostOption(program
|
|
67
|
+
.command('snapshot')
|
|
68
|
+
.description('One-process poll snapshot: install inventory + active sessions (optional feed/sync). Not the sync-status command — use `agents status` for drift.'))
|
|
69
|
+
.option('--json', 'Emit the machine-readable FleetSnapshot contract (version 1)')
|
|
70
|
+
.option('--all-hosts', 'Include remote devices in the sessions gather (same fan-out as `sessions --active`; default is this machine only)')
|
|
71
|
+
.option('--with-feed', 'Include open feed-block summary (needs-you count + compact rows)')
|
|
72
|
+
.option('--with-sync', 'Include UnifiedSyncStatus (same engine as `agents status`; opt-in)')
|
|
73
|
+
.option('--agent <name>', 'Restrict inventory to one agent (e.g. claude)');
|
|
74
|
+
setHelpSections(cmd, {
|
|
75
|
+
examples: `
|
|
76
|
+
# One-shot inventory + local active sessions (JSON)
|
|
77
|
+
agents snapshot --json
|
|
78
|
+
|
|
79
|
+
# Same, plus open feed blocks for needs-you polls
|
|
80
|
+
agents snapshot --json --with-feed
|
|
81
|
+
|
|
82
|
+
# Fleet-wide active sessions (matches sessions --active scope)
|
|
83
|
+
agents snapshot --json --all-hosts
|
|
84
|
+
|
|
85
|
+
# Inventory for one harness only (Factory-style usage poll)
|
|
86
|
+
agents snapshot --json --agent claude
|
|
87
|
+
|
|
88
|
+
# Run the whole snapshot on another device
|
|
89
|
+
agents snapshot --json --host yosemite-s0
|
|
90
|
+
`,
|
|
91
|
+
});
|
|
92
|
+
cmd.action(async (opts, command) => {
|
|
93
|
+
const surface = resolveSurface(command);
|
|
94
|
+
const agent = resolveAgentFilter(opts.agent);
|
|
95
|
+
const snap = await computeSnapshot({
|
|
96
|
+
agent,
|
|
97
|
+
local: opts.allHosts !== true,
|
|
98
|
+
withFeed: opts.withFeed === true,
|
|
99
|
+
withSync: opts.withSync === true,
|
|
100
|
+
});
|
|
101
|
+
if (surface.json || opts.json) {
|
|
102
|
+
console.log(JSON.stringify(snap, null, 2));
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
renderHuman(snap);
|
|
106
|
+
});
|
|
107
|
+
}
|
package/dist/commands/ssh.js
CHANGED
|
@@ -18,6 +18,7 @@ import ora from 'ora';
|
|
|
18
18
|
import { getCliVersion } from '../lib/version.js';
|
|
19
19
|
import { readAndResolveBundleEnv, isHeadlessSecretsContext } from '../lib/secrets/bundles.js';
|
|
20
20
|
import { machineId } from '../lib/session/sync/config.js';
|
|
21
|
+
import { isDeviceAuto, resolveDeviceAffinity } from '../lib/smart-launch.js';
|
|
21
22
|
import { registerFleetCaptureCommand } from './fleet-capture.js';
|
|
22
23
|
import { registerFleetApplyAlias } from './apply.js';
|
|
23
24
|
import { addIgnored, getDevice, loadDevices, loadIgnored, removeDevice, removeIgnored, setAutoLaunchEnabled, setAutoLaunchPreferred, upsertDevice, writeReachability, } from '../lib/devices/registry.js';
|
|
@@ -1413,9 +1414,12 @@ Examples:
|
|
|
1413
1414
|
agents ssh win-mini # interactive login
|
|
1414
1415
|
agents ssh win-mini hostname # run a command (PowerShell on Windows)
|
|
1415
1416
|
agents ssh yosemite-s0 uptime # run a command (POSIX)
|
|
1417
|
+
agents ssh auto # affinity-pick a device (same engine as 'agents run --device auto')
|
|
1416
1418
|
|
|
1417
1419
|
Devices come from 'agents devices'. Password auth pulls the secret from a
|
|
1418
1420
|
secrets bundle via an askpass shim — the password never touches argv.
|
|
1421
|
+
'auto' picks a remote device by 14-day usage; a pick landing on this machine
|
|
1422
|
+
is refused with a clear message instead of self-dialing.
|
|
1419
1423
|
`)
|
|
1420
1424
|
.action(async (name, cmd) => {
|
|
1421
1425
|
// Hidden askpass bridge: ssh execs the shim, which re-invokes us here.
|
|
@@ -1423,16 +1427,35 @@ secrets bundle via an askpass shim — the password never touches argv.
|
|
|
1423
1427
|
await runAskpass();
|
|
1424
1428
|
return;
|
|
1425
1429
|
}
|
|
1430
|
+
// `auto` is the same affinity sentinel `agents run --device auto` resolves
|
|
1431
|
+
// (RUSH-2185) — pick the concrete device up front via the SAME engine
|
|
1432
|
+
// (resolveDeviceAffinity), rather than leaning on matchHost's generic
|
|
1433
|
+
// self-resolution (../lib/hosts/registry.js): `agents ssh` connects OUT to
|
|
1434
|
+
// a remote device, so a pick that lands on THIS machine is refused with a
|
|
1435
|
+
// clear message instead of self-SSHing — which also holds when this
|
|
1436
|
+
// machine was never itself enrolled as a device (matchHost would have
|
|
1437
|
+
// nothing to resolve "self" to, and mis-report the pick as "Unknown
|
|
1438
|
+
// device").
|
|
1439
|
+
let target = name;
|
|
1440
|
+
if (isDeviceAuto(name)) {
|
|
1441
|
+
const plan = resolveDeviceAffinity({});
|
|
1442
|
+
if (!plan.host) {
|
|
1443
|
+
console.error(chalk.red(`'auto' picked this machine — 'agents ssh' connects to a remote device. Pass a device name; see 'agents devices list'.`));
|
|
1444
|
+
process.exit(1);
|
|
1445
|
+
}
|
|
1446
|
+
process.stderr.write(chalk.gray(`[agents] device=auto → ${plan.host}\n`));
|
|
1447
|
+
target = plan.host;
|
|
1448
|
+
}
|
|
1426
1449
|
// Accept the full fleet target grammar: a registered `name`, a
|
|
1427
1450
|
// `user@device` (same device, login user overridden — dialed via its
|
|
1428
1451
|
// Tailscale route, not LAN DNS), or an ad-hoc `user@host`/`host` literal.
|
|
1429
1452
|
// A bare unregistered alias still errors as "Unknown device".
|
|
1430
|
-
const device = await resolveDeviceTarget(
|
|
1453
|
+
const device = await resolveDeviceTarget(target);
|
|
1431
1454
|
if (!device) {
|
|
1432
1455
|
// Not a registered device — it may be a leased crabbox box slug. ssh into
|
|
1433
1456
|
// it directly (crabbox@<tailnet|ip>:2222) before giving up.
|
|
1434
|
-
trySshLeasedBox(
|
|
1435
|
-
console.error(chalk.red(`Unknown device '${
|
|
1457
|
+
trySshLeasedBox(target, cmd); // exits the process on a match
|
|
1458
|
+
console.error(chalk.red(`Unknown device '${target}'. See 'agents devices list'.`));
|
|
1436
1459
|
process.exit(1);
|
|
1437
1460
|
}
|
|
1438
1461
|
// Preflight: a device Tailscale last saw offline would otherwise hang
|
package/dist/commands/teams.js
CHANGED
|
@@ -17,6 +17,7 @@ import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } f
|
|
|
17
17
|
import { setHelpSections } from '../lib/help.js';
|
|
18
18
|
import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
|
|
19
19
|
import { resolveHost } from '../lib/hosts/registry.js';
|
|
20
|
+
import { isDeviceAuto, resolveDeviceAffinity } from '../lib/smart-launch.js';
|
|
20
21
|
import { sshTargetFor } from '../lib/hosts/types.js';
|
|
21
22
|
import { ensureHostReady } from '../lib/hosts/ready.js';
|
|
22
23
|
import { remoteShellFor } from '../lib/hosts/remote-cmd.js';
|
|
@@ -275,7 +276,8 @@ function throttleWarningLine(agent, version, r) {
|
|
|
275
276
|
const acct = r.email ? ` (${r.email})` : '';
|
|
276
277
|
const reason = r.reason === 'out_of_credits' ? 'is out of credits'
|
|
277
278
|
: r.reason === 'signed_out' ? 'is not signed in'
|
|
278
|
-
: '
|
|
279
|
+
: r.reason === 'revoked' ? 'needs re-login (its token was revoked)'
|
|
280
|
+
: 'is rate-limited right now';
|
|
279
281
|
return (chalk.yellow(`⚠ ${who}${acct} ${reason}.`) +
|
|
280
282
|
chalk.gray(`\n A pinned version skips account rotation, so it will launch on this account and may immediately hit its limit.` +
|
|
281
283
|
`\n Use a bare \`${agent}\` teammate to let the team pick a healthy account, or pass --force to silence this.`));
|
|
@@ -1329,7 +1331,7 @@ export function registerTeamsCommands(program) {
|
|
|
1329
1331
|
// `--device`/`--host` are aliases (addHostOption registers both). For `teams
|
|
1330
1332
|
// add` the passthrough special-cases them as PLACEMENT, not routing, so the
|
|
1331
1333
|
// local action reads them here. Reject a conflicting pair.
|
|
1332
|
-
|
|
1334
|
+
let explicitDevice = (() => {
|
|
1333
1335
|
const h = opts.host;
|
|
1334
1336
|
const d = opts.device;
|
|
1335
1337
|
if (h && d && h !== d) {
|
|
@@ -1337,6 +1339,16 @@ export function registerTeamsCommands(program) {
|
|
|
1337
1339
|
}
|
|
1338
1340
|
return h ?? d ?? null;
|
|
1339
1341
|
})();
|
|
1342
|
+
// `auto` is the same affinity sentinel `agents run --device auto` resolves
|
|
1343
|
+
// (RUSH-2185) — pick the concrete device name up front so the local-machine
|
|
1344
|
+
// check right below (and every dieFriction message further down) sees the
|
|
1345
|
+
// real target instead of the literal string "auto".
|
|
1346
|
+
if (explicitDevice && isDeviceAuto(explicitDevice)) {
|
|
1347
|
+
const plan = resolveDeviceAffinity({});
|
|
1348
|
+
const picked = plan.host ?? machineId();
|
|
1349
|
+
process.stderr.write(chalk.gray(`[teams] device=auto → ${picked === machineId() ? 'local' : picked}\n`));
|
|
1350
|
+
explicitDevice = picked;
|
|
1351
|
+
}
|
|
1340
1352
|
// Distributed teams: --device <name> PINS this teammate to a machine over
|
|
1341
1353
|
// SSH. Resolve + validate the placement here so a bad target fails at `add`
|
|
1342
1354
|
// time, not silently at launch. Persisted (hostName/hostTarget/repoPath) so
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -129,6 +129,13 @@ export declare function parseResourceSections(options: {
|
|
|
129
129
|
resources?: string | boolean;
|
|
130
130
|
detailed?: boolean;
|
|
131
131
|
} & ViewSectionFilter, jsonMode: boolean): Set<ResourceSection>;
|
|
132
|
+
/**
|
|
133
|
+
* Collect structured info for one or more agents without rendering to the
|
|
134
|
+
* terminal. Used by `--json` output and any programmatic consumer (e.g. the
|
|
135
|
+
* agents-cli extension's "resume current session in best available version"
|
|
136
|
+
* command).
|
|
137
|
+
*/
|
|
138
|
+
export declare function collectAgentsJson(filterAgentId?: AgentId, resourceSections?: Set<ResourceSection>): Promise<ViewJsonAgent[]>;
|
|
132
139
|
/**
|
|
133
140
|
* Identity key for duplicate-install detection. Prefers accountKey — which
|
|
134
141
|
* encodes account AND org — over the bare email: two installs can share an
|
package/dist/commands/view.js
CHANGED
|
@@ -1242,7 +1242,7 @@ export function parseResourceSections(options, jsonMode) {
|
|
|
1242
1242
|
* agents-cli extension's "resume current session in best available version"
|
|
1243
1243
|
* command).
|
|
1244
1244
|
*/
|
|
1245
|
-
async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
1245
|
+
export async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
1246
1246
|
const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
|
|
1247
1247
|
const wantResources = !!resourceSections && resourceSections.size > 0;
|
|
1248
1248
|
// Only pay for the git-status + resource scans when resources were requested.
|
package/dist/index.js
CHANGED
|
@@ -94,7 +94,7 @@ if (IS_DEV_BUILD) {
|
|
|
94
94
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
95
95
|
// first byte of output), the registry maps a command name to a thunk that
|
|
96
96
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
97
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
97
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadExport, loadPackages, loadRoutines, loadMonitors, loadProjects, loadRun, loadFork, loadDefaults, loadSet, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadStatus, loadSnapshot, loadProfiles, loadHarness, loadSecrets, loadLogin, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadFactory, loadUsage, loadCost, loadPerf, loadTrends, loadOutput, loadBudget, loadAlias, loadMine, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadUninstall, loadShare, loadSend, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
98
98
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
99
99
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
100
100
|
import { getCliLaunch } from './lib/cli-entry.js';
|
|
@@ -310,6 +310,15 @@ Run and dispatch:
|
|
|
310
310
|
browser Automate a browser — navigate, click, screenshot, console, network
|
|
311
311
|
pty Drive interactive terminal programs (REPLs, TUIs) via a persistent PTY session
|
|
312
312
|
|
|
313
|
+
Observe (read the fleet — no store merge; aliases point at the real readers):
|
|
314
|
+
feed / inbox Needs-you inbox (open blocks waiting on you)
|
|
315
|
+
timeline Agent progress stream (= feed --filter updates)
|
|
316
|
+
roster Live agents (= sessions --active)
|
|
317
|
+
events Unified ops + activity event trail
|
|
318
|
+
audit Tamper-evident run-dispatch log (not events)
|
|
319
|
+
snapshot One-process inventory + active sessions poll
|
|
320
|
+
status Sync/drift only (not the live fleet snapshot)
|
|
321
|
+
|
|
313
322
|
Credentials and profiles:
|
|
314
323
|
profile Activate resource profiles across skills, MCP, permissions, and secrets
|
|
315
324
|
profiles Bundles of (host CLI, endpoint, model, auth)
|
|
@@ -938,6 +947,7 @@ async function registerAllEagerCommands() {
|
|
|
938
947
|
registerCheckTombstoneCommand(program);
|
|
939
948
|
await reg(loadApply);
|
|
940
949
|
await reg(loadStatus);
|
|
950
|
+
await reg(loadSnapshot);
|
|
941
951
|
registerExecAliasCommand(program);
|
|
942
952
|
await reg(loadProfiles);
|
|
943
953
|
await reg(loadHarness);
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent gate for driving THIS machine's browser from another fleet machine.
|
|
3
|
+
*
|
|
4
|
+
* `browser --host <device>` routes a browser command to `<device>` over SSH (the
|
|
5
|
+
* fleet passthrough) and runs `agents browser ...` there. Every such remote
|
|
6
|
+
* invocation carries the {@link FLEET_REMOTE_ENV} marker, set once at the fleet
|
|
7
|
+
* dispatch site (`maybeRunOnHost`). A machine only accepts being driven when its
|
|
8
|
+
* owner has opted in via `agents browser remote-control on` (the device-scope
|
|
9
|
+
* `browser.remote-control` config key, never synced).
|
|
10
|
+
*
|
|
11
|
+
* Local invocations (no marker) are never gated — this only governs cross-machine
|
|
12
|
+
* drives. Read-only queries are not gated here; the gate sits at the drive entry
|
|
13
|
+
* point (`browser start`), the one command that opens/attaches a browser.
|
|
14
|
+
*/
|
|
15
|
+
/** Env marker set on every remote `agents` invocation by `buildRemoteAgentsInvocation`. */
|
|
16
|
+
export declare const FLEET_REMOTE_ENV = "AGENTS_FLEET_REMOTE";
|
|
17
|
+
/** True when this process was dispatched to this machine by a fleet `--host` run. */
|
|
18
|
+
export declare function isFleetRemoteInvocation(env?: NodeJS.ProcessEnv): boolean;
|
|
19
|
+
/**
|
|
20
|
+
* Whether this machine allows other fleet machines to drive its browser. Reads
|
|
21
|
+
* the device-scope `browser.remote-control` config key. Unset = off (deny).
|
|
22
|
+
*/
|
|
23
|
+
export declare function remoteControlEnabled(): boolean;
|
|
24
|
+
/**
|
|
25
|
+
* Guard the browser-drive entry point. A fleet-remote invocation may only drive
|
|
26
|
+
* this machine's browser when the owner opted in; otherwise throw a clear,
|
|
27
|
+
* actionable error. A local invocation is never gated.
|
|
28
|
+
*
|
|
29
|
+
* `env` and `enabled` are injectable so the decision is unit-testable without a
|
|
30
|
+
* real remote hop or a real config store.
|
|
31
|
+
*/
|
|
32
|
+
export declare function assertRemoteControlAllowed(opts?: {
|
|
33
|
+
env?: NodeJS.ProcessEnv;
|
|
34
|
+
enabled?: boolean;
|
|
35
|
+
}): void;
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Consent gate for driving THIS machine's browser from another fleet machine.
|
|
3
|
+
*
|
|
4
|
+
* `browser --host <device>` routes a browser command to `<device>` over SSH (the
|
|
5
|
+
* fleet passthrough) and runs `agents browser ...` there. Every such remote
|
|
6
|
+
* invocation carries the {@link FLEET_REMOTE_ENV} marker, set once at the fleet
|
|
7
|
+
* dispatch site (`maybeRunOnHost`). A machine only accepts being driven when its
|
|
8
|
+
* owner has opted in via `agents browser remote-control on` (the device-scope
|
|
9
|
+
* `browser.remote-control` config key, never synced).
|
|
10
|
+
*
|
|
11
|
+
* Local invocations (no marker) are never gated — this only governs cross-machine
|
|
12
|
+
* drives. Read-only queries are not gated here; the gate sits at the drive entry
|
|
13
|
+
* point (`browser start`), the one command that opens/attaches a browser.
|
|
14
|
+
*/
|
|
15
|
+
import { getConfigValue } from '../device-config.js';
|
|
16
|
+
/** Env marker set on every remote `agents` invocation by `buildRemoteAgentsInvocation`. */
|
|
17
|
+
export const FLEET_REMOTE_ENV = 'AGENTS_FLEET_REMOTE';
|
|
18
|
+
/** True when this process was dispatched to this machine by a fleet `--host` run. */
|
|
19
|
+
export function isFleetRemoteInvocation(env = process.env) {
|
|
20
|
+
return env[FLEET_REMOTE_ENV] === '1';
|
|
21
|
+
}
|
|
22
|
+
/**
|
|
23
|
+
* Whether this machine allows other fleet machines to drive its browser. Reads
|
|
24
|
+
* the device-scope `browser.remote-control` config key. Unset = off (deny).
|
|
25
|
+
*/
|
|
26
|
+
export function remoteControlEnabled() {
|
|
27
|
+
return getConfigValue('browser.remote-control').value === true;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* Guard the browser-drive entry point. A fleet-remote invocation may only drive
|
|
31
|
+
* this machine's browser when the owner opted in; otherwise throw a clear,
|
|
32
|
+
* actionable error. A local invocation is never gated.
|
|
33
|
+
*
|
|
34
|
+
* `env` and `enabled` are injectable so the decision is unit-testable without a
|
|
35
|
+
* real remote hop or a real config store.
|
|
36
|
+
*/
|
|
37
|
+
export function assertRemoteControlAllowed(opts) {
|
|
38
|
+
const env = opts?.env ?? process.env;
|
|
39
|
+
if (!isFleetRemoteInvocation(env))
|
|
40
|
+
return;
|
|
41
|
+
const enabled = opts?.enabled ?? remoteControlEnabled();
|
|
42
|
+
if (enabled)
|
|
43
|
+
return;
|
|
44
|
+
const who = env.AGENTS_ACTOR_HOST || env.AGENTS_ACTOR || 'A fleet machine';
|
|
45
|
+
throw new Error(`${who} tried to drive this machine's browser over \`browser --host\`, but remote ` +
|
|
46
|
+
`browser control is off here. To allow it, run on THIS machine:\n` +
|
|
47
|
+
` agents browser remote-control on`);
|
|
48
|
+
}
|
|
@@ -50,6 +50,22 @@ export interface HealInfo {
|
|
|
50
50
|
role: string;
|
|
51
51
|
name: string;
|
|
52
52
|
}
|
|
53
|
+
/**
|
|
54
|
+
* Resolve the identity stamped on a task at start: WHO (`owner`) and WHICH run
|
|
55
|
+
* (`launchId`). The forwarded values come from the caller's own CLI process and
|
|
56
|
+
* are authoritative — the browser daemon is shared and long-lived, so resolving
|
|
57
|
+
* the actor daemon-side (the RUSH-2020 bug) mis-attributes every task to the
|
|
58
|
+
* daemon's owner. `resolveLocalActor` is consulted ONLY when no actor was
|
|
59
|
+
* forwarded (a CLI that predates the field, mid-rollout) — never to override a
|
|
60
|
+
* forwarded one.
|
|
61
|
+
*/
|
|
62
|
+
export declare function resolveTaskIdentity(forwarded: {
|
|
63
|
+
actor?: string;
|
|
64
|
+
launchId?: string;
|
|
65
|
+
}, resolveLocalActor: () => string): {
|
|
66
|
+
owner: string;
|
|
67
|
+
launchId?: string;
|
|
68
|
+
};
|
|
53
69
|
export declare class BrowserService {
|
|
54
70
|
private static readonly SOURCE_PREFIX;
|
|
55
71
|
private connections;
|
|
@@ -64,6 +80,9 @@ export declare class BrowserService {
|
|
|
64
80
|
url?: string;
|
|
65
81
|
endpointName?: string;
|
|
66
82
|
skipDomainSkill?: boolean;
|
|
83
|
+
/** Caller identity, forwarded from the CLI (see IPCRequest.actor/launchId). */
|
|
84
|
+
actor?: string;
|
|
85
|
+
launchId?: string;
|
|
67
86
|
}): Promise<{
|
|
68
87
|
task: string;
|
|
69
88
|
name: string;
|
|
@@ -222,6 +222,21 @@ async function isConnHealthy(conn, timeoutMs = 1000) {
|
|
|
222
222
|
return false;
|
|
223
223
|
}
|
|
224
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* Resolve the identity stamped on a task at start: WHO (`owner`) and WHICH run
|
|
227
|
+
* (`launchId`). The forwarded values come from the caller's own CLI process and
|
|
228
|
+
* are authoritative — the browser daemon is shared and long-lived, so resolving
|
|
229
|
+
* the actor daemon-side (the RUSH-2020 bug) mis-attributes every task to the
|
|
230
|
+
* daemon's owner. `resolveLocalActor` is consulted ONLY when no actor was
|
|
231
|
+
* forwarded (a CLI that predates the field, mid-rollout) — never to override a
|
|
232
|
+
* forwarded one.
|
|
233
|
+
*/
|
|
234
|
+
export function resolveTaskIdentity(forwarded, resolveLocalActor) {
|
|
235
|
+
return {
|
|
236
|
+
owner: forwarded.actor ?? resolveLocalActor(),
|
|
237
|
+
launchId: forwarded.launchId,
|
|
238
|
+
};
|
|
239
|
+
}
|
|
225
240
|
export class BrowserService {
|
|
226
241
|
static SOURCE_PREFIX = {
|
|
227
242
|
'rush-app': 'rush-app-',
|
|
@@ -332,7 +347,10 @@ export class BrowserService {
|
|
|
332
347
|
currentTabId: undefined,
|
|
333
348
|
createdAt: Date.now(),
|
|
334
349
|
pid: conn.pid,
|
|
335
|
-
|
|
350
|
+
// Identity is forwarded from the caller (see resolveTaskIdentity): WHO
|
|
351
|
+
// (owner) and WHICH run (launchId). Resolving daemon-side would attribute
|
|
352
|
+
// every task to the shared daemon's actor (the RUSH-2020 bug).
|
|
353
|
+
...resolveTaskIdentity({ actor: opts.actor, launchId: opts.launchId }, () => resolveActor().id),
|
|
336
354
|
};
|
|
337
355
|
// For Electron, get the existing window as the tab
|
|
338
356
|
if (conn.electron) {
|