@phnx-labs/agents-cli 1.20.32 → 1.20.34

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 (110) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +28 -2
  3. package/dist/commands/commands.js +3 -3
  4. package/dist/commands/computer-actions.js +1 -0
  5. package/dist/commands/computer.d.ts +23 -0
  6. package/dist/commands/computer.js +45 -3
  7. package/dist/commands/cost.js +2 -2
  8. package/dist/commands/doctor.d.ts +10 -0
  9. package/dist/commands/doctor.js +51 -2
  10. package/dist/commands/exec.js +56 -1
  11. package/dist/commands/hooks.js +3 -3
  12. package/dist/commands/import.js +1 -1
  13. package/dist/commands/inspect.js +13 -17
  14. package/dist/commands/mcp.js +3 -3
  15. package/dist/commands/permissions.js +3 -3
  16. package/dist/commands/rules.js +3 -3
  17. package/dist/commands/secrets-migrate.js +23 -11
  18. package/dist/commands/secrets.d.ts +20 -0
  19. package/dist/commands/secrets.js +53 -1
  20. package/dist/commands/sessions.js +18 -1
  21. package/dist/commands/skills.js +3 -3
  22. package/dist/commands/status.d.ts +12 -0
  23. package/dist/commands/status.js +81 -0
  24. package/dist/commands/sync.js +2 -2
  25. package/dist/commands/teams.js +77 -18
  26. package/dist/commands/usage.js +2 -2
  27. package/dist/commands/utils.d.ts +8 -0
  28. package/dist/commands/utils.js +20 -0
  29. package/dist/commands/versions.js +4 -3
  30. package/dist/commands/view.d.ts +39 -0
  31. package/dist/commands/view.js +217 -84
  32. package/dist/commands/workflows.js +3 -3
  33. package/dist/index.js +16 -2
  34. package/dist/lib/acp/harnesses.d.ts +1 -1
  35. package/dist/lib/acp/harnesses.js +2 -2
  36. package/dist/lib/agent-spec/index.d.ts +18 -0
  37. package/dist/lib/agent-spec/index.js +35 -0
  38. package/dist/lib/agent-spec/primitives.d.ts +28 -0
  39. package/dist/lib/agent-spec/primitives.js +57 -0
  40. package/dist/lib/agent-spec/provider.d.ts +2 -0
  41. package/dist/lib/agent-spec/provider.js +9 -0
  42. package/dist/lib/agent-spec/resolve.d.ts +33 -0
  43. package/dist/lib/agent-spec/resolve.js +174 -0
  44. package/dist/lib/agent-spec/types.d.ts +57 -0
  45. package/dist/lib/agent-spec/types.js +18 -0
  46. package/dist/lib/agents.d.ts +12 -0
  47. package/dist/lib/agents.js +115 -32
  48. package/dist/lib/browser/chrome.js +20 -0
  49. package/dist/lib/browser/drivers/ssh.d.ts +19 -0
  50. package/dist/lib/browser/drivers/ssh.js +18 -3
  51. package/dist/lib/crabbox/cli.d.ts +98 -0
  52. package/dist/lib/crabbox/cli.js +218 -0
  53. package/dist/lib/crabbox/lease.d.ts +41 -0
  54. package/dist/lib/crabbox/lease.js +73 -0
  55. package/dist/lib/crabbox/runtimes.d.ts +57 -0
  56. package/dist/lib/crabbox/runtimes.js +109 -0
  57. package/dist/lib/doctor-diff.js +29 -2
  58. package/dist/lib/drift-sync.d.ts +43 -0
  59. package/dist/lib/drift-sync.js +179 -0
  60. package/dist/lib/exec.d.ts +15 -0
  61. package/dist/lib/exec.js +21 -11
  62. package/dist/lib/hosts/dispatch.d.ts +27 -10
  63. package/dist/lib/hosts/dispatch.js +55 -19
  64. package/dist/lib/hosts/option.d.ts +14 -0
  65. package/dist/lib/hosts/option.js +19 -0
  66. package/dist/lib/hosts/passthrough.d.ts +30 -0
  67. package/dist/lib/hosts/passthrough.js +141 -0
  68. package/dist/lib/hosts/remote-cmd.d.ts +36 -0
  69. package/dist/lib/hosts/remote-cmd.js +56 -0
  70. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  71. package/dist/lib/platform/winpath.d.ts +31 -2
  72. package/dist/lib/platform/winpath.js +133 -24
  73. package/dist/lib/pwsh.d.ts +11 -0
  74. package/dist/lib/pwsh.js +13 -0
  75. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  76. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  77. package/dist/lib/secrets/agent.d.ts +42 -1
  78. package/dist/lib/secrets/agent.js +89 -11
  79. package/dist/lib/secrets/bundles.js +61 -21
  80. package/dist/lib/secrets/filestore.js +31 -1
  81. package/dist/lib/secrets/index.d.ts +44 -1
  82. package/dist/lib/secrets/index.js +108 -10
  83. package/dist/lib/secrets/linux.d.ts +14 -0
  84. package/dist/lib/secrets/linux.js +21 -0
  85. package/dist/lib/secrets/windows.d.ts +74 -0
  86. package/dist/lib/secrets/windows.js +440 -0
  87. package/dist/lib/session/active.d.ts +8 -0
  88. package/dist/lib/session/active.js +18 -1
  89. package/dist/lib/session/provenance.d.ts +56 -0
  90. package/dist/lib/session/provenance.js +157 -0
  91. package/dist/lib/shims.d.ts +20 -0
  92. package/dist/lib/shims.js +53 -20
  93. package/dist/lib/ssh-exec.d.ts +22 -0
  94. package/dist/lib/ssh-exec.js +59 -2
  95. package/dist/lib/ssh-tunnel.d.ts +0 -5
  96. package/dist/lib/ssh-tunnel.js +65 -8
  97. package/dist/lib/startup/command-registry.d.ts +1 -0
  98. package/dist/lib/startup/command-registry.js +2 -0
  99. package/dist/lib/sync-status.d.ts +102 -0
  100. package/dist/lib/sync-status.js +135 -0
  101. package/dist/lib/teams/agents.d.ts +24 -0
  102. package/dist/lib/teams/agents.js +30 -1
  103. package/dist/lib/types.d.ts +20 -1
  104. package/dist/lib/usage.d.ts +30 -0
  105. package/dist/lib/usage.js +159 -2
  106. package/dist/lib/versions.d.ts +2 -4
  107. package/dist/lib/versions.js +7 -20
  108. package/package.json +2 -1
  109. package/dist/lib/agent-spec.d.ts +0 -36
  110. package/dist/lib/agent-spec.js +0 -157
