@akshar5/cohall 0.4.10 → 0.5.1
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 +23 -0
- package/README.md +87 -9
- package/bin/cohall.js +1560 -264
- package/bin/cohall.js.map +15 -11
- package/docs/install.md +60 -7
- package/docs/services.md +88 -25
- package/package.json +1 -1
package/bin/cohall.js
CHANGED
|
@@ -17,16 +17,49 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
|
|
|
17
17
|
|
|
18
18
|
// packages/protocol/src/index.ts
|
|
19
19
|
import { Schema } from "effect";
|
|
20
|
-
var version = "0.
|
|
20
|
+
var version = "0.5.1", 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
|
-
})).pipe(Schema.brand(name)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device,
|
|
22
|
+
})).pipe(Schema.brand(name)), 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
|
+
const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
|
|
24
|
+
if (match === null) {
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
const core = match.slice(1, 4).map(Number);
|
|
28
|
+
if (core.length !== 3 || core.some((part) => !Number.isSafeInteger(part))) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
return [core[0] ?? 0, core[1] ?? 0, core[2] ?? 0, match[4] !== undefined];
|
|
32
|
+
}, supportsDeviceOperations = (candidate) => {
|
|
33
|
+
if (candidate === "0.0.0-development") {
|
|
34
|
+
return true;
|
|
35
|
+
}
|
|
36
|
+
const current = parsedVersion(candidate);
|
|
37
|
+
const minimum = parsedVersion(minimumDeviceOperationVersion);
|
|
38
|
+
if (current === undefined || minimum === undefined) {
|
|
39
|
+
return false;
|
|
40
|
+
}
|
|
41
|
+
for (let index = 0;index < 3; index += 1) {
|
|
42
|
+
if (current[index] !== minimum[index]) {
|
|
43
|
+
return (current[index] ?? 0) > (minimum[index] ?? 0);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
return !current[3];
|
|
47
|
+
}, assertDeviceOperationSupport = (devices) => {
|
|
48
|
+
const unsupported = devices.filter((device) => !supportsDeviceOperations(device.version));
|
|
49
|
+
if (unsupported.length === 0) {
|
|
50
|
+
return;
|
|
51
|
+
}
|
|
52
|
+
throw new Error(`All-device upgrades require Cohall ${minimumDeviceOperationVersion} or newer on every device. Upgrade individually first: ${unsupported.map((device) => `${device.name} (${device.version})`).join(", ")}`);
|
|
53
|
+
}, Thread, Message, Task, upgradeVersion, CreateUpgradeOperationsInput, DeviceOperation, TaskStatusCounts, ProviderUsage, DeviceUsage, UsageSummary, TaskTraceEvent, TaskTrace, ThreadContext, CreateTaskInput, SocketEvent, ErrorResponse, terminalTaskStatuses, isTerminalTask = (task) => terminalTaskStatuses.has(task.status), now = () => Timestamp.make(new Date().toISOString()), makeDeviceId = () => DeviceId.make(crypto.randomUUID()), makeThreadId = () => ThreadId.make(crypto.randomUUID()), makeMessageId = () => MessageId.make(crypto.randomUUID()), makeTaskId = () => TaskId.make(crypto.randomUUID()), makeAuthSessionId = () => AuthSessionId.make(crypto.randomUUID()), makeOperationId = () => OperationId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeCreateUpgradeOperationsInput, decodeDevice, decodeSocketEvent;
|
|
23
54
|
var init_src = __esm(() => {
|
|
24
55
|
maxSocketPayloadBytes = 1024 * 1024;
|
|
56
|
+
count = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
|
|
25
57
|
DeviceId = uuid("DeviceId");
|
|
26
58
|
ThreadId = uuid("ThreadId");
|
|
27
59
|
MessageId = uuid("MessageId");
|
|
28
60
|
TaskId = uuid("TaskId");
|
|
29
61
|
AuthSessionId = uuid("AuthSessionId");
|
|
62
|
+
OperationId = uuid("OperationId");
|
|
30
63
|
Timestamp = isoTimestamp("Timestamp");
|
|
31
64
|
Platform = Schema.Literals(["darwin", "linux", "windows", "unknown"]);
|
|
32
65
|
Provider = Schema.Literals(["codex", "claude-code", "opencode"]);
|
|
@@ -40,6 +73,13 @@ var init_src = __esm(() => {
|
|
|
40
73
|
"failed",
|
|
41
74
|
"cancelled"
|
|
42
75
|
]);
|
|
76
|
+
OperationStatus = Schema.Literals([
|
|
77
|
+
"queued",
|
|
78
|
+
"assigned",
|
|
79
|
+
"running",
|
|
80
|
+
"completed",
|
|
81
|
+
"failed"
|
|
82
|
+
]);
|
|
43
83
|
TaskTraceEventKind = Schema.Literals([...TaskStatus.literals, "requeued"]);
|
|
44
84
|
MessageRole = Schema.Literals(["human", "agent", "system"]);
|
|
45
85
|
ConnectionRole = Schema.Literals(["client", "device"]);
|
|
@@ -130,6 +170,52 @@ var init_src = __esm(() => {
|
|
|
130
170
|
startedAt: Schema.optionalKey(Timestamp),
|
|
131
171
|
completedAt: Schema.optionalKey(Timestamp)
|
|
132
172
|
});
|
|
173
|
+
upgradeVersion = Schema.String.check(Schema.isPattern(/^(?:latest|v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/, {
|
|
174
|
+
expected: "latest or an exact semantic version"
|
|
175
|
+
}));
|
|
176
|
+
CreateUpgradeOperationsInput = Schema.Struct({
|
|
177
|
+
target: upgradeVersion,
|
|
178
|
+
restart: Schema.Boolean
|
|
179
|
+
});
|
|
180
|
+
DeviceOperation = Schema.Struct({
|
|
181
|
+
id: OperationId,
|
|
182
|
+
kind: Schema.Literal("upgrade"),
|
|
183
|
+
status: OperationStatus,
|
|
184
|
+
targetDeviceId: DeviceId,
|
|
185
|
+
requestedVersion: upgradeVersion,
|
|
186
|
+
restart: Schema.Boolean,
|
|
187
|
+
result: Schema.optionalKey(optionalText(16384)),
|
|
188
|
+
error: Schema.optionalKey(optionalText(16384)),
|
|
189
|
+
createdAt: Timestamp,
|
|
190
|
+
updatedAt: Timestamp,
|
|
191
|
+
completedAt: Schema.optionalKey(Timestamp)
|
|
192
|
+
});
|
|
193
|
+
TaskStatusCounts = Schema.Struct({
|
|
194
|
+
queued: count,
|
|
195
|
+
assigned: count,
|
|
196
|
+
running: count,
|
|
197
|
+
cancelling: count,
|
|
198
|
+
completed: count,
|
|
199
|
+
failed: count,
|
|
200
|
+
cancelled: count
|
|
201
|
+
});
|
|
202
|
+
ProviderUsage = Schema.Struct({
|
|
203
|
+
provider: Provider,
|
|
204
|
+
tasks: count
|
|
205
|
+
});
|
|
206
|
+
DeviceUsage = Schema.Struct({
|
|
207
|
+
deviceId: DeviceId,
|
|
208
|
+
deviceName: bounded(128),
|
|
209
|
+
tasks: count,
|
|
210
|
+
byStatus: TaskStatusCounts,
|
|
211
|
+
byProvider: boundedArray(ProviderUsage, 3)
|
|
212
|
+
});
|
|
213
|
+
UsageSummary = Schema.Struct({
|
|
214
|
+
retainedTasks: count,
|
|
215
|
+
byStatus: TaskStatusCounts,
|
|
216
|
+
byProvider: boundedArray(ProviderUsage, 3),
|
|
217
|
+
devices: boundedArray(DeviceUsage, 256)
|
|
218
|
+
});
|
|
133
219
|
TaskTraceEvent = Schema.Struct({
|
|
134
220
|
kind: TaskTraceEventKind,
|
|
135
221
|
at: Timestamp,
|
|
@@ -184,6 +270,11 @@ var init_src = __esm(() => {
|
|
|
184
270
|
TaskCancelled: { taskId: TaskId },
|
|
185
271
|
TaskSettled: { taskId: TaskId },
|
|
186
272
|
CancelTask: { taskId: TaskId },
|
|
273
|
+
OperationAssigned: { operation: DeviceOperation },
|
|
274
|
+
OperationAccepted: { operationId: OperationId },
|
|
275
|
+
OperationFinished: { operationId: OperationId, result: optionalText(16384) },
|
|
276
|
+
OperationFailed: { operationId: OperationId, error: bounded(16384) },
|
|
277
|
+
OperationSettled: { operationId: OperationId },
|
|
187
278
|
Error: { code: bounded(64), message: bounded(2048) }
|
|
188
279
|
});
|
|
189
280
|
ErrorResponse = Schema.Struct({
|
|
@@ -197,32 +288,33 @@ var init_src = __esm(() => {
|
|
|
197
288
|
decodeCreatePairingInput = Schema.decodeUnknownEffect(CreatePairingInput);
|
|
198
289
|
decodeExchangePairingInput = Schema.decodeUnknownEffect(ExchangePairingInput);
|
|
199
290
|
decodeCreateTaskInput = Schema.decodeUnknownEffect(CreateTaskInput);
|
|
291
|
+
decodeCreateUpgradeOperationsInput = Schema.decodeUnknownEffect(CreateUpgradeOperationsInput);
|
|
200
292
|
decodeDevice = Schema.decodeUnknownEffect(Device);
|
|
201
293
|
decodeSocketEvent = Schema.decodeUnknownEffect(SocketEvent);
|
|
202
294
|
});
|
|
203
295
|
|
|
204
296
|
// apps/relay/src/config.ts
|
|
205
|
-
import { Effect as
|
|
206
|
-
import { access as
|
|
207
|
-
import { homedir as
|
|
208
|
-
import { join as
|
|
297
|
+
import { Effect as Effect10, Schema as Schema10 } from "effect";
|
|
298
|
+
import { access as access5, chmod as chmod6, mkdir as mkdir6, readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
|
|
299
|
+
import { homedir as homedir5, platform as platform4 } from "node:os";
|
|
300
|
+
import { join as join7, resolve as resolve3 } from "node:path";
|
|
209
301
|
var RelayConfiguration, defaultDataDirectory = () => {
|
|
210
302
|
if (platform4() === "win32") {
|
|
211
|
-
return
|
|
303
|
+
return join7(process.env.LOCALAPPDATA ?? join7(homedir5(), "AppData", "Local"), "Cohall");
|
|
212
304
|
}
|
|
213
305
|
if (platform4() === "darwin") {
|
|
214
|
-
return
|
|
306
|
+
return join7(homedir5(), "Library", "Application Support", "Cohall", "relay");
|
|
215
307
|
}
|
|
216
|
-
return
|
|
308
|
+
return join7(process.env.XDG_DATA_HOME ?? join7(homedir5(), ".local", "share"), "cohall");
|
|
217
309
|
}, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
|
|
218
|
-
if (await
|
|
219
|
-
return (await
|
|
310
|
+
if (await access5(path).then(() => true).catch(() => false)) {
|
|
311
|
+
return (await readFile5(path, "utf8")).trim();
|
|
220
312
|
}
|
|
221
313
|
const token = generatedToken();
|
|
222
|
-
await
|
|
314
|
+
await writeFile7(path, `${token}
|
|
223
315
|
`, { mode: 384 });
|
|
224
316
|
if (platform4() !== "win32") {
|
|
225
|
-
await
|
|
317
|
+
await chmod6(path, 384);
|
|
226
318
|
}
|
|
227
319
|
return token;
|
|
228
320
|
}, booleanEnvironment = (name) => {
|
|
@@ -234,41 +326,41 @@ var RelayConfiguration, defaultDataDirectory = () => {
|
|
|
234
326
|
return true;
|
|
235
327
|
}
|
|
236
328
|
throw new Error(`${name} must be true or false`);
|
|
237
|
-
},
|
|
329
|
+
}, isLoopback2 = (host) => host === "127.0.0.1" || host === "localhost" || host === "::1", loadEnvironmentConfiguration;
|
|
238
330
|
var init_config = __esm(() => {
|
|
239
|
-
RelayConfiguration =
|
|
240
|
-
host:
|
|
241
|
-
port:
|
|
242
|
-
token:
|
|
243
|
-
dataDirectory:
|
|
244
|
-
databasePath:
|
|
245
|
-
allowRemote:
|
|
246
|
-
historyTaskLimit:
|
|
331
|
+
RelayConfiguration = Schema10.Struct({
|
|
332
|
+
host: Schema10.NonEmptyString,
|
|
333
|
+
port: Schema10.Int.check(Schema10.isBetween({ minimum: 1, maximum: 65535 })),
|
|
334
|
+
token: Schema10.String.check(Schema10.isMinLength(32)),
|
|
335
|
+
dataDirectory: Schema10.NonEmptyString,
|
|
336
|
+
databasePath: Schema10.NonEmptyString,
|
|
337
|
+
allowRemote: Schema10.Boolean,
|
|
338
|
+
historyTaskLimit: Schema10.Int.check(Schema10.isBetween({ minimum: 100, maximum: 1e5 }))
|
|
247
339
|
});
|
|
248
|
-
loadEnvironmentConfiguration =
|
|
340
|
+
loadEnvironmentConfiguration = Effect10.tryPromise({
|
|
249
341
|
try: async () => {
|
|
250
342
|
const host = process.env.COHALL_RELAY_HOST ?? "127.0.0.1";
|
|
251
343
|
const port = Number(process.env.COHALL_RELAY_PORT ?? "8787");
|
|
252
344
|
const allowRemote = booleanEnvironment("COHALL_RELAY_ALLOW_REMOTE");
|
|
253
|
-
if (!
|
|
345
|
+
if (!isLoopback2(host) && !allowRemote) {
|
|
254
346
|
throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
|
|
255
347
|
}
|
|
256
|
-
const dataDirectory =
|
|
257
|
-
await
|
|
348
|
+
const dataDirectory = resolve3(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
|
|
349
|
+
await mkdir6(dataDirectory, { recursive: true, mode: 448 });
|
|
258
350
|
if (platform4() !== "win32") {
|
|
259
|
-
await
|
|
351
|
+
await chmod6(dataDirectory, 448);
|
|
260
352
|
}
|
|
261
|
-
const token = process.env.COHALL_TOKEN ?? await tokenFrom(
|
|
353
|
+
const token = process.env.COHALL_TOKEN ?? await tokenFrom(join7(dataDirectory, "owner-token"));
|
|
262
354
|
if (token.length < 32) {
|
|
263
355
|
throw new Error("The Cohall relay owner token must be at least 32 characters");
|
|
264
356
|
}
|
|
265
357
|
const historyTaskLimit = Number(process.env.COHALL_HISTORY_TASK_LIMIT ?? "1000");
|
|
266
|
-
return
|
|
358
|
+
return Schema10.decodeUnknownSync(RelayConfiguration)({
|
|
267
359
|
host,
|
|
268
360
|
port,
|
|
269
361
|
token,
|
|
270
362
|
dataDirectory,
|
|
271
|
-
databasePath:
|
|
363
|
+
databasePath: join7(dataDirectory, "cohall.db"),
|
|
272
364
|
allowRemote,
|
|
273
365
|
historyTaskLimit
|
|
274
366
|
});
|
|
@@ -278,7 +370,7 @@ var init_config = __esm(() => {
|
|
|
278
370
|
});
|
|
279
371
|
|
|
280
372
|
// apps/relay/src/hub.ts
|
|
281
|
-
import { WebSocket as
|
|
373
|
+
import { WebSocket as WebSocket3 } from "ws";
|
|
282
374
|
|
|
283
375
|
class Hub {
|
|
284
376
|
#principals = new Map;
|
|
@@ -294,7 +386,7 @@ class Hub {
|
|
|
294
386
|
this.#ownerSockets.add(socket);
|
|
295
387
|
} else {
|
|
296
388
|
const existing = this.#sessions.get(principal.sessionId);
|
|
297
|
-
if (existing !== undefined && existing.readyState ===
|
|
389
|
+
if (existing !== undefined && existing.readyState === WebSocket3.OPEN) {
|
|
298
390
|
return false;
|
|
299
391
|
}
|
|
300
392
|
this.#sessions.set(principal.sessionId, socket);
|
|
@@ -325,7 +417,7 @@ class Hub {
|
|
|
325
417
|
return false;
|
|
326
418
|
}
|
|
327
419
|
const previous = this.#devices.get(deviceId);
|
|
328
|
-
if (previous !== undefined && previous !== socket && previous.readyState ===
|
|
420
|
+
if (previous !== undefined && previous !== socket && previous.readyState === WebSocket3.OPEN) {
|
|
329
421
|
return false;
|
|
330
422
|
}
|
|
331
423
|
this.#devices.set(deviceId, socket);
|
|
@@ -371,11 +463,11 @@ class Hub {
|
|
|
371
463
|
this.#devices.get(deviceId)?.close(4003, "Device forgotten");
|
|
372
464
|
}
|
|
373
465
|
hasDevice(deviceId) {
|
|
374
|
-
return this.#devices.get(deviceId)?.readyState ===
|
|
466
|
+
return this.#devices.get(deviceId)?.readyState === WebSocket3.OPEN;
|
|
375
467
|
}
|
|
376
468
|
sendToDevice(deviceId, event) {
|
|
377
469
|
const socket = this.#devices.get(deviceId);
|
|
378
|
-
if (socket?.readyState !==
|
|
470
|
+
if (socket?.readyState !== WebSocket3.OPEN || !this.isAuthorized(socket)) {
|
|
379
471
|
return false;
|
|
380
472
|
}
|
|
381
473
|
try {
|
|
@@ -389,7 +481,7 @@ class Hub {
|
|
|
389
481
|
var init_hub = () => {};
|
|
390
482
|
|
|
391
483
|
// apps/relay/src/database.ts
|
|
392
|
-
import { DatabaseSync } from "node:sqlite";
|
|
484
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
393
485
|
|
|
394
486
|
class Statement {
|
|
395
487
|
#statement;
|
|
@@ -412,7 +504,7 @@ class Statement {
|
|
|
412
504
|
class Database {
|
|
413
505
|
#database;
|
|
414
506
|
constructor(path) {
|
|
415
|
-
this.#database = new
|
|
507
|
+
this.#database = new DatabaseSync2(path);
|
|
416
508
|
}
|
|
417
509
|
query(sql) {
|
|
418
510
|
return new Statement(this.#database.prepare(sql));
|
|
@@ -448,12 +540,12 @@ __export(exports_store, {
|
|
|
448
540
|
RelayStore: () => exports_store,
|
|
449
541
|
PersistenceError: () => PersistenceError
|
|
450
542
|
});
|
|
451
|
-
import { Context as Context2, Effect as
|
|
452
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
543
|
+
import { Context as Context2, Effect as Effect11, Layer as Layer2, Schema as Schema11 } from "effect";
|
|
544
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
453
545
|
var PersistenceError, Service2, operationError = (operation) => (cause) => new PersistenceError({
|
|
454
546
|
operation,
|
|
455
547
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
456
|
-
}), decode = (operation, schema, value) =>
|
|
548
|
+
}), decode = (operation, schema, value) => Schema11.decodeUnknownEffect(schema)(value).pipe(Effect11.mapError(operationError(operation))), threadFromRow = (row) => decode("RelayStore.decodeThread", Thread, {
|
|
457
549
|
id: row.id,
|
|
458
550
|
title: row.title,
|
|
459
551
|
createdAt: row.created_at,
|
|
@@ -499,14 +591,34 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
499
591
|
...row.error === null ? {} : { error: row.error },
|
|
500
592
|
...row.started_at === null ? {} : { startedAt: row.started_at },
|
|
501
593
|
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
594
|
+
}), operationFromRow = (row) => decode("RelayStore.decodeOperation", DeviceOperation, {
|
|
595
|
+
id: row.id,
|
|
596
|
+
kind: row.kind,
|
|
597
|
+
status: row.status,
|
|
598
|
+
targetDeviceId: row.target_device_id,
|
|
599
|
+
requestedVersion: row.requested_version,
|
|
600
|
+
restart: row.restart === 1,
|
|
601
|
+
createdAt: row.created_at,
|
|
602
|
+
updatedAt: row.updated_at,
|
|
603
|
+
...row.result === null ? {} : { result: row.result },
|
|
604
|
+
...row.error === null ? {} : { error: row.error },
|
|
605
|
+
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
606
|
+
}), emptyStatusCounts = () => ({
|
|
607
|
+
queued: 0,
|
|
608
|
+
assigned: 0,
|
|
609
|
+
running: 0,
|
|
610
|
+
cancelling: 0,
|
|
611
|
+
completed: 0,
|
|
612
|
+
failed: 0,
|
|
613
|
+
cancelled: 0
|
|
502
614
|
}), taskTraceEventFromRow = (row) => decode("RelayStore.decodeTaskTraceEvent", TaskTraceEvent, {
|
|
503
615
|
kind: row.kind,
|
|
504
616
|
at: row.created_at,
|
|
505
617
|
...row.detail === null ? {} : { detail: row.detail }
|
|
506
|
-
}), rolesFromRow = (row) =>
|
|
618
|
+
}), rolesFromRow = (row) => Schema11.decodeUnknownSync(Schema11.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
|
|
507
619
|
const roles = rolesFromRow(row);
|
|
508
620
|
const role = roles[0];
|
|
509
|
-
return role === undefined ?
|
|
621
|
+
return role === undefined ? Effect11.fail(new PersistenceError({
|
|
510
622
|
operation: "RelayStore.decodeAuthSession",
|
|
511
623
|
message: `Session ${row.id} has no role`
|
|
512
624
|
})) : decode("RelayStore.decodeAuthSession", AuthSession, {
|
|
@@ -521,7 +633,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
521
633
|
});
|
|
522
634
|
}, secret = (prefix) => {
|
|
523
635
|
return `cohall_${prefix}_${randomBytes(32).toString("hex")}`;
|
|
524
|
-
}, tokenHash = (token) =>
|
|
636
|
+
}, tokenHash = (token) => createHash2("sha256").update(token).digest("hex"), providerName = (provider) => {
|
|
525
637
|
switch (provider) {
|
|
526
638
|
case "codex":
|
|
527
639
|
return "Codex";
|
|
@@ -587,23 +699,36 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
587
699
|
WHERE NOT EXISTS (SELECT 1 FROM tasks WHERE tasks.thread_id = threads.id)
|
|
588
700
|
AND NOT EXISTS (SELECT 1 FROM messages WHERE messages.thread_id = threads.id)`).run();
|
|
589
701
|
};
|
|
702
|
+
const pruneTerminalOperations = (preserveOperationId) => {
|
|
703
|
+
const exclusion = preserveOperationId === undefined ? "" : "AND id <> ?";
|
|
704
|
+
const offset = preserveOperationId === undefined ? retainedTerminalTasks : retainedTerminalTasks - 1;
|
|
705
|
+
const parameters = preserveOperationId === undefined ? [offset] : [preserveOperationId, offset];
|
|
706
|
+
db.query(`DELETE FROM device_operations
|
|
707
|
+
WHERE id IN (
|
|
708
|
+
SELECT id FROM device_operations
|
|
709
|
+
WHERE status IN ('completed', 'failed') ${exclusion}
|
|
710
|
+
ORDER BY completed_at DESC, updated_at DESC, id DESC
|
|
711
|
+
LIMIT -1 OFFSET ?
|
|
712
|
+
)`).run(...parameters);
|
|
713
|
+
};
|
|
590
714
|
const queryTask = (taskId) => db.query("SELECT * FROM tasks WHERE id = ?").get(taskId);
|
|
591
|
-
const
|
|
592
|
-
|
|
715
|
+
const queryOperation = (operationId) => db.query("SELECT * FROM device_operations WHERE id = ?").get(operationId);
|
|
716
|
+
const getTask = Effect11.fn("RelayStore.getTask")(function* (taskId) {
|
|
717
|
+
const row = yield* Effect11.try({
|
|
593
718
|
try: () => queryTask(taskId),
|
|
594
719
|
catch: operationError("RelayStore.getTask")
|
|
595
720
|
});
|
|
596
721
|
if (row === null) {
|
|
597
|
-
return yield*
|
|
722
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
598
723
|
operation: "RelayStore.getTask",
|
|
599
724
|
message: `Unknown task ${taskId}`
|
|
600
725
|
}));
|
|
601
726
|
}
|
|
602
727
|
return yield* taskFromRow(row);
|
|
603
728
|
});
|
|
604
|
-
const traceTask =
|
|
729
|
+
const traceTask = Effect11.fn("RelayStore.traceTask")(function* (taskId) {
|
|
605
730
|
const task = yield* getTask(taskId);
|
|
606
|
-
const [deviceRow, eventRows] = yield*
|
|
731
|
+
const [deviceRow, eventRows] = yield* Effect11.try({
|
|
607
732
|
try: () => [
|
|
608
733
|
db.query("SELECT * FROM devices WHERE id = ?").get(task.targetDeviceId),
|
|
609
734
|
db.query(`SELECT * FROM (
|
|
@@ -614,14 +739,14 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
614
739
|
catch: operationError("RelayStore.traceTask")
|
|
615
740
|
});
|
|
616
741
|
if (deviceRow === null) {
|
|
617
|
-
return yield*
|
|
742
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
618
743
|
operation: "RelayStore.traceTask.device",
|
|
619
744
|
message: `Unknown target device ${task.targetDeviceId}`
|
|
620
745
|
}));
|
|
621
746
|
}
|
|
622
747
|
const targetDevice = yield* deviceFromRow(deviceRow);
|
|
623
748
|
const selectedRows = eventRows.slice(-100);
|
|
624
|
-
const recordedEvents = yield*
|
|
749
|
+
const recordedEvents = yield* Effect11.forEach(selectedRows, taskTraceEventFromRow);
|
|
625
750
|
const events = recordedEvents.length === 0 ? derivedTraceEvents(task) : recordedEvents;
|
|
626
751
|
return TaskTrace.make({
|
|
627
752
|
taskId: task.id,
|
|
@@ -641,15 +766,196 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
641
766
|
...task.error === undefined ? {} : { error: task.error }
|
|
642
767
|
});
|
|
643
768
|
});
|
|
644
|
-
const listDevices =
|
|
645
|
-
const rows = yield*
|
|
769
|
+
const listDevices = Effect11.fn("RelayStore.listDevices")(function* () {
|
|
770
|
+
const rows = yield* Effect11.try({
|
|
646
771
|
try: () => db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all(),
|
|
647
772
|
catch: operationError("RelayStore.listDevices")
|
|
648
773
|
});
|
|
649
|
-
return yield*
|
|
774
|
+
return yield* Effect11.forEach(rows, deviceFromRow);
|
|
775
|
+
});
|
|
776
|
+
const usage = Effect11.fn("RelayStore.usage")(function* () {
|
|
777
|
+
const rows = yield* Effect11.try({
|
|
778
|
+
try: () => db.query(`SELECT tasks.target_device_id AS device_id, devices.name AS device_name,
|
|
779
|
+
tasks.status, tasks.provider, COUNT(*) AS tasks
|
|
780
|
+
FROM tasks
|
|
781
|
+
JOIN devices ON devices.id = tasks.target_device_id
|
|
782
|
+
GROUP BY tasks.target_device_id, devices.name, tasks.status, tasks.provider
|
|
783
|
+
ORDER BY devices.name COLLATE NOCASE, tasks.target_device_id, tasks.provider`).all(),
|
|
784
|
+
catch: operationError("RelayStore.usage")
|
|
785
|
+
});
|
|
786
|
+
const totalStatus = emptyStatusCounts();
|
|
787
|
+
const totalProviders = new Map;
|
|
788
|
+
const devices = new Map;
|
|
789
|
+
let retainedTasks = 0;
|
|
790
|
+
for (const row of rows) {
|
|
791
|
+
const status = Schema11.decodeUnknownSync(TaskStatus)(row.status);
|
|
792
|
+
const provider = Schema11.decodeUnknownSync(Provider)(row.provider);
|
|
793
|
+
const deviceId = Schema11.decodeUnknownSync(DeviceId)(row.device_id);
|
|
794
|
+
retainedTasks += row.tasks;
|
|
795
|
+
totalStatus[status] += row.tasks;
|
|
796
|
+
totalProviders.set(provider, (totalProviders.get(provider) ?? 0) + row.tasks);
|
|
797
|
+
const current = devices.get(deviceId) ?? {
|
|
798
|
+
name: row.device_name,
|
|
799
|
+
status: emptyStatusCounts(),
|
|
800
|
+
providers: new Map
|
|
801
|
+
};
|
|
802
|
+
current.status[status] += row.tasks;
|
|
803
|
+
current.providers.set(provider, (current.providers.get(provider) ?? 0) + row.tasks);
|
|
804
|
+
devices.set(deviceId, current);
|
|
805
|
+
}
|
|
806
|
+
const providerUsage = (providers) => Provider.literals.flatMap((provider) => {
|
|
807
|
+
const tasks = providers.get(provider);
|
|
808
|
+
return tasks === undefined ? [] : [{ provider, tasks }];
|
|
809
|
+
});
|
|
810
|
+
return UsageSummary.make({
|
|
811
|
+
retainedTasks,
|
|
812
|
+
byStatus: TaskStatusCounts.make(totalStatus),
|
|
813
|
+
byProvider: providerUsage(totalProviders),
|
|
814
|
+
devices: [...devices.entries()].map(([deviceId, device]) => DeviceUsage.make({
|
|
815
|
+
deviceId,
|
|
816
|
+
deviceName: device.name,
|
|
817
|
+
tasks: Object.values(device.status).reduce((sum, tasks) => sum + tasks, 0),
|
|
818
|
+
byStatus: TaskStatusCounts.make(device.status),
|
|
819
|
+
byProvider: providerUsage(device.providers)
|
|
820
|
+
}))
|
|
821
|
+
});
|
|
650
822
|
});
|
|
651
|
-
const
|
|
652
|
-
const row = yield*
|
|
823
|
+
const getOperation = Effect11.fn("RelayStore.getOperation")(function* (operationId) {
|
|
824
|
+
const row = yield* Effect11.try({
|
|
825
|
+
try: () => queryOperation(operationId),
|
|
826
|
+
catch: operationError("RelayStore.getOperation")
|
|
827
|
+
});
|
|
828
|
+
if (row === null) {
|
|
829
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
830
|
+
operation: "RelayStore.getOperation",
|
|
831
|
+
message: `Unknown device operation ${operationId}`
|
|
832
|
+
}));
|
|
833
|
+
}
|
|
834
|
+
return yield* operationFromRow(row);
|
|
835
|
+
});
|
|
836
|
+
const listOperations = Effect11.fn("RelayStore.listOperations")(function* () {
|
|
837
|
+
const rows = yield* Effect11.try({
|
|
838
|
+
try: () => db.query("SELECT * FROM device_operations ORDER BY created_at DESC, id DESC LIMIT 50").all(),
|
|
839
|
+
catch: operationError("RelayStore.listOperations")
|
|
840
|
+
});
|
|
841
|
+
return yield* Effect11.forEach(rows, operationFromRow);
|
|
842
|
+
});
|
|
843
|
+
const createUpgradeOperations = Effect11.fn("RelayStore.createUpgradeOperations")(function* (input) {
|
|
844
|
+
const timestamp = now();
|
|
845
|
+
const operations = yield* Effect11.try({
|
|
846
|
+
try: () => db.transaction(() => {
|
|
847
|
+
const devices = db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all();
|
|
848
|
+
if (devices.length === 0) {
|
|
849
|
+
throw new Error("No Cohall devices are registered");
|
|
850
|
+
}
|
|
851
|
+
if (devices.length > 256) {
|
|
852
|
+
throw new Error("All-device operations support at most 256 registered devices");
|
|
853
|
+
}
|
|
854
|
+
assertDeviceOperationSupport(devices);
|
|
855
|
+
for (const device of devices) {
|
|
856
|
+
const outstanding = db.query(`SELECT COUNT(*) AS count FROM device_operations
|
|
857
|
+
WHERE target_device_id = ? AND status IN ('queued', 'assigned', 'running')`).get(device.id);
|
|
858
|
+
if ((outstanding?.count ?? 0) > 0) {
|
|
859
|
+
throw new Error(`${device.name} already has an upgrade operation in progress`);
|
|
860
|
+
}
|
|
861
|
+
}
|
|
862
|
+
pruneTerminalOperations();
|
|
863
|
+
return devices.map((device) => {
|
|
864
|
+
const operation = DeviceOperation.make({
|
|
865
|
+
id: makeOperationId(),
|
|
866
|
+
kind: "upgrade",
|
|
867
|
+
status: "queued",
|
|
868
|
+
targetDeviceId: DeviceId.make(device.id),
|
|
869
|
+
requestedVersion: input.target,
|
|
870
|
+
restart: input.restart,
|
|
871
|
+
createdAt: timestamp,
|
|
872
|
+
updatedAt: timestamp
|
|
873
|
+
});
|
|
874
|
+
db.query(`INSERT INTO device_operations (
|
|
875
|
+
id, kind, status, target_device_id, requested_version, restart,
|
|
876
|
+
created_at, updated_at
|
|
877
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(operation.id, operation.kind, operation.status, operation.targetDeviceId, operation.requestedVersion, operation.restart ? 1 : 0, operation.createdAt, operation.updatedAt);
|
|
878
|
+
return operation;
|
|
879
|
+
});
|
|
880
|
+
})(),
|
|
881
|
+
catch: operationError("RelayStore.createUpgradeOperations")
|
|
882
|
+
});
|
|
883
|
+
return operations;
|
|
884
|
+
});
|
|
885
|
+
const pendingOperationsFor = Effect11.fn("RelayStore.pendingOperationsFor")(function* (deviceId) {
|
|
886
|
+
const rows = yield* Effect11.try({
|
|
887
|
+
try: () => db.query(`SELECT * FROM device_operations WHERE target_device_id = ? AND status = 'queued'
|
|
888
|
+
ORDER BY created_at, id LIMIT 10`).all(deviceId),
|
|
889
|
+
catch: operationError("RelayStore.pendingOperationsFor")
|
|
890
|
+
});
|
|
891
|
+
return yield* Effect11.forEach(rows, operationFromRow);
|
|
892
|
+
});
|
|
893
|
+
const transitionOperation = Effect11.fn("RelayStore.transitionOperation")(function* (operationId, expected, status, result, error) {
|
|
894
|
+
const current = yield* getOperation(operationId);
|
|
895
|
+
if (!expected.includes(current.status)) {
|
|
896
|
+
return current;
|
|
897
|
+
}
|
|
898
|
+
const timestamp = now();
|
|
899
|
+
const completed = status === "completed" || status === "failed" ? timestamp : undefined;
|
|
900
|
+
const placeholders = expected.map(() => "?").join(", ");
|
|
901
|
+
yield* Effect11.try({
|
|
902
|
+
try: () => db.transaction(() => {
|
|
903
|
+
const updated = db.query(`UPDATE device_operations
|
|
904
|
+
SET status = ?, result = ?, error = ?, updated_at = ?, completed_at = ?
|
|
905
|
+
WHERE id = ? AND status IN (${placeholders})`).run(status, result ?? current.result ?? null, error ?? current.error ?? null, timestamp, completed ?? current.completedAt ?? null, operationId, ...expected);
|
|
906
|
+
if (updated.changes === 1 && completed !== undefined) {
|
|
907
|
+
pruneTerminalOperations(operationId);
|
|
908
|
+
}
|
|
909
|
+
})(),
|
|
910
|
+
catch: operationError("RelayStore.transitionOperation")
|
|
911
|
+
});
|
|
912
|
+
return yield* getOperation(operationId);
|
|
913
|
+
});
|
|
914
|
+
const requireOperationTarget = Effect11.fn("RelayStore.requireOperationTarget")(function* (operationId, deviceId) {
|
|
915
|
+
const operation = yield* getOperation(operationId);
|
|
916
|
+
if (operation.targetDeviceId !== deviceId) {
|
|
917
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
918
|
+
operation: "RelayStore.requireOperationTarget",
|
|
919
|
+
message: `Device ${deviceId} cannot update operation ${operationId}`
|
|
920
|
+
}));
|
|
921
|
+
}
|
|
922
|
+
return operation;
|
|
923
|
+
});
|
|
924
|
+
const assignOperation = Effect11.fn("RelayStore.assignOperation")(function* (operationId) {
|
|
925
|
+
const current = yield* getOperation(operationId);
|
|
926
|
+
if (current.status !== "queued") {
|
|
927
|
+
return current;
|
|
928
|
+
}
|
|
929
|
+
const timestamp = now();
|
|
930
|
+
yield* Effect11.try({
|
|
931
|
+
try: () => db.query(`UPDATE device_operations SET status = 'assigned', updated_at = ?
|
|
932
|
+
WHERE id = ? AND status = 'queued'
|
|
933
|
+
AND NOT EXISTS (
|
|
934
|
+
SELECT 1 FROM tasks WHERE target_device_id = ?
|
|
935
|
+
AND status IN ('assigned', 'running', 'cancelling')
|
|
936
|
+
)
|
|
937
|
+
AND NOT EXISTS (
|
|
938
|
+
SELECT 1 FROM device_operations active
|
|
939
|
+
WHERE active.target_device_id = ? AND active.id <> ?
|
|
940
|
+
AND active.status IN ('assigned', 'running')
|
|
941
|
+
)`).run(timestamp, operationId, current.targetDeviceId, current.targetDeviceId, operationId),
|
|
942
|
+
catch: operationError("RelayStore.assignOperation")
|
|
943
|
+
});
|
|
944
|
+
return yield* getOperation(operationId);
|
|
945
|
+
});
|
|
946
|
+
const acceptOperation = (operationId, deviceId) => requireOperationTarget(operationId, deviceId).pipe(Effect11.flatMap(() => transitionOperation(operationId, ["assigned"], "running")));
|
|
947
|
+
const finishOperation = (operationId, deviceId, result) => requireOperationTarget(operationId, deviceId).pipe(Effect11.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "completed", result)));
|
|
948
|
+
const failOperation = (operationId, deviceId, error) => requireOperationTarget(operationId, deviceId).pipe(Effect11.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, error)));
|
|
949
|
+
const abandonOperation = (operationId) => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, "Abandoned by the relay owner");
|
|
950
|
+
const requeueOperationsFor = Effect11.fn("RelayStore.requeueOperationsFor")(function* (deviceId) {
|
|
951
|
+
yield* Effect11.try({
|
|
952
|
+
try: () => db.query(`UPDATE device_operations SET status = 'queued', updated_at = ?
|
|
953
|
+
WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(now(), deviceId),
|
|
954
|
+
catch: operationError("RelayStore.requeueOperationsFor")
|
|
955
|
+
});
|
|
956
|
+
});
|
|
957
|
+
const forgetDevice = Effect11.fn("RelayStore.forgetDevice")(function* (deviceId) {
|
|
958
|
+
const row = yield* Effect11.try({
|
|
653
959
|
try: () => db.transaction(() => {
|
|
654
960
|
const device = db.query("SELECT * FROM devices WHERE id = ? AND forgotten_at IS NULL").get(deviceId);
|
|
655
961
|
if (device === null) {
|
|
@@ -664,6 +970,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
664
970
|
throw new Error(`Device ${deviceId} still has outstanding tasks`);
|
|
665
971
|
}
|
|
666
972
|
const timestamp = now();
|
|
973
|
+
db.query(`UPDATE device_operations
|
|
974
|
+
SET status = 'failed', error = ?, updated_at = ?, completed_at = ?
|
|
975
|
+
WHERE target_device_id = ? AND status IN ('queued', 'assigned', 'running')`).run("Target device was forgotten by the relay owner", timestamp, timestamp, deviceId);
|
|
976
|
+
pruneTerminalOperations();
|
|
667
977
|
db.query("UPDATE devices SET forgotten_at = ? WHERE id = ?").run(timestamp, deviceId);
|
|
668
978
|
db.query("UPDATE auth_sessions SET revoked_at = ? WHERE bound_device_id = ? AND revoked_at IS NULL").run(timestamp, deviceId);
|
|
669
979
|
return device;
|
|
@@ -672,25 +982,27 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
672
982
|
});
|
|
673
983
|
return yield* deviceFromRow(row);
|
|
674
984
|
});
|
|
675
|
-
const recover =
|
|
676
|
-
yield*
|
|
985
|
+
const recover = Effect11.fn("RelayStore.recover")(function* () {
|
|
986
|
+
yield* Effect11.try({
|
|
677
987
|
try: () => db.transaction(() => {
|
|
678
988
|
const timestamp = now();
|
|
679
989
|
const interrupted = db.query("SELECT id FROM tasks WHERE status IN ('assigned', 'running')").all();
|
|
680
990
|
db.query("UPDATE devices SET status = 'offline'").run();
|
|
681
991
|
db.query("UPDATE tasks SET status = 'queued', updated_at = ? WHERE status IN ('assigned', 'running')").run(timestamp);
|
|
992
|
+
db.query("UPDATE device_operations SET status = 'queued', updated_at = ? WHERE status IN ('assigned', 'running')").run(timestamp);
|
|
682
993
|
for (const task of interrupted) {
|
|
683
994
|
recordTaskTraceEvent(TaskId.make(task.id), "requeued", "Relay restarted before the task completed", timestamp);
|
|
684
995
|
}
|
|
685
996
|
db.query("DELETE FROM auth_pairings WHERE expires_at <= ? OR used_at IS NOT NULL").run(timestamp);
|
|
686
997
|
db.query("UPDATE auth_sessions SET revoked_at = ? WHERE revoked_at IS NULL AND roles_json LIKE '%,%'").run(timestamp);
|
|
687
998
|
pruneTerminalHistory();
|
|
999
|
+
pruneTerminalOperations();
|
|
688
1000
|
})(),
|
|
689
1001
|
catch: operationError("RelayStore.recover")
|
|
690
1002
|
});
|
|
691
1003
|
});
|
|
692
|
-
const upsertDevice =
|
|
693
|
-
yield*
|
|
1004
|
+
const upsertDevice = Effect11.fn("RelayStore.upsertDevice")(function* (device) {
|
|
1005
|
+
yield* Effect11.try({
|
|
694
1006
|
try: () => db.query(`INSERT INTO devices (
|
|
695
1007
|
id, name, hostname, platform, architecture, status, providers_json,
|
|
696
1008
|
capabilities_json, workspaces_json, version, last_seen_at, connected_at
|
|
@@ -706,28 +1018,28 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
706
1018
|
});
|
|
707
1019
|
return device;
|
|
708
1020
|
});
|
|
709
|
-
const updateDeviceStatus =
|
|
1021
|
+
const updateDeviceStatus = Effect11.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status) {
|
|
710
1022
|
const timestamp = now();
|
|
711
|
-
const result = yield*
|
|
1023
|
+
const result = yield* Effect11.try({
|
|
712
1024
|
try: () => db.query("UPDATE devices SET status = ?, last_seen_at = ? WHERE id = ?").run(status, timestamp, deviceId),
|
|
713
1025
|
catch: operationError("RelayStore.updateDeviceStatus")
|
|
714
1026
|
});
|
|
715
1027
|
if (result.changes !== 1) {
|
|
716
|
-
return yield*
|
|
1028
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
717
1029
|
operation: "RelayStore.updateDeviceStatus",
|
|
718
1030
|
message: `Unknown device ${deviceId}`
|
|
719
1031
|
}));
|
|
720
1032
|
}
|
|
721
1033
|
const row = db.query("SELECT * FROM devices WHERE id = ?").get(deviceId);
|
|
722
1034
|
if (row === null) {
|
|
723
|
-
return yield*
|
|
1035
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
724
1036
|
operation: "RelayStore.updateDeviceStatus.read",
|
|
725
1037
|
message: `Device ${deviceId} disappeared after update`
|
|
726
1038
|
}));
|
|
727
1039
|
}
|
|
728
1040
|
return yield* deviceFromRow(row);
|
|
729
1041
|
});
|
|
730
|
-
const createDelegation2 =
|
|
1042
|
+
const createDelegation2 = Effect11.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, sourceDeviceId, providerSessionId) {
|
|
731
1043
|
const timestamp = now();
|
|
732
1044
|
const threadId = input.threadId ?? makeThreadId();
|
|
733
1045
|
const task = Task.make({
|
|
@@ -761,7 +1073,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
761
1073
|
taskId: task.id,
|
|
762
1074
|
...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
|
|
763
1075
|
});
|
|
764
|
-
yield*
|
|
1076
|
+
yield* Effect11.try({
|
|
765
1077
|
try: () => db.transaction(() => {
|
|
766
1078
|
const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks WHERE target_device_id = ?
|
|
767
1079
|
AND status IN ('queued', 'assigned', 'running', 'cancelling')`).get(targetDeviceId);
|
|
@@ -789,18 +1101,18 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
789
1101
|
});
|
|
790
1102
|
return task;
|
|
791
1103
|
});
|
|
792
|
-
const threadContext2 =
|
|
793
|
-
const threadRow = yield*
|
|
1104
|
+
const threadContext2 = Effect11.fn("RelayStore.threadContext")(function* (threadId) {
|
|
1105
|
+
const threadRow = yield* Effect11.try({
|
|
794
1106
|
try: () => db.query("SELECT * FROM threads WHERE id = ?").get(threadId),
|
|
795
1107
|
catch: operationError("RelayStore.threadContext.thread")
|
|
796
1108
|
});
|
|
797
1109
|
if (threadRow === null) {
|
|
798
|
-
return yield*
|
|
1110
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
799
1111
|
operation: "RelayStore.threadContext",
|
|
800
1112
|
message: `Unknown thread ${threadId}`
|
|
801
1113
|
}));
|
|
802
1114
|
}
|
|
803
|
-
const [messageRows, taskRows] = yield*
|
|
1115
|
+
const [messageRows, taskRows] = yield* Effect11.try({
|
|
804
1116
|
try: () => [
|
|
805
1117
|
db.query(`SELECT * FROM (
|
|
806
1118
|
SELECT * FROM messages WHERE thread_id = ?
|
|
@@ -813,10 +1125,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
813
1125
|
],
|
|
814
1126
|
catch: operationError("RelayStore.threadContext.items")
|
|
815
1127
|
});
|
|
816
|
-
const [thread, messages, tasks] = yield*
|
|
1128
|
+
const [thread, messages, tasks] = yield* Effect11.all([
|
|
817
1129
|
threadFromRow(threadRow),
|
|
818
|
-
|
|
819
|
-
|
|
1130
|
+
Effect11.forEach(messageRows.slice(-20), messageFromRow),
|
|
1131
|
+
Effect11.forEach(taskRows.slice(-20), taskFromRow)
|
|
820
1132
|
]);
|
|
821
1133
|
const recentMessages = recentWithin(messages, 256 * 1024);
|
|
822
1134
|
const recentTasks = recentWithin(tasks, 768 * 1024);
|
|
@@ -827,22 +1139,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
827
1139
|
truncated: messageRows.length > 20 || taskRows.length > 20 || recentMessages.truncated || recentTasks.truncated
|
|
828
1140
|
});
|
|
829
1141
|
});
|
|
830
|
-
const pendingTasksFor =
|
|
831
|
-
const rows = yield*
|
|
1142
|
+
const pendingTasksFor = Effect11.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
|
|
1143
|
+
const rows = yield* Effect11.try({
|
|
832
1144
|
try: () => db.query(`SELECT * FROM tasks WHERE target_device_id = ?
|
|
833
1145
|
AND status IN ('queued', 'cancelling') ORDER BY created_at, id LIMIT 100`).all(deviceId),
|
|
834
1146
|
catch: operationError("RelayStore.pendingTasksFor")
|
|
835
1147
|
});
|
|
836
|
-
return yield*
|
|
1148
|
+
return yield* Effect11.forEach(rows, taskFromRow);
|
|
837
1149
|
});
|
|
838
|
-
const transition =
|
|
1150
|
+
const transition = Effect11.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
|
|
839
1151
|
const current = yield* getTask(taskId);
|
|
840
1152
|
if (!expected.includes(current.status)) {
|
|
841
1153
|
return current;
|
|
842
1154
|
}
|
|
843
1155
|
const placeholders = expected.map(() => "?").join(", ");
|
|
844
1156
|
const timestamp = now();
|
|
845
|
-
yield*
|
|
1157
|
+
yield* Effect11.try({
|
|
846
1158
|
try: () => db.transaction(() => {
|
|
847
1159
|
const result = db.query(`UPDATE tasks SET status = ?, provider_session_id = ?, result = ?, error = ?,
|
|
848
1160
|
started_at = ?, completed_at = ?, updated_at = ?
|
|
@@ -858,20 +1170,24 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
858
1170
|
});
|
|
859
1171
|
return yield* getTask(taskId);
|
|
860
1172
|
});
|
|
861
|
-
const assignTask =
|
|
1173
|
+
const assignTask = Effect11.fn("RelayStore.assignTask")(function* (taskId) {
|
|
862
1174
|
const current = yield* getTask(taskId);
|
|
863
1175
|
if (current.status !== "queued") {
|
|
864
1176
|
return current;
|
|
865
1177
|
}
|
|
866
1178
|
const timestamp = now();
|
|
867
|
-
yield*
|
|
1179
|
+
yield* Effect11.try({
|
|
868
1180
|
try: () => db.transaction(() => {
|
|
869
1181
|
const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?
|
|
870
1182
|
WHERE id = ? AND status = 'queued' AND NOT EXISTS (
|
|
871
1183
|
SELECT 1 FROM tasks active
|
|
872
1184
|
WHERE active.target_device_id = ? AND active.id <> ?
|
|
873
1185
|
AND active.status IN ('assigned', 'running', 'cancelling')
|
|
874
|
-
)
|
|
1186
|
+
) AND NOT EXISTS (
|
|
1187
|
+
SELECT 1 FROM device_operations active
|
|
1188
|
+
WHERE active.target_device_id = ?
|
|
1189
|
+
AND active.status IN ('assigned', 'running')
|
|
1190
|
+
)`).run(timestamp, taskId, current.targetDeviceId, taskId, current.targetDeviceId);
|
|
875
1191
|
if (result.changes === 1) {
|
|
876
1192
|
recordTaskTraceEvent(taskId, "assigned", "Relay dispatched the task over the device WebSocket", timestamp);
|
|
877
1193
|
}
|
|
@@ -880,23 +1196,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
880
1196
|
});
|
|
881
1197
|
return yield* getTask(taskId);
|
|
882
1198
|
});
|
|
883
|
-
const requireTarget =
|
|
1199
|
+
const requireTarget = Effect11.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
|
|
884
1200
|
const task = yield* getTask(taskId);
|
|
885
1201
|
if (task.targetDeviceId !== deviceId) {
|
|
886
|
-
return yield*
|
|
1202
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
887
1203
|
operation: "RelayStore.requireTarget",
|
|
888
1204
|
message: `Task ${taskId} belongs to another device`
|
|
889
1205
|
}));
|
|
890
1206
|
}
|
|
891
1207
|
return task;
|
|
892
1208
|
});
|
|
893
|
-
const terminal =
|
|
1209
|
+
const terminal = Effect11.fn("RelayStore.terminal")(function* (taskId, deviceId, status, result, error, providerSessionId) {
|
|
894
1210
|
const current = yield* requireTarget(taskId, deviceId);
|
|
895
1211
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
896
1212
|
return current;
|
|
897
1213
|
}
|
|
898
1214
|
const timestamp = now();
|
|
899
|
-
yield*
|
|
1215
|
+
yield* Effect11.try({
|
|
900
1216
|
try: () => db.transaction(() => {
|
|
901
1217
|
const updated = db.query(`UPDATE tasks SET status = ?, result = ?, error = ?, provider_session_id = ?,
|
|
902
1218
|
completed_at = ?, updated_at = ? WHERE id = ?
|
|
@@ -926,7 +1242,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
926
1242
|
});
|
|
927
1243
|
return yield* getTask(taskId);
|
|
928
1244
|
});
|
|
929
|
-
const requestCancellation =
|
|
1245
|
+
const requestCancellation = Effect11.fn("RelayStore.requestCancellation")(function* (taskId) {
|
|
930
1246
|
const current = yield* getTask(taskId);
|
|
931
1247
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
932
1248
|
return current;
|
|
@@ -939,9 +1255,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
939
1255
|
status: "cancelling"
|
|
940
1256
|
}, { kind: "cancelling", detail: "Client requested task cancellation" });
|
|
941
1257
|
});
|
|
942
|
-
const requeueTasksFor =
|
|
1258
|
+
const requeueTasksFor = Effect11.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
|
|
943
1259
|
const timestamp = now();
|
|
944
|
-
yield*
|
|
1260
|
+
yield* Effect11.try({
|
|
945
1261
|
try: () => db.transaction(() => {
|
|
946
1262
|
const interrupted = db.query(`SELECT id FROM tasks WHERE target_device_id = ?
|
|
947
1263
|
AND status IN ('assigned', 'running')`).all(deviceId);
|
|
@@ -954,42 +1270,42 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
954
1270
|
catch: operationError("RelayStore.requeueTasksFor")
|
|
955
1271
|
});
|
|
956
1272
|
});
|
|
957
|
-
const sessionFor =
|
|
958
|
-
const row = yield*
|
|
1273
|
+
const sessionFor = Effect11.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
|
|
1274
|
+
const row = yield* Effect11.try({
|
|
959
1275
|
try: () => db.query(`SELECT session_id FROM provider_sessions
|
|
960
1276
|
WHERE thread_id = ? AND device_id = ? AND provider = ?`).get(threadId, deviceId, provider),
|
|
961
1277
|
catch: operationError("RelayStore.sessionFor")
|
|
962
1278
|
});
|
|
963
1279
|
return row?.session_id;
|
|
964
1280
|
});
|
|
965
|
-
const createPairing =
|
|
1281
|
+
const createPairing = Effect11.fn("RelayStore.createPairing")(function* (input) {
|
|
966
1282
|
const roles = [...new Set(input.roles)];
|
|
967
1283
|
if (roles.length === 0) {
|
|
968
|
-
return yield*
|
|
1284
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
969
1285
|
operation: "RelayStore.createPairing",
|
|
970
1286
|
message: "A pairing must allow at least one role"
|
|
971
1287
|
}));
|
|
972
1288
|
}
|
|
973
1289
|
const token = secret("pair");
|
|
974
1290
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
|
975
|
-
yield*
|
|
1291
|
+
yield* Effect11.try({
|
|
976
1292
|
try: () => db.query(`INSERT INTO auth_pairings (token_hash, label, roles_json, expires_at, created_at)
|
|
977
1293
|
VALUES (?, ?, ?, ?, ?)`).run(tokenHash(token), input.label, JSON.stringify(roles), expiresAt, now()),
|
|
978
1294
|
catch: operationError("RelayStore.createPairing")
|
|
979
1295
|
});
|
|
980
1296
|
return PairingCredential.make({ token, expiresAt: Timestamp.make(expiresAt) });
|
|
981
1297
|
});
|
|
982
|
-
const exchangePairing2 =
|
|
1298
|
+
const exchangePairing2 = Effect11.fn("RelayStore.exchangePairing")(function* (token) {
|
|
983
1299
|
const timestamp = now();
|
|
984
1300
|
const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
|
|
985
|
-
const created = yield*
|
|
1301
|
+
const created = yield* Effect11.try({
|
|
986
1302
|
try: () => db.transaction(() => {
|
|
987
1303
|
const pairing = db.query(`SELECT * FROM auth_pairings
|
|
988
1304
|
WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?`).get(tokenHash(token), timestamp);
|
|
989
1305
|
if (pairing === null) {
|
|
990
1306
|
throw new Error("Pairing credential is invalid, expired, or already used");
|
|
991
1307
|
}
|
|
992
|
-
const roles =
|
|
1308
|
+
const roles = Schema11.decodeUnknownSync(Schema11.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
|
|
993
1309
|
const deviceId = roles.includes("device") ? makeDeviceId() : undefined;
|
|
994
1310
|
const consumed = db.query("UPDATE auth_pairings SET used_at = ? WHERE token_hash = ? AND used_at IS NULL").run(timestamp, pairing.token_hash);
|
|
995
1311
|
if (consumed.changes !== 1) {
|
|
@@ -1017,11 +1333,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1017
1333
|
})(),
|
|
1018
1334
|
catch: operationError("RelayStore.exchangePairing")
|
|
1019
1335
|
});
|
|
1020
|
-
const credentials = yield*
|
|
1336
|
+
const credentials = yield* Effect11.forEach(created, ({ token: sessionToken, row }) => authSessionFromRow(row).pipe(Effect11.map((session) => ({ token: sessionToken, session }))));
|
|
1021
1337
|
return PairingResult.make({ credentials });
|
|
1022
1338
|
});
|
|
1023
|
-
const authenticateSession =
|
|
1024
|
-
const row = yield*
|
|
1339
|
+
const authenticateSession = Effect11.fn("RelayStore.authenticateSession")(function* (token, role) {
|
|
1340
|
+
const row = yield* Effect11.try({
|
|
1025
1341
|
try: () => db.query(`SELECT * FROM auth_sessions
|
|
1026
1342
|
WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > ?`).get(tokenHash(token), now()),
|
|
1027
1343
|
catch: operationError("RelayStore.authenticateSession")
|
|
@@ -1030,35 +1346,35 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1030
1346
|
return;
|
|
1031
1347
|
}
|
|
1032
1348
|
const lastSeenAt = now();
|
|
1033
|
-
yield*
|
|
1349
|
+
yield* Effect11.try({
|
|
1034
1350
|
try: () => db.query("UPDATE auth_sessions SET last_seen_at = ? WHERE id = ?").run(lastSeenAt, row.id),
|
|
1035
1351
|
catch: operationError("RelayStore.authenticateSession.touch")
|
|
1036
1352
|
});
|
|
1037
1353
|
const session = yield* authSessionFromRow(row);
|
|
1038
1354
|
return AuthSession.make({ ...session, lastSeenAt });
|
|
1039
1355
|
});
|
|
1040
|
-
const listAuthSessions =
|
|
1041
|
-
const rows = yield*
|
|
1356
|
+
const listAuthSessions = Effect11.fn("RelayStore.listAuthSessions")(function* () {
|
|
1357
|
+
const rows = yield* Effect11.try({
|
|
1042
1358
|
try: () => db.query("SELECT * FROM auth_sessions ORDER BY created_at DESC, id LIMIT 1000").all(),
|
|
1043
1359
|
catch: operationError("RelayStore.listAuthSessions")
|
|
1044
1360
|
});
|
|
1045
|
-
return yield*
|
|
1361
|
+
return yield* Effect11.forEach(rows, authSessionFromRow);
|
|
1046
1362
|
});
|
|
1047
|
-
const revokeAuthSession =
|
|
1363
|
+
const revokeAuthSession = Effect11.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
|
|
1048
1364
|
const revokedAt = now();
|
|
1049
|
-
const result = yield*
|
|
1365
|
+
const result = yield* Effect11.try({
|
|
1050
1366
|
try: () => db.query("UPDATE auth_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL").run(revokedAt, sessionId),
|
|
1051
1367
|
catch: operationError("RelayStore.revokeAuthSession")
|
|
1052
1368
|
});
|
|
1053
1369
|
if (result.changes !== 1) {
|
|
1054
|
-
return yield*
|
|
1370
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
1055
1371
|
operation: "RelayStore.revokeAuthSession",
|
|
1056
1372
|
message: `Unknown or already revoked session ${sessionId}`
|
|
1057
1373
|
}));
|
|
1058
1374
|
}
|
|
1059
1375
|
const row = db.query("SELECT * FROM auth_sessions WHERE id = ?").get(sessionId);
|
|
1060
1376
|
if (row === null) {
|
|
1061
|
-
return yield*
|
|
1377
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
1062
1378
|
operation: "RelayStore.revokeAuthSession.read",
|
|
1063
1379
|
message: `Session ${sessionId} disappeared after revocation`
|
|
1064
1380
|
}));
|
|
@@ -1068,6 +1384,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1068
1384
|
return Service2.of({
|
|
1069
1385
|
recover,
|
|
1070
1386
|
listDevices,
|
|
1387
|
+
usage,
|
|
1071
1388
|
forgetDevice,
|
|
1072
1389
|
upsertDevice,
|
|
1073
1390
|
heartbeat: (deviceId, status) => updateDeviceStatus(deviceId, status),
|
|
@@ -1079,12 +1396,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1079
1396
|
pendingTasksFor,
|
|
1080
1397
|
assignTask,
|
|
1081
1398
|
rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
|
|
1082
|
-
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(
|
|
1399
|
+
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect11.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }, { kind: "running", detail: "Target device accepted the task and started the provider" }))),
|
|
1083
1400
|
finishTask: (taskId, deviceId, result, providerSessionId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId),
|
|
1084
1401
|
failTask: (taskId, deviceId, error) => terminal(taskId, deviceId, "failed", undefined, error),
|
|
1085
1402
|
acknowledgeCancellation: (taskId, deviceId) => terminal(taskId, deviceId, "cancelled"),
|
|
1086
1403
|
requestCancellation,
|
|
1087
1404
|
requeueTasksFor,
|
|
1405
|
+
createUpgradeOperations,
|
|
1406
|
+
listOperations,
|
|
1407
|
+
abandonOperation,
|
|
1408
|
+
pendingOperationsFor,
|
|
1409
|
+
assignOperation,
|
|
1410
|
+
rollbackOperation: (operationId) => transitionOperation(operationId, ["assigned"], "queued"),
|
|
1411
|
+
acceptOperation,
|
|
1412
|
+
finishOperation,
|
|
1413
|
+
failOperation,
|
|
1414
|
+
requeueOperationsFor,
|
|
1088
1415
|
sessionFor,
|
|
1089
1416
|
createPairing,
|
|
1090
1417
|
exchangePairing: exchangePairing2,
|
|
@@ -1092,7 +1419,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1092
1419
|
listAuthSessions,
|
|
1093
1420
|
revokeAuthSession
|
|
1094
1421
|
});
|
|
1095
|
-
}, migrate = (db) =>
|
|
1422
|
+
}, migrate = (db) => Effect11.try({
|
|
1096
1423
|
try: () => {
|
|
1097
1424
|
db.exec("PRAGMA journal_mode = WAL");
|
|
1098
1425
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -1140,6 +1467,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1140
1467
|
provider TEXT NOT NULL, session_id TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
1141
1468
|
PRIMARY KEY(thread_id, device_id, provider)
|
|
1142
1469
|
);
|
|
1470
|
+
CREATE TABLE IF NOT EXISTS device_operations (
|
|
1471
|
+
id TEXT PRIMARY KEY, kind TEXT NOT NULL, status TEXT NOT NULL,
|
|
1472
|
+
target_device_id TEXT NOT NULL REFERENCES devices(id),
|
|
1473
|
+
requested_version TEXT NOT NULL, restart INTEGER NOT NULL,
|
|
1474
|
+
result TEXT, error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
1475
|
+
completed_at TEXT
|
|
1476
|
+
);
|
|
1477
|
+
CREATE INDEX IF NOT EXISTS device_operations_target_status
|
|
1478
|
+
ON device_operations(target_device_id, status);
|
|
1143
1479
|
CREATE TABLE IF NOT EXISTS auth_pairings (
|
|
1144
1480
|
token_hash TEXT PRIMARY KEY, label TEXT NOT NULL, roles_json TEXT NOT NULL,
|
|
1145
1481
|
expires_at TEXT NOT NULL, created_at TEXT NOT NULL, used_at TEXT
|
|
@@ -1157,15 +1493,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1157
1493
|
}
|
|
1158
1494
|
},
|
|
1159
1495
|
catch: operationError("RelayStore.migrate")
|
|
1160
|
-
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2,
|
|
1496
|
+
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect11.acquireRelease(Effect11.try({
|
|
1161
1497
|
try: () => new Database(path),
|
|
1162
1498
|
catch: operationError("RelayStore.open")
|
|
1163
|
-
}).pipe(
|
|
1499
|
+
}).pipe(Effect11.tap(migrate)), (db) => Effect11.sync(() => db.close())).pipe(Effect11.map((db) => makeService(db, retainedTerminalTasks)))), layerFromDatabase = (db) => Layer2.effect(Service2, migrate(db).pipe(Effect11.map(() => makeService(db))));
|
|
1164
1500
|
var init_store = __esm(() => {
|
|
1165
1501
|
init_src();
|
|
1166
1502
|
init_database();
|
|
1167
1503
|
init_store();
|
|
1168
|
-
PersistenceError = class PersistenceError extends
|
|
1504
|
+
PersistenceError = class PersistenceError extends Schema11.TaggedErrorClass()("RelayStore.PersistenceError", { operation: Schema11.String, message: Schema11.String }) {
|
|
1169
1505
|
};
|
|
1170
1506
|
Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
|
|
1171
1507
|
};
|
|
@@ -1177,9 +1513,9 @@ __export(exports_main, {
|
|
|
1177
1513
|
runRelay: () => runRelay,
|
|
1178
1514
|
relayListenOptions: () => relayListenOptions
|
|
1179
1515
|
});
|
|
1180
|
-
import { Effect as
|
|
1181
|
-
import { createHash as
|
|
1182
|
-
import { access as
|
|
1516
|
+
import { Effect as Effect12, ManagedRuntime, Schema as Schema12 } from "effect";
|
|
1517
|
+
import { createHash as createHash3, timingSafeEqual } from "node:crypto";
|
|
1518
|
+
import { access as access6, chmod as chmod7 } from "node:fs/promises";
|
|
1183
1519
|
import { createServer } from "node:http";
|
|
1184
1520
|
import { platform as platform5 } from "node:os";
|
|
1185
1521
|
import { WebSocketServer } from "ws";
|
|
@@ -1208,7 +1544,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1208
1544
|
if (request.method === "GET" || request.method === "HEAD") {
|
|
1209
1545
|
return Promise.resolve(undefined);
|
|
1210
1546
|
}
|
|
1211
|
-
return new Promise((
|
|
1547
|
+
return new Promise((resolve4, reject) => {
|
|
1212
1548
|
const chunks = [];
|
|
1213
1549
|
let size = 0;
|
|
1214
1550
|
const cleanup = () => {
|
|
@@ -1227,7 +1563,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1227
1563
|
};
|
|
1228
1564
|
const onEnd = () => {
|
|
1229
1565
|
cleanup();
|
|
1230
|
-
|
|
1566
|
+
resolve4(Buffer.concat(chunks, size));
|
|
1231
1567
|
};
|
|
1232
1568
|
const onError = (cause) => {
|
|
1233
1569
|
cleanup();
|
|
@@ -1276,17 +1612,17 @@ Content-Length: 0\r
|
|
|
1276
1612
|
if ("destroy" in socket && typeof socket.destroy === "function") {
|
|
1277
1613
|
socket.destroy();
|
|
1278
1614
|
}
|
|
1279
|
-
}, body = (request, decode2) =>
|
|
1615
|
+
}, body = (request, decode2) => Effect12.tryPromise({
|
|
1280
1616
|
try: () => request.json(),
|
|
1281
1617
|
catch: () => new RequestError({ status: 400, message: "Expected a JSON request body" })
|
|
1282
|
-
}).pipe(
|
|
1618
|
+
}).pipe(Effect12.flatMap(decode2), Effect12.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) => Schema12.decodeUnknownEffect(schema)(value).pipe(Effect12.mapError(() => new RequestError({ status: 400, message: `Invalid identifier: ${value}` }))), scoreDevice = (device, input, sourceDeviceId) => {
|
|
1283
1619
|
const prompt = `${input.prompt} ${input.context ?? ""}`.toLowerCase();
|
|
1284
1620
|
const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
|
|
1285
1621
|
const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
|
|
1286
1622
|
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);
|
|
1287
|
-
}, chooseDevice = (input, sourceDeviceId) =>
|
|
1623
|
+
}, chooseDevice = (input, sourceDeviceId) => Effect12.gen(function* () {
|
|
1288
1624
|
const store = yield* exports_store.Service;
|
|
1289
|
-
const devices = yield* store.listDevices().pipe(
|
|
1625
|
+
const devices = yield* store.listDevices().pipe(Effect12.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
|
|
1290
1626
|
const provider = input.provider ?? "codex";
|
|
1291
1627
|
if (input.targetDeviceId !== undefined) {
|
|
1292
1628
|
const target = devices.find((device) => device.id === input.targetDeviceId);
|
|
@@ -1313,26 +1649,26 @@ Content-Length: 0\r
|
|
|
1313
1649
|
}
|
|
1314
1650
|
return selected.id;
|
|
1315
1651
|
}), runRelay = async () => {
|
|
1316
|
-
const configuration = await
|
|
1652
|
+
const configuration = await Effect12.runPromise(loadEnvironmentConfiguration);
|
|
1317
1653
|
process.umask(63);
|
|
1318
1654
|
const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath, configuration.historyTaskLimit));
|
|
1319
1655
|
const run2 = (effect) => runtime.runPromise(effect);
|
|
1320
|
-
await run2(
|
|
1656
|
+
await run2(Effect12.gen(function* () {
|
|
1321
1657
|
const store = yield* exports_store.Service;
|
|
1322
1658
|
yield* store.recover();
|
|
1323
1659
|
}));
|
|
1324
|
-
if (platform5() !== "win32" && await
|
|
1325
|
-
await
|
|
1660
|
+
if (platform5() !== "win32" && await access6(configuration.databasePath).then(() => true).catch(() => false)) {
|
|
1661
|
+
await chmod7(configuration.databasePath, 384);
|
|
1326
1662
|
}
|
|
1327
1663
|
const hub = new Hub;
|
|
1328
1664
|
let connections = 0;
|
|
1329
|
-
const tokenDigest = (token) =>
|
|
1330
|
-
const
|
|
1665
|
+
const tokenDigest = (token) => createHash3("sha256").update(token).digest();
|
|
1666
|
+
const ownerToken2 = (token) => timingSafeEqual(tokenDigest(token), tokenDigest(configuration.token));
|
|
1331
1667
|
const authenticate = async (token, role) => {
|
|
1332
|
-
if (
|
|
1668
|
+
if (ownerToken2(token)) {
|
|
1333
1669
|
return "owner";
|
|
1334
1670
|
}
|
|
1335
|
-
return run2(
|
|
1671
|
+
return run2(Effect12.gen(function* () {
|
|
1336
1672
|
const store = yield* exports_store.Service;
|
|
1337
1673
|
return yield* store.authenticateSession(token, role);
|
|
1338
1674
|
})).catch(() => {
|
|
@@ -1347,7 +1683,7 @@ Content-Length: 0\r
|
|
|
1347
1683
|
return authenticate(authorization.slice("Bearer ".length), "client");
|
|
1348
1684
|
};
|
|
1349
1685
|
const dispatch = async (task) => {
|
|
1350
|
-
const assigned = await run2(
|
|
1686
|
+
const assigned = await run2(Effect12.gen(function* () {
|
|
1351
1687
|
const store = yield* exports_store.Service;
|
|
1352
1688
|
return yield* store.assignTask(task.id);
|
|
1353
1689
|
}));
|
|
@@ -1357,13 +1693,29 @@ Content-Length: 0\r
|
|
|
1357
1693
|
if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }))) {
|
|
1358
1694
|
return assigned;
|
|
1359
1695
|
}
|
|
1360
|
-
return run2(
|
|
1696
|
+
return run2(Effect12.gen(function* () {
|
|
1361
1697
|
const store = yield* exports_store.Service;
|
|
1362
1698
|
return yield* store.rollbackAssignment(task.id);
|
|
1363
1699
|
}));
|
|
1364
1700
|
};
|
|
1701
|
+
const dispatchOperation = async (operation) => {
|
|
1702
|
+
const assigned = await run2(Effect12.gen(function* () {
|
|
1703
|
+
const store = yield* exports_store.Service;
|
|
1704
|
+
return yield* store.assignOperation(operation.id);
|
|
1705
|
+
}));
|
|
1706
|
+
if (assigned.status !== "assigned") {
|
|
1707
|
+
return assigned;
|
|
1708
|
+
}
|
|
1709
|
+
if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "OperationAssigned", operation: assigned }))) {
|
|
1710
|
+
return assigned;
|
|
1711
|
+
}
|
|
1712
|
+
return run2(Effect12.gen(function* () {
|
|
1713
|
+
const store = yield* exports_store.Service;
|
|
1714
|
+
return yield* store.rollbackOperation(operation.id);
|
|
1715
|
+
}));
|
|
1716
|
+
};
|
|
1365
1717
|
const dispatchPending = async (deviceId) => {
|
|
1366
|
-
const tasks = await run2(
|
|
1718
|
+
const tasks = await run2(Effect12.gen(function* () {
|
|
1367
1719
|
const store = yield* exports_store.Service;
|
|
1368
1720
|
return yield* store.pendingTasksFor(deviceId);
|
|
1369
1721
|
}));
|
|
@@ -1374,6 +1726,13 @@ Content-Length: 0\r
|
|
|
1374
1726
|
await dispatch(task);
|
|
1375
1727
|
}
|
|
1376
1728
|
}
|
|
1729
|
+
const operations = await run2(Effect12.gen(function* () {
|
|
1730
|
+
const store = yield* exports_store.Service;
|
|
1731
|
+
return yield* store.pendingOperationsFor(deviceId);
|
|
1732
|
+
}));
|
|
1733
|
+
for (const operation of operations) {
|
|
1734
|
+
await dispatchOperation(operation);
|
|
1735
|
+
}
|
|
1377
1736
|
};
|
|
1378
1737
|
const sendError = (socket, code, message) => {
|
|
1379
1738
|
socket.send(JSON.stringify(SocketEvent.make({
|
|
@@ -1383,10 +1742,10 @@ Content-Length: 0\r
|
|
|
1383
1742
|
})));
|
|
1384
1743
|
};
|
|
1385
1744
|
const handleSocketEvent = async (socket, input) => {
|
|
1386
|
-
const event = await
|
|
1745
|
+
const event = await Effect12.runPromise(Effect12.try({
|
|
1387
1746
|
try: () => JSON.parse(socketText(input)),
|
|
1388
1747
|
catch: () => new RequestError({ status: 400, message: "Invalid socket JSON" })
|
|
1389
|
-
}).pipe(
|
|
1748
|
+
}).pipe(Effect12.flatMap(decodeSocketEvent))).catch((cause) => {
|
|
1390
1749
|
sendError(socket, "invalid_event", cause instanceof Error ? cause.message : String(cause));
|
|
1391
1750
|
return;
|
|
1392
1751
|
});
|
|
@@ -1443,7 +1802,7 @@ Content-Length: 0\r
|
|
|
1443
1802
|
socket.close(4009, "Device is already connected");
|
|
1444
1803
|
return;
|
|
1445
1804
|
}
|
|
1446
|
-
await run2(
|
|
1805
|
+
await run2(Effect12.gen(function* () {
|
|
1447
1806
|
const store = yield* exports_store.Service;
|
|
1448
1807
|
yield* store.upsertDevice(device);
|
|
1449
1808
|
}));
|
|
@@ -1458,7 +1817,10 @@ Content-Length: 0\r
|
|
|
1458
1817
|
const settle = (taskId) => {
|
|
1459
1818
|
socket.send(JSON.stringify(SocketEvent.make({ _tag: "TaskSettled", taskId })));
|
|
1460
1819
|
};
|
|
1461
|
-
const
|
|
1820
|
+
const settleOperation = (operationId) => {
|
|
1821
|
+
socket.send(JSON.stringify(SocketEvent.make({ _tag: "OperationSettled", operationId })));
|
|
1822
|
+
};
|
|
1823
|
+
const processed = await run2(Effect12.gen(function* () {
|
|
1462
1824
|
const store = yield* exports_store.Service;
|
|
1463
1825
|
switch (event._tag) {
|
|
1464
1826
|
case "DeviceHeartbeat":
|
|
@@ -1478,10 +1840,21 @@ Content-Length: 0\r
|
|
|
1478
1840
|
case "TaskCancelled":
|
|
1479
1841
|
yield* store.acknowledgeCancellation(event.taskId, deviceId);
|
|
1480
1842
|
return;
|
|
1843
|
+
case "OperationAccepted":
|
|
1844
|
+
yield* store.acceptOperation(event.operationId, deviceId);
|
|
1845
|
+
return;
|
|
1846
|
+
case "OperationFinished":
|
|
1847
|
+
yield* store.finishOperation(event.operationId, deviceId, event.result);
|
|
1848
|
+
return;
|
|
1849
|
+
case "OperationFailed":
|
|
1850
|
+
yield* store.failOperation(event.operationId, deviceId, event.error);
|
|
1851
|
+
return;
|
|
1481
1852
|
case "Connected":
|
|
1482
1853
|
case "TaskAssigned":
|
|
1483
1854
|
case "TaskSettled":
|
|
1484
1855
|
case "CancelTask":
|
|
1856
|
+
case "OperationAssigned":
|
|
1857
|
+
case "OperationSettled":
|
|
1485
1858
|
case "Error":
|
|
1486
1859
|
return;
|
|
1487
1860
|
}
|
|
@@ -1493,13 +1866,17 @@ Content-Length: 0\r
|
|
|
1493
1866
|
settle(event.taskId);
|
|
1494
1867
|
await dispatchPending(deviceId);
|
|
1495
1868
|
}
|
|
1869
|
+
if (processed && (event._tag === "OperationFinished" || event._tag === "OperationFailed")) {
|
|
1870
|
+
settleOperation(event.operationId);
|
|
1871
|
+
await dispatchPending(deviceId);
|
|
1872
|
+
}
|
|
1496
1873
|
};
|
|
1497
1874
|
const api = async (request, url) => {
|
|
1498
1875
|
if (url.pathname === "/api/health" && request.method === "GET") {
|
|
1499
1876
|
return json({ ok: true, version });
|
|
1500
1877
|
}
|
|
1501
1878
|
if (url.pathname === "/api/auth/pair" && request.method === "POST") {
|
|
1502
|
-
return run2(
|
|
1879
|
+
return run2(Effect12.gen(function* () {
|
|
1503
1880
|
const store = yield* exports_store.Service;
|
|
1504
1881
|
const input = yield* body(request, decodeExchangePairingInput);
|
|
1505
1882
|
return yield* store.exchangePairing(input.token);
|
|
@@ -1509,12 +1886,12 @@ Content-Length: 0\r
|
|
|
1509
1886
|
if (principal === undefined) {
|
|
1510
1887
|
return json({ error: "Unauthorized" }, 401);
|
|
1511
1888
|
}
|
|
1512
|
-
const ownerOnly = url.pathname.startsWith("/api/auth/");
|
|
1889
|
+
const ownerOnly = url.pathname.startsWith("/api/auth/") || url.pathname.startsWith("/api/operations");
|
|
1513
1890
|
if (ownerOnly && principal !== "owner") {
|
|
1514
1891
|
return json({ error: "Owner token required" }, 403);
|
|
1515
1892
|
}
|
|
1516
1893
|
const sourceDeviceId = principal === "owner" ? undefined : principal.deviceId;
|
|
1517
|
-
const effect =
|
|
1894
|
+
const effect = Effect12.gen(function* () {
|
|
1518
1895
|
const store = yield* exports_store.Service;
|
|
1519
1896
|
if (url.pathname === "/api/auth/pairings" && request.method === "POST") {
|
|
1520
1897
|
const input = yield* body(request, decodeCreatePairingInput);
|
|
@@ -1533,6 +1910,23 @@ Content-Length: 0\r
|
|
|
1533
1910
|
if (url.pathname === "/api/devices" && request.method === "GET") {
|
|
1534
1911
|
return json(yield* store.listDevices());
|
|
1535
1912
|
}
|
|
1913
|
+
if (url.pathname === "/api/usage" && request.method === "GET") {
|
|
1914
|
+
return json(yield* store.usage());
|
|
1915
|
+
}
|
|
1916
|
+
if (url.pathname === "/api/operations" && request.method === "GET") {
|
|
1917
|
+
return json(yield* store.listOperations());
|
|
1918
|
+
}
|
|
1919
|
+
if (url.pathname === "/api/operations/upgrades" && request.method === "POST") {
|
|
1920
|
+
const input = yield* body(request, decodeCreateUpgradeOperationsInput);
|
|
1921
|
+
const operations = yield* store.createUpgradeOperations(input);
|
|
1922
|
+
return json(yield* Effect12.tryPromise(() => Promise.all(operations.map(dispatchOperation))), 201);
|
|
1923
|
+
}
|
|
1924
|
+
const abandonOperation = url.pathname.match(/^\/api\/operations\/([^/]+)\/abandon$/);
|
|
1925
|
+
if (request.method === "POST" && abandonOperation?.[1] !== undefined) {
|
|
1926
|
+
const operation = yield* store.abandonOperation(yield* pathId(OperationId, abandonOperation[1]));
|
|
1927
|
+
hub.sendToDevice(operation.targetDeviceId, SocketEvent.make({ _tag: "OperationSettled", operationId: operation.id }));
|
|
1928
|
+
return json(operation);
|
|
1929
|
+
}
|
|
1536
1930
|
const forgetDevice = url.pathname.match(/^\/api\/devices\/([^/]+)\/forget$/);
|
|
1537
1931
|
if (request.method === "POST" && forgetDevice?.[1] !== undefined) {
|
|
1538
1932
|
if (principal !== "owner") {
|
|
@@ -1554,7 +1948,7 @@ Content-Length: 0\r
|
|
|
1554
1948
|
const target = yield* chooseDevice(input, sourceDeviceId);
|
|
1555
1949
|
const providerSessionId = input.threadId === undefined ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
|
|
1556
1950
|
const task2 = yield* store.createDelegation(input, target, sourceDeviceId, providerSessionId);
|
|
1557
|
-
return json(yield*
|
|
1951
|
+
return json(yield* Effect12.tryPromise(() => dispatch(task2)), 201);
|
|
1558
1952
|
}
|
|
1559
1953
|
const task = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
|
|
1560
1954
|
if (request.method === "GET" && task?.[1] !== undefined) {
|
|
@@ -1578,8 +1972,8 @@ Content-Length: 0\r
|
|
|
1578
1972
|
return json(yield* store.threadContext(yield* pathId(ThreadId, thread[1])));
|
|
1579
1973
|
}
|
|
1580
1974
|
return yield* new RequestError({ status: 404, message: "Route not found" });
|
|
1581
|
-
}).pipe(
|
|
1582
|
-
status: cause._tag !== "RelayStore.PersistenceError" ? 500 : cause.message.startsWith("Unknown") ? 404 : cause.message.includes("outstanding task limit") ? 429 : cause.message.includes("must be offline") || cause.message.includes("still has outstanding tasks") ? 409 : 500,
|
|
1975
|
+
}).pipe(Effect12.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
|
|
1976
|
+
status: cause._tag !== "RelayStore.PersistenceError" ? 500 : cause.message.startsWith("Unknown") ? 404 : cause.message.includes("outstanding task limit") ? 429 : cause.message.includes("must be offline") || cause.message.includes("still has outstanding tasks") || cause.message.includes("upgrade operation in progress") || cause.message === "No Cohall devices are registered" || cause.message.startsWith("All-device operations support") ? 409 : 500,
|
|
1583
1977
|
message: cause.message
|
|
1584
1978
|
})));
|
|
1585
1979
|
return run2(effect).catch((cause) => cause instanceof RequestError ? json({ error: cause.status === 500 ? "Relay request failed" : cause.message }, cause.status) : json({ error: "Relay request failed" }, 500));
|
|
@@ -1652,9 +2046,10 @@ Content-Length: 0\r
|
|
|
1652
2046
|
if (deviceId === undefined) {
|
|
1653
2047
|
return;
|
|
1654
2048
|
}
|
|
1655
|
-
run2(
|
|
2049
|
+
run2(Effect12.gen(function* () {
|
|
1656
2050
|
const store = yield* exports_store.Service;
|
|
1657
2051
|
yield* store.requeueTasksFor(deviceId);
|
|
2052
|
+
yield* store.requeueOperationsFor(deviceId);
|
|
1658
2053
|
yield* store.markDeviceOffline(deviceId);
|
|
1659
2054
|
})).catch((cause) => console.error(cause));
|
|
1660
2055
|
});
|
|
@@ -1679,11 +2074,11 @@ Content-Length: 0\r
|
|
|
1679
2074
|
websocketServer.emit("connection", websocket, request);
|
|
1680
2075
|
});
|
|
1681
2076
|
});
|
|
1682
|
-
await new Promise((
|
|
2077
|
+
await new Promise((resolve4, reject) => {
|
|
1683
2078
|
server.once("error", reject);
|
|
1684
2079
|
server.listen(relayListenOptions(configuration), () => {
|
|
1685
2080
|
server.off("error", reject);
|
|
1686
|
-
|
|
2081
|
+
resolve4();
|
|
1687
2082
|
});
|
|
1688
2083
|
});
|
|
1689
2084
|
const address = server.address();
|
|
@@ -1698,7 +2093,7 @@ Content-Length: 0\r
|
|
|
1698
2093
|
for (const socket of websocketServer.clients) {
|
|
1699
2094
|
socket.close(1001, "Relay shutting down");
|
|
1700
2095
|
}
|
|
1701
|
-
await new Promise((
|
|
2096
|
+
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
1702
2097
|
websocketServer.close();
|
|
1703
2098
|
await runtime.dispose();
|
|
1704
2099
|
};
|
|
@@ -1710,9 +2105,9 @@ var init_main = __esm(async () => {
|
|
|
1710
2105
|
init_config();
|
|
1711
2106
|
init_hub();
|
|
1712
2107
|
init_store();
|
|
1713
|
-
RequestError = class RequestError extends
|
|
1714
|
-
status:
|
|
1715
|
-
message:
|
|
2108
|
+
RequestError = class RequestError extends Schema12.TaggedErrorClass()("Relay.RequestError", {
|
|
2109
|
+
status: Schema12.Int,
|
|
2110
|
+
message: Schema12.String
|
|
1716
2111
|
}) {
|
|
1717
2112
|
};
|
|
1718
2113
|
if (false) {}
|
|
@@ -1720,7 +2115,7 @@ var init_main = __esm(async () => {
|
|
|
1720
2115
|
|
|
1721
2116
|
// apps/device/src/main.ts
|
|
1722
2117
|
init_src();
|
|
1723
|
-
import { Effect as
|
|
2118
|
+
import { Effect as Effect13 } from "effect";
|
|
1724
2119
|
|
|
1725
2120
|
// packages/client/src/index.ts
|
|
1726
2121
|
init_src();
|
|
@@ -1840,7 +2235,11 @@ var make = (options) => {
|
|
|
1840
2235
|
body: JSON.stringify(input)
|
|
1841
2236
|
}),
|
|
1842
2237
|
authSessions: () => request("RelayClient.authSessions", "/api/auth/sessions", Schema2.Array(AuthSession)),
|
|
1843
|
-
revokeAuthSession: (sessionId) => request("RelayClient.revokeAuthSession", `/api/auth/sessions/${encodeURIComponent(sessionId)}/revoke`, AuthSession, { method: "POST" })
|
|
2238
|
+
revokeAuthSession: (sessionId) => request("RelayClient.revokeAuthSession", `/api/auth/sessions/${encodeURIComponent(sessionId)}/revoke`, AuthSession, { method: "POST" }),
|
|
2239
|
+
usage: () => request("RelayClient.usage", "/api/usage", UsageSummary),
|
|
2240
|
+
createUpgradeOperations: (input) => request("RelayClient.createUpgradeOperations", "/api/operations/upgrades", Schema2.Array(DeviceOperation), { method: "POST", body: JSON.stringify(input) }),
|
|
2241
|
+
operations: () => request("RelayClient.operations", "/api/operations", Schema2.Array(DeviceOperation)),
|
|
2242
|
+
abandonOperation: (operationId) => request("RelayClient.abandonOperation", `/api/operations/${encodeURIComponent(operationId)}/abandon`, DeviceOperation, { method: "POST" })
|
|
1844
2243
|
});
|
|
1845
2244
|
};
|
|
1846
2245
|
var exchangePairing = (baseUrl, input) => Effect.gen(function* () {
|
|
@@ -2326,10 +2725,10 @@ var run = (options) => Effect2.tryPromise({
|
|
|
2326
2725
|
});
|
|
2327
2726
|
|
|
2328
2727
|
// apps/device/src/cli.ts
|
|
2329
|
-
import { Effect as
|
|
2330
|
-
import { access as
|
|
2331
|
-
import { homedir as
|
|
2332
|
-
import { dirname as
|
|
2728
|
+
import { Effect as Effect7, Schema as Schema8 } from "effect";
|
|
2729
|
+
import { access as access4, mkdir as mkdir5, readFile as readFile4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
2730
|
+
import { homedir as homedir4 } from "node:os";
|
|
2731
|
+
import { dirname as dirname5, join as join6 } from "node:path";
|
|
2333
2732
|
|
|
2334
2733
|
// skills/cohall/SKILL.md
|
|
2335
2734
|
var SKILL_default = `---
|
|
@@ -2553,6 +2952,7 @@ var defaultRelayDataDirectory = () => {
|
|
|
2553
2952
|
}
|
|
2554
2953
|
return join2(process.env.XDG_DATA_HOME ?? join2(homedir2(), ".local", "share"), "cohall");
|
|
2555
2954
|
};
|
|
2955
|
+
var relayDataDirectory = () => resolve(process.env.COHALL_DATA_DIR ?? defaultRelayDataDirectory());
|
|
2556
2956
|
var configurationPath = () => resolve(process.env.COHALL_CONFIG ?? join2(defaultConfigDirectory(), "config.json"));
|
|
2557
2957
|
var normalizeRelayUrl = (input) => {
|
|
2558
2958
|
const url = new URL(input);
|
|
@@ -2684,7 +3084,7 @@ var loadClientConfiguration = Effect3.tryPromise({
|
|
|
2684
3084
|
var loadOwnerConfiguration = Effect3.tryPromise({
|
|
2685
3085
|
try: async () => {
|
|
2686
3086
|
const stored = await storedOrDefaults();
|
|
2687
|
-
const ownerTokenPath = join2(
|
|
3087
|
+
const ownerTokenPath = join2(relayDataDirectory(), "owner-token");
|
|
2688
3088
|
const token = process.env.COHALL_TOKEN ?? await readFile(ownerTokenPath, "utf8").then((value) => value.trim()).catch(() => {
|
|
2689
3089
|
return;
|
|
2690
3090
|
});
|
|
@@ -2825,6 +3225,176 @@ var threadContext = Effect4.fn("Cohall.threadContext")(function* (client, thread
|
|
|
2825
3225
|
return yield* client.threadContext(threadId);
|
|
2826
3226
|
});
|
|
2827
3227
|
|
|
3228
|
+
// apps/device/src/device-overview.ts
|
|
3229
|
+
var deviceVersions = (devices, localVersion) => ({
|
|
3230
|
+
local_version: localVersion,
|
|
3231
|
+
current: devices.filter((device) => device.version === localVersion).length,
|
|
3232
|
+
different: devices.filter((device) => device.version !== localVersion).length,
|
|
3233
|
+
devices: devices.map((device) => ({
|
|
3234
|
+
id: device.id,
|
|
3235
|
+
name: device.name,
|
|
3236
|
+
status: device.status,
|
|
3237
|
+
version: device.version,
|
|
3238
|
+
current: device.version === localVersion
|
|
3239
|
+
}))
|
|
3240
|
+
});
|
|
3241
|
+
var allDeviceHealth = (devices, localVersion) => {
|
|
3242
|
+
const summaries = devices.map((device) => {
|
|
3243
|
+
const warnings = [
|
|
3244
|
+
...device.status === "offline" ? ["Device is offline"] : [],
|
|
3245
|
+
...device.providers.length === 0 ? ["No provider executable is available"] : [],
|
|
3246
|
+
...device.version === localVersion ? [] : [`Running Cohall ${device.version}; local CLI is ${localVersion}`]
|
|
3247
|
+
];
|
|
3248
|
+
return {
|
|
3249
|
+
id: device.id,
|
|
3250
|
+
name: device.name,
|
|
3251
|
+
hostname: device.hostname,
|
|
3252
|
+
platform: device.platform,
|
|
3253
|
+
architecture: device.architecture,
|
|
3254
|
+
status: device.status,
|
|
3255
|
+
version: device.version,
|
|
3256
|
+
providers: device.providers,
|
|
3257
|
+
workspaces: device.workspaces.map((workspace) => workspace.path),
|
|
3258
|
+
last_seen_at: device.lastSeenAt,
|
|
3259
|
+
warnings
|
|
3260
|
+
};
|
|
3261
|
+
});
|
|
3262
|
+
return {
|
|
3263
|
+
healthy: summaries.length > 0 && summaries.every((device) => device.warnings.length === 0),
|
|
3264
|
+
total: devices.length,
|
|
3265
|
+
online: devices.filter((device) => device.status === "online").length,
|
|
3266
|
+
busy: devices.filter((device) => device.status === "busy").length,
|
|
3267
|
+
offline: devices.filter((device) => device.status === "offline").length,
|
|
3268
|
+
warnings: summaries.length === 0 ? ["No Cohall devices are registered"] : [],
|
|
3269
|
+
devices: summaries
|
|
3270
|
+
};
|
|
3271
|
+
};
|
|
3272
|
+
|
|
3273
|
+
// apps/device/src/setup.ts
|
|
3274
|
+
import { Effect as Effect5 } from "effect";
|
|
3275
|
+
import { hostname as hostname2 } from "node:os";
|
|
3276
|
+
import { Writable } from "node:stream";
|
|
3277
|
+
import { createInterface } from "node:readline/promises";
|
|
3278
|
+
var joinRelay = async (options) => {
|
|
3279
|
+
const draft = await makeStoredConfiguration({
|
|
3280
|
+
relayUrl: options.relayUrl,
|
|
3281
|
+
...options.deviceName === undefined ? {} : { deviceName: options.deviceName },
|
|
3282
|
+
...options.workspaces === undefined ? {} : { workspaces: options.workspaces },
|
|
3283
|
+
...options.providers === undefined ? {} : { providers: options.providers }
|
|
3284
|
+
});
|
|
3285
|
+
if (!options.clientOnly && draft.workspaces.length === 0) {
|
|
3286
|
+
throw new Error("At least one workspace is required when joining a device");
|
|
3287
|
+
}
|
|
3288
|
+
const paired = await Effect5.runPromise(exchangePairing(draft.relayUrl, { token: options.token }));
|
|
3289
|
+
const clientToken = paired.credentials.find((credential) => credential.session.role === "client")?.token;
|
|
3290
|
+
const deviceCredential = paired.credentials.find((credential) => credential.session.role === "device");
|
|
3291
|
+
if (options.clientOnly && clientToken === undefined) {
|
|
3292
|
+
throw new Error("The pairing credential did not include client access");
|
|
3293
|
+
}
|
|
3294
|
+
if (!options.clientOnly && deviceCredential === undefined) {
|
|
3295
|
+
throw new Error("The pairing credential did not include device access");
|
|
3296
|
+
}
|
|
3297
|
+
const {
|
|
3298
|
+
clientToken: _retainedClientToken,
|
|
3299
|
+
deviceToken: _retainedDeviceToken,
|
|
3300
|
+
...configurationDraft
|
|
3301
|
+
} = draft;
|
|
3302
|
+
const configuration = StoredConfiguration.make({
|
|
3303
|
+
...configurationDraft,
|
|
3304
|
+
deviceId: options.clientOnly ? configurationDraft.deviceId : deviceCredential?.session.deviceId ?? configurationDraft.deviceId,
|
|
3305
|
+
...clientToken === undefined ? {} : { clientToken },
|
|
3306
|
+
...options.clientOnly || deviceCredential === undefined ? {} : { deviceToken: deviceCredential.token }
|
|
3307
|
+
});
|
|
3308
|
+
await writeStoredConfiguration(configuration);
|
|
3309
|
+
return {
|
|
3310
|
+
configuration,
|
|
3311
|
+
roles: options.clientOnly ? paired.credentials.map((credential) => credential.session.role).filter((role) => role === "client") : paired.credentials.map((credential) => credential.session.role)
|
|
3312
|
+
};
|
|
3313
|
+
};
|
|
3314
|
+
var terminalPrompter = () => {
|
|
3315
|
+
let muted = false;
|
|
3316
|
+
const output = new Writable({
|
|
3317
|
+
write(chunk, _encoding, callback) {
|
|
3318
|
+
if (!muted) {
|
|
3319
|
+
process.stderr.write(chunk);
|
|
3320
|
+
}
|
|
3321
|
+
callback();
|
|
3322
|
+
}
|
|
3323
|
+
});
|
|
3324
|
+
const terminal = createInterface({ input: process.stdin, output, terminal: true });
|
|
3325
|
+
return {
|
|
3326
|
+
answer: async (label, fallback) => {
|
|
3327
|
+
const value = (await terminal.question(`${label} [${fallback}]: `)).trim();
|
|
3328
|
+
return value || fallback;
|
|
3329
|
+
},
|
|
3330
|
+
secret: async (label) => {
|
|
3331
|
+
const pending = terminal.question(`${label}: `);
|
|
3332
|
+
muted = true;
|
|
3333
|
+
try {
|
|
3334
|
+
return (await pending).trim();
|
|
3335
|
+
} finally {
|
|
3336
|
+
muted = false;
|
|
3337
|
+
process.stderr.write(`
|
|
3338
|
+
`);
|
|
3339
|
+
}
|
|
3340
|
+
},
|
|
3341
|
+
close: () => terminal.close()
|
|
3342
|
+
};
|
|
3343
|
+
};
|
|
3344
|
+
var guidedSetupInput = async (options, prompter) => {
|
|
3345
|
+
const existing = await readStoredConfiguration();
|
|
3346
|
+
const relayUrl = normalizeRelayUrl(options.relayUrl ?? await prompter.answer("Relay URL", existing?.relayUrl ?? "http://127.0.0.1:8787"));
|
|
3347
|
+
const credentials = credentialsForRelay(existing, relayUrl);
|
|
3348
|
+
const hasRequiredCredentials = credentials.clientToken !== undefined && (options.clientOnly || credentials.deviceToken !== undefined);
|
|
3349
|
+
const workspaces = options.clientOnly ? existing?.workspaces ?? [] : options.workspaces.length > 0 ? options.workspaces : [await prompter.answer("Workspace root", existing?.workspaces[0] ?? options.cwd)];
|
|
3350
|
+
const deviceName = options.clientOnly ? undefined : options.deviceName ?? await prompter.answer("Device name", existing?.deviceName ?? hostname2());
|
|
3351
|
+
const providers = options.providers ?? (options.clientOnly ? "auto" : await prompter.answer("Providers", existing?.providers?.join(",") ?? "auto"));
|
|
3352
|
+
const token = options.token ?? (hasRequiredCredentials ? undefined : await prompter.secret("Pairing token"));
|
|
3353
|
+
if (!hasRequiredCredentials && (token === undefined || token.length === 0)) {
|
|
3354
|
+
throw new Error("A pairing token is required");
|
|
3355
|
+
}
|
|
3356
|
+
if (token !== undefined && new TextEncoder().encode(token).byteLength > 256) {
|
|
3357
|
+
throw new Error("Pairing token exceeds 256 bytes");
|
|
3358
|
+
}
|
|
3359
|
+
return {
|
|
3360
|
+
relayUrl,
|
|
3361
|
+
...token === undefined ? {} : { token },
|
|
3362
|
+
...deviceName === undefined ? {} : { deviceName },
|
|
3363
|
+
workspaces,
|
|
3364
|
+
providers,
|
|
3365
|
+
reusedConfiguration: hasRequiredCredentials && token === undefined
|
|
3366
|
+
};
|
|
3367
|
+
};
|
|
3368
|
+
|
|
3369
|
+
// apps/device/src/relay-migration.ts
|
|
3370
|
+
init_src();
|
|
3371
|
+
import { Effect as Effect6, Schema as Schema7 } from "effect";
|
|
3372
|
+
import { createHash } from "node:crypto";
|
|
3373
|
+
import { createReadStream } from "node:fs";
|
|
3374
|
+
import {
|
|
3375
|
+
access as access3,
|
|
3376
|
+
chmod as chmod5,
|
|
3377
|
+
copyFile,
|
|
3378
|
+
lstat,
|
|
3379
|
+
mkdir as mkdir4,
|
|
3380
|
+
mkdtemp as mkdtemp2,
|
|
3381
|
+
readFile as readFile3,
|
|
3382
|
+
realpath as realpath4,
|
|
3383
|
+
rename as rename3,
|
|
3384
|
+
rm as rm3,
|
|
3385
|
+
stat as stat2,
|
|
3386
|
+
writeFile as writeFile5
|
|
3387
|
+
} from "node:fs/promises";
|
|
3388
|
+
import { basename, dirname as dirname4, join as join5, resolve as resolve2 } from "node:path";
|
|
3389
|
+
import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
|
|
3390
|
+
import { WebSocket } from "ws";
|
|
3391
|
+
|
|
3392
|
+
// apps/device/src/service.ts
|
|
3393
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
3394
|
+
import { chmod as chmod4, mkdir as mkdir3, realpath as realpath3, writeFile as writeFile4 } from "node:fs/promises";
|
|
3395
|
+
import { homedir as homedir3 } from "node:os";
|
|
3396
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
3397
|
+
|
|
2828
3398
|
// apps/device/src/upgrade.ts
|
|
2829
3399
|
import { Schema as Schema6 } from "effect";
|
|
2830
3400
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -3288,6 +3858,466 @@ var upgrade = async (options) => {
|
|
|
3288
3858
|
};
|
|
3289
3859
|
};
|
|
3290
3860
|
|
|
3861
|
+
// apps/device/src/service.ts
|
|
3862
|
+
var xml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3863
|
+
var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
3864
|
+
var deviceServicePlan = (options) => {
|
|
3865
|
+
if (options.platform === "linux") {
|
|
3866
|
+
const path = join4(options.home, ".config", "systemd", "user", "cohall-device.service");
|
|
3867
|
+
return {
|
|
3868
|
+
file: {
|
|
3869
|
+
path,
|
|
3870
|
+
mode: 384,
|
|
3871
|
+
content: `[Unit]
|
|
3872
|
+
Description=Cohall device agent
|
|
3873
|
+
After=network-online.target
|
|
3874
|
+
Wants=network-online.target
|
|
3875
|
+
|
|
3876
|
+
[Service]
|
|
3877
|
+
Type=simple
|
|
3878
|
+
Environment=PATH=%h/.local/bin:%h/.npm-global/bin:%h/.bun/bin:%h/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
|
|
3879
|
+
ExecStart=${systemdArgument(options.entrypoint)} device
|
|
3880
|
+
Restart=always
|
|
3881
|
+
RestartSec=3
|
|
3882
|
+
UMask=0077
|
|
3883
|
+
NoNewPrivileges=true
|
|
3884
|
+
PrivateTmp=true
|
|
3885
|
+
|
|
3886
|
+
[Install]
|
|
3887
|
+
WantedBy=default.target
|
|
3888
|
+
`
|
|
3889
|
+
},
|
|
3890
|
+
commands: [
|
|
3891
|
+
{ command: "systemctl", arguments: ["--user", "daemon-reload"] },
|
|
3892
|
+
{ command: "systemctl", arguments: ["--user", "enable", "--now", "cohall-device.service"] }
|
|
3893
|
+
],
|
|
3894
|
+
note: "The device starts after login. Run loginctl enable-linger $USER if it must run before login."
|
|
3895
|
+
};
|
|
3896
|
+
}
|
|
3897
|
+
if (options.platform === "darwin") {
|
|
3898
|
+
const label = "com.cohall.device";
|
|
3899
|
+
const path = join4(options.home, "Library", "LaunchAgents", `${label}.plist`);
|
|
3900
|
+
const domain = `gui/${options.uid ?? 0}`;
|
|
3901
|
+
return {
|
|
3902
|
+
file: {
|
|
3903
|
+
path,
|
|
3904
|
+
mode: 384,
|
|
3905
|
+
content: `<?xml version="1.0" encoding="UTF-8"?>
|
|
3906
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3907
|
+
<plist version="1.0">
|
|
3908
|
+
<dict>
|
|
3909
|
+
<key>Label</key>
|
|
3910
|
+
<string>${label}</string>
|
|
3911
|
+
<key>ProgramArguments</key>
|
|
3912
|
+
<array>
|
|
3913
|
+
<string>${xml(options.entrypoint)}</string>
|
|
3914
|
+
<string>device</string>
|
|
3915
|
+
</array>
|
|
3916
|
+
<key>EnvironmentVariables</key>
|
|
3917
|
+
<dict>
|
|
3918
|
+
<key>PATH</key>
|
|
3919
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
3920
|
+
</dict>
|
|
3921
|
+
<key>RunAtLoad</key>
|
|
3922
|
+
<true/>
|
|
3923
|
+
<key>KeepAlive</key>
|
|
3924
|
+
<dict>
|
|
3925
|
+
<key>NetworkState</key>
|
|
3926
|
+
<true/>
|
|
3927
|
+
<key>SuccessfulExit</key>
|
|
3928
|
+
<false/>
|
|
3929
|
+
</dict>
|
|
3930
|
+
<key>ThrottleInterval</key>
|
|
3931
|
+
<integer>3</integer>
|
|
3932
|
+
<key>ProcessType</key>
|
|
3933
|
+
<string>Background</string>
|
|
3934
|
+
</dict>
|
|
3935
|
+
</plist>
|
|
3936
|
+
`
|
|
3937
|
+
},
|
|
3938
|
+
commands: [
|
|
3939
|
+
{ command: "launchctl", arguments: ["bootout", domain, path] },
|
|
3940
|
+
{ command: "launchctl", arguments: ["bootstrap", domain, path] },
|
|
3941
|
+
{ command: "launchctl", arguments: ["kickstart", "-k", `${domain}/${label}`] }
|
|
3942
|
+
]
|
|
3943
|
+
};
|
|
3944
|
+
}
|
|
3945
|
+
throw new Error("Automatic device service installation supports Linux and macOS");
|
|
3946
|
+
};
|
|
3947
|
+
var defaultRunner2 = {
|
|
3948
|
+
run: async (command, arguments_, timeoutMs = 60000) => {
|
|
3949
|
+
const executable = await trustedExecutable(command);
|
|
3950
|
+
return new Promise((resolve2) => {
|
|
3951
|
+
execFile3(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => resolve2({
|
|
3952
|
+
exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
|
|
3953
|
+
stdout,
|
|
3954
|
+
stderr,
|
|
3955
|
+
...error === null ? {} : { error: error.message }
|
|
3956
|
+
}));
|
|
3957
|
+
});
|
|
3958
|
+
}
|
|
3959
|
+
};
|
|
3960
|
+
var checked2 = async (runner, command, arguments_, allowFailure = false) => {
|
|
3961
|
+
const result = await runner.run(command, arguments_, 60000);
|
|
3962
|
+
if (result.exitCode === 0 || allowFailure) {
|
|
3963
|
+
return result;
|
|
3964
|
+
}
|
|
3965
|
+
throw new Error(`${command} failed: ${result.stderr.trim() || result.stdout.trim() || result.error || `status ${result.exitCode}`}`);
|
|
3966
|
+
};
|
|
3967
|
+
var installDeviceService = async (options = {}) => {
|
|
3968
|
+
const entrypoint = await realpath3(options.entrypoint ?? process.argv[1] ?? "");
|
|
3969
|
+
packageInstallation(entrypoint);
|
|
3970
|
+
const platform3 = options.platform ?? process.platform;
|
|
3971
|
+
const runner = options.runner ?? defaultRunner2;
|
|
3972
|
+
if (platform3 === "win32") {
|
|
3973
|
+
const script = join4(dirname3(dirname3(entrypoint)), "deploy", "windows", "install-device.ps1");
|
|
3974
|
+
await checked2(runner, "powershell.exe", [
|
|
3975
|
+
"-NoProfile",
|
|
3976
|
+
"-ExecutionPolicy",
|
|
3977
|
+
"Bypass",
|
|
3978
|
+
"-File",
|
|
3979
|
+
script
|
|
3980
|
+
]);
|
|
3981
|
+
return { installed: "scheduled-task:Cohall Device" };
|
|
3982
|
+
}
|
|
3983
|
+
const uid = options.uid ?? process.getuid?.();
|
|
3984
|
+
const plan = deviceServicePlan({
|
|
3985
|
+
platform: platform3,
|
|
3986
|
+
entrypoint,
|
|
3987
|
+
home: options.home ?? homedir3(),
|
|
3988
|
+
...uid === undefined ? {} : { uid }
|
|
3989
|
+
});
|
|
3990
|
+
await mkdir3(dirname3(plan.file.path), { recursive: true, mode: 448 });
|
|
3991
|
+
await writeFile4(plan.file.path, plan.file.content, { mode: plan.file.mode });
|
|
3992
|
+
await chmod4(plan.file.path, plan.file.mode);
|
|
3993
|
+
for (const [index, command] of plan.commands.entries()) {
|
|
3994
|
+
await checked2(runner, command.command, command.arguments, platform3 === "darwin" && index === 0);
|
|
3995
|
+
}
|
|
3996
|
+
return {
|
|
3997
|
+
installed: plan.file.path,
|
|
3998
|
+
...plan.note === undefined ? {} : { note: plan.note }
|
|
3999
|
+
};
|
|
4000
|
+
};
|
|
4001
|
+
var restartDeviceService = async (options = {}) => {
|
|
4002
|
+
const platform3 = options.platform ?? process.platform;
|
|
4003
|
+
const runner = options.runner ?? defaultRunner2;
|
|
4004
|
+
if (platform3 === "linux") {
|
|
4005
|
+
const service = "cohall-device.service";
|
|
4006
|
+
const check = await runner.run("systemctl", ["--user", "is-active", "--quiet", service], 1e4);
|
|
4007
|
+
if (check.exitCode !== 0) {
|
|
4008
|
+
return { running: false, restarted: false };
|
|
4009
|
+
}
|
|
4010
|
+
await checked2(runner, "systemctl", ["--user", "restart", service]);
|
|
4011
|
+
return { running: true, restarted: true, service };
|
|
4012
|
+
}
|
|
4013
|
+
if (platform3 === "darwin") {
|
|
4014
|
+
const service = "com.cohall.device";
|
|
4015
|
+
const target = `gui/${options.uid ?? process.getuid?.() ?? 0}/${service}`;
|
|
4016
|
+
const check = await runner.run("launchctl", ["print", target], 1e4);
|
|
4017
|
+
if (check.exitCode !== 0) {
|
|
4018
|
+
return { running: false, restarted: false };
|
|
4019
|
+
}
|
|
4020
|
+
await checked2(runner, "launchctl", ["kickstart", "-k", target]);
|
|
4021
|
+
return { running: true, restarted: true, service };
|
|
4022
|
+
}
|
|
4023
|
+
if (platform3 === "win32") {
|
|
4024
|
+
const service = "Cohall Device";
|
|
4025
|
+
const check = await runner.run("schtasks.exe", ["/Query", "/TN", service], 1e4);
|
|
4026
|
+
if (check.exitCode !== 0) {
|
|
4027
|
+
return { running: false, restarted: false };
|
|
4028
|
+
}
|
|
4029
|
+
await checked2(runner, "schtasks.exe", ["/End", "/TN", service], true);
|
|
4030
|
+
await checked2(runner, "schtasks.exe", ["/Run", "/TN", service]);
|
|
4031
|
+
return { running: true, restarted: true, service };
|
|
4032
|
+
}
|
|
4033
|
+
return { running: false, restarted: false };
|
|
4034
|
+
};
|
|
4035
|
+
|
|
4036
|
+
// apps/device/src/relay-migration.ts
|
|
4037
|
+
var sha256 = Schema7.String.check(Schema7.isPattern(/^[a-f0-9]{64}$/, { expected: "a lowercase SHA-256 digest" }));
|
|
4038
|
+
var RelayBackupManifest = Schema7.Struct({
|
|
4039
|
+
formatVersion: Schema7.Literal(1),
|
|
4040
|
+
cohallVersion: Schema7.NonEmptyString,
|
|
4041
|
+
createdAt: Timestamp,
|
|
4042
|
+
databaseSha256: sha256,
|
|
4043
|
+
ownerTokenSha256: sha256
|
|
4044
|
+
});
|
|
4045
|
+
var exists = (path) => access3(path).then(() => true).catch(() => false);
|
|
4046
|
+
var digest = async (path) => {
|
|
4047
|
+
const hash = createHash("sha256");
|
|
4048
|
+
for await (const chunk of createReadStream(path)) {
|
|
4049
|
+
hash.update(chunk);
|
|
4050
|
+
}
|
|
4051
|
+
return hash.digest("hex");
|
|
4052
|
+
};
|
|
4053
|
+
var verifyBackupMember = async (path, maximumBytes) => {
|
|
4054
|
+
const metadata = await lstat(path);
|
|
4055
|
+
if (!metadata.isFile()) {
|
|
4056
|
+
throw new Error(`Relay backup member must be a regular file: ${path}`);
|
|
4057
|
+
}
|
|
4058
|
+
if (maximumBytes !== undefined && metadata.size > maximumBytes) {
|
|
4059
|
+
throw new Error(`Relay backup member exceeds ${maximumBytes} bytes: ${path}`);
|
|
4060
|
+
}
|
|
4061
|
+
};
|
|
4062
|
+
var readManifest = async (path) => {
|
|
4063
|
+
await verifyBackupMember(path, 16384);
|
|
4064
|
+
return Schema7.decodeUnknownSync(RelayBackupManifest)(JSON.parse(await readFile3(path, "utf8")));
|
|
4065
|
+
};
|
|
4066
|
+
var verifyBackupDestinationParent = async (path) => {
|
|
4067
|
+
if (process.platform === "win32") {
|
|
4068
|
+
return;
|
|
4069
|
+
}
|
|
4070
|
+
for (let current = await realpath4(path);; current = dirname4(current)) {
|
|
4071
|
+
const metadata = await stat2(current);
|
|
4072
|
+
const writableByOthers = (metadata.mode & 18) !== 0;
|
|
4073
|
+
const sticky = (metadata.mode & 512) !== 0;
|
|
4074
|
+
if (writableByOthers && !sticky) {
|
|
4075
|
+
throw new Error(`Backup destination ancestors must not be group- or world-writable unless they use the sticky bit: ${current}`);
|
|
4076
|
+
}
|
|
4077
|
+
const parent = dirname4(current);
|
|
4078
|
+
if (parent === current) {
|
|
4079
|
+
return;
|
|
4080
|
+
}
|
|
4081
|
+
}
|
|
4082
|
+
};
|
|
4083
|
+
var ownerToken = async (dataDirectory) => {
|
|
4084
|
+
const path = join5(dataDirectory, "owner-token");
|
|
4085
|
+
const configured = process.env.COHALL_TOKEN;
|
|
4086
|
+
if (configured !== undefined) {
|
|
4087
|
+
return configured;
|
|
4088
|
+
}
|
|
4089
|
+
if (!await exists(path)) {
|
|
4090
|
+
throw new Error(`Relay owner token was not found at ${path}`);
|
|
4091
|
+
}
|
|
4092
|
+
if ((await stat2(path)).size > 512) {
|
|
4093
|
+
throw new Error("Relay owner token file exceeds 512 bytes");
|
|
4094
|
+
}
|
|
4095
|
+
return (await readFile3(path, "utf8")).trim();
|
|
4096
|
+
};
|
|
4097
|
+
var verifyDatabase = (path) => {
|
|
4098
|
+
const database = new DatabaseSync(path, { readOnly: true });
|
|
4099
|
+
try {
|
|
4100
|
+
const result = database.prepare("PRAGMA quick_check").get();
|
|
4101
|
+
if (result === undefined || Object.values(result)[0] !== "ok") {
|
|
4102
|
+
throw new Error(`SQLite integrity check failed for ${path}`);
|
|
4103
|
+
}
|
|
4104
|
+
} finally {
|
|
4105
|
+
database.close();
|
|
4106
|
+
}
|
|
4107
|
+
};
|
|
4108
|
+
var backupRelay = async (destinationInput) => {
|
|
4109
|
+
const sourceDirectory = relayDataDirectory();
|
|
4110
|
+
const sourceDatabase = join5(sourceDirectory, "cohall.db");
|
|
4111
|
+
if (!await exists(sourceDatabase)) {
|
|
4112
|
+
throw new Error(`Relay database was not found at ${sourceDatabase}`);
|
|
4113
|
+
}
|
|
4114
|
+
const destination = resolve2(destinationInput);
|
|
4115
|
+
if (await exists(destination)) {
|
|
4116
|
+
throw new Error(`Backup destination already exists: ${destination}`);
|
|
4117
|
+
}
|
|
4118
|
+
await verifyBackupDestinationParent(dirname4(destination));
|
|
4119
|
+
await mkdir4(destination, { mode: 448 });
|
|
4120
|
+
try {
|
|
4121
|
+
const databasePath = join5(destination, "cohall.db");
|
|
4122
|
+
const ownerTokenPath = join5(destination, "owner-token");
|
|
4123
|
+
const database = new DatabaseSync(sourceDatabase, { readOnly: true });
|
|
4124
|
+
try {
|
|
4125
|
+
await sqliteBackup(database, databasePath);
|
|
4126
|
+
} finally {
|
|
4127
|
+
database.close();
|
|
4128
|
+
}
|
|
4129
|
+
verifyDatabase(databasePath);
|
|
4130
|
+
const token = await ownerToken(sourceDirectory);
|
|
4131
|
+
if (token.length < 32 || new TextEncoder().encode(token).byteLength > 512) {
|
|
4132
|
+
throw new Error("The relay owner token must contain between 32 and 512 bytes");
|
|
4133
|
+
}
|
|
4134
|
+
await writeFile5(ownerTokenPath, `${token}
|
|
4135
|
+
`, { mode: 384 });
|
|
4136
|
+
await Promise.all([chmod5(databasePath, 384), chmod5(ownerTokenPath, 384)]);
|
|
4137
|
+
const createdAt = Timestamp.make(new Date().toISOString());
|
|
4138
|
+
const manifest = RelayBackupManifest.make({
|
|
4139
|
+
formatVersion: 1,
|
|
4140
|
+
cohallVersion: version,
|
|
4141
|
+
createdAt,
|
|
4142
|
+
databaseSha256: await digest(databasePath),
|
|
4143
|
+
ownerTokenSha256: await digest(ownerTokenPath)
|
|
4144
|
+
});
|
|
4145
|
+
await writeFile5(join5(destination, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
4146
|
+
`, {
|
|
4147
|
+
mode: 384
|
|
4148
|
+
});
|
|
4149
|
+
return {
|
|
4150
|
+
backup_directory: destination,
|
|
4151
|
+
database: databasePath,
|
|
4152
|
+
created_at: createdAt,
|
|
4153
|
+
warning: "This backup contains relay credentials and task history. Transfer and store it privately."
|
|
4154
|
+
};
|
|
4155
|
+
} catch (cause) {
|
|
4156
|
+
await rm3(destination, { recursive: true, force: true });
|
|
4157
|
+
throw cause;
|
|
4158
|
+
}
|
|
4159
|
+
};
|
|
4160
|
+
var restoreRelay = async (sourceInput) => {
|
|
4161
|
+
const source = resolve2(sourceInput);
|
|
4162
|
+
const sourceDatabase = join5(source, "cohall.db");
|
|
4163
|
+
const sourceOwnerToken = join5(source, "owner-token");
|
|
4164
|
+
const sourceManifest = join5(source, "manifest.json");
|
|
4165
|
+
for (const path of [sourceDatabase, sourceOwnerToken, sourceManifest]) {
|
|
4166
|
+
if (!await exists(path)) {
|
|
4167
|
+
throw new Error(`Relay backup is missing ${basename(path)}`);
|
|
4168
|
+
}
|
|
4169
|
+
}
|
|
4170
|
+
await Promise.all([
|
|
4171
|
+
verifyBackupMember(sourceDatabase),
|
|
4172
|
+
verifyBackupMember(sourceOwnerToken, 1024),
|
|
4173
|
+
verifyBackupMember(sourceManifest, 16384)
|
|
4174
|
+
]);
|
|
4175
|
+
const target = relayDataDirectory();
|
|
4176
|
+
if (await exists(target)) {
|
|
4177
|
+
throw new Error(`Relay data directory already exists: ${target}. Restore into a new COHALL_DATA_DIR.`);
|
|
4178
|
+
}
|
|
4179
|
+
const parent = dirname4(target);
|
|
4180
|
+
await mkdir4(parent, { recursive: true, mode: 448 });
|
|
4181
|
+
const staging = await mkdtemp2(join5(parent, `.${basename(target)}.restore-`));
|
|
4182
|
+
let backupVersion = "";
|
|
4183
|
+
try {
|
|
4184
|
+
await chmod5(staging, 448);
|
|
4185
|
+
const databasePath = join5(staging, "cohall.db");
|
|
4186
|
+
const ownerTokenPath = join5(staging, "owner-token");
|
|
4187
|
+
const manifestPath = join5(staging, "migration-manifest.json");
|
|
4188
|
+
await Promise.all([
|
|
4189
|
+
copyFile(sourceDatabase, databasePath),
|
|
4190
|
+
copyFile(sourceOwnerToken, ownerTokenPath),
|
|
4191
|
+
copyFile(sourceManifest, manifestPath)
|
|
4192
|
+
]);
|
|
4193
|
+
await Promise.all([
|
|
4194
|
+
chmod5(databasePath, 384),
|
|
4195
|
+
chmod5(ownerTokenPath, 384),
|
|
4196
|
+
chmod5(manifestPath, 384)
|
|
4197
|
+
]);
|
|
4198
|
+
const manifest = await readManifest(manifestPath);
|
|
4199
|
+
if (await digest(databasePath) !== manifest.databaseSha256 || await digest(ownerTokenPath) !== manifest.ownerTokenSha256) {
|
|
4200
|
+
throw new Error("Relay backup checksum verification failed");
|
|
4201
|
+
}
|
|
4202
|
+
verifyDatabase(databasePath);
|
|
4203
|
+
const token = (await readFile3(ownerTokenPath, "utf8")).trim();
|
|
4204
|
+
if (token.length < 32 || new TextEncoder().encode(token).byteLength > 512) {
|
|
4205
|
+
throw new Error("Relay backup contains an invalid owner token");
|
|
4206
|
+
}
|
|
4207
|
+
backupVersion = manifest.cohallVersion;
|
|
4208
|
+
await rename3(staging, target);
|
|
4209
|
+
} catch (cause) {
|
|
4210
|
+
await rm3(staging, { recursive: true, force: true });
|
|
4211
|
+
throw cause;
|
|
4212
|
+
}
|
|
4213
|
+
return {
|
|
4214
|
+
restored: true,
|
|
4215
|
+
data_directory: target,
|
|
4216
|
+
backup_version: backupVersion,
|
|
4217
|
+
next: "Start the relay at its new address, then run `cohall relay use <new-url>` on each client and device."
|
|
4218
|
+
};
|
|
4219
|
+
};
|
|
4220
|
+
var websocketUrl = (relayUrl) => {
|
|
4221
|
+
const url = new URL(relayUrl);
|
|
4222
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
4223
|
+
url.pathname = "/ws/device";
|
|
4224
|
+
return url.toString();
|
|
4225
|
+
};
|
|
4226
|
+
var isLoopback = (url) => url.hostname === "localhost" || url.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
|
|
4227
|
+
var verifyClientCredential = async (relayUrl, token) => {
|
|
4228
|
+
await Effect6.runPromise(exports_src.make({ baseUrl: relayUrl, token }).devices());
|
|
4229
|
+
};
|
|
4230
|
+
var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, reject) => {
|
|
4231
|
+
const socket = new WebSocket(websocketUrl(relayUrl), {
|
|
4232
|
+
handshakeTimeout: 1e4,
|
|
4233
|
+
maxPayload: 1024 * 1024,
|
|
4234
|
+
perMessageDeflate: false
|
|
4235
|
+
});
|
|
4236
|
+
let settled = false;
|
|
4237
|
+
const finish = (error) => {
|
|
4238
|
+
if (settled) {
|
|
4239
|
+
return;
|
|
4240
|
+
}
|
|
4241
|
+
settled = true;
|
|
4242
|
+
clearTimeout(timeout);
|
|
4243
|
+
socket.terminate();
|
|
4244
|
+
if (error === undefined) {
|
|
4245
|
+
resolvePromise();
|
|
4246
|
+
return;
|
|
4247
|
+
}
|
|
4248
|
+
reject(error);
|
|
4249
|
+
};
|
|
4250
|
+
const timeout = setTimeout(() => finish(new Error("Timed out while verifying the device credential")), 1e4);
|
|
4251
|
+
socket.once("open", () => {
|
|
4252
|
+
socket.send(JSON.stringify(SocketEvent.make({ _tag: "Authenticate", token })));
|
|
4253
|
+
});
|
|
4254
|
+
socket.on("message", (data) => {
|
|
4255
|
+
try {
|
|
4256
|
+
const event = Schema7.decodeUnknownSync(SocketEvent)(JSON.parse(data.toString()));
|
|
4257
|
+
if (event._tag === "Connected") {
|
|
4258
|
+
finish();
|
|
4259
|
+
} else if (event._tag === "Error") {
|
|
4260
|
+
finish(new Error(event.message));
|
|
4261
|
+
}
|
|
4262
|
+
} catch (cause) {
|
|
4263
|
+
finish(new Error(`New relay returned an invalid device response: ${String(cause)}`));
|
|
4264
|
+
}
|
|
4265
|
+
});
|
|
4266
|
+
socket.once("error", (cause) => finish(cause));
|
|
4267
|
+
socket.once("close", () => finish(new Error("New relay rejected the stored device credential")));
|
|
4268
|
+
});
|
|
4269
|
+
var switchRelay = async (options) => {
|
|
4270
|
+
if (process.env.COHALL_RELAY_URL !== undefined) {
|
|
4271
|
+
throw new Error("Unset COHALL_RELAY_URL before changing the stored relay URL");
|
|
4272
|
+
}
|
|
4273
|
+
const configuration = await readStoredConfiguration();
|
|
4274
|
+
if (configuration === undefined) {
|
|
4275
|
+
throw new Error("This user has no stored Cohall configuration");
|
|
4276
|
+
}
|
|
4277
|
+
const relayUrl = normalizeRelayUrl(options.relayUrl);
|
|
4278
|
+
const parsedRelayUrl = new URL(relayUrl);
|
|
4279
|
+
if (parsedRelayUrl.protocol === "http:" && !isLoopback(parsedRelayUrl) && options.allowHttp !== true) {
|
|
4280
|
+
throw new Error("Refusing to send stored credentials over remote HTTP. Use HTTPS, or --allow-http only on an independently encrypted private network.");
|
|
4281
|
+
}
|
|
4282
|
+
const clientTokens = [configuration.clientToken, process.env.COHALL_CLIENT_TOKEN].filter((token) => token !== undefined);
|
|
4283
|
+
const deviceTokens = [configuration.deviceToken, process.env.COHALL_DEVICE_TOKEN].filter((token) => token !== undefined);
|
|
4284
|
+
if (clientTokens.length === 0 && deviceTokens.length === 0) {
|
|
4285
|
+
throw new Error("This configuration has no client or device credentials to verify");
|
|
4286
|
+
}
|
|
4287
|
+
const roles = [];
|
|
4288
|
+
if (clientTokens.length > 0) {
|
|
4289
|
+
for (const token of new Set(clientTokens)) {
|
|
4290
|
+
await (options.verifyClient ?? verifyClientCredential)(relayUrl, token);
|
|
4291
|
+
}
|
|
4292
|
+
roles.push("client");
|
|
4293
|
+
}
|
|
4294
|
+
if (deviceTokens.length > 0) {
|
|
4295
|
+
for (const token of new Set(deviceTokens)) {
|
|
4296
|
+
await (options.verifyDevice ?? verifyDeviceCredential)(relayUrl, token);
|
|
4297
|
+
}
|
|
4298
|
+
roles.push("device");
|
|
4299
|
+
}
|
|
4300
|
+
const updated = configuration.relayUrl !== relayUrl;
|
|
4301
|
+
if (updated) {
|
|
4302
|
+
await writeStoredConfiguration(StoredConfiguration.make({ ...configuration, relayUrl }));
|
|
4303
|
+
}
|
|
4304
|
+
let service = { running: false, restarted: false };
|
|
4305
|
+
let warning;
|
|
4306
|
+
if (updated && options.restart && deviceTokens.length > 0) {
|
|
4307
|
+
service = await (options.restartService ?? restartDeviceService)().catch((cause) => {
|
|
4308
|
+
warning = `Relay URL changed, but the device service could not restart: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
4309
|
+
return { running: true, restarted: false };
|
|
4310
|
+
});
|
|
4311
|
+
}
|
|
4312
|
+
return {
|
|
4313
|
+
updated,
|
|
4314
|
+
relay_url: relayUrl,
|
|
4315
|
+
verified_roles: roles,
|
|
4316
|
+
service,
|
|
4317
|
+
...warning === undefined ? {} : { warning }
|
|
4318
|
+
};
|
|
4319
|
+
};
|
|
4320
|
+
|
|
3291
4321
|
// apps/device/src/cli.ts
|
|
3292
4322
|
var valueOptions = new Set([
|
|
3293
4323
|
"context",
|
|
@@ -3308,7 +4338,16 @@ var valueOptions = new Set([
|
|
|
3308
4338
|
"to",
|
|
3309
4339
|
"workspace"
|
|
3310
4340
|
]);
|
|
3311
|
-
var flagOptions = new Set([
|
|
4341
|
+
var flagOptions = new Set([
|
|
4342
|
+
"all",
|
|
4343
|
+
"allow-http",
|
|
4344
|
+
"client-only",
|
|
4345
|
+
"dry-run",
|
|
4346
|
+
"follow",
|
|
4347
|
+
"no-restart",
|
|
4348
|
+
"no-wait",
|
|
4349
|
+
"service"
|
|
4350
|
+
]);
|
|
3312
4351
|
var aliases = new Map([
|
|
3313
4352
|
["-c", "context"],
|
|
3314
4353
|
["-p", "prompt"],
|
|
@@ -3319,6 +4358,7 @@ var aliases = new Map([
|
|
|
3319
4358
|
var help = `Cohall connects agents running on your own devices.
|
|
3320
4359
|
|
|
3321
4360
|
Usage:
|
|
4361
|
+
cohall init [--relay url] [--workspace path] [--providers list] [--service]
|
|
3322
4362
|
cohall join --relay <url> --workspace <path> [--providers list] [--token-file <path>]
|
|
3323
4363
|
cohall configure [--relay url] [--name name] [--workspace path]
|
|
3324
4364
|
[--providers codex,opencode|auto] [--model id]
|
|
@@ -3336,10 +4376,18 @@ Usage:
|
|
|
3336
4376
|
cohall sessions
|
|
3337
4377
|
cohall revoke <session-id>
|
|
3338
4378
|
cohall forget <device-id>
|
|
3339
|
-
cohall doctor
|
|
4379
|
+
cohall doctor [--all]
|
|
4380
|
+
cohall versions
|
|
4381
|
+
cohall usage
|
|
3340
4382
|
cohall upgrade [--to version|latest] [--no-restart] [--dry-run]
|
|
4383
|
+
cohall upgrade --all [--to version|latest] [--no-restart] [--dry-run]
|
|
4384
|
+
cohall upgrades [abandon <operation-id>]
|
|
4385
|
+
cohall service install
|
|
3341
4386
|
cohall device
|
|
3342
4387
|
cohall relay
|
|
4388
|
+
cohall relay backup <directory>
|
|
4389
|
+
cohall relay restore <directory>
|
|
4390
|
+
cohall relay use <url> [--no-restart] [--allow-http]
|
|
3343
4391
|
cohall mcp
|
|
3344
4392
|
cohall skill [install [agents|claude|opencode|all]]
|
|
3345
4393
|
cohall integrations
|
|
@@ -3434,13 +4482,13 @@ var readInput = async (arguments_, name) => {
|
|
|
3434
4482
|
throw new Error(`Use either --${name} or --${name}-file, not both`);
|
|
3435
4483
|
}
|
|
3436
4484
|
if (path !== undefined) {
|
|
3437
|
-
if (!await
|
|
4485
|
+
if (!await access4(path).then(() => true).catch(() => false)) {
|
|
3438
4486
|
throw new Error(`File does not exist: ${path}`);
|
|
3439
4487
|
}
|
|
3440
|
-
if ((await
|
|
4488
|
+
if ((await stat3(path)).size > 131072) {
|
|
3441
4489
|
throw new Error(`${name} file exceeds 128 KiB`);
|
|
3442
4490
|
}
|
|
3443
|
-
return
|
|
4491
|
+
return readFile4(path, "utf8");
|
|
3444
4492
|
}
|
|
3445
4493
|
const result = direct === "-" ? await readStdin(131072, name) : direct;
|
|
3446
4494
|
if (result !== undefined && result.length > 131072) {
|
|
@@ -3470,27 +4518,27 @@ var timeout = (arguments_) => {
|
|
|
3470
4518
|
};
|
|
3471
4519
|
var pairingToken = async (arguments_) => {
|
|
3472
4520
|
const path = option(arguments_, "token-file");
|
|
3473
|
-
if (path !== undefined && !await
|
|
4521
|
+
if (path !== undefined && !await access4(path).then(() => true).catch(() => false)) {
|
|
3474
4522
|
throw new Error(`Pairing token file does not exist: ${path}`);
|
|
3475
4523
|
}
|
|
3476
|
-
if (path !== undefined && (await
|
|
4524
|
+
if (path !== undefined && (await stat3(path)).size > 512) {
|
|
3477
4525
|
throw new Error("Pairing token file exceeds 512 bytes");
|
|
3478
4526
|
}
|
|
3479
|
-
const token = (path === undefined ? await readStdin(512, "Pairing token") : await
|
|
4527
|
+
const token = (path === undefined ? await readStdin(512, "Pairing token") : await readFile4(path, "utf8")).trim();
|
|
3480
4528
|
if (token.length === 0 || new TextEncoder().encode(token).byteLength > 256) {
|
|
3481
4529
|
throw new Error("Provide a pairing token on stdin or with --token-file");
|
|
3482
4530
|
}
|
|
3483
4531
|
return token;
|
|
3484
4532
|
};
|
|
3485
4533
|
var client = async () => {
|
|
3486
|
-
const configuration = await
|
|
4534
|
+
const configuration = await Effect7.runPromise(loadClientConfiguration);
|
|
3487
4535
|
return {
|
|
3488
4536
|
configuration,
|
|
3489
4537
|
relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
|
|
3490
4538
|
};
|
|
3491
4539
|
};
|
|
3492
4540
|
var ownerClient = async () => {
|
|
3493
|
-
const configuration = await
|
|
4541
|
+
const configuration = await Effect7.runPromise(loadOwnerConfiguration);
|
|
3494
4542
|
return {
|
|
3495
4543
|
configuration,
|
|
3496
4544
|
relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
|
|
@@ -3498,17 +4546,17 @@ var ownerClient = async () => {
|
|
|
3498
4546
|
};
|
|
3499
4547
|
var installSkill = async (target) => {
|
|
3500
4548
|
const roots = {
|
|
3501
|
-
agents:
|
|
3502
|
-
claude:
|
|
3503
|
-
opencode:
|
|
4549
|
+
agents: join6(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
|
|
4550
|
+
claude: join6(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
|
|
4551
|
+
opencode: join6(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
|
|
3504
4552
|
};
|
|
3505
4553
|
const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
|
|
3506
4554
|
if (selected === undefined) {
|
|
3507
4555
|
throw new Error("Skill target must be agents, claude, opencode, or all");
|
|
3508
4556
|
}
|
|
3509
4557
|
for (const path of selected) {
|
|
3510
|
-
await
|
|
3511
|
-
await
|
|
4558
|
+
await mkdir5(dirname5(path), { recursive: true });
|
|
4559
|
+
await writeFile6(path, SKILL_default);
|
|
3512
4560
|
}
|
|
3513
4561
|
return selected;
|
|
3514
4562
|
};
|
|
@@ -3516,10 +4564,151 @@ var printHelp = () => console.log(help);
|
|
|
3516
4564
|
var printSkill = () => console.log(SKILL_default.trimEnd());
|
|
3517
4565
|
var runCli = async (command, raw) => {
|
|
3518
4566
|
const arguments_ = parseArguments(raw);
|
|
4567
|
+
if (command === "init") {
|
|
4568
|
+
allowOptions(arguments_, [
|
|
4569
|
+
"client-only",
|
|
4570
|
+
"name",
|
|
4571
|
+
"providers",
|
|
4572
|
+
"relay",
|
|
4573
|
+
"service",
|
|
4574
|
+
"token-file",
|
|
4575
|
+
"workspace"
|
|
4576
|
+
]);
|
|
4577
|
+
noPositionals(arguments_, command);
|
|
4578
|
+
const token = option(arguments_, "token-file") === undefined ? undefined : await pairingToken(arguments_);
|
|
4579
|
+
const interactive = process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
4580
|
+
const relayUrl = option(arguments_, "relay");
|
|
4581
|
+
const deviceName = option(arguments_, "name");
|
|
4582
|
+
const providerInput = option(arguments_, "providers");
|
|
4583
|
+
const prompter = interactive ? terminalPrompter() : {
|
|
4584
|
+
answer: (_label, fallback) => Promise.resolve(fallback),
|
|
4585
|
+
secret: () => pairingToken(arguments_),
|
|
4586
|
+
close: () => {
|
|
4587
|
+
return;
|
|
4588
|
+
}
|
|
4589
|
+
};
|
|
4590
|
+
try {
|
|
4591
|
+
const input = await guidedSetupInput({
|
|
4592
|
+
clientOnly: arguments_.options.has("client-only"),
|
|
4593
|
+
workspaces: values(arguments_, "workspace"),
|
|
4594
|
+
cwd: process.cwd(),
|
|
4595
|
+
...relayUrl === undefined ? {} : { relayUrl },
|
|
4596
|
+
...token === undefined ? {} : { token },
|
|
4597
|
+
...deviceName === undefined ? {} : { deviceName },
|
|
4598
|
+
...providerInput === undefined ? {} : { providers: providerInput }
|
|
4599
|
+
}, prompter);
|
|
4600
|
+
const providers = input.providers === "auto" ? "auto" : parseProviders(input.providers);
|
|
4601
|
+
const existing = await readStoredConfiguration();
|
|
4602
|
+
const result = input.reusedConfiguration ? {
|
|
4603
|
+
configuration: await makeStoredConfiguration({
|
|
4604
|
+
relayUrl: input.relayUrl,
|
|
4605
|
+
...input.deviceName === undefined ? {} : { deviceName: input.deviceName },
|
|
4606
|
+
workspaces: input.workspaces,
|
|
4607
|
+
providers
|
|
4608
|
+
}),
|
|
4609
|
+
roles: [
|
|
4610
|
+
...existing?.clientToken === undefined ? [] : ["client"],
|
|
4611
|
+
...arguments_.options.has("client-only") || existing?.deviceToken === undefined ? [] : ["device"]
|
|
4612
|
+
]
|
|
4613
|
+
} : await joinRelay({
|
|
4614
|
+
relayUrl: input.relayUrl,
|
|
4615
|
+
token: input.token ?? "",
|
|
4616
|
+
clientOnly: arguments_.options.has("client-only"),
|
|
4617
|
+
...input.deviceName === undefined ? {} : { deviceName: input.deviceName },
|
|
4618
|
+
workspaces: input.workspaces,
|
|
4619
|
+
providers
|
|
4620
|
+
});
|
|
4621
|
+
if (input.reusedConfiguration) {
|
|
4622
|
+
await writeStoredConfiguration(result.configuration);
|
|
4623
|
+
}
|
|
4624
|
+
const installedSkills = await installSkill("all");
|
|
4625
|
+
const service = arguments_.options.has("service") ? await installDeviceService() : undefined;
|
|
4626
|
+
print({
|
|
4627
|
+
initialized: true,
|
|
4628
|
+
reused_configuration: input.reusedConfiguration,
|
|
4629
|
+
config_path: configurationPath(),
|
|
4630
|
+
relay_url: result.configuration.relayUrl,
|
|
4631
|
+
device_id: result.configuration.deviceId,
|
|
4632
|
+
roles: result.roles,
|
|
4633
|
+
workspaces: result.configuration.workspaces,
|
|
4634
|
+
skills: installedSkills,
|
|
4635
|
+
...service === undefined ? {} : { service },
|
|
4636
|
+
next: service === undefined && !arguments_.options.has("client-only") ? "Run `cohall service install` for background availability, or `cohall device` now." : "Run `cohall doctor` to verify this device."
|
|
4637
|
+
});
|
|
4638
|
+
} finally {
|
|
4639
|
+
prompter.close();
|
|
4640
|
+
}
|
|
4641
|
+
return;
|
|
4642
|
+
}
|
|
4643
|
+
if (command === "service") {
|
|
4644
|
+
allowOptions(arguments_, []);
|
|
4645
|
+
if (arguments_.positionals.length !== 1 || arguments_.positionals[0] !== "install") {
|
|
4646
|
+
throw new Error("Usage: cohall service install");
|
|
4647
|
+
}
|
|
4648
|
+
print(await installDeviceService());
|
|
4649
|
+
return;
|
|
4650
|
+
}
|
|
4651
|
+
if (command === "relay") {
|
|
4652
|
+
const action = arguments_.positionals[0];
|
|
4653
|
+
if (action === "backup") {
|
|
4654
|
+
allowOptions(arguments_, []);
|
|
4655
|
+
if (arguments_.positionals.length !== 2) {
|
|
4656
|
+
throw new Error("Usage: cohall relay backup <directory>");
|
|
4657
|
+
}
|
|
4658
|
+
print(await backupRelay(arguments_.positionals[1] ?? ""));
|
|
4659
|
+
return;
|
|
4660
|
+
}
|
|
4661
|
+
if (action === "restore") {
|
|
4662
|
+
allowOptions(arguments_, []);
|
|
4663
|
+
if (arguments_.positionals.length !== 2) {
|
|
4664
|
+
throw new Error("Usage: cohall relay restore <directory>");
|
|
4665
|
+
}
|
|
4666
|
+
print(await restoreRelay(arguments_.positionals[1] ?? ""));
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
if (action === "use") {
|
|
4670
|
+
allowOptions(arguments_, ["allow-http", "no-restart"]);
|
|
4671
|
+
if (arguments_.positionals.length !== 2) {
|
|
4672
|
+
throw new Error("Usage: cohall relay use <url> [--no-restart] [--allow-http]");
|
|
4673
|
+
}
|
|
4674
|
+
print(await switchRelay({
|
|
4675
|
+
relayUrl: arguments_.positionals[1] ?? "",
|
|
4676
|
+
restart: !arguments_.options.has("no-restart"),
|
|
4677
|
+
allowHttp: arguments_.options.has("allow-http")
|
|
4678
|
+
}));
|
|
4679
|
+
return;
|
|
4680
|
+
}
|
|
4681
|
+
throw new Error("Usage: cohall relay [backup <directory>|restore <directory>|use <url> [--no-restart] [--allow-http]]");
|
|
4682
|
+
}
|
|
3519
4683
|
if (command === "upgrade") {
|
|
3520
|
-
allowOptions(arguments_, ["dry-run", "no-restart", "to"]);
|
|
4684
|
+
allowOptions(arguments_, ["all", "dry-run", "no-restart", "to"]);
|
|
3521
4685
|
noPositionals(arguments_, command);
|
|
3522
4686
|
const target = option(arguments_, "to");
|
|
4687
|
+
if (arguments_.options.has("all")) {
|
|
4688
|
+
const normalizedTarget = normalizeUpgradeTarget(target);
|
|
4689
|
+
const { relay: relay2 } = await ownerClient();
|
|
4690
|
+
const restart = !arguments_.options.has("no-restart");
|
|
4691
|
+
if (arguments_.options.has("dry-run")) {
|
|
4692
|
+
const devices = await Effect7.runPromise(relay2.devices());
|
|
4693
|
+
assertDeviceOperationSupport(devices);
|
|
4694
|
+
print({
|
|
4695
|
+
dry_run: true,
|
|
4696
|
+
requested_version: normalizedTarget,
|
|
4697
|
+
restart,
|
|
4698
|
+
devices: devices.map((device) => ({ id: device.id, name: device.name }))
|
|
4699
|
+
});
|
|
4700
|
+
return;
|
|
4701
|
+
}
|
|
4702
|
+
const operations = await Effect7.runPromise(relay2.createUpgradeOperations({ target: normalizedTarget, restart }));
|
|
4703
|
+
print({
|
|
4704
|
+
queued: operations.length,
|
|
4705
|
+
requested_version: normalizedTarget,
|
|
4706
|
+
restart,
|
|
4707
|
+
operations,
|
|
4708
|
+
next: "Run `cohall upgrades` to inspect progress."
|
|
4709
|
+
});
|
|
4710
|
+
return;
|
|
4711
|
+
}
|
|
3523
4712
|
print(await upgrade({
|
|
3524
4713
|
currentVersion: version,
|
|
3525
4714
|
...target === undefined ? {} : { target },
|
|
@@ -3529,6 +4718,19 @@ var runCli = async (command, raw) => {
|
|
|
3529
4718
|
}));
|
|
3530
4719
|
return;
|
|
3531
4720
|
}
|
|
4721
|
+
if (command === "upgrades") {
|
|
4722
|
+
allowOptions(arguments_, []);
|
|
4723
|
+
const { relay: relay2 } = await ownerClient();
|
|
4724
|
+
if (arguments_.positionals.length > 0) {
|
|
4725
|
+
if (arguments_.positionals.length !== 2 || arguments_.positionals[0] !== "abandon") {
|
|
4726
|
+
throw new Error("Usage: cohall upgrades [abandon <operation-id>]");
|
|
4727
|
+
}
|
|
4728
|
+
print(await Effect7.runPromise(relay2.abandonOperation(Schema8.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
|
|
4729
|
+
return;
|
|
4730
|
+
}
|
|
4731
|
+
print(await Effect7.runPromise(relay2.operations()));
|
|
4732
|
+
return;
|
|
4733
|
+
}
|
|
3532
4734
|
if (command === "join") {
|
|
3533
4735
|
allowOptions(arguments_, [
|
|
3534
4736
|
"client-only",
|
|
@@ -3550,41 +4752,21 @@ var runCli = async (command, raw) => {
|
|
|
3550
4752
|
if (!arguments_.options.has("client-only") && (workspaces?.length ?? 0) === 0) {
|
|
3551
4753
|
throw new Error("At least one --workspace is required when joining a device");
|
|
3552
4754
|
}
|
|
3553
|
-
const
|
|
4755
|
+
const clientOnly = arguments_.options.has("client-only");
|
|
4756
|
+
const { configuration: configuration2, roles } = await joinRelay({
|
|
3554
4757
|
relayUrl,
|
|
4758
|
+
token,
|
|
4759
|
+
clientOnly,
|
|
3555
4760
|
...deviceName === undefined ? {} : { deviceName },
|
|
3556
4761
|
...workspaces === undefined ? {} : { workspaces },
|
|
3557
4762
|
...providers === undefined ? {} : { providers }
|
|
3558
4763
|
});
|
|
3559
|
-
const paired = await Effect5.runPromise(exchangePairing(relayUrl, { token }));
|
|
3560
|
-
const clientToken = paired.credentials.find((credential) => credential.session.role === "client")?.token;
|
|
3561
|
-
const deviceToken = paired.credentials.find((credential) => credential.session.role === "device")?.token;
|
|
3562
|
-
const deviceId = paired.credentials.find((credential) => credential.session.role === "device")?.session.deviceId;
|
|
3563
|
-
const clientOnly = arguments_.options.has("client-only");
|
|
3564
|
-
if (clientOnly && clientToken === undefined) {
|
|
3565
|
-
throw new Error("The pairing credential did not include client access");
|
|
3566
|
-
}
|
|
3567
|
-
if (!clientOnly && deviceToken === undefined) {
|
|
3568
|
-
throw new Error("The pairing credential did not include device access");
|
|
3569
|
-
}
|
|
3570
|
-
const {
|
|
3571
|
-
clientToken: _retainedClientToken,
|
|
3572
|
-
deviceToken: _retainedDeviceToken,
|
|
3573
|
-
...configurationDraft
|
|
3574
|
-
} = draft;
|
|
3575
|
-
const configuration2 = StoredConfiguration.make({
|
|
3576
|
-
...configurationDraft,
|
|
3577
|
-
deviceId: clientOnly ? configurationDraft.deviceId : deviceId ?? configurationDraft.deviceId,
|
|
3578
|
-
...clientToken === undefined ? {} : { clientToken },
|
|
3579
|
-
...clientOnly || deviceToken === undefined ? {} : { deviceToken }
|
|
3580
|
-
});
|
|
3581
|
-
await writeStoredConfiguration(configuration2);
|
|
3582
4764
|
print({
|
|
3583
4765
|
configured: true,
|
|
3584
4766
|
config_path: configurationPath(),
|
|
3585
4767
|
relay_url: configuration2.relayUrl,
|
|
3586
4768
|
device_id: configuration2.deviceId,
|
|
3587
|
-
roles
|
|
4769
|
+
roles,
|
|
3588
4770
|
workspaces: configuration2.workspaces
|
|
3589
4771
|
});
|
|
3590
4772
|
return;
|
|
@@ -3616,7 +4798,7 @@ var runCli = async (command, raw) => {
|
|
|
3616
4798
|
const existing = await readStoredConfiguration();
|
|
3617
4799
|
const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl ?? "http://127.0.0.1:8787");
|
|
3618
4800
|
const supplied = values(arguments_, "workspace");
|
|
3619
|
-
const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await
|
|
4801
|
+
const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect7.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
|
|
3620
4802
|
const sandbox = option(arguments_, "sandbox");
|
|
3621
4803
|
const model = option(arguments_, "model") ?? existing?.model;
|
|
3622
4804
|
const providerInput = option(arguments_, "providers");
|
|
@@ -3631,7 +4813,7 @@ var runCli = async (command, raw) => {
|
|
|
3631
4813
|
...providers === undefined ? {} : { providers },
|
|
3632
4814
|
...model === undefined ? {} : { model },
|
|
3633
4815
|
...(sandbox ?? existing?.sandbox) === undefined ? {} : {
|
|
3634
|
-
sandbox:
|
|
4816
|
+
sandbox: Schema8.decodeUnknownSync(Schema8.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
|
|
3635
4817
|
}
|
|
3636
4818
|
});
|
|
3637
4819
|
await writeStoredConfiguration(configuration2);
|
|
@@ -3670,8 +4852,13 @@ var runCli = async (command, raw) => {
|
|
|
3670
4852
|
return;
|
|
3671
4853
|
}
|
|
3672
4854
|
if (command === "doctor") {
|
|
3673
|
-
allowOptions(arguments_, []);
|
|
4855
|
+
allowOptions(arguments_, ["all"]);
|
|
3674
4856
|
noPositionals(arguments_, command);
|
|
4857
|
+
if (arguments_.options.has("all")) {
|
|
4858
|
+
const { relay: relay2 } = await client();
|
|
4859
|
+
print(allDeviceHealth(await Effect7.runPromise(relay2.devices()), version));
|
|
4860
|
+
return;
|
|
4861
|
+
}
|
|
3675
4862
|
const configuration2 = await readStoredConfiguration();
|
|
3676
4863
|
const relayUrl = normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? configuration2?.relayUrl ?? "http://127.0.0.1:8787");
|
|
3677
4864
|
const storedCredentials = credentialsForRelay(configuration2, relayUrl);
|
|
@@ -3689,7 +4876,7 @@ var runCli = async (command, raw) => {
|
|
|
3689
4876
|
const clientToken = process.env.COHALL_CLIENT_TOKEN ?? storedCredentials.clientToken;
|
|
3690
4877
|
const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
|
|
3691
4878
|
const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
|
|
3692
|
-
const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await
|
|
4879
|
+
const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await Effect7.runPromise(exports_src.make({ baseUrl: relayUrl, token: clientToken }).devices()).then((devices) => devices.find((device) => device.id === deviceId)).catch(() => {
|
|
3693
4880
|
return;
|
|
3694
4881
|
});
|
|
3695
4882
|
const versionWarning = deviceVersionWarning(version, currentDevice?.version);
|
|
@@ -3718,13 +4905,30 @@ var runCli = async (command, raw) => {
|
|
|
3718
4905
|
});
|
|
3719
4906
|
return;
|
|
3720
4907
|
}
|
|
4908
|
+
if (command === "versions") {
|
|
4909
|
+
allowOptions(arguments_, []);
|
|
4910
|
+
noPositionals(arguments_, command);
|
|
4911
|
+
const { relay: relay2 } = await client();
|
|
4912
|
+
print(deviceVersions(await Effect7.runPromise(relay2.devices()), version));
|
|
4913
|
+
return;
|
|
4914
|
+
}
|
|
4915
|
+
if (command === "usage") {
|
|
4916
|
+
allowOptions(arguments_, []);
|
|
4917
|
+
noPositionals(arguments_, command);
|
|
4918
|
+
const { relay: relay2 } = await client();
|
|
4919
|
+
print({
|
|
4920
|
+
...await Effect7.runPromise(relay2.usage()),
|
|
4921
|
+
scope: "Retained Cohall task activity; provider token and billing usage are not available."
|
|
4922
|
+
});
|
|
4923
|
+
return;
|
|
4924
|
+
}
|
|
3721
4925
|
if (command === "pair" || command === "sessions" || command === "revoke" || command === "forget") {
|
|
3722
4926
|
const { configuration: configuration2, relay: relay2 } = await ownerClient();
|
|
3723
4927
|
if (command === "pair") {
|
|
3724
4928
|
allowOptions(arguments_, ["client-only", "label"]);
|
|
3725
4929
|
noPositionals(arguments_, command);
|
|
3726
4930
|
const roles = arguments_.options.has("client-only") ? ["client"] : ["client", "device"];
|
|
3727
|
-
const credential = await
|
|
4931
|
+
const credential = await Effect7.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
|
|
3728
4932
|
print({
|
|
3729
4933
|
relay_url: configuration2.relayUrl,
|
|
3730
4934
|
pairing_token: credential.token,
|
|
@@ -3736,23 +4940,23 @@ var runCli = async (command, raw) => {
|
|
|
3736
4940
|
if (command === "sessions") {
|
|
3737
4941
|
allowOptions(arguments_, []);
|
|
3738
4942
|
noPositionals(arguments_, command);
|
|
3739
|
-
print(await
|
|
4943
|
+
print(await Effect7.runPromise(relay2.authSessions()));
|
|
3740
4944
|
return;
|
|
3741
4945
|
}
|
|
3742
4946
|
if (command === "forget") {
|
|
3743
4947
|
allowOptions(arguments_, []);
|
|
3744
|
-
print(await
|
|
4948
|
+
print(await Effect7.runPromise(relay2.forgetDevice(Schema8.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
|
|
3745
4949
|
return;
|
|
3746
4950
|
}
|
|
3747
4951
|
allowOptions(arguments_, []);
|
|
3748
|
-
print(await
|
|
4952
|
+
print(await Effect7.runPromise(relay2.revokeAuthSession(Schema8.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
|
|
3749
4953
|
return;
|
|
3750
4954
|
}
|
|
3751
4955
|
const { configuration, relay } = await client();
|
|
3752
4956
|
if (command === "devices") {
|
|
3753
4957
|
allowOptions(arguments_, []);
|
|
3754
4958
|
noPositionals(arguments_, command);
|
|
3755
|
-
print(await
|
|
4959
|
+
print(await Effect7.runPromise(relay.devices()));
|
|
3756
4960
|
return;
|
|
3757
4961
|
}
|
|
3758
4962
|
if (command === "delegate") {
|
|
@@ -3784,50 +4988,50 @@ var runCli = async (command, raw) => {
|
|
|
3784
4988
|
const thread = option(arguments_, "thread");
|
|
3785
4989
|
const workspace = option(arguments_, "workspace");
|
|
3786
4990
|
const provider = option(arguments_, "provider");
|
|
3787
|
-
const task = await
|
|
4991
|
+
const task = await Effect7.runPromise(createDelegation(relay, configuration, {
|
|
3788
4992
|
prompt,
|
|
3789
4993
|
...target === undefined ? {} : { target },
|
|
3790
4994
|
...context === undefined ? {} : { context },
|
|
3791
|
-
...thread === undefined ? {} : { threadId:
|
|
4995
|
+
...thread === undefined ? {} : { threadId: Schema8.decodeUnknownSync(ThreadId)(thread) },
|
|
3792
4996
|
...workspace === undefined ? {} : { workspace },
|
|
3793
|
-
...provider === undefined ? {} : { provider:
|
|
4997
|
+
...provider === undefined ? {} : { provider: Schema8.decodeUnknownSync(Provider)(provider) }
|
|
3794
4998
|
}));
|
|
3795
|
-
print(taskResult(arguments_.options.has("no-wait") ? task : await
|
|
4999
|
+
print(taskResult(arguments_.options.has("no-wait") ? task : await Effect7.runPromise(waitForTask(relay, task, timeout(arguments_)))));
|
|
3796
5000
|
return;
|
|
3797
5001
|
}
|
|
3798
5002
|
if (command === "status") {
|
|
3799
5003
|
allowOptions(arguments_, []);
|
|
3800
|
-
const id =
|
|
3801
|
-
print(taskResult(await
|
|
5004
|
+
const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5005
|
+
print(taskResult(await Effect7.runPromise(relay.getTask(id))));
|
|
3802
5006
|
return;
|
|
3803
5007
|
}
|
|
3804
5008
|
if (command === "trace") {
|
|
3805
5009
|
allowOptions(arguments_, ["follow"]);
|
|
3806
|
-
const id =
|
|
5010
|
+
const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
3807
5011
|
if (arguments_.options.has("follow")) {
|
|
3808
|
-
await
|
|
5012
|
+
await Effect7.runPromise(followTaskTrace(relay, id, printLine));
|
|
3809
5013
|
return;
|
|
3810
5014
|
}
|
|
3811
|
-
print(await
|
|
5015
|
+
print(await Effect7.runPromise(relay.traceTask(id)));
|
|
3812
5016
|
return;
|
|
3813
5017
|
}
|
|
3814
5018
|
if (command === "wait") {
|
|
3815
5019
|
allowOptions(arguments_, ["timeout"]);
|
|
3816
|
-
const id =
|
|
3817
|
-
const task = await
|
|
3818
|
-
print(taskResult(await
|
|
5020
|
+
const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5021
|
+
const task = await Effect7.runPromise(relay.getTask(id));
|
|
5022
|
+
print(taskResult(await Effect7.runPromise(waitForTask(relay, task, timeout(arguments_)))));
|
|
3819
5023
|
return;
|
|
3820
5024
|
}
|
|
3821
5025
|
if (command === "cancel") {
|
|
3822
5026
|
allowOptions(arguments_, []);
|
|
3823
|
-
const id =
|
|
3824
|
-
print(taskResult(await
|
|
5027
|
+
const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
5028
|
+
print(taskResult(await Effect7.runPromise(relay.cancelTask(id))));
|
|
3825
5029
|
return;
|
|
3826
5030
|
}
|
|
3827
5031
|
if (command === "thread") {
|
|
3828
5032
|
allowOptions(arguments_, []);
|
|
3829
|
-
const id =
|
|
3830
|
-
print(await
|
|
5033
|
+
const id = Schema8.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
|
|
5034
|
+
print(await Effect7.runPromise(threadContext(relay, id)));
|
|
3831
5035
|
return;
|
|
3832
5036
|
}
|
|
3833
5037
|
throw new Error(`Unknown Cohall command: ${command}`);
|
|
@@ -3835,15 +5039,15 @@ var runCli = async (command, raw) => {
|
|
|
3835
5039
|
|
|
3836
5040
|
// apps/device/src/daemon.ts
|
|
3837
5041
|
init_src();
|
|
3838
|
-
import { Effect as
|
|
5042
|
+
import { Effect as Effect8, Schedule as Schedule2, Schema as Schema9 } from "effect";
|
|
3839
5043
|
import { constants as constants3 } from "node:fs";
|
|
3840
|
-
import { open, realpath as
|
|
3841
|
-
import { arch, hostname as
|
|
3842
|
-
import { basename, isAbsolute as isAbsolute3, relative } from "node:path";
|
|
3843
|
-
import { WebSocket } from "ws";
|
|
5044
|
+
import { open, realpath as realpath5, stat as stat4 } from "node:fs/promises";
|
|
5045
|
+
import { arch, hostname as hostname3, platform as platform3 } from "node:os";
|
|
5046
|
+
import { basename as basename2, isAbsolute as isAbsolute3, relative } from "node:path";
|
|
5047
|
+
import { WebSocket as WebSocket2 } from "ws";
|
|
3844
5048
|
var maxQueuedRelayMessages = 8;
|
|
3845
5049
|
|
|
3846
|
-
class DeviceConnectionError extends
|
|
5050
|
+
class DeviceConnectionError extends Schema9.TaggedErrorClass()("Device.ConnectionError", { message: Schema9.String }) {
|
|
3847
5051
|
}
|
|
3848
5052
|
var socketUrl = (configuration) => {
|
|
3849
5053
|
const url = new URL(configuration.relayUrl);
|
|
@@ -3887,7 +5091,7 @@ var describeDevice = (configuration, status = "online") => {
|
|
|
3887
5091
|
return Device.make({
|
|
3888
5092
|
id: configuration.id,
|
|
3889
5093
|
name: configuration.name,
|
|
3890
|
-
hostname:
|
|
5094
|
+
hostname: hostname3(),
|
|
3891
5095
|
platform: platformName,
|
|
3892
5096
|
architecture: arch(),
|
|
3893
5097
|
status,
|
|
@@ -3895,7 +5099,7 @@ var describeDevice = (configuration, status = "online") => {
|
|
|
3895
5099
|
capabilities: capabilities(providers),
|
|
3896
5100
|
workspaces: configuration.workspaces.map((path) => ({
|
|
3897
5101
|
path,
|
|
3898
|
-
label:
|
|
5102
|
+
label: basename2(path) || path
|
|
3899
5103
|
})),
|
|
3900
5104
|
version,
|
|
3901
5105
|
lastSeenAt: now()
|
|
@@ -3906,7 +5110,7 @@ var allowedWorkspace = async (configuration, requested) => {
|
|
|
3906
5110
|
if (selected === undefined) {
|
|
3907
5111
|
throw new Error("No workspace is configured on this device");
|
|
3908
5112
|
}
|
|
3909
|
-
const candidate = await
|
|
5113
|
+
const candidate = await realpath5(selected);
|
|
3910
5114
|
const allowed = configuration.workspaces.some((root) => {
|
|
3911
5115
|
const child = relative(root, candidate);
|
|
3912
5116
|
return child === "" || !child.startsWith("..") && !isAbsolute3(child);
|
|
@@ -3935,8 +5139,8 @@ var openAllowedWorkspace = async (configuration, requested) => {
|
|
|
3935
5139
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
3936
5140
|
}
|
|
3937
5141
|
if (operatingSystem2 !== "linux") {
|
|
3938
|
-
const currentPath = await
|
|
3939
|
-
const currentPathIdentity = await
|
|
5142
|
+
const currentPath = await realpath5(path);
|
|
5143
|
+
const currentPathIdentity = await stat4(currentPath);
|
|
3940
5144
|
if (currentPathIdentity.dev !== identity.dev || currentPathIdentity.ino !== identity.ino) {
|
|
3941
5145
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
3942
5146
|
}
|
|
@@ -3962,14 +5166,21 @@ ${task.prompt}${context}`
|
|
|
3962
5166
|
`);
|
|
3963
5167
|
};
|
|
3964
5168
|
var send = (state, event) => {
|
|
3965
|
-
if (state.socket?.readyState ===
|
|
5169
|
+
if (state.socket?.readyState === WebSocket2.OPEN) {
|
|
3966
5170
|
state.socket.send(JSON.stringify(event));
|
|
3967
5171
|
}
|
|
3968
5172
|
};
|
|
3969
5173
|
var sendTerminal = (state, taskId, event) => {
|
|
3970
5174
|
const payload = JSON.stringify(event);
|
|
3971
5175
|
state.terminal.set(taskId, payload);
|
|
3972
|
-
if (state.socket?.readyState ===
|
|
5176
|
+
if (state.socket?.readyState === WebSocket2.OPEN) {
|
|
5177
|
+
state.socket.send(payload);
|
|
5178
|
+
}
|
|
5179
|
+
};
|
|
5180
|
+
var sendOperationTerminal = (state, operationId, event) => {
|
|
5181
|
+
const payload = JSON.stringify(event);
|
|
5182
|
+
state.operationTerminal.set(operationId, payload);
|
|
5183
|
+
if (state.socket?.readyState === WebSocket2.OPEN) {
|
|
3973
5184
|
state.socket.send(payload);
|
|
3974
5185
|
}
|
|
3975
5186
|
};
|
|
@@ -3984,6 +5195,17 @@ var remember = (state, taskId) => {
|
|
|
3984
5195
|
state.terminal.delete(oldest.value);
|
|
3985
5196
|
}
|
|
3986
5197
|
};
|
|
5198
|
+
var rememberOperation = (state, operationId) => {
|
|
5199
|
+
state.completedOperations.add(operationId);
|
|
5200
|
+
if (state.completedOperations.size <= 100) {
|
|
5201
|
+
return;
|
|
5202
|
+
}
|
|
5203
|
+
const oldest = state.completedOperations.values().next();
|
|
5204
|
+
if (!oldest.done) {
|
|
5205
|
+
state.completedOperations.delete(oldest.value);
|
|
5206
|
+
state.operationTerminal.delete(oldest.value);
|
|
5207
|
+
}
|
|
5208
|
+
};
|
|
3987
5209
|
var sessionKey = (task) => `${task.threadId}:${task.provider}`;
|
|
3988
5210
|
var execute = (configuration, state, task) => {
|
|
3989
5211
|
if (state.tasks.has(task.id) || state.completed.has(task.id)) {
|
|
@@ -3992,8 +5214,8 @@ var execute = (configuration, state, task) => {
|
|
|
3992
5214
|
const controller = new AbortController;
|
|
3993
5215
|
state.tasks.set(task.id, controller);
|
|
3994
5216
|
send(state, SocketEvent.make({ _tag: "TaskAccepted", taskId: task.id }));
|
|
3995
|
-
const workflow =
|
|
3996
|
-
const workspace = yield*
|
|
5217
|
+
const workflow = Effect8.gen(function* () {
|
|
5218
|
+
const workspace = yield* Effect8.tryPromise({
|
|
3997
5219
|
try: () => openAllowedWorkspace(configuration, task.workspace),
|
|
3998
5220
|
catch: (cause) => new ProviderRunError({
|
|
3999
5221
|
provider: task.provider,
|
|
@@ -4010,11 +5232,11 @@ var execute = (configuration, state, task) => {
|
|
|
4010
5232
|
...sessionId === undefined ? {} : { sessionId },
|
|
4011
5233
|
...configuration.model === undefined ? {} : { model: configuration.model },
|
|
4012
5234
|
...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
|
|
4013
|
-
}).pipe(
|
|
5235
|
+
}).pipe(Effect8.ensuring(Effect8.promise(() => workspace.close().catch(() => {
|
|
4014
5236
|
return;
|
|
4015
5237
|
}))));
|
|
4016
5238
|
});
|
|
4017
|
-
|
|
5239
|
+
Effect8.runPromise(workflow, { signal: controller.signal }).then((result) => {
|
|
4018
5240
|
if (result.sessionId !== undefined) {
|
|
4019
5241
|
state.sessions.set(sessionKey(task), result.sessionId);
|
|
4020
5242
|
}
|
|
@@ -4034,12 +5256,71 @@ var execute = (configuration, state, task) => {
|
|
|
4034
5256
|
}));
|
|
4035
5257
|
}).finally(() => {
|
|
4036
5258
|
state.tasks.delete(task.id);
|
|
5259
|
+
const operation = state.operationQueue.shift();
|
|
5260
|
+
if (operation !== undefined) {
|
|
5261
|
+
executeOperation(configuration, state, operation);
|
|
5262
|
+
return;
|
|
5263
|
+
}
|
|
5264
|
+
const next = state.queue.shift();
|
|
5265
|
+
if (next !== undefined && state.operation === undefined) {
|
|
5266
|
+
execute(configuration, state, next);
|
|
5267
|
+
}
|
|
5268
|
+
});
|
|
5269
|
+
};
|
|
5270
|
+
var executeOperation = (configuration, state, operation) => {
|
|
5271
|
+
if (state.operation !== undefined || state.completedOperations.has(operation.id)) {
|
|
5272
|
+
return;
|
|
5273
|
+
}
|
|
5274
|
+
state.operation = operation;
|
|
5275
|
+
send(state, SocketEvent.make({ _tag: "OperationAccepted", operationId: operation.id }));
|
|
5276
|
+
performDeviceOperation(operation, version).then((result) => {
|
|
5277
|
+
rememberOperation(state, operation.id);
|
|
5278
|
+
sendOperationTerminal(state, operation.id, SocketEvent.make({
|
|
5279
|
+
_tag: "OperationFinished",
|
|
5280
|
+
operationId: operation.id,
|
|
5281
|
+
result
|
|
5282
|
+
}));
|
|
5283
|
+
}).catch((cause) => {
|
|
5284
|
+
rememberOperation(state, operation.id);
|
|
5285
|
+
sendOperationTerminal(state, operation.id, SocketEvent.make({
|
|
5286
|
+
_tag: "OperationFailed",
|
|
5287
|
+
operationId: operation.id,
|
|
5288
|
+
error: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
|
|
5289
|
+
}));
|
|
5290
|
+
}).finally(() => {
|
|
5291
|
+
state.operation = undefined;
|
|
5292
|
+
const pending = state.operationQueue.shift();
|
|
5293
|
+
if (pending !== undefined) {
|
|
5294
|
+
executeOperation(configuration, state, pending);
|
|
5295
|
+
return;
|
|
5296
|
+
}
|
|
4037
5297
|
const next = state.queue.shift();
|
|
4038
|
-
if (next !== undefined) {
|
|
5298
|
+
if (next !== undefined && state.tasks.size === 0) {
|
|
4039
5299
|
execute(configuration, state, next);
|
|
4040
5300
|
}
|
|
4041
5301
|
});
|
|
4042
5302
|
};
|
|
5303
|
+
var scheduleOperation = (configuration, state, operation) => {
|
|
5304
|
+
if (state.completedOperations.has(operation.id) || state.operation?.id === operation.id || state.operationQueue.some((pending) => pending.id === operation.id)) {
|
|
5305
|
+
return;
|
|
5306
|
+
}
|
|
5307
|
+
if (state.tasks.size > 0 || state.operation !== undefined) {
|
|
5308
|
+
if (state.operationQueue.length >= 10) {
|
|
5309
|
+
state.socket?.close(4008, "Operation queue limit reached");
|
|
5310
|
+
return;
|
|
5311
|
+
}
|
|
5312
|
+
state.operationQueue.push(operation);
|
|
5313
|
+
return;
|
|
5314
|
+
}
|
|
5315
|
+
executeOperation(configuration, state, operation);
|
|
5316
|
+
};
|
|
5317
|
+
var performDeviceOperation = (operation, currentVersion, runUpgrade = upgrade) => runUpgrade({
|
|
5318
|
+
currentVersion,
|
|
5319
|
+
target: operation.requestedVersion,
|
|
5320
|
+
restart: operation.restart,
|
|
5321
|
+
dryRun: false,
|
|
5322
|
+
delegated: true
|
|
5323
|
+
}).then((result) => JSON.stringify(result));
|
|
4043
5324
|
var schedule = (configuration, state, task) => {
|
|
4044
5325
|
if (state.tasks.has(task.id) || state.completed.has(task.id) || state.queue.some((queued) => queued.id === task.id)) {
|
|
4045
5326
|
return;
|
|
@@ -4047,7 +5328,7 @@ var schedule = (configuration, state, task) => {
|
|
|
4047
5328
|
if (task.providerSessionId !== undefined) {
|
|
4048
5329
|
state.sessions.set(sessionKey(task), task.providerSessionId);
|
|
4049
5330
|
}
|
|
4050
|
-
if (state.tasks.size > 0) {
|
|
5331
|
+
if (state.tasks.size > 0 || state.operation !== undefined) {
|
|
4051
5332
|
if (state.queue.length >= 100) {
|
|
4052
5333
|
state.socket?.close(4008, "Task queue limit reached");
|
|
4053
5334
|
return;
|
|
@@ -4070,9 +5351,9 @@ var cancel = (state, taskId) => {
|
|
|
4070
5351
|
remember(state, taskId);
|
|
4071
5352
|
sendTerminal(state, taskId, SocketEvent.make({ _tag: "TaskCancelled", taskId }));
|
|
4072
5353
|
};
|
|
4073
|
-
var connect = (configuration, state) =>
|
|
5354
|
+
var connect = (configuration, state) => Effect8.tryPromise({
|
|
4074
5355
|
try: (signal) => new Promise((complete) => {
|
|
4075
|
-
const socket = new
|
|
5356
|
+
const socket = new WebSocket2(socketUrl(configuration), {
|
|
4076
5357
|
maxPayload: maxSocketPayloadBytes,
|
|
4077
5358
|
perMessageDeflate: false
|
|
4078
5359
|
});
|
|
@@ -4080,7 +5361,7 @@ var connect = (configuration, state) => Effect6.tryPromise({
|
|
|
4080
5361
|
send(state, SocketEvent.make({
|
|
4081
5362
|
_tag: "DeviceHeartbeat",
|
|
4082
5363
|
deviceId: configuration.id,
|
|
4083
|
-
status: state.tasks.size > 0 ? "busy" : "online"
|
|
5364
|
+
status: state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online"
|
|
4084
5365
|
}));
|
|
4085
5366
|
}, 15000);
|
|
4086
5367
|
let closed = false;
|
|
@@ -4114,19 +5395,22 @@ var connect = (configuration, state) => Effect6.tryPromise({
|
|
|
4114
5395
|
return;
|
|
4115
5396
|
}
|
|
4116
5397
|
const text2 = Buffer.isBuffer(message) ? message.toString("utf8") : message instanceof ArrayBuffer ? Buffer.from(message).toString("utf8") : Array.isArray(message) ? Buffer.concat(message).toString("utf8") : Buffer.from(message).toString("utf8");
|
|
4117
|
-
const event = await
|
|
5398
|
+
const event = await Effect8.runPromise(Effect8.try({
|
|
4118
5399
|
try: () => JSON.parse(text2),
|
|
4119
5400
|
catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
|
|
4120
|
-
}).pipe(
|
|
5401
|
+
}).pipe(Effect8.flatMap(decodeSocketEvent)));
|
|
4121
5402
|
if (event._tag === "Connected") {
|
|
4122
5403
|
state.socket = socket;
|
|
4123
5404
|
socket.send(JSON.stringify(SocketEvent.make({
|
|
4124
5405
|
_tag: "DeviceHello",
|
|
4125
|
-
device: describeDevice(configuration, state.tasks.size > 0 ? "busy" : "online")
|
|
5406
|
+
device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online")
|
|
4126
5407
|
})));
|
|
4127
5408
|
for (const payload of state.terminal.values()) {
|
|
4128
5409
|
socket.send(payload);
|
|
4129
5410
|
}
|
|
5411
|
+
for (const payload of state.operationTerminal.values()) {
|
|
5412
|
+
socket.send(payload);
|
|
5413
|
+
}
|
|
4130
5414
|
return;
|
|
4131
5415
|
}
|
|
4132
5416
|
if (event._tag === "TaskAssigned") {
|
|
@@ -4141,6 +5425,14 @@ var connect = (configuration, state) => Effect6.tryPromise({
|
|
|
4141
5425
|
state.terminal.delete(event.taskId);
|
|
4142
5426
|
return;
|
|
4143
5427
|
}
|
|
5428
|
+
if (event._tag === "OperationAssigned") {
|
|
5429
|
+
scheduleOperation(configuration, state, event.operation);
|
|
5430
|
+
return;
|
|
5431
|
+
}
|
|
5432
|
+
if (event._tag === "OperationSettled") {
|
|
5433
|
+
state.operationTerminal.delete(event.operationId);
|
|
5434
|
+
return;
|
|
5435
|
+
}
|
|
4144
5436
|
if (event._tag === "Error") {
|
|
4145
5437
|
console.error(`Relay error: ${event.message}`);
|
|
4146
5438
|
}
|
|
@@ -4165,9 +5457,13 @@ var runDaemon = (configuration) => {
|
|
|
4165
5457
|
queue: [],
|
|
4166
5458
|
sessions: new Map,
|
|
4167
5459
|
tasks: new Map,
|
|
4168
|
-
completed: new Set
|
|
5460
|
+
completed: new Set,
|
|
5461
|
+
operation: undefined,
|
|
5462
|
+
operationQueue: [],
|
|
5463
|
+
operationTerminal: new Map,
|
|
5464
|
+
completedOperations: new Set
|
|
4169
5465
|
};
|
|
4170
|
-
return connect(configuration, state).pipe(
|
|
5466
|
+
return connect(configuration, state).pipe(Effect8.repeat({ schedule: Schedule2.spaced("2 seconds") }), Effect8.ensuring(Effect8.sync(() => {
|
|
4171
5467
|
state.socket?.close();
|
|
4172
5468
|
for (const controller of state.tasks.values()) {
|
|
4173
5469
|
controller.abort();
|
|
@@ -4179,7 +5475,7 @@ var runDaemon = (configuration) => {
|
|
|
4179
5475
|
init_src();
|
|
4180
5476
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4181
5477
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4182
|
-
import { Effect as
|
|
5478
|
+
import { Effect as Effect9 } from "effect";
|
|
4183
5479
|
import * as z from "zod/v4";
|
|
4184
5480
|
var output = (value) => ({
|
|
4185
5481
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
@@ -4194,7 +5490,7 @@ var runMcp = async (configuration) => {
|
|
|
4194
5490
|
title: "List Cohall devices",
|
|
4195
5491
|
description: "List devices, their availability, local providers, capabilities, and allowed workspaces.",
|
|
4196
5492
|
inputSchema: {}
|
|
4197
|
-
}, async () => output(await
|
|
5493
|
+
}, async () => output(await Effect9.runPromise(client2.devices())));
|
|
4198
5494
|
server.registerTool("delegate", {
|
|
4199
5495
|
title: "Delegate work to another device",
|
|
4200
5496
|
description: "Send focused work to another Cohall device. When the request depends on the current conversation, distill its motivation, relevant facts, prior findings, constraints, and desired decision into context; Cohall cannot read the host transcript. Never forward unrelated transcript content. Reuse thread_id for related follow-ups.",
|
|
@@ -4209,7 +5505,7 @@ var runMcp = async (configuration) => {
|
|
|
4209
5505
|
timeout_seconds: z.number().int().min(5).max(86400).default(900)
|
|
4210
5506
|
}
|
|
4211
5507
|
}, async ({ prompt, target, provider, context, thread_id, workspace, wait, timeout_seconds }) => {
|
|
4212
|
-
const task = await
|
|
5508
|
+
const task = await Effect9.runPromise(createDelegation(client2, configuration, {
|
|
4213
5509
|
prompt,
|
|
4214
5510
|
provider,
|
|
4215
5511
|
...target === undefined ? {} : { target },
|
|
@@ -4217,18 +5513,18 @@ var runMcp = async (configuration) => {
|
|
|
4217
5513
|
...thread_id === undefined ? {} : { threadId: ThreadId.make(thread_id) },
|
|
4218
5514
|
...workspace === undefined ? {} : { workspace }
|
|
4219
5515
|
}));
|
|
4220
|
-
return output(taskResult(wait ? await
|
|
5516
|
+
return output(taskResult(wait ? await Effect9.runPromise(waitForTask(client2, task, timeout_seconds)) : task));
|
|
4221
5517
|
});
|
|
4222
5518
|
server.registerTool("task_status", {
|
|
4223
5519
|
title: "Get delegated task status",
|
|
4224
5520
|
description: "Read the current state and result of a Cohall task.",
|
|
4225
5521
|
inputSchema: { task_id: z.string().uuid() }
|
|
4226
|
-
}, async ({ task_id }) => output(taskResult(await
|
|
5522
|
+
}, async ({ task_id }) => output(taskResult(await Effect9.runPromise(client2.getTask(TaskId.make(task_id))))));
|
|
4227
5523
|
server.registerTool("task_trace", {
|
|
4228
5524
|
title: "Trace a delegated task",
|
|
4229
5525
|
description: "Read the redacted relay and device lifecycle for troubleshooting a Cohall task.",
|
|
4230
5526
|
inputSchema: { task_id: z.string().uuid() }
|
|
4231
|
-
}, async ({ task_id }) => output(await
|
|
5527
|
+
}, async ({ task_id }) => output(await Effect9.runPromise(client2.traceTask(TaskId.make(task_id)))));
|
|
4232
5528
|
server.registerTool("wait_task", {
|
|
4233
5529
|
title: "Wait for delegated work",
|
|
4234
5530
|
description: "Wait for a Cohall task to finish and return its final result.",
|
|
@@ -4237,19 +5533,19 @@ var runMcp = async (configuration) => {
|
|
|
4237
5533
|
timeout_seconds: z.number().int().min(5).max(86400).default(900)
|
|
4238
5534
|
}
|
|
4239
5535
|
}, async ({ task_id, timeout_seconds }) => {
|
|
4240
|
-
const task = await
|
|
4241
|
-
return output(taskResult(await
|
|
5536
|
+
const task = await Effect9.runPromise(client2.getTask(TaskId.make(task_id)));
|
|
5537
|
+
return output(taskResult(await Effect9.runPromise(waitForTask(client2, task, timeout_seconds))));
|
|
4242
5538
|
});
|
|
4243
5539
|
server.registerTool("cancel_task", {
|
|
4244
5540
|
title: "Cancel delegated work",
|
|
4245
5541
|
description: "Request cancellation and return its acknowledged or pending state.",
|
|
4246
5542
|
inputSchema: { task_id: z.string().uuid() }
|
|
4247
|
-
}, async ({ task_id }) => output(taskResult(await
|
|
5543
|
+
}, async ({ task_id }) => output(taskResult(await Effect9.runPromise(client2.cancelTask(TaskId.make(task_id))))));
|
|
4248
5544
|
server.registerTool("thread_context", {
|
|
4249
5545
|
title: "Read a Cohall thread",
|
|
4250
5546
|
description: "Read shared task prompts and final results for a cross-device thread.",
|
|
4251
5547
|
inputSchema: { thread_id: z.string().uuid() }
|
|
4252
|
-
}, async ({ thread_id }) => output(await
|
|
5548
|
+
}, async ({ thread_id }) => output(await Effect9.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
|
|
4253
5549
|
await server.connect(new StdioServerTransport);
|
|
4254
5550
|
};
|
|
4255
5551
|
|
|
@@ -4269,21 +5565,21 @@ var main = async () => {
|
|
|
4269
5565
|
printHelp();
|
|
4270
5566
|
return;
|
|
4271
5567
|
}
|
|
4272
|
-
if (["device", "
|
|
5568
|
+
if (["device", "mcp"].includes(command) && process.argv.length > 3) {
|
|
4273
5569
|
throw new Error(`${command} does not accept arguments`);
|
|
4274
5570
|
}
|
|
4275
|
-
if (command === "relay") {
|
|
5571
|
+
if (command === "relay" && commandArguments.length === 0) {
|
|
4276
5572
|
const { runRelay: runRelay2 } = await init_main().then(() => exports_main);
|
|
4277
5573
|
await runRelay2();
|
|
4278
5574
|
return;
|
|
4279
5575
|
}
|
|
4280
5576
|
if (command === "device") {
|
|
4281
|
-
const configuration = await
|
|
4282
|
-
await
|
|
5577
|
+
const configuration = await Effect13.runPromise(loadDeviceConfiguration);
|
|
5578
|
+
await Effect13.runPromise(runDaemon(configuration));
|
|
4283
5579
|
return;
|
|
4284
5580
|
}
|
|
4285
5581
|
if (command === "mcp") {
|
|
4286
|
-
const configuration = await
|
|
5582
|
+
const configuration = await Effect13.runPromise(loadClientConfiguration);
|
|
4287
5583
|
await runMcp(configuration);
|
|
4288
5584
|
return;
|
|
4289
5585
|
}
|
|
@@ -4294,4 +5590,4 @@ await main().catch((cause) => {
|
|
|
4294
5590
|
process.exitCode = 1;
|
|
4295
5591
|
});
|
|
4296
5592
|
|
|
4297
|
-
//# debugId=
|
|
5593
|
+
//# debugId=2205AFF1105B05BB64756E2164756E21
|