@akshar5/cohall 0.10.2 → 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.2", 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);
@@ -3056,9 +3056,9 @@ var init_main = __esm(async () => {
3056
3056
  init_config();
3057
3057
  init_hub();
3058
3058
  init_store();
3059
- RequestError = class RequestError extends Schema16.TaggedErrorClass()("Relay.RequestError", {
3060
- status: Schema16.Int,
3061
- message: Schema16.String
3059
+ RequestError = class RequestError extends Schema17.TaggedErrorClass()("Relay.RequestError", {
3060
+ status: Schema17.Int,
3061
+ message: Schema17.String
3062
3062
  }) {
3063
3063
  };
3064
3064
  maxTaskRequestBodyBytes = 3 * 1024 * 1024;
@@ -3903,10 +3903,10 @@ var run = (options) => Effect3.callback((resume, signal) => {
3903
3903
  });
3904
3904
 
3905
3905
  // apps/device/src/cli.ts
3906
- import { Effect as Effect10, Schema as Schema11 } from "effect";
3907
- 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";
3908
3908
  import { homedir as homedir4 } from "node:os";
3909
- 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";
3910
3910
 
3911
3911
  // skills/cohall/SKILL.md
3912
3912
  var SKILL_default = `---
@@ -4172,6 +4172,125 @@ cohall devices
4172
4172
  Return the diagnostic failure to the user rather than claiming remote work ran.
4173
4173
  `;
4174
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
+
4175
4294
  // apps/device/src/config.ts
4176
4295
  init_src();
4177
4296
  import { Effect as Effect4, Schema as Schema4 } from "effect";
@@ -5239,9 +5358,15 @@ var terminalPrompter = () => {
5239
5358
  close: () => terminal.close()
5240
5359
  };
5241
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
+ };
5242
5367
  var guidedSetupInput = async (options, prompter) => {
5243
5368
  const existing = await readStoredConfiguration();
5244
- 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 ?? ""));
5245
5370
  const credentials = credentialsForRelay(existing, relayUrl);
5246
5371
  const hasRequiredCredentials = credentials.clientToken !== undefined && (options.clientOnly || credentials.deviceToken !== undefined);
5247
5372
  let workspaces = options.clientOnly ? existing?.workspaces ?? [] : options.workspaces;
@@ -6722,6 +6847,248 @@ import {
6722
6847
  getDefaultEnvironment,
6723
6848
  StdioClientTransport
6724
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
6725
7092
  var checkMcp = async (entrypoint, relayUrl, token) => {
6726
7093
  const client = new Client({ name: "cohall-doctor", version });
6727
7094
  const transport = new StdioClientTransport({
@@ -6730,6 +7097,7 @@ var checkMcp = async (entrypoint, relayUrl, token) => {
6730
7097
  env: {
6731
7098
  ...getDefaultEnvironment(),
6732
7099
  COHALL_CONFIG: configurationPath(),
7100
+ COHALL_MCP_SELF_TEST: "1",
6733
7101
  COHALL_RELAY_URL: relayUrl,
6734
7102
  COHALL_CLIENT_TOKEN: token
6735
7103
  },
@@ -6800,6 +7168,7 @@ var aliases = new Map([
6800
7168
  var help = `Cohall connects agents running on your own devices.
6801
7169
 
6802
7170
  Usage:
7171
+ cohall onboard
6803
7172
  cohall init [--relay url] [--workspace path] [--providers list] [--service]
6804
7173
  cohall join --relay <url> --workspace <path> [--providers list] [--token-file <path>]
6805
7174
  cohall configure [--relay url] [--name name] [--workspace path]
@@ -6930,7 +7299,7 @@ var printLine = (value) => console.log(JSON.stringify(value));
6930
7299
  var readTextFile = async (path, limit, exceeded) => {
6931
7300
  if ((await stat4(path)).size > limit)
6932
7301
  throw new Error(exceeded);
6933
- const file = await open4(path, "r");
7302
+ const file = await open5(path, "r");
6934
7303
  try {
6935
7304
  const data = await readBoundedFile(file, limit);
6936
7305
  if (data.length > limit)
@@ -7005,17 +7374,17 @@ var ownerClient = async () => {
7005
7374
  };
7006
7375
  var installSkill = async (target) => {
7007
7376
  const roots = {
7008
- agents: join8(homedir4(), ".agents", "skills", "cohall", "SKILL.md"),
7009
- claude: join8(homedir4(), ".claude", "skills", "cohall", "SKILL.md"),
7010
- 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")
7011
7380
  };
7012
7381
  const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
7013
7382
  if (selected === undefined) {
7014
7383
  throw new Error("Skill target must be agents, claude, opencode, or all");
7015
7384
  }
7016
7385
  for (const path of selected) {
7017
- await mkdir7(dirname6(path), { recursive: true });
7018
- await writeFile7(path, SKILL_default);
7386
+ await mkdir8(dirname6(path), { recursive: true });
7387
+ await writeFile8(path, SKILL_default);
7019
7388
  }
7020
7389
  return selected;
7021
7390
  };
@@ -7023,13 +7392,19 @@ var printHelp = () => console.log(help);
7023
7392
  var printSkill = () => console.log(SKILL_default.trimEnd());
7024
7393
  var runCli = async (command, raw) => {
7025
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
+ }
7026
7401
  if (command === "reply") {
7027
7402
  allowOptions(arguments_, ["message", "message-file", "error", "question", "run-id"]);
7028
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7403
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7029
7404
  const error = option(arguments_, "error");
7030
7405
  const question = option(arguments_, "question");
7031
7406
  const rawRunId = option(arguments_, "run-id");
7032
- const runId = rawRunId === undefined ? undefined : Schema11.decodeUnknownSync(TaskRunId)(rawRunId);
7407
+ const runId = rawRunId === undefined ? undefined : Schema12.decodeUnknownSync(TaskRunId)(rawRunId);
7033
7408
  if ((error !== undefined || question !== undefined) && (option(arguments_, "message") !== undefined || option(arguments_, "message-file") !== undefined || error !== undefined && question !== undefined)) {
7034
7409
  throw new Error("Use one of --question, --error, or --message/--message-file");
7035
7410
  }
@@ -7037,7 +7412,7 @@ var runCli = async (command, raw) => {
7037
7412
  if (error === undefined && question === undefined && message === undefined) {
7038
7413
  throw new Error("A bot reply requires --message, --message-file, --error, or --question");
7039
7414
  }
7040
- 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 };
7041
7416
  await writeBotReply(id, reply, runId);
7042
7417
  print({ submitted: true, task_id: id });
7043
7418
  return;
@@ -7053,6 +7428,9 @@ var runCli = async (command, raw) => {
7053
7428
  "workspace"
7054
7429
  ]);
7055
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
+ }
7056
7434
  const token = option(arguments_, "token-file") === undefined ? undefined : await pairingToken(arguments_);
7057
7435
  const interactive = process.stdin.isTTY === true && process.stderr.isTTY === true;
7058
7436
  const relayUrl = option(arguments_, "relay");
@@ -7111,7 +7489,7 @@ var runCli = async (command, raw) => {
7111
7489
  workspaces: result.configuration.workspaces,
7112
7490
  skills: installedSkills,
7113
7491
  ...service === undefined ? {} : { service },
7114
- 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`."
7115
7493
  });
7116
7494
  } finally {
7117
7495
  prompter.close();
@@ -7203,7 +7581,7 @@ var runCli = async (command, raw) => {
7203
7581
  if (arguments_.positionals.length !== 2 || arguments_.positionals[0] !== "abandon") {
7204
7582
  throw new Error("Usage: cohall upgrades [abandon <operation-id>]");
7205
7583
  }
7206
- 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]))));
7207
7585
  return;
7208
7586
  }
7209
7587
  print(await Effect10.runPromise(relay2.operations()));
@@ -7219,9 +7597,9 @@ var runCli = async (command, raw) => {
7219
7597
  "workspace"
7220
7598
  ]);
7221
7599
  noPositionals(arguments_, command);
7222
- const token = await pairingToken(arguments_);
7223
- const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? "http://127.0.0.1:8787");
7224
7600
  const existing = await readStoredConfiguration();
