@openagents-org/agent-launcher 0.2.138 → 0.2.140

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.138",
3
+ "version": "0.2.140",
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/registry.json CHANGED
@@ -616,7 +616,7 @@
616
616
  "requires": ["python3"],
617
617
  "macos": "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash -s -- --skip-setup",
618
618
  "linux": "curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash -s -- --skip-setup",
619
- "windows": "wsl.exe -e bash -lc \"curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash -s -- --skip-setup\""
619
+ "windows": "\"%SystemRoot%\\System32\\WindowsPowerShell\\v1.0\\powershell.exe\" -NoProfile -ExecutionPolicy Bypass -Command \"Invoke-RestMethod -UseBasicParsing 'https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1' | Invoke-Expression\""
620
620
  },
621
621
  "launch": {
622
622
  "args": []
@@ -710,6 +710,7 @@ class ClaudeAdapter extends BaseAdapter {
710
710
  messageResolve: null,
711
711
  msgChannel: channel,
712
712
  lastResponseText: [],
713
+ lastErrorText: '',
713
714
  hasToolUseSinceLastText: false,
714
715
  postedThinking: false,
715
716
  everPostedAnything: false,
@@ -783,7 +784,12 @@ class ClaudeAdapter extends BaseAdapter {
783
784
  this._saveSessions();
784
785
  }
785
786
  if (event.is_error) {
786
- this._log(`Claude error: ${String(event.result || '').slice(0, 200)}`);
787
+ // Capture the error so _handleMessage can surface it to the user.
788
+ // Without this, an auth/API failure (e.g. 401 invalid token) is only
789
+ // logged and the user just sees "No response generated", hiding the
790
+ // real cause and making it nearly impossible to diagnose.
791
+ pp.lastErrorText = String(event.result || '').trim();
792
+ this._log(`Claude error: ${pp.lastErrorText.slice(0, 200)}`);
787
793
  }
788
794
  if (pp.messageResolve) {
789
795
  pp.messageResolve({ resultEvent: event });
@@ -847,6 +853,7 @@ class ClaudeAdapter extends BaseAdapter {
847
853
  */
848
854
  _sendToPersistentProc(pp, content) {
849
855
  pp.lastResponseText = [];
856
+ pp.lastErrorText = '';
850
857
  pp.hasToolUseSinceLastText = false;
851
858
  pp.postedThinking = false;
852
859
  pp.everPostedAnything = false;
@@ -878,6 +885,20 @@ class ClaudeAdapter extends BaseAdapter {
878
885
  });
879
886
  }
880
887
 
888
+ /**
889
+ * Turn a raw Claude `result` error into a user-facing message. Auth failures
890
+ * (401 / invalid token) are the most common real-world cause and are otherwise
891
+ * invisible — add a concrete hint about which env vars to check.
892
+ */
893
+ _formatClaudeError(text) {
894
+ const msg = String(text || '').trim() || 'Claude returned an error.';
895
+ if (/401|403|authenticate|invalid.*(token|key|api)|无效的?\s*(令牌|密钥|key)/i.test(msg)) {
896
+ return `Claude authentication failed: ${msg}\n\n` +
897
+ 'Check this agent\'s API key and Base URL in the launcher — the key may be invalid or expired.';
898
+ }
899
+ return `Claude error: ${msg}`;
900
+ }
901
+
881
902
  async _handleMessage(msg) {
882
903
  let content = (msg.content || '').trim();
883
904
  const attachments = msg.attachments || [];
@@ -933,6 +954,8 @@ class ClaudeAdapter extends BaseAdapter {
933
954
  const fullResponse = existingPP.lastResponseText.join('\n').trim();
934
955
  if (fullResponse) {
935
956
  try { await this.sendResponse(msgChannel, fullResponse); } catch {}
957
+ } else if (existingPP.lastErrorText) {
958
+ try { await this.sendError(msgChannel, this._formatClaudeError(existingPP.lastErrorText)); } catch {}
936
959
  }
937
960
  this._resetIdleTimer(msgChannel);
938
961
  if (!msg._todoNudge) {
@@ -1017,7 +1040,11 @@ class ClaudeAdapter extends BaseAdapter {
1017
1040
  continue;
1018
1041
  }
1019
1042
  if (!pp.everPostedAnything) {
1020
- try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
1043
+ if (pp.lastErrorText) {
1044
+ try { await this.sendError(msgChannel, this._formatClaudeError(pp.lastErrorText)); } catch {}
1045
+ } else {
1046
+ try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
1047
+ }
1021
1048
  }
1022
1049
  break;
1023
1050
  }
@@ -1052,6 +1079,10 @@ class ClaudeAdapter extends BaseAdapter {
1052
1079
 
1053
1080
  if (finalResponse) {
1054
1081
  try { await this.sendResponse(msgChannel, finalResponse); } catch {}
1082
+ } else if (pp.lastErrorText) {
1083
+ // Claude finished with an error and no assistant text (e.g. 401 invalid
1084
+ // token). Surface it instead of silently dropping the turn.
1085
+ try { await this.sendError(msgChannel, this._formatClaudeError(pp.lastErrorText)); } catch {}
1055
1086
  }
1056
1087
 
1057
1088
  this._resetIdleTimer(msgChannel);
@@ -24,6 +24,9 @@ const { buildOpenclawSystemPrompt } = require('./workspace-prompt');
24
24
  const { whichBinary, getEnhancedEnv } = require('../paths');
25
25
 
26
26
  const IS_WINDOWS = process.platform === 'win32';
27
+ const HERMES_INSTALL_HINT = IS_WINDOWS
28
+ ? 'powershell -NoProfile -ExecutionPolicy Bypass -Command "irm https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.ps1 | iex"'
29
+ : 'curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash';
27
30
  const SESSION_ID_RE = /session_id:\s*(\S+)/;
28
31
  const MAX_HISTORY_ENTRIES = 12;
29
32
 
@@ -56,7 +59,7 @@ class HermesAdapter extends BaseAdapter {
56
59
  if (this._hermesBin) {
57
60
  this._log(`Using Hermes binary: ${this._hermesBin} (profile=${this.hermesProfile})`);
58
61
  } else {
59
- this._log('Warning: hermes CLI not found. Install: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash');
62
+ this._log(`Warning: hermes CLI not found. Install: ${HERMES_INSTALL_HINT}`);
60
63
  }
61
64
  }
62
65
 
@@ -76,7 +79,9 @@ class HermesAdapter extends BaseAdapter {
76
79
  try {
77
80
  const env = getEnhancedEnv();
78
81
  if (IS_WINDOWS) {
79
- // Native Windows unsupported upstream — we try anyway for WSL cases
82
+ // Native Windows is supported (install.ps1). The installer adds
83
+ // venv\Scripts to the user PATH; a running daemon won't see that update,
84
+ // hence the enriched env + the explicit candidates in Tier 2.
80
85
  const r = execSync('where hermes.exe 2>nul || where hermes.cmd 2>nul || where hermes 2>nul', {
81
86
  encoding: 'utf-8', timeout: 5000, windowsHide: true, env,
82
87
  });
@@ -88,9 +93,16 @@ class HermesAdapter extends BaseAdapter {
88
93
  }
89
94
  } catch {}
90
95
 
91
- // Tier 2: Common install locations. The installer drops `hermes` in
92
- // ~/.local/bin and (on Windows) ~/.hermes/bin.
96
+ // Tier 2: Common install locations. The native Windows installer
97
+ // (install.ps1) provisions a portable venv and drops hermes.exe under
98
+ // %LOCALAPPDATA%\hermes\hermes-agent\venv\Scripts (with the uv shim in
99
+ // %LOCALAPPDATA%\hermes\bin). The Unix installer uses ~/.local/bin.
100
+ const localAppData = process.env.LOCALAPPDATA || path.join(home, 'AppData', 'Local');
93
101
  const candidates = IS_WINDOWS ? [
102
+ path.join(localAppData, 'hermes', 'hermes-agent', 'venv', 'Scripts', 'hermes.exe'),
103
+ path.join(localAppData, 'hermes', 'hermes-agent', 'venv', 'Scripts', 'hermes.cmd'),
104
+ path.join(localAppData, 'hermes', 'bin', 'hermes.exe'),
105
+ path.join(localAppData, 'hermes', 'bin', 'hermes.cmd'),
94
106
  path.join(home, '.hermes', 'bin', 'hermes.exe'),
95
107
  path.join(home, '.hermes', 'bin', 'hermes.cmd'),
96
108
  path.join(home, '.hermes', 'bin', 'hermes'),
@@ -111,10 +123,10 @@ class HermesAdapter extends BaseAdapter {
111
123
  const viaWhich = whichBinary('hermes');
112
124
  if (viaWhich) return viaWhich;
113
125
 
114
- // Tier 4 (Windows only): Hermes is not supported natively on Windows — it
115
- // installs and runs inside WSL2. If a WSL distro has hermes on its login
116
- // PATH, resolve its absolute in-WSL path; _runHermes then invokes it as
117
- // `wsl -e <path> …`.
126
+ // Tier 4 (Windows only): fall back to a WSL install if one exists. Native
127
+ // Windows (Tiers 1-3) is preferred, but a user who set hermes up inside
128
+ // WSL2 still works — resolve its absolute in-WSL path; _runHermes then
129
+ // invokes it as `wsl -e <path> …`.
118
130
  if (IS_WINDOWS) {
119
131
  const wslPath = this._resolveWslHermes();
120
132
  if (wslPath) {
@@ -283,7 +295,7 @@ class HermesAdapter extends BaseAdapter {
283
295
 
284
296
  _buildHermesCmd(prompt, resumeSessionId) {
285
297
  if (!this._hermesBin) {
286
- throw new Error('hermes CLI not found. Install with: curl -fsSL https://raw.githubusercontent.com/NousResearch/hermes-agent/main/scripts/install.sh | bash');
298
+ throw new Error(`hermes CLI not found. Install with: ${HERMES_INSTALL_HINT}`);
287
299
  }
288
300
  const args = [];
289
301
  if (this.hermesProfile && this.hermesProfile !== 'default') {
@@ -22,6 +22,10 @@ const { whichBinary, getEnhancedEnv } = require('../paths');
22
22
 
23
23
  const IS_WINDOWS = process.platform === 'win32';
24
24
 
25
+ // Max wall-clock for a single `opencode run`. When it fires, Node kills the
26
+ // child with SIGTERM, the 'close' handler sees signal='SIGTERM' / code=null.
27
+ const TIMEOUT_MS = 300000; // 5 minutes
28
+
25
29
  class OpenCodeAdapter extends BaseAdapter {
26
30
  /**
27
31
  * @param {object} opts - BaseAdapter opts plus:
@@ -360,6 +364,32 @@ class OpenCodeAdapter extends BaseAdapter {
360
364
  // Subprocess execution
361
365
  // ------------------------------------------------------------------
362
366
 
367
+ /**
368
+ * Build the `--model` value (provider/model) for `opencode run`.
369
+ *
370
+ * CRITICAL: opencode-ai does NOT read the OPENCODE_MODEL env var. When no
371
+ * model is given on the command line AND none is set in opencode.json, the
372
+ * non-interactive `run` command hangs forever waiting for interactive
373
+ * provider/model selection — it emits zero output until the spawn timeout
374
+ * kills it with SIGTERM (surfaced as the misleading "exited with code null").
375
+ * Passing `--model` explicitly is what makes headless runs actually work.
376
+ */
377
+ _resolveModel() {
378
+ const env = this.agentEnv || process.env;
379
+ const model = (env.OPENCODE_MODEL || env.LLM_MODEL || '').trim();
380
+ if (!model) return '';
381
+ // Already provider-qualified (e.g. "openai/gpt-4o", "anthropic/claude-…").
382
+ if (model.includes('/')) return model;
383
+ // Infer the provider from which key/base URL the env resolved to. The
384
+ // registry maps LLM_* → OPENAI_* for OpenAI-compatible endpoints, so
385
+ // "openai" is the right default; only switch for an Anthropic endpoint.
386
+ const baseUrl = (env.OPENAI_BASE_URL || env.LLM_BASE_URL || '').toLowerCase();
387
+ const provider = (env.ANTHROPIC_API_KEY || baseUrl.includes('anthropic'))
388
+ ? 'anthropic'
389
+ : 'openai';
390
+ return `${provider}/${model}`;
391
+ }
392
+
363
393
  _runOpencode(content, msgChannel) {
364
394
  const binary = this._opencodeBinary || this._findOpencodeBinary();
365
395
  if (binary) this._opencodeBinary = binary;
@@ -371,6 +401,19 @@ class OpenCodeAdapter extends BaseAdapter {
371
401
 
372
402
  const cmd = [binary, 'run', '--format', 'json', '--dir', this.agentHome];
373
403
 
404
+ // Pin the model explicitly — without it opencode hangs waiting for
405
+ // interactive selection (see _resolveModel). Harmless when resuming a
406
+ // session (opencode keeps the session's model); essential for new ones.
407
+ const model = this._resolveModel();
408
+ if (model) {
409
+ cmd.push('--model', model);
410
+ } else {
411
+ this._log(
412
+ 'WARNING: no model configured (set LLM_MODEL / OPENCODE_MODEL). ' +
413
+ 'opencode may hang or produce no response.'
414
+ );
415
+ }
416
+
374
417
  const sessionId = this._channelSessions[msgChannel];
375
418
  let fullPrompt;
376
419
  if (sessionId) {
@@ -382,7 +425,7 @@ class OpenCodeAdapter extends BaseAdapter {
382
425
  fullPrompt = `${context}\n\n---\n\n${content}`;
383
426
  }
384
427
 
385
- this._log(`CLI: ${binary} ${cmd.slice(1, 5).join(' ')} ...`);
428
+ this._log(`CLI: ${binary} run --format json --dir … --model ${model || '(none)'}`);
386
429
 
387
430
  const spawnEnv = { ...(this.agentEnv || process.env) };
388
431
 
@@ -400,7 +443,7 @@ class OpenCodeAdapter extends BaseAdapter {
400
443
  stdio: ['pipe', 'pipe', 'pipe'],
401
444
  env: spawnEnv,
402
445
  cwd: this.agentHome,
403
- timeout: 300000, // 5 minutes
446
+ timeout: TIMEOUT_MS,
404
447
  // Never let a console window appear. Besides being ugly, an attached
405
448
  // console makes opencode start its interactive TUI and hang instead of
406
449
  // running headless — the root cause of "stuck on thinking…" on Windows.
@@ -428,11 +471,22 @@ class OpenCodeAdapter extends BaseAdapter {
428
471
 
429
472
  proc.on('error', (err) => finish(reject, err));
430
473
  // 'close' (not 'exit') fires after stdout/stderr have fully drained, so we
431
- // never parse a truncated response.
432
- proc.on('close', (code) => {
474
+ // never parse a truncated response. The handler receives (code, signal):
475
+ // when the spawn timeout kills the child, code is null and signal is the
476
+ // signal name — report that as a timeout rather than "exited with code null".
477
+ proc.on('close', (code, signal) => {
433
478
  stdout = stdout.trim();
434
479
  stderr = stderr.trim();
435
480
 
481
+ if (signal) {
482
+ this._log(`opencode killed by signal ${signal} after ${TIMEOUT_MS / 1000}s (no output: ${!stdout})`);
483
+ return finish(reject, new Error(
484
+ stderr ? stderr.slice(0, 500)
485
+ : `opencode timed out after ${TIMEOUT_MS / 1000}s with no response. ` +
486
+ 'This usually means no model/provider is configured — check LLM_MODEL / LLM_API_KEY.'
487
+ ));
488
+ }
489
+
436
490
  if (code !== 0) {
437
491
  this._log(`opencode exited with code ${code}: ${stderr.slice(0, 300)}`);
438
492
  // Surface the real failure instead of a generic "No response" so the
package/src/paths.js CHANGED
@@ -197,6 +197,15 @@ function _addWindowsPaths(dirs) {
197
197
  if (localAppData) _push(dirs, path.join(localAppData, 'cursor-agent'));
198
198
  _push(dirs, path.join(HOME, '.local', 'bin'));
199
199
 
200
+ // Hermes CLI native (no-WSL) installer drops hermes.exe in the portable
201
+ // venv's Scripts dir and the uv shim in %LOCALAPPDATA%\hermes\bin. Same
202
+ // registry-PATH staleness as Cursor — add explicitly so an already-running
203
+ // daemon resolves hermes via `where` without a reboot.
204
+ if (localAppData) {
205
+ _push(dirs, path.join(localAppData, 'hermes', 'hermes-agent', 'venv', 'Scripts'));
206
+ _push(dirs, path.join(localAppData, 'hermes', 'bin'));
207
+ }
208
+
200
209
  // Node.js install
201
210
  _push(dirs, path.join(programFiles, 'nodejs'));
202
211