@phnx-labs/agents-cli 1.20.8 → 1.20.9

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.
package/CHANGELOG.md CHANGED
@@ -2,6 +2,12 @@
2
2
 
3
3
  ## Unreleased
4
4
 
5
+ **Single-typo agent names auto-correct everywhere, not just `agents run`**
6
+
7
+ - `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`.
8
+ - 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.
9
+ - Fixes `kimi` being listed as a valid agent but missing from the alias map — `agents view kimi` previously errored. Added `kimi` / `kimi-code` entries.
10
+
5
11
  ## 1.20.7
6
12
 
7
13
  **`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
@@ -17,7 +17,7 @@ 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';
@@ -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();
@@ -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
  });
@@ -104,6 +104,17 @@ export declare function getAccountEmail(agentId: AgentId, home?: string): Promis
104
104
  * the agent's local auth/config files. Supports Claude, Codex, and Gemini.
105
105
  */
106
106
  export declare function getAccountInfo(agentId: AgentId, home?: string): Promise<AccountInfo>;
107
+ /**
108
+ * Determine when the agent was last used by checking session file mtimes,
109
+ * falling back to config mtime.
110
+ *
111
+ * The session walk stats every transcript under the home's session dir —
112
+ * thousands of files on long-lived installs — and `agents run` rotation calls
113
+ * this once per installed version on every launch. The walk result is cached
114
+ * on disk for a short window so back-to-back launches skip it entirely.
115
+ * Cache read/write is best-effort: any failure falls back to walking.
116
+ */
117
+ export declare function resolveLastActive(agentId: AgentId, base: string, configPath?: string, cachePath?: string, now?: Date): Date | null;
107
118
  /**
108
119
  * Quick count of session files for an agent (without full DB scan).
109
120
  * Used during init to show approximate session count to user.
@@ -190,7 +201,13 @@ export declare function listInstalledMcpsWithScope(agentId: AgentId, cwd?: strin
190
201
  }): InstalledMcp[];
191
202
  /** Map of agent name aliases and shorthand identifiers to canonical AgentId values. */
192
203
  export declare const AGENT_NAME_ALIASES: Record<string, AgentId>;
193
- /** Resolve a user-provided agent name (alias, shorthand, or canonical) to its AgentId. */
204
+ /**
205
+ * Resolve a user-provided agent name (alias, shorthand, or canonical) to its AgentId.
206
+ * Tolerates a single typo (insertion/deletion/substitution/transposition) against
207
+ * canonical ids and aliases — `cladue` -> claude, `kim` -> kimi, `codx` -> codex —
208
+ * but only when the correction is unambiguous (all distance-1 candidates agree on
209
+ * one agent). Two-letter shorthands are excluded as fuzzy candidates.
210
+ */
194
211
  export declare function resolveAgentName(input: string): AgentId | null;
195
212
  /** Check whether the input string matches any known agent name or alias. */
196
213
  export declare function isAgentName(input: string): boolean;