mindwire 0.1.11 → 0.1.14
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 +73 -0
- package/dist/client.d.ts +5 -0
- package/dist/index.cjs +271 -32
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +4 -0
- package/dist/index.js +268 -33
- package/dist/index.js.map +1 -1
- package/dist/run.d.ts +3 -3
- package/dist/surfaces.d.ts +177 -0
- package/dist/target/host.d.ts +5 -3
- package/dist/target/oblien.d.ts +1 -1
- package/dist/target/ssh.d.ts +1 -1
- package/dist/types.d.ts +46 -5
- package/dist/workspace.d.ts +149 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -61,6 +61,79 @@ setTimeout(() => run.cancel(), 5_000);
|
|
|
61
61
|
for await (const ev of run) { /* … */ }
|
|
62
62
|
```
|
|
63
63
|
|
|
64
|
+
## Saved workspace data
|
|
65
|
+
|
|
66
|
+
`mw.workspace` manages named agent profiles, projects and chat links in the
|
|
67
|
+
workspace's SQLite registry. It is shared by every `withAgent()` view. Harness
|
|
68
|
+
configuration/history remain in their existing harness stores; credentials still
|
|
69
|
+
use the daemon's existing auth adapters and credential store.
|
|
70
|
+
|
|
71
|
+
```ts
|
|
72
|
+
const profileId = crypto.randomUUID();
|
|
73
|
+
const projectId = crypto.randomUUID();
|
|
74
|
+
const chatId = crypto.randomUUID();
|
|
75
|
+
await mw.workspace.agents.put(profileId, { name: "My Codex", agentType: "codex" });
|
|
76
|
+
// This metadata API opens an existing directory. See createProject below for cloning.
|
|
77
|
+
await mw.workspace.projects.put(projectId, { name: "App", path: "/work/app" });
|
|
78
|
+
const saved = await mw.workspace.chats.put(chatId, {
|
|
79
|
+
agentId: profileId, projectId, title: "Build app",
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
// A second client can reconstruct all saved navigation links.
|
|
83
|
+
const snapshot = await mw.workspace.snapshot();
|
|
84
|
+
const updates = await mw.workspace.changes(saved.revision, saved.workspaceId);
|
|
85
|
+
const project = snapshot.projects.find(p => p.id === projectId)!;
|
|
86
|
+
await mw.workspace.projects.put(projectId, { ...project, name: "Renamed" }, project.revision);
|
|
87
|
+
```
|
|
88
|
+
|
|
89
|
+
Use stable IDs when retrying creates and the latest record revision when updating
|
|
90
|
+
or deleting. Conflicts return `ApiError` with status 409; a removed ID returns 410.
|
|
91
|
+
Collection deletion removes membership and dependent chat links, retaining files
|
|
92
|
+
and native transcripts. Use `deleteChat()` for an explicit transcript purge.
|
|
93
|
+
|
|
94
|
+
Check `health().workspaceMetadataVersion` before enabling registry features on an
|
|
95
|
+
older daemon. `workspace.import()` imports legacy IDs once; existing records and
|
|
96
|
+
deletion markers win. See the [registry contract](../../daemon/WORKSPACES.md) for
|
|
97
|
+
migration and recovery. The embedded Go SDK exposes the same operations through
|
|
98
|
+
`client.Workspace`, using the same SQLite implementation.
|
|
99
|
+
|
|
100
|
+
Project workflows run in the daemon, including directory validation, clone output,
|
|
101
|
+
cancellation, retries, registration and folder removal. Check `health().projectOperationsVersion`.
|
|
102
|
+
|
|
103
|
+
```ts
|
|
104
|
+
const operation = await mw.workspace.createProject({
|
|
105
|
+
id: crypto.randomUUID(), // retain this ID when retrying a lost acknowledgement
|
|
106
|
+
source: "clone", // or "folder" (existing) / "create" (new directory)
|
|
107
|
+
name: "My app",
|
|
108
|
+
path: "~/projects/my-app", // parent exists; destination must be absent for cloning
|
|
109
|
+
repoUrl: "https://github.com/owner/repo.git",
|
|
110
|
+
// auth: { kind: "token", token: installationToken } — write-only, never in origin
|
|
111
|
+
});
|
|
112
|
+
for await (const snapshot of mw.workspace.operations.watch(operation.id)) {
|
|
113
|
+
console.log(snapshot.status, snapshot.phase, snapshot.progress);
|
|
114
|
+
}
|
|
115
|
+
// A new client can list/observe the same operation, including after disconnect.
|
|
116
|
+
await mw.workspace.operations.list(true);
|
|
117
|
+
// Explicit cancellation/retry; breaking a watch loop only detaches that observer.
|
|
118
|
+
// await mw.workspace.operations.cancel(operation.id);
|
|
119
|
+
// await mw.workspace.operations.retry(operation.id, freshAuth);
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
In Go, use `client.Workspace.CreateProject(ProjectRequest{...})` and
|
|
123
|
+
`client.Workspace.Operations.Get/List/Watch/Cancel/Retry`. These call the same
|
|
124
|
+
project service as HTTP. Active destination reservations prevent duplicate clones;
|
|
125
|
+
the final project and successful operation status commit in one SQLite transaction.
|
|
126
|
+
An interrupted daemon reconciles a staged commit or marks earlier work retryable.
|
|
127
|
+
|
|
128
|
+
`workspace.projects.delete(id, revision)` removes registry membership while retaining
|
|
129
|
+
files and native transcripts. Explicit folder deletion uses
|
|
130
|
+
`workspace.removeProjectFiles(id, { operationId, expectedRevision })` (Go:
|
|
131
|
+
`Workspace.RemoveProjectFiles`). Confirm it with the user first, retain the operation
|
|
132
|
+
ID for transport retries, and observe the returned operation as above. The daemon
|
|
133
|
+
blocks running chats/overlapping projects and recovers removal across restarts.
|
|
134
|
+
Cancellation returns 409 after the `deleting` phase; cleanup then affects only the
|
|
135
|
+
owned quarantine, even if someone recreates the original directory.
|
|
136
|
+
|
|
64
137
|
## Targets — where the daemon runs
|
|
65
138
|
|
|
66
139
|
**One `new Mindwire` = one instance = one daemon = one environment.** Every agent that instance runs
|
package/dist/client.d.ts
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import { Http, type FetchLike } from "./http.js";
|
|
2
2
|
import { Run } from "./run.js";
|
|
3
|
+
import { WorkspaceApi } from "./workspace.js";
|
|
4
|
+
import { SurfacesApi } from "./surfaces.js";
|
|
3
5
|
import { type Target } from "./target/index.js";
|
|
4
6
|
import type { EnsureEvent } from "./target/host.js";
|
|
5
7
|
import type { AgentInfo, AuthMethod, AuthState, AuthStatus, Catalog, ChatSummary, CustomProvider, DeleteResult, DoctorReport, Health, MCPServer, MemoryDoc, MemoryScope, Message, ModelInfo, Notification, NotifyChannel, NotifyChannelInput, NotifyChannelTestResult, NotifyConfigInput, NotifyConfigStatus, NotifyRule, NotifyRuleInput, ProcessFrame, PromptTemplate, ResolveOptions, SetupStatus, Stats, Subagent, TurnOptions } from "./types.js";
|
|
@@ -67,6 +69,9 @@ export interface AgentScoped {
|
|
|
67
69
|
* ```
|
|
68
70
|
*/
|
|
69
71
|
export declare class Mindwire {
|
|
72
|
+
/** Workspace registry: saved agent profiles, projects and chat relationships. */
|
|
73
|
+
readonly workspace: WorkspaceApi;
|
|
74
|
+
readonly surfaces: SurfacesApi;
|
|
70
75
|
readonly http: Http;
|
|
71
76
|
/** The default agent type applied to agent-scoped calls, if set. */
|
|
72
77
|
readonly defaultAgent: string | undefined;
|
package/dist/index.cjs
CHANGED
|
@@ -289,6 +289,8 @@ async function* readSSE(body, signal) {
|
|
|
289
289
|
if (payload !== void 0) yield JSON.parse(payload);
|
|
290
290
|
} finally {
|
|
291
291
|
if (signal) signal.removeEventListener("abort", onAbort);
|
|
292
|
+
await reader.cancel().catch(() => {
|
|
293
|
+
});
|
|
292
294
|
reader.releaseLock();
|
|
293
295
|
}
|
|
294
296
|
}
|
|
@@ -413,9 +415,9 @@ var Run = class _Run {
|
|
|
413
415
|
});
|
|
414
416
|
}
|
|
415
417
|
/**
|
|
416
|
-
* Switch the permission mode
|
|
417
|
-
*
|
|
418
|
-
*
|
|
418
|
+
* Switch the live permission mode using a value from the agent's settings schema.
|
|
419
|
+
* Resolves after the harness acknowledges the change; rejects if it cannot apply it.
|
|
420
|
+
* Requires `setPermissionMode`. Codex settings apply on the next turn instead.
|
|
419
421
|
*/
|
|
420
422
|
async setPermissionMode(mode) {
|
|
421
423
|
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/set-permission-mode`, {
|
|
@@ -475,8 +477,174 @@ var Run = class _Run {
|
|
|
475
477
|
}
|
|
476
478
|
};
|
|
477
479
|
|
|
480
|
+
// src/workspace.ts
|
|
481
|
+
var ProjectOperationsApi = class {
|
|
482
|
+
constructor(mw) {
|
|
483
|
+
this.mw = mw;
|
|
484
|
+
}
|
|
485
|
+
mw;
|
|
486
|
+
async list(activeOnly = false) {
|
|
487
|
+
const response = await this.mw.http.request("GET", "/workspace/operations", {
|
|
488
|
+
query: { active: activeOnly }
|
|
489
|
+
});
|
|
490
|
+
return response.operations;
|
|
491
|
+
}
|
|
492
|
+
get(id) {
|
|
493
|
+
return this.mw.http.request("GET", `/workspace/operations/${encodeURIComponent(id)}`);
|
|
494
|
+
}
|
|
495
|
+
cancel(id) {
|
|
496
|
+
return this.mw.http.request("POST", `/workspace/operations/${encodeURIComponent(id)}/cancel`);
|
|
497
|
+
}
|
|
498
|
+
retry(id, auth) {
|
|
499
|
+
return this.mw.http.request("POST", `/workspace/operations/${encodeURIComponent(id)}/retry`, { body: { auth } });
|
|
500
|
+
}
|
|
501
|
+
/** The first event is the current snapshot, then live changes. Reconnecting never replays old
|
|
502
|
+
* progress. Breaking the loop/aborting detaches the observer; cancel(id) explicitly stops work.
|
|
503
|
+
*/
|
|
504
|
+
async *watch(id, opts = {}) {
|
|
505
|
+
const controller = new AbortController();
|
|
506
|
+
const abort = () => controller.abort();
|
|
507
|
+
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
508
|
+
if (opts.signal?.aborted) controller.abort();
|
|
509
|
+
try {
|
|
510
|
+
const response = await this.mw.http.open("GET", `/workspace/operations/${encodeURIComponent(id)}/stream`, {
|
|
511
|
+
signal: controller.signal
|
|
512
|
+
});
|
|
513
|
+
let sequence = -1;
|
|
514
|
+
for await (const operation of readSSE(response.body, controller.signal)) {
|
|
515
|
+
if (operation.sequence > sequence) {
|
|
516
|
+
sequence = operation.sequence;
|
|
517
|
+
yield operation;
|
|
518
|
+
}
|
|
519
|
+
}
|
|
520
|
+
} finally {
|
|
521
|
+
controller.abort();
|
|
522
|
+
opts.signal?.removeEventListener("abort", abort);
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
};
|
|
526
|
+
var WorkspaceCollection = class {
|
|
527
|
+
constructor(mw, kind) {
|
|
528
|
+
this.mw = mw;
|
|
529
|
+
this.kind = kind;
|
|
530
|
+
}
|
|
531
|
+
mw;
|
|
532
|
+
kind;
|
|
533
|
+
/** Create with a stable client-generated ID. For updates, supply the record's last revision. */
|
|
534
|
+
put(id, record, expectedRevision) {
|
|
535
|
+
return this.mw.http.request("PUT", `/workspace/${this.kind}/${encodeURIComponent(id)}`, {
|
|
536
|
+
body: { record, expectedRevision }
|
|
537
|
+
});
|
|
538
|
+
}
|
|
539
|
+
/** Remove membership and dependent chat links. Files and native transcripts are retained.
|
|
540
|
+
* Use deleteChat() for an explicit transcript purge. Running chats reject removal with 409.
|
|
541
|
+
*/
|
|
542
|
+
delete(id, revision) {
|
|
543
|
+
return this.mw.http.request("DELETE", `/workspace/${this.kind}/${encodeURIComponent(id)}`, {
|
|
544
|
+
query: { revision }
|
|
545
|
+
});
|
|
546
|
+
}
|
|
547
|
+
};
|
|
548
|
+
var WorkspaceApi = class {
|
|
549
|
+
constructor(mw) {
|
|
550
|
+
this.mw = mw;
|
|
551
|
+
this.operations = new ProjectOperationsApi(mw);
|
|
552
|
+
this.agents = new WorkspaceCollection(mw, "agents");
|
|
553
|
+
this.projects = new WorkspaceCollection(mw, "projects");
|
|
554
|
+
this.chats = new WorkspaceCollection(mw, "chats");
|
|
555
|
+
}
|
|
556
|
+
mw;
|
|
557
|
+
operations;
|
|
558
|
+
agents;
|
|
559
|
+
projects;
|
|
560
|
+
chats;
|
|
561
|
+
snapshot() {
|
|
562
|
+
return this.mw.http.request("GET", "/workspace");
|
|
563
|
+
}
|
|
564
|
+
/** Start an operation owned by the daemon. The same ID/payload returns the existing operation. */
|
|
565
|
+
createProject(request) {
|
|
566
|
+
return this.mw.http.request("POST", "/workspace/projects", { body: request });
|
|
567
|
+
}
|
|
568
|
+
/** Permanently remove the confirmed project's directory and membership.
|
|
569
|
+
* projects.delete() retains files. Native harness transcripts are not purged.
|
|
570
|
+
*/
|
|
571
|
+
removeProjectFiles(id, request) {
|
|
572
|
+
return this.mw.http.request("POST", `/workspace/projects/${encodeURIComponent(id)}/remove`, { body: request });
|
|
573
|
+
}
|
|
574
|
+
/** Incremental reconciliation. Pass the previous identity to detect a replaced/restored workspace.
|
|
575
|
+
* A 409 requires fetching snapshot() again; never apply a delta to a different registry.
|
|
576
|
+
*/
|
|
577
|
+
changes(since, workspaceId) {
|
|
578
|
+
return this.mw.http.request("GET", "/workspace/changes", { query: { since, workspaceId } });
|
|
579
|
+
}
|
|
580
|
+
/** Import legacy metadata before replacing a local cache. Safe to repeat after interruption. */
|
|
581
|
+
import(records) {
|
|
582
|
+
return this.mw.http.request("POST", "/workspace/import", { body: records });
|
|
583
|
+
}
|
|
584
|
+
};
|
|
585
|
+
|
|
586
|
+
// src/surfaces.ts
|
|
587
|
+
var SurfacesApi = class {
|
|
588
|
+
constructor(mw) {
|
|
589
|
+
this.mw = mw;
|
|
590
|
+
}
|
|
591
|
+
mw;
|
|
592
|
+
list() {
|
|
593
|
+
return this.mw.http.request("GET", "/surfaces");
|
|
594
|
+
}
|
|
595
|
+
status(refresh = false) {
|
|
596
|
+
return this.mw.http.request("GET", "/surfaces/desktop", { query: { refresh } });
|
|
597
|
+
}
|
|
598
|
+
bind(binding) {
|
|
599
|
+
return this.mw.http.request("PUT", "/surfaces/desktop/binding", { body: binding });
|
|
600
|
+
}
|
|
601
|
+
open(request) {
|
|
602
|
+
return this.mw.http.request("POST", "/surfaces/desktop/sessions", { body: request });
|
|
603
|
+
}
|
|
604
|
+
control(id, request) {
|
|
605
|
+
return this.mw.http.request("POST", `/surfaces/desktop/sessions/${encodeURIComponent(id)}/control`, { body: request });
|
|
606
|
+
}
|
|
607
|
+
close(id) {
|
|
608
|
+
return this.mw.http.request("DELETE", `/surfaces/desktop/sessions/${encodeURIComponent(id)}`);
|
|
609
|
+
}
|
|
610
|
+
capture(id) {
|
|
611
|
+
return this.mw.http.request("POST", `/surfaces/desktop/sessions/${encodeURIComponent(id)}/captures`);
|
|
612
|
+
}
|
|
613
|
+
action(request) {
|
|
614
|
+
return this.mw.http.request("POST", "/surfaces/desktop/actions", { body: request });
|
|
615
|
+
}
|
|
616
|
+
receipt(id) {
|
|
617
|
+
return this.mw.http.request("GET", `/surfaces/desktop/actions/${encodeURIComponent(id)}`);
|
|
618
|
+
}
|
|
619
|
+
artifact(id) {
|
|
620
|
+
return this.mw.http.request("GET", `/artifacts/${encodeURIComponent(id)}`);
|
|
621
|
+
}
|
|
622
|
+
/** Every connection starts with the current snapshot, then revisions. Never replays input. */
|
|
623
|
+
async *watch(opts = {}) {
|
|
624
|
+
const controller = new AbortController();
|
|
625
|
+
const abort = () => controller.abort();
|
|
626
|
+
opts.signal?.addEventListener("abort", abort, { once: true });
|
|
627
|
+
if (opts.signal?.aborted) controller.abort();
|
|
628
|
+
try {
|
|
629
|
+
const response = await this.mw.http.open("GET", "/surfaces/desktop/events", { signal: controller.signal });
|
|
630
|
+
let instance;
|
|
631
|
+
let revision = -1;
|
|
632
|
+
for await (const snapshot of readSSE(response.body, controller.signal)) {
|
|
633
|
+
if (instance !== snapshot.instanceId || snapshot.revision > revision) {
|
|
634
|
+
instance = snapshot.instanceId;
|
|
635
|
+
revision = snapshot.revision;
|
|
636
|
+
yield snapshot;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
} finally {
|
|
640
|
+
controller.abort();
|
|
641
|
+
opts.signal?.removeEventListener("abort", abort);
|
|
642
|
+
}
|
|
643
|
+
}
|
|
644
|
+
};
|
|
645
|
+
|
|
478
646
|
// src/version.ts
|
|
479
|
-
var SDK_VERSION = "0.1.
|
|
647
|
+
var SDK_VERSION = "0.1.14" ;
|
|
480
648
|
|
|
481
649
|
// src/daemon-binary.ts
|
|
482
650
|
function supported(platform, arch) {
|
|
@@ -734,6 +902,9 @@ function remote(baseUrl, opts = {}) {
|
|
|
734
902
|
// src/client.ts
|
|
735
903
|
var handleByTransport = /* @__PURE__ */ new WeakMap();
|
|
736
904
|
var Mindwire = class _Mindwire {
|
|
905
|
+
/** Workspace registry: saved agent profiles, projects and chat relationships. */
|
|
906
|
+
workspace;
|
|
907
|
+
surfaces;
|
|
737
908
|
http;
|
|
738
909
|
/** The default agent type applied to agent-scoped calls, if set. */
|
|
739
910
|
defaultAgent;
|
|
@@ -767,6 +938,8 @@ var Mindwire = class _Mindwire {
|
|
|
767
938
|
}
|
|
768
939
|
});
|
|
769
940
|
this.defaultAgent = opts.agent;
|
|
941
|
+
this.workspace = new WorkspaceApi(this);
|
|
942
|
+
this.surfaces = new SurfacesApi(this);
|
|
770
943
|
this.auth = new AuthApi(this);
|
|
771
944
|
this.prompts = new PromptsApi(this);
|
|
772
945
|
this.mcp = new McpApi(this);
|
|
@@ -787,6 +960,8 @@ var Mindwire = class _Mindwire {
|
|
|
787
960
|
const clone = Object.create(_Mindwire.prototype);
|
|
788
961
|
clone.http = this.http;
|
|
789
962
|
clone.defaultAgent = agent;
|
|
963
|
+
clone.workspace = new WorkspaceApi(clone);
|
|
964
|
+
clone.surfaces = new SurfacesApi(clone);
|
|
790
965
|
clone.auth = new AuthApi(clone);
|
|
791
966
|
clone.prompts = new PromptsApi(clone);
|
|
792
967
|
clone.mcp = new McpApi(clone);
|
|
@@ -1309,17 +1484,24 @@ function makeEmit(cfg) {
|
|
|
1309
1484
|
}
|
|
1310
1485
|
};
|
|
1311
1486
|
}
|
|
1312
|
-
|
|
1313
|
-
|
|
1314
|
-
|
|
1315
|
-
|
|
1316
|
-
|
|
1487
|
+
async function daemonDirectory(host) {
|
|
1488
|
+
const result = await host.exec(["sh", "-lc", 'printf "<<MW_HOME>>%s<<MW_HOME>>" "$HOME"'], { timeoutSeconds: 15 });
|
|
1489
|
+
const runtimeHome = result.stdout?.match(/<<MW_HOME>>([\s\S]*?)<<MW_HOME>>/)?.[1];
|
|
1490
|
+
if (!runtimeHome?.startsWith("/") || runtimeHome.length > 4096 || /[\x00-\x1f\x7f]/.test(runtimeHome)) {
|
|
1491
|
+
throw new MindwireError("mindwire: cannot resolve the runtime user's home directory");
|
|
1492
|
+
}
|
|
1493
|
+
return runtimeHome.replace(/\/+$/, "") + "/.mindwire";
|
|
1494
|
+
}
|
|
1317
1495
|
async function ensureDaemon(host, cfg) {
|
|
1318
1496
|
const emit2 = makeEmit(cfg);
|
|
1319
|
-
|
|
1497
|
+
let token = cfg.token ?? (await import('crypto')).randomBytes(32).toString("hex");
|
|
1320
1498
|
try {
|
|
1321
1499
|
await waitHostReady(host);
|
|
1322
1500
|
emit2({ phase: "connect", message: "runtime ready" });
|
|
1501
|
+
const directory = await daemonDirectory(host);
|
|
1502
|
+
if (!cfg.token) {
|
|
1503
|
+
token = await readWorkspaceToken(host, directory) ?? token;
|
|
1504
|
+
}
|
|
1323
1505
|
const desired = cfg.desiredVersion ?? SDK_VERSION;
|
|
1324
1506
|
const health = await probeHealth(host, cfg.port, token);
|
|
1325
1507
|
if (health.reachable) {
|
|
@@ -1340,37 +1522,54 @@ async function ensureDaemon(host, cfg) {
|
|
|
1340
1522
|
} else {
|
|
1341
1523
|
emit2({ phase: "probe", message: "no daemon reachable; deploying" });
|
|
1342
1524
|
}
|
|
1343
|
-
await deploy(host, cfg, emit2, token);
|
|
1525
|
+
await deploy(host, cfg, emit2, token, directory);
|
|
1526
|
+
if (!cfg.token) token = await readWorkspaceToken(host, directory) ?? token;
|
|
1344
1527
|
return token;
|
|
1345
1528
|
} catch (err) {
|
|
1346
1529
|
emit2({ phase: "error", message: "ensure failed", error: err instanceof Error ? err.message : String(err) });
|
|
1347
1530
|
throw err;
|
|
1348
1531
|
}
|
|
1349
1532
|
}
|
|
1350
|
-
async function
|
|
1351
|
-
const
|
|
1533
|
+
async function readWorkspaceToken(host, directory) {
|
|
1534
|
+
const saved = await host.exec(["sh", "-lc", `cat ${shellQuote(directory + "/daemon.token")} 2>/dev/null || true`], { timeoutSeconds: 15 });
|
|
1535
|
+
const candidate = saved.stdout?.trim();
|
|
1536
|
+
return candidate && candidate.length <= 4096 && /^[\x21-\x7e]+$/.test(candidate) ? candidate : void 0;
|
|
1537
|
+
}
|
|
1538
|
+
async function deploy(host, cfg, emit2, token, directory) {
|
|
1539
|
+
const newPath = directory + "/mindwired.new";
|
|
1540
|
+
const BIN = shellQuote(directory + "/mindwired"), BIN_NEW = shellQuote(newPath);
|
|
1541
|
+
const STATE = shellQuote(directory + "/agent-state.json"), LOG = shellQuote(directory + "/daemon.log");
|
|
1542
|
+
const TOKEN = shellQuote(directory + "/daemon.token");
|
|
1543
|
+
const { platform, arch } = await probePlatform(host);
|
|
1352
1544
|
const desired = cfg.desiredVersion ?? SDK_VERSION;
|
|
1353
1545
|
let acquire = "";
|
|
1546
|
+
let stagedUpload;
|
|
1354
1547
|
if (cfg.daemonBin) {
|
|
1355
|
-
const binPath = await
|
|
1548
|
+
const binPath = await resolveHostDaemon(cfg.daemonBin, platform, arch);
|
|
1356
1549
|
const bytes = await readBytes(binPath);
|
|
1357
|
-
emit2({ phase: "upload", message: `uploading daemon (${arch}, ${formatMiB(bytes.length)})`, arch, bytes: bytes.length });
|
|
1358
|
-
await
|
|
1550
|
+
emit2({ phase: "upload", message: `uploading daemon (${platform}-${arch}, ${formatMiB(bytes.length)})`, platform, arch, bytes: bytes.length });
|
|
1551
|
+
stagedUpload = `${newPath}-${(await import('crypto')).randomUUID()}`;
|
|
1552
|
+
await host.exec(["sh", "-lc", `mkdir -p ${shellQuote(directory)}`], { timeoutSeconds: 15 });
|
|
1553
|
+
await host.putFile(stagedUpload, bytes, { mode: "0755" });
|
|
1554
|
+
acquire = `mv -f ${shellQuote(stagedUpload)} ${BIN_NEW}`;
|
|
1359
1555
|
} else {
|
|
1360
1556
|
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(desired)) {
|
|
1361
1557
|
throw new MindwireError(`mindwire: cannot download daemon for non-release SDK version ${desired}`);
|
|
1362
1558
|
}
|
|
1363
|
-
const asset = `mindwired-v${desired}
|
|
1559
|
+
const asset = `mindwired-v${desired}-${platform}-${arch}`;
|
|
1364
1560
|
const release = `https://github.com/oblien/mindwire/releases/download/v${desired}`;
|
|
1365
|
-
emit2({ phase: "download", message: `downloading daemon v${desired} on the destination (${arch})`, arch });
|
|
1561
|
+
emit2({ phase: "download", message: `downloading daemon v${desired} on the destination (${platform}-${arch})`, platform, arch });
|
|
1366
1562
|
acquire = [
|
|
1367
1563
|
`release=${shellQuote(release)}`,
|
|
1368
1564
|
`asset=${shellQuote(asset)}`,
|
|
1565
|
+
"if command -v sha256sum >/dev/null 2>&1; then mw_sha256=(sha256sum);",
|
|
1566
|
+
"elif command -v shasum >/dev/null 2>&1; then mw_sha256=(shasum -a 256);",
|
|
1567
|
+
'else echo "MINDWIRE_FAIL SHA-256 verification requires sha256sum or shasum"; exit 1; fi',
|
|
1369
1568
|
"download() {",
|
|
1370
1569
|
` expected=$(curl -fsSL "$release/checksums.txt" | awk -v asset="$asset" '$2 == asset { print $1; exit }') || return 1`,
|
|
1371
1570
|
' [ -n "$expected" ] || return 1',
|
|
1372
1571
|
` curl -fsSL "$release/$asset" -o ${BIN_NEW} || return 1`,
|
|
1373
|
-
` actual=$(
|
|
1572
|
+
` actual=$("\${mw_sha256[@]}" ${BIN_NEW} | awk '{print $1}') || return 1`,
|
|
1374
1573
|
' [ "$actual" = "$expected" ] || return 2',
|
|
1375
1574
|
"}",
|
|
1376
1575
|
"if download; then :; else",
|
|
@@ -1378,33 +1577,61 @@ async function deploy(host, cfg, emit2, token) {
|
|
|
1378
1577
|
` latest=$(curl -fsSL https://api.github.com/repos/oblien/mindwire/releases/latest | sed -n 's/.*"tag_name"[[:space:]]*:[[:space:]]*"\\([^"]*\\)".*/\\1/p')`,
|
|
1379
1578
|
' case "$latest" in v[0-9]*.[0-9]*.[0-9]*) ;; *) echo "MINDWIRE_FAIL no matching or latest release"; exit 1 ;; esac',
|
|
1380
1579
|
' release="https://github.com/oblien/mindwire/releases/download/$latest"',
|
|
1381
|
-
` asset="mindwired-$latest
|
|
1580
|
+
` asset="mindwired-$latest-${platform}-${arch}"`,
|
|
1382
1581
|
' download || { echo "MINDWIRE_FAIL latest release download failed"; exit 1; }',
|
|
1383
1582
|
"fi",
|
|
1384
1583
|
`chmod +x ${BIN_NEW}`
|
|
1385
1584
|
].join("\n");
|
|
1386
1585
|
}
|
|
1387
1586
|
const script = [
|
|
1388
|
-
"set -
|
|
1389
|
-
`mkdir -p ${
|
|
1587
|
+
"set -eo pipefail",
|
|
1588
|
+
`mkdir -p ${shellQuote(directory)}`,
|
|
1589
|
+
// iOS takes this same workspace lock. Unique staging also protects concurrent local uploads.
|
|
1590
|
+
stagedUpload ? `trap ${shellQuote(`rm -f ${shellQuote(stagedUpload)}`)} EXIT` : "",
|
|
1591
|
+
`exec 9>${shellQuote(directory + "/daemon-install.lock")}`,
|
|
1592
|
+
"if command -v flock >/dev/null 2>&1; then",
|
|
1593
|
+
' flock -w 360 9 || { echo "MINDWIRE_FAIL another daemon update is still running"; exit 1; }',
|
|
1594
|
+
"elif command -v lockf >/dev/null 2>&1; then",
|
|
1595
|
+
' lockf -s -t 360 9 || { echo "MINDWIRE_FAIL another daemon update is still running"; exit 1; }',
|
|
1596
|
+
'else echo "MINDWIRE_FAIL No supported update lock is available (flock on Linux, lockf on macOS)."; exit 1; fi',
|
|
1597
|
+
`mw_token=${shellQuote(token)}`,
|
|
1598
|
+
!cfg.token ? `if [ -s ${TOKEN} ]; then mw_token=$(cat ${TOKEN}); fi` : "",
|
|
1599
|
+
!cfg.forceDeploy ? [
|
|
1600
|
+
`mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
|
|
1601
|
+
`mw_version=$(printf '%s' "$mw_health" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^" ]*\\)".*/\\1/p')`,
|
|
1602
|
+
`if [ -n "$mw_health" ] && ${cfg.autoUpdate ? `[ "$mw_version" = ${shellQuote(desired)} ]` : "true"}; then echo MINDWIRE_READY; exit 0; fi`
|
|
1603
|
+
].join("\n") : "",
|
|
1604
|
+
// macOS ships POSIX setsid in Perl, but does not include Linux's setsid executable.
|
|
1605
|
+
"if command -v setsid >/dev/null 2>&1; then mw_detach=(setsid);",
|
|
1606
|
+
`elif command -v perl >/dev/null 2>&1; then mw_detach=(perl -MPOSIX -e 'defined(my $sid = POSIX::setsid()) && $sid >= 0 or die "setsid: $!"; exec @ARGV; die "exec: $!";');`,
|
|
1607
|
+
'else echo "MINDWIRE_FAIL Cannot start the Mindwire service: setsid or Perl is required."; exit 1; fi',
|
|
1390
1608
|
acquire,
|
|
1391
1609
|
// Stop a prior daemon by exact process NAME, never `pkill -f <path>`: this whole script (which
|
|
1392
1610
|
// contains `${BIN}` several times) is the argv of the `bash -lc` shell running it, so a full-cmdline
|
|
1393
1611
|
// match would SIGTERM our own deploying shell before the daemon ever launches. `-x mindwired` matches
|
|
1394
1612
|
// only the daemon's comm (`bash`/`pkill` never match), leaving this shell alive.
|
|
1395
|
-
|
|
1613
|
+
"if command -v pkill >/dev/null 2>&1; then",
|
|
1614
|
+
" pkill -x mindwired 2>/dev/null || true",
|
|
1615
|
+
"else",
|
|
1616
|
+
" for mw_proc in /proc/[0-9]*/comm; do",
|
|
1617
|
+
' IFS= read -r mw_name 2>/dev/null < "$mw_proc" || continue',
|
|
1618
|
+
' [ "$mw_name" = mindwired ] || continue',
|
|
1619
|
+
" mw_pid=${mw_proc#/proc/}; mw_pid=${mw_pid%/comm}",
|
|
1620
|
+
' kill "$mw_pid" 2>/dev/null || true',
|
|
1621
|
+
" done",
|
|
1622
|
+
"fi",
|
|
1396
1623
|
"sleep 0.3",
|
|
1397
1624
|
`mv -f ${BIN_NEW} ${BIN}`,
|
|
1398
1625
|
`chmod +x ${BIN}`,
|
|
1399
1626
|
// Detach so the daemon survives this exec's shell exiting. ADDR=":<port>" binds 0.0.0.0.
|
|
1400
|
-
`
|
|
1627
|
+
`"\${mw_detach[@]}" 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>&- &`,
|
|
1401
1628
|
// Health-poll from inside the VM (loopback) and emit a marker — exit codes are unreliable here.
|
|
1402
|
-
`for
|
|
1629
|
+
`for ((mw_attempt=0; mw_attempt<60; mw_attempt++)); 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`,
|
|
1403
1630
|
"echo MINDWIRE_FAIL",
|
|
1404
1631
|
`tail -n 40 ${LOG} 2>/dev/null || true`
|
|
1405
1632
|
].join("\n");
|
|
1406
1633
|
emit2({ phase: "launch", message: "launching daemon" });
|
|
1407
|
-
const res = await host.exec(["bash", "-lc", script], { timeoutSeconds:
|
|
1634
|
+
const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 420 });
|
|
1408
1635
|
const out = res.stdout ?? "";
|
|
1409
1636
|
if (!out.includes("MINDWIRE_READY")) {
|
|
1410
1637
|
throw new MindwireError(
|
|
@@ -1425,10 +1652,15 @@ async function probeHealth(host, port, token) {
|
|
|
1425
1652
|
return { reachable: true };
|
|
1426
1653
|
}
|
|
1427
1654
|
}
|
|
1428
|
-
async function
|
|
1429
|
-
const res = await host.exec(["bash", "-lc", `printf '<<ARCH:%s>>' "$(uname -m)"`], { timeoutSeconds: 15 });
|
|
1430
|
-
const
|
|
1431
|
-
|
|
1655
|
+
async function probePlatform(host) {
|
|
1656
|
+
const res = await host.exec(["bash", "-lc", `printf '<<OS:%s>><<ARCH:%s>>' "$(uname -s)" "$(uname -m)"`], { timeoutSeconds: 15 });
|
|
1657
|
+
const os = ((res.stdout ?? "").match(/<<OS:([^>]*)>>/)?.[1] ?? "").trim().toLowerCase();
|
|
1658
|
+
const rawArch = ((res.stdout ?? "").match(/<<ARCH:([^>]*)>>/)?.[1] ?? "").trim();
|
|
1659
|
+
const arch = rawArch === "aarch64" || rawArch === "arm64" ? "arm64" : rawArch === "x86_64" || rawArch === "amd64" ? "amd64" : void 0;
|
|
1660
|
+
if (os !== "linux" && os !== "darwin" || !arch) {
|
|
1661
|
+
throw new MindwireError(`mindwire: unsupported workspace platform ${os || "unknown"}/${rawArch || "unknown"}; expected Linux or macOS on amd64 or arm64`);
|
|
1662
|
+
}
|
|
1663
|
+
return { platform: os, arch };
|
|
1432
1664
|
}
|
|
1433
1665
|
async function waitHostReady(host, timeoutMs = 6e4) {
|
|
1434
1666
|
const deadline = Date.now() + timeoutMs;
|
|
@@ -1448,15 +1680,18 @@ async function waitHostReady(host, timeoutMs = 6e4) {
|
|
|
1448
1680
|
);
|
|
1449
1681
|
}
|
|
1450
1682
|
async function resolveLinuxDaemon(explicit, arch) {
|
|
1683
|
+
return resolveHostDaemon(explicit, "linux", arch);
|
|
1684
|
+
}
|
|
1685
|
+
async function resolveHostDaemon(explicit, platform, arch) {
|
|
1451
1686
|
const fs = await import('fs');
|
|
1452
1687
|
if (explicit) {
|
|
1453
|
-
const resolved = explicit.replaceAll("{arch}", arch);
|
|
1688
|
+
const resolved = explicit.replaceAll("{os}", platform).replaceAll("{arch}", arch);
|
|
1454
1689
|
if (!fs.existsSync(resolved)) {
|
|
1455
1690
|
throw new MindwireError(`mindwire: sandbox daemonBin not found at ${resolved}`);
|
|
1456
1691
|
}
|
|
1457
1692
|
return resolved;
|
|
1458
1693
|
}
|
|
1459
|
-
return ensureDaemonBinary({ platform
|
|
1694
|
+
return ensureDaemonBinary({ platform, arch: arch === "arm64" ? "arm64" : "x64" });
|
|
1460
1695
|
}
|
|
1461
1696
|
async function readBytes(p) {
|
|
1462
1697
|
const fs = await import('fs/promises');
|
|
@@ -2318,12 +2553,16 @@ exports.McpApi = McpApi;
|
|
|
2318
2553
|
exports.Mindwire = Mindwire;
|
|
2319
2554
|
exports.MindwireError = MindwireError;
|
|
2320
2555
|
exports.NotifyApi = NotifyApi;
|
|
2556
|
+
exports.ProjectOperationsApi = ProjectOperationsApi;
|
|
2321
2557
|
exports.PromptsApi = PromptsApi;
|
|
2322
2558
|
exports.ProvidersApi = ProvidersApi;
|
|
2323
2559
|
exports.Run = Run;
|
|
2324
2560
|
exports.RunFailedError = RunFailedError;
|
|
2325
2561
|
exports.SDK_VERSION = SDK_VERSION;
|
|
2562
|
+
exports.SurfacesApi = SurfacesApi;
|
|
2326
2563
|
exports.TimeoutError = TimeoutError;
|
|
2564
|
+
exports.WorkspaceApi = WorkspaceApi;
|
|
2565
|
+
exports.WorkspaceCollection = WorkspaceCollection;
|
|
2327
2566
|
exports.catalogModels = catalogModels;
|
|
2328
2567
|
exports.catalogProvider = catalogProvider;
|
|
2329
2568
|
exports.catalogProviders = catalogProviders;
|