@phnx-labs/agents-cli 1.20.7 → 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.
Files changed (48) hide show
  1. package/CHANGELOG.md +6 -0
  2. package/README.md +1 -1
  3. package/dist/commands/computer-actions.d.ts +19 -0
  4. package/dist/commands/computer-actions.js +159 -1
  5. package/dist/commands/computer.js +2 -2
  6. package/dist/commands/daemon.js +6 -6
  7. package/dist/commands/import.js +3 -6
  8. package/dist/commands/inspect.js +17 -8
  9. package/dist/commands/models.js +2 -1
  10. package/dist/commands/plugins.js +3 -2
  11. package/dist/commands/refresh-rules.js +4 -4
  12. package/dist/commands/routines.js +8 -7
  13. package/dist/commands/sessions.js +17 -2
  14. package/dist/commands/setup.js +2 -2
  15. package/dist/commands/subagents.js +2 -1
  16. package/dist/commands/usage.js +11 -3
  17. package/dist/commands/versions.js +2 -2
  18. package/dist/index.js +69 -47
  19. package/dist/lib/agents.d.ts +18 -1
  20. package/dist/lib/agents.js +89 -23
  21. package/dist/lib/browser/chrome.d.ts +4 -3
  22. package/dist/lib/browser/chrome.js +87 -12
  23. package/dist/lib/browser/ipc.js +59 -13
  24. package/dist/lib/computer-rpc.d.ts +2 -0
  25. package/dist/lib/computer-rpc.js +21 -1
  26. package/dist/lib/daemon.js +20 -8
  27. package/dist/lib/fs-walk.d.ts +7 -1
  28. package/dist/lib/fs-walk.js +45 -11
  29. package/dist/lib/git.js +5 -2
  30. package/dist/lib/log-follow.d.ts +7 -0
  31. package/dist/lib/log-follow.js +65 -0
  32. package/dist/lib/platform/index.d.ts +1 -0
  33. package/dist/lib/platform/index.js +1 -0
  34. package/dist/lib/platform/ipc.d.ts +11 -0
  35. package/dist/lib/platform/ipc.js +21 -0
  36. package/dist/lib/platform/paths.d.ts +7 -0
  37. package/dist/lib/platform/paths.js +9 -0
  38. package/dist/lib/platform/process.d.ts +9 -1
  39. package/dist/lib/platform/process.js +27 -0
  40. package/dist/lib/plugins.js +5 -3
  41. package/dist/lib/refresh.js +2 -2
  42. package/dist/lib/self-update.d.ts +86 -0
  43. package/dist/lib/self-update.js +178 -0
  44. package/dist/lib/shims.d.ts +13 -8
  45. package/dist/lib/shims.js +46 -11
  46. package/dist/lib/versions.js +3 -3
  47. package/package.json +1 -1
  48. package/scripts/postinstall.js +36 -26
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
 
