@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
package/CHANGELOG.md CHANGED
@@ -1,5 +1,29 @@
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
+
9
+ ## 1.22.55
10
+
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`.
12
+
13
+ - **`agents routines status --json` — the daemon-owned scheduler status surface (PHNX-3215).** `routines status` printed human-only scheduler health and upcoming fires with no machine-readable form; the JSON that existed (`routines list --json`) is definition-shaped and does not answer "did this routine fire here, and is one running right now?" The new `--json` emits a `scheduler` block (state, pid, heartbeat age, enabled/total counts) plus a `routines` array where each row carries the routine's single **owner device** (`ownerDevice`, with `ambiguousDevicePin` when `devices:` names more than one), whether it fires here (`runsHere`), the last local fire's outcome and error (`lastStatus`, `failureReason`, `lastRunStartedAt`/`CompletedAt`), and any **in-flight spawn** (`inFlight: { runId, pid, startedAt, triggerKind }`) — surfaced only for a run that is genuinely live on this device (a live local child or a host-placed run), never a provisional pre-spawn claim. The routine set and counts are the schedulable ones (matching plain `routines status`), so the JSON never inflates totals with discoverable-but-unmaterialised project routines. Source: `cli/src/lib/scheduling/routines.ts` (`buildRoutineStatusRows`), `cli/src/commands/routines.ts`.
14
+
15
+ ### Fixed
16
+
17
+ - Daemon service restarts now replace periodic timers instead of multiplying their tick rate, and a timed-out tick parks until its real work settles instead of overlapping teardown or another tick. Live-session metadata publishing, monitor evaluation, heartbeat/process reconciliation, tmux and browser-task reaping, broker self-heal, and signed webhook ingress now run under the per-service supervisor with measured health and isolated failure handling.
18
+
19
+ - **`agents view --json` and `agents devices harnesses/accounts --json` no longer leak the internal `'stale'` sentinel in their usage-error fields (PHNX-3348).** Without `--refresh`, a read-only usage lookup on a never-cached account returns the internal `USAGE_NOT_COLLECTED_MARKER` (`'stale'`) — a cache signal, not a message — and the JSON builders emitted it verbatim (as `usageError`, and as `quota.unavailableReason`), contradicting those fields' "human-readable" contract. Both surfaces now normalize through a new shared `usageErrorForDisplay` to a plain-language, actionable string (`Usage not collected yet — run \`agents view --refresh\` to fetch it.`); every genuine, already-human error passes through unchanged, and the human terminal renders were already correct and are untouched. Source: `cli/src/lib/accounting/usage.ts`, `cli/src/commands/view.ts`, `cli/src/lib/view-types.ts`, `cli/src/lib/devices/harness-inventory.ts`.
20
+
21
+ - **`agents artifacts share open <target>` — open your own published page signed in as the owner, so its inline visibility control is actually usable (PHNX-3370).** The shared page's `Public`/`Unlisted`/… chip is an interactive dropdown only for the signed-in owner, but nothing ever handed a browser the login it needed — the Worker could *redeem* a `phoenix_ticket` cookie yet nothing *minted* one, so opening your own link left the chip a static cue. Now the share Worker mints a short-lived, single-use, self-signed login ticket at `POST /__ticket` (authenticated by the caller's Phoenix bearer — it grants nothing the caller didn't already have, and is domain-separated from the identity cookie so neither can be replayed as the other), and `share open` appends it to your page URL so the browser trades it for the `__share` cookie and the chip becomes a live control. Managed (Phoenix) endpoints only; run `agents artifacts share update` once to redeploy a Worker that predates the mint route. `agents artifacts share visibility <target> <level>` remains the no-browser way to re-scope in place. Source: `cli/src/commands/share.ts`, `cli/src/lib/share/worker-template.ts`.
22
+
23
+ - **The new human task taxonomy actually applies to existing sessions now.** The traces topic classifier's output changed (Feature work / Bug fixes / Refactor / … in 1.22.54), but `classifyTopic`'s result is cached per-session in `session_topics` keyed by `SESSION_TOPIC_EXTRACTOR_VERSION` — which wasn't bumped, so every already-classified session stayed a cache hit at v1 with its old `Engineering`/`Operations` label and `agents traces sync` kept re-uploading the *old* labels. Bumped the version 1→2 so every session re-classifies with the new taxonomy on the next sync, and the Evals console treemap reflects it. Source: `cli/src/lib/session/db.ts`.
24
+
25
+ - **`agents view claude` shows live session + week usage bars again on a personal device (RUSH-2392).** A foreground `agents view` at a TTY on a `personal` device now falls through to the interactive Claude login for the usage read — the only credential carrying the `user:profile` scope the usage endpoint needs — so `agents view --refresh` repopulates the session (5h) + week (7d) window for every signed-in account instead of showing `S: █████ unavailable` on idle homes. `3f3554c51` (RUSH-1822) had removed that fallback for *every* caller to stop the daemon's unattended usage/auth-health loops from firing the interactive token at Anthropic and getting the fleet logged out; that guarantee is intact — the read is now gated by `allowInteractiveUsageLogin(role, isTTY)`, so every background caller (daemon usage warm, auth-health probe, watchdog) and every `--json`/piped reader stays setup-token-only and default-closed. #2987's blanket-wording change was treating the symptom; this restores the data. A usage read still never *refreshes* an access token — an expired interactive login reports `expired-credential`. Source: `cli/src/lib/accounting/usage.ts`, `cli/src/commands/view.ts`, `cli/docs/secrets.md`, `cli/docs/specifications.md`.
26
+
3
27
  ## 1.22.54
