mindwire 0.1.11 → 0.1.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.d.ts CHANGED
@@ -32,3 +32,6 @@ export { catalogProviders, catalogProvider, catalogModels, lookupModel, loadCata
32
32
  export type { CatalogOptions } from "./catalog/index.js";
33
33
  export { MindwireError, ApiError, RunFailedError, TimeoutError } from "./errors.js";
34
34
  export * from "./types.js";
35
+ export { WorkspaceApi, WorkspaceCollection, ProjectOperationsApi } from "./workspace.js";
36
+ export type { ProjectRequest, ProjectAuth, ProjectOperation, ProjectRemoveRequest } from "./workspace.js";
37
+ export type { WorkspaceRecord, WorkspaceAgent, WorkspaceProject, WorkspaceChat, WorkspaceKind, WorkspaceInput, WorkspaceImport, WorkspaceSnapshot } from "./workspace.js";
package/dist/index.js CHANGED
@@ -286,6 +286,8 @@ async function* readSSE(body, signal) {
286
286
  if (payload !== void 0) yield JSON.parse(payload);
287
287
  } finally {
288
288
  if (signal) signal.removeEventListener("abort", onAbort);
289
+ await reader.cancel().catch(() => {
290
+ });
289
291
  reader.releaseLock();
290
292
  }
291
293
  }
@@ -410,9 +412,9 @@ var Run = class _Run {
410
412
  });
411
413
  }
412
414
  /**
413
- * Switch the permission mode of the live turn (e.g. `default`, `acceptEdits`, `plan`,
414
- * `bypassPermissions`). Only meaningful on a persistent (non-bypass) turn; on a one-shot turn it
415
- * is a best-effort no-op. Requires the agent's `setPermissionMode` capability.
415
+ * Switch the live permission mode using a value from the agent's settings schema.
416
+ * Resolves after the harness acknowledges the change; rejects if it cannot apply it.
417
+ * Requires `setPermissionMode`. Codex settings apply on the next turn instead.
416
418
  */
417
419
  async setPermissionMode(mode) {
418
420
  await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/set-permission-mode`, {
@@ -472,8 +474,114 @@ var Run = class _Run {
472
474
  }
473
475
  };
474
476
 
477
+ // src/workspace.ts
478
+ var ProjectOperationsApi = class {
479
+ constructor(mw) {
480
+ this.mw = mw;
481
+ }
482
+ mw;
483
+ async list(activeOnly = false) {
484
+ const response = await this.mw.http.request("GET", "/workspace/operations", {
485
+ query: { active: activeOnly }
486
+ });
487
+ return response.operations;
488
+ }
489
+ get(id) {
490
+ return this.mw.http.request("GET", `/workspace/operations/${encodeURIComponent(id)}`);
491
+ }
492
+ cancel(id) {
493
+ return this.mw.http.request("POST", `/workspace/operations/${encodeURIComponent(id)}/cancel`);
494
+ }
495
+ retry(id, auth) {
496
+ return this.mw.http.request("POST", `/workspace/operations/${encodeURIComponent(id)}/retry`, { body: { auth } });
497
+ }
498
+ /** The first event is the current snapshot, then live changes. Reconnecting never replays old
499
+ * progress. Breaking the loop/aborting detaches the observer; cancel(id) explicitly stops work.
500
+ */
501
+ async *watch(id, opts = {}) {
502
+ const controller = new AbortController();
503
+ const abort = () => controller.abort();
504
+ opts.signal?.addEventListener("abort", abort, { once: true });
505
+ if (opts.signal?.aborted) controller.abort();
506
+ try {
507
+ const response = await this.mw.http.open("GET", `/workspace/operations/${encodeURIComponent(id)}/stream`, {
508
+ signal: controller.signal
509
+ });
510
+ let sequence = -1;
511
+ for await (const operation of readSSE(response.body, controller.signal)) {
512
+ if (operation.sequence > sequence) {
513
+ sequence = operation.sequence;
514
+ yield operation;
515
+ }
516
+ }
517
+ } finally {
518
+ controller.abort();
519
+ opts.signal?.removeEventListener("abort", abort);
520
+ }
521
+ }
522
+ };
523
+ var WorkspaceCollection = class {
524
+ constructor(mw, kind) {
525
+ this.mw = mw;
526
+ this.kind = kind;
527
+ }
528
+ mw;
529
+ kind;
530
+ /** Create with a stable client-generated ID. For updates, supply the record's last revision. */
531
+ put(id, record, expectedRevision) {
532
+ return this.mw.http.request("PUT", `/workspace/${this.kind}/${encodeURIComponent(id)}`, {
533
+ body: { record, expectedRevision }
534
+ });
535
+ }
536
+ /** Remove membership and dependent chat links. Files and native transcripts are retained.
537
+ * Use deleteChat() for an explicit transcript purge. Running chats reject removal with 409.
538
+ */
539
+ delete(id, revision) {
540
+ return this.mw.http.request("DELETE", `/workspace/${this.kind}/${encodeURIComponent(id)}`, {
541
+ query: { revision }
542
+ });
543
+ }
544
+ };
545
+ var WorkspaceApi = class {
546
+ constructor(mw) {
547
+ this.mw = mw;
548
+ this.operations = new ProjectOperationsApi(mw);
549
+ this.agents = new WorkspaceCollection(mw, "agents");
550
+ this.projects = new WorkspaceCollection(mw, "projects");
551
+ this.chats = new WorkspaceCollection(mw, "chats");
552
+ }
553
+ mw;
554
+ operations;
555
+ agents;
556
+ projects;
557
+ chats;
558
+ snapshot() {
559
+ return this.mw.http.request("GET", "/workspace");
560
+ }
561
+ /** Start an operation owned by the daemon. The same ID/payload returns the existing operation. */
562
+ createProject(request) {
563
+ return this.mw.http.request("POST", "/workspace/projects", { body: request });
564
+ }
565
+ /** Permanently remove the confirmed project's directory and membership.
566
+ * projects.delete() retains files. Native harness transcripts are not purged.
567
+ */
568
+ removeProjectFiles(id, request) {
569
+ return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(id)}/remove`, { body: request });
570
+ }
571
+ /** Incremental reconciliation. Pass the previous identity to detect a replaced/restored workspace.
572
+ * A 409 requires fetching snapshot() again; never apply a delta to a different registry.
573
+ */
574
+ changes(since, workspaceId) {
575
+ return this.mw.http.request("GET", "/workspace/changes", { query: { since, workspaceId } });
576
+ }
577
+ /** Import legacy metadata before replacing a local cache. Safe to repeat after interruption. */
578
+ import(records) {
579
+ return this.mw.http.request("POST", "/workspace/import", { body: records });
580
+ }
581
+ };
582
+
475
583
  // src/version.ts
