@tritard/waterbrother 0.16.10 → 0.16.12

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/README.md CHANGED
@@ -32,6 +32,7 @@ It is Vercel-ready via `vercel.json` (clean URLs, no build step required).
32
32
  - tries a live provider model list after credentials, shows when that live list is in use, then falls back to the built-in catalog
33
33
  - prompts for default model and agent profile
34
34
  - can optionally configure Telegram during onboarding and offer to open the Telegram guide in your browser
35
+ - if Telegram is configured for startup, the TUI autostarts the background Telegram gateway on launch
35
36
  - Multi-provider chat integration through provider adapters and model registry
36
37
  - Vision command for local images: `waterbrother vision <image-path> <prompt>`
37
38
  - Authenticated GitHub repo reading for GitHub URLs, including private repos when `gh` is logged in
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.10",
3
+ "version": "0.16.12",
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": {
@@ -27,7 +27,7 @@
27
27
  "type": "git",
28
28
  "url": "git+https://github.com/PhillipHolland/waterbrother.git"
29
29
  },
30
- "homepage": "https://github.com/PhillipHolland/waterbrother#readme",
30
+ "homepage": "https://waterbrother.app",
31
31
  "bugs": {
32
32
  "url": "https://github.com/PhillipHolland/waterbrother/issues"
33
33
  },
package/src/cli.js CHANGED
@@ -1,4 +1,4 @@
1
- import { execFile } from "node:child_process";
1
+ import { execFile, spawn } from "node:child_process";
2
2
  import fs from "node:fs/promises";
3
3
  import path from "node:path";
4
4
  import process from "node:process";
@@ -10,7 +10,7 @@ import { getConfigPath, loadConfigLayers, resolveRuntimeConfig, saveConfig } fro
10
10
  import { createChatCompletion, createJsonCompletion, listModels } from "./model-client.js";
11
11
  import { buildChannelOnboardingPayload, getChannelStatuses, getGatewayStatus } from "./channels.js";
12
12
  import { runGatewayService } from "./gateway.js";
