@tritard/waterbrother 0.16.13 → 0.16.15

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 (2) hide show
  1. package/package.json +1 -1
  2. package/src/cli.js +105 -7
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.13",
3
+ "version": "0.16.15",
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
@@ -131,6 +131,7 @@ const INTERACTIVE_COMMANDS = [
131
131
  { name: "/gateway pairings", description: "List pending Telegram pairing requests" },
132
132
  { name: "/gateway pair <user-id>", description: "Approve a pending Telegram user id" },
133
133
  { name: "/gateway unpair <user-id>", description: "Remove a paired Telegram user id" },
134
+ { name: "/gateway stop", description: "Stop the tracked Telegram gateway process" },
134
135
  { name: "/channels", description: "Show messaging channel readiness" },
135
136
  { name: "/agent", description: "Show active agent profile" },
136
137
  { name: "/agent <profile>", description: "Switch profile: coder|designer|reviewer|planner" },
@@ -242,6 +243,7 @@ Usage:
242
243
  waterbrother channels show <service>
243
244
  waterbrother gateway status
244
245
  waterbrother gateway run telegram
246
+ waterbrother gateway stop [telegram]
245
247
  waterbrother gateway pairings [telegram]
246
248
  waterbrother gateway pair [telegram] <user-id>
247
249
  waterbrother gateway unpair [telegram] <user-id>
@@ -2731,22 +2733,21 @@ async function maybeAutostartGateway(runtime, { cwd, io = console } = {}) {
2731
2733
  : [];
2732
2734
 
2733
2735
  if (!gateway.enabled || !startupChannels.includes("telegram")) {
2734
- return { attempted: false, started: false, reason: "not-configured" };
2736
+ return { attempted: false, started: false, reason: "not-configured", child: null };
2735
2737
  }
2736
2738
 
2737
2739
  const telegram = runtime?.channels?.telegram || {};
2738
2740
  if (!telegram.enabled || !String(telegram.botToken || "").trim()) {
2739
- return { attempted: false, started: false, reason: "telegram-not-ready" };
2741
+ return { attempted: false, started: false, reason: "telegram-not-ready", child: null };
2740
2742
  }
2741
2743
 
2742
2744
  const processInfo = await loadGatewayProcessInfo("telegram");
2743
2745
  if (isProcessAlive(processInfo.pid)) {
2744
- return { attempted: true, started: false, reason: "already-running", pid: processInfo.pid };
2746
+ return { attempted: true, started: false, reason: "already-running", pid: processInfo.pid, child: null };
2745
2747
  }
2746
2748
 
2747
2749
  const child = spawn(process.execPath, [BIN_PATH, "gateway", "run", "telegram", "--skip-onboarding"], {
2748
2750
  cwd,
2749
- detached: true,
2750
2751
  stdio: "ignore",
2751
2752
  env: {
2752
2753
  ...process.env,
@@ -2762,7 +2763,50 @@ async function maybeAutostartGateway(runtime, { cwd, io = console } = {}) {
2762
2763
  });
2763
2764
 
2764
2765
  io.log?.(dim(`telegram gateway autostarted in background (pid ${child.pid})`));
2765
- return { attempted: true, started: true, pid: child.pid };
2766
+ return { attempted: true, started: true, pid: child.pid, child };
2767
+ }
2768
+
2769
+ async function stopManagedGateway(service, gatewayHandle, { io = console } = {}) {
2770
+ const pid = Number(gatewayHandle?.pid || 0);
2771
+ if (!Number.isFinite(pid) || pid <= 0 || !isProcessAlive(pid)) {
2772
+ return false;
2773
+ }
2774
+ try {
2775
+ process.kill(pid, "SIGTERM");
2776
+ } catch {
2777
+ return false;
2778
+ }
2779
+
2780
+ const deadline = Date.now() + 2500;
2781
+ while (Date.now() < deadline) {
2782
+ if (!isProcessAlive(pid)) {
2783
+ await saveGatewayProcessInfo(service, { pid: 0, startedAt: "", command: "" });
2784
+ return true;
2785
+ }
2786
+ await new Promise((resolve) => setTimeout(resolve, 100));
2787
+ }
2788
+
2789
+ try {
2790
+ process.kill(pid, "SIGKILL");
2791
+ } catch {}
2792
+ await saveGatewayProcessInfo(service, { pid: 0, startedAt: "", command: "" });
2793
+ io.log?.(dim(`${service} gateway forced down on TUI exit`));
2794
+ return true;
2795
+ }
2796
+
2797
+ async function stopTrackedGateway(service = "telegram", { io = console } = {}) {
2798
+ const normalized = String(service || "telegram").trim().toLowerCase();
2799
+ if (normalized !== "telegram") {
2800
+ throw new Error("Usage: waterbrother gateway stop [telegram]");
2801
+ }
2802
+ const processInfo = await loadGatewayProcessInfo(normalized);
2803
+ const pid = Number(processInfo?.pid || 0);
2804
+ if (!Number.isFinite(pid) || pid <= 0 || !isProcessAlive(pid)) {
2805
+ await saveGatewayProcessInfo(normalized, { pid: 0, startedAt: "", command: "" });
2806
+ return { stopped: false, pid: 0 };
2807
+ }
2808
+ const stopped = await stopManagedGateway(normalized, { pid }, { io });
2809
+ return { stopped, pid };
2766
2810
  }
2767
2811
 
2768
2812
  async function chooseFromInteractiveMenu({ title, options, defaultIndex = 0 }) {
@@ -3533,7 +3577,20 @@ async function runGatewayCommand(positional, runtime, { cwd = process.cwd(), asJ
3533
3577
  return;
3534
3578
  }
3535
3579
 
3536
- throw new Error("Usage: waterbrother gateway status|run <telegram>|pairings [telegram]|pair [telegram] <user-id>|unpair [telegram] <user-id>");
3580
+ if (sub === "stop") {
3581
+ const service = String(positional[2] || "telegram").trim().toLowerCase();
3582
+ const result = await stopTrackedGateway(service, { io: console });
3583
+ if (asJson) {
3584
+ printData({ ok: true, service, stopped: result.stopped, pid: result.pid }, true);
3585
+ } else if (result.stopped) {
3586
+ console.log(`Stopped ${service} gateway process ${result.pid}`);
3587
+ } else {
3588
+ console.log(`No running ${service} gateway process found`);
3589
+ }
3590
+ return;
3591
+ }
3592
+
3593
+ throw new Error("Usage: waterbrother gateway status|run <telegram>|stop [telegram]|pairings [telegram]|pair [telegram] <user-id>|unpair [telegram] <user-id>");
3537
3594
  }
3538
3595
 
3539
3596
  async function runMcpCommand(positional, runtime, { cwd, asJson = false } = {}) {
@@ -6534,7 +6591,30 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
6534
6591
  }
6535
6592
  }
6536
6593
 
6537
- await maybeAutostartGateway(context.runtime, { cwd: context.cwd });
6594
+ const gatewayHandle = await maybeAutostartGateway(context.runtime, { cwd: context.cwd });
6595
+ let gatewayCleanupDone = false;
6596
+ const cleanupManagedGateway = async () => {
6597
+ if (gatewayCleanupDone) return;
6598
+ gatewayCleanupDone = true;
6599
+ if (gatewayHandle?.started && gatewayHandle?.child) {
6600
+ await stopManagedGateway("telegram", gatewayHandle, { io: console });
6601
+ }
6602
+ };
6603
+ const cleanupManagedGatewaySync = () => {
6604
+ if (gatewayCleanupDone) return;
6605
+ gatewayCleanupDone = true;
6606
+ const pid = Number(gatewayHandle?.pid || 0);
6607
+ if (Number.isFinite(pid) && pid > 0 && isProcessAlive(pid)) {
6608
+ try {
6609
+ process.kill(pid, "SIGTERM");
6610
+ } catch {}
6611
+ }
6612
+ };
6613
+ const handleExitSignal = () => {
6614
+ cleanupManagedGatewaySync();
6615
+ };
6616
+ process.once("SIGINT", handleExitSignal);
6617
+ process.once("SIGTERM", handleExitSignal);
6538
6618
  printInteractiveHeader({ runtime: context.runtime, agent, cwd: context.cwd });
6539
6619
  console.log("Interactive mode. Type /exit to quit, /help for commands.");
6540
6620
  if (process.stdout.isTTY) {
@@ -7148,6 +7228,20 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7148
7228
  continue;
7149
7229
  }
7150
7230
 
7231
+ if (line === "/gateway stop") {
7232
+ try {
7233
+ const result = await stopTrackedGateway("telegram", { io: console });
7234
+ if (result.stopped) {
7235
+ console.log(`Stopped telegram gateway process ${result.pid}`);
7236
+ } else {
7237
+ console.log("No running telegram gateway process found");
7238
+ }
7239
+ } catch (error) {
7240
+ console.log(`gateway stop failed: ${error instanceof Error ? error.message : String(error)}`);
7241
+ }
7242
+ continue;
7243
+ }
7244
+
7151
7245
  if (line === "/channels") {
7152
7246
  for (const status of getChannelStatuses(context.runtime)) {
7153
7247
  console.log(formatChannelStatusLine(status));
@@ -8808,6 +8902,10 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
8808
8902
  }
8809
8903
  }
8810
8904
 
8905
+ await cleanupManagedGateway();
8906
+ process.off("SIGINT", handleExitSignal);
8907
+ process.off("SIGTERM", handleExitSignal);
8908
+
8811
8909
  // Kill any running preview server
8812
8910
  killPreview();
8813
8911