@akshar5/cohall 0.6.1 → 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 +17 -0
- package/README.md +25 -4
- package/bin/cohall.js +205 -81
- package/bin/cohall.js.map +10 -10
- package/deploy/windows/install-device.ps1 +21 -2
- package/docs/install.md +1 -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))
|
|
@@ -4013,7 +4105,7 @@ import {
|
|
|
4013
4105
|
realpath as realpath4,
|
|
4014
4106
|
rename as rename3,
|
|
4015
4107
|
rm as rm3,
|
|
4016
|
-
stat as
|
|
4108
|
+
stat as stat3,
|
|
4017
4109
|
writeFile as writeFile5
|
|
4018
4110
|
} from "node:fs/promises";
|
|
4019
4111
|
import { basename, dirname as dirname5, join as join6, resolve as resolve2 } from "node:path";
|
|
@@ -4021,14 +4113,14 @@ import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
|
|
|
4021
4113
|
import { WebSocket } from "ws";
|
|
4022
4114
|
|
|
4023
4115
|
// apps/device/src/service.ts
|
|
4024
|
-
import { execFile as
|
|
4116
|
+
import { execFile as execFile2 } from "node:child_process";
|
|
4025
4117
|
import { chmod as chmod5, mkdir as mkdir4, realpath as realpath3, writeFile as writeFile4 } from "node:fs/promises";
|
|
4026
4118
|
import { homedir as homedir3 } from "node:os";
|
|
4027
4119
|
import { dirname as dirname4, join as join5 } from "node:path";
|
|
4028
4120
|
|
|
4029
4121
|
// apps/device/src/upgrade.ts
|
|
4030
4122
|
import { Schema as Schema8 } from "effect";
|
|
4031
|
-
import {
|
|
4123
|
+
import { execa as execa2 } from "execa";
|
|
4032
4124
|
import { constants as constants2 } from "node:fs";
|
|
4033
4125
|
import {
|
|
4034
4126
|
access as access2,
|
|
@@ -4038,7 +4130,7 @@ import {
|
|
|
4038
4130
|
realpath as realpath2,
|
|
4039
4131
|
rename as rename2,
|
|
4040
4132
|
rm as rm2,
|
|
4041
|
-
stat,
|
|
4133
|
+
stat as stat2,
|
|
4042
4134
|
writeFile as writeFile3
|
|
4043
4135
|
} from "node:fs/promises";
|
|
4044
4136
|
import { delimiter as delimiter2, dirname as dirname3, extname as extname2, isAbsolute as isAbsolute2, join as join4, relative } from "node:path";
|
|
@@ -4139,7 +4231,7 @@ var validateExecutable = async (candidate, installationRoot) => {
|
|
|
4139
4231
|
const uid = process.getuid?.();
|
|
4140
4232
|
const writableRoot = installationRoot === undefined ? undefined : await realpath2(installationRoot);
|
|
4141
4233
|
for (let path = canonical;; path = dirname3(path)) {
|
|
4142
|
-
const metadata = await
|
|
4234
|
+
const metadata = await stat2(path);
|
|
4143
4235
|
const trustedGroupWritablePath = isTrustedGroupWritablePath({
|
|
4144
4236
|
platform: operatingSystem(),
|
|
4145
4237
|
canonical,
|
|
@@ -4285,16 +4377,23 @@ var serviceCandidates = (runtimePlatform, uid) => {
|
|
|
4285
4377
|
return [];
|
|
4286
4378
|
};
|
|
4287
4379
|
var defaultRunner = {
|
|
4288
|
-
run: (command, arguments_, timeoutMs = 300000) =>
|
|
4289
|
-
|
|
4290
|
-
|
|
4291
|
-
|
|
4292
|
-
|
|
4293
|
-
|
|
4294
|
-
|
|
4295
|
-
|
|
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
|
|
4296
4389
|
});
|
|
4297
|
-
|
|
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
|
+
}
|
|
4298
4397
|
};
|
|
4299
4398
|
var checked = async (runner, invocation, timeoutMs) => {
|
|
4300
4399
|
const result = await runner.run(invocation.command, invocation.arguments, timeoutMs);
|
|
@@ -4530,8 +4629,20 @@ var upgrade = async (options) => {
|
|
|
4530
4629
|
|
|
4531
4630
|
// apps/device/src/service.ts
|
|
4532
4631
|
var xml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
4533
|
-
var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")
|
|
4632
|
+
var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"").replaceAll("%", "%%").replaceAll(`
|
|
4633
|
+
`, "\\n").replaceAll("\r", "\\r")}"`;
|
|
4534
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(":");
|
|
4535
4646
|
if (options.platform === "linux") {
|
|
4536
4647
|
const path = join5(options.home, ".config", "systemd", "user", "cohall-device.service");
|
|
4537
4648
|
return {
|
|
@@ -4545,7 +4656,8 @@ Wants=network-online.target
|
|
|
4545
4656
|
|
|
4546
4657
|
[Service]
|
|
4547
4658
|
Type=simple
|
|
4548
|
-
Environment
|
|
4659
|
+
Environment=${systemdArgument(`PATH=${servicePath}`)}
|
|
4660
|
+
Environment=${systemdArgument(`COHALL_CONFIG=${options.configPath}`)}
|
|
4549
4661
|
ExecStart=${systemdArgument(options.entrypoint)} device
|
|
4550
4662
|
Restart=always
|
|
4551
4663
|
RestartSec=3
|
|
@@ -4559,7 +4671,8 @@ WantedBy=default.target
|
|
|
4559
4671
|
},
|
|
4560
4672
|
commands: [
|
|
4561
4673
|
{ command: "systemctl", arguments: ["--user", "daemon-reload"] },
|
|
4562
|
-
{ command: "systemctl", arguments: ["--user", "enable", "
|
|
4674
|
+
{ command: "systemctl", arguments: ["--user", "enable", "cohall-device.service"] },
|
|
4675
|
+
{ command: "systemctl", arguments: ["--user", "restart", "cohall-device.service"] }
|
|
4563
4676
|
],
|
|
4564
4677
|
note: "The device starts after login. Run loginctl enable-linger $USER if it must run before login."
|
|
4565
4678
|
};
|
|
@@ -4586,7 +4699,9 @@ WantedBy=default.target
|
|
|
4586
4699
|
<key>EnvironmentVariables</key>
|
|
4587
4700
|
<dict>
|
|
4588
4701
|
<key>PATH</key>
|
|
4589
|
-
<string
|
|
4702
|
+
<string>${xml(servicePath)}</string>
|
|
4703
|
+
<key>COHALL_CONFIG</key>
|
|
4704
|
+
<string>${xml(options.configPath)}</string>
|
|
4590
4705
|
</dict>
|
|
4591
4706
|
<key>RunAtLoad</key>
|
|
4592
4707
|
<true/>
|
|
@@ -4618,7 +4733,7 @@ var defaultRunner2 = {
|
|
|
4618
4733
|
run: async (command, arguments_, timeoutMs = 60000) => {
|
|
4619
4734
|
const executable = await trustedExecutable(command);
|
|
4620
4735
|
return new Promise((resolve2) => {
|
|
4621
|
-
|
|
4736
|
+
execFile2(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => resolve2({
|
|
4622
4737
|
exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
|
|
4623
4738
|
stdout,
|
|
4624
4739
|
stderr,
|
|
@@ -4646,7 +4761,13 @@ var installDeviceService = async (options = {}) => {
|
|
|
4646
4761
|
"-ExecutionPolicy",
|
|
4647
4762
|
"Bypass",
|
|
4648
4763
|
"-File",
|
|
4649
|
-
script
|
|
4764
|
+
script,
|
|
4765
|
+
"-NodeExecutable",
|
|
4766
|
+
process.execPath,
|
|
4767
|
+
"-Entrypoint",
|
|
4768
|
+
entrypoint,
|
|
4769
|
+
"-ConfigurationPath",
|
|
4770
|
+
configurationPath()
|
|
4650
4771
|
]);
|
|
4651
4772
|
return { installed: "scheduled-task:Cohall Device" };
|
|
4652
4773
|
}
|
|
@@ -4655,6 +4776,8 @@ var installDeviceService = async (options = {}) => {
|
|
|
4655
4776
|
platform: platform4,
|
|
4656
4777
|
entrypoint,
|
|
4657
4778
|
home: options.home ?? homedir3(),
|
|
4779
|
+
nodeExecutable: process.execPath,
|
|
4780
|
+
configPath: configurationPath(),
|
|
4658
4781
|
...uid === undefined ? {} : { uid }
|
|
4659
4782
|
});
|
|
4660
4783
|
await mkdir4(dirname4(plan.file.path), { recursive: true, mode: 448 });
|
|
@@ -4738,7 +4861,7 @@ var verifyBackupDestinationParent = async (path) => {
|
|
|
4738
4861
|
return;
|
|
4739
4862
|
}
|
|
4740
4863
|
for (let current = await realpath4(path);; current = dirname5(current)) {
|
|
4741
|
-
const metadata = await
|
|
4864
|
+
const metadata = await stat3(current);
|
|
4742
4865
|
const writableByOthers = (metadata.mode & 18) !== 0;
|
|
4743
4866
|
const sticky = (metadata.mode & 512) !== 0;
|
|
4744
4867
|
if (writableByOthers && !sticky) {
|
|
@@ -4759,7 +4882,7 @@ var ownerToken = async (dataDirectory) => {
|
|
|
4759
4882
|
if (!await exists(path)) {
|
|
4760
4883
|
throw new Error(`Relay owner token was not found at ${path}`);
|
|
4761
4884
|
}
|
|
4762
|
-
if ((await
|
|
4885
|
+
if ((await stat3(path)).size > 512) {
|
|
4763
4886
|
throw new Error("Relay owner token file exceeds 512 bytes");
|
|
4764
4887
|
}
|
|
4765
4888
|
return (await readFile3(path, "utf8")).trim();
|
|
@@ -5165,7 +5288,7 @@ var readInput = async (arguments_, name) => {
|
|
|
5165
5288
|
if (!await access4(path).then(() => true).catch(() => false)) {
|
|
5166
5289
|
throw new Error(`File does not exist: ${path}`);
|
|
5167
5290
|
}
|
|
5168
|
-
if ((await
|
|
5291
|
+
if ((await stat4(path)).size > byteLimit) {
|
|
5169
5292
|
throw new Error(`${name} file exceeds ${byteLimit / 1024} KiB`);
|
|
5170
5293
|
}
|
|
5171
5294
|
}
|
|
@@ -5200,7 +5323,7 @@ var pairingToken = async (arguments_) => {
|
|
|
5200
5323
|
if (path !== undefined && !await access4(path).then(() => true).catch(() => false)) {
|
|
5201
5324
|
throw new Error(`Pairing token file does not exist: ${path}`);
|
|
5202
5325
|
}
|
|
5203
|
-
if (path !== undefined && (await
|
|
5326
|
+
if (path !== undefined && (await stat4(path)).size > 512) {
|
|
5204
5327
|
throw new Error("Pairing token file exceeds 512 bytes");
|
|
5205
5328
|
}
|
|
5206
5329
|
const token = (path === undefined ? await readStdin(512, "Pairing token") : await readFile4(path, "utf8")).trim();
|
|
@@ -5698,6 +5821,7 @@ var runCli = async (command, raw) => {
|
|
|
5698
5821
|
"timeout",
|
|
5699
5822
|
"workspace"
|
|
5700
5823
|
]);
|
|
5824
|
+
const waitSeconds = timeout(arguments_);
|
|
5701
5825
|
const positionalTarget = command === "send" && arguments_.positionals[0]?.startsWith("@") ? arguments_.positionals[0] : undefined;
|
|
5702
5826
|
const positionals = positionalTarget === undefined ? arguments_.positionals : arguments_.positionals.slice(1);
|
|
5703
5827
|
if (positionalTarget !== undefined && option(arguments_, "target") !== undefined) {
|
|
@@ -5729,7 +5853,7 @@ var runCli = async (command, raw) => {
|
|
|
5729
5853
|
...workspace === undefined ? {} : { workspace },
|
|
5730
5854
|
...provider === undefined ? {} : { provider: Schema10.decodeUnknownSync(Provider)(provider) }
|
|
5731
5855
|
}));
|
|
5732
|
-
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))));
|
|
5733
5857
|
return;
|
|
5734
5858
|
}
|
|
5735
5859
|
if (command === "status") {
|
|
@@ -5774,7 +5898,7 @@ var runCli = async (command, raw) => {
|
|
|
5774
5898
|
init_src();
|
|
5775
5899
|
import { Effect as Effect10, Schedule as Schedule3, Schema as Schema11 } from "effect";
|
|
5776
5900
|
import { constants as constants3 } from "node:fs";
|
|
5777
|
-
import { open as open3, realpath as realpath5, stat as
|
|
5901
|
+
import { open as open3, realpath as realpath5, stat as stat5 } from "node:fs/promises";
|
|
5778
5902
|
import { arch, hostname as hostname3, platform as platform4 } from "node:os";
|
|
5779
5903
|
import { basename as basename2, isAbsolute as isAbsolute3, relative as relative2 } from "node:path";
|
|
5780
5904
|
import { WebSocket as WebSocket2 } from "ws";
|
|
@@ -5879,7 +6003,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
|
|
|
5879
6003
|
}
|
|
5880
6004
|
if (operatingSystem2 !== "linux") {
|
|
5881
6005
|
const currentPath = await realpath5(path);
|
|
5882
|
-
const currentPathIdentity = await
|
|
6006
|
+
const currentPathIdentity = await stat5(currentPath);
|
|
5883
6007
|
if (currentPathIdentity.dev !== identity.dev || currentPathIdentity.ino !== identity.ino) {
|
|
5884
6008
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
5885
6009
|
}
|
|
@@ -6390,4 +6514,4 @@ await main().catch((cause) => {
|
|
|
6390
6514
|
process.exitCode = 1;
|
|
6391
6515
|
});
|
|
6392
6516
|
|
|
6393
|
-
//# debugId=
|
|
6517
|
+
//# debugId=B9C02E5590F539C264756E2164756E21
|