@openagents-org/agent-launcher 0.2.177 → 0.2.178

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.
@@ -1,3 +1,38 @@
1
- <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 1024 1024" role="img" style="flex:none;line-height:1">
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 40 40" fill="none" role="img" style="flex:none;line-height:1">
2
2
  <title>CodeBuddy Code</title>
3
- <rect x="88" y="88" width="848" height="848" rx="182" fill="#0052d9"/><path d="M400 372 262 512l138 140m224-280 138 140-138 140M566 316 458 708" stroke="#fff" stroke-width="62" stroke-linecap="round" stroke-linejoin="round" fill="none"/></svg>
3
+ <g clip-path="url(#cb-clip)">
4
+ <rect width="40" height="40" rx="20" fill="url(#cb-bg)"/>
5
+ <g filter="url(#cb-glow-a)">
6
+ <circle cx="12.6071" cy="34.9794" r="14.081" fill="#32E6B9" fill-opacity="0.4"/>
7
+ </g>
8
+ <g filter="url(#cb-glow-b)">
9
+ <circle cx="33.3404" cy="43.6185" r="12.8704" fill="#32E6B9"/>
10
+ </g>
11
+ <path d="M27.6897 3.3426C28.057 3.01322 28.0795 2.99983 28.3485 2.98366C28.7851 2.95177 29.1858 3.16107 29.8664 3.78072C31.4566 5.22574 33.6703 8.19724 35.047 10.7343L35.5796 11.7191L36.3309 12.0927C37.0562 12.4593 38.2459 13.2111 38.7428 13.614C38.9673 13.7997 38.9992 13.8032 39.2326 13.7124C40.2872 13.3017 41.7982 13.846 43.1307 15.1244C44.3302 16.274 45.479 18.2384 45.9191 19.8778C45.9833 20.1417 46.0683 20.709 46.0994 21.1314C46.1997 22.6144 45.7243 23.7986 44.8085 24.3349C44.6213 24.443 44.6085 24.4725 44.6137 24.939C44.6559 27.1604 44.0571 29.3777 42.8543 31.54C41.4965 33.9678 39.0794 36.4798 35.8073 38.846C34.0502 40.1247 29.8918 42.5469 28.0122 43.3973C23.5096 45.4244 19.8999 46.2022 16.7645 45.8181C14.8943 45.5915 12.7771 44.8611 11.5246 44.0137C11.1951 43.786 11.1428 43.7719 10.8909 43.8439C9.55017 44.2289 7.79442 43.4374 6.30272 41.7817C5.70771 41.1197 4.74709 39.4939 4.43575 38.6241C3.71569 36.5887 3.85859 34.752 4.81783 33.6551C5.0657 33.3725 5.07387 33.3607 5.01972 32.8856C4.93033 32.1079 4.88982 30.9566 4.93066 30.2138L4.96343 29.5205L3.92091 27.6777C2.3078 24.8074 1.28326 22.3968 0.887982 20.5554C0.679402 19.546 0.692781 19.0981 0.949061 18.7667C1.10529 18.5666 1.61622 18.3594 2.23252 18.2455C3.78518 17.9729 7.17203 18.2198 10.9394 18.885L11.3303 18.9525L12.1903 18.1921C13.6179 16.9275 14.5665 16.2176 16.315 15.1274C18.1373 13.9872 20.1943 13.0498 22.5103 12.3071L23.2533 12.0687L23.6615 10.9964C25.1239 7.13587 26.6218 4.28969 27.6897 3.3426ZM15.4393 23.1261C13.7864 24.0804 12.9602 24.5582 12.3529 25.093C9.89364 27.2585 8.9738 30.6888 10.0208 33.7938C10.2794 34.5606 10.7568 35.387 11.7111 37.0398C12.6653 38.6926 13.1424 39.5194 13.6771 40.1267C15.8426 42.5859 19.2732 43.5045 22.3783 42.4574C23.145 42.1989 23.972 41.7223 25.6248 40.768L35.1333 35.2783C36.7862 34.324 37.6124 33.8462 38.2196 33.3114C40.6789 31.1459 41.5988 27.7156 40.5518 24.6105C40.2932 23.8438 39.8157 23.0174 38.8615 21.3646C37.9072 19.7117 37.4302 18.885 36.8955 18.2777C34.7299 15.8185 31.2993 14.8999 28.1943 15.947C27.4275 16.2055 26.6006 16.6821 24.9478 17.6364L15.4393 23.1261Z" fill="url(#cb-cat)"/>
12
+ <rect x="15.8816" y="30.0273" width="4.00904" height="8.32646" rx="2.00452" transform="rotate(-30 15.8816 30.0273)" fill="white"/>
13
+ <rect x="26.6978" y="23.7812" width="4.00904" height="8.32646" rx="2.00452" transform="rotate(-30 26.6978 23.7812)" fill="white"/>
14
+ </g>
15
+ <defs>
16
+ <filter id="cb-glow-a" x="-14.9163" y="7.45599" width="55.0468" height="55.047" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
17
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
18
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
19
+ <feGaussianBlur stdDeviation="6.72123" result="cb-blur"/>
20
+ </filter>
21
+ <filter id="cb-glow-b" x="9.46959" y="19.7477" width="47.7417" height="47.741" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
22
+ <feFlood flood-opacity="0" result="BackgroundImageFix"/>
23
+ <feBlend mode="normal" in="SourceGraphic" in2="BackgroundImageFix" result="shape"/>
24
+ <feGaussianBlur stdDeviation="5.50019" result="cb-blur"/>
25
+ </filter>
26
+ <linearGradient id="cb-bg" x1="20" y1="0" x2="20" y2="40" gradientUnits="userSpaceOnUse">
27
+ <stop stop-color="#6C4DFF"/>
28
+ <stop offset="1" stop-color="#583ED3"/>
29
+ </linearGradient>
30
+ <linearGradient id="cb-cat" x1="14.639" y1="10.7917" x2="32.1712" y2="41.1585" gradientUnits="userSpaceOnUse">
31
+ <stop stop-color="white" stop-opacity="0.8"/>
32
+ <stop offset="0.437689" stop-color="white"/>
33
+ </linearGradient>
34
+ <clipPath id="cb-clip">
35
+ <rect width="40" height="40" rx="20" fill="white"/>
36
+ </clipPath>
37
+ </defs>
38
+ </svg>
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.177",
3
+ "version": "0.2.178",
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
@@ -1680,7 +1680,7 @@
1680
1680
  "saved_env_key": "CODEBUDDY_API_KEY",