476
- var SDK_VERSION = "0.1.11" ;
584
+ var SDK_VERSION = "0.1.13" ;
477
585
 
478
586
  // src/daemon-binary.ts
479
587
  function supported(platform, arch) {
@@ -731,6 +839,8 @@ function remote(baseUrl, opts = {}) {
731
839
  // src/client.ts
732
840
  var handleByTransport = /* @__PURE__ */ new WeakMap();
733
841
  var Mindwire = class _Mindwire {
842
+ /** Workspace registry: saved agent profiles, projects and chat relationships. */
843
+ workspace;
734
844
  http;
735
845
  /** The default agent type applied to agent-scoped calls, if set. */
736
846
  defaultAgent;
@@ -764,6 +874,7 @@ var Mindwire = class _Mindwire {
764
874
  }
765
875
  });
766
876
  this.defaultAgent = opts.agent;
877
+ this.workspace = new WorkspaceApi(this);
767
878
  this.auth = new AuthApi(this);
768
879
  this.prompts = new PromptsApi(this);
769
880
  this.mcp = new McpApi(this);
@@ -784,6 +895,7 @@ var Mindwire = class _Mindwire {
784
895
  const clone = Object.create(_Mindwire.prototype);
785
896
  clone.http = this.http;
786
897
  clone.defaultAgent = agent;
898
+ clone.workspace = new WorkspaceApi(clone);
787
899
  clone.auth = new AuthApi(clone);
788
900
  clone.prompts = new PromptsApi(clone);
789
901
  clone.mcp = new McpApi(clone);
@@ -1306,17 +1418,24 @@ function makeEmit(cfg) {
1306
1418
  }
1307
1419
  };
1308
1420
  }
