@phnx-labs/agents-cli 1.20.7 → 1.20.8

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.
@@ -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
@@ -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:'));
@@ -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:'));
@@ -21,6 +21,8 @@ export declare function writeComputerPeers(allowedExecPaths: string[]): void;
21
21
  export declare function resolveHelperExec(): string | null;
22
22
  export declare function resolveHelperApp(): string | null;
23
23
  export declare function openComputerClient(): ComputerClient;
24
+ export declare const RPC_TIMEOUT_MS = 30000;
25
+ export declare function resolveRpcTimeoutMs(env: string | undefined): number;
24
26
  export declare function describeTransport(): {
25
27
  kind: 'socket' | 'stdio' | 'none';
26
28
  path: string | null;
@@ -200,6 +200,15 @@ export function openComputerClient() {
200
200
  }
201
201
  return new StdioClient(helperExec);
202
202
  }
203
+ // Per-call RPC timeout. Without it a hung daemon (deadlocked connection
204
+ // queue, stopped process) hangs the CLI forever — the waiter map never
205
+ // settles. 30s clears every daemon-side ceiling (wait caps at 30s,
206
+ // launch_app at 10s, screenshot at 5s). Overridable for slower flows.
207
+ export const RPC_TIMEOUT_MS = 30_000;
208
+ export function resolveRpcTimeoutMs(env) {
209
+ const n = Number(env);
210
+ return Number.isFinite(n) && n > 0 ? n : RPC_TIMEOUT_MS;
211
+ }
203
212
  // Shared waiter map + line parser. Both transports plug their reader into
204
213
  // `handleChunk` and their writer into `send`.
205
214
  class BaseClient {
@@ -241,8 +250,19 @@ class BaseClient {
241
250
  }
242
251
  const id = this.nextId++;
243
252
  const payload = JSON.stringify({ id, method, params: params ?? {} }) + '\n';
253
+ const timeoutMs = resolveRpcTimeoutMs(process.env.COMPUTER_HELPER_RPC_TIMEOUT_MS);
244
254
  return new Promise((resolve) => {
245
- this.waiters.set(id, resolve);
255
+ const timer = setTimeout(() => {
256
+ if (this.waiters.delete(id)) {
257
+ resolve({ id, error: { code: 'rpc_timeout', message: `helper did not respond within ${timeoutMs}ms` } });
258
+ }
259
+ }, timeoutMs);
260
+ // Resolve as an error (never reject) so callers flow through unwrap()
261
+ // uniformly, matching failPending's contract.
262
+ this.waiters.set(id, (r) => {
263
+ clearTimeout(timer);
264
+ resolve(r);
265
+ });
246
266
  this.send(payload);
247
267
  });
248
268
  }