@@ -0,0 +1,135 @@
1
+ /**
2
+ * Unified sync-status engine — the SINGLE source of truth for "is this resource
3
+ * synced to this agent version?" consumed by `agents doctor`, `agents view`, the
4
+ * menu-bar app, and any Agency surface.
5
+ *
6
+ * Why this exists: before this module there were four different notions of
7
+ * "synced" living in four files:
8
+ * - doctor: isStale() vs .sync-manifest.json — catches SOURCE drift only.
9
+ * - view: git working-tree state of ~/.agents/ — a resource can show green
10
+ * while its installed copy is stale/deleted/corrupted (false positive).
11
+ * - lists: file-exists-in-home — never reports content drift at all.
12
+ * - menubar: read doctor --json (so it inherited doctor's source-only blind spot).
13
+ *
14
+ * The reliable signal is diffVersionResources() (src/lib/doctor-diff.ts): it reads
15
+ * the ACTUAL version home and compares it to the resolved sources, so it catches
16
+ * every drift class — source-side changes AND home-side rot (deleted / corrupted /
17
+ * hand-edited installed copies) AND orphans. This module wraps it once, maps its
18
+ * per-resource DiffStatus onto one stable enum, folds in `.system` repo freshness,
19
+ * and lets every surface render the same warnings instead of re-deriving them.
20
+ */
21
+ import simpleGit from 'simple-git';
22
+ import { ALL_AGENT_IDS } from './agents.js';
23
+ import { diffVersionResources, } from './doctor-diff.js';
24
+ import { listInstalledVersions, getGlobalDefault } from './versions.js';
25
+ import { loadManifest } from './staleness/index.js';
26
+ import { getSystemAgentsDir } from './state.js';
27
+ import { isGitRepo } from './git.js';
28
+ const STATUS_MAP = {
29
+ ok: 'synced',
30
+ diff: 'drifted',
31
+ missing: 'missing',
32
+ extra: 'orphan',
33
+ };
34
+ function rowsFromReport(agent, version, report) {
35
+ const out = [];
36
+ for (const list of Object.values(report.kinds)) {
37
+ for (const r of list) {
38
+ out.push({
39
+ agent,
40
+ version,
41
+ kind: r.kind,
42
+ name: r.name,
43
+ status: STATUS_MAP[r.status],
44
+ ...(r.detail ? { detail: r.detail } : {}),
45
+ });
46
+ }
47
+ }
48
+ return out;
49
+ }
50
+ /**
51
+ * Read `.system` repo freshness WITHOUT touching the network. `git status`
52
+ * reports ahead/behind against the remote-tracking ref, which the detached
53
+ * auto-pull worker keeps warm via periodic `git fetch`. This is the same number
54
+ * the menu-bar surfaces; we read it once, here, so every surface agrees.
55
+ */
56
+ export async function getSystemRepoStatus() {
57
+ const dir = getSystemAgentsDir();
58
+ const base = { dir, behind: 0, ahead: 0, branch: null, unknown: true };
59
+ if (!isGitRepo(dir))
60
+ return base;
61
+ try {
62
+ const status = await simpleGit(dir).status();
63
+ return {
64
+ dir,
65
+ behind: status.behind ?? 0,
66
+ ahead: status.ahead ?? 0,
67
+ branch: status.tracking ?? status.current ?? null,
68
+ // Without a tracking branch there's no upstream to compare against.
69
+ unknown: !status.tracking,
70
+ };
71
+ }
72
+ catch {
73
+ return base;
74
+ }
75
+ }
76
+ /**
77
+ * Compute unified sync status across the fleet. Resolves against non-project
78
+ * layers only (`excludeProject: true`) — the GLOBAL version home is never
79
+ * reconciled against per-cwd `<cwd>/.agents/` resources, so counting them as
80
+ * "missing" there would be a false gap (matches doctor's overview semantics).
81
+ */
82
+ export async function computeSyncStatus(options = {}) {
83
+ const cwd = options.cwd ?? process.cwd();
84
+ const agentIds = options.agents ?? ALL_AGENT_IDS;
85
+ const agents = [];
86
+ for (const agent of agentIds) {
87
+ const def = getGlobalDefault(agent);
88
+ for (const version of listInstalledVersions(agent)) {
89
+ const report = diffVersionResources(agent, version, {
90
+ cwd,
91
+ excludeProject: true,
92
+ ...(options.kinds ? { kinds: options.kinds } : {}),
93
+ });
94
+ const resources = rowsFromReport(agent, version, report);
95
+ const counts = { synced: 0, drifted: 0, missing: 0, orphan: 0 };
96
+ for (const r of resources)
97
+ counts[r.status]++;
98
+ agents.push({
99
+ agent,
100
+ version,
101
+ isDefault: version === def,
102
+ everSynced: loadManifest(agent, version) !== null,
103
+ counts,
104
+ needsSync: counts.drifted + counts.missing > 0,
105
+ resources,
106
+ });
107
+ }
108
+ }
109
+ const system = await getSystemRepoStatus();
110
+ const agentsNeedingSync = new Set();
111
+ let drifted = 0, missing = 0, orphan = 0, versionsNeedingSync = 0, versionsNeverSynced = 0;
112
+ for (const v of agents) {
113
+ drifted += v.counts.drifted;
114
+ missing += v.counts.missing;
115
+ orphan += v.counts.orphan;
116
+ if (!v.everSynced)
117
+ versionsNeverSynced++;
118
+ if (v.needsSync) {
119
+ versionsNeedingSync++;
120
+ agentsNeedingSync.add(v.agent);
121
+ }
122
+ }
123
+ return {
124
+ system,
125
+ agents,
126
+ totals: {
127
+ drifted,
128
+ missing,
129
+ orphan,
130
+ versionsNeedingSync,
131
+ versionsNeverSynced,
132
+ agentsNeedingSync: agentsNeedingSync.size,
133
+ },
134
+ };
135
+ }
@@ -68,6 +68,30 @@ export declare function checkAllClis(): Record<string, {
68
68
  path: string | null;
69
69
  error: string | null;
70
70
  }>;
