@akshar5/cohall 0.6.0 → 0.6.2
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/CHANGELOG.md +25 -0
- package/README.md +30 -4
- package/bin/cohall.js +276 -135
- package/bin/cohall.js.map +11 -11
- package/deploy/windows/install-device.ps1 +21 -2
- package/docs/install.md +12 -1
- package/docs/services.md +17 -4
- package/package.json +5 -3
package/bin/cohall.js
CHANGED
|
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
17
17
|
|
|
18
18
|
// packages/protocol/src/index.ts
|
|
19
19
|
import { Effect, Schema } from "effect";
|
|
20
|
-
var version = "0.6.
|
|
20
|
+
var version = "0.6.2", maxSocketPayloadBytes, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
|
|
21
21
|
expected: "an ISO-8601 UTC timestamp"
|
|
22
22
|
})).pipe(Schema.brand(name)), BotId, Bot, DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, OperationId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
|
|
23
23
|
const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
@@ -487,13 +487,15 @@ class Hub {
|
|
|
487
487
|
hasDevice(deviceId) {
|
|
488
488
|
return this.#devices.get(deviceId)?.readyState === WebSocket3.OPEN;
|
|
489
489
|
}
|
|
490
|
-
sendToDevice(deviceId, event) {
|
|
490
|
+
sendToDevice(deviceId, event, beforeSend) {
|
|
491
491
|
const socket = this.#devices.get(deviceId);
|
|
492
492
|
if (socket?.readyState !== WebSocket3.OPEN || !this.isAuthorized(socket)) {
|
|
493
493
|
return false;
|
|
494
494
|
}
|
|
495
|
+
const payload = JSON.stringify(event);
|
|
496
|
+
beforeSend?.();
|
|
495
497
|
try {
|
|
496
|
-
socket.send(
|
|
498
|
+
socket.send(payload);
|
|
497
499
|
return true;
|
|
498
500
|
} catch {
|
|
499
501
|
return false;
|
|
@@ -1067,7 +1069,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1067
1069
|
}
|
|
1068
1070
|
return yield* deviceFromRow(row);
|
|
1069
1071
|
});
|
|
1070
|
-
const createDelegation2 = Effect13.fn("RelayStore.createDelegation")(function* (input, targetDeviceId,
|
|
1072
|
+
const createDelegation2 = Effect13.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, boundDeviceId, providerSessionId) {
|
|
1073
|
+
const sourceDeviceId = yield* Effect13.try({
|
|
1074
|
+
try: () => boundDeviceId === undefined || db.query("SELECT id FROM devices WHERE id = ? AND forgotten_at IS NULL").get(boundDeviceId) === null ? undefined : boundDeviceId,
|
|
1075
|
+
catch: operationError("RelayStore.createDelegation.source")
|
|
1076
|
+
});
|
|
1071
1077
|
const timestamp = now();
|
|
1072
1078
|
const threadId = input.threadId ?? makeThreadId();
|
|
1073
1079
|
const task = Task.make({
|
|
@@ -1207,7 +1213,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1207
1213
|
const timestamp = now();
|
|
1208
1214
|
yield* Effect13.try({
|
|
1209
1215
|
try: () => db.transaction(() => {
|
|
1210
|
-
const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at =
|
|
1216
|
+
const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?,
|
|
1217
|
+
provider_session_id = CASE
|
|
1218
|
+
WHEN provider = 'grok-bot' THEN NULL
|
|
1219
|
+
WHEN started_at IS NULL THEN COALESCE((
|
|
1220
|
+
SELECT session_id FROM provider_sessions
|
|
1221
|
+
WHERE thread_id = tasks.thread_id AND device_id = tasks.target_device_id
|
|
1222
|
+
AND provider = tasks.provider
|
|
1223
|
+
), provider_session_id)
|
|
1224
|
+
ELSE provider_session_id END
|
|
1211
1225
|
WHERE id = ? AND status = 'queued' AND NOT EXISTS (
|
|
1212
1226
|
SELECT 1 FROM tasks active
|
|
1213
1227
|
WHERE active.target_device_id = ? AND active.id <> ?
|
|
@@ -1278,7 +1292,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1278
1292
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
1279
1293
|
return current;
|
|
1280
1294
|
}
|
|
1281
|
-
|
|
1295
|
+
const dispatched = current.provider === "grok-bot" && (yield* Effect13.try({
|
|
1296
|
+
try: () => db.query("SELECT 1 FROM tasks WHERE id = ? AND dispatched_at IS NOT NULL").get(taskId) !== null,
|
|
1297
|
+
catch: operationError("RelayStore.requestCancellation")
|
|
1298
|
+
}));
|
|
1299
|
+
if (current.provider === "grok-bot" && (current.status !== "queued" || current.startedAt !== undefined || dispatched)) {
|
|
1282
1300
|
return yield* new PersistenceError({
|
|
1283
1301
|
operation: "RelayStore.requestCancellation",
|
|
1284
1302
|
message: "Stop this bot in Grok Bot; its gateway cannot cancel an individual Cohall request"
|
|
@@ -1358,7 +1376,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1358
1376
|
created_at: timestamp,
|
|
1359
1377
|
expires_at: expiresAt,
|
|
1360
1378
|
last_seen_at: timestamp,
|
|
1361
|
-
bound_device_id:
|
|
1379
|
+
bound_device_id: deviceId ?? null,
|
|
1362
1380
|
revoked_at: null
|
|
1363
1381
|
};
|
|
1364
1382
|
db.query(`INSERT INTO auth_sessions (
|
|
@@ -1432,6 +1450,12 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1432
1450
|
threadContext: threadContext2,
|
|
1433
1451
|
pendingTasksFor,
|
|
1434
1452
|
assignTask,
|
|
1453
|
+
markTaskDispatched: (taskId) => Effect13.try({
|
|
1454
|
+
try: () => {
|
|
1455
|
+
db.query("UPDATE tasks SET dispatched_at = COALESCE(dispatched_at, ?) WHERE id = ? AND status = 'assigned'").run(now(), taskId);
|
|
1456
|
+
},
|
|
1457
|
+
catch: operationError("RelayStore.markTaskDispatched")
|
|
1458
|
+
}),
|
|
1435
1459
|
rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
|
|
1436
1460
|
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect13.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, { kind: "running", detail: "Target device accepted the task and started the provider" }))),
|
|
1437
1461
|
finishTask: (taskId, deviceId, result, providerSessionId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId),
|
|
@@ -1487,7 +1511,8 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1487
1511
|
source_device_id TEXT REFERENCES devices(id),
|
|
1488
1512
|
target_device_id TEXT NOT NULL REFERENCES devices(id), parent_task_id TEXT,
|
|
1489
1513
|
workspace TEXT, provider_session_id TEXT, result TEXT, error TEXT,
|
|
1490
|
-
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT
|
|
1514
|
+
created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT,
|
|
1515
|
+
dispatched_at TEXT
|
|
1491
1516
|
);
|
|
1492
1517
|
CREATE INDEX IF NOT EXISTS tasks_thread_created ON tasks(thread_id, created_at);
|
|
1493
1518
|
CREATE INDEX IF NOT EXISTS tasks_target_status ON tasks(target_device_id, status);
|
|
@@ -1535,6 +1560,13 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1535
1560
|
if (!taskColumns.some((column) => column.name === "bot_id")) {
|
|
1536
1561
|
db.exec("ALTER TABLE tasks ADD COLUMN bot_id TEXT");
|
|
1537
1562
|
}
|
|
1563
|
+
if (!taskColumns.some((column) => column.name === "dispatched_at")) {
|
|
1564
|
+
db.transaction(() => {
|
|
1565
|
+
db.exec("ALTER TABLE tasks ADD COLUMN dispatched_at TEXT");
|
|
1566
|
+
db.exec(`UPDATE tasks SET dispatched_at = COALESCE(started_at, updated_at)
|
|
1567
|
+
WHERE provider = 'grok-bot' AND status NOT IN ('completed', 'failed', 'cancelled')`);
|
|
1568
|
+
})();
|
|
1569
|
+
}
|
|
1538
1570
|
},
|
|
1539
1571
|
catch: operationError("RelayStore.migrate")
|
|
1540
1572
|
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect13.acquireRelease(Effect13.try({
|
|
@@ -1555,8 +1587,8 @@ var init_store = __esm(() => {
|
|
|
1555
1587
|
var exports_main = {};
|
|
1556
1588
|
__export(exports_main, {
|
|
1557
1589
|
runRelay: () => runRelay,
|
|
1558
|
-
|
|
1559
|
-
|
|
1590
|
+
resolveDelegation: () => resolveDelegation,
|
|
1591
|
+
relayListenOptions: () => relayListenOptions
|
|
1560
1592
|
});
|
|
1561
1593
|
import { Effect as Effect14, ManagedRuntime, Schema as Schema14 } from "effect";
|
|
1562
1594
|
import { createHash as createHash3, timingSafeEqual } from "node:crypto";
|
|
@@ -1665,10 +1697,40 @@ Content-Length: 0\r
|
|
|
1665
1697
|
const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
|
|
1666
1698
|
const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
|
|
1667
1699
|
return (device.status === "offline" ? 0 : 100) + (input.workspace !== undefined && device.workspaces.some((workspace) => input.workspace === workspace.path || input.workspace?.startsWith(`${workspace.path}/`)) ? 80 : 0) + (needsXcode && device.capabilities.some((capability) => capability.id === "xcode") ? 60 : 0) + (needsBrowser && device.capabilities.some((capability) => capability.id === "browser-session") ? 40 : 0) + (sourceDeviceId === device.id ? -5 : 0);
|
|
1668
|
-
},
|
|
1700
|
+
}, resolveDelegation = (input, sourceDeviceId) => Effect14.gen(function* () {
|
|
1669
1701
|
const store = yield* exports_store.Service;
|
|
1702
|
+
const ancestors = [];
|
|
1703
|
+
const seen = new Set;
|
|
1704
|
+
let nextParentId = input.parentTaskId;
|
|
1705
|
+
while (nextParentId !== undefined && !seen.has(nextParentId)) {
|
|
1706
|
+
const parentId = nextParentId;
|
|
1707
|
+
seen.add(parentId);
|
|
1708
|
+
const parent2 = yield* store.getTask(parentId).pipe(Effect14.catch((cause) => {
|
|
1709
|
+
const missing = cause.message === `Unknown task ${parentId}`;
|
|
1710
|
+
if (missing && ancestors.length > 0)
|
|
1711
|
+
return Effect14.succeed(undefined);
|
|
1712
|
+
return Effect14.fail(new RequestError({ status: missing ? 404 : 500, message: cause.message }));
|
|
1713
|
+
}));
|
|
1714
|
+
if (parent2 === undefined)
|
|
1715
|
+
break;
|
|
1716
|
+
ancestors.push(parent2);
|
|
1717
|
+
nextParentId = parent2.parentTaskId;
|
|
1718
|
+
}
|
|
1719
|
+
const parent = ancestors[0];
|
|
1720
|
+
if (parent !== undefined && input.threadId !== undefined && input.threadId !== parent.threadId) {
|
|
1721
|
+
return yield* new RequestError({
|
|
1722
|
+
status: 400,
|
|
1723
|
+
message: "A child task must use its parent's thread"
|
|
1724
|
+
});
|
|
1725
|
+
}
|
|
1726
|
+
const resolvedInput = parent === undefined ? input : { ...input, threadId: parent.threadId };
|
|
1670
1727
|
const devices = yield* store.listDevices().pipe(Effect14.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
|
|
1671
1728
|
const provider = input.provider ?? (input.botId === undefined ? "codex" : "grok-bot");
|
|
1729
|
+
const slot = taskSlot({
|
|
1730
|
+
provider,
|
|
1731
|
+
...input.botId === undefined ? {} : { botId: input.botId }
|
|
1732
|
+
});
|
|
1733
|
+
const blocked = new Set(ancestors.filter((task) => !isTerminalTask(task) && taskSlot(task) === slot).map((task) => task.targetDeviceId));
|
|
1672
1734
|
if (input.targetDeviceId !== undefined) {
|
|
1673
1735
|
const target = devices.find((device) => device.id === input.targetDeviceId);
|
|
1674
1736
|
if (target === undefined) {
|
|
@@ -1689,7 +1751,13 @@ Content-Length: 0\r
|
|
|
1689
1751
|
message: `${target.name} does not advertise bot ${input.botId}; refresh the bot list`
|
|
1690
1752
|
});
|
|
1691
1753
|
}
|
|
1692
|
-
|
|
1754
|
+
if (blocked.has(target.id)) {
|
|
1755
|
+
return yield* new RequestError({
|
|
1756
|
+
status: 409,
|
|
1757
|
+
message: `${target.name} cannot run this task while an unfinished parent occupies the same worker slot; select another device or bot`
|
|
1758
|
+
});
|
|
1759
|
+
}
|
|
1760
|
+
return { input: resolvedInput, targetDeviceId: target.id };
|
|
1693
1761
|
}
|
|
1694
1762
|
const candidates = devices.filter((device) => device.providers.includes(provider) && (input.botId === undefined || device.bots?.some((bot) => bot.id === input.botId)));
|
|
1695
1763
|
if (input.botId !== undefined && candidates.length > 1) {
|
|
@@ -1698,14 +1766,14 @@ Content-Length: 0\r
|
|
|
1698
1766
|
message: `Bot ${input.botId} is advertised by more than one device; select a device`
|
|
1699
1767
|
});
|
|
1700
1768
|
}
|
|
1701
|
-
const selected = candidates.sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
|
|
1769
|
+
const selected = candidates.filter((device) => !blocked.has(device.id)).sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
|
|
1702
1770
|
if (selected === undefined) {
|
|
1703
1771
|
return yield* new RequestError({
|
|
1704
1772
|
status: 409,
|
|
1705
|
-
message: input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
|
|
1773
|
+
message: candidates.length > 0 ? "No available delegation target avoids an unfinished parent's worker slot; select another device or bot" : input.botId === undefined ? `No Cohall device advertises the ${provider} provider` : `No Cohall device advertises bot ${input.botId}; refresh the bot list`
|
|
1706
1774
|
});
|
|
1707
1775
|
}
|
|
1708
|
-
return selected.id;
|
|
1776
|
+
return { input: resolvedInput, targetDeviceId: selected.id };
|
|
1709
1777
|
}), runRelay = async () => {
|
|
1710
1778
|
const configuration = await Effect14.runPromise(loadEnvironmentConfiguration);
|
|
1711
1779
|
process.umask(63);
|
|
@@ -1741,20 +1809,15 @@ Content-Length: 0\r
|
|
|
1741
1809
|
return authenticate(authorization.slice("Bearer ".length), "client");
|
|
1742
1810
|
};
|
|
1743
1811
|
const dispatch = async (task) => {
|
|
1744
|
-
const
|
|
1745
|
-
|
|
1746
|
-
return yield* store.assignTask(task.id);
|
|
1747
|
-
}));
|
|
1812
|
+
const store = await run2(exports_store.Service);
|
|
1813
|
+
const assigned = await Effect14.runPromise(store.assignTask(task.id));
|
|
1748
1814
|
if (assigned.status !== "assigned") {
|
|
1749
1815
|
return assigned;
|
|
1750
1816
|
}
|
|
1751
|
-
if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }))) {
|
|
1817
|
+
if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }), assigned.provider === "grok-bot" ? () => Effect14.runSync(store.markTaskDispatched(assigned.id)) : undefined)) {
|
|
1752
1818
|
return assigned;
|
|
1753
1819
|
}
|
|
1754
|
-
return
|
|
1755
|
-
const store = yield* exports_store.Service;
|
|
1756
|
-
return yield* store.rollbackAssignment(task.id);
|
|
1757
|
-
}));
|
|
1820
|
+
return Effect14.runPromise(store.rollbackAssignment(task.id));
|
|
1758
1821
|
};
|
|
1759
1822
|
const dispatchOperation = async (operation) => {
|
|
1760
1823
|
const assigned = await run2(Effect14.gen(function* () {
|
|
@@ -2009,8 +2072,7 @@ Content-Length: 0\r
|
|
|
2009
2072
|
return json(forgotten);
|
|
2010
2073
|
}
|
|
2011
2074
|
if (url.pathname === "/api/tasks" && request.method === "POST") {
|
|
2012
|
-
const input = yield* body(request, decodeCreateTaskInput);
|
|
2013
|
-
const target = yield* chooseDevice(input, sourceDeviceId);
|
|
2075
|
+
const { input, targetDeviceId: target } = yield* resolveDelegation(yield* body(request, decodeCreateTaskInput), sourceDeviceId);
|
|
2014
2076
|
const providerSessionId = input.threadId === undefined || input.provider === "grok-bot" ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
|
|
2015
2077
|
const task2 = yield* store.createDelegation(input, target, sourceDeviceId, providerSessionId);
|
|
2016
2078
|
return json(yield* Effect14.tryPromise(() => dispatch(task2)), 201);
|
|
@@ -2051,6 +2113,7 @@ Content-Length: 0\r
|
|
|
2051
2113
|
});
|
|
2052
2114
|
websocketServer.on("connection", (rawSocket) => {
|
|
2053
2115
|
const socket = rawSocket;
|
|
2116
|
+
socket.on("error", () => socket.terminate());
|
|
2054
2117
|
socket.data = {
|
|
2055
2118
|
processing: Promise.resolve(),
|
|
2056
2119
|
stageDeadline: undefined,
|
|
@@ -2127,7 +2190,11 @@ Content-Length: 0\r
|
|
|
2127
2190
|
}).then((response) => sendResponse(outgoing, response)).catch((cause) => sendResponse(outgoing, json({ error: cause instanceof Error ? cause.message : "Invalid request" }, cause instanceof Error && cause.message.includes("256 KiB") ? 413 : 400)));
|
|
2128
2191
|
});
|
|
2129
2192
|
server.on("upgrade", (request, socket, head) => {
|
|
2130
|
-
const url =
|
|
2193
|
+
const url = URL.parse(request.url ?? "/", "http://cohall.local");
|
|
2194
|
+
if (url === null) {
|
|
2195
|
+
rejectUpgrade(socket, 400, "Bad Request");
|
|
2196
|
+
return;
|
|
2197
|
+
}
|
|
2131
2198
|
if (url.pathname !== "/ws/device") {
|
|
2132
2199
|
rejectUpgrade(socket, 404, "Not Found");
|
|
2133
2200
|
return;
|
|
@@ -2343,11 +2410,12 @@ init_src();
|
|
|
2343
2410
|
// packages/providers/src/index.ts
|
|
2344
2411
|
init_src();
|
|
2345
2412
|
import { Effect as Effect3, Schema as Schema3 } from "effect";
|
|
2413
|
+
import { execa } from "execa";
|
|
2346
2414
|
import { accessSync, constants } from "node:fs";
|
|
2347
2415
|
import { chmod, mkdtemp, rm, writeFile } from "node:fs/promises";
|
|
2348
2416
|
import { homedir, platform, tmpdir } from "node:os";
|
|
2349
2417
|
import { delimiter, extname, isAbsolute, join } from "node:path";
|
|
2350
|
-
import { execFile
|
|
2418
|
+
import { execFile } from "node:child_process";
|
|
2351
2419
|
|
|
2352
2420
|
class ProviderUnavailableError extends Schema3.TaggedErrorClass()("CohallProvider.Unavailable", { provider: Provider, message: Schema3.String }) {
|
|
2353
2421
|
}
|
|
@@ -2364,7 +2432,10 @@ var executables = {
|
|
|
2364
2432
|
opencode: "opencode"
|
|
2365
2433
|
};
|
|
2366
2434
|
var providerEnvironment = () => ({
|
|
2367
|
-
...Object.fromEntries(Object.entries(process.env).filter(([name]) =>
|
|
2435
|
+
...Object.fromEntries(Object.entries(process.env).filter(([name]) => {
|
|
2436
|
+
const key = platform() === "win32" ? name.toUpperCase() : name;
|
|
2437
|
+
return !key.startsWith("COHALL_") || key === "COHALL_CONFIG";
|
|
2438
|
+
})),
|
|
2368
2439
|
PATH: executableDirectories().join(delimiter)
|
|
2369
2440
|
});
|
|
2370
2441
|
var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined;
|
|
@@ -2708,8 +2779,8 @@ var terminateProcessTree = async (pid) => {
|
|
|
2708
2779
|
process.kill(-pid, "SIGKILL");
|
|
2709
2780
|
await waitForProcessGroup(pid, 1000);
|
|
2710
2781
|
};
|
|
2711
|
-
var run = (options) => Effect3.
|
|
2712
|
-
|
|
2782
|
+
var run = (options) => Effect3.callback((resume, signal) => {
|
|
2783
|
+
const running = (async () => {
|
|
2713
2784
|
const executable = findExecutable(executables[options.provider]);
|
|
2714
2785
|
if (executable === undefined) {
|
|
2715
2786
|
throw new ProviderUnavailableError({
|
|
@@ -2720,8 +2791,9 @@ var run = (options) => Effect3.tryPromise({
|
|
|
2720
2791
|
const prepared = await prepareCommand(options);
|
|
2721
2792
|
try {
|
|
2722
2793
|
await options.beforeSpawn?.();
|
|
2794
|
+
signal.throwIfAborted();
|
|
2723
2795
|
const [, ...arguments_] = prepared.command;
|
|
2724
|
-
const child =
|
|
2796
|
+
const child = execa(executable, arguments_, {
|
|
2725
2797
|
cwd: options.cwd,
|
|
2726
2798
|
env: {
|
|
2727
2799
|
...providerEnvironment(),
|
|
@@ -2729,12 +2801,22 @@ var run = (options) => Effect3.tryPromise({
|
|
|
2729
2801
|
COHALL_THREAD_ID: options.threadId,
|
|
2730
2802
|
...options.taskId === undefined ? {} : { COHALL_TASK_ID: options.taskId }
|
|
2731
2803
|
},
|
|
2804
|
+
extendEnv: false,
|
|
2732
2805
|
detached: platform() !== "win32",
|
|
2733
|
-
|
|
2806
|
+
stdin: "pipe",
|
|
2807
|
+
stdout: "pipe",
|
|
2808
|
+
stderr: "pipe",
|
|
2809
|
+
buffer: false,
|
|
2810
|
+
reject: false,
|
|
2811
|
+
cleanup: false
|
|
2734
2812
|
});
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2813
|
+
let finished = false;
|
|
2814
|
+
const exited = child.then(({ exitCode, failed, originalMessage, shortMessage }) => {
|
|
2815
|
+
finished = true;
|
|
2816
|
+
if (failed && exitCode === undefined) {
|
|
2817
|
+
throw new Error(originalMessage ?? shortMessage ?? "Provider failed to start");
|
|
2818
|
+
}
|
|
2819
|
+
return exitCode ?? 1;
|
|
2738
2820
|
});
|
|
2739
2821
|
let termination;
|
|
2740
2822
|
const terminate = () => {
|
|
@@ -2749,12 +2831,16 @@ var run = (options) => Effect3.tryPromise({
|
|
|
2749
2831
|
if (signal.aborted) {
|
|
2750
2832
|
terminate();
|
|
2751
2833
|
}
|
|
2752
|
-
child.stdin.end(prepared.input);
|
|
2753
2834
|
try {
|
|
2835
|
+
const written = new Promise((resolve, reject) => {
|
|
2836
|
+
child.stdin.once("error", reject);
|
|
2837
|
+
child.stdin.end(prepared.input, (cause) => cause === undefined || cause === null ? resolve() : reject(cause));
|
|
2838
|
+
});
|
|
2754
2839
|
const [result, stderr, exitCode] = await Promise.all([
|
|
2755
2840
|
parse(options, child.stdout, terminate),
|
|
2756
2841
|
captureText(child.stderr, 64 * 1024),
|
|
2757
|
-
exited
|
|
2842
|
+
exited,
|
|
2843
|
+
written
|
|
2758
2844
|
]);
|
|
2759
2845
|
if (exitCode !== 0) {
|
|
2760
2846
|
throw new ProviderRunError({
|
|
@@ -2778,7 +2864,7 @@ var run = (options) => Effect3.tryPromise({
|
|
|
2778
2864
|
return result;
|
|
2779
2865
|
} finally {
|
|
2780
2866
|
signal.removeEventListener("abort", terminate);
|
|
2781
|
-
if (
|
|
2867
|
+
if (!finished) {
|
|
2782
2868
|
terminate();
|
|
2783
2869
|
await exited.catch(() => {
|
|
2784
2870
|
return;
|
|
@@ -2789,21 +2875,21 @@ var run = (options) => Effect3.tryPromise({
|
|
|
2789
2875
|
} finally {
|
|
2790
2876
|
await prepared.cleanup();
|
|
2791
2877
|
}
|
|
2792
|
-
}
|
|
2793
|
-
|
|
2794
|
-
|
|
2795
|
-
|
|
2796
|
-
|
|
2797
|
-
|
|
2798
|
-
|
|
2799
|
-
|
|
2800
|
-
|
|
2801
|
-
}
|
|
2878
|
+
})();
|
|
2879
|
+
running.then((result) => resume(Effect3.succeed(result)), (cause) => resume(Effect3.fail(cause instanceof ProviderUnavailableError || cause instanceof ProviderRunError ? cause : new ProviderRunError({
|
|
2880
|
+
provider: options.provider,
|
|
2881
|
+
message: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
|
|
2882
|
+
}))));
|
|
2883
|
+
return Effect3.promise(() => running.then(() => {
|
|
2884
|
+
return;
|
|
2885
|
+
}, () => {
|
|
2886
|
+
return;
|
|
2887
|
+
}));
|
|
2802
2888
|
});
|
|
2803
2889
|
|
|
2804
2890
|
// apps/device/src/cli.ts
|
|
2805
2891
|
import { Effect as Effect9, Schema as Schema10 } from "effect";
|
|
2806
|
-
import { access as access4, mkdir as mkdir6, readFile as readFile4, stat as
|
|
2892
|
+
import { access as access4, mkdir as mkdir6, readFile as readFile4, stat as stat4, writeFile as writeFile6 } from "node:fs/promises";
|
|
2807
2893
|
import { homedir as homedir4 } from "node:os";
|
|
2808
2894
|
import { dirname as dirname6, join as join7, resolve as resolve3 } from "node:path";
|
|
2809
2895
|
|
|
@@ -3013,7 +3099,7 @@ Return the diagnostic failure to the user rather than claiming remote work ran.
|
|
|
3013
3099
|
// apps/device/src/config.ts
|
|
3014
3100
|
init_src();
|
|
3015
3101
|
import { Effect as Effect4, Schema as Schema4 } from "effect";
|
|
3016
|
-
import { access, chmod as chmod2, mkdir, readFile, realpath, rename, writeFile as writeFile2 } from "node:fs/promises";
|
|
3102
|
+
import { access, chmod as chmod2, mkdir, readFile, realpath, rename, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
3017
3103
|
import { homedir as homedir2, hostname, platform as platform2 } from "node:os";
|
|
3018
3104
|
import { dirname, join as join2, resolve } from "node:path";
|
|
3019
3105
|
var WorkspaceList = Schema4.Array(Schema4.NonEmptyString).check(Schema4.isMaxLength(64));
|
|
@@ -3086,10 +3172,16 @@ var normalizeRelayUrl = (input) => {
|
|
|
3086
3172
|
var parseWorkspaces = (workspaceList, workspaceJson) => Effect4.tryPromise({
|
|
3087
3173
|
try: async () => {
|
|
3088
3174
|
const values = workspaceJson === undefined ? workspaceList.split(",").map((workspace) => workspace.trim()).filter((workspace) => workspace.length > 0) : Schema4.decodeUnknownSync(WorkspaceList)(JSON.parse(workspaceJson));
|
|
3089
|
-
const canonical = await Promise.all(values.map((workspace) =>
|
|
3175
|
+
const canonical = await Promise.all(values.map(async (workspace) => {
|
|
3176
|
+
const path = await realpath(resolve(workspace));
|
|
3177
|
+
if (!(await stat(path)).isDirectory()) {
|
|
3178
|
+
throw new Error(`Not a directory: ${workspace}`);
|
|
3179
|
+
}
|
|
3180
|
+
return path;
|
|
3181
|
+
}));
|
|
3090
3182
|
return [...new Set(canonical)];
|
|
3091
3183
|
},
|
|
3092
|
-
catch: (cause) => new Error(`Workspace roots must be existing
|
|
3184
|
+
catch: (cause) => new Error(`Workspace roots must be existing directories; JSON input must be an array of paths: ${cause instanceof Error ? cause.message : String(cause)}`)
|
|
3093
3185
|
});
|
|
3094
3186
|
var parseProviders = (input) => Schema4.decodeUnknownSync(ProviderList)([
|
|
3095
3187
|
...new Set(input.split(",").map((provider) => provider.trim()).filter(Boolean))
|
|
@@ -3785,15 +3877,10 @@ var discoverGrokBots = async (path) => {
|
|
|
3785
3877
|
...description === undefined ? {} : { description: description.slice(0, 512) }
|
|
3786
3878
|
}));
|
|
3787
3879
|
};
|
|
3788
|
-
var poll =
|
|
3789
|
-
|
|
3790
|
-
|
|
3791
|
-
|
|
3792
|
-
})), { signal });
|
|
3793
|
-
if (result === undefined)
|
|
3794
|
-
throw new Error("Grok Bot polling ended before a result");
|
|
3795
|
-
return result;
|
|
3796
|
-
};
|
|
3880
|
+
var poll = (pass, intervalMs = 1000) => Effect6.tryPromise({ try: pass, catch: (cause) => cause }).pipe(Effect6.repeat({
|
|
3881
|
+
schedule: Schedule2.spaced(intervalMs),
|
|
3882
|
+
until: (value) => value !== undefined
|
|
3883
|
+
}));
|
|
3797
3884
|
var timeoutMessage = "Timed out waiting for the bot to reply through Cohall; the bot may still be running.";
|
|
3798
3885
|
var requireTime = (deadline) => {
|
|
3799
3886
|
if (Date.now() >= deadline)
|
|
@@ -3827,22 +3914,41 @@ var runGrokBot = async (path, task, signal) => {
|
|
|
3827
3914
|
return finishReply(existing);
|
|
3828
3915
|
const { deadline, command, dispatched } = await prepareBotReply(task);
|
|
3829
3916
|
requireTime(deadline);
|
|
3830
|
-
const waitForReply = () => poll(async () => {
|
|
3831
|
-
const receipt = await readBotReply(task.id);
|
|
3832
|
-
if (receipt !== undefined)
|
|
3833
|
-
return finishReply(receipt);
|
|
3834
|
-
requireTime(deadline);
|
|
3835
|
-
return;
|
|
3836
|
-
}, signal);
|
|
3837
|
-
if (dispatched)
|
|
3838
|
-
return waitForReply();
|
|
3839
|
-
if (path === undefined)
|
|
3840
|
-
throw new Error("Configure this device's Grok Bot gateway discovery file first");
|
|
3841
3917
|
const agentId = task.botId;
|
|
3842
3918
|
if (agentId === undefined)
|
|
3843
3919
|
throw new Error("Select a Grok Bot for this task");
|
|
3920
|
+
const lookup = async (lookupSignal = signal) => {
|
|
3921
|
+
if (path === undefined)
|
|
3922
|
+
throw new Error("Configure this device's Grok Bot gateway discovery file first");
|
|
3923
|
+
return rpc(await readConnection(path), "promptAcceptanceStatus", { accountSlot: "host", agentId, clientNonce: task.id }, Acceptance, lookupSignal);
|
|
3924
|
+
};
|
|
3925
|
+
const waitForReply = (reconcile = false) => {
|
|
3926
|
+
const receipt = poll(async () => {
|
|
3927
|
+
const receipt2 = await readBotReply(task.id);
|
|
3928
|
+
if (receipt2 !== undefined)
|
|
3929
|
+
return finishReply(receipt2);
|
|
3930
|
+
requireTime(deadline);
|
|
3931
|
+
return;
|
|
3932
|
+
});
|
|
3933
|
+
if (!reconcile || path === undefined)
|
|
3934
|
+
return Effect6.runPromise(receipt, { signal });
|
|
3935
|
+
const acceptance2 = poll(async (lookupSignal) => {
|
|
3936
|
+
const current = await lookup(lookupSignal).catch(() => {
|
|
3937
|
+
return;
|
|
3938
|
+
});
|
|
3939
|
+
if (current?.outcome !== "found")
|
|
3940
|
+
return;
|
|
3941
|
+
if (current.record.status === "rejected")
|
|
3942
|
+
throw new Error("Grok Bot rejected this task; check its conversation");
|
|
3943
|
+
return current.record.status === "accepted" ? true : undefined;
|
|
3944
|
+
}, 5000).pipe(Effect6.andThen(Effect6.never));
|
|
3945
|
+
return Effect6.runPromise(Effect6.raceFirst(receipt, acceptance2), { signal });
|
|
3946
|
+
};
|
|
3947
|
+
if (dispatched)
|
|
3948
|
+
return waitForReply(true);
|
|
3949
|
+
if (path === undefined)
|
|
3950
|
+
throw new Error("Configure this device's Grok Bot gateway discovery file first");
|
|
3844
3951
|
const connection = await readConnection(path);
|
|
3845
|
-
const lookup = () => rpc(connection, "promptAcceptanceStatus", { accountSlot: "host", agentId, clientNonce: task.id }, Acceptance, signal);
|
|
3846
3952
|
let acceptance = await lookup();
|
|
3847
3953
|
if (acceptance.outcome === "unknown-durability")
|
|
3848
3954
|
throw new Error("Grok Bot cannot confirm whether this task was accepted; check its conversation before retrying");
|
|
@@ -3860,7 +3966,7 @@ var runGrokBot = async (path, task, signal) => {
|
|
|
3860
3966
|
if (acceptance.outcome === "not-found") {
|
|
3861
3967
|
requireTime(deadline);
|
|
3862
3968
|
if (!await claimBotDispatch(task.id)) {
|
|
3863
|
-
return waitForReply();
|
|
3969
|
+
return waitForReply(true);
|
|
3864
3970
|
}
|
|
3865
3971
|
try {
|
|
3866
3972
|
await rpc(connection, "sendPrompt", {
|
|
@@ -3875,20 +3981,16 @@ var runGrokBot = async (path, task, signal) => {
|
|
|
3875
3981
|
const receipt2 = await readBotReply(task.id);
|
|
3876
3982
|
if (receipt2 !== undefined)
|
|
3877
3983
|
return finishReply(receipt2);
|
|
3878
|
-
|
|
3879
|
-
return;
|
|
3880
|
-
});
|
|
3881
|
-
if (reconciled?.outcome === "found" && reconciled.record.status === "rejected")
|
|
3882
|
-
throw new Error("Grok Bot rejected this task; check its conversation");
|
|
3883
|
-
return waitForReply();
|
|
3984
|
+
return waitForReply(true);
|
|
3884
3985
|
}
|
|
3986
|
+
return waitForReply();
|
|
3885
3987
|
}
|
|
3886
3988
|
}
|
|
3887
3989
|
if (acceptance.outcome === "unknown-durability")
|
|
3888
3990
|
throw new Error("Grok Bot cannot confirm task acceptance; the prompt was not resent");
|
|
3889
3991
|
if (acceptance.outcome === "found" && acceptance.record.status === "rejected")
|
|
3890
3992
|
throw new Error("Grok Bot rejected this task; check its conversation");
|
|
3891
|
-
return waitForReply();
|
|
3993
|
+
return waitForReply(acceptance.outcome === "found" && acceptance.record.status === "pending");
|
|
3892
3994
|
};
|
|
3893
3995
|
|
|
3894
3996
|
// apps/device/src/setup.ts
|
|
@@ -4003,7 +4105,7 @@ import {
|
|
|
4003
4105
|
realpath as realpath4,
|
|
4004
4106
|
rename as rename3,
|
|
4005
4107
|
rm as rm3,
|
|
4006
|
-
stat as
|
|
4108
|
+
stat as stat3,
|
|
4007
4109
|
writeFile as writeFile5
|
|
4008
4110
|
} from "node:fs/promises";
|
|
4009
4111
|
import { basename, dirname as dirname5, join as join6, resolve as resolve2 } from "node:path";
|
|
@@ -4011,14 +4113,14 @@ import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
|
|
|
4011
4113
|
import { WebSocket } from "ws";
|
|
4012
4114
|
|
|
4013
4115
|
// apps/device/src/service.ts
|
|
4014
|
-
import { execFile as
|
|
4116
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
4015
4117
|
import { chmod as chmod5, mkdir as mkdir4, realpath as realpath3, writeFile as writeFile4 } from "node:fs/promises";
|
|
4016
4118
|
import { homedir as homedir3 } from "node:os";
|
|
4017
4119
|
import { dirname as dirname4, join as join5 } from "node:path";
|
|
4018
4120
|
|
|
4019
4121
|
// apps/device/src/upgrade.ts
|
|
4020
4122
|
import { Schema as Schema8 } from "effect";
|
|
4021
|
-
import {
|
|
4123
|
+
import { execa as execa2 } from "execa";
|
|
4022
4124
|
import { constants as constants2 } from "node:fs";
|
|
4023
4125
|
import {
|
|
4024
4126
|
access as access2,
|
|
@@ -4028,7 +4130,7 @@ import {
|
|
|
4028
4130
|
realpath as realpath2,
|
|
4029
4131
|
rename as rename2,
|
|
4030
4132
|
rm as rm2,
|
|
4031
|
-
stat,
|
|
4133
|
+
stat as stat2,
|
|
4032
4134
|
writeFile as writeFile3
|
|
4033
4135
|
} from "node:fs/promises";
|
|
4034
4136
|
import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join4, relative } from "node:path";
|
|
@@ -4116,33 +4218,20 @@ var linuxSystemExecutableRoots = [
|
|
|
4116
4218
|
];
|
|
4117
4219
|
var macosHomebrewRoots = ["/opt/homebrew", "/usr/local"];
|
|
4118
4220
|
var macosAdminGroup = 80;
|
|
4119
|
-
var
|
|
4221
|
+
var isTrustedSystemPath = (platform4, path) => platform4 === "linux" && (path === "/home" || linuxSystemExecutableRoots.some((root) => containsPath(root, path) || containsPath(path, root)));
|
|
4120
4222
|
var isTrustedGroupWritablePath = (options) => {
|
|
4121
4223
|
const writableRoot = options.writableRoot;
|
|
4122
4224
|
return options.platform === "darwin" && writableRoot !== undefined && options.uid !== undefined && options.ownerUid === options.uid && options.ownerGid === macosAdminGroup && macosHomebrewRoots.some((root) => containsPath(root, writableRoot)) && containsPath(writableRoot, options.canonical) && containsPath(writableRoot, options.path);
|
|
4123
4225
|
};
|
|
4124
|
-
var
|
|
4125
|
-
const
|
|
4126
|
-
let selected;
|
|
4127
|
-
for (const path of candidates) {
|
|
4128
|
-
const available2 = await access2(path, operatingSystem() === "win32" ? constants2.F_OK : constants2.X_OK).then(() => true).catch(() => false);
|
|
4129
|
-
if (available2) {
|
|
4130
|
-
selected = path;
|
|
4131
|
-
break;
|
|
4132
|
-
}
|
|
4133
|
-
}
|
|
4134
|
-
if (selected === undefined) {
|
|
4135
|
-
throw new Error(`Could not find ${command} on PATH`);
|
|
4136
|
-
}
|
|
4137
|
-
const canonical = await realpath2(selected);
|
|
4226
|
+
var validateExecutable = async (candidate, installationRoot) => {
|
|
4227
|
+
const canonical = await realpath2(candidate);
|
|
4138
4228
|
if (operatingSystem() === "win32") {
|
|
4139
4229
|
return canonical;
|
|
4140
4230
|
}
|
|
4141
4231
|
const uid = process.getuid?.();
|
|
4142
|
-
const writableRoot =
|
|
4143
|
-
const systemExecutable = isTrustedSystemExecutablePath(operatingSystem(), canonical);
|
|
4232
|
+
const writableRoot = installationRoot === undefined ? undefined : await realpath2(installationRoot);
|
|
4144
4233
|
for (let path = canonical;; path = dirname3(path)) {
|
|
4145
|
-
const metadata = await
|
|
4234
|
+
const metadata = await stat2(path);
|
|
4146
4235
|
const trustedGroupWritablePath = isTrustedGroupWritablePath({
|
|
4147
4236
|
platform: operatingSystem(),
|
|
4148
4237
|
canonical,
|
|
@@ -4155,7 +4244,7 @@ var trustedExecutable = async (command, options = {}) => {
|
|
|
4155
4244
|
if ((metadata.mode & 2) !== 0 || (metadata.mode & 16) !== 0 && !trustedGroupWritablePath) {
|
|
4156
4245
|
throw new Error(`Refusing executable beneath group- or world-writable path ${path}`);
|
|
4157
4246
|
}
|
|
4158
|
-
if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid && !
|
|
4247
|
+
if (uid !== undefined && metadata.uid !== 0 && metadata.uid !== uid && !isTrustedSystemPath(operatingSystem(), path)) {
|
|
4159
4248
|
throw new Error(`Refusing executable owned by another user at ${path}`);
|
|
4160
4249
|
}
|
|
4161
4250
|
const parent = dirname3(path);
|
|
@@ -4165,6 +4254,21 @@ var trustedExecutable = async (command, options = {}) => {
|
|
|
4165
4254
|
}
|
|
4166
4255
|
return canonical;
|
|
4167
4256
|
};
|
|
4257
|
+
var trustedExecutable = async (command, options = {}) => {
|
|
4258
|
+
const candidates = isAbsolute2(command) ? [command] : (process.env.PATH ?? "").split(delimiter2).filter(Boolean).flatMap((directory) => executableNames(command).map((name) => join4(directory, name)));
|
|
4259
|
+
let failure;
|
|
4260
|
+
for (const path of candidates) {
|
|
4261
|
+
const available2 = await access2(path, operatingSystem() === "win32" ? constants2.F_OK : constants2.X_OK).then(() => true).catch(() => false);
|
|
4262
|
+
if (!available2)
|
|
4263
|
+
continue;
|
|
4264
|
+
try {
|
|
4265
|
+
return await validateExecutable(path, options.writableRoot);
|
|
4266
|
+
} catch (cause) {
|
|
4267
|
+
failure ??= cause;
|
|
4268
|
+
}
|
|
4269
|
+
}
|
|
4270
|
+
throw failure ?? new Error(`Could not find ${command} on PATH`);
|
|
4271
|
+
};
|
|
4168
4272
|
var trustedServices = async (services, resolveExecutable) => {
|
|
4169
4273
|
const resolved = new Map;
|
|
4170
4274
|
const invocation = async (value) => {
|
|
@@ -4273,16 +4377,23 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
4273
4377
|
return [];
|
|
4274
4378
|
};
|
|
4275
4379
|
var defaultRunner = {
|
|
4276
|
-
run: (command, arguments_, timeoutMs = 300000) =>
|
|
4277
|
-
|
|
4278
|
-
|
|
4279
|
-
|
|
4280
|
-
|
|
4281
|
-
|
|
4282
|
-
|
|
4283
|
-
|
|
4380
|
+
run: async (command, arguments_, timeoutMs = 300000) => {
|
|
4381
|
+
const result = await execa2(command, arguments_, {
|
|
4382
|
+
encoding: "utf8",
|
|
4383
|
+
maxBuffer: 1024 * 1024,
|
|
4384
|
+
timeout: timeoutMs,
|
|
4385
|
+
windowsHide: true,
|
|
4386
|
+
stdin: "ignore",
|
|
4387
|
+
reject: false,
|
|
4388
|
+
stripFinalNewline: false
|
|
4284
4389
|
});
|
|
4285
|
-
|
|
4390
|
+
return {
|
|
4391
|
+
exitCode: result.failed ? result.exitCode || 1 : 0,
|
|
4392
|
+
stdout: result.stdout,
|
|
4393
|
+
stderr: result.stderr,
|
|
4394
|
+
...result.shortMessage === undefined ? {} : { error: result.shortMessage.slice(0, 16384) }
|
|
4395
|
+
};
|
|
4396
|
+
}
|
|
4286
4397
|
};
|
|
4287
4398
|
var checked = async (runner, invocation, timeoutMs) => {
|
|
4288
4399
|
const result = await runner.run(invocation.command, invocation.arguments, timeoutMs);
|
|
@@ -4382,6 +4493,7 @@ var restartServices = async (runner, services, statePath, state, preserveDeviceR
|
|
|
4382
4493
|
};
|
|
4383
4494
|
var reportedRestartServices = (receipt) => receipt.restartingService === undefined || receipt.restartedServices.includes(receipt.restartingService) ? receipt.restartedServices : [...receipt.restartedServices, receipt.restartingService];
|
|
4384
4495
|
var upgrade = async (options) => {
|
|
4496
|
+
const target = normalizeUpgradeTarget(options.target);
|
|
4385
4497
|
const runner = options.runner ?? defaultRunner;
|
|
4386
4498
|
const resolveExecutable = options.resolveExecutable ?? trustedExecutable;
|
|
4387
4499
|
const runtimePlatform = options.platform ?? process.platform;
|
|
@@ -4389,7 +4501,7 @@ var upgrade = async (options) => {
|
|
|
4389
4501
|
const candidates = await trustedServices(serviceCandidates(runtimePlatform, uid), resolveExecutable);
|
|
4390
4502
|
const statePath = options.statePath ?? restartReceiptPath();
|
|
4391
4503
|
const previous = await readReceipt(statePath);
|
|
4392
|
-
if (previous?.version === options.currentVersion) {
|
|
4504
|
+
if (previous?.version === options.currentVersion && (target === "latest" || target === previous.version)) {
|
|
4393
4505
|
const byId = new Map(candidates.map((service) => [service.id, service]));
|
|
4394
4506
|
const pending = previous.pendingServices.flatMap((id) => {
|
|
4395
4507
|
const service = byId.get(id);
|
|
@@ -4442,15 +4554,12 @@ var upgrade = async (options) => {
|
|
|
4442
4554
|
dry_run: false
|
|
4443
4555
|
};
|
|
4444
4556
|
}
|
|
4445
|
-
await rm2(statePath, { force: true });
|
|
4446
|
-
const target = normalizeUpgradeTarget(options.target);
|
|
4447
4557
|
const entrypoint = options.entrypoint ?? process.argv[1];
|
|
4448
4558
|
if (entrypoint === undefined) {
|
|
4449
4559
|
throw new Error("Could not resolve the Cohall executable path");
|
|
4450
4560
|
}
|
|
4451
4561
|
const canonicalEntrypoint = await realpath2(entrypoint);
|
|
4452
4562
|
const installation = packageInstallation(canonicalEntrypoint, entrypoint);
|
|
4453
|
-
const install = packageInstallCommand(installation, target);
|
|
4454
4563
|
const services = await activeServices(runner, candidates);
|
|
4455
4564
|
await assertServiceInstallations(runner, services, canonicalEntrypoint, entrypoint);
|
|
4456
4565
|
if (options.dryRun) {
|
|
@@ -4466,14 +4575,21 @@ var upgrade = async (options) => {
|
|
|
4466
4575
|
dry_run: true
|
|
4467
4576
|
};
|
|
4468
4577
|
}
|
|
4469
|
-
|
|
4470
|
-
|
|
4471
|
-
|
|
4578
|
+
let nextVersion = await installedVersion(entrypoint).catch(() => {
|
|
4579
|
+
return;
|
|
4580
|
+
});
|
|
4581
|
+
if (nextVersion === undefined || target !== nextVersion) {
|
|
4582
|
+
const install = packageInstallCommand(installation, target);
|
|
4583
|
+
const installExecutable = options.resolveExecutable === undefined ? await trustedExecutable(install.command, installation.prefix === undefined ? {} : { writableRoot: installation.prefix }) : await resolveExecutable(install.command);
|
|
4584
|
+
await checked(runner, { ...install, command: installExecutable });
|
|
4585
|
+
nextVersion = await installedVersion(entrypoint);
|
|
4586
|
+
}
|
|
4472
4587
|
if (target !== "latest" && nextVersion !== target) {
|
|
4473
4588
|
throw new Error(`Installed Cohall ${nextVersion}, expected ${target}`);
|
|
4474
4589
|
}
|
|
4475
4590
|
const upgraded = nextVersion !== options.currentVersion;
|
|
4476
4591
|
if (!options.restart || services.length === 0) {
|
|
4592
|
+
await rm2(statePath, { force: true });
|
|
4477
4593
|
return {
|
|
4478
4594
|
upgraded,
|
|
4479
4595
|
from_version: options.currentVersion,
|
|
@@ -4513,8 +4629,20 @@ var upgrade = async (options) => {
|
|
|
4513
4629
|
|
|
4514
4630
|
// apps/device/src/service.ts
|
|
4515
4631
|
var xml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4516
|
-
var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")
|
|
4632
|
+
var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("%", "%%").replaceAll(`
|
|
4633
|
+
`, "\\n").replaceAll("\r", "\\r")}"`;
|
|
4517
4634
|
var deviceServicePlan = (options) => {
|
|
4635
|
+
const servicePath = [
|
|
4636
|
+
dirname4(options.nodeExecutable),
|
|
4637
|
+
join5(options.home, ".local", "bin"),
|
|
4638
|
+
join5(options.home, ".npm-global", "bin"),
|
|
4639
|
+
join5(options.home, ".bun", "bin"),
|
|
4640
|
+
options.platform === "darwin" ? join5(options.home, "Library", "pnpm") : join5(options.home, ".local", "share", "pnpm"),
|
|
4641
|
+
...options.platform === "darwin" ? ["/opt/homebrew/bin"] : [],
|
|
4642
|
+
"/usr/local/bin",
|
|
4643
|
+
"/usr/bin",
|
|
4644
|
+
"/bin"
|
|
4645
|
+
].join(":");
|
|
4518
4646
|
if (options.platform === "linux") {
|
|
4519
4647
|
const path = join5(options.home, ".config", "systemd", "user", "cohall-device.service");
|
|
4520
4648
|
return {
|
|
@@ -4528,7 +4656,8 @@ Wants=network-online.target
|
|
|
4528
4656
|
|
|
4529
4657
|
[Service]
|
|
4530
4658
|
Type=simple
|
|
4531
|
-
Environment
|
|
4659
|
+
Environment=${systemdArgument(`PATH=${servicePath}`)}
|
|
4660
|
+
Environment=${systemdArgument(`COHALL_CONFIG=${options.configPath}`)}
|
|
4532
4661
|
ExecStart=${systemdArgument(options.entrypoint)} device
|
|
4533
4662
|
Restart=always
|
|
4534
4663
|
RestartSec=3
|
|
@@ -4542,7 +4671,8 @@ WantedBy=default.target
|
|
|
4542
4671
|
},
|
|
4543
4672
|
commands: [
|
|
4544
4673
|
{ command: "systemctl", arguments: ["--user", "daemon-reload"] },
|
|
4545
|
-
{ command: "systemctl", arguments: ["--user", "enable", "
|
|
4674
|
+
{ command: "systemctl", arguments: ["--user", "enable", "cohall-device.service"] },
|
|
4675
|
+
{ command: "systemctl", arguments: ["--user", "restart", "cohall-device.service"] }
|
|
4546
4676
|
],
|
|
4547
4677
|
note: "The device starts after login. Run loginctl enable-linger $USER if it must run before login."
|
|
4548
4678
|
};
|
|
@@ -4569,7 +4699,9 @@ WantedBy=default.target
|
|
|
4569
4699
|
<key>EnvironmentVariables</key>
|
|
4570
4700
|
<dict>
|
|
4571
4701
|
<key>PATH</key>
|
|
4572
|
-
<string
|
|
4702
|
+
<string>${xml(servicePath)}</string>
|
|
4703
|
+
<key>COHALL_CONFIG</key>
|
|
4704
|
+
<string>${xml(options.configPath)}</string>
|
|
4573
4705
|
</dict>
|
|
4574
4706
|
<key>RunAtLoad</key>
|
|
4575
4707
|
<true/>
|
|
@@ -4601,7 +4733,7 @@ var defaultRunner2 = {
|
|
|
4601
4733
|
run: async (command, arguments_, timeoutMs = 60000) => {
|
|
4602
4734
|
const executable = await trustedExecutable(command);
|
|
4603
4735
|
return new Promise((resolve2) => {
|
|
4604
|
-
|
|
4736
|
+
execFile2(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => resolve2({
|
|
4605
4737
|
exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
|
|
4606
4738
|
stdout,
|
|
4607
4739
|
stderr,
|
|
@@ -4629,7 +4761,13 @@ var installDeviceService = async (options = {}) => {
|
|
|
4629
4761
|
"-ExecutionPolicy",
|
|
4630
4762
|
"Bypass",
|
|
4631
4763
|
"-File",
|
|
4632
|
-
script
|
|
4764
|
+
script,
|
|
4765
|
+
"-NodeExecutable",
|
|
4766
|
+
process.execPath,
|
|
4767
|
+
"-Entrypoint",
|
|
4768
|
+
entrypoint,
|
|
4769
|
+
"-ConfigurationPath",
|
|
4770
|
+
configurationPath()
|
|
4633
4771
|
]);
|
|
4634
4772
|
return { installed: "scheduled-task:Cohall Device" };
|
|
4635
4773
|
}
|
|
@@ -4638,6 +4776,8 @@ var installDeviceService = async (options = {}) => {
|
|
|
4638
4776
|
platform: platform4,
|
|
4639
4777
|
entrypoint,
|
|
4640
4778
|
home: options.home ?? homedir3(),
|
|
4779
|
+
nodeExecutable: process.execPath,
|
|
4780
|
+
configPath: configurationPath(),
|
|
4641
4781
|
...uid === undefined ? {} : { uid }
|
|
4642
4782
|
});
|
|
4643
4783
|
await mkdir4(dirname4(plan.file.path), { recursive: true, mode: 448 });
|
|
@@ -4721,7 +4861,7 @@ var verifyBackupDestinationParent = async (path) => {
|
|
|
4721
4861
|
return;
|
|
4722
4862
|
}
|
|
4723
4863
|
for (let current = await realpath4(path);; current = dirname5(current)) {
|
|
4724
|
-
const metadata = await
|
|
4864
|
+
const metadata = await stat3(current);
|
|
4725
4865
|
const writableByOthers = (metadata.mode & 18) !== 0;
|
|
4726
4866
|
const sticky = (metadata.mode & 512) !== 0;
|
|
4727
4867
|
if (writableByOthers && !sticky) {
|
|
@@ -4742,7 +4882,7 @@ var ownerToken = async (dataDirectory) => {
|
|
|
4742
4882
|
if (!await exists(path)) {
|
|
4743
4883
|
throw new Error(`Relay owner token was not found at ${path}`);
|
|
4744
4884
|
}
|
|
4745
|
-
if ((await
|
|
4885
|
+
if ((await stat3(path)).size > 512) {
|
|
4746
4886
|
throw new Error("Relay owner token file exceeds 512 bytes");
|
|
4747
4887
|
}
|
|
4748
4888
|
return (await readFile3(path, "utf8")).trim();
|
|
@@ -5148,7 +5288,7 @@ var readInput = async (arguments_, name) => {
|
|
|
5148
5288
|
if (!await access4(path).then(() => true).catch(() => false)) {
|
|
5149
5289
|
throw new Error(`File does not exist: ${path}`);
|
|
5150
5290
|
}
|
|
5151
|
-
if ((await
|
|
5291
|
+
if ((await stat4(path)).size > byteLimit) {
|
|
5152
5292
|
throw new Error(`${name} file exceeds ${byteLimit / 1024} KiB`);
|
|
5153
5293
|
}
|
|
5154
5294
|
}
|
|
@@ -5183,7 +5323,7 @@ var pairingToken = async (arguments_) => {
|
|
|
5183
5323
|
if (path !== undefined && !await access4(path).then(() => true).catch(() => false)) {
|
|
5184
5324
|
throw new Error(`Pairing token file does not exist: ${path}`);
|
|
5185
5325
|
}
|
|
5186
|
-
if (path !== undefined && (await
|
|
5326
|
+
if (path !== undefined && (await stat4(path)).size > 512) {
|
|
5187
5327
|
throw new Error("Pairing token file exceeds 512 bytes");
|
|
5188
5328
|
}
|
|
5189
5329
|
const token = (path === undefined ? await readStdin(512, "Pairing token") : await readFile4(path, "utf8")).trim();
|
|
@@ -5681,6 +5821,7 @@ var runCli = async (command, raw) => {
|
|
|
5681
5821
|
"timeout",
|
|
5682
5822
|
"workspace"
|
|
5683
5823
|
]);
|
|
5824
|
+
const waitSeconds = timeout(arguments_);
|
|
5684
5825
|
const positionalTarget = command === "send" && arguments_.positionals[0]?.startsWith("@") ? arguments_.positionals[0] : undefined;
|
|
5685
5826
|
const positionals = positionalTarget === undefined ? arguments_.positionals : arguments_.positionals.slice(1);
|
|
5686
5827
|
if (positionalTarget !== undefined && option(arguments_, "target") !== undefined) {
|
|
@@ -5712,7 +5853,7 @@ var runCli = async (command, raw) => {
|
|
|
5712
5853
|
...workspace === undefined ? {} : { workspace },
|
|
5713
5854
|
...provider === undefined ? {} : { provider: Schema10.decodeUnknownSync(Provider)(provider) }
|
|
5714
5855
|
}));
|
|
5715
|
-
print(taskResult(arguments_.options.has("no-wait") ? task : await Effect9.runPromise(waitForTask(relay, task,
|
|
5856
|
+
print(taskResult(arguments_.options.has("no-wait") ? task : await Effect9.runPromise(waitForTask(relay, task, waitSeconds))));
|
|
5716
5857
|
return;
|
|
5717
5858
|
}
|
|
5718
5859
|
if (command === "status") {
|
|
@@ -5757,7 +5898,7 @@ var runCli = async (command, raw) => {
|
|
|
5757
5898
|
init_src();
|
|
5758
5899
|
import { Effect as Effect10, Schedule as Schedule3, Schema as Schema11 } from "effect";
|
|
5759
5900
|
import { constants as constants3 } from "node:fs";
|
|
5760
|
-
import { open as open3, realpath as realpath5, stat as
|
|
5901
|
+
import { open as open3, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
5761
5902
|
import { arch, hostname as hostname3, platform as platform4 } from "node:os";
|
|
5762
5903
|
import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
|
|
5763
5904
|
import { WebSocket as WebSocket2 } from "ws";
|
|
@@ -5862,7 +6003,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
|
|
|
5862
6003
|
}
|
|
5863
6004
|
if (operatingSystem2 !== "linux") {
|
|
5864
6005
|
const currentPath = await realpath5(path);
|
|
5865
|
-
const currentPathIdentity = await
|
|
6006
|
+
const currentPathIdentity = await stat5(currentPath);
|
|
5866
6007
|
if (currentPathIdentity.dev !== identity.dev || currentPathIdentity.ino !== identity.ino) {
|
|
5867
6008
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
5868
6009
|
}
|
|
@@ -6373,4 +6514,4 @@ await main().catch((cause) => {
|
|
|
6373
6514
|
process.exitCode = 1;
|
|
6374
6515
|
});
|
|
6375
6516
|
|
|
6376
|
-
//# debugId=
|
|
6517
|
+
//# debugId=B9C02E5590F539C264756E2164756E21
|