@tritard/waterbrother 0.15.4 → 0.15.6
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 +85 -9
- package/package.json +2 -3
- package/src/agent.js +6 -1
- package/src/channels.js +238 -0
- package/src/cli.js +860 -229
- package/src/config.js +115 -61
- package/src/decider.js +1 -1
- package/src/frontend.js +1 -1
- package/src/gateway-state.js +44 -0
- package/src/gateway.js +389 -0
- package/src/grok-client.js +1 -268
- package/src/model-catalog.js +352 -0
- package/src/model-client.js +84 -0
- package/src/planner.js +1 -1
- package/src/providers/anthropic.js +173 -0
- package/src/providers/openai-compatible.js +162 -0
- package/src/reviewer.js +1 -1
- package/src/session-store.js +3 -0
- package/src/voice.js +10 -9
package/README.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
# waterbrother
|
|
2
2
|
|
|
3
|
-
A local coding CLI
|
|
3
|
+
A local coding CLI with codex/claude-style interactive workflows, local tool calls, session persistence, approval controls, and bring-your-own-model provider support across OpenAI, Anthropic, OpenRouter, Ollama, xAI, and compatible APIs.
|
|
4
4
|
|
|
5
5
|
## Web docs interface
|
|
6
6
|
|
|
@@ -8,6 +8,7 @@ This repo includes a static docs web interface:
|
|
|
8
8
|
|
|
9
9
|
- `/` landing page
|
|
10
10
|
- `/onboarding` onboarding + API key guide
|
|
11
|
+
- `/channels` messaging-channel guide
|
|
11
12
|
- `/install` install instructions
|
|
12
13
|
- `/usage` usage guide
|
|
13
14
|
- `/commands` command reference
|
|
@@ -26,10 +27,10 @@ It is Vercel-ready via `vercel.json` (clean URLs, no build step required).
|
|
|
26
27
|
- `waterbrother resume [session-id] [prompt]`
|
|
27
28
|
- `waterbrother resume --last`
|
|
28
29
|
- First-run onboarding wizard in terminal
|
|
29
|
-
- asks for
|
|
30
|
-
-
|
|
30
|
+
- asks for provider first
|
|
31
|
+
- asks for API key when the selected provider requires one
|
|
31
32
|
- prompts for default model and agent profile
|
|
32
|
-
-
|
|
33
|
+
- Multi-provider chat integration through provider adapters and model registry
|
|
33
34
|
- Vision command for local images: `waterbrother vision <image-path> <prompt>`
|
|
34
35
|
- Authenticated GitHub repo reading for GitHub URLs, including private repos when `gh` is logged in
|
|
35
36
|
- Built-in webpage reading and web search tools for pasted URLs and open-web research prompts
|
|
@@ -39,6 +40,9 @@ It is Vercel-ready via `vercel.json` (clean URLs, no build step required).
|
|
|
39
40
|
- Agent profiles: `coder`, `designer`, `reviewer`, `planner`
|
|
40
41
|
- Model discovery command (`waterbrother models list`)
|
|
41
42
|
- Local model catalog (`waterbrother models catalog`)
|
|
43
|
+
- Active runtime model status (`waterbrother models status`)
|
|
44
|
+
- Named runtime provider/model presets (`waterbrother runtime-profiles ...`)
|
|
45
|
+
- Messaging gateway and channel readiness commands (`waterbrother gateway status`, `waterbrother channels status`)
|
|
42
46
|
- Onboarding guide command (`waterbrother onboarding`)
|
|
43
47
|
- Local self-update command (`waterbrother update`)
|
|
44
48
|
- git-clone installs pull latest source, install deps, and run checks
|
|
@@ -169,6 +173,10 @@ Utility commands:
|
|
|
169
173
|
waterbrother doctor
|
|
170
174
|
waterbrother models list
|
|
171
175
|
waterbrother models catalog
|
|
176
|
+
waterbrother models status
|
|
177
|
+
waterbrother runtime-profiles list
|
|
178
|
+
waterbrother channels status
|
|
179
|
+
waterbrother gateway status
|
|
172
180
|
waterbrother mcp list
|
|
173
181
|
waterbrother onboarding
|
|
174
182
|
waterbrother update
|
|
@@ -177,15 +185,82 @@ waterbrother update
|
|
|
177
185
|
Web research examples:
|
|
178
186
|
|
|
179
187
|
```bash
|
|
180
|
-
waterbrother "Read https://
|
|
181
|
-
waterbrother "Search the web for the latest
|
|
188
|
+
waterbrother "Read https://docs.anthropic.com/en/api/messages and summarize tool use"
|
|
189
|
+
waterbrother "Search the web for the latest model provider docs about tool calling and cite the sources"
|
|
182
190
|
|
|
183
191
|
# interactive
|
|
184
|
-
/
|
|
185
|
-
/
|
|
192
|
+
/provider
|
|
193
|
+
/provider anthropic
|
|
194
|
+
/providers
|
|
195
|
+
/runtime
|
|
196
|
+
/channels
|
|
197
|
+
/gateway
|
|
198
|
+
/onboarding telegram
|
|
199
|
+
/runtime-profile save review-anthropic
|
|
200
|
+
/runtime-profile load review-anthropic
|
|
201
|
+
/runtime-profiles
|
|
202
|
+
/model anthropic/claude-sonnet-4-20250514
|
|
203
|
+
/models
|
|
204
|
+
|
|
205
|
+
# config
|
|
206
|
+
waterbrother config set provider anthropic --scope project
|
|
207
|
+
waterbrother config set model anthropic/claude-sonnet-4-20250514 --scope project
|
|
208
|
+
|
|
209
|
+
# inspect runtime
|
|
210
|
+
waterbrother models status
|
|
211
|
+
waterbrother runtime-profiles show review-anthropic
|
|
212
|
+
waterbrother channels status
|
|
213
|
+
waterbrother gateway status
|
|
214
|
+
|
|
215
|
+
# interactive
|
|
216
|
+
/read https://docs.anthropic.com/en/api/messages
|
|
217
|
+
/search latest model provider tool calling docs
|
|
186
218
|
/open 1
|
|
187
219
|
```
|
|
188
220
|
|
|
221
|
+
## Messaging foundation
|
|
222
|
+
|
|
223
|
+
Waterbrother now includes the first messaging-control foundation.
|
|
224
|
+
|
|
225
|
+
- Services targeted first:
|
|
226
|
+
- Telegram
|
|
227
|
+
- Discord
|
|
228
|
+
- Signal
|
|
229
|
+
- Current scope:
|
|
230
|
+
- normalized channel config
|
|
231
|
+
- gateway/channel readiness reporting
|
|
232
|
+
- service-specific onboarding
|
|
233
|
+
- Live now:
|
|
234
|
+
- Telegram single-user remote control
|
|
235
|
+
- Not shipped yet:
|
|
236
|
+
- Discord live adapter
|
|
237
|
+
- Signal live adapter
|
|
238
|
+
- group DM collaboration
|
|
239
|
+
|
|
240
|
+
Use the new commands to prepare and validate channel setup:
|
|
241
|
+
|
|
242
|
+
```bash
|
|
243
|
+
waterbrother channels list
|
|
244
|
+
waterbrother channels status
|
|
245
|
+
waterbrother channels show telegram
|
|
246
|
+
waterbrother gateway status
|
|
247
|
+
waterbrother gateway run telegram
|
|
248
|
+
waterbrother onboarding telegram
|
|
249
|
+
waterbrother onboarding discord
|
|
250
|
+
waterbrother onboarding signal
|
|
251
|
+
```
|
|
252
|
+
|
|
253
|
+
Rollout order:
|
|
254
|
+
1. single-user remote terminal control
|
|
255
|
+
2. status and observation from linked channels
|
|
256
|
+
3. approvals over messaging
|
|
257
|
+
4. only then group DM collaboration
|
|
258
|
+
|
|
259
|
+
Current Telegram limitation:
|
|
260
|
+
- remote prompts run with `approval=never`
|
|
261
|
+
- status, runtime inspection, and read-oriented prompts work best
|
|
262
|
+
- mutating and approval-heavy work stay local until remote approvals land
|
|
263
|
+
|
|
189
264
|
## Release flow
|
|
190
265
|
|
|
191
266
|
Partners should ship updates by pushing a version tag, not by running `npm publish` locally.
|
|
@@ -240,7 +315,8 @@ Long-running session controls:
|
|
|
240
315
|
waterbrother config set autoCompactThreshold 0.9
|
|
241
316
|
waterbrother config set traceMode verbose
|
|
242
317
|
waterbrother config set receiptMode verbose
|
|
243
|
-
waterbrother config set
|
|
318
|
+
waterbrother config set provider openai --scope project
|
|
319
|
+
waterbrother config set model openai/gpt-4.1 --scope project
|
|
244
320
|
waterbrother config set-json approvalPolicy '{"autoApprovePaths":["src/**"],"askPaths":["package.json"],"denyShellPatterns":["git\\s+reset\\s+--hard"]}' --scope project
|
|
245
321
|
waterbrother config set-json verification '{"commands":["npm run lint","npm test"],"timeoutMs":180000}' --scope project
|
|
246
322
|
waterbrother config set-json mcpServers '{"filesystem":{"command":"npx","args":["-y","@modelcontextprotocol/server-filesystem","."]}}' --scope project
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tritard/waterbrother",
|
|
3
|
-
"version": "0.15.
|
|
4
|
-
"description": "Waterbrother:
|
|
3
|
+
"version": "0.15.6",
|
|
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": {
|
|
7
7
|
"waterbrother": "bin/waterbrother.js"
|
|
@@ -33,7 +33,6 @@
|
|
|
33
33
|
},
|
|
34
34
|
"keywords": [
|
|
35
35
|
"cli",
|
|
36
|
-
"grok",
|
|
37
36
|
"coding-agent",
|
|
38
37
|
"terminal",
|
|
39
38
|
"ai",
|
package/src/agent.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { createChatCompletion, createChatCompletionStream } from "./
|
|
1
|
+
import { createChatCompletion, createChatCompletionStream } from "./model-client.js";
|
|
2
2
|
import { getAutonomyModePrompt, getExperienceModePrompt, normalizeAutonomyMode, normalizeExperienceMode } from "./modes.js";
|
|
3
3
|
import { createToolRuntime } from "./tools.js";
|
|
4
4
|
|
|
@@ -363,6 +363,11 @@ export class Agent {
|
|
|
363
363
|
this.model = model;
|
|
364
364
|
}
|
|
365
365
|
|
|
366
|
+
setTransport({ apiKey, baseUrl } = {}) {
|
|
367
|
+
if (apiKey !== undefined) this.apiKey = apiKey;
|
|
368
|
+
if (baseUrl !== undefined) this.baseUrl = baseUrl;
|
|
369
|
+
}
|
|
370
|
+
|
|
366
371
|
getModel() {
|
|
367
372
|
return this.model;
|
|
368
373
|
}
|
package/src/channels.js
ADDED
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
const CHANNEL_SPECS = {
|
|
2
|
+
telegram: {
|
|
3
|
+
id: "telegram",
|
|
4
|
+
label: "Telegram",
|
|
5
|
+
docsPath: "/onboarding/telegram",
|
|
6
|
+
envKeys: ["TELEGRAM_BOT_TOKEN"],
|
|
7
|
+
requiredFields: ["botToken"]
|
|
8
|
+
},
|
|
9
|
+
discord: {
|
|
10
|
+
id: "discord",
|
|
11
|
+
label: "Discord",
|
|
12
|
+
docsPath: "/onboarding/discord",
|
|
13
|
+
envKeys: ["DISCORD_BOT_TOKEN", "DISCORD_APPLICATION_ID"],
|
|
14
|
+
requiredFields: ["botToken", "applicationId"]
|
|
15
|
+
},
|
|
16
|
+
signal: {
|
|
17
|
+
id: "signal",
|
|
18
|
+
label: "Signal",
|
|
19
|
+
docsPath: "/onboarding/signal",
|
|
20
|
+
envKeys: ["SIGNAL_PHONE_NUMBER"],
|
|
21
|
+
requiredFields: ["phoneNumber"]
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const CHANNEL_IDS = Object.keys(CHANNEL_SPECS);
|
|
26
|
+
|
|
27
|
+
function asStringArray(value) {
|
|
28
|
+
if (!Array.isArray(value)) return [];
|
|
29
|
+
return value.map((item) => String(item || "").trim()).filter(Boolean);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function normalizePairingMode(value, fallback = "manual") {
|
|
33
|
+
const next = String(value || fallback).trim().toLowerCase();
|
|
34
|
+
return ["manual", "allowlist"].includes(next) ? next : fallback;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function normalizePort(value, fallback) {
|
|
38
|
+
const parsed = Number(value);
|
|
39
|
+
return Number.isFinite(parsed) ? Math.max(1, Math.min(65535, Math.floor(parsed))) : fallback;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function normalizeCommonChannelConfig(value = {}) {
|
|
43
|
+
const source = value && typeof value === "object" ? value : {};
|
|
44
|
+
return {
|
|
45
|
+
enabled: Boolean(source.enabled),
|
|
46
|
+
pairingMode: normalizePairingMode(source.pairingMode),
|
|
47
|
+
defaultRuntimeProfile: String(source.defaultRuntimeProfile || "").trim(),
|
|
48
|
+
linkedSessionId: String(source.linkedSessionId || "").trim(),
|
|
49
|
+
allowedUserIds: asStringArray(source.allowedUserIds)
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function normalizeTelegramConfig(value = {}) {
|
|
54
|
+
const source = value && typeof value === "object" ? value : {};
|
|
55
|
+
const common = normalizeCommonChannelConfig(source);
|
|
56
|
+
return {
|
|
57
|
+
...common,
|
|
58
|
+
botToken: String(source.botToken || "").trim()
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeDiscordConfig(value = {}) {
|
|
63
|
+
const source = value && typeof value === "object" ? value : {};
|
|
64
|
+
const common = normalizeCommonChannelConfig(source);
|
|
65
|
+
return {
|
|
66
|
+
...common,
|
|
67
|
+
botToken: String(source.botToken || "").trim(),
|
|
68
|
+
applicationId: String(source.applicationId || "").trim(),
|
|
69
|
+
allowedGuildIds: asStringArray(source.allowedGuildIds)
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function normalizeSignalConfig(value = {}) {
|
|
74
|
+
const source = value && typeof value === "object" ? value : {};
|
|
75
|
+
const common = normalizeCommonChannelConfig(source);
|
|
76
|
+
return {
|
|
77
|
+
...common,
|
|
78
|
+
phoneNumber: String(source.phoneNumber || "").trim(),
|
|
79
|
+
signalCliPath: String(source.signalCliPath || "signal-cli").trim()
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export function normalizeChannelsConfig(value = {}) {
|
|
84
|
+
const source = value && typeof value === "object" ? value : {};
|
|
85
|
+
return {
|
|
86
|
+
telegram: normalizeTelegramConfig(source.telegram),
|
|
87
|
+
discord: normalizeDiscordConfig(source.discord),
|
|
88
|
+
signal: normalizeSignalConfig(source.signal)
|
|
89
|
+
};
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export function normalizeGatewayConfig(value = {}) {
|
|
93
|
+
const source = value && typeof value === "object" ? value : {};
|
|
94
|
+
return {
|
|
95
|
+
enabled: Boolean(source.enabled),
|
|
96
|
+
bindAddress: String(source.bindAddress || "127.0.0.1").trim() || "127.0.0.1",
|
|
97
|
+
port: normalizePort(source.port, 4581),
|
|
98
|
+
controlMode: String(source.controlMode || "single-user").trim().toLowerCase() === "group" ? "group" : "single-user",
|
|
99
|
+
defaultRuntimeProfile: String(source.defaultRuntimeProfile || "").trim(),
|
|
100
|
+
startupChannels: asStringArray(source.startupChannels).filter((item) => CHANNEL_IDS.includes(item)),
|
|
101
|
+
requirePairing: source.requirePairing !== false
|
|
102
|
+
};
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function summarizeChannel(serviceId, config = {}) {
|
|
106
|
+
const spec = CHANNEL_SPECS[serviceId];
|
|
107
|
+
const missing = [];
|
|
108
|
+
|
|
109
|
+
if (config.enabled) {
|
|
110
|
+
for (const field of spec.requiredFields) {
|
|
111
|
+
if (!String(config[field] || "").trim()) missing.push(field);
|
|
112
|
+
}
|
|
113
|
+
if (config.pairingMode === "allowlist" && (config.allowedUserIds || []).length === 0) {
|
|
114
|
+
missing.push("allowedUserIds");
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const configured = spec.requiredFields.some((field) => String(config[field] || "").trim());
|
|
119
|
+
const ready = Boolean(config.enabled) && missing.length === 0;
|
|
120
|
+
const state = !config.enabled ? "disabled" : ready ? "ready" : "partial";
|
|
121
|
+
|
|
122
|
+
return {
|
|
123
|
+
id: spec.id,
|
|
124
|
+
label: spec.label,
|
|
125
|
+
docsPath: spec.docsPath,
|
|
126
|
+
enabled: Boolean(config.enabled),
|
|
127
|
+
configured,
|
|
128
|
+
ready,
|
|
129
|
+
state,
|
|
130
|
+
pairingMode: config.pairingMode || "manual",
|
|
131
|
+
defaultRuntimeProfile: config.defaultRuntimeProfile || "",
|
|
132
|
+
linkedSessionId: config.linkedSessionId || "",
|
|
133
|
+
missing,
|
|
134
|
+
requiredFields: [...spec.requiredFields],
|
|
135
|
+
envKeys: [...spec.envKeys]
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
export function getChannelSpec(serviceId) {
|
|
140
|
+
return CHANNEL_SPECS[String(serviceId || "").trim().toLowerCase()] || null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export function getChannelStatuses(runtime = {}) {
|
|
144
|
+
const channels = normalizeChannelsConfig(runtime.channels || {});
|
|
145
|
+
return CHANNEL_IDS.map((serviceId) => summarizeChannel(serviceId, channels[serviceId]));
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
export function getGatewayStatus(runtime = {}) {
|
|
149
|
+
const gateway = normalizeGatewayConfig(runtime.gateway || {});
|
|
150
|
+
const channels = getChannelStatuses(runtime);
|
|
151
|
+
const enabledChannels = channels.filter((channel) => channel.enabled);
|
|
152
|
+
const readyChannels = enabledChannels.filter((channel) => channel.ready);
|
|
153
|
+
return {
|
|
154
|
+
enabled: gateway.enabled,
|
|
155
|
+
bindAddress: gateway.bindAddress,
|
|
156
|
+
port: gateway.port,
|
|
157
|
+
controlMode: gateway.controlMode,
|
|
158
|
+
requirePairing: gateway.requirePairing,
|
|
159
|
+
defaultRuntimeProfile: gateway.defaultRuntimeProfile,
|
|
160
|
+
startupChannels: gateway.startupChannels,
|
|
161
|
+
enabledChannelCount: enabledChannels.length,
|
|
162
|
+
readyChannelCount: readyChannels.length,
|
|
163
|
+
ready: gateway.enabled && enabledChannels.length > 0 && readyChannels.length === enabledChannels.length,
|
|
164
|
+
channels
|
|
165
|
+
};
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export function buildChannelOnboardingPayload(serviceId) {
|
|
169
|
+
const spec = getChannelSpec(serviceId);
|
|
170
|
+
if (!spec) return null;
|
|
171
|
+
|
|
172
|
+
if (serviceId === "telegram") {
|
|
173
|
+
return {
|
|
174
|
+
id: spec.id,
|
|
175
|
+
label: spec.label,
|
|
176
|
+
docsPath: spec.docsPath,
|
|
177
|
+
summary: "Single-user Telegram control through a bot token and DM pairing.",
|
|
178
|
+
prerequisites: [
|
|
179
|
+
"Create a Telegram bot with BotFather",
|
|
180
|
+
"Capture the bot token",
|
|
181
|
+
"Choose manual pairing or an allowlist"
|
|
182
|
+
],
|
|
183
|
+
commands: [
|
|
184
|
+
"waterbrother config set-json channels '{\"telegram\":{\"enabled\":true,\"botToken\":\"YOUR_BOT_TOKEN\",\"pairingMode\":\"manual\",\"allowedUserIds\":[]}}'",
|
|
185
|
+
"waterbrother channels status",
|
|
186
|
+
"waterbrother gateway status"
|
|
187
|
+
],
|
|
188
|
+
notes: [
|
|
189
|
+
"Start with direct messages only.",
|
|
190
|
+
"Keep pairing manual until remote-control approvals are stable."
|
|
191
|
+
]
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
if (serviceId === "discord") {
|
|
196
|
+
return {
|
|
197
|
+
id: spec.id,
|
|
198
|
+
label: spec.label,
|
|
199
|
+
docsPath: spec.docsPath,
|
|
200
|
+
summary: "Single-user Discord control through a bot token, application id, and DM-first policy.",
|
|
201
|
+
prerequisites: [
|
|
202
|
+
"Create a Discord application and bot",
|
|
203
|
+
"Capture the bot token and application id",
|
|
204
|
+
"Enable only the intents you actually need"
|
|
205
|
+
],
|
|
206
|
+
commands: [
|
|
207
|
+
"waterbrother config set-json channels '{\"discord\":{\"enabled\":true,\"botToken\":\"YOUR_BOT_TOKEN\",\"applicationId\":\"YOUR_APP_ID\",\"pairingMode\":\"manual\",\"allowedUserIds\":[]}}'",
|
|
208
|
+
"waterbrother channels status",
|
|
209
|
+
"waterbrother gateway status"
|
|
210
|
+
],
|
|
211
|
+
notes: [
|
|
212
|
+
"Start with direct messages before any guild or channel workflow.",
|
|
213
|
+
"Prefer explicit user allowlists."
|
|
214
|
+
]
|
|
215
|
+
};
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
return {
|
|
219
|
+
id: spec.id,
|
|
220
|
+
label: spec.label,
|
|
221
|
+
docsPath: spec.docsPath,
|
|
222
|
+
summary: "Single-user Signal control through signal-cli and a paired local account.",
|
|
223
|
+
prerequisites: [
|
|
224
|
+
"Install signal-cli locally",
|
|
225
|
+
"Register or link a Signal number/device",
|
|
226
|
+
"Choose manual pairing or an allowlist"
|
|
227
|
+
],
|
|
228
|
+
commands: [
|
|
229
|
+
"waterbrother config set-json channels '{\"signal\":{\"enabled\":true,\"phoneNumber\":\"+15551234567\",\"signalCliPath\":\"signal-cli\",\"pairingMode\":\"manual\",\"allowedUserIds\":[]}}'",
|
|
230
|
+
"waterbrother channels status",
|
|
231
|
+
"waterbrother gateway status"
|
|
232
|
+
],
|
|
233
|
+
notes: [
|
|
234
|
+
"Signal is operationally heavier than Telegram or Discord.",
|
|
235
|
+
"Treat it as a controlled adapter, not a zero-config bot."
|
|
236
|
+
]
|
|
237
|
+
};
|
|
238
|
+
}
|