@phnx-labs/agents-cli 1.22.55 → 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 +6 -0
- package/dist/commands/share.js +7 -6
- 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 +21 -0
- package/dist/lib/scheduling/routines.js +56 -0
- package/dist/lib/share/provision.d.ts +3 -2
- package/dist/lib/share/provision.js +9 -4
- package/dist/lib/share/worker-template.d.ts +24 -1
- package/dist/lib/share/worker-template.js +65 -84
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,11 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.22.56
|
|
4
|
+
|
|
5
|
+
- **Scheduled routines claim a durable per-occurrence slot, so a duplicate delivery or a catch-up of the same slot can't double-launch (PHNX-3215).** The forward-timer path keyed its single-fire claim on croner's `currentRun()`, which is the jittered wall-clock fire instant, not the aligned schedule boundary — so two timer callbacks for one occurrence minted different run ids and both launched, and a live fire never collided with its catch-up twin. The scheduler now floors the fire to the aligned `(routine, scheduledFor)` boundary (`fireSlot`/`alignedSlotForFire`), the same derivation catch-up uses, making the atomic slot claim a structural guarantee (SING-15/16, self-overlap SING-13). Source: `cli/src/lib/scheduler.ts`, `cli/src/lib/scheduling/routines.ts`, `cli/src/lib/overdue.ts`.
|
|
6
|
+
|
|
7
|
+
- **Managed-share Open Graph cards render in Cloudflare instead of failing on every new publish (PHNX-2835).** The Worker upload now carries Yoga and resvg as compiled WebAssembly modules alongside the bundled JavaScript and fonts. The previous single-file bundle decoded the WASM into byte arrays and compiled it at request time; Node accepted that in tests, but Cloudflare workerd forbids runtime WASM code generation, so a new share's lazy `<slug>.png` render failed. A real-workerd regression test now renders and validates the PNG, and a genuine renderer failure returns a diagnostic `500` instead of falling through as a missing cover. Source: `cli/src/lib/share/{worker-template,provision}.ts`.
|
|
8
|
+
|
|
3
9
|
## 1.22.55
|
|
4
10
|
|
|
5
11
|
- **Managed shares generate their Open Graph cover server-side (PHNX-2835).** Publishing HTML to `share.agents-cli.sh` no longer launches local Chromium. The Worker lazily renders a deterministic 1200×630 AGI card with Satori and resvg-wasm, bundles Inter and JetBrains Mono, inherits the canonical page's visibility gate, and caches the PNG in R2. BYO endpoints keep their local screenshot fallback. An explicit missing `AGENTS_SHARE_BROWSER` or `PUPPETEER_EXECUTABLE_PATH` now fails loudly instead of silently falling through. Source: `cli/src/lib/share/{capture,publish,worker-template}.ts`.
|
package/dist/commands/share.js
CHANGED
|
@@ -13,7 +13,7 @@ import { DEFAULT_BUCKET_NAME, DEFAULT_CF_BUNDLE, DEFAULT_SHARE_DOMAIN, DEFAULT_W
|
|
|
13
13
|
import { addCustomDomain, configureBucketLifecycle, createBucket, deployWorker, enableWorkersDev, findZoneId, hashWorkerScript, putWorkerSecret, updateWorker, WORKER_PHOENIX_ID_BASE_SECRET, setWorkerSecret, } from '../lib/share/provision.js';
|
|
14
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
|
-
import {
|
|
16
|
+
import { renderWorkerBundle } 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';
|
|
@@ -54,7 +54,7 @@ export function formatSharePublishResult(result, json = false) {
|
|
|
54
54
|
export function shareTemplateStatus(cfg) {
|
|
55
55
|
if (!cfg.templateHash)
|
|
56
56
|
return 'unknown';
|
|
57
|
-
return cfg.templateHash === hashWorkerScript(
|
|
57
|
+
return cfg.templateHash === hashWorkerScript(renderWorkerBundle().script) ? 'current' : 'outdated';
|
|
58
58
|
}
|
|
59
59
|
export async function runShareEdit(target, opts) {
|
|
60
60
|
// Same public-listing gate as publish: label + every metadata value are
|
|
@@ -1155,7 +1155,8 @@ export async function runShareProvision(opts) {
|
|
|
1155
1155
|
const bucketName = opts.bucket;
|
|
1156
1156
|
const token = generateWriteToken();
|
|
1157
1157
|
const requestedDomain = cleanHostname(opts.domain) ?? DEFAULT_SHARE_DOMAIN;
|
|
1158
|
-
const
|
|
1158
|
+
const worker = renderWorkerBundle();
|
|
1159
|
+
const script = worker.script;
|
|
1159
1160
|
const spin = ora('Provisioning on Cloudflare…').start();
|
|
1160
1161
|
try {
|
|
1161
1162
|
const provisionOpts = opts.request ? { request: opts.request } : {};
|
|
@@ -1163,7 +1164,7 @@ export async function runShareProvision(opts) {
|
|
|
1163
1164
|
spin.text = `R2 bucket '${bucketName}' ready`;
|
|
1164
1165
|
await configureBucketLifecycle(apiToken, accountId, bucketName, provisionOpts);
|
|
1165
1166
|
spin.text = `R2 bucket '${bucketName}' lifecycle ready`;
|
|
1166
|
-
await deployWorker(apiToken, accountId, workerName,
|
|
1167
|
+
await deployWorker(apiToken, accountId, workerName, worker, bucketName, provisionOpts);
|
|
1167
1168
|
spin.text = `Worker '${workerName}' deployed`;
|
|
1168
1169
|
await setWorkerSecret(apiToken, accountId, workerName, token, provisionOpts);
|
|
1169
1170
|
spin.text = `Worker '${workerName}' write token set`;
|
|
@@ -1230,14 +1231,14 @@ export async function runShareUpdate(opts = {}) {
|
|
|
1230
1231
|
throw new Error("Share endpoint has no Cloudflare account id — `agents artifacts share update` cannot call the API. Pass --account <id>, or re-run 'agents artifacts share join'.");
|
|
1231
1232
|
}
|
|
1232
1233
|
const writeToken = readWriteToken();
|
|
1233
|
-
const
|
|
1234
|
+
const worker = renderWorkerBundle();
|
|
1234
1235
|
const phoenixIdBase = phoenixIdBaseForDeploy({ managed: opts.managed }, cfg);
|
|
1235
1236
|
const provisionOpts = {
|
|
1236
1237
|
...(opts.request ? { request: opts.request } : {}),
|
|
1237
1238
|
force: opts.force,
|
|
1238
1239
|
...(phoenixIdBase !== undefined ? { phoenixIdBase } : {}),
|
|
1239
1240
|
};
|
|
1240
|
-
const result = await updateWorker(apiToken, accountId, cfg.workerName, cfg.bucketName,
|
|
1241
|
+
const result = await updateWorker(apiToken, accountId, cfg.workerName, cfg.bucketName, worker, writeToken, cfg.templateHash, provisionOpts);
|
|
1241
1242
|
if (!result.skipped) {
|
|
1242
1243
|
writeShareConfig({ ...cfg, accountId, templateHash: result.templateHash });
|
|
1243
1244
|
}
|
package/dist/lib/overdue.js
CHANGED
|
@@ -13,48 +13,18 @@
|
|
|
13
13
|
*/
|
|
14
14
|
import * as fs from 'fs';
|
|
15
15
|
import { Cron } from 'croner';
|
|
16
|
-
import { listJobs, getLatestRun, resolveJobFilePath, isPastEndAt, isOneShotRoutine, jobRunsOnThisDevice } from './scheduling/routines.js';
|
|
16
|
+
import { alignedSlotForFire, listJobs, getLatestRun, resolveJobFilePath, isPastEndAt, isOneShotRoutine, jobRunsOnThisDevice } from './scheduling/routines.js';
|
|
17
17
|
import { notifyDesktop } from './menubar/notify-desktop.js';
|
|
18
18
|
// Tolerance between "expected fire" and "recorded run start" — accounts for
|
|
19
19
|
// the small gap between the cron tick and when the runner writes meta.json.
|
|
20
20
|
const GRACE_MS = 60_000;
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
26
|
-
* routine was never flagged overdue on any device — no missed record, no
|
|
27
|
-
* catch-up, permanently. Monthly, quarterly and annual routines were all in that
|
|
28
|
-
* class.
|
|
29
|
-
*
|
|
30
|
-
* A wider window is only tried when the narrower one found nothing, so a dense
|
|
31
|
-
* schedule (every minute, hourly, daily) never walks more than a week of
|
|
32
|
-
* occurrences. A sparse schedule has few occurrences to walk by definition.
|
|
33
|
-
*/
|
|
34
|
-
const LOOKBACK_WINDOWS_MS = [7 * DAY_MS, 32 * DAY_MS, 93 * DAY_MS, 400 * DAY_MS];
|
|
35
|
-
/** Compute the most recent fire of `pattern` at or before `now`. Croner's
|
|
36
|
-
* `previousRun()` returns the cron instance's own last fire, which is null
|
|
37
|
-
* on a freshly-constructed instance — so we walk `nextRun(cursor)` forward
|
|
38
|
-
* from a week ago and keep the last fire still ≤ now. */
|
|
21
|
+
/** Compute the most recent fire of `pattern` at or before `now`. Delegates to
|
|
22
|
+
* {@link alignedSlotForFire} so overdue detection (`missedRunId`) and the live
|
|
23
|
+
* forward-timer dispatch (`slotRunId`) key on the SAME occurrence identity — a
|
|
24
|
+
* missed fire and its live twin for one UTC slot then collide by construction
|
|
25
|
+
* (SING-15). */
|
|
39
26
|
function previousExpectedFire(cron, now) {
|
|
40
|
-
|
|
41
|
-
let cursor = new Date(now.getTime() - window);
|
|
42
|
-
let last = null;
|
|
43
|
-
// Cap iterations: an every-minute schedule yields ≤ 10080 steps over a week;
|
|
44
|
-
// 20k is a paranoia bound against pathological patterns. Only a schedule
|
|
45
|
-
// that found nothing in the narrower window reaches a wider one, and such a
|
|
46
|
-
// schedule is sparse, so the cap is never the binding constraint.
|
|
47
|
-
for (let i = 0; i < 20000; i++) {
|
|
48
|
-
const next = cron.nextRun(cursor);
|
|
49
|
-
if (!next || next.getTime() > now.getTime())
|
|
50
|
-
break;
|
|
51
|
-
last = next;
|
|
52
|
-
cursor = next;
|
|
53
|
-
}
|
|
54
|
-
if (last)
|
|
55
|
-
return last;
|
|
56
|
-
}
|
|
57
|
-
return null;
|
|
27
|
+
return alignedSlotForFire(cron, now);
|
|
58
28
|
}
|
|
59
29
|
/**
|
|
60
30
|
* When a routine started existing, and therefore the earliest fire it can
|
package/dist/lib/scheduler.d.ts
CHANGED
|
@@ -5,13 +5,32 @@
|
|
|
5
5
|
* process creates a single JobScheduler instance that loads enabled jobs
|
|
6
6
|
* on startup and reloads them on SIGHUP.
|
|
7
7
|
*/
|
|
8
|
+
import { Cron } from 'croner';
|
|
8
9
|
import type { JobConfig } from './scheduling/routines.js';
|
|
9
10
|
/** How a fire was triggered, carrying the scheduler's intended UTC slot time. */
|
|
10
11
|
export interface TriggerContext {
|
|
11
|
-
/** The cron slot this callback fires for
|
|
12
|
-
*
|
|
12
|
+
/** The ALIGNED cron slot this callback fires for, for the single-fire claim
|
|
13
|
+
* keyed on (routine, scheduledFor). Derived by {@link fireSlot} — NOT croner's
|
|
14
|
+
* raw `currentRun()`, which carries wall-clock jitter. */
|
|
13
15
|
scheduledFor?: Date;
|
|
14
16
|
}
|
|
17
|
+
/**
|
|
18
|
+
* The aligned occurrence boundary a fire callback belongs to — the value the
|
|
19
|
+
* single-fire `(routine, scheduledFor)` claim keys on.
|
|
20
|
+
*
|
|
21
|
+
* croner's `currentRun()` inside a fire callback is the JITTERED wall-clock
|
|
22
|
+
* trigger instant (it carries milliseconds — verified against croner 10.x), not
|
|
23
|
+
* the aligned schedule boundary. Keying `slotRunId` on it directly minted a
|
|
24
|
+
* distinct run id per delivery, so two callbacks for one occurrence each claimed
|
|
25
|
+
* a different run dir and both launched, and a live fire never collided with its
|
|
26
|
+
* catch-up twin (`missedRunId`, which keys on the aligned boundary). Flooring the
|
|
27
|
+
* fire to its schedule boundary via {@link alignedSlotForFire} makes the claim a
|
|
28
|
+
* structural claim on the occurrence identity (SING-15). Always returns a
|
|
29
|
+
* concrete Date — `currentRun()` falls back to now, and an unresolvable boundary
|
|
30
|
+
* falls back to the fire instant — so the forward path never dispatches without a
|
|
31
|
+
* durable slot key.
|
|
32
|
+
*/
|
|
33
|
+
export declare function fireSlot(cron: Cron): Date;
|
|
15
34
|
/** In-memory cron scheduler that triggers a callback when jobs fire. */
|
|
16
35
|
export declare class JobScheduler {
|
|
17
36
|
private jobs;
|
package/dist/lib/scheduler.js
CHANGED
|
@@ -6,7 +6,27 @@
|
|
|
6
6
|
* on startup and reloads them on SIGHUP.
|
|
7
7
|
*/
|
|
8
8
|
import { Cron } from 'croner';
|
|
9
|
-
import { listJobs, deleteJob, isPastEndAt, isPastOneShotRoutine, isOneShotRoutine, setJobEnabled, shouldPurgeCompletedOneShotRoutine, jobRunsOnThisDevice, hasAmbiguousDevicePin, routineOwnerDevice, } from './scheduling/routines.js';
|
|
9
|
+
import { alignedSlotForFire, listJobs, deleteJob, isPastEndAt, isPastOneShotRoutine, isOneShotRoutine, setJobEnabled, shouldPurgeCompletedOneShotRoutine, jobRunsOnThisDevice, hasAmbiguousDevicePin, routineOwnerDevice, } from './scheduling/routines.js';
|
|
10
|
+
/**
|
|
11
|
+
* The aligned occurrence boundary a fire callback belongs to — the value the
|
|
12
|
+
* single-fire `(routine, scheduledFor)` claim keys on.
|
|
13
|
+
*
|
|
14
|
+
* croner's `currentRun()` inside a fire callback is the JITTERED wall-clock
|
|
15
|
+
* trigger instant (it carries milliseconds — verified against croner 10.x), not
|
|
16
|
+
* the aligned schedule boundary. Keying `slotRunId` on it directly minted a
|
|
17
|
+
* distinct run id per delivery, so two callbacks for one occurrence each claimed
|
|
18
|
+
* a different run dir and both launched, and a live fire never collided with its
|
|
19
|
+
* catch-up twin (`missedRunId`, which keys on the aligned boundary). Flooring the
|
|
20
|
+
* fire to its schedule boundary via {@link alignedSlotForFire} makes the claim a
|
|
21
|
+
* structural claim on the occurrence identity (SING-15). Always returns a
|
|
22
|
+
* concrete Date — `currentRun()` falls back to now, and an unresolvable boundary
|
|
23
|
+
* falls back to the fire instant — so the forward path never dispatches without a
|
|
24
|
+
* durable slot key.
|
|
25
|
+
*/
|
|
26
|
+
export function fireSlot(cron) {
|
|
27
|
+
const fire = cron.currentRun() ?? new Date();
|
|
28
|
+
return alignedSlotForFire(cron, fire) ?? fire;
|
|
29
|
+
}
|
|
10
30
|
/** In-memory cron scheduler that triggers a callback when jobs fire. */
|
|
11
31
|
export class JobScheduler {
|
|
12
32
|
jobs = new Map();
|
|
@@ -75,10 +95,13 @@ export class JobScheduler {
|
|
|
75
95
|
return;
|
|
76
96
|
}
|
|
77
97
|
try {
|
|
78
|
-
//
|
|
79
|
-
//
|
|
80
|
-
//
|
|
81
|
-
|
|
98
|
+
// scheduledFor is the ALIGNED occurrence boundary (fireSlot), not croner's
|
|
99
|
+
// jittered currentRun(): the single-fire claim keys on (routine,
|
|
100
|
+
// scheduledFor), so the key must be the occurrence identity or a live fire
|
|
101
|
+
// and its catch-up twin (missedRunId) won't collide and two deliveries of
|
|
102
|
+
// one slot each mint a distinct id. fireSlot always returns a Date, so the
|
|
103
|
+
// forward path always carries a durable claim (SING-15).
|
|
104
|
+
await this.onTrigger(config, { scheduledFor: fireSlot(self) });
|
|
82
105
|
}
|
|
83
106
|
catch (err) {
|
|
84
107
|
console.error(`Job '${config.name}' failed:`, err.message);
|
|
@@ -6,6 +6,7 @@
|
|
|
6
6
|
* run metadata persistence, prompt variable expansion, and one-shot "at" time
|
|
7
7
|
* scheduling.
|
|
8
8
|
*/
|
|
9
|
+
import { Cron } from 'croner';
|
|
9
10
|
import { type ResolvedExecutionContext, type ProjectResolution, type PlacementMode, type RoutineKind, type ContextFsProbe } from '../routine-context.js';
|
|
10
11
|
import type { AgentId, RunStrategy } from '../types.js';
|
|
11
12
|
import type { LoopConfig } from '../loop.js';
|
|
@@ -837,6 +838,26 @@ export declare function getRunDir(jobName: string, runId: string): string;
|
|
|
837
838
|
* for the same UTC slot are one record.
|
|
838
839
|
*/
|
|
839
840
|
export declare function slotRunId(scheduledFor: Date | string): string;
|
|
841
|
+
/**
|
|
842
|
+
* The aligned schedule boundary a fire belongs to: the most recent occurrence of
|
|
843
|
+
* `cron` at or before `at`.
|
|
844
|
+
*
|
|
845
|
+
* This is the occurrence IDENTITY that {@link slotRunId} (forward dispatch) and
|
|
846
|
+
* `missedRunId` (catchup.ts) must both key on. croner's `currentRun()` inside a
|
|
847
|
+
* fire callback is the JITTERED wall-clock trigger instant (it carries
|
|
848
|
+
* milliseconds — verified), not the aligned boundary, so keying `slotRunId`
|
|
849
|
+
* directly on it produced a distinct id per delivery: two callbacks for one
|
|
850
|
+
* occurrence each claimed a different run dir and both launched, and a live fire
|
|
851
|
+
* never collided with its catch-up twin (which keys on the aligned
|
|
852
|
+
* `previousExpectedFire`). Flooring both to this boundary is what makes the
|
|
853
|
+
* single-fire claim a structural claim on `(routine, scheduledFor)` (SING-15).
|
|
854
|
+
*
|
|
855
|
+
* croner's `previousRun()` takes no argument and returns null on a freshly
|
|
856
|
+
* constructed instance, so we walk `nextRun(cursor)` forward from a lookback
|
|
857
|
+
* window and keep the last fire still ≤ `at` — the same derivation catchup's
|
|
858
|
+
* overdue detection has always used.
|
|
859
|
+
*/
|
|
860
|
+
export declare function alignedSlotForFire(cron: Cron, at: Date): Date | null;
|
|
840
861
|
/**
|
|
841
862
|
* Atomically CLAIM a run directory. Returns true on a successful claim, false
|
|
842
863
|
* when the directory already exists (another caller — even in a separate process
|
|
@@ -1518,6 +1518,62 @@ export function slotRunId(scheduledFor) {
|
|
|
1518
1518
|
const iso = typeof scheduledFor === 'string' ? scheduledFor : scheduledFor.toISOString();
|
|
1519
1519
|
return iso.replace(/[:.]/g, '-');
|
|
1520
1520
|
}
|
|
1521
|
+
/**
|
|
1522
|
+
* Lookback windows for {@link alignedSlotForFire}, narrowest first. A wider
|
|
1523
|
+
* window is tried ONLY when the narrower one found no fire, so:
|
|
1524
|
+
* - a dense schedule (every-minute) resolves in the 1-hour window — ~60 steps,
|
|
1525
|
+
* not ~10080 — which matters because the forward-timer path now runs this on
|
|
1526
|
+
* every fire (a live fire is milliseconds past its boundary, so the narrowest
|
|
1527
|
+
* window always contains it);
|
|
1528
|
+
* - a sparse schedule (`0 9 1,13,25 * *` has 12-day gaps; monthly/quarterly/
|
|
1529
|
+
* annual) still resolves, because a fixed short window silently blinded
|
|
1530
|
+
* overdue detection to any cron whose gap exceeded it.
|
|
1531
|
+
* A narrower window can only ever find the true most-recent fire ≤ `at` or
|
|
1532
|
+
* nothing (never a wrong boundary), so prepending the cheap windows is
|
|
1533
|
+
* behavior-preserving for the sparse-schedule overdue path.
|
|
1534
|
+
*/
|
|
1535
|
+
const HOUR_MS = 60 * 60 * 1000;
|
|
1536
|
+
const DAY_MS = 24 * HOUR_MS;
|
|
1537
|
+
const SLOT_LOOKBACK_WINDOWS_MS = [HOUR_MS, DAY_MS, 7 * DAY_MS, 32 * DAY_MS, 93 * DAY_MS, 400 * DAY_MS];
|
|
1538
|
+
/**
|
|
1539
|
+
* The aligned schedule boundary a fire belongs to: the most recent occurrence of
|
|
1540
|
+
* `cron` at or before `at`.
|
|
1541
|
+
*
|
|
1542
|
+
* This is the occurrence IDENTITY that {@link slotRunId} (forward dispatch) and
|
|
1543
|
+
* `missedRunId` (catchup.ts) must both key on. croner's `currentRun()` inside a
|
|
1544
|
+
* fire callback is the JITTERED wall-clock trigger instant (it carries
|
|
1545
|
+
* milliseconds — verified), not the aligned boundary, so keying `slotRunId`
|
|
1546
|
+
* directly on it produced a distinct id per delivery: two callbacks for one
|
|
1547
|
+
* occurrence each claimed a different run dir and both launched, and a live fire
|
|
1548
|
+
* never collided with its catch-up twin (which keys on the aligned
|
|
1549
|
+
* `previousExpectedFire`). Flooring both to this boundary is what makes the
|
|
1550
|
+
* single-fire claim a structural claim on `(routine, scheduledFor)` (SING-15).
|
|
1551
|
+
*
|
|
1552
|
+
* croner's `previousRun()` takes no argument and returns null on a freshly
|
|
1553
|
+
* constructed instance, so we walk `nextRun(cursor)` forward from a lookback
|
|
1554
|
+
* window and keep the last fire still ≤ `at` — the same derivation catchup's
|
|
1555
|
+
* overdue detection has always used.
|
|
1556
|
+
*/
|
|
1557
|
+
export function alignedSlotForFire(cron, at) {
|
|
1558
|
+
for (const window of SLOT_LOOKBACK_WINDOWS_MS) {
|
|
1559
|
+
let cursor = new Date(at.getTime() - window);
|
|
1560
|
+
let last = null;
|
|
1561
|
+
// Cap iterations: an every-minute schedule yields ≤ 10080 steps over a week;
|
|
1562
|
+
// 20k is a paranoia bound against pathological patterns. Only a schedule that
|
|
1563
|
+
// found nothing in the narrower window reaches a wider one, and such a
|
|
1564
|
+
// schedule is sparse, so the cap is never the binding constraint.
|
|
1565
|
+
for (let i = 0; i < 20000; i++) {
|
|
1566
|
+
const next = cron.nextRun(cursor);
|
|
1567
|
+
if (!next || next.getTime() > at.getTime())
|
|
1568
|
+
break;
|
|
1569
|
+
last = next;
|
|
1570
|
+
cursor = next;
|
|
1571
|
+
}
|
|
1572
|
+
if (last)
|
|
1573
|
+
return last;
|
|
1574
|
+
}
|
|
1575
|
+
return null;
|
|
1576
|
+
}
|
|
1521
1577
|
/**
|
|
1522
1578
|
* Atomically CLAIM a run directory. Returns true on a successful claim, false
|
|
1523
1579
|
* when the directory already exists (another caller — even in a separate process
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { WorkerBundle } from './worker-template.js';
|
|
1
2
|
export declare const SHARE_LIFECYCLE_RULE_ID = "agents-share-expire-objects";
|
|
2
3
|
export declare const SHARE_LIFECYCLE_RETENTION_DAYS = 366;
|
|
3
4
|
interface R2LifecycleRule {
|
|
@@ -50,7 +51,7 @@ export declare function mergeShareLifecycleRule(existing?: R2LifecycleRule[], ru
|
|
|
50
51
|
/** Ensure the share bucket self-cleans old objects. Exact per-link expiry is enforced by the Worker. */
|
|
51
52
|
export declare function configureBucketLifecycle(apiToken: string, accountId: string, bucketName: string, opts?: ProvisionOptions): Promise<void>;
|
|
52
53
|
/** Upload the module Worker with an R2 binding (`BUCKET`). Secrets are set via the Workers Secrets API. */
|
|
53
|
-
export declare function deployWorker(apiToken: string, accountId: string, workerName: string,
|
|
54
|
+
export declare function deployWorker(apiToken: string, accountId: string, workerName: string, worker: string | WorkerBundle, bucketName: string, opts?: ProvisionOptions): Promise<void>;
|
|
54
55
|
/** sha256 of the rendered Worker script, so a deployed endpoint can be compared
|
|
55
56
|
* against the current `worker-template.ts` without redeploying to find out. */
|
|
56
57
|
export declare function hashWorkerScript(script: string): string;
|
|
@@ -100,7 +101,7 @@ export type UpdateWorkerOpts = ProvisionOptions & {
|
|
|
100
101
|
* re-setting the secret to its own value after every deploy gets the same
|
|
101
102
|
* outcome without depending on an unverified upload-time flag.
|
|
102
103
|
*/
|
|
103
|
-
export declare function updateWorker(apiToken: string, accountId: string, workerName: string, bucketName: string,
|
|
104
|
+
export declare function updateWorker(apiToken: string, accountId: string, workerName: string, bucketName: string, worker: string | WorkerBundle, writeToken: string, previousHash: string | undefined, opts?: UpdateWorkerOpts): Promise<UpdateWorkerResult>;
|
|
104
105
|
/** Add/update a secret_text binding using Cloudflare's Workers Secrets API. */
|
|
105
106
|
export declare function putWorkerSecret(apiToken: string, accountId: string, workerName: string, name: string, text: string, opts?: ProvisionOptions): Promise<void>;
|
|
106
107
|
/** Add/update the WRITE_TOKEN binding using Cloudflare's Workers Secrets API. */
|
|
@@ -77,7 +77,7 @@ export async function configureBucketLifecycle(apiToken, accountId, bucketName,
|
|
|
77
77
|
});
|
|
78
78
|
}
|
|
79
79
|
/** Upload the module Worker with an R2 binding (`BUCKET`). Secrets are set via the Workers Secrets API. */
|
|
80
|
-
export async function deployWorker(apiToken, accountId, workerName,
|
|
80
|
+
export async function deployWorker(apiToken, accountId, workerName, worker, bucketName, opts = {}) {
|
|
81
81
|
const request = opts.request ?? defaultCloudflareRequester;
|
|
82
82
|
const metadata = {
|
|
83
83
|
main_module: 'worker.js',
|
|
@@ -88,7 +88,11 @@ export async function deployWorker(apiToken, accountId, workerName, script, buck
|
|
|
88
88
|
};
|
|
89
89
|
const form = new FormData();
|
|
90
90
|
form.set('metadata', new Blob([JSON.stringify(metadata)], { type: 'application/json' }));
|
|
91
|
-
|
|
91
|
+
const bundle = typeof worker === 'string' ? { script: worker, modules: [] } : worker;
|
|
92
|
+
form.set('worker.js', new Blob([bundle.script], { type: 'application/javascript+module' }), 'worker.js');
|
|
93
|
+
for (const module of bundle.modules) {
|
|
94
|
+
form.set(module.name, new Blob([module.contents], { type: module.contentType }), module.name);
|
|
95
|
+
}
|
|
92
96
|
await request({
|
|
93
97
|
apiToken,
|
|
94
98
|
method: 'PUT',
|
|
@@ -130,7 +134,8 @@ export const WORKER_PHOENIX_ID_BASE_SECRET = 'PHOENIX_ID_BASE';
|
|
|
130
134
|
* re-setting the secret to its own value after every deploy gets the same
|
|
131
135
|
* outcome without depending on an unverified upload-time flag.
|
|
132
136
|
*/
|
|
133
|
-
export async function updateWorker(apiToken, accountId, workerName, bucketName,
|
|
137
|
+
export async function updateWorker(apiToken, accountId, workerName, bucketName, worker, writeToken, previousHash, opts = {}) {
|
|
138
|
+
const script = typeof worker === 'string' ? worker : worker.script;
|
|
134
139
|
const templateHash = hashWorkerScript(script);
|
|
135
140
|
if (!opts.force && previousHash === templateHash) {
|
|
136
141
|
// Script is current so we skip the upload (which would wipe secrets). A
|
|
@@ -142,7 +147,7 @@ export async function updateWorker(apiToken, accountId, workerName, bucketName,
|
|
|
142
147
|
}
|
|
143
148
|
return { templateHash, skipped: true };
|
|
144
149
|
}
|
|
145
|
-
await deployWorker(apiToken, accountId, workerName,
|
|
150
|
+
await deployWorker(apiToken, accountId, workerName, worker, bucketName, opts);
|
|
146
151
|
// Script upload clears bindings/secrets (see JSDoc above). If re-applying
|
|
147
152
|
// WRITE_TOKEN fails here, the live Worker has no write token — every
|
|
148
153
|
// `agents artifacts share` publish/delete 401s until a re-run of `agents artifacts share update`
|
|
@@ -1,4 +1,27 @@
|
|
|
1
|
-
/**
|
|
1
|
+
/**
|
|
2
|
+
* Render the Worker source. Pure — the R2 binding + token are wired at deploy time.
|
|
3
|
+
*
|
|
4
|
+
* The literal below still spells the CLI `agents share` in its provenance comment,
|
|
5
|
+
* its root response, and its gallery title, even though the command is now
|
|
6
|
+
* `agents artifacts share` (RUSH-2580). That is deliberate: `hashWorkerScript` of
|
|
7
|
+
* this exact text is what `shareTemplateStatus` compares a provisioned endpoint's
|
|
8
|
+
* recorded `templateHash` against, so editing ANY byte here marks every already-
|
|
9
|
+
* deployed endpoint `outdated` — which makes `agents artifacts share list` refuse
|
|
10
|
+
* until its owner re-runs `agents artifacts share update`. Cosmetic renames are not
|
|
11
|
+
* worth that; change this text only alongside a real Worker behavior change.
|
|
12
|
+
*/
|
|
13
|
+
export interface WorkerModule {
|
|
14
|
+
name: string;
|
|
15
|
+
contentType: 'application/wasm';
|
|
16
|
+
contents: Uint8Array<ArrayBuffer>;
|
|
17
|
+
}
|
|
18
|
+
export interface WorkerBundle {
|
|
19
|
+
script: string;
|
|
20
|
+
modules: WorkerModule[];
|
|
21
|
+
}
|
|
22
|
+
/** Bundle the Worker renderer into an ES module plus workerd-compiled WASM modules. */
|
|
23
|
+
export declare function renderWorkerBundle(): WorkerBundle;
|
|
24
|
+
/** Single-file representation for direct Node tests, which cannot import compiled WASM modules. */
|
|
2
25
|
export declare function renderWorkerScript(): string;
|
|
3
26
|
/** Unbundled Worker source. Kept separate so esbuild can resolve npm modules. */
|
|
4
27
|
export declare function renderWorkerSource(): string;
|
|
@@ -1,81 +1,53 @@
|
|
|
1
1
|
import { buildSync } from 'esbuild';
|
|
2
2
|
import { dirname } from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
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>/<slug>?revisions=json — machine-readable history of the
|
|
49
|
-
// retained prior versions under that slug, newest first.
|
|
50
|
-
// - GET /<username> — public gallery of that user's shares (HTML).
|
|
51
|
-
// - GET /<username>?format=json — public machine-readable listing of that user's
|
|
52
|
-
// ACTIVE shares (`agents artifacts share list`). Same single-segment path as the HTML
|
|
53
|
-
// gallery and gated on the SAME "does <username>/ hold any object" check, so it
|
|
54
|
-
// only intercepts a genuine namespace — a legacy flat slug with ?format=json
|
|
55
|
-
// still serves its real content, never a fake empty listing.
|
|
56
|
-
// - GET /<slug> — backward-compat flat slug (legacy shares before
|
|
57
|
-
// per-user namespaces).
|
|
58
|
-
//
|
|
59
|
-
// Emitted as a string, so it compiles into `dist/**` and ships with no
|
|
60
|
-
// package.json#files change. `provision.ts` uploads
|
|
61
|
-
// this verbatim as an ES-module Worker with a BUCKET (R2) binding + a WRITE_TOKEN secret.
|
|
62
|
-
/**
|
|
63
|
-
* Render the Worker source. Pure — the R2 binding + token are wired at deploy time.
|
|
64
|
-
*
|
|
65
|
-
* The literal below still spells the CLI `agents share` in its provenance comment,
|
|
66
|
-
* its root response, and its gallery title, even though the command is now
|
|
67
|
-
* `agents artifacts share` (RUSH-2580). That is deliberate: `hashWorkerScript` of
|
|
68
|
-
* this exact text is what `shareTemplateStatus` compares a provisioned endpoint's
|
|
69
|
-
* recorded `templateHash` against, so editing ANY byte here marks every already-
|
|
70
|
-
* deployed endpoint `outdated` — which makes `agents artifacts share list` refuse
|
|
71
|
-
* until its owner re-runs `agents artifacts share update`. Cosmetic renames are not
|
|
72
|
-
* worth that; change this text only alongside a real Worker behavior change.
|
|
73
|
-
*/
|
|
74
|
-
let bundledWorkerScript;
|
|
75
|
-
/** Bundle the Worker and its renderer into one uploadable ES module. */
|
|
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. */
|
|
76
48
|
export function renderWorkerScript() {
|
|
77
|
-
if (
|
|
78
|
-
return
|
|
49
|
+
if (nodeWorkerScript)
|
|
50
|
+
return nodeWorkerScript;
|
|
79
51
|
const result = buildSync({
|
|
80
52
|
stdin: {
|
|
81
53
|
contents: renderWorkerSource(),
|
|
@@ -93,9 +65,9 @@ export function renderWorkerScript() {
|
|
|
93
65
|
});
|
|
94
66
|
const output = result.outputFiles[0];
|
|
95
67
|
if (!output)
|
|
96
|
-
throw new Error('Worker bundling produced no JavaScript output.');
|
|
97
|
-
|
|
98
|
-
return
|
|
68
|
+
throw new Error('Worker test bundling produced no JavaScript output.');
|
|
69
|
+
nodeWorkerScript = output.text;
|
|
70
|
+
return nodeWorkerScript;
|
|
99
71
|
}
|
|
100
72
|
/** Unbundled Worker source. Kept separate so esbuild can resolve npm modules. */
|
|
101
73
|
export function renderWorkerSource() {
|
|
@@ -491,13 +463,22 @@ export default {
|
|
|
491
463
|
|
|
492
464
|
const pageHtml = await page.text();
|
|
493
465
|
const meta = page.customMetadata || {};
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
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
|
+
}
|
|
501
482
|
const beforeStore = await env.BUCKET.get(pagePath);
|
|
502
483
|
if (!beforeStore || beforeStore.etag !== page.etag) { existingCover = null; continue; }
|
|
503
484
|
await env.BUCKET.put(path, png, {
|
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",
|