@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.
- package/CHANGELOG.md +7 -0
- package/README.md +28 -2
- package/dist/commands/commands.js +3 -3
- package/dist/commands/computer-actions.js +1 -0
- package/dist/commands/computer.d.ts +23 -0
- package/dist/commands/computer.js +45 -3
- package/dist/commands/cost.js +2 -2
- package/dist/commands/doctor.d.ts +10 -0
- package/dist/commands/doctor.js +51 -2
- package/dist/commands/exec.js +56 -1
- package/dist/commands/hooks.js +3 -3
- package/dist/commands/import.js +1 -1
- package/dist/commands/inspect.js +13 -17
- package/dist/commands/mcp.js +3 -3
- package/dist/commands/permissions.js +3 -3
- package/dist/commands/rules.js +3 -3
- package/dist/commands/secrets-migrate.js +23 -11
- package/dist/commands/secrets.d.ts +20 -0
- package/dist/commands/secrets.js +53 -1
- package/dist/commands/sessions.js +18 -1
- package/dist/commands/skills.js +3 -3
- package/dist/commands/status.d.ts +12 -0
- package/dist/commands/status.js +81 -0
- package/dist/commands/sync.js +2 -2
- package/dist/commands/teams.js +77 -18
- package/dist/commands/usage.js +2 -2
- package/dist/commands/utils.d.ts +8 -0
- package/dist/commands/utils.js +20 -0
- package/dist/commands/versions.js +4 -3
- package/dist/commands/view.d.ts +39 -0
- package/dist/commands/view.js +217 -84
- package/dist/commands/workflows.js +3 -3
- package/dist/index.js +16 -2
- package/dist/lib/acp/harnesses.d.ts +1 -1
- package/dist/lib/acp/harnesses.js +2 -2
- package/dist/lib/agent-spec/index.d.ts +18 -0
- package/dist/lib/agent-spec/index.js +35 -0
- package/dist/lib/agent-spec/primitives.d.ts +28 -0
- package/dist/lib/agent-spec/primitives.js +57 -0
- package/dist/lib/agent-spec/provider.d.ts +2 -0
- package/dist/lib/agent-spec/provider.js +9 -0
- package/dist/lib/agent-spec/resolve.d.ts +33 -0
- package/dist/lib/agent-spec/resolve.js +174 -0
- package/dist/lib/agent-spec/types.d.ts +57 -0
- package/dist/lib/agent-spec/types.js +18 -0
- package/dist/lib/agents.d.ts +12 -0
- package/dist/lib/agents.js +115 -32
- package/dist/lib/browser/chrome.js +20 -0
- package/dist/lib/browser/drivers/ssh.d.ts +19 -0
- package/dist/lib/browser/drivers/ssh.js +18 -3
- package/dist/lib/crabbox/cli.d.ts +98 -0
- package/dist/lib/crabbox/cli.js +218 -0
- package/dist/lib/crabbox/lease.d.ts +41 -0
- package/dist/lib/crabbox/lease.js +73 -0
- package/dist/lib/crabbox/runtimes.d.ts +57 -0
- package/dist/lib/crabbox/runtimes.js +109 -0
- package/dist/lib/doctor-diff.js +29 -2
- package/dist/lib/drift-sync.d.ts +43 -0
- package/dist/lib/drift-sync.js +179 -0
- package/dist/lib/exec.d.ts +15 -0
- package/dist/lib/exec.js +21 -11
- package/dist/lib/hosts/dispatch.d.ts +27 -10
- package/dist/lib/hosts/dispatch.js +55 -19
- package/dist/lib/hosts/option.d.ts +14 -0
- package/dist/lib/hosts/option.js +19 -0
- package/dist/lib/hosts/passthrough.d.ts +30 -0
- package/dist/lib/hosts/passthrough.js +141 -0
- package/dist/lib/hosts/remote-cmd.d.ts +36 -0
- package/dist/lib/hosts/remote-cmd.js +56 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/platform/winpath.d.ts +31 -2
- package/dist/lib/platform/winpath.js +133 -24
- package/dist/lib/pwsh.d.ts +11 -0
- package/dist/lib/pwsh.js +13 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
- package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
- package/dist/lib/secrets/agent.d.ts +42 -1
- package/dist/lib/secrets/agent.js +89 -11
- package/dist/lib/secrets/bundles.js +61 -21
- package/dist/lib/secrets/filestore.js +31 -1
- package/dist/lib/secrets/index.d.ts +44 -1
- package/dist/lib/secrets/index.js +108 -10
- package/dist/lib/secrets/linux.d.ts +14 -0
- package/dist/lib/secrets/linux.js +21 -0
- package/dist/lib/secrets/windows.d.ts +74 -0
- package/dist/lib/secrets/windows.js +440 -0
- package/dist/lib/session/active.d.ts +8 -0
- package/dist/lib/session/active.js +18 -1
- package/dist/lib/session/provenance.d.ts +56 -0
- package/dist/lib/session/provenance.js +157 -0
- package/dist/lib/shims.d.ts +20 -0
- package/dist/lib/shims.js +53 -20
- package/dist/lib/ssh-exec.d.ts +22 -0
- package/dist/lib/ssh-exec.js +59 -2
- package/dist/lib/ssh-tunnel.d.ts +0 -5
- package/dist/lib/ssh-tunnel.js +65 -8
- package/dist/lib/startup/command-registry.d.ts +1 -0
- package/dist/lib/startup/command-registry.js +2 -0
- package/dist/lib/sync-status.d.ts +102 -0
- package/dist/lib/sync-status.js +135 -0
- package/dist/lib/teams/agents.d.ts +24 -0
- package/dist/lib/teams/agents.js +30 -1
- package/dist/lib/types.d.ts +20 -1
- package/dist/lib/usage.d.ts +30 -0
- package/dist/lib/usage.js +159 -2
- package/dist/lib/versions.d.ts +2 -4
- package/dist/lib/versions.js +7 -20
- package/package.json +2 -1
- package/dist/lib/agent-spec.d.ts +0 -36
- 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
|
/**
|
package/dist/lib/teams/agents.js
CHANGED
|
@@ -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() {
|
package/dist/lib/types.d.ts
CHANGED
|
@@ -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' | '
|
|
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;
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -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
|
-
//
|
|
118
|
-
|
|
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/dist/lib/versions.d.ts
CHANGED
|
@@ -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
|
*/
|
package/dist/lib/versions.js
CHANGED
|
@@ -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
|
-
|
|
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.
|
|
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": [
|
package/dist/lib/agent-spec.d.ts
DELETED
|
@@ -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
|
-
};
|