@tritard/waterbrother 0.16.9 → 0.16.11

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
@@ -31,6 +31,8 @@ It is Vercel-ready via `vercel.json` (clean URLs, no build step required).
31
31
  - asks for API key when the selected provider requires one
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
+ - 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
34
36
  - Multi-provider chat integration through provider adapters and model registry
35
37
  - Vision command for local images: `waterbrother vision <image-path> <prompt>`
36
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.9",
3
+ "version": "0.16.11",
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,6 +60,7 @@ 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.app").trim().replace(/\/+$/, "");
63
64
 
64
65
 
65
66
  const MODEL_CATALOG = listLocalModels();
@@ -2702,6 +2703,67 @@ async function maybeOpenConsoleUrl(provider) {
2702
2703
  return maybeOpenUrl(url);
2703
2704
  }
2704
2705
 
2706
+ function getDocsUrl(pathname = "") {
2707
+ const path = `/${String(pathname || "").trim().replace(/^\/+/, "")}`;
2708
+ return `${DOCS_BASE_URL}${path}`;
2709
+ }
2710
+
2711
+ function isProcessAlive(pid) {
2712
+ const value = Number(pid);
2713
+ if (!Number.isFinite(value) || value <= 0) return false;
2714
+ try {
2715
+ process.kill(Math.floor(value), 0);
2716
+ return true;
2717
+ } catch {
2718
+ return false;
2719
+ }
2720
+ }
2721
+
2722
+ async function maybeAutostartGateway(runtime, { cwd, io = console } = {}) {
2723
+ if (process.env.WATERBROTHER_GATEWAY_CHILD === "1") {
2724
+ return { attempted: false, started: false, reason: "gateway-child" };
2725
+ }
2726
+
2727
+ const gateway = runtime?.gateway || {};
2728
+ const startupChannels = Array.isArray(gateway.startupChannels)
2729
+ ? gateway.startupChannels.map((value) => String(value || "").trim().toLowerCase()).filter(Boolean)
2730
+ : [];
2731
+
2732
+ if (!gateway.enabled || !startupChannels.includes("telegram")) {
2733
+ return { attempted: false, started: false, reason: "not-configured" };
2734
+ }
2735
+
2736
+ const telegram = runtime?.channels?.telegram || {};
2737
+ if (!telegram.enabled || !String(telegram.botToken || "").trim()) {
2738
+ return { attempted: false, started: false, reason: "telegram-not-ready" };
2739
+ }
2740
+
2741
+ const processInfo = await loadGatewayProcessInfo("telegram");
2742
+ if (isProcessAlive(processInfo.pid)) {
2743
+ return { attempted: true, started: false, reason: "already-running", pid: processInfo.pid };
2744
+ }
2745
+
2746
+ const child = spawn(process.execPath, [BIN_PATH, "gateway", "run", "telegram", "--skip-onboarding"], {
2747
+ cwd,
2748
+ detached: true,
2749
+ stdio: "ignore",
2750
+ env: {
2751
+ ...process.env,
2752
+ WATERBROTHER_GATEWAY_CHILD: "1"
2753
+ }
2754
+ });
2755
+ child.unref();
2756
+
2757
+ await saveGatewayProcessInfo("telegram", {
2758
+ pid: child.pid,
2759
+ startedAt: new Date().toISOString(),
2760
+ command: "gateway run telegram --skip-onboarding"
2761
+ });
2762
+
2763
+ io.log?.(dim(`telegram gateway autostarted in background (pid ${child.pid})`));
2764
+ return { attempted: true, started: true, pid: child.pid };
2765
+ }
2766
+
2705
2767
  async function chooseFromInteractiveMenu({ title, options, defaultIndex = 0 }) {
2706
2768
  if (!process.stdin.isTTY || !process.stdout.isTTY || options.length === 0) {
2707
2769
  return options[Math.max(0, Math.min(defaultIndex, options.length - 1))] || null;
@@ -3002,6 +3064,79 @@ async function runOnboardingWizard(config, { cwd }) {
3002
3064
  allowRemote: providerSpec?.supportsRemoteModelList !== false
3003
3065
  });
3004
3066
  next.agentProfile = await chooseAgentProfileInteractive(next.agentProfile || 'coder');
3067
+ const configureTelegram = await promptYesNo("Enable Telegram remote control now?", {
3068
+ input: process.stdin,
3069
+ output: process.stdout
3070
+ });
3071
+ if (configureTelegram) {
3072
+ const alreadyHaveToken = await promptYesNo("Do you already have a Telegram BotFather token?", {
3073
+ input: process.stdin,
3074
+ output: process.stdout
3075
+ });
3076
+ if (!alreadyHaveToken) {
3077
+ console.log("Create a bot with @BotFather, copy the token, then come back here.");
3078
+ console.log(dim(`Guide: ${getDocsUrl("/onboarding/telegram")}`));
3079
+ const openGuide = await promptYesNo("Open the Telegram setup guide in your browser?", {
3080
+ input: process.stdin,
3081
+ output: process.stdout
3082
+ });
3083
+ if (openGuide) {
3084
+ const opened = await maybeOpenUrl(getDocsUrl("/onboarding/telegram"));
3085
+ if (!opened) {
3086
+ console.log(dim(`Could not open browser automatically. Visit ${getDocsUrl("/onboarding/telegram")}`));
3087
+ }
3088
+ }
3089
+ }
3090
+
3091
+ const existingToken = String(next.channels?.telegram?.botToken || "").trim();
3092
+ let telegramToken = existingToken;
3093
+ if (telegramToken) {
3094
+ const keepExistingTelegramToken = await promptYesNo(`Use saved Telegram bot token (${formatApiKeyHint(telegramToken)})?`, {
3095
+ input: process.stdin,
3096
+ output: process.stdout
3097
+ });
3098
+ if (!keepExistingTelegramToken) {
3099
+ telegramToken = "";
3100
+ }
3101
+ }
3102
+
3103
+ while (!telegramToken) {
3104
+ const enteredTelegramToken = (await promptLine("Paste Telegram bot token (Enter to skip for now): ")).trim();
3105
+ if (!enteredTelegramToken) break;
3106
+ telegramToken = enteredTelegramToken;
3107
+ }
3108
+
3109
+ if (telegramToken) {
3110
+ const channels = next.channels && typeof next.channels === "object" ? { ...next.channels } : {};
3111
+ const telegram = channels.telegram && typeof channels.telegram === "object" ? { ...channels.telegram } : {};
3112
+ channels.telegram = {
3113
+ ...telegram,
3114
+ enabled: true,
3115
+ botToken: telegramToken,
3116
+ pairingMode: telegram.pairingMode || "manual",
3117
+ allowedUserIds: Array.isArray(telegram.allowedUserIds) ? telegram.allowedUserIds : [],
3118
+ pairingExpiryMinutes: Number.isFinite(Number(telegram.pairingExpiryMinutes)) ? Math.max(1, Math.floor(Number(telegram.pairingExpiryMinutes))) : 720
3119
+ };
3120
+ next.channels = channels;
3121
+
3122
+ const gateway = next.gateway && typeof next.gateway === "object" ? { ...next.gateway } : {};
3123
+ const startupChannels = new Set(Array.isArray(gateway.startupChannels) ? gateway.startupChannels.map((value) => String(value || "").trim()).filter(Boolean) : []);
3124
+ startupChannels.add("telegram");
3125
+ next.gateway = {
3126
+ ...gateway,
3127
+ enabled: true,
3128
+ controlMode: gateway.controlMode === "group" ? "group" : "single-user",
3129
+ startupChannels: [...startupChannels],
3130
+ requirePairing: gateway.requirePairing !== false
3131
+ };
3132
+
3133
+ console.log(green("Telegram configured for DM-first pairing."));
3134
+ console.log(dim("When you launch the Waterbrother TUI, the Telegram gateway will now autostart in the background."));
3135
+ console.log(dim("You can still run `waterbrother gateway run telegram` manually if you want a dedicated gateway process."));
3136
+ } else {
3137
+ console.log(dim("Skipping Telegram setup for now. You can always run `waterbrother onboarding telegram` later."));
3138
+ }
3139
+ }
3005
3140
  if (!next.approvalMode) next.approvalMode = 'on-request';
3006
3141
  if (!next.designModel) next.designModel = getDefaultDesignModelForProvider(next.provider);
3007
3142
  if (next.traceMode === undefined) next.traceMode = 'on';
@@ -6398,6 +6533,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
6398
6533
  }
6399
6534
  }
6400
6535
 
6536
+ await maybeAutostartGateway(context.runtime, { cwd: context.cwd });
6401
6537
  printInteractiveHeader({ runtime: context.runtime, agent, cwd: context.cwd });
6402
6538
  console.log("Interactive mode. Type /exit to quit, /help for commands.");
6403
6539
  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
+ }