@phnx-labs/agents-cli 1.22.53 → 1.22.55

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 (166) hide show
  1. package/CHANGELOG.md +208 -0
  2. package/README.md +59 -9
  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/routines.js +31 -2
  25. package/dist/commands/secrets.js +33 -14
  26. package/dist/commands/sessions.d.ts +20 -12
  27. package/dist/commands/sessions.js +64 -20
  28. package/dist/commands/setup-accounts.d.ts +8 -0
  29. package/dist/commands/setup-accounts.js +47 -0
  30. package/dist/commands/setup.d.ts +1 -1
  31. package/dist/commands/setup.js +11 -2
  32. package/dist/commands/share.d.ts +79 -3
  33. package/dist/commands/share.js +347 -18
  34. package/dist/commands/ssh.d.ts +7 -0
  35. package/dist/commands/ssh.js +18 -2
  36. package/dist/commands/status.js +14 -0
  37. package/dist/commands/view.d.ts +11 -1
  38. package/dist/commands/view.js +35 -7
  39. package/dist/lib/account-registry.js +15 -3
  40. package/dist/lib/accounting/rotate.d.ts +20 -6
  41. package/dist/lib/accounting/rotate.js +38 -7
  42. package/dist/lib/accounting/usage.d.ts +68 -1
  43. package/dist/lib/accounting/usage.js +116 -10
  44. package/dist/lib/agent-spec/agents.d.ts +5 -2
  45. package/dist/lib/agent-spec/agents.js +25 -7
  46. package/dist/lib/analytics/mix-commands.js +12 -6
  47. package/dist/lib/auth-mint.d.ts +150 -0
  48. package/dist/lib/auth-mint.js +434 -0
  49. package/dist/lib/browser/cdp.d.ts +1 -1
  50. package/dist/lib/browser/cdp.js +1 -1
  51. package/dist/lib/browser/ffmpeg.d.ts +12 -0
  52. package/dist/lib/browser/ffmpeg.js +184 -0
  53. package/dist/lib/browser/remote-control.d.ts +9 -7
  54. package/dist/lib/browser/remote-control.js +9 -7
  55. package/dist/lib/browser/service.js +119 -25
  56. package/dist/lib/claude-account-token.d.ts +10 -0
  57. package/dist/lib/claude-account-token.js +14 -4
  58. package/dist/lib/config-drift.d.ts +37 -0
  59. package/dist/lib/config-drift.js +72 -0
  60. package/dist/lib/daemon/auth-sync-service.d.ts +19 -0
  61. package/dist/lib/daemon/auth-sync-service.js +34 -0
  62. package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
  63. package/dist/lib/daemon/browser-task-reap-service.js +26 -0
  64. package/dist/lib/daemon/daemon.js +87 -176
  65. package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
  66. package/dist/lib/daemon/heartbeat-service.js +26 -0
  67. package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
  68. package/dist/lib/daemon/monitor-engine-service.js +15 -7
  69. package/dist/lib/daemon/runner.d.ts +15 -0
  70. package/dist/lib/daemon/runner.js +23 -0
  71. package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
  72. package/dist/lib/daemon/secrets-broker-service.js +17 -32
  73. package/dist/lib/daemon/service.d.ts +2 -2
  74. package/dist/lib/daemon/service.js +1 -1
  75. package/dist/lib/daemon/session-state-service.d.ts +21 -0
  76. package/dist/lib/daemon/session-state-service.js +34 -0
  77. package/dist/lib/daemon/supervisor.d.ts +17 -7
  78. package/dist/lib/daemon/supervisor.js +87 -14
  79. package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
  80. package/dist/lib/daemon/tmux-reap-service.js +28 -0
  81. package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
  82. package/dist/lib/daemon/webhook-receiver-service.js +17 -0
  83. package/dist/lib/daemon-services.d.ts +1 -1
  84. package/dist/lib/daemon-services.js +25 -0
  85. package/dist/lib/device-config.d.ts +3 -3
  86. package/dist/lib/device-config.js +5 -5
  87. package/dist/lib/devices/connect.d.ts +26 -0
  88. package/dist/lib/devices/connect.js +48 -1
  89. package/dist/lib/devices/doctor-findings.d.ts +5 -1
  90. package/dist/lib/devices/doctor-findings.js +19 -1
  91. package/dist/lib/devices/harness-inventory.js +5 -2
  92. package/dist/lib/exec.d.ts +28 -0
  93. package/dist/lib/exec.js +73 -7
  94. package/dist/lib/feed/feed.d.ts +1 -1
  95. package/dist/lib/feed/feed.js +23 -1
  96. package/dist/lib/feed-broadcast.js +1 -1
  97. package/dist/lib/fleet/apply.d.ts +11 -0
  98. package/dist/lib/fleet/apply.js +23 -3
  99. package/dist/lib/fleet/auth-sync.js +5 -3
  100. package/dist/lib/help.d.ts +9 -0
  101. package/dist/lib/help.js +29 -1
  102. package/dist/lib/hosts/passthrough.d.ts +1 -10
  103. package/dist/lib/hosts/passthrough.js +1 -13
  104. package/dist/lib/installations/versions.js +9 -1
  105. package/dist/lib/linux-userns.d.ts +58 -0
  106. package/dist/lib/linux-userns.js +116 -0
  107. package/dist/lib/memory.d.ts +26 -0
  108. package/dist/lib/memory.js +80 -1
  109. package/dist/lib/monitors/config.d.ts +11 -0
  110. package/dist/lib/monitors/config.js +8 -0
  111. package/dist/lib/monitors/engine.d.ts +5 -1
  112. package/dist/lib/monitors/engine.js +13 -4
  113. package/dist/lib/monitors/state.d.ts +37 -1
  114. package/dist/lib/monitors/state.js +79 -4
  115. package/dist/lib/permissions-registry.d.ts +2 -0
  116. package/dist/lib/permissions-registry.js +116 -14
  117. package/dist/lib/permissions.d.ts +5 -3
  118. package/dist/lib/permissions.js +25 -27
  119. package/dist/lib/profiles.d.ts +8 -7
  120. package/dist/lib/profiles.js +12 -0
  121. package/dist/lib/project-key.d.ts +9 -0
  122. package/dist/lib/project-key.js +11 -0
  123. package/dist/lib/scheduling/routines.d.ts +47 -0
  124. package/dist/lib/scheduling/routines.js +70 -1
  125. package/dist/lib/secrets/bundles.d.ts +35 -0
  126. package/dist/lib/secrets/bundles.js +78 -1
  127. package/dist/lib/secrets/push.d.ts +3 -8
  128. package/dist/lib/secrets/push.js +18 -14
  129. package/dist/lib/secrets/remote.d.ts +9 -18
  130. package/dist/lib/secrets/remote.js +11 -26
  131. package/dist/lib/secrets/reserved-sync.d.ts +65 -0
  132. package/dist/lib/secrets/reserved-sync.js +129 -0
  133. package/dist/lib/self-heal/checks/hook-manifest.d.ts +2 -0
  134. package/dist/lib/self-heal/checks/hook-manifest.js +56 -0
  135. package/dist/lib/self-heal/registry.js +4 -0
  136. package/dist/lib/self-heal/types.d.ts +1 -1
  137. package/dist/lib/session/active.js +1 -4
  138. package/dist/lib/session/db.d.ts +41 -5
  139. package/dist/lib/session/db.js +132 -30
  140. package/dist/lib/session/discover.d.ts +32 -4
  141. package/dist/lib/session/discover.js +119 -25
  142. package/dist/lib/session/insights.d.ts +14 -0
  143. package/dist/lib/session/insights.js +25 -2
  144. package/dist/lib/session/linear.js +1 -1
  145. package/dist/lib/session/shell-programs.d.ts +17 -0
  146. package/dist/lib/session/shell-programs.js +21 -0
  147. package/dist/lib/session/state.js +2 -1
  148. package/dist/lib/session/stream-render.js +2 -1
  149. package/dist/lib/session/tool-calls.js +2 -5
  150. package/dist/lib/session/trajectory-html.js +2 -1
  151. package/dist/lib/session/trajectory.js +3 -12
  152. package/dist/lib/session/types.d.ts +8 -0
  153. package/dist/lib/share/capture.js +11 -2
  154. package/dist/lib/share/publish.d.ts +56 -5
  155. package/dist/lib/share/publish.js +126 -18
  156. package/dist/lib/share/worker-template.d.ts +3 -12
  157. package/dist/lib/share/worker-template.js +860 -59
  158. package/dist/lib/startup/root-command.js +2 -1
  159. package/dist/lib/state.d.ts +16 -0
  160. package/dist/lib/state.js +178 -46
  161. package/dist/lib/sync-status.d.ts +4 -0
  162. package/dist/lib/sync-status.js +3 -0
  163. package/dist/lib/traces/classify.js +24 -19
  164. package/dist/lib/usage-refresh.js +2 -1
  165. package/dist/lib/view-types.d.ts +7 -0
  166. package/package.json +9 -2
