@openagents-org/agent-launcher 0.2.178 → 0.2.179

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/paths.js +113 -14
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.178",
3
+ "version": "0.2.179",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/paths.js CHANGED
@@ -19,21 +19,27 @@ const SEP = IS_WINDOWS ? ';' : ':';
19
19
  const HOME = process.env.HOME || process.env.USERPROFILE || '';
20
20
  const PATH_LOOKUP_CACHE_TTL_MS = 30 * 1000;
21
21
 
22
+ let knownBinDirsCache = { value: null, at: 0 };
22
23
  let extraBinDirsCache = { value: null, at: 0, path: '' };
23
24
  const whichBinaryCache = new Map();
24
25
 
25
26
  /**
26
- * Get all extra binary directories that should be checked beyond process.env.PATH.
27
- * Returns deduplicated list of existing directories.
27
+ * Every directory an agent CLI is known to land in, filtered to the ones that
28
+ * exist on this machine. Ranked: a copy the launcher installed outranks a copy
29
+ * the user installed, which outranks whatever the login shell happens to add.
30
+ *
31
+ * This is the whole curated set, INCLUDING dirs that are already on PATH —
32
+ * which is what separates it from getExtraBinDirs(). Anything resolving a
33
+ * binary off the filesystem wants this one: that lookup exists precisely for
34
+ * the cases where asking the shell failed, and a dir being on PATH is no
35
+ * evidence at all that `where`/`which` succeeded (see resolveBinaryInKnownDirs).
28
36
  */
