@phnx-labs/agents-cli 1.22.54 → 1.22.56
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/README.md +19 -2
- package/dist/commands/routines.js +31 -2
- package/dist/commands/share.d.ts +27 -0
- package/dist/commands/share.js +92 -6
- package/dist/commands/view.d.ts +8 -0
- package/dist/commands/view.js +31 -4
- package/dist/lib/accounting/usage.d.ts +31 -0
- package/dist/lib/accounting/usage.js +45 -4
- package/dist/lib/browser/cdp.d.ts +1 -1
- package/dist/lib/browser/cdp.js +1 -1
- package/dist/lib/browser/ffmpeg.d.ts +12 -0
- package/dist/lib/browser/ffmpeg.js +184 -0
- package/dist/lib/browser/service.js +119 -25
- package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
- package/dist/lib/daemon/browser-task-reap-service.js +26 -0
- package/dist/lib/daemon/daemon.js +57 -172
- package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
- package/dist/lib/daemon/heartbeat-service.js +26 -0
- package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
- package/dist/lib/daemon/monitor-engine-service.js +15 -7
- package/dist/lib/daemon/runner.d.ts +15 -0
- package/dist/lib/daemon/runner.js +23 -0
- package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
- package/dist/lib/daemon/secrets-broker-service.js +17 -32
- package/dist/lib/daemon/service.d.ts +2 -2
- package/dist/lib/daemon/service.js +1 -1
- package/dist/lib/daemon/session-state-service.d.ts +21 -0
- package/dist/lib/daemon/session-state-service.js +34 -0
- package/dist/lib/daemon/supervisor.d.ts +17 -7
- package/dist/lib/daemon/supervisor.js +87 -14
- package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
- package/dist/lib/daemon/tmux-reap-service.js +28 -0
- package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
- package/dist/lib/daemon/webhook-receiver-service.js +17 -0
- package/dist/lib/daemon-services.d.ts +1 -1
- package/dist/lib/daemon-services.js +20 -0
- package/dist/lib/devices/harness-inventory.js +5 -2
- package/dist/lib/monitors/engine.d.ts +5 -1
- package/dist/lib/monitors/engine.js +5 -3
- package/dist/lib/overdue.js +7 -37
- package/dist/lib/scheduler.d.ts +21 -2
- package/dist/lib/scheduler.js +28 -5
- package/dist/lib/scheduling/routines.d.ts +68 -0
- package/dist/lib/scheduling/routines.js +126 -1
- package/dist/lib/session/db.d.ts +2 -1
- package/dist/lib/session/db.js +2 -1
- package/dist/lib/share/capture.js +11 -2
- package/dist/lib/share/provision.d.ts +3 -2
- package/dist/lib/share/provision.js +9 -4
- package/dist/lib/share/publish.d.ts +4 -1
- package/dist/lib/share/publish.js +27 -1
- package/dist/lib/share/worker-template.d.ts +14 -0
- package/dist/lib/share/worker-template.js +330 -73
- package/dist/lib/view-types.d.ts +6 -1
- package/package.json +8 -2
|
@@ -1,78 +1,90 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
//
|
|
26
|
-
//
|
|
27
|
-
//
|
|
28
|
-
//
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
// - GET /<username>?format=json — public machine-readable listing of that user's
|
|
49
|
-
// ACTIVE shares (`agents artifacts share list`). Same single-segment path as the HTML
|
|
50
|
-
// gallery and gated on the SAME "does <username>/ hold any object" check, so it
|
|
51
|
-
// only intercepts a genuine namespace — a legacy flat slug with ?format=json
|
|
52
|
-
// still serves its real content, never a fake empty listing.
|
|
53
|
-
// - GET /<slug> — backward-compat flat slug (legacy shares before
|
|
54
|
-
// per-user namespaces).
|
|
55
|
-
//
|
|
56
|
-
// Emitted as a string, so it compiles into `dist/**` and ships with no
|
|
57
|
-
// package.json#files change. `provision.ts` uploads
|
|
58
|
-
// this verbatim as an ES-module Worker with a BUCKET (R2) binding + a WRITE_TOKEN secret.
|
|
59
|
-
/**
|
|
60
|
-
* Render the Worker source. Pure — the R2 binding + token are wired at deploy time.
|
|
61
|
-
*
|
|
62
|
-
* The literal below still spells the CLI `agents share` in its provenance comment,
|
|
63
|
-
* its root response, and its gallery title, even though the command is now
|
|
64
|
-
* `agents artifacts share` (RUSH-2580). That is deliberate: `hashWorkerScript` of
|
|
65
|
-
* this exact text is what `shareTemplateStatus` compares a provisioned endpoint's
|
|
66
|
-
* recorded `templateHash` against, so editing ANY byte here marks every already-
|
|
67
|
-
* deployed endpoint `outdated` — which makes `agents artifacts share list` refuse
|
|
68
|
-
* until its owner re-runs `agents artifacts share update`. Cosmetic renames are not
|
|
69
|
-
* worth that; change this text only alongside a real Worker behavior change.
|
|
70
|
-
*/
|
|
1
|
+
import { buildSync } from 'esbuild';
|
|
2
|
+
import { dirname } from 'node:path';
|
|
3
|
+
import { fileURLToPath } from 'node:url';
|
|
4
|
+
let bundledWorker;
|
|
5
|
+
let nodeWorkerScript;
|
|
6
|
+
/** Bundle the Worker renderer into an ES module plus workerd-compiled WASM modules. */
|
|
7
|
+
export function renderWorkerBundle() {
|
|
8
|
+
if (bundledWorker)
|
|
9
|
+
return bundledWorker;
|
|
10
|
+
const result = buildSync({
|
|
11
|
+
stdin: {
|
|
12
|
+
contents: renderWorkerSource(),
|
|
13
|
+
loader: 'js',
|
|
14
|
+
resolveDir: dirname(fileURLToPath(import.meta.url)),
|
|
15
|
+
sourcefile: 'agents-share-worker.js',
|
|
16
|
+
},
|
|
17
|
+
bundle: true,
|
|
18
|
+
format: 'esm',
|
|
19
|
+
platform: 'browser',
|
|
20
|
+
target: 'es2022',
|
|
21
|
+
write: false,
|
|
22
|
+
outdir: 'worker-bundle',
|
|
23
|
+
assetNames: '[name]-[hash]',
|
|
24
|
+
minify: true,
|
|
25
|
+
// Fonts are plain data and can live in JavaScript. WASM must remain a
|
|
26
|
+
// compiled module: workerd deliberately forbids runtime code generation,
|
|
27
|
+
// so esbuild's `binary` loader produces a bundle that works in Node but
|
|
28
|
+
// throws "Wasm code generation disallowed by embedder" in Cloudflare.
|
|
29
|
+
loader: { '.wasm': 'copy', '.woff': 'binary' },
|
|
30
|
+
});
|
|
31
|
+
const script = result.outputFiles.find((output) => output.path.endsWith('.js'));
|
|
32
|
+
if (!script)
|
|
33
|
+
throw new Error('Worker bundling produced no JavaScript output.');
|
|
34
|
+
const modules = result.outputFiles
|
|
35
|
+
.filter((output) => output.path.endsWith('.wasm'))
|
|
36
|
+
.map((output) => ({
|
|
37
|
+
name: output.path.split('/').pop(),
|
|
38
|
+
contentType: 'application/wasm',
|
|
39
|
+
contents: new Uint8Array(output.contents),
|
|
40
|
+
}));
|
|
41
|
+
if (modules.length !== 2) {
|
|
42
|
+
throw new Error(`Worker bundling produced ${modules.length} WASM modules; expected yoga and resvg.`);
|
|
43
|
+
}
|
|
44
|
+
bundledWorker = { script: script.text, modules };
|
|
45
|
+
return bundledWorker;
|
|
46
|
+
}
|
|
47
|
+
/** Single-file representation for direct Node tests, which cannot import compiled WASM modules. */
|
|
71
48
|
export function renderWorkerScript() {
|
|
72
|
-
|
|
49
|
+
if (nodeWorkerScript)
|
|
50
|
+
return nodeWorkerScript;
|
|
51
|
+
const result = buildSync({
|
|
52
|
+
stdin: {
|
|
53
|
+
contents: renderWorkerSource(),
|
|
54
|
+
loader: 'js',
|
|
55
|
+
resolveDir: dirname(fileURLToPath(import.meta.url)),
|
|
56
|
+
sourcefile: 'agents-share-worker.js',
|
|
57
|
+
},
|
|
58
|
+
bundle: true,
|
|
59
|
+
format: 'esm',
|
|
60
|
+
platform: 'browser',
|
|
61
|
+
target: 'es2022',
|
|
62
|
+
write: false,
|
|
63
|
+
minify: true,
|
|
64
|
+
loader: { '.wasm': 'binary', '.woff': 'binary' },
|
|
65
|
+
});
|
|
66
|
+
const output = result.outputFiles[0];
|
|
67
|
+
if (!output)
|
|
68
|
+
throw new Error('Worker test bundling produced no JavaScript output.');
|
|
69
|
+
nodeWorkerScript = output.text;
|
|
70
|
+
return nodeWorkerScript;
|
|
71
|
+
}
|
|
72
|
+
/** Unbundled Worker source. Kept separate so esbuild can resolve npm modules. */
|
|
73
|
+
export function renderWorkerSource() {
|
|
74
|
+
return `import satori, { init as initSatori } from 'satori/wasm';
|
|
75
|
+
import initYoga from 'yoga-wasm-web';
|
|
76
|
+
import { Resvg, initWasm as initResvg } from '@resvg/resvg-wasm';
|
|
77
|
+
import yogaWasm from 'yoga-wasm-web/dist/yoga.wasm';
|
|
78
|
+
import resvgWasm from '@resvg/resvg-wasm/index_bg.wasm';
|
|
79
|
+
import interRegular from '@fontsource/inter/files/inter-latin-400-normal.woff';
|
|
80
|
+
import interBold from '@fontsource/inter/files/inter-latin-700-normal.woff';
|
|
81
|
+
import jetbrainsMono from '@fontsource/jetbrains-mono/files/jetbrains-mono-latin-600-normal.woff';
|
|
82
|
+
|
|
83
|
+
// GENERATED by agents-cli agents share setup — do not edit here; edit
|
|
73
84
|
// src/lib/share/worker-template.ts and re-run setup.
|
|
74
85
|
export const hooks = {
|
|
75
86
|
verifyPhoenixToken: defaultVerifyPhoenixToken,
|
|
87
|
+
renderOgCard: renderOgCard,
|
|
76
88
|
};
|
|
77
89
|
|
|
78
90
|
export default {
|
|
@@ -91,6 +103,21 @@ export default {
|
|
|
91
103
|
return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
|
|
92
104
|
}
|
|
93
105
|
|
|
106
|
+
// POST /__ticket — mint a short-lived, single-navigation login ticket for the
|
|
107
|
+
// authenticated owner (PHNX-3370). The CLI (which holds the Phoenix bearer)
|
|
108
|
+
// calls this, then opens the owner's page with ?phoenix_ticket=<t> so the
|
|
109
|
+
// browser trades it for the __share cookie and the inline visibility control
|
|
110
|
+
// goes live. The ticket is SELF-signed with the Worker's own HMAC secret — no
|
|
111
|
+
// external ticket service — and carries the SAME identity authorizeWrite
|
|
112
|
+
// already proved, so it grants nothing the caller didn't already have.
|
|
113
|
+
if (request.method === 'POST' && firstSeg === '__ticket') {
|
|
114
|
+
const auth = await authorizeWrite(request, env);
|
|
115
|
+
if (auth.error) return auth.error;
|
|
116
|
+
const ticket = await signSelfTicket({ userId: auth.owner, email: auth.email || '' }, env);
|
|
117
|
+
if (!ticket) return json({ error: 'ticket minting is not configured' }, 501);
|
|
118
|
+
return json({ ticket: ticket }, 200);
|
|
119
|
+
}
|
|
120
|
+
|
|
94
121
|
if (request.method === 'PUT') {
|
|
95
122
|
const auth = await authorizeWrite(request, env);
|
|
96
123
|
if (auth.error) return auth.error;
|
|
@@ -162,6 +189,8 @@ export default {
|
|
|
162
189
|
const date = readText('date');
|
|
163
190
|
const avatar = readText('avatar');
|
|
164
191
|
const label = readText('label');
|
|
192
|
+
const ogTitle = readText('og-title');
|
|
193
|
+
const ogDescription = readText('og-description');
|
|
165
194
|
const labelSource = request.headers.get('x-share-label-source') || '';
|
|
166
195
|
let extraMeta = {};
|
|
167
196
|
// The -u meta companion carries the whole raw object percent-encoded, so a
|
|
@@ -210,6 +239,8 @@ export default {
|
|
|
210
239
|
if (avatar) customMetadata['avatar'] = avatar;
|
|
211
240
|
if (label) customMetadata['label'] = label;
|
|
212
241
|
if (labelSource) customMetadata['label-source'] = labelSource;
|
|
242
|
+
if (ogTitle) customMetadata['og-title'] = ogTitle;
|
|
243
|
+
if (ogDescription) customMetadata['og-description'] = ogDescription;
|
|
213
244
|
|
|
214
245
|
// Revision retention (RUSH-2683): R2 has no native object versioning, so a
|
|
215
246
|
// republish over an EXISTING key first copies the current object to
|
|
@@ -238,6 +269,10 @@ export default {
|
|
|
238
269
|
httpMetadata: { contentType },
|
|
239
270
|
customMetadata,
|
|
240
271
|
});
|
|
272
|
+
// A managed republish may change the title/description. Invalidate only
|
|
273
|
+
// its generated sibling so the next crawler receives a fresh card; BYO
|
|
274
|
+
// publishes send no OG metadata and keep their explicitly uploaded cover.
|
|
275
|
+
if (ogTitle) await env.BUCKET.delete(path + '.png');
|
|
241
276
|
return json({ ok: true, url: url.origin + '/' + path, expiresAt: expiresAt || null, unlisted: visibility === 'unlisted', visibility }, 200);
|
|
242
277
|
}
|
|
243
278
|
|
|
@@ -263,6 +298,7 @@ export default {
|
|
|
263
298
|
if (!edit || typeof edit !== 'object') return json({ error: 'PATCH body must be an object' }, 400);
|
|
264
299
|
const metadata = { ...(existing.customMetadata || {}) };
|
|
265
300
|
const previousVisibility = metadata.visibility || 'public';
|
|
301
|
+
let visibilityChanged = false;
|
|
266
302
|
if (Object.prototype.hasOwnProperty.call(edit, 'label')) {
|
|
267
303
|
if (edit.label !== null && typeof edit.label !== 'string') return json({ error: 'label must be a string or null' }, 400);
|
|
268
304
|
if (typeof edit.label === 'string' && (edit.label.trim() !== edit.label || !edit.label || edit.label.length > 200)) return json({ error: 'label must be 1-200 trimmed characters' }, 400);
|
|
@@ -289,6 +325,7 @@ export default {
|
|
|
289
325
|
}
|
|
290
326
|
}
|
|
291
327
|
metadata.visibility = visibility;
|
|
328
|
+
visibilityChanged = visibility !== previousVisibility;
|
|
292
329
|
// org_domain is meaningful only for org — set it there, drop the stale
|
|
293
330
|
// value on any move away from org so it can never gate a later read.
|
|
294
331
|
if (visibility === 'org') metadata.org_domain = emailDomain(auth.email);
|
|
@@ -327,6 +364,16 @@ export default {
|
|
|
327
364
|
if (existing.etag) putOpts.onlyIf = { etagMatches: existing.etag };
|
|
328
365
|
const putResult = await env.BUCKET.put(path, existing.body, putOpts);
|
|
329
366
|
if (putResult === null) return json({ error: 'conflict', key: path }, 409);
|
|
367
|
+
// A generated cover copies the canonical visibility gate into its own R2
|
|
368
|
+
// metadata. Invalidate it after a successful visibility rewrite so the
|
|
369
|
+
// next cover GET re-gates and renders from the canonical page. Explicitly
|
|
370
|
+
// uploaded BYO siblings carry no marker and remain independent assets.
|
|
371
|
+
if (visibilityChanged) {
|
|
372
|
+
const cover = await env.BUCKET.get(path + '.png');
|
|
373
|
+
if (cover && cover.customMetadata && cover.customMetadata['og-generated'] === 'true') {
|
|
374
|
+
await env.BUCKET.delete(path + '.png');
|
|
375
|
+
}
|
|
376
|
+
}
|
|
330
377
|
return json({ ok: true, url: url.origin + '/' + path, label: metadata.label || null, meta: extraMetaOf(metadata), visibility: metadata.visibility || 'public', previousVisibility: previousVisibility }, 200);
|
|
331
378
|
}
|
|
332
379
|
|
|
@@ -383,6 +430,79 @@ export default {
|
|
|
383
430
|
);
|
|
384
431
|
}
|
|
385
432
|
|
|
433
|
+
// Managed OG cover: generated siblings are caches, never visibility
|
|
434
|
+
// authorities. Gate every request against the current canonical page and
|
|
435
|
+
// bind each cached render to that page's etag. Re-read after rendering and
|
|
436
|
+
// after storing so a concurrent PATCH cannot publish a card from the old
|
|
437
|
+
// visibility/title/description snapshot. Explicit BYO siblings carry no
|
|
438
|
+
// generated marker and continue through the ordinary object route below.
|
|
439
|
+
if (segments.length === 2 && path.endsWith('.png')) {
|
|
440
|
+
let existingCover = await env.BUCKET.get(path);
|
|
441
|
+
if (!existingCover || (existingCover.customMetadata && existingCover.customMetadata['og-generated'] === 'true')) {
|
|
442
|
+
const pagePath = path.slice(0, -4);
|
|
443
|
+
const viewer = await resolveViewer(request, env, url);
|
|
444
|
+
if (viewer.redirect) return viewer.redirect;
|
|
445
|
+
for (let attempt = 0; attempt < 3; attempt += 1) {
|
|
446
|
+
const page = await env.BUCKET.get(pagePath);
|
|
447
|
+
if (!page) return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
|
|
448
|
+
const pageVisibility = (page.customMetadata && page.customMetadata.visibility) || 'public';
|
|
449
|
+
if (viewer.error && isIdentityGated(pageVisibility)) return viewer.error;
|
|
450
|
+
const denied = gateVisibility(url, env, page, viewer.identity || null);
|
|
451
|
+
if (denied) return denied;
|
|
452
|
+
|
|
453
|
+
if (existingCover && existingCover.customMetadata['og-source-etag'] === page.etag) {
|
|
454
|
+
const current = await env.BUCKET.get(pagePath);
|
|
455
|
+
if (!current || current.etag !== page.etag) { existingCover = null; continue; }
|
|
456
|
+
const currentDenied = gateVisibility(url, env, current, viewer.identity || null);
|
|
457
|
+
if (currentDenied) return currentDenied;
|
|
458
|
+
return new Response(request.method === 'HEAD' ? null : existingCover.body, {
|
|
459
|
+
status: 200,
|
|
460
|
+
headers: managedCoverHeaders(pageVisibility),
|
|
461
|
+
});
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
const pageHtml = await page.text();
|
|
465
|
+
const meta = page.customMetadata || {};
|
|
466
|
+
let png;
|
|
467
|
+
try {
|
|
468
|
+
png = await hooks.renderOgCard({
|
|
469
|
+
title: meta['og-title'] || extractHtmlMeta(pageHtml, 'title') || meta.label || segments[1],
|
|
470
|
+
description: meta['og-description'] || extractHtmlMeta(pageHtml, 'description') || '',
|
|
471
|
+
handle: segments[0],
|
|
472
|
+
visibility: pageVisibility,
|
|
473
|
+
orgDomain: meta.org_domain || '',
|
|
474
|
+
});
|
|
475
|
+
} catch (error) {
|
|
476
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
477
|
+
return new Response('OG card render failed: ' + detail, {
|
|
478
|
+
status: 500,
|
|
479
|
+
headers: { 'content-type': 'text/plain; charset=utf-8' },
|
|
480
|
+
});
|
|
481
|
+
}
|
|
482
|
+
const beforeStore = await env.BUCKET.get(pagePath);
|
|
483
|
+
if (!beforeStore || beforeStore.etag !== page.etag) { existingCover = null; continue; }
|
|
484
|
+
await env.BUCKET.put(path, png, {
|
|
485
|
+
httpMetadata: { contentType: 'image/png' },
|
|
486
|
+
customMetadata: { ...meta, 'og-generated': 'true', 'og-source-etag': page.etag },
|
|
487
|
+
});
|
|
488
|
+
const beforeServe = await env.BUCKET.get(pagePath);
|
|
489
|
+
if (!beforeServe || beforeServe.etag !== page.etag) {
|
|
490
|
+
const justStored = await env.BUCKET.get(path);
|
|
491
|
+
if (justStored && justStored.customMetadata && justStored.customMetadata['og-source-etag'] === page.etag) {
|
|
492
|
+
await env.BUCKET.delete(path);
|
|
493
|
+
}
|
|
494
|
+
existingCover = null;
|
|
495
|
+
continue;
|
|
496
|
+
}
|
|
497
|
+
return new Response(request.method === 'HEAD' ? null : png, {
|
|
498
|
+
status: 200,
|
|
499
|
+
headers: managedCoverHeaders(pageVisibility),
|
|
500
|
+
});
|
|
501
|
+
}
|
|
502
|
+
return new Response('cover changed during rendering; retry', { status: 503, headers: { 'content-type': 'text/plain', 'retry-after': '1' } });
|
|
503
|
+
}
|
|
504
|
+
}
|
|
505
|
+
|
|
386
506
|
const obj = await env.BUCKET.get(path);
|
|
387
507
|
if (!obj) return new Response('not found', { status: 404, headers: { 'content-type': 'text/plain' } });
|
|
388
508
|
const expiresAt = obj.customMetadata && obj.customMetadata['expires-at'];
|
|
@@ -694,7 +814,7 @@ async function renderRevisions(bucket, origin, key, method, identityGated) {
|
|
|
694
814
|
// before it ever reaches this Worker; see RESERVED_META_KEYS in publish.ts).
|
|
695
815
|
// One list, reused both to strip a same-named --meta collision on write and
|
|
696
816
|
// to split arbitrary --meta entries back out on read.
|
|
697
|
-
var RESERVED_METADATA_KEYS = ['expires-at', 'published-at', 'visibility', 'owner', 'org_domain', 'agent', 'session', 'host', 'repo', 'date', 'avatar', 'label', 'label-source'];
|
|
817
|
+
var RESERVED_METADATA_KEYS = ['expires-at', 'published-at', 'visibility', 'owner', 'org_domain', 'agent', 'session', 'host', 'repo', 'date', 'avatar', 'label', 'label-source', 'og-title', 'og-description', 'og-generated', 'og-source-etag'];
|
|
698
818
|
var PUBLIC_INBOX_DOMAINS = ['gmail.com', 'googlemail.com', 'outlook.com', 'hotmail.com', 'live.com', 'icloud.com', 'me.com'];
|
|
699
819
|
var SHARE_COOKIE = '__Host-phoenix_share';
|
|
700
820
|
var SHARE_COOKIE_MAX_AGE = 604800;
|
|
@@ -1140,6 +1260,18 @@ function isIdentityGated(visibility) {
|
|
|
1140
1260
|
return visibility === 'me' || visibility === 'org';
|
|
1141
1261
|
}
|
|
1142
1262
|
|
|
1263
|
+
function managedCoverHeaders(visibility) {
|
|
1264
|
+
const headers = new Headers({ 'content-type': 'image/png' });
|
|
1265
|
+
if (isIdentityGated(visibility)) {
|
|
1266
|
+
headers.set('cache-control', 'private, no-store');
|
|
1267
|
+
headers.set('X-Robots-Tag', 'noindex');
|
|
1268
|
+
} else {
|
|
1269
|
+
headers.set('cache-control', 'public, max-age=31536000, immutable');
|
|
1270
|
+
if (visibility === 'unlisted') headers.set('X-Robots-Tag', 'noindex');
|
|
1271
|
+
}
|
|
1272
|
+
return headers;
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1143
1275
|
// Gate a me/org read given the ALREADY-RESOLVED viewer identity. Pure/sync: the
|
|
1144
1276
|
// caller resolves the viewer once (it also needs the identity for the ownership
|
|
1145
1277
|
// check) and both the page GET and the ?revisions=json path share this gate.
|
|
@@ -1257,6 +1389,66 @@ async function signShareCookie(identity, env) {
|
|
|
1257
1389
|
return SHARE_COOKIE + '=' + value + '; HttpOnly; Secure; Path=/; SameSite=Lax; Max-Age=' + SHARE_COOKIE_MAX_AGE;
|
|
1258
1390
|
}
|
|
1259
1391
|
|
|
1392
|
+
// A short-lived login ticket, self-signed with the same HMAC secret as the cookie
|
|
1393
|
+
// but DOMAIN-SEPARATED (the signature covers a 'ticket:'-prefixed payload), so a
|
|
1394
|
+
// ticket value can never be replayed as a cookie value or vice versa. It rides in
|
|
1395
|
+
// the ?phoenix_ticket= param for exactly one navigation; resolveViewer verifies it
|
|
1396
|
+
// locally and exchanges it for the __share cookie. TTL is deliberately tiny — it
|
|
1397
|
+
// only has to survive the round-trip from CLI mint to the browser opening the URL.
|
|
1398
|
+
var SELF_TICKET_MAX_AGE = 120;
|
|
1399
|
+
|
|
1400
|
+
async function signSelfTicket(identity, env) {
|
|
1401
|
+
const secret = typeof env.WRITE_TOKEN === 'string' ? env.WRITE_TOKEN : '';
|
|
1402
|
+
if (!secret || !identity || !identity.userId) return '';
|
|
1403
|
+
const exp = Math.floor(Date.now() / 1000) + SELF_TICKET_MAX_AGE;
|
|
1404
|
+
const payload = identity.userId + '|' + (identity.email || '') + '|' + exp;
|
|
1405
|
+
const sig = await hmacHex(secret, 'ticket:' + payload);
|
|
1406
|
+
return toB64Url(payload) + '.' + sig;
|
|
1407
|
+
}
|
|
1408
|
+
|
|
1409
|
+
async function verifySelfTicket(ticket, env) {
|
|
1410
|
+
const secret = typeof env.WRITE_TOKEN === 'string' ? env.WRITE_TOKEN : '';
|
|
1411
|
+
if (!secret || !ticket) return null;
|
|
1412
|
+
const dot = ticket.lastIndexOf('.');
|
|
1413
|
+
if (dot < 1) return null;
|
|
1414
|
+
let payload;
|
|
1415
|
+
try {
|
|
1416
|
+
payload = fromB64Url(ticket.slice(0, dot));
|
|
1417
|
+
} catch {
|
|
1418
|
+
return null;
|
|
1419
|
+
}
|
|
1420
|
+
const sig = ticket.slice(dot + 1);
|
|
1421
|
+
const expected = await hmacHex(secret, 'ticket:' + payload);
|
|
1422
|
+
if (!safeEqual(sig, expected)) return null;
|
|
1423
|
+
const first = payload.indexOf('|');
|
|
1424
|
+
const last = payload.lastIndexOf('|');
|
|
1425
|
+
if (first < 0 || last <= first) return null;
|
|
1426
|
+
const userId = payload.slice(0, first);
|
|
1427
|
+
const email = payload.slice(first + 1, last);
|
|
1428
|
+
const exp = Number(payload.slice(last + 1));
|
|
1429
|
+
if (!userId || !Number.isFinite(exp) || Math.floor(Date.now() / 1000) > exp) return null;
|
|
1430
|
+
return { userId: userId, email: email };
|
|
1431
|
+
}
|
|
1432
|
+
|
|
1433
|
+
// Single-use enforcement for a self-signed ticket: claim its signature the first
|
|
1434
|
+
// time it is redeemed, and reject any later presentation of the same ticket. The
|
|
1435
|
+
// HMAC + short TTL stop forgery and bound the window, but without this a ticket the
|
|
1436
|
+
// CLI printed (--json / --no-open / a browser-open failure) could be replayed by
|
|
1437
|
+
// anyone who captured that output before it expired, minting the owner's 7-day
|
|
1438
|
+
// cookie a second time. Get-then-put mirrors claimHandle's one-time-claim pattern;
|
|
1439
|
+
// the marker rides a __-prefixed key (GET-blocked, excluded from every listing).
|
|
1440
|
+
async function consumeSelfTicket(env, sig) {
|
|
1441
|
+
if (!sig) return false;
|
|
1442
|
+
const key = '__ticket-used/' + sig;
|
|
1443
|
+
const existing = await env.BUCKET.get(key);
|
|
1444
|
+
if (existing) return false;
|
|
1445
|
+
await env.BUCKET.put(key, '1', {
|
|
1446
|
+
httpMetadata: { contentType: 'text/plain' },
|
|
1447
|
+
customMetadata: { visibility: 'unlisted' },
|
|
1448
|
+
});
|
|
1449
|
+
return true;
|
|
1450
|
+
}
|
|
1451
|
+
|
|
1260
1452
|
async function identityFromCookie(request, env) {
|
|
1261
1453
|
const secret = typeof env.WRITE_TOKEN === 'string' ? env.WRITE_TOKEN : '';
|
|
1262
1454
|
if (!secret) return null;
|
|
@@ -1316,7 +1508,17 @@ async function resolveViewer(request, env, url) {
|
|
|
1316
1508
|
if (cookieId) return { identity: cookieId };
|
|
1317
1509
|
const ticket = url.searchParams.get('phoenix_ticket');
|
|
1318
1510
|
if (!ticket) return {};
|
|
1319
|
-
|
|
1511
|
+
// Prefer the locally-verifiable self-signed ticket (PHNX-3370, no network), and
|
|
1512
|
+
// enforce single use by claiming its signature on first redemption — a captured
|
|
1513
|
+
// ticket replayed within its TTL finds it already spent. Fall back to the
|
|
1514
|
+
// external identity-server redeem for tickets it issued.
|
|
1515
|
+
let redeemed = await verifySelfTicket(ticket, env);
|
|
1516
|
+
if (redeemed) {
|
|
1517
|
+
const sig = ticket.slice(ticket.lastIndexOf('.') + 1);
|
|
1518
|
+
if (!(await consumeSelfTicket(env, sig))) redeemed = null;
|
|
1519
|
+
} else {
|
|
1520
|
+
redeemed = await redeemTicket(ticket, env);
|
|
1521
|
+
}
|
|
1320
1522
|
if (!redeemed) return { error: json({ error: 'invalid ticket' }, 401) };
|
|
1321
1523
|
const cookie = await signShareCookie(redeemed, env);
|
|
1322
1524
|
if (!cookie) return { error: json({ error: 'phoenix login is not configured' }, 401) };
|
|
@@ -1327,6 +1529,61 @@ async function resolveViewer(request, env, url) {
|
|
|
1327
1529
|
return { redirect: new Response(null, { status: 302, headers: headers }) };
|
|
1328
1530
|
}
|
|
1329
1531
|
|
|
1532
|
+
function extractHtmlMeta(html, field) {
|
|
1533
|
+
const pattern = field === 'title'
|
|
1534
|
+
? /<title[^>]*>([\\s\\S]*?)<\\/title>/i
|
|
1535
|
+
: /<meta[^>]+(?:name|property)=["'](?:description|og:description)["'][^>]+content=["']([^"']*)["'][^>]*>/i;
|
|
1536
|
+
const match = pattern.exec(html);
|
|
1537
|
+
return match ? match[1].replace(/<[^>]*>/g, '').replace(/&/g, '&').replace(/</g, '<').replace(/>/g, '>').replace(/"/g, '"').trim() : '';
|
|
1538
|
+
}
|
|
1539
|
+
|
|
1540
|
+
let rendererReady;
|
|
1541
|
+
async function renderOgCard(input) {
|
|
1542
|
+
if (!rendererReady) {
|
|
1543
|
+
rendererReady = Promise.all([
|
|
1544
|
+
initYoga(yogaWasm).then(function (yoga) { initSatori(yoga); }),
|
|
1545
|
+
initResvg(resvgWasm),
|
|
1546
|
+
]);
|
|
1547
|
+
}
|
|
1548
|
+
await rendererReady;
|
|
1549
|
+
const visibilityLabels = {
|
|
1550
|
+
public: 'PUBLIC',
|
|
1551
|
+
unlisted: 'UNLISTED',
|
|
1552
|
+
me: 'ONLY YOU',
|
|
1553
|
+
org: input.orgDomain ? 'ANYONE AT ' + input.orgDomain.toUpperCase() : 'ORGANIZATION',
|
|
1554
|
+
};
|
|
1555
|
+
const node = {
|
|
1556
|
+
type: 'div',
|
|
1557
|
+
props: {
|
|
1558
|
+
style: { width: '100%', height: '100%', display: 'flex', flexDirection: 'column', background: '#0a0a0a', color: '#f5f5f5', padding: '68px 76px 58px', fontFamily: 'Inter' },
|
|
1559
|
+
children: [
|
|
1560
|
+
{ type: 'div', props: { style: { display: 'flex', color: '#a3e635', fontFamily: 'JetBrains Mono', fontSize: 25, fontWeight: 600, letterSpacing: '-0.5px' }, children: 'AGI · agents-cli.sh' } },
|
|
1561
|
+
{ type: 'div', props: { style: { display: 'flex', flexDirection: 'column', flexGrow: 1, justifyContent: 'center', maxWidth: 1050 }, children: [
|
|
1562
|
+
{ type: 'div', props: { style: { display: 'flex', fontSize: 66, lineHeight: 1.06, fontWeight: 700, letterSpacing: '-2.8px', maxHeight: 218, overflow: 'hidden' }, children: input.title || 'Shared artifact' } },
|
|
1563
|
+
input.description ? { type: 'div', props: { style: { display: 'flex', marginTop: 24, color: '#a3a3a3', fontSize: 27, lineHeight: 1.35, maxHeight: 74, overflow: 'hidden' }, children: input.description } } : null,
|
|
1564
|
+
].filter(Boolean) } },
|
|
1565
|
+
{ type: 'div', props: { style: { display: 'flex', alignItems: 'center', justifyContent: 'space-between', borderTop: '1px solid #262626', paddingTop: 25, fontSize: 21, color: '#a3a3a3' }, children: [
|
|
1566
|
+
{ type: 'div', props: { style: { display: 'flex' }, children: [
|
|
1567
|
+
{ type: 'span', props: { style: { color: '#737373' }, children: 'shared by ' } },
|
|
1568
|
+
{ type: 'span', props: { style: { marginLeft: 7, color: '#f5f5f5', fontWeight: 700 }, children: '@' + input.handle } },
|
|
1569
|
+
] } },
|
|
1570
|
+
{ type: 'div', props: { style: { display: 'flex', border: '1px solid #3f3f46', borderRadius: 999, padding: '9px 17px', color: '#d4d4d8', fontFamily: 'JetBrains Mono', fontSize: 16, letterSpacing: '1px' }, children: visibilityLabels[input.visibility] || 'PUBLIC' } },
|
|
1571
|
+
] } },
|
|
1572
|
+
],
|
|
1573
|
+
},
|
|
1574
|
+
};
|
|
1575
|
+
const svg = await satori(node, {
|
|
1576
|
+
width: 1200,
|
|
1577
|
+
height: 630,
|
|
1578
|
+
fonts: [
|
|
1579
|
+
{ name: 'Inter', data: interRegular, weight: 400, style: 'normal' },
|
|
1580
|
+
{ name: 'Inter', data: interBold, weight: 700, style: 'normal' },
|
|
1581
|
+
{ name: 'JetBrains Mono', data: jetbrainsMono, weight: 600, style: 'normal' },
|
|
1582
|
+
],
|
|
1583
|
+
});
|
|
1584
|
+
return new Resvg(svg, { fitTo: { mode: 'width', value: 1200 } }).render().asPng();
|
|
1585
|
+
}
|
|
1586
|
+
|
|
1330
1587
|
async function defaultVerifyPhoenixToken(request, env) {
|
|
1331
1588
|
const presented = (request.headers.get('authorization') || '').replace(/^Bearer\\s+/i, '');
|
|
1332
1589
|
if (!presented) return null;
|
package/dist/lib/view-types.d.ts
CHANGED
|
@@ -21,7 +21,12 @@ export interface ViewJsonVersion {
|
|
|
21
21
|
amount: number;
|
|
22
22
|
currency: string;
|
|
23
23
|
} | null;
|
|
24
|
-
/**
|
|
24
|
+
/**
|
|
25
|
+
* Human-readable reason a usage snapshot is absent: a live-refresh failure, or
|
|
26
|
+
* (without `--refresh`, on a never-cached account) a plain "not collected yet"
|
|
27
|
+
* pending state. Always a full sentence — never the internal `'stale'` cache
|
|
28
|
+
* sentinel, which `usageErrorForDisplay` normalizes away (PHNX-3348).
|
|
29
|
+
*/
|
|
25
30
|
usageError?: string | null;
|
|
26
31
|
windows: Array<{
|
|
27
32
|
key: 'session' | 'week' | 'sonnet_week' | 'month';
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.22.
|
|
3
|
+
"version": "1.22.56",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -86,8 +86,11 @@
|
|
|
86
86
|
"@homebridge/node-pty-prebuilt-multiarch"
|
|
87
87
|
],
|
|
88
88
|
"dependencies": {
|
|
89
|
+
"@fontsource/inter": "^5.3.0",
|
|
90
|
+
"@fontsource/jetbrains-mono": "^5.3.0",
|
|
89
91
|
"@homebridge/node-pty-prebuilt-multiarch": "0.13.1",
|
|
90
92
|
"@inquirer/prompts": "8.5.2",
|
|
93
|
+
"@resvg/resvg-wasm": "^2.6.2",
|
|
91
94
|
"@types/proper-lockfile": "4.1.4",
|
|
92
95
|
"@xterm/headless": "6.0.0",
|
|
93
96
|
"@zed-industries/agent-client-protocol": "0.4.5",
|
|
@@ -98,17 +101,20 @@
|
|
|
98
101
|
"commander": "15.0.0",
|
|
99
102
|
"croner": "10.0.1",
|
|
100
103
|
"diff": "9.0.0",
|
|
104
|
+
"esbuild": "^0.28.2",
|
|
101
105
|
"lru-cache": "11.5.2",
|
|
102
106
|
"marked": "15.0.12",
|
|
103
107
|
"marked-terminal": "7.3.0",
|
|
104
108
|
"ora": "9.4.1",
|
|
105
109
|
"proper-lockfile": "4.1.2",
|
|
110
|
+
"satori": "0.12.2",
|
|
106
111
|
"shlex": "3.0.0",
|
|
107
112
|
"simple-git": "3.36.0",
|
|
108
113
|
"smol-toml": "1.7.0",
|
|
109
114
|
"unbash": "4.0.5",
|
|
110
115
|
"ws": "^8.21.0",
|
|
111
|
-
"yaml": "2.9.0"
|
|
116
|
+
"yaml": "2.9.0",
|
|
117
|
+
"yoga-wasm-web": "^0.3.3"
|
|
112
118
|
},
|
|
113
119
|
"devDependencies": {
|
|
114
120
|
"@types/diff": "8.0.0",
|