@@ -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. */
@@ -123,6 +172,33 @@ export declare function runShareRevisions(target: string, opts?: {
123
172
  session?: PhoenixSession | null;
124
173
  fetchListing?: ListingFetchFn;
125
174
  }): Promise<ShareRevisionsResult>;
175
+ /** The resolved page URL plus a signed-in-owner URL that carries a one-time
176
+ * login ticket, so opening it activates the shared page's inline visibility
177
+ * control (PHNX-3370). */
178
+ export interface ShareOpenResult {
179
+ /** `<user>/<slug>` key of the target page. */
180
+ key: string;
181
+ /** The plain page URL (no ticket). */
182
+ url: string;
183
+ /** The page URL with `?phoenix_ticket=<t>` — open THIS to be the owner. */
184
+ ownerUrl: string;
185
+ }
186
+ /** Mint a one-time login ticket for the signed-in owner and build the URL that
187
+ * opens their own published page **as the owner**, so the inline visibility
188
+ * control is live in the browser. Managed backend only — the control is gated on
189
+ * the Phoenix identity, and BYO (WRITE_TOKEN) endpoints have no per-viewer login.
190
+ * `target` accepts the same three forms as `visibility`/`unshare` (a full URL,
191
+ * `<user>/<slug>`, or a bare slug in the caller's namespace). */
192
+ export declare function runShareOpen(target: string, opts?: {
193
+ githubUser?: string;
194
+ config?: ShareConfig;
195
+ session?: PhoenixSession | null;
196
+ /** DI seam — override the ticket-mint POST. */
197
+ fetchTicket?: (url: string, bearer: string) => Promise<{
198
+ status: number;
199
+ body: string;
200
+ }>;
201
+ }): Promise<ShareOpenResult>;
126
202
  export declare function formatShareRevisions(result: ShareRevisionsResult, json?: boolean): string;
