@phnx-labs/agents-cli 1.20.5 → 1.20.7

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 (70) hide show
  1. package/CHANGELOG.md +13 -0
  2. package/README.md +1 -1
  3. package/dist/commands/browser.js +31 -4
  4. package/dist/commands/computer-actions.d.ts +36 -0
  5. package/dist/commands/computer-actions.js +328 -0
  6. package/dist/commands/computer.js +74 -55
  7. package/dist/commands/defaults.d.ts +7 -0
  8. package/dist/commands/defaults.js +89 -0
  9. package/dist/commands/exec.js +24 -6
  10. package/dist/commands/inspect.d.ts +38 -7
  11. package/dist/commands/inspect.js +194 -24
  12. package/dist/commands/rules.js +3 -3
  13. package/dist/commands/secrets.js +46 -9
  14. package/dist/commands/sessions.js +9 -12
  15. package/dist/commands/setup.js +2 -2
  16. package/dist/commands/teams.js +108 -11
  17. package/dist/commands/view.d.ts +12 -1
  18. package/dist/commands/view.js +121 -38
  19. package/dist/index.js +61 -22
  20. package/dist/lib/agents.d.ts +10 -6
  21. package/dist/lib/agents.js +23 -14
  22. package/dist/lib/browser/chrome.d.ts +10 -0
  23. package/dist/lib/browser/chrome.js +84 -3
  24. package/dist/lib/daemon.js +4 -7
  25. package/dist/lib/exec.d.ts +9 -0
  26. package/dist/lib/exec.js +85 -9
  27. package/dist/lib/migrate.js +6 -4
  28. package/dist/lib/permissions.d.ts +23 -0
  29. package/dist/lib/permissions.js +89 -7
  30. package/dist/lib/platform/exec.d.ts +9 -0
  31. package/dist/lib/platform/exec.js +24 -0
  32. package/dist/lib/platform/index.d.ts +20 -0
  33. package/dist/lib/platform/index.js +20 -0
  34. package/dist/lib/platform/paths.d.ts +22 -0
  35. package/dist/lib/platform/paths.js +49 -0
  36. package/dist/lib/platform/process.d.ts +12 -0
  37. package/dist/lib/platform/process.js +22 -0
  38. package/dist/lib/plugin-marketplace.js +1 -1
  39. package/dist/lib/project-launch.d.ts +5 -0
  40. package/dist/lib/project-launch.js +37 -0
  41. package/dist/lib/pty-client.js +13 -5
  42. package/dist/lib/pty-server.d.ts +24 -1
  43. package/dist/lib/pty-server.js +109 -29
  44. package/dist/lib/resources/rules.js +1 -1
  45. package/dist/lib/resources/skills.js +1 -1
  46. package/dist/lib/resources.d.ts +2 -0
  47. package/dist/lib/resources.js +2 -1
  48. package/dist/lib/rotate.js +6 -18
  49. package/dist/lib/run-config.d.ts +9 -0
  50. package/dist/lib/run-config.js +35 -0
  51. package/dist/lib/run-defaults.d.ts +42 -0
  52. package/dist/lib/run-defaults.js +180 -0
  53. package/dist/lib/secrets/Agents CLI.app/Contents/CodeResources +0 -0
  54. package/dist/lib/secrets/Agents CLI.app/Contents/MacOS/Agents CLI +0 -0
  55. package/dist/lib/secrets/install-helper.d.ts +11 -3
  56. package/dist/lib/secrets/install-helper.js +48 -6
  57. package/dist/lib/secrets/linux.d.ts +12 -0
  58. package/dist/lib/secrets/linux.js +30 -16
  59. package/dist/lib/session/artifacts.js +8 -2
  60. package/dist/lib/shims.d.ts +9 -1
  61. package/dist/lib/shims.js +80 -3
  62. package/dist/lib/staleness/detectors/hooks.js +1 -1
  63. package/dist/lib/staleness/writers/hooks.js +1 -1
  64. package/dist/lib/teams/agents.js +5 -7
  65. package/dist/lib/teams/api.d.ts +67 -0
  66. package/dist/lib/teams/api.js +78 -0
  67. package/dist/lib/types.d.ts +15 -6
  68. package/dist/lib/versions.js +4 -4
  69. package/package.json +5 -2
  70. package/scripts/postinstall.js +18 -1
package/dist/index.js CHANGED
@@ -76,6 +76,7 @@ import { registerDaemonCommands } from './commands/daemon.js';
76
76
  import { registerRoutinesCommands } from './commands/routines.js';
77
77
  import { registerRunCommand } from './commands/exec.js';
78
78
  import { registerModelsCommand } from './commands/models.js';
79
+ import { registerDefaultsCommands } from './commands/defaults.js';
79
80
  import { registerPruneCommand } from './commands/prune.js';
80
81
  import { registerTrashCommands } from './commands/trash.js';
81
82
  import { registerDoctorCommand } from './commands/doctor.js';
@@ -102,6 +103,21 @@ import { isInteractiveTerminal, isPromptCancelled } from './commands/utils.js';
102
103
  import { AGENTS } from './lib/agents.js';