1309
- var DAEMON_DIR = "/root/.mindwire";
1310
- var BIN = `${DAEMON_DIR}/mindwired`;
1311
- var BIN_NEW = `${BIN}.new`;
1312
- var STATE = `${DAEMON_DIR}/agent-state.json`;
1313
- var LOG = `${DAEMON_DIR}/daemon.log`;
1421
+ async function daemonDirectory(host) {
1422
+ const result = await host.exec(["sh", "-lc", 'printf "<<MW_HOME>>%s<<MW_HOME>>" "$HOME"'], { timeoutSeconds: 15 });
1423
+ const runtimeHome = result.stdout?.match(/<<MW_HOME>>([\s\S]*?)<<MW_HOME>>/)?.[1];
1424
+ if (!runtimeHome?.startsWith("/") || runtimeHome.length > 4096 || /[\x00-\x1f\x7f]/.test(runtimeHome)) {
1425
+ throw new MindwireError("mindwire: cannot resolve the runtime user's home directory");
1426
+ }
1427
+ return runtimeHome.replace(/\/+$/, "") + "/.mindwire";
1428
+ }
1314
1429
  async function ensureDaemon(host, cfg) {
1315
1430
  const emit2 = makeEmit(cfg);
1316
- const token = cfg.token ?? (await import('crypto')).randomBytes(32).toString("hex");
1431
+ let token = cfg.token ?? (await import('crypto')).randomBytes(32).toString("hex");
1317
1432
  try {
1318
1433
  await waitHostReady(host);
1319
1434
  emit2({ phase: "connect", message: "runtime ready" });
1435
+ const directory = await daemonDirectory(host);
1436
+ if (!cfg.token) {
1437
+ token = await readWorkspaceToken(host, directory) ?? token;
1438
+ }
1320
1439
  const desired = cfg.desiredVersion ?? SDK_VERSION;
1321
1440
  const health = await probeHealth(host, cfg.port, token);
1322
1441
  if (health.reachable) {
@@ -1337,22 +1456,35 @@ async function ensureDaemon(host, cfg) {
1337
1456
  } else {
1338
1457
  emit2({ phase: "probe", message: "no daemon reachable; deploying" });
1339
1458
  }
1340
- await deploy(host, cfg, emit2, token);
1459
+ await deploy(host, cfg, emit2, token, directory);
1460
+ if (!cfg.token) token = await readWorkspaceToken(host, directory) ?? token;
1341
1461
  return token;
1342
1462
  } catch (err) {
1343
1463
  emit2({ phase: "error", message: "ensure failed", error: err instanceof Error ? err.message : String(err) });
1344
1464
  throw err;
1345
1465
  }
1346
1466
  }
1347
- async function deploy(host, cfg, emit2, token) {
1467
+ async function readWorkspaceToken(host, directory) {
1468
+ const saved = await host.exec(["sh", "-lc", `cat ${shellQuote(directory + "/daemon.token")} 2>/dev/null || true`], { timeoutSeconds: 15 });
1469
+ const candidate = saved.stdout?.trim();
1470
+ return candidate && candidate.length <= 4096 && /^[\x21-\x7e]+$/.test(candidate) ? candidate : void 0;
1471
+ }
1472
+ async function deploy(host, cfg, emit2, token, directory) {
1473
+ const newPath = directory + "/mindwired.new";
1474
+ const BIN = shellQuote(directory + "/mindwired"), BIN_NEW = shellQuote(newPath);
1475
+ const STATE = shellQuote(directory + "/agent-state.json"), LOG = shellQuote(directory + "/daemon.log");
1476
+ const TOKEN = shellQuote(directory + "/daemon.token");
1348
1477
  const arch = await probeArch(host);
1349
1478
  const desired = cfg.desiredVersion ?? SDK_VERSION;
1350
1479
  let acquire = "";
1480
+ let stagedUpload;
1351
1481
  if (cfg.daemonBin) {
1352
1482
  const binPath = await resolveLinuxDaemon(cfg.daemonBin, arch);
1353
1483
  const bytes = await readBytes(binPath);
1354
1484
  emit2({ phase: "upload", message: `uploading daemon (${arch}, ${formatMiB(bytes.length)})`, arch, bytes: bytes.length });
1355
- await host.putFile(BIN_NEW, bytes, { mode: "0755" });
1485
+ stagedUpload = `${newPath}-${(await import('crypto')).randomUUID()}`;
1486
+ await host.putFile(stagedUpload, bytes, { mode: "0755" });
1487
+ acquire = `mv -f ${shellQuote(stagedUpload)} ${BIN_NEW}`;
1356
1488
  } else {
1357
1489
  if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(desired)) {
1358
1490
  throw new MindwireError(`mindwire: cannot download daemon for non-release SDK version ${desired}`);
@@ -1383,25 +1515,46 @@ async function deploy(host, cfg, emit2, token) {
1383
1515
  }
1384
1516
  const script = [
1385
1517
  "set -e",
1386
- `mkdir -p ${DAEMON_DIR}`,
1518
+ `mkdir -p ${shellQuote(directory)}`,
1519
+ // iOS takes this same workspace lock. Unique staging also protects concurrent local uploads.
1520
+ stagedUpload ? `trap ${shellQuote(`rm -f ${shellQuote(stagedUpload)}`)} EXIT` : "",
1521
+ 'command -v flock >/dev/null 2>&1 || { echo "MINDWIRE_FAIL flock is required to lock daemon updates"; exit 1; }',
1522
+ `exec 9>${shellQuote(directory + "/daemon-install.lock")}`,
1523
+ 'flock -w 360 9 || { echo "MINDWIRE_FAIL another daemon update is still running"; exit 1; }',
1524
+ `mw_token=${shellQuote(token)}`,
1525
+ !cfg.token ? `if [ -s ${TOKEN} ]; then mw_token=$(cat ${TOKEN}); fi` : "",
1526
+ !cfg.forceDeploy ? [
1527
+ `mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
1528
+ `mw_version=$(printf '%s' "$mw_health" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^" ]*\\)".*/\\1/p')`,
1529
+ `if [ -n "$mw_health" ] && ${cfg.autoUpdate ? `[ "$mw_version" = ${shellQuote(desired)} ]` : "true"}; then echo MINDWIRE_READY; exit 0; fi`
1530
+ ].join("\n") : "",
1387
1531
  acquire,
1388
1532
  // Stop a prior daemon by exact process NAME, never `pkill -f <path>`: this whole script (which
1389
1533
  // contains `${BIN}` several times) is the argv of the `bash -lc` shell running it, so a full-cmdline
1390
1534
  // match would SIGTERM our own deploying shell before the daemon ever launches. `-x mindwired` matches
1391
1535
  // only the daemon's comm (`bash`/`pkill` never match), leaving this shell alive.
1392
- `pkill -x mindwired 2>/dev/null || true`,
1536
+ "if command -v pkill >/dev/null 2>&1; then",
1537
+ " pkill -x mindwired 2>/dev/null || true",
1538
+ "else",
1539
+ " for mw_proc in /proc/[0-9]*/comm; do",
1540
+ ' IFS= read -r mw_name 2>/dev/null < "$mw_proc" || continue',
1541
+ ' [ "$mw_name" = mindwired ] || continue',
1542
+ " mw_pid=${mw_proc#/proc/}; mw_pid=${mw_pid%/comm}",
1543
+ ' kill "$mw_pid" 2>/dev/null || true',
1544
+ " done",
1545
+ "fi",
1393
1546
  "sleep 0.3",
1394
1547
  `mv -f ${BIN_NEW} ${BIN}`,
1395
1548
  `chmod +x ${BIN}`,
1396
1549
  // Detach so the daemon survives this exec's shell exiting. ADDR=":<port>" binds 0.0.0.0.
1397
- `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 &`,
1550
+ `setsid nohup env ADDR=":${cfg.port}" AGENT_TYPE=${shellQuote(cfg.agent)} AGENT_CWD=${shellQuote(cfg.agentCwd)} STATE_PATH=${STATE} DAEMON_TOKEN="$mw_token" ${BIN} > ${LOG} 2>&1 < /dev/null 9>&- &`,
1398
1551
  // Health-poll from inside the VM (loopback) and emit a marker — exit codes are unreliable here.
1399
- `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`,
1552
+ `for i in $(seq 1 60); do curl -fsS --max-time 2 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz >/dev/null 2>&1 && { echo MINDWIRE_READY; exit 0; }; sleep 0.25; done`,
1400
1553
  "echo MINDWIRE_FAIL",
1401
1554
  `tail -n 40 ${LOG} 2>/dev/null || true`
1402
1555
  ].join("\n");
1403
1556
  emit2({ phase: "launch", message: "launching daemon" });
1404
- const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 45 });
1557
+ const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 420 });
1405
1558
  const out = res.stdout ?? "";
1406
1559
  if (!out.includes("MINDWIRE_READY")) {
1407
1560
  throw new MindwireError(
@@ -2306,6 +2459,6 @@ function clearCatalogCache() {
2306
2459
  inflight = null;
2307
2460
  }
2308
2461
 
2309
- export { ApiError, AuthApi, ContainerHost, Http, MODELS_DEV_URL, McpApi, Mindwire, MindwireError, NotifyApi, PromptsApi, ProvidersApi, Run, RunFailedError, SDK_VERSION, TimeoutError, catalogModels, catalogProvider, catalogProviders, clearCatalogCache, docker, ensureDaemon, ensureDaemonBinary, loadCatalog, local, lookupModel, oblien, provisionContainer, provisionDocker, provisionOblien, provisionSsh, provisionSshContainer, remote, resolveLinuxDaemon, ssh, startEmbedded };
2462
+ export { ApiError, AuthApi, ContainerHost, Http, MODELS_DEV_URL, McpApi, Mindwire, MindwireError, NotifyApi, ProjectOperationsApi, PromptsApi, ProvidersApi, Run, RunFailedError, SDK_VERSION, TimeoutError, WorkspaceApi, WorkspaceCollection, catalogModels, catalogProvider, catalogProviders, clearCatalogCache, docker, ensureDaemon, ensureDaemonBinary, loadCatalog, local, lookupModel, oblien, provisionContainer, provisionDocker, provisionOblien, provisionSsh, provisionSshContainer, remote, resolveLinuxDaemon, ssh, startEmbedded };
2310
2463
  //# sourceMappingURL=index.js.map
2311
2464
  //# sourceMappingURL=index.js.map