@phnx-labs/agents-cli 1.20.5 → 1.20.7

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 (70) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/dist/commands/browser.js +31 -4
  4. package/dist/commands/computer-actions.d.ts +36 -0
  5. package/dist/commands/computer-actions.js +328 -0
  6. package/dist/commands/computer.js +74 -55
  7. package/dist/commands/defaults.d.ts +7 -0
  8. package/dist/commands/defaults.js +89 -0
  9. package/dist/commands/exec.js +24 -6
  10. package/dist/commands/inspect.d.ts +38 -7
  11. package/dist/commands/inspect.js +194 -24
  12. package/dist/commands/rules.js +3 -3
  13. package/dist/commands/secrets.js +46 -9
  14. package/dist/commands/sessions.js +9 -12
  15. package/dist/commands/setup.js +2 -2
  16. package/dist/commands/teams.js +108 -11
  17. package/dist/commands/view.d.ts +12 -1
  18. package/dist/commands/view.js +121 -38
  19. package/dist/index.js +61 -22
  20. package/dist/lib/agents.d.ts +10 -6
  21. package/dist/lib/agents.js +23 -14
  22. package/dist/lib/browser/chrome.d.ts +10 -0
  23. package/dist/lib/browser/chrome.js +84 -3
  24. package/dist/lib/daemon.js +4 -7
  25. package/dist/lib/exec.d.ts +9 -0
  26. package/dist/lib/exec.js +85 -9
  27. package/dist/lib/migrate.js +6 -4
  28. package/dist/lib/permissions.d.ts +23 -0
  29. package/dist/lib/permissions.js +89 -7
  30. package/dist/lib/platform/exec.d.ts +9 -0
  31. package/dist/lib/platform/exec.js +24 -0
  32. package/dist/lib/platform/index.d.ts +20 -0
  33. package/dist/lib/platform/index.js +20 -0
  34. package/dist/lib/platform/paths.d.ts +22 -0
  35. package/dist/lib/platform/paths.js +49 -0
  36. package/dist/lib/platform/process.d.ts +12 -0
  37. package/dist/lib/platform/process.js +22 -0
  38. package/dist/lib/plugin-marketplace.js +1 -1
  39. package/dist/lib/project-launch.d.ts +5 -0
  40. package/dist/lib/project-launch.js +37 -0
  41. package/dist/lib/pty-client.js +13 -5
  42. package/dist/lib/pty-server.d.ts +24 -1
  43. package/dist/lib/pty-server.js +109 -29
  44. package/dist/lib/resources/rules.js +1 -1
  45. package/dist/lib/resources/skills.js +1 -1
  46. package/dist/lib/resources.d.ts +2 -0
  47. package/dist/lib/resources.js +2 -1
  48. package/dist/lib/rotate.js +6 -18
  49. package/dist/lib/run-config.d.ts +9 -0
  50. package/dist/lib/run-config.js +35 -0
  51. package/dist/lib/run-defaults.d.ts +42 -0
  52. package/dist/lib/run-defaults.js +180 -0
  53. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  54. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  55. package/dist/lib/secrets/install-helper.d.ts +11 -3
  56. package/dist/lib/secrets/install-helper.js +48 -6
  57. package/dist/lib/secrets/linux.d.ts +12 -0
  58. package/dist/lib/secrets/linux.js +30 -16
  59. package/dist/lib/session/artifacts.js +8 -2
  60. package/dist/lib/shims.d.ts +9 -1
  61. package/dist/lib/shims.js +80 -3
  62. package/dist/lib/staleness/detectors/hooks.js +1 -1
  63. package/dist/lib/staleness/writers/hooks.js +1 -1
  64. package/dist/lib/teams/agents.js +5 -7
  65. package/dist/lib/teams/api.d.ts +67 -0
  66. package/dist/lib/teams/api.js +78 -0
  67. package/dist/lib/types.d.ts +15 -6
  68. package/dist/lib/versions.js +4 -4
  69. package/package.json +5 -2
  70. package/scripts/postinstall.js +18 -1
@@ -15,6 +15,7 @@ import chalk from 'chalk';
15
15
  import ora from 'ora';
16
16
  import { SESSION_AGENTS } from '../lib/session/types.js';
17
17
  import { discoverArtifacts, readArtifact, resolveArtifact } from '../lib/session/artifacts.js';
18
+ import { looksLikePath, toComparablePath, homeDir } from '../lib/platform/index.js';
18
19
  import { getActiveSessions } from '../lib/session/active.js';