71
+ /**
72
+ * Advisory sign-in probe for a teammate's agent. Reads the account-global login
73
+ * (no `home` → active config) via `getAccountInfo`. Deliberately best-effort:
74
+ * sign-in detection is UNRELIABLE for opaque-credential agents (Kimi/Antigravity
75
+ * store an OAuth/JWT with no email claim) and for keychain-probed agents, so a
76
+ * `false` here is often a false negative. Callers must WARN and continue — never
77
+ * block a team on this result. Never throws (returns false on any error).
78
+ */
79
+ export declare function checkCliSignedIn(agentType: AgentType): Promise<boolean>;
80
+ /** Advisory sign-in status for a `teams doctor` row. */
81
+ export interface SignInAdvisory {
82
+ /** true / false from the probe, or null when the agent isn't installed. */
83
+ signedIn: boolean | null;
84
+ /** Whether the agent is currently a running teammate. */
85
+ running: boolean;
86
+ }
87
+ /**
88
+ * Resolve the advisory sign-in status shown by `teams doctor`. An agent that is
89
+ * currently RUNNING in a team is live proof it works, so it overrides a
90
+ * (frequently false-negative) sign-in probe — doctor must never report a
91
+ * working agent as logged out. Not installed → `signedIn: null` (nothing to
92
+ * probe). Never flips the authoritative installed/ready column.
93
+ */
94
+ export declare function resolveSignInAdvisory(installed: boolean, running: boolean, probeSignedIn: boolean): SignInAdvisory;
71
95
  /** Resolve and cache the base directory where teammate process data is stored. */
