@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 +2 -0
- package/package.json +2 -2
- package/src/cli.js +138 -2
- package/src/gateway-state.js +37 -0
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.
|
|
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://
|
|
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) {
|
package/src/gateway-state.js
CHANGED
|
@@ -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
|
+
}
|