@phnx-labs/agents-cli 1.22.102 → 1.22.104
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 +31 -0
- package/README.md +1 -1
- package/dist/bootstrap.js +15 -0
- package/dist/commands/accounts.js +2 -2
- package/dist/commands/computer.d.ts +100 -79
- package/dist/commands/computer.js +290 -830
- package/dist/commands/setup-computer.js +20 -1
- package/dist/commands/setup-secrets.d.ts +2 -2
- package/dist/commands/setup-secrets.js +1 -1
- package/dist/commands/view.js +4 -6
- package/dist/lib/account-catalog.d.ts +22 -1
- package/dist/lib/account-catalog.js +72 -38
- package/dist/lib/accounting/usage.js +18 -14
- package/dist/lib/agent-spec/agents.d.ts +1 -0
- package/dist/lib/agent-spec/agents.js +1 -1
- package/dist/lib/browser/drivers/ssh.js +1 -1
- package/dist/lib/computer/context.d.ts +83 -0
- package/dist/lib/computer/context.js +91 -0
- package/dist/lib/computer/policy.d.ts +46 -0
- package/dist/lib/computer/policy.js +160 -0
- package/dist/lib/computer/record.d.ts +38 -0
- package/dist/lib/computer/record.js +86 -0
- package/dist/lib/computer/sessions-list.js +6 -6
- package/dist/lib/computer-client.d.ts +150 -0
- package/dist/lib/computer-client.js +222 -0
- package/dist/lib/exec.js +35 -1
- package/dist/lib/harness/adapters/claude.d.ts +37 -0
- package/dist/lib/harness/adapters/claude.js +69 -0
- package/dist/lib/helper-download.d.ts +1 -1
- package/dist/lib/helper-download.js +1 -1
- package/dist/lib/helper-versions.d.ts +9 -4
- package/dist/lib/helper-versions.js +9 -9
- package/dist/lib/installations/shims.js +8 -4
- package/dist/lib/menubar/download-menubar.d.ts +2 -1
- package/dist/lib/menubar/download-menubar.js +2 -1
- package/dist/lib/menubar/install-menubar.d.ts +56 -0
- package/dist/lib/menubar/install-menubar.js +133 -17
- package/dist/lib/menubar/resolve-version.d.ts +82 -0
- package/dist/lib/menubar/resolve-version.js +133 -0
- package/dist/lib/profiles.js +13 -2
- package/dist/lib/secrets-client.d.ts +23 -3
- package/dist/lib/secrets-client.js +57 -57
- package/dist/lib/self-heal/checks/menubar-helper.d.ts +2 -0
- package/dist/lib/self-heal/checks/menubar-helper.js +21 -0
- package/dist/lib/self-heal/registry.js +3 -0
- package/dist/lib/self-heal/types.d.ts +1 -1
- package/dist/lib/session/db.js +1 -1
- package/dist/lib/sha256-asset.d.ts +2 -1
- package/dist/lib/sha256-asset.js +2 -1
- package/dist/lib/ssh-tunnel.d.ts +61 -0
- package/dist/lib/ssh-tunnel.js +105 -0
- package/dist/lib/summarizer/summarize.d.ts +2 -2
- package/dist/lib/summarizer/summarize.js +10 -3
- package/package.json +2 -3
- package/dist/commands/computer-actions.d.ts +0 -55
- package/dist/commands/computer-actions.js +0 -594
- package/dist/computer.d.ts +0 -2
- package/dist/computer.js +0 -7
- package/dist/lib/computer/actions.d.ts +0 -36
- package/dist/lib/computer/actions.js +0 -162
- package/dist/lib/computer/computer-rpc.d.ts +0 -39
- package/dist/lib/computer/computer-rpc.js +0 -447
- package/dist/lib/computer/des.d.ts +0 -1
- package/dist/lib/computer/des.js +0 -114
- package/dist/lib/computer/dispatch.d.ts +0 -10
- package/dist/lib/computer/dispatch.js +0 -133
- package/dist/lib/computer/download.d.ts +0 -54
- package/dist/lib/computer/download.js +0 -83
- package/dist/lib/computer/loop.d.ts +0 -62
- package/dist/lib/computer/loop.js +0 -98
- package/dist/lib/computer/model.d.ts +0 -44
- package/dist/lib/computer/model.js +0 -157
- package/dist/lib/computer/rfb-client.d.ts +0 -53
- package/dist/lib/computer/rfb-client.js +0 -562
- package/dist/lib/computer/ssh-tunnel.d.ts +0 -189
- package/dist/lib/computer/ssh-tunnel.js +0 -584
|
@@ -9,9 +9,10 @@
|
|
|
9
9
|
* Idempotent: re-running re-installs the current helper and re-checks trust.
|
|
10
10
|
*/
|
|
11
11
|
import os from 'node:os';
|
|
12
|
-
import { execFileSync } from 'node:child_process';
|
|
12
|
+
import { execFileSync, spawnSync } from 'node:child_process';
|
|
13
13
|
import chalk from 'chalk';
|
|
14
14
|
import { installComputerHelperMacLocal, activateComputerHelperMacLocal, probeComputerTrust, } from './computer.js';
|
|
15
|
+
import { resolveComputerBin, ComputerClientError } from '../lib/computer-client.js';
|
|
15
16
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
16
17
|
const ACCESSIBILITY_PANE = 'x-apple.systempreferences:com.apple.preference.security?Privacy_Accessibility';
|
|
17
18
|
const SCREEN_PANE = 'x-apple.systempreferences:com.apple.preference.security?Privacy_ScreenCapture';
|
|
@@ -36,6 +37,24 @@ export async function runComputerWizard() {
|
|
|
36
37
|
console.log(chalk.dim('On Windows, provision a remote host from a Mac/Linux box instead: `agents computer setup --device <device>`.'));
|
|
37
38
|
return false;
|
|
38
39
|
}
|
|
40
|
+
try {
|
|
41
|
+
resolveComputerBin();
|
|
42
|
+
}
|
|
43
|
+
catch (error) {
|
|
44
|
+
if (!(error instanceof ComputerClientError) || error.code !== 'COMPUTER_BIN_MISSING')
|
|
45
|
+
throw error;
|
|
46
|
+
console.log('Installing @phnx-labs/computer-cli@0.1.1…');
|
|
47
|
+
const installed = spawnSync('npm', ['install', '-g', '@phnx-labs/computer-cli@0.1.1'], { stdio: 'inherit' });
|
|
48
|
+
if (installed.status !== 0)
|
|
49
|
+
return false;
|
|
50
|
+
try {
|
|
51
|
+
resolveComputerBin();
|
|
52
|
+
}
|
|
53
|
+
catch {
|
|
54
|
+
console.error('Computer CLI installation did not produce an executable on PATH.');
|
|
55
|
+
return false;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
39
58
|
// 1. Download + verify + install the signed, notarized helper.
|
|
40
59
|
console.log(chalk.bold('Installing the Agents Computer helper...'));
|
|
41
60
|
try {
|
|
@@ -8,8 +8,8 @@
|
|
|
8
8
|
* `npm i -g`. Users do not set extra env vars.
|
|
9
9
|
*/
|
|
10
10
|
import type { Command } from 'commander';
|
|
11
|
-
export declare const SECRETS_CLI_PACKAGE = "@phnx-labs/secrets-cli@0.1.
|
|
12
|
-
export declare const INSTALL_HINT = "agents clis install secrets # or: npm i -g @phnx-labs/secrets-cli@0.1.
|
|
11
|
+
export declare const SECRETS_CLI_PACKAGE = "@phnx-labs/secrets-cli@0.1.4";
|
|
12
|
+
export declare const INSTALL_HINT = "agents clis install secrets # or: npm i -g @phnx-labs/secrets-cli@0.1.4";
|
|
13
13
|
export declare function setupSecretsPrefsPath(): string;
|
|
14
14
|
/** True when the standalone `secrets` executable resolves ($SECRETS_BIN or PATH). */
|
|
15
15
|
export declare function isSecretsCliInstalled(): boolean;
|
|
@@ -14,7 +14,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
14
14
|
import { getHistoryDir } from '../lib/state.js';
|
|
15
15
|
import { resolveSecretsBin, invocation, SecretsClientError, _resetSecretsClientForTest } from '../lib/secrets-client.js';
|
|
16
16
|
import { installCli, resolveCliManifest } from '../lib/cli-resources.js';
|
|
17
|
-
export const SECRETS_CLI_PACKAGE = '@phnx-labs/secrets-cli@0.1.
|
|
17
|
+
export const SECRETS_CLI_PACKAGE = '@phnx-labs/secrets-cli@0.1.4';
|
|
18
18
|
export const INSTALL_HINT = `agents clis install secrets # or: npm i -g ${SECRETS_CLI_PACKAGE}`;
|
|
19
19
|
export function setupSecretsPrefsPath() {
|
|
20
20
|
return path.join(getHistoryDir(), 'setup', 'secrets.json');
|
package/dist/commands/view.js
CHANGED
|
@@ -31,7 +31,7 @@ import { isCapable } from '../lib/capabilities.js';
|
|
|
31
31
|
import { discoverPlugins, pluginSupportsAgent } from '../lib/plugins/plugins.js';
|
|
32
32
|
import { getAgentsDir, getUserAgentsDir, getEffectivePromptcutsPath, readMergedPromptcuts, readMeta } from '../lib/state.js';
|
|
33
33
|
import { findNativeAccountByIdentity } from '../lib/account-registry.js';
|
|
34
|
-
import { accountListJson, loadAccountCatalog, renderAccountRows, secretsUnavailableNote } from '../lib/account-catalog.js';
|
|
34
|
+
import { ACCOUNT_LISTING_LEGEND, accountListJson, loadAccountCatalog, renderAccountRows, secretsUnavailableNote } from '../lib/account-catalog.js';
|
|
35
35
|
import { addSupported } from '../lib/accounts/add.js';
|
|
36
36
|
import { readInstallation } from '../lib/installations/store.js';
|
|
37
37
|
import { isAutoUpdateEnabledForAgent } from '../lib/installations/update-policy.js';
|
|
@@ -557,12 +557,10 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
557
557
|
}
|
|
558
558
|
if (filterAgentId) {
|
|
559
559
|
console.log(chalk.gray(` Add an account: agents accounts add ${filterAgentId} [name]`));
|
|
560
|
-
console.log(chalk.gray(' STATE: LIVE ready · LIMITED rate-limited · EXPIRED needs refresh · REVOKED needs login · UNVERIFIED unconfirmed · MISSING not provisioned · * stale usage\n'));
|
|
561
|
-
}
|
|
562
|
-
else {
|
|
563
|
-
// Overview also renders account tables (footer:false) — explain STATE there too.
|
|
564
|
-
console.log(chalk.gray(' STATE: LIVE ready · LIMITED rate-limited · EXPIRED needs refresh · REVOKED needs login · UNVERIFIED unconfirmed · MISSING not provisioned · * stale usage\n'));
|
|
565
560
|
}
|
|
561
|
+
// Overview renders the same account rows with footer:false, so both paths
|
|
562
|
+
// need the legend the shared renderer would otherwise have printed.
|
|
563
|
+
console.log(chalk.gray(` ${ACCOUNT_LISTING_LEGEND}\n`));
|
|
566
564
|
}
|
|
567
565
|
if (versionManaged.length > 0 && viewOpts?.versions) {
|
|
568
566
|
// Calculate column widths across all agents for alignment
|
|
@@ -201,8 +201,29 @@ export declare function loadAccountCatalog(): Promise<AccountCatalog>;
|
|
|
201
201
|
*/
|
|
202
202
|
export declare function secretsUnavailableNote(catalog: Pick<AccountCatalog, 'secretsUnavailable'>): string | null;
|
|
203
203
|
export declare function accountListJson(native: NativeAccountCatalogRow[], providers?: ProviderAccountCatalogRow[], harness?: AgentId): AccountListJson;
|
|
204
|
-
|
|
204
|
+
/**
|
|
205
|
+
* A state worth an operator's attention, rendered at the END of the row.
|
|
206
|
+
* `live`, `ready`, `unverified` and `per-device` are the ordinary cases: they
|
|
207
|
+
* were on every row of the old table and pushed it past the terminal width
|
|
208
|
+
* without telling anyone anything. They stay in `accounts list --fleet`,
|
|
209
|
+
* `accounts view <name>` and `--json`.
|
|
210
|
+
*/
|
|
211
|
+
export declare function verdictNote(verdict: AccountVerdict): string | null;
|
|
212
|
+
/**
|
|
213
|
+
* Device coverage, but only when it SPLITS the fleet: some boxes can use the
|
|
214
|
+
* account and some cannot. Full coverage is the ordinary case, and "none of
|
|
215
|
+
* them" is already exactly what the row's state note says, so both render
|
|
216
|
+
* nothing — `accounts list --fleet` is the per-device table.
|
|
217
|
+
*/
|
|
218
|
+
export declare function coverageNote(row: NativeAccountCatalogRow, localDevice: string): string | null;
|
|
205
219
|
export declare const OVERVIEW_MAX_USAGE_WINDOWS = 2;
|
|
220
|
+
/**
|
|
221
|
+
* The one line under an account listing. It explains the two marks a row can
|
|
222
|
+
* carry and names where the columns the listing no longer prints — identity,
|
|
223
|
+
* per-device state — still live. Shared with `agents view` so the two surfaces
|
|
224
|
+
* cannot drift.
|
|
225
|
+
*/
|
|
226
|
+
export declare const ACCOUNT_LISTING_LEGEND = "* stale usage \u00B7 a healthy account carries no state \u00B7 identity + per-box state: agents accounts list --fleet";
|
|
206
227
|
/** Shared text renderer used by both `accounts list` and account-first `view`. */
|
|
207
228
|
export declare function renderAccountRows(rows: NativeAccountCatalogRow[], opts: {
|
|
208
229
|
heading?: boolean;
|
|
@@ -367,35 +367,55 @@ export function accountListJson(native, providers = [], harness) {
|
|
|
367
367
|
],
|
|
368
368
|
};
|
|
369
369
|
}
|
|
370
|
-
|
|
370
|
+
/**
|
|
371
|
+
* A state worth an operator's attention, rendered at the END of the row.
|
|
372
|
+
* `live`, `ready`, `unverified` and `per-device` are the ordinary cases: they
|
|
373
|
+
* were on every row of the old table and pushed it past the terminal width
|
|
374
|
+
* without telling anyone anything. They stay in `accounts list --fleet`,
|
|
375
|
+
* `accounts view <name>` and `--json`.
|
|
376
|
+
*/
|
|
377
|
+
export function verdictNote(verdict) {
|
|
371
378
|
if (verdict === 'rate_limited')
|
|
372
|
-
return '
|
|
373
|
-
|
|
379
|
+
return chalk.yellow('rate-limited');
|
|
380
|
+
if (verdict === 'expired' || verdict === 'revoked' || verdict === 'missing') {
|
|
381
|
+
return chalk.red(verdict);
|
|
382
|
+
}
|
|
383
|
+
return null;
|
|
374
384
|
}
|
|
375
|
-
|
|
385
|
+
/**
|
|
386
|
+
* Device coverage, but only when it SPLITS the fleet: some boxes can use the
|
|
387
|
+
* account and some cannot. Full coverage is the ordinary case, and "none of
|
|
388
|
+
* them" is already exactly what the row's state note says, so both render
|
|
389
|
+
* nothing — `accounts list --fleet` is the per-device table.
|
|
390
|
+
*/
|
|
391
|
+
export function coverageNote(row, localDevice) {
|
|
376
392
|
const names = [...new Set(row.devices.map((device) => device.device))];
|
|
377
393
|
// Peers on an older release do not publish slot verdicts, so the only
|
|
378
|
-
// observation is this box —
|
|
394
|
+
// observation is this box — that is a gap in what we can see, not a gap in
|
|
395
|
+
// provisioning, so it is not an alarm.
|
|
379
396
|
if (names.length === 1 && names[0] === localDevice)
|
|
380
|
-
return
|
|
381
|
-
// Per-device accounts are provisioned per box;
|
|
382
|
-
// rather than a
|
|
397
|
+
return null;
|
|
398
|
+
// Per-device accounts are provisioned per box; name the boxes it is NOT on
|
|
399
|
+
// rather than a usable/total fraction — the fraction would hide that each box
|
|
383
400
|
// is its own account.
|
|
384
401
|
if (row.provisioning === 'per-device') {
|
|
385
|
-
const
|
|
386
|
-
|
|
387
|
-
.map((device) => device.device);
|
|
388
|
-
return present.join(', ') || 'this box';
|
|
402
|
+
const absent = row.devices.filter((device) => device.verdict === 'missing').map((device) => device.device);
|
|
403
|
+
return absent.length > 0 && absent.length < row.devices.length ? `not on ${absent.join(', ')}` : null;
|
|
389
404
|
}
|
|
390
405
|
const provisioned = row.devices.filter((device) => device.verdict !== 'missing').length;
|
|
391
|
-
if (provisioned === 0)
|
|
392
|
-
return '—';
|
|
393
406
|
const usable = row.devices.filter((device) => device.verdict === 'live' || device.verdict === 'rate_limited' || device.verdict === 'unverified').length;
|
|
394
|
-
if (usable === provisioned)
|
|
395
|
-
return
|
|
396
|
-
return `on ${usable} of ${provisioned} boxes`;
|
|
407
|
+
if (usable === 0 || usable === provisioned)
|
|
408
|
+
return null;
|
|
409
|
+
return `usable on ${usable} of ${provisioned} boxes`;
|
|
397
410
|
}
|
|
398
411
|
export const OVERVIEW_MAX_USAGE_WINDOWS = 2;
|
|
412
|
+
/**
|
|
413
|
+
* The one line under an account listing. It explains the two marks a row can
|
|
414
|
+
* carry and names where the columns the listing no longer prints — identity,
|
|
415
|
+
* per-device state — still live. Shared with `agents view` so the two surfaces
|
|
416
|
+
* cannot drift.
|
|
417
|
+
*/
|
|
418
|
+
export const ACCOUNT_LISTING_LEGEND = '* stale usage · a healthy account carries no state · identity + per-box state: agents accounts list --fleet';
|
|
399
419
|
function usageText(row, maxWindows) {
|
|
400
420
|
if (row.usageSnapshot) {
|
|
401
421
|
const usageInfo = { snapshot: row.usageSnapshot, error: row.usageError ?? null };
|
|
@@ -411,13 +431,35 @@ function usageText(row, maxWindows) {
|
|
|
411
431
|
const percent = `${row.usage.usedPercent}%${row.usage.stale ? '*' : ''}`;
|
|
412
432
|
return `${renderBar(row.usage.usedPercent, LISTING_USAGE_BAR_LEN)} ${percent}`;
|
|
413
433
|
}
|
|
434
|
+
/**
|
|
435
|
+
* True only when the USAGE cell is certain to print a throttle marker, so the
|
|
436
|
+
* `rate-limited` note would say the same thing twice. Mirrors `usageText`
|
|
437
|
+
* branch for branch: with a snapshot, `formatUsageSummary` appends
|
|
438
|
+
* 'out of credits' or 'session-limited (…)' exactly for these `unavailable`
|
|
439
|
+
* reasons; without one, `usageText` prints 'limited' or 'no credits'. A verdict
|
|
440
|
+
* thrown by a maxed WINDOW is deliberately NOT here: the overview caps the cell
|
|
441
|
+
* at `OVERVIEW_MAX_USAGE_WINDOWS`, so the window that tripped it can be hidden
|
|
442
|
+
* behind the `+N` count with no color at all, and the note is the only signal.
|
|
443
|
+
*/
|
|
444
|
+
function usageCellNamesThrottle(row) {
|
|
445
|
+
if (row.usageSnapshot) {
|
|
446
|
+
const unavailable = row.usageSnapshot.unavailable;
|
|
447
|
+
return unavailable?.reason === 'out_of_credits'
|
|
448
|
+
|| (unavailable?.reason === 'session_limit' && !!unavailable.resetsAt);
|
|
449
|
+
}
|
|
450
|
+
return row.usage?.status === 'out_of_credits'
|
|
451
|
+
|| (row.usage?.status === 'rate_limited' && (row.usage.usedPercent === null || row.usage.usedPercent === undefined));
|
|
452
|
+
}
|
|
414
453
|
function nativeLine(row, localDevice, maxWindows) {
|
|
415
454
|
return {
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
455
|
+
// A discovered login nobody has named is still identified by what it is:
|
|
456
|
+
// the row no longer carries an IDENTITY column, so the identity IS the name.
|
|
457
|
+
name: row.name ?? row.identityLabel,
|
|
458
|
+
notes: [
|
|
459
|
+
usageCellNamesThrottle(row) && row.verdict === 'rate_limited' ? null : verdictNote(row.verdict),
|
|
460
|
+
coverageNote(row, localDevice),
|
|
461
|
+
row.fix ? chalk.gray(`fix: ${row.fix}`) : null,
|
|
462
|
+
].filter((note) => !!note),
|
|
421
463
|
usage: usageText(row, maxWindows),
|
|
422
464
|
isDefault: row.isDefault,
|
|
423
465
|
};
|
|
@@ -425,32 +467,27 @@ function nativeLine(row, localDevice, maxWindows) {
|
|
|
425
467
|
function providerLine(row, harness) {
|
|
426
468
|
return {
|
|
427
469
|
name: row.name,
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
470
|
+
notes: [
|
|
471
|
+
verdictNote(row.verdict),
|
|
472
|
+
row.fix ? chalk.gray(`fix: ${row.fix}`) : null,
|
|
473
|
+
].filter((note) => !!note),
|
|
432
474
|
usage: '',
|
|
433
475
|
isDefault: harness ? row.defaultFor.includes(harness) : false,
|
|
434
476
|
};
|
|
435
477
|
}
|
|
436
|
-
function formatListingLine(line, nameW,
|
|
478
|
+
function formatListingLine(line, nameW, usageW) {
|
|
437
479
|
const marker = line.isDefault ? '*' : ' ';
|
|
438
|
-
const fix = line.fix ? `fix: ${line.fix}` : '';
|
|
439
480
|
return (` ${chalk.green(marker)} ${chalk.cyan(line.name.padEnd(nameW))} `
|
|
440
|
-
+ `${line.identityLabel.padEnd(identityW)} `
|
|
441
|
-
+ `${verdictText(line.verdict).padEnd(stateW)} `
|
|
442
|
-
+ `${line.where.padEnd(whereW)} `
|
|
443
481
|
+ `${padToWidth(line.usage, usageW)} `
|
|
444
|
-
+
|
|
482
|
+
+ line.notes.join(chalk.gray(' · '))).trimEnd();
|
|
445
483
|
}
|
|
446
484
|
function pushGroup(out, title, lines, widths, harnessHeadings) {
|
|
447
485
|
if (lines.length === 0)
|
|
448
486
|
return;
|
|
449
487
|
if (harnessHeadings)
|
|
450
488
|
out.push(chalk.bold(title));
|
|
451
|
-
out.push(chalk.gray(` ${'ACCOUNT'.padEnd(widths.nameW + 2)}${'IDENTITY'.padEnd(widths.identityW + 2)}${'STATE'.padEnd(widths.stateW + 2)}${'WHERE'.padEnd(widths.whereW + 2)}${'USAGE'.padEnd(widths.usageW + 2)}FIX`));
|
|
452
489
|
for (const line of lines) {
|
|
453
|
-
out.push(formatListingLine(line, widths.nameW, widths.
|
|
490
|
+
out.push(formatListingLine(line, widths.nameW, widths.usageW));
|
|
454
491
|
}
|
|
455
492
|
out.push('');
|
|
456
493
|
}
|
|
@@ -501,9 +538,6 @@ export function renderAccountRows(rows, opts) {
|
|
|
501
538
|
const allLines = [...grouped.values()].flat().concat(orphans);
|
|
502
539
|
const widths = {
|
|
503
540
|
nameW: Math.max(7, ...allLines.map((line) => line.name.length)),
|
|
504
|
-
identityW: Math.max(8, ...allLines.map((line) => line.identityLabel.length)),
|
|
505
|
-
stateW: Math.max(5, ...allLines.map((line) => verdictText(line.verdict).length)),
|
|
506
|
-
whereW: Math.max(5, ...allLines.map((line) => line.where.length)),
|
|
507
541
|
usageW: Math.max(5, ...allLines.map((line) => stringWidth(line.usage))),
|
|
508
542
|
};
|
|
509
543
|
for (const [harness, lines] of grouped) {
|
|
@@ -519,7 +553,7 @@ export function renderAccountRows(rows, opts) {
|
|
|
519
553
|
const count = visibleNative.filter((row) => !!row.fix).length
|
|
520
554
|
+ visibleProviders.filter((row) => !!row.fix).length;
|
|
521
555
|
out.push(chalk.gray(`${count} accounts need you · add: agents accounts add <harness>`));
|
|
522
|
-
out.push(chalk.gray(
|
|
556
|
+
out.push(chalk.gray(ACCOUNT_LISTING_LEGEND));
|
|
523
557
|
}
|
|
524
558
|
return out.join('\n').trimEnd();
|
|
525
559
|
}
|
|
@@ -624,12 +624,13 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
624
624
|
parts.push(chalk.gray(plan.padEnd(planWidth)));
|
|
625
625
|
}
|
|
626
626
|
if (snapshot) {
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
627
|
+
// A blocking marker renders AFTER the bars: leading it pushed every gauge
|
|
628
|
+
// out of its column, so one throttled account misaligned the whole table.
|
|
629
|
+
const blocked = snapshot.unavailable?.reason === 'out_of_credits'
|
|
630
|
+
? chalk.red('out of credits')
|
|
631
|
+
: snapshot.unavailable?.reason === 'session_limit' && snapshot.unavailable.resetsAt
|
|
632
|
+
? chalk.yellow(`session-limited (${formatResetHint(snapshot.unavailable.resetsAt)})`)
|
|
633
|
+
: null;
|
|
633
634
|
// Compact rows show BLOCKING windows — the same set
|
|
634
635
|
// deriveUsageStatusFromSnapshot uses for the rate-limited badge — so an
|
|
635
636
|
// account throttled by its month window (Droid meters on 5h/week/month)
|
|
@@ -698,6 +699,8 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
|
698
699
|
else if (opts?.unverified) {
|
|
699
700
|
parts.push(chalk.yellow('unverified'));
|
|
700
701
|
}
|
|
702
|
+
if (blocked)
|
|
703
|
+
parts.push(blocked);
|
|
701
704
|
}
|
|
702
705
|
else if (opts?.headless) {
|
|
703
706
|
// No bars at all: still name the scope gap so "usage pending" is not
|
|
@@ -2357,7 +2360,7 @@ function formatAgeShort(diffMs) {
|
|
|
2357
2360
|
* Staleness suffix for a last-known window the freshness gate dropped. A window
|
|
2358
2361
|
* whose reset/period boundary passed while the sample itself is still inside its
|
|
2359
2362
|
* `windowMinutes` rolled OVER — the number describes a period that is done, so
|
|
2360
|
-
* name it ("period ended 1h
|
|
2363
|
+
* name it ("period ended 1h", e.g. Grok's weekly billing period). A window
|
|
2361
2364
|
* that aged past its own `windowMinutes` (Claude's 5h session read never
|
|
2362
2365
|
* refreshed in time) is a stale sample of a still-rolling window, so report the
|
|
2363
2366
|
* capture age ("6h old"). Falls back to the reset age, then a bare "stale".
|
|
@@ -2368,25 +2371,26 @@ function formatStaleWindowSuffix(window, capturedAt, now) {
|
|
|
2368
2371
|
window.windowMinutes !== null &&
|
|
2369
2372
|
capturedAt.getTime() + window.windowMinutes * 60 * 1000 <= now.getTime();
|
|
2370
2373
|
if (resetPassed && !captureExpired) {
|
|
2371
|
-
return `
|
|
2374
|
+
return `period ended ${formatAgeShort(now.getTime() - window.resetsAt.getTime())}`;
|
|
2372
2375
|
}
|
|
2373
2376
|
if (capturedAt)
|
|
2374
2377
|
return `${formatAgeShort(now.getTime() - capturedAt.getTime())} old`;
|
|
2375
2378
|
if (resetPassed)
|
|
2376
|
-
return `
|
|
2379
|
+
return `period ended ${formatAgeShort(now.getTime() - window.resetsAt.getTime())}`;
|
|
2377
2380
|
return 'stale';
|
|
2378
2381
|
}
|
|
2379
2382
|
/**
|
|
2380
|
-
* Render a dropped-but-last-known window as "S: ▍░░░░ 30
|
|
2381
|
-
* and percentage exactly as a live bar, then
|
|
2382
|
-
*
|
|
2383
|
-
*
|
|
2383
|
+
* Render a dropped-but-last-known window as "S: ▍░░░░ 30%* (6h old)": the gauge
|
|
2384
|
+
* and percentage exactly as a live bar, then the `*` stale marker the listing
|
|
2385
|
+
* legend explains and a dim age, so the number is always visible and
|
|
2386
|
+
* unmistakably not current. VIEW-ONLY — these windows are never in
|
|
2387
|
+
* `snapshot.windows`, so routing never sees them.
|
|
2384
2388
|
*/
|
|
2385
2389
|
function renderStaleUsageWindow(window, capturedAt, shortLabel, now) {
|
|
2386
2390
|
const bar = renderCompactUsageBar(window.usedPercent);
|
|
2387
2391
|
const pct = colorUsage(`${Math.round(window.usedPercent)}%`, window.usedPercent);
|
|
2388
2392
|
const suffix = formatStaleWindowSuffix(window, capturedAt, now);
|
|
2389
|
-
return `${chalk.gray(`${shortLabel}:`)} ${bar} ${pct}
|
|
2393
|
+
return `${chalk.gray(`${shortLabel}:`)} ${bar} ${pct}${chalk.dim('*')} ${chalk.dim(`(${suffix})`)}`;
|
|
2390
2394
|
}
|
|
2391
2395
|
/** Format a reset timestamp as a human-readable relative or absolute time. */
|
|
2392
2396
|
function formatResetAt(date) {
|
|
@@ -134,7 +134,7 @@ export function findInPath(command, options = {}) {
|
|
|
134
134
|
if (process.platform !== 'win32')
|
|
135
135
|
fs.accessSync(full, fs.constants.X_OK);
|
|
136
136
|
const native = resolveNativeBinaryPath(command, full, options);
|
|
137
|
-
if (native)
|
|
137
|
+
if (native && (!options.accept || options.accept(native)))
|
|
138
138
|
return native;
|
|
139
139
|
}
|
|
140
140
|
catch {
|
|
@@ -11,7 +11,7 @@ export { shellQuote };
|
|
|
11
11
|
// The `ssh -L` tunnel spawn is shared with `agents computer --device`; it lives in
|
|
12
12
|
// the single ssh-tunnel helper. Calling it with no options preserves this
|
|
13
13
|
// driver's original foreground, stderr-captured behavior exactly.
|
|
14
|
-
import { startSSHTunnel } from '../../
|
|
14
|
+
import { startSSHTunnel } from '../../ssh-tunnel.js';
|
|
15
15
|
import { encodePwshBase64 } from '../../pwsh.js';
|
|
16
16
|
/** Lifecycle for a registry-driven tunnel vs a profile the daemon launched. */
|
|
17
17
|
export function persistRemoteLifecycle(persistRemote) {
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.ts — everything agents-cli knows that the standalone `computer`
|
|
3
|
+
* engine cannot work out for itself, serialized as one JSON object onto fd 3.
|
|
4
|
+
*
|
|
5
|
+
* This is the whole contract of the consumer half, and it is deliberately four
|
|
6
|
+
* fields wide. The engine accepts exactly this shape:
|
|
7
|
+
*
|
|
8
|
+
* version `1`. The engine matches on it; a field added later must be
|
|
9
|
+
* optional so an older engine keeps working.
|
|
10
|
+
* permissions which apps are allowed, derived from `Computer(<bundle-id>)`
|
|
11
|
+
* rules in the agents permissions resource layer. The engine
|
|
12
|
+
* would have to re-learn resource layering to compute this.
|
|
13
|
+
* peers which executables the daemon accepts a connection from.
|
|
14
|
+
* target the `--device <name>` target, resolved against the fleet:
|
|
15
|
+
* devices registry, ssh identity, platform. The engine matches
|
|
16
|
+
* its own `--device <alias>` against this instead of reading a
|
|
17
|
+
* registry it does not have. Absent for a local invocation.
|
|
18
|
+
* session who is acting — actor id and agent session — so an action
|
|
19
|
+
* lands in the right session history.
|
|
20
|
+
*
|
|
21
|
+
* WHAT IS DELIBERATELY NOT HERE. The transport (`COMPUTER_HELPER_TCP`,
|
|
22
|
+
* `COMPUTER_HELPER_VNC`, `COMPUTER_HELPER_SOCKET`), the remote helper's auth
|
|
23
|
+
* token, and the policy-file paths are the ENGINE's: it provisions the Windows
|
|
24
|
+
* helper, mints and stores the token, opens the `ssh -L` tunnel and hydrates its
|
|
25
|
+
* own transport from the state it wrote. agents-cli publishing a loopback
|
|
26
|
+
* endpoint here (or on `COMPUTER_HELPER_TCP`) would be a second, drifting copy
|
|
27
|
+
* of that answer — and a copy without the token, which the daemon rejects with
|
|
28
|
+
* `auth_failed`. Service-manager safety (launchd/systemd registration under a
|
|
29
|
+
* redirected HOME) is likewise the standalone's own: it inherits `HOME` and
|
|
30
|
+
* `AGENTS_REAL_HOME` and renders its own manifest, so agents-cli neither
|
|
31
|
+
* computes a label nor issues a verdict for it.
|
|
32
|
+
*
|
|
33
|
+
* The context is PUSHED (written and closed) rather than exposed as a callback,
|
|
34
|
+
* so the engine never re-enters agents-cli and there is exactly one direction of
|
|
35
|
+
* dependency.
|
|
36
|
+
*/
|
|
37
|
+
/** A `--device <name>` target, resolved against the fleet. */
|
|
38
|
+
export interface ComputerTargetContext {
|
|
39
|
+
/** The device name as the user typed it. */
|
|
40
|
+
alias: string;
|
|
41
|
+
/** `user@host`, already validated against ssh option injection. */
|
|
42
|
+
host: string;
|
|
43
|
+
user: string;
|
|
44
|
+
/** Bare host — the ssh-config Host name or address, without the user. */
|
|
45
|
+
hostname: string;
|
|
46
|
+
platform: string;
|
|
47
|
+
/** Per-device ssh identity flags, in argv order. Possibly empty. */
|
|
48
|
+
sshArgs: string[];
|
|
49
|
+
}
|
|
50
|
+
/** Who is acting, so the engine can stamp the action it reports back. */
|
|
51
|
+
export interface ComputerSessionContext {
|
|
52
|
+
sessionId?: string;
|
|
53
|
+
launchId?: string;
|
|
54
|
+
actor: string;
|
|
55
|
+
}
|
|
56
|
+
export interface ComputerContext {
|
|
57
|
+
version: 1;
|
|
58
|
+
permissions?: {
|
|
59
|
+
allow: string[];
|
|
60
|
+
};
|
|
61
|
+
peers: {
|
|
62
|
+
allow: string[];
|
|
63
|
+
};
|
|
64
|
+
target?: ComputerTargetContext;
|
|
65
|
+
session: ComputerSessionContext;
|
|
66
|
+
}
|
|
67
|
+
export interface BuildContextOptions {
|
|
68
|
+
/** `--device <name>`, if given. */
|
|
69
|
+
device?: string;
|
|
70
|
+
/** Direct host targeting, which bypasses fleet resolution. */
|
|
71
|
+
host?: string;
|
|
72
|
+
/** Resolved path of the standalone executable, for the peer allow list. */
|
|
73
|
+
computerBin?: string;
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Build the context handed to the engine on fd 3.
|
|
77
|
+
*
|
|
78
|
+
* `device` resolution goes through the shared fleet resolver and keeps the
|
|
79
|
+
* Windows expectation the computer subsystem has always enforced — a
|
|
80
|
+
* `--device` pointing at a Mac gets the same refusal as before, from the fleet
|
|
81
|
+
* layer that can actually see the device's platform.
|
|
82
|
+
*/
|
|
83
|
+
export declare function buildComputerContext(opts?: BuildContextOptions): Promise<ComputerContext>;
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* context.ts — everything agents-cli knows that the standalone `computer`
|
|
3
|
+
* engine cannot work out for itself, serialized as one JSON object onto fd 3.
|
|
4
|
+
*
|
|
5
|
+
* This is the whole contract of the consumer half, and it is deliberately four
|
|
6
|
+
* fields wide. The engine accepts exactly this shape:
|
|
7
|
+
*
|
|
8
|
+
* version `1`. The engine matches on it; a field added later must be
|
|
9
|
+
* optional so an older engine keeps working.
|
|
10
|
+
* permissions which apps are allowed, derived from `Computer(<bundle-id>)`
|
|
11
|
+
* rules in the agents permissions resource layer. The engine
|
|
12
|
+
* would have to re-learn resource layering to compute this.
|
|
13
|
+
* peers which executables the daemon accepts a connection from.
|
|
14
|
+
* target the `--device <name>` target, resolved against the fleet:
|
|
15
|
+
* devices registry, ssh identity, platform. The engine matches
|
|
16
|
+
* its own `--device <alias>` against this instead of reading a
|
|
17
|
+
* registry it does not have. Absent for a local invocation.
|
|
18
|
+
* session who is acting — actor id and agent session — so an action
|
|
19
|
+
* lands in the right session history.
|
|
20
|
+
*
|
|
21
|
+
* WHAT IS DELIBERATELY NOT HERE. The transport (`COMPUTER_HELPER_TCP`,
|
|
22
|
+
* `COMPUTER_HELPER_VNC`, `COMPUTER_HELPER_SOCKET`), the remote helper's auth
|
|
23
|
+
* token, and the policy-file paths are the ENGINE's: it provisions the Windows
|
|
24
|
+
* helper, mints and stores the token, opens the `ssh -L` tunnel and hydrates its
|
|
25
|
+
* own transport from the state it wrote. agents-cli publishing a loopback
|
|
26
|
+
* endpoint here (or on `COMPUTER_HELPER_TCP`) would be a second, drifting copy
|
|
27
|
+
* of that answer — and a copy without the token, which the daemon rejects with
|
|
28
|
+
* `auth_failed`. Service-manager safety (launchd/systemd registration under a
|
|
29
|
+
* redirected HOME) is likewise the standalone's own: it inherits `HOME` and
|
|
30
|
+
* `AGENTS_REAL_HOME` and renders its own manifest, so agents-cli neither
|
|
31
|
+
* computes a label nor issues a verdict for it.
|
|
32
|
+
*
|
|
33
|
+
* The context is PUSHED (written and closed) rather than exposed as a callback,
|
|
34
|
+
* so the engine never re-enters agents-cli and there is exactly one direction of
|
|
35
|
+
* dependency.
|
|
36
|
+
*/
|
|
37
|
+
import { resolveActor } from '../actor.js';
|
|
38
|
+
import { loadComputerAllowList, loadDefaultPeers } from './policy.js';
|
|
39
|
+
import { resolveRemoteDevice } from '../ssh-tunnel.js';
|
|
40
|
+
/**
|
|
41
|
+
* Which agent session is acting. Same precedence the admission cache used
|
|
42
|
+
* before the extraction — the harness-native id first, then agents' own.
|
|
43
|
+
*/
|
|
44
|
+
function agentSessionId(env = process.env) {
|
|
45
|
+
return env.CODEX_THREAD_ID
|
|
46
|
+
|| env.CLAUDE_CODE_SESSION_ID
|
|
47
|
+
|| env.CLAUDE_SESSION_ID
|
|
48
|
+
|| env.AGENTS_SESSION_ID
|
|
49
|
+
|| env.AGENT_SESSION_ID
|
|
50
|
+
|| env.AGENTS_RUN_ID
|
|
51
|
+
|| undefined;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Build the context handed to the engine on fd 3.
|
|
55
|
+
*
|
|
56
|
+
* `device` resolution goes through the shared fleet resolver and keeps the
|
|
57
|
+
* Windows expectation the computer subsystem has always enforced — a
|
|
58
|
+
* `--device` pointing at a Mac gets the same refusal as before, from the fleet
|
|
59
|
+
* layer that can actually see the device's platform.
|
|
60
|
+
*/
|
|
61
|
+
export async function buildComputerContext(opts = {}) {
|
|
62
|
+
let target;
|
|
63
|
+
if (opts.device) {
|
|
64
|
+
const resolved = await resolveRemoteDevice(opts.device, {
|
|
65
|
+
expectPlatform: 'windows',
|
|
66
|
+
forWhat: '`agents computer --device` drives the Windows computer-helper daemon, so it',
|
|
67
|
+
});
|
|
68
|
+
target = {
|
|
69
|
+
alias: opts.device,
|
|
70
|
+
host: resolved.target,
|
|
71
|
+
user: resolved.user,
|
|
72
|
+
hostname: resolved.host,
|
|
73
|
+
platform: resolved.device.platform,
|
|
74
|
+
sshArgs: resolved.identityArgs,
|
|
75
|
+
};
|
|
76
|
+
}
|
|
77
|
+
return {
|
|
78
|
+
version: 1,
|
|
79
|
+
...(!opts.device && !opts.host && !process.env.COMPUTER_HELPER_TCP && !process.env.COMPUTER_HELPER_VNC
|
|
80
|
+
? { permissions: { allow: loadComputerAllowList() } } : {}),
|
|
81
|
+
peers: { allow: loadDefaultPeers({ computerBin: opts.computerBin }) },
|
|
82
|
+
// Spread rather than assigned: a local invocation must not ship a `target`
|
|
83
|
+
// key at all, so the engine never has to distinguish absent from null.
|
|
84
|
+
...(target ? { target } : {}),
|
|
85
|
+
session: {
|
|
86
|
+
sessionId: agentSessionId(),
|
|
87
|
+
launchId: process.env.AGENT_LAUNCH_ID,
|
|
88
|
+
actor: resolveActor().id,
|
|
89
|
+
},
|
|
90
|
+
};
|
|
91
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
/** Resolve Agents permission groups and caller identities for the standalone
|
|
2
|
+
* engine. The engine alone writes helper policy and peer files. */
|
|
3
|
+
export declare function loadComputerAllowList(): string[];
|
|
4
|
+
/**
|
|
5
|
+
* Default peer set: the standalone `computer` executable, this `agents` CLI's
|
|
6
|
+
* own runtime, plus Rush.app if it's installed. realpath() the symlink chain so
|
|
7
|
+
* we record the on-disk path the helper will see via proc_pidpath, not the shim
|
|
8
|
+
* path.
|
|
9
|
+
*
|
|
10
|
+
* The standalone's path is the one that changed with PHNX-4075: the daemon's
|
|
11
|
+
* caller is now the engine process, not this CLI. `agents`' own execPath stays
|
|
12
|
+
* on the list because the engine may be a `.js` bin run through this same
|
|
13
|
+
* runtime (`invocation()` in computer-client.ts), in which case proc_pidpath
|
|
14
|
+
* still reports the runtime.
|
|
15
|
+
*
|
|
16
|
+
* Why path-based instead of codesign-team-id? The agents CLI is unsigned
|
|
17
|
+
* today (npm distribution), and even if we sign Rush.app the team-id
|
|
18
|
+
* check would need a separate roundtrip. Path is concrete and fast; the
|
|
19
|
+
* daemon already runs as the user so anyone who can swap a binary at
|
|
20
|
+
* these paths can do worse via other means.
|
|
21
|
+
*/
|
|
22
|
+
export declare function loadDefaultPeers(opts?: {
|
|
23
|
+
computerBin?: string;
|
|
24
|
+
}): string[];
|
|
25
|
+
/**
|
|
26
|
+
* Parse a `host:port` VNC endpoint, defaulting the port to 5901. Pure.
|
|
27
|
+
*
|
|
28
|
+
* Kept on the consumer side because the `--vnc` FLAG is parsed here — the
|
|
29
|
+
* platform gate has to know whether a remote desktop was named before the
|
|
30
|
+
* engine is ever spawned (see `shouldBlockOffPlatform`). The RFB protocol
|
|
31
|
+
* implementation itself went to the engine.
|
|
32
|
+
*/
|
|
33
|
+
export declare function parseVncEndpoint(raw: string | undefined): {
|
|
34
|
+
host: string;
|
|
35
|
+
port: number;
|
|
36
|
+
} | null;
|
|
37
|
+
export declare function resolveTcpEndpoint(): {
|
|
38
|
+
host: string;
|
|
39
|
+
port: number;
|
|
40
|
+
token: string | null;
|
|
41
|
+
} | null;
|
|
42
|
+
export declare function resolveVncEndpoint(): {
|
|
43
|
+
host: string;
|
|
44
|
+
port: number;
|
|
45
|
+
password: string;
|
|
46
|
+
} | null;
|