127
203
  export declare function formatShareDeleteResult(result: DeleteShareResult, json?: boolean): string;
128
204
  interface ShareDeleteCliOpts {
@@ -7,17 +7,18 @@
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';
18
18
  import { phoenixIdBaseForDeploy, resolveShareBackend, } from '../lib/share/backend.js';
19
19
  import { resolveGitHubUsername } from '../lib/git.js';
20
20
  import { setHelpSections } from '../lib/help.js';
21
+ import { showUrl } from '../lib/open-url.js';
21
22
  export function formatSharePublishResult(result, json = false) {
22
23
  if (json)
23
24
  return JSON.stringify(result, null, 2);
@@ -55,14 +56,63 @@ export function shareTemplateStatus(cfg) {
55
56
  return 'unknown';
56
57
  return cfg.templateHash === hashWorkerScript(renderWorkerScript()) ? 'current' : 'outdated';
57
58
  }
59
+ export async function runShareEdit(target, opts) {
60
+ // Same public-listing gate as publish: label + every metadata value are
61
+ // world-readable in the gallery and `share list --list-json`.
62
+ if (opts.force !== true) {
63
+ const hits = [
64
+ ...(typeof opts.label === 'string' ? scanShareContent(opts.label) : []),
65
+ ...Object.values(opts.meta ?? {}).flatMap((value) => scanShareContent(value)),
66
+ ];
67
+ if (hits.length > 0)
68
+ throw new Error(formatSensitiveContentError(hits));
69
+ }
70
+ const backend = resolveShareBackend({ githubUser: opts.githubUser, config: opts.config, writeToken: opts.writeToken, byo: opts.byo, session: opts.session });
71
+ // me/org are Phoenix-only — surface a crisp login hint here rather than the
72
+ // Worker's generic 400 for a signed-out caller (BYO WRITE_TOKEN can only set
73
+ // public/unlisted).
74
+ if ((opts.visibility === 'me' || opts.visibility === 'org') && backend.kind !== 'managed') {
75
+ throw new Error(`Changing visibility to '${opts.visibility}' requires a Phoenix session. Run 'agents auth login'.`);
76
+ }
77
+ const { key } = await resolveDeleteTarget(target, { githubUser: backend.kind === 'managed' ? backend.namespace : opts.githubUser || backend.namespace });
78
+ const res = await (opts.fetchEdit ?? fetch)(`${backend.baseUrl.replace(/\/+$/, '')}/${key}`, {
79
+ method: 'PATCH',
80
+ headers: { authorization: `Bearer ${backend.token}`, 'content-type': 'application/json' },
81
+ body: JSON.stringify({
82
+ ...(opts.label !== undefined ? { label: opts.label } : {}),
83
+ ...(opts.visibility !== undefined ? { visibility: opts.visibility } : {}),
84
+ meta: opts.meta ?? {}, metaMode: opts.metaMode ?? 'merge', removeMeta: opts.removeMeta ?? [],
85
+ }),
86
+ });
87
+ const body = await res.text();
88
+ if (!res.ok) {
89
+ let detail = body;
90
+ try {
91
+ detail = String(JSON.parse(body).error ?? body);
92
+ }
93
+ catch { /* keep body */ }
94
+ throw new Error(`Share metadata edit failed (${res.status}): ${detail || 'unknown error'}`);
95
+ }
96
+ const result = JSON.parse(body);
97
+ // A Worker template that predates the visibility edit ignores the `visibility`
98
+ // field and 200s without echoing it back — which would otherwise report a
99
+ // silent success while the page's visibility never changed. Fail loud with the
100
+ // update path instead (visibility rode in on RUSH-3135's metadata-edit route).
101
+ if (opts.visibility !== undefined && result.visibility === undefined) {
102
+ throw new Error("This share endpoint's Worker doesn't support in-place visibility changes yet — it predates them. " +
103
+ "Run 'agents artifacts share update' to deploy the current Worker template, then retry " +
104
+ "('agents artifacts share status' shows whether an update is due).");
105
+ }
106
+ return result;
107
+ }
58
108
  /** Shown whenever the deployed Worker has no `?format=json` listing route — an
59
109
  * endpoint provisioned before this feature. Points at the RUSH-2449 update path
60
110
  * (`agents artifacts share update`) instead of letting the caller hit a 404 or an
61
111
  * HTML body and get a confusing parse error. */
62
112
  const OUTDATED_TEMPLATE_HINT = 'Your deployed share Worker has no machine-readable listing route — it predates `agents artifacts share list`. ' +
63
113
  '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' } });