29
- function getExtraBinDirs() {
30
- const currentPATH = process.env.PATH || '';
37
+ function getKnownBinDirs() {
31
38
  if (
32
- extraBinDirsCache.value &&
33
- extraBinDirsCache.path === currentPATH &&
34
- Date.now() - extraBinDirsCache.at < PATH_LOOKUP_CACHE_TTL_MS
39
+ knownBinDirsCache.value &&
40
+ Date.now() - knownBinDirsCache.at < PATH_LOOKUP_CACHE_TTL_MS
35
41
  ) {
36
- return [...extraBinDirsCache.value];
42
+ return [...knownBinDirsCache.value];
37
43
  }
38
44
 
39
45
  const dirs = [];
@@ -99,8 +105,6 @@ function getExtraBinDirs() {
99
105
  const seen = new Set();
100
106
  const value = dirs.filter(d => {
101
107
  if (!d || seen.has(d)) return false;
102
- // Skip if already in PATH (case-insensitive on Windows)
103
- if (IS_WINDOWS ? currentPATH.toLowerCase().includes(d.toLowerCase()) : currentPATH.includes(d)) return false;
104
108
  seen.add(d);
105
109
  try {
106
110
  return fs.statSync(d).isDirectory();
@@ -108,6 +112,32 @@ function getExtraBinDirs() {
108
112
  return false;
109
113
  }
110
114
  });
115
+ knownBinDirsCache = { value, at: Date.now() };
116
+ return [...value];
117
+ }
118
+
119
+ /**
120
+ * The known bin dirs that are NOT already on PATH — i.e. what has to be
121
+ * PREPENDED to build an environment in which every agent CLI resolves.
122
+ *
123
+ * Only ever use this to build a PATH. It is a strict subset of
124
+ * getKnownBinDirs(), and the dirs it drops (%APPDATA%\npm, /usr/local/bin, …)
125
+ * are the most common install locations there are, so using it as "the dirs to
126
+ * search" hides exactly the agents a user installed themselves.
127
+ */
128
+ function getExtraBinDirs() {
129
+ const currentPATH = process.env.PATH || '';
130
+ if (
131
+ extraBinDirsCache.value &&
132
+ extraBinDirsCache.path === currentPATH &&
133
+ Date.now() - extraBinDirsCache.at < PATH_LOOKUP_CACHE_TTL_MS
134
+ ) {
135
+ return [...extraBinDirsCache.value];
136
+ }
137
+ const value = getKnownBinDirs().filter((d) =>
138
+ // Case-insensitive on Windows.
139
+ IS_WINDOWS ? !currentPATH.toLowerCase().includes(d.toLowerCase()) : !currentPATH.includes(d),
140
+ );
111
141
  extraBinDirsCache = { value, at: Date.now(), path: currentPATH };
112
142
  return [...value];
113
143
  }
@@ -176,7 +206,13 @@ function whichBinary(name) {
176
206
  const result = execSync(cmd, {
177
207
  encoding: 'utf-8',
178
208
  stdio: ['pipe', 'pipe', 'pipe'],
179
- env: { ...process.env, PATH: getEnhancedPATH() },
209
+ // getEnhancedEnv(), never `{...process.env, PATH}`: spreading process.env
210
+ // on Windows yields a "Path" key, so assigning PATH creates a SECOND path
211
+ // variable and which of the two the child reads is undefined. It also
212
+ // supplies ComSpec, without which execSync's shell cannot even start in an
213
+ // Electron process that has no cmd.exe on PATH — and a `where` that never
214
+ // ran looks exactly like a CLI that isn't installed.
215
+ env: getEnhancedEnv(),
180
216
  timeout: 5000,
181
217
  windowsHide: true,
182
218
  });
@@ -354,6 +390,35 @@ function _addAgentInstallerPaths(dirs) {
354
390
  _push(dirs, path.join(HOME, '.npm-global', 'bin'));
355
391
  _push(dirs, path.join(HOME, '.openagents', 'npm-global', 'bin'));
356
392
 
393
+ // codebuddy — the npm package drops the usual global shim, but CodeBuddy also
394
+ // ships a native build (the engine behind the WorkBuddy desktop app) that
395
+ // installs to its own directory. The codebuddy adapter has always searched
396
+ // these; the installer that decides whether it EXISTS had not.
397
+ _push(dirs, path.join(HOME, '.codebuddy', 'bin'));
398
+
399
+ // claude — `claude install` (the native, non-npm build) relocates the CLI to
400
+ // ~/.claude/local and reaches it through a shell alias, which a GUI process
401
+ // inherits no more than it inherits an rc-file PATH edit.
402
+ _push(dirs, path.join(HOME, '.claude', 'local'));
403
+
404
+ // hermes — the Unix installer's own bin dir. Its Windows counterparts are in
405
+ // _addWindowsPaths; this is the half nothing covered.
406
+ _push(dirs, path.join(HOME, '.hermes', 'bin'));
407
+
408
+ // pipx — `pipx install X` puts the executable in <PIPX_HOME>/venvs/X/bin and
409
+ // only SYMLINKS it into PIPX_BIN_DIR. When that link step is skipped, or its
410
+ // dir is one a GUI launch can't see, the venv copy is the only one there is.
411
+ // Enumerated rather than listed by name: aider, mini-swe-agent and openworker
412
+ // are all installable this way, and so is the next Python agent.
413
+ for (const root of _pipxHomes()) {
414
+ try {
415
+ const venvs = path.join(root, 'venvs');
416
+ for (const d of fs.readdirSync(venvs, { withFileTypes: true })) {
417
+ if (d.isDirectory()) _push(dirs, path.join(venvs, d.name, IS_WINDOWS ? 'Scripts' : 'bin'));
418
+ }
419
+ } catch {}
420
+ }
421
+
357
422
  // The plain ~/bin some installers fall back to when ~/.local/bin is absent.
358
423
  _push(dirs, path.join(HOME, 'bin'));
359
424
 
@@ -365,9 +430,32 @@ function _addAgentInstallerPaths(dirs) {
365
430
  // cursor-agent also ships under Programs\ on some Windows installs — the
366
431
  // cursor adapter checks both, the installer only knew one.
367
432
  _push(dirs, path.join(lad, 'Programs', 'cursor-agent'));
433
+ // codebuddy's native Windows install.
434
+ _push(dirs, path.join(lad, 'CodeBuddy', 'bin'));
435
+ // winget's shim dir — how GitHub Copilot CLI arrives on Windows for anyone
436
+ // who didn't take the npm route. The copilot adapter knew it; nothing else did.
437
+ _push(dirs, path.join(lad, 'Microsoft', 'WinGet', 'Links'));
368
438
  }
369
439
  }
370
440
 
441
+ /**
442
+ * Roots pipx may keep its venvs under. PIPX_HOME wins when exported; otherwise
443
+ * pipx has moved its default once (~/.local/pipx → the platform user-data dir),
444
+ * so both layouts are still in the field.
445
+ */
446
+ function _pipxHomes() {
447
+ if (process.env.PIPX_HOME) return [process.env.PIPX_HOME];
448
+ const roots = [path.join(HOME, '.local', 'pipx')];
449
+ if (IS_WINDOWS) {
450
+ if (process.env.LOCALAPPDATA) roots.push(path.join(process.env.LOCALAPPDATA, 'pipx', 'pipx'));
451
+ } else if (IS_MACOS) {
452
+ roots.push(path.join(HOME, 'Library', 'Application Support', 'pipx'));
453
+ } else {
454
+ roots.push(path.join(process.env.XDG_DATA_HOME || path.join(HOME, '.local', 'share'), 'pipx'));
455
+ }
456
+ return roots;
457
+ }
458
+
371
459
  let npmPrefixCache;
372
460
 
373
461
  /**
@@ -767,6 +855,7 @@ function defaultAgentWorkdir(agentName) {
767
855
  * `whichBinary(name) === null` cached before install doesn't survive.
768
856
  */
769
857
  function clearBinaryLookupCache() {
858
+ knownBinDirsCache = { value: null, at: 0 };
770
859
  extraBinDirsCache = { value: null, at: 0, path: '' };
771
860
  whichBinaryCache.clear();
772
861
  // The login-shell / npm-prefix probes are deliberately NOT cleared: they each
@@ -785,7 +874,7 @@ function clearBinaryLookupCache() {
785
874
  function primeBinaryLookup() {
786
875
  try { loginShellDirs(); } catch {}
787
876
  try { _npmPrefix(); } catch {}
788
- try { getExtraBinDirs(); } catch {}
877
+ try { getKnownBinDirs(); } catch {}
789
878
  }
790
879
 
791
880
  /**
@@ -845,10 +934,19 @@ const BIN_EXTS = IS_WINDOWS ? ['.cmd', '.exe', '.bat', ''] : [''];
845
934
  * find it" and then opened a terminal running a bare command that could only
846
935
  * fail with "'cursor-agent' is not recognized".
847
936
  *
848
- * Deliberately reuses getExtraBinDirs() rather than a second hardcoded list:
937
+ * Deliberately reuses getKnownBinDirs() rather than a second hardcoded list:
849
938
  * that list already IS the curated set, and a copy of it is exactly how the
850
939
  * launcher and the adapters drifted apart in the first place.
851
940
  *
941
+ * It has to be the KNOWN dirs, not the extra ones. getExtraBinDirs() drops
942
+ * everything already on PATH, on the reasoning that PATH covers it — but the
943
+ * only way execution gets here is a `where`/`which` that ALREADY failed, and it
944
+ * fails wholesale on Windows (mangled OEM-codepage output on a non-English
945
+ * install, a 5s timeout, a cmd.exe that isn't resolvable from an Electron
946
+ * process). With the filter in place that failure erased every agent installed
947
+ * to a normal location — %APPDATA%\npm and friends — and the marketplace
948
+ * offered to install CodeBuddy/opencode/dsh over the copies already there.
949
+ *
852
950
  * Runs only after a PATH lookup has already missed, so it can add resolutions
853
951
  * but never change one that already works.
854
952
  *
@@ -868,7 +966,7 @@ function resolveBinaryInKnownDirs(names, agentType) {
868
966
  if (agentType) push(path.join(getRuntimePrefix(agentType), 'node_modules', '.bin'));
869
967
  push(path.join(HOME, '.openagents', 'nodejs', 'node_modules', '.bin'));
870
968
  try { push(path.dirname(process.execPath)); } catch {}
871
- for (const d of getExtraBinDirs()) push(d);
969
+ for (const d of getKnownBinDirs()) push(d);
872
970
 
873
971
  for (const dir of dirs) {
874
972
  for (const name of list) {
@@ -887,6 +985,7 @@ function resolveBinaryInKnownDirs(names, agentType) {
887
985
 
888
986
 
889
987
  module.exports = {
988
+ getKnownBinDirs,
890
989
  getExtraBinDirs,
891
990
  getEnhancedPATH,
892
991
  getEnhancedEnv,