baychat 0.33.1 → 0.33.3
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/.claude-plugin/plugin.json +1 -1
- package/dist/auto-update/policy.js +6 -3
- package/dist/auto-update/updater.js +85 -23
- package/dist/bay-mcp.js +6 -6
- package/dist/channel/launch.js +5 -1
- package/dist/commands.js +35 -43
- package/dist/connect-cursor.js +4 -2
- package/dist/doctor-command.js +7 -2
- package/dist/multiplexer-launch.js +12 -4
- package/dist/relay/acp/client.js +5 -2
- package/dist/relay/adapters.js +1 -0
- package/dist/relay/codex-app-server.js +6 -2
- package/dist/relay/codex-queue.js +10 -11
- package/dist/relay/daemon.js +9 -2
- package/dist/runtime-binary.js +37 -9
- package/dist/self-upgrade.js +10 -8
- package/dist/windows-cmd.js +142 -0
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.3",
|
|
4
4
|
"displayName": "BayChat",
|
|
5
5
|
"description": "Wakes this Claude Code session the moment a BayChat message arrives: no Monitor, no 30-minute deadline. Uses the baychat CLI already on this computer (npm install -g baychat, then baychat login).",
|
|
6
6
|
"author": {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.HELD_FRESH_MS = exports.QUIET_MS = exports.CARD_POLL_MS = exports.ASK_AGAIN_MS = exports.HINT_COOLDOWN_MS = exports.HINT_SPREAD_MS = exports.HINT_MIN_MS = exports.CHECK_INTERVAL_MS = exports.AUTO_UPDATE_ENV = void 0;
|
|
3
|
+
exports.HELD_FRESH_MS = exports.QUIET_MS = exports.CARD_POLL_MS = exports.ASK_AGAIN_MS = exports.HINT_RECHECK_MS = exports.HINT_COOLDOWN_MS = exports.HINT_SPREAD_MS = exports.HINT_MIN_MS = exports.CHECK_INTERVAL_MS = exports.AUTO_UPDATE_ENV = void 0;
|
|
4
4
|
exports.planUpdate = planUpdate;
|
|
5
5
|
exports.autoUpdateOffReason = autoUpdateOffReason;
|
|
6
6
|
exports.nextCheckAt = nextCheckAt;
|
|
@@ -54,9 +54,12 @@ const FIRST_CHECK_SPREAD_MS = 10 * MINUTE;
|
|
|
54
54
|
* 1–5 minutes — spread so a fleet that hears the same frame never asks npm together. */
|
|
55
55
|
exports.HINT_MIN_MS = MINUTE;
|
|
56
56
|
exports.HINT_SPREAD_MS = 4 * MINUTE;
|
|
57
|
-
/** A hint
|
|
58
|
-
* must not become an npm query on every reconnect. */
|
|
57
|
+
/** A hint never brings a check closer than this to the last one: a server that names a release
|
|
58
|
+
* on every reconnect must not become an npm query on every reconnect. */
|
|
59
59
|
exports.HINT_COOLDOWN_MS = 15 * MINUTE;
|
|
60
|
+
/** The server can name a release before npm has it (deployed first, published after): npm is
|
|
61
|
+
* asked again this long after each answer that is still older, then the hint is given up. */
|
|
62
|
+
exports.HINT_RECHECK_MS = [15 * MINUTE, 30 * MINUTE, HOUR];
|
|
60
63
|
/** A check that failed (offline, npm down) is tried again sooner than a day, then backs off. */
|
|
61
64
|
const CHECK_RETRY_MS = HOUR;
|
|
62
65
|
/** A failed install of one version waits a day, then two, … up to a week. A newer version resets it. */
|
|
@@ -17,10 +17,6 @@ class AutoUpdater {
|
|
|
17
17
|
running = false;
|
|
18
18
|
stopped = false;
|
|
19
19
|
lastCardPoll = 0;
|
|
20
|
-
/** A release the server named (hint.ts), and when npm is asked about it. In memory only: a
|
|
21
|
-
* restarted relay hears the next `ready` frame anyway. */
|
|
22
|
-
hinted;
|
|
23
|
-
hintCheckAt;
|
|
24
20
|
constructor(deps) {
|
|
25
21
|
this.deps = deps;
|
|
26
22
|
this.startedAt = deps.now();
|
|
@@ -28,6 +24,7 @@ class AutoUpdater {
|
|
|
28
24
|
}
|
|
29
25
|
/** Finish an update the previous relay installed, then look at the state once a minute. */
|
|
30
26
|
start() {
|
|
27
|
+
this.freshStart();
|
|
31
28
|
void this.runExclusive(() => this.finishPendingUpdate());
|
|
32
29
|
this.timer = setInterval(() => void this.tick(), exports.TICK_MS);
|
|
33
30
|
this.timer.unref();
|
|
@@ -50,34 +47,86 @@ class AutoUpdater {
|
|
|
50
47
|
failure: state.failure?.reason,
|
|
51
48
|
};
|
|
52
49
|
}
|
|
50
|
+
/**
|
|
51
|
+
* New code is a new chance. Check failures recorded by ANOTHER version say nothing about this
|
|
52
|
+
* one — the failure may be the very bug this version fixes. Real case, 2026-10-21: relays that
|
|
53
|
+
* failed 11 times on 0.33.0 ("npm was not found on PATH") carried that backoff into 0.33.1, which
|
|
54
|
+
* fixed it, and would not have asked npm again for a day. So the schedule starts over, and the
|
|
55
|
+
* first check comes 5–15 minutes after this start.
|
|
56
|
+
*/
|
|
57
|
+
freshStart() {
|
|
58
|
+
const state = this.deps.loadState();
|
|
59
|
+
if (!state.current || state.current === this.deps.current)
|
|
60
|
+
return;
|
|
61
|
+
if (state.lastCheckAt === undefined && !state.checkFailures)
|
|
62
|
+
return;
|
|
63
|
+
const was = state.current;
|
|
64
|
+
state.lastCheckAt = undefined;
|
|
65
|
+
state.checkFailures = undefined;
|
|
66
|
+
state.lastCheckError = undefined;
|
|
67
|
+
state.current = this.deps.current;
|
|
68
|
+
this.deps.saveState(state);
|
|
69
|
+
this.deps.log(`auto-update: now running ${this.deps.current} (checks were recorded by ${was}); asking npm afresh`);
|
|
70
|
+
}
|
|
53
71
|
/**
|
|
54
72
|
* The server's feed named `version` as the newest BayChat (hint.ts). Newer than this copy, and
|
|
55
73
|
* newer than what npm last said: ask npm in 1–5 minutes instead of at the daily check. Only the
|
|
56
74
|
* moment moves — npm still names the version, and planUpdate still decides what happens.
|
|
57
75
|
*
|
|
58
|
-
* The moment never moves earlier than the backoff allows: not within
|
|
59
|
-
* last check,
|
|
60
|
-
*
|
|
76
|
+
* The moment never moves earlier than the backoff allows (`hintDue`): not within
|
|
77
|
+
* HINT_COOLDOWN_MS of the last check, not before a failed check's retry. The hint is kept, on
|
|
78
|
+
* disk, rather than dropped: a server deployed before the npm publish names a version npm does
|
|
79
|
+
* not have yet (2026-10-21, 0.33.2: the hint's check came 1 minute before the publish), so npm
|
|
80
|
+
* is asked again after each HINT_RECHECK_MS step until it names that version, then given up.
|
|
61
81
|
*/
|
|
62
82
|
hint(version) {
|
|
63
83
|
const { mode } = this.mode();
|
|
64
84
|
if (mode === "off" || mode === "none")
|
|
65
85
|
return;
|
|
66
86
|
const ahead = (0, self_upgrade_1.compareVersions)(version, this.deps.current);
|
|
67
|
-
if (ahead === undefined || ahead <= 0
|
|
87
|
+
if (ahead === undefined || ahead <= 0)
|
|
68
88
|
return;
|
|
69
89
|
const state = this.deps.loadState();
|
|
70
90
|
const known = state.latest ? (0, self_upgrade_1.compareVersions)(state.latest, version) : undefined;
|
|
71
91
|
if (known !== undefined && known >= 0)
|
|
72
92
|
return; // npm already named it (or newer)
|
|
73
|
-
|
|
74
|
-
|
|
93
|
+
if (validHint(state.hint)?.version === version)
|
|
94
|
+
return; // waiting on it, or gave up on it
|
|
95
|
+
const nextAt = this.hintDue(state, this.deps.now() + policy_1.HINT_MIN_MS + this.deps.random() * policy_1.HINT_SPREAD_MS);
|
|
96
|
+
state.hint = { version, attempts: 0, nextAt };
|
|
97
|
+
this.deps.saveState(state);
|
|
98
|
+
this.deps.log(`auto-update: the server names ${version}; asking npm in ${Math.ceil((nextAt - this.deps.now()) / 60_000)} min`);
|
|
99
|
+
}
|
|
100
|
+
/** The earliest a hinted check may run: `wanted`, or later if the backoff says so. */
|
|
101
|
+
hintDue(state, wanted) {
|
|
102
|
+
let at = wanted;
|
|
103
|
+
if (state.lastCheckAt !== undefined)
|
|
104
|
+
at = Math.max(at, state.lastCheckAt + policy_1.HINT_COOLDOWN_MS);
|
|
105
|
+
if ((state.checkFailures ?? 0) > 0)
|
|
106
|
+
at = Math.max(at, (0, policy_1.nextCheckAt)(state, this.startedAt, this.spread));
|
|
107
|
+
return at;
|
|
108
|
+
}
|
|
109
|
+
/** After a check: the hint is answered (npm names it, or this copy has it), retried, or given up. */
|
|
110
|
+
settleHint(state, now, wasDue) {
|
|
111
|
+
const hint = validHint(state.hint);
|
|
112
|
+
if (!hint)
|
|
113
|
+
return void (state.hint = undefined);
|
|
114
|
+
const latest = state.latest ? (0, self_upgrade_1.compareVersions)(state.latest, hint.version) : undefined;
|
|
115
|
+
if ((latest !== undefined && latest >= 0) || (0, self_upgrade_1.compareVersions)(hint.version, this.deps.current) !== 1) {
|
|
116
|
+
state.hint = undefined;
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
if (!wasDue || hint.nextAt === undefined)
|
|
75
120
|
return;
|
|
76
|
-
|
|
121
|
+
const attempts = hint.attempts + 1;
|
|
122
|
+
const wait = policy_1.HINT_RECHECK_MS[attempts - 1];
|
|
123
|
+
if (wait === undefined) {
|
|
124
|
+
state.hint = { version: hint.version, attempts };
|
|
125
|
+
this.deps.log(`auto-update: npm still does not have ${hint.version}; the daily check takes over`);
|
|
77
126
|
return;
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
this.deps.log(`auto-update:
|
|
127
|
+
}
|
|
128
|
+
state.hint = { version: hint.version, attempts, nextAt: now + wait };
|
|
129
|
+
this.deps.log(`auto-update: npm does not have ${hint.version} yet; asking again in ${wait / 60_000} min`);
|
|
81
130
|
}
|
|
82
131
|
/** One look at the state. Never throws; never runs twice at once. */
|
|
83
132
|
async tick() {
|
|
@@ -97,11 +146,10 @@ class AutoUpdater {
|
|
|
97
146
|
if (mode === "off" || mode === "none")
|
|
98
147
|
return;
|
|
99
148
|
const now = this.deps.now();
|
|
100
|
-
const
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
await this.check(state, mode);
|
|
104
|
-
}
|
|
149
|
+
const hintAt = validHint(state.hint)?.nextAt;
|
|
150
|
+
const hinted = hintAt !== undefined && now >= hintAt;
|
|
151
|
+
if (hinted || now >= (0, policy_1.nextCheckAt)(state, this.startedAt, this.spread))
|
|
152
|
+
await this.check(state, mode, hinted);
|
|
105
153
|
if (mode !== "auto")
|
|
106
154
|
return;
|
|
107
155
|
if (state.card?.requestId && now - this.lastCardPoll >= policy_1.CARD_POLL_MS)
|
|
@@ -129,20 +177,25 @@ class AutoUpdater {
|
|
|
129
177
|
return { mode: "notify", reason: "for a relay started by hand (install with `baychat update`)" };
|
|
130
178
|
return { mode: "auto" };
|
|
131
179
|
}
|
|
132
|
-
/** Ask npm, and decide. */
|
|
133
|
-
async check(state, mode) {
|
|
180
|
+
/** Ask npm, and decide. `hinted`: the check a server's hint asked for (counts as a hint attempt). */
|
|
181
|
+
async check(state, mode, hinted = false) {
|
|
134
182
|
const looked = await this.deps.lookupLatest();
|
|
135
|
-
|
|
183
|
+
const now = this.deps.now();
|
|
184
|
+
state.lastCheckAt = now;
|
|
136
185
|
if (!looked.ok) {
|
|
137
|
-
// Offline, npm down: quiet. Tried again within the hour, then less often.
|
|
186
|
+
// Offline, npm down: quiet. Tried again within the hour, then less often — a waiting hint too.
|
|
138
187
|
state.checkFailures = (state.checkFailures ?? 0) + 1;
|
|
139
188
|
state.lastCheckError = looked.reason;
|
|
189
|
+
const hint = validHint(state.hint);
|
|
190
|
+
if (hint?.nextAt !== undefined)
|
|
191
|
+
state.hint = { ...hint, nextAt: this.hintDue(state, now) };
|
|
140
192
|
this.deps.log(`auto-update: could not ask npm (${looked.reason})`);
|
|
141
193
|
return this.deps.saveState(state);
|
|
142
194
|
}
|
|
143
195
|
state.checkFailures = 0;
|
|
144
196
|
state.lastCheckError = undefined;
|
|
145
197
|
state.latest = looked.release.version;
|
|
198
|
+
this.settleHint(state, now, hinted);
|
|
146
199
|
const plan = (0, policy_1.planUpdate)(this.deps.current, looked.release);
|
|
147
200
|
if (plan.kind === "current") {
|
|
148
201
|
// Updated some other way (`baychat update`), or nothing new: nothing may stay on the phone.
|
|
@@ -355,6 +408,15 @@ class AutoUpdater {
|
|
|
355
408
|
}
|
|
356
409
|
}
|
|
357
410
|
exports.AutoUpdater = AutoUpdater;
|
|
411
|
+
/** The persisted hint, if it is one: the state file is ours, but a hand edit or an older shape must
|
|
412
|
+
* not stop the updater. */
|
|
413
|
+
function validHint(raw) {
|
|
414
|
+
if (!raw || typeof raw !== "object" || typeof raw.version !== "string" || typeof raw.attempts !== "number")
|
|
415
|
+
return undefined;
|
|
416
|
+
if (raw.nextAt !== undefined && typeof raw.nextAt !== "number")
|
|
417
|
+
return undefined;
|
|
418
|
+
return raw;
|
|
419
|
+
}
|
|
358
420
|
function errText(err) {
|
|
359
421
|
return err instanceof Error ? err.message : String(err);
|
|
360
422
|
}
|
package/dist/bay-mcp.js
CHANGED
|
@@ -20,6 +20,7 @@ exports.realClaudeRun = realClaudeRun;
|
|
|
20
20
|
exports.syncBayEntriesWithClaude = syncBayEntriesWithClaude;
|
|
21
21
|
exports.applyBayEntries = applyBayEntries;
|
|
22
22
|
const node_child_process_1 = require("node:child_process");
|
|
23
|
+
const windows_cmd_1 = require("./windows-cmd");
|
|
23
24
|
const client_config_writer_1 = require("./client-config-writer");
|
|
24
25
|
const client_paths_1 = require("./client-paths");
|
|
25
26
|
const mcp_dialects_1 = require("./mcp-dialects");
|
|
@@ -96,21 +97,20 @@ function claudeBayAddArgs(name, baseUrl, token) {
|
|
|
96
97
|
`Authorization: Bearer ${token}`,
|
|
97
98
|
];
|
|
98
99
|
}
|
|
99
|
-
/** Characters cmd.exe quoting cannot hold (commands.ts `WINDOWS_UNSAFE`). */
|
|
100
|
-
const WINDOWS_UNSAFE = /["%\u0000-\u001f\u007f]/;
|
|
101
100
|
/** Run `claude <args>` the way `registerWithClaude` does, or null when it cannot run safely. */
|
|
102
101
|
function realClaudeRun(platform = process.platform) {
|
|
103
102
|
return (args) => {
|
|
104
|
-
const
|
|
105
|
-
if (
|
|
103
|
+
const windows = platform === "win32";
|
|
104
|
+
if (windows && args.some((a) => windows_cmd_1.WINDOWS_UNSAFE.test(a))) {
|
|
106
105
|
return { status: null, stderr: "", error: new Error("unsafe argument for cmd.exe") };
|
|
107
106
|
}
|
|
108
|
-
const
|
|
107
|
+
const run = windows ? (0, windows_cmd_1.cmdExeSpawn)("claude", args.map(windows_cmd_1.quoteForCmd)) : { command: "claude", args };
|
|
108
|
+
const res = (0, node_child_process_1.spawnSync)(run.command, run.args, {
|
|
109
109
|
encoding: "utf8",
|
|
110
110
|
stdio: ["ignore", "ignore", "pipe"],
|
|
111
111
|
timeout: 15_000,
|
|
112
112
|
killSignal: "SIGKILL",
|
|
113
|
-
|
|
113
|
+
windowsVerbatimArguments: windows,
|
|
114
114
|
});
|
|
115
115
|
return { status: res.status, stderr: String(res.stderr ?? ""), ...(res.error ? { error: res.error } : {}) };
|
|
116
116
|
};
|
package/dist/channel/launch.js
CHANGED
|
@@ -113,7 +113,11 @@ function channelLaunchFor(claudePath, deps = {}) {
|
|
|
113
113
|
return { on: true, args: [detect_1.DEV_CHANNELS_FLAG, ...entries], env: { ...exports.CHANNEL_ENV } };
|
|
114
114
|
}
|
|
115
115
|
function probeVersion(claudePath) {
|
|
116
|
-
|
|
116
|
+
// claude.cmd on Windows cannot be spawned as is (EINVAL), and then the channel stayed off.
|
|
117
|
+
const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(claudePath, process.platform), ["--version"]);
|
|
118
|
+
if (!run.ok)
|
|
119
|
+
return undefined;
|
|
120
|
+
const probe = (0, node_child_process_1.spawnSync)(run.file, run.args, { encoding: "utf8", timeout: 10_000, windowsVerbatimArguments: run.verbatim });
|
|
117
121
|
return probe.error || probe.status !== 0 ? undefined : String(probe.stdout ?? "");
|
|
118
122
|
}
|
|
119
123
|
/**
|
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.realConfigIo = exports.WINDOWS_UNSAFE = exports.CATCHUP_UNTRUSTED_REMINDER = void 0;
|
|
6
|
+
exports.realConfigIo = exports.WINDOWS_UNSAFE = exports.quoteForCmd = exports.cmdExeSpawn = exports.CATCHUP_UNTRUSTED_REMINDER = void 0;
|
|
7
7
|
exports.requireCredentials = requireCredentials;
|
|
8
8
|
exports.cmdPair = cmdPair;
|
|
9
9
|
exports.cmdWhoami = cmdWhoami;
|
|
@@ -18,7 +18,7 @@ exports.resetSessionState = resetSessionState;
|
|
|
18
18
|
exports.cmdCheck = cmdCheck;
|
|
19
19
|
exports.cmdWatch = cmdWatch;
|
|
20
20
|
exports.cmdLink = cmdLink;
|
|
21
|
-
exports.
|
|
21
|
+
exports.claudeSpawn = claudeSpawn;
|
|
22
22
|
exports.claudeMcpAddSpawn = claudeMcpAddSpawn;
|
|
23
23
|
exports.refreshConnectedClients = refreshConnectedClients;
|
|
24
24
|
exports.registerWithClaude = registerWithClaude;
|
|
@@ -37,6 +37,7 @@ const protocol_1 = require("./protocol");
|
|
|
37
37
|
const connection_qr_1 = require("./connection-qr");
|
|
38
38
|
const config_1 = require("./config");
|
|
39
39
|
const bay_mcp_1 = require("./bay-mcp");
|
|
40
|
+
const windows_cmd_1 = require("./windows-cmd");
|
|
40
41
|
const context_1 = require("./context");
|
|
41
42
|
const runtime_install_1 = require("./runtime-install");
|
|
42
43
|
const credential_refresh_1 = require("./credential-refresh");
|
|
@@ -570,44 +571,37 @@ function printManualMcpAdd(baseUrl) {
|
|
|
570
571
|
console.log(` claude mcp add --transport http --scope user baychat ${baseUrl}/api/mcp \\`);
|
|
571
572
|
console.log(` --header "Authorization: Bearer <your token — in ~/.baychat/credentials.json under device.token>"`);
|
|
572
573
|
}
|
|
574
|
+
var windows_cmd_2 = require("./windows-cmd");
|
|
575
|
+
Object.defineProperty(exports, "cmdExeSpawn", { enumerable: true, get: function () { return windows_cmd_2.cmdExeSpawn; } });
|
|
576
|
+
Object.defineProperty(exports, "quoteForCmd", { enumerable: true, get: function () { return windows_cmd_2.quoteForCmd; } });
|
|
577
|
+
Object.defineProperty(exports, "WINDOWS_UNSAFE", { enumerable: true, get: function () { return windows_cmd_2.WINDOWS_UNSAFE; } });
|
|
573
578
|
/**
|
|
574
|
-
*
|
|
575
|
-
* stay literal. Only usable on values `WINDOWS_UNSAFE` has already cleared.
|
|
576
|
-
*/
|
|
577
|
-
function quoteForCmd(arg) {
|
|
578
|
-
return `"${arg}"`;
|
|
579
|
-
}
|
|
580
|
-
/**
|
|
581
|
-
* Characters that survive — or break out of — cmd.exe double quotes.
|
|
582
|
-
*
|
|
583
|
-
* A `"` ends the wrapper, so everything after it is parsed as shell syntax. A
|
|
584
|
-
* `%NAME%` is expanded *inside* quotes. A newline ends the command line. Nothing
|
|
585
|
-
* else in cmd's metacharacter set (`&`, `|`, `<`, `>`, `^`) is interpreted while
|
|
586
|
-
* quoted, and `!` only expands under delayed expansion, which `cmd /d /s /c` —
|
|
587
|
-
* what Node's `shell: true` invokes — does not enable.
|
|
588
|
-
*/
|
|
589
|
-
exports.WINDOWS_UNSAFE = /["%\u0000-\u001f\u007f]/;
|
|
590
|
-
/**
|
|
591
|
-
* The spawn recipe for `claude mcp add`, or null when it cannot be run safely.
|
|
579
|
+
* The spawn recipe for `claude <args>`, or null when it cannot be run safely.
|
|
592
580
|
*
|
|
593
581
|
* On Windows `claude` is a `.cmd` shim, and Node has refused to spawn `.bat` /
|
|
594
|
-
* `.cmd`
|
|
595
|
-
*
|
|
596
|
-
*
|
|
597
|
-
* shell
|
|
582
|
+
* `.cmd` directly since the CVE-2024-27980 fix (18.20.2 / 20.12.2+) — it throws
|
|
583
|
+
* EINVAL. Every runtime this package supports (node >=20) is past that fix, so
|
|
584
|
+
* naming `claude.cmd` explicitly cannot work either: cmd.exe is the only route
|
|
585
|
+
* (`cmdExeSpawn`, the same line `shell: true` used to build).
|
|
598
586
|
*
|
|
599
|
-
* The cost of
|
|
600
|
-
*
|
|
601
|
-
*
|
|
602
|
-
*
|
|
603
|
-
*
|
|
604
|
-
*
|
|
605
|
-
* something quoting cannot contain. Refusing costs the user one manual paste;
|
|
606
|
-
* guessing would run their token through a command interpreter.
|
|
587
|
+
* The cost of cmd.exe is that arguments become shell syntax. It gets one string
|
|
588
|
+
* of argv joined with spaces, so `Authorization: Bearer <token>` would arrive as
|
|
589
|
+
* three separate arguments, and a `&` in an interpolated value would arrive as a
|
|
590
|
+
* command separator. Hence: quote every argument here, and refuse outright when
|
|
591
|
+
* a value contains something quoting cannot contain. Refusing costs the user one
|
|
592
|
+
* manual paste; guessing would run their token through a command interpreter.
|
|
607
593
|
*
|
|
608
594
|
* POSIX keeps `execve` semantics — argv is passed verbatim, there is no shell to
|
|
609
595
|
* interpret it, and so nothing to quote or refuse.
|
|
610
596
|
*/
|
|
597
|
+
function claudeSpawn(platform, args) {
|
|
598
|
+
if (platform !== "win32")
|
|
599
|
+
return { command: "claude", args, viaCmd: false };
|
|
600
|
+
if (args.some((a) => windows_cmd_1.WINDOWS_UNSAFE.test(a)))
|
|
601
|
+
return null;
|
|
602
|
+
return { ...(0, windows_cmd_1.cmdExeSpawn)("claude", args.map(windows_cmd_1.quoteForCmd)), viaCmd: true };
|
|
603
|
+
}
|
|
604
|
+
/** `claude mcp add` for the remote BayChat server — see `claudeSpawn`. */
|
|
611
605
|
function claudeMcpAddSpawn(platform, baseUrl, token) {
|
|
612
606
|
const args = [
|
|
613
607
|
"mcp",
|
|
@@ -621,11 +615,7 @@ function claudeMcpAddSpawn(platform, baseUrl, token) {
|
|
|
621
615
|
"--header",
|
|
622
616
|
`Authorization: Bearer ${token}`,
|
|
623
617
|
];
|
|
624
|
-
|
|
625
|
-
return { command: "claude", args, shell: false };
|
|
626
|
-
if (args.some((a) => exports.WINDOWS_UNSAFE.test(a)))
|
|
627
|
-
return null;
|
|
628
|
-
return { command: "claude", args: args.map(quoteForCmd), shell: true };
|
|
618
|
+
return claudeSpawn(platform, args);
|
|
629
619
|
}
|
|
630
620
|
/**
|
|
631
621
|
* Register the remote BayChat MCP server with Claude Code, carrying the device
|
|
@@ -705,9 +695,9 @@ function registerWithClaude(baseUrl, token) {
|
|
|
705
695
|
stdio: ["ignore", "ignore", "pipe"],
|
|
706
696
|
timeout: 15_000,
|
|
707
697
|
killSignal: "SIGKILL",
|
|
708
|
-
|
|
698
|
+
windowsVerbatimArguments: recipe.viaCmd,
|
|
709
699
|
});
|
|
710
|
-
// 9009 is cmd.exe's "'claude' is not recognized":
|
|
700
|
+
// 9009 is cmd.exe's "'claude' is not recognized": through cmd.exe there is no
|
|
711
701
|
// ENOENT to catch, and reporting a missing binary as a generic failure would
|
|
712
702
|
// point the user at the renewal advice below instead of at installing it.
|
|
713
703
|
// "already exists" is what a RENEWAL looks like, and it was being reported as
|
|
@@ -716,18 +706,20 @@ function registerWithClaude(baseUrl, token) {
|
|
|
716
706
|
// scheduled state of a credential with an expiry, so it is handled, not
|
|
717
707
|
// reported.
|
|
718
708
|
if (res.status !== 0 && /already exists/i.test(String(res.stderr ?? ""))) {
|
|
719
|
-
|
|
709
|
+
// Fixed words only, so this recipe is never null where `recipe` was not.
|
|
710
|
+
const remove = claudeSpawn(process.platform, ["mcp", "remove", "baychat"]);
|
|
711
|
+
const removed = (0, node_child_process_1.spawnSync)(remove.command, remove.args, {
|
|
720
712
|
encoding: "utf8",
|
|
721
713
|
stdio: ["ignore", "ignore", "pipe"],
|
|
722
714
|
timeout: 15_000,
|
|
723
|
-
|
|
715
|
+
windowsVerbatimArguments: remove.viaCmd,
|
|
724
716
|
});
|
|
725
717
|
if (removed.status === 0) {
|
|
726
718
|
const readded = (0, node_child_process_1.spawnSync)(recipe.command, recipe.args, {
|
|
727
719
|
encoding: "utf8",
|
|
728
720
|
stdio: ["ignore", "ignore", "pipe"],
|
|
729
721
|
timeout: 15_000,
|
|
730
|
-
|
|
722
|
+
windowsVerbatimArguments: recipe.viaCmd,
|
|
731
723
|
});
|
|
732
724
|
if (readded.status === 0) {
|
|
733
725
|
console.log("✓ BayChat re-registered with Claude Code (credential renewed)");
|
|
@@ -736,7 +728,7 @@ function registerWithClaude(baseUrl, token) {
|
|
|
736
728
|
}
|
|
737
729
|
}
|
|
738
730
|
}
|
|
739
|
-
if (recipe.
|
|
731
|
+
if (recipe.viaCmd && res.status === 9009) {
|
|
740
732
|
console.log("\nClaude Code CLI not found — add BayChat manually:");
|
|
741
733
|
printManualMcpAdd(baseUrl);
|
|
742
734
|
return false;
|
package/dist/connect-cursor.js
CHANGED
|
@@ -25,9 +25,11 @@ function cursorLoginState(statusOutput) {
|
|
|
25
25
|
}
|
|
26
26
|
function runAgentStatus(binaryPath) {
|
|
27
27
|
// No shell: the path came from PATH resolution. A Windows `.cmd` shim needs its interpreter named.
|
|
28
|
-
const
|
|
28
|
+
const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(binaryPath, process.platform), ["status"]);
|
|
29
|
+
if (!run.ok)
|
|
30
|
+
return Promise.resolve(run.reason);
|
|
29
31
|
return new Promise((resolve) => {
|
|
30
|
-
(0, child_process_1.execFile)(
|
|
32
|
+
(0, child_process_1.execFile)(run.file, run.args, { timeout: STATUS_TIMEOUT_MS, windowsHide: true, shell: false, windowsVerbatimArguments: run.verbatim }, (err, stdout, stderr) => {
|
|
31
33
|
const said = `${stdout ?? ""}${stderr ?? ""}`;
|
|
32
34
|
// A failed run is still an answer: "Not logged in" may come with a non-zero exit.
|
|
33
35
|
resolve(said || (err ? err.message : ""));
|
package/dist/doctor-command.js
CHANGED
|
@@ -147,9 +147,14 @@ async function currentDoctorEnv() {
|
|
|
147
147
|
// Same .cmd rule as the probe: an npm-installed CLI on Windows is a shim
|
|
148
148
|
// that cannot be spawned without a shell, and this call would otherwise
|
|
149
149
|
// throw EINVAL and be reported as "could not run `codex mcp list`".
|
|
150
|
-
const
|
|
150
|
+
const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(resolved.path, process.platform), ["mcp", "list"]);
|
|
151
|
+
if (!run.ok)
|
|
152
|
+
return null;
|
|
153
|
+
// execFileSync passes this on to spawnSync, which honours it; its option type leaves it out.
|
|
154
|
+
const verbatim = { windowsVerbatimArguments: run.verbatim };
|
|
151
155
|
try {
|
|
152
|
-
return (0, child_process_1.execFileSync)(
|
|
156
|
+
return (0, child_process_1.execFileSync)(run.file, run.args, {
|
|
157
|
+
...verbatim,
|
|
153
158
|
encoding: "utf8",
|
|
154
159
|
timeout: MCP_LIST_TIMEOUT_MS,
|
|
155
160
|
stdio: ["ignore", "pipe", "pipe"],
|
|
@@ -74,6 +74,7 @@ const path = __importStar(require("path"));
|
|
|
74
74
|
const readline = __importStar(require("readline"));
|
|
75
75
|
const config_1 = require("./config");
|
|
76
76
|
const terminal_pane_1 = require("./relay/terminal-pane");
|
|
77
|
+
const runtime_binary_1 = require("./runtime-binary");
|
|
77
78
|
/**
|
|
78
79
|
* Start `program` where session commands can reach it — inside tmux or screen when possible —
|
|
79
80
|
* and return its exit status. Never refuses to start the agent: every failure on the way falls
|
|
@@ -93,9 +94,16 @@ async function launchAgent(program, naming, deps = realLaunchDeps()) {
|
|
|
93
94
|
return runHere(program, deps);
|
|
94
95
|
return result.status ?? 0;
|
|
95
96
|
}
|
|
96
|
-
/** Run the agent in this terminal, as it would run without BayChat.
|
|
97
|
+
/** Run the agent in this terminal, as it would run without BayChat. On Windows an npm-installed
|
|
98
|
+
* agent is a `.cmd` shim, which Node will not spawn directly (EINVAL): `spawnPlanFor` starts
|
|
99
|
+
* what the shim runs. */
|
|
97
100
|
function runHere(program, deps = realLaunchDeps()) {
|
|
98
|
-
const
|
|
101
|
+
const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(program.binary, deps.platform), program.args);
|
|
102
|
+
if (!run.ok) {
|
|
103
|
+
deps.say(`Could not start ${path.basename(program.binary)}: ${run.reason}.`);
|
|
104
|
+
return 1;
|
|
105
|
+
}
|
|
106
|
+
const result = deps.run(run.file, run.args, { ...deps.env, ...program.env }, run.verbatim);
|
|
99
107
|
if (result.error) {
|
|
100
108
|
deps.say(`Could not start ${path.basename(program.binary)}: ${result.error.message}`);
|
|
101
109
|
return 1;
|
|
@@ -222,8 +230,8 @@ function realLaunchDeps() {
|
|
|
222
230
|
env: process.env,
|
|
223
231
|
atTerminal: !!process.stdin.isTTY && !!process.stdout.isTTY,
|
|
224
232
|
pid: process.pid,
|
|
225
|
-
run: (file, args, env) => {
|
|
226
|
-
const r = (0, child_process_1.spawnSync)(file, args, { stdio: "inherit", env });
|
|
233
|
+
run: (file, args, env, verbatim) => {
|
|
234
|
+
const r = (0, child_process_1.spawnSync)(file, args, { stdio: "inherit", env, windowsVerbatimArguments: verbatim });
|
|
227
235
|
return { status: r.status, ...(r.error ? { error: r.error } : {}) };
|
|
228
236
|
},
|
|
229
237
|
works: (program) => {
|
package/dist/relay/acp/client.js
CHANGED
|
@@ -71,11 +71,14 @@ async function runAcpTurn(req, deps) {
|
|
|
71
71
|
let child;
|
|
72
72
|
try {
|
|
73
73
|
// A Windows `.cmd` shim needs its interpreter named — same fix as `codex-app-server.ts`.
|
|
74
|
-
const
|
|
75
|
-
|
|
74
|
+
const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(req.binaryPath, deps.platform ?? process.platform), req.args);
|
|
75
|
+
if (!run.ok)
|
|
76
|
+
return failed("spawn", `the agent could not be started: ${run.reason}`);
|
|
77
|
+
child = deps.spawn(run.file, run.args, {
|
|
76
78
|
cwd: req.cwd,
|
|
77
79
|
env: req.env,
|
|
78
80
|
shell: false,
|
|
81
|
+
windowsVerbatimArguments: run.verbatim,
|
|
79
82
|
windowsHide: true,
|
|
80
83
|
stdio: ["pipe", "pipe", "pipe"],
|
|
81
84
|
});
|
package/dist/relay/adapters.js
CHANGED
|
@@ -397,6 +397,7 @@ function runHeadless(file, args, opts = {}) {
|
|
|
397
397
|
return new Promise((resolve) => {
|
|
398
398
|
const child = (0, child_process_1.spawn)(file, args, {
|
|
399
399
|
windowsHide: true,
|
|
400
|
+
windowsVerbatimArguments: opts.windowsVerbatimArguments,
|
|
400
401
|
cwd: opts.cwd,
|
|
401
402
|
// An npm-installed runtime is a script with a `#!/usr/bin/env node`
|
|
402
403
|
// shebang, and the daemon's systemd PATH has no node. See ./spawn-env.ts.
|
|
@@ -53,9 +53,13 @@ function runCodexTurn(req, deps) {
|
|
|
53
53
|
// `delivery failed for CodexTest: spawn EINVAL` on every wake — the
|
|
54
54
|
// transport failing before the handshake, on a machine where Codex was
|
|
55
55
|
// installed and working.
|
|
56
|
-
|
|
57
|
-
const
|
|
56
|
+
// `app-server` is a fixed word, so a plan always yields argv — `!ok` is unreachable here.
|
|
57
|
+
const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(req.binaryPath, process.platform), ["app-server"]);
|
|
58
|
+
if (!run.ok)
|
|
59
|
+
return resolve({ kind: "failed", transportUnusable: true, reason: run.reason });
|
|
60
|
+
const child = spawn(run.file, run.args, {
|
|
58
61
|
windowsHide: true,
|
|
62
|
+
windowsVerbatimArguments: run.verbatim,
|
|
59
63
|
cwd: req.cwd,
|
|
60
64
|
// Same reason as `runHeadless`: an npm-installed codex is a script with a
|
|
61
65
|
// `#!/usr/bin/env node` shebang and the daemon's PATH has no node. Both
|
|
@@ -39,18 +39,14 @@ function queueToThread(input) {
|
|
|
39
39
|
// interpreter is named explicitly. NEVER `shell: true`: the message is text
|
|
40
40
|
// written by other people in the room, and a shell string would make `$(…)`
|
|
41
41
|
// in a chat message run on this box.
|
|
42
|
-
const
|
|
42
|
+
const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(input.binaryPath, input.platform ?? process.platform), ["queue", "--thread", input.threadId, "--message", input.message]);
|
|
43
|
+
if (!run.ok)
|
|
44
|
+
return Promise.resolve({ kind: "failed", reason: run.reason });
|
|
43
45
|
return new Promise((resolve) => {
|
|
44
|
-
runner(
|
|
45
|
-
...plan.prefixArgs,
|
|
46
|
-
"queue",
|
|
47
|
-
"--thread",
|
|
48
|
-
input.threadId,
|
|
49
|
-
"--message",
|
|
50
|
-
input.message,
|
|
51
|
-
], {
|
|
46
|
+
runner(run.file, run.args, {
|
|
52
47
|
cwd: input.cwd,
|
|
53
48
|
windowsHide: true,
|
|
49
|
+
windowsVerbatimArguments: run.verbatim,
|
|
54
50
|
env: (0, spawn_env_1.headlessSpawnEnv)(),
|
|
55
51
|
timeout: input.timeoutMs ?? QUEUE_TIMEOUT_MS,
|
|
56
52
|
maxBuffer: 1024 * 1024,
|
|
@@ -80,11 +76,14 @@ function queueToThread(input) {
|
|
|
80
76
|
}
|
|
81
77
|
/** Verify native queue support without submitting a message or changing a task. */
|
|
82
78
|
function verifyCodexQueue(binaryPath) {
|
|
83
|
-
const
|
|
79
|
+
const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(binaryPath, process.platform), ["queue", "--help"]);
|
|
80
|
+
if (!run.ok)
|
|
81
|
+
return Promise.reject(new Error(run.reason));
|
|
84
82
|
return new Promise((resolve, reject) => {
|
|
85
|
-
(0, child_process_1.execFile)(
|
|
83
|
+
(0, child_process_1.execFile)(run.file, run.args, {
|
|
86
84
|
env: (0, spawn_env_1.headlessSpawnEnv)(),
|
|
87
85
|
windowsHide: true,
|
|
86
|
+
windowsVerbatimArguments: run.verbatim,
|
|
88
87
|
timeout: 5_000,
|
|
89
88
|
maxBuffer: 64 * 1024,
|
|
90
89
|
}, (error, stdout) => {
|
package/dist/relay/daemon.js
CHANGED
|
@@ -1281,12 +1281,19 @@ class RelayDaemon {
|
|
|
1281
1281
|
// the path — resolution or the session's own runtimeBin — so the decision
|
|
1282
1282
|
// lives here, after both, rather than inside either. Never a shell: these
|
|
1283
1283
|
// args carry the wake prompt, and therefore other people's message text.
|
|
1284
|
-
const
|
|
1284
|
+
const run = (0, runtime_binary_1.planArgv)((0, runtime_binary_1.spawnPlanFor)(executable, process.platform), args);
|
|
1285
|
+
if (!run.ok) {
|
|
1286
|
+
// A .cmd we cannot start directly, and a prompt cmd.exe would read as syntax: say so
|
|
1287
|
+
// rather than run it through cmd.exe anyway.
|
|
1288
|
+
this.record({ kind: "pending", session, reason: run.reason }, session, batch);
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1285
1291
|
// The session's own directory when we know it: `cwd` is only where the
|
|
1286
1292
|
// attach process ran, and `codex exec` refuses to start outside a trusted
|
|
1287
1293
|
// directory at all.
|
|
1288
|
-
const { exitCode, stderr } = await this.spawnHeadless(
|
|
1294
|
+
const { exitCode, stderr } = await this.spawnHeadless(run.file, run.args, {
|
|
1289
1295
|
cwd: resolved.resumeCwd ?? resolved.cwd,
|
|
1296
|
+
...(run.verbatim ? { windowsVerbatimArguments: true } : {}),
|
|
1290
1297
|
});
|
|
1291
1298
|
if (exitCode !== 0) {
|
|
1292
1299
|
// The binary ran and the turn still failed, so what we resolved may be
|
package/dist/runtime-binary.js
CHANGED
|
@@ -36,10 +36,12 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
36
36
|
exports.resolveRuntimeBinary = resolveRuntimeBinary;
|
|
37
37
|
exports.describeResolution = describeResolution;
|
|
38
38
|
exports.spawnPlanFor = spawnPlanFor;
|
|
39
|
+
exports.planArgv = planArgv;
|
|
39
40
|
exports.probeBinaryVersion = probeBinaryVersion;
|
|
40
41
|
exports.summarizeProbe = summarizeProbe;
|
|
41
42
|
exports.currentBinaryEnv = currentBinaryEnv;
|
|
42
43
|
exports.summarizeResolutionFailure = summarizeResolutionFailure;
|
|
44
|
+
const windows_cmd_1 = require("./windows-cmd");
|
|
43
45
|
const child_process_1 = require("child_process");
|
|
44
46
|
const fs = __importStar(require("fs"));
|
|
45
47
|
const spawn_env_1 = require("./relay/spawn-env");
|
|
@@ -190,9 +192,9 @@ function joinPath(dir, file, platform) {
|
|
|
190
192
|
}
|
|
191
193
|
/**
|
|
192
194
|
* @param candidate an executable path already resolved by `resolveRuntimeBinary`
|
|
193
|
-
* @returns
|
|
195
|
+
* @returns what to spawn; turn it into argv with `planArgv`, always `shell: false`
|
|
194
196
|
*/
|
|
195
|
-
function spawnPlanFor(candidate, platform) {
|
|
197
|
+
function spawnPlanFor(candidate, platform, io = windows_cmd_1.realShimIo) {
|
|
196
198
|
// A SCRIPT NEEDS ITS INTERPRETER, on every platform.
|
|
197
199
|
//
|
|
198
200
|
// The managed Codex fallback records `<CODEX_MANAGED_PACKAGE_ROOT>/bin/codex.js`
|
|
@@ -207,10 +209,31 @@ function spawnPlanFor(candidate, platform) {
|
|
|
207
209
|
// `runtimeBin` is recorded by the session in the first place.
|
|
208
210
|
if (/\.(mjs|cjs|js)$/i.test(candidate))
|
|
209
211
|
return { file: process.execPath, prefixArgs: [candidate] };
|
|
210
|
-
|
|
211
|
-
if (!needsInterpreter)
|
|
212
|
+
if (platform !== "win32" || !/\.(cmd|bat)$/i.test(candidate))
|
|
212
213
|
return { file: candidate, prefixArgs: [] };
|
|
213
|
-
|
|
214
|
+
const target = (0, windows_cmd_1.npmShimTarget)(candidate, io);
|
|
215
|
+
if (target?.kind === "exe")
|
|
216
|
+
return { file: target.path, prefixArgs: [] };
|
|
217
|
+
if (target?.kind === "node")
|
|
218
|
+
return { file: target.node ?? process.execPath, prefixArgs: [target.script] };
|
|
219
|
+
return { file: candidate, prefixArgs: [], viaCmd: true };
|
|
220
|
+
}
|
|
221
|
+
/**
|
|
222
|
+
* The argv for a plan. For a `.cmd` run through cmd.exe, one quoted line passed verbatim — and a
|
|
223
|
+
* refusal, never a guess, when an argument holds `"`, `%` or a line break, which cmd.exe would read
|
|
224
|
+
* as syntax inside quotes too.
|
|
225
|
+
*/
|
|
226
|
+
function planArgv(plan, args) {
|
|
227
|
+
if (!plan.viaCmd)
|
|
228
|
+
return { ok: true, file: plan.file, args: [...plan.prefixArgs, ...args], verbatim: false };
|
|
229
|
+
if ([plan.file, ...args].some((a) => windows_cmd_1.WINDOWS_UNSAFE.test(a))) {
|
|
230
|
+
return {
|
|
231
|
+
ok: false,
|
|
232
|
+
reason: `${plan.file} is a .cmd BayChat cannot start directly, and an argument holds " or % or a line break, which cannot be passed safely through cmd.exe`,
|
|
233
|
+
};
|
|
234
|
+
}
|
|
235
|
+
const line = (0, windows_cmd_1.cmdExeSpawn)((0, windows_cmd_1.quoteForCmd)(plan.file), args.map(windows_cmd_1.quoteForCmd));
|
|
236
|
+
return { ok: true, file: line.command, args: line.args, verbatim: true };
|
|
214
237
|
}
|
|
215
238
|
/** How long a `--version` probe may run before it is treated as broken. */
|
|
216
239
|
const PROBE_TIMEOUT_MS = 5_000;
|
|
@@ -220,9 +243,11 @@ const PROBE_TIMEOUT_MS = 5_000;
|
|
|
220
243
|
* probe: no shell, the environment the agent will get, `summarizeProbe` decides.
|
|
221
244
|
*/
|
|
222
245
|
function probeBinaryVersion(binaryPath, env) {
|
|
223
|
-
const
|
|
246
|
+
const run = planArgv(spawnPlanFor(binaryPath, process.platform), ["--version"]);
|
|
247
|
+
if (!run.ok)
|
|
248
|
+
return Promise.resolve(summarizeProbe({ status: null, stdout: "", stderr: "", error: new Error(run.reason) }));
|
|
224
249
|
return new Promise((resolve) => {
|
|
225
|
-
(0, child_process_1.execFile)(
|
|
250
|
+
(0, child_process_1.execFile)(run.file, run.args, { timeout: PROBE_TIMEOUT_MS, windowsHide: true, shell: false, windowsVerbatimArguments: run.verbatim, env, encoding: "utf8" }, (err, stdout, stderr) => {
|
|
226
251
|
const status = err ? (typeof err.code === "number" ? err.code : null) : 0;
|
|
227
252
|
// A non-zero exit is still a run: only a failure to run at all is an `error`.
|
|
228
253
|
const failedToRun = err && typeof err.code !== "number" ? err : undefined;
|
|
@@ -304,12 +329,15 @@ function currentBinaryEnv(override, env = process.env, execPath = process.execPa
|
|
|
304
329
|
}
|
|
305
330
|
},
|
|
306
331
|
probe(candidate) {
|
|
307
|
-
const
|
|
308
|
-
|
|
332
|
+
const planned = planArgv(spawnPlanFor(candidate, process.platform), ["--version"]);
|
|
333
|
+
if (!planned.ok)
|
|
334
|
+
return summarizeProbe({ status: null, stdout: "", stderr: "", error: new Error(planned.reason) });
|
|
335
|
+
const run = (0, child_process_1.spawnSync)(planned.file, planned.args, {
|
|
309
336
|
windowsHide: true,
|
|
310
337
|
timeout: PROBE_TIMEOUT_MS,
|
|
311
338
|
encoding: "utf8",
|
|
312
339
|
shell: false,
|
|
340
|
+
windowsVerbatimArguments: planned.verbatim,
|
|
313
341
|
// THE PROBE MUST BE ABLE TO START WHAT IT IS PROBING.
|
|
314
342
|
//
|
|
315
343
|
// An npm runtime is a `#!/usr/bin/env node` script, and a spawn inherits
|
package/dist/self-upgrade.js
CHANGED
|
@@ -50,7 +50,7 @@ exports.ownVersion = ownVersion;
|
|
|
50
50
|
const child_process_1 = require("child_process");
|
|
51
51
|
const fs = __importStar(require("fs"));
|
|
52
52
|
const path = __importStar(require("path"));
|
|
53
|
-
const
|
|
53
|
+
const windows_cmd_1 = require("./windows-cmd");
|
|
54
54
|
/** A path as the filesystem sees it: every symlink on the way resolved. */
|
|
55
55
|
function resolveEntry(file) {
|
|
56
56
|
try {
|
|
@@ -275,23 +275,25 @@ function npmCommand(execPath, platform, pathEnv, exists) {
|
|
|
275
275
|
return { command: beside, path: pathEnv ? `${dir}${sep}${pathEnv}` : dir };
|
|
276
276
|
}
|
|
277
277
|
/** Run npm for real, without blocking the event loop (the relay keeps serving while npm runs).
|
|
278
|
-
* Windows' npm is a `.cmd` shim, which Node will only start through
|
|
279
|
-
* is first checked against what cmd.exe could misread and then quoted. */
|
|
278
|
+
* Windows' npm is a `.cmd` shim, which Node will only start through cmd.exe (`cmdExeSpawn`), so
|
|
279
|
+
* every argument is first checked against what cmd.exe could misread and then quoted. */
|
|
280
280
|
function realNpm(args, timeoutMs) {
|
|
281
281
|
const windows = process.platform === "win32";
|
|
282
|
-
if (windows && args.some((a) =>
|
|
282
|
+
if (windows && args.some((a) => windows_cmd_1.WINDOWS_UNSAFE.test(a))) {
|
|
283
283
|
return Promise.resolve({ status: null, stdout: "", stderr: "", error: new Error("a path npm needs cannot be passed safely through cmd.exe") });
|
|
284
284
|
}
|
|
285
285
|
const found = npmCommand(process.execPath, process.platform, process.env.PATH, fs.existsSync);
|
|
286
286
|
// A Node directory cmd.exe could misread is not run through it: PATH's npm then, as before.
|
|
287
|
-
const npm = windows &&
|
|
287
|
+
const npm = windows && windows_cmd_1.WINDOWS_UNSAFE.test(found.command) ? { command: "npm", path: process.env.PATH } : found;
|
|
288
288
|
// Through cmd.exe the command is a string too: a path with spaces must arrive as one word.
|
|
289
|
-
const
|
|
289
|
+
const run = windows
|
|
290
|
+
? (0, windows_cmd_1.cmdExeSpawn)(npm.command !== "npm" ? (0, windows_cmd_1.quoteForCmd)(npm.command) : npm.command, args.map(windows_cmd_1.quoteForCmd))
|
|
291
|
+
: { command: npm.command, args };
|
|
290
292
|
return new Promise((resolve) => {
|
|
291
|
-
(0, child_process_1.execFile)(command,
|
|
293
|
+
(0, child_process_1.execFile)(run.command, run.args, {
|
|
292
294
|
encoding: "utf8",
|
|
293
295
|
timeout: timeoutMs,
|
|
294
|
-
|
|
296
|
+
windowsVerbatimArguments: windows,
|
|
295
297
|
windowsHide: true,
|
|
296
298
|
maxBuffer: 16 * 1024 * 1024,
|
|
297
299
|
env: { ...process.env, ...(npm.path !== undefined ? { PATH: npm.path } : {}) },
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Starting a Windows `.cmd` shim (npm, claude) safely. Node will not spawn a `.cmd` directly
|
|
4
|
+
* (CVE-2024-27980), and `shell: true` with an argument list is deprecated (DEP0190), so the shim
|
|
5
|
+
* runs through cmd.exe, named explicitly, with every argument checked and quoted here first.
|
|
6
|
+
* Its own module because commands.ts, bay-mcp.ts and self-upgrade.ts all need it, and
|
|
7
|
+
* commands.ts already imports bay-mcp.ts.
|
|
8
|
+
*/
|
|
9
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
12
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
13
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
14
|
+
}
|
|
15
|
+
Object.defineProperty(o, k2, desc);
|
|
16
|
+
}) : (function(o, m, k, k2) {
|
|
17
|
+
if (k2 === undefined) k2 = k;
|
|
18
|
+
o[k2] = m[k];
|
|
19
|
+
}));
|
|
20
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
21
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
22
|
+
}) : function(o, v) {
|
|
23
|
+
o["default"] = v;
|
|
24
|
+
});
|
|
25
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
26
|
+
var ownKeys = function(o) {
|
|
27
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
28
|
+
var ar = [];
|
|
29
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
30
|
+
return ar;
|
|
31
|
+
};
|
|
32
|
+
return ownKeys(o);
|
|
33
|
+
};
|
|
34
|
+
return function (mod) {
|
|
35
|
+
if (mod && mod.__esModule) return mod;
|
|
36
|
+
var result = {};
|
|
37
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
38
|
+
__setModuleDefault(result, mod);
|
|
39
|
+
return result;
|
|
40
|
+
};
|
|
41
|
+
})();
|
|
42
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
43
|
+
exports.realShimIo = exports.WINDOWS_UNSAFE = void 0;
|
|
44
|
+
exports.quoteForCmd = quoteForCmd;
|
|
45
|
+
exports.cmdExeSpawn = cmdExeSpawn;
|
|
46
|
+
exports.cmdExePath = cmdExePath;
|
|
47
|
+
exports.npmShimTarget = npmShimTarget;
|
|
48
|
+
const fs = __importStar(require("fs"));
|
|
49
|
+
const path = __importStar(require("path"));
|
|
50
|
+
/**
|
|
51
|
+
* cmd.exe quoting: wrap the whole argument so spaces, `&`, `|` and `>` inside it
|
|
52
|
+
* stay literal. Only usable on values `WINDOWS_UNSAFE` has already cleared.
|
|
53
|
+
*
|
|
54
|
+
* Backslashes at the very end are doubled: the program behind the shim splits its
|
|
55
|
+
* command line by the C runtime's rules, where `\"` is an escaped quote, so
|
|
56
|
+
* `"C:\proj\"` would swallow the closing quote and merge with the next argument.
|
|
57
|
+
* Doubled, they read back as the backslashes they were.
|
|
58
|
+
*/
|
|
59
|
+
function quoteForCmd(arg) {
|
|
60
|
+
return `"${arg.replace(/(\\+)$/, "$1$1")}"`;
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Characters that survive — or break out of — cmd.exe double quotes.
|
|
64
|
+
*
|
|
65
|
+
* A `"` ends the wrapper, so everything after it is parsed as shell syntax. A
|
|
66
|
+
* `%NAME%` is expanded *inside* quotes. A newline ends the command line. Nothing
|
|
67
|
+
* else in cmd's metacharacter set (`&`, `|`, `<`, `>`, `^`) is interpreted while
|
|
68
|
+
* quoted, and `!` only expands under delayed expansion, which `cmdExeSpawn`
|
|
69
|
+
* switches off with `/v:off` (the registry can switch it on for every cmd.exe).
|
|
70
|
+
*/
|
|
71
|
+
exports.WINDOWS_UNSAFE = /["%\u0000-\u001f\u007f]/;
|
|
72
|
+
/**
|
|
73
|
+
* Run a `.cmd` shim through cmd.exe, invoked EXPLICITLY, so the spawn itself needs no `shell: true`.
|
|
74
|
+
*
|
|
75
|
+
* `shell: true` with an argument list is deprecated (DEP0190) because Node only joins the list with
|
|
76
|
+
* spaces — newer Node may refuse it outright. This builds the very line Node built for it,
|
|
77
|
+
* `cmd.exe /d /s /c "<command> <args>"`, so cmd.exe sees the same line as before, plus `/v:off`:
|
|
78
|
+
* `/d` skips AutoRun, `/v:off` keeps `!VAR!` literal, `/s` strips exactly the outer quotes, `/c`
|
|
79
|
+
* runs and exits. Spawn it with `shell: false`
|
|
80
|
+
* and `windowsVerbatimArguments: true`, or Node would quote the line a second time.
|
|
81
|
+
*
|
|
82
|
+
* The caller still owns the quoting: `command` and every arg must already be safe for cmd.exe
|
|
83
|
+
* (`WINDOWS_UNSAFE` cleared, then `quoteForCmd`) — this only removes the deprecated route.
|
|
84
|
+
*/
|
|
85
|
+
function cmdExeSpawn(command, quotedArgs, env = process.env) {
|
|
86
|
+
return { command: cmdExePath(env), args: ["/d", "/v:off", "/s", "/c", `"${[command, ...quotedArgs].join(" ")}"`] };
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* cmd.exe by its full path, as Node's own `shell: true` took it from ComSpec. A bare `cmd.exe` is
|
|
90
|
+
* looked up in the current folder FIRST, so `baychat claude` run inside a cloned repo that ships a
|
|
91
|
+
* `cmd.exe` would start that file.
|
|
92
|
+
*/
|
|
93
|
+
function cmdExePath(env = process.env) {
|
|
94
|
+
return env.ComSpec ?? env.COMSPEC ?? path.win32.join(env.SystemRoot ?? env.SYSTEMROOT ?? "C:\\Windows", "System32", "cmd.exe");
|
|
95
|
+
}
|
|
96
|
+
exports.realShimIo = {
|
|
97
|
+
read: (file) => {
|
|
98
|
+
try {
|
|
99
|
+
return fs.readFileSync(file, "utf8");
|
|
100
|
+
}
|
|
101
|
+
catch {
|
|
102
|
+
// Unreadable is "not a shim we understand": the caller falls back to cmd.exe.
|
|
103
|
+
return undefined;
|
|
104
|
+
}
|
|
105
|
+
},
|
|
106
|
+
exists: (file) => fs.existsSync(file),
|
|
107
|
+
};
|
|
108
|
+
/** A shim is a few hundred bytes; anything this big is not one npm wrote. */
|
|
109
|
+
const SHIM_MAX_BYTES = 16 * 1024;
|
|
110
|
+
/** `%dp0%\` (cmd-shim today) or `%~dp0\` (older npm): the shim's own folder. */
|
|
111
|
+
const DP0 = /^(?:%dp0%|%~dp0)\\/i;
|
|
112
|
+
/**
|
|
113
|
+
* The program an npm `.cmd` shim runs, so it can be started directly — no cmd.exe at all, and so
|
|
114
|
+
* nothing in the arguments (a wake prompt with `"`, `%`, `&`) is ever read as cmd syntax.
|
|
115
|
+
*
|
|
116
|
+
* npm's shims end in one line that runs one file from the shim's folder with `%*`. Exactly one
|
|
117
|
+
* such file, present on disk, of a kind we know (`.exe`, or a script run by node) — anything else
|
|
118
|
+
* is undefined, and the caller uses cmd.exe with its stricter rules.
|
|
119
|
+
*/
|
|
120
|
+
function npmShimTarget(shim, io = exports.realShimIo) {
|
|
121
|
+
const text = io.read(shim);
|
|
122
|
+
if (!text || text.length > SHIM_MAX_BYTES)
|
|
123
|
+
return undefined;
|
|
124
|
+
const line = text.split(/\r?\n/).reverse().find((l) => l.includes("%*"));
|
|
125
|
+
if (!line)
|
|
126
|
+
return undefined;
|
|
127
|
+
const dir = path.win32.dirname(shim);
|
|
128
|
+
const local = [...line.matchAll(/"([^"]*)"/g)]
|
|
129
|
+
.map((m) => m[1])
|
|
130
|
+
.filter((t) => DP0.test(t))
|
|
131
|
+
.map((t) => path.win32.join(dir, t.replace(DP0, "")));
|
|
132
|
+
const scripts = local.filter((f) => /\.[cm]?js$/i.test(f));
|
|
133
|
+
if (scripts.length === 1) {
|
|
134
|
+
if (!io.exists(scripts[0]))
|
|
135
|
+
return undefined;
|
|
136
|
+
const node = path.win32.join(dir, "node.exe");
|
|
137
|
+
return { kind: "node", script: scripts[0], ...(io.exists(node) ? { node } : {}) };
|
|
138
|
+
}
|
|
139
|
+
if (local.length === 1 && /\.exe$/i.test(local[0]) && io.exists(local[0]))
|
|
140
|
+
return { kind: "exe", path: local[0] };
|
|
141
|
+
return undefined;
|
|
142
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.33.
|
|
3
|
+
"version": "0.33.3",
|
|
4
4
|
"description": "BayChat connector CLI — pair an agent session (Claude Code, Codex, Cursor) with BayChat, or have the relay run DeepSeek or the Cursor CLI for you",
|
|
5
5
|
"bin": {
|
|
6
6
|
"baychat": "dist/index.js"
|