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,197 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* `baychat help <topic>` — the instructions a PERSON needs, in the terminal.
|
|
4
|
+
*
|
|
5
|
+
* WHY THIS FILE EXISTS. The group tools shipped on 2026-08-25 and were written up in
|
|
6
|
+
* five places: the connect guide (served at baychat.io/connect.md), the agent protocol,
|
|
7
|
+
* the npm README, the skill this CLI writes into a client, and the API's own tool
|
|
8
|
+
* descriptions. Every one of those is read by an AGENT, by a stranger evaluating
|
|
9
|
+
* BayChat, or by somebody with a browser open.
|
|
10
|
+
*
|
|
11
|
+
* None of them is reachable from the terminal the person is actually sitting in.
|
|
12
|
+
* `baychat --help` lists CLI SUBCOMMANDS, and `list_groups` is not one — it is an MCP
|
|
13
|
+
* tool their client calls. So the honest answer to "how do I make a group?" was: read a
|
|
14
|
+
* website. That is how a user ends up reporting that a capability does not exist when it
|
|
15
|
+
* shipped weeks ago.
|
|
16
|
+
*
|
|
17
|
+
* ONE SOURCE, NOT A SIXTH COPY. `ROOMS_TOPIC` below is spliced verbatim into the skill
|
|
18
|
+
* `runtimes.ts` writes, so the words a person reads here and the words their agent was
|
|
19
|
+
* given are the same words. A rule written twice is a rule that will one day be true in
|
|
20
|
+
* only one of the two places.
|
|
21
|
+
*/
|
|
22
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
23
|
+
exports.HELP_TOPICS = exports.ROOMS_TOPIC = void 0;
|
|
24
|
+
exports.findTopic = findTopic;
|
|
25
|
+
exports.topicIndex = topicIndex;
|
|
26
|
+
/**
|
|
27
|
+
* The rooms guidance, shared with the skill in `runtimes.ts`.
|
|
28
|
+
*
|
|
29
|
+
* Kept as a plain string with no template placeholders precisely so it can be used in
|
|
30
|
+
* both places unchanged — the moment it needs interpolation it stops being one text.
|
|
31
|
+
*/
|
|
32
|
+
exports.ROOMS_TOPIC = `## Rooms — find one, or open one
|
|
33
|
+
|
|
34
|
+
\`list_groups\` prints the groups this login is in: the exact title, who is in
|
|
35
|
+
them, and the id. Reach for it whenever a title is uncertain — \`join_session\`
|
|
36
|
+
matches titles exactly and never guesses, so read the title from here and pass it
|
|
37
|
+
back verbatim rather than approximating it.
|
|
38
|
+
|
|
39
|
+
\`create_group\` (\`session\`, \`title\`, optional \`agents\`) opens a new room and
|
|
40
|
+
lands this session in it, with your owner as its admin — exactly as if they had
|
|
41
|
+
made it in the app. \`agents\` takes the exact names \`list_agents\` prints; an
|
|
42
|
+
unknown one is refused with the roster rather than nearest-matched.
|
|
43
|
+
|
|
44
|
+
**Only when the user asked for a new room, and only with the title they gave.**
|
|
45
|
+
That does not weaken the rule above — opening a room is still never your choice.
|
|
46
|
+
In particular, \`create_group\` is **not** how you recover from a join that missed:
|
|
47
|
+
a title that missed is a typo far more often than it is a new room, and creating
|
|
48
|
+
one would fork the conversation in two. Run \`list_groups\`, show the user what is
|
|
49
|
+
really there, and stop.
|
|
50
|
+
|
|
51
|
+
A title that already names one of their groups is refused, and that refusal is
|
|
52
|
+
correct: two rooms sharing one title make either of them impossible to join by
|
|
53
|
+
name until somebody renames one.`;
|
|
54
|
+
const GROUPS = {
|
|
55
|
+
name: "groups",
|
|
56
|
+
summary: "Make a group, or find the ones you are already in",
|
|
57
|
+
keywords: ["group", "groups", "room", "rooms", "create_group", "list_groups", "new group", "channel"],
|
|
58
|
+
body: `${exports.ROOMS_TOPIC}
|
|
59
|
+
|
|
60
|
+
## Where these tools live, and why yours may not have them
|
|
61
|
+
|
|
62
|
+
\`list_groups\` and \`create_group\` are NOT CLI subcommands — you never type them
|
|
63
|
+
into a shell. They are MCP tools your client (Claude Code, Codex, Cursor,
|
|
64
|
+
Desktop) calls on your behalf. You ask in words; the agent makes the call.
|
|
65
|
+
|
|
66
|
+
You: "make a group called Ad Review and put Codex in it"
|
|
67
|
+
→ create_group({ session: "<your session>", title: "Ad Review", agents: ["Codex"] })
|
|
68
|
+
|
|
69
|
+
They are on the SESSION branch only, which means they need a device credential —
|
|
70
|
+
a \`bay_u_\` token from \`baychat login\`. Every call also carries a required
|
|
71
|
+
\`session\` argument, because a terminal has no single agent identity: each call
|
|
72
|
+
names the session it is acting as.
|
|
73
|
+
|
|
74
|
+
**A standing agent gets neither tool, deliberately.** An agent authenticated with
|
|
75
|
+
a \`bay_\` token is a guest in a room somebody else composed. Letting it create
|
|
76
|
+
rooms would let it invent a room, put the agents it likes in it, and talk to them
|
|
77
|
+
unobserved — the escalation the security model exists to prevent. If your agent
|
|
78
|
+
says it cannot create a group, that is correct behaviour, not a bug: ask a person,
|
|
79
|
+
or run it as a session.
|
|
80
|
+
|
|
81
|
+
## If your client cannot see them
|
|
82
|
+
|
|
83
|
+
1. \`baychat login\` — mints the device credential and registers the MCP server.
|
|
84
|
+
Without this you are on the agent branch and the tools are genuinely absent.
|
|
85
|
+
2. Update: \`npx baychat@latest login\`. A client installed before 2026-08-28 was
|
|
86
|
+
written a skill listing ten tools, from a build published on 1 August that
|
|
87
|
+
predates these two entirely.
|
|
88
|
+
3. Restart your client. Tool lists are read once at connect — a running session
|
|
89
|
+
keeps the list it started with, however current the server is.
|
|
90
|
+
4. \`baychat help tools\` shows what each branch actually gets.`,
|
|
91
|
+
};
|
|
92
|
+
const TOOLS = {
|
|
93
|
+
name: "tools",
|
|
94
|
+
summary: "Every MCP tool, and which credential it needs",
|
|
95
|
+
keywords: ["tool", "tools", "mcp", "skills", "capabilities", "what can it do"],
|
|
96
|
+
body: `## The two branches
|
|
97
|
+
|
|
98
|
+
Which tools you get depends on WHAT YOU ARE, not on which client you use.
|
|
99
|
+
|
|
100
|
+
**Agent branch** — a standing agent holding a \`bay_\` token. Thirteen tools:
|
|
101
|
+
|
|
102
|
+
list_conversations get_room_context get_conversation_summary
|
|
103
|
+
get_messages send_message set_typing
|
|
104
|
+
react_to_message list_files get_file
|
|
105
|
+
web_search web_fetch list_agents
|
|
106
|
+
ask_connector
|
|
107
|
+
|
|
108
|
+
Plus the \`baychat://protocol\` resource, which serves the full agent protocol.
|
|
109
|
+
|
|
110
|
+
**Session branch** — a person's terminal, holding a \`bay_u_\` device credential
|
|
111
|
+
from \`baychat login\`. Gets all thirteen above, each with a REQUIRED \`session\`
|
|
112
|
+
argument, and these on top:
|
|
113
|
+
|
|
114
|
+
join_session list_sessions end_session
|
|
115
|
+
list_groups create_group
|
|
116
|
+
request_approval await_approval
|
|
117
|
+
create_upload_url whoami
|
|
118
|
+
|
|
119
|
+
The extra ones are things a PERSON does: name a terminal, park it, see their
|
|
120
|
+
rooms, open a new one, be asked a yes/no question on their phone. An agent token
|
|
121
|
+
never reaches them.
|
|
122
|
+
|
|
123
|
+
If you are counting tools and getting ten, your client is running a build from
|
|
124
|
+
before 2026-08-28 — see \`baychat help groups\`.`,
|
|
125
|
+
};
|
|
126
|
+
const SESSIONS = {
|
|
127
|
+
name: "sessions",
|
|
128
|
+
summary: "Name this terminal, join a room as it, park it when done",
|
|
129
|
+
keywords: ["session", "sessions", "join", "join_session", "attach", "terminal", "park", "end_session"],
|
|
130
|
+
body: `## Sessions
|
|
131
|
+
|
|
132
|
+
A session is one terminal, named by you. It appears in the app as an agent your
|
|
133
|
+
messages can reach, and it survives being parked.
|
|
134
|
+
|
|
135
|
+
join_session({ session: "Session-A" }) → a 1:1 with you
|
|
136
|
+
join_session({ session: "Session-A", group: "Ad Review" }) → that group INSTEAD
|
|
137
|
+
|
|
138
|
+
**The group form joins that group and NOT the 1:1** — this catches people out. A
|
|
139
|
+
message you send in the 1:1 lands somewhere a group-joined session cannot see, so
|
|
140
|
+
it reads as the agent ignoring you.
|
|
141
|
+
|
|
142
|
+
list_sessions() → name, live or idle, last seen
|
|
143
|
+
end_session(...) → park it; the chat and its history survive, and rejoining
|
|
144
|
+
the same name revives the same agent
|
|
145
|
+
|
|
146
|
+
**Never invent a session name.** The user names the session and the user names
|
|
147
|
+
the group. Given neither, run \`list_sessions\` and stop — do not derive a name
|
|
148
|
+
from the directory, the repo, the branch, or the hostname. Answering in the wrong
|
|
149
|
+
room is the worst failure this feature has.`,
|
|
150
|
+
};
|
|
151
|
+
const APPROVALS = {
|
|
152
|
+
name: "approvals",
|
|
153
|
+
summary: "Ask a yes/no question that lands on your phone",
|
|
154
|
+
keywords: ["approval", "approvals", "permission", "request_approval", "await_approval", "decision", "hook"],
|
|
155
|
+
body: `## Approvals
|
|
156
|
+
|
|
157
|
+
\`request_approval\` puts a decision card on the owner's phone; \`await_approval\`
|
|
158
|
+
blocks until they answer. Session branch only — a standing agent has no owner to
|
|
159
|
+
ask and no terminal to block.
|
|
160
|
+
|
|
161
|
+
There is no timeout by design. A question worth asking is worth waiting for, and
|
|
162
|
+
a decision that expires silently is worse than one that waits.
|
|
163
|
+
|
|
164
|
+
\`baychat approve-hook\` wires Claude Code's own permission prompts to the same
|
|
165
|
+
cards. It is NOT on by default and moves the last line between an agent and your
|
|
166
|
+
machine onto a phone — read docs/features/REMOTE_APPROVAL_HOOK.md before enabling
|
|
167
|
+
it. It fails CLOSED: every error denies.`,
|
|
168
|
+
};
|
|
169
|
+
exports.HELP_TOPICS = [GROUPS, SESSIONS, TOOLS, APPROVALS];
|
|
170
|
+
/** Exact name first, then keyword, then a substring of the body. */
|
|
171
|
+
function findTopic(query) {
|
|
172
|
+
const q = query.trim().toLowerCase();
|
|
173
|
+
if (!q)
|
|
174
|
+
return undefined;
|
|
175
|
+
const exact = exports.HELP_TOPICS.find((t) => t.name === q);
|
|
176
|
+
if (exact)
|
|
177
|
+
return exact;
|
|
178
|
+
const keyed = exports.HELP_TOPICS.find((t) => t.keywords.some((k) => k === q));
|
|
179
|
+
if (keyed)
|
|
180
|
+
return keyed;
|
|
181
|
+
// Substring over keywords, so "make a group" and "new room" both land.
|
|
182
|
+
const loose = exports.HELP_TOPICS.find((t) => t.keywords.some((k) => q.includes(k) || k.includes(q)));
|
|
183
|
+
if (loose)
|
|
184
|
+
return loose;
|
|
185
|
+
return exports.HELP_TOPICS.find((t) => t.body.toLowerCase().includes(q));
|
|
186
|
+
}
|
|
187
|
+
/** The index printed by `baychat help` with no topic, and on a miss. */
|
|
188
|
+
function topicIndex() {
|
|
189
|
+
const rows = exports.HELP_TOPICS.map((t) => ` baychat help ${t.name.padEnd(10)} ${t.summary}`);
|
|
190
|
+
return [
|
|
191
|
+
"Topics — how to actually use BayChat from a client:",
|
|
192
|
+
"",
|
|
193
|
+
...rows,
|
|
194
|
+
"",
|
|
195
|
+
"Any wording works: `baychat help \"make a new group\"` finds the groups topic.",
|
|
196
|
+
].join("\n");
|
|
197
|
+
}
|
package/dist/index.js
CHANGED
|
@@ -3,10 +3,13 @@
|
|
|
3
3
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
4
4
|
const commands_1 = require("./commands");
|
|
5
5
|
const approve_hook_1 = require("./approve-hook");
|
|
6
|
+
const doctor_command_1 = require("./doctor-command");
|
|
6
7
|
const connect_1 = require("./connect");
|
|
7
8
|
const mcp_1 = require("./mcp");
|
|
8
9
|
const mcp_config_1 = require("./mcp-config");
|
|
9
10
|
const commands_2 = require("./relay/commands");
|
|
11
|
+
const help_topics_1 = require("./help-topics");
|
|
12
|
+
const args_1 = require("./args");
|
|
10
13
|
const HELP = `baychat — BayChat connector CLI for agent sessions (Claude Code, Codex)
|
|
11
14
|
|
|
12
15
|
Usage:
|
|
@@ -27,6 +30,12 @@ Usage:
|
|
|
27
30
|
baychat pair <code> [--base <url>] Redeem a pairing code from the BayChat app
|
|
28
31
|
baychat link [--name <n>] [--base <url>]
|
|
29
32
|
Link this session via a QR you scan with your phone
|
|
33
|
+
baychat help [topic|question] How to USE BayChat from a client — making a
|
|
34
|
+
group, joining a room, which tools your
|
|
35
|
+
credential gets. Ask it however you like:
|
|
36
|
+
\`baychat help "make a new group"\`. These are
|
|
37
|
+
MCP tools your client calls, not subcommands,
|
|
38
|
+
so they are not in the list below
|
|
30
39
|
baychat whoami Show the connected agent identity
|
|
31
40
|
baychat conversations List conversations this agent is in
|
|
32
41
|
baychat send <conversationId> <text> Send a message
|
|
@@ -76,6 +85,14 @@ Usage:
|
|
|
76
85
|
came from, so "we can wake this headlessly"
|
|
77
86
|
is a claim you can check
|
|
78
87
|
baychat relay stop Stop the relay and disable it at boot
|
|
88
|
+
baychat doctor [--json] Check every link between this machine and
|
|
89
|
+
BayChat — credential, relay, and per runtime
|
|
90
|
+
its MCP registration, skill, executable and
|
|
91
|
+
live session — and print exactly what to type
|
|
92
|
+
for each thing that is wrong. Exit 0 clear,
|
|
93
|
+
1 broken, 2 messages nothing answered.
|
|
94
|
+
--json emits the same report as data, for
|
|
95
|
+
pasting into a support thread
|
|
79
96
|
baychat approve-hook [--session <name>] [--timeout <sec>]
|
|
80
97
|
Claude Code PermissionRequest hook: send the
|
|
81
98
|
permission prompt to BayChat as a decision card
|
|
@@ -101,46 +118,45 @@ Usage:
|
|
|
101
118
|
Connect flow: in BayChat, open the agent -> Connect -> copy the pairing code,
|
|
102
119
|
then run \`baychat pair <code>\`. Pairing rotates the agent token; use a
|
|
103
120
|
dedicated agent per session.`;
|
|
104
|
-
function flag(args, name) {
|
|
105
|
-
const i = args.indexOf(name);
|
|
106
|
-
return i >= 0 && i + 1 < args.length ? args[i + 1] : undefined;
|
|
107
|
-
}
|
|
108
|
-
/** The first positional (non `--flag`) argument, so a command's id isn't shadowed
|
|
109
|
-
* by a leading boolean flag like `--catch-up`/`--refresh`. */
|
|
110
|
-
function positional(args) {
|
|
111
|
-
return args.find((a) => !a.startsWith("--"));
|
|
112
|
-
}
|
|
113
121
|
/** Drop a `--name <value>` pair, so a multi-word positional (a search query)
|
|
114
|
-
* doesn't swallow the flag's value as part of itself.
|
|
122
|
+
* doesn't swallow the flag's value as part of itself.
|
|
123
|
+
*
|
|
124
|
+
* Only the flag is dropped when its value is missing — the next token is
|
|
125
|
+
* another option, or there is none. Consuming two entries there would eat an
|
|
126
|
+
* unrelated argument, the same mistake `flag` refuses to make. */
|
|
115
127
|
function withoutFlag(args, name) {
|
|
116
128
|
const i = args.indexOf(name);
|
|
117
|
-
|
|
129
|
+
if (i < 0)
|
|
130
|
+
return args;
|
|
131
|
+
const next = args[i + 1];
|
|
132
|
+
const consumed = next !== undefined && !next.startsWith("--") ? 2 : 1;
|
|
133
|
+
return [...args.slice(0, i), ...args.slice(i + consumed)];
|
|
118
134
|
}
|
|
119
135
|
/** A flag's value as a number, or undefined when absent. A non-numeric value
|
|
120
136
|
* becomes NaN and is rejected by the tool's own bounds check with a message
|
|
121
137
|
* that names the field. */
|
|
122
138
|
function numberFlag(args, name) {
|
|
123
|
-
const raw = flag(args, name);
|
|
139
|
+
const raw = (0, args_1.flag)(args, name);
|
|
124
140
|
return raw === undefined ? undefined : Number(raw);
|
|
125
141
|
}
|
|
126
142
|
async function main() {
|
|
127
143
|
const [command, ...args] = process.argv.slice(2);
|
|
128
144
|
switch (command) {
|
|
129
145
|
case "onboard":
|
|
130
|
-
await (0, commands_1.cmdOnboard)(positional(args), { catchUp: args.includes("--catch-up") });
|
|
146
|
+
await (0, commands_1.cmdOnboard)((0, args_1.positional)(args), { catchUp: args.includes("--catch-up") });
|
|
131
147
|
return 0;
|
|
132
148
|
case "login": {
|
|
133
|
-
const loggedIn = await (0, commands_1.cmdLogin)({ base: flag(args, "--base"), token: flag(args, "--token") });
|
|
149
|
+
const loggedIn = await (0, commands_1.cmdLogin)({ base: (0, args_1.flag)(args, "--base"), token: (0, args_1.flag)(args, "--token") });
|
|
134
150
|
return loggedIn ? 0 : 2; // 2 = the link request expired without approval
|
|
135
151
|
}
|
|
136
152
|
case "pair": {
|
|
137
153
|
if (!args[0])
|
|
138
154
|
throw new Error("Usage: baychat pair <code>");
|
|
139
|
-
await (0, commands_1.cmdPair)(args[0], flag(args, "--base"));
|
|
155
|
+
await (0, commands_1.cmdPair)(args[0], (0, args_1.flag)(args, "--base"));
|
|
140
156
|
return 0;
|
|
141
157
|
}
|
|
142
158
|
case "link": {
|
|
143
|
-
const linked = await (0, commands_1.cmdLink)({ name: flag(args, "--name"), base: flag(args, "--base") });
|
|
159
|
+
const linked = await (0, commands_1.cmdLink)({ name: (0, args_1.flag)(args, "--name"), base: (0, args_1.flag)(args, "--base") });
|
|
144
160
|
return linked ? 0 : 2;
|
|
145
161
|
}
|
|
146
162
|
case "qr":
|
|
@@ -202,7 +218,7 @@ async function main() {
|
|
|
202
218
|
return 0;
|
|
203
219
|
}
|
|
204
220
|
case "summary": {
|
|
205
|
-
const conversationId = positional(args);
|
|
221
|
+
const conversationId = (0, args_1.positional)(args);
|
|
206
222
|
if (!conversationId)
|
|
207
223
|
throw new Error("Usage: baychat summary <conversationId> [--refresh]");
|
|
208
224
|
await (0, commands_1.cmdSummary)(conversationId, { refresh: args.includes("--refresh") });
|
|
@@ -216,7 +232,7 @@ async function main() {
|
|
|
216
232
|
return 0;
|
|
217
233
|
}
|
|
218
234
|
case "fetch": {
|
|
219
|
-
const url = positional(withoutFlag(args, "--max-chars"));
|
|
235
|
+
const url = (0, args_1.positional)(withoutFlag(args, "--max-chars"));
|
|
220
236
|
if (!url)
|
|
221
237
|
throw new Error("Usage: baychat fetch <url> [--max-chars <n>]");
|
|
222
238
|
await (0, commands_1.cmdFetch)(url, { maxChars: numberFlag(args, "--max-chars") });
|
|
@@ -225,8 +241,8 @@ async function main() {
|
|
|
225
241
|
case "watch": {
|
|
226
242
|
if (!args[0])
|
|
227
243
|
throw new Error("Usage: baychat watch <conversationId>");
|
|
228
|
-
const intervalSec = Number(flag(args, "--interval") ?? "5");
|
|
229
|
-
const timeoutSec = Number(flag(args, "--timeout") ?? "300");
|
|
244
|
+
const intervalSec = Number((0, args_1.flag)(args, "--interval") ?? "5");
|
|
245
|
+
const timeoutSec = Number((0, args_1.flag)(args, "--timeout") ?? "300");
|
|
230
246
|
const got = await (0, commands_1.cmdWatch)(args[0], {
|
|
231
247
|
intervalMs: intervalSec * 1000,
|
|
232
248
|
timeoutMs: timeoutSec * 1000,
|
|
@@ -234,7 +250,7 @@ async function main() {
|
|
|
234
250
|
return got ? 0 : 2;
|
|
235
251
|
}
|
|
236
252
|
case "relay": {
|
|
237
|
-
const sub = positional(args) ?? "";
|
|
253
|
+
const sub = (0, args_1.positional)(args) ?? "";
|
|
238
254
|
const rest = args.filter((a) => a !== sub);
|
|
239
255
|
switch (sub) {
|
|
240
256
|
case "start":
|
|
@@ -245,15 +261,15 @@ async function main() {
|
|
|
245
261
|
case "stop":
|
|
246
262
|
return await (0, commands_2.cmdRelayStop)();
|
|
247
263
|
case "attach": {
|
|
248
|
-
const session = flag(rest, "--session");
|
|
264
|
+
const session = (0, args_1.flag)(rest, "--session");
|
|
249
265
|
if (!session) {
|
|
250
266
|
throw new Error("Usage: baychat relay attach --session <name> [--runtime claude|codex|hermes] [--resume-id <id>] [--timeout <sec>]");
|
|
251
267
|
}
|
|
252
268
|
const timeoutSec = numberFlag(rest, "--timeout");
|
|
253
269
|
return await (0, commands_2.cmdRelayAttach)({
|
|
254
270
|
session,
|
|
255
|
-
runtime: flag(rest, "--runtime") ?? "claude",
|
|
256
|
-
resumeId: flag(rest, "--resume-id"),
|
|
271
|
+
runtime: (0, args_1.flag)(rest, "--runtime") ?? "claude",
|
|
272
|
+
resumeId: (0, args_1.flag)(rest, "--resume-id"),
|
|
257
273
|
timeoutMs: timeoutSec ? timeoutSec * 1000 : undefined,
|
|
258
274
|
});
|
|
259
275
|
}
|
|
@@ -267,15 +283,20 @@ async function main() {
|
|
|
267
283
|
// exit-1 hook with no JSON on stdout is a NON-BLOCKING error: the tool call proceeds. The
|
|
268
284
|
// guard in the catch below covers that anyway.
|
|
269
285
|
return await (0, approve_hook_1.cmdApproveHook)(args);
|
|
286
|
+
case "doctor":
|
|
287
|
+
// Deliberately placed with the setup commands: it is the thing you run
|
|
288
|
+
// when one of them did not take, and the one command that inspects all
|
|
289
|
+
// of them at once.
|
|
290
|
+
return await (0, doctor_command_1.cmdDoctor)(args);
|
|
270
291
|
case "connect": {
|
|
271
292
|
// A bare `connect` prints the client menu; positional() skips a leading flag
|
|
272
293
|
// so `connect --base x codex` still finds the client.
|
|
273
|
-
return await (0, connect_1.cmdConnect)(positional(args), { base: flag(args, "--base") });
|
|
294
|
+
return await (0, connect_1.cmdConnect)((0, args_1.positional)(args), { base: (0, args_1.flag)(args, "--base") });
|
|
274
295
|
}
|
|
275
296
|
case "mcp-config": {
|
|
276
297
|
// `--client` with no value is a typo, not a request for the menu: pass the
|
|
277
298
|
// empty string so it is rejected by name rather than silently listing.
|
|
278
|
-
(0, mcp_config_1.cmdMcpConfig)(args.includes("--client") ? (flag(args, "--client") ?? "") : undefined);
|
|
299
|
+
(0, mcp_config_1.cmdMcpConfig)(args.includes("--client") ? ((0, args_1.flag)(args, "--client") ?? "") : undefined);
|
|
279
300
|
return 0;
|
|
280
301
|
}
|
|
281
302
|
case "mcp": {
|
|
@@ -288,9 +309,30 @@ async function main() {
|
|
|
288
309
|
}
|
|
289
310
|
case "help":
|
|
290
311
|
case "--help":
|
|
291
|
-
case undefined:
|
|
312
|
+
case undefined: {
|
|
313
|
+
// `baychat help <anything>` answers a question about USING BayChat from a
|
|
314
|
+
// client — how to make a group, what tools you get, why yours are missing.
|
|
315
|
+
// Those are MCP tools, not subcommands, so they can never appear in the
|
|
316
|
+
// usage block above, and until this existed the only answer was "read a
|
|
317
|
+
// website" — which is how a shipped capability gets reported as absent.
|
|
318
|
+
const query = args.join(" ").trim();
|
|
319
|
+
if (query) {
|
|
320
|
+
const topic = (0, help_topics_1.findTopic)(query);
|
|
321
|
+
if (topic) {
|
|
322
|
+
console.log(topic.body);
|
|
323
|
+
return 0;
|
|
324
|
+
}
|
|
325
|
+
// A miss is not an error: they asked a real question and deserve the
|
|
326
|
+
// list rather than a usage dump.
|
|
327
|
+
console.error(`No help topic matches "${query}".\n`);
|
|
328
|
+
console.log((0, help_topics_1.topicIndex)());
|
|
329
|
+
return 1;
|
|
330
|
+
}
|
|
292
331
|
console.log(HELP);
|
|
332
|
+
console.log("");
|
|
333
|
+
console.log((0, help_topics_1.topicIndex)());
|
|
293
334
|
return 0;
|
|
335
|
+
}
|
|
294
336
|
default:
|
|
295
337
|
console.error(`Unknown command: ${command}`);
|
|
296
338
|
console.log(HELP);
|
package/dist/relay/adapters.js
CHANGED
|
@@ -7,7 +7,10 @@ exports.isKnownRuntime = isKnownRuntime;
|
|
|
7
7
|
exports.runHeadless = runHeadless;
|
|
8
8
|
const child_process_1 = require("child_process");
|
|
9
9
|
const attachments_1 = require("../attachments");
|
|
10
|
+
const codex_app_server_1 = require("./codex-app-server");
|
|
11
|
+
const codex_queue_1 = require("./codex-queue");
|
|
10
12
|
const resume_1 = require("./resume");
|
|
13
|
+
const spawn_env_1 = require("./spawn-env");
|
|
11
14
|
/** How long a headless turn may run before the relay gives up on it. */
|
|
12
15
|
const HEADLESS_TIMEOUT_MS = 10 * 60_000;
|
|
13
16
|
/**
|
|
@@ -18,7 +21,7 @@ const HEADLESS_TIMEOUT_MS = 10 * 60_000;
|
|
|
18
21
|
* relay carries the message and nothing more. Telling a resumed turn to "answer
|
|
19
22
|
* this" would route around the room's reply policy from outside the room.
|
|
20
23
|
*/
|
|
21
|
-
function buildWakePrompt(session, conversationId, batch) {
|
|
24
|
+
function buildWakePrompt(session, conversationId, batch, reArm) {
|
|
22
25
|
const lines = [];
|
|
23
26
|
for (const m of batch) {
|
|
24
27
|
const flag = m.shouldRespond ? " [shouldRespond=true]" : "";
|
|
@@ -40,8 +43,47 @@ function buildWakePrompt(session, conversationId, batch) {
|
|
|
40
43
|
...lines,
|
|
41
44
|
"",
|
|
42
45
|
`Re-read the room with the BayChat tools (session="${session}") before acting. Reply ONLY if the server marks shouldRespond for you; otherwise stay silent and end the turn.`,
|
|
46
|
+
...reArmLines(session, reArm),
|
|
43
47
|
].join("\n");
|
|
44
48
|
}
|
|
49
|
+
/**
|
|
50
|
+
* How this session gets woken a SECOND time.
|
|
51
|
+
*
|
|
52
|
+
* A headless resume is spawned by the daemon, so it inherits the daemon's
|
|
53
|
+
* environment — and under systemd that PATH contains neither `node` nor
|
|
54
|
+
* `baychat` nor the runtime itself. The installed skill tells a woken session to
|
|
55
|
+
* re-arm with a bare `baychat relay attach`, which a resumed session cannot run:
|
|
56
|
+
* command not found. It answers once and then goes quiet forever, which is
|
|
57
|
+
* precisely the failure re-arming exists to prevent — and it looks like the
|
|
58
|
+
* model forgetting rather than like us handing it an impossible instruction.
|
|
59
|
+
*
|
|
60
|
+
* So the daemon, which knows both absolute paths because it IS them, spells the
|
|
61
|
+
* command out. Omitted entirely when the caller has no paths to offer, rather
|
|
62
|
+
* than printed with holes in it.
|
|
63
|
+
*/
|
|
64
|
+
function reArmLines(session, reArm) {
|
|
65
|
+
if (!reArm)
|
|
66
|
+
return [];
|
|
67
|
+
const command = ` ${reArm.node} ${reArm.cli} relay attach --session "${session}" --runtime ${reArm.runtime}`;
|
|
68
|
+
if (reArm.transport === "fifo") {
|
|
69
|
+
return [
|
|
70
|
+
"",
|
|
71
|
+
"To stay reachable, run this IN THE FOREGROUND before ending your turn. It",
|
|
72
|
+
"blocks until a message arrives, prints it, and exits — that exit is the wake.",
|
|
73
|
+
"Do NOT put it in the background: your sandbox kills backgrounded processes",
|
|
74
|
+
"when the command returns, so a backgrounded attach listens to nothing while",
|
|
75
|
+
"looking like it worked.",
|
|
76
|
+
command,
|
|
77
|
+
];
|
|
78
|
+
}
|
|
79
|
+
return [
|
|
80
|
+
"",
|
|
81
|
+
"To stay reachable, run this in the BACKGROUND before ending your turn. It is",
|
|
82
|
+
"written with absolute paths because a resumed session's PATH may not contain",
|
|
83
|
+
"node or baychat at all:",
|
|
84
|
+
command,
|
|
85
|
+
];
|
|
86
|
+
}
|
|
45
87
|
/**
|
|
46
88
|
* One attachment line for the wake prompt: what it is, then how to fetch it.
|
|
47
89
|
*
|
|
@@ -91,6 +133,19 @@ const claudeAdapter = {
|
|
|
91
133
|
};
|
|
92
134
|
const codexAdapter = {
|
|
93
135
|
runtime: "codex",
|
|
136
|
+
queueMessage({ binaryPath, target, message }) {
|
|
137
|
+
// `codex queue` landed in 0.149.0. Delivering here rather than through the
|
|
138
|
+
// headless spawn is what lets a woken Codex actually REPLY: the message goes
|
|
139
|
+
// to the live session, where the human is present to approve, instead of a
|
|
140
|
+
// separate turn that must run `approvalPolicy: "never"` and is therefore
|
|
141
|
+
// blocked from calling BayChat's write tools at all.
|
|
142
|
+
return (0, codex_queue_1.queueToThread)({
|
|
143
|
+
binaryPath,
|
|
144
|
+
threadId: target.resumeId,
|
|
145
|
+
message,
|
|
146
|
+
cwd: target.resumeCwd ?? target.cwd,
|
|
147
|
+
});
|
|
148
|
+
},
|
|
94
149
|
canResume(target) {
|
|
95
150
|
if (!target.resumeId) {
|
|
96
151
|
return {
|
|
@@ -109,6 +164,29 @@ const codexAdapter = {
|
|
|
109
164
|
args: ["exec", "resume", target.resumeId, prompt],
|
|
110
165
|
};
|
|
111
166
|
},
|
|
167
|
+
/**
|
|
168
|
+
* Prefer `codex app-server`, which can say what `codex exec` never could.
|
|
169
|
+
*
|
|
170
|
+
* `exec resume` reports only an exit code, so "the thread id names nothing on
|
|
171
|
+
* this machine", "the turn stopped at an approval" and "the model was
|
|
172
|
+
* unavailable" all arrive as `exited 1`. app-server distinguishes them, and
|
|
173
|
+
* the reason is what a user needs to act.
|
|
174
|
+
*
|
|
175
|
+
* Opting out with BAYCHAT_CODEX_TRANSPORT=exec is deliberate: the app-server
|
|
176
|
+
* interface is marked `[experimental]` by OpenAI and its shape is not frozen,
|
|
177
|
+
* so there has to be a way back that does not require a new release.
|
|
178
|
+
*/
|
|
179
|
+
async runTurn({ binaryPath, target, prompt }) {
|
|
180
|
+
if (process.env.BAYCHAT_CODEX_TRANSPORT === "exec") {
|
|
181
|
+
return { kind: "failed", transportUnusable: true, reason: "BAYCHAT_CODEX_TRANSPORT=exec" };
|
|
182
|
+
}
|
|
183
|
+
return (0, codex_app_server_1.runCodexTurn)({
|
|
184
|
+
binaryPath,
|
|
185
|
+
threadId: target.resumeId,
|
|
186
|
+
prompt,
|
|
187
|
+
cwd: target.resumeCwd ?? target.cwd,
|
|
188
|
+
}, { spawn: child_process_1.spawn });
|
|
189
|
+
},
|
|
112
190
|
};
|
|
113
191
|
/**
|
|
114
192
|
* Cursor can be woken, and cannot be resumed. Both halves matter.
|
|
@@ -200,6 +278,9 @@ function runHeadless(file, args, opts = {}) {
|
|
|
200
278
|
return new Promise((resolve) => {
|
|
201
279
|
const child = (0, child_process_1.spawn)(file, args, {
|
|
202
280
|
cwd: opts.cwd,
|
|
281
|
+
// An npm-installed runtime is a script with a `#!/usr/bin/env node`
|
|
282
|
+
// shebang, and the daemon's systemd PATH has no node. See ./spawn-env.ts.
|
|
283
|
+
env: (0, spawn_env_1.headlessSpawnEnv)(),
|
|
203
284
|
shell: false,
|
|
204
285
|
stdio: ["ignore", "ignore", "pipe"],
|
|
205
286
|
detached: false,
|