114
+ async function defaultListingFetch(url, headers) {
115
+ const res = await fetch(url, { headers: { accept: 'application/json', ...(headers || {}) } });
66
116
  return { status: res.status, contentType: res.headers.get('content-type') ?? '', body: await res.text() };
67
117
  }
68
118
  /** Normalize the Worker's `meta` field (arbitrary `--meta key=value` entries,
@@ -108,6 +158,9 @@ export function parseShareListing(user, body) {
108
158
  host: item.host == null ? null : String(item.host),
109
159
  repo: item.repo == null ? null : String(item.repo),
110
160
  revisionCount: typeof item.revisionCount === 'number' ? item.revisionCount : 0,
161
+ visibility: item.visibility === 'unlisted' || item.visibility === 'me' || item.visibility === 'org'
162
+ ? item.visibility
163
+ : 'public',
111
164
  meta: parseMetaField(item.meta),
112
165
  };
113
166
  });
@@ -148,9 +201,21 @@ export async function runShareList(opts = {}) {
148
201
  }
149
202
  }
150
203
  const user = await namespaceForBackend(backend, opts.githubUser);
151
- const listUrl = `${backend.baseUrl.replace(/\/+$/, '')}/${encodeURIComponent(user)}?format=json`;
204
+ const scope = opts.scope ?? 'public';
205
+ const includeHidden = scope !== 'public';
206
+ const query = new URLSearchParams({ format: 'json' });
207
+ if (includeHidden)
208
+ query.set('scope', 'mine');
209
+ const listUrl = `${backend.baseUrl.replace(/\/+$/, '')}/${encodeURIComponent(user)}?${query.toString()}`;
210
+ const listingHeaders = {};
211
+ if (includeHidden) {
212
+ if (!backend.token) {
213
+ 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.`);
214
+ }
215
+ listingHeaders.authorization = `Bearer ${backend.token}`;
216
+ }
152
217
  const fetchListing = opts.fetchListing ?? defaultListingFetch;
153
- const res = await fetchListing(listUrl);
218
+ const res = await fetchListing(listUrl, listingHeaders);
154
219
  if (res.status === 404) {
155
220
  // A single-segment path 404s either because the namespace is genuinely empty
156
221
  // (current template — the listing route gates on the namespace holding objects,
@@ -177,14 +242,23 @@ export async function runShareList(opts = {}) {
177
242
  throw new Error(OUTDATED_TEMPLATE_HINT);
178
243
  }
179
244
  const parsed = parseShareListing(user, res.body);
180
- return applyShareListFilters(parsed, opts);
245
+ return applyShareListFilters(parsed, {
246
+ agent: opts.agent,
247
+ session: opts.session,
248
+ label: opts.label,
249
+ meta: opts.meta,
250
+ scope,
251
+ });
181
252
  }
182
253
  /** Client-side filtering over an already-fetched listing (RUSH-2683) — the
183
254
  * Worker has no query surface for this, so it narrows the fetched set instead
184
255
  * of a second round trip. `count` reflects the FILTERED set, matching what the
185
256
  * caller actually sees. */
