mindwire 0.1.34 → 0.1.38

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.
Files changed (44) hide show
  1. package/dist/binary-cache-CG52TXCZ.js +3 -0
  2. package/dist/{binary-cache-VM7VHCEU.js.map → binary-cache-CG52TXCZ.js.map} +1 -1
  3. package/dist/{chunk-3QFDSYKC.js → chunk-CKTVMUS4.js} +547 -177
  4. package/dist/chunk-CKTVMUS4.js.map +1 -0
  5. package/dist/{chunk-3FR7BFAW.js → chunk-PFYKJ6LX.js} +56 -4
  6. package/dist/chunk-PFYKJ6LX.js.map +1 -0
  7. package/dist/chunk-QODJKRFK.js +20 -0
  8. package/dist/chunk-QODJKRFK.js.map +1 -0
  9. package/dist/cli.js +763 -32
  10. package/dist/cli.js.map +1 -1
  11. package/dist/client.d.ts +6 -8
  12. package/dist/computer/connection-change.d.ts +13 -0
  13. package/dist/computer/connection-flow.d.ts +3 -3
  14. package/dist/computer/directory-http.d.ts +8 -0
  15. package/dist/computer/discovery.d.ts +13 -0
  16. package/dist/computer/lifecycle.d.ts +2 -1
  17. package/dist/computer/managed-relay.d.ts +27 -0
  18. package/dist/computer/oblien-auth.d.ts +25 -0
  19. package/dist/computer/provider-errors.d.ts +9 -0
  20. package/dist/computer/provider-info.d.ts +7 -0
  21. package/dist/computer/provider-setup.d.ts +20 -0
  22. package/dist/computer/relay-health.d.ts +2 -2
  23. package/dist/computer/relay.d.ts +11 -3
  24. package/dist/computer/setup-prompt.d.ts +9 -0
  25. package/dist/computer.d.ts +27 -1
  26. package/dist/connection-change-5F4WNCYV.js +41 -0
  27. package/dist/connection-change-5F4WNCYV.js.map +1 -0
  28. package/dist/{controller-AGT6MCJG.js → controller-3SZI7TBW.js} +199 -279
  29. package/dist/controller-3SZI7TBW.js.map +1 -0
  30. package/dist/{controller-handoff-3LAYBJUL.js → controller-handoff-KV7WRDAI.js} +4 -4
  31. package/dist/{controller-handoff-3LAYBJUL.js.map → controller-handoff-KV7WRDAI.js.map} +1 -1
  32. package/dist/conversations.d.ts +57 -0
  33. package/dist/index.cjs +37 -8
  34. package/dist/index.cjs.map +1 -1
  35. package/dist/index.d.ts +2 -0
  36. package/dist/index.js +37 -9
  37. package/dist/index.js.map +1 -1
  38. package/dist/types.d.ts +16 -0
  39. package/dist/workspace.d.ts +2 -0
  40. package/package.json +5 -5
  41. package/dist/binary-cache-VM7VHCEU.js +0 -4
  42. package/dist/chunk-3FR7BFAW.js.map +0 -1
  43. package/dist/chunk-3QFDSYKC.js.map +0 -1
  44. package/dist/controller-AGT6MCJG.js.map +0 -1
@@ -1,12 +1,12 @@
1
- import { MindwireError, TimeoutError, ApiError, RunFailedError } from './chunk-T6TD76UA.js';
1
+ import { MindwireError, binaryCacheDirectory, cachedExecutable, verifyReleaseBytes, cacheExecutable, executableChecksum, TimeoutError, ApiError, RunFailedError } from './chunk-PFYKJ6LX.js';
2
2
  import { execFile, spawn } from 'child_process';
3
- import * as fs from 'fs/promises';
4
- import { readFile } from 'fs/promises';
3
+ import * as fs2 from 'fs/promises';
4
+ import { readFile, open, rm, mkdtemp, writeFile } from 'fs/promises';
5
5
  import { promisify } from 'util';
6
6
  import { setTimeout as setTimeout$1 } from 'timers/promises';
7
7
  import { randomUUID } from 'crypto';
8
- import { homedir, networkInterfaces } from 'os';
9
- import * as path from 'path';
8
+ import { tmpdir, homedir, networkInterfaces } from 'os';
9
+ import * as path4 from 'path';
10
10
 
11
11
  // src/computer.ts
