@phnx-labs/agents-cli 1.22.53 → 1.22.54

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (134) hide show
  1. package/CHANGELOG.md +190 -0
  2. package/README.md +41 -8
  3. package/dist/bootstrap.js +55 -154
  4. package/dist/cli/command-registry.d.ts +5 -0
  5. package/dist/cli/command-registry.js +8 -1
  6. package/dist/commands/accounts.js +219 -173
  7. package/dist/commands/apply.js +6 -3
  8. package/dist/commands/auth-mint.d.ts +8 -0
  9. package/dist/commands/auth-mint.js +96 -0
  10. package/dist/commands/auth.js +5 -1
  11. package/dist/commands/browser.js +1 -1
  12. package/dist/commands/cost.js +8 -2
  13. package/dist/commands/daemon.js +2 -2
  14. package/dist/commands/doctor.js +6 -1
  15. package/dist/commands/exec.js +10 -8
  16. package/dist/commands/focus.d.ts +1 -0
  17. package/dist/commands/focus.js +2 -2
  18. package/dist/commands/go.d.ts +5 -4
  19. package/dist/commands/go.js +7 -7
  20. package/dist/commands/insights.js +9 -0
  21. package/dist/commands/monitors.js +85 -30
  22. package/dist/commands/output.js +8 -2
  23. package/dist/commands/repo.js +18 -0
  24. package/dist/commands/secrets.js +33 -14
  25. package/dist/commands/sessions.d.ts +20 -12
  26. package/dist/commands/sessions.js +64 -20
  27. package/dist/commands/setup-accounts.d.ts +8 -0
  28. package/dist/commands/setup-accounts.js +47 -0
  29. package/dist/commands/setup.d.ts +1 -1
  30. package/dist/commands/setup.js +11 -2
  31. package/dist/commands/share.d.ts +52 -3
  32. package/dist/commands/share.js +262 -18
  33. package/dist/commands/ssh.d.ts +7 -0
  34. package/dist/commands/ssh.js +18 -2
  35. package/dist/commands/status.js +14 -0
  36. package/dist/commands/view.d.ts +3 -1
  37. package/dist/commands/view.js +5 -4
  38. package/dist/lib/account-registry.js +15 -3
  39. package/dist/lib/accounting/rotate.d.ts +20 -6
  40. package/dist/lib/accounting/rotate.js +38 -7
  41. package/dist/lib/accounting/usage.d.ts +37 -1
  42. package/dist/lib/accounting/usage.js +71 -6
  43. package/dist/lib/agent-spec/agents.d.ts +5 -2
  44. package/dist/lib/agent-spec/agents.js +25 -7
  45. package/dist/lib/analytics/mix-commands.js +12 -6
  46. package/dist/lib/auth-mint.d.ts +150 -0
  47. package/dist/lib/auth-mint.js +434 -0
  48. package/dist/lib/browser/remote-control.d.ts +9 -7
  49. package/dist/lib/browser/remote-control.js +9 -7
  50. package/dist/lib/claude-account-token.d.ts +10 -0
  51. package/dist/lib/claude-account-token.js +14 -4
  52. package/dist/lib/config-drift.d.ts +37 -0
  53. package/dist/lib/config-drift.js +72 -0
  54. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  55. package/dist/lib/daemon/auth-sync-service.js +34 -0
  56. package/dist/lib/daemon/daemon.js +30 -4
  57. package/dist/lib/daemon-services.d.ts +1 -1
  58. package/dist/lib/daemon-services.js +5 -0
  59. package/dist/lib/device-config.d.ts +3 -3
  60. package/dist/lib/device-config.js +5 -5
  61. package/dist/lib/devices/connect.d.ts +26 -0
  62. package/dist/lib/devices/connect.js +48 -1
  63. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  64. package/dist/lib/devices/doctor-findings.js +19 -1
  65. package/dist/lib/exec.d.ts +28 -0
  66. package/dist/lib/exec.js +73 -7
  67. package/dist/lib/feed/feed.d.ts +1 -1
  68. package/dist/lib/feed/feed.js +23 -1
  69. package/dist/lib/feed-broadcast.js +1 -1
  70. package/dist/lib/fleet/apply.d.ts +11 -0
  71. package/dist/lib/fleet/apply.js +23 -3
  72. package/dist/lib/fleet/auth-sync.js +5 -3
  73. package/dist/lib/help.d.ts +9 -0
  74. package/dist/lib/help.js +29 -1
  75. package/dist/lib/hosts/passthrough.d.ts +1 -10
  76. package/dist/lib/hosts/passthrough.js +1 -13
  77. package/dist/lib/installations/versions.js +9 -1
  78. package/dist/lib/linux-userns.d.ts +58 -0
  79. package/dist/lib/linux-userns.js +116 -0
  80. package/dist/lib/memory.d.ts +26 -0
  81. package/dist/lib/memory.js +80 -1
  82. package/dist/lib/monitors/config.d.ts +11 -0
  83. package/dist/lib/monitors/config.js +8 -0
  84. package/dist/lib/monitors/engine.js +8 -1
  85. package/dist/lib/monitors/state.d.ts +37 -1
  86. package/dist/lib/monitors/state.js +79 -4
  87. package/dist/lib/permissions-registry.d.ts +2 -0
  88. package/dist/lib/permissions-registry.js +116 -14
  89. package/dist/lib/permissions.d.ts +5 -3
  90. package/dist/lib/permissions.js +25 -27
  91. package/dist/lib/profiles.d.ts +8 -7
  92. package/dist/lib/profiles.js +12 -0
  93. package/dist/lib/project-key.d.ts +9 -0
  94. package/dist/lib/project-key.js +11 -0
  95. package/dist/lib/secrets/bundles.d.ts +35 -0
  96. package/dist/lib/secrets/bundles.js +78 -1
  97. package/dist/lib/secrets/push.d.ts +3 -8
  98. package/dist/lib/secrets/push.js +18 -14
  99. package/dist/lib/secrets/remote.d.ts +9 -18
  100. package/dist/lib/secrets/remote.js +11 -26
  101. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  102. package/dist/lib/secrets/reserved-sync.js +129 -0
  103. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  104. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  105. package/dist/lib/self-heal/registry.js +4 -0
  106. package/dist/lib/self-heal/types.d.ts +1 -1
  107. package/dist/lib/session/active.js +1 -4
  108. package/dist/lib/session/db.d.ts +39 -4
  109. package/dist/lib/session/db.js +130 -29
  110. package/dist/lib/session/discover.d.ts +32 -4
  111. package/dist/lib/session/discover.js +119 -25
  112. package/dist/lib/session/insights.d.ts +14 -0
  113. package/dist/lib/session/insights.js +25 -2
  114. package/dist/lib/session/linear.js +1 -1
  115. package/dist/lib/session/shell-programs.d.ts +17 -0
  116. package/dist/lib/session/shell-programs.js +21 -0
  117. package/dist/lib/session/state.js +2 -1
  118. package/dist/lib/session/stream-render.js +2 -1
  119. package/dist/lib/session/tool-calls.js +2 -5
  120. package/dist/lib/session/trajectory-html.js +2 -1
  121. package/dist/lib/session/trajectory.js +3 -12
  122. package/dist/lib/session/types.d.ts +8 -0
  123. package/dist/lib/share/publish.d.ts +53 -5
  124. package/dist/lib/share/publish.js +99 -17
  125. package/dist/lib/share/worker-template.js +582 -57
  126. package/dist/lib/startup/root-command.js +2 -1
  127. package/dist/lib/state.d.ts +16 -0
  128. package/dist/lib/state.js +178 -46
  129. package/dist/lib/sync-status.d.ts +4 -0
  130. package/dist/lib/sync-status.js +3 -0
  131. package/dist/lib/traces/classify.js +24 -19
  132. package/dist/lib/usage-refresh.js +2 -1
  133. package/dist/lib/view-types.d.ts +2 -0
  134. package/package.json +2 -1
