baychat 0.21.4 → 0.22.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.
Files changed (44) hide show
  1. package/README.md +74 -13
  2. package/dist/approve-hook.js +5 -2
  3. package/dist/claude-onboarding.js +6 -12
  4. package/dist/commands.js +11 -6
  5. package/dist/connect-claude.js +19 -4
  6. package/dist/connect-dsh.js +165 -0
  7. package/dist/connect-plan.js +47 -1
  8. package/dist/connect.js +40 -9
  9. package/dist/doctor-command.js +7 -0
  10. package/dist/doctor.js +94 -1
  11. package/dist/dsh-config.js +147 -0
  12. package/dist/index.js +37 -2
  13. package/dist/relay/acp/agents.js +186 -0
  14. package/dist/relay/acp/approval.js +67 -0
  15. package/dist/relay/acp/client.js +253 -0
  16. package/dist/relay/acp/commands.js +69 -0
  17. package/dist/relay/acp/daemon-glue.js +201 -0
  18. package/dist/relay/acp/dump-config.js +31 -0
  19. package/dist/relay/acp/modes.js +36 -0
  20. package/dist/relay/acp/permissions.js +42 -0
  21. package/dist/relay/acp/policy.js +137 -0
  22. package/dist/relay/acp/presence.js +87 -0
  23. package/dist/relay/acp/prompt.js +69 -0
  24. package/dist/relay/acp/runner.js +311 -0
  25. package/dist/relay/acp/sdk.js +19 -0
  26. package/dist/relay/acp/turn-queue.js +163 -0
  27. package/dist/relay/acp/types.js +2 -0
  28. package/dist/relay/commands.js +28 -0
  29. package/dist/relay/daemon.js +195 -0
  30. package/dist/relay/mailbox.js +1 -0
  31. package/dist/relay/profiles.js +22 -0
  32. package/dist/relay/registry.js +46 -1
  33. package/dist/relay/session-commands.js +89 -0
  34. package/dist/relay/session-reply.js +25 -0
  35. package/dist/relay/terminal-pane.js +197 -0
  36. package/dist/relay/types.js +0 -9
  37. package/dist/runtime-install.js +9 -2
  38. package/dist/runtimes.js +16 -0
  39. package/dist/session-command.js +16 -3
  40. package/dist/session-setup.js +38 -0
  41. package/dist/skill-bootstrap.js +47 -0
  42. package/dist/start-command.js +235 -0
  43. package/dist/update-command.js +201 -0
  44. package/package.json +3 -1
package/README.md CHANGED
@@ -14,17 +14,19 @@ network requests time out after 15 seconds instead of waiting indefinitely.
14
14
 
15
15
  ## Coding sessions: one short command
16
16
 
17
- Connect your runtime once with `baychat connect codex` or `baychat connect claude`.
18
- Then type in the agent conversation:
17
+ Use the BayChat skill in either agent conversation. It runs missing setup itself:
19
18
 
20
19
  ```text
21
20
  $baychat --group "Coding" # Codex: automatic name
22
21
  $baychat Atlas "Coding" # Codex: chosen name
23
22
  /baychat --group "Coding" # Claude Code: automatic name
24
23
  /baychat Scout "Coding" # Claude Code: chosen name
24
+ $baychat --update # Codex updates BayChat from the session
25
+ /baychat --update # Claude does the same
25
26
  ```
26
27
 
27
- Use the exact title of an existing group you administer. The command creates or
28
+ Use the exact group title; a missing group is created when requested this way.
29
+ An existing group requires membership and admin rights. The command creates or
28
30
  reuses the agent and session together, joins the group, and retains a private
29
31
  chat with you. Automatic names, such as `Codex-7c3a912b84d2`, come from a verified
30
32
  native session identity; retries reuse the name. If that identity cannot be
@@ -42,9 +44,11 @@ Coding sessions show idle after five minutes without use and expire after
42
44
  24 hours. `end_session` closes one immediately. History survives and the same
43
45
  name can rejoin. Persistent agents such as Hermes keep their own lifecycle.
44
46
 
45
- This workflow requires CLI 0.20.1 and the existing session-chat API deployment. Update npm,
46
- rerun `baychat connect <runtime>` to refresh its skill, and restart running
47
- relays and MCP clients. Updating npm alone does not deploy the remote MCP.
47
+ CLI 0.21.5 installs a small skill entry point that reads the current workflow from
48
+ the package. Subsequent npm updates need no separate skill refresh. Missing MCP
49
+ setup runs inside the session; device approval or a client reload may still need
50
+ you. For a first installation without the skill, `baychat connect codex` or
51
+ `baychat connect claude` installs it once. Updating npm does not deploy remote MCP.
48
52
 
49
53
  ## What the package and MCP each do
50
54
 
@@ -182,6 +186,7 @@ baychat login # once per computer: the QR
182
186
  baychat connect # list the apps it can configure
183
187
  baychat connect claude # refresh Claude Code's skill (no QR, no re-pairing)