19
20
  import { discoverSessions, countSessionsInScope, resolveSessionById, searchContentIndex } from '../lib/session/discover.js';
20
21
  import { filterTeamSessions } from '../lib/session/team-filter.js';
@@ -69,15 +70,6 @@ function createScanProgressTracker(verbs, suffix, spinner) {
69
70
  }
70
71
  const PICKER_RECENT_COUNT = 15;
71
72
  const PICKER_POOL_LIMIT = 200;
72
- /**
73
- * Detect whether a positional argument looks like a filesystem path.
74
- * Naked paths (., ./, ../, /, ~) filter sessions by project directory.
75
- * Everything else is treated as a search query string.
76
- */
77
- function isPathLike(query) {
78
- return query === '.' || query.startsWith('./') || query.startsWith('../')
79
- || query.startsWith('/') || query.startsWith('~');
80
- }
81
73
  /**
82
74
  * Resolve a path-like query to an absolute directory path.
83
75
  */
@@ -153,8 +145,13 @@ function contextColor(context) {
153
145
  function shortCwd(cwd) {
154
146
  if (!cwd)
155
147
  return '-';
156
- const home = os.homedir();
157
- return cwd.startsWith(home) ? '~' + cwd.slice(home.length) : cwd;
148
+ const home = homeDir();
149
+ // Compare in normalized form so the `~` shorthand also lands on Windows
150
+ // (case-insensitive, backslash paths); on POSIX this is byte-identical to the
151
+ // previous `cwd.startsWith(home)`. The displayed tail keeps original casing.
152
+ return toComparablePath(cwd).startsWith(toComparablePath(home))
153
+ ? '~' + cwd.slice(home.length)
154
+ : cwd;
158
155
  }
159
156
  function formatStartedAt(startedAtMs) {
160
157
  if (!startedAtMs)
@@ -334,7 +331,7 @@ async function sessionsAction(query, options) {
334
331
  // Path-like queries filter by project directory instead of text search.
335
332
  let pathFilter;
336
333
  let searchQuery;
337
- if (query && isPathLike(query)) {
334
+ if (query && looksLikePath(query)) {
338
335
  const resolved = resolvePathFilter(query);
339
336
  if (!fs.existsSync(resolved)) {
340
337
  console.log(chalk.yellow(`Path not found: ${resolved}`));
@@ -14,7 +14,7 @@ import { DEFAULT_SYSTEM_REPO, systemRepoSlug } from '../lib/types.js';
14
14
  import { getAgentsDir, getVersionsDir, ensureAgentsDir } from '../lib/state.js';
15
15
  import { isGitRepo, cloneIntoExisting, pullRepo } from '../lib/git.js';
16
16
  import { isPromptCancelled, isInteractiveTerminal } from './utils.js';
17
- import { AGENTS, getUnmanagedAgentInstalls, countSessionFiles, agentLabel } from '../lib/agents.js';
17
+ import { AGENTS, agentConfigDirName, getUnmanagedAgentInstalls, countSessionFiles, agentLabel } from '../lib/agents.js';
18
18
  import { setGlobalDefault } from '../lib/versions.js';
19
19
  import { ensureShimCurrent, switchHomeFileSymlinks, isShimsInPath, addShimsToPath, getPathSetupInstructions } from '../lib/shims.js';
20
20
  import { setHelpSections } from '../lib/help.js';
@@ -28,7 +28,7 @@ async function importAgent(agentId, version) {
28
28
  const configDir = agent.configDir;
29
29
  const versionsDir = getVersionsDir();
30
30
  const versionHome = path.join(versionsDir, agentId, version, 'home');
31
- const versionConfigDir = path.join(versionHome, `.${agentId}`);
31
+ const versionConfigDir = path.join(versionHome, agentConfigDirName(agentId));
32
32
  // Skip if version dir already exists (collision)
33
33
  if (fs.existsSync(versionConfigDir)) {
34
34
  return { success: false, skipped: true, error: `${version} already installed` };
@@ -4,7 +4,7 @@ import * as path from 'path';
4
4
  import { AgentManager, checkAllClis, getAgentsDir, VALID_TASK_TYPES, } from '../lib/teams/agents.js';
5
5
  import { resolveProvider } from '../lib/cloud/registry.js';
6
6
  import { runSupervisor } from '../lib/teams/supervisor.js';
7
- import { handleSpawn, handleStatus, handleStop, handleTasks, } from '../lib/teams/api.js';
7
+ import { handleSpawn, handleStatus, handleStop, handleTasks, toTaskStatusSummary, } from '../lib/teams/api.js';
8
8
  import { createTeam, ensureTeam, getTeam, loadTeams, removeTeam, teamExists, } from '../lib/teams/registry.js';
9
9
  import { setHelpSections } from '../lib/help.js';
10
10
  import { createWorktree, isGitRepo, hasUncommittedChanges, removeWorktree, } from '../lib/teams/worktree.js';
@@ -382,6 +382,55 @@ async function resolveTeammateSessions(agents) {
382
382
  }
383
383
  return map;
384
384
  }
385
+ // Default compact renderer — one block per teammate, optimized for the
386
+ // orchestrator scanning "what state, what did you touch, what did you say
387
+ // last." Caller passes the projected AgentStatusSummary; for the full
388
+ // verbose layout use printAgentDetail above.
389
+ function printAgentSummary(s) {
390
+ const label = statusColor(s.status)(s.status.toUpperCase());
391
+ const handle = s.name ?? shortId(s.agent_id);
392
+ const ident = s.name ? chalk.gray(`(${shortId(s.agent_id)})`) : '';
393
+ const duration = s.duration ? `${chalk.gray(' · ')}${chalk.white(s.duration)}` : '';
394
+ const errBadge = s.has_errors ? chalk.red(' !') : '';
395
+ const tools = chalk.gray(` · ${s.tool_count} tools`);
396
+ console.log(` ${chalk.cyan(handle.padEnd(14))} ${ident.padEnd(11)} ${label}${duration}${tools}${errBadge}`);
397
+ // Files: counts + basenames. Read is count only.
398
+ const fileLines = [];
399
+ const renderCat = (label, cat) => {
400
+ if (cat.count === 0)
401
+ return;
402
+ const more = cat.count > cat.names.length ? ` +${cat.count - cat.names.length}` : '';
403
+ const names = cat.names.length ? ` ${cat.names.join(', ')}${more}` : '';
404
+ fileLines.push(`${label} ${cat.count}${names}`);
405
+ };
406
+ renderCat('modified', s.files.modified);
407
+ renderCat('created', s.files.created);
408
+ renderCat('deleted', s.files.deleted);
409
+ if (s.files.read.count > 0)
410
+ fileLines.push(`read ${s.files.read.count}`);
411
+ if (fileLines.length) {
412
+ console.log(` ${chalk.gray('files ')} ${fileLines.join(chalk.gray(' · '))}`);
413
+ }
414
+ // Last 3 bash commands.
415
+ const recentBash = s.bash_commands.slice(-3);
416
+ if (recentBash.length) {
417
+ console.log(` ${chalk.gray('bash ')}`);
418
+ for (const cmd of recentBash) {
419
+ console.log(` ${chalk.gray('$')} ${truncate(cmd, 96)}`);
420
+ }
421
+ }
422
+ // Last messages — first non-empty line of each, truncated.
423
+ if (s.last_messages.length) {
424
+ console.log(` ${chalk.gray('messages')}`);
425
+ for (const msg of s.last_messages) {
426
+ const firstLine = msg.split(/\r?\n/).find((l) => l.trim()) || '';
427
+ if (firstLine)
428
+ console.log(` ${chalk.gray('>')} ${truncate(firstLine, 96)}`);
429
+ }
430
+ }
431
+ if (s.pr_url)
432
+ console.log(` ${chalk.gray('PR ')} ${chalk.cyan(s.pr_url)}`);
433
+ }
385
434
  // Render a team's status in the same format the `status` subcommand uses, so
386
435
  // the interactive picker's Enter action drops the user into a familiar view.
387
436
  async function printTeamStatus(team, result) {
@@ -409,6 +458,36 @@ async function printTeamStatus(team, result) {
409
458
  console.log();
410
459
  console.log(chalk.gray(`cursor: ${result.cursor}`));
411
460
  }
461
+ // Compact default renderer — no session-file dive, no per-teammate
462
+ // 15-line preview. One block per teammate, suitable for the orchestrator
463
+ // scanning what each agent did. Use `printTeamStatus` (above) for the
464
+ // verbose/legacy layout.
465
+ function printTeamSummary(team, result) {
466
+ const { summary, agents } = result;
467
+ console.log(chalk.bold(`Team ${chalk.cyan(team)} `) +
468
+ chalk.gray(summary.pending > 0
469
+ ? `(${summary.pending} pending, ${summary.running} working, ${summary.completed} done, ${summary.failed} failed, ${summary.stopped} stopped)`
470
+ : `(${summary.running} working, ${summary.completed} done, ${summary.failed} failed, ${summary.stopped} stopped)`));
471
+ if (agents.length === 0) {
472
+ console.log(chalk.gray(' (no teammates yet — add one with `agents teams add`)'));
473
+ }
474
+ else {
475
+ const width = Math.min(process.stdout.columns || 80, 80);
476
+ const divider = chalk.gray('┈'.repeat(width));
477
+ for (let i = 0; i < agents.length; i++) {
478
+ console.log();
479
+ if (i > 0) {
480
+ console.log(divider);
481
+ console.log();
482
+ }
483
+ printAgentSummary(agents[i]);
484
+ }
485
+ }
486
+ console.log();
487
+ console.log(chalk.gray(`cursor: ${result.cursor}`));
488
+ console.log(chalk.gray('Full detail: agents teams status ' + team + ' --verbose'));
489
+ console.log(chalk.gray('Raw log: agents teams logs --team ' + team + ' --teammate <name>'));
490
+ }
412
491
  // Classify a team into a single bucket for --status filtering.
413
492
  // - empty: no teammates (created but nobody added yet)
414
493
  // - waiting: only staged teammates — call `teams start` to kick them off
@@ -964,10 +1043,11 @@ export function registerTeamsCommands(program) {
964
1043
  teams
965
1044
  .command('status [team]')
966
1045
  .aliases(['s', 'st', 'check'])
967
- .description("Check in on a team: who's working, what files they touched, recent commands, last output. Pass --since for efficient delta polling.")
1046
+ .description("Check in on a team: status, files touched, recent commands, last messages. Pass --verbose for the full per-teammate dump; --since for delta polling.")
968
1047
  .option('-f, --filter <state>', 'Show only teammates in this state: running, completed, failed, stopped, or all (default: all)', 'all')
969
1048
  .option('-s, --since <iso>', 'Cursor from a previous status call; only show updates after this timestamp (enables efficient polling)')
970
1049
  .option('--agent-id <id>', 'Show only this one teammate (by UUID or UUID prefix)')
1050
+ .option('-v, --verbose', 'Emit the full per-teammate detail (prompt, all file paths, all messages). Default is a compact summary.')
971
1051
  .option('--json', 'Output machine-readable JSON')
972
1052
  .action(async (team, opts) => {
973
1053
  const filter = opts.filter;
@@ -984,8 +1064,15 @@ export function registerTeamsCommands(program) {
984
1064
  const agents = opts.agentId
985
1065
  ? result.agents.filter((a) => a.agent_id.startsWith(opts.agentId))
986
1066
  : result.agents;
1067
+ const filtered = { ...result, agents };
987
1068
  if (isJsonMode(opts)) {
988
- console.log(JSON.stringify({ ...result, agents }, null, 2));
1069
+ // JSON output also respects --verbose. Default = compact summary
1070
+ // shape; --verbose = full AgentStatusDetail. Same toggle covers
1071
+ // both text and JSON so MCP-style consumers can opt into detail.
1072
+ const payload = opts.verbose
1073
+ ? filtered
1074
+ : toTaskStatusSummary(filtered);
1075
+ console.log(JSON.stringify(payload, null, 2));
989
1076
  return;
990
1077
  }
991
1078
  const exists = await teamExists(team);
@@ -993,7 +1080,12 @@ export function registerTeamsCommands(program) {
993
1080
  console.log(chalk.yellow(`No team called '${team}'. Create it with: agents teams create ${team}`));
994
1081
  return;
995
1082
  }
996
- await printTeamStatus(team, { ...result, agents });
1083
+ if (opts.verbose) {
1084
+ await printTeamStatus(team, filtered);
1085
+ }
1086
+ else {
1087
+ printTeamSummary(team, toTaskStatusSummary(filtered));
1088
+ }
997
1089
  }
998
1090
  catch (err) {
999
1091
  die(`Could not check on team ${team}: ${err.message}`);
@@ -1308,14 +1400,19 @@ export function registerTeamsCommands(program) {
1308
1400
  teams
1309
1401
  .command('logs [teammate]')
1310
1402
  .alias('log')
1311
- .description("Read a teammate's raw log output. Accepts name, UUID, or UUID prefix.")
1403
+ .description("Read a teammate's raw log output. Accepts positional name, --teammate <name>, UUID, or UUID prefix.")
1312
1404
  .option('-n, --tail <n>', 'Show only the last N lines instead of the full log')
1313
1405
  .option('--team <team>', 'Disambiguate when the same name appears in multiple teams')
1406
+ .option('--teammate <name>', 'Teammate name (alias for the positional arg; useful for scripts)')
1314
1407
  .action(async (ref, opts) => {
1315
1408
  const base = await getAgentsDir();
1316
- // No teammate picker in TTY, hard fail outside.
1409
+ // Resolve teammate identity. Precedence:
1410
+ // 1. positional `[teammate]` arg (back-compat, most common)
1411
+ // 2. --teammate <name> flag (script-friendly alias)
1412
+ // 3. interactive picker (TTY only)
1413
+ const teammateRef = ref ?? opts.teammate;
1317
1414
  let agentId;
1318
- if (!ref) {
1415
+ if (!teammateRef) {
1319
1416
  const mgr = mkManager();
1320
1417
  const picked = await pickTeammateOr(mgr, 'agents teams logs');
1321
1418
  if (!picked)
@@ -1323,13 +1420,13 @@ export function registerTeamsCommands(program) {
1323
1420
  agentId = picked.agentId;
1324
1421
  }
1325
1422
  else {
1326
- const resolved = await resolveTeammateAcrossTeams(base, ref, opts.team);
1423
+ const resolved = await resolveTeammateAcrossTeams(base, teammateRef, opts.team);
1327
1424
  if (resolved.kind === 'none') {
1328
- die(`No notes on record for teammate '${ref}'`, 2);
1425
+ die(`No notes on record for teammate '${teammateRef}'`, 2);
1329
1426
  }
1330
1427
  if (resolved.kind === 'ambiguous') {
1331
1428
  const hints = resolved.candidates.map((c) => `${c.team}/${c.display}`).join(', ');
1332
- die(`'${ref}' matches multiple teammates: ${hints}.\n` +
1429
+ die(`'${teammateRef}' matches multiple teammates: ${hints}.\n` +
1333
1430
  ` Narrow it with --team <team>, or pass a UUID prefix.`, 2);
1334
1431
  }
1335
1432
  agentId = resolved.agentId;
@@ -1346,7 +1443,7 @@ export function registerTeamsCommands(program) {
1346
1443
  process.stdout.write(lines.slice(-n).join('\n'));
1347
1444
  }
1348
1445
  catch {
1349
- die(`No notes on record for teammate '${ref ?? agentId}' (looked in ${logPath})`, 2);
1446
+ die(`No notes on record for teammate '${teammateRef ?? agentId}' (looked in ${logPath})`, 2);
1350
1447
  }
1351
1448
  });
1352
1449
  // doctor
@@ -9,6 +9,17 @@
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
+ /** Per-section filter flags. When any are true, only those sections render. */
13
+ export interface ViewSectionFilter {
14
+ commands?: boolean;
15
+ skills?: boolean;
16
+ mcp?: boolean;
17
+ workflows?: boolean;
18
+ plugins?: boolean;
19
+ rules?: boolean;
20
+ hooks?: boolean;
21
+ promptcuts?: boolean;
22
+ }
12
23
  /** Machine-readable entry for a single installed version. */
13
24
  export interface ViewJsonVersion {
14
25
  version: string;
@@ -54,6 +65,6 @@ export declare function viewAction(agentArg?: string, options?: {
54
65
  prune?: boolean;
55
66
  yes?: boolean;
56
67
  dryRun?: boolean;
57
- }): Promise<void>;
68
+ } & ViewSectionFilter): Promise<void>;
58
69
  /** Register the `agents view` command. */
59
70
  export declare function registerViewCommand(program: Command): void;
@@ -15,6 +15,7 @@ import { isGitRepo, getGitSyncStatus } from '../lib/git.js';
15
15
  import { getCentralRulesFileName } from '../lib/rules/rules.js';
16
16
  import { composeRulesFromState } from '../lib/rules/compose.js';
17
17
  import { getConfiguredRunStrategy } from '../lib/rotate.js';
18
+ import { resolveRunDefaults } from '../lib/run-defaults.js';
18
19
  import { listProfiles, profileSummary } from '../lib/profiles.js';
19
20
  import { loadManifest, isStale } from '../lib/staleness/index.js';
20
21
  import { confirm } from '@inquirer/prompts';
@@ -113,6 +114,29 @@ function getProjectVersionFromCwd(agent) {
113
114
  return null;
114
115
  }
115
116
  }
117
+ const SECTION_KEYS = ['commands', 'skills', 'mcp', 'workflows', 'plugins', 'rules', 'hooks', 'promptcuts'];
118
+ /**
119
+ * Decide whether a section should render given the filter. If no flags are set,
120
+ * everything renders (current behavior). If any flag is set, only those sections
121
+ * render — flags are additive.
122
+ */
123
+ function shouldRenderSection(key, filter) {
124
+ if (!filter)
125
+ return true;
126
+ const anySet = SECTION_KEYS.some((k) => filter[k]);
127
+ if (!anySet)
128
+ return true;
129
+ return filter[key] === true;
130
+ }
131
+ /** Trim a description to a column-friendly snippet. Strips newlines, collapses whitespace. */
132
+ function summarizeDescription(desc, maxLen = 80) {
133
+ if (!desc)
134
+ return '';
135
+ const cleaned = desc.replace(/\s+/g, ' ').trim();
136
+ if (cleaned.length <= maxLen)
137
+ return cleaned;
138
+ return cleaned.slice(0, maxLen - 1).trimEnd() + '…';
139
+ }
116
140
  function getProfileSummaries(filterAgentId) {
117
141
  return listProfiles()
118
142
  .filter((profile) => !filterAgentId || profile.host.agent === filterAgentId)
@@ -338,6 +362,12 @@ async function showInstalledVersions(filterAgentId) {
338
362
  // Otherwise it reflects install time (misleading "just now" for fresh installs).
339
363
  const activeStr = vInfo && hasEmail ? formatLastActive(vInfo.lastActive) : '';
340
364
  const hasActive = activeStr.length > 0;
365
+ const runDefaults = resolveRunDefaults(agentId, version);
366
+ const runDefaultBits = [];
367
+ if (runDefaults.mode)
368
+ runDefaultBits.push(`mode:${runDefaults.mode}`);
369
+ if (runDefaults.model)
370
+ runDefaultBits.push(`model:${runDefaults.model}`);
341
371
  if (!hasEmail && !hasUsage) {
342
372
  // Installed but never signed in
343
373
  parts.push(chalk.gray('(not signed in — run ' + agent.cliCommand + ' to log in)'));
@@ -359,6 +389,9 @@ async function showInstalledVersions(filterAgentId) {
359
389
  if (hasActive)
360
390
  parts.push(activeStr);
361
391
  }
392
+ if (runDefaultBits.length > 0) {
393
+ parts.push(chalk.gray(`run ${runDefaultBits.join(' ')}`));
394
+ }
362
395
  console.log(parts.join(' '));
363
396
  if (showPaths) {
364
397
  const versionDir = getVersionDir(agentId, version);
@@ -568,7 +601,7 @@ async function showInstalledVersions(filterAgentId) {
568
601
  * Show detailed resources for a specific agent version.
569
602
  * Called when: `agents view claude@2.0.65` or `agents view claude@default`
570
603
  */
571
- async function showAgentResources(agentId, requestedVersion) {
604
+ async function showAgentResources(agentId, requestedVersion, filter) {
572
605
  const spinner = ora({ text: 'Loading...', isSilent: !process.stdout.isTTY }).start();
573
606
  const cwd = process.cwd();
574
607
  const agentsDir = getAgentsDir();
@@ -660,6 +693,8 @@ async function showAgentResources(agentId, requestedVersion) {
660
693
  })),
661
694
  skills: resources.skills.map(r => ({
662
695
  ...r,
696
+ // ruleCount of 0 is noise — every skill has 0 unless it ships subrules, which is rare.
697
+ ruleCount: r.ruleCount && r.ruleCount > 0 ? r.ruleCount : undefined,
663
698
  syncState: r.scope === 'project' ? undefined : getSyncState(r.name, 'skills', skillsSync),
664
699
  })),
665
700
  skillErrors: resources.skillErrors,
@@ -705,7 +740,9 @@ async function showAgentResources(agentId, requestedVersion) {
705
740
  : chalk.gray('[system]');
706
741
  display += ` ${sourceTag}`;
707
742
  const syncStr = r.syncState ? chalk.gray(` [${r.syncState}]`) : '';
708
- console.log(` ${display}${syncStr}`);
743
+ const descSnippet = summarizeDescription(r.description);
744
+ const descStr = descSnippet ? chalk.gray(` ${descSnippet}`) : '';
745
+ console.log(` ${display}${syncStr}${descStr}`);
709
746
  }
710
747
  }
711
748
  // Render promptcuts (cross-agent, not per-version). Shortcuts are layered
@@ -722,43 +759,53 @@ async function showAgentResources(agentId, requestedVersion) {
722
759
  const label = `${count} shortcut${count === 1 ? '' : 's'}`;
723
760
  console.log(` ${chalk.green(label).padEnd(24)} ${chalk.gray(formatPath(getEffectivePromptcutsPath(), cwd))}`);
724
761
  }
725
- // 1. Agent CLI info
726
- console.log(chalk.bold('Agent CLIs\n'));
727
- const accountInfo = await getAccountInfo(agentId, home);
728
- const usageInfo = await getUsageInfoForIdentity({
729
- agentId,
730
- home,
731
- cliVersion: version,
732
- info: accountInfo,
733
- });
734
- const emailStr = accountInfo.email ? chalk.cyan(` ${accountInfo.email}`) : '';
735
- const status = chalk.green(version);
736
- const usageStr = formatUsageSummary(accountInfo.plan, null);
737
- const usagePart = usageStr ? ` ${usageStr}` : '';
738
- console.log(` ${colorAgent(agentId)(AGENTS[agentId].name.padEnd(14))} ${status}${emailStr}${usagePart}`);
739
- const usageLines = formatUsageSection(usageInfo);
740
- if (usageLines.length > 0) {
741
- console.log();
742
- for (const line of usageLines) {
743
- console.log(line);
762
+ const anyFilterSet = filter && SECTION_KEYS.some((k) => filter[k]);
763
+ // 1. Agent CLI info — skip the header entirely when the user asked for a
764
+ // specific section. They want "nothing more or less."
765
+ if (!anyFilterSet) {
766
+ console.log(chalk.bold('Agent CLIs\n'));
767
+ const accountInfo = await getAccountInfo(agentId, home);
768
+ const usageInfo = await getUsageInfoForIdentity({
769
+ agentId,
770
+ home,
771
+ cliVersion: version,
772
+ info: accountInfo,
773
+ });
774
+ const emailStr = accountInfo.email ? chalk.cyan(` ${accountInfo.email}`) : '';
775
+ const status = chalk.green(version);
776
+ const usageStr = formatUsageSummary(accountInfo.plan, null);
777
+ const usagePart = usageStr ? ` ${usageStr}` : '';
778
+ console.log(` ${colorAgent(agentId)(AGENTS[agentId].name.padEnd(14))} ${status}${emailStr}${usagePart}`);
779
+ const usageLines = formatUsageSection(usageInfo);
780
+ if (usageLines.length > 0) {
781
+ console.log();
782
+ for (const line of usageLines) {
783
+ console.log(line);
784
+ }
744
785
  }
745
786
  }
746
787
  // 2. Resources
747
- renderSection('Commands', agentData.commands);
748
- renderSection('Skills', agentData.skills);
749
- // Show skill parse errors if any
750
- if (agentData.skillErrors.length > 0) {
751
- console.log(`\n ${chalk.red('Skill Errors')}:`);
752
- for (const err of agentData.skillErrors) {
753
- console.log(` ${chalk.red(err.name.padEnd(20))} ${chalk.gray(err.error)}`);
754
- console.log(` ${chalk.gray(formatPath(err.path, cwd))}`);
788
+ if (shouldRenderSection('commands', filter)) {
789
+ renderSection('Commands', agentData.commands);
790
+ }
791
+ if (shouldRenderSection('skills', filter)) {
792
+ renderSection('Skills', agentData.skills);
793
+ // Show skill parse errors only when skills section is visible
794
+ if (agentData.skillErrors.length > 0) {
795
+ console.log(`\n ${chalk.red('Skill Errors')}:`);
796
+ for (const err of agentData.skillErrors) {
797
+ console.log(` ${chalk.red(err.name.padEnd(20))} ${chalk.gray(err.error)}`);
798
+ console.log(` ${chalk.gray(formatPath(err.path, cwd))}`);
799
+ }
755
800
  }
756
801
  }
757
- renderSection('MCP Servers', agentData.mcp);
758
- if (isCapable(agentId, 'workflows')) {
802
+ if (shouldRenderSection('mcp', filter)) {
803
+ renderSection('MCP Servers', agentData.mcp);
804
+ }
805
+ if (shouldRenderSection('workflows', filter) && isCapable(agentId, 'workflows')) {
759
806
  renderSection('Workflows', agentData.workflows);
760
807
  }
761
- if (isCapable(agentId, 'plugins')) {
808
+ if (shouldRenderSection('plugins', filter) && isCapable(agentId, 'plugins')) {
762
809
  const plugins = discoverPlugins().filter(p => pluginSupportsAgent(p, agentId));
763
810
  console.log(chalk.bold('\nPlugins\n'));
764
811
  if (plugins.length === 0) {
@@ -847,11 +894,18 @@ async function showAgentResources(agentId, requestedVersion) {
847
894
  }
848
895
  }
849
896
  }
850
- renderRulesSection();
851
- renderSection('Hooks', agentData.hooks);
852
- renderPromptcuts();
853
- // Show legend at the end if git repo exists
854
- if (hasGitRepo) {
897
+ if (shouldRenderSection('rules', filter)) {
898
+ renderRulesSection();
899
+ }
900
+ if (shouldRenderSection('hooks', filter)) {
901
+ renderSection('Hooks', agentData.hooks);
902
+ }
903
+ if (shouldRenderSection('promptcuts', filter)) {
904
+ renderPromptcuts();
905
+ }
906
+ // Show legend at the end if git repo exists and we showed all sections.
907
+ // Filtered single-section views skip it — noise for promptcuts or plugins.
908
+ if (hasGitRepo && !anyFilterSet) {
855
909
  console.log();
856
910
  console.log(chalk.gray('Legend:'), chalk.green('Tracked'), chalk.blue('Local-only'), chalk.yellow('Modified'), chalk.red('Deleted'));
857
911
  }
@@ -1131,6 +1185,17 @@ export async function viewAction(agentArg, options) {
1131
1185
  const prune = options?.prune === true;
1132
1186
  const yes = options?.yes === true;
1133
1187
  const dryRun = options?.dryRun === true;
1188
+ const filter = {
1189
+ commands: options?.commands,
1190
+ skills: options?.skills,
1191
+ mcp: options?.mcp,
1192
+ workflows: options?.workflows,
1193
+ plugins: options?.plugins,
1194
+ rules: options?.rules,
1195
+ hooks: options?.hooks,
1196
+ promptcuts: options?.promptcuts,
1197
+ };
1198
+ const filterIsSet = SECTION_KEYS.some((k) => filter[k]);
1134
1199
  if (!agentArg) {
1135
1200
  if (prune) {
1136
1201
  await pruneDuplicates(undefined, yes, dryRun);
@@ -1180,7 +1245,12 @@ export async function viewAction(agentArg, options) {
1180
1245
  }
1181
1246
  if (requestedVersion) {
1182
1247
  // Specific version requested: show detailed resources
1183
- await showAgentResources(agentId, requestedVersion);
1248
+ await showAgentResources(agentId, requestedVersion, filter);
1249
+ }
1250
+ else if (filterIsSet) {
1251
+ // `agents view claude --skills` → fall through to detail view on default.
1252
+ // Section filters only make sense for the per-version detail view.
1253
+ await showAgentResources(agentId, 'default', filter);
1184
1254
  }
1185
1255
  else {
1186
1256
  // Just agent name: show versions for that agent
@@ -1196,6 +1266,14 @@ export function registerViewCommand(program) {
1196
1266
  .option('--prune', 'Remove older installed versions that share an account with a newer installed version. Skips the global default.')
1197
1267
  .option('--dry-run', 'With --prune, show duplicate versions without deleting')
1198
1268
  .option('-y, --yes', 'Skip the prune confirmation prompt.')
1269
+ .option('--commands', 'Show only commands in the detail view.')
1270
+ .option('--skills', 'Show only skills in the detail view.')
1271
+ .option('--mcp', 'Show only MCP servers in the detail view.')
1272
+ .option('--workflows', 'Show only workflows in the detail view.')
1273
+ .option('--plugins', 'Show only plugins in the detail view.')
1274
+ .option('--rules', 'Show only rules in the detail view.')
1275
+ .option('--hooks', 'Show only hooks in the detail view.')
1276
+ .option('--promptcuts', 'Show only promptcuts in the detail view.')
1199
1277
  .addHelpText('after', `
1200
1278
  Examples:
1201
1279
  # Show all installed agents with versions, accounts, and usage
@@ -1216,6 +1294,11 @@ Examples:
1216
1294
  agents view claude --prune
1217
1295
  agents view claude --prune -y
1218
1296
 
1297
+ # Filter the detail view to a single section (combinable)
1298
+ agents view claude@default --skills
1299
+ agents view claude@default --plugins --workflows
1300
+ agents view claude --commands # implicitly the default version
1301
+
1219
1302
  When to use:
1220
1303
  - Checking which agents are installed and what their default versions are
1221
1304
  - Seeing which account each version is logged into (useful for multi-account setups)