baychat 0.22.0 → 0.23.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/README.md CHANGED
@@ -187,6 +187,7 @@ baychat connect # list the apps it can configure
187
187
  baychat connect claude # refresh Claude Code's skill (no QR, no re-pairing)
188
188
  baychat connect codex # configure Codex — restart Codex afterwards
189
189
  baychat connect dsh # a DeepSeek Harness session the relay runs for you — see below
190
+ baychat connect cursor-cli # a Cursor CLI session the relay runs for you — see below
190
191
  baychat relay start # the process that wakes your sessions
191
192
  baychat relay status # transport, sessions, anything pending
192
193
  baychat doctor # checks every link and prints what to type
@@ -487,6 +488,27 @@ It also needs its own API key on that computer, separate from BayChat: run `dsh
487
488
  it on the Models page. `baychat doctor` reports whether the key exists (never its contents) and
488
489
  whether the installed `dsh` matches the version BayChat was tested against.
489
490
 
491
+ ## `baychat connect cursor-cli` — Cursor's CLI, run by the relay
492
+
493
+ ```
494
+ baychat connect cursor-cli [--folder <path>] [--max-mode <mode>] [--mode <mode>] [--session <name>]
495
+ ```
496
+
497
+ The same relay path as `connect dsh`, for Cursor's CLI (`agent acp`). Log it in first with
498
+ `agent login`; `connect cursor-cli` refuses until `agent status` says you are. Flags are as for
499
+ `dsh` (default session name `Cursor`; no `--terminal`). This is **not** `baychat connect cursor`,
500
+ which still configures the Cursor editor's MCP file.
501
+
502
+ Modes: `chat` (default — BayChat tools only), `read` (+ reads; no shell, no writes), and on Linux
503
+ only `ask` (edits files inside its folder without asking, edits outside it are refused, every
504
+ command asks on your phone first). There is no `full` for Cursor. Every mode above `chat` can read
505
+ anything your account can. BayChat keeps each session's Cursor config in its own folder under
506
+ `~/.baychat/acp/`, never in your project. A `.cursor/cli.json` or `.cursor/hooks.json` in the
507
+ session's folder or any folder above it stops the session (Cursor would let it override BayChat's
508
+ limits): remove or rename it. Worked on Cursor's free Hobby plan when checked on
509
+ 19 Sep 2026; Pro is recommended for regular use. Detail: the Cursor section of
510
+ [`docs/features/ACP_RELAY.md`](../../docs/features/ACP_RELAY.md).
511
+
490
512
  ## Group instructions
491
513
 
492
514
  Group conversations carry a short, server-authored **primer** — who's in the
@@ -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.realConnectAcpDeps = realConnectAcpDeps;
37
+ exports.connectAcpAgent = connectAcpAgent;
38
+ const readline = __importStar(require("readline"));
39
+ const commands_1 = require("./commands");
40
+ const config_1 = require("./config");
41
+ const agents_1 = require("./relay/acp/agents");
42
+ const modes_1 = require("./relay/acp/modes");
43
+ const policy_1 = require("./relay/acp/policy");
44
+ const presence_1 = require("./relay/acp/presence");
45
+ const commands_2 = require("./relay/commands");
46
+ const registry_1 = require("./relay/registry");
47
+ const runtime_binary_1 = require("./runtime-binary");
48
+ function askOnTerminal(question) {
49
+ const rl = readline.createInterface({ input: process.stdin, output: process.stdout });
50
+ return new Promise((resolve) => rl.question(question, (answer) => (rl.close(), resolve(answer.trim()))));
51
+ }
52
+ /** The real world, for every ACP agent's `connect`. */
53
+ function realConnectAcpDeps() {
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
+ ask: askOnTerminal,
67
+ print: (line) => console.log(line),
68
+ platform: process.platform,
69
+ };
70
+ }
71
+ /**
72
+ * `baychat connect <agent>` for a relay-run (ACP) agent.
73
+ *
74
+ * ORDER MATTERS: everything that can refuse (no login, no binary, the agent's own preflight, a
75
+ * bad folder, a bad mode, a session name already taken by something the relay does not run)
76
+ * refuses BEFORE anything is written, so a failed connect leaves no half-made rule file.
77
+ */
78
+ async function connectAcpAgent(row, spec, opts, deps) {
79
+ const say = deps.print;
80
+ let device = deps.device();
81
+ if (!device) {
82
+ say(spec.greeting);
83
+ if (!(await deps.login(opts.base)))
84
+ return 2;
85
+ device = deps.device();
86
+ }
87
+ if (!device) {
88
+ say(`No device credential was saved. Retry \`${row.connectCommand}\`.`);
89
+ return 1;
90
+ }
91
+ const binary = deps.resolveBinary(row.bin);
92
+ if (!binary.ok) {
93
+ say(`${row.label} is not installed here. ${row.installHint}`);
94
+ return 1;
95
+ }
96
+ if (!(0, agents_1.binaryIdentified)(row, binary.version)) {
97
+ say((0, agents_1.unidentifiedBinary)(row, binary.path, binary.version));
98
+ return 1;
99
+ }
100
+ const refusal = await spec.preflight?.(binary.path);
101
+ if (refusal) {
102
+ say(refusal);
103
+ return 1;
104
+ }
105
+ const folderInput = opts.folder ?? ((await deps.ask(`Folder ${row.label} may work in [${process.cwd()}]: `)) || process.cwd());
106
+ const folder = (0, policy_1.validateNewRoot)(folderInput);
107
+ if (!folder.ok) {
108
+ say(folder.reason);
109
+ return 1;
110
+ }
111
+ const offered = row.modesFor(deps.platform);
112
+ 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)));
113
+ if (!(0, modes_1.isAcpMode)(maxInput) || !offered.includes(maxInput)) {
114
+ say(`"${maxInput}" is not a mode offered on this computer. Choose one of: ${offered.join(", ")}.`);
115
+ return 1;
116
+ }
117
+ const mode = opts.agentMode ?? "chat";
118
+ if (!(0, modes_1.isAcpMode)(mode) || (0, modes_1.modeRank)(mode) > (0, modes_1.modeRank)(maxInput)) {
119
+ say(`The starting mode must be one of ${offered.join(", ")} and no higher than ${maxInput}.`);
120
+ return 1;
121
+ }
122
+ const session = opts.session?.trim() || spec.defaultSession;
123
+ const registry = new registry_1.SessionRegistry();
124
+ registry.load();
125
+ const existing = registry.get(session);
126
+ if (existing && !existing.acp) {
127
+ say(`A session named "${session}" already exists on this computer and is not run by the relay. Choose another name with --session <name>.`);
128
+ return 1;
129
+ }
130
+ const otherAgent = (0, agents_1.otherAgentRefusal)(row, session, existing?.acp?.agent);
131
+ if (otherAgent) {
132
+ say(otherAgent);
133
+ return 1;
134
+ }
135
+ try {
136
+ row.launch(mode, (0, agents_1.acpPatchDir)(), session); // a session name the agent's config cannot hold throws here
137
+ }
138
+ catch (err) {
139
+ say(err instanceof Error ? err.message : String(err));
140
+ return 1;
141
+ }
142
+ // ── Nothing above this line wrote anything. ─────────────────────────────────────────────
143
+ (0, policy_1.savePolicy)((0, policy_1.upsertRoot)((0, policy_1.loadPolicy)(), row.id, folder.path, maxInput));
144
+ (0, agents_1.writeModePatch)(row, mode, session, (0, agents_1.acpPatchDir)());
145
+ await deps.join(session);
146
+ const acp = { agent: row.id, cwd: folder.path, mode };
147
+ registry.upsert({ name: session, runtime: row.id, runtimeBin: binary.path, relayOwned: true, acp });
148
+ const live = await deps.registerWithRelay({ type: "acp-register", session, runtime: row.id, runtimeBin: binary.path, acp });
149
+ say("");
150
+ say(` ${row.label}: ${binary.path} (${binary.version})`);
151
+ if (!(0, agents_1.matchesTestedVersion)(binary.version, row.testedVersion))
152
+ say(` Note: BayChat was tested with ${row.testedVersion}. ${row.label} is a preview and changes often — if something misbehaves, run \`baychat doctor\`.`);
153
+ for (const line of spec.notes(maxInput))
154
+ say(` ${line}`);
155
+ say(` Folder: ${folder.path}`);
156
+ say(` Mode: ${(0, agents_1.describeAgentMode)(row, mode)}`);
157
+ say(` Ceiling: ${maxInput} — the chat can never switch it higher. Change it by running this command again.`);
158
+ for (const line of spec.extras(device))
159
+ say(` ${line}`);
160
+ say(` ${await deps.ensureRelay()}${live ? "" : " (it will pick this session up when it starts)"}`);
161
+ say("");
162
+ say(` ${spec.firstMessage(session)}`);
163
+ say(` From the chat (you, or anyone you gave command access): ${offered.map((m) => `/mode-${m}`).join(" ")} · /stop /new /status.`);
164
+ return 0;
165
+ }
@@ -0,0 +1,65 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.cursorLoginState = cursorLoginState;
4
+ exports.cmdConnectCursor = cmdConnectCursor;
5
+ const child_process_1 = require("child_process");
6
+ const connect_acp_1 = require("./connect-acp");
7
+ const agents_1 = require("./relay/acp/agents");
8
+ const modes_1 = require("./relay/acp/modes");
9
+ const runtime_binary_1 = require("./runtime-binary");
10
+ const STATUS_TIMEOUT_MS = 15_000;
11
+ /** Colour codes, which a terminal-aware CLI may print even into a pipe. */
12
+ const ANSI = /\u001b\[[0-9;]*[A-Za-z]/g;
13
+ /**
14
+ * What `agent status` says about the Cursor login [run, CLI 2026.09.18-9a7762b: "Logged in" /
15
+ * "Not logged in"]. Anything else is `unknown` — Cursor changed its wording, or the `agent`
16
+ * found on PATH is some other program — and `connect` refuses rather than guess.
17
+ */
18
+ function cursorLoginState(statusOutput) {
19
+ const text = statusOutput.replace(ANSI, "");
20
+ if (/not logged in/i.test(text))
21
+ return "logged-out"; // first: it contains "logged in"
22
+ if (/logged in/i.test(text))
23
+ return "logged-in";
24
+ return "unknown";
25
+ }
26
+ function runAgentStatus(binaryPath) {
27
+ // No shell: the path came from PATH resolution. A Windows `.cmd` shim needs its interpreter named.
28
+ const plan = (0, runtime_binary_1.spawnPlanFor)(binaryPath, process.platform);
29
+ return new Promise((resolve) => {
30
+ (0, child_process_1.execFile)(plan.file, [...plan.prefixArgs, "status"], { timeout: STATUS_TIMEOUT_MS, windowsHide: true, shell: false }, (err, stdout, stderr) => {
31
+ const said = `${stdout ?? ""}${stderr ?? ""}`;
32
+ // A failed run is still an answer: "Not logged in" may come with a non-zero exit.
33
+ resolve(said || (err ? err.message : ""));
34
+ });
35
+ });
36
+ }
37
+ function realDeps() {
38
+ return { ...(0, connect_acp_1.realConnectAcpDeps)(), loginStatus: runAgentStatus };
39
+ }
40
+ /** `baychat connect cursor-cli` — a Cursor CLI session the relay runs by itself. */
41
+ async function cmdConnectCursor(opts = {}, deps = realDeps()) {
42
+ const row = (0, agents_1.acpAgent)("cursor");
43
+ return (0, connect_acp_1.connectAcpAgent)(row, {
44
+ greeting: "Connect Cursor — approve the QR in BayChat.",
45
+ defaultSession: "Cursor",
46
+ async preflight(binaryPath) {
47
+ const state = cursorLoginState(await deps.loginStatus(binaryPath));
48
+ if (state === "logged-in")
49
+ return undefined;
50
+ if (state === "logged-out")
51
+ return `Cursor is not logged in on this computer. Run \`agent login\`, then \`${row.connectCommand}\` again.`;
52
+ return `Could not tell whether Cursor is logged in: \`${binaryPath} status\` said neither "Logged in" nor "Not logged in". If that is not Cursor's CLI, install it with \`curl https://cursor.com/install -fsS | bash\`; if it is, run \`agent login\`, then \`${row.connectCommand}\` again.`;
53
+ },
54
+ notes: (ceiling) => [
55
+ "Plan: Works on Cursor's free Hobby plan as of 19 Sep 2026; Pro recommended for regular use.",
56
+ "Modes the chat can switch it to:",
57
+ ...row
58
+ .modesFor(deps.platform)
59
+ .filter((m) => (0, modes_1.modeRank)(m) <= (0, modes_1.modeRank)(ceiling))
60
+ .map((m) => ` ${(0, agents_1.describeAgentMode)(row, m)}`),
61
+ ],
62
+ extras: () => [],
63
+ firstMessage: (session) => `Message "${session}" in BayChat. It uses this computer's Cursor login (\`agent login\`).`,
64
+ }, opts, deps);
65
+ }
@@ -1,68 +1,13 @@
1
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
2
  Object.defineProperty(exports, "__esModule", { value: true });
36
3
  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");
4
+ const connect_acp_1 = require("./connect-acp");
46
5
  const dsh_config_1 = require("./dsh-config");
6
+ const agents_1 = require("./relay/acp/agents");
47
7
  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
8
  function realDeps() {
54
9
  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,
10
+ ...(0, connect_acp_1.realConnectAcpDeps)(),
66
11
  installTerminalPath: (device) => {
67
12
  const lines = (0, runtime_install_1.describeInstall)((0, runtime_install_1.installRuntimeCommand)("dsh"));
68
13
  // writeDshMcpEntry can throw (RULING R22: a damaged ~/.dsh/cordis.patch.yml block). By this
@@ -79,87 +24,22 @@ function realDeps() {
79
24
  }
80
25
  return lines;
81
26
  },
82
- ask: askOnTerminal,
83
- print: (line) => console.log(line),
84
- platform: process.platform,
85
27
  };
86
28
  }
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
- */
29
+ /** `baychat connect dsh` — a DeepSeek session the relay runs by itself. */
94
30
  async function cmdConnectDsh(opts = {}, deps = realDeps()) {
95
31
  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;
32
+ return (0, connect_acp_1.connectAcpAgent)(row, {
33
+ greeting: "Connect DeepSeek — approve the QR in BayChat.",
34
+ defaultSession: "DeepSeek",
35
+ notes: () => [
36
+ `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.`,
37
+ ],
38
+ // Opt-in (Ruling R26): the terminal path writes the BayChat token into a file every dsh
39
+ // profile reads, and the relay-run path needs none of it.
40
+ extras: (device) => opts.terminal
41
+ ? deps.installTerminalPath(device)
42
+ : ["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)."],
43
+ firstMessage: (session) => `Message "${session}" in BayChat. It needs a DeepSeek API key on this computer: run \`dsh web\` and add it on the Models page.`,
44
+ }, opts, deps);
165
45
  }
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_cursor_1 = require("./connect-cursor");
55
56
  const connect_dsh_1 = require("./connect-dsh");
