@phnx-labs/agents-cli 1.22.114 → 1.22.115
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 +110 -0
- package/README.md +22 -102
- package/dist/browser.js +0 -0
- package/dist/cli/command-registry.js +0 -7
- package/dist/commands/accounts.d.ts +3 -77
- package/dist/commands/accounts.js +13 -366
- package/dist/commands/audit.js +1 -1
- package/dist/commands/auth.js +0 -2
- package/dist/commands/browser.d.ts +53 -0
- package/dist/commands/browser.js +199 -9
- package/dist/commands/doctor.d.ts +2 -3
- package/dist/commands/doctor.js +0 -11
- package/dist/commands/events.d.ts +1 -3
- package/dist/commands/events.js +4 -7
- package/dist/commands/exec.d.ts +0 -1
- package/dist/commands/exec.js +2 -7
- package/dist/commands/feed-watch.js +52 -8
- package/dist/commands/focus.js +1 -1
- package/dist/commands/go.d.ts +0 -17
- package/dist/commands/go.js +2 -19
- package/dist/commands/logs.js +1 -1
- package/dist/commands/mcp.js +8 -83
- package/dist/commands/memory.js +4 -47
- package/dist/commands/message.js +4 -4
- package/dist/commands/plugins.js +1 -93
- package/dist/commands/repo.js +0 -44
- package/dist/commands/resume.js +4 -1
- package/dist/commands/secrets-passthrough.js +2 -2
- package/dist/commands/send.d.ts +4 -4
- package/dist/commands/send.js +6 -51
- package/dist/commands/sessions-backup-setup.js +1 -1
- package/dist/commands/sessions-resume.js +0 -1
- package/dist/commands/sessions-share.d.ts +5 -7
- package/dist/commands/sessions-share.js +98 -49
- package/dist/commands/sessions.js +1 -8
- package/dist/commands/setup-browser.js +18 -2
- package/dist/commands/setup-computer.js +20 -6
- package/dist/commands/setup-secrets.js +24 -4
- package/dist/commands/setup-terminal.d.ts +3 -0
- package/dist/commands/setup-terminal.js +22 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +20 -10
- package/dist/commands/skills.js +0 -8
- package/dist/commands/ssh.d.ts +6 -0
- package/dist/commands/ssh.js +92 -233
- package/dist/commands/sync.js +14 -5
- package/dist/commands/teams.js +1 -1
- package/dist/commands/traces.js +1 -1
- package/dist/lib/accounts/add.d.ts +0 -5
- package/dist/lib/accounts/add.js +1 -7
- package/dist/lib/artifacts-client.d.ts +20 -0
- package/dist/lib/artifacts-client.js +46 -0
- package/dist/lib/auth-mint.js +2 -2
- package/dist/lib/browser/runtime-state.d.ts +55 -0
- package/dist/lib/browser/runtime-state.js +99 -18
- package/dist/lib/browser/service.js +21 -1
- package/dist/lib/cli-resources.js +3 -1
- package/dist/lib/cloud/dispatch.js +1 -1
- package/dist/lib/cloudflare/creds.d.ts +10 -0
- package/dist/lib/cloudflare/creds.js +46 -0
- package/dist/lib/cloudflare/provision.d.ts +35 -0
- package/dist/lib/cloudflare/provision.js +144 -0
- package/dist/lib/computer/sessions-list.d.ts +55 -0
- package/dist/lib/computer/sessions-list.js +168 -1
- package/dist/lib/daemon/daemon.js +8 -1
- package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
- package/dist/lib/daemon/feed-stream-service.js +40 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +5 -0
- package/dist/lib/devices/connect.d.ts +49 -5
- package/dist/lib/devices/connect.js +169 -21
- package/dist/lib/feed/envelope.d.ts +79 -0
- package/dist/lib/feed/envelope.js +23 -0
- package/dist/lib/feed/events.d.ts +6 -0
- package/dist/lib/feed/events.js +8 -0
- package/dist/lib/feed/hub-server.d.ts +86 -0
- package/dist/lib/feed/hub-server.js +334 -0
- package/dist/lib/feed/hub.d.ts +95 -0
- package/dist/lib/feed/hub.js +255 -0
- package/dist/lib/feed/tool-activity.d.ts +108 -0
- package/dist/lib/feed/tool-activity.js +313 -0
- package/dist/lib/feed/tools.d.ts +198 -0
- package/dist/lib/feed/tools.js +265 -0
- package/dist/lib/feed/watch.d.ts +50 -50
- package/dist/lib/feed/watch.js +147 -16
- package/dist/lib/format.d.ts +1 -1
- package/dist/lib/format.js +1 -1
- package/dist/lib/git.d.ts +0 -16
- package/dist/lib/git.js +0 -58
- package/dist/lib/helper-versions.js +1 -1
- package/dist/lib/hosts/remote-cmd.d.ts +51 -1
- package/dist/lib/hosts/remote-cmd.js +125 -8
- package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
- package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
- package/dist/lib/mcp.js +17 -11
- package/dist/lib/probe.d.ts +4 -1
- package/dist/lib/probe.js +5 -2
- package/dist/lib/pwsh.d.ts +33 -0
- package/dist/lib/pwsh.js +56 -0
- package/dist/lib/redact.d.ts +8 -0
- package/dist/lib/redact.js +11 -0
- package/dist/lib/refresh.d.ts +6 -2
- package/dist/lib/refresh.js +92 -72
- package/dist/lib/secrets-cli.d.ts +11 -0
- package/dist/lib/secrets-cli.js +30 -0
- package/dist/lib/secrets-client.js +3 -2
- package/dist/lib/session/detached.d.ts +7 -0
- package/dist/lib/session/detached.js +29 -0
- package/dist/lib/session/remote/peer-stream.d.ts +24 -2
- package/dist/lib/session/remote/peer-stream.js +33 -6
- package/dist/lib/session/sync/backend.d.ts +3 -3
- package/dist/lib/session/sync/backend.js +3 -3
- package/dist/lib/session/sync/provision.d.ts +1 -1
- package/dist/lib/session/sync/provision.js +2 -2
- package/dist/lib/sessions-client.js +0 -3
- package/dist/lib/setup-tool-install.d.ts +3 -0
- package/dist/lib/setup-tool-install.js +26 -0
- package/dist/lib/setup-tool-status.d.ts +22 -0
- package/dist/lib/setup-tool-status.js +215 -0
- package/dist/lib/share-runtime.d.ts +11 -0
- package/dist/lib/share-runtime.js +63 -0
- package/dist/lib/smart-launch.d.ts +1 -5
- package/dist/lib/smart-launch.js +3 -11
- package/dist/lib/ssh-exec.d.ts +44 -0
- package/dist/lib/ssh-exec.js +119 -0
- package/dist/lib/startup/command-registry.d.ts +6 -4
- package/dist/lib/startup/command-registry.js +10 -7
- package/dist/lib/state.js +2 -2
- package/dist/lib/storage/selection.d.ts +2 -2
- package/dist/lib/storage/selection.js +2 -2
- package/dist/lib/sync-umbrella.d.ts +5 -0
- package/dist/lib/sync-umbrella.js +18 -10
- package/dist/lib/traces/backend.d.ts +1 -2
- package/dist/lib/traces/backend.js +1 -2
- package/dist/lib/traces/provision.d.ts +1 -1
- package/dist/lib/traces/provision.js +2 -2
- package/dist/lib/types.d.ts +8 -6
- package/package.json +2 -3
- package/dist/commands/artifacts-setup.d.ts +0 -53
- package/dist/commands/artifacts-setup.js +0 -161
- package/dist/commands/artifacts.d.ts +0 -18
- package/dist/commands/artifacts.js +0 -58
- package/dist/commands/attach.d.ts +0 -12
- package/dist/commands/attach.js +0 -86
- package/dist/commands/auth-mint.d.ts +0 -12
- package/dist/commands/auth-mint.js +0 -108
- package/dist/commands/reconnect.d.ts +0 -46
- package/dist/commands/reconnect.js +0 -115
- package/dist/commands/share.d.ts +0 -293
- package/dist/commands/share.js +0 -1424
- package/dist/lib/share/analytics.d.ts +0 -13
- package/dist/lib/share/analytics.js +0 -45
- package/dist/lib/share/backend.d.ts +0 -120
- package/dist/lib/share/backend.js +0 -176
- package/dist/lib/share/capture.d.ts +0 -31
- package/dist/lib/share/capture.js +0 -174
- package/dist/lib/share/config.d.ts +0 -72
- package/dist/lib/share/config.js +0 -211
- package/dist/lib/share/delete.d.ts +0 -123
- package/dist/lib/share/delete.js +0 -173
- package/dist/lib/share/html.d.ts +0 -20
- package/dist/lib/share/html.js +0 -88
- package/dist/lib/share/http-error.d.ts +0 -53
- package/dist/lib/share/http-error.js +0 -65
- package/dist/lib/share/og.d.ts +0 -26
- package/dist/lib/share/og.js +0 -84
- package/dist/lib/share/provision.d.ts +0 -127
- package/dist/lib/share/provision.js +0 -285
- package/dist/lib/share/publish.d.ts +0 -379
- package/dist/lib/share/publish.js +0 -818
- package/dist/lib/share/worker-template.d.ts +0 -27
- package/dist/lib/share/worker-template.js +0 -2424
- package/dist/lib/storage/index.d.ts +0 -14
- package/dist/lib/storage/index.js +0 -14
- package/dist/lib/storage/visibility.d.ts +0 -82
- package/dist/lib/storage/visibility.js +0 -99
|
@@ -144,10 +144,59 @@ export declare function listAllProfileSnapshots(): ProfileSnapshot[];
|
|
|
144
144
|
* alone — we'd rather leak than wrongly kill a user-owned process that
|
|
145
145
|
* happens to share metadata.
|
|
146
146
|
*/
|
|
147
|
+
/**
|
|
148
|
+
* Clean up what a DEAD daemon genuinely left behind — which is not the browser.
|
|
149
|
+
*
|
|
150
|
+
* A live local browser outliving its daemon is the NORMAL case, not an orphan.
|
|
151
|
+
* `BrowserService.shutdown` deliberately closes only CDP and leaves the browser
|
|
152
|
+
* running, and on macOS the spawn is detached so the daemon's own positive-pid
|
|
153
|
+
* `killTree` reaches the daemon alone. This reaper used to read "the recorded
|
|
154
|
+
* `daemonPid` has exited" as "everything it recorded is garbage" and SIGTERM the
|
|
155
|
+
* browser — so replacing the daemon (a menubar activation, an upgrade, a crash
|
|
156
|
+
* restart) killed a browser the user was looking at, and `clearProfileRuntime`
|
|
157
|
+
* then removed the `port`/meta that {@link readProfileRuntime} needs, so even a
|
|
158
|
+
* survivor could not be re-attached and a second browser would launch beside it.
|
|
159
|
+
*
|
|
160
|
+
* The three cases are now decided independently:
|
|
161
|
+
*
|
|
162
|
+
* - **A live local browser/electron is preserved untouched** — not signalled, not
|
|
163
|
+
* cleared, and its `daemonPid` deliberately NOT rewritten. Ownership is adopted
|
|
164
|
+
* only after an identity-checked attach succeeds (`adoptProfileRuntimeOwner`,
|
|
165
|
+
* called from the reuse branch in `BrowserService.connectProfile`), so nothing
|
|
166
|
+
* claims a browser it has not proven it can talk to. Re-running this is a no-op.
|
|
167
|
+
* - **A dead browser record is cleared**, with no signal sent: there is nothing
|
|
168
|
+
* alive to signal, and the stale pid/port would otherwise mislead the next
|
|
169
|
+
* attach.
|
|
170
|
+
* - **A stale tunnel IS reaped.** A local `ssh -L` cannot outlive its purpose, so
|
|
171
|
+
* a dead owner does make it an orphan. This is the one thing the old code was
|
|
172
|
+
* right about; it just shared a code path with the browser.
|
|
173
|
+
*
|
|
174
|
+
* The accepted trade: a genuinely leaked browser from a long-dead daemon is no
|
|
175
|
+
* longer force-killed here. Idle/abandoned cleanup has its own owner in
|
|
176
|
+
* `browser/hygiene.ts` (session-dead + idle), which the daemon's periodic tick and
|
|
177
|
+
* `agents browser prune` both call — that is the right place for it, because it can
|
|
178
|
+
* tell "idle and abandoned" from "in use right now" and this function cannot.
|
|
179
|
+
*/
|
|
147
180
|
export declare function reapOrphanedProcesses(): {
|
|
148
181
|
reaped: number;
|
|
149
182
|
details: string[];
|
|
150
183
|
};
|
|
184
|
+
/**
|
|
185
|
+
* Take ownership of a runtime record after an identity-checked attach succeeded.
|
|
186
|
+
*
|
|
187
|
+
* Called from the reuse branch in `BrowserService.connectProfile`, which has just
|
|
188
|
+
* probed `/json/version` and run `verifyBrowserIdentity` against the profile's
|
|
189
|
+
* expected browser family — so by this point the process on the recorded port is
|
|
190
|
+
* proven to be ours. Only then may a new service claim it.
|
|
191
|
+
*
|
|
192
|
+
* Rewrites `daemonPid` ALONE. Every other field — `pid`, `port`, `command`,
|
|
193
|
+
* `userDataDir`, `spawnedAt`, `kind`, `tunnelPid` — is the original launch's
|
|
194
|
+
* metadata and stays exactly as recorded: this is an ownership handover, not a
|
|
195
|
+
* re-registration, and rewriting the rest would lose the provenance the reaper and
|
|
196
|
+
* the hygiene pass both read. A record that has since disappeared is left alone
|
|
197
|
+
* rather than recreated from a partial view.
|
|
198
|
+
*/
|
|
199
|
+
export declare function adoptProfileRuntimeOwner(profileName: string, daemonPid?: number): boolean;
|
|
151
200
|
/**
|
|
152
201
|
* True when ANY cache dir belonging to this profile still has a live browser or
|
|
153
202
|
* SSH tunnel, or an open task. The composite naming (`<name>@<endpoint>`) means
|
|
@@ -233,4 +282,10 @@ export declare function buildProfilePrunePlan(): Promise<PrunePlan>;
|
|
|
233
282
|
* went, matching what `--dry-run` printed.
|
|
234
283
|
*/
|
|
235
284
|
export declare function pruneProfiles(plan: PrunePlan): Promise<PrunePlan>;
|
|
285
|
+
/**
|
|
286
|
+
* The executable/image name the live `pid` is running, or null if it can't be
|
|
287
|
+
* determined. The process-listing API differs per OS: Windows has no `ps`, so
|
|
288
|
+
* we query `tasklist` (CSV image name in column 1); POSIX uses `ps -o comm=`.
|
|
289
|
+
*/
|
|
290
|
+
export declare function liveProcessCommand(pid: number): string | null;
|
|
236
291
|
export {};
|
|
@@ -258,6 +258,39 @@ function readTaskCount(dir) {
|
|
|
258
258
|
* alone — we'd rather leak than wrongly kill a user-owned process that
|
|
259
259
|
* happens to share metadata.
|
|
260
260
|
*/
|
|
261
|
+
/**
|
|
262
|
+
* Clean up what a DEAD daemon genuinely left behind — which is not the browser.
|
|
263
|
+
*
|
|
264
|
+
* A live local browser outliving its daemon is the NORMAL case, not an orphan.
|
|
265
|
+
* `BrowserService.shutdown` deliberately closes only CDP and leaves the browser
|
|
266
|
+
* running, and on macOS the spawn is detached so the daemon's own positive-pid
|
|
267
|
+
* `killTree` reaches the daemon alone. This reaper used to read "the recorded
|
|
268
|
+
* `daemonPid` has exited" as "everything it recorded is garbage" and SIGTERM the
|
|
269
|
+
* browser — so replacing the daemon (a menubar activation, an upgrade, a crash
|
|
270
|
+
* restart) killed a browser the user was looking at, and `clearProfileRuntime`
|
|
271
|
+
* then removed the `port`/meta that {@link readProfileRuntime} needs, so even a
|
|
272
|
+
* survivor could not be re-attached and a second browser would launch beside it.
|
|
273
|
+
*
|
|
274
|
+
* The three cases are now decided independently:
|
|
275
|
+
*
|
|
276
|
+
* - **A live local browser/electron is preserved untouched** — not signalled, not
|
|
277
|
+
* cleared, and its `daemonPid` deliberately NOT rewritten. Ownership is adopted
|
|
278
|
+
* only after an identity-checked attach succeeds (`adoptProfileRuntimeOwner`,
|
|
279
|
+
* called from the reuse branch in `BrowserService.connectProfile`), so nothing
|
|
280
|
+
* claims a browser it has not proven it can talk to. Re-running this is a no-op.
|
|
281
|
+
* - **A dead browser record is cleared**, with no signal sent: there is nothing
|
|
282
|
+
* alive to signal, and the stale pid/port would otherwise mislead the next
|
|
283
|
+
* attach.
|
|
284
|
+
* - **A stale tunnel IS reaped.** A local `ssh -L` cannot outlive its purpose, so
|
|
285
|
+
* a dead owner does make it an orphan. This is the one thing the old code was
|
|
286
|
+
* right about; it just shared a code path with the browser.
|
|
287
|
+
*
|
|
288
|
+
* The accepted trade: a genuinely leaked browser from a long-dead daemon is no
|
|
289
|
+
* longer force-killed here. Idle/abandoned cleanup has its own owner in
|
|
290
|
+
* `browser/hygiene.ts` (session-dead + idle), which the daemon's periodic tick and
|
|
291
|
+
* `agents browser prune` both call — that is the right place for it, because it can
|
|
292
|
+
* tell "idle and abandoned" from "in use right now" and this function cannot.
|
|
293
|
+
*/
|
|
261
294
|
export function reapOrphanedProcesses() {
|
|
262
295
|
const root = getBrowserRuntimeDir();
|
|
263
296
|
if (!fs.existsSync(root))
|
|
@@ -275,28 +308,76 @@ export function reapOrphanedProcesses() {
|
|
|
275
308
|
// Owning daemon still alive — leave its kids alone.
|
|
276
309
|
if (isProcessAlive(meta.daemonPid))
|
|
277
310
|
continue;
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
311
|
+
// A local `ssh -L` whose owner is gone is genuinely orphaned: nothing is
|
|
312
|
+
// driving it and it cannot be re-adopted, so it is the one process to signal.
|
|
313
|
+
// Matched against the recorded command (or `ssh`) so a recycled pid is not
|
|
314
|
+
// mistaken for our tunnel.
|
|
315
|
+
if (meta.tunnelPid && meta.tunnelPid !== 0) {
|
|
316
|
+
const cmdOk = !meta.command
|
|
317
|
+
|| matchesCommand(meta.tunnelPid, meta.command)
|
|
318
|
+
|| matchesCommand(meta.tunnelPid, 'ssh');
|
|
319
|
+
if (cmdOk) {
|
|
320
|
+
try {
|
|
321
|
+
process.kill(meta.tunnelPid, 'SIGTERM');
|
|
322
|
+
reaped++;
|
|
323
|
+
details.push(`reaped tunnel ${meta.tunnelPid} (profile ${profileName})`);
|
|
324
|
+
}
|
|
325
|
+
catch { /* already gone */ }
|
|
291
326
|
}
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
327
|
+
}
|
|
328
|
+
// A tunnel-kind record has no local browser of its own, so clearing it is the
|
|
329
|
+
// whole cleanup. `pid` on such a record is the REMOTE browser (normally 0) and
|
|
330
|
+
// is never ours to signal.
|
|
331
|
+
if (meta.kind === 'tunnel') {
|
|
332
|
+
clearProfileRuntime(profileName);
|
|
333
|
+
continue;
|
|
334
|
+
}
|
|
335
|
+
// `isProcessAlive` re-checks the recorded command, so a recycled pid does not
|
|
336
|
+
// read as our browser. A record with no pid at all is stale by definition.
|
|
337
|
+
const liveBrowser = !!meta.pid && meta.pid !== 0 && isProcessAlive(meta.pid, meta.command);
|
|
338
|
+
if (liveBrowser) {
|
|
339
|
+
details.push(`preserved live browser ${meta.pid} (profile ${profileName})`);
|
|
340
|
+
continue;
|
|
341
|
+
}
|
|
342
|
+
// Dead browser: clear the stale record. No signal — there is nothing there.
|
|
296
343
|
clearProfileRuntime(profileName);
|
|
344
|
+
if (meta.pid)
|
|
345
|
+
details.push(`cleared dead browser record ${meta.pid} (profile ${profileName})`);
|
|
297
346
|
}
|
|
298
347
|
return { reaped, details };
|
|
299
348
|
}
|
|
349
|
+
/**
|
|
350
|
+
* Take ownership of a runtime record after an identity-checked attach succeeded.
|
|
351
|
+
*
|
|
352
|
+
* Called from the reuse branch in `BrowserService.connectProfile`, which has just
|
|
353
|
+
* probed `/json/version` and run `verifyBrowserIdentity` against the profile's
|
|
354
|
+
* expected browser family — so by this point the process on the recorded port is
|
|
355
|
+
* proven to be ours. Only then may a new service claim it.
|
|
356
|
+
*
|
|
357
|
+
* Rewrites `daemonPid` ALONE. Every other field — `pid`, `port`, `command`,
|
|
358
|
+
* `userDataDir`, `spawnedAt`, `kind`, `tunnelPid` — is the original launch's
|
|
359
|
+
* metadata and stays exactly as recorded: this is an ownership handover, not a
|
|
360
|
+
* re-registration, and rewriting the rest would lose the provenance the reaper and
|
|
361
|
+
* the hygiene pass both read. A record that has since disappeared is left alone
|
|
362
|
+
* rather than recreated from a partial view.
|
|
363
|
+
*/
|
|
364
|
+
export function adoptProfileRuntimeOwner(profileName, daemonPid = process.pid) {
|
|
365
|
+
const meta = readProfileRuntimeMeta(profileName);
|
|
366
|
+
if (!meta)
|
|
367
|
+
return false;
|
|
368
|
+
if (meta.daemonPid === daemonPid)
|
|
369
|
+
return true;
|
|
370
|
+
const dir = getProfileRuntimeDir(profileName);
|
|
371
|
+
try {
|
|
372
|
+
fs.writeFileSync(path.join(dir, META_FILE), JSON.stringify({ ...meta, daemonPid }));
|
|
373
|
+
return true;
|
|
374
|
+
}
|
|
375
|
+
catch {
|
|
376
|
+
// Losing the handover is not worth failing an otherwise good attach; the next
|
|
377
|
+
// startup simply preserves the browser again and re-attaches.
|
|
378
|
+
return false;
|
|
379
|
+
}
|
|
380
|
+
}
|
|
300
381
|
/**
|
|
301
382
|
* True when ANY cache dir belonging to this profile still has a live browser or
|
|
302
383
|
* SSH tunnel, or an open task. The composite naming (`<name>@<endpoint>`) means
|
|
@@ -478,7 +559,7 @@ function matchesCommand(pid, expectedCommand) {
|
|
|
478
559
|
* determined. The process-listing API differs per OS: Windows has no `ps`, so
|
|
479
560
|
* we query `tasklist` (CSV image name in column 1); POSIX uses `ps -o comm=`.
|
|
480
561
|
*/
|
|
481
|
-
function liveProcessCommand(pid) {
|
|
562
|
+
export function liveProcessCommand(pid) {
|
|
482
563
|
try {
|
|
483
564
|
if (process.platform === 'win32') {
|
|
484
565
|
const out = execFileSync('tasklist', ['/FI', `PID eq ${pid}`, '/NH', '/FO', 'CSV'], {
|
|
@@ -8,7 +8,7 @@ import { assertRemoteControlAllowedForRequest } from './remote-control.js';
|
|
|
8
8
|
import { connectLocal } from './drivers/local.js';
|
|
9
9
|
import { connectSSH, shellQuote } from './drivers/ssh.js';
|
|
10
10
|
import { adoptLegacyRuntimeIfLocal, isLegacyEndpointKey, resolveBrowserTarget, shouldForkProfile, } from './resolve-target.js';
|
|
11
|
-
import { clearProfileRuntime, listProfileCacheDirs, readProfileRuntimeMeta, isProcessAlive } from './runtime-state.js';
|
|
11
|
+
import { adoptProfileRuntimeOwner, clearProfileRuntime, listProfileCacheDirs, readProfileRuntimeMeta, isProcessAlive } from './runtime-state.js';
|
|
12
12
|
import { resolveDomainSkill } from './domain-skills.js';
|
|
13
13
|
import { generateTaskId, generateShortId, asConnectionKey, parseConnectionKey, keyBelongsToProfile, } from './types.js';
|
|
14
14
|
import { reapAbandonedTasks, resolveLiveIdentities, taskOwnerIsGone, } from './hygiene.js';
|
|
@@ -1212,6 +1212,8 @@ export class BrowserService {
|
|
|
1212
1212
|
await this.saveTaskState(task.profile, conn.tasks);
|
|
1213
1213
|
return { tabId: resolvedTabId };
|
|
1214
1214
|
}
|
|
1215
|
+
requireCdp(conn, 'enumerate');
|
|
1216
|
+
await conn.cdp.send('Target.activateTarget', { targetId: task.tabs[resolvedTabId] });
|
|
1215
1217
|
task.currentTabId = resolvedTabId;
|
|
1216
1218
|
await this.saveTaskState(task.profile, conn.tasks);
|
|
1217
1219
|
return { tabId: resolvedTabId };
|
|
@@ -2771,6 +2773,12 @@ export class BrowserService {
|
|
|
2771
2773
|
await cdp.connect(wsUrl);
|
|
2772
2774
|
await this.enableDomains(cdp);
|
|
2773
2775
|
const tasks = this.loadTaskState(key);
|
|
2776
|
+
// The attach is now PROVEN: the stored port answered /json/version and
|
|
2777
|
+
// `verifyBrowserIdentity` matched the profile's browser family. Only here
|
|
2778
|
+
// may this service claim a browser the startup reaper deliberately left
|
|
2779
|
+
// alone — it rewrites `daemonPid` only, keeping the original launch's
|
|
2780
|
+
// metadata. An ambiguous identity throws above and adopts nothing.
|
|
2781
|
+
adoptProfileRuntimeOwner(key);
|
|
2774
2782
|
return {
|
|
2775
2783
|
backend: 'cdp',
|
|
2776
2784
|
cdp,
|
|
@@ -3398,6 +3406,10 @@ export class BrowserService {
|
|
|
3398
3406
|
const port = parseEndpointUrl(resolved.target)?.port ?? 9600;
|
|
3399
3407
|
try {
|
|
3400
3408
|
const ff = await connectFirefox(profile, key, port, { profileDir: profile.userDataDir });
|
|
3409
|
+
// A served port that accepted a fresh BiDi session is this profile's own
|
|
3410
|
+
// Firefox, so claim the record for this daemon (see the local-CDP claim
|
|
3411
|
+
// below for why adoption waits for a proven attach).
|
|
3412
|
+
adoptProfileRuntimeOwner(key);
|
|
3401
3413
|
const tasks = this.loadTaskState(key);
|
|
3402
3414
|
for (const [k, t] of diskTasks) {
|
|
3403
3415
|
if (!tasks.has(k))
|
|
@@ -3476,6 +3488,14 @@ export class BrowserService {
|
|
|
3476
3488
|
const cdp = new CDPClient();
|
|
3477
3489
|
await cdp.connect(wsUrl);
|
|
3478
3490
|
await this.enableDomains(cdp);
|
|
3491
|
+
// This is the path a replacement daemon takes to recover a browser its
|
|
3492
|
+
// predecessor launched, so it is where ownership actually transfers. Only
|
|
3493
|
+
// now — after `verifyBrowserIdentity` confirmed the browser family on the
|
|
3494
|
+
// recorded port and CDP is attached — is it proven that this record
|
|
3495
|
+
// describes a browser we are driving. `adoptProfileRuntimeOwner` rewrites
|
|
3496
|
+
// `daemonPid` alone; an ambiguous identity throws above and adopts
|
|
3497
|
+
// nothing, leaving the record (and the browser) untouched.
|
|
3498
|
+
adoptProfileRuntimeOwner(key);
|
|
3479
3499
|
const tasks = this.loadTaskState(key);
|
|
3480
3500
|
for (const [k, t] of diskTasks) {
|
|
3481
3501
|
if (!tasks.has(k))
|
|
@@ -23,6 +23,7 @@ import * as yaml from 'yaml';
|
|
|
23
23
|
import { listResources, resolveResource } from './resources.js';
|
|
24
24
|
import { probeCapture } from './probe.js';
|
|
25
25
|
import { composeWin32CommandLine } from './platform/index.js';
|
|
26
|
+
import { execFileShellSpec } from './platform/exec.js';
|
|
26
27
|
import { localBinDir } from './platform/posixpath.js';
|
|
27
28
|
// ─── Validation primitives ───────────────────────────────────────────────────
|
|
28
29
|
/** Token allowed inside `check:` strings — letters, digits, underscore, dot, slash, dash. */
|
|
@@ -457,7 +458,8 @@ export function buildInstallCommand(method) {
|
|
|
457
458
|
function runInstallMethod(method) {
|
|
458
459
|
if ('npm' in method) {
|
|
459
460
|
assertNpmPackage(method.npm);
|
|
460
|
-
const
|
|
461
|
+
const invocation = execFileShellSpec('npm', ['install', '-g', method.npm]);
|
|
462
|
+
const r = spawnSync(invocation.command, invocation.args, { stdio: 'inherit', shell: invocation.shell });
|
|
461
463
|
if (r.status !== 0) {
|
|
462
464
|
throw new Error(`npm install -g ${method.npm} exited with status ${r.status ?? 'unknown'}`);
|
|
463
465
|
}
|
|
@@ -16,7 +16,7 @@ import { insertTask, updateTaskStatus } from './store.js';
|
|
|
16
16
|
import { renderStream } from './stream.js';
|
|
17
17
|
import { MissingTargetError, MAX_IMAGES_PER_DISPATCH } from './types.js';
|
|
18
18
|
import { emit } from '../feed/events.js';
|
|
19
|
-
import { shareRuntimeEnv } from '../share
|
|
19
|
+
import { shareRuntimeEnv } from '../share-runtime.js';
|
|
20
20
|
/** Map a supported image file extension to its wire mimeType. Rejects anything else. */
|
|
21
21
|
function imageMimeFromPath(file) {
|
|
22
22
|
const ext = path.extname(file).toLowerCase();
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
export declare const DEFAULT_CF_BUNDLE = "cloudflare";
|
|
2
|
+
/** Cloudflare API credentials for provisioning, read from `cloudflare` (or a
|
|
3
|
+
* user-named bundle). Fuzzy-matches key names so it works across bundle layouts. */
|
|
4
|
+
export declare function readCloudflareCreds(bundle?: string, override?: {
|
|
5
|
+
apiToken?: string;
|
|
6
|
+
accountId?: string;
|
|
7
|
+
}): {
|
|
8
|
+
apiToken: string;
|
|
9
|
+
accountId: string;
|
|
10
|
+
};
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
// Cloudflare API credential glue, shared by the commands that provision or read
|
|
2
|
+
// Cloudflare-backed endpoints (`agents traces`, `agents sessions backup`).
|
|
3
|
+
//
|
|
4
|
+
// This used to live in `lib/share/config.ts` alongside the embedded artifact
|
|
5
|
+
// share engine. The share engine moved out to `@phnx-labs/artifacts-cli`
|
|
6
|
+
// (PHNX-3992); `readCloudflareCreds` stayed because it is a generic Cloudflare
|
|
7
|
+
// util that `traces`/`sessions backup` borrow, unrelated to sharing.
|
|
8
|
+
import { bundleExistsSync as bundleExists, readAndResolveBundleEnvSync as readAndResolveBundleEnv, } from '../secrets-client.js';
|
|
9
|
+
export const DEFAULT_CF_BUNDLE = 'cloudflare';
|
|
10
|
+
/** Cloudflare API credentials for provisioning, read from `cloudflare` (or a
|
|
11
|
+
* user-named bundle). Fuzzy-matches key names so it works across bundle layouts. */
|
|
12
|
+
export function readCloudflareCreds(bundle = DEFAULT_CF_BUNDLE, override) {
|
|
13
|
+
// Explicit --token/--account bypass the bundle entirely (robust escape hatch).
|
|
14
|
+
if (override?.apiToken) {
|
|
15
|
+
return { apiToken: override.apiToken, accountId: override.accountId ?? '' };
|
|
16
|
+
}
|
|
17
|
+
// Check existence first: resolving a missing bundle through the process client
|
|
18
|
+
// surfaces an opaque transport code (e.g. LOCKED), never naming the bundle the
|
|
19
|
+
// user typed — so name it here.
|
|
20
|
+
if (!bundleExists(bundle)) {
|
|
21
|
+
throw new Error(`The '${bundle}' bundle does not exist. ` +
|
|
22
|
+
`Pass credentials directly with --token <t> [--account <id>], or create it: ` +
|
|
23
|
+
`agents secrets add ${bundle} CLOUDFLARE_API_TOKEN`);
|
|
24
|
+
}
|
|
25
|
+
const { env } = readAndResolveBundleEnv(bundle, {
|
|
26
|
+
caller: 'cloudflare',
|
|
27
|
+
// Setup is still a read; only `agents secrets unlock` may authenticate.
|
|
28
|
+
agentOnly: true,
|
|
29
|
+
});
|
|
30
|
+
const find = (re) => {
|
|
31
|
+
for (const [k, v] of Object.entries(env))
|
|
32
|
+
if (re.test(k) && v)
|
|
33
|
+
return v;
|
|
34
|
+
return '';
|
|
35
|
+
};
|
|
36
|
+
const apiToken = env.CLOUDFLARE_API_TOKEN || env.CF_API_TOKEN || find(/API[_-]?TOKEN|(?:^|_)TOKEN$/i);
|
|
37
|
+
const accountId = override?.accountId || env.CLOUDFLARE_ACCOUNT_ID || env.CF_ACCOUNT_ID || find(/ACCOUNT[_-]?ID/i);
|
|
38
|
+
if (!apiToken) {
|
|
39
|
+
const keys = Object.keys(env);
|
|
40
|
+
throw new Error(`No Cloudflare API token in the '${bundle}' bundle ` +
|
|
41
|
+
`(keys present: ${keys.length ? keys.join(', ') : 'none'}). ` +
|
|
42
|
+
`Pass it directly with --token <t> [--account <id>], or add it: ` +
|
|
43
|
+
`agents secrets add ${bundle} CLOUDFLARE_API_TOKEN`);
|
|
44
|
+
}
|
|
45
|
+
return { apiToken, accountId };
|
|
46
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
/** One compiled non-JS Worker module (e.g. WASM) uploaded alongside the script. */
|
|
2
|
+
export interface WorkerModule {
|
|
3
|
+
name: string;
|
|
4
|
+
contentType: string;
|
|
5
|
+
contents: Uint8Array<ArrayBuffer>;
|
|
6
|
+
}
|
|
7
|
+
/** A Worker script plus any extra modules to upload with it. */
|
|
8
|
+
export interface WorkerBundle {
|
|
9
|
+
script: string;
|
|
10
|
+
modules: WorkerModule[];
|
|
11
|
+
}
|
|
12
|
+
export interface CloudflareRequest {
|
|
13
|
+
apiToken: string;
|
|
14
|
+
method: string;
|
|
15
|
+
pathname: string;
|
|
16
|
+
body?: unknown;
|
|
17
|
+
form?: FormData;
|
|
18
|
+
}
|
|
19
|
+
export type CloudflareRequester = <T = unknown>(request: CloudflareRequest) => Promise<T>;
|
|
20
|
+
interface ProvisionOptions {
|
|
21
|
+
request?: CloudflareRequester;
|
|
22
|
+
}
|
|
23
|
+
/** Create the R2 bucket (idempotent). */
|
|
24
|
+
export declare function createBucket(apiToken: string, accountId: string, name: string, opts?: ProvisionOptions): Promise<void>;
|
|
25
|
+
/** Upload the module Worker with an R2 binding (`BUCKET`). Secrets are set via the Workers Secrets API. */
|
|
26
|
+
export declare function deployWorker(apiToken: string, accountId: string, workerName: string, worker: string | WorkerBundle, bucketName: string, opts?: ProvisionOptions): Promise<void>;
|
|
27
|
+
/** Add/update a secret_text binding using Cloudflare's Workers Secrets API. */
|
|
28
|
+
export declare function putWorkerSecret(apiToken: string, accountId: string, workerName: string, name: string, text: string, opts?: ProvisionOptions): Promise<void>;
|
|
29
|
+
/** Enable the free `*.workers.dev` route for the script, and return the account subdomain. */
|
|
30
|
+
export declare function enableWorkersDev(apiToken: string, accountId: string, workerName: string, opts?: ProvisionOptions): Promise<string>;
|
|
31
|
+
/** Resolve a zone id for a domain the token can see, or null if not owned/visible. */
|
|
32
|
+
export declare function findZoneId(apiToken: string, domain: string, opts?: ProvisionOptions): Promise<string | null>;
|
|
33
|
+
/** Map a custom hostname (e.g. `share.getrush.ai`) to the Worker via Workers Custom Domains. */
|
|
34
|
+
export declare function addCustomDomain(apiToken: string, accountId: string, workerName: string, zoneId: string, hostname: string, opts?: ProvisionOptions): Promise<void>;
|
|
35
|
+
export {};
|
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
// Generic Cloudflare orchestration — plain `fetch` against the CF REST API (the
|
|
2
|
+
// repo has no CF wrapper). Create an R2 bucket, upload a module Worker with an R2
|
|
3
|
+
// binding, set secrets via the Workers Secrets API, enable the free
|
|
4
|
+
// `*.workers.dev` subdomain, and map a custom domain when the token owns the zone.
|
|
5
|
+
//
|
|
6
|
+
// This was `lib/share/provision.ts`. The artifact share engine moved out to
|
|
7
|
+
// `@phnx-labs/artifacts-cli` (PHNX-3992), but `agents traces` provisions its own
|
|
8
|
+
// isolated Cloudflare-backed store with the SAME primitives — so the generic
|
|
9
|
+
// layer relocated here (share-specific lifecycle/collab/managed helpers left
|
|
10
|
+
// with the share engine). `traces/provision.ts` is now the only consumer.
|
|
11
|
+
const CF_API = 'https://api.cloudflare.com/client/v4';
|
|
12
|
+
async function cf(apiToken, method, pathname, body, form) {
|
|
13
|
+
const headers = { authorization: `Bearer ${apiToken}` };
|
|
14
|
+
let payload;
|
|
15
|
+
if (form) {
|
|
16
|
+
payload = form; // fetch sets multipart boundary
|
|
17
|
+
}
|
|
18
|
+
else if (body !== undefined) {
|
|
19
|
+
headers['content-type'] = 'application/json';
|
|
20
|
+
payload = JSON.stringify(body);
|
|
21
|
+
}
|
|
22
|
+
const res = await fetch(`${CF_API}${pathname}`, { method, headers, body: payload });
|
|
23
|
+
const json = (await res.json().catch(() => ({})));
|
|
24
|
+
if (!res.ok || json.success === false) {
|
|
25
|
+
const msg = (json.errors ?? []).map((e) => `${e.code ?? ''} ${e.message ?? ''}`.trim()).join('; ') ||
|
|
26
|
+
res.statusText;
|
|
27
|
+
throw new Error(`Cloudflare ${method} ${pathname} failed (${res.status}): ${msg}`);
|
|
28
|
+
}
|
|
29
|
+
return json.result;
|
|
30
|
+
}
|
|
31
|
+
const defaultCloudflareRequester = (request) => cf(request.apiToken, request.method, request.pathname, request.body, request.form);
|
|
32
|
+
/** True if the CF error looks like "the thing already exists" (idempotent create). */
|
|
33
|
+
function isAlreadyExists(e) {
|
|
34
|
+
return /already exists|duplicate|10004|10014/i.test(String(e));
|
|
35
|
+
}
|
|
36
|
+
/** Create the R2 bucket (idempotent). */
|
|
37
|
+
export async function createBucket(apiToken, accountId, name, opts = {}) {
|
|
38
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
39
|
+
try {
|
|
40
|
+
await request({
|
|
41
|
+
apiToken,
|
|
42
|
+
method: 'POST',
|
|
43
|
+
pathname: `/accounts/${accountId}/r2/buckets`,
|
|
44
|
+
body: { name },
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
catch (e) {
|
|
48
|
+
if (!isAlreadyExists(e))
|
|
49
|
+
throw e;
|
|
50
|
+
}
|
|
51
|
+
}
|
|
52
|
+
/** Upload the module Worker with an R2 binding (`BUCKET`). Secrets are set via the Workers Secrets API. */
|
|
53
|
+
export async function deployWorker(apiToken, accountId, workerName, worker, bucketName, opts = {}) {
|
|
54
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
55
|
+
const metadata = {
|
|
56
|
+
main_module: 'worker.js',
|
|
57
|
+
compatibility_date: '2024-11-06',
|
|
58
|
+
bindings: [
|
|
59
|
+
{ type: 'r2_bucket', name: 'BUCKET', bucket_name: bucketName },
|
|
60
|
+
],
|
|
61
|
+
};
|
|
62
|
+
const form = new FormData();
|
|
63
|
+
form.set('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
64
|
+
const bundle = typeof worker === 'string' ? { script: worker, modules: [] } : worker;
|
|
65
|
+
form.set('worker.js', new Blob([bundle.script], { type: 'application/javascript+module' }), 'worker.js');
|
|
66
|
+
for (const module of bundle.modules) {
|
|
67
|
+
form.set(module.name, new Blob([module.contents], { type: module.contentType }), module.name);
|
|
68
|
+
}
|
|
69
|
+
await request({
|
|
70
|
+
apiToken,
|
|
71
|
+
method: 'PUT',
|
|
72
|
+
pathname: `/accounts/${accountId}/workers/scripts/${workerName}`,
|
|
73
|
+
form,
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
/** Add/update a secret_text binding using Cloudflare's Workers Secrets API. */
|
|
77
|
+
export async function putWorkerSecret(apiToken, accountId, workerName, name, text, opts = {}) {
|
|
78
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
79
|
+
await request({
|
|
80
|
+
apiToken,
|
|
81
|
+
method: 'PUT',
|
|
82
|
+
pathname: `/accounts/${accountId}/workers/scripts/${workerName}/secrets`,
|
|
83
|
+
body: { name, text, type: 'secret_text' },
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
/** Enable the free `*.workers.dev` route for the script, and return the account subdomain. */
|
|
87
|
+
export async function enableWorkersDev(apiToken, accountId, workerName, opts = {}) {
|
|
88
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
89
|
+
await request({
|
|
90
|
+
apiToken,
|
|
91
|
+
method: 'POST',
|
|
92
|
+
pathname: `/accounts/${accountId}/workers/scripts/${workerName}/subdomain`,
|
|
93
|
+
body: {
|
|
94
|
+
enabled: true,
|
|
95
|
+
previews_enabled: false,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
const sub = await request({
|
|
99
|
+
apiToken,
|
|
100
|
+
method: 'GET',
|
|
101
|
+
pathname: `/accounts/${accountId}/workers/subdomain`,
|
|
102
|
+
});
|
|
103
|
+
if (!sub?.subdomain) {
|
|
104
|
+
throw new Error('No workers.dev subdomain on this account yet — register one at dash.cloudflare.com → Workers → Subdomain, then re-run.');
|
|
105
|
+
}
|
|
106
|
+
return sub.subdomain;
|
|
107
|
+
}
|
|
108
|
+
/** Resolve a zone id for a domain the token can see, or null if not owned/visible. */
|
|
109
|
+
export async function findZoneId(apiToken, domain, opts = {}) {
|
|
110
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
111
|
+
// Try the exact name, then the registrable parent (share.getrush.ai -> getrush.ai).
|
|
112
|
+
const candidates = [domain, domain.split('.').slice(-2).join('.')];
|
|
113
|
+
for (const name of candidates) {
|
|
114
|
+
const zones = await request({
|
|
115
|
+
apiToken,
|
|
116
|
+
method: 'GET',
|
|
117
|
+
pathname: `/zones?name=${encodeURIComponent(name)}`,
|
|
118
|
+
}).catch(() => []);
|
|
119
|
+
if (zones?.length)
|
|
120
|
+
return zones[0].id;
|
|
121
|
+
}
|
|
122
|
+
return null;
|
|
123
|
+
}
|
|
124
|
+
/** Map a custom hostname (e.g. `share.getrush.ai`) to the Worker via Workers Custom Domains. */
|
|
125
|
+
export async function addCustomDomain(apiToken, accountId, workerName, zoneId, hostname, opts = {}) {
|
|
126
|
+
const request = opts.request ?? defaultCloudflareRequester;
|
|
127
|
+
try {
|
|
128
|
+
await request({
|
|
129
|
+
apiToken,
|
|
130
|
+
method: 'PUT',
|
|
131
|
+
pathname: `/accounts/${accountId}/workers/domains`,
|
|
132
|
+
body: {
|
|
133
|
+
zone_id: zoneId,
|
|
134
|
+
hostname,
|
|
135
|
+
service: workerName,
|
|
136
|
+
environment: 'production',
|
|
137
|
+
},
|
|
138
|
+
});
|
|
139
|
+
}
|
|
140
|
+
catch (e) {
|
|
141
|
+
if (!isAlreadyExists(e))
|
|
142
|
+
throw e;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
@@ -26,7 +26,61 @@ export interface ComputerAction {
|
|
|
26
26
|
agent?: string;
|
|
27
27
|
machineId?: string;
|
|
28
28
|
hostname?: string;
|
|
29
|
+
/**
|
|
30
|
+
* The file a successful `screenshot` action actually wrote.
|
|
31
|
+
*
|
|
32
|
+
* Recorded by the standalone engine only AFTER the write succeeded, so its
|
|
33
|
+
* presence is proof the file existed — which is why nothing here ever derives
|
|
34
|
+
* a path from the verb. An action from before the producer carried this field
|
|
35
|
+
* has no capture, and that is reported honestly as none rather than guessed at
|
|
36
|
+
* from an output flag that may never have been written.
|
|
37
|
+
*/
|
|
38
|
+
capture?: {
|
|
39
|
+
path: string;
|
|
40
|
+
kind: 'screenshot';
|
|
41
|
+
name: string;
|
|
42
|
+
bytes?: number;
|
|
43
|
+
};
|
|
29
44
|
}
|
|
45
|
+
/**
|
|
46
|
+
* The standalone engine's OWN action ledger.
|
|
47
|
+
*
|
|
48
|
+
* `agents computer` is a thin consumer of the standalone `computer` engine
|
|
49
|
+
* (PHNX-4075), and that engine ALWAYS appends every action it performs to
|
|
50
|
+
* `<cache>/computer/actions/<day>.jsonl` — independently of whether agents-cli
|
|
51
|
+
* was in the call at all. So a `computer` command the operator ran directly
|
|
52
|
+
* appears ONLY here: it never reached `recordComputerAction`, so it is in
|
|
53
|
+
* neither the feed event ledger nor `computer_sessions`. Reading only those two
|
|
54
|
+
* meant the actions an operator actually performed were invisible to every
|
|
55
|
+
* agents-cli surface.
|
|
56
|
+
*/
|
|
57
|
+
export declare function standaloneComputerActionsDir(): string;
|
|
58
|
+
/**
|
|
59
|
+
* Read the standalone ledger, newest day first, bounded by `limit` actions.
|
|
60
|
+
*
|
|
61
|
+
* Bounded by construction: days are read newest-first and reading stops as soon
|
|
62
|
+
* as the budget is met, so a box with months of history costs the same as one
|
|
63
|
+
* with a day of it.
|
|
64
|
+
*/
|
|
65
|
+
export declare function listStandaloneComputerActions(opts?: {
|
|
66
|
+
limit?: number;
|
|
67
|
+
dir?: string;
|
|
68
|
+
observer?: string;
|
|
69
|
+
}): ComputerAction[];
|
|
70
|
+
/**
|
|
71
|
+
* Union the two ledgers, preferring the standalone record for any run present in
|
|
72
|
+
* both.
|
|
73
|
+
*
|
|
74
|
+
* Dedupe keys on `invocationId`, NOT on a timestamp or pid. When a command is
|
|
75
|
+
* forwarded through `agents computer`, BOTH stores receive it — and the
|
|
76
|
+
* forwarding rewrites `ts` and `pid` on the way through, so the two copies of one
|
|
77
|
+
* action do not agree on either. `invocationId` is minted by the engine and
|
|
78
|
+
* echoed unchanged, which makes it the only field that identifies the same run in
|
|
79
|
+
* both files. A legacy feed record with no invocationId cannot be matched to
|
|
80
|
+
* anything, so it is kept: dropping it would lose history the standalone ledger
|
|
81
|
+
* never had.
|
|
82
|
+
*/
|
|
83
|
+
export declare function mergeComputerActionSources(standalone: ComputerAction[], legacy: ComputerAction[]): ComputerAction[];
|
|
30
84
|
/** Read `computer.action` events straight from the durable event ledger,
|
|
31
85
|
* newest first, bounded by `limit`. Malformed/legacy records (missing
|
|
32
86
|
* `command` or `pid`, or an unparseable `ts`) are skipped, never thrown —
|
|
@@ -94,6 +148,7 @@ export declare function groupIntoComputerRuns(actions: ComputerAction[], resolve
|
|
|
94
148
|
export declare function buildComputerSessionRows(opts?: {
|
|
95
149
|
limit?: number;
|
|
96
150
|
machine?: string;
|
|
151
|
+
observer?: string;
|
|
97
152
|
}): ComputerRunRow[];
|
|
98
153
|
/**
|
|
99
154
|
* Search predicate for the interactive picker: task text, machine/remote
|