@phnx-labs/agents-cli 1.22.43 → 1.22.44
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 +77 -0
- package/README.md +14 -12
- package/dist/bootstrap.js +8 -6
- package/dist/cli/command-registry.d.ts +0 -5
- package/dist/cli/command-registry.js +1 -17
- package/dist/commands/apply.d.ts +7 -10
- package/dist/commands/apply.js +15 -35
- package/dist/commands/artifacts.js +4 -5
- package/dist/commands/audit.d.ts +6 -6
- package/dist/commands/audit.js +12 -12
- package/dist/commands/auth.js +7 -3
- package/dist/commands/beta.d.ts +7 -1
- package/dist/commands/beta.js +16 -9
- package/dist/commands/commands.js +1 -1
- package/dist/commands/doctor.js +1 -4
- package/dist/commands/events.d.ts +2 -2
- package/dist/commands/events.js +6 -5
- package/dist/commands/exec.d.ts +1 -1
- package/dist/commands/exec.js +8 -4
- package/dist/commands/factory.d.ts +1 -1
- package/dist/commands/fleet-capture.d.ts +1 -1
- package/dist/commands/fleet-capture.js +4 -5
- package/dist/commands/harness.js +1 -49
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/insights.d.ts +1 -1
- package/dist/commands/insights.js +3 -2
- package/dist/commands/menubar.js +58 -1
- package/dist/commands/org.d.ts +5 -0
- package/dist/commands/org.js +82 -29
- package/dist/commands/output.js +2 -2
- package/dist/commands/prune.js +5 -4
- package/dist/commands/routines.test-fixture.d.ts +86 -0
- package/dist/commands/routines.test-fixture.js +345 -0
- package/dist/commands/sessions.test-fixture.d.ts +24 -0
- package/dist/commands/sessions.test-fixture.js +224 -0
- package/dist/commands/setup.js +5 -1
- package/dist/commands/share.d.ts +9 -10
- package/dist/commands/share.js +68 -50
- package/dist/commands/skills.js +1 -1
- package/dist/commands/ssh.js +15 -62
- package/dist/commands/subagents.js +1 -1
- package/dist/commands/view.js +5 -2
- package/dist/lib/analytics/mix-commands.d.ts +6 -28
- package/dist/lib/analytics/mix-commands.js +14 -40
- package/dist/lib/audit/log.d.ts +2 -2
- package/dist/lib/audit/log.js +2 -2
- package/dist/lib/beta.js +1 -1
- package/dist/lib/browser/domain-skills.d.ts +26 -6
- package/dist/lib/browser/domain-skills.js +81 -43
- package/dist/lib/daemon/daemon.js +11 -0
- package/dist/lib/daemon/daemon.test-fixture.d.ts +24 -0
- package/dist/lib/daemon/daemon.test-fixture.js +71 -0
- package/dist/lib/daemon-ticks.d.ts +38 -1
- package/dist/lib/daemon-ticks.js +52 -5
- package/dist/lib/device-config.js +1 -2
- package/dist/lib/devices/discovery-policy.d.ts +3 -3
- package/dist/lib/devices/discovery-policy.js +3 -3
- package/dist/lib/devices/fleet.d.ts +4 -8
- package/dist/lib/devices/fleet.js +3 -15
- package/dist/lib/devices/health-report.js +3 -3
- package/dist/lib/devices/pool.d.ts +0 -6
- package/dist/lib/devices/pool.js +0 -6
- package/dist/lib/devices/registry.d.ts +0 -15
- package/dist/lib/devices/registry.js +0 -9
- package/dist/lib/devices/stats-cache.d.ts +12 -0
- package/dist/lib/devices/stats-cache.js +24 -5
- package/dist/lib/fleet/capture.d.ts +1 -1
- package/dist/lib/fleet/manifest.js +1 -1
- package/dist/lib/fleet/remote-login.d.ts +2 -2
- package/dist/lib/fleet/remote-login.js +3 -4
- package/dist/lib/hosts/dispatch.d.ts +2 -0
- package/dist/lib/hosts/dispatch.js +8 -1
- package/dist/lib/hosts/passthrough.js +0 -1
- package/dist/lib/hosts/providers/devices.js +1 -13
- package/dist/lib/hosts/ready.d.ts +12 -3
- package/dist/lib/hosts/ready.js +27 -12
- package/dist/lib/hosts/registry.d.ts +4 -5
- package/dist/lib/hosts/registry.js +3 -7
- package/dist/lib/installations/versions.js +21 -2
- package/dist/lib/menubar/MenubarHelper.app/Contents/Info.plist +1 -5
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/_CodeSignature/CodeResources +2 -15
- package/dist/lib/menubar/install-menubar.d.ts +109 -0
- package/dist/lib/menubar/install-menubar.js +199 -4
- package/dist/lib/prix-account.d.ts +2 -1
- package/dist/lib/prix-account.js +2 -1
- package/dist/lib/remote-agents-json.js +1 -7
- package/dist/lib/routines-placement.d.ts +1 -1
- package/dist/lib/routines-placement.js +1 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Info.plist +0 -2
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/_CodeSignature/CodeResources +1 -13
- package/dist/lib/session/active.d.ts +4 -16
- package/dist/lib/session/active.js +2 -6
- package/dist/lib/session/db.d.ts +13 -98
- package/dist/lib/session/db.js +4 -11
- package/dist/lib/session/discover.d.ts +5 -81
- package/dist/lib/session/discover.js +5 -5
- package/dist/lib/session/remote/remote-bundle.d.ts +12 -0
- package/dist/lib/session/remote/remote-bundle.js +61 -0
- package/dist/lib/session/remote/remote-list.d.ts +148 -0
- package/dist/lib/session/remote/remote-list.js +607 -0
- package/dist/lib/session/remote/remote.d.ts +132 -0
- package/dist/lib/session/remote/remote.js +314 -0
- package/dist/lib/session/remote/watch.d.ts +101 -0
- package/dist/lib/session/remote/watch.js +241 -0
- package/dist/lib/session/remote-bundle.d.ts +1 -12
- package/dist/lib/session/remote-bundle.js +3 -61
- package/dist/lib/session/remote-list.d.ts +1 -148
- package/dist/lib/session/remote-list.js +3 -607
- package/dist/lib/session/remote.d.ts +1 -132
- package/dist/lib/session/remote.js +3 -314
- package/dist/lib/session/watch.d.ts +1 -101
- package/dist/lib/session/watch.js +3 -242
- package/dist/lib/share/config.d.ts +12 -2
- package/dist/lib/share/config.js +47 -11
- package/dist/lib/share/delete.js +1 -1
- package/dist/lib/share/publish.js +1 -1
- package/dist/lib/share/worker-template.js +2 -2
- package/dist/lib/smart-launch.d.ts +4 -4
- package/dist/lib/smart-launch.js +8 -18
- package/dist/lib/staleness/index.d.ts +3 -1
- package/dist/lib/staleness/index.js +21 -1
- package/dist/lib/staleness/types.d.ts +11 -0
- package/dist/lib/staleness/writers/commands.js +13 -4
- package/dist/lib/staleness/writers/hooks.js +3 -1
- package/dist/lib/staleness/writers/mcp.d.ts +0 -7
- package/dist/lib/staleness/writers/mcp.js +16 -1
- package/dist/lib/staleness/writers/rules.js +1 -1
- package/dist/lib/staleness/writers/skills.js +3 -1
- package/dist/lib/staleness/writers/subagents.d.ts +0 -9
- package/dist/lib/staleness/writers/subagents.js +18 -1
- package/dist/lib/staleness/writers/types.d.ts +10 -0
- package/dist/lib/startup/command-registry.d.ts +7 -1
- package/dist/lib/startup/command-registry.js +17 -5
- package/dist/lib/startup/root-command.d.ts +18 -1
- package/dist/lib/startup/root-command.js +18 -1
- package/dist/lib/state.js +7 -0
- package/dist/lib/teams/placement-probe.js +27 -12
- package/dist/lib/teams/scheduler.d.ts +3 -1
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/view-types.d.ts +3 -0
- package/package.json +2 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/serve.d.ts +0 -10
- package/dist/commands/serve.js +0 -68
- package/dist/commands/trends.d.ts +0 -10
- package/dist/commands/trends.js +0 -12
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/Resources/AppIcon.icns +0 -0
- package/dist/lib/serve/control.d.ts +0 -95
- package/dist/lib/serve/control.js +0 -260
- package/dist/lib/serve/data.d.ts +0 -81
- package/dist/lib/serve/data.js +0 -91
- package/dist/lib/serve/page.d.ts +0 -7
- package/dist/lib/serve/page.js +0 -140
- package/dist/lib/serve/server.d.ts +0 -80
- package/dist/lib/serve/server.js +0 -145
- package/dist/lib/serve/stream.d.ts +0 -43
- package/dist/lib/serve/stream.js +0 -116
- package/dist/lib/serve/token.d.ts +0 -35
- 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
|
-
|
|
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
|
-
|
|
3
|
-
*
|
|
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
|
-
|
|
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';
|