@akshar5/cohall 0.10.1 → 0.11.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/bin/cohall.js CHANGED
@@ -17,7 +17,7 @@ var __esm = (fn, res) => () => (fn && (res = fn(fn = 0)), res);
17
17
 
18
18
  // packages/protocol/src/index.ts
19
19
  import { Effect, Schema } from "effect";
20
- var version = "0.10.1", maxSocketPayloadBytes, maxAttachmentBytes, maxTaskAttachments = 2, maxProgressNoteBytes = 1024, 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$/, {
20
+ var version = "0.11.0", maxSocketPayloadBytes, maxAttachmentBytes, maxTaskAttachments = 2, maxProgressNoteBytes = 1024, bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), count, uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
21
21
  expected: "an ISO-8601 UTC timestamp"
22
22
  })).pipe(Schema.brand(name)), BotId, Bot, DeviceId, ThreadId, MessageId, TaskId, TaskRequestId, TaskRunId, ClarificationId, AuthSessionId, OperationId, Timestamp, TaskDeadline, taskDeadlineError = "Task deadline exceeded", taskDeadlinePassed = (task, time = Date.now()) => task.expiresAt !== undefined && Date.parse(task.expiresAt) <= time, Platform, Provider, DeviceStatus, TaskStatus, OperationStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, maxDevicePageDevices = 16, maxDevicePageResponseBytes, DevicePage, minimumDeviceOperationVersion = "0.5.0", parsedVersion = (value) => {
23
23
  const match = /^v?(\d+)\.(\d+)\.(\d+)(-[0-9A-Za-z.-]+)?(?:\+[0-9A-Za-z.-]+)?$/.exec(value);
@@ -420,25 +420,25 @@ var init_src = __esm(() => {
420
420
  });
421
421
 
422
422
  // apps/relay/src/config.ts
423
- import { Effect as Effect13, Schema as Schema14 } from "effect";
424
- import { access as access5, chmod as chmod7, mkdir as mkdir8, readFile as readFile4, writeFile as writeFile9 } from "node:fs/promises";
423
+ import { Effect as Effect13, Schema as Schema15 } from "effect";
424
+ import { access as access5, chmod as chmod7, mkdir as mkdir9, readFile as readFile4, writeFile as writeFile10 } from "node:fs/promises";
425
425
  import { homedir as homedir5, platform as platform5 } from "node:os";
426
- import { isAbsolute as isAbsolute6, join as join9, resolve as resolve5 } from "node:path";
426
+ import { isAbsolute as isAbsolute6, join as join10, resolve as resolve5 } from "node:path";
427
427
  var RelayConfiguration, defaultDataDirectory = () => {
428
428
  if (platform5() === "win32") {
429
- return join9(process.env.LOCALAPPDATA ?? join9(homedir5(), "AppData", "Local"), "Cohall");
429
+ return join10(process.env.LOCALAPPDATA ?? join10(homedir5(), "AppData", "Local"), "Cohall");
430
430
  }
431
431
  if (platform5() === "darwin") {
432
- return join9(homedir5(), "Library", "Application Support", "Cohall", "relay");
432
+ return join10(homedir5(), "Library", "Application Support", "Cohall", "relay");
433
433
  }
434
434
  const dataHome = process.env.XDG_DATA_HOME;
435
- return join9(dataHome !== undefined && isAbsolute6(dataHome) ? dataHome : join9(homedir5(), ".local", "share"), "cohall");
435
+ return join10(dataHome !== undefined && isAbsolute6(dataHome) ? dataHome : join10(homedir5(), ".local", "share"), "cohall");
436
436
  }, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
437
437
  if (await access5(path).then(() => true).catch(() => false)) {
438
438
  return (await readFile4(path, "utf8")).trim();
439
439
  }
440
440
  const token = generatedToken();
441
- await writeFile9(path, `${token}
441
+ await writeFile10(path, `${token}
442
442
  `, { mode: 384 });
443
443
  if (platform5() !== "win32") {
444
444
  await chmod7(path, 384);
@@ -455,14 +455,14 @@ var RelayConfiguration, defaultDataDirectory = () => {
455
455
  throw new Error(`${name} must be true or false`);
456
456
  }, isLoopback2 = (host) => host === "127.0.0.1" || host === "localhost" || host === "::1", loadEnvironmentConfiguration;
457
457
  var init_config = __esm(() => {
458
- RelayConfiguration = Schema14.Struct({
459
- host: Schema14.NonEmptyString,
460
- port: Schema14.Int.check(Schema14.isBetween({ minimum: 1, maximum: 65535 })),
461
- token: Schema14.String.check(Schema14.isMinLength(32)),
462
- dataDirectory: Schema14.NonEmptyString,
463
- databasePath: Schema14.NonEmptyString,
464
- allowRemote: Schema14.Boolean,
465
- historyTaskLimit: Schema14.Int.check(Schema14.isBetween({ minimum: 100, maximum: 1e5 }))
458
+ RelayConfiguration = Schema15.Struct({
459
+ host: Schema15.NonEmptyString,
460
+ port: Schema15.Int.check(Schema15.isBetween({ minimum: 1, maximum: 65535 })),
461
+ token: Schema15.String.check(Schema15.isMinLength(32)),
462
+ dataDirectory: Schema15.NonEmptyString,
463
+ databasePath: Schema15.NonEmptyString,
464
+ allowRemote: Schema15.Boolean,
465
+ historyTaskLimit: Schema15.Int.check(Schema15.isBetween({ minimum: 100, maximum: 1e5 }))
466
466
  });
467
467
  loadEnvironmentConfiguration = Effect13.tryPromise({
468
468
  try: async () => {
@@ -473,21 +473,21 @@ var init_config = __esm(() => {
473
473
  throw new Error("Remote relay binding is disabled. Use a private network or TLS reverse proxy, then set COHALL_RELAY_ALLOW_REMOTE=true.");
474
474
  }
475
475
  const dataDirectory = resolve5(process.env.COHALL_DATA_DIR ?? defaultDataDirectory());
476
- await mkdir8(dataDirectory, { recursive: true, mode: 448 });
476
+ await mkdir9(dataDirectory, { recursive: true, mode: 448 });
477
477
  if (platform5() !== "win32") {
478
478
  await chmod7(dataDirectory, 448);
479
479
  }
480
- const token = process.env.COHALL_TOKEN ?? await tokenFrom(join9(dataDirectory, "owner-token"));
480
+ const token = process.env.COHALL_TOKEN ?? await tokenFrom(join10(dataDirectory, "owner-token"));
481
481
  if (token.length < 32) {
482
482
  throw new Error("The Cohall relay owner token must be at least 32 characters");
483
483
  }
484
484
  const historyTaskLimit = Number(process.env.COHALL_HISTORY_TASK_LIMIT ?? "1000");
485
- return Schema14.decodeUnknownSync(RelayConfiguration)({
485
+ return Schema15.decodeUnknownSync(RelayConfiguration)({
486
486
  host,
487
487
  port,
488
488
  token,
489
489
  dataDirectory,
490
- databasePath: join9(dataDirectory, "cohall.db"),
490
+ databasePath: join10(dataDirectory, "cohall.db"),
491
491
  allowRemote,
492
492
  historyTaskLimit
493
493
  });
@@ -673,12 +673,12 @@ __export(exports_store, {
673
673
  RelayStore: () => exports_store,
674
674
  PersistenceError: () => PersistenceError
675
675
  });
676
- import { Context as Context2, Effect as Effect14, Layer as Layer2, Schema as Schema15 } from "effect";
676
+ import { Context as Context2, Effect as Effect14, Layer as Layer2, Schema as Schema16 } from "effect";
677
677
  import { createHash as createHash2, randomBytes } from "node:crypto";
678
678
  var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, TaskRequestError, Service2, operationError = (operation) => (cause) => new PersistenceError({
679
679
  operation,
680
680
  message: cause instanceof Error ? cause.message : String(cause)
681
- }), decode2 = (operation, schema, value) => Schema15.decodeUnknownEffect(schema)(value).pipe(Effect14.mapError(operationError(operation))), threadFromRow = (row) => decode2("RelayStore.decodeThread", Thread, {
681
+ }), decode2 = (operation, schema, value) => Schema16.decodeUnknownEffect(schema)(value).pipe(Effect14.mapError(operationError(operation))), threadFromRow = (row) => decode2("RelayStore.decodeThread", Thread, {
682
682
  id: row.id,
683
683
  title: row.title,
684
684
  createdAt: row.created_at,
@@ -777,7 +777,7 @@ var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, Task
777
777
  kind: row.kind,
778
778
  at: row.created_at,
779
779
  ...row.detail === null ? {} : { detail: row.detail }
780
- }), rolesFromRow = (row) => Schema15.decodeUnknownSync(Schema15.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
780
+ }), rolesFromRow = (row) => Schema16.decodeUnknownSync(Schema16.Array(ConnectionRole))(JSON.parse(row.roles_json)), authSessionFromRow = (row) => {
781
781
  const roles = rolesFromRow(row);
782
782
  const role = roles[0];
783
783
  return role === undefined ? Effect14.fail(new PersistenceError({
@@ -1115,9 +1115,9 @@ var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, Task
1115
1115
  const devices = new Map;
1116
1116
  let retainedTasks = 0;
1117
1117
  for (const row of rows) {
1118
- const status = Schema15.decodeUnknownSync(TaskStatus)(row.status);
1119
- const provider = Schema15.decodeUnknownSync(Provider)(row.provider);
1120
- const deviceId = Schema15.decodeUnknownSync(DeviceId)(row.device_id);
1118
+ const status = Schema16.decodeUnknownSync(TaskStatus)(row.status);
1119
+ const provider = Schema16.decodeUnknownSync(Provider)(row.provider);
1120
+ const deviceId = Schema16.decodeUnknownSync(DeviceId)(row.device_id);
1121
1121
  retainedTasks += row.tasks;
1122
1122
  totalStatus[status] += row.tasks;
1123
1123
  totalProviders.set(provider, (totalProviders.get(provider) ?? 0) + row.tasks);
@@ -1909,7 +1909,7 @@ var PersistenceError, TaskProgressError, TaskInputError, TaskDeadlineError, Task
1909
1909
  if (pairing === null) {
1910
1910
  throw new Error("Pairing credential is invalid, expired, or already used");
1911
1911
  }
1912
- const roles = Schema15.decodeUnknownSync(Schema15.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
1912
+ const roles = Schema16.decodeUnknownSync(Schema16.Array(ConnectionRole))(JSON.parse(pairing.roles_json));
1913
1913
  const deviceId = roles.includes("device") ? makeDeviceId() : undefined;
1914
1914
  const consumed = db.query("UPDATE auth_pairings SET used_at = ? WHERE token_hash = ? AND used_at IS NULL").run(timestamp, pairing.token_hash);
1915
1915
  if (consumed.changes !== 1) {
@@ -2183,19 +2183,19 @@ var init_store = __esm(() => {
2183
2183
  init_src();
2184
2184
  init_database();
2185
2185
  init_store();
2186
- PersistenceError = class PersistenceError extends Schema15.TaggedErrorClass()("RelayStore.PersistenceError", {
2187
- operation: Schema15.String,
2188
- message: Schema15.String,
2189
- missingRecord: Schema15.optionalKey(Schema15.Literals(["task", "operation"]))
2186
+ PersistenceError = class PersistenceError extends Schema16.TaggedErrorClass()("RelayStore.PersistenceError", {
2187
+ operation: Schema16.String,
2188
+ message: Schema16.String,
2189
+ missingRecord: Schema16.optionalKey(Schema16.Literals(["task", "operation"]))
2190
2190
  }) {
2191
2191
  };
2192
- TaskProgressError = class TaskProgressError extends Schema15.TaggedErrorClass()("RelayStore.TaskProgressError", { status: Schema15.Literals([403, 409]), message: Schema15.String }) {
2192
+ TaskProgressError = class TaskProgressError extends Schema16.TaggedErrorClass()("RelayStore.TaskProgressError", { status: Schema16.Literals([403, 409]), message: Schema16.String }) {
2193
2193
  };
2194
- TaskInputError = class TaskInputError extends Schema15.TaggedErrorClass()("RelayStore.TaskInputError", { status: Schema15.Literals([403, 409]), message: Schema15.String }) {
2194
+ TaskInputError = class TaskInputError extends Schema16.TaggedErrorClass()("RelayStore.TaskInputError", { status: Schema16.Literals([403, 409]), message: Schema16.String }) {
2195
2195
  };
2196
- TaskDeadlineError = class TaskDeadlineError extends Schema15.TaggedErrorClass()("RelayStore.TaskDeadlineError", { status: Schema15.Literals([400, 409]), message: Schema15.String }) {
2196
+ TaskDeadlineError = class TaskDeadlineError extends Schema16.TaggedErrorClass()("RelayStore.TaskDeadlineError", { status: Schema16.Literals([400, 409]), message: Schema16.String }) {
2197
2197
  };
2198
- TaskRequestError = class TaskRequestError extends Schema15.TaggedErrorClass()("RelayStore.TaskRequestError", { status: Schema15.Literals([409, 410]), message: Schema15.String }) {
2198
+ TaskRequestError = class TaskRequestError extends Schema16.TaggedErrorClass()("RelayStore.TaskRequestError", { status: Schema16.Literals([409, 410]), message: Schema16.String }) {
2199
2199
  };
2200
2200
  Service2 = class Service2 extends Context2.Service()("@cohall/RelayStore") {
2201
2201
  };
@@ -2211,7 +2211,7 @@ __export(exports_main, {
2211
2211
  canReadTaskAttachments: () => canReadTaskAttachments,
2212
2212
  canDispatchTaskToDevice: () => canDispatchTaskToDevice
2213
2213
  });
2214
- import { Effect as Effect15, ManagedRuntime, Schema as Schema16, Schedule as Schedule4 } from "effect";
2214
+ import { Effect as Effect15, ManagedRuntime, Schema as Schema17, Schedule as Schedule4 } from "effect";
2215
2215
  import { createHash as createHash3, timingSafeEqual } from "node:crypto";
2216
2216
  import { access as access6, chmod as chmod8 } from "node:fs/promises";
2217
2217
  import { createServer } from "node:http";
@@ -2314,7 +2314,7 @@ Content-Length: 0\r
2314
2314
  }, body = (request, decode3) => Effect15.tryPromise({
2315
2315
  try: () => request.json(),
2316
2316
  catch: () => new RequestError({ status: 400, message: "Expected a JSON request body" })
2317
- }).pipe(Effect15.flatMap(decode3), Effect15.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) => Schema16.decodeUnknownEffect(schema)(value).pipe(Effect15.mapError(() => new RequestError({ status: 400, message: `Invalid identifier: ${value}` }))), scoreDevice = (device, input, sourceDeviceId) => {
2317
+ }).pipe(Effect15.flatMap(decode3), Effect15.mapError((cause) => cause instanceof RequestError ? cause : new RequestError({ status: 400, message: String(cause).slice(0, 2048) }))), pathId = (schema, value) => Schema17.decodeUnknownEffect(schema)(value).pipe(Effect15.mapError(() => new RequestError({ status: 400, message: `Invalid identifier: ${value}` }))), scoreDevice = (device, input, sourceDeviceId) => {
2318
2318
  const prompt = `${input.prompt} ${input.context ?? ""}`.toLowerCase();
2319
2319
  const needsXcode = /\b(xcode|ios|macos|swiftui|app store)\b/.test(prompt);
2320
2320
  const needsBrowser = /\b(browser|chrome|youtube|twitter|logged[- ]?in|signed[- ]?in|x\.com)\b/.test(prompt);
@@ -2713,6 +2713,14 @@ Content-Length: 0\r
2713
2713
  return yield* store.exchangePairing(input.token);
2714
2714
  })).then((value) => json(value, 201)).catch(() => json({ error: "Pairing credential is invalid, expired, or already used" }, 401));
2715
2715
  }
2716
+ if (url.pathname === "/api/device/credential" && request.method === "GET") {
2717
+ const authorization = request.headers.get("authorization");
2718
+ const token = authorization?.startsWith("Bearer ") === true ? authorization.slice("Bearer ".length) : undefined;
2719
+ if (token === undefined || await authenticate(token, "device") === undefined) {
2720
+ return json({ error: "Unauthorized" }, 401);
2721
+ }
2722
+ return json({ ok: true });
2723
+ }
2716
2724
  const attachmentRoute = request.method === "GET" && /^\/api\/tasks\/[^/]+\/attachments(?:\/[^/]+)?$/.test(url.pathname);
2717
2725
  const progressRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/progress$/.test(url.pathname);
2718
2726
  const inputRoute = request.method === "POST" && /^\/api\/tasks\/[^/]+\/input$/.test(url.pathname);
@@ -3048,9 +3056,9 @@ var init_main = __esm(async () => {
3048
3056
  init_config();
3049
3057
  init_hub();
3050
3058
  init_store();
3051
- RequestError = class RequestError extends Schema16.TaggedErrorClass()("Relay.RequestError", {
3052
- status: Schema16.Int,
3053
- message: Schema16.String
3059
+ RequestError = class RequestError extends Schema17.TaggedErrorClass()("Relay.RequestError", {
3060
+ status: Schema17.Int,
3061
+ message: Schema17.String
3054
3062
  }) {
3055
3063
  };
3056
3064
  maxTaskRequestBodyBytes = 3 * 1024 * 1024;
@@ -3174,6 +3182,7 @@ var make = (options) => {
3174
3182
  });
3175
3183
  const devicePage = (after) => request("RelayClient.devices", `/api/devices/page${after === undefined ? "" : `?after=${encodeURIComponent(after)}`}`, DevicePage, undefined, maxDevicePageResponseBytes);
3176
3184
  return Service.of({
3185
+ verifyDeviceCredential: () => request("RelayClient.verifyDeviceCredential", "/api/device/credential", Schema2.Struct({ ok: Schema2.Literal(true) })).pipe(Effect2.asVoid),
3177
3186
  devices: () => Effect2.gen(function* () {
3178
3187
  const first = yield* devicePage().pipe(Effect2.catch((cause) => cause instanceof RelayRequestError && cause.status === 404 ? request("RelayClient.devices", "/api/devices", Schema2.Array(Device)) : Effect2.fail(cause)));
3179
3188
  if (!("devices" in first))
@@ -3317,29 +3326,30 @@ var make = (options) => {
3317
3326
  };
3318
3327
  var exchangePairing = (baseUrl, input) => Effect2.gen(function* () {
3319
3328
  const operation = "RelayClient.exchangePairing";
3320
- const response = yield* Effect2.tryPromise({
3321
- try: (signal) => fetch(`${normalizeBaseUrl(baseUrl)}/api/auth/pair`, {
3322
- method: "POST",
3323
- signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
3324
- headers: { "content-type": "application/json" },
3325
- body: JSON.stringify(input)
3326
- }),
3327
- catch: (cause) => new RelayRequestError({
3329
+ const json = yield* Effect2.tryPromise({
3330
+ try: async (signal) => {
3331
+ const response = await fetch(`${normalizeBaseUrl(baseUrl)}/api/auth/pair`, {
3332
+ method: "POST",
3333
+ signal: AbortSignal.any([signal, AbortSignal.timeout(30000)]),
3334
+ headers: { "content-type": "application/json" },
3335
+ body: JSON.stringify(input)
3336
+ });
3337
+ if (!response.ok) {
3338
+ throw new RelayRequestError({
3339
+ operation,
3340
+ message: await responseMessage(response, operation),
3341
+ status: response.status
3342
+ });
3343
+ }
3344
+ return jsonBody(response, operation).catch((cause) => {
3345
+ throw cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) });
3346
+ });
3347
+ },
3348
+ catch: (cause) => cause instanceof RelayRequestError || cause instanceof RelayDecodeError ? cause : new RelayRequestError({
3328
3349
  operation,
3329
3350
  message: cause instanceof Error ? cause.message : String(cause)
3330
3351
  })
3331
3352
  });
3332
- if (!response.ok) {
3333
- return yield* Effect2.fail(new RelayRequestError({
3334
- operation,
3335
- message: yield* Effect2.promise(() => responseMessage(response, operation)),
3336
- status: response.status
3337
- }));
3338
- }
3339
- const json = yield* Effect2.tryPromise({
3340
- try: () => jsonBody(response, operation),
3341
- catch: (cause) => cause instanceof RelayDecodeError ? cause : new RelayDecodeError({ operation, message: String(cause) })
3342
- });
3343
3353
  return yield* Schema2.decodeUnknownEffect(PairingResult)(json).pipe(Effect2.mapError((cause) => new RelayDecodeError({ operation, message: String(cause) })));
3344
3354
  });
3345
3355
  var layer = (options) => Layer.succeed(Service, make(options));
@@ -3893,10 +3903,10 @@ var run = (options) => Effect3.callback((resume, signal) => {
3893
3903
  });
3894
3904
 
3895
3905
  // apps/device/src/cli.ts
3896
- import { Effect as Effect10, Schema as Schema11 } from "effect";
3897
- import { access as access4, mkdir as mkdir7, open as open4, stat as stat4, writeFile as writeFile7 } from "node:fs/promises";
3906
+ import { Effect as Effect10, Schema as Schema12 } from "effect";
3907
+ import { access as access4, mkdir as mkdir8, open as open5, stat as stat4, writeFile as writeFile8 } from "node:fs/promises";
3898
3908
  import { homedir as homedir4 } from "node:os";
3899
- import { dirname as dirname6, join as join8, resolve as resolve4 } from "node:path";
3909
+ import { dirname as dirname6, join as join9, resolve as resolve4 } from "node:path";
3900
3910
 
3901
3911
  // skills/cohall/SKILL.md
3902
3912
  var SKILL_default = `---
@@ -4162,6 +4172,125 @@ cohall devices
4162
4172
  Return the diagnostic failure to the user rather than claiming remote work ran.
4163
4173
  `;
4164
4174
 
4175
+ // docs/onboarding.md
4176
+ var onboarding_default = `# Set up Cohall with your agent
4177
+
4178
+ These instructions are for the agent doing the setup. Read the path that matches
4179
+ the owner's request, complete the verification steps, and report what works.
4180
+ \`cohall onboard\` prints this same guide without changing the machine.
4181
+
4182
+ ## Choose the role
4183
+
4184
+ - A relay address and pairing token mean **join an existing relay**.
4185
+ - A request to host a relay means **host a relay** on an always-on machine.
4186
+ - If neither is clear, ask whether this machine should host or join, and obtain
4187
+ the relay address before pairing. A laptop that sleeps is a poor relay host.
4188
+ - For an existing installation, inspect \`cohall config\` and run \`cohall doctor\`
4189
+ before repairing it. Reuse working credentials and workspace settings.
4190
+
4191
+ Detect the OS, Node.js version, package manager, installed provider CLIs, and
4192
+ existing workspace roots. Cohall requires Node.js 24 or newer. Follow
4193
+ [installation](https://github.com/AksharP5/cohall/blob/main/docs/install.md) for package commands and
4194
+ [services](https://github.com/AksharP5/cohall/blob/main/docs/services.md) for the current OS. Use the owner's authorization for
4195
+ installation and autostart; obtain it when missing. Provider login requires the
4196
+ owner's account and should stay on the machine that runs that provider.
4197
+
4198
+ Keep tokens out of command arguments, shell history, logs, and setup briefs.
4199
+ Read them through a hidden terminal prompt, stdin, or a private token file.
4200
+
4201
+ ## Host a relay
4202
+
4203
+ 1. Choose a machine that stays awake and a persistent relay data directory.
4204
+ Install Cohall globally so its service has a stable executable path.
4205
+ 2. Start \`cohall relay\` once in the foreground. The default listener is
4206
+ \`127.0.0.1:8787\`; the relay creates a private owner token in its data directory
4207
+ when none is supplied. Preserve that directory and include it in backups.
4208
+ 3. Make the relay reachable through the owner's private Tailscale network or
4209
+ HTTPS reverse proxy. Confirm the address from a joining machine with
4210
+ \`GET /api/health\`. Plain HTTP is suitable only inside an independently
4211
+ encrypted private network, never as a public listener.
4212
+ 4. Configure relay autostart. The packaged relay service instructions cover
4213
+ Linux; macOS and Windows relay hosts need an owner-approved supervisor.
4214
+ \`cohall service install\` installs a **device worker**, not a relay.
4215
+ Prepare the managed relay with the same data and owner configuration. Stop
4216
+ the foreground relay to release its address, start the managed service, and
4217
+ verify relay health before pairing devices.
4218
+ 5. On the relay host, create a ten-minute, single-use pairing token for each
4219
+ joining machine:
4220
+
4221
+ \`\`\`bash
4222
+ cohall pair --label "Workstation"
4223
+ \`\`\`
4224
+
4225
+ Set \`COHALL_RELAY_URL\` to the reachable address when creating the pairing so
4226
+ its \`join_instructions\` use that address. The owner token is read from the
4227
+ relay data directory on the host; a service using a different user or data
4228
+ directory needs the matching owner configuration. Transfer the pairing token
4229
+ privately and send the returned join instructions separately.
4230
+
4231
+ 6. If this host should also run agent work, pair it as another device and
4232
+ complete the joining path below. Hosting a relay alone does not register a
4233
+ worker or install its agent integration.
4234
+
4235
+ ## Join an existing relay
4236
+
4237
+ 1. Confirm the relay address and the role. A worker runs providers against its
4238
+ own allowed workspaces; a client-only installation submits work elsewhere.
4239
+ Request a fresh token if the supplied one has expired or was already used.
4240
+ 2. Choose existing workspace roots and installed providers for a worker.
4241
+ Confirm provider login on this machine. Use \`--providers codex\`,
4242
+ \`--providers claude-code\`, or \`--providers opencode\` to limit selection;
4243
+ \`auto\` enables every detected provider. Grok Bots have a separate
4244
+ [gateway setup](https://github.com/AksharP5/cohall/blob/main/docs/grok-bot.md).
4245
+ 3. Run guided setup with the confirmed address:
4246
+
4247
+ \`\`\`bash
4248
+ cohall init --relay https://your-relay.example
4249
+ \`\`\`
4250
+
4251
+ A terminal prompts for the pairing token without echoing it. Scripts and
4252
+ agents without an interactive terminal supply it on stdin or with
4253
+ \`--token-file\`; see the [pairing examples](https://github.com/AksharP5/cohall/blob/main/docs/install.md#pair-a-machine).
4254
+ Client-only setup adds \`--client-only\` and uses a client-only pairing.
4255
+ \`init\` writes configuration and installs the Cohall skill for Codex,
4256
+ Claude Code, and OpenCode. It preserves existing settings during repair.
4257
+
4258
+ 4. For a worker, run \`cohall device\` in the foreground or install its service
4259
+ after a global package installation. \`cohall init --service\` can install the
4260
+ worker service during setup. A client-only installation needs no device
4261
+ service. For Grok Bots, use the gateway instructions to verify availability.
4262
+ 5. Use the CLI and installed skill in a harness that can run commands. If the
4263
+ owner prefers MCP, run \`cohall integrations\` and follow the entry for the
4264
+ actual harness in [agent integrations](https://github.com/AksharP5/cohall/blob/main/docs/integrations.md). Configure exactly
4265
+ one Cohall server for that configuration, restart or reconnect the harness,
4266
+ and ask it to call \`list_devices\`. Installing a skill does not configure MCP.
4267
+
4268
+ ## Verify before calling setup complete
4269
+
4270
+ 1. Run \`cohall doctor\`. Confirm relay access and client authentication. A worker
4271
+ should be online or busy with the intended providers and workspaces.
4272
+ \`doctor --all\` checks the other registered workers.
4273
+ 2. If using MCP, check the real host connection in the doctor report after
4274
+ asking the harness to call \`list_devices\`. A successful server self-test
4275
+ alone does not establish that the harness loaded it.
4276
+ 3. From the requesting client, run a harmless delegation to the intended
4277
+ worker, using its target from \`cohall devices\` and an advertised provider:
4278
+
4279
+ \`\`\`bash
4280
+ cohall delegate --target @workstation --provider codex --timeout 60 \\
4281
+ "Reply with exactly cohall-ready. Do not inspect or change files."
4282
+ \`\`\`
4283
+
4284
+ Require a completed result containing \`cohall-ready\`. Provider detection
4285
+ alone does not prove authentication or execution works. For a Grok Bot,
4286
+ use its target from \`cohall bots\` and \`--provider grok-bot\`.
4287
+
4288
+ 4. Report the relay host/address, this machine's role, selected providers and
4289
+ workspaces, autostart state, integration used, and checks that passed.
4290
+ Identify any remaining login, permission, or connectivity step. Include no
4291
+ tokens or private task content.
4292
+ `;
4293
+
4165
4294
  // apps/device/src/config.ts
4166
4295
  init_src();
4167
4296
  import { Effect as Effect4, Schema as Schema4 } from "effect";
@@ -5229,9 +5358,15 @@ var terminalPrompter = () => {
5229
5358
  close: () => terminal.close()
5230
5359
  };
5231
5360
  };
5361
+ var setupRelayUrl = (input) => {
5362
+ if (input === undefined || input.trim().length === 0) {
5363
+ throw new Error("Relay URL is required. Run `cohall onboard` to choose a relay host or join an existing relay.");
5364
+ }
5365
+ return normalizeRelayUrl(input);
5366
+ };
5232
5367
  var guidedSetupInput = async (options, prompter) => {
5233
5368
  const existing = await readStoredConfiguration();
5234
- const relayUrl = normalizeRelayUrl(options.relayUrl ?? await prompter.answer("Relay URL", existing?.relayUrl ?? "http://127.0.0.1:8787"));
5369
+ const relayUrl = setupRelayUrl(options.relayUrl ?? await prompter.answer("Relay URL", existing?.relayUrl ?? ""));
5235
5370
  const credentials = credentialsForRelay(existing, relayUrl);
5236
5371
  const hasRequiredCredentials = credentials.clientToken !== undefined && (options.clientOnly || credentials.deviceToken !== undefined);
5237
5372
  let workspaces = options.clientOnly ? existing?.workspaces ?? [] : options.workspaces;
@@ -6489,7 +6624,7 @@ var isLoopback = (url) => url.hostname === "localhost" || url.hostname === "[::1
6489
6624
  var verifyClientCredential = async (relayUrl, token) => {
6490
6625
  await Effect8.runPromise(exports_src.make({ baseUrl: relayUrl, token }).devices());
6491
6626
  };
6492
- var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, reject) => {
6627
+ var verifyLegacyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, reject) => {
6493
6628
  const socket = new WebSocket(websocketUrl(relayUrl), {
6494
6629
  handshakeTimeout: 1e4,
6495
6630
  maxPayload: 1024 * 1024,
@@ -6528,6 +6663,12 @@ var verifyDeviceCredential = (relayUrl, token) => new Promise((resolvePromise, r
6528
6663
  socket.once("error", (cause) => finish(cause));
6529
6664
  socket.once("close", () => finish(new Error("New relay rejected the stored device credential")));
6530
6665
  });
6666
+ var verifyDeviceCredential = (relayUrl, token) => Effect8.runPromise(exports_src.make({ baseUrl: relayUrl, token }).verifyDeviceCredential()).catch((cause) => {
6667
+ if (cause instanceof RelayRequestError && (cause.status === 401 || cause.status === 404)) {
6668
+ return verifyLegacyDeviceCredential(relayUrl, token);
6669
+ }
6670
+ throw cause;
6671
+ });
6531
6672
  var switchRelay = async (options) => {
6532
6673
  if (process.env.COHALL_RELAY_URL !== undefined) {
6533
6674
  throw new Error("Unset COHALL_RELAY_URL before changing the stored relay URL");
@@ -6623,7 +6764,7 @@ var readInputAttachments = async (paths2) => {
6623
6764
  if (!source.isFile()) {
6624
6765
  throw new Error(`Attachment ${name} must be a regular file`);
6625
6766
  }
6626
- const handle = await open3(path, constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW));
6767
+ const handle = await open3(path, constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW | constants3.O_NONBLOCK));
6627
6768
  try {
6628
6769
  const metadata = await handle.stat();
6629
6770
  if (!metadata.isFile() || metadata.dev !== source.dev || metadata.ino !== source.ino || metadata.size < 1 || metadata.size > maxAttachmentBytes) {
@@ -6680,7 +6821,7 @@ var collectOutputs = async (directory) => {
6680
6821
  throw new Error(`Output ${name} repeats another file name`);
6681
6822
  }
6682
6823
  names.add(key);
6683
- const handle = await open3(join7(directory, name), constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW));
6824
+ const handle = await open3(join7(directory, name), constants3.O_RDONLY | (process.platform === "win32" ? 0 : constants3.O_NOFOLLOW | constants3.O_NONBLOCK));
6684
6825
  try {
6685
6826
  const metadata = await handle.stat();
6686
6827
  const pathMetadata = await lstat2(join7(directory, name));
@@ -6706,6 +6847,248 @@ import {
6706
6847
  getDefaultEnvironment,
6707
6848
  StdioClientTransport
6708
6849
  } from "@modelcontextprotocol/sdk/client/stdio.js";
6850
+
6851
+ // apps/device/src/mcp-host-diagnostics.ts
6852
+ init_src();
6853
+ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
6854
+ import { Schema as Schema11 } from "effect";
6855
+ import { randomUUID as randomUUID2 } from "node:crypto";
6856
+ import { constants as constants4 } from "node:fs";
6857
+ import { lstat as lstat3, mkdir as mkdir7, open as open4, opendir, rename as rename4, rm as rm5, writeFile as writeFile7 } from "node:fs/promises";
6858
+ import { join as join8 } from "node:path";
6859
+ var maxRecords = 8;
6860
+ var maxRecordBytes = 4096;
6861
+ var retentionMs = 7 * 24 * 60 * 60 * 1000;
6862
+ var recordName = /^[\da-f]{8}-[\da-f]{4}-4[\da-f]{3}-[89ab][\da-f]{3}-[\da-f]{12}\.json$/;
6863
+ var text3 = Schema11.String.check(Schema11.isMaxLength(128));
6864
+ var count2 = Schema11.Int.check(Schema11.isBetween({ minimum: 0, maximum: Number.MAX_SAFE_INTEGER }));
6865
+ var McpHostSession = Schema11.Struct({
6866
+ schema_version: Schema11.Literal(1),
6867
+ launch_id: Schema11.String.check(Schema11.isUUID(4)),
6868
+ launched_at: Timestamp,
6869
+ server_version: text3,
6870
+ initialized_at: Schema11.optionalKey(Timestamp),
6871
+ client: Schema11.optionalKey(Schema11.Struct({ name: text3, version: text3 })),
6872
+ tools_list: Schema11.optionalKey(Schema11.Struct({ at: Timestamp, tool_count: count2 })),
6873
+ tools_call_count: count2,
6874
+ last_tool_call_at: Schema11.optionalKey(Timestamp),
6875
+ closed_at: Schema11.optionalKey(Timestamp)
6876
+ });
6877
+ var storagePath = (configPath) => `${configPath}.mcp-hosts`;
6878
+ var recordsIn = async (directory) => {
6879
+ const records = [];
6880
+ let entries = 0;
6881
+ for await (const entry of await opendir(directory)) {
6882
+ if (++entries > 64)
6883
+ throw new Error("MCP host diagnostics directory exceeded its entry limit");
6884
+ if (!recordName.test(entry.name))
6885
+ continue;
6886
+ const path = join8(directory, entry.name);
6887
+ const metadata = await lstat3(path).catch((cause) => {
6888
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT")
6889
+ return;
6890
+ throw cause;
6891
+ });
6892
+ if (metadata !== undefined)
6893
+ records.push({ path, modified: metadata.mtimeMs });
6894
+ }
6895
+ return records.sort((left, right) => right.modified - left.modified || left.path.localeCompare(right.path));
6896
+ };
6897
+ var readMcpHostDiagnostics = async (configPath = configurationPath()) => {
6898
+ const path = storagePath(configPath);
6899
+ const warnings = [];
6900
+ let records;
6901
+ try {
6902
+ records = await recordsIn(path);
6903
+ } catch (cause) {
6904
+ if (cause instanceof Error && "code" in cause && cause.code === "ENOENT") {
6905
+ return { status: "not_observed", path, sessions: [], warnings };
6906
+ }
6907
+ return {
6908
+ status: "unavailable",
6909
+ path,
6910
+ sessions: [],
6911
+ warnings,
6912
+ error: `Cannot read MCP host diagnostics at ${path}. Restore access to this diagnostics directory, then restart the Cohall MCP connection in your host.`
6913
+ };
6914
+ }
6915
+ const sessions = [];
6916
+ let invalid = 0;
6917
+ for (const record2 of records.filter((record3) => record3.modified >= Date.now() - retentionMs).slice(0, maxRecords)) {
6918
+ try {
6919
+ const handle = await open4(record2.path, constants4.O_RDONLY | (process.platform === "win32" ? 0 : constants4.O_NOFOLLOW | constants4.O_NONBLOCK));
6920
+ try {
6921
+ const metadata = await handle.stat();
6922
+ if (!metadata.isFile() || metadata.size > maxRecordBytes)
6923
+ throw new Error("Invalid record");
6924
+ const data = await readBoundedFile(handle, maxRecordBytes);
6925
+ if (data.length > maxRecordBytes)
6926
+ throw new Error("Invalid record");
6927
+ sessions.push(Schema11.decodeUnknownSync(McpHostSession)(JSON.parse(data.toString("utf8"))));
6928
+ } finally {
6929
+ await handle.close();
6930
+ }
6931
+ } catch (cause) {
6932
+ if (!(cause instanceof Error && ("code" in cause) && cause.code === "ENOENT"))
6933
+ invalid += 1;
6934
+ }
6935
+ }
6936
+ if (invalid > 0) {
6937
+ warnings.push(`${invalid} MCP host diagnostics record(s) could not be read. Reset the diagnostics directory ${path}, then restart the Cohall MCP connection in your host.`);
6938
+ }
6939
+ if (sessions.length === 0 && invalid > 0) {
6940
+ return {
6941
+ status: "unavailable",
6942
+ path,
6943
+ sessions,
6944
+ warnings,
6945
+ error: "No valid MCP host diagnostics records could be read"
6946
+ };
6947
+ }
6948
+ const latest = sessions[0];
6949
+ if (latest !== undefined) {
6950
+ if (latest.initialized_at === undefined) {
6951
+ warnings.push(`${latest.closed_at === undefined ? "An MCP launch was recorded" : "A recorded MCP launch ended"} without client initialization. If your host still lacks Cohall tools, check its MCP command and selected config, then restart its Cohall connection.`);
6952
+ } else if (latest.tools_list?.tool_count === 0) {
6953
+ warnings.push("A recorded MCP launch returned an empty tools list. If Cohall tools are missing, check the host's Cohall MCP command and update or restart its connection.");
6954
+ } else if (latest.tools_list === undefined && latest.tools_call_count === 0) {
6955
+ warnings.push("An MCP host initialized Cohall but no tool discovery or calls were observed. If Cohall tools are missing, refresh or restart its MCP connection in that host.");
6956
+ }
6957
+ }
6958
+ return {
6959
+ status: sessions.some((session) => session.initialized_at !== undefined) ? "observed" : "not_observed",
6960
+ path,
6961
+ sessions,
6962
+ warnings
6963
+ };
6964
+ };
6965
+ var createMcpHostDiagnostics = (server) => {
6966
+ const transport = new StdioServerTransport;
6967
+ if (process.env.COHALL_MCP_SELF_TEST === "1")
6968
+ return transport;
6969
+ const directory = storagePath(configurationPath());
6970
+ let session = McpHostSession.make({
6971
+ schema_version: 1,
6972
+ launch_id: randomUUID2(),
6973
+ launched_at: now(),
6974
+ server_version: version,
6975
+ tools_call_count: 0
6976
+ });
6977
+ const path = join8(directory, `${session.launch_id}.json`);
6978
+ let dirty = true;
6979
+ let disabled = false;
6980
+ let stopped = false;
6981
+ let writing;
6982
+ let timer;
6983
+ const schedule = () => {
6984
+ if (disabled || stopped || timer !== undefined)
6985
+ return;
6986
+ timer = setTimeout(() => {
6987
+ timer = undefined;
6988
+ flush();
6989
+ }, 250);
6990
+ timer.unref();
6991
+ };
6992
+ const flush = () => {
6993
+ if (writing !== undefined)
6994
+ return writing;
6995
+ if (!dirty || disabled)
6996
+ return Promise.resolve();
6997
+ dirty = false;
6998
+ const snapshot = session;
6999
+ writing = (async () => {
7000
+ await mkdir7(directory, { recursive: true, mode: 448 });
7001
+ await recordsIn(directory);
7002
+ const temporary = `${path}.tmp`;
7003
+ try {
7004
+ const json = `${JSON.stringify(snapshot)}
7005
+ `;
7006
+ if (Buffer.byteLength(json) > maxRecordBytes)
7007
+ throw new Error("Record exceeded its limit");
7008
+ await writeFile7(temporary, json, { flag: "wx", mode: 384 });
7009
+ await rename4(temporary, path);
7010
+ } finally {
7011
+ await rm5(temporary, { force: true });
7012
+ }
7013
+ const records = await recordsIn(directory);
7014
+ const expired = records.filter((record2, index) => index >= maxRecords || record2.modified < Date.now() - retentionMs);
7015
+ await Promise.all(expired.map((record2) => rm5(record2.path, { force: true })));
7016
+ })().catch((cause) => {
7017
+ disabled = true;
7018
+ const reason = cause instanceof Error ? "code" in cause && typeof cause.code === "string" ? cause.code : cause.message : "Unknown filesystem error";
7019
+ console.error(`Cohall MCP host diagnostics are unavailable (${reason.replace(/[\r\n]/g, " ").slice(0, 160)}). Check diagnostics storage access; MCP tools remain available.`);
7020
+ }).finally(() => {
7021
+ writing = undefined;
7022
+ if (dirty)
7023
+ schedule();
7024
+ });
7025
+ return writing;
7026
+ };
7027
+ const changed = () => {
7028
+ dirty = true;
7029
+ schedule();
7030
+ };
7031
+ const shutdown = async () => {
7032
+ if (stopped)
7033
+ return;
7034
+ stopped = true;
7035
+ clearTimeout(timer);
7036
+ session = { ...session, closed_at: now() };
7037
+ dirty = true;
7038
+ await writing;
7039
+ await flush();
7040
+ };
7041
+ flush();
7042
+ process.once("beforeExit", () => void shutdown());
7043
+ const initialized = server.oninitialized;
7044
+ server.oninitialized = () => {
7045
+ initialized?.();
7046
+ const client = server.getClientVersion();
7047
+ session = {
7048
+ ...session,
7049
+ initialized_at: now(),
7050
+ ...client === undefined ? {} : { client: { name: client.name.slice(0, 128), version: client.version.slice(0, 128) } }
7051
+ };
7052
+ changed();
7053
+ };
7054
+ const closed = server.onclose;
7055
+ server.onclose = () => {
7056
+ closed?.();
7057
+ shutdown();
7058
+ };
7059
+ const pendingLists = new Set;
7060
+ const start = transport.start.bind(transport);
7061
+ transport.start = async () => {
7062
+ const receive = transport.onmessage;
7063
+ transport.onmessage = (message) => {
7064
+ if ("id" in message && message.id !== undefined && "method" in message) {
7065
+ if (message.method === "tools/list" && pendingLists.size < 32)
7066
+ pendingLists.add(message.id);
7067
+ if (message.method === "tools/call") {
7068
+ session = {
7069
+ ...session,
7070
+ tools_call_count: Math.min(session.tools_call_count + 1, Number.MAX_SAFE_INTEGER),
7071
+ last_tool_call_at: now()
7072
+ };
7073
+ changed();
7074
+ }
7075
+ }
7076
+ receive?.(message);
7077
+ };
7078
+ await start();
7079
+ };
7080
+ const send = transport.send.bind(transport);
7081
+ transport.send = async (message) => {
7082
+ await send(message);
7083
+ if ("id" in message && message.id !== undefined && pendingLists.delete(message.id) && "result" in message && Array.isArray(message.result.tools)) {
7084
+ session = { ...session, tools_list: { at: now(), tool_count: message.result.tools.length } };
7085
+ changed();
7086
+ }
7087
+ };
7088
+ return transport;
7089
+ };
7090
+
7091
+ // apps/device/src/mcp-diagnostics.ts
6709
7092
  var checkMcp = async (entrypoint, relayUrl, token) => {
6710
7093
  const client = new Client({ name: "cohall-doctor", version });
6711
7094
  const transport = new StdioClientTransport({
@@ -6714,6 +7097,7 @@ var checkMcp = async (entrypoint, relayUrl, token) => {
6714
7097
  env: {
6715
7098
  ...getDefaultEnvironment(),
6716
7099
  COHALL_CONFIG: configurationPath(),
7100
+ COHALL_MCP_SELF_TEST: "1",
6717
7101
  COHALL_RELAY_URL: relayUrl,
6718
7102
  COHALL_CLIENT_TOKEN: token
6719
7103
  },
@@ -6784,6 +7168,7 @@ var aliases = new Map([
6784
7168
  var help = `Cohall connects agents running on your own devices.
6785
7169
 
6786
7170
  Usage:
7171
+ cohall onboard
6787
7172
  cohall init [--relay url] [--workspace path] [--providers list] [--service]
6788
7173
  cohall join --relay <url> --workspace <path> [--providers list] [--token-file <path>]
6789
7174
  cohall configure [--relay url] [--name name] [--workspace path]
@@ -6914,7 +7299,7 @@ var printLine = (value) => console.log(JSON.stringify(value));
6914
7299
  var readTextFile = async (path, limit, exceeded) => {
6915
7300
  if ((await stat4(path)).size > limit)
6916
7301
  throw new Error(exceeded);
6917
- const file = await open4(path, "r");
7302
+ const file = await open5(path, "r");
6918
7303
  try {
6919
7304
  const data = await readBoundedFile(file, limit);
6920
7305
  if (data.length > limit)
@@ -6989,17 +7374,17 @@ var ownerClient = async () => {
6989
7374
  };
6990
7375
  var installSkill = async (target) => {
6991
7376
  const roots = {
6992
- agents: join8(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
6993
- claude: join8(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
6994
- opencode: join8(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
7377
+ agents: join9(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
7378
+ claude: join9(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
7379
+ opencode: join9(homedir4(), ".config", "opencode", "skills", "cohall", "SKILL.md")
6995
7380
  };
6996
7381
  const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
6997
7382
  if (selected === undefined) {
6998
7383
  throw new Error("Skill target must be agents, claude, opencode, or all");
6999
7384
  }
7000
7385
  for (const path of selected) {
7001
- await mkdir7(dirname6(path), { recursive: true });
7002
- await writeFile7(path, SKILL_default);
7386
+ await mkdir8(dirname6(path), { recursive: true });
7387
+ await writeFile8(path, SKILL_default);
7003
7388
  }
7004
7389
  return selected;
7005
7390
  };
@@ -7007,13 +7392,19 @@ var printHelp = () => console.log(help);
7007
7392
  var printSkill = () => console.log(SKILL_default.trimEnd());
7008
7393
  var runCli = async (command, raw) => {
7009
7394
  const arguments_ = parseArguments(raw);
7395
+ if (command === "onboard") {
7396
+ allowOptions(arguments_, []);
7397
+ noPositionals(arguments_, command);
7398
+ console.log(onboarding_default.trimEnd());
7399
+ return;
7400
+ }
7010
7401
  if (command === "reply") {
7011
7402
  allowOptions(arguments_, ["message", "message-file", "error", "question", "run-id"]);
7012
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7403
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7013
7404
  const error = option(arguments_, "error");
7014
7405
  const question = option(arguments_, "question");
7015
7406
  const rawRunId = option(arguments_, "run-id");
7016
- const runId = rawRunId === undefined ? undefined : Schema11.decodeUnknownSync(TaskRunId)(rawRunId);
7407
+ const runId = rawRunId === undefined ? undefined : Schema12.decodeUnknownSync(TaskRunId)(rawRunId);
7017
7408
  if ((error !== undefined || question !== undefined) && (option(arguments_, "message") !== undefined || option(arguments_, "message-file") !== undefined || error !== undefined && question !== undefined)) {
7018
7409
  throw new Error("Use one of --question, --error, or --message/--message-file");
7019
7410
  }
@@ -7021,7 +7412,7 @@ var runCli = async (command, raw) => {
7021
7412
  if (error === undefined && question === undefined && message === undefined) {
7022
7413
  throw new Error("A bot reply requires --message, --message-file, --error, or --question");
7023
7414
  }
7024
- const reply = question !== undefined ? { question: Schema11.decodeUnknownSync(RequestTaskInput.fields.question)(question) } : error === undefined ? { result: message ?? "" } : { error };
7415
+ const reply = question !== undefined ? { question: Schema12.decodeUnknownSync(RequestTaskInput.fields.question)(question) } : error === undefined ? { result: message ?? "" } : { error };
7025
7416
  await writeBotReply(id, reply, runId);
7026
7417
  print({ submitted: true, task_id: id });
7027
7418
  return;
@@ -7037,6 +7428,9 @@ var runCli = async (command, raw) => {
7037
7428
  "workspace"
7038
7429
  ]);
7039
7430
  noPositionals(arguments_, command);
7431
+ if (arguments_.options.has("client-only") && arguments_.options.has("service")) {
7432
+ throw new Error("A client-only setup cannot install a device service");
7433
+ }
7040
7434
  const token = option(arguments_, "token-file") === undefined ? undefined : await pairingToken(arguments_);
7041
7435
  const interactive = process.stdin.isTTY === true && process.stderr.isTTY === true;
7042
7436
  const relayUrl = option(arguments_, "relay");
@@ -7095,7 +7489,7 @@ var runCli = async (command, raw) => {
7095
7489
  workspaces: result.configuration.workspaces,
7096
7490
  skills: installedSkills,
7097
7491
  ...service === undefined ? {} : { service },
7098
- 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."
7492
+ next: service === undefined && !arguments_.options.has("client-only") ? "Run `cohall service install` for background availability, or `cohall device` now. Then run `cohall doctor` and the delegation check in `cohall onboard`." : "Run `cohall doctor`, then the delegation check in `cohall onboard`."
7099
7493
  });
7100
7494
  } finally {
7101
7495
  prompter.close();
@@ -7187,7 +7581,7 @@ var runCli = async (command, raw) => {
7187
7581
  if (arguments_.positionals.length !== 2 || arguments_.positionals[0] !== "abandon") {
7188
7582
  throw new Error("Usage: cohall upgrades [abandon <operation-id>]");
7189
7583
  }
7190
- print(await Effect10.runPromise(relay2.abandonOperation(Schema11.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
7584
+ print(await Effect10.runPromise(relay2.abandonOperation(Schema12.decodeUnknownSync(OperationId)(arguments_.positionals[1]))));
7191
7585
  return;
7192
7586
  }
7193
7587
  print(await Effect10.runPromise(relay2.operations()));
@@ -7203,9 +7597,9 @@ var runCli = async (command, raw) => {
7203
7597
  "workspace"
7204
7598
  ]);
7205
7599
  noPositionals(arguments_, command);
7206
- const token = await pairingToken(arguments_);
7207
- const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? "http://127.0.0.1:8787");
7208
7600
  const existing = await readStoredConfiguration();
7601
+ const relayUrl = setupRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl);
7602
+ const token = await pairingToken(arguments_);
7209
7603
  const suppliedWorkspaces = values(arguments_, "workspace");
7210
7604
  const workspaces = suppliedWorkspaces.length === 0 ? existing?.workspaces : suppliedWorkspaces;
7211
7605
  const deviceName = option(arguments_, "name");
@@ -7287,7 +7681,7 @@ var runCli = async (command, raw) => {
7287
7681
  ...model === undefined ? {} : { model },
7288
7682
  ...grokGateway === undefined ? {} : { grokGateway },
7289
7683
  ...(sandbox ?? existing?.sandbox) === undefined ? {} : {
7290
- sandbox: Schema11.decodeUnknownSync(Schema11.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
7684
+ sandbox: Schema12.decodeUnknownSync(Schema12.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
7291
7685
  }
7292
7686
  });
7293
7687
  await writeStoredConfiguration(configuration2);
@@ -7352,6 +7746,7 @@ var runCli = async (command, raw) => {
7352
7746
  return;
7353
7747
  });
7354
7748
  const clientToken = process.env.COHALL_CLIENT_TOKEN ?? storedCredentials.clientToken;
7749
+ const mcpHost = await readMcpHostDiagnostics();
7355
7750
  const mcp = clientToken === undefined ? { status: "not configured" } : await checkMcp(process.argv[1] ?? "", relayUrl, clientToken);
7356
7751
  const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
7357
7752
  const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
@@ -7373,6 +7768,8 @@ var runCli = async (command, raw) => {
7373
7768
  ...response?.ok === true ? [] : ["Relay is unreachable"],
7374
7769
  ...versionWarning === undefined ? [] : [versionWarning],
7375
7770
  ...mcp.status === "error" ? [`MCP check failed: ${mcp.error}`] : [],
7771
+ ...mcpHost.warnings,
7772
+ ...mcpHost.status === "unavailable" ? [`MCP host diagnostics unavailable: ${mcpHost.error}`] : [],
7376
7773
  ...clientAuthentication.status === "error" ? [clientAuthentication.error] : [],
7377
7774
  ...hasDeviceCredential && response?.ok === true && clientAuthentication.status !== "error" && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
7378
7775
  clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
@@ -7388,7 +7785,8 @@ var runCli = async (command, raw) => {
7388
7785
  config_path: configurationPath(),
7389
7786
  client_credential: process.env.COHALL_CLIENT_TOKEN !== undefined || storedCredentials.clientToken !== undefined,
7390
7787
  client_authentication: clientAuthentication.status === "ok" ? { status: "ok" } : clientAuthentication,
7391
- mcp,
7788
+ mcp: { ...mcp, scope: "server self-test" },
7789
+ mcp_host: mcpHost,
7392
7790
  device_credential: hasDeviceCredential,
7393
7791
  workspaces: configuration2?.workspaces ?? [],
7394
7792
  providers: providerExecutables,
@@ -7435,7 +7833,8 @@ var runCli = async (command, raw) => {
7435
7833
  relay_url: configuration2.relayUrl,
7436
7834
  pairing_token: credential.token,
7437
7835
  expires_at: credential.expiresAt,
7438
- roles
7836
+ roles,
7837
+ join_instructions: `Join my existing Cohall relay at ${configuration2.relayUrl} as ${arguments_.options.has("client-only") ? "a client that submits work only" : "a worker and client"}. Follow https://github.com/AksharP5/cohall/blob/main/docs/onboarding.md, "Join an existing relay". I will provide the one-time pairing token separately; read it through a hidden terminal prompt, stdin, or a token file. ${arguments_.options.has("client-only") ? "Use cohall init --client-only; no device service is needed." : "Choose existing workspace roots and installed providers before running cohall init."} Finish the verification steps and report what passed.`
7439
7838
  });
7440
7839
  return;
7441
7840
  }
@@ -7447,11 +7846,11 @@ var runCli = async (command, raw) => {
7447
7846
  }
7448
7847
  if (command === "forget") {
7449
7848
  allowOptions(arguments_, []);
7450
- print(await Effect10.runPromise(relay2.forgetDevice(Schema11.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
7849
+ print(await Effect10.runPromise(relay2.forgetDevice(Schema12.decodeUnknownSync(DeviceId)(identifier(arguments_, "device id")))));
7451
7850
  return;
7452
7851
  }
7453
7852
  allowOptions(arguments_, []);
7454
- print(await Effect10.runPromise(relay2.revokeAuthSession(Schema11.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
7853
+ print(await Effect10.runPromise(relay2.revokeAuthSession(Schema12.decodeUnknownSync(AuthSessionId)(identifier(arguments_, "session id")))));
7455
7854
  return;
7456
7855
  }
7457
7856
  const { configuration, relay } = await client();
@@ -7460,20 +7859,20 @@ var runCli = async (command, raw) => {
7460
7859
  const rawQuestion = option(arguments_, "question");
7461
7860
  if (rawQuestion === undefined)
7462
7861
  throw new Error("--question is required");
7463
- const question = Schema11.decodeUnknownSync(RequestTaskInput.fields.question)(rawQuestion);
7464
- const id = arguments_.positionals.length === 0 ? undefined : Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7862
+ const question = Schema12.decodeUnknownSync(RequestTaskInput.fields.question)(rawQuestion);
7863
+ const id = arguments_.positionals.length === 0 ? undefined : Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7465
7864
  const rawRunId = option(arguments_, "run-id");
7466
- const runId = rawRunId === undefined ? undefined : Schema11.decodeUnknownSync(TaskRunId)(rawRunId);
7865
+ const runId = rawRunId === undefined ? undefined : Schema12.decodeUnknownSync(TaskRunId)(rawRunId);
7467
7866
  print(await Effect10.runPromise(requestInput(relay, configuration, question, id, runId)));
7468
7867
  return;
7469
7868
  }
7470
7869
  if (command === "answer") {
7471
7870
  allowOptions(arguments_, ["request-id", "message", "message-file"]);
7472
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7871
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7473
7872
  const requestId = option(arguments_, "request-id");
7474
7873
  if (requestId === undefined)
7475
7874
  throw new Error("--request-id is required; use the input request id from status or inbox");
7476
- const input = Schema11.decodeUnknownSync(AnswerTaskInput)({
7875
+ const input = Schema12.decodeUnknownSync(AnswerTaskInput)({
7477
7876
  requestId,
7478
7877
  answer: await readInput(arguments_, "message")
7479
7878
  });
@@ -7488,8 +7887,8 @@ var runCli = async (command, raw) => {
7488
7887
  const note = option(arguments_, "message");
7489
7888
  if (note === undefined)
7490
7889
  throw new Error("--message is required");
7491
- const input = Schema11.decodeUnknownSync(TaskProgressInput)({ note });
7492
- print(await Effect10.runPromise(relay.reportTaskProgress(Schema11.decodeUnknownSync(TaskId)(rawId), input)));
7890
+ const input = Schema12.decodeUnknownSync(TaskProgressInput)({ note });
7891
+ print(await Effect10.runPromise(relay.reportTaskProgress(Schema12.decodeUnknownSync(TaskId)(rawId), input)));
7493
7892
  return;
7494
7893
  }
7495
7894
  if (command === "devices") {
@@ -7549,15 +7948,15 @@ var runCli = async (command, raw) => {
7549
7948
  const requestId = option(arguments_, "request-id");
7550
7949
  const task = await Effect10.runPromise(createDelegation(relay, configuration, {
7551
7950
  prompt,
7552
- ...requestId === undefined ? {} : { requestId: Schema11.decodeUnknownSync(TaskRequestId)(requestId) },
7951
+ ...requestId === undefined ? {} : { requestId: Schema12.decodeUnknownSync(TaskRequestId)(requestId) },
7553
7952
  ...target === undefined ? {} : { target },
7554
7953
  ...context === undefined ? {} : { context },
7555
- ...thread === undefined ? {} : { threadId: Schema11.decodeUnknownSync(ThreadId)(thread) },
7556
- ...parent === undefined ? {} : { parentTaskId: Schema11.decodeUnknownSync(TaskId)(parent) },
7954
+ ...thread === undefined ? {} : { threadId: Schema12.decodeUnknownSync(ThreadId)(thread) },
7955
+ ...parent === undefined ? {} : { parentTaskId: Schema12.decodeUnknownSync(TaskId)(parent) },
7557
7956
  ...workspace === undefined ? {} : { workspace },
7558
- ...provider === undefined ? {} : { provider: Schema11.decodeUnknownSync(Provider)(provider) },
7957
+ ...provider === undefined ? {} : { provider: Schema12.decodeUnknownSync(Provider)(provider) },
7559
7958
  ...attachments2.length === 0 ? {} : { attachments: attachments2 },
7560
- ...deadline === undefined ? {} : { expiresAt: Schema11.decodeUnknownSync(TaskDeadline)(deadline) }
7959
+ ...deadline === undefined ? {} : { expiresAt: Schema12.decodeUnknownSync(TaskDeadline)(deadline) }
7561
7960
  }));
7562
7961
  const completed = arguments_.options.has("no-wait") ? task : await Effect10.runPromise(waitForTask(relay, task, waitSeconds));
7563
7962
  print(arguments_.options.has("no-wait") ? taskResult(completed) : await acknowledgedTaskResult(relay, completed));
@@ -7568,7 +7967,7 @@ var runCli = async (command, raw) => {
7568
7967
  if (arguments_.positionals[0] === "ack") {
7569
7968
  if (arguments_.positionals.length !== 2)
7570
7969
  throw new Error("Usage: cohall inbox ack <task-id>");
7571
- const id = Schema11.decodeUnknownSync(TaskId)(arguments_.positionals[1]);
7970
+ const id = Schema12.decodeUnknownSync(TaskId)(arguments_.positionals[1]);
7572
7971
  print(await Effect10.runPromise(relay.acknowledgeCompletion(id)));
7573
7972
  return;
7574
7973
  }
@@ -7578,13 +7977,13 @@ var runCli = async (command, raw) => {
7578
7977
  }
7579
7978
  if (command === "status") {
7580
7979
  allowOptions(arguments_, []);
7581
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7980
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7582
7981
  print(taskResult(await Effect10.runPromise(relay.getTask(id))));
7583
7982
  return;
7584
7983
  }
7585
7984
  if (command === "attachments") {
7586
7985
  allowOptions(arguments_, []);
7587
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7986
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7588
7987
  print(await Effect10.runPromise(relay.listAttachments(id)));
7589
7988
  return;
7590
7989
  }
@@ -7597,18 +7996,18 @@ var runCli = async (command, raw) => {
7597
7996
  if (destination === undefined) {
7598
7997
  throw new Error("--output is required");
7599
7998
  }
7600
- const id = Schema11.decodeUnknownSync(TaskId)(arguments_.positionals[0]);
7601
- const name = Schema11.decodeUnknownSync(AttachmentName)(arguments_.positionals[1]);
7999
+ const id = Schema12.decodeUnknownSync(TaskId)(arguments_.positionals[0]);
8000
+ const name = Schema12.decodeUnknownSync(AttachmentName)(arguments_.positionals[1]);
7602
8001
  const directionInput = option(arguments_, "direction");
7603
- const direction = directionInput === undefined ? undefined : Schema11.decodeUnknownSync(AttachmentDirection)(directionInput);
8002
+ const direction = directionInput === undefined ? undefined : Schema12.decodeUnknownSync(AttachmentDirection)(directionInput);
7604
8003
  const data = await Effect10.runPromise(relay.readAttachment(id, name, direction));
7605
- await writeFile7(destination, data, { flag: "wx", mode: 384 });
8004
+ await writeFile8(destination, data, { flag: "wx", mode: 384 });
7606
8005
  print({ task_id: id, name, path: resolve4(destination), bytes: data.length });
7607
8006
  return;
7608
8007
  }
7609
8008
  if (command === "trace") {
7610
8009
  allowOptions(arguments_, ["follow"]);
7611
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
8010
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7612
8011
  if (arguments_.options.has("follow")) {
7613
8012
  await Effect10.runPromise(followTaskTrace(relay, id, printLine));
7614
8013
  return;
@@ -7618,19 +8017,19 @@ var runCli = async (command, raw) => {
7618
8017
  }
7619
8018
  if (command === "wait") {
7620
8019
  allowOptions(arguments_, ["timeout"]);
7621
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
8020
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7622
8021
  print(taskResult(await Effect10.runPromise(waitForTask(relay, id, timeout(arguments_)))));
7623
8022
  return;
7624
8023
  }
7625
8024
  if (command === "cancel") {
7626
8025
  allowOptions(arguments_, []);
7627
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
8026
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7628
8027
  print(taskResult(await Effect10.runPromise(relay.cancelTask(id))));
7629
8028
  return;
7630
8029
  }
7631
8030
  if (command === "thread") {
7632
8031
  allowOptions(arguments_, []);
7633
- const id = Schema11.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
8032
+ const id = Schema12.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
7634
8033
  print(await Effect10.runPromise(threadContext(relay, id)));
7635
8034
  return;
7636
8035
  }
@@ -7639,15 +8038,15 @@ var runCli = async (command, raw) => {
7639
8038
 
7640
8039
  // apps/device/src/daemon.ts
7641
8040
  init_src();
7642
- import { Effect as Effect11, Schedule as Schedule3, Schema as Schema12 } from "effect";
7643
- import { constants as constants4 } from "node:fs";
7644
- import { open as open5, realpath as realpath4, stat as stat5 } from "node:fs/promises";
8041
+ import { Effect as Effect11, Schedule as Schedule3, Schema as Schema13 } from "effect";
8042
+ import { constants as constants5 } from "node:fs";
8043
+ import { open as open6, realpath as realpath4, stat as stat5 } from "node:fs/promises";
7645
8044
  import { arch, hostname as hostname3, platform as platform4 } from "node:os";
7646
- import { basename as basename3, isAbsolute as isAbsolute5, relative as relative2 } from "node:path";
8045
+ import { basename as basename3, isAbsolute as isAbsolute5, relative as relative2, sep } from "node:path";
7647
8046
  import { WebSocket as WebSocket2 } from "ws";
7648
8047
  var maxQueuedRelayMessages = 8;
7649
8048
 
7650
- class DeviceConnectionError extends Schema12.TaggedErrorClass()("Device.ConnectionError", { message: Schema12.String }) {
8049
+ class DeviceConnectionError extends Schema13.TaggedErrorClass()("Device.ConnectionError", { message: Schema13.String }) {
7651
8050
  }
7652
8051
  var socketUrl = (configuration) => {
7653
8052
  const url = new URL(configuration.relayUrl);
@@ -7724,7 +8123,7 @@ var allowedWorkspace = async (configuration, requested) => {
7724
8123
  const candidate = await realpath4(selected);
7725
8124
  const allowed = configuration.workspaces.some((root) => {
7726
8125
  const child = relative2(root, candidate);
7727
- return child === "" || !child.startsWith("..") && !isAbsolute5(child);
8126
+ return child !== ".." && !child.startsWith(`..${sep}`) && !isAbsolute5(child);
7728
8127
  });
7729
8128
  if (!allowed) {
7730
8129
  throw new Error(`Workspace ${candidate} is outside this device's configured workspace roots`);
@@ -7734,8 +8133,8 @@ var allowedWorkspace = async (configuration, requested) => {
7734
8133
  var openAllowedWorkspace = async (configuration, requested) => {
7735
8134
  const path = await allowedWorkspace(configuration, requested);
7736
8135
  const operatingSystem2 = platform4();
7737
- const flags = constants4.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants4.O_DIRECTORY | constants4.O_NOFOLLOW);
7738
- const handle = await open5(path, flags);
8136
+ const flags = constants5.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants5.O_DIRECTORY | constants5.O_NOFOLLOW);
8137
+ const handle = await open6(path, flags);
7739
8138
  const identity = await handle.stat();
7740
8139
  if (!identity.isDirectory()) {
7741
8140
  await handle.close();
@@ -8168,9 +8567,9 @@ var connect = (configuration, state) => Effect11.tryPromise({
8168
8567
  if (closed) {
8169
8568
  return;
8170
8569
  }
8171
- const text3 = 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");
8570
+ const text4 = 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");
8172
8571
  const event = await Effect11.runPromise(Effect11.try({
8173
- try: () => JSON.parse(text3),
8572
+ try: () => JSON.parse(text4),
8174
8573
  catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
8175
8574
  }).pipe(Effect11.flatMap(decodeSocketEvent)));
8176
8575
  if (event._tag === "Connected") {
@@ -8263,10 +8662,9 @@ var runDaemon = (configuration) => {
8263
8662
  // apps/device/src/mcp.ts
8264
8663
  init_src();
8265
8664
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8266
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8267
- import { Effect as Effect12, Schema as Schema13 } from "effect";
8665
+ import { Effect as Effect12, Schema as Schema14 } from "effect";
8268
8666
  import * as z from "zod/v4";
8269
- import { writeFile as writeFile8 } from "node:fs/promises";
8667
+ import { writeFile as writeFile9 } from "node:fs/promises";
8270
8668
 
8271
8669
  // apps/device/src/mcp-build-notice.ts
8272
8670
  import { execFile as execFile3 } from "node:child_process";
@@ -8336,7 +8734,7 @@ var runMcp = async (configuration) => {
8336
8734
  run_id: z.string().uuid().optional(),
8337
8735
  question: z.string().min(1).max(maxClarificationBytes)
8338
8736
  }
8339
- }, async ({ task_id, run_id, question }) => output(await Effect12.runPromise(requestInput(client2, configuration, Schema13.decodeUnknownSync(RequestTaskInput.fields.question)(question), task_id === undefined ? undefined : Schema13.decodeUnknownSync(TaskId)(task_id), run_id === undefined ? undefined : Schema13.decodeUnknownSync(TaskRunId)(run_id)))));
8737
+ }, async ({ task_id, run_id, question }) => output(await Effect12.runPromise(requestInput(client2, configuration, Schema14.decodeUnknownSync(RequestTaskInput.fields.question)(question), task_id === undefined ? undefined : Schema14.decodeUnknownSync(TaskId)(task_id), run_id === undefined ? undefined : Schema14.decodeUnknownSync(TaskRunId)(run_id)))));
8340
8738
  server.registerTool("task_answer", {
8341
8739
  title: "Answer a worker's question",
8342
8740
  description: "Answer the current clarification and resume the same task. Use input_request.id from task_status or delegate as request_id, or inputRequest.id from completion_inbox. Only the original requester or relay owner can answer. Use wait_task to collect the resumed result.",
@@ -8345,7 +8743,7 @@ var runMcp = async (configuration) => {
8345
8743
  request_id: z.string().uuid(),
8346
8744
  answer: z.string().min(1).max(maxClarificationBytes)
8347
8745
  }
8348
- }, async ({ task_id, request_id, answer }) => output(taskResult(await Effect12.runPromise(client2.answerTaskInput(Schema13.decodeUnknownSync(TaskId)(task_id), Schema13.decodeUnknownSync(AnswerTaskInput)({ requestId: request_id, answer }))))));
8746
+ }, async ({ task_id, request_id, answer }) => output(taskResult(await Effect12.runPromise(client2.answerTaskInput(Schema14.decodeUnknownSync(TaskId)(task_id), Schema14.decodeUnknownSync(AnswerTaskInput)({ requestId: request_id, answer }))))));
8349
8747
  server.registerTool("task_progress", {
8350
8748
  title: "Report task progress",
8351
8749
  description: `Replace the latest progress note for a running task on this device. Use brief milestones, up to ${maxProgressNoteBytes} UTF-8 bytes. Inherits the delegated task ID when omitted.`,
@@ -8357,8 +8755,8 @@ var runMcp = async (configuration) => {
8357
8755
  const id = task_id ?? configuration.mcpTaskId;
8358
8756
  if (id === undefined)
8359
8757
  throw new Error("Task id is required outside delegated work");
8360
- const input = Schema13.decodeUnknownSync(TaskProgressInput)({ note });
8361
- return output(await Effect12.runPromise(client2.reportTaskProgress(Schema13.decodeUnknownSync(TaskId)(id), input)));
8758
+ const input = Schema14.decodeUnknownSync(TaskProgressInput)({ note });
8759
+ return output(await Effect12.runPromise(client2.reportTaskProgress(Schema14.decodeUnknownSync(TaskId)(id), input)));
8362
8760
  });
8363
8761
  server.registerTool("list_devices", {
8364
8762
  title: "List Cohall devices",
@@ -8404,7 +8802,7 @@ var runMcp = async (configuration) => {
8404
8802
  const attachments2 = await readInputAttachments(attachment_paths ?? []);
8405
8803
  const task = await Effect12.runPromise(createDelegation(client2, configuration, {
8406
8804
  prompt,
8407
- ...request_id === undefined ? {} : { requestId: Schema13.decodeUnknownSync(TaskRequestId)(request_id) },
8805
+ ...request_id === undefined ? {} : { requestId: Schema14.decodeUnknownSync(TaskRequestId)(request_id) },
8408
8806
  ...provider === undefined ? {} : { provider },
8409
8807
  ...target === undefined ? {} : { target },
8410
8808
  ...context === undefined ? {} : { context },
@@ -8412,7 +8810,7 @@ var runMcp = async (configuration) => {
8412
8810
  ...parent_task_id === undefined ? {} : { parentTaskId: TaskId.make(parent_task_id) },
8413
8811
  ...workspace === undefined ? {} : { workspace },
8414
8812
  ...attachments2.length === 0 ? {} : { attachments: attachments2 },
8415
- ...deadline === undefined ? {} : { expiresAt: Schema13.decodeUnknownSync(TaskDeadline)(deadline) }
8813
+ ...deadline === undefined ? {} : { expiresAt: Schema14.decodeUnknownSync(TaskDeadline)(deadline) }
8416
8814
  }), { signal });
8417
8815
  const completed = wait ? await Effect12.runPromise(waitForTask(client2, task, timeout_seconds), { signal }) : task;
8418
8816
  return output(wait ? await acknowledgedTaskResult(client2, completed, signal) : taskResult(completed));
@@ -8446,11 +8844,12 @@ var runMcp = async (configuration) => {
8446
8844
  output_path: z.string(),
8447
8845
  direction: z.enum(["input", "output"]).optional()
8448
8846
  }
8449
- }, async ({ task_id, name, output_path, direction }) => {
8450
- const safeName = Schema13.decodeUnknownSync(AttachmentName)(name);
8451
- const safeDirection = direction === undefined ? undefined : Schema13.decodeUnknownSync(AttachmentDirection)(direction);
8452
- const data = await Effect12.runPromise(client2.readAttachment(TaskId.make(task_id), safeName, safeDirection));
8453
- await writeFile8(output_path, data, { flag: "wx", mode: 384 });
8847
+ }, async ({ task_id, name, output_path, direction }, { signal }) => {
8848
+ const safeName = Schema14.decodeUnknownSync(AttachmentName)(name);
8849
+ const safeDirection = direction === undefined ? undefined : Schema14.decodeUnknownSync(AttachmentDirection)(direction);
8850
+ const data = await Effect12.runPromise(client2.readAttachment(TaskId.make(task_id), safeName, safeDirection), { signal });
8851
+ signal.throwIfAborted();
8852
+ await writeFile9(output_path, data, { flag: "wx", mode: 384, signal });
8454
8853
  return output({ task_id, name: safeName, output_path, bytes: data.length });
8455
8854
  });
8456
8855
  server.registerTool("task_trace", {
@@ -8480,7 +8879,7 @@ var runMcp = async (configuration) => {
8480
8879
  description: "Read shared task prompts and final results for a cross-device thread.",
8481
8880
  inputSchema: { thread_id: z.string().uuid() }
8482
8881
  }, async ({ thread_id }) => output(await Effect12.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
8483
- await server.connect(new StdioServerTransport);
8882
+ await server.connect(createMcpHostDiagnostics(server.server));
8484
8883
  };
8485
8884
 
8486
8885
  // apps/device/src/main.ts
@@ -8536,4 +8935,4 @@ await main().catch((cause) => {
8536
8935
  process.exitCode = 1;
8537
8936
  });
8538
8937
 
8539
- //# debugId=10C40468ADA26DC864756E2164756E21
8938
+ //# debugId=6DD87AF6E4752A1764756E2164756E21