@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
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.
|
|
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).
|
|
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
|
-
.
|
|
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')
|
package/dist/commands/share.d.ts
CHANGED
|
@@ -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 {
|
package/dist/commands/share.js
CHANGED
|
@@ -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 {
|
|
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(
|
|
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
|
|
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,
|
|
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
|
|
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,
|
|
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
|
}
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -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. */
|
package/dist/commands/view.js
CHANGED
|
@@ -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
|
-
], {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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;
|
package/dist/lib/browser/cdp.js
CHANGED