@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
@@ -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)
package/dist/index.js CHANGED
@@ -76,6 +76,7 @@ import { registerDaemonCommands } from './commands/daemon.js';
76
76
  import { registerRoutinesCommands } from './commands/routines.js';
77
77
  import { registerRunCommand } from './commands/exec.js';
78
78
  import { registerModelsCommand } from './commands/models.js';
79
+ import { registerDefaultsCommands } from './commands/defaults.js';
79
80
  import { registerPruneCommand } from './commands/prune.js';
80
81
  import { registerTrashCommands } from './commands/trash.js';
81
82
  import { registerDoctorCommand } from './commands/doctor.js';
@@ -151,6 +152,7 @@ Packages:
151
152
 
152
153
  Run and dispatch:
153
154
  run <agent|profile> [prompt] Run an agent. Omit prompt for interactive mode.
155
+ defaults Configure run defaults by agent/version selector
154
156
  teams Coordinate multiple agents on shared work
155
157
  routines Run agents on a cron schedule (scheduler auto-starts)
156
158
  sessions Browse, search, and replay past runs (live-search in TTY; grouped by workspace)
@@ -217,9 +219,12 @@ async function showWhatsNew(fromVersion, toVersion) {
217
219
  const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/);
218
220
  if (versionMatch) {
219
221
  currentVersion = versionMatch[1];
220
- const isNewer = currentVersion !== fromVersion &&
221
- compareVersions(currentVersion, fromVersion) > 0;
222
- inRelevantSection = isNewer;
222
+ // Only the range the user actually moved through: (fromVersion, toVersion].
223
+ // Bounding the top end matters when upgrading to a specific older
224
+ // version, and guards against a changelog that lists unreleased entries.
225
+ const inRange = compareVersions(currentVersion, fromVersion) > 0 &&
226
+ compareVersions(currentVersion, toVersion) <= 0;
227
+ inRelevantSection = inRange;
223
228
  if (inRelevantSection) {
224
229
  relevantChanges.push('');
225
230
  relevantChanges.push(chalk.bold(`v${currentVersion}`));
@@ -279,11 +284,14 @@ function saveUpdateCheck(latestVersion) {
279
284
  }
280
285
  }
281
286
  /** Fetch the exact latest npm version plus its registry integrity hash. */
282
- async function fetchLatestNpmPackageMetadata(timeoutMs = 5000) {
283
- const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`, {
287
+ async function fetchNpmPackageMetadata(versionOrTag = 'latest', timeoutMs = 5000) {
288
+ const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/${versionOrTag}`, {
284
289
  signal: AbortSignal.timeout(timeoutMs),
285
290
  });
286
291
  if (!response.ok) {
292
+ if (response.status === 404) {
293
+ throw new Error(`${NPM_PACKAGE_NAME}@${versionOrTag} not found on npm`);
294
+ }
287
295
  throw new Error('Could not reach npm registry');
288
296
  }
289
297
  const data = await response.json();
@@ -338,7 +346,7 @@ async function promptUpgrade(latestVersion) {
338
346
  const { spawnSync } = await import('child_process');
339
347
  let spinner = ora('Resolving package metadata...').start();
340
348
  try {
341
- const metadata = await fetchLatestNpmPackageMetadata();
349
+ const metadata = await fetchNpmPackageMetadata();
342
350
  spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${metadata.version}`);
343
351
  printResolvedPackage(metadata);
344
352
  const approved = await confirm({
@@ -583,6 +591,7 @@ registerPackagesCommands(program);
583
591
  registerDaemonCommands(program);
584
592
  registerRoutinesCommands(program);
585
593
  registerRunCommand(program);
594
+ registerDefaultsCommands(program);
586
595
  registerModelsCommand(program);
587
596
  registerPruneCommand(program);
588
597
  registerTrashCommands(program);
@@ -627,26 +636,31 @@ for (const alias of ['jobs', 'cron']) {
627
636
  }
628
637
  program
629
638
  .command('upgrade')
630
- .description('Upgrade agents-cli to the latest version')
639
+ .description('Upgrade agents-cli to the latest version (or a specific [version])')
640
+ .argument('[version]', 'Target version or dist-tag to install (default: latest)')
631
641
  .option('-y, --yes', 'Install without an interactive confirmation prompt')
632
- .action(async (options) => {
633
- let spinner = ora('Checking for updates...').start();
642
+ .action(async (version, options) => {
643
+ const target = version ?? 'latest';
644
+ let spinner = ora(version ? `Resolving ${NPM_PACKAGE_NAME}@${target}...` : 'Checking for updates...').start();
634
645
  try {
635
- const metadata = await fetchLatestNpmPackageMetadata();
636
- const latestVersion = metadata.version;
637
- if (latestVersion === VERSION) {
638
- spinner.succeed(`Already on latest version (${VERSION})`);
646
+ const metadata = await fetchNpmPackageMetadata(target);
647
+ const resolvedVersion = metadata.version;
648
+ if (resolvedVersion === VERSION) {
649
+ spinner.succeed(`Already on ${VERSION}`);
639
650
  return;
640
651
  }
641
- if (compareVersions(latestVersion, VERSION) <= 0) {
642
- spinner.succeed(`Already ahead of latest (${VERSION} >= ${latestVersion})`);
652
+ // For `latest` (no explicit version) skip when already ahead. When a
653
+ // version is named explicitly, honor it even if it's a downgrade.
654
+ if (!version && compareVersions(resolvedVersion, VERSION) <= 0) {
655
+ spinner.succeed(`Already ahead of latest (${VERSION} >= ${resolvedVersion})`);
643
656
  return;
644
657
  }
645
- spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${latestVersion}`);
658
+ const direction = compareVersions(resolvedVersion, VERSION) < 0 ? 'Downgrade' : 'Upgrade';
659
+ spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${resolvedVersion}`);
646
660
  printResolvedPackage(metadata);
647
661
  if (isInteractiveTerminal() && !options.yes) {
648
662
  const approved = await confirm({
649
- message: `Install ${NPM_PACKAGE_NAME}@${latestVersion}?`,
663
+ message: `Install ${NPM_PACKAGE_NAME}@${resolvedVersion}?`,
650
664
  default: false,
651
665
  });
652
666
  if (!approved) {
@@ -654,14 +668,17 @@ program
654
668
  return;
655
669
  }
656
670
  }
657
- spinner = ora(`Upgrading ${VERSION} -> ${latestVersion}...`).start();
671
+ spinner = ora(`${direction === 'Downgrade' ? 'Downgrading' : 'Upgrading'} ${VERSION} -> ${resolvedVersion}...`).start();
658
672
  await installResolvedPackage(metadata);
659
- spinner.succeed(`Upgraded to ${latestVersion}`);
660
- await showWhatsNew(VERSION, latestVersion);
673
+ spinner.succeed(`${direction}d to ${resolvedVersion}`);
674
+ // Only show the changelog for a genuine upgrade range.
675
+ if (compareVersions(resolvedVersion, VERSION) > 0) {
676
+ await showWhatsNew(VERSION, resolvedVersion);
677
+ }
661
678
  }
662
679
  catch (err) {
663
680
  spinner.fail('Upgrade failed');
664
- console.log(chalk.gray('Run manually: agents upgrade --yes'));
681
+ console.log(chalk.gray(`Run manually: agents upgrade ${version ? version + ' ' : ''}--yes`));
665
682
  }
666
683
  });
667
684
  registerPullCommand(program);
@@ -69,12 +69,16 @@ export declare function ensureSkillsDir(agentId: AgentId): void;
69
69
  * The agent's config-dir name relative to $HOME — e.g. '.claude',
70
70
  * '.gemini/antigravity-cli', '.config/amp', '.kimi-code'.
71
71
  *
72
- * This is the path segment to join onto a (version) home root when locating an
73
- * agent's commands/skills/plugins. Do NOT hardcode `.${agentId}`: it is wrong
74
- * for every agent whose config dir is nested or lives under ~/.config —
75
- * antigravity (~/.gemini/antigravity-cli), amp (~/.config/amp),
76
- * goose (~/.config/goose), kimi (~/.kimi-code). Mirrors the shim `configDirName`
77
- * derivation in shims.ts.
72
+ * Path segment to join onto a (version) home root when locating an agent's
73
+ * commands/skills/plugins. Do NOT hardcode `.${agentId}`: it is wrong for
74
+ * every agent whose config dir is nested or under ~/.config — antigravity
75
+ * (~/.gemini/antigravity-cli), amp (~/.config/amp), goose (~/.config/goose),
76
+ * kimi (~/.kimi-code). Mirrors the shim configDirName derivation in shims.ts.
77
+ *
78
+ * Relativized against the module-level HOME constant (the same value used to
79
+ * build every `configDir`), NOT a fresh `os.homedir()` — so the result stays a
80
+ * clean relative name even when HOME is overridden after module load (tests,
81
+ * sandboxes). Using `os.homedir()` here would yield `../../real/home/.claude`.
78
82
  */
79
83
  export declare function agentConfigDirName(agentId: AgentId): string;
80
84
  /** Account identity and billing information extracted from an agent's auth config. */
@@ -446,13 +446,18 @@ export const AGENTS = {
446
446
  rulesImports: true,
447
447
  },
448
448
  },
449
+ // Kimi Code CLI (`kimi`) — Moonshot AI coding agent.
450
+ // Install: `curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash`
451
+ // or: `npm install -g @moonshot-ai/kimi-code`
452
+ // Config: `~/.kimi-code/config.toml`, `~/.kimi-code/mcp.json`,
453
+ // `~/.kimi-code/skills/`, `~/.kimi-code/hooks/`
449
454
  kimi: {
450
455
  id: 'kimi',
451
456
  name: 'Kimi',
452
457
  color: 'magentaBright',
453
- cliCommand: 'kimi-code',
454
- npmPackage: '',
455
- installScript: '',
458
+ cliCommand: 'kimi',
459
+ npmPackage: '@moonshot-ai/kimi-code',
460
+ installScript: 'curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash',
456
461
  configDir: path.join(HOME, '.kimi-code'),
457
462
  commandsDir: '',
458
463
  commandsSubdir: '',
@@ -465,14 +470,14 @@ export const AGENTS = {
465
470
  capabilities: {
466
471
  hooks: true,
467
472
  mcp: true,
468
- allowlist: false,
469
- skills: false,
473
+ allowlist: true,
474
+ skills: true,
470
475
  commands: false,
471
- plugins: false,
476
+ plugins: true,
472
477
  subagents: false,
473
478
  rules: { file: 'AGENTS.md' },
474
479
  workflows: false,
475
- modes: ['plan', 'edit', 'skip'],
480
+ modes: ['plan', 'edit', 'auto', 'skip'],
476
481
  rulesImports: false,
477
482
  },
478
483
  },
@@ -690,15 +695,19 @@ export function ensureSkillsDir(agentId) {
690
695
  * The agent's config-dir name relative to $HOME — e.g. '.claude',
691
696
  * '.gemini/antigravity-cli', '.config/amp', '.kimi-code'.
692
697
  *
693
- * This is the path segment to join onto a (version) home root when locating an
694
- * agent's commands/skills/plugins. Do NOT hardcode `.${agentId}`: it is wrong
695
- * for every agent whose config dir is nested or lives under ~/.config —
696
- * antigravity (~/.gemini/antigravity-cli), amp (~/.config/amp),
697
- * goose (~/.config/goose), kimi (~/.kimi-code). Mirrors the shim `configDirName`
698
- * derivation in shims.ts.
698
+ * Path segment to join onto a (version) home root when locating an agent's
699
+ * commands/skills/plugins. Do NOT hardcode `.${agentId}`: it is wrong for
700
+ * every agent whose config dir is nested or under ~/.config — antigravity
701
+ * (~/.gemini/antigravity-cli), amp (~/.config/amp), goose (~/.config/goose),
702
+ * kimi (~/.kimi-code). Mirrors the shim configDirName derivation in shims.ts.
703
+ *
704
+ * Relativized against the module-level HOME constant (the same value used to
705
+ * build every `configDir`), NOT a fresh `os.homedir()` — so the result stays a
706
+ * clean relative name even when HOME is overridden after module load (tests,
707
+ * sandboxes). Using `os.homedir()` here would yield `../../real/home/.claude`.
699
708
  */
700
709
  export function agentConfigDirName(agentId) {
701
- return path.relative(os.homedir(), AGENTS[agentId].configDir);
710
+ return path.relative(HOME, AGENTS[agentId].configDir);
702
711
  }
703
712
  /** Return the email address associated with the agent's auth config, or null. */
704
713
  export async function getAccountEmail(agentId, home) {
@@ -1,5 +1,15 @@
1
1
  import type { ChromeOptions } from './types.js';
2
2
  import type { BrowserType } from './types.js';
3
+ /**
4
+ * True when `binaryPath` is a shebang script rather than a native browser
5
+ * executable. The Linux distro launchers (`/usr/bin/brave-browser`, …) are such
6
+ * scripts; `launchBrowser` can't drive one over `--remote-debugging-pipe` (see
7
+ * resolveBrowserBinary). `profiles doctor` uses this to flag a profile whose
8
+ * binary resolves to a wrapper we couldn't unwrap. Shebang scripts are a
9
+ * Linux/Unix concept — returns false on Windows/macOS app bundles.
10
+ */
11
+ export declare function isLauncherScript(binaryPath: string): boolean;
12
+ export declare function resolveBrowserBinary(binaryPath: string): string;
3
13
  export declare function findBrowserPath(browserType: BrowserType, customBinary?: string): string;
4
14
  /**
5
15
  * Walk the per-platform priority list and return the first browser that's
@@ -42,12 +42,90 @@ const BROWSER_PATHS = {
42
42
  custom: [],
43
43
  },
44
44
  };
45
+ /**
46
+ * On Debian/Ubuntu the canonical launchers under `/usr/bin`
47
+ * (`brave-browser`, `google-chrome`, `chromium`) are not the browser ELF —
48
+ * they're `#!/bin/bash` wrapper scripts (the upstream Chromium wrapper) that,
49
+ * as their final step, run the real binary as a NON-exec child:
50
+ *
51
+ * exec < /dev/null
52
+ * exec > >(exec cat)
53
+ * exec 2> >(exec cat >&2)
54
+ * "$HERE/brave" "$@" || true
55
+ *
56
+ * That breaks `launchBrowser`'s `--remote-debugging-pipe` transport two ways:
57
+ * the std-fd sanitization (and the extra `cat` process-substitution children)
58
+ * disturbs the inherited CDP pipe on fd 3/4, and the pid we record is the
59
+ * wrapper's, not the browser's. The symptom is `read ECONNRESET` /
60
+ * `CDP connection closed` right after spawn (issue #229).
61
+ *
62
+ * Follow the wrapper to the ELF it execs. The wrapper sets
63
+ * `HERE="dirname(readlink -f "$0")"` and invokes `"$HERE/<name>"`, so we
64
+ * resolve the script path, scan for that invocation line, and join the two.
65
+ * Returns the original path untouched when it's already an ELF, when it's not
66
+ * a resolvable wrapper, or on any non-Linux platform.
67
+ */
68
+ function readsAsShebangScript(binaryPath) {
69
+ let fd;
70
+ try {
71
+ fd = fs.openSync(binaryPath, 'r');
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ try {
77
+ const head = Buffer.alloc(2);
78
+ fs.readSync(fd, head, 0, 2, 0);
79
+ // ELF binaries start with 0x7f 'E'; shebang scripts with '#!'.
80
+ return head[0] === 0x23 && head[1] === 0x21;
81
+ }
82
+ finally {
83
+ fs.closeSync(fd);
84
+ }
85
+ }
86
+ /**
87
+ * True when `binaryPath` is a shebang script rather than a native browser
88
+ * executable. The Linux distro launchers (`/usr/bin/brave-browser`, …) are such
89
+ * scripts; `launchBrowser` can't drive one over `--remote-debugging-pipe` (see
90
+ * resolveBrowserBinary). `profiles doctor` uses this to flag a profile whose
91
+ * binary resolves to a wrapper we couldn't unwrap. Shebang scripts are a
92
+ * Linux/Unix concept — returns false on Windows/macOS app bundles.
93
+ */
94
+ export function isLauncherScript(binaryPath) {
95
+ if (os.platform() === 'win32')
96
+ return false;
97
+ return readsAsShebangScript(binaryPath);
98
+ }
99
+ export function resolveBrowserBinary(binaryPath) {
100
+ if (os.platform() !== 'linux')
101
+ return binaryPath;
102
+ // Only shebang scripts need unwrapping; a real ELF passes straight through.
103
+ if (!readsAsShebangScript(binaryPath))
104
+ return binaryPath;
105
+ let script;
106
+ let realScriptPath;
107
+ try {
108
+ realScriptPath = fs.realpathSync(binaryPath);
109
+ script = fs.readFileSync(realScriptPath, 'utf8');
110
+ }
111
+ catch {
112
+ return binaryPath;
113
+ }
114
+ // Match the Chromium wrapper's launch line: `"$HERE/<name>" "$@"`, optionally
115
+ // prefixed with `exec -a "$0"`. The captured name is the real ELF, sitting in
116
+ // the same directory as the resolved wrapper.
117
+ const match = script.match(/"\$HERE\/([A-Za-z0-9._-]+)"\s+"\$@"/);
118
+ if (!match)
119
+ return binaryPath;
120
+ const realBinary = path.join(path.dirname(realScriptPath), match[1]);
121
+ return fs.existsSync(realBinary) ? realBinary : binaryPath;
122
+ }
45
123
  export function findBrowserPath(browserType, customBinary) {
46
124
  if (customBinary) {
47
125
  if (!fs.existsSync(customBinary)) {
48
126
  throw new Error(`Custom binary not found: ${customBinary}`);
49
127
  }
50
- return customBinary;
128
+ return resolveBrowserBinary(customBinary);
51
129
  }
52
130
  if (browserType === 'custom') {
53
131
  throw new Error('browser: custom requires a binary path in the profile');
@@ -60,9 +138,12 @@ export function findBrowserPath(browserType, customBinary) {
60
138
  const candidates = platformPaths[browserType] || [];
61
139
  for (const p of candidates) {
62
140
  if (fs.existsSync(p)) {
63
- return p;
141
+ return resolveBrowserBinary(p);
64
142
  }
65
143
  }
144
+ if (browserType === 'comet' && platform !== 'darwin') {
145
+ throw new Error('Browser "comet" is macOS-only (Comet is a macOS Chromium fork). Use chrome, chromium, brave, or edge on this platform.');
146
+ }
66
147
  throw new Error(`Browser "${browserType}" not found. Install it first.`);
67
148
  }
68
149
  // Per-platform Chromium-family priority list for "no --profile" auto-pick.
@@ -98,7 +179,7 @@ export function findFirstInstalledBrowser(platform = os.platform()) {
98
179
  const candidates = platformPaths[browserType] || [];
99
180
  for (const p of candidates) {
100
181
  if (fs.existsSync(p)) {
101
- return { browserType, binary: p };
182
+ return { browserType, binary: resolveBrowserBinary(p) };
102
183
  }
103
184
  }
104
185
  }