mindwire 0.1.25 → 0.1.27
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/dist/binary-cache-VM7VHCEU.js +4 -0
- package/dist/{binary-cache-CG52TXCZ.js.map → binary-cache-VM7VHCEU.js.map} +1 -1
- package/dist/{chunk-PFYKJ6LX.js → chunk-3FR7BFAW.js} +4 -56
- package/dist/chunk-3FR7BFAW.js.map +1 -0
- package/dist/chunk-QXHI4TMC.js +2050 -0
- package/dist/chunk-QXHI4TMC.js.map +1 -0
- package/dist/cli.js +567 -2180
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +23 -3
- package/dist/computer/connection-flow.d.ts +22 -0
- package/dist/computer/controller.d.ts +3 -0
- package/dist/computer/lifecycle.d.ts +13 -3
- package/dist/computer/lock.d.ts +11 -0
- package/dist/computer/pairing-display.d.ts +23 -0
- package/dist/computer/process.d.ts +26 -0
- package/dist/computer/relay.d.ts +5 -1
- package/dist/computer/startup.d.ts +38 -0
- package/dist/computer.d.ts +26 -2
- package/dist/controller-7YV6O7GT.js +496 -0
- package/dist/controller-7YV6O7GT.js.map +1 -0
- package/dist/index.cjs +51 -14
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +51 -15
- package/dist/index.js.map +1 -1
- package/dist/service.d.ts +5 -1
- package/dist/types.d.ts +13 -0
- package/dist/workspace.d.ts +16 -2
- package/package.json +1 -1
- package/dist/binary-cache-CG52TXCZ.js +0 -3
- package/dist/chunk-PFYKJ6LX.js.map +0 -1
package/dist/cli.js
CHANGED
|
@@ -1,2172 +1,480 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
import {
|
|
2
|
+
import { defaultStateDirectory, superviseComputer, ensureComputer, computerPairingURI, stopComputer, computerClient, SDK_VERSION, readJSON, writeJSON, currentProcessIdentity, processStateAlive, acquireProcessLock, computerPairingQRData } from './chunk-QXHI4TMC.js';
|
|
3
|
+
import './chunk-T6TD76UA.js';
|
|
3
4
|
import { promisify, parseArgs } from 'util';
|
|
4
|
-
import { fileURLToPath } from 'url';
|
|
5
|
-
import { realpathSync } from 'fs';
|
|
6
|
-
import * as
|
|
7
|
-
import {
|
|
5
|
+
import { fileURLToPath, pathToFileURL } from 'url';
|
|
6
|
+
import { realpathSync, mkdtempSync, chmodSync, writeFileSync, rmSync } from 'fs';
|
|
7
|
+
import * as path from 'path';
|
|
8
|
+
import { join } from 'path';
|
|
9
|
+
import { createHash } from 'crypto';
|
|
10
|
+
import { hostname, homedir, tmpdir } from 'os';
|
|
8
11
|
import { createInterface } from 'readline/promises';
|
|
9
12
|
import { setTimeout as setTimeout$1 } from 'timers/promises';
|
|
10
|
-
import { execFile,
|
|
13
|
+
import { execFile, spawn } from 'child_process';
|
|
11
14
|
import * as fs from 'fs/promises';
|
|
12
|
-
import { readFile, mkdtemp, writeFile, rm } from 'fs/promises';
|
|
13
|
-
import { homedir, networkInterfaces, tmpdir } from 'os';
|
|
14
15
|
|
|
15
|
-
|
|
16
|
-
var
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
}
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
}
|
|
93
|
-
this.fetchImpl = f;
|
|
94
|
-
this.baseHeaders = { ...opts.headers };
|
|
95
|
-
this.timeoutMs = opts.timeoutMs ?? DEFAULT_TIMEOUT_MS;
|
|
96
|
-
if (opts.baseUrl) {
|
|
97
|
-
const base = { baseUrl: opts.baseUrl.replace(/\/+$/, ""), token: opts.token };
|
|
98
|
-
this.resolver = async () => base;
|
|
99
|
-
} else if (opts.resolveBase) {
|
|
100
|
-
const rb = opts.resolveBase;
|
|
101
|
-
this.resolver = async () => {
|
|
102
|
-
const r = await rb();
|
|
103
|
-
return {
|
|
104
|
-
baseUrl: r.baseUrl.replace(/\/+$/, ""),
|
|
105
|
-
token: r.token ?? opts.token,
|
|
106
|
-
getToken: r.getToken,
|
|
107
|
-
headers: r.headers,
|
|
108
|
-
fetch: r.fetch
|
|
109
|
-
};
|
|
110
|
-
};
|
|
111
|
-
} else {
|
|
112
|
-
throw new MindwireError("mindwire: Http requires a baseUrl or a resolveBase");
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
/**
|
|
116
|
-
* One fetch, with raw network failures wrapped as {@link MindwireError} so callers see a typed
|
|
117
|
-
* SDK error instead of a bare `TypeError: fetch failed`. An abort (the request's own signal, or
|
|
118
|
-
* our timeout controller) is re-thrown untouched — `fetchWithTimeout` classifies it.
|
|
119
|
-
*/
|
|
120
|
-
async fetchOnce(f, url, init, method, path4) {
|
|
121
|
-
try {
|
|
122
|
-
return await f(url, init);
|
|
123
|
-
} catch (e) {
|
|
124
|
-
const aborted = init.signal?.aborted ?? false;
|
|
125
|
-
if (aborted || isAbortError(e)) throw e;
|
|
126
|
-
throw new MindwireError(`mindwire: ${method} ${path4} \u2014 network request failed`, { cause: e });
|
|
127
|
-
}
|
|
128
|
-
}
|
|
129
|
-
/**
|
|
130
|
-
* `fetchOnce` plus a timeout that composes with the caller's `signal`. A hand-rolled controller
|
|
131
|
-
* (not `AbortSignal.timeout`/`AbortSignal.any`, which need Node 20.3+/18.17+) fires after
|
|
132
|
-
* `timeoutMs`; whichever aborts first wins. Timeout → {@link TimeoutError}; the caller's own abort
|
|
133
|
-
* propagates as-is (its `reason`). `timeoutMs<=0` disables the deadline entirely.
|
|
134
|
-
*/
|
|
135
|
-
async fetchWithTimeout(f, url, init, userSignal, method, path4) {
|
|
136
|
-
if (!this.timeoutMs || this.timeoutMs <= 0) {
|
|
137
|
-
return this.fetchOnce(f, url, { ...init, ...userSignal ? { signal: userSignal } : {} }, method, path4);
|
|
138
|
-
}
|
|
139
|
-
const ctrl = new AbortController();
|
|
140
|
-
const onAbort = () => ctrl.abort(userSignal?.reason);
|
|
141
|
-
if (userSignal) {
|
|
142
|
-
if (userSignal.aborted) ctrl.abort(userSignal.reason);
|
|
143
|
-
else userSignal.addEventListener("abort", onAbort, { once: true });
|
|
144
|
-
}
|
|
145
|
-
let timedOut = false;
|
|
146
|
-
const timer = setTimeout(() => {
|
|
147
|
-
timedOut = true;
|
|
148
|
-
ctrl.abort();
|
|
149
|
-
}, this.timeoutMs);
|
|
150
|
-
try {
|
|
151
|
-
return await this.fetchOnce(f, url, { ...init, signal: ctrl.signal }, method, path4);
|
|
152
|
-
} catch (e) {
|
|
153
|
-
if (timedOut) throw new TimeoutError(method, path4, this.timeoutMs);
|
|
154
|
-
if (userSignal?.aborted) throw userSignal.reason ?? e;
|
|
155
|
-
throw e;
|
|
156
|
-
} finally {
|
|
157
|
-
clearTimeout(timer);
|
|
158
|
-
if (userSignal) userSignal.removeEventListener("abort", onAbort);
|
|
159
|
-
}
|
|
160
|
-
}
|
|
161
|
-
/**
|
|
162
|
-
* The bearer token for the next request. Prefers the transport's dynamic `getToken` (which
|
|
163
|
-
* caches and mints on demand); falls back to the static token resolved at base time. `force`
|
|
164
|
-
* asks the getter to re-mint — used once on a 401 before retrying.
|
|
165
|
-
*/
|
|
166
|
-
async authToken(base, force) {
|
|
167
|
-
if (base.getToken) {
|
|
168
|
-
const t = await base.getToken(force ? { force: true } : void 0);
|
|
169
|
-
return t ?? base.token;
|
|
170
|
-
}
|
|
171
|
-
return base.token;
|
|
172
|
-
}
|
|
173
|
-
base() {
|
|
174
|
-
if (this.cached) return Promise.resolve(this.cached);
|
|
175
|
-
if (!this.pending) {
|
|
176
|
-
this.pending = this.resolver().then((b) => this.cached = b).catch((err) => {
|
|
177
|
-
this.pending = null;
|
|
178
|
-
throw err;
|
|
179
|
-
});
|
|
180
|
-
}
|
|
181
|
-
return this.pending;
|
|
182
|
-
}
|
|
183
|
-
/**
|
|
184
|
-
* Resolve the base eagerly and memoize it — provisions the transport (spawns the embedded daemon,
|
|
185
|
-
* connects the sandbox/SSH/Docker target, …) now instead of on the first request. Idempotent: this
|
|
186
|
-
* awaits the *same* memoized promise the first `request()`/`open()` awaits, so the target's
|
|
187
|
-
* `connect()` fires exactly once. Backs {@link import("./client.js").Mindwire.ensure}.
|
|
188
|
-
*/
|
|
189
|
-
async ready() {
|
|
190
|
-
await this.base();
|
|
191
|
-
}
|
|
192
|
-
url(baseUrl, path4, query) {
|
|
193
|
-
const u = new URL(baseUrl + (path4.startsWith("/") ? path4 : `/${path4}`));
|
|
194
|
-
if (query) {
|
|
195
|
-
for (const [k, v] of Object.entries(query)) {
|
|
196
|
-
if (v !== void 0) u.searchParams.set(k, String(v));
|
|
197
|
-
}
|
|
198
|
-
}
|
|
199
|
-
return u.toString();
|
|
200
|
-
}
|
|
201
|
-
headers(token, extra, hasBody = false) {
|
|
202
|
-
const h = { Accept: "application/json", ...this.baseHeaders, ...extra };
|
|
203
|
-
if (token) h["Authorization"] = `Bearer ${token}`;
|
|
204
|
-
if (hasBody) h["Content-Type"] = "application/json";
|
|
205
|
-
return h;
|
|
206
|
-
}
|
|
207
|
-
async request(method, path4, init = {}) {
|
|
208
|
-
const base = await this.base();
|
|
209
|
-
const hasBody = init.body !== void 0;
|
|
210
|
-
const url = this.url(base.baseUrl, path4, init.query);
|
|
211
|
-
const extra = { ...base.headers, ...init.headers };
|
|
212
|
-
const f = base.fetch ?? this.fetchImpl;
|
|
213
|
-
const send = async (force) => this.fetchWithTimeout(
|
|
214
|
-
f,
|
|
215
|
-
url,
|
|
216
|
-
{
|
|
217
|
-
method,
|
|
218
|
-
headers: this.headers(await this.authToken(base, force), extra, hasBody),
|
|
219
|
-
body: hasBody ? JSON.stringify(init.body) : void 0
|
|
220
|
-
},
|
|
221
|
-
init.signal,
|
|
222
|
-
method,
|
|
223
|
-
path4
|
|
224
|
-
);
|
|
225
|
-
let res = await send(false);
|
|
226
|
-
if (res.status === 401 && base.getToken) res = await send(true);
|
|
227
|
-
if (!res.ok) throw await this.toApiError(method, res);
|
|
228
|
-
if (res.status === 204) return void 0;
|
|
229
|
-
const text = await res.text();
|
|
230
|
-
if (text === "") return void 0;
|
|
231
|
-
try {
|
|
232
|
-
return JSON.parse(text);
|
|
233
|
-
} catch (e) {
|
|
234
|
-
throw new MindwireError(`mindwire: ${method} ${path4} returned a non-JSON body`, { cause: e });
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
/** Open a streaming response (SSE). Caller owns the body. */
|
|
238
|
-
async open(method, path4, init = {}) {
|
|
239
|
-
const base = await this.base();
|
|
240
|
-
const url = this.url(base.baseUrl, path4, init.query);
|
|
241
|
-
const extra = { Accept: "text/event-stream", ...base.headers, ...init.headers };
|
|
242
|
-
const f = base.fetch ?? this.fetchImpl;
|
|
243
|
-
const send = async (force) => this.fetchOnce(
|
|
244
|
-
f,
|
|
245
|
-
url,
|
|
246
|
-
{
|
|
247
|
-
method,
|
|
248
|
-
headers: this.headers(await this.authToken(base, force), extra, init.body !== void 0),
|
|
249
|
-
...init.body !== void 0 ? { body: JSON.stringify(init.body) } : {},
|
|
250
|
-
...init.signal ? { signal: init.signal } : {}
|
|
251
|
-
},
|
|
252
|
-
method,
|
|
253
|
-
path4
|
|
254
|
-
);
|
|
255
|
-
let res = await send(false);
|
|
256
|
-
if (res.status === 401 && base.getToken) res = await send(true);
|
|
257
|
-
if (!res.ok) throw await this.toApiError(method, res);
|
|
258
|
-
if (!res.body)
|
|
259
|
-
throw new MindwireError(`mindwire: ${method} ${path4} returned no response body to stream`);
|
|
260
|
-
return res;
|
|
261
|
-
}
|
|
262
|
-
async toApiError(method, res) {
|
|
263
|
-
let body = "";
|
|
264
|
-
try {
|
|
265
|
-
const text = await res.text();
|
|
266
|
-
try {
|
|
267
|
-
body = text ? JSON.parse(text) : "";
|
|
268
|
-
} catch {
|
|
269
|
-
body = text;
|
|
270
|
-
}
|
|
271
|
-
} catch {
|
|
272
|
-
}
|
|
273
|
-
return new ApiError({ status: res.status, url: res.url, method, body });
|
|
274
|
-
}
|
|
275
|
-
};
|
|
276
|
-
|
|
277
|
-
// src/sse.ts
|
|
278
|
-
async function* readSSE(body, signal) {
|
|
279
|
-
const reader = body.getReader();
|
|
280
|
-
const decoder = new TextDecoder();
|
|
281
|
-
let buffer = "";
|
|
282
|
-
const onAbort = () => void reader.cancel().catch(() => {
|
|
283
|
-
});
|
|
284
|
-
if (signal) {
|
|
285
|
-
if (signal.aborted) {
|
|
286
|
-
await reader.cancel().catch(() => {
|
|
287
|
-
});
|
|
288
|
-
return;
|
|
289
|
-
}
|
|
290
|
-
signal.addEventListener("abort", onAbort, { once: true });
|
|
291
|
-
}
|
|
292
|
-
try {
|
|
293
|
-
for (; ; ) {
|
|
294
|
-
const { done, value } = await reader.read();
|
|
295
|
-
if (done) break;
|
|
296
|
-
buffer += decoder.decode(value, { stream: true });
|
|
297
|
-
let sep;
|
|
298
|
-
while ((sep = indexOfBlankLine(buffer)) !== -1) {
|
|
299
|
-
const rawEvent = buffer.slice(0, sep);
|
|
300
|
-
buffer = buffer.slice(sepEnd(buffer, sep));
|
|
301
|
-
const payload2 = parseEvent(rawEvent);
|
|
302
|
-
if (payload2 !== void 0) yield JSON.parse(payload2);
|
|
303
|
-
}
|
|
304
|
-
}
|
|
305
|
-
const payload = parseEvent(buffer);
|
|
306
|
-
if (payload !== void 0) yield JSON.parse(payload);
|
|
307
|
-
} finally {
|
|
308
|
-
if (signal) signal.removeEventListener("abort", onAbort);
|
|
309
|
-
await reader.cancel().catch(() => {
|
|
310
|
-
});
|
|
311
|
-
reader.releaseLock();
|
|
312
|
-
}
|
|
313
|
-
}
|
|
314
|
-
function indexOfBlankLine(s) {
|
|
315
|
-
const a = s.indexOf("\n\n");
|
|
316
|
-
const b = s.indexOf("\r\n\r\n");
|
|
317
|
-
if (a === -1) return b;
|
|
318
|
-
if (b === -1) return a;
|
|
319
|
-
return Math.min(a, b);
|
|
320
|
-
}
|
|
321
|
-
function sepEnd(s, idx) {
|
|
322
|
-
return s.startsWith("\r\n\r\n", idx) ? idx + 4 : idx + 2;
|
|
323
|
-
}
|
|
324
|
-
function parseEvent(block) {
|
|
325
|
-
const lines = block.split(/\r\n|\n/);
|
|
326
|
-
const data = [];
|
|
327
|
-
for (const line of lines) {
|
|
328
|
-
if (line === "" || line.startsWith(":")) continue;
|
|
329
|
-
const colon = line.indexOf(":");
|
|
330
|
-
const field = colon === -1 ? line : line.slice(0, colon);
|
|
331
|
-
if (field !== "data") continue;
|
|
332
|
-
let value = colon === -1 ? "" : line.slice(colon + 1);
|
|
333
|
-
if (value.startsWith(" ")) value = value.slice(1);
|
|
334
|
-
data.push(value);
|
|
335
|
-
}
|
|
336
|
-
if (data.length === 0) return void 0;
|
|
337
|
-
return data.join("\n");
|
|
338
|
-
}
|
|
339
|
-
|
|
340
|
-
// src/run.ts
|
|
341
|
-
var TERMINAL = /* @__PURE__ */ new Set(["done", "error", "cancelled"]);
|
|
342
|
-
var Run = class _Run {
|
|
343
|
-
data;
|
|
344
|
-
http;
|
|
345
|
-
constructor(http, data) {
|
|
346
|
-
this.http = http;
|
|
347
|
-
this.data = data;
|
|
348
|
-
}
|
|
349
|
-
get id() {
|
|
350
|
-
return this.data.id;
|
|
351
|
-
}
|
|
352
|
-
get chatId() {
|
|
353
|
-
return this.data.chatId;
|
|
354
|
-
}
|
|
355
|
-
get agent() {
|
|
356
|
-
return this.data.agent;
|
|
357
|
-
}
|
|
358
|
-
get status() {
|
|
359
|
-
return this.data.status;
|
|
360
|
-
}
|
|
361
|
-
/** `"resolve"` on the parent of a global-resolve run; `undefined` for an ordinary turn. */
|
|
362
|
-
get kind() {
|
|
363
|
-
return this.data.kind;
|
|
364
|
-
}
|
|
365
|
-
/** On a child iteration of a resolve run, the id of its parent resolve run; else `undefined`. */
|
|
366
|
-
get parentId() {
|
|
367
|
-
return this.data.parentId;
|
|
368
|
-
}
|
|
369
|
-
/** Parent of a resolve run only: why the loop ended (`"done"` | `"capped"` | `"error"` | …). */
|
|
370
|
-
get stopReason() {
|
|
371
|
-
return this.data.stopReason;
|
|
372
|
-
}
|
|
373
|
-
/** Parent of a resolve run only: how many child turns the loop ran. */
|
|
374
|
-
get iterations() {
|
|
375
|
-
return this.data.iterations;
|
|
376
|
-
}
|
|
377
|
-
/** The latest known run record. */
|
|
378
|
-
get value() {
|
|
379
|
-
return this.data;
|
|
380
|
-
}
|
|
381
|
-
/** Unified SSE event stream: replay buffer, then live events, then close. */
|
|
382
|
-
async *stream(opts = {}) {
|
|
383
|
-
const cursor = opts.after === void 0 ? "" : `?after=${encodeURIComponent(opts.after)}`;
|
|
384
|
-
const res = await this.http.open("GET", `/runs/${encodeURIComponent(this.id)}/stream${cursor}`, {
|
|
385
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
386
|
-
});
|
|
387
|
-
for await (const ev of readSSE(res.body, opts.signal)) {
|
|
388
|
-
if (!opts.includeOpenSentinel && ev.type === "status" && ev.meta?.["stream"] === "open") {
|
|
389
|
-
continue;
|
|
390
|
-
}
|
|
391
|
-
yield ev;
|
|
392
|
-
}
|
|
393
|
-
}
|
|
394
|
-
/** `for await (const ev of run)` — sugar for `run.stream()`. */
|
|
395
|
-
[Symbol.asyncIterator]() {
|
|
396
|
-
return this.stream();
|
|
397
|
-
}
|
|
398
|
-
/** Cancel the in-flight turn (kills the underlying agent process). */
|
|
399
|
-
async cancel() {
|
|
400
|
-
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/cancel`);
|
|
401
|
-
}
|
|
402
|
-
/**
|
|
403
|
-
* Answer a pending permission, question or plan. Message-mode questions remain answerable
|
|
404
|
-
* after completion; the daemon steers or resumes the conversation. Read the chat's latest
|
|
405
|
-
* run after replying to a completed run. Requires the agent's `respond` capability.
|
|
406
|
-
*/
|
|
407
|
-
async respond(input = {}) {
|
|
408
|
-
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/respond`, { body: input });
|
|
409
|
-
}
|
|
410
|
-
/**
|
|
411
|
-
* Steer a follow-up message into the running turn without cancelling it. Requires the agent's
|
|
412
|
-
* `input` capability.
|
|
413
|
-
*/
|
|
414
|
-
async sendInput(text) {
|
|
415
|
-
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/input`, { body: { text } });
|
|
416
|
-
}
|
|
417
|
-
/**
|
|
418
|
-
* Soft-stop the running turn (ask the agent to halt current work) without the hard process kill
|
|
419
|
-
* {@link Run.cancel} does — the turn stays open for a follow-up via {@link Run.sendInput}.
|
|
420
|
-
* Requires the agent's `interrupt` capability.
|
|
421
|
-
*/
|
|
422
|
-
async interrupt() {
|
|
423
|
-
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/interrupt`);
|
|
424
|
-
}
|
|
425
|
-
/**
|
|
426
|
-
* Switch the model of the live turn. An empty/omitted `model` resets the turn to the agent/CLI
|
|
427
|
-
* default. Only meaningful on a persistent (non-bypass) turn; on a one-shot turn it is a
|
|
428
|
-
* best-effort no-op. Requires the agent's `setModel` capability.
|
|
429
|
-
*/
|
|
430
|
-
async setModel(model) {
|
|
431
|
-
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/set-model`, {
|
|
432
|
-
body: { model: model ?? "" }
|
|
433
|
-
});
|
|
434
|
-
}
|
|
435
|
-
/**
|
|
436
|
-
* Switch the live permission mode using a value from the agent's settings schema.
|
|
437
|
-
* Resolves after the harness acknowledges the change; rejects if it cannot apply it.
|
|
438
|
-
* Requires `setPermissionMode`. Codex settings apply on the next turn instead.
|
|
439
|
-
*/
|
|
440
|
-
async setPermissionMode(mode) {
|
|
441
|
-
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/set-permission-mode`, {
|
|
442
|
-
body: { mode }
|
|
443
|
-
});
|
|
444
|
-
}
|
|
445
|
-
/**
|
|
446
|
-
* `GET /runs/{id}/children` — the child iterations of a global-resolve run, oldest→newest, each as
|
|
447
|
-
* its own {@link Run} handle. An ordinary turn (or a resolve that ran a single iteration) returns an
|
|
448
|
-
* empty array. Use it to inspect the run tree a {@link Mindwire.resolve} produced.
|
|
449
|
-
*/
|
|
450
|
-
async children() {
|
|
451
|
-
const data = await this.http.request(
|
|
452
|
-
"GET",
|
|
453
|
-
`/runs/${encodeURIComponent(this.id)}/children`
|
|
454
|
-
);
|
|
455
|
-
return data.map((d) => new _Run(this.http, d));
|
|
456
|
-
}
|
|
457
|
-
/** Re-fetch the run record from the daemon and update this handle. */
|
|
458
|
-
async refresh() {
|
|
459
|
-
this.data = await this.http.request("GET", `/runs/${encodeURIComponent(this.id)}`);
|
|
460
|
-
return this.data;
|
|
461
|
-
}
|
|
462
|
-
/** Restore current output once, then follow with `stream({ after: snapshot.sequence })`. */
|
|
463
|
-
async snapshot() {
|
|
464
|
-
const snapshot = await this.http.request("GET", `/runs/${encodeURIComponent(this.id)}/snapshot`);
|
|
465
|
-
this.data = snapshot.run;
|
|
466
|
-
return snapshot;
|
|
467
|
-
}
|
|
468
|
-
/**
|
|
469
|
-
* Consume the event stream to completion. Returns the final run record and the `result`
|
|
470
|
-
* event's summary (if any). Throws {@link RunFailedError} on an `error`/`cancelled` outcome
|
|
471
|
-
* unless `throwOnError` is set to `false`.
|
|
472
|
-
*/
|
|
473
|
-
async wait(opts = {}) {
|
|
474
|
-
let result;
|
|
475
|
-
let streamError;
|
|
476
|
-
for await (const ev of this.stream(opts)) {
|
|
477
|
-
if (ev.type === "result") result = ev.result;
|
|
478
|
-
else if (ev.type === "error") streamError = ev.error;
|
|
479
|
-
}
|
|
480
|
-
const run = await this.refresh();
|
|
481
|
-
if (opts.throwOnError !== false) {
|
|
482
|
-
if (TERMINAL.has(run.status)) {
|
|
483
|
-
if (run.status !== "done") {
|
|
484
|
-
throw new RunFailedError(run.id, run.status, run.error ?? streamError);
|
|
485
|
-
}
|
|
486
|
-
} else {
|
|
487
|
-
throw new RunFailedError(
|
|
488
|
-
run.id,
|
|
489
|
-
run.status,
|
|
490
|
-
streamError ?? "event stream ended before the run reached a terminal state"
|
|
491
|
-
);
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
return result !== void 0 ? { run, result } : { run };
|
|
495
|
-
}
|
|
496
|
-
};
|
|
497
|
-
|
|
498
|
-
// src/workspace.ts
|
|
499
|
-
var ProjectOperationsApi = class {
|
|
500
|
-
constructor(mw) {
|
|
501
|
-
this.mw = mw;
|
|
502
|
-
}
|
|
503
|
-
mw;
|
|
504
|
-
async list(activeOnly = false) {
|
|
505
|
-
const response = await this.mw.http.request("GET", "/workspace/operations", {
|
|
506
|
-
query: { active: activeOnly }
|
|
507
|
-
});
|
|
508
|
-
return response.operations;
|
|
509
|
-
}
|
|
510
|
-
get(id) {
|
|
511
|
-
return this.mw.http.request("GET", `/workspace/operations/${encodeURIComponent(id)}`);
|
|
512
|
-
}
|
|
513
|
-
cancel(id) {
|
|
514
|
-
return this.mw.http.request("POST", `/workspace/operations/${encodeURIComponent(id)}/cancel`);
|
|
515
|
-
}
|
|
516
|
-
retry(id, auth) {
|
|
517
|
-
return this.mw.http.request("POST", `/workspace/operations/${encodeURIComponent(id)}/retry`, { body: { auth } });
|
|
518
|
-
}
|
|
519
|
-
/** The first event is the current snapshot, then live changes. Reconnecting never replays old
|
|
520
|
-
* progress. Breaking the loop/aborting detaches the observer; cancel(id) explicitly stops work.
|
|
521
|
-
*/
|
|
522
|
-
async *watch(id, opts = {}) {
|
|
523
|
-
const controller = new AbortController();
|
|
524
|
-
const abort = () => controller.abort();
|
|
525
|
-
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
526
|
-
if (opts.signal?.aborted) controller.abort();
|
|
527
|
-
try {
|
|
528
|
-
const response = await this.mw.http.open("GET", `/workspace/operations/${encodeURIComponent(id)}/stream`, {
|
|
529
|
-
signal: controller.signal
|
|
530
|
-
});
|
|
531
|
-
let sequence = -1;
|
|
532
|
-
for await (const operation of readSSE(response.body, controller.signal)) {
|
|
533
|
-
if (operation.sequence > sequence) {
|
|
534
|
-
sequence = operation.sequence;
|
|
535
|
-
yield operation;
|
|
536
|
-
}
|
|
537
|
-
}
|
|
538
|
-
} finally {
|
|
539
|
-
controller.abort();
|
|
540
|
-
opts.signal?.removeEventListener("abort", abort);
|
|
541
|
-
}
|
|
542
|
-
}
|
|
543
|
-
};
|
|
544
|
-
var WorkspaceCollection = class {
|
|
545
|
-
constructor(mw, kind) {
|
|
546
|
-
this.mw = mw;
|
|
547
|
-
this.kind = kind;
|
|
548
|
-
}
|
|
549
|
-
mw;
|
|
550
|
-
kind;
|
|
551
|
-
/** Create with a stable client-generated ID. For updates, supply the record's last revision. */
|
|
552
|
-
put(id, record, expectedRevision) {
|
|
553
|
-
return this.mw.http.request("PUT", `/workspace/${this.kind}/${encodeURIComponent(id)}`, {
|
|
554
|
-
body: { record, expectedRevision }
|
|
555
|
-
});
|
|
556
|
-
}
|
|
557
|
-
/** Remove membership and dependent chat links. Files and native transcripts are retained.
|
|
558
|
-
* Use deleteChat() for an explicit transcript purge. Running chats reject removal with 409.
|
|
559
|
-
*/
|
|
560
|
-
delete(id, revision) {
|
|
561
|
-
return this.mw.http.request("DELETE", `/workspace/${this.kind}/${encodeURIComponent(id)}`, {
|
|
562
|
-
query: { revision }
|
|
563
|
-
});
|
|
564
|
-
}
|
|
565
|
-
};
|
|
566
|
-
var WorkspaceApi = class {
|
|
567
|
-
constructor(mw) {
|
|
568
|
-
this.mw = mw;
|
|
569
|
-
this.git = new GitAccessApi(mw);
|
|
570
|
-
this.operations = new ProjectOperationsApi(mw);
|
|
571
|
-
this.agents = new WorkspaceCollection(mw, "agents");
|
|
572
|
-
this.projects = new WorkspaceCollection(mw, "projects");
|
|
573
|
-
this.chats = new WorkspaceCollection(mw, "chats");
|
|
574
|
-
}
|
|
575
|
-
mw;
|
|
576
|
-
git;
|
|
577
|
-
operations;
|
|
578
|
-
agents;
|
|
579
|
-
projects;
|
|
580
|
-
chats;
|
|
581
|
-
snapshot() {
|
|
582
|
-
return this.mw.http.request("GET", "/workspace");
|
|
583
|
-
}
|
|
584
|
-
/** Start an operation owned by the daemon. The same ID/payload returns the existing operation. */
|
|
585
|
-
createProject(request) {
|
|
586
|
-
return this.mw.http.request("POST", "/workspace/projects", { body: request });
|
|
587
|
-
}
|
|
588
|
-
/** Permanently remove the confirmed project's directory and membership.
|
|
589
|
-
* projects.delete() retains files. Native harness transcripts are not purged.
|
|
590
|
-
*/
|
|
591
|
-
removeProjectFiles(id, request) {
|
|
592
|
-
return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(id)}/remove`, { body: request });
|
|
593
|
-
}
|
|
594
|
-
/** Incremental reconciliation. Pass the previous identity to detect a replaced/restored workspace.
|
|
595
|
-
* A 409 requires fetching snapshot() again; never apply a delta to a different registry.
|
|
596
|
-
*/
|
|
597
|
-
changes(since, workspaceId) {
|
|
598
|
-
return this.mw.http.request("GET", "/workspace/changes", { query: { since, workspaceId } });
|
|
599
|
-
}
|
|
600
|
-
/** Import legacy metadata before replacing a local cache. Safe to repeat after interruption. */
|
|
601
|
-
import(records) {
|
|
602
|
-
return this.mw.http.request("POST", "/workspace/import", { body: records });
|
|
603
|
-
}
|
|
604
|
-
};
|
|
605
|
-
var GitAccessApi = class {
|
|
606
|
-
constructor(mw) {
|
|
607
|
-
this.mw = mw;
|
|
608
|
-
}
|
|
609
|
-
mw;
|
|
610
|
-
/** Reads attribution independently of GitHub authentication. Requires gitIdentityVersion >= 1. */
|
|
611
|
-
identity(context = {}) {
|
|
612
|
-
return this.mw.http.request("GET", "/workspace/git/identity", { query: context });
|
|
613
|
-
}
|
|
614
|
-
/** Saves settings only; never stages or commits. all_workspaces installs this
|
|
615
|
-
* workspace's copy of a client-managed default; the client handles fan-out. */
|
|
616
|
-
setIdentity(update, context = {}) {
|
|
617
|
-
return this.mw.http.request("PUT", "/workspace/git/identity", { query: context, body: update });
|
|
618
|
-
}
|
|
619
|
-
state() {
|
|
620
|
-
return this.mw.http.request("GET", "/workspace/git");
|
|
621
|
-
}
|
|
622
|
-
setDefault(connection, auth) {
|
|
623
|
-
return this.mw.http.request("PUT", "/workspace/git", { body: { connection, auth } });
|
|
624
|
-
}
|
|
625
|
-
forget(connectionId) {
|
|
626
|
-
return this.mw.http.request("DELETE", `/workspace/git/connections/${encodeURIComponent(connectionId)}`);
|
|
627
|
-
}
|
|
628
|
-
project(projectId) {
|
|
629
|
-
return this.mw.http.request("GET", `/workspace/projects/${encodeURIComponent(projectId)}/git`);
|
|
630
|
-
}
|
|
631
|
-
setProject(projectId, connection, expectedRevision, auth) {
|
|
632
|
-
return this.mw.http.request("PUT", `/workspace/projects/${encodeURIComponent(projectId)}/git`, {
|
|
633
|
-
body: { connection, expectedRevision, auth }
|
|
634
|
-
});
|
|
635
|
-
}
|
|
636
|
-
/** Compatibility call. Prefer start() and operation()/watch() for reconnectable writes. */
|
|
637
|
-
run(projectId, operation, auth) {
|
|
638
|
-
return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(projectId)}/git/${operation}`, { body: { auth } });
|
|
639
|
-
}
|
|
640
|
-
/** Requires health.gitOperationsVersion >= 1 (>= 2 for branches, >= 4 for restore_commit). Acceptance persists before Git runs;
|
|
641
|
-
* disconnecting only detaches the client. The same ID/intent never runs twice.
|
|
642
|
-
*/
|
|
643
|
-
start(projectId, request) {
|
|
644
|
-
return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(projectId)}/git/operations`, { body: request });
|
|
645
|
-
}
|
|
646
|
-
async operations(projectId, activeOnly = false) {
|
|
647
|
-
const result = await this.mw.http.request(
|
|
648
|
-
"GET",
|
|
649
|
-
`/workspace/projects/${encodeURIComponent(projectId)}/git/operations`,
|
|
650
|
-
{ query: { active: activeOnly, actionsVersion: 4 } }
|
|
651
|
-
);
|
|
652
|
-
return result.operations;
|
|
653
|
-
}
|
|
654
|
-
operation(id) {
|
|
655
|
-
return this.mw.http.request("GET", `/workspace/git/operations/${encodeURIComponent(id)}`);
|
|
656
|
-
}
|
|
657
|
-
cancel(id) {
|
|
658
|
-
return this.mw.http.request("POST", `/workspace/git/operations/${encodeURIComponent(id)}/cancel`);
|
|
659
|
-
}
|
|
660
|
-
/** Current snapshot followed by state changes. Aborting observation never cancels the operation. */
|
|
661
|
-
async *watch(id, opts = {}) {
|
|
662
|
-
const controller = new AbortController();
|
|
663
|
-
const abort = () => controller.abort();
|
|
664
|
-
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
665
|
-
if (opts.signal?.aborted) controller.abort();
|
|
666
|
-
try {
|
|
667
|
-
const response = await this.mw.http.open("GET", `/workspace/git/operations/${encodeURIComponent(id)}/stream`, {
|
|
668
|
-
signal: controller.signal
|
|
669
|
-
});
|
|
670
|
-
let sequence = -1;
|
|
671
|
-
for await (const operation of readSSE(response.body, controller.signal)) {
|
|
672
|
-
if (operation.sequence > sequence) {
|
|
673
|
-
sequence = operation.sequence;
|
|
674
|
-
yield operation;
|
|
675
|
-
}
|
|
676
|
-
}
|
|
677
|
-
} finally {
|
|
678
|
-
controller.abort();
|
|
679
|
-
opts.signal?.removeEventListener("abort", abort);
|
|
680
|
-
}
|
|
681
|
-
}
|
|
682
|
-
};
|
|
683
|
-
|
|
684
|
-
// src/surfaces.ts
|
|
685
|
-
var SurfacesApi = class {
|
|
686
|
-
constructor(mw) {
|
|
687
|
-
this.mw = mw;
|
|
688
|
-
}
|
|
689
|
-
mw;
|
|
690
|
-
list() {
|
|
691
|
-
return this.mw.http.request("GET", "/surfaces");
|
|
692
|
-
}
|
|
693
|
-
status(refresh = false) {
|
|
694
|
-
return this.mw.http.request("GET", "/surfaces/desktop", { query: { refresh } });
|
|
695
|
-
}
|
|
696
|
-
bind(binding) {
|
|
697
|
-
return this.mw.http.request("PUT", "/surfaces/desktop/binding", { body: binding });
|
|
698
|
-
}
|
|
699
|
-
open(request) {
|
|
700
|
-
return this.mw.http.request("POST", "/surfaces/desktop/sessions", { body: request });
|
|
701
|
-
}
|
|
702
|
-
control(id, request) {
|
|
703
|
-
return this.mw.http.request("POST", `/surfaces/desktop/sessions/${encodeURIComponent(id)}/control`, { body: request });
|
|
704
|
-
}
|
|
705
|
-
close(id) {
|
|
706
|
-
return this.mw.http.request("DELETE", `/surfaces/desktop/sessions/${encodeURIComponent(id)}`);
|
|
707
|
-
}
|
|
708
|
-
capture(id) {
|
|
709
|
-
return this.mw.http.request("POST", `/surfaces/desktop/sessions/${encodeURIComponent(id)}/captures`);
|
|
710
|
-
}
|
|
711
|
-
action(request) {
|
|
712
|
-
return this.mw.http.request("POST", "/surfaces/desktop/actions", { body: request });
|
|
713
|
-
}
|
|
714
|
-
receipt(id) {
|
|
715
|
-
return this.mw.http.request("GET", `/surfaces/desktop/actions/${encodeURIComponent(id)}`);
|
|
716
|
-
}
|
|
717
|
-
artifact(id) {
|
|
718
|
-
return this.mw.http.request("GET", `/artifacts/${encodeURIComponent(id)}`);
|
|
719
|
-
}
|
|
720
|
-
/** Every connection starts with the current snapshot, then revisions. Never replays input. */
|
|
721
|
-
async *watch(opts = {}) {
|
|
722
|
-
const controller = new AbortController();
|
|
723
|
-
const abort = () => controller.abort();
|
|
724
|
-
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
725
|
-
if (opts.signal?.aborted) controller.abort();
|
|
726
|
-
try {
|
|
727
|
-
const response = await this.mw.http.open("GET", "/surfaces/desktop/events", { signal: controller.signal });
|
|
728
|
-
let instance;
|
|
729
|
-
let revision = -1;
|
|
730
|
-
for await (const snapshot of readSSE(response.body, controller.signal)) {
|
|
731
|
-
if (instance !== snapshot.instanceId || snapshot.revision > revision) {
|
|
732
|
-
instance = snapshot.instanceId;
|
|
733
|
-
revision = snapshot.revision;
|
|
734
|
-
yield snapshot;
|
|
735
|
-
}
|
|
736
|
-
}
|
|
737
|
-
} finally {
|
|
738
|
-
controller.abort();
|
|
739
|
-
opts.signal?.removeEventListener("abort", abort);
|
|
740
|
-
}
|
|
741
|
-
}
|
|
742
|
-
};
|
|
743
|
-
|
|
744
|
-
// src/service.ts
|
|
745
|
-
var ServiceApi = class {
|
|
746
|
-
constructor(client) {
|
|
747
|
-
this.client = client;
|
|
748
|
-
}
|
|
749
|
-
client;
|
|
750
|
-
updateStatus() {
|
|
751
|
-
return this.client.http.request("GET", "/service/update");
|
|
752
|
-
}
|
|
753
|
-
/** Download first. Acquire immediately before replacement; busy work returns 409.
|
|
754
|
-
* Admission stays closed until release, process exit, or the one-minute expiry. */
|
|
755
|
-
acquireUpdate() {
|
|
756
|
-
return this.client.http.request("POST", "/service/update");
|
|
757
|
-
}
|
|
758
|
-
releaseUpdate(id) {
|
|
759
|
-
return this.client.http.request("DELETE", `/service/update/${encodeURIComponent(id)}`);
|
|
760
|
-
}
|
|
761
|
-
};
|
|
762
|
-
|
|
763
|
-
// src/execution.ts
|
|
764
|
-
var ExecutionApi = class {
|
|
765
|
-
constructor(client) {
|
|
766
|
-
this.client = client;
|
|
767
|
-
this.terminals = new TerminalsApi(client);
|
|
768
|
-
}
|
|
769
|
-
client;
|
|
770
|
-
terminals;
|
|
771
|
-
host() {
|
|
772
|
-
return this.client.http.request("GET", "/workspace/host");
|
|
773
|
-
}
|
|
774
|
-
resources() {
|
|
775
|
-
return this.client.http.request("GET", "/workspace/resources");
|
|
776
|
-
}
|
|
777
|
-
files(path4 = "", query) {
|
|
778
|
-
return this.client.http.request("GET", "/workspace/files", { query: { path: path4, query } });
|
|
779
|
-
}
|
|
780
|
-
read(path4) {
|
|
781
|
-
return this.client.http.request("GET", "/workspace/file", { query: { path: path4 } });
|
|
782
|
-
}
|
|
783
|
-
write(path4, content) {
|
|
784
|
-
return this.client.http.request("PUT", "/workspace/file", { body: { path: path4, content } });
|
|
785
|
-
}
|
|
786
|
-
remove(path4) {
|
|
787
|
-
return this.client.http.request("DELETE", "/workspace/file", { query: { path: path4 } });
|
|
788
|
-
}
|
|
789
|
-
exec(command, signal) {
|
|
790
|
-
return this.client.http.request("POST", "/workspace/exec", { body: command, signal });
|
|
791
|
-
}
|
|
792
|
-
/** Output data is base64, preserving partial UTF-8 chunks. Cancelling stops this command. */
|
|
793
|
-
async *stream(command, signal) {
|
|
794
|
-
const response = await this.client.http.open("POST", "/workspace/exec/stream", { body: command, signal });
|
|
795
|
-
yield* readSSE(response.body, signal);
|
|
796
|
-
}
|
|
797
|
-
};
|
|
798
|
-
var TerminalsApi = class {
|
|
799
|
-
constructor(client) {
|
|
800
|
-
this.client = client;
|
|
801
|
-
}
|
|
802
|
-
client;
|
|
803
|
-
list(directory) {
|
|
804
|
-
return this.client.http.request("GET", "/workspace/terminals", { query: { directory } });
|
|
805
|
-
}
|
|
806
|
-
open(request) {
|
|
807
|
-
return this.client.http.request("POST", "/workspace/terminals", { body: request });
|
|
808
|
-
}
|
|
809
|
-
get(id) {
|
|
810
|
-
return this.client.http.request("GET", this.path(id));
|
|
811
|
-
}
|
|
812
|
-
close(id) {
|
|
813
|
-
return this.client.http.request("DELETE", this.path(id));
|
|
814
|
-
}
|
|
815
|
-
input(id, input) {
|
|
816
|
-
return this.client.http.request("POST", `${this.path(id)}/input`, { body: input });
|
|
817
|
-
}
|
|
818
|
-
resize(id, columns, rows) {
|
|
819
|
-
return this.client.http.request("PUT", `${this.path(id)}/size`, { body: { columns, rows } });
|
|
820
|
-
}
|
|
821
|
-
/** Detaching only closes the subscription. Resume from the last sequence; reset replaces the screen. */
|
|
822
|
-
async *events(id, after = 0, signal) {
|
|
823
|
-
const response = await this.client.http.open("GET", `${this.path(id)}/events`, { query: { after }, signal });
|
|
824
|
-
yield* readSSE(response.body, signal);
|
|
825
|
-
}
|
|
826
|
-
path(id) {
|
|
827
|
-
return `/workspace/terminals/${encodeURIComponent(id)}`;
|
|
828
|
-
}
|
|
829
|
-
};
|
|
830
|
-
|
|
831
|
-
// src/daemon-binary.ts
|
|
832
|
-
function supported(platform, arch) {
|
|
833
|
-
if (!["darwin", "linux", "win32"].includes(platform) || !["x64", "arm64"].includes(arch)) {
|
|
834
|
-
throw new MindwireError(`mindwire: no daemon release for ${platform}-${arch}`);
|
|
835
|
-
}
|
|
836
|
-
}
|
|
837
|
-
function assetName(version, platform, arch) {
|
|
838
|
-
const releasePlatform = platform === "win32" ? "windows" : platform;
|
|
839
|
-
const releaseArch = arch === "x64" ? "amd64" : arch;
|
|
840
|
-
return `mindwired-v${version}-${releasePlatform}-${releaseArch}${platform === "win32" ? ".exe" : ""}`;
|
|
841
|
-
}
|
|
842
|
-
function checksumFor(text, asset) {
|
|
843
|
-
const line = text.split("\n").find((l) => l.trim().endsWith(` ${asset}`) || l.trim().endsWith(` ${asset}`));
|
|
844
|
-
const hash = line?.trim().split(/\s+/)[0];
|
|
845
|
-
return hash && /^[a-f0-9]{64}$/i.test(hash) ? hash.toLowerCase() : void 0;
|
|
846
|
-
}
|
|
847
|
-
async function ensureDaemonBinary(opts = {}) {
|
|
848
|
-
const proc = globalThis;
|
|
849
|
-
const platform = opts.platform ?? proc.process?.platform;
|
|
850
|
-
const arch = opts.arch ?? proc.process?.arch;
|
|
851
|
-
supported(platform ?? "unknown", arch ?? "unknown");
|
|
852
|
-
const daemonPlatform = platform;
|
|
853
|
-
const daemonArch = arch;
|
|
854
|
-
const version = opts.version ?? SDK_VERSION;
|
|
855
|
-
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
|
|
856
|
-
throw new MindwireError(`mindwire: cannot download daemon for non-release SDK version ${version}`);
|
|
857
|
-
}
|
|
858
|
-
const path4 = await import('path');
|
|
859
|
-
const { binaryCacheDirectory: binaryCacheDirectory2, cachedExecutable: cachedExecutable2, cacheExecutable: cacheExecutable2 } = await import('./binary-cache-CG52TXCZ.js');
|
|
860
|
-
const dir = path4.join(opts.cacheDir ?? await binaryCacheDirectory2(daemonPlatform), version, `${daemonPlatform}-${daemonArch}`);
|
|
861
|
-
const asset = assetName(version, daemonPlatform, daemonArch);
|
|
862
|
-
const bin = path4.join(dir, asset);
|
|
863
|
-
const cached = await cachedExecutable2(bin);
|
|
864
|
-
if (cached) return cached;
|
|
865
|
-
const base = (opts.releaseBaseUrl ?? proc.process?.env?.MINDWIRE_RELEASE_BASE_URL ?? "https://github.com/oblien/mindwire/releases/download").replace(/\/$/, "");
|
|
866
|
-
const release = `${base}/v${version}`;
|
|
867
|
-
const request = opts.fetch ?? globalThis.fetch;
|
|
868
|
-
if (!request) throw new MindwireError("mindwire: fetch is unavailable; set daemonBin or MINDWIRE_DAEMON");
|
|
869
|
-
const [checksums, binary] = await Promise.all([request(`${release}/checksums.txt`), request(`${release}/${asset}`)]);
|
|
870
|
-
if (!checksums.ok || !binary.ok) {
|
|
871
|
-
if (!opts.releaseBaseUrl && !proc.process?.env?.MINDWIRE_RELEASE_BASE_URL) {
|
|
872
|
-
const latest = await request("https://api.github.com/repos/oblien/mindwire/releases/latest");
|
|
873
|
-
if (latest.ok) {
|
|
874
|
-
const tag = (await latest.json()).tag_name;
|
|
875
|
-
const fallback = typeof tag === "string" ? tag.replace(/^v/, "") : "";
|
|
876
|
-
if (/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(fallback) && fallback !== version) {
|
|
877
|
-
return ensureDaemonBinary({ ...opts, version: fallback });
|
|
878
|
-
}
|
|
879
|
-
}
|
|
880
|
-
}
|
|
881
|
-
throw new MindwireError(`mindwire: failed to download daemon v${version} for ${daemonPlatform}-${daemonArch}`);
|
|
882
|
-
}
|
|
883
|
-
const expected = checksumFor(await checksums.text(), asset);
|
|
884
|
-
if (!expected) throw new MindwireError(`mindwire: release v${version} has no checksum for ${asset}`);
|
|
885
|
-
const bytes = new Uint8Array(await binary.arrayBuffer());
|
|
886
|
-
return cacheExecutable2(bin, bytes, expected);
|
|
887
|
-
}
|
|
888
|
-
|
|
889
|
-
// src/embedded.ts
|
|
890
|
-
var shared = /* @__PURE__ */ new Map();
|
|
891
|
-
function configKey(opts) {
|
|
892
|
-
return JSON.stringify([opts.cwd ?? "", opts.statePath ?? "", opts.bin ?? ""]);
|
|
893
|
-
}
|
|
894
|
-
function startEmbedded(opts = {}) {
|
|
895
|
-
const key = configKey(opts);
|
|
896
|
-
let daemon = shared.get(key);
|
|
897
|
-
if (!daemon) {
|
|
898
|
-
daemon = spawnDaemon(opts).catch((err) => {
|
|
899
|
-
shared.delete(key);
|
|
900
|
-
throw err;
|
|
901
|
-
});
|
|
902
|
-
shared.set(key, daemon);
|
|
903
|
-
}
|
|
904
|
-
return daemon;
|
|
905
|
-
}
|
|
906
|
-
function serverRuntime() {
|
|
907
|
-
return globalThis;
|
|
908
|
-
}
|
|
909
|
-
function isServerRuntime() {
|
|
910
|
-
const g = serverRuntime();
|
|
911
|
-
return typeof g.process !== "undefined" && !!g.process.versions?.node || typeof g.Bun !== "undefined" || typeof g.Deno !== "undefined";
|
|
912
|
-
}
|
|
913
|
-
async function spawnDaemon(opts) {
|
|
914
|
-
if (!isServerRuntime()) {
|
|
915
|
-
throw new MindwireError(
|
|
916
|
-
"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."
|
|
917
|
-
);
|
|
918
|
-
}
|
|
919
|
-
const { spawn: spawn3 } = await import('child_process');
|
|
920
|
-
const net = await import('net');
|
|
921
|
-
const proc = serverRuntime().process;
|
|
922
|
-
const { randomBytes: randomBytes2 } = await import('crypto');
|
|
923
|
-
const resolved = await resolveBinary(opts.bin);
|
|
924
|
-
const bin = resolved.bin;
|
|
925
|
-
const port = await freePort(net);
|
|
926
|
-
const baseUrl = `http://127.0.0.1:${port}`;
|
|
927
|
-
const token = randomBytes2(32).toString("hex");
|
|
928
|
-
const child = spawn3(bin, [], {
|
|
929
|
-
env: {
|
|
930
|
-
...proc.env,
|
|
931
|
-
ADDR: `127.0.0.1:${port}`,
|
|
932
|
-
AGENT_CWD: opts.cwd ?? proc.cwd(),
|
|
933
|
-
STATE_PATH: opts.statePath ?? ".mindwire-state.json",
|
|
934
|
-
DAEMON_TOKEN: token
|
|
935
|
-
},
|
|
936
|
-
stdio: "ignore"
|
|
937
|
-
});
|
|
938
|
-
let spawnError = null;
|
|
939
|
-
child.on("error", (e) => {
|
|
940
|
-
spawnError = e;
|
|
941
|
-
});
|
|
942
|
-
const stop = () => {
|
|
943
|
-
try {
|
|
944
|
-
child.kill();
|
|
945
|
-
} catch {
|
|
946
|
-
}
|
|
947
|
-
};
|
|
948
|
-
proc.once?.("exit", stop);
|
|
949
|
-
proc.once?.("SIGINT", () => {
|
|
950
|
-
stop();
|
|
951
|
-
proc.exit?.(130);
|
|
952
|
-
});
|
|
953
|
-
await waitHealthy(baseUrl, token, () => spawnError, resolved);
|
|
954
|
-
return { baseUrl, token, stop };
|
|
955
|
-
}
|
|
956
|
-
async function resolveBinary(explicit) {
|
|
957
|
-
const fs2 = await import('fs');
|
|
958
|
-
const path4 = await import('path');
|
|
959
|
-
const proc = serverRuntime().process;
|
|
960
|
-
const ext = proc.platform === "win32" ? ".exe" : "";
|
|
961
|
-
const candidates = [];
|
|
962
|
-
if (explicit) candidates.push(explicit);
|
|
963
|
-
if (proc.env?.MINDWIRE_DAEMON) candidates.push(proc.env.MINDWIRE_DAEMON);
|
|
964
|
-
try {
|
|
965
|
-
const url = await import('url');
|
|
966
|
-
const here = path4.dirname(url.fileURLToPath(import.meta.url));
|
|
967
|
-
const named = `mindwired-${proc.platform}-${proc.arch}${ext}`;
|
|
968
|
-
candidates.push(path4.join(here, "bin", named), path4.join(here, "..", "bin", named));
|
|
969
|
-
} catch {
|
|
970
|
-
}
|
|
971
|
-
for (const c of candidates) {
|
|
972
|
-
try {
|
|
973
|
-
if (fs2.existsSync(c)) return { bin: c, found: true, checked: candidates };
|
|
974
|
-
} catch {
|
|
975
|
-
}
|
|
976
|
-
}
|
|
977
|
-
const downloaded = await ensureDaemonBinary({ platform: proc.platform, arch: proc.arch });
|
|
978
|
-
return { bin: downloaded, found: true, checked: [...candidates, downloaded] };
|
|
979
|
-
}
|
|
980
|
-
function daemonStartError(resolved, cause) {
|
|
981
|
-
if (!resolved.found) {
|
|
982
|
-
return [
|
|
983
|
-
"Couldn't start the mindwire daemon: the `mindwired` binary was not found.",
|
|
984
|
-
`Looked in: ${resolved.checked.join(", ")}.`,
|
|
985
|
-
"Fixes:",
|
|
986
|
-
" \u2022 check GitHub Release access so the SDK can download its matching daemon binary; or",
|
|
987
|
-
" \u2022 set MINDWIRE_DAEMON to a `mindwired` binary; or",
|
|
988
|
-
" \u2022 pass { baseUrl } to connect to a daemon you run yourself.",
|
|
989
|
-
" \u2022 Working inside the mindwire monorepo? Run `go build -o /tmp/mindwired ./daemon/cmd/daemon` and set MINDWIRE_DAEMON.",
|
|
990
|
-
`(underlying error: ${cause.message})`
|
|
991
|
-
].join("\n");
|
|
992
|
-
}
|
|
993
|
-
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.`;
|
|
994
|
-
}
|
|
995
|
-
function freePort(net) {
|
|
996
|
-
return new Promise((resolve3, reject) => {
|
|
997
|
-
const srv = net.createServer();
|
|
998
|
-
srv.on("error", reject);
|
|
999
|
-
srv.listen(0, "127.0.0.1", () => {
|
|
1000
|
-
const addr = srv.address();
|
|
1001
|
-
const port = addr && typeof addr === "object" ? addr.port : 0;
|
|
1002
|
-
srv.close(() => resolve3(port));
|
|
1003
|
-
});
|
|
1004
|
-
});
|
|
1005
|
-
}
|
|
1006
|
-
async function waitHealthy(baseUrl, token, getError, resolved, timeoutMs = 15e3) {
|
|
1007
|
-
const deadline = Date.now() + timeoutMs;
|
|
1008
|
-
while (Date.now() < deadline) {
|
|
1009
|
-
const err = getError();
|
|
1010
|
-
if (err) {
|
|
1011
|
-
throw new MindwireError(daemonStartError(resolved, err), { cause: err });
|
|
1012
|
-
}
|
|
1013
|
-
try {
|
|
1014
|
-
const res = await fetch(`${baseUrl}/healthz`, { headers: { Authorization: `Bearer ${token}` } });
|
|
1015
|
-
if (res.ok) return;
|
|
1016
|
-
} catch {
|
|
1017
|
-
}
|
|
1018
|
-
await new Promise((r) => setTimeout(r, 150));
|
|
1019
|
-
}
|
|
1020
|
-
throw new MindwireError("mindwire embedded daemon did not become healthy in time.");
|
|
1021
|
-
}
|
|
1022
|
-
|
|
1023
|
-
// src/target/index.ts
|
|
1024
|
-
function emit(spec, e) {
|
|
1025
|
-
if (!spec.onLog) return;
|
|
1026
|
-
try {
|
|
1027
|
-
spec.onLog(e);
|
|
1028
|
-
} catch {
|
|
1029
|
-
}
|
|
1030
|
-
}
|
|
1031
|
-
function local(opts = {}) {
|
|
1032
|
-
return {
|
|
1033
|
-
name: "local",
|
|
1034
|
-
async connect(spec) {
|
|
1035
|
-
const d = await startEmbedded(opts);
|
|
1036
|
-
emit(spec, { target: "local", phase: "ready", message: `embedded daemon on ${d.baseUrl}` });
|
|
1037
|
-
return {
|
|
1038
|
-
id: d.baseUrl,
|
|
1039
|
-
baseUrl: d.baseUrl,
|
|
1040
|
-
...d.token !== void 0 ? { token: d.token } : {},
|
|
1041
|
-
// No-op: the embedded daemon is shared (keyed memoization) and reaped on process exit.
|
|
1042
|
-
stop: async () => {
|
|
1043
|
-
}
|
|
1044
|
-
};
|
|
1045
|
-
}
|
|
1046
|
-
};
|
|
1047
|
-
}
|
|
1048
|
-
function remote(baseUrl, opts = {}) {
|
|
1049
|
-
return {
|
|
1050
|
-
name: "remote",
|
|
1051
|
-
async connect(spec) {
|
|
1052
|
-
emit(spec, { target: "remote", phase: "skip", message: `using remote daemon at ${baseUrl}` });
|
|
1053
|
-
return {
|
|
1054
|
-
id: baseUrl,
|
|
1055
|
-
baseUrl,
|
|
1056
|
-
...opts.token !== void 0 ? { token: opts.token } : {},
|
|
1057
|
-
...opts.headers !== void 0 ? { headers: opts.headers } : {},
|
|
1058
|
-
...opts.fetch !== void 0 ? { fetch: opts.fetch } : {},
|
|
1059
|
-
stop: async () => {
|
|
1060
|
-
}
|
|
1061
|
-
};
|
|
1062
|
-
}
|
|
1063
|
-
};
|
|
1064
|
-
}
|
|
1065
|
-
|
|
1066
|
-
// src/client.ts
|
|
1067
|
-
var handleByTransport = /* @__PURE__ */ new WeakMap();
|
|
1068
|
-
var Mindwire = class _Mindwire {
|
|
1069
|
-
/** Workspace registry: saved agent profiles, projects and chat relationships. */
|
|
1070
|
-
workspace;
|
|
1071
|
-
surfaces;
|
|
1072
|
-
service;
|
|
1073
|
-
execution;
|
|
1074
|
-
computer;
|
|
1075
|
-
http;
|
|
1076
|
-
/** The default agent type applied to agent-scoped calls, if set. */
|
|
1077
|
-
defaultAgent;
|
|
1078
|
-
/** Step-flow auth, scoped to this client's default agent (override per call). */
|
|
1079
|
-
auth;
|
|
1080
|
-
/** Persistent memory files + saved prompt templates, scoped to this client's default agent. */
|
|
1081
|
-
prompts;
|
|
1082
|
-
/** Persistent MCP-server config (the config an agent loads every run), scoped to this client's default agent. */
|
|
1083
|
-
mcp;
|
|
1084
|
-
/** Custom LLM-provider registration (opencode/Codex native config), scoped to this client's default agent. */
|
|
1085
|
-
providers;
|
|
1086
|
-
/** Daemon-driven notification channels + routing rules (webhook/slack/discord/telegram; per-agent/session/global). */
|
|
1087
|
-
notify;
|
|
1088
|
-
constructor(opts = {}) {
|
|
1089
|
-
const target = opts.target ?? local();
|
|
1090
|
-
const spec = { agent: opts.agent, onLog: opts.logger };
|
|
1091
|
-
this.http = new Http({
|
|
1092
|
-
fetch: opts.fetch,
|
|
1093
|
-
headers: opts.headers,
|
|
1094
|
-
timeoutMs: opts.requestTimeoutMs,
|
|
1095
|
-
resolveBase: () => {
|
|
1096
|
-
const h = target.connect(spec);
|
|
1097
|
-
handleByTransport.set(this.http, h);
|
|
1098
|
-
return h.then((x) => ({
|
|
1099
|
-
baseUrl: x.baseUrl,
|
|
1100
|
-
token: x.token,
|
|
1101
|
-
getToken: x.getToken,
|
|
1102
|
-
headers: x.headers,
|
|
1103
|
-
fetch: x.fetch
|
|
1104
|
-
}));
|
|
1105
|
-
}
|
|
1106
|
-
});
|
|
1107
|
-
this.defaultAgent = opts.agent;
|
|
1108
|
-
this.workspace = new WorkspaceApi(this);
|
|
1109
|
-
this.surfaces = new SurfacesApi(this);
|
|
1110
|
-
this.service = new ServiceApi(this);
|
|
1111
|
-
this.execution = new ExecutionApi(this);
|
|
1112
|
-
this.computer = new ComputerApi(this);
|
|
1113
|
-
this.auth = new AuthApi(this);
|
|
1114
|
-
this.prompts = new PromptsApi(this);
|
|
1115
|
-
this.mcp = new McpApi(this);
|
|
1116
|
-
this.providers = new ProvidersApi(this);
|
|
1117
|
-
this.notify = new NotifyApi(this);
|
|
1118
|
-
}
|
|
1119
|
-
/**
|
|
1120
|
-
* Provision the destination and await readiness **now**, rather than lazily on the first request —
|
|
1121
|
-
* useful to front-load a fresh SSH/Docker/Oblien box (and to stream its {@link EnsureEvent}s to the
|
|
1122
|
-
* `logger`) before the first `turn()`. Idempotent and memoized: repeated `ensure()` calls, and the
|
|
1123
|
-
* first real request, all await the same provisioning, so the target connects exactly once.
|
|
1124
|
-
*/
|
|
1125
|
-
async ensure() {
|
|
1126
|
-
await this.http.ready();
|
|
1127
|
-
}
|
|
1128
|
-
/** Return a new client bound to a different default agent (shares the same transport config). */
|
|
1129
|
-
withAgent(agent) {
|
|
1130
|
-
const clone = Object.create(_Mindwire.prototype);
|
|
1131
|
-
clone.http = this.http;
|
|
1132
|
-
clone.defaultAgent = agent;
|
|
1133
|
-
clone.workspace = new WorkspaceApi(clone);
|
|
1134
|
-
clone.surfaces = new SurfacesApi(clone);
|
|
1135
|
-
clone.service = new ServiceApi(clone);
|
|
1136
|
-
clone.execution = new ExecutionApi(clone);
|
|
1137
|
-
clone.computer = new ComputerApi(clone);
|
|
1138
|
-
clone.auth = new AuthApi(clone);
|
|
1139
|
-
clone.prompts = new PromptsApi(clone);
|
|
1140
|
-
clone.mcp = new McpApi(clone);
|
|
1141
|
-
clone.providers = new ProvidersApi(clone);
|
|
1142
|
-
clone.notify = new NotifyApi(clone);
|
|
1143
|
-
return clone;
|
|
1144
|
-
}
|
|
1145
|
-
/** The `?agent=` value to send for a scoped call: explicit override → client default → none. */
|
|
1146
|
-
agentParam(scoped) {
|
|
1147
|
-
const a = scoped?.agent ?? this.defaultAgent;
|
|
1148
|
-
return a ? { agent: a } : void 0;
|
|
1149
|
-
}
|
|
1150
|
-
/**
|
|
1151
|
-
* Release target-owned resources by calling the {@link TargetHandle}'s `stop()` — once, even across
|
|
1152
|
-
* {@link withAgent} clones that share the transport. For an `ssh`/`docker`/`oblien` target this reaps
|
|
1153
|
-
* the box (tear down the tunnel, stop or delete the container/workspace, per `stopOnExit`); a no-op
|
|
1154
|
-
* for `local`/`remote` (embedded self-cleans on process exit; remote is not ours to stop). Safe to
|
|
1155
|
-
* call before the target has connected (nothing to reap yet).
|
|
1156
|
-
*/
|
|
1157
|
-
async close() {
|
|
1158
|
-
const handle = handleByTransport.get(this.http);
|
|
1159
|
-
if (!handle) return;
|
|
1160
|
-
handleByTransport.delete(this.http);
|
|
1161
|
-
const h = await handle.catch(() => null);
|
|
1162
|
-
if (h) await h.stop();
|
|
1163
|
-
}
|
|
1164
|
-
// ---- health & catalog ----------------------------------------------------
|
|
1165
|
-
/** `GET /healthz` — authenticated liveness check. Resolves with the daemon's health payload when it is up. */
|
|
1166
|
-
health() {
|
|
1167
|
-
return this.http.request("GET", "/healthz");
|
|
1168
|
-
}
|
|
1169
|
-
/**
|
|
1170
|
-
* `GET /stats` — the daemon **process's** resource snapshot (heap in use, memory reserved from the
|
|
1171
|
-
* OS, goroutines, GC cycles, cores, platform, uptime). Cheap enough to call on demand — the daemon
|
|
1172
|
-
* reads its own Go runtime, not the machine — so a UI can fetch it when a user opens a daemon's page
|
|
1173
|
-
* without any background polling. See {@link Stats} for what each field means and doesn't.
|
|
1174
|
-
*/
|
|
1175
|
-
stats() {
|
|
1176
|
-
return this.http.request("GET", "/stats");
|
|
1177
|
-
}
|
|
1178
|
-
/** `GET /catalog` — every agent this daemon binary supports. */
|
|
1179
|
-
catalog() {
|
|
1180
|
-
return this.http.request("GET", "/catalog");
|
|
1181
|
-
}
|
|
1182
|
-
/** `GET /agent` — capabilities + settings schema + auth methods/status for the selected agent. */
|
|
1183
|
-
agent(scoped) {
|
|
1184
|
-
return this.http.request("GET", "/agent", { query: this.agentParam(scoped) });
|
|
1185
|
-
}
|
|
1186
|
-
/** `GET /agent/software` — installed version, compatibility and approved update target. */
|
|
1187
|
-
software(opts = {}) {
|
|
1188
|
-
return this.http.request("GET", "/agent/software", {
|
|
1189
|
-
query: { ...this.agentParam(opts), ...opts.refresh ? { refresh: "true" } : {} }
|
|
1190
|
-
});
|
|
1191
|
-
}
|
|
1192
|
-
/**
|
|
1193
|
-
* `GET /models` — the models the selected agent can run for the configured account. An empty array
|
|
1194
|
-
* is valid (no credentials yet / offline). Throws a 400 {@link ApiError} for an agent whose model is
|
|
1195
|
-
* free text (check `capabilities.models` first).
|
|
1196
|
-
*/
|
|
1197
|
-
models(scoped) {
|
|
1198
|
-
return this.http.request("GET", "/models", { query: this.agentParam(scoped) });
|
|
1199
|
-
}
|
|
1200
|
-
/** `GET /doctor` — daemon-level health plus the selected agent's own checks. */
|
|
1201
|
-
doctor(scoped) {
|
|
1202
|
-
return this.http.request("GET", "/doctor", { query: this.agentParam(scoped) });
|
|
1203
|
-
}
|
|
1204
|
-
// ---- toolchain setup -----------------------------------------------------
|
|
1205
|
-
/** `POST /setup` — start the agent's install toolchain (background; poll {@link setupStatus}). */
|
|
1206
|
-
setup(scoped) {
|
|
1207
|
-
return this.http.request("POST", "/setup", { query: this.agentParam(scoped) });
|
|
1208
|
-
}
|
|
1209
|
-
/** `POST /update` — install the catalog's newest tested version compatible with this daemon. */
|
|
1210
|
-
update(scoped) {
|
|
1211
|
-
return this.http.request("POST", "/update", { query: this.agentParam(scoped) });
|
|
1212
|
-
}
|
|
1213
|
-
/** `GET /setup` — current toolchain install progress. */
|
|
1214
|
-
setupStatus(scoped) {
|
|
1215
|
-
return this.http.request("GET", "/setup", { query: this.agentParam(scoped) });
|
|
1216
|
-
}
|
|
1217
|
-
// ---- config --------------------------------------------------------------
|
|
1218
|
-
/** `GET /config` — the declared, non-secret settings for the agent. */
|
|
1219
|
-
getConfig(scoped) {
|
|
1220
|
-
return this.http.request("GET", "/config", {
|
|
1221
|
-
query: this.agentParam(scoped)
|
|
1222
|
-
});
|
|
1223
|
-
}
|
|
1224
|
-
/** `PUT /config` — merge recognized (non-secret) setting keys. Unknown keys are ignored server-side. */
|
|
1225
|
-
async setConfig(values, scoped) {
|
|
1226
|
-
await this.http.request("PUT", "/config", {
|
|
1227
|
-
query: this.agentParam(scoped),
|
|
1228
|
-
body: values
|
|
1229
|
-
});
|
|
1230
|
-
}
|
|
1231
|
-
// ---- chats & history -----------------------------------------------------
|
|
1232
|
-
/** `GET /chats` — recorded chats (newest first), shared across agents. */
|
|
1233
|
-
chats() {
|
|
1234
|
-
return this.http.request("GET", "/chats");
|
|
1235
|
-
}
|
|
1236
|
-
/**
|
|
1237
|
-
* `PUT /chats/{id}` — rename a chat. The user title wins over the agent's native auto-title in
|
|
1238
|
-
* every listing; an empty title clears the rename (reverting to the native/derived title).
|
|
1239
|
-
* Returns the updated summary.
|
|
1240
|
-
*/
|
|
1241
|
-
renameChat(chatId, title) {
|
|
1242
|
-
return this.http.request("PUT", `/chats/${encodeURIComponent(chatId)}`, {
|
|
1243
|
-
body: { title }
|
|
1244
|
-
});
|
|
1245
|
-
}
|
|
1246
|
-
/**
|
|
1247
|
-
* `DELETE /chats/{id}` — a true, irreversible delete: purges ALL of the chat's mindwire
|
|
1248
|
-
* bookkeeping and, for every session the chat mapped to, removes that agent's native transcript
|
|
1249
|
-
* (the source of truth). Rejects with a 409 error if a turn is live. Native deletion is
|
|
1250
|
-
* best-effort per agent; the result reports what was purged vs. failed.
|
|
1251
|
-
*/
|
|
1252
|
-
deleteChat(chatId) {
|
|
1253
|
-
return this.http.request("DELETE", `/chats/${encodeURIComponent(chatId)}`);
|
|
1254
|
-
}
|
|
1255
|
-
/**
|
|
1256
|
-
* `POST /chats/{id}/fork` — clone a chat into a new id (generated when `newChatId` is omitted).
|
|
1257
|
-
* The fork shares the source's native session until its first turn, which branches it (natively
|
|
1258
|
-
* on Claude via `--fork-session`; a fresh session on agents without native fork). Rejects with a
|
|
1259
|
-
* 409 if the source has a live turn, 404 if the source is unknown, 400 if the target id is in
|
|
1260
|
-
* use. Returns the new chat's summary.
|
|
1261
|
-
*/
|
|
1262
|
-
forkChat(chatId, opts = {}) {
|
|
1263
|
-
return this.http.request("POST", `/chats/${encodeURIComponent(chatId)}/fork`, {
|
|
1264
|
-
body: opts.newChatId ? { newChatId: opts.newChatId } : {}
|
|
1265
|
-
});
|
|
1266
|
-
}
|
|
1267
|
-
/**
|
|
1268
|
-
* `GET /chats/{id}/messages` — a chat's transcript (native when the agent supports it, else
|
|
1269
|
-
* the recorded fallback). `limit` caps to the newest N; `before` pages older history.
|
|
1270
|
-
*/
|
|
1271
|
-
messages(chatId, opts = {}) {
|
|
1272
|
-
const query = { ...this.agentParam(opts) };
|
|
1273
|
-
if (opts.limit !== void 0) query["limit"] = opts.limit;
|
|
1274
|
-
if (opts.before !== void 0) query["before"] = opts.before;
|
|
1275
|
-
return this.http.request("GET", `/chats/${encodeURIComponent(chatId)}/messages`, {
|
|
1276
|
-
query
|
|
1277
|
-
});
|
|
1278
|
-
}
|
|
1279
|
-
/** `GET /chats/{id}/run` — the latest run for a chat (reattach anchor), or `null` if none yet. */
|
|
1280
|
-
async latestRun(chatId) {
|
|
1281
|
-
const data = await this.http.request(
|
|
1282
|
-
"GET",
|
|
1283
|
-
`/chats/${encodeURIComponent(chatId)}/run`
|
|
1284
|
-
);
|
|
1285
|
-
return data ? new Run(this.http, data) : null;
|
|
1286
|
-
}
|
|
1287
|
-
// ---- turns & runs --------------------------------------------------------
|
|
1288
|
-
/**
|
|
1289
|
-
* `POST /turns` — start a turn. Returns a {@link Run} handle you can stream, cancel, or await.
|
|
1290
|
-
* Rejects with an {@link ApiError} (409) if a turn is already running for the chat.
|
|
1291
|
-
*
|
|
1292
|
-
* `mode` defaults to `"turn"` — one agent turn that ends when the CLI settles. Pass
|
|
1293
|
-
* `mode: "resolve"` (with optional {@link ResolveOptions} `resolve` caps) for a global-resolve run
|
|
1294
|
-
* that auto-continues the agent's multi-step work until it's done; {@link Mindwire.resolve} is the
|
|
1295
|
-
* clearer entry point for that. The returned {@link Run} is then the parent of the run tree.
|
|
1296
|
-
*/
|
|
1297
|
-
async turn(input) {
|
|
1298
|
-
const body = {
|
|
1299
|
-
chatId: input.chatId,
|
|
1300
|
-
message: input.message
|
|
1301
|
-
};
|
|
1302
|
-
if (input.cwd !== void 0) body.cwd = input.cwd;
|
|
1303
|
-
if (input.options !== void 0) body.options = input.options;
|
|
1304
|
-
if (input.mode !== void 0) body.mode = input.mode;
|
|
1305
|
-
if (input.resolve !== void 0) body.resolve = input.resolve;
|
|
1306
|
-
if (input.gitAuth !== void 0) body.gitAuth = input.gitAuth;
|
|
1307
|
-
const data = await this.http.request("POST", "/turns", {
|
|
1308
|
-
query: this.agentParam(input),
|
|
1309
|
-
body
|
|
1310
|
-
});
|
|
1311
|
-
return new Run(this.http, data);
|
|
1312
|
-
}
|
|
1313
|
-
/**
|
|
1314
|
-
* `POST /turns {mode:"resolve"}` — start a **global-resolve** run: instead of returning after one
|
|
1315
|
-
* turn, the daemon holds the task open and auto-continues the agent (resuming on continuable stops
|
|
1316
|
-
* and probing for completion) until the work is globally resolved, then aggregates one final result.
|
|
1317
|
-
*
|
|
1318
|
-
* The returned {@link Run} is the **parent** of a run tree: each auto-continued iteration is a child
|
|
1319
|
-
* turn ({@link Run.children}) whose events stream onto the parent's topic, delimited by `continuation`
|
|
1320
|
-
* boundary events. `run.wait()` resolves once with the aggregated result; `run.stopReason` /
|
|
1321
|
-
* `run.iterations` report how the loop ended. Resolve turns run unattended (no mid-turn approvals)
|
|
1322
|
-
* and are bounded by {@link ResolveOptions} caps — see the resolve guide. Rejects with an
|
|
1323
|
-
* {@link ApiError} (409) if a turn is already running for the chat.
|
|
1324
|
-
*/
|
|
1325
|
-
async resolve(input) {
|
|
1326
|
-
return this.turn({ ...input, mode: "resolve" });
|
|
1327
|
-
}
|
|
1328
|
-
/** `GET /runs/{id}` — fetch an existing run as a {@link Run} handle. */
|
|
1329
|
-
async run(id) {
|
|
1330
|
-
const data = await this.http.request("GET", `/runs/${encodeURIComponent(id)}`);
|
|
1331
|
-
return new Run(this.http, data);
|
|
1332
|
-
}
|
|
1333
|
-
/**
|
|
1334
|
-
* `POST /chats/{id}/compact` — run an on-demand conversation compaction as a first-class {@link Run}
|
|
1335
|
-
* you can stream or await. The agent folds prior context into a summary it carries forward, emitting
|
|
1336
|
-
* a `compaction` event on the stream and recording the boundary in history exactly like an
|
|
1337
|
-
* auto-compaction. Optional `instructions` focus the continuation summary (Claude's
|
|
1338
|
-
* `/compact <instructions>`; agents that don't honor focus still compact). Rejects with an
|
|
1339
|
-
* {@link ApiError}: 400 if the agent doesn't support compaction (`capabilities.compactNow`) or the
|
|
1340
|
-
* chat has no conversation yet, 409 if a turn is already running for the chat.
|
|
1341
|
-
*/
|
|
1342
|
-
async compact(chatId, opts = {}) {
|
|
1343
|
-
const data = await this.http.request(
|
|
1344
|
-
"POST",
|
|
1345
|
-
`/chats/${encodeURIComponent(chatId)}/compact`,
|
|
1346
|
-
{
|
|
1347
|
-
query: this.agentParam(opts),
|
|
1348
|
-
body: opts.instructions ? { instructions: opts.instructions } : {}
|
|
1349
|
-
}
|
|
1350
|
-
);
|
|
1351
|
-
return new Run(this.http, data);
|
|
1352
|
-
}
|
|
1353
|
-
// ---- notifications -------------------------------------------------------
|
|
1354
|
-
/** `GET /notify/config` — whether a notification channel is wired (token never returned). */
|
|
1355
|
-
getNotifyConfig() {
|
|
1356
|
-
return this.http.request("GET", "/notify/config");
|
|
1357
|
-
}
|
|
1358
|
-
/** `PUT /notify/config` — store the provisioned notification channel (daemon-wide). */
|
|
1359
|
-
async setNotifyConfig(input) {
|
|
1360
|
-
await this.http.request("PUT", "/notify/config", { body: input });
|
|
1361
|
-
}
|
|
1362
|
-
/** `GET /notify/stream` — SSE feed of the daemon's notifications (replay, then live). */
|
|
1363
|
-
async *notifications(opts = {}) {
|
|
1364
|
-
const res = await this.http.open("GET", "/notify/stream", {
|
|
1365
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
1366
|
-
});
|
|
1367
|
-
yield* readSSE(res.body, opts.signal);
|
|
1368
|
-
}
|
|
1369
|
-
// ---- live resources ------------------------------------------------------
|
|
1370
|
-
/**
|
|
1371
|
-
* `GET /processes/stream` — SSE feed of live per-turn CPU/memory, one {@link ProcessFrame} per tick.
|
|
1372
|
-
* Sampling is **on demand**: the daemon starts measuring only while a client is connected and stops
|
|
1373
|
-
* the instant the last one disconnects, so aborting the `signal` (or ending the loop) tells the
|
|
1374
|
-
* daemon to stop — no background work, no leak. Pass `agent` to filter each frame's samples to one
|
|
1375
|
-
* agent type. Frames are live snapshots (no replay); an empty `samples` array is a valid keep-alive.
|
|
1376
|
-
*/
|
|
1377
|
-
async *processes(opts = {}) {
|
|
1378
|
-
const path4 = "/processes/stream" + (opts.agent ? `?agent=${encodeURIComponent(opts.agent)}` : "");
|
|
1379
|
-
const res = await this.http.open("GET", path4, {
|
|
1380
|
-
...opts.signal ? { signal: opts.signal } : {}
|
|
1381
|
-
});
|
|
1382
|
-
yield* readSSE(res.body, opts.signal);
|
|
1383
|
-
}
|
|
1384
|
-
};
|
|
1385
|
-
var AuthApi = class {
|
|
1386
|
-
constructor(mw) {
|
|
1387
|
-
this.mw = mw;
|
|
1388
|
-
}
|
|
1389
|
-
mw;
|
|
1390
|
-
/** `GET /auth/methods` — the options list to present. */
|
|
1391
|
-
methods(scoped) {
|
|
1392
|
-
return this.mw.http.request("GET", "/auth/methods", {
|
|
1393
|
-
query: this.mw.agentParam(scoped)
|
|
1394
|
-
});
|
|
1395
|
-
}
|
|
1396
|
-
/** `POST /auth/begin` — start one method (may return `{ url, code, fields, pending }`). */
|
|
1397
|
-
begin(method, scoped) {
|
|
1398
|
-
return this.mw.http.request("POST", "/auth/begin", {
|
|
1399
|
-
query: this.mw.agentParam(scoped),
|
|
1400
|
-
body: { method }
|
|
1401
|
-
});
|
|
1402
|
-
}
|
|
1403
|
-
/** `POST /auth/step` — submit fields, or poll an interactive login to completion. */
|
|
1404
|
-
step(input, scoped) {
|
|
1405
|
-
return this.mw.http.request("POST", "/auth/step", {
|
|
1406
|
-
query: this.mw.agentParam(scoped),
|
|
1407
|
-
body: input
|
|
1408
|
-
});
|
|
1409
|
-
}
|
|
1410
|
-
/** Read one interactive attempt, scoped by the returned AuthState.flowId. */
|
|
1411
|
-
poll(flowId, scoped) {
|
|
1412
|
-
return this.step({ _flowId: flowId }, scoped);
|
|
1413
|
-
}
|
|
1414
|
-
/** Cancel this native login without affecting a newer sign-in attempt. */
|
|
1415
|
-
cancel(flowId, scoped) {
|
|
1416
|
-
return this.step({ _flowId: flowId, _action: "cancel" }, scoped);
|
|
1417
|
-
}
|
|
1418
|
-
/** `GET /auth/status` — is the agent authenticated, and via which method. */
|
|
1419
|
-
status(scoped) {
|
|
1420
|
-
return this.mw.http.request("GET", "/auth/status", {
|
|
1421
|
-
query: this.mw.agentParam(scoped)
|
|
1422
|
-
});
|
|
1423
|
-
}
|
|
1424
|
-
/** `POST /auth/logout` — disconnect this harness once its running chats finish. */
|
|
1425
|
-
logout(scoped) {
|
|
1426
|
-
return this.mw.http.request("POST", "/auth/logout", {
|
|
1427
|
-
query: this.mw.agentParam(scoped)
|
|
1428
|
-
});
|
|
1429
|
-
}
|
|
1430
|
-
};
|
|
1431
|
-
var PromptsApi = class {
|
|
1432
|
-
constructor(mw) {
|
|
1433
|
-
this.mw = mw;
|
|
1434
|
-
}
|
|
1435
|
-
mw;
|
|
1436
|
-
query(scoped, extra) {
|
|
1437
|
-
return { ...this.mw.agentParam(scoped), ...extra };
|
|
1438
|
-
}
|
|
1439
|
-
/**
|
|
1440
|
-
* `GET /memory` — the agent's memory file at every supported scope (project + user for both
|
|
1441
|
-
* Claude and Codex). Each entry carries the resolved `path` and `exists`; `content` is `""` for
|
|
1442
|
-
* an absent file.
|
|
1443
|
-
*/
|
|
1444
|
-
memory(opts = {}) {
|
|
1445
|
-
return this.mw.http.request("GET", "/memory", {
|
|
1446
|
-
query: this.query(opts, { dir: opts.dir })
|
|
1447
|
-
});
|
|
1448
|
-
}
|
|
1449
|
-
/** `PUT /memory` — write the memory file at `scope`. Returns the resulting {@link MemoryDoc}. */
|
|
1450
|
-
setMemory(input, opts = {}) {
|
|
1451
|
-
return this.mw.http.request("PUT", "/memory", {
|
|
1452
|
-
query: this.query(opts, { dir: opts.dir }),
|
|
1453
|
-
body: input
|
|
1454
|
-
});
|
|
1455
|
-
}
|
|
1456
|
-
/**
|
|
1457
|
-
* `DELETE /memory` — remove the memory file at `scope` (defaults to `user`). Returns the resulting
|
|
1458
|
-
* {@link MemoryDoc} (`exists: false` at the resolved path). Idempotent: deleting an absent file still
|
|
1459
|
-
* succeeds.
|
|
1460
|
-
*/
|
|
1461
|
-
deleteMemory(opts = {}) {
|
|
1462
|
-
return this.mw.http.request("DELETE", "/memory", {
|
|
1463
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1464
|
-
});
|
|
1465
|
-
}
|
|
1466
|
-
/**
|
|
1467
|
-
* `GET /prompts` — saved prompt templates across every supported scope (Claude: project + user;
|
|
1468
|
-
* Codex: user only). `content` is omitted here; fetch it with {@link get}. A missing project
|
|
1469
|
-
* directory yields an empty list for that scope rather than an error.
|
|
1470
|
-
*/
|
|
1471
|
-
list(opts = {}) {
|
|
1472
|
-
return this.mw.http.request("GET", "/prompts", {
|
|
1473
|
-
query: this.query(opts, { dir: opts.dir })
|
|
1474
|
-
});
|
|
1475
|
-
}
|
|
1476
|
-
/** `GET /prompts/{name}` — one template's full body. Rejects with a 404 `ApiError` if absent. */
|
|
1477
|
-
get(name, opts = {}) {
|
|
1478
|
-
return this.mw.http.request("GET", `/prompts/${encodeURIComponent(name)}`, {
|
|
1479
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1480
|
-
});
|
|
1481
|
-
}
|
|
1482
|
-
/** `PUT /prompts/{name}` — create or overwrite a template. Returns the resulting {@link PromptTemplate}. */
|
|
1483
|
-
set(name, content, opts = {}) {
|
|
1484
|
-
return this.mw.http.request("PUT", `/prompts/${encodeURIComponent(name)}`, {
|
|
1485
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir }),
|
|
1486
|
-
body: { content }
|
|
1487
|
-
});
|
|
1488
|
-
}
|
|
1489
|
-
/**
|
|
1490
|
-
* `DELETE /prompts/{name}` — remove one template at `scope` (defaults to `user`). Idempotent:
|
|
1491
|
-
* deleting an absent template still succeeds. A traversal name rejects with a 400 `ApiError`.
|
|
1492
|
-
*/
|
|
1493
|
-
async delete(name, opts = {}) {
|
|
1494
|
-
await this.mw.http.request("DELETE", `/prompts/${encodeURIComponent(name)}`, {
|
|
1495
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1496
|
-
});
|
|
1497
|
-
}
|
|
1498
|
-
/**
|
|
1499
|
-
* `GET /subagents` — persistent subagent definitions (Claude `.claude/agents/*.md`) across every
|
|
1500
|
-
* supported scope. `content` is omitted here (fetch it with {@link subagent}); `meta` is the parsed
|
|
1501
|
-
* frontmatter view. Rejects with a 400 `ApiError` on an agent without the subagent-definition module.
|
|
1502
|
-
* Distinct from a turn's per-turn `subagents` passthrough — this is the on-disk definition store.
|
|
1503
|
-
*/
|
|
1504
|
-
subagents(opts = {}) {
|
|
1505
|
-
return this.mw.http.request("GET", "/subagents", {
|
|
1506
|
-
query: this.query(opts, { dir: opts.dir })
|
|
1507
|
-
});
|
|
1508
|
-
}
|
|
1509
|
-
/** `GET /subagents/{name}` — one definition's raw body + parsed meta. 404 `ApiError` if absent. */
|
|
1510
|
-
subagent(name, opts = {}) {
|
|
1511
|
-
return this.mw.http.request("GET", `/subagents/${encodeURIComponent(name)}`, {
|
|
1512
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1513
|
-
});
|
|
1514
|
-
}
|
|
1515
|
-
/** `PUT /subagents/{name}` — create or overwrite a definition (raw content is canonical). Returns it. */
|
|
1516
|
-
setSubagent(name, content, opts = {}) {
|
|
1517
|
-
return this.mw.http.request("PUT", `/subagents/${encodeURIComponent(name)}`, {
|
|
1518
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir }),
|
|
1519
|
-
body: { content }
|
|
1520
|
-
});
|
|
1521
|
-
}
|
|
1522
|
-
/**
|
|
1523
|
-
* `DELETE /subagents/{name}` — remove one definition at `scope` (defaults to `user`). Idempotent:
|
|
1524
|
-
* deleting an absent definition still succeeds. A traversal name rejects with a 400 `ApiError`.
|
|
1525
|
-
*/
|
|
1526
|
-
async deleteSubagent(name, opts = {}) {
|
|
1527
|
-
await this.mw.http.request("DELETE", `/subagents/${encodeURIComponent(name)}`, {
|
|
1528
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1529
|
-
});
|
|
1530
|
-
}
|
|
1531
|
-
};
|
|
1532
|
-
var McpApi = class {
|
|
1533
|
-
constructor(mw) {
|
|
1534
|
-
this.mw = mw;
|
|
1535
|
-
}
|
|
1536
|
-
mw;
|
|
1537
|
-
query(scoped, extra) {
|
|
1538
|
-
return { ...this.mw.agentParam(scoped), ...extra };
|
|
1539
|
-
}
|
|
1540
|
-
/**
|
|
1541
|
-
* `GET /mcp` — every persistent MCP server across the agent's supported scopes, keyed
|
|
1542
|
-
* `scope → name → server` (Claude: project + user; Codex: user only). A missing config file yields
|
|
1543
|
-
* an empty object for that scope rather than an error.
|
|
1544
|
-
*/
|
|
1545
|
-
list(opts = {}) {
|
|
1546
|
-
return this.mw.http.request("GET", "/mcp", {
|
|
1547
|
-
query: this.query(opts, { dir: opts.dir })
|
|
1548
|
-
});
|
|
1549
|
-
}
|
|
1550
|
-
/** `GET /mcp/{name}` — one server's definition. Rejects with a 404 `ApiError` if it isn't configured. */
|
|
1551
|
-
get(name, opts = {}) {
|
|
1552
|
-
return this.mw.http.request("GET", `/mcp/${encodeURIComponent(name)}`, {
|
|
1553
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1554
|
-
});
|
|
1555
|
-
}
|
|
1556
|
-
/** `PUT /mcp/{name}` — create or overwrite one server. Returns the stored definition. */
|
|
1557
|
-
set(name, server, opts = {}) {
|
|
1558
|
-
return this.mw.http.request("PUT", `/mcp/${encodeURIComponent(name)}`, {
|
|
1559
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir }),
|
|
1560
|
-
body: server
|
|
1561
|
-
});
|
|
1562
|
-
}
|
|
1563
|
-
/** `DELETE /mcp/{name}` — remove one server. Idempotent: deleting an absent server still succeeds. */
|
|
1564
|
-
async delete(name, opts = {}) {
|
|
1565
|
-
await this.mw.http.request("DELETE", `/mcp/${encodeURIComponent(name)}`, {
|
|
1566
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1567
|
-
});
|
|
1568
|
-
}
|
|
1569
|
-
};
|
|
1570
|
-
var ProvidersApi = class {
|
|
1571
|
-
constructor(mw) {
|
|
1572
|
-
this.mw = mw;
|
|
1573
|
-
}
|
|
1574
|
-
mw;
|
|
1575
|
-
query(scoped, extra) {
|
|
1576
|
-
return { ...this.mw.agentParam(scoped), ...extra };
|
|
1577
|
-
}
|
|
1578
|
-
/**
|
|
1579
|
-
* `GET /providers` — every registered custom provider across the agent's supported scopes, keyed
|
|
1580
|
-
* `scope → id → provider` (opencode/Codex: user only). A missing config file yields an empty object for
|
|
1581
|
-
* that scope rather than an error. `hasKey` reports whether a secret is stored; the key is never returned.
|
|
1582
|
-
*/
|
|
1583
|
-
list(opts = {}) {
|
|
1584
|
-
return this.mw.http.request("GET", "/providers", {
|
|
1585
|
-
query: this.query(opts, { dir: opts.dir })
|
|
1586
|
-
});
|
|
1587
|
-
}
|
|
1588
|
-
/** `GET /providers/{id}` — one provider's definition. Rejects with a 404 `ApiError` if it isn't configured. */
|
|
1589
|
-
get(id, opts = {}) {
|
|
1590
|
-
return this.mw.http.request("GET", `/providers/${encodeURIComponent(id)}`, {
|
|
1591
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1592
|
-
});
|
|
1593
|
-
}
|
|
1594
|
-
/**
|
|
1595
|
-
* `PUT /providers/{id}` — create or overwrite one provider, returning the stored definition (with
|
|
1596
|
-
* `hasKey` and the stored `envVars`). Two write-only secret channels, both optional: `opts.apiKey` is a
|
|
1597
|
-
* single key (custom endpoints, single-key catalog brands); `opts.secrets` is a NAME→VALUE map for a
|
|
1598
|
-
* catalog provider whose entry declares MULTIPLE env vars (e.g. AWS Bedrock). Omitting both leaves any
|
|
1599
|
-
* previously stored secret intact. The path `id` wins over any `id` on the provider value.
|
|
1600
|
-
*/
|
|
1601
|
-
set(id, provider, opts = {}) {
|
|
1602
|
-
return this.mw.http.request("PUT", `/providers/${encodeURIComponent(id)}`, {
|
|
1603
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir }),
|
|
1604
|
-
body: { ...provider, id, apiKey: opts.apiKey, secrets: opts.secrets }
|
|
1605
|
-
});
|
|
1606
|
-
}
|
|
1607
|
-
/** `DELETE /providers/{id}` — remove one provider and clear its stored key. Idempotent. */
|
|
1608
|
-
async delete(id, opts = {}) {
|
|
1609
|
-
await this.mw.http.request("DELETE", `/providers/${encodeURIComponent(id)}`, {
|
|
1610
|
-
query: this.query(opts, { scope: opts.scope, dir: opts.dir })
|
|
1611
|
-
});
|
|
1612
|
-
}
|
|
1613
|
-
};
|
|
1614
|
-
var NotifyApi = class {
|
|
1615
|
-
constructor(mw) {
|
|
1616
|
-
this.mw = mw;
|
|
1617
|
-
}
|
|
1618
|
-
mw;
|
|
1619
|
-
/** `GET /notify/channels` — every channel, masked (no secrets). */
|
|
1620
|
-
channels() {
|
|
1621
|
-
return this.mw.http.request("GET", "/notify/channels");
|
|
1622
|
-
}
|
|
1623
|
-
/** `POST /notify/channels` — create a channel (server-assigns the id). Returns it masked. */
|
|
1624
|
-
createChannel(input) {
|
|
1625
|
-
return this.mw.http.request("POST", "/notify/channels", { body: input });
|
|
1626
|
-
}
|
|
1627
|
-
/**
|
|
1628
|
-
* `PUT /notify/channels/{id}` — update a channel, merge-preserving any omitted secret
|
|
1629
|
-
* (`url`/`token`/`secret`). Returns the updated masked channel. 404 `ApiError` if unknown.
|
|
1630
|
-
*/
|
|
1631
|
-
setChannel(id, input) {
|
|
1632
|
-
return this.mw.http.request("PUT", `/notify/channels/${encodeURIComponent(id)}`, {
|
|
1633
|
-
body: input
|
|
1634
|
-
});
|
|
1635
|
-
}
|
|
1636
|
-
/** `DELETE /notify/channels/{id}` — remove a channel. Idempotent. */
|
|
1637
|
-
async deleteChannel(id) {
|
|
1638
|
-
await this.mw.http.request("DELETE", `/notify/channels/${encodeURIComponent(id)}`);
|
|
1639
|
-
}
|
|
1640
|
-
/**
|
|
1641
|
-
* `POST /notify/channels/{id}/test` — deliver a synthetic notification to a channel. A failed
|
|
1642
|
-
* delivery is DATA (`{ ok: false, error }`), not a thrown error; only an unknown id rejects (404).
|
|
1643
|
-
*/
|
|
1644
|
-
testChannel(id) {
|
|
1645
|
-
return this.mw.http.request(
|
|
1646
|
-
"POST",
|
|
1647
|
-
`/notify/channels/${encodeURIComponent(id)}/test`
|
|
1648
|
-
);
|
|
1649
|
-
}
|
|
1650
|
-
/** `GET /notify/rules` — every routing rule. */
|
|
1651
|
-
rules() {
|
|
1652
|
-
return this.mw.http.request("GET", "/notify/rules");
|
|
1653
|
-
}
|
|
1654
|
-
/** `POST /notify/rules` — create a rule (server-assigns the id). Returns it. */
|
|
1655
|
-
createRule(input) {
|
|
1656
|
-
return this.mw.http.request("POST", "/notify/rules", { body: input });
|
|
1657
|
-
}
|
|
1658
|
-
/** `PUT /notify/rules/{id}` — replace a rule. Returns it. 404 `ApiError` if unknown. */
|
|
1659
|
-
setRule(id, input) {
|
|
1660
|
-
return this.mw.http.request("PUT", `/notify/rules/${encodeURIComponent(id)}`, {
|
|
1661
|
-
body: input
|
|
1662
|
-
});
|
|
1663
|
-
}
|
|
1664
|
-
/** `DELETE /notify/rules/{id}` — remove a rule. Idempotent. */
|
|
1665
|
-
async deleteRule(id) {
|
|
1666
|
-
await this.mw.http.request("DELETE", `/notify/rules/${encodeURIComponent(id)}`);
|
|
1667
|
-
}
|
|
1668
|
-
};
|
|
1669
|
-
var CLOUDFLARED_VERSION = "2026.9.3";
|
|
1670
|
-
var execute = promisify(execFile);
|
|
1671
|
-
async function ensureCloudflareBinary(options = {}) {
|
|
1672
|
-
const platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
|
|
1673
|
-
const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : "";
|
|
1674
|
-
if (!cpu || !["darwin", "linux", "win32"].includes(platform) || platform === "win32" && cpu !== "amd64") {
|
|
1675
|
-
throw new Error("Install cloudflared for this computer, or select another connection provider.");
|
|
1676
|
-
}
|
|
1677
|
-
const asset = `cloudflared-${platform === "win32" ? "windows" : platform}-${cpu}${platform === "darwin" ? ".tgz" : platform === "win32" ? ".exe" : ""}`;
|
|
1678
|
-
const bin = path2.join(
|
|
1679
|
-
options.cacheDir ?? await binaryCacheDirectory(),
|
|
1680
|
-
"cloudflared",
|
|
1681
|
-
CLOUDFLARED_VERSION,
|
|
1682
|
-
`${platform}-${arch}`,
|
|
1683
|
-
platform === "win32" ? "cloudflared.exe" : "cloudflared"
|
|
1684
|
-
);
|
|
1685
|
-
const cached = await cachedExecutable(bin);
|
|
1686
|
-
if (cached) return cached;
|
|
1687
|
-
await options.onProgress?.("Downloading the secure connection helper\u2026");
|
|
1688
|
-
const request = options.fetch ?? fetch;
|
|
1689
|
-
const release = await request(
|
|
1690
|
-
`https://api.github.com/repos/cloudflare/cloudflared/releases/tags/${CLOUDFLARED_VERSION}`,
|
|
1691
|
-
{ headers: { Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(2e4) }
|
|
1692
|
-
);
|
|
1693
|
-
if (!release.ok) throw new Error("Couldn't download the connection helper. Retry, install cloudflared yourself, or choose another provider.");
|
|
1694
|
-
const metadata = await release.json();
|
|
1695
|
-
const item = metadata.assets?.find((value) => value.name === asset);
|
|
1696
|
-
const checksum = item?.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1];
|
|
1697
|
-
const expectedURL = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset}`;
|
|
1698
|
-
if (!checksum || item?.browser_download_url !== expectedURL) throw new Error("The connection helper has no verified release asset for this computer.");
|
|
1699
|
-
const downloaded = await request(expectedURL, { signal: AbortSignal.timeout(12e4) });
|
|
1700
|
-
if (!downloaded.ok) throw new Error("Couldn't download the connection helper. Check your internet connection and retry.");
|
|
1701
|
-
const archive = new Uint8Array(await downloaded.arrayBuffer());
|
|
1702
|
-
await verifyReleaseBytes(archive, checksum, asset);
|
|
1703
|
-
let executable = archive;
|
|
1704
|
-
if (platform === "darwin") {
|
|
1705
|
-
const temporary = await mkdtemp(path2.join(tmpdir(), "mindwire-cloudflared-"));
|
|
1706
|
-
try {
|
|
1707
|
-
const archivePath = path2.join(temporary, asset);
|
|
1708
|
-
await writeFile(archivePath, archive, { mode: 384 });
|
|
1709
|
-
const result = await execute(
|
|
1710
|
-
"/usr/bin/tar",
|
|
1711
|
-
["-xzOf", archivePath, "cloudflared"],
|
|
1712
|
-
{ encoding: "buffer", maxBuffer: 128 * 1024 * 1024, timeout: 3e4 }
|
|
1713
|
-
);
|
|
1714
|
-
executable = new Uint8Array(result.stdout);
|
|
1715
|
-
} finally {
|
|
1716
|
-
await rm(temporary, { recursive: true, force: true });
|
|
1717
|
-
}
|
|
1718
|
-
}
|
|
1719
|
-
return cacheExecutable(bin, executable, await executableChecksum(executable));
|
|
1720
|
-
}
|
|
1721
|
-
async function cloudflareCommand(options) {
|
|
1722
|
-
try {
|
|
1723
|
-
await execute("cloudflared", ["--version"], { timeout: 3e3, windowsHide: true });
|
|
1724
|
-
return "cloudflared";
|
|
1725
|
-
} catch (error) {
|
|
1726
|
-
if (error.code !== "ENOENT") throw new Error("The installed cloudflared could not start. Repair it or choose another connection provider.");
|
|
1727
|
-
return ensureCloudflareBinary(options);
|
|
1728
|
-
}
|
|
16
|
+
var enterScreen = "\x1B[?1049h\x1B[?25l";
|
|
17
|
+
var leaveScreen = "\x1B[0m\x1B[?25h\x1B[?1049l";
|
|
18
|
+
var clearScreen = "\x1B[H\x1B[2J";
|
|
19
|
+
var scanInstruction = "Mindwire \u2192 Add computer \u2192 Scan QR code";
|
|
20
|
+
async function createPairingQR(invitation) {
|
|
21
|
+
const QR = await import('qrcode');
|
|
22
|
+
const data = computerPairingQRData(invitation);
|
|
23
|
+
const options = { errorCorrectionLevel: "L", margin: 4 };
|
|
24
|
+
const { modules } = QR.create(data, options);
|
|
25
|
+
const width = modules.size + options.margin * 2;
|
|
26
|
+
const black = (x, y) => x >= 0 && y >= 0 && x < modules.size && y < modules.size && !!modules.data[y * modules.size + x];
|
|
27
|
+
const lines = [];
|
|
28
|
+
for (let y = -options.margin; y < modules.size + options.margin; y += 2) {
|
|
29
|
+
let line = "";
|
|
30
|
+
for (let x = -options.margin; x < modules.size + options.margin; x++) {
|
|
31
|
+
const top = black(x, y), bottom = black(x, y + 1);
|
|
32
|
+
line += top ? bottom ? "\u2588" : "\u2580" : bottom ? "\u2584" : " ";
|
|
33
|
+
}
|
|
34
|
+
lines.push("\x1B[30;107m" + line + "\x1B[0m");
|
|
35
|
+
}
|
|
36
|
+
const svg = await QR.toString(data, { ...options, type: "svg" });
|
|
37
|
+
return { lines, columns: Math.max(width, scanInstruction.length) + 1, rows: lines.length + 5, svg };
|
|
38
|
+
}
|
|
39
|
+
function escapeHTML(value) {
|
|
40
|
+
return value.replace(/[&<>"']/g, (ch) => ({ "&": "&", "<": "<", ">": ">", '"': """, "'": "'" })[ch]);
|
|
41
|
+
}
|
|
42
|
+
function browserPage(invitation, svg) {
|
|
43
|
+
const reconnect = "mode" in invitation && invitation.mode === "reconnect";
|
|
44
|
+
const title = reconnect ? "Reconnect your phone" : "Connect your phone";
|
|
45
|
+
return `<!doctype html>
|
|
46
|
+
<html lang="en"><head><meta charset="utf-8"><meta name="viewport" content="width=device-width,initial-scale=1">
|
|
47
|
+
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; style-src 'unsafe-inline'; script-src 'unsafe-inline'; base-uri 'none'; form-action 'none'">
|
|
48
|
+
<title>${title} \xB7 Mindwire</title>
|
|
49
|
+
<style>
|
|
50
|
+
*{box-sizing:border-box}body{margin:0;background:#141416;color:#f5f5f7;font:16px/1.5 system-ui,sans-serif}
|
|
51
|
+
main{width:min(100% - 40px,520px);margin:32px auto;text-align:center}small{color:#a6a6af;letter-spacing:.16em}
|
|
52
|
+
h1{font-size:28px;letter-spacing:-.04em;line-height:1.2;margin:16px 0 6px}p{color:#b7b7bf;margin:8px 0;overflow-wrap:anywhere}
|
|
53
|
+
.code{width:min(100%,480px);margin:24px auto 16px;background:#fff;border-radius:16px;padding:8px}
|
|
54
|
+
svg{display:block;width:100%;height:auto}.steps{margin:20px 0}.steps p{color:#e1e1e6}
|
|
55
|
+
button{font:inherit;font-weight:600;color:#f5f5f7;border:1px solid #45454b;background:#252528;border-radius:10px;padding:10px 18px;cursor:pointer}
|
|
56
|
+
button:disabled{opacity:.45;cursor:default}button:focus-visible{outline:3px solid #a4b8ef;outline-offset:3px}
|
|
57
|
+
textarea{width:100%;margin-top:12px;padding:8px;color:inherit;background:#252528;border:1px solid #45454b;border-radius:8px}
|
|
58
|
+
[hidden]{display:none!important}#expiry{font-size:14px}footer{font-size:13px;color:#93939d;margin:18px 0}
|
|
59
|
+
</style></head><body><main>
|
|
60
|
+
<small>MINDWIRE</small><h1>${title}</h1><p>${escapeHTML(invitation.name)}</p>
|
|
61
|
+
<div class="code" id="code" role="img" aria-label="Mindwire connection QR code">${svg}</div>
|
|
62
|
+
<p id="expiry" data-expires="${escapeHTML(invitation.expiresAt)}" aria-live="off"></p>
|
|
63
|
+
<div class="steps"><p>In Mindwire, open <strong>Add computer</strong> and scan.</p>
|
|
64
|
+
<p>${reconnect ? "Your saved pairing and workspace stay the same. No new approval is needed." : "Then return to the terminal to approve your phone."}</p></div>
|
|
65
|
+
<button type="button" id="copy">Copy ${reconnect ? "reconnect" : "pairing"} link</button>
|
|
66
|
+
<textarea id="link" readonly hidden aria-label="Pairing link">${escapeHTML(computerPairingURI(invitation))}</textarea>
|
|
67
|
+
<footer>You can close this page after ${reconnect ? "reconnecting" : "pairing"}.</footer>
|
|
68
|
+
</main><script>
|
|
69
|
+
const expiry=document.getElementById('expiry'),code=document.getElementById('code'),copy=document.getElementById('copy'),link=document.getElementById('link');
|
|
70
|
+
function tick(){const seconds=Math.max(0,Math.ceil((Date.parse(expiry.dataset.expires)-Date.now())/1000));
|
|
71
|
+
if(!seconds){code.remove();copy.disabled=true;link.value='';link.hidden=true;expiry.textContent='Code expired. Run mindwire ${reconnect ? "reconnect" : "connect pair"} for a new code.';return;}
|
|
72
|
+
expiry.textContent='Expires in '+Math.floor(seconds/60)+':'+String(seconds%60).padStart(2,'0');}
|
|
73
|
+
tick();setInterval(tick,1000);
|
|
74
|
+
copy.addEventListener('click',async()=>{try{await navigator.clipboard.writeText(link.value);copy.textContent='Copied';}
|
|
75
|
+
catch{link.hidden=false;link.focus();link.select();copy.textContent='Select and copy the link';}});
|
|
76
|
+
</script></body></html>`;
|
|
77
|
+
}
|
|
78
|
+
async function openBrowser(url) {
|
|
79
|
+
const [command, args] = process.platform === "darwin" ? ["open", [url]] : process.platform === "win32" ? ["rundll32.exe", ["url.dll,FileProtocolHandler", url]] : ["xdg-open", [url]];
|
|
80
|
+
return new Promise((resolve2) => {
|
|
81
|
+
const child = spawn(command, args, { stdio: "ignore", windowsHide: true });
|
|
82
|
+
const finish = (ok) => {
|
|
83
|
+
clearTimeout(timer);
|
|
84
|
+
resolve2(ok);
|
|
85
|
+
};
|
|
86
|
+
const timer = setTimeout(() => {
|
|
87
|
+
child.kill();
|
|
88
|
+
finish(false);
|
|
89
|
+
}, 5e3);
|
|
90
|
+
child.once("error", () => finish(false));
|
|
91
|
+
child.once("exit", (code) => finish(code === 0));
|
|
92
|
+
});
|
|
1729
93
|
}
|
|
94
|
+
async function showPairingInvitation(invitation, options = {}) {
|
|
95
|
+
const reconnect = "mode" in invitation && invitation.mode === "reconnect";
|
|
96
|
+
const nextStep = reconnect ? "Scan with a saved phone, then press Enter here." : "Return here to approve your phone.";
|
|
97
|
+
const waiting = reconnect ? "Your saved pairing stays the same." : "Waiting for your phone\u2026";
|
|
98
|
+
const command = reconnect ? "mindwire reconnect" : "mindwire connect pair";
|
|
99
|
+
const output = options.output ?? process.stdout;
|
|
100
|
+
const environment = options.environment ?? process.env;
|
|
101
|
+
const mode = options.mode ?? "auto";
|
|
102
|
+
const interactive = !!output.isTTY && environment.TERM !== "dumb";
|
|
103
|
+
const linkOnly = mode === "none" || !interactive && mode !== "browser";
|
|
104
|
+
if (linkOnly) {
|
|
105
|
+
output.write(`Paste this link in Mindwire \u2192 Add computer (expires in 5 minutes):
|
|
106
|
+
${computerPairingURI(invitation)}
|
|
1730
107
|
|
|
1731
|
-
|
|
1732
|
-
|
|
1733
|
-
|
|
1734
|
-
if (url.protocol === "https:") url.protocol = "wss:";
|
|
1735
|
-
if (url.protocol !== "wss:" || !url.hostname || url.username || url.password || url.hash) {
|
|
1736
|
-
throw new Error("Relay URLs must use wss:// with no embedded credentials or fragment.");
|
|
1737
|
-
}
|
|
1738
|
-
if (!url.pathname || url.pathname === "/") url.pathname = "/ssh";
|
|
1739
|
-
return url.toString();
|
|
1740
|
-
}
|
|
1741
|
-
async function startRelay(options, port, runtime = {}) {
|
|
1742
|
-
if (options.kind === "none") return { close() {
|
|
1743
|
-
} };
|
|
1744
|
-
if (options.kind === "custom") {
|
|
1745
|
-
if (!options.url) throw new Error("A custom relay needs --relay-url wss://your-host/ssh.");
|
|
1746
|
-
return { route: { kind: "websocket", url: websocketURL(options.url) }, close() {
|
|
108
|
+
${waiting}
|
|
109
|
+
`);
|
|
110
|
+
return { close() {
|
|
1747
111
|
} };
|
|
1748
112
|
}
|
|
1749
|
-
const
|
|
1750
|
-
const
|
|
1751
|
-
let
|
|
1752
|
-
|
|
1753
|
-
|
|
1754
|
-
|
|
1755
|
-
|
|
1756
|
-
|
|
1757
|
-
|
|
1758
|
-
|
|
113
|
+
const qr = await createPairingQR(invitation);
|
|
114
|
+
const useScreen = interactive && mode !== "browser";
|
|
115
|
+
let closed = false, pageDirectory;
|
|
116
|
+
let browser = "idle";
|
|
117
|
+
let pageURL;
|
|
118
|
+
let browserAttempt;
|
|
119
|
+
const fits = () => (output.columns ?? 0) >= qr.columns && (output.rows ?? 0) >= qr.rows;
|
|
120
|
+
const draw = () => {
|
|
121
|
+
if (closed || !useScreen) return;
|
|
122
|
+
let lines;
|
|
123
|
+
if (fits()) {
|
|
124
|
+
lines = [scanInstruction, "", ...qr.lines, "", waiting];
|
|
1759
125
|
} else {
|
|
1760
|
-
|
|
1761
|
-
|
|
126
|
+
const size = `Terminal QR needs ${qr.columns} columns \xD7 ${qr.rows} rows.`;
|
|
127
|
+
lines = browser === "opened" ? ["QR opened in your browser.", scanInstruction, nextStep] : browser === "opening" ? ["Opening a resizable QR in your browser\u2026", nextStep] : browser === "failed" ? ["Couldn't open a browser here.", size, "Enlarge this terminal, or run:", `${command} --no-qr`] : [size, "Enlarge this terminal, or use --qr browser."];
|
|
128
|
+
lines = lines.map((line) => line.slice(0, Math.max(0, (output.columns ?? 80) - 1))).slice(0, Math.max(0, (output.rows ?? 24) - 1));
|
|
1762
129
|
}
|
|
1763
|
-
|
|
1764
|
-
|
|
1765
|
-
|
|
1766
|
-
if (
|
|
1767
|
-
|
|
1768
|
-
|
|
130
|
+
output.write(clearScreen + lines.join("\r\n"));
|
|
131
|
+
};
|
|
132
|
+
const ensureBrowser = () => {
|
|
133
|
+
if (browserAttempt) return browserAttempt;
|
|
134
|
+
browser = "opening";
|
|
135
|
+
draw();
|
|
136
|
+
browserAttempt = (async () => {
|
|
137
|
+
try {
|
|
138
|
+
pageDirectory = mkdtempSync(join(tmpdir(), "mindwire-pair-"));
|
|
139
|
+
chmodSync(pageDirectory, 448);
|
|
140
|
+
const file = join(pageDirectory, "pair.html");
|
|
141
|
+
writeFileSync(file, browserPage(invitation, qr.svg), { mode: 384 });
|
|
142
|
+
pageURL = pathToFileURL(file).href;
|
|
143
|
+
const remoteShell = !!(environment.SSH_CONNECTION || environment.SSH_CLIENT || environment.SSH_TTY);
|
|
144
|
+
const opened = (mode === "browser" || !remoteShell) && await (options.openBrowser ?? openBrowser)(pageURL);
|
|
145
|
+
browser = opened ? "opened" : "failed";
|
|
146
|
+
} catch {
|
|
147
|
+
browser = "failed";
|
|
148
|
+
}
|
|
149
|
+
if (closed) return;
|
|
150
|
+
if (useScreen) draw();
|
|
151
|
+
else output.write(browser === "opened" ? `QR opened in your browser.
|
|
152
|
+
${scanInstruction}
|
|
153
|
+
${nextStep}
|
|
154
|
+
` : `Open this local QR page in a browser:
|
|
155
|
+
${pageURL ?? `Page unavailable. Run ${command} --no-qr for a link.`}
|
|
156
|
+
|
|
157
|
+
${waiting}
|
|
158
|
+
`);
|
|
159
|
+
})();
|
|
160
|
+
return browserAttempt;
|
|
161
|
+
};
|
|
162
|
+
const resize = () => {
|
|
163
|
+
draw();
|
|
164
|
+
if (!closed && mode === "auto" && !fits()) void ensureBrowser();
|
|
165
|
+
};
|
|
166
|
+
const close = () => {
|
|
167
|
+
if (closed) return;
|
|
168
|
+
closed = true;
|
|
169
|
+
output.off("resize", resize);
|
|
170
|
+
process.off("exit", close);
|
|
171
|
+
process.off("SIGINT", interrupt);
|
|
172
|
+
process.off("SIGTERM", terminate);
|
|
173
|
+
process.off("SIGHUP", hangup);
|
|
174
|
+
if (useScreen) output.write(leaveScreen);
|
|
175
|
+
if (pageDirectory) {
|
|
176
|
+
try {
|
|
177
|
+
rmSync(pageDirectory, { recursive: true, force: true });
|
|
178
|
+
} catch {
|
|
179
|
+
}
|
|
1769
180
|
}
|
|
181
|
+
};
|
|
182
|
+
const interrupt = () => {
|
|
183
|
+
close();
|
|
184
|
+
process.exit(130);
|
|
185
|
+
};
|
|
186
|
+
const terminate = () => {
|
|
187
|
+
close();
|
|
188
|
+
process.exit(143);
|
|
189
|
+
};
|
|
190
|
+
const hangup = () => {
|
|
191
|
+
close();
|
|
192
|
+
process.exit(129);
|
|
193
|
+
};
|
|
194
|
+
process.once("exit", close);
|
|
195
|
+
process.once("SIGINT", interrupt);
|
|
196
|
+
process.once("SIGTERM", terminate);
|
|
197
|
+
process.once("SIGHUP", hangup);
|
|
198
|
+
if (useScreen) {
|
|
199
|
+
output.write(enterScreen);
|
|
200
|
+
output.on("resize", resize);
|
|
201
|
+
draw();
|
|
1770
202
|
}
|
|
1771
|
-
const child = spawn(command, args, { env, stdio: ["ignore", "pipe", "pipe"], windowsHide: true });
|
|
1772
|
-
let buffer = "";
|
|
1773
203
|
try {
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
cleanup();
|
|
1777
|
-
reject(new Error(`${command} did not establish its tunnel. Check its account and tunnel configuration.`));
|
|
1778
|
-
}, 45e3);
|
|
1779
|
-
const cleanup = () => {
|
|
1780
|
-
clearTimeout(timer);
|
|
1781
|
-
child.off("error", fail);
|
|
1782
|
-
child.off("exit", exited);
|
|
1783
|
-
};
|
|
1784
|
-
const fail = (error) => {
|
|
1785
|
-
cleanup();
|
|
1786
|
-
reject(new Error(`Could not run ${command}. Install it on the computer first.`, { cause: error }));
|
|
1787
|
-
};
|
|
1788
|
-
const exited = () => {
|
|
1789
|
-
cleanup();
|
|
1790
|
-
reject(new Error(`${command} exited before the tunnel connected. Check its account configuration.`));
|
|
1791
|
-
};
|
|
1792
|
-
let cloudflareAddress = options.url;
|
|
1793
|
-
let cloudflareConnected = false;
|
|
1794
|
-
const receive = (chunk) => {
|
|
1795
|
-
const received = buffer + chunk.toString();
|
|
1796
|
-
buffer = received.slice(-16384);
|
|
1797
|
-
let found;
|
|
1798
|
-
if (options.kind === "cloudflare") {
|
|
1799
|
-
cloudflareAddress ??= received.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
|
|
1800
|
-
cloudflareConnected ||= /Registered tunnel connection/.test(received);
|
|
1801
|
-
if (cloudflareConnected) found = cloudflareAddress;
|
|
1802
|
-
} else {
|
|
1803
|
-
for (const line of received.split(/\r?\n/)) {
|
|
1804
|
-
try {
|
|
1805
|
-
const event = JSON.parse(line);
|
|
1806
|
-
if (event.msg === "started tunnel" && event.url?.startsWith("https://")) found = event.url;
|
|
1807
|
-
} catch {
|
|
1808
|
-
}
|
|
1809
|
-
}
|
|
1810
|
-
}
|
|
1811
|
-
if (found) {
|
|
1812
|
-
cleanup();
|
|
1813
|
-
resolve3(websocketURL(found));
|
|
1814
|
-
}
|
|
1815
|
-
};
|
|
1816
|
-
child.once("error", fail);
|
|
1817
|
-
child.once("exit", exited);
|
|
1818
|
-
child.stdout.on("data", receive);
|
|
1819
|
-
child.stderr.on("data", receive);
|
|
1820
|
-
});
|
|
1821
|
-
child.stdout.removeAllListeners("data");
|
|
1822
|
-
child.stderr.removeAllListeners("data");
|
|
1823
|
-
child.stdout.resume();
|
|
1824
|
-
child.stderr.resume();
|
|
1825
|
-
child.on("error", () => {
|
|
1826
|
-
});
|
|
1827
|
-
return { route: { kind: "websocket", url: address }, process: child, close() {
|
|
1828
|
-
child.kill();
|
|
1829
|
-
} };
|
|
204
|
+
if (mode === "browser" || mode === "auto" && !fits()) await ensureBrowser();
|
|
205
|
+
return { close };
|
|
1830
206
|
} catch (error) {
|
|
1831
|
-
|
|
207
|
+
close();
|
|
1832
208
|
throw error;
|
|
1833
209
|
}
|
|
1834
210
|
}
|
|
1835
|
-
|
|
1836
|
-
|
|
1837
|
-
var
|
|
1838
|
-
var
|
|
1839
|
-
|
|
1840
|
-
|
|
1841
|
-
|
|
1842
|
-
|
|
1843
|
-
|
|
1844
|
-
|
|
211
|
+
var execute = promisify(execFile);
|
|
212
|
+
var xml = (value) => value.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
213
|
+
var systemd = (value, expandEnvironment = false) => '"' + value.replace(/\\/g, "\\\\").replace(/"/g, '\\"').replace(/%/g, "%%").replace(/\$/g, () => expandEnvironment ? "$$" : "$") + '"';
|
|
214
|
+
var windowsArg = (value) => '"' + value.replace(/(\\*)"/g, '$1$1\\"').replace(/(\\+)$/g, "$1$1") + '"';
|
|
215
|
+
function startupPlan(options) {
|
|
216
|
+
const { directory, cliPath } = options;
|
|
217
|
+
const executable = options.executable ?? process.execPath, home = options.home ?? homedir();
|
|
218
|
+
const platform = options.platform ?? process.platform;
|
|
219
|
+
const environmentPath = options.environmentPath ?? process.env.PATH ?? path.dirname(executable);
|
|
220
|
+
for (const value of [directory, cliPath, executable, home, environmentPath]) {
|
|
221
|
+
if (/[\r\n\x00]/.test(value)) throw new Error("Startup paths cannot contain control characters.");
|
|
222
|
+
}
|
|
223
|
+
const id = createHash("sha256").update(directory).digest("hex").slice(0, 12);
|
|
224
|
+
const name = `sh.mindwire.computer.${id}`;
|
|
225
|
+
const args = [cliPath, "_watch", "--state-dir", directory];
|
|
226
|
+
if (platform === "darwin") {
|
|
227
|
+
const domain = `gui/${options.uid ?? process.getuid?.()}`;
|
|
228
|
+
const file = path.join(home, "Library", "LaunchAgents", name + ".plist");
|
|
229
|
+
return {
|
|
230
|
+
kind: "launchd",
|
|
231
|
+
name,
|
|
232
|
+
file,
|
|
233
|
+
content: `<?xml version="1.0" encoding="UTF-8"?>
|
|
234
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
235
|
+
<plist version="1.0"><dict>
|
|
236
|
+
<key>Label</key><string>${name}</string>
|
|
237
|
+
<key>ProgramArguments</key><array>${[executable, ...args].map((v) => `<string>${xml(v)}</string>`).join("")}</array>
|
|
238
|
+
<key>WorkingDirectory</key><string>${xml(home)}</string>
|
|
239
|
+
<key>EnvironmentVariables</key><dict><key>PATH</key><string>${xml(environmentPath)}</string></dict>
|
|
240
|
+
<key>RunAtLoad</key><true/><key>KeepAlive</key><true/>
|
|
241
|
+
<key>ThrottleInterval</key><integer>10</integer>
|
|
242
|
+
<key>StandardOutPath</key><string>${xml(path.join(directory, "startup.log"))}</string>
|
|
243
|
+
<key>StandardErrorPath</key><string>${xml(path.join(directory, "startup.log"))}</string>
|
|
244
|
+
</dict></plist>
|
|
245
|
+
`,
|
|
246
|
+
query: ["launchctl", "print", `${domain}/${name}`],
|
|
247
|
+
enable: [["launchctl", "enable", `${domain}/${name}`], ["launchctl", "bootstrap", domain, file]],
|
|
248
|
+
disable: [["launchctl", "bootout", `${domain}/${name}`]]
|
|
249
|
+
};
|
|
1845
250
|
}
|
|
1846
|
-
|
|
1847
|
-
|
|
1848
|
-
|
|
1849
|
-
|
|
1850
|
-
|
|
1851
|
-
|
|
1852
|
-
|
|
1853
|
-
|
|
251
|
+
if (platform === "linux") {
|
|
252
|
+
const file = path.join(home, ".config", "systemd", "user", name + ".service");
|
|
253
|
+
return {
|
|
254
|
+
kind: "systemd",
|
|
255
|
+
name,
|
|
256
|
+
file,
|
|
257
|
+
content: `[Unit]
|
|
258
|
+
Description=Mindwire computer connection
|
|
259
|
+
After=network-online.target
|
|
260
|
+
[Service]
|
|
261
|
+
Type=simple
|
|
262
|
+
ExecStart=${[executable, ...args].map((value) => systemd(value, true)).join(" ")}
|
|
263
|
+
WorkingDirectory=${systemd(home)}
|
|
264
|
+
Environment=${systemd("PATH=" + environmentPath)}
|
|
265
|
+
Restart=always
|
|
266
|
+
RestartSec=10
|
|
267
|
+
KillMode=process
|
|
268
|
+
[Install]
|
|
269
|
+
WantedBy=default.target
|
|
270
|
+
`,
|
|
271
|
+
query: ["systemctl", "--user", "is-active", name + ".service"],
|
|
272
|
+
enable: [["systemctl", "--user", "daemon-reload"], ["systemctl", "--user", "enable", "--now", name + ".service"]],
|
|
273
|
+
disable: [["systemctl", "--user", "disable", "--now", name + ".service"], ["systemctl", "--user", "daemon-reload"]]
|
|
274
|
+
};
|
|
1854
275
|
}
|
|
1855
|
-
|
|
1856
|
-
|
|
1857
|
-
|
|
1858
|
-
|
|
1859
|
-
|
|
1860
|
-
|
|
1861
|
-
|
|
1862
|
-
|
|
276
|
+
if (platform === "win32") {
|
|
277
|
+
if (!options.user) throw new Error("Couldn't determine the Windows user for startup.");
|
|
278
|
+
const file = path.join(directory, "startup-task.xml");
|
|
279
|
+
return {
|
|
280
|
+
kind: "task-scheduler",
|
|
281
|
+
name,
|
|
282
|
+
file,
|
|
283
|
+
content: `<?xml version="1.0" encoding="UTF-16"?>
|
|
284
|
+
<Task version="1.2" xmlns="http://schemas.microsoft.com/windows/2004/02/mit/task">
|
|
285
|
+
<Triggers><LogonTrigger><Enabled>true</Enabled><UserId>${xml(options.user)}</UserId></LogonTrigger></Triggers>
|
|
286
|
+
<Principals><Principal id="Owner"><UserId>${xml(options.user)}</UserId><LogonType>InteractiveToken</LogonType><RunLevel>LeastPrivilege</RunLevel></Principal></Principals>
|
|
287
|
+
<Settings><MultipleInstancesPolicy>IgnoreNew</MultipleInstancesPolicy><DisallowStartIfOnBatteries>false</DisallowStartIfOnBatteries><StopIfGoingOnBatteries>false</StopIfGoingOnBatteries><StartWhenAvailable>true</StartWhenAvailable><ExecutionTimeLimit>PT0S</ExecutionTimeLimit><RestartOnFailure><Interval>PT1M</Interval><Count>999</Count></RestartOnFailure></Settings>
|
|
288
|
+
<Actions Context="Owner"><Exec><Command>${xml(executable)}</Command><Arguments>${xml(args.map(windowsArg).join(" "))}</Arguments><WorkingDirectory>${xml(home)}</WorkingDirectory></Exec></Actions>
|
|
289
|
+
</Task>
|
|
290
|
+
`,
|
|
291
|
+
query: ["schtasks.exe", "/Query", "/TN", name],
|
|
292
|
+
enable: [["schtasks.exe", "/Create", "/TN", name, "/XML", file, "/F"], ["schtasks.exe", "/Run", "/TN", name]],
|
|
293
|
+
disable: [["schtasks.exe", "/End", "/TN", name], ["schtasks.exe", "/Delete", "/TN", name, "/F"]]
|
|
294
|
+
};
|
|
1863
295
|
}
|
|
296
|
+
throw new Error("Automatic startup supports macOS, Linux with systemd, and Windows. Run mindwire start on this system.");
|
|
1864
297
|
}
|
|
1865
|
-
async
|
|
1866
|
-
|
|
1867
|
-
|
|
1868
|
-
|
|
1869
|
-
|
|
1870
|
-
const
|
|
1871
|
-
|
|
1872
|
-
const info = await client.computer.info();
|
|
1873
|
-
if (info.computerId !== runtime.computerId || info.pid !== runtime.pid) throw new Error("The local Mindwire process changed. Retry the command.");
|
|
1874
|
-
return client;
|
|
1875
|
-
}
|
|
1876
|
-
function directRoutes(config, port) {
|
|
1877
|
-
if (config.host) return [{ kind: "ssh", host: config.host, port }];
|
|
1878
|
-
if (!["0.0.0.0", "::"].includes(config.bind)) return [{ kind: "ssh", host: config.bind, port }];
|
|
1879
|
-
const addresses = /* @__PURE__ */ new Set();
|
|
1880
|
-
for (const entries of Object.values(networkInterfaces())) {
|
|
1881
|
-
for (const address of entries ?? []) if (address.family === "IPv4" && !address.internal) addresses.add(address.address);
|
|
1882
|
-
}
|
|
1883
|
-
return [...addresses].slice(0, 6).map((host) => ({ kind: "ssh", host, port }));
|
|
298
|
+
var run = async ([command, ...args]) => {
|
|
299
|
+
await execute(command, args, { timeout: 15e3, windowsHide: true });
|
|
300
|
+
};
|
|
301
|
+
async function startupStatus(directory) {
|
|
302
|
+
const state = await readJSON(path.join(directory, "computer-startup.json")) ?? { enabled: false };
|
|
303
|
+
const guardian = await readJSON(path.join(directory, "computer-guardian.json"));
|
|
304
|
+
return { ...state, running: state.enabled && await processStateAlive(guardian) };
|
|
1884
305
|
}
|
|
1885
|
-
async function
|
|
306
|
+
async function configureStartup(directory, cliPath, enabled, runtime = {}) {
|
|
1886
307
|
await fs.mkdir(directory, { recursive: true, mode: 448 });
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
} catch {
|
|
1891
|
-
}
|
|
1892
|
-
const configPath = path2.join(directory, "computer-config.json");
|
|
1893
|
-
const previous = await readJSON(configPath);
|
|
1894
|
-
const config = { ...defaultComputerConfig(), ...previous, ...patch };
|
|
1895
|
-
const waitUntilReady = async () => {
|
|
1896
|
-
let phase;
|
|
1897
|
-
for (let attempt = 0; attempt < 1200; attempt++) {
|
|
1898
|
-
const controller = await readJSON(path2.join(directory, "computer-controller.json"));
|
|
1899
|
-
if (controller?.error) throw new Error(controller.error);
|
|
1900
|
-
if (controller?.ready) return computerClient(directory);
|
|
1901
|
-
if (controller?.phase && controller.phase !== phase) {
|
|
1902
|
-
phase = controller.phase;
|
|
1903
|
-
onProgress?.(phase);
|
|
1904
|
-
}
|
|
1905
|
-
await setTimeout$1(250);
|
|
1906
|
-
}
|
|
1907
|
-
throw new Error(`Mindwire did not finish starting. Inspect ${path2.join(directory, "computer.log")}.`);
|
|
1908
|
-
};
|
|
1909
|
-
if (current) {
|
|
1910
|
-
if (Object.keys(patch).some((key) => JSON.stringify(config[key]) !== JSON.stringify(previous?.[key]))) {
|
|
1911
|
-
throw new Error("Mindwire is running with different connection settings. Run mindwire stop when idle, then retry with the new settings.");
|
|
1912
|
-
}
|
|
1913
|
-
return waitUntilReady();
|
|
1914
|
-
}
|
|
1915
|
-
const lockPath = path2.join(directory, "launch.lock");
|
|
1916
|
-
let lock;
|
|
1917
|
-
for (let attempt = 0; attempt < 120; attempt++) {
|
|
1918
|
-
try {
|
|
1919
|
-
lock = await fs.open(lockPath, "wx", 384);
|
|
1920
|
-
await lock.writeFile(String(process.pid));
|
|
1921
|
-
break;
|
|
1922
|
-
} catch (error) {
|
|
1923
|
-
if (error.code !== "EEXIST") throw error;
|
|
1924
|
-
const owner = Number(await fs.readFile(lockPath, "utf8").catch(() => ""));
|
|
1925
|
-
const age = Date.now() - (await fs.stat(lockPath).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
|
|
1926
|
-
if (age > 5e3 && !alive(owner)) {
|
|
1927
|
-
await fs.rm(lockPath, { force: true });
|
|
1928
|
-
continue;
|
|
1929
|
-
}
|
|
1930
|
-
const controller = await readJSON(path2.join(directory, "computer-controller.json"));
|
|
1931
|
-
if (alive(controller?.pid)) {
|
|
1932
|
-
const saved = await readJSON(configPath);
|
|
1933
|
-
if (Object.keys(patch).some((key) => JSON.stringify(patch[key]) !== JSON.stringify(saved?.[key]))) {
|
|
1934
|
-
throw new Error("Another start used different connection settings. Stop Mindwire when idle before changing them.");
|
|
1935
|
-
}
|
|
1936
|
-
return waitUntilReady();
|
|
1937
|
-
}
|
|
1938
|
-
await setTimeout$1(250);
|
|
1939
|
-
}
|
|
1940
|
-
}
|
|
1941
|
-
if (!lock) throw new Error("Another Mindwire start is in progress. Try again shortly.");
|
|
308
|
+
const lockPath = path.join(directory, "startup.lock");
|
|
309
|
+
const lock = await acquireProcessLock(lockPath);
|
|
310
|
+
if (!lock) throw new Error("Startup is already being configured. Try again shortly.");
|
|
1942
311
|
try {
|
|
1943
|
-
|
|
312
|
+
const user = process.platform === "win32" && !runtime.plan ? (await execute("whoami.exe", [], { timeout: 3e3, windowsHide: true })).stdout.trim() : void 0;
|
|
313
|
+
const plan = runtime.plan ?? startupPlan({ directory, cliPath, user });
|
|
314
|
+
const command = runtime.run ?? run;
|
|
315
|
+
let loaded = false;
|
|
1944
316
|
try {
|
|
1945
|
-
|
|
317
|
+
await command(plan.query);
|
|
318
|
+
loaded = true;
|
|
1946
319
|
} catch {
|
|
1947
320
|
}
|
|
1948
|
-
if (
|
|
1949
|
-
const
|
|
1950
|
-
|
|
1951
|
-
|
|
321
|
+
if (enabled) {
|
|
322
|
+
const previous = await fs.readFile(plan.file, plan.kind === "task-scheduler" ? "utf16le" : "utf8").catch(() => "");
|
|
323
|
+
const changed = previous !== plan.content;
|
|
324
|
+
if (changed) {
|
|
325
|
+
await fs.mkdir(path.dirname(plan.file), { recursive: true, mode: 448 });
|
|
326
|
+
const temporary = plan.file + `.tmp-${process.pid}`;
|
|
327
|
+
try {
|
|
328
|
+
await fs.writeFile(temporary, plan.content, { mode: 384, encoding: plan.kind === "task-scheduler" ? "utf16le" : "utf8" });
|
|
329
|
+
await fs.rename(temporary, plan.file);
|
|
330
|
+
} finally {
|
|
331
|
+
await fs.rm(temporary, { force: true });
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
if (!loaded || changed && plan.kind !== "launchd") {
|
|
335
|
+
for (const action of plan.enable) await command(action);
|
|
336
|
+
}
|
|
337
|
+
await command(plan.query);
|
|
338
|
+
if (runtime.verify) await runtime.verify();
|
|
339
|
+
else {
|
|
340
|
+
let running = false;
|
|
341
|
+
for (let attempt = 0; attempt < 60; attempt++) {
|
|
342
|
+
const guardian = await readJSON(path.join(directory, "computer-guardian.json"));
|
|
343
|
+
if (await processStateAlive(guardian)) {
|
|
344
|
+
running = true;
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
await setTimeout$1(250);
|
|
348
|
+
}
|
|
349
|
+
if (!running) throw new Error(`Automatic startup was registered but its guardian didn't start. Inspect ${path.join(directory, "startup.log")}.`);
|
|
1952
350
|
}
|
|
1953
|
-
return waitUntilReady();
|
|
1954
|
-
}
|
|
1955
|
-
const controller = await readJSON(path2.join(directory, "computer-controller.json"));
|
|
1956
|
-
if (alive(controller?.pid)) {
|
|
1957
|
-
if (controller?.error) throw new Error(controller.error);
|
|
1958
351
|
} else {
|
|
1959
|
-
await
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
}
|
|
1968
|
-
await new Promise((resolve3, reject) => {
|
|
1969
|
-
child.once("spawn", resolve3);
|
|
1970
|
-
child.once("error", reject);
|
|
1971
|
-
});
|
|
1972
|
-
child.unref();
|
|
1973
|
-
} finally {
|
|
1974
|
-
await log.close();
|
|
352
|
+
if (loaded || await fs.stat(plan.file).then(() => true, () => false)) {
|
|
353
|
+
for (const action of plan.disable) {
|
|
354
|
+
try {
|
|
355
|
+
await command(action);
|
|
356
|
+
} catch (error) {
|
|
357
|
+
if (action.includes("/End") || action.includes("bootout") && !loaded) continue;
|
|
358
|
+
throw error;
|
|
359
|
+
}
|
|
360
|
+
}
|
|
1975
361
|
}
|
|
362
|
+
await fs.rm(plan.file, { force: true });
|
|
1976
363
|
}
|
|
1977
|
-
|
|
364
|
+
const state = { enabled, kind: plan.kind, name: plan.name, file: plan.file };
|
|
365
|
+
await writeJSON(path.join(directory, "computer-startup.json"), state);
|
|
366
|
+
return { ...state, running: enabled };
|
|
1978
367
|
} finally {
|
|
1979
368
|
await lock.close();
|
|
1980
|
-
await fs.rm(lockPath, { force: true });
|
|
1981
|
-
}
|
|
1982
|
-
}
|
|
1983
|
-
async function terminate(child) {
|
|
1984
|
-
if (child.exitCode !== null || child.signalCode !== null) return;
|
|
1985
|
-
const exited = new Promise((resolve3) => child.once("exit", () => resolve3()));
|
|
1986
|
-
child.kill("SIGTERM");
|
|
1987
|
-
const timer = setTimeout(() => child.kill("SIGKILL"), 15e3);
|
|
1988
|
-
try {
|
|
1989
|
-
await exited;
|
|
1990
|
-
} finally {
|
|
1991
|
-
clearTimeout(timer);
|
|
1992
369
|
}
|
|
1993
370
|
}
|
|
1994
|
-
async function
|
|
1995
|
-
let
|
|
1996
|
-
|
|
1997
|
-
const controllerPath = path2.join(directory, "computer-controller.json");
|
|
1998
|
-
const previous = await readJSON(controllerPath);
|
|
1999
|
-
if (alive(previous?.pid)) throw new Error("The computer already has a service controller.");
|
|
2000
|
-
const phase = (message) => writeJSON(controllerPath, { pid: process.pid, ready: false, phase: message });
|
|
2001
|
-
await phase("Preparing Mindwire\u2026");
|
|
2002
|
-
let child, relay, stopping = false;
|
|
371
|
+
async function watchComputer(directory, cliPath) {
|
|
372
|
+
let stopping = false;
|
|
373
|
+
const abort = new AbortController();
|
|
2003
374
|
const stop = () => {
|
|
2004
375
|
stopping = true;
|
|
376
|
+
abort.abort();
|
|
2005
377
|
};
|
|
2006
378
|
process.on("SIGTERM", stop);
|
|
2007
379
|
process.on("SIGINT", stop);
|
|
2008
|
-
let
|
|
2009
|
-
|
|
2010
|
-
token = (await fs.readFile(path2.join(directory, "daemon.token"), "utf8")).trim();
|
|
2011
|
-
} catch (error) {
|
|
2012
|
-
if (error.code !== "ENOENT") throw error;
|
|
2013
|
-
token = randomBytes(32).toString("hex");
|
|
2014
|
-
await fs.writeFile(path2.join(directory, "daemon.token"), token, { mode: 384, flag: "wx" });
|
|
2015
|
-
}
|
|
2016
|
-
const daemonEnv = { ...process.env };
|
|
2017
|
-
if (process.platform === "win32") {
|
|
2018
|
-
try {
|
|
2019
|
-
const git = execFileSync("where.exe", ["git"], { encoding: "utf8", timeout: 3e3, windowsHide: true }).split(/\r?\n/)[0];
|
|
2020
|
-
if (git) {
|
|
2021
|
-
const shellDirectory = path2.resolve(path2.dirname(git), "..", "bin");
|
|
2022
|
-
await fs.access(path2.join(shellDirectory, "bash.exe"));
|
|
2023
|
-
const pathKey = Object.keys(daemonEnv).find((key) => key.toLowerCase() === "path") ?? "PATH";
|
|
2024
|
-
daemonEnv[pathKey] = `${shellDirectory}${path2.delimiter}${daemonEnv[pathKey] ?? ""}`;
|
|
2025
|
-
}
|
|
2026
|
-
} catch {
|
|
2027
|
-
}
|
|
2028
|
-
}
|
|
2029
|
-
let sshPort = config.sshPort, websocketPort = config.websocketPort;
|
|
2030
|
-
const launch = async (binary) => {
|
|
2031
|
-
await fs.rm(path2.join(directory, "computer-runtime.json"), { force: true });
|
|
2032
|
-
child = spawn(binary, ["--computer"], {
|
|
2033
|
-
cwd: config.directory,
|
|
2034
|
-
stdio: ["ignore", "inherit", "inherit"],
|
|
2035
|
-
windowsHide: true,
|
|
2036
|
-
env: {
|
|
2037
|
-
...daemonEnv,
|
|
2038
|
-
DAEMON_TOKEN: token,
|
|
2039
|
-
ADDR: "127.0.0.1:0",
|
|
2040
|
-
STATE_PATH: path2.join(directory, "agent-state.json"),
|
|
2041
|
-
WORKSPACE_DB_PATH: path2.join(directory, "workspace.db"),
|
|
2042
|
-
AGENT_CWD: config.directory,
|
|
2043
|
-
COMPUTER_SSH_ADDR: `${config.bind.includes(":") ? `[${config.bind}]` : config.bind}:${sshPort}`,
|
|
2044
|
-
COMPUTER_WS_ADDR: `127.0.0.1:${websocketPort}`,
|
|
2045
|
-
MINDWIRE_COMPUTER_MANAGED: "1"
|
|
2046
|
-
}
|
|
2047
|
-
});
|
|
2048
|
-
let failure;
|
|
2049
|
-
child.on("error", (error) => {
|
|
2050
|
-
failure = error;
|
|
2051
|
-
});
|
|
2052
|
-
for (let attempt = 0; attempt < 100; attempt++) {
|
|
2053
|
-
if (failure) throw failure;
|
|
2054
|
-
if (child.exitCode !== null || child.signalCode !== null) throw new Error("Mindwire exited during startup. Check computer.log; the downloaded release must support computer connections.");
|
|
2055
|
-
try {
|
|
2056
|
-
const client = await computerClient(directory);
|
|
2057
|
-
const health = await client.health();
|
|
2058
|
-
if (health.computerConnectionVersion !== 1) throw new Error("This daemon release does not support computer connections. Update Mindwire.");
|
|
2059
|
-
return client;
|
|
2060
|
-
} catch (error) {
|
|
2061
|
-
if (attempt === 99) throw error;
|
|
2062
|
-
}
|
|
2063
|
-
await setTimeout$1(100);
|
|
2064
|
-
}
|
|
2065
|
-
throw new Error("Mindwire startup timed out.");
|
|
2066
|
-
};
|
|
2067
|
-
let routes = [];
|
|
380
|
+
let reportedError = "";
|
|
381
|
+
const guardianPath = path.join(directory, "computer-guardian.json");
|
|
2068
382
|
try {
|
|
2069
|
-
await
|
|
2070
|
-
let binary = config.daemonBin ?? await ensureDaemonBinary({ version: config.version ?? SDK_VERSION, cacheDir: process.env.MINDWIRE_RELEASE_CACHE_DIR });
|
|
2071
|
-
await phase("Starting the Mindwire service\u2026");
|
|
2072
|
-
let client = await launch(binary);
|
|
2073
|
-
const info = await client.computer.info();
|
|
2074
|
-
sshPort = info.sshPort;
|
|
2075
|
-
websocketPort = info.websocketPort;
|
|
2076
|
-
await phase(config.relay.kind === "none" ? "Preparing direct SSH\u2026" : "Preparing your internet connection\u2026");
|
|
2077
|
-
relay = await startRelay(config.relay, info.websocketPort, { cacheDir: path2.join(directory, "tools"), onProgress: phase });
|
|
2078
|
-
routes = directRoutes(config, info.sshPort);
|
|
2079
|
-
if (relay.route) routes.push(relay.route);
|
|
2080
|
-
if (routes.length === 0) throw new Error("No network address is available. Use --host with your LAN/VPN address or choose a relay.");
|
|
2081
|
-
await client.computer.setRoutes(routes);
|
|
2082
|
-
await writeJSON(controllerPath, { pid: process.pid, ready: true, routes });
|
|
383
|
+
await writeJSON(guardianPath, await currentProcessIdentity());
|
|
2083
384
|
while (!stopping) {
|
|
2084
|
-
await
|
|
2085
|
-
|
|
2086
|
-
if (
|
|
2087
|
-
routes = routes.filter((r) => r.kind === "ssh");
|
|
2088
|
-
if (routes.length > 0) await client.computer.setRoutes(routes);
|
|
2089
|
-
await writeJSON(controllerPath, { pid: process.pid, ready: true, routes, error: "The relay disconnected. Direct SSH still works. Restart Mindwire when idle to reconnect the relay." });
|
|
2090
|
-
relay = { close() {
|
|
2091
|
-
} };
|
|
2092
|
-
}
|
|
2093
|
-
const updatePath = path2.join(directory, "computer-update.json");
|
|
2094
|
-
const update = await readJSON(updatePath);
|
|
2095
|
-
if (!update || !["queued", "downloading", "waiting", "restarting"].includes(update.status)) continue;
|
|
2096
|
-
const status = async (state, error) => writeJSON(updatePath, { ...update, status: state, error, updatedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2097
|
-
try {
|
|
2098
|
-
await status("downloading");
|
|
2099
|
-
const replacement = await ensureDaemonBinary({ version: update.version, cacheDir: process.env.MINDWIRE_RELEASE_CACHE_DIR });
|
|
2100
|
-
await status("waiting");
|
|
2101
|
-
let lease;
|
|
2102
|
-
while (!stopping && !lease) {
|
|
2103
|
-
try {
|
|
2104
|
-
lease = await client.service.acquireUpdate();
|
|
2105
|
-
} catch (error) {
|
|
2106
|
-
if (error.status !== 409) throw error;
|
|
2107
|
-
await setTimeout$1(2e3);
|
|
2108
|
-
}
|
|
2109
|
-
}
|
|
2110
|
-
if (stopping || !lease) break;
|
|
2111
|
-
if (lease.pid !== child.pid) {
|
|
2112
|
-
await client.service.releaseUpdate(lease.id);
|
|
2113
|
-
throw new Error("The leased service process changed.");
|
|
2114
|
-
}
|
|
2115
|
-
await status("restarting");
|
|
2116
|
-
await terminate(child);
|
|
385
|
+
const paused = await readJSON(path.join(directory, "computer-stopped.json"));
|
|
386
|
+
const controller = await readJSON(path.join(directory, "computer-controller.json"));
|
|
387
|
+
if (!paused?.stopped && !await processStateAlive(controller)) {
|
|
2117
388
|
try {
|
|
2118
|
-
|
|
2119
|
-
|
|
2120
|
-
const health = await client.health();
|
|
2121
|
-
if (health.version.replace(/^v/, "") !== update.version) throw new Error("The replacement service reports a different release version.");
|
|
389
|
+
await ensureComputer(directory, cliPath, {}, void 0, { resume: false, signal: abort.signal });
|
|
390
|
+
reportedError = "";
|
|
2122
391
|
} catch (error) {
|
|
2123
|
-
if (
|
|
2124
|
-
|
|
2125
|
-
|
|
2126
|
-
|
|
392
|
+
if (stopping) break;
|
|
393
|
+
const message = error instanceof Error ? error.message : "Computer recovery failed.";
|
|
394
|
+
if (message !== reportedError) {
|
|
395
|
+
process.stderr.write(`Mindwire recovery: ${message}
|
|
396
|
+
`);
|
|
397
|
+
reportedError = message;
|
|
398
|
+
}
|
|
2127
399
|
}
|
|
2128
|
-
binary = replacement;
|
|
2129
|
-
config = { ...config, daemonBin: void 0, version: update.version };
|
|
2130
|
-
await writeJSON(path2.join(directory, "computer-config.json"), config);
|
|
2131
|
-
await status("complete");
|
|
2132
|
-
} catch (error) {
|
|
2133
|
-
await status("failed", error instanceof Error ? error.message : "Service update failed.");
|
|
2134
400
|
}
|
|
401
|
+
for (let tick = 0; tick < 10 && !stopping; tick++) await setTimeout$1(500);
|
|
2135
402
|
}
|
|
2136
|
-
} catch (error) {
|
|
2137
|
-
await writeJSON(controllerPath, { pid: process.pid, ready: false, error: error instanceof Error ? error.message : "Computer service failed." });
|
|
2138
|
-
throw error;
|
|
2139
403
|
} finally {
|
|
2140
|
-
|
|
2141
|
-
if (child) await terminate(child);
|
|
404
|
+
if ((await readJSON(guardianPath))?.pid === process.pid) await writeJSON(guardianPath, { pid: 0 });
|
|
2142
405
|
process.off("SIGTERM", stop);
|
|
2143
406
|
process.off("SIGINT", stop);
|
|
2144
|
-
if (stopping) await writeJSON(controllerPath, { pid: 0, ready: false });
|
|
2145
407
|
}
|
|
2146
408
|
}
|
|
2147
|
-
|
|
2148
|
-
|
|
2149
|
-
|
|
2150
|
-
if (
|
|
2151
|
-
|
|
2152
|
-
|
|
2153
|
-
|
|
2154
|
-
|
|
2155
|
-
|
|
2156
|
-
|
|
2157
|
-
|
|
2158
|
-
|
|
2159
|
-
|
|
2160
|
-
|
|
2161
|
-
}
|
|
2162
|
-
|
|
409
|
+
|
|
410
|
+
// src/computer/connection-flow.ts
|
|
411
|
+
function connectionAction(value) {
|
|
412
|
+
if (value === void 0) return void 0;
|
|
413
|
+
if (value === "resume" || value === "reconnect" || value === "pair") return value;
|
|
414
|
+
throw new Error("Use mindwire connect, mindwire connect resume, mindwire reconnect, or mindwire connect pair.");
|
|
415
|
+
}
|
|
416
|
+
async function chooseConnectionAction(options) {
|
|
417
|
+
const paired = options.devices.filter((device) => !device.revoked);
|
|
418
|
+
if (options.requested) {
|
|
419
|
+
if (options.requested !== "pair" && paired.length === 0) {
|
|
420
|
+
throw new Error("No saved phones yet. Run mindwire connect to pair one first.");
|
|
421
|
+
}
|
|
422
|
+
return options.requested;
|
|
423
|
+
}
|
|
424
|
+
if (paired.length === 0) return "pair";
|
|
425
|
+
if (!options.question) return "resume";
|
|
426
|
+
for (; ; ) {
|
|
427
|
+
const answer = (await options.question(
|
|
428
|
+
"\n 1 Resume saved connection\n 2 Refresh address on a saved phone (QR)\n 3 Pair another phone\n\nChoose [1]: "
|
|
429
|
+
)).trim().toLowerCase();
|
|
430
|
+
if (answer === "" || answer === "1" || answer === "resume") return "resume";
|
|
431
|
+
if (answer === "2" || answer === "reconnect") return "reconnect";
|
|
432
|
+
if (answer === "3" || answer === "pair") return "pair";
|
|
433
|
+
}
|
|
434
|
+
}
|
|
435
|
+
async function chooseStartupAction(options) {
|
|
436
|
+
if (options.startup && options.noStartup) throw new Error("Choose either --startup or --no-startup.");
|
|
437
|
+
if (options.startup) return !options.state.enabled || !options.state.running ? true : void 0;
|
|
438
|
+
if (options.noStartup) return options.state.kind ? void 0 : false;
|
|
439
|
+
if (options.state.kind) return options.state.enabled && !options.state.running ? true : void 0;
|
|
440
|
+
if (!options.question) return void 0;
|
|
441
|
+
for (; ; ) {
|
|
442
|
+
const answer = (await options.question(
|
|
443
|
+
"\nStart Mindwire automatically after you sign in to this computer? [Y/n] "
|
|
444
|
+
)).trim().toLowerCase();
|
|
445
|
+
if (["", "y", "yes"].includes(answer)) return true;
|
|
446
|
+
if (["n", "no"].includes(answer)) return false;
|
|
447
|
+
}
|
|
448
|
+
}
|
|
449
|
+
function reconnectCode(info, name, now = /* @__PURE__ */ new Date()) {
|
|
450
|
+
if (info.routes.length === 0) throw new Error("No connection address is ready. Run mindwire status and retry once the tunnel is online.");
|
|
451
|
+
return {
|
|
452
|
+
version: 1,
|
|
453
|
+
mode: "reconnect",
|
|
454
|
+
computerId: info.computerId,
|
|
455
|
+
registryId: info.registryId,
|
|
456
|
+
fingerprint: info.fingerprint,
|
|
457
|
+
name,
|
|
458
|
+
routes: info.routes,
|
|
459
|
+
expiresAt: new Date(now.getTime() + 5 * 6e4).toISOString()
|
|
460
|
+
};
|
|
461
|
+
}
|
|
462
|
+
function deviceSummary(devices) {
|
|
463
|
+
return devices.filter((device) => !device.revoked).sort((a, b) => a.name.localeCompare(b.name) || a.id.localeCompare(b.id)).map((device) => {
|
|
464
|
+
const name = device.name.replace(/[\x00-\x1f\x7f-\x9f]/g, "");
|
|
465
|
+
const status = device.connected === true ? "connected" : "paired";
|
|
466
|
+
return ` ${name} \xB7 ${status}`;
|
|
467
|
+
}).join("\n");
|
|
2163
468
|
}
|
|
2164
469
|
|
|
2165
470
|
// src/cli.ts
|
|
2166
471
|
var help = `Mindwire \u2014 connect this computer to your phone
|
|
2167
472
|
|
|
2168
473
|
npm install -g mindwire
|
|
2169
|
-
mindwire connect
|
|
474
|
+
mindwire connect Resume a saved connection, or pair your first phone
|
|
475
|
+
mindwire connect resume Resume without creating another pairing
|
|
476
|
+
mindwire reconnect Refresh a saved phone's address with a QR code
|
|
477
|
+
mindwire connect pair Pair another phone with this computer
|
|
2170
478
|
mindwire connect --relay none --host laptop.tailnet Use your existing VPN
|
|
2171
479
|
mindwire connect --relay none Direct SSH only (reachable network required)
|
|
2172
480
|
mindwire connect --relay cloudflare Cloudflare quick tunnel
|
|
@@ -2174,6 +482,8 @@ var help = `Mindwire \u2014 connect this computer to your phone
|
|
|
2174
482
|
mindwire connect --relay custom --relay-url wss://computer.example/ssh
|
|
2175
483
|
|
|
2176
484
|
mindwire start Start the saved connection in the background
|
|
485
|
+
mindwire startup enable|disable Manage automatic startup after computer login
|
|
486
|
+
mindwire startup status Check automatic startup
|
|
2177
487
|
mindwire status Show connection and running service
|
|
2178
488
|
mindwire devices List paired phones
|
|
2179
489
|
mindwire revoke DEVICE_ID Disconnect and revoke one phone
|
|
@@ -2188,7 +498,10 @@ Options:
|
|
|
2188
498
|
--cloudflare-token-file PATH Named Cloudflare tunnel token file
|
|
2189
499
|
--state-dir PATH Private state directory
|
|
2190
500
|
--json Structured output (QR invitation includes a secret)
|
|
501
|
+
--qr auto|terminal|browser Fit the QR to your terminal or open a local page
|
|
2191
502
|
--no-qr Print only the pairing link
|
|
503
|
+
--startup Enable automatic startup without a prompt
|
|
504
|
+
--no-startup Skip automatic startup setup
|
|
2192
505
|
|
|
2193
506
|
The default works across Wi-Fi and mobile networks. Mindwire privately downloads
|
|
2194
507
|
a verified Cloudflare helper when needed. Relays carry encrypted SSH.
|
|
@@ -2204,6 +517,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2204
517
|
help: { type: "boolean", short: "h" },
|
|
2205
518
|
json: { type: "boolean" },
|
|
2206
519
|
"no-qr": { type: "boolean" },
|
|
520
|
+
qr: { type: "string" },
|
|
2207
521
|
"state-dir": { type: "string" },
|
|
2208
522
|
directory: { type: "string" },
|
|
2209
523
|
host: { type: "string" },
|
|
@@ -2215,15 +529,19 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2215
529
|
port: { type: "string" },
|
|
2216
530
|
"websocket-port": { type: "string" },
|
|
2217
531
|
version: { type: "string" },
|
|
2218
|
-
force: { type: "boolean" }
|
|
532
|
+
force: { type: "boolean" },
|
|
533
|
+
startup: { type: "boolean" },
|
|
534
|
+
"no-startup": { type: "boolean" }
|
|
2219
535
|
} });
|
|
2220
536
|
const command = positionals[0] ?? "help";
|
|
2221
537
|
if (values.help || command === "help") {
|
|
2222
538
|
process.stdout.write(help);
|
|
2223
539
|
return;
|
|
2224
540
|
}
|
|
2225
|
-
|
|
2226
|
-
|
|
541
|
+
if (values.qr && !["auto", "terminal", "browser"].includes(values.qr)) throw new Error("Choose --qr auto, terminal or browser.");
|
|
542
|
+
if (values.startup && values["no-startup"]) throw new Error("Choose either --startup or --no-startup.");
|
|
543
|
+
const directory = path.resolve(values["state-dir"] ?? defaultStateDirectory());
|
|
544
|
+
const emit = (event, data = {}, text) => {
|
|
2227
545
|
if (values.json) process.stdout.write(JSON.stringify({ event, ...data }) + "\n");
|
|
2228
546
|
else if (text) process.stdout.write(text + "\n");
|
|
2229
547
|
};
|
|
@@ -2231,9 +549,21 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2231
549
|
await superviseComputer(directory);
|
|
2232
550
|
return;
|
|
2233
551
|
}
|
|
2234
|
-
if (
|
|
552
|
+
if (command === "_watch") {
|
|
553
|
+
await watchComputer(directory, fileURLToPath(import.meta.url));
|
|
554
|
+
return;
|
|
555
|
+
}
|
|
556
|
+
if (command === "startup") {
|
|
557
|
+
const action = positionals[1] ?? "status";
|
|
558
|
+
if (!["enable", "disable", "status"].includes(action)) throw new Error("Use mindwire startup enable, disable or status.");
|
|
559
|
+
const state = action === "status" ? await startupStatus(directory) : await configureStartup(directory, fileURLToPath(import.meta.url), action === "enable");
|
|
560
|
+
emit("startup", state, state.enabled ? "Mindwire starts after you sign in to this computer and recovers automatically." : "Automatic startup is off. Use mindwire start to run the service.");
|
|
561
|
+
return;
|
|
562
|
+
}
|
|
563
|
+
if (["connect", "start", "reconnect"].includes(command)) {
|
|
564
|
+
const requestedAction = command === "reconnect" ? "reconnect" : command === "connect" ? connectionAction(positionals[1]) : void 0;
|
|
2235
565
|
const patch = {};
|
|
2236
|
-
if (values.directory) patch.directory =
|
|
566
|
+
if (values.directory) patch.directory = path.resolve(values.directory);
|
|
2237
567
|
if (values.host) patch.host = values.host;
|
|
2238
568
|
if (values.bind) patch.bind = values.bind;
|
|
2239
569
|
for (const [flag, field] of [["port", "sshPort"], ["websocket-port", "websocketPort"]]) {
|
|
@@ -2243,7 +573,7 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2243
573
|
patch[field] = n;
|
|
2244
574
|
}
|
|
2245
575
|
}
|
|
2246
|
-
if (values["daemon-bin"] || process.env.MINDWIRE_DAEMON) patch.daemonBin =
|
|
576
|
+
if (values["daemon-bin"] || process.env.MINDWIRE_DAEMON) patch.daemonBin = path.resolve(values["daemon-bin"] ?? process.env.MINDWIRE_DAEMON);
|
|
2247
577
|
if (values.version) patch.version = values.version.replace(/^v/, "");
|
|
2248
578
|
if (values.relay || values["relay-url"] || values["cloudflare-token-file"]) {
|
|
2249
579
|
const kind = values.relay ?? "custom";
|
|
@@ -2251,98 +581,155 @@ async function main(args = process.argv.slice(2)) {
|
|
|
2251
581
|
patch.relay = {
|
|
2252
582
|
kind,
|
|
2253
583
|
url: values["relay-url"],
|
|
2254
|
-
cloudflareTokenFile: values["cloudflare-token-file"] ?
|
|
584
|
+
cloudflareTokenFile: values["cloudflare-token-file"] ? path.resolve(values["cloudflare-token-file"]) : void 0
|
|
2255
585
|
};
|
|
2256
586
|
}
|
|
2257
|
-
|
|
587
|
+
emit("starting", {}, "Starting Mindwire\u2026");
|
|
2258
588
|
const client2 = await ensureComputer(
|
|
2259
589
|
directory,
|
|
2260
590
|
fileURLToPath(import.meta.url),
|
|
2261
591
|
patch,
|
|
2262
|
-
(phase) =>
|
|
592
|
+
(phase) => emit("progress", { message: phase }, phase)
|
|
2263
593
|
);
|
|
2264
594
|
const info = await client2.computer.info();
|
|
2265
|
-
|
|
595
|
+
emit("ready", { computerId: info.computerId, routes: info.routes }, "Mindwire is running in the background.");
|
|
2266
596
|
if (command === "start") return;
|
|
2267
|
-
if (info.routes.some((route) => route.kind === "websocket"
|
|
2268
|
-
|
|
2269
|
-
}
|
|
2270
|
-
const
|
|
2271
|
-
const
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2276
|
-
|
|
597
|
+
if (info.routes.some((route) => route.kind === "websocket")) {
|
|
598
|
+
emit("secure_connection", {}, "Your phone connects over end-to-end encrypted SSH. The tunnel carries encrypted traffic; only phones you approve can connect.");
|
|
599
|
+
}
|
|
600
|
+
const devices = await client2.computer.devices();
|
|
601
|
+
const saved = devices.filter((device) => !device.revoked);
|
|
602
|
+
if (saved.length) emit("saved_devices", { devices: saved }, `
|
|
603
|
+
Saved phones
|
|
604
|
+
${deviceSummary(saved)}`);
|
|
605
|
+
const question = !values.json && process.stdin.isTTY ? async (prompt) => {
|
|
606
|
+
const input = createInterface({ input: process.stdin, output: process.stderr });
|
|
607
|
+
try {
|
|
608
|
+
return await input.question(prompt);
|
|
609
|
+
} finally {
|
|
610
|
+
input.close();
|
|
2277
611
|
}
|
|
2278
|
-
|
|
2279
|
-
|
|
2280
|
-
|
|
2281
|
-
|
|
2282
|
-
|
|
2283
|
-
|
|
612
|
+
} : void 0;
|
|
613
|
+
const action = await chooseConnectionAction({ requested: requestedAction, devices, question });
|
|
614
|
+
const startupPreference = await startupStatus(directory);
|
|
615
|
+
const startupAction = await chooseStartupAction({
|
|
616
|
+
state: startupPreference,
|
|
617
|
+
startup: values.startup,
|
|
618
|
+
noStartup: values["no-startup"],
|
|
619
|
+
question
|
|
620
|
+
});
|
|
621
|
+
if (startupAction !== void 0) {
|
|
622
|
+
try {
|
|
623
|
+
const startup = await configureStartup(directory, fileURLToPath(import.meta.url), startupAction);
|
|
624
|
+
emit("startup", startup, startup.enabled ? "Automatic startup enabled. Saved phones can reconnect after you sign in to this computer." : "Automatic startup is off. Run mindwire connect after restarting, or mindwire startup enable to change this.");
|
|
625
|
+
} catch {
|
|
626
|
+
emit("startup_unavailable", {}, "Mindwire is running, but startup setup didn't complete. Run mindwire startup enable to check your system's startup service.");
|
|
627
|
+
}
|
|
628
|
+
} else {
|
|
629
|
+
emit("startup", startupPreference, startupPreference.enabled ? "Automatic startup is on. Change it with mindwire startup disable." : "Automatic startup is off. Enable it with mindwire startup enable.");
|
|
2284
630
|
}
|
|
2285
|
-
|
|
2286
|
-
|
|
2287
|
-
|
|
2288
|
-
|
|
2289
|
-
emit2("paired", {}, "Phone connected. You can close this terminal; Mindwire keeps running.");
|
|
2290
|
-
return;
|
|
631
|
+
if (action === "resume") {
|
|
632
|
+
emit("resumed", { computerId: info.computerId, devices: saved }, "\nReady for your saved phone. Open this computer in Mindwire.");
|
|
633
|
+
if (info.routes.some((route) => route.kind === "websocket" && new URL(route.url).hostname.endsWith(".trycloudflare.com"))) {
|
|
634
|
+
emit("reconnect_hint", {}, "If its temporary tunnel address changed, run mindwire reconnect to refresh your phone.");
|
|
2291
635
|
}
|
|
2292
|
-
|
|
2293
|
-
|
|
2294
|
-
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
if (action === "reconnect") {
|
|
639
|
+
const code = reconnectCode(await client2.computer.info(), hostname());
|
|
640
|
+
emit("reconnect_code", { code, uri: computerPairingURI(code) });
|
|
641
|
+
if (values.json) return;
|
|
642
|
+
const display2 = await showPairingInvitation(code, {
|
|
643
|
+
mode: values["no-qr"] ? "none" : values.qr ?? "auto"
|
|
644
|
+
});
|
|
645
|
+
try {
|
|
646
|
+
if (question) await question("\nScan with your saved phone, then press Enter to close this code. ");
|
|
647
|
+
else if (values.qr === "browser") {
|
|
648
|
+
emit("waiting", {}, "This code stays open until it expires. Press Ctrl+C to close it; Mindwire keeps running.");
|
|
649
|
+
await setTimeout$1(Math.max(0, Date.parse(code.expiresAt) - Date.now()));
|
|
650
|
+
}
|
|
651
|
+
} finally {
|
|
652
|
+
display2.close();
|
|
2295
653
|
}
|
|
2296
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
|
|
2300
|
-
|
|
2301
|
-
|
|
2302
|
-
|
|
2303
|
-
|
|
654
|
+
emit("resumed", { computerId: info.computerId }, "Saved pairing kept. Mindwire continues running in the background.");
|
|
655
|
+
return;
|
|
656
|
+
}
|
|
657
|
+
const invitation = await client2.computer.invite((await client2.computer.info()).routes);
|
|
658
|
+
const uri = computerPairingURI(invitation);
|
|
659
|
+
emit("invitation", { invitation, uri });
|
|
660
|
+
const display = values.json ? void 0 : await showPairingInvitation(invitation, {
|
|
661
|
+
mode: values["no-qr"] ? "none" : values.qr ?? "auto"
|
|
662
|
+
});
|
|
663
|
+
try {
|
|
664
|
+
let shownRequest;
|
|
665
|
+
while (Date.now() < Date.parse(invitation.expiresAt)) {
|
|
666
|
+
const pairing = await client2.computer.pairing(invitation.pairingId);
|
|
667
|
+
if (pairing.status === "approved") {
|
|
668
|
+
display?.close();
|
|
669
|
+
emit("paired", {}, "Phone connected. You can close this terminal; Mindwire keeps running.");
|
|
670
|
+
return;
|
|
671
|
+
}
|
|
672
|
+
if (pairing.status === "rejected") {
|
|
673
|
+
display?.close();
|
|
674
|
+
emit("rejected", {}, "Pairing declined.");
|
|
675
|
+
return;
|
|
676
|
+
}
|
|
677
|
+
if (pairing.request && shownRequest !== pairing.request.id) {
|
|
678
|
+
display?.close();
|
|
679
|
+
shownRequest = pairing.request.id;
|
|
680
|
+
const key = Buffer.from(pairing.request.publicKey.split(" ")[1] ?? "", "base64");
|
|
681
|
+
const fingerprint = "SHA256:" + createHash("sha256").update(key).digest("base64").replace(/=+$/, "");
|
|
682
|
+
emit(
|
|
683
|
+
"approval_required",
|
|
684
|
+
{ pairingId: invitation.pairingId, request: pairing.request, fingerprint },
|
|
685
|
+
`
|
|
2304
686
|
${safeName(pairing.request.name)} wants to connect.
|
|
2305
687
|
Device key: ${fingerprint}`
|
|
2306
|
-
);
|
|
2307
|
-
if (!process.stdin.isTTY) {
|
|
2308
|
-
emit2(
|
|
2309
|
-
"approval_command",
|
|
2310
|
-
{ command: `mindwire approve ${invitation.pairingId} ${pairing.request.id}` },
|
|
2311
|
-
`Review this device, then run:
|
|
2312
|
-
mindwire approve ${invitation.pairingId} ${pairing.request.id}`
|
|
2313
688
|
);
|
|
2314
|
-
|
|
2315
|
-
|
|
2316
|
-
|
|
2317
|
-
|
|
2318
|
-
|
|
2319
|
-
|
|
2320
|
-
|
|
689
|
+
if (!process.stdin.isTTY) {
|
|
690
|
+
emit(
|
|
691
|
+
"approval_command",
|
|
692
|
+
{ command: `mindwire approve ${invitation.pairingId} ${pairing.request.id}` },
|
|
693
|
+
`Review this device, then run:
|
|
694
|
+
mindwire approve ${invitation.pairingId} ${pairing.request.id}`
|
|
695
|
+
);
|
|
696
|
+
} else {
|
|
697
|
+
const input = createInterface({ input: process.stdin, output: process.stderr });
|
|
698
|
+
try {
|
|
699
|
+
const answer = await input.question("Allow this phone to access this workspace? [y/N] ");
|
|
700
|
+
await client2.computer.decide(invitation.pairingId, pairing.request.id, /^y(es)?$/i.test(answer.trim()));
|
|
701
|
+
} finally {
|
|
702
|
+
input.close();
|
|
703
|
+
}
|
|
2321
704
|
}
|
|
2322
705
|
}
|
|
706
|
+
await setTimeout$1(750);
|
|
2323
707
|
}
|
|
2324
|
-
|
|
708
|
+
throw new Error("Pairing expired. Run mindwire connect to show a new QR code.");
|
|
709
|
+
} finally {
|
|
710
|
+
display?.close();
|
|
2325
711
|
}
|
|
2326
|
-
throw new Error("Pairing expired. Run mindwire connect to show a new QR code.");
|
|
2327
712
|
}
|
|
2328
713
|
if (command === "stop") {
|
|
2329
714
|
await stopComputer(directory, values.force === true);
|
|
2330
|
-
|
|
715
|
+
emit("stopped", {}, "Mindwire stopped.");
|
|
2331
716
|
return;
|
|
2332
717
|
}
|
|
2333
718
|
const client = await computerClient(directory);
|
|
2334
719
|
switch (command) {
|
|
2335
720
|
case "status": {
|
|
2336
|
-
const [info, health, work, controller] = await Promise.all([
|
|
721
|
+
const [info, health, work, controller, startup] = await Promise.all([
|
|
2337
722
|
client.computer.info(),
|
|
2338
723
|
client.health(),
|
|
2339
724
|
client.service.updateStatus(),
|
|
2340
|
-
readJSON(
|
|
725
|
+
readJSON(path.join(directory, "computer-controller.json")),
|
|
726
|
+
startupStatus(directory)
|
|
2341
727
|
]);
|
|
2342
|
-
|
|
728
|
+
emit(
|
|
2343
729
|
"status",
|
|
2344
|
-
{ ...info, daemonVersion: health.version, ...work, relayError: controller?.error },
|
|
730
|
+
{ ...info, daemonVersion: health.version, ...work, startup, recovering: controller?.recovering, relayError: controller?.error },
|
|
2345
731
|
`Mindwire ${health.version} \xB7 ${work.idle ? "idle" : "working"}
|
|
732
|
+
Startup: ${startup.enabled ? "on" : "off"}
|
|
2346
733
|
${info.routes.map((route) => route.kind === "ssh" ? `SSH ${route.host}:${route.port}` : route.url).join("\n")}${controller?.error ? `
|
|
2347
734
|
${controller.error}` : ""}`
|
|
2348
735
|
);
|
|
@@ -2350,38 +737,38 @@ ${controller.error}` : ""}`
|
|
|
2350
737
|
}
|
|
2351
738
|
case "devices": {
|
|
2352
739
|
const devices = await client.computer.devices();
|
|
2353
|
-
|
|
740
|
+
emit("devices", { devices }, devices.length ? devices.map((d) => `${safeName(d.name)} ${d.id}${d.revoked ? " (revoked)" : ""}`).join("\n") : "No paired phones yet. Run mindwire connect.");
|
|
2354
741
|
break;
|
|
2355
742
|
}
|
|
2356
743
|
case "revoke": {
|
|
2357
744
|
if (!positionals[1]) throw new Error("Use mindwire revoke DEVICE_ID (from mindwire devices).");
|
|
2358
745
|
await client.computer.revoke(positionals[1]);
|
|
2359
|
-
|
|
746
|
+
emit("revoked", {}, "Device revoked and disconnected.");
|
|
2360
747
|
break;
|
|
2361
748
|
}
|
|
2362
749
|
case "approve":
|
|
2363
750
|
case "reject": {
|
|
2364
751
|
if (!positionals[1] || !positionals[2]) throw new Error(`Use mindwire ${command} PAIRING_ID REQUEST_ID.`);
|
|
2365
752
|
await client.computer.decide(positionals[1], positionals[2], command === "approve");
|
|
2366
|
-
|
|
753
|
+
emit(command, {}, command === "approve" ? "Phone approved." : "Phone declined.");
|
|
2367
754
|
break;
|
|
2368
755
|
}
|
|
2369
756
|
case "update": {
|
|
2370
757
|
const version = (values.version ?? SDK_VERSION).replace(/^v/, "");
|
|
2371
758
|
const health = await client.health();
|
|
2372
759
|
if (health.version.replace(/^v/, "") === version) {
|
|
2373
|
-
|
|
760
|
+
emit("updated", { version }, `Mindwire ${version} is already installed.`);
|
|
2374
761
|
return;
|
|
2375
762
|
}
|
|
2376
763
|
const requested = await client.computer.requestUpdate(version);
|
|
2377
|
-
|
|
764
|
+
emit("update_queued", requested, "Update queued. It will wait until all work and terminals are finished.");
|
|
2378
765
|
let last = "";
|
|
2379
766
|
for (; ; ) {
|
|
2380
|
-
const update = await readJSON(
|
|
767
|
+
const update = await readJSON(path.join(directory, "computer-update.json"));
|
|
2381
768
|
if (update?.id !== requested.id) throw new Error("The update request changed. Run mindwire status.");
|
|
2382
769
|
if (update.status !== last) {
|
|
2383
770
|
last = update.status;
|
|
2384
|
-
|
|
771
|
+
emit("update", update, `Update: ${update.status}`);
|
|
2385
772
|
}
|
|
2386
773
|
if (update.status === "complete") return;
|
|
2387
774
|
if (update.status === "failed") throw new Error(update.error ?? "Update failed.");
|