@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.
Files changed (3) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +62 -39
  3. package/src/discord.js +133 -9
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.123",
3
+ "version": "0.16.125",
4
4
  "description": "Waterbrother: bring-your-own-model coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
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 bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3069
- bridge.activeHost = createTelegramBridgeHostRecord({ sessionId, cwd, label, surface, ownerId, ownerName, provider, model, runtimeProfile });
3070
- bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3071
- bridge.deliveredReplies = Array.isArray(bridge.deliveredReplies) ? bridge.deliveredReplies.slice(-50) : [];
3072
- bridge.pendingRequests = Array.isArray(bridge.pendingRequests) ? bridge.pendingRequests : [];
3073
- await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3074
- await syncSharedTelegramBridgeAgent({ cwd, host: bridge.activeHost, actor: actor || { id: ownerId, name: ownerName } });
3075
- return bridge.activeHost;
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
- const bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3080
- const activeHost = bridge.activeHost || {};
3081
- if (Number(activeHost.pid || 0) !== process.pid) {
3082
- bridge.activeHost = createTelegramBridgeHostRecord({ sessionId, cwd, label, surface, ownerId, ownerName, provider, model, runtimeProfile });
3083
- } else {
3084
- bridge.activeHost = {
3085
- ...activeHost,
3086
- sessionId: String(sessionId || "").trim(),
3087
- cwd: String(cwd || "").trim(),
3088
- label: String(label || activeHost.label || "").trim(),
3089
- surface: String(surface || activeHost.surface || "live-tui").trim() || "live-tui",
3090
- ownerId: String(ownerId || activeHost.ownerId || "").trim(),
3091
- ownerName: String(ownerName || activeHost.ownerName || "").trim(),
3092
- provider: String(provider || activeHost.provider || "").trim(),
3093
- model: String(model || activeHost.model || "").trim(),
3094
- runtimeProfile: String(runtimeProfile || activeHost.runtimeProfile || "").trim(),
3095
- updatedAt: new Date().toISOString()
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
- bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3099
- await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
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 bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3106
- if (Number(bridge.activeHost?.pid || 0) === process.pid) {
3107
- bridge.activeHost = { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" };
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 bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
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(TELEGRAM_BRIDGE_SERVICE, bridge);
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 bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
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(TELEGRAM_BRIDGE_SERVICE, bridge);
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 (content.includes("status") || content.includes("online")) {
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 (content.includes("hello") || content.includes("hi") || content.includes("hey")) {
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
- if (shouldReplyToMessage(msg, botUser?.id)) {
263
- const reply = buildReply(msg, botUser?.id);
375
+ const reply = shouldReplyToMessage(msg, botUser?.id)
376
+ ? buildReply(msg, botUser?.id)
377
+ : null;
378
+ try {
264
379
  if (reply) {
265
- try {
266
- await sendChannelMessage(discord, msg.channel_id, reply);
267
- log(`discord: replied in ${msg.channel_id}`);
268
- } catch (error) {
269
- log(`discord: reply failed: ${error instanceof Error ? error.message : String(error)}`);
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
  });