@phnx-labs/agents-cli 1.22.41 → 1.22.43
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 +86 -0
- package/README.md +10 -4
- package/dist/bin/agents +0 -0
- package/dist/bootstrap.js +25 -7
- package/dist/cli/command-registry.d.ts +2 -3
- package/dist/cli/command-registry.js +4 -8
- package/dist/commands/accounts.d.ts +29 -0
- package/dist/commands/accounts.js +220 -29
- package/dist/commands/alias.d.ts +7 -4
- package/dist/commands/alias.js +34 -9
- package/dist/commands/auth.d.ts +9 -0
- package/dist/commands/auth.js +108 -0
- package/dist/commands/browser-sessions-picker.js +42 -62
- package/dist/commands/browser.js +30 -38
- package/dist/commands/computer-actions.d.ts +1 -1
- package/dist/commands/computer-actions.js +1 -1
- package/dist/commands/computer-sessions-picker.js +32 -55
- package/dist/commands/computer.d.ts +1 -1
- package/dist/commands/computer.js +22 -8
- package/dist/commands/daemon.js +1 -1
- package/dist/commands/doctor.d.ts +5 -3
- package/dist/commands/doctor.js +23 -8
- package/dist/commands/exec.js +3 -2
- package/dist/commands/feed.js +2 -31
- package/dist/commands/harness.js +3 -3
- package/dist/commands/hooks.js +1 -1
- package/dist/commands/import.js +1 -1
- package/dist/commands/insights.js +163 -85
- package/dist/commands/inspect.js +2 -2
- package/dist/commands/mine.js +1 -1
- package/dist/commands/monitors.js +1 -1
- package/dist/commands/org.d.ts +6 -0
- package/dist/commands/org.js +175 -0
- package/dist/commands/packages.js +1 -1
- package/dist/commands/profiles.js +2 -2
- package/dist/commands/prune.js +1 -1
- package/dist/commands/routines.js +44 -26
- package/dist/commands/run-account-picker.d.ts +19 -0
- package/dist/commands/run-account-picker.js +81 -0
- package/dist/commands/sessions-picker-factory.d.ts +40 -0
- package/dist/commands/sessions-picker-factory.js +67 -0
- package/dist/commands/sessions-picker.js +124 -122
- package/dist/commands/setup-browser.js +4 -4
- package/dist/commands/setup-mine.js +1 -1
- package/dist/commands/setup-preferences.js +2 -2
- package/dist/commands/setup.js +10 -4
- package/dist/commands/snapshot.d.ts +1 -1
- package/dist/commands/snapshot.js +4 -4
- package/dist/commands/status.d.ts +4 -2
- package/dist/commands/status.js +10 -7
- package/dist/commands/sync.d.ts +1 -0
- package/dist/commands/sync.js +30 -3
- package/dist/commands/versions.js +1 -1
- package/dist/commands/view.js +7 -1
- package/dist/commands/webhook.js +1 -1
- package/dist/lib/account-capabilities.d.ts +10 -0
- package/dist/lib/account-capabilities.js +34 -0
- package/dist/lib/accounting/rotate.d.ts +5 -3
- package/dist/lib/accounting/rotate.js +36 -7
- package/dist/lib/accounting/usage.d.ts +18 -4
- package/dist/lib/accounting/usage.js +138 -27
- package/dist/lib/agent-spec/agents.d.ts +584 -0
- package/dist/lib/agent-spec/agents.js +3127 -0
- package/dist/lib/agent-spec/provider.js +1 -1
- package/dist/lib/agents.d.ts +2 -584
- package/dist/lib/agents.js +2 -3127
- package/dist/lib/browser/drivers/local.d.ts +8 -2
- package/dist/lib/browser/drivers/local.js +8 -2
- package/dist/lib/browser/drivers/ssh.d.ts +8 -2
- package/dist/lib/browser/drivers/ssh.js +10 -4
- package/dist/lib/browser/hygiene.d.ts +1 -1
- package/dist/lib/browser/hygiene.js +5 -1
- package/dist/lib/browser/profiles.d.ts +74 -1
- package/dist/lib/browser/profiles.js +114 -5
- package/dist/lib/browser/runtime-state.d.ts +11 -5
- package/dist/lib/browser/runtime-state.js +13 -9
- package/dist/lib/browser/service.d.ts +68 -30
- package/dist/lib/browser/service.js +305 -194
- package/dist/lib/browser/types.d.ts +95 -3
- package/dist/lib/browser/types.js +51 -0
- package/dist/lib/catchup.js +1 -1
- package/dist/lib/cloud/rush.d.ts +0 -15
- package/dist/lib/cloud/rush.js +0 -35
- package/dist/lib/computer/actions.d.ts +1 -1
- package/dist/lib/computer/actions.js +1 -1
- package/dist/lib/{computer-rpc.js → computer/computer-rpc.js} +5 -5
- package/dist/lib/computer/dispatch.d.ts +1 -1
- package/dist/lib/computer/download.d.ts +1 -1
- package/dist/lib/computer/download.js +3 -3
- package/dist/lib/{ssh-tunnel.d.ts → computer/ssh-tunnel.d.ts} +1 -1
- package/dist/lib/{ssh-tunnel.js → computer/ssh-tunnel.js} +12 -12
- package/dist/lib/crabbox/runtimes.d.ts +6 -2
- package/dist/lib/crabbox/runtimes.js +21 -5
- package/dist/lib/daemon/daemon.js +87 -59
- package/dist/lib/{runner.d.ts → daemon/runner.d.ts} +28 -11
- package/dist/lib/{runner.js → daemon/runner.js} +143 -123
- package/dist/lib/devices/doctor-findings.d.ts +1 -1
- package/dist/lib/devices/doctor-findings.js +4 -4
- package/dist/lib/devices/fleet-inventory.js +1 -1
- package/dist/lib/devices/harness-inventory.js +1 -1
- package/dist/lib/devices/health-report.js +1 -1
- package/dist/lib/devices/registry.js +4 -22
- package/dist/lib/doctor-diff.d.ts +1 -1
- package/dist/lib/doctor-diff.js +1 -1
- package/dist/lib/drift-sync.d.ts +1 -1
- package/dist/lib/drift-sync.js +2 -2
- package/dist/lib/drift.d.ts +1 -1
- package/dist/lib/drift.js +1 -1
- package/dist/lib/entitlement.d.ts +31 -0
- package/dist/lib/entitlement.js +137 -0
- package/dist/lib/exec.js +52 -205
- package/dist/lib/feed/feed.js +2 -8
- package/dist/lib/format.d.ts +9 -0
- package/dist/lib/format.js +16 -0
- package/dist/lib/fs-atomic.d.ts +7 -0
- package/dist/lib/fs-atomic.js +9 -0
- package/dist/lib/github/pr-mergeable.d.ts +49 -0
- package/dist/lib/github/pr-mergeable.js +157 -0
- package/dist/lib/github/pr-verdict.d.ts +67 -0
- package/dist/lib/github/pr-verdict.js +72 -0
- package/dist/lib/harness/adapter.d.ts +161 -0
- package/dist/lib/harness/adapter.js +47 -0
- package/dist/lib/harness/adapters/claude.d.ts +2 -0
- package/dist/lib/harness/adapters/claude.js +110 -0
- package/dist/lib/harness/adapters/codex.d.ts +2 -0
- package/dist/lib/harness/adapters/codex.js +83 -0
- package/dist/lib/harness/adapters/copilot.d.ts +2 -0
- package/dist/lib/harness/adapters/copilot.js +23 -0
- package/dist/lib/harness/adapters/cursor.d.ts +2 -0
- package/dist/lib/harness/adapters/cursor.js +38 -0
- package/dist/lib/harness/adapters/droid.d.ts +2 -0
- package/dist/lib/harness/adapters/droid.js +17 -0
- package/dist/lib/harness/adapters/grok.d.ts +2 -0
- package/dist/lib/harness/adapters/grok.js +14 -0
- package/dist/lib/harness/adapters/kimi.d.ts +2 -0
- package/dist/lib/harness/adapters/kimi.js +49 -0
- package/dist/lib/harness/adapters/muse.d.ts +2 -0
- package/dist/lib/harness/adapters/muse.js +44 -0
- package/dist/lib/harness/adapters/opencode.d.ts +2 -0
- package/dist/lib/harness/adapters/opencode.js +14 -0
- package/dist/lib/harness/exec-config-version.d.ts +8 -0
- package/dist/lib/harness/exec-config-version.js +22 -0
- package/dist/lib/harness/index.d.ts +1 -0
- package/dist/lib/harness/index.js +34 -0
- package/dist/lib/{hooks.d.ts → hooks/install.d.ts} +1 -1
- package/dist/lib/{hooks.js → hooks/install.js} +11 -11
- package/dist/lib/hosts/passthrough.d.ts +10 -0
- package/dist/lib/hosts/passthrough.js +28 -6
- package/dist/lib/hosts/session-index.js +1 -1
- package/dist/lib/import.js +1 -1
- package/dist/lib/installations/index.d.ts +4 -4
- package/dist/lib/installations/index.js +3 -4
- package/dist/lib/installations/migrate.js +6 -6
- package/dist/lib/{shims.d.ts → installations/shims.d.ts} +2 -2
- package/dist/lib/{shims.js → installations/shims.js} +13 -115
- package/dist/lib/installations/store.d.ts +141 -3
- package/dist/lib/installations/store.js +475 -5
- package/dist/lib/installations/versions.d.ts +40 -125
- package/dist/lib/installations/versions.js +57 -462
- package/dist/lib/isolation-boundary-report.d.ts +1 -1
- package/dist/lib/isolation-boundary-report.js +1 -1
- package/dist/lib/mailbox-gc.js +2 -3
- package/dist/lib/menubar/MenubarHelper.app/Contents/CodeResources +0 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/menubar/install-menubar.js +19 -8
- package/dist/lib/monitors/config.d.ts +2 -2
- package/dist/lib/monitors/config.js +3 -2
- package/dist/lib/monitors/dispatch.d.ts +1 -1
- package/dist/lib/monitors/dispatch.js +2 -2
- package/dist/lib/monitors/state.d.ts +1 -1
- package/dist/lib/origin-machine.d.ts +18 -0
- package/dist/lib/origin-machine.js +34 -0
- package/dist/lib/perf/db.js +1 -1
- package/dist/lib/perf/spool.js +1 -1
- package/dist/lib/plugins/plugins.js +1 -1
- package/dist/lib/plugins/skills.js +1 -1
- package/dist/lib/prix-account.d.ts +158 -0
- package/dist/lib/prix-account.js +214 -0
- package/dist/lib/profiles.d.ts +7 -0
- package/dist/lib/profiles.js +26 -1
- package/dist/lib/project-key.js +2 -2
- package/dist/lib/project-launch.js +1 -1
- package/dist/lib/refresh.js +2 -2
- package/dist/lib/resource-inventory.d.ts +1 -1
- package/dist/lib/resource-inventory.js +1 -1
- package/dist/lib/rules/rules.js +1 -1
- package/dist/lib/sandbox.d.ts +24 -0
- package/dist/lib/sandbox.js +101 -0
- package/dist/lib/scheduling/routines.d.ts +18 -9
- package/dist/lib/scheduling/routines.js +17 -9
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.js +9 -1
- package/dist/lib/secrets/index.js +1 -1
- package/dist/lib/secrets/list-filter.js +1 -1
- package/dist/lib/self-heal/checks/hook-runtime.js +1 -1
- package/dist/lib/self-heal/checks/path.js +1 -1
- package/dist/lib/self-heal/checks/shadowing.js +1 -1
- package/dist/lib/self-heal/checks/shims.js +1 -1
- package/dist/lib/service-manifest.d.ts +20 -0
- package/dist/lib/service-manifest.js +34 -0
- package/dist/lib/session/cloud.js +1 -1
- package/dist/lib/session/db.js +1 -1
- package/dist/lib/session/discover.d.ts +39 -0
- package/dist/lib/session/discover.js +3 -3
- package/dist/lib/session/fork.js +1 -1
- package/dist/lib/session/live-metadata.js +1 -1
- package/dist/lib/session/origin-machine.d.ts +1 -18
- package/dist/lib/session/origin-machine.js +4 -34
- package/dist/lib/session/parse.js +24 -42
- package/dist/lib/session/recovery.js +1 -1
- package/dist/lib/session/relative-time.d.ts +1 -44
- package/dist/lib/session/relative-time.js +4 -120
- package/dist/lib/session/remote-list.js +1 -1
- package/dist/lib/session/remote.js +2 -2
- package/dist/lib/session/short-id.d.ts +1 -17
- package/dist/lib/session/short-id.js +4 -20
- package/dist/lib/session/width.d.ts +1 -29
- package/dist/lib/session/width.js +4 -101
- package/dist/lib/snapshot.d.ts +2 -2
- package/dist/lib/snapshot.js +1 -1
- package/dist/lib/staleness/prune.d.ts +1 -1
- package/dist/lib/staleness/prune.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +1 -1
- package/dist/lib/startup/command-registry.d.ts +4 -1
- package/dist/lib/startup/command-registry.js +12 -5
- package/dist/lib/teams/agents.js +2 -19
- package/dist/lib/teams/parsers.js +20 -26
- package/dist/lib/teams/placement-probe.js +1 -1
- package/dist/lib/teams/registry.js +2 -21
- package/dist/lib/teams/scheduler.js +1 -1
- package/dist/lib/text/relative-time.d.ts +44 -0
- package/dist/lib/text/relative-time.js +120 -0
- package/dist/lib/text/short-id.d.ts +17 -0
- package/dist/lib/text/short-id.js +20 -0
- package/dist/lib/text/width.d.ts +29 -0
- package/dist/lib/text/width.js +101 -0
- package/dist/lib/triggers/handlers.d.ts +7 -0
- package/dist/lib/triggers/handlers.js +9 -1
- package/dist/lib/triggers/webhook.d.ts +1 -0
- package/dist/lib/triggers/webhook.js +3 -2
- package/dist/lib/uninstall.js +1 -1
- package/dist/lib/version.d.ts +9 -2
- package/dist/lib/version.js +11 -9
- package/dist/lib/view-types.d.ts +4 -0
- package/package.json +1 -1
- package/dist/commands/tickets.d.ts +0 -2
- package/dist/commands/tickets.js +0 -43
- package/dist/lib/observe-aliases.d.ts +0 -30
- package/dist/lib/observe-aliases.js +0 -38
- package/dist/lib/tickets/list.d.ts +0 -53
- package/dist/lib/tickets/list.js +0 -153
- /package/dist/lib/{computer-rpc.d.ts → computer/computer-rpc.d.ts} +0 -0
package/dist/commands/sync.js
CHANGED
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
*
|
|
4
4
|
* Forms:
|
|
5
5
|
* agents sync # umbrella: fetch config repos -> reconcile all (secrets opt-in)
|
|
6
|
+
* agents sync status # report fleet drift (former top-level `agents status`)
|
|
6
7
|
* agents sync --repos|--secrets # umbrella: fetch only those, then reconcile
|
|
7
8
|
* agents sync --cloud # umbrella: fetch all, skip reconcile
|
|
8
9
|
* agents sync --local # umbrella: reconcile all, no fetch
|
|
@@ -34,9 +35,9 @@ import { Option } from 'commander';
|
|
|
34
35
|
import chalk from 'chalk';
|
|
35
36
|
import { resolveSyncPassphraseFromEnv } from '../lib/secrets/sync-passphrase.js';
|
|
36
37
|
import { agentLabel, resolveAgentName, MANAGED_AGENT_IDS, isAgentHardDeprecated, hardDeprecationError } from '../lib/agents.js';
|
|
37
|
-
import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, buildSelection, mergeRepoScopedSelections, listRepoNames, getVersionHomePath, } from '../lib/installations/versions.js';
|
|
38
|
+
import { isVersionInstalled, syncResourcesToVersion, parseAgentSpec, resolveVersion, resolveVersionAlias, listInstalledVersions, healDanglingVersionPointers, getAvailableResources, getActuallySyncedResources, getProjectOnlyResources, getNewResources, hasNewResources, promptResourceSelection, promptNewResourceSelection, buildSelection, mergeRepoScopedSelections, listRepoNames, getVersionHomePath, } from '../lib/installations/versions.js';
|
|
38
39
|
import { capableAgents } from '../lib/capabilities.js';
|
|
39
|
-
import { parseHookManifest, registerHooksToSettings } from '../lib/hooks.js';
|
|
40
|
+
import { parseHookManifest, registerHooksToSettings } from '../lib/hooks/install.js';
|
|
40
41
|
import { compileRulesForProject } from '../lib/rules/compile.js';
|
|
41
42
|
import { runLaunchSync } from '../lib/project-launch.js';
|
|
42
43
|
import { formatKeptProjectResources } from '../lib/project-resources.js';
|
|
@@ -45,6 +46,7 @@ import { runUmbrellaSync } from '../lib/sync-umbrella.js';
|
|
|
45
46
|
import { addHostOption } from '../lib/hosts/option.js';
|
|
46
47
|
import { syncRepoGit } from '../lib/git.js';
|
|
47
48
|
import { getSystemAgentsDir, getUserAgentsDir, getEnabledExtraRepos } from '../lib/state.js';
|
|
49
|
+
import { registerStatusCommand } from './status.js';
|
|
48
50
|
/** Emit one JSON object to stdout for `--json` callers / fleet fan-out. */
|
|
49
51
|
function emitJson(payload) {
|
|
50
52
|
console.log(JSON.stringify(payload));
|
|
@@ -134,7 +136,7 @@ export function addSelectorOptions(cmd) {
|
|
|
134
136
|
export function registerSyncCommand(program) {
|
|
135
137
|
const cmd = addHostOption(program.command('sync [agentSpec] [repo]'))
|
|
136
138
|
.summary('Make this machine current, or sync resources into one agent')
|
|
137
|
-
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", a selector: @latest / @oldest / @pinned (= @default), or @all for every installed version.\n\nAppend a [repo] (or pass --repo) to scope the sync to a single DotAgent repo — system / user / project / <alias>. e.g. "agents sync claude@all system" reconciles only the system repo\'s resources into every installed Claude.\n\nGive a DotAgent repo name ALONE — "agents sync system" / "agents sync user" / "agents sync <alias>" — to git-sync that one repo: git pull --rebase against origin when the tree is clean; when it is dirty, fast-forward anyway if no incoming path is uncommitted, else refuse and name what collided. The user repo and extra aliases also push local commits up; the system repo is a pull-only mirror.\n\nWith NO agent, runs the umbrella verb: fetch the config repos then reconcile them into every installed agent. Secrets are opt-in — add --secrets to pull secret bundles. Session transcripts are queryable live via "agents sessions --device <machine>", or moved with "agents sessions export/import". Also: --cloud (fetch only), --local (reconcile only)
|
|
139
|
+
.description('With an [agentSpec], syncs resources (commands, skills, hooks, rules, MCPs, plugins, etc.) into that installed agent version — previews changes and lets you pick. e.g. "claude", "claude@2.1.142", a selector: @latest / @oldest / @pinned (= @default), or @all for every installed version.\n\nAppend a [repo] (or pass --repo) to scope the sync to a single DotAgent repo — system / user / project / <alias>. e.g. "agents sync claude@all system" reconciles only the system repo\'s resources into every installed Claude.\n\nGive a DotAgent repo name ALONE — "agents sync system" / "agents sync user" / "agents sync <alias>" — to git-sync that one repo: git pull --rebase against origin when the tree is clean; when it is dirty, fast-forward anyway if no incoming path is uncommitted, else refuse and name what collided. The user repo and extra aliases also push local commits up; the system repo is a pull-only mirror.\n\nWith NO agent, runs the umbrella verb: fetch the config repos then reconcile them into every installed agent. Secrets are opt-in — add --secrets to pull secret bundles. Session transcripts are queryable live via "agents sessions --device <machine>", or moved with "agents sessions export/import". Also: --cloud (fetch only), --local (reconcile only).\n\n`agents sync status` reports fleet drift (what is drifted, missing, or behind) and can reconcile it — the former top-level `agents status`.')
|
|
138
140
|
.option('--agent <agent>', 'Agent identifier (legacy form; prefer the positional spec)')
|
|
139
141
|
.option('--agent-version <version>', 'Version to sync into (legacy form; prefer "agent@version" or --version)')
|
|
140
142
|
.option('--repo <name>', 'Scope the sync to a single DotAgent repo: system / user / project / <alias> (also accepted as a positional)')
|
|
@@ -158,6 +160,8 @@ export function registerSyncCommand(program) {
|
|
|
158
160
|
// Per-kind resource selectors + --version. Registered after the main flags so
|
|
159
161
|
// help output groups the positional/repo/agent flags first.
|
|
160
162
|
addSelectorOptions(cmd);
|
|
163
|
+
// `status` is a reserved subcommand (not an agentSpec/repo positional).
|
|
164
|
+
registerStatusCommand(cmd);
|
|
161
165
|
}
|
|
162
166
|
/**
|
|
163
167
|
* Resolve a DotAgent repo name to its git working directory + whether local
|
|
@@ -507,6 +511,27 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
507
511
|
const projectDir = opts.projectDir;
|
|
508
512
|
const cwd = opts.cwd || process.cwd();
|
|
509
513
|
const force = !!opts.force;
|
|
514
|
+
// RUSH-2471: self-heal any version pointer (global/isolated default, ~/.<agent>
|
|
515
|
+
// symlink) left aimed at a version that is no longer installed BEFORE resolving
|
|
516
|
+
// the version to sync. Skipped on --dry-run since it mutates on-disk pointers.
|
|
517
|
+
let healed = {};
|
|
518
|
+
if (!opts.dryRun) {
|
|
519
|
+
healed = await healDanglingVersionPointers(agentId, cwd);
|
|
520
|
+
if (!quiet && !json) {
|
|
521
|
+
if (healed.globalDefault) {
|
|
522
|
+
const to = healed.globalDefault.to ? `@${healed.globalDefault.to}` : 'none';
|
|
523
|
+
outLog(chalk.yellow(`Reassigned ${agentLabel(agentId)} default from @${healed.globalDefault.from} (not installed) to ${to}.`));
|
|
524
|
+
}
|
|
525
|
+
if (healed.isolatedDefault) {
|
|
526
|
+
const to = healed.isolatedDefault.to ? `@${healed.isolatedDefault.to}` : 'none';
|
|
527
|
+
outLog(chalk.yellow(`Reassigned ${agentLabel(agentId)} isolated default from @${healed.isolatedDefault.from} (not installed) to ${to}.`));
|
|
528
|
+
}
|
|
529
|
+
if (healed.configSymlink) {
|
|
530
|
+
outLog(chalk.yellow(`Repointed ${agentLabel(agentId)} config symlink from @${healed.configSymlink.from} (not installed) to @${healed.configSymlink.to}.`));
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
const healedPointers = Object.keys(healed).length > 0 ? { healedPointers: healed } : {};
|
|
510
535
|
// Promote to @all when no version can be resolved and multiple are installed.
|
|
511
536
|
// Replaces the old "no default version pinned" error: bare `agents sync claude`
|
|
512
537
|
// with multiple installed versions and no pinned default now syncs them all.
|
|
@@ -582,6 +607,7 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
582
607
|
mode: 'agent-all',
|
|
583
608
|
agent: agentId,
|
|
584
609
|
repo: repoScope,
|
|
610
|
+
...healedPointers,
|
|
585
611
|
versions: versions.map(({ version: v, result }) => ({
|
|
586
612
|
version: v,
|
|
587
613
|
commands: !!result.commands,
|
|
@@ -786,6 +812,7 @@ async function runSync(agentSpec, repoArg, opts) {
|
|
|
786
812
|
if (json) {
|
|
787
813
|
emitJson({
|
|
788
814
|
...agentSyncJson(agentId, version, result),
|
|
815
|
+
...healedPointers,
|
|
789
816
|
projectCompile: projectCompile
|
|
790
817
|
? {
|
|
791
818
|
compiled: !!projectCompile.compiled,
|
|
@@ -11,7 +11,7 @@ import { resolveConfiguredModel, formatAgentIdentity } from '../lib/models.js';
|
|
|
11
11
|
import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
|
|
12
12
|
import { installVersion, removeVersion, listInstalledVersions, isVersionInstalled, isLatestInstalled, isOldestInstalled, getGlobalDefault, setGlobalDefault, markVersionIsolated, setIsolatedDefault, isVersionIsolated, getVersionHomePath, getVersionDir, syncResourcesToVersion, parseAgentSpec, promptResourceSelection, promptNewResourceSelection, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, printTrashFooter, } from '../lib/installations/versions.js';
|
|
13
13
|
import { carryForwardSettings } from '../lib/settings-manifest.js';
|
|
14
|
-
import { createShim, createVersionedAlias, supportsIsolatedInstall, isIsolationProtected, CONFIG_ENV_ISOLATED_AGENTS, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/shims.js';
|
|
14
|
+
import { createShim, createVersionedAlias, supportsIsolatedInstall, isIsolationProtected, CONFIG_ENV_ISOLATED_AGENTS, removeShim, shimExists, getShimsDir, getShimPath, getPathShadowingExecutable, isShimsInPath, getPathSetupInstructions, addShimsToPath, switchConfigSymlink, switchHomeFileSymlinks, } from '../lib/installations/shims.js';
|
|
15
15
|
import { isInteractiveTerminal, isPromptCancelled, requireInteractiveSelection } from './utils.js';
|
|
16
16
|
import { tryAutoPull } from '../lib/git.js';
|
|
17
17
|
import { getAgentsDir, getTrashVersionsDir } from '../lib/state.js';
|
package/dist/commands/view.js
CHANGED
|
@@ -11,7 +11,7 @@ import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth
|
|
|
11
11
|
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, isUsageHeadlessScopeError, } from '../lib/accounting/usage.js';
|
|
12
12
|
import { readManifest } from '../lib/manifest.js';
|
|
13
13
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, isVersionIsolated, getIsolatedDefault, } from '../lib/installations/versions.js';
|
|
14
|
-
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/shims.js';
|
|
14
|
+
import { ensureVersionedAliasCurrent, removeShim, } from '../lib/installations/shims.js';
|
|
15
15
|
import { getAgentResources } from '../lib/resources.js';
|
|
16
16
|
import { renderMergedResources } from '../lib/merged-resources.js';
|
|
17
17
|
import { resolveVersionFilter, AgentSpecError } from '../lib/agent-spec/index.js';
|
|
@@ -1308,6 +1308,12 @@ export async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
|
1308
1308
|
resetsAt: w.resetsAt ? w.resetsAt.toISOString() : null,
|
|
1309
1309
|
}))
|
|
1310
1310
|
: [],
|
|
1311
|
+
unavailable: snapshot?.unavailable
|
|
1312
|
+
? {
|
|
1313
|
+
reason: snapshot.unavailable.reason,
|
|
1314
|
+
resetsAt: snapshot.unavailable.resetsAt.toISOString(),
|
|
1315
|
+
}
|
|
1316
|
+
: undefined,
|
|
1311
1317
|
lastActive: info.lastActive ? info.lastActive.toISOString() : null,
|
|
1312
1318
|
path: getVersionDir(agentId, version),
|
|
1313
1319
|
configuredModel: resolveConfiguredModel(agentId, version),
|
package/dist/commands/webhook.js
CHANGED
|
@@ -85,7 +85,7 @@ export function registerWebhooksCommand(program) {
|
|
|
85
85
|
const address = server.address();
|
|
86
86
|
const bound = typeof address === 'object' && address ? address.port : port;
|
|
87
87
|
console.log(`${chalk.green('agents webhooks')} ${chalk.dim('→')} ${chalk.cyan(`http://${opts.bind ?? DEFAULT_HOST}:${bound}`)}`);
|
|
88
|
-
console.log(chalk.dim('signed · localhost by default · endpoints: /hooks/github, /hooks/linear · acks 202 then dispatches · Ctrl-C to stop'));
|
|
88
|
+
console.log(chalk.dim('signed · localhost by default · endpoints: /hooks/github, /hooks/linear, /hooks/slack · acks 202 then dispatches · Ctrl-C to stop'));
|
|
89
89
|
console.log(chalk.dim('for a supervised receiver that survives reboot: agents daemon webhooks add --secrets-bundle <name>'));
|
|
90
90
|
const shutdown = () => {
|
|
91
91
|
server.close(() => process.exit(0));
|
|
@@ -24,6 +24,16 @@ export declare const NATIVE_ACCOUNT_CAPABILITIES: Record<AgentId, NativeAccountC
|
|
|
24
24
|
export declare function nativeAccountCapability(agent: AgentId): NativeAccountCapability;
|
|
25
25
|
/** Whether a harness's native login can be named/attached (supported or conditional). */
|
|
26
26
|
export declare function nativeAccountNameable(agent: AgentId): boolean;
|
|
27
|
+
/** Harnesses whose native login is fully nameable/attachable today. */
|
|
28
|
+
export declare function supportedNativeHarnesses(): AgentId[];
|
|
29
|
+
/**
|
|
30
|
+
* Named reason to refuse native name/attach, or null when the harness is
|
|
31
|
+
* nameable. Provider-credential `accounts add` is a different path and MUST
|
|
32
|
+
* not consult this — that surface is supported for every registered provider.
|
|
33
|
+
*/
|
|
34
|
+
export declare function nativeAccountNamingRefusal(agent: AgentId): string | null;
|
|
35
|
+
/** Fail loud on the native name/attach path for an unsupported harness. */
|
|
36
|
+
export declare function assertNativeAccountNameable(agent: AgentId): void;
|
|
27
37
|
/**
|
|
28
38
|
* The single identity key used at `accounts name`, attach, exec validation,
|
|
29
39
|
* view, and inventory. Prefer the harness's stable `accountKey`. For an
|
|
@@ -42,6 +42,40 @@ export function nativeAccountNameable(agent) {
|
|
|
42
42
|
const cap = NATIVE_ACCOUNT_CAPABILITIES[agent];
|
|
43
43
|
return (cap.status === 'supported' || cap.status === 'conditional') && cap.scope !== 'unsupported';
|
|
44
44
|
}
|
|
45
|
+
/** Harnesses whose native login is fully nameable/attachable today. */
|
|
46
|
+
export function supportedNativeHarnesses() {
|
|
47
|
+
return Object.entries(NATIVE_ACCOUNT_CAPABILITIES)
|
|
48
|
+
.filter(([, cap]) => cap.status === 'supported')
|
|
49
|
+
.map(([id]) => id)
|
|
50
|
+
.sort();
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Named reason to refuse native name/attach, or null when the harness is
|
|
54
|
+
* nameable. Provider-credential `accounts add` is a different path and MUST
|
|
55
|
+
* not consult this — that surface is supported for every registered provider.
|
|
56
|
+
*/
|
|
57
|
+
export function nativeAccountNamingRefusal(agent) {
|
|
58
|
+
if (nativeAccountNameable(agent))
|
|
59
|
+
return null;
|
|
60
|
+
const cap = NATIVE_ACCOUNT_CAPABILITIES[agent];
|
|
61
|
+
const suffix = `Supported today: ${supportedNativeHarnesses().join(', ')}.`;
|
|
62
|
+
if (cap.scope === 'device') {
|
|
63
|
+
return `${agent} accounts can't be isolated by agents-cli yet (device-scoped login). ${suffix}`;
|
|
64
|
+
}
|
|
65
|
+
if (agent === 'cursor') {
|
|
66
|
+
return `${agent} accounts can't be isolated by agents-cli yet (multi-account isolation unresolved). ${suffix}`;
|
|
67
|
+
}
|
|
68
|
+
if (cap.status === 'discovery-only') {
|
|
69
|
+
return `${agent} native accounts are discovery-only; agents-cli cannot name or attach this login. ${suffix}`;
|
|
70
|
+
}
|
|
71
|
+
return `${agent} accounts can't be named by agents-cli yet. ${suffix}`;
|
|
72
|
+
}
|
|
73
|
+
/** Fail loud on the native name/attach path for an unsupported harness. */
|
|
74
|
+
export function assertNativeAccountNameable(agent) {
|
|
75
|
+
const reason = nativeAccountNamingRefusal(agent);
|
|
76
|
+
if (reason)
|
|
77
|
+
throw new Error(reason);
|
|
78
|
+
}
|
|
45
79
|
/**
|
|
46
80
|
* The single identity key used at `accounts name`, attach, exec validation,
|
|
47
81
|
* view, and inventory. Prefer the harness's stable `accountKey`. For an
|
|
@@ -58,9 +58,11 @@ export interface RotateResult {
|
|
|
58
58
|
/** Candidates excluded (not signed in, or out of credits). */
|
|
59
59
|
excluded: RotateCandidate[];
|
|
60
60
|
/**
|
|
61
|
-
* True when
|
|
62
|
-
*
|
|
63
|
-
*
|
|
61
|
+
* True when the PICKED candidate's usage could not be verified fresh, so the
|
|
62
|
+
* route was decided on an unverified snapshot. Callers surface it — routing
|
|
63
|
+
* blind is a fact the operator needs, not an internal detail. (Previously
|
|
64
|
+
* this only reported the all-stale case; a stale pick out of a mixed pool
|
|
65
|
+
* was silently reported as verified.)
|
|
64
66
|
*/
|
|
65
67
|
usageUnverified?: boolean;
|
|
66
68
|
}
|
|
@@ -122,7 +122,7 @@ export function isUsageVerified(candidate, nowMs = Date.now()) {
|
|
|
122
122
|
}
|
|
123
123
|
function hasUsageAvailable(candidate) {
|
|
124
124
|
const snapshot = candidate.usageSnapshot;
|
|
125
|
-
if (snapshot
|
|
125
|
+
if (snapshot) {
|
|
126
126
|
// Eligibility mirrors the `agents view` throttle badge exactly
|
|
127
127
|
// (deriveUsageStatusFromSnapshot): an account maxed on ANY blocking window —
|
|
128
128
|
// including the 5-hour session window — cannot serve the next request, so it
|
|
@@ -131,7 +131,9 @@ function hasUsageAvailable(candidate) {
|
|
|
131
131
|
// stayed "eligible" and the router kept launching into it while `ag view`
|
|
132
132
|
// showed it rate-limited. Capacity *weighting* still ranks eligible accounts
|
|
133
133
|
// by weekly headroom; this gate only decides can-it-run-right-now.
|
|
134
|
-
|
|
134
|
+
const status = deriveUsageStatusFromSnapshot(snapshot);
|
|
135
|
+
if (status !== null)
|
|
136
|
+
return status !== 'rate_limited';
|
|
135
137
|
}
|
|
136
138
|
// No live snapshot: fall back to the coarse cached status.
|
|
137
139
|
if (candidate.usageStatus === 'out_of_credits' || candidate.usageStatus === 'rate_limited') {
|
|
@@ -300,11 +302,12 @@ export function pickBalancedCandidate(candidates, nowMs = Date.now()) {
|
|
|
300
302
|
if (!deduped.has(c))
|
|
301
303
|
excluded.push(c);
|
|
302
304
|
}
|
|
303
|
-
const { picked, usageUnverified } = preferVerified(sorted, nowMs, weightedRandomByCapacity);
|
|
305
|
+
const { picked, usageUnverified } = preferVerified(sorted, nowMs, weightedRandomByCapacity, 'representative');
|
|
304
306
|
return { picked, healthy: sorted, excluded, usageUnverified };
|
|
305
307
|
}
|
|
306
308
|
/**
|
|
307
|
-
* Choose from the VERIFIED candidates when
|
|
309
|
+
* Choose from the VERIFIED candidates when they are a representative share of
|
|
310
|
+
* the pool (at least half), else from the whole pool.
|
|
308
311
|
*
|
|
309
312
|
* An eligible account whose usage we could not confirm is a guess, not a green
|
|
310
313
|
* light: the snapshot reads "48% used" with equal confidence whether it was
|
|
@@ -312,6 +315,29 @@ export function pickBalancedCandidate(candidates, nowMs = Date.now()) {
|
|
|
312
315
|
* wrong indefinitely. Confirmed headroom therefore beats apparent headroom, even
|
|
313
316
|
* when the unconfirmed number looks better.
|
|
314
317
|
*
|
|
318
|
+
* But narrowing to a verified MINORITY inverts the safety. When the usage
|
|
319
|
+
* endpoint 429-throttles a machine, each refresh cycle confirms roughly one
|
|
320
|
+
* account before backing off — so "verified" is a singleton, `choose` runs over
|
|
321
|
+
* a one-element list, and every launch lands on the same account. Launching
|
|
322
|
+
* into it refreshes that account's snapshot again, so it stays the only
|
|
323
|
+
* verified candidate while the rest are never picked and never probed: the
|
|
324
|
+
* rotation degrades to a fixed pin that burns one account to its weekly cap
|
|
325
|
+
* while its siblings idle (observed 2026-08-20 across yosemite-s0/s1 — the
|
|
326
|
+
* "same version every time under --strategy balanced" incident).
|
|
327
|
+
*
|
|
328
|
+
* The two failure modes belong to different CHOOSERS, so `narrowing` is picked
|
|
329
|
+
* per caller: `'representative'` (the weighted-random balanced path) narrows to
|
|
330
|
+
* verified only when they cover at least half the pool — below that the whole
|
|
331
|
+
* pool competes, fresh candidates keep their confirmed weights, stale/unknown
|
|
332
|
+
* ones get the full default weight `capacityWeight` assigns to "no signal", and
|
|
333
|
+
* the random roll spreads the load. `'any-verified'` (deterministic `from[0]`
|
|
334
|
+
* choosers: `--strategy available`, run-auto harness classification) keeps the
|
|
335
|
+
* original rule — narrow whenever ANY verified candidate exists — because a
|
|
336
|
+
* deterministic pick over the whole pool would hand the front slot back to an
|
|
337
|
+
* unconfirmed "48% used" over an accurate "90% used", the exact yosemite-s1
|
|
338
|
+
* inversion above, and a deterministic chooser cannot spread load anyway, so
|
|
339
|
+
* the singleton-collapse concern does not apply to it.
|
|
340
|
+
*
|
|
315
341
|
* `healthy` deliberately keeps every eligible candidate rather than just the
|
|
316
342
|
* verified ones. Declining to *pick* an account on stale data and declining to
|
|
317
343
|
* *fail over to* it after the primary has already hit a 429 are different risks:
|
|
@@ -319,11 +345,14 @@ export function pickBalancedCandidate(candidates, nowMs = Date.now()) {
|
|
|
319
345
|
* (rotationFailoverChain, which reads `healthy`) keeps its full safety net —
|
|
320
346
|
* exactly on the machines this guard is protecting.
|
|
321
347
|
*/
|
|
322
|
-
function preferVerified(pool, nowMs, choose) {
|
|
348
|
+
function preferVerified(pool, nowMs, choose, narrowing = 'any-verified') {
|
|
323
349
|
const verified = pool.filter((c) => isUsageVerified(c, nowMs));
|
|
350
|
+
const narrow = verified.length > 0 &&
|
|
351
|
+
(narrowing === 'any-verified' || verified.length >= Math.ceil(pool.length / 2));
|
|
352
|
+
const picked = choose(narrow ? verified : pool);
|
|
324
353
|
return {
|
|
325
|
-
picked
|
|
326
|
-
usageUnverified:
|
|
354
|
+
picked,
|
|
355
|
+
usageUnverified: !isUsageVerified(picked, nowMs),
|
|
327
356
|
};
|
|
328
357
|
}
|
|
329
358
|
/**
|
|
@@ -91,6 +91,11 @@ export interface UsageSnapshot {
|
|
|
91
91
|
capturedAt: Date | null;
|
|
92
92
|
windows: UsageWindow[];
|
|
93
93
|
plan?: string | null;
|
|
94
|
+
/** A refusal observed from a real harness run, independent of API windows. */
|
|
95
|
+
unavailable?: {
|
|
96
|
+
reason: 'session_limit';
|
|
97
|
+
resetsAt: Date;
|
|
98
|
+
};
|
|
94
99
|
}
|
|
95
100
|
/** Usage data plus any error encountered while fetching. */
|
|
96
101
|
export interface UsageInfo {
|
|
@@ -464,7 +469,6 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
|
|
|
464
469
|
* the Claude CLI stores its OAuth token in this plaintext file instead. The
|
|
465
470
|
* keychain read above finds nothing on that platform, so we fall back to the
|
|
466
471
|
* file. Same wrapped `{ claudeAiOauth }` shape, so one parser handles both.
|
|
467
|
-
* Mirrors `readClaudeCredentialsBlob` (cloud/rush.ts), the proven pattern.
|
|
468
472
|
*
|
|
469
473
|
* Without step 2 the live usage fetch got no token on Linux, so `agents view`
|
|
470
474
|
* (run remotely over SSH by `--device`) rendered no usage bars even though the
|
|
@@ -476,9 +480,10 @@ export declare function normalizeDroidWindows(data: DroidBillingLimitsResponse):
|
|
|
476
480
|
* reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
|
|
477
481
|
* to Anthropic's API is what gets it revoked; see the branch body and
|
|
478
482
|
* docs/credential-management.md). It is OFF by default so full-credential
|
|
479
|
-
* callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`)
|
|
480
|
-
* the
|
|
481
|
-
* the
|
|
483
|
+
* callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) still
|
|
484
|
+
* read the interactive login. Rush Cloud dispatch does not call this helper
|
|
485
|
+
* at all (SING-1b: the account manifest is email-only; RUSH-2359 removed the
|
|
486
|
+
* leftover blob reader that used to send the interactive login).
|
|
482
487
|
*
|
|
483
488
|
* `opts.fileOnly` skips the ACL keychain read entirely — setup-token and
|
|
484
489
|
* `.credentials.json` only. Used by the daemon usage refresher so a background
|
|
@@ -506,8 +511,17 @@ export declare function getClaudeKeychainService(home?: string): string;
|
|
|
506
511
|
export declare function isClaudeUsageOrgMatch(requestedOrgId: string | null | undefined, liveOrgId: string | null | undefined): boolean;
|
|
507
512
|
/** Read a cached usage snapshot for a given usage key. Returns null if absent or stale. */
|
|
508
513
|
export declare function readClaudeUsageCache(usageKey: string, cachePath?: string, now?: Date): UsageSnapshot | null;
|
|
514
|
+
/** Delete an expired cache row only if it is still expired under the write lock. */
|
|
515
|
+
export declare function pruneExpiredClaudeUsageCacheEntry(usageKey: string, cachePath?: string, now?: Date): void;
|
|
509
516
|
/** Write a usage snapshot to the on-disk cache. */
|
|
510
517
|
export declare function writeClaudeUsageCache(usageKey: string, snapshot: UsageSnapshot, cachePath?: string): void;
|
|
518
|
+
/**
|
|
519
|
+
* Persist a Claude session-limit refusal from a real run until its stated reset.
|
|
520
|
+
* This quota is not part of Anthropic's five-hour/weekly usage response.
|
|
521
|
+
*/
|
|
522
|
+
export declare function noteClaudeSessionLimit(usageKey: string, resetsAt: Date, cachePath?: string): void;
|
|
523
|
+
/** Parse Claude's `hit your session limit · resets …` refusal. */
|
|
524
|
+
export declare function parseClaudeSessionLimitReset(text: string, nowMs?: number): Date | null;
|
|
511
525
|
/**
|
|
512
526
|
* Check whether the Claude OAuth credentials for a given home are usable.
|
|
513
527
|
* Attempts a token refresh if the access token is expired.
|
|
@@ -398,6 +398,9 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
398
398
|
parts.push(chalk.gray(plan.padEnd(planWidth)));
|
|
399
399
|
}
|
|
400
400
|
if (snapshot) {
|
|
401
|
+
if (snapshot.unavailable?.reason === 'session_limit') {
|
|
402
|
+
parts.push(chalk.yellow(`session-limited (${formatResetHint(snapshot.unavailable.resetsAt)})`));
|
|
403
|
+
}
|
|
401
404
|
// Compact rows show BLOCKING windows — the same set
|
|
402
405
|
// deriveUsageStatusFromSnapshot uses for the rate-limited badge — so an
|
|
403
406
|
// account throttled by its month window (Droid meters on 5h/week/month)
|
|
@@ -467,7 +470,12 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
467
470
|
* one model, not the account, so it shouldn't flip the whole row to throttled.
|
|
468
471
|
*/
|
|
469
472
|
export function deriveUsageStatusFromSnapshot(snapshot) {
|
|
470
|
-
if (!snapshot
|
|
473
|
+
if (!snapshot)
|
|
474
|
+
return null;
|
|
475
|
+
if (snapshot.unavailable && snapshot.unavailable.resetsAt.getTime() > Date.now()) {
|
|
476
|
+
return 'rate_limited';
|
|
477
|
+
}
|
|
478
|
+
if (snapshot.windows.length === 0)
|
|
471
479
|
return null;
|
|
472
480
|
const blocking = snapshot.windows.filter((window) => window.key !== 'sonnet_week');
|
|
473
481
|
const windows = blocking.length > 0 ? blocking : snapshot.windows;
|
|
@@ -1286,7 +1294,6 @@ function deleteCachedClaudeOauth(service) {
|
|
|
1286
1294
|
* the Claude CLI stores its OAuth token in this plaintext file instead. The
|
|
1287
1295
|
* keychain read above finds nothing on that platform, so we fall back to the
|
|
1288
1296
|
* file. Same wrapped `{ claudeAiOauth }` shape, so one parser handles both.
|
|
1289
|
-
* Mirrors `readClaudeCredentialsBlob` (cloud/rush.ts), the proven pattern.
|
|
1290
1297
|
*
|
|
1291
1298
|
* Without step 2 the live usage fetch got no token on Linux, so `agents view`
|
|
1292
1299
|
* (run remotely over SSH by `--device`) rendered no usage bars even though the
|
|
@@ -1298,9 +1305,10 @@ function deleteCachedClaudeOauth(service) {
|
|
|
1298
1305
|
* reads Claude Code's interactive login (transmitting that ACL-bound OAuth token
|
|
1299
1306
|
* to Anthropic's API is what gets it revoked; see the branch body and
|
|
1300
1307
|
* docs/credential-management.md). It is OFF by default so full-credential
|
|
1301
|
-
* callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`)
|
|
1302
|
-
* the
|
|
1303
|
-
* the
|
|
1308
|
+
* callers that refresh (`isClaudeAuthValid` -> `getClaudeAccessToken`) still
|
|
1309
|
+
* read the interactive login. Rush Cloud dispatch does not call this helper
|
|
1310
|
+
* at all (SING-1b: the account manifest is email-only; RUSH-2359 removed the
|
|
1311
|
+
* leftover blob reader that used to send the interactive login).
|
|
1304
1312
|
*
|
|
1305
1313
|
* `opts.fileOnly` skips the ACL keychain read entirely — setup-token and
|
|
1306
1314
|
* `.credentials.json` only. Used by the daemon usage refresher so a background
|
|
@@ -1334,9 +1342,10 @@ export async function loadClaudeOauth(home, opts) {
|
|
|
1334
1342
|
// setup-token via the mint-auth path to restore usage/probe for the account.
|
|
1335
1343
|
return null;
|
|
1336
1344
|
}
|
|
1337
|
-
// Full-credential callers (isClaudeAuthValid -> getClaudeAccessToken
|
|
1338
|
-
//
|
|
1339
|
-
//
|
|
1345
|
+
// Full-credential callers (isClaudeAuthValid -> getClaudeAccessToken)
|
|
1346
|
+
// legitimately read the interactive login to run/refresh Claude. Rush Cloud
|
|
1347
|
+
// dispatch does not (SING-1b / RUSH-2359). The OS keychain/keyring step is
|
|
1348
|
+
// macOS/Linux-only; Windows and any
|
|
1340
1349
|
// fileOnly caller skip to the .credentials.json read below (the Claude CLI
|
|
1341
1350
|
// stores its OAuth token in that file too). An injected test backend makes the
|
|
1342
1351
|
// keychain path exercisable anywhere, so the platform check yields to it.
|
|
@@ -1446,11 +1455,29 @@ export function readClaudeUsageCache(usageKey, cachePath = getClaudeUsageCachePa
|
|
|
1446
1455
|
}
|
|
1447
1456
|
const snapshot = deserializeClaudeUsageSnapshot(cached, now);
|
|
1448
1457
|
if (!snapshot) {
|
|
1449
|
-
|
|
1450
|
-
writeClaudeUsageCacheFile(cache, cachePath);
|
|
1458
|
+
pruneExpiredClaudeUsageCacheEntry(usageKey, cachePath, now);
|
|
1451
1459
|
}
|
|
1452
1460
|
return snapshot;
|
|
1453
1461
|
}
|
|
1462
|
+
/** Delete an expired cache row only if it is still expired under the write lock. */
|
|
1463
|
+
export function pruneExpiredClaudeUsageCacheEntry(usageKey, cachePath = getClaudeUsageCachePath(), now = new Date()) {
|
|
1464
|
+
try {
|
|
1465
|
+
ensureLockTarget(cachePath, '{}');
|
|
1466
|
+
withFileLock(cachePath, () => {
|
|
1467
|
+
// The row may have been refreshed after readClaudeUsageCache observed it.
|
|
1468
|
+
// Re-read under the lock so stale cleanup cannot erase that newer write.
|
|
1469
|
+
const latest = readClaudeUsageCacheFile(cachePath);
|
|
1470
|
+
const current = latest[usageKey];
|
|
1471
|
+
if (!current || deserializeClaudeUsageSnapshot(current, now))
|
|
1472
|
+
return;
|
|
1473
|
+
delete latest[usageKey];
|
|
1474
|
+
atomicWriteFileSync(cachePath, JSON.stringify(latest, null, 2), 'utf-8');
|
|
1475
|
+
});
|
|
1476
|
+
}
|
|
1477
|
+
catch {
|
|
1478
|
+
/* best-effort cache cleanup — lock busy or disk full */
|
|
1479
|
+
}
|
|
1480
|
+
}
|
|
1454
1481
|
/** Write a usage snapshot to the on-disk cache. */
|
|
1455
1482
|
export function writeClaudeUsageCache(usageKey, snapshot, cachePath = getClaudeUsageCachePath()) {
|
|
1456
1483
|
try {
|
|
@@ -1459,7 +1486,14 @@ export function writeClaudeUsageCache(usageKey, snapshot, cachePath = getClaudeU
|
|
|
1459
1486
|
// Re-read under the lock so a concurrent daemon tick / agents view
|
|
1460
1487
|
// refresh cannot drop another account's row (lost update).
|
|
1461
1488
|
const cache = readClaudeUsageCacheFile(cachePath);
|
|
1462
|
-
cache[usageKey]
|
|
1489
|
+
const prior = cache[usageKey];
|
|
1490
|
+
const priorReset = parseDateValue(prior?.unavailable?.resetsAt);
|
|
1491
|
+
cache[usageKey] = serializeClaudeUsageSnapshot({
|
|
1492
|
+
...snapshot,
|
|
1493
|
+
unavailable: priorReset && priorReset.getTime() > Date.now()
|
|
1494
|
+
? { reason: 'session_limit', resetsAt: priorReset }
|
|
1495
|
+
: snapshot.unavailable,
|
|
1496
|
+
});
|
|
1463
1497
|
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1464
1498
|
});
|
|
1465
1499
|
}
|
|
@@ -1495,6 +1529,9 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1495
1529
|
return {
|
|
1496
1530
|
capturedAt: snapshot.capturedAt?.toISOString() || null,
|
|
1497
1531
|
plan: snapshot.plan ?? null,
|
|
1532
|
+
unavailable: snapshot.unavailable
|
|
1533
|
+
? { reason: snapshot.unavailable.reason, resetsAt: snapshot.unavailable.resetsAt.toISOString() }
|
|
1534
|
+
: undefined,
|
|
1498
1535
|
windows: snapshot.windows.map((window) => ({
|
|
1499
1536
|
key: window.key,
|
|
1500
1537
|
label: window.label,
|
|
@@ -1505,25 +1542,34 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1505
1542
|
})),
|
|
1506
1543
|
};
|
|
1507
1544
|
}
|
|
1508
|
-
/**
|
|
1545
|
+
/**
|
|
1546
|
+
* Deserialize a cached snapshot, dropping windows whose reset time has passed.
|
|
1547
|
+
*
|
|
1548
|
+
* An expired window is UNKNOWN, not 0%: the counter reset, and anything may
|
|
1549
|
+
* have burned since. Zeroing-but-keeping it (the previous behavior) rendered a
|
|
1550
|
+
* weeks-frozen cache as "S: 0% (now)" with `deriveUsageStatusFromSnapshot` →
|
|
1551
|
+
* 'available', so a genuinely rate-limited account read as an idle dispatch
|
|
1552
|
+
* candidate (RUSH-2858). Dropping mirrors the Grok collector, and an all-expired
|
|
1553
|
+
* snapshot deserializes to null so `readClaudeUsageCache` deletes the entry and
|
|
1554
|
+
* callers surface "usage unavailable" plus the recorded throttle reason.
|
|
1555
|
+
*/
|
|
1509
1556
|
function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
1510
1557
|
const capturedAt = parseDateValue(snapshot.capturedAt);
|
|
1511
1558
|
const windows = snapshot.windows
|
|
1512
|
-
.map((window) => {
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1516
|
-
|
|
1517
|
-
|
|
1518
|
-
|
|
1519
|
-
|
|
1520
|
-
|
|
1521
|
-
|
|
1522
|
-
|
|
1523
|
-
}
|
|
1524
|
-
|
|
1525
|
-
|
|
1526
|
-
if (windows.length === 0) {
|
|
1559
|
+
.map((window) => ({
|
|
1560
|
+
key: window.key,
|
|
1561
|
+
label: window.label,
|
|
1562
|
+
shortLabel: window.shortLabel,
|
|
1563
|
+
usedPercent: window.usedPercent,
|
|
1564
|
+
resetsAt: parseDateValue(window.resetsAt),
|
|
1565
|
+
windowMinutes: window.windowMinutes,
|
|
1566
|
+
}))
|
|
1567
|
+
.filter((window) => isCachedUsageWindowFresh(window, capturedAt, now));
|
|
1568
|
+
const unavailableReset = parseDateValue(snapshot.unavailable?.resetsAt);
|
|
1569
|
+
const unavailable = unavailableReset && unavailableReset.getTime() > now.getTime()
|
|
1570
|
+
? { reason: 'session_limit', resetsAt: unavailableReset }
|
|
1571
|
+
: undefined;
|
|
1572
|
+
if (windows.length === 0 && !unavailable) {
|
|
1527
1573
|
return null;
|
|
1528
1574
|
}
|
|
1529
1575
|
return {
|
|
@@ -1532,8 +1578,73 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
|
|
|
1532
1578
|
capturedAt,
|
|
1533
1579
|
windows,
|
|
1534
1580
|
plan: snapshot.plan ?? null,
|
|
1581
|
+
unavailable,
|
|
1535
1582
|
};
|
|
1536
1583
|
}
|
|
1584
|
+
/**
|
|
1585
|
+
* Persist a Claude session-limit refusal from a real run until its stated reset.
|
|
1586
|
+
* This quota is not part of Anthropic's five-hour/weekly usage response.
|
|
1587
|
+
*/
|
|
1588
|
+
export function noteClaudeSessionLimit(usageKey, resetsAt, cachePath = getClaudeUsageCachePath()) {
|
|
1589
|
+
try {
|
|
1590
|
+
ensureLockTarget(cachePath, '{}');
|
|
1591
|
+
withFileLock(cachePath, () => {
|
|
1592
|
+
const cache = readClaudeUsageCacheFile(cachePath);
|
|
1593
|
+
const existing = cache[usageKey] ?? { capturedAt: null, windows: [] };
|
|
1594
|
+
cache[usageKey] = {
|
|
1595
|
+
...existing,
|
|
1596
|
+
unavailable: { reason: 'session_limit', resetsAt: resetsAt.toISOString() },
|
|
1597
|
+
};
|
|
1598
|
+
atomicWriteFileSync(cachePath, JSON.stringify(cache, null, 2), 'utf-8');
|
|
1599
|
+
});
|
|
1600
|
+
}
|
|
1601
|
+
catch {
|
|
1602
|
+
/* best-effort cache write — lock busy or disk full */
|
|
1603
|
+
}
|
|
1604
|
+
}
|
|
1605
|
+
/** Parse Claude's `hit your session limit · resets …` refusal. */
|
|
1606
|
+
export function parseClaudeSessionLimitReset(text, nowMs = Date.now()) {
|
|
1607
|
+
if (!/hit your session limit/i.test(text))
|
|
1608
|
+
return null;
|
|
1609
|
+
const match = /resets\s+([^.;!\n]+)/i.exec(text);
|
|
1610
|
+
if (!match)
|
|
1611
|
+
return null;
|
|
1612
|
+
const segment = match[1].trim();
|
|
1613
|
+
const timeZone = /\(([A-Za-z_]+\/[A-Za-z_]+)\)/.exec(segment)?.[1];
|
|
1614
|
+
const timePart = segment.replace(/\([A-Za-z_]+\/[A-Za-z_]+\)/, '').trim();
|
|
1615
|
+
const absolute = Date.parse(timePart);
|
|
1616
|
+
if (!Number.isNaN(absolute) && absolute > nowMs)
|
|
1617
|
+
return new Date(absolute);
|
|
1618
|
+
const clock = /(\d{1,2})(?::(\d{2}))?\s*(am|pm)/i.exec(timePart);
|
|
1619
|
+
if (!clock)
|
|
1620
|
+
return null;
|
|
1621
|
+
let hour = Number(clock[1]) % 12;
|
|
1622
|
+
if (clock[3].toLowerCase() === 'pm')
|
|
1623
|
+
hour += 12;
|
|
1624
|
+
const minute = clock[2] ? Number(clock[2]) : 0;
|
|
1625
|
+
try {
|
|
1626
|
+
if (!timeZone) {
|
|
1627
|
+
const result = new Date(nowMs);
|
|
1628
|
+
result.setHours(hour, minute, 0, 0);
|
|
1629
|
+
if (result.getTime() <= nowMs)
|
|
1630
|
+
result.setDate(result.getDate() + 1);
|
|
1631
|
+
return result;
|
|
1632
|
+
}
|
|
1633
|
+
const parts = new Intl.DateTimeFormat('en-US', {
|
|
1634
|
+
timeZone, hour12: false, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit',
|
|
1635
|
+
}).formatToParts(new Date(nowMs));
|
|
1636
|
+
const part = (type) => Number(parts.find((item) => item.type === type)?.value ?? 0);
|
|
1637
|
+
const wallNow = Date.UTC(part('year'), part('month') - 1, part('day'), part('hour') % 24, part('minute'));
|
|
1638
|
+
const offset = wallNow - nowMs;
|
|
1639
|
+
let result = Date.UTC(part('year'), part('month') - 1, part('day'), hour, minute) - offset;
|
|
1640
|
+
if (result <= nowMs)
|
|
1641
|
+
result += 24 * 60 * 60 * 1000;
|
|
1642
|
+
return new Date(result);
|
|
1643
|
+
}
|
|
1644
|
+
catch {
|
|
1645
|
+
return null;
|
|
1646
|
+
}
|
|
1647
|
+
}
|
|
1537
1648
|
/** Check whether a cached usage window is still relevant (not expired or reset). */
|
|
1538
1649
|
function isCachedUsageWindowFresh(window, capturedAt, now) {
|
|
1539
1650
|
if (window.resetsAt && window.resetsAt.getTime() <= now.getTime()) {
|