hilos-agent 0.10.1 → 0.11.2
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/LICENSE +21 -0
- package/README.md +54 -10
- package/bin/hilos-agent.mjs +87 -14
- package/package.json +7 -5
- package/src/acp-session.mjs +16 -1
- package/src/cli.mjs +34 -1
- package/src/codex-mcp-session.mjs +36 -0
- package/src/config.mjs +39 -5
- package/src/conflicts.mjs +156 -0
- package/src/cursor-store.mjs +43 -8
- package/src/handler.mjs +1197 -127
- package/src/iterate-claim-recovery.mjs +574 -0
- package/src/local-harness-events.mjs +370 -0
- package/src/local-harness-transport.mjs +157 -0
- package/src/mcp.mjs +411 -34
- package/src/model-resolve.mjs +3 -1
- package/src/opencode-session.mjs +22 -2
- package/src/progress-emitter.mjs +1 -1
- package/src/queue.mjs +70 -3
- package/src/redact.mjs +360 -7
- package/src/run-inputs.mjs +211 -0
- package/src/run.mjs +462 -76
- package/src/thread-pr.mjs +26 -4
- package/src/webmcp-bridge.mjs +77 -4
package/src/mcp.mjs
CHANGED
|
@@ -1,7 +1,27 @@
|
|
|
1
|
-
//
|
|
2
|
-
//
|
|
1
|
+
// Small MCP-over-HTTP client for the local daemon. It speaks hilos's current
|
|
2
|
+
// stateless protocol when the server advertises it and falls back to the bare
|
|
3
|
+
// legacy request shape for older servers. No runtime dependency is required.
|
|
4
|
+
import { readFileSync } from "node:fs";
|
|
3
5
|
import os from "node:os";
|
|
4
6
|
|
|
7
|
+
export const MCP_PROTOCOL_VERSION = "2026-07-28";
|
|
8
|
+
const META_PROTOCOL_VERSION = "io.modelcontextprotocol/protocolVersion";
|
|
9
|
+
const META_CLIENT_CAPABILITIES = "io.modelcontextprotocol/clientCapabilities";
|
|
10
|
+
const META_CLIENT_INFO = "io.modelcontextprotocol/clientInfo";
|
|
11
|
+
const DEFAULT_TIMEOUT_MS = 35_000;
|
|
12
|
+
const DISCOVERY_TIMEOUT_MS = 10_000;
|
|
13
|
+
const RETRY_DELAY_MS = 250;
|
|
14
|
+
|
|
15
|
+
function installedVersion() {
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(readFileSync(new URL("../package.json", import.meta.url), "utf8")).version;
|
|
18
|
+
} catch {
|
|
19
|
+
return "unknown";
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
const CLIENT_INFO = { name: "hilos-agent", version: installedVersion() };
|
|
24
|
+
|
|
5
25
|
// Best-effort host label so hilos can show "whose machine" this daemon runs on.
|
|
6
26
|
export const DAEMON_CLIENT = (() => {
|
|
7
27
|
try {
|
|
@@ -11,36 +31,379 @@ export const DAEMON_CLIENT = (() => {
|
|
|
11
31
|
}
|
|
12
32
|
})();
|
|
13
33
|
|
|
14
|
-
|
|
15
|
-
|
|
34
|
+
class McpRequestError extends Error {
|
|
35
|
+
constructor(message, details = {}) {
|
|
36
|
+
super(message);
|
|
37
|
+
this.name = "McpRequestError";
|
|
38
|
+
Object.assign(this, details);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
16
41
|
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
42
|
+
// An authenticated daemon can be told that its credential is permanently no
|
|
43
|
+
// longer usable (revoked/deleted agent, rotated token, etc.). Keep that signal
|
|
44
|
+
// distinct from an ordinary auth lookup failure so the poll loop can stop
|
|
45
|
+
// instead of retrying forever, without broadening every 401/403 into shutdown.
|
|
46
|
+
export class McpAuthStopError extends McpRequestError {
|
|
47
|
+
constructor(message, details = {}) {
|
|
48
|
+
super(message, details);
|
|
49
|
+
this.name = "McpAuthStopError";
|
|
50
|
+
this.stop = true;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function headerValue(value) {
|
|
55
|
+
const text = String(value);
|
|
56
|
+
return /^[\x20-\x7e]+$/.test(text)
|
|
57
|
+
? text
|
|
58
|
+
: `=?base64?${Buffer.from(text, "utf8").toString("base64")}?=`;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function namedParam(method) {
|
|
62
|
+
if (method === "tools/call" || method === "prompts/get") return "name";
|
|
63
|
+
if (method === "resources/read") return "uri";
|
|
64
|
+
if (["tasks/get", "tasks/update", "tasks/cancel"].includes(method)) return "taskId";
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function requestSignal(parentSignal, timeoutMs, stopSignal) {
|
|
69
|
+
const controller = new AbortController();
|
|
70
|
+
const abortFromParent = () => controller.abort(parentSignal?.reason);
|
|
71
|
+
const abortFromStop = () => controller.abort(stopSignal?.reason);
|
|
72
|
+
if (stopSignal?.aborted) abortFromStop();
|
|
73
|
+
else if (parentSignal?.aborted) abortFromParent();
|
|
74
|
+
else {
|
|
75
|
+
parentSignal?.addEventListener("abort", abortFromParent, { once: true });
|
|
76
|
+
stopSignal?.addEventListener("abort", abortFromStop, { once: true });
|
|
77
|
+
}
|
|
78
|
+
const timer = setTimeout(() => controller.abort(new Error("request timed out")), timeoutMs);
|
|
79
|
+
timer.unref?.();
|
|
80
|
+
return {
|
|
81
|
+
signal: controller.signal,
|
|
82
|
+
cleanup() {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
parentSignal?.removeEventListener("abort", abortFromParent);
|
|
85
|
+
stopSignal?.removeEventListener("abort", abortFromStop);
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function combinedSignal(...signals) {
|
|
91
|
+
const active = signals.filter(Boolean);
|
|
92
|
+
if (active.length === 0) return { signal: undefined, cleanup() {} };
|
|
93
|
+
const controller = new AbortController();
|
|
94
|
+
const listeners = new Map();
|
|
95
|
+
for (const source of active) {
|
|
96
|
+
const abort = () => controller.abort(source.reason);
|
|
97
|
+
listeners.set(source, abort);
|
|
98
|
+
if (source.aborted) {
|
|
99
|
+
abort();
|
|
100
|
+
break;
|
|
101
|
+
}
|
|
102
|
+
source.addEventListener("abort", abort, { once: true });
|
|
103
|
+
}
|
|
104
|
+
return {
|
|
105
|
+
signal: controller.signal,
|
|
106
|
+
cleanup() {
|
|
107
|
+
for (const [source, abort] of listeners) source.removeEventListener("abort", abort);
|
|
108
|
+
},
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function retryable(error) {
|
|
113
|
+
if (error instanceof McpAuthStopError || error?.abortedByCaller || error?.retryExhausted) {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
if (typeof error?.status === "number") {
|
|
117
|
+
return error.status === 408 || error.status === 429 || error.status >= 500;
|
|
118
|
+
}
|
|
119
|
+
return error?.code !== -32600 && error?.code !== -32601 && error?.code !== -32602;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function callerCanceled(cause) {
|
|
123
|
+
return new McpRequestError("hilos request canceled", {
|
|
124
|
+
cause,
|
|
125
|
+
abortedByCaller: true,
|
|
126
|
+
});
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const defaultDelay = (ms, signal) => new Promise((resolve, reject) => {
|
|
130
|
+
if (signal?.aborted) {
|
|
131
|
+
reject(callerCanceled(signal.reason));
|
|
132
|
+
return;
|
|
133
|
+
}
|
|
134
|
+
const onAbort = () => {
|
|
135
|
+
clearTimeout(timer);
|
|
136
|
+
reject(callerCanceled(signal.reason));
|
|
137
|
+
};
|
|
138
|
+
const timer = setTimeout(() => {
|
|
139
|
+
signal?.removeEventListener("abort", onAbort);
|
|
140
|
+
resolve();
|
|
141
|
+
}, ms);
|
|
142
|
+
timer.unref?.();
|
|
143
|
+
signal?.addEventListener("abort", onAbort, { once: true });
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
function retryAfterMs(response) {
|
|
147
|
+
const raw = response?.headers?.get?.("retry-after");
|
|
148
|
+
if (!raw) return 0;
|
|
149
|
+
const seconds = Number(raw);
|
|
150
|
+
if (Number.isFinite(seconds) && seconds >= 0) return Math.ceil(seconds * 1000);
|
|
151
|
+
const at = Date.parse(raw);
|
|
152
|
+
return Number.isFinite(at) ? Math.max(0, at - Date.now()) : 0;
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
async function abortableDelay(delay, ms, signal) {
|
|
156
|
+
if (delay === defaultDelay) return defaultDelay(ms, signal);
|
|
157
|
+
if (!signal) return delay(ms);
|
|
158
|
+
if (signal.aborted) throw callerCanceled(signal.reason);
|
|
159
|
+
let onAbort;
|
|
160
|
+
const canceled = new Promise((_resolve, reject) => {
|
|
161
|
+
onAbort = () => reject(callerCanceled(signal.reason));
|
|
162
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
163
|
+
});
|
|
164
|
+
try {
|
|
165
|
+
await Promise.race([Promise.resolve().then(() => delay(ms)), canceled]);
|
|
166
|
+
} finally {
|
|
167
|
+
signal.removeEventListener("abort", onAbort);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
function assertJsonRpcResponse(json, expectedId) {
|
|
172
|
+
const record = json && typeof json === "object" && !Array.isArray(json) ? json : null;
|
|
173
|
+
const hasResult = record && Object.prototype.hasOwnProperty.call(record, "result");
|
|
174
|
+
const hasError = record && Object.prototype.hasOwnProperty.call(record, "error");
|
|
175
|
+
const error = record?.error;
|
|
176
|
+
if (
|
|
177
|
+
!record ||
|
|
178
|
+
record.jsonrpc !== "2.0" ||
|
|
179
|
+
record.id !== expectedId ||
|
|
180
|
+
Boolean(hasResult) === Boolean(hasError) ||
|
|
181
|
+
(hasError && (
|
|
182
|
+
!error ||
|
|
183
|
+
typeof error !== "object" ||
|
|
184
|
+
Array.isArray(error) ||
|
|
185
|
+
!Number.isInteger(error.code) ||
|
|
186
|
+
typeof error.message !== "string"
|
|
187
|
+
))
|
|
188
|
+
) {
|
|
189
|
+
throw new McpRequestError("hilos returned an invalid JSON-RPC response", {
|
|
190
|
+
malformedResponse: true,
|
|
27
191
|
});
|
|
192
|
+
}
|
|
193
|
+
return record;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
export function makeClient({
|
|
197
|
+
url,
|
|
198
|
+
token,
|
|
199
|
+
timeoutMs = DEFAULT_TIMEOUT_MS,
|
|
200
|
+
retryDelayMs = RETRY_DELAY_MS,
|
|
201
|
+
delay = defaultDelay,
|
|
202
|
+
fetchImpl = globalThis.fetch,
|
|
203
|
+
}) {
|
|
204
|
+
let id = 0;
|
|
205
|
+
let negotiated = null;
|
|
206
|
+
let terminalError = null;
|
|
207
|
+
const stopController = new AbortController();
|
|
208
|
+
|
|
209
|
+
const latchStop = (error) => {
|
|
210
|
+
if (!terminalError) {
|
|
211
|
+
terminalError = error;
|
|
212
|
+
stopController.abort(error);
|
|
213
|
+
}
|
|
214
|
+
return terminalError;
|
|
215
|
+
};
|
|
216
|
+
|
|
217
|
+
async function request(method, params, options = {}) {
|
|
218
|
+
if (terminalError) throw terminalError;
|
|
219
|
+
const modern = options.protocolVersion === MCP_PROTOCOL_VERSION;
|
|
220
|
+
const requestParams = modern
|
|
221
|
+
? {
|
|
222
|
+
...(params ?? {}),
|
|
223
|
+
_meta: {
|
|
224
|
+
[META_PROTOCOL_VERSION]: MCP_PROTOCOL_VERSION,
|
|
225
|
+
[META_CLIENT_CAPABILITIES]: {},
|
|
226
|
+
[META_CLIENT_INFO]: CLIENT_INFO,
|
|
227
|
+
},
|
|
228
|
+
}
|
|
229
|
+
: params;
|
|
230
|
+
const headers = {
|
|
231
|
+
"content-type": "application/json",
|
|
232
|
+
accept: "application/json, text/event-stream",
|
|
233
|
+
authorization: `Bearer ${token}`,
|
|
234
|
+
"x-hilos-connection-mode": "daemon",
|
|
235
|
+
...(DAEMON_CLIENT ? { "x-hilos-client": DAEMON_CLIENT } : {}),
|
|
236
|
+
...(modern
|
|
237
|
+
? {
|
|
238
|
+
"mcp-protocol-version": MCP_PROTOCOL_VERSION,
|
|
239
|
+
"mcp-method": method,
|
|
240
|
+
}
|
|
241
|
+
: {}),
|
|
242
|
+
};
|
|
243
|
+
const nameField = modern ? namedParam(method) : null;
|
|
244
|
+
if (nameField && typeof requestParams?.[nameField] === "string") {
|
|
245
|
+
headers["mcp-name"] = headerValue(requestParams[nameField]);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
const callerSignal = options.signal;
|
|
249
|
+
if (callerSignal?.aborted) throw callerCanceled(callerSignal.reason);
|
|
250
|
+
const bounded = requestSignal(
|
|
251
|
+
callerSignal,
|
|
252
|
+
options.timeoutMs ?? timeoutMs,
|
|
253
|
+
stopController.signal,
|
|
254
|
+
);
|
|
255
|
+
const requestId = ++id;
|
|
256
|
+
let res;
|
|
257
|
+
let json = null;
|
|
258
|
+
let transportError = null;
|
|
259
|
+
try {
|
|
260
|
+
try {
|
|
261
|
+
res = await fetchImpl(url, {
|
|
262
|
+
method: "POST",
|
|
263
|
+
headers,
|
|
264
|
+
body: JSON.stringify({ jsonrpc: "2.0", id: requestId, method, params: requestParams }),
|
|
265
|
+
signal: bounded.signal,
|
|
266
|
+
});
|
|
267
|
+
} catch (error) {
|
|
268
|
+
transportError = error;
|
|
269
|
+
}
|
|
270
|
+
if (res) {
|
|
271
|
+
try {
|
|
272
|
+
// Keep the request bound alive until the entire response body has
|
|
273
|
+
// arrived. Fetch can resolve as soon as headers land while the body
|
|
274
|
+
// remains stalled indefinitely.
|
|
275
|
+
json = await res.json();
|
|
276
|
+
} catch (error) {
|
|
277
|
+
transportError = error;
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} finally {
|
|
281
|
+
bounded.cleanup();
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
if (terminalError) throw terminalError;
|
|
285
|
+
if (callerSignal?.aborted) throw callerCanceled(transportError || callerSignal.reason);
|
|
286
|
+
if (bounded.signal.aborted) {
|
|
287
|
+
throw new McpRequestError("hilos request failed: request timed out", {
|
|
288
|
+
cause: transportError || bounded.signal.reason,
|
|
289
|
+
});
|
|
290
|
+
}
|
|
291
|
+
if (!res) {
|
|
292
|
+
throw new McpRequestError(
|
|
293
|
+
`hilos request failed: ${transportError?.message || transportError || "no response"}`,
|
|
294
|
+
{ cause: transportError },
|
|
295
|
+
);
|
|
296
|
+
}
|
|
28
297
|
if (!res.ok) {
|
|
29
|
-
const
|
|
298
|
+
const authHint =
|
|
30
299
|
res.status === 401 || res.status === 403
|
|
31
|
-
? "token rejected
|
|
32
|
-
:
|
|
33
|
-
|
|
300
|
+
? "token rejected; generate a fresh one in hilos (Connect)"
|
|
301
|
+
: typeof json?.error?.message === "string"
|
|
302
|
+
? json.error.message
|
|
303
|
+
: `request failed with HTTP ${res.status}`;
|
|
304
|
+
const ErrorType =
|
|
305
|
+
(res.status === 401 || res.status === 403) && json?.stop === true
|
|
306
|
+
? McpAuthStopError
|
|
307
|
+
: McpRequestError;
|
|
308
|
+
const error = new ErrorType(`hilos ${res.status}: ${authHint.slice(0, 200)}`, {
|
|
309
|
+
status: res.status,
|
|
310
|
+
stop: json?.stop === true,
|
|
311
|
+
code: json?.error?.code,
|
|
312
|
+
retryAfterMs: retryAfterMs(res),
|
|
313
|
+
});
|
|
314
|
+
throw error instanceof McpAuthStopError ? latchStop(error) : error;
|
|
34
315
|
}
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
316
|
+
if (transportError) {
|
|
317
|
+
throw new McpRequestError("hilos returned an invalid JSON-RPC response", {
|
|
318
|
+
cause: transportError,
|
|
319
|
+
malformedResponse: true,
|
|
320
|
+
});
|
|
321
|
+
}
|
|
322
|
+
const envelope = assertJsonRpcResponse(json, requestId);
|
|
323
|
+
if (Object.prototype.hasOwnProperty.call(envelope, "error")) {
|
|
324
|
+
throw new McpRequestError(envelope.error.message, {
|
|
325
|
+
code: envelope.error.code,
|
|
326
|
+
data: envelope.error.data,
|
|
327
|
+
});
|
|
328
|
+
}
|
|
329
|
+
return envelope.result;
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
async function waitBeforeRetry(error, options) {
|
|
333
|
+
const waitMs = Math.max(retryDelayMs, Number(error?.retryAfterMs) || 0);
|
|
334
|
+
const linked = combinedSignal(options?.signal, stopController.signal);
|
|
335
|
+
try {
|
|
336
|
+
await abortableDelay(delay, waitMs, linked.signal);
|
|
337
|
+
} catch (delayError) {
|
|
338
|
+
if (terminalError) throw terminalError;
|
|
339
|
+
throw delayError;
|
|
340
|
+
} finally {
|
|
341
|
+
linked.cleanup();
|
|
342
|
+
}
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
async function discover(options = {}) {
|
|
346
|
+
let lastError;
|
|
347
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
348
|
+
try {
|
|
349
|
+
const result = await request("server/discover", undefined, {
|
|
350
|
+
signal: options.signal,
|
|
351
|
+
timeoutMs: Math.min(timeoutMs, DISCOVERY_TIMEOUT_MS),
|
|
352
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
353
|
+
});
|
|
354
|
+
if (!Array.isArray(result?.supportedVersions)) {
|
|
355
|
+
throw new McpRequestError("hilos server/discover returned an invalid result", {
|
|
356
|
+
malformedResponse: true,
|
|
357
|
+
});
|
|
358
|
+
}
|
|
359
|
+
return result.supportedVersions.includes(MCP_PROTOCOL_VERSION) ? MCP_PROTOCOL_VERSION : null;
|
|
360
|
+
} catch (error) {
|
|
361
|
+
if (error?.code === -32601 || error?.status === 404) return null;
|
|
362
|
+
lastError = error;
|
|
363
|
+
if (attempt === 1 || !retryable(error)) {
|
|
364
|
+
// listTools also has one read retry. Mark a discovery error whose own
|
|
365
|
+
// retry is spent so that outer layer cannot accidentally negotiate
|
|
366
|
+
// two more times before surfacing the same failure.
|
|
367
|
+
if (attempt === 1 && error && typeof error === "object") error.retryExhausted = true;
|
|
368
|
+
throw error;
|
|
369
|
+
}
|
|
370
|
+
await waitBeforeRetry(error, options);
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
throw lastError;
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
async function protocol(options = {}) {
|
|
377
|
+
if (!negotiated) {
|
|
378
|
+
negotiated = discover(options).catch((error) => {
|
|
379
|
+
negotiated = null;
|
|
380
|
+
throw error;
|
|
381
|
+
});
|
|
382
|
+
}
|
|
383
|
+
return negotiated;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
async function rpc(method, params, options = {}) {
|
|
387
|
+
if (method === "server/discover") {
|
|
388
|
+
return request(method, params, {
|
|
389
|
+
...options,
|
|
390
|
+
protocolVersion: MCP_PROTOCOL_VERSION,
|
|
391
|
+
});
|
|
392
|
+
}
|
|
393
|
+
const protocolVersion = await protocol(options);
|
|
394
|
+
return request(method, params, { ...options, protocolVersion });
|
|
38
395
|
}
|
|
39
396
|
|
|
40
397
|
// Call a tool and parse its first text block (hilos tools return JSON text).
|
|
41
|
-
async function tool(name, args = {}) {
|
|
42
|
-
const
|
|
43
|
-
const
|
|
398
|
+
async function tool(name, args = {}, options = {}) {
|
|
399
|
+
const result = await rpc("tools/call", { name, arguments: args }, options);
|
|
400
|
+
const first = Array.isArray(result?.content) ? result.content[0] : null;
|
|
401
|
+
if (!first || first.type !== "text" || typeof first.text !== "string") {
|
|
402
|
+
throw new McpRequestError("hilos tools/call returned an invalid result", {
|
|
403
|
+
malformedResponse: true,
|
|
404
|
+
});
|
|
405
|
+
}
|
|
406
|
+
const text = first.text;
|
|
44
407
|
try {
|
|
45
408
|
return JSON.parse(text);
|
|
46
409
|
} catch {
|
|
@@ -48,20 +411,34 @@ export function makeClient({ url, token }) {
|
|
|
48
411
|
}
|
|
49
412
|
}
|
|
50
413
|
|
|
51
|
-
// Full tool objects, schemas included
|
|
52
|
-
//
|
|
53
|
-
//
|
|
54
|
-
async function listTools() {
|
|
414
|
+
// Full tool objects, schemas included. Never turn discovery errors into an
|
|
415
|
+
// empty registry: that would silently downgrade a current daemon until its
|
|
416
|
+
// next restart. One bounded retry handles a transient read failure.
|
|
417
|
+
async function listTools(options = {}) {
|
|
55
418
|
try {
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
419
|
+
const result = await rpc("tools/list", undefined, options);
|
|
420
|
+
if (!Array.isArray(result?.tools) || result.tools.some((tool) => !tool || typeof tool.name !== "string")) {
|
|
421
|
+
throw new McpRequestError("hilos tools/list returned an invalid result", {
|
|
422
|
+
malformedResponse: true,
|
|
423
|
+
});
|
|
424
|
+
}
|
|
425
|
+
return result.tools;
|
|
426
|
+
} catch (error) {
|
|
427
|
+
if (!retryable(error)) throw error;
|
|
428
|
+
await waitBeforeRetry(error, options);
|
|
429
|
+
const result = await rpc("tools/list", undefined, options);
|
|
430
|
+
if (!Array.isArray(result?.tools) || result.tools.some((tool) => !tool || typeof tool.name !== "string")) {
|
|
431
|
+
throw new McpRequestError("hilos tools/list returned an invalid result", {
|
|
432
|
+
malformedResponse: true,
|
|
433
|
+
});
|
|
434
|
+
}
|
|
435
|
+
return result.tools;
|
|
59
436
|
}
|
|
60
437
|
}
|
|
61
438
|
|
|
62
|
-
async function listToolNames() {
|
|
63
|
-
return (await listTools()).map((
|
|
439
|
+
async function listToolNames(options = {}) {
|
|
440
|
+
return (await listTools(options)).map((tool) => tool.name);
|
|
64
441
|
}
|
|
65
442
|
|
|
66
|
-
return { rpc, tool, listTools, listToolNames };
|
|
443
|
+
return { rpc, tool, listTools, listToolNames, stopSignal: stopController.signal };
|
|
67
444
|
}
|
package/src/model-resolve.mjs
CHANGED
|
@@ -16,6 +16,8 @@
|
|
|
16
16
|
// PURE + node-builtins-only; the resolver takes an injected `run` (runCli) so
|
|
17
17
|
// tests drive it with no CLI; a resolution failure NEVER breaks a run ([]).
|
|
18
18
|
|
|
19
|
+
import { commandArgv } from "./argv.mjs";
|
|
20
|
+
|
|
19
21
|
/** Strip ANSI SGR color codes (`--list-models` output is colorized). */
|
|
20
22
|
export function stripAnsi(s) {
|
|
21
23
|
// eslint-disable-next-line no-control-regex
|
|
@@ -253,7 +255,7 @@ export function createModelArgsResolver({ run } = {}) {
|
|
|
253
255
|
// Hand-pinned wins — including codex's short spelling (`-m gpt-5.6-sol`),
|
|
254
256
|
// which is what its own help leads with.
|
|
255
257
|
if (/(^|\s)(--model|-m)(\s|=)/.test(cmd + " ")) return [];
|
|
256
|
-
const bin = cmd
|
|
258
|
+
const bin = commandArgv(cmd)[0] || spec.bin;
|
|
257
259
|
const key = `${bin} ${tier}`;
|
|
258
260
|
if (cache.has(key)) return cache.get(key);
|
|
259
261
|
const r = await run({ cmd: bin, args: spec.args, timeoutMs: 30000, heartbeatMs: 0 });
|
package/src/opencode-session.mjs
CHANGED
|
@@ -29,6 +29,13 @@ function abortError(reason = "cancelled") {
|
|
|
29
29
|
return error;
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
function authorityLostError(reason) {
|
|
33
|
+
const error = new Error("execution authority lost before OpenCode spawn");
|
|
34
|
+
error.code = "HILOS_AUTHORITY_LOST";
|
|
35
|
+
if (reason !== undefined) error.cause = reason;
|
|
36
|
+
return error;
|
|
37
|
+
}
|
|
38
|
+
|
|
32
39
|
function basicAuthorization(username, password) {
|
|
33
40
|
return `Basic ${Buffer.from(`${username}:${password}`, "utf8").toString("base64")}`;
|
|
34
41
|
}
|
|
@@ -220,7 +227,7 @@ function terminateProcessGroup(child, {
|
|
|
220
227
|
* @param {any} [options]
|
|
221
228
|
* @returns {Promise<any>}
|
|
222
229
|
*/
|
|
223
|
-
export function startOpenCodeServer({
|
|
230
|
+
export async function startOpenCodeServer({
|
|
224
231
|
cmd = "opencode",
|
|
225
232
|
cwd,
|
|
226
233
|
env,
|
|
@@ -228,12 +235,21 @@ export function startOpenCodeServer({
|
|
|
228
235
|
password = randomBytes(24).toString("base64url"),
|
|
229
236
|
username = "opencode",
|
|
230
237
|
signal,
|
|
238
|
+
beforeSpawn,
|
|
231
239
|
spawnImpl = spawn,
|
|
232
240
|
kill = process.kill,
|
|
233
241
|
setTimer = setTimeout,
|
|
234
242
|
clearTimer = clearTimeout,
|
|
235
243
|
startupTimeoutMs = STARTUP_TIMEOUT_MS,
|
|
236
244
|
} = {}) {
|
|
245
|
+
if (typeof beforeSpawn === "function") {
|
|
246
|
+
try {
|
|
247
|
+
if ((await beforeSpawn()) === false) throw authorityLostError();
|
|
248
|
+
} catch (error) {
|
|
249
|
+
if (error?.code === "HILOS_AUTHORITY_LOST") throw error;
|
|
250
|
+
throw authorityLostError(error);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
237
253
|
return new Promise((resolve, reject) => {
|
|
238
254
|
if (signal?.aborted) return reject(abortError(signal.reason));
|
|
239
255
|
let child;
|
|
@@ -376,6 +392,7 @@ export async function runOpenCodeHttpSession({
|
|
|
376
392
|
env,
|
|
377
393
|
timeoutMs = 30 * 60_000,
|
|
378
394
|
signal,
|
|
395
|
+
beforeSpawn,
|
|
379
396
|
onData,
|
|
380
397
|
requestPermission,
|
|
381
398
|
getPermissionDecision,
|
|
@@ -470,6 +487,7 @@ export async function runOpenCodeHttpSession({
|
|
|
470
487
|
serverArgs: parsed.serverArgs,
|
|
471
488
|
password: randomPassword(),
|
|
472
489
|
signal: controller.signal,
|
|
490
|
+
beforeSpawn,
|
|
473
491
|
spawnImpl,
|
|
474
492
|
kill,
|
|
475
493
|
setTimer,
|
|
@@ -730,13 +748,15 @@ export async function runOpenCodeHttpSession({
|
|
|
730
748
|
sessionId,
|
|
731
749
|
};
|
|
732
750
|
} catch (error) {
|
|
733
|
-
const
|
|
751
|
+
const authorityLost = error?.code === "HILOS_AUTHORITY_LOST";
|
|
752
|
+
const aborted = abortKind === "cancelled" || authorityLost;
|
|
734
753
|
const timedOut = abortKind === "timeout";
|
|
735
754
|
return {
|
|
736
755
|
status: null,
|
|
737
756
|
stdout: stdout.trimEnd(),
|
|
738
757
|
stderr: stderr.trimEnd(),
|
|
739
758
|
...(aborted ? { aborted: true } : {}),
|
|
759
|
+
...(authorityLost ? { authorityLost: true } : {}),
|
|
740
760
|
...(sessionId ? { sessionId } : {}),
|
|
741
761
|
error:
|
|
742
762
|
error instanceof Error
|
package/src/progress-emitter.mjs
CHANGED
|
@@ -186,7 +186,7 @@ export function imagesNeedReading(vendor, { gated = false } = {}) {
|
|
|
186
186
|
* @returns {string|null}
|
|
187
187
|
*/
|
|
188
188
|
export function attachTarget(codingCmd) {
|
|
189
|
-
const tokens =
|
|
189
|
+
const tokens = commandArgv(codingCmd);
|
|
190
190
|
for (let i = 0; i < tokens.length; i++) {
|
|
191
191
|
if (tokens[i] === "--attach") return tokens[i + 1] && !tokens[i + 1].startsWith("-") ? tokens[i + 1] : "";
|
|
192
192
|
if (tokens[i].startsWith("--attach=")) return tokens[i].slice("--attach=".length);
|