@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 +1 -1
- package/registry.json +1 -1
- package/src/adapters/claude.js +33 -2
- package/src/adapters/hermes.js +21 -9
- package/src/adapters/opencode.js +58 -4
- package/src/paths.js +9 -0
package/package.json
CHANGED
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": "
|
|
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": []
|
package/src/adapters/claude.js
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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);
|
package/src/adapters/hermes.js
CHANGED
|
@@ -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(
|
|
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
|
|
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
|
|
92
|
-
//
|
|
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):
|
|
115
|
-
//
|
|
116
|
-
//
|
|
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(
|
|
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') {
|
package/src/adapters/opencode.js
CHANGED
|
@@ -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} ${
|
|
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:
|
|
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
|
-
|
|
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
|
|