7601
+ const relayUrl = setupRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl);
7602
+ const token = await pairingToken(arguments_);
7225
7603
  const suppliedWorkspaces = values(arguments_, "workspace");
7226
7604
  const workspaces = suppliedWorkspaces.length === 0 ? existing?.workspaces : suppliedWorkspaces;
7227
7605
  const deviceName = option(arguments_, "name");
@@ -7303,7 +7681,7 @@ var runCli = async (command, raw) => {
7303
7681
  ...model === undefined ? {} : { model },
7304
7682
  ...grokGateway === undefined ? {} : { grokGateway },
7305
7683
  ...(sandbox ?? existing?.sandbox) === undefined ? {} : {
7306
- 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)
7307
7685
  }
7308
7686
  });
7309
7687
  await writeStoredConfiguration(configuration2);
@@ -7368,6 +7746,7 @@ var runCli = async (command, raw) => {
7368
7746
  return;
7369
7747
  });
7370
7748
  const clientToken = process.env.COHALL_CLIENT_TOKEN ?? storedCredentials.clientToken;
7749
+ const mcpHost = await readMcpHostDiagnostics();
7371
7750
  const mcp = clientToken === undefined ? { status: "not configured" } : await checkMcp(process.argv[1] ?? "", relayUrl, clientToken);