72
96
  export declare function getAgentsDir(): Promise<string>;
73
97
  /**
@@ -19,7 +19,7 @@ import { normalizeEvents } from './parsers.js';
19
19
  import { debug } from './debug.js';
20
20
  import { setGeminiAutoUpdateDisabled, updateGeminiSettings } from '../gemini-settings.js';
21
21
  import { getAgentsDir as getSystemAgentsDir, getShimsDir } from '../state.js';
22
- import { AGENTS } from '../agents.js';
22
+ import { AGENTS, getAccountInfo } from '../agents.js';
23
23
  import { sanitizeProcessEnv } from '../secrets/bundles.js';
24
24
  let lastMemoryWarnAt = 0;
25
25
  // On macOS, os.freemem() returns only the truly-free pool and ignores the
@@ -353,6 +353,35 @@ export function checkAllClis() {
353
353
  }
354
354
  return results;
355
355
  }
356
+ /**
357
+ * Advisory sign-in probe for a teammate's agent. Reads the account-global login
358
+ * (no `home` → active config) via `getAccountInfo`. Deliberately best-effort:
359
+ * sign-in detection is UNRELIABLE for opaque-credential agents (Kimi/Antigravity
360
+ * store an OAuth/JWT with no email claim) and for keychain-probed agents, so a
361
+ * `false` here is often a false negative. Callers must WARN and continue — never
362
+ * block a team on this result. Never throws (returns false on any error).
363
+ */
364
+ export async function checkCliSignedIn(agentType) {
365
+ try {
366
+ const info = await getAccountInfo(agentType);
367
+ return info.signedIn;
368
+ }
369
+ catch {
370
+ return false;
371
+ }
372
+ }
373
+ /**
374
+ * Resolve the advisory sign-in status shown by `teams doctor`. An agent that is
375
+ * currently RUNNING in a team is live proof it works, so it overrides a
376
+ * (frequently false-negative) sign-in probe — doctor must never report a
377
+ * working agent as logged out. Not installed → `signedIn: null` (nothing to
378
+ * probe). Never flips the authoritative installed/ready column.
379
+ */
380
+ export function resolveSignInAdvisory(installed, running, probeSignedIn) {
381
+ if (!installed)
382
+ return { signedIn: null, running: false };
383
+ return { signedIn: running ? true : probeSignedIn, running };
384
+ }
356
385
  let AGENTS_DIR = null;
357
386
  /** Resolve and cache the base directory where teammate process data is stored. */