4
28
 
5
29
  - **`agents accounts mint claude` (also `agents auth mint claude`) mints a
package/README.md CHANGED
@@ -959,6 +959,9 @@ agents browser refs # Get interactive element refs
959
959
  agents browser click 42 # Click element ref 42
960
960
  agents browser type 15 --text "hello" # Type into element ref 15
961
961
  agents browser screenshot # Smart resizing, token-efficient
962
+ agents browser record start --fps 10 --duration 40
963
+ # Drive the page, then finalize a playable WebM under the task's recordings/ dir.
964
+ agents browser record stop
962
965
  agents browser tabs # List tabs open for the current task
963
966
  agents browser tab focus tab123 # Switch focus to another tab
964
967
  agents browser done # Close task's tabs when finished
@@ -973,6 +976,11 @@ printf '%s\n' \
973
976
  | agents browser stream --task "$AGENTS_BROWSER_TASK"
974
977
  ```
975
978
 
979
+ Recording resolves ffmpeg automatically. It reuses a usable binary on `PATH` or
980
+ under `~/.agents/.cache`, and when none exists it installs through Homebrew on
981
+ macOS, apt on Debian/Ubuntu, or the available platform package manager. If the
982
+ automatic path cannot run, the error names the exact manual install command.
983
+
976
984
  ### Why this works where Playwright fails
977
985
 
978
986
  Playwright and Puppeteer spin up fresh browser instances with automation flags. Sites like LinkedIn, Google, and most finance apps detect and block them immediately.
@@ -1156,6 +1164,7 @@ agents routines add daily-digest \
1156
1164
  --prompt "Review yesterday's PRs and summarize key changes"
1157
1165
 
1158
1166
  agents routines list # All jobs + next run times
1167
+ agents routines status # Scheduler health + next fires (add --json for owner device, last fire, last error, in-flight run per routine)
1159
1168
  agents routines run daily-digest # Test it now, ignore the schedule
1160
1169
  agents routines logs daily-digest # Last execution — status + report (add --full for raw stdout)
1161
1170
  agents routines runs daily-digest # Every attempt, including blocked/skipped pre-session runs
@@ -1287,6 +1296,7 @@ agents artifacts share list --scope me # just the owner-onl
1287
1296
  agents artifacts share edit fleet --label "Final fleet plan" --meta status=final
1288
1297
  agents artifacts share revisions fleet # prior versions kept under a slug
1289
1298
  agents artifacts share visibility fleet me # re-scope a published page in place (public|unlisted|me|org)
1299
+ agents artifacts share open fleet # open your page signed in, so its in-page visibility control is live
1290
1300
  agents artifacts share status # show the endpoint
1291
1301
  agents artifacts unshare fleet # take a published link (+ its OG cover) down
1292
1302
  ```
@@ -1298,7 +1308,10 @@ Cloudflare account, bucket, or write token — and `share status` / `list` / `re
1298
1308
  / `unshare` talk to that same endpoint. The handle is the local-part of the signed-in
1299
1309
  email (`muqsitnawaz@gmail.com` → `muqsitnawaz`); the page slug is readable plus a
1300
1310
  short view-id. HTML is stored as one object: local images are inlined, `file://` TOC
1301
- links become in-page hashes, so the published page is actually viewable. `--visibility unlisted` (hidden aliases
1311
+ links become in-page hashes, so the published page is actually viewable. The managed
1312
+ Worker lazily renders and caches the branded 1200×630 Open Graph card at `<slug>.png`,
1313
+ so publishing from Linux does not require a local Chromium; BYO endpoints keep the
1314
+ local screenshot fallback. `--visibility unlisted` (hidden aliases
1302
1315
  `--unlisted` / `--private`) is a capability URL: GET still works, the gallery hides it,
1303
1316
  and the Worker sends `X-Robots-Tag: noindex`. `--visibility me` is visible only to
1304
1317
  you (the signed-in owner); `--visibility org` is visible to anyone at your email
@@ -1309,7 +1322,11 @@ stranger is redirected to sign in, then 404s). `share visibility <target> <level
1309
1322
  re-scopes an **already-published** page in place — the slug/URL is preserved and the
1310
1323
  body is untouched (a metadata-only change, like `share edit`, so no revision) — so you
1311
1324
  can promote a draft to `public` or pull a link back to `me`/`org` without re-publishing
1312
- (`me`/`org` need a Phoenix session; `org` needs a workspace domain). Sign-in is a
1325
+ (`me`/`org` need a Phoenix session; `org` needs a workspace domain). The served page
1326
+ also carries an **in-page visibility control** — a chip you can switch right on the page
1327
+ — but it is interactive only for the signed-in owner, and a plain link has no login;
1328
+ `share open <target>` mints a short-lived, single-use login ticket and opens your page
1329
+ with it so the chip becomes a live dropdown. Sign-in is a
1313
1330
  single **Phoenix ID** (Google-only device-code OAuth, `agents auth login`) — see
1314
1331
  [`docs/share.md`](cli/docs/share.md) for the full identity + visibility model.
1315
1332
  **BYO Cloudflare** remains: `setup` reads
@@ -16,7 +16,7 @@ import { parseAgentVersionSpec, isAgentHardDeprecated, hardDeprecationError, ROU
16
16
  import { isCustomHarnessName } from '../lib/profiles.js';
17
17
  import { RUN_STRATEGIES, normalizeRunStrategy } from '../lib/accounting/rotate.js';
18
18
  import { formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
19
- import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, isCanonicalRoutineSource, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, HOST_STRATEGIES, computeProjectGroupKind, projectGroupKey, projectGroupTitle, projectGroupOrder, normalizeProjects, buildRoutineListJson, fireConditionLabel, nextRunForDisplay, nextRunLabel, localLatestRun, listJobsForDisplay, } from '../lib/scheduling/routines.js';
19
+ import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, routineStats, getLatestRun, getRunDir, getJobPath, isCanonicalRoutineSource, parseAtTime, hasCompletedOneShotRun, isOneShotLikeSchedule, isOneShotRoutine, jobRunsOnThisDevice, checkJobDeviceEligibility, normalizeTriggerEvent, parseHostStrategy, resolveHostStrategy, HOST_STRATEGIES, computeProjectGroupKind, projectGroupKey, projectGroupTitle, projectGroupOrder, normalizeProjects, buildRoutineListJson, buildRoutineStatusRows, fireConditionLabel, nextRunForDisplay, nextRunLabel, localLatestRun, listJobsForDisplay, } from '../lib/scheduling/routines.js';
20
20
  export { buildRoutineListJson } from '../lib/scheduling/routines.js';
21
21
  import { listProjectDefs, isSafeProjectName } from '../lib/projects.js';
22
22
  import { evaluateActivationReadinessLive } from '../lib/routine-readiness.js';
@@ -831,6 +831,9 @@ export function registerRoutinesCommands(program) {
831
831
 
832
832
  # Check whether the scheduler is running
833
833
  agents routines status
834
+
835
+ # Machine-readable scheduler + per-routine status: owner device, last fire, last error, in-flight run
836
+ agents routines status --json
834
837
  `,