@@ -1,7 +1,7 @@
1
1
  import { type Command } from 'commander';
2
2
  import { type ShareConfig } from '../lib/share/config.js';
3
3
  import { type CloudflareRequester } from '../lib/share/provision.js';
4
- import { type PublishResult } from '../lib/share/publish.js';
4
+ import { type PublishResult, type ShareVisibility } from '../lib/share/publish.js';
5
5
  import { deleteShare, type DeleteShareResult } from '../lib/share/delete.js';
6
6
  import type { PhoenixSession } from '../lib/identity/client.js';
7
7
  export declare function formatSharePublishResult(result: PublishResult, json?: boolean): string;
@@ -36,6 +36,9 @@ export interface ShareListItem {
36
36
  repo: string | null;
37
37
  /** Count of retained prior versions under this slug (see `share revisions`). */
38
38
  revisionCount: number;
39
+ /** The visibility stamped at publish time: public | unlisted | me | org.
40
+ * Missing on Workers that predate this field → treated as public. */
41
+ visibility: ShareVisibility;
39
42
  /** Arbitrary `--meta key=value` entries attached at publish time (RUSH-2683)
40
43
  * — everything that isn't a reserved provenance/label key. `{}` when none
41
44
  * were set, or when the deployed Worker predates this field. */
@@ -47,12 +50,41 @@ export interface ShareListResult {
47
50
  count: number;
48
51
  objects: ShareListItem[];
49
52
  }
50
- /** DI seam for tests — override the real HTTP GET of the JSON listing route. */
51
- export type ListingFetchFn = (url: string) => Promise<{
53
+ /** DI seam for tests — override the real HTTP GET of the JSON listing route.
54
+ * The optional `headers` carry the owner bearer for hidden-visibility listings. */
55
+ export type ListingFetchFn = (url: string, headers?: Record<string, string>) => Promise<{
52
56
  status: number;
53
57
  contentType: string;
54
58
  body: string;
55
59
  }>;
60
+ export interface ShareEditResult {
61
+ ok: true;
62
+ url: string;
63
+ label: string | null;
64
+ meta: Record<string, string>;
65
+ /** The visibility the page now carries (present on Workers that support the
66
+ * visibility edit; absent from an older deployed template). */
67
+ visibility?: ShareVisibility;
68
+ /** The visibility the page had before this edit, when the Worker reports it. */
69
+ previousVisibility?: ShareVisibility;
70
+ }
71
+ export declare function runShareEdit(target: string, opts: {
72
+ githubUser?: string;
73
+ config?: ShareConfig;
74
+ writeToken?: string;
75
+ byo?: boolean;
76
+ session?: PhoenixSession | null;
77
+ label?: string | null;
78
+ meta?: Record<string, string>;
79
+ metaMode?: 'merge' | 'replace';
80
+ removeMeta?: string[];
81
+ /** Change the page's visibility in place (public | unlisted | me | org).
82
+ * A metadata-only rewrite like label/meta — the body is untouched, so no
83
+ * revision is created. me/org require a Phoenix session. */
84
+ visibility?: ShareVisibility;
85
+ force?: boolean;
86
+ fetchEdit?: typeof fetch;
87
+ }): Promise<ShareEditResult>;
56
88
  /** Parse the Worker's listing JSON into a validated result, failing loud (with the
57
89
  * outdated-template hint) on any body that isn't the expected shape — an old Worker
58
90
  * serves the HTML gallery for a non-empty namespace, which must not be silently
@@ -79,7 +111,24 @@ export declare function runShareList(opts?: {
79
111
  session?: string;
80
112
  /** Only shares whose label contains this text (case-insensitive substring). */
81
113
  label?: string;
114
+ /** Only shares matching every provided `--meta key=value` entry. */
115
+ meta?: Record<string, string>;
116
+ /** Visibility filter. `public` (default) lists only public gallery pages;
117
+ * `unlisted`/`me`/`org` include that owner's hidden pages; `all` includes
118
+ * every active page. Hidden scopes send the owner's bearer and `scope=mine`. */
119
+ scope?: 'public' | 'unlisted' | 'me' | 'org' | 'all';
82
120
  }): Promise<ShareListResult>;