7372
7751
  const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || storedCredentials.deviceToken !== undefined;
7373
7752
  const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
@@ -7389,6 +7768,8 @@ var runCli = async (command, raw) => {
7389
7768
  ...response?.ok === true ? [] : ["Relay is unreachable"],
7390
7769
  ...versionWarning === undefined ? [] : [versionWarning],
7391
7770
  ...mcp.status === "error" ? [`MCP check failed: ${mcp.error}`] : [],
7771
+ ...mcpHost.warnings,
7772
+ ...mcpHost.status === "unavailable" ? [`MCP host diagnostics unavailable: ${mcpHost.error}`] : [],
7392
7773
  ...clientAuthentication.status === "error" ? [clientAuthentication.error] : [],
7393
7774
  ...hasDeviceCredential && response?.ok === true && clientAuthentication.status !== "error" && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
7394
7775
  clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
@@ -7404,7 +7785,8 @@ var runCli = async (command, raw) => {
7404
7785
  config_path: configurationPath(),
7405
7786
  client_credential: process.env.COHALL_CLIENT_TOKEN !== undefined || storedCredentials.clientToken !== undefined,
7406
7787
  client_authentication: clientAuthentication.status === "ok" ? { status: "ok" } : clientAuthentication,
7407
- mcp,
7788
+ mcp: { ...mcp, scope: "server self-test" },
7789
+ mcp_host: mcpHost,
7408
7790
  device_credential: hasDeviceCredential,
7409
7791
  workspaces: configuration2?.workspaces ?? [],
7410
7792
  providers: providerExecutables,
@@ -7451,7 +7833,8 @@ var runCli = async (command, raw) => {
7451
7833
  relay_url: configuration2.relayUrl,
7452
7834
  pairing_token: credential.token,
7453
7835
  expires_at: credential.expiresAt,
7454
- 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.`
7455
7838
  });
7456
7839
  return;
7457
7840
  }
@@ -7463,11 +7846,11 @@ var runCli = async (command, raw) => {
7463
7846
  }
7464
7847
  if (command === "forget") {
7465
7848
  allowOptions(arguments_, []);
7466
- 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")))));
7467
7850
  return;
7468
7851
  }
7469
7852
  allowOptions(arguments_, []);
7470
- 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")))));
7471
7854
  return;
7472
7855
  }
7473
7856
  const { configuration, relay } = await client();
@@ -7476,20 +7859,20 @@ var runCli = async (command, raw) => {
7476
7859
  const rawQuestion = option(arguments_, "question");
7477
7860
  if (rawQuestion === undefined)
7478
7861
  throw new Error("--question is required");
7479
- const question = Schema11.decodeUnknownSync(RequestTaskInput.fields.question)(rawQuestion);
7480
- 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"));
7481
7864
  const rawRunId = option(arguments_, "run-id");
7482
- const runId = rawRunId === undefined ? undefined : Schema11.decodeUnknownSync(TaskRunId)(rawRunId);
7865
+ const runId = rawRunId === undefined ? undefined : Schema12.decodeUnknownSync(TaskRunId)(rawRunId);
7483
7866
  print(await Effect10.runPromise(requestInput(relay, configuration, question, id, runId)));
7484
7867
  return;
7485
7868
  }
7486
7869
  if (command === "answer") {
7487
7870
  allowOptions(arguments_, ["request-id", "message", "message-file"]);
7488
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7871
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7489
7872
  const requestId = option(arguments_, "request-id");
7490
7873
  if (requestId === undefined)
7491
7874
  throw new Error("--request-id is required; use the input request id from status or inbox");
7492
- const input = Schema11.decodeUnknownSync(AnswerTaskInput)({
7875
+ const input = Schema12.decodeUnknownSync(AnswerTaskInput)({
7493
7876
  requestId,
7494
7877
  answer: await readInput(arguments_, "message")
7495
7878
  });
@@ -7504,8 +7887,8 @@ var runCli = async (command, raw) => {
7504
7887
  const note = option(arguments_, "message");
7505
7888
  if (note === undefined)
7506
7889
  throw new Error("--message is required");
7507
- const input = Schema11.decodeUnknownSync(TaskProgressInput)({ note });
7508
- 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)));
7509
7892
  return;
7510
7893
  }
7511
7894
  if (command === "devices") {
@@ -7565,15 +7948,15 @@ var runCli = async (command, raw) => {
7565
7948
  const requestId = option(arguments_, "request-id");
7566
7949
  const task = await Effect10.runPromise(createDelegation(relay, configuration, {
7567
7950
  prompt,
7568
- ...requestId === undefined ? {} : { requestId: Schema11.decodeUnknownSync(TaskRequestId)(requestId) },
7951
+ ...requestId === undefined ? {} : { requestId: Schema12.decodeUnknownSync(TaskRequestId)(requestId) },
7569
7952
  ...target === undefined ? {} : { target },
7570
7953
  ...context === undefined ? {} : { context },
7571
- ...thread === undefined ? {} : { threadId: Schema11.decodeUnknownSync(ThreadId)(thread) },
7572
- ...parent === undefined ? {} : { parentTaskId: Schema11.decodeUnknownSync(TaskId)(parent) },
7954
+ ...thread === undefined ? {} : { threadId: Schema12.decodeUnknownSync(ThreadId)(thread) },
7955
+ ...parent === undefined ? {} : { parentTaskId: Schema12.decodeUnknownSync(TaskId)(parent) },
7573
7956
  ...workspace === undefined ? {} : { workspace },
7574
- ...provider === undefined ? {} : { provider: Schema11.decodeUnknownSync(Provider)(provider) },
7957
+ ...provider === undefined ? {} : { provider: Schema12.decodeUnknownSync(Provider)(provider) },
7575
7958
  ...attachments2.length === 0 ? {} : { attachments: attachments2 },
7576
- ...deadline === undefined ? {} : { expiresAt: Schema11.decodeUnknownSync(TaskDeadline)(deadline) }
7959
+ ...deadline === undefined ? {} : { expiresAt: Schema12.decodeUnknownSync(TaskDeadline)(deadline) }
7577
7960
  }));
7578
7961
  const completed = arguments_.options.has("no-wait") ? task : await Effect10.runPromise(waitForTask(relay, task, waitSeconds));
7579
7962
  print(arguments_.options.has("no-wait") ? taskResult(completed) : await acknowledgedTaskResult(relay, completed));
@@ -7584,7 +7967,7 @@ var runCli = async (command, raw) => {
7584
7967
  if (arguments_.positionals[0] === "ack") {
7585
7968
  if (arguments_.positionals.length !== 2)
7586
7969
  throw new Error("Usage: cohall inbox ack <task-id>");
7587
- const id = Schema11.decodeUnknownSync(TaskId)(arguments_.positionals[1]);
7970
+ const id = Schema12.decodeUnknownSync(TaskId)(arguments_.positionals[1]);
7588
7971
  print(await Effect10.runPromise(relay.acknowledgeCompletion(id)));
7589
7972
  return;
7590
7973
  }
@@ -7594,13 +7977,13 @@ var runCli = async (command, raw) => {
7594
7977
  }
7595
7978
  if (command === "status") {
7596
7979
  allowOptions(arguments_, []);
7597
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7980
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7598
7981
  print(taskResult(await Effect10.runPromise(relay.getTask(id))));
7599
7982
  return;
7600
7983
  }
7601
7984
  if (command === "attachments") {
7602
7985
  allowOptions(arguments_, []);
7603
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7986
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7604
7987
  print(await Effect10.runPromise(relay.listAttachments(id)));
7605
7988
  return;
7606
7989
  }
@@ -7613,18 +7996,18 @@ var runCli = async (command, raw) => {
7613
7996
  if (destination === undefined) {
7614
7997
  throw new Error("--output is required");
7615
7998
  }
7616
- const id = Schema11.decodeUnknownSync(TaskId)(arguments_.positionals[0]);
7617
- 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]);
7618
8001
  const directionInput = option(arguments_, "direction");
7619
- const direction = directionInput === undefined ? undefined : Schema11.decodeUnknownSync(AttachmentDirection)(directionInput);
8002
+ const direction = directionInput === undefined ? undefined : Schema12.decodeUnknownSync(AttachmentDirection)(directionInput);
7620
8003
  const data = await Effect10.runPromise(relay.readAttachment(id, name, direction));
7621
- await writeFile7(destination, data, { flag: "wx", mode: 384 });
8004
+ await writeFile8(destination, data, { flag: "wx", mode: 384 });
7622
8005
  print({ task_id: id, name, path: resolve4(destination), bytes: data.length });
7623
8006
  return;
7624
8007
  }
7625
8008
  if (command === "trace") {
7626
8009
  allowOptions(arguments_, ["follow"]);
7627
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
8010
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7628
8011
  if (arguments_.options.has("follow")) {
7629
8012
  await Effect10.runPromise(followTaskTrace(relay, id, printLine));
7630
8013
  return;
@@ -7634,19 +8017,19 @@ var runCli = async (command, raw) => {
7634
8017
  }
7635
8018
  if (command === "wait") {
7636
8019
  allowOptions(arguments_, ["timeout"]);
7637
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
8020
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7638
8021
  print(taskResult(await Effect10.runPromise(waitForTask(relay, id, timeout(arguments_)))));
7639
8022
  return;
7640
8023
  }
7641
8024
  if (command === "cancel") {
7642
8025
  allowOptions(arguments_, []);
7643
- const id = Schema11.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
8026
+ const id = Schema12.decodeUnknownSync(TaskId)(identifier(arguments_, "task id"));
7644
8027
  print(taskResult(await Effect10.runPromise(relay.cancelTask(id))));
7645
8028
  return;
7646
8029
  }
7647
8030
  if (command === "thread") {
7648
8031
  allowOptions(arguments_, []);
7649
- const id = Schema11.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
8032
+ const id = Schema12.decodeUnknownSync(ThreadId)(identifier(arguments_, "thread id"));
7650
8033
  print(await Effect10.runPromise(threadContext(relay, id)));
7651
8034
  return;
7652
8035
  }
@@ -7655,15 +8038,15 @@ var runCli = async (command, raw) => {
7655
8038
 
7656
8039
  // apps/device/src/daemon.ts
7657
8040
  init_src();
7658
- import { Effect as Effect11, Schedule as Schedule3, Schema as Schema12 } from "effect";
7659
- import { constants as constants4 } from "node:fs";
7660
- 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";
7661
8044
  import { arch, hostname as hostname3, platform as platform4 } from "node:os";
7662
8045
  import { basename as basename3, isAbsolute as isAbsolute5, relative as relative2, sep } from "node:path";
7663
8046
  import { WebSocket as WebSocket2 } from "ws";
7664
8047
  var maxQueuedRelayMessages = 8;
7665
8048
 
7666
- class DeviceConnectionError extends Schema12.TaggedErrorClass()("Device.ConnectionError", { message: Schema12.String }) {
8049
+ class DeviceConnectionError extends Schema13.TaggedErrorClass()("Device.ConnectionError", { message: Schema13.String }) {
7667
8050
  }
7668
8051
  var socketUrl = (configuration) => {
7669
8052
  const url = new URL(configuration.relayUrl);
@@ -7750,8 +8133,8 @@ var allowedWorkspace = async (configuration, requested) => {
7750
8133
  var openAllowedWorkspace = async (configuration, requested) => {
7751
8134
  const path = await allowedWorkspace(configuration, requested);
7752
8135
  const operatingSystem2 = platform4();
7753
- const flags = constants4.O_RDONLY | (operatingSystem2 === "win32" ? 0 : constants4.O_DIRECTORY | constants4.O_NOFOLLOW);
7754
- 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);
7755
8138
  const identity = await handle.stat();
7756
8139
  if (!identity.isDirectory()) {
7757
8140
  await handle.close();
@@ -8184,9 +8567,9 @@ var connect = (configuration, state) => Effect11.tryPromise({
8184
8567
  if (closed) {
8185
8568
  return;
8186
8569
  }
8187
- 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");
8188
8571
  const event = await Effect11.runPromise(Effect11.try({
8189
- try: () => JSON.parse(text3),
8572
+ try: () => JSON.parse(text4),
8190
8573
  catch: () => new DeviceConnectionError({ message: "Relay sent invalid JSON" })
8191
8574
  }).pipe(Effect11.flatMap(decodeSocketEvent)));
8192
8575
  if (event._tag === "Connected") {
@@ -8279,10 +8662,9 @@ var runDaemon = (configuration) => {
8279
8662
  // apps/device/src/mcp.ts
8280
8663
  init_src();
8281
8664
  import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
8282
- import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
8283
- import { Effect as Effect12, Schema as Schema13 } from "effect";
8665
+ import { Effect as Effect12, Schema as Schema14 } from "effect";
8284
8666
  import * as z from "zod/v4";
8285
- import { writeFile as writeFile8 } from "node:fs/promises";
8667
+ import { writeFile as writeFile9 } from "node:fs/promises";
8286
8668
 
8287
8669
  // apps/device/src/mcp-build-notice.ts
8288
8670
  import { execFile as execFile3 } from "node:child_process";
@@ -8352,7 +8734,7 @@ var runMcp = async (configuration) => {
8352
8734
  run_id: z.string().uuid().optional(),
8353
8735
  question: z.string().min(1).max(maxClarificationBytes)
8354
8736
  }
8355
- }, 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)))));
8356
8738
  server.registerTool("task_answer", {
8357
8739
  title: "Answer a worker's question",
8358
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.",
@@ -8361,7 +8743,7 @@ var runMcp = async (configuration) => {
8361
8743
  request_id: z.string().uuid(),
8362
8744
  answer: z.string().min(1).max(maxClarificationBytes)
8363
8745
  }
8364
- }, 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 }))))));
8365
8747
  server.registerTool("task_progress", {
8366
8748
  title: "Report task progress",
8367
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.`,
@@ -8373,8 +8755,8 @@ var runMcp = async (configuration) => {
8373
8755
  const id = task_id ?? configuration.mcpTaskId;
8374
8756
  if (id === undefined)
8375
8757
  throw new Error("Task id is required outside delegated work");
8376
- const input = Schema13.decodeUnknownSync(TaskProgressInput)({ note });
8377
- 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)));
8378
8760
  });
