@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  # waterbrother
2
2
 
3
- A local coding CLI that connects to Grok (`api.x.ai`) with codex/claude-style interactive workflows, local tool calls, session persistence, and approval controls.
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 API key
30
- - offers opening `https://console.x.ai/`
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
- - Grok API integration (`/chat/completions`)
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://console.x.ai and summarize how to create an API key"
181
- waterbrother "Search the web for the latest xAI API docs about vision support and cite the sources"
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
- /read https://console.x.ai/
185
- /search latest xAI vision docs
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 model grok-4.20-beta-0309-reasoning --scope project
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",
4
- "description": "Waterbrother: Grok-powered coding CLI with local tools, sessions, operator modes, and approval controls",
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 "./grok-client.js";
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
  }
@@ -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
+ }