@openagents-org/agent-launcher 0.2.143 → 0.2.145

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/src/paths.js CHANGED
@@ -50,6 +50,12 @@ function getExtraBinDirs() {
50
50
  // Common: ~/.local/bin (pipx, user installs)
51
51
  _push(dirs, path.join(HOME, '.local', 'bin'));
52
52
 
53
+ // Aider (uv tool install) — its executable honors XDG_BIN_HOME /
54
+ // XDG_DATA_HOME/../bin / ~/.local/bin and also always lands in the uv tools
55
+ // venv. A GUI/daemon process won't see the installer's PATH edit, so add the
56
+ // real install dirs explicitly (filtered to existing dirs by the caller).
57
+ for (const d of aiderBinDirs()) _push(dirs, d);
58
+
53
59
  // Also add the directory containing the current node binary
54
60
  try {
55
61
  const nodeDir = path.dirname(process.execPath);
@@ -142,6 +148,14 @@ function whichBinary(name) {
142
148
  return cached.value;
143
149
  }
144
150
 
151
+ // On a non-English Windows the console OUTPUT codepage is OEM (e.g. 936/GBK on
152
+ // zh-CN), so `where` prints a path whose non-ASCII bytes don't match the utf-8
153
+ // decoding execSync does — a Chinese username comes back mangled (e.g.
154
+ // `C:\Users\??.?[\…`) and yields ENOENT downstream. We can't reliably re-encode
155
+ // it (and forcing `chcp` is unsafe under windowsHide's console-less cmd), so we
156
+ // existence-check every hit and only return one that's real; a mangled path
157
+ // fails and the caller falls through to its Node-derived tiers (built from
158
+ // process.env / os.homedir, which the OS hands us as correct UTF-16).
145
159
  const cmd = IS_WINDOWS ? `where ${name}` : `which ${name}`;
146
160
  try {
147
161
  const result = execSync(cmd, {
@@ -150,8 +164,12 @@ function whichBinary(name) {
150
164
  env: { ...process.env, PATH: getEnhancedPATH() },
151
165
  timeout: 5000,
152
166
  windowsHide: true,
153
- }).trim();
154
- const value = result.split(/\r?\n/)[0] || null;
167
+ });
168
+ let value = null;
169
+ for (const line of result.split(/\r?\n/)) {
170
+ const hit = line.trim();
171
+ if (hit && fs.existsSync(hit)) { value = hit; break; }
172
+ }
155
173
  whichBinaryCache.set(cacheKey, { value, at: Date.now() });
156
174
  return value;
157
175
  } catch {
@@ -160,6 +178,64 @@ function whichBinary(name) {
160
178
  }
161
179
  }
162
180
 
181
+ /**
182
+ * Codepage-safe `where`/`which` lookup, shared by every CLI adapter.
183
+ *
184
+ * Hazard: on a non-English Windows the console output codepage is OEM (e.g.
185
+ * 936/GBK on zh-CN), so execSync decodes `where` stdout with the wrong codepage
186
+ * and mangles a non-ASCII (e.g. Chinese) username in the path — yielding ENOENT
187
+ * downstream (the `C:\Users\??.?[\…\claude.cmd` failure). Two safe defenses,
188
+ * no `chcp` (which is unreliable under windowsHide's console-less cmd):
189
+ * 1. (Windows) check the npm-global default `%APPDATA%\npm\<name>.cmd` FIRST.
190
+ * APPDATA comes from the OS as UTF-16 via Node, so this path is always
191
+ * correctly encoded — and it's where the npm-installed CLIs actually live.
192
+ * 2. Fall back to `where`/`which`, but existence-check every hit so a mangled
193
+ * path is skipped and the caller drops to its own Node-derived tiers.
194
+ *
195
+ * @param {string|string[]} names base name(s), e.g. 'claude' or ['cursor-agent','agent'].
196
+ * On Windows each is tried as <name>.cmd, <name>.exe, <name>.
197
+ * @param {object} [env] env for the lookup (defaults to getEnhancedEnv()).
198
+ * @returns {string|null} an existing absolute path, or null.
199
+ */
200
+ function whereBinary(names, env) {
201
+ const bases = Array.isArray(names) ? names : [names];
202
+
203
+ // 1. npm-global default, derived from Node's env (correct encoding even when
204
+ // the username is non-ASCII). This is where `npm i -g <cli>` drops the shim.
205
+ if (IS_WINDOWS && process.env.APPDATA) {
206
+ for (const b of bases) {
207
+ const c = path.join(process.env.APPDATA, 'npm', `${b}.cmd`);
208
+ try { if (fs.existsSync(c)) return c; } catch {}
209
+ }
210
+ }
211
+
212
+ // 2. PATH lookup, existence-guarded.
213
+ let cmd;
214
+ if (IS_WINDOWS) {
215
+ const parts = [];
216
+ for (const b of bases) {
217
+ parts.push(`where ${b}.cmd 2>nul`, `where ${b}.exe 2>nul`, `where ${b} 2>nul`);
218
+ }
219
+ cmd = parts.join(' || ');
220
+ } else {
221
+ cmd = bases.map((b) => `which ${b}`).join(' || ');
222
+ }
223
+ try {
224
+ const out = execSync(cmd, {
225
+ encoding: 'utf-8',
226
+ stdio: ['pipe', 'pipe', 'pipe'],
227
+ timeout: 5000,
228
+ windowsHide: true,
229
+ env: env || getEnhancedEnv(),
230
+ });
231
+ for (const line of out.split(/\r?\n/)) {
232
+ const hit = line.trim();
233
+ if (hit && fs.existsSync(hit)) return hit;
234
+ }
235
+ } catch {}
236
+ return null;
237
+ }
238
+
163
239
  // ---- Windows paths ----
164
240
 
165
241
  function _addWindowsPaths(dirs) {
@@ -197,6 +273,12 @@ function _addWindowsPaths(dirs) {
197
273
  if (localAppData) _push(dirs, path.join(localAppData, 'cursor-agent'));
198
274
  _push(dirs, path.join(HOME, '.local', 'bin'));
199
275
 
276
+ // Amp CLI (irm https://ampcode.com/install.ps1 | iex) — same registry-PATH
277
+ // staleness as Cursor; add the canonical install dir(s) so an already-running
278
+ // daemon resolves amp without a reboot.
279
+ _push(dirs, path.join(HOME, '.amp', 'bin'));
280
+ if (localAppData) _push(dirs, path.join(localAppData, 'amp'));
281
+
200
282
  // Hermes CLI native (no-WSL) installer drops hermes.exe in the portable
201
283
  // venv's Scripts dir and the uv shim in %LOCALAPPDATA%\hermes\bin. Same
202
284
  // registry-PATH staleness as Cursor — add explicitly so an already-running
@@ -291,6 +373,12 @@ function _addUnixPaths(dirs) {
291
373
 
292
374
  // Cursor CLI native installer (curl https://cursor.com/install | bash)
293
375
  _push(dirs, path.join(HOME, '.cursor', 'bin'));
376
+
377
+ // Amp CLI native installer (curl https://ampcode.com/install.sh | bash)
378
+ // drops the binary in ~/.amp/bin and only symlinks into ~/.local/bin when
379
+ // that dir is already on PATH — so a GUI- or daemon-spawned process never
380
+ // sees it unless the canonical dir is listed here explicitly.
381
+ _push(dirs, path.join(HOME, '.amp', 'bin'));
294
382
  }
295
383
 
296
384
  // ---- macOS-specific ----
@@ -396,15 +484,50 @@ function clearBinaryLookupCache() {
396
484
  whichBinaryCache.clear();
397
485
  }
398
486
 
487
+ /**
488
+ * Directories where the Aider CLI executable can land, in the SAME priority the
489
+ * official installer (aider.chat/install.{sh,ps1} → `uv tool install`) uses, so
490
+ * detection matches reality on every platform:
491
+ * 1. $XDG_BIN_HOME
492
+ * 2. $XDG_DATA_HOME/../bin
493
+ * 3. ~/.local/bin (the default uv-tool / pipx / pip --user bin)
494
+ * 4. the uv tools venv for aider-chat (Scripts on Windows, bin on Unix) — the
495
+ * executable always lands here on a successful `uv tool install`, even if
496
+ * the bin-dir copy/PATH edit didn't happen.
497
+ * Uses os.homedir()/live env so it reflects the current process (test-friendly).
498
+ * Returns directories only; callers join the platform binary names.
499
+ */
500
+ function aiderBinDirs() {
501
+ const home = os.homedir();
502
+ const dirs = [];
503
+ if (process.env.XDG_BIN_HOME) dirs.push(process.env.XDG_BIN_HOME);
504
+ if (process.env.XDG_DATA_HOME) dirs.push(path.join(process.env.XDG_DATA_HOME, '..', 'bin'));
505
+ dirs.push(path.join(home, '.local', 'bin'));
506
+ if (IS_WINDOWS) {
507
+ const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
508
+ const uvTools = process.env.UV_TOOL_DIR || path.join(appData, 'uv', 'tools');
509
+ dirs.push(path.join(uvTools, 'aider-chat', 'Scripts'));
510
+ dirs.push(path.join(home, 'bin'));
511
+ } else {
512
+ const uvTools = process.env.UV_TOOL_DIR
513
+ || path.join(home, '.local', 'share', 'uv', 'tools');
514
+ dirs.push(path.join(uvTools, 'aider-chat', 'bin'));
515
+ dirs.push(path.join(home, 'bin'), '/usr/local/bin', '/opt/homebrew/bin');
516
+ }
517
+ return dirs;
518
+ }
519
+
399
520
  module.exports = {
400
521
  getExtraBinDirs,
401
522
  getEnhancedPATH,
402
523
  getEnhancedEnv,
403
524
  whichBinary,
525
+ whereBinary,
404
526
  clearBinaryLookupCache,
405
527
  getRuntimePrefix,
406
528
  getCorePrefix,
407
529
  defaultAgentWorkdir,
530
+ aiderBinDirs,
408
531
  IS_WINDOWS,
409
532
  IS_MACOS,
410
533
  SEP,
package/src/tui.js CHANGED
@@ -13,6 +13,7 @@ const path = require('path');
13
13
  const fs = require('fs');
14
14
  const { spawn } = require('child_process');
15
15
  const { getExtraBinDirs } = require('./paths');
16
+ const { hasCredentialMetadata, formatAuthGuidance } = require('./auth-guidance');
16
17
 
17
18
  const IS_WINDOWS = process.platform === 'win32';
18
19
 
@@ -67,7 +68,14 @@ function getConnector() {
67
68
 
68
69
  function describeHealth(health) {
69
70
  if (!health) return '';
70
- if (!health.ready) return health.message || 'Not configured';
71
+ if (!health.ready) {
72
+ // "Not installed" is reserved for a genuinely missing executable; an
73
+ // installed-but-signed-out agent shows its login/config message instead.
74
+ if (health.reason === 'not_installed' || health.installed === false) {
75
+ return 'Not installed';
76
+ }
77
+ return health.message || 'Not configured';
78
+ }
71
79
  const parts = ['Ready'];
72
80
  if (health.auth_mode === 'api_key') parts.push('API key');
73
81
  else if (health.auth_mode === 'cli_login') parts.push('CLI login');
@@ -283,11 +291,14 @@ function createTUI() {
283
291
  // Local-only agents (no workspace) show a dimmed "(local)" marker.
284
292
  const ws = r.workspace || `{gray-fg}${r.workspaceLabel}{/gray-fg}`;
285
293
  items.push(` ${r.name.padEnd(22)} ${r.type.padEnd(14)} ${state.padEnd(30)} ${ws}`);
286
- // Detail row: working dir + config warning
294
+ // Detail row: working dir + config warning + the REAL runtime error
295
+ // (spawn/join/heartbeat) when the daemon reported one, so a failure shows
296
+ // its actual cause instead of a swallowed/"Not installed" guess.
287
297
  const details = [];
288
298
  details.push(r.path || process.env.HOME || '~');
289
299
  if (r.health) details.push(`{cyan-fg}${describeHealth(r.health)}{/cyan-fg}`);
290
300
  if (r.notReadyMsg) details.push(`{yellow-fg}⚠ ${r.notReadyMsg}{/yellow-fg}`);
301
+ if (r.lastError) details.push(`{red-fg}✗ ${r.lastError}{/red-fg}`);
291
302
  items.push(` {gray-fg} ${details.join(' | ')}{/gray-fg}`);
292
303
  }
293
304
  agentList.setItems(items);
@@ -754,6 +765,22 @@ function createTUI() {
754
765
  currentView = 'configure';
755
766
  const envFields = connector.registry.getEnvFields(agent.type);
756
767
  if (!envFields || envFields.length === 0) {
768
+ // Some agents declare no env_config yet still REQUIRE a sign-in/credential
769
+ // (e.g. Gemini's OAuth login). Detect that via the registry's check_ready
770
+ // and show readiness + login guidance instead of a misleading "no
771
+ // configuration required". Agents without any credential metadata keep the
772
+ // original message — this is gated, so other agents are untouched.
773
+ const entry = connector.registry.getEntry(agent.type);
774
+ const checkReady = entry && entry.check_ready;
775
+ if (hasCredentialMetadata(checkReady)) {
776
+ let health = null;
777
+ try { health = connector.healthCheck(agent.type); } catch {}
778
+ const g = formatAuthGuidance(entry, health);
779
+ const tag = g.ready ? 'green-fg' : 'yellow-fg';
780
+ log(`{bold}{${tag}}${entry.label || agent.type} authentication: ${g.ready ? 'Ready' : 'Not ready'}{/}{/bold}`);
781
+ for (const line of g.lines) log(`{gray-fg}${line}{/gray-fg}`);
782
+ return;
783
+ }
757
784
  log('{gray-fg}No configuration required for this agent type.{/gray-fg}');
758
785
  return;
759
786
  }