mindwire 0.1.10 → 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/README.md +73 -0
- package/dist/client.d.ts +3 -0
- package/dist/index.cjs +182 -19
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.ts +3 -0
- package/dist/index.js +180 -20
- package/dist/index.js.map +1 -1
- package/dist/run.d.ts +8 -4
- package/dist/types.d.ts +53 -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,6 @@
|
|
|
1
1
|
import { Http, type FetchLike } from "./http.js";
|
|
2
2
|
import { Run } from "./run.js";
|
|
3
|
+
import { WorkspaceApi } from "./workspace.js";
|
|
3
4
|
import { type Target } from "./target/index.js";
|
|
4
5
|
import type { EnsureEvent } from "./target/host.js";
|
|
5
6
|
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 +68,8 @@ export interface AgentScoped {
|
|
|
67
68
|
* ```
|
|
68
69
|
*/
|
|
69
70
|
export declare class Mindwire {
|
|
71
|
+
/** Workspace registry: saved agent profiles, projects and chat relationships. */
|
|
72
|
+
readonly workspace: WorkspaceApi;
|
|
70
73
|
readonly http: Http;
|
|
71
74
|
/** The default agent type applied to agent-scoped calls, if set. */
|
|
72
75
|
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
|
}
|
|
@@ -361,7 +363,8 @@ var Run = class _Run {
|
|
|
361
363
|
}
|
|
362
364
|
/** Unified SSE event stream: replay buffer, then live events, then close. */
|
|
363
365
|
async *stream(opts = {}) {
|
|
364
|
-
const
|
|
366
|
+
const cursor = opts.after === void 0 ? "" : `?after=${encodeURIComponent(opts.after)}`;
|
|
367
|
+
const res = await this.http.open("GET", `/runs/${encodeURIComponent(this.id)}/stream${cursor}`, {
|
|
365
368
|
...opts.signal ? { signal: opts.signal } : {}
|
|
366
369
|
});
|
|
367
370
|
for await (const ev of readSSE(res.body, opts.signal)) {
|
|
@@ -412,9 +415,9 @@ var Run = class _Run {
|
|
|
412
415
|
});
|
|
413
416
|
}
|
|
414
417
|
/**
|
|
415
|
-
* Switch the permission mode
|
|
416
|
-
*
|
|
417
|
-
*
|
|
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.
|
|
418
421
|
*/
|
|
419
422
|
async setPermissionMode(mode) {
|
|
420
423
|
await this.http.request("POST", `/runs/${encodeURIComponent(this.id)}/set-permission-mode`, {
|
|
@@ -438,6 +441,12 @@ var Run = class _Run {
|
|
|
438
441
|
this.data = await this.http.request("GET", `/runs/${encodeURIComponent(this.id)}`);
|
|
439
442
|
return this.data;
|
|
440
443
|
}
|
|
444
|
+
/** Restore current output once, then follow with `stream({ after: snapshot.sequence })`. */
|
|
445
|
+
async snapshot() {
|
|
446
|
+
const snapshot = await this.http.request("GET", `/runs/${encodeURIComponent(this.id)}/snapshot`);
|
|
447
|
+
this.data = snapshot.run;
|
|
448
|
+
return snapshot;
|
|
449
|
+
}
|
|
441
450
|
/**
|
|
442
451
|
* Consume the event stream to completion. Returns the final run record and the `result`
|
|
443
452
|
* event's summary (if any). Throws {@link RunFailedError} on an `error`/`cancelled` outcome
|
|
@@ -468,8 +477,114 @@ var Run = class _Run {
|
|
|
468
477
|
}
|
|
469
478
|
};
|
|
470
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
|
+
|
|
471
586
|
// src/version.ts
|
|
472
|
-
var SDK_VERSION = "0.1.
|
|
587
|
+
var SDK_VERSION = "0.1.13" ;
|
|
473
588
|
|
|
474
589
|
// src/daemon-binary.ts
|
|
475
590
|
function supported(platform, arch) {
|
|
@@ -727,6 +842,8 @@ function remote(baseUrl, opts = {}) {
|
|
|
727
842
|
// src/client.ts
|
|
728
843
|
var handleByTransport = /* @__PURE__ */ new WeakMap();
|
|
729
844
|
var Mindwire = class _Mindwire {
|
|
845
|
+
/** Workspace registry: saved agent profiles, projects and chat relationships. */
|
|
846
|
+
workspace;
|
|
730
847
|
http;
|
|
731
848
|
/** The default agent type applied to agent-scoped calls, if set. */
|
|
732
849
|
defaultAgent;
|
|
@@ -760,6 +877,7 @@ var Mindwire = class _Mindwire {
|
|
|
760
877
|
}
|
|
761
878
|
});
|
|
762
879
|
this.defaultAgent = opts.agent;
|
|
880
|
+
this.workspace = new WorkspaceApi(this);
|
|
763
881
|
this.auth = new AuthApi(this);
|
|
764
882
|
this.prompts = new PromptsApi(this);
|
|
765
883
|
this.mcp = new McpApi(this);
|
|
@@ -780,6 +898,7 @@ var Mindwire = class _Mindwire {
|
|
|
780
898
|
const clone = Object.create(_Mindwire.prototype);
|
|
781
899
|
clone.http = this.http;
|
|
782
900
|
clone.defaultAgent = agent;
|
|
901
|
+
clone.workspace = new WorkspaceApi(clone);
|
|
783
902
|
clone.auth = new AuthApi(clone);
|
|
784
903
|
clone.prompts = new PromptsApi(clone);
|
|
785
904
|
clone.mcp = new McpApi(clone);
|
|
@@ -1302,17 +1421,24 @@ function makeEmit(cfg) {
|
|
|
1302
1421
|
}
|
|
1303
1422
|
};
|
|
1304
1423
|
}
|
|
1305
|
-
|
|
1306
|
-
|
|
1307
|
-
|
|
1308
|
-
|
|
1309
|
-
|
|
1424
|
+
async function daemonDirectory(host) {
|
|
1425
|
+
const result = await host.exec(["sh", "-lc", 'printf "<<MW_HOME>>%s<<MW_HOME>>" "$HOME"'], { timeoutSeconds: 15 });
|
|
1426
|
+
const runtimeHome = result.stdout?.match(/<<MW_HOME>>([\s\S]*?)<<MW_HOME>>/)?.[1];
|
|
1427
|
+
if (!runtimeHome?.startsWith("/") || runtimeHome.length > 4096 || /[\x00-\x1f\x7f]/.test(runtimeHome)) {
|
|
1428
|
+
throw new MindwireError("mindwire: cannot resolve the runtime user's home directory");
|
|
1429
|
+
}
|
|
1430
|
+
return runtimeHome.replace(/\/+$/, "") + "/.mindwire";
|
|
1431
|
+
}
|
|
1310
1432
|
async function ensureDaemon(host, cfg) {
|
|
1311
1433
|
const emit2 = makeEmit(cfg);
|
|
1312
|
-
|
|
1434
|
+
let token = cfg.token ?? (await import('crypto')).randomBytes(32).toString("hex");
|
|
1313
1435
|
try {
|
|
1314
1436
|
await waitHostReady(host);
|
|
1315
1437
|
emit2({ phase: "connect", message: "runtime ready" });
|
|
1438
|
+
const directory = await daemonDirectory(host);
|
|
1439
|
+
if (!cfg.token) {
|
|
1440
|
+
token = await readWorkspaceToken(host, directory) ?? token;
|
|
1441
|
+
}
|
|
1316
1442
|
const desired = cfg.desiredVersion ?? SDK_VERSION;
|
|
1317
1443
|
const health = await probeHealth(host, cfg.port, token);
|
|
1318
1444
|
if (health.reachable) {
|
|
@@ -1333,22 +1459,35 @@ async function ensureDaemon(host, cfg) {
|
|
|
1333
1459
|
} else {
|
|
1334
1460
|
emit2({ phase: "probe", message: "no daemon reachable; deploying" });
|
|
1335
1461
|
}
|
|
1336
|
-
await deploy(host, cfg, emit2, token);
|
|
1462
|
+
await deploy(host, cfg, emit2, token, directory);
|
|
1463
|
+
if (!cfg.token) token = await readWorkspaceToken(host, directory) ?? token;
|
|
1337
1464
|
return token;
|
|
1338
1465
|
} catch (err) {
|
|
1339
1466
|
emit2({ phase: "error", message: "ensure failed", error: err instanceof Error ? err.message : String(err) });
|
|
1340
1467
|
throw err;
|
|
1341
1468
|
}
|
|
1342
1469
|
}
|
|
1343
|
-
async function
|
|
1470
|
+
async function readWorkspaceToken(host, directory) {
|
|
1471
|
+
const saved = await host.exec(["sh", "-lc", `cat ${shellQuote(directory + "/daemon.token")} 2>/dev/null || true`], { timeoutSeconds: 15 });
|
|
1472
|
+
const candidate = saved.stdout?.trim();
|
|
1473
|
+
return candidate && candidate.length <= 4096 && /^[\x21-\x7e]+$/.test(candidate) ? candidate : void 0;
|
|
1474
|
+
}
|
|
1475
|
+
async function deploy(host, cfg, emit2, token, directory) {
|
|
1476
|
+
const newPath = directory + "/mindwired.new";
|
|
1477
|
+
const BIN = shellQuote(directory + "/mindwired"), BIN_NEW = shellQuote(newPath);
|
|
1478
|
+
const STATE = shellQuote(directory + "/agent-state.json"), LOG = shellQuote(directory + "/daemon.log");
|
|
1479
|
+
const TOKEN = shellQuote(directory + "/daemon.token");
|
|
1344
1480
|
const arch = await probeArch(host);
|
|
1345
1481
|
const desired = cfg.desiredVersion ?? SDK_VERSION;
|
|
1346
1482
|
let acquire = "";
|
|
1483
|
+
let stagedUpload;
|
|
1347
1484
|
if (cfg.daemonBin) {
|
|
1348
1485
|
const binPath = await resolveLinuxDaemon(cfg.daemonBin, arch);
|
|
1349
1486
|
const bytes = await readBytes(binPath);
|
|
1350
1487
|
emit2({ phase: "upload", message: `uploading daemon (${arch}, ${formatMiB(bytes.length)})`, arch, bytes: bytes.length });
|
|
1351
|
-
await
|
|
1488
|
+
stagedUpload = `${newPath}-${(await import('crypto')).randomUUID()}`;
|
|
1489
|
+
await host.putFile(stagedUpload, bytes, { mode: "0755" });
|
|
1490
|
+
acquire = `mv -f ${shellQuote(stagedUpload)} ${BIN_NEW}`;
|
|
1352
1491
|
} else {
|
|
1353
1492
|
if (!/^[0-9]+\.[0-9]+\.[0-9]+(?:-[0-9A-Za-z.-]+)?$/.test(desired)) {
|
|
1354
1493
|
throw new MindwireError(`mindwire: cannot download daemon for non-release SDK version ${desired}`);
|
|
@@ -1379,25 +1518,46 @@ async function deploy(host, cfg, emit2, token) {
|
|
|
1379
1518
|
}
|
|
1380
1519
|
const script = [
|
|
1381
1520
|
"set -e",
|
|
1382
|
-
`mkdir -p ${
|
|
1521
|
+
`mkdir -p ${shellQuote(directory)}`,
|
|
1522
|
+
// iOS takes this same workspace lock. Unique staging also protects concurrent local uploads.
|
|
1523
|
+
stagedUpload ? `trap ${shellQuote(`rm -f ${shellQuote(stagedUpload)}`)} EXIT` : "",
|
|
1524
|
+
'command -v flock >/dev/null 2>&1 || { echo "MINDWIRE_FAIL flock is required to lock daemon updates"; exit 1; }',
|
|
1525
|
+
`exec 9>${shellQuote(directory + "/daemon-install.lock")}`,
|
|
1526
|
+
'flock -w 360 9 || { echo "MINDWIRE_FAIL another daemon update is still running"; exit 1; }',
|
|
1527
|
+
`mw_token=${shellQuote(token)}`,
|
|
1528
|
+
!cfg.token ? `if [ -s ${TOKEN} ]; then mw_token=$(cat ${TOKEN}); fi` : "",
|
|
1529
|
+
!cfg.forceDeploy ? [
|
|
1530
|
+
`mw_health=$(curl -fsS --max-time 3 -H "Authorization: Bearer $mw_token" http://127.0.0.1:${cfg.port}/healthz 2>/dev/null || true)`,
|
|
1531
|
+
`mw_version=$(printf '%s' "$mw_health" | sed -n 's/.*"version"[[:space:]]*:[[:space:]]*"\\([^" ]*\\)".*/\\1/p')`,
|
|
1532
|
+
`if [ -n "$mw_health" ] && ${cfg.autoUpdate ? `[ "$mw_version" = ${shellQuote(desired)} ]` : "true"}; then echo MINDWIRE_READY; exit 0; fi`
|
|
1533
|
+
].join("\n") : "",
|
|
1383
1534
|
acquire,
|
|
1384
1535
|
// Stop a prior daemon by exact process NAME, never `pkill -f <path>`: this whole script (which
|
|
1385
1536
|
// contains `${BIN}` several times) is the argv of the `bash -lc` shell running it, so a full-cmdline
|
|
1386
1537
|
// match would SIGTERM our own deploying shell before the daemon ever launches. `-x mindwired` matches
|
|
1387
1538
|
// only the daemon's comm (`bash`/`pkill` never match), leaving this shell alive.
|
|
1388
|
-
|
|
1539
|
+
"if command -v pkill >/dev/null 2>&1; then",
|
|
1540
|
+
" pkill -x mindwired 2>/dev/null || true",
|
|
1541
|
+
"else",
|
|
1542
|
+
" for mw_proc in /proc/[0-9]*/comm; do",
|
|
1543
|
+
' IFS= read -r mw_name 2>/dev/null < "$mw_proc" || continue',
|
|
1544
|
+
' [ "$mw_name" = mindwired ] || continue',
|
|
1545
|
+
" mw_pid=${mw_proc#/proc/}; mw_pid=${mw_pid%/comm}",
|
|
1546
|
+
' kill "$mw_pid" 2>/dev/null || true',
|
|
1547
|
+
" done",
|
|
1548
|
+
"fi",
|
|
1389
1549
|
"sleep 0.3",
|
|
1390
1550
|
`mv -f ${BIN_NEW} ${BIN}`,
|
|
1391
1551
|
`chmod +x ${BIN}`,
|
|
1392
1552
|
// Detach so the daemon survives this exec's shell exiting. ADDR=":<port>" binds 0.0.0.0.
|
|
1393
|
-
`setsid nohup env ADDR=":${cfg.port}" AGENT_TYPE
|
|
1553
|
+
`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>&- &`,
|
|
1394
1554
|
// Health-poll from inside the VM (loopback) and emit a marker — exit codes are unreliable here.
|
|
1395
|
-
`for i in $(seq 1 60); do curl -fsS --max-time 2 -H
|
|
1555
|
+
`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`,
|
|
1396
1556
|
"echo MINDWIRE_FAIL",
|
|
1397
1557
|
`tail -n 40 ${LOG} 2>/dev/null || true`
|
|
1398
1558
|
].join("\n");
|
|
1399
1559
|
emit2({ phase: "launch", message: "launching daemon" });
|
|
1400
|
-
const res = await host.exec(["bash", "-lc", script], { timeoutSeconds:
|
|
1560
|
+
const res = await host.exec(["bash", "-lc", script], { timeoutSeconds: 420 });
|
|
1401
1561
|
const out = res.stdout ?? "";
|
|
1402
1562
|
if (!out.includes("MINDWIRE_READY")) {
|
|
1403
1563
|
throw new MindwireError(
|
|
@@ -2311,12 +2471,15 @@ exports.McpApi = McpApi;
|
|
|
2311
2471
|
exports.Mindwire = Mindwire;
|
|
2312
2472
|
exports.MindwireError = MindwireError;
|
|
2313
2473
|
exports.NotifyApi = NotifyApi;
|
|
2474
|
+
exports.ProjectOperationsApi = ProjectOperationsApi;
|
|
2314
2475
|
exports.PromptsApi = PromptsApi;
|
|
2315
2476
|
exports.ProvidersApi = ProvidersApi;
|
|
2316
2477
|
exports.Run = Run;
|
|
2317
2478
|
exports.RunFailedError = RunFailedError;
|
|
2318
2479
|
exports.SDK_VERSION = SDK_VERSION;
|
|
2319
2480
|
exports.TimeoutError = TimeoutError;
|
|
2481
|
+
exports.WorkspaceApi = WorkspaceApi;
|
|
2482
|
+
exports.WorkspaceCollection = WorkspaceCollection;
|
|
2320
2483
|
exports.catalogModels = catalogModels;
|
|
2321
2484
|
exports.catalogProvider = catalogProvider;
|
|
2322
2485
|
exports.catalogProviders = catalogProviders;
|