@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.
Files changed (50) hide show
  1. package/CHANGELOG.md +88 -0
  2. package/README.md +3 -0
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/check.js +96 -2
  5. package/dist/commands/doctor.js +37 -9
  6. package/dist/commands/exec.js +45 -1
  7. package/dist/commands/plugins.js +11 -2
  8. package/dist/commands/repo.js +47 -2
  9. package/dist/commands/ssh.js +103 -2
  10. package/dist/commands/teams.d.ts +11 -0
  11. package/dist/commands/teams.js +40 -2
  12. package/dist/commands/view.d.ts +1 -0
  13. package/dist/commands/view.js +12 -8
  14. package/dist/lib/agents.d.ts +11 -5
  15. package/dist/lib/agents.js +35 -24
  16. package/dist/lib/browser/profiles.js +4 -25
  17. package/dist/lib/cli-entry.d.ts +23 -0
  18. package/dist/lib/cli-entry.js +116 -0
  19. package/dist/lib/daemon.d.ts +2 -2
  20. package/dist/lib/daemon.js +8 -89
  21. package/dist/lib/devices/fleet.d.ts +23 -0
  22. package/dist/lib/devices/fleet.js +38 -0
  23. package/dist/lib/devices/health-report.d.ts +46 -0
  24. package/dist/lib/devices/health-report.js +159 -0
  25. package/dist/lib/devices/health.d.ts +14 -4
  26. package/dist/lib/devices/health.js +49 -8
  27. package/dist/lib/exec.d.ts +15 -0
  28. package/dist/lib/exec.js +31 -6
  29. package/dist/lib/git.d.ts +10 -0
  30. package/dist/lib/git.js +23 -0
  31. package/dist/lib/hosts/option.js +1 -1
  32. package/dist/lib/hosts/ready.js +5 -3
  33. package/dist/lib/hosts/remote-cmd.d.ts +12 -0
  34. package/dist/lib/hosts/remote-cmd.js +17 -1
  35. package/dist/lib/mcp.d.ts +21 -0
  36. package/dist/lib/mcp.js +278 -13
  37. package/dist/lib/resources/mcp.js +20 -342
  38. package/dist/lib/routines.d.ts +13 -0
  39. package/dist/lib/routines.js +21 -0
  40. package/dist/lib/runner.js +24 -47
  41. package/dist/lib/secrets/agent.js +11 -15
  42. package/dist/lib/share/capture.js +21 -3
  43. package/dist/lib/signin-badge.d.ts +41 -0
  44. package/dist/lib/signin-badge.js +64 -0
  45. package/dist/lib/ssh-exec.d.ts +5 -0
  46. package/dist/lib/ssh-exec.js +55 -1
  47. package/dist/lib/types.d.ts +10 -0
  48. package/dist/lib/usage.d.ts +17 -3
  49. package/dist/lib/usage.js +63 -16
  50. 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
- for (const bin of candidateBrowsers()) {
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
- // try the next candidate
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
+ }
@@ -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;
@@ -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
  *
@@ -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
@@ -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[]): Promise<{
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): Promise<UsageInfo>;
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): string;
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
- // Fresh: cache is recent enough, skip network entirely.
143
- if (cached && ageMs < USAGE_CACHE_FRESH_MS) {
144
- return { snapshot: cached, error: null };
145
- }
146
- // Stale-while-revalidate: cache exists and isn't ancient, return it now and
147
- // refresh in the background so the next invocation has fresh data.
148
- if (cached && ageMs < USAGE_CACHE_SWR_MS) {
149
- triggerBackgroundUsageRefresh(input, usageKey);
150
- return { snapshot: cached, error: null };
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) => `${chalk.gray(`${window.shortLabel}:`)} ${renderCompactUsageBar(window.usedPercent)}`);
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.66",
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",