@openagents-org/agent-launcher 0.2.176 → 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 +38 -0
- package/icons/openworker.svg +3 -0
- package/package.json +1 -1
- package/registry.json +269 -0
- package/src/adapters/base.js +19 -0
- package/src/adapters/claude.js +36 -5
- package/src/adapters/codebuddy-stream.js +497 -0
- package/src/adapters/codebuddy.js +913 -0
- package/src/adapters/codex.js +2 -12
- package/src/adapters/index.js +6 -1
- package/src/adapters/opencode.js +2 -13
- package/src/adapters/openworker-runtime.js +572 -0
- package/src/adapters/openworker.js +921 -0
- package/src/adapters/utils.js +27 -0
- package/src/adapters/workspace-prompt.js +33 -0
- package/src/install-preflight.js +84 -6
- package/src/installer.js +16 -0
- package/src/paths.js +20 -8
- package/src/probe.js +53 -4
- package/src/utils.js +24 -5
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
|
};
|
|
@@ -1021,6 +1021,38 @@ function buildPiSystemPrompt({ agentName, workspaceId, channelName, endpoint, to
|
|
|
1021
1021
|
return identity + directive + '\n' + collab + '\n' + modePrompt + '\n' + api + '\n' + buildGuardrails();
|
|
1022
1022
|
}
|
|
1023
1023
|
|
|
1024
|
+
/**
|
|
1025
|
+
* Build the workspace briefing that seeds an OpenWorker session.
|
|
1026
|
+
*
|
|
1027
|
+
* Shape mirrors buildPiSystemPrompt — identity, browser directive,
|
|
1028
|
+
* collaboration, mode, then the REST block — because OpenWorker reaches the
|
|
1029
|
+
* workspace the same way: its own shell tool plus curl, with no MCP wiring from
|
|
1030
|
+
* our side (registering an MCP server there writes into the user's global
|
|
1031
|
+
* OpenWorker config, which is not ours to edit).
|
|
1032
|
+
*
|
|
1033
|
+
* Two differences from every other builder here, both forced by OpenWorker
|
|
1034
|
+
* having no system-prompt hook at all:
|
|
1035
|
+
*
|
|
1036
|
+
* - `execTool` is `run_shell`, the actual name of its shell tool. Naming a
|
|
1037
|
+
* tool the agent does not have is how a model ends up printing curl
|
|
1038
|
+
* commands as text instead of running them.
|
|
1039
|
+
* - This is prepended to the first USER message of a session rather than
|
|
1040
|
+
* installed as a system prompt. That makes it part of the persisted
|
|
1041
|
+
* conversation, so it is sent once and still in context on every later turn
|
|
1042
|
+
* — which is why it must read as a briefing, not as a system directive.
|
|
1043
|
+
*/
|
|
1044
|
+
function buildOpenWorkerSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
|
|
1045
|
+
const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
|
|
1046
|
+
const directive = buildBrowserDirective(browserEnabled);
|
|
1047
|
+
const collab = buildCollaborationPrompt('skills', workspaceSkillName(agentName));
|
|
1048
|
+
const modePrompt = buildModePrompt(mode);
|
|
1049
|
+
const api = buildApiSkillsPrompt({
|
|
1050
|
+
endpoint, workspaceId, token, agentName, channelName, disabledModules, mode,
|
|
1051
|
+
execTool: 'run_shell',
|
|
1052
|
+
});
|
|
1053
|
+
return identity + directive + '\n' + collab + '\n' + modePrompt + '\n' + api + '\n' + buildGuardrails();
|
|
1054
|
+
}
|
|
1055
|
+
|
|
1024
1056
|
/**
|
|
1025
1057
|
* Build the task FILE contents for the DeepSeek Harness adapter.
|
|
1026
1058
|
*
|
|
@@ -1223,6 +1255,7 @@ module.exports = {
|
|
|
1223
1255
|
buildOpenCodeSkillMd,
|
|
1224
1256
|
buildCommandCodeSkillMd,
|
|
1225
1257
|
buildPiSystemPrompt,
|
|
1258
|
+
buildOpenWorkerSystemPrompt,
|
|
1226
1259
|
buildDeepSeekTaskFile,
|
|
1227
1260
|
buildClaudeSkillMd,
|
|
1228
1261
|
buildCursorSkillMd,
|
package/src/install-preflight.js
CHANGED
|
@@ -29,11 +29,10 @@
|
|
|
29
29
|
'use strict';
|
|
30
30
|
|
|
31
31
|
const fs = require('fs');
|
|
32
|
+
const os = require('os');
|
|
32
33
|
const path = require('path');
|
|
33
34
|
const { execFileSync } = require('child_process');
|
|
34
35
|
|
|
35
|
-
const IS_MACOS = process.platform === 'darwin';
|
|
36
|
-
|
|
37
36
|
/** Directories on PATH, minus any the caller wants skipped. */
|
|
38
37
|
function pathDirs(exclude = []) {
|
|
39
38
|
const raw = process.env.PATH || '';
|
|
@@ -154,30 +153,106 @@ function probeGit(deps = {}) {
|
|
|
154
153
|
|
|
155
154
|
/**
|
|
156
155
|
* How the user fixes each missing dependency, phrased for the install screen.
|
|
157
|
-
*
|
|
158
|
-
*
|
|
156
|
+
*
|
|
157
|
+
* `action` is a hint the launcher UI can turn into a button. `summaryKey` and
|
|
158
|
+
* `alternativeKind` are the same thing for text: the launcher is translated
|
|
159
|
+
* and this package is not, so it looks up its own wording by those keys and
|
|
160
|
+
* keeps `summary` only as the fallback — which is also what the CLI and the
|
|
161
|
+
* install log print. `alternativeKind` in particular has to be carried rather
|
|
162
|
+
* than guessed: the alternative is Homebrew only on macOS, and a UI that
|
|
163
|
+
* labels every alternative "if you use Homebrew" is wrong on two of the three
|
|
164
|
+
* platforms we ship.
|
|
165
|
+
*
|
|
166
|
+
* @param {string} [platform] - seam for tests; production passes nothing.
|
|
159
167
|
*/
|
|
160
|
-
function gitRemedy() {
|
|
161
|
-
if (
|
|
168
|
+
function gitRemedy(platform = process.platform) {
|
|
169
|
+
if (platform === 'darwin') {
|
|
162
170
|
return {
|
|
163
171
|
name: 'git',
|
|
164
172
|
action: 'install-xcode-clt',
|
|
173
|
+
summaryKey: 'gitXcodeClt',
|
|
165
174
|
summary: 'Git is required, and it comes with the Xcode Command Line Tools.',
|
|
166
175
|
command: 'xcode-select --install',
|
|
167
176
|
alternative: 'brew install git',
|
|
177
|
+
alternativeKind: 'homebrew',
|
|
168
178
|
};
|
|
169
179
|
}
|
|
170
180
|
return {
|
|
171
181
|
name: 'git',
|
|
172
182
|
action: null,
|
|
183
|
+
summaryKey: 'git',
|
|
173
184
|
summary: 'Git is required to download this agent.',
|
|
174
185
|
command: 'sudo apt install git # or dnf / pacman, per your distro',
|
|
175
186
|
alternative: null,
|
|
187
|
+
alternativeKind: null,
|
|
188
|
+
};
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
/**
|
|
192
|
+
* Is `uv` on PATH?
|
|
193
|
+
*
|
|
194
|
+
* Unlike git, nothing provisions uv for us: an agent whose install command IS
|
|
195
|
+
* `uv tool install …` and whose machine has no uv gets the shell's own
|
|
196
|
+
* "command not found" buried in installer output, several seconds after
|
|
197
|
+
* pressing Install. The probe is a plain filesystem check — running `uv
|
|
198
|
+
* --version` would be the same answer at the cost of a process spawn, and uv
|
|
199
|
+
* has no shim behaviour to catch out the way macOS's /usr/bin/git does.
|
|
200
|
+
*
|
|
201
|
+
* Also checks uv's own default install location, which its installer script
|
|
202
|
+
* adds to PATH via a shell profile a running GUI/daemon never re-read.
|
|
203
|
+
*/
|
|
204
|
+
function probeUv(deps = {}) {
|
|
205
|
+
const platform = deps.platform || process.platform;
|
|
206
|
+
const exists = deps.exists || ((p) => fs.existsSync(p));
|
|
207
|
+
const home = deps.home || os.homedir();
|
|
208
|
+
const isWindows = platform === 'win32';
|
|
209
|
+
const search = deps.dirs || [
|
|
210
|
+
...pathDirs(),
|
|
211
|
+
path.join(home, '.local', 'bin'),
|
|
212
|
+
path.join(home, '.cargo', 'bin'),
|
|
213
|
+
...(isWindows ? [] : ['/opt/homebrew/bin', '/usr/local/bin']),
|
|
214
|
+
];
|
|
215
|
+
const found = existsIn(search, 'uv', exists, isWindows);
|
|
216
|
+
return found ? { ok: true, detail: found } : { ok: false };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/** @param {string} [platform] - seam for tests; production passes nothing. */
|
|
220
|
+
function uvRemedy(platform = process.platform) {
|
|
221
|
+
const summary = 'uv is required to install this agent (it is a Python tool).';
|
|
222
|
+
|
|
223
|
+
if (platform === 'win32') {
|
|
224
|
+
return {
|
|
225
|
+
name: 'uv',
|
|
226
|
+
action: null,
|
|
227
|
+
summaryKey: 'uv',
|
|
228
|
+
summary,
|
|
229
|
+
// Spelled out instead of a bare `irm … | iex`, because "run this in a
|
|
230
|
+
// terminal" on Windows lands in cmd.exe as often as in PowerShell, and
|
|
231
|
+
// the bare form is a syntax error there.
|
|
232
|
+
command:
|
|
233
|
+
'powershell -ExecutionPolicy ByPass -c "irm https://astral.sh/uv/install.ps1 | iex"',
|
|
234
|
+
// pipx is the obvious parallel to Homebrew, but a machine with no uv
|
|
235
|
+
// seldom has pipx either — whereas winget ships with Windows.
|
|
236
|
+
alternative: 'winget install --id=astral-sh.uv -e',
|
|
237
|
+
alternativeKind: 'winget',
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
const viaHomebrew = platform === 'darwin';
|
|
242
|
+
return {
|
|
243
|
+
name: 'uv',
|
|
244
|
+
action: null,
|
|
245
|
+
summaryKey: 'uv',
|
|
246
|
+
summary,
|
|
247
|
+
command: 'curl -LsSf https://astral.sh/uv/install.sh | sh',
|
|
248
|
+
alternative: viaHomebrew ? 'brew install uv' : 'pipx install uv',
|
|
249
|
+
alternativeKind: viaHomebrew ? 'homebrew' : 'pipx',
|
|
176
250
|
};
|
|
177
251
|
}
|
|
178
252
|
|
|
179
253
|
const PROBES = {
|
|
180
254
|
git: { probe: probeGit, remedy: gitRemedy },
|
|
255
|
+
uv: { probe: probeUv, remedy: uvRemedy },
|
|
181
256
|
};
|
|
182
257
|
|
|
183
258
|
/**
|
|
@@ -230,4 +305,7 @@ module.exports = {
|
|
|
230
305
|
checkInstallPrereqs,
|
|
231
306
|
missingPrereqError,
|
|
232
307
|
probeGit,
|
|
308
|
+
probeUv,
|
|
309
|
+
gitRemedy,
|
|
310
|
+
uvRemedy,
|
|
233
311
|
};
|
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/paths.js
CHANGED
|
@@ -58,11 +58,13 @@ function getExtraBinDirs() {
|
|
|
58
58
|
// CLIs whose own installer script picks a directory of its own.
|
|
59
59
|
_addAgentInstallerPaths(dirs);
|
|
60
60
|
|
|
61
|
-
//
|
|
61
|
+
// CLIs installed with `uv tool install` — the executable honors XDG_BIN_HOME /
|
|
62
62
|
// XDG_DATA_HOME/../bin / ~/.local/bin and also always lands in the uv tools
|
|
63
63
|
// venv. A GUI/daemon process won't see the installer's PATH edit, so add the
|
|
64
64
|
// real install dirs explicitly (filtered to existing dirs by the caller).
|
|
65
|
+
// `coworker` is OpenWorker's distribution name — see uvToolBinDirs.
|
|
65
66
|
for (const d of aiderBinDirs()) _push(dirs, d);
|
|
67
|
+
for (const d of uvToolBinDirs('coworker')) _push(dirs, d);
|
|
66
68
|
|
|
67
69
|
// Also add the directory containing the current node binary
|
|
68
70
|
try {
|
|
@@ -787,19 +789,23 @@ function primeBinaryLookup() {
|
|
|
787
789
|
}
|
|
788
790
|
|
|
789
791
|
/**
|
|
790
|
-
* Directories where
|
|
791
|
-
*
|
|
792
|
-
* detection matches reality on every platform:
|
|
792
|
+
* Directories where a `uv tool install`-ed CLI can land, in the SAME priority uv
|
|
793
|
+
* itself uses, so detection matches reality on every platform:
|
|
793
794
|
* 1. $XDG_BIN_HOME
|
|
794
795
|
* 2. $XDG_DATA_HOME/../bin
|
|
795
796
|
* 3. ~/.local/bin (the default uv-tool / pipx / pip --user bin)
|
|
796
|
-
* 4. the uv tools venv for
|
|
797
|
+
* 4. the uv tools venv for `pkg` (Scripts on Windows, bin on Unix) — the
|
|
797
798
|
* executable always lands here on a successful `uv tool install`, even if
|
|
798
799
|
* the bin-dir copy/PATH edit didn't happen.
|
|
800
|
+
*
|
|
801
|
+
* `pkg` is the DISTRIBUTION name uv keys its tool dir by, which is not always
|
|
802
|
+
* the command name: aider's CLI is `aider` from `aider-chat`, and OpenWorker's
|
|
803
|
+
* is `openworker-server` from `coworker`.
|
|
804
|
+
*
|
|
799
805
|
* Uses os.homedir()/live env so it reflects the current process (test-friendly).
|
|
800
806
|
* Returns directories only; callers join the platform binary names.
|
|
801
807
|
*/
|
|
802
|
-
function
|
|
808
|
+
function uvToolBinDirs(pkg) {
|
|
803
809
|
const home = os.homedir();
|
|
804
810
|
const dirs = [];
|
|
805
811
|
if (process.env.XDG_BIN_HOME) dirs.push(process.env.XDG_BIN_HOME);
|
|
@@ -808,17 +814,22 @@ function aiderBinDirs() {
|
|
|
808
814
|
if (IS_WINDOWS) {
|
|
809
815
|
const appData = process.env.APPDATA || path.join(home, 'AppData', 'Roaming');
|
|
810
816
|
const uvTools = process.env.UV_TOOL_DIR || path.join(appData, 'uv', 'tools');
|
|
811
|
-
dirs.push(path.join(uvTools,
|
|
817
|
+
dirs.push(path.join(uvTools, pkg, 'Scripts'));
|
|
812
818
|
dirs.push(path.join(home, 'bin'));
|
|
813
819
|
} else {
|
|
814
820
|
const uvTools = process.env.UV_TOOL_DIR
|
|
815
821
|
|| path.join(home, '.local', 'share', 'uv', 'tools');
|
|
816
|
-
dirs.push(path.join(uvTools,
|
|
822
|
+
dirs.push(path.join(uvTools, pkg, 'bin'));
|
|
817
823
|
dirs.push(path.join(home, 'bin'), '/usr/local/bin', '/opt/homebrew/bin');
|
|
818
824
|
}
|
|
819
825
|
return dirs;
|
|
820
826
|
}
|
|
821
827
|
|
|
828
|
+
/** Aider's CLI, installed by aider.chat/install.{sh,ps1} → `uv tool install`. */
|
|
829
|
+
function aiderBinDirs() {
|
|
830
|
+
return uvToolBinDirs('aider-chat');
|
|
831
|
+
}
|
|
832
|
+
|
|
822
833
|
/** Executable suffixes to try for a bare binary name, per platform. */
|
|
823
834
|
const BIN_EXTS = IS_WINDOWS ? ['.cmd', '.exe', '.bat', ''] : [''];
|
|
824
835
|
|
|
@@ -889,6 +900,7 @@ module.exports = {
|
|
|
889
900
|
getCorePrefix,
|
|
890
901
|
defaultAgentWorkdir,
|
|
891
902
|
aiderBinDirs,
|
|
903
|
+
uvToolBinDirs,
|
|
892
904
|
IS_WINDOWS,
|
|
893
905
|
IS_MACOS,
|
|
894
906
|
SEP,
|
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
|
});
|