835
838
  notes: `
836
839
  A routine is a YAML file that schedules an agent invocation. It specifies:
@@ -2148,12 +2151,38 @@ export function registerRoutinesCommands(program) {
2148
2151
  routinesCmd
2149
2152
  .command('status')
2150
2153
  .description('Show scheduler status, enabled routines, and when each one fires next.')
2151
- .action(() => {
2154
+ .option('--json', 'Emit machine-readable scheduler + per-routine status (owner device, last fire, last error, in-flight run)')
2155
+ .action((options) => {
2152
2156
  try {
2153
2157
  monitorRunningJobs();
2154
2158
  }
2155
2159
  catch { /* best-effort orphan reap */ }
2156
2160
  const status = getDaemonStatus();
2161
+ // The daemon-owned status surface (PHNX-3215): scheduler truth plus, per
2162
+ // routine, its single owner device, last fire outcome + error, and any
2163
+ // in-flight spawn — the fields `list --json` (definition-shaped) does not
2164
+ // carry. Emitted from THIS device, so per-device fields read as this box's.
2165
+ if (options.json) {
2166
+ const rows = buildRoutineStatusRows();
2167
+ writeJson({
2168
+ device: machineId(),
2169
+ scheduler: {
2170
+ state: status.state,
2171
+ pid: status.pid,
2172
+ binaryPath: status.binaryPath,
2173
+ heartbeat: status.heartbeat
2174
+ ? {
2175
+ lastTick: status.heartbeat.lastTick,
2176
+ ageSec: Math.round((Date.now() - Date.parse(status.heartbeat.lastTick)) / 1000),
2177
+ }
2178
+ : null,
2179
+ enabled: rows.filter((r) => r.enabled).length,
2180
+ total: rows.length,
2181
+ },
2182
+ routines: rows,
2183
+ });
2184
+ return;
2185
+ }
2157
2186
  console.log(chalk.bold('Scheduler\n'));
2158
2187
  const stateLabel = status.state === 'running'
2159
2188
  ? chalk.green('running')
@@ -172,6 +172,33 @@ export declare function runShareRevisions(target: string, opts?: {
172
172
  session?: PhoenixSession | null;
173
173
  fetchListing?: ListingFetchFn;
174
174
  }): Promise<ShareRevisionsResult>;
175
+ /** The resolved page URL plus a signed-in-owner URL that carries a one-time
176
+ * login ticket, so opening it activates the shared page's inline visibility
177
+ * control (PHNX-3370). */
178
+ export interface ShareOpenResult {
179
+ /** `<user>/<slug>` key of the target page. */
180
+ key: string;
181
+ /** The plain page URL (no ticket). */
182
+ url: string;
183
+ /** The page URL with `?phoenix_ticket=<t>` — open THIS to be the owner. */
184
+ ownerUrl: string;
185
+ }
186
+ /** Mint a one-time login ticket for the signed-in owner and build the URL that
187
+ * opens their own published page **as the owner**, so the inline visibility
188
+ * control is live in the browser. Managed backend only — the control is gated on
189
+ * the Phoenix identity, and BYO (WRITE_TOKEN) endpoints have no per-viewer login.
190
+ * `target` accepts the same three forms as `visibility`/`unshare` (a full URL,
191
+ * `<user>/<slug>`, or a bare slug in the caller's namespace). */
192
+ export declare function runShareOpen(target: string, opts?: {
193
+ githubUser?: string;
194
+ config?: ShareConfig;
195
+ session?: PhoenixSession | null;
196
+ /** DI seam — override the ticket-mint POST. */
197
+ fetchTicket?: (url: string, bearer: string) => Promise<{
198
+ status: number;
199
+ body: string;
200
+ }>;
201
+ }): Promise<ShareOpenResult>;
175
202
  export declare function formatShareRevisions(result: ShareRevisionsResult, json?: boolean): string;
176
203
  export declare function formatShareDeleteResult(result: DeleteShareResult, json?: boolean): string;
177
204
  interface ShareDeleteCliOpts {
@@ -13,11 +13,12 @@ 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 { renderWorkerScript } from '../lib/share/worker-template.js';
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';
20
20
  import { setHelpSections } from '../lib/help.js';
21
+ import { showUrl } from '../lib/open-url.js';
21
22
  export function formatSharePublishResult(result, json = false) {
22
23
  if (json)
23
24
  return JSON.stringify(result, null, 2);
@@ -53,7 +54,7 @@ export function formatSharePublishResult(result, json = false) {
53
54
  export function shareTemplateStatus(cfg) {
54
55
  if (!cfg.templateHash)
55
56
  return 'unknown';
56
- return cfg.templateHash === hashWorkerScript(renderWorkerScript()) ? 'current' : 'outdated';
57
+ return cfg.templateHash === hashWorkerScript(renderWorkerBundle().script) ? 'current' : 'outdated';
57
58
  }
58
59
  export async function runShareEdit(target, opts) {
59
60
  // Same public-listing gate as publish: label + every metadata value are
@@ -383,6 +384,49 @@ export async function runShareRevisions(target, opts = {}) {
383
384
  }
384
385
  return parseShareRevisions(key, res.body);
385
386
  }
387
+ /** Mint a one-time login ticket for the signed-in owner and build the URL that
388
+ * opens their own published page **as the owner**, so the inline visibility
389
+ * control is live in the browser. Managed backend only — the control is gated on
390
+ * the Phoenix identity, and BYO (WRITE_TOKEN) endpoints have no per-viewer login.
391
+ * `target` accepts the same three forms as `visibility`/`unshare` (a full URL,
392
+ * `<user>/<slug>`, or a bare slug in the caller's namespace). */
393
+ export async function runShareOpen(target, opts = {}) {
394
+ const backend = resolveShareBackend({
395
+ githubUser: opts.githubUser,
396
+ config: opts.config,
397
+ session: opts.session,
398
+ requireToken: false,
399
+ });
400
+ if (backend.kind !== 'managed') {
401
+ throw new Error("'agents artifacts share open' needs a managed (Phoenix) endpoint — the page's inline " +
402
+ "visibility control is gated on your signed-in identity. Run 'agents auth login' first.");
403
+ }
404
+ const baseUrl = backend.baseUrl.replace(/\/+$/, '');
405
+ const { key } = await resolveDeleteTarget(target, { githubUser: backend.namespace });
406
+ const url = `${baseUrl}/${key}`;
407
+ const doFetch = opts.fetchTicket ??
408
+ (async (u, bearer) => {
409
+ const res = await fetch(u, { method: 'POST', headers: { authorization: `Bearer ${bearer}` } });
410
+ return { status: res.status, body: await res.text() };
411
+ });
412
+ const res = await doFetch(`${baseUrl}/__ticket`, backend.token);
413
+ if (res.status !== 200) {
414
+ throw new Error(`Could not mint a login ticket (${res.status}) from ${baseUrl}/__ticket. ` +
415
+ "The endpoint may predate this feature — run 'agents artifacts share update' to redeploy the Worker.");
416
+ }
417
+ let ticket;
418
+ try {
419
+ const parsed = JSON.parse(res.body);
420
+ if (typeof parsed.ticket !== 'string' || !parsed.ticket)
421
+ throw new Error('no ticket in response');
422
+ ticket = parsed.ticket;
423
+ }
424
+ catch {
425
+ throw new Error(`Ticket endpoint at ${baseUrl}/__ticket did not return a usable ticket.`);
426
+ }
427
+ const ownerUrl = `${url}?phoenix_ticket=${encodeURIComponent(ticket)}`;
428
+ return { key, url, ownerUrl };
429
+ }
386
430
  export function formatShareRevisions(result, json = false) {
387
431
  if (json)
388
432
  return JSON.stringify(result, null, 2);
@@ -714,6 +758,47 @@ ${SHARE_DELETE_NOTES}
714
758
  examples: SHARE_VISIBILITY_EXAMPLES,
715
759
  notes: SHARE_VISIBILITY_NOTES,
716
760
  });
761
+ const shareOpenCmd = shareCmd
762
+ .command('open')
763
+ .description("Open your own published page in the browser signed in as the owner, so the page's inline visibility control is live. Mints a one-time login ticket and appends it to the URL.")
764
+ .addArgument(new Argument('<target>', 'the published page: a full URL, <user>/<slug>, or a bare slug in your namespace'))
765
+ .option('--for-user <user>', 'GitHub username for resolving a bare-slug target (default: resolved from your Phoenix handle)')
766
+ .option('--no-open', 'print the owner URL instead of opening a browser')
767
+ .option('--json', 'emit a machine-readable result (includes the ticketed owner URL)')
768
+ .action(async (target, opts) => {
769
+ try {
770
+ const result = await runShareOpen(target, { githubUser: opts.forUser });
771
+ if (opts.json) {
772
+ console.log(JSON.stringify(result, null, 2));
773
+ return;
774
+ }
775
+ if (opts.open === false) {
776
+ console.log(chalk.green(result.ownerUrl));
777
+ console.log(chalk.dim(' open this to land on the page as the owner (the visibility chip becomes a control)'));
778
+ return;
779
+ }
780
+ const shown = await showUrl(result.ownerUrl);
781
+ if (shown.via === 'none') {
782
+ console.log(chalk.green(result.ownerUrl));
783
+ console.error(chalk.dim(' could not open a browser — open the URL above yourself'));
784
+ }
785
+ else {
786
+ console.log(chalk.green(`Opened ${result.url} as owner — the visibility chip is now a live control.`));
787
+ }
788
+ }
789
+ catch (e) {
790
+ console.error(chalk.red(e.message));
791
+ process.exitCode = 1;
792
+ }
793
+ });
794
+ setHelpSections(shareOpenCmd, {
795
+ examples: `agents artifacts share open q3-plan # open your page, signed in, ready to re-scope
796
+ agents artifacts share open octocat/q3-plan
797
+ agents artifacts share open https://share.agents-cli.sh/octocat/q3-plan --no-open`,
798
+ notes: `The shared page's visibility chip is an interactive control only for the signed-in owner; a plain link has no login, so the chip renders as a static cue. This mints a short-lived, single-use login ticket (self-signed by your share Worker) and opens the page with it, so the chip becomes a dropdown you can switch in place.
799
+ Managed (Phoenix) endpoints only — run 'agents auth login' first. If the mint 501s/404s, your Worker predates the feature: run 'agents artifacts share update' to redeploy it.
800
+ Prefer to change visibility without a browser? Use 'agents artifacts share visibility <target> <level>'.`,
801
+ });
717
802
  shareCmd
