@phnx-labs/agents-cli 1.20.33 → 1.20.34

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +28 -2
  3. package/dist/commands/computer.d.ts +23 -0
  4. package/dist/commands/computer.js +45 -3
  5. package/dist/commands/doctor.d.ts +10 -0
  6. package/dist/commands/doctor.js +49 -0
  7. package/dist/commands/import.js +1 -1
  8. package/dist/commands/rules.js +1 -1
  9. package/dist/commands/secrets-migrate.js +23 -11
  10. package/dist/commands/secrets.d.ts +20 -0
  11. package/dist/commands/secrets.js +53 -1
  12. package/dist/commands/status.d.ts +12 -0
  13. package/dist/commands/status.js +81 -0
  14. package/dist/commands/teams.js +70 -6
  15. package/dist/commands/versions.js +2 -1
  16. package/dist/commands/view.d.ts +39 -0
  17. package/dist/commands/view.js +194 -75
  18. package/dist/index.js +4 -2
  19. package/dist/lib/acp/harnesses.d.ts +1 -1
  20. package/dist/lib/acp/harnesses.js +2 -2
  21. package/dist/lib/agents.d.ts +12 -0
  22. package/dist/lib/agents.js +115 -32
  23. package/dist/lib/browser/chrome.js +20 -0
  24. package/dist/lib/browser/drivers/ssh.d.ts +19 -0
  25. package/dist/lib/browser/drivers/ssh.js +18 -3
  26. package/dist/lib/doctor-diff.js +29 -2
  27. package/dist/lib/drift-sync.d.ts +43 -0
  28. package/dist/lib/drift-sync.js +179 -0
  29. package/dist/lib/exec.d.ts +15 -0
  30. package/dist/lib/exec.js +21 -11
  31. package/dist/lib/platform/winpath.d.ts +31 -2
  32. package/dist/lib/platform/winpath.js +133 -24
  33. package/dist/lib/pwsh.d.ts +11 -0
  34. package/dist/lib/pwsh.js +13 -0
  35. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  36. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  37. package/dist/lib/secrets/agent.d.ts +42 -1
  38. package/dist/lib/secrets/agent.js +89 -11
  39. package/dist/lib/secrets/bundles.js +40 -9
  40. package/dist/lib/secrets/filestore.js +31 -1
  41. package/dist/lib/secrets/index.d.ts +33 -1
  42. package/dist/lib/secrets/index.js +90 -9
  43. package/dist/lib/secrets/windows.d.ts +74 -0
  44. package/dist/lib/secrets/windows.js +440 -0
  45. package/dist/lib/shims.d.ts +20 -0
  46. package/dist/lib/shims.js +53 -20
  47. package/dist/lib/startup/command-registry.d.ts +1 -0
  48. package/dist/lib/startup/command-registry.js +2 -0
  49. package/dist/lib/sync-status.d.ts +102 -0
  50. package/dist/lib/sync-status.js +135 -0
  51. package/dist/lib/teams/agents.d.ts +24 -0
  52. package/dist/lib/teams/agents.js +30 -1
  53. package/dist/lib/types.d.ts +20 -1
  54. package/dist/lib/usage.d.ts +30 -0
  55. package/dist/lib/usage.js +159 -2
  56. package/package.json +1 -1
@@ -2,7 +2,7 @@ import chalk from 'chalk';
2
2
  import * as fs from 'fs/promises';
3
3
  import { addHostOption } from '../lib/hosts/option.js';
4
4
  import * as path from 'path';
5
- import { AgentManager, checkAllClis, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
5
+ import { AgentManager, checkAllClis, checkCliSignedIn, getAgentsDir, resolveSignInAdvisory, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
6
6
  import { resolveProvider } from '../lib/cloud/registry.js';
7
7
  import { runSupervisor } from '../lib/teams/supervisor.js';
8
8
  import { handleSpawn, handleStatus, handleStop, handleTasks, toTaskStatusSummary, } from '../lib/teams/api.js';
@@ -10,6 +10,7 @@ import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } f
10
10
  import { setHelpSections } from '../lib/help.js';
11
11
  import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
12
12
  import { isVersionInstalled, resolveVersionAlias, resolveVersionAliasLoose } from '../lib/versions.js';
13
+ import { AGENTS, warnAgentDeprecated } from '../lib/agents.js';
13
14
  import { discoverSessions, parseTimeFilter, resolveSessionById } from '../lib/session/discover.js';
14
15
  import { buildPreview as buildSessionPreview } from './sessions-picker.js';
15
16
  import { parseExecEnv } from '../lib/exec.js';
@@ -133,7 +134,7 @@ function parseTeammate(spec) {
133
134
  }
134
135
  }
135
136
  die(`Unknown teammate '${spec}'. Available agents: ${VALID_AGENTS.join(', ')}.\n` +
136
- ` Use 'claude', 'claude@2.1.112', or the name of a profile from 'agents view'.`);
137
+ ` Use 'claude', 'kimi@latest', 'kimi@0.19.2' (a version from 'agents view'), or a profile name.`);
137
138
  }
138
139
  function shortId(id) {
139
140
  return id.slice(0, 8);
@@ -183,6 +184,31 @@ export function wireCloudDispatcher(mgr) {
183
184
  return { cloudSessionId: cloudTask.id };
184
185
  });