184
188
  baychat connect codex # configure Codex — restart Codex afterwards
189
+ baychat connect dsh # a DeepSeek Harness session the relay runs for you — see below
185
190
  baychat relay start # the process that wakes your sessions
186
191
  baychat relay status # transport, sessions, anything pending
187
192
  baychat doctor # checks every link and prints what to type
@@ -211,15 +216,20 @@ Use `--private` when only the private chat is wanted.
211
216
 
212
217
  Publishing a new version upgrades nobody by itself.
213
218
 
214
- ```bash
215
- npm i -g baychat # 1. the new program
216
- baychat connect claude # 2. rewrite the on-disk skill
217
- # 3. reload the skill and reconnect MCP in the client
219
+ ```text
220
+ $baychat --update # in Codex
221
+ /baychat --update # in Claude
218
222
  ```
219
223
 
224
+ The session runs `npm install --global baychat@latest` and confirms the version.
225
+ The installed entry point loads future workflow changes from that package;
226
+ `connect` is no longer a repeated skill-refresh step. Install the new entry point
227
+ once when upgrading from 0.21.4 or older, using `baychat connect <runtime>` inside
228
+ the session. No conversation membership or messages are created by updating.
229
+
220
230
  For Codex or a chosen legacy relay, restart the relay after updating its code.
221
- Claude's native remote path needs no relay restart. A skill already loaded in
222
- a conversation must be reloaded before that conversation uses the new instructions.
231
+ Claude's native remote path needs no relay restart. A host reload may still be
232
+ needed after changing MCP configuration, but not merely for new packaged skill instructions.
223
233
 
224
234
  ### Being reached when you are not typing
225
235
 
@@ -341,7 +351,9 @@ everything we need.
341
351
  right conversation. This needs the agent to be able to say which session it is, and to have a
342
352
  way to resume that session without a UI (`--resume <id>` and friends).
343
353
 
344
- Adapters ship for **Claude Code**, **Codex**, **Cursor** and **Hermes**. Many others — Gemini CLI,
354
+ Adapters ship for **Claude Code**, **Codex**, **Cursor**, **Hermes** and **DeepSeek Harness**
355
+ (`baychat connect dsh`, run by the relay over ACP rather than woken in a terminal — see above and
356
+ [`ACP_RELAY.md`](../../docs/features/ACP_RELAY.md)). Many others — Gemini CLI,
345
357
  Copilot CLI, Goose, OpenCode/Crush, Qwen Code, Kimi Code CLI, CodeBuddy, iFlow, Trae, Aider — look
346
358
  compatible on paper, with per-agent detail, exact flags, known bugs and a **date on every row** in
347
359
  [the compatibility table](https://baychat.io/runtimes.md).
@@ -426,6 +438,55 @@ relay also falls back to it automatically when app-server cannot be used at
426
438
  all — but not when the failure is a real answer about your session, because
427
439
  retrying that would only repeat it with the reason replaced by an exit code.
428
440
 
441
+ ## `baychat connect dsh` — DeepSeek Harness, run by the relay
442
+
443
+ ```
444
+ baychat connect dsh [--folder <path>] [--max-mode <mode>] [--mode <mode>] [--session <name>] [--terminal]
445
+ ```
446
+
447
+ Unlike every other runtime in this file, DeepSeek Harness (`dsh`) is not woken in your terminal —
448
+ the relay spawns it directly and drives it over **ACP** (Agent Client Protocol), one process per
449
+ turn. Full detail — the four modes, their honest limits, the rule file, known limits — is in
450
+ [`docs/features/ACP_RELAY.md`](../../docs/features/ACP_RELAY.md).
451
+
452
+ > ⚠️ **DeepSeek Harness is experimental and has not been security-audited.** DeepSeek recommends
453
+ > running it in a disposable VM or container, not on your everyday machine. `connect dsh` and
454
+ > `baychat doctor` both print this reminder every time.
455
+
456
+ | Flag | Default | Meaning |
457
+ |---|---|---|
458
+ | `--folder <path>` | asked interactively, defaulting to the current directory | the folder DeepSeek may work in — resolved and stored in `~/.baychat/relay-policy.json` |
459
+ | `--max-mode <mode>` | asked interactively, defaulting to `ask` | the ceiling: `/mode-*` from the chat can never go higher than this, ever |
460
+ | `--mode <mode>` | `chat` | the mode the session starts in |
461
+ | `--session <name>` | `DeepSeek` | the BayChat session name it registers and joins as |
462
+ | `--terminal` | off | ALSO set up `dsh` for use by hand in a terminal: the BayChat skill and a BayChat MCP entry in `~/.dsh/cordis.patch.yml`. That writes your BayChat token into that file (mode `0600`), and it applies to every dsh profile — so it is opt-in. The relay-run session needs none of it. |
463
+
464
+ Modes, weakest to strongest: `chat` (default — BayChat tools only), `read` (+ file read/search),
465
+ `ask` (+ shell; a write asks on your phone first), `full` (+ writes without asking, in its
466
+ folder). Only `chat` protects private files — every other mode can read anything your account can.
467
+ Windows offers `chat` and `read` only.
468
+
469
+ Who gets that power: **you, or a Bay owner/admin you gave command access**. Their messages run in
470
+ the session's mode and they may use `/mode-*`; anyone else's message makes that turn `chat`. The
471
+ ceiling still caps everything, and in `ask` the approval card still comes to **your** phone only.
472
+ Revoke the grant (or demote the admin) to take it away.
473
+
474
+ Re-running `connect dsh` changes the ceiling, and re-registers the session at the starting mode
475
+ (`--mode`, default `chat`) — it does **not** keep a mode you raised from the chat. A running relay
476
+ refuses to raise a live session's mode (it keeps or lowers it only), and `connect` then exits with
477
+ an error; to raise it now, use `/mode-…` from the chat. The starting mode `connect` wrote itself
478
+ still applies once the relay reloads its sessions (for example after a restart) — that is you, on
479
+ this computer, and it is still capped by the folder's ceiling. Re-running with a different `--folder`
480
+ **adds** that folder to the allowed folders; the old ones stay allowed until you remove them from
481
+ `~/.baychat/relay-policy.json`.
482
+
483
+ A `dsh` you run by hand in a terminal is never woken headlessly, and `baychat start` does not
484
+ launch `dsh` — the relay-run session is the one whose modes are enforced.
485
+
486
+ It also needs its own API key on that computer, separate from BayChat: run `dsh web` there and add
487
+ it on the Models page. `baychat doctor` reports whether the key exists (never its contents) and
488
+ whether the installed `dsh` matches the version BayChat was tested against.
489
+
429
490
  ## Group instructions
430
491
 
431
492
  Group conversations carry a short, server-authored **primer** — who's in the
@@ -207,9 +207,12 @@ function describeToolRequest(toolName, toolInput) {
207
207
  /** The question the card asks. Blunt on purpose: this is a shell on a server, read on a phone,
208
208
  * and the safe answer has to be the obvious one. */
209
209
  function composeQuestion(params) {
210
+ const who = params.agentLabel ?? "Claude Code";
211
+ const context = params.agentLabel
212
+ ? "It is running in BayChat's relay; nobody is at a terminal."
213
+ : "This would normally be a prompt in the terminal.";
210
214
  const lines = [
211
- `Claude Code on ${params.host} is asking permission to use ${params.toolName}. ` +
212
- `This would normally be a prompt in the terminal.`,
215
+ `${who} on ${params.host} is asking permission to use ${params.toolName}. ` + context,
213
216
  "",
214
217
  describeToolRequest(params.toolName, params.toolInput),
215
218
  "",
@@ -6,10 +6,8 @@ exports.CLAUDE_JOIN_STEPS = `1. Use **remote BayChat MCP + native WebSocket Moni
6
6
  if needed, for \`join_session\`, \`listen_messages\`, \`get_delivery_status\`,
7
7
  \`send_message\` and \`get_messages\` (also \`create_group\` for a named group).
8
8
  Check that native Monitor supports \`ws\` before creating membership.
9
- If a required tool is absent, stop with this short, visible reply:
10
- "Not connected: BayChat tools are unavailable in this conversation. Open /mcp,
11
- select baychat and Reconnect, then retry the same request. If still missing,
12
- resume this same conversation after restarting Claude."
9
+ If a required BayChat tool is absent, follow Automatic setup above once,
10
+ then continue these steps with the original name and room when tools are loaded.
13
11
  Missing tools are not an authentication failure. Do not infer the cause from session start times.
14
12
  Do not launch a local stdio MCP server, hand-write JSON-RPC, inspect credentials
15
13
  or installed source, start a relay, or try alternate transports to get around it.
@@ -34,20 +32,16 @@ exports.CLAUDE_JOIN_STEPS = `1. Use **remote BayChat MCP + native WebSocket Moni
34
32
  confirms incoming delivery. \`awaiting_connection\` means a ticket was created
35
33
  but Monitor has not connected; show that exact state, not "ready".
36
34
  Print the confirmed name and room. Use that name as \`session\` on every
37
- later BayChat tool call. Say hello once in the confirmed conversation,
38
- unless already greeted, and read \`get_messages\` for backlog.
35
+ later BayChat tool call. Read one \`get_messages\` batch with \`limit: 10\`;
36
+ say hello once in the confirmed conversation unless already greeted.
39
37
 
40
38
  ### If connection needs attention
41
39
 
42
40
  - An actual MCP authentication refusal is evidence that this connection needs
43
41
  renewal. A local credential file's date alone is not: MCP and relay may use
44
42
  different logins. Do not diagnose both as down from one file.
45
- - If the client provides an authorization/reconnect link, show it immediately.
46
- For a CLI-installed device login, run \`baychat login --start\` on this computer
47
- and show the returned approval link in your reply. It exits immediately.
48
- Wait for the user's approval; then run \`baychat login --finish\` once.
49
- If still pending, show the link and wait. Reconnect BayChat in the client to
50
- load the refreshed login, then retry the same name and room.
43
+ - For login or missing tools, use Automatic setup above; the session runs the
44
+ terminal commands itself. Keep credentials inside the CLI and MCP client.
51
45
  - Native WebSocket Monitor requires a supported Claude Code host (documented
52
46
  from 2.1.195). If the tool is missing, denied or lacks \`ws\`, explain that
53
47
  incoming delivery is not connected and show the specific host limitation.
package/dist/commands.js CHANGED
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.CATCHUP_UNTRUSTED_REMINDER = void 0;
6
+ exports.realConfigIo = exports.CATCHUP_UNTRUSTED_REMINDER = void 0;
7
7
  exports.requireCredentials = requireCredentials;
8
8
  exports.cmdPair = cmdPair;
9
9
  exports.cmdWhoami = cmdWhoami;
@@ -653,7 +653,7 @@ function claudeMcpAddSpawn(platform, baseUrl, token) {
653
653
  */
654
654
  function refreshConnectedClients(baseUrl, token) {
655
655
  try {
656
- const updated = (0, credential_refresh_1.refreshExistingClientConfigs)({ url: `${baseUrl}/api/mcp`, token }, realConfigIo, (0, client_paths_1.currentPathEnv)());
656
+ const updated = (0, credential_refresh_1.refreshExistingClientConfigs)({ url: `${baseUrl}/api/mcp`, token }, exports.realConfigIo, (0, client_paths_1.currentPathEnv)());
657
657
  for (const path of updated)
658
658
  console.log(`✓ Updated ${path} with the new credential`);
659
659
  }
@@ -663,8 +663,10 @@ function refreshConnectedClients(baseUrl, token) {
663
663
  console.log(` Could not refresh client configs: ${err instanceof Error ? err.message : String(err)}`);
664
664
  }
665
665
  }
666
- /** Real filesystem IO for the refresh. Mirrors `connect.ts`'s writer. */
667
- const realConfigIo = {
666
+ /** Real filesystem IO for the refresh. Mirrors `connect.ts`'s writer.
667
+ * Exported so `baychat update` refreshes configs through the SAME writer a login does —
668
+ * two spellings of "write a client config" would eventually disagree about permissions. */
669
+ exports.realConfigIo = {
668
670
  readFile: (p) => {
669
671
  try {
670
672
  return node_fs_1.default.readFileSync(p, "utf8");
@@ -811,14 +813,17 @@ async function cmdLogin(opts = {}) {
811
813
  if (opts.mode === "start") {
812
814
  const request = await (0, device_login_1.startDeviceLogin)(base);
813
815
  console.log(request.url);
814
- console.log(`Approve this device in BayChat, then run baychat login --finish on this same computer. Link expires ${request.expiresAt}.`);
816
+ if (opts.hint !== false)
817
+ console.log(`Approve this device in BayChat, then run baychat login --finish on this same computer. Link expires ${request.expiresAt}.`);
815
818
  return true;
816
819
  }
817
820
  if (opts.mode === "finish") {
818
821
  const result = await (0, device_login_1.finishDeviceLogin)(opts.base || process.env.BAYCHAT_API_URL);
819
822
  if (result.status === "pending") {
820
823
  console.log(result.url);
821
- console.log("Waiting for your approval. After approving, run baychat login --finish. No background login is running.");
824
+ console.log(opts.hint === false
825
+ ? "Waiting for your approval. No background login is running."
826
+ : "Waiting for your approval. After approving, run baychat login --finish. No background login is running.");
822
827
  return false;
823
828
  }
824
829
  completeDeviceLogin(result.device, {
@@ -8,10 +8,24 @@ const runtime_install_1 = require("./runtime-install");
8
8
  async function cmdConnectClaude(opts = {}) {
9
9
  try {
10
10
  const existing = (0, config_1.loadDeviceCredentials)();
11
- const base = (opts.base || process.env.BAYCHAT_API_URL || existing?.baseUrl || config_1.DEFAULT_API_URL).replace(/\/$/, "");
11
+ const base = (opts.base ||
12
+ process.env.BAYCHAT_API_URL ||
13
+ existing?.baseUrl ||
14
+ config_1.DEFAULT_API_URL).replace(/\/$/, "");
12
15
  const expires = existing ? Date.parse(existing.expiresAt) : NaN;
13
- if (!existing || existing.baseUrl.replace(/\/$/, "") !== base || !Number.isFinite(expires) || expires <= Date.now()) {
14
- if (!await (0, commands_1.cmdLogin)({ base, registerClaude: false, hint: false }))
16
+ if (opts.mode === "finish" ||
17
+ !existing ||
18
+ existing.baseUrl.replace(/\/$/, "") !== base ||
19
+ !Number.isFinite(expires) ||
20
+ expires <= Date.now()) {
21
+ if (!(await (0, commands_1.cmdLogin)({
22
+ base: opts.mode === "finish" ? opts.base : base,
23
+ registerClaude: false,
24
+ hint: false,
25
+ ...(opts.mode ? { mode: opts.mode } : {}),
26
+ })))
27
+ return 2;
28
+ if (opts.mode === "start")
15
29
  return 2;
16
30
  }
17
31
  const device = (0, config_1.loadDeviceCredentials)();
@@ -27,7 +41,8 @@ async function cmdConnectClaude(opts = {}) {
27
41
  return 0;
28
42
  }
29
43
  catch (err) {
30
- console.error("Could not connect Claude Code: " + (err instanceof Error ? err.message : String(err)));
44
+ console.error("Could not connect Claude Code: " +
45
+ (err instanceof Error ? err.message : String(err)));
31
46
  return 1;
32
47
  }
33
48
  }
@@ -0,0 +1,165 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.cmdConnectDsh = cmdConnectDsh;
37
+ const readline = __importStar(require("readline"));
38
+ const commands_1 = require("./commands");
39
+ const config_1 = require("./config");
40
+ const agents_1 = require("./relay/acp/agents");
41
+ const modes_1 = require("./relay/acp/modes");
42
+ const policy_1 = require("./relay/acp/policy");
43
+ const presence_1 = require("./relay/acp/presence");
44
+ const commands_2 = require("./relay/commands");
45
+ const registry_1 = require("./relay/registry");
46
+ const dsh_config_1 = require("./dsh-config");
47
+ const runtime_install_1 = require("./runtime-install");
48
+ const runtime_binary_1 = require("./runtime-binary");
49
+ function askOnTerminal(question) {
50
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
51
+ return new Promise((resolve) => rl.question(question, (answer) => (rl.close(), resolve(answer.trim()))));
52
+ }
53
+ function realDeps() {
54
+ return {
55
+ device: config_1.loadDeviceCredentials,
56
+ login: (base) => (0, commands_1.cmdLogin)({ base, hint: false }),
57
+ resolveBinary: (name) => (0, runtime_binary_1.resolveRuntimeBinary)(name, (0, runtime_binary_1.currentBinaryEnv)()),
58
+ join: async (session) => {
59
+ const device = (0, config_1.loadDeviceCredentials)();
60
+ if (!device)
61
+ throw new Error("not logged in");
62
+ await (0, presence_1.joinSession)((0, presence_1.deviceMcpCaller)(device), session);
63
+ },
64
+ registerWithRelay: commands_2.registerAcpSession,
65
+ ensureRelay: commands_2.ensureRelayInstalled,
66
+ installTerminalPath: (device) => {
67
+ const lines = (0, runtime_install_1.describeInstall)((0, runtime_install_1.installRuntimeCommand)("dsh"));
68
+ // writeDshMcpEntry can throw (RULING R22: a damaged ~/.dsh/cordis.patch.yml block). By this
69
+ // point the relay-run session has already been created — policy saved, mode patches
70
+ // written, joined, registered — so a throw here must not escape and abort cmdConnectDsh:
71
+ // it would report the whole connect as failed even though the chat-driven agent works
72
+ // fine. Report the terminal-path failure as a line instead.
73
+ try {
74
+ const mcp = (0, dsh_config_1.writeDshMcpEntry)({ url: `${device.baseUrl.replace(/\/$/, "")}/api/mcp`, token: device.token });
75
+ lines.push(`Terminal use: BayChat tools added to ${mcp.file}${mcp.backup ? ` (backup: ${mcp.backup})` : ""} — in dsh, type /baychat <name>`);
76
+ }
77
+ catch (err) {
78
+ lines.push(`Terminal use: FAILED — ${err instanceof Error ? err.message : String(err)}`);
79
+ }
80
+ return lines;
81
+ },
82
+ ask: askOnTerminal,
83
+ print: (line) => console.log(line),
84
+ platform: process.platform,
85
+ };
86
+ }
87
+ /**
88
+ * `baychat connect dsh` — a DeepSeek session the relay runs by itself.
89
+ *
90
+ * ORDER MATTERS: everything that can refuse (no login, no binary, a bad folder, a bad mode,
91
+ * a session name already taken by something the relay does not run) refuses BEFORE anything
92
+ * is written, so a failed connect leaves no half-made rule file.
93
+ */
94
+ async function cmdConnectDsh(opts = {}, deps = realDeps()) {
95
+ const row = (0, agents_1.acpAgent)("dsh");
96
+ const say = deps.print;
97
+ let device = deps.device();
98
+ if (!device) {
99
+ say("Connect DeepSeek — approve the QR in BayChat.");
100
+ if (!(await deps.login(opts.base)))
101
+ return 2;
102
+ device = deps.device();
103
+ }
104
+ if (!device) {
105
+ say("No device credential was saved. Retry `baychat connect dsh`.");
106
+ return 1;
107
+ }
108
+ const binary = deps.resolveBinary(row.bin);
109
+ if (!binary.ok) {
110
+ say(`${row.label} is not installed here. ${row.installHint}`);
111
+ return 1;
112
+ }
113
+ const folderInput = opts.folder ?? ((await deps.ask(`Folder ${row.label} may work in [${process.cwd()}]: `)) || process.cwd());
114
+ const folder = (0, policy_1.validateNewRoot)(folderInput);
115
+ if (!folder.ok) {
116
+ say(folder.reason);
117
+ return 1;
118
+ }
119
+ const offered = row.modesFor(deps.platform);
120
+ const maxInput = opts.maxMode ?? ((await deps.ask(`Highest mode the chat may ever switch it to (${offered.join(" / ")}) [${offered.includes("ask") ? "ask" : offered.at(-1)}]: `)) || (offered.includes("ask") ? "ask" : offered.at(-1)));
121
+ if (!(0, modes_1.isAcpMode)(maxInput) || !offered.includes(maxInput)) {
122
+ say(`"${maxInput}" is not a mode offered on this computer. Choose one of: ${offered.join(", ")}.`);
123
+ return 1;
124
+ }
125
+ const mode = opts.agentMode ?? "chat";
126
+ if (!(0, modes_1.isAcpMode)(mode) || (0, modes_1.modeRank)(mode) > (0, modes_1.modeRank)(maxInput)) {
127
+ say(`The starting mode must be one of ${offered.join(", ")} and no higher than ${maxInput}.`);
128
+ return 1;
129
+ }
130
+ const session = opts.session?.trim() || "DeepSeek";
131
+ const registry = new registry_1.SessionRegistry();
132
+ registry.load();
133
+ const existing = registry.get(session);
134
+ if (existing && !existing.acp) {
135
+ say(`A session named "${session}" already exists on this computer and is not run by the relay. Choose another name with --session <name>.`);
136
+ return 1;
137
+ }
138
+ // ── Nothing above this line wrote anything. ─────────────────────────────────────────────
139
+ (0, policy_1.savePolicy)((0, policy_1.upsertRoot)((0, policy_1.loadPolicy)(), row.id, folder.path, maxInput));
140
+ (0, agents_1.writeModePatches)(row, (0, agents_1.acpPatchDir)());
141
+ await deps.join(session);
142
+ const acp = { agent: row.id, cwd: folder.path, mode };
143
+ registry.upsert({ name: session, runtime: row.id, runtimeBin: binary.path, relayOwned: true, acp });
144
+ const live = await deps.registerWithRelay({ type: "acp-register", session, runtime: row.id, runtimeBin: binary.path, acp });
145
+ say("");
146
+ say(` ${row.label}: ${binary.path} (${binary.version})`);
147
+ if (!(0, agents_1.matchesTestedVersion)(binary.version, row.testedVersion))
148
+ say(` Note: BayChat was tested with ${row.testedVersion}. ${row.label} is a preview and changes often — if something misbehaves, run \`baychat doctor\`.`);
149
+ say(` Safety: ${row.label} is experimental and has not been security-audited. DeepSeek recommends running it in a disposable VM or container, not on your everyday computer.`);
150
+ say(` Folder: ${folder.path}`);
151
+ say(` Mode: ${(0, modes_1.describeMode)(mode)}`);
152
+ say(` Ceiling: ${maxInput} — the chat can never switch it higher. Change it by running this command again.`);
153
+ // Opt-in (Ruling R26): the terminal path writes the BayChat token into a file every dsh
154
+ // profile reads, and the relay-run path needs none of it.
155
+ if (opts.terminal)
156
+ for (const line of deps.installTerminalPath(device))
157
+ say(` ${line}`);
158
+ else
159
+ say(" Terminal use: not set up (add --terminal to also use BayChat from dsh by hand — it writes your BayChat token into ~/.dsh/cordis.patch.yml, which applies to every dsh profile).");
160
+ say(` ${await deps.ensureRelay()}${live ? "" : " (it will pick this session up when it starts)"}`);
161
+ say("");
162
+ say(` Message "${session}" in BayChat. It needs a DeepSeek API key on this computer: run \`dsh web\` and add it on the Models page.`);
163
+ say(" From the chat (you, or anyone you gave command access): /mode-chat /mode-read /mode-ask /mode-full · /stop /new /status.");
164
+ return 0;
165
+ }
@@ -35,11 +35,53 @@
35
35
  // carries them out - so the sequencing is testable without a network, a terminal,
