mindwire 0.1.47 → 0.1.50

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
@@ -185,6 +185,22 @@ new Mindwire({ target: oblien({ clientId: process.env.OBLIEN_CLIENT_ID, clientSe
185
185
 
186
186
  Any object implementing `Target` can also be passed as `target` (bring-your-own).
187
187
 
188
+ For an application-owned local profile, use `local({ cwd, statePath, environment,
189
+ authSource: "host", shared: false })`. `environment` replaces the inherited process
190
+ environment; the optional `authSource` lets the native adapters automatically reuse
191
+ the host's saved connection on the first agent/status read or turn. Existing account
192
+ choices and explicit sign-out are preserved. No client-side `auth.begin()` is needed
193
+ for first setup. Use `authSource: { home, environment, reuse: "manual" }` if a host
194
+ requires explicit selection instead. Methods with `existing` metadata display the
195
+ discovered account/provider and model without exposing credentials;
196
+ `auth.begin(method.id)` remains available to reconnect after sign-out or retry a
197
+ failed import. Native tools, hooks and unrelated host configuration are not imported.
198
+ `await mw.close()` stops the owned daemon. Shared local targets remain alive until
199
+ the host exits or their embedded handle is explicitly stopped.
200
+ `local().profileVersion` advertises this SDK contract before a process starts;
201
+ applications requiring private profiles should reject targets without it. The
202
+ daemon separately reports `health.nativeAuthSource`, which the SDK checks at startup.
203
+
188
204
  ### Provisioning logs & `ensure()`
189
205
 
190
206
  `ssh` / `docker` / `oblien` prep the box on connect — upload the daemon binary, launch it, health-poll
@@ -88,7 +88,7 @@ function computerPairingURI(invitation) {
88
88
  }
89
89
 
90
90
  // src/version.ts
91
- var SDK_VERSION = "0.1.47" ;
91
+ var SDK_VERSION = "0.1.50" ;
92
92
  function versionAtLeast(actual, desired) {
93
93
  if (actual === desired) return true;
94
94
  if (!actual || !/^\d+\.\d+\.\d+$/.test(actual) || !/^\d+\.\d+\.\d+$/.test(desired)) return false;
@@ -1694,17 +1694,135 @@ var TerminalsApi = class {
1694
1694
  }
1695
1695
  };
1696
1696
 
1697
+ // src/voice.ts
1698
+ var VoiceApi = class {
1699
+ constructor(client) {
1700
+ this.client = client;
1701
+ }
1702
+ client;
1703
+ status(scoped) {
1704
+ return this.client.http.request("GET", "/voice", {
1705
+ query: { agent: scoped?.agent ?? this.client.defaultAgent }
1706
+ });
1707
+ }
1708
+ /** Start in an idle chat. Keep requestId/clientId when retrying a lost receipt. */
1709
+ async start(input) {
1710
+ const clientId = input.clientId ?? crypto.randomUUID();
1711
+ const data = await this.client.http.request("POST", "/turns", {
1712
+ query: { agent: input.agent ?? this.client.defaultAgent },
1713
+ body: {
1714
+ chatId: input.chatId,
1715
+ message: "",
1716
+ cwd: input.cwd,
1717
+ gitAuth: input.gitAuth,
1718
+ requestId: input.requestId ?? crypto.randomUUID(),
1719
+ options: { voice: { clientId, voice: input.voice } }
1720
+ }
1721
+ });
1722
+ return new VoiceSession(this.client.http, new Run(this.client.http, data), clientId);
1723
+ }
1724
+ };
1725
+ var VoiceSession = class {
1726
+ constructor(http, run, clientId) {
1727
+ this.http = http;
1728
+ this.run = run;
1729
+ this.clientId = clientId;
1730
+ }
1731
+ http;
1732
+ run;
1733
+ clientId;
1734
+ sequence = 0;
1735
+ sending = false;
1736
+ ended = false;
1737
+ streaming = false;
1738
+ get path() {
1739
+ return `/runs/${encodeURIComponent(this.run.id)}/voice`;
1740
+ }
1741
+ /** Await each ordered batch. No automatic audio replay after a network failure. */
1742
+ async appendAudio(audio, signal) {
1743
+ if (this.ended || this.sending) throw new Error("Voice ended or the previous audio batch is still being delivered.");
1744
+ this.sending = true;
1745
+ try {
1746
+ await this.http.request("POST", this.path, { signal, body: {
1747
+ clientId: this.clientId,
1748
+ action: "audio",
1749
+ sequence: ++this.sequence,
1750
+ audio
1751
+ } });
1752
+ } catch (error) {
1753
+ await this.stop().catch(() => {
1754
+ });
1755
+ throw error;
1756
+ } finally {
1757
+ this.sending = false;
1758
+ }
1759
+ }
1760
+ /** Subscribe before sending audio; wait for ready. Breaking the loop ends voice. */
1761
+ async *stream(signal) {
1762
+ if (this.streaming || this.ended) throw new Error("This voice stream is already open or ended.");
1763
+ this.streaming = true;
1764
+ const controller = new AbortController();
1765
+ const abort = () => controller.abort(signal?.reason);
1766
+ signal?.addEventListener("abort", abort, { once: true });
1767
+ if (signal?.aborted) abort();
1768
+ let heartbeatPending = false;
1769
+ const heartbeat = setInterval(() => {
1770
+ if (heartbeatPending || controller.signal.aborted) return;
1771
+ heartbeatPending = true;
1772
+ void this.http.request("POST", this.path, {
1773
+ signal: controller.signal,
1774
+ body: { clientId: this.clientId, action: "heartbeat" }
1775
+ }).catch((error) => controller.abort(error)).finally(() => {
1776
+ heartbeatPending = false;
1777
+ });
1778
+ }, 5e3);
1779
+ try {
1780
+ const response = await this.http.open("GET", `${this.path}/stream`, {
1781
+ query: { clientId: this.clientId },
1782
+ signal: controller.signal
1783
+ });
1784
+ for await (const event of readSSE(response.body, controller.signal)) {
1785
+ if (event.type !== "heartbeat") yield event;
1786
+ if (event.type === "closed" || event.type === "error") break;
1787
+ }
1788
+ } finally {
1789
+ clearInterval(heartbeat);
1790
+ signal?.removeEventListener("abort", abort);
1791
+ controller.abort();
1792
+ this.streaming = false;
1793
+ await this.stop().catch(() => {
1794
+ });
1795
+ }
1796
+ }
1797
+ async stop() {
1798
+ if (this.ended) return;
1799
+ this.ended = true;
1800
+ await this.http.request("POST", this.path, { body: { clientId: this.clientId, action: "stop" } });
1801
+ }
1802
+ };
1803
+
1697
1804
  // src/embedded.ts
1698
1805
  var shared = /* @__PURE__ */ new Map();
1699
1806
  function configKey(opts) {
1700
- return JSON.stringify([opts.cwd ?? "", opts.statePath ?? "", opts.bin ?? ""]);
1807
+ const entries = (env) => env && Object.entries(env).sort(([a], [b]) => a.localeCompare(b));
1808
+ return JSON.stringify([
1809
+ opts.cwd ?? "",
1810
+ opts.statePath ?? "",
1811
+ opts.bin ?? "",
1812
+ opts.exactVersion,
1813
+ entries(opts.environment),
1814
+ opts.authSource === "host" ? ["host", entries(serverRuntime().process?.env)] : opts.authSource && [opts.authSource.home, opts.authSource.reuse ?? "automatic", entries(opts.authSource.environment)]
1815
+ ]);
1701
1816
  }
1702
1817
  function startEmbedded(opts = {}) {
1818
+ if (opts.shared === false) return spawnDaemon(opts);
1703
1819
  const key = configKey(opts);
1704
1820
  let daemon = shared.get(key);
1705
1821
  if (!daemon) {
1706
- daemon = spawnDaemon(opts).catch((err) => {
1707
- shared.delete(key);
1822
+ daemon = spawnDaemon(opts, () => {
1823
+ if (shared.get(key) === daemon) shared.delete(key);
1824
+ }).catch((err) => {
1825
+ if (shared.get(key) === daemon) shared.delete(key);
1708
1826
  throw err;
1709
1827
  });
1710
1828
  shared.set(key, daemon);
@@ -1718,7 +1836,8 @@ function isServerRuntime() {
1718
1836
  const g = serverRuntime();
1719
1837
  return typeof g.process !== "undefined" && !!g.process.versions?.node || typeof g.Bun !== "undefined" || typeof g.Deno !== "undefined";
1720
1838
  }
1721
- async function spawnDaemon(opts) {
1839
+ async function spawnDaemon(opts, onStop = () => {
1840
+ }) {
1722
1841
  if (!isServerRuntime()) {
1723
1842
  throw new MindwireError(
1724
1843
  "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."
@@ -1728,40 +1847,78 @@ async function spawnDaemon(opts) {
1728
1847
  const net = await import('net');
1729
1848
  const proc = serverRuntime().process;
1730
1849
  const { randomBytes } = await import('crypto');
1731
- const resolved = await resolveBinary(opts.bin);
1850
+ const resolved = await resolveBinary(opts.bin, opts.exactVersion);
1732
1851
  const bin = resolved.bin;
1733
1852
  const port = await freePort(net);
1734
1853
  const baseUrl = `http://127.0.0.1:${port}`;
1735
1854
  const token = randomBytes(32).toString("hex");
1855
+ const authSource = opts.authSource === "host" ? { home: (await import('os')).homedir(), environment: proc.env } : opts.authSource;
1736
1856
  const child = spawn3(bin, [], {
1737
1857
  env: {
1738
- ...proc.env,
1858
+ ...opts.environment ?? proc.env,
1739
1859
  ADDR: `127.0.0.1:${port}`,
1740
1860
  AGENT_CWD: opts.cwd ?? proc.cwd(),
1741
1861
  STATE_PATH: opts.statePath ?? ".mindwire-state.json",
1742
- DAEMON_TOKEN: token
1862
+ DAEMON_TOKEN: token,
1863
+ // The daemon consumes and unsets this before starting any native process.
1864
+ ...authSource ? { MINDWIRE_AUTH_SOURCE: JSON.stringify(authSource) } : {}
1743
1865
  },
1866
+ cwd: opts.cwd ?? proc.cwd(),
1744
1867
  stdio: "ignore"
1745
1868
  });
1746
1869
  let spawnError = null;
1747
- child.on("error", (e) => {
1748
- spawnError = e;
1749
- });
1750
- const stop = () => {
1870
+ let exited = false;
1871
+ let stopping;
1872
+ const isAlive = () => !exited && !stopping && !spawnError;
1873
+ const killOnExit = () => {
1751
1874
  try {
1752
1875
  child.kill();
1753
1876
  } catch {
1754
1877
  }
1755
1878
  };
1756
- proc.once?.("exit", stop);
1757
- proc.once?.("SIGINT", () => {
1758
- stop();
1759
- proc.exit?.(130);
1879
+ const cleanup = () => {
1880
+ exited = true;
1881
+ proc.off?.("exit", killOnExit);
1882
+ onStop();
1883
+ };
1884
+ child.once("exit", cleanup);
1885
+ child.on("error", (e) => {
1886
+ spawnError = e;
1887
+ cleanup();
1760
1888
  });
1761
- await waitHealthy(baseUrl, token, () => spawnError, resolved);
1762
- return { baseUrl, token, stop };
1889
+ const stop = () => {
1890
+ return stopping ??= new Promise((resolve3) => {
1891
+ if (exited) {
1892
+ resolve3();
1893
+ return;
1894
+ }
1895
+ const timer = setTimeout(() => {
1896
+ try {
1897
+ child.kill("SIGKILL");
1898
+ } catch {
1899
+ }
1900
+ }, 2e3);
1901
+ child.once("exit", () => {
1902
+ clearTimeout(timer);
1903
+ resolve3();
1904
+ });
1905
+ child.once("error", () => {
1906
+ clearTimeout(timer);
1907
+ resolve3();
1908
+ });
1909
+ killOnExit();
1910
+ });
1911
+ };
1912
+ proc.once?.("exit", killOnExit);
1913
+ try {
1914
+ await waitHealthy(baseUrl, token, () => spawnError ?? (exited ? new Error("daemon exited during startup") : null), resolved, 15e3, !!authSource);
1915
+ return { baseUrl, token, isAlive, stop };
1916
+ } catch (error) {
1917
+ await stop();
1918
+ throw error;
1919
+ }
1763
1920
  }
1764
- async function resolveBinary(explicit) {
1921
+ async function resolveBinary(explicit, exactVersion) {
1765
1922
  const fs4 = await import('fs');
1766
1923
  const path5 = await import('path');
1767
1924
  const proc = serverRuntime().process;
@@ -1782,7 +1939,7 @@ async function resolveBinary(explicit) {
1782
1939
  } catch {
1783
1940
  }
1784
1941
  }
1785
- const downloaded = await ensureDaemonBinary({ platform: proc.platform, arch: proc.arch });
1942
+ const downloaded = await ensureDaemonBinary({ platform: proc.platform, arch: proc.arch, exactVersion });
1786
1943
  return { bin: downloaded, found: true, checked: [...candidates, downloaded] };
1787
1944
  }
1788
1945
  function daemonStartError(resolved, cause) {
@@ -1811,7 +1968,7 @@ function freePort(net) {
1811
1968
  });
1812
1969
  });
1813
1970
  }
1814
- async function waitHealthy(baseUrl, token, getError, resolved, timeoutMs = 15e3) {
1971
+ async function waitHealthy(baseUrl, token, getError, resolved, timeoutMs = 15e3, authSource = false) {
1815
1972
  const deadline = Date.now() + timeoutMs;
1816
1973
  while (Date.now() < deadline) {
1817
1974
  const err = getError();
@@ -1819,9 +1976,14 @@ async function waitHealthy(baseUrl, token, getError, resolved, timeoutMs = 15e3)
1819
1976
  throw new MindwireError(daemonStartError(resolved, err), { cause: err });
1820
1977
  }
1821
1978
  try {
1822
- const res = await fetch(`${baseUrl}/healthz`, { headers: { Authorization: `Bearer ${token}` } });
1823
- if (res.ok) return;
1824
- } catch {
1979
+ const res = await fetch(`${baseUrl}/healthz`, { headers: { Authorization: `Bearer ${token}` }, signal: AbortSignal.timeout(1e3) });
1980
+ if (res.ok) {
1981
+ if (authSource && (await res.json()).nativeAuthSource !== true)
1982
+ throw new MindwireError("This daemon does not support native auth sources. Update Mindwire or build the matching daemon before using authSource.");
1983
+ return;
1984
+ }
1985
+ } catch (error) {
1986
+ if (error instanceof MindwireError) throw error;
1825
1987
  }
1826
1988
  await new Promise((r) => setTimeout(r, 150));
1827
1989
  }
@@ -1837,17 +1999,27 @@ function emit(spec, e) {
1837
1999
  }
1838
2000
  }
1839
2001
  function local(opts = {}) {
2002
+ let daemon;
2003
+ let pending;
1840
2004
  return {
1841
2005
  name: "local",
2006
+ profileVersion: 1,
1842
2007
  async connect(spec) {
1843
- const d = await startEmbedded(opts);
2008
+ if (daemon && !daemon.isAlive()) {
2009
+ daemon = void 0;
2010
+ pending = void 0;
2011
+ }
2012
+ const d = await (pending ??= startEmbedded(opts).then((value) => daemon = value).catch((error) => {
2013
+ pending = void 0;
2014
+ throw error;
2015
+ }));
1844
2016
  emit(spec, { target: "local", phase: "ready", message: `embedded daemon on ${d.baseUrl}` });
1845
2017
  return {
1846
2018
  id: d.baseUrl,
1847
2019
  baseUrl: d.baseUrl,
1848
2020
  ...d.token !== void 0 ? { token: d.token } : {},
1849
- // No-op: the embedded daemon is shared (keyed memoization) and reaped on process exit.
1850
- stop: async () => {
2021
+ isAlive: d.isAlive,
2022
+ stop: opts.shared === false ? d.stop : async () => {
1851
2023
  }
1852
2024
  };
1853
2025
  }
@@ -1880,6 +2052,7 @@ var Mindwire = class _Mindwire {
1880
2052
  service;
1881
2053
  execution;
1882
2054
  computer;
2055
+ voice;
1883
2056
  http;
1884
2057
  /** The default agent type applied to agent-scoped calls, if set. */
1885
2058
  defaultAgent;
@@ -1918,6 +2091,7 @@ var Mindwire = class _Mindwire {
1918
2091
  this.service = new ServiceApi(this);
1919
2092
  this.execution = new ExecutionApi(this);
1920
2093
  this.computer = new ComputerApi(this);
2094
+ this.voice = new VoiceApi(this);
1921
2095
  this.auth = new AuthApi(this);
1922
2096
  this.prompts = new PromptsApi(this);
1923
2097
  this.mcp = new McpApi(this);
@@ -1943,6 +2117,7 @@ var Mindwire = class _Mindwire {
1943
2117
  clone.service = new ServiceApi(clone);
1944
2118
  clone.execution = new ExecutionApi(clone);
1945
2119
  clone.computer = new ComputerApi(clone);
2120
+ clone.voice = new VoiceApi(clone);
1946
2121
  clone.auth = new AuthApi(clone);
1947
2122
  clone.prompts = new PromptsApi(clone);
1948
2123
  clone.mcp = new McpApi(clone);
@@ -1959,8 +2134,8 @@ var Mindwire = class _Mindwire {
1959
2134
  * Release target-owned resources by calling the {@link TargetHandle}'s `stop()` — once, even across
1960
2135
  * {@link withAgent} clones that share the transport. For an `ssh`/`docker`/`oblien` target this reaps
1961
2136
  * the box (tear down the tunnel, stop or delete the container/workspace, per `stopOnExit`); a no-op
1962
- * for `local`/`remote` (embedded self-cleans on process exit; remote is not ours to stop). Safe to
1963
- * call before the target has connected (nothing to reap yet).
2137
+ * for shared local and remote targets. With `local({ shared: false })`, closes the owned daemon.
2138
+ * Safe to call before the target has connected (nothing to reap yet).
1964
2139
  */
1965
2140
  async close() {
1966
2141
  const handle = handleByTransport.get(this.http);
@@ -2005,6 +2180,12 @@ var Mindwire = class _Mindwire {
2005
2180
  models(scoped) {
2006
2181
  return this.http.request("GET", "/models", { query: this.agentParam(scoped) });
2007
2182
  }
2183
+ /** Discover native commands for a project without starting a model turn. */
2184
+ commands(opts = {}) {
2185
+ return this.http.request("GET", "/commands", {
2186
+ query: { ...this.agentParam(opts), dir: opts.directory, refresh: opts.refresh }
2187
+ });
2188
+ }
2008
2189
  /** `GET /doctor` — daemon-level health plus the selected agent's own checks. */
2009
2190
  doctor(scoped) {
2010
2191
  return this.http.request("GET", "/doctor", { query: this.agentParam(scoped) });
@@ -2142,7 +2323,8 @@ var Mindwire = class _Mindwire {
2142
2323
  limit,
2143
2324
  before: opts.before,
2144
2325
  paged: "true",
2145
- maxBytes: opts.maxBytes ?? 1048576
2326
+ maxBytes: opts.maxBytes ?? 1048576,
2327
+ ifRevision: opts.before ? void 0 : opts.ifRevision
2146
2328
  }
2147
2329
  }
2148
2330
  );
@@ -2221,7 +2403,7 @@ var Mindwire = class _Mindwire {
2221
2403
  `/chats/${encodeURIComponent(chatId)}/compact`,
2222
2404
  {
2223
2405
  query: this.agentParam(opts),
2224
- body: opts.instructions ? { instructions: opts.instructions } : {}
2406
+ body: { instructions: opts.instructions, requestId: opts.requestId }
2225
2407
  }
2226
2408
  );
2227
2409
  return new Run(this.http, data);
@@ -2647,7 +2829,7 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2647
2829
  if (!compatible(controller2) || !await processStateAlive(controller2)) {
2648
2830
  await ensureComputer(directory, cliPath, {}, onProgress, { ...options, waitForRoutes: false });
2649
2831
  }
2650
- const { changeComputerConnection } = await import('./connection-change-LCT3UM7M.js');
2832
+ const { changeComputerConnection } = await import('./connection-change-ZYFBOPEE.js');
2651
2833
  await changeComputerConnection(directory, Object.fromEntries(changed.map((key) => [key, patch[key]])), { signal: options.signal, onProgress });
2652
2834
  return computerClient(directory);
2653
2835
  }
@@ -2688,7 +2870,7 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2688
2870
  let controller = await readJSON(path4.join(directory, "computer-controller.json"));
2689
2871
  if (existing && controller && !compatible(controller) && await processStateAlive(controller)) {
2690
2872
  onProgress?.("Refreshing the background connection service\u2026");
2691
- const { handoffController } = await import('./controller-handoff-RPZJDZJR.js');
2873
+ const { handoffController } = await import('./controller-handoff-YB6DMOUW.js');
2692
2874
  await handoffController(directory, controller, config, await existing.computer.info());
2693
2875
  controller = void 0;
2694
2876
  }
@@ -2722,7 +2904,7 @@ async function ensureComputer(directory, cliPath, patch = {}, onProgress, option
2722
2904
  }
2723
2905
  }
2724
2906
  async function superviseComputer(directory) {
2725
- const controller = await import('./controller-I7A3APTF.js');
2907
+ const controller = await import('./controller-63NGJNKK.js');
2726
2908
  await controller.superviseComputer(directory);
2727
2909
  }
2728
2910
  async function stopComputer(directory, force = false) {
@@ -2747,5 +2929,5 @@ async function stopComputer(directory, force = false) {
2747
2929
  }
2748
2930
 
2749
2931
  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, versionAtLeast, websocketURL, writeJSON };
2750
- //# sourceMappingURL=chunk-7QLYFISG.js.map
2751
- //# sourceMappingURL=chunk-7QLYFISG.js.map
2932
+ //# sourceMappingURL=chunk-3LN2KXJK.js.map
2933
+ //# sourceMappingURL=chunk-3LN2KXJK.js.map