13
- import { DEFAULT_PENDING_PAIRING_TTL_MINUTES, loadGatewayState, prunePendingPairings, saveGatewayState } from "./gateway-state.js";
13
+ import { DEFAULT_PENDING_PAIRING_TTL_MINUTES, loadGatewayProcessInfo, loadGatewayState, prunePendingPairings, saveGatewayProcessInfo, saveGatewayState } from "./gateway-state.js";
14
14
  import {
15
15
  getDefaultDesignModelForProvider,
16
16
  getDefaultModelForProvider,
@@ -60,7 +60,8 @@ import { parseCharterFromGoal, runExperimentLoop, formatExperimentSummary, gitRe
60
60
 
61
61
  const execFileAsync = promisify(execFile);
62
62
  const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
63
- const DOCS_BASE_URL = String(process.env.WATERBROTHER_DOCS_BASE_URL || "https://waterbrother.ai").trim().replace(/\/+$/, "");
63
+ const BIN_PATH = path.join(PACKAGE_ROOT, "bin", "waterbrother.js");
64
+ const DOCS_BASE_URL = String(process.env.WATERBROTHER_DOCS_BASE_URL || "https://waterbrother.app").trim().replace(/\/+$/, "");
64
65
 
65
66
 
66
67
  const MODEL_CATALOG = listLocalModels();
@@ -2708,6 +2709,62 @@ function getDocsUrl(pathname = "") {
2708
2709
  return `${DOCS_BASE_URL}${path}`;
2709
2710
  }
2710
2711
 
2712
+ function isProcessAlive(pid) {
2713
+ const value = Number(pid);
2714
+ if (!Number.isFinite(value) || value <= 0) return false;
2715
+ try {
2716
+ process.kill(Math.floor(value), 0);
2717
+ return true;
2718
+ } catch {
2719
+ return false;
2720
+ }
2721
+ }
2722
+
2723
+ async function maybeAutostartGateway(runtime, { cwd, io = console } = {}) {
2724
+ if (process.env.WATERBROTHER_GATEWAY_CHILD === "1") {
2725
+ return { attempted: false, started: false, reason: "gateway-child" };
2726
+ }
2727
+
2728
+ const gateway = runtime?.gateway || {};
2729
+ const startupChannels = Array.isArray(gateway.startupChannels)
2730
+ ? gateway.startupChannels.map((value) => String(value || "").trim().toLowerCase()).filter(Boolean)
2731
+ : [];
2732
+
2733
+ if (!gateway.enabled || !startupChannels.includes("telegram")) {
2734
+ return { attempted: false, started: false, reason: "not-configured" };
2735
+ }
2736
+
2737
+ const telegram = runtime?.channels?.telegram || {};
2738
+ if (!telegram.enabled || !String(telegram.botToken || "").trim()) {
2739
+ return { attempted: false, started: false, reason: "telegram-not-ready" };
2740
+ }
2741
+
2742
+ const processInfo = await loadGatewayProcessInfo("telegram");
2743
+ if (isProcessAlive(processInfo.pid)) {
2744
+ return { attempted: true, started: false, reason: "already-running", pid: processInfo.pid };
2745
+ }
2746
+
2747
+ const child = spawn(process.execPath, [BIN_PATH, "gateway", "run", "telegram", "--skip-onboarding"], {
2748
+ cwd,
2749
+ detached: true,
2750
+ stdio: "ignore",
2751
+ env: {
2752
+ ...process.env,
2753
+ WATERBROTHER_GATEWAY_CHILD: "1"
2754
+ }
2755
+ });
2756
+ child.unref();
2757
+
2758
+ await saveGatewayProcessInfo("telegram", {
2759
+ pid: child.pid,
2760
+ startedAt: new Date().toISOString(),
2761
+ command: "gateway run telegram --skip-onboarding"
2762
+ });
2763
+
2764
+ io.log?.(dim(`telegram gateway autostarted in background (pid ${child.pid})`));
2765
+ return { attempted: true, started: true, pid: child.pid };
2766
+ }
2767
+
2711
2768
  async function chooseFromInteractiveMenu({ title, options, defaultIndex = 0 }) {
2712
2769
  if (!process.stdin.isTTY || !process.stdout.isTTY || options.length === 0) {
2713
2770
  return options[Math.max(0, Math.min(defaultIndex, options.length - 1))] || null;
@@ -3075,7 +3132,8 @@ async function runOnboardingWizard(config, { cwd }) {
3075
3132
  };
3076
3133
 
3077
3134
  console.log(green("Telegram configured for DM-first pairing."));
3078
- console.log(dim("After onboarding, run `waterbrother gateway run telegram`, DM the bot once, then approve the pending pairing locally."));
3135
+ console.log(dim("When you launch the Waterbrother TUI, the Telegram gateway will now autostart in the background."));
3136
+ console.log(dim("You can still run `waterbrother gateway run telegram` manually if you want a dedicated gateway process."));
3079
3137
  } else {
3080
3138
  console.log(dim("Skipping Telegram setup for now. You can always run `waterbrother onboarding telegram` later."));
3081
3139
  }
@@ -6476,6 +6534,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
6476
6534
  }
6477
6535
  }
6478
6536
 
6537
+ await maybeAutostartGateway(context.runtime, { cwd: context.cwd });
6479
6538
  printInteractiveHeader({ runtime: context.runtime, agent, cwd: context.cwd });
6480
6539
  console.log("Interactive mode. Type /exit to quit, /help for commands.");
6481
6540
  if (process.stdout.isTTY) {
@@ -9,6 +9,10 @@ function gatewayStatePath(serviceId) {
9
9
  return path.join(GATEWAY_DIR, `${String(serviceId || "").trim().toLowerCase()}.json`);
10
10
  }
11
11
 
12
+ function gatewayProcessPath(serviceId) {
13
+ return path.join(GATEWAY_DIR, `${String(serviceId || "").trim().toLowerCase()}.process.json`);
14
+ }
15
+
12
16
  function normalizeGatewayState(parsed = {}) {
13
17
  return {
14
18
  offset: Number.isFinite(Number(parsed?.offset)) ? Math.max(0, Math.floor(Number(parsed.offset))) : 0,
@@ -68,3 +72,36 @@ export async function saveGatewayState(serviceId, state) {
68
72
  await fs.rename(tmpPath, filePath);
69
73
  return next;
70
74
  }
75
+
76
+ export async function loadGatewayProcessInfo(serviceId) {
77
+ await ensureGatewayStateDir();
78
+ const filePath = gatewayProcessPath(serviceId);
79
+ try {
80
+ const raw = await fs.readFile(filePath, "utf8");
81
+ const parsed = JSON.parse(raw);
82
+ return {
83
+ pid: Number.isFinite(Number(parsed?.pid)) ? Math.floor(Number(parsed.pid)) : 0,
84
+ startedAt: String(parsed?.startedAt || "").trim(),
85
+ command: String(parsed?.command || "").trim()
86
+ };
87
+ } catch (error) {
88
+ if (error?.code === "ENOENT") {
89
+ return { pid: 0, startedAt: "", command: "" };
90
+ }
91
+ throw error;
92
+ }
93
+ }
94
+
95
+ export async function saveGatewayProcessInfo(serviceId, info = {}) {
96
+ await ensureGatewayStateDir();
97
+ const filePath = gatewayProcessPath(serviceId);
98
+ const next = {
99
+ pid: Number.isFinite(Number(info?.pid)) ? Math.floor(Number(info.pid)) : 0,
100
+ startedAt: String(info?.startedAt || "").trim(),
101
+ command: String(info?.command || "").trim()
102
+ };
103
+ const tmpPath = `${filePath}.${process.pid}.${Date.now()}.tmp`;
104
+ await fs.writeFile(tmpPath, `${JSON.stringify(next, null, 2)}\n`, "utf8");
105
+ await fs.rename(tmpPath, filePath);
106
+ return next;
107
+ }