@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
|
@@ -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';
|
|
@@ -16,6 +17,53 @@ export declare function nextRunLabel(job: JobConfig, scheduler: JobScheduler, no
|
|
|
16
17
|
export declare function localLatestRun(job: JobConfig): RunMeta | null;
|
|
17
18
|
export declare function listJobsForDisplay(cwd?: string): JobConfig[];
|
|
18
19
|
export declare function buildRoutineListJson(): Record<string, unknown>[];
|
|
20
|
+
/** One routine's live scheduler-status row for `agents routines status --json`. */
|
|
21
|
+
export interface RoutineStatusRow {
|
|
22
|
+
name: string;
|
|
23
|
+
/** The single device this routine is pinned to fire on, or null when unpinned. */
|
|
24
|
+
ownerDevice: string | null;
|
|
25
|
+
/** True when `devices:` names more than one device — no single owner (doctor flags it). */
|
|
26
|
+
ambiguousDevicePin: boolean;
|
|
27
|
+
/** Every device this routine is enabled on. */
|
|
28
|
+
enabledDevices: string[];
|
|
29
|
+
/** Whether THIS device is the one that fires the routine. */
|
|
30
|
+
runsHere: boolean;
|
|
31
|
+
enabled: boolean;
|
|
32
|
+
overdue: boolean;
|
|
33
|
+
nextRun: string | null;
|
|
34
|
+
/** Terminal status of the last local fire: completed/failed/timeout/missed/blocked/skipped/running, or null if it never ran here. */
|
|
35
|
+
lastStatus: RunMeta['status'] | null;
|
|
36
|
+
/** The last fire's failure reason, when it did not complete cleanly. Named to match `list --json`'s `failureReason`. */
|
|
37
|
+
failureReason: string | null;
|
|
38
|
+
lastRunStartedAt: string | null;
|
|
39
|
+
lastRunCompletedAt: string | null;
|
|
40
|
+
/** Present only while a run is genuinely in flight on THIS device (a live local child or a host-placed run) — never a provisional pre-spawn claim. */
|
|
41
|
+
inFlight: {
|
|
42
|
+
runId: string;
|
|
43
|
+
pid: number | null;
|
|
44
|
+
startedAt: string;
|
|
45
|
+
triggerKind: RunMeta['triggerKind'] | null;
|
|
46
|
+
} | null;
|
|
47
|
+
}
|
|
48
|
+
/**
|
|
49
|
+
* The per-routine rows behind `agents routines status --json`. Distinct from
|
|
50
|
+
* {@link buildRoutineListJson}: this is the scheduler-truth surface the daemon
|
|
51
|
+
* owns — per routine it names the single owner device, the last fire's outcome
|
|
52
|
+
* and error, and any in-flight spawn — the fields an operator (or the menu bar /
|
|
53
|
+
* ext) needs to answer "did this routine fire, and is one running right now?"
|
|
54
|
+
* that the definition-shaped `list --json` does not carry (PHNX-3215).
|
|
55
|
+
*
|
|
56
|
+
* `monitorRunningJobs()` runs first to reap runs whose process has exited, then
|
|
57
|
+
* `inFlight` is gated on {@link isRunGenuinelyInFlight} — NOT on `status ===
|
|
58
|
+
* 'running'` alone, because a provisional pre-spawn claim is `running` with a
|
|
59
|
+
* null pid that the reaper does not touch (RUSH-2640).
|
|
60
|
+
*
|
|
61
|
+
* The routine set is the schedulable one ({@link listJobs}, the same
|
|
62
|
+
* `getDaemonStatus`/`routines status` counts), not the display set
|
|
63
|
+
* {@link buildRoutineListJson} uses — a scheduler-status surface names what the
|
|
64
|
+
* daemon can actually fire, not discoverable-but-unmaterialised project routines.
|
|
65
|
+
*/
|
|
66
|
+
export declare function buildRoutineStatusRows(): RoutineStatusRow[];
|
|
19
67
|
/** Tool/site/directory allow-list for sandboxed job execution. */
|
|
20
68
|
export interface JobAllowConfig {
|
|
21
69
|
tools?: string[];
|
|
@@ -790,6 +838,26 @@ export declare function getRunDir(jobName: string, runId: string): string;
|
|
|
790
838
|
* for the same UTC slot are one record.
|
|
791
839
|
*/
|
|
792
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;
|
|
793
861
|
/**
|
|
794
862
|
* Atomically CLAIM a run directory. Returns true on a successful claim, false
|
|
795
863
|
* when the directory already exists (another caller — even in a separate process
|
|
@@ -26,7 +26,7 @@ import { enabledRoutineNames, devicesWithRoutineEnabled, replaceEnabledRoutines,
|
|
|
26
26
|
import { humanizeCron, humanizeNextRun } from '../routines-format.js';
|
|
27
27
|
import { discoverProjectRoutines } from '../routines-project.js';
|
|
28
28
|
import { listProjectDefs } from '../projects.js';
|
|
29
|
-
import { monitorRunningJobs } from '../daemon/runner.js';
|
|
29
|
+
import { monitorRunningJobs, isRunGenuinelyInFlight } from '../daemon/runner.js';
|
|
30
30
|
import { JobScheduler } from '../scheduler.js';
|
|
31
31
|
import { detectOverdueJobs } from '../overdue.js';
|
|
32
32
|
export function fireConditionLabel(job) {
|
|
@@ -142,6 +142,75 @@ export function buildRoutineListJson() {
|
|
|
142
142
|
scheduler.stopAll();
|
|
143
143
|
}
|
|
144
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* The per-routine rows behind `agents routines status --json`. Distinct from
|
|
147
|
+
* {@link buildRoutineListJson}: this is the scheduler-truth surface the daemon
|
|
148
|
+
* owns — per routine it names the single owner device, the last fire's outcome
|
|
149
|
+
* and error, and any in-flight spawn — the fields an operator (or the menu bar /
|
|
150
|
+
* ext) needs to answer "did this routine fire, and is one running right now?"
|
|
151
|
+
* that the definition-shaped `list --json` does not carry (PHNX-3215).
|
|
152
|
+
*
|
|
153
|
+
* `monitorRunningJobs()` runs first to reap runs whose process has exited, then
|
|
154
|
+
* `inFlight` is gated on {@link isRunGenuinelyInFlight} — NOT on `status ===
|
|
155
|
+
* 'running'` alone, because a provisional pre-spawn claim is `running` with a
|
|
156
|
+
* null pid that the reaper does not touch (RUSH-2640).
|
|
157
|
+
*
|
|
158
|
+
* The routine set is the schedulable one ({@link listJobs}, the same
|
|
159
|
+
* `getDaemonStatus`/`routines status` counts), not the display set
|
|
160
|
+
* {@link buildRoutineListJson} uses — a scheduler-status surface names what the
|
|
161
|
+
* daemon can actually fire, not discoverable-but-unmaterialised project routines.
|
|
162
|
+
*/
|
|
163
|
+
export function buildRoutineStatusRows() {
|
|
164
|
+
try {
|
|
165
|
+
monitorRunningJobs();
|
|
166
|
+
}
|
|
167
|
+
catch { /* best-effort orphan reap */ }
|
|
168
|
+
const jobs = listJobs();
|
|
169
|
+
if (jobs.length === 0)
|
|
170
|
+
return [];
|
|
171
|
+
const scheduler = new JobScheduler(async () => { });
|
|
172
|
+
scheduler.loadAll();
|
|
173
|
+
try {
|
|
174
|
+
const overdueSet = new Set();
|
|
175
|
+
try {
|
|
176
|
+
for (const job of detectOverdueJobs())
|
|
177
|
+
overdueSet.add(job.name);
|
|
178
|
+
}
|
|
179
|
+
catch {
|
|
180
|
+
// Best-effort indicator; never block status on detection errors.
|
|
181
|
+
}
|
|
182
|
+
const now = new Date();
|
|
183
|
+
return jobs.map((job) => {
|
|
184
|
+
const latestRun = localLatestRun(job);
|
|
185
|
+
const inFlight = latestRun && isRunGenuinelyInFlight(latestRun)
|
|
186
|
+
? {
|
|
187
|
+
runId: latestRun.runId,
|
|
188
|
+
pid: latestRun.pid,
|
|
189
|
+
startedAt: latestRun.startedAt,
|
|
190
|
+
triggerKind: latestRun.triggerKind ?? null,
|
|
191
|
+
}
|
|
192
|
+
: null;
|
|
193
|
+
return {
|
|
194
|
+
name: job.name,
|
|
195
|
+
ownerDevice: routineOwnerDevice(job),
|
|
196
|
+
ambiguousDevicePin: hasAmbiguousDevicePin(job),
|
|
197
|
+
enabledDevices: devicesWithRoutineEnabled(job.name),
|
|
198
|
+
runsHere: jobRunsOnThisDevice(job),
|
|
199
|
+
enabled: job.enabled,
|
|
200
|
+
overdue: overdueSet.has(job.name),
|
|
201
|
+
nextRun: nextRunForDisplay(job, scheduler)?.toISOString() ?? null,
|
|
202
|
+
lastStatus: latestRun?.status ?? null,
|
|
203
|
+
failureReason: latestRun?.errorMessage ?? null,
|
|
204
|
+
lastRunStartedAt: latestRun?.startedAt ?? null,
|
|
205
|
+
lastRunCompletedAt: latestRun?.completedAt ?? null,
|
|
206
|
+
inFlight,
|
|
207
|
+
};
|
|
208
|
+
});
|
|
209
|
+
}
|
|
210
|
+
finally {
|
|
211
|
+
scheduler.stopAll();
|
|
212
|
+
}
|
|
213
|
+
}
|
|
145
214
|
export const HOST_STRATEGIES = ['local', 'host', 'fleet', 'cloud'];
|
|
146
215
|
/** Canonical set of accepted GitHub trigger events — single source for validation. */
|
|
147
216
|
export const GITHUB_TRIGGER_EVENTS = [
|
|
@@ -1449,6 +1518,62 @@ export function slotRunId(scheduledFor) {
|
|
|
1449
1518
|
const iso = typeof scheduledFor === 'string' ? scheduledFor : scheduledFor.toISOString();
|
|
1450
1519
|
return iso.replace(/[:.]/g, '-');
|
|
1451
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
|
+
}
|
|
1452
1577
|
/**
|
|
1453
1578
|
* Atomically CLAIM a run directory. Returns true on a successful claim, false
|
|
1454
1579
|
* when the directory already exists (another caller — even in a separate process
|
package/dist/lib/session/db.d.ts
CHANGED
|
@@ -33,7 +33,8 @@ export declare const CONTENT_INDEX_VERSION = 1;
|
|
|
33
33
|
*/
|
|
34
34
|
/** Bump when facet extraction changes so cached rows recompute (shell-command-by-binary v7). */
|
|
35
35
|
export declare const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
36
|
-
|
|
36
|
+
/** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
|
|
37
|
+
export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
|
|
37
38
|
/** File stat snapshot used to detect changes between scan runs. */
|
|
38
39
|
export interface ScanStamp {
|
|
39
40
|
fileMtimeMs: number;
|
package/dist/lib/session/db.js
CHANGED
|
@@ -435,7 +435,8 @@ CREATE INDEX IF NOT EXISTS idx_computer_sessions_started ON computer_sessions(st
|
|
|
435
435
|
*/
|
|
436
436
|
/** Bump when facet extraction changes so cached rows recompute (shell-command-by-binary v7). */
|
|
437
437
|
export const INSIGHTS_EXTRACTOR_VERSION = 7;
|
|
438
|
-
|
|
438
|
+
/** Bump when classifyTopic's output changes so cached topics recompute (human task taxonomy v2). */
|
|
439
|
+
export const SESSION_TOPIC_EXTRACTOR_VERSION = 2;
|
|
439
440
|
const PREVIEW_EXTRACTOR_VERSION = 1;
|
|
440
441
|
let dbInstance = null;
|
|
441
442
|
/**
|
|
@@ -36,8 +36,17 @@ export function candidateBrowsers() {
|
|
|
36
36
|
if (p && fs.existsSync(p) && !out.includes(p))
|
|
37
37
|
out.push(p);
|
|
38
38
|
};
|
|
39
|
-
// 1)
|
|
40
|
-
|
|
39
|
+
// 1) Explicit overrides are contracts, not hints. A typo must fail at the
|
|
40
|
+
// boundary instead of silently falling through to an unrelated browser.
|
|
41
|
+
for (const name of ['PUPPETEER_EXECUTABLE_PATH', 'AGENTS_SHARE_BROWSER']) {
|
|
42
|
+
const value = process.env[name]?.trim();
|
|
43
|
+
if (!value)
|
|
44
|
+
continue;
|
|
45
|
+
if (!fs.existsSync(value)) {
|
|
46
|
+
throw new Error(`${name} points to a browser that does not exist: ${value}`);
|
|
47
|
+
}
|
|
48
|
+
push(value);
|
|
49
|
+
}
|
|
41
50
|
// 2) Managed Chromium in the Playwright / Puppeteer caches — purpose-built for
|
|
42
51
|
// headless, so it's the most reliable capture host when present.
|
|
43
52
|
for (const bin of scanCaches())
|
|
@@ -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,5 @@
|
|
|
1
1
|
import { type ShareConfig } from './config.js';
|
|
2
|
+
import { type ShareBackendKind } from './backend.js';
|
|
2
3
|
export type PutFn = (url: string, body: Buffer, headers: Record<string, string>) => Promise<{
|
|
3
4
|
ok: boolean;
|
|
4
5
|
status: number;
|
|
@@ -9,6 +10,8 @@ export interface PublishEndpoint {
|
|
|
9
10
|
token: string;
|
|
10
11
|
}
|
|
11
12
|
export interface PublishOptions {
|
|
13
|
+
/** Internal resolved backend; publishFile sets this after authentication. */
|
|
14
|
+
backendKind?: ShareBackendKind;
|
|
12
15
|
slug?: string;
|
|
13
16
|
/**
|
|
14
17
|
* Auto-expire window. Relative (`30d`, `12h`), absolute (`2026-08-01`), or
|
|
@@ -133,7 +136,7 @@ export interface ShareProvenance {
|
|
|
133
136
|
* provenance the CLI sets automatically, plus `expires-at` / `visibility` /
|
|
134
137
|
* `owner` which the Worker stamps itself.
|
|
135
138
|
*/
|
|
136
|
-
export declare const RESERVED_META_KEYS: readonly ["expires-at", "published-at", "visibility", "owner", "org_domain", "agent", "session", "host", "repo", "date", "avatar", "label", "label-source"];
|
|
139
|
+
export declare const RESERVED_META_KEYS: readonly ["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"];
|
|
137
140
|
/**
|
|
138
141
|
* Auto-capture publish provenance from the exec env, git, and the local clock.
|
|
139
142
|
* Every field is present only when the environment genuinely carries it — a
|
|
@@ -52,6 +52,10 @@ export const RESERVED_META_KEYS = [
|
|
|
52
52
|
'avatar',
|
|
53
53
|
'label',
|
|
54
54
|
'label-source',
|
|
55
|
+
'og-title',
|
|
56
|
+
'og-description',
|
|
57
|
+
'og-generated',
|
|
58
|
+
'og-source-etag',
|
|
55
59
|
];
|
|
56
60
|
const META_KEY_RE = /^[a-z0-9-]{1,64}$/;
|
|
57
61
|
/**
|
|
@@ -506,6 +510,7 @@ export async function publishFile(filePath, opts = {}) {
|
|
|
506
510
|
...opts,
|
|
507
511
|
githubUser: username,
|
|
508
512
|
analyticsToken,
|
|
513
|
+
backendKind: backend.kind,
|
|
509
514
|
});
|
|
510
515
|
}
|
|
511
516
|
export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
@@ -536,6 +541,19 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
536
541
|
// is only reached when --label is omitted.
|
|
537
542
|
const label = explicitLabel ? sanitizeLabel(explicitLabel) : deriveLabel(filePath, body);
|
|
538
543
|
const labelSource = explicitLabel ? 'explicit' : 'derived';
|
|
544
|
+
const ogMeta = isHtml ? deriveMeta(body.toString('utf8')) : undefined;
|
|
545
|
+
// The managed Worker owns deterministic OG generation. Point crawlers at the
|
|
546
|
+
// lazy sibling route without invoking a browser on the publishing machine.
|
|
547
|
+
if (isHtml && opts.cover !== false && opts.backendKind === 'managed' && ogMeta) {
|
|
548
|
+
coverUrl = `${pageUrl}.png`;
|
|
549
|
+
body = Buffer.from(injectOgMeta(body.toString('utf8'), {
|
|
550
|
+
...ogMeta,
|
|
551
|
+
imageUrl: coverUrl,
|
|
552
|
+
pageUrl,
|
|
553
|
+
imageWidth: OG_WIDTH,
|
|
554
|
+
imageHeight: OG_HEIGHT,
|
|
555
|
+
}), 'utf8');
|
|
556
|
+
}
|
|
539
557
|
// Pre-publish scan (RUSH-2443/RUSH-2683): refuse emails / credential-shaped
|
|
540
558
|
// strings unless --force. Runs on the raw file body AND on every piece of
|
|
541
559
|
// free-text metadata that lands in public customMetadata — --label (explicit
|
|
@@ -558,6 +576,10 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
558
576
|
// Validate the FULL customMetadata payload before any network call — fail
|
|
559
577
|
// fast, not mid-upload.
|
|
560
578
|
const metadataPreview = { ...meta, label, 'label-source': labelSource };
|
|
579
|
+
if (ogMeta && opts.backendKind === 'managed') {
|
|
580
|
+
metadataPreview['og-title'] = ogMeta.title;
|
|
581
|
+
metadataPreview['og-description'] = ogMeta.description;
|
|
582
|
+
}
|
|
561
583
|
if (provenance.agent)
|
|
562
584
|
metadataPreview.agent = provenance.agent;
|
|
563
585
|
if (provenance.session)
|
|
@@ -606,6 +628,10 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
606
628
|
setText('x-share-avatar', avatarUrl);
|
|
607
629
|
setText('x-share-label', label);
|
|
608
630
|
h['x-share-label-source'] = labelSource;
|
|
631
|
+
if (ogMeta && opts.backendKind === 'managed') {
|
|
632
|
+
setText('x-share-og-title', ogMeta.title);
|
|
633
|
+
setText('x-share-og-description', ogMeta.description);
|
|
634
|
+
}
|
|
609
635
|
// Per VALUE, before JSON.stringify — folding the serialized form would rewrite
|
|
610
636
|
// a curly quote inside a value into a bare `"`, which is structural in JSON and
|
|
611
637
|
// makes the Worker's JSON.parse throw. It swallows that error, so every --meta
|
|
@@ -633,7 +659,7 @@ export async function publishToEndpoint(filePath, endpoint, opts = {}) {
|
|
|
633
659
|
// Cover: screenshot the page's hero → upload <slug>.png → inject og:image meta.
|
|
634
660
|
// Unlisted pages still get a cover (the direct URL is the capability), but the
|
|
635
661
|
// cover inherits visibility=unlisted so it is also omitted from the gallery.
|
|
636
|
-
if (isHtml && opts.cover !== false) {
|
|
662
|
+
if (isHtml && opts.cover !== false && opts.backendKind !== 'managed') {
|
|
637
663
|
const res = await attachOgCover(filePath, body, {
|
|
638
664
|
pngUrl: `${pageUrl}.png`,
|
|
639
665
|
pageUrl,
|
|
@@ -10,4 +10,18 @@
|
|
|
10
10
|
* until its owner re-runs `agents artifacts share update`. Cosmetic renames are not
|
|
11
11
|
* worth that; change this text only alongside a real Worker behavior change.
|
|
12
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. */
|
|
13
25
|
export declare function renderWorkerScript(): string;
|
|
26
|
+
/** Unbundled Worker source. Kept separate so esbuild can resolve npm modules. */
|
|
27
|
+
export declare function renderWorkerSource(): string;
|