1681
1681
  "login_command": "codebuddy",
1682
1682
  "unverifiable": true,
1683
- "not_ready_message": "Sign-in not confirmed. Set CODEBUDDY_API_KEY (press e), or run `codebuddy` and sign in with /login — there is no non-interactive login command. Auth is confirmed when you run a task.",
1683
+ "not_ready_message": "Not signed in for this site. Sign in with the Login button — it opens the CLI on the site CODEBUDDY_REGION selects, where you run /login — or set CODEBUDDY_API_KEY / CODEBUDDY_AUTH_TOKEN (press e). A session for the other site does not carry over: the CLI keeps only one, and codebuddy.ai and codebuddy.cn are separate accounts.",
1684
1684
  "auth_detected_labels": {
1685
1685
  "cli_login": "CodeBuddy sign-in detected",
1686
1686
  "api_key": "API key detected"
@@ -1079,11 +1079,23 @@ class BaseAdapter {
1079
1079
  }
1080
1080
  }
1081
1081
 
1082
+ /**
1083
+ * Post the agent's answer for a turn — the last thing said before control
1084
+ * returns to the user.
1085
+ *
1086
+ * `status_kind: 'completed'` marks it as exactly that. The backend's push
1087
+ * fan-out (workspace/backend/app/services/push.py) reads the field and
1088
+ * notifies under the user's "task completions" switch instead of the
1089
+ * "all messages" firehose, which is off by default. It has to be a
1090
+ * structured field: an earlier attempt matched the word "completed" in the
1091
+ * message text and fired on every bash for-loop that ended in `done`.
1092
+ */
1082
1093
  async sendResponse(channel, content) {
1083
1094
  try {
1084
1095
  await this.client.sendMessage(this.workspaceId, channel, this.token, content, {
1085
1096
  senderType: 'agent',
1086
1097
  senderName: this.agentName,
1098
+ metadata: { status_kind: 'completed' },
1087
1099
  sessionId: this._sessionId,
1088
1100
  });
1089
1101
  } catch (e) {
@@ -1154,11 +1166,18 @@ class BaseAdapter {
1154
1166
  }
1155
1167
  }
1156
1168
 
1169
+ /**
1170
+ * Post a turn that ended badly. Carries `status_kind: 'error'` so the push
1171
+ * fan-out routes it to the user's "agent errors" switch rather than the
1172
+ * general message firehose — same structured-field contract as
1173
+ * sendResponse's 'completed'.
1174
+ */
1157
1175
  async sendError(channel, error) {
1158
1176
  try {
1159
1177
  await this.client.sendMessage(this.workspaceId, channel, this.token, error, {
1160
1178
  senderType: 'agent',
1161
1179
  senderName: this.agentName,
1180
+ metadata: { status_kind: 'error' },
1162
1181
  sessionId: this._sessionId,
1163
1182
  });
1164
1183
  } catch (e) {
@@ -17,7 +17,7 @@ const path = require('path');
17
17
  const { execSync, spawn } = require('child_process');
18
18
 
19
19
  const BaseAdapter = require('./base');
20
- const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
20
+ const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle, redactSecrets } = require('./utils');
21
21
  const { buildClaudeSystemPrompt, buildClaudeSkillMd, workspaceSkillName } = require('./workspace-prompt');
22
22
  const { pinnedFingerprint, sampleRecap } = require('./decision-log');
23
23
  const { defaultAgentWorkdir, whichBinary, whereBinary } = require('../paths');
@@ -737,9 +737,17 @@ class ClaudeAdapter extends BaseAdapter {
737
737
  }
738
738
 
739
739
  if (consecutiveTimeouts >= this._WATCHDOG_MAX_TIMEOUTS) {
740
+ const tail = this._stderrTail(pp);
740
741
  this._log(`Watchdog: process unresponsive for ${consecutiveTimeouts * 15}s on ${pp.msgChannel} — killing`);
742
+ if (tail) this._log(`stderr: ${tail}`);
741
743
  this._stopWatchdog(pp);
742
- try { await this.sendError(pp.msgChannel, 'Agent process became unresponsive and was restarted.'); } catch {}
744
+ try {
745
+ await this.sendError(
746
+ pp.msgChannel,
747
+ 'Agent process became unresponsive and was restarted.' +
748
+ (tail ? `\n\n\`\`\`\n${tail}\n\`\`\`` : ''),
749
+ );
750
+ } catch {}
743
751
  if (pp.messageResolve) {
744
752
  const resolve = pp.messageResolve;
745
753
  pp.messageResolve = null;
@@ -926,6 +934,8 @@ class ClaudeAdapter extends BaseAdapter {
926
934
 
927
935
  proc.on('exit', (code) => {
928
936
  this._log(`Persistent process exited: channel=${channel} code=${code}`);
937
+ const tail = this._stderrTail(pp);
938
+ if (tail) this._log(`stderr: ${tail}`);
929
939
  pp.alive = false;
930
940
  if (pp.idleTimer) clearTimeout(pp.idleTimer);
931
941
  this._stopWatchdog(pp);
@@ -992,6 +1002,22 @@ class ClaudeAdapter extends BaseAdapter {
992
1002
  });
993
1003
  }
994
1004
 
1005
+ /**
1006
+ * What the CLI wrote to stderr this turn, trimmed and redacted, or ''.
1007
+ *
1008
+ * `stderrBuf` was collected and then read by nothing at all: when the process
1009
+ * died before emitting a single JSON event, or hung without emitting one, the
1010
+ * only account of why sat in a string no code path ever looked at. The channel
1011
+ * got "No response generated" and the daemon log got an exit code, so a run
1012
+ * that had the reason in hand reported none. codex, gemini and opencode all
1013
+ * log their stderr on exit; this is that, plus the two paths — watchdog kill
1014
+ * and silent exit — where it is the ONLY thing there is to report.
1015
+ */
1016
+ _stderrTail(pp) {
1017
+ const tail = String((pp && pp.stderrBuf) || '').trim();
1018
+ return tail ? redactSecrets(tail).slice(-800) : '';
1019
+ }
1020
+
995
1021
  /**
996
1022
  * Turn a raw Claude `result` error into a user-facing message. Auth failures
997
1023
  * (401 / invalid token) are the most common real-world cause and are otherwise
@@ -1291,10 +1317,15 @@ class ClaudeAdapter extends BaseAdapter {
1291
1317
  continue;
1292
1318
  }
1293
1319
  if (!pp.everPostedAnything) {
1294
- if (pp.lastErrorText) {
1295
- try { await this.sendError(msgChannel, this._formatClaudeError(pp.lastErrorText)); } catch {}
1320
+ // stderr is the fallback, not nothing: a CLI that dies before its
1321
+ // first JSON event leaves no `result` to read, and "No response
1322
+ // generated. Please try again." is what turned those into an
1323
+ // unreportable failure.
1324
+ const detail = pp.lastErrorText || this._stderrTail(pp);
1325
+ if (detail) {
1326
+ try { await this.sendError(msgChannel, this._formatClaudeError(detail)); } catch {}
1296
1327
  } else {
1297
- try { await this.sendResponse(msgChannel, 'No response generated. Please try again.'); } catch {}
1328
+ try { await this.sendResponse(msgChannel, `No response generated (exit code ${result.code === undefined ? 'unknown' : result.code}). Please try again.`); } catch {}
1298
1329
  }
1299
1330
  }
1300
1331
  break;
@@ -21,6 +21,7 @@ const https = require('https');
21
21
 
22
22
  const { whereBinary } = require('../paths');
23
23
  const BaseAdapter = require('./base');
24
+ const { redactSecrets } = require('./utils');
24
25
  const { buildOpenclawSystemPrompt } = require('./workspace-prompt');
25
26
 
26
27
  const IS_WINDOWS = process.platform === 'win32';
@@ -535,18 +536,7 @@ class CodexAdapter extends BaseAdapter {
535
536
 
536
537
  /** Redact secrets (keys, tokens, bearer/authorization, query secrets) from diagnostics. */
537
538
  static _redact(s) {
538
- let out = String(s == null ? '' : s);
539
- out = out
540
- .replace(/\bsk-[A-Za-z0-9_-]{6,}/g, 'sk-[REDACTED]')
541
- .replace(/\b(?:github_pat|gh[pousr])_[A-Za-z0-9_]{10,}/g, '[REDACTED_TOKEN]')
542
- .replace(/\bxox[baprs]-[A-Za-z0-9-]{8,}/g, '[REDACTED_TOKEN]')
543
- .replace(/\bAKIA[0-9A-Z]{12,}/g, '[REDACTED_KEY]')
544
- .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g, '[REDACTED_JWT]')
545
- .replace(/(authorization|api[_-]?key|x-api-key|token|bearer|secret|password|passwd)(["'\s:=]+)([^\s"',}]+)/gi,
546
- (m, k, sep) => `${k}${sep}[REDACTED]`)
547
- .replace(/([?&](?:api[_-]?key|key|token|access_token)=)[^&\s"']+/gi, '$1[REDACTED]')
548
- .replace(/\b[A-Za-z0-9_-]{40,}\b/g, '[REDACTED]');
549
- return out;
539
+ return redactSecrets(s);
550
540
  }
551
541
 
552
542
  // ------------------------------------------------------------------
@@ -17,7 +17,7 @@ const crypto = require('crypto');
17
17
  const { execSync, spawn } = require('child_process');
18
18
 
19
19
  const BaseAdapter = require('./base');
20
- const { formatAttachmentsForPrompt } = require('./utils');
20
+ const { formatAttachmentsForPrompt, redactSecrets } = require('./utils');
21
21
  const { buildOpenCodeSkillMd, buildOpenCodeSystemPrompt, workspaceSkillName } = require('./workspace-prompt');
22
22
  const { whichBinary, whereBinary, getEnhancedEnv } = require('../paths');
23
23
 
@@ -1224,18 +1224,7 @@ class OpenCodeAdapter extends BaseAdapter {
1224
1224
 
1225
1225
  /** Redact secrets (keys, tokens, bearer/authorization, query secrets) from diagnostics. */
1226
1226
  static _redact(s) {
1227
- let out = String(s == null ? '' : s);
1228
- out = out
1229
- .replace(/\bsk-[A-Za-z0-9_-]{6,}/g, 'sk-[REDACTED]')
1230
- .replace(/\b(?:github_pat|gh[pousr])_[A-Za-z0-9_]{10,}/g, '[REDACTED_TOKEN]')
1231
- .replace(/\bxox[baprs]-[A-Za-z0-9-]{8,}/g, '[REDACTED_TOKEN]')
1232
- .replace(/\bAKIA[0-9A-Z]{12,}/g, '[REDACTED_KEY]')
1233
- .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g, '[REDACTED_JWT]')
1234
- .replace(/(authorization|api[_-]?key|x-api-key|token|bearer|secret|password|passwd)(["'\s:=]+)([^\s"',}]+)/gi,
1235
- (m, k, sep) => `${k}${sep}[REDACTED]`)
1236
- .replace(/([?&](?:api[_-]?key|key|token|access_token)=)[^&\s"']+/gi, '$1[REDACTED]')
1237
- .replace(/\b[A-Za-z0-9_-]{40,}\b/g, '[REDACTED]');
1238
- return out;
1227
+ return redactSecrets(s);
1239
1228
  }
1240
1229
  }
1241
1230
 
@@ -120,8 +120,35 @@ function formatAttachmentsForPrompt(
120
120
  return lines.join('\n');
121
121
  }
122
122
 
123
+ /**
124
+ * Strip secrets out of anything on its way to a log line or a channel message.
125
+ *
126
+ * Adapter diagnostics quote raw CLI output, and a CLI that fails on auth tends
127
+ * to echo the credential it was handed. The shapes here are the ones that
128
+ * actually turn up in that output; the closing catch-all takes any long opaque
129
+ * token the named patterns missed.
130
+ *
131
+ * Lived as a private static on two adapters before claude needed it as well —
132
+ * a third identical copy is one copy too many for a security-relevant rule.
133
+ */
134
+ function redactSecrets(s) {
135
+ let out = String(s == null ? '' : s);
136
+ out = out
137
+ .replace(/\bsk-[A-Za-z0-9_-]{6,}/g, 'sk-[REDACTED]')
138
+ .replace(/\b(?:github_pat|gh[pousr])_[A-Za-z0-9_]{10,}/g, '[REDACTED_TOKEN]')
139
+ .replace(/\bxox[baprs]-[A-Za-z0-9-]{8,}/g, '[REDACTED_TOKEN]')
140
+ .replace(/\bAKIA[0-9A-Z]{12,}/g, '[REDACTED_KEY]')
141
+ .replace(/\beyJ[A-Za-z0-9_-]{8,}\.[A-Za-z0-9_-]{6,}\.[A-Za-z0-9_-]{6,}/g, '[REDACTED_JWT]')
142
+ .replace(/(authorization|api[_-]?key|x-api-key|token|bearer|secret|password|passwd)(["'\s:=]+)([^\s"',}]+)/gi,
143
+ (m, k, sep) => `${k}${sep}[REDACTED]`)
144
+ .replace(/([?&](?:api[_-]?key|key|token|access_token)=)[^&\s"']+/gi, '$1[REDACTED]')
145
+ .replace(/\b[A-Za-z0-9_-]{40,}\b/g, '[REDACTED]');
146
+ return out;
147
+ }
148
+
123
149
  module.exports = {
124
150
  SESSION_DEFAULT_RE,
125
151
  generateSessionTitle,
126
152
  formatAttachmentsForPrompt,
153
+ redactSecrets,
127
154
  };
package/src/installer.js CHANGED
@@ -1805,6 +1805,22 @@ class Installer {
1805
1805
  } catch {}
1806
1806
  }
1807
1807
  }
1808
+ // Where a user-level installer puts things, on every platform. uv's own
1809
+ // `curl … | sh` (and its Windows script) install into ~/.local/bin and add
1810
+ // it to PATH through a shell profile that a GUI-launched process never
1811
+ // reads — so an install command that IS `uv tool install …` died with
1812
+ // "uv: command not found" on a machine that had uv. install-preflight's
1813
+ // probeUv already searches these two, so leaving them out here meant the
1814
+ // pre-flight passed and the spawn it was guarding then failed.
1815
+ // unshift, not push: the loop below PREPENDS each entry, so the first one
1816
+ // here ends up LAST on PATH. These are a fallback for a binary nothing else
1817
+ // provides — they must never shadow the user's own PATH or the runtime
1818
+ // directories above.
1819
+ extraDirs.unshift(
1820
+ path.join(os.homedir(), '.local', 'bin'),
1821
+ path.join(os.homedir(), '.cargo', 'bin'),
1822
+ );
1823
+
1808
1824
  // On Windows the spread above yields a "Path" key, not "PATH". Writing
1809
1825
  // `env.PATH` would create a duplicate key holding only extraDirs and drop
1810
1826
  // everything else; libuv then resolves spawned binaries against the wrong,
package/src/probe.js CHANGED
@@ -30,7 +30,6 @@ const { spawn } = require('child_process');
30
30
  const { getEnhancedEnv } = require('./paths');
31
31
  const { shouldUseShellForBinary } = require('./adapters/health-status');
32
32
  const { formatAuthGuidance } = require('./auth-guidance');
33
- const { testLLMConnection } = require('./utils');
34
33
 
35
34
  const PROBE_PROMPT = 'hi';
36
35
  const DEFAULT_TIMEOUT_MS = 90_000;
@@ -90,7 +89,10 @@ function authFlavor(entry, agentEnv) {
90
89
  */
91
90
  /** Classify an error by its OUTPUT TEXT alone; null when nothing matches. */
92
91
  function classifyOutputText(text) {
93
- if (/credit balance|insufficient[_ ]?(quota|credits|funds)|payment required|\b402\b|billing/.test(text)) return CODE.OUT_OF_CREDIT;
92
+ // Relays word this their own way and answer 403 rather than 402, so the
93
+ // English-and-402 pattern alone read "out of credit" as "bad credentials"
94
+ // — the one misreading that sends a user to re-authenticate a working key.
95
+ if (/credit balance|insufficient[_ ]?(quota|credits|funds)|payment required|\b402\b|billing|quota[_ ]?exceeded|exceeded your current quota|额度|余额|欠费|余额不足/.test(text)) return CODE.OUT_OF_CREDIT;
94
96
  if (/rate[- ]?limit|too many requests|\b429\b|overloaded|\b529\b/.test(text)) return CODE.RATE_LIMITED;
95
97
  if (/enotfound|econnrefused|econnreset|etimedout|eai_again|fetch failed|socket hang up|network error|self[- ]signed certificate|unable to get local issuer/.test(text)) return CODE.NETWORK;
96
98
  if (/api key.{0,24}(not set|not found|missing)|no api key|missing api key|environment variable.{0,40}(is )?(not set|required)/.test(text)) return CODE.MISSING_API_KEY;
@@ -171,6 +173,37 @@ function buildGuidance(code, entry, agentEnv, extra = {}) {
171
173
  return lines;
172
174
  }
173
175
 
176
+ /**
177
+ * Ask the model endpoint directly why an agent is not answering.
178
+ *
179
+ * Only ever used to REPLACE a verdict that explains nothing, and only when it
180
+ * can do better: a successful call means the endpoint is healthy and whatever
181
+ * ails the CLI is the CLI's own, which the caller's vaguer answer already says
182
+ * more honestly than "everything is fine" would. Likewise an unclassifiable
183
+ * error — swapping one shrug for another helps nobody.
184
+ */
185
+ async function directApiVerdict(agentEnv, entry) {
186
+ // Required here rather than at the top so a test can stand in for it, and
187
+ // so a probe that never reaches this path never loads the HTTP client.
188
+ const { testLLMConnection } = require('./utils');
189
+ let res;
190
+ try {
191
+ res = await testLLMConnection(agentEnv);
192
+ } catch (e) {
193
+ res = { success: false, error: e.message };
194
+ }
195
+ if (!res || res.success) return null;
196
+ const code = classifyFailure(res.error, {});
197
+ if (code === CODE.CLI_ERROR) return null;
198
+ return {
199
+ ok: false,
200
+ method: 'llm_api',
201
+ code,
202
+ message: tail(res.error, OUTPUT_CAP),
203
+ guidance: buildGuidance(code, entry, agentEnv),
204
+ };
205
+ }
206
+
174
207
  /** Run a CLI to completion with a hard timeout. Never rejects. */
175
208
  function runCommand(cmd, args, { env, timeoutMs, shell = false }) {
176
209
  return new Promise((resolve) => {
@@ -266,6 +299,10 @@ async function probeAgentType(connector, type, opts = {}) {
266
299
  || (entry.probe && entry.probe.timeout_s ? entry.probe.timeout_s * 1000 : DEFAULT_TIMEOUT_MS);
267
300
  const env = { ...getEnhancedEnv(), ...agentEnv };
268
301
 
302
+ // Hoisted above the CLI tier: a failing CLI probe consults it too (below).
303
+ const hasApiKey = !!(agentEnv.LLM_API_KEY || agentEnv.OPENAI_API_KEY || agentEnv.ANTHROPIC_API_KEY
304
+ || agentEnv.ANTHROPIC_AUTH_TOKEN || agentEnv.KIMI_API_KEY || agentEnv.MOONSHOT_API_KEY);
305
+
269
306
  // ── Live tier 1: the agent's own CLI, when the registry declares how ──
270
307
  const probeArgs = entry.probe && Array.isArray(entry.probe.args) ? entry.probe.args : null;
271
308
  const altCheck = entry.check_ready && entry.check_ready.alt_check;
@@ -298,6 +335,19 @@ async function probeAgentType(connector, type, opts = {}) {
298
335
  guidance: buildGuidance(code, entry, agentEnv),
299
336
  });
300
337
  }
338
+ // A CLI verdict is not always the true one. A relay answering 403 "out of
339
+ // credit" makes Claude Code retry in silence until the probe's clock runs
340
+ // out, and all that reaches here is a timeout with an unrelated warning on
341
+ // stderr — which the guidance then reads as "it may be waiting for a login
342
+ // prompt", sending the user to a terminal to fix something that is not
343
+ // broken. Where the agent carries an endpoint and a key, ask the API the
344
+ // same question directly: one round trip, and it says why.
345
+ const unexplained = code === CODE.TIMEOUT || code === CODE.CLI_ERROR || code === CODE.EMPTY_RESPONSE;
346
+ if (unexplained && hasApiKey) {
347
+ const direct = await directApiVerdict(agentEnv, entry);
348
+ if (direct) return done(direct);
349
+ }
350
+
301
351
  return done({
302
352
  ok: false, method: 'cli', code,
303
353
  message: tail(res.stderr || res.stdout, OUTPUT_CAP) || (res.timedOut ? 'Timed out' : `Exited with code ${res.code}`),
@@ -306,9 +356,8 @@ async function probeAgentType(connector, type, opts = {}) {
306
356
  }
307
357
 
308
358
  // ── Live tier 2: direct LLM API call for API-key agents (e.g. OpenClaw) ──
309
- const hasApiKey = !!(agentEnv.LLM_API_KEY || agentEnv.OPENAI_API_KEY || agentEnv.ANTHROPIC_API_KEY
310
- || agentEnv.KIMI_API_KEY || agentEnv.MOONSHOT_API_KEY);
311
359
  if (hasApiKey || (entry.install && entry.install.api_only)) {
360
+ const { testLLMConnection } = require('./utils');
312
361
  let res;
313
362
  try { res = await testLLMConnection(agentEnv); } catch (e) { res = { success: false, error: e.message }; }
314
363
  if (res && res.success) {
package/src/utils.js CHANGED
@@ -13,12 +13,20 @@ function testLLMConnection(env) {
13
13
  const http = require('http');
14
14
 
15
15
  const hasKimiConfig = !!(env.KIMI_API_KEY || env.MOONSHOT_API_KEY || env.KIMI_BASE_URL || env.KIMI_MODEL);
16
- const apiKey = env.KIMI_API_KEY || env.MOONSHOT_API_KEY || env.LLM_API_KEY || env.OPENAI_API_KEY || env.ANTHROPIC_API_KEY || '';
16
+ const apiKey = env.KIMI_API_KEY || env.MOONSHOT_API_KEY || env.LLM_API_KEY || env.OPENAI_API_KEY
17
+ || env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || '';
17
18
  if (!apiKey) return Promise.resolve({ success: false, error: 'No API key provided' });
18
19
 
19
20
  let baseUrl = (env.KIMI_BASE_URL || env.LLM_BASE_URL || env.OPENAI_BASE_URL || (hasKimiConfig ? 'https://api.moonshot.ai/v1' : 'https://api.openai.com/v1')).replace(/\/$/, '');
20
21
  const model = env.KIMI_MODEL || env.LLM_MODEL || env.OPENCLAW_MODEL || '';
21
- const isAnthropic = baseUrl.includes('anthropic');
22
+ // An agent configured the Anthropic way is Anthropic-shaped wherever it
23
+ // points. Reading only the URL missed every relay whose host says nothing
24
+ // about the protocol it speaks — the common case for Claude Code, which is
25
+ // configured entirely through ANTHROPIC_* — and sent the test to OpenAI's
26
+ // default endpoint with an Anthropic key.
27
+ const hasAnthropicConfig = !hasKimiConfig && !env.LLM_API_KEY && !env.OPENAI_API_KEY
28
+ && !!(env.ANTHROPIC_BASE_URL || env.ANTHROPIC_MODEL || env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY);
29
+ const isAnthropic = hasAnthropicConfig || baseUrl.includes('anthropic');
22
30
 
23
31
  if (!isAnthropic && !baseUrl.endsWith('/v1')) {
24
32
  baseUrl += '/v1';
@@ -28,14 +36,25 @@ function testLLMConnection(env) {
28
36
  let url, headers, body;
29
37
 
30
38
  if (isAnthropic) {
31
- url = 'https://api.anthropic.com/v1/messages';
39
+ // The configured endpoint, not the official one: a relay IS the API as
40
+ // far as this agent is concerned, and testing api.anthropic.com would
41
+ // report on a service the agent never talks to.
42
+ const base = (env.ANTHROPIC_BASE_URL || 'https://api.anthropic.com').replace(/\/+$/, '');
43
+ url = /\/v1$/.test(base) ? `${base}/messages` : `${base}/v1/messages`;
44
+ const token = env.ANTHROPIC_AUTH_TOKEN || env.ANTHROPIC_API_KEY || apiKey;
32
45
  headers = {
33
- 'x-api-key': apiKey,
46
+ 'x-api-key': token,
34
47
  'anthropic-version': '2023-06-01',
35
48
  'content-type': 'application/json',
36
49
  };
50
+ // Relays are split on which header they read, and Claude Code itself
51
+ // sends the bearer when pointed at one. Only off the official host: the
52
+ // real API has no use for it.
53
+ if (!/api\.anthropic\.com$/.test(new URL(url).hostname)) {
54
+ headers['authorization'] = `Bearer ${token}`;
55
+ }
37
56
  body = JSON.stringify({
38
- model: model || 'claude-sonnet-4-20250514',
57
+ model: env.ANTHROPIC_MODEL || model || 'claude-sonnet-4-20250514',
39
58
  max_tokens: 32,
40
59
  messages: [{ role: 'user', content: 'Say hi in 5 words.' }],
41
60
  });