36
36
  // or a phone.
37
37
  Object.defineProperty(exports, "__esModule", { value: true });
38
+ exports.parseConnectArgs = parseConnectArgs;
38
39
  exports.deviceStateFrom = deviceStateFrom;
39
40
  exports.planConnect = planConnect;
40
41
  exports.planNeedsPhoneApproval = planNeedsPhoneApproval;
41
42
  exports.expiryNudge = expiryNudge;
42
43
  exports.describeConnection = describeConnection;
44
+ /** Parse setup arguments without treating a flag value as a runtime name. */
45
+ function parseConnectArgs(args) {
46
+ const result = {};
47
+ for (let index = 0; index < args.length; index++) {
48
+ const arg = args[index];
49
+ if (arg === "--base") {
50
+ const value = args[++index];
51
+ if (result.base || !value?.trim() || value.startsWith("--"))
52
+ throw new Error("Supply --base once with a URL.");
53
+ result.base = value;
54
+ }
55
+ else if (arg === "--start" || arg === "--finish") {
56
+ if (result.mode)
57
+ throw new Error("Choose --start or --finish once.");
58
+ result.mode = arg === "--start" ? "start" : "finish";
59
+ }
60
+ else if (arg === "--folder" || arg === "--max-mode" || arg === "--mode" || arg === "--session") {
61
+ const value = args[++index];
62
+ if (!value?.trim() || value.startsWith("--"))
63
+ throw new Error(`Supply ${arg} with a value.`);
64
+ const key = arg === "--folder" ? "folder" : arg === "--max-mode" ? "maxMode" : arg === "--mode" ? "agentMode" : "session";
65
+ if (result[key])
66
+ throw new Error(`Supply ${arg} once.`);
67
+ result[key] = value;
68
+ }
69
+ else if (arg === "--terminal") {
70
+ if (result.terminal)
71
+ throw new Error("Supply --terminal once.");
72
+ result.terminal = true;
73
+ }
74
+ else if (arg.startsWith("-") || result.client || !arg.trim()) {
75
+ throw new Error("Usage: baychat connect <runtime> [--start | --finish] [--base <url>] " +
76
+ "[--folder <path>] [--max-mode <mode>] [--mode <mode>] [--session <name>] [--terminal]");
77
+ }
78
+ else
79
+ result.client = arg;
80
+ }
81
+ if (result.mode && !result.client)
82
+ throw new Error("Choose a runtime to configure.");
83
+ return result;
84
+ }
43
85
  const THREE_DAYS_MS = 3 * 86_400_000;
