@akshar5/cohall 0.4.9 → 0.5.0
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 +85 -9
- package/bin/cohall.js +1529 -266
- package/bin/cohall.js.map +15 -11
- package/docs/install.md +59 -7
- package/docs/services.md +88 -25
- package/package.json +1 -1
package/bin/cohall.js
CHANGED
|
@@ -17,16 +17,18 @@ 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.0", 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, Thread, Message, Task, 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()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeDevice, decodeSocketEvent;
|
|
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, 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
23
|
var init_src = __esm(() => {
|
|
24
24
|
maxSocketPayloadBytes = 1024 * 1024;
|
|
25
|
+
count = Schema.Int.check(Schema.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
|
|
25
26
|
DeviceId = uuid("DeviceId");
|
|
26
27
|
ThreadId = uuid("ThreadId");
|
|
27
28
|
MessageId = uuid("MessageId");
|
|
28
29
|
TaskId = uuid("TaskId");
|
|
29
30
|
AuthSessionId = uuid("AuthSessionId");
|
|
31
|
+
OperationId = uuid("OperationId");
|
|
30
32
|
Timestamp = isoTimestamp("Timestamp");
|
|
31
33
|
Platform = Schema.Literals(["darwin", "linux", "windows", "unknown"]);
|
|
32
34
|
Provider = Schema.Literals(["codex", "claude-code", "opencode"]);
|
|
@@ -40,6 +42,13 @@ var init_src = __esm(() => {
|
|
|
40
42
|
"failed",
|
|
41
43
|
"cancelled"
|
|
42
44
|
]);
|
|
45
|
+
OperationStatus = Schema.Literals([
|
|
46
|
+
"queued",
|
|
47
|
+
"assigned",
|
|
48
|
+
"running",
|
|
49
|
+
"completed",
|
|
50
|
+
"failed"
|
|
51
|
+
]);
|
|
43
52
|
TaskTraceEventKind = Schema.Literals([...TaskStatus.literals, "requeued"]);
|
|
44
53
|
MessageRole = Schema.Literals(["human", "agent", "system"]);
|
|
45
54
|
ConnectionRole = Schema.Literals(["client", "device"]);
|
|
@@ -130,6 +139,52 @@ var init_src = __esm(() => {
|
|
|
130
139
|
startedAt: Schema.optionalKey(Timestamp),
|
|
131
140
|
completedAt: Schema.optionalKey(Timestamp)
|
|
132
141
|
});
|
|
142
|
+
upgradeVersion = Schema.String.check(Schema.isPattern(/^(?:latest|v?\d+\.\d+\.\d+(?:-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?)$/, {
|
|
143
|
+
expected: "latest or an exact semantic version"
|
|
144
|
+
}));
|
|
145
|
+
CreateUpgradeOperationsInput = Schema.Struct({
|
|
146
|
+
target: upgradeVersion,
|
|
147
|
+
restart: Schema.Boolean
|
|
148
|
+
});
|
|
149
|
+
DeviceOperation = Schema.Struct({
|
|
150
|
+
id: OperationId,
|
|
151
|
+
kind: Schema.Literal("upgrade"),
|
|
152
|
+
status: OperationStatus,
|
|
153
|
+
targetDeviceId: DeviceId,
|
|
154
|
+
requestedVersion: upgradeVersion,
|
|
155
|
+
restart: Schema.Boolean,
|
|
156
|
+
result: Schema.optionalKey(optionalText(16384)),
|
|
157
|
+
error: Schema.optionalKey(optionalText(16384)),
|
|
158
|
+
createdAt: Timestamp,
|
|
159
|
+
updatedAt: Timestamp,
|
|
160
|
+
completedAt: Schema.optionalKey(Timestamp)
|
|
161
|
+
});
|
|
162
|
+
TaskStatusCounts = Schema.Struct({
|
|
163
|
+
queued: count,
|
|
164
|
+
assigned: count,
|
|
165
|
+
running: count,
|
|
166
|
+
cancelling: count,
|
|
167
|
+
completed: count,
|
|
168
|
+
failed: count,
|
|
169
|
+
cancelled: count
|
|
170
|
+
});
|
|
171
|
+
ProviderUsage = Schema.Struct({
|
|
172
|
+
provider: Provider,
|
|
173
|
+
tasks: count
|
|
174
|
+
});
|
|
175
|
+
DeviceUsage = Schema.Struct({
|
|
176
|
+
deviceId: DeviceId,
|
|
177
|
+
deviceName: bounded(128),
|
|
178
|
+
tasks: count,
|
|
179
|
+
byStatus: TaskStatusCounts,
|
|
180
|
+
byProvider: boundedArray(ProviderUsage, 3)
|
|
181
|
+
});
|
|
182
|
+
UsageSummary = Schema.Struct({
|
|
183
|
+
retainedTasks: count,
|
|
184
|
+
byStatus: TaskStatusCounts,
|
|
185
|
+
byProvider: boundedArray(ProviderUsage, 3),
|
|
186
|
+
devices: boundedArray(DeviceUsage, 256)
|
|
187
|
+
});
|
|
133
188
|
TaskTraceEvent = Schema.Struct({
|
|
134
189
|
kind: TaskTraceEventKind,
|
|
135
190
|
at: Timestamp,
|
|
@@ -184,6 +239,11 @@ var init_src = __esm(() => {
|
|
|
184
239
|
TaskCancelled: { taskId: TaskId },
|
|
185
240
|
TaskSettled: { taskId: TaskId },
|
|
186
241
|
CancelTask: { taskId: TaskId },
|
|
242
|
+
OperationAssigned: { operation: DeviceOperation },
|
|
243
|
+
OperationAccepted: { operationId: OperationId },
|
|
244
|
+
OperationFinished: { operationId: OperationId, result: optionalText(16384) },
|
|
245
|
+
OperationFailed: { operationId: OperationId, error: bounded(16384) },
|
|
246
|
+
OperationSettled: { operationId: OperationId },
|
|
187
247
|
Error: { code: bounded(64), message: bounded(2048) }
|
|
188
248
|
});
|
|
189
249
|
ErrorResponse = Schema.Struct({
|
|
@@ -197,32 +257,33 @@ var init_src = __esm(() => {
|
|
|
197
257
|
decodeCreatePairingInput = Schema.decodeUnknownEffect(CreatePairingInput);
|
|
198
258
|
decodeExchangePairingInput = Schema.decodeUnknownEffect(ExchangePairingInput);
|
|
199
259
|
decodeCreateTaskInput = Schema.decodeUnknownEffect(CreateTaskInput);
|
|
260
|
+
decodeCreateUpgradeOperationsInput = Schema.decodeUnknownEffect(CreateUpgradeOperationsInput);
|
|
200
261
|
decodeDevice = Schema.decodeUnknownEffect(Device);
|
|
201
262
|
decodeSocketEvent = Schema.decodeUnknownEffect(SocketEvent);
|
|
202
263
|
});
|
|
203
264
|
|
|
204
265
|
// apps/relay/src/config.ts
|
|
205
|
-
import { Effect as
|
|
206
|
-
import { access as
|
|
207
|
-
import { homedir as
|
|
208
|
-
import { join as
|
|
266
|
+
import { Effect as Effect10, Schema as Schema10 } from "effect";
|
|
267
|
+
import { access as access5, chmod as chmod6, mkdir as mkdir6, readFile as readFile5, writeFile as writeFile7 } from "node:fs/promises";
|
|
268
|
+
import { homedir as homedir5, platform as platform4 } from "node:os";
|
|
269
|
+
import { join as join7, resolve as resolve3 } from "node:path";
|
|
209
270
|
var RelayConfiguration, defaultDataDirectory = () => {
|
|
210
271
|
if (platform4() === "win32") {
|
|
211
|
-
return
|
|
272
|
+
return join7(process.env.LOCALAPPDATA ?? join7(homedir5(), "AppData", "Local"), "Cohall");
|
|
212
273
|
}
|
|
213
274
|
if (platform4() === "darwin") {
|
|
214
|
-
return
|
|
275
|
+
return join7(homedir5(), "Library", "Application Support", "Cohall", "relay");
|
|
215
276
|
}
|
|
216
|
-
return
|
|
277
|
+
return join7(process.env.XDG_DATA_HOME ?? join7(homedir5(), ".local", "share"), "cohall");
|
|
217
278
|
}, 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
|
|
279
|
+
if (await access5(path).then(() => true).catch(() => false)) {
|
|
280
|
+
return (await readFile5(path, "utf8")).trim();
|
|
220
281
|
}
|
|
221
282
|
const token = generatedToken();
|
|
222
|
-
await
|
|
283
|
+
await writeFile7(path, `${token}
|
|
223
284
|
`, { mode: 384 });
|
|
224
285
|
if (platform4() !== "win32") {
|
|
225
|
-
await
|
|
286
|
+
await chmod6(path, 384);
|
|
226
287
|
}
|
|
227
288
|
return token;
|
|
228
289
|
}, booleanEnvironment = (name) => {
|
|
@@ -234,41 +295,41 @@ var RelayConfiguration, defaultDataDirectory = () => {
|
|
|
234
295
|
return true;
|
|
235
296
|
}
|
|
236
297
|
throw new Error(`${name} must be true or false`);
|
|
237
|
-
},
|
|
298
|
+
}, isLoopback2 = (host) => host === "127.0.0.1" || host === "localhost" || host === "::1", loadEnvironmentConfiguration;
|
|
238
299
|
var init_config = __esm(() => {
|
|
239
|
-
RelayConfiguration =
|
|
240
|
-
host:
|
|
241
|
-
port:
|
|
242
|
-
token:
|
|
243
|
-
dataDirectory:
|
|
244
|
-
databasePath:
|
|
245
|
-
allowRemote:
|
|
246
|
-
historyTaskLimit:
|
|
300
|
+
RelayConfiguration = Schema10.Struct({
|
|
301
|
+
host: Schema10.NonEmptyString,
|
|
302
|
+
port: Schema10.Int.check(Schema10.isBetween({ minimum: 1, maximum: 65535 })),
|
|
303
|
+
token: Schema10.String.check(Schema10.isMinLength(32)),
|
|
304
|
+
dataDirectory: Schema10.NonEmptyString,
|
|
305
|
+
databasePath: Schema10.NonEmptyString,
|
|
306
|
+
allowRemote: Schema10.Boolean,
|
|
307
|
+
historyTaskLimit: Schema10.Int.check(Schema10.isBetween({ minimum: 100, maximum: 1e5 }))
|
|
247
308
|
});
|
|
248
|
-
loadEnvironmentConfiguration =
|
|
309
|
+
loadEnvironmentConfiguration = Effect10.tryPromise({
|
|
249
310
|
try: async () => {
|
|
250
311
|
const host = process.env.COHALL_RELAY_HOST ?? "127.0.0.1";
|
|
251
312
|
const port = Number(process.env.COHALL_RELAY_PORT ?? "8787");
|
|
252
313
|
const allowRemote = booleanEnvironment("COHALL_RELAY_ALLOW_REMOTE");
|
|
253
|
-
if (!
|
|
314
|
+
if (!isLoopback2(host) && !allowRemote) {
|
|
254
315
|
throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
|
|
255
316
|
}
|
|
256
|
-
const dataDirectory =
|
|
257
|
-
await
|
|
317
|
+
const dataDirectory = resolve3(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
|
|
318
|
+
await mkdir6(dataDirectory, { recursive: true, mode: 448 });
|
|
258
319
|
if (platform4() !== "win32") {
|
|
259
|
-
await
|
|
320
|
+
await chmod6(dataDirectory, 448);
|
|
260
321
|
}
|
|
261
|
-
const token = process.env.COHALL_TOKEN ?? await tokenFrom(
|
|
322
|
+
const token = process.env.COHALL_TOKEN ?? await tokenFrom(join7(dataDirectory, "owner-token"));
|
|
262
323
|
if (token.length < 32) {
|
|
263
324
|
throw new Error("The Cohall relay owner token must be at least 32 characters");
|
|
264
325
|
}
|
|
265
326
|
const historyTaskLimit = Number(process.env.COHALL_HISTORY_TASK_LIMIT ?? "1000");
|
|
266
|
-
return
|
|
327
|
+
return Schema10.decodeUnknownSync(RelayConfiguration)({
|
|
267
328
|
host,
|
|
268
329
|
port,
|
|
269
330
|
token,
|
|
270
331
|
dataDirectory,
|
|
271
|
-
databasePath:
|
|
332
|
+
databasePath: join7(dataDirectory, "cohall.db"),
|
|
272
333
|
allowRemote,
|
|
273
334
|
historyTaskLimit
|
|
274
335
|
});
|
|
@@ -278,7 +339,7 @@ var init_config = __esm(() => {
|
|
|
278
339
|
});
|
|
279
340
|
|
|
280
341
|
// apps/relay/src/hub.ts
|
|
281
|
-
import { WebSocket as
|
|
342
|
+
import { WebSocket as WebSocket3 } from "ws";
|
|
282
343
|
|
|
283
344
|
class Hub {
|
|
284
345
|
#principals = new Map;
|
|
@@ -294,7 +355,7 @@ class Hub {
|
|
|
294
355
|
this.#ownerSockets.add(socket);
|
|
295
356
|
} else {
|
|
296
357
|
const existing = this.#sessions.get(principal.sessionId);
|
|
297
|
-
if (existing !== undefined && existing.readyState ===
|
|
358
|
+
if (existing !== undefined && existing.readyState === WebSocket3.OPEN) {
|
|
298
359
|
return false;
|
|
299
360
|
}
|
|
300
361
|
this.#sessions.set(principal.sessionId, socket);
|
|
@@ -325,7 +386,7 @@ class Hub {
|
|
|
325
386
|
return false;
|
|
326
387
|
}
|
|
327
388
|
const previous = this.#devices.get(deviceId);
|
|
328
|
-
if (previous !== undefined && previous !== socket && previous.readyState ===
|
|
389
|
+
if (previous !== undefined && previous !== socket && previous.readyState === WebSocket3.OPEN) {
|
|
329
390
|
return false;
|
|
330
391
|
}
|
|
331
392
|
this.#devices.set(deviceId, socket);
|
|
@@ -371,11 +432,11 @@ class Hub {
|
|
|
371
432
|
this.#devices.get(deviceId)?.close(4003, "Device forgotten");
|
|
372
433
|
}
|
|
373
434
|
hasDevice(deviceId) {
|
|
374
|
-
return this.#devices.get(deviceId)?.readyState ===
|
|
435
|
+
return this.#devices.get(deviceId)?.readyState === WebSocket3.OPEN;
|
|
375
436
|
}
|
|
376
437
|
sendToDevice(deviceId, event) {
|
|
377
438
|
const socket = this.#devices.get(deviceId);
|
|
378
|
-
if (socket?.readyState !==
|
|
439
|
+
if (socket?.readyState !== WebSocket3.OPEN || !this.isAuthorized(socket)) {
|
|
379
440
|
return false;
|
|
380
441
|
}
|
|
381
442
|
try {
|
|
@@ -389,7 +450,7 @@ class Hub {
|
|
|
389
450
|
var init_hub = () => {};
|
|
390
451
|
|
|
391
452
|
// apps/relay/src/database.ts
|
|
392
|
-
import { DatabaseSync } from "node:sqlite";
|
|
453
|
+
import { DatabaseSync as DatabaseSync2 } from "node:sqlite";
|
|
393
454
|
|
|
394
455
|
class Statement {
|
|
395
456
|
#statement;
|
|
@@ -412,7 +473,7 @@ class Statement {
|
|
|
412
473
|
class Database {
|
|
413
474
|
#database;
|
|
414
475
|
constructor(path) {
|
|
415
|
-
this.#database = new
|
|
476
|
+
this.#database = new DatabaseSync2(path);
|
|
416
477
|
}
|
|
417
478
|
query(sql) {
|
|
418
479
|
return new Statement(this.#database.prepare(sql));
|
|
@@ -448,12 +509,12 @@ __export(exports_store, {
|
|
|
448
509
|
RelayStore: () => exports_store,
|
|
449
510
|
PersistenceError: () => PersistenceError
|
|
450
511
|
});
|
|
451
|
-
import { Context as Context2, Effect as
|
|
452
|
-
import { createHash, randomBytes } from "node:crypto";
|
|
512
|
+
import { Context as Context2, Effect as Effect11, Layer as Layer2, Schema as Schema11 } from "effect";
|
|
513
|
+
import { createHash as createHash2, randomBytes } from "node:crypto";
|
|
453
514
|
var PersistenceError, Service2, operationError = (operation) => (cause) => new PersistenceError({
|
|
454
515
|
operation,
|
|
455
516
|
message: cause instanceof Error ? cause.message : String(cause)
|
|
456
|
-
}), decode = (operation, schema, value) =>
|
|
517
|
+
}), decode = (operation, schema, value) => Schema11.decodeUnknownEffect(schema)(value).pipe(Effect11.mapError(operationError(operation))), threadFromRow = (row) => decode("RelayStore.decodeThread", Thread, {
|
|
457
518
|
id: row.id,
|
|
458
519
|
title: row.title,
|
|
459
520
|
createdAt: row.created_at,
|
|
@@ -499,14 +560,34 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
499
560
|
...row.error === null ? {} : { error: row.error },
|
|
500
561
|
...row.started_at === null ? {} : { startedAt: row.started_at },
|
|
501
562
|
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
563
|
+
}), operationFromRow = (row) => decode("RelayStore.decodeOperation", DeviceOperation, {
|
|
564
|
+
id: row.id,
|
|
565
|
+
kind: row.kind,
|
|
566
|
+
status: row.status,
|
|
567
|
+
targetDeviceId: row.target_device_id,
|
|
568
|
+
requestedVersion: row.requested_version,
|
|
569
|
+
restart: row.restart === 1,
|
|
570
|
+
createdAt: row.created_at,
|
|
571
|
+
updatedAt: row.updated_at,
|
|
572
|
+
...row.result === null ? {} : { result: row.result },
|
|
573
|
+
...row.error === null ? {} : { error: row.error },
|
|
574
|
+
...row.completed_at === null ? {} : { completedAt: row.completed_at }
|
|
575
|
+
}), emptyStatusCounts = () => ({
|
|
576
|
+
queued: 0,
|
|
577
|
+
assigned: 0,
|
|
578
|
+
running: 0,
|
|
579
|
+
cancelling: 0,
|
|
580
|
+
completed: 0,
|
|
581
|
+
failed: 0,
|
|
582
|
+
cancelled: 0
|
|
502
583
|
}), taskTraceEventFromRow = (row) => decode("RelayStore.decodeTaskTraceEvent", TaskTraceEvent, {
|
|
503
584
|
kind: row.kind,
|
|
504
585
|
at: row.created_at,
|
|
505
586
|
...row.detail === null ? {} : { detail: row.detail }
|
|
506
|
-
}), rolesFromRow = (row) =>
|
|
587
|
+
}), rolesFromRow = (row) => Schema11.decodeUnknownSync(Schema11.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
|
|
507
588
|
const roles = rolesFromRow(row);
|
|
508
589
|
const role = roles[0];
|
|
509
|
-
return role === undefined ?
|
|
590
|
+
return role === undefined ? Effect11.fail(new PersistenceError({
|
|
510
591
|
operation: "RelayStore.decodeAuthSession",
|
|
511
592
|
message: `Session ${row.id} has no role`
|
|
512
593
|
})) : decode("RelayStore.decodeAuthSession", AuthSession, {
|
|
@@ -521,7 +602,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
521
602
|
});
|
|
522
603
|
}, secret = (prefix) => {
|
|
523
604
|
return `cohall_${prefix}_${randomBytes(32).toString("hex")}`;
|
|
524
|
-
}, tokenHash = (token) =>
|
|
605
|
+
}, tokenHash = (token) => createHash2("sha256").update(token).digest("hex"), providerName = (provider) => {
|
|
525
606
|
switch (provider) {
|
|
526
607
|
case "codex":
|
|
527
608
|
return "Codex";
|
|
@@ -587,23 +668,36 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
587
668
|
WHERE NOT EXISTS (SELECT 1 FROM tasks WHERE tasks.thread_id = threads.id)
|
|
588
669
|
AND NOT EXISTS (SELECT 1 FROM messages WHERE messages.thread_id = threads.id)`).run();
|
|
589
670
|
};
|
|
671
|
+
const pruneTerminalOperations = (preserveOperationId) => {
|
|
672
|
+
const exclusion = preserveOperationId === undefined ? "" : "AND id <> ?";
|
|
673
|
+
const offset = preserveOperationId === undefined ? retainedTerminalTasks : retainedTerminalTasks - 1;
|
|
674
|
+
const parameters = preserveOperationId === undefined ? [offset] : [preserveOperationId, offset];
|
|
675
|
+
db.query(`DELETE FROM device_operations
|
|
676
|
+
WHERE id IN (
|
|
677
|
+
SELECT id FROM device_operations
|
|
678
|
+
WHERE status IN ('completed', 'failed') ${exclusion}
|
|
679
|
+
ORDER BY completed_at DESC, updated_at DESC, id DESC
|
|
680
|
+
LIMIT -1 OFFSET ?
|
|
681
|
+
)`).run(...parameters);
|
|
682
|
+
};
|
|
590
683
|
const queryTask = (taskId) => db.query("SELECT * FROM tasks WHERE id = ?").get(taskId);
|
|
591
|
-
const
|
|
592
|
-
|
|
684
|
+
const queryOperation = (operationId) => db.query("SELECT * FROM device_operations WHERE id = ?").get(operationId);
|
|
685
|
+
const getTask = Effect11.fn("RelayStore.getTask")(function* (taskId) {
|
|
686
|
+
const row = yield* Effect11.try({
|
|
593
687
|
try: () => queryTask(taskId),
|
|
594
688
|
catch: operationError("RelayStore.getTask")
|
|
595
689
|
});
|
|
596
690
|
if (row === null) {
|
|
597
|
-
return yield*
|
|
691
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
598
692
|
operation: "RelayStore.getTask",
|
|
599
693
|
message: `Unknown task ${taskId}`
|
|
600
694
|
}));
|
|
601
695
|
}
|
|
602
696
|
return yield* taskFromRow(row);
|
|
603
697
|
});
|
|
604
|
-
const traceTask =
|
|
698
|
+
const traceTask = Effect11.fn("RelayStore.traceTask")(function* (taskId) {
|
|
605
699
|
const task = yield* getTask(taskId);
|
|
606
|
-
const [deviceRow, eventRows] = yield*
|
|
700
|
+
const [deviceRow, eventRows] = yield* Effect11.try({
|
|
607
701
|
try: () => [
|
|
608
702
|
db.query("SELECT * FROM devices WHERE id = ?").get(task.targetDeviceId),
|
|
609
703
|
db.query(`SELECT * FROM (
|
|
@@ -614,14 +708,14 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
614
708
|
catch: operationError("RelayStore.traceTask")
|
|
615
709
|
});
|
|
616
710
|
if (deviceRow === null) {
|
|
617
|
-
return yield*
|
|
711
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
618
712
|
operation: "RelayStore.traceTask.device",
|
|
619
713
|
message: `Unknown target device ${task.targetDeviceId}`
|
|
620
714
|
}));
|
|
621
715
|
}
|
|
622
716
|
const targetDevice = yield* deviceFromRow(deviceRow);
|
|
623
717
|
const selectedRows = eventRows.slice(-100);
|
|
624
|
-
const recordedEvents = yield*
|
|
718
|
+
const recordedEvents = yield* Effect11.forEach(selectedRows, taskTraceEventFromRow);
|
|
625
719
|
const events = recordedEvents.length === 0 ? derivedTraceEvents(task) : recordedEvents;
|
|
626
720
|
return TaskTrace.make({
|
|
627
721
|
taskId: task.id,
|
|
@@ -641,15 +735,195 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
641
735
|
...task.error === undefined ? {} : { error: task.error }
|
|
642
736
|
});
|
|
643
737
|
});
|
|
644
|
-
const listDevices =
|
|
645
|
-
const rows = yield*
|
|
738
|
+
const listDevices = Effect11.fn("RelayStore.listDevices")(function* () {
|
|
739
|
+
const rows = yield* Effect11.try({
|
|
646
740
|
try: () => db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all(),
|
|
647
741
|
catch: operationError("RelayStore.listDevices")
|
|
648
742
|
});
|
|
649
|
-
return yield*
|
|
743
|
+
return yield* Effect11.forEach(rows, deviceFromRow);
|
|
744
|
+
});
|
|
745
|
+
const usage = Effect11.fn("RelayStore.usage")(function* () {
|
|
746
|
+
const rows = yield* Effect11.try({
|
|
747
|
+
try: () => db.query(`SELECT tasks.target_device_id AS device_id, devices.name AS device_name,
|
|
748
|
+
tasks.status, tasks.provider, COUNT(*) AS tasks
|
|
749
|
+
FROM tasks
|
|
750
|
+
JOIN devices ON devices.id = tasks.target_device_id
|
|
751
|
+
GROUP BY tasks.target_device_id, devices.name, tasks.status, tasks.provider
|
|
752
|
+
ORDER BY devices.name COLLATE NOCASE, tasks.target_device_id, tasks.provider`).all(),
|
|
753
|
+
catch: operationError("RelayStore.usage")
|
|
754
|
+
});
|
|
755
|
+
const totalStatus = emptyStatusCounts();
|
|
756
|
+
const totalProviders = new Map;
|
|
757
|
+
const devices = new Map;
|
|
758
|
+
let retainedTasks = 0;
|
|
759
|
+
for (const row of rows) {
|
|
760
|
+
const status = Schema11.decodeUnknownSync(TaskStatus)(row.status);
|
|
761
|
+
const provider = Schema11.decodeUnknownSync(Provider)(row.provider);
|
|
762
|
+
const deviceId = Schema11.decodeUnknownSync(DeviceId)(row.device_id);
|
|
763
|
+
retainedTasks += row.tasks;
|
|
764
|
+
totalStatus[status] += row.tasks;
|
|
765
|
+
totalProviders.set(provider, (totalProviders.get(provider) ?? 0) + row.tasks);
|
|
766
|
+
const current = devices.get(deviceId) ?? {
|
|
767
|
+
name: row.device_name,
|
|
768
|
+
status: emptyStatusCounts(),
|
|
769
|
+
providers: new Map
|
|
770
|
+
};
|
|
771
|
+
current.status[status] += row.tasks;
|
|
772
|
+
current.providers.set(provider, (current.providers.get(provider) ?? 0) + row.tasks);
|
|
773
|
+
devices.set(deviceId, current);
|
|
774
|
+
}
|
|
775
|
+
const providerUsage = (providers) => Provider.literals.flatMap((provider) => {
|
|
776
|
+
const tasks = providers.get(provider);
|
|
777
|
+
return tasks === undefined ? [] : [{ provider, tasks }];
|
|
778
|
+
});
|
|
779
|
+
return UsageSummary.make({
|
|
780
|
+
retainedTasks,
|
|
781
|
+
byStatus: TaskStatusCounts.make(totalStatus),
|
|
782
|
+
byProvider: providerUsage(totalProviders),
|
|
783
|
+
devices: [...devices.entries()].map(([deviceId, device]) => DeviceUsage.make({
|
|
784
|
+
deviceId,
|
|
785
|
+
deviceName: device.name,
|
|
786
|
+
tasks: Object.values(device.status).reduce((sum, tasks) => sum + tasks, 0),
|
|
787
|
+
byStatus: TaskStatusCounts.make(device.status),
|
|
788
|
+
byProvider: providerUsage(device.providers)
|
|
789
|
+
}))
|
|
790
|
+
});
|
|
791
|
+
});
|
|
792
|
+
const getOperation = Effect11.fn("RelayStore.getOperation")(function* (operationId) {
|
|
793
|
+
const row = yield* Effect11.try({
|
|
794
|
+
try: () => queryOperation(operationId),
|
|
795
|
+
catch: operationError("RelayStore.getOperation")
|
|
796
|
+
});
|
|
797
|
+
if (row === null) {
|
|
798
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
799
|
+
operation: "RelayStore.getOperation",
|
|
800
|
+
message: `Unknown device operation ${operationId}`
|
|
801
|
+
}));
|
|
802
|
+
}
|
|
803
|
+
return yield* operationFromRow(row);
|
|
804
|
+
});
|
|
805
|
+
const listOperations = Effect11.fn("RelayStore.listOperations")(function* () {
|
|
806
|
+
const rows = yield* Effect11.try({
|
|
807
|
+
try: () => db.query("SELECT * FROM device_operations ORDER BY created_at DESC, id DESC LIMIT 50").all(),
|
|
808
|
+
catch: operationError("RelayStore.listOperations")
|
|
809
|
+
});
|
|
810
|
+
return yield* Effect11.forEach(rows, operationFromRow);
|
|
811
|
+
});
|
|
812
|
+
const createUpgradeOperations = Effect11.fn("RelayStore.createUpgradeOperations")(function* (input) {
|
|
813
|
+
const timestamp = now();
|
|
814
|
+
const operations = yield* Effect11.try({
|
|
815
|
+
try: () => db.transaction(() => {
|
|
816
|
+
const devices = db.query("SELECT * FROM devices WHERE forgotten_at IS NULL ORDER BY name COLLATE NOCASE, id").all();
|
|
817
|
+
if (devices.length === 0) {
|
|
818
|
+
throw new Error("No Cohall devices are registered");
|
|
819
|
+
}
|
|
820
|
+
if (devices.length > 256) {
|
|
821
|
+
throw new Error("All-device operations support at most 256 registered devices");
|
|
822
|
+
}
|
|
823
|
+
for (const device of devices) {
|
|
824
|
+
const outstanding = db.query(`SELECT COUNT(*) AS count FROM device_operations
|
|
825
|
+
WHERE target_device_id = ? AND status IN ('queued', 'assigned', 'running')`).get(device.id);
|
|
826
|
+
if ((outstanding?.count ?? 0) > 0) {
|
|
827
|
+
throw new Error(`${device.name} already has an upgrade operation in progress`);
|
|
828
|
+
}
|
|
829
|
+
}
|
|
830
|
+
pruneTerminalOperations();
|
|
831
|
+
return devices.map((device) => {
|
|
832
|
+
const operation = DeviceOperation.make({
|
|
833
|
+
id: makeOperationId(),
|
|
834
|
+
kind: "upgrade",
|
|
835
|
+
status: "queued",
|
|
836
|
+
targetDeviceId: DeviceId.make(device.id),
|
|
837
|
+
requestedVersion: input.target,
|
|
838
|
+
restart: input.restart,
|
|
839
|
+
createdAt: timestamp,
|
|
840
|
+
updatedAt: timestamp
|
|
841
|
+
});
|
|
842
|
+
db.query(`INSERT INTO device_operations (
|
|
843
|
+
id, kind, status, target_device_id, requested_version, restart,
|
|
844
|
+
created_at, updated_at
|
|
845
|
+
) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(operation.id, operation.kind, operation.status, operation.targetDeviceId, operation.requestedVersion, operation.restart ? 1 : 0, operation.createdAt, operation.updatedAt);
|
|
846
|
+
return operation;
|
|
847
|
+
});
|
|
848
|
+
})(),
|
|
849
|
+
catch: operationError("RelayStore.createUpgradeOperations")
|
|
850
|
+
});
|
|
851
|
+
return operations;
|
|
650
852
|
});
|
|
651
|
-
const
|
|
652
|
-
const
|
|
853
|
+
const pendingOperationsFor = Effect11.fn("RelayStore.pendingOperationsFor")(function* (deviceId) {
|
|
854
|
+
const rows = yield* Effect11.try({
|
|
855
|
+
try: () => db.query(`SELECT * FROM device_operations WHERE target_device_id = ? AND status = 'queued'
|
|
856
|
+
ORDER BY created_at, id LIMIT 10`).all(deviceId),
|
|
857
|
+
catch: operationError("RelayStore.pendingOperationsFor")
|
|
858
|
+
});
|
|
859
|
+
return yield* Effect11.forEach(rows, operationFromRow);
|
|
860
|
+
});
|
|
861
|
+
const transitionOperation = Effect11.fn("RelayStore.transitionOperation")(function* (operationId, expected, status, result, error) {
|
|
862
|
+
const current = yield* getOperation(operationId);
|
|
863
|
+
if (!expected.includes(current.status)) {
|
|
864
|
+
return current;
|
|
865
|
+
}
|
|
866
|
+
const timestamp = now();
|
|
867
|
+
const completed = status === "completed" || status === "failed" ? timestamp : undefined;
|
|
868
|
+
const placeholders = expected.map(() => "?").join(", ");
|
|
869
|
+
yield* Effect11.try({
|
|
870
|
+
try: () => db.transaction(() => {
|
|
871
|
+
const updated = db.query(`UPDATE device_operations
|
|
872
|
+
SET status = ?, result = ?, error = ?, updated_at = ?, completed_at = ?
|
|
873
|
+
WHERE id = ? AND status IN (${placeholders})`).run(status, result ?? current.result ?? null, error ?? current.error ?? null, timestamp, completed ?? current.completedAt ?? null, operationId, ...expected);
|
|
874
|
+
if (updated.changes === 1 && completed !== undefined) {
|
|
875
|
+
pruneTerminalOperations(operationId);
|
|
876
|
+
}
|
|
877
|
+
})(),
|
|
878
|
+
catch: operationError("RelayStore.transitionOperation")
|
|
879
|
+
});
|
|
880
|
+
return yield* getOperation(operationId);
|
|
881
|
+
});
|
|
882
|
+
const requireOperationTarget = Effect11.fn("RelayStore.requireOperationTarget")(function* (operationId, deviceId) {
|
|
883
|
+
const operation = yield* getOperation(operationId);
|
|
884
|
+
if (operation.targetDeviceId !== deviceId) {
|
|
885
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
886
|
+
operation: "RelayStore.requireOperationTarget",
|
|
887
|
+
message: `Device ${deviceId} cannot update operation ${operationId}`
|
|
888
|
+
}));
|
|
889
|
+
}
|
|
890
|
+
return operation;
|
|
891
|
+
});
|
|
892
|
+
const assignOperation = Effect11.fn("RelayStore.assignOperation")(function* (operationId) {
|
|
893
|
+
const current = yield* getOperation(operationId);
|
|
894
|
+
if (current.status !== "queued") {
|
|
895
|
+
return current;
|
|
896
|
+
}
|
|
897
|
+
const timestamp = now();
|
|
898
|
+
yield* Effect11.try({
|
|
899
|
+
try: () => db.query(`UPDATE device_operations SET status = 'assigned', updated_at = ?
|
|
900
|
+
WHERE id = ? AND status = 'queued'
|
|
901
|
+
AND NOT EXISTS (
|
|
902
|
+
SELECT 1 FROM tasks WHERE target_device_id = ?
|
|
903
|
+
AND status IN ('assigned', 'running', 'cancelling')
|
|
904
|
+
)
|
|
905
|
+
AND NOT EXISTS (
|
|
906
|
+
SELECT 1 FROM device_operations active
|
|
907
|
+
WHERE active.target_device_id = ? AND active.id <> ?
|
|
908
|
+
AND active.status IN ('assigned', 'running')
|
|
909
|
+
)`).run(timestamp, operationId, current.targetDeviceId, current.targetDeviceId, operationId),
|
|
910
|
+
catch: operationError("RelayStore.assignOperation")
|
|
911
|
+
});
|
|
912
|
+
return yield* getOperation(operationId);
|
|
913
|
+
});
|
|
914
|
+
const acceptOperation = (operationId, deviceId) => requireOperationTarget(operationId, deviceId).pipe(Effect11.flatMap(() => transitionOperation(operationId, ["assigned"], "running")));
|
|
915
|
+
const finishOperation = (operationId, deviceId, result) => requireOperationTarget(operationId, deviceId).pipe(Effect11.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "completed", result)));
|
|
916
|
+
const failOperation = (operationId, deviceId, error) => requireOperationTarget(operationId, deviceId).pipe(Effect11.flatMap(() => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, error)));
|
|
917
|
+
const abandonOperation = (operationId) => transitionOperation(operationId, ["queued", "assigned", "running"], "failed", undefined, "Abandoned by the relay owner");
|
|
918
|
+
const requeueOperationsFor = Effect11.fn("RelayStore.requeueOperationsFor")(function* (deviceId) {
|
|
919
|
+
yield* Effect11.try({
|
|
920
|
+
try: () => db.query(`UPDATE device_operations SET status = 'queued', updated_at = ?
|
|
921
|
+
WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(now(), deviceId),
|
|
922
|
+
catch: operationError("RelayStore.requeueOperationsFor")
|
|
923
|
+
});
|
|
924
|
+
});
|
|
925
|
+
const forgetDevice = Effect11.fn("RelayStore.forgetDevice")(function* (deviceId) {
|
|
926
|
+
const row = yield* Effect11.try({
|
|
653
927
|
try: () => db.transaction(() => {
|
|
654
928
|
const device = db.query("SELECT * FROM devices WHERE id = ? AND forgotten_at IS NULL").get(deviceId);
|
|
655
929
|
if (device === null) {
|
|
@@ -664,6 +938,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
664
938
|
throw new Error(`Device ${deviceId} still has outstanding tasks`);
|
|
665
939
|
}
|
|
666
940
|
const timestamp = now();
|
|
941
|
+
db.query(`UPDATE device_operations
|
|
942
|
+
SET status = 'failed', error = ?, updated_at = ?, completed_at = ?
|
|
943
|
+
WHERE target_device_id = ? AND status IN ('queued', 'assigned', 'running')`).run("Target device was forgotten by the relay owner", timestamp, timestamp, deviceId);
|
|
944
|
+
pruneTerminalOperations();
|
|
667
945
|
db.query("UPDATE devices SET forgotten_at = ? WHERE id = ?").run(timestamp, deviceId);
|
|
668
946
|
db.query("UPDATE auth_sessions SET revoked_at = ? WHERE bound_device_id = ? AND revoked_at IS NULL").run(timestamp, deviceId);
|
|
669
947
|
return device;
|
|
@@ -672,25 +950,27 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
672
950
|
});
|
|
673
951
|
return yield* deviceFromRow(row);
|
|
674
952
|
});
|
|
675
|
-
const recover =
|
|
676
|
-
yield*
|
|
953
|
+
const recover = Effect11.fn("RelayStore.recover")(function* () {
|
|
954
|
+
yield* Effect11.try({
|
|
677
955
|
try: () => db.transaction(() => {
|
|
678
956
|
const timestamp = now();
|
|
679
957
|
const interrupted = db.query("SELECT id FROM tasks WHERE status IN ('assigned', 'running')").all();
|
|
680
958
|
db.query("UPDATE devices SET status = 'offline'").run();
|
|
681
959
|
db.query("UPDATE tasks SET status = 'queued', updated_at = ? WHERE status IN ('assigned', 'running')").run(timestamp);
|
|
960
|
+
db.query("UPDATE device_operations SET status = 'queued', updated_at = ? WHERE status IN ('assigned', 'running')").run(timestamp);
|
|
682
961
|
for (const task of interrupted) {
|
|
683
962
|
recordTaskTraceEvent(TaskId.make(task.id), "requeued", "Relay restarted before the task completed", timestamp);
|
|
684
963
|
}
|
|
685
964
|
db.query("DELETE FROM auth_pairings WHERE expires_at <= ? OR used_at IS NOT NULL").run(timestamp);
|
|
686
965
|
db.query("UPDATE auth_sessions SET revoked_at = ? WHERE revoked_at IS NULL AND roles_json LIKE '%,%'").run(timestamp);
|
|
687
966
|
pruneTerminalHistory();
|
|
967
|
+
pruneTerminalOperations();
|
|
688
968
|
})(),
|
|
689
969
|
catch: operationError("RelayStore.recover")
|
|
690
970
|
});
|
|
691
971
|
});
|
|
692
|
-
const upsertDevice =
|
|
693
|
-
yield*
|
|
972
|
+
const upsertDevice = Effect11.fn("RelayStore.upsertDevice")(function* (device) {
|
|
973
|
+
yield* Effect11.try({
|
|
694
974
|
try: () => db.query(`INSERT INTO devices (
|
|
695
975
|
id, name, hostname, platform, architecture, status, providers_json,
|
|
696
976
|
capabilities_json, workspaces_json, version, last_seen_at, connected_at
|
|
@@ -706,28 +986,28 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
706
986
|
});
|
|
707
987
|
return device;
|
|
708
988
|
});
|
|
709
|
-
const updateDeviceStatus =
|
|
989
|
+
const updateDeviceStatus = Effect11.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status) {
|
|
710
990
|
const timestamp = now();
|
|
711
|
-
const result = yield*
|
|
991
|
+
const result = yield* Effect11.try({
|
|
712
992
|
try: () => db.query("UPDATE devices SET status = ?, last_seen_at = ? WHERE id = ?").run(status, timestamp, deviceId),
|
|
713
993
|
catch: operationError("RelayStore.updateDeviceStatus")
|
|
714
994
|
});
|
|
715
995
|
if (result.changes !== 1) {
|
|
716
|
-
return yield*
|
|
996
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
717
997
|
operation: "RelayStore.updateDeviceStatus",
|
|
718
998
|
message: `Unknown device ${deviceId}`
|
|
719
999
|
}));
|
|
720
1000
|
}
|
|
721
1001
|
const row = db.query("SELECT * FROM devices WHERE id = ?").get(deviceId);
|
|
722
1002
|
if (row === null) {
|
|
723
|
-
return yield*
|
|
1003
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
724
1004
|
operation: "RelayStore.updateDeviceStatus.read",
|
|
725
1005
|
message: `Device ${deviceId} disappeared after update`
|
|
726
1006
|
}));
|
|
727
1007
|
}
|
|
728
1008
|
return yield* deviceFromRow(row);
|
|
729
1009
|
});
|
|
730
|
-
const createDelegation2 =
|
|
1010
|
+
const createDelegation2 = Effect11.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, sourceDeviceId, providerSessionId) {
|
|
731
1011
|
const timestamp = now();
|
|
732
1012
|
const threadId = input.threadId ?? makeThreadId();
|
|
733
1013
|
const task = Task.make({
|
|
@@ -761,7 +1041,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
761
1041
|
taskId: task.id,
|
|
762
1042
|
...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
|
|
763
1043
|
});
|
|
764
|
-
yield*
|
|
1044
|
+
yield* Effect11.try({
|
|
765
1045
|
try: () => db.transaction(() => {
|
|
766
1046
|
const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks WHERE target_device_id = ?
|
|
767
1047
|
AND status IN ('queued', 'assigned', 'running', 'cancelling')`).get(targetDeviceId);
|
|
@@ -789,18 +1069,18 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
789
1069
|
});
|
|
790
1070
|
return task;
|
|
791
1071
|
});
|
|
792
|
-
const threadContext2 =
|
|
793
|
-
const threadRow = yield*
|
|
1072
|
+
const threadContext2 = Effect11.fn("RelayStore.threadContext")(function* (threadId) {
|
|
1073
|
+
const threadRow = yield* Effect11.try({
|
|
794
1074
|
try: () => db.query("SELECT * FROM threads WHERE id = ?").get(threadId),
|
|
795
1075
|
catch: operationError("RelayStore.threadContext.thread")
|
|
796
1076
|
});
|
|
797
1077
|
if (threadRow === null) {
|
|
798
|
-
return yield*
|
|
1078
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
799
1079
|
operation: "RelayStore.threadContext",
|
|
800
1080
|
message: `Unknown thread ${threadId}`
|
|
801
1081
|
}));
|
|
802
1082
|
}
|
|
803
|
-
const [messageRows, taskRows] = yield*
|
|
1083
|
+
const [messageRows, taskRows] = yield* Effect11.try({
|
|
804
1084
|
try: () => [
|
|
805
1085
|
db.query(`SELECT * FROM (
|
|
806
1086
|
SELECT * FROM messages WHERE thread_id = ?
|
|
@@ -813,10 +1093,10 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
813
1093
|
],
|
|
814
1094
|
catch: operationError("RelayStore.threadContext.items")
|
|
815
1095
|
});
|
|
816
|
-
const [thread, messages, tasks] = yield*
|
|
1096
|
+
const [thread, messages, tasks] = yield* Effect11.all([
|
|
817
1097
|
threadFromRow(threadRow),
|
|
818
|
-
|
|
819
|
-
|
|
1098
|
+
Effect11.forEach(messageRows.slice(-20), messageFromRow),
|
|
1099
|
+
Effect11.forEach(taskRows.slice(-20), taskFromRow)
|
|
820
1100
|
]);
|
|
821
1101
|
const recentMessages = recentWithin(messages, 256 * 1024);
|
|
822
1102
|
const recentTasks = recentWithin(tasks, 768 * 1024);
|
|
@@ -827,22 +1107,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
827
1107
|
truncated: messageRows.length > 20 || taskRows.length > 20 || recentMessages.truncated || recentTasks.truncated
|
|
828
1108
|
});
|
|
829
1109
|
});
|
|
830
|
-
const pendingTasksFor =
|
|
831
|
-
const rows = yield*
|
|
1110
|
+
const pendingTasksFor = Effect11.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
|
|
1111
|
+
const rows = yield* Effect11.try({
|
|
832
1112
|
try: () => db.query(`SELECT * FROM tasks WHERE target_device_id = ?
|
|
833
1113
|
AND status IN ('queued', 'cancelling') ORDER BY created_at, id LIMIT 100`).all(deviceId),
|
|
834
1114
|
catch: operationError("RelayStore.pendingTasksFor")
|
|
835
1115
|
});
|
|
836
|
-
return yield*
|
|
1116
|
+
return yield* Effect11.forEach(rows, taskFromRow);
|
|
837
1117
|
});
|
|
838
|
-
const transition =
|
|
1118
|
+
const transition = Effect11.fn("RelayStore.transition")(function* (taskId, expected, update, event) {
|
|
839
1119
|
const current = yield* getTask(taskId);
|
|
840
1120
|
if (!expected.includes(current.status)) {
|
|
841
1121
|
return current;
|
|
842
1122
|
}
|
|
843
1123
|
const placeholders = expected.map(() => "?").join(", ");
|
|
844
1124
|
const timestamp = now();
|
|
845
|
-
yield*
|
|
1125
|
+
yield* Effect11.try({
|
|
846
1126
|
try: () => db.transaction(() => {
|
|
847
1127
|
const result = db.query(`UPDATE tasks SET status = ?, provider_session_id = ?, result = ?, error = ?,
|
|
848
1128
|
started_at = ?, completed_at = ?, updated_at = ?
|
|
@@ -858,20 +1138,24 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
858
1138
|
});
|
|
859
1139
|
return yield* getTask(taskId);
|
|
860
1140
|
});
|
|
861
|
-
const assignTask =
|
|
1141
|
+
const assignTask = Effect11.fn("RelayStore.assignTask")(function* (taskId) {
|
|
862
1142
|
const current = yield* getTask(taskId);
|
|
863
1143
|
if (current.status !== "queued") {
|
|
864
1144
|
return current;
|
|
865
1145
|
}
|
|
866
1146
|
const timestamp = now();
|
|
867
|
-
yield*
|
|
1147
|
+
yield* Effect11.try({
|
|
868
1148
|
try: () => db.transaction(() => {
|
|
869
1149
|
const result = db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?
|
|
870
1150
|
WHERE id = ? AND status = 'queued' AND NOT EXISTS (
|
|
871
1151
|
SELECT 1 FROM tasks active
|
|
872
1152
|
WHERE active.target_device_id = ? AND active.id <> ?
|
|
873
1153
|
AND active.status IN ('assigned', 'running', 'cancelling')
|
|
874
|
-
)
|
|
1154
|
+
) AND NOT EXISTS (
|
|
1155
|
+
SELECT 1 FROM device_operations active
|
|
1156
|
+
WHERE active.target_device_id = ?
|
|
1157
|
+
AND active.status IN ('assigned', 'running')
|
|
1158
|
+
)`).run(timestamp, taskId, current.targetDeviceId, taskId, current.targetDeviceId);
|
|
875
1159
|
if (result.changes === 1) {
|
|
876
1160
|
recordTaskTraceEvent(taskId, "assigned", "Relay dispatched the task over the device WebSocket", timestamp);
|
|
877
1161
|
}
|
|
@@ -880,23 +1164,23 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
880
1164
|
});
|
|
881
1165
|
return yield* getTask(taskId);
|
|
882
1166
|
});
|
|
883
|
-
const requireTarget =
|
|
1167
|
+
const requireTarget = Effect11.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
|
|
884
1168
|
const task = yield* getTask(taskId);
|
|
885
1169
|
if (task.targetDeviceId !== deviceId) {
|
|
886
|
-
return yield*
|
|
1170
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
887
1171
|
operation: "RelayStore.requireTarget",
|
|
888
1172
|
message: `Task ${taskId} belongs to another device`
|
|
889
1173
|
}));
|
|
890
1174
|
}
|
|
891
1175
|
return task;
|
|
892
1176
|
});
|
|
893
|
-
const terminal =
|
|
1177
|
+
const terminal = Effect11.fn("RelayStore.terminal")(function* (taskId, deviceId, status, result, error, providerSessionId) {
|
|
894
1178
|
const current = yield* requireTarget(taskId, deviceId);
|
|
895
1179
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
896
1180
|
return current;
|
|
897
1181
|
}
|
|
898
1182
|
const timestamp = now();
|
|
899
|
-
yield*
|
|
1183
|
+
yield* Effect11.try({
|
|
900
1184
|
try: () => db.transaction(() => {
|
|
901
1185
|
const updated = db.query(`UPDATE tasks SET status = ?, result = ?, error = ?, provider_session_id = ?,
|
|
902
1186
|
completed_at = ?, updated_at = ? WHERE id = ?
|
|
@@ -926,7 +1210,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
926
1210
|
});
|
|
927
1211
|
return yield* getTask(taskId);
|
|
928
1212
|
});
|
|
929
|
-
const requestCancellation =
|
|
1213
|
+
const requestCancellation = Effect11.fn("RelayStore.requestCancellation")(function* (taskId) {
|
|
930
1214
|
const current = yield* getTask(taskId);
|
|
931
1215
|
if (["completed", "failed", "cancelled"].includes(current.status)) {
|
|
932
1216
|
return current;
|
|
@@ -939,9 +1223,9 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
939
1223
|
status: "cancelling"
|
|
940
1224
|
}, { kind: "cancelling", detail: "Client requested task cancellation" });
|
|
941
1225
|
});
|
|
942
|
-
const requeueTasksFor =
|
|
1226
|
+
const requeueTasksFor = Effect11.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
|
|
943
1227
|
const timestamp = now();
|
|
944
|
-
yield*
|
|
1228
|
+
yield* Effect11.try({
|
|
945
1229
|
try: () => db.transaction(() => {
|
|
946
1230
|
const interrupted = db.query(`SELECT id FROM tasks WHERE target_device_id = ?
|
|
947
1231
|
AND status IN ('assigned', 'running')`).all(deviceId);
|
|
@@ -954,42 +1238,42 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
954
1238
|
catch: operationError("RelayStore.requeueTasksFor")
|
|
955
1239
|
});
|
|
956
1240
|
});
|
|
957
|
-
const sessionFor =
|
|
958
|
-
const row = yield*
|
|
1241
|
+
const sessionFor = Effect11.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
|
|
1242
|
+
const row = yield* Effect11.try({
|
|
959
1243
|
try: () => db.query(`SELECT session_id FROM provider_sessions
|
|
960
1244
|
WHERE thread_id = ? AND device_id = ? AND provider = ?`).get(threadId, deviceId, provider),
|
|
961
1245
|
catch: operationError("RelayStore.sessionFor")
|
|
962
1246
|
});
|
|
963
1247
|
return row?.session_id;
|
|
964
1248
|
});
|
|
965
|
-
const createPairing =
|
|
1249
|
+
const createPairing = Effect11.fn("RelayStore.createPairing")(function* (input) {
|
|
966
1250
|
const roles = [...new Set(input.roles)];
|
|
967
1251
|
if (roles.length === 0) {
|
|
968
|
-
return yield*
|
|
1252
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
969
1253
|
operation: "RelayStore.createPairing",
|
|
970
1254
|
message: "A pairing must allow at least one role"
|
|
971
1255
|
}));
|
|
972
1256
|
}
|
|
973
1257
|
const token = secret("pair");
|
|
974
1258
|
const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
|
|
975
|
-
yield*
|
|
1259
|
+
yield* Effect11.try({
|
|
976
1260
|
try: () => db.query(`INSERT INTO auth_pairings (token_hash, label, roles_json, expires_at, created_at)
|
|
977
1261
|
VALUES (?, ?, ?, ?, ?)`).run(tokenHash(token), input.label, JSON.stringify(roles), expiresAt, now()),
|
|
978
1262
|
catch: operationError("RelayStore.createPairing")
|
|
979
1263
|
});
|
|
980
1264
|
return PairingCredential.make({ token, expiresAt: Timestamp.make(expiresAt) });
|
|
981
1265
|
});
|
|
982
|
-
const exchangePairing2 =
|
|
1266
|
+
const exchangePairing2 = Effect11.fn("RelayStore.exchangePairing")(function* (token) {
|
|
983
1267
|
const timestamp = now();
|
|
984
1268
|
const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
|
|
985
|
-
const created = yield*
|
|
1269
|
+
const created = yield* Effect11.try({
|
|
986
1270
|
try: () => db.transaction(() => {
|
|
987
1271
|
const pairing = db.query(`SELECT * FROM auth_pairings
|
|
988
1272
|
WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?`).get(tokenHash(token), timestamp);
|
|
989
1273
|
if (pairing === null) {
|
|
990
1274
|
throw new Error("Pairing credential is invalid, expired, or already used");
|
|
991
1275
|
}
|
|
992
|
-
const roles =
|
|
1276
|
+
const roles = Schema11.decodeUnknownSync(Schema11.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
|
|
993
1277
|
const deviceId = roles.includes("device") ? makeDeviceId() : undefined;
|
|
994
1278
|
const consumed = db.query("UPDATE auth_pairings SET used_at = ? WHERE token_hash = ? AND used_at IS NULL").run(timestamp, pairing.token_hash);
|
|
995
1279
|
if (consumed.changes !== 1) {
|
|
@@ -1017,11 +1301,11 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1017
1301
|
})(),
|
|
1018
1302
|
catch: operationError("RelayStore.exchangePairing")
|
|
1019
1303
|
});
|
|
1020
|
-
const credentials = yield*
|
|
1304
|
+
const credentials = yield* Effect11.forEach(created, ({ token: sessionToken, row }) => authSessionFromRow(row).pipe(Effect11.map((session) => ({ token: sessionToken, session }))));
|
|
1021
1305
|
return PairingResult.make({ credentials });
|
|
1022
1306
|
});
|
|
1023
|
-
const authenticateSession =
|
|
1024
|
-
const row = yield*
|
|
1307
|
+
const authenticateSession = Effect11.fn("RelayStore.authenticateSession")(function* (token, role) {
|
|
1308
|
+
const row = yield* Effect11.try({
|
|
1025
1309
|
try: () => db.query(`SELECT * FROM auth_sessions
|
|
1026
1310
|
WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > ?`).get(tokenHash(token), now()),
|
|
1027
1311
|
catch: operationError("RelayStore.authenticateSession")
|
|
@@ -1030,35 +1314,35 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1030
1314
|
return;
|
|
1031
1315
|
}
|
|
1032
1316
|
const lastSeenAt = now();
|
|
1033
|
-
yield*
|
|
1317
|
+
yield* Effect11.try({
|
|
1034
1318
|
try: () => db.query("UPDATE auth_sessions SET last_seen_at = ? WHERE id = ?").run(lastSeenAt, row.id),
|
|
1035
1319
|
catch: operationError("RelayStore.authenticateSession.touch")
|
|
1036
1320
|
});
|
|
1037
1321
|
const session = yield* authSessionFromRow(row);
|
|
1038
1322
|
return AuthSession.make({ ...session, lastSeenAt });
|
|
1039
1323
|
});
|
|
1040
|
-
const listAuthSessions =
|
|
1041
|
-
const rows = yield*
|
|
1324
|
+
const listAuthSessions = Effect11.fn("RelayStore.listAuthSessions")(function* () {
|
|
1325
|
+
const rows = yield* Effect11.try({
|
|
1042
1326
|
try: () => db.query("SELECT * FROM auth_sessions ORDER BY created_at DESC, id LIMIT 1000").all(),
|
|
1043
1327
|
catch: operationError("RelayStore.listAuthSessions")
|
|
1044
1328
|
});
|
|
1045
|
-
return yield*
|
|
1329
|
+
return yield* Effect11.forEach(rows, authSessionFromRow);
|
|
1046
1330
|
});
|
|
1047
|
-
const revokeAuthSession =
|
|
1331
|
+
const revokeAuthSession = Effect11.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
|
|
1048
1332
|
const revokedAt = now();
|
|
1049
|
-
const result = yield*
|
|
1333
|
+
const result = yield* Effect11.try({
|
|
1050
1334
|
try: () => db.query("UPDATE auth_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL").run(revokedAt, sessionId),
|
|
1051
1335
|
catch: operationError("RelayStore.revokeAuthSession")
|
|
1052
1336
|
});
|
|
1053
1337
|
if (result.changes !== 1) {
|
|
1054
|
-
return yield*
|
|
1338
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
1055
1339
|
operation: "RelayStore.revokeAuthSession",
|
|
1056
1340
|
message: `Unknown or already revoked session ${sessionId}`
|
|
1057
1341
|
}));
|
|
1058
1342
|
}
|
|
1059
1343
|
const row = db.query("SELECT * FROM auth_sessions WHERE id = ?").get(sessionId);
|
|
1060
1344
|
if (row === null) {
|
|
1061
|
-
return yield*
|
|
1345
|
+
return yield* Effect11.fail(new PersistenceError({
|
|
1062
1346
|
operation: "RelayStore.revokeAuthSession.read",
|
|
1063
1347
|
message: `Session ${sessionId} disappeared after revocation`
|
|
1064
1348
|
}));
|
|
@@ -1068,6 +1352,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1068
1352
|
return Service2.of({
|
|
1069
1353
|
recover,
|
|
1070
1354
|
listDevices,
|
|
1355
|
+
usage,
|
|
1071
1356
|
forgetDevice,
|
|
1072
1357
|
upsertDevice,
|
|
1073
1358
|
heartbeat: (deviceId, status) => updateDeviceStatus(deviceId, status),
|
|
@@ -1079,12 +1364,22 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1079
1364
|
pendingTasksFor,
|
|
1080
1365
|
assignTask,
|
|
1081
1366
|
rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }, { kind: "requeued", detail: "Target device was unavailable during dispatch" }),
|
|
1082
|
-
acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(
|
|
1367
|
+
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
1368
|
finishTask: (taskId, deviceId, result, providerSessionId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId),
|
|
1084
1369
|
failTask: (taskId, deviceId, error) => terminal(taskId, deviceId, "failed", undefined, error),
|
|
1085
1370
|
acknowledgeCancellation: (taskId, deviceId) => terminal(taskId, deviceId, "cancelled"),
|
|
1086
1371
|
requestCancellation,
|
|
1087
1372
|
requeueTasksFor,
|
|
1373
|
+
createUpgradeOperations,
|
|
1374
|
+
listOperations,
|
|
1375
|
+
abandonOperation,
|
|
1376
|
+
pendingOperationsFor,
|
|
1377
|
+
assignOperation,
|
|
1378
|
+
rollbackOperation: (operationId) => transitionOperation(operationId, ["assigned"], "queued"),
|
|
1379
|
+
acceptOperation,
|
|
1380
|
+
finishOperation,
|
|
1381
|
+
failOperation,
|
|
1382
|
+
requeueOperationsFor,
|
|
1088
1383
|
sessionFor,
|
|
1089
1384
|
createPairing,
|
|
1090
1385
|
exchangePairing: exchangePairing2,
|
|
@@ -1092,7 +1387,7 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1092
1387
|
listAuthSessions,
|
|
1093
1388
|
revokeAuthSession
|
|
1094
1389
|
});
|
|
1095
|
-
}, migrate = (db) =>
|
|
1390
|
+
}, migrate = (db) => Effect11.try({
|
|
1096
1391
|
try: () => {
|
|
1097
1392
|
db.exec("PRAGMA journal_mode = WAL");
|
|
1098
1393
|
db.exec("PRAGMA foreign_keys = ON");
|
|
@@ -1140,6 +1435,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1140
1435
|
provider TEXT NOT NULL, session_id TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
1141
1436
|
PRIMARY KEY(thread_id, device_id, provider)
|
|
1142
1437
|
);
|
|
1438
|
+
CREATE TABLE IF NOT EXISTS device_operations (
|
|
1439
|
+
id TEXT PRIMARY KEY, kind TEXT NOT NULL, status TEXT NOT NULL,
|
|
1440
|
+
target_device_id TEXT NOT NULL REFERENCES devices(id),
|
|
1441
|
+
requested_version TEXT NOT NULL, restart INTEGER NOT NULL,
|
|
1442
|
+
result TEXT, error TEXT, created_at TEXT NOT NULL, updated_at TEXT NOT NULL,
|
|
1443
|
+
completed_at TEXT
|
|
1444
|
+
);
|
|
1445
|
+
CREATE INDEX IF NOT EXISTS device_operations_target_status
|
|
1446
|
+
ON device_operations(target_device_id, status);
|
|
1143
1447
|
CREATE TABLE IF NOT EXISTS auth_pairings (
|
|
1144
1448
|
token_hash TEXT PRIMARY KEY, label TEXT NOT NULL, roles_json TEXT NOT NULL,
|
|
1145
1449
|
expires_at TEXT NOT NULL, created_at TEXT NOT NULL, used_at TEXT
|
|
@@ -1157,15 +1461,15 @@ var PersistenceError, Service2, operationError = (operation) => (cause) => new P
|
|
|
1157
1461
|
}
|
|
1158
1462
|
},
|
|
1159
1463
|
catch: operationError("RelayStore.migrate")
|
|
1160
|
-
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2,
|
|
1464
|
+
}), layer2 = (path, retainedTerminalTasks = 1000) => Layer2.effect(Service2, Effect11.acquireRelease(Effect11.try({
|
|
1161
1465
|
try: () => new Database(path),
|
|
1162
1466
|
catch: operationError("RelayStore.open")
|
|
1163
|
-
}).pipe(
|
|
1467
|
+
}).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
1468
|
var init_store = __esm(() => {
|
|
1165
1469
|
init_src();
|
|
1166
1470
|
init_database();
|
|
1167
1471
|
init_store();
|
|
1168
|
-
PersistenceError = class PersistenceError extends
|
|
1472
|
+
PersistenceError = class PersistenceError extends Schema11.TaggedErrorClass()("RelayStore.PersistenceError", { operation: Schema11.String, message: Schema11.String }) {
|
|
1169
1473
|
};
|
|
1170
1474
|
Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
|
|
1171
1475
|
};
|
|
@@ -1177,9 +1481,9 @@ __export(exports_main, {
|
|
|
1177
1481
|
runRelay: () => runRelay,
|
|
1178
1482
|
relayListenOptions: () => relayListenOptions
|
|
1179
1483
|
});
|
|
1180
|
-
import { Effect as
|
|
1181
|
-
import { createHash as
|
|
1182
|
-
import { access as
|
|
1484
|
+
import { Effect as Effect12, ManagedRuntime, Schema as Schema12 } from "effect";
|
|
1485
|
+
import { createHash as createHash3, timingSafeEqual } from "node:crypto";
|
|
1486
|
+
import { access as access6, chmod as chmod7 } from "node:fs/promises";
|
|
1183
1487
|
import { createServer } from "node:http";
|
|
1184
1488
|
import { platform as platform5 } from "node:os";
|
|
1185
1489
|
import { WebSocketServer } from "ws";
|
|
@@ -1208,7 +1512,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1208
1512
|
if (request.method === "GET" || request.method === "HEAD") {
|
|
1209
1513
|
return Promise.resolve(undefined);
|
|
1210
1514
|
}
|
|
1211
|
-
return new Promise((
|
|
1515
|
+
return new Promise((resolve4, reject) => {
|
|
1212
1516
|
const chunks = [];
|
|
1213
1517
|
let size = 0;
|
|
1214
1518
|
const cleanup = () => {
|
|
@@ -1227,7 +1531,7 @@ var RequestError, relayListenOptions = (configuration, environment = process.env
|
|
|
1227
1531
|
};
|
|
1228
1532
|
const onEnd = () => {
|
|
1229
1533
|
cleanup();
|
|
1230
|
-
|
|
1534
|
+
resolve4(Buffer.concat(chunks, size));
|
|
1231
1535
|
};
|
|
1232
1536
|
const onError = (cause) => {
|
|
1233
1537
|
cleanup();
|
|
@@ -1276,17 +1580,17 @@ Content-Length: 0\r
|
|
|
1276
1580
|
if ("destroy" in socket && typeof socket.destroy === "function") {
|
|
1277
1581
|
socket.destroy();
|
|
1278
1582
|
}
|
|
1279
|
-
}, body = (request, decode2) =>
|
|
1583
|
+
}, body = (request, decode2) => Effect12.tryPromise({
|
|
1280
1584
|
try: () => request.json(),
|
|
1281
1585
|
catch: () => new RequestError({ status: 400, message: "Expected a JSON request body" })
|
|
1282
|
-
}).pipe(
|
|
1586
|
+
}).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
1587
|
const prompt = `${input.prompt} ${input.context ?? ""}`.toLowerCase();
|
|
1284
1588
|
const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
|
|
1285
1589
|
const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
|
|
1286
1590
|
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) =>
|
|
1591
|
+
}, chooseDevice = (input, sourceDeviceId) => Effect12.gen(function* () {
|
|
1288
1592
|
const store = yield* exports_store.Service;
|
|
1289
|
-
const devices = yield* store.listDevices().pipe(
|
|
1593
|
+
const devices = yield* store.listDevices().pipe(Effect12.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
|
|
1290
1594
|
const provider = input.provider ?? "codex";
|
|
1291
1595
|
if (input.targetDeviceId !== undefined) {
|
|
1292
1596
|
const target = devices.find((device) => device.id === input.targetDeviceId);
|
|
@@ -1313,26 +1617,26 @@ Content-Length: 0\r
|
|
|
1313
1617
|
}
|
|
1314
1618
|
return selected.id;
|
|
1315
1619
|
}), runRelay = async () => {
|
|
1316
|
-
const configuration = await
|
|
1620
|
+
const configuration = await Effect12.runPromise(loadEnvironmentConfiguration);
|
|
1317
1621
|
process.umask(63);
|
|
1318
1622
|
const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath, configuration.historyTaskLimit));
|
|
1319
1623
|
const run2 = (effect) => runtime.runPromise(effect);
|
|
1320
|
-
await run2(
|
|
1624
|
+
await run2(Effect12.gen(function* () {
|
|
1321
1625
|
const store = yield* exports_store.Service;
|
|
1322
1626
|
yield* store.recover();
|
|
1323
1627
|
}));
|
|
1324
|
-
if (platform5() !== "win32" && await
|
|
1325
|
-
await
|
|
1628
|
+
if (platform5() !== "win32" && await access6(configuration.databasePath).then(() => true).catch(() => false)) {
|
|
1629
|
+
await chmod7(configuration.databasePath, 384);
|
|
1326
1630
|
}
|
|
1327
1631
|
const hub = new Hub;
|
|
1328
1632
|
let connections = 0;
|
|
1329
|
-
const tokenDigest = (token) =>
|
|
1330
|
-
const
|
|
1633
|
+
const tokenDigest = (token) => createHash3("sha256").update(token).digest();
|
|
1634
|
+
const ownerToken2 = (token) => timingSafeEqual(tokenDigest(token), tokenDigest(configuration.token));
|
|
1331
1635
|
const authenticate = async (token, role) => {
|
|
1332
|
-
if (
|
|
1636
|
+
if (ownerToken2(token)) {
|
|
1333
1637
|
return "owner";
|
|
1334
1638
|
}
|
|
1335
|
-
return run2(
|
|
1639
|
+
return run2(Effect12.gen(function* () {
|
|
1336
1640
|
const store = yield* exports_store.Service;
|
|
1337
1641
|
return yield* store.authenticateSession(token, role);
|
|
1338
1642
|
})).catch(() => {
|
|
@@ -1347,7 +1651,7 @@ Content-Length: 0\r
|
|
|
1347
1651
|
return authenticate(authorization.slice("Bearer ".length), "client");
|
|
1348
1652
|
};
|
|
1349
1653
|
const dispatch = async (task) => {
|
|
1350
|
-
const assigned = await run2(
|
|
1654
|
+
const assigned = await run2(Effect12.gen(function* () {
|
|
1351
1655
|
const store = yield* exports_store.Service;
|
|
1352
1656
|
return yield* store.assignTask(task.id);
|
|
1353
1657
|
}));
|
|
@@ -1357,13 +1661,29 @@ Content-Length: 0\r
|
|
|
1357
1661
|
if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }))) {
|
|
1358
1662
|
return assigned;
|
|
1359
1663
|
}
|
|
1360
|
-
return run2(
|
|
1664
|
+
return run2(Effect12.gen(function* () {
|
|
1361
1665
|
const store = yield* exports_store.Service;
|
|
1362
1666
|
return yield* store.rollbackAssignment(task.id);
|
|
1363
1667
|
}));
|
|
1364
1668
|
};
|
|
1669
|
+
const dispatchOperation = async (operation) => {
|
|
1670
|
+
const assigned = await run2(Effect12.gen(function* () {
|
|
1671
|
+
const store = yield* exports_store.Service;
|
|
1672
|
+
return yield* store.assignOperation(operation.id);
|
|
1673
|
+
}));
|
|
1674
|
+
if (assigned.status !== "assigned") {
|
|
1675
|
+
return assigned;
|
|
1676
|
+
}
|
|
1677
|
+
if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "OperationAssigned", operation: assigned }))) {
|
|
1678
|
+
return assigned;
|
|
1679
|
+
}
|
|
1680
|
+
return run2(Effect12.gen(function* () {
|
|
1681
|
+
const store = yield* exports_store.Service;
|
|
1682
|
+
return yield* store.rollbackOperation(operation.id);
|
|
1683
|
+
}));
|
|
1684
|
+
};
|
|
1365
1685
|
const dispatchPending = async (deviceId) => {
|
|
1366
|
-
const tasks = await run2(
|
|
1686
|
+
const tasks = await run2(Effect12.gen(function* () {
|
|
1367
1687
|
const store = yield* exports_store.Service;
|
|
1368
1688
|
return yield* store.pendingTasksFor(deviceId);
|
|
1369
1689
|
}));
|
|
@@ -1374,6 +1694,13 @@ Content-Length: 0\r
|
|
|
1374
1694
|
await dispatch(task);
|
|
1375
1695
|
}
|
|
1376
1696
|
}
|
|
1697
|
+
const operations = await run2(Effect12.gen(function* () {
|
|
1698
|
+
const store = yield* exports_store.Service;
|
|
1699
|
+
return yield* store.pendingOperationsFor(deviceId);
|
|
1700
|
+
}));
|
|
1701
|
+
for (const operation of operations) {
|
|
1702
|
+
await dispatchOperation(operation);
|
|
1703
|
+
}
|
|
1377
1704
|
};
|
|
1378
1705
|
const sendError = (socket, code, message) => {
|
|
1379
1706
|
socket.send(JSON.stringify(SocketEvent.make({
|
|
@@ -1383,10 +1710,10 @@ Content-Length: 0\r
|
|
|
1383
1710
|
})));
|
|
1384
1711
|
};
|
|
1385
1712
|
const handleSocketEvent = async (socket, input) => {
|
|
1386
|
-
const event = await
|
|
1713
|
+
const event = await Effect12.runPromise(Effect12.try({
|
|
1387
1714
|
try: () => JSON.parse(socketText(input)),
|
|
1388
1715
|
catch: () => new RequestError({ status: 400, message: "Invalid socket JSON" })
|
|
1389
|
-
}).pipe(
|
|
1716
|
+
}).pipe(Effect12.flatMap(decodeSocketEvent))).catch((cause) => {
|
|
1390
1717
|
sendError(socket, "invalid_event", cause instanceof Error ? cause.message : String(cause));
|
|
1391
1718
|
return;
|
|
1392
1719
|
});
|
|
@@ -1443,7 +1770,7 @@ Content-Length: 0\r
|
|
|
1443
1770
|
socket.close(4009, "Device is already connected");
|
|
1444
1771
|
return;
|
|
1445
1772
|
}
|
|
1446
|
-
await run2(
|
|
1773
|
+
await run2(Effect12.gen(function* () {
|
|
1447
1774
|
const store = yield* exports_store.Service;
|
|
1448
1775
|
yield* store.upsertDevice(device);
|
|
1449
1776
|
}));
|
|
@@ -1458,7 +1785,10 @@ Content-Length: 0\r
|
|
|
1458
1785
|
const settle = (taskId) => {
|
|
1459
1786
|
socket.send(JSON.stringify(SocketEvent.make({ _tag: "TaskSettled", taskId })));
|
|
1460
1787
|
};
|
|
1461
|
-
const
|
|
1788
|
+
const settleOperation = (operationId) => {
|
|
1789
|
+
socket.send(JSON.stringify(SocketEvent.make({ _tag: "OperationSettled", operationId })));
|
|
1790
|
+
};
|
|
1791
|
+
const processed = await run2(Effect12.gen(function* () {
|
|
1462
1792
|
const store = yield* exports_store.Service;
|
|
1463
1793
|
switch (event._tag) {
|
|
1464
1794
|
case "DeviceHeartbeat":
|
|
@@ -1478,10 +1808,21 @@ Content-Length: 0\r
|
|
|
1478
1808
|
case "TaskCancelled":
|
|
1479
1809
|
yield* store.acknowledgeCancellation(event.taskId, deviceId);
|
|
1480
1810
|
return;
|
|
1811
|
+
case "OperationAccepted":
|
|
1812
|
+
yield* store.acceptOperation(event.operationId, deviceId);
|
|
1813
|
+
return;
|
|
1814
|
+
case "OperationFinished":
|
|
1815
|
+
yield* store.finishOperation(event.operationId, deviceId, event.result);
|
|
1816
|
+
return;
|
|
1817
|
+
case "OperationFailed":
|
|
1818
|
+
yield* store.failOperation(event.operationId, deviceId, event.error);
|
|
1819
|
+
return;
|
|
1481
1820
|
case "Connected":
|
|
1482
1821
|
case "TaskAssigned":
|
|
1483
1822
|
case "TaskSettled":
|
|
1484
1823
|
case "CancelTask":
|
|
1824
|
+
case "OperationAssigned":
|
|
1825
|
+
case "OperationSettled":
|
|
1485
1826
|
case "Error":
|
|
1486
1827
|
return;
|
|
1487
1828
|
}
|
|
@@ -1493,13 +1834,17 @@ Content-Length: 0\r
|
|
|
1493
1834
|
settle(event.taskId);
|
|
1494
1835
|
await dispatchPending(deviceId);
|
|
1495
1836
|
}
|
|
1837
|
+
if (processed && (event._tag === "OperationFinished" || event._tag === "OperationFailed")) {
|
|
1838
|
+
settleOperation(event.operationId);
|
|
1839
|
+
await dispatchPending(deviceId);
|
|
1840
|
+
}
|
|
1496
1841
|
};
|
|
1497
1842
|
const api = async (request, url) => {
|
|
1498
1843
|
if (url.pathname === "/api/health" && request.method === "GET") {
|
|
1499
1844
|
return json({ ok: true, version });
|
|
1500
1845
|
}
|
|
1501
1846
|
if (url.pathname === "/api/auth/pair" && request.method === "POST") {
|
|
1502
|
-
return run2(
|
|
1847
|
+
return run2(Effect12.gen(function* () {
|
|
1503
1848
|
const store = yield* exports_store.Service;
|
|
1504
1849
|
const input = yield* body(request, decodeExchangePairingInput);
|
|
1505
1850
|
return yield* store.exchangePairing(input.token);
|
|
@@ -1509,12 +1854,12 @@ Content-Length: 0\r
|
|
|
1509
1854
|
if (principal === undefined) {
|
|
1510
1855
|
return json({ error: "Unauthorized" }, 401);
|
|
1511
1856
|
}
|
|
1512
|
-
const ownerOnly = url.pathname.startsWith("/api/auth/");
|
|
1857
|
+
const ownerOnly = url.pathname.startsWith("/api/auth/") || url.pathname.startsWith("/api/operations");
|
|
1513
1858
|
if (ownerOnly && principal !== "owner") {
|
|
1514
1859
|
return json({ error: "Owner token required" }, 403);
|
|
1515
1860
|
}
|
|
1516
1861
|
const sourceDeviceId = principal === "owner" ? undefined : principal.deviceId;
|
|
1517
|
-
const effect =
|
|
1862
|
+
const effect = Effect12.gen(function* () {
|
|
1518
1863
|
const store = yield* exports_store.Service;
|
|
1519
1864
|
if (url.pathname === "/api/auth/pairings" && request.method === "POST") {
|
|
1520
1865
|
const input = yield* body(request, decodeCreatePairingInput);
|
|
@@ -1533,6 +1878,23 @@ Content-Length: 0\r
|
|
|
1533
1878
|
if (url.pathname === "/api/devices" && request.method === "GET") {
|
|
1534
1879
|
return json(yield* store.listDevices());
|
|
1535
1880
|
}
|
|
1881
|
+
if (url.pathname === "/api/usage" && request.method === "GET") {
|
|
1882
|
+
return json(yield* store.usage());
|
|
1883
|
+
}
|
|
1884
|
+
if (url.pathname === "/api/operations" && request.method === "GET") {
|
|
1885
|
+
return json(yield* store.listOperations());
|
|
1886
|
+
}
|
|
1887
|
+
if (url.pathname === "/api/operations/upgrades" && request.method === "POST") {
|
|
1888
|
+
const input = yield* body(request, decodeCreateUpgradeOperationsInput);
|
|
1889
|
+
const operations = yield* store.createUpgradeOperations(input);
|
|
1890
|
+
return json(yield* Effect12.tryPromise(() => Promise.all(operations.map(dispatchOperation))), 201);
|
|
1891
|
+
}
|
|
1892
|
+
const abandonOperation = url.pathname.match(/^\/api\/operations\/([^/]+)\/abandon$/);
|
|
1893
|
+
if (request.method === "POST" && abandonOperation?.[1] !== undefined) {
|
|
1894
|
+
const operation = yield* store.abandonOperation(yield* pathId(OperationId, abandonOperation[1]));
|
|
1895
|
+
hub.sendToDevice(operation.targetDeviceId, SocketEvent.make({ _tag: "OperationSettled", operationId: operation.id }));
|
|
1896
|
+
return json(operation);
|
|
1897
|
+
}
|
|
1536
1898
|
const forgetDevice = url.pathname.match(/^\/api\/devices\/([^/]+)\/forget$/);
|
|
1537
1899
|
if (request.method === "POST" && forgetDevice?.[1] !== undefined) {
|
|
1538
1900
|
if (principal !== "owner") {
|
|
@@ -1554,7 +1916,7 @@ Content-Length: 0\r
|
|
|
1554
1916
|
const target = yield* chooseDevice(input, sourceDeviceId);
|
|
1555
1917
|
const providerSessionId = input.threadId === undefined ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
|
|
1556
1918
|
const task2 = yield* store.createDelegation(input, target, sourceDeviceId, providerSessionId);
|
|
1557
|
-
return json(yield*
|
|
1919
|
+
return json(yield* Effect12.tryPromise(() => dispatch(task2)), 201);
|
|
1558
1920
|
}
|
|
1559
1921
|
const task = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
|
|
1560
1922
|
if (request.method === "GET" && task?.[1] !== undefined) {
|
|
@@ -1578,8 +1940,8 @@ Content-Length: 0\r
|
|
|
1578
1940
|
return json(yield* store.threadContext(yield* pathId(ThreadId, thread[1])));
|
|
1579
1941
|
}
|
|
1580
1942
|
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,
|
|
1943
|
+
}).pipe(Effect12.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
|
|
1944
|
+
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
1945
|
message: cause.message
|
|
1584
1946
|
})));
|
|
1585
1947
|
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 +2014,10 @@ Content-Length: 0\r
|
|
|
1652
2014
|
if (deviceId === undefined) {
|
|
1653
2015
|
return;
|
|
1654
2016
|
}
|
|
1655
|
-
run2(
|
|
2017
|
+
run2(Effect12.gen(function* () {
|
|
1656
2018
|
const store = yield* exports_store.Service;
|
|
1657
2019
|
yield* store.requeueTasksFor(deviceId);
|
|
2020
|
+
yield* store.requeueOperationsFor(deviceId);
|
|
1658
2021
|
yield* store.markDeviceOffline(deviceId);
|
|
1659
2022
|
})).catch((cause) => console.error(cause));
|
|
1660
2023
|
});
|
|
@@ -1679,11 +2042,11 @@ Content-Length: 0\r
|
|
|
1679
2042
|
websocketServer.emit("connection", websocket, request);
|
|
1680
2043
|
});
|
|
1681
2044
|
});
|
|
1682
|
-
await new Promise((
|
|
2045
|
+
await new Promise((resolve4, reject) => {
|
|
1683
2046
|
server.once("error", reject);
|
|
1684
2047
|
server.listen(relayListenOptions(configuration), () => {
|
|
1685
2048
|
server.off("error", reject);
|
|
1686
|
-
|
|
2049
|
+
resolve4();
|
|
1687
2050
|
});
|
|
1688
2051
|
});
|
|
1689
2052
|
const address = server.address();
|
|
@@ -1698,7 +2061,7 @@ Content-Length: 0\r
|
|
|
1698
2061
|
for (const socket of websocketServer.clients) {
|
|
1699
2062
|
socket.close(1001, "Relay shutting down");
|
|
1700
2063
|
}
|
|
1701
|
-
await new Promise((
|
|
2064
|
+
await new Promise((resolve4) => server.close(() => resolve4()));
|
|
1702
2065
|
websocketServer.close();
|
|
1703
2066
|
await runtime.dispose();
|
|
1704
2067
|
};
|
|
@@ -1710,9 +2073,9 @@ var init_main = __esm(async () => {
|
|
|
1710
2073
|
init_config();
|
|
1711
2074
|
init_hub();
|
|
1712
2075
|
init_store();
|
|
1713
|
-
RequestError = class RequestError extends
|
|
1714
|
-
status:
|
|
1715
|
-
message:
|
|
2076
|
+
RequestError = class RequestError extends Schema12.TaggedErrorClass()("Relay.RequestError", {
|
|
2077
|
+
status: Schema12.Int,
|
|
2078
|
+
message: Schema12.String
|
|
1716
2079
|
}) {
|
|
1717
2080
|
};
|
|
1718
2081
|
if (false) {}
|
|
@@ -1720,7 +2083,7 @@ var init_main = __esm(async () => {
|
|
|
1720
2083
|
|
|
1721
2084
|
// apps/device/src/main.ts
|
|
1722
2085
|
init_src();
|
|
1723
|
-
import { Effect as
|
|
2086
|
+
import { Effect as Effect13 } from "effect";
|
|
1724
2087
|
|
|
1725
2088
|
// packages/client/src/index.ts
|
|
1726
2089
|
init_src();
|
|
@@ -1840,7 +2203,11 @@ var make = (options) => {
|
|
|
1840
2203
|
body: JSON.stringify(input)
|
|
1841
2204
|
}),
|
|
1842
2205
|
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" })
|
|
2206
|
+
revokeAuthSession: (sessionId) => request("RelayClient.revokeAuthSession", `/api/auth/sessions/${encodeURIComponent(sessionId)}/revoke`, AuthSession, { method: "POST" }),
|
|
2207
|
+
usage: () => request("RelayClient.usage", "/api/usage", UsageSummary),
|
|
2208
|
+
createUpgradeOperations: (input) => request("RelayClient.createUpgradeOperations", "/api/operations/upgrades", Schema2.Array(DeviceOperation), { method: "POST", body: JSON.stringify(input) }),
|
|
2209
|
+
operations: () => request("RelayClient.operations", "/api/operations", Schema2.Array(DeviceOperation)),
|
|
2210
|
+
abandonOperation: (operationId) => request("RelayClient.abandonOperation", `/api/operations/${encodeURIComponent(operationId)}/abandon`, DeviceOperation, { method: "POST" })
|
|
1844
2211
|
});
|
|
1845
2212
|
};
|
|
1846
2213
|
var exchangePairing = (baseUrl, input) => Effect.gen(function* () {
|
|
@@ -2326,10 +2693,10 @@ var run = (options) => Effect2.tryPromise({
|
|
|
2326
2693
|
});
|
|
2327
2694
|
|
|
2328
2695
|
// apps/device/src/cli.ts
|
|
2329
|
-
import { Effect as
|
|
2330
|
-
import { access as
|
|
2331
|
-
import { homedir as
|
|
2332
|
-
import { dirname as
|
|
2696
|
+
import { Effect as Effect7, Schema as Schema8 } from "effect";
|
|
2697
|
+
import { access as access4, mkdir as mkdir5, readFile as readFile4, stat as stat3, writeFile as writeFile6 } from "node:fs/promises";
|
|
2698
|
+
import { homedir as homedir4 } from "node:os";
|
|
2699
|
+
import { dirname as dirname5, join as join6 } from "node:path";
|
|
2333
2700
|
|
|
2334
2701
|
// skills/cohall/SKILL.md
|
|
2335
2702
|
var SKILL_default = `---
|
|
@@ -2553,6 +2920,7 @@ var defaultRelayDataDirectory = () => {
|
|
|
2553
2920
|
}
|
|
2554
2921
|
return join2(process.env.XDG_DATA_HOME ?? join2(homedir2(), ".local", "share"), "cohall");
|
|
2555
2922
|
};
|
|
2923
|
+
var relayDataDirectory = () => resolve(process.env.COHALL_DATA_DIR ?? defaultRelayDataDirectory());
|
|
2556
2924
|
var configurationPath = () => resolve(process.env.COHALL_CONFIG ?? join2(defaultConfigDirectory(), "config.json"));
|
|
2557
2925
|
var normalizeRelayUrl = (input) => {
|
|
2558
2926
|
const url = new URL(input);
|
|
@@ -2684,7 +3052,7 @@ var loadClientConfiguration = Effect3.tryPromise({
|
|
|
2684
3052
|
var loadOwnerConfiguration = Effect3.tryPromise({
|
|
2685
3053
|
try: async () => {
|
|
2686
3054
|
const stored = await storedOrDefaults();
|
|
2687
|
-
const ownerTokenPath = join2(
|
|
3055
|
+
const ownerTokenPath = join2(relayDataDirectory(), "owner-token");
|
|
2688
3056
|
const token = process.env.COHALL_TOKEN ?? await readFile(ownerTokenPath, "utf8").then((value) => value.trim()).catch(() => {
|
|
2689
3057
|
return;
|
|
2690
3058
|
});
|
|
@@ -2825,6 +3193,176 @@ var threadContext = Effect4.fn("Cohall.threadContext")(function* (client, thread
|
|
|
2825
3193
|
return yield* client.threadContext(threadId);
|
|
2826
3194
|
});
|
|
2827
3195
|
|
|
3196
|
+
// apps/device/src/device-overview.ts
|
|
3197
|
+
var deviceVersions = (devices, localVersion) => ({
|
|
3198
|
+
local_version: localVersion,
|
|
3199
|
+
current: devices.filter((device) => device.version === localVersion).length,
|
|
3200
|
+
different: devices.filter((device) => device.version !== localVersion).length,
|
|
3201
|
+
devices: devices.map((device) => ({
|
|
3202
|
+
id: device.id,
|
|
3203
|
+
name: device.name,
|
|
3204
|
+
status: device.status,
|
|
3205
|
+
version: device.version,
|
|
3206
|
+
current: device.version === localVersion
|
|
3207
|
+
}))
|
|
3208
|
+
});
|
|
3209
|
+
var allDeviceHealth = (devices, localVersion) => {
|
|
3210
|
+
const summaries = devices.map((device) => {
|
|
3211
|
+
const warnings = [
|
|
3212
|
+
...device.status === "offline" ? ["Device is offline"] : [],
|
|
3213
|
+
...device.providers.length === 0 ? ["No provider executable is available"] : [],
|
|
3214
|
+
...device.version === localVersion ? [] : [`Running Cohall ${device.version}; local CLI is ${localVersion}`]
|
|
3215
|
+
];
|
|
3216
|
+
return {
|
|
3217
|
+
id: device.id,
|
|
3218
|
+
name: device.name,
|
|
3219
|
+
hostname: device.hostname,
|
|
3220
|
+
platform: device.platform,
|
|
3221
|
+
architecture: device.architecture,
|
|
3222
|
+
status: device.status,
|
|
3223
|
+
version: device.version,
|
|
3224
|
+
providers: device.providers,
|
|
3225
|
+
workspaces: device.workspaces.map((workspace) => workspace.path),
|
|
3226
|
+
last_seen_at: device.lastSeenAt,
|
|
3227
|
+
warnings
|
|
3228
|
+
};
|
|
3229
|
+
});
|
|
3230
|
+
return {
|
|
3231
|
+
healthy: summaries.length > 0 && summaries.every((device) => device.warnings.length === 0),
|
|
3232
|
+
total: devices.length,
|
|
3233
|
+
online: devices.filter((device) => device.status === "online").length,
|
|
3234
|
+
busy: devices.filter((device) => device.status === "busy").length,
|
|
3235
|
+
offline: devices.filter((device) => device.status === "offline").length,
|
|
3236
|
+
warnings: summaries.length === 0 ? ["No Cohall devices are registered"] : [],
|
|
3237
|
+
devices: summaries
|
|
3238
|
+
};
|
|
3239
|
+
};
|
|
3240
|
+
|
|
3241
|
+
// apps/device/src/setup.ts
|
|
3242
|
+
import { Effect as Effect5 } from "effect";
|
|
3243
|
+
import { hostname as hostname2 } from "node:os";
|
|
3244
|
+
import { Writable } from "node:stream";
|
|
3245
|
+
import { createInterface } from "node:readline/promises";
|
|
3246
|
+
var joinRelay = async (options) => {
|
|
3247
|
+
const draft = await makeStoredConfiguration({
|
|
3248
|
+
relayUrl: options.relayUrl,
|
|
3249
|
+
...options.deviceName === undefined ? {} : { deviceName: options.deviceName },
|
|
3250
|
+
...options.workspaces === undefined ? {} : { workspaces: options.workspaces },
|
|
3251
|
+
...options.providers === undefined ? {} : { providers: options.providers }
|
|
3252
|
+
});
|
|
3253
|
+
if (!options.clientOnly && draft.workspaces.length === 0) {
|
|
3254
|
+
throw new Error("At least one workspace is required when joining a device");
|
|
3255
|
+
}
|
|
3256
|
+
const paired = await Effect5.runPromise(exchangePairing(draft.relayUrl, { token: options.token }));
|
|
3257
|
+
const clientToken = paired.credentials.find((credential) => credential.session.role === "client")?.token;
|
|
3258
|
+
const deviceCredential = paired.credentials.find((credential) => credential.session.role === "device");
|
|
3259
|
+
if (options.clientOnly && clientToken === undefined) {
|
|
3260
|
+
throw new Error("The pairing credential did not include client access");
|
|
3261
|
+
}
|
|
3262
|
+
if (!options.clientOnly && deviceCredential === undefined) {
|
|
3263
|
+
throw new Error("The pairing credential did not include device access");
|
|
3264
|
+
}
|
|
3265
|
+
const {
|
|
3266
|
+
clientToken: _retainedClientToken,
|
|
3267
|
+
deviceToken: _retainedDeviceToken,
|
|
3268
|
+
...configurationDraft
|
|
3269
|
+
} = draft;
|
|
3270
|
+
const configuration = StoredConfiguration.make({
|
|
3271
|
+
...configurationDraft,
|
|
3272
|
+
deviceId: options.clientOnly ? configurationDraft.deviceId : deviceCredential?.session.deviceId ?? configurationDraft.deviceId,
|
|
3273
|
+
...clientToken === undefined ? {} : { clientToken },
|
|
3274
|
+
...options.clientOnly || deviceCredential === undefined ? {} : { deviceToken: deviceCredential.token }
|
|
3275
|
+
});
|
|
3276
|
+
await writeStoredConfiguration(configuration);
|
|
3277
|
+
return {
|
|
3278
|
+
configuration,
|
|
3279
|
+
roles: options.clientOnly ? paired.credentials.map((credential) => credential.session.role).filter((role) => role === "client") : paired.credentials.map((credential) => credential.session.role)
|
|
3280
|
+
};
|
|
3281
|
+
};
|
|
3282
|
+
var terminalPrompter = () => {
|
|
3283
|
+
let muted = false;
|
|
3284
|
+
const output = new Writable({
|
|
3285
|
+
write(chunk, _encoding, callback) {
|
|
3286
|
+
if (!muted) {
|
|
3287
|
+
process.stderr.write(chunk);
|
|
3288
|
+
}
|
|
3289
|
+
callback();
|
|
3290
|
+
}
|
|
3291
|
+
});
|
|
3292
|
+
const terminal = createInterface({ input: process.stdin, output, terminal: true });
|
|
3293
|
+
return {
|
|
3294
|
+
answer: async (label, fallback) => {
|
|
3295
|
+
const value = (await terminal.question(`${label} [${fallback}]: `)).trim();
|
|
3296
|
+
return value || fallback;
|
|
3297
|
+
},
|
|
3298
|
+
secret: async (label) => {
|
|
3299
|
+
const pending = terminal.question(`${label}: `);
|
|
3300
|
+
muted = true;
|
|
3301
|
+
try {
|
|
3302
|
+
return (await pending).trim();
|
|
3303
|
+
} finally {
|
|
3304
|
+
muted = false;
|
|
3305
|
+
process.stderr.write(`
|
|
3306
|
+
`);
|
|
3307
|
+
}
|
|
3308
|
+
},
|
|
3309
|
+
close: () => terminal.close()
|
|
3310
|
+
};
|
|
3311
|
+
};
|
|
3312
|
+
var guidedSetupInput = async (options, prompter) => {
|
|
3313
|
+
const existing = await readStoredConfiguration();
|
|
3314
|
+
const relayUrl = normalizeRelayUrl(options.relayUrl ?? await prompter.answer("Relay URL", existing?.relayUrl ?? "http://127.0.0.1:8787"));
|
|
3315
|
+
const credentials = credentialsForRelay(existing, relayUrl);
|
|
3316
|
+
const hasRequiredCredentials = credentials.clientToken !== undefined && (options.clientOnly || credentials.deviceToken !== undefined);
|
|
3317
|
+
const workspaces = options.clientOnly ? existing?.workspaces ?? [] : options.workspaces.length > 0 ? options.workspaces : [await prompter.answer("Workspace root", existing?.workspaces[0] ?? options.cwd)];
|
|
3318
|
+
const deviceName = options.clientOnly ? undefined : options.deviceName ?? await prompter.answer("Device name", existing?.deviceName ?? hostname2());
|
|
3319
|
+
const providers = options.providers ?? (options.clientOnly ? "auto" : await prompter.answer("Providers", existing?.providers?.join(",") ?? "auto"));
|
|
3320
|
+
const token = options.token ?? (hasRequiredCredentials ? undefined : await prompter.secret("Pairing token"));
|
|
3321
|
+
if (!hasRequiredCredentials && (token === undefined || token.length === 0)) {
|
|
3322
|
+
throw new Error("A pairing token is required");
|
|
3323
|
+
}
|
|
3324
|
+
if (token !== undefined && new TextEncoder().encode(token).byteLength > 256) {
|
|
3325
|
+
throw new Error("Pairing token exceeds 256 bytes");
|
|
3326
|
+
}
|
|
3327
|
+
return {
|
|
3328
|
+
relayUrl,
|
|
3329
|
+
...token === undefined ? {} : { token },
|
|
3330
|
+
...deviceName === undefined ? {} : { deviceName },
|
|
3331
|
+
workspaces,
|
|
3332
|
+
providers,
|
|
3333
|
+
reusedConfiguration: hasRequiredCredentials && token === undefined
|
|
3334
|
+
};
|
|
3335
|
+
};
|
|
3336
|
+
|
|
3337
|
+
// apps/device/src/relay-migration.ts
|
|
3338
|
+
init_src();
|
|
3339
|
+
import { Effect as Effect6, Schema as Schema7 } from "effect";
|
|
3340
|
+
import { createHash } from "node:crypto";
|
|
3341
|
+
import { createReadStream } from "node:fs";
|
|
3342
|
+
import {
|
|
3343
|
+
access as access3,
|
|
3344
|
+
chmod as chmod5,
|
|
3345
|
+
copyFile,
|
|
3346
|
+
lstat,
|
|
3347
|
+
mkdir as mkdir4,
|
|
3348
|
+
mkdtemp as mkdtemp2,
|
|
3349
|
+
readFile as readFile3,
|
|
3350
|
+
realpath as realpath4,
|
|
3351
|
+
rename as rename3,
|
|
3352
|
+
rm as rm3,
|
|
3353
|
+
stat as stat2,
|
|
3354
|
+
writeFile as writeFile5
|
|
3355
|
+
} from "node:fs/promises";
|
|
3356
|
+
import { basename, dirname as dirname4, join as join5, resolve as resolve2 } from "node:path";
|
|
3357
|
+
import { backup as sqliteBackup, DatabaseSync } from "node:sqlite";
|
|
3358
|
+
import { WebSocket } from "ws";
|
|
3359
|
+
|
|
3360
|
+
// apps/device/src/service.ts
|
|
3361
|
+
import { execFile as execFile3 } from "node:child_process";
|
|
3362
|
+
import { chmod as chmod4, mkdir as mkdir3, realpath as realpath3, writeFile as writeFile4 } from "node:fs/promises";
|
|
3363
|
+
import { homedir as homedir3 } from "node:os";
|
|
3364
|
+
import { dirname as dirname3, join as join4 } from "node:path";
|
|
3365
|
+
|
|
2828
3366
|
// apps/device/src/upgrade.ts
|
|
2829
3367
|
import { Schema as Schema6 } from "effect";
|
|
2830
3368
|
import { execFile as execFile2 } from "node:child_process";
|
|
@@ -3288,6 +3826,466 @@ var upgrade = async (options) => {
|
|
|
3288
3826
|
};
|
|
3289
3827
|
};
|
|
3290
3828
|
|
|
3829
|
+
// apps/device/src/service.ts
|
|
3830
|
+
var xml = (value) => value.replaceAll("&", "&").replaceAll("<", "<").replaceAll(">", ">").replaceAll('"', """).replaceAll("'", "'");
|
|
3831
|
+
var systemdArgument = (value) => `"${value.replaceAll("\\", "\\\\").replaceAll('"', "\\\"")}"`;
|
|
3832
|
+
var deviceServicePlan = (options) => {
|
|
3833
|
+
if (options.platform === "linux") {
|
|
3834
|
+
const path = join4(options.home, ".config", "systemd", "user", "cohall-device.service");
|
|
3835
|
+
return {
|
|
3836
|
+
file: {
|
|
3837
|
+
path,
|
|
3838
|
+
mode: 384,
|
|
3839
|
+
content: `[Unit]
|
|
3840
|
+
Description=Cohall device agent
|
|
3841
|
+
After=network-online.target
|
|
3842
|
+
Wants=network-online.target
|
|
3843
|
+
|
|
3844
|
+
[Service]
|
|
3845
|
+
Type=simple
|
|
3846
|
+
Environment=PATH=%h/.local/bin:%h/.npm-global/bin:%h/.bun/bin:%h/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
|
|
3847
|
+
ExecStart=${systemdArgument(options.entrypoint)} device
|
|
3848
|
+
Restart=always
|
|
3849
|
+
RestartSec=3
|
|
3850
|
+
UMask=0077
|
|
3851
|
+
NoNewPrivileges=true
|
|
3852
|
+
PrivateTmp=true
|
|
3853
|
+
|
|
3854
|
+
[Install]
|
|
3855
|
+
WantedBy=default.target
|
|
3856
|
+
`
|
|
3857
|
+
},
|
|
3858
|
+
commands: [
|
|
3859
|
+
{ command: "systemctl", arguments: ["--user", "daemon-reload"] },
|
|
3860
|
+
{ command: "systemctl", arguments: ["--user", "enable", "--now", "cohall-device.service"] }
|
|
3861
|
+
],
|
|
3862
|
+
note: "The device starts after login. Run loginctl enable-linger $USER if it must run before login."
|
|
3863
|
+
};
|
|
3864
|
+
}
|
|
3865
|
+
if (options.platform === "darwin") {
|
|
3866
|
+
const label = "com.cohall.device";
|
|
3867
|
+
const path = join4(options.home, "Library", "LaunchAgents", `${label}.plist`);
|
|
3868
|
+
const domain = `gui/${options.uid ?? 0}`;
|
|
3869
|
+
return {
|
|
3870
|
+
file: {
|
|
3871
|
+
path,
|
|
3872
|
+
mode: 384,
|
|
3873
|
+
content: `<?xml version="1.0" encoding="UTF-8"?>
|
|
3874
|
+
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
3875
|
+
<plist version="1.0">
|
|
3876
|
+
<dict>
|
|
3877
|
+
<key>Label</key>
|
|
3878
|
+
<string>${label}</string>
|
|
3879
|
+
<key>ProgramArguments</key>
|
|
3880
|
+
<array>
|
|
3881
|
+
<string>${xml(options.entrypoint)}</string>
|
|
3882
|
+
<string>device</string>
|
|
3883
|
+
</array>
|
|
3884
|
+
<key>EnvironmentVariables</key>
|
|
3885
|
+
<dict>
|
|
3886
|
+
<key>PATH</key>
|
|
3887
|
+
<string>/opt/homebrew/bin:/usr/local/bin:/usr/bin:/bin</string>
|
|
3888
|
+
</dict>
|
|
3889
|
+
<key>RunAtLoad</key>
|
|
3890
|
+
<true/>
|
|
3891
|
+
<key>KeepAlive</key>
|
|
3892
|
+
<dict>
|
|
3893
|
+
<key>NetworkState</key>
|
|
3894
|
+
<true/>
|
|
3895
|
+
<key>SuccessfulExit</key>
|
|
3896
|
+
<false/>
|
|
3897
|
+
</dict>
|
|
3898
|
+
<key>ThrottleInterval</key>
|
|
3899
|
+
<integer>3</integer>
|
|
3900
|
+
<key>ProcessType</key>
|
|
3901
|
+
<string>Background</string>
|
|
3902
|
+
</dict>
|
|
3903
|
+
</plist>
|
|
3904
|
+
`
|
|
3905
|
+
},
|
|
3906
|
+
commands: [
|
|
3907
|
+
{ command: "launchctl", arguments: ["bootout", domain, path] },
|
|
3908
|
+
{ command: "launchctl", arguments: ["bootstrap", domain, path] },
|
|
3909
|
+
{ command: "launchctl", arguments: ["kickstart", "-k", `${domain}/${label}`] }
|
|
3910
|
+
]
|
|
3911
|
+
};
|
|
3912
|
+
}
|
|
3913
|
+
throw new Error("Automatic device service installation supports Linux and macOS");
|
|
3914
|
+
};
|
|
3915
|
+
var defaultRunner2 = {
|
|
3916
|
+
run: async (command, arguments_, timeoutMs = 60000) => {
|
|
3917
|
+
const executable = await trustedExecutable(command);
|
|
3918
|
+
return new Promise((resolve2) => {
|
|
3919
|
+
execFile3(executable, [...arguments_], { encoding: "utf8", maxBuffer: 1024 * 1024, timeout: timeoutMs, windowsHide: true }, (error, stdout, stderr) => resolve2({
|
|
3920
|
+
exitCode: typeof error?.code === "number" ? error.code : error === null ? 0 : 1,
|
|
3921
|
+
stdout,
|
|
3922
|
+
stderr,
|
|
3923
|
+
...error === null ? {} : { error: error.message }
|
|
3924
|
+
}));
|
|
3925
|
+
});
|
|
3926
|
+
}
|
|
3927
|
+
};
|
|
3928
|
+
var checked2 = async (runner, command, arguments_, allowFailure = false) => {
|
|
3929
|
+
const result = await runner.run(command, arguments_, 60000);
|
|
3930
|
+
if (result.exitCode === 0 || allowFailure) {
|
|
3931
|
+
return result;
|
|
3932
|
+
}
|
|
3933
|
+
throw new Error(`${command} failed: ${result.stderr.trim() || result.stdout.trim() || result.error || `status ${result.exitCode}`}`);
|
|
3934
|
+
};
|
|
3935
|
+
var installDeviceService = async (options = {}) => {
|
|
3936
|
+
const entrypoint = await realpath3(options.entrypoint ?? process.argv[1] ?? "");
|
|
3937
|
+
packageInstallation(entrypoint);
|
|
3938
|
+
const platform3 = options.platform ?? process.platform;
|
|
3939
|
+
const runner = options.runner ?? defaultRunner2;
|
|
3940
|
+
if (platform3 === "win32") {
|
|
3941
|
+
const script = join4(dirname3(dirname3(entrypoint)), "deploy", "windows", "install-device.ps1");
|
|
3942
|
+
await checked2(runner, "powershell.exe", [
|
|
3943
|
+
"-NoProfile",
|
|
3944
|
+
"-ExecutionPolicy",
|
|
3945
|
+
"Bypass",
|
|
3946
|
+
"-File",
|
|
3947
|
+
script
|
|
3948
|
+
]);
|
|
3949
|
+
return { installed: "scheduled-task:Cohall Device" };
|
|
3950
|
+
}
|
|
3951
|
+
const uid = options.uid ?? process.getuid?.();
|
|
3952
|
+
const plan = deviceServicePlan({
|
|
3953
|
+
platform: platform3,
|
|
3954
|
+
entrypoint,
|
|
3955
|
+
home: options.home ?? homedir3(),
|
|
3956
|
+
...uid === undefined ? {} : { uid }
|
|
3957
|
+
});
|
|
3958
|
+
await mkdir3(dirname3(plan.file.path), { recursive: true, mode: 448 });
|
|
3959
|
+
await writeFile4(plan.file.path, plan.file.content, { mode: plan.file.mode });
|
|
3960
|
+
await chmod4(plan.file.path, plan.file.mode);
|
|
3961
|
+
for (const [index, command] of plan.commands.entries()) {
|
|
3962
|
+
await checked2(runner, command.command, command.arguments, platform3 === "darwin" && index === 0);
|
|
3963
|
+
}
|
|
3964
|
+
return {
|
|
3965
|
+
installed: plan.file.path,
|
|
3966
|
+
...plan.note === undefined ? {} : { note: plan.note }
|
|
3967
|
+
};
|
|
3968
|
+
};
|
|
3969
|
+
var restartDeviceService = async (options = {}) => {
|
|
3970
|
+
const platform3 = options.platform ?? process.platform;
|
|
3971
|
+
const runner = options.runner ?? defaultRunner2;
|
|
3972
|
+
if (platform3 === "linux") {
|
|
3973
|
+
const service = "cohall-device.service";
|
|
3974
|
+
const check = await runner.run("systemctl", ["--user", "is-active", "--quiet", service], 1e4);
|
|
3975
|
+
if (check.exitCode !== 0) {
|
|
3976
|
+
return { running: false, restarted: false };
|
|
3977
|
+
}
|
|
3978
|
+
await checked2(runner, "systemctl", ["--user", "restart", service]);
|
|
3979
|
+
return { running: true, restarted: true, service };
|
|
3980
|
+
}
|
|
3981
|
+
if (platform3 === "darwin") {
|
|
3982
|
+
const service = "com.cohall.device";
|
|
3983
|
+
const target = `gui/${options.uid ?? process.getuid?.() ?? 0}/${service}`;
|
|
3984
|
+
const check = await runner.run("launchctl", ["print", target], 1e4);
|
|
3985
|
+
if (check.exitCode !== 0) {
|
|
3986
|
+
return { running: false, restarted: false };
|
|
3987
|
+
}
|
|
3988
|
+
await checked2(runner, "launchctl", ["kickstart", "-k", target]);
|
|
3989
|
+
return { running: true, restarted: true, service };
|
|
3990
|
+
}
|
|
3991
|
+
if (platform3 === "win32") {
|
|
3992
|
+
const service = "Cohall Device";
|
|
3993
|
+
const check = await runner.run("schtasks.exe", ["/Query", "/TN", service], 1e4);
|
|
3994
|
+
if (check.exitCode !== 0) {
|
|
3995
|
+
return { running: false, restarted: false };
|
|
3996
|
+
}
|
|
3997
|
+
await checked2(runner, "schtasks.exe", ["/End", "/TN", service], true);
|
|
3998
|
+
await checked2(runner, "schtasks.exe", ["/Run", "/TN", service]);
|
|
3999
|
+
return { running: true, restarted: true, service };
|
|
4000
|
+
}
|
|
4001
|
+
return { running: false, restarted: false };
|
|
4002
|
+
};
|
|
4003
|
+
|
|
4004
|
+
// apps/device/src/relay-migration.ts
|
|
4005
|
+
var sha256 = Schema7.String.check(Schema7.isPattern(/^[a-f0-9]{64}$/, { expected: "a lowercase SHA-256 digest" }));
|
|
4006
|
+
var RelayBackupManifest = Schema7.Struct({
|
|
4007
|
+
formatVersion: Schema7.Literal(1),
|
|
4008
|
+
cohallVersion: Schema7.NonEmptyString,
|
|
4009
|
+
createdAt: Timestamp,
|
|
4010
|
+
databaseSha256: sha256,
|
|
4011
|
+
ownerTokenSha256: sha256
|
|
4012
|
+
});
|
|
4013
|
+
var exists = (path) => access3(path).then(() => true).catch(() => false);
|
|
4014
|
+
var digest = async (path) => {
|
|
4015
|
+
const hash = createHash("sha256");
|
|
4016
|
+
for await (const chunk of createReadStream(path)) {
|
|
4017
|
+
hash.update(chunk);
|
|
4018
|
+
}
|
|
4019
|
+
return hash.digest("hex");
|
|
4020
|
+
};
|
|
4021
|
+
var verifyBackupMember = async (path, maximumBytes) => {
|
|
4022
|
+
const metadata = await lstat(path);
|
|
4023
|
+
if (!metadata.isFile()) {
|
|
4024
|
+
throw new Error(`Relay backup member must be a regular file: ${path}`);
|
|
4025
|
+
}
|
|
4026
|
+
if (maximumBytes !== undefined && metadata.size > maximumBytes) {
|
|
4027
|
+
throw new Error(`Relay backup member exceeds ${maximumBytes} bytes: ${path}`);
|
|
4028
|
+
}
|
|
4029
|
+
};
|
|
4030
|
+
var readManifest = async (path) => {
|
|
4031
|
+
await verifyBackupMember(path, 16384);
|
|
4032
|
+
return Schema7.decodeUnknownSync(RelayBackupManifest)(JSON.parse(await readFile3(path, "utf8")));
|
|
4033
|
+
};
|
|
4034
|
+
var verifyBackupDestinationParent = async (path) => {
|
|
4035
|
+
if (process.platform === "win32") {
|
|
4036
|
+
return;
|
|
4037
|
+
}
|
|
4038
|
+
for (let current = await realpath4(path);; current = dirname4(current)) {
|
|
4039
|
+
const metadata = await stat2(current);
|
|
4040
|
+
const writableByOthers = (metadata.mode & 18) !== 0;
|
|
4041
|
+
const sticky = (metadata.mode & 512) !== 0;
|
|
4042
|
+
if (writableByOthers && !sticky) {
|
|
4043
|
+
throw new Error(`Backup destination ancestors must not be group- or world-writable unless they use the sticky bit: ${current}`);
|
|
4044
|
+
}
|
|
4045
|
+
const parent = dirname4(current);
|
|
4046
|
+
if (parent === current) {
|
|
4047
|
+
return;
|
|
4048
|
+
}
|
|
4049
|
+
}
|
|
4050
|
+
};
|
|
4051
|
+
var ownerToken = async (dataDirectory) => {
|
|
4052
|
+
const path = join5(dataDirectory, "owner-token");
|
|
4053
|
+
const configured = process.env.COHALL_TOKEN;
|
|
4054
|
+
if (configured !== undefined) {
|
|
4055
|
+
return configured;
|
|
4056
|
+
}
|
|
4057
|
+
if (!await exists(path)) {
|
|
4058
|
+
throw new Error(`Relay owner token was not found at ${path}`);
|
|
4059
|
+
}
|
|
4060
|
+
if ((await stat2(path)).size > 512) {
|
|
4061
|
+
throw new Error("Relay owner token file exceeds 512 bytes");
|
|
4062
|
+
}
|
|
4063
|
+
return (await readFile3(path, "utf8")).trim();
|
|
4064
|
+
};
|
|
4065
|
+
var verifyDatabase = (path) => {
|
|
4066
|
+
const database = new DatabaseSync(path, { readOnly: true });
|
|
4067
|
+
try {
|
|
4068
|
+
const result = database.prepare("PRAGMA quick_check").get();
|
|
4069
|
+
if (result === undefined || Object.values(result)[0] !== "ok") {
|
|
4070
|
+
throw new Error(`SQLite integrity check failed for ${path}`);
|
|
4071
|
+
}
|
|
4072
|
+
} finally {
|
|
4073
|
+
database.close();
|
|
4074
|
+
}
|
|
4075
|
+
};
|
|
4076
|
+
var backupRelay = async (destinationInput) => {
|
|
4077
|
+
const sourceDirectory = relayDataDirectory();
|
|
4078
|
+
const sourceDatabase = join5(sourceDirectory, "cohall.db");
|
|
4079
|
+
if (!await exists(sourceDatabase)) {
|
|
4080
|
+
throw new Error(`Relay database was not found at ${sourceDatabase}`);
|
|
4081
|
+
}
|
|
4082
|
+
const destination = resolve2(destinationInput);
|
|
4083
|
+
if (await exists(destination)) {
|
|
4084
|
+
throw new Error(`Backup destination already exists: ${destination}`);
|
|
4085
|
+
}
|
|
4086
|
+
await verifyBackupDestinationParent(dirname4(destination));
|
|
4087
|
+
await mkdir4(destination, { mode: 448 });
|
|
4088
|
+
try {
|
|
4089
|
+
const databasePath = join5(destination, "cohall.db");
|
|
4090
|
+
const ownerTokenPath = join5(destination, "owner-token");
|
|
4091
|
+
const database = new DatabaseSync(sourceDatabase, { readOnly: true });
|
|
4092
|
+
try {
|
|
4093
|
+
await sqliteBackup(database, databasePath);
|
|
4094
|
+
} finally {
|
|
4095
|
+
database.close();
|
|
4096
|
+
}
|
|
4097
|
+
verifyDatabase(databasePath);
|
|
4098
|
+
const token = await ownerToken(sourceDirectory);
|
|
4099
|
+
if (token.length < 32 || new TextEncoder().encode(token).byteLength > 512) {
|
|
4100
|
+
throw new Error("The relay owner token must contain between 32 and 512 bytes");
|
|
4101
|
+
}
|
|
4102
|
+
await writeFile5(ownerTokenPath, `${token}
|
|
4103
|
+
`, { mode: 384 });
|
|
4104
|
+
await Promise.all([chmod5(databasePath, 384), chmod5(ownerTokenPath, 384)]);
|
|
4105
|
+
const createdAt = Timestamp.make(new Date().toISOString());
|
|
4106
|
+
const manifest = RelayBackupManifest.make({
|
|
4107
|
+
formatVersion: 1,
|
|
4108
|
+
cohallVersion: version,
|
|
4109
|
+
createdAt,
|
|
4110
|
+
databaseSha256: await digest(databasePath),
|
|
4111
|
+
ownerTokenSha256: await digest(ownerTokenPath)
|
|
4112
|
+
});
|
|
4113
|
+
await writeFile5(join5(destination, "manifest.json"), `${JSON.stringify(manifest, null, 2)}
|
|
4114
|
+
`, {
|
|
4115
|
+
mode: 384
|
|
4116
|
+
});
|
|
4117
|
+
return {
|
|
4118
|
+
backup_directory: destination,
|
|
4119
|
+
database: databasePath,
|
|
4120
|
+
created_at: createdAt,
|
|
4121
|
+
warning: "This backup contains relay credentials and task history. Transfer and store it privately."
|
|
4122
|
+
};
|
|
4123
|
+
} catch (cause) {
|
|
4124
|
+
await rm3(destination, { recursive: true, force: true });
|
|
4125
|
+
throw cause;
|
|
4126
|
+
}
|
|
4127
|
+
};
|
|
4128
|
+
var restoreRelay = async (sourceInput) => {
|
|
4129
|
+
const source = resolve2(sourceInput);
|
|
4130
|
+
const sourceDatabase = join5(source, "cohall.db");
|
|
4131
|
+
const sourceOwnerToken = join5(source, "owner-token");
|
|
4132
|
+
const sourceManifest = join5(source, "manifest.json");
|
|
4133
|
+
for (const path of [sourceDatabase, sourceOwnerToken, sourceManifest]) {
|
|
4134
|
+
if (!await exists(path)) {
|
|
4135
|
+
throw new Error(`Relay backup is missing ${basename(path)}`);
|
|
4136
|
+
}
|
|
4137
|
+
}
|
|
4138
|
+
await Promise.all([
|
|
4139
|
+
verifyBackupMember(sourceDatabase),
|
|
4140
|
+
verifyBackupMember(sourceOwnerToken, 1024),
|
|
4141
|
+
verifyBackupMember(sourceManifest, 16384)
|
|
4142
|
+
]);
|
|
4143
|
+
const target = relayDataDirectory();
|
|
4144
|
+
if (await exists(target)) {
|
|
4145
|
+
throw new Error(`Relay data directory already exists: ${target}. Restore into a new COHALL_DATA_DIR.`);
|
|
4146
|
+
}
|
|
4147
|
+
const parent = dirname4(target);
|
|
4148
|
+
await mkdir4(parent, { recursive: true, mode: 448 });
|
|
4149
|
+
const staging = await mkdtemp2(join5(parent, `.${basename(target)}.restore-`));
|
|
4150
|
+
let backupVersion = "";
|
|
4151
|
+
try {
|
|
4152
|
+
await chmod5(staging, 448);
|
|
4153
|
+
const databasePath = join5(staging, "cohall.db");
|
|
4154
|
+
const ownerTokenPath = join5(staging, "owner-token");
|
|
4155
|
+
const manifestPath = join5(staging, "migration-manifest.json");
|
|
4156
|
+
await Promise.all([
|
|
4157
|
+
copyFile(sourceDatabase, databasePath),
|
|
4158
|
+
copyFile(sourceOwnerToken, ownerTokenPath),
|
|
4159
|
+
copyFile(sourceManifest, manifestPath)
|
|
4160
|
+
]);
|
|
4161
|
+
await Promise.all([
|
|
4162
|
+
chmod5(databasePath, 384),
|
|
4163
|
+
chmod5(ownerTokenPath, 384),
|
|
4164
|
+
chmod5(manifestPath, 384)
|
|
4165
|
+
]);
|
|
4166
|
+
const manifest = await readManifest(manifestPath);
|
|
4167
|
+
if (await digest(databasePath) !== manifest.databaseSha256 || await digest(ownerTokenPath) !== manifest.ownerTokenSha256) {
|
|
4168
|
+
throw new Error("Relay backup checksum verification failed");
|
|
4169
|
+
}
|
|
4170
|
+
verifyDatabase(databasePath);
|
|
4171
|
+
const token = (await readFile3(ownerTokenPath, "utf8")).trim();
|
|
4172
|
+
if (token.length < 32 || new TextEncoder().encode(token).byteLength > 512) {
|
|
4173
|
+
throw new Error("Relay backup contains an invalid owner token");
|
|
4174
|
+
}
|
|
4175
|
+
backupVersion = manifest.cohallVersion;
|
|
4176
|
+
await rename3(staging, target);
|
|
4177
|
+
} catch (cause) {
|
|
4178
|
+
await rm3(staging, { recursive: true, force: true });
|
|
4179
|
+
throw cause;
|
|
4180
|
+
}
|
|
4181
|
+
return {
|
|
4182
|
+
restored: true,
|
|
4183
|
+
data_directory: target,
|
|
4184
|
+
backup_version: backupVersion,
|
|
4185
|
+
next: "Start the relay at its new address, then run `cohall relay use <new-url>` on each client and device."
|
|
4186
|
+
};
|
|
4187
|
+
};
|
|
4188
|
+
var websocketUrl = (relayUrl) => {
|
|
4189
|
+
const url = new URL(relayUrl);
|
|
4190
|
+
url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
|
|
4191
|
+
url.pathname = "/ws/device";
|
|
4192
|
+
return url.toString();
|
|
4193
|
+
};
|
|
4194
|
+
var isLoopback = (url) => url.hostname === "localhost" || url.hostname === "[::1]" || /^127(?:\.\d{1,3}){3}$/.test(url.hostname);
|
|
4195
|
+
var verifyClientCredential = async (relayUrl, token) => {
|
|
4196
|
+
await Effect6.runPromise(exports_src.make({ baseUrl: relayUrl, token }).devices());
|
|
4197
|
+
};
|
|
4198
|
+
var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, reject) => {
|
|
4199
|
+
const socket = new WebSocket(websocketUrl(relayUrl), {
|
|
4200
|
+
handshakeTimeout: 1e4,
|
|
4201
|
+
maxPayload: 1024 * 1024,
|
|
4202
|
+
perMessageDeflate: false
|
|
4203
|
+
});
|
|
4204
|
+
let settled = false;
|
|
4205
|
+
const finish = (error) => {
|
|
4206
|
+
if (settled) {
|
|
4207
|
+
return;
|
|
4208
|
+
}
|
|
4209
|
+
settled = true;
|
|
4210
|
+
clearTimeout(timeout);
|
|
4211
|
+
socket.terminate();
|
|
4212
|
+
if (error === undefined) {
|
|
4213
|
+
resolvePromise();
|
|
4214
|
+
return;
|
|
4215
|
+
}
|
|
4216
|
+
reject(error);
|
|
4217
|
+
};
|
|
4218
|
+
const timeout = setTimeout(() => finish(new Error("Timed out while verifying the device credential")), 1e4);
|
|
4219
|
+
socket.once("open", () => {
|
|
4220
|
+
socket.send(JSON.stringify(SocketEvent.make({ _tag: "Authenticate", token })));
|
|
4221
|
+
});
|
|
4222
|
+
socket.on("message", (data) => {
|
|
4223
|
+
try {
|
|
4224
|
+
const event = Schema7.decodeUnknownSync(SocketEvent)(JSON.parse(data.toString()));
|
|
4225
|
+
if (event._tag === "Connected") {
|
|
4226
|
+
finish();
|
|
4227
|
+
} else if (event._tag === "Error") {
|
|
4228
|
+
finish(new Error(event.message));
|
|
4229
|
+
}
|
|
4230
|
+
} catch (cause) {
|
|
4231
|
+
finish(new Error(`New relay returned an invalid device response: ${String(cause)}`));
|
|
4232
|
+
}
|
|
4233
|
+
});
|
|
4234
|
+
socket.once("error", (cause) => finish(cause));
|
|
4235
|
+
socket.once("close", () => finish(new Error("New relay rejected the stored device credential")));
|
|
4236
|
+
});
|
|
4237
|
+
var switchRelay = async (options) => {
|
|
4238
|
+
if (process.env.COHALL_RELAY_URL !== undefined) {
|
|
4239
|
+
throw new Error("Unset COHALL_RELAY_URL before changing the stored relay URL");
|
|
4240
|
+
}
|
|
4241
|
+
const configuration = await readStoredConfiguration();
|
|
4242
|
+
if (configuration === undefined) {
|
|
4243
|
+
throw new Error("This user has no stored Cohall configuration");
|
|
4244
|
+
}
|
|
4245
|
+
const relayUrl = normalizeRelayUrl(options.relayUrl);
|
|
4246
|
+
const parsedRelayUrl = new URL(relayUrl);
|
|
4247
|
+
if (parsedRelayUrl.protocol === "http:" && !isLoopback(parsedRelayUrl) && options.allowHttp !== true) {
|
|
4248
|
+
throw new Error("Refusing to send stored credentials over remote HTTP. Use HTTPS, or --allow-http only on an independently encrypted private network.");
|
|
4249
|
+
}
|
|
4250
|
+
const clientTokens = [configuration.clientToken, process.env.COHALL_CLIENT_TOKEN].filter((token) => token !== undefined);
|
|
4251
|
+
const deviceTokens = [configuration.deviceToken, process.env.COHALL_DEVICE_TOKEN].filter((token) => token !== undefined);
|
|
4252
|
+
if (clientTokens.length === 0 && deviceTokens.length === 0) {
|
|
4253
|
+
throw new Error("This configuration has no client or device credentials to verify");
|
|
4254
|
+
}
|
|
4255
|
+
const roles = [];
|
|
4256
|
+
if (clientTokens.length > 0) {
|
|
4257
|
+
for (const token of new Set(clientTokens)) {
|
|
4258
|
+
await (options.verifyClient ?? verifyClientCredential)(relayUrl, token);
|
|
4259
|
+
}
|
|
4260
|
+
roles.push("client");
|
|
4261
|
+
}
|
|
4262
|
+
if (deviceTokens.length > 0) {
|
|
4263
|
+
for (const token of new Set(deviceTokens)) {
|
|
4264
|
+
await (options.verifyDevice ?? verifyDeviceCredential)(relayUrl, token);
|
|
4265
|
+
}
|
|
4266
|
+
roles.push("device");
|
|
4267
|
+
}
|
|
4268
|
+
const updated = configuration.relayUrl !== relayUrl;
|
|
4269
|
+
if (updated) {
|
|
4270
|
+
await writeStoredConfiguration(StoredConfiguration.make({ ...configuration, relayUrl }));
|
|
4271
|
+
}
|
|
4272
|
+
let service = { running: false, restarted: false };
|
|
4273
|
+
let warning;
|
|
4274
|
+
if (updated && options.restart && deviceTokens.length > 0) {
|
|
4275
|
+
service = await (options.restartService ?? restartDeviceService)().catch((cause) => {
|
|
4276
|
+
warning = `Relay URL changed, but the device service could not restart: ${cause instanceof Error ? cause.message : String(cause)}`;
|
|
4277
|
+
return { running: true, restarted: false };
|
|
4278
|
+
});
|
|
4279
|
+
}
|
|
4280
|
+
return {
|
|
4281
|
+
updated,
|
|
4282
|
+
relay_url: relayUrl,
|
|
4283
|
+
verified_roles: roles,
|
|
4284
|
+
service,
|
|
4285
|
+
...warning === undefined ? {} : { warning }
|
|
4286
|
+
};
|
|
4287
|
+
};
|
|
4288
|
+
|
|
3291
4289
|
// apps/device/src/cli.ts
|
|
3292
4290
|
var valueOptions = new Set([
|
|
3293
4291
|
"context",
|
|
@@ -3308,7 +4306,16 @@ var valueOptions = new Set([
|
|
|
3308
4306
|
"to",
|
|
3309
4307
|
"workspace"
|
|
3310
4308
|
]);
|
|
3311
|
-
var flagOptions = new Set([
|
|
4309
|
+
var flagOptions = new Set([
|
|
4310
|
+
"all",
|
|
4311
|
+
"allow-http",
|
|
4312
|
+
"client-only",
|
|
4313
|
+
"dry-run",
|
|
4314
|
+
"follow",
|
|
4315
|
+
"no-restart",
|
|
4316
|
+
"no-wait",
|
|
4317
|
+
"service"
|
|
4318
|
+
]);
|
|
3312
4319
|
var aliases = new Map([
|
|
3313
4320
|
["-c", "context"],
|
|
3314
4321
|
["-p", "prompt"],
|
|
@@ -3319,6 +4326,7 @@ var aliases = new Map([
|
|
|
3319
4326
|
var help = `Cohall connects agents running on your own devices.
|
|
3320
4327
|
|
|
3321
4328
|
Usage:
|
|
4329
|
+
cohall init [--relay url] [--workspace path] [--providers list] [--service]
|
|
3322
4330
|
cohall join --relay <url> --workspace <path> [--providers list] [--token-file <path>]
|
|
3323
4331
|
cohall configure [--relay url] [--name name] [--workspace path]
|
|
3324
4332
|
[--providers codex,opencode|auto] [--model id]
|
|
@@ -3336,10 +4344,18 @@ Usage:
|
|
|
3336
4344
|
cohall sessions
|
|
3337
4345
|
cohall revoke <session-id>
|
|
3338
4346
|
cohall forget <device-id>
|
|
3339
|
-
cohall doctor
|
|
4347
|
+
cohall doctor [--all]
|
|
4348
|
+
cohall versions
|
|
4349
|
+
cohall usage
|
|
3340
4350
|
cohall upgrade [--to version|latest] [--no-restart] [--dry-run]
|
|
4351
|
+
cohall upgrade --all [--to version|latest] [--no-restart] [--dry-run]
|
|
4352
|
+
cohall upgrades [abandon <operation-id>]
|
|
4353
|
+
cohall service install
|
|
3341
4354
|
cohall device
|
|
3342
4355
|
cohall relay
|
|
4356
|
+
cohall relay backup <directory>
|
|
4357
|
+
cohall relay restore <directory>
|
|
4358
|
+
cohall relay use <url> [--no-restart] [--allow-http]
|
|
3343
4359
|
cohall mcp
|
|
3344
4360
|
cohall skill [install [agents|claude|opencode|all]]
|
|
3345
4361
|
cohall integrations
|
|
@@ -3434,13 +4450,13 @@ var readInput = async (arguments_, name) => {
|
|
|
3434
4450
|
throw new Error(`Use either --${name} or --${name}-file, not both`);
|
|
3435
4451
|
}
|
|
3436
4452
|
if (path !== undefined) {
|
|
3437
|
-
if (!await
|
|
4453
|
+
if (!await access4(path).then(() => true).catch(() => false)) {
|
|
3438
4454
|
throw new Error(`File does not exist: ${path}`);
|
|
3439
4455
|
}
|
|
3440
|
-
if ((await
|
|
4456
|
+
if ((await stat3(path)).size > 131072) {
|
|
3441
4457
|
throw new Error(`${name} file exceeds 128 KiB`);
|
|
3442
4458
|
}
|
|
3443
|
-
return
|
|
4459
|
+
return readFile4(path, "utf8");
|
|
3444
4460
|
}
|
|
3445
4461
|
const result = direct === "-" ? await readStdin(131072, name) : direct;
|
|
3446
4462
|
if (result !== undefined && result.length > 131072) {
|
|
@@ -3470,27 +4486,27 @@ var timeout = (arguments_) => {
|
|
|
3470
4486
|
};
|
|
3471
4487
|
var pairingToken = async (arguments_) => {
|
|
3472
4488
|
const path = option(arguments_, "token-file");
|
|
3473
|
-
if (path !== undefined && !await
|
|
4489
|
+
if (path !== undefined && !await access4(path).then(() => true).catch(() => false)) {
|
|
3474
4490
|
throw new Error(`Pairing token file does not exist: ${path}`);
|
|
3475
4491
|
}
|
|
3476
|
-
if (path !== undefined && (await
|
|
4492
|
+
if (path !== undefined && (await stat3(path)).size > 512) {
|
|
3477
4493
|
throw new Error("Pairing token file exceeds 512 bytes");
|
|
3478
4494
|
}
|
|
3479
|
-
const token = (path === undefined ? await readStdin(512, "Pairing token") : await
|
|
4495
|
+
const token = (path === undefined ? await readStdin(512, "Pairing token") : await readFile4(path, "utf8")).trim();
|
|
3480
4496
|
if (token.length === 0 || new TextEncoder().encode(token).byteLength > 256) {
|
|
3481
4497
|
throw new Error("Provide a pairing token on stdin or with --token-file");
|
|
3482
4498
|
}
|
|
3483
4499
|
return token;
|
|
3484
4500
|
};
|
|
3485
4501
|
var client = async () => {
|
|
3486
|
-
const configuration = await
|
|
4502
|
+
const configuration = await Effect7.runPromise(loadClientConfiguration);
|
|
3487
4503
|
return {
|
|
3488
4504
|
configuration,
|
|
3489
4505
|
relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
|
|
3490
4506
|
};
|
|
3491
4507
|
};
|
|
3492
4508
|
var ownerClient = async () => {
|
|
3493
|
-
const configuration = await
|
|
4509
|
+
const configuration = await Effect7.runPromise(loadOwnerConfiguration);
|
|
3494
4510
|
return {
|
|
3495
4511
|
configuration,
|
|
3496
4512
|
relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
|
|
@@ -3498,17 +4514,17 @@ var ownerClient = async () => {
|
|
|
3498
4514
|
};
|
|
3499
4515
|
var installSkill = async (target) => {
|
|
3500
4516
|
const roots = {
|
|
3501
|
-
agents:
|
|
3502
|
-
claude:
|
|
3503
|
-
opencode:
|
|
4517
|
+
agents: join6(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
|
|
4518
|
+
claude: join6(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
|
|
4519
|
+
opencode: join6(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
|
|
3504
4520
|
};
|
|
3505
4521
|
const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
|
|
3506
4522
|
if (selected === undefined) {
|
|
3507
4523
|
throw new Error("Skill target must be agents, claude, opencode, or all");
|
|
3508
4524
|
}
|
|
3509
4525
|
for (const path of selected) {
|
|
3510
|
-
await
|
|
3511
|
-
await
|
|
4526
|
+
await mkdir5(dirname5(path), { recursive: true });
|
|
4527
|
+
await writeFile6(path, SKILL_default);
|
|
3512
4528
|
}
|
|
3513
4529
|
return selected;
|
|
3514
4530
|
};
|
|
@@ -3516,10 +4532,150 @@ var printHelp = () => console.log(help);
|
|
|
3516
4532
|
var printSkill = () => console.log(SKILL_default.trimEnd());
|
|
3517
4533
|
var runCli = async (command, raw) => {
|
|
3518
4534
|
const arguments_ = parseArguments(raw);
|
|
4535
|
+
if (command === "init") {
|
|
4536
|
+
allowOptions(arguments_, [
|
|
4537
|
+
"client-only",
|
|
4538
|
+
"name",
|
|
4539
|
+
"providers",
|
|
4540
|
+
"relay",
|
|
4541
|
+
"service",
|
|
4542
|
+
"token-file",
|
|
4543
|
+
"workspace"
|
|
4544
|
+
]);
|
|
4545
|
+
noPositionals(arguments_, command);
|
|
4546
|
+
const token = option(arguments_, "token-file") === undefined ? undefined : await pairingToken(arguments_);
|
|
4547
|
+
const interactive = process.stdin.isTTY === true && process.stderr.isTTY === true;
|
|
4548
|
+
const relayUrl = option(arguments_, "relay");
|
|
4549
|
+
const deviceName = option(arguments_, "name");
|
|
4550
|
+
const providerInput = option(arguments_, "providers");
|
|
4551
|
+
const prompter = interactive ? terminalPrompter() : {
|
|
4552
|
+
answer: (_label, fallback) => Promise.resolve(fallback),
|
|
4553
|
+
secret: () => pairingToken(arguments_),
|
|
4554
|
+
close: () => {
|
|
4555
|
+
return;
|
|
4556
|
+
}
|
|
4557
|
+
};
|
|
4558
|
+
try {
|
|
4559
|
+
const input = await guidedSetupInput({
|
|
4560
|
+
clientOnly: arguments_.options.has("client-only"),
|
|
4561
|
+
workspaces: values(arguments_, "workspace"),
|
|
4562
|
+
cwd: process.cwd(),
|
|
4563
|
+
...relayUrl === undefined ? {} : { relayUrl },
|
|
4564
|
+
...token === undefined ? {} : { token },
|
|
4565
|
+
...deviceName === undefined ? {} : { deviceName },
|
|
4566
|
+
...providerInput === undefined ? {} : { providers: providerInput }
|
|
4567
|
+
}, prompter);
|
|
4568
|
+
const providers = input.providers === "auto" ? "auto" : parseProviders(input.providers);
|
|
4569
|
+
const existing = await readStoredConfiguration();
|
|
4570
|
+
const result = input.reusedConfiguration ? {
|
|
4571
|
+
configuration: await makeStoredConfiguration({
|
|
4572
|
+
relayUrl: input.relayUrl,
|
|
4573
|
+
...input.deviceName === undefined ? {} : { deviceName: input.deviceName },
|
|
4574
|
+
workspaces: input.workspaces,
|
|
4575
|
+
providers
|
|
4576
|
+
}),
|
|
4577
|
+
roles: [
|
|
4578
|
+
...existing?.clientToken === undefined ? [] : ["client"],
|
|
4579
|
+
...arguments_.options.has("client-only") || existing?.deviceToken === undefined ? [] : ["device"]
|
|
4580
|
+
]
|
|
4581
|
+
} : await joinRelay({
|
|
4582
|
+
relayUrl: input.relayUrl,
|
|
4583
|
+
token: input.token ?? "",
|
|
4584
|
+
clientOnly: arguments_.options.has("client-only"),
|
|
4585
|
+
...input.deviceName === undefined ? {} : { deviceName: input.deviceName },
|
|
4586
|
+
workspaces: input.workspaces,
|
|
4587
|
+
providers
|
|
4588
|
+
});
|
|
4589
|
+
if (input.reusedConfiguration) {
|
|
4590
|
+
await writeStoredConfiguration(result.configuration);
|
|
4591
|
+
}
|
|
4592
|
+
const installedSkills = await installSkill("all");
|
|
4593
|
+
const service = arguments_.options.has("service") ? await installDeviceService() : undefined;
|
|
4594
|
+
print({
|
|
4595
|
+
initialized: true,
|
|
4596
|
+
reused_configuration: input.reusedConfiguration,
|
|
4597
|
+
config_path: configurationPath(),
|
|
4598
|
+
relay_url: result.configuration.relayUrl,
|
|
4599
|
+
device_id: result.configuration.deviceId,
|
|
4600
|
+
roles: result.roles,
|
|
4601
|
+
workspaces: result.configuration.workspaces,
|
|
4602
|
+
skills: installedSkills,
|
|
4603
|
+
...service === undefined ? {} : { service },
|
|
4604
|
+
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."
|
|
4605
|
+
});
|
|
4606
|
+
} finally {
|
|
4607
|
+
prompter.close();
|
|
4608
|
+
}
|
|
4609
|
+
return;
|
|
4610
|
+
}
|
|
4611
|
+
if (command === "service") {
|
|
4612
|
+
allowOptions(arguments_, []);
|
|
4613
|
+
if (arguments_.positionals.length !== 1 || arguments_.positionals[0] !== "install") {
|
|
4614
|
+
throw new Error("Usage: cohall service install");
|
|
4615
|
+
}
|
|
4616
|
+
print(await installDeviceService());
|
|
4617
|
+
return;
|
|
4618
|
+
}
|
|
4619
|
+
if (command === "relay") {
|
|
4620
|
+
const action = arguments_.positionals[0];
|
|
4621
|
+
if (action === "backup") {
|
|
4622
|
+
allowOptions(arguments_, []);
|
|
4623
|
+
if (arguments_.positionals.length !== 2) {
|
|
4624
|
+
throw new Error("Usage: cohall relay backup <directory>");
|
|
4625
|
+
}
|
|
4626
|
+
print(await backupRelay(arguments_.positionals[1] ?? ""));
|
|
4627
|
+
return;
|
|
4628
|
+
}
|
|
4629
|
+
if (action === "restore") {
|
|
4630
|
+
allowOptions(arguments_, []);
|
|
4631
|
+
if (arguments_.positionals.length !== 2) {
|
|
4632
|
+
throw new Error("Usage: cohall relay restore <directory>");
|
|
4633
|
+
}
|
|
4634
|
+
print(await restoreRelay(arguments_.positionals[1] ?? ""));
|
|
4635
|
+
return;
|
|
4636
|
+
}
|
|
4637
|
+
if (action === "use") {
|
|
4638
|
+
allowOptions(arguments_, ["allow-http", "no-restart"]);
|
|
4639
|
+
if (arguments_.positionals.length !== 2) {
|
|
4640
|
+
throw new Error("Usage: cohall relay use <url> [--no-restart] [--allow-http]");
|
|
4641
|
+
}
|
|
4642
|
+
print(await switchRelay({
|
|
4643
|
+
relayUrl: arguments_.positionals[1] ?? "",
|
|
4644
|
+
restart: !arguments_.options.has("no-restart"),
|
|
4645
|
+
allowHttp: arguments_.options.has("allow-http")
|
|
4646
|
+
}));
|
|
4647
|
+
return;
|
|
4648
|
+
}
|
|
4649
|
+
throw new Error("Usage: cohall relay [backup <directory>|restore <directory>|use <url> [--no-restart] [--allow-http]]");
|
|
4650
|
+
}
|
|
3519
4651
|
if (command === "upgrade") {
|
|
3520
|
-
allowOptions(arguments_, ["dry-run", "no-restart", "to"]);
|
|
4652
|
+
allowOptions(arguments_, ["all", "dry-run", "no-restart", "to"]);
|
|
3521
4653
|
noPositionals(arguments_, command);
|
|
3522
4654
|
const target = option(arguments_, "to");
|
|
4655
|
+
if (arguments_.options.has("all")) {
|
|
4656
|
+
const normalizedTarget = normalizeUpgradeTarget(target);
|
|
4657
|
+
const { relay: relay2 } = await ownerClient();
|
|
4658
|
+
const restart = !arguments_.options.has("no-restart");
|
|
4659
|
+
if (arguments_.options.has("dry-run")) {
|
|
4660
|
+
const devices = await Effect7.runPromise(relay2.devices());
|
|
4661
|
+
print({
|
|
4662
|
+
dry_run: true,
|
|
4663
|
+
requested_version: normalizedTarget,
|
|
4664
|
+
restart,
|
|
4665
|
+
devices: devices.map((device) => ({ id: device.id, name: device.name }))
|
|
4666
|
+
});
|
|
4667
|
+
return;
|
|
4668
|
+
}
|
|
4669
|
+
const operations = await Effect7.runPromise(relay2.createUpgradeOperations({ target: normalizedTarget, restart }));
|
|
4670
|
+
print({
|
|
4671
|
+
queued: operations.length,
|
|
4672
|
+
requested_version: normalizedTarget,
|
|
4673
|
+
restart,
|
|
4674
|
+
operations,
|
|
4675
|
+
next: "Run `cohall upgrades` to inspect progress."
|
|
4676
|
+
});
|
|
4677
|
+
return;
|
|
4678
|
+
}
|
|
3523
4679
|
print(await upgrade({
|
|
3524
4680
|
currentVersion: version,
|
|
3525
4681
|
...target === undefined ? {} : { target },
|
|
@@ -3529,6 +4685,19 @@ var runCli = async (command, raw) => {
|
|
|
3529
4685
|
}));
|
|
3530
4686
|
return;
|
|
3531
4687
|
}
|
|
4688
|
+
if (command === "upgrades") {
|
|
4689
|
+
allowOptions(arguments_, []);
|
|
4690
|
+
const { relay: relay2 } = await ownerClient();
|
|
4691
|
+
if (arguments_.positionals.length > 0) {
|
|
4692
|
+
if (arguments_.positionals.length !== 2 || arguments_.positionals[0] !== "abandon") {
|
|
4693
|
+
throw new Error("Usage: cohall upgrades [abandon <operation-id>]");
|
|
4694
|
+
}
|
|
4695
|
+
print(await Effect7.runPromise(relay2.abandonOperation(Schema8.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
|
|
4696
|
+
return;
|
|
4697
|
+
}
|
|
4698
|
+
print(await Effect7.runPromise(relay2.operations()));
|
|
4699
|
+
return;
|
|
4700
|
+
}
|
|
3532
4701
|
if (command === "join") {
|
|
3533
4702
|
allowOptions(arguments_, [
|
|
3534
4703
|
"client-only",
|
|
@@ -3550,41 +4719,21 @@ var runCli = async (command, raw) => {
|
|
|
3550
4719
|
if (!arguments_.options.has("client-only") && (workspaces?.length ?? 0) === 0) {
|
|
3551
4720
|
throw new Error("At least one --workspace is required when joining a device");
|
|
3552
4721
|
}
|
|
3553
|
-
const
|
|
4722
|
+
const clientOnly = arguments_.options.has("client-only");
|
|
4723
|
+
const { configuration: configuration2, roles } = await joinRelay({
|
|
3554
4724
|
relayUrl,
|
|
4725
|
+
token,
|
|
4726
|
+
clientOnly,
|
|
3555
4727
|
...deviceName === undefined ? {} : { deviceName },
|
|
3556
4728
|
...workspaces === undefined ? {} : { workspaces },
|
|
3557
4729
|
...providers === undefined ? {} : { providers }
|
|
3558
4730
|
});
|
|
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
4731
|
print({
|
|
3583
4732
|
configured: true,
|
|
3584
4733
|
config_path: configurationPath(),
|
|
3585
4734
|
relay_url: configuration2.relayUrl,
|
|
3586
4735
|
device_id: configuration2.deviceId,
|
|
3587
|
-
roles
|
|
4736
|
+
roles,
|
|
3588
4737
|
workspaces: configuration2.workspaces
|
|
3589
4738
|
});
|
|
3590
4739
|
return;
|
|
@@ -3616,7 +4765,7 @@ var runCli = async (command, raw) => {
|
|
|
3616
4765
|
const existing = await readStoredConfiguration();
|
|
3617
4766
|
const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl ?? "http://127.0.0.1:8787");
|
|
3618
4767
|
const supplied = values(arguments_, "workspace");
|
|
3619
|
-
const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await
|
|
4768
|
+
const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect7.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
|
|
3620
4769
|
const sandbox = option(arguments_, "sandbox");
|
|
3621
4770
|
const model = option(arguments_, "model") ?? existing?.model;
|
|
3622
4771
|
const providerInput = option(arguments_, "providers");
|
|
@@ -3631,7 +4780,7 @@ var runCli = async (command, raw) => {
|
|
|
3631
4780
|
...providers === undefined ? {} : { providers },
|
|
3632
4781
|
...model === undefined ? {} : { model },
|
|
3633
4782
|
...(sandbox ?? existing?.sandbox) === undefined ? {} : {
|
|
3634
|
-
sandbox:
|
|
4783
|
+
sandbox: Schema8.decodeUnknownSync(Schema8.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
|
|
3635
4784
|
}
|
|
3636
4785
|
});
|
|
3637
4786
|
await writeStoredConfiguration(configuration2);
|
|
@@ -3670,8 +4819,13 @@ var runCli = async (command, raw) => {
|
|
|
3670
4819
|
return;
|
|
3671
4820
|
}
|
|
3672
4821
|
if (command === "doctor") {
|
|
3673
|
-
allowOptions(arguments_, []);
|
|
4822
|
+
allowOptions(arguments_, ["all"]);
|
|
3674
4823
|
noPositionals(arguments_, command);
|
|
4824
|
+
if (arguments_.options.has("all")) {
|
|
4825
|
+
const { relay: relay2 } = await client();
|
|
4826
|
+
print(allDeviceHealth(await Effect7.runPromise(relay2.devices()), version));
|
|
4827
|
+
return;
|
|
4828
|
+
}
|
|
3675
4829
|
const configuration2 = await readStoredConfiguration();
|
|
3676
4830
|
const relayUrl = normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? configuration2?.relayUrl ?? "http://127.0.0.1:8787");
|
|
3677
4831
|
const storedCredentials = credentialsForRelay(configuration2, relayUrl);
|
|
@@ -3689,7 +4843,7 @@ var runCli = async (command, raw) => {
|
|
|
3689
4843
|
const clientToken = process.env.COHALL_CLIENT_TOKEN ?? storedCredentials.clientToken;
|
|
3690
4844
|
const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
|
|
3691
4845
|
const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
|
|
3692
|
-
const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await
|
|
4846
|
+
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
4847
|
return;
|
|
3694
4848
|
});
|
|
3695
4849
|
const versionWarning = deviceVersionWarning(version, currentDevice?.version);
|
|
@@ -3718,13 +4872,30 @@ var runCli = async (command, raw) => {
|
|
|
3718
4872
|
});
|
|
3719
4873
|
return;
|
|
3720
4874
|
}
|
|
4875
|
+
if (command === "versions") {
|
|
4876
|
+
allowOptions(arguments_, []);
|
|
4877
|
+
noPositionals(arguments_, command);
|
|
4878
|
+
const { relay: relay2 } = await client();
|
|
4879
|
+
print(deviceVersions(await Effect7.runPromise(relay2.devices()), version));
|
|
4880
|
+
return;
|
|
4881
|
+
}
|
|
4882
|
+
if (command === "usage") {
|
|
4883
|
+
allowOptions(arguments_, []);
|
|
4884
|
+
noPositionals(arguments_, command);
|
|
4885
|
+
const { relay: relay2 } = await client();
|
|
4886
|
+
print({
|
|
4887
|
+
...await Effect7.runPromise(relay2.usage()),
|
|
4888
|
+
scope: "Retained Cohall task activity; provider token and billing usage are not available."
|
|
4889
|
+
});
|
|
4890
|
+
return;
|
|
4891
|
+
}
|
|
3721
4892
|
if (command === "pair" || command === "sessions" || command === "revoke" || command === "forget") {
|
|
3722
4893
|
const { configuration: configuration2, relay: relay2 } = await ownerClient();
|
|
3723
4894
|
if (command === "pair") {
|
|
3724
4895
|
allowOptions(arguments_, ["client-only", "label"]);
|
|
3725
4896
|
noPositionals(arguments_, command);
|
|
3726
4897
|
const roles = arguments_.options.has("client-only") ? ["client"] : ["client", "device"];
|
|
3727
|
-
const credential = await
|
|
4898
|
+
const credential = await Effect7.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
|
|
3728
4899
|
print({
|
|
3729
4900
|
relay_url: configuration2.relayUrl,
|
|
3730
4901
|
pairing_token: credential.token,
|
|
@@ -3736,23 +4907,23 @@ var runCli = async (command, raw) => {
|
|
|
3736
4907
|
if (command === "sessions") {
|
|
3737
4908
|
allowOptions(arguments_, []);
|
|
3738
4909
|
noPositionals(arguments_, command);
|
|
3739
|
-
print(await
|
|
4910
|
+
print(await Effect7.runPromise(relay2.authSessions()));
|
|
3740
4911
|
return;
|
|
3741
4912
|
}
|
|
3742
4913
|
if (command === "forget") {
|
|
3743
4914
|
allowOptions(arguments_, []);
|
|
3744
|
-
print(await
|
|
4915
|
+
print(await Effect7.runPromise(relay2.forgetDevice(Schema8.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
|
|
3745
4916
|
return;
|
|
3746
4917
|
}
|
|
3747
4918
|
allowOptions(arguments_, []);
|
|
3748
|
-
print(await
|
|
4919
|
+
print(await Effect7.runPromise(relay2.revokeAuthSession(Schema8.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
|
|
3749
4920
|
return;
|
|
3750
4921
|
}
|
|
3751
4922
|
const { configuration, relay } = await client();
|
|
3752
4923
|
if (command === "devices") {
|
|
3753
4924
|
allowOptions(arguments_, []);
|
|
3754
4925
|
noPositionals(arguments_, command);
|
|
3755
|
-
print(await
|
|
4926
|
+
print(await Effect7.runPromise(relay.devices()));
|
|
3756
4927
|
return;
|
|
3757
4928
|
}
|
|
3758
4929
|
if (command === "delegate") {
|
|
@@ -3784,50 +4955,50 @@ var runCli = async (command, raw) => {
|
|
|
3784
4955
|
const thread = option(arguments_, "thread");
|
|
3785
4956
|
const workspace = option(arguments_, "workspace");
|
|
3786
4957
|
const provider = option(arguments_, "provider");
|
|
3787
|
-
const task = await
|
|
4958
|
+
const task = await Effect7.runPromise(createDelegation(relay, configuration, {
|
|
3788
4959
|
prompt,
|
|
3789
4960
|
...target === undefined ? {} : { target },
|
|
3790
4961
|
...context === undefined ? {} : { context },
|
|
3791
|
-
...thread === undefined ? {} : { threadId:
|
|
4962
|
+
...thread === undefined ? {} : { threadId: Schema8.decodeUnknownSync(ThreadId)(thread) },
|
|
3792
4963
|
...workspace === undefined ? {} : { workspace },
|
|
3793
|
-
...provider === undefined ? {} : { provider:
|
|
4964
|
+
...provider === undefined ? {} : { provider: Schema8.decodeUnknownSync(Provider)(provider) }
|
|
3794
4965
|
}));
|
|
3795
|
-
print(taskResult(arguments_.options.has("no-wait") ? task : await
|
|
4966
|
+
print(taskResult(arguments_.options.has("no-wait") ? task : await Effect7.runPromise(waitForTask(relay, task, timeout(arguments_)))));
|
|
3796
4967
|
return;
|
|
3797
4968
|
}
|
|
3798
4969
|
if (command === "status") {
|
|
3799
4970
|
allowOptions(arguments_, []);
|
|
3800
|
-
const id =
|
|
3801
|
-
print(taskResult(await
|
|
4971
|
+
const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
4972
|
+
print(taskResult(await Effect7.runPromise(relay.getTask(id))));
|
|
3802
4973
|
return;
|
|
3803
4974
|
}
|
|
3804
4975
|
if (command === "trace") {
|
|
3805
4976
|
allowOptions(arguments_, ["follow"]);
|
|
3806
|
-
const id =
|
|
4977
|
+
const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
3807
4978
|
if (arguments_.options.has("follow")) {
|
|
3808
|
-
await
|
|
4979
|
+
await Effect7.runPromise(followTaskTrace(relay, id, printLine));
|
|
3809
4980
|
return;
|
|
3810
4981
|
}
|
|
3811
|
-
print(await
|
|
4982
|
+
print(await Effect7.runPromise(relay.traceTask(id)));
|
|
3812
4983
|
return;
|
|
3813
4984
|
}
|
|
3814
4985
|
if (command === "wait") {
|
|
3815
4986
|
allowOptions(arguments_, ["timeout"]);
|
|
3816
|
-
const id =
|
|
3817
|
-
const task = await
|
|
3818
|
-
print(taskResult(await
|
|
4987
|
+
const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
4988
|
+
const task = await Effect7.runPromise(relay.getTask(id));
|
|
4989
|
+
print(taskResult(await Effect7.runPromise(waitForTask(relay, task, timeout(arguments_)))));
|
|
3819
4990
|
return;
|
|
3820
4991
|
}
|
|
3821
4992
|
if (command === "cancel") {
|
|
3822
4993
|
allowOptions(arguments_, []);
|
|
3823
|
-
const id =
|
|
3824
|
-
print(taskResult(await
|
|
4994
|
+
const id = Schema8.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
|
|
4995
|
+
print(taskResult(await Effect7.runPromise(relay.cancelTask(id))));
|
|
3825
4996
|
return;
|
|
3826
4997
|
}
|
|
3827
4998
|
if (command === "thread") {
|
|
3828
4999
|
allowOptions(arguments_, []);
|
|
3829
|
-
const id =
|
|
3830
|
-
print(await
|
|
5000
|
+
const id = Schema8.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
|
|
5001
|
+
print(await Effect7.runPromise(threadContext(relay, id)));
|
|
3831
5002
|
return;
|
|
3832
5003
|
}
|
|
3833
5004
|
throw new Error(`Unknown Cohall command: ${command}`);
|
|
@@ -3835,15 +5006,15 @@ var runCli = async (command, raw) => {
|
|
|
3835
5006
|
|
|
3836
5007
|
// apps/device/src/daemon.ts
|
|
3837
5008
|
init_src();
|
|
3838
|
-
import { Effect as
|
|
5009
|
+
import { Effect as Effect8, Schedule as Schedule2, Schema as Schema9 } from "effect";
|
|
3839
5010
|
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";
|
|
5011
|
+
import { open, realpath as realpath5, stat as stat4 } from "node:fs/promises";
|
|
5012
|
+
import { arch, hostname as hostname3, platform as platform3 } from "node:os";
|
|
5013
|
+
import { basename as basename2, isAbsolute as isAbsolute3, relative } from "node:path";
|
|
5014
|
+
import { WebSocket as WebSocket2 } from "ws";
|
|
3844
5015
|
var maxQueuedRelayMessages = 8;
|
|
3845
5016
|
|
|
3846
|
-
class DeviceConnectionError extends
|
|
5017
|
+
class DeviceConnectionError extends Schema9.TaggedErrorClass()("Device.ConnectionError", { message: Schema9.String }) {
|
|
3847
5018
|
}
|
|
3848
5019
|
var socketUrl = (configuration) => {
|
|
3849
5020
|
const url = new URL(configuration.relayUrl);
|
|
@@ -3887,7 +5058,7 @@ var describeDevice = (configuration, status = "online") => {
|
|
|
3887
5058
|
return Device.make({
|
|
3888
5059
|
id: configuration.id,
|
|
3889
5060
|
name: configuration.name,
|
|
3890
|
-
hostname:
|
|
5061
|
+
hostname: hostname3(),
|
|
3891
5062
|
platform: platformName,
|
|
3892
5063
|
architecture: arch(),
|
|
3893
5064
|
status,
|
|
@@ -3895,7 +5066,7 @@ var describeDevice = (configuration, status = "online") => {
|
|
|
3895
5066
|
capabilities: capabilities(providers),
|
|
3896
5067
|
workspaces: configuration.workspaces.map((path) => ({
|
|
3897
5068
|
path,
|
|
3898
|
-
label:
|
|
5069
|
+
label: basename2(path) || path
|
|
3899
5070
|
})),
|
|
3900
5071
|
version,
|
|
3901
5072
|
lastSeenAt: now()
|
|
@@ -3906,7 +5077,7 @@ var allowedWorkspace = async (configuration, requested) => {
|
|
|
3906
5077
|
if (selected === undefined) {
|
|
3907
5078
|
throw new Error("No workspace is configured on this device");
|
|
3908
5079
|
}
|
|
3909
|
-
const candidate = await
|
|
5080
|
+
const candidate = await realpath5(selected);
|
|
3910
5081
|
const allowed = configuration.workspaces.some((root) => {
|
|
3911
5082
|
const child = relative(root, candidate);
|
|
3912
5083
|
return child === "" || !child.startsWith("..") && !isAbsolute3(child);
|
|
@@ -3926,7 +5097,7 @@ var openAllowedWorkspace = async (configuration, requested) => {
|
|
|
3926
5097
|
await handle.close();
|
|
3927
5098
|
throw new Error(`Workspace ${path} is not a directory`);
|
|
3928
5099
|
}
|
|
3929
|
-
const cwd = operatingSystem2 === "linux" ? `/proc/self/fd/${handle.fd}` :
|
|
5100
|
+
const cwd = operatingSystem2 === "linux" ? `/proc/self/fd/${handle.fd}` : path;
|
|
3930
5101
|
return {
|
|
3931
5102
|
cwd,
|
|
3932
5103
|
validate: async () => {
|
|
@@ -3934,9 +5105,9 @@ var openAllowedWorkspace = async (configuration, requested) => {
|
|
|
3934
5105
|
if (!current.isDirectory() || current.dev !== identity.dev || current.ino !== identity.ino) {
|
|
3935
5106
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
3936
5107
|
}
|
|
3937
|
-
if (operatingSystem2
|
|
3938
|
-
const currentPath = await
|
|
3939
|
-
const currentPathIdentity = await
|
|
5108
|
+
if (operatingSystem2 !== "linux") {
|
|
5109
|
+
const currentPath = await realpath5(path);
|
|
5110
|
+
const currentPathIdentity = await stat4(currentPath);
|
|
3940
5111
|
if (currentPathIdentity.dev !== identity.dev || currentPathIdentity.ino !== identity.ino) {
|
|
3941
5112
|
throw new Error(`Workspace ${path} changed before provider startup`);
|
|
3942
5113
|
}
|
|
@@ -3962,14 +5133,21 @@ ${task.prompt}${context}`
|
|
|
3962
5133
|
`);
|
|
3963
5134
|
};
|
|
3964
5135
|
var send = (state, event) => {
|
|
3965
|
-
if (state.socket?.readyState ===
|
|
5136
|
+
if (state.socket?.readyState === WebSocket2.OPEN) {
|
|
3966
5137
|
state.socket.send(JSON.stringify(event));
|
|
3967
5138
|
}
|
|
3968
5139
|
};
|
|
3969
5140
|
var sendTerminal = (state, taskId, event) => {
|
|
3970
5141
|
const payload = JSON.stringify(event);
|
|
3971
5142
|
state.terminal.set(taskId, payload);
|
|
3972
|
-
if (state.socket?.readyState ===
|
|
5143
|
+
if (state.socket?.readyState === WebSocket2.OPEN) {
|
|
5144
|
+
state.socket.send(payload);
|
|
5145
|
+
}
|
|
5146
|
+
};
|
|
5147
|
+
var sendOperationTerminal = (state, operationId, event) => {
|
|
5148
|
+
const payload = JSON.stringify(event);
|
|
5149
|
+
state.operationTerminal.set(operationId, payload);
|
|
5150
|
+
if (state.socket?.readyState === WebSocket2.OPEN) {
|
|
3973
5151
|
state.socket.send(payload);
|
|
3974
5152
|
}
|
|
3975
5153
|
};
|
|
@@ -3984,6 +5162,17 @@ var remember = (state, taskId) => {
|
|
|
3984
5162
|
state.terminal.delete(oldest.value);
|
|
3985
5163
|
}
|
|
3986
5164
|
};
|
|
5165
|
+
var rememberOperation = (state, operationId) => {
|
|
5166
|
+
state.completedOperations.add(operationId);
|
|
5167
|
+
if (state.completedOperations.size <= 100) {
|
|
5168
|
+
return;
|
|
5169
|
+
}
|
|
5170
|
+
const oldest = state.completedOperations.values().next();
|
|
5171
|
+
if (!oldest.done) {
|
|
5172
|
+
state.completedOperations.delete(oldest.value);
|
|
5173
|
+
state.operationTerminal.delete(oldest.value);
|
|
5174
|
+
}
|
|
5175
|
+
};
|
|
3987
5176
|
var sessionKey = (task) => `${task.threadId}:${task.provider}`;
|
|
3988
5177
|
var execute = (configuration, state, task) => {
|
|
3989
5178
|
if (state.tasks.has(task.id) || state.completed.has(task.id)) {
|
|
@@ -3992,8 +5181,8 @@ var execute = (configuration, state, task) => {
|
|
|
3992
5181
|
const controller = new AbortController;
|
|
3993
5182
|
state.tasks.set(task.id, controller);
|
|
3994
5183
|
send(state, SocketEvent.make({ _tag: "TaskAccepted", taskId: task.id }));
|
|
3995
|
-
const workflow =
|
|
3996
|
-
const workspace = yield*
|
|
5184
|
+
const workflow = Effect8.gen(function* () {
|
|
5185
|
+
const workspace = yield* Effect8.tryPromise({
|
|
3997
5186
|
try: () => openAllowedWorkspace(configuration, task.workspace),
|
|
3998
5187
|
catch: (cause) => new ProviderRunError({
|
|
3999
5188
|
provider: task.provider,
|
|
@@ -4010,11 +5199,11 @@ var execute = (configuration, state, task) => {
|
|
|
4010
5199
|
...sessionId === undefined ? {} : { sessionId },
|
|
4011
5200
|
...configuration.model === undefined ? {} : { model: configuration.model },
|
|
4012
5201
|
...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
|
|
4013
|
-
}).pipe(
|
|
5202
|
+
}).pipe(Effect8.ensuring(Effect8.promise(() => workspace.close().catch(() => {
|
|
4014
5203
|
return;
|
|
4015
5204
|
}))));
|
|
4016
5205
|
});
|
|
4017
|
-
|
|
5206
|
+
Effect8.runPromise(workflow, { signal: controller.signal }).then((result) => {
|
|
4018
5207
|
if (result.sessionId !== undefined) {
|
|
4019
5208
|
state.sessions.set(sessionKey(task), result.sessionId);
|
|
4020
5209
|
}
|
|
@@ -4034,12 +5223,71 @@ var execute = (configuration, state, task) => {
|
|
|
4034
5223
|
}));
|
|
4035
5224
|
}).finally(() => {
|
|
4036
5225
|
state.tasks.delete(task.id);
|
|
5226
|
+
const operation = state.operationQueue.shift();
|
|
5227
|
+
if (operation !== undefined) {
|
|
5228
|
+
executeOperation(configuration, state, operation);
|
|
5229
|
+
return;
|
|
5230
|
+
}
|
|
5231
|
+
const next = state.queue.shift();
|
|
5232
|
+
if (next !== undefined && state.operation === undefined) {
|
|
5233
|
+
execute(configuration, state, next);
|
|
5234
|
+
}
|
|
5235
|
+
});
|
|
5236
|
+
};
|
|
5237
|
+
var executeOperation = (configuration, state, operation) => {
|
|
5238
|
+
if (state.operation !== undefined || state.completedOperations.has(operation.id)) {
|
|
5239
|
+
return;
|
|
5240
|
+
}
|
|
5241
|
+
state.operation = operation;
|
|
5242
|
+
send(state, SocketEvent.make({ _tag: "OperationAccepted", operationId: operation.id }));
|
|
5243
|
+
performDeviceOperation(operation, version).then((result) => {
|
|
5244
|
+
rememberOperation(state, operation.id);
|
|
5245
|
+
sendOperationTerminal(state, operation.id, SocketEvent.make({
|
|
5246
|
+
_tag: "OperationFinished",
|
|
5247
|
+
operationId: operation.id,
|
|
5248
|
+
result
|
|
5249
|
+
}));
|
|
5250
|
+
}).catch((cause) => {
|
|
5251
|
+
rememberOperation(state, operation.id);
|
|
5252
|
+
sendOperationTerminal(state, operation.id, SocketEvent.make({
|
|
5253
|
+
_tag: "OperationFailed",
|
|
5254
|
+
operationId: operation.id,
|
|
5255
|
+
error: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
|
|
5256
|
+
}));
|
|
5257
|
+
}).finally(() => {
|
|
5258
|
+
state.operation = undefined;
|
|
5259
|
+
const pending = state.operationQueue.shift();
|
|
5260
|
+
if (pending !== undefined) {
|
|
5261
|
+
executeOperation(configuration, state, pending);
|
|
5262
|
+
return;
|
|
5263
|
+
}
|
|
4037
5264
|
const next = state.queue.shift();
|
|
4038
|
-
if (next !== undefined) {
|
|
5265
|
+
if (next !== undefined && state.tasks.size === 0) {
|
|
4039
5266
|
execute(configuration, state, next);
|
|
4040
5267
|
}
|
|
4041
5268
|
});
|
|
4042
5269
|
};
|
|
5270
|
+
var scheduleOperation = (configuration, state, operation) => {
|
|
5271
|
+
if (state.completedOperations.has(operation.id) || state.operation?.id === operation.id || state.operationQueue.some((pending) => pending.id === operation.id)) {
|
|
5272
|
+
return;
|
|
5273
|
+
}
|
|
5274
|
+
if (state.tasks.size > 0 || state.operation !== undefined) {
|
|
5275
|
+
if (state.operationQueue.length >= 10) {
|
|
5276
|
+
state.socket?.close(4008, "Operation queue limit reached");
|
|
5277
|
+
return;
|
|
5278
|
+
}
|
|
5279
|
+
state.operationQueue.push(operation);
|
|
5280
|
+
return;
|
|
5281
|
+
}
|
|
5282
|
+
executeOperation(configuration, state, operation);
|
|
5283
|
+
};
|
|
5284
|
+
var performDeviceOperation = (operation, currentVersion, runUpgrade = upgrade) => runUpgrade({
|
|
5285
|
+
currentVersion,
|
|
5286
|
+
target: operation.requestedVersion,
|
|
5287
|
+
restart: operation.restart,
|
|
5288
|
+
dryRun: false,
|
|
5289
|
+
delegated: true
|
|
5290
|
+
}).then((result) => JSON.stringify(result));
|
|
4043
5291
|
var schedule = (configuration, state, task) => {
|
|
4044
5292
|
if (state.tasks.has(task.id) || state.completed.has(task.id) || state.queue.some((queued) => queued.id === task.id)) {
|
|
4045
5293
|
return;
|
|
@@ -4047,7 +5295,7 @@ var schedule = (configuration, state, task) => {
|
|
|
4047
5295
|
if (task.providerSessionId !== undefined) {
|
|
4048
5296
|
state.sessions.set(sessionKey(task), task.providerSessionId);
|
|
4049
5297
|
}
|
|
4050
|
-
if (state.tasks.size > 0) {
|
|
5298
|
+
if (state.tasks.size > 0 || state.operation !== undefined) {
|
|
4051
5299
|
if (state.queue.length >= 100) {
|
|
4052
5300
|
state.socket?.close(4008, "Task queue limit reached");
|
|
4053
5301
|
return;
|
|
@@ -4070,9 +5318,9 @@ var cancel = (state, taskId) => {
|
|
|
4070
5318
|
remember(state, taskId);
|
|
4071
5319
|
sendTerminal(state, taskId, SocketEvent.make({ _tag: "TaskCancelled", taskId }));
|
|
4072
5320
|
};
|
|
4073
|
-
var connect = (configuration, state) =>
|
|
5321
|
+
var connect = (configuration, state) => Effect8.tryPromise({
|
|
4074
5322
|
try: (signal) => new Promise((complete) => {
|
|
4075
|
-
const socket = new
|
|
5323
|
+
const socket = new WebSocket2(socketUrl(configuration), {
|
|
4076
5324
|
maxPayload: maxSocketPayloadBytes,
|
|
4077
5325
|
perMessageDeflate: false
|
|
4078
5326
|
});
|
|
@@ -4080,7 +5328,7 @@ var connect = (configuration, state) => Effect6.tryPromise({
|
|
|
4080
5328
|
send(state, SocketEvent.make({
|
|
4081
5329
|
_tag: "DeviceHeartbeat",
|
|
4082
5330
|
deviceId: configuration.id,
|
|
4083
|
-
status: state.tasks.size > 0 ? "busy" : "online"
|
|
5331
|
+
status: state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online"
|
|
4084
5332
|
}));
|
|
4085
5333
|
}, 15000);
|
|
4086
5334
|
let closed = false;
|
|
@@ -4114,19 +5362,22 @@ var connect = (configuration, state) => Effect6.tryPromise({
|
|
|
4114
5362
|
return;
|
|
4115
5363
|
}
|
|
4116
5364
|
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
|
|
5365
|
+
const event = await Effect8.runPromise(Effect8.try({
|
|
4118
5366
|
try: () => JSON.parse(text2),
|
|
4119
5367
|
catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
|
|
4120
|
-
}).pipe(
|
|
5368
|
+
}).pipe(Effect8.flatMap(decodeSocketEvent)));
|
|
4121
5369
|
if (event._tag === "Connected") {
|
|
4122
5370
|
state.socket = socket;
|
|
4123
5371
|
socket.send(JSON.stringify(SocketEvent.make({
|
|
4124
5372
|
_tag: "DeviceHello",
|
|
4125
|
-
device: describeDevice(configuration, state.tasks.size > 0 ? "busy" : "online")
|
|
5373
|
+
device: describeDevice(configuration, state.tasks.size > 0 || state.operation !== undefined ? "busy" : "online")
|
|
4126
5374
|
})));
|
|
4127
5375
|
for (const payload of state.terminal.values()) {
|
|
4128
5376
|
socket.send(payload);
|
|
4129
5377
|
}
|
|
5378
|
+
for (const payload of state.operationTerminal.values()) {
|
|
5379
|
+
socket.send(payload);
|
|
5380
|
+
}
|
|
4130
5381
|
return;
|
|
4131
5382
|
}
|
|
4132
5383
|
if (event._tag === "TaskAssigned") {
|
|
@@ -4141,6 +5392,14 @@ var connect = (configuration, state) => Effect6.tryPromise({
|
|
|
4141
5392
|
state.terminal.delete(event.taskId);
|
|
4142
5393
|
return;
|
|
4143
5394
|
}
|
|
5395
|
+
if (event._tag === "OperationAssigned") {
|
|
5396
|
+
scheduleOperation(configuration, state, event.operation);
|
|
5397
|
+
return;
|
|
5398
|
+
}
|
|
5399
|
+
if (event._tag === "OperationSettled") {
|
|
5400
|
+
state.operationTerminal.delete(event.operationId);
|
|
5401
|
+
return;
|
|
5402
|
+
}
|
|
4144
5403
|
if (event._tag === "Error") {
|
|
4145
5404
|
console.error(`Relay error: ${event.message}`);
|
|
4146
5405
|
}
|
|
@@ -4165,9 +5424,13 @@ var runDaemon = (configuration) => {
|
|
|
4165
5424
|
queue: [],
|
|
4166
5425
|
sessions: new Map,
|
|
4167
5426
|
tasks: new Map,
|
|
4168
|
-
completed: new Set
|
|
5427
|
+
completed: new Set,
|
|
5428
|
+
operation: undefined,
|
|
5429
|
+
operationQueue: [],
|
|
5430
|
+
operationTerminal: new Map,
|
|
5431
|
+
completedOperations: new Set
|
|
4169
5432
|
};
|
|
4170
|
-
return connect(configuration, state).pipe(
|
|
5433
|
+
return connect(configuration, state).pipe(Effect8.repeat({ schedule: Schedule2.spaced("2 seconds") }), Effect8.ensuring(Effect8.sync(() => {
|
|
4171
5434
|
state.socket?.close();
|
|
4172
5435
|
for (const controller of state.tasks.values()) {
|
|
4173
5436
|
controller.abort();
|
|
@@ -4179,7 +5442,7 @@ var runDaemon = (configuration) => {
|
|
|
4179
5442
|
init_src();
|
|
4180
5443
|
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
4181
5444
|
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
4182
|
-
import { Effect as
|
|
5445
|
+
import { Effect as Effect9 } from "effect";
|
|
4183
5446
|
import * as z from "zod/v4";
|
|
4184
5447
|
var output = (value) => ({
|
|
4185
5448
|
content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
|
|
@@ -4194,7 +5457,7 @@ var runMcp = async (configuration) => {
|
|
|
4194
5457
|
title: "List Cohall devices",
|
|
4195
5458
|
description: "List devices, their availability, local providers, capabilities, and allowed workspaces.",
|
|
4196
5459
|
inputSchema: {}
|
|
4197
|
-
}, async () => output(await
|
|
5460
|
+
}, async () => output(await Effect9.runPromise(client2.devices())));
|
|
4198
5461
|
server.registerTool("delegate", {
|
|
4199
5462
|
title: "Delegate work to another device",
|
|
4200
5463
|
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 +5472,7 @@ var runMcp = async (configuration) => {
|
|
|
4209
5472
|
timeout_seconds: z.number().int().min(5).max(86400).default(900)
|
|
4210
5473
|
}
|
|
4211
5474
|
}, async ({ prompt, target, provider, context, thread_id, workspace, wait, timeout_seconds }) => {
|
|
4212
|
-
const task = await
|
|
5475
|
+
const task = await Effect9.runPromise(createDelegation(client2, configuration, {
|
|
4213
5476
|
prompt,
|
|
4214
5477
|
provider,
|
|
4215
5478
|
...target === undefined ? {} : { target },
|
|
@@ -4217,18 +5480,18 @@ var runMcp = async (configuration) => {
|
|
|
4217
5480
|
...thread_id === undefined ? {} : { threadId: ThreadId.make(thread_id) },
|
|
4218
5481
|
...workspace === undefined ? {} : { workspace }
|
|
4219
5482
|
}));
|
|
4220
|
-
return output(taskResult(wait ? await
|
|
5483
|
+
return output(taskResult(wait ? await Effect9.runPromise(waitForTask(client2, task, timeout_seconds)) : task));
|
|
4221
5484
|
});
|
|
4222
5485
|
server.registerTool("task_status", {
|
|
4223
5486
|
title: "Get delegated task status",
|
|
4224
5487
|
description: "Read the current state and result of a Cohall task.",
|
|
4225
5488
|
inputSchema: { task_id: z.string().uuid() }
|
|
4226
|
-
}, async ({ task_id }) => output(taskResult(await
|
|
5489
|
+
}, async ({ task_id }) => output(taskResult(await Effect9.runPromise(client2.getTask(TaskId.make(task_id))))));
|
|
4227
5490
|
server.registerTool("task_trace", {
|
|
4228
5491
|
title: "Trace a delegated task",
|
|
4229
5492
|
description: "Read the redacted relay and device lifecycle for troubleshooting a Cohall task.",
|
|
4230
5493
|
inputSchema: { task_id: z.string().uuid() }
|
|
4231
|
-
}, async ({ task_id }) => output(await
|
|
5494
|
+
}, async ({ task_id }) => output(await Effect9.runPromise(client2.traceTask(TaskId.make(task_id)))));
|
|
4232
5495
|
server.registerTool("wait_task", {
|
|
4233
5496
|
title: "Wait for delegated work",
|
|
4234
5497
|
description: "Wait for a Cohall task to finish and return its final result.",
|
|
@@ -4237,19 +5500,19 @@ var runMcp = async (configuration) => {
|
|
|
4237
5500
|
timeout_seconds: z.number().int().min(5).max(86400).default(900)
|
|
4238
5501
|
}
|
|
4239
5502
|
}, async ({ task_id, timeout_seconds }) => {
|
|
4240
|
-
const task = await
|
|
4241
|
-
return output(taskResult(await
|
|
5503
|
+
const task = await Effect9.runPromise(client2.getTask(TaskId.make(task_id)));
|
|
5504
|
+
return output(taskResult(await Effect9.runPromise(waitForTask(client2, task, timeout_seconds))));
|
|
4242
5505
|
});
|
|
4243
5506
|
server.registerTool("cancel_task", {
|
|
4244
5507
|
title: "Cancel delegated work",
|
|
4245
5508
|
description: "Request cancellation and return its acknowledged or pending state.",
|
|
4246
5509
|
inputSchema: { task_id: z.string().uuid() }
|
|
4247
|
-
}, async ({ task_id }) => output(taskResult(await
|
|
5510
|
+
}, async ({ task_id }) => output(taskResult(await Effect9.runPromise(client2.cancelTask(TaskId.make(task_id))))));
|
|
4248
5511
|
server.registerTool("thread_context", {
|
|
4249
5512
|
title: "Read a Cohall thread",
|
|
4250
5513
|
description: "Read shared task prompts and final results for a cross-device thread.",
|
|
4251
5514
|
inputSchema: { thread_id: z.string().uuid() }
|
|
4252
|
-
}, async ({ thread_id }) => output(await
|
|
5515
|
+
}, async ({ thread_id }) => output(await Effect9.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
|
|
4253
5516
|
await server.connect(new StdioServerTransport);
|
|
4254
5517
|
};
|
|
4255
5518
|
|
|
@@ -4269,21 +5532,21 @@ var main = async () => {
|
|
|
4269
5532
|
printHelp();
|
|
4270
5533
|
return;
|
|
4271
5534
|
}
|
|
4272
|
-
if (["device", "
|
|
5535
|
+
if (["device", "mcp"].includes(command) && process.argv.length > 3) {
|
|
4273
5536
|
throw new Error(`${command} does not accept arguments`);
|
|
4274
5537
|
}
|
|
4275
|
-
if (command === "relay") {
|
|
5538
|
+
if (command === "relay" && commandArguments.length === 0) {
|
|
4276
5539
|
const { runRelay: runRelay2 } = await init_main().then(() => exports_main);
|
|
4277
5540
|
await runRelay2();
|
|
4278
5541
|
return;
|
|
4279
5542
|
}
|
|
4280
5543
|
if (command === "device") {
|
|
4281
|
-
const configuration = await
|
|
4282
|
-
await
|
|
5544
|
+
const configuration = await Effect13.runPromise(loadDeviceConfiguration);
|
|
5545
|
+
await Effect13.runPromise(runDaemon(configuration));
|
|
4283
5546
|
return;
|
|
4284
5547
|
}
|
|
4285
5548
|
if (command === "mcp") {
|
|
4286
|
-
const configuration = await
|
|
5549
|
+
const configuration = await Effect13.runPromise(loadClientConfiguration);
|
|
4287
5550
|
await runMcp(configuration);
|
|
4288
5551
|
return;
|
|
4289
5552
|
}
|
|
@@ -4294,4 +5557,4 @@ await main().catch((cause) => {
|
|
|
4294
5557
|
process.exitCode = 1;
|
|
4295
5558
|
});
|
|
4296
5559
|
|
|
4297
|
-
//# debugId=
|
|
5560
|
+
//# debugId=42161A8EF448D66964756E2164756E21
|