@akshar5/cohall 0.2.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/bin/cohall.js ADDED
@@ -0,0 +1,3129 @@
1
+ #!/usr/bin/env node
2
+ var __defProp = Object.defineProperty;
3
+ var __returnValue = (v) => v;
4
+ function __exportSetter(name, newValue) {
5
+ this[name] = __returnValue.bind(null, newValue);
6
+ }
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, {
10
+ get: all[name],
11
+ enumerable: true,
12
+ configurable: true,
13
+ set: __exportSetter.bind(all, name)
14
+ });
15
+ };
16
+ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
+
18
+ // packages/protocol/src/index.ts
19
+ import { Schema } from "effect";
20
+ var version = "0.2.0", 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)), 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
+ expected: "an ISO-8601 UTC timestamp"
22
+ })).pipe(Schema.brand(name)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, Thread, Message, Task, 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;
23
+ var init_src = __esm(() => {
24
+ DeviceId = uuid("DeviceId");
25
+ ThreadId = uuid("ThreadId");
26
+ MessageId = uuid("MessageId");
27
+ TaskId = uuid("TaskId");
28
+ AuthSessionId = uuid("AuthSessionId");
29
+ Timestamp = isoTimestamp("Timestamp");
30
+ Platform = Schema.Literals(["darwin", "linux", "windows", "unknown"]);
31
+ Provider = Schema.Literals(["codex", "claude-code", "opencode"]);
32
+ DeviceStatus = Schema.Literals(["online", "busy", "offline"]);
33
+ TaskStatus = Schema.Literals([
34
+ "queued",
35
+ "assigned",
36
+ "running",
37
+ "cancelling",
38
+ "completed",
39
+ "failed",
40
+ "cancelled"
41
+ ]);
42
+ MessageRole = Schema.Literals(["human", "agent", "system"]);
43
+ ConnectionRole = Schema.Literals(["client", "device"]);
44
+ AuthSession = Schema.Struct({
45
+ id: AuthSessionId,
46
+ label: bounded(128),
47
+ role: ConnectionRole,
48
+ createdAt: Timestamp,
49
+ expiresAt: Timestamp,
50
+ lastSeenAt: Timestamp,
51
+ deviceId: Schema.optionalKey(DeviceId),
52
+ revokedAt: Schema.optionalKey(Timestamp)
53
+ });
54
+ CreatePairingInput = Schema.Struct({
55
+ label: bounded(128),
56
+ roles: boundedArray(ConnectionRole, 2)
57
+ });
58
+ PairingCredential = Schema.Struct({
59
+ token: bounded(256),
60
+ expiresAt: Timestamp
61
+ });
62
+ ExchangePairingInput = Schema.Struct({
63
+ token: bounded(256)
64
+ });
65
+ SessionCredential = Schema.Struct({
66
+ token: bounded(256),
67
+ session: AuthSession
68
+ });
69
+ PairingResult = Schema.Struct({
70
+ credentials: boundedArray(SessionCredential, 2)
71
+ });
72
+ Capability = Schema.Struct({
73
+ id: bounded(64),
74
+ label: bounded(128),
75
+ detail: Schema.optionalKey(optionalText(512))
76
+ });
77
+ Workspace = Schema.Struct({
78
+ path: bounded(4096),
79
+ label: bounded(256)
80
+ });
81
+ Device = Schema.Struct({
82
+ id: DeviceId,
83
+ name: bounded(128),
84
+ hostname: bounded(256),
85
+ platform: Platform,
86
+ architecture: bounded(64),
87
+ status: DeviceStatus,
88
+ providers: boundedArray(Provider, 3),
89
+ capabilities: boundedArray(Capability, 64),
90
+ workspaces: boundedArray(Workspace, 64),
91
+ version: bounded(32),
92
+ lastSeenAt: Timestamp,
93
+ connectedAt: Schema.optionalKey(Timestamp)
94
+ });
95
+ Thread = Schema.Struct({
96
+ id: ThreadId,
97
+ title: bounded(256),
98
+ createdAt: Timestamp,
99
+ updatedAt: Timestamp,
100
+ defaultDeviceId: Schema.optionalKey(DeviceId)
101
+ });
102
+ Message = Schema.Struct({
103
+ id: MessageId,
104
+ threadId: ThreadId,
105
+ role: MessageRole,
106
+ authorName: bounded(128),
107
+ content: optionalText(131072),
108
+ createdAt: Timestamp,
109
+ taskId: Schema.optionalKey(TaskId),
110
+ deviceId: Schema.optionalKey(DeviceId)
111
+ });
112
+ Task = Schema.Struct({
113
+ id: TaskId,
114
+ threadId: ThreadId,
115
+ prompt: bounded(131072),
116
+ context: Schema.optionalKey(optionalText(131072)),
117
+ provider: Provider,
118
+ status: TaskStatus,
119
+ sourceDeviceId: Schema.optionalKey(DeviceId),
120
+ targetDeviceId: DeviceId,
121
+ parentTaskId: Schema.optionalKey(TaskId),
122
+ workspace: Schema.optionalKey(bounded(4096)),
123
+ providerSessionId: Schema.optionalKey(bounded(4096)),
124
+ result: Schema.optionalKey(optionalText(131072)),
125
+ error: Schema.optionalKey(optionalText(16384)),
126
+ createdAt: Timestamp,
127
+ updatedAt: Timestamp,
128
+ startedAt: Schema.optionalKey(Timestamp),
129
+ completedAt: Schema.optionalKey(Timestamp)
130
+ });
131
+ ThreadContext = Schema.Struct({
132
+ thread: Thread,
133
+ messages: boundedArray(Message, 20),
134
+ tasks: boundedArray(Task, 20),
135
+ truncated: Schema.Boolean
136
+ });
137
+ CreateTaskInput = Schema.Struct({
138
+ threadId: Schema.optionalKey(ThreadId),
139
+ title: Schema.optionalKey(bounded(256)),
140
+ prompt: bounded(131072),
141
+ context: Schema.optionalKey(optionalText(131072)),
142
+ provider: Schema.optionalKey(Provider),
143
+ targetDeviceId: Schema.optionalKey(DeviceId),
144
+ parentTaskId: Schema.optionalKey(TaskId),
145
+ workspace: Schema.optionalKey(bounded(4096))
146
+ });
147
+ SocketEvent = Schema.TaggedUnion({
148
+ Authenticate: { token: bounded(256) },
149
+ Connected: { serverVersion: bounded(32), connectedAt: Timestamp },
150
+ DeviceHello: { device: Device },
151
+ DeviceHeartbeat: { deviceId: DeviceId, status: DeviceStatus },
152
+ TaskAssigned: { task: Task },
153
+ TaskAccepted: { taskId: TaskId },
154
+ TaskFinished: {
155
+ taskId: TaskId,
156
+ result: optionalText(131072),
157
+ providerSessionId: Schema.optionalKey(bounded(4096))
158
+ },
159
+ TaskFailed: { taskId: TaskId, error: bounded(16384) },
160
+ TaskCancelled: { taskId: TaskId },
161
+ TaskSettled: { taskId: TaskId },
162
+ CancelTask: { taskId: TaskId },
163
+ Error: { code: bounded(64), message: bounded(2048) }
164
+ });
165
+ ErrorResponse = Schema.Struct({
166
+ error: bounded(2048)
167
+ });
168
+ terminalTaskStatuses = new Set([
169
+ "completed",
170
+ "failed",
171
+ "cancelled"
172
+ ]);
173
+ decodeCreatePairingInput = Schema.decodeUnknownEffect(CreatePairingInput);
174
+ decodeExchangePairingInput = Schema.decodeUnknownEffect(ExchangePairingInput);
175
+ decodeCreateTaskInput = Schema.decodeUnknownEffect(CreateTaskInput);
176
+ decodeDevice = Schema.decodeUnknownEffect(Device);
177
+ decodeSocketEvent = Schema.decodeUnknownEffect(SocketEvent);
178
+ });
179
+
180
+ // apps/relay/src/config.ts
181
+ import { Effect as Effect8, Schema as Schema8 } from "effect";
182
+ import { access as access3, chmod as chmod2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
183
+ import { homedir as homedir3, platform as platform4 } from "node:os";
184
+ import { join as join4, resolve as resolve2 } from "node:path";
185
+ var RelayConfiguration, defaultDataDirectory = () => {
186
+ if (platform4() === "win32") {
187
+ return join4(process.env.LOCALAPPDATA ?? join4(homedir3(), "AppData", "Local"), "Cohall");
188
+ }
189
+ if (platform4() === "darwin") {
190
+ return join4(homedir3(), "Library", "Application Support", "Cohall", "relay");
191
+ }
192
+ return join4(process.env.XDG_DATA_HOME ?? join4(homedir3(), ".local", "share"), "cohall");
193
+ }, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
194
+ if (await access3(path).then(() => true).catch(() => false)) {
195
+ return (await readFile3(path, "utf8")).trim();
196
+ }
197
+ const token = generatedToken();
198
+ await writeFile3(path, `${token}
199
+ `, { mode: 384 });
200
+ if (platform4() !== "win32") {
201
+ await chmod2(path, 384);
202
+ }
203
+ return token;
204
+ }, booleanEnvironment = (name) => {
205
+ const value = process.env[name];
206
+ if (value === undefined || value === "false" || value === "0") {
207
+ return false;
208
+ }
209
+ if (value === "true" || value === "1") {
210
+ return true;
211
+ }
212
+ throw new Error(`${name} must be true or false`);
213
+ }, isLoopback = (host) => host === "127.0.0.1" || host === "localhost" || host === "::1", loadEnvironmentConfiguration;
214
+ var init_config = __esm(() => {
215
+ RelayConfiguration = Schema8.Struct({
216
+ host: Schema8.NonEmptyString,
217
+ port: Schema8.Int.check(Schema8.isBetween({ minimum: 1, maximum: 65535 })),
218
+ token: Schema8.NonEmptyString,
219
+ dataDirectory: Schema8.NonEmptyString,
220
+ databasePath: Schema8.NonEmptyString,
221
+ allowRemote: Schema8.Boolean
222
+ });
223
+ loadEnvironmentConfiguration = Effect8.tryPromise({
224
+ try: async () => {
225
+ const host = process.env.COHALL_RELAY_HOST ?? "127.0.0.1";
226
+ const port = Number(process.env.COHALL_RELAY_PORT ?? "8787");
227
+ const allowRemote = booleanEnvironment("COHALL_RELAY_ALLOW_REMOTE");
228
+ if (!isLoopback(host) && !allowRemote) {
229
+ throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
230
+ }
231
+ const dataDirectory = resolve2(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
232
+ await mkdir3(dataDirectory, { recursive: true, mode: 448 });
233
+ if (platform4() !== "win32") {
234
+ await chmod2(dataDirectory, 448);
235
+ }
236
+ const token = process.env.COHALL_TOKEN ?? await tokenFrom(join4(dataDirectory, "owner-token"));
237
+ return Schema8.decodeUnknownSync(RelayConfiguration)({
238
+ host,
239
+ port,
240
+ token,
241
+ dataDirectory,
242
+ databasePath: join4(dataDirectory, "cohall.db"),
243
+ allowRemote
244
+ });
245
+ },
246
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
247
+ });
248
+ });
249
+
250
+ // apps/relay/src/hub.ts
251
+ import { WebSocket as WebSocket2 } from "ws";
252
+
253
+ class Hub {
254
+ #principals = new Map;
255
+ #devices = new Map;
256
+ #deviceIds = new Map;
257
+ attach(socket, principal) {
258
+ this.#principals.set(socket, principal);
259
+ if (socket.data.authDeadline !== undefined) {
260
+ clearTimeout(socket.data.authDeadline);
261
+ socket.data.authDeadline = undefined;
262
+ }
263
+ }
264
+ isAuthorized(socket) {
265
+ const principal = this.#principals.get(socket);
266
+ if (principal === undefined) {
267
+ return false;
268
+ }
269
+ if (principal.expiresAt !== undefined && Date.parse(principal.expiresAt) <= Date.now()) {
270
+ socket.close(4003, "Session expired");
271
+ return false;
272
+ }
273
+ return true;
274
+ }
275
+ boundDeviceId(socket) {
276
+ return this.#principals.get(socket)?.boundDeviceId;
277
+ }
278
+ registerDevice(deviceId, socket) {
279
+ const previous = this.#devices.get(deviceId);
280
+ if (previous !== undefined && previous !== socket && previous.readyState === WebSocket2.OPEN) {
281
+ return false;
282
+ }
283
+ this.#devices.set(deviceId, socket);
284
+ this.#deviceIds.set(socket, deviceId);
285
+ return true;
286
+ }
287
+ detach(socket) {
288
+ if (socket.data.authDeadline !== undefined) {
289
+ clearTimeout(socket.data.authDeadline);
290
+ }
291
+ this.#principals.delete(socket);
292
+ const deviceId = this.#deviceIds.get(socket);
293
+ this.#deviceIds.delete(socket);
294
+ if (deviceId === undefined || this.#devices.get(deviceId) !== socket) {
295
+ return;
296
+ }
297
+ this.#devices.delete(deviceId);
298
+ return deviceId;
299
+ }
300
+ deviceId(socket) {
301
+ return this.#deviceIds.get(socket);
302
+ }
303
+ closeSession(sessionId) {
304
+ for (const [socket, principal] of this.#principals) {
305
+ if (principal.sessionId === sessionId) {
306
+ socket.close(4003, "Session revoked");
307
+ }
308
+ }
309
+ }
310
+ hasDevice(deviceId) {
311
+ return this.#devices.get(deviceId)?.readyState === WebSocket2.OPEN;
312
+ }
313
+ sendToDevice(deviceId, event) {
314
+ const socket = this.#devices.get(deviceId);
315
+ if (socket?.readyState !== WebSocket2.OPEN || !this.isAuthorized(socket)) {
316
+ return false;
317
+ }
318
+ try {
319
+ socket.send(JSON.stringify(event));
320
+ return true;
321
+ } catch {
322
+ return false;
323
+ }
324
+ }
325
+ }
326
+ var init_hub = () => {};
327
+
328
+ // apps/relay/src/database.ts
329
+ import { DatabaseSync } from "node:sqlite";
330
+
331
+ class Statement {
332
+ #statement;
333
+ constructor(statement) {
334
+ this.#statement = statement;
335
+ }
336
+ get(...parameters) {
337
+ const row = this.#statement.get(...parameters);
338
+ return row === undefined ? null : row;
339
+ }
340
+ all(...parameters) {
341
+ return this.#statement.all(...parameters);
342
+ }
343
+ run(...parameters) {
344
+ const result = this.#statement.run(...parameters);
345
+ return { changes: Number(result.changes) };
346
+ }
347
+ }
348
+
349
+ class Database {
350
+ #database;
351
+ constructor(path) {
352
+ this.#database = new DatabaseSync(path);
353
+ }
354
+ query(sql) {
355
+ return new Statement(this.#database.prepare(sql));
356
+ }
357
+ exec(sql) {
358
+ this.#database.exec(sql);
359
+ }
360
+ transaction(operation) {
361
+ return () => {
362
+ this.#database.exec("BEGIN IMMEDIATE");
363
+ try {
364
+ const result = operation();
365
+ this.#database.exec("COMMIT");
366
+ return result;
367
+ } catch (cause) {
368
+ this.#database.exec("ROLLBACK");
369
+ throw cause;
370
+ }
371
+ };
372
+ }
373
+ close() {
374
+ this.#database.close();
375
+ }
376
+ }
377
+ var init_database = () => {};
378
+
379
+ // apps/relay/src/store.ts
380
+ var exports_store = {};
381
+ __export(exports_store, {
382
+ layerFromDatabase: () => layerFromDatabase,
383
+ layer: () => layer2,
384
+ Service: () => Service2,
385
+ RelayStore: () => exports_store,
386
+ PersistenceError: () => PersistenceError
387
+ });
388
+ import { Context as Context2, Effect as Effect9, Layer as Layer2, Schema as Schema9 } from "effect";
389
+ import { createHash, randomBytes } from "node:crypto";
390
+ var PersistenceError, Service2, operationError = (operation) => (cause) => new PersistenceError({
391
+ operation,
392
+ message: cause instanceof Error ? cause.message : String(cause)
393
+ }), decode = (operation, schema, value) => Schema9.decodeUnknownEffect(schema)(value).pipe(Effect9.mapError(operationError(operation))), threadFromRow = (row) => decode("RelayStore.decodeThread", Thread, {
394
+ id: row.id,
395
+ title: row.title,
396
+ createdAt: row.created_at,
397
+ updatedAt: row.updated_at,
398
+ ...row.default_device_id === null ? {} : { defaultDeviceId: row.default_device_id }
399
+ }), messageFromRow = (row) => decode("RelayStore.decodeMessage", Message, {
400
+ id: row.id,
401
+ threadId: row.thread_id,
402
+ role: row.role === "tool" ? "agent" : row.role,
403
+ authorName: row.author_name,
404
+ content: row.content,
405
+ createdAt: row.created_at,
406
+ ...row.task_id === null ? {} : { taskId: row.task_id },
407
+ ...row.device_id === null ? {} : { deviceId: row.device_id }
408
+ }), deviceFromRow = (row) => decode("RelayStore.decodeDevice", Device, {
409
+ id: row.id,
410
+ name: row.name,
411
+ hostname: row.hostname,
412
+ platform: row.platform,
413
+ architecture: row.architecture,
414
+ status: row.status,
415
+ providers: JSON.parse(row.providers_json),
416
+ capabilities: JSON.parse(row.capabilities_json),
417
+ workspaces: JSON.parse(row.workspaces_json),
418
+ version: row.version,
419
+ lastSeenAt: row.last_seen_at,
420
+ ...row.connected_at === null ? {} : { connectedAt: row.connected_at }
421
+ }), taskFromRow = (row) => decode("RelayStore.decodeTask", Task, {
422
+ id: row.id,
423
+ threadId: row.thread_id,
424
+ prompt: row.prompt,
425
+ provider: row.provider,
426
+ status: row.status,
427
+ targetDeviceId: row.target_device_id,
428
+ createdAt: row.created_at,
429
+ updatedAt: row.updated_at,
430
+ ...row.context === null ? {} : { context: row.context },
431
+ ...row.source_device_id === null ? {} : { sourceDeviceId: row.source_device_id },
432
+ ...row.parent_task_id === null ? {} : { parentTaskId: row.parent_task_id },
433
+ ...row.workspace === null ? {} : { workspace: row.workspace },
434
+ ...row.provider_session_id === null ? {} : { providerSessionId: row.provider_session_id },
435
+ ...row.result === null ? {} : { result: row.result },
436
+ ...row.error === null ? {} : { error: row.error },
437
+ ...row.started_at === null ? {} : { startedAt: row.started_at },
438
+ ...row.completed_at === null ? {} : { completedAt: row.completed_at }
439
+ }), rolesFromRow = (row) => Schema9.decodeUnknownSync(Schema9.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
440
+ const roles = rolesFromRow(row);
441
+ const role = roles[0];
442
+ return role === undefined ? Effect9.fail(new PersistenceError({
443
+ operation: "RelayStore.decodeAuthSession",
444
+ message: `Session ${row.id} has no role`
445
+ })) : decode("RelayStore.decodeAuthSession", AuthSession, {
446
+ id: row.id,
447
+ label: row.label,
448
+ role,
449
+ createdAt: row.created_at,
450
+ expiresAt: row.expires_at,
451
+ lastSeenAt: row.last_seen_at,
452
+ ...row.bound_device_id === null ? {} : { deviceId: row.bound_device_id },
453
+ ...row.revoked_at === null ? {} : { revokedAt: row.revoked_at }
454
+ });
455
+ }, secret = (prefix) => {
456
+ return `cohall_${prefix}_${randomBytes(32).toString("hex")}`;
457
+ }, tokenHash = (token) => createHash("sha256").update(token).digest("hex"), providerName = (provider) => {
458
+ switch (provider) {
459
+ case "codex":
460
+ return "Codex";
461
+ case "claude-code":
462
+ return "Claude Code";
463
+ case "opencode":
464
+ return "OpenCode";
465
+ }
466
+ }, recentWithin = (items, byteBudget) => {
467
+ const encoder = new TextEncoder;
468
+ const selected = [];
469
+ let bytes = 0;
470
+ for (let index = items.length - 1;index >= 0; index -= 1) {
471
+ const item = items[index];
472
+ if (item === undefined) {
473
+ continue;
474
+ }
475
+ const itemBytes = encoder.encode(JSON.stringify(item)).byteLength;
476
+ if (bytes + itemBytes > byteBudget) {
477
+ break;
478
+ }
479
+ bytes += itemBytes;
480
+ selected.unshift(item);
481
+ }
482
+ return { items: selected, truncated: selected.length < items.length };
483
+ }, makeService = (db) => {
484
+ const queryTask = (taskId) => db.query("SELECT * FROM tasks WHERE id = ?").get(taskId);
485
+ const getTask = Effect9.fn("RelayStore.getTask")(function* (taskId) {
486
+ const row = yield* Effect9.try({
487
+ try: () => queryTask(taskId),
488
+ catch: operationError("RelayStore.getTask")
489
+ });
490
+ if (row === null) {
491
+ return yield* Effect9.fail(new PersistenceError({
492
+ operation: "RelayStore.getTask",
493
+ message: `Unknown task ${taskId}`
494
+ }));
495
+ }
496
+ return yield* taskFromRow(row);
497
+ });
498
+ const listDevices = Effect9.fn("RelayStore.listDevices")(function* () {
499
+ const rows = yield* Effect9.try({
500
+ try: () => db.query("SELECT * FROM devices ORDER BY name COLLATE NOCASE, id").all(),
501
+ catch: operationError("RelayStore.listDevices")
502
+ });
503
+ return yield* Effect9.forEach(rows, deviceFromRow);
504
+ });
505
+ const recover = Effect9.fn("RelayStore.recover")(function* () {
506
+ yield* Effect9.try({
507
+ try: () => db.transaction(() => {
508
+ const timestamp = now();
509
+ db.query("UPDATE devices SET status = 'offline'").run();
510
+ db.query("UPDATE tasks SET status = 'queued', updated_at = ? WHERE status IN ('assigned', 'running')").run(timestamp);
511
+ db.query("DELETE FROM auth_pairings WHERE expires_at <= ? OR used_at IS NOT NULL").run(timestamp);
512
+ db.query("UPDATE auth_sessions SET revoked_at = ? WHERE revoked_at IS NULL AND roles_json LIKE '%,%'").run(timestamp);
513
+ })(),
514
+ catch: operationError("RelayStore.recover")
515
+ });
516
+ });
517
+ const upsertDevice = Effect9.fn("RelayStore.upsertDevice")(function* (device) {
518
+ yield* Effect9.try({
519
+ try: () => db.query(`INSERT INTO devices (
520
+ id, name, hostname, platform, architecture, status, providers_json,
521
+ capabilities_json, workspaces_json, version, last_seen_at, connected_at
522
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
523
+ ON CONFLICT(id) DO UPDATE SET
524
+ name = excluded.name, hostname = excluded.hostname, platform = excluded.platform,
525
+ architecture = excluded.architecture, status = excluded.status,
526
+ providers_json = excluded.providers_json, capabilities_json = excluded.capabilities_json,
527
+ workspaces_json = excluded.workspaces_json, version = excluded.version,
528
+ last_seen_at = excluded.last_seen_at, connected_at = excluded.connected_at`).run(device.id, device.name, device.hostname, device.platform, device.architecture, device.status, JSON.stringify(device.providers), JSON.stringify(device.capabilities), JSON.stringify(device.workspaces), device.version, device.lastSeenAt, device.connectedAt ?? null),
529
+ catch: operationError("RelayStore.upsertDevice")
530
+ });
531
+ return device;
532
+ });
533
+ const updateDeviceStatus = Effect9.fn("RelayStore.updateDeviceStatus")(function* (deviceId, status) {
534
+ const timestamp = now();
535
+ const result = yield* Effect9.try({
536
+ try: () => db.query("UPDATE devices SET status = ?, last_seen_at = ? WHERE id = ?").run(status, timestamp, deviceId),
537
+ catch: operationError("RelayStore.updateDeviceStatus")
538
+ });
539
+ if (result.changes !== 1) {
540
+ return yield* Effect9.fail(new PersistenceError({
541
+ operation: "RelayStore.updateDeviceStatus",
542
+ message: `Unknown device ${deviceId}`
543
+ }));
544
+ }
545
+ const row = db.query("SELECT * FROM devices WHERE id = ?").get(deviceId);
546
+ if (row === null) {
547
+ return yield* Effect9.fail(new PersistenceError({
548
+ operation: "RelayStore.updateDeviceStatus.read",
549
+ message: `Device ${deviceId} disappeared after update`
550
+ }));
551
+ }
552
+ return yield* deviceFromRow(row);
553
+ });
554
+ const createDelegation2 = Effect9.fn("RelayStore.createDelegation")(function* (input, targetDeviceId, sourceDeviceId, providerSessionId) {
555
+ const timestamp = now();
556
+ const threadId = input.threadId ?? makeThreadId();
557
+ const task = Task.make({
558
+ id: makeTaskId(),
559
+ threadId,
560
+ prompt: input.prompt,
561
+ provider: input.provider ?? "codex",
562
+ status: "queued",
563
+ targetDeviceId,
564
+ createdAt: timestamp,
565
+ updatedAt: timestamp,
566
+ ...input.context === undefined ? {} : { context: input.context },
567
+ ...sourceDeviceId === undefined ? {} : { sourceDeviceId },
568
+ ...input.parentTaskId === undefined ? {} : { parentTaskId: input.parentTaskId },
569
+ ...input.workspace === undefined ? {} : { workspace: input.workspace },
570
+ ...providerSessionId === undefined ? {} : { providerSessionId }
571
+ });
572
+ const thread = input.threadId === undefined ? Thread.make({
573
+ id: threadId,
574
+ title: input.title ?? input.prompt.slice(0, 72),
575
+ createdAt: timestamp,
576
+ updatedAt: timestamp
577
+ }) : undefined;
578
+ const message = Message.make({
579
+ id: makeMessageId(),
580
+ threadId,
581
+ role: "human",
582
+ authorName: sourceDeviceId === undefined ? "User" : "Remote agent",
583
+ content: input.prompt,
584
+ createdAt: timestamp,
585
+ taskId: task.id,
586
+ ...sourceDeviceId === undefined ? {} : { deviceId: sourceDeviceId }
587
+ });
588
+ yield* Effect9.try({
589
+ try: () => db.transaction(() => {
590
+ const outstanding = db.query(`SELECT COUNT(*) AS count FROM tasks WHERE target_device_id = ?
591
+ AND status IN ('queued', 'assigned', 'running', 'cancelling')`).get(targetDeviceId);
592
+ if ((outstanding?.count ?? 0) >= 100) {
593
+ throw new Error(`Device ${targetDeviceId} has reached its outstanding task limit`);
594
+ }
595
+ if (thread !== undefined) {
596
+ db.query("INSERT INTO threads (id, title, created_at, updated_at) VALUES (?, ?, ?, ?)").run(thread.id, thread.title, thread.createdAt, thread.updatedAt);
597
+ } else if (db.query("SELECT id FROM threads WHERE id = ?").get(threadId) === null) {
598
+ throw new Error(`Unknown thread ${threadId}`);
599
+ }
600
+ db.query(`INSERT INTO tasks (
601
+ id, thread_id, prompt, context, provider, status, source_device_id,
602
+ target_device_id, parent_task_id, workspace, provider_session_id,
603
+ created_at, updated_at
604
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(task.id, task.threadId, task.prompt, task.context ?? null, task.provider, task.status, task.sourceDeviceId ?? null, task.targetDeviceId, task.parentTaskId ?? null, task.workspace ?? null, task.providerSessionId ?? null, task.createdAt, task.updatedAt);
605
+ db.query(`INSERT INTO messages (
606
+ id, thread_id, role, kind, author_id, author_name, content, created_at,
607
+ task_id, device_id
608
+ ) VALUES (?, ?, ?, 'chat', ?, ?, ?, ?, ?, ?)`).run(message.id, message.threadId, message.role, sourceDeviceId ?? "user", message.authorName, message.content, message.createdAt, message.taskId ?? null, message.deviceId ?? null);
609
+ db.query("UPDATE threads SET updated_at = ? WHERE id = ?").run(timestamp, threadId);
610
+ })(),
611
+ catch: operationError("RelayStore.createDelegation")
612
+ });
613
+ return task;
614
+ });
615
+ const threadContext2 = Effect9.fn("RelayStore.threadContext")(function* (threadId) {
616
+ const threadRow = yield* Effect9.try({
617
+ try: () => db.query("SELECT * FROM threads WHERE id = ?").get(threadId),
618
+ catch: operationError("RelayStore.threadContext.thread")
619
+ });
620
+ if (threadRow === null) {
621
+ return yield* Effect9.fail(new PersistenceError({
622
+ operation: "RelayStore.threadContext",
623
+ message: `Unknown thread ${threadId}`
624
+ }));
625
+ }
626
+ const [messageRows, taskRows] = yield* Effect9.try({
627
+ try: () => [
628
+ db.query(`SELECT * FROM (
629
+ SELECT * FROM messages WHERE thread_id = ?
630
+ ORDER BY created_at DESC, id DESC LIMIT 21
631
+ ) ORDER BY created_at, id`).all(threadId),
632
+ db.query(`SELECT * FROM (
633
+ SELECT * FROM tasks WHERE thread_id = ?
634
+ ORDER BY created_at DESC, id DESC LIMIT 21
635
+ ) ORDER BY created_at, id`).all(threadId)
636
+ ],
637
+ catch: operationError("RelayStore.threadContext.items")
638
+ });
639
+ const [thread, messages, tasks] = yield* Effect9.all([
640
+ threadFromRow(threadRow),
641
+ Effect9.forEach(messageRows.slice(-20), messageFromRow),
642
+ Effect9.forEach(taskRows.slice(-20), taskFromRow)
643
+ ]);
644
+ const recentMessages = recentWithin(messages, 256 * 1024);
645
+ const recentTasks = recentWithin(tasks, 768 * 1024);
646
+ return ThreadContext.make({
647
+ thread,
648
+ messages: recentMessages.items,
649
+ tasks: recentTasks.items,
650
+ truncated: messageRows.length > 20 || taskRows.length > 20 || recentMessages.truncated || recentTasks.truncated
651
+ });
652
+ });
653
+ const pendingTasksFor = Effect9.fn("RelayStore.pendingTasksFor")(function* (deviceId) {
654
+ const rows = yield* Effect9.try({
655
+ try: () => db.query(`SELECT * FROM tasks WHERE target_device_id = ?
656
+ AND status IN ('queued', 'cancelling') ORDER BY created_at, id LIMIT 100`).all(deviceId),
657
+ catch: operationError("RelayStore.pendingTasksFor")
658
+ });
659
+ return yield* Effect9.forEach(rows, taskFromRow);
660
+ });
661
+ const transition = Effect9.fn("RelayStore.transition")(function* (taskId, expected, update) {
662
+ const current = yield* getTask(taskId);
663
+ if (!expected.includes(current.status)) {
664
+ return current;
665
+ }
666
+ const placeholders = expected.map(() => "?").join(", ");
667
+ yield* Effect9.try({
668
+ try: () => db.query(`UPDATE tasks SET status = ?, provider_session_id = ?, result = ?, error = ?,
669
+ started_at = ?, completed_at = ?, updated_at = ?
670
+ WHERE id = ? AND status IN (${placeholders})`).run(update.status ?? current.status, update.providerSessionId ?? current.providerSessionId ?? null, update.result ?? current.result ?? null, update.error ?? current.error ?? null, update.startedAt ?? current.startedAt ?? null, update.completedAt ?? current.completedAt ?? null, now(), taskId, ...expected),
671
+ catch: operationError("RelayStore.transition")
672
+ });
673
+ return yield* getTask(taskId);
674
+ });
675
+ const assignTask = Effect9.fn("RelayStore.assignTask")(function* (taskId) {
676
+ const current = yield* getTask(taskId);
677
+ if (current.status !== "queued") {
678
+ return current;
679
+ }
680
+ yield* Effect9.try({
681
+ try: () => db.query(`UPDATE tasks SET status = 'assigned', updated_at = ?
682
+ WHERE id = ? AND status = 'queued' AND NOT EXISTS (
683
+ SELECT 1 FROM tasks active
684
+ WHERE active.target_device_id = ? AND active.id <> ?
685
+ AND active.status IN ('assigned', 'running', 'cancelling')
686
+ )`).run(now(), taskId, current.targetDeviceId, taskId),
687
+ catch: operationError("RelayStore.assignTask")
688
+ });
689
+ return yield* getTask(taskId);
690
+ });
691
+ const requireTarget = Effect9.fn("RelayStore.requireTarget")(function* (taskId, deviceId) {
692
+ const task = yield* getTask(taskId);
693
+ if (task.targetDeviceId !== deviceId) {
694
+ return yield* Effect9.fail(new PersistenceError({
695
+ operation: "RelayStore.requireTarget",
696
+ message: `Task ${taskId} belongs to another device`
697
+ }));
698
+ }
699
+ return task;
700
+ });
701
+ const terminal = Effect9.fn("RelayStore.terminal")(function* (taskId, deviceId, status, result, error, providerSessionId) {
702
+ const current = yield* requireTarget(taskId, deviceId);
703
+ if (["completed", "failed", "cancelled"].includes(current.status)) {
704
+ return current;
705
+ }
706
+ const timestamp = now();
707
+ yield* Effect9.try({
708
+ try: () => db.transaction(() => {
709
+ const updated = db.query(`UPDATE tasks SET status = ?, result = ?, error = ?, provider_session_id = ?,
710
+ completed_at = ?, updated_at = ? WHERE id = ?
711
+ AND status NOT IN ('completed', 'failed', 'cancelled')`).run(status, result ?? null, error ?? null, providerSessionId ?? current.providerSessionId ?? null, timestamp, timestamp, taskId);
712
+ if (updated.changes !== 1) {
713
+ return;
714
+ }
715
+ if (providerSessionId !== undefined) {
716
+ db.query(`INSERT INTO provider_sessions (thread_id, device_id, provider, session_id, updated_at)
717
+ VALUES (?, ?, ?, ?, ?)
718
+ ON CONFLICT(thread_id, device_id, provider) DO UPDATE SET
719
+ session_id = excluded.session_id, updated_at = excluded.updated_at`).run(current.threadId, deviceId, current.provider, providerSessionId, timestamp);
720
+ }
721
+ const content = result ?? error;
722
+ if (content !== undefined) {
723
+ const role = status === "completed" ? "agent" : "system";
724
+ db.query(`INSERT INTO messages (
725
+ id, thread_id, role, kind, author_id, author_name, content, created_at,
726
+ task_id, device_id
727
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`).run(makeMessageId(), current.threadId, role, status === "failed" ? "error" : "chat", deviceId, status === "completed" ? providerName(current.provider) : "Cohall", content, timestamp, taskId, deviceId);
728
+ db.query("UPDATE threads SET updated_at = ? WHERE id = ?").run(timestamp, current.threadId);
729
+ }
730
+ })(),
731
+ catch: operationError("RelayStore.terminal")
732
+ });
733
+ return yield* getTask(taskId);
734
+ });
735
+ const requestCancellation = Effect9.fn("RelayStore.requestCancellation")(function* (taskId) {
736
+ const current = yield* getTask(taskId);
737
+ if (["completed", "failed", "cancelled"].includes(current.status)) {
738
+ return current;
739
+ }
740
+ return current.status === "queued" ? yield* transition(taskId, ["queued"], { status: "cancelled", completedAt: now() }) : yield* transition(taskId, ["assigned", "running", "cancelling"], {
741
+ status: "cancelling"
742
+ });
743
+ });
744
+ const requeueTasksFor = Effect9.fn("RelayStore.requeueTasksFor")(function* (deviceId) {
745
+ yield* Effect9.try({
746
+ try: () => db.query(`UPDATE tasks SET status = 'queued', updated_at = ?
747
+ WHERE target_device_id = ? AND status IN ('assigned', 'running')`).run(now(), deviceId),
748
+ catch: operationError("RelayStore.requeueTasksFor")
749
+ });
750
+ });
751
+ const sessionFor = Effect9.fn("RelayStore.sessionFor")(function* (threadId, deviceId, provider) {
752
+ const row = yield* Effect9.try({
753
+ try: () => db.query(`SELECT session_id FROM provider_sessions
754
+ WHERE thread_id = ? AND device_id = ? AND provider = ?`).get(threadId, deviceId, provider),
755
+ catch: operationError("RelayStore.sessionFor")
756
+ });
757
+ return row?.session_id;
758
+ });
759
+ const createPairing = Effect9.fn("RelayStore.createPairing")(function* (input) {
760
+ const roles = [...new Set(input.roles)];
761
+ if (roles.length === 0) {
762
+ return yield* Effect9.fail(new PersistenceError({
763
+ operation: "RelayStore.createPairing",
764
+ message: "A pairing must allow at least one role"
765
+ }));
766
+ }
767
+ const token = secret("pair");
768
+ const expiresAt = new Date(Date.now() + 10 * 60 * 1000).toISOString();
769
+ yield* Effect9.try({
770
+ try: () => db.query(`INSERT INTO auth_pairings (token_hash, label, roles_json, expires_at, created_at)
771
+ VALUES (?, ?, ?, ?, ?)`).run(tokenHash(token), input.label, JSON.stringify(roles), expiresAt, now()),
772
+ catch: operationError("RelayStore.createPairing")
773
+ });
774
+ return PairingCredential.make({ token, expiresAt: Timestamp.make(expiresAt) });
775
+ });
776
+ const exchangePairing2 = Effect9.fn("RelayStore.exchangePairing")(function* (token) {
777
+ const timestamp = now();
778
+ const expiresAt = new Date(Date.now() + 90 * 24 * 60 * 60 * 1000).toISOString();
779
+ const created = yield* Effect9.try({
780
+ try: () => db.transaction(() => {
781
+ const pairing = db.query(`SELECT * FROM auth_pairings
782
+ WHERE token_hash = ? AND used_at IS NULL AND expires_at > ?`).get(tokenHash(token), timestamp);
783
+ if (pairing === null) {
784
+ throw new Error("Pairing credential is invalid, expired, or already used");
785
+ }
786
+ const roles = Schema9.decodeUnknownSync(Schema9.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
787
+ const deviceId = roles.includes("device") ? makeDeviceId() : undefined;
788
+ const consumed = db.query("UPDATE auth_pairings SET used_at = ? WHERE token_hash = ? AND used_at IS NULL").run(timestamp, pairing.token_hash);
789
+ if (consumed.changes !== 1) {
790
+ throw new Error("Pairing credential was already used");
791
+ }
792
+ return [...new Set(roles)].map((role) => {
793
+ const sessionToken = secret("session");
794
+ const row = {
795
+ id: makeAuthSessionId(),
796
+ token_hash: tokenHash(sessionToken),
797
+ label: pairing.label,
798
+ roles_json: JSON.stringify([role]),
799
+ created_at: timestamp,
800
+ expires_at: expiresAt,
801
+ last_seen_at: timestamp,
802
+ bound_device_id: role === "device" ? deviceId ?? null : null,
803
+ revoked_at: null
804
+ };
805
+ db.query(`INSERT INTO auth_sessions (
806
+ id, token_hash, label, roles_json, created_at, expires_at, last_seen_at,
807
+ bound_device_id
808
+ ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`).run(row.id, row.token_hash, row.label, row.roles_json, row.created_at, row.expires_at, row.last_seen_at, row.bound_device_id);
809
+ return { token: sessionToken, row };
810
+ });
811
+ })(),
812
+ catch: operationError("RelayStore.exchangePairing")
813
+ });
814
+ const credentials = yield* Effect9.forEach(created, ({ token: sessionToken, row }) => authSessionFromRow(row).pipe(Effect9.map((session) => ({ token: sessionToken, session }))));
815
+ return PairingResult.make({ credentials });
816
+ });
817
+ const authenticateSession = Effect9.fn("RelayStore.authenticateSession")(function* (token, role) {
818
+ const row = yield* Effect9.try({
819
+ try: () => db.query(`SELECT * FROM auth_sessions
820
+ WHERE token_hash = ? AND revoked_at IS NULL AND expires_at > ?`).get(tokenHash(token), now()),
821
+ catch: operationError("RelayStore.authenticateSession")
822
+ });
823
+ if (row === null || !rolesFromRow(row).includes(role)) {
824
+ return;
825
+ }
826
+ const lastSeenAt = now();
827
+ yield* Effect9.try({
828
+ try: () => db.query("UPDATE auth_sessions SET last_seen_at = ? WHERE id = ?").run(lastSeenAt, row.id),
829
+ catch: operationError("RelayStore.authenticateSession.touch")
830
+ });
831
+ const session = yield* authSessionFromRow(row);
832
+ return AuthSession.make({ ...session, lastSeenAt });
833
+ });
834
+ const listAuthSessions = Effect9.fn("RelayStore.listAuthSessions")(function* () {
835
+ const rows = yield* Effect9.try({
836
+ try: () => db.query("SELECT * FROM auth_sessions ORDER BY created_at DESC, id LIMIT 1000").all(),
837
+ catch: operationError("RelayStore.listAuthSessions")
838
+ });
839
+ return yield* Effect9.forEach(rows, authSessionFromRow);
840
+ });
841
+ const revokeAuthSession = Effect9.fn("RelayStore.revokeAuthSession")(function* (sessionId) {
842
+ const revokedAt = now();
843
+ const result = yield* Effect9.try({
844
+ try: () => db.query("UPDATE auth_sessions SET revoked_at = ? WHERE id = ? AND revoked_at IS NULL").run(revokedAt, sessionId),
845
+ catch: operationError("RelayStore.revokeAuthSession")
846
+ });
847
+ if (result.changes !== 1) {
848
+ return yield* Effect9.fail(new PersistenceError({
849
+ operation: "RelayStore.revokeAuthSession",
850
+ message: `Unknown or already revoked session ${sessionId}`
851
+ }));
852
+ }
853
+ const row = db.query("SELECT * FROM auth_sessions WHERE id = ?").get(sessionId);
854
+ if (row === null) {
855
+ return yield* Effect9.fail(new PersistenceError({
856
+ operation: "RelayStore.revokeAuthSession.read",
857
+ message: `Session ${sessionId} disappeared after revocation`
858
+ }));
859
+ }
860
+ return yield* authSessionFromRow(row);
861
+ });
862
+ return Service2.of({
863
+ recover,
864
+ listDevices,
865
+ upsertDevice,
866
+ heartbeat: (deviceId, status) => updateDeviceStatus(deviceId, status),
867
+ markDeviceOffline: (deviceId) => updateDeviceStatus(deviceId, "offline"),
868
+ createDelegation: createDelegation2,
869
+ getTask,
870
+ threadContext: threadContext2,
871
+ pendingTasksFor,
872
+ assignTask,
873
+ rollbackAssignment: (taskId) => transition(taskId, ["assigned"], { status: "queued" }),
874
+ acceptTask: (taskId, deviceId) => requireTarget(taskId, deviceId).pipe(Effect9.flatMap(() => transition(taskId, ["assigned"], { status: "running", startedAt: now() }))),
875
+ finishTask: (taskId, deviceId, result, providerSessionId) => terminal(taskId, deviceId, "completed", result, undefined, providerSessionId),
876
+ failTask: (taskId, deviceId, error) => terminal(taskId, deviceId, "failed", undefined, error),
877
+ acknowledgeCancellation: (taskId, deviceId) => terminal(taskId, deviceId, "cancelled"),
878
+ requestCancellation,
879
+ requeueTasksFor,
880
+ sessionFor,
881
+ createPairing,
882
+ exchangePairing: exchangePairing2,
883
+ authenticateSession,
884
+ listAuthSessions,
885
+ revokeAuthSession
886
+ });
887
+ }, migrate = (db) => Effect9.try({
888
+ try: () => {
889
+ db.exec("PRAGMA journal_mode = WAL");
890
+ db.exec("PRAGMA foreign_keys = ON");
891
+ db.exec("PRAGMA busy_timeout = 5000");
892
+ db.exec(`
893
+ CREATE TABLE IF NOT EXISTS devices (
894
+ id TEXT PRIMARY KEY, name TEXT NOT NULL, hostname TEXT NOT NULL,
895
+ platform TEXT NOT NULL, architecture TEXT NOT NULL, status TEXT NOT NULL,
896
+ providers_json TEXT NOT NULL, capabilities_json TEXT NOT NULL,
897
+ workspaces_json TEXT NOT NULL, version TEXT NOT NULL, last_seen_at TEXT NOT NULL,
898
+ connected_at TEXT
899
+ );
900
+ CREATE TABLE IF NOT EXISTS threads (
901
+ id TEXT PRIMARY KEY, title TEXT NOT NULL, created_at TEXT NOT NULL,
902
+ updated_at TEXT NOT NULL, archived_at TEXT,
903
+ default_device_id TEXT REFERENCES devices(id)
904
+ );
905
+ CREATE TABLE IF NOT EXISTS messages (
906
+ id TEXT PRIMARY KEY, thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
907
+ role TEXT NOT NULL, kind TEXT NOT NULL DEFAULT 'chat', author_id TEXT NOT NULL,
908
+ author_name TEXT NOT NULL, content TEXT NOT NULL, created_at TEXT NOT NULL,
909
+ task_id TEXT, reply_to TEXT, device_id TEXT REFERENCES devices(id)
910
+ );
911
+ CREATE INDEX IF NOT EXISTS messages_thread_created ON messages(thread_id, created_at);
912
+ CREATE TABLE IF NOT EXISTS tasks (
913
+ id TEXT PRIMARY KEY, thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
914
+ prompt TEXT NOT NULL, context TEXT, provider TEXT NOT NULL, status TEXT NOT NULL,
915
+ source_device_id TEXT REFERENCES devices(id),
916
+ target_device_id TEXT NOT NULL REFERENCES devices(id), parent_task_id TEXT,
917
+ workspace TEXT, provider_session_id TEXT, result TEXT, error TEXT,
918
+ created_at TEXT NOT NULL, updated_at TEXT NOT NULL, started_at TEXT, completed_at TEXT
919
+ );
920
+ CREATE INDEX IF NOT EXISTS tasks_thread_created ON tasks(thread_id, created_at);
921
+ CREATE INDEX IF NOT EXISTS tasks_target_status ON tasks(target_device_id, status);
922
+ CREATE TABLE IF NOT EXISTS provider_sessions (
923
+ thread_id TEXT NOT NULL REFERENCES threads(id) ON DELETE CASCADE,
924
+ device_id TEXT NOT NULL REFERENCES devices(id) ON DELETE CASCADE,
925
+ provider TEXT NOT NULL, session_id TEXT NOT NULL, updated_at TEXT NOT NULL,
926
+ PRIMARY KEY(thread_id, device_id, provider)
927
+ );
928
+ CREATE TABLE IF NOT EXISTS auth_pairings (
929
+ token_hash TEXT PRIMARY KEY, label TEXT NOT NULL, roles_json TEXT NOT NULL,
930
+ expires_at TEXT NOT NULL, created_at TEXT NOT NULL, used_at TEXT
931
+ );
932
+ CREATE TABLE IF NOT EXISTS auth_sessions (
933
+ id TEXT PRIMARY KEY, token_hash TEXT NOT NULL UNIQUE, label TEXT NOT NULL,
934
+ roles_json TEXT NOT NULL, created_at TEXT NOT NULL, expires_at TEXT NOT NULL,
935
+ last_seen_at TEXT NOT NULL, bound_device_id TEXT, revoked_at TEXT
936
+ );
937
+ CREATE INDEX IF NOT EXISTS auth_sessions_token ON auth_sessions(token_hash);
938
+ `);
939
+ },
940
+ catch: operationError("RelayStore.migrate")
941
+ }), layer2 = (path) => Layer2.effect(Service2, Effect9.acquireRelease(Effect9.try({
942
+ try: () => new Database(path),
943
+ catch: operationError("RelayStore.open")
944
+ }).pipe(Effect9.tap(migrate)), (db) => Effect9.sync(() => db.close())).pipe(Effect9.map(makeService))), layerFromDatabase = (db) => Layer2.effect(Service2, migrate(db).pipe(Effect9.map(() => makeService(db))));
945
+ var init_store = __esm(() => {
946
+ init_src();
947
+ init_database();
948
+ init_store();
949
+ PersistenceError = class PersistenceError extends Schema9.TaggedErrorClass()("RelayStore.PersistenceError", { operation: Schema9.String, message: Schema9.String }) {
950
+ };
951
+ Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
952
+ };
953
+ });
954
+
955
+ // apps/relay/src/main.ts
956
+ var exports_main = {};
957
+ __export(exports_main, {
958
+ runRelay: () => runRelay
959
+ });
960
+ import { Effect as Effect10, ManagedRuntime, Schema as Schema10 } from "effect";
961
+ import { createHash as createHash2, timingSafeEqual } from "node:crypto";
962
+ import { access as access4, chmod as chmod3 } from "node:fs/promises";
963
+ import { createServer } from "node:http";
964
+ import { platform as platform5 } from "node:os";
965
+ import { WebSocketServer } from "ws";
966
+ var RequestError, json = (value, status = 200) => Response.json(value, {
967
+ status,
968
+ headers: {
969
+ "cache-control": "no-store",
970
+ "x-content-type-options": "nosniff"
971
+ }
972
+ }), requestBody = (request, limit) => {
973
+ if (request.method === "GET" || request.method === "HEAD") {
974
+ return Promise.resolve(undefined);
975
+ }
976
+ return new Promise((resolve3, reject) => {
977
+ const chunks = [];
978
+ let size = 0;
979
+ const cleanup = () => {
980
+ request.off("data", onData);
981
+ request.off("end", onEnd);
982
+ request.off("error", onError);
983
+ };
984
+ const onData = (chunk) => {
985
+ size += chunk.byteLength;
986
+ if (size <= limit) {
987
+ chunks.push(chunk);
988
+ return;
989
+ }
990
+ cleanup();
991
+ reject(new Error("Request body exceeded 256 KiB"));
992
+ };
993
+ const onEnd = () => {
994
+ cleanup();
995
+ resolve3(Buffer.concat(chunks, size));
996
+ };
997
+ const onError = (cause) => {
998
+ cleanup();
999
+ reject(cause);
1000
+ };
1001
+ request.on("data", onData);
1002
+ request.once("end", onEnd);
1003
+ request.once("error", onError);
1004
+ });
1005
+ }, webRequest = async (request) => {
1006
+ const headers = new Headers;
1007
+ for (let index = 0;index < request.rawHeaders.length; index += 2) {
1008
+ const name = request.rawHeaders[index];
1009
+ const value = request.rawHeaders[index + 1];
1010
+ if (name !== undefined && value !== undefined) {
1011
+ headers.append(name, value);
1012
+ }
1013
+ }
1014
+ const payload = await requestBody(request, 256 * 1024);
1015
+ return new Request(new URL(request.url ?? "/", "http://cohall.local"), {
1016
+ method: request.method ?? "GET",
1017
+ headers,
1018
+ ...payload === undefined ? {} : { body: new Uint8Array(payload) }
1019
+ });
1020
+ }, sendResponse = async (target, response) => {
1021
+ target.statusCode = response.status;
1022
+ response.headers.forEach((value, name) => target.setHeader(name, value));
1023
+ target.end(Buffer.from(await response.arrayBuffer()));
1024
+ }, socketText = (message) => {
1025
+ if (Buffer.isBuffer(message)) {
1026
+ return message.toString("utf8");
1027
+ }
1028
+ if (message instanceof ArrayBuffer) {
1029
+ return Buffer.from(message).toString("utf8");
1030
+ }
1031
+ if (Array.isArray(message)) {
1032
+ return Buffer.concat(message).toString("utf8");
1033
+ }
1034
+ return Buffer.from(message).toString("utf8");
1035
+ }, rejectUpgrade = (socket, status, message) => {
1036
+ socket.write(`HTTP/1.1 ${status} ${message}\r
1037
+ Connection: close\r
1038
+ Content-Length: 0\r
1039
+ \r
1040
+ `);
1041
+ if ("destroy" in socket && typeof socket.destroy === "function") {
1042
+ socket.destroy();
1043
+ }
1044
+ }, body = (request, decode2) => Effect10.tryPromise({
1045
+ try: () => request.json(),
1046
+ catch: () => new RequestError({ status: 400, message: "Expected a JSON request body" })
1047
+ }).pipe(Effect10.flatMap(decode2), Effect10.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) => Schema10.decodeUnknownEffect(schema)(value).pipe(Effect10.mapError(() => new RequestError({ status: 400, message: `Invalid identifier: ${value}` }))), scoreDevice = (device, input, sourceDeviceId) => {
1048
+ const prompt = `${input.prompt} ${input.context ?? ""}`.toLowerCase();
1049
+ const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
1050
+ const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
1051
+ 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);
1052
+ }, chooseDevice = (input, sourceDeviceId) => Effect10.gen(function* () {
1053
+ const store = yield* exports_store.Service;
1054
+ const devices = yield* store.listDevices().pipe(Effect10.mapError((cause) => new RequestError({ status: 500, message: cause.message })));
1055
+ const provider = input.provider ?? "codex";
1056
+ if (input.targetDeviceId !== undefined) {
1057
+ const target = devices.find((device) => device.id === input.targetDeviceId);
1058
+ if (target === undefined) {
1059
+ return yield* new RequestError({
1060
+ status: 404,
1061
+ message: `Unknown target device ${input.targetDeviceId}`
1062
+ });
1063
+ }
1064
+ if (!target.providers.includes(provider)) {
1065
+ return yield* new RequestError({
1066
+ status: 409,
1067
+ message: `${target.name} does not advertise the ${provider} provider`
1068
+ });
1069
+ }
1070
+ return target.id;
1071
+ }
1072
+ const selected = devices.filter((device) => device.providers.includes(provider)).sort((left, right) => scoreDevice(right, input, sourceDeviceId) - scoreDevice(left, input, sourceDeviceId))[0];
1073
+ if (selected === undefined) {
1074
+ return yield* new RequestError({
1075
+ status: 409,
1076
+ message: `No Cohall device advertises the ${provider} provider`
1077
+ });
1078
+ }
1079
+ return selected.id;
1080
+ }), runRelay = async () => {
1081
+ const configuration = await Effect10.runPromise(loadEnvironmentConfiguration);
1082
+ process.umask(63);
1083
+ const runtime = ManagedRuntime.make(exports_store.layer(configuration.databasePath));
1084
+ const run2 = (effect) => runtime.runPromise(effect);
1085
+ await run2(Effect10.gen(function* () {
1086
+ const store = yield* exports_store.Service;
1087
+ yield* store.recover();
1088
+ }));
1089
+ if (platform5() !== "win32" && await access4(configuration.databasePath).then(() => true).catch(() => false)) {
1090
+ await chmod3(configuration.databasePath, 384);
1091
+ }
1092
+ const hub = new Hub;
1093
+ let connections = 0;
1094
+ const tokenDigest = (token) => createHash2("sha256").update(token).digest();
1095
+ const ownerToken = (token) => timingSafeEqual(tokenDigest(token), tokenDigest(configuration.token));
1096
+ const authenticate = async (token, role) => {
1097
+ if (ownerToken(token)) {
1098
+ return "owner";
1099
+ }
1100
+ return run2(Effect10.gen(function* () {
1101
+ const store = yield* exports_store.Service;
1102
+ return yield* store.authenticateSession(token, role);
1103
+ })).catch(() => {
1104
+ return;
1105
+ });
1106
+ };
1107
+ const principalFor = async (request) => {
1108
+ const authorization = request.headers.get("authorization");
1109
+ if (authorization?.startsWith("Bearer ") !== true) {
1110
+ return;
1111
+ }
1112
+ return authenticate(authorization.slice("Bearer ".length), "client");
1113
+ };
1114
+ const dispatch = async (task) => {
1115
+ const assigned = await run2(Effect10.gen(function* () {
1116
+ const store = yield* exports_store.Service;
1117
+ return yield* store.assignTask(task.id);
1118
+ }));
1119
+ if (assigned.status !== "assigned") {
1120
+ return assigned;
1121
+ }
1122
+ if (hub.sendToDevice(assigned.targetDeviceId, SocketEvent.make({ _tag: "TaskAssigned", task: assigned }))) {
1123
+ return assigned;
1124
+ }
1125
+ return run2(Effect10.gen(function* () {
1126
+ const store = yield* exports_store.Service;
1127
+ return yield* store.rollbackAssignment(task.id);
1128
+ }));
1129
+ };
1130
+ const dispatchPending = async (deviceId) => {
1131
+ const tasks = await run2(Effect10.gen(function* () {
1132
+ const store = yield* exports_store.Service;
1133
+ return yield* store.pendingTasksFor(deviceId);
1134
+ }));
1135
+ for (const task of tasks) {
1136
+ if (task.status === "cancelling") {
1137
+ hub.sendToDevice(deviceId, SocketEvent.make({ _tag: "CancelTask", taskId: task.id }));
1138
+ } else {
1139
+ await dispatch(task);
1140
+ }
1141
+ }
1142
+ };
1143
+ const sendError = (socket, code, message) => {
1144
+ socket.send(JSON.stringify(SocketEvent.make({
1145
+ _tag: "Error",
1146
+ code: code.slice(0, 64),
1147
+ message: message.slice(0, 2048)
1148
+ })));
1149
+ };
1150
+ const handleSocketEvent = async (socket, input) => {
1151
+ const event = await Effect10.runPromise(Effect10.try({
1152
+ try: () => JSON.parse(socketText(input)),
1153
+ catch: () => new RequestError({ status: 400, message: "Invalid socket JSON" })
1154
+ }).pipe(Effect10.flatMap(decodeSocketEvent))).catch((cause) => {
1155
+ sendError(socket, "invalid_event", cause instanceof Error ? cause.message : String(cause));
1156
+ return;
1157
+ });
1158
+ if (event === undefined) {
1159
+ return;
1160
+ }
1161
+ if (!hub.isAuthorized(socket)) {
1162
+ if (event._tag !== "Authenticate") {
1163
+ socket.close(4003, "Authentication required");
1164
+ return;
1165
+ }
1166
+ const principal = await authenticate(event.token, "device");
1167
+ if (principal === undefined) {
1168
+ socket.close(4003, "Authentication failed");
1169
+ return;
1170
+ }
1171
+ hub.attach(socket, {
1172
+ ...principal === "owner" ? {} : { sessionId: principal.id },
1173
+ ...principal === "owner" || principal.deviceId === undefined ? {} : { boundDeviceId: principal.deviceId },
1174
+ ...principal === "owner" ? {} : { expiresAt: principal.expiresAt }
1175
+ });
1176
+ socket.send(JSON.stringify(SocketEvent.make({ _tag: "Connected", serverVersion: version, connectedAt: now() })));
1177
+ return;
1178
+ }
1179
+ if (event._tag === "Authenticate") {
1180
+ return;
1181
+ }
1182
+ if (event._tag === "DeviceHello") {
1183
+ const bound = hub.boundDeviceId(socket);
1184
+ if (bound !== undefined && bound !== event.device.id) {
1185
+ socket.close(4003, "Device identity does not match its session");
1186
+ return;
1187
+ }
1188
+ const device = Device.make({
1189
+ ...event.device,
1190
+ status: "online",
1191
+ connectedAt: now(),
1192
+ lastSeenAt: now()
1193
+ });
1194
+ if (!hub.registerDevice(device.id, socket)) {
1195
+ socket.close(4009, "Device is already connected");
1196
+ return;
1197
+ }
1198
+ await run2(Effect10.gen(function* () {
1199
+ const store = yield* exports_store.Service;
1200
+ yield* store.upsertDevice(device);
1201
+ }));
1202
+ await dispatchPending(device.id);
1203
+ return;
1204
+ }
1205
+ const deviceId = hub.deviceId(socket);
1206
+ if (deviceId === undefined) {
1207
+ sendError(socket, "device_hello_required", "Send DeviceHello before device events");
1208
+ return;
1209
+ }
1210
+ const settle = (taskId) => {
1211
+ socket.send(JSON.stringify(SocketEvent.make({ _tag: "TaskSettled", taskId })));
1212
+ };
1213
+ const processed = await run2(Effect10.gen(function* () {
1214
+ const store = yield* exports_store.Service;
1215
+ switch (event._tag) {
1216
+ case "DeviceHeartbeat":
1217
+ if (event.deviceId === deviceId) {
1218
+ yield* store.heartbeat(deviceId, event.status === "busy" ? "busy" : "online");
1219
+ }
1220
+ return;
1221
+ case "TaskAccepted":
1222
+ yield* store.acceptTask(event.taskId, deviceId);
1223
+ return;
1224
+ case "TaskFinished":
1225
+ yield* store.finishTask(event.taskId, deviceId, event.result, event.providerSessionId);
1226
+ return;
1227
+ case "TaskFailed":
1228
+ yield* store.failTask(event.taskId, deviceId, event.error);
1229
+ return;
1230
+ case "TaskCancelled":
1231
+ yield* store.acknowledgeCancellation(event.taskId, deviceId);
1232
+ return;
1233
+ case "Connected":
1234
+ case "TaskAssigned":
1235
+ case "TaskSettled":
1236
+ case "CancelTask":
1237
+ case "Error":
1238
+ return;
1239
+ }
1240
+ })).then(() => true).catch((cause) => {
1241
+ sendError(socket, "event_failed", cause instanceof Error ? cause.message : String(cause));
1242
+ return false;
1243
+ });
1244
+ if (processed && (event._tag === "TaskFinished" || event._tag === "TaskFailed" || event._tag === "TaskCancelled")) {
1245
+ settle(event.taskId);
1246
+ await dispatchPending(deviceId);
1247
+ }
1248
+ };
1249
+ const api = async (request, url) => {
1250
+ if (url.pathname === "/api/health" && request.method === "GET") {
1251
+ return json({ ok: true, version });
1252
+ }
1253
+ if (url.pathname === "/api/auth/pair" && request.method === "POST") {
1254
+ return run2(Effect10.gen(function* () {
1255
+ const store = yield* exports_store.Service;
1256
+ const input = yield* body(request, decodeExchangePairingInput);
1257
+ return yield* store.exchangePairing(input.token);
1258
+ })).then((value) => json(value, 201)).catch(() => json({ error: "Pairing credential is invalid, expired, or already used" }, 401));
1259
+ }
1260
+ const principal = await principalFor(request);
1261
+ if (principal === undefined) {
1262
+ return json({ error: "Unauthorized" }, 401);
1263
+ }
1264
+ const ownerOnly = url.pathname.startsWith("/api/auth/");
1265
+ if (ownerOnly && principal !== "owner") {
1266
+ return json({ error: "Owner token required" }, 403);
1267
+ }
1268
+ const sourceDeviceId = principal === "owner" ? undefined : principal.deviceId;
1269
+ const effect = Effect10.gen(function* () {
1270
+ const store = yield* exports_store.Service;
1271
+ if (url.pathname === "/api/auth/pairings" && request.method === "POST") {
1272
+ const input = yield* body(request, decodeCreatePairingInput);
1273
+ return json(yield* store.createPairing(input), 201);
1274
+ }
1275
+ if (url.pathname === "/api/auth/sessions" && request.method === "GET") {
1276
+ return json(yield* store.listAuthSessions());
1277
+ }
1278
+ const revoke = url.pathname.match(/^\/api\/auth\/sessions\/([^/]+)\/revoke$/);
1279
+ if (request.method === "POST" && revoke?.[1] !== undefined) {
1280
+ const id = yield* pathId(AuthSessionId, revoke[1]);
1281
+ const session = yield* store.revokeAuthSession(id);
1282
+ hub.closeSession(id);
1283
+ return json(session);
1284
+ }
1285
+ if (url.pathname === "/api/devices" && request.method === "GET") {
1286
+ return json(yield* store.listDevices());
1287
+ }
1288
+ if (url.pathname === "/api/tasks" && request.method === "POST") {
1289
+ const input = yield* body(request, decodeCreateTaskInput);
1290
+ const target = yield* chooseDevice(input, sourceDeviceId);
1291
+ const providerSessionId = input.threadId === undefined ? undefined : yield* store.sessionFor(input.threadId, target, input.provider ?? "codex");
1292
+ const task2 = yield* store.createDelegation(input, target, sourceDeviceId, providerSessionId);
1293
+ return json(yield* Effect10.tryPromise(() => dispatch(task2)), 201);
1294
+ }
1295
+ const task = url.pathname.match(/^\/api\/tasks\/([^/]+)$/);
1296
+ if (request.method === "GET" && task?.[1] !== undefined) {
1297
+ return json(yield* store.getTask(yield* pathId(TaskId, task[1])));
1298
+ }
1299
+ const cancel2 = url.pathname.match(/^\/api\/tasks\/([^/]+)\/cancel$/);
1300
+ if (request.method === "POST" && cancel2?.[1] !== undefined) {
1301
+ const id = yield* pathId(TaskId, cancel2[1]);
1302
+ const updated = yield* store.requestCancellation(id);
1303
+ if (updated.status === "cancelling") {
1304
+ hub.sendToDevice(updated.targetDeviceId, SocketEvent.make({ _tag: "CancelTask", taskId: id }));
1305
+ }
1306
+ return json(updated);
1307
+ }
1308
+ const thread = url.pathname.match(/^\/api\/threads\/([^/]+)$/);
1309
+ if (request.method === "GET" && thread?.[1] !== undefined) {
1310
+ return json(yield* store.threadContext(yield* pathId(ThreadId, thread[1])));
1311
+ }
1312
+ return yield* new RequestError({ status: 404, message: "Route not found" });
1313
+ }).pipe(Effect10.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({
1314
+ status: cause._tag !== "RelayStore.PersistenceError" ? 500 : cause.message.startsWith("Unknown") ? 404 : cause.message.includes("outstanding task limit") ? 429 : 500,
1315
+ message: cause.message
1316
+ })));
1317
+ 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));
1318
+ };
1319
+ const websocketServer = new WebSocketServer({
1320
+ noServer: true,
1321
+ maxPayload: 256 * 1024,
1322
+ perMessageDeflate: false
1323
+ });
1324
+ websocketServer.on("connection", (rawSocket) => {
1325
+ const socket = rawSocket;
1326
+ socket.data = {
1327
+ processing: Promise.resolve(),
1328
+ authDeadline: undefined,
1329
+ preAuthFrameReceived: false,
1330
+ queuedMessages: 0,
1331
+ closed: false
1332
+ };
1333
+ connections += 1;
1334
+ socket.data.authDeadline = setTimeout(() => {
1335
+ if (!hub.isAuthorized(socket)) {
1336
+ socket.close(4003, "Authentication timeout");
1337
+ }
1338
+ }, 5000);
1339
+ let alive = true;
1340
+ socket.on("pong", () => {
1341
+ alive = true;
1342
+ });
1343
+ const heartbeat = setInterval(() => {
1344
+ if (!alive) {
1345
+ socket.terminate();
1346
+ return;
1347
+ }
1348
+ alive = false;
1349
+ socket.ping();
1350
+ }, 30000);
1351
+ heartbeat.unref();
1352
+ socket.on("message", (message) => {
1353
+ if (!hub.isAuthorized(socket)) {
1354
+ const size = Buffer.byteLength(socketText(message));
1355
+ if (socket.data.preAuthFrameReceived || size > 1024) {
1356
+ socket.close(4003, "Only one authentication frame is allowed");
1357
+ return;
1358
+ }
1359
+ socket.data.preAuthFrameReceived = true;
1360
+ socket.data.processing = handleSocketEvent(socket, message).catch((cause) => {
1361
+ sendError(socket, "event_failed", cause instanceof Error ? cause.message : String(cause));
1362
+ });
1363
+ return;
1364
+ }
1365
+ if (socket.data.queuedMessages >= 8) {
1366
+ socket.close(4008, "Message queue limit reached");
1367
+ return;
1368
+ }
1369
+ socket.data.queuedMessages += 1;
1370
+ socket.data.processing = socket.data.processing.then(() => socket.data.closed ? undefined : handleSocketEvent(socket, message)).catch((cause) => {
1371
+ sendError(socket, "event_failed", cause instanceof Error ? cause.message : String(cause));
1372
+ }).finally(() => {
1373
+ socket.data.queuedMessages = Math.max(0, socket.data.queuedMessages - 1);
1374
+ });
1375
+ });
1376
+ socket.once("close", () => {
1377
+ clearInterval(heartbeat);
1378
+ socket.data.closed = true;
1379
+ connections = Math.max(0, connections - 1);
1380
+ const deviceId = hub.detach(socket);
1381
+ if (deviceId === undefined) {
1382
+ return;
1383
+ }
1384
+ run2(Effect10.gen(function* () {
1385
+ const store = yield* exports_store.Service;
1386
+ yield* store.requeueTasksFor(deviceId);
1387
+ yield* store.markDeviceOffline(deviceId);
1388
+ })).catch((cause) => console.error(cause));
1389
+ });
1390
+ });
1391
+ const server = createServer((incoming, outgoing) => {
1392
+ webRequest(incoming).then((request) => {
1393
+ const url = new URL(request.url);
1394
+ return url.pathname.startsWith("/api/") ? api(request, url) : json({ name: "Cohall relay", version, status: "running" });
1395
+ }).then((response) => sendResponse(outgoing, response)).catch((cause) => sendResponse(outgoing, json({ error: cause instanceof Error ? cause.message : "Invalid request" }, cause instanceof Error && cause.message.includes("256 KiB") ? 413 : 400)));
1396
+ });
1397
+ server.on("upgrade", (request, socket, head) => {
1398
+ const url = new URL(request.url ?? "/", "http://cohall.local");
1399
+ if (url.pathname !== "/ws/device") {
1400
+ rejectUpgrade(socket, 404, "Not Found");
1401
+ return;
1402
+ }
1403
+ if (connections >= 1024) {
1404
+ rejectUpgrade(socket, 503, "Service Unavailable");
1405
+ return;
1406
+ }
1407
+ websocketServer.handleUpgrade(request, socket, head, (websocket) => {
1408
+ websocketServer.emit("connection", websocket, request);
1409
+ });
1410
+ });
1411
+ await new Promise((resolve3, reject) => {
1412
+ server.once("error", reject);
1413
+ server.listen(configuration.port, configuration.host, () => {
1414
+ server.off("error", reject);
1415
+ resolve3();
1416
+ });
1417
+ });
1418
+ console.log(`Cohall relay listening on http://${configuration.host}:${configuration.port}`);
1419
+ let stopping = false;
1420
+ const shutdown = async () => {
1421
+ if (stopping) {
1422
+ return;
1423
+ }
1424
+ stopping = true;
1425
+ for (const socket of websocketServer.clients) {
1426
+ socket.close(1001, "Relay shutting down");
1427
+ }
1428
+ await new Promise((resolve3) => server.close(() => resolve3()));
1429
+ websocketServer.close();
1430
+ await runtime.dispose();
1431
+ };
1432
+ process.once("SIGINT", () => void shutdown());
1433
+ process.once("SIGTERM", () => void shutdown());
1434
+ };
1435
+ var init_main = __esm(async () => {
1436
+ init_src();
1437
+ init_config();
1438
+ init_hub();
1439
+ init_store();
1440
+ RequestError = class RequestError extends Schema10.TaggedErrorClass()("Relay.RequestError", {
1441
+ status: Schema10.Int,
1442
+ message: Schema10.String
1443
+ }) {
1444
+ };
1445
+ if (false) {}
1446
+ });
1447
+
1448
+ // apps/device/src/main.ts
1449
+ init_src();
1450
+ import { Effect as Effect11 } from "effect";
1451
+
1452
+ // packages/client/src/index.ts
1453
+ init_src();
1454
+ var exports_src = {};
1455
+ __export(exports_src, {
1456
+ make: () => make,
1457
+ layer: () => layer,
1458
+ exchangePairing: () => exchangePairing,
1459
+ Service: () => Service,
1460
+ RelayRequestError: () => RelayRequestError,
1461
+ RelayDecodeError: () => RelayDecodeError,
1462
+ RelayClient: () => exports_src
1463
+ });
1464
+ import { Context, Effect, Layer, Schema as Schema2 } from "effect";
1465
+
1466
+ class RelayRequestError extends Schema2.TaggedErrorClass()("RelayClient.RequestError", {
1467
+ operation: Schema2.String,
1468
+ message: Schema2.String,
1469
+ status: Schema2.optionalKey(Schema2.Number)
1470
+ }) {
1471
+ }
1472
+
1473
+ class RelayDecodeError extends Schema2.TaggedErrorClass()("RelayClient.DecodeError", { operation: Schema2.String, message: Schema2.String }) {
1474
+ }
1475
+
1476
+ class Service extends Context.Service()("@cohall/RelayClient") {
1477
+ }
1478
+ var normalizeBaseUrl = (url) => url.replace(/\/+$/, "");
1479
+ var jsonBody = async (response, operation) => {
1480
+ if (response.body === null) {
1481
+ throw new RelayDecodeError({ operation, message: "Relay returned an empty response" });
1482
+ }
1483
+ const reader = response.body.getReader();
1484
+ const chunks = [];
1485
+ let size = 0;
1486
+ while (true) {
1487
+ const chunk = await reader.read();
1488
+ if (chunk.done) {
1489
+ break;
1490
+ }
1491
+ size += chunk.value.byteLength;
1492
+ if (size > 2 * 1024 * 1024) {
1493
+ await reader.cancel();
1494
+ throw new RelayDecodeError({ operation, message: "Relay response exceeded 2 MiB" });
1495
+ }
1496
+ chunks.push(chunk.value);
1497
+ }
1498
+ const bytes = new Uint8Array(size);
1499
+ let offset = 0;
1500
+ for (const chunk of chunks) {
1501
+ bytes.set(chunk, offset);
1502
+ offset += chunk.byteLength;
1503
+ }
1504
+ const text = new TextDecoder().decode(bytes);
1505
+ try {
1506
+ return JSON.parse(text);
1507
+ } catch (cause) {
1508
+ throw new RelayDecodeError({
1509
+ operation,
1510
+ message: cause instanceof Error ? cause.message : String(cause)
1511
+ });
1512
+ }
1513
+ };
1514
+ var responseMessage = async (response, operation) => {
1515
+ const json = await jsonBody(response, operation).catch(() => {
1516
+ return;
1517
+ });
1518
+ const decoded = Schema2.decodeUnknownOption(ErrorResponse)(json);
1519
+ return decoded._tag === "Some" ? decoded.value.error : `${response.status} ${response.statusText}`.trim();
1520
+ };
1521
+ var make = (options) => {
1522
+ const baseUrl = normalizeBaseUrl(options.baseUrl);
1523
+ const request = (operation, path, schema, init) => Effect.gen(function* () {
1524
+ const response = yield* Effect.tryPromise({
1525
+ try: (signal) => fetch(`${baseUrl}${path}`, {
1526
+ ...init,
1527
+ signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
1528
+ headers: {
1529
+ authorization: `Bearer ${options.token}`,
1530
+ "content-type": "application/json",
1531
+ ...init?.headers
1532
+ }
1533
+ }),
1534
+ catch: (cause) => new RelayRequestError({
1535
+ operation,
1536
+ message: cause instanceof Error ? cause.message : String(cause)
1537
+ })
1538
+ });
1539
+ if (!response.ok) {
1540
+ return yield* Effect.fail(new RelayRequestError({
1541
+ operation,
1542
+ message: yield* Effect.promise(() => responseMessage(response, operation)),
1543
+ status: response.status
1544
+ }));
1545
+ }
1546
+ const json = yield* Effect.tryPromise({
1547
+ try: () => jsonBody(response, operation),
1548
+ catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
1549
+ });
1550
+ return yield* Schema2.decodeUnknownEffect(schema)(json).pipe(Effect.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
1551
+ });
1552
+ return Service.of({
1553
+ devices: () => request("RelayClient.devices", "/api/devices", Schema2.Array(Device)),
1554
+ createTask: (input) => request("RelayClient.createTask", "/api/tasks", Task, {
1555
+ method: "POST",
1556
+ body: JSON.stringify(input)
1557
+ }),
1558
+ getTask: (taskId) => request("RelayClient.getTask", `/api/tasks/${encodeURIComponent(taskId)}`, Task),
1559
+ cancelTask: (taskId) => request("RelayClient.cancelTask", `/api/tasks/${encodeURIComponent(taskId)}/cancel`, Task, {
1560
+ method: "POST"
1561
+ }),
1562
+ threadContext: (threadId) => request("RelayClient.threadContext", `/api/threads/${encodeURIComponent(threadId)}`, ThreadContext),
1563
+ createPairing: (input) => request("RelayClient.createPairing", "/api/auth/pairings", PairingCredential, {
1564
+ method: "POST",
1565
+ body: JSON.stringify(input)
1566
+ }),
1567
+ authSessions: () => request("RelayClient.authSessions", "/api/auth/sessions", Schema2.Array(AuthSession)),
1568
+ revokeAuthSession: (sessionId) => request("RelayClient.revokeAuthSession", `/api/auth/sessions/${encodeURIComponent(sessionId)}/revoke`, AuthSession, { method: "POST" })
1569
+ });
1570
+ };
1571
+ var exchangePairing = (baseUrl, input) => Effect.gen(function* () {
1572
+ const operation = "RelayClient.exchangePairing";
1573
+ const response = yield* Effect.tryPromise({
1574
+ try: (signal) => fetch(`${normalizeBaseUrl(baseUrl)}/api/auth/pair`, {
1575
+ method: "POST",
1576
+ signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
1577
+ headers: { "content-type": "application/json" },
1578
+ body: JSON.stringify(input)
1579
+ }),
1580
+ catch: (cause) => new RelayRequestError({
1581
+ operation,
1582
+ message: cause instanceof Error ? cause.message : String(cause)
1583
+ })
1584
+ });
1585
+ if (!response.ok) {
1586
+ return yield* Effect.fail(new RelayRequestError({
1587
+ operation,
1588
+ message: yield* Effect.promise(() => responseMessage(response, operation)),
1589
+ status: response.status
1590
+ }));
1591
+ }
1592
+ const json = yield* Effect.tryPromise({
1593
+ try: () => jsonBody(response, operation),
1594
+ catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
1595
+ });
1596
+ return yield* Schema2.decodeUnknownEffect(PairingResult)(json).pipe(Effect.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
1597
+ });
1598
+ var layer = (options) => Layer.succeed(Service, make(options));
1599
+
1600
+ // apps/device/src/cli.ts
1601
+ init_src();
1602
+
1603
+ // packages/providers/src/index.ts
1604
+ init_src();
1605
+ import { Effect as Effect2, Schema as Schema3 } from "effect";
1606
+ import { accessSync, constants } from "node:fs";
1607
+ import { platform } from "node:os";
1608
+ import { delimiter, extname, isAbsolute, join } from "node:path";
1609
+ import { spawn } from "node:child_process";
1610
+
1611
+ class ProviderUnavailableError extends Schema3.TaggedErrorClass()("CohallProvider.Unavailable", { provider: Provider, message: Schema3.String }) {
1612
+ }
1613
+
1614
+ class ProviderRunError extends Schema3.TaggedErrorClass()("CohallProvider.RunError", {
1615
+ provider: Provider,
1616
+ message: Schema3.String,
1617
+ exitCode: Schema3.optionalKey(Schema3.Number)
1618
+ }) {
1619
+ }
1620
+ var executables = {
1621
+ codex: "codex",
1622
+ "claude-code": "claude",
1623
+ opencode: "opencode"
1624
+ };
1625
+ var providerEnvironment = () => Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("COHALL_")));
1626
+ var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined;
1627
+ var text = (value) => typeof value === "string" ? value : undefined;
1628
+ var executableCandidates = (command) => {
1629
+ if (platform() !== "win32" || extname(command).length > 0) {
1630
+ return [command];
1631
+ }
1632
+ return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((extension) => extension.length > 0).map((extension) => `${command}${extension.toLowerCase()}`);
1633
+ };
1634
+ var findExecutable = (command) => {
1635
+ const paths = isAbsolute(command) || command.includes("/") || command.includes("\\") ? [""] : (process.env.PATH ?? "").split(delimiter);
1636
+ for (const directory of paths) {
1637
+ for (const candidate of executableCandidates(command)) {
1638
+ const path = directory.length === 0 ? candidate : join(directory, candidate);
1639
+ try {
1640
+ accessSync(path, platform() === "win32" ? constants.F_OK : constants.X_OK);
1641
+ return path;
1642
+ } catch {
1643
+ continue;
1644
+ }
1645
+ }
1646
+ }
1647
+ return;
1648
+ };
1649
+ var available = (provider) => findExecutable(executables[provider]) !== undefined;
1650
+ var availableProviders = () => Provider.literals.filter(available);
1651
+ var boundedText = (stream, limit, onOverflow) => new Promise((resolve, reject) => {
1652
+ const chunks = [];
1653
+ let size = 0;
1654
+ const cleanup = () => {
1655
+ stream.off("data", onData);
1656
+ stream.off("end", onEnd);
1657
+ stream.off("error", onError);
1658
+ };
1659
+ const onData = (chunk) => {
1660
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
1661
+ size += bytes.byteLength;
1662
+ if (size <= limit) {
1663
+ chunks.push(bytes);
1664
+ return;
1665
+ }
1666
+ cleanup();
1667
+ onOverflow();
1668
+ reject(new Error(`Provider output exceeded ${Math.floor(limit / 1024)} KiB`));
1669
+ };
1670
+ const onEnd = () => {
1671
+ cleanup();
1672
+ resolve(Buffer.concat(chunks, size).toString("utf8"));
1673
+ };
1674
+ const onError = (cause) => {
1675
+ cleanup();
1676
+ reject(cause);
1677
+ };
1678
+ stream.on("data", onData);
1679
+ stream.once("end", onEnd);
1680
+ stream.once("error", onError);
1681
+ });
1682
+ var codexCommand = (options) => {
1683
+ const common = [
1684
+ "--json",
1685
+ "-c",
1686
+ 'approval_policy="never"',
1687
+ ...options.model === undefined ? [] : ["--model", options.model],
1688
+ ...options.sandbox === undefined ? [] : ["-c", `sandbox_mode=${JSON.stringify(options.sandbox)}`]
1689
+ ];
1690
+ return options.sessionId === undefined ? ["codex", "exec", ...common, "-"] : ["codex", "exec", "resume", ...common, options.sessionId, "-"];
1691
+ };
1692
+ var claudeCommand = (options) => [
1693
+ "claude",
1694
+ "-p",
1695
+ "--output-format",
1696
+ "json",
1697
+ ...options.model === undefined ? [] : ["--model", options.model],
1698
+ ...options.sessionId === undefined ? [] : ["--resume", options.sessionId]
1699
+ ];
1700
+ var opencodeCommand = (options) => [
1701
+ "opencode",
1702
+ "run",
1703
+ "--format",
1704
+ "json",
1705
+ ...options.model === undefined ? [] : ["--model", options.model],
1706
+ ...options.sessionId === undefined ? [] : ["--session", options.sessionId]
1707
+ ];
1708
+ var command = (options) => {
1709
+ switch (options.provider) {
1710
+ case "codex":
1711
+ return codexCommand(options);
1712
+ case "claude-code":
1713
+ return claudeCommand(options);
1714
+ case "opencode":
1715
+ return opencodeCommand(options);
1716
+ }
1717
+ };
1718
+ var parseCodex = (stdout, existingSession) => {
1719
+ let result = "";
1720
+ let sessionId = existingSession;
1721
+ for (const line of stdout.split(`
1722
+ `)) {
1723
+ if (line.trim().length === 0) {
1724
+ continue;
1725
+ }
1726
+ const event = record(JSON.parse(line));
1727
+ if (event === undefined) {
1728
+ continue;
1729
+ }
1730
+ if (text(event.type) === "thread.started") {
1731
+ sessionId = text(event.thread_id) ?? sessionId;
1732
+ }
1733
+ if (text(event.type) !== "item.completed") {
1734
+ continue;
1735
+ }
1736
+ const item = record(event.item);
1737
+ if (text(item?.type) === "agent_message") {
1738
+ result = text(item?.text) ?? text(item?.content) ?? result;
1739
+ }
1740
+ }
1741
+ return {
1742
+ result: result || "Codex completed the task without a text response.",
1743
+ ...sessionId === undefined ? {} : { sessionId }
1744
+ };
1745
+ };
1746
+ var parseClaude = (stdout, existingSession) => {
1747
+ const value = record(JSON.parse(stdout));
1748
+ const result = text(value?.result) ?? text(value?.content);
1749
+ if (result === undefined) {
1750
+ throw new Error("Claude Code returned JSON without a result");
1751
+ }
1752
+ const sessionId = text(value?.session_id) ?? text(value?.sessionId) ?? existingSession;
1753
+ return { result, ...sessionId === undefined ? {} : { sessionId } };
1754
+ };
1755
+ var textFromPart = (value) => {
1756
+ const part = record(value);
1757
+ if (part === undefined) {
1758
+ return;
1759
+ }
1760
+ return text(part.text) ?? text(part.content) ?? text(record(part.part)?.text);
1761
+ };
1762
+ var parseOpenCode = (stdout, existingSession) => {
1763
+ let result = "";
1764
+ let sessionId = existingSession;
1765
+ for (const line of stdout.split(`
1766
+ `)) {
1767
+ if (line.trim().length === 0) {
1768
+ continue;
1769
+ }
1770
+ const event = record(JSON.parse(line));
1771
+ if (event === undefined) {
1772
+ continue;
1773
+ }
1774
+ sessionId = text(event.sessionID) ?? text(event.sessionId) ?? text(event.session_id) ?? text(record(event.info)?.sessionID) ?? sessionId;
1775
+ const content = textFromPart(event.part) ?? textFromPart(event) ?? text(event.result);
1776
+ if (content !== undefined && content.length > 0) {
1777
+ result = content;
1778
+ }
1779
+ }
1780
+ if (result.length === 0) {
1781
+ throw new Error("OpenCode completed without a text response");
1782
+ }
1783
+ return { result, ...sessionId === undefined ? {} : { sessionId } };
1784
+ };
1785
+ var parse = (options, stdout) => {
1786
+ switch (options.provider) {
1787
+ case "codex":
1788
+ return parseCodex(stdout, options.sessionId);
1789
+ case "claude-code":
1790
+ return parseClaude(stdout, options.sessionId);
1791
+ case "opencode":
1792
+ return parseOpenCode(stdout, options.sessionId);
1793
+ }
1794
+ };
1795
+ var byteLength = (value) => new TextEncoder().encode(value).byteLength;
1796
+ var run = (options) => Effect2.tryPromise({
1797
+ try: async (signal) => {
1798
+ if (!available(options.provider)) {
1799
+ throw new ProviderUnavailableError({
1800
+ provider: options.provider,
1801
+ message: `${executables[options.provider]} is not available on this device`
1802
+ });
1803
+ }
1804
+ const [executable, ...arguments_] = command(options);
1805
+ if (executable === undefined) {
1806
+ throw new ProviderRunError({
1807
+ provider: options.provider,
1808
+ message: `No command is configured for ${options.provider}`
1809
+ });
1810
+ }
1811
+ const child = spawn(executable, arguments_, {
1812
+ cwd: options.cwd,
1813
+ env: {
1814
+ ...providerEnvironment(),
1815
+ COHALL_PROVIDER: options.provider,
1816
+ COHALL_THREAD_ID: options.threadId
1817
+ },
1818
+ stdio: ["pipe", "pipe", "pipe"]
1819
+ });
1820
+ const exited = new Promise((resolve, reject) => {
1821
+ child.once("error", reject);
1822
+ child.once("exit", (code) => resolve(code ?? 1));
1823
+ });
1824
+ let killTimer;
1825
+ const terminate = () => {
1826
+ if (child.exitCode !== null) {
1827
+ return;
1828
+ }
1829
+ child.kill("SIGTERM");
1830
+ killTimer ??= setTimeout(() => {
1831
+ if (child.exitCode === null) {
1832
+ child.kill("SIGKILL");
1833
+ }
1834
+ }, 5000);
1835
+ killTimer.unref();
1836
+ };
1837
+ signal.addEventListener("abort", terminate, { once: true });
1838
+ child.stdin.end(options.prompt);
1839
+ const [stdout, stderr, exitCode] = await Promise.all([
1840
+ boundedText(child.stdout, 1024 * 1024, terminate),
1841
+ boundedText(child.stderr, 64 * 1024, terminate),
1842
+ exited
1843
+ ]);
1844
+ signal.removeEventListener("abort", terminate);
1845
+ if (killTimer !== undefined && child.exitCode !== null) {
1846
+ clearTimeout(killTimer);
1847
+ }
1848
+ if (exitCode !== 0) {
1849
+ throw new ProviderRunError({
1850
+ provider: options.provider,
1851
+ message: (stderr.trim() || `${executables[options.provider]} exited with status ${exitCode}`).slice(0, 16384),
1852
+ exitCode
1853
+ });
1854
+ }
1855
+ const result = parse(options, stdout);
1856
+ if (byteLength(result.result) > 131072) {
1857
+ throw new ProviderRunError({
1858
+ provider: options.provider,
1859
+ message: "Provider result exceeded 128 KiB"
1860
+ });
1861
+ }
1862
+ if (result.sessionId !== undefined && byteLength(result.sessionId) > 4096) {
1863
+ throw new ProviderRunError({
1864
+ provider: options.provider,
1865
+ message: "Provider session ID exceeded 4 KiB"
1866
+ });
1867
+ }
1868
+ return result;
1869
+ },
1870
+ catch: (cause) => {
1871
+ if (cause instanceof ProviderUnavailableError || cause instanceof ProviderRunError) {
1872
+ return cause;
1873
+ }
1874
+ return new ProviderRunError({
1875
+ provider: options.provider,
1876
+ message: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
1877
+ });
1878
+ }
1879
+ });
1880
+
1881
+ // apps/device/src/cli.ts
1882
+ import { Effect as Effect5, Schema as Schema6 } from "effect";
1883
+ import { access as access2, mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
1884
+ import { homedir as homedir2 } from "node:os";
1885
+ import { dirname as dirname2, join as join3 } from "node:path";
1886
+
1887
+ // skills/cohall/SKILL.md
1888
+ var SKILL_default = `---
1889
+ name: cohall
1890
+ description: Delegate work to an agent on another user-owned device through the Cohall CLI. Use when a task needs machine-local state or capabilities such as a signed-in browser, Xcode, a simulator, Docker, deployment access, a repository checkout, or tools unavailable on the current device.
1891
+ ---
1892
+
1893
+ # Cohall
1894
+
1895
+ Cohall sends a focused task to an agent running on another device. The target
1896
+ keeps its local files, credentials, browser state, provider login, skills, and
1897
+ permissions. The relay carries prompts, final results, and task state; it is not
1898
+ a lead agent.
1899
+
1900
+ ## Delegate work
1901
+
1902
+ 1. List devices unless the target is already explicit:
1903
+
1904
+ \`\`\`bash
1905
+ npx -y @akshar5/cohall devices
1906
+ \`\`\`
1907
+
1908
+ 2. Choose a target whose provider, workspaces, platform, and capabilities fit
1909
+ the task. Stay local when another device offers no material advantage.
1910
+
1911
+ 3. Delegate one concrete outcome. Send only the context the target needs:
1912
+
1913
+ \`\`\`bash
1914
+ npx -y @akshar5/cohall delegate \\
1915
+ --target @macbook \\
1916
+ --provider codex \\
1917
+ --workspace /Users/me/dev/project \\
1918
+ --prompt 'Inspect the authenticated dashboard and identify why deployment 184 failed.' \\
1919
+ --context 'Focus on events after 15:00 UTC. Return evidence and source URLs.'
1920
+ \`\`\`
1921
+
1922
+ Providers are \`codex\`, \`claude-code\`, and \`opencode\`. Omit \`--provider\` to
1923
+ use Codex. Omit \`--target\` only when Cohall may choose a matching device.
1924
+
1925
+ 4. The command waits by default and returns JSON. Treat work as successful only
1926
+ when \`status\` is \`completed\`; use \`result\` in the current task. Report a
1927
+ \`failed\`, \`cancelled\`, or \`cancelling\` state accurately.
1928
+
1929
+ 5. Reuse \`thread_id\` for related follow-ups so the target provider can resume
1930
+ its local session:
1931
+
1932
+ \`\`\`bash
1933
+ npx -y @akshar5/cohall delegate \\
1934
+ --thread 11111111-1111-4111-8111-111111111111 \\
1935
+ --target @macbook \\
1936
+ --prompt 'Check whether yesterday’s deployment failed for the same reason.'
1937
+ \`\`\`
1938
+
1939
+ When an agent running as a Cohall task delegates again, \`COHALL_THREAD_ID\`
1940
+ automatically carries the current thread. Pass \`--thread\` explicitly from an
1941
+ ordinary shell or a separate agent turn.
1942
+
1943
+ ## Context and safety
1944
+
1945
+ - Include the goal, constraints, relevant prior findings, and expected evidence.
1946
+ - Do not paste a full private transcript when a short brief is sufficient.
1947
+ - Never send provider credentials, Cohall tokens, cookies, or browser-profile data.
1948
+ - Request a path only when the target advertises a matching workspace root.
1949
+ - Use the same thread for clarification instead of creating duplicate tasks.
1950
+ - Do not submit the same work through both CLI and MCP.
1951
+ - Respect user confirmation requirements for consequential actions on the target.
1952
+
1953
+ ## Asynchronous work
1954
+
1955
+ Queue work when the current agent can make independent progress:
1956
+
1957
+ \`\`\`bash
1958
+ npx -y @akshar5/cohall delegate --target @linux --no-wait \\
1959
+ --prompt 'Run the project test suite and report failures.'
1960
+ npx -y @akshar5/cohall status <task-id>
1961
+ npx -y @akshar5/cohall wait <task-id> --timeout 1800
1962
+ \`\`\`
1963
+
1964
+ The timeout error includes the task ID and last known status. The task continues
1965
+ unless cancelled:
1966
+
1967
+ \`\`\`bash
1968
+ npx -y @akshar5/cohall cancel <task-id>
1969
+ \`\`\`
1970
+
1971
+ Active cancellation is acknowledged by the target device; \`cancelling\` means
1972
+ the provider process has not confirmed termination yet.
1973
+
1974
+ ## Read shared context
1975
+
1976
+ \`\`\`bash
1977
+ npx -y @akshar5/cohall thread <thread-id>
1978
+ \`\`\`
1979
+
1980
+ This returns a byte-bounded recent window of prompts, final responses, and task
1981
+ states. Check \`truncated\`; older history remains on the relay when it is true.
1982
+
1983
+ ## Input forms
1984
+
1985
+ Use \`--prompt-file\` or stdin for multiline work without shell interpolation:
1986
+
1987
+ \`\`\`bash
1988
+ npx -y @akshar5/cohall delegate --target @macbook --prompt - <<'COHALL_PROMPT'
1989
+ Analyze the authenticated pages in the attached task context.
1990
+ Return the shared conclusions, disagreements, and source URLs.
1991
+ COHALL_PROMPT
1992
+ \`\`\`
1993
+
1994
+ Only one input may read stdin. Use \`--context-file\` when the prompt uses stdin.
1995
+
1996
+ ## Diagnostics
1997
+
1998
+ If a command cannot connect or no target is available, run:
1999
+
2000
+ \`\`\`bash
2001
+ npx -y @akshar5/cohall doctor
2002
+ npx -y @akshar5/cohall devices
2003
+ \`\`\`
2004
+
2005
+ Return the diagnostic failure to the user rather than claiming remote work ran.
2006
+ `;
2007
+
2008
+ // apps/device/src/config.ts
2009
+ init_src();
2010
+ import { Effect as Effect3, Schema as Schema4 } from "effect";
2011
+ import { access, chmod, mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
2012
+ import { homedir, hostname, platform as platform2 } from "node:os";
2013
+ import { dirname, join as join2, resolve } from "node:path";
2014
+ var WorkspaceList = Schema4.Array(Schema4.NonEmptyString).check(Schema4.isMaxLength(64));
2015
+ var Sandbox = Schema4.Literals(["read-only", "workspace-write", "danger-full-access"]);
2016
+ var StoredConfiguration = Schema4.Struct({
2017
+ version: Schema4.Literal(1),
2018
+ relayUrl: Schema4.NonEmptyString,
2019
+ deviceId: DeviceId,
2020
+ deviceName: Schema4.NonEmptyString,
2021
+ workspaces: WorkspaceList,
2022
+ clientToken: Schema4.optionalKey(Schema4.NonEmptyString),
2023
+ deviceToken: Schema4.optionalKey(Schema4.NonEmptyString),
2024
+ model: Schema4.optionalKey(Schema4.NonEmptyString),
2025
+ sandbox: Schema4.optionalKey(Sandbox)
2026
+ });
2027
+ var ClientConfiguration = Schema4.Struct({
2028
+ relayUrl: Schema4.NonEmptyString,
2029
+ token: Schema4.NonEmptyString,
2030
+ mcpThreadId: Schema4.optionalKey(Schema4.String)
2031
+ });
2032
+ var DeviceConfiguration = Schema4.Struct({
2033
+ relayUrl: Schema4.NonEmptyString,
2034
+ token: Schema4.NonEmptyString,
2035
+ id: DeviceId,
2036
+ name: Schema4.NonEmptyString,
2037
+ workspaces: WorkspaceList,
2038
+ model: Schema4.optionalKey(Schema4.NonEmptyString),
2039
+ sandbox: Schema4.optionalKey(Sandbox)
2040
+ });
2041
+ var defaultConfigDirectory = () => {
2042
+ if (platform2() === "win32") {
2043
+ return join2(process.env.APPDATA ?? join2(homedir(), "AppData", "Roaming"), "Cohall");
2044
+ }
2045
+ if (platform2() === "darwin") {
2046
+ return join2(homedir(), "Library", "Application Support", "Cohall");
2047
+ }
2048
+ return join2(process.env.XDG_CONFIG_HOME ?? join2(homedir(), ".config"), "cohall");
2049
+ };
2050
+ var configurationPath = () => resolve(process.env.COHALL_CONFIG ?? join2(defaultConfigDirectory(), "config.json"));
2051
+ var normalizeRelayUrl = (input) => {
2052
+ const url = new URL(input);
2053
+ if (url.protocol !== "http:" && url.protocol !== "https:") {
2054
+ throw new Error("Relay URL must use http or https");
2055
+ }
2056
+ if (url.username.length > 0 || url.password.length > 0 || url.search.length > 0 || url.hash.length > 0) {
2057
+ throw new Error("Relay URL cannot include credentials, a query, or a fragment");
2058
+ }
2059
+ if (url.pathname !== "/" && url.pathname !== "") {
2060
+ throw new Error("Relay URL cannot include a path");
2061
+ }
2062
+ url.pathname = "/";
2063
+ return url.toString().replace(/\/$/, "");
2064
+ };
2065
+ var parseWorkspaces = (workspaceList, workspaceJson) => Effect3.tryPromise({
2066
+ try: async () => {
2067
+ const values = workspaceJson === undefined ? workspaceList.split(",").map((workspace) => workspace.trim()).filter((workspace) => workspace.length > 0) : Schema4.decodeUnknownSync(WorkspaceList)(JSON.parse(workspaceJson));
2068
+ const canonical = await Promise.all(values.map((workspace) => realpath(resolve(workspace))));
2069
+ return [...new Set(canonical)];
2070
+ },
2071
+ catch: (cause) => new Error(`Workspace roots must be existing paths; JSON input must be an array of paths: ${cause instanceof Error ? cause.message : String(cause)}`)
2072
+ });
2073
+ var readStoredConfiguration = async () => {
2074
+ const path = configurationPath();
2075
+ if (!await access(path).then(() => true).catch(() => false)) {
2076
+ return;
2077
+ }
2078
+ if (platform2() !== "win32") {
2079
+ await chmod(path, 384);
2080
+ }
2081
+ try {
2082
+ return Schema4.decodeUnknownSync(StoredConfiguration)(JSON.parse(await readFile(path, "utf8")));
2083
+ } catch (cause) {
2084
+ throw new Error(`Invalid Cohall configuration at ${path}: ${String(cause)}`);
2085
+ }
2086
+ };
2087
+ var writeStoredConfiguration = async (configuration) => {
2088
+ const path = configurationPath();
2089
+ const directory = dirname(path);
2090
+ const temporary = `${path}.${process.pid}.tmp`;
2091
+ await mkdir(directory, { recursive: true, mode: 448 });
2092
+ if (platform2() !== "win32") {
2093
+ await chmod(directory, 448);
2094
+ }
2095
+ await writeFile(temporary, `${JSON.stringify(configuration, null, 2)}
2096
+ `, { mode: 384 });
2097
+ await rename(temporary, path);
2098
+ if (platform2() !== "win32") {
2099
+ await chmod(path, 384);
2100
+ }
2101
+ };
2102
+ var makeStoredConfiguration = async (input) => {
2103
+ const existing = await readStoredConfiguration();
2104
+ const canonicalWorkspaces = input.workspaces === undefined ? existing?.workspaces ?? [] : await Effect3.runPromise(parseWorkspaces("", JSON.stringify(input.workspaces)));
2105
+ const clientToken = input.clientToken ?? existing?.clientToken;
2106
+ const deviceToken = input.deviceToken ?? existing?.deviceToken;
2107
+ return StoredConfiguration.make({
2108
+ version: 1,
2109
+ relayUrl: normalizeRelayUrl(input.relayUrl),
2110
+ deviceId: existing?.deviceId ?? makeDeviceId(),
2111
+ deviceName: input.deviceName ?? existing?.deviceName ?? hostname(),
2112
+ workspaces: canonicalWorkspaces,
2113
+ ...clientToken === undefined ? {} : { clientToken },
2114
+ ...deviceToken === undefined ? {} : { deviceToken },
2115
+ ...existing?.model === undefined ? {} : { model: existing.model },
2116
+ ...existing?.sandbox === undefined ? {} : { sandbox: existing.sandbox }
2117
+ });
2118
+ };
2119
+ var environmentWorkspaces = async (stored) => {
2120
+ const list = process.env.COHALL_DEVICE_WORKSPACES;
2121
+ const json = process.env.COHALL_DEVICE_WORKSPACES_JSON;
2122
+ if (list === undefined && json === undefined) {
2123
+ return stored?.workspaces ?? [];
2124
+ }
2125
+ return Effect3.runPromise(parseWorkspaces(list ?? "", json));
2126
+ };
2127
+ var environmentProvider = () => process.env.COHALL_MODEL ?? process.env.COHALL_CODEX_MODEL;
2128
+ var environmentSandbox = () => {
2129
+ const value = process.env.COHALL_SANDBOX ?? process.env.COHALL_CODEX_SANDBOX;
2130
+ return value === undefined ? undefined : Schema4.decodeUnknownSync(Sandbox)(value);
2131
+ };
2132
+ var storedOrDefaults = async () => {
2133
+ const stored = await readStoredConfiguration();
2134
+ return stored ?? StoredConfiguration.make({
2135
+ version: 1,
2136
+ relayUrl: "http://127.0.0.1:8787",
2137
+ deviceId: makeDeviceId(),
2138
+ deviceName: hostname(),
2139
+ workspaces: []
2140
+ });
2141
+ };
2142
+ var loadClientConfiguration = Effect3.tryPromise({
2143
+ try: async () => {
2144
+ const stored = await storedOrDefaults();
2145
+ const token = process.env.COHALL_CLIENT_TOKEN ?? stored.clientToken;
2146
+ if (token === undefined) {
2147
+ throw new Error(`No client credential. Run cohall join with a pairing token on stdin or set COHALL_CLIENT_TOKEN.`);
2148
+ }
2149
+ return ClientConfiguration.make({
2150
+ relayUrl: normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl),
2151
+ token,
2152
+ ...process.env.COHALL_THREAD_ID === undefined ? {} : { mcpThreadId: process.env.COHALL_THREAD_ID }
2153
+ });
2154
+ },
2155
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
2156
+ });
2157
+ var loadOwnerConfiguration = Effect3.tryPromise({
2158
+ try: async () => {
2159
+ const stored = await storedOrDefaults();
2160
+ const token = process.env.COHALL_TOKEN;
2161
+ if (token === undefined) {
2162
+ throw new Error("No owner credential. Set COHALL_TOKEN on the owner-authenticated machine.");
2163
+ }
2164
+ return ClientConfiguration.make({
2165
+ relayUrl: normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl),
2166
+ token
2167
+ });
2168
+ },
2169
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
2170
+ });
2171
+ var loadDeviceConfiguration = Effect3.tryPromise({
2172
+ try: async () => {
2173
+ const stored = await storedOrDefaults();
2174
+ const token = process.env.COHALL_DEVICE_TOKEN ?? stored.deviceToken;
2175
+ if (token === undefined) {
2176
+ throw new Error(`No device credential. Run cohall join with a pairing token on stdin or set COHALL_DEVICE_TOKEN.`);
2177
+ }
2178
+ const workspaces = await environmentWorkspaces(stored);
2179
+ if (workspaces.length === 0) {
2180
+ throw new Error("No device workspace roots are configured");
2181
+ }
2182
+ const model = environmentProvider() ?? stored.model;
2183
+ const sandbox = environmentSandbox() ?? stored.sandbox;
2184
+ return DeviceConfiguration.make({
2185
+ relayUrl: normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl),
2186
+ token,
2187
+ id: process.env.COHALL_DEVICE_ID === undefined ? stored.deviceId : DeviceId.make(process.env.COHALL_DEVICE_ID),
2188
+ name: process.env.COHALL_DEVICE_NAME ?? stored.deviceName,
2189
+ workspaces,
2190
+ ...model === undefined ? {} : { model },
2191
+ ...sandbox === undefined ? {} : { sandbox }
2192
+ });
2193
+ },
2194
+ catch: (cause) => cause instanceof Error ? cause : new Error(String(cause))
2195
+ });
2196
+ var knownProviders = Provider.literals;
2197
+
2198
+ // apps/device/src/delegation.ts
2199
+ init_src();
2200
+ import { Effect as Effect4, Schema as Schema5 } from "effect";
2201
+
2202
+ class DeviceSelectionError extends Schema5.TaggedErrorClass()("Cohall.DeviceSelectionError", { message: Schema5.String, devices: Schema5.Array(Schema5.String) }) {
2203
+ }
2204
+
2205
+ class TaskWaitTimeoutError extends Schema5.TaggedErrorClass()("Cohall.TaskWaitTimeoutError", { message: Schema5.String, taskId: TaskId, status: TaskStatus }) {
2206
+ }
2207
+ var TaskResult = Schema5.Struct({
2208
+ task_id: TaskId,
2209
+ thread_id: ThreadId,
2210
+ status: TaskStatus,
2211
+ provider: Provider,
2212
+ target_device_id: DeviceId,
2213
+ result: Schema5.optionalKey(Schema5.String),
2214
+ error: Schema5.optionalKey(Schema5.String)
2215
+ });
2216
+ var matchingDevices = (devices, target) => {
2217
+ const normalized = target.replace(/^@/, "").toLowerCase();
2218
+ const byId = devices.find((device) => device.id === target);
2219
+ return byId === undefined ? devices.filter((device) => device.name.toLowerCase() === normalized || device.hostname.toLowerCase() === normalized) : [byId];
2220
+ };
2221
+ var taskResult = (task) => TaskResult.make({
2222
+ task_id: task.id,
2223
+ thread_id: task.threadId,
2224
+ status: task.status,
2225
+ provider: task.provider,
2226
+ target_device_id: task.targetDeviceId,
2227
+ ...task.result === undefined ? {} : { result: task.result },
2228
+ ...task.error === undefined ? {} : { error: task.error }
2229
+ });
2230
+ var createDelegation = Effect4.fn("Cohall.createDelegation")(function* (client, configuration, options) {
2231
+ const devices = yield* client.devices();
2232
+ const matches = options.target === undefined ? [] : matchingDevices(devices, options.target);
2233
+ if (options.target !== undefined && matches.length !== 1) {
2234
+ return yield* new DeviceSelectionError({
2235
+ message: matches.length === 0 ? `No Cohall device matches ${options.target}` : `${options.target} is ambiguous; use a device ID`,
2236
+ devices: matches.length === 0 ? devices.map((device) => `${device.name} (${device.id})`) : matches.map((device) => `${device.name} (${device.id})`)
2237
+ });
2238
+ }
2239
+ if (devices.length === 0) {
2240
+ return yield* new DeviceSelectionError({
2241
+ message: "No Cohall devices are registered",
2242
+ devices: []
2243
+ });
2244
+ }
2245
+ const target = matches[0];
2246
+ const inheritedThread = configuration.mcpThreadId;
2247
+ return yield* client.createTask({
2248
+ prompt: options.prompt,
2249
+ ...target === undefined ? {} : { targetDeviceId: target.id },
2250
+ ...options.threadId !== undefined ? { threadId: options.threadId } : inheritedThread === undefined ? {} : { threadId: ThreadId.make(inheritedThread) },
2251
+ ...options.context === undefined ? {} : { context: options.context },
2252
+ ...options.workspace === undefined ? {} : { workspace: options.workspace },
2253
+ ...options.provider === undefined ? {} : { provider: options.provider }
2254
+ });
2255
+ });
2256
+ var waitForTask = Effect4.fn("Cohall.waitForTask")(function* (client, initial, timeoutSeconds) {
2257
+ const deadline = Date.now() + timeoutSeconds * 1000;
2258
+ let task = initial;
2259
+ while (!isTerminalTask(task)) {
2260
+ if (Date.now() >= deadline) {
2261
+ return yield* new TaskWaitTimeoutError({
2262
+ message: `Task ${task.id} is still ${task.status} after ${timeoutSeconds} seconds`,
2263
+ taskId: task.id,
2264
+ status: task.status
2265
+ });
2266
+ }
2267
+ yield* Effect4.sleep("1 second");
2268
+ task = yield* client.getTask(task.id);
2269
+ }
2270
+ return task;
2271
+ });
2272
+ var threadContext = Effect4.fn("Cohall.threadContext")(function* (client, threadId) {
2273
+ return yield* client.threadContext(threadId);
2274
+ });
2275
+
2276
+ // apps/device/src/cli.ts
2277
+ var valueOptions = new Set([
2278
+ "context",
2279
+ "context-file",
2280
+ "label",
2281
+ "model",
2282
+ "name",
2283
+ "prompt",
2284
+ "prompt-file",
2285
+ "provider",
2286
+ "relay",
2287
+ "sandbox",
2288
+ "target",
2289
+ "thread",
2290
+ "timeout",
2291
+ "token-file",
2292
+ "workspace"
2293
+ ]);
2294
+ var flagOptions = new Set(["client-only", "no-wait"]);
2295
+ var aliases = new Map([
2296
+ ["-c", "context"],
2297
+ ["-p", "prompt"],
2298
+ ["-t", "target"],
2299
+ ["-T", "thread"],
2300
+ ["-w", "workspace"]
2301
+ ]);
2302
+ var help = `Cohall connects agents running on your own devices.
2303
+
2304
+ Usage:
2305
+ cohall join --relay <url> --workspace <path> [--token-file <path>]
2306
+ cohall configure [--relay url] [--name name] [--workspace path] [--model id]
2307
+ cohall config
2308
+ cohall devices
2309
+ cohall delegate [prompt] [--target @device] [--provider provider]
2310
+ [--context text] [--thread uuid] [--workspace path]
2311
+ [--timeout seconds] [--no-wait]
2312
+ cohall status <task-id>
2313
+ cohall wait <task-id> [--timeout seconds]
2314
+ cohall cancel <task-id>
2315
+ cohall thread <thread-id>
2316
+ cohall pair [--label name] [--client-only]
2317
+ cohall sessions
2318
+ cohall revoke <session-id>
2319
+ cohall doctor
2320
+ cohall device
2321
+ cohall relay
2322
+ cohall mcp
2323
+ cohall skill [install [agents|claude|opencode|all]]
2324
+ cohall integrations
2325
+
2326
+ Providers: codex, claude-code, opencode.
2327
+ Configuration is stored per user; environment variables override it.`;
2328
+ var parseArguments = (values) => {
2329
+ const options = new Map;
2330
+ const positionals = [];
2331
+ for (let index = 0;index < values.length; index += 1) {
2332
+ const argument = values[index];
2333
+ if (argument === undefined) {
2334
+ break;
2335
+ }
2336
+ if (argument === "--") {
2337
+ positionals.push(...values.slice(index + 1));
2338
+ break;
2339
+ }
2340
+ if (!argument.startsWith("-")) {
2341
+ positionals.push(argument);
2342
+ continue;
2343
+ }
2344
+ const separator = argument.indexOf("=");
2345
+ const rawName = separator === -1 ? argument : argument.slice(0, separator);
2346
+ const inline = separator === -1 ? undefined : argument.slice(separator + 1);
2347
+ const name = aliases.get(rawName) ?? rawName.replace(/^--/, "");
2348
+ if (flagOptions.has(name)) {
2349
+ if (inline !== undefined) {
2350
+ throw new Error(`--${name} does not accept a value`);
2351
+ }
2352
+ options.set(name, true);
2353
+ continue;
2354
+ }
2355
+ if (!valueOptions.has(name)) {
2356
+ throw new Error(`Unknown option: ${argument}`);
2357
+ }
2358
+ const value = inline ?? values[index + 1];
2359
+ if (value === undefined || inline === undefined && value.startsWith("-") && value !== "-") {
2360
+ throw new Error(`${rawName} requires a value`);
2361
+ }
2362
+ const previous = options.get(name);
2363
+ options.set(name, previous === undefined || previous === true ? [value] : [...previous, value]);
2364
+ if (inline === undefined) {
2365
+ index += 1;
2366
+ }
2367
+ }
2368
+ return { options, positionals };
2369
+ };
2370
+ var values = (arguments_, name) => {
2371
+ const option = arguments_.options.get(name);
2372
+ if (option === true) {
2373
+ throw new Error(`--${name} requires a value`);
2374
+ }
2375
+ return option ?? [];
2376
+ };
2377
+ var option = (arguments_, name) => {
2378
+ const found = values(arguments_, name);
2379
+ if (found.length > 1 && name !== "workspace") {
2380
+ throw new Error(`--${name} may only be specified once`);
2381
+ }
2382
+ return found[0];
2383
+ };
2384
+ var allowOptions = (arguments_, names) => {
2385
+ const allowed = new Set(names);
2386
+ for (const name of arguments_.options.keys()) {
2387
+ if (!allowed.has(name)) {
2388
+ throw new Error(`--${name} is not valid for this command`);
2389
+ }
2390
+ }
2391
+ };
2392
+ var noPositionals = (arguments_, command2) => {
2393
+ if (arguments_.positionals.length > 0) {
2394
+ throw new Error(`${command2} does not accept positional arguments`);
2395
+ }
2396
+ };
2397
+ var identifier = (arguments_, label) => {
2398
+ const value = arguments_.positionals[0];
2399
+ if (value === undefined) {
2400
+ throw new Error(`${label} is required`);
2401
+ }
2402
+ if (arguments_.positionals.length > 1) {
2403
+ throw new Error(`Unexpected arguments after ${label}`);
2404
+ }
2405
+ return value;
2406
+ };
2407
+ var print = (value) => console.log(JSON.stringify(value, null, 2));
2408
+ var readInput = async (arguments_, name) => {
2409
+ const direct = option(arguments_, name);
2410
+ const path = option(arguments_, `${name}-file`);
2411
+ if (direct !== undefined && path !== undefined) {
2412
+ throw new Error(`Use either --${name} or --${name}-file, not both`);
2413
+ }
2414
+ if (path !== undefined) {
2415
+ if (!await access2(path).then(() => true).catch(() => false)) {
2416
+ throw new Error(`File does not exist: ${path}`);
2417
+ }
2418
+ if ((await stat(path)).size > 131072) {
2419
+ throw new Error(`${name} file exceeds 128 KiB`);
2420
+ }
2421
+ return readFile2(path, "utf8");
2422
+ }
2423
+ const result = direct === "-" ? await readStdin(131072, name) : direct;
2424
+ if (result !== undefined && result.length > 131072) {
2425
+ throw new Error(`${name} exceeds 128 KiB`);
2426
+ }
2427
+ return result;
2428
+ };
2429
+ var readStdin = async (limit, label) => {
2430
+ const chunks = [];
2431
+ let size = 0;
2432
+ for await (const chunk of process.stdin) {
2433
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : Buffer.from(chunk);
2434
+ size += bytes.byteLength;
2435
+ if (size > limit) {
2436
+ throw new Error(`${label} exceeds ${Math.floor(limit / 1024)} KiB`);
2437
+ }
2438
+ chunks.push(bytes);
2439
+ }
2440
+ return Buffer.concat(chunks, size).toString("utf8");
2441
+ };
2442
+ var timeout = (arguments_) => {
2443
+ const seconds = Number(option(arguments_, "timeout") ?? "900");
2444
+ if (!Number.isInteger(seconds) || seconds < 5 || seconds > 86400) {
2445
+ throw new Error("--timeout must be an integer between 5 and 86400");
2446
+ }
2447
+ return seconds;
2448
+ };
2449
+ var pairingToken = async (arguments_) => {
2450
+ const path = option(arguments_, "token-file");
2451
+ if (path !== undefined && !await access2(path).then(() => true).catch(() => false)) {
2452
+ throw new Error(`Pairing token file does not exist: ${path}`);
2453
+ }
2454
+ if (path !== undefined && (await stat(path)).size > 512) {
2455
+ throw new Error("Pairing token file exceeds 512 bytes");
2456
+ }
2457
+ const token = (path === undefined ? await readStdin(512, "Pairing token") : await readFile2(path, "utf8")).trim();
2458
+ if (token.length === 0 || new TextEncoder().encode(token).byteLength > 256) {
2459
+ throw new Error("Provide a pairing token on stdin or with --token-file");
2460
+ }
2461
+ return token;
2462
+ };
2463
+ var client = async () => {
2464
+ const configuration = await Effect5.runPromise(loadClientConfiguration);
2465
+ return {
2466
+ configuration,
2467
+ relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
2468
+ };
2469
+ };
2470
+ var ownerClient = async () => {
2471
+ const configuration = await Effect5.runPromise(loadOwnerConfiguration);
2472
+ return {
2473
+ configuration,
2474
+ relay: exports_src.make({ baseUrl: configuration.relayUrl, token: configuration.token })
2475
+ };
2476
+ };
2477
+ var installSkill = async (target) => {
2478
+ const roots = {
2479
+ agents: join3(homedir2(), ".agents", "skills", "cohall", "SKILL.md"),
2480
+ claude: join3(homedir2(), ".claude", "skills", "cohall", "SKILL.md"),
2481
+ opencode: join3(homedir2(), ".config", "opencode", "skills", "cohall", "SKILL.md")
2482
+ };
2483
+ const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
2484
+ if (selected === undefined) {
2485
+ throw new Error("Skill target must be agents, claude, opencode, or all");
2486
+ }
2487
+ for (const path of selected) {
2488
+ await mkdir2(dirname2(path), { recursive: true });
2489
+ await writeFile2(path, SKILL_default);
2490
+ }
2491
+ return selected;
2492
+ };
2493
+ var printHelp = () => console.log(help);
2494
+ var printSkill = () => console.log(SKILL_default.trimEnd());
2495
+ var runCli = async (command2, raw) => {
2496
+ const arguments_ = parseArguments(raw);
2497
+ if (command2 === "join") {
2498
+ allowOptions(arguments_, ["client-only", "name", "relay", "token-file", "workspace"]);
2499
+ noPositionals(arguments_, command2);
2500
+ const token = await pairingToken(arguments_);
2501
+ const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? "http://127.0.0.1:8787");
2502
+ const existing = await readStoredConfiguration();
2503
+ const suppliedWorkspaces = values(arguments_, "workspace");
2504
+ const workspaces = suppliedWorkspaces.length === 0 ? existing?.workspaces : suppliedWorkspaces;
2505
+ const deviceName = option(arguments_, "name");
2506
+ if (!arguments_.options.has("client-only") && (workspaces?.length ?? 0) === 0) {
2507
+ throw new Error("At least one --workspace is required when joining a device");
2508
+ }
2509
+ const draft = await makeStoredConfiguration({
2510
+ relayUrl,
2511
+ ...deviceName === undefined ? {} : { deviceName },
2512
+ ...workspaces === undefined ? {} : { workspaces }
2513
+ });
2514
+ const paired = await Effect5.runPromise(exchangePairing(relayUrl, { token }));
2515
+ const clientToken = paired.credentials.find((credential) => credential.session.role === "client")?.token;
2516
+ const deviceToken = paired.credentials.find((credential) => credential.session.role === "device")?.token;
2517
+ const deviceId = paired.credentials.find((credential) => credential.session.role === "device")?.session.deviceId;
2518
+ if (!arguments_.options.has("client-only") && deviceToken === undefined) {
2519
+ throw new Error("The pairing credential did not include device access");
2520
+ }
2521
+ const configuration2 = StoredConfiguration.make({
2522
+ ...draft,
2523
+ deviceId: deviceId ?? draft.deviceId,
2524
+ ...clientToken === undefined ? {} : { clientToken },
2525
+ ...deviceToken === undefined ? {} : { deviceToken }
2526
+ });
2527
+ await writeStoredConfiguration(configuration2);
2528
+ print({
2529
+ configured: true,
2530
+ config_path: configurationPath(),
2531
+ relay_url: configuration2.relayUrl,
2532
+ device_id: configuration2.deviceId,
2533
+ roles: paired.credentials.map((credential) => credential.session.role),
2534
+ workspaces: configuration2.workspaces
2535
+ });
2536
+ return;
2537
+ }
2538
+ if (command2 === "config") {
2539
+ allowOptions(arguments_, []);
2540
+ noPositionals(arguments_, command2);
2541
+ const configuration2 = await readStoredConfiguration();
2542
+ print({
2543
+ config_path: configurationPath(),
2544
+ configured: configuration2 !== undefined,
2545
+ ...configuration2 === undefined ? {} : {
2546
+ relay_url: configuration2.relayUrl,
2547
+ device_id: configuration2.deviceId,
2548
+ device_name: configuration2.deviceName,
2549
+ workspaces: configuration2.workspaces,
2550
+ client_credential: configuration2.clientToken !== undefined,
2551
+ device_credential: configuration2.deviceToken !== undefined,
2552
+ model: configuration2.model,
2553
+ sandbox: configuration2.sandbox
2554
+ }
2555
+ });
2556
+ return;
2557
+ }
2558
+ if (command2 === "configure") {
2559
+ allowOptions(arguments_, ["model", "name", "relay", "sandbox", "workspace"]);
2560
+ noPositionals(arguments_, command2);
2561
+ const existing = await readStoredConfiguration();
2562
+ const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl ?? "http://127.0.0.1:8787");
2563
+ const supplied = values(arguments_, "workspace");
2564
+ const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect5.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
2565
+ const sandbox = option(arguments_, "sandbox");
2566
+ const model = option(arguments_, "model") ?? existing?.model;
2567
+ const configuration2 = StoredConfiguration.make({
2568
+ version: 1,
2569
+ relayUrl,
2570
+ deviceId: existing?.deviceId ?? makeDeviceId(),
2571
+ deviceName: option(arguments_, "name") ?? existing?.deviceName ?? "cohall-device",
2572
+ workspaces,
2573
+ ...existing?.clientToken === undefined ? {} : { clientToken: existing.clientToken },
2574
+ ...existing?.deviceToken === undefined ? {} : { deviceToken: existing.deviceToken },
2575
+ ...model === undefined ? {} : { model },
2576
+ ...(sandbox ?? existing?.sandbox) === undefined ? {} : {
2577
+ sandbox: Schema6.decodeUnknownSync(Schema6.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
2578
+ }
2579
+ });
2580
+ await writeStoredConfiguration(configuration2);
2581
+ print({ configured: true, config_path: configurationPath() });
2582
+ return;
2583
+ }
2584
+ if (command2 === "skill") {
2585
+ allowOptions(arguments_, []);
2586
+ if (arguments_.positionals.length === 0) {
2587
+ printSkill();
2588
+ return;
2589
+ }
2590
+ if (arguments_.positionals[0] !== "install" || arguments_.positionals.length > 2) {
2591
+ throw new Error("Usage: cohall skill [install [agents|claude|opencode|all]]");
2592
+ }
2593
+ print({ installed: await installSkill(arguments_.positionals[1] ?? "all") });
2594
+ return;
2595
+ }
2596
+ if (command2 === "integrations") {
2597
+ allowOptions(arguments_, []);
2598
+ noPositionals(arguments_, command2);
2599
+ print({
2600
+ cli_skill: "npx -y @akshar5/cohall skill install all",
2601
+ codex_mcp: "codex mcp add cohall -- npx -y @akshar5/cohall mcp",
2602
+ claude_mcp: "claude mcp add --scope user cohall -- npx -y @akshar5/cohall mcp",
2603
+ opencode_mcp: "Run opencode mcp add and use: npx -y @akshar5/cohall mcp"
2604
+ });
2605
+ return;
2606
+ }
2607
+ if (command2 === "doctor") {
2608
+ allowOptions(arguments_, []);
2609
+ noPositionals(arguments_, command2);
2610
+ const configuration2 = await readStoredConfiguration();
2611
+ const relayUrl = normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? configuration2?.relayUrl ?? "http://127.0.0.1:8787");
2612
+ const response = await fetch(`${relayUrl}/api/health`, {
2613
+ signal: AbortSignal.timeout(1e4)
2614
+ }).catch(() => {
2615
+ return;
2616
+ });
2617
+ print({
2618
+ relay: response?.ok === true ? "reachable" : "unreachable",
2619
+ relay_url: relayUrl,
2620
+ config_path: configurationPath(),
2621
+ client_credential: process.env.COHALL_CLIENT_TOKEN !== undefined || configuration2?.clientToken !== undefined,
2622
+ device_credential: process.env.COHALL_DEVICE_TOKEN !== undefined || configuration2?.deviceToken !== undefined,
2623
+ workspaces: configuration2?.workspaces ?? [],
2624
+ providers: Object.fromEntries(Provider.literals.map((provider) => [
2625
+ provider,
2626
+ findExecutable(provider === "claude-code" ? "claude" : provider) ?? "not found"
2627
+ ]))
2628
+ });
2629
+ return;
2630
+ }
2631
+ if (command2 === "pair" || command2 === "sessions" || command2 === "revoke") {
2632
+ const { configuration: configuration2, relay: relay2 } = await ownerClient();
2633
+ if (command2 === "pair") {
2634
+ allowOptions(arguments_, ["client-only", "label"]);
2635
+ noPositionals(arguments_, command2);
2636
+ const roles = arguments_.options.has("client-only") ? ["client"] : ["client", "device"];
2637
+ const credential = await Effect5.runPromise(relay2.createPairing({ label: option(arguments_, "label") ?? "Cohall device", roles }));
2638
+ print({
2639
+ relay_url: configuration2.relayUrl,
2640
+ pairing_token: credential.token,
2641
+ expires_at: credential.expiresAt,
2642
+ roles
2643
+ });
2644
+ return;
2645
+ }
2646
+ if (command2 === "sessions") {
2647
+ allowOptions(arguments_, []);
2648
+ noPositionals(arguments_, command2);
2649
+ print(await Effect5.runPromise(relay2.authSessions()));
2650
+ return;
2651
+ }
2652
+ allowOptions(arguments_, []);
2653
+ print(await Effect5.runPromise(relay2.revokeAuthSession(Schema6.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
2654
+ return;
2655
+ }
2656
+ const { configuration, relay } = await client();
2657
+ if (command2 === "devices") {
2658
+ allowOptions(arguments_, []);
2659
+ noPositionals(arguments_, command2);
2660
+ print(await Effect5.runPromise(relay.devices()));
2661
+ return;
2662
+ }
2663
+ if (command2 === "delegate") {
2664
+ allowOptions(arguments_, [
2665
+ "context",
2666
+ "context-file",
2667
+ "no-wait",
2668
+ "prompt",
2669
+ "prompt-file",
2670
+ "provider",
2671
+ "target",
2672
+ "thread",
2673
+ "timeout",
2674
+ "workspace"
2675
+ ]);
2676
+ if (option(arguments_, "prompt") === "-" && option(arguments_, "context") === "-") {
2677
+ throw new Error("Only one of --prompt and --context may read from stdin");
2678
+ }
2679
+ const suppliedPrompt = await readInput(arguments_, "prompt");
2680
+ const prompt = suppliedPrompt ?? arguments_.positionals.join(" ");
2681
+ if (prompt.trim().length === 0) {
2682
+ throw new Error("A prompt is required");
2683
+ }
2684
+ if (suppliedPrompt !== undefined && arguments_.positionals.length > 0) {
2685
+ throw new Error("Use either a positional prompt or --prompt, not both");
2686
+ }
2687
+ const context = await readInput(arguments_, "context");
2688
+ const target = option(arguments_, "target");
2689
+ const thread = option(arguments_, "thread");
2690
+ const workspace = option(arguments_, "workspace");
2691
+ const provider = option(arguments_, "provider");
2692
+ const task = await Effect5.runPromise(createDelegation(relay, configuration, {
2693
+ prompt,
2694
+ ...target === undefined ? {} : { target },
2695
+ ...context === undefined ? {} : { context },
2696
+ ...thread === undefined ? {} : { threadId: Schema6.decodeUnknownSync(ThreadId)(thread) },
2697
+ ...workspace === undefined ? {} : { workspace },
2698
+ ...provider === undefined ? {} : { provider: Schema6.decodeUnknownSync(Provider)(provider) }
2699
+ }));
2700
+ print(taskResult(arguments_.options.has("no-wait") ? task : await Effect5.runPromise(waitForTask(relay, task, timeout(arguments_)))));
2701
+ return;
2702
+ }
2703
+ if (command2 === "status") {
2704
+ allowOptions(arguments_, []);
2705
+ const id = Schema6.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
2706
+ print(taskResult(await Effect5.runPromise(relay.getTask(id))));
2707
+ return;
2708
+ }
2709
+ if (command2 === "wait") {
2710
+ allowOptions(arguments_, ["timeout"]);
2711
+ const id = Schema6.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
2712
+ const task = await Effect5.runPromise(relay.getTask(id));
2713
+ print(taskResult(await Effect5.runPromise(waitForTask(relay, task, timeout(arguments_)))));
2714
+ return;
2715
+ }
2716
+ if (command2 === "cancel") {
2717
+ allowOptions(arguments_, []);
2718
+ const id = Schema6.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
2719
+ print(taskResult(await Effect5.runPromise(relay.cancelTask(id))));
2720
+ return;
2721
+ }
2722
+ if (command2 === "thread") {
2723
+ allowOptions(arguments_, []);
2724
+ const id = Schema6.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
2725
+ print(await Effect5.runPromise(threadContext(relay, id)));
2726
+ return;
2727
+ }
2728
+ throw new Error(`Unknown Cohall command: ${command2}`);
2729
+ };
2730
+
2731
+ // apps/device/src/daemon.ts
2732
+ init_src();
2733
+ import { Effect as Effect6, Schedule, Schema as Schema7 } from "effect";
2734
+ import { realpath as realpath2 } from "node:fs/promises";
2735
+ import { arch, hostname as hostname2, platform as platform3 } from "node:os";
2736
+ import { basename, isAbsolute as isAbsolute2, relative } from "node:path";
2737
+ import { WebSocket } from "ws";
2738
+
2739
+ class DeviceConnectionError extends Schema7.TaggedErrorClass()("Device.ConnectionError", { message: Schema7.String }) {
2740
+ }
2741
+ var socketUrl = (configuration) => {
2742
+ const url = new URL(configuration.relayUrl);
2743
+ url.protocol = url.protocol === "https:" ? "wss:" : "ws:";
2744
+ url.pathname = "/ws/device";
2745
+ return url.toString();
2746
+ };
2747
+ var providerLabel = (provider) => {
2748
+ switch (provider) {
2749
+ case "codex":
2750
+ return "Codex";
2751
+ case "claude-code":
2752
+ return "Claude Code";
2753
+ case "opencode":
2754
+ return "OpenCode";
2755
+ }
2756
+ };
2757
+ var capabilities = (providers) => {
2758
+ const values2 = providers.map((provider) => ({
2759
+ id: provider,
2760
+ label: providerLabel(provider),
2761
+ detail: `Uses this device's local ${providerLabel(provider)} login, configuration, and tools`
2762
+ }));
2763
+ if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined || platform3() === "darwin") {
2764
+ values2.push({ id: "browser-session", label: "Signed-in browser" });
2765
+ }
2766
+ if (findExecutable("xcodebuild") !== undefined) {
2767
+ values2.push({ id: "xcode", label: "Xcode" });
2768
+ }
2769
+ if (findExecutable("docker") !== undefined) {
2770
+ values2.push({ id: "docker", label: "Docker" });
2771
+ }
2772
+ return values2;
2773
+ };
2774
+ var describeDevice = (configuration, status = "online") => {
2775
+ const operatingSystem = platform3();
2776
+ const platformName = operatingSystem === "darwin" || operatingSystem === "linux" ? operatingSystem : operatingSystem === "win32" ? "windows" : "unknown";
2777
+ const providers = availableProviders();
2778
+ return Device.make({
2779
+ id: configuration.id,
2780
+ name: configuration.name,
2781
+ hostname: hostname2(),
2782
+ platform: platformName,
2783
+ architecture: arch(),
2784
+ status,
2785
+ providers,
2786
+ capabilities: capabilities(providers),
2787
+ workspaces: configuration.workspaces.map((path) => ({
2788
+ path,
2789
+ label: basename(path) || path
2790
+ })),
2791
+ version,
2792
+ lastSeenAt: now()
2793
+ });
2794
+ };
2795
+ var allowedWorkspace = async (configuration, requested) => {
2796
+ const selected = requested ?? configuration.workspaces[0];
2797
+ if (selected === undefined) {
2798
+ throw new Error("No workspace is configured on this device");
2799
+ }
2800
+ const candidate = await realpath2(selected);
2801
+ const allowed = configuration.workspaces.some((root) => {
2802
+ const child = relative(root, candidate);
2803
+ return child === "" || !child.startsWith("..") && !isAbsolute2(child);
2804
+ });
2805
+ if (!allowed) {
2806
+ throw new Error(`Workspace ${candidate} is outside this device's configured workspace roots`);
2807
+ }
2808
+ return candidate;
2809
+ };
2810
+ var promptFor = (task, deviceName) => {
2811
+ const context = task.context === undefined ? "" : `
2812
+
2813
+ Relevant context supplied by the sending agent:
2814
+ ${task.context}`;
2815
+ return [
2816
+ `You are the Cohall agent running on ${deviceName}.`,
2817
+ "Complete the delegated task using this device's local workspace, tools, credentials, and signed-in services.",
2818
+ "Never read, reveal, copy, or use Cohall configuration files or Cohall authentication tokens.",
2819
+ "Return a concise, complete result with the evidence the sending agent needs.",
2820
+ `
2821
+ Task:
2822
+ ${task.prompt}${context}`
2823
+ ].join(`
2824
+ `);
2825
+ };
2826
+ var send = (state, event) => {
2827
+ if (state.socket?.readyState === WebSocket.OPEN) {
2828
+ state.socket.send(JSON.stringify(event));
2829
+ }
2830
+ };
2831
+ var sendTerminal = (state, taskId, event) => {
2832
+ const payload = JSON.stringify(event);
2833
+ state.terminal.set(taskId, payload);
2834
+ if (state.socket?.readyState === WebSocket.OPEN) {
2835
+ state.socket.send(payload);
2836
+ }
2837
+ };
2838
+ var remember = (state, taskId) => {
2839
+ state.completed.add(taskId);
2840
+ if (state.completed.size <= 1000) {
2841
+ return;
2842
+ }
2843
+ const oldest = state.completed.values().next();
2844
+ if (!oldest.done) {
2845
+ state.completed.delete(oldest.value);
2846
+ state.terminal.delete(oldest.value);
2847
+ }
2848
+ };
2849
+ var sessionKey = (task) => `${task.threadId}:${task.provider}`;
2850
+ var execute = (configuration, state, task) => {
2851
+ if (state.tasks.has(task.id) || state.completed.has(task.id)) {
2852
+ return;
2853
+ }
2854
+ const controller = new AbortController;
2855
+ state.tasks.set(task.id, controller);
2856
+ send(state, SocketEvent.make({ _tag: "TaskAccepted", taskId: task.id }));
2857
+ const workflow = Effect6.gen(function* () {
2858
+ const cwd = yield* Effect6.tryPromise({
2859
+ try: () => allowedWorkspace(configuration, task.workspace),
2860
+ catch: (cause) => new ProviderRunError({
2861
+ provider: task.provider,
2862
+ message: cause instanceof Error ? cause.message : String(cause)
2863
+ })
2864
+ });
2865
+ const sessionId = task.providerSessionId ?? state.sessions.get(sessionKey(task));
2866
+ return yield* run({
2867
+ provider: task.provider,
2868
+ threadId: task.threadId,
2869
+ prompt: promptFor(task, configuration.name),
2870
+ cwd,
2871
+ ...sessionId === undefined ? {} : { sessionId },
2872
+ ...configuration.model === undefined ? {} : { model: configuration.model },
2873
+ ...configuration.sandbox === undefined ? {} : { sandbox: configuration.sandbox }
2874
+ });
2875
+ });
2876
+ Effect6.runPromise(workflow, { signal: controller.signal }).then((result) => {
2877
+ if (result.sessionId !== undefined) {
2878
+ state.sessions.set(sessionKey(task), result.sessionId);
2879
+ }
2880
+ remember(state, task.id);
2881
+ sendTerminal(state, task.id, SocketEvent.make({
2882
+ _tag: "TaskFinished",
2883
+ taskId: task.id,
2884
+ result: result.result,
2885
+ ...result.sessionId === undefined ? {} : { providerSessionId: result.sessionId }
2886
+ }));
2887
+ }).catch((cause) => {
2888
+ remember(state, task.id);
2889
+ sendTerminal(state, task.id, controller.signal.aborted ? SocketEvent.make({ _tag: "TaskCancelled", taskId: task.id }) : SocketEvent.make({
2890
+ _tag: "TaskFailed",
2891
+ taskId: task.id,
2892
+ error: (cause instanceof Error ? cause.message : String(cause)).slice(0, 16384)
2893
+ }));
2894
+ }).finally(() => {
2895
+ state.tasks.delete(task.id);
2896
+ const next = state.queue.shift();
2897
+ if (next !== undefined) {
2898
+ execute(configuration, state, next);
2899
+ }
2900
+ });
2901
+ };
2902
+ var schedule = (configuration, state, task) => {
2903
+ if (state.tasks.has(task.id) || state.completed.has(task.id) || state.queue.some((queued) => queued.id === task.id)) {
2904
+ return;
2905
+ }
2906
+ if (task.providerSessionId !== undefined) {
2907
+ state.sessions.set(sessionKey(task), task.providerSessionId);
2908
+ }
2909
+ if (state.tasks.size > 0) {
2910
+ if (state.queue.length >= 100) {
2911
+ state.socket?.close(4008, "Task queue limit reached");
2912
+ return;
2913
+ }
2914
+ state.queue.push(task);
2915
+ return;
2916
+ }
2917
+ execute(configuration, state, task);
2918
+ };
2919
+ var cancel = (state, taskId) => {
2920
+ const running = state.tasks.get(taskId);
2921
+ if (running !== undefined) {
2922
+ running.abort();
2923
+ return;
2924
+ }
2925
+ const index = state.queue.findIndex((task) => task.id === taskId);
2926
+ if (index !== -1) {
2927
+ state.queue.splice(index, 1);
2928
+ }
2929
+ remember(state, taskId);
2930
+ sendTerminal(state, taskId, SocketEvent.make({ _tag: "TaskCancelled", taskId }));
2931
+ };
2932
+ var connect = (configuration, state) => Effect6.tryPromise({
2933
+ try: (signal) => new Promise((complete) => {
2934
+ const socket = new WebSocket(socketUrl(configuration));
2935
+ const heartbeat = setInterval(() => {
2936
+ send(state, SocketEvent.make({
2937
+ _tag: "DeviceHeartbeat",
2938
+ deviceId: configuration.id,
2939
+ status: state.tasks.size > 0 ? "busy" : "online"
2940
+ }));
2941
+ }, 15000);
2942
+ let closed = false;
2943
+ const close = () => {
2944
+ if (closed) {
2945
+ return;
2946
+ }
2947
+ closed = true;
2948
+ clearInterval(heartbeat);
2949
+ if (state.socket === socket) {
2950
+ state.socket = undefined;
2951
+ }
2952
+ complete();
2953
+ };
2954
+ signal.addEventListener("abort", () => {
2955
+ socket.close();
2956
+ close();
2957
+ }, { once: true });
2958
+ socket.once("open", () => {
2959
+ socket.send(JSON.stringify(SocketEvent.make({ _tag: "Authenticate", token: configuration.token })));
2960
+ });
2961
+ socket.on("message", (message) => {
2962
+ 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");
2963
+ state.processing = state.processing.then(async () => {
2964
+ const event = await Effect6.runPromise(Effect6.try({
2965
+ try: () => JSON.parse(text2),
2966
+ catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
2967
+ }).pipe(Effect6.flatMap(decodeSocketEvent)));
2968
+ if (event._tag === "Connected") {
2969
+ state.socket = socket;
2970
+ socket.send(JSON.stringify(SocketEvent.make({
2971
+ _tag: "DeviceHello",
2972
+ device: describeDevice(configuration, state.tasks.size > 0 ? "busy" : "online")
2973
+ })));
2974
+ for (const payload of state.terminal.values()) {
2975
+ socket.send(payload);
2976
+ }
2977
+ return;
2978
+ }
2979
+ if (event._tag === "TaskAssigned") {
2980
+ schedule(configuration, state, event.task);
2981
+ return;
2982
+ }
2983
+ if (event._tag === "CancelTask") {
2984
+ cancel(state, event.taskId);
2985
+ return;
2986
+ }
2987
+ if (event._tag === "TaskSettled") {
2988
+ state.terminal.delete(event.taskId);
2989
+ return;
2990
+ }
2991
+ if (event._tag === "Error") {
2992
+ console.error(`Relay error: ${event.message}`);
2993
+ }
2994
+ }).catch((cause) => {
2995
+ console.error(`Invalid relay event: ${cause instanceof Error ? cause.message : String(cause)}`);
2996
+ });
2997
+ });
2998
+ socket.once("close", close);
2999
+ socket.once("error", () => socket.close());
3000
+ }),
3001
+ catch: (cause) => new DeviceConnectionError({
3002
+ message: cause instanceof Error ? cause.message : String(cause)
3003
+ })
3004
+ });
3005
+ var runDaemon = (configuration) => {
3006
+ const state = {
3007
+ socket: undefined,
3008
+ processing: Promise.resolve(),
3009
+ terminal: new Map,
3010
+ queue: [],
3011
+ sessions: new Map,
3012
+ tasks: new Map,
3013
+ completed: new Set
3014
+ };
3015
+ return connect(configuration, state).pipe(Effect6.repeat({ schedule: Schedule.spaced("2 seconds") }), Effect6.ensuring(Effect6.sync(() => {
3016
+ state.socket?.close();
3017
+ for (const controller of state.tasks.values()) {
3018
+ controller.abort();
3019
+ }
3020
+ })));
3021
+ };
3022
+
3023
+ // apps/device/src/mcp.ts
3024
+ init_src();
3025
+ import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
3026
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
3027
+ import { Effect as Effect7 } from "effect";
3028
+ import * as z from "zod/v4";
3029
+ var output = (value) => ({
3030
+ content: [{ type: "text", text: JSON.stringify(value, null, 2) }]
3031
+ });
3032
+ var runMcp = async (configuration) => {
3033
+ const client2 = exports_src.make({
3034
+ baseUrl: configuration.relayUrl,
3035
+ token: configuration.token
3036
+ });
3037
+ const server = new McpServer({ name: "cohall", version });
3038
+ server.registerTool("list_devices", {
3039
+ title: "List Cohall devices",
3040
+ description: "List devices, their availability, local providers, capabilities, and allowed workspaces.",
3041
+ inputSchema: {}
3042
+ }, async () => output(await Effect7.runPromise(client2.devices())));
3043
+ server.registerTool("delegate", {
3044
+ title: "Delegate work to another device",
3045
+ description: "Send focused work to another Cohall device. Include only relevant context; Cohall cannot read the host transcript. Reuse thread_id for related follow-ups.",
3046
+ inputSchema: {
3047
+ prompt: z.string().min(1).max(131072),
3048
+ target: z.string().optional().describe("Device name, @name, hostname, or ID"),
3049
+ provider: z.enum(["codex", "claude-code", "opencode"]).default("codex"),
3050
+ context: z.string().max(131072).optional(),
3051
+ thread_id: z.string().uuid().optional(),
3052
+ workspace: z.string().max(4096).optional(),
3053
+ wait: z.boolean().default(true),
3054
+ timeout_seconds: z.number().int().min(5).max(86400).default(900)
3055
+ }
3056
+ }, async ({ prompt, target, provider, context, thread_id, workspace, wait, timeout_seconds }) => {
3057
+ const task = await Effect7.runPromise(createDelegation(client2, configuration, {
3058
+ prompt,
3059
+ provider,
3060
+ ...target === undefined ? {} : { target },
3061
+ ...context === undefined ? {} : { context },
3062
+ ...thread_id === undefined ? {} : { threadId: ThreadId.make(thread_id) },
3063
+ ...workspace === undefined ? {} : { workspace }
3064
+ }));
3065
+ return output(taskResult(wait ? await Effect7.runPromise(waitForTask(client2, task, timeout_seconds)) : task));
3066
+ });
3067
+ server.registerTool("task_status", {
3068
+ title: "Get delegated task status",
3069
+ description: "Read the current state and result of a Cohall task.",
3070
+ inputSchema: { task_id: z.string().uuid() }
3071
+ }, async ({ task_id }) => output(taskResult(await Effect7.runPromise(client2.getTask(TaskId.make(task_id))))));
3072
+ server.registerTool("wait_task", {
3073
+ title: "Wait for delegated work",
3074
+ description: "Wait for a Cohall task to finish and return its final result.",
3075
+ inputSchema: {
3076
+ task_id: z.string().uuid(),
3077
+ timeout_seconds: z.number().int().min(5).max(86400).default(900)
3078
+ }
3079
+ }, async ({ task_id, timeout_seconds }) => {
3080
+ const task = await Effect7.runPromise(client2.getTask(TaskId.make(task_id)));
3081
+ return output(taskResult(await Effect7.runPromise(waitForTask(client2, task, timeout_seconds))));
3082
+ });
3083
+ server.registerTool("cancel_task", {
3084
+ title: "Cancel delegated work",
3085
+ description: "Request cancellation and return its acknowledged or pending state.",
3086
+ inputSchema: { task_id: z.string().uuid() }
3087
+ }, async ({ task_id }) => output(taskResult(await Effect7.runPromise(client2.cancelTask(TaskId.make(task_id))))));
3088
+ server.registerTool("thread_context", {
3089
+ title: "Read a Cohall thread",
3090
+ description: "Read shared task prompts and final results for a cross-device thread.",
3091
+ inputSchema: { thread_id: z.string().uuid() }
3092
+ }, async ({ thread_id }) => output(await Effect7.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
3093
+ await server.connect(new StdioServerTransport);
3094
+ };
3095
+
3096
+ // apps/device/src/main.ts
3097
+ var main = async () => {
3098
+ const command2 = process.argv[2];
3099
+ if (command2 === undefined || command2 === "help" || command2 === "--help" || command2 === "-h") {
3100
+ printHelp();
3101
+ return;
3102
+ }
3103
+ if (command2 === "--version" || command2 === "version") {
3104
+ console.log(version);
3105
+ return;
3106
+ }
3107
+ if (command2 === "relay") {
3108
+ const { runRelay: runRelay2 } = await init_main().then(() => exports_main);
3109
+ await runRelay2();
3110
+ return;
3111
+ }
3112
+ if (command2 === "device") {
3113
+ const configuration = await Effect11.runPromise(loadDeviceConfiguration);
3114
+ await Effect11.runPromise(runDaemon(configuration));
3115
+ return;
3116
+ }
3117
+ if (command2 === "mcp") {
3118
+ const configuration = await Effect11.runPromise(loadClientConfiguration);
3119
+ await runMcp(configuration);
3120
+ return;
3121
+ }
3122
+ await runCli(command2, process.argv.slice(3));
3123
+ };
3124
+ await main().catch((cause) => {
3125
+ console.error(JSON.stringify({ error: cause instanceof Error ? cause.message : String(cause) }, null, 2));
3126
+ process.exitCode = 1;
3127
+ });
3128
+
3129
+ //# debugId=7CC47D186DE52B1264756E2164756E21