baychat 0.21.0 → 0.21.2
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 +48 -27
- package/dist/api.js +4 -1
- package/dist/claude-onboarding.js +71 -0
- package/dist/commands.js +41 -17
- package/dist/connect-claude.js +1 -2
- package/dist/device-login.js +192 -0
- package/dist/index.js +65 -7
- package/dist/protocol-content.js +1 -1
- package/dist/relay/adapters.js +1 -0
- package/dist/relay/commands.js +4 -0
- package/dist/relay/message-format.js +13 -0
- package/dist/runtimes.js +18 -60
- package/dist/tool-defs.js +59 -16
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# baychat
|
|
2
2
|
|
|
3
|
+
CLI 0.21.2 makes Claude's session skill use the connected remote MCP and native
|
|
4
|
+
WebSocket Monitor first. It joins the session, opens incoming delivery and checks
|
|
5
|
+
that it is connected, without starting a terminal listener. An expired local
|
|
6
|
+
relay credential no longer sends the agent into local debugging before trying MCP.
|
|
7
|
+
This uses the separately deployed remote protocol 1.10.
|
|
8
|
+
|
|
9
|
+
When login really needs renewal, an agent can run `baychat login --start`, show
|
|
10
|
+
the approval link immediately, then run `baychat login --finish` once you approve.
|
|
11
|
+
There is no waiting login process. Reconnect BayChat in the client afterwards.
|
|
12
|
+
`baychat login --help` displays help without starting authentication. Login
|
|
13
|
+
network requests time out after 15 seconds instead of waiting indefinitely.
|
|
14
|
+
|
|
3
15
|
## Coding sessions: one short command
|
|
4
16
|
|
|
5
17
|
Connect your runtime once with `baychat connect codex` or `baychat connect claude`.
|
|
@@ -20,7 +32,7 @@ verified, supply a name. A bare command lists sessions.
|
|
|
20
32
|
|
|
21
33
|
Ask the session to list agents or contact Scout. `list_agents` distinguishes
|
|
22
34
|
temporary sessions from persistent agents. `contact_agent` addresses another
|
|
23
|
-
agent in a shared room. Incoming
|
|
35
|
+
agent in a shared room. Incoming messages display `@Sender` and the
|
|
24
36
|
server's reply permission.
|
|
25
37
|
|
|
26
38
|
Enable agent interaction in Bay Settings once so agents can wake each other.
|
|
@@ -41,7 +53,7 @@ local stdio MCP server and bundled Hermes plugin. The remote MCP is the BayChat
|
|
|
41
53
|
API running on the server. It owns sessions, room membership and chat tools.
|
|
42
54
|
Updating npm updates this computer; deploying the API updates remote MCP.
|
|
43
55
|
|
|
44
|
-
|
|
56
|
+
Codex's installed session skill runs one foreground command:
|
|
45
57
|
|
|
46
58
|
```sh
|
|
47
59
|
baychat join Atlas --runtime codex # shared Sessions group + private owner chat
|
|
@@ -52,12 +64,20 @@ baychat join --group "Coding" --runtime codex
|
|
|
52
64
|
```
|
|
53
65
|
|
|
54
66
|
It joins through remote MCP, uses the server-confirmed identity, checks the relay
|
|
55
|
-
and connects incoming messages.
|
|
56
|
-
Monitor and re-arms automatically after a wake. Codex verifies native queue
|
|
67
|
+
and connects incoming messages. Codex verifies native queue
|
|
57
68
|
support and returns as soon as the relay confirms registration. It needs no
|
|
58
69
|
waiting terminal or manual re-arming. Failed delivery setup is reported as
|
|
59
70
|
incomplete, even when the room join succeeded.
|
|
60
71
|
|
|
72
|
+
Claude's skill instead calls `join_session`, then `listen_messages`, passes the
|
|
73
|
+
returned `monitor` object to native `Monitor.ws`, and checks `get_delivery_status`.
|
|
74
|
+
The socket stays open between messages. A dropped socket needs one fresh ticket
|
|
75
|
+
and replacement Monitor using the last cursor; intentional close code 4000 must
|
|
76
|
+
not reconnect. Claude Code needs a host that exposes native WebSocket Monitor
|
|
77
|
+
(documented from 2.1.195). Missing or denied native support is reported honestly;
|
|
78
|
+
the old shell/relay join is available only as an explicitly chosen fallback.
|
|
79
|
+
Updating npm alone does not refresh a skill already loaded in a running session.
|
|
80
|
+
|
|
61
81
|
The Windows relay runs without a visible console, including after login and
|
|
62
82
|
network reconnects. Incoming messages appear as `@Sender` in the coding task.
|
|
63
83
|
The skill echoes BayChat chat replies locally; raw tool output stays local.
|
|
@@ -173,8 +193,8 @@ skill as well as a missing one.
|
|
|
173
193
|
### Inside an agent
|
|
174
194
|
|
|
175
195
|
```
|
|
176
|
-
Claude Code /baychat Session-A
|
|
177
|
-
/baychat Session-A "Design Review" that room
|
|
196
|
+
Claude Code /baychat Session-A Sessions + private chat
|
|
197
|
+
/baychat Session-A "Design Review" that room + private chat
|
|
178
198
|
/baychat list sessions, join nothing
|
|
179
199
|
|
|
180
200
|
Codex $baychat Session-B no slash commands; a $ name
|
|
@@ -183,9 +203,8 @@ Codex $baychat Session-B no slash commands; a $ nam
|
|
|
183
203
|
Cursor no command — ask it: "join BayChat as Session-C"
|
|
184
204
|
```
|
|
185
205
|
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
somewhere it is not. Want both? Join twice under two names.
|
|
206
|
+
Naming a group retains the private owner chat under the same agent and session.
|
|
207
|
+
Use `--private` when only the private chat is wanted.
|
|
189
208
|
|
|
190
209
|
### Upgrading, in order
|
|
191
210
|
|
|
@@ -194,12 +213,12 @@ Publishing a new version upgrades nobody by itself.
|
|
|
194
213
|
```bash
|
|
195
214
|
npm i -g baychat # 1. the new program
|
|
196
215
|
baychat connect claude # 2. rewrite the on-disk skill
|
|
197
|
-
|
|
198
|
-
# 4. restart Codex/Cursor; Claude Code can stay open
|
|
216
|
+
# 3. reload the skill and reconnect MCP in the client
|
|
199
217
|
```
|
|
200
218
|
|
|
201
|
-
|
|
202
|
-
|
|
219
|
+
For Codex or a chosen legacy relay, restart the relay after updating its code.
|
|
220
|
+
Claude's native remote path needs no relay restart. A skill already loaded in
|
|
221
|
+
a conversation must be reloaded before that conversation uses the new instructions.
|
|
203
222
|
|
|
204
223
|
### Being reached when you are not typing
|
|
205
224
|
|
|
@@ -208,7 +227,7 @@ listener, and in whether anything reaches them once the window is closed.
|
|
|
208
227
|
|
|
209
228
|
| Runtime | Who re-arms the listener | Window closed |
|
|
210
229
|
|---|---|---|
|
|
211
|
-
| **Claude Code** |
|
|
230
|
+
| **Claude Code (native)** | socket stays open; a closed socket needs a replacement Monitor | not reachable until resumed |
|
|
212
231
|
| **Codex** | nobody needs to — its own queue | not reachable |
|
|
213
232
|
| **Cursor** | **the agent itself**, after every wake | not reachable |
|
|
214
233
|
|
|
@@ -231,7 +250,7 @@ one where remembering to re-arm falls to the agent.
|
|
|
231
250
|
|
|
232
251
|
| Command | Description |
|
|
233
252
|
|---------|-------------|
|
|
234
|
-
| `baychat login [--token <PAT>] [--base <url>]` | Log this
|
|
253
|
+
| `baychat login [--start \| --finish \| --token <PAT>] [--base <url>]` | Log this computer in. Agents use `--start` to show an approval link immediately and `--finish` once approved; interactive login displays a QR and waits |
|
|
235
254
|
| `baychat onboard [<conv>]` | **Run first.** Print the agent protocol + your live identity, conversations, and (a) room's context |
|
|
236
255
|
| `baychat pair <code> [--base <url>]` | Redeem a pairing code and store credentials |
|
|
237
256
|
| `baychat hermes init [--home <dir>] [--enable]` | Install the BayChat platform plugin into a Hermes agent (`~/.hermes`) and put your paired token in the `.env` its adapter reads. `--enable` also runs the two `hermes` commands that switch it on |
|
|
@@ -338,13 +357,10 @@ provider, then join a Bay normally.
|
|
|
338
357
|
|
|
339
358
|
## Relay
|
|
340
359
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
process holding it, and that process still has to wake the session.
|
|
346
|
-
|
|
347
|
-
`baychat relay` is that process.
|
|
360
|
+
This section covers Codex and the legacy shell delivery path. Claude's native
|
|
361
|
+
WebSocket Monitor receives remote events without this local process. A host
|
|
362
|
+
without native incoming events still needs its supported runtime adapter;
|
|
363
|
+
MCP tools alone do not wake an idle model.
|
|
348
364
|
|
|
349
365
|
```
|
|
350
366
|
baychat relay start # systemd user unit, survives reboot
|
|
@@ -364,7 +380,7 @@ rather than guess:
|
|
|
364
380
|
1. **You tell it**: `--resume-id <id>` on attach.
|
|
365
381
|
2. **The session tells it**: `attach` reads the id out of the environment its own
|
|
366
382
|
runtime gave it (`CLAUDE_CODE_SESSION_ID` for Claude Code), so no flag is
|
|
367
|
-
needed. The
|
|
383
|
+
needed. The normal Codex join corroborates its own native task id.
|
|
368
384
|
3. **The relay finds it**: it searches the runtime's own state —
|
|
369
385
|
`~/.claude/projects`, `~/.codex/sessions` — for the session whose transcript
|
|
370
386
|
records *this* attach. Sub-agent threads are skipped, and if two sessions
|
|
@@ -670,10 +686,10 @@ launch through a shell, so the CLI shells out there and quotes each argument its
|
|
|
670
686
|
|
|
671
687
|
### What the remote server gives you — sessions and groups
|
|
672
688
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
676
|
-
|
|
689
|
+
With a device login, the remote server authenticates you as a person. Each
|
|
690
|
+
session action requires `session`, because one login can have several coding
|
|
691
|
+
sessions. Agent tokens act as their own agent. Device connections add these
|
|
692
|
+
session and group tools:
|
|
677
693
|
|
|
678
694
|
| Tool | What it does |
|
|
679
695
|
|------|--------------|
|
|
@@ -683,6 +699,11 @@ exist only here:
|
|
|
683
699
|
| `list_groups` | The groups you are in — exact title, who is in them, and the conversation id |
|
|
684
700
|
| `create_group` | Open a new group and land this session in it. You become its admin, exactly as if you had created it in the app |
|
|
685
701
|
|
|
702
|
+
Remote protocol 1.10 also provides `listen_messages`, `get_delivery_status` and
|
|
703
|
+
`stop_listening` to start, verify and stop native incoming WebSocket delivery.
|
|
704
|
+
Pass the returned `monitor` object to a supported native host; opening a ticket
|
|
705
|
+
alone does not establish the connection.
|
|
706
|
+
|
|
686
707
|
`request_approval` and `await_approval` are here too: they put a yes/no decision card on your phone
|
|
687
708
|
and block until you answer.
|
|
688
709
|
|
package/dist/api.js
CHANGED
|
@@ -11,6 +11,7 @@ exports.createDeviceLink = createDeviceLink;
|
|
|
11
11
|
exports.pollDeviceLink = pollDeviceLink;
|
|
12
12
|
exports.deviceMe = deviceMe;
|
|
13
13
|
const attachments_1 = require("./attachments");
|
|
14
|
+
const DEVICE_AUTH_TIMEOUT_MS = 15_000;
|
|
14
15
|
class ApiError extends Error {
|
|
15
16
|
status;
|
|
16
17
|
code;
|
|
@@ -178,6 +179,7 @@ async function pollLinkRequest(baseUrl, id, pollSecret) {
|
|
|
178
179
|
/** Create a device link request. `deviceName` labels the laptop in the approve UI. */
|
|
179
180
|
async function createDeviceLink(baseUrl, deviceName) {
|
|
180
181
|
const res = await fetch(`${baseUrl}/api/device-links`, {
|
|
182
|
+
signal: AbortSignal.timeout(DEVICE_AUTH_TIMEOUT_MS),
|
|
181
183
|
method: "POST",
|
|
182
184
|
headers: { "Content-Type": "application/json" },
|
|
183
185
|
body: JSON.stringify(deviceName ? { deviceName } : {}),
|
|
@@ -194,7 +196,7 @@ async function createDeviceLink(baseUrl, deviceName) {
|
|
|
194
196
|
* `cmdLogin` treats it as "expired" and stops polling.
|
|
195
197
|
*/
|
|
196
198
|
async function pollDeviceLink(baseUrl, id, pollSecret) {
|
|
197
|
-
const res = await fetch(`${baseUrl}/api/device-links/${id}?secret=${encodeURIComponent(pollSecret)}
|
|
199
|
+
const res = await fetch(`${baseUrl}/api/device-links/${id}?secret=${encodeURIComponent(pollSecret)}`, { signal: AbortSignal.timeout(DEVICE_AUTH_TIMEOUT_MS) });
|
|
198
200
|
if (!res.ok)
|
|
199
201
|
throw await parseError(res);
|
|
200
202
|
return (await res.json());
|
|
@@ -202,6 +204,7 @@ async function pollDeviceLink(baseUrl, id, pollSecret) {
|
|
|
202
204
|
/** Verify a device token and learn whose Bay it opens (`baychat login --token`). */
|
|
203
205
|
async function deviceMe(baseUrl, token) {
|
|
204
206
|
const res = await fetch(`${baseUrl}/api/device-credentials/me`, {
|
|
207
|
+
signal: AbortSignal.timeout(DEVICE_AUTH_TIMEOUT_MS),
|
|
205
208
|
headers: { Authorization: `Bearer ${token}` },
|
|
206
209
|
});
|
|
207
210
|
if (!res.ok)
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.CLAUDE_REACHABILITY = exports.CLAUDE_JOIN_STEPS = void 0;
|
|
4
|
+
/** Claude's native entry point. An expired local relay login says nothing about a separately configured MCP connection. */
|
|
5
|
+
exports.CLAUDE_JOIN_STEPS = `1. Use the connected **BayChat MCP tools first**. Do not inspect local credentials,
|
|
6
|
+
run a login probe, or start a relay before trying the requested join.
|
|
7
|
+
For a supplied name call \`join_session({session: "<name>", sessions: true})\`.
|
|
8
|
+
For a supplied group use \`join_session({session: "<name>", group: "<exact title>"})\`.
|
|
9
|
+
For explicit \`--private\`, omit both \`sessions\` and \`group\`.
|
|
10
|
+
If automatic naming was requested, run only
|
|
11
|
+
\`baychat session-name --runtime claude\` to obtain the verified name, then join
|
|
12
|
+
with it. A bare invocation lists sessions and stops; it never joins.
|
|
13
|
+
2. Call \`listen_messages({session: "<confirmed name>"})\`. Pass its returned
|
|
14
|
+
\`monitor\` object directly to the native **Monitor** tool: it contains
|
|
15
|
+
\`ws: {url, protocols}\` and \`persistent: true\`. Keep its ticket private.
|
|
16
|
+
This is a native WebSocket source, not a shell command. No terminal, relay,
|
|
17
|
+
attach loop or manual re-arming is needed while this listener remains open.
|
|
18
|
+
3. Call \`get_delivery_status({session: "<confirmed name>"})\`. Only \`connected\`
|
|
19
|
+
confirms incoming delivery. \`awaiting_connection\` means a ticket was created
|
|
20
|
+
but Monitor has not connected; show that exact state, not "ready".
|
|
21
|
+
Print the confirmed name and room. Use that name as \`session\` on every
|
|
22
|
+
later BayChat tool call. Say hello once in the confirmed conversation,
|
|
23
|
+
unless already greeted, and read \`get_messages\` for backlog.
|
|
24
|
+
|
|
25
|
+
### If connection needs attention
|
|
26
|
+
|
|
27
|
+
- An actual MCP authentication refusal is evidence that this connection needs
|
|
28
|
+
renewal. A local credential file's date alone is not: MCP and relay may use
|
|
29
|
+
different logins. Do not diagnose both as down from one file.
|
|
30
|
+
- If the client provides an authorization/reconnect link, show it immediately.
|
|
31
|
+
For a CLI-installed device login, run \`baychat login --start\` on this computer
|
|
32
|
+
and show the returned approval link in your reply. It exits immediately.
|
|
33
|
+
Wait for the user's approval; then run \`baychat login --finish\` once.
|
|
34
|
+
If still pending, show the link and wait. Reconnect BayChat in the client to
|
|
35
|
+
load the refreshed login, then retry the same name and room.
|
|
36
|
+
- Discover deferred BayChat tools with the host's tool search first. If still
|
|
37
|
+
absent, refresh/reconnect BayChat once. Do not assume an expired token from
|
|
38
|
+
an absent tool list. If setup is still missing,
|
|
39
|
+
use the same short login flow. Do not inspect installed JavaScript, search
|
|
40
|
+
credential files, run repeated diagnostics or leave a waiting login process.
|
|
41
|
+
- Native WebSocket Monitor requires a supported Claude Code host (documented
|
|
42
|
+
from 2.1.195). If the tool is missing, denied or lacks \`ws\`, explain that
|
|
43
|
+
incoming delivery is not connected and show the specific host limitation.
|
|
44
|
+
When the user requested remote-only delivery, stop there. Only if they choose
|
|
45
|
+
the legacy relay fallback, run the same arguments through \`baychat join\`
|
|
46
|
+
with a persistent shell Monitor; do not create a second attach loop.
|
|
47
|
+
For example: \`baychat join "<name>" --runtime claude\`, or
|
|
48
|
+
\`baychat join --group "<group>" --runtime claude\` for automatic naming.
|
|
49
|
+
Preserve \`--private\`, \`--sessions\`, or the exact group as requested.
|
|
50
|
+
`;
|
|
51
|
+
/** Native socket lifecycle, including the intentional-stop boundary. */
|
|
52
|
+
exports.CLAUDE_REACHABILITY = `## Staying reachable — native remote messages
|
|
53
|
+
|
|
54
|
+
Keep the native WebSocket Monitor open between messages. On a \`messages\` event,
|
|
55
|
+
read the indicated conversations with \`get_messages\`, deduplicate message IDs,
|
|
56
|
+
obey \`shouldRespond\`, acknowledge slow work with 👀, and answer in BayChat.
|
|
57
|
+
Use \`contact_agent\` for directed replies to peers. A delivery event is not a
|
|
58
|
+
model acknowledgement; never claim a message is read just because it arrived.
|
|
59
|
+
|
|
60
|
+
A \`reset\` event means catch up through history. If the socket closes, Monitor
|
|
61
|
+
has ended: \`persistent: true\` does not reconnect a closed socket by itself.
|
|
62
|
+
Close code **4000** means intentionally stopped or replaced: **do not reconnect**.
|
|
63
|
+
For other closes, call \`listen_messages\` with this session and the last cursor,
|
|
64
|
+
open one replacement native Monitor, and confirm \`get_delivery_status\` again.
|
|
65
|
+
Keep the last cursor in this conversation; do not save tickets in files.
|
|
66
|
+
|
|
67
|
+
\`stop_listening\` stops incoming delivery without ending membership.
|
|
68
|
+
\`end_session\` ends the coding session when the user asks to leave. An application
|
|
69
|
+
that has been closed cannot answer until resumed. Do not promise instant model
|
|
70
|
+
replies: show connected delivery separately from actual reading and answering.
|
|
71
|
+
`;
|
package/dist/commands.js
CHANGED
|
@@ -38,6 +38,7 @@ const context_1 = require("./context");
|
|
|
38
38
|
const runtime_install_1 = require("./runtime-install");
|
|
39
39
|
const credential_refresh_1 = require("./credential-refresh");
|
|
40
40
|
const client_paths_1 = require("./client-paths");
|
|
41
|
+
const device_login_1 = require("./device-login");
|
|
41
42
|
const tools_1 = require("./tools");
|
|
42
43
|
const DEFAULT_BASE_URL = "https://api.baychat.io";
|
|
43
44
|
function requireCredentials() {
|
|
@@ -804,28 +805,47 @@ function installClaudeSkill() {
|
|
|
804
805
|
* Returns true when logged in, false on expiry/timeout (exit 2 in index.ts).
|
|
805
806
|
*/
|
|
806
807
|
async function cmdLogin(opts = {}) {
|
|
807
|
-
const base = (opts.base ||
|
|
808
|
+
const base = (opts.base ||
|
|
809
|
+
process.env.BAYCHAT_API_URL ||
|
|
810
|
+
DEFAULT_BASE_URL).replace(/\/$/, "");
|
|
811
|
+
if (opts.mode === "start") {
|
|
812
|
+
const request = await (0, device_login_1.startDeviceLogin)(base);
|
|
813
|
+
console.log(request.url);
|
|
814
|
+
console.log(`Approve this device in BayChat, then run baychat login --finish on this same computer. Link expires ${request.expiresAt}.`);
|
|
815
|
+
return true;
|
|
816
|
+
}
|
|
817
|
+
if (opts.mode === "finish") {
|
|
818
|
+
const result = await (0, device_login_1.finishDeviceLogin)(opts.base || process.env.BAYCHAT_API_URL);
|
|
819
|
+
if (result.status === "pending") {
|
|
820
|
+
console.log(result.url);
|
|
821
|
+
console.log("Waiting for your approval. After approving, run baychat login --finish. No background login is running.");
|
|
822
|
+
return false;
|
|
823
|
+
}
|
|
824
|
+
completeDeviceLogin(result.device, {
|
|
825
|
+
...opts,
|
|
826
|
+
message: `✓ Logged in as ${result.device.user.name}`,
|
|
827
|
+
});
|
|
828
|
+
console.log("Reconnect BayChat in your client to load the refreshed login, then retry the same session name.");
|
|
829
|
+
return true;
|
|
830
|
+
}
|
|
808
831
|
if (opts.token) {
|
|
809
832
|
const me = await (0, api_1.deviceMe)(base, opts.token);
|
|
810
|
-
(
|
|
833
|
+
completeDeviceLogin({
|
|
811
834
|
baseUrl: base,
|
|
812
835
|
token: opts.token,
|
|
813
836
|
user: me.user,
|
|
814
837
|
expiresAt: me.expiresAt,
|
|
838
|
+
}, {
|
|
839
|
+
...opts,
|
|
840
|
+
message: `✓ Logged in as ${me.user.name} (${me.tenant.name})`,
|
|
815
841
|
});
|
|
816
|
-
console.log(`✓ Logged in as ${me.user.name} (${me.tenant.name})`);
|
|
817
|
-
refreshConnectedClients(base, opts.token);
|
|
818
|
-
if (opts.registerClaude !== false)
|
|
819
|
-
registerWithClaude(base, opts.token);
|
|
820
|
-
if (opts.hint !== false)
|
|
821
|
-
console.log("\n Run /baychat <name> in any session.");
|
|
822
842
|
return true;
|
|
823
843
|
}
|
|
824
844
|
// The hostname labels this laptop in the approve UI; the server caps the field
|
|
825
845
|
// at 60 chars, so a long corporate hostname must not 400 the whole login.
|
|
826
846
|
const request = await (0, api_1.createDeviceLink)(base, node_os_1.default.hostname().slice(0, 60));
|
|
827
|
-
console.log(await qrcode_1.default.toString(request.url, { type: "terminal", small: true }));
|
|
828
847
|
console.log(request.url);
|
|
848
|
+
console.log(await qrcode_1.default.toString(request.url, { type: "terminal", small: true }));
|
|
829
849
|
console.log("Scan with your phone — BayChat will open to approve this laptop.");
|
|
830
850
|
// Stop polling shortly after the server-declared expiry (+5s for clock skew).
|
|
831
851
|
const deadline = new Date(request.expiresAt).getTime() + 5_000;
|
|
@@ -863,24 +883,28 @@ async function cmdLogin(opts = {}) {
|
|
|
863
883
|
// string (flag → env → default), so there is nothing to fall back to.
|
|
864
884
|
//
|
|
865
885
|
// Never print the token — it lives in the credentials file only.
|
|
866
|
-
(
|
|
886
|
+
completeDeviceLogin({
|
|
867
887
|
baseUrl: base,
|
|
868
888
|
token: status.token,
|
|
869
889
|
user: status.user,
|
|
870
890
|
expiresAt: status.expiresAt,
|
|
871
|
-
});
|
|
872
|
-
console.log(`✓ Logged in as ${status.user.name}`);
|
|
873
|
-
refreshConnectedClients(base, status.token);
|
|
874
|
-
if (opts.registerClaude !== false)
|
|
875
|
-
registerWithClaude(base, status.token);
|
|
876
|
-
if (opts.hint !== false)
|
|
877
|
-
console.log("\n Run /baychat <name> in any session.");
|
|
891
|
+
}, { ...opts, message: `✓ Logged in as ${status.user.name}` });
|
|
878
892
|
return true;
|
|
879
893
|
}
|
|
880
894
|
}
|
|
881
895
|
console.log("Login request expired — run baychat login again.");
|
|
882
896
|
return false;
|
|
883
897
|
}
|
|
898
|
+
/** Persist an approved login before refreshing the clients that use it. */
|
|
899
|
+
function completeDeviceLogin(device, options) {
|
|
900
|
+
(0, config_1.saveDeviceCredentials)(device);
|
|
901
|
+
console.log(options.message);
|
|
902
|
+
refreshConnectedClients(device.baseUrl, device.token);
|
|
903
|
+
if (options.registerClaude !== false)
|
|
904
|
+
registerWithClaude(device.baseUrl, device.token);
|
|
905
|
+
if (options.hint !== false)
|
|
906
|
+
console.log("\n Run /baychat <name> in any session.");
|
|
907
|
+
}
|
|
884
908
|
/**
|
|
885
909
|
* The renewal nudge for a device credential, or null when none is due. Device
|
|
886
910
|
* credentials expire (30 days), and the failure mode without a warning is an
|
package/dist/connect-claude.js
CHANGED
|
@@ -3,7 +3,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.cmdConnectClaude = cmdConnectClaude;
|
|
4
4
|
const commands_1 = require("./commands");
|
|
5
5
|
const config_1 = require("./config");
|
|
6
|
-
const commands_2 = require("./relay/commands");
|
|
7
6
|
const runtime_install_1 = require("./runtime-install");
|
|
8
7
|
/** One-time Claude setup reuses a valid login, registers MCP, and refreshes its skill. */
|
|
9
8
|
async function cmdConnectClaude(opts = {}) {
|
|
@@ -23,7 +22,7 @@ async function cmdConnectClaude(opts = {}) {
|
|
|
23
22
|
console.log(" " + line);
|
|
24
23
|
if (!registered)
|
|
25
24
|
return 1;
|
|
26
|
-
console.log("
|
|
25
|
+
console.log(" Incoming messages use native WebSocket Monitor; the session will verify its connection after joining.");
|
|
27
26
|
console.log(' Restart Claude Code once, then use /baychat <name> "<group>".');
|
|
28
27
|
return 0;
|
|
29
28
|
}
|
|
@@ -0,0 +1,192 @@
|
|
|
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.startDeviceLogin = startDeviceLogin;
|
|
37
|
+
exports.finishDeviceLogin = finishDeviceLogin;
|
|
38
|
+
const fs = __importStar(require("node:fs"));
|
|
39
|
+
const os = __importStar(require("node:os"));
|
|
40
|
+
const path = __importStar(require("node:path"));
|
|
41
|
+
const node_crypto_1 = require("node:crypto");
|
|
42
|
+
const zod_1 = require("zod");
|
|
43
|
+
const api_1 = require("./api");
|
|
44
|
+
const config_1 = require("./config");
|
|
45
|
+
const deviceSchema = zod_1.z.object({
|
|
46
|
+
baseUrl: zod_1.z.string().url(),
|
|
47
|
+
token: zod_1.z.string().min(1),
|
|
48
|
+
user: zod_1.z.object({ id: zod_1.z.string().min(1), name: zod_1.z.string() }),
|
|
49
|
+
expiresAt: zod_1.z.string().datetime(),
|
|
50
|
+
});
|
|
51
|
+
const pendingSchema = zod_1.z.object({
|
|
52
|
+
baseUrl: zod_1.z.string().url(),
|
|
53
|
+
id: zod_1.z.string().min(1),
|
|
54
|
+
pollSecret: zod_1.z.string().min(1),
|
|
55
|
+
url: zod_1.z.string().url(),
|
|
56
|
+
expiresAt: zod_1.z.string().datetime(),
|
|
57
|
+
approved: deviceSchema.optional(),
|
|
58
|
+
});
|
|
59
|
+
const LOCK_EXPIRY_MS = 60_000; // Each login request has a 15-second network deadline.
|
|
60
|
+
/** Create or reuse an approval link, without polling or exposing its pickup secret. */
|
|
61
|
+
async function startDeviceLogin(baseUrl) {
|
|
62
|
+
return withLoginLock(async () => {
|
|
63
|
+
const base = normalizeBase(baseUrl);
|
|
64
|
+
const previous = readPending();
|
|
65
|
+
if (previous?.approved)
|
|
66
|
+
throw new Error("A login was approved. Run baychat login --finish to save it first.");
|
|
67
|
+
if (previous?.baseUrl === base &&
|
|
68
|
+
Date.parse(previous.expiresAt) > Date.now()) {
|
|
69
|
+
return { url: previous.url, expiresAt: previous.expiresAt };
|
|
70
|
+
}
|
|
71
|
+
const request = await (0, api_1.createDeviceLink)(base, os.hostname().slice(0, 60));
|
|
72
|
+
const parsed = pendingSchema.safeParse({ ...request, baseUrl: base });
|
|
73
|
+
if (!parsed.success)
|
|
74
|
+
throw new Error("BayChat returned an invalid login link. Try baychat login --start again.");
|
|
75
|
+
writePending(parsed.data);
|
|
76
|
+
return { url: parsed.data.url, expiresAt: parsed.data.expiresAt };
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
/** Check approval once. Save a consumed credential before removing its recovery record. */
|
|
80
|
+
async function finishDeviceLogin(baseUrl) {
|
|
81
|
+
return withLoginLock(async () => {
|
|
82
|
+
const pending = readPending();
|
|
83
|
+
if (!pending)
|
|
84
|
+
throw new Error("No usable login request. Run baychat login --start.");
|
|
85
|
+
if (baseUrl && normalizeBase(baseUrl) !== pending.baseUrl) {
|
|
86
|
+
throw new Error("This login was started for a different server. Finish without --base, or start a new login for that server.");
|
|
87
|
+
}
|
|
88
|
+
let device = pending.approved;
|
|
89
|
+
if (!device) {
|
|
90
|
+
if (Date.parse(pending.expiresAt) <= Date.now())
|
|
91
|
+
throw new Error("Login request expired. Run baychat login --start.");
|
|
92
|
+
let status;
|
|
93
|
+
try {
|
|
94
|
+
status = await (0, api_1.pollDeviceLink)(pending.baseUrl, pending.id, pending.pollSecret);
|
|
95
|
+
}
|
|
96
|
+
catch (error) {
|
|
97
|
+
if (error instanceof api_1.ApiError && error.status === 404) {
|
|
98
|
+
throw new Error("Login request expired or was already collected. Run baychat login --start.");
|
|
99
|
+
}
|
|
100
|
+
throw error;
|
|
101
|
+
}
|
|
102
|
+
if (status.status === "pending")
|
|
103
|
+
return { status: "pending", url: pending.url };
|
|
104
|
+
if (status.status !== "approved")
|
|
105
|
+
throw new Error("BayChat returned an invalid login status. Run baychat login --start again.");
|
|
106
|
+
const parsed = deviceSchema.safeParse({
|
|
107
|
+
...status,
|
|
108
|
+
baseUrl: pending.baseUrl,
|
|
109
|
+
});
|
|
110
|
+
if (!parsed.success)
|
|
111
|
+
throw new Error("BayChat returned an invalid login approval. Run baychat login --start again.");
|
|
112
|
+
device = parsed.data;
|
|
113
|
+
// Pickup is single-use. Retain the approved credential if merging the user's
|
|
114
|
+
// existing credentials fails; the next finish must not consume it again.
|
|
115
|
+
writePending({ ...pending, approved: device });
|
|
116
|
+
}
|
|
117
|
+
(0, config_1.saveDeviceCredentials)(device);
|
|
118
|
+
fs.unlinkSync(pendingPath());
|
|
119
|
+
return { status: "approved", device };
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
function pendingPath() {
|
|
123
|
+
return path.join((0, config_1.configDir)(), "login-pending.json");
|
|
124
|
+
}
|
|
125
|
+
function normalizeBase(base) {
|
|
126
|
+
const url = new URL(base);
|
|
127
|
+
if (!["http:", "https:"].includes(url.protocol) ||
|
|
128
|
+
url.username ||
|
|
129
|
+
url.password ||
|
|
130
|
+
url.search ||
|
|
131
|
+
url.hash) {
|
|
132
|
+
throw new Error("Login server must be an HTTP(S) URL without credentials, query or fragment.");
|
|
133
|
+
}
|
|
134
|
+
return url.toString().replace(/\/$/, "");
|
|
135
|
+
}
|
|
136
|
+
function readPending() {
|
|
137
|
+
try {
|
|
138
|
+
const parsed = pendingSchema.safeParse(JSON.parse(fs.readFileSync(pendingPath(), "utf8")));
|
|
139
|
+
return parsed.success ? parsed.data : undefined;
|
|
140
|
+
}
|
|
141
|
+
catch (error) {
|
|
142
|
+
if (error instanceof SyntaxError ||
|
|
143
|
+
error.code === "ENOENT")
|
|
144
|
+
return undefined;
|
|
145
|
+
throw new Error("Cannot read the pending BayChat login. Check access to your BayChat configuration directory.");
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
/** Atomic private storage; an interrupted write cannot truncate a live approval. */
|
|
149
|
+
function writePending(pending) {
|
|
150
|
+
const file = pendingPath();
|
|
151
|
+
const temporary = `${file}.${(0, node_crypto_1.randomUUID)()}.tmp`;
|
|
152
|
+
try {
|
|
153
|
+
fs.writeFileSync(temporary, JSON.stringify(pending), {
|
|
154
|
+
mode: 0o600,
|
|
155
|
+
flag: "wx",
|
|
156
|
+
});
|
|
157
|
+
fs.renameSync(temporary, file);
|
|
158
|
+
fs.chmodSync(file, 0o600);
|
|
159
|
+
}
|
|
160
|
+
finally {
|
|
161
|
+
fs.rmSync(temporary, { force: true });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
/** Serialize local login attempts so two sessions cannot consume the same approval. */
|
|
165
|
+
async function withLoginLock(action) {
|
|
166
|
+
fs.mkdirSync((0, config_1.configDir)(), { recursive: true, mode: 0o700 });
|
|
167
|
+
const lock = path.join((0, config_1.configDir)(), "login.lock");
|
|
168
|
+
try {
|
|
169
|
+
if (Date.now() - fs.statSync(lock).mtimeMs > LOCK_EXPIRY_MS)
|
|
170
|
+
fs.unlinkSync(lock);
|
|
171
|
+
}
|
|
172
|
+
catch (error) {
|
|
173
|
+
if (error.code !== "ENOENT")
|
|
174
|
+
throw error;
|
|
175
|
+
}
|
|
176
|
+
let descriptor;
|
|
177
|
+
try {
|
|
178
|
+
descriptor = fs.openSync(lock, "wx", 0o600);
|
|
179
|
+
}
|
|
180
|
+
catch (error) {
|
|
181
|
+
if (error.code === "EEXIST")
|
|
182
|
+
throw new Error("Another BayChat login is running. Retry when it finishes (a stopped login clears within one minute).");
|
|
183
|
+
throw error;
|
|
184
|
+
}
|
|
185
|
+
try {
|
|
186
|
+
return await action();
|
|
187
|
+
}
|
|
188
|
+
finally {
|
|
189
|
+
fs.closeSync(descriptor);
|
|
190
|
+
fs.rmSync(lock, { force: true });
|
|
191
|
+
}
|
|
192
|
+
}
|