@tritard/waterbrother 0.16.123 → 0.16.125
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/package.json +1 -1
- package/src/cli.js +62 -39
- package/src/discord.js +133 -9
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -107,6 +107,7 @@ const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)),
|
|
|
107
107
|
const BIN_PATH = path.join(PACKAGE_ROOT, "bin", "waterbrother.js");
|
|
108
108
|
const DOCS_BASE_URL = String(process.env.WATERBROTHER_DOCS_BASE_URL || "https://waterbrother.app").trim().replace(/\/+$/, "");
|
|
109
109
|
const TELEGRAM_BRIDGE_SERVICE = "telegram";
|
|
110
|
+
const DISCORD_BRIDGE_SERVICE = "discord";
|
|
110
111
|
const TELEGRAM_BRIDGE_POLL_MS = 250;
|
|
111
112
|
|
|
112
113
|
|
|
@@ -3036,6 +3037,10 @@ function upsertTelegramBridgeHostEntry(hosts = [], nextHost = {}) {
|
|
|
3036
3037
|
return filtered.slice(0, 20);
|
|
3037
3038
|
}
|
|
3038
3039
|
|
|
3040
|
+
function bridgeServicesForLiveTui() {
|
|
3041
|
+
return [TELEGRAM_BRIDGE_SERVICE, DISCORD_BRIDGE_SERVICE];
|
|
3042
|
+
}
|
|
3043
|
+
|
|
3039
3044
|
async function syncSharedTelegramBridgeAgent({ cwd, host = {}, actor = {} }) {
|
|
3040
3045
|
try {
|
|
3041
3046
|
const project = await loadSharedProject(cwd);
|
|
@@ -3065,53 +3070,70 @@ async function syncSharedTelegramBridgeAgent({ cwd, host = {}, actor = {} }) {
|
|
|
3065
3070
|
}
|
|
3066
3071
|
|
|
3067
3072
|
async function registerTelegramBridgeHost({ sessionId, cwd, label = "", surface = "live-tui", ownerId = "", ownerName = "", provider = "", model = "", runtimeProfile = "", actor = null }) {
|
|
3068
|
-
const
|
|
3069
|
-
|
|
3070
|
-
|
|
3071
|
-
|
|
3072
|
-
|
|
3073
|
-
|
|
3074
|
-
|
|
3075
|
-
|
|
3073
|
+
const hostRecord = createTelegramBridgeHostRecord({ sessionId, cwd, label, surface, ownerId, ownerName, provider, model, runtimeProfile });
|
|
3074
|
+
for (const serviceId of bridgeServicesForLiveTui()) {
|
|
3075
|
+
const bridge = await loadGatewayBridge(serviceId);
|
|
3076
|
+
bridge.activeHost = hostRecord;
|
|
3077
|
+
bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
|
|
3078
|
+
bridge.deliveredReplies = Array.isArray(bridge.deliveredReplies) ? bridge.deliveredReplies.slice(-50) : [];
|
|
3079
|
+
bridge.pendingRequests = Array.isArray(bridge.pendingRequests) ? bridge.pendingRequests : [];
|
|
3080
|
+
await saveGatewayBridge(serviceId, bridge);
|
|
3081
|
+
}
|
|
3082
|
+
await syncSharedTelegramBridgeAgent({ cwd, host: hostRecord, actor: actor || { id: ownerId, name: ownerName } });
|
|
3083
|
+
return hostRecord;
|
|
3076
3084
|
}
|
|
3077
3085
|
|
|
3078
3086
|
async function touchTelegramBridgeHost({ sessionId, cwd, label = "", surface = "live-tui", ownerId = "", ownerName = "", provider = "", model = "", runtimeProfile = "", actor = null }) {
|
|
3079
|
-
|
|
3080
|
-
const
|
|
3081
|
-
|
|
3082
|
-
bridge.activeHost
|
|
3083
|
-
|
|
3084
|
-
|
|
3085
|
-
|
|
3086
|
-
|
|
3087
|
-
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3087
|
+
let latestHost = null;
|
|
3088
|
+
for (const serviceId of bridgeServicesForLiveTui()) {
|
|
3089
|
+
const bridge = await loadGatewayBridge(serviceId);
|
|
3090
|
+
const activeHost = bridge.activeHost || {};
|
|
3091
|
+
if (Number(activeHost.pid || 0) !== process.pid) {
|
|
3092
|
+
bridge.activeHost = createTelegramBridgeHostRecord({ sessionId, cwd, label, surface, ownerId, ownerName, provider, model, runtimeProfile });
|
|
3093
|
+
} else {
|
|
3094
|
+
bridge.activeHost = {
|
|
3095
|
+
...activeHost,
|
|
3096
|
+
sessionId: String(sessionId || "").trim(),
|
|
3097
|
+
cwd: String(cwd || "").trim(),
|
|
3098
|
+
label: String(label || activeHost.label || "").trim(),
|
|
3099
|
+
surface: String(surface || activeHost.surface || "live-tui").trim() || "live-tui",
|
|
3100
|
+
ownerId: String(ownerId || activeHost.ownerId || "").trim(),
|
|
3101
|
+
ownerName: String(ownerName || activeHost.ownerName || "").trim(),
|
|
3102
|
+
provider: String(provider || activeHost.provider || "").trim(),
|
|
3103
|
+
model: String(model || activeHost.model || "").trim(),
|
|
3104
|
+
runtimeProfile: String(runtimeProfile || activeHost.runtimeProfile || "").trim(),
|
|
3105
|
+
updatedAt: new Date().toISOString()
|
|
3106
|
+
};
|
|
3107
|
+
}
|
|
3108
|
+
bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
|
|
3109
|
+
await saveGatewayBridge(serviceId, bridge);
|
|
3110
|
+
latestHost = bridge.activeHost;
|
|
3097
3111
|
}
|
|
3098
|
-
|
|
3099
|
-
|
|
3100
|
-
await syncSharedTelegramBridgeAgent({ cwd, host: bridge.activeHost, actor: actor || { id: ownerId, name: ownerName } });
|
|
3101
|
-
return bridge.activeHost;
|
|
3112
|
+
await syncSharedTelegramBridgeAgent({ cwd, host: latestHost || {}, actor: actor || { id: ownerId, name: ownerName } });
|
|
3113
|
+
return latestHost;
|
|
3102
3114
|
}
|
|
3103
3115
|
|
|
3104
3116
|
async function clearTelegramBridgeHost() {
|
|
3105
|
-
const
|
|
3106
|
-
|
|
3107
|
-
bridge.activeHost
|
|
3117
|
+
for (const serviceId of bridgeServicesForLiveTui()) {
|
|
3118
|
+
const bridge = await loadGatewayBridge(serviceId);
|
|
3119
|
+
if (Number(bridge.activeHost?.pid || 0) === process.pid) {
|
|
3120
|
+
bridge.activeHost = { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" };
|
|
3121
|
+
}
|
|
3122
|
+
bridge.hosts = Array.isArray(bridge.hosts) ? bridge.hosts.filter((host) => Number(host?.pid || 0) !== process.pid) : [];
|
|
3123
|
+
await saveGatewayBridge(serviceId, bridge);
|
|
3108
3124
|
}
|
|
3109
|
-
bridge.hosts = Array.isArray(bridge.hosts) ? bridge.hosts.filter((host) => Number(host?.pid || 0) !== process.pid) : [];
|
|
3110
|
-
await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
|
|
3111
3125
|
}
|
|
3112
3126
|
|
|
3113
3127
|
async function dequeueTelegramBridgeRequest({ cwd }) {
|
|
3114
|
-
const
|
|
3128
|
+
for (const serviceId of bridgeServicesForLiveTui()) {
|
|
3129
|
+
const next = await dequeueBridgeRequestForService(serviceId, { cwd });
|
|
3130
|
+
if (next) return next;
|
|
3131
|
+
}
|
|
3132
|
+
return null;
|
|
3133
|
+
}
|
|
3134
|
+
|
|
3135
|
+
async function dequeueBridgeRequestForService(serviceId, { cwd }) {
|
|
3136
|
+
const bridge = await loadGatewayBridge(serviceId);
|
|
3115
3137
|
const activeHost = bridge.activeHost || {};
|
|
3116
3138
|
const currentHost = Number(activeHost.pid || 0) === process.pid
|
|
3117
3139
|
? activeHost
|
|
@@ -3156,12 +3178,13 @@ async function dequeueTelegramBridgeRequest({ cwd }) {
|
|
|
3156
3178
|
};
|
|
3157
3179
|
bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
|
|
3158
3180
|
}
|
|
3159
|
-
await saveGatewayBridge(
|
|
3181
|
+
await saveGatewayBridge(serviceId, bridge);
|
|
3160
3182
|
return next;
|
|
3161
3183
|
}
|
|
3162
3184
|
|
|
3163
3185
|
async function deliverTelegramBridgeReply(request, { sessionId, content = "", error = "", meta = {} }) {
|
|
3164
|
-
const
|
|
3186
|
+
const serviceId = String(request?.source || TELEGRAM_BRIDGE_SERVICE).trim().toLowerCase() || TELEGRAM_BRIDGE_SERVICE;
|
|
3187
|
+
const bridge = await loadGatewayBridge(serviceId);
|
|
3165
3188
|
const replies = Array.isArray(bridge.deliveredReplies) ? bridge.deliveredReplies : [];
|
|
3166
3189
|
replies.push({
|
|
3167
3190
|
requestId: String(request?.id || "").trim(),
|
|
@@ -3184,7 +3207,7 @@ async function deliverTelegramBridgeReply(request, { sessionId, content = "", er
|
|
|
3184
3207
|
if (Number(bridge.activeHost?.pid || 0) === process.pid) {
|
|
3185
3208
|
bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
|
|
3186
3209
|
}
|
|
3187
|
-
await saveGatewayBridge(
|
|
3210
|
+
await saveGatewayBridge(serviceId, bridge);
|
|
3188
3211
|
}
|
|
3189
3212
|
|
|
3190
3213
|
function formatTelegramRemoteActor(request = {}) {
|
package/src/discord.js
CHANGED
|
@@ -1,9 +1,12 @@
|
|
|
1
1
|
import fs from "node:fs/promises";
|
|
2
2
|
import os from "node:os";
|
|
3
3
|
import path from "node:path";
|
|
4
|
+
import { loadGatewayBridge, saveGatewayBridge } from "./gateway-state.js";
|
|
4
5
|
|
|
5
6
|
const DISCORD_API_BASE = "https://discord.com/api/v10";
|
|
6
7
|
const STATUS_PATH = path.join(os.homedir(), ".waterbrother", "discord-status.json");
|
|
8
|
+
const DISCORD_BRIDGE_TIMEOUT_MS = 5 * 60 * 1000;
|
|
9
|
+
const DISCORD_BRIDGE_POLL_MS = 250;
|
|
7
10
|
|
|
8
11
|
const INTENT_BITS = {
|
|
9
12
|
GUILDS: 1 << 0,
|
|
@@ -113,18 +116,128 @@ function shouldReplyToMessage(message, botUserId) {
|
|
|
113
116
|
|
|
114
117
|
function buildReply(message, botUserId) {
|
|
115
118
|
const content = extractMentionContent(message.content, botUserId).toLowerCase();
|
|
119
|
+
const normalized = content.replace(/\s+/g, " ").trim();
|
|
116
120
|
if (!content || content === "ping") {
|
|
117
121
|
return "pong";
|
|
118
122
|
}
|
|
119
|
-
if (
|
|
123
|
+
if (normalized === "status" || normalized === "online" || normalized === "are you online") {
|
|
120
124
|
return "Discord gateway is online. I can see messages and basic mentions. Full conversational Discord runtime is the next slice.";
|
|
121
125
|
}
|
|
122
|
-
if (
|
|
126
|
+
if (["hello", "hi", "hey"].includes(normalized)) {
|
|
123
127
|
return "Waterbrother is here. Discord setup is live at the gateway level; deeper room workflows come next.";
|
|
124
128
|
}
|
|
125
129
|
return null;
|
|
126
130
|
}
|
|
127
131
|
|
|
132
|
+
function createBridgeRequestId() {
|
|
133
|
+
return `dc_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function describeDiscordUser(message = {}) {
|
|
137
|
+
const author = message?.author || {};
|
|
138
|
+
const username = String(author.username || "").trim();
|
|
139
|
+
const globalName = String(author.global_name || "").trim();
|
|
140
|
+
const displayName = globalName || username || String(author.id || "").trim() || "discord";
|
|
141
|
+
return {
|
|
142
|
+
userId: String(author.id || "").trim(),
|
|
143
|
+
username,
|
|
144
|
+
displayName
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
async function getLiveBridgeHost({ cwd = "" } = {}) {
|
|
149
|
+
const bridge = await loadGatewayBridge("discord");
|
|
150
|
+
const hosts = Array.isArray(bridge.hosts) ? bridge.hosts : [];
|
|
151
|
+
const nextHosts = [];
|
|
152
|
+
let changed = false;
|
|
153
|
+
for (const host of hosts) {
|
|
154
|
+
const pid = Number(host?.pid || 0);
|
|
155
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
156
|
+
changed = true;
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
try {
|
|
160
|
+
process.kill(pid, 0);
|
|
161
|
+
} catch {
|
|
162
|
+
changed = true;
|
|
163
|
+
continue;
|
|
164
|
+
}
|
|
165
|
+
if (cwd && host.cwd && String(host.cwd) !== String(cwd || "")) {
|
|
166
|
+
continue;
|
|
167
|
+
}
|
|
168
|
+
nextHosts.push(host);
|
|
169
|
+
}
|
|
170
|
+
if (changed || nextHosts.length !== hosts.length) {
|
|
171
|
+
bridge.hosts = nextHosts;
|
|
172
|
+
const activePid = Number(bridge.activeHost?.pid || 0);
|
|
173
|
+
if (activePid > 0 && !nextHosts.some((host) => Number(host?.pid || 0) === activePid)) {
|
|
174
|
+
bridge.activeHost = { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" };
|
|
175
|
+
}
|
|
176
|
+
await saveGatewayBridge("discord", bridge);
|
|
177
|
+
}
|
|
178
|
+
return bridge.activeHost?.pid ? bridge.activeHost : (nextHosts[0] || null);
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async function runPromptViaBridge(runtime, message, promptText) {
|
|
182
|
+
const host = await getLiveBridgeHost();
|
|
183
|
+
if (!host) {
|
|
184
|
+
return { error: "No live Waterbrother TUI is connected. Start Waterbrother in the terminal first, then retry." };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
const requestId = createBridgeRequestId();
|
|
188
|
+
const actor = describeDiscordUser(message);
|
|
189
|
+
const bridge = await loadGatewayBridge("discord");
|
|
190
|
+
const requests = Array.isArray(bridge.pendingRequests) ? bridge.pendingRequests : [];
|
|
191
|
+
requests.push({
|
|
192
|
+
id: requestId,
|
|
193
|
+
chatId: String(message.channel_id || "").trim(),
|
|
194
|
+
userId: actor.userId,
|
|
195
|
+
username: actor.username,
|
|
196
|
+
usernameHandle: actor.username ? `@${actor.username}` : "",
|
|
197
|
+
displayName: actor.displayName,
|
|
198
|
+
sessionId: "",
|
|
199
|
+
text: String(promptText || "").trim(),
|
|
200
|
+
requestKind: "prompt",
|
|
201
|
+
explicitExecution: true,
|
|
202
|
+
targetPid: Number(host?.pid || 0),
|
|
203
|
+
targetSessionId: String(host?.sessionId || "").trim(),
|
|
204
|
+
targetOwnerId: String(host?.ownerId || "").trim(),
|
|
205
|
+
runtimeProfile: "",
|
|
206
|
+
replyToMessageId: 0,
|
|
207
|
+
requestedAt: new Date().toISOString(),
|
|
208
|
+
status: "pending",
|
|
209
|
+
source: "discord"
|
|
210
|
+
});
|
|
211
|
+
bridge.pendingRequests = requests.slice(-100);
|
|
212
|
+
bridge.deliveredReplies = Array.isArray(bridge.deliveredReplies) ? bridge.deliveredReplies : [];
|
|
213
|
+
await saveGatewayBridge("discord", bridge);
|
|
214
|
+
|
|
215
|
+
const deadline = Date.now() + DISCORD_BRIDGE_TIMEOUT_MS;
|
|
216
|
+
while (Date.now() < deadline) {
|
|
217
|
+
await new Promise((resolve) => setTimeout(resolve, DISCORD_BRIDGE_POLL_MS));
|
|
218
|
+
const nextBridge = await loadGatewayBridge("discord");
|
|
219
|
+
const replyIndex = Array.isArray(nextBridge.deliveredReplies)
|
|
220
|
+
? nextBridge.deliveredReplies.findIndex((item) => item?.requestId === requestId)
|
|
221
|
+
: -1;
|
|
222
|
+
if (replyIndex >= 0) {
|
|
223
|
+
const reply = nextBridge.deliveredReplies[replyIndex];
|
|
224
|
+
nextBridge.deliveredReplies.splice(replyIndex, 1);
|
|
225
|
+
await saveGatewayBridge("discord", nextBridge);
|
|
226
|
+
if (reply.error) {
|
|
227
|
+
return { error: String(reply.error || "").trim() || "Discord bridge execution failed." };
|
|
228
|
+
}
|
|
229
|
+
return { content: String(reply.content || "").trim() || "(no content)" };
|
|
230
|
+
}
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
const finalBridge = await loadGatewayBridge("discord");
|
|
234
|
+
finalBridge.pendingRequests = Array.isArray(finalBridge.pendingRequests)
|
|
235
|
+
? finalBridge.pendingRequests.filter((item) => item?.id !== requestId)
|
|
236
|
+
: [];
|
|
237
|
+
await saveGatewayBridge("discord", finalBridge);
|
|
238
|
+
return { error: "Discord bridge timed out waiting for the live terminal." };
|
|
239
|
+
}
|
|
240
|
+
|
|
128
241
|
export async function getDiscordStatus(runtime = {}) {
|
|
129
242
|
const discord = normalizeDiscordRuntime(runtime);
|
|
130
243
|
const saved = await loadDiscordStatus();
|
|
@@ -259,16 +372,27 @@ export async function runDiscordGateway(runtime = {}, { log = console.log, signa
|
|
|
259
372
|
if (!msg || msg.author?.bot) return;
|
|
260
373
|
const scope = msg.guild_id ? `guild:${msg.guild_id}` : "dm";
|
|
261
374
|
log(`discord: ${scope} #${msg.channel_id} ${msg.author?.username || "unknown"} -> ${String(msg.content || "").trim()}`);
|
|
262
|
-
|
|
263
|
-
|
|
375
|
+
const reply = shouldReplyToMessage(msg, botUser?.id)
|
|
376
|
+
? buildReply(msg, botUser?.id)
|
|
377
|
+
: null;
|
|
378
|
+
try {
|
|
264
379
|
if (reply) {
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
380
|
+
await sendChannelMessage(discord, msg.channel_id, reply);
|
|
381
|
+
log(`discord: replied in ${msg.channel_id}`);
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
384
|
+
if (!msg.guild_id) {
|
|
385
|
+
const bridged = await runPromptViaBridge(runtime, msg, String(msg.content || "").trim());
|
|
386
|
+
if (bridged?.content) {
|
|
387
|
+
await sendChannelMessage(discord, msg.channel_id, bridged.content);
|
|
388
|
+
log(`discord: bridged reply in ${msg.channel_id}`);
|
|
389
|
+
} else if (bridged?.error) {
|
|
390
|
+
await sendChannelMessage(discord, msg.channel_id, bridged.error);
|
|
391
|
+
log(`discord: bridge error in ${msg.channel_id}`);
|
|
270
392
|
}
|
|
271
393
|
}
|
|
394
|
+
} catch (error) {
|
|
395
|
+
log(`discord: reply failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
272
396
|
}
|
|
273
397
|
}
|
|
274
398
|
});
|