8379
8761
  server.registerTool("list_devices", {
8380
8762
  title: "List Cohall devices",
@@ -8420,7 +8802,7 @@ var runMcp = async (configuration) => {
8420
8802
  const attachments2 = await readInputAttachments(attachment_paths ?? []);
8421
8803
  const task = await Effect12.runPromise(createDelegation(client2, configuration, {
8422
8804
  prompt,
8423
- ...request_id === undefined ? {} : { requestId: Schema13.decodeUnknownSync(TaskRequestId)(request_id) },
8805
+ ...request_id === undefined ? {} : { requestId: Schema14.decodeUnknownSync(TaskRequestId)(request_id) },
8424
8806
  ...provider === undefined ? {} : { provider },
8425
8807
  ...target === undefined ? {} : { target },
8426
8808
  ...context === undefined ? {} : { context },
@@ -8428,7 +8810,7 @@ var runMcp = async (configuration) => {
8428
8810
  ...parent_task_id === undefined ? {} : { parentTaskId: TaskId.make(parent_task_id) },
8429
8811
  ...workspace === undefined ? {} : { workspace },
8430
8812
  ...attachments2.length === 0 ? {} : { attachments: attachments2 },
8431
- ...deadline === undefined ? {} : { expiresAt: Schema13.decodeUnknownSync(TaskDeadline)(deadline) }
8813
+ ...deadline === undefined ? {} : { expiresAt: Schema14.decodeUnknownSync(TaskDeadline)(deadline) }
8432
8814
  }), { signal });
8433
8815
  const completed = wait ? await Effect12.runPromise(waitForTask(client2, task, timeout_seconds), { signal }) : task;
8434
8816
  return output(wait ? await acknowledgedTaskResult(client2, completed, signal) : taskResult(completed));
@@ -8463,11 +8845,11 @@ var runMcp = async (configuration) => {
8463
8845
  direction: z.enum(["input", "output"]).optional()
8464
8846
  }
8465
8847
  }, async ({ task_id, name, output_path, direction }, { signal }) => {
8466
- const safeName = Schema13.decodeUnknownSync(AttachmentName)(name);
8467
- const safeDirection = direction === undefined ? undefined : Schema13.decodeUnknownSync(AttachmentDirection)(direction);
8848
+ const safeName = Schema14.decodeUnknownSync(AttachmentName)(name);
8849
+ const safeDirection = direction === undefined ? undefined : Schema14.decodeUnknownSync(AttachmentDirection)(direction);
8468
8850
  const data = await Effect12.runPromise(client2.readAttachment(TaskId.make(task_id), safeName, safeDirection), { signal });
8469
8851
  signal.throwIfAborted();
8470
- await writeFile8(output_path, data, { flag: "wx", mode: 384, signal });
8852
+ await writeFile9(output_path, data, { flag: "wx", mode: 384, signal });
8471
8853
  return output({ task_id, name: safeName, output_path, bytes: data.length });
8472
8854
  });
8473
8855
  server.registerTool("task_trace", {
@@ -8497,7 +8879,7 @@ var runMcp = async (configuration) => {
8497
8879
  description: "Read shared task prompts and final results for a cross-device thread.",
8498
8880
  inputSchema: { thread_id: z.string().uuid() }
8499
8881
  }, async ({ thread_id }) => output(await Effect12.runPromise(threadContext(client2, ThreadId.make(thread_id)))));
8500
- await server.connect(new StdioServerTransport);
8882
+ await server.connect(createMcpHostDiagnostics(server.server));
8501
8883
  };
8502
8884
 
8503
8885
  // apps/device/src/main.ts
@@ -8553,4 +8935,4 @@ await main().catch((cause) => {
8553
8935
  process.exitCode = 1;
8554
8936
  });
8555
8937
 
8556
- //# debugId=49CCA945DC24F88C64756E2164756E21
8938
+ //# debugId=6DD87AF6E4752A1764756E2164756E21