@tritard/waterbrother 0.16.128 → 0.16.131

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.128",
3
+ "version": "0.16.131",
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": {
package/src/cli.js CHANGED
@@ -336,6 +336,10 @@ Usage:
336
336
  waterbrother channels list
337
337
  waterbrother channels status
338
338
  waterbrother channels show <service>
339
+ waterbrother ps
340
+ waterbrother up
341
+ waterbrother down
342
+ waterbrother restart
339
343
  waterbrother discord status
340
344
  waterbrother discord run
341
345
  waterbrother gateway status
@@ -3026,6 +3030,133 @@ async function stopTrackedGateway(service = "telegram", { io = console } = {}) {
3026
3030
  return { stopped, pid };
3027
3031
  }
3028
3032
 
3033
+ async function withTrackedGatewayProcess(service, command, runner) {
3034
+ const normalized = String(service || "").trim().toLowerCase();
3035
+ const processInfo = await loadGatewayProcessInfo(normalized);
3036
+ if (isProcessAlive(processInfo.pid) && Number(processInfo.pid || 0) !== process.pid) {
3037
+ throw new Error(`${normalized} is already running (pid ${processInfo.pid}). Use \`waterbrother down\`, \`waterbrother restart\`, or \`waterbrother gateway stop ${normalized}\` first.`);
3038
+ }
3039
+ await saveGatewayProcessInfo(normalized, {
3040
+ pid: process.pid,
3041
+ startedAt: new Date().toISOString(),
3042
+ command
3043
+ });
3044
+ try {
3045
+ return await runner();
3046
+ } finally {
3047
+ const current = await loadGatewayProcessInfo(normalized);
3048
+ if (Number(current.pid || 0) === process.pid) {
3049
+ await saveGatewayProcessInfo(normalized, { pid: 0, startedAt: "", command: "" });
3050
+ }
3051
+ }
3052
+ }
3053
+
3054
+ async function getTrackedServiceStatus(service, runtime) {
3055
+ const normalized = String(service || "").trim().toLowerCase();
3056
+ const processInfo = await loadGatewayProcessInfo(normalized);
3057
+ const bridge = await loadGatewayBridge(normalized);
3058
+ const processAlive = isProcessAlive(processInfo.pid);
3059
+ const activeHostPid = Number(bridge.activeHost?.pid || 0);
3060
+ const liveHost = activeHostPid > 0 && isProcessAlive(activeHostPid) ? bridge.activeHost : null;
3061
+ const channel = runtime?.channels?.[normalized] || {};
3062
+ const configured = normalized === "telegram"
3063
+ ? Boolean(channel.enabled && String(channel.botToken || "").trim())
3064
+ : Boolean(channel.enabled && String(channel.botToken || "").trim() && String(channel.applicationId || "").trim());
3065
+ return {
3066
+ service: normalized,
3067
+ configured,
3068
+ running: processAlive,
3069
+ pid: processAlive ? Number(processInfo.pid || 0) : 0,
3070
+ command: processAlive ? String(processInfo.command || "").trim() : "",
3071
+ liveHost: liveHost ? {
3072
+ pid: Number(liveHost.pid || 0),
3073
+ sessionId: String(liveHost.sessionId || "").trim(),
3074
+ cwd: String(liveHost.cwd || "").trim(),
3075
+ label: String(liveHost.label || "").trim(),
3076
+ ownerName: String(liveHost.ownerName || "").trim()
3077
+ } : null,
3078
+ staleProcessInfo: Number(processInfo.pid || 0) > 0 && !processAlive
3079
+ };
3080
+ }
3081
+
3082
+ async function clearTrackedGatewayArtifacts(service) {
3083
+ const normalized = String(service || "").trim().toLowerCase();
3084
+ if (!["telegram", "discord"].includes(normalized)) return;
3085
+ await saveGatewayProcessInfo(normalized, { pid: 0, startedAt: "", command: "" });
3086
+ }
3087
+
3088
+ async function runProcessStatusCommand(runtime, { asJson = false } = {}) {
3089
+ const services = await Promise.all(["telegram", "discord"].map((service) => getTrackedServiceStatus(service, runtime)));
3090
+ if (asJson) {
3091
+ printData({ services }, true);
3092
+ return;
3093
+ }
3094
+ console.log("Waterbrother services");
3095
+ console.log("");
3096
+ for (const status of services) {
3097
+ console.log(`${status.service}: ${status.running ? `running (pid ${status.pid})` : "stopped"}`);
3098
+ console.log(` configured: ${status.configured ? "yes" : "no"}`);
3099
+ if (status.command) console.log(` command: ${status.command}`);
3100
+ if (status.liveHost) {
3101
+ console.log(` live host: ${status.liveHost.label || status.liveHost.ownerName || status.liveHost.sessionId || status.liveHost.pid}`);
3102
+ } else {
3103
+ console.log(" live host: none");
3104
+ }
3105
+ if (status.staleProcessInfo) {
3106
+ console.log(" stale process file: yes");
3107
+ }
3108
+ }
3109
+ }
3110
+
3111
+ async function runServicesUp(runtime, { cwd = process.cwd(), asJson = false } = {}) {
3112
+ const result = await maybeAutostartGateway(runtime, { cwd, io: console });
3113
+ if (asJson) {
3114
+ printData(result, true);
3115
+ return;
3116
+ }
3117
+ if (!result.attempted) {
3118
+ console.log(`No channel services started (${result.reason || "not configured"})`);
3119
+ return;
3120
+ }
3121
+ const started = (result.handles || []).filter((item) => item.started);
3122
+ const alreadyRunning = (result.handles || []).filter((item) => item.reason === "already-running");
3123
+ if (started.length) {
3124
+ for (const item of started) {
3125
+ console.log(`Started ${item.service} (pid ${item.pid})`);
3126
+ }
3127
+ }
3128
+ if (alreadyRunning.length) {
3129
+ for (const item of alreadyRunning) {
3130
+ console.log(`${item.service} already running (pid ${item.pid})`);
3131
+ }
3132
+ }
3133
+ }
3134
+
3135
+ async function runServicesDown({ asJson = false } = {}) {
3136
+ const results = [];
3137
+ for (const service of ["telegram", "discord"]) {
3138
+ const result = await stopTrackedGateway(service, { io: console });
3139
+ await clearTrackedGatewayArtifacts(service);
3140
+ results.push({ service, ...result });
3141
+ }
3142
+ if (asJson) {
3143
+ printData({ services: results }, true);
3144
+ return;
3145
+ }
3146
+ for (const result of results) {
3147
+ console.log(result.stopped ? `Stopped ${result.service} (pid ${result.pid})` : `No running ${result.service} process found`);
3148
+ }
3149
+ }
3150
+
3151
+ async function runServicesRestart(runtime, { cwd = process.cwd(), asJson = false } = {}) {
3152
+ await runServicesDown({ asJson: false });
3153
+ await sleep(250);
3154
+ await runServicesUp(runtime, { cwd, asJson: false });
3155
+ if (asJson) {
3156
+ await runProcessStatusCommand(runtime, { asJson: true });
3157
+ }
3158
+ }
3159
+
3029
3160
  function createTelegramBridgeHostRecord({ sessionId, cwd, label = "", surface = "live-tui", ownerId = "", ownerName = "", provider = "", model = "", runtimeProfile = "" }) {
3030
3161
  const now = new Date().toISOString();
3031
3162
  return {
@@ -4076,7 +4207,9 @@ async function runDiscordCommand(positional, runtime, { asJson = false } = {}) {
4076
4207
  console.log("Discord gateway");
4077
4208
  console.log("");
4078
4209
  console.log("Connecting to Discord Gateway. Press Ctrl+C to stop.");
4079
- await runDiscordGateway(runtime, { log: (line) => console.log(line) });
4210
+ await withTrackedGatewayProcess("discord", "discord run", async () => {
4211
+ await runDiscordGateway(runtime, { log: (line) => console.log(line) });
4212
+ });
4080
4213
  }
4081
4214
 
4082
4215
  async function runChannelsCommand(positional, runtime, { asJson = false } = {}) {
@@ -4227,11 +4360,13 @@ async function runGatewayCommand(positional, runtime, { cwd = process.cwd(), asJ
4227
4360
  if (runtimeRequiresApiKey(runtime) && (!runtime.apiKey || isLikelyPlaceholderApiKey(runtime.apiKey))) {
4228
4361
  throw new Error("Missing provider API key. Run `waterbrother onboarding` or set one with `waterbrother config set apiKey ...`.");
4229
4362
  }
4230
- await runGatewayService({
4231
- service,
4232
- runtime,
4233
- cwd,
4234
- io: console
4363
+ await withTrackedGatewayProcess(service, `gateway run ${service}`, async () => {
4364
+ await runGatewayService({
4365
+ service,
4366
+ runtime,
4367
+ cwd,
4368
+ io: console
4369
+ });
4235
4370
  });
4236
4371
  return;
4237
4372
  }
@@ -11360,6 +11495,26 @@ export async function runCli(argv) {
11360
11495
  return;
11361
11496
  }
11362
11497
 
11498
+ if (command === "ps") {
11499
+ await runProcessStatusCommand(runtime, { asJson });
11500
+ return;
11501
+ }
11502
+
11503
+ if (command === "up") {
11504
+ await runServicesUp(runtime, { cwd: startupCwd, asJson });
11505
+ return;
11506
+ }
11507
+
11508
+ if (command === "down") {
11509
+ await runServicesDown({ asJson });
11510
+ return;
11511
+ }
11512
+
11513
+ if (command === "restart") {
11514
+ await runServicesRestart(runtime, { cwd: startupCwd, asJson });
11515
+ return;
11516
+ }
11517
+
11363
11518
  if (command === "models") {
11364
11519
  await runModelsCommand(positional, runtime, { asJson });
11365
11520
  return;
package/src/discord.js CHANGED
@@ -1,6 +1,7 @@
1
1
  import fs from "node:fs/promises";
2
2
  import os from "node:os";
3
3
  import path from "node:path";
4
+ import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState } from "./self-awareness.js";
4
5
  import { createSession, listSessions, loadSession, saveSession } from "./session-store.js";
5
6
  import { loadGatewayBridge, loadGatewayState, saveGatewayBridge, saveGatewayState } from "./gateway-state.js";
6
7
 
@@ -113,7 +114,8 @@ function shouldReplyToMessage(message, botUserId) {
113
114
  if (!content) return false;
114
115
  const dm = !message.guild_id;
115
116
  const mentioned = botUserId ? content.includes(`<@${botUserId}>`) || content.includes(`<@!${botUserId}>`) : false;
116
- return dm || mentioned;
117
+ const guildCommand = !dm && content.startsWith("/");
118
+ return dm || mentioned || guildCommand;
117
119
  }
118
120
 
119
121
  function buildReply(message, botUserId) {
@@ -122,15 +124,32 @@ function buildReply(message, botUserId) {
122
124
  if (!content || content === "ping") {
123
125
  return "pong";
124
126
  }
125
- if (normalized === "status" || normalized === "online" || normalized === "are you online") {
126
- return "Discord gateway is online. I can see messages and basic mentions. Full conversational Discord runtime is the next slice.";
127
- }
128
127
  if (["hello", "hi", "hey"].includes(normalized)) {
129
128
  return "Waterbrother is here. Discord setup is live at the gateway level; deeper room workflows come next.";
130
129
  }
131
130
  return null;
132
131
  }
133
132
 
133
+ function buildDiscordHelp() {
134
+ return [
135
+ "Waterbrother Discord control",
136
+ "",
137
+ "Basic commands",
138
+ "/help show this help",
139
+ "/about show Waterbrother identity and capabilities",
140
+ "/state show current Waterbrother self-awareness state",
141
+ "/status show Discord gateway and linked remote session status",
142
+ "",
143
+ "Remote session commands",
144
+ "/new start a fresh remote session",
145
+ "/sessions list recent linked remote sessions",
146
+ "/resume <session-id> switch the linked remote session",
147
+ "/clear clear the current remote conversation history",
148
+ "",
149
+ "Use DMs or mention @Waterbrother in a server channel to run work through the live TUI."
150
+ ].join("\n");
151
+ }
152
+
134
153
  function continuationKey(channelId, userId) {
135
154
  return `${String(channelId || "").trim()}:${String(userId || "").trim()}`;
136
155
  }
@@ -409,6 +428,66 @@ async function handleDiscordSessionCommand(runtime, state, message, command) {
409
428
  return null;
410
429
  }
411
430
 
431
+ async function buildDiscordAbout(runtime, state, message) {
432
+ const actor = describeDiscordUser(message);
433
+ const sessionId = getPeerState(state, actor.userId)?.sessionId || "";
434
+ const currentSession = sessionId ? await loadSession(sessionId).catch(() => null) : null;
435
+ const manifest = await buildSelfAwarenessManifest({
436
+ cwd: currentSession?.cwd || process.cwd(),
437
+ runtime,
438
+ currentSession
439
+ });
440
+ return formatAboutWaterbrother(manifest);
441
+ }
442
+
443
+ async function buildDiscordState(runtime, state, message) {
444
+ const actor = describeDiscordUser(message);
445
+ const sessionId = getPeerState(state, actor.userId)?.sessionId || "";
446
+ const currentSession = sessionId ? await loadSession(sessionId).catch(() => null) : null;
447
+ const manifest = await buildSelfAwarenessManifest({
448
+ cwd: currentSession?.cwd || process.cwd(),
449
+ runtime,
450
+ currentSession
451
+ });
452
+ return formatSelfState(manifest);
453
+ }
454
+
455
+ async function buildDiscordStatusMessage(runtime, state, message) {
456
+ const discord = normalizeDiscordRuntime(runtime);
457
+ const actor = describeDiscordUser(message);
458
+ const peer = getPeerState(state, actor.userId);
459
+ const sessionId = String(peer?.sessionId || "").trim();
460
+ const session = sessionId ? await loadSession(sessionId).catch(() => null) : null;
461
+ const liveHost = await getLiveBridgeHost({ cwd: session?.cwd || "" });
462
+ return [
463
+ "Discord status",
464
+ `gateway: ${discord.enabled && discord.token && discord.applicationId ? "configured" : "incomplete"}`,
465
+ `application id: ${discord.applicationId || "missing"}`,
466
+ `linked session: ${sessionId || "none"}`,
467
+ session?.cwd ? `cwd: ${session.cwd}` : "",
468
+ session?.runtimeProfile ? `runtime profile: ${session.runtimeProfile}` : "",
469
+ liveHost ? `live terminal: ${String(liveHost.label || liveHost.ownerName || liveHost.sessionId || "connected").trim()}` : "live terminal: not connected"
470
+ ].filter(Boolean).join("\n");
471
+ }
472
+
473
+ async function handleDiscordControlCommand(runtime, state, message, rawText) {
474
+ const normalized = String(rawText || "").replace(/\s+/g, " ").trim().toLowerCase();
475
+ if (!normalized) return null;
476
+ if (normalized === "/help" || normalized === "help") {
477
+ return buildDiscordHelp();
478
+ }
479
+ if (normalized === "/about" || normalized === "about") {
480
+ return buildDiscordAbout(runtime, state, message);
481
+ }
482
+ if (normalized === "/state" || normalized === "state") {
483
+ return buildDiscordState(runtime, state, message);
484
+ }
485
+ if (normalized === "/status" || normalized === "status" || normalized === "online" || normalized === "are you online") {
486
+ return buildDiscordStatusMessage(runtime, state, message);
487
+ }
488
+ return null;
489
+ }
490
+
412
491
  async function getLiveBridgeHost({ cwd = "" } = {}) {
413
492
  const bridge = await loadGatewayBridge("discord");
414
493
  const hosts = Array.isArray(bridge.hosts) ? bridge.hosts : [];
@@ -488,7 +567,7 @@ async function runPromptViaBridge(runtime, message, promptText, options = {}) {
488
567
  nextBridge.deliveredReplies.splice(replyIndex, 1);
489
568
  await saveGatewayBridge("discord", nextBridge);
490
569
  if (reply.error) {
491
- return { error: String(reply.error || "").trim() || "Discord bridge execution failed." };
570
+ return { error: String(reply.error || "").trim() || "Discord bridge execution failed." };
492
571
  }
493
572
  return {
494
573
  content: String(reply.content || "").trim() || "(no content)",
@@ -651,6 +730,12 @@ export async function runDiscordGateway(runtime = {}, { log = console.log, signa
651
730
  }
652
731
  if (shouldReply) {
653
732
  const state = await loadDiscordGatewayState();
733
+ const controlReply = await handleDiscordControlCommand(runtime, state, msg, rawText);
734
+ if (controlReply) {
735
+ await sendChannelMessage(discord, msg.channel_id, controlReply);
736
+ log(`discord: control reply in ${msg.channel_id}`);
737
+ return;
738
+ }
654
739
  const command = parseDiscordSessionCommand(rawText);
655
740
  if (command) {
656
741
  const commandReply = await handleDiscordSessionCommand(runtime, state, msg, command);
package/src/gateway.js CHANGED
@@ -4914,10 +4914,14 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
4914
4914
  });
4915
4915
  return;
4916
4916
  }
4917
+ const targetBridgeHost = selectedLiveHost || host || null;
4918
+ if (!targetBridgeHost) {
4919
+ throw new Error("No live Waterbrother TUI is connected. Start Waterbrother in the terminal first, then retry.");
4920
+ }
4917
4921
  previewMessage = await this.sendProgressMessage(message.chat.id, message.message_id);
4918
4922
  const result = (await this.runPromptViaBridge(message, sessionId, promptText, {
4919
4923
  explicitExecution: shouldExecutePrompt,
4920
- targetHost: selectedLiveHost || host || null,
4924
+ targetHost: targetBridgeHost,
4921
4925
  includeSource: true
4922
4926
  }))
4923
4927
  ?? (await this.runPromptFallback(sessionId, promptText));