@phnx-labs/agents-cli 1.22.43 → 1.22.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (164) hide show
  1. package/CHANGELOG.md +85 -0
  2. package/README.md +14 -12
  3. package/dist/bootstrap.js +3 -6
  4. package/dist/cli/command-registry.d.ts +0 -7
  5. package/dist/cli/command-registry.js +1 -21
  6. package/dist/commands/accounts.d.ts +1 -9
  7. package/dist/commands/accounts.js +12 -90
  8. package/dist/commands/apply.d.ts +7 -10
  9. package/dist/commands/apply.js +15 -35
  10. package/dist/commands/artifacts.js +4 -5
  11. package/dist/commands/audit.d.ts +6 -6
  12. package/dist/commands/audit.js +12 -12
  13. package/dist/commands/beta.d.ts +7 -1
  14. package/dist/commands/beta.js +16 -9
  15. package/dist/commands/commands.js +1 -1
  16. package/dist/commands/doctor.js +1 -4
  17. package/dist/commands/events.d.ts +2 -2
  18. package/dist/commands/events.js +6 -5
  19. package/dist/commands/exec.d.ts +1 -1
  20. package/dist/commands/exec.js +8 -4
  21. package/dist/commands/factory.d.ts +1 -1
  22. package/dist/commands/fleet-capture.d.ts +1 -1
  23. package/dist/commands/fleet-capture.js +4 -5
  24. package/dist/commands/harness.js +1 -49
  25. package/dist/commands/hooks.js +1 -1
  26. package/dist/commands/insights.d.ts +1 -1
  27. package/dist/commands/insights.js +85 -156
  28. package/dist/commands/menubar.js +58 -1
  29. package/dist/commands/output.js +2 -2
  30. package/dist/commands/prune.js +5 -4
  31. package/dist/commands/routines.test-fixture.d.ts +86 -0
  32. package/dist/commands/routines.test-fixture.js +345 -0
  33. package/dist/commands/sessions.test-fixture.d.ts +24 -0
  34. package/dist/commands/sessions.test-fixture.js +224 -0
  35. package/dist/commands/setup.js +5 -1
  36. package/dist/commands/share.d.ts +9 -10
  37. package/dist/commands/share.js +68 -50
  38. package/dist/commands/skills.js +1 -1
  39. package/dist/commands/ssh.js +15 -62
  40. package/dist/commands/subagents.js +1 -1
  41. package/dist/commands/view.js +5 -2
  42. package/dist/lib/analytics/mix-commands.d.ts +6 -28
  43. package/dist/lib/analytics/mix-commands.js +14 -40
  44. package/dist/lib/audit/log.d.ts +2 -2
  45. package/dist/lib/audit/log.js +2 -2
  46. package/dist/lib/beta.js +1 -1
  47. package/dist/lib/browser/domain-skills.d.ts +26 -6
  48. package/dist/lib/browser/domain-skills.js +81 -43
  49. package/dist/lib/daemon/daemon.js +11 -0
  50. package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
  51. package/dist/lib/daemon/daemon.test-fixture.js +71 -0
  52. package/dist/lib/daemon-ticks.d.ts +38 -1
  53. package/dist/lib/daemon-ticks.js +52 -5
  54. package/dist/lib/device-config.js +1 -2
  55. package/dist/lib/devices/discovery-policy.d.ts +3 -3
  56. package/dist/lib/devices/discovery-policy.js +3 -3
  57. package/dist/lib/devices/fleet.d.ts +4 -8
  58. package/dist/lib/devices/fleet.js +3 -15
  59. package/dist/lib/devices/health-report.js +3 -3
  60. package/dist/lib/devices/pool.d.ts +0 -6
  61. package/dist/lib/devices/pool.js +0 -6
  62. package/dist/lib/devices/registry.d.ts +0 -15
  63. package/dist/lib/devices/registry.js +0 -9
  64. package/dist/lib/devices/stats-cache.d.ts +12 -0
  65. package/dist/lib/devices/stats-cache.js +24 -5
  66. package/dist/lib/fleet/capture.d.ts +1 -1
  67. package/dist/lib/fleet/manifest.js +1 -1
  68. package/dist/lib/fleet/remote-login.d.ts +2 -2
  69. package/dist/lib/fleet/remote-login.js +3 -4
  70. package/dist/lib/hosts/dispatch.d.ts +2 -0
  71. package/dist/lib/hosts/dispatch.js +8 -1
  72. package/dist/lib/hosts/passthrough.js +0 -1
  73. package/dist/lib/hosts/providers/devices.js +1 -13
  74. package/dist/lib/hosts/ready.d.ts +12 -3
  75. package/dist/lib/hosts/ready.js +27 -12
  76. package/dist/lib/hosts/registry.d.ts +4 -5
  77. package/dist/lib/hosts/registry.js +3 -7
  78. package/dist/lib/installations/versions.js +21 -2
  79. package/dist/lib/menubar/install-menubar.d.ts +109 -0
  80. package/dist/lib/menubar/install-menubar.js +199 -4
  81. package/dist/lib/remote-agents-json.js +1 -7
  82. package/dist/lib/routines-placement.d.ts +1 -1
  83. package/dist/lib/routines-placement.js +1 -1
  84. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  85. package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
  86. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  87. package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
  88. package/dist/lib/session/active.d.ts +4 -16
  89. package/dist/lib/session/active.js +2 -6
  90. package/dist/lib/session/db.d.ts +13 -98
  91. package/dist/lib/session/db.js +4 -11
  92. package/dist/lib/session/discover.d.ts +5 -81
  93. package/dist/lib/session/discover.js +5 -5
  94. package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
  95. package/dist/lib/session/remote/remote-bundle.js +61 -0
  96. package/dist/lib/session/remote/remote-list.d.ts +148 -0
  97. package/dist/lib/session/remote/remote-list.js +607 -0
  98. package/dist/lib/session/remote/remote.d.ts +132 -0
  99. package/dist/lib/session/remote/remote.js +314 -0
  100. package/dist/lib/session/remote/watch.d.ts +101 -0
  101. package/dist/lib/session/remote/watch.js +241 -0
  102. package/dist/lib/session/remote-bundle.d.ts +1 -12
  103. package/dist/lib/session/remote-bundle.js +3 -61
  104. package/dist/lib/session/remote-list.d.ts +1 -148
  105. package/dist/lib/session/remote-list.js +3 -607
  106. package/dist/lib/session/remote.d.ts +1 -132
  107. package/dist/lib/session/remote.js +3 -314
  108. package/dist/lib/session/watch.d.ts +1 -101
  109. package/dist/lib/session/watch.js +3 -242
  110. package/dist/lib/share/config.d.ts +12 -2
  111. package/dist/lib/share/config.js +47 -11
  112. package/dist/lib/share/delete.js +1 -1
  113. package/dist/lib/share/publish.js +1 -1
  114. package/dist/lib/share/worker-template.js +2 -2
  115. package/dist/lib/smart-launch.d.ts +4 -4
  116. package/dist/lib/smart-launch.js +8 -18
  117. package/dist/lib/staleness/index.d.ts +3 -1
  118. package/dist/lib/staleness/index.js +21 -1
  119. package/dist/lib/staleness/types.d.ts +11 -0
  120. package/dist/lib/staleness/writers/commands.js +13 -4
  121. package/dist/lib/staleness/writers/hooks.js +3 -1
  122. package/dist/lib/staleness/writers/mcp.d.ts +0 -7
  123. package/dist/lib/staleness/writers/mcp.js +16 -1
  124. package/dist/lib/staleness/writers/rules.js +1 -1
  125. package/dist/lib/staleness/writers/skills.js +3 -1
  126. package/dist/lib/staleness/writers/subagents.d.ts +0 -9
  127. package/dist/lib/staleness/writers/subagents.js +18 -1
  128. package/dist/lib/staleness/writers/types.d.ts +10 -0
  129. package/dist/lib/startup/command-registry.d.ts +8 -1
  130. package/dist/lib/startup/command-registry.js +21 -6
  131. package/dist/lib/startup/root-command.d.ts +18 -1
  132. package/dist/lib/startup/root-command.js +18 -1
  133. package/dist/lib/state.js +7 -0
  134. package/dist/lib/teams/placement-probe.js +27 -12
  135. package/dist/lib/teams/scheduler.d.ts +3 -1
  136. package/dist/lib/types.d.ts +1 -1
  137. package/dist/lib/view-types.d.ts +3 -0
  138. package/package.json +2 -3
  139. package/dist/bin/agents +0 -0
  140. package/dist/commands/auth.d.ts +0 -9
  141. package/dist/commands/auth.js +0 -108
  142. package/dist/commands/org.d.ts +0 -6
  143. package/dist/commands/org.js +0 -175
  144. package/dist/commands/serve.d.ts +0 -10
  145. package/dist/commands/serve.js +0 -68
  146. package/dist/commands/trends.d.ts +0 -10
  147. package/dist/commands/trends.js +0 -12
  148. package/dist/lib/entitlement.d.ts +0 -31
  149. package/dist/lib/entitlement.js +0 -137
  150. package/dist/lib/prix-account.d.ts +0 -158
  151. package/dist/lib/prix-account.js +0 -214
  152. package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
  153. package/dist/lib/serve/control.d.ts +0 -95
  154. package/dist/lib/serve/control.js +0 -260
  155. package/dist/lib/serve/data.d.ts +0 -81
  156. package/dist/lib/serve/data.js +0 -91
  157. package/dist/lib/serve/page.d.ts +0 -7
  158. package/dist/lib/serve/page.js +0 -140
  159. package/dist/lib/serve/server.d.ts +0 -80
  160. package/dist/lib/serve/server.js +0 -145
  161. package/dist/lib/serve/stream.d.ts +0 -43
  162. package/dist/lib/serve/stream.js +0 -116
  163. package/dist/lib/serve/token.d.ts +0 -35
  164. package/dist/lib/serve/token.js +0 -85
