@phnx-labs/agents-cli 1.20.5 → 1.20.6

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 (49) hide show
  1. package/README.md +1 -1
  2. package/dist/commands/browser.js +31 -4
  3. package/dist/commands/computer.js +10 -2
  4. package/dist/commands/defaults.d.ts +7 -0
  5. package/dist/commands/defaults.js +89 -0
  6. package/dist/commands/exec.js +24 -6
  7. package/dist/commands/rules.js +3 -3
  8. package/dist/commands/secrets.js +46 -9
  9. package/dist/commands/setup.js +2 -2
  10. package/dist/commands/teams.js +108 -11
  11. package/dist/commands/view.d.ts +12 -1
  12. package/dist/commands/view.js +121 -38
  13. package/dist/index.js +38 -21
  14. package/dist/lib/agents.d.ts +10 -6
  15. package/dist/lib/agents.js +23 -14
  16. package/dist/lib/browser/chrome.d.ts +10 -0
  17. package/dist/lib/browser/chrome.js +84 -3
  18. package/dist/lib/exec.js +24 -4
  19. package/dist/lib/migrate.js +6 -4
  20. package/dist/lib/permissions.d.ts +23 -0
  21. package/dist/lib/permissions.js +89 -7
  22. package/dist/lib/plugin-marketplace.js +1 -1
  23. package/dist/lib/project-launch.d.ts +5 -0
  24. package/dist/lib/project-launch.js +37 -0
  25. package/dist/lib/pty-server.js +7 -4
  26. package/dist/lib/resources/rules.js +1 -1
  27. package/dist/lib/resources/skills.js +1 -1
  28. package/dist/lib/resources.d.ts +2 -0
  29. package/dist/lib/resources.js +2 -1
  30. package/dist/lib/rotate.js +6 -18
  31. package/dist/lib/run-config.d.ts +9 -0
  32. package/dist/lib/run-config.js +35 -0
  33. package/dist/lib/run-defaults.d.ts +42 -0
  34. package/dist/lib/run-defaults.js +180 -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/install-helper.d.ts +11 -3
  38. package/dist/lib/secrets/install-helper.js +48 -6
  39. package/dist/lib/secrets/linux.d.ts +12 -0
  40. package/dist/lib/secrets/linux.js +30 -16
  41. package/dist/lib/shims.d.ts +9 -1
  42. package/dist/lib/shims.js +35 -3
  43. package/dist/lib/staleness/detectors/hooks.js +1 -1
  44. package/dist/lib/staleness/writers/hooks.js +1 -1
  45. package/dist/lib/teams/api.d.ts +67 -0
  46. package/dist/lib/teams/api.js +78 -0
  47. package/dist/lib/types.d.ts +15 -6
  48. package/dist/lib/versions.js +4 -4
  49. package/package.json +5 -2
package/README.md CHANGED
@@ -243,7 +243,7 @@ agents teams status auth-feature # Who's working, what they changed, what the
243
243
 
244
244
  Teammates run detached -- close your terminal, they keep working. Check in with `teams status`, read full output with `teams logs <name>`, clean up with `teams disband`.
245
245
 
246
- Team state is observable via `agents teams list --json` / `agents teams status --json`. External tools join it with `sessions --json` (teammates get `isTeamOrigin: true`) and `cloud list --json` (for `--cloud` teammates) to build a unified fleet view. See [docs/06-observability.md](docs/06-observability.md).
246
+ Team state is observable via `agents teams list --json` / `agents teams status --json` (compact by default; add `--verbose` for the full per-teammate shape). External tools join it with `sessions --json` (teammates get `isTeamOrigin: true`) and `cloud list --json` (for `--cloud` teammates) to build a unified fleet view. See [docs/06-observability.md](docs/06-observability.md).
247
247
 
248
248
  ---
249
249
 
@@ -1,7 +1,7 @@
1
1
  import * as fs from 'fs';
2
2
  import * as path from 'path';
