mindwire 0.1.0 → 0.1.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/README.md +183 -0
- package/dist/catalog/index.d.ts +41 -0
- package/dist/client.d.ts +476 -0
- package/dist/daemon-binary.d.ts +13 -0
- package/dist/embedded.d.ts +14 -0
- package/dist/errors.d.ts +46 -0
- package/dist/http.d.ts +97 -0
- package/dist/index.cjs +2327 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.ts +34 -0
- package/dist/index.js +2290 -0
- package/dist/index.js.map +1 -0
- package/dist/run.d.ts +97 -0
- package/dist/sse.d.ts +13 -0
- package/dist/target/container.d.ts +60 -0
- package/dist/target/docker.d.ts +120 -0
- package/dist/target/host.d.ts +81 -0
- package/dist/target/index.d.ts +76 -0
- package/dist/target/oblien.d.ts +112 -0
- package/dist/target/ssh.d.ts +139 -0
- package/dist/types.d.ts +909 -0
- package/dist/version.d.ts +1 -0
- package/package.json +72 -3
package/dist/index.js
ADDED
|
@@ -0,0 +1,2290 @@
|
|
|
1
|
+
// src/errors.ts
|
|
2
|
+
var MindwireError = class extends Error {
|
|
3
|
+
constructor(message, options) {
|
|
4
|
+
super(message, options);
|
|
5
|
+
this.name = "MindwireError";
|
|
6
|
+
}
|
|
7
|
+
};
|
|
8
|
+
var ApiError = class _ApiError extends MindwireError {
|
|
9
|
+
status;
|
|
10
|
+
url;
|
|
11
|
+
method;
|
|
12
|
+
body;
|
|
13
|
+
constructor(args) {
|
|
14
|
+
super(_ApiError.messageFor(args));
|
|
15
|
+
this.name = "ApiError";
|
|
16
|
+
this.status = args.status;
|
|
17
|
+
this.url = args.url;
|
|
18
|
+
this.method = args.method;
|
|
19
|
+
this.body = args.body;
|
|
20
|
+
}
|
|
21
|
+
static messageFor({
|
|
22
|
+
status,
|
|
23
|
+
method,
|
|
24
|
+
url,
|
|
25
|
+
body
|
|
26
|
+
}) {
|
|
27
|
+
const detail = body && typeof body === "object" && "error" in body && typeof body.error === "string" ? body.error : typeof body === "string" && body.trim() !== "" ? body.trim() : "";
|
|
28
|
+
const base = `${method} ${url} failed with ${status}`;
|
|
29
|
+
return detail ? `${base}: ${detail}` : base;
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
var RunFailedError = class extends MindwireError {
|
|
33
|
+
status;
|
|
34
|
+
runId;
|
|
35
|
+
constructor(runId, status, detail) {
|
|
36
|
+
super(detail ? `run ${runId} ${status}: ${detail}` : `run ${runId} ${status}`);
|
|
37
|
+
this.name = "RunFailedError";
|
|
38
|
+
this.runId = runId;
|
|
39
|
+
this.status = status;
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
var TimeoutError = class extends MindwireError {
|
|
43
|
+
method;
|
|
44
|
+
path;
|
|
45
|
+
timeoutMs;
|
|
46
|
+
constructor(method, path, timeoutMs) {
|
|
47
|
+
super(`mindwire: ${method} ${path} timed out after ${timeoutMs}ms`);
|
|
48
|
+
this.name = "TimeoutError";
|
|
49
|
+
this.method = method;
|
|
50
|
+
this.path = path;
|
|
51
|
+
this.timeoutMs = timeoutMs;
|
|
52
|
+
}
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// src/http.ts
|
|
56
|
+
var DEFAULT_TIMEOUT_MS = 12e4;
|
|
57
|
+
function isAbortError(e) {
|
|
58
|
+
return typeof e === "object" && e !== null && e.name === "AbortError";
|
|
59
|
+
}
|
|
60
|
+
var Http = class {
|
|
61
|
+
fetchImpl;
|
|
62
|
+
baseHeaders;
|
|
63
|
+
resolver;
|
|
64
|
+
timeoutMs;
|
|
65
|
+
cached = null;
|
|
66
|
+
pending = null;
|
|
67
|
+
constructor(opts) {
|
|
68
|
+
const f = opts.fetch ?? globalThis.fetch;
|
|
69
|
+
if (!f) {
|
|
70
|
+
throw new MindwireError(
|
|
71
|
+
"mindwire: no global fetch found \u2014 pass a `fetch` implementation in the client options"
|
|
72
|
+
);
|
|
73
|
+
}
|
|
74
|
+
this.fetchImpl = f;
|
|
75
|
+
this.baseHeaders = { ...opts.headers };
|
|
76
|
+
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
77
|
+
if (opts.baseUrl) {
|
|
78
|
+
const base = { baseUrl: opts.baseUrl.replace(/\/+$/, ""), token: opts.token };
|
|
79
|
+
this.resolver = async () => base;
|
|
80
|
+
} else if (opts.resolveBase) {
|
|
81
|
+
const rb = opts.resolveBase;
|
|
82
|
+
this.resolver = async () => {
|
|
83
|
+
const r = await rb();
|
|
84
|
+
return {
|
|
85
|
+
baseUrl: r.baseUrl.replace(/\/+$/, ""),
|
|
86
|
+
token: r.token ?? opts.token,
|
|
87
|
+
getToken: r.getToken,
|
|
88
|
+
headers: r.headers,
|
|
89
|
+
fetch: r.fetch
|
|
90
|
+
};
|
|
91
|
+
};
|
|
92
|
+
} else {
|
|
93
|
+
throw new MindwireError("mindwire: Http requires a baseUrl or a resolveBase");
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* One fetch, with raw network failures wrapped as {@link MindwireError} so callers see a typed
|
|
98
|
+
* SDK error instead of a bare `TypeError: fetch failed`. An abort (the request's own signal, or
|
|
99
|
+
* our timeout controller) is re-thrown untouched — `fetchWithTimeout` classifies it.
|
|
100
|
+
*/
|
|
101
|
+
async fetchOnce(f, url, init, method, path) {
|
|
102
|
+
try {
|
|
103
|
+
return await f(url, init);
|
|
104
|
+
} catch (e) {
|
|
105
|
+
const aborted = init.signal?.aborted ?? false;
|
|
106
|
+
if (aborted || isAbortError(e)) throw e;
|
|
107
|
+
throw new MindwireError(`mindwire: ${method} ${path} \u2014 network request failed`, { cause: e });
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* `fetchOnce` plus a timeout that composes with the caller's `signal`. A hand-rolled controller
|
|
112
|
+
* (not `AbortSignal.timeout`/`AbortSignal.any`, which need Node 20.3+/18.17+) fires after
|
|
113
|
+
* `timeoutMs`; whichever aborts first wins. Timeout → {@link TimeoutError}; the caller's own abort
|
|
114
|
+
* propagates as-is (its `reason`). `timeoutMs<=0` disables the deadline entirely.
|
|
115
|
+
*/
|
|
116
|
+
async fetchWithTimeout(f, url, init, userSignal, method, path) {
|
|
117
|
+
if (!this.timeoutMs || this.timeoutMs <= 0) {
|
|
118
|
+
return this.fetchOnce(f, url, { ...init, ...userSignal ? { signal: userSignal } : {} }, method, path);
|
|
119
|
+
}
|
|
120
|
+
const ctrl = new AbortController();
|
|
121
|
+
const onAbort = () => ctrl.abort(userSignal?.reason);
|
|
122
|
+
if (userSignal) {
|
|
123
|
+
if (userSignal.aborted) ctrl.abort(userSignal.reason);
|
|
124
|
+
else userSignal.addEventListener("abort", onAbort, { once: true });
|
|
125
|
+
}
|
|
126
|
+
let timedOut = false;
|
|
127
|
+
const timer = setTimeout(() => {
|
|
128
|
+
timedOut = true;
|
|
129
|
+
ctrl.abort();
|
|
130
|
+
}, this.timeoutMs);
|
|
131
|
+
try {
|
|
132
|
+
return await this.fetchOnce(f, url, { ...init, signal: ctrl.signal }, method, path);
|
|
133
|
+
} catch (e) {
|
|
134
|
+
if (timedOut) throw new TimeoutError(method, path, this.timeoutMs);
|
|
135
|
+
if (userSignal?.aborted) throw userSignal.reason ?? e;
|
|
136
|
+
throw e;
|
|
137
|
+
} finally {
|
|
138
|
+
clearTimeout(timer);
|
|
139
|
+
if (userSignal) userSignal.removeEventListener("abort", onAbort);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
/**
|
|
143
|
+
* The bearer token for the next request. Prefers the transport's dynamic `getToken` (which
|
|
144
|
+
* caches and mints on demand); falls back to the static token resolved at base time. `force`
|
|
145
|
+
* asks the getter to re-mint — used once on a 401 before retrying.
|
|
146
|
+
*/
|
|
147
|
+
async authToken(base, force) {
|
|
148
|
+
if (base.getToken) {
|
|
149
|
+
const t = await base.getToken(force ? { force: true } : void 0);
|
|
150
|
+
return t ?? base.token;
|
|
151
|
+
}
|
|
152
|
+
return base.token;
|
|
153
|
+
}
|
|
154
|
+
base() {
|
|
155
|
+
if (this.cached) return Promise.resolve(this.cached);
|
|
156
|
+
if (!this.pending) {
|
|
157
|
+
this.pending = this.resolver().then((b) => this.cached = b).catch((err) => {
|
|
158
|
+
this.pending = null;
|
|
159
|
+
throw err;
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
return this.pending;
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Resolve the base eagerly and memoize it — provisions the transport (spawns the embedded daemon,
|
|
166
|
+
* connects the sandbox/SSH/Docker target, …) now instead of on the first request. Idempotent: this
|
|
167
|
+
* awaits the *same* memoized promise the first `request()`/`open()` awaits, so the target's
|
|
168
|
+
* `connect()` fires exactly once. Backs {@link import("./client.js").Mindwire.ensure}.
|
|
169
|
+
*/
|
|
170
|
+
async ready() {
|
|
171
|
+
await this.base();
|
|
172
|
+
}
|
|
173
|
+
url(baseUrl, path, query) {
|
|
174
|
+
const u = new URL(baseUrl + (path.startsWith("/") ? path : `/${path}`));
|
|
175
|
+
if (query) {
|
|
176
|
+
for (const [k, v] of Object.entries(query)) {
|
|
177
|
+
if (v !== void 0) u.searchParams.set(k, String(v));
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
return u.toString();
|
|
181
|
+
}
|
|
182
|
+
headers(token, extra, hasBody = false) {
|
|
183
|
+
const h = { Accept: "application/json", ...this.baseHeaders, ...extra };
|
|
184
|
+
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
185
|
+
if (hasBody) h["Content-Type"] = "application/json";
|
|
186
|
+
return h;
|
|
187
|
+
}
|
|
188
|
+
async request(method, path, init = {}) {
|
|
189
|
+
const base = await this.base();
|
|
190
|
+
const hasBody = init.body !== void 0;
|
|
191
|
+
const url = this.url(base.baseUrl, path, init.query);
|
|
192
|
+
const extra = { ...base.headers, ...init.headers };
|
|
193
|
+
const f = base.fetch ?? this.fetchImpl;
|
|
194
|
+
const send = async (force) => this.fetchWithTimeout(
|
|
195
|
+
f,
|
|
196
|
+
url,
|
|
197
|
+
{
|
|
198
|
+
method,
|
|
199
|
+
headers: this.headers(await this.authToken(base, force), extra, hasBody),
|
|
200
|
+
body: hasBody ? JSON.stringify(init.body) : void 0
|
|
201
|
+
},
|
|
202
|
+
init.signal,
|
|
203
|
+
method,
|
|
204
|
+
path
|
|
205
|
+
);
|
|
206
|
+
let res = await send(false);
|
|
207
|
+
if (res.status === 401 && base.getToken) res = await send(true);
|
|
208
|
+
if (!res.ok) throw await this.toApiError(method, res);
|
|
209
|
+
if (res.status === 204) return void 0;
|
|
210
|
+
const text = await res.text();
|
|
211
|
+
if (text === "") return void 0;
|
|
212
|
+
try {
|
|
213
|
+
return JSON.parse(text);
|
|
214
|
+
} catch (e) {
|
|
215
|
+
throw new MindwireError(`mindwire: ${method} ${path} returned a non-JSON body`, { cause: e });
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
/** Open a streaming response (SSE). Caller owns the body. */
|
|
219
|
+
async open(method, path, init = {}) {
|
|
220
|
+
const base = await this.base();
|
|
221
|
+
const url = this.url(base.baseUrl, path, init.query);
|
|
222
|
+
const extra = { Accept: "text/event-stream", ...base.headers, ...init.headers };
|
|
223
|
+
const f = base.fetch ?? this.fetchImpl;
|
|
224
|
+
const send = async (force) => this.fetchOnce(
|
|
225
|
+
f,
|
|
226
|
+
url,
|
|
227
|
+
{
|
|
228
|
+
method,
|
|
229
|
+
headers: this.headers(await this.authToken(base, force), extra),
|
|
230
|
+
...init.signal ? { signal: init.signal } : {}
|
|
231
|
+
},
|
|
232
|
+
method,
|
|
233
|
+
path
|
|
234
|
+
);
|
|
235
|
+
let res = await send(false);
|
|
236
|
+
if (res.status === 401 && base.getToken) res = await send(true);
|
|
237
|
+
if (!res.ok) throw await this.toApiError(method, res);
|
|
238
|
+
if (!res.body)
|
|
239
|
+
throw new MindwireError(`mindwire: ${method} ${path} returned no response body to stream`);
|
|
240
|
+
return res;
|
|
241
|
+
}
|
|
242
|
+
async toApiError(method, res) {
|
|
243
|
+
let body = "";
|
|
244
|
+
try {
|
|
245
|
+
const text = await res.text();
|
|
246
|
+
try {
|
|
247
|
+
body = text ? JSON.parse(text) : "";
|
|
248
|
+
} catch {
|
|
249
|
+
body = text;
|
|
250
|
+
}
|
|
251
|
+
} catch {
|
|
252
|
+
}
|
|
253
|
+
return new ApiError({ status: res.status, url: res.url, method, body });
|
|
254
|
+
}
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
// src/sse.ts
|
|
258
|
+
async function* readSSE(body, signal) {
|
|
259
|
+
const reader = body.getReader();
|
|
260
|
+
const decoder = new TextDecoder();
|
|
261
|
+
let buffer = "";
|
|
262
|
+
const onAbort = () => void reader.cancel().catch(() => {
|
|
263
|
+
});
|
|
264
|
+
if (signal) {
|
|
265
|
+
if (signal.aborted) {
|
|
266
|
+
await reader.cancel().catch(() => {
|
|
267
|
+
});
|
|
268
|
+
return;
|
|
269
|
+
}
|
|
270
|
+
signal.addEventListener("abort", onAbort, { once: true });
|
|
271
|
+
}
|
|
272
|
+
try {
|
|
273
|
+
for (; ; ) {
|
|
274
|
+
const { done, value } = await reader.read();
|
|
275
|
+
if (done) break;
|
|
276
|
+
buffer += decoder.decode(value, { stream: true });
|
|
277
|
+
let sep;
|
|
278
|
+
while ((sep = indexOfBlankLine(buffer)) !== -1) {
|
|
279
|
+
const rawEvent = buffer.slice(0, sep);
|
|
280
|
+
buffer = buffer.slice(sepEnd(buffer, sep));
|
|
281
|
+
const payload2 = parseEvent(rawEvent);
|
|
282
|
+
if (payload2 !== void 0) yield JSON.parse(payload2);
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
const payload = parseEvent(buffer);
|
|
286
|
+
if (payload !== void 0) yield JSON.parse(payload);
|
|
287
|
+
} finally {
|
|
288
|
+
if (signal) signal.removeEventListener("abort", onAbort);
|
|
289
|
+
reader.releaseLock();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
function indexOfBlankLine(s) {
|
|
293
|
+
const a = s.indexOf("\n\n");
|
|
294
|
+
const b = s.indexOf("\r\n\r\n");
|
|
295
|
+
if (a === -1) return b;
|
|
296
|
+
if (b === -1) return a;
|
|
297
|
+
return Math.min(a, b);
|
|
298
|
+
}
|
|
299
|
+
function sepEnd(s, idx) {
|
|
300
|
+
return s.startsWith("\r\n\r\n", idx) ? idx + 4 : idx + 2;
|
|
301
|
+
}
|
|
302
|
+
function parseEvent(block) {
|
|
303
|
+
const lines = block.split(/\r\n|\n/);
|
|
304
|
+
const data = [];
|
|
305
|
+
for (const line of lines) {
|
|
306
|
+
if (line === "" || line.startsWith(":")) continue;
|
|
307
|
+
const colon = line.indexOf(":");
|
|
308
|
+
const field = colon === -1 ? line : line.slice(0, colon);
|
|
309
|
+
if (field !== "data") continue;
|
|
310
|
+
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
311
|
+
if (value.startsWith(" ")) value = value.slice(1);
|
|
312
|
+
data.push(value);
|
|
313
|
+
}
|
|
314
|
+
if (data.length === 0) return void 0;
|
|
315
|
+
return data.join("\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// src/run.ts
|
|
319
|
+
var TERMINAL = /* @__PURE__ */ new Set(["done", "error", "cancelled"]);
|
|
320
|
+
var Run = class _Run {
|
|
321
|
+
data;
|
|
322
|
+
http;
|
|
323
|
+
constructor(http, data) {
|
|
324
|
+
this.http = http;
|
|
325
|
+
this.data = data;
|
|
326
|
+
}
|
|
327
|
+
get id() {
|
|
328
|
+
return this.data.id;
|
|
329
|
+
}
|
|
330
|
+
get chatId() {
|
|
331
|
+
return this.data.chatId;
|
|
332
|
+
}
|
|
333
|
+
get agent() {
|
|
334
|
+
return this.data.agent;
|
|
335
|
+
}
|
|
336
|
+
get status() {
|
|
337
|
+
return this.data.status;
|
|
338
|
+
}
|
|
339
|
+
/** `"resolve"` on the parent of a global-resolve run; `undefined` for an ordinary turn. */
|
|
340
|
+
get kind() {
|
|
341
|
+
return this.data.kind;
|
|
342
|
+
}
|
|
343
|
+
/** On a child iteration of a resolve run, the id of its parent resolve run; else `undefined`. */
|
|
344
|
+
get parentId() {
|
|
345
|
+
return this.data.parentId;
|
|
346
|
+
}
|
|
347
|
+
/** Parent of a resolve run only: why the loop ended (`"done"` | `"capped"` | `"error"` | …). */
|
|
348
|
+
get stopReason() {
|
|
349
|
+
return this.data.stopReason;
|
|
350
|
+
}
|
|
351
|
+
/** Parent of a resolve run only: how many child turns the loop ran. */
|
|
352
|
+
get iterations() {
|
|
353
|
+
return this.data.iterations;
|
|
354
|
+
}
|
|
355
|
+
/** The latest known run record. */
|
|
356
|
+
get value() {
|
|
357
|
+
return this.data;
|
|
358
|
+
}
|
|
359
|
+
/** Unified SSE event stream: replay buffer, then live events, then close. */
|
|
360
|
+
async *stream(opts = {}) {
|
|
361
|
+
const res = await this.http.open("GET", `/runs/${encodeURIComponent(this.id)}/stream`, {
|
|
362
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
363
|
+
});
|
|
364
|
+
for await (const ev of readSSE(res.body, opts.signal)) {
|
|
365
|
+
if (!opts.includeOpenSentinel && ev.type === "status" && ev.meta?.["stream"] === "open") {
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
yield ev;
|
|
369
|
+
}
|
|
370
|
+
}
|
|
371
|
+
/** `for await (const ev of run)` — sugar for `run.stream()`. */
|
|
372
|
+
[Symbol.asyncIterator]() {
|
|
373
|
+
return this.stream();
|
|
374
|
+
}
|
|
375
|
+
/** Cancel the in-flight turn (kills the underlying agent process). */
|
|
376
|
+
async cancel() {
|
|
377
|
+
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/cancel`);
|
|
378
|
+
}
|
|
379
|
+
/**
|
|
380
|
+
* Answer a mid-turn interaction the turn is waiting on — a permission approval, or an
|
|
381
|
+
* AskUserQuestion / ExitPlanMode reply. Requires the agent's `respond` capability.
|
|
382
|
+
*/
|
|
383
|
+
async respond(input = {}) {
|
|
384
|
+
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/respond`, { body: input });
|
|
385
|
+
}
|
|
386
|
+
/**
|
|
387
|
+
* Steer a follow-up message into the running turn without cancelling it. Requires the agent's
|
|
388
|
+
* `input` capability.
|
|
389
|
+
*/
|
|
390
|
+
async sendInput(text) {
|
|
391
|
+
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/input`, { body: { text } });
|
|
392
|
+
}
|
|
393
|
+
/**
|
|
394
|
+
* Soft-stop the running turn (ask the agent to halt current work) without the hard process kill
|
|
395
|
+
* {@link Run.cancel} does — the turn stays open for a follow-up via {@link Run.sendInput}.
|
|
396
|
+
* Requires the agent's `interrupt` capability.
|
|
397
|
+
*/
|
|
398
|
+
async interrupt() {
|
|
399
|
+
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/interrupt`);
|
|
400
|
+
}
|
|
401
|
+
/**
|
|
402
|
+
* Switch the model of the live turn. An empty/omitted `model` resets the turn to the agent/CLI
|
|
403
|
+
* default. Only meaningful on a persistent (non-bypass) turn; on a one-shot turn it is a
|
|
404
|
+
* best-effort no-op. Requires the agent's `setModel` capability.
|
|
405
|
+
*/
|
|
406
|
+
async setModel(model) {
|
|
407
|
+
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/set-model`, {
|
|
408
|
+
body: { model: model ?? "" }
|
|
409
|
+
});
|
|
410
|
+
}
|
|
411
|
+
/**
|
|
412
|
+
* Switch the permission mode of the live turn (e.g. `default`, `acceptEdits`, `plan`,
|
|
413
|
+
* `bypassPermissions`). Only meaningful on a persistent (non-bypass) turn; on a one-shot turn it
|
|
414
|
+
* is a best-effort no-op. Requires the agent's `setPermissionMode` capability.
|
|
415
|
+
*/
|
|
416
|
+
async setPermissionMode(mode) {
|
|
417
|
+
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/set-permission-mode`, {
|
|
418
|
+
body: { mode }
|
|
419
|
+
});
|
|
420
|
+
}
|
|
421
|
+
/**
|
|
422
|
+
* `GET /runs/{id}/children` — the child iterations of a global-resolve run, oldest→newest, each as
|
|
423
|
+
* its own {@link Run} handle. An ordinary turn (or a resolve that ran a single iteration) returns an
|
|
424
|
+
* empty array. Use it to inspect the run tree a {@link Mindwire.resolve} produced.
|
|
425
|
+
*/
|
|
426
|
+
async children() {
|
|
427
|
+
const data = await this.http.request(
|
|
428
|
+
"GET",
|
|
429
|
+
`/runs/${encodeURIComponent(this.id)}/children`
|
|
430
|
+
);
|
|
431
|
+
return data.map((d) => new _Run(this.http, d));
|
|
432
|
+
}
|
|
433
|
+
/** Re-fetch the run record from the daemon and update this handle. */
|
|
434
|
+
async refresh() {
|
|
435
|
+
this.data = await this.http.request("GET", `/runs/${encodeURIComponent(this.id)}`);
|
|
436
|
+
return this.data;
|
|
437
|
+
}
|
|
438
|
+
/**
|
|
439
|
+
* Consume the event stream to completion. Returns the final run record and the `result`
|
|
440
|
+
* event's summary (if any). Throws {@link RunFailedError} on an `error`/`cancelled` outcome
|
|
441
|
+
* unless `throwOnError` is set to `false`.
|
|
442
|
+
*/
|
|
443
|
+
async wait(opts = {}) {
|
|
444
|
+
let result;
|
|
445
|
+
let streamError;
|
|
446
|
+
for await (const ev of this.stream(opts)) {
|
|
447
|
+
if (ev.type === "result") result = ev.result;
|
|
448
|
+
else if (ev.type === "error") streamError = ev.error;
|
|
449
|
+
}
|
|
450
|
+
const run = await this.refresh();
|
|
451
|
+
if (opts.throwOnError !== false) {
|
|
452
|
+
if (TERMINAL.has(run.status)) {
|
|
453
|
+
if (run.status !== "done") {
|
|
454
|
+
throw new RunFailedError(run.id, run.status, run.error ?? streamError);
|
|
455
|
+
}
|
|
456
|
+
} else {
|
|
457
|
+
throw new RunFailedError(
|
|
458
|
+
run.id,
|
|
459
|
+
run.status,
|
|
460
|
+
streamError ?? "event stream ended before the run reached a terminal state"
|
|
461
|
+
);
|
|
462
|
+
}
|
|
463
|
+
}
|
|
464
|
+
return result !== void 0 ? { run, result } : { run };
|
|
465
|
+
}
|
|
466
|
+
};
|
|
467
|
+
|
|
468
|
+
// src/version.ts
|
|
469
|
+
var SDK_VERSION = "0.1.2" ;
|
|
470
|
+
|
|
471
|
+
// src/daemon-binary.ts
|
|
472
|
+
function supported(platform, arch) {
|
|
473
|
+
if (!["darwin", "linux", "win32"].includes(platform) || !["x64", "arm64"].includes(arch)) {
|
|
474
|
+
throw new MindwireError(`mindwire: no daemon release for ${platform}-${arch}`);
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
function assetName(version, platform, arch) {
|
|
478
|
+
const releasePlatform = platform === "win32" ? "windows" : platform;
|
|
479
|
+
const releaseArch = arch === "x64" ? "amd64" : arch;
|
|
480
|
+
return `mindwired-v${version}-${releasePlatform}-${releaseArch}${platform === "win32" ? ".exe" : ""}`;
|
|
481
|
+
}
|
|
482
|
+
function checksumFor(text, asset) {
|
|
483
|
+
const line = text.split("\n").find((l) => l.trim().endsWith(` ${asset}`) || l.trim().endsWith(` ${asset}`));
|
|
484
|
+
const hash = line?.trim().split(/\s+/)[0];
|
|
485
|
+
return hash && /^[a-f0-9]{64}$/i.test(hash) ? hash.toLowerCase() : void 0;
|
|
486
|
+
}
|
|
487
|
+
async function ensureDaemonBinary(opts = {}) {
|
|
488
|
+
const proc = globalThis;
|
|
489
|
+
const platform = opts.platform ?? proc.process?.platform;
|
|
490
|
+
const arch = opts.arch ?? proc.process?.arch;
|
|
491
|
+
supported(platform ?? "unknown", arch ?? "unknown");
|
|
492
|
+
const daemonPlatform = platform;
|
|
493
|
+
const daemonArch = arch;
|
|
494
|
+
const version = opts.version ?? SDK_VERSION;
|
|
495
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
496
|
+
throw new MindwireError(`mindwire: cannot download daemon for non-release SDK version ${version}`);
|
|
497
|
+
}
|
|
498
|
+
const fs = await import('fs/promises');
|
|
499
|
+
const path = await import('path');
|
|
500
|
+
const os = await import('os');
|
|
501
|
+
const crypto = await import('crypto');
|
|
502
|
+
const home = os.homedir();
|
|
503
|
+
const defaultCache = daemonPlatform === "darwin" ? path.join(home, "Library", "Caches", "mindwire") : daemonPlatform === "win32" ? path.join(proc.process?.env?.LOCALAPPDATA ?? home, "mindwire", "Cache") : path.join(proc.process?.env?.XDG_CACHE_HOME ?? path.join(home, ".cache"), "mindwire");
|
|
504
|
+
const dir = path.join(opts.cacheDir ?? defaultCache, version, `${daemonPlatform}-${daemonArch}`);
|
|
505
|
+
const asset = assetName(version, daemonPlatform, daemonArch);
|
|
506
|
+
const bin = path.join(dir, asset);
|
|
507
|
+
const shaFile = `${bin}.sha256`;
|
|
508
|
+
try {
|
|
509
|
+
const [bytes2, expected2] = await Promise.all([fs.readFile(bin), fs.readFile(shaFile, "utf8")]);
|
|
510
|
+
if (crypto.createHash("sha256").update(bytes2).digest("hex") === expected2.trim()) return bin;
|
|
511
|
+
} catch {
|
|
512
|
+
}
|
|
513
|
+
const base = (opts.releaseBaseUrl ?? proc.process?.env?.MINDWIRE_RELEASE_BASE_URL ?? "https://github.com/oblien/mindwire/releases/download").replace(/\/$/, "");
|
|
514
|
+
const release = `${base}/v${version}`;
|
|
515
|
+
const request = opts.fetch ?? globalThis.fetch;
|
|
516
|
+
if (!request) throw new MindwireError("mindwire: fetch is unavailable; set daemonBin or MINDWIRE_DAEMON");
|
|
517
|
+
const [checksums, binary] = await Promise.all([request(`${release}/checksums.txt`), request(`${release}/${asset}`)]);
|
|
518
|
+
if (!checksums.ok || !binary.ok) {
|
|
519
|
+
if (!opts.releaseBaseUrl && !proc.process?.env?.MINDWIRE_RELEASE_BASE_URL) {
|
|
520
|
+
const latest = await request("https://api.github.com/repos/oblien/mindwire/releases/latest");
|
|
521
|
+
if (latest.ok) {
|
|
522
|
+
const tag = (await latest.json()).tag_name;
|
|
523
|
+
const fallback = typeof tag === "string" ? tag.replace(/^v/, "") : "";
|
|
524
|
+
if (/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(fallback) && fallback !== version) {
|
|
525
|
+
return ensureDaemonBinary({ ...opts, version: fallback });
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
}
|
|
529
|
+
throw new MindwireError(`mindwire: failed to download daemon v${version} for ${daemonPlatform}-${daemonArch}`);
|
|
530
|
+
}
|
|
531
|
+
const expected = checksumFor(await checksums.text(), asset);
|
|
532
|
+
if (!expected) throw new MindwireError(`mindwire: release v${version} has no checksum for ${asset}`);
|
|
533
|
+
const bytes = new Uint8Array(await binary.arrayBuffer());
|
|
534
|
+
const actual = crypto.createHash("sha256").update(bytes).digest("hex");
|
|
535
|
+
if (actual !== expected) throw new MindwireError(`mindwire: checksum verification failed for ${asset}`);
|
|
536
|
+
await fs.mkdir(dir, { recursive: true });
|
|
537
|
+
const temp = `${bin}.${process.pid}.tmp`;
|
|
538
|
+
await fs.writeFile(temp, bytes, { mode: 493 });
|
|
539
|
+
await fs.writeFile(`${shaFile}.${process.pid}.tmp`, `${expected}
|
|
540
|
+
`);
|
|
541
|
+
await fs.rename(temp, bin);
|
|
542
|
+
await fs.rename(`${shaFile}.${process.pid}.tmp`, shaFile);
|
|
543
|
+
if (daemonPlatform !== "win32") await fs.chmod(bin, 493);
|
|
544
|
+
return bin;
|
|
545
|
+
}
|
|
546
|
+
|
|
547
|
+
// src/embedded.ts
|
|
548
|
+
var shared = /* @__PURE__ */ new Map();
|
|
549
|
+
function configKey(opts) {
|
|
550
|
+
return JSON.stringify([opts.cwd ?? "", opts.statePath ?? "", opts.bin ?? ""]);
|
|
551
|
+
}
|
|
552
|
+
function startEmbedded(opts = {}) {
|
|
553
|
+
const key = configKey(opts);
|
|
554
|
+
let daemon = shared.get(key);
|
|
555
|
+
if (!daemon) {
|
|
556
|
+
daemon = spawnDaemon(opts).catch((err) => {
|
|
557
|
+
shared.delete(key);
|
|
558
|
+
throw err;
|
|
559
|
+
});
|
|
560
|
+
shared.set(key, daemon);
|
|
561
|
+
}
|
|
562
|
+
return daemon;
|
|
563
|
+
}
|
|
564
|
+
function serverRuntime() {
|
|
565
|
+
return globalThis;
|
|
566
|
+
}
|
|
567
|
+
function isServerRuntime() {
|
|
568
|
+
const g = serverRuntime();
|
|
569
|
+
return typeof g.process !== "undefined" && !!g.process.versions?.node || typeof g.Bun !== "undefined" || typeof g.Deno !== "undefined";
|
|
570
|
+
}
|
|
571
|
+
async function spawnDaemon(opts) {
|
|
572
|
+
if (!isServerRuntime()) {
|
|
573
|
+
throw new MindwireError(
|
|
574
|
+
"MindWire embedded mode needs a server runtime (Node/Bun/Deno). In the browser or an edge runtime, pass { baseUrl } to connect to a running daemon."
|
|
575
|
+
);
|
|
576
|
+
}
|
|
577
|
+
const { spawn } = await import('child_process');
|
|
578
|
+
const net = await import('net');
|
|
579
|
+
const proc = serverRuntime().process;
|
|
580
|
+
const resolved = await resolveBinary(opts.bin);
|
|
581
|
+
const bin = resolved.bin;
|
|
582
|
+
const port = await freePort(net);
|
|
583
|
+
const baseUrl = `http://127.0.0.1:${port}`;
|
|
584
|
+
const child = spawn(bin, [], {
|
|
585
|
+
env: {
|
|
586
|
+
...proc.env,
|
|
587
|
+
ADDR: `127.0.0.1:${port}`,
|
|
588
|
+
AGENT_CWD: opts.cwd ?? proc.cwd(),
|
|
589
|
+
STATE_PATH: opts.statePath ?? ".mindwire-state.json",
|
|
590
|
+
DAEMON_TOKEN: ""
|
|
591
|
+
},
|
|
592
|
+
stdio: "ignore"
|
|
593
|
+
});
|
|
594
|
+
let spawnError = null;
|
|
595
|
+
child.on("error", (e) => {
|
|
596
|
+
spawnError = e;
|
|
597
|
+
});
|
|
598
|
+
const stop = () => {
|
|
599
|
+
try {
|
|
600
|
+
child.kill();
|
|
601
|
+
} catch {
|
|
602
|
+
}
|
|
603
|
+
};
|
|
604
|
+
proc.once?.("exit", stop);
|
|
605
|
+
proc.once?.("SIGINT", () => {
|
|
606
|
+
stop();
|
|
607
|
+
proc.exit?.(130);
|
|
608
|
+
});
|
|
609
|
+
await waitHealthy(baseUrl, () => spawnError, resolved);
|
|
610
|
+
return { baseUrl, stop };
|
|
611
|
+
}
|
|
612
|
+
async function resolveBinary(explicit) {
|
|
613
|
+
const fs = await import('fs');
|
|
614
|
+
const path = await import('path');
|
|
615
|
+
const proc = serverRuntime().process;
|
|
616
|
+
const ext = proc.platform === "win32" ? ".exe" : "";
|
|
617
|
+
const candidates = [];
|
|
618
|
+
if (explicit) candidates.push(explicit);
|
|
619
|
+
if (proc.env?.MINDWIRE_DAEMON) candidates.push(proc.env.MINDWIRE_DAEMON);
|
|
620
|
+
try {
|
|
621
|
+
const url = await import('url');
|
|
622
|
+
const here = path.dirname(url.fileURLToPath(import.meta.url));
|
|
623
|
+
const named = `mindwired-${proc.platform}-${proc.arch}${ext}`;
|
|
624
|
+
candidates.push(path.join(here, "bin", named), path.join(here, "..", "bin", named));
|
|
625
|
+
} catch {
|
|
626
|
+
}
|
|
627
|
+
for (const c of candidates) {
|
|
628
|
+
try {
|
|
629
|
+
if (fs.existsSync(c)) return { bin: c, found: true, checked: candidates };
|
|
630
|
+
} catch {
|
|
631
|
+
}
|
|
632
|
+
}
|
|
633
|
+
const downloaded = await ensureDaemonBinary({ platform: proc.platform, arch: proc.arch });
|
|
634
|
+
return { bin: downloaded, found: true, checked: [...candidates, downloaded] };
|
|
635
|
+
}
|
|
636
|
+
function daemonStartError(resolved, cause) {
|
|
637
|
+
if (!resolved.found) {
|
|
638
|
+
return [
|
|
639
|
+
"Couldn't start the mindwire daemon: the `mindwired` binary was not found.",
|
|
640
|
+
`Looked in: ${resolved.checked.join(", ")}.`,
|
|
641
|
+
"Fixes:",
|
|
642
|
+
" \u2022 check GitHub Release access so the SDK can download its matching daemon binary; or",
|
|
643
|
+
" \u2022 set MINDWIRE_DAEMON to a `mindwired` binary; or",
|
|
644
|
+
" \u2022 pass { baseUrl } to connect to a daemon you run yourself.",
|
|
645
|
+
" \u2022 Working inside the mindwire monorepo? Run `go build -o /tmp/mindwired ./daemon/cmd/daemon` and set MINDWIRE_DAEMON.",
|
|
646
|
+
`(underlying error: ${cause.message})`
|
|
647
|
+
].join("\n");
|
|
648
|
+
}
|
|
649
|
+
return `Couldn't start the mindwire daemon (${resolved.bin}): ${cause.message}. Set MINDWIRE_DAEMON to the binary path, or pass { baseUrl } to use a running daemon.`;
|
|
650
|
+
}
|
|
651
|
+
function freePort(net) {
|
|
652
|
+
return new Promise((resolve, reject) => {
|
|
653
|
+
const srv = net.createServer();
|
|
654
|
+
srv.on("error", reject);
|
|
655
|
+
srv.listen(0, "127.0.0.1", () => {
|
|
656
|
+
const addr = srv.address();
|
|
657
|
+
const port = addr && typeof addr === "object" ? addr.port : 0;
|
|
658
|
+
srv.close(() => resolve(port));
|
|
659
|
+
});
|
|
660
|
+
});
|
|
661
|
+
}
|
|
662
|
+
async function waitHealthy(baseUrl, getError, resolved, timeoutMs = 15e3) {
|
|
663
|
+
const deadline = Date.now() + timeoutMs;
|
|
664
|
+
while (Date.now() < deadline) {
|
|
665
|
+
const err = getError();
|
|
666
|
+
if (err) {
|
|
667
|
+
throw new MindwireError(daemonStartError(resolved, err), { cause: err });
|
|
668
|
+
}
|
|
669
|
+
try {
|
|
670
|
+
const res = await fetch(`${baseUrl}/healthz`);
|
|
671
|
+
if (res.ok) return;
|
|
672
|
+
} catch {
|
|
673
|
+
}
|
|
674
|
+
await new Promise((r) => setTimeout(r, 150));
|
|
675
|
+
}
|
|
676
|
+
throw new MindwireError("mindwire embedded daemon did not become healthy in time.");
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
// src/target/index.ts
|
|
680
|
+
function emit(spec, e) {
|
|
681
|
+
if (!spec.onLog) return;
|
|
682
|
+
try {
|
|
683
|
+
spec.onLog(e);
|
|
684
|
+
} catch {
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
function local(opts = {}) {
|
|
688
|
+
return {
|
|
689
|
+
name: "local",
|
|
690
|
+
async connect(spec) {
|
|
691
|
+
const d = await startEmbedded(opts);
|
|
692
|
+
emit(spec, { target: "local", phase: "ready", message: `embedded daemon on ${d.baseUrl}` });
|
|
693
|
+
return {
|
|
694
|
+
id: d.baseUrl,
|
|
695
|
+
baseUrl: d.baseUrl,
|
|
696
|
+
...d.token !== void 0 ? { token: d.token } : {},
|
|
697
|
+
// No-op: the embedded daemon is shared (keyed memoization) and reaped on process exit.
|
|
698
|
+
stop: async () => {
|
|
699
|
+
}
|
|
700
|
+
};
|
|
701
|
+
}
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
function remote(baseUrl, opts = {}) {
|
|
705
|
+
return {
|
|
706
|
+
name: "remote",
|
|
707
|
+
async connect(spec) {
|
|
708
|
+
emit(spec, { target: "remote", phase: "skip", message: `using remote daemon at ${baseUrl}` });
|
|
709
|
+
return {
|
|
710
|
+
id: baseUrl,
|
|
711
|
+
baseUrl,
|
|
712
|
+
...opts.token !== void 0 ? { token: opts.token } : {},
|
|
713
|
+
...opts.headers !== void 0 ? { headers: opts.headers } : {},
|
|
714
|
+
...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
|
|
715
|
+
stop: async () => {
|
|
716
|
+
}
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
};
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
// src/client.ts
|
|
723
|
+
var handleByTransport = /* @__PURE__ */ new WeakMap();
|
|
724
|
+
var Mindwire = class _Mindwire {
|
|
725
|
+
http;
|
|
726
|
+
/** The default agent type applied to agent-scoped calls, if set. */
|
|
727
|
+
defaultAgent;
|
|
728
|
+
/** Step-flow auth, scoped to this client's default agent (override per call). */
|
|
729
|
+
auth;
|
|
730
|
+
/** Persistent memory files + saved prompt templates, scoped to this client's default agent. */
|
|
731
|
+
prompts;
|
|
732
|
+
/** Persistent MCP-server config (the config an agent loads every run), scoped to this client's default agent. */
|
|
733
|
+
mcp;
|
|
734
|
+
/** Custom LLM-provider registration (opencode/Codex native config), scoped to this client's default agent. */
|
|
735
|
+
providers;
|
|
736
|
+
/** Daemon-driven notification channels + routing rules (webhook/slack/discord/telegram; per-agent/session/global). */
|
|
737
|
+
notify;
|
|
738
|
+
constructor(opts = {}) {
|
|
739
|
+
const target = opts.target ?? local();
|
|
740
|
+
const spec = { agent: opts.agent, onLog: opts.logger };
|
|
741
|
+
this.http = new Http({
|
|
742
|
+
fetch: opts.fetch,
|
|
743
|
+
headers: opts.headers,
|
|
744
|
+
timeoutMs: opts.requestTimeoutMs,
|
|
745
|
+
resolveBase: () => {
|
|
746
|
+
const h = target.connect(spec);
|
|
747
|
+
handleByTransport.set(this.http, h);
|
|
748
|
+
return h.then((x) => ({
|
|
749
|
+
baseUrl: x.baseUrl,
|
|
750
|
+
token: x.token,
|
|
751
|
+
getToken: x.getToken,
|
|
752
|
+
headers: x.headers,
|
|
753
|
+
fetch: x.fetch
|
|
754
|
+
}));
|
|
755
|
+
}
|
|
756
|
+
});
|
|
757
|
+
this.defaultAgent = opts.agent;
|
|
758
|
+
this.auth = new AuthApi(this);
|
|
759
|
+
this.prompts = new PromptsApi(this);
|
|
760
|
+
this.mcp = new McpApi(this);
|
|
761
|
+
this.providers = new ProvidersApi(this);
|
|
762
|
+
this.notify = new NotifyApi(this);
|
|
763
|
+
}
|
|
764
|
+
/**
|
|
765
|
+
* Provision the destination and await readiness **now**, rather than lazily on the first request —
|
|
766
|
+
* useful to front-load a fresh SSH/Docker/Oblien box (and to stream its {@link EnsureEvent}s to the
|
|
767
|
+
* `logger`) before the first `turn()`. Idempotent and memoized: repeated `ensure()` calls, and the
|
|
768
|
+
* first real request, all await the same provisioning, so the target connects exactly once.
|
|
769
|
+
*/
|
|
770
|
+
async ensure() {
|
|
771
|
+
await this.http.ready();
|
|
772
|
+
}
|
|
773
|
+
/** Return a new client bound to a different default agent (shares the same transport config). */
|
|
774
|
+
withAgent(agent) {
|
|
775
|
+
const clone = Object.create(_Mindwire.prototype);
|
|
776
|
+
clone.http = this.http;
|
|
777
|
+
clone.defaultAgent = agent;
|
|
778
|
+
clone.auth = new AuthApi(clone);
|
|
779
|
+
clone.prompts = new PromptsApi(clone);
|
|
780
|
+
clone.mcp = new McpApi(clone);
|
|
781
|
+
clone.providers = new ProvidersApi(clone);
|
|
782
|
+
clone.notify = new NotifyApi(clone);
|
|
783
|
+
return clone;
|
|
784
|
+
}
|
|
785
|
+
/** The `?agent=` value to send for a scoped call: explicit override → client default → none. */
|
|
786
|
+
agentParam(scoped) {
|
|
787
|
+
const a = scoped?.agent ?? this.defaultAgent;
|
|
788
|
+
return a ? { agent: a } : void 0;
|
|
789
|
+
}
|
|
790
|
+
/**
|
|
791
|
+
* Release target-owned resources by calling the {@link TargetHandle}'s `stop()` — once, even across
|
|
792
|
+
* {@link withAgent} clones that share the transport. For an `ssh`/`docker`/`oblien` target this reaps
|
|
793
|
+
* the box (tear down the tunnel, stop or delete the container/workspace, per `stopOnExit`); a no-op
|
|
794
|
+
* for `local`/`remote` (embedded self-cleans on process exit; remote is not ours to stop). Safe to
|
|
795
|
+
* call before the target has connected (nothing to reap yet).
|
|
796
|
+
*/
|
|
797
|
+
async close() {
|
|
798
|
+
const handle = handleByTransport.get(this.http);
|
|
799
|
+
if (!handle) return;
|
|
800
|
+
handleByTransport.delete(this.http);
|
|
801
|
+
const h = await handle.catch(() => null);
|
|
802
|
+
if (h) await h.stop();
|
|
803
|
+
}
|
|
804
|
+
// ---- health & catalog ----------------------------------------------------
|
|
805
|
+
/** `GET /healthz` — public liveness check. Resolves with the daemon's health payload when it is up. */
|
|
806
|
+
health() {
|
|
807
|
+
return this.http.request("GET", "/healthz");
|
|
808
|
+
}
|
|
809
|
+
/**
|
|
810
|
+
* `GET /stats` — the daemon **process's** resource snapshot (heap in use, memory reserved from the
|
|
811
|
+
* OS, goroutines, GC cycles, cores, platform, uptime). Cheap enough to call on demand — the daemon
|
|
812
|
+
* reads its own Go runtime, not the machine — so a UI can fetch it when a user opens a daemon's page
|
|
813
|
+
* without any background polling. See {@link Stats} for what each field means and doesn't.
|
|
814
|
+
*/
|
|
815
|
+
stats() {
|
|
816
|
+
return this.http.request("GET", "/stats");
|
|
817
|
+
}
|
|
818
|
+
/** `GET /catalog` — every agent this daemon binary supports. */
|
|
819
|
+
catalog() {
|
|
820
|
+
return this.http.request("GET", "/catalog");
|
|
821
|
+
}
|
|
822
|
+
/** `GET /agent` — capabilities + settings schema + auth methods/status for the selected agent. */
|
|
823
|
+
agent(scoped) {
|
|
824
|
+
return this.http.request("GET", "/agent", { query: this.agentParam(scoped) });
|
|
825
|
+
}
|
|
826
|
+
/**
|
|
827
|
+
* `GET /models` — the models the selected agent can run for the configured account. An empty array
|
|
828
|
+
* is valid (no credentials yet / offline). Throws a 400 {@link ApiError} for an agent whose model is
|
|
829
|
+
* free text (check `capabilities.models` first).
|
|
830
|
+
*/
|
|
831
|
+
models(scoped) {
|
|
832
|
+
return this.http.request("GET", "/models", { query: this.agentParam(scoped) });
|
|
833
|
+
}
|
|
834
|
+
/** `GET /doctor` — daemon-level health plus the selected agent's own checks. */
|
|
835
|
+
doctor(scoped) {
|
|
836
|
+
return this.http.request("GET", "/doctor", { query: this.agentParam(scoped) });
|
|
837
|
+
}
|
|
838
|
+
// ---- toolchain setup -----------------------------------------------------
|
|
839
|
+
/** `POST /setup` — start the agent's install toolchain (background; poll {@link setupStatus}). */
|
|
840
|
+
setup(scoped) {
|
|
841
|
+
return this.http.request("POST", "/setup", { query: this.agentParam(scoped) });
|
|
842
|
+
}
|
|
843
|
+
/** `POST /update` — re-run the toolchain, forcing reinstall of installable steps. */
|
|
844
|
+
update(scoped) {
|
|
845
|
+
return this.http.request("POST", "/update", { query: this.agentParam(scoped) });
|
|
846
|
+
}
|
|
847
|
+
/** `GET /setup` — current toolchain install progress. */
|
|
848
|
+
setupStatus(scoped) {
|
|
849
|
+
return this.http.request("GET", "/setup", { query: this.agentParam(scoped) });
|
|
850
|
+
}
|
|
851
|
+
// ---- config --------------------------------------------------------------
|
|
852
|
+
/** `GET /config` — the declared, non-secret settings for the agent. */
|
|
853
|
+
getConfig(scoped) {
|
|
854
|
+
return this.http.request("GET", "/config", {
|
|
855
|
+
query: this.agentParam(scoped)
|
|
856
|
+
});
|
|
857
|
+
}
|
|
858
|
+
/** `PUT /config` — merge recognized (non-secret) setting keys. Unknown keys are ignored server-side. */
|
|
859
|
+
async setConfig(values, scoped) {
|
|
860
|
+
await this.http.request("PUT", "/config", {
|
|
861
|
+
query: this.agentParam(scoped),
|
|
862
|
+
body: values
|
|
863
|
+
});
|
|
864
|
+
}
|
|
865
|
+
// ---- chats & history -----------------------------------------------------
|
|
866
|
+
/** `GET /chats` — recorded chats (newest first), shared across agents. */
|
|
867
|
+
chats() {
|
|
868
|
+
return this.http.request("GET", "/chats");
|
|
869
|
+
}
|
|
870
|
+
/**
|
|
871
|
+
* `PUT /chats/{id}` — rename a chat. The user title wins over the agent's native auto-title in
|
|
872
|
+
* every listing; an empty title clears the rename (reverting to the native/derived title).
|
|
873
|
+
* Returns the updated summary.
|
|
874
|
+
*/
|
|
875
|
+
renameChat(chatId, title) {
|
|
876
|
+
return this.http.request("PUT", `/chats/${encodeURIComponent(chatId)}`, {
|
|
877
|
+
body: { title }
|
|
878
|
+
});
|
|
879
|
+
}
|
|
880
|
+
/**
|
|
881
|
+
* `DELETE /chats/{id}` — a true, irreversible delete: purges ALL of the chat's mindwire
|
|
882
|
+
* bookkeeping and, for every session the chat mapped to, removes that agent's native transcript
|
|
883
|
+
* (the source of truth). Rejects with a 409 error if a turn is live. Native deletion is
|
|
884
|
+
* best-effort per agent; the result reports what was purged vs. failed.
|
|
885
|
+
*/
|
|
886
|
+
deleteChat(chatId) {
|
|
887
|
+
return this.http.request("DELETE", `/chats/${encodeURIComponent(chatId)}`);
|
|
888
|
+
}
|
|
889
|
+
/**
|
|
890
|
+
* `POST /chats/{id}/fork` — clone a chat into a new id (generated when `newChatId` is omitted).
|
|
891
|
+
* The fork shares the source's native session until its first turn, which branches it (natively
|
|
892
|
+
* on Claude via `--fork-session`; a fresh session on agents without native fork). Rejects with a
|
|
893
|
+
* 409 if the source has a live turn, 404 if the source is unknown, 400 if the target id is in
|
|
894
|
+
* use. Returns the new chat's summary.
|
|
895
|
+
*/
|
|
896
|
+
forkChat(chatId, opts = {}) {
|
|
897
|
+
return this.http.request("POST", `/chats/${encodeURIComponent(chatId)}/fork`, {
|
|
898
|
+
body: opts.newChatId ? { newChatId: opts.newChatId } : {}
|
|
899
|
+
});
|
|
900
|
+
}
|
|
901
|
+
/**
|
|
902
|
+
* `GET /chats/{id}/messages` — a chat's transcript (native when the agent supports it, else
|
|
903
|
+
* the recorded fallback). `limit` caps to the newest N; `before` pages older history.
|
|
904
|
+
*/
|
|
905
|
+
messages(chatId, opts = {}) {
|
|
906
|
+
const query = { ...this.agentParam(opts) };
|
|
907
|
+
if (opts.limit !== void 0) query["limit"] = opts.limit;
|
|
908
|
+
if (opts.before !== void 0) query["before"] = opts.before;
|
|
909
|
+
return this.http.request("GET", `/chats/${encodeURIComponent(chatId)}/messages`, {
|
|
910
|
+
query
|
|
911
|
+
});
|
|
912
|
+
}
|
|
913
|
+
/** `GET /chats/{id}/run` — the latest run for a chat (reattach anchor), or `null` if none yet. */
|
|
914
|
+
async latestRun(chatId) {
|
|
915
|
+
const data = await this.http.request(
|
|
916
|
+
"GET",
|
|
917
|
+
`/chats/${encodeURIComponent(chatId)}/run`
|
|
918
|
+
);
|
|
919
|
+
return data ? new Run(this.http, data) : null;
|
|
920
|
+
}
|
|
921
|
+
// ---- turns & runs --------------------------------------------------------
|
|
922
|
+
/**
|
|
923
|
+
* `POST /turns` — start a turn. Returns a {@link Run} handle you can stream, cancel, or await.
|
|
924
|
+
* Rejects with an {@link ApiError} (409) if a turn is already running for the chat.
|
|
925
|
+
*
|
|
926
|
+
* `mode` defaults to `"turn"` — one agent turn that ends when the CLI settles. Pass
|
|
927
|
+
* `mode: "resolve"` (with optional {@link ResolveOptions} `resolve` caps) for a global-resolve run
|
|
928
|
+
* that auto-continues the agent's multi-step work until it's done; {@link Mindwire.resolve} is the
|
|
929
|
+
* clearer entry point for that. The returned {@link Run} is then the parent of the run tree.
|
|
930
|
+
*/
|
|
931
|
+
async turn(input) {
|
|
932
|
+
const body = {
|
|
933
|
+
chatId: input.chatId,
|
|
934
|
+
message: input.message
|
|
935
|
+
};
|
|
936
|
+
if (input.cwd !== void 0) body.cwd = input.cwd;
|
|
937
|
+
if (input.options !== void 0) body.options = input.options;
|
|
938
|
+
if (input.mode !== void 0) body.mode = input.mode;
|
|
939
|
+
if (input.resolve !== void 0) body.resolve = input.resolve;
|
|
940
|
+
const data = await this.http.request("POST", "/turns", {
|
|
941
|
+
query: this.agentParam(input),
|
|
942
|
+
body
|
|
943
|
+
});
|
|
944
|
+
return new Run(this.http, data);
|
|
945
|
+
}
|
|
946
|
+
/**
|
|
947
|
+
* `POST /turns {mode:"resolve"}` — start a **global-resolve** run: instead of returning after one
|
|
948
|
+
* turn, the daemon holds the task open and auto-continues the agent (resuming on continuable stops
|
|
949
|
+
* and probing for completion) until the work is globally resolved, then aggregates one final result.
|
|
950
|
+
*
|
|
951
|
+
* The returned {@link Run} is the **parent** of a run tree: each auto-continued iteration is a child
|
|
952
|
+
* turn ({@link Run.children}) whose events stream onto the parent's topic, delimited by `continuation`
|
|
953
|
+
* boundary events. `run.wait()` resolves once with the aggregated result; `run.stopReason` /
|
|
954
|
+
* `run.iterations` report how the loop ended. Resolve turns run unattended (no mid-turn approvals)
|
|
955
|
+
* and are bounded by {@link ResolveOptions} caps — see the resolve guide. Rejects with an
|
|
956
|
+
* {@link ApiError} (409) if a turn is already running for the chat.
|
|
957
|
+
*/
|
|
958
|
+
async resolve(input) {
|
|
959
|
+
return this.turn({ ...input, mode: "resolve" });
|
|
960
|
+
}
|
|
961
|
+
/** `GET /runs/{id}` — fetch an existing run as a {@link Run} handle. */
|
|
962
|
+
async run(id) {
|
|
963
|
+
const data = await this.http.request("GET", `/runs/${encodeURIComponent(id)}`);
|
|
964
|
+
return new Run(this.http, data);
|
|
965
|
+
}
|
|
966
|
+
/**
|
|
967
|
+
* `POST /chats/{id}/compact` — run an on-demand conversation compaction as a first-class {@link Run}
|
|
968
|
+
* you can stream or await. The agent folds prior context into a summary it carries forward, emitting
|
|
969
|
+
* a `compaction` event on the stream and recording the boundary in history exactly like an
|
|
970
|
+
* auto-compaction. Optional `instructions` focus the continuation summary (Claude's
|
|
971
|
+
* `/compact <instructions>`; agents that don't honor focus still compact). Rejects with an
|
|
972
|
+
* {@link ApiError}: 400 if the agent doesn't support compaction (`capabilities.compactNow`) or the
|
|
973
|
+
* chat has no conversation yet, 409 if a turn is already running for the chat.
|
|
974
|
+
*/
|
|
975
|
+
async compact(chatId, opts = {}) {
|
|
976
|
+
const data = await this.http.request(
|
|
977
|
+
"POST",
|
|
978
|
+
`/chats/${encodeURIComponent(chatId)}/compact`,
|
|
979
|
+
{
|
|
980
|
+
query: this.agentParam(opts),
|
|
981
|
+
body: opts.instructions ? { instructions: opts.instructions } : {}
|
|
982
|
+
}
|
|
983
|
+
);
|
|
984
|
+
return new Run(this.http, data);
|
|
985
|
+
}
|
|
986
|
+
// ---- notifications -------------------------------------------------------
|
|
987
|
+
/** `GET /notify/config` — whether a notification channel is wired (token never returned). */
|
|
988
|
+
getNotifyConfig() {
|
|
989
|
+
return this.http.request("GET", "/notify/config");
|
|
990
|
+
}
|
|
991
|
+
/** `PUT /notify/config` — store the provisioned notification channel (daemon-wide). */
|
|
992
|
+
async setNotifyConfig(input) {
|
|
993
|
+
await this.http.request("PUT", "/notify/config", { body: input });
|
|
994
|
+
}
|
|
995
|
+
/** `GET /notify/stream` — SSE feed of the daemon's notifications (replay, then live). */
|
|
996
|
+
async *notifications(opts = {}) {
|
|
997
|
+
const res = await this.http.open("GET", "/notify/stream", {
|
|
998
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
999
|
+
});
|
|
1000
|
+
yield* readSSE(res.body, opts.signal);
|
|
1001
|
+
}
|
|
1002
|
+
// ---- live resources ------------------------------------------------------
|
|
1003
|
+
/**
|
|
1004
|
+
* `GET /processes/stream` — SSE feed of live per-turn CPU/memory, one {@link ProcessFrame} per tick.
|
|
1005
|
+
* Sampling is **on demand**: the daemon starts measuring only while a client is connected and stops
|
|
1006
|
+
* the instant the last one disconnects, so aborting the `signal` (or ending the loop) tells the
|
|
1007
|
+
* daemon to stop — no background work, no leak. Pass `agent` to filter each frame's samples to one
|
|
1008
|
+
* agent type. Frames are live snapshots (no replay); an empty `samples` array is a valid keep-alive.
|
|
1009
|
+
*/
|
|
1010
|
+
async *processes(opts = {}) {
|
|
1011
|
+
const path = "/processes/stream" + (opts.agent ? `?agent=${encodeURIComponent(opts.agent)}` : "");
|
|
1012
|
+
const res = await this.http.open("GET", path, {
|
|
1013
|
+
...opts.signal ? { signal: opts.signal } : {}
|
|
1014
|
+
});
|
|
1015
|
+
yield* readSSE(res.body, opts.signal);
|
|
1016
|
+
}
|
|
1017
|
+
};
|
|
1018
|
+
var AuthApi = class {
|
|
1019
|
+
constructor(mw) {
|
|
1020
|
+
this.mw = mw;
|
|
1021
|
+
}
|
|
1022
|
+
mw;
|
|
1023
|
+
/** `GET /auth/methods` — the options list to present. */
|
|
1024
|
+
methods(scoped) {
|
|
1025
|
+
return this.mw.http.request("GET", "/auth/methods", {
|
|
1026
|
+
query: this.mw.agentParam(scoped)
|
|
1027
|
+
});
|
|
1028
|
+
}
|
|
1029
|
+
/** `POST /auth/begin` — start one method (may return `{ url, code, fields, pending }`). */
|
|
1030
|
+
begin(method, scoped) {
|
|
1031
|
+
return this.mw.http.request("POST", "/auth/begin", {
|
|
1032
|
+
query: this.mw.agentParam(scoped),
|
|
1033
|
+
body: { method }
|
|
1034
|
+
});
|
|
1035
|
+
}
|
|
1036
|
+
/** `POST /auth/step` — submit fields, or poll an interactive login to completion. */
|
|
1037
|
+
step(input, scoped) {
|
|
1038
|
+
return this.mw.http.request("POST", "/auth/step", {
|
|
1039
|
+
query: this.mw.agentParam(scoped),
|
|
1040
|
+
body: input
|
|
1041
|
+
});
|
|
1042
|
+
}
|
|
1043
|
+
/** `GET /auth/status` — is the agent authenticated, and via which method. */
|
|
1044
|
+
status(scoped) {
|
|
1045
|
+
return this.mw.http.request("GET", "/auth/status", {
|
|
1046
|
+
query: this.mw.agentParam(scoped)
|
|
1047
|
+
});
|
|
1048
|
+
}
|
|
1049
|
+
};
|
|
1050
|
+
var PromptsApi = class {
|
|
1051
|
+
constructor(mw) {
|
|
1052
|
+
this.mw = mw;
|
|
1053
|
+
}
|
|
1054
|
+
mw;
|
|
1055
|
+
query(scoped, extra) {
|
|
1056
|
+
return { ...this.mw.agentParam(scoped), ...extra };
|
|
1057
|
+
}
|
|
1058
|
+
/**
|
|
1059
|
+
* `GET /memory` — the agent's memory file at every supported scope (project + user for both
|
|
1060
|
+
* Claude and Codex). Each entry carries the resolved `path` and `exists`; `content` is `""` for
|
|
1061
|
+
* an absent file.
|
|
1062
|
+
*/
|
|
1063
|
+
memory(opts = {}) {
|
|
1064
|
+
return this.mw.http.request("GET", "/memory", {
|
|
1065
|
+
query: this.query(opts, { dir: opts.dir })
|
|
1066
|
+
});
|
|
1067
|
+
}
|
|
1068
|
+
/** `PUT /memory` — write the memory file at `scope`. Returns the resulting {@link MemoryDoc}. */
|
|
1069
|
+
setMemory(input, opts = {}) {
|
|
1070
|
+
return this.mw.http.request("PUT", "/memory", {
|
|
1071
|
+
query: this.query(opts, { dir: opts.dir }),
|
|
1072
|
+
body: input
|
|
1073
|
+
});
|
|
1074
|
+
}
|
|
1075
|
+
/**
|
|
1076
|
+
* `DELETE /memory` — remove the memory file at `scope` (defaults to `user`). Returns the resulting
|
|
1077
|
+
* {@link MemoryDoc} (`exists: false` at the resolved path). Idempotent: deleting an absent file still
|
|
1078
|
+
* succeeds.
|
|
1079
|
+
*/
|
|
1080
|
+
deleteMemory(opts = {}) {
|
|
1081
|
+
return this.mw.http.request("DELETE", "/memory", {
|
|
1082
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1083
|
+
});
|
|
1084
|
+
}
|
|
1085
|
+
/**
|
|
1086
|
+
* `GET /prompts` — saved prompt templates across every supported scope (Claude: project + user;
|
|
1087
|
+
* Codex: user only). `content` is omitted here; fetch it with {@link get}. A missing project
|
|
1088
|
+
* directory yields an empty list for that scope rather than an error.
|
|
1089
|
+
*/
|
|
1090
|
+
list(opts = {}) {
|
|
1091
|
+
return this.mw.http.request("GET", "/prompts", {
|
|
1092
|
+
query: this.query(opts, { dir: opts.dir })
|
|
1093
|
+
});
|
|
1094
|
+
}
|
|
1095
|
+
/** `GET /prompts/{name}` — one template's full body. Rejects with a 404 `ApiError` if absent. */
|
|
1096
|
+
get(name, opts = {}) {
|
|
1097
|
+
return this.mw.http.request("GET", `/prompts/${encodeURIComponent(name)}`, {
|
|
1098
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1099
|
+
});
|
|
1100
|
+
}
|
|
1101
|
+
/** `PUT /prompts/{name}` — create or overwrite a template. Returns the resulting {@link PromptTemplate}. */
|
|
1102
|
+
set(name, content, opts = {}) {
|
|
1103
|
+
return this.mw.http.request("PUT", `/prompts/${encodeURIComponent(name)}`, {
|
|
1104
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir }),
|
|
1105
|
+
body: { content }
|
|
1106
|
+
});
|
|
1107
|
+
}
|
|
1108
|
+
/**
|
|
1109
|
+
* `DELETE /prompts/{name}` — remove one template at `scope` (defaults to `user`). Idempotent:
|
|
1110
|
+
* deleting an absent template still succeeds. A traversal name rejects with a 400 `ApiError`.
|
|
1111
|
+
*/
|
|
1112
|
+
async delete(name, opts = {}) {
|
|
1113
|
+
await this.mw.http.request("DELETE", `/prompts/${encodeURIComponent(name)}`, {
|
|
1114
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1115
|
+
});
|
|
1116
|
+
}
|
|
1117
|
+
/**
|
|
1118
|
+
* `GET /subagents` — persistent subagent definitions (Claude `.claude/agents/*.md`) across every
|
|
1119
|
+
* supported scope. `content` is omitted here (fetch it with {@link subagent}); `meta` is the parsed
|
|
1120
|
+
* frontmatter view. Rejects with a 400 `ApiError` on an agent without the subagent-definition module.
|
|
1121
|
+
* Distinct from a turn's per-turn `subagents` passthrough — this is the on-disk definition store.
|
|
1122
|
+
*/
|
|
1123
|
+
subagents(opts = {}) {
|
|
1124
|
+
return this.mw.http.request("GET", "/subagents", {
|
|
1125
|
+
query: this.query(opts, { dir: opts.dir })
|
|
1126
|
+
});
|
|
1127
|
+
}
|
|
1128
|
+
/** `GET /subagents/{name}` — one definition's raw body + parsed meta. 404 `ApiError` if absent. */
|
|
1129
|
+
subagent(name, opts = {}) {
|
|
1130
|
+
return this.mw.http.request("GET", `/subagents/${encodeURIComponent(name)}`, {
|
|
1131
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1132
|
+
});
|
|
1133
|
+
}
|
|
1134
|
+
/** `PUT /subagents/{name}` — create or overwrite a definition (raw content is canonical). Returns it. */
|
|
1135
|
+
setSubagent(name, content, opts = {}) {
|
|
1136
|
+
return this.mw.http.request("PUT", `/subagents/${encodeURIComponent(name)}`, {
|
|
1137
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir }),
|
|
1138
|
+
body: { content }
|
|
1139
|
+
});
|
|
1140
|
+
}
|
|
1141
|
+
/**
|
|
1142
|
+
* `DELETE /subagents/{name}` — remove one definition at `scope` (defaults to `user`). Idempotent:
|
|
1143
|
+
* deleting an absent definition still succeeds. A traversal name rejects with a 400 `ApiError`.
|
|
1144
|
+
*/
|
|
1145
|
+
async deleteSubagent(name, opts = {}) {
|
|
1146
|
+
await this.mw.http.request("DELETE", `/subagents/${encodeURIComponent(name)}`, {
|
|
1147
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
};
|
|
1151
|
+
var McpApi = class {
|
|
1152
|
+
constructor(mw) {
|
|
1153
|
+
this.mw = mw;
|
|
1154
|
+
}
|
|
1155
|
+
mw;
|
|
1156
|
+
query(scoped, extra) {
|
|
1157
|
+
return { ...this.mw.agentParam(scoped), ...extra };
|
|
1158
|
+
}
|
|
1159
|
+
/**
|
|
1160
|
+
* `GET /mcp` — every persistent MCP server across the agent's supported scopes, keyed
|
|
1161
|
+
* `scope → name → server` (Claude: project + user; Codex: user only). A missing config file yields
|
|
1162
|
+
* an empty object for that scope rather than an error.
|
|
1163
|
+
*/
|
|
1164
|
+
list(opts = {}) {
|
|
1165
|
+
return this.mw.http.request("GET", "/mcp", {
|
|
1166
|
+
query: this.query(opts, { dir: opts.dir })
|
|
1167
|
+
});
|
|
1168
|
+
}
|
|
1169
|
+
/** `GET /mcp/{name}` — one server's definition. Rejects with a 404 `ApiError` if it isn't configured. */
|
|
1170
|
+
get(name, opts = {}) {
|
|
1171
|
+
return this.mw.http.request("GET", `/mcp/${encodeURIComponent(name)}`, {
|
|
1172
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1173
|
+
});
|
|
1174
|
+
}
|
|
1175
|
+
/** `PUT /mcp/{name}` — create or overwrite one server. Returns the stored definition. */
|
|
1176
|
+
set(name, server, opts = {}) {
|
|
1177
|
+
return this.mw.http.request("PUT", `/mcp/${encodeURIComponent(name)}`, {
|
|
1178
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir }),
|
|
1179
|
+
body: server
|
|
1180
|
+
});
|
|
1181
|
+
}
|
|
1182
|
+
/** `DELETE /mcp/{name}` — remove one server. Idempotent: deleting an absent server still succeeds. */
|
|
1183
|
+
async delete(name, opts = {}) {
|
|
1184
|
+
await this.mw.http.request("DELETE", `/mcp/${encodeURIComponent(name)}`, {
|
|
1185
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1186
|
+
});
|
|
1187
|
+
}
|
|
1188
|
+
};
|
|
1189
|
+
var ProvidersApi = class {
|
|
1190
|
+
constructor(mw) {
|
|
1191
|
+
this.mw = mw;
|
|
1192
|
+
}
|
|
1193
|
+
mw;
|
|
1194
|
+
query(scoped, extra) {
|
|
1195
|
+
return { ...this.mw.agentParam(scoped), ...extra };
|
|
1196
|
+
}
|
|
1197
|
+
/**
|
|
1198
|
+
* `GET /providers` — every registered custom provider across the agent's supported scopes, keyed
|
|
1199
|
+
* `scope → id → provider` (opencode/Codex: user only). A missing config file yields an empty object for
|
|
1200
|
+
* that scope rather than an error. `hasKey` reports whether a secret is stored; the key is never returned.
|
|
1201
|
+
*/
|
|
1202
|
+
list(opts = {}) {
|
|
1203
|
+
return this.mw.http.request("GET", "/providers", {
|
|
1204
|
+
query: this.query(opts, { dir: opts.dir })
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
/** `GET /providers/{id}` — one provider's definition. Rejects with a 404 `ApiError` if it isn't configured. */
|
|
1208
|
+
get(id, opts = {}) {
|
|
1209
|
+
return this.mw.http.request("GET", `/providers/${encodeURIComponent(id)}`, {
|
|
1210
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1211
|
+
});
|
|
1212
|
+
}
|
|
1213
|
+
/**
|
|
1214
|
+
* `PUT /providers/{id}` — create or overwrite one provider, returning the stored definition (with
|
|
1215
|
+
* `hasKey` and the stored `envVars`). Two write-only secret channels, both optional: `opts.apiKey` is a
|
|
1216
|
+
* single key (custom endpoints, single-key catalog brands); `opts.secrets` is a NAME→VALUE map for a
|
|
1217
|
+
* catalog provider whose entry declares MULTIPLE env vars (e.g. AWS Bedrock). Omitting both leaves any
|
|
1218
|
+
* previously stored secret intact. The path `id` wins over any `id` on the provider value.
|
|
1219
|
+
*/
|
|
1220
|
+
set(id, provider, opts = {}) {
|
|
1221
|
+
return this.mw.http.request("PUT", `/providers/${encodeURIComponent(id)}`, {
|
|
1222
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir }),
|
|
1223
|
+
body: { ...provider, id, apiKey: opts.apiKey, secrets: opts.secrets }
|
|
1224
|
+
});
|
|
1225
|
+
}
|
|
1226
|
+
/** `DELETE /providers/{id}` — remove one provider and clear its stored key. Idempotent. */
|
|
1227
|
+
async delete(id, opts = {}) {
|
|
1228
|
+
await this.mw.http.request("DELETE", `/providers/${encodeURIComponent(id)}`, {
|
|
1229
|
+
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
};
|
|
1233
|
+
var NotifyApi = class {
|
|
1234
|
+
constructor(mw) {
|
|
1235
|
+
this.mw = mw;
|
|
1236
|
+
}
|
|
1237
|
+
mw;
|
|
1238
|
+
/** `GET /notify/channels` — every channel, masked (no secrets). */
|
|
1239
|
+
channels() {
|
|
1240
|
+
return this.mw.http.request("GET", "/notify/channels");
|
|
1241
|
+
}
|
|
1242
|
+
/** `POST /notify/channels` — create a channel (server-assigns the id). Returns it masked. */
|
|
1243
|
+
createChannel(input) {
|
|
1244
|
+
return this.mw.http.request("POST", "/notify/channels", { body: input });
|
|
1245
|
+
}
|
|
1246
|
+
/**
|
|
1247
|
+
* `PUT /notify/channels/{id}` — update a channel, merge-preserving any omitted secret
|
|
1248
|
+
* (`url`/`token`/`secret`). Returns the updated masked channel. 404 `ApiError` if unknown.
|
|
1249
|
+
*/
|
|
1250
|
+
setChannel(id, input) {
|
|
1251
|
+
return this.mw.http.request("PUT", `/notify/channels/${encodeURIComponent(id)}`, {
|
|
1252
|
+
body: input
|
|
1253
|
+
});
|
|
1254
|
+
}
|
|
1255
|
+
/** `DELETE /notify/channels/{id}` — remove a channel. Idempotent. */
|
|
1256
|
+
async deleteChannel(id) {
|
|
1257
|
+
await this.mw.http.request("DELETE", `/notify/channels/${encodeURIComponent(id)}`);
|
|
1258
|
+
}
|
|
1259
|
+
/**
|
|
1260
|
+
* `POST /notify/channels/{id}/test` — deliver a synthetic notification to a channel. A failed
|
|
1261
|
+
* delivery is DATA (`{ ok: false, error }`), not a thrown error; only an unknown id rejects (404).
|
|
1262
|
+
*/
|
|
1263
|
+
testChannel(id) {
|
|
1264
|
+
return this.mw.http.request(
|
|
1265
|
+
"POST",
|
|
1266
|
+
`/notify/channels/${encodeURIComponent(id)}/test`
|
|
1267
|
+
);
|
|
1268
|
+
}
|
|
1269
|
+
/** `GET /notify/rules` — every routing rule. */
|
|
1270
|
+
rules() {
|
|
1271
|
+
return this.mw.http.request("GET", "/notify/rules");
|
|
1272
|
+
}
|
|
1273
|
+
/** `POST /notify/rules` — create a rule (server-assigns the id). Returns it. */
|
|
1274
|
+
createRule(input) {
|
|
1275
|
+
return this.mw.http.request("POST", "/notify/rules", { body: input });
|
|
1276
|
+
}
|
|
1277
|
+
/** `PUT /notify/rules/{id}` — replace a rule. Returns it. 404 `ApiError` if unknown. */
|
|
1278
|
+
setRule(id, input) {
|
|
1279
|
+
return this.mw.http.request("PUT", `/notify/rules/${encodeURIComponent(id)}`, {
|
|
1280
|
+
body: input
|
|
1281
|
+
});
|
|
1282
|
+
}
|
|
1283
|
+
/** `DELETE /notify/rules/{id}` — remove a rule. Idempotent. */
|
|
1284
|
+
async deleteRule(id) {
|
|
1285
|
+
await this.mw.http.request("DELETE", `/notify/rules/${encodeURIComponent(id)}`);
|
|
1286
|
+
}
|
|
1287
|
+
};
|
|
1288
|
+
|
|
1289
|
+
// src/target/host.ts
|
|
1290
|
+
function makeEmit(cfg) {
|
|
1291
|
+
const target = cfg.target ?? "sandbox";
|
|
1292
|
+
return (e) => {
|
|
1293
|
+
if (!cfg.onLog) return;
|
|
1294
|
+
try {
|
|
1295
|
+
cfg.onLog({ target, ...e });
|
|
1296
|
+
} catch {
|
|
1297
|
+
}
|
|
1298
|
+
};
|
|
1299
|
+
}
|
|
1300
|
+
var DAEMON_DIR = "/root/.mindwire";
|
|
1301
|
+
var BIN = `${DAEMON_DIR}/mindwired`;
|
|
1302
|
+
var BIN_NEW = `${BIN}.new`;
|
|
1303
|
+
var STATE = `${DAEMON_DIR}/agent-state.json`;
|
|
1304
|
+
var LOG = `${DAEMON_DIR}/daemon.log`;
|
|
1305
|
+
async function ensureDaemon(host, cfg) {
|
|
1306
|
+
const emit2 = makeEmit(cfg);
|
|
1307
|
+
try {
|
|
1308
|
+
await waitHostReady(host);
|
|
1309
|
+
emit2({ phase: "connect", message: "runtime ready" });
|
|
1310
|
+
const desired = cfg.desiredVersion ?? SDK_VERSION;
|
|
1311
|
+
const health = await probeHealth(host, cfg.port);
|
|
1312
|
+
if (health.reachable) {
|
|
1313
|
+
emit2({
|
|
1314
|
+
phase: "probe",
|
|
1315
|
+
message: health.version ? `daemon reachable (v${health.version})` : "daemon reachable (version unknown)",
|
|
1316
|
+
version: health.version
|
|
1317
|
+
});
|
|
1318
|
+
const upToDate = health.version !== void 0 && health.version === desired;
|
|
1319
|
+
if (upToDate || !cfg.autoUpdate) {
|
|
1320
|
+
emit2({
|
|
1321
|
+
phase: "skip",
|
|
1322
|
+
message: upToDate ? `daemon already at v${desired}` : "keeping the running daemon",
|
|
1323
|
+
version: health.version
|
|
1324
|
+
});
|
|
1325
|
+
return;
|
|
1326
|
+
}
|
|
1327
|
+
} else {
|
|
1328
|
+
emit2({ phase: "probe", message: "no daemon reachable; deploying" });
|
|
1329
|
+
}
|
|
1330
|
+
await deploy(host, cfg, emit2);
|
|
1331
|
+
} catch (err) {
|
|
1332
|
+
emit2({ phase: "error", message: "ensure failed", error: err instanceof Error ? err.message : String(err) });
|
|
1333
|
+
throw err;
|
|
1334
|
+
}
|
|
1335
|
+
}
|
|
1336
|
+
async function deploy(host, cfg, emit2) {
|
|
1337
|
+
const arch = await probeArch(host);
|
|
1338
|
+
const desired = cfg.desiredVersion ?? SDK_VERSION;
|
|
1339
|
+
let acquire = "";
|
|
1340
|
+
if (cfg.daemonBin) {
|
|
1341
|
+
const binPath = await resolveLinuxDaemon(cfg.daemonBin, arch);
|
|
1342
|
+
const bytes = await readBytes(binPath);
|
|
1343
|
+
emit2({ phase: "upload", message: `uploading daemon (${arch}, ${bytes.length} bytes)`, arch, bytes: bytes.length });
|
|
1344
|
+
await host.putFile(BIN_NEW, bytes, { mode: "0755" });
|
|
1345
|
+
} else {
|
|
1346
|
+
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(desired)) {
|
|
1347
|
+
throw new MindwireError(`mindwire: cannot download daemon for non-release SDK version ${desired}`);
|
|
1348
|
+
}
|
|
1349
|
+
const asset = `mindwired-v${desired}-linux-${arch === "arm64" ? "arm64" : "amd64"}`;
|
|
1350
|
+
const release = `https://github.com/oblien/mindwire/releases/download/v${desired}`;
|
|
1351
|
+
emit2({ phase: "download", message: `downloading daemon v${desired} on the destination (${arch})`, arch });
|
|
1352
|
+
acquire = [
|
|
1353
|
+
`release=${shellQuote(release)}`,
|
|
1354
|
+
`asset=${shellQuote(asset)}`,
|
|
1355
|
+
"download() {",
|
|
1356
|
+
` expected=$(curl -fsSL "$release/checksums.txt" | awk -v asset="$asset" '$2 == asset { print $1; exit }') || return 1`,
|
|
1357
|
+
' [ -n "$expected" ] || return 1',
|
|
1358
|
+
` curl -fsSL "$release/$asset" -o ${BIN_NEW} || return 1`,
|
|
1359
|
+
` actual=$(sha256sum ${BIN_NEW} | awk '{print $1}')`,
|
|
1360
|
+
' [ "$actual" = "$expected" ] || return 2',
|
|
1361
|
+
"}",
|
|
1362
|
+
"if download; then :; else",
|
|
1363
|
+
' status=$?; [ "$status" -ne 2 ] || { echo "MINDWIRE_FAIL checksum mismatch for $asset"; exit 1; }',
|
|
1364
|
+
` latest=$(curl -fsSL https://api.github.com/repos/oblien/mindwire/releases/latest | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')`,
|
|
1365
|
+
' case "$latest" in v[0-9]*.[0-9]*.[0-9]*) ;; *) echo "MINDWIRE_FAIL no matching or latest release"; exit 1 ;; esac',
|
|
1366
|
+
' release="https://github.com/oblien/mindwire/releases/download/$latest"',
|
|
1367
|
+
` asset="mindwired-$latest-linux-${arch === "arm64" ? "arm64" : "amd64"}"`,
|
|
1368
|
+
' download || { echo "MINDWIRE_FAIL latest release download failed"; exit 1; }',
|
|
1369
|
+
"fi",
|
|
1370
|
+
`chmod +x ${BIN_NEW}`
|
|
1371
|
+
].join("\n");
|
|
1372
|
+
}
|
|
1373
|
+
const script = [
|
|
1374
|
+
"set -e",
|
|
1375
|
+
`mkdir -p ${DAEMON_DIR}`,
|
|
1376
|
+
acquire,
|
|
1377
|
+
// Stop a prior daemon by exact process NAME, never `pkill -f <path>`: this whole script (which
|
|
1378
|
+
// contains `${BIN}` several times) is the argv of the `bash -lc` shell running it, so a full-cmdline
|
|
1379
|
+
// match would SIGTERM our own deploying shell before the daemon ever launches. `-x mindwired` matches
|
|
1380
|
+
// only the daemon's comm (`bash`/`pkill` never match), leaving this shell alive.
|
|
1381
|
+
`pkill -x mindwired 2>/dev/null || true`,
|
|
1382
|
+
"sleep 0.3",
|
|
1383
|
+
`mv -f ${BIN_NEW} ${BIN}`,
|
|
1384
|
+
`chmod +x ${BIN}`,
|
|
1385
|
+
// Detach so the daemon survives this exec's shell exiting. ADDR=":<port>" binds 0.0.0.0.
|
|
1386
|
+
`setsid nohup env ADDR=":${cfg.port}" AGENT_TYPE="${cfg.agent}" AGENT_CWD="${cfg.agentCwd}" STATE_PATH="${STATE}" DAEMON_TOKEN="" ${BIN} > ${LOG} 2>&1 < /dev/null &`,
|
|
1387
|
+
// Health-poll from inside the VM (loopback) and emit a marker — exit codes are unreliable here.
|
|
1388
|
+
`for i in $(seq 1 60); do curl -fsS --max-time 2 http://127.0.0.1:${cfg.port}/healthz >/dev/null 2>&1 && { echo MINDWIRE_READY; exit 0; }; sleep 0.25; done`,
|
|
1389
|
+
"echo MINDWIRE_FAIL",
|
|
1390
|
+
`tail -n 40 ${LOG} 2>/dev/null || true`
|
|
1391
|
+
].join("\n");
|
|
1392
|
+
emit2({ phase: "launch", message: "launching daemon" });
|
|
1393
|
+
const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 45 });
|
|
1394
|
+
const out = res.stdout ?? "";
|
|
1395
|
+
if (!out.includes("MINDWIRE_READY")) {
|
|
1396
|
+
throw new MindwireError(
|
|
1397
|
+
"mindwire: the in-sandbox daemon did not become healthy after deploy.\n" + (out || res.stderr || res.error || "(no output)").trim()
|
|
1398
|
+
);
|
|
1399
|
+
}
|
|
1400
|
+
emit2({ phase: "ready", message: "daemon deployed and healthy" });
|
|
1401
|
+
}
|
|
1402
|
+
async function probeHealth(host, port) {
|
|
1403
|
+
const script = `out=$(curl -fsS --max-time 3 http://127.0.0.1:${port}/healthz 2>/dev/null) && printf '<<MW_H>>%s<<MW_H>>' "$out" || printf '<<MW_H>><<MW_H>>'`;
|
|
1404
|
+
const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 15 });
|
|
1405
|
+
const body = ((res.stdout ?? "").match(/<<MW_H>>([\s\S]*?)<<MW_H>>/)?.[1] ?? "").trim();
|
|
1406
|
+
if (!body) return { reachable: false };
|
|
1407
|
+
try {
|
|
1408
|
+
const j = JSON.parse(body);
|
|
1409
|
+
return { reachable: true, version: typeof j.version === "string" ? j.version : void 0 };
|
|
1410
|
+
} catch {
|
|
1411
|
+
return { reachable: true };
|
|
1412
|
+
}
|
|
1413
|
+
}
|
|
1414
|
+
async function probeArch(host) {
|
|
1415
|
+
const res = await host.exec(["bash", "-lc", `printf '<<ARCH:%s>>' "$(uname -m)"`], { timeoutSeconds: 15 });
|
|
1416
|
+
const raw = ((res.stdout ?? "").match(/<<ARCH:([^>]*)>>/)?.[1] ?? "").trim();
|
|
1417
|
+
return raw === "aarch64" || raw === "arm64" ? "arm64" : "amd64";
|
|
1418
|
+
}
|
|
1419
|
+
async function waitHostReady(host, timeoutMs = 6e4) {
|
|
1420
|
+
const deadline = Date.now() + timeoutMs;
|
|
1421
|
+
let lastErr = null;
|
|
1422
|
+
while (Date.now() < deadline) {
|
|
1423
|
+
try {
|
|
1424
|
+
const r = await host.exec(["bash", "-lc", "echo mw_ready"], { timeoutSeconds: 10 });
|
|
1425
|
+
if ((r.stdout ?? "").includes("mw_ready")) return;
|
|
1426
|
+
} catch (e) {
|
|
1427
|
+
lastErr = e;
|
|
1428
|
+
}
|
|
1429
|
+
await sleep(1e3);
|
|
1430
|
+
}
|
|
1431
|
+
throw new MindwireError(
|
|
1432
|
+
"mindwire: the sandbox runtime did not become ready in time.",
|
|
1433
|
+
lastErr ? { cause: lastErr } : void 0
|
|
1434
|
+
);
|
|
1435
|
+
}
|
|
1436
|
+
async function resolveLinuxDaemon(explicit, arch) {
|
|
1437
|
+
const fs = await import('fs');
|
|
1438
|
+
if (explicit) {
|
|
1439
|
+
if (!fs.existsSync(explicit)) {
|
|
1440
|
+
throw new MindwireError(`mindwire: sandbox daemonBin not found at ${explicit}`);
|
|
1441
|
+
}
|
|
1442
|
+
return explicit;
|
|
1443
|
+
}
|
|
1444
|
+
return ensureDaemonBinary({ platform: "linux", arch: arch === "arm64" ? "arm64" : "x64" });
|
|
1445
|
+
}
|
|
1446
|
+
async function readBytes(p) {
|
|
1447
|
+
const fs = await import('fs/promises');
|
|
1448
|
+
return fs.readFile(p);
|
|
1449
|
+
}
|
|
1450
|
+
function sleep(ms) {
|
|
1451
|
+
return new Promise((r) => setTimeout(r, ms));
|
|
1452
|
+
}
|
|
1453
|
+
function shellQuote(value) {
|
|
1454
|
+
return `'${value.replace(/'/g, "'\\''")}'`;
|
|
1455
|
+
}
|
|
1456
|
+
|
|
1457
|
+
// src/target/container.ts
|
|
1458
|
+
async function provisionContainer(host, cfg, onLog) {
|
|
1459
|
+
const target = cfg.target ?? "container";
|
|
1460
|
+
const emit2 = (e) => {
|
|
1461
|
+
if (!onLog) return;
|
|
1462
|
+
try {
|
|
1463
|
+
onLog({ target, ...e });
|
|
1464
|
+
} catch {
|
|
1465
|
+
}
|
|
1466
|
+
};
|
|
1467
|
+
await ensureDockerReady(host, cfg.install ?? "never", emit2);
|
|
1468
|
+
const { containerId, hostPort, created } = await runContainer(host, cfg, emit2);
|
|
1469
|
+
await ensureDaemon(new ContainerHost(host, containerId), {
|
|
1470
|
+
port: cfg.daemonPort,
|
|
1471
|
+
agent: cfg.agent,
|
|
1472
|
+
agentCwd: cfg.agentCwd,
|
|
1473
|
+
daemonBin: cfg.daemonBin,
|
|
1474
|
+
autoUpdate: cfg.autoUpdate,
|
|
1475
|
+
target,
|
|
1476
|
+
onLog
|
|
1477
|
+
});
|
|
1478
|
+
let stopped = false;
|
|
1479
|
+
const stop = async () => {
|
|
1480
|
+
if (stopped) return;
|
|
1481
|
+
stopped = true;
|
|
1482
|
+
if (!created || !(cfg.stopOnExit ?? true)) return;
|
|
1483
|
+
try {
|
|
1484
|
+
await host.exec(["docker", "rm", "-f", containerId]);
|
|
1485
|
+
} catch {
|
|
1486
|
+
}
|
|
1487
|
+
};
|
|
1488
|
+
return { containerId, hostPort, stop };
|
|
1489
|
+
}
|
|
1490
|
+
var ContainerHost = class {
|
|
1491
|
+
constructor(base, containerId) {
|
|
1492
|
+
this.base = base;
|
|
1493
|
+
this.containerId = containerId;
|
|
1494
|
+
}
|
|
1495
|
+
base;
|
|
1496
|
+
containerId;
|
|
1497
|
+
exec(argv, opts) {
|
|
1498
|
+
return this.base.exec(["docker", "exec", this.containerId, ...argv], opts);
|
|
1499
|
+
}
|
|
1500
|
+
async putFile(path, data, opts) {
|
|
1501
|
+
const slash = path.lastIndexOf("/");
|
|
1502
|
+
const dir = slash > 0 ? path.slice(0, slash) : "/";
|
|
1503
|
+
const base = path.slice(slash + 1);
|
|
1504
|
+
const mode = opts?.mode ?? "0644";
|
|
1505
|
+
const tmp = `/tmp/.mw-${this.containerId.slice(0, 12)}-${base}`;
|
|
1506
|
+
await this.base.exec(["docker", "exec", this.containerId, "mkdir", "-p", dir]);
|
|
1507
|
+
await this.base.putFile(tmp, data, opts);
|
|
1508
|
+
await this.base.exec(["docker", "cp", tmp, `${this.containerId}:${path}`]);
|
|
1509
|
+
await this.base.exec(["docker", "exec", this.containerId, "chmod", mode, path]);
|
|
1510
|
+
await this.base.exec(["rm", "-f", tmp]);
|
|
1511
|
+
}
|
|
1512
|
+
};
|
|
1513
|
+
async function ensureDockerReady(host, install, emit2) {
|
|
1514
|
+
let state = await detectDocker(host);
|
|
1515
|
+
if (state.status === "running") {
|
|
1516
|
+
emit2({ phase: "install", message: `docker present (v${state.version ?? "?"})`, version: state.version });
|
|
1517
|
+
return;
|
|
1518
|
+
}
|
|
1519
|
+
if (state.status === "denied") {
|
|
1520
|
+
throw new MindwireError(
|
|
1521
|
+
"mindwire: connected to the host, but this user can't reach the Docker daemon socket (permission denied). Connect as root, or add the SSH user to the `docker` group."
|
|
1522
|
+
);
|
|
1523
|
+
}
|
|
1524
|
+
if (state.status === "stopped") {
|
|
1525
|
+
emit2({ phase: "install", message: "docker installed but not running; starting it" });
|
|
1526
|
+
await startDocker(host);
|
|
1527
|
+
state = await detectDocker(host);
|
|
1528
|
+
if (state.status === "running") {
|
|
1529
|
+
emit2({ phase: "install", message: `docker started (v${state.version ?? "?"})`, version: state.version });
|
|
1530
|
+
return;
|
|
1531
|
+
}
|
|
1532
|
+
throw new MindwireError("mindwire: Docker is installed on the host but could not be started.");
|
|
1533
|
+
}
|
|
1534
|
+
if (install !== "ifMissing") {
|
|
1535
|
+
throw new MindwireError(
|
|
1536
|
+
'mindwire: Docker is not installed on the remote host, and mindwire won\'t install system packages unless you opt in. Pass docker: { install: "ifMissing" } to have it run the official get.docker.com script (`curl -fsSL https://get.docker.com | sh`) and enable the service \u2014 or install Docker on the host yourself.'
|
|
1537
|
+
);
|
|
1538
|
+
}
|
|
1539
|
+
emit2({ phase: "install", message: "docker not found; installing via get.docker.com" });
|
|
1540
|
+
await installDocker(host);
|
|
1541
|
+
await startDocker(host);
|
|
1542
|
+
state = await detectDocker(host);
|
|
1543
|
+
if (state.status !== "running") {
|
|
1544
|
+
throw new MindwireError(
|
|
1545
|
+
"mindwire: ran the get.docker.com install script, but the Docker daemon did not come up."
|
|
1546
|
+
);
|
|
1547
|
+
}
|
|
1548
|
+
emit2({ phase: "install", message: `docker installed and started (v${state.version ?? "?"})`, version: state.version });
|
|
1549
|
+
}
|
|
1550
|
+
async function detectDocker(host) {
|
|
1551
|
+
const script = [
|
|
1552
|
+
'if ! command -v docker >/dev/null 2>&1; then echo "MW_DOCKER=absent"; exit 0; fi',
|
|
1553
|
+
'if v=$(docker info --format "{{.ServerVersion}}" 2>/dev/null); then echo "MW_DOCKER=running:$v"; exit 0; fi',
|
|
1554
|
+
"err=$(docker info 2>&1 >/dev/null || true)",
|
|
1555
|
+
'case "$err" in',
|
|
1556
|
+
' *ermission*denied*) echo "MW_DOCKER=denied" ;;',
|
|
1557
|
+
' *) echo "MW_DOCKER=stopped" ;;',
|
|
1558
|
+
"esac"
|
|
1559
|
+
].join("\n");
|
|
1560
|
+
const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 30 });
|
|
1561
|
+
const m = (res.stdout ?? "").match(/MW_DOCKER=([a-z]+)(?::([^\n]*))?/);
|
|
1562
|
+
const status = m?.[1] ?? "absent";
|
|
1563
|
+
const version = m?.[2]?.trim() || void 0;
|
|
1564
|
+
return { status, version };
|
|
1565
|
+
}
|
|
1566
|
+
async function installDocker(host) {
|
|
1567
|
+
const res = await host.exec(["bash", "-lc", "curl -fsSL https://get.docker.com | sh"], {
|
|
1568
|
+
timeoutSeconds: 600
|
|
1569
|
+
});
|
|
1570
|
+
if ((res.exitCode ?? 0) !== 0) {
|
|
1571
|
+
throw new MindwireError(
|
|
1572
|
+
"mindwire: the Docker install script (get.docker.com) failed.\n" + (res.stderr || res.stdout || "(no output)").trim()
|
|
1573
|
+
);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
1576
|
+
async function startDocker(host) {
|
|
1577
|
+
await host.exec(
|
|
1578
|
+
["bash", "-lc", "systemctl enable --now docker 2>/dev/null || service docker start 2>/dev/null || true"],
|
|
1579
|
+
{ timeoutSeconds: 60 }
|
|
1580
|
+
);
|
|
1581
|
+
}
|
|
1582
|
+
async function runContainer(host, cfg, emit2) {
|
|
1583
|
+
const port = cfg.daemonPort;
|
|
1584
|
+
if (cfg.container !== void 0) {
|
|
1585
|
+
await host.exec(["docker", "start", cfg.container]);
|
|
1586
|
+
const hostPort2 = await resolveHostPort(host, cfg.container, port);
|
|
1587
|
+
emit2({
|
|
1588
|
+
phase: "provision",
|
|
1589
|
+
message: `attached to container ${cfg.container}; daemon published on host port ${hostPort2}`
|
|
1590
|
+
});
|
|
1591
|
+
return { containerId: cfg.container, hostPort: hostPort2, created: false };
|
|
1592
|
+
}
|
|
1593
|
+
if (!cfg.image) {
|
|
1594
|
+
throw new MindwireError(
|
|
1595
|
+
"mindwire: running the daemon in a container needs an image (docker.image) to create one, or an existing container (docker.container) to attach to."
|
|
1596
|
+
);
|
|
1597
|
+
}
|
|
1598
|
+
const runArgs = ["docker", "run", "-d"];
|
|
1599
|
+
if (cfg.name) runArgs.push("--name", cfg.name);
|
|
1600
|
+
runArgs.push("-p", `127.0.0.1:0:${port}`);
|
|
1601
|
+
if (cfg.createArgs?.length) runArgs.push(...cfg.createArgs);
|
|
1602
|
+
runArgs.push(cfg.image, "sleep", "infinity");
|
|
1603
|
+
const res = await host.exec(runArgs, { timeoutSeconds: 120 });
|
|
1604
|
+
const containerId = (res.stdout ?? "").trim().split(/\s+/).pop() ?? "";
|
|
1605
|
+
if (!containerId || (res.exitCode ?? 0) !== 0) {
|
|
1606
|
+
throw new MindwireError(
|
|
1607
|
+
"mindwire: `docker run` did not start a container.\n" + (res.stderr || res.stdout || "(no output)").trim()
|
|
1608
|
+
);
|
|
1609
|
+
}
|
|
1610
|
+
const hostPort = await resolveHostPort(host, containerId, port);
|
|
1611
|
+
emit2({
|
|
1612
|
+
phase: "provision",
|
|
1613
|
+
message: `container ${containerId.slice(0, 12)} up; daemon published on host port ${hostPort}`
|
|
1614
|
+
});
|
|
1615
|
+
return { containerId, hostPort, created: true };
|
|
1616
|
+
}
|
|
1617
|
+
async function resolveHostPort(host, containerId, port) {
|
|
1618
|
+
const res = await host.exec(["docker", "port", containerId, `${port}/tcp`]);
|
|
1619
|
+
const line = (res.stdout ?? "").split("\n").map((s) => s.trim()).find(Boolean);
|
|
1620
|
+
const hostPort = line ? Number.parseInt(line.match(/:(\d+)\s*$/)?.[1] ?? "", 10) : NaN;
|
|
1621
|
+
if (!Number.isFinite(hostPort) || hostPort <= 0) {
|
|
1622
|
+
throw new MindwireError(
|
|
1623
|
+
`mindwire: could not resolve the published host port for ${port}/tcp on container ${containerId.slice(0, 12)}. ` + cfgHint(res) + `docker port output: ${JSON.stringify((res.stdout ?? "").trim())}`
|
|
1624
|
+
);
|
|
1625
|
+
}
|
|
1626
|
+
return hostPort;
|
|
1627
|
+
}
|
|
1628
|
+
function cfgHint(res) {
|
|
1629
|
+
return (res.stdout ?? "").trim() === "" ? "Attach to a container started with that port published (e.g. `-p 127.0.0.1:0:<port>`). " : "";
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
// src/target/ssh.ts
|
|
1633
|
+
function ssh(opts) {
|
|
1634
|
+
return { name: "ssh", connect: (spec) => connectSsh(opts, spec) };
|
|
1635
|
+
}
|
|
1636
|
+
var DEFAULT_PORT = 8790;
|
|
1637
|
+
var DEFAULT_AGENT = "claude-code";
|
|
1638
|
+
var DEFAULT_AGENT_CWD = "/root";
|
|
1639
|
+
var SshHost = class {
|
|
1640
|
+
constructor(client) {
|
|
1641
|
+
this.client = client;
|
|
1642
|
+
}
|
|
1643
|
+
client;
|
|
1644
|
+
exec(argv) {
|
|
1645
|
+
const command = toCommandLine(argv);
|
|
1646
|
+
return new Promise((resolve, reject) => {
|
|
1647
|
+
this.client.exec(command, (err, channel) => {
|
|
1648
|
+
if (err) {
|
|
1649
|
+
reject(err);
|
|
1650
|
+
return;
|
|
1651
|
+
}
|
|
1652
|
+
const out = [];
|
|
1653
|
+
const errb = [];
|
|
1654
|
+
let code;
|
|
1655
|
+
channel.on("data", (chunk) => out.push(Buffer.from(chunk)));
|
|
1656
|
+
channel.stderr?.on("data", (chunk) => errb.push(Buffer.from(chunk)));
|
|
1657
|
+
channel.on("close", (c) => {
|
|
1658
|
+
code = typeof c === "number" ? c : void 0;
|
|
1659
|
+
resolve({
|
|
1660
|
+
exitCode: code,
|
|
1661
|
+
stdout: Buffer.concat(out).toString("utf8"),
|
|
1662
|
+
stderr: Buffer.concat(errb).toString("utf8")
|
|
1663
|
+
});
|
|
1664
|
+
});
|
|
1665
|
+
channel.on("error", (e) => reject(e));
|
|
1666
|
+
});
|
|
1667
|
+
});
|
|
1668
|
+
}
|
|
1669
|
+
async putFile(path, data, opts) {
|
|
1670
|
+
const slash = path.lastIndexOf("/");
|
|
1671
|
+
const dir = slash > 0 ? path.slice(0, slash) : "/";
|
|
1672
|
+
await this.exec(["mkdir", "-p", dir]);
|
|
1673
|
+
const sftp = await this.sftp();
|
|
1674
|
+
const mode = parseInt(opts?.mode ?? "0644", 8) || 420;
|
|
1675
|
+
await new Promise((resolve, reject) => {
|
|
1676
|
+
const ws = sftp.createWriteStream(path, { mode });
|
|
1677
|
+
let done = false;
|
|
1678
|
+
const finish = () => {
|
|
1679
|
+
if (!done) {
|
|
1680
|
+
done = true;
|
|
1681
|
+
resolve();
|
|
1682
|
+
}
|
|
1683
|
+
};
|
|
1684
|
+
ws.on("error", (e) => reject(e));
|
|
1685
|
+
ws.on("close", finish);
|
|
1686
|
+
ws.on("finish", finish);
|
|
1687
|
+
ws.end?.(Buffer.from(data));
|
|
1688
|
+
});
|
|
1689
|
+
}
|
|
1690
|
+
sftp() {
|
|
1691
|
+
return new Promise((resolve, reject) => {
|
|
1692
|
+
this.client.sftp((err, sftp) => err ? reject(err) : resolve(sftp));
|
|
1693
|
+
});
|
|
1694
|
+
}
|
|
1695
|
+
};
|
|
1696
|
+
async function connectSsh(opts, spec) {
|
|
1697
|
+
const mod = await importSsh2();
|
|
1698
|
+
const net = await import('net');
|
|
1699
|
+
const client = await openConnection(mod, opts);
|
|
1700
|
+
const daemonPort = opts.daemonPort ?? DEFAULT_PORT;
|
|
1701
|
+
const makeTunnel = (c, port) => createTunnel(net, c, port);
|
|
1702
|
+
const shared2 = {
|
|
1703
|
+
daemonPort,
|
|
1704
|
+
agent: opts.agentType ?? spec.agent ?? DEFAULT_AGENT,
|
|
1705
|
+
agentCwd: opts.agentCwd ?? DEFAULT_AGENT_CWD,
|
|
1706
|
+
daemonBin: opts.daemonBin,
|
|
1707
|
+
autoUpdate: opts.autoUpdate,
|
|
1708
|
+
stopOnExit: opts.stopOnExit ?? true,
|
|
1709
|
+
onLog: spec.onLog
|
|
1710
|
+
};
|
|
1711
|
+
if (opts.docker) {
|
|
1712
|
+
return provisionSshContainer(client, makeTunnel, {
|
|
1713
|
+
...shared2,
|
|
1714
|
+
id: `${opts.username}@${opts.host}/docker`,
|
|
1715
|
+
docker: opts.docker
|
|
1716
|
+
});
|
|
1717
|
+
}
|
|
1718
|
+
return provisionSsh(client, makeTunnel, {
|
|
1719
|
+
...shared2,
|
|
1720
|
+
id: `${opts.username}@${opts.host}:${daemonPort}`
|
|
1721
|
+
});
|
|
1722
|
+
}
|
|
1723
|
+
async function provisionSsh(client, makeTunnel, cfg) {
|
|
1724
|
+
await ensureDaemon(new SshHost(client), {
|
|
1725
|
+
port: cfg.daemonPort,
|
|
1726
|
+
agent: cfg.agent,
|
|
1727
|
+
agentCwd: cfg.agentCwd,
|
|
1728
|
+
daemonBin: cfg.daemonBin,
|
|
1729
|
+
autoUpdate: cfg.autoUpdate,
|
|
1730
|
+
target: "ssh",
|
|
1731
|
+
onLog: cfg.onLog
|
|
1732
|
+
});
|
|
1733
|
+
const tunnel = await makeTunnel(client, cfg.daemonPort);
|
|
1734
|
+
let stopped = false;
|
|
1735
|
+
const stop = async () => {
|
|
1736
|
+
if (stopped) return;
|
|
1737
|
+
stopped = true;
|
|
1738
|
+
await tunnel.close().catch(() => {
|
|
1739
|
+
});
|
|
1740
|
+
if (cfg.stopOnExit ?? true) {
|
|
1741
|
+
try {
|
|
1742
|
+
client.end();
|
|
1743
|
+
} catch {
|
|
1744
|
+
}
|
|
1745
|
+
}
|
|
1746
|
+
};
|
|
1747
|
+
return { id: cfg.id, baseUrl: tunnel.baseUrl, stop };
|
|
1748
|
+
}
|
|
1749
|
+
async function provisionSshContainer(client, makeTunnel, cfg) {
|
|
1750
|
+
const container = await provisionContainer(
|
|
1751
|
+
new SshHost(client),
|
|
1752
|
+
{
|
|
1753
|
+
image: cfg.docker.image,
|
|
1754
|
+
container: cfg.docker.container,
|
|
1755
|
+
name: cfg.docker.name,
|
|
1756
|
+
install: cfg.docker.install,
|
|
1757
|
+
createArgs: cfg.docker.createArgs,
|
|
1758
|
+
daemonPort: cfg.daemonPort,
|
|
1759
|
+
agent: cfg.agent,
|
|
1760
|
+
agentCwd: cfg.agentCwd,
|
|
1761
|
+
daemonBin: cfg.daemonBin,
|
|
1762
|
+
autoUpdate: cfg.autoUpdate,
|
|
1763
|
+
stopOnExit: cfg.stopOnExit ?? true,
|
|
1764
|
+
target: "ssh"
|
|
1765
|
+
},
|
|
1766
|
+
cfg.onLog
|
|
1767
|
+
);
|
|
1768
|
+
const tunnel = await makeTunnel(client, container.hostPort);
|
|
1769
|
+
let stopped = false;
|
|
1770
|
+
const stop = async () => {
|
|
1771
|
+
if (stopped) return;
|
|
1772
|
+
stopped = true;
|
|
1773
|
+
await tunnel.close().catch(() => {
|
|
1774
|
+
});
|
|
1775
|
+
await container.stop().catch(() => {
|
|
1776
|
+
});
|
|
1777
|
+
if (cfg.stopOnExit ?? true) {
|
|
1778
|
+
try {
|
|
1779
|
+
client.end();
|
|
1780
|
+
} catch {
|
|
1781
|
+
}
|
|
1782
|
+
}
|
|
1783
|
+
};
|
|
1784
|
+
return { id: cfg.id, baseUrl: tunnel.baseUrl, stop };
|
|
1785
|
+
}
|
|
1786
|
+
async function openConnection(mod, opts) {
|
|
1787
|
+
const auth = await buildAuth(opts);
|
|
1788
|
+
return new Promise((resolve, reject) => {
|
|
1789
|
+
const client = new mod.Client();
|
|
1790
|
+
let settled = false;
|
|
1791
|
+
client.on("ready", () => {
|
|
1792
|
+
if (settled) return;
|
|
1793
|
+
settled = true;
|
|
1794
|
+
resolve(client);
|
|
1795
|
+
});
|
|
1796
|
+
client.on("error", (err) => {
|
|
1797
|
+
if (settled) return;
|
|
1798
|
+
settled = true;
|
|
1799
|
+
reject(
|
|
1800
|
+
new MindwireError(
|
|
1801
|
+
`mindwire: SSH connection to ${opts.username}@${opts.host}:${opts.port ?? 22} failed: ${err.message}`,
|
|
1802
|
+
{ cause: err }
|
|
1803
|
+
)
|
|
1804
|
+
);
|
|
1805
|
+
});
|
|
1806
|
+
client.connect({
|
|
1807
|
+
host: opts.host,
|
|
1808
|
+
port: opts.port ?? 22,
|
|
1809
|
+
username: opts.username,
|
|
1810
|
+
readyTimeout: opts.readyTimeoutMs ?? 2e4,
|
|
1811
|
+
...auth
|
|
1812
|
+
});
|
|
1813
|
+
});
|
|
1814
|
+
}
|
|
1815
|
+
async function buildAuth(opts) {
|
|
1816
|
+
if (opts.privateKey !== void 0) {
|
|
1817
|
+
return { privateKey: opts.privateKey, ...opts.passphrase ? { passphrase: opts.passphrase } : {} };
|
|
1818
|
+
}
|
|
1819
|
+
if (opts.privateKeyPath !== void 0) {
|
|
1820
|
+
const fs = await import('fs/promises');
|
|
1821
|
+
const key = await fs.readFile(opts.privateKeyPath);
|
|
1822
|
+
return { privateKey: key, ...opts.passphrase ? { passphrase: opts.passphrase } : {} };
|
|
1823
|
+
}
|
|
1824
|
+
if (opts.password !== void 0) {
|
|
1825
|
+
return { password: opts.password };
|
|
1826
|
+
}
|
|
1827
|
+
const sock = opts.agent ?? env("SSH_AUTH_SOCK");
|
|
1828
|
+
return sock ? { agent: sock } : {};
|
|
1829
|
+
}
|
|
1830
|
+
async function createTunnel(net, client, daemonPort) {
|
|
1831
|
+
await new Promise((resolve, reject) => {
|
|
1832
|
+
client.forwardOut("127.0.0.1", 0, "127.0.0.1", daemonPort, (err, stream) => {
|
|
1833
|
+
if (err) {
|
|
1834
|
+
reject(
|
|
1835
|
+
new MindwireError(
|
|
1836
|
+
`mindwire: the SSH server refused a port-forward to 127.0.0.1:${daemonPort} (direct-tcpip). Enable \`AllowTcpForwarding yes\` in the remote sshd_config \u2014 mindwire tunnels the daemon's HTTP/SSE over the SSH connection.`,
|
|
1837
|
+
{ cause: err }
|
|
1838
|
+
)
|
|
1839
|
+
);
|
|
1840
|
+
return;
|
|
1841
|
+
}
|
|
1842
|
+
try {
|
|
1843
|
+
(stream.destroy ?? stream.end)?.call(stream);
|
|
1844
|
+
} catch {
|
|
1845
|
+
}
|
|
1846
|
+
resolve();
|
|
1847
|
+
});
|
|
1848
|
+
});
|
|
1849
|
+
const sockets = /* @__PURE__ */ new Set();
|
|
1850
|
+
const server = net.createServer((socket) => {
|
|
1851
|
+
sockets.add(socket);
|
|
1852
|
+
socket.on("close", () => sockets.delete(socket));
|
|
1853
|
+
socket.on("error", () => {
|
|
1854
|
+
});
|
|
1855
|
+
client.forwardOut("127.0.0.1", 0, "127.0.0.1", daemonPort, (err, stream) => {
|
|
1856
|
+
if (err) {
|
|
1857
|
+
socket.destroy();
|
|
1858
|
+
return;
|
|
1859
|
+
}
|
|
1860
|
+
stream.on("error", () => socket.destroy());
|
|
1861
|
+
const duplex = stream;
|
|
1862
|
+
socket.pipe(duplex).pipe(socket);
|
|
1863
|
+
});
|
|
1864
|
+
});
|
|
1865
|
+
const localPort = await new Promise((resolve, reject) => {
|
|
1866
|
+
server.on("error", reject);
|
|
1867
|
+
server.listen(0, "127.0.0.1", () => {
|
|
1868
|
+
const addr = server.address();
|
|
1869
|
+
resolve(addr && typeof addr === "object" ? addr.port : 0);
|
|
1870
|
+
});
|
|
1871
|
+
});
|
|
1872
|
+
const close = async () => {
|
|
1873
|
+
for (const s of sockets) {
|
|
1874
|
+
try {
|
|
1875
|
+
s.destroy();
|
|
1876
|
+
} catch {
|
|
1877
|
+
}
|
|
1878
|
+
}
|
|
1879
|
+
sockets.clear();
|
|
1880
|
+
await new Promise((resolve) => server.close(() => resolve()));
|
|
1881
|
+
};
|
|
1882
|
+
return { baseUrl: `http://127.0.0.1:${localPort}`, close };
|
|
1883
|
+
}
|
|
1884
|
+
function shQuote(s) {
|
|
1885
|
+
return `'${s.replace(/'/g, `'\\''`)}'`;
|
|
1886
|
+
}
|
|
1887
|
+
function toCommandLine(argv) {
|
|
1888
|
+
return argv.map(shQuote).join(" ");
|
|
1889
|
+
}
|
|
1890
|
+
async function importSsh2() {
|
|
1891
|
+
const spec = "ssh2";
|
|
1892
|
+
try {
|
|
1893
|
+
const mod = await import(
|
|
1894
|
+
/* @vite-ignore */
|
|
1895
|
+
spec
|
|
1896
|
+
);
|
|
1897
|
+
return mod.default && mod.default.Client ? mod.default : mod;
|
|
1898
|
+
} catch (err) {
|
|
1899
|
+
throw new MindwireError(
|
|
1900
|
+
"mindwire: the SSH target needs the optional `ssh2` package. Install it (`npm i ssh2`). It's an optional peer dependency, so the core SDK stays dependency-free.",
|
|
1901
|
+
{ cause: err }
|
|
1902
|
+
);
|
|
1903
|
+
}
|
|
1904
|
+
}
|
|
1905
|
+
function env(key) {
|
|
1906
|
+
const proc = globalThis.process;
|
|
1907
|
+
return proc?.env?.[key];
|
|
1908
|
+
}
|
|
1909
|
+
|
|
1910
|
+
// src/target/docker.ts
|
|
1911
|
+
function docker(config = {}) {
|
|
1912
|
+
return { name: "docker", connect: (spec) => connectDocker(config, spec) };
|
|
1913
|
+
}
|
|
1914
|
+
var DEFAULT_PORT2 = 8790;
|
|
1915
|
+
var DEFAULT_AGENT2 = "claude-code";
|
|
1916
|
+
var DEFAULT_AGENT_CWD2 = "/root";
|
|
1917
|
+
var DEFAULT_RUNTIME_IMAGE = `ghcr.io/oblien/mindwire-runtime:${SDK_VERSION}`;
|
|
1918
|
+
var DockerHost = class {
|
|
1919
|
+
constructor(container) {
|
|
1920
|
+
this.container = container;
|
|
1921
|
+
}
|
|
1922
|
+
container;
|
|
1923
|
+
async exec(argv) {
|
|
1924
|
+
const { Writable } = await import('stream');
|
|
1925
|
+
const exec = await this.container.exec({ Cmd: argv, AttachStdout: true, AttachStderr: true });
|
|
1926
|
+
const stream = await exec.start({ hijack: true, stdin: false });
|
|
1927
|
+
const out = [];
|
|
1928
|
+
const err = [];
|
|
1929
|
+
const sink = (arr) => new Writable({
|
|
1930
|
+
write(chunk, _enc, cb) {
|
|
1931
|
+
arr.push(Buffer.from(chunk));
|
|
1932
|
+
cb();
|
|
1933
|
+
}
|
|
1934
|
+
});
|
|
1935
|
+
this.container.modem.demuxStream(stream, sink(out), sink(err));
|
|
1936
|
+
await new Promise((resolve, reject) => {
|
|
1937
|
+
stream.on("end", () => resolve());
|
|
1938
|
+
stream.on("close", () => resolve());
|
|
1939
|
+
stream.on("error", (e) => reject(e instanceof Error ? e : new Error(String(e))));
|
|
1940
|
+
});
|
|
1941
|
+
const info = await exec.inspect();
|
|
1942
|
+
return {
|
|
1943
|
+
exitCode: info.ExitCode,
|
|
1944
|
+
stdout: Buffer.concat(out).toString("utf8"),
|
|
1945
|
+
stderr: Buffer.concat(err).toString("utf8")
|
|
1946
|
+
};
|
|
1947
|
+
}
|
|
1948
|
+
async putFile(path, data, opts) {
|
|
1949
|
+
const slash = path.lastIndexOf("/");
|
|
1950
|
+
const dir = slash > 0 ? path.slice(0, slash) : "/";
|
|
1951
|
+
const base = path.slice(slash + 1);
|
|
1952
|
+
await this.exec(["mkdir", "-p", dir]);
|
|
1953
|
+
const tar = tarSingleFile(base, data, opts?.mode ?? "0644");
|
|
1954
|
+
await this.container.putArchive(tar, { path: dir });
|
|
1955
|
+
}
|
|
1956
|
+
};
|
|
1957
|
+
async function connectDocker(config, spec) {
|
|
1958
|
+
const Docker = await importDockerode();
|
|
1959
|
+
const instance = config.docker ?? new Docker(config.engine);
|
|
1960
|
+
const merged = { ...config, agent: config.agent ?? spec.agent };
|
|
1961
|
+
return provisionDocker(instance, merged, spec.onLog);
|
|
1962
|
+
}
|
|
1963
|
+
async function provisionDocker(docker2, config = {}, onLog) {
|
|
1964
|
+
const port = config.port ?? DEFAULT_PORT2;
|
|
1965
|
+
const agent = config.agent ?? DEFAULT_AGENT2;
|
|
1966
|
+
const agentCwd = config.agentCwd ?? DEFAULT_AGENT_CWD2;
|
|
1967
|
+
const created = config.container === void 0;
|
|
1968
|
+
let container;
|
|
1969
|
+
if (config.container !== void 0) {
|
|
1970
|
+
container = docker2.getContainer(config.container);
|
|
1971
|
+
} else {
|
|
1972
|
+
const image = config.image ?? DEFAULT_RUNTIME_IMAGE;
|
|
1973
|
+
await ensureImage(docker2, image, config.pull ?? "missing", onLog);
|
|
1974
|
+
const portKey = `${port}/tcp`;
|
|
1975
|
+
container = await docker2.createContainer({
|
|
1976
|
+
// The runtime image starts mindwired itself. Do not replace its command: doing so turns the
|
|
1977
|
+
// image ENTRYPOINT into `mindwired sleep infinity` and prevents the runtime from starting.
|
|
1978
|
+
Image: image,
|
|
1979
|
+
Env: [`ADDR=:${port}`, `AGENT_TYPE=${agent}`, `AGENT_CWD=${agentCwd}`],
|
|
1980
|
+
Tty: false,
|
|
1981
|
+
ExposedPorts: { [portKey]: {} },
|
|
1982
|
+
HostConfig: { PortBindings: { [portKey]: [{ HostPort: "0" }] } },
|
|
1983
|
+
...config.createOptions ?? {}
|
|
1984
|
+
});
|
|
1985
|
+
}
|
|
1986
|
+
await container.start().catch(() => {
|
|
1987
|
+
});
|
|
1988
|
+
const info = await container.inspect();
|
|
1989
|
+
const binding = info.NetworkSettings?.Ports?.[`${port}/tcp`];
|
|
1990
|
+
const hostPort = binding?.[0]?.HostPort;
|
|
1991
|
+
if (!hostPort) {
|
|
1992
|
+
throw new MindwireError(
|
|
1993
|
+
`mindwire: the Docker container does not publish a host port for ${port}/tcp. ` + (created ? "This is unexpected for a container mindwire created." : "Attach to a container started with that port published (e.g. `-p 0:" + port + "`).")
|
|
1994
|
+
);
|
|
1995
|
+
}
|
|
1996
|
+
await ensureDaemon(new DockerHost(container), {
|
|
1997
|
+
port,
|
|
1998
|
+
agent,
|
|
1999
|
+
agentCwd,
|
|
2000
|
+
daemonBin: config.daemonBin,
|
|
2001
|
+
autoUpdate: config.autoUpdate,
|
|
2002
|
+
target: "docker",
|
|
2003
|
+
onLog
|
|
2004
|
+
});
|
|
2005
|
+
const stop = async () => {
|
|
2006
|
+
if (!config.stopOnExit) return;
|
|
2007
|
+
if (!created) return;
|
|
2008
|
+
try {
|
|
2009
|
+
await container.stop().catch(() => {
|
|
2010
|
+
});
|
|
2011
|
+
await container.remove({ force: true });
|
|
2012
|
+
} catch {
|
|
2013
|
+
}
|
|
2014
|
+
};
|
|
2015
|
+
return { id: container.id, baseUrl: `http://127.0.0.1:${hostPort}`, stop };
|
|
2016
|
+
}
|
|
2017
|
+
async function ensureImage(docker2, image, policy, onLog) {
|
|
2018
|
+
const emit2 = (message, error) => {
|
|
2019
|
+
try {
|
|
2020
|
+
onLog?.({ phase: "pull", target: "docker", message, ...error ? { error } : {} });
|
|
2021
|
+
} catch {
|
|
2022
|
+
}
|
|
2023
|
+
};
|
|
2024
|
+
if (policy === "never") return;
|
|
2025
|
+
if (policy === "missing") {
|
|
2026
|
+
try {
|
|
2027
|
+
await docker2.getImage(image).inspect();
|
|
2028
|
+
return;
|
|
2029
|
+
} catch {
|
|
2030
|
+
}
|
|
2031
|
+
}
|
|
2032
|
+
emit2(`pulling runtime image ${image}`);
|
|
2033
|
+
try {
|
|
2034
|
+
const stream = await docker2.pull(image);
|
|
2035
|
+
await new Promise((resolve, reject) => {
|
|
2036
|
+
stream.on("end", () => resolve());
|
|
2037
|
+
stream.on("close", () => resolve());
|
|
2038
|
+
stream.on("error", (error) => reject(error));
|
|
2039
|
+
});
|
|
2040
|
+
emit2(`runtime image ready: ${image}`);
|
|
2041
|
+
} catch (error) {
|
|
2042
|
+
const detail = error instanceof Error ? error.message : String(error);
|
|
2043
|
+
emit2(`failed to pull runtime image ${image}`, detail);
|
|
2044
|
+
throw new MindwireError(`mindwire: failed to pull Docker image ${image}: ${detail}`, { cause: error });
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
function tarSingleFile(name, data, mode) {
|
|
2048
|
+
const header = Buffer.alloc(512, 0);
|
|
2049
|
+
header.write(name.slice(0, 100), 0, "utf8");
|
|
2050
|
+
header.write(octal(parseInt(mode, 8) || 420, 8), 100, "utf8");
|
|
2051
|
+
header.write(octal(0, 8), 108, "utf8");
|
|
2052
|
+
header.write(octal(0, 8), 116, "utf8");
|
|
2053
|
+
header.write(octal(data.length, 12), 124, "utf8");
|
|
2054
|
+
header.write(octal(0, 12), 136, "utf8");
|
|
2055
|
+
header.write(" ", 148, "utf8");
|
|
2056
|
+
header.write("0", 156, "utf8");
|
|
2057
|
+
header.write("ustar\0", 257, "latin1");
|
|
2058
|
+
header.write("00", 263, "utf8");
|
|
2059
|
+
let sum = 0;
|
|
2060
|
+
for (const b of header) sum += b;
|
|
2061
|
+
header.write(sum.toString(8).padStart(6, "0") + "\0 ", 148, "latin1");
|
|
2062
|
+
const body = Buffer.from(data);
|
|
2063
|
+
const pad = (512 - body.length % 512) % 512;
|
|
2064
|
+
return Buffer.concat([header, body, Buffer.alloc(pad), Buffer.alloc(1024)]);
|
|
2065
|
+
}
|
|
2066
|
+
function octal(n, len) {
|
|
2067
|
+
return n.toString(8).padStart(len - 1, "0").slice(-(len - 1)) + "\0";
|
|
2068
|
+
}
|
|
2069
|
+
async function importDockerode() {
|
|
2070
|
+
const spec = "dockerode";
|
|
2071
|
+
try {
|
|
2072
|
+
const mod = await import(
|
|
2073
|
+
/* @vite-ignore */
|
|
2074
|
+
spec
|
|
2075
|
+
);
|
|
2076
|
+
return mod.default ?? mod;
|
|
2077
|
+
} catch (err) {
|
|
2078
|
+
throw new MindwireError(
|
|
2079
|
+
"mindwire: the Docker sandbox adapter needs the optional `dockerode` package. Install it (`npm i dockerode`). It's an optional peer dependency, so the core SDK stays dependency-free.",
|
|
2080
|
+
{ cause: err }
|
|
2081
|
+
);
|
|
2082
|
+
}
|
|
2083
|
+
}
|
|
2084
|
+
|
|
2085
|
+
// src/target/oblien.ts
|
|
2086
|
+
function oblien(config = {}) {
|
|
2087
|
+
return { name: "oblien", connect: (spec) => connectOblien(config, spec) };
|
|
2088
|
+
}
|
|
2089
|
+
var DEFAULT_PORT3 = 8790;
|
|
2090
|
+
var DEFAULT_IMAGE = "node-22";
|
|
2091
|
+
var DEFAULT_AGENT3 = "claude-code";
|
|
2092
|
+
var DEFAULT_AGENT_CWD3 = "/root";
|
|
2093
|
+
var SENTINEL_BASE = "http://mindwire-sandbox.local";
|
|
2094
|
+
var OblienHost = class {
|
|
2095
|
+
constructor(rt) {
|
|
2096
|
+
this.rt = rt;
|
|
2097
|
+
}
|
|
2098
|
+
rt;
|
|
2099
|
+
async exec(argv, opts) {
|
|
2100
|
+
const r = await this.rt.exec.run(argv, {
|
|
2101
|
+
execMode: "direct",
|
|
2102
|
+
timeoutSeconds: opts?.timeoutSeconds,
|
|
2103
|
+
keepLogs: true
|
|
2104
|
+
});
|
|
2105
|
+
return { exitCode: r.exit_code, stdout: r.stdout, stderr: r.stderr, error: r.error };
|
|
2106
|
+
}
|
|
2107
|
+
async putFile(path, data, opts) {
|
|
2108
|
+
const b64 = Buffer.from(data).toString("base64");
|
|
2109
|
+
const b64Path = `${path}.b64`;
|
|
2110
|
+
await this.rt.files.write({ fullPath: b64Path, content: b64, createDirs: true });
|
|
2111
|
+
const mode = opts?.mode ?? "0644";
|
|
2112
|
+
await this.rt.exec.run(
|
|
2113
|
+
["bash", "-lc", `base64 -d ${b64Path} > ${path} && chmod ${mode} ${path} && rm -f ${b64Path}`],
|
|
2114
|
+
{ execMode: "direct", timeoutSeconds: 30, keepLogs: true }
|
|
2115
|
+
);
|
|
2116
|
+
}
|
|
2117
|
+
};
|
|
2118
|
+
async function connectOblien(config, spec) {
|
|
2119
|
+
const clientId = config.clientId ?? env2("MINDWIRE_SANDBOX_CLIENT_ID") ?? env2("OBLIEN_CLIENT_ID");
|
|
2120
|
+
const clientSecret = config.clientSecret ?? env2("MINDWIRE_SANDBOX_CLIENT_SECRET") ?? env2("OBLIEN_CLIENT_SECRET");
|
|
2121
|
+
if (!clientId || !clientSecret) {
|
|
2122
|
+
throw new MindwireError(
|
|
2123
|
+
"mindwire: the Oblien target needs credentials \u2014 pass oblien({ clientId, clientSecret }) or set MINDWIRE_SANDBOX_CLIENT_ID / MINDWIRE_SANDBOX_CLIENT_SECRET (OBLIEN_CLIENT_ID / OBLIEN_CLIENT_SECRET are also accepted)."
|
|
2124
|
+
);
|
|
2125
|
+
}
|
|
2126
|
+
const mod = await importOblien();
|
|
2127
|
+
const client = new mod.default({
|
|
2128
|
+
clientId,
|
|
2129
|
+
clientSecret,
|
|
2130
|
+
...config.baseUrl ? { baseUrl: config.baseUrl } : {}
|
|
2131
|
+
});
|
|
2132
|
+
return provisionOblien(client, { ...config, agent: config.agent ?? spec.agent }, spec.onLog);
|
|
2133
|
+
}
|
|
2134
|
+
async function provisionOblien(client, config = {}, onLog) {
|
|
2135
|
+
const port = config.port ?? DEFAULT_PORT3;
|
|
2136
|
+
const agent = config.agent ?? DEFAULT_AGENT3;
|
|
2137
|
+
const agentCwd = config.agentCwd ?? DEFAULT_AGENT_CWD3;
|
|
2138
|
+
const created = config.workspaceId === void 0;
|
|
2139
|
+
let workspaceId = config.workspaceId;
|
|
2140
|
+
if (workspaceId === void 0) {
|
|
2141
|
+
const params = {
|
|
2142
|
+
image: config.image ?? DEFAULT_IMAGE,
|
|
2143
|
+
mode: config.mode ?? "temporary"
|
|
2144
|
+
};
|
|
2145
|
+
if (config.name !== void 0) params["name"] = config.name;
|
|
2146
|
+
if (config.cpus !== void 0) params["cpus"] = config.cpus;
|
|
2147
|
+
if (config.memoryMb !== void 0) params["memory_mb"] = config.memoryMb;
|
|
2148
|
+
if (config.diskMb !== void 0) params["disk_size_mb"] = config.diskMb;
|
|
2149
|
+
const ws = await client.workspaces.create(params);
|
|
2150
|
+
workspaceId = ws.id;
|
|
2151
|
+
}
|
|
2152
|
+
const handle = client.workspace(workspaceId);
|
|
2153
|
+
await handle.start().catch(() => {
|
|
2154
|
+
});
|
|
2155
|
+
let rt = await handle.runtime();
|
|
2156
|
+
await ensureDaemon(new OblienHost(rt), {
|
|
2157
|
+
port,
|
|
2158
|
+
agent,
|
|
2159
|
+
agentCwd,
|
|
2160
|
+
daemonBin: config.daemonBin,
|
|
2161
|
+
autoUpdate: config.autoUpdate,
|
|
2162
|
+
target: "oblien",
|
|
2163
|
+
onLog
|
|
2164
|
+
});
|
|
2165
|
+
const fetchImpl = async (url, init) => {
|
|
2166
|
+
const { pathname, search } = new URL(url);
|
|
2167
|
+
const path = pathname + search;
|
|
2168
|
+
let res = await rt.proxy(port).fetch(path, init);
|
|
2169
|
+
if (res.status === 401) {
|
|
2170
|
+
rt = await handle.runtime({ force: true });
|
|
2171
|
+
res = await rt.proxy(port).fetch(path, init);
|
|
2172
|
+
}
|
|
2173
|
+
return res;
|
|
2174
|
+
};
|
|
2175
|
+
const stop = async () => {
|
|
2176
|
+
if (!config.stopOnExit) return;
|
|
2177
|
+
try {
|
|
2178
|
+
await (created ? handle.delete() : handle.stop());
|
|
2179
|
+
} catch {
|
|
2180
|
+
}
|
|
2181
|
+
};
|
|
2182
|
+
return { id: workspaceId, workspaceId, baseUrl: SENTINEL_BASE, fetch: fetchImpl, stop };
|
|
2183
|
+
}
|
|
2184
|
+
async function importOblien() {
|
|
2185
|
+
const spec = "oblien";
|
|
2186
|
+
try {
|
|
2187
|
+
return await import(
|
|
2188
|
+
/* @vite-ignore */
|
|
2189
|
+
spec
|
|
2190
|
+
);
|
|
2191
|
+
} catch (err) {
|
|
2192
|
+
throw new MindwireError(
|
|
2193
|
+
"mindwire: sandbox mode with the default (Oblien) adapter needs the optional `oblien` package. Install it (`npm i oblien`). It's an optional peer dependency, so the core SDK stays dependency-free.",
|
|
2194
|
+
{ cause: err }
|
|
2195
|
+
);
|
|
2196
|
+
}
|
|
2197
|
+
}
|
|
2198
|
+
function env2(key) {
|
|
2199
|
+
const proc = globalThis.process;
|
|
2200
|
+
return proc?.env?.[key];
|
|
2201
|
+
}
|
|
2202
|
+
|
|
2203
|
+
// src/catalog/index.ts
|
|
2204
|
+
var MODELS_DEV_URL = "https://models.dev/api.json";
|
|
2205
|
+
var TTL_MS = 6 * 60 * 60 * 1e3;
|
|
2206
|
+
function toCost(c) {
|
|
2207
|
+
if (!c) return void 0;
|
|
2208
|
+
const cost = {};
|
|
2209
|
+
if (typeof c.input === "number") cost.input = c.input;
|
|
2210
|
+
if (typeof c.output === "number") cost.output = c.output;
|
|
2211
|
+
if (typeof c.cache_read === "number") cost.cacheRead = c.cache_read;
|
|
2212
|
+
if (typeof c.cache_write === "number") cost.cacheWrite = c.cache_write;
|
|
2213
|
+
return Object.keys(cost).length ? cost : void 0;
|
|
2214
|
+
}
|
|
2215
|
+
function toModel(providerId, id, m) {
|
|
2216
|
+
const info = { id: m.id ?? id, label: m.name || (m.id ?? id), provider: providerId };
|
|
2217
|
+
if (m.limit?.context) info.contextWindow = m.limit.context;
|
|
2218
|
+
if (m.limit?.output) info.maxOutput = m.limit.output;
|
|
2219
|
+
if (m.modalities?.input?.length) info.inputModalities = m.modalities.input;
|
|
2220
|
+
if (m.modalities?.output?.length) info.outputModalities = m.modalities.output;
|
|
2221
|
+
if (m.reasoning) info.reasoning = true;
|
|
2222
|
+
if (m.tool_call) info.toolCall = true;
|
|
2223
|
+
if (m.attachment) info.attachment = true;
|
|
2224
|
+
const cost = toCost(m.cost);
|
|
2225
|
+
if (cost) info.cost = cost;
|
|
2226
|
+
return info;
|
|
2227
|
+
}
|
|
2228
|
+
function project(feed) {
|
|
2229
|
+
const providers = [];
|
|
2230
|
+
for (const [pid, prov] of Object.entries(feed ?? {})) {
|
|
2231
|
+
if (!prov || typeof prov !== "object") continue;
|
|
2232
|
+
const id = prov.id ?? pid;
|
|
2233
|
+
const models = Object.entries(prov.models ?? {}).map(([mid, m]) => toModel(id, mid, m)).sort((a, b) => a.label.localeCompare(b.label) || a.id.localeCompare(b.id));
|
|
2234
|
+
providers.push({
|
|
2235
|
+
id,
|
|
2236
|
+
name: prov.name || id,
|
|
2237
|
+
env: Array.isArray(prov.env) ? prov.env.filter((v) => typeof v === "string" && !!v) : [],
|
|
2238
|
+
...prov.npm ? { npm: prov.npm } : {},
|
|
2239
|
+
...prov.api ? { api: prov.api } : {},
|
|
2240
|
+
...prov.doc ? { doc: prov.doc } : {},
|
|
2241
|
+
models
|
|
2242
|
+
});
|
|
2243
|
+
}
|
|
2244
|
+
return providers.sort((a, b) => a.name.localeCompare(b.name));
|
|
2245
|
+
}
|
|
2246
|
+
var cache = null;
|
|
2247
|
+
var inflight = null;
|
|
2248
|
+
async function loadCatalog(opts = {}) {
|
|
2249
|
+
const url = opts.url ?? MODELS_DEV_URL;
|
|
2250
|
+
if (cache && cache.url === url && Date.now() - cache.at < TTL_MS) return cache.data;
|
|
2251
|
+
if (inflight) return inflight;
|
|
2252
|
+
const doFetch = opts.fetch ?? globalThis.fetch;
|
|
2253
|
+
if (typeof doFetch !== "function") {
|
|
2254
|
+
throw new Error("no fetch implementation available \u2014 pass CatalogOptions.fetch");
|
|
2255
|
+
}
|
|
2256
|
+
inflight = (async () => {
|
|
2257
|
+
const res = await doFetch(url, { headers: { accept: "application/json" }, signal: opts.signal });
|
|
2258
|
+
if (!res.ok) throw new Error(`models.dev catalog fetch failed: ${res.status} ${res.statusText}`);
|
|
2259
|
+
const data = project(await res.json());
|
|
2260
|
+
cache = { url, at: Date.now(), data };
|
|
2261
|
+
return data;
|
|
2262
|
+
})();
|
|
2263
|
+
try {
|
|
2264
|
+
return await inflight;
|
|
2265
|
+
} finally {
|
|
2266
|
+
inflight = null;
|
|
2267
|
+
}
|
|
2268
|
+
}
|
|
2269
|
+
function catalogProviders(opts) {
|
|
2270
|
+
return loadCatalog(opts);
|
|
2271
|
+
}
|
|
2272
|
+
async function catalogProvider(id, opts) {
|
|
2273
|
+
return (await loadCatalog(opts)).find((p) => p.id === id);
|
|
2274
|
+
}
|
|
2275
|
+
async function catalogModels(provider, opts) {
|
|
2276
|
+
const providers = await loadCatalog(opts);
|
|
2277
|
+
if (provider) return providers.find((p) => p.id === provider)?.models ?? [];
|
|
2278
|
+
return providers.flatMap((p) => p.models);
|
|
2279
|
+
}
|
|
2280
|
+
async function lookupModel(provider, id, opts) {
|
|
2281
|
+
return (await catalogProvider(provider, opts))?.models.find((m) => m.id === id);
|
|
2282
|
+
}
|
|
2283
|
+
function clearCatalogCache() {
|
|
2284
|
+
cache = null;
|
|
2285
|
+
inflight = null;
|
|
2286
|
+
}
|
|
2287
|
+
|
|
2288
|
+
export { ApiError, AuthApi, ContainerHost, Http, MODELS_DEV_URL, McpApi, Mindwire, MindwireError, NotifyApi, PromptsApi, ProvidersApi, Run, RunFailedError, SDK_VERSION, TimeoutError, catalogModels, catalogProvider, catalogProviders, clearCatalogCache, docker, ensureDaemon, ensureDaemonBinary, loadCatalog, local, lookupModel, oblien, provisionContainer, provisionDocker, provisionOblien, provisionSsh, provisionSshContainer, remote, resolveLinuxDaemon, ssh, startEmbedded };
|
|
2289
|
+
//# sourceMappingURL=index.js.map
|
|
2290
|
+
//# sourceMappingURL=index.js.map
|