@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
package/dist/index.js
CHANGED
|
@@ -98,7 +98,7 @@ import { COMMAND_LOADERS, LAZY_COMMAND_NAMES, loadView, loadInspect, loadFeedbac
|
|
|
98
98
|
import { applyGlobalHelpConventions } from './lib/help.js';
|
|
99
99
|
import { renderWhatsNew } from './lib/whats-new.js';
|
|
100
100
|
import { getCliLaunch } from './lib/cli-entry.js';
|
|
101
|
-
import { emit, redactArgs } from './lib/events.js';
|
|
101
|
+
import { emit, emitFriction, redactArgs } from './lib/events.js';
|
|
102
102
|
// Transparent shim delegate: the generated Windows `.cmd` shims invoke
|
|
103
103
|
// `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
|
|
104
104
|
// parses anything, so the agent's own flags (`--help`, `--version`, etc.) pass
|
|
@@ -691,6 +691,32 @@ function registerResourcesTombstoneCommand(p) {
|
|
|
691
691
|
await program.parseAsync(['node', 'agents', ...args]);
|
|
692
692
|
});
|
|
693
693
|
}
|
|
694
|
+
/**
|
|
695
|
+
* Hidden `agents _internal <sub>` namespace for machine-to-machine calls that
|
|
696
|
+
* are not user-facing. The first subcommand is `friction`, used by shell guard
|
|
697
|
+
* hooks (git-guard, rm-guard, …) to self-report a block into the event log
|
|
698
|
+
* before they exit 2 — they run before any `agents` process exists, so they
|
|
699
|
+
* cannot emit in-process.
|
|
700
|
+
*/
|
|
701
|
+
function registerInternalCommand(p) {
|
|
702
|
+
const internal = p.command('_internal', { hidden: true });
|
|
703
|
+
internal
|
|
704
|
+
.command('friction')
|
|
705
|
+
.option('--surface <surface>', 'Subsystem that hit the failure (e.g. guard, teams)')
|
|
706
|
+
.option('--id <failureId>', 'Stable failure slug (e.g. git.reset-hard)')
|
|
707
|
+
.option('--error <message>', 'Human-readable failure reason')
|
|
708
|
+
.option('--command <command>', 'The command that was blocked')
|
|
709
|
+
.action((opts) => {
|
|
710
|
+
if (!opts.surface || !opts.id) {
|
|
711
|
+
process.exit(0); // fail-open: never break the caller
|
|
712
|
+
}
|
|
713
|
+
emitFriction(opts.surface, opts.id, {
|
|
714
|
+
...(opts.error ? { error: opts.error } : {}),
|
|
715
|
+
...(opts.command ? { command: opts.command } : {}),
|
|
716
|
+
});
|
|
717
|
+
process.exit(0);
|
|
718
|
+
});
|
|
719
|
+
}
|
|
694
720
|
/** Self-upgrade command (`agents upgrade [version]`). */
|
|
695
721
|
function registerUpgradeCommand(p) {
|
|
696
722
|
p.command('upgrade')
|
|
@@ -784,6 +810,9 @@ async function registerEagerForRequest(name) {
|
|
|
784
810
|
registerResourcesTombstoneCommand(program);
|
|
785
811
|
await reg(loadView);
|
|
786
812
|
return true;
|
|
813
|
+
case '_internal':
|
|
814
|
+
registerInternalCommand(program);
|
|
815
|
+
return true;
|
|
787
816
|
case 'upgrade':
|
|
788
817
|
registerUpgradeCommand(program);
|
|
789
818
|
return true;
|
|
@@ -878,6 +907,7 @@ async function registerAllEagerCommands() {
|
|
|
878
907
|
await reg(loadSsh);
|
|
879
908
|
registerJobsCronAliasCommand(program, 'jobs');
|
|
880
909
|
registerJobsCronAliasCommand(program, 'cron');
|
|
910
|
+
registerInternalCommand(program);
|
|
881
911
|
registerUpgradeCommand(program);
|
|
882
912
|
await reg(loadPull);
|
|
883
913
|
await reg(loadPush);
|
package/dist/lib/agents.d.ts
CHANGED
|
@@ -165,7 +165,7 @@ export declare function formatClaudeOrgLabel(orgType: string | null | undefined)
|
|
|
165
165
|
*/
|
|
166
166
|
export declare function accountOrgBadge(info?: Pick<AccountInfo, 'organizationType' | 'organizationName'> | null): string | null;
|
|
167
167
|
/** Agents whose local credential formats expose enough state for account selection. */
|
|
168
|
-
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "gemini", "grok", "antigravity", "kimi", "droid", "opencode"];
|
|
168
|
+
export declare const ACCOUNT_INSPECTION_AGENT_IDS: readonly ["claude", "codex", "gemini", "cursor", "grok", "antigravity", "kimi", "droid", "opencode"];
|
|
169
169
|
/** Whether agents-cli can determine this agent's per-version sign-in state. */
|
|
170
170
|
export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
171
171
|
/**
|
|
@@ -176,6 +176,36 @@ export declare function supportsAccountInspection(agentId: AgentId): boolean;
|
|
|
176
176
|
export declare function accountDisplayLabel(info?: Pick<AccountInfo, 'email' | 'accountId' | 'signedIn' | 'organizationType' | 'organizationName'> | null): string;
|
|
177
177
|
/** Return the email address associated with the agent's auth config, or null. */
|
|
178
178
|
export declare function getAccountEmail(agentId: AgentId, home?: string): Promise<string | null>;
|
|
179
|
+
/** Where an agent's credential file lives, split into the per-version copy and
|
|
180
|
+
* the active/global copy under the real HOME. */
|
|
181
|
+
export interface CredentialPresence {
|
|
182
|
+
/** The credential file exists inside the passed version home. */
|
|
183
|
+
perVersion: boolean;
|
|
184
|
+
/** The credential file exists under the active/global HOME (the one the login
|
|
185
|
+
* symlink actually targets), independent of the version home. */
|
|
186
|
+
active: boolean;
|
|
187
|
+
/** Whether agents-cli knows WHERE this agent's credential lives at all — i.e.
|
|
188
|
+
* the agent has an entry in {@link CREDENTIAL_FILE_SEGMENTS}. When false both
|
|
189
|
+
* probes are trivially false because there is nothing to look for, so absence
|
|
190
|
+
* is NOT evidence of a logout and no caller may treat it as provable.
|
|
191
|
+
*
|
|
192
|
+
* This is deliberately separate from `supportsAccountInspection`: the two
|
|
193
|
+
* registries move independently, and an agent has already been added to the
|
|
194
|
+
* inspection set without a credential path (cursor), which without this flag
|
|
195
|
+
* produced a false "logged out" critical for every installed version. */
|
|
196
|
+
knownLocation: boolean;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* File-presence probe for an agent's credential, split by location: whether it
|
|
200
|
+
* exists in a SPECIFIC version home (`perVersion`) and whether it exists under
|
|
201
|
+
* the active/global HOME (`active`). A logged-out claim is only *provable* when
|
|
202
|
+
* BOTH are absent — a version that merely lacks its own copy but shares the
|
|
203
|
+
* global login is signed in, not logged out. Pure file existence; no decrypt,
|
|
204
|
+
* no network, no keychain prompt. Agents with no inspectable identity return
|
|
205
|
+
* `{ perVersion: false, active: false }` and must NEVER yield a provable-logout
|
|
206
|
+
* claim (the caller gates on {@link supportsAccountInspection}).
|
|
207
|
+
*/
|
|
208
|
+
export declare function credentialPresence(agentId: AgentId, versionHome: string): CredentialPresence;
|
|
179
209
|
/** Decrypted contents of Droid's auth.v2.file (subset we consume). */
|
|
180
210
|
export interface DroidAuthPayload {
|
|
181
211
|
access_token?: string;
|
package/dist/lib/agents.js
CHANGED
|
@@ -1014,6 +1014,7 @@ export const ACCOUNT_INSPECTION_AGENT_IDS = [
|
|
|
1014
1014
|
'claude',
|
|
1015
1015
|
'codex',
|
|
1016
1016
|
'gemini',
|
|
1017
|
+
'cursor',
|
|
1017
1018
|
'grok',
|
|
1018
1019
|
'antigravity',
|
|
1019
1020
|
'kimi',
|
|
@@ -1077,6 +1078,60 @@ function resolveAccountCredentialPath(base, ...segments) {
|
|
|
1077
1078
|
}
|
|
1078
1079
|
return null;
|
|
1079
1080
|
}
|
|
1081
|
+
/**
|
|
1082
|
+
* The on-disk credential file(s) each account-inspectable agent authenticates
|
|
1083
|
+
* from, expressed as path segments under a home. Mirrors the exact files
|
|
1084
|
+
* {@link getAccountInfo} reads, so a presence check here matches what a real
|
|
1085
|
+
* launch would find. Each entry is a list of alternatives — the FIRST that
|
|
1086
|
+
* exists counts as present (claude writes either `.claude/.claude.json` under
|
|
1087
|
+
* the shimmed config dir or a home-level `.claude.json`). Agents whose login is
|
|
1088
|
+
* stored only in the OS keychain on some platforms (antigravity, and claude's
|
|
1089
|
+
* token) still expose a credential FILE — the presence of that file is the
|
|
1090
|
+
* signal we key off; its absence on BOTH the per-version home and the active
|
|
1091
|
+
* home is what makes a logged-out claim provable.
|
|
1092
|
+
*/
|
|
1093
|
+
const CREDENTIAL_FILE_SEGMENTS = {
|
|
1094
|
+
claude: [['.claude', '.claude.json'], ['.claude.json']],
|
|
1095
|
+
codex: [['.codex', 'auth.json']],
|
|
1096
|
+
gemini: [['.gemini', 'google_accounts.json']],
|
|
1097
|
+
grok: [['.grok', 'auth.json']],
|
|
1098
|
+
kimi: [['.kimi-code', 'credentials', 'kimi-code.json']],
|
|
1099
|
+
droid: [['.factory', 'auth.v2.file']],
|
|
1100
|
+
antigravity: [['.gemini', 'antigravity-cli', 'antigravity-oauth-token']],
|
|
1101
|
+
opencode: [['.local', 'share', 'opencode', 'auth.json']],
|
|
1102
|
+
};
|
|
1103
|
+
/** Whether an agent's credential file exists under a given home. */
|
|
1104
|
+
function credentialFileExistsUnder(agentId, home) {
|
|
1105
|
+
const alternatives = CREDENTIAL_FILE_SEGMENTS[agentId];
|
|
1106
|
+
if (!alternatives)
|
|
1107
|
+
return false;
|
|
1108
|
+
for (const segments of alternatives) {
|
|
1109
|
+
const p = path.join(home, ...segments);
|
|
1110
|
+
try {
|
|
1111
|
+
if (fs.existsSync(p))
|
|
1112
|
+
return true;
|
|
1113
|
+
}
|
|
1114
|
+
catch { /* unreadable */ }
|
|
1115
|
+
}
|
|
1116
|
+
return false;
|
|
1117
|
+
}
|
|
1118
|
+
/**
|
|
1119
|
+
* File-presence probe for an agent's credential, split by location: whether it
|
|
1120
|
+
* exists in a SPECIFIC version home (`perVersion`) and whether it exists under
|
|
1121
|
+
* the active/global HOME (`active`). A logged-out claim is only *provable* when
|
|
1122
|
+
* BOTH are absent — a version that merely lacks its own copy but shares the
|
|
1123
|
+
* global login is signed in, not logged out. Pure file existence; no decrypt,
|
|
1124
|
+
* no network, no keychain prompt. Agents with no inspectable identity return
|
|
1125
|
+
* `{ perVersion: false, active: false }` and must NEVER yield a provable-logout
|
|
1126
|
+
* claim (the caller gates on {@link supportsAccountInspection}).
|
|
1127
|
+
*/
|
|
1128
|
+
export function credentialPresence(agentId, versionHome) {
|
|
1129
|
+
const realHome = process.env.AGENTS_REAL_HOME || os.homedir();
|
|
1130
|
+
const perVersion = credentialFileExistsUnder(agentId, versionHome);
|
|
1131
|
+
const active = credentialFileExistsUnder(agentId, realHome);
|
|
1132
|
+
const knownLocation = (CREDENTIAL_FILE_SEGMENTS[agentId]?.length ?? 0) > 0;
|
|
1133
|
+
return { perVersion, active, knownLocation };
|
|
1134
|
+
}
|
|
1080
1135
|
/**
|
|
1081
1136
|
* Factory Droid stores its OAuth credential encrypted at ~/.factory/auth.v2.file
|
|
1082
1137
|
* (AES-256-GCM, format `ivB64:tagB64:ctB64`) with the 32-byte key base64-stored
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/** The per-account key an email maps to inside the `auth` bundle. */
|
|
2
|
+
export declare function claudeAccountTokenKey(account: string): string;
|
|
3
|
+
/** Signed-in account email for a version home, from `.claude.json` (no keychain). */
|
|
4
|
+
export declare function readClaudeAccountEmail(home?: string): string | null;
|
|
5
|
+
/**
|
|
6
|
+
* Resolve a long-lived `claude setup-token` for the account signed into `home`
|
|
7
|
+
* from the reserved FILE-BASED `auth` bundle. Returns the token or null. Reads
|
|
8
|
+
* ONLY when the bundle is file-backed (never keychain), so this path itself can
|
|
9
|
+
* never trigger a Touch ID prompt — that is the entire point: usage/probe reads
|
|
10
|
+
* authenticate with the shareable setup-token, not the ACL-bound login item.
|
|
11
|
+
*/
|
|
12
|
+
export declare function resolveClaudeSetupToken(home?: string): string | null;
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import * as fs from 'fs';
|
|
2
|
+
import * as os from 'os';
|
|
3
|
+
import * as path from 'path';
|
|
4
|
+
import { bundleBackend, bundleExists, readAndResolveBundleEnv } from './secrets/bundles.js';
|
|
5
|
+
/**
|
|
6
|
+
* Reserved FILE-BASED secrets bundle holding long-lived, non-rotating Claude
|
|
7
|
+
* setup-tokens. Usage/probe reads authenticate with these instead of Claude
|
|
8
|
+
* Code's ACL-bound login item, so they never pop Touch ID. Keyed strictly
|
|
9
|
+
* per-account (`CLAUDE_CODE_OAUTH_TOKEN_<slug>` from the account email) — never a
|
|
10
|
+
* bare key, so one account's token can't be misapplied to another in a
|
|
11
|
+
* multi-account fleet.
|
|
12
|
+
*/
|
|
13
|
+
const AUTH_BUNDLE = 'auth';
|
|
14
|
+
/** The per-account key an email maps to inside the `auth` bundle. */
|
|
15
|
+
export function claudeAccountTokenKey(account) {
|
|
16
|
+
const slug = account
|
|
17
|
+
.trim()
|
|
18
|
+
.toUpperCase()
|
|
19
|
+
.replace(/@/g, '_AT_')
|
|
20
|
+
.replace(/\./g, '_DOT_')
|
|
21
|
+
.replace(/[^A-Z0-9_]/g, '_');
|
|
22
|
+
return `CLAUDE_CODE_OAUTH_TOKEN_${slug}`;
|
|
23
|
+
}
|
|
24
|
+
/** Signed-in account email for a version home, from `.claude.json` (no keychain). */
|
|
25
|
+
export function readClaudeAccountEmail(home) {
|
|
26
|
+
const base = home ?? os.homedir();
|
|
27
|
+
for (const p of [path.join(base, '.claude', '.claude.json'), path.join(base, '.claude.json')]) {
|
|
28
|
+
try {
|
|
29
|
+
const email = JSON.parse(fs.readFileSync(p, 'utf-8')).oauthAccount?.emailAddress;
|
|
30
|
+
if (typeof email === 'string' && email.trim().length > 0)
|
|
31
|
+
return email.trim();
|
|
32
|
+
}
|
|
33
|
+
catch {
|
|
34
|
+
// Missing/unreadable at this location — try the next.
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return null;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Resolve a long-lived `claude setup-token` for the account signed into `home`
|
|
41
|
+
* from the reserved FILE-BASED `auth` bundle. Returns the token or null. Reads
|
|
42
|
+
* ONLY when the bundle is file-backed (never keychain), so this path itself can
|
|
43
|
+
* never trigger a Touch ID prompt — that is the entire point: usage/probe reads
|
|
44
|
+
* authenticate with the shareable setup-token, not the ACL-bound login item.
|
|
45
|
+
*/
|
|
46
|
+
export function resolveClaudeSetupToken(home) {
|
|
47
|
+
try {
|
|
48
|
+
// Require a known account (email) up front: without it we cannot key a
|
|
49
|
+
// per-account token, and we must NOT fall back to a bare shared key that
|
|
50
|
+
// would misapply one account's setup-token to another.
|
|
51
|
+
const email = readClaudeAccountEmail(home);
|
|
52
|
+
if (!email)
|
|
53
|
+
return null;
|
|
54
|
+
if (!bundleExists(AUTH_BUNDLE) || bundleBackend(AUTH_BUNDLE) !== 'file')
|
|
55
|
+
return null;
|
|
56
|
+
const { env } = readAndResolveBundleEnv(AUTH_BUNDLE, { caller: 'usage', agentOnly: true });
|
|
57
|
+
const v = (env[claudeAccountTokenKey(email)] ?? '').trim();
|
|
58
|
+
return v.length > 0 ? v : null;
|
|
59
|
+
}
|
|
60
|
+
catch {
|
|
61
|
+
return null;
|
|
62
|
+
}
|
|
63
|
+
}
|
|
@@ -4,6 +4,16 @@
|
|
|
4
4
|
*/
|
|
5
5
|
import type { AgentId } from './types.js';
|
|
6
6
|
export declare function shouldInstallCommandAsSkill(agent: AgentId, version: string): boolean;
|
|
7
|
+
/**
|
|
8
|
+
* Agents whose native command files serve a separate surface while their CLI
|
|
9
|
+
* consumes the generated skill form. Keep this registry distinct from
|
|
10
|
+
* `shouldInstallCommandAsSkill`: these targets need both writes, not a format
|
|
11
|
+
* replacement.
|
|
12
|
+
*/
|
|
13
|
+
export declare const COMMAND_SKILL_DUAL_WRITE_TARGETS: {
|
|
14
|
+
cursor: true;
|
|
15
|
+
};
|
|
16
|
+
export declare function shouldAlsoInstallCommandAsSkill(agent: AgentId, version: string): boolean;
|
|
7
17
|
export declare function commandSkillName(commandName: string): string;
|
|
8
18
|
export declare function buildCommandSkillContent(commandName: string, sourcePath: string): string;
|
|
9
19
|
export declare function skillSourceExists(skillName: string, skillSourceDirs: Array<string | null | undefined>): boolean;
|
|
@@ -52,6 +52,20 @@ function readSkillCommandMarker(skillMdPath) {
|
|
|
52
52
|
export function shouldInstallCommandAsSkill(agent, version) {
|
|
53
53
|
return !supports(agent, 'commands', version).ok && supports(agent, 'skills', version).ok;
|
|
54
54
|
}
|
|
55
|
+
/**
|
|
56
|
+
* Agents whose native command files serve a separate surface while their CLI
|
|
57
|
+
* consumes the generated skill form. Keep this registry distinct from
|
|
58
|
+
* `shouldInstallCommandAsSkill`: these targets need both writes, not a format
|
|
59
|
+
* replacement.
|
|
60
|
+
*/
|
|
61
|
+
export const COMMAND_SKILL_DUAL_WRITE_TARGETS = {
|
|
62
|
+
cursor: true,
|
|
63
|
+
};
|
|
64
|
+
export function shouldAlsoInstallCommandAsSkill(agent, version) {
|
|
65
|
+
return COMMAND_SKILL_DUAL_WRITE_TARGETS[agent] === true
|
|
66
|
+
&& supports(agent, 'commands', version).ok
|
|
67
|
+
&& supports(agent, 'skills', version).ok;
|
|
68
|
+
}
|
|
55
69
|
export function commandSkillName(commandName) {
|
|
56
70
|
return commandName;
|
|
57
71
|
}
|
package/dist/lib/commands.js
CHANGED
|
@@ -15,7 +15,7 @@ import { markdownToToml } from './convert.js';
|
|
|
15
15
|
import { getCommandsDir, getUserCommandsDir, getEnabledExtraRepos, getProjectAgentsDir, getSkillsDir, getTrashCommandsDir } from './state.js';
|
|
16
16
|
import { getEffectiveHome, getVersionHomePath, listInstalledVersions, resolveVersion } from './versions.js';
|
|
17
17
|
import { discoverPlugins } from './plugins.js';
|
|
18
|
-
import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldInstallCommandAsSkill, } from './command-skills.js';
|
|
18
|
+
import { commandSkillMatches, installCommandSkillToVersion, listCommandSkillsInVersion, removeCommandSkillFromVersion, shouldAlsoInstallCommandAsSkill, shouldInstallCommandAsSkill, } from './command-skills.js';
|
|
19
19
|
import { installGooseCommandToVersion, listGooseCommandsInVersion, gooseCommandMatches, removeGooseCommandFromVersion, } from './goose-commands.js';
|
|
20
20
|
function compareVersions(a, b) {
|
|
21
21
|
const aParts = a.split('.').map((n) => parseInt(n, 10) || 0);
|
|
@@ -211,6 +211,14 @@ export function installCommand(sourcePath, agentId, commandName, method = 'symli
|
|
|
211
211
|
const agent = AGENTS[agentId];
|
|
212
212
|
ensureCommandsDir(agentId);
|
|
213
213
|
const home = getEffectiveHome(agentId);
|
|
214
|
+
const installVersion = pinnedVersion ?? listInstalledVersions(agentId)[0] ?? '';
|
|
215
|
+
const alsoInstallAsSkill = shouldAlsoInstallCommandAsSkill(agentId, installVersion);
|
|
216
|
+
if (alsoInstallAsSkill) {
|
|
217
|
+
const installed = installCommandSkillToVersion(path.join(home, agentConfigDirName(agentId)), commandName, sourcePath, [getSkillsDir(), ...getEnabledExtraRepos().map((repo) => path.join(repo.dir, 'skills'))]);
|
|
218
|
+
if (!installed.success) {
|
|
219
|
+
return { path: '', method: 'copy', error: installed.error, warnings: validation.warnings };
|
|
220
|
+
}
|
|
221
|
+
}
|
|
214
222
|
// Goose: a slash command is a recipe YAML registered in config.yaml, not a
|
|
215
223
|
// native command file under commandsSubdir.
|
|
216
224
|
if (agentId === 'goose') {
|
|
@@ -401,6 +409,11 @@ export function installCommandToVersion(agent, version, commandName, method = 'c
|
|
|
401
409
|
...getEnabledExtraRepos().map((repo) => path.join(repo.dir, 'skills')),
|
|
402
410
|
]);
|
|
403
411
|
}
|
|
412
|
+
if (shouldAlsoInstallCommandAsSkill(agent, version)) {
|
|
413
|
+
const installed = installCommandSkillToVersion(agentDir, commandName, sourcePath, [getSkillsDir(), ...getEnabledExtraRepos().map((repo) => path.join(repo.dir, 'skills'))]);
|
|
414
|
+
if (!installed.success)
|
|
415
|
+
return installed;
|
|
416
|
+
}
|
|
404
417
|
// Goose: a slash command is a recipe YAML registered in config.yaml, not a
|
|
405
418
|
// native command file. Write the recipe + slash_commands entry.
|
|
406
419
|
if (agent === 'goose') {
|
|
@@ -441,6 +454,11 @@ export function removeCommandFromVersion(agent, version, commandName) {
|
|
|
441
454
|
if (shouldInstallCommandAsSkill(agent, version)) {
|
|
442
455
|
return removeCommandSkillFromVersion(agentDir, commandName);
|
|
443
456
|
}
|
|
457
|
+
if (shouldAlsoInstallCommandAsSkill(agent, version)) {
|
|
458
|
+
const removed = removeCommandSkillFromVersion(agentDir, commandName);
|
|
459
|
+
if (!removed.success)
|
|
460
|
+
return removed;
|
|
461
|
+
}
|
|
444
462
|
if (agent === 'goose') {
|
|
445
463
|
const trashDir = path.join(getTrashCommandsDir(), agent, version, commandName);
|
|
446
464
|
return removeGooseCommandFromVersion(versionHome, commandName, trashDir);
|
package/dist/lib/daemon.js
CHANGED
|
@@ -880,7 +880,7 @@ ${[launch.command, ...launch.args].map((arg) => ` <string>${xmlEscape(arg)}</
|
|
|
880
880
|
<key>EnvironmentVariables</key>
|
|
881
881
|
<dict>
|
|
882
882
|
<key>PATH</key>
|
|
883
|
-
<string>${
|
|
883
|
+
<string>${daemonPathValue(agentsBin, ['/usr/local/bin', '/usr/bin', '/bin', '/opt/homebrew/bin', `${os.homedir()}/.bun/bin`])}</string>
|
|
884
884
|
</dict>
|
|
885
885
|
</dict>
|
|
886
886
|
</plist>`;
|
|
@@ -909,7 +909,7 @@ Type=simple
|
|
|
909
909
|
ExecStart=${execStart}
|
|
910
910
|
Restart=always
|
|
911
911
|
RestartSec=10
|
|
912
|
-
Environment=PATH=${
|
|
912
|
+
Environment=PATH=${daemonPathValue(agentsBin, ['/usr/local/bin', '/usr/bin', '/bin'])}
|
|
913
913
|
|
|
914
914
|
[Install]
|
|
915
915
|
WantedBy=default.target`;
|
|
@@ -1074,6 +1074,21 @@ export function getDaemonLaunch(agentsBin = getAgentsBinPath()) {
|
|
|
1074
1074
|
function daemonNodeBinDir() {
|
|
1075
1075
|
return path.dirname(process.execPath);
|
|
1076
1076
|
}
|
|
1077
|
+
/**
|
|
1078
|
+
* The full PATH value the daemon service manifest pins, in order: the Node runtime
|
|
1079
|
+
* dir (so the shim's shebang and any child resolve the exact installing Node — see
|
|
1080
|
+
* {@link daemonNodeBinDir}), then the directory of the `agents` shim itself, then
|
|
1081
|
+
* the platform's system dirs. The shim's own dir matters because a scheduled
|
|
1082
|
+
* `command` routine shells out to the bare name `agents`
|
|
1083
|
+
* (`/bin/sh -c 'agents watchdog --nudge'`): when the shim lives outside the Node
|
|
1084
|
+
* bin dir — a `~/.local/bin` global install, a separate npm prefix — a PATH
|
|
1085
|
+
* carrying only the Node dir resolves `agents` to nothing and every routine dies
|
|
1086
|
+
* with `exit 127`. Deduped across the whole list, so a Node/shim dir that already
|
|
1087
|
+
* appears among the system dirs (e.g. a `/usr/local/bin` install) never doubles.
|
|
1088
|
+
*/
|
|
1089
|
+
function daemonPathValue(agentsBin, systemDirs) {
|
|
1090
|
+
return [...new Set([daemonNodeBinDir(), path.dirname(agentsBin), ...systemDirs])].join(':');
|
|
1091
|
+
}
|
|
1077
1092
|
/**
|
|
1078
1093
|
* Build the argv to relaunch the `agents` CLI with the given subcommand args.
|
|
1079
1094
|
*
|
|
@@ -0,0 +1,167 @@
|
|
|
1
|
+
import type { AgentId } from '../types.js';
|
|
2
|
+
import type { DuplicateVersionHook } from '../hooks.js';
|
|
3
|
+
import type { RcSecretFinding } from '../secrets/rc-hygiene.js';
|
|
4
|
+
import type { SyncStatusRow, OrphanRow } from '../drift.js';
|
|
5
|
+
import type { FetchStatusMarker } from '../auto-pull.js';
|
|
6
|
+
import type { VersionResourceReport } from '../doctor-diff.js';
|
|
7
|
+
import type { FleetDivergence, FleetVersionSignIn } from './fleet-divergence.js';
|
|
8
|
+
export type FindingSeverity = 'critical' | 'warning';
|
|
9
|
+
/** A machine-stable class for a finding — drives {@link remediationFor} and lets
|
|
10
|
+
* the JSON consumer group by kind. */
|
|
11
|
+
export declare const ALL_FINDING_KINDS: readonly ["logged-out", "logout-unprovable", "missing-hook", "missing-plugin", "unwired-hook", "cli-missing", "missing-resource", "content-drift", "never-synced", "stale", "repo-behind", "repo-drift", "fleet-resource-gap", "host-cli-missing", "host-cli-invalid", "version-skew", "orphan", "duplicate-hook", "duplicate-hook-drift", "rc-secret-export", "exec-policy", "stale-cli"];
|
|
12
|
+
/** A machine-stable class for a finding. Derived from the runtime list above so
|
|
13
|
+
* the rubric test can enumerate every kind. */
|
|
14
|
+
export type FindingKind = typeof ALL_FINDING_KINDS[number];
|
|
15
|
+
/** One prioritized finding, attributed to a device (and, when relevant, an agent
|
|
16
|
+
* version + account). `remediation` is the exact command/hint to fix it. */
|
|
17
|
+
export interface DoctorFinding {
|
|
18
|
+
severity: FindingSeverity;
|
|
19
|
+
kind: FindingKind;
|
|
20
|
+
/** The device this finding is about. */
|
|
21
|
+
device: string;
|
|
22
|
+
/** Agent id, when the finding is about a specific agent (else undefined). */
|
|
23
|
+
agent?: AgentId;
|
|
24
|
+
/** Version id, when about a specific installed version. Absent on a finding
|
|
25
|
+
* collapsed across versions — read {@link DoctorFinding.versions} instead. */
|
|
26
|
+
version?: string;
|
|
27
|
+
/** Set only on a finding collapsed across several versions of one agent (the
|
|
28
|
+
* same problem on each). The row renders `<agent> (N versions)` and the
|
|
29
|
+
* remediation widens to the agent-wide sweep. */
|
|
30
|
+
versions?: string[];
|
|
31
|
+
/** Human account label (email/org/opaque id), when known. */
|
|
32
|
+
account?: string | null;
|
|
33
|
+
/** One-line plain-English description of the problem. */
|
|
34
|
+
message: string;
|
|
35
|
+
/** Exact remediation command / hint. */
|
|
36
|
+
remediation: string;
|
|
37
|
+
}
|
|
38
|
+
/**
|
|
39
|
+
* The exact remediation for a finding. Login fixes are harness-native
|
|
40
|
+
* (`loginHint`); a per-version login is offered ONLY for agents that isolate the
|
|
41
|
+
* credential per home (`agents run <agent>@<version>` then log in) — for
|
|
42
|
+
* gemini/antigravity/droid/cursor the login is shared, so we say so instead of
|
|
43
|
+
* faking a per-version fix. Every other kind maps to its canonical command.
|
|
44
|
+
*/
|
|
45
|
+
export declare function remediationFor(finding: DoctorFinding): string;
|
|
46
|
+
export interface LocalFindingInputs {
|
|
47
|
+
device: string;
|
|
48
|
+
syncRows: SyncStatusRow[];
|
|
49
|
+
orphanRows: OrphanRow[];
|
|
50
|
+
repoBehind: FetchStatusMarker[];
|
|
51
|
+
/** Per-version resource reports (one per installed version) — the source of the
|
|
52
|
+
* missing-hook / missing-plugin / missing-resource / content-drift / unwired
|
|
53
|
+
* criticals+warnings. */
|
|
54
|
+
reports: VersionResourceReport[];
|
|
55
|
+
/** Per-version sign-in per agent id. */
|
|
56
|
+
signIn: Record<string, FleetVersionSignIn[]>;
|
|
57
|
+
/** Managed agents (installed versions) whose binary won't resolve. */
|
|
58
|
+
cliMissing?: AgentId[];
|
|
59
|
+
/** Host CLIs declared in a DotAgents repo's `cli/`: their install state on this
|
|
60
|
+
* box, plus any manifest the loader could not parse. Host-global (installed to
|
|
61
|
+
* PATH, never synced into a version home), so they are a machine-level
|
|
62
|
+
* finding, not a per-version one. */
|
|
63
|
+
hostClis?: {
|
|
64
|
+
statuses: Array<{
|
|
65
|
+
name: string;
|
|
66
|
+
installed: boolean;
|
|
67
|
+
}>;
|
|
68
|
+
errors: Array<{
|
|
69
|
+
file: string;
|
|
70
|
+
reason: string;
|
|
71
|
+
}>;
|
|
72
|
+
};
|
|
73
|
+
/** Hooks materialized into several version homes at once — identical copies are
|
|
74
|
+
* installation noise, differing ones are drift a stale gate can act on. */
|
|
75
|
+
duplicateHooks?: DuplicateVersionHook[];
|
|
76
|
+
/** Credential-shaped exports found in the user's shell rc files (RUSH-1968). */
|
|
77
|
+
rcSecrets?: RcSecretFinding[];
|
|
78
|
+
/** The effective PowerShell execution policy and the platform it was read on.
|
|
79
|
+
* Only `win32` yields a finding — the `agents.ps1` launcher is Windows-only. */
|
|
80
|
+
execPolicy?: {
|
|
81
|
+
platform: NodeJS.Platform;
|
|
82
|
+
policy: string | null;
|
|
83
|
+
};
|
|
84
|
+
/** `<agent>@<version>` keys whose home is an isolated copy. Their findings are
|
|
85
|
+
* never collapsed across versions: the agent-wide `agents doctor <agent> --fix`
|
|
86
|
+
* sweep deliberately skips isolated copies, so a collapsed row would print a
|
|
87
|
+
* remediation that does not fix them. */
|
|
88
|
+
isolatedVersions?: string[];
|
|
89
|
+
}
|
|
90
|
+
/**
|
|
91
|
+
* Fold this machine's signals into findings. Missing hooks/plugins and unwired
|
|
92
|
+
* hooks are CRITICAL; provable logouts are CRITICAL and unprovable ones WARNING;
|
|
93
|
+
* everything else (other missing kinds, drift, stale/never-synced, repo-behind,
|
|
94
|
+
* orphans) is a WARNING. Pure.
|
|
95
|
+
*/
|
|
96
|
+
export declare function buildLocalFindings(input: LocalFindingInputs): DoctorFinding[];
|
|
97
|
+
/**
|
|
98
|
+
* Fold findings that say the SAME thing about several versions of one agent into
|
|
99
|
+
* a single row carrying `versions`, and widen its remediation to the agent-wide
|
|
100
|
+
* sweep (`agents doctor claude --fix` heals every non-isolated version in one
|
|
101
|
+
* go). Five identical `plugin 'code' — mirror missing` rows, one per installed
|
|
102
|
+
* claude, is the same fact five times.
|
|
103
|
+
*
|
|
104
|
+
* Three things never merge, because for each of them the widened remediation
|
|
105
|
+
* would be wrong:
|
|
106
|
+
* - **Isolated copies** — the agent-wide sweep deliberately skips them
|
|
107
|
+
* (`runFix`), so a folded row would print a command that leaves one broken.
|
|
108
|
+
* - **Findings with no agent** (repo-behind, rc-secret-export, …) — their
|
|
109
|
+
* `version` field is an alias, not a version.
|
|
110
|
+
* - **Logouts** ({@link NEVER_COLLAPSED}) — a login is inherently per-version:
|
|
111
|
+
* the fix is `agents run <agent>@<version> -- login`, and there is no `@all`
|
|
112
|
+
* equivalent. Dropping `version` would fall back to the bare native hint,
|
|
113
|
+
* which the shim resolves to the DEFAULT version — logging into the wrong one
|
|
114
|
+
* and leaving the finding to reappear.
|
|
115
|
+
*
|
|
116
|
+
* Pure; input order is kept.
|
|
117
|
+
*/
|
|
118
|
+
export declare function collapseAcrossVersions(findings: DoctorFinding[], isolated: Set<string>): DoctorFinding[];
|
|
119
|
+
/**
|
|
120
|
+
* Map a device's per-version sign-in into logout findings: a PROVABLE logout is
|
|
121
|
+
* CRITICAL, an unprovable one is a hedged WARNING ("could not verify sign-in"),
|
|
122
|
+
* and a signed-in version yields nothing.
|
|
123
|
+
*
|
|
124
|
+
* An agent with no inspectable identity never appears at all — not even as the
|
|
125
|
+
* hedged warning: agents-cli knows no credential file for it, so "logged out" is
|
|
126
|
+
* unknowable and silence beats a false claim. Membership is
|
|
127
|
+
* `supportsAccountInspection` (`lib/agents.ts`) and is deliberately NOT listed
|
|
128
|
+
* here — agents move between the sets, and a copy of the list in prose becomes a
|
|
129
|
+
* lie the next time one does. Note the caller also requires
|
|
130
|
+
* `CredentialPresence.knownLocation`: the inspection set and the credential-path
|
|
131
|
+
* map move independently, so being inspectable is not on its own enough to call a
|
|
132
|
+
* logout provable. Pure.
|
|
133
|
+
*/
|
|
134
|
+
export declare function signInToFindings(device: string, signIn: Record<string, FleetVersionSignIn[]>): DoctorFinding[];
|
|
135
|
+
/**
|
|
136
|
+
* Map cross-device divergence (from {@link compareFleetInventories}) into
|
|
137
|
+
* warnings: an agent version present elsewhere but absent on a device is a
|
|
138
|
+
* version-skew warning; a diverged config repo is a repo-drift warning; a
|
|
139
|
+
* missing resource is a missing-resource warning. Baseline = the local machine.
|
|
140
|
+
* Only the *lagging* box is attributed (a `*-missing-local` finding is the
|
|
141
|
+
* baseline's gap). Pure.
|
|
142
|
+
*/
|
|
143
|
+
export declare function fleetDivergenceToFindings(divergences: FleetDivergence[], baseline: string): DoctorFinding[];
|
|
144
|
+
export interface RenderOptions {
|
|
145
|
+
/** Fleet mode (`--devices`): render the `─── by computer ───` header + one
|
|
146
|
+
* block per device. Single-machine mode collapses to one `▸ <machine>` block
|
|
147
|
+
* with no fleet header. */
|
|
148
|
+
fleet: boolean;
|
|
149
|
+
/** The baseline (local) machine name — tagged `· this machine`. */
|
|
150
|
+
baseline?: string;
|
|
151
|
+
/** Header line context: device count (fleet) or the local version string. */
|
|
152
|
+
header?: string;
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Render the two-part hybrid layout from a flat findings list. Pure — returns the
|
|
156
|
+
* lines so the exact output is snapshot-tested. Criticals across ALL devices go
|
|
157
|
+
* to the top section, worst-first; the per-computer section lists each device's
|
|
158
|
+
* warnings + a `✗ N critical (above)` marker, worst device first.
|
|
159
|
+
*/
|
|
160
|
+
export declare function renderFindings(findings: DoctorFinding[], accounts: Record<string, Record<string, FleetVersionSignIn[]>>, opts: RenderOptions): string[];
|
|
161
|
+
/**
|
|
162
|
+
* The compact accounts/versions line for one device: every installed version and
|
|
163
|
+
* its account, grouped by agent — green ✓ signed in, red ✗ provably logged out,
|
|
164
|
+
* gray ? unknown (see {@link badge}). e.g.
|
|
165
|
+
* `claude 2.1.170 ✓muqsit@gmail(Max) 2.1.999 ✓team(Team) · codex ✗ · grok ✓`
|
|
166
|
+
*/
|
|
167
|
+
export declare function renderAccountsLine(signIn: Record<string, FleetVersionSignIn[]>): string;
|