baychat 0.29.2 → 0.29.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/.claude-plugin/plugin.json +1 -1
- package/dist/bay-deleted.js +95 -0
- package/dist/bays-command.js +3 -1
- package/dist/bays.js +8 -2
- package/dist/relay/daemon.js +6 -0
- package/dist/relay/launcher/approval-settings.js +20 -6
- package/dist/relay/launcher/argv.js +18 -9
- package/dist/relay/launcher/handle.js +35 -1
- package/dist/relay/launcher/loop.js +2 -0
- package/dist/relay/updates.js +26 -6
- package/package.json +1 -1
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.4",
|
|
4
4
|
"displayName": "BayChat",
|
|
5
5
|
"description": "Wakes this Claude Code session the moment a BayChat message arrives: no Monitor, no 30-minute deadline. Uses the baychat CLI already on this computer (npm install -g baychat, then baychat login).",
|
|
6
6
|
"author": {
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// A Bay login whose Bay an owner deleted (the API's docs/features/BAY_DELETE_AND_DEFAULT.md).
|
|
3
|
+
//
|
|
4
|
+
// The relay is the one that finds out — its poll answers 410 BAY_DELETED — and `baychat bays`
|
|
5
|
+
// is where a person looks, so the relay leaves a note here and clears it the next time a poll
|
|
6
|
+
// succeeds (an owner restored the Bay). The note is keyed by a SHA-256 prefix of the login's
|
|
7
|
+
// token, never the token itself: this file is not a secret and must not hold one.
|
|
8
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
9
|
+
if (k2 === undefined) k2 = k;
|
|
10
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
11
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
12
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
13
|
+
}
|
|
14
|
+
Object.defineProperty(o, k2, desc);
|
|
15
|
+
}) : (function(o, m, k, k2) {
|
|
16
|
+
if (k2 === undefined) k2 = k;
|
|
17
|
+
o[k2] = m[k];
|
|
18
|
+
}));
|
|
19
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
20
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
21
|
+
}) : function(o, v) {
|
|
22
|
+
o["default"] = v;
|
|
23
|
+
});
|
|
24
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
25
|
+
var ownKeys = function(o) {
|
|
26
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
27
|
+
var ar = [];
|
|
28
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
29
|
+
return ar;
|
|
30
|
+
};
|
|
31
|
+
return ownKeys(o);
|
|
32
|
+
};
|
|
33
|
+
return function (mod) {
|
|
34
|
+
if (mod && mod.__esModule) return mod;
|
|
35
|
+
var result = {};
|
|
36
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
37
|
+
__setModuleDefault(result, mod);
|
|
38
|
+
return result;
|
|
39
|
+
};
|
|
40
|
+
})();
|
|
41
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
42
|
+
exports.bayDeletedNotesPath = bayDeletedNotesPath;
|
|
43
|
+
exports.markBayDeleted = markBayDeleted;
|
|
44
|
+
exports.clearBayDeleted = clearBayDeleted;
|
|
45
|
+
exports.bayDeletedSince = bayDeletedSince;
|
|
46
|
+
const crypto = __importStar(require("crypto"));
|
|
47
|
+
const fs = __importStar(require("fs"));
|
|
48
|
+
const path = __importStar(require("path"));
|
|
49
|
+
const config_1 = require("./config");
|
|
50
|
+
function bayDeletedNotesPath() {
|
|
51
|
+
return path.join((0, config_1.configDir)(), "deleted-bays.json");
|
|
52
|
+
}
|
|
53
|
+
function keyOf(token) {
|
|
54
|
+
return crypto.createHash("sha256").update(token).digest("hex").slice(0, 16);
|
|
55
|
+
}
|
|
56
|
+
function readNotes() {
|
|
57
|
+
try {
|
|
58
|
+
const parsed = JSON.parse(fs.readFileSync(bayDeletedNotesPath(), "utf8"));
|
|
59
|
+
return parsed && typeof parsed === "object" && !Array.isArray(parsed) ? parsed : {};
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return {};
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
function writeNotes(notes) {
|
|
66
|
+
const file = bayDeletedNotesPath();
|
|
67
|
+
if (Object.keys(notes).length === 0) {
|
|
68
|
+
fs.rmSync(file, { force: true });
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
72
|
+
fs.writeFileSync(file, `${JSON.stringify(notes, null, 2)}\n`, { mode: 0o600 });
|
|
73
|
+
}
|
|
74
|
+
/** The relay met 410 BAY_DELETED with this login. Keeps the first time it was seen. */
|
|
75
|
+
function markBayDeleted(token, now = new Date()) {
|
|
76
|
+
const notes = readNotes();
|
|
77
|
+
const key = keyOf(token);
|
|
78
|
+
if (notes[key])
|
|
79
|
+
return;
|
|
80
|
+
notes[key] = now.toISOString();
|
|
81
|
+
writeNotes(notes);
|
|
82
|
+
}
|
|
83
|
+
/** A poll with this login succeeded: the Bay is not (or no longer) deleted. Cheap when unmarked. */
|
|
84
|
+
function clearBayDeleted(token) {
|
|
85
|
+
const notes = readNotes();
|
|
86
|
+
const key = keyOf(token);
|
|
87
|
+
if (!notes[key])
|
|
88
|
+
return;
|
|
89
|
+
delete notes[key];
|
|
90
|
+
writeNotes(notes);
|
|
91
|
+
}
|
|
92
|
+
/** When the relay first found this login's Bay deleted, or null. */
|
|
93
|
+
function bayDeletedSince(token) {
|
|
94
|
+
return readNotes()[keyOf(token)] ?? null;
|
|
95
|
+
}
|
package/dist/bays-command.js
CHANGED
|
@@ -7,6 +7,7 @@ exports.cmdBays = cmdBays;
|
|
|
7
7
|
const config_1 = require("./config");
|
|
8
8
|
const bays_1 = require("./bays");
|
|
9
9
|
const bay_mcp_1 = require("./bay-mcp");
|
|
10
|
+
const bay_deleted_1 = require("./bay-deleted");
|
|
10
11
|
const commands_1 = require("./commands");
|
|
11
12
|
const client_paths_1 = require("./client-paths");
|
|
12
13
|
exports.BAYS_USAGE = `Usage: baychat bays List the Bay logins on this computer
|
|
@@ -22,7 +23,8 @@ exports.BAYS_USAGE = `Usage: baychat bays List the Bay logins on
|
|
|
22
23
|
function cmdBays(args) {
|
|
23
24
|
const [sub, ...rest] = args;
|
|
24
25
|
if (sub === undefined) {
|
|
25
|
-
|
|
26
|
+
const lines = (0, bays_1.renderBayList)((0, config_1.loadBayLogins)(), (0, config_1.loadDeviceCredentials)(), Date.now(), (bay) => (0, bay_deleted_1.bayDeletedSince)(bay.token));
|
|
27
|
+
for (const line of lines)
|
|
26
28
|
console.log(line);
|
|
27
29
|
return 0;
|
|
28
30
|
}
|
package/dist/bays.js
CHANGED
|
@@ -114,7 +114,9 @@ function withDefaultBay(bays, device, target) {
|
|
|
114
114
|
};
|
|
115
115
|
}
|
|
116
116
|
/** `baychat bays` — one line per Bay login, the default marked. Pure, for testing. */
|
|
117
|
-
function renderBayList(bays, device, now = Date.now()
|
|
117
|
+
function renderBayList(bays, device, now = Date.now(),
|
|
118
|
+
/** When the relay found this login's Bay deleted by an owner (bay-deleted.ts), or null. */
|
|
119
|
+
deletedSince = () => null) {
|
|
118
120
|
if (bays.length === 0) {
|
|
119
121
|
if (!device)
|
|
120
122
|
return ["This computer is not logged in to BayChat — run `baychat login`."];
|
|
@@ -127,7 +129,11 @@ function renderBayList(bays, device, now = Date.now()) {
|
|
|
127
129
|
const width = Math.max(...bays.map((b) => b.bayName.length));
|
|
128
130
|
for (const bay of bays) {
|
|
129
131
|
const entry = bay.isDefault || bays.length < 2 ? exports.DEFAULT_MCP_ENTRY : bayEntryName(bay, bays);
|
|
130
|
-
|
|
132
|
+
const deleted = deletedSince(bay);
|
|
133
|
+
const state = deleted
|
|
134
|
+
? `DELETED by its owner on ${deleted.slice(0, 10)} — an owner can restore it in the app for 30 days`
|
|
135
|
+
: expiryText(bay.expiresAt, now);
|
|
136
|
+
lines.push(` ${bay.isDefault ? "*" : " "} ${bay.bayName.padEnd(width)} ${state} MCP: ${entry}`);
|
|
131
137
|
}
|
|
132
138
|
lines.push("* = default Bay (a session that names no Bay joins it).");
|
|
133
139
|
if (bays.length > 1)
|
package/dist/relay/daemon.js
CHANGED
|
@@ -42,6 +42,7 @@ const fs = __importStar(require("fs"));
|
|
|
42
42
|
const net = __importStar(require("net"));
|
|
43
43
|
const path = __importStar(require("path"));
|
|
44
44
|
const config_1 = require("../config");
|
|
45
|
+
const bay_deleted_1 = require("../bay-deleted");
|
|
45
46
|
const context_report_1 = require("../context-report");
|
|
46
47
|
const codex_context_1 = require("./codex-context");
|
|
47
48
|
const session_commands_1 = require("./session-commands");
|
|
@@ -372,10 +373,15 @@ class RelayDaemon {
|
|
|
372
373
|
onPoll: (cursor) => {
|
|
373
374
|
this.cursor = cursor;
|
|
374
375
|
this.polls += 1;
|
|
376
|
+
// A poll that answers means the Bay is live — restored, if it had been deleted.
|
|
377
|
+
(0, bay_deleted_1.clearBayDeleted)(device.token);
|
|
375
378
|
},
|
|
376
379
|
onError: (err, willRetry) => {
|
|
377
380
|
this.lastError = `${errText(err)}${willRetry ? " (retrying)" : ""}`;
|
|
378
381
|
this.log(this.lastError);
|
|
382
|
+
// An owner deleted this login's Bay: leave a note `baychat bays` can show.
|
|
383
|
+
if (err?.code === "BAY_DELETED")
|
|
384
|
+
(0, bay_deleted_1.markBayDeleted)(device.token);
|
|
379
385
|
},
|
|
380
386
|
onTransport: (kind, detail) => {
|
|
381
387
|
this.transport = kind;
|
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.TERMINAL_SETTINGS_FILE = exports.JOIN_ALLOW = void 0;
|
|
36
|
+
exports.TERMINAL_SETTINGS_FILE = exports.UNATTENDED_SETTINGS = exports.JOIN_ALLOW = void 0;
|
|
37
37
|
exports.approvalPathFor = approvalPathFor;
|
|
38
38
|
exports.launchDir = launchDir;
|
|
39
39
|
exports.approvalSettingsPath = approvalSettingsPath;
|
|
@@ -94,10 +94,24 @@ function permissionHook(cli, flags) {
|
|
|
94
94
|
],
|
|
95
95
|
};
|
|
96
96
|
}
|
|
97
|
-
/**
|
|
98
|
-
|
|
97
|
+
/**
|
|
98
|
+
* Claude Code nudges that stop a session with a menu nobody can answer, switched off for
|
|
99
|
+
* launched sessions only. "Teach auto mode about your environment?" (1 Yes / 2 Not now / 3 Don't
|
|
100
|
+
* show again) stopped Karmen's first WSL launch (2026-10-06). In Claude Code 2.1.289 that nudge,
|
|
101
|
+
* and the /auto-mode-setup tip, are offered only while `skillOverrides["auto-mode-setup"]` is not
|
|
102
|
+
* "off" (read from its code: the gate `x7()`). Off here costs nothing: the skill needs a person.
|
|
103
|
+
*/
|
|
104
|
+
exports.UNATTENDED_SETTINGS = { skillOverrides: { "auto-mode-setup": "off" } };
|
|
105
|
+
/**
|
|
106
|
+
* The settings object for a session started from BayChat. Pure; the test pins its exact content.
|
|
107
|
+
* `bayEntry`: the MCP connection the session joins on, when it is not the default `baychat`
|
|
108
|
+
* (a computer logged in to several Bays) — its tools are join tools too.
|
|
109
|
+
*/
|
|
110
|
+
function approvalSettings(sessionName, cli, bayEntry) {
|
|
111
|
+
const extra = bayEntry && bayEntry !== "baychat" ? [`mcp__${bayEntry}__*`] : [];
|
|
99
112
|
return {
|
|
100
|
-
|
|
113
|
+
...exports.UNATTENDED_SETTINGS,
|
|
114
|
+
permissions: { allow: [...exports.JOIN_ALLOW, ...extra] },
|
|
101
115
|
hooks: permissionHook(cli, ["--session", sessionName]),
|
|
102
116
|
};
|
|
103
117
|
}
|
|
@@ -129,9 +143,9 @@ function currentCliLaunch() {
|
|
|
129
143
|
return { node: fs.realpathSync(process.execPath), entry: fs.realpathSync(process.argv[1] ?? "") };
|
|
130
144
|
}
|
|
131
145
|
/** Write the session's settings file (0600) and return its path. `sessionName` must already be validated. */
|
|
132
|
-
function writeApprovalSettings(sessionName, cli, dir = launchDir()) {
|
|
146
|
+
function writeApprovalSettings(sessionName, cli, dir = launchDir(), bayEntry) {
|
|
133
147
|
const file = approvalSettingsPath(sessionName, dir);
|
|
134
|
-
writePrivateJson(dir, file, approvalSettings(sessionName, cli));
|
|
148
|
+
writePrivateJson(dir, file, approvalSettings(sessionName, cli, bayEntry));
|
|
135
149
|
return file;
|
|
136
150
|
}
|
|
137
151
|
/** Delete the session's settings file. Idempotent: an absent file is already deleted. */
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.LAUNCH_SESSION_PREFIX = void 0;
|
|
3
|
+
exports.UNATTENDED_NOTE = exports.LAUNCH_SESSION_PREFIX = void 0;
|
|
4
4
|
exports.multiplexerSessionName = multiplexerSessionName;
|
|
5
5
|
exports.baychatPrompt = baychatPrompt;
|
|
6
6
|
exports.buildLaunchCommand = buildLaunchCommand;
|
|
@@ -29,13 +29,22 @@ exports.LAUNCH_SESSION_PREFIX = "bc-";
|
|
|
29
29
|
function multiplexerSessionName(sessionName) {
|
|
30
30
|
return `${exports.LAUNCH_SESSION_PREFIX}${sessionName}`;
|
|
31
31
|
}
|
|
32
|
-
/**
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
32
|
+
/**
|
|
33
|
+
* Said after every launched `/baychat` line. Nobody is at a launched session's keyboard, and
|
|
34
|
+
* before it joins nothing it asks can reach a phone (the approve-hook finds the session by name,
|
|
35
|
+
* so it refuses everything until then). A question there waits forever in a pane nobody sees —
|
|
36
|
+
* Karmen's first WSL test sat at "Which Bay?" (2026-10-06).
|
|
37
|
+
*/
|
|
38
|
+
exports.UNATTENDED_NOTE = "(Started from the BayChat app: nobody is at this keyboard. Never ask anything before you have joined; if you cannot join, say why in one line and stop.)";
|
|
39
|
+
/**
|
|
40
|
+
* The first message Claude Code receives: the BayChat skill, joining the chosen room — on the
|
|
41
|
+
* named Bay connection when the computer has several, so the skill never asks which.
|
|
42
|
+
*/
|
|
43
|
+
function baychatPrompt(sessionName, join, bayEntry) {
|
|
44
|
+
const room = !join ? "" : "sessions" in join ? " --sessions" : ` --group "${join.group}"`;
|
|
45
|
+
const bay = bayEntry ? ` --bay ${bayEntry}` : "";
|
|
46
|
+
const connection = bayEntry ? ` Use only the ${bayEntry} connection (its mcp__${bayEntry}__ tools).` : "";
|
|
47
|
+
return `/baychat ${sessionName}${room}${bay} ${exports.UNATTENDED_NOTE}${connection}`;
|
|
39
48
|
}
|
|
40
49
|
function buildLaunchCommand(input) {
|
|
41
50
|
const name = multiplexerSessionName(input.sessionName);
|
|
@@ -44,7 +53,7 @@ function buildLaunchCommand(input) {
|
|
|
44
53
|
args: [
|
|
45
54
|
...input.channelArgs,
|
|
46
55
|
...(input.settingsFile ? ["--settings", input.settingsFile] : []),
|
|
47
|
-
baychatPrompt(input.sessionName, input.join),
|
|
56
|
+
baychatPrompt(input.sessionName, input.join, input.bayEntry),
|
|
48
57
|
],
|
|
49
58
|
env: input.env,
|
|
50
59
|
});
|
|
@@ -3,6 +3,8 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.CONFIRM_DEADLINE_MS = exports.WARNING_POLL_MS = exports.WARNING_WATCH_MS = void 0;
|
|
4
4
|
exports.handleLaunch = handleLaunch;
|
|
5
5
|
exports.parseLaunch = parseLaunch;
|
|
6
|
+
exports.launchBayEntry = launchBayEntry;
|
|
7
|
+
const bays_1 = require("../../bays");
|
|
6
8
|
const policy_1 = require("../../launch/policy");
|
|
7
9
|
const argv_1 = require("./argv");
|
|
8
10
|
const approval_settings_1 = require("./approval-settings");
|
|
@@ -49,6 +51,11 @@ function startSession(launch, deps) {
|
|
|
49
51
|
return refused("RUNTIME_NOT_YET");
|
|
50
52
|
if (launch.join && "group" in launch.join && (0, argv_1.roomTitleProblem)(launch.join.group))
|
|
51
53
|
return refused("ROOM_INVALID");
|
|
54
|
+
const bay = launchBayEntry(launch.bay, deps.loadBays());
|
|
55
|
+
if (!bay) {
|
|
56
|
+
deps.log(`launch ${launch.id}: refused, this computer is not logged in to the Bay "${launch.bay?.name ?? ""}". Run \`baychat login\` here.`);
|
|
57
|
+
return refused("BAY_NOT_CONNECTED");
|
|
58
|
+
}
|
|
52
59
|
const folder = deps.resolveFolder(spot);
|
|
53
60
|
if (!folder.ok) {
|
|
54
61
|
deps.log(`launch ${launch.id}: refused, ${folder.reason}`);
|
|
@@ -79,7 +86,7 @@ function startSession(launch, deps) {
|
|
|
79
86
|
return refused("RUNTIME_MISSING");
|
|
80
87
|
// Monitor: no channel flag and no channel marker — the session listens with listen_messages + Monitor.
|
|
81
88
|
const channel = launch.listen === "monitor" ? { args: [], env: {} } : deps.channelLaunch(binary.path);
|
|
82
|
-
const settingsFile = (0, approval_settings_1.approvalPathFor)(launch.sessionName) === "hook" ? (0, approval_settings_1.writeApprovalSettings)(launch.sessionName, deps.cli, deps.launchDir) : undefined;
|
|
89
|
+
const settingsFile = (0, approval_settings_1.approvalPathFor)(launch.sessionName) === "hook" ? (0, approval_settings_1.writeApprovalSettings)(launch.sessionName, deps.cli, deps.launchDir, bay.entry) : undefined;
|
|
83
90
|
const command = (0, argv_1.buildLaunchCommand)({
|
|
84
91
|
multiplexer,
|
|
85
92
|
sessionName: launch.sessionName,
|
|
@@ -89,6 +96,7 @@ function startSession(launch, deps) {
|
|
|
89
96
|
env: { ...channel.env, PATH: deps.pathEnv },
|
|
90
97
|
settingsFile,
|
|
91
98
|
join: launch.join,
|
|
99
|
+
bayEntry: bay.entry,
|
|
92
100
|
});
|
|
93
101
|
const res = deps.run(command.file, command.args, { cwd: command.cwd, env: command.env });
|
|
94
102
|
if (res.error || res.status !== 0) {
|
|
@@ -286,10 +294,31 @@ function parseLaunch(raw) {
|
|
|
286
294
|
listen,
|
|
287
295
|
requestedBy: { id: requestedBy.id, name: requestedBy.name },
|
|
288
296
|
requestedByIsOwner: l.requestedByIsOwner === true,
|
|
297
|
+
...parseBay(l.bay),
|
|
289
298
|
// Only an explicit `true` authorises; a missing or odd value is a refusal.
|
|
290
299
|
commandAuthorized: l.commandAuthorized === true,
|
|
291
300
|
};
|
|
292
301
|
}
|
|
302
|
+
/**
|
|
303
|
+
* Which of this computer's Bay connections a launch joins on. `entry` is the MCP connection to
|
|
304
|
+
* name in the `/baychat` line — undefined when there is only one, so there is nothing to choose
|
|
305
|
+
* (a one-Bay computer's line stays exactly as before). Null: the launch's Bay has no connection
|
|
306
|
+
* here, and the session must not be started into some other Bay.
|
|
307
|
+
*
|
|
308
|
+
* The launcher runs in the DEFAULT Bay's relay, so a launch normally names that Bay; an older
|
|
309
|
+
* server names none, which means the same.
|
|
310
|
+
*/
|
|
311
|
+
function launchBayEntry(bay, bays) {
|
|
312
|
+
// No saved Bay set (an older login, or --token): the one `baychat` connection, as always.
|
|
313
|
+
if (bays.length === 0)
|
|
314
|
+
return { entry: undefined };
|
|
315
|
+
const found = bay ? bays.find((b) => b.bayId === bay.id) : bays.find((b) => b.isDefault);
|
|
316
|
+
if (!found)
|
|
317
|
+
return null;
|
|
318
|
+
if (bays.length < 2)
|
|
319
|
+
return { entry: undefined };
|
|
320
|
+
return { entry: found.isDefault ? bays_1.DEFAULT_MCP_ENTRY : (0, bays_1.bayEntryName)(found, bays) };
|
|
321
|
+
}
|
|
293
322
|
/** `null` is "join nothing"; undefined is a join that is present but malformed. */
|
|
294
323
|
function parseJoin(raw) {
|
|
295
324
|
if (raw === null || raw === undefined)
|
|
@@ -301,3 +330,8 @@ function parseJoin(raw) {
|
|
|
301
330
|
return { sessions: true };
|
|
302
331
|
return undefined;
|
|
303
332
|
}
|
|
333
|
+
/** An optional `{id, name}`; anything else is treated as absent (an older server sends none). */
|
|
334
|
+
function parseBay(raw) {
|
|
335
|
+
const b = raw;
|
|
336
|
+
return b && typeof b.id === "string" && b.id !== "" ? { bay: { id: b.id, name: typeof b.name === "string" ? b.name : "" } } : {};
|
|
337
|
+
}
|
|
@@ -38,6 +38,7 @@ exports.startLauncher = startLauncher;
|
|
|
38
38
|
const fs = __importStar(require("fs"));
|
|
39
39
|
const api_1 = require("../../api");
|
|
40
40
|
const launch_1 = require("../../channel/launch");
|
|
41
|
+
const config_1 = require("../../config");
|
|
41
42
|
const claude_trust_1 = require("../../launch/claude-trust");
|
|
42
43
|
const policy_1 = require("../../launch/policy");
|
|
43
44
|
const runtime_binary_1 = require("../../runtime-binary");
|
|
@@ -148,6 +149,7 @@ function realLoopDeps(log, signal) {
|
|
|
148
149
|
loadPolicy: () => (0, policy_1.loadLaunchPolicy)(),
|
|
149
150
|
resolveFolder: policy_1.resolveSpotFolder,
|
|
150
151
|
isFolderTrusted: (folder) => (0, claude_trust_1.isFolderTrusted)(folder),
|
|
152
|
+
loadBays: () => (0, config_1.loadBayLogins)(),
|
|
151
153
|
resolveBinary: (name) => (0, runtime_binary_1.resolveRuntimeBinary)(name, binaryEnv()),
|
|
152
154
|
channelLaunch: (claudePath) => {
|
|
153
155
|
const launch = (0, launch_1.channelLaunchFor)(claudePath);
|
package/dist/relay/updates.js
CHANGED
|
@@ -68,7 +68,14 @@ function sleepUnlessAborted(ms, signal) {
|
|
|
68
68
|
* means it resumes by itself when a Bay suspension is lifted.
|
|
69
69
|
*/
|
|
70
70
|
function standingRefusal(err) {
|
|
71
|
-
if (!(err instanceof api_1.ApiError)
|
|
71
|
+
if (!(err instanceof api_1.ApiError))
|
|
72
|
+
return null;
|
|
73
|
+
// 410 BAY_DELETED: an owner deleted the Bay this laptop is logged in to (the API's
|
|
74
|
+
// docs/features/BAY_DELETE_AND_DEFAULT.md). Not a bad credential either — an owner can restore
|
|
75
|
+
// the Bay for 30 days and this same login works again — so it pauses like a suspension.
|
|
76
|
+
if (err.status === 410 && err.code === "BAY_DELETED")
|
|
77
|
+
return "deleted";
|
|
78
|
+
if (err.status !== 403)
|
|
72
79
|
return null;
|
|
73
80
|
if (err.code === "ACCOUNT_SUSPENDED")
|
|
74
81
|
return "account";
|
|
@@ -76,6 +83,19 @@ function standingRefusal(err) {
|
|
|
76
83
|
return "bay";
|
|
77
84
|
return null;
|
|
78
85
|
}
|
|
86
|
+
/** What the paused relay says, per standing refusal. */
|
|
87
|
+
function standingMessage(standing, minutes) {
|
|
88
|
+
if (standing === "account") {
|
|
89
|
+
return `this BayChat account has been suspended — the relay is paused and will check again in ${minutes} min. ` +
|
|
90
|
+
"Logging in again will not help; if you think this is a mistake, contact support@baychat.io.";
|
|
91
|
+
}
|
|
92
|
+
if (standing === "deleted") {
|
|
93
|
+
return `the Bay this machine is logged in to was deleted by its owner — the relay is paused and will check again in ${minutes} min ` +
|
|
94
|
+
"(an owner can restore it in the app for 30 days). To use another of your Bays here: baychat bays default <name>.";
|
|
95
|
+
}
|
|
96
|
+
return `the Bay this machine is logged in to has been suspended — the relay is paused and will check again in ${minutes} min. ` +
|
|
97
|
+
"If you think this is a mistake, contact support@baychat.io.";
|
|
98
|
+
}
|
|
79
99
|
/**
|
|
80
100
|
* Transient failures are ridden out, not surfaced. A 502 while the api
|
|
81
101
|
* container restarts, a network throw, a 429 — all mean "later", not "stop".
|
|
@@ -171,11 +191,11 @@ async function runUpdatesLoop(opts) {
|
|
|
171
191
|
const standing = standingRefusal(err);
|
|
172
192
|
if (standing) {
|
|
173
193
|
const minutes = Math.round(suspendedRecheckMs / 60_000);
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
:
|
|
178
|
-
|
|
194
|
+
// The error keeps the server's code, so the daemon can tell a deleted Bay apart
|
|
195
|
+
// (`baychat bays` marks it) without reading the sentence.
|
|
196
|
+
onError?.(Object.assign(new Error(standingMessage(standing, minutes)), {
|
|
197
|
+
code: standing === "account" ? "ACCOUNT_SUSPENDED" : standing === "deleted" ? "BAY_DELETED" : "BAY_SUSPENDED",
|
|
198
|
+
}), true);
|
|
179
199
|
await sleepUnlessAborted(suspendedRecheckMs, signal);
|
|
180
200
|
backoff = BACKOFF_MIN_MS;
|
|
181
201
|
continue;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.29.
|
|
3
|
+
"version": "0.29.4",
|
|
4
4
|
"description": "BayChat connector CLI — pair an agent session (Claude Code, Codex, Cursor) with BayChat, or have the relay run DeepSeek or the Cursor CLI for you",
|
|
5
5
|
"bin": {
|
|
6
6
|
"baychat": "dist/index.js"
|