185
186
  }
187
+ /**
188
+ * Advisory: warn once per agent type of any still-pending (staged `--after`)
189
+ * teammate whose CLI may not be signed in. Warn-only — never blocks `start`.
190
+ * Local teammates only; cloud teammates authenticate through their provider.
191
+ */
192
+ async function warnUnsignedTeammates(mgr, team) {
193
+ let pending;
194
+ try {
195
+ pending = (await mgr.listByTask(team)).filter((a) => a.status === 'pending' && !a.cloudProvider);
196
+ }
197
+ catch {
198
+ return; // team not loadable yet — nothing to warn about
199
+ }
200
+ const seen = new Set();
201
+ for (const a of pending) {
202
+ const agent = a.agentType;
203
+ if (seen.has(agent) || !AGENT_NAMES[agent])
204
+ continue;
205
+ seen.add(agent);
206
+ if (!(await checkCliSignedIn(agent))) {
207
+ console.error(chalk.yellow(`⚠ ${AGENT_NAMES[agent]} may not be signed in (detection is unreliable). Launching anyway.`) +
208
+ chalk.gray(`\n If it fails to start, run \`${AGENTS[agent].cliCommand}\` to log in, or pass --force to silence this.`));
209
+ }
210
+ }
211
+ }
186
212
  /** Single-wave start used by `teams start` without --watch. */