@@ -212,8 +212,8 @@ export async function refresh(options = {}) {
212
212
  if (!isShimsInPath()) {
213
213
  const pathResult = addShimsToPath();
214
214
  if (pathResult.success && !pathResult.alreadyPresent) {
215
- console.log(chalk.green(`\nAdded shims to ~/${pathResult.rcFile}`));
216
- console.log(chalk.gray('Restart your shell or run: source ~/' + pathResult.rcFile));
215
+ console.log(chalk.green(`\nAdded shims to ${pathResult.location}`));
216
+ console.log(chalk.gray(pathResult.reloadHint));
217
217
  }
218
218
  else if (!pathResult.success) {
219
219
  console.log(chalk.yellow('\nCould not auto-add shims to PATH:'));
@@ -248,20 +248,25 @@ export declare function isShimsInPath(): boolean;
248
248
  * Get shell configuration instructions for adding shims to PATH.
249
249
  */
250
250
  export declare function getPathSetupInstructions(): string;
251
+ interface ShimPathResult {
252
+ success: boolean;
253
+ alreadyPresent?: boolean;
254
+ rcFile?: string;
255
+ /** Human label of where the entry landed, e.g. `~/.zshrc` or `your user PATH`. */
256
+ location?: string;
257
+ /** Per-platform "how to pick it up" hint, e.g. `source ~/.zshrc` / open a new terminal. */
258
+ reloadHint?: string;
259
+ error?: string;
260
+ }
251
261
  /**
252
- * Add shims directory to shell PATH configuration.
253
- * Returns true if added, false if already present or failed.
262
+ * Add the shims directory to PATH: edits the shell rc file on POSIX, or registers
263
+ * it on the Windows User PATH (registry + WM_SETTINGCHANGE). Idempotent.
254
264
  */
255
265
  export declare function addShimsToPath(overrides?: {
256
266
  homeDir?: string;
257
267
  shell?: string;
258
268
  shimsDir?: string;
259
- }): {
260
- success: boolean;
261
- alreadyPresent?: boolean;
262
- rcFile?: string;
263
- error?: string;
264
- };
269
+ }): ShimPathResult;
265
270
  export declare function listAgentsWithInstalledVersions(): AgentId[];
266
271
  /**
267
272
  * Resource diff between two versions. Each field lists resources present in
package/dist/lib/shims.js CHANGED
@@ -11,6 +11,7 @@
11
11
  import * as fs from 'fs';
12
12
  import * as path from 'path';
13
13
  import * as os from 'os';
14
+ import { execFileSync } from 'child_process';
14
15
  import { fileURLToPath } from 'url';
15
16
  import { confirm, select } from '@inquirer/prompts';
16
17
  import { IS_WINDOWS } from './platform/index.js';
@@ -1520,18 +1521,15 @@ Then restart your shell or run:
1520
1521
  source ~/${rcFile}`;
1521
1522
  }
1522
1523
  /**
1523
- * Add shims directory to shell PATH configuration.
1524
- * Returns true if added, false if already present or failed.
1524
+ * Add the shims directory to PATH: edits the shell rc file on POSIX, or registers
1525
+ * it on the Windows User PATH (registry + WM_SETTINGCHANGE). Idempotent.
1525
1526
  */
1526
1527
  export function addShimsToPath(overrides) {
1527
- // Windows has no shell rc file to edit: the primary `agents` command is already
1528
- // on PATH via npm's global bin, and bare shorthands / versioned aliases are
1529
- // handled by the `.cmd` shims plus the PATH guidance printed at install time.
1530
- // Report "already present" so callers don't emit a misleading "added to
1531
- // ~/.bashrc / source ~/.bashrc" message. (The `shell` override is the test hook
1532
- // for exercising the POSIX path, so it bypasses this short-circuit.)
1528
+ // Windows has no shell rc file to edit. Register the shims dir on the User PATH
1529
+ // via the platform-native mechanism instead. (The `shell` override is the test
1530
+ // hook for exercising the POSIX path, so it bypasses this branch.)
1533
1531
  if (IS_WINDOWS && !overrides?.shell) {
1534
- return { success: true, alreadyPresent: true };
1532
+ return addShimsToWindowsUserPath(overrides?.shimsDir || getShimsDir());
1535
1533
  }
1536
1534
  const shimsDir = overrides?.shimsDir || getShimsDir();
1537
1535
  const { rcFile, rcPath, shell } = getShellRcFile(overrides);
@@ -1565,16 +1563,53 @@ export function addShimsToPath(overrides) {
1565
1563
  const separator = contentWithoutShimLines.length > 0 && !contentWithoutShimLines.endsWith('\n') ? '\n' : '';
1566
1564
  let newContent = contentWithoutShimLines + separator + exportBlock;
1567
1565
  newContent = newContent.replace(/\n{2,}$/g, '\n');
1566
+ const location = `~/${rcFile}`;
1567
+ const reloadHint = `Restart your shell or run: source ~/${rcFile}`;
1568
1568
  if (newContent === content) {
1569
- return { success: true, alreadyPresent: true, rcFile };
1569
+ return { success: true, alreadyPresent: true, rcFile, location, reloadHint };
1570
1570
  }
1571
1571
  fs.writeFileSync(rcPath, newContent, 'utf-8');
1572
- return { success: true, rcFile };
1572
+ return { success: true, rcFile, location, reloadHint };
1573
1573
  }
1574
1574
  catch (err) {
1575
1575
  return { success: false, error: `Could not write ${rcFile}: ${err.message}` };
1576
1576
  }
1577
1577
  }
1578
+ /**
1579
+ * Register the shims dir on the Windows User PATH via the .NET environment API,
1580
+ * which writes the registry AND broadcasts WM_SETTINGCHANGE — the correct analog
1581
+ * of editing a shell rc file (no `setx` truncation, no manual step). Idempotent:
1582
+ * a no-op when the dir is already present. The shims dir is passed via an env var
1583
+ * so it is never interpolated into the PowerShell script text.
1584
+ */
1585
+ function addShimsToWindowsUserPath(shimsDir) {
1586
+ const script = [
1587
+ '$d = $env:AGENTS_SHIMS_DIR',
1588
+ "$u = [Environment]::GetEnvironmentVariable('Path','User')",
1589
+ "if ($null -eq $u) { $u = '' }",
1590
+ "$parts = @($u -split ';' | Where-Object { $_ -ne '' })",
1591
+ "if ($parts -contains $d) { 'present' } else {",
1592
+ " [Environment]::SetEnvironmentVariable('Path', (($parts + $d) -join ';'), 'User')",
1593
+ " 'added'",
1594
+ '}',
1595
+ ].join('\n');
1596
+ try {
1597
+ const out = execFileSync('powershell', ['-NoProfile', '-NonInteractive', '-Command', script], {
1598
+ encoding: 'utf-8',
1599
+ env: { ...process.env, AGENTS_SHIMS_DIR: shimsDir },
1600
+ stdio: ['ignore', 'pipe', 'pipe'],
1601
+ }).trim();
1602
+ return {
1603
+ success: true,
1604
+ alreadyPresent: out.includes('present'),
1605
+ location: 'your user PATH',
1606
+ reloadHint: 'Open a new terminal for the change to take effect.',
1607
+ };
1608
+ }
1609
+ catch (err) {
1610
+ return { success: false, error: `Could not update the Windows user PATH: ${err.message}` };
1611
+ }
1612
+ }
1578
1613
  export function listAgentsWithInstalledVersions() {
1579
1614
  const versionsDir = getVersionsDir();
1580
1615
  if (!fs.existsSync(versionsDir)) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@phnx-labs/agents-cli",
3
- "version": "1.20.7",
3
+ "version": "1.20.8",
4
4
  "description": "One CLI for all your AI coding agents - versions, config, cloud dispatch, sessions, and teams (now with first-class Grok Build CLI support)",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",
@@ -144,17 +144,17 @@ function isAlreadyConfigured(rcFile) {
144
144
  }
145
145
 
146
146
  async function main() {
147
- // Windows has no shell rc files to edit. Write the `.cmd` shorthands and point
148
- // the user at the PATH entry they can add (the primary `agents` command is
149
- // already on PATH via npm's global bin, so this only affects bare shorthands
150
- // and versioned aliases).
147
+ // Windows has no shell rc files to edit. Write the `.cmd` shorthands here; the
148
+ // shims dir gets registered on the User PATH by `agents setup` (via the native
149
+ // registry API), so we point the user there instead of mutating PATH from an
150
+ // npm lifecycle script. The primary `agents` command is already on PATH via
151
+ // npm's global bin and works immediately.
151
152
  if (process.platform === 'win32') {
152
153
  console.log(`\nagents-cli installed.`);
153
154
  const written = writeAliasShims();
154
155
  console.log(` Installed shorthands: ${written.join(', ')}`);
155
- console.log(`\nTo use bare shorthands (${ALIASES.join(', ')}) and versioned aliases, add this to your PATH:`);
156
- console.log(` ${SHIMS_DIR}`);
157
- console.log(` PowerShell: setx PATH "$env:PATH;${SHIMS_DIR}" (then open a new terminal)`);
156
+ console.log(`\nNext: run agents setup — it finishes setup and adds the shims dir to your PATH`);
157
+ console.log(`(so the bare shorthands ${ALIASES.join(', ')} and versioned aliases work in a new terminal).`);
158
158
  }
159
159
  // Opt-in: AGENTS_INIT_SHELL=1 npm install -g @phnx-labs/agents-cli
160
160
  else if (process.env.AGENTS_INIT_SHELL === '1') {