@tritard/waterbrother 0.16.77 → 0.16.78

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.77",
3
+ "version": "0.16.78",
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
@@ -3023,6 +3023,15 @@ function createTelegramBridgeHostRecord({ sessionId, cwd, label = "", surface =
3023
3023
  };
3024
3024
  }
3025
3025
 
3026
+ function upsertTelegramBridgeHostEntry(hosts = [], nextHost = {}) {
3027
+ const list = Array.isArray(hosts) ? [...hosts] : [];
3028
+ const pid = Number(nextHost?.pid || 0);
3029
+ if (!pid) return list;
3030
+ const filtered = list.filter((host) => Number(host?.pid || 0) !== pid);
3031
+ filtered.unshift(nextHost);
3032
+ return filtered.slice(0, 20);
3033
+ }
3034
+
3026
3035
  async function syncSharedTelegramBridgeAgent({ cwd, host = {}, actor = {} }) {
3027
3036
  try {
3028
3037
  const project = await loadSharedProject(cwd);
@@ -3052,6 +3061,7 @@ async function syncSharedTelegramBridgeAgent({ cwd, host = {}, actor = {} }) {
3052
3061
  async function registerTelegramBridgeHost({ sessionId, cwd, label = "", surface = "live-tui", ownerId = "", ownerName = "", provider = "", model = "", runtimeProfile = "", actor = null }) {
3053
3062
  const bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3054
3063
  bridge.activeHost = createTelegramBridgeHostRecord({ sessionId, cwd, label, surface, ownerId, ownerName, provider, model, runtimeProfile });
3064
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3055
3065
  bridge.deliveredReplies = Array.isArray(bridge.deliveredReplies) ? bridge.deliveredReplies.slice(-50) : [];
3056
3066
  bridge.pendingRequests = Array.isArray(bridge.pendingRequests) ? bridge.pendingRequests : [];
3057
3067
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
@@ -3079,6 +3089,7 @@ async function touchTelegramBridgeHost({ sessionId, cwd, label = "", surface = "
3079
3089
  updatedAt: new Date().toISOString()
3080
3090
  };
3081
3091
  }
3092
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3082
3093
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3083
3094
  await syncSharedTelegramBridgeAgent({ cwd, host: bridge.activeHost, actor: actor || { id: ownerId, name: ownerName } });
3084
3095
  return bridge.activeHost;
@@ -3088,8 +3099,9 @@ async function clearTelegramBridgeHost() {
3088
3099
  const bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3089
3100
  if (Number(bridge.activeHost?.pid || 0) === process.pid) {
3090
3101
  bridge.activeHost = { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" };
3091
- await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3092
3102
  }
3103
+ bridge.hosts = Array.isArray(bridge.hosts) ? bridge.hosts.filter((host) => Number(host?.pid || 0) !== process.pid) : [];
3104
+ await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3093
3105
  }
3094
3106
 
3095
3107
  async function dequeueTelegramBridgeRequest({ cwd }) {
@@ -3111,6 +3123,7 @@ async function dequeueTelegramBridgeRequest({ cwd }) {
3111
3123
  ...activeHost,
3112
3124
  updatedAt: new Date().toISOString()
3113
3125
  };
3126
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3114
3127
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3115
3128
  return next;
3116
3129
  }
@@ -3135,6 +3148,9 @@ async function deliverTelegramBridgeReply(request, { sessionId, content = "", er
3135
3148
  sessionId: String(sessionId || "").trim() || String(bridge.activeHost.sessionId || "")
3136
3149
  };
3137
3150
  }
3151
+ if (Number(bridge.activeHost?.pid || 0) === process.pid) {
3152
+ bridge.hosts = upsertTelegramBridgeHostEntry(bridge.hosts, bridge.activeHost);
3153
+ }
3138
3154
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3139
3155
  }
3140
3156
 
@@ -75,29 +75,39 @@ function normalizeBridgeReply(parsed = {}) {
75
75
  }
76
76
 
77
77
  function normalizeGatewayBridge(parsed = {}) {
78
+ const normalizeHost = (host = {}) => ({
79
+ pid: Number.isFinite(Number(host.pid)) ? Math.floor(Number(host.pid)) : 0,
80
+ sessionId: String(host.sessionId || "").trim(),
81
+ cwd: String(host.cwd || "").trim(),
82
+ label: String(host.label || "").trim(),
83
+ surface: String(host.surface || "").trim(),
84
+ ownerId: String(host.ownerId || "").trim(),
85
+ ownerName: String(host.ownerName || "").trim(),
86
+ provider: String(host.provider || "").trim(),
87
+ model: String(host.model || "").trim(),
88
+ runtimeProfile: String(host.runtimeProfile || "").trim(),
89
+ startedAt: String(host.startedAt || "").trim(),
90
+ updatedAt: String(host.updatedAt || "").trim()
91
+ });
78
92
  const pendingRequests = Array.isArray(parsed?.pendingRequests)
79
93
  ? parsed.pendingRequests.map(normalizeBridgeRequest).filter((item) => item.id)
80
94
  : [];
81
95
  const deliveredReplies = Array.isArray(parsed?.deliveredReplies)
82
96
  ? parsed.deliveredReplies.map(normalizeBridgeReply).filter((item) => item.requestId)
83
97
  : [];
98
+ const hosts = Array.isArray(parsed?.hosts)
99
+ ? parsed.hosts.map(normalizeHost).filter((host) => Number(host.pid || 0) > 0)
100
+ : [];
101
+ const activeHost = parsed?.activeHost && typeof parsed.activeHost === "object"
102
+ ? normalizeHost(parsed.activeHost)
103
+ : normalizeHost();
104
+ const mergedHosts = [...hosts];
105
+ if (Number(activeHost.pid || 0) > 0 && !mergedHosts.some((host) => Number(host.pid || 0) === Number(activeHost.pid || 0))) {
106
+ mergedHosts.unshift(activeHost);
107
+ }
84
108
  return {
85
- activeHost: parsed?.activeHost && typeof parsed.activeHost === "object"
86
- ? {
87
- pid: Number.isFinite(Number(parsed.activeHost.pid)) ? Math.floor(Number(parsed.activeHost.pid)) : 0,
88
- sessionId: String(parsed.activeHost.sessionId || "").trim(),
89
- cwd: String(parsed.activeHost.cwd || "").trim(),
90
- label: String(parsed.activeHost.label || "").trim(),
91
- surface: String(parsed.activeHost.surface || "").trim(),
92
- ownerId: String(parsed.activeHost.ownerId || "").trim(),
93
- ownerName: String(parsed.activeHost.ownerName || "").trim(),
94
- provider: String(parsed.activeHost.provider || "").trim(),
95
- model: String(parsed.activeHost.model || "").trim(),
96
- runtimeProfile: String(parsed.activeHost.runtimeProfile || "").trim(),
97
- startedAt: String(parsed.activeHost.startedAt || "").trim(),
98
- updatedAt: String(parsed.activeHost.updatedAt || "").trim()
99
- }
100
- : { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" },
109
+ activeHost,
110
+ hosts: mergedHosts.slice(-20),
101
111
  pendingRequests,
102
112
  deliveredReplies
103
113
  };
package/src/gateway.js CHANGED
@@ -466,6 +466,13 @@ function summarizeRuntimeConflict(project, fallbackExecutor = {}) {
466
466
  };
467
467
  }
468
468
 
469
+ function formatBridgeHostLabel(host = {}) {
470
+ const owner = String(host?.ownerName || host?.ownerId || "").trim();
471
+ const label = String(host?.label || "").trim();
472
+ const runtime = host?.provider && host?.model ? `${host.provider}/${host.model}` : "";
473
+ return [owner || label, label && label !== owner ? `(${label})` : "", runtime ? `[${runtime}]` : ""].filter(Boolean).join(" ").trim();
474
+ }
475
+
469
476
  function getLatestBlockingReviewPolicy(project) {
470
477
  const events = Array.isArray(project?.recentEvents) ? [...project.recentEvents] : [];
471
478
  const ordered = events
@@ -730,6 +737,9 @@ function parseTelegramStateIntent(text = "") {
730
737
  if (/\bwho are the bots\b/.test(lower) || /\bwho are the agents\b/.test(lower) || /\bwhat bots are here\b/.test(lower) || /\bwhat agents are here\b/.test(lower)) {
731
738
  return { action: "agent-list" };
732
739
  }
740
+ if (/\bwhich terminals are live\b/.test(lower) || /\bwhich bots are live\b/.test(lower) || /\bwho is live\b/.test(lower) || /\bwhat terminals are live\b/.test(lower)) {
741
+ return { action: "live-hosts" };
742
+ }
733
743
  if (/\bmodel conflict\b/.test(lower) || /\bruntime conflict\b/.test(lower) || /\bmodel split\b/.test(lower) || /\bruntime split\b/.test(lower) || /\bcompare models\b/.test(lower) || /\bcompare bots\b/.test(lower)) {
734
744
  return { action: "model-conflict" };
735
745
  }
@@ -854,6 +864,7 @@ function formatTelegramRoomMarkup(project, options = {}) {
854
864
  ? agents.map((agent) => `• ${escapeTelegramHtml(formatAgentLabel(agent) || agent.id || "agent")} <code>${escapeTelegramHtml(agent.surface || "unknown")}</code>`)
855
865
  : ["• none"];
856
866
  const runtimeConflict = summarizeRuntimeConflict(project, executor);
867
+ const liveHosts = Array.isArray(options.liveHosts) ? options.liveHosts : [];
857
868
  const executorBits = [
858
869
  `surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
859
870
  `provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
@@ -888,6 +899,11 @@ function formatTelegramRoomMarkup(project, options = {}) {
888
899
  .filter(Boolean);
889
900
  return `• <code>${escapeTelegramHtml(group.runtime)}</code>${owners.length ? ` — ${escapeTelegramHtml(owners.join(", "))}` : ""}`;
890
901
  }),
902
+ "<b>Live Terminals</b>",
903
+ `count: <code>${escapeTelegramHtml(String(liveHosts.length))}</code>`,
904
+ ...(liveHosts.length
905
+ ? liveHosts.map((host) => `• ${escapeTelegramHtml(formatBridgeHostLabel(host) || host.sessionId || "terminal")} <code>${escapeTelegramHtml(host.surface || "live-tui")}</code>`)
906
+ : ["• none"]),
891
907
  "<b>Task Summary</b>",
892
908
  `total: <code>${tasks.length}</code>`,
893
909
  ...[...byState.entries()].map(([state, count]) => `${escapeTelegramHtml(state)}: <code>${count}</code>`),
@@ -2067,6 +2083,20 @@ class TelegramGateway {
2067
2083
  ].join("\n");
2068
2084
  }
2069
2085
 
2086
+ if (intent.action === "live-hosts") {
2087
+ const hosts = await this.getLiveBridgeHosts({ cwd });
2088
+ if (!hosts.length) {
2089
+ return "<b>Live terminals</b>\n• none";
2090
+ }
2091
+ return [
2092
+ "<b>Live terminals</b>",
2093
+ ...hosts.map((host) => {
2094
+ const roleAgent = listProjectAgents(project).find((agent) => String(agent?.sessionId || "").trim() === String(host?.sessionId || "").trim()) || null;
2095
+ return `• ${escapeTelegramHtml(formatBridgeHostLabel(host) || host.sessionId || "terminal")} <i>(${escapeTelegramHtml(roleAgent?.role || "live")})</i> <code>${escapeTelegramHtml(host.surface || "live-tui")}</code>`;
2096
+ })
2097
+ ].join("\n");
2098
+ }
2099
+
2070
2100
  if (intent.action === "model-conflict") {
2071
2101
  if (!project?.enabled) {
2072
2102
  return "This project is not shared.";
@@ -2141,12 +2171,20 @@ class TelegramGateway {
2141
2171
  const operatorLabel = project.activeOperator?.id
2142
2172
  ? `${project.activeOperator.name || project.activeOperator.id}`
2143
2173
  : "none";
2174
+ const liveHosts = await this.getLiveBridgeHosts({ cwd });
2175
+ const isLive = liveHosts.some((host) => {
2176
+ const ownerId = String(host?.ownerId || "").trim();
2177
+ const sessionId = String(host?.sessionId || "").trim();
2178
+ return (ownerId && ownerId === String(agent.ownerId || "").trim())
2179
+ || (sessionId && sessionId === String(agent.sessionId || "").trim());
2180
+ });
2144
2181
  return [
2145
2182
  "<b>Execution recommendation</b>",
2146
2183
  `take this on: <code>${escapeTelegramHtml(agent.ownerName || agent.label || agent.ownerId || agent.id || "unknown")}</code>`,
2147
2184
  `role: <code>${escapeTelegramHtml(agent.role || "executor")}</code>`,
2148
2185
  `surface: <code>${escapeTelegramHtml(agent.surface || "unknown")}</code>`,
2149
2186
  `runtime: <code>${escapeTelegramHtml(agent.provider && agent.model ? `${agent.provider}/${agent.model}` : "unknown")}</code>`,
2187
+ `live now: <code>${escapeTelegramHtml(isLive ? "yes" : "no")}</code>`,
2150
2188
  `active operator: <code>${escapeTelegramHtml(operatorLabel)}</code>`
2151
2189
  ].join("\n");
2152
2190
  }
@@ -2493,6 +2531,7 @@ class TelegramGateway {
2493
2531
  async buildRoomMarkup(message, sessionId) {
2494
2532
  const { project } = await this.bindSharedRoomForMessage(message, sessionId);
2495
2533
  const host = await this.getLiveBridgeHost();
2534
+ const liveHosts = await this.getLiveBridgeHosts({ cwd: project?.cwd || this.cwd });
2496
2535
  const executor = {
2497
2536
  surface: host?.surface || (host ? "live-tui" : "telegram-fallback"),
2498
2537
  provider: host?.provider || this.runtime.provider,
@@ -2504,10 +2543,10 @@ class TelegramGateway {
2504
2543
  ? `${formatTelegramSummaryMarkup({
2505
2544
  cwd: project.cwd || this.cwd,
2506
2545
  project,
2507
- chatId: String(message.chat.id),
2508
- title: String(message.chat.title || "").trim(),
2509
- executor
2510
- })}\n\n${formatTelegramRoomMarkup(project, { executor })}`
2546
+ chatId: String(message.chat.id),
2547
+ title: String(message.chat.title || "").trim(),
2548
+ executor
2549
+ })}\n\n${formatTelegramRoomMarkup(project, { executor, liveHosts })}`
2511
2550
  : "<b>Shared room</b>\nThis project is not shared.";
2512
2551
  }
2513
2552
 
@@ -2558,6 +2597,39 @@ class TelegramGateway {
2558
2597
  return host;
2559
2598
  }
2560
2599
 
2600
+ async getLiveBridgeHosts({ cwd = "" } = {}) {
2601
+ const bridge = await loadGatewayBridge("telegram");
2602
+ const hosts = Array.isArray(bridge.hosts) ? bridge.hosts : [];
2603
+ const nextHosts = [];
2604
+ let changed = false;
2605
+ for (const host of hosts) {
2606
+ const pid = Number(host?.pid || 0);
2607
+ if (!Number.isFinite(pid) || pid <= 0) {
2608
+ changed = true;
2609
+ continue;
2610
+ }
2611
+ try {
2612
+ process.kill(pid, 0);
2613
+ } catch {
2614
+ changed = true;
2615
+ continue;
2616
+ }
2617
+ if (cwd && host.cwd && String(host.cwd) !== String(cwd || "")) {
2618
+ continue;
2619
+ }
2620
+ nextHosts.push(host);
2621
+ }
2622
+ if (changed || nextHosts.length !== hosts.length) {
2623
+ bridge.hosts = nextHosts;
2624
+ const activePid = Number(bridge.activeHost?.pid || 0);
2625
+ if (activePid > 0 && !nextHosts.some((host) => Number(host?.pid || 0) === activePid)) {
2626
+ bridge.activeHost = { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" };
2627
+ }
2628
+ await saveGatewayBridge("telegram", bridge);
2629
+ }
2630
+ return nextHosts;
2631
+ }
2632
+
2561
2633
  async runPromptViaBridge(message, sessionId, promptText, options = {}) {
2562
2634
  const host = await this.getLiveBridgeHost();
2563
2635
  if (!host) {