baychat 0.13.0 → 0.14.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 +146 -1
- package/dist/args.js +57 -0
- package/dist/client-paths.js +69 -0
- package/dist/commands.js +108 -0
- package/dist/connect.js +46 -14
- package/dist/credential-refresh.js +97 -0
- package/dist/doctor-command.js +154 -0
- package/dist/doctor.js +502 -0
- package/dist/help-topics.js +197 -0
- package/dist/index.js +69 -27
- package/dist/relay/adapters.js +82 -1
- package/dist/relay/codex-app-server.js +217 -0
- package/dist/relay/codex-queue.js +68 -0
- package/dist/relay/commands.js +177 -31
- package/dist/relay/daemon.js +259 -3
- package/dist/relay/mailbox-watcher.js +118 -0
- package/dist/relay/mailbox.js +319 -0
- package/dist/relay/parent-watch.js +68 -0
- package/dist/relay/resume.js +39 -11
- package/dist/relay/socket.js +98 -14
- package/dist/relay/spawn-env.js +69 -0
- package/dist/runtime-binary.js +269 -0
- package/dist/runtimes.js +122 -68
- package/package.json +2 -2
|
@@ -0,0 +1,69 @@
|
|
|
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.headlessSpawnEnv = headlessSpawnEnv;
|
|
37
|
+
const path = __importStar(require("path"));
|
|
38
|
+
/**
|
|
39
|
+
* The environment a headless turn is spawned with.
|
|
40
|
+
*
|
|
41
|
+
* WHY THIS EXISTS. A runtime installed by npm is a JavaScript file with a
|
|
42
|
+
* `#!/usr/bin/env node` shebang, so running it asks the SPAWNING process's PATH
|
|
43
|
+
* to find `node`. The relay daemon runs under systemd, whose PATH is a minimal
|
|
44
|
+
* system one — it does not contain nvm's node, or any node at all on a machine
|
|
45
|
+
* where node was never installed system-wide.
|
|
46
|
+
*
|
|
47
|
+
* Measured 2026-08-31 00:06: the headless rung became reachable for the first
|
|
48
|
+
* time and every wake died instantly with
|
|
49
|
+
* `/usr/bin/env: 'node': No such file or directory`, exit 127. The binary was
|
|
50
|
+
* correct and present; nothing could run it.
|
|
51
|
+
*
|
|
52
|
+
* This is `SessionTarget.runtimeBin` one layer down. That rule says the session
|
|
53
|
+
* identifies its own binary because only it can; this says the daemon must also
|
|
54
|
+
* hand that binary an environment it can actually start in — and the daemon is
|
|
55
|
+
* the only process that knows where its own node lives (`process.execPath`,
|
|
56
|
+
* which is exactly the interpreter a shebang is looking for).
|
|
57
|
+
*
|
|
58
|
+
* PREPENDED, not replaced: a runtime may legitimately need the rest of the
|
|
59
|
+
* inherited PATH to find its own helpers, and clobbering it would trade this
|
|
60
|
+
* failure for a subtler one.
|
|
61
|
+
*/
|
|
62
|
+
function headlessSpawnEnv(env = process.env, execPath = process.execPath) {
|
|
63
|
+
const nodeDir = path.dirname(execPath);
|
|
64
|
+
const current = env.PATH ?? "";
|
|
65
|
+
const parts = current.split(path.delimiter).filter(Boolean);
|
|
66
|
+
if (parts.includes(nodeDir))
|
|
67
|
+
return env;
|
|
68
|
+
return { ...env, PATH: [nodeDir, ...parts].join(path.delimiter) };
|
|
69
|
+
}
|
|
@@ -0,0 +1,269 @@
|
|
|
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.resolveRuntimeBinary = resolveRuntimeBinary;
|
|
37
|
+
exports.describeResolution = describeResolution;
|
|
38
|
+
exports.spawnPlanFor = spawnPlanFor;
|
|
39
|
+
exports.summarizeProbe = summarizeProbe;
|
|
40
|
+
exports.currentBinaryEnv = currentBinaryEnv;
|
|
41
|
+
exports.summarizeResolutionFailure = summarizeResolutionFailure;
|
|
42
|
+
const child_process_1 = require("child_process");
|
|
43
|
+
const fs = __importStar(require("fs"));
|
|
44
|
+
/**
|
|
45
|
+
* Resolve a runtime's executable, proving each candidate before accepting it.
|
|
46
|
+
*
|
|
47
|
+
* Precedence: explicit override (exclusively), then native PATH entries in
|
|
48
|
+
* order, then foreign-filesystem PATH entries. Foreign entries are demoted
|
|
49
|
+
* rather than excluded — a user genuinely running the Windows build through WSL
|
|
50
|
+
* interop must still be able to work — but they never beat a native binary that
|
|
51
|
+
* runs, because spawning across the interop boundary means a different
|
|
52
|
+
* filesystem and a different home than the session it is answering for.
|
|
53
|
+
*/
|
|
54
|
+
function resolveRuntimeBinary(name, env) {
|
|
55
|
+
if (env.override !== undefined && env.override.trim() !== "") {
|
|
56
|
+
return resolveOverride(env.override, env);
|
|
57
|
+
}
|
|
58
|
+
const candidates = pathCandidates(name, env);
|
|
59
|
+
if (candidates.length === 0) {
|
|
60
|
+
return {
|
|
61
|
+
reason: `${name} not found on PATH (${env.pathEntries.length} entries searched)`,
|
|
62
|
+
rejected: [],
|
|
63
|
+
ok: false,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
const rejected = [];
|
|
67
|
+
for (const candidate of candidates) {
|
|
68
|
+
const probed = env.probe(candidate.path);
|
|
69
|
+
if (probed.ok) {
|
|
70
|
+
return { ok: true, path: candidate.path, version: probed.version, source: candidate.source };
|
|
71
|
+
}
|
|
72
|
+
rejected.push({ path: candidate.path, reason: probed.detail });
|
|
73
|
+
}
|
|
74
|
+
return { ok: false, reason: `no working ${name} binary on this machine`, rejected };
|
|
75
|
+
}
|
|
76
|
+
/** Render a resolution for a human — one line on success, a full account on failure. */
|
|
77
|
+
function describeResolution(name, resolution) {
|
|
78
|
+
if (resolution.ok)
|
|
79
|
+
return [`${resolution.path} (${resolution.version})`];
|
|
80
|
+
const lines = [resolution.reason];
|
|
81
|
+
for (const candidate of resolution.rejected) {
|
|
82
|
+
lines.push(` tried ${candidate.path}`);
|
|
83
|
+
lines.push(` → ${candidate.reason}`);
|
|
84
|
+
}
|
|
85
|
+
return lines;
|
|
86
|
+
}
|
|
87
|
+
/**
|
|
88
|
+
* An override is considered alone, and its failure is the answer.
|
|
89
|
+
*
|
|
90
|
+
* Falling through to PATH would mean the CLI used a binary the user did not name
|
|
91
|
+
* while their explicit setting sat broken and unreported.
|
|
92
|
+
*/
|
|
93
|
+
function resolveOverride(override, env) {
|
|
94
|
+
if (!env.isExecutable(override)) {
|
|
95
|
+
return {
|
|
96
|
+
ok: false,
|
|
97
|
+
reason: `configured override ${override} is not an executable file`,
|
|
98
|
+
rejected: [],
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
const probed = env.probe(override);
|
|
102
|
+
if (probed.ok)
|
|
103
|
+
return { ok: true, path: override, version: probed.version, source: "override" };
|
|
104
|
+
return {
|
|
105
|
+
ok: false,
|
|
106
|
+
reason: `configured override ${override} does not run`,
|
|
107
|
+
rejected: [{ path: override, reason: probed.detail }],
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
/**
|
|
111
|
+
* Every executable candidate on PATH, native entries first.
|
|
112
|
+
*
|
|
113
|
+
* Deduplicated by path: WSL routinely lists the same Windows npm directory
|
|
114
|
+
* twice, and probing spawns a process, so a duplicate entry would double the
|
|
115
|
+
* cost of resolution for no new information.
|
|
116
|
+
*/
|
|
117
|
+
function pathCandidates(name, env) {
|
|
118
|
+
const native = [];
|
|
119
|
+
const foreign = [];
|
|
120
|
+
const seen = new Set();
|
|
121
|
+
for (const entry of env.pathEntries) {
|
|
122
|
+
if (entry.trim() === "")
|
|
123
|
+
continue;
|
|
124
|
+
const source = isForeignMount(entry, env.platform) ? "foreign-path" : "path";
|
|
125
|
+
for (const fileName of executableNames(name, env.platform)) {
|
|
126
|
+
const candidate = joinPath(entry, fileName, env.platform);
|
|
127
|
+
if (seen.has(candidate))
|
|
128
|
+
continue;
|
|
129
|
+
seen.add(candidate);
|
|
130
|
+
if (!env.isExecutable(candidate))
|
|
131
|
+
continue;
|
|
132
|
+
(source === "path" ? native : foreign).push({ path: candidate, source });
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
return [...native, ...foreign];
|
|
136
|
+
}
|
|
137
|
+
/**
|
|
138
|
+
* Whether a PATH entry lives on a Windows drive mounted into Linux.
|
|
139
|
+
*
|
|
140
|
+
* Deliberately narrow: only `/mnt/<single letter>/…` counts. `/mnt/data` is an
|
|
141
|
+
* ordinary Linux mount and demoting it would be wrong.
|
|
142
|
+
*/
|
|
143
|
+
function isForeignMount(entry, platform) {
|
|
144
|
+
if (platform !== "linux")
|
|
145
|
+
return false;
|
|
146
|
+
return /^\/mnt\/[a-z]\//i.test(entry);
|
|
147
|
+
}
|
|
148
|
+
/**
|
|
149
|
+
* The file names a runtime may have on this platform.
|
|
150
|
+
*
|
|
151
|
+
* Windows resolves an unqualified command against PATHEXT; an npm-installed CLI
|
|
152
|
+
* is typically a `.cmd` shim, so omitting the extensions would find nothing at
|
|
153
|
+
* all there.
|
|
154
|
+
*/
|
|
155
|
+
function executableNames(name, platform) {
|
|
156
|
+
if (platform !== "win32")
|
|
157
|
+
return [name];
|
|
158
|
+
return [`${name}.exe`, `${name}.cmd`, `${name}.bat`, name];
|
|
159
|
+
}
|
|
160
|
+
/**
|
|
161
|
+
* Join a directory and a file name for the TARGET platform.
|
|
162
|
+
*
|
|
163
|
+
* Node's `path.join` follows the host, not the injected platform, so using it
|
|
164
|
+
* would make every Windows case untestable from Linux CI.
|
|
165
|
+
*/
|
|
166
|
+
function joinPath(dir, file, platform) {
|
|
167
|
+
const separator = platform === "win32" ? "\\" : "/";
|
|
168
|
+
const trimmed = dir.endsWith(separator) ? dir.slice(0, -separator.length) : dir;
|
|
169
|
+
return `${trimmed}${separator}${file}`;
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* @param candidate an executable path already resolved by `resolveRuntimeBinary`
|
|
173
|
+
* @returns file + prefix args to spawn it with, always `shell: false`
|
|
174
|
+
*/
|
|
175
|
+
function spawnPlanFor(candidate, platform) {
|
|
176
|
+
const needsInterpreter = platform === "win32" && /\.(cmd|bat)$/i.test(candidate);
|
|
177
|
+
if (!needsInterpreter)
|
|
178
|
+
return { file: candidate, prefixArgs: [] };
|
|
179
|
+
return { file: "cmd.exe", prefixArgs: ["/d", "/s", "/c", candidate] };
|
|
180
|
+
}
|
|
181
|
+
/** How long a `--version` probe may run before it is treated as broken. */
|
|
182
|
+
const PROBE_TIMEOUT_MS = 5_000;
|
|
183
|
+
/** The most stderr worth quoting back to a user in a rejection reason. */
|
|
184
|
+
const PROBE_DETAIL_LIMIT = 200;
|
|
185
|
+
/**
|
|
186
|
+
* Turn a finished `--version` run into a probe result.
|
|
187
|
+
*
|
|
188
|
+
* Split out from the spawn so the interesting half — deciding what counts as
|
|
189
|
+
* working, and what to quote when it does not — is testable without a process.
|
|
190
|
+
*/
|
|
191
|
+
function summarizeProbe(outcome) {
|
|
192
|
+
if (outcome.error)
|
|
193
|
+
return { ok: false, detail: outcome.error.message };
|
|
194
|
+
if (outcome.status !== 0) {
|
|
195
|
+
// Prefer stderr: a failing CLI puts its diagnosis there, and it is what
|
|
196
|
+
// names the actual fault (e.g. the missing optional dependency).
|
|
197
|
+
const said = firstMeaningfulLine(outcome.stderr) || firstMeaningfulLine(outcome.stdout);
|
|
198
|
+
const exited = `exits ${outcome.status ?? "on a signal"}`;
|
|
199
|
+
return { ok: false, detail: said ? `${exited}: ${said}` : exited };
|
|
200
|
+
}
|
|
201
|
+
const version = firstMeaningfulLine(outcome.stdout) || firstMeaningfulLine(outcome.stderr);
|
|
202
|
+
// Exit 0 is the contract. A binary that runs but prints nothing recognisable
|
|
203
|
+
// still runs, so it is accepted with an honest placeholder rather than
|
|
204
|
+
// rejected for a cosmetic reason.
|
|
205
|
+
return { ok: true, version: version || "version not reported" };
|
|
206
|
+
}
|
|
207
|
+
function firstMeaningfulLine(text) {
|
|
208
|
+
for (const line of text.split("\n")) {
|
|
209
|
+
const trimmed = line.trim();
|
|
210
|
+
if (trimmed !== "")
|
|
211
|
+
return trimmed.slice(0, PROBE_DETAIL_LIMIT);
|
|
212
|
+
}
|
|
213
|
+
return "";
|
|
214
|
+
}
|
|
215
|
+
/**
|
|
216
|
+
* Read the binary environment from this process.
|
|
217
|
+
*
|
|
218
|
+
* `shell: false` throughout: nothing here is ever concatenated into a command
|
|
219
|
+
* line, and a PATH entry is attacker-adjacent data on a shared machine.
|
|
220
|
+
*/
|
|
221
|
+
function currentBinaryEnv(override) {
|
|
222
|
+
const delimiter = process.platform === "win32" ? ";" : ":";
|
|
223
|
+
return {
|
|
224
|
+
platform: process.platform,
|
|
225
|
+
pathEntries: (process.env.PATH ?? "").split(delimiter),
|
|
226
|
+
override,
|
|
227
|
+
isExecutable(candidate) {
|
|
228
|
+
try {
|
|
229
|
+
return fs.statSync(candidate).isFile();
|
|
230
|
+
}
|
|
231
|
+
catch {
|
|
232
|
+
// Absent, unreadable, or a dangling symlink — all mean "not a candidate".
|
|
233
|
+
// There is nothing to log: most PATH entries do not contain most binaries.
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
},
|
|
237
|
+
probe(candidate) {
|
|
238
|
+
const plan = spawnPlanFor(candidate, process.platform);
|
|
239
|
+
const run = (0, child_process_1.spawnSync)(plan.file, [...plan.prefixArgs, "--version"], {
|
|
240
|
+
timeout: PROBE_TIMEOUT_MS,
|
|
241
|
+
encoding: "utf8",
|
|
242
|
+
shell: false,
|
|
243
|
+
});
|
|
244
|
+
return summarizeProbe({
|
|
245
|
+
status: run.status,
|
|
246
|
+
stdout: run.stdout ?? "",
|
|
247
|
+
stderr: run.stderr ?? "",
|
|
248
|
+
error: run.error,
|
|
249
|
+
});
|
|
250
|
+
},
|
|
251
|
+
};
|
|
252
|
+
}
|
|
253
|
+
/**
|
|
254
|
+
* One line naming the failure and the best evidence for it.
|
|
255
|
+
*
|
|
256
|
+
* `describeResolution` is shaped for a report a human is reading deliberately;
|
|
257
|
+
* this is for a place that has room for a sentence — a `DELIVERY PENDING`
|
|
258
|
+
* reason in `relay status`, a log line — where a multi-line block would wrap
|
|
259
|
+
* into noise. It names the first rejected candidate because that is the one
|
|
260
|
+
* PATH would have chosen, and therefore the one the user believes is in use.
|
|
261
|
+
*/
|
|
262
|
+
function summarizeResolutionFailure(resolution) {
|
|
263
|
+
if (resolution.ok)
|
|
264
|
+
return "";
|
|
265
|
+
const first = resolution.rejected[0];
|
|
266
|
+
if (!first)
|
|
267
|
+
return resolution.reason;
|
|
268
|
+
return `${resolution.reason} (tried ${first.path}: ${first.reason})`;
|
|
269
|
+
}
|
package/dist/runtimes.js
CHANGED
|
@@ -25,6 +25,9 @@ exports.isRuntime = isRuntime;
|
|
|
25
25
|
exports.runtimeSpec = runtimeSpec;
|
|
26
26
|
exports.commandContextFor = commandContextFor;
|
|
27
27
|
exports.renderCommandFor = renderCommandFor;
|
|
28
|
+
// The rooms guidance is shared with `baychat help groups`, so the words a person
|
|
29
|
+
// reads in their terminal and the words their agent was given are the same words.
|
|
30
|
+
const help_topics_1 = require("./help-topics");
|
|
28
31
|
exports.RUNTIMES = ["claude", "codex", "cursor", "desktop", "pi", "hermes", "generic"];
|
|
29
32
|
const GENERIC_RESUME_NOTE = `The relay can only wake this session while \`attach\` is running. Re-arm it after
|
|
30
33
|
every wake; a message that arrives while nothing is listening is recorded
|
|
@@ -45,24 +48,108 @@ function attachFor(spec) {
|
|
|
45
48
|
throw new Error(`runtime "${spec.id}" ships a skill but declares no relay attach spec — ` +
|
|
46
49
|
"its skill would tell the session to run an attach the relay rejects");
|
|
47
50
|
}
|
|
51
|
+
const attachLine = 'baychat relay attach --session "<name>" --runtime <this runtime>';
|
|
48
52
|
return {
|
|
49
|
-
attachLine
|
|
53
|
+
attachLine,
|
|
50
54
|
resumeNote: GENERIC_RESUME_NOTE,
|
|
55
|
+
reachability: reachabilityFor(attachLine, GENERIC_RESUME_NOTE, "background-every-wake"),
|
|
51
56
|
};
|
|
52
57
|
}
|
|
53
58
|
const resumeFlag = spec.relay.sessionIdExpr ? ` --resume-id "${spec.relay.sessionIdExpr}"` : "";
|
|
59
|
+
const attachLine = `baychat relay attach --session "<name>" --runtime ${spec.relay.runtime}${resumeFlag}`;
|
|
54
60
|
return {
|
|
55
|
-
attachLine
|
|
61
|
+
attachLine,
|
|
56
62
|
resumeNote: spec.relay.resumeNote,
|
|
63
|
+
reachability: reachabilityFor(attachLine, spec.relay.resumeNote, spec.relay.reArm),
|
|
57
64
|
};
|
|
58
65
|
}
|
|
66
|
+
/**
|
|
67
|
+
* The "Staying reachable" section, which is NOT the same advice for every runtime.
|
|
68
|
+
*
|
|
69
|
+
* It was one fixed block until 2026-08-31, telling every agent to background its
|
|
70
|
+
* attach and re-arm after every wake. That is right for Claude Code and actively
|
|
71
|
+
* harmful for Codex: a sandboxed Codex is killed with its process group when a
|
|
72
|
+
* command returns, so a backgrounded attach listens to nothing — and the agent,
|
|
73
|
+
* having run the command it was told to run, reports success. An instruction
|
|
74
|
+
* that cannot work is worse than no instruction, because it produces confident
|
|
75
|
+
* reports of work never done.
|
|
76
|
+
*/
|
|
77
|
+
function reachabilityFor(attachLine, resumeNote, reArm) {
|
|
78
|
+
if (reArm === "arm-once") {
|
|
79
|
+
// BOUNDED. `cmdRelayAttach` only installs a timer when `--timeout` is given,
|
|
80
|
+
// and the mailbox path blocks in `readFile(fifo)` with no deadline at all —
|
|
81
|
+
// so an unbounded "arm once" command hangs the agent's turn until a message
|
|
82
|
+
// happens to arrive. Registration completes in milliseconds; the wait is not
|
|
83
|
+
// what this rung is for.
|
|
84
|
+
const bounded = `${attachLine} --timeout 30`;
|
|
85
|
+
return `## Staying reachable — arm once
|
|
86
|
+
|
|
87
|
+
If \`baychat relay\` is running on this machine (\`baychat connect\` starts it), run
|
|
88
|
+
this ONCE, in the FOREGROUND, as soon as you have joined:
|
|
89
|
+
|
|
90
|
+
\`\`\`
|
|
91
|
+
${bounded}
|
|
92
|
+
\`\`\`
|
|
93
|
+
|
|
94
|
+
This is how the relay learns which runtime session you are. \`--timeout 30\` is
|
|
95
|
+
part of the command, not a suggestion: registration is the point here, and the
|
|
96
|
+
wait is not. Without it the command blocks until a message happens to arrive,
|
|
97
|
+
which hangs your turn for no benefit.
|
|
98
|
+
|
|
99
|
+
**Do NOT put it in the background.** Your sandbox kills backgrounded processes
|
|
100
|
+
when the command returns, so a backgrounded attach listens to nothing while
|
|
101
|
+
looking like it worked.
|
|
102
|
+
|
|
103
|
+
**You do not need to re-arm it.** ${resumeNote}
|
|
104
|
+
|
|
105
|
+
It prints \`No new messages before timeout.\` and exits — that is success, not a
|
|
106
|
+
failure. Arm once, then get on with your work.
|
|
107
|
+
|
|
108
|
+
If you are ever unsure whether the relay knows you, run \`baychat relay status\`:
|
|
109
|
+
you are reachable when your session is listed with a resume id, whether or not
|
|
110
|
+
anything is attached.`;
|
|
111
|
+
}
|
|
112
|
+
return `## Staying reachable — re-arm every time
|
|
113
|
+
|
|
114
|
+
If \`baychat relay\` is running on this machine (\`baychat connect\` starts it), run
|
|
115
|
+
this in the BACKGROUND as soon as you have joined:
|
|
116
|
+
|
|
117
|
+
\`\`\`
|
|
118
|
+
${attachLine}
|
|
119
|
+
\`\`\`
|
|
120
|
+
|
|
121
|
+
It blocks until a message arrives, prints it, and **exits**. That exit is the
|
|
122
|
+
wake: it is what causes you to be invoked again. A process that never exits
|
|
123
|
+
could never wake you, which is why this is one-shot rather than a stream.
|
|
124
|
+
|
|
125
|
+
**Run it exactly as written.** Whether the relay can still reach you once this
|
|
126
|
+
process has exited is the difference between being woken while detached and
|
|
127
|
+
having your messages recorded DELIVERY PENDING until a human comes back.
|
|
128
|
+
${resumeNote}
|
|
129
|
+
|
|
130
|
+
**So it must be re-armed after every wake.** The moment you finish handling a
|
|
131
|
+
wake — whether you replied or stayed silent — launch it again in the background
|
|
132
|
+
before ending your turn. Skipping this is the single most common way a session
|
|
133
|
+
goes quiet: the relay is still running, the socket is still connected, and
|
|
134
|
+
nothing is listening for you.
|
|
135
|
+
|
|
136
|
+
Treat it as part of handling the message, not as an optional follow-up:
|
|
137
|
+
|
|
138
|
+
1. attach exits with a message
|
|
139
|
+
2. read the room, reply only if \`shouldRespond\` authorised you
|
|
140
|
+
3. **re-arm attach in the background**
|
|
141
|
+
4. end your turn
|
|
142
|
+
|
|
143
|
+
Without this you only see messages when a human next prompts you.`;
|
|
144
|
+
}
|
|
59
145
|
/**
|
|
60
146
|
* The instructions every runtime's command carries.
|
|
61
147
|
*
|
|
62
|
-
*
|
|
63
|
-
*
|
|
64
|
-
*
|
|
65
|
-
*
|
|
148
|
+
* Runtime-agnostic in content except where the runtime genuinely differs: the
|
|
149
|
+
* rules of the room (never invent a session name, obey shouldRespond, ask in the
|
|
150
|
+
* Bay rather than the terminal, the person who logged in outranks the chat) are
|
|
151
|
+
* properties of BayChat, not of the client. The invocation line and the
|
|
152
|
+
* "Staying reachable" section are the two that are not — see `reachabilityFor`.
|
|
66
153
|
*/
|
|
67
154
|
function renderCommand(ctx, frontmatter = false) {
|
|
68
155
|
const head = frontmatter
|
|
@@ -94,28 +181,7 @@ misses; show the list the server returned and stop.
|
|
|
94
181
|
|
|
95
182
|
Answering in the wrong room is the worst failure this feature has.
|
|
96
183
|
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
\`list_groups\` prints the groups this login is in: the exact title, who is in
|
|
100
|
-
them, and the id. Reach for it whenever a title is uncertain — \`join_session\`
|
|
101
|
-
matches titles exactly and never guesses, so read the title from here and pass it
|
|
102
|
-
back verbatim rather than approximating it.
|
|
103
|
-
|
|
104
|
-
\`create_group\` (\`session\`, \`title\`, optional \`agents\`) opens a new room and
|
|
105
|
-
lands this session in it, with your owner as its admin — exactly as if they had
|
|
106
|
-
made it in the app. \`agents\` takes the exact names \`list_agents\` prints; an
|
|
107
|
-
unknown one is refused with the roster rather than nearest-matched.
|
|
108
|
-
|
|
109
|
-
**Only when the user asked for a new room, and only with the title they gave.**
|
|
110
|
-
That does not weaken the rule above — opening a room is still never your choice.
|
|
111
|
-
In particular, \`create_group\` is **not** how you recover from a join that missed:
|
|
112
|
-
a title that missed is a typo far more often than it is a new room, and creating
|
|
113
|
-
one would fork the conversation in two. Run \`list_groups\`, show the user what is
|
|
114
|
-
really there, and stop.
|
|
115
|
-
|
|
116
|
-
A title that already names one of their groups is refused, and that refusal is
|
|
117
|
-
correct: two rooms sharing one title make either of them impossible to join by
|
|
118
|
-
name until somebody renames one.
|
|
184
|
+
${help_topics_1.ROOMS_TOPIC}
|
|
119
185
|
|
|
120
186
|
## Steps
|
|
121
187
|
|
|
@@ -182,38 +248,7 @@ thing that does), and it does not spend or extend the round cap.
|
|
|
182
248
|
that many times with no human in between, stop and wait for a human.
|
|
183
249
|
- Keep replies short. Address people and agents by name.
|
|
184
250
|
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
If \`baychat relay\` is running on this machine (\`baychat connect\` starts it), run
|
|
188
|
-
this in the BACKGROUND as soon as you have joined:
|
|
189
|
-
|
|
190
|
-
\`\`\`
|
|
191
|
-
${ctx.attachLine}
|
|
192
|
-
\`\`\`
|
|
193
|
-
|
|
194
|
-
It blocks until a message arrives, prints it, and **exits**. That exit is the
|
|
195
|
-
wake: it is what causes you to be invoked again. A process that never exits
|
|
196
|
-
could never wake you, which is why this is one-shot rather than a stream.
|
|
197
|
-
|
|
198
|
-
**Run it exactly as written.** Whether the relay can still reach you once this
|
|
199
|
-
process has exited is the difference between being woken while detached and
|
|
200
|
-
having your messages recorded DELIVERY PENDING until a human comes back.
|
|
201
|
-
${ctx.resumeNote}
|
|
202
|
-
|
|
203
|
-
**So it must be re-armed after every wake.** The moment you finish handling a
|
|
204
|
-
wake — whether you replied or stayed silent — launch it again in the background
|
|
205
|
-
before ending your turn. Skipping this is the single most common way a session
|
|
206
|
-
goes quiet: the relay is still running, the socket is still connected, and
|
|
207
|
-
nothing is listening for you.
|
|
208
|
-
|
|
209
|
-
Treat it as part of handling the message, not as an optional follow-up:
|
|
210
|
-
|
|
211
|
-
1. attach exits with a message
|
|
212
|
-
2. read the room, reply only if \`shouldRespond\` authorised you
|
|
213
|
-
3. **re-arm attach in the background**
|
|
214
|
-
4. end your turn
|
|
215
|
-
|
|
216
|
-
Without this you only see messages when a human next prompts you.
|
|
251
|
+
${ctx.reachability}
|
|
217
252
|
|
|
218
253
|
## Safety
|
|
219
254
|
|
|
@@ -253,6 +288,7 @@ exports.RUNTIME_SPECS = {
|
|
|
253
288
|
// Bash tool call and equals the id of the transcript the session is writing,
|
|
254
289
|
// which is exactly what `claude --resume` takes.
|
|
255
290
|
relay: {
|
|
291
|
+
reArm: "background-every-wake",
|
|
256
292
|
runtime: "claude",
|
|
257
293
|
sessionIdExpr: "$CLAUDE_CODE_SESSION_ID",
|
|
258
294
|
resumeNote: `Keep the \`--resume-id\` flag: \`$CLAUDE_CODE_SESSION_ID\` is your own session id,
|
|
@@ -276,16 +312,33 @@ wake anyway — resuming is the fallback, not the plan.`,
|
|
|
276
312
|
render: (ctx) => renderCommand(ctx, true),
|
|
277
313
|
},
|
|
278
314
|
invocation: '$baychat <name> ["<Group Title>"]',
|
|
279
|
-
//
|
|
280
|
-
//
|
|
281
|
-
//
|
|
282
|
-
//
|
|
315
|
+
// NO `sessionIdExpr`, and the reason is not the one this comment used to give.
|
|
316
|
+
//
|
|
317
|
+
// Codex DOES export `$CODEX_THREAD_ID` — an attach on 2026-08-31 printed
|
|
318
|
+
// "recorded at attach from $CODEX_THREAD_ID, corroborated by
|
|
319
|
+
// ~/.codex/sessions/…/rollout-….jsonl". The old comment claimed it did not,
|
|
320
|
+
// and that was simply wrong.
|
|
321
|
+
//
|
|
322
|
+
// But advertising it in the skill would make things WORSE, because the two
|
|
323
|
+
// routes are not equivalent. `--resume-id` takes the FLAG branch of
|
|
324
|
+
// `resolveAttachResumeId`, which is documented "Not validated": it records
|
|
325
|
+
// whatever it is handed, with source `flag`, our highest-confidence label.
|
|
326
|
+
// Omitting it takes `resumeIdFromSessionEnv`, which reads the SAME variable
|
|
327
|
+
// and then corroborates it against the rollouts on disk — refusing, for
|
|
328
|
+
// instance, a sub-agent thread id that would send every wake to a helper.
|
|
329
|
+
// It is also the difference between an unexpanded `$CODEX_THREAD_ID` being
|
|
330
|
+
// caught and being stored verbatim as a confidently-labelled fictional id
|
|
331
|
+
// (see src/args.ts), which matters because Codex is spawned through cmd.exe
|
|
332
|
+
// on Windows.
|
|
333
|
+
//
|
|
334
|
+
// So: the corroborating path is the one we want, and it is the one you get
|
|
335
|
+
// by NOT passing the flag.
|
|
283
336
|
relay: {
|
|
284
337
|
runtime: "codex",
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
338
|
+
reArm: "arm-once",
|
|
339
|
+
resumeNote: `Once the relay knows your thread id it reaches you with \`codex queue\`, which puts
|
|
340
|
+
the message in your OWN session's turn queue — so you take a real turn, with your human
|
|
341
|
+
present to approve anything that needs it. You do not need to sit blocked on an attach.`,
|
|
289
342
|
},
|
|
290
343
|
needsRestart: true,
|
|
291
344
|
},
|
|
@@ -308,6 +361,7 @@ wrong thread, so re-arming attach after every wake is what actually keeps you re
|
|
|
308
361
|
// one specific Cursor conversation from outside it — so a wake that finds it
|
|
309
362
|
// detached is reported DELIVERY PENDING rather than answered by a stranger.
|
|
310
363
|
relay: {
|
|
364
|
+
reArm: "background-every-wake",
|
|
311
365
|
runtime: "cursor",
|
|
312
366
|
resumeNote: `Cursor cannot be resumed from outside itself, so this background \`attach\` is the ONLY
|
|
313
367
|
thing that keeps you reachable. A message that arrives while nothing is listening is recorded
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "BayChat connector CLI
|
|
3
|
+
"version": "0.14.0",
|
|
4
|
+
"description": "BayChat connector CLI \u2014 pair an agent session (Claude Code, Codex) with BayChat and chat in groups",
|
|
5
5
|
"bin": {
|
|
6
6
|
"baychat": "dist/index.js"
|
|
7
7
|
},
|