@phnx-labs/agents-cli 1.22.37 → 1.22.38
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 +42 -0
- package/README.md +8 -8
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +1 -0
- package/dist/commands/artifacts-setup.d.ts +53 -0
- package/dist/commands/{setup-share.js → artifacts-setup.js} +59 -13
- package/dist/commands/artifacts.d.ts +18 -0
- package/dist/commands/artifacts.js +58 -0
- package/dist/commands/browser.js +2 -0
- package/dist/commands/exec.js +1 -1
- package/dist/commands/models.js +67 -0
- package/dist/commands/setup.js +5 -5
- package/dist/commands/share.d.ts +20 -7
- package/dist/commands/share.js +74 -75
- package/dist/lib/browser/hygiene.d.ts +90 -0
- package/dist/lib/browser/hygiene.js +146 -0
- package/dist/lib/browser/ipc.js +12 -0
- package/dist/lib/browser/service.d.ts +75 -1
- package/dist/lib/browser/service.js +201 -11
- package/dist/lib/browser/types.d.ts +44 -1
- package/dist/lib/git.d.ts +1 -1
- package/dist/lib/git.js +1 -1
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/routines.d.ts +3 -1
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/share/analytics.js +1 -1
- package/dist/lib/share/capture.d.ts +1 -1
- package/dist/lib/share/capture.js +3 -3
- package/dist/lib/share/config.d.ts +2 -2
- package/dist/lib/share/config.js +5 -5
- package/dist/lib/share/delete.js +3 -3
- package/dist/lib/share/provision.js +3 -3
- package/dist/lib/share/publish.d.ts +1 -1
- package/dist/lib/share/publish.js +3 -3
- package/dist/lib/share/worker-template.d.ts +12 -1
- package/dist/lib/share/worker-template.js +13 -2
- package/dist/lib/startup/command-registry.d.ts +10 -2
- package/dist/lib/startup/command-registry.js +16 -7
- package/dist/lib/tmux/orphan-reap.d.ts +15 -19
- package/dist/lib/tmux/orphan-reap.js +15 -21
- package/dist/lib/tmux/session.js +4 -3
- package/dist/lib/triggers/handlers.js +10 -0
- package/dist/lib/triggers/webhook.js +10 -0
- package/dist/lib/types.d.ts +2 -2
- package/package.json +1 -1
- package/dist/commands/set.d.ts +0 -15
- package/dist/commands/set.js +0 -79
- package/dist/commands/setup-share.d.ts +0 -17
package/dist/commands/share.js
CHANGED
|
@@ -1,5 +1,9 @@
|
|
|
1
|
-
// `agents share` — publish an HTML file to your own Cloudflare R2
|
|
2
|
-
// Worker, and get a shareable link (~$0). See apps/cli/docs/share.md.
|
|
1
|
+
// `agents artifacts share` — publish an HTML file to your own Cloudflare R2
|
|
2
|
+
// behind a tiny Worker, and get a shareable link (~$0). See apps/cli/docs/share.md.
|
|
3
|
+
//
|
|
4
|
+
// Registered under the `artifacts` group by commands/artifacts.ts; the
|
|
5
|
+
// provisioning door lives beside it at `agents artifacts setup`
|
|
6
|
+
// (commands/artifacts-setup.ts), which calls `runShareProvision` below.
|
|
3
7
|
import { existsSync } from 'node:fs';
|
|
4
8
|
import chalk from 'chalk';
|
|
5
9
|
import { DEFAULT_BUCKET_NAME, DEFAULT_CF_BUNDLE, DEFAULT_SHARE_DOMAIN, DEFAULT_WORKER_NAME, generateWriteToken, readCloudflareCreds, readShareConfig, readWriteToken, readWriteTokenEnv, readWriteTokenFromBundle, storeWriteToken, writeShareConfig, } from '../lib/share/config.js';
|
|
@@ -35,10 +39,10 @@ export function shareTemplateStatus(cfg) {
|
|
|
35
39
|
}
|
|
36
40
|
/** Shown whenever the deployed Worker has no `?format=json` listing route — an
|
|
37
41
|
* endpoint provisioned before this feature. Points at the RUSH-2449 update path
|
|
38
|
-
* (`agents share update`) instead of letting the caller hit a 404 or an
|
|
39
|
-
* and get a confusing parse error. */
|
|
40
|
-
const OUTDATED_TEMPLATE_HINT = 'Your deployed share Worker has no machine-readable listing route — it predates `agents share list`. ' +
|
|
41
|
-
'Run `agents share update` to deploy the current Worker template, then retry (`agents share status` shows whether an update is due).';
|
|
42
|
+
* (`agents artifacts share update`) instead of letting the caller hit a 404 or an
|
|
43
|
+
* HTML body and get a confusing parse error. */
|
|
44
|
+
const OUTDATED_TEMPLATE_HINT = 'Your deployed share Worker has no machine-readable listing route — it predates `agents artifacts share list`. ' +
|
|
45
|
+
'Run `agents artifacts share update` to deploy the current Worker template, then retry (`agents artifacts share status` shows whether an update is due).';
|
|
42
46
|
async function defaultListingFetch(url) {
|
|
43
47
|
const res = await fetch(url, { headers: { accept: 'application/json' } });
|
|
44
48
|
return { status: res.status, contentType: res.headers.get('content-type') ?? '', body: await res.text() };
|
|
@@ -80,7 +84,7 @@ export function parseShareListing(user, body) {
|
|
|
80
84
|
export async function runShareList(opts = {}) {
|
|
81
85
|
const cfg = opts.config ?? readShareConfig();
|
|
82
86
|
if (!cfg) {
|
|
83
|
-
throw new Error("Not set up yet. Run 'agents
|
|
87
|
+
throw new Error("Not set up yet. Run 'agents artifacts setup' (provision your own endpoint) or 'agents artifacts share join' (use an existing one).");
|
|
84
88
|
}
|
|
85
89
|
// A known-stale template can't have the listing route — say so before any network
|
|
86
90
|
// call. 'unknown' (provisioned before templateHash tracking) is attempted, then
|
|
@@ -100,14 +104,14 @@ export async function runShareList(opts = {}) {
|
|
|
100
104
|
// endpoint predates the listing route entirely. The recorded templateHash
|
|
101
105
|
// disambiguates: a 'current' template HAS the route, so its 404 means an empty
|
|
102
106
|
// namespace ("nothing published"); otherwise the route may be absent, so point
|
|
103
|
-
// at `agents share update`.
|
|
107
|
+
// at `agents artifacts share update`.
|
|
104
108
|
if (templateStatus === 'current') {
|
|
105
109
|
return { user, count: 0, objects: [] };
|
|
106
110
|
}
|
|
107
111
|
throw new Error(OUTDATED_TEMPLATE_HINT);
|
|
108
112
|
}
|
|
109
113
|
if (res.status !== 200) {
|
|
110
|
-
throw new Error(`Listing failed (${res.status}) for ${listUrl}. Check the endpoint is reachable, or that 'agents
|
|
114
|
+
throw new Error(`Listing failed (${res.status}) for ${listUrl}. Check the endpoint is reachable, or that 'agents artifacts setup' completed.`);
|
|
111
115
|
}
|
|
112
116
|
if (!/application\/json/i.test(res.contentType)) {
|
|
113
117
|
// A 200 that isn't JSON means the old Worker ignored ?format=json and served
|
|
@@ -157,8 +161,8 @@ export function formatShareDeleteResult(result, json = false) {
|
|
|
157
161
|
}
|
|
158
162
|
return lines.join('\n');
|
|
159
163
|
}
|
|
160
|
-
/** Shared handler for `agents share delete <targets...>` and the
|
|
161
|
-
* `agents unshare <targets...>` alias. Deletes each target independently and
|
|
164
|
+
/** Shared handler for `agents artifacts share delete <targets...>` and the
|
|
165
|
+
* top-level `agents unshare <targets...>` alias. Deletes each target independently and
|
|
162
166
|
* continues past a failed one (rm-style), reporting all results and exiting
|
|
163
167
|
* non-zero if any target failed to verify as gone.
|
|
164
168
|
*
|
|
@@ -200,10 +204,10 @@ function registerShareDeleteOptions(cmd) {
|
|
|
200
204
|
}
|
|
201
205
|
const SHARE_DELETE_EXAMPLES = `
|
|
202
206
|
# Delete by full URL — also takes down the sibling OG cover
|
|
203
|
-
agents share delete https://share.agents-cli.sh/octocat/my-plan-a1b2
|
|
207
|
+
agents artifacts share delete https://share.agents-cli.sh/octocat/my-plan-a1b2
|
|
204
208
|
|
|
205
209
|
# Delete by <user>/<slug>, or a bare slug in your own namespace
|
|
206
|
-
agents share delete octocat/my-plan-a1b2
|
|
210
|
+
agents artifacts share delete octocat/my-plan-a1b2
|
|
207
211
|
agents unshare my-plan-a1b2
|
|
208
212
|
|
|
209
213
|
# Several at once
|
|
@@ -220,17 +224,23 @@ const SHARE_DELETE_NOTES = `
|
|
|
220
224
|
Worker's DELETE is idempotent and returns {"ok":true} even for a key that was
|
|
221
225
|
never there, so that response alone is never proof of a takedown.
|
|
222
226
|
|
|
223
|
-
agents share delete === agents unshare (same command, different name).
|
|
227
|
+
agents artifacts share delete === agents unshare (same command, different name).
|
|
224
228
|
`;
|
|
225
|
-
|
|
226
|
-
|
|
229
|
+
/**
|
|
230
|
+
* Register the `share` subtree under its parent group — `agents artifacts share`
|
|
231
|
+
* (see commands/artifacts.ts). The top-level `agents unshare` alias is a sibling
|
|
232
|
+
* registration on the ROOT program, so it is {@link registerUnshareCommand}, not
|
|
233
|
+
* part of this subtree.
|
|
234
|
+
*/
|
|
235
|
+
export function registerShareCommands(artifactsCmd) {
|
|
236
|
+
const shareCmd = artifactsCmd
|
|
227
237
|
.command('share')
|
|
228
238
|
.description('Publish an HTML file to your own Cloudflare R2 and get a shareable link (~$0).')
|
|
229
239
|
.argument('[file]', 'file to publish (HTML or any static asset)')
|
|
230
240
|
.option('--slug <slug>', 'custom URL slug under your namespace (default: <project>-<feature>-<hash>)')
|
|
231
241
|
.option('--github-user <user>', 'GitHub username for the share namespace (default: resolved from gh/git config)')
|
|
232
242
|
.option('--expire <spec>', "auto-expire (default 30d). e.g. 12h, 30d, 2026-08-01, or 'never'")
|
|
233
|
-
.option('--unlisted', 'hide from the public gallery and `agents share list` (direct URL still works)')
|
|
243
|
+
.option('--unlisted', 'hide from the public gallery and `agents artifacts share list` (direct URL still works)')
|
|
234
244
|
.option('--private', 'alias of --unlisted')
|
|
235
245
|
.option('--force', 'publish even when the file contains emails or credential-shaped strings')
|
|
236
246
|
.option('--no-cover', 'skip the OG preview image (HTML pages get one by default)')
|
|
@@ -266,30 +276,30 @@ export function registerShareCommands(program) {
|
|
|
266
276
|
setHelpSections(shareCmd, {
|
|
267
277
|
examples: `
|
|
268
278
|
# Publish an HTML file — auto OG cover, default 30d expiry, shareable link
|
|
269
|
-
agents share ./out/plan.html
|
|
279
|
+
agents artifacts share ./out/plan.html
|
|
270
280
|
|
|
271
281
|
# Hide from the public gallery (direct URL still works) and expire sooner
|
|
272
|
-
agents share ./out/report.html --unlisted --expire 12h
|
|
282
|
+
agents artifacts share ./out/report.html --unlisted --expire 12h
|
|
273
283
|
|
|
274
284
|
# Permanent public page (opt out of the default 30d expiry)
|
|
275
|
-
agents share ./out/landing.html --slug landing --expire never
|
|
285
|
+
agents artifacts share ./out/landing.html --slug landing --expire never
|
|
276
286
|
|
|
277
287
|
# Custom slug, expiring in 7 days
|
|
278
|
-
agents share ./out/report.html --slug q3-report --expire 7d
|
|
288
|
+
agents artifacts share ./out/report.html --slug q3-report --expire 7d
|
|
279
289
|
${SHARE_DELETE_EXAMPLES}
|
|
280
290
|
# One-time setup (or join an existing endpoint)
|
|
281
|
-
agents
|
|
282
|
-
agents share join https://share.agents-cli.sh
|
|
291
|
+
agents artifacts setup
|
|
292
|
+
agents artifacts share join https://share.agents-cli.sh
|
|
283
293
|
|
|
284
294
|
# Push a worker-template.ts change out to an already-provisioned endpoint
|
|
285
|
-
agents share update
|
|
295
|
+
agents artifacts share update
|
|
286
296
|
`,
|
|
287
297
|
notes: `
|
|
288
298
|
Default expiry is 30d so an accidental publish decays. Pass --expire never for
|
|
289
299
|
a permanent link. --unlisted / --private hides the page from the public gallery
|
|
290
|
-
and agents share list; the direct URL is still world-readable
|
|
291
|
-
secret). A pre-publish scan refuses emails and credential-shaped
|
|
292
|
-
unless --force is passed.
|
|
300
|
+
and agents artifacts share list; the direct URL is still world-readable
|
|
301
|
+
(unlisted, not secret). A pre-publish scan refuses emails and credential-shaped
|
|
302
|
+
strings unless --force is passed.
|
|
293
303
|
${SHARE_DELETE_NOTES}
|
|
294
304
|
`,
|
|
295
305
|
});
|
|
@@ -300,32 +310,6 @@ ${SHARE_DELETE_NOTES}
|
|
|
300
310
|
shareDeleteCmd.action(async (targets, opts) => {
|
|
301
311
|
await runShareDelete(targets, opts);
|
|
302
312
|
});
|
|
303
|
-
const unshareCmd = registerShareDeleteOptions(program
|
|
304
|
-
.command('unshare <targets...>')
|
|
305
|
-
.description('Alias of `agents share delete` — take down a published page (and by default its OG cover).'));
|
|
306
|
-
setHelpSections(unshareCmd, { examples: SHARE_DELETE_EXAMPLES, notes: SHARE_DELETE_NOTES });
|
|
307
|
-
unshareCmd.action(async (targets, opts) => {
|
|
308
|
-
await runShareDelete(targets, opts);
|
|
309
|
-
});
|
|
310
|
-
shareCmd
|
|
311
|
-
.command('setup')
|
|
312
|
-
.description('One-time: provision an R2 bucket + Worker on your Cloudflare and save the config.')
|
|
313
|
-
.option('--bundle <name>', 'secrets bundle holding the Cloudflare API token', DEFAULT_CF_BUNDLE)
|
|
314
|
-
.option('--worker <name>', 'Worker name', DEFAULT_WORKER_NAME)
|
|
315
|
-
.option('--bucket <name>', 'R2 bucket name', DEFAULT_BUCKET_NAME)
|
|
316
|
-
.option('--account <id>', 'Cloudflare account id (else read from the bundle / prompt)')
|
|
317
|
-
.option('--token <t>', 'Cloudflare API token (else read from the --bundle)')
|
|
318
|
-
.option('--domain <host>', `custom domain to map (default: ${DEFAULT_SHARE_DOMAIN}; workers.dev if zone is not visible)`)
|
|
319
|
-
.option('--analytics-token <token>', 'Cloudflare Web Analytics token to inject into published HTML pages')
|
|
320
|
-
.action(async (opts) => {
|
|
321
|
-
try {
|
|
322
|
-
await runShareProvision(opts);
|
|
323
|
-
}
|
|
324
|
-
catch (e) {
|
|
325
|
-
console.error(chalk.red(e.message));
|
|
326
|
-
process.exitCode = 1;
|
|
327
|
-
}
|
|
328
|
-
});
|
|
329
313
|
shareCmd
|
|
330
314
|
.command('join')
|
|
331
315
|
.description('Use an existing synced share endpoint and write token (no provisioning).')
|
|
@@ -370,15 +354,15 @@ ${SHARE_DELETE_NOTES}
|
|
|
370
354
|
setHelpSections(shareUpdateCmd, {
|
|
371
355
|
examples: `
|
|
372
356
|
# Push a worker-template.ts change out to your already-provisioned endpoint
|
|
373
|
-
agents share update
|
|
357
|
+
agents artifacts share update
|
|
374
358
|
|
|
375
359
|
# Force a re-deploy even though the template hash already matches
|
|
376
|
-
agents share update --force
|
|
360
|
+
agents artifacts share update --force
|
|
377
361
|
`,
|
|
378
362
|
notes: `
|
|
379
|
-
Reuses the existing account/worker/bucket from 'agents share status' and the
|
|
363
|
+
Reuses the existing account/worker/bucket from 'agents artifacts share status' and the
|
|
380
364
|
existing write token — it never re-provisions a bucket, touches routes, or
|
|
381
|
-
regenerates the token. See 'agents share status' for whether an update is due.
|
|
365
|
+
regenerates the token. See 'agents artifacts share status' for whether an update is due.
|
|
382
366
|
`,
|
|
383
367
|
});
|
|
384
368
|
shareCmd
|
|
@@ -387,7 +371,7 @@ ${SHARE_DELETE_NOTES}
|
|
|
387
371
|
.action(async () => {
|
|
388
372
|
const cfg = readShareConfig();
|
|
389
373
|
if (!cfg) {
|
|
390
|
-
console.log(chalk.dim("Not configured. Run 'agents
|
|
374
|
+
console.log(chalk.dim("Not configured. Run 'agents artifacts setup' or 'agents artifacts share join'."));
|
|
391
375
|
return;
|
|
392
376
|
}
|
|
393
377
|
console.log(`${chalk.bold('endpoint')} ${chalk.green(cfg.baseUrl)}`);
|
|
@@ -399,8 +383,8 @@ ${SHARE_DELETE_NOTES}
|
|
|
399
383
|
const templateLabel = templateStatus === 'current'
|
|
400
384
|
? chalk.green('current')
|
|
401
385
|
: templateStatus === 'outdated'
|
|
402
|
-
? chalk.yellow('outdated — run `agents share update`')
|
|
403
|
-
: chalk.dim("unknown — provisioned before version tracking; run `agents share update` to adopt it");
|
|
386
|
+
? chalk.yellow('outdated — run `agents artifacts share update`')
|
|
387
|
+
: chalk.dim("unknown — provisioned before version tracking; run `agents artifacts share update` to adopt it");
|
|
404
388
|
console.log(`${chalk.bold('template')} ${templateLabel}`);
|
|
405
389
|
});
|
|
406
390
|
const shareListCmd = shareCmd
|
|
@@ -421,21 +405,21 @@ ${SHARE_DELETE_NOTES}
|
|
|
421
405
|
setHelpSections(shareListCmd, {
|
|
422
406
|
examples: `
|
|
423
407
|
# Everything you've published, newest first
|
|
424
|
-
agents share list
|
|
408
|
+
agents artifacts share list
|
|
425
409
|
|
|
426
410
|
# Machine-readable — e.g. pull every still-public URL with jq
|
|
427
|
-
agents share list --json | jq -r '.objects[].url'
|
|
411
|
+
agents artifacts share list --json | jq -r '.objects[].url'
|
|
428
412
|
|
|
429
413
|
# List another namespace
|
|
430
|
-
agents share list --github-user octocat
|
|
414
|
+
agents artifacts share list --github-user octocat
|
|
431
415
|
`,
|
|
432
416
|
notes: `
|
|
433
417
|
Lists the ACTIVE pages in your namespace — expired links and the sibling .png OG
|
|
434
418
|
covers are omitted (it mirrors the public gallery). It reads the endpoint's JSON
|
|
435
419
|
listing route, which ships with the current Worker template. If your deployed
|
|
436
|
-
Worker predates this feature the command says so and points you at 'agents
|
|
437
|
-
update' (RUSH-2449) rather than returning a wrong or empty result
|
|
438
|
-
share status' for whether an update is due.
|
|
420
|
+
Worker predates this feature the command says so and points you at 'agents
|
|
421
|
+
artifacts share update' (RUSH-2449) rather than returning a wrong or empty result
|
|
422
|
+
— see 'agents artifacts share status' for whether an update is due.
|
|
439
423
|
`,
|
|
440
424
|
});
|
|
441
425
|
shareCmd
|
|
@@ -444,7 +428,7 @@ ${SHARE_DELETE_NOTES}
|
|
|
444
428
|
.action(async () => {
|
|
445
429
|
const cfg = readShareConfig();
|
|
446
430
|
if (!cfg) {
|
|
447
|
-
console.log(chalk.dim("Not configured. Run 'agents
|
|
431
|
+
console.log(chalk.dim("Not configured. Run 'agents artifacts setup' or 'agents artifacts share join'."));
|
|
448
432
|
return;
|
|
449
433
|
}
|
|
450
434
|
if (!analyticsEnabled(cfg)) {
|
|
@@ -464,9 +448,24 @@ ${SHARE_DELETE_NOTES}
|
|
|
464
448
|
}
|
|
465
449
|
});
|
|
466
450
|
}
|
|
451
|
+
/**
|
|
452
|
+
* Register the top-level `agents unshare <targets...>` alias of
|
|
453
|
+
* `agents artifacts share delete`. It takes the ROOT program (not the artifacts
|
|
454
|
+
* group) because it is deliberately a top-level convenience verb — taking a page
|
|
455
|
+
* down is the one artifact action typed often enough to keep at the root.
|
|
456
|
+
*/
|
|
457
|
+
export function registerUnshareCommand(program) {
|
|
458
|
+
const unshareCmd = registerShareDeleteOptions(program
|
|
459
|
+
.command('unshare <targets...>')
|
|
460
|
+
.description('Alias of `agents artifacts share delete` — take down a published page (and by default its OG cover).'));
|
|
461
|
+
setHelpSections(unshareCmd, { examples: SHARE_DELETE_EXAMPLES, notes: SHARE_DELETE_NOTES });
|
|
462
|
+
unshareCmd.action(async (targets, opts) => {
|
|
463
|
+
await runShareDelete(targets, opts);
|
|
464
|
+
});
|
|
465
|
+
}
|
|
467
466
|
/** Provision a fresh R2 bucket + Worker on the user's Cloudflare and persist the
|
|
468
|
-
* endpoint config + write token. Shared by `agents
|
|
469
|
-
*
|
|
467
|
+
* endpoint config + write token. Shared by both modes of `agents artifacts setup`
|
|
468
|
+
* (the flag-driven provision and the interactive wizard). */
|
|
470
469
|
export async function runShareProvision(opts) {
|
|
471
470
|
const { default: ora } = await import('ora');
|
|
472
471
|
const { input } = await import('@inquirer/prompts');
|
|
@@ -521,7 +520,7 @@ export async function runShareProvision(opts) {
|
|
|
521
520
|
writeShareConfig(cfg);
|
|
522
521
|
storeWriteToken(token);
|
|
523
522
|
console.log(chalk.green(`\nShare endpoint ready → ${chalk.bold(baseUrl)}`));
|
|
524
|
-
console.log(chalk.dim('Publish with: ') + chalk.cyan('agents share <file>'));
|
|
523
|
+
console.log(chalk.dim('Publish with: ') + chalk.cyan('agents artifacts share <file>'));
|
|
525
524
|
console.log(chalk.dim(`Fleet: push the token with 'agents secrets export share --host <box>' and pull config with 'agents repo pull'.`));
|
|
526
525
|
}
|
|
527
526
|
catch (e) {
|
|
@@ -539,7 +538,7 @@ export async function runShareProvision(opts) {
|
|
|
539
538
|
export async function runShareUpdate(opts = {}) {
|
|
540
539
|
const cfg = readShareConfig();
|
|
541
540
|
if (!cfg) {
|
|
542
|
-
throw new Error("Not configured. Run 'agents
|
|
541
|
+
throw new Error("Not configured. Run 'agents artifacts setup' (to provision) or 'agents artifacts share join' first.");
|
|
543
542
|
}
|
|
544
543
|
const { apiToken, accountId: acctFromBundle } = readCloudflareCreds(opts.bundle ?? DEFAULT_CF_BUNDLE, {
|
|
545
544
|
apiToken: opts.token,
|
|
@@ -568,14 +567,14 @@ function cleanHostname(domain) {
|
|
|
568
567
|
}
|
|
569
568
|
}
|
|
570
569
|
/** Join an existing share endpoint (no provisioning): prompt for the endpoint
|
|
571
|
-
* details + write token and persist them. Shared by `agents share join`
|
|
572
|
-
*
|
|
570
|
+
* details + write token and persist them. Shared by `agents artifacts share join`
|
|
571
|
+
* and the `agents artifacts setup` wizard. */
|
|
573
572
|
export async function runShareJoin(baseUrl, opts = {}) {
|
|
574
573
|
const { password, input } = await import('@inquirer/prompts');
|
|
575
574
|
const existing = readShareConfig();
|
|
576
575
|
const clean = baseUrl?.replace(/\/+$/, '');
|
|
577
576
|
if (!clean && !existing) {
|
|
578
|
-
throw new Error("No synced share endpoint found. Pull config first with 'agents repo pull', or pass the endpoint URL: agents share join <baseUrl>.");
|
|
577
|
+
throw new Error("No synced share endpoint found. Pull config first with 'agents repo pull', or pass the endpoint URL: agents artifacts share join <baseUrl>.");
|
|
579
578
|
}
|
|
580
579
|
let cfg;
|
|
581
580
|
if (existing && (!clean || clean === existing.baseUrl)) {
|
|
@@ -608,5 +607,5 @@ export async function runShareJoin(baseUrl, opts = {}) {
|
|
|
608
607
|
throw new Error('A write token is required to join.');
|
|
609
608
|
writeShareConfig(cfg);
|
|
610
609
|
storeWriteToken(token);
|
|
611
|
-
console.log(chalk.green(`Joined ${chalk.bold(cfg.baseUrl)} — publish with `) + chalk.cyan('agents share <file>'));
|
|
610
|
+
console.log(chalk.green(`Joined ${chalk.bold(cfg.baseUrl)} — publish with `) + chalk.cyan('agents artifacts share <file>'));
|
|
612
611
|
}
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
import { type PidSessionEntry } from '../session/pid-registry.js';
|
|
2
|
+
import type { ReapResult, Task } from './types.js';
|
|
3
|
+
/** Default idle window before an untouched task is reaped. */
|
|
4
|
+
export declare const DEFAULT_IDLE_MS: number;
|
|
5
|
+
/**
|
|
6
|
+
* The slice of `BrowserService` the reaper needs. Structural, so a test drives
|
|
7
|
+
* it with a stub and `BrowserService` satisfies it without an import — which
|
|
8
|
+
* also keeps this module off the service's import cycle.
|
|
9
|
+
*/
|
|
10
|
+
export interface ReapableService {
|
|
11
|
+
listTasks(): Array<{
|
|
12
|
+
profile: string;
|
|
13
|
+
task: Task;
|
|
14
|
+
}>;
|
|
15
|
+
recordStatus(taskName: string): Promise<{
|
|
16
|
+
recording: boolean;
|
|
17
|
+
}>;
|
|
18
|
+
stop(taskName: string): Promise<{
|
|
19
|
+
ok: boolean;
|
|
20
|
+
profile?: string;
|
|
21
|
+
}>;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Injectable liveness sources. Same shape as `feed-post.ts`
|
|
25
|
+
* (`input.listEntries ?? listPidSessionEntries`, feed-post.ts:119) — the
|
|
26
|
+
* defaults are the real registry and the real process table, so a test can
|
|
27
|
+
* substitute them without mocking a module.
|
|
28
|
+
*/
|
|
29
|
+
export interface ReapDeps {
|
|
30
|
+
listEntries?: () => PidSessionEntry[];
|
|
31
|
+
pidAlive?: (pid: number, startedAtMs?: number) => boolean;
|
|
32
|
+
sessionIdOfPid?: (pid: number) => string | undefined;
|
|
33
|
+
sessionLiveOnProcessTable?: (sessionId: string) => Promise<boolean>;
|
|
34
|
+
}
|
|
35
|
+
export interface ReapOptions {
|
|
36
|
+
/** Idle window in ms. Default {@link DEFAULT_IDLE_MS}. */
|
|
37
|
+
idleMs?: number;
|
|
38
|
+
/** Clock, injectable for tests. Default `Date.now()`. */
|
|
39
|
+
now?: number;
|
|
40
|
+
/** Report what would be closed without closing anything. */
|
|
41
|
+
dryRun?: boolean;
|
|
42
|
+
deps?: ReapDeps;
|
|
43
|
+
}
|
|
44
|
+
export interface LiveIdentities {
|
|
45
|
+
sessions: Set<string>;
|
|
46
|
+
launches: Set<string>;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* Session and launch ids belonging to a process that is alive right now.
|
|
50
|
+
*
|
|
51
|
+
* Built from the per-pid launch registry, filtered to live pids —
|
|
52
|
+
* `isPidAlive(pid, startedAtMs)` rather than a bare existence check, so a pid
|
|
53
|
+
* the OS recycled onto an unrelated process does not read as a live agent.
|
|
54
|
+
* An entry with no recorded `sessionId` still contributes one when the live
|
|
55
|
+
* process carries `--session-id` on its argv, which is the RUSH-2384 recovery
|
|
56
|
+
* path the registry itself documents (pid-registry.ts:102-107).
|
|
57
|
+
*/
|
|
58
|
+
export declare function resolveLiveIdentities(deps?: ReapDeps): LiveIdentities;
|
|
59
|
+
/**
|
|
60
|
+
* True when the task's owner is PROVABLY gone. The bar is proof, not absence of
|
|
61
|
+
* evidence, because being wrong here closes a working agent's tabs.
|
|
62
|
+
*
|
|
63
|
+
* Only a `sessionId` can carry that proof. It has two independent sources — the
|
|
64
|
+
* per-pid launch registry, and a live process carrying `--session-id <id>` in
|
|
65
|
+
* its argv — so a session the registry missed is still caught by the process
|
|
66
|
+
* table. The registry misses constantly: a wrapper pid exits, a prune sweeps
|
|
67
|
+
* the entry, or the agent was never launched via `agents run`
|
|
68
|
+
* (pid-registry.ts:102-107, RUSH-2384).
|
|
69
|
+
*
|
|
70
|
+
* A `launchId` has NO second source — the registry is its only witness, and
|
|
71
|
+
* that witness is the unreliable one. So a task carrying only a `launchId` is
|
|
72
|
+
* never session-reaped, exactly like a task carrying no identity at all; both
|
|
73
|
+
* fall through to the idle rule. This is not a corner case: `launchId` is
|
|
74
|
+
* minted for every run (`exec.ts` `resolveLaunchId`) while `AGENT_SESSION_ID`
|
|
75
|
+
* is Claude-only and skipped on resume, so treating a missing registry entry as
|
|
76
|
+
* proof of death would close the tabs of every live codex/droid/grok run whose
|
|
77
|
+
* launch pid had already exited.
|
|
78
|
+
*
|
|
79
|
+
* A live `launchId` still RESCUES a task whose `sessionId` looks dead — proof of
|
|
80
|
+
* life needs only one witness, unlike proof of death.
|
|
81
|
+
*/
|
|
82
|
+
export declare function taskOwnerIsGone(task: Task, live: LiveIdentities, deps?: ReapDeps): Promise<boolean>;
|
|
83
|
+
/**
|
|
84
|
+
* Stop every task whose owner is gone or that has sat untouched past `idleMs`.
|
|
85
|
+
*
|
|
86
|
+
* Returns what it closed and how many it left alone. A task that is mid-
|
|
87
|
+
* recording is always left alone: reaping it would truncate a capture the user
|
|
88
|
+
* asked for, and an in-flight recording is itself proof the task is in use.
|
|
89
|
+
*/
|
|
90
|
+
export declare function reapAbandonedTasks(service: ReapableService, opts?: ReapOptions): Promise<ReapResult>;
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Abandoned browser-task reaper (RUSH-2622).
|
|
3
|
+
*
|
|
4
|
+
* `agents browser done` / `stop` already close a task's tabs, but agents
|
|
5
|
+
* routinely never call them — the run ends, the process exits, and the task's
|
|
6
|
+
* tabs stay open in the profile window forever. Over a day of fleet activity
|
|
7
|
+
* that is dozens of leftover tabs in Comet/Chrome.
|
|
8
|
+
*
|
|
9
|
+
* This closes the loop from the other end: a periodic pass that finds tasks
|
|
10
|
+
* nobody is driving any more and stops them.
|
|
11
|
+
*
|
|
12
|
+
* Two independent reasons, both conservative:
|
|
13
|
+
*
|
|
14
|
+
* - `session-dead` — the task recorded WHICH agent session (`sessionId`) or
|
|
15
|
+
* WHICH run (`launchId`) started it, and neither is alive on this host.
|
|
16
|
+
* That is the honest end of the task: the thing that would have called
|
|
17
|
+
* `done` no longer exists.
|
|
18
|
+
* - `idle` — nothing has touched the task for `idleMs` (default 30 minutes).
|
|
19
|
+
* The catch-all for a task with no recorded identity (a human running
|
|
20
|
+
* `agents browser start` by hand) or an agent that stalled without exiting.
|
|
21
|
+
*
|
|
22
|
+
* Closing always goes through `BrowserService.stop`, never a direct
|
|
23
|
+
* `Target.closeTarget`, so history, the session cache, the target cache, and
|
|
24
|
+
* forked-profile teardown all stay on the one code path that already handles
|
|
25
|
+
* them. Two things this deliberately never does: touch a tab that is not in
|
|
26
|
+
* `task.tabs` (a tab the user opened themselves is not ours to close), and kill
|
|
27
|
+
* the profile window or the browser process because one task went idle.
|
|
28
|
+
*/
|
|
29
|
+
import { isPidAlive, isSessionIdLiveOnProcessTable } from '../session/active.js';
|
|
30
|
+
import { listPidSessionEntries, sessionIdFromLivePid } from '../session/pid-registry.js';
|
|
31
|
+
/** Default idle window before an untouched task is reaped. */
|
|
32
|
+
export const DEFAULT_IDLE_MS = 30 * 60_000;
|
|
33
|
+
/**
|
|
34
|
+
* Session and launch ids belonging to a process that is alive right now.
|
|
35
|
+
*
|
|
36
|
+
* Built from the per-pid launch registry, filtered to live pids —
|
|
37
|
+
* `isPidAlive(pid, startedAtMs)` rather than a bare existence check, so a pid
|
|
38
|
+
* the OS recycled onto an unrelated process does not read as a live agent.
|
|
39
|
+
* An entry with no recorded `sessionId` still contributes one when the live
|
|
40
|
+
* process carries `--session-id` on its argv, which is the RUSH-2384 recovery
|
|
41
|
+
* path the registry itself documents (pid-registry.ts:102-107).
|
|
42
|
+
*/
|
|
43
|
+
export function resolveLiveIdentities(deps = {}) {
|
|
44
|
+
const listEntries = deps.listEntries ?? listPidSessionEntries;
|
|
45
|
+
const alive = deps.pidAlive ?? isPidAlive;
|
|
46
|
+
const sessionIdOf = deps.sessionIdOfPid ?? sessionIdFromLivePid;
|
|
47
|
+
const sessions = new Set();
|
|
48
|
+
const launches = new Set();
|
|
49
|
+
for (const entry of listEntries()) {
|
|
50
|
+
if (!alive(entry.pid, entry.startedAtMs))
|
|
51
|
+
continue;
|
|
52
|
+
const sessionId = entry.sessionId ?? sessionIdOf(entry.pid);
|
|
53
|
+
if (sessionId)
|
|
54
|
+
sessions.add(sessionId);
|
|
55
|
+
if (entry.launchId)
|
|
56
|
+
launches.add(entry.launchId);
|
|
57
|
+
}
|
|
58
|
+
return { sessions, launches };
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* True when the task's owner is PROVABLY gone. The bar is proof, not absence of
|
|
62
|
+
* evidence, because being wrong here closes a working agent's tabs.
|
|
63
|
+
*
|
|
64
|
+
* Only a `sessionId` can carry that proof. It has two independent sources — the
|
|
65
|
+
* per-pid launch registry, and a live process carrying `--session-id <id>` in
|
|
66
|
+
* its argv — so a session the registry missed is still caught by the process
|
|
67
|
+
* table. The registry misses constantly: a wrapper pid exits, a prune sweeps
|
|
68
|
+
* the entry, or the agent was never launched via `agents run`
|
|
69
|
+
* (pid-registry.ts:102-107, RUSH-2384).
|
|
70
|
+
*
|
|
71
|
+
* A `launchId` has NO second source — the registry is its only witness, and
|
|
72
|
+
* that witness is the unreliable one. So a task carrying only a `launchId` is
|
|
73
|
+
* never session-reaped, exactly like a task carrying no identity at all; both
|
|
74
|
+
* fall through to the idle rule. This is not a corner case: `launchId` is
|
|
75
|
+
* minted for every run (`exec.ts` `resolveLaunchId`) while `AGENT_SESSION_ID`
|
|
76
|
+
* is Claude-only and skipped on resume, so treating a missing registry entry as
|
|
77
|
+
* proof of death would close the tabs of every live codex/droid/grok run whose
|
|
78
|
+
* launch pid had already exited.
|
|
79
|
+
*
|
|
80
|
+
* A live `launchId` still RESCUES a task whose `sessionId` looks dead — proof of
|
|
81
|
+
* life needs only one witness, unlike proof of death.
|
|
82
|
+
*/
|
|
83
|
+
export async function taskOwnerIsGone(task, live, deps = {}) {
|
|
84
|
+
if (!task.sessionId)
|
|
85
|
+
return false;
|
|
86
|
+
if (task.launchId && live.launches.has(task.launchId))
|
|
87
|
+
return false;
|
|
88
|
+
if (live.sessions.has(task.sessionId))
|
|
89
|
+
return false;
|
|
90
|
+
const onProcessTable = deps.sessionLiveOnProcessTable ?? ((id) => isSessionIdLiveOnProcessTable(id));
|
|
91
|
+
return !(await onProcessTable(task.sessionId));
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Stop every task whose owner is gone or that has sat untouched past `idleMs`.
|
|
95
|
+
*
|
|
96
|
+
* Returns what it closed and how many it left alone. A task that is mid-
|
|
97
|
+
* recording is always left alone: reaping it would truncate a capture the user
|
|
98
|
+
* asked for, and an in-flight recording is itself proof the task is in use.
|
|
99
|
+
*/
|
|
100
|
+
export async function reapAbandonedTasks(service, opts = {}) {
|
|
101
|
+
const idleMs = opts.idleMs ?? DEFAULT_IDLE_MS;
|
|
102
|
+
// Fail loud rather than reap everything. A caller-supplied `0` survives `??`
|
|
103
|
+
// and would close every task including one created a millisecond ago; a
|
|
104
|
+
// non-numeric value makes every `>=` comparison false and silently disables
|
|
105
|
+
// idle reaping. Both are worse than an error.
|
|
106
|
+
if (!Number.isFinite(idleMs) || idleMs <= 0) {
|
|
107
|
+
throw new Error(`idleMs must be a positive number of milliseconds, got ${String(idleMs)}`);
|
|
108
|
+
}
|
|
109
|
+
const now = opts.now ?? Date.now();
|
|
110
|
+
const deps = opts.deps ?? {};
|
|
111
|
+
const live = resolveLiveIdentities(deps);
|
|
112
|
+
const closed = [];
|
|
113
|
+
let skipped = 0;
|
|
114
|
+
for (const { profile, task } of service.listTasks()) {
|
|
115
|
+
if ((await service.recordStatus(task.name)).recording) {
|
|
116
|
+
skipped++;
|
|
117
|
+
continue;
|
|
118
|
+
}
|
|
119
|
+
let reason;
|
|
120
|
+
if (await taskOwnerIsGone(task, live, deps)) {
|
|
121
|
+
reason = 'session-dead';
|
|
122
|
+
}
|
|
123
|
+
else if (now - (task.lastActionAt ?? task.createdAt) >= idleMs) {
|
|
124
|
+
reason = 'idle';
|
|
125
|
+
}
|
|
126
|
+
if (!reason) {
|
|
127
|
+
skipped++;
|
|
128
|
+
continue;
|
|
129
|
+
}
|
|
130
|
+
if (opts.dryRun) {
|
|
131
|
+
closed.push({ task: task.name, profile, reason });
|
|
132
|
+
continue;
|
|
133
|
+
}
|
|
134
|
+
const result = await service.stop(task.name);
|
|
135
|
+
if (result.ok) {
|
|
136
|
+
closed.push({ task: task.name, profile: result.profile ?? profile, reason });
|
|
137
|
+
}
|
|
138
|
+
else {
|
|
139
|
+
// `stop` reports not-found only when the task already left the map — a
|
|
140
|
+
// concurrent `done`, or a profile torn down mid-pass. Nothing was closed,
|
|
141
|
+
// so it is not reported as closed.
|
|
142
|
+
skipped++;
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return { closed, skipped };
|
|
146
|
+
}
|
package/dist/lib/browser/ipc.js
CHANGED
|
@@ -340,6 +340,7 @@ export class BrowserIPCServer {
|
|
|
340
340
|
url: request.url,
|
|
341
341
|
endpointName: request.endpoint,
|
|
342
342
|
skipDomainSkill: request.skipDomainSkill,
|
|
343
|
+
fresh: request.fresh,
|
|
343
344
|
actor: request.actor,
|
|
344
345
|
launchId: request.launchId,
|
|
345
346
|
sessionId: request.sessionId,
|
|
@@ -352,6 +353,17 @@ export class BrowserIPCServer {
|
|
|
352
353
|
skill: result.skill,
|
|
353
354
|
};
|
|
354
355
|
}
|
|
356
|
+
// The out-of-process seam onto the abandoned-task reaper: the daemon
|
|
357
|
+
// owns the live BrowserService, so a CLI verb (`agents browser gc`) can
|
|
358
|
+
// only reach `reapAbandoned` through IPC. The daemon's own periodic tick
|
|
359
|
+
// calls the service method directly.
|
|
360
|
+
case 'gc': {
|
|
361
|
+
const reaped = await this.service.reapAbandoned({
|
|
362
|
+
idleMs: request.idleMinutes !== undefined ? request.idleMinutes * 60_000 : undefined,
|
|
363
|
+
dryRun: request.dryRun,
|
|
364
|
+
});
|
|
365
|
+
return { ok: true, reaped };
|
|
366
|
+
}
|
|
355
367
|
case 'done': {
|
|
356
368
|
if (!request.task) {
|
|
357
369
|
return { ok: false, error: 'Task required' };
|