718
803
  .command('join')
719
804
  .description('Use an existing synced share endpoint and write token (no provisioning).')
@@ -1070,7 +1155,8 @@ export async function runShareProvision(opts) {
1070
1155
  const bucketName = opts.bucket;
1071
1156
  const token = generateWriteToken();
1072
1157
  const requestedDomain = cleanHostname(opts.domain) ?? DEFAULT_SHARE_DOMAIN;
1073
- const script = renderWorkerScript();
1158
+ const worker = renderWorkerBundle();
1159
+ const script = worker.script;
1074
1160
  const spin = ora('Provisioning on Cloudflare…').start();
1075
1161
  try {
1076
1162
  const provisionOpts = opts.request ? { request: opts.request } : {};
@@ -1078,7 +1164,7 @@ export async function runShareProvision(opts) {
1078
1164
  spin.text = `R2 bucket '${bucketName}' ready`;
1079
1165
  await configureBucketLifecycle(apiToken, accountId, bucketName, provisionOpts);
1080
1166
  spin.text = `R2 bucket '${bucketName}' lifecycle ready`;
1081
- await deployWorker(apiToken, accountId, workerName, script, bucketName, provisionOpts);
1167
+ await deployWorker(apiToken, accountId, workerName, worker, bucketName, provisionOpts);
1082
1168
  spin.text = `Worker '${workerName}' deployed`;
1083
1169
  await setWorkerSecret(apiToken, accountId, workerName, token, provisionOpts);
1084
1170
  spin.text = `Worker '${workerName}' write token set`;
@@ -1145,14 +1231,14 @@ export async function runShareUpdate(opts = {}) {
1145
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'.");
1146
1232
  }
1147
1233
  const writeToken = readWriteToken();
1148
- const script = renderWorkerScript();
1234
+ const worker = renderWorkerBundle();
1149
1235
  const phoenixIdBase = phoenixIdBaseForDeploy({ managed: opts.managed }, cfg);
1150
1236
  const provisionOpts = {
1151
1237
  ...(opts.request ? { request: opts.request } : {}),
1152
1238
  force: opts.force,
1153
1239
  ...(phoenixIdBase !== undefined ? { phoenixIdBase } : {}),
1154
1240
  };
1155
- const result = await updateWorker(apiToken, accountId, cfg.workerName, cfg.bucketName, script, writeToken, cfg.templateHash, provisionOpts);
1241
+ const result = await updateWorker(apiToken, accountId, cfg.workerName, cfg.bucketName, worker, writeToken, cfg.templateHash, provisionOpts);
1156
1242
  if (!result.skipped) {
1157
1243
  writeShareConfig({ ...cfg, accountId, templateHash: result.templateHash });
1158
1244
  }
@@ -11,6 +11,7 @@ import { accountDisplayLabel } from '../lib/agents.js';
11
11
  import type { AccountInfo } from '../lib/agents.js';
12
12
  import type { AgentId } from '../lib/types.js';
13
13
  import type { FormatUsageSummaryOpts, UsageInfo } from '../lib/accounting/usage.js';
14
+ import { type ConfiguredDeviceRole } from '../lib/device-config.js';
14
15
  import type { ResourceSection, ViewJsonAgent } from '../lib/view-types.js';
15
16
  export type { ResourceItemJson, ResourceSection, SyncState, VersionResourcesJson, ViewJsonAgent, ViewJsonVersion } from '../lib/view-types.js';
16
17
  import { type ProfileSummary } from '../lib/profiles.js';
@@ -68,6 +69,13 @@ export declare function descriptionForPrefix(desc: string | undefined, prefix: s
68
69
  * host is missing is flagged rather than silently listed as runnable.
69
70
  */
70
71
  export declare function renderHarnessBlocks(harnesses: ProfileSummary[], installedHosts: Set<AgentId>, showPaths: boolean, byokMap?: Map<string, ByokUsageResult>): void;
72
+ /**
73
+ * The USAGE-READ-2 decision, isolated from its runtime inputs so it can be
74
+ * tested directly: a usage read may fall through to the interactive OAuth login
75
+ * ONLY for a foreground human render on a `personal` device. Both conditions are
76
+ * required — role alone is not sufficient.
77
+ */
78
+ export declare function allowInteractiveUsageLogin(role: ConfiguredDeviceRole | undefined, isTTY: boolean): boolean;
71
79
  /** Build the set of resource sections to include in `--json` from the
72
80
  * `--resources` / `--detailed` flags, plus (in --json mode) the per-section
73
81
  * boolean filters (`--skills` etc.) that plain `--json` historically ignored. */
@@ -8,7 +8,8 @@ import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getUnmanag
8
8
  import { ambientClaudeToken, loginHint } from '../lib/signin-badge.js';
9
9
  import { machineId } from '../lib/machine-id.js';
10
10
  import { authCacheKey, readAuthHealthCache } from '../lib/auth-health.js';
11
- import { agentReportsUsage, classifyUsageErrorKind, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageBenignState, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, } from '../lib/accounting/usage.js';
11
+ import { agentReportsUsage, classifyUsageErrorKind, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageBenignState, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, usageErrorForDisplay, } from '../lib/accounting/usage.js';
12
+ import { selfConfiguredDeviceRole } from '../lib/device-config.js';
12
13
  import { readManifest } from '../lib/manifest.js';
13
14
  import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/installations/versions.js';
14
15
  import { ensureVersionedAliasCurrent, removeShim, } from '../lib/installations/shims.js';
@@ -316,6 +317,29 @@ function renderHostClisSection(cwd) {
316
317
  console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
317
318
  }
318
319
  }
320
+ /**
321
+ * The USAGE-READ-2 decision, isolated from its runtime inputs so it can be
322
+ * tested directly: a usage read may fall through to the interactive OAuth login
323
+ * ONLY for a foreground human render on a `personal` device. Both conditions are
324
+ * required — role alone is not sufficient.
325
+ */
326
+ export function allowInteractiveUsageLogin(role, isTTY) {
327
+ return role === 'personal' && isTTY === true;
328
+ }
329
+ /**
330
+ * Whether this `agents view` invocation may fall through to the interactive
331
+ * OAuth login for a usage read (USAGE-READ-2). True only for a foreground human
332
+ * render on a `personal` device: the interactive login is the sole credential
333
+ * carrying the `user:profile` scope the usage endpoint needs, and a human
334
+ * running one command is not the unattended-loop revocation risk RUSH-1822
335
+ * fixed. The `--json` path never reaches these render functions (it returns
336
+ * early via `collectAgentsJson`), and a non-TTY (piped/scripted) run is excluded
337
+ * here too, so a machine reader can never silently acquire the interactive
338
+ * credential — role alone is not sufficient.
339
+ */
340
+ function usageAllowInteractiveLogin() {
341
+ return allowInteractiveUsageLogin(selfConfiguredDeviceRole(), process.stdout.isTTY === true);
342
+ }
319
343
  async function showInstalledVersions(filterAgentId, viewOpts) {
320
344
  const spinnerText = filterAgentId
321
345
  ? `Checking ${agentLabel(filterAgentId)} agents...`
@@ -422,7 +446,10 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
422
446
  cliVersion,
423
447
  info,
424
448
  })),
425
- ], { forceRefresh: viewOpts?.forceRefresh });
449
+ ], {
450
+ forceRefresh: viewOpts?.forceRefresh,
451
+ allowInteractiveLogin: usageAllowInteractiveLogin(),
452
+ });
426
453
  spinner.stop();