103
104
  import { getGlobalDefault, listInstalledVersions } from './lib/versions.js';
104
105
  import { addShimsToPath, ensureShimCurrent, ensureVersionedAliasCurrent, getPathShadowingExecutable, getPathSetupInstructions, getShimsDir, isShimsInPath, listAgentsWithInstalledVersions, removeLegacyUserShim, } from './lib/shims.js';
106
+ import { IS_WINDOWS } from './lib/platform/index.js';
107
+ // Transparent shim delegate: the generated Windows `.cmd` shims invoke
108
+ // `agents __shim <agent>[@version] <raw args>`. Intercept here, before commander
109
+ // parses anything, so the agent's own flags (`--help`, `--version`, etc.) pass
110
+ // through completely untouched and we skip registering the full command tree.
111
+ if (process.argv[2] === '__shim') {
112
+ const spec = process.argv[3] || '';
113
+ const rawArgs = process.argv.slice(4);
114
+ const atIndex = spec.indexOf('@');
115
+ const agent = atIndex === -1 ? spec : spec.slice(0, atIndex);
116
+ const pinned = atIndex === -1 ? undefined : spec.slice(atIndex + 1);
117
+ const { execShimPassthrough } = await import('./lib/exec.js');
118
+ const code = await execShimPassthrough(agent, rawArgs, process.cwd(), pinned || undefined);
119
+ process.exit(code);
120
+ }
105
121
  const program = new Command();
106
122
  program
107
123
  .name('agents')
@@ -133,7 +149,7 @@ Agent versions:
133
149
  prune cleanup [target] Remove orphan resources and older duplicate version installs
134
150
  trash Inspect and restore soft-deleted version directories
135
151
  view [agent[@version]] List versions, or inspect one in detail
136
- inspect <agent>[@version] Deep details for one agent+version — paths, capabilities, resources, drill into any kind
152
+ inspect <target> Deep details for one agent+version, or a DotAgents repo (user|system|project|alias|path)
137
153
 
138
154
  Agent configuration (synced across versions):
139
155
  rules Instructions given to agents (CLAUDE.md, etc.)
@@ -151,6 +167,7 @@ Packages:
151
167
 
152
168
  Run and dispatch:
153
169
  run <agent|profile> [prompt] Run an agent. Omit prompt for interactive mode.
170
+ defaults Configure run defaults by agent/version selector
154
171
  teams Coordinate multiple agents on shared work
155
172
  routines Run agents on a cron schedule (scheduler auto-starts)
156
173
  sessions Browse, search, and replay past runs (live-search in TTY; grouped by workspace)
