@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.
Files changed (56) hide show
  1. package/CHANGELOG.md +24 -0
  2. package/README.md +19 -2
  3. package/dist/commands/routines.js +31 -2
  4. package/dist/commands/share.d.ts +27 -0
  5. package/dist/commands/share.js +92 -6
  6. package/dist/commands/view.d.ts +8 -0
  7. package/dist/commands/view.js +31 -4
  8. package/dist/lib/accounting/usage.d.ts +31 -0
  9. package/dist/lib/accounting/usage.js +45 -4
  10. package/dist/lib/browser/cdp.d.ts +1 -1
  11. package/dist/lib/browser/cdp.js +1 -1
  12. package/dist/lib/browser/ffmpeg.d.ts +12 -0
  13. package/dist/lib/browser/ffmpeg.js +184 -0
  14. package/dist/lib/browser/service.js +119 -25
  15. package/dist/lib/daemon/browser-task-reap-service.d.ts +14 -0
  16. package/dist/lib/daemon/browser-task-reap-service.js +26 -0
  17. package/dist/lib/daemon/daemon.js +57 -172
  18. package/dist/lib/daemon/heartbeat-service.d.ts +13 -0
  19. package/dist/lib/daemon/heartbeat-service.js +26 -0
  20. package/dist/lib/daemon/monitor-engine-service.d.ts +9 -5
  21. package/dist/lib/daemon/monitor-engine-service.js +15 -7
  22. package/dist/lib/daemon/runner.d.ts +15 -0
  23. package/dist/lib/daemon/runner.js +23 -0
  24. package/dist/lib/daemon/secrets-broker-service.d.ts +5 -4
  25. package/dist/lib/daemon/secrets-broker-service.js +17 -32
  26. package/dist/lib/daemon/service.d.ts +2 -2
  27. package/dist/lib/daemon/service.js +1 -1
  28. package/dist/lib/daemon/session-state-service.d.ts +21 -0
  29. package/dist/lib/daemon/session-state-service.js +34 -0
  30. package/dist/lib/daemon/supervisor.d.ts +17 -7
  31. package/dist/lib/daemon/supervisor.js +87 -14
  32. package/dist/lib/daemon/tmux-reap-service.d.ts +11 -0
  33. package/dist/lib/daemon/tmux-reap-service.js +28 -0
  34. package/dist/lib/daemon/webhook-receiver-service.d.ts +9 -0
  35. package/dist/lib/daemon/webhook-receiver-service.js +17 -0
  36. package/dist/lib/daemon-services.d.ts +1 -1
  37. package/dist/lib/daemon-services.js +20 -0
  38. package/dist/lib/devices/harness-inventory.js +5 -2
  39. package/dist/lib/monitors/engine.d.ts +5 -1
  40. package/dist/lib/monitors/engine.js +5 -3
  41. package/dist/lib/overdue.js +7 -37
  42. package/dist/lib/scheduler.d.ts +21 -2
  43. package/dist/lib/scheduler.js +28 -5
  44. package/dist/lib/scheduling/routines.d.ts +68 -0
  45. package/dist/lib/scheduling/routines.js +126 -1
  46. package/dist/lib/session/db.d.ts +2 -1
  47. package/dist/lib/session/db.js +2 -1
  48. package/dist/lib/share/capture.js +11 -2
  49. package/dist/lib/share/provision.d.ts +3 -2
  50. package/dist/lib/share/provision.js +9 -4
  51. package/dist/lib/share/publish.d.ts +4 -1
  52. package/dist/lib/share/publish.js +27 -1
  53. package/dist/lib/share/worker-template.d.ts +14 -0
  54. package/dist/lib/share/worker-template.js +330 -73
  55. package/dist/lib/view-types.d.ts +6 -1
  56. 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
@@ -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
- export declare const SESSION_TOPIC_EXTRACTOR_VERSION = 1;
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;
@@ -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
- export const SESSION_TOPIC_EXTRACTOR_VERSION = 1;
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) An explicit override always wins.
40
- push(process.env.PUPPETEER_EXECUTABLE_PATH || process.env.AGENTS_SHARE_BROWSER);
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, script: string, bucketName: string, opts?: ProvisionOptions): Promise<void>;
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, script: string, writeToken: string, previousHash: string | undefined, opts?: UpdateWorkerOpts): Promise<UpdateWorkerResult>;
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, script, bucketName, opts = {}) {
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
- form.set('worker.js', new Blob([script], { type: 'application/javascript+module' }), 'worker.js');
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, script, writeToken, previousHash, opts = {}) {
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, script, bucketName, opts);
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;