@phnx-labs/agents-cli 1.20.66 → 1.20.68
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 +88 -0
- package/README.md +3 -0
- package/dist/bin/agents +0 -0
- package/dist/commands/check.js +96 -2
- package/dist/commands/doctor.js +37 -9
- package/dist/commands/exec.js +45 -1
- package/dist/commands/plugins.js +11 -2
- package/dist/commands/repo.js +47 -2
- package/dist/commands/ssh.js +103 -2
- package/dist/commands/teams.d.ts +11 -0
- package/dist/commands/teams.js +40 -2
- package/dist/commands/view.d.ts +1 -0
- package/dist/commands/view.js +12 -8
- package/dist/lib/agents.d.ts +11 -5
- package/dist/lib/agents.js +35 -24
- package/dist/lib/browser/profiles.js +4 -25
- package/dist/lib/cli-entry.d.ts +23 -0
- package/dist/lib/cli-entry.js +116 -0
- package/dist/lib/daemon.d.ts +2 -2
- package/dist/lib/daemon.js +8 -89
- package/dist/lib/devices/fleet.d.ts +23 -0
- package/dist/lib/devices/fleet.js +38 -0
- package/dist/lib/devices/health-report.d.ts +46 -0
- package/dist/lib/devices/health-report.js +159 -0
- package/dist/lib/devices/health.d.ts +14 -4
- package/dist/lib/devices/health.js +49 -8
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +31 -6
- package/dist/lib/git.d.ts +10 -0
- package/dist/lib/git.js +23 -0
- package/dist/lib/hosts/option.js +1 -1
- package/dist/lib/hosts/ready.js +5 -3
- package/dist/lib/hosts/remote-cmd.d.ts +12 -0
- package/dist/lib/hosts/remote-cmd.js +17 -1
- package/dist/lib/mcp.d.ts +21 -0
- package/dist/lib/mcp.js +278 -13
- package/dist/lib/resources/mcp.js +20 -342
- package/dist/lib/routines.d.ts +13 -0
- package/dist/lib/routines.js +21 -0
- package/dist/lib/runner.js +24 -47
- package/dist/lib/secrets/agent.js +11 -15
- package/dist/lib/share/capture.js +21 -3
- package/dist/lib/signin-badge.d.ts +41 -0
- package/dist/lib/signin-badge.js +64 -0
- package/dist/lib/ssh-exec.d.ts +5 -0
- package/dist/lib/ssh-exec.js +55 -1
- package/dist/lib/types.d.ts +10 -0
- package/dist/lib/usage.d.ts +17 -3
- package/dist/lib/usage.js +63 -16
- package/package.json +1 -1
|
@@ -100,7 +100,17 @@ export async function captureCover(htmlPath, timeoutMs = 15_000) {
|
|
|
100
100
|
if (!fs.existsSync(abs))
|
|
101
101
|
return null;
|
|
102
102
|
const fileUrl = `file://${abs.split('/').map(encodeURIComponent).join('/')}`;
|
|
103
|
-
|
|
103
|
+
const candidates = candidateBrowsers();
|
|
104
|
+
if (candidates.length === 0) {
|
|
105
|
+
// A silently-dropped cover reads as "the CLI decided this page needs none",
|
|
106
|
+
// when in fact no headless browser was found. Say so and point at the escape
|
|
107
|
+
// hatch instead of leaving the publish coverless with no explanation.
|
|
108
|
+
process.stderr.write('[agents share] no headless browser found for the OG cover — install Chrome/Chromium ' +
|
|
109
|
+
'or set AGENTS_SHARE_BROWSER=/path/to/chrome. Publishing without a preview image.\n');
|
|
110
|
+
return null;
|
|
111
|
+
}
|
|
112
|
+
let lastFailure = '';
|
|
113
|
+
for (const bin of candidates) {
|
|
104
114
|
const outPng = path.join(os.tmpdir(), `agents-share-cover-${process.pid}-${Date.now()}.png`);
|
|
105
115
|
const userDir = fs.mkdtempSync(path.join(os.tmpdir(), 'agents-share-chrome-'));
|
|
106
116
|
try {
|
|
@@ -126,15 +136,23 @@ export async function captureCover(htmlPath, timeoutMs = 15_000) {
|
|
|
126
136
|
// A valid PNG starts with the 8-byte signature; guard against 0-byte writes.
|
|
127
137
|
if (buf.length > 8 && buf[0] === 0x89 && buf[1] === 0x50)
|
|
128
138
|
return buf;
|
|
139
|
+
lastFailure = `${path.basename(bin)}: produced no valid PNG`;
|
|
140
|
+
}
|
|
141
|
+
else {
|
|
142
|
+
lastFailure = `${path.basename(bin)}: no screenshot written`;
|
|
129
143
|
}
|
|
130
144
|
}
|
|
131
|
-
catch {
|
|
132
|
-
|
|
145
|
+
catch (err) {
|
|
146
|
+
lastFailure = `${path.basename(bin)}: ${err.message}`;
|
|
133
147
|
}
|
|
134
148
|
finally {
|
|
135
149
|
fs.rmSync(outPng, { force: true });
|
|
136
150
|
fs.rmSync(userDir, { recursive: true, force: true });
|
|
137
151
|
}
|
|
138
152
|
}
|
|
153
|
+
// Every candidate ran but none yielded a cover — surface the last reason so a
|
|
154
|
+
// missing preview card is diagnosable (timeout, crash, bad binary) rather than silent.
|
|
155
|
+
process.stderr.write(`[agents share] OG cover capture failed (${lastFailure || 'unknown error'}) — ` +
|
|
156
|
+
'publishing without a preview image. Set AGENTS_SHARE_BROWSER to override.\n');
|
|
139
157
|
return null;
|
|
140
158
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import type { AccountInfo } from './agents.js';
|
|
2
|
+
import type { AgentId } from './types.js';
|
|
3
|
+
/**
|
|
4
|
+
* The exact command that logs a given agent in — for warn banners and nudges.
|
|
5
|
+
* Driven off the registry `cliCommand` with the per-agent subcommand overrides
|
|
6
|
+
* (verified against the real CLIs): codex/grok use `<cli> login`, opencode uses
|
|
7
|
+
* `<cli> auth login`, claude logs in from inside its TUI via `/login`, and the
|
|
8
|
+
* remaining agents (kimi, gemini, …) start their device/oauth flow on launch.
|
|
9
|
+
*/
|
|
10
|
+
export declare function loginHint(agentId: AgentId): string;
|
|
11
|
+
/**
|
|
12
|
+
* Whether `agents run` should probe login state before launching. True only for
|
|
13
|
+
* a launch that actually opens the interactive TUI — where discovering a logged-out
|
|
14
|
+
* account after the fact wastes time. Suppressed when there is no preamble surface
|
|
15
|
+
* (`--json`/`--quiet`), when the check is explicitly disabled
|
|
16
|
+
* (`--no-auth-check` / `AGENTS_NO_AUTH_CHECK=1`), or when a rotation already picked a
|
|
17
|
+
* signed-in account.
|
|
18
|
+
*
|
|
19
|
+
* `forceInteractive` is load-bearing: a resume of a non-native-resume agent
|
|
20
|
+
* (`agents run kimi --resume`, also grok/opencode/gemini) rewrites the prompt to
|
|
21
|
+
* `/continue <id>` — so `hasPrompt` is true even though the run still opens the TUI.
|
|
22
|
+
* Keying only off `hasPrompt` would silently skip the warning on exactly those
|
|
23
|
+
* agents (the ones the feature is for), so the resume's `forceInteractive` flag is
|
|
24
|
+
* consulted directly.
|
|
25
|
+
*/
|
|
26
|
+
export declare function shouldCheckLoginBeforeLaunch(o: {
|
|
27
|
+
interactive?: boolean;
|
|
28
|
+
forceInteractive?: boolean;
|
|
29
|
+
headless?: boolean;
|
|
30
|
+
hasPrompt: boolean;
|
|
31
|
+
json?: boolean;
|
|
32
|
+
quiet?: boolean;
|
|
33
|
+
authCheckDisabled?: boolean;
|
|
34
|
+
rotated?: boolean;
|
|
35
|
+
}): boolean;
|
|
36
|
+
/**
|
|
37
|
+
* Colored `✓ signed in <account>` / `✗ logged out` badge. When signed in and an
|
|
38
|
+
* account label is derivable (email, else an account id), it is appended in cyan;
|
|
39
|
+
* opaque-credential agents with no email still read as signed in.
|
|
40
|
+
*/
|
|
41
|
+
export declare function formatSignInBadge(info: Pick<AccountInfo, 'signedIn' | 'email' | 'accountId'> | null | undefined): string;
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared login-state LOOK. One place decides how "signed in / logged out"
|
|
3
|
+
* renders and what the login command is, so `agents doctor`, `agents view`, and
|
|
4
|
+
* the `agents run` preflight banner all read identically.
|
|
5
|
+
*
|
|
6
|
+
* The signal is `AccountInfo.signedIn` from `getAccountInfo` (file-based, cheap,
|
|
7
|
+
* no Keychain ACL prompt). It is advisory — opaque-credential agents (Kimi,
|
|
8
|
+
* Antigravity) and keychain-bound Claude can false-negative — so callers that act
|
|
9
|
+
* on it (the run preflight) WARN and continue; they never block.
|
|
10
|
+
*/
|
|
11
|
+
import chalk from 'chalk';
|
|
12
|
+
import { AGENTS } from './agents.js';
|
|
13
|
+
/**
|
|
14
|
+
* The exact command that logs a given agent in — for warn banners and nudges.
|
|
15
|
+
* Driven off the registry `cliCommand` with the per-agent subcommand overrides
|
|
16
|
+
* (verified against the real CLIs): codex/grok use `<cli> login`, opencode uses
|
|
17
|
+
* `<cli> auth login`, claude logs in from inside its TUI via `/login`, and the
|
|
18
|
+
* remaining agents (kimi, gemini, …) start their device/oauth flow on launch.
|
|
19
|
+
*/
|
|
20
|
+
export function loginHint(agentId) {
|
|
21
|
+
const cli = AGENTS[agentId]?.cliCommand ?? agentId;
|
|
22
|
+
switch (agentId) {
|
|
23
|
+
case 'claude':
|
|
24
|
+
return `${cli}, then /login`;
|
|
25
|
+
case 'codex':
|
|
26
|
+
case 'grok':
|
|
27
|
+
return `${cli} login`;
|
|
28
|
+
case 'opencode':
|
|
29
|
+
return `${cli} auth login`;
|
|
30
|
+
default:
|
|
31
|
+
return cli;
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Whether `agents run` should probe login state before launching. True only for
|
|
36
|
+
* a launch that actually opens the interactive TUI — where discovering a logged-out
|
|
37
|
+
* account after the fact wastes time. Suppressed when there is no preamble surface
|
|
38
|
+
* (`--json`/`--quiet`), when the check is explicitly disabled
|
|
39
|
+
* (`--no-auth-check` / `AGENTS_NO_AUTH_CHECK=1`), or when a rotation already picked a
|
|
40
|
+
* signed-in account.
|
|
41
|
+
*
|
|
42
|
+
* `forceInteractive` is load-bearing: a resume of a non-native-resume agent
|
|
43
|
+
* (`agents run kimi --resume`, also grok/opencode/gemini) rewrites the prompt to
|
|
44
|
+
* `/continue <id>` — so `hasPrompt` is true even though the run still opens the TUI.
|
|
45
|
+
* Keying only off `hasPrompt` would silently skip the warning on exactly those
|
|
46
|
+
* agents (the ones the feature is for), so the resume's `forceInteractive` flag is
|
|
47
|
+
* consulted directly.
|
|
48
|
+
*/
|
|
49
|
+
export function shouldCheckLoginBeforeLaunch(o) {
|
|
50
|
+
if (o.json || o.quiet || o.authCheckDisabled || o.rotated)
|
|
51
|
+
return false;
|
|
52
|
+
return o.interactive === true || o.forceInteractive === true || (!o.hasPrompt && o.headless !== true);
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Colored `✓ signed in <account>` / `✗ logged out` badge. When signed in and an
|
|
56
|
+
* account label is derivable (email, else an account id), it is appended in cyan;
|
|
57
|
+
* opaque-credential agents with no email still read as signed in.
|
|
58
|
+
*/
|
|
59
|
+
export function formatSignInBadge(info) {
|
|
60
|
+
if (!info?.signedIn)
|
|
61
|
+
return chalk.red('✗ logged out');
|
|
62
|
+
const who = info.email ?? (info.accountId ? `id:${info.accountId}` : '');
|
|
63
|
+
return who ? `${chalk.green('✓ signed in')} ${chalk.cyan(who)}` : chalk.green('✓ signed in');
|
|
64
|
+
}
|
package/dist/lib/ssh-exec.d.ts
CHANGED
|
@@ -70,6 +70,11 @@ export interface SshExecResult {
|
|
|
70
70
|
* it); callers that build it from user input must `shellQuote` the pieces.
|
|
71
71
|
*/
|
|
72
72
|
export declare function sshExec(target: string, remoteCmd: string, opts?: SshExecOptions): SshExecResult;
|
|
73
|
+
/**
|
|
74
|
+
* Async variant of {@link sshExec}. Same hardened argv composition, but uses
|
|
75
|
+
* child_process.spawn so fleet fan-outs can probe multiple hosts concurrently.
|
|
76
|
+
*/
|
|
77
|
+
export declare function sshExecAsync(target: string, remoteCmd: string, opts?: SshExecOptions): Promise<SshExecResult>;
|
|
73
78
|
export interface SshExecRawResult {
|
|
74
79
|
code: number | null;
|
|
75
80
|
stdout: Buffer;
|
package/dist/lib/ssh-exec.js
CHANGED
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* target-injection guard live in exactly one place. Target validation is the
|
|
8
8
|
* canonical definition; `commands/secrets.ts` re-exports it.
|
|
9
9
|
*/
|
|
10
|
-
import { spawnSync } from 'child_process';
|
|
10
|
+
import { spawn, spawnSync } from 'child_process';
|
|
11
11
|
import * as fs from 'fs';
|
|
12
12
|
import * as path from 'path';
|
|
13
13
|
import { getCacheDir } from './state.js';
|
|
@@ -125,6 +125,60 @@ export function sshExec(target, remoteCmd, opts = {}) {
|
|
|
125
125
|
timedOut,
|
|
126
126
|
};
|
|
127
127
|
}
|
|
128
|
+
/**
|
|
129
|
+
* Async variant of {@link sshExec}. Same hardened argv composition, but uses
|
|
130
|
+
* child_process.spawn so fleet fan-outs can probe multiple hosts concurrently.
|
|
131
|
+
*/
|
|
132
|
+
export function sshExecAsync(target, remoteCmd, opts = {}) {
|
|
133
|
+
assertValidSshTarget(target);
|
|
134
|
+
const mux = opts.multiplex === false ? [] : controlOpts();
|
|
135
|
+
const args = [...sshConnectOpts(mux, opts.hostKeyOpts), ...(opts.extraSshArgs ?? []), target, remoteCmd];
|
|
136
|
+
return new Promise((resolve) => {
|
|
137
|
+
const child = spawn('ssh', args, {
|
|
138
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
139
|
+
windowsHide: true,
|
|
140
|
+
});
|
|
141
|
+
let stdout = '';
|
|
142
|
+
let stderr = '';
|
|
143
|
+
let settled = false;
|
|
144
|
+
let timedOut = false;
|
|
145
|
+
const timer = opts.timeoutMs
|
|
146
|
+
? setTimeout(() => {
|
|
147
|
+
timedOut = true;
|
|
148
|
+
child.kill('SIGTERM');
|
|
149
|
+
}, opts.timeoutMs)
|
|
150
|
+
: null;
|
|
151
|
+
child.stdout.setEncoding('utf-8');
|
|
152
|
+
child.stderr.setEncoding('utf-8');
|
|
153
|
+
child.stdout.on('data', (chunk) => { stdout += chunk; });
|
|
154
|
+
child.stderr.on('data', (chunk) => { stderr += chunk; });
|
|
155
|
+
// Guard the stdin pipe: if the child closes stdin early (e.g. exits fast), the
|
|
156
|
+
// end()/write below emits EPIPE on the stream. With no listener Node escalates
|
|
157
|
+
// that to an uncaught exception that kills the whole CLI. Swallow it — the real
|
|
158
|
+
// outcome is still reported by the 'close'/'error' handlers below.
|
|
159
|
+
child.stdin.on('error', () => { });
|
|
160
|
+
if (opts.input !== undefined)
|
|
161
|
+
child.stdin.end(opts.input);
|
|
162
|
+
else
|
|
163
|
+
child.stdin.end();
|
|
164
|
+
child.on('error', (err) => {
|
|
165
|
+
if (settled)
|
|
166
|
+
return;
|
|
167
|
+
settled = true;
|
|
168
|
+
if (timer)
|
|
169
|
+
clearTimeout(timer);
|
|
170
|
+
resolve({ code: null, stdout, stderr: stderr + err.message, timedOut });
|
|
171
|
+
});
|
|
172
|
+
child.on('close', (code) => {
|
|
173
|
+
if (settled)
|
|
174
|
+
return;
|
|
175
|
+
settled = true;
|
|
176
|
+
if (timer)
|
|
177
|
+
clearTimeout(timer);
|
|
178
|
+
resolve({ code, stdout, stderr, timedOut });
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
}
|
|
128
182
|
/**
|
|
129
183
|
* Like {@link sshExec} but returns raw stdout/stderr Buffers — no UTF-8 decode.
|
|
130
184
|
*
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -160,6 +160,16 @@ export interface AgentConfig {
|
|
|
160
160
|
* `skip` errors with a clear message naming the supported modes.
|
|
161
161
|
*/
|
|
162
162
|
modes: Mode[];
|
|
163
|
+
/**
|
|
164
|
+
* Whether `plan` mode works in a HEADLESS run (`--prompt`/`-p`). Some CLIs
|
|
165
|
+
* list a `plan` mode that only works interactively — kimi refuses `--prompt`
|
|
166
|
+
* combined with `--plan`, and grok's `--permission-mode plan` silently stalls
|
|
167
|
+
* a headless run at its ExitPlanMode gate. Absent (undefined) means true:
|
|
168
|
+
* headless plan is assumed to work unless a agent opts out with `false`, in
|
|
169
|
+
* which case a headless `--mode plan` request auto-downgrades to `auto`
|
|
170
|
+
* (see resolveHeadlessMode). Interactive plan is unaffected.
|
|
171
|
+
*/
|
|
172
|
+
headlessPlan?: boolean;
|
|
163
173
|
/**
|
|
164
174
|
* Whether the agent natively resolves `@path/to/file` imports inside its
|
|
165
175
|
* rules file at session start. If false, agents-cli must pre-compile the
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -66,8 +66,18 @@ export declare function buildCanonicalUsageContext(inputs: UsageIdentityInput[])
|
|
|
66
66
|
canonicalByUsageKey: Map<string, AccountInfo>;
|
|
67
67
|
usageFetchInputs: Map<string, UsageFetchInput>;
|
|
68
68
|
};
|
|
69
|
+
/**
|
|
70
|
+
* Whether an agent exposes usage/limit data we can render — Claude/Kimi/Droid via
|
|
71
|
+
* a live API, Codex via local session logs. Everything else has no usage concept,
|
|
72
|
+
* so callers use this to decide whether a missing snapshot is worth flagging as
|
|
73
|
+
* "usage unavailable" (a signed-in Claude account with no data) versus simply not
|
|
74
|
+
* applicable (Antigravity, Grok, OpenCode).
|
|
75
|
+
*/
|
|
76
|
+
export declare function agentReportsUsage(agentId: AgentId): boolean;
|
|
69
77
|
/** Fetch usage info for all unique accounts in parallel, keyed by usage key. */
|
|
70
|
-
export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[]
|
|
78
|
+
export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[], opts?: {
|
|
79
|
+
forceRefresh?: boolean;
|
|
80
|
+
}): Promise<{
|
|
71
81
|
canonicalByUsageKey: Map<string, AccountInfo>;
|
|
72
82
|
usageByKey: Map<string, UsageInfo>;
|
|
73
83
|
}>;
|
|
@@ -83,9 +93,13 @@ export declare function getUsageInfoByIdentity(inputs: UsageIdentityInput[]): Pr
|
|
|
83
93
|
* cache; every run after that returns instantly while the cache silently
|
|
84
94
|
* refreshes in the background.
|
|
85
95
|
*/
|
|
86
|
-
export declare function getUsageInfoForIdentity(input: UsageIdentityInput
|
|
96
|
+
export declare function getUsageInfoForIdentity(input: UsageIdentityInput, opts?: {
|
|
97
|
+
forceRefresh?: boolean;
|
|
98
|
+
}): Promise<UsageInfo>;
|
|
87
99
|
/** Format a one-line usage summary with compact bars for inline display. */
|
|
88
|
-
export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number
|
|
100
|
+
export declare function formatUsageSummary(plan: string | null, snapshot: UsageSnapshot | null, planWidth?: number, opts?: {
|
|
101
|
+
unavailable?: boolean;
|
|
102
|
+
}): string;
|
|
89
103
|
/**
|
|
90
104
|
* Derive an account's real throttle state from its live usage windows — the
|
|
91
105
|
* single signal both the `agents view` badge and run-rotation eligibility share
|
package/dist/lib/usage.js
CHANGED
|
@@ -87,8 +87,18 @@ export function buildCanonicalUsageContext(inputs) {
|
|
|
87
87
|
}
|
|
88
88
|
return { canonicalByUsageKey, usageFetchInputs };
|
|
89
89
|
}
|
|
90
|
+
/**
|
|
91
|
+
* Whether an agent exposes usage/limit data we can render — Claude/Kimi/Droid via
|
|
92
|
+
* a live API, Codex via local session logs. Everything else has no usage concept,
|
|
93
|
+
* so callers use this to decide whether a missing snapshot is worth flagging as
|
|
94
|
+
* "usage unavailable" (a signed-in Claude account with no data) versus simply not
|
|
95
|
+
* applicable (Antigravity, Grok, OpenCode).
|
|
96
|
+
*/
|
|
97
|
+
export function agentReportsUsage(agentId) {
|
|
98
|
+
return agentId === 'claude' || agentId === 'codex' || agentId === 'kimi' || agentId === 'droid';
|
|
99
|
+
}
|
|
90
100
|
/** Fetch usage info for all unique accounts in parallel, keyed by usage key. */
|
|
91
|
-
export async function getUsageInfoByIdentity(inputs) {
|
|
101
|
+
export async function getUsageInfoByIdentity(inputs, opts) {
|
|
92
102
|
const { canonicalByUsageKey, usageFetchInputs } = buildCanonicalUsageContext(inputs);
|
|
93
103
|
const usageResults = await Promise.all([...usageFetchInputs.entries()].map(async ([key, input]) => ({
|
|
94
104
|
key,
|
|
@@ -97,7 +107,7 @@ export async function getUsageInfoByIdentity(inputs) {
|
|
|
97
107
|
home: input.home,
|
|
98
108
|
cliVersion: input.cliVersion,
|
|
99
109
|
info: canonicalByUsageKey.get(key),
|
|
100
|
-
}),
|
|
110
|
+
}, opts),
|
|
101
111
|
})));
|
|
102
112
|
return {
|
|
103
113
|
canonicalByUsageKey,
|
|
@@ -120,8 +130,9 @@ const inFlightRefreshes = new Map();
|
|
|
120
130
|
* cache; every run after that returns instantly while the cache silently
|
|
121
131
|
* refreshes in the background.
|
|
122
132
|
*/
|
|
123
|
-
export async function getUsageInfoForIdentity(input) {
|
|
133
|
+
export async function getUsageInfoForIdentity(input, opts) {
|
|
124
134
|
const usageKey = getUsageLookupKey(input.info);
|
|
135
|
+
const forceRefresh = opts?.forceRefresh === true;
|
|
125
136
|
// Agents whose usage comes from a live network call (Claude, Kimi, Droid) go
|
|
126
137
|
// through the stale-while-revalidate cache below so `agents run`/`agents view`
|
|
127
138
|
// stay off the network on the hot path. Everything else (Codex reads local
|
|
@@ -139,15 +150,20 @@ export async function getUsageInfoForIdentity(input) {
|
|
|
139
150
|
}
|
|
140
151
|
const cached = readClaudeUsageCache(usageKey);
|
|
141
152
|
const ageMs = cached?.capturedAt ? Date.now() - cached.capturedAt.getTime() : Infinity;
|
|
142
|
-
//
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
153
|
+
// `--refresh` (forceRefresh) skips both cache short-circuits and blocks on a
|
|
154
|
+
// live fetch below, so `agents view --refresh` repopulates every account we can
|
|
155
|
+
// actually reach a token for.
|
|
156
|
+
if (!forceRefresh) {
|
|
157
|
+
// Fresh: cache is recent enough, skip network entirely.
|
|
158
|
+
if (cached && ageMs < USAGE_CACHE_FRESH_MS) {
|
|
159
|
+
return { snapshot: cached, error: null };
|
|
160
|
+
}
|
|
161
|
+
// Stale-while-revalidate: cache exists and isn't ancient, return it now and
|
|
162
|
+
// refresh in the background so the next invocation has fresh data.
|
|
163
|
+
if (cached && ageMs < USAGE_CACHE_SWR_MS) {
|
|
164
|
+
triggerBackgroundUsageRefresh(input, usageKey);
|
|
165
|
+
return { snapshot: cached, error: null };
|
|
166
|
+
}
|
|
151
167
|
}
|
|
152
168
|
// Cold cache or > 24h old: block on live fetch.
|
|
153
169
|
const usage = await getUsageInfo(input.agentId, {
|
|
@@ -201,7 +217,7 @@ function triggerBackgroundUsageRefresh(input, usageKey) {
|
|
|
201
217
|
inFlightRefreshes.set(usageKey, promise);
|
|
202
218
|
}
|
|
203
219
|
/** Format a one-line usage summary with compact bars for inline display. */
|
|
204
|
-
export function formatUsageSummary(plan, snapshot, planWidth = 3) {
|
|
220
|
+
export function formatUsageSummary(plan, snapshot, planWidth = 3, opts) {
|
|
205
221
|
const parts = [];
|
|
206
222
|
if (plan) {
|
|
207
223
|
parts.push(chalk.gray(plan.padEnd(planWidth)));
|
|
@@ -212,14 +228,26 @@ export function formatUsageSummary(plan, snapshot, planWidth = 3) {
|
|
|
212
228
|
// account throttled by its month window (Droid meters on 5h/week/month)
|
|
213
229
|
// shows the bar that explains why. Claude's Sonnet week is a per-model
|
|
214
230
|
// sub-limit, not a blocking window; it renders only in the full
|
|
215
|
-
// per-version usage section.
|
|
231
|
+
// per-version usage section. Each window reads "S: ███░░ 58% (3d)" — the
|
|
232
|
+
// gauge, the exact percentage, and a compact hint of when it resets.
|
|
216
233
|
const windows = snapshot.windows
|
|
217
234
|
.filter((window) => window.key !== 'sonnet_week')
|
|
218
|
-
.map((window) =>
|
|
235
|
+
.map((window) => {
|
|
236
|
+
const bar = renderCompactUsageBar(window.usedPercent);
|
|
237
|
+
const pct = colorUsage(`${Math.round(window.usedPercent)}%`, window.usedPercent);
|
|
238
|
+
const reset = window.resetsAt ? chalk.dim(` (${formatResetHint(window.resetsAt)})`) : '';
|
|
239
|
+
return `${chalk.gray(`${window.shortLabel}:`)} ${bar} ${pct}${reset}`;
|
|
240
|
+
});
|
|
219
241
|
if (windows.length > 0) {
|
|
220
|
-
parts.push(windows.join('
|
|
242
|
+
parts.push(windows.join(' '));
|
|
221
243
|
}
|
|
222
244
|
}
|
|
245
|
+
else if (opts?.unavailable) {
|
|
246
|
+
// Signed-in account we could NOT fetch usage for (no live token in a reachable
|
|
247
|
+
// home / org mismatch / fetch error). Say so explicitly instead of drawing a
|
|
248
|
+
// blank gauge that reads like "0% used".
|
|
249
|
+
parts.push(chalk.dim('usage unavailable'));
|
|
250
|
+
}
|
|
223
251
|
return parts.join(' ');
|
|
224
252
|
}
|
|
225
253
|
/**
|
|
@@ -1096,6 +1124,25 @@ function formatPercent(value) {
|
|
|
1096
1124
|
const rounded = Math.round(value * 10) / 10;
|
|
1097
1125
|
return Number.isInteger(rounded) ? String(rounded) : rounded.toFixed(1);
|
|
1098
1126
|
}
|
|
1127
|
+
/**
|
|
1128
|
+
* Compact "time until reset" hint for the inline usage bars: "5m", "2h", "3d",
|
|
1129
|
+
* or "now" once elapsed. Deliberately coarse (single unit, whole numbers) so it
|
|
1130
|
+
* fits after a bar without wrapping the row — the detailed section
|
|
1131
|
+
* (`formatResetAt`) carries the precise clock time.
|
|
1132
|
+
*/
|
|
1133
|
+
function formatResetHint(date) {
|
|
1134
|
+
const diffMs = date.getTime() - Date.now();
|
|
1135
|
+
if (diffMs <= 0)
|
|
1136
|
+
return 'now';
|
|
1137
|
+
const mins = Math.round(diffMs / 60000);
|
|
1138
|
+
if (mins < 60)
|
|
1139
|
+
return `${Math.max(1, mins)}m`;
|
|
1140
|
+
const hours = Math.round(mins / 60);
|
|
1141
|
+
if (hours < 24)
|
|
1142
|
+
return `${hours}h`;
|
|
1143
|
+
const days = Math.round(hours / 24);
|
|
1144
|
+
return `${days}d`;
|
|
1145
|
+
}
|
|
1099
1146
|
/** Format a reset timestamp as a human-readable relative or absolute time. */
|
|
1100
1147
|
function formatResetAt(date) {
|
|
1101
1148
|
const timezone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@phnx-labs/agents-cli",
|
|
3
|
-
"version": "1.20.
|
|
3
|
+
"version": "1.20.68",
|
|
4
4
|
"description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|