187
213
  async function runOneWave(mgr, team, json) {
188
214
  const launched = await mgr.startReady(team);
@@ -839,6 +865,7 @@ export function registerTeamsCommands(program) {
839
865
  .option('--cloud <provider>', `Dispatch to cloud backend instead of local CLI: ${VALID_CLOUD_PROVIDERS.join('|')}`)
840
866
  .option('--repo <owner/repo>', 'GitHub repository (required for --cloud rush)')
841
867
  .option('--branch <name>', 'Target git branch for cloud dispatch')
868
+ .option('--force', "Skip the advisory 'may not be signed in' warning (detection is unreliable)")
842
869
  .option('--json', 'Output machine-readable JSON')
843
870
  .action(async (team, teammate, task, opts) => {
844
871
  if (!VALID_MODES.includes(opts.mode)) {
@@ -865,10 +892,18 @@ export function registerTeamsCommands(program) {
865
892
  }
866
893
  }
867
894
  const { agent, version, profileName } = parseTeammate(teammate);
895
+ warnAgentDeprecated(agent);
868
896
  if (version && !isVersionInstalled(agent, version)) {
869
897
  die(`${AGENT_NAMES[agent]} ${version} isn't installed.\n` +
870
898
  ` Install it: agents add ${agent}@${version}\n` +
871
- ` Or see what's installed: agents view ${agent}`);
899
+ ` Or see what's installed (incl. @latest): agents view ${agent}`);
900
+ }
901
+ // Advisory sign-in check: warn but NEVER block. Detection is unreliable
902
+ // for opaque-cred agents, so a false negative must not stop a team. Cloud
903
+ // dispatch authenticates through the provider, not the local CLI — skip it.
904
+ if (!opts.force && !cloudProviderId && !(await checkCliSignedIn(agent))) {
905
+ console.error(chalk.yellow(`⚠ ${AGENT_NAMES[agent]} may not be signed in (detection is unreliable). Adding anyway.`) +
906
+ chalk.gray(`\n If it fails to start, run \`${AGENTS[agent].cliCommand}\` to log in, or pass --force to silence this.`));
872
907
  }
873
908
  if (opts.name !== undefined) {
874
909
  if (!opts.name || !/^[A-Za-z0-9_-]+$/.test(opts.name)) {
@@ -1139,6 +1174,7 @@ export function registerTeamsCommands(program) {
1139
1174
  .option('--watch', 'Keep running: poll every --interval seconds, fire new waves, exit when the DAG drains.')
1140
1175
  .option('--interval <seconds>', 'Seconds between waves in --watch mode (default 8)', '8')
1141
1176
  .option('--max-waves <n>', 'Safety cap on waves in --watch mode (default 1000)', '1000')
1177
+ .option('--force', "Skip the advisory 'may not be signed in' warning for staged teammates (detection is unreliable)")
1142
1178
  .action(async (team, opts) => {
1143
1179
  const mgr = mkManager();
1144
1180
  wireCloudDispatcher(mgr);
@@ -1148,6 +1184,8 @@ export function registerTeamsCommands(program) {
1148
1184
  return;
1149
1185
  team = picked;
1150
1186
  }
1187
+ if (!opts.force && !isJsonMode(opts))
1188
+ await warnUnsignedTeammates(mgr, team);
1151
1189
  if (!opts.watch) {
1152
1190
  await runOneWave(mgr, team, Boolean(opts.json));
1153
1191
  return;
@@ -1446,19 +1484,45 @@ export function registerTeamsCommands(program) {
1446
1484
  teams
1447
1485
  .command('doctor')
1448
1486
  .alias('dr')
1449
- .description('Check which agents are installed and available to join a team. Verifies CLI paths.')
1487
+ .description('Check which agents are installed and available to join a team. Verifies CLI paths and shows an advisory sign-in hint.')
1450
1488
  .option('--json', 'Output machine-readable JSON')
1451
1489
  .action(async (opts) => {
1452
1490
  const info = checkAllClis();
1491
+ // Advisory enrichment only. Sign-in detection is UNRELIABLE, so it never
1492
+ // changes the authoritative installed/ready column — it annotates. And an
1493
+ // agent that is actually running in a team is treated as signed in
1494
+ // regardless of the probe, so doctor never reports a working agent as
1495
+ // logged out ("don't show wrong stuff").
1496
+ const running = new Set();
1497
+ try {
1498
+ for (const a of await mkManager().listRunning())
1499
+ running.add(a.agentType);
1500
+ }
1501
+ catch { /* no teams yet — leave running empty */ }
1502
+ const auth = {};
1503
+ await Promise.all(Object.entries(info).map(async ([name, entry]) => {
1504
+ const isRunning = running.has(name);
1505
+ const probe = entry.installed && !isRunning ? await checkCliSignedIn(name) : false;
1506
+ auth[name] = resolveSignInAdvisory(entry.installed, isRunning, probe);
1507
+ }));
1453
1508
  if (isJsonMode(opts)) {
1454
- console.log(JSON.stringify(info, null, 2));
1509
+ const merged = {};
1510
+ for (const [name, entry] of Object.entries(info))
1511
+ merged[name] = { ...entry, ...auth[name] };
1512
+ console.log(JSON.stringify(merged, null, 2));
1455
1513
  return;
1456
1514
  }
1457
1515
  console.log(chalk.bold('Who can join a team:'));
1458
1516
  for (const [name, entry] of Object.entries(info)) {
1459
1517
  const pretty = AGENT_NAMES[name] || name;
1460
1518
  if (entry.installed) {
1461
- console.log(` ${chalk.green('ready')} ${pretty.padEnd(10)} ${chalk.gray(entry.path || '')}`);
1519
+ const { signedIn, running: isRunning } = auth[name];
1520
+ const hint = isRunning
1521
+ ? chalk.gray('in use')
1522
+ : signedIn
1523
+ ? chalk.gray('signed in')
1524
+ : chalk.gray('sign-in unverified');
1525
+ console.log(` ${chalk.green('ready')} ${pretty.padEnd(10)} ${chalk.gray(entry.path || '')} ${hint}`);
1462
1526
  }
1463
1527
  else {
1464
1528
  console.log(` ${chalk.red('no ')} ${pretty.padEnd(10)} ${chalk.gray(entry.error || 'not installed')}`);
@@ -4,7 +4,7 @@ import ora from 'ora';
4
4
  import * as fs from 'fs';
5
5
  import * as path from 'path';
6
6
  import { select, confirm, checkbox } from '@inquirer/prompts';
7
- import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, } from '../lib/agents.js';
7
+ import { AGENTS, ALL_AGENT_IDS, getAccountEmail, getAccountInfo, agentLabel, warnAgentDeprecated, } from '../lib/agents.js';
8
8
  import { formatUsageSummary, getUsageInfoForIdentity, getUsageInfoByIdentity, getUsageLookupKey, } from '../lib/usage.js';
9
9
  import { viewAction } from './view.js';
10
10
  import { readManifest, writeManifest, createDefaultManifest } from '../lib/manifest.js';
@@ -273,6 +273,7 @@ export function registerVersionsCommands(program) {
273
273
  }
274
274
  const { agent, version } = parsed;
275
275
  const agentConfig = AGENTS[agent];
276
+ warnAgentDeprecated(agent);
276
277
  if (!agentConfig.npmPackage && !agentConfig.installScript) {
277
278
  console.log(chalk.yellow(`${agentLabel(agentConfig.id)} has no npm package. Install manually.`));
278
279
  continue;
@@ -9,6 +9,7 @@
9
9
  import type { Command } from 'commander';
10
10
  import type { AgentId } from '../lib/types.js';
11
11
  import { type ProfileSummary } from '../lib/profiles.js';
12
+ type SyncState = 'synced' | 'new' | 'modified' | 'deleted';
12
13
  /** Per-section filter flags. When any are true, only those sections render. */
13
14
  export interface ViewSectionFilter {
14
15
  commands?: boolean;
@@ -27,6 +28,7 @@ export interface ViewJsonVersion {
27
28
  isDefault: boolean;
28
29
  signedIn: boolean;
29
30
  email: string | null;
31
+ accountId?: string | null;
30
32
  plan: string | null;
31
33
  usageStatus: 'available' | 'rate_limited' | 'out_of_credits' | null;
32
34
  overageCredits?: {
@@ -40,6 +42,9 @@ export interface ViewJsonVersion {
40
42
  }>;
41
43
  lastActive: string | null;
42
44
  path: string;
45
+ /** Present only when --resources / --detailed (or --json with a section
46
+ * flag) is passed: this version's resource inventory with git sync-state. */
47
+ resources?: VersionResourcesJson;
43
48
  }
44
49
  /** Machine-readable entry for one agent's installed versions. */
45
50
  export interface ViewJsonAgent {
@@ -47,6 +52,37 @@ export interface ViewJsonAgent {
47
52
  versions: ViewJsonVersion[];
48
53
  profiles: ProfileSummary[];
49
54
  }
55
+ /** Resource sections that --resources can include in --json output. */
56
+ export type ResourceSection = 'commands' | 'skills' | 'mcp' | 'memory' | 'hooks' | 'workflows' | 'plugins';
57
+ /** One resource entry in `--json --resources` output. */
58
+ export interface ResourceItemJson {
59
+ name: string;
60
+ scope?: 'user' | 'project';
61
+ /** Git sync-state vs ~/.agents. Omitted for project-scoped resources or when
62
+ * ~/.agents isn't a git repo. When queried via --host it reflects the
63
+ * remote's ~/.agents — i.e. per-host resource drift. */
64
+ syncState?: SyncState;
65
+ description?: string;
66
+ /** Skills only, when > 0. */
67
+ ruleCount?: number;
68
+ }
69
+ /** Per-version resource inventory; one key per requested section. */
70
+ export interface VersionResourcesJson {
71
+ commands?: ResourceItemJson[];
72
+ skills?: ResourceItemJson[];
73
+ mcp?: ResourceItemJson[];
74
+ memory?: ResourceItemJson[];
75
+ hooks?: ResourceItemJson[];
76
+ workflows?: ResourceItemJson[];
77
+ plugins?: ResourceItemJson[];
78
+ }
79
+ /** Build the set of resource sections to include in `--json` from the
80
+ * `--resources` / `--detailed` flags, plus (in --json mode) the per-section
81
+ * boolean filters (`--skills` etc.) that plain `--json` historically ignored. */
82
+ export declare function parseResourceSections(options: {
83
+ resources?: string | boolean;
84
+ detailed?: boolean;
85
+ } & ViewSectionFilter, jsonMode: boolean): Set<ResourceSection>;
50
86
  /**
51
87
  * Prune older installed versions that share an email with a newer installed
52
88
  * version. Keeps the highest semver per email, skips the global default (with
@@ -66,6 +102,9 @@ export declare function viewAction(agentArg?: string, options?: {
66
102
  prune?: boolean;
67
103
  yes?: boolean;
68
104
  dryRun?: boolean;
105
+ resources?: string | boolean;
106
+ detailed?: boolean;
69
107
  } & ViewSectionFilter): Promise<void>;
70
108
  /** Register the `agents view` command. */
71
109
  export declare function registerViewCommand(program: Command): void;
110
+ export {};
@@ -24,8 +24,24 @@ import { loadManifest, isStale } from '../lib/staleness/index.js';
24
24
  import { confirm } from '@inquirer/prompts';
25
25
  import { formatPath, isInteractiveTerminal, isPromptCancelled } from './utils.js';
26
26
  // Shown in the email column for agents that are signed in but expose no email
27
- // address locally (Antigravity, Kimi store an opaque OAuth/JWT credential).
27
+ // address locally (Antigravity stores an opaque OAuth grant with no identity).
28
28
  const SIGNED_IN_LABEL = 'signed in';
29
+ /**
30
+ * Text for the account (email) column. Prefers the real email; otherwise, for a
31
+ * signed-in agent whose credential carries no email but does carry an opaque
32
+ * account id (Kimi's user_id), show `id:<user_id>` so distinct accounts read
33
+ * distinctly instead of a generic "signed in". Falls back to "signed in" when we
34
+ * have neither (Antigravity), and empty when signed out.
35
+ */
36
+ function accountColumnLabel(info) {
37
+ if (!info)
38
+ return '';
39
+ if (info.email)
40
+ return info.email;
41
+ if (info.signedIn)
42
+ return info.accountId ? `id:${info.accountId}` : SIGNED_IN_LABEL;
43
+ return '';
44
+ }
29
45
  /**
30
46
  * Group profile summaries by their host harness, optionally filtered to a
31
47
  * single agent. Profile YAMLs that fail validation are silently skipped by
@@ -310,7 +326,10 @@ async function showInstalledVersions(filterAgentId) {
310
326
  return info;
311
327
  return {
312
328
  ...info,
313
- plan: canon.plan,
329
+ // Prefer a plan the live usage fetch surfaced (Kimi reports membership
330
+ // tier in /usages; its local auth file has none) and fall back to the
331
+ // account-derived plan (Claude's billingType).
332
+ plan: usageByKey.get(key)?.snapshot?.plan ?? canon.plan,
314
333
  // Throttle state comes from the live usage windows, not the pay-as-you-go
315
334
  // overage flag that AccountInfo.usageStatus used to carry. A maxed window
316
335
  // means rate-limited; no snapshot means no badge. See
@@ -353,10 +372,9 @@ async function showInstalledVersions(filterAgentId) {
353
372
  maxVerLabel = Math.max(maxVerLabel, label.length);
354
373
  const rawInfo = infoMap.get(`${agentId}:${v}`);
355
374
  const info = rawInfo ? mergeCanonical(rawInfo) : undefined;
356
- if (info?.email)
357
- maxEmail = Math.max(maxEmail, info.email.length);
358
- else if (info?.signedIn)
359
- maxEmail = Math.max(maxEmail, SIGNED_IN_LABEL.length);
375
+ const accountLabel = accountColumnLabel(info);
376
+ if (accountLabel)
377
+ maxEmail = Math.max(maxEmail, accountLabel.length);
360
378
  if (info?.plan)
361
379
  maxPlanWidth = Math.max(maxPlanWidth, info.plan.length);
362
380
  }
@@ -432,9 +450,10 @@ async function showInstalledVersions(filterAgentId) {
432
450
  }
433
451
  else {
434
452
  if (hasEmail || hasUsage || hasActive || signedIn) {
435
- // Signed-in agents without a local email (Antigravity, Kimi) show a
436
- // "signed in" placeholder so they read as logged in, not blank.
437
- const display = vInfo?.email || (signedIn ? SIGNED_IN_LABEL : '');
453
+ // Signed-in agents without a local email show their account id
454
+ // (Kimi's user_id) or a "signed in" placeholder (Antigravity) so they
455
+ // read as logged in, not blank.
456
+ const display = accountColumnLabel(vInfo);
438
457
  const emailCol = display.padEnd(maxEmail);
439
458
  parts.push(display ? chalk.cyan(emailCol) : ' '.repeat(maxEmail));
440
459
  }
@@ -515,7 +534,7 @@ async function showInstalledVersions(filterAgentId) {
515
534
  const gUsageStr = formatUsageSummary(gInfo?.plan || null, gUsage?.snapshot || null);
516
535
  const gActiveStr = gInfo ? formatLastActive(gInfo.lastActive) : '';
517
536
  if (gInfo?.email || gUsageStr || gActiveStr || gInfo?.signedIn) {
518
- const gDisplay = gInfo?.email || (gInfo?.signedIn ? SIGNED_IN_LABEL : '');
537
+ const gDisplay = accountColumnLabel(gInfo);
519
538
  parts.push(gDisplay ? chalk.cyan(gDisplay) : '');
520
539
  }
521
540
  if (gUsageStr || gActiveStr)
@@ -686,53 +705,9 @@ async function showAgentResources(agentId, requestedVersion, filter) {
686
705
  }
687
706
  }
688
707
  const home = getVersionHomePath(agentId, version);
689
- // Get git sync status if ~/.agents/ is a git repo
690
- const userAgentsDir = getUserAgentsDir();
691
- const hasGitRepo = isGitRepo(userAgentsDir);
692
- const commandsSync = hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'commands') : null;
693
- const skillsSync = hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'skills') : null;
694
- const hooksSync = hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'hooks') : null;
695
- const memorySync = hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'rules') : null;
696
- // Helper to determine sync state for a resource
697
- const getSyncState = (resourceName, resourceType, syncStatus) => {
698
- if (!syncStatus)
699
- return undefined;
700
- let relativePath;
701
- if (resourceType === 'commands') {
702
- relativePath = `commands/${resourceName}.md`;
703
- }
704
- else if (resourceType === 'skills') {
705
- relativePath = `skills/${resourceName}`;
706
- }
707
- else if (resourceType === 'hooks') {
708
- relativePath = `hooks/${resourceName}`;
709
- }
710
- else {
711
- // Rules files: map agent-specific name (CLAUDE.md) back to canonical (AGENTS.md)
712
- const centralName = getCentralRulesFileName(agentId);
713
- relativePath = `rules/${centralName}`;
714
- }
715
- const matchesPath = (f) => f === relativePath || f.startsWith(relativePath + '/');
716
- const isNew = syncStatus.new.some(matchesPath);
717
- const isStaged = syncStatus.staged.some(matchesPath);
718
- const isModified = syncStatus.modified.some(matchesPath);
719
- const isDeleted = syncStatus.deleted.some(matchesPath);
720
- const isSynced = syncStatus.synced.some(matchesPath);
721
- if (isNew || isStaged) {
722
- return 'new';
723
- }
724
- if (isModified) {
725
- return 'modified';
726
- }
727
- if (isDeleted) {
728
- return 'deleted';
729
- }
730
- if (isSynced) {
731
- return 'synced';
732
- }
733
- // Not in any array = local-only (untracked with no files)
734
- return 'new';
735
- };
708
+ // Git sync status if ~/.agents/ is a git repo (shared loader — see
709
+ // loadResourceSyncData / resolveResourceSyncState, reused by --json --resources).
710
+ const { hasGitRepo, commands: commandsSync, skills: skillsSync, hooks: hooksSync, memory: memorySync } = await loadResourceSyncData();
736
711
  const resources = getAgentResources(agentId, {
737
712
  cwd,
738
713
  scope: 'all',
@@ -745,23 +720,23 @@ async function showAgentResources(agentId, requestedVersion, filter) {
745
720
  version,
746
721
  commands: resources.commands.map(r => ({
747
722
  ...r,
748
- syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'commands', commandsSync),
723
+ syncState: r.scope === 'project' ? undefined : resolveResourceSyncState(agentId, r.name, 'commands', commandsSync),
749
724
  })),
750
725
  skills: resources.skills.map(r => ({
751
726
  ...r,
752
727
  // ruleCount of 0 is noise — every skill has 0 unless it ships subrules, which is rare.
753
728
  ruleCount: r.ruleCount && r.ruleCount > 0 ? r.ruleCount : undefined,
754
- syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'skills', skillsSync),
729
+ syncState: r.scope === 'project' ? undefined : resolveResourceSyncState(agentId, r.name, 'skills', skillsSync),
755
730
  })),
756
731
  skillErrors: resources.skillErrors,
757
732
  mcp: resources.mcp.map(r => ({ name: r.name, scope: r.scope, syncState: r.scope === 'project' ? undefined : 'synced' })),
758
733
  memory: resources.memory.map(r => ({
759
734
  ...r,
760
- syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'memory', memorySync),
735
+ syncState: r.scope === 'project' ? undefined : resolveResourceSyncState(agentId, r.name, 'memory', memorySync),
761
736
  })),
762
737
  hooks: resources.hooks.map(r => ({
763
738
  ...r,
764
- syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'hooks', hooksSync),
739
+ syncState: r.scope === 'project' ? undefined : resolveResourceSyncState(agentId, r.name, 'hooks', hooksSync),
765
740
  })),
766
741
  workflows: resources.workflows.map(r => ({ name: r.name, path: r.path, scope: r.scope })),
767
742
  };
@@ -827,13 +802,10 @@ async function showAgentResources(agentId, requestedVersion, filter) {
827
802
  cliVersion: version,
828
803
  info: accountInfo,
829
804
  });
830
- const emailStr = accountInfo.email
831
- ? chalk.cyan(` ${accountInfo.email}`)
832
- : accountInfo.signedIn
833
- ? chalk.cyan(` ${SIGNED_IN_LABEL}`)
834
- : '';
805
+ const accountLabel = accountColumnLabel(accountInfo);
806
+ const emailStr = accountLabel ? chalk.cyan(` ${accountLabel}`) : '';
835
807
  const status = chalk.green(version);
836
- const usageStr = formatUsageSummary(accountInfo.plan, null);
808
+ const usageStr = formatUsageSummary(usageInfo.snapshot?.plan ?? accountInfo.plan, null);
837
809
  const usagePart = usageStr ? ` ${usageStr}` : '';
838
810
  console.log(` ${colorAgent(agentId)(AGENTS[agentId].name.padEnd(14))} ${status}${emailStr}${usagePart}`);
839
811
  const usageLines = formatUsageSection(usageInfo);
@@ -973,14 +945,144 @@ async function showAgentResources(agentId, requestedVersion, filter) {
973
945
  console.log(chalk.gray('Legend:'), chalk.green('Tracked'), chalk.blue('Local-only'), chalk.yellow('Modified'), chalk.red('Deleted'));
974
946
  }
975
947
  }
948
+ const ALL_RESOURCE_SECTIONS = ['commands', 'skills', 'mcp', 'memory', 'hooks', 'workflows', 'plugins'];
949
+ /** Load git sync-state for the tracked resource kinds. Shared by the human
950
+ * detail view (showAgentResources) and the `--json --resources` path. */
951
+ async function loadResourceSyncData() {
952
+ const userAgentsDir = getUserAgentsDir();
953
+ const hasGitRepo = isGitRepo(userAgentsDir);
954
+ return {
955
+ hasGitRepo,
956
+ commands: hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'commands') : null,
957
+ skills: hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'skills') : null,
958
+ hooks: hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'hooks') : null,
959
+ memory: hasGitRepo ? await getGitSyncStatus(userAgentsDir, 'rules') : null,
960
+ };
961
+ }
962
+ /** Resolve one resource's git sync-state. Extracted from showAgentResources so
963
+ * the human view and the JSON path derive drift identically. */
964
+ function resolveResourceSyncState(agentId, resourceName, resourceType, syncStatus) {
965
+ if (!syncStatus)
966
+ return undefined;
967
+ let relativePath;
968
+ if (resourceType === 'commands') {
969
+ relativePath = `commands/${resourceName}.md`;
970
+ }
971
+ else if (resourceType === 'skills') {
972
+ relativePath = `skills/${resourceName}`;
973
+ }
974
+ else if (resourceType === 'hooks') {
975
+ relativePath = `hooks/${resourceName}`;
976
+ }
977
+ else {
978
+ // Rules files: map agent-specific name (CLAUDE.md) back to canonical (AGENTS.md)
979
+ const centralName = getCentralRulesFileName(agentId);
980
+ relativePath = `rules/${centralName}`;
981
+ }
982
+ const matchesPath = (f) => f === relativePath || f.startsWith(relativePath + '/');
983
+ if (syncStatus.new.some(matchesPath) || syncStatus.staged.some(matchesPath))
984
+ return 'new';
985
+ if (syncStatus.modified.some(matchesPath))
986
+ return 'modified';
987
+ if (syncStatus.deleted.some(matchesPath))
988
+ return 'deleted';
989
+ if (syncStatus.synced.some(matchesPath))
990
+ return 'synced';
991
+ // Not in any array = local-only (untracked with no files)
992
+ return 'new';
993
+ }
994
+ /** Collect one version's resources for `--json`, limited to `sections`. Scans
995
+ * the version's `home`, so per-version differences are reported accurately. */
996
+ function collectVersionResources(agentId, home, cwd, cliInstalled, sections, sync) {
997
+ const res = getAgentResources(agentId, { cwd, scope: 'all', cliInstalled, home });
998
+ const out = {};
999
+ const withSync = (r, type, syncStatus) => {
1000
+ const item = { name: r.name, scope: r.scope };
1001
+ if (r.scope !== 'project') {
1002
+ const s = resolveResourceSyncState(agentId, r.name, type, syncStatus);
1003
+ if (s)
1004
+ item.syncState = s;
1005
+ }
1006
+ if (r.description)
1007
+ item.description = r.description;
1008
+ return item;
1009
+ };
1010
+ if (sections.has('commands'))
1011
+ out.commands = res.commands.map((r) => withSync(r, 'commands', sync.commands));
1012
+ if (sections.has('skills')) {
1013
+ out.skills = res.skills.map((r) => {
1014
+ const item = withSync(r, 'skills', sync.skills);
1015
+ // ruleCount of 0 is noise — every skill has 0 unless it ships subrules.
1016
+ if (r.ruleCount && r.ruleCount > 0)
1017
+ item.ruleCount = r.ruleCount;
1018
+ return item;
1019
+ });
1020
+ }
1021
+ if (sections.has('mcp')) {
1022
+ out.mcp = res.mcp.map((r) => {
1023
+ const item = { name: r.name, scope: r.scope };
1024
+ if (r.scope !== 'project')
1025
+ item.syncState = 'synced';
1026
+ return item;
1027
+ });
1028
+ }
1029
+ if (sections.has('memory'))
1030
+ out.memory = res.memory.map((r) => withSync(r, 'memory', sync.memory));
1031
+ if (sections.has('hooks'))
1032
+ out.hooks = res.hooks.map((r) => withSync(r, 'hooks', sync.hooks));
1033
+ if (sections.has('workflows'))
1034
+ out.workflows = res.workflows.map((r) => ({ name: r.name, scope: r.scope }));
1035
+ if (sections.has('plugins')) {
1036
+ out.plugins = discoverPlugins()
1037
+ .filter((p) => pluginSupportsAgent(p, agentId))
1038
+ .map((p) => ({ name: p.name }));
1039
+ }
1040
+ return out;
1041
+ }
1042
+ /** Build the set of resource sections to include in `--json` from the
1043
+ * `--resources` / `--detailed` flags, plus (in --json mode) the per-section
1044
+ * boolean filters (`--skills` etc.) that plain `--json` historically ignored. */
1045
+ export function parseResourceSections(options, jsonMode) {
1046
+ const set = new Set();
1047
+ const addAll = () => ALL_RESOURCE_SECTIONS.forEach((s) => set.add(s));
1048
+ if (options.detailed)
1049
+ addAll();
1050
+ const rv = options.resources;
1051
+ if (rv !== undefined) {
1052
+ if (rv === true || String(rv).trim() === '' || String(rv).toLowerCase() === 'all') {
1053
+ addAll();
1054
+ }
1055
+ else {
1056
+ for (const raw of String(rv).split(',').map((s) => s.trim().toLowerCase()).filter(Boolean)) {
1057
+ const section = raw === 'rules' ? 'memory' : raw; // --rules is the memory file
1058
+ if (ALL_RESOURCE_SECTIONS.includes(section))
1059
+ set.add(section);
1060
+ }
1061
+ }
1062
+ }
1063
+ if (jsonMode) {
1064
+ const flagToSection = [
1065
+ ['commands', 'commands'], ['skills', 'skills'], ['mcp', 'mcp'],
1066
+ ['workflows', 'workflows'], ['plugins', 'plugins'], ['hooks', 'hooks'], ['rules', 'memory'],
1067
+ ];
1068
+ for (const [flag, section] of flagToSection)
1069
+ if (options[flag])
1070
+ set.add(section);
1071
+ }
1072
+ return set;
1073
+ }
976
1074
  /**
977
1075
  * Collect structured info for one or more agents without rendering to the
978
1076
  * terminal. Used by `--json` output and any programmatic consumer (e.g. the
979
1077
  * agents-cli extension's "resume current session in best available version"
980
1078
  * command).
981
1079
  */
982
- async function collectAgentsJson(filterAgentId) {
1080
+ async function collectAgentsJson(filterAgentId, resourceSections) {
983
1081
  const agentsToShow = filterAgentId ? [filterAgentId] : ALL_AGENT_IDS;
1082
+ const wantResources = !!resourceSections && resourceSections.size > 0;
1083
+ // Only pay for the git-status + resource scans when resources were requested.
1084
+ const resourceSync = wantResources ? await loadResourceSyncData() : null;
1085
+ const cliStates = wantResources ? await getAllCliStates() : null;
984
1086
  const infoFetches = [];
985
1087
  for (const agentId of agentsToShow) {
986
1088
  for (const ver of listInstalledVersions(agentId)) {
@@ -1004,7 +1106,10 @@ async function collectAgentsJson(filterAgentId) {
1004
1106
  return info;
1005
1107
  return {
1006
1108
  ...info,
1007
- plan: canon.plan,
1109
+ // Prefer a plan the live usage fetch surfaced (Kimi reports membership
1110
+ // tier in /usages; its local auth file has none) and fall back to the
1111
+ // account-derived plan (Claude's billingType).
1112
+ plan: usageByKey.get(key)?.snapshot?.plan ?? canon.plan,
1008
1113
  // Throttle state comes from the live usage windows, not the pay-as-you-go
1009
1114
  // overage flag that AccountInfo.usageStatus used to carry. A maxed window
1010
1115
  // means rate-limited; no snapshot means no badge. See
@@ -1014,7 +1119,7 @@ async function collectAgentsJson(filterAgentId) {
1014
1119
  };
1015
1120
  };
1016
1121
  const byAgent = new Map();
1017
- for (const { agentId, version, info: rawInfo } of infoResults) {
1122
+ for (const { agentId, version, home, info: rawInfo } of infoResults) {
1018
1123
  const info = mergeCanonical(rawInfo);
1019
1124
  const globalDefault = getGlobalDefault(agentId);
1020
1125
  const usageKey = getUsageLookupKey(info);
@@ -1025,6 +1130,7 @@ async function collectAgentsJson(filterAgentId) {
1025
1130
  isDefault: version === globalDefault,
1026
1131
  signedIn: info.signedIn,
1027
1132
  email: info.email,
1133
+ accountId: info.accountId,
1028
1134
  plan: info.plan,
1029
1135
  usageStatus: info.usageStatus,
1030
1136
  overageCredits: info.overageCredits,
@@ -1038,6 +1144,9 @@ async function collectAgentsJson(filterAgentId) {
1038
1144
  lastActive: info.lastActive ? info.lastActive.toISOString() : null,
1039
1145
  path: getVersionDir(agentId, version),
1040
1146
  };
1147
+ if (wantResources && resourceSync) {
1148
+ entry.resources = collectVersionResources(agentId, home, process.cwd(), cliStates?.[agentId]?.installed ?? false, resourceSections, resourceSync);
1149
+ }
1041
1150
  const existing = byAgent.get(agentId);
1042
1151
  if (existing)
1043
1152
  existing.push(entry);
@@ -1248,7 +1357,10 @@ export async function pruneDuplicates(filterAgentId, yes, dryRun) {
1248
1357
  * Exported for use by deprecated aliases.
1249
1358
  */
1250
1359
  export async function viewAction(agentArg, options) {
1251
- const json = options?.json === true;
1360
+ // --resources / --detailed imply --json (they only shape structured output).
1361
+ const explicitResources = options?.detailed === true || options?.resources !== undefined;
1362
+ const json = options?.json === true || explicitResources;
1363
+ const resourceSections = parseResourceSections(options ?? {}, json);
1252
1364
  const prune = options?.prune === true;
1253
1365
  const yes = options?.yes === true;
1254
1366
  const dryRun = options?.dryRun === true;
@@ -1280,7 +1392,7 @@ export async function viewAction(agentArg, options) {
1280
1392
  return;
1281
1393
  }
1282
1394
  if (json) {
1283
- const data = await collectAgentsJson();
1395
+ const data = await collectAgentsJson(undefined, resourceSections);
1284
1396
  console.log(JSON.stringify(data, null, 2));
1285
1397
  return;
1286
1398
  }
@@ -1329,9 +1441,9 @@ export async function viewAction(agentArg, options) {
1329
1441
  return;
1330
1442
  }
1331
1443
  if (json) {
1332
- // --json ignores the @version suffix (detailed resource view is not yet
1333
- // exposed as structured data). Emit the version list for the agent.
1334
- const data = await collectAgentsJson(agentId);
1444
+ // --json ignores the @version suffix, but --resources/--detailed (or a
1445
+ // section flag) now attach each version's resource inventory + sync-state.
1446
+ const data = await collectAgentsJson(agentId, resourceSections);
1335
1447
  console.log(JSON.stringify(data[0] ?? { agent: agentId, versions: [], profiles: [] }, null, 2));
1336
1448
  return;
1337
1449
  }
@@ -1354,6 +1466,8 @@ export function registerViewCommand(program) {
1354
1466
  addHostOption(program.command('view [agent]'))
1355
1467
  .description('Show what agent CLIs are installed and which versions you have. Inspect resources when you pass agent@version.')
1356
1468
  .option('--json', 'Emit machine-readable JSON (version list, usage, signed-in status).')
1469
+ .option('--resources [sections]', 'In --json mode, include each version\'s resources: "all" (default) or a comma list (skills,plugins,mcp,commands,workflows,memory,hooks). Implies --json.')
1470
+ .option('--detailed', 'Include all resources in --json output (alias for --resources all). Implies --json.')
1357
1471
  .option('--prune', 'Remove older installed versions that share an account with a newer installed version. Skips the global default.')
1358
1472
  .option('--dry-run', 'With --prune, show duplicate versions without deleting')
1359
1473
  .option('-y, --yes', 'Skip the prune confirmation prompt.')
@@ -1381,6 +1495,11 @@ Examples:
1381
1495
  # Machine-readable output (used by tools that pick a version programmatically)
1382
1496
  agents view claude --json
1383
1497
 
1498
+ # One call: full inventory + what's synced on a host (installed agents,
1499
+ # versions, accounts, usage, and per-version resource sync-state)
1500
+ agents view claude --host yosemite-s0 --json --resources all
1501
+ agents view claude --json --resources skills,plugins # just those sections
1502
+
1384
1503
  # Prune older versions that duplicate an account already used by a newer version
1385
1504
  agents view --prune --dry-run
1386
1505
  agents view claude --prune