3
3
  import { listProfiles, getProfile, createProfile, deleteProfile, ensureDefaultBrowserProfile, getProfileRuntimeDir, extractConfiguredPort, findFreeProfilePort, getEndpointPresets, } from '../lib/browser/profiles.js';
4
- import { findBrowserPath, getPortOccupant } from '../lib/browser/chrome.js';
4
+ import { findBrowserPath, getPortOccupant, isLauncherScript } from '../lib/browser/chrome.js';
5
5
  import { listProfileCacheDirs, removeProfileCache, listAllProfileSnapshots, } from '../lib/browser/runtime-state.js';
6
6
  import { DEFAULT_VIEWPORT } from '../lib/browser/devices.js';
7
7
  import { discoverBrowserWsUrl, verifyBrowserIdentity } from '../lib/browser/cdp.js';
@@ -311,10 +311,28 @@ function registerProfilesCommands(browser) {
311
311
  process.exit(1);
312
312
  }
313
313
  const checks = [];
314
- // 1. Binary exists for declared browser type
314
+ // 1. Binary exists for declared browser type, and is a real executable we
315
+ // can drive — not a distro launcher script. findBrowserPath already
316
+ // unwraps the known Chromium wrappers to their ELF; if it still hands
317
+ // back a shebang script we couldn't resolve, `start` would fail with
318
+ // `CDP connection closed` (the wrapper re-execs the browser as a child,
319
+ // breaking the --remote-debugging-pipe transport — issue #229). Flag it
320
+ // here instead of letting launch fail opaquely.
315
321
  try {
316
322
  const binPath = findBrowserPath(profile.browser, profile.binary);
317
- checks.push({ label: 'binary', ok: true, detail: binPath });
323
+ if (isLauncherScript(binPath)) {
324
+ checks.push({
325
+ label: 'binary',
326
+ ok: false,
327
+ detail: `${binPath} is a launcher script, not the browser executable — ` +
328
+ `agents browser drives the browser over --remote-debugging-pipe and ` +
329
+ `can't attach to a wrapper that re-execs it. Point the profile at the ` +
330
+ `real binary (\`--binary /path/to/browser\`) or reinstall the standard package.`,
331
+ });
332
+ }
333
+ else {
334
+ checks.push({ label: 'binary', ok: true, detail: binPath });
335
+ }
318
336
  }
