@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 +1 -1
- package/src/cli.js +161 -6
- package/src/discord.js +90 -5
- package/src/gateway.js +5 -1
package/package.json
CHANGED
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
|
|
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
|
|
4231
|
-
|
|
4232
|
-
|
|
4233
|
-
|
|
4234
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
4924
|
+
targetHost: targetBridgeHost,
|
|
4921
4925
|
includeSource: true
|
|
4922
4926
|
}))
|
|
4923
4927
|
?? (await this.runPromptFallback(sessionId, promptText));
|