56
57
  const connect_hermes_1 = require("./connect-hermes");
57
58
  /** Clients `connect` can configure. `claude` is an alias users reach for. */
@@ -71,6 +72,7 @@ function renderConnectMenu() {
71
72
  " npx baychat connect claude → Claude Code (login, MCP and skill)",
72
73
  " npx baychat connect hermes → Hermes (pair and enable its persistent agent)",
73
74
  " npx baychat connect dsh → DeepSeek Harness (the relay runs it; talk to it from the app)",
75
+ " npx baychat connect cursor-cli → Cursor's CLI (the relay runs it; talk to it from the app)",
74
76
  "",
75
77
  "You approve a QR on your phone. After that, in any coding session:",
76
78
  "",
@@ -81,6 +83,8 @@ function renderConnectMenu() {
81
83
  "on its own: no QR, no re-pairing. Run it after every CLI upgrade.",
82
84
  ].join("\n");
83
85
  }
86
+ /** Spellings of the relay-run Cursor CLI (`agent acp`) accepted by `connect`. */
87
+ const CURSOR_CLI_ALIASES = new Set(["cursor-cli", "cursor-agent"]);
84
88
  /** Spellings of Claude Code accepted by `connect`. */
85
89
  const CLAUDE_CONNECT_ALIASES = new Set(["claude", "claude-code", "claudecode"]);
