@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.
Files changed (82) hide show
  1. package/CHANGELOG.md +172 -0
  2. package/README.md +3 -3
  3. package/dist/bin/agents +0 -0
  4. package/dist/commands/doctor.d.ts +0 -19
  5. package/dist/commands/doctor.js +219 -305
  6. package/dist/commands/exec.js +7 -19
  7. package/dist/commands/inspect.js +3 -5
  8. package/dist/commands/routines.js +2 -2
  9. package/dist/commands/sessions-browser.d.ts +18 -0
  10. package/dist/commands/sessions-browser.js +126 -24
  11. package/dist/commands/sessions-picker.d.ts +21 -8
  12. package/dist/commands/sessions-picker.js +83 -7
  13. package/dist/commands/sessions.d.ts +19 -0
  14. package/dist/commands/sessions.js +148 -18
  15. package/dist/commands/ssh.js +62 -4
  16. package/dist/commands/teams-picker.d.ts +2 -0
  17. package/dist/commands/teams-picker.js +2 -1
  18. package/dist/commands/teams.d.ts +4 -1
  19. package/dist/commands/teams.js +106 -70
  20. package/dist/commands/usage.d.ts +3 -2
  21. package/dist/commands/usage.js +2 -9
  22. package/dist/commands/view.js +14 -3
  23. package/dist/index.js +31 -1
  24. package/dist/lib/agents.d.ts +31 -1
  25. package/dist/lib/agents.js +55 -0
  26. package/dist/lib/claude-account-token.d.ts +12 -0
  27. package/dist/lib/claude-account-token.js +63 -0
  28. package/dist/lib/command-skills.d.ts +10 -0
  29. package/dist/lib/command-skills.js +14 -0
  30. package/dist/lib/commands.js +19 -1
  31. package/dist/lib/daemon.js +17 -2
  32. package/dist/lib/devices/doctor-findings.d.ts +167 -0
  33. package/dist/lib/devices/doctor-findings.js +893 -0
  34. package/dist/lib/devices/fleet-divergence.d.ts +22 -0
  35. package/dist/lib/devices/fleet-divergence.js +34 -10
  36. package/dist/lib/devices/fleet-inventory.d.ts +17 -6
  37. package/dist/lib/devices/fleet-inventory.js +56 -8
  38. package/dist/lib/devices/registry.d.ts +25 -0
  39. package/dist/lib/devices/registry.js +82 -1
  40. package/dist/lib/events.d.ts +8 -1
  41. package/dist/lib/events.js +13 -0
  42. package/dist/lib/exec.d.ts +14 -3
  43. package/dist/lib/exec.js +51 -9
  44. package/dist/lib/format.d.ts +7 -0
  45. package/dist/lib/format.js +11 -0
  46. package/dist/lib/menubar/MenubarHelper.app/Contents/MacOS/MenubarHelper +0 -0
  47. package/dist/lib/project-resources.js +34 -20
  48. package/dist/lib/runner.d.ts +14 -1
  49. package/dist/lib/runner.js +37 -8
  50. package/dist/lib/sandbox.d.ts +2 -0
  51. package/dist/lib/sandbox.js +38 -0
  52. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  53. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  54. package/dist/lib/secrets/rc-hygiene.d.ts +0 -6
  55. package/dist/lib/secrets/rc-hygiene.js +0 -24
  56. package/dist/lib/session/active.d.ts +6 -6
  57. package/dist/lib/session/active.js +6 -6
  58. package/dist/lib/session/db.d.ts +21 -0
  59. package/dist/lib/session/db.js +45 -4
  60. package/dist/lib/session/discover.d.ts +5 -0
  61. package/dist/lib/session/discover.js +137 -1
  62. package/dist/lib/session/parse.d.ts +2 -0
  63. package/dist/lib/session/parse.js +76 -37
  64. package/dist/lib/session/remote-list.d.ts +7 -0
  65. package/dist/lib/session/remote-list.js +8 -4
  66. package/dist/lib/session/state.js +69 -2
  67. package/dist/lib/session/sync/agents.js +0 -0
  68. package/dist/lib/session/team-filter.d.ts +22 -3
  69. package/dist/lib/session/team-filter.js +106 -17
  70. package/dist/lib/session/types.d.ts +9 -1
  71. package/dist/lib/session/types.js +1 -1
  72. package/dist/lib/signin-badge.d.ts +17 -0
  73. package/dist/lib/signin-badge.js +19 -0
  74. package/dist/lib/staleness/detectors/commands.js +14 -5
  75. package/dist/lib/staleness/types.d.ts +2 -0
  76. package/dist/lib/staleness/writers/commands.js +13 -7
  77. package/dist/lib/state.d.ts +2 -0
  78. package/dist/lib/state.js +2 -0
  79. package/dist/lib/usage.d.ts +72 -1
  80. package/dist/lib/usage.js +22 -87
  81. package/dist/lib/versions.js +30 -13
  82. 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);
@@ -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;
@@ -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
  }
@@ -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);
@@ -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>${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin:/opt/homebrew/bin:${os.homedir()}/.bun/bin</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=${daemonNodeBinDir()}:/usr/local/bin:/usr/bin:/bin
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;