@openagents-org/agent-launcher 0.2.186 → 0.2.187
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 +2 -2
- package/registry.json +13 -8
- package/src/adapters/cline.js +34 -38
- package/src/adapters/codex.js +246 -66
- package/src/adapters/hermes.js +40 -0
- package/src/adapters/llm-direct.js +2 -1
- package/src/adapters/utils.js +7 -4
- package/src/daemon.js +81 -1
- package/src/installer.js +232 -59
- package/src/model-list.js +100 -0
- package/src/paths.js +102 -0
- package/src/probe.js +23 -2
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Ask an OpenAI- or Anthropic-compatible endpoint which models it serves.
|
|
5
|
+
*
|
|
6
|
+
* The daemon answers the workspace's `list_models` command with this, so the
|
|
7
|
+
* model picker offers what the agent's relay actually has, and the key the
|
|
8
|
+
* request needs never leaves the device. Mirrors the launcher's
|
|
9
|
+
* model-catalog listOpenAiModels / listAnthropicModels.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
const MAX_MODELS = 300;
|
|
13
|
+
|
|
14
|
+
/** The one line worth showing from an error body (relays nest it in JSON). */
|
|
15
|
+
function briefError(text) {
|
|
16
|
+
try {
|
|
17
|
+
const j = JSON.parse(text);
|
|
18
|
+
const msg = typeof j.error === 'string' ? j.error : j.error && j.error.message;
|
|
19
|
+
const line = String(msg || j.message || '').trim();
|
|
20
|
+
if (line) return line.slice(0, 160);
|
|
21
|
+
} catch {}
|
|
22
|
+
return String(text || '').trim().slice(0, 160);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function isOfficialAnthropic(url) {
|
|
26
|
+
return url.hostname.toLowerCase() === 'api.anthropic.com';
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** `{data: [...]}`, a bare array, or a relay's `{models: [...]}`; ids deduped and sorted. */
|
|
30
|
+
function parseModels(text) {
|
|
31
|
+
let parsed;
|
|
32
|
+
try { parsed = JSON.parse(text); } catch { return []; }
|
|
33
|
+
const list = Array.isArray(parsed)
|
|
34
|
+
? parsed
|
|
35
|
+
: (parsed && (parsed.data || parsed.models)) || [];
|
|
36
|
+
if (!Array.isArray(list)) return [];
|
|
37
|
+
const seen = new Map();
|
|
38
|
+
for (const entry of list) {
|
|
39
|
+
const id = String(typeof entry === 'string' ? entry : (entry && entry.id) || '').trim();
|
|
40
|
+
if (!id || seen.has(id)) continue;
|
|
41
|
+
const label = entry && typeof entry === 'object' && typeof entry.display_name === 'string'
|
|
42
|
+
? entry.display_name.trim()
|
|
43
|
+
: '';
|
|
44
|
+
seen.set(id, { id, label: label || id });
|
|
45
|
+
}
|
|
46
|
+
return [...seen.values()].sort((a, b) => a.id.localeCompare(b.id)).slice(0, MAX_MODELS);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* @param {object} opts
|
|
51
|
+
* @param {string} opts.baseUrl The base URL the agent's CLI is pointed at.
|
|
52
|
+
* @param {string} opts.apiKey
|
|
53
|
+
* @param {string} [opts.protocol] 'anthropic', or OpenAI-compatible (default).
|
|
54
|
+
* @returns {Promise<{models: {id: string, label: string}[], error?: string}>}
|
|
55
|
+
*/
|
|
56
|
+
async function listEndpointModels({ baseUrl, apiKey, protocol = 'openai', fetchImpl = globalThis.fetch, timeoutMs = 15000 }) {
|
|
57
|
+
let url;
|
|
58
|
+
try {
|
|
59
|
+
url = new URL(/^[a-z][a-z0-9+.-]*:\/\//i.test(baseUrl) ? baseUrl : `http://${baseUrl}`);
|
|
60
|
+
} catch {
|
|
61
|
+
return { models: [], error: 'The configured base URL is not a valid URL.' };
|
|
62
|
+
}
|
|
63
|
+
if (!apiKey) return { models: [], error: 'No API key is configured for this agent.' };
|
|
64
|
+
|
|
65
|
+
const headers = { Accept: 'application/json' };
|
|
66
|
+
let path = url.pathname.replace(/\/+$/, '');
|
|
67
|
+
if (protocol === 'anthropic') {
|
|
68
|
+
// Official takes x-api-key; relays only ever honor a bearer token.
|
|
69
|
+
if (isOfficialAnthropic(url)) headers['x-api-key'] = apiKey;
|
|
70
|
+
else headers.Authorization = `Bearer ${apiKey}`;
|
|
71
|
+
headers['anthropic-version'] = '2023-06-01';
|
|
72
|
+
path = `${path.replace(/\/v1$/, '')}/v1/models`;
|
|
73
|
+
url.search = '?limit=100';
|
|
74
|
+
} else {
|
|
75
|
+
headers.Authorization = `Bearer ${apiKey}`;
|
|
76
|
+
// A bare host means the /v1 API; a path (…/v1, …/v1beta/openai) is used as given.
|
|
77
|
+
path = `${path || '/v1'}/models`;
|
|
78
|
+
url.search = '';
|
|
79
|
+
}
|
|
80
|
+
url.pathname = path;
|
|
81
|
+
url.username = '';
|
|
82
|
+
url.password = '';
|
|
83
|
+
|
|
84
|
+
let res;
|
|
85
|
+
let text;
|
|
86
|
+
try {
|
|
87
|
+
res = await fetchImpl(url.toString(), { headers, signal: AbortSignal.timeout(timeoutMs) });
|
|
88
|
+
text = await res.text();
|
|
89
|
+
} catch (e) {
|
|
90
|
+
const reason = e && e.name === 'TimeoutError' ? 'the endpoint did not answer in time' : (e && e.message) || String(e);
|
|
91
|
+
return { models: [], error: `Could not reach ${url.host}: ${reason}` };
|
|
92
|
+
}
|
|
93
|
+
if (res.status >= 400) {
|
|
94
|
+
return { models: [], error: `HTTP ${res.status}: ${briefError(text)}` };
|
|
95
|
+
}
|
|
96
|
+
const models = parseModels(text);
|
|
97
|
+
return models.length ? { models } : { models: [], error: 'The endpoint listed no models.' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = { listEndpointModels, parseModels };
|
package/src/paths.js
CHANGED
|
@@ -1132,6 +1132,105 @@ function aiderBinDirs() {
|
|
|
1132
1132
|
/** Executable suffixes to try for a bare binary name, per platform. */
|
|
1133
1133
|
const BIN_EXTS = IS_WINDOWS ? ['.cmd', '.exe', '.bat', ''] : [''];
|
|
1134
1134
|
|
|
1135
|
+
/**
|
|
1136
|
+
* A `#!...node` shebang line, as the Windows launcher matches it (win-exec.ts).
|
|
1137
|
+
* `env -S`/flag forms are allowed; `#!/bin/sh -c node` is NOT a node script.
|
|
1138
|
+
*/
|
|
1139
|
+
const NODE_SHEBANG_RE =
|
|
1140
|
+
/^#!\s*(?:(?:\S*[\\/])?env(?:\.exe)?\s+(?:-\S+\s+)*)?(?:\S*[\\/])?node(?:\.exe)?(?:\s|$)/i;
|
|
1141
|
+
|
|
1142
|
+
/**
|
|
1143
|
+
* True when a file is an extensionless Node script — npm's package bin shape
|
|
1144
|
+
* (e.g. node_modules/cline/bin/cline). Windows cannot execute one: handed to
|
|
1145
|
+
* cmd.exe it fails with "is not recognized as an internal or external command",
|
|
1146
|
+
* so every caller that shells out to a resolved binary must prefix `node`.
|
|
1147
|
+
*/
|
|
1148
|
+
function isNodeShebangScript(filePath) {
|
|
1149
|
+
if (!filePath) return false;
|
|
1150
|
+
try {
|
|
1151
|
+
const fd = fs.openSync(filePath, 'r');
|
|
1152
|
+
try {
|
|
1153
|
+
const buf = Buffer.alloc(64);
|
|
1154
|
+
const n = fs.readSync(fd, buf, 0, buf.length, 0);
|
|
1155
|
+
const firstLine = buf.subarray(0, n).toString('utf-8').split('\n', 1)[0];
|
|
1156
|
+
return NODE_SHEBANG_RE.test(firstLine);
|
|
1157
|
+
} finally {
|
|
1158
|
+
fs.closeSync(fd);
|
|
1159
|
+
}
|
|
1160
|
+
} catch {
|
|
1161
|
+
return false;
|
|
1162
|
+
}
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
/** Managed npm module roots, in the same priority order used by Installer. */
|
|
1166
|
+
function managedNpmModuleDirs(agentType) {
|
|
1167
|
+
const dirs = [
|
|
1168
|
+
path.join(getRuntimePrefix(agentType), 'node_modules'),
|
|
1169
|
+
path.join(os.homedir(), '.openagents', 'nodejs', 'node_modules'),
|
|
1170
|
+
];
|
|
1171
|
+
return [...new Set(dirs)];
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
/**
|
|
1175
|
+
* Resolve a package's declared `bin` target from an OpenAgents-managed npm
|
|
1176
|
+
* prefix. This covers packages such as Cline whose root package is installed
|
|
1177
|
+
* successfully but npm does not create a node_modules/.bin shim for it.
|
|
1178
|
+
*/
|
|
1179
|
+
function resolveManagedNpmPackageBin(agentType, packageName, binaryName) {
|
|
1180
|
+
if (!agentType || !packageName || !binaryName) return null;
|
|
1181
|
+
|
|
1182
|
+
for (const modules of managedNpmModuleDirs(agentType)) {
|
|
1183
|
+
const pkgDir = path.join(modules, packageName);
|
|
1184
|
+
const pkgJsonPath = path.join(pkgDir, 'package.json');
|
|
1185
|
+
try {
|
|
1186
|
+
if (!fs.existsSync(pkgJsonPath)) continue;
|
|
1187
|
+
const bin = JSON.parse(fs.readFileSync(pkgJsonPath, 'utf-8')).bin;
|
|
1188
|
+
let rel = null;
|
|
1189
|
+
if (typeof bin === 'string') rel = bin;
|
|
1190
|
+
else if (bin && typeof bin === 'object') {
|
|
1191
|
+
rel = bin[binaryName] || bin[packageName] || Object.values(bin)[0];
|
|
1192
|
+
}
|
|
1193
|
+
if (!rel) continue;
|
|
1194
|
+
const abs = path.join(pkgDir, rel);
|
|
1195
|
+
if (fs.existsSync(abs) && fs.statSync(abs).isFile()) return abs;
|
|
1196
|
+
} catch {
|
|
1197
|
+
/* malformed or unreadable package — keep looking */
|
|
1198
|
+
}
|
|
1199
|
+
}
|
|
1200
|
+
return null;
|
|
1201
|
+
}
|
|
1202
|
+
|
|
1203
|
+
/**
|
|
1204
|
+
* Resolve the copy of an npm CLI installed in an OpenAgents-managed prefix.
|
|
1205
|
+
* Package presence gates the lookup so an orphaned shim cannot shadow a
|
|
1206
|
+
* working global install. The package's own bin is the final managed fallback.
|
|
1207
|
+
*/
|
|
1208
|
+
function resolveManagedNpmBinary(agentType, packageName, names) {
|
|
1209
|
+
const list = (Array.isArray(names) ? names : [names]).filter(Boolean);
|
|
1210
|
+
if (!agentType || !packageName || !list.length) return null;
|
|
1211
|
+
|
|
1212
|
+
for (const modules of managedNpmModuleDirs(agentType)) {
|
|
1213
|
+
try {
|
|
1214
|
+
if (!fs.existsSync(path.join(modules, packageName, 'package.json'))) continue;
|
|
1215
|
+
} catch {
|
|
1216
|
+
continue;
|
|
1217
|
+
}
|
|
1218
|
+
for (const name of list) {
|
|
1219
|
+
for (const ext of BIN_EXTS) {
|
|
1220
|
+
const candidate = path.join(modules, '.bin', `${name}${ext}`);
|
|
1221
|
+
try {
|
|
1222
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
1223
|
+
} catch {
|
|
1224
|
+
/* unreadable shim — keep looking */
|
|
1225
|
+
}
|
|
1226
|
+
}
|
|
1227
|
+
}
|
|
1228
|
+
const own = resolveManagedNpmPackageBin(agentType, packageName, list[0]);
|
|
1229
|
+
if (own) return own;
|
|
1230
|
+
}
|
|
1231
|
+
return null;
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1135
1234
|
/**
|
|
1136
1235
|
* Last-resort binary lookup: walk the directories we already know agent CLIs
|
|
1137
1236
|
* land in and check the filesystem directly, instead of asking the shell.
|
|
@@ -1205,6 +1304,9 @@ module.exports = {
|
|
|
1205
1304
|
getEnhancedEnv,
|
|
1206
1305
|
whichBinary,
|
|
1207
1306
|
whereBinary,
|
|
1307
|
+
resolveManagedNpmBinary,
|
|
1308
|
+
resolveManagedNpmPackageBin,
|
|
1309
|
+
isNodeShebangScript,
|
|
1208
1310
|
resolveBinaryInKnownDirs,
|
|
1209
1311
|
clearBinaryLookupCache,
|
|
1210
1312
|
primeBinaryLookup,
|
package/src/probe.js
CHANGED
|
@@ -58,6 +58,24 @@ const CODE = {
|
|
|
58
58
|
CLI_ERROR: 'cli_error',
|
|
59
59
|
};
|
|
60
60
|
|
|
61
|
+
/**
|
|
62
|
+
* The install command for THIS device's platform.
|
|
63
|
+
*
|
|
64
|
+
* `entry.install_command` comes off the catalog API, which picks macOS → Linux
|
|
65
|
+
* → Windows with no idea who is asking — so a Windows user was told to fix a
|
|
66
|
+
* missing CLI by running a `curl … | bash` one-liner that cannot work there.
|
|
67
|
+
* The entry's own per-platform `install` block is the authority (same rule as
|
|
68
|
+
* Installer._getInstallCommand); the flat field stays as the fallback for
|
|
69
|
+
* catalog entries that carry nothing else.
|
|
70
|
+
*/
|
|
71
|
+
function installCommandFor(entry) {
|
|
72
|
+
const inst = (entry && entry.install) || {};
|
|
73
|
+
const plat = process.platform === 'darwin' ? 'macos'
|
|
74
|
+
: process.platform === 'win32' ? 'windows'
|
|
75
|
+
: 'linux';
|
|
76
|
+
return inst[plat] || inst.command || inst.npm || (entry && entry.install_command) || null;
|
|
77
|
+
}
|
|
78
|
+
|
|
61
79
|
/** Redact anything that looks like a secret before output leaves the device. */
|
|
62
80
|
function scrub(text) {
|
|
63
81
|
if (!text) return '';
|
|
@@ -129,7 +147,10 @@ function buildGuidance(code, entry, agentEnv, extra = {}) {
|
|
|
129
147
|
switch (code) {
|
|
130
148
|
case CODE.NOT_INSTALLED:
|
|
131
149
|
lines.push(`${label} is not installed on this device. Install it from the launcher's Install page.`);
|
|
132
|
-
|
|
150
|
+
{
|
|
151
|
+
const cmd = installCommandFor(entry);
|
|
152
|
+
if (cmd) lines.push(`Or install manually: ${cmd}`);
|
|
153
|
+
}
|
|
133
154
|
break;
|
|
134
155
|
case CODE.INVALID_API_KEY:
|
|
135
156
|
case CODE.MISSING_API_KEY:
|
|
@@ -388,4 +409,4 @@ async function probeAgentType(connector, type, opts = {}) {
|
|
|
388
409
|
});
|
|
389
410
|
}
|
|
390
411
|
|
|
391
|
-
module.exports = { probeAgentType, classifyFailure, buildGuidance, authFlavor, scrub, CODE, DEFAULT_TIMEOUT_MS };
|
|
412
|
+
module.exports = { probeAgentType, classifyFailure, buildGuidance, authFlavor, installCommandFor, scrub, CODE, DEFAULT_TIMEOUT_MS };
|