358
387
  export async function getAgentsDir() {
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { CloudProviderId } from './cloud/types.js';
9
9
  /** Unique identifier for a supported AI coding agent. */
10
- export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'roo' | 'antigravity' | 'grok' | 'kimi' | 'droid';
10
+ export type AgentId = 'claude' | 'codex' | 'gemini' | 'cursor' | 'opencode' | 'openclaw' | 'copilot' | 'amp' | 'kiro' | 'goose' | 'antigravity' | 'grok' | 'kimi' | 'droid';
11
11
  /** How `agents run <agent>` chooses an installed version when none is pinned. */
12
12
  export type RunStrategy = 'pinned' | 'available' | 'balanced';
13
13
  /** Per-agent run strategy config. */
@@ -99,6 +99,25 @@ export interface AgentConfig {
99
99
  * cloud and falls back to the configured default.
100
100
  */
101
101
  cloudProvider?: CloudProviderId;
102
+ /**
103
+ * Set when the upstream vendor has retired this agent's CLI. Presence marks
104
+ * the agent deprecated (it is never blocked from use); `warnAgentDeprecated`
105
+ * surfaces this in yellow whenever a user installs the agent or adds it to a
106
+ * team. Point `replacement` at the successor agent so the warning can suggest
107
+ * a migration path.
108
+ */
109
+ deprecated?: {
110
+ /** Vendor that retired it, e.g. "Google". */
111
+ by: string;
112
+ /** Human date it stopped working / was retired, e.g. "June 18, 2026". */
113
+ date: string;
114
+ /** One-line explanation shown under the warning header. */
115
+ reason: string;
116
+ /** Successor agent id to suggest instead (e.g. 'antigravity'). */
117
+ replacement?: AgentId;
118
+ /** Announcement URL for the deprecation. */
119
+ url?: string;
120
+ };
102
121
  capabilities: {
103
122
  hooks: Capability;
104
123
  mcp: Capability;
@@ -17,6 +17,7 @@ export interface UsageSnapshot {
17
17
  sourceLabel: string;
18
18
  capturedAt: Date | null;
19
19
  windows: UsageWindow[];
20
+ plan?: string | null;
20
21
  }
21
22
  /** Usage data plus any error encountered while fetching. */
22
23
  export interface UsageInfo {
@@ -122,6 +123,35 @@ export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot |
122
123
  export declare function formatUsageStatusBadge(usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null | undefined): string;
123
124
  /** Format a multi-line usage section for detailed agent views. */
124
125
  export declare function formatUsageSection(usage: UsageInfo): string[];
126
+ /** Raw quota bucket from the Kimi /usages response (numbers arrive as strings). */
127
+ interface KimiUsageQuota {
128
+ limit?: string | number | null;
129
+ used?: string | number | null;
130
+ remaining?: string | number | null;
131
+ resetTime?: string | null;
132
+ }
133
+ /** Response shape from the Kimi Code /usages endpoint (subset we render). */
134
+ export interface KimiUsagesResponse {
135
+ user?: {
136
+ userId?: string | null;
137
+ membership?: {
138
+ level?: string | null;
139
+ } | null;
140
+ } | null;
141
+ usage?: KimiUsageQuota | null;
142
+ limits?: Array<{
143
+ window?: {
144
+ duration?: number | null;
145
+ timeUnit?: string | null;
146
+ } | null;
147
+ detail?: KimiUsageQuota | null;
148
+ } | null> | null;
149
+ subType?: string | null;
150
+ }
151
+ /** Normalize the Kimi /usages payload into the common UsageWindow shape. */
152
+ export declare function normalizeKimiWindows(data: KimiUsagesResponse): UsageWindow[];
153
+ /** Derive a display plan label from Kimi's membership tier or subscription type. */
154
+ export declare function formatKimiPlan(data: KimiUsagesResponse): string | null;
125
155
  /** Load Claude OAuth credentials from the system keychain/keyring. */
126
156
  export declare function loadClaudeOauth(home?: string): Promise<ClaudeOauthCredentials | null>;
127
157
  /**
package/dist/lib/usage.js CHANGED
@@ -33,6 +33,7 @@ const CLAUDE_SCOPES = [
33
33
  const CLAUDE_KEYCHAIN_SERVICE = 'Claude Code-credentials';
34
34
  const getClaudeUsageCachePath = () => path.join(getCacheDir(), 'claude-usage.json');
35
35
  const CACHED_CLAUDE_USAGE_SOURCE_LABEL = 'last seen live account data';
36
+ const KIMI_USAGES_URL = 'https://api.kimi.com/coding/v1/usages';
36
37
  const COMPACT_BAR_LEN = 5;
37
38
  const USAGE_BAR_LEN = 10;
38
39
  const FULL = '\u2588';
@@ -44,6 +45,8 @@ export async function getUsageInfo(agentId, options) {
44
45
  return getClaudeUsageInfo(options);
45
46
  case 'codex':
46
47
  return getCodexUsageInfo(options);
48
+ case 'kimi':
49
+ return getKimiUsageInfo(options);
47
50
  default:
48
51
  return { snapshot: null, error: null };
49
52
  }
@@ -114,8 +117,14 @@ const inFlightRefreshes = new Map();
114
117
  */
115
118
  export async function getUsageInfoForIdentity(input) {
116
119
  const usageKey = getUsageLookupKey(input.info);
117
- // Non-Claude or no identity: legacy path, blocking fetch.
118
- if (input.agentId !== 'claude' || !usageKey) {
120
+ // Agents whose usage comes from a live network call (Claude, Kimi) go through
121
+ // the stale-while-revalidate cache below so `agents run`/`agents view` stay off
122
+ // the network on the hot path. Everything else (Codex reads local session
123
+ // logs) takes the legacy blocking path. The on-disk cache is shared and keyed
124
+ // by usageKey, which is namespaced per agent (`claude:org=…`, `kimi:user=…`),
125
+ // so one cache file holds every account without collision.
126
+ const usesNetworkUsage = input.agentId === 'claude' || input.agentId === 'kimi';
127
+ if (!usesNetworkUsage || !usageKey) {
119
128
  return getUsageInfo(input.agentId, {
120
129
  home: input.home,
121
130
  cliVersion: input.cliVersion,
@@ -356,6 +365,152 @@ async function getClaudeUsageInfo(options) {
356
365
  return { snapshot: null, error: 'Usage data unavailable right now.' };
357
366
  }
358
367
  }
368
+ /**
369
+ * Resolve Kimi's OAuth credential file. Sign-in is account-global but each
370
+ * installed version has an isolated home; the file physically lives only in the
371
+ * home the user logged in under. Check the per-version home first, then the
372
+ * active location under the real HOME — mirrors resolveAccountCredentialPath in
373
+ * agents.ts so every version reflects the true account state.
374
+ */
375
+ function resolveKimiCredentialPath(home) {
376
+ const rel = ['.kimi-code', 'credentials', 'kimi-code.json'];
377
+ const perVersion = path.join(home || os.homedir(), ...rel);
378
+ try {
379
+ if (fs.existsSync(perVersion))
380
+ return perVersion;
381
+ }
382
+ catch { /* unreadable */ }
383
+ const active = path.join(process.env.AGENTS_REAL_HOME || os.homedir(), ...rel);
384
+ if (active !== perVersion) {
385
+ try {
386
+ if (fs.existsSync(active))
387
+ return active;
388
+ }
389
+ catch { /* unreadable */ }
390
+ }
391
+ return null;
392
+ }
393
+ /**
394
+ * Fetch Kimi usage via the Kimi Code /usages API. Kimi's JWT has no email
395
+ * claim, so the account row can't show an address — but /usages returns quota
396
+ * windows and the membership tier, which is what we render.
397
+ *
398
+ * Deliberately NO token refresh: `agents view` is a read/inspect command and
399
+ * must not rotate the user's Kimi OAuth credential (rewriting the file,
400
+ * invalidating the old refresh token, racing a concurrently-running kimi CLI).
401
+ * The kimi CLI refreshes on its own launch; if the stored token is expired we
402
+ * skip the live fetch and let the SWR cache serve the last-seen snapshot.
403
+ */
404
+ async function getKimiUsageInfo(options) {
405
+ try {
406
+ const credPath = resolveKimiCredentialPath(options?.home);
407
+ if (!credPath)
408
+ return { snapshot: null, error: null };
409
+ const cred = JSON.parse(fs.readFileSync(credPath, 'utf-8'));
410
+ const accessToken = cred?.access_token;
411
+ if (typeof accessToken !== 'string' || !accessToken) {
412
+ return { snapshot: null, error: null };
413
+ }
414
+ const expiresAt = typeof cred?.expires_at === 'number' ? cred.expires_at : null;
415
+ if (expiresAt !== null && Date.now() / 1000 >= expiresAt) {
416
+ return { snapshot: null, error: null };
417
+ }
418
+ const response = await fetch(KIMI_USAGES_URL, {
419
+ method: 'GET',
420
+ headers: {
421
+ Authorization: `Bearer ${accessToken}`,
422
+ Accept: 'application/json',
423
+ },
424
+ signal: AbortSignal.timeout(5000),
425
+ });
426
+ // 401/403/404 => expired token or no Kimi For Coding subscription; render
427
+ // nothing rather than a misleading empty bar.
428
+ if (!response.ok) {
429
+ return { snapshot: null, error: null };
430
+ }
431
+ const data = await response.json();
432
+ const windows = normalizeKimiWindows(data);
433
+ if (windows.length === 0) {
434
+ return { snapshot: null, error: null };
435
+ }
436
+ return {
437
+ snapshot: {
438
+ source: 'live',
439
+ sourceLabel: 'live account data',
440
+ capturedAt: new Date(),
441
+ windows,
442
+ plan: formatKimiPlan(data),
443
+ },
444
+ error: null,
445
+ };
446
+ }
447
+ catch {
448
+ return { snapshot: null, error: null };
449
+ }
450
+ }
451
+ /** Normalize the Kimi /usages payload into the common UsageWindow shape. */
452
+ export function normalizeKimiWindows(data) {
453
+ const windows = [];
454
+ // Per-window rate limit (e.g. a 300-minute bucket) -> "session".
455
+ const shortLimit = Array.isArray(data.limits)
456
+ ? data.limits.find((entry) => entry?.detail)
457
+ : null;
458
+ const session = normalizeKimiWindow(shortLimit?.detail, 'session', 'Current session', 'S', kimiWindowMinutes(shortLimit?.window));
459
+ if (session)
460
+ windows.push(session);
461
+ // Rolling account quota -> "week".
462
+ const period = normalizeKimiWindow(data.usage, 'week', 'Current period', 'W', null);
463
+ if (period)
464
+ windows.push(period);
465
+ return windows;
466
+ }
467
+ /** Normalize a single Kimi quota bucket (used/limit strings) into a UsageWindow. */
468
+ function normalizeKimiWindow(quota, key, label, shortLabel, windowMinutes) {
469
+ const limit = kimiNumber(quota?.limit);
470
+ const used = kimiNumber(quota?.used);
471
+ if (limit === null || used === null || limit <= 0)
472
+ return null;
473
+ const usedPercent = normalizePercent((used / limit) * 100);
474
+ if (usedPercent === null)
475
+ return null;
476
+ return {
477
+ key,
478
+ label,
479
+ shortLabel,
480
+ usedPercent,
481
+ resetsAt: parseDateValue(quota?.resetTime),
482
+ windowMinutes: windowMinutes ?? inferWindowMinutes(key),
483
+ };
484
+ }
485
+ /** Parse a numeric field that Kimi serializes as a string (e.g. "100"). */
486
+ function kimiNumber(value) {
487
+ if (typeof value === 'number' && Number.isFinite(value))
488
+ return value;
489
+ if (typeof value === 'string' && value.trim() !== '' && !Number.isNaN(Number(value))) {
490
+ return Number(value);
491
+ }
492
+ return null;
493
+ }
494
+ /** Convert a Kimi limit window (duration + timeUnit enum) to minutes. */
495
+ function kimiWindowMinutes(window) {
496
+ const duration = typeof window?.duration === 'number' ? window.duration : null;
497
+ if (duration === null || duration <= 0)
498
+ return null;
499
+ switch (window?.timeUnit) {
500
+ case 'TIME_UNIT_HOUR': return duration * 60;
501
+ case 'TIME_UNIT_SECOND': return duration / 60;
502
+ default: return duration; // TIME_UNIT_MINUTE or unknown -> minutes
503
+ }
504
+ }
505
+ /** Derive a display plan label from Kimi's membership tier or subscription type. */
506
+ export function formatKimiPlan(data) {
507
+ const level = data.user?.membership?.level;
508
+ const raw = (typeof level === 'string' && level) || (typeof data.subType === 'string' && data.subType) || '';
509
+ const tail = raw.split('_').pop() || ''; // LEVEL_INTERMEDIATE -> INTERMEDIATE
510
+ if (!tail)
511
+ return null;
512
+ return tail.charAt(0).toUpperCase() + tail.slice(1).toLowerCase();
513
+ }
359
514
  /** Collect Codex JSONL session files sorted newest-first. */
360
515
  function collectCodexSessionFiles(home) {
361
516
  const base = home || os.homedir();
@@ -590,6 +745,7 @@ function writeClaudeUsageCacheFile(cache, cachePath) {
590
745
  function serializeClaudeUsageSnapshot(snapshot) {
591
746
  return {
592
747
  capturedAt: snapshot.capturedAt?.toISOString() || null,
748
+ plan: snapshot.plan ?? null,
593
749
  windows: snapshot.windows.map((window) => ({
594
750
  key: window.key,
595
751
  label: window.label,
@@ -626,6 +782,7 @@ function deserializeClaudeUsageSnapshot(snapshot, now) {
626
782
  sourceLabel: CACHED_CLAUDE_USAGE_SOURCE_LABEL,
627
783
  capturedAt,
628
784
  windows,
785
+ plan: snapshot.plan ?? null,
629
786
  };
630
787
  }
631
788
  /** Check whether a cached usage window is still relevant (not expired or reset). */
@@ -1,4 +1,5 @@
1
1
  import type { AgentId } from './types.js';
2
+ import { compareVersions } from './agent-spec/primitives.js';
2
3
  /**
3
4
  * Resource selection for syncing to a version.
4
5
  * Each field can be:
@@ -279,10 +280,7 @@ export declare function resolveVersionAliasLoose(agent: AgentId, raw: string | u
279
280
  * Get version specified in a project-root agents.yaml (not the user ~/.agents/.system/agents.yaml).
280
281
  */
281
282
  export declare function getProjectVersion(agent: AgentId, startPath: string): string | null;
282
- /**
283
- * Compare semver versions for sorting.
284
- */
285
- export declare function compareVersions(a: string, b: string): number;
283
+ export { compareVersions };
286
284
  /**
287
285
  * Get actual version from an installed 'latest' directory.
288
286
  */
@@ -24,6 +24,10 @@ import { checkbox, select } from '@inquirer/prompts';
24
24
  import { getVersionsDir, ensureAgentsDir, readMeta, writeMeta, getCommandsDir, getSkillsDir, getHooksDir, getResolvedRulesDir, getUserRulesDir, getVersionResources, ensureVersionResourcePatterns, getProjectAgentsDir, getPromptcutsPath, getUserPromptcutsPath, getEnabledExtraRepos, getAgentsDir, getUserAgentsDir, getTrashVersionsDir, getActiveRulesPreset, getHomeDir } from './state.js';
25
25
  import { defaultPatterns, expandPatterns } from './resource-patterns.js';
26
26
  import { listResources } from './resources.js';
27
+ // VERSION_RE + compareVersions are owned by the agent-spec engine primitives
28
+ // (single source of truth). Re-exported below so existing importers of
29
+ // `compareVersions` from './versions.js' keep working.
30
+ import { VERSION_RE, compareVersions } from './agent-spec/primitives.js';
27
31
  import { AGENTS, agentConfigDirName, getAccountEmail, resolveAgentName, formatAgentError, findInPath } from './agents.js';
28
32
  import { discoverPermissionGroups, getActivePermissionPresetName, readPermissionPresetRecipe, PERMISSION_PRESET_ENV_VAR } from './permissions.js';
29
33
  import { parseMcpServerConfig } from './mcp.js';
@@ -41,11 +45,6 @@ import { getWriter, getDetector } from './staleness/registry.js';
41
45
  const execAsync = promisify(exec);
42
46
  const execFileAsync = promisify(execFile);
43
47
  const RULES_DOC_FILENAME = 'README.md';
44
- // Strict shape for an agent version string. Anything outside this is rejected
45
- // at parse time so it can't reach an exec/shell boundary or get interpolated
46
- // into a generated bash alias. Must allow "latest" plus npm-dist-tag /
47
- // semver-shaped values (digits, dots, dashes, +, _).
48
- const VERSION_RE = /^(?:latest|(?!.*\.\.)[A-Za-z0-9._+-]{1,64})$/;
49
48
  function getResourceBases(cwd) {
50
49
  const projectAgentsDir = getProjectAgentsDir(cwd);
51
50
  const userBase = getUserAgentsDir();
@@ -1475,21 +1474,9 @@ export function getProjectVersion(agent, startPath) {
1475
1474
  }
1476
1475
  return null;
1477
1476
  }
1478
- /**
1479
- * Compare semver versions for sorting.
1480
- */
1481
- export function compareVersions(a, b) {
1482
- const aParts = a.split('.').map((n) => parseInt(n, 10) || 0);
1483
- const bParts = b.split('.').map((n) => parseInt(n, 10) || 0);
1484
- for (let i = 0; i < Math.max(aParts.length, bParts.length); i++) {
1485
- const aVal = aParts[i] || 0;
1486
- const bVal = bParts[i] || 0;
1487
- if (aVal !== bVal) {
1488
- return aVal - bVal;
1489
- }
1490
- }
1491
- return 0;
1492
- }
1477
+ // compareVersions is defined in ./agent-spec/primitives.ts and re-exported here
1478
+ // so existing `import { compareVersions } from './versions.js'` sites keep working.
1479
+ export { compareVersions };
1493
1480
  /**
1494
1481
  * Get actual version from an installed 'latest' directory.
1495
1482
  */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.32",
3
+ "version": "1.20.34",
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",
@@ -51,6 +51,7 @@
51
51
  "dev": "tsx src/index.ts",
52
52
  "start": "node dist/index.js",
53
53
  "test": "node ./node_modules/vitest/vitest.mjs run",
54
+ "test:remote": "scripts/sandbox.sh 'bun install && bun run build && bun run test'",
54
55
  "test:watch": "node ./node_modules/vitest/vitest.mjs"
55
56
  },
56
57
  "keywords": [
@@ -1,36 +0,0 @@
1
- import type { AgentId } from './types.js';
2
- export interface AgentTarget {
3
- agent: AgentId;
4
- /** Resolved exact version, or null when the agent has no installed versions yet. */
5
- version: string | null;
6
- }
7
- /** Canonical qualifier set, in help/display order. `pinned` ≡ `default`. */
8
- export declare const AGENT_QUALIFIERS: readonly ["latest", "oldest", "pinned", "default", "all"];
9
- export type AgentQualifier = (typeof AGENT_QUALIFIERS)[number];
10
- /** Shared `--help` epilog so every agent-spec command documents the same grammar. */
11
- export declare const AGENT_SPEC_HELP: string;
12
- export declare class AgentSpecError extends Error {
13
- constructor(message: string);
14
- }
15
- export interface ResolveAgentTargetsOptions {
16
- /** Project dir for resolving a bare spec's project pin. Defaults to process.cwd(). */
17
- cwd?: string;
18
- /** Restrict the agents a spec may name (e.g. only mcp-capable). Defaults to all. */
19
- availableAgents?: readonly AgentId[];
20
- }
21
- /**
22
- * Resolve an agent spec (single or comma-list) into concrete installed targets.
23
- * Domain = installed: `@latest`/`@oldest`/`@all` range over installed versions
24
- * (`add`/`install` use a separate available-version path). Throws AgentSpecError
25
- * on bad input — never calls process.exit, so it is safe on the hot path and in
26
- * library contexts.
27
- */
28
- export declare function resolveAgentTargets(spec: string, opts?: ResolveAgentTargetsOptions): AgentTarget[];
29
- /**
30
- * Convenience for single-target commands (`use`, `run`): resolve a spec that
31
- * must name exactly one installed version. Rejects `@all` / multi-target specs.
32
- */
33
- export declare function resolveSingleAgentTarget(spec: string, opts?: ResolveAgentTargetsOptions): {
34
- agent: AgentId;
35
- version: string;
36
- };