@@ -217,9 +234,12 @@ async function showWhatsNew(fromVersion, toVersion) {
217
234
  const versionMatch = line.match(/^## (\d+\.\d+\.\d+)/);
218
235
  if (versionMatch) {
219
236
  currentVersion = versionMatch[1];
220
- const isNewer = currentVersion !== fromVersion &&
221
- compareVersions(currentVersion, fromVersion) > 0;
222
- inRelevantSection = isNewer;
237
+ // Only the range the user actually moved through: (fromVersion, toVersion].
238
+ // Bounding the top end matters when upgrading to a specific older
239
+ // version, and guards against a changelog that lists unreleased entries.
240
+ const inRange = compareVersions(currentVersion, fromVersion) > 0 &&
241
+ compareVersions(currentVersion, toVersion) <= 0;
242
+ inRelevantSection = inRange;
223
243
  if (inRelevantSection) {
224
244
  relevantChanges.push('');
225
245
  relevantChanges.push(chalk.bold(`v${currentVersion}`));
@@ -279,11 +299,14 @@ function saveUpdateCheck(latestVersion) {
279
299
  }
280
300
  }
281
301
  /** Fetch the exact latest npm version plus its registry integrity hash. */
282
- async function fetchLatestNpmPackageMetadata(timeoutMs = 5000) {
283
- const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/latest`, {
302
+ async function fetchNpmPackageMetadata(versionOrTag = 'latest', timeoutMs = 5000) {
303
+ const response = await fetch(`https://registry.npmjs.org/${NPM_PACKAGE_NAME}/${versionOrTag}`, {
284
304
  signal: AbortSignal.timeout(timeoutMs),
285
305
  });
286
306
  if (!response.ok) {
307
+ if (response.status === 404) {
308
+ throw new Error(`${NPM_PACKAGE_NAME}@${versionOrTag} not found on npm`);
309
+ }
287
310
  throw new Error('Could not reach npm registry');
288
311
  }
289
312
  const data = await response.json();
@@ -338,7 +361,7 @@ async function promptUpgrade(latestVersion) {
338
361
  const { spawnSync } = await import('child_process');
339
362
  let spinner = ora('Resolving package metadata...').start();
340
363
  try {
341
- const metadata = await fetchLatestNpmPackageMetadata();
364
+ const metadata = await fetchNpmPackageMetadata();
342
365
  spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${metadata.version}`);
343
366
  printResolvedPackage(metadata);
344
367
  const approved = await confirm({
@@ -453,6 +476,13 @@ async function maybeBootstrapShimIntegration(requestedCommand, helpOrVersionRequ
453
476
  for (const agent of installedAgents) {
454
477
  removeLegacyUserShim(agent);
455
478
  }
479
+ // The remaining flow is rc-file PATH repair, which is POSIX-only. On Windows
480
+ // the shims were just regenerated (incl. `.cmd` companions) above; PATH setup
481
+ // is covered by the install-time guidance, so stop here rather than printing
482
+ // shell-rc instructions that don't apply.
483
+ if (IS_WINDOWS) {
484
+ return;
485
+ }
456
486
  const defaultAgents = installedAgents.filter((agent) => getGlobalDefault(agent));
457
487
  const shadowed = defaultAgents
458
488
  .map((agent) => ({ agent, shadowedBy: getPathShadowingExecutable(agent) }))
@@ -583,6 +613,7 @@ registerPackagesCommands(program);
583
613
  registerDaemonCommands(program);
584
614
  registerRoutinesCommands(program);
585
615
  registerRunCommand(program);
616
+ registerDefaultsCommands(program);
586
617
  registerModelsCommand(program);
587
618
  registerPruneCommand(program);
588
619
  registerTrashCommands(program);
@@ -627,26 +658,31 @@ for (const alias of ['jobs', 'cron']) {
627
658
  }
628
659
  program
629
660
  .command('upgrade')
630
- .description('Upgrade agents-cli to the latest version')
661
+ .description('Upgrade agents-cli to the latest version (or a specific [version])')
662
+ .argument('[version]', 'Target version or dist-tag to install (default: latest)')
631
663
  .option('-y, --yes', 'Install without an interactive confirmation prompt')
632
- .action(async (options) => {
633
- let spinner = ora('Checking for updates...').start();
664
+ .action(async (version, options) => {
665
+ const target = version ?? 'latest';
666
+ let spinner = ora(version ? `Resolving ${NPM_PACKAGE_NAME}@${target}...` : 'Checking for updates...').start();
634
667
  try {
635
- const metadata = await fetchLatestNpmPackageMetadata();
636
- const latestVersion = metadata.version;
637
- if (latestVersion === VERSION) {
638
- spinner.succeed(`Already on latest version (${VERSION})`);
668
+ const metadata = await fetchNpmPackageMetadata(target);
669
+ const resolvedVersion = metadata.version;
670
+ if (resolvedVersion === VERSION) {
671
+ spinner.succeed(`Already on ${VERSION}`);
639
672
  return;
640
673
  }
641
- if (compareVersions(latestVersion, VERSION) <= 0) {
642
- spinner.succeed(`Already ahead of latest (${VERSION} >= ${latestVersion})`);
674
+ // For `latest` (no explicit version) skip when already ahead. When a
675
+ // version is named explicitly, honor it even if it's a downgrade.
676
+ if (!version && compareVersions(resolvedVersion, VERSION) <= 0) {
677
+ spinner.succeed(`Already ahead of latest (${VERSION} >= ${resolvedVersion})`);
643
678
  return;
644
679
  }
645
- spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${latestVersion}`);
680
+ const direction = compareVersions(resolvedVersion, VERSION) < 0 ? 'Downgrade' : 'Upgrade';
681
+ spinner.succeed(`Resolved ${NPM_PACKAGE_NAME}@${resolvedVersion}`);
646
682
  printResolvedPackage(metadata);
647
683
  if (isInteractiveTerminal() && !options.yes) {
648
684
  const approved = await confirm({
649
- message: `Install ${NPM_PACKAGE_NAME}@${latestVersion}?`,
685
+ message: `Install ${NPM_PACKAGE_NAME}@${resolvedVersion}?`,
650
686
  default: false,
651
687
  });
652
688
  if (!approved) {
@@ -654,14 +690,17 @@ program
654
690
  return;
655
691
  }
656
692
  }
657
- spinner = ora(`Upgrading ${VERSION} -> ${latestVersion}...`).start();
693
+ spinner = ora(`${direction === 'Downgrade' ? 'Downgrading' : 'Upgrading'} ${VERSION} -> ${resolvedVersion}...`).start();
658
694
  await installResolvedPackage(metadata);
659
- spinner.succeed(`Upgraded to ${latestVersion}`);
660
- await showWhatsNew(VERSION, latestVersion);
695
+ spinner.succeed(`${direction}d to ${resolvedVersion}`);
696
+ // Only show the changelog for a genuine upgrade range.
697
+ if (compareVersions(resolvedVersion, VERSION) > 0) {
698
+ await showWhatsNew(VERSION, resolvedVersion);
699
+ }
661
700
  }
662
701
  catch (err) {
663
702
  spinner.fail('Upgrade failed');
664
- console.log(chalk.gray('Run manually: agents upgrade --yes'));
703
+ console.log(chalk.gray(`Run manually: agents upgrade ${version ? version + ' ' : ''}--yes`));
665
704
  }
666
705
  });
667
706
  registerPullCommand(program);