319
337
  catch (err) {
320
338
  checks.push({
@@ -351,7 +369,16 @@ function registerProfilesCommands(browser) {
351
369
  else {
352
370
  const occupant = getPortOccupant(port);
353
371
  if (!occupant) {
354
- checks.push({ label: 'port', ok: true, detail: `${port} is free` });
372
+ // A free port doesn't mean "ready to launch here": for a local
373
+ // profile we self-launch over an internal --remote-debugging-pipe and
374
+ // never bind this port. The port is consulted only to attach to a
375
+ // browser someone already started on it. Say so, so a green doctor
376
+ // can't be read as "the port is what launch depends on" (#229).
377
+ checks.push({
378
+ label: 'port',
379
+ ok: true,
380
+ detail: `${port} free — will self-launch over an internal pipe (port used only to attach to an already-running browser)`,
381
+ });
355
382
  }
356
383
  else {
357
384
  try {
@@ -12,8 +12,16 @@ const COMPUTER_HELP_GROUPS = [
12
12
  ];
13
13
  export function registerComputerCommand(program) {
14
14
  const computer = program
15
- .command('computer')
16
- .description('Drive macOS apps via Accessibility — list, screenshot, click, type');
15
+ .command('computers')
16
+ .description('Drive macOS apps via Accessibility — list, screenshot, click, type (macOS only)')
17
+ // The whole subsystem is macOS Accessibility / TCC. Fail fast with a clear
18
+ // message on other platforms instead of a downstream ENOENT / launchctl error.
19
+ .hook('preAction', () => {
20
+ if (process.platform !== 'darwin') {
21
+ console.error('agents computers: macOS only — it drives apps via the macOS Accessibility API.');
22
+ process.exit(1);
23
+ }
24
+ });
17
25
  registerComputerSubcommands(computer);
18
26
  registerCommandGroups(computer, COMPUTER_HELP_GROUPS);
19
27
  }
@@ -0,0 +1,7 @@
1
+ /**
2
+ * Defaults command tree.
3
+ *
4
+ * `agents defaults run ...` manages selector-based defaults for `agents run`.
5
+ */
6
+ import type { Command } from 'commander';
7
+ export declare function registerDefaultsCommands(program: Command): void;
@@ -0,0 +1,89 @@
1
+ /**
2
+ * Defaults command tree.
3
+ *
4
+ * `agents defaults run ...` manages selector-based defaults for `agents run`.
5
+ */
6
+ import chalk from 'chalk';
7
+ import { setHelpSections } from '../lib/help.js';
8
+ import { listRunDefaults, setRunDefault, unsetRunDefault, } from '../lib/run-defaults.js';
9
+ function formatRunDefault(entry) {
10
+ const parts = [];
11
+ if (entry.defaults.mode)
12
+ parts.push(`mode ${chalk.white(entry.defaults.mode)}`);
13
+ if (entry.defaults.model)
14
+ parts.push(`model ${chalk.white(entry.defaults.model)}`);
15
+ return `${chalk.cyan(entry.selector.padEnd(22))} ${parts.join(' ')}`;
16
+ }
17
+ export function registerDefaultsCommands(program) {
18
+ const defaults = program
19
+ .command('defaults')
20
+ .description('Manage default options for agents-cli commands');
21
+ const run = defaults
22
+ .command('run')
23
+ .description('Manage selector-based defaults for `agents run`');
24
+ setHelpSections(run, {
25
+ examples: `
26
+ agents defaults run list
27
+ agents defaults run set 'claude:*' --mode auto --model opus
28
+ agents defaults run set claude@2.1.45 --mode plan --model sonnet
29
+ agents defaults run unset 'claude:*'
30
+ `,
31
+ notes: `
32
+ Selectors use <agent>:<version>. Use * for all versions of an agent.
33
+ Exact selectors override wildcard selectors field by field.
34
+ Explicit flags on agents run always win over configured defaults.
35
+ `,
36
+ });
37
+ run
38
+ .command('list')
39
+ .description('List configured run defaults')
40
+ .action(() => {
41
+ const entries = listRunDefaults();
42
+ if (entries.length === 0) {
43
+ console.log(chalk.gray('No run defaults configured.'));
44
+ console.log(chalk.gray("Set one with: agents defaults run set 'claude:*' --mode auto --model opus"));
45
+ return;
46
+ }
47
+ console.log(chalk.bold('Run Defaults\n'));
48
+ for (const entry of entries) {
49
+ console.log(` ${formatRunDefault(entry)}`);
50
+ }
51
+ });
52
+ run
53
+ .command('set <selector>')
54
+ .description('Set defaults for an agent/version selector')
55
+ .option('--mode <mode>', "Default mode: plan, edit, auto, skip. 'full' accepted as alias for skip.")
56
+ .option('--model <model>', 'Default model or model alias, forwarded via --model')
57
+ .action((selector, options) => {
58
+ try {
59
+ const entry = setRunDefault(selector, {
60
+ ...(options.mode !== undefined ? { mode: options.mode } : {}),
61
+ ...(options.model !== undefined ? { model: options.model } : {}),
62
+ });
63
+ console.log(chalk.green('Set run default:'));
64
+ console.log(` ${formatRunDefault(entry)}`);
65
+ }
66
+ catch (err) {
67
+ console.error(chalk.red(err.message));
68
+ process.exit(1);
69
+ }
70
+ });
71
+ run
72
+ .command('unset <selector>')
73
+ .description('Remove defaults for an agent/version selector')
74
+ .action((selector) => {
75
+ try {
76
+ const removed = unsetRunDefault(selector);
77
+ if (removed) {
78
+ console.log(chalk.green(`Removed run default ${selector}`));
79
+ }
80
+ else {
81
+ console.log(chalk.gray(`No run default matched ${selector}`));
82
+ }
83
+ }
84
+ catch (err) {
85
+ console.error(chalk.red(err.message));
86
+ process.exit(1);
87
+ }
88
+ });
89
+ }
@@ -85,7 +85,7 @@ export function registerRunCommand(program) {
85
85
  `,
86
86
  });
87
87
  runCmd.action(async (agentSpec, prompt, options) => {
88
- const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef },] = await Promise.all([
88
+ const [{ buildExecCommand, parseExecEnv, execAgent, runWithFallback, normalizeMode, resolveMode, defaultModeFor }, { ALL_AGENT_IDS }, { profileExists, resolveProfileForRun }, { readAndResolveBundleEnv, describeBundle }, { getConfiguredRunStrategy, normalizeRunStrategy, resolveRunVersion, RUN_STRATEGIES }, { getGlobalDefault, getVersionHomePath, resolveVersion, resolveVersionAlias }, { buildDiscoveredPlugin, loadPluginManifest, syncPluginToVersion }, { parseWorkflowFrontmatter, resolveWorkflowRef }, { resolveRunDefaults },] = await Promise.all([
89
89
  import('../lib/exec.js'),
90
90
  import('../lib/agents.js'),
91
91
  import('../lib/profiles.js'),
@@ -94,6 +94,7 @@ export function registerRunCommand(program) {
94
94
  import('../lib/versions.js'),
95
95
  import('../lib/plugins.js'),
96
96
  import('../lib/workflows.js'),
97
+ import('../lib/run-defaults.js'),
97
98
  ]);
98
99
  const isValidAgent = (agent) => ALL_AGENT_IDS.includes(agent);
99
100
  // Parse agent@version
@@ -102,6 +103,7 @@ export function registerRunCommand(program) {
102
103
  let version = rawVersion || undefined;
103
104
  let profileEnv;
104
105
  let fromProfile = false;
106
+ let workflowModel;
105
107
  const cwd = options.cwd ?? process.cwd();
106
108
  if (isValidAgent(rawAgent)) {
107
109
  agent = rawAgent;
@@ -133,6 +135,10 @@ export function registerRunCommand(program) {
133
135
  // subagents/*.md ← flat .md files copied to ~/.claude/agents/ for Agent tool discovery
134
136
  const workflowDir = resolveWorkflowRef(rawAgent, cwd);
135
137
  agent = 'claude';
138
+ const workflowFrontmatter = parseWorkflowFrontmatter(workflowDir);
139
+ if (typeof workflowFrontmatter?.model === 'string' && workflowFrontmatter.model.trim() !== '') {
140
+ workflowModel = workflowFrontmatter.model.trim();
141
+ }
136
142
  const resolvedVersion = resolveVersionAlias('claude', version);
137
143
  const versionHome = getVersionHomePath('claude', resolvedVersion ?? getGlobalDefault('claude') ?? '');
138
144
  const claudeAgentsDir = path.join(versionHome, '.claude', 'agents');
@@ -179,8 +185,7 @@ export function registerRunCommand(program) {
179
185
  // Auto-inject secrets bundles declared in the workflow's frontmatter `secrets:` field.
180
186
  // Union with any --secrets flags the user passed; dedupe. Skip when --no-auto-secrets is set.
181
187
  if (!options.noAutoSecrets) {
182
- const fm = parseWorkflowFrontmatter(workflowDir);
183
- const declared = fm?.secrets ?? [];
188
+ const declared = workflowFrontmatter?.secrets ?? [];
184
189
  if (declared.length > 0) {
185
190
  const existing = new Set(options.secrets);
186
191
  const added = [];
@@ -262,9 +267,18 @@ export function registerRunCommand(program) {
262
267
  }
263
268
  }
264
269
  }
270
+ const defaultVersion = version ?? resolveVersion(agent, cwd);
271
+ const runDefaults = fromProfile
272
+ ? { sources: {} }
273
+ : resolveRunDefaults(agent, defaultVersion, cwd);
265
274
  // Accept the four canonical modes plus 'full' as a permanent silent
266
275
  // alias for 'skip' (rewritten downstream by normalizeMode in exec.ts).
267
276
  let mode = options.mode;
277
+ const modeSource = runCmd.getOptionValueSource('mode');
278
+ const modeFromRunDefault = modeSource === 'default' && !!runDefaults.mode;
279
+ if (modeFromRunDefault) {
280
+ mode = runDefaults.mode;
281
+ }
268
282
  if (!['plan', 'edit', 'auto', 'skip', 'full'].includes(mode)) {
269
283
  console.error(chalk.red(`Invalid mode: ${mode}. Use plan, edit, auto, or skip ('full' accepted as alias for skip).`));
270
284
  process.exit(1);
@@ -276,13 +290,12 @@ export function registerRunCommand(program) {
276
290
  // user did not ask for read-only, they asked for "just run it." An
277
291
  // explicit --mode plan still throws (see resolveMode), because silently
278
292
  // elevating an explicit read-only request to edit is unsafe.
279
- const modeSource = runCmd.getOptionValueSource('mode');
280
293
  const modeIsDefault = modeSource === 'default';
281
294
  try {
282
295
  resolveMode(agent, normalizeMode(mode));
283
296
  }
284
297
  catch (err) {
285
- if (modeIsDefault) {
298
+ if (modeIsDefault && !modeFromRunDefault) {
286
299
  mode = defaultModeFor(agent);
287
300
  if (!options.quiet) {
288
301
  process.stderr.write(chalk.gray(`[agents] ${agent} has no '${options.mode}' mode; using '${mode}'\n`));
@@ -334,6 +347,11 @@ export function registerRunCommand(program) {
334
347
  const env = hasOverrides
335
348
  ? { ...(profileEnv ?? {}), ...secretsEnv, ...(userEnv ?? {}) }
336
349
  : undefined;
350
+ const modelSource = runCmd.getOptionValueSource('model');
351
+ const model = options.model
352
+ ?? (!fromProfile && modelSource === undefined
353
+ ? (workflowModel ?? (options.fallback ? undefined : runDefaults.model))
354
+ : undefined);
337
355
  const execOptions = {
338
356
  agent,
339
357
  version,
@@ -342,7 +360,7 @@ export function registerRunCommand(program) {
342
360
  mode,
343
361
  effort,
344
362
  cwd: options.cwd,
345
- model: options.model,
363
+ model,
346
364
  addDirs: options.addDir,
347
365
  json: options.json,
348
366
  headless: options.headless ?? true,
@@ -4,7 +4,7 @@ import * as fs from 'fs';
4
4
  import * as os from 'os';
5
5
  import * as path from 'path';
6
6
  import { select, checkbox } from '@inquirer/prompts';
7
- import { AGENTS, ALL_AGENT_IDS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
7
+ import { AGENTS, agentConfigDirName, ALL_AGENT_IDS, resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
8
8
  import { cloneRepo } from '../lib/git.js';
9
9
  import { discoverInstructionsFromRepo, discoverRuleFilesFromRepo, installInstructionsCentrally, uninstallInstructions, listInstalledInstructionsWithScope, instructionsExists, getInstructionsContent, listCentralRules, } from '../lib/rules/rules.js';
10
10
  import { listInstalledVersions, getGlobalDefault, resolveVersionAlias, syncResourcesToVersion, promptAgentVersionSelection, getVersionHomePath, } from '../lib/versions.js';
@@ -426,7 +426,7 @@ Examples:
426
426
  return;
427
427
  }
428
428
  const home = getVersionHomePath(agentId, requestedVersion);
429
- const filePath = path.join(home, `.${agentId}`, AGENTS[agentId].instructionsFile);
429
+ const filePath = path.join(home, agentConfigDirName(agentId), AGENTS[agentId].instructionsFile);
430
430
  if (!fs.existsSync(filePath)) {
431
431
  console.log(chalk.yellow(`No user rules found for ${agentLabel(agentId)}@${requestedVersion}`));
432
432
  return;
@@ -486,7 +486,7 @@ Examples:
486
486
  }
487
487
  const home = getVersionHomePath(agentId, requestedVersion);
488
488
  const agent = AGENTS[agentId];
489
- const filePath = path.join(home, `.${agentId}`, agent.instructionsFile);
489
+ const filePath = path.join(home, agentConfigDirName(agentId), agent.instructionsFile);
490
490
  if (fs.existsSync(filePath)) {
491
491
  fs.unlinkSync(filePath);
492
492
  console.log(chalk.green(`Removed ${agent.instructionsFile} from ${agentLabel(agent.id)}@${requestedVersion}`));
@@ -1052,15 +1052,15 @@ Examples:
1052
1052
  password += charClass[charIndex];
1053
1053
  }
1054
1054
  if (opts.copy) {
1055
- const { spawn } = await import('child_process');
1056
- const proc = spawn('pbcopy', [], { stdio: ['pipe', 'inherit', 'inherit'] });
1057
- proc.stdin.write(password);
1058
- proc.stdin.end();
1059
- await new Promise((resolve, reject) => {
1060
- proc.on('close', (code) => code === 0 ? resolve() : reject(new Error('pbcopy failed')));
1061
- proc.on('error', reject);
1062
- });
1063
- console.log(chalk.green(`Password copied to clipboard (${length} chars)`));
1055
+ try {
1056
+ await copyToClipboard(password);
1057
+ console.log(chalk.green(`Password copied to clipboard (${length} chars)`));
1058
+ }
1059
+ catch (err) {
1060
+ console.error(chalk.red(`Clipboard copy failed: ${err.message}`));
1061
+ console.error(chalk.gray('Re-run without --copy to print the password instead.'));
1062
+ process.exitCode = 1;
1063
+ }
1064
1064
  }
1065
1065
  else {
1066
1066
  console.log(password);
@@ -1069,3 +1069,40 @@ Examples:
1069
1069
  registerSecretsSyncCommands(cmd);
1070
1070
  registerSecretsMigrateAclCommand(cmd);
1071
1071
  }
1072
+ /**
1073
+ * Copy text to the system clipboard, cross-platform.
1074
+ * macOS: `pbcopy`. Windows: `clip`. Linux: tries `wl-copy` (Wayland), then
1075
+ * `xclip`, then `xsel` (X11). Throws with an install hint if none are present.
1076
+ */
1077
+ async function copyToClipboard(text) {
1078
+ const { spawn } = await import('child_process');
1079
+ const candidates = process.platform === 'darwin'
1080
+ ? [['pbcopy', []]]
1081
+ : process.platform === 'win32'
1082
+ ? [['clip', []]]
1083
+ : [
1084
+ ['wl-copy', []],
1085
+ ['xclip', ['-selection', 'clipboard']],
1086
+ ['xsel', ['--clipboard', '--input']],
1087
+ ];
1088
+ let lastErr = null;
1089
+ for (const [cmd, args] of candidates) {
1090
+ try {
1091
+ await new Promise((resolve, reject) => {
1092
+ const proc = spawn(cmd, args, { stdio: ['pipe', 'ignore', 'ignore'] });
1093
+ proc.on('error', reject);
1094
+ proc.on('close', (code) => (code === 0 ? resolve() : reject(new Error(`${cmd} exited ${code}`))));
1095
+ proc.stdin.write(text);
1096
+ proc.stdin.end();
1097
+ });
1098
+ return;
1099
+ }
1100
+ catch (err) {
1101
+ lastErr = err;
1102
+ }
1103
+ }
1104
+ const hint = process.platform === 'linux'
1105
+ ? ' Install one: wl-clipboard (Wayland) or xclip / xsel (X11).'
1106
+ : '';
1107
+ throw new Error(`no clipboard tool available (${lastErr?.message ?? 'none found'}).${hint}`);
1108
+ }
@@ -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;