@phnx-labs/agents-cli 1.22.114 → 1.22.115

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (176) hide show
  1. package/CHANGELOG.md +110 -0
  2. package/README.md +22 -102
  3. package/dist/browser.js +0 -0
  4. package/dist/cli/command-registry.js +0 -7
  5. package/dist/commands/accounts.d.ts +3 -77
  6. package/dist/commands/accounts.js +13 -366
  7. package/dist/commands/audit.js +1 -1
  8. package/dist/commands/auth.js +0 -2
  9. package/dist/commands/browser.d.ts +53 -0
  10. package/dist/commands/browser.js +199 -9
  11. package/dist/commands/doctor.d.ts +2 -3
  12. package/dist/commands/doctor.js +0 -11
  13. package/dist/commands/events.d.ts +1 -3
  14. package/dist/commands/events.js +4 -7
  15. package/dist/commands/exec.d.ts +0 -1
  16. package/dist/commands/exec.js +2 -7
  17. package/dist/commands/feed-watch.js +52 -8
  18. package/dist/commands/focus.js +1 -1
  19. package/dist/commands/go.d.ts +0 -17
  20. package/dist/commands/go.js +2 -19
  21. package/dist/commands/logs.js +1 -1
  22. package/dist/commands/mcp.js +8 -83
  23. package/dist/commands/memory.js +4 -47
  24. package/dist/commands/message.js +4 -4
  25. package/dist/commands/plugins.js +1 -93
  26. package/dist/commands/repo.js +0 -44
  27. package/dist/commands/resume.js +4 -1
  28. package/dist/commands/secrets-passthrough.js +2 -2
  29. package/dist/commands/send.d.ts +4 -4
  30. package/dist/commands/send.js +6 -51
  31. package/dist/commands/sessions-backup-setup.js +1 -1
  32. package/dist/commands/sessions-resume.js +0 -1
  33. package/dist/commands/sessions-share.d.ts +5 -7
  34. package/dist/commands/sessions-share.js +98 -49
  35. package/dist/commands/sessions.js +1 -8
  36. package/dist/commands/setup-browser.js +18 -2
  37. package/dist/commands/setup-computer.js +20 -6
  38. package/dist/commands/setup-secrets.js +24 -4
  39. package/dist/commands/setup-terminal.d.ts +3 -0
  40. package/dist/commands/setup-terminal.js +22 -0
  41. package/dist/commands/setup.d.ts +1 -1
  42. package/dist/commands/setup.js +20 -10
  43. package/dist/commands/skills.js +0 -8
  44. package/dist/commands/ssh.d.ts +6 -0
  45. package/dist/commands/ssh.js +92 -233
  46. package/dist/commands/sync.js +14 -5
  47. package/dist/commands/teams.js +1 -1
  48. package/dist/commands/traces.js +1 -1
  49. package/dist/lib/accounts/add.d.ts +0 -5
  50. package/dist/lib/accounts/add.js +1 -7
  51. package/dist/lib/artifacts-client.d.ts +20 -0
  52. package/dist/lib/artifacts-client.js +46 -0
  53. package/dist/lib/auth-mint.js +2 -2
  54. package/dist/lib/browser/runtime-state.d.ts +55 -0
  55. package/dist/lib/browser/runtime-state.js +99 -18
  56. package/dist/lib/browser/service.js +21 -1
  57. package/dist/lib/cli-resources.js +3 -1
  58. package/dist/lib/cloud/dispatch.js +1 -1
  59. package/dist/lib/cloudflare/creds.d.ts +10 -0
  60. package/dist/lib/cloudflare/creds.js +46 -0
  61. package/dist/lib/cloudflare/provision.d.ts +35 -0
  62. package/dist/lib/cloudflare/provision.js +144 -0
  63. package/dist/lib/computer/sessions-list.d.ts +55 -0
  64. package/dist/lib/computer/sessions-list.js +168 -1
  65. package/dist/lib/daemon/daemon.js +8 -1
  66. package/dist/lib/daemon/feed-stream-service.d.ts +23 -0
  67. package/dist/lib/daemon/feed-stream-service.js +40 -0
  68. package/dist/lib/daemon-services.d.ts +1 -1
  69. package/dist/lib/daemon-services.js +5 -0
  70. package/dist/lib/devices/connect.d.ts +49 -5
  71. package/dist/lib/devices/connect.js +169 -21
  72. package/dist/lib/feed/envelope.d.ts +79 -0
  73. package/dist/lib/feed/envelope.js +23 -0
  74. package/dist/lib/feed/events.d.ts +6 -0
  75. package/dist/lib/feed/events.js +8 -0
  76. package/dist/lib/feed/hub-server.d.ts +86 -0
  77. package/dist/lib/feed/hub-server.js +334 -0
  78. package/dist/lib/feed/hub.d.ts +95 -0
  79. package/dist/lib/feed/hub.js +255 -0
  80. package/dist/lib/feed/tool-activity.d.ts +108 -0
  81. package/dist/lib/feed/tool-activity.js +313 -0
  82. package/dist/lib/feed/tools.d.ts +198 -0
  83. package/dist/lib/feed/tools.js +265 -0
  84. package/dist/lib/feed/watch.d.ts +50 -50
  85. package/dist/lib/feed/watch.js +147 -16
  86. package/dist/lib/format.d.ts +1 -1
  87. package/dist/lib/format.js +1 -1
  88. package/dist/lib/git.d.ts +0 -16
  89. package/dist/lib/git.js +0 -58
  90. package/dist/lib/helper-versions.js +1 -1
  91. package/dist/lib/hosts/remote-cmd.d.ts +51 -1
  92. package/dist/lib/hosts/remote-cmd.js +125 -8
  93. package/dist/lib/hosts/remote-cmd.test-fixture.d.ts +2 -0
  94. package/dist/lib/hosts/remote-cmd.test-fixture.js +22 -0
  95. package/dist/lib/mcp.js +17 -11
  96. package/dist/lib/probe.d.ts +4 -1
  97. package/dist/lib/probe.js +5 -2
  98. package/dist/lib/pwsh.d.ts +33 -0
  99. package/dist/lib/pwsh.js +56 -0
  100. package/dist/lib/redact.d.ts +8 -0
  101. package/dist/lib/redact.js +11 -0
  102. package/dist/lib/refresh.d.ts +6 -2
  103. package/dist/lib/refresh.js +92 -72
  104. package/dist/lib/secrets-cli.d.ts +11 -0
  105. package/dist/lib/secrets-cli.js +30 -0
  106. package/dist/lib/secrets-client.js +3 -2
  107. package/dist/lib/session/detached.d.ts +7 -0
  108. package/dist/lib/session/detached.js +29 -0
  109. package/dist/lib/session/remote/peer-stream.d.ts +24 -2
  110. package/dist/lib/session/remote/peer-stream.js +33 -6
  111. package/dist/lib/session/sync/backend.d.ts +3 -3
  112. package/dist/lib/session/sync/backend.js +3 -3
  113. package/dist/lib/session/sync/provision.d.ts +1 -1
  114. package/dist/lib/session/sync/provision.js +2 -2
  115. package/dist/lib/sessions-client.js +0 -3
  116. package/dist/lib/setup-tool-install.d.ts +3 -0
  117. package/dist/lib/setup-tool-install.js +26 -0
  118. package/dist/lib/setup-tool-status.d.ts +22 -0
  119. package/dist/lib/setup-tool-status.js +215 -0
  120. package/dist/lib/share-runtime.d.ts +11 -0
  121. package/dist/lib/share-runtime.js +63 -0
  122. package/dist/lib/smart-launch.d.ts +1 -5
  123. package/dist/lib/smart-launch.js +3 -11
  124. package/dist/lib/ssh-exec.d.ts +44 -0
  125. package/dist/lib/ssh-exec.js +119 -0
  126. package/dist/lib/startup/command-registry.d.ts +6 -4
  127. package/dist/lib/startup/command-registry.js +10 -7
  128. package/dist/lib/state.js +2 -2
  129. package/dist/lib/storage/selection.d.ts +2 -2
  130. package/dist/lib/storage/selection.js +2 -2
  131. package/dist/lib/sync-umbrella.d.ts +5 -0
  132. package/dist/lib/sync-umbrella.js +18 -10
  133. package/dist/lib/traces/backend.d.ts +1 -2
  134. package/dist/lib/traces/backend.js +1 -2
  135. package/dist/lib/traces/provision.d.ts +1 -1
  136. package/dist/lib/traces/provision.js +2 -2
  137. package/dist/lib/types.d.ts +8 -6
  138. package/package.json +2 -3
  139. package/dist/commands/artifacts-setup.d.ts +0 -53
  140. package/dist/commands/artifacts-setup.js +0 -161
  141. package/dist/commands/artifacts.d.ts +0 -18
  142. package/dist/commands/artifacts.js +0 -58
  143. package/dist/commands/attach.d.ts +0 -12
  144. package/dist/commands/attach.js +0 -86
  145. package/dist/commands/auth-mint.d.ts +0 -12
  146. package/dist/commands/auth-mint.js +0 -108
  147. package/dist/commands/reconnect.d.ts +0 -46
  148. package/dist/commands/reconnect.js +0 -115
  149. package/dist/commands/share.d.ts +0 -293
  150. package/dist/commands/share.js +0 -1424
  151. package/dist/lib/share/analytics.d.ts +0 -13
  152. package/dist/lib/share/analytics.js +0 -45
  153. package/dist/lib/share/backend.d.ts +0 -120
  154. package/dist/lib/share/backend.js +0 -176
  155. package/dist/lib/share/capture.d.ts +0 -31
  156. package/dist/lib/share/capture.js +0 -174
  157. package/dist/lib/share/config.d.ts +0 -72
  158. package/dist/lib/share/config.js +0 -211
  159. package/dist/lib/share/delete.d.ts +0 -123
  160. package/dist/lib/share/delete.js +0 -173
  161. package/dist/lib/share/html.d.ts +0 -20
  162. package/dist/lib/share/html.js +0 -88
  163. package/dist/lib/share/http-error.d.ts +0 -53
  164. package/dist/lib/share/http-error.js +0 -65
  165. package/dist/lib/share/og.d.ts +0 -26
  166. package/dist/lib/share/og.js +0 -84
  167. package/dist/lib/share/provision.d.ts +0 -127
  168. package/dist/lib/share/provision.js +0 -285
  169. package/dist/lib/share/publish.d.ts +0 -379
  170. package/dist/lib/share/publish.js +0 -818
  171. package/dist/lib/share/worker-template.d.ts +0 -27
  172. package/dist/lib/share/worker-template.js +0 -2424
  173. package/dist/lib/storage/index.d.ts +0 -14
  174. package/dist/lib/storage/index.js +0 -14
  175. package/dist/lib/storage/visibility.d.ts +0 -82
  176. package/dist/lib/storage/visibility.js +0 -99