44
86
  /** Read the on-disk credential into a state, without judging what to do about it. */
45
87
  function deviceStateFrom(device, now = Date.now()) {
@@ -67,7 +109,11 @@ function planConnect(input) {
67
109
  steps.push({ kind: "device-login", reason: device.kind });
68
110
  }
69
111
  else {
70
- steps.push({ kind: "device-ok", userName: device.userName, msLeft: device.msLeft });
112
+ steps.push({
113
+ kind: "device-ok",
114
+ userName: device.userName,
115
+ msLeft: device.msLeft,
116
+ });
71
117
  }
72
118
  // No code, no room, no agent. The config carries the device credential, and the
73
119
  // session tools it unlocks let `/baychat <name>` do the rest, per session.
package/dist/connect.js CHANGED
@@ -52,6 +52,7 @@ const runtime_binary_1 = require("./runtime-binary");
52
52
  const connect_claude_1 = require("./connect-claude");
53
53
  var connect_claude_2 = require("./connect-claude");
54
54
  Object.defineProperty(exports, "cmdConnectClaude", { enumerable: true, get: function () { return connect_claude_2.cmdConnectClaude; } });
55
+ const connect_dsh_1 = require("./connect-dsh");
55
56
  const connect_hermes_1 = require("./connect-hermes");
56
57
  /** Clients `connect` can configure. `claude` is an alias users reach for. */
57
58
  const CLIENT_ALIASES = {
@@ -69,6 +70,7 @@ function renderConnectMenu() {
69
70
  ...rows,
70
71
  " npx baychat connect claude → Claude Code (login, MCP and skill)",
71
72
  " npx baychat connect hermes → Hermes (pair and enable its persistent agent)",
73
+ " npx baychat connect dsh → DeepSeek Harness (the relay runs it; talk to it from the app)",
72
74
  "",
73
75
  "You approve a QR on your phone. After that, in any coding session:",
74
76
  "",
@@ -193,17 +195,40 @@ async function cmdConnect(clientArg, opts = {}) {
193
195
  // `parseConnectClient`, which would reject it.
194
196
  if (isClaudeConnectTarget(clientArg))
195
197
  return (0, connect_claude_1.cmdConnectClaude)(opts);
196
- if (clientArg.trim().toLowerCase() === "hermes")
198
+ if (clientArg.trim().toLowerCase() === "hermes") {
199
+ if (opts.mode)
200
+ throw new Error("Hermes pairing does not support --start/--finish.");
197
201
  return (0, connect_hermes_1.cmdConnectHermes)(opts);
202
+ }
203
+ if (clientArg.trim().toLowerCase() === "dsh" || clientArg.trim().toLowerCase() === "deepseek") {
204
+ if (opts.mode)
205
+ throw new Error("DeepSeek setup does not support --start/--finish.");
206
+ return (0, connect_dsh_1.cmdConnectDsh)(opts);
207
+ }
198
208
  const client = parseConnectClient(clientArg);
199
- const base = (opts.base || process.env.BAYCHAT_API_URL || config_1.DEFAULT_API_URL).replace(/\/$/, "");
209
+ if (opts.mode === "finish") {
210
+ // Let the pending approval select its server unless one was explicitly supplied.
211
+ if (!(await (0, commands_1.cmdLogin)({
212
+ base: opts.base,
213
+ mode: "finish",
214
+ registerClaude: client === "desktop",
215
+ hint: false,
216
+ })))
217
+ return 2;
218
+ }
219
+ const device = (0, config_1.loadDeviceCredentials)();
220
+ const base = (opts.base ||
221
+ process.env.BAYCHAT_API_URL ||
222
+ device?.baseUrl ||
223
+ config_1.DEFAULT_API_URL).replace(/\/$/, "");
200
224
  const steps = (0, connect_plan_1.planConnect)({
201
225
  client,
202
- device: (0, connect_plan_1.deviceStateFrom)((0, config_1.loadDeviceCredentials)()),
226
+ // Credentials belong to their server; a different endpoint needs its own login.
227
+ device: (0, connect_plan_1.deviceStateFrom)(device?.baseUrl.replace(/\/$/, "") === base ? device : null),
203
228
  needsRestart: (0, client_paths_1.needsRestart)(client),
204
229
  });
205
230
  for (const step of steps) {
206
- const outcome = await runStep(step, { base, client });
231
+ const outcome = await runStep(step, { base, client, mode: opts.mode });
207
232
  if (outcome.kind === "abort")
208
233
  return outcome.code;
209
234
  }
@@ -233,24 +258,30 @@ async function cmdConnect(clientArg, opts = {}) {
233
258
  // The relay is what makes a joined session hear messages without being
234
259
  // prompted. Best-effort by design: it reports what it did (or didn't) and
235
260
  // never fails the connect it is tacked onto.
236
- console.log(` ${await (0, commands_2.ensureRelayInstalled)()}`);
261
+ if (!opts.mode)
262
+ console.log(` ${await (0, commands_2.ensureRelayInstalled)()}`);
237
263
  return 0;
238
264
  }
239
265
  async function runStep(step, ctx) {
240
266
  switch (step.kind) {
241
267
  case "device-login": {
242
- console.log(step.reason === "expired"
243
- ? "Your laptop login has expired. Reconnecting this laptop…\n"
244
- : "Connecting this laptop approve the QR on your phone.\n");
268
+ if (!ctx.mode)
269
+ console.log(step.reason === "expired"
270
+ ? "Your laptop login has expired. Reconnecting this laptop…\n"
271
+ : "Connecting this laptop — approve the QR on your phone.\n");
245
272
  // registerClaude is left ON only for the desktop/claude clients; for any
246
273
  // other client, editing Claude's config unasked is a surprise.
247
274
  const ok = await (0, commands_1.cmdLogin)({
248
275
  base: ctx.base,
249
276
  registerClaude: ctx.client === "desktop",
250
277
  hint: false,
278
+ ...(ctx.mode ? { mode: ctx.mode } : {}),
251
279
  });
280
+ if (ctx.mode === "start")
281
+ return { kind: "abort", code: 2 };
252
282
  if (!ok) {
253
- console.log("\nLaptop not connected — run `npx baychat connect` again when ready.");
283
+ if (!ctx.mode)
284
+ console.log("\nLaptop not connected — run `npx baychat connect` again when ready.");
254
285
  return { kind: "abort", code: 2 };
255
286
  }
256
287
  console.log("✓ Laptop connected.\n");
@@ -48,6 +48,7 @@ const os = __importStar(require("os"));
48
48
  const config_1 = require("./config");
49
49
  const doctor_1 = require("./doctor");
50
50
  const commands_1 = require("./relay/commands");
51
+ const registry_1 = require("./relay/registry");
51
52
  const credential_refresh_1 = require("./credential-refresh");
52
53
  const client_paths_1 = require("./client-paths");
53
54
  const runtime_binary_1 = require("./runtime-binary");
@@ -69,12 +70,18 @@ function codexPath() {
69
70
  return resolved.ok ? resolved.path : undefined;
70
71
  }
71
72
  async function currentDoctorEnv() {
73
+ // Read directly, not through `relay.sessions`: a relay-owned dsh session is worth
74
+ // diagnosing precisely when the relay is NOT running, which is often why `doctor` was
75
+ // reached for in the first place.
76
+ const registry = new registry_1.SessionRegistry();
77
+ registry.load();
72
78
  return {
73
79
  home: os.homedir(),
74
80
  platform: process.platform,
75
81
  now: Date.now(),
76
82
  device: (0, config_1.loadDeviceCredentials)(),
77
83
  relay: await (0, commands_1.tryRelayStatus)(),
84
+ targets: registry.all(),
78
85
  readText(filePath) {
79
86
  try {
80
87
  return fs.readFileSync(filePath, "utf8");