@akshar5/cohall 0.3.0 → 0.3.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/CHANGELOG.md CHANGED
@@ -1,5 +1,19 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.3.2](https://github.com/AksharP5/cohall/compare/v0.3.1...v0.3.2) (2026-08-06)
4
+
5
+
6
+ ### Bug Fixes
7
+
8
+ * make setup and provider diagnostics reliable ([#11](https://github.com/AksharP5/cohall/issues/11)) ([3c621c7](https://github.com/AksharP5/cohall/commit/3c621c7b2c13ba192ced7d93319cc41df3af256e))
9
+
10
+ ## [0.3.1](https://github.com/AksharP5/cohall/compare/v0.3.0...v0.3.1) (2026-08-06)
11
+
12
+
13
+ ### Bug Fixes
14
+
15
+ * make Codex and OpenCode delegation reliable ([974e0d1](https://github.com/AksharP5/cohall/commit/974e0d127a56060ee9fec0210b138a0879dde9d8))
16
+
3
17
  ## [0.3.0](https://github.com/AksharP5/cohall/compare/v0.2.0...v0.3.0) (2026-08-06)
4
18
 
5
19
 
package/README.md CHANGED
@@ -78,6 +78,7 @@ read -rsp 'Pairing token: ' pairing_token; printf '\n'
78
78
  printf '%s' "$pairing_token" | npx -y @akshar5/cohall join \
79
79
  --relay https://cohall.example.com \
80
80
  --name macbook \
81
+ --providers codex \
81
82
  --workspace "$HOME/dev" \
82
83
  --workspace "$HOME/.skillsync/repo"
83
84
  unset pairing_token
@@ -130,8 +131,50 @@ terminal.
130
131
 
131
132
  ## Optional MCP
132
133
 
133
- Run `npx -y @akshar5/cohall integrations` for current setup commands. The MCP
134
- server exposes:
134
+ The MCP subprocess uses the current user's stored Cohall client configuration.
135
+ For clients that accept the common `mcpServers` JSON shape, paste:
136
+
137
+ ```json
138
+ {
139
+ "mcpServers": {
140
+ "cohall": {
141
+ "command": "npx",
142
+ "args": ["-y", "@akshar5/cohall", "mcp"]
143
+ }
144
+ }
145
+ }
146
+ ```
147
+
148
+ Codex:
149
+
150
+ ```bash
151
+ codex mcp add cohall -- npx -y @akshar5/cohall mcp
152
+ ```
153
+
154
+ Claude Code, available in every project for the current user:
155
+
156
+ ```bash
157
+ claude mcp add --transport stdio --scope user cohall -- \
158
+ npx -y @akshar5/cohall mcp
159
+ ```
160
+
161
+ OpenCode `opencode.json`:
162
+
163
+ ```json
164
+ {
165
+ "$schema": "https://opencode.ai/config.json",
166
+ "mcp": {
167
+ "cohall": {
168
+ "type": "local",
169
+ "command": ["npx", "-y", "@akshar5/cohall", "mcp"],
170
+ "enabled": true
171
+ }
172
+ }
173
+ }
174
+ ```
175
+
176
+ Run `npx -y @akshar5/cohall integrations` to print these command components.
177
+ The MCP server exposes:
135
178
 
136
179
  - `list_devices`
137
180
  - `delegate`
@@ -149,7 +192,14 @@ and [service setup](docs/services.md).
149
192
 
150
193
  ## Provider behavior
151
194
 
152
- Target devices advertise only provider executables they actually have:
195
+ Target devices advertise provider executables they actually have. Authentication
196
+ is checked when delegated work starts. Restrict a device to providers you have
197
+ configured locally:
198
+
199
+ ```bash
200
+ cohall configure --providers codex,claude-code
201
+ cohall configure --providers auto
202
+ ```
153
203
 
154
204
  | Provider | Required command | Session continuation |
155
205
  | ----------- | ---------------- | ------------------------ |
@@ -167,7 +217,9 @@ enforced after resolving symlinks.
167
217
 
168
218
  `npx -y @akshar5/cohall config` shows the active stored configuration without
169
219
  printing tokens. `npx -y @akshar5/cohall configure` changes relay, name,
170
- workspaces, model, or Codex sandbox.
220
+ workspaces, enabled providers, model, or Codex sandbox.
221
+ `npx -y @akshar5/cohall doctor` checks relay reachability, this device's relay
222
+ status, provider selection, executable paths, and version information.
171
223
  Environment variables override stored values:
172
224
 
173
225
  | Variable | Purpose |
@@ -179,6 +231,7 @@ Environment variables override stored values:
179
231
  | `COHALL_TOKEN` | Relay owner credential |
180
232
  | `COHALL_DEVICE_ID` | Stable device ID override |
181
233
  | `COHALL_DEVICE_NAME` | Advertised device name |
234
+ | `COHALL_DEVICE_PROVIDERS` | Provider allowlist or `auto` |
182
235
  | `COHALL_DEVICE_WORKSPACES` | Comma-separated workspace roots |
183
236
  | `COHALL_DEVICE_WORKSPACES_JSON` | JSON workspace roots; supports commas in paths |
184
237
  | `COHALL_MODEL` | Target provider model override |
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 { Schema } from "effect";
20
- var version = "0.3.0", bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
20
+ var version = "0.3.2", bounded = (maxLength) => Schema.String.check(Schema.isMinLength(1), Schema.isMaxLength(maxLength)), optionalText = (maxLength) => Schema.String.check(Schema.isMaxLength(maxLength)), boundedArray = (schema, maxLength) => Schema.Array(schema).check(Schema.isMaxLength(maxLength)), uuid = (name) => Schema.String.check(Schema.isUUID(4)).pipe(Schema.brand(name)), isoTimestamp = (name) => Schema.String.check(Schema.isPattern(/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}(?:\.\d{1,9})?Z$/, {
21
21
  expected: "an ISO-8601 UTC timestamp"
22
22
  })).pipe(Schema.brand(name)), DeviceId, ThreadId, MessageId, TaskId, AuthSessionId, Timestamp, Platform, Provider, DeviceStatus, TaskStatus, TaskTraceEventKind, MessageRole, ConnectionRole, AuthSession, CreatePairingInput, PairingCredential, ExchangePairingInput, SessionCredential, PairingResult, Capability, Workspace, Device, Thread, Message, Task, TaskTraceEvent, TaskTrace, ThreadContext, CreateTaskInput, SocketEvent, ErrorResponse, terminalTaskStatuses, isTerminalTask = (task) => terminalTaskStatuses.has(task.status), now = () => Timestamp.make(new Date().toISOString()), makeDeviceId = () => DeviceId.make(crypto.randomUUID()), makeThreadId = () => ThreadId.make(crypto.randomUUID()), makeMessageId = () => MessageId.make(crypto.randomUUID()), makeTaskId = () => TaskId.make(crypto.randomUUID()), makeAuthSessionId = () => AuthSessionId.make(crypto.randomUUID()), decodeCreatePairingInput, decodeExchangePairingInput, decodeCreateTaskInput, decodeDevice, decodeSocketEvent;
23
23
  var init_src = __esm(() => {
@@ -203,16 +203,16 @@ var init_src = __esm(() => {
203
203
  // apps/relay/src/config.ts
204
204
  import { Effect as Effect8, Schema as Schema8 } from "effect";
205
205
  import { access as access3, chmod as chmod2, mkdir as mkdir3, readFile as readFile3, writeFile as writeFile3 } from "node:fs/promises";
206
- import { homedir as homedir3, platform as platform4 } from "node:os";
206
+ import { homedir as homedir4, platform as platform4 } from "node:os";
207
207
  import { join as join4, resolve as resolve2 } from "node:path";
208
208
  var RelayConfiguration, defaultDataDirectory = () => {
209
209
  if (platform4() === "win32") {
210
- return join4(process.env.LOCALAPPDATA ?? join4(homedir3(), "AppData", "Local"), "Cohall");
210
+ return join4(process.env.LOCALAPPDATA ?? join4(homedir4(), "AppData", "Local"), "Cohall");
211
211
  }
212
212
  if (platform4() === "darwin") {
213
- return join4(homedir3(), "Library", "Application Support", "Cohall", "relay");
213
+ return join4(homedir4(), "Library", "Application Support", "Cohall", "relay");
214
214
  }
215
- return join4(process.env.XDG_DATA_HOME ?? join4(homedir3(), ".local", "share"), "cohall");
215
+ return join4(process.env.XDG_DATA_HOME ?? join4(homedir4(), ".local", "share"), "cohall");
216
216
  }, generatedToken = () => Array.from(crypto.getRandomValues(new Uint8Array(32)), (value) => value.toString(16).padStart(2, "0")).join(""), tokenFrom = async (path) => {
217
217
  if (await access3(path).then(() => true).catch(() => false)) {
218
218
  return (await readFile3(path, "utf8")).trim();
@@ -1738,7 +1738,7 @@ init_src();
1738
1738
  init_src();
1739
1739
  import { Effect as Effect2, Schema as Schema3 } from "effect";
1740
1740
  import { accessSync, constants } from "node:fs";
1741
- import { platform } from "node:os";
1741
+ import { homedir, platform } from "node:os";
1742
1742
  import { delimiter, extname, isAbsolute, join } from "node:path";
1743
1743
  import { spawn } from "node:child_process";
1744
1744
 
@@ -1756,7 +1756,10 @@ var executables = {
1756
1756
  "claude-code": "claude",
1757
1757
  opencode: "opencode"
1758
1758
  };
1759
- var providerEnvironment = () => Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("COHALL_")));
1759
+ var providerEnvironment = () => ({
1760
+ ...Object.fromEntries(Object.entries(process.env).filter(([name]) => !name.startsWith("COHALL_"))),
1761
+ PATH: executableDirectories().join(delimiter)
1762
+ });
1760
1763
  var record = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined;
1761
1764
  var text = (value) => typeof value === "string" ? value : undefined;
1762
1765
  var executableCandidates = (command) => {
@@ -1765,8 +1768,25 @@ var executableCandidates = (command) => {
1765
1768
  }
1766
1769
  return (process.env.PATHEXT ?? ".COM;.EXE;.BAT;.CMD").split(";").filter((extension) => extension.length > 0).map((extension) => `${command}${extension.toLowerCase()}`);
1767
1770
  };
1771
+ var executableDirectories = () => {
1772
+ const home = homedir();
1773
+ const configured = (process.env.PATH ?? "").split(delimiter).filter(Boolean);
1774
+ const common = platform() === "win32" ? [
1775
+ process.env.APPDATA === undefined ? undefined : join(process.env.APPDATA, "npm"),
1776
+ process.env.LOCALAPPDATA === undefined ? undefined : join(process.env.LOCALAPPDATA, "pnpm"),
1777
+ join(home, ".bun", "bin")
1778
+ ] : [
1779
+ join(home, ".local", "bin"),
1780
+ join(home, ".npm-global", "bin"),
1781
+ join(home, ".bun", "bin"),
1782
+ join(home, ".local", "share", "pnpm"),
1783
+ "/opt/homebrew/bin",
1784
+ "/usr/local/bin"
1785
+ ];
1786
+ return [...new Set([...configured, ...common.filter((path) => path !== undefined)])];
1787
+ };
1768
1788
  var findExecutable = (command) => {
1769
- const paths = isAbsolute(command) || command.includes("/") || command.includes("\\") ? [""] : (process.env.PATH ?? "").split(delimiter);
1789
+ const paths = isAbsolute(command) || command.includes("/") || command.includes("\\") ? [""] : executableDirectories();
1770
1790
  for (const directory of paths) {
1771
1791
  for (const candidate of executableCandidates(command)) {
1772
1792
  const path = directory.length === 0 ? candidate : join(directory, candidate);
@@ -1813,9 +1833,36 @@ var boundedText = (stream, limit, onOverflow) => new Promise((resolve, reject) =
1813
1833
  stream.once("end", onEnd);
1814
1834
  stream.once("error", onError);
1815
1835
  });
1836
+ var captureText = (stream, limit) => new Promise((resolve, reject) => {
1837
+ const chunks = [];
1838
+ let captured = 0;
1839
+ let truncated = false;
1840
+ const onData = (chunk) => {
1841
+ const bytes = typeof chunk === "string" ? Buffer.from(chunk) : chunk;
1842
+ const remaining = limit - captured;
1843
+ if (remaining > 0) {
1844
+ const selected = bytes.byteLength <= remaining ? bytes : bytes.subarray(0, remaining);
1845
+ chunks.push(selected);
1846
+ captured += selected.byteLength;
1847
+ }
1848
+ truncated ||= bytes.byteLength > remaining;
1849
+ };
1850
+ const onEnd = () => resolve({ text: Buffer.concat(chunks, captured).toString("utf8"), truncated });
1851
+ stream.on("data", onData);
1852
+ stream.once("end", onEnd);
1853
+ stream.once("error", reject);
1854
+ });
1855
+ var failureMessage = (command, stderr, exitCode) => {
1856
+ const fallback = `${command} exited with status ${exitCode}`;
1857
+ const suffix = stderr.truncated ? `
1858
+ [stderr truncated after 64 KiB]` : "";
1859
+ const available2 = 16384 - suffix.length;
1860
+ return `${(stderr.text.trim() || fallback).slice(0, available2)}${suffix}`;
1861
+ };
1816
1862
  var codexCommand = (options) => {
1817
1863
  const common = [
1818
1864
  "--json",
1865
+ "--skip-git-repo-check",
1819
1866
  "-c",
1820
1867
  'approval_policy="never"',
1821
1868
  ...options.model === undefined ? [] : ["--model", options.model],
@@ -1837,7 +1884,8 @@ var opencodeCommand = (options) => [
1837
1884
  "--format",
1838
1885
  "json",
1839
1886
  ...options.model === undefined ? [] : ["--model", options.model],
1840
- ...options.sessionId === undefined ? [] : ["--session", options.sessionId]
1887
+ ...options.sessionId === undefined ? [] : ["--session", options.sessionId],
1888
+ options.prompt
1841
1889
  ];
1842
1890
  var command = (options) => {
1843
1891
  switch (options.provider) {
@@ -1893,6 +1941,18 @@ var textFromPart = (value) => {
1893
1941
  }
1894
1942
  return text(part.text) ?? text(part.content) ?? text(record(part.part)?.text);
1895
1943
  };
1944
+ var openCodeError = (event) => {
1945
+ if (text(event.type) !== "error") {
1946
+ return;
1947
+ }
1948
+ const error = record(event.error);
1949
+ const name = text(error?.name);
1950
+ const message = text(record(error?.data)?.message) ?? text(error?.message) ?? text(event.message) ?? text(event.error);
1951
+ if (name !== undefined && message !== undefined) {
1952
+ return `OpenCode ${name}: ${message}`;
1953
+ }
1954
+ return message === undefined ? "OpenCode reported an error" : `OpenCode: ${message}`;
1955
+ };
1896
1956
  var parseOpenCode = (stdout, existingSession) => {
1897
1957
  let result = "";
1898
1958
  let sessionId = existingSession;
@@ -1905,6 +1965,10 @@ var parseOpenCode = (stdout, existingSession) => {
1905
1965
  if (event === undefined) {
1906
1966
  continue;
1907
1967
  }
1968
+ const failure = openCodeError(event);
1969
+ if (failure !== undefined) {
1970
+ throw new Error(failure);
1971
+ }
1908
1972
  sessionId = text(event.sessionID) ?? text(event.sessionId) ?? text(event.session_id) ?? text(record(event.info)?.sessionID) ?? sessionId;
1909
1973
  const content = textFromPart(event.part) ?? textFromPart(event) ?? text(event.result);
1910
1974
  if (content !== undefined && content.length > 0) {
@@ -1912,7 +1976,7 @@ var parseOpenCode = (stdout, existingSession) => {
1912
1976
  }
1913
1977
  }
1914
1978
  if (result.length === 0) {
1915
- throw new Error("OpenCode completed without a text response");
1979
+ throw new Error("OpenCode produced no result; verify its authentication and use a supported project workspace");
1916
1980
  }
1917
1981
  return { result, ...sessionId === undefined ? {} : { sessionId } };
1918
1982
  };
@@ -1929,19 +1993,14 @@ var parse = (options, stdout) => {
1929
1993
  var byteLength = (value) => new TextEncoder().encode(value).byteLength;
1930
1994
  var run = (options) => Effect2.tryPromise({
1931
1995
  try: async (signal) => {
1932
- if (!available(options.provider)) {
1996
+ const [commandName, ...arguments_] = command(options);
1997
+ const executable = commandName === undefined ? undefined : findExecutable(commandName);
1998
+ if (executable === undefined) {
1933
1999
  throw new ProviderUnavailableError({
1934
2000
  provider: options.provider,
1935
2001
  message: `${executables[options.provider]} is not available on this device`
1936
2002
  });
1937
2003
  }
1938
- const [executable, ...arguments_] = command(options);
1939
- if (executable === undefined) {
1940
- throw new ProviderRunError({
1941
- provider: options.provider,
1942
- message: `No command is configured for ${options.provider}`
1943
- });
1944
- }
1945
2004
  const child = spawn(executable, arguments_, {
1946
2005
  cwd: options.cwd,
1947
2006
  env: {
@@ -1969,10 +2028,10 @@ var run = (options) => Effect2.tryPromise({
1969
2028
  killTimer.unref();
1970
2029
  };
1971
2030
  signal.addEventListener("abort", terminate, { once: true });
1972
- child.stdin.end(options.prompt);
2031
+ child.stdin.end(options.provider === "opencode" ? undefined : options.prompt);
1973
2032
  const [stdout, stderr, exitCode] = await Promise.all([
1974
2033
  boundedText(child.stdout, 1024 * 1024, terminate),
1975
- boundedText(child.stderr, 64 * 1024, terminate),
2034
+ captureText(child.stderr, 64 * 1024),
1976
2035
  exited
1977
2036
  ]);
1978
2037
  signal.removeEventListener("abort", terminate);
@@ -1982,7 +2041,7 @@ var run = (options) => Effect2.tryPromise({
1982
2041
  if (exitCode !== 0) {
1983
2042
  throw new ProviderRunError({
1984
2043
  provider: options.provider,
1985
- message: (stderr.trim() || `${executables[options.provider]} exited with status ${exitCode}`).slice(0, 16384),
2044
+ message: failureMessage(executables[options.provider], stderr, exitCode),
1986
2045
  exitCode
1987
2046
  });
1988
2047
  }
@@ -2015,7 +2074,7 @@ var run = (options) => Effect2.tryPromise({
2015
2074
  // apps/device/src/cli.ts
2016
2075
  import { Effect as Effect5, Schema as Schema6 } from "effect";
2017
2076
  import { access as access2, mkdir as mkdir2, readFile as readFile2, stat, writeFile as writeFile2 } from "node:fs/promises";
2018
- import { homedir as homedir2 } from "node:os";
2077
+ import { homedir as homedir3 } from "node:os";
2019
2078
  import { dirname as dirname2, join as join3 } from "node:path";
2020
2079
 
2021
2080
  // skills/cohall/SKILL.md
@@ -2156,9 +2215,10 @@ Return the diagnostic failure to the user rather than claiming remote work ran.
2156
2215
  init_src();
2157
2216
  import { Effect as Effect3, Schema as Schema4 } from "effect";
2158
2217
  import { access, chmod, mkdir, readFile, realpath, rename, writeFile } from "node:fs/promises";
2159
- import { homedir, hostname, platform as platform2 } from "node:os";
2218
+ import { homedir as homedir2, hostname, platform as platform2 } from "node:os";
2160
2219
  import { dirname, join as join2, resolve } from "node:path";
2161
2220
  var WorkspaceList = Schema4.Array(Schema4.NonEmptyString).check(Schema4.isMaxLength(64));
2221
+ var ProviderList = Schema4.Array(Provider).check(Schema4.isMinLength(1)).check(Schema4.isMaxLength(3));
2162
2222
  var Sandbox = Schema4.Literals(["read-only", "workspace-write", "danger-full-access"]);
2163
2223
  var StoredConfiguration = Schema4.Struct({
2164
2224
  version: Schema4.Literal(1),
@@ -2168,6 +2228,7 @@ var StoredConfiguration = Schema4.Struct({
2168
2228
  workspaces: WorkspaceList,
2169
2229
  clientToken: Schema4.optionalKey(Schema4.NonEmptyString),
2170
2230
  deviceToken: Schema4.optionalKey(Schema4.NonEmptyString),
2231
+ providers: Schema4.optionalKey(ProviderList),
2171
2232
  model: Schema4.optionalKey(Schema4.NonEmptyString),
2172
2233
  sandbox: Schema4.optionalKey(Sandbox)
2173
2234
  });
@@ -2182,17 +2243,18 @@ var DeviceConfiguration = Schema4.Struct({
2182
2243
  id: DeviceId,
2183
2244
  name: Schema4.NonEmptyString,
2184
2245
  workspaces: WorkspaceList,
2246
+ providers: Schema4.optionalKey(ProviderList),
2185
2247
  model: Schema4.optionalKey(Schema4.NonEmptyString),
2186
2248
  sandbox: Schema4.optionalKey(Sandbox)
2187
2249
  });
2188
2250
  var defaultConfigDirectory = () => {
2189
2251
  if (platform2() === "win32") {
2190
- return join2(process.env.APPDATA ?? join2(homedir(), "AppData", "Roaming"), "Cohall");
2252
+ return join2(process.env.APPDATA ?? join2(homedir2(), "AppData", "Roaming"), "Cohall");
2191
2253
  }
2192
2254
  if (platform2() === "darwin") {
2193
- return join2(homedir(), "Library", "Application Support", "Cohall");
2255
+ return join2(homedir2(), "Library", "Application Support", "Cohall");
2194
2256
  }
2195
- return join2(process.env.XDG_CONFIG_HOME ?? join2(homedir(), ".config"), "cohall");
2257
+ return join2(process.env.XDG_CONFIG_HOME ?? join2(homedir2(), ".config"), "cohall");
2196
2258
  };
2197
2259
  var configurationPath = () => resolve(process.env.COHALL_CONFIG ?? join2(defaultConfigDirectory(), "config.json"));
2198
2260
  var normalizeRelayUrl = (input) => {
@@ -2217,6 +2279,9 @@ var parseWorkspaces = (workspaceList, workspaceJson) => Effect3.tryPromise({
2217
2279
  },
2218
2280
  catch: (cause) => new Error(`Workspace roots must be existing paths; JSON input must be an array of paths: ${cause instanceof Error ? cause.message : String(cause)}`)
2219
2281
  });
2282
+ var parseProviders = (input) => Schema4.decodeUnknownSync(ProviderList)([
2283
+ ...new Set(input.split(",").map((provider) => provider.trim()).filter(Boolean))
2284
+ ]);
2220
2285
  var readStoredConfiguration = async () => {
2221
2286
  const path = configurationPath();
2222
2287
  if (!await access(path).then(() => true).catch(() => false)) {
@@ -2251,6 +2316,7 @@ var makeStoredConfiguration = async (input) => {
2251
2316
  const canonicalWorkspaces = input.workspaces === undefined ? existing?.workspaces ?? [] : await Effect3.runPromise(parseWorkspaces("", JSON.stringify(input.workspaces)));
2252
2317
  const clientToken = input.clientToken ?? existing?.clientToken;
2253
2318
  const deviceToken = input.deviceToken ?? existing?.deviceToken;
2319
+ const providers = input.providers === "auto" ? undefined : input.providers ?? existing?.providers;
2254
2320
  return StoredConfiguration.make({
2255
2321
  version: 1,
2256
2322
  relayUrl: normalizeRelayUrl(input.relayUrl),
@@ -2259,6 +2325,7 @@ var makeStoredConfiguration = async (input) => {
2259
2325
  workspaces: canonicalWorkspaces,
2260
2326
  ...clientToken === undefined ? {} : { clientToken },
2261
2327
  ...deviceToken === undefined ? {} : { deviceToken },
2328
+ ...providers === undefined ? {} : { providers },
2262
2329
  ...existing?.model === undefined ? {} : { model: existing.model },
2263
2330
  ...existing?.sandbox === undefined ? {} : { sandbox: existing.sandbox }
2264
2331
  });
@@ -2276,6 +2343,10 @@ var environmentSandbox = () => {
2276
2343
  const value = process.env.COHALL_SANDBOX ?? process.env.COHALL_CODEX_SANDBOX;
2277
2344
  return value === undefined ? undefined : Schema4.decodeUnknownSync(Sandbox)(value);
2278
2345
  };
2346
+ var environmentProviders = (stored) => {
2347
+ const value = process.env.COHALL_DEVICE_PROVIDERS;
2348
+ return value === undefined ? stored?.providers : value === "auto" ? undefined : parseProviders(value);
2349
+ };
2279
2350
  var storedOrDefaults = async () => {
2280
2351
  const stored = await readStoredConfiguration();
2281
2352
  return stored ?? StoredConfiguration.make({
@@ -2328,12 +2399,14 @@ var loadDeviceConfiguration = Effect3.tryPromise({
2328
2399
  }
2329
2400
  const model = environmentProvider() ?? stored.model;
2330
2401
  const sandbox = environmentSandbox() ?? stored.sandbox;
2402
+ const providers = environmentProviders(stored);
2331
2403
  return DeviceConfiguration.make({
2332
2404
  relayUrl: normalizeRelayUrl(process.env.COHALL_RELAY_URL ?? stored.relayUrl),
2333
2405
  token,
2334
2406
  id: process.env.COHALL_DEVICE_ID === undefined ? stored.deviceId : DeviceId.make(process.env.COHALL_DEVICE_ID),
2335
2407
  name: process.env.COHALL_DEVICE_NAME ?? stored.deviceName,
2336
2408
  workspaces,
2409
+ ...providers === undefined ? {} : { providers },
2337
2410
  ...model === undefined ? {} : { model },
2338
2411
  ...sandbox === undefined ? {} : { sandbox }
2339
2412
  });
@@ -2446,6 +2519,7 @@ var valueOptions = new Set([
2446
2519
  "prompt",
2447
2520
  "prompt-file",
2448
2521
  "provider",
2522
+ "providers",
2449
2523
  "relay",
2450
2524
  "sandbox",
2451
2525
  "target",
@@ -2465,8 +2539,9 @@ var aliases = new Map([
2465
2539
  var help = `Cohall connects agents running on your own devices.
2466
2540
 
2467
2541
  Usage:
2468
- cohall join --relay <url> --workspace <path> [--token-file <path>]
2469
- cohall configure [--relay url] [--name name] [--workspace path] [--model id]
2542
+ cohall join --relay <url> --workspace <path> [--providers list] [--token-file <path>]
2543
+ cohall configure [--relay url] [--name name] [--workspace path]
2544
+ [--providers codex,opencode|auto] [--model id]
2470
2545
  cohall config
2471
2546
  cohall devices
2472
2547
  cohall delegate [prompt] [--target @device] [--provider provider]
@@ -2641,9 +2716,9 @@ var ownerClient = async () => {
2641
2716
  };
2642
2717
  var installSkill = async (target) => {
2643
2718
  const roots = {
2644
- agents: join3(homedir2(), ".agents", "skills", "cohall", "SKILL.md"),
2645
- claude: join3(homedir2(), ".claude", "skills", "cohall", "SKILL.md"),
2646
- opencode: join3(homedir2(), ".config", "opencode", "skills", "cohall", "SKILL.md")
2719
+ agents: join3(homedir3(), ".agents", "skills", "cohall", "SKILL.md"),
2720
+ claude: join3(homedir3(), ".claude", "skills", "cohall", "SKILL.md"),
2721
+ opencode: join3(homedir3(), ".config", "opencode", "skills", "cohall", "SKILL.md")
2647
2722
  };
2648
2723
  const selected = target === "all" ? Object.values(roots) : (target in roots) ? [roots[target]] : undefined;
2649
2724
  if (selected === undefined) {
@@ -2660,7 +2735,14 @@ var printSkill = () => console.log(SKILL_default.trimEnd());
2660
2735
  var runCli = async (command2, raw) => {
2661
2736
  const arguments_ = parseArguments(raw);
2662
2737
  if (command2 === "join") {
2663
- allowOptions(arguments_, ["client-only", "name", "relay", "token-file", "workspace"]);
2738
+ allowOptions(arguments_, [
2739
+ "client-only",
2740
+ "name",
2741
+ "providers",
2742
+ "relay",
2743
+ "token-file",
2744
+ "workspace"
2745
+ ]);
2664
2746
  noPositionals(arguments_, command2);
2665
2747
  const token = await pairingToken(arguments_);
2666
2748
  const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? "http://127.0.0.1:8787");
@@ -2668,13 +2750,16 @@ var runCli = async (command2, raw) => {
2668
2750
  const suppliedWorkspaces = values(arguments_, "workspace");
2669
2751
  const workspaces = suppliedWorkspaces.length === 0 ? existing?.workspaces : suppliedWorkspaces;
2670
2752
  const deviceName = option(arguments_, "name");
2753
+ const providerInput = option(arguments_, "providers");
2754
+ const providers = providerInput === undefined ? undefined : providerInput === "auto" ? "auto" : parseProviders(providerInput);
2671
2755
  if (!arguments_.options.has("client-only") && (workspaces?.length ?? 0) === 0) {
2672
2756
  throw new Error("At least one --workspace is required when joining a device");
2673
2757
  }
2674
2758
  const draft = await makeStoredConfiguration({
2675
2759
  relayUrl,
2676
2760
  ...deviceName === undefined ? {} : { deviceName },
2677
- ...workspaces === undefined ? {} : { workspaces }
2761
+ ...workspaces === undefined ? {} : { workspaces },
2762
+ ...providers === undefined ? {} : { providers }
2678
2763
  });
2679
2764
  const paired = await Effect5.runPromise(exchangePairing(relayUrl, { token }));
2680
2765
  const clientToken = paired.credentials.find((credential) => credential.session.role === "client")?.token;
@@ -2714,6 +2799,7 @@ var runCli = async (command2, raw) => {
2714
2799
  workspaces: configuration2.workspaces,
2715
2800
  client_credential: configuration2.clientToken !== undefined,
2716
2801
  device_credential: configuration2.deviceToken !== undefined,
2802
+ providers: configuration2.providers ?? "auto",
2717
2803
  model: configuration2.model,
2718
2804
  sandbox: configuration2.sandbox
2719
2805
  }
@@ -2721,7 +2807,7 @@ var runCli = async (command2, raw) => {
2721
2807
  return;
2722
2808
  }
2723
2809
  if (command2 === "configure") {
2724
- allowOptions(arguments_, ["model", "name", "relay", "sandbox", "workspace"]);
2810
+ allowOptions(arguments_, ["model", "name", "providers", "relay", "sandbox", "workspace"]);
2725
2811
  noPositionals(arguments_, command2);
2726
2812
  const existing = await readStoredConfiguration();
2727
2813
  const relayUrl = normalizeRelayUrl(option(arguments_, "relay") ?? existing?.relayUrl ?? "http://127.0.0.1:8787");
@@ -2729,6 +2815,8 @@ var runCli = async (command2, raw) => {
2729
2815
  const workspaces = supplied.length === 0 ? existing?.workspaces ?? [] : await Effect5.runPromise(parseWorkspaces("", JSON.stringify(supplied)));
2730
2816
  const sandbox = option(arguments_, "sandbox");
2731
2817
  const model = option(arguments_, "model") ?? existing?.model;
2818
+ const providerInput = option(arguments_, "providers");
2819
+ const providers = providerInput === undefined ? existing?.providers : providerInput === "auto" ? undefined : parseProviders(providerInput);
2732
2820
  const configuration2 = StoredConfiguration.make({
2733
2821
  version: 1,
2734
2822
  relayUrl,
@@ -2737,6 +2825,7 @@ var runCli = async (command2, raw) => {
2737
2825
  workspaces,
2738
2826
  ...existing?.clientToken === undefined ? {} : { clientToken: existing.clientToken },
2739
2827
  ...existing?.deviceToken === undefined ? {} : { deviceToken: existing.deviceToken },
2828
+ ...providers === undefined ? {} : { providers },
2740
2829
  ...model === undefined ? {} : { model },
2741
2830
  ...(sandbox ?? existing?.sandbox) === undefined ? {} : {
2742
2831
  sandbox: Schema6.decodeUnknownSync(Schema6.Literals(["read-only", "workspace-write", "danger-full-access"]))(sandbox ?? existing?.sandbox)
@@ -2764,8 +2853,16 @@ var runCli = async (command2, raw) => {
2764
2853
  print({
2765
2854
  cli_skill: "npx -y @akshar5/cohall skill install all",
2766
2855
  codex_mcp: "codex mcp add cohall -- npx -y @akshar5/cohall mcp",
2767
- claude_mcp: "claude mcp add --scope user cohall -- npx -y @akshar5/cohall mcp",
2768
- opencode_mcp: "Run opencode mcp add and use: npx -y @akshar5/cohall mcp"
2856
+ claude_mcp: "claude mcp add --transport stdio --scope user cohall -- npx -y @akshar5/cohall mcp",
2857
+ stdio_config: {
2858
+ command: "npx",
2859
+ args: ["-y", "@akshar5/cohall", "mcp"]
2860
+ },
2861
+ opencode_mcp: {
2862
+ type: "local",
2863
+ command: ["npx", "-y", "@akshar5/cohall", "mcp"],
2864
+ enabled: true
2865
+ }
2769
2866
  });
2770
2867
  return;
2771
2868
  }
@@ -2779,17 +2876,39 @@ var runCli = async (command2, raw) => {
2779
2876
  }).catch(() => {
2780
2877
  return;
2781
2878
  });
2879
+ const providerInput = process.env.COHALL_DEVICE_PROVIDERS;
2880
+ const selectedProviders = providerInput === undefined ? configuration2?.providers : providerInput === "auto" ? undefined : parseProviders(providerInput);
2881
+ const providerExecutables = Object.fromEntries(Provider.literals.map((provider) => [
2882
+ provider,
2883
+ findExecutable(provider === "claude-code" ? "claude" : provider) ?? "not found"
2884
+ ]));
2885
+ const clientToken = process.env.COHALL_CLIENT_TOKEN ?? configuration2?.clientToken;
2886
+ const hasDeviceCredential = process.env.COHALL_DEVICE_TOKEN !== undefined || configuration2?.deviceToken !== undefined;
2887
+ const deviceId = process.env.COHALL_DEVICE_ID ?? configuration2?.deviceId;
2888
+ const currentDevice = response?.ok !== true || clientToken === undefined || deviceId === undefined ? undefined : await Effect5.runPromise(exports_src.make({ baseUrl: relayUrl, token: clientToken }).devices()).then((devices) => devices.find((device) => device.id === deviceId)).catch(() => {
2889
+ return;
2890
+ });
2891
+ const warnings = [
2892
+ ...response?.ok === true ? [] : ["Relay is unreachable"],
2893
+ ...hasDeviceCredential && response?.ok === true && currentDevice?.status !== "online" && currentDevice?.status !== "busy" ? [
2894
+ clientToken === undefined ? "Device status cannot be checked without a client credential" : "Device daemon is not connected or is not registered with the relay"
2895
+ ] : [],
2896
+ ...selectedProviders?.flatMap((provider) => providerExecutables[provider] === "not found" ? [`Configured provider ${provider} is not installed or not on PATH`] : []) ?? []
2897
+ ];
2782
2898
  print({
2899
+ version,
2783
2900
  relay: response?.ok === true ? "reachable" : "unreachable",
2784
2901
  relay_url: relayUrl,
2785
2902
  config_path: configurationPath(),
2786
2903
  client_credential: process.env.COHALL_CLIENT_TOKEN !== undefined || configuration2?.clientToken !== undefined,
2787
- device_credential: process.env.COHALL_DEVICE_TOKEN !== undefined || configuration2?.deviceToken !== undefined,
2904
+ device_credential: hasDeviceCredential,
2788
2905
  workspaces: configuration2?.workspaces ?? [],
2789
- providers: Object.fromEntries(Provider.literals.map((provider) => [
2790
- provider,
2791
- findExecutable(provider === "claude-code" ? "claude" : provider) ?? "not found"
2792
- ]))
2906
+ providers: providerExecutables,
2907
+ provider_selection: selectedProviders ?? "auto",
2908
+ provider_authentication: "checked when delegated work starts",
2909
+ device_status: hasDeviceCredential === false ? "not configured" : currentDevice?.status ?? "unknown",
2910
+ device_version: currentDevice?.version,
2911
+ warnings
2793
2912
  });
2794
2913
  return;
2795
2914
  }
@@ -2933,7 +3052,7 @@ var capabilities = (providers) => {
2933
3052
  const values2 = providers.map((provider) => ({
2934
3053
  id: provider,
2935
3054
  label: providerLabel(provider),
2936
- detail: `Uses this device's local ${providerLabel(provider)} login, configuration, and tools`
3055
+ detail: `${providerLabel(provider)} executable detected; authentication is checked when work starts`
2937
3056
  }));
2938
3057
  if (findExecutable("google-chrome") !== undefined || findExecutable("chromium") !== undefined || platform3() === "darwin") {
2939
3058
  values2.push({ id: "browser-session", label: "Signed-in browser" });
@@ -2946,10 +3065,12 @@ var capabilities = (providers) => {
2946
3065
  }
2947
3066
  return values2;
2948
3067
  };
3068
+ var selectProviders = (installed, configured) => configured === undefined ? installed : configured.filter((provider) => installed.includes(provider));
2949
3069
  var describeDevice = (configuration, status = "online") => {
2950
3070
  const operatingSystem = platform3();
2951
3071
  const platformName = operatingSystem === "darwin" || operatingSystem === "linux" ? operatingSystem : operatingSystem === "win32" ? "windows" : "unknown";
2952
- const providers = availableProviders();
3072
+ const installed = availableProviders();
3073
+ const providers = selectProviders(installed, configuration.providers);
2953
3074
  return Device.make({
2954
3075
  id: configuration.id,
2955
3076
  name: configuration.name,
@@ -3284,6 +3405,14 @@ var main = async () => {
3284
3405
  console.log(version);
3285
3406
  return;
3286
3407
  }
3408
+ if (["device", "relay", "mcp"].includes(command2) && process.argv.length > 3) {
3409
+ const arguments_ = process.argv.slice(3);
3410
+ if (arguments_.length === 1 && ["--help", "-h", "help"].includes(arguments_[0] ?? "")) {
3411
+ printHelp();
3412
+ return;
3413
+ }
3414
+ throw new Error(`${command2} does not accept arguments`);
3415
+ }
3287
3416
  if (command2 === "relay") {
3288
3417
  const { runRelay: runRelay2 } = await init_main().then(() => exports_main);
3289
3418
  await runRelay2();
@@ -3306,4 +3435,4 @@ await main().catch((cause) => {
3306
3435
  process.exitCode = 1;
3307
3436
  });
3308
3437
 
3309
- //# debugId=AB53FFE376B97AD664756E2164756E21
3438
+ //# debugId=480B573B626A966164756E2164756E21