186
- function applyShareListFilters(result, filters) {
257
+ export function applyShareListFilters(result, filters) {
187
258
  let objects = result.objects;
259
+ if (filters.scope && filters.scope !== 'all') {
260
+ objects = objects.filter((o) => o.visibility === filters.scope);
261
+ }
188
262
  if (filters.agent) {
189
263
  const needle = filters.agent.toLowerCase();
190
264
  objects = objects.filter((o) => (o.agent ?? '').toLowerCase() === needle);
@@ -196,6 +270,9 @@ function applyShareListFilters(result, filters) {
196
270
  const needle = filters.label.toLowerCase();
197
271
  objects = objects.filter((o) => (o.label ?? '').toLowerCase().includes(needle));
198
272
  }
273
+ if (filters.meta) {
274
+ objects = objects.filter((o) => Object.entries(filters.meta).every(([key, value]) => o.meta[key] === value));
275
+ }
199
276
  return { user: result.user, count: objects.length, objects };
200
277
  }
201
278
  /** Human-readable bytes, e.g. `1.2 KB`, `640 B`. */
@@ -219,7 +296,9 @@ export function formatShareList(result, json = false) {
219
296
  chalk.dim(` ${result.count} published ${result.count === 1 ? 'page' : 'pages'}`);
220
297
  const rows = result.objects.map((o) => {
221
298
  const when = o.publishedAt ? o.publishedAt.slice(0, 10) : 'unknown';
222
- const bits = [when, formatBytes(o.size)];
299
+ const visibility = o.visibility ?? 'public';
300
+ const visibilityBit = visibility === 'public' ? chalk.green('public') : chalk.yellow(visibility);
301
+ const bits = [visibilityBit, when, formatBytes(o.size)];
223
302
  if (o.agent)
224
303
  bits.push(o.agent);
225
304
  if (o.revisionCount > 0)
@@ -305,6 +384,49 @@ export async function runShareRevisions(target, opts = {}) {
305
384
  }
306
385
  return parseShareRevisions(key, res.body);
307
386
  }
387
+ /** Mint a one-time login ticket for the signed-in owner and build the URL that
388
+ * opens their own published page **as the owner**, so the inline visibility
389
+ * control is live in the browser. Managed backend only — the control is gated on
390
+ * the Phoenix identity, and BYO (WRITE_TOKEN) endpoints have no per-viewer login.
391
+ * `target` accepts the same three forms as `visibility`/`unshare` (a full URL,
392
+ * `<user>/<slug>`, or a bare slug in the caller's namespace). */
393
+ export async function runShareOpen(target, opts = {}) {
394
+ const backend = resolveShareBackend({
395
+ githubUser: opts.githubUser,
396
+ config: opts.config,
397
+ session: opts.session,
398
+ requireToken: false,
399
+ });
400
+ if (backend.kind !== 'managed') {
401
+ throw new Error("'agents artifacts share open' needs a managed (Phoenix) endpoint — the page's inline " +
402
+ "visibility control is gated on your signed-in identity. Run 'agents auth login' first.");
403
+ }
404
+ const baseUrl = backend.baseUrl.replace(/\/+$/, '');
405
+ const { key } = await resolveDeleteTarget(target, { githubUser: backend.namespace });
406
+ const url = `${baseUrl}/${key}`;
407
+ const doFetch = opts.fetchTicket ??
408
+ (async (u, bearer) => {
409
+ const res = await fetch(u, { method: 'POST', headers: { authorization: `Bearer ${bearer}` } });
410
+ return { status: res.status, body: await res.text() };
411
+ });
412
+ const res = await doFetch(`${baseUrl}/__ticket`, backend.token);
413
+ if (res.status !== 200) {
414
+ throw new Error(`Could not mint a login ticket (${res.status}) from ${baseUrl}/__ticket. ` +
415
+ "The endpoint may predate this feature — run 'agents artifacts share update' to redeploy the Worker.");
416
+ }
417
+ let ticket;
418
+ try {
419
+ const parsed = JSON.parse(res.body);
420
+ if (typeof parsed.ticket !== 'string' || !parsed.ticket)
421
+ throw new Error('no ticket in response');
422
+ ticket = parsed.ticket;
423
+ }
424
+ catch {
425
+ throw new Error(`Ticket endpoint at ${baseUrl}/__ticket did not return a usable ticket.`);
426
+ }
427
+ const ownerUrl = `${url}?phoenix_ticket=${encodeURIComponent(ticket)}`;
428
+ return { key, url, ownerUrl };
429
+ }
308
430
  export function formatShareRevisions(result, json = false) {
309
431
  if (json)
310
432
  return JSON.stringify(result, null, 2);
@@ -341,6 +463,18 @@ export function formatShareDeleteResult(result, json = false) {
341
463
  }
342
464
  return lines.join('\n');
343
465
  }
466
+ /** Human summary of an in-place visibility change. */
467
+ function formatShareVisibilityResult(result, requested) {
468
+ const now = result.visibility ?? requested;
469
+ const from = result.previousVisibility && result.previousVisibility !== now
470
+ ? `${chalk.dim(result.previousVisibility)} → `
471
+ : '';
472
+ return [
473
+ `${chalk.green('✓')} visibility ${from}${chalk.bold(now)}`,
474
+ chalk.dim(` ${result.url}`),
475
+ chalk.dim(' body unchanged — no revision created'),
476
+ ].join('\n');
477
+ }
344
478
  /** Shared handler for `agents artifacts share delete <targets...>` and the
345
479
  * nested `agents artifacts unshare <targets...>` alias. Deletes each target independently and
346
480
  * continues past a failed one (rm-style), reporting all results and exiting
@@ -426,6 +560,36 @@ const SHARE_DELETE_NOTES = `
426
560
 
427
561
  agents artifacts share delete === agents artifacts unshare (same command, different name).
428
562
  `;
563
+ const SHARE_VISIBILITY_EXAMPLES = `
564
+ # Take a public page private to just you (Phoenix session required for me/org)
565
+ agents artifacts share visibility https://share.agents-cli.sh/octocat/q3-plan me
566
+
567
+ # Make it public again — by <user>/<slug> or a bare slug in your namespace
568
+ agents artifacts share visibility octocat/q3-plan public
569
+ agents artifacts share visibility q3-plan unlisted
570
+
571
+ # Share with your Phoenix org (rejected on a public-inbox email domain)
572
+ agents artifacts share visibility q3-plan org
573
+
574
+ # Machine-readable result
575
+ agents artifacts share visibility q3-plan me --visibility-json
576
+ `;
577
+ const SHARE_VISIBILITY_NOTES = `
578
+ Changes an ALREADY-published page's visibility in place: the slug — and so the
579
+ URL — is preserved. It re-stamps only the visibility on the stored object; the
580
+ body, provenance, label, and --meta are untouched, so — like 'agents artifacts
581
+ share edit' — this is a metadata-only rewrite and creates no revision.
582
+
583
+ public is listed in the gallery; unlisted is a capability URL (GET still 200,
584
+ X-Robots-Tag: noindex, hidden from gallery/list); me is visible only to the
585
+ signed-in owner; org is visible to members of the same Phoenix organization.
586
+ me and org require a Phoenix session — run 'agents auth login' first — and org
587
+ is refused on a public-inbox email domain (gmail.com, outlook.com, …).
588
+
589
+ This hits the live endpoint via the owner's Phoenix session (or the BYO
590
+ WRITE_TOKEN for public/unlisted). It does not re-run the pre-publish
591
+ sensitive-content scan — the body is unchanged.
592
+ `;
429
593
  /**
430
594
  * Register the `share` subtree under its parent group — `agents artifacts share`
431
595
  * (see commands/artifacts.ts). `agents artifacts unshare` is the nested alias
@@ -436,6 +600,10 @@ const SHARE_DELETE_NOTES = `
436
600
  export function registerShareCommands(artifactsCmd) {
437
601
  const shareCmd = artifactsCmd
438
602
  .command('share')
603
+ // Child task commands intentionally reuse publish vocabulary (`edit --label`,
604
+ // `list --meta`). Keep option ownership positional within this subtree so
605
+ // Commander does not silently resolve those flags to the parent publisher.
606
+ .enablePositionalOptions()
439
607
  .description('Publish an HTML file to a shareable link — managed if signed in, otherwise your Cloudflare R2.')
440
608
  .argument('[file]', 'file to publish (HTML or any static asset)')
441
609
  .option('--slug <slug>', 'URL slug override (default: stable slug of the artifact title, then filename)')
@@ -553,6 +721,84 @@ ${SHARE_DELETE_NOTES}
553
721
  shareDeleteCmd.action(async (targets, opts) => {
554
722
  await runShareDelete(targets, opts);
555
723
  });
724
+ // `agents artifacts share visibility <target> <level>` — change an
725
+ // already-published page's visibility in place. Kept in its own block; it
726
+ // reuses the delete target parser and the same PATCH metadata-edit route as
727
+ // `share edit` (visibility is metadata), so it re-stamps the stored object
728
+ // rather than re-publishing — a re-publish over the PUT path can't express an
729
+ // in-place change (GET injects the attribution bar at serve time and never
730
+ // strips it, and the served body carries no provenance/label/--meta back to
731
+ // re-send). --for-user / --visibility-json (not --github-user / --json): the
732
+ // parent `share <file>` already owns both long names, and commander resolves a
733
+ // child option's long name against the whole ancestor chain, silently dropping
734
+ // a same-named child (RUSH-2687) — the same rename the delete/list blocks make.
735
+ const shareVisibilityCmd = shareCmd
736
+ .command('visibility')
737
+ .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.")
738
+ .addArgument(new Argument('<target>', 'the published page: a full URL, <user>/<slug>, or a bare slug in your namespace'))
739
+ .addArgument(new Argument('<level>', 'new visibility').choices([...SHARE_VISIBILITY_LEVELS]))
740
+ .option('--for-user <user>', 'GitHub username for resolving a bare-slug target (default: resolved from gh/git config)')
741
+ .option('--visibility-json', 'emit a machine-readable result')
742
+ .action(async (target, level, opts) => {
743
+ try {
744
+ const visibility = level;
745
+ const result = await runShareEdit(target, { visibility, githubUser: opts.forUser });
746
+ if (opts.visibilityJson) {
747
+ console.log(JSON.stringify(result, null, 2));
748
+ return;
749
+ }
750
+ console.log(formatShareVisibilityResult(result, visibility));
751
+ }
752
+ catch (e) {
753
+ console.error(chalk.red(e.message));
754
+ process.exitCode = 1;
755
+ }
756
+ });
757
+ setHelpSections(shareVisibilityCmd, {
758
+ examples: SHARE_VISIBILITY_EXAMPLES,
759
+ notes: SHARE_VISIBILITY_NOTES,
760
+ });
761
+ const shareOpenCmd = shareCmd
762
+ .command('open')
763
+ .description("Open your own published page in the browser signed in as the owner, so the page's inline visibility control is live. Mints a one-time login ticket and appends it to the URL.")
764
+ .addArgument(new Argument('<target>', 'the published page: a full URL, <user>/<slug>, or a bare slug in your namespace'))
765
+ .option('--for-user <user>', 'GitHub username for resolving a bare-slug target (default: resolved from your Phoenix handle)')
766
+ .option('--no-open', 'print the owner URL instead of opening a browser')
767
+ .option('--json', 'emit a machine-readable result (includes the ticketed owner URL)')
768
+ .action(async (target, opts) => {
769
+ try {
770
+ const result = await runShareOpen(target, { githubUser: opts.forUser });
771
+ if (opts.json) {
772
+ console.log(JSON.stringify(result, null, 2));
773
+ return;
774
+ }
775
+ if (opts.open === false) {
776
+ console.log(chalk.green(result.ownerUrl));
777
+ console.log(chalk.dim(' open this to land on the page as the owner (the visibility chip becomes a control)'));
778
+ return;
779
+ }
780
+ const shown = await showUrl(result.ownerUrl);
781
+ if (shown.via === 'none') {
782
+ console.log(chalk.green(result.ownerUrl));
783
+ console.error(chalk.dim(' could not open a browser — open the URL above yourself'));
784
+ }
785
+ else {
786
+ console.log(chalk.green(`Opened ${result.url} as owner — the visibility chip is now a live control.`));
787
+ }
788
+ }
789
+ catch (e) {
790
+ console.error(chalk.red(e.message));
791
+ process.exitCode = 1;
792
+ }
793
+ });
794
+ setHelpSections(shareOpenCmd, {
795
+ examples: `agents artifacts share open q3-plan # open your page, signed in, ready to re-scope
796
+ agents artifacts share open octocat/q3-plan
797
+ agents artifacts share open https://share.agents-cli.sh/octocat/q3-plan --no-open`,
798
+ notes: `The shared page's visibility chip is an interactive control only for the signed-in owner; a plain link has no login, so the chip renders as a static cue. This mints a short-lived, single-use login ticket (self-signed by your share Worker) and opens the page with it, so the chip becomes a dropdown you can switch in place.
799
+ Managed (Phoenix) endpoints only — run 'agents auth login' first. If the mint 501s/404s, your Worker predates the feature: run 'agents artifacts share update' to redeploy it.
800
+ Prefer to change visibility without a browser? Use 'agents artifacts share visibility <target> <level>'.`,
801
+ });
556
802
  shareCmd
557
803
  .command('join')
558
804
  .description('Use an existing synced share endpoint and write token (no provisioning).')
@@ -664,20 +910,33 @@ ${SHARE_DELETE_NOTES}
664
910
  // share.test.ts). Matches the `revisions` precedent (--for-user/
665
911
  // --revisions-json) and the `delete`/`unshare` precedent (--for-user/
666
912
  // --delete-json) below.
913
+ //
914
+ // `--visibility` is also owned by the parent `share <file>` command (publish),
915
+ // so the listing filter is named `--scope` here; `--all` is the convenience
916
+ // alias for `--scope all`.
667
917
  .option('--for-user <user>', 'GitHub username whose namespace to list (default: resolved from gh/git config)')
918
+ .addOption(new Option('--scope <level>', 'visibility filter: public (default), unlisted, me, org, or all')
919
+ .choices(['public', 'unlisted', 'me', 'org', 'all'])
920
+ .default('public'))
921
+ .option('--all', "list every page including hidden unlisted/me/org shares (alias for --scope all)")
668
922
  .option('--agent <name>', 'filter to shares published by this agent/harness (case-insensitive)')
669
923
  .option('--session <id>', 'filter to shares published from this session id')
670
924
  // Named --label-contains, not --label: `share <file>` (the parent) already owns
671
925
  // `--label`/`--title`, same collision class as --for-user/--list-json above.
672
926
  .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)')
927
+ .option('--meta <key=value>', 'filter by an exact arbitrary metadata entry (repeatable)', (v, p) => [...p, v], [])
928
+ .option('--list-json', 'emit the machine-readable listing (slug, url, size, contentType, publishedAt, expiresAt, label, visibility, agent, session, host, repo, revisionCount, meta)')
674
929
  .action(async (opts) => {
675
930
  try {
931
+ const scope = opts.all ? 'all' : (opts.scope ?? 'public');
932
+ const parentMeta = shareCmd.opts().meta ?? [];
676
933
  const result = await runShareList({
677
934
  githubUser: opts.forUser,
935
+ scope,
678
936
  agent: opts.agent,
679
937
  session: opts.session,
680
938
  label: opts.labelContains,
939
+ meta: parseMetaEntries(opts.meta.length ? opts.meta : parentMeta),
681
940
  });
682
941
  console.log(formatShareList(result, Boolean(opts.listJson)));
683
942
  }
@@ -691,6 +950,11 @@ ${SHARE_DELETE_NOTES}
691
950
  # Everything you've published, newest first
692
951
  agents artifacts share list
693
952
 
953
+ # Include your hidden pages (unlisted / me / org) so you can see everything
954
+ agents artifacts share list --all
955
+ agents artifacts share list --scope me
956
+ agents artifacts share list --scope unlisted
957
+
694
958
  # Machine-readable — e.g. pull every still-public URL with jq
695
959
  agents artifacts share list --list-json | jq -r '.objects[].url'
696
960
 
@@ -700,18 +964,83 @@ ${SHARE_DELETE_NOTES}
700
964
  # Narrow by who/what published it
701
965
  agents artifacts share list --agent claude
702
966
  agents artifacts share list --label-contains "fleet plan"
967
+ agents artifacts share list --meta kind=plan --meta status=final
703
968
  `,
704
969
  notes: `
705
970
  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.
971
+ covers are omitted. By default this mirrors the public gallery (public pages only).
972
+ Use --scope unlisted|me|org or --all to include your hidden pages; those scopes
973
+ send the owner's bearer and a 'scope=mine' hint to the Worker's JSON listing route,
974
+ which returns hidden pages ONLY after verifying the caller owns the namespace.
975
+ Signed-in users list the managed endpoint (share.agents-cli.sh/<handle>); otherwise
976
+ BYO. It reads the endpoint's JSON listing route, which ships with the current
977
+ Worker template. If a BYO Worker predates this feature the command says so and
978
+ points you at 'agents artifacts share update' (RUSH-2449) rather than returning a
979
+ wrong or empty result — see 'agents artifacts share status' for whether an update
980
+ is due.
981
+
982
+ --agent/--session/--label-contains/--meta filter the fetched listing client-side;
983
+ --list-json's count reflects the filtered set. Each human row shows the page's
984
+ visibility so public vs hidden is obvious at a glance.
985
+ `,
986
+ });
987
+ const shareEditCmd = shareCmd
988
+ .command('edit <target>')
989
+ .description('Edit a published share\'s label or arbitrary metadata without republishing its body.')
990
+ .option('--for-user <user>', 'namespace for resolving a bare slug (default: your namespace)')
991
+ .option('--label <text>', 'set or replace the display label')
992
+ .option('--remove-label', 'remove the display label')
993
+ .option('--meta <key=value>', 'merge a metadata entry (repeatable)', (v, p) => [...p, v], [])
994
+ .option('--replace-meta <key=value>', 'replace all arbitrary metadata with these entries (repeatable)', (v, p) => [...p, v], [])
995
+ .option('--remove-meta <key>', 'remove an arbitrary metadata key (repeatable)', (v, p) => [...p, v], [])
996
+ .option('--edit-json', 'emit the machine-readable edit result')
997
+ .action(async (target, opts) => {
998
+ try {
999
+ const parent = shareCmd.opts();
1000
+ const label = opts.label ?? parent.label;
1001
+ const mergeMeta = opts.meta.length ? opts.meta : (parent.meta ?? []);
1002
+ if (label !== undefined && opts.removeLabel)
1003
+ throw new Error('--label and --remove-label are mutually exclusive.');
1004
+ if (mergeMeta.length && opts.replaceMeta.length)
1005
+ throw new Error('--meta and --replace-meta are mutually exclusive.');
1006
+ if (opts.replaceMeta.length && opts.removeMeta.length)
1007
+ throw new Error('--replace-meta and --remove-meta are mutually exclusive.');
1008
+ if (label === undefined && !opts.removeLabel && !mergeMeta.length && !opts.replaceMeta.length && !opts.removeMeta.length)
1009
+ throw new Error('Nothing to edit. Pass --label, --remove-label, --meta, --replace-meta, or --remove-meta.');
1010
+ const removeMeta = opts.removeMeta.map((key) => Object.keys(parseMetaEntries([`${key}=x`]))[0]);
1011
+ const result = await runShareEdit(target, {
1012
+ githubUser: opts.forUser,
1013
+ label: opts.removeLabel ? null : label === undefined ? undefined : sanitizeLabel(label),
1014
+ meta: parseMetaEntries(opts.replaceMeta.length ? opts.replaceMeta : mergeMeta),
1015
+ metaMode: opts.replaceMeta.length ? 'replace' : 'merge',
1016
+ removeMeta,
1017
+ // Parent `share --force` (RUSH-2687: a child --force is silently dropped).
1018
+ force: parent.force,
1019
+ });
1020
+ console.log(opts.editJson ? JSON.stringify(result, null, 2) : chalk.green(`updated ${result.url}`));
1021
+ }
1022
+ catch (e) {
1023
+ console.error(chalk.red(e.message));
1024
+ process.exitCode = 1;
1025
+ }
1026
+ });
1027
+ setHelpSections(shareEditCmd, {
1028
+ examples: `
1029
+ # Change only the gallery/list title
1030
+ agents artifacts share edit q3-plan --label "Q3 fleet plan"
712
1031
 
713
- --agent/--session/--label-contains filter the fetched listing client-side;
714
- --list-json's count reflects the filtered set.
1032
+ # Merge metadata, or deliberately replace/remove it
1033
+ agents artifacts share edit q3-plan --meta status=final --meta ticket=PHNX-3278
1034
+ agents artifacts share edit q3-plan --replace-meta kind=plan --replace-meta status=final
1035
+ agents artifacts share edit q3-plan --remove-meta status
1036
+ `,
1037
+ notes: `
1038
+ Metadata edits preserve the exact published body, content type/HTTP metadata,
1039
+ publication time, visibility, expiry, provenance, cover, and retained revisions. --meta merges;
1040
+ --replace-meta replaces all arbitrary metadata; --remove-meta deletes named keys.
1041
+ Reserved Worker/provenance keys cannot be changed through metadata flags.
1042
+ Edited labels and metadata values are scanned for emails and credential-shaped
1043
+ strings the same way publish is — pass --force to write them anyway.
715
1044
  `,
716
1045
  });
717
1046
  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
  *