121
+ /** Client-side filtering over an already-fetched listing (RUSH-2683) — the
122
+ * Worker has no query surface for this, so it narrows the fetched set instead
123
+ * of a second round trip. `count` reflects the FILTERED set, matching what the
124
+ * caller actually sees. */
125
+ export declare function applyShareListFilters(result: ShareListResult, filters: {
126
+ agent?: string;
127
+ session?: string;
128
+ label?: string;
129
+ meta?: Record<string, string>;
130
+ scope?: 'public' | 'unlisted' | 'me' | 'org' | 'all';
131
+ }): ShareListResult;
83
132
  export declare function formatShareList(result: ShareListResult, json?: boolean): string;
84
133
  /** One retained prior version of a slug, as reported by the Worker's
85
134
  * `?revisions=json` route. */
@@ -7,11 +7,11 @@
7
7
  // (commands/artifacts-setup.ts), which calls `runShareProvision` below.
8
8
  import { existsSync } from 'node:fs';
9
9
  import { formatBytes } from '../lib/format.js';
10
- import { Option } from 'commander';
10
+ import { Argument, Option } from 'commander';
11
11
  import chalk from 'chalk';
12
12
  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';
13
13
  import { addCustomDomain, configureBucketLifecycle, createBucket, deployWorker, enableWorkersDev, findZoneId, hashWorkerScript, putWorkerSecret, updateWorker, WORKER_PHOENIX_ID_BASE_SECRET, setWorkerSecret, } from '../lib/share/provision.js';
14
- import { publishFile, resolveShareUsername, parseMetaEntries, resolveShareVisibility, } from '../lib/share/publish.js';
14
+ import { publishFile, resolveShareUsername, parseMetaEntries, sanitizeLabel, resolveShareVisibility, scanShareContent, formatSensitiveContentError, SHARE_VISIBILITY_LEVELS, } from '../lib/share/publish.js';
15
15
  import { deleteShare, resolveDeleteTarget } from '../lib/share/delete.js';
16
16
  import { renderWorkerScript } from '../lib/share/worker-template.js';
17
17
  import { analyticsEnabled } from '../lib/share/analytics.js';
@@ -55,14 +55,63 @@ export function shareTemplateStatus(cfg) {
55
55
  return 'unknown';
56
56
  return cfg.templateHash === hashWorkerScript(renderWorkerScript()) ? 'current' : 'outdated';
57
57
  }
58
+ export async function runShareEdit(target, opts) {
59
+ // Same public-listing gate as publish: label + every metadata value are
60
+ // world-readable in the gallery and `share list --list-json`.
61
+ if (opts.force !== true) {
62
+ const hits = [
63
+ ...(typeof opts.label === 'string' ? scanShareContent(opts.label) : []),
64
+ ...Object.values(opts.meta ?? {}).flatMap((value) => scanShareContent(value)),
65
+ ];
66
+ if (hits.length > 0)
67
+ throw new Error(formatSensitiveContentError(hits));
68
+ }
69
+ const backend = resolveShareBackend({ githubUser: opts.githubUser, config: opts.config, writeToken: opts.writeToken, byo: opts.byo, session: opts.session });
70
+ // me/org are Phoenix-only — surface a crisp login hint here rather than the
71
+ // Worker's generic 400 for a signed-out caller (BYO WRITE_TOKEN can only set
72
+ // public/unlisted).
73
+ if ((opts.visibility === 'me' || opts.visibility === 'org') && backend.kind !== 'managed') {
74
+ throw new Error(`Changing visibility to '${opts.visibility}' requires a Phoenix session. Run 'agents auth login'.`);
75
+ }
76
+ const { key } = await resolveDeleteTarget(target, { githubUser: backend.kind === 'managed' ? backend.namespace : opts.githubUser || backend.namespace });
77
+ const res = await (opts.fetchEdit ?? fetch)(`${backend.baseUrl.replace(/\/+$/, '')}/${key}`, {
78
+ method: 'PATCH',
79
+ headers: { authorization: `Bearer ${backend.token}`, 'content-type': 'application/json' },
80
+ body: JSON.stringify({
81
+ ...(opts.label !== undefined ? { label: opts.label } : {}),
82
+ ...(opts.visibility !== undefined ? { visibility: opts.visibility } : {}),
83
+ meta: opts.meta ?? {}, metaMode: opts.metaMode ?? 'merge', removeMeta: opts.removeMeta ?? [],
84
+ }),
85
+ });
86
+ const body = await res.text();
87
+ if (!res.ok) {
88
+ let detail = body;
89
+ try {
90
+ detail = String(JSON.parse(body).error ?? body);
91
+ }
92
+ catch { /* keep body */ }
93
+ throw new Error(`Share metadata edit failed (${res.status}): ${detail || 'unknown error'}`);
94
+ }
95
+ const result = JSON.parse(body);
96
+ // A Worker template that predates the visibility edit ignores the `visibility`
97
+ // field and 200s without echoing it back — which would otherwise report a
98
+ // silent success while the page's visibility never changed. Fail loud with the
99
+ // update path instead (visibility rode in on RUSH-3135's metadata-edit route).
100
+ if (opts.visibility !== undefined && result.visibility === undefined) {
101
+ throw new Error("This share endpoint's Worker doesn't support in-place visibility changes yet — it predates them. " +
102
+ "Run 'agents artifacts share update' to deploy the current Worker template, then retry " +
103
+ "('agents artifacts share status' shows whether an update is due).");
104
+ }
105
+ return result;
106
+ }
58
107
  /** Shown whenever the deployed Worker has no `?format=json` listing route — an
59
108
  * endpoint provisioned before this feature. Points at the RUSH-2449 update path
60
109
  * (`agents artifacts share update`) instead of letting the caller hit a 404 or an
61
110
  * HTML body and get a confusing parse error. */