@@ -69,12 +69,16 @@ export declare function ensureSkillsDir(agentId: AgentId): void;
69
69
  * The agent's config-dir name relative to $HOME — e.g. '.claude',
70
70
  * '.gemini/antigravity-cli', '.config/amp', '.kimi-code'.
71
71
  *
72
- * This is the path segment to join onto a (version) home root when locating an
73
- * agent's commands/skills/plugins. Do NOT hardcode `.${agentId}`: it is wrong
74
- * for every agent whose config dir is nested or lives under ~/.config —
75
- * antigravity (~/.gemini/antigravity-cli), amp (~/.config/amp),
76
- * goose (~/.config/goose), kimi (~/.kimi-code). Mirrors the shim `configDirName`
77
- * derivation in shims.ts.
72
+ * Path segment to join onto a (version) home root when locating an agent's
73
+ * commands/skills/plugins. Do NOT hardcode `.${agentId}`: it is wrong for
74
+ * every agent whose config dir is nested or under ~/.config — antigravity
75
+ * (~/.gemini/antigravity-cli), amp (~/.config/amp), goose (~/.config/goose),
76
+ * kimi (~/.kimi-code). Mirrors the shim configDirName derivation in shims.ts.
77
+ *
78
+ * Relativized against the module-level HOME constant (the same value used to
79
+ * build every `configDir`), NOT a fresh `os.homedir()` — so the result stays a
80
+ * clean relative name even when HOME is overridden after module load (tests,
81
+ * sandboxes). Using `os.homedir()` here would yield `../../real/home/.claude`.
78
82
  */
79
83
  export declare function agentConfigDirName(agentId: AgentId): string;
80
84
  /** Account identity and billing information extracted from an agent's auth config. */
@@ -446,13 +446,18 @@ export const AGENTS = {
446
446
  rulesImports: true,
447
447
  },
448
448
  },