@@ -31,6 +31,25 @@ export declare function buildElementOrCoords(opts: {
31
31
  ok: false;
32
32
  error: string;
33
33
  };
34
+ export declare function buildRaiseParams(opts: {
35
+ windowId?: number;
36
+ title?: string;
37
+ }): Record<string, unknown>;
38
+ export declare function buildWaitParams(opts: {
39
+ duration?: number;
40
+ id?: string;
41
+ until?: string;
42
+ role?: string;
43
+ label?: string;
44
+ identifier?: string;
45
+ timeout?: number;
46
+ }): {
47
+ ok: true;
48
+ params: Record<string, unknown>;
49
+ } | {
50
+ ok: false;
51
+ error: string;
52
+ };
34
53
  export declare function withClient<T>(fn: (client: ComputerClient) => Promise<T>): Promise<T>;
35
54
  export declare function unwrap(r: RPCResponse): Record<string, unknown>;
36
55
  export declare function registerActionCommands(program: Command): void;
@@ -60,6 +60,51 @@ export function buildElementOrCoords(opts) {
60
60
  return { ok: true, params: { x: opts.x, y: opts.y } };
61
61
  return { ok: false, error: 'pass --id <@eN> (from `describe`) or --x <n> --y <n>' };
62
62
  }
63
+ // Build the focus_window params for `raise`. Pure + tested: window_id and
64
+ // title are both optional refinements over the app-level activate.
65
+ export function buildRaiseParams(opts) {
66
+ const params = {};
67
+ if (opts.windowId != null)
68
+ params.window_id = opts.windowId;
69
+ if (opts.title)
70
+ params.title = opts.title;
71
+ return params;
72
+ }
73
+ // Build the wait RPC params. Pure + tested. Three modes, mirroring the
74
+ // daemon's Wait.run: --duration (unconditional sleep), --id + --until
75
+ // (cached-element poll), or --role/--label/--identifier (live locator poll).
76
+ export function buildWaitParams(opts) {
77
+ if (opts.duration != null) {
78
+ return { ok: true, params: { duration_ms: opts.duration } };
79
+ }
80
+ const params = {};
81
+ if (opts.until)
82
+ params.until = opts.until;
83
+ if (opts.timeout != null)
84
+ params.timeout_ms = opts.timeout;
85
+ if (opts.id) {
86
+ return { ok: true, params: { ...params, element_id: opts.id } };
87
+ }
88
+ const locator = {};
89
+ if (opts.role)
90
+ locator.role = opts.role;
91
+ if (opts.label)
92
+ locator.label = opts.label;
93
+ if (opts.identifier)
94
+ locator.identifier = opts.identifier;
95
+ if (Object.keys(locator).length === 0) {
96
+ return { ok: false, error: 'pass --duration <ms>, --id <@eN>, or a locator (--role/--label/--identifier)' };
97
+ }
98
+ return { ok: true, params: { ...params, locator } };
99
+ }
100
+ // postToPid keyboard delivery is dropped by key-window-gated apps (Parallels
101
+ // VMs and friends) — when the daemon reports the target was not frontmost,
102
+ // the keystrokes may have landed nowhere. Surface that loudly on stderr.
103
+ function warnIfNotFrontmost(res) {
104
+ if (res.frontmost === false) {
105
+ console.error('warning: target was not the frontmost app — keystrokes may have been dropped. Run `agents computer raise` first.');
106
+ }
107
+ }
63
108
  function reportMissingHelper() {
64
109
  console.error('helper not built. Run: ./packages/computer-helper/scripts/build.sh debug');
65
110
  process.exit(1);
@@ -99,6 +144,12 @@ async function resolveTargetPid(client, opts) {
99
144
  }
100
145
  return picked.app.pid;
101
146
  }
147
+ // --raise flag: app-level focus_window before the main action so coordinate
148
+ // clicks and keystrokes land on a visible, key window.
149
+ async function raiseIfRequested(client, pid, raise) {
150
+ if (raise)
151
+ unwrap(await client.call('focus_window', { pid }));
152
+ }
102
153
  function emit(result, json, human) {
103
154
  if (json) {
104
155
  console.log(JSON.stringify(result, null, 2));
@@ -158,6 +209,7 @@ export function registerActionCommands(program) {
158
209
  .description('Click an element (--id) or screen coordinate (--x --y)')
159
210
  .option('--count <n>', 'Click count (2 = double-click)', (v) => parseInt(v, 10))
160
211
  .option('--background', 'Focus-safe postToPid delivery (plain AppKit only; skips HID tap)')
212
+ .option('--raise', 'Bring the target app to the front first')
161
213
  .option('--json', 'Emit JSON'))).action(async (opts) => {
162
214
  await withClient(async (client) => {
163
215
  const pid = await resolveTargetPid(client, opts);
@@ -166,6 +218,7 @@ export function registerActionCommands(program) {
166
218
  console.error(spec.error);
167
219
  process.exit(1);
168
220
  }
221
+ await raiseIfRequested(client, pid, opts.raise);
169
222
  const params = { pid, ...spec.params };
170
223
  if (opts.count != null)
171
224
  params.count = opts.count;
@@ -221,13 +274,19 @@ export function registerActionCommands(program) {
221
274
  .description('Type an arbitrary unicode string into the focused field (focus first via click/focus)')
222
275
  .requiredOption('--text <s>', 'Text to type')
223
276
  .option('--commit', 'Press Return after typing')
277
+ .option('--raise', 'Bring the target app to the front first')
278
+ .option('--require-frontmost', 'Fail (not warn) if the target is not the frontmost app')
224
279
  .option('--json', 'Emit JSON')).action(async (opts) => {
225
280
  await withClient(async (client) => {
226
281
  const pid = await resolveTargetPid(client, opts);
282
+ await raiseIfRequested(client, pid, opts.raise);
227
283
  const params = { pid, text: opts.text };
228
284
  if (opts.commit)
229
285
  params.commit = true;
286
+ if (opts.requireFrontmost)
287
+ params.require_frontmost = true;
230
288
  const res = unwrap(await client.call('type_text', params));
289
+ warnIfNotFrontmost(res);
231
290
  emit(res, Boolean(opts.json), () => `typed ${res.chars ?? opts.text.length} char(s)`);
232
291
  });
233
292
  });
@@ -236,10 +295,17 @@ export function registerActionCommands(program) {
236
295
  .command('key')
237
296
  .description('Send a key chord, e.g. "cmd+shift+s", "enter", "esc"')
238
297
  .requiredOption('--keys <chord>', 'Key chord')
298
+ .option('--raise', 'Bring the target app to the front first')
299
+ .option('--require-frontmost', 'Fail (not warn) if the target is not the frontmost app')
239
300
  .option('--json', 'Emit JSON')).action(async (opts) => {
240
301
  await withClient(async (client) => {
241
302
  const pid = await resolveTargetPid(client, opts);
242
- const res = unwrap(await client.call('key', { pid, keys: opts.keys }));
303
+ await raiseIfRequested(client, pid, opts.raise);
304
+ const params = { pid, keys: opts.keys };
305
+ if (opts.requireFrontmost)
306
+ params.require_frontmost = true;
307
+ const res = unwrap(await client.call('key', params));
308
+ warnIfNotFrontmost(res);
243
309
  emit(res, Boolean(opts.json), () => `sent ${opts.keys}`);
244
310
  });
245
311
  });
@@ -251,6 +317,7 @@ export function registerActionCommands(program) {
251
317
  .requiredOption('--to <x,y>', 'End coordinate "x,y"')
252
318
  .option('--button <left|right>', 'Mouse button', 'left')
253
319
  .option('--background', 'Focus-safe postToPid delivery (plain AppKit only)')
320
+ .option('--raise', 'Bring the target app to the front first')
254
321
  .option('--json', 'Emit JSON')).action(async (opts) => {
255
322
  let from;
256
323
  let to;
@@ -264,6 +331,7 @@ export function registerActionCommands(program) {
264
331
  }
265
332
  await withClient(async (client) => {
266
333
  const pid = await resolveTargetPid(client, opts);
334
+ await raiseIfRequested(client, pid, opts.raise);
267
335
  const params = {
268
336
  pid,
269
337
  from: [from.x, from.y],
@@ -282,9 +350,11 @@ export function registerActionCommands(program) {
282
350
  .description('Scroll by a pixel delta at an element or coordinate')
283
351
  .option('--dy <n>', 'Vertical delta (negative = down)', (v) => parseInt(v, 10))
284
352
  .option('--dx <n>', 'Horizontal delta', (v) => parseInt(v, 10))
353
+ .option('--raise', 'Bring the target app to the front first')
285
354
  .option('--json', 'Emit JSON'))).action(async (opts) => {
286
355
  await withClient(async (client) => {
287
356
  const pid = await resolveTargetPid(client, opts);
357
+ await raiseIfRequested(client, pid, opts.raise);
288
358
  const params = { pid };
289
359
  if (opts.id)
290
360
  params.element_id = opts.id;
@@ -325,4 +395,92 @@ export function registerActionCommands(program) {
325
395
  emit(res, Boolean(opts.json), () => `focused ${opts.id}`);
326
396
  });
327
397
  });
398
+ // raise — bring an app (or one of its windows) to the front. The window
399
+ // forms (--window-id/--title) also switch macOS Spaces, which is the only
400
+ // way to reach a fullscreen-Space window (VM, fullscreen editor) for
401
+ // capture and HID-tap input.
402
+ addTargetOpts(program
403
+ .command('raise')
404
+ .description('Bring an app (or a specific window) to the front — switches Spaces for fullscreen windows')
405
+ .option('--window-id <n>', 'Raise a specific window by id (from `screenshot --list`)', (v) => parseInt(v, 10))
406
+ .option('--title <s>', 'Raise the window whose title contains this string')
407
+ .option('--json', 'Emit JSON')).action(async (opts) => {
408
+ await withClient(async (client) => {
409
+ const pid = await resolveTargetPid(client, opts);
410
+ const res = unwrap(await client.call('focus_window', { pid, ...buildRaiseParams(opts) }));
411
+ emit(res, Boolean(opts.json), () => {
412
+ const scope = res.raised_window ? `window ${res.title ?? res.window_id ?? ''}`.trim() : 'app';
413
+ return `raised ${scope} (${res.focus_elapsed_ms ?? 0}ms)`;
414
+ });
415
+ });
416
+ });
417
+ // wait — settle the UI before the next action
418
+ addTargetOpts(program
419
+ .command('wait')
420
+ .description('Wait for a duration (--duration) or for an element (--id / --role/--label) to satisfy --until')
421
+ .option('--duration <ms>', 'Unconditional sleep in ms (50-30000)', (v) => parseInt(v, 10))
422
+ .option('--id <@eN>', 'Element id from `describe` to poll')
423
+ .option('--until <cond>', 'Condition: exists | enabled | disappears (default: exists)')
424
+ .option('--role <s>', 'Locator: AX role (e.g. AXButton)')
425
+ .option('--label <s>', 'Locator: element label')
426
+ .option('--identifier <s>', 'Locator: AX identifier')
427
+ .option('--timeout <ms>', 'Poll timeout in ms (default 5000)', (v) => parseInt(v, 10))
428
+ .option('--json', 'Emit JSON')).action(async (opts) => {
429
+ const spec = buildWaitParams(opts);
430
+ if (!spec.ok) {
431
+ console.error(spec.error);
432
+ process.exit(1);
433
+ }
434
+ await withClient(async (client) => {
435
+ const params = { ...spec.params };
436
+ // duration-only waits don't need a target pid
437
+ if (params.duration_ms == null)
438
+ params.pid = await resolveTargetPid(client, opts);
439
+ const res = unwrap(await client.call('wait', params));
440
+ emit(res, Boolean(opts.json), () => res.satisfied ? `satisfied (${res.waited_ms}ms)` : `timed out (${res.waited_ms}ms)`);
441
+ });
442
+ });
443
+ // get-text — read text without OCR
444
+ addTargetOpts(program
445
+ .command('get-text')
446
+ .description('Extract visible text from the app (or a subtree via --id)')
447
+ .option('--id <@eN>', 'Element id from `describe` to scope the extraction')
448
+ .option('--max-chars <n>', 'Cap the extracted text length', (v) => parseInt(v, 10))
449
+ .option('--json', 'Emit JSON')).action(async (opts) => {
450
+ await withClient(async (client) => {
451
+ const pid = await resolveTargetPid(client, opts);
452
+ const params = { pid };
453
+ if (opts.id)
454
+ params.element_id = opts.id;
455
+ if (opts.maxChars != null)
456
+ params.max_chars = opts.maxChars;
457
+ const res = unwrap(await client.call('get_text', params));
458
+ emit(res, Boolean(opts.json), () => String(res.text ?? ''));
459
+ });
460
+ });
461
+ // launch — start an app (no target resolution: it isn't running yet)
462
+ program
463
+ .command('launch')
464
+ .description('Launch an app by bundle id, path, or name')
465
+ .option('--bundle <id>', 'Bundle id (e.g. com.apple.TextEdit)')
466
+ .option('--path <p>', 'Path to the .app bundle')
467
+ .option('--name <s>', 'App name (resolved via /Applications and LaunchServices)')
468
+ .option('--json', 'Emit JSON')
469
+ .action(async (opts) => {
470
+ if (!opts.bundle && !opts.path && !opts.name) {
471
+ console.error('pass one of --bundle, --path, --name');
472
+ process.exit(1);
473
+ }
474
+ await withClient(async (client) => {
475
+ const params = {};
476
+ if (opts.bundle)
477
+ params.bundle_id = opts.bundle;
478
+ if (opts.path)
479
+ params.path = opts.path;
480
+ if (opts.name)
481
+ params.name = opts.name;
482
+ const res = unwrap(await client.call('launch_app', params));
483
+ emit(res, Boolean(opts.json), () => `launched ${res.name} (pid ${res.pid})`);
484
+ });
485
+ });
328
486
  }
@@ -9,8 +9,8 @@ import { registerActionCommands, withClient, unwrap, pickTarget } from './comput
9
9
  const COMPUTER_HELP_GROUPS = [
10
10
  { title: 'Installation', names: ['setup'] },
11
11
  { title: 'Daemon lifecycle', names: ['start', 'stop', 'reload', 'status'] },
12
- { title: 'Observe', names: ['apps', 'describe', 'screenshot'] },
13
- { title: 'Interact', names: ['click', 'right-click', 'type', 'type-text', 'key', 'drag', 'scroll', 'ax-action', 'focus'] },
12
+ { title: 'Observe', names: ['apps', 'describe', 'screenshot', 'get-text'] },
13
+ { title: 'Interact', names: ['launch', 'raise', 'click', 'right-click', 'type', 'type-text', 'key', 'drag', 'scroll', 'ax-action', 'focus', 'wait'] },
14
14
  ];
15
15
  export function registerComputerCommand(program) {
16
16
  const computer = program
@@ -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
  }
@@ -145,8 +145,8 @@ export async function runSetup(program, options = {}) {
145
145
  if (!isShimsInPath()) {
146
146
  const pathResult = addShimsToPath();
147
147
  if (pathResult.success && !pathResult.alreadyPresent) {
148
- console.log(chalk.green(`\nAdded shims to ~/${pathResult.rcFile}`));
149
- console.log(chalk.gray('Restart your shell or run: source ~/' + pathResult.rcFile));
148
+ console.log(chalk.green(`\nAdded shims to ${pathResult.location}`));
149
+ console.log(chalk.gray(pathResult.reloadHint));
150
150
  }
151
151
  else if (!pathResult.success) {
152
152
  console.log(chalk.yellow('\nTo enable version switching, add shims to PATH:'));
@@ -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.'));
@@ -402,8 +402,8 @@ export function registerVersionsCommands(program) {
402
402
  if (!isShimsInPath()) {
403
403
  const pathResult = addShimsToPath();
404
404
  if (pathResult.success && !pathResult.alreadyPresent) {
405
- console.log(chalk.green(` Added shims to ~/${pathResult.rcFile}`));
406
- console.log(chalk.gray(' Restart your shell or run: source ~/' + pathResult.rcFile));
405
+ console.log(chalk.green(` Added shims to ${pathResult.location}`));
406
+ console.log(chalk.gray(' ' + pathResult.reloadHint));
407
407
  }
408
408
  else if (!pathResult.success) {
409
409
  console.log(chalk.yellow('\nCould not auto-add shims to PATH:'));