mindwire 0.1.1 → 0.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # mindwire
2
2
 
3
3
  A typed TypeScript client for the **mindwire daemon** — one SDK for every coding-agent harness
4
- (Claude Code, Codex, Copilot CLI, opencode). The daemon normalizes each agent to a single
4
+ (Claude Code, Codex, Grok Build, opencode). The daemon normalizes each agent to a single
5
5
  protocol; this package is a thin, dependency-free client over its REST + SSE surface.
6
6
 
7
7
  - **Embedded by default.** On a server runtime (Node/Bun/Deno) the SDK auto-spawns the bundled
@@ -107,11 +107,9 @@ const mw = new Mindwire({
107
107
  await mw.ensure(); // resolves once the daemon is healthy; then turns stream as usual
108
108
  ```
109
109
 
110
- Binary discovery for the `local` daemon, in order: the `bin` option → `$MINDWIRE_DAEMON` →
111
- the matching prebuilt package (`mindwire-daemon-<platform>-<arch>`, installed automatically as an
112
- optional dependency) → a binary bundled next to the SDK (`bin/mindwired-<platform>-<arch>`) →
113
- `mindwired` on `PATH`. For local dev without the prebuilt package, set `$MINDWIRE_DAEMON` to a
114
- `go build`-produced binary.
110
+ Binary discovery for the `local` daemon, in order: the `bin` option → `$MINDWIRE_DAEMON` → a
111
+ verified SDK-matched binary downloaded from the GitHub Release → `mindwired` on `PATH`. For local
112
+ development, set `$MINDWIRE_DAEMON` to a `go build`-produced binary.
115
113
 
116
114
  `ssh` / `docker` / `oblien` require an optional peer (`ssh2` / `dockerode` / `oblien`) — install only
117
115
  the one you use. Importing `mindwire` never loads them.
@@ -180,12 +178,6 @@ bun install
180
178
  bun run build # tsup → dist (ESM + CJS + .d.ts)
181
179
  bun run typecheck # tsc --noEmit
182
180
  bun test # client + SSE parser tests (mocked fetch)
183
- bun run build:daemon # cross-compile mindwired → npm/mindwire-daemon-<platform>-<arch>/ (needs Go)
184
181
  ```
185
182
 
186
- `build:daemon` produces the per-platform daemon packages and syncs the main package's
187
- `optionalDependencies`. **Release order:** publish every `npm/mindwire-daemon-*` package first,
188
- then publish `mindwire` — its optional dependencies must already exist on the registry at the
189
- same version.
190
-
191
183
  License: Apache-2.0.
package/dist/client.d.ts CHANGED
@@ -102,7 +102,7 @@ export declare class Mindwire {
102
102
  * call before the target has connected (nothing to reap yet).
103
103
  */
104
104
  close(): Promise<void>;
105
- /** `GET /healthz` — public liveness check. Resolves with the daemon's health payload when it is up. */
105
+ /** `GET /healthz` — authenticated liveness check. Resolves with the daemon's health payload when it is up. */
106
106
  health(): Promise<Health>;
107
107
  /**
108
108
  * `GET /stats` — the daemon **process's** resource snapshot (heap in use, memory reserved from the
package/dist/index.cjs CHANGED
@@ -469,7 +469,7 @@ var Run = class _Run {
469
469
  };
470
470
 
471
471
  // src/version.ts
472
- var SDK_VERSION = "0.1.1" ;
472
+ var SDK_VERSION = "0.1.3" ;
473
473
 
474
474
  // src/daemon-binary.ts
475
475
  function supported(platform, arch) {
@@ -580,17 +580,19 @@ async function spawnDaemon(opts) {
580
580
  const { spawn } = await import('child_process');
581
581
  const net = await import('net');
582
582
  const proc = serverRuntime().process;
583
+ const { randomBytes } = await import('crypto');
583
584
  const resolved = await resolveBinary(opts.bin);
584
585
  const bin = resolved.bin;
585
586
  const port = await freePort(net);
586
587
  const baseUrl = `http://127.0.0.1:${port}`;
588
+ const token = randomBytes(32).toString("hex");
587
589
  const child = spawn(bin, [], {
588
590
  env: {
589
591
  ...proc.env,
590
592
  ADDR: `127.0.0.1:${port}`,
591
593
  AGENT_CWD: opts.cwd ?? proc.cwd(),
592
594
  STATE_PATH: opts.statePath ?? ".mindwire-state.json",
593
- DAEMON_TOKEN: ""
595
+ DAEMON_TOKEN: token
594
596
  },
595
597
  stdio: "ignore"
596
598
  });
@@ -609,8 +611,8 @@ async function spawnDaemon(opts) {
609
611
  stop();
610
612
  proc.exit?.(130);
611
613
  });
612
- await waitHealthy(baseUrl, () => spawnError, resolved);
613
- return { baseUrl, stop };
614
+ await waitHealthy(baseUrl, token, () => spawnError, resolved);
615
+ return { baseUrl, token, stop };
614
616
  }
615
617
  async function resolveBinary(explicit) {
616
618
  const fs = await import('fs');
@@ -645,7 +647,7 @@ function daemonStartError(resolved, cause) {
645
647
  " \u2022 check GitHub Release access so the SDK can download its matching daemon binary; or",
646
648
  " \u2022 set MINDWIRE_DAEMON to a `mindwired` binary; or",
647
649
  " \u2022 pass { baseUrl } to connect to a daemon you run yourself.",
648
- " \u2022 Working inside the mindwire monorepo? Run `bun run build:daemon` from the repo root to build it.",
650
+ " \u2022 Working inside the mindwire monorepo? Run `go build -o /tmp/mindwired ./daemon/cmd/daemon` and set MINDWIRE_DAEMON.",
649
651
  `(underlying error: ${cause.message})`
650
652
  ].join("\n");
651
653
  }
@@ -662,7 +664,7 @@ function freePort(net) {
662
664
  });
663
665
  });
664
666
  }
665
- async function waitHealthy(baseUrl, getError, resolved, timeoutMs = 15e3) {
667
+ async function waitHealthy(baseUrl, token, getError, resolved, timeoutMs = 15e3) {
666
668
  const deadline = Date.now() + timeoutMs;
667
669
  while (Date.now() < deadline) {
668
670
  const err = getError();
@@ -670,7 +672,7 @@ async function waitHealthy(baseUrl, getError, resolved, timeoutMs = 15e3) {
670
672
  throw new MindwireError(daemonStartError(resolved, err), { cause: err });
671
673
  }
672
674
  try {
673
- const res = await fetch(`${baseUrl}/healthz`);
675
+ const res = await fetch(`${baseUrl}/healthz`, { headers: { Authorization: `Bearer ${token}` } });
674
676
  if (res.ok) return;
675
677
  } catch {
676
678
  }
@@ -805,7 +807,7 @@ var Mindwire = class _Mindwire {
805
807
  if (h) await h.stop();
806
808
  }
807
809
  // ---- health & catalog ----------------------------------------------------
808
- /** `GET /healthz` — public liveness check. Resolves with the daemon's health payload when it is up. */
810
+ /** `GET /healthz` — authenticated liveness check. Resolves with the daemon's health payload when it is up. */
809
811
  health() {
810
812
  return this.http.request("GET", "/healthz");
811
813
  }
@@ -1307,11 +1309,12 @@ var STATE = `${DAEMON_DIR}/agent-state.json`;
1307
1309
  var LOG = `${DAEMON_DIR}/daemon.log`;
1308
1310
  async function ensureDaemon(host, cfg) {
1309
1311
  const emit2 = makeEmit(cfg);
1312
+ const token = cfg.token ?? (await import('crypto')).randomBytes(32).toString("hex");
1310
1313
  try {
1311
1314
  await waitHostReady(host);
1312
1315
  emit2({ phase: "connect", message: "runtime ready" });
1313
1316
  const desired = cfg.desiredVersion ?? SDK_VERSION;
1314
- const health = await probeHealth(host, cfg.port);
1317
+ const health = await probeHealth(host, cfg.port, token);
1315
1318
  if (health.reachable) {
1316
1319
  emit2({
1317
1320
  phase: "probe",
@@ -1319,31 +1322,32 @@ async function ensureDaemon(host, cfg) {
1319
1322
  version: health.version
1320
1323
  });
1321
1324
  const upToDate = health.version !== void 0 && health.version === desired;
1322
- if (upToDate || !cfg.autoUpdate) {
1325
+ if (!cfg.forceDeploy && (upToDate || !cfg.autoUpdate)) {
1323
1326
  emit2({
1324
1327
  phase: "skip",
1325
1328
  message: upToDate ? `daemon already at v${desired}` : "keeping the running daemon",
1326
1329
  version: health.version
1327
1330
  });
1328
- return;
1331
+ return token;
1329
1332
  }
1330
1333
  } else {
1331
1334
  emit2({ phase: "probe", message: "no daemon reachable; deploying" });
1332
1335
  }
1333
- await deploy(host, cfg, emit2);
1336
+ await deploy(host, cfg, emit2, token);
1337
+ return token;
1334
1338
  } catch (err) {
1335
1339
  emit2({ phase: "error", message: "ensure failed", error: err instanceof Error ? err.message : String(err) });
1336
1340
  throw err;
1337
1341
  }
1338
1342
  }
1339
- async function deploy(host, cfg, emit2) {
1343
+ async function deploy(host, cfg, emit2, token) {
1340
1344
  const arch = await probeArch(host);
1341
1345
  const desired = cfg.desiredVersion ?? SDK_VERSION;
1342
1346
  let acquire = "";
1343
1347
  if (cfg.daemonBin) {
1344
1348
  const binPath = await resolveLinuxDaemon(cfg.daemonBin, arch);
1345
1349
  const bytes = await readBytes(binPath);
1346
- emit2({ phase: "upload", message: `uploading daemon (${arch}, ${bytes.length} bytes)`, arch, bytes: bytes.length });
1350
+ emit2({ phase: "upload", message: `uploading daemon (${arch}, ${formatMiB(bytes.length)})`, arch, bytes: bytes.length });
1347
1351
  await host.putFile(BIN_NEW, bytes, { mode: "0755" });
1348
1352
  } else {
1349
1353
  if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(desired)) {
@@ -1386,9 +1390,9 @@ async function deploy(host, cfg, emit2) {
1386
1390
  `mv -f ${BIN_NEW} ${BIN}`,
1387
1391
  `chmod +x ${BIN}`,
1388
1392
  // Detach so the daemon survives this exec's shell exiting. ADDR=":<port>" binds 0.0.0.0.
1389
- `setsid nohup env ADDR=":${cfg.port}" AGENT_TYPE="${cfg.agent}" AGENT_CWD="${cfg.agentCwd}" STATE_PATH="${STATE}" DAEMON_TOKEN="" ${BIN} > ${LOG} 2>&1 < /dev/null &`,
1393
+ `setsid nohup env ADDR=":${cfg.port}" AGENT_TYPE="${cfg.agent}" AGENT_CWD="${cfg.agentCwd}" STATE_PATH="${STATE}" DAEMON_TOKEN=${shellQuote(token)} ${BIN} > ${LOG} 2>&1 < /dev/null &`,
1390
1394
  // Health-poll from inside the VM (loopback) and emit a marker — exit codes are unreliable here.
1391
- `for i in $(seq 1 60); do curl -fsS --max-time 2 http://127.0.0.1:${cfg.port}/healthz >/dev/null 2>&1 && { echo MINDWIRE_READY; exit 0; }; sleep 0.25; done`,
1395
+ `for i in $(seq 1 60); do curl -fsS --max-time 2 -H ${shellQuote(`Authorization: Bearer ${token}`)} http://127.0.0.1:${cfg.port}/healthz >/dev/null 2>&1 && { echo MINDWIRE_READY; exit 0; }; sleep 0.25; done`,
1392
1396
  "echo MINDWIRE_FAIL",
1393
1397
  `tail -n 40 ${LOG} 2>/dev/null || true`
1394
1398
  ].join("\n");
@@ -1402,8 +1406,8 @@ async function deploy(host, cfg, emit2) {
1402
1406
  }
1403
1407
  emit2({ phase: "ready", message: "daemon deployed and healthy" });
1404
1408
  }
1405
- async function probeHealth(host, port) {
1406
- const script = `out=$(curl -fsS --max-time 3 http://127.0.0.1:${port}/healthz 2>/dev/null) && printf '<<MW_H>>%s<<MW_H>>' "$out" || printf '<<MW_H>><<MW_H>>'`;
1409
+ async function probeHealth(host, port, token) {
1410
+ const script = `out=$(curl -fsS --max-time 3 -H ${shellQuote(`Authorization: Bearer ${token}`)} http://127.0.0.1:${port}/healthz 2>/dev/null) && printf '<<MW_H>>%s<<MW_H>>' "$out" || printf '<<MW_H>><<MW_H>>'`;
1407
1411
  const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 15 });
1408
1412
  const body = ((res.stdout ?? "").match(/<<MW_H>>([\s\S]*?)<<MW_H>>/)?.[1] ?? "").trim();
1409
1413
  if (!body) return { reachable: false };
@@ -1439,10 +1443,11 @@ async function waitHostReady(host, timeoutMs = 6e4) {
1439
1443
  async function resolveLinuxDaemon(explicit, arch) {
1440
1444
  const fs = await import('fs');
1441
1445
  if (explicit) {
1442
- if (!fs.existsSync(explicit)) {
1443
- throw new MindwireError(`mindwire: sandbox daemonBin not found at ${explicit}`);
1446
+ const resolved = explicit.replaceAll("{arch}", arch);
1447
+ if (!fs.existsSync(resolved)) {
1448
+ throw new MindwireError(`mindwire: sandbox daemonBin not found at ${resolved}`);
1444
1449
  }
1445
- return explicit;
1450
+ return resolved;
1446
1451
  }
1447
1452
  return ensureDaemonBinary({ platform: "linux", arch: arch === "arm64" ? "arm64" : "x64" });
1448
1453
  }
@@ -1456,6 +1461,9 @@ function sleep(ms) {
1456
1461
  function shellQuote(value) {
1457
1462
  return `'${value.replace(/'/g, "'\\''")}'`;
1458
1463
  }
1464
+ function formatMiB(bytes) {
1465
+ return `${(bytes / (1024 * 1024)).toFixed(1)} MB`;
1466
+ }
1459
1467
 
1460
1468
  // src/target/container.ts
1461
1469
  async function provisionContainer(host, cfg, onLog) {
@@ -1469,7 +1477,7 @@ async function provisionContainer(host, cfg, onLog) {
1469
1477
  };
1470
1478
  await ensureDockerReady(host, cfg.install ?? "never", emit2);
1471
1479
  const { containerId, hostPort, created } = await runContainer(host, cfg, emit2);
1472
- await ensureDaemon(new ContainerHost(host, containerId), {
1480
+ const token = await ensureDaemon(new ContainerHost(host, containerId), {
1473
1481
  port: cfg.daemonPort,
1474
1482
  agent: cfg.agent,
1475
1483
  agentCwd: cfg.agentCwd,
@@ -1488,7 +1496,7 @@ async function provisionContainer(host, cfg, onLog) {
1488
1496
  } catch {
1489
1497
  }
1490
1498
  };
1491
- return { containerId, hostPort, stop };
1499
+ return { containerId, hostPort, token, stop };
1492
1500
  }
1493
1501
  var ContainerHost = class {
1494
1502
  constructor(base, containerId) {
@@ -1724,7 +1732,7 @@ async function connectSsh(opts, spec) {
1724
1732
  });
1725
1733
  }
1726
1734
  async function provisionSsh(client, makeTunnel, cfg) {
1727
- await ensureDaemon(new SshHost(client), {
1735
+ const token = await ensureDaemon(new SshHost(client), {
1728
1736
  port: cfg.daemonPort,
1729
1737
  agent: cfg.agent,
1730
1738
  agentCwd: cfg.agentCwd,
@@ -1747,7 +1755,7 @@ async function provisionSsh(client, makeTunnel, cfg) {
1747
1755
  }
1748
1756
  }
1749
1757
  };
1750
- return { id: cfg.id, baseUrl: tunnel.baseUrl, stop };
1758
+ return { id: cfg.id, baseUrl: tunnel.baseUrl, token, stop };
1751
1759
  }
1752
1760
  async function provisionSshContainer(client, makeTunnel, cfg) {
1753
1761
  const container = await provisionContainer(
@@ -1784,7 +1792,7 @@ async function provisionSshContainer(client, makeTunnel, cfg) {
1784
1792
  }
1785
1793
  }
1786
1794
  };
1787
- return { id: cfg.id, baseUrl: tunnel.baseUrl, stop };
1795
+ return { id: cfg.id, baseUrl: tunnel.baseUrl, token: container.token, stop };
1788
1796
  }
1789
1797
  async function openConnection(mod, opts) {
1790
1798
  const auth = await buildAuth(opts);
@@ -1917,6 +1925,7 @@ function docker(config = {}) {
1917
1925
  var DEFAULT_PORT2 = 8790;
1918
1926
  var DEFAULT_AGENT2 = "claude-code";
1919
1927
  var DEFAULT_AGENT_CWD2 = "/root";
1928
+ var DEFAULT_RUNTIME_IMAGE = `ghcr.io/oblien/mindwire-runtime:${SDK_VERSION}`;
1920
1929
  var DockerHost = class {
1921
1930
  constructor(container) {
1922
1931
  this.container = container;
@@ -1971,15 +1980,14 @@ async function provisionDocker(docker2, config = {}, onLog) {
1971
1980
  if (config.container !== void 0) {
1972
1981
  container = docker2.getContainer(config.container);
1973
1982
  } else {
1974
- if (!config.image) {
1975
- throw new MindwireError(
1976
- "mindwire: the Docker adapter needs an image or a container \u2014 pass docker({ image }) to create one, or docker({ container }) to attach to an existing one."
1977
- );
1978
- }
1983
+ const image = config.image ?? DEFAULT_RUNTIME_IMAGE;
1984
+ await ensureImage(docker2, image, config.pull ?? "missing", onLog);
1979
1985
  const portKey = `${port}/tcp`;
1980
1986
  container = await docker2.createContainer({
1981
- Image: config.image,
1982
- Cmd: ["sleep", "infinity"],
1987
+ // The runtime image starts mindwired itself. Do not replace its command: doing so turns the
1988
+ // image ENTRYPOINT into `mindwired sleep infinity` and prevents the runtime from starting.
1989
+ Image: image,
1990
+ Env: [`ADDR=:${port}`, `AGENT_TYPE=${agent}`, `AGENT_CWD=${agentCwd}`],
1983
1991
  Tty: false,
1984
1992
  ExposedPorts: { [portKey]: {} },
1985
1993
  HostConfig: { PortBindings: { [portKey]: [{ HostPort: "0" }] } },
@@ -1996,7 +2004,7 @@ async function provisionDocker(docker2, config = {}, onLog) {
1996
2004
  `mindwire: the Docker container does not publish a host port for ${port}/tcp. ` + (created ? "This is unexpected for a container mindwire created." : "Attach to a container started with that port published (e.g. `-p 0:" + port + "`).")
1997
2005
  );
1998
2006
  }
1999
- await ensureDaemon(new DockerHost(container), {
2007
+ const token = await ensureDaemon(new DockerHost(container), {
2000
2008
  port,
2001
2009
  agent,
2002
2010
  agentCwd,
@@ -2015,7 +2023,37 @@ async function provisionDocker(docker2, config = {}, onLog) {
2015
2023
  } catch {
2016
2024
  }
2017
2025
  };
2018
- return { id: container.id, baseUrl: `http://127.0.0.1:${hostPort}`, stop };
2026
+ return { id: container.id, baseUrl: `http://127.0.0.1:${hostPort}`, token, stop };
2027
+ }
2028
+ async function ensureImage(docker2, image, policy, onLog) {
2029
+ const emit2 = (message, error) => {
2030
+ try {
2031
+ onLog?.({ phase: "pull", target: "docker", message, ...error ? { error } : {} });
2032
+ } catch {
2033
+ }
2034
+ };
2035
+ if (policy === "never") return;
2036
+ if (policy === "missing") {
2037
+ try {
2038
+ await docker2.getImage(image).inspect();
2039
+ return;
2040
+ } catch {
2041
+ }
2042
+ }
2043
+ emit2(`pulling runtime image ${image}`);
2044
+ try {
2045
+ const stream = await docker2.pull(image);
2046
+ await new Promise((resolve, reject) => {
2047
+ stream.on("end", () => resolve());
2048
+ stream.on("close", () => resolve());
2049
+ stream.on("error", (error) => reject(error));
2050
+ });
2051
+ emit2(`runtime image ready: ${image}`);
2052
+ } catch (error) {
2053
+ const detail = error instanceof Error ? error.message : String(error);
2054
+ emit2(`failed to pull runtime image ${image}`, detail);
2055
+ throw new MindwireError(`mindwire: failed to pull Docker image ${image}: ${detail}`, { cause: error });
2056
+ }
2019
2057
  }
2020
2058
  function tarSingleFile(name, data, mode) {
2021
2059
  const header = Buffer.alloc(512, 0);
@@ -2122,26 +2160,31 @@ async function provisionOblien(client, config = {}, onLog) {
2122
2160
  const ws = await client.workspaces.create(params);
2123
2161
  workspaceId = ws.id;
2124
2162
  }
2163
+ await config.onWorkspace?.(workspaceId);
2125
2164
  const handle = client.workspace(workspaceId);
2126
2165
  await handle.start().catch(() => {
2127
2166
  });
2128
2167
  let rt = await handle.runtime();
2129
- await ensureDaemon(new OblienHost(rt), {
2168
+ const token = await ensureDaemon(new OblienHost(rt), {
2130
2169
  port,
2131
2170
  agent,
2132
2171
  agentCwd,
2133
2172
  daemonBin: config.daemonBin,
2134
2173
  autoUpdate: config.autoUpdate,
2174
+ forceDeploy: config.forceDeploy,
2135
2175
  target: "oblien",
2136
2176
  onLog
2137
2177
  });
2138
2178
  const fetchImpl = async (url, init) => {
2139
2179
  const { pathname, search } = new URL(url);
2140
2180
  const path = pathname + search;
2141
- let res = await rt.proxy(port).fetch(path, init);
2181
+ const headers = new Headers(init?.headers);
2182
+ headers.set("X-Mindwire-Token", token);
2183
+ headers.delete("Authorization");
2184
+ let res = await rt.proxy(port).fetch(path, { ...init, headers });
2142
2185
  if (res.status === 401) {
2143
2186
  rt = await handle.runtime({ force: true });
2144
- res = await rt.proxy(port).fetch(path, init);
2187
+ res = await rt.proxy(port).fetch(path, { ...init, headers });
2145
2188
  }
2146
2189
  return res;
2147
2190
  };
@@ -2152,7 +2195,7 @@ async function provisionOblien(client, config = {}, onLog) {
2152
2195
  } catch {
2153
2196
  }
2154
2197
  };
2155
- return { id: workspaceId, workspaceId, baseUrl: SENTINEL_BASE, fetch: fetchImpl, stop };
2198
+ return { id: workspaceId, workspaceId, baseUrl: SENTINEL_BASE, token, fetch: fetchImpl, stop };
2156
2199
  }
2157
2200
  async function importOblien() {
2158
2201
  const spec = "oblien";