@phnx-labs/agents-cli 1.22.68 → 1.22.70
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 +46 -0
- package/README.md +17 -14
- package/dist/bootstrap.js +3 -0
- package/dist/commands/accounts.js +23 -7
- package/dist/commands/exec.js +33 -23
- package/dist/commands/feed.js +20 -7
- package/dist/commands/import.js +2 -2
- package/dist/commands/models.js +2 -2
- package/dist/commands/monitors.js +3 -0
- package/dist/commands/permissions.js +2 -2
- package/dist/commands/projects.d.ts +26 -6
- package/dist/commands/projects.js +55 -22
- package/dist/commands/rules.js +1 -1
- package/dist/commands/send.js +35 -2
- package/dist/commands/sessions-inject.d.ts +58 -0
- package/dist/commands/sessions-inject.js +143 -7
- package/dist/commands/sessions-picker.js +1 -0
- package/dist/commands/share.js +43 -14
- package/dist/commands/ssh.js +205 -2
- package/dist/commands/traces.js +1 -1
- package/dist/lib/account-capabilities.js +0 -2
- package/dist/lib/account-registry.d.ts +3 -3
- package/dist/lib/account-registry.js +25 -7
- package/dist/lib/accounting/usage.d.ts +7 -2
- package/dist/lib/accounting/usage.js +142 -10
- package/dist/lib/acp/client.d.ts +1 -1
- package/dist/lib/acp/client.js +12 -1
- package/dist/lib/acp/harnesses.js +1 -1
- package/dist/lib/add-dir.js +0 -2
- package/dist/lib/agent-cli-commands.js +0 -2
- package/dist/lib/agent-spec/agents.d.ts +1 -1
- package/dist/lib/agent-spec/agents.js +2 -83
- package/dist/lib/boot-profile.d.ts +14 -0
- package/dist/lib/boot-profile.js +66 -0
- package/dist/lib/browser/ipc.js +9 -1
- package/dist/lib/browser/remote-control.d.ts +6 -3
- package/dist/lib/browser/remote-control.js +6 -3
- package/dist/lib/browser/service.d.ts +4 -1
- package/dist/lib/browser/service.js +7 -1
- package/dist/lib/channels/providers/desktop.d.ts +5 -4
- package/dist/lib/channels/providers/desktop.js +5 -4
- package/dist/lib/channels/registry.d.ts +6 -0
- package/dist/lib/channels/send.d.ts +11 -2
- package/dist/lib/channels/send.js +11 -2
- package/dist/lib/claude-account-token.js +108 -4
- package/dist/lib/cloud/rush.d.ts +10 -2
- package/dist/lib/cloud/rush.js +15 -9
- package/dist/lib/daemon/daemon.js +28 -8
- package/dist/lib/daemon/runner.js +6 -2
- package/dist/lib/devices/health.d.ts +38 -2
- package/dist/lib/devices/health.js +43 -5
- package/dist/lib/devices/worker-pick.d.ts +1 -1
- package/dist/lib/devices/worker-pick.js +4 -1
- package/dist/lib/exec.d.ts +2 -2
- package/dist/lib/exec.js +7 -34
- package/dist/lib/feed-broadcast.d.ts +47 -5
- package/dist/lib/feed-broadcast.js +127 -24
- package/dist/lib/hooks/install.js +0 -87
- package/dist/lib/installations/strategies.js +1 -1
- package/dist/lib/mcp-registry.js +0 -13
- package/dist/lib/mcp.js +2 -2
- package/dist/lib/model-tiers.js +1 -1
- package/dist/lib/models.js +0 -63
- package/dist/lib/monitors/engine.js +18 -0
- package/dist/lib/monitors/sources/command.js +13 -3
- package/dist/lib/monitors/sources/failure.d.ts +32 -0
- package/dist/lib/monitors/sources/failure.js +52 -0
- package/dist/lib/monitors/sources/types.d.ts +9 -0
- package/dist/lib/notify.d.ts +11 -0
- package/dist/lib/notify.js +17 -4
- package/dist/lib/owner-message.d.ts +55 -0
- package/dist/lib/owner-message.js +64 -0
- package/dist/lib/permissions-registry.d.ts +0 -2
- package/dist/lib/permissions-registry.js +3 -50
- package/dist/lib/permissions.d.ts +3 -17
- package/dist/lib/permissions.js +4 -73
- package/dist/lib/run-trace-sync.d.ts +15 -0
- package/dist/lib/run-trace-sync.js +43 -21
- package/dist/lib/rush-session.d.ts +19 -0
- package/dist/lib/rush-session.js +24 -0
- package/dist/lib/secrets/drivers/rush.js +2 -1
- package/dist/lib/secrets/filestore.d.ts +4 -0
- package/dist/lib/secrets/filestore.js +164 -3
- package/dist/lib/session/active.d.ts +10 -0
- package/dist/lib/session/active.js +3 -0
- package/dist/lib/session/cloud.js +2 -1
- package/dist/lib/session/db.d.ts +12 -1
- package/dist/lib/session/db.js +20 -1
- package/dist/lib/session/discover.js +81 -1
- package/dist/lib/session/linear.d.ts +13 -0
- package/dist/lib/session/linear.js +44 -0
- package/dist/lib/session/live-metadata.js +1 -0
- package/dist/lib/session/parse.js +2 -3
- package/dist/lib/session/prompt.d.ts +7 -1
- package/dist/lib/session/prompt.js +12 -2
- package/dist/lib/session/recovery.d.ts +21 -12
- package/dist/lib/session/recovery.js +29 -11
- package/dist/lib/session/remote/watch.js +5 -2
- package/dist/lib/session/state.js +11 -13
- package/dist/lib/share/backend.d.ts +2 -2
- package/dist/lib/share/backend.js +20 -9
- package/dist/lib/share/delete.d.ts +5 -1
- package/dist/lib/share/delete.js +7 -2
- package/dist/lib/share/http-error.d.ts +52 -0
- package/dist/lib/share/http-error.js +65 -0
- package/dist/lib/share/publish.d.ts +13 -3
- package/dist/lib/share/publish.js +19 -15
- package/dist/lib/share/worker-template.js +5 -1
- package/dist/lib/sink-format.d.ts +34 -0
- package/dist/lib/sink-format.js +17 -0
- package/dist/lib/smart-launch.js +27 -4
- package/dist/lib/staleness/detectors/permissions.js +0 -20
- package/dist/lib/staleness/writers/commands.js +1 -1
- package/dist/lib/staleness/writers/hooks.js +2 -2
- package/dist/lib/storage/index.d.ts +14 -0
- package/dist/lib/storage/index.js +14 -0
- package/dist/lib/storage/selection.d.ts +48 -0
- package/dist/lib/storage/selection.js +39 -0
- package/dist/lib/storage/visibility.d.ts +82 -0
- package/dist/lib/storage/visibility.js +99 -0
- package/dist/lib/subagents-registry.js +2 -12
- package/dist/lib/subagents.d.ts +0 -10
- package/dist/lib/subagents.js +0 -37
- package/dist/lib/teams/agents.js +3 -1
- package/dist/lib/teams/placement-probe.js +1 -0
- package/dist/lib/teams/scheduler.d.ts +8 -1
- package/dist/lib/teams/scheduler.js +4 -1
- package/dist/lib/traces/backend.js +13 -2
- package/dist/lib/types.d.ts +1 -1
- package/dist/lib/worktree/held.d.ts +166 -0
- package/dist/lib/worktree/held.js +368 -0
- package/package.json +2 -2
package/dist/commands/ssh.js
CHANGED
|
@@ -37,7 +37,7 @@ import { hostNameFor, renderSshConfig } from '../lib/devices/ssh-config.js';
|
|
|
37
37
|
import { ASKPASS_BUNDLE_ENV, ASKPASS_KEY_ENV, buildSshInvocation, deviceIdentityArgs, fleetDialTarget, isAgentsBrowserDrive, markFleetRemote, writeAskpassShim, } from '../lib/devices/connect.js';
|
|
38
38
|
import { ensureManagedKnownHostsDir, isHostPinned } from '../lib/devices/known-hosts.js';
|
|
39
39
|
import { shouldSyncTerminfo, syncTerminfoToDevice, terminfoHostKey } from '../lib/devices/terminfo.js';
|
|
40
|
-
import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
40
|
+
import { fanOutDevices, fleetHealthSkip, planFleetTargets, remoteFleetTargets, runFleet, runOnDevice, runLocalCommand, skipLabel, upgradeCommand, } from '../lib/devices/fleet.js';
|
|
41
41
|
import { isRolloutSuccess, verifyFleetRollout, } from '../lib/devices/rollout-verify.js';
|
|
42
42
|
import { fleetCapacity, fmtBytes, headroom, } from '../lib/devices/health.js';
|
|
43
43
|
import { buildFleetHealthReport, renderFleetMatrix, renderFleetSummary, renderFleetWarnings, } from '../lib/devices/health-report.js';
|
|
@@ -61,6 +61,8 @@ import { runFleetLogin } from '../lib/fleet/remote-login.js';
|
|
|
61
61
|
import { getConfigValue, listConfig, setConfigValue, unsetConfigValue, configKeySpec, autoPoolMode, configuredDeviceRole, listConfiguredDeviceRoles, setConfiguredDeviceRole, } from '../lib/device-config.js';
|
|
62
62
|
import { filterAutoPool, listWorkerDevices } from '../lib/devices/pool.js';
|
|
63
63
|
import { registerCommandGroups, setHelpSections } from '../lib/help.js';
|
|
64
|
+
import { isSelfHost } from '../lib/devices/self-host.js';
|
|
65
|
+
import { collectHeldWorktrees, collectHeldWorktreesUnder, summarizeHeld, aggregateHeld, pushStrandedBranch, } from '../lib/worktree/held.js';
|
|
64
66
|
/** One-line summary of a device for `list`. `isSelf` marks the machine this
|
|
65
67
|
* command is running on so it stands out from the rest of the tailnet.
|
|
66
68
|
* `isInteractive` marks the configured interactive host (`devices config <name> interactive.host`). */
|
|
@@ -1130,7 +1132,7 @@ function registerDevicesCommands(program) {
|
|
|
1130
1132
|
{ title: 'Inspect', names: ['list', 'show', 'status', 'ping', 'harnesses', 'accounts', 'snapshot'] },
|
|
1131
1133
|
{ title: 'Disposable devices', names: ['lease'] },
|
|
1132
1134
|
{ title: 'Configure a device', names: ['config', 'describe', 'render'] },
|
|
1133
|
-
{ title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply'] },
|
|
1135
|
+
{ title: 'Fleet operations', names: ['update', 'run', 'login', 'capture', 'apply', 'worktrees'] },
|
|
1134
1136
|
]);
|
|
1135
1137
|
devicesCmd
|
|
1136
1138
|
.command('sync')
|
|
@@ -2363,6 +2365,207 @@ is reported \`failed\` (exit non-zero), never a stranded \`ok\`.
|
|
|
2363
2365
|
.alias('kill')
|
|
2364
2366
|
.description('Terminate a running dispatched task from this machine (SIGTERM the remote process group; marks it failed/143).')
|
|
2365
2367
|
.action((id) => doDeviceTaskStop(id));
|
|
2368
|
+
const worktreesCmd = devicesCmd
|
|
2369
|
+
.command('worktrees')
|
|
2370
|
+
.description("Surface the held set of agent worktrees the sweep only counts, broken into buckets: unmerged-commits (real stranded work), uncommitted-changes, undeterminable. Read-only; --push publishes stranded branches.")
|
|
2371
|
+
.option('--json', 'emit the structured held set (device, repo, worktree, branch, reason, age, size) for machine callers / fleet aggregation')
|
|
2372
|
+
.option('--home <dir>', 'search root to discover repos under (default: $HOME)')
|
|
2373
|
+
.option('--repo <path>', 'scope to a single repo root instead of discovering')
|
|
2374
|
+
.option('--bucket <name>', 'show only one bucket: unmerged-commits | uncommitted-changes | undeterminable')
|
|
2375
|
+
.option('--fleet', 'fan out across every online device and aggregate the held set fleet-wide')
|
|
2376
|
+
.option('--push', 'PUBLISH each unmerged-commits branch that is on no remote (the safe recovery action); never deletes')
|
|
2377
|
+
.option('--yes', 'skip the confirmation prompt for --push')
|
|
2378
|
+
.action((opts) => runWorktreesHeld(opts));
|
|
2379
|
+
setHelpSections(worktreesCmd, {
|
|
2380
|
+
examples: `
|
|
2381
|
+
Inspect the residue:
|
|
2382
|
+
agents fleet worktrees # this box, grouped by bucket
|
|
2383
|
+
agents fleet worktrees --bucket unmerged-commits
|
|
2384
|
+
agents fleet worktrees --json # structured, for scripts
|
|
2385
|
+
|
|
2386
|
+
Fleet-wide:
|
|
2387
|
+
agents fleet worktrees --fleet # aggregate every online device
|
|
2388
|
+
agents fleet run 'agents fleet worktrees --json' # per-device composition
|
|
2389
|
+
|
|
2390
|
+
Recover stranded work (never deletes):
|
|
2391
|
+
agents fleet worktrees --push # publish on-no-remote branches
|
|
2392
|
+
`,
|
|
2393
|
+
notes: 'The nightly worktree-sweep (phnx-labs/.agents) reclaims merged worktrees and HOLDS the rest, reporting only a count. This surfaces that held set. unmerged-commits is the one that matters — a branch with commits on no remote is the PHNX-2951/PHNX-2732 stranded-work class; --push makes it visible. --push and --fleet are mutually exclusive (recovery is per-device on purpose).',
|
|
2394
|
+
});
|
|
2395
|
+
}
|
|
2396
|
+
const BUCKET_ORDER = ['unmerged-commits', 'uncommitted-changes', 'undeterminable'];
|
|
2397
|
+
const BUCKET_LABEL = {
|
|
2398
|
+
'unmerged-commits': 'unmerged-commits (stranded work — push or open a PR)',
|
|
2399
|
+
'uncommitted-changes': 'uncommitted-changes (dirty tree — needs review)',
|
|
2400
|
+
'undeterminable': 'undeterminable (broken/locked — re-examine)',
|
|
2401
|
+
};
|
|
2402
|
+
function isHeldBucket(v) {
|
|
2403
|
+
return BUCKET_ORDER.includes(v);
|
|
2404
|
+
}
|
|
2405
|
+
function fmtWtSize(n) {
|
|
2406
|
+
if (n < 0)
|
|
2407
|
+
return '?';
|
|
2408
|
+
if (n < 1024)
|
|
2409
|
+
return `${n}B`;
|
|
2410
|
+
const units = ['K', 'M', 'G', 'T'];
|
|
2411
|
+
let v = n / 1024;
|
|
2412
|
+
let i = 0;
|
|
2413
|
+
while (v >= 1024 && i < units.length - 1) {
|
|
2414
|
+
v /= 1024;
|
|
2415
|
+
i++;
|
|
2416
|
+
}
|
|
2417
|
+
return `${v.toFixed(v < 10 ? 1 : 0)}${units[i]}`;
|
|
2418
|
+
}
|
|
2419
|
+
/**
|
|
2420
|
+
* `agents fleet worktrees` — surface the held set the sweep only counts. Never
|
|
2421
|
+
* removes anything; `--push` publishes on-no-remote stranded branches. This is
|
|
2422
|
+
* the CLI-native surfacing half of PHNX-3520 (the sweep itself lives in
|
|
2423
|
+
* phnx-labs/.agents; it still emits a bare count until a sibling change teaches
|
|
2424
|
+
* it to consume this).
|
|
2425
|
+
*/
|
|
2426
|
+
async function runWorktreesHeld(opts) {
|
|
2427
|
+
if (opts.bucket && !isHeldBucket(opts.bucket)) {
|
|
2428
|
+
console.error(chalk.red(`Unknown bucket '${opts.bucket}'. Use one of: ${BUCKET_ORDER.join(', ')}`));
|
|
2429
|
+
process.exitCode = 1;
|
|
2430
|
+
return;
|
|
2431
|
+
}
|
|
2432
|
+
if (opts.fleet && opts.push) {
|
|
2433
|
+
console.error(chalk.red('--push and --fleet are mutually exclusive; run --push on the device that holds the work.'));
|
|
2434
|
+
process.exitCode = 1;
|
|
2435
|
+
return;
|
|
2436
|
+
}
|
|
2437
|
+
if (opts.fleet && opts.repo) {
|
|
2438
|
+
console.error(chalk.red('--repo scopes a single local repo and cannot combine with --fleet; drop one. To scope on each device, run `agents fleet run \'agents fleet worktrees --repo <path> --json\'`.'));
|
|
2439
|
+
process.exitCode = 1;
|
|
2440
|
+
return;
|
|
2441
|
+
}
|
|
2442
|
+
if (opts.fleet) {
|
|
2443
|
+
await runWorktreesHeldFleet(opts);
|
|
2444
|
+
return;
|
|
2445
|
+
}
|
|
2446
|
+
const searchHome = opts.home ?? os.homedir();
|
|
2447
|
+
const held = opts.repo
|
|
2448
|
+
? await collectHeldWorktrees(path.resolve(opts.repo))
|
|
2449
|
+
: await collectHeldWorktreesUnder(searchHome);
|
|
2450
|
+
if (opts.push) {
|
|
2451
|
+
await runWorktreesPush(held, opts);
|
|
2452
|
+
return;
|
|
2453
|
+
}
|
|
2454
|
+
const shown = opts.bucket ? held.filter((w) => w.bucket === opts.bucket) : held;
|
|
2455
|
+
if (opts.json) {
|
|
2456
|
+
console.log(JSON.stringify(shown, null, 2));
|
|
2457
|
+
return;
|
|
2458
|
+
}
|
|
2459
|
+
const summary = summarizeHeld(shown);
|
|
2460
|
+
if (summary.total === 0) {
|
|
2461
|
+
console.log(chalk.green('No held worktrees — nothing stranded, dirty, or undeterminable.'));
|
|
2462
|
+
return;
|
|
2463
|
+
}
|
|
2464
|
+
for (const bucket of BUCKET_ORDER) {
|
|
2465
|
+
const items = summary.buckets[bucket];
|
|
2466
|
+
if (items.length === 0)
|
|
2467
|
+
continue;
|
|
2468
|
+
const color = bucket === 'unmerged-commits' ? chalk.yellow : chalk.gray;
|
|
2469
|
+
console.log('\n' + color(chalk.bold(`${BUCKET_LABEL[bucket]} — ${items.length}`)));
|
|
2470
|
+
for (const w of items.sort((a, b) => b.unmergedCommits - a.unmergedCommits)) {
|
|
2471
|
+
const detail = bucket === 'unmerged-commits'
|
|
2472
|
+
? `${w.unmergedCommits} commit${w.unmergedCommits === 1 ? '' : 's'}, ${w.hasRemoteBranch ? 'on remote' : chalk.red('NO remote')}`
|
|
2473
|
+
: bucket === 'uncommitted-changes'
|
|
2474
|
+
? `${w.dirtyFiles} dirty file${w.dirtyFiles === 1 ? '' : 's'}`
|
|
2475
|
+
: w.reason;
|
|
2476
|
+
console.log(` ${chalk.cyan(w.repoName + '/' + w.name).padEnd(48)} ${chalk.gray((w.branch ?? 'detached').padEnd(28))} ${detail} ${chalk.dim(`${w.ageDays}d · ${fmtWtSize(w.sizeBytes)}`)}`);
|
|
2477
|
+
}
|
|
2478
|
+
}
|
|
2479
|
+
const stranded = summary.buckets['unmerged-commits'].filter((w) => !w.hasRemoteBranch).length;
|
|
2480
|
+
if (stranded > 0) {
|
|
2481
|
+
console.log('\n' + chalk.yellow(`${stranded} branch${stranded === 1 ? '' : 'es'} with unmerged commits on no remote. Recover: agents fleet worktrees --push`));
|
|
2482
|
+
}
|
|
2483
|
+
}
|
|
2484
|
+
/** `--push`: publish every on-no-remote stranded branch. Never deletes. */
|
|
2485
|
+
async function runWorktreesPush(held, opts) {
|
|
2486
|
+
const candidates = held.filter((w) => w.bucket === 'unmerged-commits' && !w.hasRemoteBranch && w.branch);
|
|
2487
|
+
if (candidates.length === 0) {
|
|
2488
|
+
console.log(chalk.green('No stranded branches to publish (nothing with unmerged commits on no remote).'));
|
|
2489
|
+
return;
|
|
2490
|
+
}
|
|
2491
|
+
if (isInteractiveTerminal() && !opts.yes) {
|
|
2492
|
+
console.log(chalk.yellow(`About to push ${candidates.length} stranded branch${candidates.length === 1 ? '' : 'es'} to origin:`));
|
|
2493
|
+
for (const w of candidates)
|
|
2494
|
+
console.log(` ${chalk.cyan(w.repoName + '/' + w.name)} → ${w.branch}`);
|
|
2495
|
+
const { confirm } = await import('@inquirer/prompts');
|
|
2496
|
+
const go = await confirm({ message: 'Push these branches?', default: true }).catch(() => false);
|
|
2497
|
+
if (!go) {
|
|
2498
|
+
console.log(chalk.gray('Cancelled — nothing pushed.'));
|
|
2499
|
+
return;
|
|
2500
|
+
}
|
|
2501
|
+
}
|
|
2502
|
+
let pushed = 0;
|
|
2503
|
+
for (const w of candidates) {
|
|
2504
|
+
const res = await pushStrandedBranch(w.repo, w);
|
|
2505
|
+
if (res.pushed) {
|
|
2506
|
+
pushed++;
|
|
2507
|
+
console.log(chalk.green(` pushed ${w.repoName}/${w.name} (${res.branch})`));
|
|
2508
|
+
}
|
|
2509
|
+
else {
|
|
2510
|
+
console.log(chalk.yellow(` skipped ${w.repoName}/${w.name}: ${res.reason}`));
|
|
2511
|
+
}
|
|
2512
|
+
}
|
|
2513
|
+
console.log('\n' + chalk.green(`Published ${pushed}/${candidates.length} stranded branch${candidates.length === 1 ? '' : 'es'}.`));
|
|
2514
|
+
}
|
|
2515
|
+
/** `--fleet`: run this same read-only surface on every online device, aggregate. */
|
|
2516
|
+
async function runWorktreesHeldFleet(opts) {
|
|
2517
|
+
const reg = await loadDevices();
|
|
2518
|
+
const targets = planFleetTargets(reg);
|
|
2519
|
+
const online = targets.filter((t) => !t.skip);
|
|
2520
|
+
if (online.length === 0) {
|
|
2521
|
+
console.log(chalk.gray("No online devices. Run 'agents devices sync' first."));
|
|
2522
|
+
return;
|
|
2523
|
+
}
|
|
2524
|
+
const self = machineId();
|
|
2525
|
+
const remoteCmd = ['agents', 'fleet', 'worktrees', '--json'];
|
|
2526
|
+
if (opts.home)
|
|
2527
|
+
remoteCmd.push('--home', opts.home);
|
|
2528
|
+
const perDevice = [];
|
|
2529
|
+
for (const t of online) {
|
|
2530
|
+
const name = t.device.name;
|
|
2531
|
+
const isSelf = name === self || isSelfHost(name);
|
|
2532
|
+
const res = isSelf ? runLocalCommand(remoteCmd) : runOnDevice(t.device, remoteCmd);
|
|
2533
|
+
if (res.code !== 0) {
|
|
2534
|
+
console.error(chalk.gray(` ${name}: ${(res.stderr || 'failed').trim().slice(0, 120)}`));
|
|
2535
|
+
continue;
|
|
2536
|
+
}
|
|
2537
|
+
try {
|
|
2538
|
+
perDevice.push({ device: name, held: JSON.parse(res.stdout) });
|
|
2539
|
+
}
|
|
2540
|
+
catch {
|
|
2541
|
+
console.error(chalk.gray(` ${name}: unparseable output (older CLI?)`));
|
|
2542
|
+
}
|
|
2543
|
+
}
|
|
2544
|
+
// Apply the bucket filter to the per-device inputs BEFORE aggregating, so
|
|
2545
|
+
// `total`, per-`devices` totals, and `buckets` all describe the same filtered
|
|
2546
|
+
// set — zeroing buckets after the sum leaves `total` counting rows the output
|
|
2547
|
+
// no longer lists.
|
|
2548
|
+
const scoped = opts.bucket
|
|
2549
|
+
? perDevice.map((d) => ({ device: d.device, held: d.held.filter((w) => w.bucket === opts.bucket) }))
|
|
2550
|
+
: perDevice;
|
|
2551
|
+
const agg = aggregateHeld(scoped);
|
|
2552
|
+
if (opts.json) {
|
|
2553
|
+
console.log(JSON.stringify(agg, null, 2));
|
|
2554
|
+
return;
|
|
2555
|
+
}
|
|
2556
|
+
console.log(chalk.bold(`Held worktrees across ${perDevice.length} device(s): ${agg.total}`));
|
|
2557
|
+
for (const bucket of BUCKET_ORDER) {
|
|
2558
|
+
const items = agg.buckets[bucket];
|
|
2559
|
+
if (items.length === 0)
|
|
2560
|
+
continue;
|
|
2561
|
+
const color = bucket === 'unmerged-commits' ? chalk.yellow : chalk.gray;
|
|
2562
|
+
console.log('\n' + color(chalk.bold(`${BUCKET_LABEL[bucket]} — ${items.length}`)));
|
|
2563
|
+
for (const w of items) {
|
|
2564
|
+
const dev = w.device ?? '?';
|
|
2565
|
+
const detail = bucket === 'unmerged-commits' ? `${w.unmergedCommits} commits, ${w.hasRemoteBranch ? 'on remote' : chalk.red('NO remote')}` : w.reason;
|
|
2566
|
+
console.log(` ${chalk.magenta(dev.padEnd(14))} ${chalk.cyan((w.repoName + '/' + w.name).padEnd(40))} ${chalk.gray((w.branch ?? 'detached').padEnd(24))} ${detail}`);
|
|
2567
|
+
}
|
|
2568
|
+
}
|
|
2366
2569
|
}
|
|
2367
2570
|
/**
|
|
2368
2571
|
* `agents devices ps` — list tasks dispatched to devices (`agents run --device
|
package/dist/commands/traces.js
CHANGED
|
@@ -182,7 +182,7 @@ export function registerTracesCommands(program) {
|
|
|
182
182
|
.description("Sync this device's derived, redacted trajectories to your Phoenix account");
|
|
183
183
|
const setupCmd = tracesCmd
|
|
184
184
|
.command('setup')
|
|
185
|
-
.description('Provision
|
|
185
|
+
.description('(advanced) Provision a self-hosted Cloudflare Worker + R2 bucket for traces — NOT required; `agents auth login` syncs to the managed endpoint with zero setup')
|
|
186
186
|
.option('--bundle <name>', 'secrets bundle holding the Cloudflare API token', DEFAULT_CF_BUNDLE)
|
|
187
187
|
.option('--worker <name>', 'Worker name', DEFAULT_WORKER_NAME)
|
|
188
188
|
.option('--bucket <name>', 'R2 bucket name', DEFAULT_BUCKET_NAME)
|
|
@@ -35,10 +35,8 @@ export const NATIVE_ACCOUNT_CAPABILITIES = {
|
|
|
35
35
|
copilot: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
36
36
|
openclaw: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
37
37
|
amp: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
38
|
-
kiro: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
39
38
|
goose: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
40
39
|
hermes: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
41
|
-
pi: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
42
40
|
warp: { inspection: 'none', scope: 'unsupported', status: 'unsupported' },
|
|
43
41
|
};
|
|
44
42
|
export function nativeAccountCapability(agent) {
|
|
@@ -54,12 +54,12 @@ export declare function listNativeAccounts(meta: Pick<Meta, 'accounts' | 'device
|
|
|
54
54
|
* running before the body, so callers that only need a native lookup must be
|
|
55
55
|
* able to omit it.
|
|
56
56
|
*/
|
|
57
|
-
export declare function findUnifiedAccount(nameOrId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, doc?: AccountRegistryDocument): UnifiedAccount | null;
|
|
57
|
+
export declare function findUnifiedAccount(nameOrId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>, doc?: AccountRegistryDocument, preferAgent?: AgentId): UnifiedAccount | null;
|
|
58
58
|
export declare function addNativeAccount(name: string, agent: AgentId, identityKey: string, identityLabel: string | undefined, scope: 'version' | 'device'): NativeAccount;
|
|
59
59
|
/** Create or replace the version-independent label for one native identity. */
|
|
60
60
|
export declare function labelNativeAccount(agent: AgentId, identityKey: string, identityLabel: string | undefined, label: string | undefined, scope: 'version' | 'device'): NativeAccount;
|
|
61
|
-
export declare function bindAccount(nameOrId: string, target: string): UnifiedAccount;
|
|
62
|
-
export declare function unbindAccount(nameOrId: string, target: string): void;
|
|
61
|
+
export declare function bindAccount(nameOrId: string, target: string, preferAgent?: AgentId): UnifiedAccount;
|
|
62
|
+
export declare function unbindAccount(nameOrId: string, target: string, preferAgent?: AgentId): void;
|
|
63
63
|
/** Every target bound to `accountId`: this box's device-doc bindings merged over
|
|
64
64
|
* the fleet-shared central bindings (PHNX-3315). */
|
|
65
65
|
export declare function accountBindings(accountId: string, meta: Pick<Meta, 'accounts' | 'deviceAccounts'>): string[];
|
|
@@ -159,9 +159,18 @@ export function listNativeAccounts(meta) {
|
|
|
159
159
|
* running before the body, so callers that only need a native lookup must be
|
|
160
160
|
* able to omit it.
|
|
161
161
|
*/
|
|
162
|
-
export function findUnifiedAccount(nameOrId, meta, doc) {
|
|
162
|
+
export function findUnifiedAccount(nameOrId, meta, doc, preferAgent) {
|
|
163
163
|
const needle = nameOrId.toLowerCase();
|
|
164
|
-
const
|
|
164
|
+
const matches = listNativeAccounts(meta).filter(account => account.id === nameOrId || account.name.toLowerCase() === needle || account.identityLabel?.toLowerCase() === needle);
|
|
165
|
+
// `identityLabel` defaults to the login's own identifier (the email), so ONE
|
|
166
|
+
// selector legitimately matches several harnesses: `muqsitnawaz@gmail.com` is a
|
|
167
|
+
// claude login AND a codex login. Un-scoped, `.find()` returned whichever row the
|
|
168
|
+
// merged store happened to order first, so `agents run claude#<email>` died with
|
|
169
|
+
// "Account 'personal' is a codex login and cannot authenticate the claude harness"
|
|
170
|
+
// while that identity's own claude login sat right there. Prefer the harness being
|
|
171
|
+
// launched; fall back to the first match so a management lookup with no harness in
|
|
172
|
+
// hand (rename/remove/view) behaves exactly as before.
|
|
173
|
+
const native = matches.find(account => account.agent === preferAgent) ?? matches[0];
|
|
165
174
|
if (native)
|
|
166
175
|
return native;
|
|
167
176
|
const provider = findAccount(nameOrId, doc ?? readAccountRegistry());
|
|
@@ -253,9 +262,13 @@ export function labelNativeAccount(agent, identityKey, identityLabel, label, sco
|
|
|
253
262
|
});
|
|
254
263
|
return { ...matches[0], name: resolvedLabel, identityLabel, scope: rowScope };
|
|
255
264
|
}
|
|
256
|
-
export function bindAccount(nameOrId, target) {
|
|
265
|
+
export function bindAccount(nameOrId, target, preferAgent) {
|
|
257
266
|
const meta = readMeta();
|
|
258
|
-
|
|
267
|
+
// Scope to the harness being bound to: a bare identity selector matches every
|
|
268
|
+
// harness it is signed into, so without this the binding could persist against
|
|
269
|
+
// a different harness's row than the caller validated (the attach command
|
|
270
|
+
// resolves `targetAgent` and passes it here).
|
|
271
|
+
const account = findUnifiedAccount(nameOrId, meta, undefined, preferAgent);
|
|
259
272
|
if (!account)
|
|
260
273
|
throw new Error(`Unknown account '${nameOrId}'.`);
|
|
261
274
|
// A binding follows its account: one that targets a device-scoped native login
|
|
@@ -275,9 +288,11 @@ export function bindAccount(nameOrId, target) {
|
|
|
275
288
|
}
|
|
276
289
|
return account;
|
|
277
290
|
}
|
|
278
|
-
export function unbindAccount(nameOrId, target) {
|
|
291
|
+
export function unbindAccount(nameOrId, target, preferAgent) {
|
|
279
292
|
const meta = readMeta();
|
|
280
|
-
|
|
293
|
+
// Same scoping as bindAccount: detach the row on the harness the caller means,
|
|
294
|
+
// not whichever the store happened to order first for a colliding identity.
|
|
295
|
+
const account = findUnifiedAccount(nameOrId, meta, undefined, preferAgent);
|
|
281
296
|
if (!account)
|
|
282
297
|
throw new Error(`Unknown account '${nameOrId}'.`);
|
|
283
298
|
const inCentral = meta.accounts?.bindings?.[target] === account.id;
|
|
@@ -527,7 +542,10 @@ export function resolveSpawnAccount(explicit, agent, version, meta, opts = {}) {
|
|
|
527
542
|
const selection = resolveAccountSelection(explicit, agent, meta, { useDefault: opts.useDefault, target });
|
|
528
543
|
if (!selection)
|
|
529
544
|
return null;
|
|
530
|
-
|
|
545
|
+
// Scope the lookup to the harness being launched: a bare identity selector
|
|
546
|
+
// (`claude#muqsitnawaz@gmail.com`) matches every harness that identity is signed
|
|
547
|
+
// into, and only this one can authenticate the spawn.
|
|
548
|
+
const unified = findUnifiedAccount(selection.id, meta, undefined, agent);
|
|
531
549
|
if (!unified) {
|
|
532
550
|
// A stale per-harness default is a preference, not a hard requirement: the
|
|
533
551
|
// machine stays runnable by falling back to balanced rotation. Bindings and
|
|
@@ -180,8 +180,13 @@ export interface UsageSnapshot {
|
|
|
180
180
|
* MUST NEVER read this field — `isUsageVerified`/`hasStaleUsage`/
|
|
181
181
|
* `hasUsageAvailable`/`deriveUsageStatusFromSnapshot` consult only `windows`,
|
|
182
182
|
* so a stale number rendered here can never make a stale account read as
|
|
183
|
-
* verified or eligible (the RUSH-2858 property).
|
|
184
|
-
*
|
|
183
|
+
* verified or eligible (the RUSH-2858 property). Not a serialized key of its
|
|
184
|
+
* own, and dropped from `--json` (which projects `windows` explicitly) — but
|
|
185
|
+
* the READINGS it holds do round-trip through the on-disk cache:
|
|
186
|
+
* `serializeClaudeUsageSnapshot` persists the union of `windows` and
|
|
187
|
+
* `staleWindows`, and `deserializeClaudeUsageSnapshot` re-runs the freshness
|
|
188
|
+
* gate on read to re-partition them (so a collector like Grok that pre-splits
|
|
189
|
+
* an ended-period reading onto `staleWindows` still survives the round-trip).
|
|
185
190
|
*/
|
|
186
191
|
staleWindows?: UsageWindow[];
|
|
187
192
|
plan?: string | null;
|
|
@@ -1900,6 +1900,17 @@ function writeClaudeUsageCacheFile(cache, cachePath) {
|
|
|
1900
1900
|
}
|
|
1901
1901
|
/** Convert a live UsageSnapshot to its JSON-serializable cached form. */
|
|
1902
1902
|
function serializeClaudeUsageSnapshot(snapshot) {
|
|
1903
|
+
// Persist the union of fresh `windows` and last-known `staleWindows`.
|
|
1904
|
+
// `deserializeClaudeUsageSnapshot` re-runs the freshness gate on read and
|
|
1905
|
+
// re-partitions the serialized windows into fresh vs. stale, so what matters
|
|
1906
|
+
// is that every last-known reading reaches disk. Claude's collector returns
|
|
1907
|
+
// raw windows (no `staleWindows`) and relies on that read-side partition. But
|
|
1908
|
+
// Grok's collector pre-partitions in the fetch, moving an ended-period reading
|
|
1909
|
+
// onto `staleWindows` — serializing only `windows` dropped it, so the very
|
|
1910
|
+
// number a daemon `--refresh` just captured was gone from the next cached
|
|
1911
|
+
// `agents view grok`, which rendered the plan alone (no bar). Include the
|
|
1912
|
+
// stale windows here so the round-trip preserves them for any collector.
|
|
1913
|
+
const persistedWindows = [...snapshot.windows, ...(snapshot.staleWindows ?? [])];
|
|
1903
1914
|
return {
|
|
1904
1915
|
capturedAt: snapshot.capturedAt?.toISOString() || null,
|
|
1905
1916
|
plan: snapshot.plan ?? null,
|
|
@@ -1910,7 +1921,7 @@ function serializeClaudeUsageSnapshot(snapshot) {
|
|
|
1910
1921
|
resetsAt: snapshot.unavailable.resetsAt?.toISOString(),
|
|
1911
1922
|
}
|
|
1912
1923
|
: undefined,
|
|
1913
|
-
windows:
|
|
1924
|
+
windows: persistedWindows.map((window) => ({
|
|
1914
1925
|
key: window.key,
|
|
1915
1926
|
label: window.label,
|
|
1916
1927
|
shortLabel: window.shortLabel,
|
|
@@ -2400,17 +2411,131 @@ function safeStatSync(filePath) {
|
|
|
2400
2411
|
return null;
|
|
2401
2412
|
}
|
|
2402
2413
|
}
|
|
2414
|
+
/**
|
|
2415
|
+
* Resolve the Grok billing log to read usage from.
|
|
2416
|
+
*
|
|
2417
|
+
* `agents view grok` reads usage per INSTALLED VERSION, passing each version's
|
|
2418
|
+
* isolated home (`~/.agents/.history/versions/grok/<ver>`). Grok writes
|
|
2419
|
+
* `unified.jsonl` only to the shared real home `~/.grok/logs/unified.jsonl`
|
|
2420
|
+
* even though GROK_HOME isolates auth/config/models per version. A per-version
|
|
2421
|
+
* log, if one ever appears, still wins; otherwise we return the shared path
|
|
2422
|
+
* marked `shared: true` so the caller can attribute it to at most one identity.
|
|
2423
|
+
* Grok accounts are version-scoped (`NATIVE_ACCOUNT_CAPABILITIES.grok.scope ===
|
|
2424
|
+
* 'version'`) — the shared file has no owner, so it must not be stamped onto
|
|
2425
|
+
* every version home.
|
|
2426
|
+
*/
|
|
2427
|
+
function resolveGrokBillingLogPath(home) {
|
|
2428
|
+
const rel = ['.grok', 'logs', 'unified.jsonl'];
|
|
2429
|
+
const perVersion = path.join(home || os.homedir(), ...rel);
|
|
2430
|
+
try {
|
|
2431
|
+
if (fs.existsSync(perVersion))
|
|
2432
|
+
return { logPath: perVersion, shared: false };
|
|
2433
|
+
}
|
|
2434
|
+
catch { /* unreadable */ }
|
|
2435
|
+
// `AGENTS_REAL_HOME` is the seam every version-home consumer honors: a
|
|
2436
|
+
// daemon/service-manager child's HOME can be baked to something other than
|
|
2437
|
+
// the account's real home, so os.homedir() alone is not a reliable stand-in.
|
|
2438
|
+
const shared = path.join(process.env.AGENTS_REAL_HOME || os.homedir(), ...rel);
|
|
2439
|
+
if (shared !== perVersion) {
|
|
2440
|
+
try {
|
|
2441
|
+
if (fs.existsSync(shared))
|
|
2442
|
+
return { logPath: shared, shared: true };
|
|
2443
|
+
}
|
|
2444
|
+
catch { /* unreadable */ }
|
|
2445
|
+
}
|
|
2446
|
+
return null;
|
|
2447
|
+
}
|
|
2448
|
+
/** This home's own `.grok/auth.json` only — never the shared-home fallback. */
|
|
2449
|
+
function readGrokAuthIdentity(home) {
|
|
2450
|
+
const authPath = path.join(home, '.grok', 'auth.json');
|
|
2451
|
+
try {
|
|
2452
|
+
if (!fs.existsSync(authPath))
|
|
2453
|
+
return null;
|
|
2454
|
+
const data = JSON.parse(fs.readFileSync(authPath, 'utf-8'));
|
|
2455
|
+
const records = (data && typeof data === 'object' ? [data, ...Object.values(data)] : [])
|
|
2456
|
+
.filter((r) => !!r && typeof r === 'object' && !Array.isArray(r));
|
|
2457
|
+
const account = records
|
|
2458
|
+
.filter((r) => typeof r.refresh_token === 'string' || typeof r.email === 'string' || typeof r.user_id === 'string')
|
|
2459
|
+
.sort((a, b) => String(b.create_time || '').localeCompare(String(a.create_time || '')))[0];
|
|
2460
|
+
if (!account)
|
|
2461
|
+
return null;
|
|
2462
|
+
const userRaw = account.user_id ?? account.principal_id;
|
|
2463
|
+
const userId = typeof userRaw === 'string' && userRaw.trim() ? userRaw.trim() : null;
|
|
2464
|
+
const email = typeof account.email === 'string' && account.email.trim()
|
|
2465
|
+
? account.email.trim().toLowerCase()
|
|
2466
|
+
: null;
|
|
2467
|
+
if (!userId && !email)
|
|
2468
|
+
return null;
|
|
2469
|
+
return { userId, email };
|
|
2470
|
+
}
|
|
2471
|
+
catch {
|
|
2472
|
+
return null;
|
|
2473
|
+
}
|
|
2474
|
+
}
|
|
2475
|
+
function grokIdentitiesMatch(a, b) {
|
|
2476
|
+
if (!a || !b)
|
|
2477
|
+
return false;
|
|
2478
|
+
if (a.userId && b.userId)
|
|
2479
|
+
return a.userId === b.userId;
|
|
2480
|
+
if (a.email && b.email)
|
|
2481
|
+
return a.email === b.email;
|
|
2482
|
+
return false;
|
|
2483
|
+
}
|
|
2484
|
+
function grokIdentityFromBillingPayload(parsed) {
|
|
2485
|
+
const ctx = parsed.ctx && typeof parsed.ctx === 'object' && !Array.isArray(parsed.ctx)
|
|
2486
|
+
? parsed.ctx
|
|
2487
|
+
: null;
|
|
2488
|
+
const config = ctx?.config && typeof ctx.config === 'object' && !Array.isArray(ctx.config)
|
|
2489
|
+
? ctx.config
|
|
2490
|
+
: null;
|
|
2491
|
+
const pick = (...cands) => {
|
|
2492
|
+
for (const c of cands) {
|
|
2493
|
+
if (typeof c === 'string' && c.trim())
|
|
2494
|
+
return c.trim();
|
|
2495
|
+
}
|
|
2496
|
+
return null;
|
|
2497
|
+
};
|
|
2498
|
+
const userId = pick(ctx?.user_id, ctx?.userId, ctx?.principal_id, config?.user_id, parsed.user_id);
|
|
2499
|
+
const emailRaw = pick(ctx?.email, config?.email, parsed.email);
|
|
2500
|
+
if (!userId && !emailRaw)
|
|
2501
|
+
return null;
|
|
2502
|
+
return { userId, email: emailRaw ? emailRaw.toLowerCase() : null };
|
|
2503
|
+
}
|
|
2504
|
+
function sameHomePath(a, b) {
|
|
2505
|
+
return (safeRealpathSync(a) ?? path.resolve(a)) === (safeRealpathSync(b) ?? path.resolve(b));
|
|
2506
|
+
}
|
|
2507
|
+
/**
|
|
2508
|
+
* Whether the shared `~/.grok` billing log may be attached to this home.
|
|
2509
|
+
* Grok logins are per version home; the shared last line is one account's
|
|
2510
|
+
* meter. Fail loud: never copy it onto every installed identity.
|
|
2511
|
+
*/
|
|
2512
|
+
function sharedGrokLogAppliesToHome(home, match) {
|
|
2513
|
+
const realHome = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
2514
|
+
const requestedHome = home || os.homedir();
|
|
2515
|
+
const thisId = readGrokAuthIdentity(requestedHome);
|
|
2516
|
+
if (match.identity) {
|
|
2517
|
+
return grokIdentitiesMatch(match.identity, thisId);
|
|
2518
|
+
}
|
|
2519
|
+
// No identity on the line (the live Grok shape). Attribute the reading to
|
|
2520
|
+
// exactly one canonical identity: the real home itself, or the version home
|
|
2521
|
+
// whose own auth.json matches the shared `~/.grok/auth.json`.
|
|
2522
|
+
if (sameHomePath(requestedHome, realHome))
|
|
2523
|
+
return true;
|
|
2524
|
+
return grokIdentitiesMatch(thisId, readGrokAuthIdentity(realHome));
|
|
2525
|
+
}
|
|
2403
2526
|
/** Parse the latest billing info from Grok's unified log. */
|
|
2404
2527
|
async function getGrokUsageInfo(options) {
|
|
2405
2528
|
try {
|
|
2406
|
-
const
|
|
2407
|
-
const logPath = path.join(base, '.grok', 'logs', 'unified.jsonl');
|
|
2529
|
+
const resolved = resolveGrokBillingLogPath(options?.home);
|
|
2408
2530
|
// No log yet: a benign "nothing recorded here", not a failure (RUSH-3040).
|
|
2409
|
-
if (!
|
|
2531
|
+
if (!resolved)
|
|
2410
2532
|
return usageNoRecentUsageInfo();
|
|
2411
|
-
const match = await readLatestGrokBilling(logPath);
|
|
2533
|
+
const match = await readLatestGrokBilling(resolved.logPath);
|
|
2412
2534
|
if (!match)
|
|
2413
2535
|
return usageNoRecentUsageInfo();
|
|
2536
|
+
if (resolved.shared && !sharedGrokLogAppliesToHome(options?.home, match)) {
|
|
2537
|
+
return usageNoRecentUsageInfo();
|
|
2538
|
+
}
|
|
2414
2539
|
// Grok has no live usage API (`network: false`) — bars are last-seen from
|
|
2415
2540
|
// this machine's unified.jsonl only. Drop windows whose billing period has
|
|
2416
2541
|
// already ended so a stale 100% does not paint "rate-limited" after reset,
|
|
@@ -2703,10 +2828,16 @@ async function readLatestGrokBilling(filePath) {
|
|
|
2703
2828
|
return;
|
|
2704
2829
|
try {
|
|
2705
2830
|
const parsed = JSON.parse(line);
|
|
2706
|
-
|
|
2707
|
-
|
|
2831
|
+
const ctx = parsed.ctx && typeof parsed.ctx === 'object' && !Array.isArray(parsed.ctx)
|
|
2832
|
+
? parsed.ctx
|
|
2833
|
+
: null;
|
|
2834
|
+
if (parsed.msg === 'billing: fetched credits config' && ctx?.config) {
|
|
2835
|
+
const config = ctx.config;
|
|
2708
2836
|
const windows = [];
|
|
2709
|
-
|
|
2837
|
+
const currentPeriod = config.currentPeriod && typeof config.currentPeriod === 'object'
|
|
2838
|
+
? config.currentPeriod
|
|
2839
|
+
: null;
|
|
2840
|
+
if (currentPeriod?.end && typeof config.creditUsagePercent === 'number') {
|
|
2710
2841
|
// `creditUsagePercent` is Grok's weekly credit consumption (0-100);
|
|
2711
2842
|
// the billing period's `end` is when that window resets.
|
|
2712
2843
|
// Do NOT coerce a missing percent to 0 — a new period often lands a
|
|
@@ -2718,14 +2849,15 @@ async function readLatestGrokBilling(filePath) {
|
|
|
2718
2849
|
label: 'Current week',
|
|
2719
2850
|
shortLabel: 'W',
|
|
2720
2851
|
usedPercent: Math.max(0, Math.min(100, rawPercent)),
|
|
2721
|
-
resetsAt: parseDateValue(
|
|
2852
|
+
resetsAt: parseDateValue(currentPeriod.end),
|
|
2722
2853
|
windowMinutes: inferWindowMinutes('week'),
|
|
2723
2854
|
});
|
|
2724
2855
|
}
|
|
2725
2856
|
latest = {
|
|
2726
2857
|
capturedAt: parseDateValue(parsed.ts),
|
|
2727
|
-
subscriptionTier:
|
|
2858
|
+
subscriptionTier: typeof ctx.subscriptionTier === 'string' ? ctx.subscriptionTier : null,
|
|
2728
2859
|
windows,
|
|
2860
|
+
identity: grokIdentityFromBillingPayload(parsed),
|
|
2729
2861
|
};
|
|
2730
2862
|
}
|
|
2731
2863
|
}
|
package/dist/lib/acp/client.d.ts
CHANGED
|
@@ -11,7 +11,7 @@
|
|
|
11
11
|
*/
|
|
12
12
|
import { type SessionNotification } from '@zed-industries/agent-client-protocol';
|
|
13
13
|
import type { AgentId } from '../types.js';
|
|
14
|
-
import type
|
|
14
|
+
import { type ExecMode } from '../exec.js';
|
|
15
15
|
export interface AcpRunOptions {
|
|
16
16
|
agent: AgentId;
|
|
17
17
|
prompt: string;
|
package/dist/lib/acp/client.js
CHANGED
|
@@ -15,6 +15,7 @@ import { promises as fs } from 'fs';
|
|
|
15
15
|
import * as path from 'path';
|
|
16
16
|
import { ClientSideConnection, ndJsonStream, } from '@zed-industries/agent-client-protocol';
|
|
17
17
|
import { getAcpSpec, supportsAcp } from './harnesses.js';
|
|
18
|
+
import { buildExecEnv } from '../exec.js';
|
|
18
19
|
const PROTOCOL_VERSION = 1;
|
|
19
20
|
/**
|
|
20
21
|
* Runs a single prompt turn against an ACP-capable agent and streams updates
|
|
@@ -25,10 +26,20 @@ export async function runAcp(opts) {
|
|
|
25
26
|
throw new Error(`Agent '${opts.agent}' does not support ACP. Use direct exec instead.`);
|
|
26
27
|
}
|
|
27
28
|
const spec = getAcpSpec(opts.agent);
|
|
29
|
+
// Build the harness's exec env the SAME way `agents run` does, instead of
|
|
30
|
+
// handing it a raw process.env. buildExecEnv is what injects the per-account
|
|
31
|
+
// `CLAUDE_CODE_OAUTH_TOKEN` on a worker device (gated by device role: a
|
|
32
|
+
// headed/personal box still defers to its native login). Without this, an ACP
|
|
33
|
+
// launch inherited no setup-token and fell through to the version home's
|
|
34
|
+
// copied native `.credentials.json` — which expires in ~15h and 401s once its
|
|
35
|
+
// refresh chain dies, exactly the "logged out on a worker" report (PHNX-3681).
|
|
36
|
+
// A verified precedence test shows the env token wins over a present
|
|
37
|
+
// `.credentials.json`, so injecting here is sufficient — the stale file no
|
|
38
|
+
// longer decides the session.
|
|
28
39
|
const child = spawn(spec.command, spec.args, {
|
|
29
40
|
cwd: opts.cwd,
|
|
30
41
|
stdio: ['pipe', 'pipe', 'inherit'],
|
|
31
|
-
env:
|
|
42
|
+
env: buildExecEnv({ agent: opts.agent, cwd: opts.cwd, mode: opts.mode, effort: 'auto', interactive: true }),
|
|
32
43
|
});
|
|
33
44
|
const stream = ndJsonStream(Writable.toWeb(child.stdin), Readable.toWeb(child.stdout));
|
|
34
45
|
const client = buildClient(opts);
|
|
@@ -52,7 +52,7 @@ export const ACP_HARNESSES = {
|
|
|
52
52
|
},
|
|
53
53
|
// antigravity: no documented ACP support (May 2026).
|
|
54
54
|
// goose: ACP over HTTP via `goosed`, not a clean stdio subcommand.
|
|
55
|
-
// copilot
|
|
55
|
+
// copilot: excluded for now (not installed in the reference environment,
|
|
56
56
|
// no local verification possible).
|
|
57
57
|
// amp: not on the ACP agents list.
|
|
58
58
|
};
|
package/dist/lib/add-dir.js
CHANGED
|
@@ -33,11 +33,9 @@ export const ADD_DIR_STRATEGY = {
|
|
|
33
33
|
// No multi-root CLI surface today (single --dir / project path).
|
|
34
34
|
opencode: 'none',
|
|
35
35
|
gemini: 'none',
|
|
36
|
-
pi: 'none',
|
|
37
36
|
openclaw: 'none',
|
|
38
37
|
copilot: 'none',
|
|
39
38
|
amp: 'none',
|
|
40
|
-
kiro: 'none',
|
|
41
39
|
goose: 'none',
|
|
42
40
|
antigravity: 'none',
|
|
43
41
|
droid: 'none',
|
|
@@ -104,7 +104,7 @@ export declare const MANAGED_AGENT_IDS: AgentId[];
|
|
|
104
104
|
* `curl … | sh` / `brew install` script that carries NO version token — the
|
|
105
105
|
* installer can only ever fetch the *current* release, and the binary then keeps
|
|
106
106
|
* itself up to date in place (droid, grok, antigravity, cursor, hermes,
|
|
107
|
-
*
|
|
107
|
+
* goose). There is no semver to pin, so agents-cli must not model these as
|
|
108
108
|
* having multiple installable version-homes the way it does for npm-packaged
|
|
109
109
|
* agents (claude, codex, kimi, …).
|
|
110
110
|
*
|