86
90
  /**
@@ -205,6 +209,12 @@ async function cmdConnect(clientArg, opts = {}) {
205
209
  throw new Error("DeepSeek setup does not support --start/--finish.");
206
210
  return (0, connect_dsh_1.cmdConnectDsh)(opts);
207
211
  }
212
+ // Before `parseConnectClient`: plain `cursor` is the Cursor EDITOR's MCP file and stays that.
213
+ if (CURSOR_CLI_ALIASES.has(clientArg.trim().toLowerCase())) {
214
+ if (opts.mode)
215
+ throw new Error("Cursor CLI setup does not support --start/--finish.");
216
+ return (0, connect_cursor_1.cmdConnectCursor)(opts);
217
+ }
208
218
  const client = parseConnectClient(clientArg);
209
219
  if (opts.mode === "finish") {
210
220
  // Let the pending approval select its server unless one was explicitly supplied.
package/dist/doctor.js CHANGED
@@ -184,14 +184,15 @@ function mailboxCheck() {
184
184
  * running two dsh sessions in two folders can have one healthy and one not, and collapsing them
185
185
  * would hide whichever came second.
186
186
  *
187
- * DeepSeek Harness is experimental and has not been security-audited (R11/R17): every line that
188
- * gets far enough to say a version repeats DeepSeek's own recommendation to run it disposably —
189
- * one short clause, not a paragraph, because the reader has already seen it once from `connect`.
187
+ * An agent whose maker advises a precaution (dsh: experimental, not security-audited R11/R17)
188
+ * has it repeated on every line that gets far enough to say a version one short clause, not a
189
+ * paragraph, because the reader has already seen it once from `connect`.
190
190
  */
