@phnx-labs/agents-cli 1.20.89 → 1.20.90
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 +240 -0
- package/README.md +6 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/exec.js +7 -1
- package/dist/commands/harness.d.ts +27 -0
- package/dist/commands/harness.js +120 -13
- package/dist/commands/profiles.d.ts +3 -0
- package/dist/commands/profiles.js +1 -1
- package/dist/commands/routines.d.ts +19 -0
- package/dist/commands/routines.js +28 -6
- package/dist/commands/secrets.d.ts +10 -1
- package/dist/commands/secrets.js +18 -6
- package/dist/commands/sessions-browser.d.ts +4 -0
- package/dist/commands/sessions-browser.js +51 -9
- package/dist/commands/sessions-favorite.d.ts +20 -0
- package/dist/commands/sessions-favorite.js +120 -0
- package/dist/commands/sessions.d.ts +103 -20
- package/dist/commands/sessions.js +356 -62
- package/dist/commands/setup-secrets.d.ts +7 -0
- package/dist/commands/setup-secrets.js +12 -9
- package/dist/commands/versions.js +12 -4
- package/dist/commands/view.d.ts +14 -1
- package/dist/commands/view.js +103 -128
- package/dist/lib/agents.d.ts +4 -2
- package/dist/lib/agents.js +21 -6
- package/dist/lib/hosts/dispatch.js +19 -1
- package/dist/lib/hq/floor.js +12 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/picker.d.ts +27 -2
- package/dist/lib/picker.js +71 -7
- package/dist/lib/profiles.d.ts +48 -0
- package/dist/lib/profiles.js +67 -0
- package/dist/lib/rotate.d.ts +24 -2
- package/dist/lib/rotate.js +63 -6
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/session/active.d.ts +109 -3
- package/dist/lib/session/active.js +269 -13
- package/dist/lib/session/db.d.ts +14 -0
- package/dist/lib/session/db.js +35 -0
- package/dist/lib/session/favorites.d.ts +39 -0
- package/dist/lib/session/favorites.js +101 -0
- package/dist/lib/session/host-link.d.ts +68 -0
- package/dist/lib/session/host-link.js +64 -0
- package/dist/lib/session/presence.d.ts +85 -0
- package/dist/lib/session/presence.js +150 -0
- package/dist/lib/session/remote-list.d.ts +10 -0
- package/dist/lib/session/remote-list.js +47 -9
- package/dist/lib/tmux/binary.d.ts +7 -0
- package/dist/lib/tmux/binary.js +11 -1
- package/dist/lib/types.d.ts +4 -3
- package/dist/lib/usage-backoff.d.ts +29 -0
- package/dist/lib/usage-backoff.js +165 -0
- package/dist/lib/usage.d.ts +112 -5
- package/dist/lib/usage.js +464 -46
- package/dist/lib/watchdog/runner.d.ts +13 -0
- package/dist/lib/watchdog/runner.js +16 -1
- package/package.json +1 -1
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* `agents secrets import` command, keeping bundle storage as the source of truth.
|
|
6
6
|
*/
|
|
7
7
|
import type { Command } from 'commander';
|
|
8
|
+
import type { SecretsPolicy } from '../lib/secrets/bundles.js';
|
|
8
9
|
interface SetupSecretsOptions {
|
|
9
10
|
backend?: string;
|
|
10
11
|
policy?: string;
|
|
@@ -14,6 +15,12 @@ interface SetupSecretsOptions {
|
|
|
14
15
|
iUnderstand?: boolean;
|
|
15
16
|
force?: boolean;
|
|
16
17
|
}
|
|
18
|
+
/** The wizard's `--policy` shares the canonical parser with `agents secrets
|
|
19
|
+
* policy`, so it accepts `hold` (and the `daily`/`session` aliases) rather than
|
|
20
|
+
* carrying its own copy that knows only the retired vocabulary. `parsePolicyOpt`
|
|
21
|
+
* defaults an absent value to `always`; the wizard's own default is `hold`, so
|
|
22
|
+
* it is supplied here rather than inherited. */
|
|
23
|
+
export declare function parsePolicy(raw: string | undefined): SecretsPolicy;
|
|
17
24
|
export declare function runSecretsSetupWizard(opts?: SetupSecretsOptions): Promise<boolean>;
|
|
18
25
|
/** Register `agents setup secrets` under the parent `setup` command. */
|
|
19
26
|
export declare function registerSetupSecretsCommand(setupCmd: Command): void;
|
|
@@ -11,6 +11,7 @@ import { spawnSync } from 'node:child_process';
|
|
|
11
11
|
import { getCliLaunch } from '../lib/cli-entry.js';
|
|
12
12
|
import { getHistoryDir, updateMeta } from '../lib/state.js';
|
|
13
13
|
import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
14
|
+
import { parsePolicyOpt } from './secrets.js';
|
|
14
15
|
function defaultBackendForPlatform(platform = process.platform) {
|
|
15
16
|
return platform === 'darwin' ? 'keychain' : 'file';
|
|
16
17
|
}
|
|
@@ -23,11 +24,13 @@ function parseBackend(raw) {
|
|
|
23
24
|
return value;
|
|
24
25
|
throw new Error(`Invalid --backend '${raw}'. Use keychain, file, or vault.`);
|
|
25
26
|
}
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
/** The wizard's `--policy` shares the canonical parser with `agents secrets
|
|
28
|
+
* policy`, so it accepts `hold` (and the `daily`/`session` aliases) rather than
|
|
29
|
+
* carrying its own copy that knows only the retired vocabulary. `parsePolicyOpt`
|
|
30
|
+
* defaults an absent value to `always`; the wizard's own default is `hold`, so
|
|
31
|
+
* it is supplied here rather than inherited. */
|
|
32
|
+
export function parsePolicy(raw) {
|
|
33
|
+
return parsePolicyOpt(raw ?? 'hold');
|
|
31
34
|
}
|
|
32
35
|
function parseImportSource(raw) {
|
|
33
36
|
const value = (raw ?? 'none').toLowerCase();
|
|
@@ -140,9 +143,9 @@ async function resolveInteractiveChoices(opts) {
|
|
|
140
143
|
});
|
|
141
144
|
const policy = opts.policy ? parsePolicy(opts.policy) : await select({
|
|
142
145
|
message: 'Default prompt policy',
|
|
143
|
-
default: '
|
|
146
|
+
default: 'hold',
|
|
144
147
|
choices: [
|
|
145
|
-
{ name: '
|
|
148
|
+
{ name: 'hold — ask once, then hold for the configured window (7 days by default)', value: 'hold' },
|
|
146
149
|
{ name: 'always — ask on every read', value: 'always' },
|
|
147
150
|
{ name: 'never — no biometry ACL; use only for automation-only bundles', value: 'never' },
|
|
148
151
|
],
|
|
@@ -171,7 +174,7 @@ function printOnboardingSummary(backend, policy, prefsFile) {
|
|
|
171
174
|
console.log(chalk.gray(`policy: ${policy} — used by bundles that do not set their own policy.`));
|
|
172
175
|
}
|
|
173
176
|
console.log(chalk.bold('\nTry:'));
|
|
174
|
-
console.log(chalk.cyan(` agents secrets create prod ${backendArgs(backend).join(' ')} --policy ${policy === 'never' ? '
|
|
177
|
+
console.log(chalk.cyan(` agents secrets create prod ${backendArgs(backend).join(' ')} --policy ${policy === 'never' ? 'hold' : policy}`));
|
|
175
178
|
console.log(chalk.cyan(' agents secrets add prod API_KEY'));
|
|
176
179
|
console.log(chalk.cyan(' agents run claude "deploy" --secrets prod'));
|
|
177
180
|
}
|
|
@@ -201,7 +204,7 @@ export function registerSetupSecretsCommand(setupCmd) {
|
|
|
201
204
|
.command('secrets')
|
|
202
205
|
.description('Configure `agents secrets` defaults and optionally import existing secrets.')
|
|
203
206
|
.option('--backend <backend>', 'default backend: keychain, file, or vault')
|
|
204
|
-
.option('--policy <policy>',
|
|
207
|
+
.option('--policy <policy>', "default prompt policy: hold, always, or never ('daily'/'session' are accepted aliases for 'hold')")
|
|
205
208
|
.option('--import-from <source>', 'optional import source: none, env, 1password, or icloud')
|
|
206
209
|
.option('--bundle <name>', 'bundle name for optional imports')
|
|
207
210
|
.option('--vault <name-or-path>', '1Password vault name, or .env path with --import-from env')
|
|
@@ -31,7 +31,7 @@ function fixSessionFilePaths(agent, version, oldVersionDir) {
|
|
|
31
31
|
const trashPath = path.join(trashAgentDir, stamps[0]);
|
|
32
32
|
updateSessionFilePaths(oldVersionDir, trashPath);
|
|
33
33
|
}
|
|
34
|
-
function formatAccountHint(info, usage) {
|
|
34
|
+
function formatAccountHint(info, usage, unverified = false) {
|
|
35
35
|
const parts = [];
|
|
36
36
|
if (info.email) {
|
|
37
37
|
// Same-email accounts can live in different orgs (personal Max vs a Team
|
|
@@ -39,7 +39,7 @@ function formatAccountHint(info, usage) {
|
|
|
39
39
|
const badge = accountOrgBadge(info);
|
|
40
40
|
parts.push(badge ? `${info.email} (${badge})` : info.email);
|
|
41
41
|
}
|
|
42
|
-
const usageSummary = formatUsageSummary(info.plan, usage);
|
|
42
|
+
const usageSummary = formatUsageSummary(info.plan, usage, 3, { unverified });
|
|
43
43
|
if (usageSummary)
|
|
44
44
|
parts.push(usageSummary);
|
|
45
45
|
if (parts.length === 0)
|
|
@@ -535,7 +535,10 @@ export function registerVersionsCommands(program) {
|
|
|
535
535
|
cliVersion: installedVersion,
|
|
536
536
|
info,
|
|
537
537
|
});
|
|
538
|
-
|
|
538
|
+
// This hint sits in a "switch your default to this version?"
|
|
539
|
+
// confirm — the one place a stale reading directly steers a
|
|
540
|
+
// choice, so it must not present an unconfirmed bar as fact.
|
|
541
|
+
const accountHint = formatAccountHint(info, usage.snapshot, !!usage.snapshot && !!usage.error);
|
|
539
542
|
const message = `Switch default from ${agentLabel(agentConfig.id)}@${currentDefault} to ${agentLabel(agentConfig.id)}@${installedVersion}${accountHint}?`;
|
|
540
543
|
const setAsDefault = await confirm({
|
|
541
544
|
message,
|
|
@@ -702,7 +705,12 @@ export function registerVersionsCommands(program) {
|
|
|
702
705
|
const accountInfo = pickerAccountMap.get(v);
|
|
703
706
|
const email = accountInfo?.email || '';
|
|
704
707
|
const usageKey = getUsageLookupKey(accountInfo);
|
|
705
|
-
const
|
|
708
|
+
const versionUsage = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
709
|
+
const usageSummary = usageKey
|
|
710
|
+
? formatUsageSummary(null, versionUsage?.snapshot || null, 3, {
|
|
711
|
+
unverified: !!versionUsage?.snapshot && !!versionUsage.error,
|
|
712
|
+
})
|
|
713
|
+
: '';
|
|
706
714
|
if (maxEmailLen > 0) {
|
|
707
715
|
label += ' ';
|
|
708
716
|
label += email ? chalk.cyan(email.padEnd(maxEmailLen)) : ' '.repeat(maxEmailLen);
|
package/dist/commands/view.d.ts
CHANGED
|
@@ -30,6 +30,17 @@ export interface ViewSectionFilter {
|
|
|
30
30
|
/** Trim a description to a column-friendly snippet. Strips newlines, collapses whitespace. */
|
|
31
31
|
export declare function summarizeDescription(desc: string | undefined, maxLen?: number): string;
|
|
32
32
|
export declare function descriptionForPrefix(desc: string | undefined, prefix: string): string;
|
|
33
|
+
/**
|
|
34
|
+
* Render custom harnesses as their own agent-type blocks — peers of the native
|
|
35
|
+
* Claude/Codex blocks, not indented rows under the host CLI that executes them.
|
|
36
|
+
* `agents run <name>` already treats a custom harness like a native agent id, so
|
|
37
|
+
* `agents view` lists it the same way: a bold name header, then one row carrying
|
|
38
|
+
* the model, the account/auth state, and the host it runs on.
|
|
39
|
+
*
|
|
40
|
+
* `installedHosts` is the set of agent ids with a usable install; a harness whose
|
|
41
|
+
* host is missing is flagged rather than silently listed as runnable.
|
|
42
|
+
*/
|
|
43
|
+
export declare function renderHarnessBlocks(harnesses: ProfileSummary[], installedHosts: Set<AgentId>, showPaths: boolean): void;
|
|
33
44
|
/** Machine-readable entry for a single installed version. */
|
|
34
45
|
export interface ViewJsonVersion {
|
|
35
46
|
version: string;
|
|
@@ -47,6 +58,7 @@ export interface ViewJsonVersion {
|
|
|
47
58
|
} | null;
|
|
48
59
|
windows: Array<{
|
|
49
60
|
key: 'session' | 'week' | 'sonnet_week' | 'month';
|
|
61
|
+
label?: string;
|
|
50
62
|
usedPercent: number;
|
|
51
63
|
resetsAt: string | null;
|
|
52
64
|
}>;
|
|
@@ -64,7 +76,8 @@ export interface ViewJsonVersion {
|
|
|
64
76
|
export interface ViewJsonAgent {
|
|
65
77
|
agent: AgentId;
|
|
66
78
|
versions: ViewJsonVersion[];
|
|
67
|
-
|
|
79
|
+
/** Custom harnesses that run on this agent as their host CLI. */
|
|
80
|
+
harnesses: ProfileSummary[];
|
|
68
81
|
}
|
|
69
82
|
/** Resource sections that --resources can include in --json output. */
|
|
70
83
|
export type ResourceSection = 'commands' | 'skills' | 'mcp' | 'memory' | 'hooks' | 'workflows' | 'plugins';
|
package/dist/commands/view.js
CHANGED
|
@@ -25,7 +25,8 @@ import { composeRulesFromState } from '../lib/rules/compose.js';
|
|
|
25
25
|
import { getConfiguredRunStrategy } from '../lib/rotate.js';
|
|
26
26
|
import { resolveRunDefaults } from '../lib/run-defaults.js';
|
|
27
27
|
import { resolveConfiguredModel } from '../lib/models.js';
|
|
28
|
-
import { listProfiles, profileSummary } from '../lib/profiles.js';
|
|
28
|
+
import { listProfiles, profileExists, profileSummary, readProfile } from '../lib/profiles.js';
|
|
29
|
+
import { renderHarnessDetail } from './harness.js';
|
|
29
30
|
import { loadManifest, isStale } from '../lib/staleness/index.js';
|
|
30
31
|
import { confirm } from '@inquirer/prompts';
|
|
31
32
|
import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
|
|
@@ -33,28 +34,19 @@ import { terminalWidth, truncateToWidth, stringWidth } from '../lib/session/widt
|
|
|
33
34
|
/** Shared account identity formatter, re-exported for the view-specific tests. */
|
|
34
35
|
export const accountColumnLabel = accountDisplayLabel;
|
|
35
36
|
/**
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
* `listProfiles
|
|
37
|
+
* Custom harnesses (the `~/.agents/profiles/*.yml` bundles), sorted by name and
|
|
38
|
+
* optionally narrowed to the ones that run on one host agent. YAMLs that fail
|
|
39
|
+
* validation are silently skipped by `listProfiles`, so this never throws on a
|
|
40
|
+
* malformed file.
|
|
39
41
|
*/
|
|
40
|
-
function
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
continue;
|
|
45
|
-
const summary = profileSummary(profile);
|
|
46
|
-
const existing = byAgent.get(profile.host.agent);
|
|
47
|
-
if (existing)
|
|
48
|
-
existing.push(summary);
|
|
49
|
-
else
|
|
50
|
-
byAgent.set(profile.host.agent, [summary]);
|
|
51
|
-
}
|
|
52
|
-
return byAgent;
|
|
42
|
+
function getHarnesses(filterAgentId) {
|
|
43
|
+
return listProfiles()
|
|
44
|
+
.filter((profile) => !filterAgentId || profile.host.agent === filterAgentId)
|
|
45
|
+
.map(profileSummary);
|
|
53
46
|
}
|
|
54
|
-
/**
|
|
55
|
-
function
|
|
56
|
-
|
|
57
|
-
return `${kind} ${model}`;
|
|
47
|
+
/** "via <host> <version>" — which native harness actually executes this one. */
|
|
48
|
+
function harnessHostTag(harness) {
|
|
49
|
+
return harness.hostVersion ? `via ${harness.agent} ${harness.hostVersion}` : `via ${harness.agent}`;
|
|
58
50
|
}
|
|
59
51
|
/**
|
|
60
52
|
* Resolve a resource path to something the IDE can open inline. When `p` is a
|
|
@@ -147,31 +139,38 @@ export function descriptionForPrefix(desc, prefix) {
|
|
|
147
139
|
const budget = Math.max(1, terminalWidth() - stringWidth(visiblePrefix));
|
|
148
140
|
return summarizeDescription(desc, budget);
|
|
149
141
|
}
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
142
|
+
/**
|
|
143
|
+
* Render custom harnesses as their own agent-type blocks — peers of the native
|
|
144
|
+
* Claude/Codex blocks, not indented rows under the host CLI that executes them.
|
|
145
|
+
* `agents run <name>` already treats a custom harness like a native agent id, so
|
|
146
|
+
* `agents view` lists it the same way: a bold name header, then one row carrying
|
|
147
|
+
* the model, the account/auth state, and the host it runs on.
|
|
148
|
+
*
|
|
149
|
+
* `installedHosts` is the set of agent ids with a usable install; a harness whose
|
|
150
|
+
* host is missing is flagged rather than silently listed as runnable.
|
|
151
|
+
*/
|
|
152
|
+
export function renderHarnessBlocks(harnesses, installedHosts, showPaths) {
|
|
153
|
+
if (harnesses.length === 0)
|
|
157
154
|
return;
|
|
158
|
-
const
|
|
159
|
-
const
|
|
160
|
-
const
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
155
|
+
const modelWidth = Math.max(...harnesses.map((h) => h.model.length));
|
|
156
|
+
const authWidth = Math.max(...harnesses.map((h) => h.auth.length));
|
|
157
|
+
for (const harness of harnesses) {
|
|
158
|
+
// The `via <host>` tag on the row already names a native fork parent, so
|
|
159
|
+
// only a fork of another custom harness adds lineage worth printing.
|
|
160
|
+
const origin = harness.forkedFrom && harness.forkedFrom !== harness.agent
|
|
161
|
+
? `custom · forked from ${harness.forkedFrom}`
|
|
162
|
+
: 'custom';
|
|
163
|
+
const missingHost = installedHosts.has(harness.agent)
|
|
164
|
+
? ''
|
|
165
|
+
: chalk.yellow(` (host ${harness.agent} not installed)`);
|
|
166
|
+
console.log(` ${chalk.bold(harness.label)}${chalk.gray(` (${origin})`)}${missingHost}`);
|
|
167
|
+
console.log(` ${chalk.yellow(harness.model.padEnd(modelWidth))} ` +
|
|
168
|
+
`${chalk.cyan(harness.auth.padEnd(authWidth))} ` +
|
|
169
|
+
chalk.gray(harnessHostTag(harness)));
|
|
170
|
+
if (showPaths)
|
|
171
|
+
console.log(chalk.gray(` ${harness.path}`));
|
|
172
|
+
console.log();
|
|
173
|
+
}
|
|
175
174
|
}
|
|
176
175
|
/**
|
|
177
176
|
* Show installed versions for one or all agents.
|
|
@@ -267,8 +266,7 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
267
266
|
.map(async (agentId) => [agentId, await getUnmanagedCliState(agentId)])));
|
|
268
267
|
spinner.stop();
|
|
269
268
|
const showPaths = !!filterAgentId;
|
|
270
|
-
const
|
|
271
|
-
const profileSummaries = [...profilesByAgent.values()].flat();
|
|
269
|
+
const harnesses = getHarnesses(filterAgentId);
|
|
272
270
|
// Auto-heal stale versioned aliases. Pre-v2 aliases (e.g. pre-CLAUDE_CONFIG_DIR
|
|
273
271
|
// claude shims) silently route login through the default version's symlinked
|
|
274
272
|
// home, so `agents view` would never reflect the right account. Regenerate on
|
|
@@ -367,11 +365,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
367
365
|
// Separate version-managed from globally-installed agents
|
|
368
366
|
const versionManaged = [];
|
|
369
367
|
const globallyInstalled = [];
|
|
370
|
-
const profileOnly = [];
|
|
371
368
|
for (const agentId of agentsToShow) {
|
|
372
369
|
const versions = listInstalledVersions(agentId);
|
|
373
370
|
const cliState = cliStates[agentId];
|
|
374
|
-
const hasProfiles = (profilesByAgent.get(agentId)?.length ?? 0) > 0;
|
|
375
371
|
if (versions.length > 0) {
|
|
376
372
|
versionManaged.push(agentId);
|
|
377
373
|
}
|
|
@@ -380,10 +376,10 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
380
376
|
if (!hasNonIsolatedVersion(agentId))
|
|
381
377
|
globallyInstalled.push(agentId);
|
|
382
378
|
}
|
|
383
|
-
else if (versions.length === 0 && hasProfiles) {
|
|
384
|
-
profileOnly.push(agentId);
|
|
385
|
-
}
|
|
386
379
|
}
|
|
380
|
+
// A custom harness runs through its host CLI, so it is only launchable when
|
|
381
|
+
// that host has an install of some kind.
|
|
382
|
+
const installedHosts = new Set([...versionManaged, ...globallyInstalled]);
|
|
387
383
|
// For self-updating global-binary agents (droid) the on-disk version-dir name
|
|
388
384
|
// is a stale label — the real version is whatever `<cli> --version` reports.
|
|
389
385
|
// Resolve it once so every row/width pass shows the live version, while the
|
|
@@ -443,12 +439,6 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
443
439
|
maxModelWidth = Math.max(maxModelWidth, model.length);
|
|
444
440
|
}
|
|
445
441
|
}
|
|
446
|
-
// Profile rows share these columns with version rows so they line up.
|
|
447
|
-
for (const profile of profilesByAgent.get(agentId) ?? []) {
|
|
448
|
-
maxVerLabel = Math.max(maxVerLabel, profile.name.length);
|
|
449
|
-
maxEmail = Math.max(maxEmail, profile.auth.length);
|
|
450
|
-
maxPlanWidth = Math.max(maxPlanWidth, 'profile'.length);
|
|
451
|
-
}
|
|
452
442
|
}
|
|
453
443
|
// Second pass: compute max visible usage + status widths (now that maxPlanWidth is settled)
|
|
454
444
|
for (const agentId of versionManaged) {
|
|
@@ -459,15 +449,12 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
459
449
|
const usageKey = getUsageLookupKey(info);
|
|
460
450
|
const usageInfo = usageKey ? usageByKey.get(usageKey) : undefined;
|
|
461
451
|
const usageUnavailable = agentReportsUsage(agentId) && !!info?.signedIn && !usageInfo?.snapshot;
|
|
462
|
-
const
|
|
452
|
+
const usageUnverified = !!usageInfo?.snapshot && !!usageInfo.error;
|
|
453
|
+
const usageStr = formatUsageSummary(info?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable, unverified: usageUnverified });
|
|
463
454
|
maxUsageWidth = Math.max(maxUsageWidth, visibleWidth(usageStr));
|
|
464
455
|
const statusStr = formatUsageStatusBadge(info?.usageStatus);
|
|
465
456
|
maxStatusWidth = Math.max(maxStatusWidth, visibleWidth(statusStr));
|
|
466
457
|
}
|
|
467
|
-
for (const profile of profilesByAgent.get(agentId) ?? []) {
|
|
468
|
-
const usageEquivalent = profileKindAndModel(profile.model, maxPlanWidth);
|
|
469
|
-
maxUsageWidth = Math.max(maxUsageWidth, visibleWidth(usageEquivalent));
|
|
470
|
-
}
|
|
471
458
|
}
|
|
472
459
|
for (const agentId of versionManaged) {
|
|
473
460
|
const agent = AGENTS[agentId];
|
|
@@ -518,7 +505,8 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
518
505
|
const hasEmail = !!vInfo?.email;
|
|
519
506
|
const signedIn = !!vInfo?.signedIn;
|
|
520
507
|
const usageUnavailable = agentReportsUsage(agentId) && signedIn && !usageInfo?.snapshot;
|
|
521
|
-
const
|
|
508
|
+
const usageUnverified = !!usageInfo?.snapshot && !!usageInfo.error;
|
|
509
|
+
const usageStr = formatUsageSummary(vInfo?.plan || null, usageInfo?.snapshot || null, maxPlanWidth, { unavailable: usageUnavailable, unverified: usageUnverified });
|
|
522
510
|
const hasUsage = usageStr.length > 0;
|
|
523
511
|
// Only show lastActive for versions with an actual logged-in account.
|
|
524
512
|
// Otherwise it reflects install time (misleading "just now" for fresh installs).
|
|
@@ -577,20 +565,6 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
577
565
|
console.log(chalk.gray(` ${versionDir}`));
|
|
578
566
|
}
|
|
579
567
|
}
|
|
580
|
-
// Profile rows share the same columns as versions: name | auth | "profile"+model.
|
|
581
|
-
// No status badge, no last-active — profiles don't accumulate usage state.
|
|
582
|
-
for (const profile of profilesByAgent.get(agentId) ?? []) {
|
|
583
|
-
const nameCol = chalk.cyan(profile.name.padEnd(maxVerLabel));
|
|
584
|
-
// Pad the model column so profile rows line up with version rows.
|
|
585
|
-
const modelPad = maxModelWidth > 0 ? `${' '.repeat(maxModelWidth)} ` : '';
|
|
586
|
-
const authCol = chalk.gray(profile.auth.padEnd(maxEmail));
|
|
587
|
-
const usageEquivalent = profileKindAndModel(profile.model, maxPlanWidth);
|
|
588
|
-
const usagePad = ' '.repeat(Math.max(0, maxUsageWidth - visibleWidth(usageEquivalent)));
|
|
589
|
-
console.log(` ${nameCol} ${modelPad}${authCol} ${chalk.gray(usageEquivalent + usagePad)}`);
|
|
590
|
-
if (showPaths) {
|
|
591
|
-
console.log(chalk.gray(` ${profile.path}`));
|
|
592
|
-
}
|
|
593
|
-
}
|
|
594
568
|
// Check for project override
|
|
595
569
|
const projectVersion = getProjectVersionFromCwd(agentId);
|
|
596
570
|
if (projectVersion && projectVersion !== globalDefault) {
|
|
@@ -599,6 +573,10 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
599
573
|
console.log();
|
|
600
574
|
}
|
|
601
575
|
}
|
|
576
|
+
// Custom harnesses sit in the same list as the native ones — they are run the
|
|
577
|
+
// same way (`agents run <name>`), so they read as their own agent type rather
|
|
578
|
+
// than as an indented row under whichever host CLI executes them.
|
|
579
|
+
renderHarnessBlocks(harnesses, installedHosts, showPaths);
|
|
602
580
|
// Show globally installed (not managed) agents
|
|
603
581
|
if (globallyInstalled.length > 0) {
|
|
604
582
|
console.log(chalk.bold('Not Managed by Agents CLI\n'));
|
|
@@ -632,7 +610,9 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
632
610
|
const parts = [` ${verLabel}${padding}`];
|
|
633
611
|
const gUsageKey = getUsageLookupKey(gInfo);
|
|
634
612
|
const gUsage = gUsageKey ? usageByKey.get(gUsageKey) : undefined;
|
|
635
|
-
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null
|
|
613
|
+
const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null, 3, {
|
|
614
|
+
unverified: !!gUsage?.snapshot && !!gUsage.error,
|
|
615
|
+
});
|
|
636
616
|
const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
|
|
637
617
|
if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
|
|
638
618
|
const gDisplay = accountColumnLabel(gInfo);
|
|
@@ -651,25 +631,6 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
651
631
|
if (showPaths && cliState?.path) {
|
|
652
632
|
console.log(chalk.gray(` ${cliState.path}`));
|
|
653
633
|
}
|
|
654
|
-
// Profile rows under a globally-installed harness. Use a simpler
|
|
655
|
-
// alignment here since this section doesn't share column state with
|
|
656
|
-
// the version-managed block.
|
|
657
|
-
// An isolated-only agent now appears in BOTH blocks; its profiles already
|
|
658
|
-
// rendered under the version-managed one, so don't print them twice.
|
|
659
|
-
const profilesHere = versionManaged.includes(agentId) ? [] : (profilesByAgent.get(agentId) ?? []);
|
|
660
|
-
if (profilesHere.length > 0) {
|
|
661
|
-
const nameWidth = Math.max(globalMaxVerLabel, ...profilesHere.map((p) => p.name.length));
|
|
662
|
-
const authWidth = Math.max(...profilesHere.map((p) => p.auth.length));
|
|
663
|
-
for (const profile of profilesHere) {
|
|
664
|
-
console.log(` ${chalk.cyan(profile.name.padEnd(nameWidth))} ` +
|
|
665
|
-
`${chalk.gray(profile.auth.padEnd(authWidth))} ` +
|
|
666
|
-
`${chalk.gray('profile')} ` +
|
|
667
|
-
chalk.gray(profile.model));
|
|
668
|
-
if (showPaths) {
|
|
669
|
-
console.log(chalk.gray(` ${profile.path}`));
|
|
670
|
-
}
|
|
671
|
-
}
|
|
672
|
-
}
|
|
673
634
|
if (agent.npmPackage && cliState?.version) {
|
|
674
635
|
console.log(chalk.gray(` Manage: agents add ${agentId}@${cliState.version} -y`));
|
|
675
636
|
}
|
|
@@ -681,47 +642,44 @@ async function showInstalledVersions(filterAgentId, viewOpts) {
|
|
|
681
642
|
console.log();
|
|
682
643
|
}
|
|
683
644
|
}
|
|
684
|
-
// Agents with no install but with profiles defined — render under the same
|
|
685
|
-
// harness header so users find them where they look.
|
|
686
|
-
if (profileOnly.length > 0) {
|
|
687
|
-
if (versionManaged.length === 0 && globallyInstalled.length === 0) {
|
|
688
|
-
console.log(chalk.bold('Profile-only Agents\n'));
|
|
689
|
-
}
|
|
690
|
-
for (const agentId of profileOnly) {
|
|
691
|
-
const profilesHere = profilesByAgent.get(agentId) ?? [];
|
|
692
|
-
console.log(` ${chalk.bold(agentLabel(agentId))}${chalk.yellow(' (profile only)')}`);
|
|
693
|
-
const nameWidth = Math.max(...profilesHere.map((p) => p.name.length));
|
|
694
|
-
const authWidth = Math.max(...profilesHere.map((p) => p.auth.length));
|
|
695
|
-
for (const profile of profilesHere) {
|
|
696
|
-
console.log(` ${chalk.cyan(profile.name.padEnd(nameWidth))} ` +
|
|
697
|
-
`${chalk.gray(profile.auth.padEnd(authWidth))} ` +
|
|
698
|
-
`${chalk.gray('profile')} ` +
|
|
699
|
-
chalk.gray(profile.model));
|
|
700
|
-
if (showPaths) {
|
|
701
|
-
console.log(chalk.gray(` ${profile.path}`));
|
|
702
|
-
}
|
|
703
|
-
}
|
|
704
|
-
console.log();
|
|
705
|
-
}
|
|
706
|
-
}
|
|
707
645
|
// If filtering to a specific agent and not found
|
|
708
646
|
if (filterAgentId &&
|
|
709
647
|
versionManaged.length === 0 &&
|
|
710
648
|
globallyInstalled.length === 0 &&
|
|
711
|
-
|
|
649
|
+
harnesses.length === 0) {
|
|
712
650
|
console.log(` ${chalk.bold(agentLabel(filterAgentId))}: ${chalk.gray('not installed')}`);
|
|
713
651
|
console.log();
|
|
714
652
|
}
|
|
715
653
|
// No agents installed at all
|
|
716
654
|
if (versionManaged.length === 0 &&
|
|
717
655
|
globallyInstalled.length === 0 &&
|
|
718
|
-
|
|
719
|
-
profileSummaries.length === 0 &&
|
|
656
|
+
harnesses.length === 0 &&
|
|
720
657
|
!filterAgentId) {
|
|
721
658
|
console.log(chalk.gray(' No agent CLIs installed.'));
|
|
722
659
|
console.log(chalk.gray(' Run: agents add claude@latest'));
|
|
723
660
|
console.log();
|
|
724
661
|
}
|
|
662
|
+
// `--refresh` used to print a table that looked fully refreshed no matter how
|
|
663
|
+
// many accounts it had failed to reach, so a box whose every Claude credential
|
|
664
|
+
// had expired rendered identically to a healthy one — the bars beside each row
|
|
665
|
+
// came from a cache that the run had not managed to update. Name the accounts
|
|
666
|
+
// it could not confirm, and why.
|
|
667
|
+
if (viewOpts?.forceRefresh) {
|
|
668
|
+
const unrefreshed = [];
|
|
669
|
+
for (const [key, usage] of usageByKey) {
|
|
670
|
+
if (!usage.error)
|
|
671
|
+
continue;
|
|
672
|
+
const label = canonicalByUsageKey.get(key)?.email ?? key;
|
|
673
|
+
unrefreshed.push(` ${label.padEnd(24)} ${chalk.gray(usage.error)}`);
|
|
674
|
+
}
|
|
675
|
+
if (unrefreshed.length > 0) {
|
|
676
|
+
const noun = unrefreshed.length === 1 ? 'account' : 'accounts';
|
|
677
|
+
console.log(chalk.yellow(` Could not refresh ${unrefreshed.length} ${noun} — bars above are the last cached reading:`));
|
|
678
|
+
for (const line of unrefreshed)
|
|
679
|
+
console.log(line);
|
|
680
|
+
console.log();
|
|
681
|
+
}
|
|
682
|
+
}
|
|
725
683
|
// Host CLIs are host-global, not per-agent — show them once in the overview.
|
|
726
684
|
if (!filterAgentId) {
|
|
727
685
|
renderHostClisSection(process.cwd());
|
|
@@ -1246,6 +1204,7 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
|
1246
1204
|
windows: snapshot
|
|
1247
1205
|
? snapshot.windows.map((w) => ({
|
|
1248
1206
|
key: w.key,
|
|
1207
|
+
label: w.label,
|
|
1249
1208
|
usedPercent: w.usedPercent,
|
|
1250
1209
|
resetsAt: w.resetsAt ? w.resetsAt.toISOString() : null,
|
|
1251
1210
|
}))
|
|
@@ -1263,7 +1222,7 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
|
1263
1222
|
else
|
|
1264
1223
|
byAgent.set(agentId, [entry]);
|
|
1265
1224
|
}
|
|
1266
|
-
const
|
|
1225
|
+
const harnesses = getHarnesses(filterAgentId);
|
|
1267
1226
|
const out = [];
|
|
1268
1227
|
for (const agentId of agentsToShow) {
|
|
1269
1228
|
const versions = byAgent.get(agentId) ?? [];
|
|
@@ -1272,7 +1231,7 @@ async function collectAgentsJson(filterAgentId, resourceSections) {
|
|
|
1272
1231
|
return a.isDefault ? -1 : 1;
|
|
1273
1232
|
return compareVersions(b.version, a.version);
|
|
1274
1233
|
});
|
|
1275
|
-
out.push({ agent: agentId, versions,
|
|
1234
|
+
out.push({ agent: agentId, versions, harnesses: harnesses.filter((h) => h.agent === agentId) });
|
|
1276
1235
|
}
|
|
1277
1236
|
return out;
|
|
1278
1237
|
}
|
|
@@ -1536,6 +1495,17 @@ export async function viewAction(agentArg, options) {
|
|
|
1536
1495
|
const agentName = parts[0];
|
|
1537
1496
|
const agentId = resolveAgentName(agentName);
|
|
1538
1497
|
if (!agentId) {
|
|
1498
|
+
// A custom harness is an agent type here, not an unknown name: `agents run
|
|
1499
|
+
// <name>` launches it, so `agents view <name>` describes it.
|
|
1500
|
+
if (profileExists(agentName)) {
|
|
1501
|
+
const harness = profileSummary(readProfile(agentName));
|
|
1502
|
+
if (json) {
|
|
1503
|
+
console.log(JSON.stringify(harness, null, 2));
|
|
1504
|
+
return;
|
|
1505
|
+
}
|
|
1506
|
+
renderHarnessDetail(agentName);
|
|
1507
|
+
return;
|
|
1508
|
+
}
|
|
1539
1509
|
if (json) {
|
|
1540
1510
|
console.log(JSON.stringify({ error: formatAgentError(agentName) }));
|
|
1541
1511
|
process.exit(1);
|
|
@@ -1575,7 +1545,7 @@ export async function viewAction(agentArg, options) {
|
|
|
1575
1545
|
// --json ignores the @version suffix, but --resources/--detailed (or a
|
|
1576
1546
|
// section flag) now attach each version's resource inventory + sync-state.
|
|
1577
1547
|
const data = await collectAgentsJson(agentId, resourceSections);
|
|
1578
|
-
console.log(JSON.stringify(data[0] ?? { agent: agentId, versions: [],
|
|
1548
|
+
console.log(JSON.stringify(data[0] ?? { agent: agentId, versions: [], harnesses: [] }, null, 2));
|
|
1579
1549
|
return;
|
|
1580
1550
|
}
|
|
1581
1551
|
if (requestedVersion) {
|
|
@@ -1622,6 +1592,9 @@ Examples:
|
|
|
1622
1592
|
# Show versions for one agent
|
|
1623
1593
|
agents view claude
|
|
1624
1594
|
|
|
1595
|
+
# Describe one custom harness (host, model, provider, auth, path)
|
|
1596
|
+
agents view deepseek-flash
|
|
1597
|
+
|
|
1625
1598
|
# Detailed view: resources, commands, skills, MCP servers for a specific version
|
|
1626
1599
|
agents view claude@2.1.112
|
|
1627
1600
|
agents view claude@default
|
|
@@ -1655,8 +1628,10 @@ When to use:
|
|
|
1655
1628
|
- Cleaning up stale versions left behind after upgrading (--prune)
|
|
1656
1629
|
|
|
1657
1630
|
Output:
|
|
1658
|
-
- Without arguments: table of all agents with versions, emails, usage stats
|
|
1631
|
+
- Without arguments: table of all agents with versions, emails, usage stats,
|
|
1632
|
+
then one block per custom harness (see 'agents harness')
|
|
1659
1633
|
- With agent name: versions for that agent, showing which is the default
|
|
1634
|
+
- With a custom harness name: that harness's host, model, provider, and auth
|
|
1660
1635
|
- With agent@version: detailed breakdown of resources synced to that version
|
|
1661
1636
|
- With --json: structured JSON with version, isDefault, signedIn, email, plan,
|
|
1662
1637
|
usageStatus, per-window usedPercent, lastActive, and path
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -242,8 +242,10 @@ export declare function decryptDroidAuthFile(filePath: string, keyPath: string):
|
|
|
242
242
|
* -> email / org_id / sub.
|
|
243
243
|
* - kimi: credentials/kimi-code.json -> access-token JWT -> user_id / sub.
|
|
244
244
|
* - antigravity: antigravity-oauth-token -> token.refresh_token -> JWT sub
|
|
245
|
-
* when the token is a JWT, else the raw refresh-token
|
|
246
|
-
* consumer tokens are stable per login
|
|
245
|
+
* when the token is a JWT, else a SHA-256 hash of the raw refresh-token
|
|
246
|
+
* value (opaque Google consumer tokens are stable per login — hashed so
|
|
247
|
+
* the identity key, which is persisted as a usage-cache key, never carries
|
|
248
|
+
* a live credential).
|
|
247
249
|
* Two directories for the SAME account compare equal; two DIFFERENT accounts
|
|
248
250
|
* compare distinct. Used by carryForwardAuthFiles to refuse overwriting one
|
|
249
251
|
* account's login with a credential that belongs to a DIFFERENT account
|
package/dist/lib/agents.js
CHANGED
|
@@ -1190,8 +1190,10 @@ export function decryptDroidAuthFile(filePath, keyPath) {
|
|
|
1190
1190
|
* -> email / org_id / sub.
|
|
1191
1191
|
* - kimi: credentials/kimi-code.json -> access-token JWT -> user_id / sub.
|
|
1192
1192
|
* - antigravity: antigravity-oauth-token -> token.refresh_token -> JWT sub
|
|
1193
|
-
* when the token is a JWT, else the raw refresh-token
|
|
1194
|
-
* consumer tokens are stable per login
|
|
1193
|
+
* when the token is a JWT, else a SHA-256 hash of the raw refresh-token
|
|
1194
|
+
* value (opaque Google consumer tokens are stable per login — hashed so
|
|
1195
|
+
* the identity key, which is persisted as a usage-cache key, never carries
|
|
1196
|
+
* a live credential).
|
|
1195
1197
|
* Two directories for the SAME account compare equal; two DIFFERENT accounts
|
|
1196
1198
|
* compare distinct. Used by carryForwardAuthFiles to refuse overwriting one
|
|
1197
1199
|
* account's login with a credential that belongs to a DIFFERENT account
|
|
@@ -1226,7 +1228,11 @@ export function readAuthAccountIdentity(agent, configDir) {
|
|
|
1226
1228
|
return null;
|
|
1227
1229
|
const claims = decodeJwtPayload(refreshToken);
|
|
1228
1230
|
const sub = normalizeIdentityPart(claims?.sub ?? claims?.user_id);
|
|
1229
|
-
|
|
1231
|
+
// An opaque (non-JWT) Google refresh token IS the credential — hash it
|
|
1232
|
+
// so the identity key stays stable per login without embedding a live
|
|
1233
|
+
// secret (the key is persisted as a usage-cache filename key).
|
|
1234
|
+
const fallback = crypto.createHash('sha256').update(refreshToken).digest('hex').slice(0, 16);
|
|
1235
|
+
return buildIdentityKey(agent, [['sub', sub ?? fallback]]);
|
|
1230
1236
|
}
|
|
1231
1237
|
default:
|
|
1232
1238
|
return null;
|
|
@@ -1648,11 +1654,20 @@ export async function getAccountInfo(agentId, home) {
|
|
|
1648
1654
|
if (tokenPath) {
|
|
1649
1655
|
const data = JSON.parse(await fs.promises.readFile(tokenPath, 'utf-8'));
|
|
1650
1656
|
if (typeof data?.token?.refresh_token === 'string' && data.token.refresh_token) {
|
|
1651
|
-
|
|
1657
|
+
// A stable account/usage key (derived from the refresh token — see
|
|
1658
|
+
// readAuthAccountIdentity) lets `agents view` dedupe and cache the
|
|
1659
|
+
// per-model quota bars for this login.
|
|
1660
|
+
const identity = readAuthAccountIdentity('antigravity', path.dirname(tokenPath));
|
|
1661
|
+
return { ...empty, signedIn: true, lastActive, accountKey: identity, usageKey: identity };
|
|
1652
1662
|
}
|
|
1653
1663
|
}
|
|
1654
|
-
if (await antigravityKeychainSignedIn())
|
|
1655
|
-
|
|
1664
|
+
if (await antigravityKeychainSignedIn()) {
|
|
1665
|
+
// Keyring-only login (the macOS case): the OS keyring holds exactly
|
|
1666
|
+
// ONE antigravity credential, so a stable singleton key identifies it
|
|
1667
|
+
// for usage-cache dedup without reading the secret value here.
|
|
1668
|
+
const identity = buildIdentityKey('antigravity', [['sub', 'keychain']]);
|
|
1669
|
+
return { ...empty, signedIn: true, lastActive, accountKey: identity, usageKey: identity };
|
|
1670
|
+
}
|
|
1656
1671
|
return { ...empty, lastActive };
|
|
1657
1672
|
}
|
|
1658
1673
|
case 'kimi': {
|