@phnx-labs/agents-cli 1.20.86 → 1.20.88
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 +172 -0
- package/README.md +3 -3
- package/dist/bin/agents +0 -0
- package/dist/commands/doctor.d.ts +0 -19
- package/dist/commands/doctor.js +219 -305
- package/dist/commands/exec.js +7 -19
- package/dist/commands/inspect.js +3 -5
- package/dist/commands/routines.js +2 -2
- package/dist/commands/sessions-browser.d.ts +18 -0
- package/dist/commands/sessions-browser.js +126 -24
- package/dist/commands/sessions-picker.d.ts +21 -8
- package/dist/commands/sessions-picker.js +83 -7
- package/dist/commands/sessions.d.ts +19 -0
- package/dist/commands/sessions.js +148 -18
- package/dist/commands/ssh.js +62 -4
- package/dist/commands/teams-picker.d.ts +2 -0
- package/dist/commands/teams-picker.js +2 -1
- package/dist/commands/teams.d.ts +4 -1
- package/dist/commands/teams.js +106 -70
- package/dist/commands/usage.d.ts +3 -2
- package/dist/commands/usage.js +2 -9
- package/dist/commands/view.js +14 -3
- package/dist/index.js +31 -1
- package/dist/lib/agents.d.ts +31 -1
- package/dist/lib/agents.js +55 -0
- package/dist/lib/claude-account-token.d.ts +12 -0
- package/dist/lib/claude-account-token.js +63 -0
- package/dist/lib/command-skills.d.ts +10 -0
- package/dist/lib/command-skills.js +14 -0
- package/dist/lib/commands.js +19 -1
- package/dist/lib/daemon.js +17 -2
- package/dist/lib/devices/doctor-findings.d.ts +167 -0
- package/dist/lib/devices/doctor-findings.js +893 -0
- package/dist/lib/devices/fleet-divergence.d.ts +22 -0
- package/dist/lib/devices/fleet-divergence.js +34 -10
- package/dist/lib/devices/fleet-inventory.d.ts +17 -6
- package/dist/lib/devices/fleet-inventory.js +56 -8
- package/dist/lib/devices/registry.d.ts +25 -0
- package/dist/lib/devices/registry.js +82 -1
- package/dist/lib/events.d.ts +8 -1
- package/dist/lib/events.js +13 -0
- package/dist/lib/exec.d.ts +14 -3
- package/dist/lib/exec.js +51 -9
- package/dist/lib/format.d.ts +7 -0
- package/dist/lib/format.js +11 -0
- package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
- package/dist/lib/project-resources.js +34 -20
- package/dist/lib/runner.d.ts +14 -1
- package/dist/lib/runner.js +37 -8
- package/dist/lib/sandbox.d.ts +2 -0
- package/dist/lib/sandbox.js +38 -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/rc-hygiene.d.ts +0 -6
- package/dist/lib/secrets/rc-hygiene.js +0 -24
- package/dist/lib/session/active.d.ts +6 -6
- package/dist/lib/session/active.js +6 -6
- package/dist/lib/session/db.d.ts +21 -0
- package/dist/lib/session/db.js +45 -4
- package/dist/lib/session/discover.d.ts +5 -0
- package/dist/lib/session/discover.js +137 -1
- package/dist/lib/session/parse.d.ts +2 -0
- package/dist/lib/session/parse.js +76 -37
- package/dist/lib/session/remote-list.d.ts +7 -0
- package/dist/lib/session/remote-list.js +8 -4
- package/dist/lib/session/state.js +69 -2
- package/dist/lib/session/sync/agents.js +0 -0
- package/dist/lib/session/team-filter.d.ts +22 -3
- package/dist/lib/session/team-filter.js +106 -17
- package/dist/lib/session/types.d.ts +9 -1
- package/dist/lib/session/types.js +1 -1
- package/dist/lib/signin-badge.d.ts +17 -0
- package/dist/lib/signin-badge.js +19 -0
- package/dist/lib/staleness/detectors/commands.js +14 -5
- package/dist/lib/staleness/types.d.ts +2 -0
- package/dist/lib/staleness/writers/commands.js +13 -7
- package/dist/lib/state.d.ts +2 -0
- package/dist/lib/state.js +2 -0
- package/dist/lib/usage.d.ts +72 -1
- package/dist/lib/usage.js +22 -87
- package/dist/lib/versions.js +30 -13
- package/package.json +1 -1
|
@@ -9,6 +9,7 @@
|
|
|
9
9
|
import * as fs from 'fs';
|
|
10
10
|
import * as os from 'os';
|
|
11
11
|
import * as path from 'path';
|
|
12
|
+
import { sanitizeForTerminal } from './parse.js';
|
|
12
13
|
import { getTeamsAgentsDir } from '../state.js';
|
|
13
14
|
const HOME = os.homedir();
|
|
14
15
|
// Default path; tests can override via AGENTS_TEAMS_DIR env var.
|
|
@@ -21,33 +22,121 @@ function teamsAgentsDir() {
|
|
|
21
22
|
*
|
|
22
23
|
* Primary signal is `session.isTeamOrigin`, captured at scan time from the
|
|
23
24
|
* JSONL `entrypoint` field ('sdk-cli' for team spawns, 'cli' for real CLI).
|
|
24
|
-
* When a team meta.json exists we additionally enrich with handle/mode
|
|
25
|
-
*
|
|
26
|
-
* was cleaned up still get recognized
|
|
25
|
+
* When a team meta.json exists we additionally enrich with handle/mode/team and
|
|
26
|
+
* the orchestrator that spawned it — but its absence no longer demotes a
|
|
27
|
+
* session: older team runs whose meta dir was cleaned up still get recognized
|
|
28
|
+
* via the entrypoint flag.
|
|
27
29
|
*
|
|
28
30
|
* Returns the TeamOrigin metadata when the session is team-origin, or null
|
|
29
31
|
* when it is a normal interactive session.
|
|
30
32
|
*/
|
|
31
33
|
export function classifyTeamSession(session) {
|
|
32
|
-
const
|
|
33
|
-
if (
|
|
34
|
-
|
|
35
|
-
const raw = fs.readFileSync(metaPath, 'utf-8');
|
|
36
|
-
const meta = JSON.parse(raw);
|
|
37
|
-
const name = typeof meta.name === 'string' && meta.name ? meta.name : undefined;
|
|
38
|
-
const handle = name ?? session.id.slice(0, 8);
|
|
39
|
-
const mode = typeof meta.mode === 'string' ? meta.mode : undefined;
|
|
40
|
-
return { handle, mode };
|
|
41
|
-
}
|
|
42
|
-
catch {
|
|
43
|
-
return { handle: session.id.slice(0, 8) };
|
|
44
|
-
}
|
|
45
|
-
}
|
|
34
|
+
const origin = teamOriginIndex().get(session.id);
|
|
35
|
+
if (origin)
|
|
36
|
+
return origin;
|
|
46
37
|
if (session.isTeamOrigin) {
|
|
47
38
|
return { handle: session.id.slice(0, 8) };
|
|
48
39
|
}
|
|
49
40
|
return null;
|
|
50
41
|
}
|
|
42
|
+
/**
|
|
43
|
+
* Parse one teammate `meta.json` into a {@link TeamOrigin}. Degrades to a bare
|
|
44
|
+
* handle when the file is unreadable or malformed — a teammate whose record we
|
|
45
|
+
* can't parse is still a teammate.
|
|
46
|
+
*/
|
|
47
|
+
function readTeamOrigin(metaPath, agentId) {
|
|
48
|
+
try {
|
|
49
|
+
const meta = JSON.parse(fs.readFileSync(metaPath, 'utf-8'));
|
|
50
|
+
const str = (v) => (typeof v === 'string' && v ? v : undefined);
|
|
51
|
+
return {
|
|
52
|
+
origin: {
|
|
53
|
+
handle: str(meta.name) ?? agentId.slice(0, 8),
|
|
54
|
+
mode: str(meta.mode),
|
|
55
|
+
team: str(meta.task_name),
|
|
56
|
+
parentSessionId: str(meta.parent_session_id),
|
|
57
|
+
},
|
|
58
|
+
sessionId: str(meta.remote_session_id),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return { origin: { handle: agentId.slice(0, 8) } };
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
/**
|
|
66
|
+
* A team name / handle as it is safe to render: a real string, terminal escapes
|
|
67
|
+
* stripped, or undefined.
|
|
68
|
+
*
|
|
69
|
+
* These values reach the row and the preview pane, and for a peer's row they are
|
|
70
|
+
* whatever JSON that machine sent — `parseRemoteList` copies the object through
|
|
71
|
+
* without inspecting its fields, so neither the type nor the content is ours to
|
|
72
|
+
* assume. A non-string `spawnedTeam` used to throw out of `teamBadge`, which runs
|
|
73
|
+
* on every row, taking down the whole listing rather than one entry.
|
|
74
|
+
*/
|
|
75
|
+
export function safeTeamText(value) {
|
|
76
|
+
if (typeof value !== 'string' || value === '')
|
|
77
|
+
return undefined;
|
|
78
|
+
return sanitizeForTerminal(value);
|
|
79
|
+
}
|
|
80
|
+
/** Cached teammate index; the directory is small (teams GC it after 7 days). */
|
|
81
|
+
let originIndexCache = null;
|
|
82
|
+
/** Drop the cached teammate index — for tests that rewrite AGENTS_TEAMS_DIR. */
|
|
83
|
+
export function _resetTeamOriginIndex() {
|
|
84
|
+
originIndexCache = null;
|
|
85
|
+
}
|
|
86
|
+
/**
|
|
87
|
+
* Every teammate record, keyed by the session ids it can be reached under.
|
|
88
|
+
*
|
|
89
|
+
* A teammate's directory name is its **agent id**, which is only sometimes the
|
|
90
|
+
* id of the transcript it produced: the harness mints its own session id, and
|
|
91
|
+
* the spawn records it separately as `remote_session_id`. Keying the lookup on
|
|
92
|
+
* the directory name alone therefore missed most teammates — on a live box, 14
|
|
93
|
+
* of 16 records were reachable only by `remote_session_id` — so a teammate row
|
|
94
|
+
* could not name its team however good the record was. Both keys are registered.
|
|
95
|
+
*
|
|
96
|
+
* Read once per process rather than per row: the old per-session `existsSync` +
|
|
97
|
+
* `readFileSync` cost a pair of syscalls for every row in the pool, which the
|
|
98
|
+
* interactive browser re-pays on each hotkey.
|
|
99
|
+
*/
|
|
100
|
+
function teamOriginIndex() {
|
|
101
|
+
if (originIndexCache)
|
|
102
|
+
return originIndexCache;
|
|
103
|
+
const dir = teamsAgentsDir();
|
|
104
|
+
const index = new Map();
|
|
105
|
+
let entries;
|
|
106
|
+
try {
|
|
107
|
+
entries = fs.readdirSync(dir);
|
|
108
|
+
}
|
|
109
|
+
catch {
|
|
110
|
+
entries = [];
|
|
111
|
+
}
|
|
112
|
+
for (const agentId of entries) {
|
|
113
|
+
const metaPath = path.join(dir, agentId, 'meta.json');
|
|
114
|
+
if (!fs.existsSync(metaPath))
|
|
115
|
+
continue;
|
|
116
|
+
const { origin, sessionId } = readTeamOrigin(metaPath, agentId);
|
|
117
|
+
index.set(agentId, origin);
|
|
118
|
+
if (sessionId)
|
|
119
|
+
index.set(sessionId, origin);
|
|
120
|
+
}
|
|
121
|
+
originIndexCache = index;
|
|
122
|
+
return index;
|
|
123
|
+
}
|
|
124
|
+
/**
|
|
125
|
+
* Attach `teamOrigin` to every team-spawned row in `sessions`, from the shared
|
|
126
|
+
* teammate index rather than a stat per row.
|
|
127
|
+
*/
|
|
128
|
+
export function enrichTeamOrigins(sessions) {
|
|
129
|
+
const index = teamOriginIndex();
|
|
130
|
+
return sessions.map((session) => {
|
|
131
|
+
// A peer's rows are classified on the peer (its meta.json is on its disk, not
|
|
132
|
+
// ours) and ride across in the --json fan-out already populated. Re-deriving
|
|
133
|
+
// here would find no local record and downgrade a named teammate to a bare id.
|
|
134
|
+
if (session.teamOrigin)
|
|
135
|
+
return session;
|
|
136
|
+
const origin = index.get(session.id) ?? (session.isTeamOrigin ? { handle: session.id.slice(0, 8) } : null);
|
|
137
|
+
return origin ? { ...session, teamOrigin: origin } : session;
|
|
138
|
+
});
|
|
139
|
+
}
|
|
51
140
|
/**
|
|
52
141
|
* Split `sessions` into visible and hidden (team-origin) groups.
|
|
53
142
|
* When `showTeams` is true every session is visible and `teamOrigin` is
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** Agents that store session data on disk and can be discovered by `agents sessions`. */
|
|
10
|
-
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'antigravity' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid';
|
|
10
|
+
export type SessionAgentId = 'claude' | 'codex' | 'gemini' | 'antigravity' | 'opencode' | 'openclaw' | 'rush' | 'hermes' | 'grok' | 'kimi' | 'droid' | 'cursor';
|
|
11
11
|
/** All agents with session discovery support, in display order. */
|
|
12
12
|
export declare const SESSION_AGENTS: SessionAgentId[];
|
|
13
13
|
/**
|
|
@@ -88,6 +88,14 @@ export interface TeamOrigin {
|
|
|
88
88
|
handle?: string;
|
|
89
89
|
/** Agent mode: 'plan', 'edit', 'auto', or 'skip' ('full' accepted as legacy alias for 'skip'). */
|
|
90
90
|
mode?: string;
|
|
91
|
+
/** The team this teammate belongs to (`task_name` in its meta.json). */
|
|
92
|
+
team?: string;
|
|
93
|
+
/**
|
|
94
|
+
* The orchestrator session that spawned this teammate (`parent_session_id`).
|
|
95
|
+
* Absent for a team started outside any agent session, and for teammates whose
|
|
96
|
+
* meta dir has aged past the teams cleanup window.
|
|
97
|
+
*/
|
|
98
|
+
parentSessionId?: string;
|
|
91
99
|
}
|
|
92
100
|
/** Lightweight metadata for a discovered session, used in listings and pickers. */
|
|
93
101
|
export interface SessionMeta {
|
|
@@ -7,7 +7,7 @@
|
|
|
7
7
|
* speaks these types.
|
|
8
8
|
*/
|
|
9
9
|
/** All agents with session discovery support, in display order. */
|
|
10
|
-
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'antigravity', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid'];
|
|
10
|
+
export const SESSION_AGENTS = ['claude', 'codex', 'gemini', 'antigravity', 'opencode', 'openclaw', 'rush', 'hermes', 'grok', 'kimi', 'droid', 'cursor'];
|
|
11
11
|
/**
|
|
12
12
|
* True when `agent` stores session data `agents sessions` can discover (a member
|
|
13
13
|
* of {@link SESSION_AGENTS}). The single predicate every session-index writer
|
|
@@ -23,6 +23,23 @@ export declare function loginHint(agentId: AgentId): string;
|
|
|
23
23
|
* agents (the ones the feature is for), so the resume's `forceInteractive` flag is
|
|
24
24
|
* consulted directly.
|
|
25
25
|
*/
|
|
26
|
+
/**
|
|
27
|
+
* Is a Claude run on this box going to authenticate from an ambient
|
|
28
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` rather than a per-version login?
|
|
29
|
+
*
|
|
30
|
+
* `AccountInfo.signedIn` is `!!email` read from a version home's `.claude.json`
|
|
31
|
+
* (agents.ts), so a version with no account written there reports signed-out —
|
|
32
|
+
* even though Claude Code authenticates fine from the env token and the run
|
|
33
|
+
* succeeds. Rendering that as "logged out" sends people hunting a login that is
|
|
34
|
+
* not missing (a real fleet incident: every version on a box read as locked out
|
|
35
|
+
* while all of them answered a live prompt).
|
|
36
|
+
*
|
|
37
|
+
* It is also the more useful warning: an ambient token is ONE account, so every
|
|
38
|
+
* version on the box resolves to it and balanced rotation across them rotates
|
|
39
|
+
* nothing. `env` is a parameter so the branch is testable without mutating the
|
|
40
|
+
* process environment.
|
|
41
|
+
*/
|
|
42
|
+
export declare function ambientClaudeToken(agentId: AgentId | string, env?: NodeJS.ProcessEnv): boolean;
|
|
26
43
|
export declare function shouldCheckLoginBeforeLaunch(o: {
|
|
27
44
|
interactive?: boolean;
|
|
28
45
|
forceInteractive?: boolean;
|
package/dist/lib/signin-badge.js
CHANGED
|
@@ -46,6 +46,25 @@ export function loginHint(agentId) {
|
|
|
46
46
|
* agents (the ones the feature is for), so the resume's `forceInteractive` flag is
|
|
47
47
|
* consulted directly.
|
|
48
48
|
*/
|
|
49
|
+
/**
|
|
50
|
+
* Is a Claude run on this box going to authenticate from an ambient
|
|
51
|
+
* `CLAUDE_CODE_OAUTH_TOKEN` rather than a per-version login?
|
|
52
|
+
*
|
|
53
|
+
* `AccountInfo.signedIn` is `!!email` read from a version home's `.claude.json`
|
|
54
|
+
* (agents.ts), so a version with no account written there reports signed-out —
|
|
55
|
+
* even though Claude Code authenticates fine from the env token and the run
|
|
56
|
+
* succeeds. Rendering that as "logged out" sends people hunting a login that is
|
|
57
|
+
* not missing (a real fleet incident: every version on a box read as locked out
|
|
58
|
+
* while all of them answered a live prompt).
|
|
59
|
+
*
|
|
60
|
+
* It is also the more useful warning: an ambient token is ONE account, so every
|
|
61
|
+
* version on the box resolves to it and balanced rotation across them rotates
|
|
62
|
+
* nothing. `env` is a parameter so the branch is testable without mutating the
|
|
63
|
+
* process environment.
|
|
64
|
+
*/
|
|
65
|
+
export function ambientClaudeToken(agentId, env = process.env) {
|
|
66
|
+
return agentId === 'claude' && (env.CLAUDE_CODE_OAUTH_TOKEN ?? '').trim().length > 0;
|
|
67
|
+
}
|
|
49
68
|
export function shouldCheckLoginBeforeLaunch(o) {
|
|
50
69
|
if (o.json || o.quiet || o.authCheckDisabled || o.rotated)
|
|
51
70
|
return false;
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Commands detector — mirrors versions.ts
|
|
2
|
+
* Commands detector — mirrors the command dispatch in versions.ts. Inspects the version home,
|
|
3
3
|
* returns command names. Honors the commands-as-skills marker for skills-only
|
|
4
|
-
* agents (
|
|
5
|
-
*
|
|
4
|
+
* agents (Kimi, Codex >= 0.117.0, …), treats the native file as authoritative
|
|
5
|
+
* for dual-write targets (the skill copy is deliberately absent on a name
|
|
6
|
+
* collision), and scans `{agentDir}/<commandsSubdir>/` for native-only targets.
|
|
6
7
|
*/
|
|
7
8
|
import * as fs from 'fs';
|
|
8
9
|
import * as path from 'path';
|
|
9
10
|
import { AGENTS, MANAGED_AGENT_IDS, agentConfigDirName } from '../../agents.js';
|
|
10
|
-
import { shouldInstallCommandAsSkill,
|
|
11
|
+
import { listCommandSkillsInVersion, shouldInstallCommandAsSkill, } from '../../command-skills.js';
|
|
11
12
|
import { lazyAgentMap } from '../writers/lazy-map.js';
|
|
12
13
|
function buildCommandsDetector(agent) {
|
|
13
14
|
return {
|
|
@@ -23,9 +24,17 @@ function buildCommandsDetector(agent) {
|
|
|
23
24
|
if (!fs.existsSync(commandsDir))
|
|
24
25
|
return [];
|
|
25
26
|
const ext = agentConfig.format === 'toml' ? '.toml' : '.md';
|
|
26
|
-
|
|
27
|
+
const nativeCommands = fs.readdirSync(commandsDir)
|
|
27
28
|
.filter(f => f.endsWith(ext))
|
|
28
29
|
.map(f => f.replace(new RegExp(`\\${ext}$`), ''));
|
|
30
|
+
// For a dual-write target the native file is the authoritative record that
|
|
31
|
+
// the command synced. The skill copy is derived, and
|
|
32
|
+
// installCommandSkillToVersion deliberately writes none when a real skill
|
|
33
|
+
// source already owns the name -- requiring both copies reported those
|
|
34
|
+
// commands missing forever and drove an `agents refresh` loop no sync could
|
|
35
|
+
// clear. This also matches `agents doctor`/`prune`, which read the
|
|
36
|
+
// unfiltered listCommandsInVersionHome.
|
|
37
|
+
return nativeCommands;
|
|
29
38
|
},
|
|
30
39
|
};
|
|
31
40
|
}
|
|
@@ -44,6 +44,8 @@ export interface SyncManifest {
|
|
|
44
44
|
v: typeof MANIFEST_VERSION;
|
|
45
45
|
syncedAt: string;
|
|
46
46
|
commands: Record<string, FileEntry>;
|
|
47
|
+
/** Command names the version writer emitted during the preceding full sync. */
|
|
48
|
+
writtenCommands?: string[];
|
|
47
49
|
skills: Record<string, DirEntry>;
|
|
48
50
|
hooks: Record<string, FileEntry>;
|
|
49
51
|
rules: RulesEntry;
|
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Commands writer.
|
|
3
3
|
*
|
|
4
|
-
* Two physical formats,
|
|
4
|
+
* Two physical formats, selected per-(agent, version) at write time. Most
|
|
5
|
+
* agents receive one format; dual-write registry targets receive both:
|
|
5
6
|
*
|
|
6
7
|
* - command-as-skill — fires when `shouldInstallCommandAsSkill(agent, version)`
|
|
7
8
|
* is true. Used for Codex >= 0.117.0 (commands capability ends, skills
|
|
8
9
|
* capability remains) and agents with skills but no native command-file dir
|
|
9
|
-
* such as
|
|
10
|
+
* such as Kimi. Cursor also receives this format in addition to its IDE
|
|
11
|
+
* command file. Writes `{agentDir}/skills/<name>/SKILL.md` with the
|
|
10
12
|
* `agents_command` marker; the agent picks it up as a slash-command equivalent.
|
|
11
13
|
*
|
|
12
14
|
* - native command file — `{agentDir}/<commandsSubdir>/<name>.md` (or .toml
|
|
@@ -23,7 +25,7 @@ import { supports } from '../../capabilities.js';
|
|
|
23
25
|
import { safeJoin } from '../../paths.js';
|
|
24
26
|
import { markdownToToml } from '../../convert.js';
|
|
25
27
|
import { commandAppliesTo, parseCommandMetadata } from '../../commands.js';
|
|
26
|
-
import { installCommandSkillToVersion, shouldInstallCommandAsSkill } from '../../command-skills.js';
|
|
28
|
+
import { installCommandSkillToVersion, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill, } from '../../command-skills.js';
|
|
27
29
|
import { installGooseCommandToVersion } from '../../goose-commands.js';
|
|
28
30
|
import { resolveCommandSource, trustedSkillRoots } from './sources.js';
|
|
29
31
|
import { lazyAgentMap } from './lazy-map.js';
|
|
@@ -35,6 +37,7 @@ function buildCommandsWriter(agent) {
|
|
|
35
37
|
const agentConfig = AGENTS[agent];
|
|
36
38
|
const agentDir = path.join(versionHome, agentConfigDirName(agent));
|
|
37
39
|
const commandsAsSkills = shouldInstallCommandAsSkill(agent, version);
|
|
40
|
+
const commandsAlsoAsSkills = shouldAlsoInstallCommandAsSkill(agent, version);
|
|
38
41
|
const supportsCommands = supports(agent, 'commands', version).ok;
|
|
39
42
|
// Version-gated agents (e.g. goose skills >= 1.25.0) are registered but
|
|
40
43
|
// may be called at a version too old for both paths — skip gracefully.
|
|
@@ -54,23 +57,25 @@ function buildCommandsWriter(agent) {
|
|
|
54
57
|
const metadata = parseCommandMetadata(srcFile);
|
|
55
58
|
if (!commandAppliesTo(agent, version, metadata).ok)
|
|
56
59
|
continue;
|
|
57
|
-
if (commandsAsSkills) {
|
|
60
|
+
if (commandsAsSkills || commandsAlsoAsSkills) {
|
|
58
61
|
const installed = installCommandSkillToVersion(agentDir, cmd, srcFile, skillRoots);
|
|
62
|
+
// installed.skipped means a real skill source already owns this name,
|
|
63
|
+
// which is a deliberate no-op, not a failure — the native file is still written.
|
|
59
64
|
if (!installed.success)
|
|
60
65
|
continue;
|
|
61
66
|
}
|
|
62
|
-
|
|
67
|
+
if (supportsCommands && agent === 'goose') {
|
|
63
68
|
// Goose: recipe YAML + config.yaml slash_commands entry, not a file copy.
|
|
64
69
|
const installed = installGooseCommandToVersion(versionHome, cmd, srcFile);
|
|
65
70
|
if (!installed.success)
|
|
66
71
|
continue;
|
|
67
72
|
}
|
|
68
|
-
else if (agentConfig.format === 'toml') {
|
|
73
|
+
else if (supportsCommands && agentConfig.format === 'toml') {
|
|
69
74
|
const content = fs.readFileSync(srcFile, 'utf-8');
|
|
70
75
|
const tomlContent = markdownToToml(cmd, content);
|
|
71
76
|
fs.writeFileSync(safeJoin(commandsTarget, `${cmd}.toml`), tomlContent);
|
|
72
77
|
}
|
|
73
|
-
else {
|
|
78
|
+
else if (supportsCommands) {
|
|
74
79
|
fs.copyFileSync(srcFile, safeJoin(commandsTarget, `${cmd}.md`));
|
|
75
80
|
}
|
|
76
81
|
synced.push(cmd);
|
|
@@ -84,6 +89,7 @@ function buildCommandsWriter(agent) {
|
|
|
84
89
|
// Registration covers two cases:
|
|
85
90
|
// - native commands (claude, codex < 0.117.0, grok, etc.) — `commands` cap
|
|
86
91
|
// - commands-as-skills (kimi, codex >= 0.117.0)
|
|
92
|
+
// - dual-write commands plus command-skills (cursor)
|
|
87
93
|
//
|
|
88
94
|
// Agents that have skills but use a NATIVE non-file slash-command system
|
|
89
95
|
// (openclaw → Gateway-based commands) are NOT registered. They declare
|
package/dist/lib/state.d.ts
CHANGED
|
@@ -181,6 +181,8 @@ export declare function getTeamsRegistryPath(): string;
|
|
|
181
181
|
export declare function getDevicesRegistryPath(): string;
|
|
182
182
|
/** Path to the device ignore-list — tailscale node names the user dismissed, so auto-discovery never re-suggests them. Per-machine, same dir as the registry. */
|
|
183
183
|
export declare function getDevicesIgnoredPath(): string;
|
|
184
|
+
/** Path to the device auto-launch preference file — which registered devices are eligible/preferred for Factory's auto-host selection. Per-machine, same dir as the registry. */
|
|
185
|
+
export declare function getDevicesAutoLaunchPath(): string;
|
|
184
186
|
/** Dir of "pending device" sentinels (~/.agents/.cache/state/devices-pending/) — one empty-ish file per newly-discovered, not-yet-approved tailnet node. Written by the daemon probe, read by the menu-bar helper (mirrors the attention sentinel dir). */
|
|
185
187
|
export declare function getDevicesPendingDir(): string;
|
|
186
188
|
/** Path to cloud dispatch cache (~/.agents/.cache/cloud/). */
|
package/dist/lib/state.js
CHANGED
|
@@ -402,6 +402,8 @@ function getDevicesDir() {
|
|
|
402
402
|
export function getDevicesRegistryPath() { return path.join(getDevicesDir(), 'registry.json'); }
|
|
403
403
|
/** Path to the device ignore-list — tailscale node names the user dismissed, so auto-discovery never re-suggests them. Per-machine, same dir as the registry. */
|
|
404
404
|
export function getDevicesIgnoredPath() { return path.join(getDevicesDir(), 'ignored.json'); }
|
|
405
|
+
/** Path to the device auto-launch preference file — which registered devices are eligible/preferred for Factory's auto-host selection. Per-machine, same dir as the registry. */
|
|
406
|
+
export function getDevicesAutoLaunchPath() { return path.join(getDevicesDir(), 'auto-launch.json'); }
|
|
405
407
|
/** Dir of "pending device" sentinels (~/.agents/.cache/state/devices-pending/) — one empty-ish file per newly-discovered, not-yet-approved tailnet node. Written by the daemon probe, read by the menu-bar helper (mirrors the attention sentinel dir). */
|
|
406
408
|
export function getDevicesPendingDir() { return path.join(RUNTIME_STATE_DIR, 'devices-pending'); }
|
|
407
409
|
/** Path to cloud dispatch cache (~/.agents/.cache/cloud/). */
|
package/dist/lib/usage.d.ts
CHANGED
|
@@ -66,7 +66,35 @@ interface ClaudeOauthCredentials {
|
|
|
66
66
|
rateLimitTier?: string | null;
|
|
67
67
|
organizationUuid?: string | null;
|
|
68
68
|
}
|
|
69
|
-
/**
|
|
69
|
+
/** The single registry of agent usage sources and their transport. */
|
|
70
|
+
declare const USAGE_SOURCES: {
|
|
71
|
+
readonly claude: {
|
|
72
|
+
readonly fetch: typeof getClaudeUsageInfo;
|
|
73
|
+
readonly network: true;
|
|
74
|
+
};
|
|
75
|
+
readonly codex: {
|
|
76
|
+
readonly fetch: typeof getCodexUsageInfo;
|
|
77
|
+
readonly network: false;
|
|
78
|
+
};
|
|
79
|
+
readonly kimi: {
|
|
80
|
+
readonly fetch: typeof getKimiUsageInfo;
|
|
81
|
+
readonly network: true;
|
|
82
|
+
};
|
|
83
|
+
readonly droid: {
|
|
84
|
+
readonly fetch: typeof getDroidUsageInfo;
|
|
85
|
+
readonly network: true;
|
|
86
|
+
};
|
|
87
|
+
readonly grok: {
|
|
88
|
+
readonly fetch: typeof getGrokUsageInfo;
|
|
89
|
+
readonly network: false;
|
|
90
|
+
};
|
|
91
|
+
readonly cursor: {
|
|
92
|
+
readonly fetch: typeof getCursorUsageInfo;
|
|
93
|
+
readonly network: true;
|
|
94
|
+
};
|
|
95
|
+
};
|
|
96
|
+
export declare const USAGE_SOURCE_AGENT_IDS: (keyof typeof USAGE_SOURCES)[];
|
|
97
|
+
/** Fetch usage info for a given agent through the canonical source registry. */
|
|
70
98
|
export declare function getUsageInfo(agentId: AgentId, options?: UsageOptions): Promise<UsageInfo>;
|
|
71
99
|
/** Derive a stable lookup key from account info for usage deduplication. */
|
|
72
100
|
export declare function getUsageLookupKey(info?: Pick<AccountInfo, 'usageKey' | 'accountKey'> | null): string | null;
|
|
@@ -151,6 +179,8 @@ export declare function deriveUsageStatusFromSnapshot(snapshot: UsageSnapshot |
|
|
|
151
179
|
export declare function formatUsageStatusBadge(usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null | undefined): string;
|
|
152
180
|
/** Format a multi-line usage section for detailed agent views. */
|
|
153
181
|
export declare function formatUsageSection(usage: UsageInfo): string[];
|
|
182
|
+
/** Fetch Codex usage by scanning the most recent session files for rate-limit events. */
|
|
183
|
+
declare function getCodexUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
|
|
154
184
|
/**
|
|
155
185
|
* The access token to use for a READ-ONLY Claude usage fetch, or null when the
|
|
156
186
|
* stored token is within the refresh leeway.
|
|
@@ -167,6 +197,8 @@ export declare function formatUsageSection(usage: UsageInfo): string[];
|
|
|
167
197
|
* Pure — unit-tested.
|
|
168
198
|
*/
|
|
169
199
|
export declare function claudeUsageAccessTokenNoRefresh(oauth: Pick<ClaudeOauthCredentials, 'accessToken' | 'expiresAt'>): string | null;
|
|
200
|
+
/** Fetch Claude usage via the Anthropic OAuth usage API. */
|
|
201
|
+
declare function getClaudeUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
|
|
170
202
|
/** Raw quota bucket from the Kimi /usages response (numbers arrive as strings). */
|
|
171
203
|
interface KimiUsageQuota {
|
|
172
204
|
limit?: string | number | null;
|
|
@@ -192,6 +224,18 @@ export interface KimiUsagesResponse {
|
|
|
192
224
|
} | null> | null;
|
|
193
225
|
subType?: string | null;
|
|
194
226
|
}
|
|
227
|
+
/**
|
|
228
|
+
* Fetch Kimi usage via the Kimi Code /usages API. Kimi's JWT has no email
|
|
229
|
+
* claim, so the account row can't show an address — but /usages returns quota
|
|
230
|
+
* windows and the membership tier, which is what we render.
|
|
231
|
+
*
|
|
232
|
+
* Deliberately NO token refresh: `agents view` is a read/inspect command and
|
|
233
|
+
* must not rotate the user's Kimi OAuth credential (rewriting the file,
|
|
234
|
+
* invalidating the old refresh token, racing a concurrently-running kimi CLI).
|
|
235
|
+
* The kimi CLI refreshes on its own launch; if the stored token is expired we
|
|
236
|
+
* skip the live fetch and let the SWR cache serve the last-seen snapshot.
|
|
237
|
+
*/
|
|
238
|
+
declare function getKimiUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
|
|
195
239
|
/** Normalize the Kimi /usages payload into the common UsageWindow shape. */
|
|
196
240
|
export declare function normalizeKimiWindows(data: KimiUsagesResponse): UsageWindow[];
|
|
197
241
|
/** Derive a display plan label from Kimi's membership tier or subscription type. */
|
|
@@ -212,6 +256,22 @@ export interface DroidBillingLimitsResponse {
|
|
|
212
256
|
} | null;
|
|
213
257
|
} | null;
|
|
214
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Fetch Droid usage via Factory's billing limits API — the same endpoint the
|
|
261
|
+
* droid CLI polls for its token-limit banner. The WorkOS access token comes
|
|
262
|
+
* from the locally decrypted ~/.factory/auth.v2.file (the same credential
|
|
263
|
+
* account identity in agents.ts reads).
|
|
264
|
+
*
|
|
265
|
+
* Deliberately NO token refresh, for a sharper reason than Kimi's: WorkOS
|
|
266
|
+
* refresh tokens are single-use and rotate on every exchange, so refreshing
|
|
267
|
+
* here would race a concurrently running droid session and can permanently
|
|
268
|
+
* invalidate the user's login chain. Droid refreshes its own credential when
|
|
269
|
+
* it runs; if the stored token is expired we skip the live fetch and let the
|
|
270
|
+
* SWR cache serve the last-seen snapshot. This same single-use-rotation property
|
|
271
|
+
* is why `agents apply` refuses to propagate droid credentials across machines
|
|
272
|
+
* (see `isCredentialSafeToPropagate` in `../fleet/auth-sync.ts`).
|
|
273
|
+
*/
|
|
274
|
+
declare function getDroidUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
|
|
215
275
|
/**
|
|
216
276
|
* Live auth probes — the same authenticated GET the usage fetchers above do,
|
|
217
277
|
* but surfacing the raw HTTP status instead of swallowing 401/expired to null.
|
|
@@ -296,6 +356,8 @@ export declare function writeClaudeUsageCache(usageKey: string, snapshot: UsageS
|
|
|
296
356
|
* Returns true only when a valid access token can be obtained.
|
|
297
357
|
*/
|
|
298
358
|
export declare function isClaudeAuthValid(home?: string): Promise<boolean>;
|
|
359
|
+
/** Parse the latest billing info from Grok's unified log. */
|
|
360
|
+
declare function getGrokUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
|
|
299
361
|
/** Per-model bucket in Cursor's /api/usage response. */
|
|
300
362
|
interface CursorUsageModel {
|
|
301
363
|
numRequests?: number | null;
|
|
@@ -319,4 +381,13 @@ export interface CursorUsageResponse {
|
|
|
319
381
|
* bar to draw here and return no windows rather than a misleading empty gauge.
|
|
320
382
|
*/
|
|
321
383
|
export declare function normalizeCursorUsage(data: CursorUsageResponse): UsageWindow[];
|
|
384
|
+
/**
|
|
385
|
+
* Fetch Cursor usage from the dashboard usage endpoint. Cursor authenticates the
|
|
386
|
+
* request with a `WorkosCursorSessionToken` cookie of the form
|
|
387
|
+
* `<oauth-subject>::<access-token>` (the same pair the web dashboard sends), not a
|
|
388
|
+
* bearer header. Free/legacy plans return a monthly request bar; usage-based plans
|
|
389
|
+
* have no request cap, so they return a live-but-window-less snapshot (the account
|
|
390
|
+
* row still renders, without a misleading empty gauge).
|
|
391
|
+
*/
|
|
392
|
+
declare function getCursorUsageInfo(options?: UsageOptions): Promise<UsageInfo>;
|
|
322
393
|
export {};
|
package/dist/lib/usage.js
CHANGED
|
@@ -18,7 +18,7 @@ import chalk from 'chalk';
|
|
|
18
18
|
import { decodeJwtPayload, decryptDroidAuthPayload } from './agents.js';
|
|
19
19
|
import { walkForFiles } from './fs-walk.js';
|
|
20
20
|
import { getKeychainToken, setKeychainToken, deleteKeychainToken, isKeychainBackendOverridden, } from './secrets/index.js';
|
|
21
|
-
import {
|
|
21
|
+
import { resolveClaudeSetupToken } from './claude-account-token.js';
|
|
22
22
|
import { getCacheDir } from './state.js';
|
|
23
23
|
const execFileAsync = promisify(execFile);
|
|
24
24
|
const CLAUDE_USAGE_URL = 'https://api.anthropic.com/api/oauth/usage';
|
|
@@ -59,24 +59,23 @@ const COMPACT_BAR_LEN = 5;
|
|
|
59
59
|
const USAGE_BAR_LEN = 10;
|
|
60
60
|
const FULL = '\u2588';
|
|
61
61
|
const EMPTY = '\u2591';
|
|
62
|
-
/**
|
|
62
|
+
/** The single registry of agent usage sources and their transport. */
|
|
63
|
+
const USAGE_SOURCES = {
|
|
64
|
+
claude: { fetch: getClaudeUsageInfo, network: true },
|
|
65
|
+
codex: { fetch: getCodexUsageInfo, network: false },
|
|
66
|
+
kimi: { fetch: getKimiUsageInfo, network: true },
|
|
67
|
+
droid: { fetch: getDroidUsageInfo, network: true },
|
|
68
|
+
grok: { fetch: getGrokUsageInfo, network: false },
|
|
69
|
+
cursor: { fetch: getCursorUsageInfo, network: true },
|
|
70
|
+
};
|
|
71
|
+
export const USAGE_SOURCE_AGENT_IDS = Object.keys(USAGE_SOURCES);
|
|
72
|
+
function getUsageSource(agentId) {
|
|
73
|
+
return USAGE_SOURCES[agentId];
|
|
74
|
+
}
|
|
75
|
+
/** Fetch usage info for a given agent through the canonical source registry. */
|
|
63
76
|
export async function getUsageInfo(agentId, options) {
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
return getClaudeUsageInfo(options);
|
|
67
|
-
case 'codex':
|
|
68
|
-
return getCodexUsageInfo(options);
|
|
69
|
-
case 'kimi':
|
|
70
|
-
return getKimiUsageInfo(options);
|
|
71
|
-
case 'droid':
|
|
72
|
-
return getDroidUsageInfo(options);
|
|
73
|
-
case 'grok':
|
|
74
|
-
return getGrokUsageInfo(options);
|
|
75
|
-
case 'cursor':
|
|
76
|
-
return getCursorUsageInfo(options);
|
|
77
|
-
default:
|
|
78
|
-
return { snapshot: null, error: null };
|
|
79
|
-
}
|
|
77
|
+
const source = getUsageSource(agentId);
|
|
78
|
+
return source ? source.fetch(options) : { snapshot: null, error: null };
|
|
80
79
|
}
|
|
81
80
|
/** Derive a stable lookup key from account info for usage deduplication. */
|
|
82
81
|
export function getUsageLookupKey(info) {
|
|
@@ -117,12 +116,7 @@ export function buildCanonicalUsageContext(inputs) {
|
|
|
117
116
|
* versus simply not applicable (Antigravity, OpenCode).
|
|
118
117
|
*/
|
|
119
118
|
export function agentReportsUsage(agentId) {
|
|
120
|
-
return (agentId
|
|
121
|
-
agentId === 'codex' ||
|
|
122
|
-
agentId === 'kimi' ||
|
|
123
|
-
agentId === 'droid' ||
|
|
124
|
-
agentId === 'grok' ||
|
|
125
|
-
agentId === 'cursor');
|
|
119
|
+
return getUsageSource(agentId) !== undefined;
|
|
126
120
|
}
|
|
127
121
|
/** Fetch usage info for all unique accounts in parallel, keyed by usage key. */
|
|
128
122
|
export async function getUsageInfoByIdentity(inputs, opts) {
|
|
@@ -160,14 +154,14 @@ const inFlightRefreshes = new Map();
|
|
|
160
154
|
export async function getUsageInfoForIdentity(input, opts) {
|
|
161
155
|
const usageKey = getUsageLookupKey(input.info);
|
|
162
156
|
const forceRefresh = opts?.forceRefresh === true;
|
|
163
|
-
// Agents whose usage
|
|
157
|
+
// Agents whose registered usage source makes a live network call go
|
|
164
158
|
// through the stale-while-revalidate cache below so `agents run`/`agents view`
|
|
165
159
|
// stay off the network on the hot path. Everything else (Codex reads local
|
|
166
160
|
// session logs) takes the legacy blocking path. The on-disk cache is shared and
|
|
167
161
|
// keyed by usageKey, which is namespaced per agent (`claude:org=…`,
|
|
168
|
-
// `kimi:user=…`, `droid:org=…`), so one cache file holds every
|
|
169
|
-
// collision.
|
|
170
|
-
const usesNetworkUsage = input.agentId ===
|
|
162
|
+
// `kimi:user=…`, `droid:org=…`, `cursor:user=…`), so one cache file holds every
|
|
163
|
+
// account without collision.
|
|
164
|
+
const usesNetworkUsage = getUsageSource(input.agentId)?.network === true;
|
|
171
165
|
if (!usesNetworkUsage || !usageKey) {
|
|
172
166
|
return getUsageInfo(input.agentId, {
|
|
173
167
|
home: input.home,
|
|
@@ -999,65 +993,6 @@ function deleteCachedClaudeOauth(service) {
|
|
|
999
993
|
/* best-effort — cache is an optimization */
|
|
1000
994
|
}
|
|
1001
995
|
}
|
|
1002
|
-
/**
|
|
1003
|
-
* Reserved FILE-BASED secrets bundle holding long-lived, non-rotating Claude
|
|
1004
|
-
* setup-tokens. Usage/probe reads authenticate with these instead of Claude
|
|
1005
|
-
* Code's ACL-bound login item, so they never pop Touch ID. Keyed strictly
|
|
1006
|
-
* per-account (`CLAUDE_CODE_OAUTH_TOKEN_<slug>` from the account email) — never a
|
|
1007
|
-
* bare key, so one account's token can't be misapplied to another in a
|
|
1008
|
-
* multi-account fleet.
|
|
1009
|
-
*/
|
|
1010
|
-
const AUTH_BUNDLE = 'auth';
|
|
1011
|
-
/** The per-account key an email maps to inside the `auth` bundle. */
|
|
1012
|
-
function claudeAccountTokenKey(account) {
|
|
1013
|
-
const slug = account
|
|
1014
|
-
.trim()
|
|
1015
|
-
.toUpperCase()
|
|
1016
|
-
.replace(/@/g, '_AT_')
|
|
1017
|
-
.replace(/\./g, '_DOT_')
|
|
1018
|
-
.replace(/[^A-Z0-9_]/g, '_');
|
|
1019
|
-
return `CLAUDE_CODE_OAUTH_TOKEN_${slug}`;
|
|
1020
|
-
}
|
|
1021
|
-
/** Signed-in account email for a version home, from `.claude.json` (no keychain). */
|
|
1022
|
-
function readClaudeAccountEmail(home) {
|
|
1023
|
-
const base = home ?? os.homedir();
|
|
1024
|
-
for (const p of [path.join(base, '.claude', '.claude.json'), path.join(base, '.claude.json')]) {
|
|
1025
|
-
try {
|
|
1026
|
-
const email = JSON.parse(fs.readFileSync(p, 'utf-8')).oauthAccount?.emailAddress;
|
|
1027
|
-
if (typeof email === 'string' && email.trim().length > 0)
|
|
1028
|
-
return email.trim();
|
|
1029
|
-
}
|
|
1030
|
-
catch {
|
|
1031
|
-
// Missing/unreadable at this location — try the next.
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
return null;
|
|
1035
|
-
}
|
|
1036
|
-
/**
|
|
1037
|
-
* Resolve a long-lived `claude setup-token` for the account signed into `home`
|
|
1038
|
-
* from the reserved FILE-BASED `auth` bundle. Returns the token or null. Reads
|
|
1039
|
-
* ONLY when the bundle is file-backed (never keychain), so this path itself can
|
|
1040
|
-
* never trigger a Touch ID prompt — that is the entire point: usage/probe reads
|
|
1041
|
-
* authenticate with the shareable setup-token, not the ACL-bound login item.
|
|
1042
|
-
*/
|
|
1043
|
-
function resolveClaudeSetupToken(home) {
|
|
1044
|
-
try {
|
|
1045
|
-
// Require a known account (email) up front: without it we cannot key a
|
|
1046
|
-
// per-account token, and we must NOT fall back to a bare shared key that
|
|
1047
|
-
// would misapply one account's setup-token to another.
|
|
1048
|
-
const email = readClaudeAccountEmail(home);
|
|
1049
|
-
if (!email)
|
|
1050
|
-
return null;
|
|
1051
|
-
if (!bundleExists(AUTH_BUNDLE) || bundleBackend(AUTH_BUNDLE) !== 'file')
|
|
1052
|
-
return null;
|
|
1053
|
-
const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
|
|
1054
|
-
const v = (env[claudeAccountTokenKey(email)] ?? '').trim();
|
|
1055
|
-
return v.length > 0 ? v : null;
|
|
1056
|
-
}
|
|
1057
|
-
catch {
|
|
1058
|
-
return null;
|
|
1059
|
-
}
|
|
1060
|
-
}
|
|
1061
996
|
/**
|
|
1062
997
|
* Load a version home's Claude OAuth credential from the two stores Claude Code
|
|
1063
998
|
* uses, tried in order:
|