191
191
  function acpChecks(env) {
192
192
  const owned = env.targets.filter((t) => t.relayOwned && t.acp);
193
193
  if (owned.length === 0) {
194
- return [{ name: "relay-run agents", status: "skip", detail: "none — `baychat connect dsh` sets one up" }];
194
+ const commands = agents_1.ACP_AGENTS.map((row) => `\`${row.connectCommand}\``).join(" or ");
195
+ return [{ name: "relay-run agents", status: "skip", detail: `none — ${commands} sets one up` }];
195
196
  }
196
197
  const policy = (0, policy_1.loadPolicy)();
197
198
  return owned.map((t) => {
@@ -202,7 +203,7 @@ function acpChecks(env) {
202
203
  name,
203
204
  status: "fail",
204
205
  detail: `unknown agent "${t.acp.agent}"`,
205
- remedy: "Update baychat, then run `baychat connect dsh` again.",
206
+ remedy: `Update baychat, then run \`${(0, agents_1.connectCommandFor)(t.acp.agent)}\` again.`,
206
207
  };
207
208
  }
208
209
  const bin = env.resolveBinary(row.bin);
@@ -220,22 +221,20 @@ function acpChecks(env) {
220
221
  name,
221
222
  status: "fail",
222
223
  detail: folder.reason,
223
- remedy: "Run `baychat connect dsh` in the folder it should work in.",
224
+ remedy: `Run \`${row.connectCommand}\` in the folder it should work in.`,
224
225
  };
225
226
  }
226
- const home = env.homedir ?? os.homedir();
227
- // EXISTENCE only. The key is dsh's; BayChat never opens the file that holds it, and never
228
- // logs it the check is "is there a file there", not "what does it say".
229
- const keySeen = Boolean(process.env.DEEPSEEK_API_KEY) ||
230
- fs.existsSync(path.join(process.env.DSH_HOME ?? path.join(home, ".dsh"), ".credentials.yaml"));
227
+ // EXISTENCE only, and only for an agent whose credential BayChat can see at all (the row
228
+ // says): the key is the agent's; BayChat never opens the file that holds it, and never logs it.
229
+ const keySeen = row.credentialSeen?.(env.homedir ?? os.homedir()) ?? true;
231
230
  const line = `${row.label} ${bin.version} · ${t.acp.cwd} · mode ${t.acp.mode} (ceiling ${folder.maxMode})` +
232
- " DeepSeek recommends a disposable VM or container";
231
+ (row.safetyNote ? ` ${row.safetyNote}` : "");
233
232
  if (!keySeen) {
234
233
  return {
235
234
  name,
236
235
  status: "warn",
237
- detail: `${line} — no DeepSeek key found`,
238
- remedy: "Run `dsh web` and add the key on the Models page.",
236
+ detail: `${line} — no ${row.label} key found`,
237
+ remedy: row.keyHint,
239
238
  };
240
239
  }
241
240
  // R20: a substring check lets a later release ("0.1.5-rc.20") pass silently as the tested
package/dist/index.js CHANGED
@@ -59,6 +59,11 @@ Usage:
59
59
  phone, and your client's MCP config is written for
60
60
  you. Then run /baychat <name> in any session to
61
61
  join as that session. No config blocks, no editing
62
+ baychat connect dsh|cursor-cli [--folder <path>] [--max-mode <mode>] [--mode <mode>] [--session <name>]
63
+ An agent the relay runs by itself (DeepSeek Harness,
64
+ Cursor's CLI): you talk to it from the app. The
65
+ folder and the highest mode are set here, never
66
+ from a chat
62
67
  baychat login [--start | --finish | --token <PAT>] [--base <url>]
63
68
  Log this laptop in to BayChat (QR) and add the
64
69
  BayChat MCP server to Claude Code. Creates a