@akshar5/cohall 0.4.10 → 0.5.1

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