@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.
- package/icons/codebuddy.svg +37 -2
- package/package.json +1 -1
- package/registry.json +1 -1
- package/src/adapters/base.js +19 -0
- package/src/adapters/claude.js +36 -5
- package/src/adapters/codex.js +2 -12
- package/src/adapters/opencode.js +2 -13
- package/src/adapters/utils.js +27 -0
- package/src/installer.js +16 -0
- package/src/probe.js +53 -4
- package/src/utils.js +24 -5
package/icons/codebuddy.svg
CHANGED
|
@@ -1,3 +1,38 @@
|
|
|
1
|
-
<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0
|
|
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
|
-
<
|
|
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
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": "
|
|
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"
|
package/src/adapters/base.js
CHANGED
|
@@ -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) {
|
package/src/adapters/claude.js
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
1295
|
-
|
|
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,
|
|
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;
|
package/src/adapters/codex.js
CHANGED
|
@@ -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
|
-
|
|
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
|
// ------------------------------------------------------------------
|
package/src/adapters/opencode.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
package/src/adapters/utils.js
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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':
|
|
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
|
});
|