@@ -1,211 +0,0 @@
1
- // Config + credential glue for `agents artifacts share`.
2
- //
3
- // - The endpoint config (base URL, account, worker/bucket names) lives in
4
- // `agents.yaml` under `share:` (Meta.share) so it syncs fleet-wide via
5
- // `agents repo push/pull`.
6
- // - The raw write token lives in the `share` secrets bundle (keychain-backed,
7
- // fleet-injectable) — never on disk in plaintext.
8
- // - The Cloudflare API token (for provisioning) is read from the user's existing
9
- // `cloudflare` bundle.
10
- import { randomBytes } from 'node:crypto';
11
- import { readMeta, updateMeta } from '../state.js';
12
- import { bundleExistsSync as bundleExists, keychainRef, readAndResolveBundleEnvSync as readAndResolveBundleEnv, readBundleSync as readBundle, secretsKeychainItem, writeBundleWithItemsSync as writeBundleWithItems, } from '../secrets-client.js';
13
- export const SHARE_BUNDLE = 'share';
14
- export const SHARE_TOKEN_KEY = 'WRITE_TOKEN';
15
- export const SHARE_TOKEN_ENV_KEY = 'SHARE_WRITE_TOKEN';
16
- export const DEFAULT_CF_BUNDLE = 'cloudflare';
17
- export const DEFAULT_WORKER_NAME = 'agents-share';
18
- export const DEFAULT_BUCKET_NAME = 'agents-share';
19
- // The Rush product share endpoint (PHNX-3960): share.getrush.ai on the product
20
- // domain. Publishing prints this host; bound to the `agents-share` Worker in the
21
- // Phoenix CF account. share.agents-cli.sh + artifacts.prix.dev stay bound as legacy aliases.
22
- export const DEFAULT_SHARE_DOMAIN = 'share.getrush.ai';
23
- // Managed hosts still recognized as "the platform endpoint" for backward
24
- // compatibility. `share.agents-cli.sh` is the pre-PHNX-3960 default: it stays
25
- // attached to the same Worker, so every link already shared on it keeps
26
- // resolving, and a persisted config pointing at it is still treated as managed
27
- // (e.g. for deploy-time PHOENIX_ID_BASE binding). Compared lowercased.
28
- export const LEGACY_MANAGED_SHARE_DOMAINS = ['share.agents-cli.sh', 'artifacts.prix.dev'];
29
- /** The write token may be injected ephemerally into fleet/cloud agents. */
30
- export function readWriteTokenEnv(env = process.env) {
31
- const token = env[SHARE_TOKEN_ENV_KEY]?.trim();
32
- return token ? token : null;
33
- }
34
- /** Trim; empty / whitespace-only strings are absent. */
35
- function nonempty(v) {
36
- const t = v?.trim();
37
- return t ? t : undefined;
38
- }
39
- /**
40
- * Read the persisted endpoint config, or null if there is no `share.baseUrl`.
41
- *
42
- * Publish talks only to `baseUrl` with `WRITE_TOKEN`. `accountId` / worker /
43
- * bucket are provisioning metadata for `setup`/`update`. Requiring all four
44
- * (and treating `accountId: ""` as missing) made `status` and publish both
45
- * report "not set up" on an endpoint that still worked — RUSH-2837.
46
- */
47
- export function readShareConfig() {
48
- const s = readMeta().share;
49
- const baseUrl = nonempty(s?.baseUrl)?.replace(/\/+$/, '');
50
- if (!baseUrl)
51
- return null;
52
- return {
53
- baseUrl,
54
- accountId: nonempty(s?.accountId) ?? '',
55
- workerName: nonempty(s?.workerName) ?? DEFAULT_WORKER_NAME,
56
- bucketName: nonempty(s?.bucketName) ?? DEFAULT_BUCKET_NAME,
57
- domain: nonempty(s?.domain),
58
- analyticsToken: nonempty(s?.analyticsToken),
59
- templateHash: nonempty(s?.templateHash),
60
- };
61
- }
62
- /** Persist the endpoint config to `agents.yaml` (syncs across the fleet).
63
- * Empty strings never overwrite a previously stored value and are not written
64
- * back as `accountId: ""` (that form made {@link readShareConfig} return null
65
- * under the old all-fields-required check, and it is still useless). */
66
- export function writeShareConfig(cfg) {
67
- updateMeta((meta) => {
68
- const prev = meta.share ?? {};
69
- const next = { ...prev };
70
- const assign = (key, incoming) => {
71
- const v = nonempty(incoming);
72
- if (v)
73
- next[key] = v;
74
- };
75
- assign('baseUrl', cfg.baseUrl);
76
- assign('accountId', cfg.accountId);
77
- assign('workerName', cfg.workerName);
78
- assign('bucketName', cfg.bucketName);
79
- assign('domain', cfg.domain);
80
- assign('analyticsToken', cfg.analyticsToken);
81
- assign('templateHash', cfg.templateHash);
82
- for (const key of Object.keys(next)) {
83
- if (typeof next[key] === 'string' && !nonempty(next[key]))
84
- delete next[key];
85
- }
86
- return { ...meta, share: next };
87
- });
88
- }
89
- /** A fresh 32-byte hex write token. */
90
- export function generateWriteToken() {
91
- return randomBytes(32).toString('hex');
92
- }
93
- /** Persist the raw write token into the `share` secrets bundle (keychain-backed,
94
- * fleet-injectable). Mirrors the add-key sequence in `commands/secrets.ts`. */
95
- export function storeWriteToken(token) {
96
- let bundle;
97
- try {
98
- bundle = readBundle(SHARE_BUNDLE);
99
- }
100
- catch {
101
- bundle = {
102
- name: SHARE_BUNDLE,
103
- description: 'agents artifacts share — write token for the R2 share endpoint',
104
- // A NEW share bundle defaults to the `never` tier (no biometry ACL). The R2
105
- // write token is low-sensitivity automation infra that is auto-read on EVERY
106
- // `agents run` (shareRuntimeEnv) — a biometry ACL there is what produced the
107
- // per-run Touch ID storm. `never` stores it no-ACL so auto-share is silent
108
- // and needs no unlock. An EXISTING bundle keeps its tier (we never silently
109
- // downgrade one the user already made); change it explicitly with
110
- // `agents secrets policy share <tier>`.
111
- policy: 'never',
112
- vars: {},
113
- };
114
- }
115
- // writeBundleWithItems decides the ACL (keychain backend) purely from
116
- // `bundle.policy === 'never'` — the fresh-bundle case above sets it
117
- // explicitly, and an existing bundle already carries its own explicit tier.
118
- bundle.vars[SHARE_TOKEN_KEY] = keychainRef(SHARE_TOKEN_KEY);
119
- const item = secretsKeychainItem(bundle.name, SHARE_TOKEN_KEY);
120
- writeBundleWithItems(bundle, new Map([[item, token]]));
121
- }
122
- /** Read the raw write token from the `share` secrets bundle. */
123
- export function readWriteTokenFromBundle() {
124
- const { env } = readAndResolveBundleEnv(SHARE_BUNDLE, {
125
- caller: 'share',
126
- // Explicit share commands are reads, not authorization to authenticate.
127
- agentOnly: true,
128
- });
129
- const token = env[SHARE_TOKEN_KEY];
130
- if (!token) {
131
- throw new Error(`No ${SHARE_TOKEN_KEY} in the '${SHARE_BUNDLE}' secrets bundle. ` +
132
- `Run 'agents artifacts setup' (to provision your own endpoint) or 'agents artifacts share join' (to use an existing one).`);
133
- }
134
- return token;
135
- }
136
- /** Read the raw write token from injected env first, then the local bundle.
137
- * Throws with an actionable message if absent (run setup/join first). */
138
- export function readWriteToken() {
139
- return readWriteTokenEnv() ?? readWriteTokenFromBundle();
140
- }
141
- /** Best-effort runtime env for spawned agents. Never throws AND never prompts.
142
- *
143
- * Auto-injecting the share write token on every `agents run` is a background
144
- * convenience, NOT a user-initiated secret access — so it MUST NOT raise a Touch
145
- * ID sheet (SEC-13: an agent launch never pops biometry on its own). This was the
146
- * per-run prompt storm: `share` is a keychain bundle that is rarely broker-held,
147
- * so an interactive read here spawned the helper and popped Touch ID on EVERY
148
- * launch. The read is now always `agentOnly` — it resolves the token only from the
149
- * injected env or an already-held / no-ACL bundle, and silently returns undefined
150
- * otherwise (the caller runs without auto-share; the agent can still publish via
151
- * its own `agents artifacts share`). To get zero-friction auto-share with no prompt: unlock
152
- * once (`agents secrets unlock share`) or make it no-ACL (`agents secrets policy
153
- * share never`). */
154
- export function shareRuntimeEnv() {
155
- if (!readShareConfig())
156
- return undefined;
157
- const fromEnv = readWriteTokenEnv();
158
- if (fromEnv)
159
- return { [SHARE_TOKEN_ENV_KEY]: fromEnv };
160
- try {
161
- if (!bundleExists(SHARE_BUNDLE))
162
- return undefined;
163
- const { env } = readAndResolveBundleEnv(SHARE_BUNDLE, {
164
- caller: 'share',
165
- keys: [SHARE_TOKEN_KEY],
166
- agentOnly: true, // never raise a Touch ID sheet on an agent launch (SEC-13)
167
- });
168
- const token = env[SHARE_TOKEN_KEY];
169
- return token ? { [SHARE_TOKEN_ENV_KEY]: token } : undefined;
170
- }
171
- catch {
172
- return undefined;
173
- }
174
- }
175
- /** Cloudflare API credentials for provisioning, read from `cloudflare` (or a
176
- * user-named bundle). Fuzzy-matches key names so it works across bundle layouts. */
177
- export function readCloudflareCreds(bundle = DEFAULT_CF_BUNDLE, override) {
178
- // Explicit --token/--account bypass the bundle entirely (robust escape hatch).
179
- if (override?.apiToken) {
180
- return { apiToken: override.apiToken, accountId: override.accountId ?? '' };
181
- }
182
- // Check existence first (same guard as readShareToken above): resolving a
183
- // missing bundle through the process client surfaces an opaque transport code
184
- // (e.g. LOCKED), never naming the bundle the user typed — so name it here.
185
- if (!bundleExists(bundle)) {
186
- throw new Error(`The '${bundle}' bundle does not exist. ` +
187
- `Pass credentials directly with --token <t> [--account <id>], or create it: ` +
188
- `agents secrets add ${bundle} CLOUDFLARE_API_TOKEN`);
189
- }
190
- const { env } = readAndResolveBundleEnv(bundle, {
191
- caller: 'share',
192
- // Setup is still a read; only `agents secrets unlock` may authenticate.
193
- agentOnly: true,
194
- });
195
- const find = (re) => {
196
- for (const [k, v] of Object.entries(env))
197
- if (re.test(k) && v)
198
- return v;
199
- return '';
200
- };
201
- const apiToken = env.CLOUDFLARE_API_TOKEN || env.CF_API_TOKEN || find(/API[_-]?TOKEN|(?:^|_)TOKEN$/i);
202
- const accountId = override?.accountId || env.CLOUDFLARE_ACCOUNT_ID || env.CF_ACCOUNT_ID || find(/ACCOUNT[_-]?ID/i);
203
- if (!apiToken) {
204
- const keys = Object.keys(env);
205
- throw new Error(`No Cloudflare API token in the '${bundle}' bundle ` +
206
- `(keys present: ${keys.length ? keys.join(', ') : 'none'}). ` +
207
- `Pass it directly with --token <t> [--account <id>], or add it: ` +
208
- `agents secrets add ${bundle} CLOUDFLARE_API_TOKEN`);
209
- }
210
- return { apiToken, accountId };
211
- }
@@ -1,123 +0,0 @@
1
- import { type ShareConfig } from './config.js';
2
- /** DI seam for tests — override the real HTTP DELETE. `body`/`retryAfter` carry
3
- * the Worker's error response (read only on `!ok`) so a failed delete surfaces
4
- * WHY, via the bounded {@link extractShareHttpError}. */
5
- export type DeleteFn = (url: string, headers: Record<string, string>) => Promise<{
6
- ok: boolean;
7
- status: number;
8
- body?: string;
9
- retryAfter?: string;
10
- }>;
11
- /** DI seam for tests — override the real HTTP existence check (HEAD). */
12
- export type CheckFn = (url: string) => Promise<{
13
- status: number;
14
- }>;
15
- /** DI seam for tests — override the real HTTP GET of the revisions listing route. */
16
- export type RevisionsFetchFn = (url: string) => Promise<{
17
- status: number;
18
- contentType: string;
19
- body: string;
20
- }>;
21
- interface DeleteEndpoint {
22
- baseUrl: string;
23
- token: string;
24
- }
25
- interface ResolvedShareTarget {
26
- /** R2 object key for the page, `<user>/<slug>`. */
27
- key: string;
28
- /** R2 object key for the sibling OG cover, `<user>/<slug>.png`. */
29
- coverKey: string;
30
- }
31
- /**
32
- * Normalize any of the three accepted target forms to the R2 key that publish
33
- * would have written:
34
- * - a full share URL: `https://share.getrush.ai/<user>/<slug>`
35
- * - `<user>/<slug>`
36
- * - a bare `<slug>` — resolved against the caller's own namespace exactly as
37
- * `publishToEndpoint` resolves it at publish time (resolveShareUsername +
38
- * buildShareKey), so a bare slug always targets *your* published page.
39
- *
40
- * The URL and `<user>/<slug>` forms are taken as already the exact key a prior
41
- * publish produced (no re-sanitizing) — only the bare-slug form runs through
42
- * `buildShareKey`, because that's the one case where the slug hasn't already
43
- * been normalized by a publish.
44
- */
45
- export declare function resolveDeleteTarget(target: string, opts?: {
46
- githubUser?: string;
47
- }): Promise<ResolvedShareTarget>;
48
- interface DeleteObjectResult {
49
- key: string;
50
- url: string;
51
- /** Whether the object resolved (non-404) before the DELETE was issued. */
52
- existedBefore: boolean;
53
- /** Whether the Worker's DELETE call itself reported ok. */
54
- deleted: boolean;
55
- /** The postcondition: a follow-up check resolved 404 after the DELETE. */
56
- verified404: boolean;
57
- }
58
- /**
59
- * Delete one R2 object behind the share Worker and assert the postcondition.
60
- * `{"ok":true}` from the Worker is not evidence a page came down — R2 delete is
61
- * idempotent, so a DELETE on a key that was never there also returns `ok:true`.
62
- * This checks existence before (so callers can tell "deleted" from "was never
63
- * there") and re-checks after (so callers can tell "deleted" from "still public").
64
- */
65
- export declare function deleteObject(endpoint: DeleteEndpoint, key: string, opts?: {
66
- deleter?: DeleteFn;
67
- checker?: CheckFn;
68
- }): Promise<DeleteObjectResult>;
69
- interface DeleteShareOptions {
70
- /** Skip deleting the sibling `<slug>.png` OG cover (default: delete it too). */
71
- keepCover?: boolean;
72
- /** Skip deleting the target's retained revisions (`<key>/rev-*`, RUSH-2683
73
- * revision retention) — default: delete them too. A republished-then-deleted
74
- * share would otherwise leave its prior world-readable versions live for up
75
- * to the bucket's lifecycle max-age (see docs/distribution.md). */
76
- keepRevisions?: boolean;
77
- /** Treat an already-missing target as a no-op success instead of an error
78
- * (mirrors SQL's `DROP ... IF EXISTS`). Default: missing target is an error. */
79
- ifExists?: boolean;
80
- /** Override the GitHub username used to resolve a bare-slug target. */
81
- githubUser?: string;
82
- /** DI seam for tests — override the persisted share endpoint config. */
83
- config?: ShareConfig;
84
- /** DI seam for tests — override the keychain-backed write token. Selects BYO. */
85
- writeToken?: string;
86
- /** DI seam for tests — override `readSession()`. `null` means signed out. */
87
- session?: import('../identity/client.js').PhoenixSession | null;
88
- /** Force the BYO Cloudflare path even when signed in. */
89
- byo?: boolean;
90
- /** DI seam for tests — override the real HTTP DELETE. */
91
- deleter?: DeleteFn;
92
- /** DI seam for tests — override the real HTTP existence check. */
93
- checker?: CheckFn;
94
- /** DI seam for tests — override the real HTTP GET of the revisions listing route. */
95
- fetchRevisions?: RevisionsFetchFn;
96
- }
97
- export interface DeleteShareResult {
98
- key: string;
99
- url: string;
100
- existedBefore: boolean;
101
- verified404: boolean;
102
- /** True when `ifExists` was set and the target was already gone — nothing ran. */
103
- skipped?: boolean;
104
- cover?: {
105
- key: string;
106
- url: string;
107
- existedBefore: boolean;
108
- verified404: boolean;
109
- };
110
- /** Retained revisions of this target that were also deleted (omitted when
111
- * there were none, or `keepRevisions` was passed). */
112
- revisions?: Array<{
113
- key: string;
114
- url: string;
115
- existedBefore: boolean;
116
- verified404: boolean;
117
- }>;
118
- }
119
- /** Delete one share target (page + by default its OG cover) and verify both
120
- * are gone. Throws on an unverified takedown — never reports success for an
121
- * object that still resolves. */
122
- export declare function deleteShare(target: string, opts?: DeleteShareOptions): Promise<DeleteShareResult>;
123
- export {};
@@ -1,173 +0,0 @@
1
- // The delete path for `agents artifacts share delete` / `agents artifacts unshare` — an authed DELETE
2
- // to the Worker, which already implements it (worker-template.ts). Mirrors
3
- // publish.ts: pure target-resolution logic is exported for tests, the network
4
- // calls (a status check + a delete) sit behind an injectable DI seam.
5
- //
6
- // The Worker's R2 delete is idempotent — DELETE on a key that never existed still
7
- // returns `{"ok":true}` — so `{"ok":true}` alone is never proof of a takedown.
8
- // Every delete here is followed by a status check that must observe 404 before
9
- // the operation is reported as successful.
10
- import { resolveShareBackend } from './backend.js';
11
- import { buildShareKey, resolveShareUsername } from './publish.js';
12
- import { extractShareHttpError, formatShareHttpErrorDetail } from './http-error.js';
13
- /**
14
- * Normalize any of the three accepted target forms to the R2 key that publish
15
- * would have written:
16
- * - a full share URL: `https://share.getrush.ai/<user>/<slug>`
17
- * - `<user>/<slug>`
18
- * - a bare `<slug>` — resolved against the caller's own namespace exactly as
19
- * `publishToEndpoint` resolves it at publish time (resolveShareUsername +
20
- * buildShareKey), so a bare slug always targets *your* published page.
21
- *
22
- * The URL and `<user>/<slug>` forms are taken as already the exact key a prior
23
- * publish produced (no re-sanitizing) — only the bare-slug form runs through
24
- * `buildShareKey`, because that's the one case where the slug hasn't already
25
- * been normalized by a publish.
26
- */
27
- export async function resolveDeleteTarget(target, opts = {}) {
28
- const trimmed = target.trim();
29
- if (!trimmed)
30
- throw new Error('Share target is empty.');
31
- let key;
32
- if (/^https?:\/\//i.test(trimmed)) {
33
- const url = new URL(trimmed);
34
- const segments = url.pathname
35
- .replace(/^\/+|\/+$/g, '')
36
- .split('/')
37
- .filter(Boolean)
38
- .map(decodeURIComponent);
39
- if (segments.length < 2) {
40
- throw new Error(`Not a share page URL (expected .../<user>/<slug>): ${trimmed}`);
41
- }
42
- key = segments.slice(0, 2).join('/');
43
- }
44
- else if (trimmed.includes('/')) {
45
- const segments = trimmed.replace(/^\/+|\/+$/g, '').split('/').filter(Boolean);
46
- if (segments.length !== 2) {
47
- throw new Error(`Expected <user>/<slug>, got: ${trimmed}`);
48
- }
49
- key = segments.join('/');
50
- }
51
- else {
52
- const username = await resolveShareUsername(opts);
53
- key = buildShareKey(username, trimmed);
54
- }
55
- return { key, coverKey: `${key}.png` };
56
- }
57
- async function defaultCheck(url) {
58
- const res = await fetch(url, { method: 'HEAD' });
59
- return { status: res.status };
60
- }
61
- async function defaultDelete(url, headers) {
62
- const res = await fetch(url, { method: 'DELETE', headers });
63
- if (res.ok)
64
- return { ok: true, status: res.status };
65
- const body = await res.text().catch(() => undefined);
66
- return { ok: false, status: res.status, body, retryAfter: res.headers.get('retry-after') ?? undefined };
67
- }
68
- async function defaultRevisionsFetch(url) {
69
- const res = await fetch(url, { headers: { accept: 'application/json' } });
70
- return { status: res.status, contentType: res.headers.get('content-type') ?? '', body: await res.text() };
71
- }
72
- /**
73
- * List the R2 keys of a slug's retained revisions via the Worker's
74
- * `?revisions=json` route (worker-template.ts `renderRevisions`). Best-effort:
75
- * any failure to reach or parse the route (an endpoint provisioned before
76
- * revisions existed, a network blip) yields an empty list rather than
77
- * throwing — purging revisions is a safety improvement on top of the primary
78
- * delete contract (take the canonical page down), never a reason to fail it.
79
- */
80
- async function listRevisionKeys(endpoint, key, fetchRevisions) {
81
- try {
82
- const url = `${endpoint.baseUrl.replace(/\/+$/, '')}/${key}?revisions=json`;
83
- const res = await fetchRevisions(url);
84
- if (res.status !== 200 || !/application\/json/i.test(res.contentType))
85
- return [];
86
- const data = JSON.parse(res.body);
87
- if (!Array.isArray(data.revisions))
88
- return [];
89
- return data.revisions.map((r) => String(r.key ?? '')).filter(Boolean);
90
- }
91
- catch {
92
- return [];
93
- }
94
- }
95
- /**
96
- * Delete one R2 object behind the share Worker and assert the postcondition.
97
- * `{"ok":true}` from the Worker is not evidence a page came down — R2 delete is
98
- * idempotent, so a DELETE on a key that was never there also returns `ok:true`.
99
- * This checks existence before (so callers can tell "deleted" from "was never
100
- * there") and re-checks after (so callers can tell "deleted" from "still public").
101
- */
102
- export async function deleteObject(endpoint, key, opts = {}) {
103
- const url = `${endpoint.baseUrl.replace(/\/+$/, '')}/${key}`;
104
- const check = opts.checker ?? defaultCheck;
105
- const del = opts.deleter ?? defaultDelete;
106
- const before = await check(url);
107
- const existedBefore = before.status !== 404;
108
- const r = await del(url, { authorization: `Bearer ${endpoint.token}` });
109
- if (!r.ok) {
110
- const detail = formatShareHttpErrorDetail(extractShareHttpError({ status: r.status, body: r.body, retryAfter: r.retryAfter }));
111
- throw new Error(`Delete failed (${r.status}) for ${url}${detail}. Check the bearer (Phoenix session or WRITE_TOKEN), or that 'agents artifacts setup' completed.`);
112
- }
113
- const after = await check(url);
114
- const verified404 = after.status === 404;
115
- return { key, url, existedBefore, deleted: r.ok, verified404 };
116
- }
117
- /** Delete one share target (page + by default its OG cover) and verify both
118
- * are gone. Throws on an unverified takedown — never reports success for an
119
- * object that still resolves. */
120
- export async function deleteShare(target, opts = {}) {
121
- const backend = resolveShareBackend(opts);
122
- const githubUser = backend.kind === 'managed'
123
- ? backend.namespace
124
- : opts.githubUser || backend.namespace || undefined;
125
- const resolved = await resolveDeleteTarget(target, { githubUser });
126
- const endpoint = { baseUrl: backend.baseUrl, token: backend.token };
127
- const page = await deleteObject(endpoint, resolved.key, { deleter: opts.deleter, checker: opts.checker });
128
- if (!page.existedBefore) {
129
- if (opts.ifExists) {
130
- return { key: page.key, url: page.url, existedBefore: false, verified404: page.verified404, skipped: true };
131
- }
132
- throw new Error(`Nothing to delete — ${page.url} was already not found. Pass --if-exists to treat this as a no-op instead of an error.`);
133
- }
134
- if (!page.verified404) {
135
- throw new Error(`Delete reported success but ${page.url} still resolves — takedown NOT verified. Retry, or investigate the Worker/R2 directly.`);
136
- }
137
- const result = {
138
- key: page.key,
139
- url: page.url,
140
- existedBefore: page.existedBefore,
141
- verified404: page.verified404,
142
- };
143
- if (!opts.keepCover) {
144
- const cover = await deleteObject(endpoint, resolved.coverKey, { deleter: opts.deleter, checker: opts.checker });
145
- result.cover = {
146
- key: cover.key,
147
- url: cover.url,
148
- existedBefore: cover.existedBefore,
149
- verified404: cover.verified404,
150
- };
151
- // A missing cover is normal (non-HTML publishes, or --no-cover at publish
152
- // time never made one) — only a cover that existed and is still up is a bug.
153
- if (cover.existedBefore && !cover.verified404) {
154
- throw new Error(`Cover delete reported success but ${cover.url} still resolves — takedown NOT verified. Retry, or pass --keep-cover and delete it manually.`);
155
- }
156
- }
157
- if (!opts.keepRevisions) {
158
- const revisionKeys = await listRevisionKeys(endpoint, resolved.key, opts.fetchRevisions ?? defaultRevisionsFetch);
159
- if (revisionKeys.length > 0) {
160
- const revisions = [];
161
- for (const revisionKey of revisionKeys) {
162
- const rev = await deleteObject(endpoint, revisionKey, { deleter: opts.deleter, checker: opts.checker });
163
- revisions.push({ key: rev.key, url: rev.url, existedBefore: rev.existedBefore, verified404: rev.verified404 });
164
- }
165
- result.revisions = revisions;
166
- const stillUp = revisions.filter((r) => r.existedBefore && !r.verified404);
167
- if (stillUp.length > 0) {
168
- throw new Error(`Delete reported success but ${stillUp.length} retained revision(s) still resolve — takedown NOT verified. Retry, or pass --keep-revisions and delete them manually.`);
169
- }
170
- }
171
- }
172
- return result;
173
- }
@@ -1,20 +0,0 @@
1
- /**
2
- * Make an HTML page self-contained enough to view on share.getrush.ai.
3
- *
4
- * Chrome "Save as" (and some renderers) leave `file:///…html#section` TOC
5
- * links and sibling image paths. Those work on disk and 404 on the share
6
- * Worker, which only stores the one PUT body. Rewrite same-document file
7
- * anchors to `#section`, and inline local image files as data URIs.
8
- *
9
- * https citations, data: URIs already in the page, and missing files are
10
- * left alone. This is not a bundler — it only fixes what would otherwise
11
- * render as a broken page after a single-file publish.
12
- */
13
- /**
14
- * `href="file:///tmp/page.html#purpose"` → `href="#purpose"`.
15
- * Bare `file:///tmp/page.html` (no hash) becomes `href="#"`.
16
- */
17
- export declare function rewriteFileAnchors(html: string): string;
18
- /** Replace local image src/href with a data URI when the file exists next to the page. */
19
- export declare function inlineLocalAssets(html: string, htmlPath: string): string;
20
- export declare function prepareShareHtml(html: string, htmlPath: string): string;
@@ -1,88 +0,0 @@
1
- /**
2
- * Make an HTML page self-contained enough to view on share.getrush.ai.
3
- *
4
- * Chrome "Save as" (and some renderers) leave `file:///…html#section` TOC
5
- * links and sibling image paths. Those work on disk and 404 on the share
6
- * Worker, which only stores the one PUT body. Rewrite same-document file
7
- * anchors to `#section`, and inline local image files as data URIs.
8
- *
9
- * https citations, data: URIs already in the page, and missing files are
10
- * left alone. This is not a bundler — it only fixes what would otherwise
11
- * render as a broken page after a single-file publish.
12
- */
13
- import { existsSync, readFileSync } from 'node:fs';
14
- import { dirname, extname, resolve } from 'node:path';
15
- import { fileURLToPath } from 'node:url';
16
- /** Skip inlining a sibling that would bloat the page past a reasonable PUT. */
17
- const MAX_INLINE_BYTES = 4 * 1024 * 1024;
18
- const IMAGE_MIME = {
19
- '.png': 'image/png',
20
- '.jpg': 'image/jpeg',
21
- '.jpeg': 'image/jpeg',
22
- '.gif': 'image/gif',
23
- '.webp': 'image/webp',
24
- '.svg': 'image/svg+xml',
25
- '.ico': 'image/x-icon',
26
- '.avif': 'image/avif',
27
- };
28
- /**
29
- * `href="file:///tmp/page.html#purpose"` → `href="#purpose"`.
30
- * Bare `file:///tmp/page.html` (no hash) becomes `href="#"`.
31
- */
32
- export function rewriteFileAnchors(html) {
33
- return html.replace(/\b(href\s*=\s*["'])file:\/\/[^"'#]*(#[^"']*)?(["'])/gi, (_m, attr, hash, quote) => `${attr}${hash ?? '#'}${quote}`);
34
- }
35
- /**
36
- * Only a bare path or a `file:` URL can name a local asset; every other scheme
37
- * is left alone. A single-letter "scheme" is a Windows drive (`C:\\x.png`), not a URL.
38
- */
39
- function isRemoteOrSpecial(url) {
40
- if (url.startsWith('//') || url.startsWith('#'))
41
- return true;
42
- const scheme = /^([a-z][a-z0-9+.-]*):/i.exec(url)?.[1];
43
- return scheme !== undefined && scheme.length > 1 && scheme.toLowerCase() !== 'file';
44
- }
45
- function localImagePath(url, htmlDir) {
46
- if (!url || isRemoteOrSpecial(url))
47
- return null;
48
- const withoutHash = url.split('#')[0] ?? url;
49
- let fsPath;
50
- if (withoutHash.startsWith('file:')) {
51
- try {
52
- fsPath = fileURLToPath(withoutHash);
53
- }
54
- catch {
55
- return null;
56
- }
57
- }
58
- else {
59
- fsPath = resolve(htmlDir, withoutHash);
60
- }
61
- if (!existsSync(fsPath))
62
- return null;
63
- const mime = IMAGE_MIME[extname(fsPath).toLowerCase()];
64
- if (!mime)
65
- return null;
66
- return { path: fsPath, mime };
67
- }
68
- /** Replace local image src/href with a data URI when the file exists next to the page. */
69
- export function inlineLocalAssets(html, htmlPath) {
70
- const dir = dirname(htmlPath);
71
- return html.replace(/\b((?:src|href)\s*=\s*["'])([^"']+)(["'])/gi, (full, pre, url, post) => {
72
- const local = localImagePath(url, dir);
73
- if (!local)
74
- return full;
75
- try {
76
- const buf = readFileSync(local.path);
77
- if (buf.length > MAX_INLINE_BYTES)
78
- return full;
79
- return `${pre}data:${local.mime};base64,${buf.toString('base64')}${post}`;
80
- }
81
- catch {
82
- return full;
83
- }
84
- });
85
- }
86
- export function prepareShareHtml(html, htmlPath) {
87
- return inlineLocalAssets(rewriteFileAnchors(html), htmlPath);
88
- }
@@ -1,53 +0,0 @@
1
- /**
2
- * ONE bounded extractor for the share Worker's error responses.
3
- *
4
- * Every share HTTP call (publish PUT, list/revisions GET, delete DELETE)
5
- * previously threw on `!ok` with only the status code, discarding the JSON error
6
- * body the Worker actually returns (`{"error":"…"}` for a 400/413/429, plus a
7
- * `Retry-After` on a 429). The result: a rate-limited or quota-exceeded publish
8
- * surfaced as a bare "Publish failed (429)" with no reason. This pulls the
9
- * server's own `error` string and preserves the status + `Retry-After`, so the
10
- * user sees why.
11
- *
12
- * Bounded and safe by construction: it parses at most {@link MAX_ERROR_BODY_BYTES}
13
- * of the body, only lifts a string `error` field, caps its length, and NEVER
14
- * surfaces an arbitrary body — a non-JSON or oversized body yields no message,
15
- * so an HTML error page or a huge payload can't leak into a CLI error string.
16
- */
17
- /** The raw pieces a caller's fetch wrapper hands in — status plus the (optional)
18
- * response body text and `Retry-After` header value. */
19
- interface ShareHttpResponse {
20
- status: number;
21
- /** The response body text, when the caller read it (only on `!ok` paths). */
22
- body?: string;
23
- /** The `Retry-After` header value, when present (a 429 carries it). */
24
- retryAfter?: string;
25
- }
26
- /** The extracted, bounded error facts. `serverMessage` is present only when the
27
- * body parsed as JSON carrying a non-empty string `error`. */
28
- interface ShareHttpError {
29
- status: number;
30
- serverMessage?: string;
31
- retryAfter?: string;
32
- }
33
- /** Never parse more than this many chars of a body — a legitimate `{"error":"…"}`
34
- * is tiny; a larger body is an HTML page or noise we deliberately don't surface. */
35
- export declare const MAX_ERROR_BODY_BYTES = 4096;
36
- /** Cap the surfaced message so even a pathological (but valid-JSON) `error`
37
- * string can't blow up the CLI output. */
38
- export declare const MAX_ERROR_MESSAGE_CHARS = 300;
39
- /**
40
- * Extract the bounded error facts from a share Worker response. Pulls a string
41
- * `error` field from a small JSON body, preserves status + `Retry-After`, and
42
- * never surfaces an arbitrary body.
43
- */
44
- export declare function extractShareHttpError(res: ShareHttpResponse): ShareHttpError;
45
- /**
46
- * A one-line suffix summarizing the server's structured error, or `''` when the
47
- * response carried nothing extractable. Appended after the status-bearing prefix
48
- * a caller already builds, e.g.:
49
- * `Publish failed (429) for <url>` + `formatShareHttpErrorDetail(err)`
50
- * → `Publish failed (429) for <url> — rate limit exceeded; retry after 37s`
51
- */
52
- export declare function formatShareHttpErrorDetail(err: ShareHttpError): string;
53
- export {};