449
+ // Kimi Code CLI (`kimi`) — Moonshot AI coding agent.
450
+ // Install: `curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash`
451
+ // or: `npm install -g @moonshot-ai/kimi-code`
452
+ // Config: `~/.kimi-code/config.toml`, `~/.kimi-code/mcp.json`,
453
+ // `~/.kimi-code/skills/`, `~/.kimi-code/hooks/`
449
454
  kimi: {
450
455
  id: 'kimi',
451
456
  name: 'Kimi',
452
457
  color: 'magentaBright',
453
- cliCommand: 'kimi-code',
454
- npmPackage: '',
455
- installScript: '',
458
+ cliCommand: 'kimi',
459
+ npmPackage: '@moonshot-ai/kimi-code',
460
+ installScript: 'curl -fsSL https://code.kimi.com/kimi-code/install.sh | bash',
456
461
  configDir: path.join(HOME, '.kimi-code'),
457
462
  commandsDir: '',
458
463
  commandsSubdir: '',
@@ -465,14 +470,14 @@ export const AGENTS = {
465
470
  capabilities: {
466
471
  hooks: true,
467
472
  mcp: true,
468
- allowlist: false,
469
- skills: false,
473
+ allowlist: true,
474
+ skills: true,
470
475
  commands: false,
471
- plugins: false,
476
+ plugins: true,
472
477
  subagents: false,
473
478
  rules: { file: 'AGENTS.md' },
474
479
  workflows: false,
475
- modes: ['plan', 'edit', 'skip'],
480
+ modes: ['plan', 'edit', 'auto', 'skip'],
476
481
  rulesImports: false,
477
482
  },
478
483
  },
@@ -690,15 +695,19 @@ export function ensureSkillsDir(agentId) {
690
695
  * The agent's config-dir name relative to $HOME — e.g. '.claude',
691
696
  * '.gemini/antigravity-cli', '.config/amp', '.kimi-code'.
692
697
  *
693
- * This is the path segment to join onto a (version) home root when locating an
694
- * agent's commands/skills/plugins. Do NOT hardcode `.${agentId}`: it is wrong
695
- * for every agent whose config dir is nested or lives under ~/.config —
696
- * antigravity (~/.gemini/antigravity-cli), amp (~/.config/amp),
697
- * goose (~/.config/goose), kimi (~/.kimi-code). Mirrors the shim `configDirName`
698
- * derivation in shims.ts.
698
+ * Path segment to join onto a (version) home root when locating an agent's
699
+ * commands/skills/plugins. Do NOT hardcode `.${agentId}`: it is wrong for
700
+ * every agent whose config dir is nested or under ~/.config — antigravity
701
+ * (~/.gemini/antigravity-cli), amp (~/.config/amp), goose (~/.config/goose),
702
+ * kimi (~/.kimi-code). Mirrors the shim configDirName derivation in shims.ts.
703
+ *
704
+ * Relativized against the module-level HOME constant (the same value used to
705
+ * build every `configDir`), NOT a fresh `os.homedir()` — so the result stays a
706
+ * clean relative name even when HOME is overridden after module load (tests,
707
+ * sandboxes). Using `os.homedir()` here would yield `../../real/home/.claude`.
699
708
  */
700
709
  export function agentConfigDirName(agentId) {
701
- return path.relative(os.homedir(), AGENTS[agentId].configDir);
710
+ return path.relative(HOME, AGENTS[agentId].configDir);
702
711
  }
703
712
  /** Return the email address associated with the agent's auth config, or null. */
704
713
  export async function getAccountEmail(agentId, home) {
@@ -1,5 +1,15 @@
1
1
  import type { ChromeOptions } from './types.js';
2
2
  import type { BrowserType } from './types.js';
3
+ /**
4
+ * True when `binaryPath` is a shebang script rather than a native browser
5
+ * executable. The Linux distro launchers (`/usr/bin/brave-browser`, …) are such
6
+ * scripts; `launchBrowser` can't drive one over `--remote-debugging-pipe` (see
7
+ * resolveBrowserBinary). `profiles doctor` uses this to flag a profile whose
8
+ * binary resolves to a wrapper we couldn't unwrap. Shebang scripts are a
9
+ * Linux/Unix concept — returns false on Windows/macOS app bundles.
10
+ */
11
+ export declare function isLauncherScript(binaryPath: string): boolean;
12
+ export declare function resolveBrowserBinary(binaryPath: string): string;
3
13
  export declare function findBrowserPath(browserType: BrowserType, customBinary?: string): string;
4
14
  /**
5
15
  * Walk the per-platform priority list and return the first browser that's
@@ -42,12 +42,90 @@ const BROWSER_PATHS = {
42
42
  custom: [],
43
43
  },
44
44
  };
45
+ /**
46
+ * On Debian/Ubuntu the canonical launchers under `/usr/bin`
47
+ * (`brave-browser`, `google-chrome`, `chromium`) are not the browser ELF —
48
+ * they're `#!/bin/bash` wrapper scripts (the upstream Chromium wrapper) that,
49
+ * as their final step, run the real binary as a NON-exec child:
50
+ *
51
+ * exec < /dev/null
52
+ * exec > >(exec cat)
53
+ * exec 2> >(exec cat >&2)
54
+ * "$HERE/brave" "$@" || true
55
+ *
56
+ * That breaks `launchBrowser`'s `--remote-debugging-pipe` transport two ways:
57
+ * the std-fd sanitization (and the extra `cat` process-substitution children)
58
+ * disturbs the inherited CDP pipe on fd 3/4, and the pid we record is the
59
+ * wrapper's, not the browser's. The symptom is `read ECONNRESET` /
60
+ * `CDP connection closed` right after spawn (issue #229).
61
+ *
62
+ * Follow the wrapper to the ELF it execs. The wrapper sets
63
+ * `HERE="dirname(readlink -f "$0")"` and invokes `"$HERE/<name>"`, so we
64
+ * resolve the script path, scan for that invocation line, and join the two.
65
+ * Returns the original path untouched when it's already an ELF, when it's not
66
+ * a resolvable wrapper, or on any non-Linux platform.
67
+ */
68
+ function readsAsShebangScript(binaryPath) {
69
+ let fd;
70
+ try {
71
+ fd = fs.openSync(binaryPath, 'r');
72
+ }
73
+ catch {
74
+ return false;
75
+ }
76
+ try {
77
+ const head = Buffer.alloc(2);
78
+ fs.readSync(fd, head, 0, 2, 0);
79
+ // ELF binaries start with 0x7f 'E'; shebang scripts with '#!'.
80
+ return head[0] === 0x23 && head[1] === 0x21;
81
+ }
82
+ finally {
83
+ fs.closeSync(fd);
84
+ }
85
+ }
86
+ /**
87
+ * True when `binaryPath` is a shebang script rather than a native browser
88
+ * executable. The Linux distro launchers (`/usr/bin/brave-browser`, …) are such
89
+ * scripts; `launchBrowser` can't drive one over `--remote-debugging-pipe` (see
90
+ * resolveBrowserBinary). `profiles doctor` uses this to flag a profile whose
91
+ * binary resolves to a wrapper we couldn't unwrap. Shebang scripts are a
92
+ * Linux/Unix concept — returns false on Windows/macOS app bundles.
93
+ */
94
+ export function isLauncherScript(binaryPath) {
95
+ if (os.platform() === 'win32')
96
+ return false;
97
+ return readsAsShebangScript(binaryPath);
98
+ }
99
+ export function resolveBrowserBinary(binaryPath) {
100
+ if (os.platform() !== 'linux')
101
+ return binaryPath;
102
+ // Only shebang scripts need unwrapping; a real ELF passes straight through.
103
+ if (!readsAsShebangScript(binaryPath))
104
+ return binaryPath;
105
+ let script;
106
+ let realScriptPath;
107
+ try {
108
+ realScriptPath = fs.realpathSync(binaryPath);
109
+ script = fs.readFileSync(realScriptPath, 'utf8');
110
+ }
111
+ catch {
112
+ return binaryPath;
113
+ }
114
+ // Match the Chromium wrapper's launch line: `"$HERE/<name>" "$@"`, optionally
115
+ // prefixed with `exec -a "$0"`. The captured name is the real ELF, sitting in
116
+ // the same directory as the resolved wrapper.
117
+ const match = script.match(/"\$HERE\/([A-Za-z0-9._-]+)"\s+"\$@"/);
118
+ if (!match)
119
+ return binaryPath;
120
+ const realBinary = path.join(path.dirname(realScriptPath), match[1]);
121
+ return fs.existsSync(realBinary) ? realBinary : binaryPath;
122
+ }
45
123
  export function findBrowserPath(browserType, customBinary) {
46
124
  if (customBinary) {
47
125
  if (!fs.existsSync(customBinary)) {
48
126
  throw new Error(`Custom binary not found: ${customBinary}`);
49
127
  }
50
- return customBinary;
128
+ return resolveBrowserBinary(customBinary);
51
129
  }
52
130
  if (browserType === 'custom') {
53
131
  throw new Error('browser: custom requires a binary path in the profile');
@@ -60,9 +138,12 @@ export function findBrowserPath(browserType, customBinary) {
60
138
  const candidates = platformPaths[browserType] || [];
61
139
  for (const p of candidates) {
62
140
  if (fs.existsSync(p)) {
63
- return p;
141
+ return resolveBrowserBinary(p);
64
142
  }
65
143
  }
144
+ if (browserType === 'comet' && platform !== 'darwin') {
145
+ throw new Error('Browser "comet" is macOS-only (Comet is a macOS Chromium fork). Use chrome, chromium, brave, or edge on this platform.');
146
+ }
66
147
  throw new Error(`Browser "${browserType}" not found. Install it first.`);
67
148
  }
68
149
  // Per-platform Chromium-family priority list for "no --profile" auto-pick.
@@ -98,7 +179,7 @@ export function findFirstInstalledBrowser(platform = os.platform()) {
98
179
  const candidates = platformPaths[browserType] || [];
99
180
  for (const p of candidates) {
100
181
  if (fs.existsSync(p)) {
101
- return { browserType, binary: p };
182
+ return { browserType, binary: resolveBrowserBinary(p) };
102
183
  }
103
184
  }
104
185
  }
@@ -11,6 +11,7 @@ import * as fs from 'fs';
11
11
  import * as path from 'path';
12
12
  import * as os from 'os';
13
13
  import { getDaemonDir as getDaemonDirRoot } from './state.js';
14
+ import { isAlive } from './platform/index.js';
14
15
  import { listJobs as listAllJobs } from './routines.js';
15
16
  import { JobScheduler } from './scheduler.js';
16
17
  import { executeJobDetached, monitorRunningJobs } from './runner.js';
@@ -114,14 +115,10 @@ export function isDaemonRunning() {
114
115
  const pid = readDaemonPid();
115
116
  if (!pid)
116
117
  return false;
117
- try {
118
- process.kill(pid, 0);
118
+ if (isAlive(pid))
119
119
  return true;
120
- }
121
- catch {
122
- removeDaemonPid();
123
- return false;
124
- }
120
+ removeDaemonPid();
121
+ return false;
125
122
  }
126
123
  /** Redact values that look like tokens or credentials in a log message. */
127
124
  function redactSecrets(message) {
@@ -95,6 +95,15 @@ export declare const AGENT_COMMANDS: Record<AgentId, AgentCommandTemplate>;
95
95
  export declare function buildExecCommand(options: ExecOptions): string[];
96
96
  /** Spawn an agent and return its exit code. Convenience wrapper over spawnAgent. */
97
97
  export declare function execAgent(options: ExecOptions): Promise<number>;
98
+ /**
99
+ * Transparent passthrough exec for generated shims — the node-side delegate that
100
+ * Windows `.cmd` shims call. Resolves the active version (explicit pin, else
101
+ * project/default) and execs the real binary with the user's RAW args and the
102
+ * per-version env isolation, WITHOUT injecting mode/model/reasoning flags. This
103
+ * mirrors what the POSIX bash shim does inline (`exec $BINARY $launchArgs "$@"`),
104
+ * keeping version resolution in one place instead of reimplementing it in batch.
105
+ */
106
+ export declare function execShimPassthrough(agent: AgentId, rawArgs: string[], cwd: string, pinnedVersion?: string): Promise<number>;
98
107
  /**
99
108
  * Patterns that indicate a rate/usage limit. Matching is intentionally broad
100
109
  * because providers phrase these differently -- Anthropic uses "5-hour limit"
package/dist/lib/exec.js CHANGED
@@ -11,7 +11,7 @@ import * as path from 'path';
11
11
  import { ALL_MODES } from './types.js';
12
12
  import { AGENTS } from './agents.js';
13
13
  import { parseTimeout } from './routines.js';
14
- import { getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
14
+ import { getBinaryPath, getVersionHomePath, isVersionInstalled, resolveVersion } from './versions.js';
15
15
  import { resolveModel, buildReasoningFlags } from './models.js';
16
16
  import { maybeRotate, createTimer, redactPrompt, redactArgs } from './events.js';
17
17
  import { sanitizeProcessEnv } from './secrets/bundles.js';
@@ -118,6 +118,7 @@ export function buildExecEnv(options) {
118
118
  }
119
119
  delete result.CODEX_HOME;
120
120
  delete result.COPILOT_HOME;
121
+ delete result.KIMI_CODE_HOME;
121
122
  }
122
123
  else if (options.agent === 'codex') {
123
124
  const cwd = options.cwd || process.cwd();
@@ -130,6 +131,7 @@ export function buildExecEnv(options) {
130
131
  }
131
132
  delete result.CLAUDE_CONFIG_DIR;
132
133
  delete result.COPILOT_HOME;
134
+ delete result.KIMI_CODE_HOME;
133
135
  }
134
136
  else if (options.agent === 'copilot') {
135
137
  // Copilot honors COPILOT_HOME (relocates ~/.copilot, including settings,
@@ -145,11 +147,28 @@ export function buildExecEnv(options) {
145
147
  }
146
148
  delete result.CLAUDE_CONFIG_DIR;
147
149
  delete result.CODEX_HOME;
150
+ delete result.KIMI_CODE_HOME;
151
+ }
152
+ else if (options.agent === 'kimi') {
153
+ // Kimi honors KIMI_CODE_HOME (relocates ~/.kimi-code, including config,
154
+ // skills, hooks, sessions). Pin it at the per-version home.
155
+ const cwd = options.cwd || process.cwd();
156
+ const resolvedVersion = options.version ?? resolveVersion('kimi', cwd);
157
+ const version = options.version
158
+ ? resolvedVersion
159
+ : (resolvedVersion && isVersionInstalled('kimi', resolvedVersion) ? resolvedVersion : null);
160
+ if (version) {
161
+ result.KIMI_CODE_HOME = path.join(getVersionHomePath('kimi', version), '.kimi-code');
162
+ }
163
+ delete result.CLAUDE_CONFIG_DIR;
164
+ delete result.CODEX_HOME;
165
+ delete result.COPILOT_HOME;
148
166
  }
149
167
  else {
150
168
  delete result.CLAUDE_CONFIG_DIR;
151
169
  delete result.CODEX_HOME;
152
170
  delete result.COPILOT_HOME;
171
+ delete result.KIMI_CODE_HOME;
153
172
  }
154
173
  return {
155
174
  ...result,
@@ -322,14 +341,15 @@ export const AGENT_COMMANDS = {
322
341
  modelFlag: '--model',
323
342
  },
324
343
  kimi: {
325
- base: ['kimi-code'],
344
+ base: ['kimi'],
326
345
  promptFlag: '-p',
327
346
  modeFlags: {
328
- plan: [],
347
+ plan: ['--plan'],
329
348
  edit: [],
330
- skip: [],
349
+ auto: ['--auto'],
350
+ skip: ['--yolo'],
331
351
  },
332
- jsonFlags: [],
352
+ jsonFlags: ['--output-format', 'stream-json'],
333
353
  modelFlag: '--model',
334
354
  },
335
355
  };
@@ -347,10 +367,27 @@ export function buildExecCommand(options) {
347
367
  // Resolve to the absolute path of the shim so spawn doesn't depend on PATH —
348
368
  // on Linux installs where the shims dir isn't on PATH, spawning the bare
349
369
  // versioned name fails with ENOENT even though `agents view` shows the agent.
370
+ //
371
+ // On Windows, shims are bash scripts and cannot be executed by spawn() directly.
372
+ // buildExecEnv() already sets the isolation env vars (CLAUDE_CONFIG_DIR, CODEX_HOME,
373
+ // etc.) that the bash shim would set, so we can skip the shim entirely and resolve
374
+ // straight to the real binary via getBinaryPath.
350
375
  if (options.version && cmd.length > 0) {
351
- const versionedName = `${cmd[0]}@${options.version}`;
352
- const absPath = path.join(getShimsDir(), versionedName);
353
- cmd[0] = fs.existsSync(absPath) ? absPath : versionedName;
376
+ if (process.platform === 'win32') {
377
+ const binaryPath = getBinaryPath(options.agent, options.version);
378
+ const binaryPathCmd = binaryPath + '.cmd';
379
+ if (fs.existsSync(binaryPathCmd)) {
380
+ cmd[0] = binaryPathCmd;
381
+ }
382
+ else if (fs.existsSync(binaryPath)) {
383
+ cmd[0] = binaryPath;
384
+ }
385
+ }
386
+ else {
387
+ const versionedName = `${cmd[0]}@${options.version}`;
388
+ const absPath = path.join(getShimsDir(), versionedName);
389
+ cmd[0] = fs.existsSync(absPath) ? absPath : versionedName;
390
+ }
354
391
  }
355
392
  // Add reasoning effort flags (before mode flags for codex -c positioning)
356
393
  // For codex, -c must come before 'exec' subcommand, so we insert at position 1
@@ -438,6 +475,42 @@ export async function execAgent(options) {
438
475
  const { exitCode } = await spawnAgent(options);
439
476
  return exitCode;
440
477
  }
478
+ /**
479
+ * Transparent passthrough exec for generated shims — the node-side delegate that
480
+ * Windows `.cmd` shims call. Resolves the active version (explicit pin, else
481
+ * project/default) and execs the real binary with the user's RAW args and the
482
+ * per-version env isolation, WITHOUT injecting mode/model/reasoning flags. This
483
+ * mirrors what the POSIX bash shim does inline (`exec $BINARY $launchArgs "$@"`),
484
+ * keeping version resolution in one place instead of reimplementing it in batch.
485
+ */
486
+ export async function execShimPassthrough(agent, rawArgs, cwd, pinnedVersion) {
487
+ const version = pinnedVersion ?? resolveVersion(agent, cwd) ?? undefined;
488
+ if (!version || !isVersionInstalled(agent, version)) {
489
+ process.stderr.write(`agents: no installed default for ${agent}. Set one with: agents use ${agent} <version>\n`);
490
+ return 127;
491
+ }
492
+ let binary = getBinaryPath(agent, version);
493
+ if (process.platform === 'win32') {
494
+ // npm ships <cmd>.cmd alongside the bare script on Windows; that's the runnable form.
495
+ const cmdPath = binary + '.cmd';
496
+ if (fs.existsSync(cmdPath))
497
+ binary = cmdPath;
498
+ }
499
+ // The only flag the bash shim injects (codex); everything else is transparent.
500
+ const launchArgs = agent === 'codex' ? ['-c', 'check_for_update_on_startup=false'] : [];
501
+ // mode/effort are required by ExecOptions but unused by buildExecEnv (which only
502
+ // derives the per-version config-dir env); pass the agent's default to satisfy the type.
503
+ const env = buildExecEnv({ agent, version, cwd, mode: defaultModeFor(agent), effort: 'auto' });
504
+ const useShell = process.platform === 'win32' && (!path.isAbsolute(binary) || binary.endsWith('.cmd'));
505
+ return new Promise((resolve) => {
506
+ const child = spawn(binary, [...launchArgs, ...rawArgs], { cwd, stdio: 'inherit', env, shell: useShell });
507
+ child.on('exit', (code, signal) => resolve(code ?? (signal ? 1 : 0)));
508
+ child.on('error', (err) => {
509
+ process.stderr.write(`agents: failed to launch ${agent}: ${err.message}\n`);
510
+ resolve(127);
511
+ });
512
+ });
513
+ }
441
514
  /**
442
515
  * Spawn an agent process and return its exit code plus a tee'd copy of stderr.
443
516
  *
@@ -475,11 +548,14 @@ async function spawnAgent(options) {
475
548
  const stdio = interactive
476
549
  ? ['inherit', 'inherit', 'inherit']
477
550
  : ['inherit', piped ? 'pipe' : 'inherit', 'pipe'];
551
+ // On Windows, .cmd batch wrappers (npm-installed CLIs) require shell:true
552
+ // whether addressed by name or absolute path.
553
+ const useShell = process.platform === 'win32' && (!path.isAbsolute(executable) || executable.endsWith('.cmd'));
478
554
  const child = spawn(executable, args, {
479
555
  cwd: options.cwd || process.cwd(),
480
556
  stdio,
481
557
  env: buildExecEnv(options),
482
- shell: false,
558
+ shell: useShell,
483
559
  });
484
560
  // Mark startup time (time from function call to process spawn)
485
561
  timer.mark('startup');
@@ -8,6 +8,7 @@ import * as fs from 'fs';
8
8
  import * as path from 'path';
9
9
  import * as os from 'os';
10
10
  import * as yaml from 'yaml';
11
+ import { AGENTS, agentConfigDirName } from './agents.js';
11
12
  const HOME = process.env.HOME ?? os.homedir();
12
13
  const USER_DIR = path.join(HOME, '.agents');
13
14
  /** Canonical system-repo location (post-fold). */
@@ -672,12 +673,13 @@ function repairAgentConfigSymlinks() {
672
673
  }
673
674
  let repaired = 0;
674
675
  for (const { agent, version } of defaults) {
675
- const userTarget = fs.existsSync(path.join(HISTORY_DIR, 'versions', agent, version, 'home', `.${agent}`))
676
- ? path.join(HISTORY_DIR, 'versions', agent, version, 'home', `.${agent}`)
677
- : path.join(USER_DIR, 'versions', agent, version, 'home', `.${agent}`);
676
+ const configDirName = agent in AGENTS ? agentConfigDirName(agent) : `.${agent}`;
677
+ const userTarget = fs.existsSync(path.join(HISTORY_DIR, 'versions', agent, version, 'home', configDirName))
678
+ ? path.join(HISTORY_DIR, 'versions', agent, version, 'home', configDirName)
679
+ : path.join(USER_DIR, 'versions', agent, version, 'home', configDirName);
678
680
  if (!fs.existsSync(userTarget))
679
681
  continue;
680
- const symlinkPath = path.join(HOME, `.${agent}`);
682
+ const symlinkPath = path.join(HOME, configDirName);
681
683
  let stat = null;
682
684
  try {
683
685
  stat = fs.lstatSync(symlinkPath);