@phnx-labs/agents-cli 1.20.8 → 1.20.10

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 (39) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/README.md +1 -1
  3. package/dist/commands/daemon.js +6 -6
  4. package/dist/commands/import.js +3 -6
  5. package/dist/commands/inspect.d.ts +2 -0
  6. package/dist/commands/inspect.js +75 -28
  7. package/dist/commands/models.js +2 -1
  8. package/dist/commands/plugins.js +3 -2
  9. package/dist/commands/refresh-rules.js +4 -4
  10. package/dist/commands/routines.js +8 -7
  11. package/dist/commands/sessions.js +17 -2
  12. package/dist/commands/subagents.js +2 -1
  13. package/dist/commands/usage.js +11 -3
  14. package/dist/index.js +69 -47
  15. package/dist/lib/agents.d.ts +18 -1
  16. package/dist/lib/agents.js +89 -23
  17. package/dist/lib/browser/chrome.d.ts +4 -3
  18. package/dist/lib/browser/chrome.js +87 -12
  19. package/dist/lib/browser/ipc.js +59 -13
  20. package/dist/lib/daemon.js +20 -8
  21. package/dist/lib/fs-walk.d.ts +7 -1
  22. package/dist/lib/fs-walk.js +45 -11
  23. package/dist/lib/git.js +5 -2
  24. package/dist/lib/log-follow.d.ts +7 -0
  25. package/dist/lib/log-follow.js +65 -0
  26. package/dist/lib/platform/index.d.ts +1 -0
  27. package/dist/lib/platform/index.js +1 -0
  28. package/dist/lib/platform/ipc.d.ts +11 -0
  29. package/dist/lib/platform/ipc.js +21 -0
  30. package/dist/lib/platform/paths.d.ts +7 -0
  31. package/dist/lib/platform/paths.js +9 -0
  32. package/dist/lib/platform/process.d.ts +9 -1
  33. package/dist/lib/platform/process.js +27 -0
  34. package/dist/lib/plugins.js +5 -3
  35. package/dist/lib/self-update.d.ts +86 -0
  36. package/dist/lib/self-update.js +178 -0
  37. package/dist/lib/versions.js +3 -3
  38. package/package.json +1 -1
  39. package/scripts/postinstall.js +29 -19
package/CHANGELOG.md CHANGED
@@ -2,6 +2,17 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ **`agents inspect .` reads the project `.agents/`, and plugin drill-down shows bundled skills**
6
+
7
+ - `agents inspect .` (and any path to a repo root) now resolves to the project's nested `.agents/` tree when that tree is a populated DotAgents root, instead of the project root itself. Previously a top-level `agents.yaml` version-pin or an unrelated source `skills/` dir at the repo root was mistaken for a DotAgents root, so `inspect .` reported the wrong directory's resources (e.g. `plugins 0` while the real `.agents/plugins/` held a plugin). A bare `.agents`-named dir still resolves to itself, and standalone clones / extra repos that keep resources at the top level (using `.agents/` only for worktrees) are unaffected — their nested `.agents/` is not a DotAgents root, so the top level still wins.
8
+ - `agents inspect <repo> --plugins` now reads plugin bundles through the plugin discoverer: the list shows each plugin's manifest description, and drilling into one (`--plugins <name>`) reports its bundled skills, commands, subagents, hooks, MCP servers, and version. Previously plugins were treated as opaque directories with no description and no view into what they ship.
9
+
10
+ **Single-typo agent names auto-correct everywhere, not just `agents run`**
11
+
12
+ - `agents view cladue` used to print `Unknown agent 'cladue'` even though `agents run cladue` auto-corrected. `resolveAgentName` — the canonical resolver behind `view`, `usage`, `inspect`, `doctor`, `sync`, `models`, `skills`, `hooks`, `import`, `sessions --agent`, and every `agent@version` spec (`agents add claud@latest`, `agents use codx@2.1.170`) — now falls back to Damerau-Levenshtein distance-1 matching against canonical ids and multi-letter aliases: `cladue` -> `claude` (transposition), `kim` -> `kimi`, `codx` -> `codex`, `gemni` -> `gemini`.
13
+ - Corrections apply only when unambiguous: every distance-1 candidate must agree on one agent. `kiri` (one edit from both `kiro` and `kimi`) and inputs under 3 characters still error. `agents run` keeps its existing exact -> profile -> workflow -> fuzzy precedence, so a profile named `claud` still beats the typo correction.
14
+ - Fixes `kimi` being listed as a valid agent but missing from the alias map — `agents view kimi` previously errored. Added `kimi` / `kimi-code` entries.
15
+
5
16
  ## 1.20.7
6
17
 
