@tritard/waterbrother 0.16.125 → 0.16.127

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.125",
3
+ "version": "0.16.127",
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/channels.js CHANGED
@@ -229,7 +229,7 @@ export function buildChannelOnboardingPayload(serviceId) {
229
229
  "Enable Message Content Intent before testing DMs or mentions"
230
230
  ],
231
231
  commands: [
232
- "waterbrother config set-json channels '{\"discord\":{\"enabled\":true,\"botToken\":\"{env:DISCORD_BOT_TOKEN}\",\"applicationId\":\"{env:DISCORD_APPLICATION_ID}\",\"pairingMode\":\"manual\",\"allowedUserIds\":[]}}'",
232
+ "waterbrother config set-json channels '{\"discord\":{\"enabled\":true,\"botToken\":\"YOUR_BOT_TOKEN\",\"applicationId\":\"YOUR_APP_ID\",\"pairingMode\":\"manual\",\"allowedUserIds\":[]}}'",
233
233
  "waterbrother channels status",
234
234
  "waterbrother discord status",
235
235
  "waterbrother discord run"
@@ -237,7 +237,8 @@ export function buildChannelOnboardingPayload(serviceId) {
237
237
  notes: [
238
238
  "Start with direct messages before any guild or channel workflow.",
239
239
  "This first runtime slice handles DMs and mentions, not full Roundtable rooms yet.",
240
- "Prefer explicit user allowlists."
240
+ "Prefer explicit user allowlists.",
241
+ "The interactive TUI onboarding wizard can store the Discord token and application id directly, just like Telegram."
241
242
  ]
242
243
  };
243
244
  }
