baychat 0.27.1 → 0.28.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/.claude-plugin/plugin.json +12 -0
- package/README.md +1 -1
- package/dist/channel/detect.js +10 -2
- package/dist/channel/feed.js +1 -1
- package/dist/channel/launch.js +46 -5
- package/dist/channel/permission.js +118 -0
- package/dist/channel/server.js +101 -5
- package/dist/claude-onboarding.js +7 -5
- package/dist/connect-claude.js +6 -1
- package/dist/skill-bootstrap.js +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "baychat",
|
|
3
|
+
"displayName": "BayChat",
|
|
4
|
+
"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).",
|
|
5
|
+
"author": { "name": "BayChat", "url": "https://baychat.io" },
|
|
6
|
+
"homepage": "https://baychat.io",
|
|
7
|
+
"keywords": ["baychat", "chat", "channel", "mobile", "notifications"],
|
|
8
|
+
"mcpServers": {
|
|
9
|
+
"channel": { "command": "baychat", "args": ["channel"] }
|
|
10
|
+
},
|
|
11
|
+
"channels": [{ "server": "channel", "displayName": "BayChat" }]
|
|
12
|
+
}
|
package/README.md
CHANGED
|
@@ -112,7 +112,7 @@ the old shell/relay join is available only as an explicitly chosen fallback.
|
|
|
112
112
|
Updating npm alone does not refresh a skill already loaded in a running session.
|
|
113
113
|
|
|
114
114
|
**No Monitor deadline with the channel.** `baychat connect claude` also registers
|
|
115
|
-
|
|
115
|
+
a local Claude Code channel (as the `baychat` plugin, from https://baychat.io/claude/marketplace.json). Start Claude Code with
|
|
116
116
|
`baychat claude` (or `baychat start`, which uses tmux when installed)
|
|
117
117
|
and the skill calls its `listen` tool instead of opening a Monitor: the channel holds the
|
|
118
118
|
feed, reconnects by itself, and wakes the session only when a message arrives. Started
|
package/dist/channel/detect.js
CHANGED
|
@@ -33,7 +33,8 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.DEV_CHANNELS_FLAG = exports.CHANNEL_FLAG_VALUE = exports.CHANNEL_MCP_ENTRY = void 0;
|
|
36
|
+
exports.DEV_CHANNELS_FLAG = exports.PLUGIN_FLAG_VALUE = exports.PLUGIN_ID = exports.CHANNEL_FLAG_VALUE = exports.CHANNEL_MCP_ENTRY = void 0;
|
|
37
|
+
exports.isOurChannelValue = isOurChannelValue;
|
|
37
38
|
exports.argsEnableChannel = argsEnableChannel;
|
|
38
39
|
exports.realProcessReader = realProcessReader;
|
|
39
40
|
exports.detectChannel = detectChannel;
|
|
@@ -67,6 +68,13 @@ const fs = __importStar(require("node:fs"));
|
|
|
67
68
|
exports.CHANNEL_MCP_ENTRY = "baychat_channel";
|
|
68
69
|
/** What `--dangerously-load-development-channels` takes for a bare MCP server. */
|
|
69
70
|
exports.CHANNEL_FLAG_VALUE = `server:${exports.CHANNEL_MCP_ENTRY}`;
|
|
71
|
+
/** The plugin's id and its channel entry, when BayChat is installed as a Claude Code plugin. */
|
|
72
|
+
exports.PLUGIN_ID = "baychat@baychat";
|
|
73
|
+
exports.PLUGIN_FLAG_VALUE = `plugin:${exports.PLUGIN_ID}`;
|
|
74
|
+
/** Does one channel-flag value name BayChat's channel, as a server or as the plugin (any marketplace)? */
|
|
75
|
+
function isOurChannelValue(value) {
|
|
76
|
+
return value === exports.CHANNEL_FLAG_VALUE || /^plugin:baychat@[A-Za-z0-9._-]+$/.test(value);
|
|
77
|
+
}
|
|
70
78
|
exports.DEV_CHANNELS_FLAG = "--dangerously-load-development-channels";
|
|
71
79
|
const CHANNEL_FLAGS = [exports.DEV_CHANNELS_FLAG, "--channels"];
|
|
72
80
|
/** How far up the process tree to look. */
|
|
@@ -88,7 +96,7 @@ function argsEnableChannel(args) {
|
|
|
88
96
|
for (let j = i + 1; j < args.length && !args[j].startsWith("-"); j++)
|
|
89
97
|
values.push(...args[j].split(/[\s,]+/));
|
|
90
98
|
}
|
|
91
|
-
if (values.
|
|
99
|
+
if (values.some(isOurChannelValue))
|
|
92
100
|
return true;
|
|
93
101
|
}
|
|
94
102
|
return false;
|
package/dist/channel/feed.js
CHANGED
|
@@ -183,7 +183,7 @@ class ChannelFeed {
|
|
|
183
183
|
this.stop();
|
|
184
184
|
await this.deps.push(`BayChat stopped listening for session "${this.deps.session}": ${minted.detail} ` +
|
|
185
185
|
`If the session expired, call join_session again with the same name and room, then call ` +
|
|
186
|
-
`
|
|
186
|
+
`this BayChat channel's listen tool again. Until then this session does not hear the Bay.`, this.meta({ event: "stopped" }));
|
|
187
187
|
return;
|
|
188
188
|
}
|
|
189
189
|
this.scheduleReconnect(`could not reconnect (${minted.detail})`);
|
package/dist/channel/launch.js
CHANGED
|
@@ -33,7 +33,7 @@ var __importStar = (this && this.__importStar) || (function () {
|
|
|
33
33
|
};
|
|
34
34
|
})();
|
|
35
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
36
|
-
exports.CHANNEL_ENV = exports.CHANNEL_LAUNCH_COMMAND = exports.CHANNEL_LAUNCH_ARGS = exports.CHANNEL_MIN_CLAUDE = void 0;
|
|
36
|
+
exports.CHANNEL_ENV = exports.CHANNEL_LAUNCH_COMMAND = exports.PLUGIN_MARKETPLACE_URL = exports.CHANNEL_LAUNCH_ARGS = exports.CHANNEL_MIN_CLAUDE = void 0;
|
|
37
37
|
exports.parseClaudeVersion = parseClaudeVersion;
|
|
38
38
|
exports.claudeSupportsChannel = claudeSupportsChannel;
|
|
39
39
|
exports.channelLaunchFor = channelLaunchFor;
|
|
@@ -42,6 +42,9 @@ exports.claudeChannelAddArgs = claudeChannelAddArgs;
|
|
|
42
42
|
exports.registerChannelWithClaude = registerChannelWithClaude;
|
|
43
43
|
exports.claudeUserConfigPath = claudeUserConfigPath;
|
|
44
44
|
exports.channelRegistered = channelRegistered;
|
|
45
|
+
exports.claudeUserSettingsPath = claudeUserSettingsPath;
|
|
46
|
+
exports.pluginEnabled = pluginEnabled;
|
|
47
|
+
exports.installPluginWithClaude = installPluginWithClaude;
|
|
45
48
|
/**
|
|
46
49
|
* Getting the channel into Claude Code: registering the server, and starting sessions with it on.
|
|
47
50
|
*
|
|
@@ -77,8 +80,10 @@ function claudeSupportsChannel(version) {
|
|
|
77
80
|
}
|
|
78
81
|
return true;
|
|
79
82
|
}
|
|
80
|
-
/** The arguments
|
|
83
|
+
/** The arguments that switch the hand-registered channel server on. */
|
|
81
84
|
exports.CHANNEL_LAUNCH_ARGS = [detect_1.DEV_CHANNELS_FLAG, detect_1.CHANNEL_FLAG_VALUE];
|
|
85
|
+
/** Where `claude plugin marketplace add` finds BayChat's plugin catalog (served by the web app). */
|
|
86
|
+
exports.PLUGIN_MARKETPLACE_URL = "https://baychat.io/claude/marketplace.json";
|
|
82
87
|
/** What to type to start Claude Code with the channel on. */
|
|
83
88
|
exports.CHANNEL_LAUNCH_COMMAND = "baychat claude";
|
|
84
89
|
/**
|
|
@@ -92,14 +97,20 @@ exports.CHANNEL_ENV = { BAYCHAT_CHANNEL: "1" };
|
|
|
92
97
|
* stop it from starting at all.
|
|
93
98
|
*/
|
|
94
99
|
function channelLaunchFor(claudePath, deps = {}) {
|
|
95
|
-
|
|
96
|
-
|
|
100
|
+
// Every way BayChat's channel is installed here, each named on the flag. Both can exist after a
|
|
101
|
+
// move to the plugin; Claude Code then keeps the hand-registered server and hides the plugin's
|
|
102
|
+
// duplicate, so naming both is what keeps the channel switched on either way.
|
|
103
|
+
const entries = [
|
|
104
|
+
...((deps.plugin ?? (() => pluginEnabled()))() ? [detect_1.PLUGIN_FLAG_VALUE] : []),
|
|
105
|
+
...((deps.registered ?? (() => channelRegistered()))() ? [detect_1.CHANNEL_FLAG_VALUE] : []),
|
|
106
|
+
];
|
|
107
|
+
if (!entries.length)
|
|
97
108
|
return { on: false, args: [], env: {}, why: "the BayChat channel is not set up here (run `baychat connect claude`)" };
|
|
98
109
|
const text = (deps.version ?? probeVersion)(claudePath);
|
|
99
110
|
if (!claudeSupportsChannel(text ? parseClaudeVersion(text) : undefined)) {
|
|
100
111
|
return { on: false, args: [], env: {}, why: `Claude Code ${exports.CHANNEL_MIN_CLAUDE.join(".")} or newer is needed for the channel (run \`claude update\`)` };
|
|
101
112
|
}
|
|
102
|
-
return { on: true, args: [...
|
|
113
|
+
return { on: true, args: [detect_1.DEV_CHANNELS_FLAG, ...entries], env: { ...exports.CHANNEL_ENV } };
|
|
103
114
|
}
|
|
104
115
|
function probeVersion(claudePath) {
|
|
105
116
|
const probe = (0, node_child_process_1.spawnSync)(claudePath, ["--version"], { encoding: "utf8", timeout: 10_000 });
|
|
@@ -157,3 +168,33 @@ function channelRegistered(file = claudeUserConfigPath(), read = (p) => fs.readF
|
|
|
157
168
|
return false;
|
|
158
169
|
}
|
|
159
170
|
}
|
|
171
|
+
/** Claude Code's user settings, where installed plugins are switched on. */
|
|
172
|
+
function claudeUserSettingsPath(home = os.homedir()) {
|
|
173
|
+
return path.join(home, ".claude", "settings.json");
|
|
174
|
+
}
|
|
175
|
+
/** Is BayChat's plugin installed and switched on for this user? Unreadable counts as no. */
|
|
176
|
+
function pluginEnabled(file = claudeUserSettingsPath(), read = (p) => fs.readFileSync(p, "utf8")) {
|
|
177
|
+
try {
|
|
178
|
+
const parsed = JSON.parse(read(file));
|
|
179
|
+
return parsed?.enabledPlugins?.[detect_1.PLUGIN_ID] === true;
|
|
180
|
+
}
|
|
181
|
+
catch {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
/**
|
|
186
|
+
* Install BayChat as a Claude Code plugin: add the catalog from baychat.io, install from it (the
|
|
187
|
+
* plugin itself is fetched from the `baychat` npm package), then remove the hand-registered
|
|
188
|
+
* server, which would otherwise hide the plugin's. Returns false at the first step that fails,
|
|
189
|
+
* leaving whatever worked before untouched — the caller then registers the server by hand.
|
|
190
|
+
*/
|
|
191
|
+
function installPluginWithClaude(run) {
|
|
192
|
+
const added = run(["plugin", "marketplace", "add", exports.PLUGIN_MARKETPLACE_URL]);
|
|
193
|
+
if (added.status !== 0 && !/already/i.test(added.stderr))
|
|
194
|
+
return false;
|
|
195
|
+
const installed = run(["plugin", "install", detect_1.PLUGIN_ID]);
|
|
196
|
+
if (installed.status !== 0 && !/already installed/i.test(installed.stderr))
|
|
197
|
+
return false;
|
|
198
|
+
run(["mcp", "remove", "--scope", "user", detect_1.CHANNEL_MCP_ENTRY]); // Absent is fine.
|
|
199
|
+
return true;
|
|
200
|
+
}
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PermissionRelay = exports.RELAY_OPTIONS = void 0;
|
|
4
|
+
exports.composeRelayQuestion = composeRelayQuestion;
|
|
5
|
+
/** Button labels, and the ONLY index that means yes. Same as `baychat approve-hook`. */
|
|
6
|
+
exports.RELAY_OPTIONS = ["Allow this once", "Deny"];
|
|
7
|
+
const ALLOW_INDEX = 0;
|
|
8
|
+
/** One window of the server's wait; it answers "waiting" when nobody tapped yet. */
|
|
9
|
+
const POLL_WAIT_SECONDS = 25;
|
|
10
|
+
const MAX_CONSECUTIVE_FAILURES = 5;
|
|
11
|
+
const RETRY_DELAY_MS = 3_000;
|
|
12
|
+
const QUESTION_MAX = 2_000;
|
|
13
|
+
const PREVIEW_MAX = 1_200;
|
|
14
|
+
function clip(text, max) {
|
|
15
|
+
return text.length <= max ? text : `${text.slice(0, max - 1)}…`;
|
|
16
|
+
}
|
|
17
|
+
/** What the person on the phone reads. They decide from this text alone. */
|
|
18
|
+
function composeRelayQuestion(req, host, session) {
|
|
19
|
+
const lines = [
|
|
20
|
+
`Claude Code (session "${session}" on ${host}) asks to use ${req.tool_name}: ${req.description}`,
|
|
21
|
+
"",
|
|
22
|
+
req.input_preview ? clip(req.input_preview, PREVIEW_MAX) : "",
|
|
23
|
+
"",
|
|
24
|
+
"The same question is open in the terminal; whichever answer comes first counts. " +
|
|
25
|
+
"Allowing runs it on that machine, now. If you did not expect this, deny.",
|
|
26
|
+
];
|
|
27
|
+
return clip(lines.join("\n").replace(/\n{3,}/g, "\n\n").trim(), QUESTION_MAX);
|
|
28
|
+
}
|
|
29
|
+
class PermissionRelay {
|
|
30
|
+
deps;
|
|
31
|
+
current;
|
|
32
|
+
constructor(deps) {
|
|
33
|
+
this.deps = deps;
|
|
34
|
+
}
|
|
35
|
+
/** Relay one dialog. Resolves when it is settled, superseded, or given up on. Never throws. */
|
|
36
|
+
async onRequest(req) {
|
|
37
|
+
await this.retire();
|
|
38
|
+
const target = this.deps.target();
|
|
39
|
+
if (!target)
|
|
40
|
+
return; // Nobody is listening through this channel: the terminal alone decides.
|
|
41
|
+
const pending = { claudeId: req.request_id, login: target.login, retired: false };
|
|
42
|
+
this.current = pending;
|
|
43
|
+
try {
|
|
44
|
+
let reply = await this.deps.api(target.login, "POST", "/api/device-api/approvals", {
|
|
45
|
+
session: target.session,
|
|
46
|
+
question: composeRelayQuestion(req, this.deps.host(), target.session),
|
|
47
|
+
options: [...exports.RELAY_OPTIONS],
|
|
48
|
+
// The private chat with the owner: a session is usually in a group too, and a prompt about
|
|
49
|
+
// one person's machine belongs to that person, not to the room.
|
|
50
|
+
direct: true,
|
|
51
|
+
wait: 1,
|
|
52
|
+
});
|
|
53
|
+
pending.cardId = typeof reply.requestId === "string" && reply.requestId ? reply.requestId : undefined;
|
|
54
|
+
if (!pending.cardId) {
|
|
55
|
+
this.deps.log("permission relay: BayChat returned no card id");
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
let failures = 0;
|
|
59
|
+
for (;;) {
|
|
60
|
+
if (pending.retired) {
|
|
61
|
+
await this.withdraw(pending);
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
if (reply.status === "answered") {
|
|
65
|
+
await this.deps.verdict(req.request_id, reply.answerIndex === ALLOW_INDEX ? "allow" : "deny");
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (reply.status !== "waiting")
|
|
69
|
+
return; // cancelled, or a status we do not know: no verdict.
|
|
70
|
+
try {
|
|
71
|
+
reply = await this.deps.api(pending.login, "GET", `/api/device-api/approvals/${encodeURIComponent(pending.cardId)}?wait=${POLL_WAIT_SECONDS}`);
|
|
72
|
+
failures = 0;
|
|
73
|
+
}
|
|
74
|
+
catch (err) {
|
|
75
|
+
failures++;
|
|
76
|
+
this.deps.log(`permission relay: waiting failed (${err instanceof Error ? err.message : String(err)}), attempt ${failures}`);
|
|
77
|
+
if (failures >= MAX_CONSECUTIVE_FAILURES) {
|
|
78
|
+
// Give the decision back to the terminal alone, and take the card off the phone.
|
|
79
|
+
await this.withdraw(pending);
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
await (this.deps.sleep ?? ((ms) => new Promise((r) => setTimeout(r, ms))))(RETRY_DELAY_MS);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
this.deps.log(`permission relay: could not ask BayChat (${err instanceof Error ? err.message : String(err)}); the terminal still decides`);
|
|
88
|
+
}
|
|
89
|
+
finally {
|
|
90
|
+
if (this.current === pending)
|
|
91
|
+
this.current = undefined;
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
/** The current dialog is over (a newer one opened, or shutdown): stop and withdraw its card. */
|
|
95
|
+
async retire() {
|
|
96
|
+
const pending = this.current;
|
|
97
|
+
if (!pending)
|
|
98
|
+
return;
|
|
99
|
+
pending.retired = true;
|
|
100
|
+
this.current = undefined;
|
|
101
|
+
await this.withdraw(pending);
|
|
102
|
+
}
|
|
103
|
+
async withdraw(pending) {
|
|
104
|
+
if (!pending.cardId)
|
|
105
|
+
return; // Not created yet: the create path sees `retired` and withdraws.
|
|
106
|
+
const cardId = pending.cardId;
|
|
107
|
+
pending.cardId = undefined;
|
|
108
|
+
try {
|
|
109
|
+
await this.deps.api(pending.login, "POST", `/api/device-api/approvals/${encodeURIComponent(cardId)}/withdraw`, {});
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
// An older server (no route) or a blip: the card stays up, and a tap on it is dropped by
|
|
113
|
+
// Claude Code as an unknown id. Untidy, never unsafe.
|
|
114
|
+
this.deps.log(`permission relay: could not withdraw card ${cardId} (${err instanceof Error ? err.message : String(err)})`);
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
exports.PermissionRelay = PermissionRelay;
|
package/dist/channel/server.js
CHANGED
|
@@ -1,4 +1,37 @@
|
|
|
1
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
|
+
})();
|
|
2
35
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
36
|
exports.ChannelService = exports.nodeFeedConnect = exports.CHANNEL_INSTRUCTIONS = void 0;
|
|
4
37
|
exports.realLogin = realLogin;
|
|
@@ -23,20 +56,26 @@ const stdio_js_1 = require("@modelcontextprotocol/sdk/server/stdio.js");
|
|
|
23
56
|
const index_js_2 = require("@modelcontextprotocol/sdk/client/index.js");
|
|
24
57
|
const streamableHttp_js_1 = require("@modelcontextprotocol/sdk/client/streamableHttp.js");
|
|
25
58
|
const types_js_1 = require("@modelcontextprotocol/sdk/types.js");
|
|
59
|
+
const os = __importStar(require("node:os"));
|
|
60
|
+
const zod_1 = require("zod");
|
|
61
|
+
const api_1 = require("../api");
|
|
26
62
|
const bays_1 = require("../bays");
|
|
27
63
|
const config_1 = require("../config");
|
|
28
64
|
const ws_1 = require("../relay/ws");
|
|
29
65
|
const detect_1 = require("./detect");
|
|
30
66
|
const feed_1 = require("./feed");
|
|
67
|
+
const permission_1 = require("./permission");
|
|
31
68
|
/** Delivered to Claude as context when this server connects. */
|
|
32
69
|
exports.CHANNEL_INSTRUCTIONS = `This is BayChat's wake-up channel. After join_session, call this server's listen tool with the ` +
|
|
33
70
|
`confirmed session name (and bay, when the session joined a Bay other than the default). If it ` +
|
|
34
|
-
`answers active, BayChat messages arrive as <channel source="
|
|
71
|
+
`answers active, BayChat messages arrive as <channel source="…" session="…" ` +
|
|
35
72
|
`event="messages" conversation_ids="…">: read those conversations with the remote BayChat ` +
|
|
36
73
|
`get_messages tool (the same Bay's tools the session joined with), obey shouldRespond and answer ` +
|
|
37
74
|
`in BayChat. Do NOT also open a Monitor or call listen_messages: that would replace this feed. ` +
|
|
38
75
|
`If listen answers inactive or failed, use listen_messages and a native Monitor as before. ` +
|
|
39
|
-
`event="reset" means catch up with get_messages; event="stopped" means the feed ended and says why
|
|
76
|
+
`event="reset" means catch up with get_messages; event="stopped" means the feed ended and says why. ` +
|
|
77
|
+
`Tool-permission prompts are also offered to the session's owner as a BayChat card; answer in the terminal or ` +
|
|
78
|
+
`on the phone, whichever is first.`;
|
|
40
79
|
const LISTEN_TOOL = {
|
|
41
80
|
name: "listen",
|
|
42
81
|
description: "Hear this BayChat session's incoming messages through the Claude Code channel instead of a Monitor. " +
|
|
@@ -167,6 +206,8 @@ class ChannelService {
|
|
|
167
206
|
deps;
|
|
168
207
|
push;
|
|
169
208
|
feeds = new Map();
|
|
209
|
+
/** The most recent session that listened successfully: where permission prompts go. */
|
|
210
|
+
last;
|
|
170
211
|
constructor(deps, push) {
|
|
171
212
|
this.deps = deps;
|
|
172
213
|
this.push = push;
|
|
@@ -217,17 +258,40 @@ class ChannelService {
|
|
|
217
258
|
const started = await feed.start();
|
|
218
259
|
if (!started.ok) {
|
|
219
260
|
this.feeds.delete(key);
|
|
261
|
+
if (this.last?.key === key)
|
|
262
|
+
this.last = undefined;
|
|
220
263
|
return toolResult("failed", `Failed: ${started.detail} Use listen_messages and a native Monitor instead.`, { session });
|
|
221
264
|
}
|
|
265
|
+
this.last = { key, session, bay };
|
|
222
266
|
return toolResult("active", `Active: session "${session}"${bayName ? ` (${bayName})` : ""} now hears the Bay through the channel. New messages ` +
|
|
223
|
-
`arrive here as <channel
|
|
267
|
+
`arrive here as <channel event="messages"> events, with no deadline and no re-arming. Do NOT open a ` +
|
|
224
268
|
`Monitor or call listen_messages — either would replace this feed. Now read one get_messages batch (limit 10) ` +
|
|
225
269
|
`to catch up on anything from before listening.`, { session, ...(bayName ? { bay: bayName } : {}), how: evidence.how });
|
|
226
270
|
}
|
|
271
|
+
/**
|
|
272
|
+
* Where a permission prompt goes: the session that most recently started listening, while its
|
|
273
|
+
* feed is still up. None → the prompt stays in the terminal only.
|
|
274
|
+
*/
|
|
275
|
+
permissionTarget() {
|
|
276
|
+
const last = this.last;
|
|
277
|
+
if (!last)
|
|
278
|
+
return undefined;
|
|
279
|
+
const state = this.feeds.get(last.key)?.currentState;
|
|
280
|
+
if (!state || state === "stopped")
|
|
281
|
+
return undefined;
|
|
282
|
+
try {
|
|
283
|
+
const login = this.deps.login(last.bay);
|
|
284
|
+
return { session: last.session, login: { baseUrl: login.baseUrl, token: login.token } };
|
|
285
|
+
}
|
|
286
|
+
catch {
|
|
287
|
+
return undefined;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
227
290
|
stopAll() {
|
|
228
291
|
for (const feed of this.feeds.values())
|
|
229
292
|
feed.stop();
|
|
230
293
|
this.feeds.clear();
|
|
294
|
+
this.last = undefined;
|
|
231
295
|
}
|
|
232
296
|
}
|
|
233
297
|
exports.ChannelService = ChannelService;
|
|
@@ -235,11 +299,24 @@ function safeJsonStatus(text) {
|
|
|
235
299
|
const parsed = safeJson(text);
|
|
236
300
|
return parsed?.status;
|
|
237
301
|
}
|
|
302
|
+
/** What Claude Code sends when a tool-approval dialog opens (channels reference). */
|
|
303
|
+
const PermissionRequestSchema = zod_1.z.object({
|
|
304
|
+
method: zod_1.z.literal("notifications/claude/channel/permission_request"),
|
|
305
|
+
params: zod_1.z.object({
|
|
306
|
+
request_id: zod_1.z.string(),
|
|
307
|
+
tool_name: zod_1.z.string(),
|
|
308
|
+
description: zod_1.z.string(),
|
|
309
|
+
input_preview: zod_1.z.string(),
|
|
310
|
+
}),
|
|
311
|
+
});
|
|
238
312
|
/** Boot on stdio and block on the transport. */
|
|
239
313
|
async function startChannelServer(deps) {
|
|
240
314
|
const log = deps?.log ?? ((line) => console.error(`[baychat channel] ${line}`));
|
|
241
315
|
const mcp = new index_js_1.Server({ name: detect_1.CHANNEL_MCP_ENTRY, version: "1" }, {
|
|
242
|
-
|
|
316
|
+
// `claude/channel/permission`: Claude Code also offers this channel its tool-approval
|
|
317
|
+
// dialogs (permission.ts). Safe to declare because only the session's owner can answer the
|
|
318
|
+
// card it becomes — the docs' condition for declaring it.
|
|
319
|
+
capabilities: { experimental: { "claude/channel": {}, "claude/channel/permission": {} }, tools: {} },
|
|
243
320
|
instructions: exports.CHANNEL_INSTRUCTIONS,
|
|
244
321
|
});
|
|
245
322
|
const service = new ChannelService({
|
|
@@ -256,6 +333,23 @@ async function startChannelServer(deps) {
|
|
|
256
333
|
log(`could not hand an event to Claude Code: ${err instanceof Error ? err.message : String(err)}`);
|
|
257
334
|
}
|
|
258
335
|
});
|
|
336
|
+
const relay = new permission_1.PermissionRelay({
|
|
337
|
+
target: () => service.permissionTarget(),
|
|
338
|
+
api: (login, method, path, body) => (0, api_1.apiRequest)(login, method, path, body),
|
|
339
|
+
verdict: async (requestId, behavior) => {
|
|
340
|
+
try {
|
|
341
|
+
await mcp.notification({ method: "notifications/claude/channel/permission", params: { request_id: requestId, behavior } });
|
|
342
|
+
}
|
|
343
|
+
catch (err) {
|
|
344
|
+
log(`could not hand a verdict to Claude Code: ${err instanceof Error ? err.message : String(err)}`);
|
|
345
|
+
}
|
|
346
|
+
},
|
|
347
|
+
host: () => os.hostname(),
|
|
348
|
+
log,
|
|
349
|
+
});
|
|
350
|
+
mcp.setNotificationHandler(PermissionRequestSchema, async ({ params }) => {
|
|
351
|
+
void relay.onRequest(params);
|
|
352
|
+
});
|
|
259
353
|
mcp.setRequestHandler(types_js_1.ListToolsRequestSchema, async () => ({ tools: [LISTEN_TOOL] }));
|
|
260
354
|
mcp.setRequestHandler(types_js_1.CallToolRequestSchema, async (req) => {
|
|
261
355
|
if (req.params.name === LISTEN_TOOL.name)
|
|
@@ -266,7 +360,9 @@ async function startChannelServer(deps) {
|
|
|
266
360
|
// keep this process alive with nobody left to tell, holding the session "connected".
|
|
267
361
|
const shutdown = () => {
|
|
268
362
|
service.stopAll();
|
|
269
|
-
|
|
363
|
+
// Take any open approval card off the phone before going: its dialog dies with this session.
|
|
364
|
+
// Bounded, so a dead network cannot hold the exit.
|
|
365
|
+
void Promise.race([relay.retire(), new Promise((r) => setTimeout(r, 2_000))]).finally(() => process.exit(0));
|
|
270
366
|
};
|
|
271
367
|
mcp.onclose = shutdown;
|
|
272
368
|
process.stdin.once("end", shutdown);
|
|
@@ -5,7 +5,8 @@ exports.BAY_CHOICE = exports.CLAUDE_REACHABILITY = exports.CLAUDE_JOIN_STEPS = v
|
|
|
5
5
|
exports.CLAUDE_JOIN_STEPS = `1. Use **remote BayChat MCP + native WebSocket Monitor**. Search deferred tools once
|
|
6
6
|
if needed, for \`join_session\`, \`listen_messages\`, \`get_delivery_status\`,
|
|
7
7
|
\`send_message\` and \`get_messages\` (also \`create_group\` for a named group),
|
|
8
|
-
and
|
|
8
|
+
and BayChat's channel \`listen\` tool when it is present
|
|
9
|
+
(\`mcp__plugin_baychat_channel__listen\` or \`mcp__baychat_channel__listen\`).
|
|
9
10
|
Check that native Monitor supports \`ws\` before creating membership.
|
|
10
11
|
If a required BayChat tool is absent, follow Automatic setup above once,
|
|
11
12
|
then continue these steps with the original name and room when tools are loaded.
|
|
@@ -24,10 +25,11 @@ exports.CLAUDE_JOIN_STEPS = `1. Use **remote BayChat MCP + native WebSocket Moni
|
|
|
24
25
|
Do not ask again when the owner already requested creation. A supplied group title
|
|
25
26
|
also authorizes creation on GROUP_NOT_FOUND; never substitute a similarly named group.
|
|
26
27
|
If blocked, state the exact requested title and needed action in your reply.
|
|
27
|
-
3. **Channel first.** If
|
|
28
|
-
(\`
|
|
28
|
+
3. **Channel first.** If BayChat's channel \`listen\` tool exists
|
|
29
|
+
(\`mcp__plugin_baychat_channel__listen\` from the plugin, or
|
|
30
|
+
\`mcp__baychat_channel__listen\`), call it with \`{session: "<confirmed name>"}\`
|
|
29
31
|
(add \`bay: "<Bay>"\` when the session joined a Bay other than the default).
|
|
30
|
-
If it answers **active**, messages now arrive as \`<channel
|
|
32
|
+
If it answers **active**, messages now arrive as \`<channel … event="messages">\`
|
|
31
33
|
events: open NO Monitor and do not call \`listen_messages\` — either replaces the
|
|
32
34
|
channel's feed. Go to step 4. If it answers inactive or failed, or the tool is
|
|
33
35
|
absent, use the Monitor instead:
|
|
@@ -60,7 +62,7 @@ exports.CLAUDE_JOIN_STEPS = `1. Use **remote BayChat MCP + native WebSocket Moni
|
|
|
60
62
|
exports.CLAUDE_REACHABILITY = `## Staying reachable — native remote messages
|
|
61
63
|
|
|
62
64
|
**With the channel** (\`listen\` answered active): each new message arrives as a
|
|
63
|
-
\`<channel
|
|
65
|
+
\`<channel … event="messages" conversation_ids="…">\` event.
|
|
64
66
|
Read those conversations with \`get_messages\` and answer as below. There is no
|
|
65
67
|
deadline and nothing to re-arm; the channel reconnects by itself. \`event="reset"\`
|
|
66
68
|
means catch up with \`get_messages\`; \`event="stopped"\` says why the feed ended
|
package/dist/connect-claude.js
CHANGED
|
@@ -66,7 +66,12 @@ function registerChannel() {
|
|
|
66
66
|
const problem = (0, context_hooks_command_1.launchProblem)(launch);
|
|
67
67
|
if (problem)
|
|
68
68
|
return { skipped: problem };
|
|
69
|
-
|
|
69
|
+
const run = (0, bay_mcp_2.realClaudeRun)();
|
|
70
|
+
// The plugin first: it updates with the npm package and is the form Anthropic can approve.
|
|
71
|
+
// Where this Claude Code cannot install it, the same server is registered by hand.
|
|
72
|
+
if ((0, launch_1.installPluginWithClaude)(run))
|
|
73
|
+
return "registered";
|
|
74
|
+
return (0, launch_1.registerChannelWithClaude)(launch, run)
|
|
70
75
|
? "registered"
|
|
71
76
|
: { skipped: "Claude Code did not accept the channel server." };
|
|
72
77
|
}
|
package/dist/skill-bootstrap.js
CHANGED
|
@@ -5,7 +5,7 @@ exports.renderSkillBootstrap = renderSkillBootstrap;
|
|
|
5
5
|
function renderSkillBootstrap(runtime) {
|
|
6
6
|
const invocation = runtime === "codex" ? "$baychat" : "/baychat";
|
|
7
7
|
const tools = runtime === "claude"
|
|
8
|
-
? "allowed-tools:\n - Bash\n - Monitor\n - mcp__baychat\n - mcp__baychat_channel\n"
|
|
8
|
+
? "allowed-tools:\n - Bash\n - Monitor\n - mcp__baychat\n - mcp__baychat_channel\n - mcp__plugin_baychat_channel\n"
|
|
9
9
|
: "";
|
|
10
10
|
return `---
|
|
11
11
|
name: baychat
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "baychat",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.28.0",
|
|
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"
|
|
@@ -37,6 +37,7 @@
|
|
|
37
37
|
],
|
|
38
38
|
"files": [
|
|
39
39
|
"dist",
|
|
40
|
+
".claude-plugin",
|
|
40
41
|
"README.md",
|
|
41
42
|
"LICENSE"
|
|
42
43
|
],
|