7
18
  **`agents inspect` — DotAgents repo targets (#256)**
package/README.md CHANGED
@@ -122,7 +122,7 @@ agents run codex "Fix the issues Claude found"
122
122
  agents run gemini "Write tests for the fixed code"
123
123
  ```
124
124
 
125
- Each resolves to the project-pinned version with skills, MCP servers, and permissions already synced.
125
+ Each resolves to the project-pinned version with skills, MCP servers, and permissions already synced. Single-typo names auto-correct across every command — `agents view cladue` resolves to `claude`, `agents add codx@latest` to `codex`.
126
126
 
127
127
  ### Rate-limited? Keep working.
128
128
 
@@ -5,7 +5,6 @@
5
5
  * aliases for `agents routines` scheduler lifecycle commands. Scheduled
6
6
  * for removal in v2.0.
7
7
  */
8
- import { spawn } from 'child_process';
9
8
  import chalk from 'chalk';
10
9
  import * as path from 'path';
11
10
  import { startDaemon, stopDaemon, isDaemonRunning, readDaemonPid, readDaemonLog, runDaemon, } from '../lib/daemon.js';
@@ -92,12 +91,13 @@ you never need to start it manually.
92
91
  warnDeprecated('logs', 'agents routines scheduler-logs');
93
92
  if (options.follow) {
94
93
  const { getDaemonDir } = await import('../lib/state.js');
94
+ const { followFile } = await import('../lib/log-follow.js');
95
95
  const logPath = path.join(getDaemonDir(), 'logs.jsonl');
96
- const child = spawn('tail', ['-f', logPath], { stdio: ['ignore', 'pipe', 'pipe'] });
97
- child.stdout.pipe(process.stdout);
98
- child.stderr.pipe(process.stderr);
99
- child.on('exit', () => process.exit(0));
100
- process.on('SIGINT', () => { child.kill(); process.exit(0); });
96
+ const recent = readDaemonLog(parseInt(options.lines, 10));
97
+ if (recent)
98
+ console.log(recent);
99
+ const stop = followFile(logPath, (text) => process.stdout.write(text), { fromEnd: true });
100
+ process.on('SIGINT', () => { stop(); process.exit(0); });
101
101
  return;
102
102
  }
103
103
  const lines = parseInt(options.lines, 10);
@@ -27,20 +27,17 @@ import * as os from 'os';
27
27
  import * as path from 'path';
28
28
  import { confirm } from '@inquirer/prompts';
29
29
  import { ALL_AGENT_IDS } from '../lib/agents.js';
30
- import { AGENTS, getCliPath, getCliVersion, agentLabel } from '../lib/agents.js';
30
+ import { AGENTS, getCliPath, getCliVersion, agentLabel, resolveAgentName } from '../lib/agents.js';
31
31
  import { getVersionDir } from '../lib/versions.js';
32
32
  import { finalizeImport, importAgentBinary, importAgentConfig, importInstallScriptBinary, isValidImportVersion, resolvePackageDirFromBinary, } from '../lib/import.js';
33
33
  import { isPromptCancelled, isInteractiveTerminal } from './utils.js';
34
- function isValidAgentId(value) {
35
- return ALL_AGENT_IDS.includes(value);
36
- }
37
34
  async function runImport(agentArg, opts) {
38
- if (!isValidAgentId(agentArg)) {
35
+ const agentId = resolveAgentName(agentArg);
36
+ if (!agentId) {
39
37
  console.error(chalk.red(`Unknown agent: ${agentArg}`));
40
38
  console.error(chalk.gray(`Known agents: ${ALL_AGENT_IDS.join(', ')}`));
41
39
  process.exit(1);
42
40
  }
43
- const agentId = agentArg;
44
41
  const agent = AGENTS[agentId];
45
42
  // installScript-based agents (Grok, Antigravity, Cursor, Kiro, Goose, Roo)
46
43
  // don't have an npm package; their binary lives wherever the curl/brew
@@ -24,6 +24,8 @@ interface ResourceItem {
24
24
  linkTarget: string;
25
25
  /** One-line description (frontmatter `description:` or first non-frontmatter line). */
26
26
  description: string;
27
+ /** Extra detail rows surfaced in detail mode (e.g. a plugin's bundled skills/commands). */
28
+ extra?: Array<[string, string]>;
27
29
  }
28
30
  interface InspectOptions {
29
31
  brief?: boolean;
@@ -17,13 +17,13 @@ import * as os from 'os';
17
17
  import * as path from 'path';
18
18
  import chalk from 'chalk';
19
19
  import * as yaml from 'yaml';
20
- import { AGENTS, getCliState } from '../lib/agents.js';
20
+ import { AGENTS, getCliState, resolveAgentName } from '../lib/agents.js';
21
21
  import { supports } from '../lib/capabilities.js';
22
22
  import { readMeta, getUserAgentsDir, getSystemAgentsDir, getProjectAgentsDir, getEnabledExtraRepos, } from '../lib/state.js';
23
23
  import { getVersionHomePath } from '../lib/versions.js';
24
24
  import { getShimsDir, getVersionedAliasPath } from '../lib/shims.js';
25
25
  import { getAgentResources, listResources, } from '../lib/resources.js';
26
- import { discoverPlugins } from '../lib/plugins.js';
26
+ import { discoverPlugins, discoverPluginsInDir } from '../lib/plugins.js';
27
27
  import { countSessionsInScope } from '../lib/session/discover.js';
28
28
  import { damerauLevenshtein } from '../lib/fuzzy.js';
29
29
  /** Resource kinds the inspect command can drill into. */
@@ -63,12 +63,16 @@ export async function inspectAction(target, options) {
63
63
  await inspectRepo(repo, options);
64
64
  return;
65
65
  }
66
- const extras = getEnabledExtraRepos();
67
- console.error(chalk.red(`Unknown target: ${target}`));
68
- console.error(chalk.gray(`Agents: ${Object.keys(AGENTS).join(', ')}`));
69
- const aliases = extras.length > 0 ? `, ${extras.map(e => e.alias).join(', ')}` : '';
70
- console.error(chalk.gray(`Repos: user, system, project${aliases} — or a path to a repo with a .agents/ dir`));
71
- process.exit(1);
66
+ // Repo targets take precedence over typo correction; only fall through to
67
+ // parseTarget when the key resolves to an agent (alias or single-edit fix).
68
+ if (!resolveAgentName(agentKey)) {
69
+ const extras = getEnabledExtraRepos();
70
+ console.error(chalk.red(`Unknown target: ${target}`));
71
+ console.error(chalk.gray(`Agents: ${Object.keys(AGENTS).join(', ')}`));
72
+ const aliases = extras.length > 0 ? `, ${extras.map(e => e.alias).join(', ')}` : '';
73
+ console.error(chalk.gray(`Repos: user, system, project${aliases} — or a path to a repo with a .agents/ dir`));
74
+ process.exit(1);
75
+ }
72
76
  }
73
77
  const { agent, version } = parseTarget(target);
74
78
  const versionHome = getVersionHomePath(agent, version);
@@ -93,7 +97,12 @@ export async function inspectAction(target, options) {
93
97
  }
94
98
  function parseTarget(target) {
95
99
  const [rawAgent, rawVersion] = target.split('@');
96
- const agent = (rawAgent || '').toLowerCase();
100
+ const agent = resolveAgentName(rawAgent || '');
101
+ if (!agent) {
102
+ console.error(chalk.red(`Unknown agent: ${rawAgent}`));
103
+ console.error(chalk.gray(`Known agents: ${Object.keys(AGENTS).join(', ')}`));
104
+ process.exit(1);
105
+ }
97
106
  let version = rawVersion;
98
107
  if (!version || version === 'default') {
99
108
  const meta = readMeta();
@@ -152,18 +161,23 @@ export function resolveRepoTarget(target, cwd) {
152
161
  const stat = safeStat(abs);
153
162
  if (!stat || !stat.isDirectory())
154
163
  return null;
155
- // A dir that is itself a DotAgents root wins over its nested .agents/ —
156
- // extra repos like ~/.agents-extras keep resources at the top level and use
157
- // .agents/ only for worktrees.
164
+ // A dir literally named `.agents` is the root itself.
165
+ if (path.basename(abs) === '.agents') {
166
+ return { label: path.basename(path.dirname(abs)), root: abs };
167
+ }
168
+ // A nested `.agents/` that is a populated DotAgents root wins over `abs` — the
169
+ // project case (`agents inspect .` from a repo root whose resources live under
170
+ // `.agents/`, while the repo's own top-level `skills/`, `agents.yaml` pin, etc.
171
+ // are unrelated source, not a DotAgents tree).
172
+ const nested = path.join(abs, '.agents');
173
+ if (isDotAgentsRoot(nested)) {
174
+ return { label: path.basename(abs), root: nested };
175
+ }
176
+ // Otherwise treat `abs` itself as the root: standalone clones and extra repos
177
+ // like ~/.agents-extras keep resources at the top level and use `.agents/`
178
+ // only for worktrees (so their nested `.agents/` is not a DotAgents root).
158
179
  if (isDotAgentsRoot(abs)) {
159
- const label = path.basename(abs) === '.agents' ? path.basename(path.dirname(abs)) : path.basename(abs);
160
- return { label, root: abs };
161
- }
162
- if (path.basename(abs) !== '.agents') {
163
- const nested = path.join(abs, '.agents');
164
- if (safeStat(nested)?.isDirectory()) {
165
- return { label: path.basename(abs), root: nested };
166
- }
180
+ return { label: path.basename(abs), root: abs };
167
181
  }
168
182
  return null;
169
183
  }
@@ -195,6 +209,14 @@ async function inspectRepo(repo, options) {
195
209
  }
196
210
  /** List one resource kind from a single repo root — no layering, no overrides. */
197
211
  export function collectRepoKind(repo, kind) {
212
+ // Plugins are bundles with a manifest + nested skills/commands/hooks — read
213
+ // them through the plugin discoverer so the manifest description and bundled
214
+ // resources surface, rather than treating each as an opaque directory.
215
+ if (kind === 'plugins') {
216
+ return discoverPluginsInDir(path.join(repo.root, 'plugins'))
217
+ .map(p => pluginToItem(p, repo.label))
218
+ .sort((a, b) => a.name.localeCompare(b.name));
219
+ }
198
220
  const dir = path.join(repo.root, kind);
199
221
  let entries;
200
222
  try {
@@ -477,14 +499,35 @@ function collectKind(agent, versionHome, kind) {
477
499
  }
478
500
  }
479
501
  function pluginItems() {
480
- const plugins = discoverPlugins();
481
- return plugins.map(p => ({
482
- name: p.name,
483
- source: 'user',
484
- path: p.root,
485
- linkTarget: linkTarget(p.root),
486
- description: p.manifest.description ?? '',
487
- }));
502
+ return discoverPlugins().map(p => pluginToItem(p, 'user'));
503
+ }
504
+ /**
505
+ * Map a discovered plugin to a resource item, surfacing the manifest description
506
+ * and the bundle's nested resources (skills, commands, hooks, ...) as detail rows.
507
+ */
508
+ function pluginToItem(plugin, source) {
509
+ const extra = [];
510
+ const list = (names) => names.length <= 8 ? names.join(', ') : `${names.slice(0, 8).join(', ')}, +${names.length - 8} more`;
511
+ if (plugin.skills.length)
512
+ extra.push(['skills', `${plugin.skills.length} (${list(plugin.skills)})`]);
513
+ if (plugin.commands.length)
514
+ extra.push(['commands', `${plugin.commands.length} (${list(plugin.commands)})`]);
515
+ if (plugin.agentDefs.length)
516
+ extra.push(['subagents', `${plugin.agentDefs.length} (${list(plugin.agentDefs)})`]);
517
+ if (plugin.hooks.length)
518
+ extra.push(['hooks', String(plugin.hooks.length)]);
519
+ if (plugin.mcpServers.length)
520
+ extra.push(['mcp', list(plugin.mcpServers)]);
521
+ if (plugin.manifest.version)
522
+ extra.push(['version', plugin.manifest.version]);
523
+ return {
524
+ name: plugin.name,
525
+ source,
526
+ path: plugin.root,
527
+ linkTarget: linkTarget(plugin.root),
528
+ description: plugin.manifest.description ?? '',
529
+ extra,
530
+ };
488
531
  }
489
532
  function entriesFromAgentResources(agent, versionHome, kind) {
490
533
  const res = getAgentResources(agent, { home: versionHome });
@@ -553,6 +596,10 @@ function buildDetailRows(item, kind) {
553
596
  rows.push(['tools', fm.tools.join(', ')]);
554
597
  }
555
598
  }
599
+ // Plugin bundles carry their nested resources as pre-built rows.
600
+ if (kind === 'plugins' && item.extra) {
601
+ rows.push(...item.extra);
602
+ }
556
603
  return rows;
557
604
  }
558
605
  function findMatches(items, query) {
@@ -7,6 +7,7 @@
7
7
  */
8
8
  import chalk from 'chalk';
9
9
  import * as fs from 'fs';
10
+ import { homeDir } from '../lib/platform/index.js';
10
11
  import { resolveAgentName, formatAgentError, agentLabel, } from '../lib/agents.js';
11
12
  import { listInstalledVersions, getGlobalDefault, resolveVersion, resolveVersionAlias } from '../lib/versions.js';
12
13
  import { getModelCatalog, locateModelSource } from '../lib/models.js';
@@ -166,5 +167,5 @@ function printCatalog(agent, version, isDefault, options) {
166
167
  }
167
168
  /** Abbreviate a path by replacing the home directory with ~. */
168
169
  function shortPath(p) {
169
- return p.replace(process.env.HOME || '~', '~');
170
+ return p.replace(homeDir(), '~');
170
171
  }
@@ -8,6 +8,7 @@
8
8
  import * as fs from 'fs';
9
9
  import * as path from 'path';
10
10
  import chalk from 'chalk';
11
+ import { homeDir } from '../lib/platform/index.js';
11
12
  import { input } from '@inquirer/prompts';
12
13
  import { agentLabel } from '../lib/agents.js';
13
14
  import { capableAgents, isCapable } from '../lib/capabilities.js';
@@ -21,7 +22,7 @@ import { safeJoin } from '../lib/paths.js';
21
22
  import { discoverMarketplaces } from '../lib/plugin-marketplace.js';
22
23
  /** Replace the home directory prefix with ~ for display. */
23
24
  function formatPath(p) {
24
- const home = process.env.HOME || '';
25
+ const home = homeDir();
25
26
  if (home && p.startsWith(home)) {
26
27
  return '~' + p.slice(home.length);
27
28
  }
@@ -355,7 +356,7 @@ Examples:
355
356
  });
356
357
  }
357
358
  const name = nameArg;
358
- const pluginsDir = path.join(process.env.HOME || '', '.agents', 'plugins');
359
+ const pluginsDir = path.join(homeDir(), '.agents', 'plugins');
359
360
  const pluginRoot = safeJoin(pluginsDir, name);
360
361
  // Use discovered plugin when present; fall back to name+root if source is already gone
361
362
  const plugin = getPlugin(name);
@@ -6,7 +6,7 @@
6
6
  * Recompiles only when source files have changed.
7
7
  */
8
8
  import chalk from 'chalk';
9
- import { AGENTS } from '../lib/agents.js';
9
+ import { AGENTS, resolveAgentName } from '../lib/agents.js';
10
10
  import { isVersionInstalled } from '../lib/versions.js';
11
11
  import { ensureRulesFresh, supportsRulesImports } from '../lib/rules/compile.js';
12
12
  /**
@@ -23,12 +23,12 @@ export function registerRefreshRulesCommand(program) {
23
23
  .requiredOption('--agent-version <version>', 'Installed version whose rules file should be refreshed')
24
24
  .option('--quiet', 'Suppress all output (exit code indicates success)', false)
25
25
  .action((opts) => {
26
- const agentId = opts.agent;
26
+ const agentId = resolveAgentName(opts.agent);
27
27
  const version = opts.agentVersion;
28
28
  const quiet = !!opts.quiet;
29
- if (!AGENTS[agentId]) {
29
+ if (!agentId) {
30
30
  if (!quiet)
31
- console.error(chalk.red(`Unknown agent '${agentId}'`));
31
+ console.error(chalk.red(`Unknown agent '${opts.agent}'`));
32
32
  process.exitCode = 1;
33
33
  return;
34
34
  }
@@ -14,6 +14,7 @@ import { isDaemonRunning, signalDaemonReload, startDaemon, stopDaemon, readDaemo
14
14
  import { humanizeCron, humanizeNextRun, formatRepoLink, REPO_DISPLAY_MAX } from '../lib/routines-format.js';
15
15
  import { listJobs as listAllJobs, deleteJob, readJob, validateJob, writeJob, setJobEnabled, listRuns, getLatestRun, getRunDir, getJobPath, parseAtTime, } from '../lib/routines.js';
16
16
  import { getRoutinesDir } from '../lib/state.js';
17
+ import { IS_WINDOWS } from '../lib/platform/index.js';
17
18
  import { safeJoin } from '../lib/paths.js';
18
19
  import { executeJob, executeJobDetached } from '../lib/runner.js';
19
20
  import { JobScheduler } from '../lib/scheduler.js';
@@ -385,7 +386,7 @@ export function registerRoutinesCommands(program) {
385
386
  console.log(chalk.gray(`Created new job file: ${newPath}`));
386
387
  }
387
388
  const targetPath = jobPath || path.join(getRoutinesDir(), `${name}.yml`);
388
- const editor = process.env.EDITOR || process.env.VISUAL || 'vi';
389
+ const editor = process.env.EDITOR || process.env.VISUAL || (IS_WINDOWS ? 'notepad' : 'vi');
389
390
  const editorParts = editor.split(/\s+/).filter(Boolean);
390
391
  const editorBin = editorParts[0];
391
392
  const editorArgs = [...editorParts.slice(1), targetPath];
@@ -691,14 +692,14 @@ export function registerRoutinesCommands(program) {
691
692
  .option('-f, --follow', 'Stream log output in real time (like tail -f)')
692
693
  .action(async (options) => {
693
694
  if (options.follow) {
694
- const { spawn } = await import('child_process');
695
695
  const { getDaemonDir } = await import('../lib/state.js');
696
+ const { followFile } = await import('../lib/log-follow.js');
696
697
  const logPath = path.join(getDaemonDir(), 'logs.jsonl');
697
- const child = spawn('tail', ['-f', logPath]);
698
- child.stdout?.pipe(process.stdout);
699
- child.stderr?.pipe(process.stderr);
700
- child.on('exit', () => process.exit(0));
701
- process.on('SIGINT', () => { child.kill(); process.exit(0); });
698
+ const recent = readDaemonLog(parseInt(options.lines, 10));
699
+ if (recent)
700
+ console.log(recent);
701
+ const stop = followFile(logPath, (text) => process.stdout.write(text), { fromEnd: true });
702
+ process.on('SIGINT', () => { stop(); process.exit(0); });
702
703
  return;
703
704
  }
704
705
  const lines = parseInt(options.lines, 10);
@@ -23,6 +23,7 @@ import { parseSession } from '../lib/session/parse.js';
23
23
  import { renderConversationMarkdown, renderSummary, renderSummaryHeader, computeSummaryStats, renderJson, filterEvents, parseRoleList } from '../lib/session/render.js';
24
24
  import { renderMarkdown } from '../lib/markdown.js';
25
25
  import { colorAgent, resolveAgentName } from '../lib/agents.js';
26
+ import { fuzzyMatch, FUZZY_PRESETS } from '../lib/fuzzy.js';
26
27
  import { resolveVersionAliasLoose } from '../lib/versions.js';
27
28
  import { isInteractiveTerminal, isPromptCancelled } from './utils.js';
28
29
  import { sessionPicker } from './sessions-picker.js';
@@ -822,8 +823,22 @@ function parseAgentFilter(agentName) {
822
823
  if (!agentName)
823
824
  return {};
824
825
  const [name, version] = agentName.split('@', 2);
825
- const agent = name;
826
- if (!SESSION_AGENTS.includes(agent)) {
826
+ let agent = SESSION_AGENTS.includes(name)
827
+ ? name
828
+ : null;
829
+ if (!agent) {
830
+ // Aliases and single-typo corrections (cladue -> claude). SESSION_AGENTS
831
+ // includes ids (rush, hermes) that resolveAgentName doesn't know, so fall
832
+ // back to fuzzy-matching the session list directly.
833
+ const resolved = resolveAgentName(name);
834
+ if (resolved && SESSION_AGENTS.includes(resolved)) {
835
+ agent = resolved;
836
+ }
837
+ else {
838
+ agent = fuzzyMatch(name, SESSION_AGENTS, FUZZY_PRESETS.agents);
839
+ }
840
+ }
841
+ if (!agent) {
827
842
  console.error(chalk.red(`Unknown agent: ${name}. Use: ${SESSION_AGENTS.join(', ')}`));
828
843
  process.exit(1);
829
844
  }
@@ -10,6 +10,7 @@ import ora from 'ora';
10
10
  import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import { agentLabel } from '../lib/agents.js';
13
+ import { homeDir } from '../lib/platform/index.js';
13
14
  import { capableAgents } from '../lib/capabilities.js';
14
15
  import { cloneRepo } from '../lib/git.js';
15
16
  import { discoverSubagentsFromRepo, installSubagentCentrally, listInstalledSubagents, getInstalledSubagent, listSubagentsForAgent, iterSubagentsCapableVersions, removeSubagentFromVersion, } from '../lib/subagents.js';
@@ -19,7 +20,7 @@ import { requireDestructiveArg, promptRemovalTargets, parseCommaSeparatedList, r
19
20
  import { showResourceList, buildTargetsSection, } from './resource-view.js';
20
21
  /** Replace the home directory prefix with ~ for display. */
21
22
  function formatPath(p) {
22
- const home = process.env.HOME || '';
23
+ const home = homeDir();
23
24
  if (home && p.startsWith(home)) {
24
25
  return '~' + p.slice(home.length);
25
26
  }
@@ -1,5 +1,5 @@
1
1
  import chalk from 'chalk';
2
- import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, } from '../lib/agents.js';
2
+ import { ALL_AGENT_IDS, AGENTS, getAccountInfo, agentLabel, resolveAgentName, formatAgentError, } from '../lib/agents.js';
3
3
  import { listInstalledVersions, getGlobalDefault, getVersionHomePath } from '../lib/versions.js';
4
4
  import { formatUsageSection, getUsageInfoForIdentity } from '../lib/usage.js';
5
5
  /** Agents whose CLI surfaces usage data we can read today. */
@@ -15,9 +15,17 @@ Examples:
15
15
  agents usage codex Show usage for Codex only
16
16
  `)