package/src/cli.js CHANGED
@@ -2925,7 +2925,7 @@ function isProcessAlive(pid) {
2925
2925
 
2926
2926
  async function maybeAutostartGateway(runtime, { cwd, io = console } = {}) {
2927
2927
  if (process.env.WATERBROTHER_GATEWAY_CHILD === "1") {
2928
- return { attempted: false, started: false, reason: "gateway-child" };
2928
+ return { attempted: false, started: false, reason: "gateway-child", handles: [] };
2929
2929
  }
2930
2930
 
2931
2931
  const gateway = runtime?.gateway || {};
@@ -2933,38 +2933,54 @@ async function maybeAutostartGateway(runtime, { cwd, io = console } = {}) {
2933
2933
  ? gateway.startupChannels.map((value) => String(value || "").trim().toLowerCase()).filter(Boolean)
2934
2934
  : [];
2935
2935
 
2936
- if (!gateway.enabled || !startupChannels.includes("telegram")) {
2937
- return { attempted: false, started: false, reason: "not-configured", child: null };
2936
+ if (!gateway.enabled || startupChannels.length === 0) {
2937
+ return { attempted: false, started: false, reason: "not-configured", handles: [] };
2938
2938
  }
2939
2939
 
2940
- const telegram = runtime?.channels?.telegram || {};
2941
- if (!telegram.enabled || !String(telegram.botToken || "").trim()) {
2942
- return { attempted: false, started: false, reason: "telegram-not-ready", child: null };
2943
- }
2940
+ const handles = [];
2941
+ let attempted = false;
2942
+ let started = false;
2944
2943
 
2945
- const processInfo = await loadGatewayProcessInfo("telegram");
2946
- if (isProcessAlive(processInfo.pid)) {
2947
- return { attempted: true, started: false, reason: "already-running", pid: processInfo.pid, child: null };
2948
- }
2944
+ for (const serviceId of startupChannels) {
2945
+ if (serviceId === "telegram") {
2946
+ const telegram = runtime?.channels?.telegram || {};
2947
+ if (!telegram.enabled || !String(telegram.botToken || "").trim()) continue;
2948
+ } else if (serviceId === "discord") {
2949
+ const discord = runtime?.channels?.discord || {};
2950
+ if (!discord.enabled || !String(discord.botToken || "").trim() || !String(discord.applicationId || "").trim()) continue;
2951
+ } else {
2952
+ continue;
2953
+ }
2949
2954
 
2950
- const child = spawn(process.execPath, [BIN_PATH, "gateway", "run", "telegram", "--skip-onboarding"], {
2951
- cwd,
2952
- stdio: "ignore",
2953
- env: {
2954
- ...process.env,
2955
- WATERBROTHER_GATEWAY_CHILD: "1"
2955
+ attempted = true;
2956
+ const processInfo = await loadGatewayProcessInfo(serviceId);
2957
+ if (isProcessAlive(processInfo.pid)) {
2958
+ handles.push({ service: serviceId, attempted: true, started: false, reason: "already-running", pid: processInfo.pid, child: null });
2959
+ continue;
2956
2960
  }
2957
- });
2958
- child.unref();
2959
2961
 
2960
- await saveGatewayProcessInfo("telegram", {
2961
- pid: child.pid,
2962
- startedAt: new Date().toISOString(),
2963
- command: "gateway run telegram --skip-onboarding"
2964
- });
2962
+ const child = spawn(process.execPath, [BIN_PATH, serviceId === "telegram" ? "gateway" : "discord", "run", ...(serviceId === "telegram" ? [serviceId] : []), "--skip-onboarding"].filter(Boolean), {
2963
+ cwd,
2964
+ stdio: "ignore",
2965
+ env: {
2966
+ ...process.env,
2967
+ WATERBROTHER_GATEWAY_CHILD: "1"
2968
+ }
2969
+ });
2970
+ child.unref();
2971
+
2972
+ await saveGatewayProcessInfo(serviceId, {
2973
+ pid: child.pid,
2974
+ startedAt: new Date().toISOString(),
2975
+ command: serviceId === "telegram" ? "gateway run telegram --skip-onboarding" : "discord run --skip-onboarding"
2976
+ });
2977
+
2978
+ io.log?.(dim(`${serviceId} gateway autostarted in background (pid ${child.pid})`));
2979
+ handles.push({ service: serviceId, attempted: true, started: true, pid: child.pid, child });
2980
+ started = true;
2981
+ }
2965
2982
 
2966
- io.log?.(dim(`telegram gateway autostarted in background (pid ${child.pid})`));
2967
- return { attempted: true, started: true, pid: child.pid, child };
2983
+ return { attempted, started, reason: handles.length ? "" : "not-ready", handles };
2968
2984
  }
2969
2985
 
2970
2986
  async function stopManagedGateway(service, gatewayHandle, { io = console } = {}) {
@@ -2997,8 +3013,8 @@ async function stopManagedGateway(service, gatewayHandle, { io = console } = {})
2997
3013
 
2998
3014
  async function stopTrackedGateway(service = "telegram", { io = console } = {}) {
2999
3015
  const normalized = String(service || "telegram").trim().toLowerCase();
3000
- if (normalized !== "telegram") {
3001
- throw new Error("Usage: waterbrother gateway stop [telegram]");
3016
+ if (!["telegram", "discord"].includes(normalized)) {
3017
+ throw new Error("Usage: waterbrother gateway stop [telegram|discord]");
3002
3018
  }
3003
3019
  const processInfo = await loadGatewayProcessInfo(normalized);
3004
3020
  const pid = Number(processInfo?.pid || 0);
@@ -3694,6 +3710,87 @@ async function runOnboardingWizard(config, { cwd }) {
3694
3710
  console.log(dim("Skipping Telegram setup for now. You can always run `waterbrother onboarding telegram` later."));
3695
3711
  }
3696
3712
  }
3713
+
3714
+ const configureDiscord = await promptYesNo("Enable Discord DM control now?", {
3715
+ input: process.stdin,
3716
+ output: process.stdout
3717
+ });
3718
+ if (configureDiscord) {
3719
+ const alreadyHaveDiscordSetup = await promptYesNo("Do you already have a Discord bot token and application id?", {
3720
+ input: process.stdin,
3721
+ output: process.stdout
3722
+ });
3723
+ if (!alreadyHaveDiscordSetup) {
3724
+ console.log("Create a Discord application and bot, enable Message Content Intent, then come back here.");
3725
+ console.log(dim(`Guide: ${getDocsUrl("/onboarding/discord")}`));
3726
+ const openGuide = await promptYesNo("Open the Discord setup guide in your browser?", {
3727
+ input: process.stdin,
3728
+ output: process.stdout
3729
+ });
3730
+ if (openGuide) {
3731
+ const opened = await maybeOpenUrl(getDocsUrl("/onboarding/discord"));
3732
+ if (!opened) {
3733
+ console.log(dim(`Could not open browser automatically. Visit ${getDocsUrl("/onboarding/discord")}`));
3734
+ }
3735
+ }
3736
+ }
3737
+
3738
+ const existingDiscord = next.channels?.discord && typeof next.channels.discord === "object" ? next.channels.discord : {};
3739
+ let discordToken = String(existingDiscord.botToken || existingDiscord.token || "").trim();
3740
+ let discordApplicationId = String(existingDiscord.applicationId || "").trim();
3741
+
3742
+ if (discordToken) {
3743
+ const keepExistingDiscordToken = await promptYesNo(`Use saved Discord bot token (${formatApiKeyHint(discordToken)})?`, {
3744
+ input: process.stdin,
3745
+ output: process.stdout
3746
+ });
3747
+ if (!keepExistingDiscordToken) {
3748
+ discordToken = "";
3749
+ }
3750
+ }
3751
+
3752
+ while (!discordToken) {
3753
+ const enteredDiscordToken = (await promptLine("Paste Discord bot token (Enter to skip for now): ")).trim();
3754
+ if (!enteredDiscordToken) break;
3755
+ discordToken = enteredDiscordToken;
3756
+ }
3757
+
3758
+ if (discordApplicationId) {
3759
+ const keepExistingDiscordAppId = await promptYesNo(`Use saved Discord application id (${discordApplicationId})?`, {
3760
+ input: process.stdin,
3761
+ output: process.stdout
3762
+ });
3763
+ if (!keepExistingDiscordAppId) {
3764
+ discordApplicationId = "";
3765
+ }
3766
+ }
3767
+
3768
+ while (!discordApplicationId && discordToken) {
3769
+ const enteredDiscordApplicationId = (await promptLine("Paste Discord application id (Enter to skip for now): ")).trim();
3770
+ if (!enteredDiscordApplicationId) break;
3771
+ discordApplicationId = enteredDiscordApplicationId;
3772
+ }
3773
+
3774
+ if (discordToken && discordApplicationId) {
3775
+ const channels = next.channels && typeof next.channels === "object" ? { ...next.channels } : {};
3776
+ const discord = channels.discord && typeof channels.discord === "object" ? { ...channels.discord } : {};
3777
+ channels.discord = {
3778
+ ...discord,
3779
+ enabled: true,
3780
+ botToken: discordToken,
3781
+ applicationId: discordApplicationId,
3782
+ pairingMode: discord.pairingMode || "manual",
3783
+ allowedUserIds: Array.isArray(discord.allowedUserIds) ? discord.allowedUserIds : []
3784
+ };
3785
+ next.channels = channels;
3786
+
3787
+ console.log(green("Discord configured for DM-first control."));
3788
+ console.log(dim("Run `waterbrother discord run` in a second terminal when you want the Discord gateway live."));
3789
+ } else {
3790
+ console.log(dim("Skipping Discord setup for now. You can always run `waterbrother onboarding discord` later."));
3791
+ }
3792
+ }
3793
+
3697
3794
  if (!next.approvalMode) next.approvalMode = 'on-request';
3698
3795
  if (!next.designModel) next.designModel = getDefaultDesignModelForProvider(next.provider);
3699
3796
  if (next.traceMode === undefined) next.traceMode = 'on';
@@ -7834,18 +7931,22 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7834
7931
  if (gatewayCleanupDone) return;
7835
7932
  gatewayCleanupDone = true;
7836
7933
  await clearTelegramBridgeHost();
7837
- if (gatewayHandle?.started && gatewayHandle?.child) {
7838
- await stopManagedGateway("telegram", gatewayHandle, { io: console });
7934
+ for (const handle of Array.isArray(gatewayHandle?.handles) ? gatewayHandle.handles : []) {
7935
+ if (handle?.started && handle?.child) {
7936
+ await stopManagedGateway(handle.service, handle, { io: console });
7937
+ }
7839
7938
  }
7840
7939
  };
7841
7940
  const cleanupManagedGatewaySync = () => {
7842
7941
  if (gatewayCleanupDone) return;
7843
7942
  gatewayCleanupDone = true;
7844
- const pid = Number(gatewayHandle?.pid || 0);
7845
- if (Number.isFinite(pid) && pid > 0 && isProcessAlive(pid)) {
7846
- try {
7847
- process.kill(pid, "SIGTERM");
7848
- } catch {}
7943
+ for (const handle of Array.isArray(gatewayHandle?.handles) ? gatewayHandle.handles : []) {
7944
+ const pid = Number(handle?.pid || 0);
7945
+ if (Number.isFinite(pid) && pid > 0 && isProcessAlive(pid)) {
7946
+ try {
7947
+ process.kill(pid, "SIGTERM");
7948
+ } catch {}
7949
+ }
7849
7950
  }
7850
7951
  };
7851
7952
  const handleExitSignal = () => {
package/src/discord.js CHANGED
@@ -381,8 +381,8 @@ export async function runDiscordGateway(runtime = {}, { log = console.log, signa
381
381
  log(`discord: replied in ${msg.channel_id}`);
382
382
  return;
383
383
  }
384
- if (!msg.guild_id) {
385
- const bridged = await runPromptViaBridge(runtime, msg, String(msg.content || "").trim());
384
+ if (shouldReplyToMessage(msg, botUser?.id)) {
385
+ const bridged = await runPromptViaBridge(runtime, msg, extractMentionContent(String(msg.content || "").trim(), botUser?.id));
386
386
  if (bridged?.content) {
387
387
  await sendChannelMessage(discord, msg.channel_id, bridged.content);
388
388
  log(`discord: bridged reply in ${msg.channel_id}`);