12
12
  var ComputerApi = class {
@@ -17,8 +17,17 @@ var ComputerApi = class {
17
17
  info() {
18
18
  return this.client.http.request("GET", "/computer");
19
19
  }
20
- setRoutes(routes) {
21
- return this.client.http.request("PUT", "/computer/routes", { body: { routes } });
20
+ discovery() {
21
+ return this.client.http.request("GET", "/computer/discovery");
22
+ }
23
+ configureDiscovery(enabled, url) {
24
+ return this.client.http.request("PUT", "/computer/discovery", { body: { enabled, url } });
25
+ }
26
+ directoryEnrollment(url, challenge) {
27
+ return this.client.http.request("POST", "/computer/discovery/enrollment", { body: { url, challenge } });
28
+ }
29
+ setRoutes(routes, connection) {
30
+ return this.client.http.request("PUT", "/computer/routes", { body: { routes, connection } });
22
31
  }
23
32
  updateStatus() {
24
33
  return this.client.http.request("GET", "/computer/update");
@@ -79,7 +88,7 @@ function computerPairingURI(invitation) {
79
88
  }
80
89
 
81
90
  // src/version.ts
82
- var SDK_VERSION = "0.1.34" ;
91
+ var SDK_VERSION = "0.1.38" ;
83
92
  function versionAtLeast(actual, desired) {
84
93
  if (actual === desired) return true;
85
94
  if (!actual || !/^\d+\.\d+\.\d+$/.test(actual) || !/^\d+\.\d+\.\d+$/.test(desired)) return false;
@@ -117,12 +126,12 @@ async function ensureDaemonBinary(opts = {}) {
117
126
  if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(version)) {
118
127
  throw new MindwireError(`mindwire: cannot download daemon for non-release SDK version ${version}`);
119
128
  }
120
- const path2 = await import('path');
121
- const { binaryCacheDirectory, cachedExecutable, cacheExecutable } = await import('./binary-cache-VM7VHCEU.js');
122
- const dir = path2.join(opts.cacheDir ?? await binaryCacheDirectory(daemonPlatform), version, `${daemonPlatform}-${daemonArch}`);
129
+ const path5 = await import('path');
130
+ const { binaryCacheDirectory: binaryCacheDirectory2, cachedExecutable: cachedExecutable2, cacheExecutable: cacheExecutable2 } = await import('./binary-cache-CG52TXCZ.js');
131
+ const dir = path5.join(opts.cacheDir ?? await binaryCacheDirectory2(daemonPlatform), version, `${daemonPlatform}-${daemonArch}`);
123
132
  const asset = assetName(version, daemonPlatform, daemonArch);
124
- const bin = path2.join(dir, asset);
125
- const cached = await cachedExecutable(bin);
133
+ const bin = path5.join(dir, asset);
134
+ const cached = await cachedExecutable2(bin);
126
135
  if (cached) return cached;
127
136
  const base = (opts.releaseBaseUrl ?? proc.process?.env?.MINDWIRE_RELEASE_BASE_URL ?? "https://github.com/oblien/mindwire/releases/download").replace(/\/$/, "");
128
137
  const release = `${base}/v${version}`;
@@ -145,7 +154,7 @@ async function ensureDaemonBinary(opts = {}) {
145
154
  const expected = checksumFor(await checksums.text(), asset);
146
155
  if (!expected) throw new MindwireError(`mindwire: release v${version} has no checksum for ${asset}`);
147
156
  const bytes = new Uint8Array(await binary.arrayBuffer());
148
- return cacheExecutable(bin, bytes, expected);
157
+ return cacheExecutable2(bin, bytes, expected);
149
158
  }
150
159
  var execute = promisify(execFile);
151
160
  function processAlive(pid) {
@@ -215,7 +224,7 @@ async function ownChild(child) {
215
224
  const alive = () => child.exitCode === null && child.signalCode === null;
216
225
  return { pid, identity, alive, async close() {
217
226
  if (!alive()) return;
218
- const exited = new Promise((resolve) => child.once("exit", () => resolve()));
227
+ const exited = new Promise((resolve3) => child.once("exit", () => resolve3()));
219
228
  child.kill("SIGTERM");
220
229
  const timer = setTimeout(() => {
221
230
  if (alive()) child.kill("SIGKILL");
@@ -240,6 +249,413 @@ async function adoptProcess(identity) {
240
249
  if (await matches()) process.kill(identity.pid, "SIGKILL");
241
250
  } };
242
251
  }
252
+ var RelayOutput = class _RelayOutput {
253
+ constructor(file, handle) {
254
+ this.file = file;
255
+ this.handle = handle;
256
+ }
257
+ file;
258
+ handle;
259
+ position = 0;
260
+ timer;
261
+ pending = Promise.resolve();
262
+ closing;
263
+ static async create(directory = tmpdir()) {
264
+ const file = path4.join(directory, `.relay-output-${randomUUID()}.log`);
265
+ return new _RelayOutput(file, await open(file, "ax+", 384));
266
+ }
267
+ static async adopt(directory, file) {
268
+ if (path4.dirname(file) !== path4.resolve(directory) || !/^\.relay-output-[a-f0-9-]{36}\.log$/.test(path4.basename(file))) return;
269
+ try {
270
+ const output = new _RelayOutput(file, await open(file, "r+"));
271
+ output.discard();
272
+ return output;
273
+ } catch (error) {
274
+ if (error.code !== "ENOENT") throw error;
275
+ }
276
+ }
277
+ get fd() {
278
+ return this.handle.fd;
279
+ }
280
+ async read() {
281
+ const { size } = await this.handle.stat();
282
+ if (size > 2 * 1024 * 1024) throw new Error("The tunnel helper produced too much startup output. Check its configuration.");
283
+ const data = Buffer.alloc(Math.min(65536, Math.max(0, size - this.position)));
284
+ const { bytesRead } = await this.handle.read(data, 0, data.length, this.position);
285
+ this.position += bytesRead;
286
+ return data.subarray(0, bytesRead);
287
+ }
288
+ discard() {
289
+ if (this.timer || this.closing) return;
290
+ const drain = () => {
291
+ this.pending = this.pending.then(() => this.handle.truncate(0)).catch(() => {
292
+ });
293
+ };
294
+ drain();
295
+ this.timer = setInterval(drain, 2e3);
296
+ this.timer.unref();
297
+ }
298
+ close() {
299
+ return this.closing ??= (async () => {
300
+ clearInterval(this.timer);
301
+ await this.pending;
302
+ await this.handle.close();
303
+ await rm(this.file, { force: true });
304
+ })();
305
+ }
306
+ };
307
+
308
+ // src/computer/provider-errors.ts
309
+ var ProviderConnectionError = class extends Error {
310
+ constructor(code, message) {
311
+ super(message);
312
+ this.code = code;
313
+ this.name = "ProviderConnectionError";
314
+ }
315
+ code;
316
+ };
317
+ function providerNeedsAction(code) {
318
+ return ["provider_sign_in", "provider_limit", "provider_missing", "provider_certificate", "provider_configuration"].includes(code ?? "");
319
+ }
320
+ function hasCertificateError(error) {
321
+ const seen = /* @__PURE__ */ new Set();
322
+ for (let current = error; current && typeof current === "object" && seen.size < 8 && !seen.has(current); ) {
323
+ seen.add(current);
324
+ const value = current;
325
+ if ([
326
+ "CERT_HAS_EXPIRED",
327
+ "CERT_NOT_YET_VALID",
328
+ "ERR_TLS_CERT_ALTNAME_INVALID",
329
+ "DEPTH_ZERO_SELF_SIGNED_CERT",
330
+ "SELF_SIGNED_CERT_IN_CHAIN",
331
+ "UNABLE_TO_VERIFY_LEAF_SIGNATURE",
332
+ "UNABLE_TO_GET_ISSUER_CERT_LOCALLY",
333
+ "CERT_REVOKED"
334
+ ].includes(value.code ?? "") || /certificate (?:has expired|expired|verify failed)|self[- ]signed certificate|CERT_HAS_EXPIRED/i.test(value.message ?? "")) return true;
335
+ current = value.cause;
336
+ }
337
+ return false;
338
+ }
339
+ function providerFailure(error, provider) {
340
+ if (error instanceof ProviderConnectionError) return error;
341
+ const value = error;
342
+ const setup = `mindwire connection ${provider.toLowerCase()}`;
343
+ if (hasCertificateError(error)) return new ProviderConnectionError(
344
+ "provider_certificate",
345
+ `${provider}'s TLS certificate could not be verified. The tunnel provider must fix its certificate; also check this computer's date and time. Your saved phone keys are unchanged.`
346
+ );
347
+ if (value?.code === "provider_sign_in" || [401, 403].includes(value?.status ?? 0) || /unexpected server response: (?:401|403)\b/i.test(value?.message ?? "")) {
348
+ return new ProviderConnectionError("provider_sign_in", `Sign in to ${provider} again with ${setup}. Your saved phone keys are unchanged.`);
349
+ }
350
+ if (value?.status === 402) return new ProviderConnectionError("provider_limit", `${provider}'s tunnel allowance was reached. Check your plan in its dashboard or choose another provider.`);
351
+ if (value?.status === 404) return new ProviderConnectionError("provider_missing", `This computer's ${provider} tunnel was removed. Run ${setup} to configure it again.`);
352
+ if (value?.status === 400 || value?.code === "provider_configuration") return new ProviderConnectionError("provider_configuration", `${provider}'s connection settings are incomplete or invalid. Run ${setup} to configure it again.`);
353
+ return new ProviderConnectionError("provider_connection", `${provider} couldn't connect. Check internet access and the provider account, then run ${setup}.`);
354
+ }
355
+ var CLOUDFLARED_VERSION = "2026.9.3";
356
+ var execute2 = promisify(execFile);
357
+ async function ensureCloudflareBinary(options = {}) {
358
+ const platform = options.platform ?? process.platform, arch = options.arch ?? process.arch;
359
+ const cpu = arch === "x64" ? "amd64" : arch === "arm64" ? "arm64" : "";
360
+ if (!cpu || !["darwin", "linux", "win32"].includes(platform) || platform === "win32" && cpu !== "amd64") {
361
+ throw new Error("Install cloudflared for this computer, or select another connection provider.");
362
+ }
363
+ const asset = `cloudflared-${platform === "win32" ? "windows" : platform}-${cpu}${platform === "darwin" ? ".tgz" : platform === "win32" ? ".exe" : ""}`;
364
+ const bin = path4.join(
365
+ options.cacheDir ?? await binaryCacheDirectory(),
366
+ "cloudflared",
367
+ CLOUDFLARED_VERSION,
368
+ `${platform}-${arch}`,
369
+ platform === "win32" ? "cloudflared.exe" : "cloudflared"
370
+ );
371
+ const cached = await cachedExecutable(bin);
372
+ if (cached) return cached;
373
+ await options.onProgress?.("Downloading the secure connection helper\u2026");
374
+ const request = options.fetch ?? fetch;
375
+ const release = await request(
376
+ `https://api.github.com/repos/cloudflare/cloudflared/releases/tags/${CLOUDFLARED_VERSION}`,
377
+ { headers: { Accept: "application/vnd.github+json" }, signal: AbortSignal.timeout(2e4) }
378
+ );
379
+ if (!release.ok) throw new Error("Couldn't download the connection helper. Retry, install cloudflared yourself, or choose another provider.");
380
+ const metadata = await release.json();
381
+ const item = metadata.assets?.find((value) => value.name === asset);
382
+ const checksum = item?.digest?.match(/^sha256:([a-f0-9]{64})$/i)?.[1];
383
+ const expectedURL = `https://github.com/cloudflare/cloudflared/releases/download/${CLOUDFLARED_VERSION}/${asset}`;
384
+ if (!checksum || item?.browser_download_url !== expectedURL) throw new Error("The connection helper has no verified release asset for this computer.");
385
+ const downloaded = await request(expectedURL, { signal: AbortSignal.timeout(12e4) });
386
+ if (!downloaded.ok) throw new Error("Couldn't download the connection helper. Check your internet connection and retry.");
387
+ const archive = new Uint8Array(await downloaded.arrayBuffer());
388
+ await verifyReleaseBytes(archive, checksum, asset);
389
+ let executable = archive;
390
+ if (platform === "darwin") {
391
+ const temporary = await mkdtemp(path4.join(tmpdir(), "mindwire-cloudflared-"));
392
+ try {
393
+ const archivePath = path4.join(temporary, asset);
394
+ await writeFile(archivePath, archive, { mode: 384 });
395
+ const result = await execute2(
396
+ "/usr/bin/tar",
397
+ ["-xzOf", archivePath, "cloudflared"],
398
+ { encoding: "buffer", maxBuffer: 128 * 1024 * 1024, timeout: 3e4 }
399
+ );
400
+ executable = new Uint8Array(result.stdout);
401
+ } finally {
402
+ await rm(temporary, { recursive: true, force: true });
403
+ }
404
+ }
405
+ return cacheExecutable(bin, executable, await executableChecksum(executable));
406
+ }
407
+ async function cloudflareCommand(options) {
408
+ try {
409
+ await execute2("cloudflared", ["--version"], { timeout: 3e3, windowsHide: true });
410
+ return "cloudflared";
411
+ } catch (error) {
412
+ if (error.code !== "ENOENT") throw new Error("The installed cloudflared could not start. Repair it or choose another connection provider.");
413
+ return ensureCloudflareBinary(options);
414
+ }
415
+ }
416
+
417
+ // src/computer/relay.ts
418
+ async function adoptRelay(owner, route, directory, outputFile, configFile) {
419
+ const output = outputFile ? await RelayOutput.adopt(directory, outputFile) : void 0;
420
+ const ownedConfig = configFile && path4.dirname(configFile) === path4.resolve(directory) && /^\.relay-config-[a-f0-9-]{36}\.json$/.test(path4.basename(configFile)) ? configFile : void 0;
421
+ return {
422
+ route,
423
+ owner,
424
+ outputFile: output?.file,
425
+ configFile: ownedConfig,
426
+ async close() {
427
+ await owner.close();
428
+ await output?.close();
429
+ if (ownedConfig) await fs2.rm(ownedConfig, { force: true });
430
+ }
431
+ };
432
+ }
433
+ function websocketURL(value) {
434
+ const url = new URL(value);
435
+ if (url.protocol === "https:") url.protocol = "wss:";
436
+ if (url.protocol !== "wss:" || !url.hostname || url.username || url.password || url.hash) {
437
+ throw new Error("Relay URLs must use wss:// with no embedded credentials or fragment.");
438
+ }
439
+ if (!url.pathname || url.pathname === "/") url.pathname = "/ssh";
440
+ return url.toString();
441
+ }
442
+ function validateRelayOptions(options) {
443
+ if (options.url) websocketURL(options.url);
444
+ if (options.kind === "custom" && !options.url) throw new Error("A custom relay needs --relay-url wss://your-host/ssh.");
445
+ if (options.kind === "oblien" && (!options.url || !options.oblienCredentialsFile || !Number.isSafeInteger(options.oblienTunnelId) || options.oblienTunnelId < 1)) {
446
+ throw new Error("Oblien setup is incomplete. Run mindwire connection oblien.");
447
+ }
448
+ if (options.kind === "cloudflare") {
449
+ const named = !!options.cloudflareTokenFile || !!options.cloudflareCredentialsFile && !!options.cloudflareTunnelId;
450
+ if (named && !options.url || options.url && !named) throw new Error("A named Cloudflare tunnel needs its hostname and credentials. Run mindwire connection cloudflare.");
451
+ if (options.cloudflareTokenFile && options.cloudflareCredentialsFile) throw new Error("Choose one Cloudflare tunnel credential source.");
452
+ }
453
+ if (options.kind === "ngrok" && options.ngrokTokenFile && !options.url) throw new Error("ngrok needs a reserved domain. Run mindwire connection ngrok.");
454
+ }
455
+ async function startRelay(options, port, runtime = {}) {
456
+ runtime.signal?.throwIfAborted();
457
+ validateRelayOptions(options);
458
+ if (options.kind === "none") return { close() {
459
+ } };
460
+ if (options.kind === "custom") {
461
+ if (!options.url) throw new Error("A custom relay needs --relay-url wss://your-host/ssh.");
462
+ return { route: { kind: "websocket", url: websocketURL(options.url) }, close() {
463
+ } };
464
+ }
465
+ const origin = `http://127.0.0.1:${port}`;
466
+ const env = { ...process.env };
467
+ let configFile;
468
+ const managed = options.kind === "oblien" || options.kind === "ngrok" && !!options.ngrokTokenFile;
469
+ let command, args;
470
+ if (managed) {
471
+ if (!runtime.cliPath || !runtime.outputDirectory) throw new Error("The managed relay needs its connection controller.");
472
+ configFile = path4.join(runtime.outputDirectory, `.relay-config-${randomUUID()}.json`);
473
+ await fs2.writeFile(configFile, JSON.stringify({ options, port }), { mode: 384 });
474
+ command = process.execPath;
475
+ args = [runtime.cliPath, "_relay", "--relay-config", configFile];
476
+ await runtime.onProgress?.(`Connecting through ${options.kind === "oblien" ? "Oblien" : "ngrok"}\u2026`);
477
+ } else if (options.kind === "cloudflare") {
478
+ command = await cloudflareCommand(runtime);
479
+ await runtime.onProgress?.("Connecting through Cloudflare\u2026");
480
+ if (options.cloudflareCredentialsFile && options.cloudflareTunnelId) {
481
+ if (!options.url || !runtime.outputDirectory) throw new Error("Named Cloudflare tunnel setup is incomplete.");
482
+ configFile = path4.join(runtime.outputDirectory, `.relay-config-${randomUUID()}.json`);
483
+ await fs2.writeFile(configFile, JSON.stringify({
484
+ tunnel: options.cloudflareTunnelId,
485
+ "credentials-file": options.cloudflareCredentialsFile,
486
+ ingress: [{ hostname: new URL(websocketURL(options.url)).hostname, service: origin }, { service: "http_status:404" }]
487
+ }), { mode: 384 });
488
+ args = ["tunnel", "--config", configFile, "--no-autoupdate", "run"];
489
+ } else if (options.cloudflareTokenFile) {
490
+ if (!options.url) throw new Error("A named Cloudflare tunnel needs --relay-url for its configured hostname.");
491
+ env.TUNNEL_TOKEN = (await readFile(options.cloudflareTokenFile, "utf8")).trim();
492
+ args = ["tunnel", "--no-autoupdate", "run"];
493
+ } else {
494
+ if (options.url) throw new Error("Use --cloudflare-token-file with a named tunnel, or --relay custom for an existing tunnel.");
495
+ args = ["tunnel", "--no-autoupdate", "--url", origin];
496
+ }
497
+ } else {
498
+ command = "ngrok";
499
+ args = ["http", origin, "--log", "stdout", "--log-format", "json"];
500
+ if (options.url) {
501
+ const url = new URL(websocketURL(options.url));
502
+ args.push("--url", `https://${url.host}`);
503
+ }
504
+ }
505
+ runtime.signal?.throwIfAborted();
506
+ const output = await RelayOutput.create(runtime.outputDirectory);
507
+ const child = spawn(command, args, { env, detached: true, stdio: ["ignore", output.fd, output.fd], windowsHide: true });
508
+ let failure;
509
+ child.on("error", (error) => {
510
+ failure = error;
511
+ });
512
+ let owner;
513
+ try {
514
+ await new Promise((resolve3, reject) => {
515
+ child.once("spawn", resolve3);
516
+ child.once("error", reject);
517
+ });
518
+ owner = await ownChild(child);
519
+ await runtime.onSpawn?.(owner, output.file, configFile);
520
+ const deadline = Date.now() + 45e3;
521
+ let buffer = "", cloudflareAddress = options.url, cloudflareConnected = false;
522
+ while (Date.now() < deadline) {
523
+ runtime.signal?.throwIfAborted();
524
+ if (failure) throw new Error(`Could not run ${options.kind}. Check its installation.`, { cause: failure });
525
+ const received = buffer + (await output.read()).toString();
526
+ buffer = received.slice(-16384);
527
+ let found;
528
+ if (!managed && options.kind === "cloudflare") {
529
+ cloudflareAddress ??= received.match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/)?.[0];
530
+ cloudflareConnected ||= /Registered tunnel connection/.test(received);
531
+ if (cloudflareConnected) found = cloudflareAddress;
532
+ } else {
533
+ for (const line of received.split(/\r?\n/)) {
534
+ try {
535
+ const event = JSON.parse(line);
536
+ if (managed && event.event === "relay_error") failure = new ProviderConnectionError(
537
+ providerNeedsAction(event.code) ? event.code : "provider_connection",
538
+ event.message ?? "The provider couldn't connect."
539
+ );
540
+ if (managed && event.event === "relay_ready") found = event.url;
541
+ if (event.msg === "started tunnel" && event.url?.startsWith("https://")) found = event.url;
542
+ } catch {
543
+ }
544
+ }
545
+ }
546
+ if (found) {
547
+ const route = { kind: "websocket", url: websocketURL(found) };
548
+ output.discard();
549
+ const provider = owner;
550
+ return {
551
+ route,
552
+ process: child,
553
+ owner,
554
+ outputFile: output.file,
555
+ configFile,
556
+ async close() {
557
+ await provider.close();
558
+ await output.close();
559
+ if (configFile) await fs2.rm(configFile, { force: true });
560
+ }
561
+ };
562
+ }
563
+ if (failure) throw failure;
564
+ if (!owner.alive()) throw new Error(`${options.kind} exited before the tunnel connected. Check its account configuration.`);
565
+ await setTimeout$1(50, void 0, { signal: runtime.signal });
566
+ }
567
+ throw new Error(`${options.kind} did not establish its tunnel. Check your internet connection and its configuration.`);
568
+ } catch (error) {
569
+ if (owner) await owner.close();
570
+ else if (child.pid) await (await ownChild(child)).close();
571
+ await output.close();
572
+ if (configFile) await fs2.rm(configFile, { force: true });
573
+ throw error;
574
+ }
575
+ }
576
+ async function acquireProcessLock(file, options = {}) {
577
+ const owner = await currentProcessIdentity();
578
+ const queue = file + ".queue", id = randomUUID() + ".json", claimPath = queue + "/" + id;
579
+ await fs2.mkdir(queue, { recursive: true, mode: 448 });
580
+ let lock;
581
+ const release = async () => {
582
+ try {
583
+ if (lock) {
584
+ const held = lock;
585
+ lock = void 0;
586
+ const owned = await held.stat();
587
+ await held.close();
588
+ const current = await fs2.stat(file).catch(() => void 0);
589
+ if (current?.ino === owned.ino && current.dev === owned.dev) await fs2.rm(file, { force: true });
590
+ }
591
+ } finally {
592
+ await fs2.rm(claimPath, { force: true });
593
+ }
594
+ };
595
+ const publish = async (claim) => {
596
+ const temporary = claimPath + ".tmp";
597
+ try {
598
+ await fs2.writeFile(temporary, JSON.stringify(claim), { mode: 384 });
599
+ await fs2.rename(temporary, claimPath);
600
+ } finally {
601
+ await fs2.rm(temporary, { force: true });
602
+ }
603
+ };
604
+ const claims = async () => {
605
+ const entries = [];
606
+ for (const candidate of await fs2.readdir(queue)) {
607
+ if (!candidate.endsWith(".json")) continue;
608
+ const content = await fs2.readFile(queue + "/" + candidate, "utf8").catch((error) => {
609
+ if (error.code === "ENOENT") return void 0;
610
+ throw error;
611
+ });
612
+ if (content === void 0) continue;
613
+ const claim = JSON.parse(content);
614
+ if (!claim.owner || claim.ticket !== null && (!Number.isSafeInteger(claim.ticket) || claim.ticket < 1)) {
615
+ throw new Error("Invalid Mindwire lock ticket.");
616
+ }
617
+ if (await processStateAlive(claim.owner)) entries.push({ id: candidate, claim });
618
+ else await fs2.rm(queue + "/" + candidate, { force: true });
619
+ }
620
+ return entries;
621
+ };
622
+ try {
623
+ options.signal?.throwIfAborted();
624
+ await publish({ owner, ticket: null });
625
+ const ticket = Math.max(0, ...(await claims()).map((entry) => entry.claim.ticket ?? 0)) + 1;
626
+ if (!Number.isSafeInteger(ticket)) throw new Error("Mindwire's connection queue is full. Retry shortly.");
627
+ await publish({ owner, ticket });
628
+ const deadline = Date.now() + 3e4;
629
+ while (Date.now() < deadline) {
630
+ options.signal?.throwIfAborted();
631
+ if (await options.onWait?.()) {
632
+ await release();
633
+ return void 0;
634
+ }
635
+ const ahead = (await claims()).some((entry) => entry.id !== id && (entry.claim.ticket === null || entry.claim.ticket < ticket || entry.claim.ticket === ticket && entry.id < id));
636
+ if (!ahead) {
637
+ try {
638
+ lock = await fs2.open(file, "wx", 384);
639
+ await lock.writeFile(JSON.stringify(owner));
640
+ return { close: release };
641
+ } catch (error) {
642
+ if (error.code !== "EEXIST") throw error;
643
+ const holder = await readProcessState(file);
644
+ const age = Date.now() - (await fs2.stat(file).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
645
+ if (age > 5e3 && !await processStateAlive(holder)) {
646
+ await fs2.rm(file, { force: true });
647
+ continue;
648
+ }
649
+ }
650
+ }
651
+ await setTimeout$1(250, void 0, { signal: options.signal });
652
+ }
653
+ throw new Error("Another Mindwire operation is in progress. Try again shortly.");
654
+ } catch (error) {
655
+ await release();
656
+ throw error;
657
+ }
658
+ }
243
659
 
244
660
  // src/http.ts
245
661
  var DEFAULT_TIMEOUT_MS = 12e4;
@@ -287,13 +703,13 @@ var Http = class {
287
703
  * SDK error instead of a bare `TypeError: fetch failed`. An abort (the request's own signal, or
288
704
  * our timeout controller) is re-thrown untouched — `fetchWithTimeout` classifies it.
289
705
  */
290
- async fetchOnce(f, url, init, method, path2) {
706
+ async fetchOnce(f, url, init, method, path5) {
291
707
  try {
292
708
  return await f(url, init);
293
709
  } catch (e) {
294
710
  const aborted = init.signal?.aborted ?? false;
295
711
  if (aborted || isAbortError(e)) throw e;
296
- throw new MindwireError(`mindwire: ${method} ${path2} \u2014 network request failed`, { cause: e });
712
+ throw new MindwireError(`mindwire: ${method} ${path5} \u2014 network request failed`, { cause: e });
297
713
  }
298
714
  }
299
715
  /**
@@ -302,9 +718,9 @@ var Http = class {
302
718
  * `timeoutMs`; whichever aborts first wins. Timeout → {@link TimeoutError}; the caller's own abort
303
719
  * propagates as-is (its `reason`). `timeoutMs<=0` disables the deadline entirely.
304
720
  */
305
- async fetchWithTimeout(f, url, init, userSignal, method, path2) {
721
+ async fetchWithTimeout(f, url, init, userSignal, method, path5) {
306
722
  if (!this.timeoutMs || this.timeoutMs <= 0) {
307
- return this.fetchOnce(f, url, { ...init, ...userSignal ? { signal: userSignal } : {} }, method, path2);
723
+ return this.fetchOnce(f, url, { ...init, ...userSignal ? { signal: userSignal } : {} }, method, path5);
308
724
  }
309
725
  const ctrl = new AbortController();
310
726
  const onAbort = () => ctrl.abort(userSignal?.reason);
@@ -318,9 +734,9 @@ var Http = class {
318
734
  ctrl.abort();
319
735
  }, this.timeoutMs);
320
736
  try {
321
- return await this.fetchOnce(f, url, { ...init, signal: ctrl.signal }, method, path2);
737
+ return await this.fetchOnce(f, url, { ...init, signal: ctrl.signal }, method, path5);
322
738
  } catch (e) {
323
- if (timedOut) throw new TimeoutError(method, path2, this.timeoutMs);
739
+ if (timedOut) throw new TimeoutError(method, path5, this.timeoutMs);
324
740
  if (userSignal?.aborted) throw userSignal.reason ?? e;
325
741
  throw e;
326
742
  } finally {
@@ -359,8 +775,8 @@ var Http = class {
359
775
  async ready() {
360
776
  await this.base();
361
777
  }
362
- url(baseUrl, path2, query) {
363
- const u = new URL(baseUrl + (path2.startsWith("/") ? path2 : `/${path2}`));
778
+ url(baseUrl, path5, query) {
779
+ const u = new URL(baseUrl + (path5.startsWith("/") ? path5 : `/${path5}`));
364
780
  if (query) {
365
781
  for (const [k, v] of Object.entries(query)) {
366
782
  if (v !== void 0) u.searchParams.set(k, String(v));
@@ -374,10 +790,10 @@ var Http = class {
374
790
  if (hasBody) h["Content-Type"] = "application/json";
375
791
  return h;
376
792
  }
377
- async request(method, path2, init = {}) {
793
+ async request(method, path5, init = {}) {
378
794
  const base = await this.base();
379
795
  const hasBody = init.body !== void 0;
380
- const url = this.url(base.baseUrl, path2, init.query);
796
+ const url = this.url(base.baseUrl, path5, init.query);
381
797
  const extra = { ...base.headers, ...init.headers };
382
798
  const f = base.fetch ?? this.fetchImpl;
383
799
  const send = async (force) => this.fetchWithTimeout(
@@ -390,7 +806,7 @@ var Http = class {
390
806
  },
391
807
  init.signal,
392
808
  method,
393
- path2
809
+ path5
394
810
  );
395
811
  let res = await send(false);
396
812
  if (res.status === 401 && base.getToken) res = await send(true);
@@ -401,13 +817,13 @@ var Http = class {
401
817
  try {
402
818
  return JSON.parse(text);
403
819
  } catch (e) {
404
- throw new MindwireError(`mindwire: ${method} ${path2} returned a non-JSON body`, { cause: e });
820
+ throw new MindwireError(`mindwire: ${method} ${path5} returned a non-JSON body`, { cause: e });
405
821
  }
406
822
  }
407
823
  /** Open a streaming response (SSE). Caller owns the body. */
408
- async open(method, path2, init = {}) {
824
+ async open(method, path5, init = {}) {
409
825
  const base = await this.base();
410
- const url = this.url(base.baseUrl, path2, init.query);
826
+ const url = this.url(base.baseUrl, path5, init.query);
411
827
  const extra = { Accept: "text/event-stream", ...base.headers, ...init.headers };
412
828
  const f = base.fetch ?? this.fetchImpl;
413
829
  const send = async (force) => this.fetchOnce(
@@ -420,13 +836,13 @@ var Http = class {
420
836
  ...init.signal ? { signal: init.signal } : {}
421
837
  },
422
838
  method,
423
- path2
839
+ path5
424
840
  );
425
841
  let res = await send(false);
426
842
  if (res.status === 401 && base.getToken) res = await send(true);
427
843
  if (!res.ok) throw await this.toApiError(method, res);
428
844
  if (!res.body)
429
- throw new MindwireError(`mindwire: ${method} ${path2} returned no response body to stream`);
845
+ throw new MindwireError(`mindwire: ${method} ${path5} returned no response body to stream`);
430
846
  return res;
431
847
  }
432
848
  async toApiError(method, res) {
@@ -676,12 +1092,12 @@ var ProjectSyncError = class extends MindwireError {
676
1092
  };
677
1093
  var active = (o) => ["queued", "exporting", "preparing", "applying"].includes(o.status);
678
1094
  var prefix = "/workspace/sync";
679
- function delay2(ms, signal) {
1095
+ function delay4(ms, signal) {
680
1096
  signal?.throwIfAborted();
681
- return new Promise((resolve, reject) => {
1097
+ return new Promise((resolve3, reject) => {
682
1098
  const done = () => {
683
1099
  signal?.removeEventListener("abort", abort);
684
- resolve();
1100
+ resolve3();
685
1101
  };
686
1102
  const timer = setTimeout(done, ms);
687
1103
  const abort = () => {
@@ -743,7 +1159,7 @@ var ProjectSyncApi = class {
743
1159
  const o = await this.get(id, { signal: options.signal });
744
1160
  options.onUpdate?.(o);
745
1161
  if (!active(o)) return o;
746
- await delay2(500, options.signal);
1162
+ await delay4(500, options.signal);
747
1163
  }
748
1164
  }
749
1165
  /** Repeating this relay skips acknowledged checkpoints and verified chunks.
@@ -830,6 +1246,23 @@ var ProjectSyncApi = class {
830
1246
  }
831
1247
  };
832
1248
 
1249
+ // src/conversations.ts
1250
+ var ConversationsApi = class {
1251
+ constructor(mw) {
1252
+ this.mw = mw;
1253
+ }
1254
+ mw;
1255
+ /** Read-only; includes folders not registered as Mindwire projects. */
1256
+ list(query = {}) {
1257
+ return this.mw.http.request("GET", "/workspace/conversations", { query: { ...query } });
1258
+ }
1259
+ /** Idempotently associates the selected native session with its original folder.
1260
+ * Use chatId with messages()/turn() and the ordinary chat APIs afterward. */
1261
+ open(request) {
1262
+ return this.mw.http.request("POST", "/workspace/conversations/open", { body: request });
1263
+ }
1264
+ };
1265
+
833
1266
  // src/workspace.ts
834
1267
  var ProjectOperationsApi = class {
835
1268
  constructor(mw) {
@@ -901,6 +1334,7 @@ var WorkspaceCollection = class {
901
1334
  var WorkspaceApi = class {
902
1335
  constructor(mw) {
903
1336
  this.mw = mw;
1337
+ this.conversations = new ConversationsApi(mw);
904
1338
  this.sync = new ProjectSyncApi(mw);
905
1339
  this.git = new GitAccessApi(mw);
906
1340
  this.operations = new ProjectOperationsApi(mw);
@@ -909,6 +1343,7 @@ var WorkspaceApi = class {
909
1343
  this.chats = new WorkspaceCollection(mw, "chats");
910
1344
  }
911
1345
  mw;
1346
+ conversations;
912
1347
  sync;
913
1348
  git;
914
1349
  operations;
@@ -919,8 +1354,8 @@ var WorkspaceApi = class {
919
1354
  return this.mw.http.request("GET", "/workspace", { query: options });
920
1355
  }
921
1356
  /** Read the saved icon, or preview a candidate relative path within the project. Requires projectIconsVersion >= 1. */
922
- projectIcon(projectId, path2) {
923
- return this.mw.http.request("GET", `/workspace/projects/${encodeURIComponent(projectId)}/icon`, { query: { path: path2 } });
1357
+ projectIcon(projectId, path5) {
1358
+ return this.mw.http.request("GET", `/workspace/projects/${encodeURIComponent(projectId)}/icon`, { query: { path: path5 } });
924
1359
  }
925
1360
  /** Start an operation owned by the daemon. The same ID/payload returns the existing operation. */
926
1361
  createProject(request) {
@@ -1117,17 +1552,17 @@ var ExecutionApi = class {
1117
1552
  resources() {
1118
1553
  return this.client.http.request("GET", "/workspace/resources");
1119
1554
  }
1120
- files(path2 = "", query) {
1121
- return this.client.http.request("GET", "/workspace/files", { query: { path: path2, query } });
1555
+ files(path5 = "", query) {
1556
+ return this.client.http.request("GET", "/workspace/files", { query: { path: path5, query } });
1122
1557
  }
1123
- read(path2) {
1124
- return this.client.http.request("GET", "/workspace/file", { query: { path: path2 } });
1558
+ read(path5) {
1559
+ return this.client.http.request("GET", "/workspace/file", { query: { path: path5 } });
1125
1560
  }
1126
- write(path2, content) {
1127
- return this.client.http.request("PUT", "/workspace/file", { body: { path: path2, content } });
1561
+ write(path5, content) {
1562
+ return this.client.http.request("PUT", "/workspace/file", { body: { path: path5, content } });
1128
1563
  }
1129
- remove(path2) {
1130
- return this.client.http.request("DELETE", "/workspace/file", { query: { path: path2 } });
1564
+ remove(path5) {
1565
+ return this.client.http.request("DELETE", "/workspace/file", { query: { path: path5 } });
1131
1566
  }
1132
1567
  exec(command, signal) {
1133
1568
  return this.client.http.request("POST", "/workspace/exec", { body: command, signal });
@@ -1201,7 +1636,7 @@ async function spawnDaemon(opts) {
1201
1636
  "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."
1202
1637
  );
1203
1638
  }
1204
- const { spawn: spawn2 } = await import('child_process');
1639
+ const { spawn: spawn3 } = await import('child_process');
1205
1640
  const net = await import('net');
1206
1641
  const proc = serverRuntime().process;
1207
1642
  const { randomBytes } = await import('crypto');
@@ -1210,7 +1645,7 @@ async function spawnDaemon(opts) {
1210
1645
  const port = await freePort(net);
1211
1646
  const baseUrl = `http://127.0.0.1:${port}`;
1212
1647
  const token = randomBytes(32).toString("hex");
1213
- const child = spawn2(bin, [], {
1648
+ const child = spawn3(bin, [], {
1214
1649
  env: {
1215
1650
  ...proc.env,
1216
1651
  ADDR: `127.0.0.1:${port}`,
@@ -1239,8 +1674,8 @@ async function spawnDaemon(opts) {
1239
1674
  return { baseUrl, token, stop };
1240
1675
  }
1241
1676
  async function resolveBinary(explicit) {
1242
- const fs3 = await import('fs');
1243
- const path2 = await import('path');
1677
+ const fs4 = await import('fs');
1678
+ const path5 = await import('path');
1244
1679
  const proc = serverRuntime().process;
1245
1680
  const ext = proc.platform === "win32" ? ".exe" : "";
1246
1681
  const candidates = [];
@@ -1248,14 +1683,14 @@ async function resolveBinary(explicit) {
1248
1683
  if (proc.env?.MINDWIRE_DAEMON) candidates.push(proc.env.MINDWIRE_DAEMON);
1249
1684
  try {
1250
1685
  const url = await import('url');
1251
- const here = path2.dirname(url.fileURLToPath(import.meta.url));
1686
+ const here = path5.dirname(url.fileURLToPath(import.meta.url));
1252
1687
  const named = `mindwired-${proc.platform}-${proc.arch}${ext}`;
1253
- candidates.push(path2.join(here, "bin", named), path2.join(here, "..", "bin", named));
1688
+ candidates.push(path5.join(here, "bin", named), path5.join(here, "..", "bin", named));
1254
1689
  } catch {
1255
1690
  }
1256
1691
  for (const c of candidates) {
1257
1692
  try {
1258
- if (fs3.existsSync(c)) return { bin: c, found: true, checked: candidates };
1693
+ if (fs4.existsSync(c)) return { bin: c, found: true, checked: candidates };
1259
1694
  } catch {
1260
1695
  }
1261
1696
  }
@@ -1278,13 +1713,13 @@ function daemonStartError(resolved, cause) {
1278
1713
  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.`;
1279
1714
  }
1280
1715
  function freePort(net) {
1281
- return new Promise((resolve, reject) => {
1716
+ return new Promise((resolve3, reject) => {
1282
1717
  const srv = net.createServer();
1283
1718
  srv.on("error", reject);
1284
1719
  srv.listen(0, "127.0.0.1", () => {
1285
1720
  const addr = srv.address();
1286
1721
  const port = addr && typeof addr === "object" ? addr.port : 0;
1287
- srv.close(() => resolve(port));
1722
+ srv.close(() => resolve3(port));
1288
1723
  });
1289
1724
  });
1290
1725
  }
@@ -1542,14 +1977,14 @@ var Mindwire = class _Mindwire {
1542
1977
  }
1543
1978
  /**
1544
1979
  * `POST /chats/{id}/fork` — clone a chat into a new id (generated when `newChatId` is omitted).
1545
- * The fork shares the source's native session until its first turn, which branches it (natively
1546
- * on Claude via `--fork-session`; a fresh session on agents without native fork). Rejects with a
1547
- * 409 if the source has a live turn, 404 if the source is unknown, 400 if the target id is in
1548
- * use. Returns the new chat's summary.
1980
+ * Codex and Claude preserve native context. `beforeMessageId` excludes that prompt and every
1981
+ * later message; send the edited prompt to the returned chat with `turn`. Reuse `newChatId`
1982
+ * and the turn's `requestId` on retries. The source chat and filesystem are unchanged.
1983
+ * A source with a live turn returns 409; an unknown source returns 404.
1549
1984
  */
1550
1985
  forkChat(chatId, opts = {}) {
1551
1986
  return this.http.request("POST", `/chats/${encodeURIComponent(chatId)}/fork`, {
1552
- body: opts.newChatId ? { newChatId: opts.newChatId } : {}
1987
+ body: opts
1553
1988
  });
1554
1989
  }
1555
1990
  /**
@@ -1688,8 +2123,8 @@ var Mindwire = class _Mindwire {
1688
2123
  * agent type. Frames are live snapshots (no replay); an empty `samples` array is a valid keep-alive.
1689
2124
  */
1690
2125
  async *processes(opts = {}) {
1691
- const path2 = "/processes/stream" + (opts.agent ? `?agent=${encodeURIComponent(opts.agent)}` : "");
1692
- const res = await this.http.open("GET", path2, {
2126
+ const path5 = "/processes/stream" + (opts.agent ? `?agent=${encodeURIComponent(opts.agent)}` : "");
2127
+ const res = await this.http.open("GET", path5, {
1693
2128
  ...opts.signal ? { signal: opts.signal } : {}
1694
2129
  });
1695
2130
  yield* readSSE(res.body, opts.signal);
@@ -1979,97 +2414,14 @@ var NotifyApi = class {
1979
2414
  await this.mw.http.request("DELETE", `/notify/rules/${encodeURIComponent(id)}`);
1980
2415
  }
1981
2416
  };
1982
- async function acquireProcessLock(file, options = {}) {
1983
- const owner = await currentProcessIdentity();
1984
- const queue = file + ".queue", id = randomUUID() + ".json", claimPath = queue + "/" + id;
1985
- await fs.mkdir(queue, { recursive: true, mode: 448 });
1986
- let lock;
1987
- const release = async () => {
1988
- try {
1989
- if (lock) {
1990
- const held = lock;
1991
- lock = void 0;
1992
- const owned = await held.stat();
1993
- await held.close();
1994
- const current = await fs.stat(file).catch(() => void 0);
1995
- if (current?.ino === owned.ino && current.dev === owned.dev) await fs.rm(file, { force: true });
1996
- }
1997
- } finally {
1998
- await fs.rm(claimPath, { force: true });
1999
- }
2000
- };
2001
- const publish = async (claim) => {
2002
- const temporary = claimPath + ".tmp";
2003
- try {
2004
- await fs.writeFile(temporary, JSON.stringify(claim), { mode: 384 });
2005
- await fs.rename(temporary, claimPath);
2006
- } finally {
2007
- await fs.rm(temporary, { force: true });
2008
- }
2009
- };
2010
- const claims = async () => {
2011
- const entries = [];
2012
- for (const candidate of await fs.readdir(queue)) {
2013
- if (!candidate.endsWith(".json")) continue;
2014
- const content = await fs.readFile(queue + "/" + candidate, "utf8").catch((error) => {
2015
- if (error.code === "ENOENT") return void 0;
2016
- throw error;
2017
- });
2018
- if (content === void 0) continue;
2019
- const claim = JSON.parse(content);
2020
- if (!claim.owner || claim.ticket !== null && (!Number.isSafeInteger(claim.ticket) || claim.ticket < 1)) {
2021
- throw new Error("Invalid Mindwire lock ticket.");
2022
- }
2023
- if (await processStateAlive(claim.owner)) entries.push({ id: candidate, claim });
2024
- else await fs.rm(queue + "/" + candidate, { force: true });
2025
- }
2026
- return entries;
2027
- };
2028
- try {
2029
- options.signal?.throwIfAborted();
2030
- await publish({ owner, ticket: null });
2031
- const ticket = Math.max(0, ...(await claims()).map((entry) => entry.claim.ticket ?? 0)) + 1;
2032
- if (!Number.isSafeInteger(ticket)) throw new Error("Mindwire's connection queue is full. Retry shortly.");
2033
- await publish({ owner, ticket });
2034
- const deadline = Date.now() + 3e4;
2035
- while (Date.now() < deadline) {
2036
- options.signal?.throwIfAborted();
2037
- if (await options.onWait?.()) {
2038
- await release();
2039
- return void 0;
2040
- }
2041
- const ahead = (await claims()).some((entry) => entry.id !== id && (entry.claim.ticket === null || entry.claim.ticket < ticket || entry.claim.ticket === ticket && entry.id < id));
2042
- if (!ahead) {
2043
- try {
2044
- lock = await fs.open(file, "wx", 384);
2045
- await lock.writeFile(JSON.stringify(owner));
2046
- return { close: release };
2047
- } catch (error) {
2048
- if (error.code !== "EEXIST") throw error;
2049
- const holder = await readProcessState(file);
2050
- const age = Date.now() - (await fs.stat(file).catch(() => ({ mtimeMs: Date.now() }))).mtimeMs;
2051
- if (age > 5e3 && !await processStateAlive(holder)) {
2052
- await fs.rm(file, { force: true });
2053
- continue;
2054
- }
2055
- }
2056
- }
2057
- await setTimeout$1(250, void 0, { signal: options.signal });
2058
- }
2059
- throw new Error("Another Mindwire operation is in progress. Try again shortly.");
2060
- } catch (error) {
2061
- await release();
2062
- throw error;
2063
- }
2064
- }
2065
2417
 
2066
2418
  // src/computer/lifecycle.ts
2067
- var CONTROLLER_PROTOCOL = 2;
2068
- var defaultStateDirectory = () => path.join(homedir(), ".mindwire", "computer");
2419
+ var CONTROLLER_PROTOCOL = 3;
2420
+ var defaultStateDirectory = () => path4.join(homedir(), ".mindwire", "computer");
2069
2421
  var defaultComputerConfig = () => ({ directory: homedir(), bind: "0.0.0.0", sshPort: 8791, websocketPort: 8792, relay: { kind: "cloudflare" } });
2070
2422
  async function readJSON(file) {
2071
2423
  try {
2072
- return JSON.parse(await fs.readFile(file, "utf8"));
2424
+ return JSON.parse(await fs2.readFile(file, "utf8"));
2073
2425
  } catch (error) {
2074
2426
  if (error.code === "ENOENT") return void 0;
2075
2427
  throw error;
@@ -2078,18 +2430,18 @@ async function readJSON(file) {
2078
2430
  async function writeJSON(file, value) {
2079
2431
  const temp = `${file}.${randomUUID()}.tmp`;
2080
2432
  try {
2081
- await fs.writeFile(temp, JSON.stringify(value) + "\n", { mode: 384 });
2082
- await fs.rename(temp, file);
2433
+ await fs2.writeFile(temp, JSON.stringify(value) + "\n", { mode: 384 });
2434
+ await fs2.rename(temp, file);
2083
2435
  } finally {
2084
- await fs.rm(temp, { force: true });
2436
+ await fs2.rm(temp, { force: true });
2085
2437
  }
2086
2438
  }
2087
2439
  async function computerClient(directory) {
2088
- const runtime = await readJSON(path.join(directory, "computer-runtime.json"));
2440
+ const runtime = await readJSON(path4.join(directory, "computer-runtime.json"));
2089
2441
  if (!runtime) throw new Error("Mindwire is not running. Run mindwire connect on the computer.");
2090
2442
  const url = new URL(`http://${runtime.apiAddress}`);
2091
2443
  if (!["127.0.0.1", "[::1]"].includes(url.hostname) || url.username || url.password) throw new Error("Invalid local Mindwire address.");
2092
- const token = (await fs.readFile(path.join(directory, "daemon.token"), "utf8")).trim();
2444
+ const token = (await fs2.readFile(path4.join(directory, "daemon.token"), "utf8")).trim();
2093
2445
  const client = new Mindwire({ target: remote(url.origin, { token }), requestTimeoutMs: 3e3 });
2094
2446
  const info = await client.computer.info();
2095
2447
  if (info.computerId !== runtime.computerId || info.pid !== runtime.pid) throw new Error("The local Mindwire process changed. Retry the command.");
@@ -2107,20 +2459,21 @@ function directRoutes(config, port) {
2107
2459
  async function ensureComputer(directory, cliPath, patch = {}, onProgress, options = {}) {
2108
2460
  const resume = options.resume !== false;
2109
2461
  options.signal?.throwIfAborted();
2110
- await fs.mkdir(directory, { recursive: true, mode: 448 });
2111
- const stoppedPath = path.join(directory, "computer-stopped.json");
2112
- if (resume) await fs.rm(stoppedPath, { force: true });
2462
+ await fs2.mkdir(directory, { recursive: true, mode: 448 });
2463
+ const stoppedPath = path4.join(directory, "computer-stopped.json");
2464
+ if (resume) await fs2.rm(stoppedPath, { force: true });
2113
2465
  else if ((await readJSON(stoppedPath))?.stopped) throw new Error("Mindwire was stopped by its owner.");
2114
2466
  let current;
2115
2467
  try {
2116
2468
  current = await computerClient(directory);
2117
2469
  } catch {
2118
2470
  }
2119
- const configPath = path.join(directory, "computer-config.json");
2471
+ const configPath = path4.join(directory, "computer-config.json");
2120
2472
  const previous = await readJSON(configPath);
2121
2473
  const config = { ...defaultComputerConfig(), ...previous, ...patch };
2474
+ validateRelayOptions(config.relay);
2122
2475
  const requestedAt = resume ? Date.now() : 0;
2123
- if (resume) await writeJSON(path.join(directory, "computer-check.json"), { requestedAt });
2476
+ if (resume) await writeJSON(path4.join(directory, "computer-check.json"), { requestedAt });
2124
2477
  const compatible = (controller) => (controller?.protocol ?? 0) >= CONTROLLER_PROTOCOL && versionAtLeast(controller?.cliVersion, SDK_VERSION);
2125
2478
  let launchedPID;
2126
2479
  let launchFailure;
@@ -2130,14 +2483,21 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2130
2483
  const deadline = Date.now() + (options.timeoutMs ?? 12e4);
2131
2484
  while (Date.now() < deadline) {
2132
2485
  options.signal?.throwIfAborted();
2133
- const controller = await readJSON(path.join(directory, "computer-controller.json"));
2486
+ const controller = await readJSON(path4.join(directory, "computer-controller.json"));
2134
2487
  lastState = controller;
2135
2488
  if (launchFailure) throw launchFailure;
2136
2489
  if (launchedPID && controller?.pid !== launchedPID) {
2137
2490
  await setTimeout$1(250, void 0, { signal: options.signal });
2138
2491
  continue;
2139
2492
  }
2493
+ if (options.waitForRoutes === false && compatible(controller) && await processStateAlive(controller)) {
2494
+ try {
2495
+ return await computerClient(directory);
2496
+ } catch {
2497
+ }
2498
+ }
2140
2499
  if (controller?.error && !controller.recovering) throw new Error(controller.error);
2500
+ if (controller?.error && providerNeedsAction(controller.errorCode) && (controller.checkedAt ?? 0) >= requestedAt) throw new Error(controller.error);
2141
2501
  if (controller?.ready && compatible(controller) && (controller.checkedAt ?? 0) >= requestedAt && await processStateAlive(controller)) return computerClient(directory);
2142
2502
  if (controller?.phase && controller.phase !== phase) {
2143
2503
  phase = controller.phase;
@@ -2150,17 +2510,27 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2150
2510
  throw new Error(`Mindwire's connection is not ready. ${lastStep}${recovering ? " Background recovery continues." : ""} Run mindwire reconnect to retry, or mindwire status for details.`);
2151
2511
  };
2152
2512
  if (current) {
2153
- if (Object.keys(patch).some((key) => JSON.stringify(config[key]) !== JSON.stringify(previous?.[key]))) {
2154
- throw new Error("Mindwire is running with different connection settings. Run mindwire stop when idle, then retry with the new settings.");
2513
+ const changed = Object.keys(patch).filter((key) => JSON.stringify(config[key]) !== JSON.stringify(previous?.[key]));
2514
+ if (changed.length) {
2515
+ if (changed.some((key) => !["relay", "host"].includes(key))) {
2516
+ throw new Error("Changing the service's directory or listening ports requires mindwire stop when idle. Connection providers can be changed live with mindwire connection.");
2517
+ }
2518
+ const controller2 = await readJSON(path4.join(directory, "computer-controller.json"));
2519
+ if (!compatible(controller2) || !await processStateAlive(controller2)) {
2520
+ await ensureComputer(directory, cliPath, {}, onProgress, { ...options, waitForRoutes: false });
2521
+ }
2522
+ const { changeComputerConnection } = await import('./connection-change-5F4WNCYV.js');
2523
+ await changeComputerConnection(directory, Object.fromEntries(changed.map((key) => [key, patch[key]])), { signal: options.signal, onProgress });
2524
+ return computerClient(directory);
2155
2525
  }
2156
- const controller = await readJSON(path.join(directory, "computer-controller.json"));
2526
+ const controller = await readJSON(path4.join(directory, "computer-controller.json"));
2157
2527
  if (compatible(controller) && await processStateAlive(controller)) return waitUntilReady();
2158
2528
  }
2159
- const lockPath = path.join(directory, "launch.lock");
2529
+ const lockPath = path4.join(directory, "launch.lock");
2160
2530
  const lock = await acquireProcessLock(lockPath, {
2161
2531
  signal: options.signal,
2162
2532
  onWait: async () => {
2163
- const controller = await readJSON(path.join(directory, "computer-controller.json"));
2533
+ const controller = await readJSON(path4.join(directory, "computer-controller.json"));
2164
2534
  if (compatible(controller) && await processStateAlive(controller)) {
2165
2535
  const saved = await readJSON(configPath);
2166
2536
  if (Object.keys(patch).some((key) => JSON.stringify(patch[key]) !== JSON.stringify(saved?.[key]))) {
@@ -2184,13 +2554,13 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2184
2554
  if (Object.keys(patch).some((key) => JSON.stringify(patch[key]) !== JSON.stringify(saved?.[key]))) {
2185
2555
  throw new Error("Another start used different connection settings. Stop Mindwire when idle before changing them.");
2186
2556
  }
2187
- const controller2 = await readJSON(path.join(directory, "computer-controller.json"));
2557
+ const controller2 = await readJSON(path4.join(directory, "computer-controller.json"));
2188
2558
  if (compatible(controller2) && await processStateAlive(controller2)) return waitUntilReady();
2189
2559
  }
2190
- let controller = await readJSON(path.join(directory, "computer-controller.json"));
2560
+ let controller = await readJSON(path4.join(directory, "computer-controller.json"));
2191
2561
  if (existing && controller && !compatible(controller) && await processStateAlive(controller)) {
2192
2562
  onProgress?.("Refreshing the background connection service\u2026");
2193
- const { handoffController } = await import('./controller-handoff-3LAYBJUL.js');
2563
+ const { handoffController } = await import('./controller-handoff-KV7WRDAI.js');
2194
2564
  await handoffController(directory, controller, config, await existing.computer.info());
2195
2565
  controller = void 0;
2196
2566
  }
@@ -2198,20 +2568,20 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2198
2568
  if (controller?.error && !controller.recovering) throw new Error(controller.error);
2199
2569
  } else {
2200
2570
  await writeJSON(configPath, config);
2201
- const log = await fs.open(path.join(directory, "computer.log"), "a", 384);
2571
+ const log = await fs2.open(path4.join(directory, "computer.log"), "a", 384);
2202
2572
  try {
2203
2573
  const child = spawn(process.execPath, [cliPath, "_serve", "--state-dir", directory], {
2204
2574
  detached: true,
2205
2575
  stdio: ["ignore", log.fd, log.fd],
2206
2576
  windowsHide: true
2207
2577
  });
2208
- await new Promise((resolve, reject) => {
2209
- child.once("spawn", resolve);
2578
+ await new Promise((resolve3, reject) => {
2579
+ child.once("spawn", resolve3);
2210
2580
  child.once("error", reject);
2211
2581
  });
2212
2582
  launchedPID = child.pid;
2213
2583
  child.on("exit", () => {
2214
- launchFailure = new Error(`The computer controller stopped. Inspect ${path.join(directory, "computer.log")}.`);
2584
+ launchFailure = new Error(`The computer controller stopped. Inspect ${path4.join(directory, "computer.log")}.`);
2215
2585
  });
2216
2586
  child.unref();
2217
2587
  } finally {
@@ -2224,20 +2594,20 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2224
2594
  }
2225
2595
  }
2226
2596
  async function superviseComputer(directory) {
2227
- const controller = await import('./controller-AGT6MCJG.js');
2597
+ const controller = await import('./controller-3SZI7TBW.js');
2228
2598
  await controller.superviseComputer(directory);
2229
2599
  }
2230
2600
  async function stopComputer(directory, force = false) {
2231
2601
  const client = await computerClient(directory);
2232
- const controller = await readJSON(path.join(directory, "computer-controller.json"));
2602
+ const controller = await readJSON(path4.join(directory, "computer-controller.json"));
2233
2603
  if (!await processStateAlive(controller)) throw new Error("The Mindwire controller is not running.");
2234
2604
  const lease = force ? void 0 : await client.service.acquireUpdate();
2235
2605
  try {
2236
- await writeJSON(path.join(directory, "computer-stopped.json"), { stopped: true });
2606
+ await writeJSON(path4.join(directory, "computer-stopped.json"), { stopped: true });
2237
2607
  if (!await processStateAlive(controller)) throw new Error("The Mindwire controller changed. Retry the command.");
2238
2608
  process.kill(controller.pid, "SIGTERM");
2239
2609
  } catch (error) {
2240
- await fs.rm(path.join(directory, "computer-stopped.json"), { force: true });
2610
+ await fs2.rm(path4.join(directory, "computer-stopped.json"), { force: true });
2241
2611
  if (lease) await client.service.releaseUpdate(lease.id);
2242
2612
  throw error;
2243
2613
  }
@@ -2248,6 +2618,6 @@ async function stopComputer(directory, force = false) {
2248
2618
  throw new Error("Mindwire is still stopping. Check computer.log.");
2249
2619
  }
2250
2620
 
2251
- export { CONTROLLER_PROTOCOL, SDK_VERSION, acquireProcessLock, adoptProcess, computerClient, computerPairingQRData, computerPairingURI, currentProcessIdentity, defaultStateDirectory, directRoutes, ensureComputer, ensureDaemonBinary, ownChild, processIdentity, processStateAlive, readJSON, stopComputer, superviseComputer, writeJSON };
2252
- //# sourceMappingURL=chunk-3QFDSYKC.js.map
2253
- //# sourceMappingURL=chunk-3QFDSYKC.js.map
2621
+ export { CONTROLLER_PROTOCOL, ProviderConnectionError, RelayOutput, SDK_VERSION, acquireProcessLock, adoptProcess, adoptRelay, cloudflareCommand, computerClient, computerPairingQRData, computerPairingURI, currentProcessIdentity, defaultComputerConfig, defaultStateDirectory, directRoutes, ensureComputer, ensureDaemonBinary, hasCertificateError, ownChild, processIdentity, processStateAlive, providerFailure, providerNeedsAction, readJSON, startRelay, stopComputer, superviseComputer, websocketURL, writeJSON };
2622
+ //# sourceMappingURL=chunk-CKTVMUS4.js.map
2623
+ //# sourceMappingURL=chunk-CKTVMUS4.js.map