17
17
  .action(async (agentFilter) => {
18
- const filter = agentFilter;
18
+ let filter;
19
+ if (agentFilter) {
20
+ const resolved = resolveAgentName(agentFilter);
21
+ if (!resolved) {
22
+ console.error(chalk.red(formatAgentError(agentFilter)));
23
+ process.exit(1);
24
+ }
25
+ filter = resolved;
26
+ }
19
27
  const targets = filter
20
- ? [filter].filter((id) => ALL_AGENT_IDS.includes(id))
28
+ ? [filter]
21
29
  : ALL_AGENT_IDS.filter((id) => listInstalledVersions(id).length > 0);
22
30
  if (targets.length === 0) {
23
31
  console.log(chalk.gray('No agents installed. Run `agents add <agent>` first.'));
package/dist/index.js CHANGED
@@ -23,7 +23,7 @@ const __dirname = path.dirname(fileURLToPath(import.meta.url));
23
23
  const packageJsonPath = path.join(__dirname, '..', 'package.json');
24
24
  const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
25
25
  const VERSION = packageJson.version;
26
- const NPM_PACKAGE_NAME = '@phnx-labs/agents-cli';
26
+ import { NPM_PACKAGE_NAME, deriveGlobalPrefix, installPackageIntoPrefix, verifyInstalledVersion, refreshAliasShims, } from './lib/self-update.js';
27
27
  // Detect dev/working-tree builds and default the noisy startup steps off.
28
28
  // Three cases trip this:
29
29
  // 1. Dev install (scripts/install.sh) — package.json version stamped 0.0.0-dev.<sha>
@@ -268,17 +268,52 @@ async function showWhatsNew(fromVersion, toVersion) {
268
268
  }
269
269
  }
270
270
  const UPDATE_CHECK_INTERVAL_MS = 24 * 60 * 60 * 1000; // 24 hours
271
- import { getUpdateCheckPath, getMigratedSentinelPath, getUserAgentsDir } from './lib/state.js';
271
+ import { getUpdateCheckPath, getMigratedSentinelPath, getUserAgentsDir, getRuntimeStateDir } from './lib/state.js';
272
+ import { readUpdateCache, saveUpdateCheck, dismissUpdateVersion, shouldPromptUpgrade, findAgentsCliInstalls, } from './lib/self-update.js';
272
273
  const UPDATE_CHECK_FILE = getUpdateCheckPath();
273
- /** Read the cached update-check state from disk. Returns null if the file is missing or corrupt. */
274
- function readUpdateCache() {
274
+ /**
275
+ * Warn once when PATH resolves `agents` to a different agents-cli install
276
+ * than the copy that is currently running (or to several). Divergent installs
277
+ * are how self-updates "succeed" without changing the command the user types.
278
+ * The warning re-fires only when the set of install roots changes; dev builds
279
+ * (0.0.0-dev) are ignored because side-by-side dev installs are a supported
280
+ * workflow.
281
+ */
282
+ function maybeWarnMultiInstall() {
283
+ const sentinel = path.join(getRuntimeStateDir(), 'multi-install-warned');
284
+ const runningRoot = path.resolve(__dirname, '..');
285
+ const byRoot = new Map();
286
+ byRoot.set(runningRoot, { version: VERSION, note: 'running' });
287
+ for (const install of findAgentsCliInstalls(process.env.PATH || '')) {
288
+ if (install.version.startsWith('0.0.0-dev'))
289
+ continue;
290
+ if (!byRoot.has(install.packageRoot)) {
291
+ byRoot.set(install.packageRoot, { version: install.version, note: `agents on PATH: ${install.binPath}` });
292
+ }
293
+ }
294
+ if (byRoot.size < 2) {
295
+ try {
296
+ fs.unlinkSync(sentinel);
297
+ }
298
+ catch { /* nothing recorded */ }
299
+ return;
300
+ }
301
+ const key = [...byRoot.keys()].sort().join('\n');
275
302
  try {
276
- return JSON.parse(fs.readFileSync(UPDATE_CHECK_FILE, 'utf-8'));
303
+ if (fs.readFileSync(sentinel, 'utf-8') === key)
304
+ return;
277
305
  }
278
- catch {
279
- /* cache file missing or corrupt */
280
- return null;
306
+ catch { /* not warned for this set yet */ }
307
+ console.error(chalk.yellow('Multiple agents-cli installs detected:'));
308
+ for (const [root, info] of byRoot) {
309
+ console.error(chalk.gray(` ${root} ${info.version} (${info.note})`));
310
+ }
311
+ console.error(chalk.gray('Upgrades apply to the running copy. Remove a stale copy with: npm uninstall -g --prefix <prefix> @phnx-labs/agents-cli'));
312
+ try {
313
+ fs.mkdirSync(path.dirname(sentinel), { recursive: true });
314
+ fs.writeFileSync(sentinel, key);
281
315
  }
316
+ catch { /* best-effort; worst case the warning repeats */ }
282
317
  }
283
318
  /** Determine whether enough time has elapsed since the last registry fetch. */
284
319
  function shouldFetchLatest(cache) {
@@ -286,18 +321,6 @@ function shouldFetchLatest(cache) {
286
321
  return true;
287
322
  return Date.now() - cache.lastCheck > UPDATE_CHECK_INTERVAL_MS;
288
323
  }
289
- /** Persist the latest known version and current timestamp to the update-check cache. */
290
- function saveUpdateCheck(latestVersion) {
291
- try {
292
- const dir = path.dirname(UPDATE_CHECK_FILE);
293
- if (!fs.existsSync(dir))
294
- fs.mkdirSync(dir, { recursive: true });
295
- fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({ lastCheck: Date.now(), latestVersion }));
296
- }
297
- catch {
298
- /* best-effort cache update */
299
- }
300
- }
301
324
  /** Fetch the exact latest npm version plus its registry integrity hash. */
302
325
  async function fetchNpmPackageMetadata(versionOrTag = 'latest', timeoutMs = 5000) {
303
326
  const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/${versionOrTag}`, {
@@ -320,12 +343,11 @@ function printResolvedPackage(metadata) {
320
343
  console.log(chalk.gray(`Integrity: ${metadata.integrity}`));
321
344
  }
322
345
  async function installResolvedPackage(metadata) {
323
- const { execFile } = await import('child_process');
324
- const { promisify } = await import('util');
325
- const execFileAsync = promisify(execFile);
326
- const installArgs = ['install', '-g', '@phnx-labs/agents-cli', '--ignore-scripts'];
327
- installArgs[2] = `${NPM_PACKAGE_NAME}@${metadata.version}`;
328
- await execFileAsync('npm', installArgs);
346
+ const packageRoot = path.resolve(__dirname, '..');
347
+ const prefix = deriveGlobalPrefix(packageRoot);
348
+ await installPackageIntoPrefix(`${NPM_PACKAGE_NAME}@${metadata.version}`, prefix);
349
+ verifyInstalledVersion(packageRoot, metadata.version);
350
+ refreshAliasShims(packageRoot);
329
351
  }
330
352
  /** Present an interactive upgrade prompt (TTY) or a one-line hint (non-TTY). */
331
353
  async function promptUpgrade(latestVersion) {
@@ -342,19 +364,7 @@ async function promptUpgrade(latestVersion) {
342
364
  ],
343
365
  });
344
366
  if (answer === 'dismiss') {
345
- try {
346
- const dir = path.dirname(UPDATE_CHECK_FILE);
347
- if (!fs.existsSync(dir))
348
- fs.mkdirSync(dir, { recursive: true });
349
- const existing = readUpdateCache();
350
- fs.writeFileSync(UPDATE_CHECK_FILE, JSON.stringify({
351
- ...existing,
352
- lastCheck: existing?.lastCheck ?? Date.now(),
353
- latestVersion,
354
- dismissed: latestVersion,
355
- }));
356
- }
357
- catch { /* best-effort */ }
367
+ dismissUpdateVersion(UPDATE_CHECK_FILE, latestVersion);
358
368
  return;
359
369
  }
360
370
  if (answer === 'now') {
@@ -362,6 +372,10 @@ async function promptUpgrade(latestVersion) {
362
372
  let spinner = ora('Resolving package metadata...').start();
363
373
  try {
364
374
  const metadata = await fetchNpmPackageMetadata();
375
+ // The prompt showed the cached latest, which can lag the registry (the
376
+ // 24h window) — sync the cache to what was actually resolved so later
377
+ // prompts and the install agree on the same version.
378
+ saveUpdateCheck(UPDATE_CHECK_FILE, metadata.version);
365
379
  spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${metadata.version}`);
366
380
  printResolvedPackage(metadata);
367
381
  const approved = await confirm({
@@ -377,15 +391,20 @@ async function promptUpgrade(latestVersion) {
377
391
  spinner.succeed(`Upgraded to ${metadata.version}`);
378
392
  await showWhatsNew(VERSION, metadata.version);
379
393
  console.log();
380
- // Re-exec with new version and exit
381
- const result = spawnSync('agents', process.argv.slice(2), {
394
+ // Re-exec the verified install's entrypoint and exit. PATH lookup of
395
+ // `agents` could resolve a different copy (dev build, another prefix)
396
+ // than the one that was just upgraded.
397
+ const entrypoint = path.resolve(__dirname, '..', 'dist', 'index.js');
398
+ const result = spawnSync(process.execPath, [entrypoint, ...process.argv.slice(2)], {
382
399
  stdio: 'inherit',
383
400
  shell: false,
384
401
  });
385
402
  process.exit(result.status ?? 0);
386
403
  }
387
- catch {
388
- spinner.fail('Upgrade failed');
404
+ catch (err) {
405
+ if (isPromptCancelled(err))
406
+ return;
407
+ spinner.fail(`Upgrade failed: ${err instanceof Error ? err.message : String(err)}`);
389
408
  console.log(chalk.gray('Run manually: agents upgrade --yes'));
390
409
  }
391
410
  console.log();
@@ -405,7 +424,7 @@ function refreshUpdateCacheInBackground() {
405
424
  .then((response) => (response.ok ? response.json() : null))
406
425
  .then((data) => {
407
426
  if (data && typeof data.version === 'string') {
408
- saveUpdateCheck(data.version);
427
+ saveUpdateCheck(UPDATE_CHECK_FILE, data.version);
409
428
  }
410
429
  })
411
430
  .catch(() => {
@@ -416,7 +435,8 @@ function refreshUpdateCacheInBackground() {
416
435
  async function checkForUpdates() {
417
436
  if (process.env.AGENTS_CLI_DISABLE_AUTO_UPDATE)
418
437
  return;
419
- const cache = readUpdateCache();
438
+ maybeWarnMultiInstall();
439
+ const cache = readUpdateCache(UPDATE_CHECK_FILE);
420
440
  // Kick off network refresh in background if stale. Does not block.
421
441
  if (shouldFetchLatest(cache)) {
422
442
  refreshUpdateCacheInBackground();
@@ -424,7 +444,7 @@ async function checkForUpdates() {
424
444
  // Prompt based on current cache (may be from a previous run's background refresh).
425
445
  // Skip if the user dismissed this exact version — they'll be prompted again when
426
446
  // a newer version appears.
427
- if (cache?.latestVersion && cache.latestVersion !== VERSION && compareVersions(cache.latestVersion, VERSION) > 0 && cache.latestVersion !== cache.dismissed) {
447
+ if (shouldPromptUpgrade(cache, VERSION)) {
428
448
  try {
429
449
  await promptUpgrade(cache.latestVersion);
430
450
  }
@@ -699,7 +719,9 @@ program
699
719
  }
700
720
  }
701
721
  catch (err) {
702
- spinner.fail('Upgrade failed');
722
+ if (isPromptCancelled(err))
723
+ return;
724
+ spinner.fail(`Upgrade failed: ${err instanceof Error ? err.message : String(err)}`);
703
725
  console.log(chalk.gray(`Run manually: agents upgrade ${version ? version + ' ' : ''}--yes`));
704
726
  }
705
727
  });