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.
- package/README.md +74 -13
- package/dist/approve-hook.js +5 -2
- package/dist/claude-onboarding.js +6 -12
- package/dist/commands.js +11 -6
- package/dist/connect-claude.js +19 -4
- package/dist/connect-dsh.js +165 -0
- package/dist/connect-plan.js +47 -1
- package/dist/connect.js +40 -9
- package/dist/doctor-command.js +7 -0
- package/dist/doctor.js +94 -1
- package/dist/dsh-config.js +147 -0
- package/dist/index.js +37 -2
- package/dist/relay/acp/agents.js +186 -0
- package/dist/relay/acp/approval.js +67 -0
- package/dist/relay/acp/client.js +253 -0
- package/dist/relay/acp/commands.js +69 -0
- package/dist/relay/acp/daemon-glue.js +201 -0
- package/dist/relay/acp/dump-config.js +31 -0
- package/dist/relay/acp/modes.js +36 -0
- package/dist/relay/acp/permissions.js +42 -0
- package/dist/relay/acp/policy.js +137 -0
- package/dist/relay/acp/presence.js +87 -0
- package/dist/relay/acp/prompt.js +69 -0
- package/dist/relay/acp/runner.js +311 -0
- package/dist/relay/acp/sdk.js +19 -0
- package/dist/relay/acp/turn-queue.js +163 -0
- package/dist/relay/acp/types.js +2 -0
- package/dist/relay/commands.js +28 -0
- package/dist/relay/daemon.js +195 -0
- package/dist/relay/mailbox.js +1 -0
- package/dist/relay/profiles.js +22 -0
- package/dist/relay/registry.js +46 -1
- package/dist/relay/session-commands.js +89 -0
- package/dist/relay/session-reply.js +25 -0
- package/dist/relay/terminal-pane.js +197 -0
- package/dist/relay/types.js +0 -9
- package/dist/runtime-install.js +9 -2
- package/dist/runtimes.js +16 -0
- package/dist/session-command.js +16 -3
- package/dist/session-setup.js +38 -0
- package/dist/skill-bootstrap.js +47 -0
- package/dist/start-command.js +235 -0
- package/dist/update-command.js +201 -0
- package/package.json +3 -1
package/dist/doctor.js
CHANGED
|
@@ -57,13 +57,17 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
57
57
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
58
58
|
exports.buildReport = buildReport;
|
|
59
59
|
exports.mailboxCheck = mailboxCheck;
|
|
60
|
+
exports.acpChecks = acpChecks;
|
|
60
61
|
exports.exitCodeFor = exitCodeFor;
|
|
61
62
|
exports.renderReport = renderReport;
|
|
62
63
|
const fs = __importStar(require("fs"));
|
|
64
|
+
const os = __importStar(require("os"));
|
|
63
65
|
const path = __importStar(require("path"));
|
|
64
66
|
const mailbox_1 = require("./relay/mailbox");
|
|
65
67
|
const parent_watch_1 = require("./relay/parent-watch");
|
|
66
68
|
const client_paths_1 = require("./client-paths");
|
|
69
|
+
const agents_1 = require("./relay/acp/agents");
|
|
70
|
+
const policy_1 = require("./relay/acp/policy");
|
|
67
71
|
const runtime_binary_1 = require("./runtime-binary");
|
|
68
72
|
const runtimes_1 = require("./runtimes");
|
|
69
73
|
/**
|
|
@@ -106,6 +110,7 @@ function buildReport(env) {
|
|
|
106
110
|
mailbox: mailboxCheck(),
|
|
107
111
|
staleCredentials: staleCredentialCheck(env),
|
|
108
112
|
runtimes: REPORTED.map((runtime) => runtimeReport(runtime, env)),
|
|
113
|
+
agents: acpChecks({ resolveBinary: env.resolveBinary, targets: env.targets, homedir: env.home }),
|
|
109
114
|
pending: pendingCheck(env),
|
|
110
115
|
};
|
|
111
116
|
}
|
|
@@ -168,6 +173,84 @@ function mailboxCheck() {
|
|
|
168
173
|
// where the truth is established, honestly, as pending-or-delivered.
|
|
169
174
|
return { name: "mailbox", status: "pass", detail: `registered a mailbox FIFO: ${live.join(", ")}` };
|
|
170
175
|
}
|
|
176
|
+
/**
|
|
177
|
+
* Relay-run (ACP) agents: is each one startable, inside its rules, and able to reach a model?
|
|
178
|
+
*
|
|
179
|
+
* Takes `targets` rather than the full `DoctorEnv` for the same testability reason every other
|
|
180
|
+
* check here does — see `DoctorEnv.targets` for why these come from the persisted registry and
|
|
181
|
+
* not from `relay.sessions`.
|
|
182
|
+
*
|
|
183
|
+
* ONE finding per relay-owned session (not one report for the feature), because a machine
|
|
184
|
+
* running two dsh sessions in two folders can have one healthy and one not, and collapsing them
|
|
185
|
+
* would hide whichever came second.
|
|
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`.
|
|
190
|
+
*/
|
|
191
|
+
function acpChecks(env) {
|
|
192
|
+
const owned = env.targets.filter((t) => t.relayOwned && t.acp);
|
|
193
|
+
if (owned.length === 0) {
|
|
194
|
+
return [{ name: "relay-run agents", status: "skip", detail: "none — `baychat connect dsh` sets one up" }];
|
|
195
|
+
}
|
|
196
|
+
const policy = (0, policy_1.loadPolicy)();
|
|
197
|
+
return owned.map((t) => {
|
|
198
|
+
const name = `agent ${t.name}`;
|
|
199
|
+
const row = (0, agents_1.acpAgent)(t.acp.agent);
|
|
200
|
+
if (!row) {
|
|
201
|
+
return {
|
|
202
|
+
name,
|
|
203
|
+
status: "fail",
|
|
204
|
+
detail: `unknown agent "${t.acp.agent}"`,
|
|
205
|
+
remedy: "Update baychat, then run `baychat connect dsh` again.",
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
const bin = env.resolveBinary(row.bin);
|
|
209
|
+
if (!bin.ok) {
|
|
210
|
+
return {
|
|
211
|
+
name,
|
|
212
|
+
status: "fail",
|
|
213
|
+
detail: `${row.label} is not installed where the relay can find it`,
|
|
214
|
+
remedy: row.installHint,
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
const folder = (0, policy_1.checkFolder)(policy, row.id, t.acp.cwd);
|
|
218
|
+
if (!folder.ok) {
|
|
219
|
+
return {
|
|
220
|
+
name,
|
|
221
|
+
status: "fail",
|
|
222
|
+
detail: folder.reason,
|
|
223
|
+
remedy: "Run `baychat connect dsh` in the folder it should work in.",
|
|
224
|
+
};
|
|
225
|
+
}
|
|
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"));
|
|
231
|
+
const line = `${row.label} ${bin.version} · ${t.acp.cwd} · mode ${t.acp.mode} (ceiling ${folder.maxMode})` +
|
|
232
|
+
" — DeepSeek recommends a disposable VM or container";
|
|
233
|
+
if (!keySeen) {
|
|
234
|
+
return {
|
|
235
|
+
name,
|
|
236
|
+
status: "warn",
|
|
237
|
+
detail: `${line} — no DeepSeek key found`,
|
|
238
|
+
remedy: "Run `dsh web` and add the key on the Models page.",
|
|
239
|
+
};
|
|
240
|
+
}
|
|
241
|
+
// R20: a substring check lets a later release ("0.1.5-rc.20") pass silently as the tested
|
|
242
|
+
// one ("0.1.5-rc.2"). `matchesTestedVersion` bounds the match to a whole version token.
|
|
243
|
+
if (!(0, agents_1.matchesTestedVersion)(bin.version, row.testedVersion)) {
|
|
244
|
+
return {
|
|
245
|
+
name,
|
|
246
|
+
status: "warn",
|
|
247
|
+
detail: `${line} — BayChat was tested with ${row.testedVersion}`,
|
|
248
|
+
remedy: "If it misbehaves, install the tested version or update baychat.",
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
return { name, status: "pass", detail: line };
|
|
252
|
+
});
|
|
253
|
+
}
|
|
171
254
|
/**
|
|
172
255
|
* The process exit code for a report.
|
|
173
256
|
*
|
|
@@ -198,6 +281,8 @@ function renderReport(report) {
|
|
|
198
281
|
for (const check of rest)
|
|
199
282
|
lines.push(...renderGroupCheck("", check));
|
|
200
283
|
}
|
|
284
|
+
for (const check of report.agents)
|
|
285
|
+
lines.push(...renderSummaryCheck(check));
|
|
201
286
|
lines.push(...renderSummaryCheck(report.pending));
|
|
202
287
|
return lines;
|
|
203
288
|
}
|
|
@@ -513,7 +598,15 @@ function withRemedy(line, check) {
|
|
|
513
598
|
return [line, `${" ".repeat(LABEL_WIDTH + 2 + CHECK_WIDTH)}→ ${check.remedy}`];
|
|
514
599
|
}
|
|
515
600
|
function allChecks(report) {
|
|
516
|
-
return [
|
|
601
|
+
return [
|
|
602
|
+
report.credentials,
|
|
603
|
+
report.staleCredentials,
|
|
604
|
+
report.relay,
|
|
605
|
+
report.mailbox,
|
|
606
|
+
...report.runtimes.flatMap((r) => r.checks),
|
|
607
|
+
...report.agents,
|
|
608
|
+
report.pending,
|
|
609
|
+
];
|
|
517
610
|
}
|
|
518
611
|
/** Shorten a path under the user's home, which is where nearly all of these are. */
|
|
519
612
|
function display(filePath, env) {
|
|
@@ -0,0 +1,147 @@
|
|
|
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.mergeBayChatEntry = mergeBayChatEntry;
|
|
37
|
+
exports.writeDshMcpEntry = writeDshMcpEntry;
|
|
38
|
+
const fs = __importStar(require("fs"));
|
|
39
|
+
const os = __importStar(require("os"));
|
|
40
|
+
const path = __importStar(require("path"));
|
|
41
|
+
const crypto_1 = require("crypto");
|
|
42
|
+
// dsh composes every profile from layers, and `~/.dsh/cordis.patch.yml` applies to all of them —
|
|
43
|
+
// its equivalent of `~/.codex/config.toml`. The entry below was hand-wired and RUN on
|
|
44
|
+
// 2026-09-18 (docs/planning/2026-09-18-deepseek-harness-step-by-step.md).
|
|
45
|
+
//
|
|
46
|
+
// WHY A MARKED TEXT BLOCK AND NOT A YAML MERGE. The client's file may contain dsh's `!!js` tags,
|
|
47
|
+
// which a generic YAML parser rejects or, worse, round-trips wrongly. We never parse their file:
|
|
48
|
+
// we own the lines between two markers and nothing else, and we back the file up before writing.
|
|
49
|
+
//
|
|
50
|
+
// The token is a literal, as it is in Codex's config — the file is 0600. `connect` rewrites the
|
|
51
|
+
// block on every run, so a re-issued token replaces the old one rather than leaving it behind.
|
|
52
|
+
const START = "# >>> baychat (managed by `baychat connect dsh` — edits between these lines are overwritten)";
|
|
53
|
+
const END = "# <<< baychat";
|
|
54
|
+
const CORRUPT_BLOCK_MESSAGE = "~/.dsh/cordis.patch.yml has a damaged BayChat block (a start marker without its end, or " +
|
|
55
|
+
"duplicates). Remove the lines between '# >>> baychat' and '# <<< baychat' by hand, then run " +
|
|
56
|
+
"`baychat connect dsh` again.";
|
|
57
|
+
/**
|
|
58
|
+
* Refuse to interpolate anything that could break out of the block's YAML.
|
|
59
|
+
*
|
|
60
|
+
* Device tokens are `bay_u_` + hex and the MCP url is ours, so neither check should ever fire in
|
|
61
|
+
* practice — but a value we did not generate should never be trusted to hold still inside a
|
|
62
|
+
* double-quoted YAML scalar just because it happens to today.
|
|
63
|
+
*/
|
|
64
|
+
function validateEntry(entry) {
|
|
65
|
+
if (!/^[A-Za-z0-9_]+$/.test(entry.token)) {
|
|
66
|
+
throw new Error("Refusing to write a BayChat MCP entry: the device token contains characters other than letters, digits and underscore.");
|
|
67
|
+
}
|
|
68
|
+
if (/["'\s]/.test(entry.url)) {
|
|
69
|
+
throw new Error("Refusing to write a BayChat MCP entry: the MCP URL contains a quote, newline or whitespace.");
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
/**
|
|
73
|
+
* Find the single well-formed managed block in `existing`, or `null` when there is none at all.
|
|
74
|
+
*
|
|
75
|
+
* RULING R22: a START with no END after it, an END with no START, or more than one of either
|
|
76
|
+
* marker is CORRUPT — the file was hand-edited into a shape we cannot safely replace. Silently
|
|
77
|
+
* appending a second block on top of that would leave the old one, stale token included, in the
|
|
78
|
+
* file forever, so this throws instead of guessing.
|
|
79
|
+
*/
|
|
80
|
+
function findManagedBlock(existing) {
|
|
81
|
+
const firstStart = existing.indexOf(START);
|
|
82
|
+
const lastStart = existing.lastIndexOf(START);
|
|
83
|
+
const firstEnd = existing.indexOf(END);
|
|
84
|
+
const lastEnd = existing.lastIndexOf(END);
|
|
85
|
+
const hasStart = firstStart >= 0;
|
|
86
|
+
const hasEnd = firstEnd >= 0;
|
|
87
|
+
if (!hasStart && !hasEnd)
|
|
88
|
+
return null;
|
|
89
|
+
if (hasStart !== hasEnd)
|
|
90
|
+
throw new Error(CORRUPT_BLOCK_MESSAGE);
|
|
91
|
+
if (firstStart !== lastStart || firstEnd !== lastEnd)
|
|
92
|
+
throw new Error(CORRUPT_BLOCK_MESSAGE);
|
|
93
|
+
if (firstEnd < firstStart)
|
|
94
|
+
throw new Error(CORRUPT_BLOCK_MESSAGE);
|
|
95
|
+
return { from: firstStart, to: firstEnd };
|
|
96
|
+
}
|
|
97
|
+
function mergeBayChatEntry(existing, entry) {
|
|
98
|
+
validateEntry(entry);
|
|
99
|
+
const block = [
|
|
100
|
+
START,
|
|
101
|
+
"- insert:",
|
|
102
|
+
" - id: mcp-baychat",
|
|
103
|
+
" name: '@deepseek-ai/dsh-mcp-client'",
|
|
104
|
+
" config:",
|
|
105
|
+
" serverName: baychat",
|
|
106
|
+
" transport: streamable-http",
|
|
107
|
+
` url: ${entry.url}`,
|
|
108
|
+
" headers:",
|
|
109
|
+
` Authorization: "Bearer ${entry.token}"`,
|
|
110
|
+
END,
|
|
111
|
+
"",
|
|
112
|
+
].join("\n");
|
|
113
|
+
const found = findManagedBlock(existing);
|
|
114
|
+
if (found) {
|
|
115
|
+
const after = existing.slice(found.to + END.length).replace(/^\n/, "");
|
|
116
|
+
return `${existing.slice(0, found.from)}${block}${after}`;
|
|
117
|
+
}
|
|
118
|
+
const head = existing === "" || existing.endsWith("\n") ? existing : `${existing}\n`;
|
|
119
|
+
return `${head}${block}`;
|
|
120
|
+
}
|
|
121
|
+
function writeDshMcpEntry(entry, home = os.homedir()) {
|
|
122
|
+
const file = path.join(home, ".dsh", "cordis.patch.yml");
|
|
123
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
124
|
+
const existing = fs.existsSync(file) ? fs.readFileSync(file, "utf8") : "";
|
|
125
|
+
// Computed BEFORE anything is written: a corrupt block, or an unsafe entry, throws here and
|
|
126
|
+
// leaves the file — and any backup — untouched.
|
|
127
|
+
const next = mergeBayChatEntry(existing, entry);
|
|
128
|
+
let backup;
|
|
129
|
+
if (existing) {
|
|
130
|
+
backup = `${file}.baychat-backup`;
|
|
131
|
+
fs.writeFileSync(backup, existing, { mode: 0o600 });
|
|
132
|
+
}
|
|
133
|
+
// Atomic: write to a temp file in the same directory, then rename over the target. A reader —
|
|
134
|
+
// or a crash mid-write — never sees a half-written config. Same pattern as `writeModePatches`
|
|
135
|
+
// in relay/acp/agents.ts and `writeHermesMcp` in hermes-mcp.ts.
|
|
136
|
+
const temp = `${file}.${process.pid}.${(0, crypto_1.randomUUID)()}.tmp`;
|
|
137
|
+
try {
|
|
138
|
+
fs.writeFileSync(temp, next, { mode: 0o600, flag: "wx" });
|
|
139
|
+
fs.chmodSync(temp, 0o600); // `mode` above is filtered by the umask
|
|
140
|
+
fs.renameSync(temp, file);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
fs.rmSync(temp, { force: true });
|
|
144
|
+
throw err;
|
|
145
|
+
}
|
|
146
|
+
return { file, backup };
|
|
147
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -4,10 +4,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
4
4
|
const commands_1 = require("./commands");
|
|
5
5
|
const approve_hook_1 = require("./approve-hook");
|
|
6
6
|
const session_name_1 = require("./session-name");
|
|
7
|
+
const start_command_1 = require("./start-command");
|
|
8
|
+
const update_command_1 = require("./update-command");
|
|
7
9
|
const session_command_1 = require("./session-command");
|
|
8
10
|
const doctor_command_1 = require("./doctor-command");
|
|
9
11
|
const hermes_1 = require("./hermes");
|
|
10
12
|
const connect_1 = require("./connect");
|
|
13
|
+
const connect_plan_1 = require("./connect-plan");
|
|
14
|
+
const runtimes_1 = require("./runtimes");
|
|
11
15
|
const mcp_1 = require("./mcp");
|
|
12
16
|
const mcp_config_1 = require("./mcp-config");
|
|
13
17
|
const commands_2 = require("./relay/commands");
|
|
@@ -29,8 +33,19 @@ Exit 0: step succeeded; 2: awaiting approval or expired; 1: error.
|
|
|
29
33
|
const HELP = `baychat — BayChat connector CLI for agent sessions (Claude Code, Codex)
|
|
30
34
|
|
|
31
35
|
Usage:
|
|
36
|
+
baychat Start a session. This is the whole thing: it runs
|
|
37
|
+
your agent somewhere BayChat can reach, so /clear
|
|
38
|
+
and /new from the app work. Sign in once with
|
|
39
|
+
\`baychat login\` first
|
|
40
|
+
baychat start [name] [--runtime <runtime>]
|
|
41
|
+
The same, with a name for the session
|
|
42
|
+
baychat update Bring this machine onto the current version —
|
|
43
|
+
the package, every connected client's config, and
|
|
44
|
+
the relay daemon (which otherwise keeps running
|
|
45
|
+
old code). \`baychat\` does this for you once a day
|
|
32
46
|
baychat join [name] [group] [--sessions | --private | --group <title>] [--runtime <runtime>]
|
|
33
47
|
baychat join --session <name> --group <title> --runtime codex
|
|
48
|
+
baychat skill --runtime codex|claude Read the current packaged skill workflow
|
|
34
49
|
Join and connect incoming messages in this terminal
|
|
35
50
|
baychat session-name --runtime <runtime>
|
|
36
51
|
Stable automatic name for this verified session
|
|
@@ -167,9 +182,15 @@ function numberFlag(args, name) {
|
|
|
167
182
|
}
|
|
168
183
|
async function main() {
|
|
169
184
|
const [command, ...args] = process.argv.slice(2);
|
|
185
|
+
// BARE `baychat` STARTS A SESSION. The bar is "download BayChat, start a session, nothing
|
|
186
|
+
// else", and a usage dump printed at somebody who typed the product's own name is the
|
|
187
|
+
// "something else". `--help` still prints the help, and `start` refuses with one sentence
|
|
188
|
+
// when this machine is not signed in, so nothing is launched from an unusable state.
|
|
189
|
+
if (command === undefined)
|
|
190
|
+
return (0, start_command_1.cmdStart)([]);
|
|
170
191
|
// Setup help must be side-effect free. Do not apply this to chat commands:
|
|
171
192
|
// a sent message may legitimately contain the literal text "--help".
|
|
172
|
-
if (["login", "link", "connect", "pair", "join"].includes(command) &&
|
|
193
|
+
if (["login", "link", "connect", "pair", "join", "skill"].includes(command) &&
|
|
173
194
|
args.some((arg) => arg === "--help" || arg === "-h")) {
|
|
174
195
|
console.log(command === "login" ? LOGIN_HELP : HELP);
|
|
175
196
|
return 0;
|
|
@@ -181,6 +202,19 @@ async function main() {
|
|
|
181
202
|
return 0;
|
|
182
203
|
case "join":
|
|
183
204
|
return (0, session_command_1.cmdJoinSession)(args);
|
|
205
|
+
case "start":
|
|
206
|
+
return (0, start_command_1.cmdStart)(args);
|
|
207
|
+
case "update":
|
|
208
|
+
return (0, update_command_1.cmdUpdate)();
|
|
209
|
+
case "skill": {
|
|
210
|
+
const runtime = args[1];
|
|
211
|
+
if (args.length !== 2 ||
|
|
212
|
+
args[0] !== "--runtime" ||
|
|
213
|
+
(runtime !== "codex" && runtime !== "claude"))
|
|
214
|
+
throw new Error("Usage: baychat skill --runtime codex|claude");
|
|
215
|
+
console.log((0, runtimes_1.renderCommandFor)(runtime));
|
|
216
|
+
return 0;
|
|
217
|
+
}
|
|
184
218
|
case "session-name": {
|
|
185
219
|
(0, args_1.rejectUnknownFlags)(args, ["--runtime"], "baychat session-name --runtime <runtime>");
|
|
186
220
|
const runtime = (0, args_1.flag)(args, "--runtime") ?? (0, owner_pid_1.detectRuntime)(profiles_1.RUNTIME_PROFILES);
|
|
@@ -393,7 +427,8 @@ async function main() {
|
|
|
393
427
|
case "connect": {
|
|
394
428
|
// A bare `connect` prints the client menu; positional() skips a leading flag
|
|
395
429
|
// so `connect --base x codex` still finds the client.
|
|
396
|
-
|
|
430
|
+
const { client, ...options } = (0, connect_plan_1.parseConnectArgs)(args);
|
|
431
|
+
return await (0, connect_1.cmdConnect)(client, options);
|
|
397
432
|
}
|
|
398
433
|
case "mcp-config": {
|
|
399
434
|
// `--client` with no value is a typo, not a request for the menu: pass the
|
|
@@ -0,0 +1,186 @@
|
|
|
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.ACP_AGENTS = exports.DSH_LOCAL_TOOLS = void 0;
|
|
37
|
+
exports.acpAgent = acpAgent;
|
|
38
|
+
exports.matchesTestedVersion = matchesTestedVersion;
|
|
39
|
+
exports.modeAllowedHere = modeAllowedHere;
|
|
40
|
+
exports.acpPatchDir = acpPatchDir;
|
|
41
|
+
exports.patchPath = patchPath;
|
|
42
|
+
exports.writeModePatches = writeModePatches;
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
const path = __importStar(require("path"));
|
|
45
|
+
const config_1 = require("../../config");
|
|
46
|
+
const modes_1 = require("./modes");
|
|
47
|
+
// Every tool plugin in dsh's `acp` profile (`--profile acp --dump-config`, 0.1.5-rc.2) [run].
|
|
48
|
+
// MCP clients are mounted per ACP session by dsh-acp itself and are untouched by these.
|
|
49
|
+
// Confirmed against docs/planning/2026-09-19-acp-research/dump-acp-default.yml — the tool-* ids
|
|
50
|
+
// there match this list exactly, in the same order.
|
|
51
|
+
exports.DSH_LOCAL_TOOLS = [
|
|
52
|
+
"tool-bash", "tool-pwsh", "tool-jobs", "tool-fs", "tool-fs-search", "tool-skill",
|
|
53
|
+
"tool-subagent-control", "tool-subagent-list-agents", "tool-subagent", "tool-subagent-fork",
|
|
54
|
+
"tool-workflow", "tool-todo", "tool-goal", "tool-ralph", "tool-web",
|
|
55
|
+
];
|
|
56
|
+
const disable = (ids, why) => [`# Written by BayChat. ${why}`, ...ids.flatMap((id) => [`- id: ${id}`, " disabled: true"]), ""].join("\n");
|
|
57
|
+
const dsh = {
|
|
58
|
+
id: "dsh",
|
|
59
|
+
label: "DeepSeek Harness",
|
|
60
|
+
bin: "dsh",
|
|
61
|
+
patches: {
|
|
62
|
+
chat: disable(exports.DSH_LOCAL_TOOLS, "chat mode: no local tools at all."),
|
|
63
|
+
read: disable(exports.DSH_LOCAL_TOOLS.filter((id) => id !== "tool-fs" && id !== "tool-fs-search"), "read mode: file read and search only — no shell, no subagents, no web."),
|
|
64
|
+
},
|
|
65
|
+
launch(mode, patchDir) {
|
|
66
|
+
const args = ["--profile", "acp"];
|
|
67
|
+
if (this.patches[mode])
|
|
68
|
+
args.push("--patch", patchPath(this, mode, patchDir));
|
|
69
|
+
// `danger-full-access` is deliberately unreachable from here: no mode maps to it.
|
|
70
|
+
return { args, env: { DSH_PERMISSION_MODE: mode === "full" ? "workspace-write" : "read-only" } };
|
|
71
|
+
},
|
|
72
|
+
// dsh's Windows sandbox reports itself `partial` (Everyone-writable objects, NTFS hard links)
|
|
73
|
+
// [source], so the modes that RELY on it are withheld there. `chat` needs no sandbox and
|
|
74
|
+
// `read`'s write fence is an in-process check.
|
|
75
|
+
modesFor: (platform) => (platform === "win32" ? ["chat", "read"] : ["chat", "read", "ask", "full"]),
|
|
76
|
+
classifyError(message) {
|
|
77
|
+
if (/no API key|MISSING_CREDENTIAL|INVALID_CREDENTIAL/i.test(message))
|
|
78
|
+
return "missing-key";
|
|
79
|
+
if (/mcp-client\(baychat\)/i.test(message))
|
|
80
|
+
return "baychat-unreachable";
|
|
81
|
+
if (/already active|session is active/i.test(message))
|
|
82
|
+
return "busy";
|
|
83
|
+
if (/session cwd does not match/i.test(message))
|
|
84
|
+
return "session";
|
|
85
|
+
return undefined;
|
|
86
|
+
},
|
|
87
|
+
keyHint: "DeepSeek has no API key on that computer. There, run `dsh web` and add the key on the Models page.",
|
|
88
|
+
installHint: "Install it with `npm install -g @deepseek-ai/dsh`, then run `baychat connect dsh` again.",
|
|
89
|
+
confidence: "run",
|
|
90
|
+
checked: "2026-09-19",
|
|
91
|
+
testedVersion: "0.1.5-rc.2",
|
|
92
|
+
};
|
|
93
|
+
exports.ACP_AGENTS = [dsh];
|
|
94
|
+
function acpAgent(id) {
|
|
95
|
+
return exports.ACP_AGENTS.find((row) => row.id === id);
|
|
96
|
+
}
|
|
97
|
+
/** A version-string character: digits, letters, dot, plus, hyphen — the semver/rc alphabet. */
|
|
98
|
+
const VERSION_TOKEN_CHAR = /[0-9A-Za-z.+-]/;
|
|
99
|
+
/**
|
|
100
|
+
* Does `version` (raw `--version` output) contain `tested` as a WHOLE version token, not merely
|
|
101
|
+
* as a substring?
|
|
102
|
+
*
|
|
103
|
+
* `"0.1.5-rc.20".includes("0.1.5-rc.2")` is `true` — a plain substring check silently passes a
|
|
104
|
+
* different release and the mismatch warning this exists for never shows. So `tested` must be
|
|
105
|
+
* bounded: the character before it is the start of the string, something that is not part of a
|
|
106
|
+
* version token, or a leading `v` (allowed unconditionally — a version prefix, not a token
|
|
107
|
+
* character to fence against); the character after it is the end of the string or something that
|
|
108
|
+
* is not part of a version token. `--version` output varies by CLI — `"0.1.5-rc.2"`,
|
|
109
|
+
* `"dsh 0.1.5-rc.2"`, `"v0.1.5-rc.2"`, `"0.1.5-rc.2 (linux-x64)"` — and this matches all of them
|
|
110
|
+
* while refusing `"0.1.5-rc.20"`, `"10.1.5-rc.2"`, `"0.1.5-rc.2.1"` and `"0.1.5-rc.2-beta"`.
|
|
111
|
+
*/
|
|
112
|
+
function matchesTestedVersion(version, tested) {
|
|
113
|
+
if (!tested)
|
|
114
|
+
return false;
|
|
115
|
+
let from = 0;
|
|
116
|
+
for (;;) {
|
|
117
|
+
const at = version.indexOf(tested, from);
|
|
118
|
+
if (at === -1)
|
|
119
|
+
return false;
|
|
120
|
+
const before = at === 0 ? undefined : version[at - 1];
|
|
121
|
+
const after = version[at + tested.length];
|
|
122
|
+
const boundedBefore = before === undefined || before === "v" || !VERSION_TOKEN_CHAR.test(before);
|
|
123
|
+
const boundedAfter = after === undefined || !VERSION_TOKEN_CHAR.test(after);
|
|
124
|
+
if (boundedBefore && boundedAfter)
|
|
125
|
+
return true;
|
|
126
|
+
from = at + 1;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
/**
|
|
130
|
+
* May a session of this agent run in `mode`, in a folder whose ceiling is `maxMode`, on this
|
|
131
|
+
* platform? The ONE rule: `/mode` from the chat and `acp-register` from `connect` both ask it.
|
|
132
|
+
*/
|
|
133
|
+
function modeAllowedHere(row, mode, maxMode, platform) {
|
|
134
|
+
if ((0, modes_1.modeRank)(mode) > (0, modes_1.modeRank)(maxMode)) {
|
|
135
|
+
return { ok: false, reason: `${mode} is above the highest mode allowed for that folder (${maxMode}). That ceiling is set on the computer, never from a chat: run \`baychat connect ${row.id}\` there to change it.` };
|
|
136
|
+
}
|
|
137
|
+
if (!row.modesFor(platform).includes(mode)) {
|
|
138
|
+
return { ok: false, reason: `${mode} is not offered on that computer's operating system, because ${row.label}'s sandbox cannot fully enforce it there.` };
|
|
139
|
+
}
|
|
140
|
+
return { ok: true };
|
|
141
|
+
}
|
|
142
|
+
/** Where mode patches live: `~/.baychat/acp/`. */
|
|
143
|
+
function acpPatchDir() {
|
|
144
|
+
return path.join((0, config_1.configDir)(), "acp");
|
|
145
|
+
}
|
|
146
|
+
function patchPath(row, mode, dir) {
|
|
147
|
+
return path.join(dir, `${row.id}-${mode}.yml`);
|
|
148
|
+
}
|
|
149
|
+
/**
|
|
150
|
+
* (Re)write this agent's mode patches. Idempotent; called by `connect`, at daemon start and
|
|
151
|
+
* before every turn.
|
|
152
|
+
*
|
|
153
|
+
* NEVER TRUNCATE IN PLACE. Two sessions drain concurrently, so one session's rewrite can land
|
|
154
|
+
* while another's freshly spawned agent is reading the same patch — and an empty `chat` patch is
|
|
155
|
+
* an agent with every local tool. A patch that is already right is left alone; one that is not is
|
|
156
|
+
* written beside it and renamed over it, so a reader sees the old file or the new, never half.
|
|
157
|
+
*/
|
|
158
|
+
function writeModePatches(row, dir) {
|
|
159
|
+
fs.mkdirSync(dir, { recursive: true, mode: 0o700 });
|
|
160
|
+
for (const [mode, content] of Object.entries(row.patches)) {
|
|
161
|
+
const file = patchPath(row, mode, dir);
|
|
162
|
+
if (readOrUndefined(file) === content) {
|
|
163
|
+
fs.chmodSync(file, 0o600); // permissions only — the content and the inode stay as they are
|
|
164
|
+
continue;
|
|
165
|
+
}
|
|
166
|
+
const tmp = `${file}.${process.pid}.${Math.random().toString(36).slice(2)}.tmp`;
|
|
167
|
+
try {
|
|
168
|
+
fs.writeFileSync(tmp, content, { mode: 0o600, flag: "wx" });
|
|
169
|
+
fs.chmodSync(tmp, 0o600); // `mode` is filtered by the umask
|
|
170
|
+
fs.renameSync(tmp, file);
|
|
171
|
+
}
|
|
172
|
+
catch (err) {
|
|
173
|
+
fs.rmSync(tmp, { force: true });
|
|
174
|
+
throw err;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
/** A read error (absent, unreadable) means "write it". */
|
|
179
|
+
function readOrUndefined(file) {
|
|
180
|
+
try {
|
|
181
|
+
return fs.readFileSync(file, "utf8");
|
|
182
|
+
}
|
|
183
|
+
catch {
|
|
184
|
+
return undefined;
|
|
185
|
+
}
|
|
186
|
+
}
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.askPhone = askPhone;
|
|
4
|
+
const api_1 = require("../../api");
|
|
5
|
+
const approve_hook_1 = require("../../approve-hook");
|
|
6
|
+
const HOLD_SECONDS = 25; // under the usual 60s proxy idle timeout — same as approve-hook
|
|
7
|
+
const MAX_CONSECUTIVE_FAILURES = 3;
|
|
8
|
+
const RETRY_DELAY_MS = 2_000;
|
|
9
|
+
const ALLOW_INDEX = 0; // the ONLY index that means yes
|
|
10
|
+
/**
|
|
11
|
+
* Put a decision card on the owner's phone and wait for the tap.
|
|
12
|
+
*
|
|
13
|
+
* NEVER THROWS, and every path that is not an explicit "Allow this once" is a refusal: an
|
|
14
|
+
* unreachable server, a status we do not know, a withdrawn card, the deadline. The card itself
|
|
15
|
+
* never expires server-side; our deadline exists so a turn cannot hold a process forever, and
|
|
16
|
+
* reaching it REFUSES. Silence is never a yes.
|
|
17
|
+
*/
|
|
18
|
+
async function askPhone(params, deps = {}) {
|
|
19
|
+
const request = deps.request ?? api_1.apiRequest;
|
|
20
|
+
const now = deps.now ?? Date.now;
|
|
21
|
+
const sleep = deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms)));
|
|
22
|
+
const waitSeconds = params.waitSeconds ?? approve_hook_1.DEFAULT_WAIT_SECONDS;
|
|
23
|
+
const deadline = now() + waitSeconds * 1000;
|
|
24
|
+
const hold = () => Math.max(1, Math.min(HOLD_SECONDS, Math.ceil((deadline - now()) / 1000)));
|
|
25
|
+
const no = (reason) => ({ allowed: false, reason });
|
|
26
|
+
try {
|
|
27
|
+
let reply = await request(params.auth, "POST", "/api/device-api/approvals", {
|
|
28
|
+
session: params.session,
|
|
29
|
+
conversationId: params.conversationId,
|
|
30
|
+
question: params.question,
|
|
31
|
+
options: [...approve_hook_1.APPROVE_OPTIONS],
|
|
32
|
+
wait: hold(),
|
|
33
|
+
});
|
|
34
|
+
const requestId = typeof reply.requestId === "string" ? reply.requestId : "";
|
|
35
|
+
if (!requestId)
|
|
36
|
+
return no("BayChat did not return an approval id");
|
|
37
|
+
let failures = 0;
|
|
38
|
+
for (;;) {
|
|
39
|
+
if (reply.status === "answered") {
|
|
40
|
+
return reply.answerIndex === ALLOW_INDEX
|
|
41
|
+
? { allowed: true, reason: "approved in BayChat" }
|
|
42
|
+
: no(`answered "${reply.answer ?? approve_hook_1.APPROVE_OPTIONS[1]}" in BayChat`);
|
|
43
|
+
}
|
|
44
|
+
if (reply.status === "cancelled")
|
|
45
|
+
return no("the decision was withdrawn in BayChat");
|
|
46
|
+
if (reply.status !== "waiting")
|
|
47
|
+
return no(`BayChat answered with a status this relay does not understand ("${String(reply.status)}")`);
|
|
48
|
+
if (deadline - now() <= 0)
|
|
49
|
+
return no(`nobody answered within ${waitSeconds}s — the card is still in BayChat, and this request was refused`);
|
|
50
|
+
try {
|
|
51
|
+
reply = await request(params.auth, "GET", `/api/device-api/approvals/${encodeURIComponent(requestId)}?wait=${hold()}`);
|
|
52
|
+
failures = 0;
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (err instanceof api_1.ApiError && err.status < 500 && err.status !== 429)
|
|
56
|
+
return no(`BayChat refused the wait — ${err.message}`);
|
|
57
|
+
failures += 1;
|
|
58
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES)
|
|
59
|
+
return no(`BayChat became unreachable while waiting — ${err instanceof Error ? err.message : String(err)}`);
|
|
60
|
+
await sleep(RETRY_DELAY_MS);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
return no(`BayChat could not be asked — ${err instanceof Error ? err.message : String(err)}`);
|
|
66
|
+
}
|
|
67
|
+
}
|