@phnx-labs/agents-cli 1.20.33 → 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 (56) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +28 -2
  3. package/dist/commands/computer.d.ts +23 -0
  4. package/dist/commands/computer.js +45 -3
  5. package/dist/commands/doctor.d.ts +10 -0
  6. package/dist/commands/doctor.js +49 -0
  7. package/dist/commands/import.js +1 -1
  8. package/dist/commands/rules.js +1 -1
  9. package/dist/commands/secrets-migrate.js +23 -11
  10. package/dist/commands/secrets.d.ts +20 -0
  11. package/dist/commands/secrets.js +53 -1
  12. package/dist/commands/status.d.ts +12 -0
  13. package/dist/commands/status.js +81 -0
  14. package/dist/commands/teams.js +70 -6
  15. package/dist/commands/versions.js +2 -1
  16. package/dist/commands/view.d.ts +39 -0
  17. package/dist/commands/view.js +194 -75
  18. package/dist/index.js +4 -2
  19. package/dist/lib/acp/harnesses.d.ts +1 -1
  20. package/dist/lib/acp/harnesses.js +2 -2
  21. package/dist/lib/agents.d.ts +12 -0
  22. package/dist/lib/agents.js +115 -32
  23. package/dist/lib/browser/chrome.js +20 -0
  24. package/dist/lib/browser/drivers/ssh.d.ts +19 -0
  25. package/dist/lib/browser/drivers/ssh.js +18 -3
  26. package/dist/lib/doctor-diff.js +29 -2
  27. package/dist/lib/drift-sync.d.ts +43 -0
  28. package/dist/lib/drift-sync.js +179 -0
  29. package/dist/lib/exec.d.ts +15 -0
  30. package/dist/lib/exec.js +21 -11
  31. package/dist/lib/platform/winpath.d.ts +31 -2
  32. package/dist/lib/platform/winpath.js +133 -24
  33. package/dist/lib/pwsh.d.ts +11 -0
  34. package/dist/lib/pwsh.js +13 -0
  35. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  36. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  37. package/dist/lib/secrets/agent.d.ts +42 -1
  38. package/dist/lib/secrets/agent.js +89 -11
  39. package/dist/lib/secrets/bundles.js +40 -9
  40. package/dist/lib/secrets/filestore.js +31 -1
  41. package/dist/lib/secrets/index.d.ts +33 -1
  42. package/dist/lib/secrets/index.js +90 -9
  43. package/dist/lib/secrets/windows.d.ts +74 -0
  44. package/dist/lib/secrets/windows.js +440 -0
  45. package/dist/lib/shims.d.ts +20 -0
  46. package/dist/lib/shims.js +53 -20
  47. package/dist/lib/startup/command-registry.d.ts +1 -0
  48. package/dist/lib/startup/command-registry.js +2 -0
  49. package/dist/lib/sync-status.d.ts +102 -0
  50. package/dist/lib/sync-status.js +135 -0
  51. package/dist/lib/teams/agents.d.ts +24 -0
  52. package/dist/lib/teams/agents.js +30 -1
  53. package/dist/lib/types.d.ts +20 -1
  54. package/dist/lib/usage.d.ts +30 -0
  55. package/dist/lib/usage.js +159 -2
  56. package/package.json +1 -1
@@ -0,0 +1,102 @@
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 { AgentId } from './types.js';
22
+ import { type DoctorKind } from './doctor-diff.js';
23
+ /**
24
+ * One stable status per resource, unified across every surface.
25
+ * - `synced` — installed copy matches the resolved source (DiffStatus 'ok').
26
+ * - `drifted` — installed copy exists but differs from source (DiffStatus 'diff').
27
+ * - `missing` — source exists, nothing installed in the version home ('missing').
28
+ * - `orphan` — installed in the home with no source ('extra'); prune's job, not sync's.
29
+ */
30
+ export type ResourceSyncStatus = 'synced' | 'drifted' | 'missing' | 'orphan';
31
+ export interface ResourceStatusRow {
32
+ agent: AgentId;
33
+ version: string;
34
+ kind: DoctorKind;
35
+ name: string;
36
+ status: ResourceSyncStatus;
37
+ /** Human-readable specifics for a drifted row (e.g. plugin version delta). */
38
+ detail?: string;
39
+ }
40
+ export interface AgentVersionStatus {
41
+ agent: AgentId;
42
+ version: string;
43
+ isDefault: boolean;
44
+ /** False = no .sync-manifest.json: this version was never synced (cold). */
45
+ everSynced: boolean;
46
+ counts: {
47
+ synced: number;
48
+ drifted: number;
49
+ missing: number;
50
+ orphan: number;
51
+ };
52
+ /** drifted + missing > 0 — a real reconcile is owed. Orphans do NOT set this
53
+ * (heal never deletes; orphan removal is `agents prune cleanup`). */
54
+ needsSync: boolean;
55
+ resources: ResourceStatusRow[];
56
+ }
57
+ export interface SystemRepoStatus {
58
+ dir: string;
59
+ /** Commits the local `.system` checkout is behind its tracking branch, as of
60
+ * the last background fetch (no network is performed here). 0 = up to date. */
61
+ behind: number;
62
+ ahead: number;
63
+ branch: string | null;
64
+ /** True when the dir isn't a git repo or has no upstream — behind is unknown. */
65
+ unknown: boolean;
66
+ }
67
+ export interface UnifiedSyncStatus {
68
+ system: SystemRepoStatus;
69
+ agents: AgentVersionStatus[];
70
+ totals: {
71
+ drifted: number;
72
+ missing: number;
73
+ orphan: number;
74
+ /** Versions with a manifest that are behind on content. */
75
+ versionsNeedingSync: number;
76
+ /** Versions that were never synced at all. */
77
+ versionsNeverSynced: number;
78
+ /** Distinct agent ids that own at least one version needing sync. */
79
+ agentsNeedingSync: number;
80
+ };
81
+ }
82
+ export interface SyncStatusOptions {
83
+ cwd?: string;
84
+ /** Restrict to specific agent ids; undefined = every supported agent. */
85
+ agents?: AgentId[];
86
+ /** Restrict to specific resource kinds; undefined = all. */
87
+ kinds?: DoctorKind[];
88
+ }
89
+ /**
90
+ * Read `.system` repo freshness WITHOUT touching the network. `git status`
91
+ * reports ahead/behind against the remote-tracking ref, which the detached
92
+ * auto-pull worker keeps warm via periodic `git fetch`. This is the same number
93
+ * the menu-bar surfaces; we read it once, here, so every surface agrees.
94
+ */
95
+ export declare function getSystemRepoStatus(): Promise<SystemRepoStatus>;
96
+ /**
97
+ * Compute unified sync status across the fleet. Resolves against non-project
98
+ * layers only (`excludeProject: true`) — the GLOBAL version home is never
99
+ * reconciled against per-cwd `<cwd>/.agents/` resources, so counting them as
100
+ * "missing" there would be a false gap (matches doctor's overview semantics).
101
+ */
102
+ export declare function computeSyncStatus(options?: SyncStatusOptions): Promise<UnifiedSyncStatus>;
@@ -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). */
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.33",
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",