427
454
  console.log(chalk.bold('Installed Agent CLIs\n'));
428
455
  const mergeCanonical = (info) => {
@@ -930,7 +957,7 @@ async function showAgentResources(agentId, requestedVersion, filter) {
930
957
  home,
931
958
  cliVersion: version,
932
959
  info: accountInfo,
933
- });
960
+ }, { allowInteractiveLogin: usageAllowInteractiveLogin() });
934
961
  const accountLabel = accountColumnLabel(accountInfo);
935
962
  const emailStr = accountLabel ? chalk.cyan(` ${accountLabel}`) : '';
936
963
  const status = chalk.green(version);
@@ -1273,7 +1300,7 @@ export async function collectAgentsJson(filterAgentId, resourceSections, opts) {
1273
1300
  plan: info.plan,
1274
1301
  usageStatus: info.usageStatus,
1275
1302
  overageCredits: info.overageCredits,
1276
- usageError: usageInfo?.error ?? null,
1303
+ usageError: usageErrorForDisplay(usageInfo?.error),
1277
1304
  windows: snapshot
1278
1305
  ? snapshot.windows.map((w) => ({
1279
1306
  key: w.key,
@@ -108,6 +108,18 @@ export type UsageBenignState = 'no-recent-usage';
108
108
  * is unchanged; callers that already compare against `'stale'` keep working.
109
109
  */
110
110
  export declare const USAGE_NOT_COLLECTED_MARKER = "stale";
111
+ /**
112
+ * Human-facing form of a `UsageInfo.error` for a machine/JSON consumer
113
+ * (`agents view --json`'s `usageError`). Every error string this module
114
+ * constructs is already a full human sentence EXCEPT the internal
115
+ * {@link USAGE_NOT_COLLECTED_MARKER} (`'stale'`) sentinel, which a read-only
116
+ * lookup returns for a never-cached account when `--refresh` was not passed. That
117
+ * value is an internal cache signal, not an error message, and leaking it verbatim
118
+ * contradicts the field's "human-readable" contract (PHNX-3348). Map it to a
119
+ * plain-language, actionable string and pass every genuine error through
120
+ * unchanged. Returns `null` when there is no error.
121
+ */
122
+ export declare function usageErrorForDisplay(error: string | null | undefined): string | null;
111
123
  /**
112
124
  * Shared error-classification + 429 backoff for a networked usage fetch whose
113
125
  * only signal is an HTTP status (or none at all, on a network failure) —
@@ -212,6 +224,16 @@ interface UsageOptions {
212
224
  * setup-token, or `<home>/.claude/.credentials.json` only.
213
225
  */
214
226
  fileOnly?: boolean;
227
+ /**
228
+ * When true, a read that finds no file-based setup-token MAY fall through to
229
+ * Claude Code's interactive OAuth login (the only credential carrying
230
+ * `user:profile`, which `/api/oauth/usage` requires). OFF by default and set
231
+ * ONLY by a foreground human `agents view` on a `personal` device (see
232
+ * USAGE-READ-2). Every background caller — daemon usage warm, auth-health
233
+ * probe, watchdog — leaves it unset, preserving the RUSH-1822 guarantee that
234
+ * an unattended loop never transmits the interactive login to Anthropic.
235
+ */
236
+ allowInteractiveLogin?: boolean;
215
237
  }
216
238
  /** Canonical input for a single usage fetch operation. */
217
239
  export interface UsageFetchInput {
@@ -308,6 +330,14 @@ export declare const USAGE_FETCH_CONCURRENCY = 3;
308
330
  export interface UsageLookupOptions {
309
331
  forceRefresh?: boolean;
310
332
  fileOnly?: boolean;
333
+ /**
334
+ * Permit a foreground personal-device usage read to fall through to the
335
+ * interactive login when no setup-token exists (USAGE-READ-2). Set ONLY by
336
+ * `agents view` when `selfConfiguredDeviceRole() === 'personal'` and the
337
+ * output is a human TTY (not `--json`). Threads into `getClaudeUsageInfo` →
338
+ * `loadClaudeOauth`. Unset for every other lookup.
339
+ */
340
+ allowInteractiveLogin?: boolean;
311
341
  }
312
342
  export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[], opts?: UsageLookupOptions): Promise<{
313
343
  canonicalByUsageKey: Map<string, AccountInfo>;
@@ -610,6 +640,7 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
610
640
  export declare function loadClaudeOauth(home?: string, opts?: {
611
641
  accessTokenCache?: boolean;
612
642
  fileOnly?: boolean;
643
+ allowInteractiveLogin?: boolean;
613
644
  }): Promise<ClaudeOauthCredentials | null>;
614
645
  /**
615
646
  * Save Claude OAuth credentials to the system keychain/keyring.
@@ -173,6 +173,25 @@ export const USAGE_BENIGN_STATE = Symbol('usageBenignState');
173
173
  * is unchanged; callers that already compare against `'stale'` keep working.
174
174
  */
175
175
  export const USAGE_NOT_COLLECTED_MARKER = 'stale';
176
+ /**
177
+ * Human-facing form of a `UsageInfo.error` for a machine/JSON consumer
178
+ * (`agents view --json`'s `usageError`). Every error string this module
179
+ * constructs is already a full human sentence EXCEPT the internal
180
+ * {@link USAGE_NOT_COLLECTED_MARKER} (`'stale'`) sentinel, which a read-only
181
+ * lookup returns for a never-cached account when `--refresh` was not passed. That
182
+ * value is an internal cache signal, not an error message, and leaking it verbatim
183
+ * contradicts the field's "human-readable" contract (PHNX-3348). Map it to a
184
+ * plain-language, actionable string and pass every genuine error through
185
+ * unchanged. Returns `null` when there is no error.
186
+ */
187
+ export function usageErrorForDisplay(error) {
188
+ if (!error)
189
+ return null;
190
+ if (error === USAGE_NOT_COLLECTED_MARKER) {
191
+ return 'Usage not collected yet — run `agents view --refresh` to fetch it.';
192
+ }
193
+ return error;
194
+ }
176
195
  /**
177
196
  * Shared error-classification + 429 backoff for a networked usage fetch whose
178
197
  * only signal is an HTTP status (or none at all, on a network failure) —
@@ -395,6 +414,7 @@ export async function getUsageInfoForIdentity(input, opts) {
395
414
  cliVersion: input.cliVersion,
396
415
  organizationId: input.info.organizationId,
397
416
  fileOnly: opts?.fileOnly,
417
+ allowInteractiveLogin: opts?.allowInteractiveLogin,
398
418
  });
399
419
  }
400
420
  const cached = readClaudeUsageCache(usageKey);
@@ -416,14 +436,16 @@ export async function getUsageInfoForIdentity(input, opts) {
416
436
  return { snapshot: null, error: USAGE_NOT_COLLECTED_MARKER };
417
437
  }
418
438
  // Explicit refresh: block on the shared device collector.
419
- return fetchLiveUsageDeduped(input, usageKey, cached, opts?.fileOnly === true);
439
+ return fetchLiveUsageDeduped(input, usageKey, cached, opts?.fileOnly === true, {
440
+ allowInteractiveLogin: opts?.allowInteractiveLogin === true,
441
+ });
420
442
  }
421
443
  /**
422
444
  * Single-flight live usage fetch per usage key. Concurrent callers (view +
423
445
  * rotation, or two rows sharing an account) await the same promise rather than
424
446
  * opening duplicate HTTP requests that then time out and pile up.
425
447
  */
426
- async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
448
+ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly, opts) {
427
449
  const existing = inFlightLiveFetches.get(usageKey);
428
450
  if (existing)
429
451
  return existing;
@@ -446,6 +468,7 @@ async function fetchLiveUsageDeduped(input, usageKey, cached, fileOnly) {
446
468
  // throttled account cannot park the whole provider (RUSH-3036).
447
469
  usageScope: usageKey,
448
470
  fileOnly,
471
+ allowInteractiveLogin: opts?.allowInteractiveLogin === true,
449
472
  });
450
473
  if (usage.snapshot) {
451
474
  if (!usage.snapshot.capturedAt || usage.snapshot.capturedAt.getTime() <= previousCapturedAt) {
@@ -822,9 +845,17 @@ async function getClaudeUsageInfo(options) {
822
845
  // the interactive login (reading that ACL-bound token and firing it at the
823
846
  // usage API is what got it revoked — RUSH-1822). No setup-token => null =>
824
847
  // "usage pending". fileOnly additionally forbids the ACL keychain path.
848
+ //
849
+ // allowInteractiveLogin is the one sanctioned exception (USAGE-READ-1/2): a
850
+ // foreground human `agents view` on a `personal` device MAY fall through to
851
+ // the interactive login when no setup-token exists, because that login is the
852
+ // only credential carrying the `user:profile` scope the usage endpoint
853
+ // requires (the setup-token is user:inference → 403, RUSH-2392). It is unset
854
+ // for every background caller, so the RUSH-1822 guarantee is untouched there.
825
855
  const oauth = await loadClaudeOauth(options?.home, {
826
856
  accessTokenCache: true,
827
857
  fileOnly: options?.fileOnly === true,
858
+ allowInteractiveLogin: options?.allowInteractiveLogin === true,
828
859
  });
829
860
  if (!oauth?.accessToken) {
830
861
  // NOT the shared no-credential message: "sign in" is not a remedy here.
@@ -1515,7 +1546,7 @@ export async function loadClaudeOauth(home, opts) {
1515
1546
  // the source of truth if it has actually been revoked.
1516
1547
  return { accessToken: setupToken };
1517
1548
  }
1518
- // No provisioned setup-token: a read-only usage/health probe MUST NOT fall
1549
+ // No provisioned setup-token. A read-only usage/health probe MUST NOT fall
1519
1550
  // through to Claude Code's interactive login credential. The daemon's usage
1520
1551
  // (~60s) and auth-health (~3min) warms would otherwise read the ACL-bound
1521
1552
  // OAuth token and transmit it to api.anthropic.com/api/oauth/usage — an
@@ -1525,7 +1556,17 @@ export async function loadClaudeOauth(home, opts) {
1525
1556
  // (docs/secrets.md). Report unprovisioned (-> probe
1526
1557
  // token 'missing' -> auth-health 'unconfigured', benign for rotation); seed a
1527
1558
  // setup-token via the mint-auth path to restore usage/probe for the account.
1528
- return null;
1559
+ //
1560
+ // The single sanctioned exception (USAGE-READ-1/2): a foreground human
1561
+ // `agents view` on a `personal` device sets allowInteractiveLogin, and only
1562
+ // then do we fall through to the interactive-login read below — the one
1563
+ // credential carrying `user:profile`, which the usage endpoint requires. This
1564
+ // is a human running one command, not an unattended loop, so it is not the
1565
+ // revocation risk RUSH-1822 fixed. Every background caller leaves the flag
1566
+ // unset and still returns null here.
1567
+ if (opts?.allowInteractiveLogin !== true) {
1568
+ return null;
1569
+ }
1529
1570
  }
1530
1571
  // Full-credential callers (isClaudeAuthValid -> getClaudeAccessToken)
1531
1572
  // legitimately read the interactive login to run/refresh Claude. Rush Cloud
@@ -3,7 +3,7 @@ export interface CDPPipeTransport {
3
3
  read: Readable;
4
4
  write: Writable;
5
5
  }
6
- type EventHandler = (params: Record<string, unknown>) => void;
6
+ type EventHandler = (params: Record<string, unknown>, sessionId?: string) => void;
7
7
  export declare function registerPipeTransport(transport: CDPPipeTransport): string;
8
8
  export declare class CDPClient {
9
9
  private ws;
@@ -146,7 +146,7 @@ export class CDPClient {
146
146
  const handlers = this.eventHandlers.get(msg.method);
147
147
  if (handlers) {
148
148
  for (const handler of handlers) {
149
- handler(msg.params || {});
149
+ handler(msg.params || {}, msg.sessionId);
150
150
  }
151
151
  }
152
152
  }