62
111
  const OUTDATED_TEMPLATE_HINT = 'Your deployed share Worker has no machine-readable listing route — it predates `agents artifacts share list`. ' +
63
112
  'Run `agents artifacts share update` to deploy the current Worker template, then retry (`agents artifacts share status` shows whether an update is due).';
64
- async function defaultListingFetch(url) {
65
- const res = await fetch(url, { headers: { accept: 'application/json' } });
113
+ async function defaultListingFetch(url, headers) {
114
+ const res = await fetch(url, { headers: { accept: 'application/json', ...(headers || {}) } });
66
115
  return { status: res.status, contentType: res.headers.get('content-type') ?? '', body: await res.text() };
67
116
  }
68
117
  /** Normalize the Worker's `meta` field (arbitrary `--meta key=value` entries,
@@ -108,6 +157,9 @@ export function parseShareListing(user, body) {
108
157
  host: item.host == null ? null : String(item.host),
109
158
  repo: item.repo == null ? null : String(item.repo),
110
159
  revisionCount: typeof item.revisionCount === 'number' ? item.revisionCount : 0,
160
+ visibility: item.visibility === 'unlisted' || item.visibility === 'me' || item.visibility === 'org'
161
+ ? item.visibility
162
+ : 'public',
111
163
  meta: parseMetaField(item.meta),
112
164
  };
113
165
  });
@@ -148,9 +200,21 @@ export async function runShareList(opts = {}) {
148
200
  }
149
201
  }
150
202
  const user = await namespaceForBackend(backend, opts.githubUser);
151
- const listUrl = `${backend.baseUrl.replace(/\/+$/, '')}/${encodeURIComponent(user)}?format=json`;
203
+ const scope = opts.scope ?? 'public';
204
+ const includeHidden = scope !== 'public';
205
+ const query = new URLSearchParams({ format: 'json' });
206
+ if (includeHidden)
207
+ query.set('scope', 'mine');
208
+ const listUrl = `${backend.baseUrl.replace(/\/+$/, '')}/${encodeURIComponent(user)}?${query.toString()}`;
209
+ const listingHeaders = {};
210
+ if (includeHidden) {
211
+ if (!backend.token) {
212
+ throw new Error(`Listing ${scope} shares requires owner authentication. Run 'agents auth login' for the managed endpoint, or ensure SHARE_WRITE_TOKEN / 'agents artifacts share join' is configured for BYO.`);
213
+ }
214
+ listingHeaders.authorization = `Bearer ${backend.token}`;
215
+ }
152
216
  const fetchListing = opts.fetchListing ?? defaultListingFetch;
153
- const res = await fetchListing(listUrl);
217
+ const res = await fetchListing(listUrl, listingHeaders);
154
218
  if (res.status === 404) {
155
219
  // A single-segment path 404s either because the namespace is genuinely empty
156
220
  // (current template — the listing route gates on the namespace holding objects,
@@ -177,14 +241,23 @@ export async function runShareList(opts = {}) {
177
241
  throw new Error(OUTDATED_TEMPLATE_HINT);
178
242
  }
179
243
  const parsed = parseShareListing(user, res.body);
180
- return applyShareListFilters(parsed, opts);
244
+ return applyShareListFilters(parsed, {
245
+ agent: opts.agent,
246
+ session: opts.session,
247
+ label: opts.label,
248
+ meta: opts.meta,
249
+ scope,
250
+ });
181
251
  }
182
252
  /** Client-side filtering over an already-fetched listing (RUSH-2683) — the
183
253
  * Worker has no query surface for this, so it narrows the fetched set instead
184
254
  * of a second round trip. `count` reflects the FILTERED set, matching what the
185
255
  * caller actually sees. */
186
- function applyShareListFilters(result, filters) {
256
+ export function applyShareListFilters(result, filters) {
187
257
  let objects = result.objects;
258
+ if (filters.scope && filters.scope !== 'all') {
259
+ objects = objects.filter((o) => o.visibility === filters.scope);
260
+ }
188
261
  if (filters.agent) {
189
262
  const needle = filters.agent.toLowerCase();
190
263
  objects = objects.filter((o) => (o.agent ?? '').toLowerCase() === needle);
@@ -196,6 +269,9 @@ function applyShareListFilters(result, filters) {
196
269
  const needle = filters.label.toLowerCase();
197
270
  objects = objects.filter((o) => (o.label ?? '').toLowerCase().includes(needle));
198
271
  }
272
+ if (filters.meta) {
273
+ objects = objects.filter((o) => Object.entries(filters.meta).every(([key, value]) => o.meta[key] === value));
274
+ }
199
275
  return { user: result.user, count: objects.length, objects };
200
276
  }
201
277
  /** Human-readable bytes, e.g. `1.2 KB`, `640 B`. */
@@ -219,7 +295,9 @@ export function formatShareList(result, json = false) {
219
295
  chalk.dim(` ${result.count} published ${result.count === 1 ? 'page' : 'pages'}`);
220
296
  const rows = result.objects.map((o) => {
221
297
  const when = o.publishedAt ? o.publishedAt.slice(0, 10) : 'unknown';
222
- const bits = [when, formatBytes(o.size)];
298
+ const visibility = o.visibility ?? 'public';
299
+ const visibilityBit = visibility === 'public' ? chalk.green('public') : chalk.yellow(visibility);
300
+ const bits = [visibilityBit, when, formatBytes(o.size)];
223
301
  if (o.agent)
224
302
  bits.push(o.agent);
225
303
  if (o.revisionCount > 0)
@@ -341,6 +419,18 @@ export function formatShareDeleteResult(result, json = false) {
341
419
  }
342
420
  return lines.join('\n');
343
421
  }
422
+ /** Human summary of an in-place visibility change. */
423
+ function formatShareVisibilityResult(result, requested) {
424
+ const now = result.visibility ?? requested;
425
+ const from = result.previousVisibility && result.previousVisibility !== now
426
+ ? `${chalk.dim(result.previousVisibility)} → `
427
+ : '';
428
+ return [
429
+ `${chalk.green('✓')} visibility ${from}${chalk.bold(now)}`,
430
+ chalk.dim(` ${result.url}`),
431
+ chalk.dim(' body unchanged — no revision created'),
432
+ ].join('\n');
433
+ }
344
434
  /** Shared handler for `agents artifacts share delete <targets...>` and the
345
435
  * nested `agents artifacts unshare <targets...>` alias. Deletes each target independently and
346
436
  * continues past a failed one (rm-style), reporting all results and exiting
@@ -426,6 +516,36 @@ const SHARE_DELETE_NOTES = `
426
516
 
427
517
  agents artifacts share delete === agents artifacts unshare (same command, different name).
428
518
  `;
519
+ const SHARE_VISIBILITY_EXAMPLES = `
520
+ # Take a public page private to just you (Phoenix session required for me/org)
521
+ agents artifacts share visibility https://share.agents-cli.sh/octocat/q3-plan me
522
+
523
+ # Make it public again — by <user>/<slug> or a bare slug in your namespace
524
+ agents artifacts share visibility octocat/q3-plan public
525
+ agents artifacts share visibility q3-plan unlisted
526
+
527
+ # Share with your Phoenix org (rejected on a public-inbox email domain)
528
+ agents artifacts share visibility q3-plan org
529
+
530
+ # Machine-readable result
531
+ agents artifacts share visibility q3-plan me --visibility-json
532
+ `;
533
+ const SHARE_VISIBILITY_NOTES = `
534
+ Changes an ALREADY-published page's visibility in place: the slug — and so the
535
+ URL — is preserved. It re-stamps only the visibility on the stored object; the
536
+ body, provenance, label, and --meta are untouched, so — like 'agents artifacts
537
+ share edit' — this is a metadata-only rewrite and creates no revision.
538
+
539
+ public is listed in the gallery; unlisted is a capability URL (GET still 200,
540
+ X-Robots-Tag: noindex, hidden from gallery/list); me is visible only to the
541
+ signed-in owner; org is visible to members of the same Phoenix organization.
542
+ me and org require a Phoenix session — run 'agents auth login' first — and org
543
+ is refused on a public-inbox email domain (gmail.com, outlook.com, …).
544
+
545
+ This hits the live endpoint via the owner's Phoenix session (or the BYO
546
+ WRITE_TOKEN for public/unlisted). It does not re-run the pre-publish
547
+ sensitive-content scan — the body is unchanged.
548
+ `;
429
549
  /**
430
550
  * Register the `share` subtree under its parent group — `agents artifacts share`
431
551
  * (see commands/artifacts.ts). `agents artifacts unshare` is the nested alias
@@ -436,6 +556,10 @@ const SHARE_DELETE_NOTES = `
436
556
  export function registerShareCommands(artifactsCmd) {
437
557
  const shareCmd = artifactsCmd
438
558
  .command('share')
559
+ // Child task commands intentionally reuse publish vocabulary (`edit --label`,
560
+ // `list --meta`). Keep option ownership positional within this subtree so
561
+ // Commander does not silently resolve those flags to the parent publisher.
562
+ .enablePositionalOptions()
439
563
  .description('Publish an HTML file to a shareable link — managed if signed in, otherwise your Cloudflare R2.')
440
564
  .argument('[file]', 'file to publish (HTML or any static asset)')
441
565
  .option('--slug <slug>', 'URL slug override (default: stable slug of the artifact title, then filename)')
@@ -553,6 +677,43 @@ ${SHARE_DELETE_NOTES}
553
677
  shareDeleteCmd.action(async (targets, opts) => {
554
678
  await runShareDelete(targets, opts);
555
679
  });
680
+ // `agents artifacts share visibility <target> <level>` — change an
681
+ // already-published page's visibility in place. Kept in its own block; it
682
+ // reuses the delete target parser and the same PATCH metadata-edit route as
683
+ // `share edit` (visibility is metadata), so it re-stamps the stored object
684
+ // rather than re-publishing — a re-publish over the PUT path can't express an
685
+ // in-place change (GET injects the attribution bar at serve time and never
686
+ // strips it, and the served body carries no provenance/label/--meta back to
687
+ // re-send). --for-user / --visibility-json (not --github-user / --json): the
688
+ // parent `share <file>` already owns both long names, and commander resolves a
689
+ // child option's long name against the whole ancestor chain, silently dropping
690
+ // a same-named child (RUSH-2687) — the same rename the delete/list blocks make.
691
+ const shareVisibilityCmd = shareCmd
692
+ .command('visibility')
693
+ .description("Change an already-published page's visibility in place (public | unlisted | me | org). The slug/URL is preserved; the body is untouched, so no revision is created.")
694
+ .addArgument(new Argument('<target>', 'the published page: a full URL, <user>/<slug>, or a bare slug in your namespace'))
695
+ .addArgument(new Argument('<level>', 'new visibility').choices([...SHARE_VISIBILITY_LEVELS]))
696
+ .option('--for-user <user>', 'GitHub username for resolving a bare-slug target (default: resolved from gh/git config)')
697
+ .option('--visibility-json', 'emit a machine-readable result')
698
+ .action(async (target, level, opts) => {
699
+ try {
700
+ const visibility = level;
701
+ const result = await runShareEdit(target, { visibility, githubUser: opts.forUser });
702
+ if (opts.visibilityJson) {
703
+ console.log(JSON.stringify(result, null, 2));
704
+ return;
705
+ }
706
+ console.log(formatShareVisibilityResult(result, visibility));
707
+ }
708
+ catch (e) {
709
+ console.error(chalk.red(e.message));
710
+ process.exitCode = 1;
711
+ }
712
+ });
713
+ setHelpSections(shareVisibilityCmd, {
714
+ examples: SHARE_VISIBILITY_EXAMPLES,
715
+ notes: SHARE_VISIBILITY_NOTES,
716
+ });
556
717
  shareCmd
557
718
  .command('join')
558
719
  .description('Use an existing synced share endpoint and write token (no provisioning).')
@@ -664,20 +825,33 @@ ${SHARE_DELETE_NOTES}
664
825
  // share.test.ts). Matches the `revisions` precedent (--for-user/
665
826
  // --revisions-json) and the `delete`/`unshare` precedent (--for-user/
666
827
  // --delete-json) below.
828
+ //
829
+ // `--visibility` is also owned by the parent `share <file>` command (publish),
830
+ // so the listing filter is named `--scope` here; `--all` is the convenience
831
+ // alias for `--scope all`.
667
832
  .option('--for-user <user>', 'GitHub username whose namespace to list (default: resolved from gh/git config)')
833
+ .addOption(new Option('--scope <level>', 'visibility filter: public (default), unlisted, me, org, or all')
834
+ .choices(['public', 'unlisted', 'me', 'org', 'all'])
835
+ .default('public'))
836
+ .option('--all', "list every page including hidden unlisted/me/org shares (alias for --scope all)")
668
837
  .option('--agent <name>', 'filter to shares published by this agent/harness (case-insensitive)')
669
838
  .option('--session <id>', 'filter to shares published from this session id')
670
839
  // Named --label-contains, not --label: `share <file>` (the parent) already owns
671
840
  // `--label`/`--title`, same collision class as --for-user/--list-json above.
672
841
  .option('--label-contains <substr>', 'filter to shares whose label contains this text (case-insensitive)')
673
- .option('--list-json', 'emit the machine-readable listing (slug, url, size, contentType, publishedAt, expiresAt, label, agent, session, host, repo, revisionCount, meta)')
842
+ .option('--meta <key=value>', 'filter by an exact arbitrary metadata entry (repeatable)', (v, p) => [...p, v], [])
843
+ .option('--list-json', 'emit the machine-readable listing (slug, url, size, contentType, publishedAt, expiresAt, label, visibility, agent, session, host, repo, revisionCount, meta)')
674
844
  .action(async (opts) => {
675
845
  try {
846
+ const scope = opts.all ? 'all' : (opts.scope ?? 'public');
847
+ const parentMeta = shareCmd.opts().meta ?? [];
676
848
  const result = await runShareList({
677
849
  githubUser: opts.forUser,
850
+ scope,
678
851
  agent: opts.agent,
679
852
  session: opts.session,
680
853
  label: opts.labelContains,
854
+ meta: parseMetaEntries(opts.meta.length ? opts.meta : parentMeta),
681
855
  });
682
856
  console.log(formatShareList(result, Boolean(opts.listJson)));
683
857
  }
@@ -691,6 +865,11 @@ ${SHARE_DELETE_NOTES}
691
865
  # Everything you've published, newest first
692
866
  agents artifacts share list
693
867
 
868
+ # Include your hidden pages (unlisted / me / org) so you can see everything
869
+ agents artifacts share list --all
870
+ agents artifacts share list --scope me
871
+ agents artifacts share list --scope unlisted
872
+
694
873
  # Machine-readable — e.g. pull every still-public URL with jq
695
874
  agents artifacts share list --list-json | jq -r '.objects[].url'
696
875
 
@@ -700,18 +879,83 @@ ${SHARE_DELETE_NOTES}
700
879
  # Narrow by who/what published it
701
880
  agents artifacts share list --agent claude
702
881
  agents artifacts share list --label-contains "fleet plan"
882
+ agents artifacts share list --meta kind=plan --meta status=final
703
883
  `,
704
884
  notes: `
705
885
  Lists the ACTIVE pages in your namespace — expired links and the sibling .png OG
706
- covers are omitted (it mirrors the public gallery). Signed-in users list the
707
- managed endpoint (share.agents-cli.sh/<handle>); otherwise BYO. It reads the
708
- endpoint's JSON listing route, which ships with the current Worker template. If
709
- a BYO Worker predates this feature the command says so and points you at 'agents
710
- artifacts share update' (RUSH-2449) rather than returning a wrong or empty result
711
- — see 'agents artifacts share status' for whether an update is due.
886
+ covers are omitted. By default this mirrors the public gallery (public pages only).
887
+ Use --scope unlisted|me|org or --all to include your hidden pages; those scopes
888
+ send the owner's bearer and a 'scope=mine' hint to the Worker's JSON listing route,
889
+ which returns hidden pages ONLY after verifying the caller owns the namespace.
890
+ Signed-in users list the managed endpoint (share.agents-cli.sh/<handle>); otherwise
891
+ BYO. It reads the endpoint's JSON listing route, which ships with the current
892
+ Worker template. If a BYO Worker predates this feature the command says so and
893
+ points you at 'agents artifacts share update' (RUSH-2449) rather than returning a
894
+ wrong or empty result — see 'agents artifacts share status' for whether an update
895
+ is due.
896
+
897
+ --agent/--session/--label-contains/--meta filter the fetched listing client-side;
898
+ --list-json's count reflects the filtered set. Each human row shows the page's
899
+ visibility so public vs hidden is obvious at a glance.
900
+ `,
901
+ });
902
+ const shareEditCmd = shareCmd
903
+ .command('edit <target>')
904
+ .description('Edit a published share\'s label or arbitrary metadata without republishing its body.')
905
+ .option('--for-user <user>', 'namespace for resolving a bare slug (default: your namespace)')
906
+ .option('--label <text>', 'set or replace the display label')
907
+ .option('--remove-label', 'remove the display label')
908
+ .option('--meta <key=value>', 'merge a metadata entry (repeatable)', (v, p) => [...p, v], [])
909
+ .option('--replace-meta <key=value>', 'replace all arbitrary metadata with these entries (repeatable)', (v, p) => [...p, v], [])
910
+ .option('--remove-meta <key>', 'remove an arbitrary metadata key (repeatable)', (v, p) => [...p, v], [])
911
+ .option('--edit-json', 'emit the machine-readable edit result')
912
+ .action(async (target, opts) => {
913
+ try {
914
+ const parent = shareCmd.opts();
915
+ const label = opts.label ?? parent.label;
916
+ const mergeMeta = opts.meta.length ? opts.meta : (parent.meta ?? []);
917
+ if (label !== undefined && opts.removeLabel)
918
+ throw new Error('--label and --remove-label are mutually exclusive.');
919
+ if (mergeMeta.length && opts.replaceMeta.length)
920
+ throw new Error('--meta and --replace-meta are mutually exclusive.');
921
+ if (opts.replaceMeta.length && opts.removeMeta.length)
922
+ throw new Error('--replace-meta and --remove-meta are mutually exclusive.');
923
+ if (label === undefined && !opts.removeLabel && !mergeMeta.length && !opts.replaceMeta.length && !opts.removeMeta.length)
924
+ throw new Error('Nothing to edit. Pass --label, --remove-label, --meta, --replace-meta, or --remove-meta.');
925
+ const removeMeta = opts.removeMeta.map((key) => Object.keys(parseMetaEntries([`${key}=x`]))[0]);
926
+ const result = await runShareEdit(target, {
927
+ githubUser: opts.forUser,
928
+ label: opts.removeLabel ? null : label === undefined ? undefined : sanitizeLabel(label),
929
+ meta: parseMetaEntries(opts.replaceMeta.length ? opts.replaceMeta : mergeMeta),
930
+ metaMode: opts.replaceMeta.length ? 'replace' : 'merge',
931
+ removeMeta,
932
+ // Parent `share --force` (RUSH-2687: a child --force is silently dropped).
933
+ force: parent.force,
934
+ });
935
+ console.log(opts.editJson ? JSON.stringify(result, null, 2) : chalk.green(`updated ${result.url}`));
936
+ }
937
+ catch (e) {
938
+ console.error(chalk.red(e.message));
939
+ process.exitCode = 1;
940
+ }
941
+ });
942
+ setHelpSections(shareEditCmd, {
943
+ examples: `
944
+ # Change only the gallery/list title
945
+ agents artifacts share edit q3-plan --label "Q3 fleet plan"
712
946
 
713
- --agent/--session/--label-contains filter the fetched listing client-side;
714
- --list-json's count reflects the filtered set.
947
+ # Merge metadata, or deliberately replace/remove it
948
+ agents artifacts share edit q3-plan --meta status=final --meta ticket=PHNX-3278
949
+ agents artifacts share edit q3-plan --replace-meta kind=plan --replace-meta status=final
950
+ agents artifacts share edit q3-plan --remove-meta status
951
+ `,
952
+ notes: `
953
+ Metadata edits preserve the exact published body, content type/HTTP metadata,
954
+ publication time, visibility, expiry, provenance, cover, and retained revisions. --meta merges;
955
+ --replace-meta replaces all arbitrary metadata; --remove-meta deletes named keys.
956
+ Reserved Worker/provenance keys cannot be changed through metadata flags.
957
+ Edited labels and metadata values are scanned for emails and credential-shaped
958
+ strings the same way publish is — pass --force to write them anyway.
715
959
  `,
716
960
  });
717
961
  const shareRevisionsCmd = shareCmd
@@ -48,6 +48,13 @@ export declare function showLeasedBoxesSection(opts: {
48
48
  all?: boolean;
49
49
  stats?: boolean;
50
50
  }): boolean;
51
+ /**
52
+ * Remote argv for `agents ssh <slug>` into a leased crabbox. Crabbox ssh does
53
+ * not go through {@link buildSshInvocation}, so this stamps the same
54
+ * AGENTS_FLEET_REMOTE consent marker a registered-device ssh would (PHNX-3065).
55
+ * Exported so the branch is unit-testable without a live crabbox.
56
+ */
57
+ export declare function leasedBoxRemoteCmd(cmd: string[]): string[];
51
58
  /**
52
59
  * Race `fanOut` against an overall wall-clock deadline (RUSH-2041).
53
60
  *
@@ -34,7 +34,7 @@ import { clearPendingSentinel } from '../lib/devices/pending.js';
34
34
  import { getDeviceDiscoveryStatus, setDeviceDiscoveryStatus } from '../lib/devices/discovery-policy.js';
35
35
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
36
36
  import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
37
- import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, writeAskpassShim, } from '../lib/devices/connect.js';
37
+ import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, isAgentsBrowserDrive, markFleetRemote, writeAskpassShim, } from '../lib/devices/connect.js';
38
38
  import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
39
39
  import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
40
40
  import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
@@ -351,9 +351,21 @@ function trySshLeasedBox(name, cmd) {
351
351
  console.error(chalk.red(`Leased box '${name}' is not reachable yet (status: ${boxStatus(box)}).`));
352
352
  process.exit(1);
353
353
  }
354
- const res = spawnSync(sshArgv[0], [...sshArgv.slice(1), ...cmd], { stdio: 'inherit' });
354
+ // Crabbox ssh does not go through buildSshInvocation; stamp the same
355
+ // consent marker so a browser drive on a leased box is gated too.
356
+ const remoteCmd = leasedBoxRemoteCmd(cmd);
357
+ const res = spawnSync(sshArgv[0], [...sshArgv.slice(1), ...remoteCmd], { stdio: 'inherit' });
355
358
  process.exit(res.status ?? 1);
356
359
  }
360
+ /**
361
+ * Remote argv for `agents ssh <slug>` into a leased crabbox. Crabbox ssh does
362
+ * not go through {@link buildSshInvocation}, so this stamps the same
363
+ * AGENTS_FLEET_REMOTE consent marker a registered-device ssh would (PHNX-3065).
364
+ * Exported so the branch is unit-testable without a live crabbox.
365
+ */
366
+ export function leasedBoxRemoteCmd(cmd) {
367
+ return isAgentsBrowserDrive(cmd) ? markFleetRemote(cmd, { shell: 'posix' }) : cmd;
368
+ }
357
369
  /** Resolve a device or exit with a clear error. */
358
370
  async function mustGetDevice(name) {
359
371
  const d = await getDevice(name);
@@ -2412,6 +2424,10 @@ An interactive login with no command mirrors the home-relative directory you
2412
2424
  launched from — 'agents ssh yosemite-s0' from ~/src/app lands in ~/src/app on
2413
2425
  the target when it exists, else the remote home. Same portable-cwd rule as
2414
2426
  'agents run --device'. Passing a command keeps the remote home.
2427
+
2428
+ An 'agents browser …', 'ag browser …', or standalone 'browser …' command is
2429
+ stamped AGENTS_FLEET_REMOTE so the target's browser.remote-control consent
2430
+ gate applies, same as 'agents browser <verb> --device <name>'.
2415
2431
  `)
2416
2432
  .action(async (name, cmd) => {
2417
2433
  // Hidden askpass bridge: ssh execs the shim, which re-invokes us here.
@@ -97,6 +97,20 @@ export function registerStatusCommand(syncCmd) {
97
97
  if (status.totals.orphan > 0) {
98
98
  console.log(chalk.gray(` (${status.totals.orphan} orphan${status.totals.orphan === 1 ? '' : 's'} — run \`agents prune cleanup\`)`));
99
99
  }
100
+ // Config drift is its OWN class, not a per-agent "N missing": a box that has
101
+ // not folded its device-scoped state still carries a stale top-level header or
102
+ // a lingering central fleet/hosts/accounts/browser block. Surface it distinctly
103
+ // so an un-drained box is SEEN here instead of via a mystery pull conflict
104
+ // (PHNX-3315).
105
+ if (status.config.staleHeader || status.config.centralLeaks.length > 0) {
106
+ console.log(` ${'config (device-scoped)'.padEnd(28)} ${chalk.yellow('not drained — folds automatically on normal `agents` use here (idempotent)')}`);
107
+ if (status.config.staleHeader) {
108
+ console.log(chalk.gray(' · top-level agents.yaml header is stale (pre-rename)'));
109
+ }
110
+ for (const leak of status.config.centralLeaks) {
111
+ console.log(chalk.gray(` · central ${leak} should be device-scoped`));
112
+ }
113
+ }
100
114
  // Hand off to the shared interactive/apply flow (summary already printed above).
101
115
  await promptDriftSync({ cwd, yes: opts.yes, status, quiet: true });
102
116
  });
@@ -81,7 +81,9 @@ export declare function parseResourceSections(options: {
81
81
  * agents-cli extension's "resume current session in best available version"
82
82
  * command).
83
83
  */
84
- export declare function collectAgentsJson(filterAgentId?: AgentId, resourceSections?: Set<ResourceSection>): Promise<ViewJsonAgent[]>;
84
+ export declare function collectAgentsJson(filterAgentId?: AgentId, resourceSections?: Set<ResourceSection>, opts?: {
85
+ forceRefresh?: boolean;
86
+ }): Promise<ViewJsonAgent[]>;
85
87
  /**
86
88
  * Identity key for duplicate-install detection. Prefers accountKey — which
87
89
  * encodes account AND org — over the bare email: two installs can share an
@@ -1209,7 +1209,7 @@ export function parseResourceSections(options, jsonMode) {
1209
1209
  * agents-cli extension's "resume current session in best available version"
1210
1210
  * command).
1211
1211
  */
1212
- export async function collectAgentsJson(filterAgentId, resourceSections) {
1212
+ export async function collectAgentsJson(filterAgentId, resourceSections, opts) {
1213
1213
  const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
1214
1214
  const authCache = readAuthHealthCache();
1215
1215
  const host = machineId();
@@ -1230,7 +1230,7 @@ export async function collectAgentsJson(filterAgentId, resourceSections) {
1230
1230
  home,
1231
1231
  cliVersion: version,
1232
1232
  info,
1233
- })));
1233
+ })), { forceRefresh: opts?.forceRefresh === true });
1234
1234
  const mergeCanonical = (info) => {
1235
1235
  const key = getUsageLookupKey(info);
1236
1236
  if (!key)
@@ -1273,6 +1273,7 @@ export async function collectAgentsJson(filterAgentId, resourceSections) {
1273
1273
  plan: info.plan,
1274
1274
  usageStatus: info.usageStatus,
1275
1275
  overageCredits: info.overageCredits,
1276
+ usageError: usageInfo?.error ?? null,
1276
1277
  windows: snapshot
1277
1278
  ? snapshot.windows.map((w) => ({
1278
1279
  key: w.key,
@@ -1562,7 +1563,7 @@ export async function viewAction(agentArg, options) {
1562
1563
  return;
1563
1564
  }
1564
1565
  if (json) {
1565
- const data = await collectAgentsJson(undefined, resourceSections);
1566
+ const data = await collectAgentsJson(undefined, resourceSections, { forceRefresh });
1566
1567
  console.log(JSON.stringify(data, null, 2));
1567
1568
  return;
1568
1569
  }
@@ -1624,7 +1625,7 @@ export async function viewAction(agentArg, options) {
1624
1625
  if (json) {
1625
1626
  // --json ignores the @version suffix, but --resources/--detailed (or a
1626
1627
  // section flag) now attach each version's resource inventory + sync-state.
1627
- const data = await collectAgentsJson(agentId, resourceSections);
1628
+ const data = await collectAgentsJson(agentId, resourceSections, { forceRefresh });
1628
1629
  console.log(JSON.stringify(data[0] ?? { agent: agentId, versions: [], harnesses: [] }, null, 2));
1629
1630
  return;
1630
1631
  }