@@ -0,0 +1,241 @@
1
+ import { createHash, randomUUID } from 'node:crypto';
2
+ import { spawn } from 'node:child_process';
3
+ import { createInterface } from 'node:readline';
4
+ import * as fs from 'node:fs';
5
+ import * as path from 'node:path';
6
+ import { loadDevices, isDialableDevice } from '../../devices/registry.js';
7
+ import { deviceIdentityArgs, sshTargetFor } from '../../devices/connect.js';
8
+ import { machineId, normalizeHost } from '../../machine-id.js';
9
+ import { SSH_OPTS, controlOpts, shellQuote } from '../../ssh-exec.js';
10
+ import { buildWindowsAgentsCommand, remoteShellFor } from '../../hosts/remote-cmd.js';
11
+ import { activeSessionsJournalPath, activeSessionJournalIdentity, readActiveSessionsCache, } from '../session-cache.js';
12
+ export const SESSION_WATCH_VERSION = 1;
13
+ export const SESSION_WATCH_HEARTBEAT_MS = 15_000;
14
+ /** Stable, opaque identity for one row within one device scope. */
15
+ export function sessionWatchRowKey(scope, row) {
16
+ const identity = activeSessionJournalIdentity(row);
17
+ return createHash('sha256').update(`${scope}\0${identity}`).digest('base64url').slice(0, 22);
18
+ }
19
+ export function toSessionWatchRow(scope, row) {
20
+ const rowKey = sessionWatchRowKey(scope, row);
21
+ const resumable = Boolean(row.sessionId);
22
+ const viewingIn = row.viewingIn
23
+ ? [row.viewingIn.app, row.viewingIn.tab ? `tab ${row.viewingIn.tab}` : undefined].filter(Boolean).join(' ')
24
+ : null;
25
+ return {
26
+ ...row,
27
+ rowKey,
28
+ sourceDevice: scope,
29
+ resumable,
30
+ unwatched: !viewingIn,
31
+ viewingIn,
32
+ recovery: resumable
33
+ ? { command: 'agents', args: ['sessions', 'resume', row.sessionId, '--device', scope], ...(row.cwd ? { cwd: row.cwd } : {}) }
34
+ : null,
35
+ };
36
+ }
37
+ function sameRow(a, b) {
38
+ return JSON.stringify(a) === JSON.stringify(b);
39
+ }
40
+ /** Stream-local sequencer and convergent row diff. */
41
+ export class SessionWatchState {
42
+ streamId;
43
+ sequence = 0;
44
+ rows = new Map();
45
+ constructor(streamId = randomUUID()) { this.streamId = streamId; }
46
+ base(type) {
47
+ return { version: SESSION_WATCH_VERSION, type, streamId: this.streamId, sequence: ++this.sequence, capturedAt: Date.now() };
48
+ }
49
+ reset(scope, sourceRows) {
50
+ const rows = sourceRows.map((row) => toSessionWatchRow(scope, row));
51
+ this.rows.set(scope, new Map(rows.map((row) => [row.rowKey, row])));
52
+ return { ...this.base('reset'), scope, rows };
53
+ }
54
+ update(scope, sourceRows) {
55
+ const previous = this.rows.get(scope) ?? new Map();
56
+ const nextRows = sourceRows.map((row) => toSessionWatchRow(scope, row));
57
+ const next = new Map(nextRows.map((row) => [row.rowKey, row]));
58
+ const events = [];
59
+ for (const [rowKey, row] of next) {
60
+ if (!previous.has(rowKey) || !sameRow(previous.get(rowKey), row)) {
61
+ events.push({ ...this.base('upsert'), scope, rowKey, row });
62
+ }
63
+ }
64
+ for (const rowKey of previous.keys()) {
65
+ if (!next.has(rowKey))
66
+ events.push({ ...this.base('remove'), scope, rowKey });
67
+ }
68
+ this.rows.set(scope, next);
69
+ return events;
70
+ }
71
+ patch(scope, upserts, removes) {
72
+ const current = new Map(this.rows.get(scope) ?? []);
73
+ const events = [];
74
+ for (const source of upserts) {
75
+ const row = toSessionWatchRow(scope, source);
76
+ if (!current.has(row.rowKey) || !sameRow(current.get(row.rowKey), row)) {
77
+ current.set(row.rowKey, row);
78
+ events.push({ ...this.base('upsert'), scope, rowKey: row.rowKey, row });
79
+ }
80
+ }
81
+ for (const identity of removes) {
82
+ const rowKey = createHash('sha256').update(`${scope}\0${identity}`).digest('base64url').slice(0, 22);
83
+ if (current.delete(rowKey))
84
+ events.push({ ...this.base('remove'), scope, rowKey });
85
+ }
86
+ this.rows.set(scope, current);
87
+ return events;
88
+ }
89
+ scope(scope, status, reason) {
90
+ // Deliberately retain rows while unavailable. A reconnecting scope replaces
91
+ // them with a reset; transient fleet loss must not look like session death.
92
+ return { ...this.base('scope'), scope, status, ...(reason ? { reason } : {}) };
93
+ }
94
+ heartbeat(scope, capturedAt = Date.now()) {
95
+ return { ...this.base('heartbeat'), scope, capturedAt };
96
+ }
97
+ }
98
+ /**
99
+ * Keep one local subscription alive. Startup reads one canonical reset snapshot;
100
+ * steady state tails the canonical writer journal and never invokes a gather.
101
+ */
102
+ export async function watchLocalSessions(options) {
103
+ const state = new SessionWatchState();
104
+ const readCache = options.readCache ?? readActiveSessionsCache;
105
+ const journal = options.journalPath ?? activeSessionsJournalPath();
106
+ const heartbeatMs = options.heartbeatMs ?? SESSION_WATCH_HEARTBEAT_MS;
107
+ let offset = 0;
108
+ try {
109
+ offset = fs.statSync(journal).size;
110
+ }
111
+ catch { /* first publication */ }
112
+ const initial = readCache('local');
113
+ options.emit(state.reset(options.scope, initial?.sessions ?? []));
114
+ options.emit(state.scope(options.scope, initial ? 'available' : 'unavailable', initial ? undefined : 'awaiting publisher'));
115
+ if (options.signal.aborted)
116
+ return;
117
+ await new Promise((resolve) => {
118
+ let partial = '';
119
+ let reading = false;
120
+ const readAppended = () => {
121
+ if (reading || options.signal.aborted)
122
+ return;
123
+ reading = true;
124
+ try {
125
+ const size = fs.statSync(journal).size;
126
+ if (size < offset) {
127
+ offset = 0;
128
+ partial = '';
129
+ }
130
+ if (size === offset)
131
+ return;
132
+ const fd = fs.openSync(journal, 'r');
133
+ try {
134
+ const buffer = Buffer.alloc(size - offset);
135
+ fs.readSync(fd, buffer, 0, buffer.length, offset);
136
+ offset = size;
137
+ const lines = (partial + buffer.toString('utf8')).split('\n');
138
+ partial = lines.pop() ?? '';
139
+ for (const line of lines) {
140
+ if (!line)
141
+ continue;
142
+ try {
143
+ const record = JSON.parse(line);
144
+ if (record.version !== 1 || record.scope !== 'local' || !Array.isArray(record.upserts) || !Array.isArray(record.removes))
145
+ continue;
146
+ for (const event of state.patch(options.scope, record.upserts, record.removes))
147
+ options.emit(event);
148
+ options.emit(state.scope(options.scope, 'available'));
149
+ }
150
+ catch { /* partial/corrupt journal lines are not state */ }
151
+ }
152
+ }
153
+ finally {
154
+ fs.closeSync(fd);
155
+ }
156
+ }
157
+ catch { /* journal may not exist until the first publisher write */ }
158
+ finally {
159
+ reading = false;
160
+ }
161
+ };
162
+ fs.mkdirSync(path.dirname(journal), { recursive: true });
163
+ const journalListener = () => readAppended();
164
+ fs.watchFile(journal, { interval: options.journalPollMs ?? 250 }, journalListener);
165
+ const heartbeatTimer = setInterval(() => options.emit(state.heartbeat(options.scope)), heartbeatMs);
166
+ const stop = () => { fs.unwatchFile(journal, journalListener); clearInterval(heartbeatTimer); resolve(); };
167
+ options.signal.addEventListener('abort', stop, { once: true });
168
+ // Close the offset/read/watch handoff: a publisher may append after the
169
+ // startup offset is captured but before watchFile is registered.
170
+ readAppended();
171
+ });
172
+ }
173
+ function remoteWatchCommand(os) {
174
+ const args = ['sessions', 'watch', '--json', '--local'];
175
+ return remoteShellFor(os) === 'powershell'
176
+ ? buildWindowsAgentsCommand({ args })
177
+ : `bash -lc ${shellQuote(`agents ${args.map(shellQuote).join(' ')}`)}`;
178
+ }
179
+ /**
180
+ * Subscribe to every dialable compute device with one persistent SSH process.
181
+ * A peer disconnect emits `scope: unavailable` but no removes; reconnecting
182
+ * peer resets only its own scope. There is no recurring fleet list command.
183
+ */
184
+ export async function watchFleetSessions(options) {
185
+ const local = watchLocalSessions({ scope: machineId(), signal: options.signal, emit: options.emit });
186
+ let devices;
187
+ try {
188
+ devices = await loadDevices();
189
+ }
190
+ catch {
191
+ await local;
192
+ return;
193
+ }
194
+ const self = machineId();
195
+ const peers = Object.values(devices).filter((device) => isDialableDevice(device)
196
+ && normalizeHost(device.name) !== self
197
+ && ['windows', 'linux', 'macos'].includes(device.platform));
198
+ const reconnectMs = options.reconnectMs ?? 2_000;
199
+ const peerTasks = peers.map(async (device) => {
200
+ const scope = normalizeHost(device.name);
201
+ const state = new SessionWatchState();
202
+ while (!options.signal.aborted) {
203
+ let target;
204
+ try {
205
+ target = sshTargetFor(device);
206
+ }
207
+ catch (error) {
208
+ options.emit(state.scope(scope, 'unavailable', error instanceof Error ? error.message : String(error)));
209
+ break;
210
+ }
211
+ const child = spawn('ssh', [
212
+ ...SSH_OPTS, ...controlOpts(), ...deviceIdentityArgs(device), target, remoteWatchCommand(device.platform),
213
+ ], { stdio: ['ignore', 'pipe', 'ignore'] });
214
+ const stop = () => child.kill('SIGTERM');
215
+ options.signal.addEventListener('abort', stop, { once: true });
216
+ const reader = createInterface({ input: child.stdout });
217
+ reader.on('line', (line) => {
218
+ try {
219
+ const event = JSON.parse(line);
220
+ if (event.version === SESSION_WATCH_VERSION && typeof event.sequence === 'number')
221
+ options.emit(event);
222
+ }
223
+ catch { /* incomplete/non-protocol peer output is not state */ }
224
+ });
225
+ const code = await new Promise((resolve) => {
226
+ child.once('error', () => resolve(null));
227
+ child.once('close', resolve);
228
+ });
229
+ reader.close();
230
+ options.signal.removeEventListener('abort', stop);
231
+ if (options.signal.aborted)
232
+ break;
233
+ options.emit(state.scope(scope, 'unavailable', code === null ? 'ssh failed' : `ssh exited ${code}`));
234
+ await new Promise((resolve) => {
235
+ const timer = setTimeout(resolve, reconnectMs);
236
+ options.signal.addEventListener('abort', () => { clearTimeout(timer); resolve(); }, { once: true });
237
+ });
238
+ }
239
+ });
240
+ await Promise.all([local, ...peerTasks]);
241
+ }
@@ -1,12 +1 @@
1
- import { type ParsedBundle } from './bundle.js';
2
- export interface RemotePullResult {
3
- bundles: ParsedBundle[];
4
- errors: string[];
5
- }
6
- /**
7
- * Run `agents sessions export …exportArgs --stdout` on each host and parse the
8
- * streamed bundle. A host that fails (unreachable, remote error, bad output) is
9
- * collected in `errors` and skipped — one asleep peer never aborts the pull.
10
- * `exportArgs` must NOT contain --device (the remote export runs for itself only).
11
- */
12
- export declare function pullBundlesFromHosts(hosts: string[], exportArgs: string[]): Promise<RemotePullResult>;
1
+ export * from './remote/remote-bundle.js';
@@ -1,61 +1,3 @@
1
- /**
2
- * Multi-device session transfer over the EXISTING SSH fleet transport
3
- * (RUSH-1712) — no R2, no daemon. `agents sessions export --device <h>` and
4
- * `agents sessions import --from-host <h>` both run `agents sessions export
5
- * … --stdout` ON the peer and stream the bundle back over the same SSH path the
6
- * cross-machine listing already uses (resolveExplicitTargets + ssh-exec), then
7
- * either write it (export) or import it (import) locally.
8
- *
9
- * This deliberately reuses ssh-exec / resolve-target rather than adding a second
10
- * transport: the raw form `agents ssh boxA 'agents sessions export --stdout' |
11
- * agents sessions import -` works with plain export/import; this module is just
12
- * the one-shot wrapper around it.
13
- */
14
- import chalk from 'chalk';
15
- import { sshExec } from '../ssh-exec.js';
16
- import { shellQuote } from '../ssh-exec.js';
17
- import { resolveExplicitTargets } from '../devices/resolve-target.js';
18
- import { remoteShellFor, buildWindowsAgentsCommand } from '../hosts/remote-cmd.js';
19
- import { parseBundle } from './bundle.js';
20
- /** Remote export can traverse many sessions; give it a generous ceiling. */
21
- const REMOTE_EXPORT_TIMEOUT_MS = 300_000;
22
- /** Build `agents <args>` for the peer's login shell (bash or PowerShell). */
23
- function remoteAgentsCommand(args, os) {
24
- if (remoteShellFor(os) === 'powershell') {
25
- return buildWindowsAgentsCommand({ args });
26
- }
27
- const inner = ['agents', ...args].map((t, i) => (i === 0 ? t : shellQuote(t))).join(' ');
28
- return `bash -lc ${shellQuote(inner)}`;
29
- }
30
- /**
31
- * Run `agents sessions export …exportArgs --stdout` on each host and parse the
32
- * streamed bundle. A host that fails (unreachable, remote error, bad output) is
33
- * collected in `errors` and skipped — one asleep peer never aborts the pull.
34
- * `exportArgs` must NOT contain --device (the remote export runs for itself only).
35
- */
36
- export async function pullBundlesFromHosts(hosts, exportArgs) {
37
- const targets = await resolveExplicitTargets(hosts);
38
- const bundles = [];
39
- const errors = [];
40
- for (const t of targets) {
41
- const cmd = remoteAgentsCommand(['sessions', 'export', ...exportArgs, '--stdout'], t.os);
42
- process.stderr.write(chalk.dim(`Pulling sessions from ${t.name}…\n`));
43
- const res = sshExec(t.target, cmd, { timeoutMs: REMOTE_EXPORT_TIMEOUT_MS, extraSshArgs: t.extraSshArgs });
44
- if (res.timedOut) {
45
- errors.push(`${t.name}: timed out after ${Math.round(REMOTE_EXPORT_TIMEOUT_MS / 1000)}s`);
46
- continue;
47
- }
48
- if (res.code !== 0) {
49
- const tail = res.stderr.trim().split('\n').filter(Boolean).pop();
50
- errors.push(`${t.name}: remote export failed (${res.code ?? 'ssh error'})${tail ? ': ' + tail : ''}`);
51
- continue;
52
- }
53
- try {
54
- bundles.push(parseBundle(res.stdout));
55
- }
56
- catch (err) {
57
- errors.push(`${t.name}: ${err.message}`);
58
- }
59
- }
60
- return { bundles, errors };
61
- }
1
+ // Moved to ./remote/remote-bundle.ts (move 4 of the core-module refactor).
2
+ // Kept as a re-export shim so existing importers don't break.
3
+ export * from './remote/remote-bundle.js';
@@ -1,148 +1 @@
1
- import { type DeviceProfile } from '../devices/registry.js';
2
- import { type RemoteAgentsJsonParseResult } from '../remote-agents-json.js';
3
- import type { SessionMeta } from './types.js';
4
- import { type ToolProgramCountEnvelope, type ToolSearchEnvelope } from './tool-index.js';
5
- export { REMOTE_STDOUT_MAX_BYTES, RemoteUtf8Accumulator } from '../ssh-exec.js';
6
- export declare const REMOTE_TOOL_AGGREGATE_MAX_BYTES: number;
7
- export interface RemoteToolByteBudget {
8
- remainingBytes: number;
9
- exhausted: boolean;
10
- }
11
- /** Claim received bytes against one fleet-query budget before retaining them. */
12
- export declare function consumeRemoteToolByteBudget(budget: RemoteToolByteBudget, bytes: number): boolean;
13
- /** Charge sanitized, machine-stamped evidence because redaction may expand it. */
14
- export declare function consumeParsedRemoteToolSearchBudget(budget: RemoteToolByteBudget, envelope: ToolSearchEnvelope): boolean;
15
- /**
16
- * The command run on each peer: answer for itself, as JSON, without recursing.
17
- * `forwardedArgs` carry the caller's own query/filters (already including the
18
- * leading `sessions` and a `--json`) so each peer returns a comparable slice.
19
- * A Windows peer gets a PowerShell invocation (ssh lands in cmd.exe/PowerShell
20
- * there, where `bash -lc` is not a command); every other OS keeps `bash -lc`.
21
- */
22
- export declare function remoteListCommand(forwardedArgs: string[], os?: string): string;
23
- /**
24
- * Parse a peer's `sessions --json` stdout into `SessionMeta[]`, tagging each
25
- * with `machine`. Defensive against version skew / partial output: non-JSON or
26
- * a non-array yields `[]`, and non-object entries are dropped rather than
27
- * throwing. The `machine` we dialed always wins over any value the peer set on
28
- * its own rows, so grouping keys off the computer we asked. Exported for unit
29
- * testing without a live tailnet.
30
- */
31
- export declare function parseRemoteList(stdout: string, machine: string): SessionMeta[];
32
- export declare function parseRemoteListPayload(stdout: string, machine: string, safeResolver?: boolean): {
33
- items: SessionMeta[];
34
- valid: boolean;
35
- };
36
- /** Run one remote `agents sessions … --json` and capture stdout. Resolves
37
- * `{ code: null }` on spawn error or timeout (host treated as dead). */
38
- export declare function sshCapture(target: string, remoteCmd: string, timeoutMs: number, aggregateBudget?: RemoteToolByteBudget, options?: {
39
- multiplex?: boolean;
40
- port?: number;
41
- hostKeyOpts?: string[];
42
- }): Promise<{
43
- code: number | null;
44
- stdout: string;
45
- aggregateBudgetExceeded?: boolean;
46
- }>;
47
- export interface RemoteListResult {
48
- sessions: SessionMeta[];
49
- /** How many peer machines we attempted to reach (drives the empty-fleet tip). */
50
- deviceCount: number;
51
- /**
52
- * Peers that failed to answer, by display name. The stderr note above is
53
- * enough for a printed listing, but the interactive browser repaints over it —
54
- * so callers rendering a full-screen UI need the outcome as data to tell
55
- * "that box is asleep" apart from "that box has no matching sessions".
56
- */
57
- unreachable: string[];
58
- }
59
- /** Keep browse and tool-search fan-out on the same automatic peer set. */
60
- export declare function isAutomaticSessionPeer(d: DeviceProfile, self: string): boolean;
61
- /**
62
- * Gather listing sessions from other machines. With an explicit `hosts` list
63
- * (from `--device`), fan out to exactly those. Otherwise sweep the registered,
64
- * online devices from `ag devices`, excluding this machine and any without an
65
- * address. `forwardedArgs` are the caller's own sessions args (query + filters,
66
- * already `--json`) so every peer returns the same slice this machine asked for.
67
- */
68
- export interface GatherRemoteListOptions {
69
- /**
70
- * Opt-in early-exit for a globally-unique id lookup (a full UUID): the first
71
- * peer to return the matching row resolves the fan-out and cancels the rest.
72
- * Omitted for browse/label/prefix sweeps, which must wait for every peer to
73
- * know whether the match is unique or conflicting.
74
- */
75
- isDefinitive?: (session: SessionMeta, machine: string) => boolean;
76
- /** Per-peer deadline for slower indexed browse queries. */
77
- timeoutMs?: number;
78
- }
79
- export declare function gatherRemoteList(forwardedArgs: string[], hosts?: string[], opts?: GatherRemoteListOptions): Promise<RemoteListResult>;
80
- export interface RemoteToolSearchResult {
81
- envelopes: Array<{
82
- machine: string;
83
- envelope: ToolSearchEnvelope;
84
- }>;
85
- deviceCount: number;
86
- unreachable: string[];
87
- truncated: string[];
88
- }
89
- export interface RemoteToolProgramCountResult {
90
- envelopes: Array<{
91
- machine: string;
92
- envelope: ToolProgramCountEnvelope;
93
- }>;
94
- deviceCount: number;
95
- unreachable: string[];
96
- }
97
- export declare function parseRemoteToolProgramCount(stdout: string, machine: string, expectedProgram: string): RemoteAgentsJsonParseResult<{
98
- machine: string;
99
- envelope: ToolProgramCountEnvelope;
100
- }>;
101
- export declare function gatherRemoteToolProgramCounts(forwardedArgs: string[], hosts: string[] | undefined, expectedProgram: string): Promise<RemoteToolProgramCountResult>;
102
- export declare function parseRemoteToolSearch(stdout: string, machine: string, expectedClauses?: string[]): ToolSearchEnvelope | undefined;
103
- /**
104
- * Fleet sibling of {@link gatherRemoteList} for the versioned tool-search
105
- * envelope. Each peer executes the same local-only query against its own
106
- * SQLite index; only compact matches cross SSH.
107
- */
108
- export declare function gatherRemoteToolSearch(forwardedArgs: string[], hosts?: string[], maxAggregateBytes?: number, expectedClauses?: string[]): Promise<RemoteToolSearchResult>;
109
- /** Resolve a peer's SSH target (and OS) from the device registry by its
110
- * normalized machine id — the same id the fan-out tags rows with. Returns
111
- * undefined when no registered device with an address matches. */
112
- export declare function resolvePeerTarget(machine: string): Promise<{
113
- target: string;
114
- os?: string;
115
- } | undefined>;
116
- /**
117
- * Fetch one remote session's preview digest from its owning peer — the data
118
- * behind the picker pane for a `_remote` row, whose transcript file this
119
- * machine cannot parse. Runs the peer's own `agents sessions preview <id>
120
- * --local --json` (the same envelope `agents sessions preview` already
121
- * delegates to for a remote id) and returns its `preview` object, verbatim and
122
- * UNSANITIZED — the caller owns scrubbing peer-supplied strings before any of
123
- * them reach a TTY. Undefined on every failure: unregistered machine, SSH
124
- * error, timeout, version-skewed peer with no `--json` preview envelope.
125
- */
126
- export declare function fetchPeerPreviewDigest(sessionId: string, machine: string, timeoutMs?: number): Promise<unknown | undefined>;
127
- /**
128
- * Run `agents <args>` ON a peer over SSH, attached to this terminal (inherited
129
- * stdio). `args` is the full arg vector after the binary — callers pass e.g.
130
- * `['sessions', id, '--markdown']` or `['sessions', 'resume', id]`. Used when a
131
- * picked session lives on another machine: its transcript and agent binary are
132
- * there, so both reading (no TTY) and resuming (TTY) must execute on the peer —
133
- * not via a local `--device` hop, which would discover locally and dead-end for a
134
- * session that exists only on the peer. Resolves 'no-target' when the machine
135
- * isn't a dialable registered device; the caller surfaces a clear message.
136
- *
137
- * `opts.env` adds variables to the remote command. It deliberately does NOT
138
- * carry `AGENTS_FLEET_REMOTE` the way the `--device` passthrough does: that marker
139
- * gates consent-sensitive actions on the far side
140
- * (lib/browser/remote-control.ts), and a resumed agent is a long-lived session
141
- * that would inherit it for its whole life — `agents browser start` inside it
142
- * would then be refused as a cross-machine drive. A one-shot `--device` command
143
- * can carry the marker; a session cannot.
144
- */
145
- export declare function runOnPeer(args: string[], machine: string, opts?: {
146
- tty?: boolean;
147
- env?: Record<string, string>;
148
- }): Promise<'ok' | 'no-target'>;
1
+ export * from './remote/remote-list.js';