baychat 0.19.0 → 0.20.1
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 +40 -1
- package/dist/commands.js +7 -5
- package/dist/connect-claude.js +34 -0
- package/dist/connect-hermes.js +20 -0
- package/dist/connect.js +10 -26
- package/dist/hermes-mcp.js +58 -0
- package/dist/hermes.js +19 -8
- package/dist/index.js +5 -0
- package/dist/relay/adapters.js +26 -7
- package/dist/relay/autostart.js +74 -15
- package/dist/relay/codex-app-server.js +36 -7
- package/dist/relay/codex-queue.js +42 -4
- package/dist/relay/commands.js +59 -11
- package/dist/relay/daemon.js +116 -17
- package/dist/relay/held.js +32 -5
- package/dist/relay/message-format.js +7 -4
- package/dist/runtime-binary.js +26 -6
- package/dist/runtimes.js +62 -43
- package/dist/session-command.js +137 -0
- package/package.json +2 -1
package/README.md
CHANGED
|
@@ -30,10 +30,49 @@ Coding sessions show idle after five minutes without use and expire after
|
|
|
30
30
|
24 hours. `end_session` closes one immediately. History survives and the same
|
|
31
31
|
name can rejoin. Persistent agents such as Hermes keep their own lifecycle.
|
|
32
32
|
|
|
33
|
-
This workflow requires CLI 0.
|
|
33
|
+
This workflow requires CLI 0.20.1 and the existing session-chat API deployment. Update npm,
|
|
34
34
|
rerun `baychat connect <runtime>` to refresh its skill, and restart running
|
|
35
35
|
relays and MCP clients. Updating npm alone does not deploy the remote MCP.
|
|
36
36
|
|
|
37
|
+
## What the package and MCP each do
|
|
38
|
+
|
|
39
|
+
The npm package contains the CLI, installed runtime commands, local relay,
|
|
40
|
+
local stdio MCP server and bundled Hermes plugin. The remote MCP is the BayChat
|
|
41
|
+
API running on the server. It owns sessions, room membership and chat tools.
|
|
42
|
+
Updating npm updates this computer; deploying the API updates remote MCP.
|
|
43
|
+
|
|
44
|
+
The installed session command runs one foreground command:
|
|
45
|
+
|
|
46
|
+
```sh
|
|
47
|
+
baychat join Atlas "Coding" --runtime codex
|
|
48
|
+
baychat join --group "Coding" --runtime codex
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
It joins through remote MCP, uses the server-confirmed identity, checks the relay
|
|
52
|
+
and connects incoming messages. Claude runs this command with its persistent
|
|
53
|
+
Monitor and re-arms automatically after a wake. Codex verifies native queue
|
|
54
|
+
support and returns as soon as the relay confirms registration. It needs no
|
|
55
|
+
waiting terminal or manual re-arming. Failed delivery setup is reported as
|
|
56
|
+
incomplete, even when the room join succeeded.
|
|
57
|
+
|
|
58
|
+
The Windows relay runs without a visible console, including after login and
|
|
59
|
+
network reconnects. Incoming messages appear as `@Sender` in the coding task.
|
|
60
|
+
The skill echoes BayChat chat replies locally; raw tool output stays local.
|
|
61
|
+
Codex can queue messages behind an active turn, so an accepted message is not
|
|
62
|
+
proof of an immediate reply.
|
|
63
|
+
|
|
64
|
+
For a persistent Hermes installation, run this on the Hermes machine:
|
|
65
|
+
|
|
66
|
+
```sh
|
|
67
|
+
baychat connect hermes
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
Approve the QR once and choose its agent on the phone. The command installs the
|
|
71
|
+
bundled plugin, configures remote MCP tools, and enables its gateway. Existing
|
|
72
|
+
Hermes settings and other MCP servers are preserved, with a backup before changes.
|
|
73
|
+
The MCP header references the credential in .env. An existing agent credential is reused;
|
|
74
|
+
a failed enable or restart returns an error. Hermes must already be installed.
|
|
75
|
+
|
|
37
76
|
## Pair a persistent agent
|
|
38
77
|
|
|
39
78
|
Connect an AI agent session — Claude Code, Codex, or any CLI with a shell — to
|
package/dist/commands.js
CHANGED
|
@@ -19,6 +19,7 @@ exports.cmdCheck = cmdCheck;
|
|
|
19
19
|
exports.cmdWatch = cmdWatch;
|
|
20
20
|
exports.cmdLink = cmdLink;
|
|
21
21
|
exports.claudeMcpAddSpawn = claudeMcpAddSpawn;
|
|
22
|
+
exports.registerWithClaude = registerWithClaude;
|
|
22
23
|
exports.cmdLogin = cmdLogin;
|
|
23
24
|
exports.deviceExpiryWarning = deviceExpiryWarning;
|
|
24
25
|
exports.printDeviceExpiryWarning = printDeviceExpiryWarning;
|
|
@@ -688,7 +689,7 @@ function registerWithClaude(baseUrl, token) {
|
|
|
688
689
|
// Windows only, and only for a value a quoted cmd.exe argument cannot hold.
|
|
689
690
|
console.log("\nCould not add BayChat to Claude Code safely on Windows — add it manually:");
|
|
690
691
|
printManualMcpAdd(baseUrl);
|
|
691
|
-
return;
|
|
692
|
+
return false;
|
|
692
693
|
}
|
|
693
694
|
// stderr is captured (not ignored) so a failure can quote claude's own words;
|
|
694
695
|
// the timeout keeps a hung binary from hanging a login whose credential is
|
|
@@ -725,19 +726,19 @@ function registerWithClaude(baseUrl, token) {
|
|
|
725
726
|
if (readded.status === 0) {
|
|
726
727
|
console.log("✓ BayChat re-registered with Claude Code (credential renewed)");
|
|
727
728
|
installClaudeSkill();
|
|
728
|
-
return;
|
|
729
|
+
return true;
|
|
729
730
|
}
|
|
730
731
|
}
|
|
731
732
|
}
|
|
732
733
|
if (recipe.shell && res.status === 9009) {
|
|
733
734
|
console.log("\nClaude Code CLI not found — add BayChat manually:");
|
|
734
735
|
printManualMcpAdd(baseUrl);
|
|
735
|
-
return;
|
|
736
|
+
return false;
|
|
736
737
|
}
|
|
737
738
|
if (res.error && res.error.code === "ENOENT") {
|
|
738
739
|
console.log("\nClaude Code CLI not found — add BayChat manually:");
|
|
739
740
|
printManualMcpAdd(baseUrl);
|
|
740
|
-
return;
|
|
741
|
+
return false;
|
|
741
742
|
}
|
|
742
743
|
if (res.error || res.status !== 0) {
|
|
743
744
|
const reason = res.error?.code ??
|
|
@@ -758,10 +759,11 @@ function registerWithClaude(baseUrl, token) {
|
|
|
758
759
|
// because of it: a session with the rules and no tools still knows how to
|
|
759
760
|
// ask for them, while one with neither knows nothing at all.
|
|
760
761
|
installClaudeSkill();
|
|
761
|
-
return;
|
|
762
|
+
return false;
|
|
762
763
|
}
|
|
763
764
|
console.log("✓ BayChat added to Claude Code");
|
|
764
765
|
installClaudeSkill();
|
|
766
|
+
return true;
|
|
765
767
|
}
|
|
766
768
|
/**
|
|
767
769
|
* Install Claude Code's BayChat skill, the other half of registering the server.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cmdConnectClaude = cmdConnectClaude;
|
|
4
|
+
const commands_1 = require("./commands");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const commands_2 = require("./relay/commands");
|
|
7
|
+
const runtime_install_1 = require("./runtime-install");
|
|
8
|
+
/** One-time Claude setup reuses a valid login, registers MCP, and refreshes its skill. */
|
|
9
|
+
async function cmdConnectClaude(opts = {}) {
|
|
10
|
+
try {
|
|
11
|
+
const existing = (0, config_1.loadDeviceCredentials)();
|
|
12
|
+
const base = (opts.base || process.env.BAYCHAT_API_URL || existing?.baseUrl || config_1.DEFAULT_API_URL).replace(/\/$/, "");
|
|
13
|
+
const expires = existing ? Date.parse(existing.expiresAt) : NaN;
|
|
14
|
+
if (!existing || existing.baseUrl.replace(/\/$/, "") !== base || !Number.isFinite(expires) || expires <= Date.now()) {
|
|
15
|
+
if (!await (0, commands_1.cmdLogin)({ base, registerClaude: false, hint: false }))
|
|
16
|
+
return 2;
|
|
17
|
+
}
|
|
18
|
+
const device = (0, config_1.loadDeviceCredentials)();
|
|
19
|
+
if (!device)
|
|
20
|
+
throw new Error("No device login was saved. Retry baychat connect claude.");
|
|
21
|
+
const registered = (0, commands_1.registerWithClaude)(device.baseUrl, device.token);
|
|
22
|
+
for (const line of (0, runtime_install_1.describeInstall)((0, runtime_install_1.installRuntimeCommand)("claude")))
|
|
23
|
+
console.log(" " + line);
|
|
24
|
+
if (!registered)
|
|
25
|
+
return 1;
|
|
26
|
+
console.log(" " + await (0, commands_2.ensureRelayInstalled)());
|
|
27
|
+
console.log(' Restart Claude Code once, then use /baychat <name> "<group>".');
|
|
28
|
+
return 0;
|
|
29
|
+
}
|
|
30
|
+
catch (err) {
|
|
31
|
+
console.error("Could not connect Claude Code: " + (err instanceof Error ? err.message : String(err)));
|
|
32
|
+
return 1;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.cmdConnectHermes = cmdConnectHermes;
|
|
4
|
+
const commands_1 = require("./commands");
|
|
5
|
+
const config_1 = require("./config");
|
|
6
|
+
const hermes_1 = require("./hermes");
|
|
7
|
+
/** Hermes owns a persistent agent. The owner chooses its name and Bay on the phone. */
|
|
8
|
+
async function cmdConnectHermes(opts = {}) {
|
|
9
|
+
const existing = (0, config_1.loadCredentials)();
|
|
10
|
+
if (!existing || (opts.base && opts.base.replace(/\/$/, "") !== existing.baseUrl.replace(/\/$/, ""))) {
|
|
11
|
+
console.log("Connect Hermes — approve the QR and choose its agent in BayChat.");
|
|
12
|
+
if (!await (0, commands_1.cmdLink)(opts))
|
|
13
|
+
return 2;
|
|
14
|
+
}
|
|
15
|
+
if (!(0, config_1.loadCredentials)()) {
|
|
16
|
+
console.error("No paired agent credential was saved. Retry baychat connect hermes.");
|
|
17
|
+
return 1;
|
|
18
|
+
}
|
|
19
|
+
return (0, hermes_1.cmdHermesInit)(["--enable"]);
|
|
20
|
+
}
|
package/dist/connect.js
CHANGED
|
@@ -31,11 +31,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
31
31
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
32
32
|
};
|
|
33
33
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
34
|
+
exports.cmdConnectClaude = void 0;
|
|
34
35
|
exports.renderConnectMenu = renderConnectMenu;
|
|
35
36
|
exports.isClaudeConnectTarget = isClaudeConnectTarget;
|
|
36
37
|
exports.parseConnectClient = parseConnectClient;
|
|
37
38
|
exports.writeClientConfig = writeClientConfig;
|
|
38
|
-
exports.cmdConnectClaude = cmdConnectClaude;
|
|
39
39
|
exports.cmdConnect = cmdConnect;
|
|
40
40
|
const node_fs_1 = __importDefault(require("node:fs"));
|
|
41
41
|
const node_path_1 = __importDefault(require("node:path"));
|
|
@@ -49,6 +49,10 @@ const commands_2 = require("./relay/commands");
|
|
|
49
49
|
const runtime_install_1 = require("./runtime-install");
|
|
50
50
|
const runtimes_1 = require("./runtimes");
|
|
51
51
|
const runtime_binary_1 = require("./runtime-binary");
|
|
52
|
+
const connect_claude_1 = require("./connect-claude");
|
|
53
|
+
var connect_claude_2 = require("./connect-claude");
|
|
54
|
+
Object.defineProperty(exports, "cmdConnectClaude", { enumerable: true, get: function () { return connect_claude_2.cmdConnectClaude; } });
|
|
55
|
+
const connect_hermes_1 = require("./connect-hermes");
|
|
52
56
|
/** Clients `connect` can configure. `claude` is an alias users reach for. */
|
|
53
57
|
const CLIENT_ALIASES = {
|
|
54
58
|
codex: "codex",
|
|
@@ -63,7 +67,8 @@ function renderConnectMenu() {
|
|
|
63
67
|
"Connect this laptop to BayChat, once, and configure your client:",
|
|
64
68
|
"",
|
|
65
69
|
...rows,
|
|
66
|
-
" npx baychat connect claude → Claude Code (
|
|
70
|
+
" npx baychat connect claude → Claude Code (login, MCP and skill)",
|
|
71
|
+
" npx baychat connect hermes → Hermes (pair and enable its persistent agent)",
|
|
67
72
|
"",
|
|
68
73
|
"You approve a QR on your phone. After that, in any coding session:",
|
|
69
74
|
"",
|
|
@@ -179,29 +184,6 @@ const realIo = {
|
|
|
179
184
|
*
|
|
180
185
|
* @returns 0 on success, 2 when the laptop login expired without approval.
|
|
181
186
|
*/
|
|
182
|
-
/**
|
|
183
|
-
* `baychat connect claude` — refresh Claude Code's skill.
|
|
184
|
-
*
|
|
185
|
-
* Deliberately does NOT touch the credential or the MCP registration. Both are
|
|
186
|
-
* `login`'s job, and re-doing them would put a device-pairing QR in front of
|
|
187
|
-
* somebody whose login is fine. Writing the skill needs no auth at all: it is
|
|
188
|
-
* generated locally from this CLI's own version, which is exactly why it goes
|
|
189
|
-
* stale on upgrade and exactly why this command has to exist.
|
|
190
|
-
*/
|
|
191
|
-
function cmdConnectClaude() {
|
|
192
|
-
try {
|
|
193
|
-
for (const line of (0, runtime_install_1.describeInstall)((0, runtime_install_1.installRuntimeCommand)("claude")))
|
|
194
|
-
console.log(` ${line}`);
|
|
195
|
-
console.log("");
|
|
196
|
-
console.log(" Restart Claude Code once so it picks the skill up.");
|
|
197
|
-
console.log(" Not logged in yet? `npx baychat login` — that registers the MCP server too.");
|
|
198
|
-
return 0;
|
|
199
|
-
}
|
|
200
|
-
catch (err) {
|
|
201
|
-
console.log(`Could not install the Claude Code skill: ${err instanceof Error ? err.message : String(err)}`);
|
|
202
|
-
return 1;
|
|
203
|
-
}
|
|
204
|
-
}
|
|
205
187
|
async function cmdConnect(clientArg, opts = {}) {
|
|
206
188
|
if (clientArg === undefined) {
|
|
207
189
|
console.log(renderConnectMenu());
|
|
@@ -210,7 +192,9 @@ async function cmdConnect(clientArg, opts = {}) {
|
|
|
210
192
|
// Claude Code first: it is not an MCP-config client, so it must not reach
|
|
211
193
|
// `parseConnectClient`, which would reject it.
|
|
212
194
|
if (isClaudeConnectTarget(clientArg))
|
|
213
|
-
return cmdConnectClaude();
|
|
195
|
+
return (0, connect_claude_1.cmdConnectClaude)(opts);
|
|
196
|
+
if (clientArg.trim().toLowerCase() === "hermes")
|
|
197
|
+
return (0, connect_hermes_1.cmdConnectHermes)(opts);
|
|
214
198
|
const client = parseConnectClient(clientArg);
|
|
215
199
|
const base = (opts.base || process.env.BAYCHAT_API_URL || config_1.DEFAULT_API_URL).replace(/\/$/, "");
|
|
216
200
|
const steps = (0, connect_plan_1.planConnect)({
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.writeHermesMcp = writeHermesMcp;
|
|
7
|
+
const node_fs_1 = __importDefault(require("node:fs"));
|
|
8
|
+
const node_path_1 = __importDefault(require("node:path"));
|
|
9
|
+
const node_crypto_1 = require("node:crypto");
|
|
10
|
+
const yaml_1 = require("yaml");
|
|
11
|
+
/** Preserve other Hermes settings, comments, tools policies and MCP servers.
|
|
12
|
+
* The credential stays in .env; config only references BAYCHAT_TOKEN.
|
|
13
|
+
*/
|
|
14
|
+
function writeHermesMcp(home, baseUrl) {
|
|
15
|
+
const written = node_path_1.default.join(home, "config.yaml");
|
|
16
|
+
const existing = node_fs_1.default.existsSync(written) ? node_fs_1.default.readFileSync(written, "utf8") : "";
|
|
17
|
+
const doc = (0, yaml_1.parseDocument)(existing);
|
|
18
|
+
// Never quote parser errors: they can contain lines holding another API key.
|
|
19
|
+
if (doc.errors.length || (doc.contents !== null && !(0, yaml_1.isMap)(doc.contents))) {
|
|
20
|
+
throw new Error("Hermes config.yaml is not a valid YAML mapping. Fix it before connecting.");
|
|
21
|
+
}
|
|
22
|
+
if (doc.contents === null)
|
|
23
|
+
doc.contents = doc.createNode({});
|
|
24
|
+
for (const keys of [["mcp_servers"], ["mcp_servers", "baychat"], ["mcp_servers", "baychat", "headers"]]) {
|
|
25
|
+
if (!doc.hasIn(keys))
|
|
26
|
+
doc.setIn(keys, doc.createNode({}));
|
|
27
|
+
if (!(0, yaml_1.isMap)(doc.getIn(keys, true))) {
|
|
28
|
+
throw new Error("Hermes mcp_servers.baychat configuration must use YAML mappings. Existing config was preserved.");
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
const root = ["mcp_servers", "baychat"];
|
|
32
|
+
// A former stdio/SSE entry cannot coexist with the remote HTTP transport.
|
|
33
|
+
for (const key of ["command", "args", "transport"])
|
|
34
|
+
doc.deleteIn([...root, key]);
|
|
35
|
+
doc.setIn([...root, "url"], new URL("/api/mcp", baseUrl).href);
|
|
36
|
+
doc.setIn([...root, "headers", "Authorization"], "Bearer ${BAYCHAT_TOKEN}");
|
|
37
|
+
doc.setIn([...root, "enabled"], true);
|
|
38
|
+
const next = doc.toString();
|
|
39
|
+
if (next === existing)
|
|
40
|
+
return { written };
|
|
41
|
+
node_fs_1.default.mkdirSync(home, { recursive: true });
|
|
42
|
+
const suffix = (0, node_crypto_1.randomUUID)();
|
|
43
|
+
const backup = existing ? written + ".baychat-backup-" + suffix : undefined;
|
|
44
|
+
if (backup) {
|
|
45
|
+
node_fs_1.default.copyFileSync(written, backup, node_fs_1.default.constants.COPYFILE_EXCL);
|
|
46
|
+
node_fs_1.default.chmodSync(backup, 0o600);
|
|
47
|
+
}
|
|
48
|
+
const temporary = written + ".baychat-" + suffix;
|
|
49
|
+
try {
|
|
50
|
+
node_fs_1.default.writeFileSync(temporary, next, { mode: 0o600, flag: "wx" });
|
|
51
|
+
node_fs_1.default.renameSync(temporary, written);
|
|
52
|
+
}
|
|
53
|
+
finally {
|
|
54
|
+
if (node_fs_1.default.existsSync(temporary))
|
|
55
|
+
node_fs_1.default.unlinkSync(temporary);
|
|
56
|
+
}
|
|
57
|
+
return { written, backup };
|
|
58
|
+
}
|
package/dist/hermes.js
CHANGED
|
@@ -56,6 +56,7 @@ const path = __importStar(require("path"));
|
|
|
56
56
|
const args_1 = require("./args");
|
|
57
57
|
const config_1 = require("./config");
|
|
58
58
|
const hermes_plugin_content_1 = require("./hermes-plugin-content");
|
|
59
|
+
const hermes_mcp_1 = require("./hermes-mcp");
|
|
59
60
|
/** The two commands Hermes needs after the files are in place. */
|
|
60
61
|
exports.ENABLE_STEPS = [
|
|
61
62
|
"hermes config set gateway.platforms.baychat.enabled true",
|
|
@@ -128,9 +129,15 @@ function writeHermesEnv(home, token, baseUrl) {
|
|
|
128
129
|
return envPath;
|
|
129
130
|
}
|
|
130
131
|
function installHermes(home) {
|
|
132
|
+
const creds = (0, config_1.loadCredentials)();
|
|
133
|
+
// Refuse a malformed config before changing the gateway or its credential.
|
|
134
|
+
const mcp = creds ? (0, hermes_mcp_1.writeHermesMcp)(home, creds.baseUrl) : null;
|
|
131
135
|
const { written, backedUp } = installHermesPlugin(home);
|
|
136
|
+
if (mcp)
|
|
137
|
+
written.push(mcp.written);
|
|
138
|
+
if (mcp?.backup)
|
|
139
|
+
backedUp.push(mcp.backup);
|
|
132
140
|
const report = { home, written, backedUp, envWritten: null };
|
|
133
|
-
const creds = (0, config_1.loadCredentials)();
|
|
134
141
|
if (!creds) {
|
|
135
142
|
report.envSkipped =
|
|
136
143
|
"no agent token on this machine — run `baychat pair <code>` (get the code in the BayChat app: Agents → your agent), then re-run this";
|
|
@@ -139,18 +146,19 @@ function installHermes(home) {
|
|
|
139
146
|
report.envWritten = writeHermesEnv(home, creds.token, creds.baseUrl);
|
|
140
147
|
return report;
|
|
141
148
|
}
|
|
142
|
-
/**
|
|
143
|
-
function runEnableSteps() {
|
|
144
|
-
const
|
|
149
|
+
/** Fail visibly if the executable or either gateway setup step fails. */
|
|
150
|
+
function runEnableSteps(home) {
|
|
151
|
+
const env = { ...process.env, HERMES_HOME: home };
|
|
152
|
+
const probe = (0, child_process_1.spawnSync)("hermes", ["--version"], { stdio: "ignore", env });
|
|
145
153
|
if (probe.error)
|
|
146
154
|
return false;
|
|
147
155
|
for (const step of exports.ENABLE_STEPS) {
|
|
148
156
|
const [bin, ...rest] = step.split(" ");
|
|
149
157
|
console.log(` $ ${step}`);
|
|
150
|
-
const run = (0, child_process_1.spawnSync)(bin, rest, { stdio: "inherit" });
|
|
158
|
+
const run = (0, child_process_1.spawnSync)(bin, rest, { stdio: "inherit", env });
|
|
151
159
|
if (run.status !== 0) {
|
|
152
160
|
console.log(` ↑ exited ${run.status ?? "abnormally"} — finish the remaining steps by hand.`);
|
|
153
|
-
return
|
|
161
|
+
return false;
|
|
154
162
|
}
|
|
155
163
|
}
|
|
156
164
|
return true;
|
|
@@ -169,13 +177,16 @@ function cmdHermesInit(args) {
|
|
|
169
177
|
}
|
|
170
178
|
else {
|
|
171
179
|
console.log(`\nNo token written — ${report.envSkipped}`);
|
|
180
|
+
if (args.includes("--enable"))
|
|
181
|
+
return 1;
|
|
172
182
|
}
|
|
173
183
|
console.log("\nEnable it in Hermes:");
|
|
174
184
|
if (args.includes("--enable")) {
|
|
175
|
-
if (!runEnableSteps()) {
|
|
176
|
-
console.log("
|
|
185
|
+
if (!runEnableSteps(home)) {
|
|
186
|
+
console.log(" Hermes could not be enabled. Resolve the error, then run:");
|
|
177
187
|
for (const step of exports.ENABLE_STEPS)
|
|
178
188
|
console.log(` ${step}`);
|
|
189
|
+
return 1;
|
|
179
190
|
}
|
|
180
191
|
}
|
|
181
192
|
else {
|
package/dist/index.js
CHANGED
|
@@ -4,6 +4,7 @@ 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 session_command_1 = require("./session-command");
|
|
7
8
|
const doctor_command_1 = require("./doctor-command");
|
|
8
9
|
const hermes_1 = require("./hermes");
|
|
9
10
|
const connect_1 = require("./connect");
|
|
@@ -17,6 +18,8 @@ const profiles_1 = require("./relay/profiles");
|
|
|
17
18
|
const HELP = `baychat — BayChat connector CLI for agent sessions (Claude Code, Codex)
|
|
18
19
|
|
|
19
20
|
Usage:
|
|
21
|
+
baychat join [name] [group] [--group <title>] [--runtime <runtime>]
|
|
22
|
+
Join and connect incoming messages in this terminal
|
|
20
23
|
baychat session-name --runtime <runtime>
|
|
21
24
|
Stable automatic name for this verified session
|
|
22
25
|
baychat onboard [conversationId] [--catch-up]
|
|
@@ -153,6 +156,8 @@ function numberFlag(args, name) {
|
|
|
153
156
|
async function main() {
|
|
154
157
|
const [command, ...args] = process.argv.slice(2);
|
|
155
158
|
switch (command) {
|
|
159
|
+
case "join":
|
|
160
|
+
return (0, session_command_1.cmdJoinSession)(args);
|
|
156
161
|
case "session-name": {
|
|
157
162
|
(0, args_1.rejectUnknownFlags)(args, ["--runtime"], "baychat session-name --runtime <runtime>");
|
|
158
163
|
const runtime = (0, args_1.flag)(args, "--runtime") ?? (0, owner_pid_1.detectRuntime)(profiles_1.RUNTIME_PROFILES);
|
package/dist/relay/adapters.js
CHANGED
|
@@ -197,7 +197,11 @@ const codexAdapter = {
|
|
|
197
197
|
*/
|
|
198
198
|
async runTurn({ binaryPath, target, prompt }) {
|
|
199
199
|
if (process.env.BAYCHAT_CODEX_TRANSPORT === "exec") {
|
|
200
|
-
return {
|
|
200
|
+
return {
|
|
201
|
+
kind: "failed",
|
|
202
|
+
transportUnusable: true,
|
|
203
|
+
reason: "BAYCHAT_CODEX_TRANSPORT=exec",
|
|
204
|
+
};
|
|
201
205
|
}
|
|
202
206
|
return (0, codex_app_server_1.runCodexTurn)({
|
|
203
207
|
binaryPath,
|
|
@@ -234,7 +238,10 @@ const cursorAdapter = {
|
|
|
234
238
|
// Nothing to discover: Cursor writes no on-disk session state this package
|
|
235
239
|
// can identify a conversation from, and a search that cannot succeed is
|
|
236
240
|
// only a way to produce a confident-looking wrong answer.
|
|
237
|
-
return {
|
|
241
|
+
return {
|
|
242
|
+
ok: false,
|
|
243
|
+
reason: "Cursor records no resumable session id on this machine",
|
|
244
|
+
};
|
|
238
245
|
},
|
|
239
246
|
headlessCommand() {
|
|
240
247
|
// Unreachable: the daemon consults canResume first and reports pending.
|
|
@@ -259,7 +266,10 @@ const hermesAdapter = {
|
|
|
259
266
|
async discoverResume() {
|
|
260
267
|
// Nothing to discover: Hermes runs somewhere else and leaves no local
|
|
261
268
|
// transcript. Searching would only produce a confident-looking wrong answer.
|
|
262
|
-
return {
|
|
269
|
+
return {
|
|
270
|
+
ok: false,
|
|
271
|
+
reason: "Hermes keeps no local session state on this machine",
|
|
272
|
+
};
|
|
263
273
|
},
|
|
264
274
|
headlessCommand() {
|
|
265
275
|
// Unreachable: the daemon consults canResume first and reports pending.
|
|
@@ -317,7 +327,9 @@ function profileAdapter(profile) {
|
|
|
317
327
|
// `session-name` runtimes are resumed by the name the human chose, which
|
|
318
328
|
// IS the BayChat session name — so there is nothing to discover and
|
|
319
329
|
// nothing that could name someone else's session.
|
|
320
|
-
const id = profile.sessionId.kind === "session-name"
|
|
330
|
+
const id = profile.sessionId.kind === "session-name"
|
|
331
|
+
? target.name
|
|
332
|
+
: target.resumeId;
|
|
321
333
|
if (!id) {
|
|
322
334
|
return {
|
|
323
335
|
ok: false,
|
|
@@ -337,7 +349,9 @@ function profileAdapter(profile) {
|
|
|
337
349
|
headlessCommand(target, prompt) {
|
|
338
350
|
if (!profile.headless)
|
|
339
351
|
throw new Error(`${profile.id} has no headless command`);
|
|
340
|
-
const id = profile.sessionId.kind === "session-name"
|
|
352
|
+
const id = profile.sessionId.kind === "session-name"
|
|
353
|
+
? target.name
|
|
354
|
+
: target.resumeId;
|
|
341
355
|
return {
|
|
342
356
|
file: target.runtimeBin ?? profile.bin,
|
|
343
357
|
args: (0, profiles_1.fillTemplate)(profile.headless.args, { id, prompt }),
|
|
@@ -353,7 +367,10 @@ function adapterFor(runtime) {
|
|
|
353
367
|
return profile ? profileAdapter(profile) : unknownAdapter(runtime);
|
|
354
368
|
}
|
|
355
369
|
/** The runtimes we ship knowledge of, for help text. NOT a list of what is accepted. */
|
|
356
|
-
exports.KNOWN_RUNTIMES = [
|
|
370
|
+
exports.KNOWN_RUNTIMES = [
|
|
371
|
+
...Object.keys(ADAPTERS),
|
|
372
|
+
...profiles_1.RUNTIME_PROFILES.map((p) => p.id),
|
|
373
|
+
];
|
|
357
374
|
/**
|
|
358
375
|
* Do we ship knowledge of this runtime?
|
|
359
376
|
*
|
|
@@ -364,7 +381,8 @@ exports.KNOWN_RUNTIMES = [...Object.keys(ADAPTERS), ...profiles_1.RUNTIME_PROFIL
|
|
|
364
381
|
* us, and the Cursor outage was the same mistake from the other side.
|
|
365
382
|
*/
|
|
366
383
|
function isKnownRuntime(value) {
|
|
367
|
-
return Object.prototype.hasOwnProperty.call(ADAPTERS, value) ||
|
|
384
|
+
return (Object.prototype.hasOwnProperty.call(ADAPTERS, value) ||
|
|
385
|
+
(0, profiles_1.profileFor)(value) !== undefined);
|
|
368
386
|
}
|
|
369
387
|
/**
|
|
370
388
|
* Run a headless turn to completion.
|
|
@@ -377,6 +395,7 @@ function isKnownRuntime(value) {
|
|
|
377
395
|
function runHeadless(file, args, opts = {}) {
|
|
378
396
|
return new Promise((resolve) => {
|
|
379
397
|
const child = (0, child_process_1.spawn)(file, args, {
|
|
398
|
+
windowsHide: true,
|
|
380
399
|
cwd: opts.cwd,
|
|
381
400
|
// An npm-installed runtime is a script with a `#!/usr/bin/env node`
|
|
382
401
|
// shebang, and the daemon's systemd PATH has no node. See ./spawn-env.ts.
|
package/dist/relay/autostart.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.AUTOSTART_IMPLEMENTATIONS = exports.LingerUnavailable = exports.SYSTEMD_UNIT_NAME = exports.SERVICE_LABEL = void 0;
|
|
36
|
+
exports.AUTOSTART_IMPLEMENTATIONS = exports.WINDOWS_RELAY_LAUNCHER = exports.LingerUnavailable = exports.SYSTEMD_UNIT_NAME = exports.SERVICE_LABEL = void 0;
|
|
37
37
|
exports.autostartForPlatform = autostartForPlatform;
|
|
38
38
|
exports.relayCommand = relayCommand;
|
|
39
39
|
// Making the relay come back by itself, on all three platforms.
|
|
@@ -101,9 +101,17 @@ WantedBy=default.target
|
|
|
101
101
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
102
102
|
fs.writeFileSync(target, systemd.unit(cmd), { mode: 0o644 });
|
|
103
103
|
await execFileAsync("systemctl", ["--user", "daemon-reload"]);
|
|
104
|
-
await execFileAsync("systemctl", [
|
|
104
|
+
await execFileAsync("systemctl", [
|
|
105
|
+
"--user",
|
|
106
|
+
"enable",
|
|
107
|
+
"--now",
|
|
108
|
+
exports.SYSTEMD_UNIT_NAME,
|
|
109
|
+
]);
|
|
105
110
|
try {
|
|
106
|
-
await execFileAsync("loginctl", [
|
|
111
|
+
await execFileAsync("loginctl", [
|
|
112
|
+
"enable-linger",
|
|
113
|
+
os.userInfo().username,
|
|
114
|
+
]);
|
|
107
115
|
}
|
|
108
116
|
catch {
|
|
109
117
|
// Not fatal and not silent: the caller prints the degraded promise rather
|
|
@@ -112,7 +120,12 @@ WantedBy=default.target
|
|
|
112
120
|
}
|
|
113
121
|
},
|
|
114
122
|
async uninstall() {
|
|
115
|
-
await execFileAsync("systemctl", [
|
|
123
|
+
await execFileAsync("systemctl", [
|
|
124
|
+
"--user",
|
|
125
|
+
"disable",
|
|
126
|
+
"--now",
|
|
127
|
+
exports.SYSTEMD_UNIT_NAME,
|
|
128
|
+
]).catch(() => undefined);
|
|
116
129
|
fs.rmSync(systemd.unitPath(), { force: true });
|
|
117
130
|
},
|
|
118
131
|
};
|
|
@@ -175,12 +188,18 @@ const launchd = {
|
|
|
175
188
|
const domain = `gui/${os.userInfo().uid}`;
|
|
176
189
|
// Replacing an agent means removing the old one first; bootout on a label
|
|
177
190
|
// that was never loaded exits non-zero, which is not a failure here.
|
|
178
|
-
await execFileAsync("launchctl", [
|
|
191
|
+
await execFileAsync("launchctl", [
|
|
192
|
+
"bootout",
|
|
193
|
+
`${domain}/${exports.SERVICE_LABEL}`,
|
|
194
|
+
]).catch(() => undefined);
|
|
179
195
|
await execFileAsync("launchctl", ["bootstrap", domain, target]);
|
|
180
196
|
},
|
|
181
197
|
async uninstall() {
|
|
182
198
|
const domain = `gui/${os.userInfo().uid}`;
|
|
183
|
-
await execFileAsync("launchctl", [
|
|
199
|
+
await execFileAsync("launchctl", [
|
|
200
|
+
"bootout",
|
|
201
|
+
`${domain}/${exports.SERVICE_LABEL}`,
|
|
202
|
+
]).catch(() => undefined);
|
|
184
203
|
fs.rmSync(launchd.unitPath(), { force: true });
|
|
185
204
|
},
|
|
186
205
|
};
|
|
@@ -246,8 +265,8 @@ const scheduledTask = {
|
|
|
246
265
|
</Settings>
|
|
247
266
|
<Actions Context="Author">
|
|
248
267
|
<Exec>
|
|
249
|
-
<Command>${escapeXml(
|
|
250
|
-
<Arguments
|
|
268
|
+
<Command>${escapeXml(path.win32.join(process.env.SystemRoot || "C:\\Windows", "System32", "wscript.exe"))}</Command>
|
|
269
|
+
<Arguments>//B //NoLogo ${escapeXml(`"${windowsLauncherPath()}" "${cmd.node}" "${cmd.entry}"`)} relay start --foreground</Arguments>
|
|
251
270
|
</Exec>
|
|
252
271
|
</Actions>
|
|
253
272
|
</Task>
|
|
@@ -258,17 +277,47 @@ const scheduledTask = {
|
|
|
258
277
|
async install(cmd) {
|
|
259
278
|
const target = scheduledTask.unitPath();
|
|
260
279
|
fs.mkdirSync(path.dirname(target), { recursive: true });
|
|
280
|
+
fs.writeFileSync(windowsLauncherPath(), exports.WINDOWS_RELAY_LAUNCHER, "utf8");
|
|
261
281
|
// UTF-16LE with a BOM, because that is what the XML declaration above claims
|
|
262
282
|
// and what schtasks refuses the file without.
|
|
263
|
-
fs.writeFileSync(target, "\ufeff" + scheduledTask.unit(cmd), {
|
|
264
|
-
|
|
265
|
-
|
|
283
|
+
fs.writeFileSync(target, "\ufeff" + scheduledTask.unit(cmd), {
|
|
284
|
+
encoding: "utf16le",
|
|
285
|
+
});
|
|
286
|
+
await execFileAsync("schtasks", ["/create", "/tn", exports.SERVICE_LABEL, "/xml", target, "/f"], { windowsHide: true });
|
|
287
|
+
await execFileAsync("schtasks", ["/run", "/tn", exports.SERVICE_LABEL], {
|
|
288
|
+
windowsHide: true,
|
|
289
|
+
});
|
|
266
290
|
},
|
|
267
291
|
async uninstall() {
|
|
268
|
-
|
|
292
|
+
// Deleting a task does not stop its running child. End the supervised
|
|
293
|
+
// process first so `relay stop` cannot leave an invisible orphan behind.
|
|
294
|
+
await execFileAsync("schtasks", ["/end", "/tn", exports.SERVICE_LABEL], {
|
|
295
|
+
windowsHide: true,
|
|
296
|
+
}).catch(() => undefined);
|
|
297
|
+
await execFileAsync("schtasks", ["/delete", "/tn", exports.SERVICE_LABEL, "/f"], {
|
|
298
|
+
windowsHide: true,
|
|
299
|
+
}).catch(() => undefined);
|
|
269
300
|
fs.rmSync(scheduledTask.unitPath(), { force: true });
|
|
301
|
+
fs.rmSync(windowsLauncherPath(), { force: true });
|
|
270
302
|
},
|
|
271
303
|
};
|
|
304
|
+
/** WSH is a GUI executable, so starting it never allocates a visible console.
|
|
305
|
+
* Run's window style 0 hides the child too. Waiting and forwarding its exit
|
|
306
|
+
* code keeps Task Scheduler's restart supervision attached to the actual relay.
|
|
307
|
+
* This is Windows Script Host JScript, not Node; no cmd.exe or shell is involved.
|
|
308
|
+
*/
|
|
309
|
+
exports.WINDOWS_RELAY_LAUNCHER = `var shell = new ActiveXObject("WScript.Shell");
|
|
310
|
+
var command = [];
|
|
311
|
+
for (var index = 0; index < WScript.Arguments.length; index++) {
|
|
312
|
+
var argument = WScript.Arguments(index);
|
|
313
|
+
if (/["\\r\\n]/.test(argument)) throw new Error("Invalid relay startup argument");
|
|
314
|
+
command.push('"' + argument + '"');
|
|
315
|
+
}
|
|
316
|
+
WScript.Quit(shell.Run(command.join(" "), 0, true));
|
|
317
|
+
`;
|
|
318
|
+
function windowsLauncherPath() {
|
|
319
|
+
return path.join(configHome(), "relay-launcher.js");
|
|
320
|
+
}
|
|
272
321
|
/** `DOMAIN\user`, which is what a task's `UserId` wants. `USERDOMAIN` is absent
|
|
273
322
|
* on a machine that was never joined to a domain; the bare username is correct
|
|
274
323
|
* there, and inventing a domain would register a task for a principal that does
|
|
@@ -316,9 +365,19 @@ function autostartForPlatform(platform = process.platform) {
|
|
|
316
365
|
* `process.argv[1]` is the script node was given; resolving it is what keeps an
|
|
317
366
|
* npx-run or `npm link`ed CLI producing a unit that still works after the shell
|
|
318
367
|
* that installed it is gone. */
|
|
319
|
-
function relayCommand() {
|
|
320
|
-
|
|
368
|
+
function relayCommand(physicalPath = (value) => fs.realpathSync.native(value)) {
|
|
369
|
+
// Windows packaged apps virtualize AppData. A path visible inside Codex can
|
|
370
|
+
// be absent from Task Scheduler's filesystem view. Resolve the actual file
|
|
371
|
+
// before handing it to a process outside that package (observed on Windows).
|
|
372
|
+
return {
|
|
373
|
+
node: physicalPath(process.execPath),
|
|
374
|
+
entry: physicalPath(path.resolve(process.argv[1] ?? "")),
|
|
375
|
+
};
|
|
321
376
|
}
|
|
322
377
|
/** Exported for tests, which need to reach a platform other than the one they
|
|
323
378
|
* run on. Production code goes through {@link autostartForPlatform}. */
|
|
324
|
-
exports.AUTOSTART_IMPLEMENTATIONS = {
|
|
379
|
+
exports.AUTOSTART_IMPLEMENTATIONS = {
|
|
380
|
+
systemd,
|
|
381
|
+
launchd,
|
|
382
|
+
scheduledTask,
|
|
383
|
+
};
|