@phnx-labs/agents-cli 1.20.68 → 1.20.69
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 +45 -0
- package/README.md +11 -1
- package/dist/bin/agents +0 -0
- package/dist/commands/computer.d.ts +26 -0
- package/dist/commands/computer.js +173 -149
- package/dist/commands/exec.d.ts +18 -0
- package/dist/commands/exec.js +88 -6
- package/dist/commands/fork.d.ts +9 -0
- package/dist/commands/fork.js +67 -0
- package/dist/commands/run-account-picker.d.ts +14 -0
- package/dist/commands/run-account-picker.js +131 -0
- package/dist/commands/setup-browser.d.ts +18 -0
- package/dist/commands/setup-browser.js +142 -0
- package/dist/commands/setup-computer.d.ts +19 -0
- package/dist/commands/setup-computer.js +133 -0
- package/dist/commands/setup-share.d.ts +17 -0
- package/dist/commands/setup-share.js +85 -0
- package/dist/commands/setup.d.ts +1 -1
- package/dist/commands/setup.js +58 -1
- package/dist/commands/share.d.ts +15 -0
- package/dist/commands/share.js +10 -4
- package/dist/commands/ssh.js +164 -0
- package/dist/commands/view.d.ts +3 -14
- package/dist/commands/view.js +27 -28
- package/dist/index.js +2 -1
- package/dist/lib/agents.d.ts +10 -0
- package/dist/lib/agents.js +32 -0
- package/dist/lib/auth-health.d.ts +103 -0
- package/dist/lib/auth-health.js +232 -0
- package/dist/lib/browser/chrome.d.ts +9 -0
- package/dist/lib/browser/chrome.js +22 -0
- package/dist/lib/computer/download.d.ts +51 -0
- package/dist/lib/computer/download.js +145 -0
- package/dist/lib/computer-rpc.js +9 -3
- package/dist/lib/devices/connect.d.ts +15 -0
- package/dist/lib/devices/connect.js +17 -5
- package/dist/lib/devices/terminfo.d.ts +44 -0
- package/dist/lib/devices/terminfo.js +167 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/rotate.d.ts +12 -8
- package/dist/lib/rotate.js +22 -23
- package/dist/lib/session/fork.d.ts +32 -0
- package/dist/lib/session/fork.js +101 -0
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/usage.d.ts +23 -0
- package/dist/lib/usage.js +84 -0
- package/package.json +1 -1
package/dist/commands/view.js
CHANGED
|
@@ -4,8 +4,10 @@ import { visibleWidth, termLink } from '../lib/format.js';
|
|
|
4
4
|
import ora from 'ora';
|
|
5
5
|
import * as fs from 'fs';
|
|
6
6
|
import * as path from 'path';
|
|
7
|
-
import { AGENTS, ALL_AGENT_IDS,
|
|
7
|
+
import { AGENTS, ALL_AGENT_IDS, accountDisplayLabel, getAllCliStates, getAccountInfo, resolveAgentName, formatAgentError, agentLabel, colorAgent, } from '../lib/agents.js';
|
|
8
8
|
import { loginHint } from '../lib/signin-badge.js';
|
|
9
|
+
import { machineId } from '../lib/machine-id.js';
|
|
10
|
+
import { authCacheKey, formatCheckedAge, readAuthHealthCache } from '../lib/auth-health.js';
|
|
9
11
|
import { agentReportsUsage, deriveUsageStatusFromSnapshot, formatUsageSection, formatUsageSummary, formatUsageStatusBadge, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
|
|
10
12
|
import { readManifest } from '../lib/manifest.js';
|
|
11
13
|
import { listInstalledVersions, listInstalledVersionDirs, getGlobalDefault, getVersionHomePath, getVersionDir, getAvailableResources, getActuallySyncedResources, getNewResources, getProjectOnlyResources, hasNewResources, promptNewResourceSelection, syncResourcesToVersion, removeVersion, printTrashFooter, reconcileStaleLatestForAgent, isGlobalBinaryAgent, getLiveVersion, } from '../lib/versions.js';
|
|
@@ -26,33 +28,8 @@ import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
|
26
28
|
import { confirm } from '@inquirer/prompts';
|
|
27
29
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
28
30
|
import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/width.js';
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
const SIGNED_IN_LABEL = 'signed in';
|
|
32
|
-
/**
|
|
33
|
-
* Text for the account (email) column. Prefers the real email; otherwise, for a
|
|
34
|
-
* signed-in agent whose credential carries no email but does carry an opaque
|
|
35
|
-
* account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
|
|
36
|
-
* distinctly instead of a generic "signed in". Falls back to "signed in" when we
|
|
37
|
-
* have neither (Antigravity), and empty when signed out.
|
|
38
|
-
*
|
|
39
|
-
* When the account carries a Claude organizationType, the org badge is appended
|
|
40
|
-
* — "email (Turing Labs · Team)" — so two installs signed into the same email
|
|
41
|
-
* under different orgs (personal Max vs a Team seat) read distinctly. The
|
|
42
|
-
* suffix is plain text inside the label so the existing column-width pass
|
|
43
|
-
* measures and pads it for free.
|
|
44
|
-
*/
|
|
45
|
-
export function accountColumnLabel(info) {
|
|
46
|
-
if (!info)
|
|
47
|
-
return '';
|
|
48
|
-
if (info.email) {
|
|
49
|
-
const badge = accountOrgBadge(info);
|
|
50
|
-
return badge ? `${info.email} (${badge})` : info.email;
|
|
51
|
-
}
|
|
52
|
-
if (info.signedIn)
|
|
53
|
-
return info.accountId ? `id:${info.accountId}` : SIGNED_IN_LABEL;
|
|
54
|
-
return '';
|
|
55
|
-
}
|
|
31
|
+
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
32
|
+
export const accountColumnLabel = accountDisplayLabel;
|
|
56
33
|
/**
|
|
57
34
|
* Group profile summaries by their host harness, optionally filtered to a
|
|
58
35
|
* single agent. Profile YAMLs that fail validation are silently skipped by
|
|
@@ -245,6 +222,22 @@ function renderHostClisSection(cwd) {
|
|
|
245
222
|
console.log(` ${chalk.red('error')} ${chalk.gray(err.file)}: ${chalk.gray(err.reason)}`);
|
|
246
223
|
}
|
|
247
224
|
}
|
|
225
|
+
/**
|
|
226
|
+
* A compact live-auth chip for a version row, read from the fleet auth-health
|
|
227
|
+
* cache (written by `agents fleet ping` / the daemon). Empty when the install
|
|
228
|
+
* hasn't been probed — this is the local "signed in" flag's ground-truth
|
|
229
|
+
* companion: ● live, ○ revoked (re-login), ◐ unverified/limited, plus its age.
|
|
230
|
+
*/
|
|
231
|
+
function liveAuthChip(cache, host, agentId, version) {
|
|
232
|
+
const h = cache[authCacheKey(host, agentId, version)];
|
|
233
|
+
if (!h)
|
|
234
|
+
return '';
|
|
235
|
+
const glyph = h.verdict === 'live' ? chalk.green('●')
|
|
236
|
+
: h.verdict === 'revoked' ? chalk.red('○')
|
|
237
|
+
: h.verdict === 'expired' ? chalk.yellow('○')
|
|
238
|
+
: chalk.yellow('◐');
|
|
239
|
+
return `${glyph} ${chalk.gray(formatCheckedAge(h.checkedAt))}`;
|
|
240
|
+
}
|
|
248
241
|
async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
249
242
|
const spinnerText = filterAgentId
|
|
250
243
|
? `Checking ${agentLabel(filterAgentId)} agents...`
|
|
@@ -276,6 +269,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
276
269
|
}
|
|
277
270
|
// Shim healing is silent — users don't need to know about internal repairs
|
|
278
271
|
console.log(chalk.bold('Installed Agent CLIs\n'));
|
|
272
|
+
const selfHost = machineId();
|
|
273
|
+
// Read the auth-health cache once (not per version row — see the batching note above).
|
|
274
|
+
const authCache = readAuthHealthCache();
|
|
279
275
|
// Pre-fetch account info for all versions in parallel
|
|
280
276
|
const infoFetches = [];
|
|
281
277
|
const globalInfoFetches = [];
|
|
@@ -500,6 +496,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
500
496
|
if (runDefaultBits.length > 0) {
|
|
501
497
|
parts.push(chalk.gray(`run ${runDefaultBits.join(' ')}`));
|
|
502
498
|
}
|
|
499
|
+
const authChip = liveAuthChip(authCache, selfHost, agentId, version);
|
|
500
|
+
if (authChip)
|
|
501
|
+
parts.push(authChip);
|
|
503
502
|
console.log(parts.join(' '));
|
|
504
503
|
if (showPaths) {
|
|
505
504
|
const versionDir = getVersionDir(agentId, version);
|
package/dist/index.js
CHANGED
|
@@ -50,7 +50,7 @@ if (IS_DEV_BUILD) {
|
|
|
50
50
|
// module on each invocation (which loaded the whole ~50-module tree before the
|
|
51
51
|
// first byte of output), the registry maps a command name to a thunk that
|
|
52
52
|
// imports only what that command needs. See src/lib/startup/command-registry.ts.
|
|
53
|
-
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadMonitors, loadRun, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadShare, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
53
|
+
import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedback, loadCommands, loadHooks, loadSkills, loadRules, loadMemory, loadPermissions, loadMcp, loadCli, loadSubagents, loadPlugins, loadWorkflows, loadWorktree, loadVersions, loadImport, loadPackages, loadDaemon, loadRoutines, loadMonitors, loadRun, loadFork, loadDefaults, loadModels, loadPrune, loadTrash, loadRestore, loadDoctor, loadApply, loadCheck, loadStatus, loadProfiles, loadSecrets, loadWallet, loadHelper, loadMenubar, loadBeta, loadSync, loadLock, loadRefreshRules, loadDrive, loadFactory, loadUsage, loadCost, loadOutput, loadBudget, loadAlias, loadPty, loadTmux, loadWatchdog, loadBrowser, loadComputer, loadHosts, loadLogs, loadEvents, loadAudit, loadWebhook, loadFunnel, loadSsh, loadPull, loadPush, loadRepo, loadSetup, loadShare, loadFeed, loadMailboxes, } from './lib/startup/command-registry.js';
|
|
54
54
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
55
55
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
56
56
|
import { emit, redactArgs } from './lib/events.js';
|
|
@@ -690,6 +690,7 @@ async function registerAllEagerCommands() {
|
|
|
690
690
|
await reg(loadRoutines);
|
|
691
691
|
await reg(loadMonitors);
|
|
692
692
|
await reg(loadRun);
|
|
693
|
+
await reg(loadFork);
|
|
693
694
|
await reg(loadDefaults);
|
|
694
695
|
await reg(loadModels);
|
|
695
696
|
await reg(loadPrune);
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -148,6 +148,16 @@ export declare function formatClaudeOrgLabel(orgType: string | null | undefined)
|
|
|
148
148
|
* organizationType (signed out, non-Claude agents, configs predating the field).
|
|
149
149
|
*/
|
|
150
150
|
export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
|
|
151
|
+
/** Agents whose local credential formats expose enough state for account selection. */
|
|
152
|
+
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "gemini", "grok", "antigravity", "kimi", "droid", "opencode"];
|
|
153
|
+
/** Whether agents-cli can determine this agent's per-version sign-in state. */
|
|
154
|
+
export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
155
|
+
/**
|
|
156
|
+
* Human-readable account identity shared by every account-aware surface.
|
|
157
|
+
* Prefer email, append a multi-seat Claude organization name when present,
|
|
158
|
+
* then fall back to a non-secret account id or a generic signed-in label.
|
|
159
|
+
*/
|
|
160
|
+
export declare function accountDisplayLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
151
161
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
152
162
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
|
153
163
|
/** Decrypted contents of Droid's auth.v2.file (subset we consume). */
|
package/dist/lib/agents.js
CHANGED
|
@@ -992,6 +992,38 @@ export function accountOrgBadge(info) {
|
|
|
992
992
|
return info.organizationName;
|
|
993
993
|
return null;
|
|
994
994
|
}
|
|
995
|
+
/** Agents whose local credential formats expose enough state for account selection. */
|
|
996
|
+
export const ACCOUNT_INSPECTION_AGENT_IDS = [
|
|
997
|
+
'claude',
|
|
998
|
+
'codex',
|
|
999
|
+
'gemini',
|
|
1000
|
+
'grok',
|
|
1001
|
+
'antigravity',
|
|
1002
|
+
'kimi',
|
|
1003
|
+
'droid',
|
|
1004
|
+
'opencode',
|
|
1005
|
+
];
|
|
1006
|
+
const ACCOUNT_INSPECTION_AGENTS = new Set(ACCOUNT_INSPECTION_AGENT_IDS);
|
|
1007
|
+
/** Whether agents-cli can determine this agent's per-version sign-in state. */
|
|
1008
|
+
export function supportsAccountInspection(agentId) {
|
|
1009
|
+
return ACCOUNT_INSPECTION_AGENTS.has(agentId);
|
|
1010
|
+
}
|
|
1011
|
+
/**
|
|
1012
|
+
* Human-readable account identity shared by every account-aware surface.
|
|
1013
|
+
* Prefer email, append a multi-seat Claude organization name when present,
|
|
1014
|
+
* then fall back to a non-secret account id or a generic signed-in label.
|
|
1015
|
+
*/
|
|
1016
|
+
export function accountDisplayLabel(info) {
|
|
1017
|
+
if (!info)
|
|
1018
|
+
return '';
|
|
1019
|
+
if (info.email) {
|
|
1020
|
+
const badge = accountOrgBadge(info);
|
|
1021
|
+
return badge ? `${info.email} (${badge})` : info.email;
|
|
1022
|
+
}
|
|
1023
|
+
if (info.signedIn)
|
|
1024
|
+
return info.accountId ? `id:${info.accountId}` : 'signed in';
|
|
1025
|
+
return '';
|
|
1026
|
+
}
|
|
995
1027
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
996
1028
|
export async function getAccountEmail(agentId, home) {
|
|
997
1029
|
const info = await getAccountInfo(agentId, home);
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import { type AccountInfo } from './agents.js';
|
|
2
|
+
import type { AgentId } from './types.js';
|
|
3
|
+
import { type ProviderProbe } from './usage.js';
|
|
4
|
+
/**
|
|
5
|
+
* - `live` — completed an authenticated request (200).
|
|
6
|
+
* - `revoked` — the server rejected the token (401/403).
|
|
7
|
+
* - `expired` — locally-detected expiry; not network-verified (no refresh on the read path).
|
|
8
|
+
* - `rate_limited`— token works but is throttled right now (429).
|
|
9
|
+
* - `unverified` — credential present, not locally expired, but this agent has no in-repo probe endpoint (codex/grok).
|
|
10
|
+
* - `unconfigured`— no usable credential on disk.
|
|
11
|
+
* - `error` — network/other failure; verdict indeterminate (keep the last known one).
|
|
12
|
+
*/
|
|
13
|
+
export type AuthVerdict = 'live' | 'revoked' | 'expired' | 'rate_limited' | 'unverified' | 'unconfigured' | 'error';
|
|
14
|
+
export interface AuthHealth {
|
|
15
|
+
verdict: AuthVerdict;
|
|
16
|
+
/** epoch ms of the probe. */
|
|
17
|
+
checkedAt: number;
|
|
18
|
+
/** optional short human detail (e.g. "HTTP 401", a network error). */
|
|
19
|
+
detail?: string;
|
|
20
|
+
/** account label for display (email / id), when known. Never part of the key. */
|
|
21
|
+
account?: string;
|
|
22
|
+
}
|
|
23
|
+
/** Agents with a live network probe wired up today. The rest are best-effort. */
|
|
24
|
+
export declare const LIVE_PROBE_AGENTS: ReadonlySet<AgentId>;
|
|
25
|
+
/** Map an HTTP status from a live probe to a verdict. */
|
|
26
|
+
export declare function classifyHttpStatus(status: number): AuthVerdict;
|
|
27
|
+
/** Turn a raw provider probe (from usage.ts) into a verdict. */
|
|
28
|
+
export declare function verdictFromProbe(probe: ProviderProbe): AuthVerdict;
|
|
29
|
+
/** A short human detail line for a probe result (rendered under --verbose). */
|
|
30
|
+
export declare function probeDetail(probe: ProviderProbe): string | undefined;
|
|
31
|
+
/** Uncolored glyph for a verdict (color is applied by the caller). */
|
|
32
|
+
export declare function verdictGlyph(verdict: AuthVerdict): string;
|
|
33
|
+
/** One-word label for matrices/verbose output. */
|
|
34
|
+
export declare function verdictLabel(verdict: AuthVerdict): string;
|
|
35
|
+
/** Roll a set of verdicts (one host×agent's installs) into counts for a matrix cell. */
|
|
36
|
+
export interface VerdictSummary {
|
|
37
|
+
live: number;
|
|
38
|
+
/** revoked — the server rejected the token (401/403). Genuinely needs re-login. */
|
|
39
|
+
bad: number;
|
|
40
|
+
/**
|
|
41
|
+
* expired / rate_limited / unverified / error — degraded or unknown, but NOT
|
|
42
|
+
* "re-login now". `expired` is soft for kimi/droid (their CLIs refresh the
|
|
43
|
+
* token on next launch; we don't refresh on the read path), so it must not be
|
|
44
|
+
* lumped with revoked or we'd cry wolf on a self-healing token.
|
|
45
|
+
*/
|
|
46
|
+
warn: number;
|
|
47
|
+
total: number;
|
|
48
|
+
}
|
|
49
|
+
export declare function summarizeVerdicts(verdicts: AuthVerdict[]): VerdictSummary;
|
|
50
|
+
/** Verdicts that mean "this token was rejected by the server — re-login required". */
|
|
51
|
+
export declare function isDeadVerdict(verdict: AuthVerdict): boolean;
|
|
52
|
+
/** Human "3m ago" style age for a checkedAt timestamp. */
|
|
53
|
+
export declare function formatCheckedAge(checkedAt: number, now?: number): string;
|
|
54
|
+
/**
|
|
55
|
+
* Human account label for display (email, else id). NOT used in the cache key —
|
|
56
|
+
* two installs on one host can hold the same account with independently valid
|
|
57
|
+
* tokens, so the key is keyed by version (below), not account.
|
|
58
|
+
*/
|
|
59
|
+
export declare function authAccountLabel(info: Pick<AccountInfo, 'email' | 'accountId' | 'userId'> | null | undefined): string | undefined;
|
|
60
|
+
/** Cache key: one entry per install — (host, agent, version). Unique per token. */
|
|
61
|
+
export declare function authCacheKey(host: string, agent: AgentId | string, version: string): string;
|
|
62
|
+
/** Read the whole cache (best-effort; a corrupt/missing file yields an empty map). */
|
|
63
|
+
export declare function readAuthHealthCache(): Record<string, AuthHealth>;
|
|
64
|
+
/** Read one entry, or null. */
|
|
65
|
+
export declare function readAuthHealth(host: string, agent: AgentId | string, version: string): AuthHealth | null;
|
|
66
|
+
/**
|
|
67
|
+
* Merge entries into the cache. An incoming `error` verdict (a network blip,
|
|
68
|
+
* not a server rejection) is indeterminate, so it must NOT clobber a prior
|
|
69
|
+
* known verdict — otherwise one 8s timeout flips a `live` chip to `error`,
|
|
70
|
+
* exactly the "cry wolf" the verdict model avoids for `expired`. This is the
|
|
71
|
+
* behaviour promised by the `error` doc on AuthVerdict ("keep the last known
|
|
72
|
+
* one"). Pure, so it's unit-tested directly. */
|
|
73
|
+
export declare function mergeAuthHealthEntries(current: Record<string, AuthHealth>, incoming: Record<string, AuthHealth>): Record<string, AuthHealth>;
|
|
74
|
+
/** Merge one or more entries into the cache (best-effort write). */
|
|
75
|
+
export declare function writeAuthHealthEntries(entries: Record<string, AuthHealth>): void;
|
|
76
|
+
/**
|
|
77
|
+
* Complete a live auth probe for one (agent, home). For claude/kimi/droid this
|
|
78
|
+
* hits the provider; for everyone else it reports a best-effort local verdict
|
|
79
|
+
* (`unverified` when a credential is present, `unconfigured` otherwise) — never
|
|
80
|
+
* masquerading as `live`.
|
|
81
|
+
*/
|
|
82
|
+
export declare function probeAuthHealth(agent: AgentId, home: string | undefined, opts?: {
|
|
83
|
+
cliVersion?: string | null;
|
|
84
|
+
info?: AccountInfo | null;
|
|
85
|
+
}): Promise<AuthHealth>;
|
|
86
|
+
/** One probed install on a host. */
|
|
87
|
+
export interface AuthProbeRow {
|
|
88
|
+
agent: AgentId;
|
|
89
|
+
version: string;
|
|
90
|
+
account?: string;
|
|
91
|
+
health: AuthHealth;
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* Enumerate every installed (agent, version) on THIS host, probe each in
|
|
95
|
+
* parallel, and return the rows (installs with no credential at all are
|
|
96
|
+
* dropped). Shared by `agents fleet ping --local` and the daemon refresh.
|
|
97
|
+
*/
|
|
98
|
+
export declare function probeLocalFleetAuth(opts?: {
|
|
99
|
+
cliVersion?: string | null;
|
|
100
|
+
agents?: readonly AgentId[];
|
|
101
|
+
}): Promise<AuthProbeRow[]>;
|
|
102
|
+
/** Persist a host's probed rows into the cache (keyed by host+agent+version). */
|
|
103
|
+
export declare function writeFleetAuthRows(host: string, rows: AuthProbeRow[]): void;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Live auth-health: does an agent account's stored credential actually complete
|
|
3
|
+
* an authenticated request right now?
|
|
4
|
+
*
|
|
5
|
+
* The rest of the CLI reports "signed in" from a local heuristic — a credential
|
|
6
|
+
* file is present and its email decodes — which cannot distinguish a good token
|
|
7
|
+
* from a revoked-but-unexpired one. This module completes a real request per
|
|
8
|
+
* (agent, account) and records the verdict in a small cache that `agents view`,
|
|
9
|
+
* `agents fleet status`, and the run rotation all read. The daemon and
|
|
10
|
+
* `agents fleet ping` are the writers; everyone else reads.
|
|
11
|
+
*
|
|
12
|
+
* The network probes themselves live in lib/usage.ts (where the per-provider
|
|
13
|
+
* token loaders + endpoints already are); this module classifies their result,
|
|
14
|
+
* covers the best-effort (non-networked) providers, and owns the cache.
|
|
15
|
+
*/
|
|
16
|
+
import * as fs from 'fs';
|
|
17
|
+
import * as path from 'path';
|
|
18
|
+
import { ALL_AGENT_IDS, getAccountInfo } from './agents.js';
|
|
19
|
+
import { getCacheDir } from './state.js';
|
|
20
|
+
import { probeClaudeStatus, probeDroidStatus, probeKimiStatus, } from './usage.js';
|
|
21
|
+
import { getVersionHomePath, listInstalledVersions } from './versions.js';
|
|
22
|
+
/** Agents with a live network probe wired up today. The rest are best-effort. */
|
|
23
|
+
export const LIVE_PROBE_AGENTS = new Set(['claude', 'kimi', 'droid']);
|
|
24
|
+
// ---------------------------------------------------------------------------
|
|
25
|
+
// Pure classifiers / render (unit-tested; no network, no fs)
|
|
26
|
+
// ---------------------------------------------------------------------------
|
|
27
|
+
/** Map an HTTP status from a live probe to a verdict. */
|
|
28
|
+
export function classifyHttpStatus(status) {
|
|
29
|
+
if (status >= 200 && status < 300)
|
|
30
|
+
return 'live';
|
|
31
|
+
if (status === 401 || status === 403)
|
|
32
|
+
return 'revoked';
|
|
33
|
+
if (status === 429)
|
|
34
|
+
return 'rate_limited';
|
|
35
|
+
return 'error';
|
|
36
|
+
}
|
|
37
|
+
/** Turn a raw provider probe (from usage.ts) into a verdict. */
|
|
38
|
+
export function verdictFromProbe(probe) {
|
|
39
|
+
if (probe.token === 'missing')
|
|
40
|
+
return 'unconfigured';
|
|
41
|
+
if (probe.token === 'expired')
|
|
42
|
+
return 'expired';
|
|
43
|
+
if (probe.status == null)
|
|
44
|
+
return 'error';
|
|
45
|
+
return classifyHttpStatus(probe.status);
|
|
46
|
+
}
|
|
47
|
+
/** A short human detail line for a probe result (rendered under --verbose). */
|
|
48
|
+
export function probeDetail(probe) {
|
|
49
|
+
if (probe.status != null && (probe.status < 200 || probe.status >= 300))
|
|
50
|
+
return `HTTP ${probe.status}`;
|
|
51
|
+
if (probe.error)
|
|
52
|
+
return probe.error;
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
const VERDICT_GLYPHS = {
|
|
56
|
+
live: '●', // ●
|
|
57
|
+
revoked: '○', // ○
|
|
58
|
+
expired: '○', // ○
|
|
59
|
+
rate_limited: '◐', // ◐
|
|
60
|
+
unverified: '◐', // ◐
|
|
61
|
+
unconfigured: '·', // ·
|
|
62
|
+
error: '·', // ·
|
|
63
|
+
};
|
|
64
|
+
/** Uncolored glyph for a verdict (color is applied by the caller). */
|
|
65
|
+
export function verdictGlyph(verdict) {
|
|
66
|
+
return VERDICT_GLYPHS[verdict] ?? '·';
|
|
67
|
+
}
|
|
68
|
+
/** One-word label for matrices/verbose output. */
|
|
69
|
+
export function verdictLabel(verdict) {
|
|
70
|
+
switch (verdict) {
|
|
71
|
+
case 'live': return 'live';
|
|
72
|
+
case 'revoked': return 'revoked';
|
|
73
|
+
case 'expired': return 'expired';
|
|
74
|
+
case 'rate_limited': return 'limited';
|
|
75
|
+
case 'unverified': return 'unverified';
|
|
76
|
+
case 'unconfigured': return '—';
|
|
77
|
+
case 'error': return '?';
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
export function summarizeVerdicts(verdicts) {
|
|
81
|
+
let live = 0;
|
|
82
|
+
let bad = 0;
|
|
83
|
+
let warn = 0;
|
|
84
|
+
for (const v of verdicts) {
|
|
85
|
+
if (v === 'live')
|
|
86
|
+
live++;
|
|
87
|
+
else if (v === 'revoked')
|
|
88
|
+
bad++;
|
|
89
|
+
else
|
|
90
|
+
warn++;
|
|
91
|
+
}
|
|
92
|
+
return { live, bad, warn, total: verdicts.length };
|
|
93
|
+
}
|
|
94
|
+
/** Verdicts that mean "this token was rejected by the server — re-login required". */
|
|
95
|
+
export function isDeadVerdict(verdict) {
|
|
96
|
+
return verdict === 'revoked';
|
|
97
|
+
}
|
|
98
|
+
/** Human "3m ago" style age for a checkedAt timestamp. */
|
|
99
|
+
export function formatCheckedAge(checkedAt, now = Date.now()) {
|
|
100
|
+
const secs = Math.max(0, Math.round((now - checkedAt) / 1000));
|
|
101
|
+
if (secs < 60)
|
|
102
|
+
return `${secs}s ago`;
|
|
103
|
+
const mins = Math.round(secs / 60);
|
|
104
|
+
if (mins < 60)
|
|
105
|
+
return `${mins}m ago`;
|
|
106
|
+
const hours = Math.round(mins / 60);
|
|
107
|
+
if (hours < 24)
|
|
108
|
+
return `${hours}h ago`;
|
|
109
|
+
return `${Math.round(hours / 24)}d ago`;
|
|
110
|
+
}
|
|
111
|
+
// ---------------------------------------------------------------------------
|
|
112
|
+
// Cache identity + IO (single source of truth read by view/fleet/rotation)
|
|
113
|
+
// ---------------------------------------------------------------------------
|
|
114
|
+
/**
|
|
115
|
+
* Human account label for display (email, else id). NOT used in the cache key —
|
|
116
|
+
* two installs on one host can hold the same account with independently valid
|
|
117
|
+
* tokens, so the key is keyed by version (below), not account.
|
|
118
|
+
*/
|
|
119
|
+
export function authAccountLabel(info) {
|
|
120
|
+
return info?.email || info?.accountId || info?.userId || undefined;
|
|
121
|
+
}
|
|
122
|
+
/** Cache key: one entry per install — (host, agent, version). Unique per token. */
|
|
123
|
+
export function authCacheKey(host, agent, version) {
|
|
124
|
+
return `${host}:${agent}:${version}`;
|
|
125
|
+
}
|
|
126
|
+
function cacheFilePath() {
|
|
127
|
+
return path.join(getCacheDir(), '.auth-health.json');
|
|
128
|
+
}
|
|
129
|
+
/** Read the whole cache (best-effort; a corrupt/missing file yields an empty map). */
|
|
130
|
+
export function readAuthHealthCache() {
|
|
131
|
+
try {
|
|
132
|
+
const parsed = JSON.parse(fs.readFileSync(cacheFilePath(), 'utf-8'));
|
|
133
|
+
if (parsed && parsed.entries && typeof parsed.entries === 'object')
|
|
134
|
+
return parsed.entries;
|
|
135
|
+
}
|
|
136
|
+
catch {
|
|
137
|
+
// missing or corrupt — treat as empty
|
|
138
|
+
}
|
|
139
|
+
return {};
|
|
140
|
+
}
|
|
141
|
+
/** Read one entry, or null. */
|
|
142
|
+
export function readAuthHealth(host, agent, version) {
|
|
143
|
+
return readAuthHealthCache()[authCacheKey(host, agent, version)] ?? null;
|
|
144
|
+
}
|
|
145
|
+
/**
|
|
146
|
+
* Merge entries into the cache. An incoming `error` verdict (a network blip,
|
|
147
|
+
* not a server rejection) is indeterminate, so it must NOT clobber a prior
|
|
148
|
+
* known verdict — otherwise one 8s timeout flips a `live` chip to `error`,
|
|
149
|
+
* exactly the "cry wolf" the verdict model avoids for `expired`. This is the
|
|
150
|
+
* behaviour promised by the `error` doc on AuthVerdict ("keep the last known
|
|
151
|
+
* one"). Pure, so it's unit-tested directly. */
|
|
152
|
+
export function mergeAuthHealthEntries(current, incoming) {
|
|
153
|
+
const merged = { ...current };
|
|
154
|
+
for (const [key, health] of Object.entries(incoming)) {
|
|
155
|
+
if (health.verdict === 'error' && merged[key])
|
|
156
|
+
continue; // keep last known
|
|
157
|
+
merged[key] = health;
|
|
158
|
+
}
|
|
159
|
+
return merged;
|
|
160
|
+
}
|
|
161
|
+
/** Merge one or more entries into the cache (best-effort write). */
|
|
162
|
+
export function writeAuthHealthEntries(entries) {
|
|
163
|
+
try {
|
|
164
|
+
const dir = getCacheDir();
|
|
165
|
+
if (!fs.existsSync(dir))
|
|
166
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
167
|
+
const merged = {
|
|
168
|
+
version: 1,
|
|
169
|
+
entries: mergeAuthHealthEntries(readAuthHealthCache(), entries),
|
|
170
|
+
};
|
|
171
|
+
fs.writeFileSync(cacheFilePath(), JSON.stringify(merged, null, 2));
|
|
172
|
+
}
|
|
173
|
+
catch {
|
|
174
|
+
// best-effort; a failed write just means the next reader falls back to heuristics
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
// ---------------------------------------------------------------------------
|
|
178
|
+
// The probe (writer side)
|
|
179
|
+
// ---------------------------------------------------------------------------
|
|
180
|
+
/**
|
|
181
|
+
* Complete a live auth probe for one (agent, home). For claude/kimi/droid this
|
|
182
|
+
* hits the provider; for everyone else it reports a best-effort local verdict
|
|
183
|
+
* (`unverified` when a credential is present, `unconfigured` otherwise) — never
|
|
184
|
+
* masquerading as `live`.
|
|
185
|
+
*/
|
|
186
|
+
export async function probeAuthHealth(agent, home, opts) {
|
|
187
|
+
const checkedAt = Date.now();
|
|
188
|
+
if (LIVE_PROBE_AGENTS.has(agent)) {
|
|
189
|
+
let probe;
|
|
190
|
+
if (agent === 'claude')
|
|
191
|
+
probe = await probeClaudeStatus(home, opts?.cliVersion);
|
|
192
|
+
else if (agent === 'kimi')
|
|
193
|
+
probe = await probeKimiStatus(home);
|
|
194
|
+
else
|
|
195
|
+
probe = await probeDroidStatus(home);
|
|
196
|
+
return { verdict: verdictFromProbe(probe), checkedAt, detail: probeDetail(probe) };
|
|
197
|
+
}
|
|
198
|
+
const info = opts?.info !== undefined ? opts.info : await getAccountInfo(agent, home).catch(() => null);
|
|
199
|
+
return { verdict: info?.signedIn ? 'unverified' : 'unconfigured', checkedAt };
|
|
200
|
+
}
|
|
201
|
+
/**
|
|
202
|
+
* Enumerate every installed (agent, version) on THIS host, probe each in
|
|
203
|
+
* parallel, and return the rows (installs with no credential at all are
|
|
204
|
+
* dropped). Shared by `agents fleet ping --local` and the daemon refresh.
|
|
205
|
+
*/
|
|
206
|
+
export async function probeLocalFleetAuth(opts) {
|
|
207
|
+
const agentIds = opts?.agents ?? ALL_AGENT_IDS;
|
|
208
|
+
const tasks = [];
|
|
209
|
+
for (const agent of agentIds) {
|
|
210
|
+
for (const version of listInstalledVersions(agent)) {
|
|
211
|
+
const home = getVersionHomePath(agent, version);
|
|
212
|
+
tasks.push((async () => {
|
|
213
|
+
const info = await getAccountInfo(agent, home).catch(() => null);
|
|
214
|
+
const health = await probeAuthHealth(agent, home, { cliVersion: opts?.cliVersion, info });
|
|
215
|
+
health.account = authAccountLabel(info);
|
|
216
|
+
if (health.verdict === 'unconfigured')
|
|
217
|
+
return null;
|
|
218
|
+
return { agent, version, account: health.account, health };
|
|
219
|
+
})());
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
const settled = await Promise.all(tasks);
|
|
223
|
+
return settled.filter((r) => r !== null);
|
|
224
|
+
}
|
|
225
|
+
/** Persist a host's probed rows into the cache (keyed by host+agent+version). */
|
|
226
|
+
export function writeFleetAuthRows(host, rows) {
|
|
227
|
+
const entries = {};
|
|
228
|
+
for (const row of rows) {
|
|
229
|
+
entries[authCacheKey(host, row.agent, row.version)] = row.health;
|
|
230
|
+
}
|
|
231
|
+
writeAuthHealthEntries(entries);
|
|
232
|
+
}
|
|
@@ -24,6 +24,15 @@ export declare function findFirstInstalledBrowser(platform?: string): {
|
|
|
24
24
|
browserType: BrowserType;
|
|
25
25
|
binary: string;
|
|
26
26
|
} | null;
|
|
27
|
+
/**
|
|
28
|
+
* List every installed Chromium-family browser on this machine, in the platform
|
|
29
|
+
* priority order. Used by `agents setup browser` to offer a pick when more than
|
|
30
|
+
* one is present. Returns [] if none are installed.
|
|
31
|
+
*/
|
|
32
|
+
export declare function listInstalledBrowsers(platform?: string): {
|
|
33
|
+
browserType: BrowserType;
|
|
34
|
+
binary: string;
|
|
35
|
+
}[];
|
|
27
36
|
export interface LaunchResult {
|
|
28
37
|
pid: number;
|
|
29
38
|
port: number;
|
|
@@ -203,6 +203,28 @@ export function findFirstInstalledBrowser(platform = os.platform()) {
|
|
|
203
203
|
}
|
|
204
204
|
return null;
|
|
205
205
|
}
|
|
206
|
+
/**
|
|
207
|
+
* List every installed Chromium-family browser on this machine, in the platform
|
|
208
|
+
* priority order. Used by `agents setup browser` to offer a pick when more than
|
|
209
|
+
* one is present. Returns [] if none are installed.
|
|
210
|
+
*/
|
|
211
|
+
export function listInstalledBrowsers(platform = os.platform()) {
|
|
212
|
+
const priority = DEFAULT_BROWSER_PRIORITY[platform];
|
|
213
|
+
const platformPaths = BROWSER_PATHS[platform];
|
|
214
|
+
if (!priority || !platformPaths)
|
|
215
|
+
return [];
|
|
216
|
+
const found = [];
|
|
217
|
+
for (const browserType of priority) {
|
|
218
|
+
const candidates = platformPaths[browserType] || [];
|
|
219
|
+
for (const p of candidates) {
|
|
220
|
+
if (fs.existsSync(p)) {
|
|
221
|
+
found.push({ browserType, binary: resolveBrowserBinary(p) });
|
|
222
|
+
break; // one install path per browser type is enough
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
return found;
|
|
227
|
+
}
|
|
206
228
|
export async function launchBrowser(profileName, browserType, port, options = {}, secrets, customBinary,
|
|
207
229
|
// `electron: true` distinguishes Notion / VS Code-style apps from
|
|
208
230
|
// regular Chrome — purely informational, stored in meta.json so the
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* On-demand download + verification of the macOS `agents computer` helper
|
|
3
|
+
* ("ComputerHelper.app").
|
|
4
|
+
*
|
|
5
|
+
* The helper is a signed + notarized universal `.app` bundle published as a
|
|
6
|
+
* GitHub release asset per tagged CLI version — the same distribution model as
|
|
7
|
+
* the Windows helper (see `lib/ssh-tunnel.ts`). A fresh `npm i -g` machine has
|
|
8
|
+
* no local build, so `agents computer setup` / `agents setup computer` fetch the
|
|
9
|
+
* asset for the running CLI version, verify its sha256 against the published
|
|
10
|
+
* `.sha256`, then verify the code signature (Developer ID Team + notarization)
|
|
11
|
+
* before it is ever copied to /Applications.
|
|
12
|
+
*
|
|
13
|
+
* A `.app` is a directory, so the asset is a zip (`ditto -c -k --keepParent`);
|
|
14
|
+
* we extract it with `ditto -x -k` after the checksum passes.
|
|
15
|
+
*/
|
|
16
|
+
/** GitHub repo whose `v<version>` releases carry the helper asset. */
|
|
17
|
+
export declare const HELPER_RELEASE_REPO = "phnx-labs/agents-cli";
|
|
18
|
+
/** The zipped `.app` release asset name. */
|
|
19
|
+
export declare const MAC_HELPER_ASSET = "ComputerHelper.app.zip";
|
|
20
|
+
/** The bundle directory name once extracted. */
|
|
21
|
+
export declare const MAC_HELPER_APP_NAME = "ComputerHelper.app";
|
|
22
|
+
/** Apple Developer ID Team the helper must be signed by (defense in depth on top
|
|
23
|
+
* of `spctl` notarization assessment). "Developer ID Application: Muqit Nawaz". */
|
|
24
|
+
export declare const EXPECTED_TEAM_ID = "2HTP252L87";
|
|
25
|
+
/** Cache dir for the downloaded helper, one subdir per release tag. */
|
|
26
|
+
export declare function macHelperCacheDir(version: string): string;
|
|
27
|
+
/** Release-asset URLs for the helper zip + its checksum at one `v<version>` tag. */
|
|
28
|
+
export declare function macHelperAssetUrls(version: string): {
|
|
29
|
+
zip: string;
|
|
30
|
+
sha256: string;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Verify a helper `.app` bundle is intact, signed by the expected Developer ID
|
|
34
|
+
* Team, and notarized (Gatekeeper-accepted). Throws with an actionable message
|
|
35
|
+
* on any failure — a downloaded bundle is never trusted without this.
|
|
36
|
+
*/
|
|
37
|
+
export declare function verifyMacHelper(appPath: string): void;
|
|
38
|
+
/**
|
|
39
|
+
* Download the helper release asset for `version`, verify sha256, extract the
|
|
40
|
+
* `.app`, and verify its signature. Returns the path to the extracted
|
|
41
|
+
* `ComputerHelper.app`. A missing asset is a hard error naming the exact tag —
|
|
42
|
+
* never a silent fallback to another release.
|
|
43
|
+
*/
|
|
44
|
+
export declare function downloadMacHelperApp(version: string): Promise<string>;
|
|
45
|
+
/**
|
|
46
|
+
* Resolve the helper `.app` to install from: a local build / bundled copy first
|
|
47
|
+
* (repo checkout), else the checksum + signature-verified release-asset download
|
|
48
|
+
* for the running CLI version. Throws with the tag it checked when neither
|
|
49
|
+
* exists. macOS only.
|
|
50
|
+
*/
|
|
51
|
+
export declare function ensureMacHelperApp(version?: string): Promise<string>;
|