@tritard/waterbrother 0.16.51 → 0.16.53

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/gateway.js +135 -22
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.51",
3
+ "version": "0.16.53",
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/gateway.js CHANGED
@@ -270,19 +270,28 @@ function formatGatewaySessionStatus({ sessionId, userId, username, cwd, runtimeP
270
270
  return bits.join("\n");
271
271
  }
272
272
 
273
- function formatTelegramProjectMarkup({ cwd, project, chatId = "", title = "" }) {
273
+ function formatTelegramSummaryMarkup({ cwd, project, chatId = "", title = "", executor = {} }) {
274
+ const roomLabel = project?.room?.chatId
275
+ ? `${project.room.provider || "telegram"} ${project.room.chatId}${project.room.title ? ` (${project.room.title})` : ""}`
276
+ : "not linked";
277
+ const activeOperator = project?.activeOperator?.id
278
+ ? `${project.activeOperator.name || project.activeOperator.id} (${project.activeOperator.id})`
279
+ : "none";
274
280
  const lines = [
275
- "<b>Linked project</b>",
281
+ "<b>Project summary</b>",
282
+ `project: <code>${escapeTelegramHtml(project?.projectName || path.basename(cwd || "") || "-")}</code>`,
276
283
  `cwd: <code>${escapeTelegramHtml(cwd || "-")}</code>`,
277
- `name: <code>${escapeTelegramHtml(project?.projectName || path.basename(cwd || "") || "-")}</code>`,
278
284
  `shared: <code>${project?.enabled ? "yes" : "no"}</code>`
279
285
  ];
280
286
  if (project?.enabled) {
281
287
  lines.push(`room mode: <code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`);
282
- lines.push(
283
- `bound chat: <code>${escapeTelegramHtml(project.room?.provider === "telegram" && project.room?.chatId ? `${project.room.provider} ${project.room.chatId}` : "none")}</code>`
284
- );
288
+ lines.push(`bound chat: <code>${escapeTelegramHtml(roomLabel)}</code>`);
289
+ lines.push(`active operator: <code>${escapeTelegramHtml(activeOperator)}</code>`);
285
290
  lines.push(`members: <code>${escapeTelegramHtml(String((project.members || []).length))}</code>`);
291
+ if (executor?.surface) lines.push(`executor: <code>${escapeTelegramHtml(executor.surface)}</code>`);
292
+ if (executor?.provider && executor?.model) {
293
+ lines.push(`runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>`);
294
+ }
286
295
  }
287
296
  if (!project?.enabled) {
288
297
  lines.push("Use <code>/project share</code> to turn on Roundtable for this project in this chat.");
@@ -292,6 +301,10 @@ function formatTelegramProjectMarkup({ cwd, project, chatId = "", title = "" })
292
301
  return lines.join("\n");
293
302
  }
294
303
 
304
+ function formatTelegramProjectMarkup({ cwd, project, chatId = "", title = "" }) {
305
+ return formatTelegramSummaryMarkup({ cwd, project, chatId, title });
306
+ }
307
+
295
308
  function normalizeTelegramProjectIntentText(text = "") {
296
309
  return String(text || "").trim().replace(/\s+/g, " ");
297
310
  }
@@ -519,6 +532,31 @@ function formatTelegramRoomMarkup(project, options = {}) {
519
532
  ].join("\n");
520
533
  }
521
534
 
535
+ function formatOwnerActionMarkup({
536
+ title = "Roundtable update",
537
+ projectName = "",
538
+ cwd = "",
539
+ paired = null,
540
+ member = null,
541
+ role = "",
542
+ targetName = "",
543
+ targetId = "",
544
+ note = ""
545
+ } = {}) {
546
+ const lines = [
547
+ `<b>${escapeTelegramHtml(title)}</b>`,
548
+ targetName || targetId ? `person: <code>${escapeTelegramHtml(targetName || targetId)}</code>` : "",
549
+ targetId ? `user id: <code>${escapeTelegramHtml(targetId)}</code>` : "",
550
+ paired === null ? "" : `paired: <code>${paired ? "yes" : "no"}</code>`,
551
+ member === null ? "" : `project member: <code>${member ? "yes" : "no"}</code>`,
552
+ role ? `role: <code>${escapeTelegramHtml(role)}</code>` : "",
553
+ projectName ? `project: <code>${escapeTelegramHtml(projectName)}</code>` : "",
554
+ cwd ? `cwd: <code>${escapeTelegramHtml(cwd)}</code>` : "",
555
+ note ? escapeTelegramHtml(note) : ""
556
+ ].filter(Boolean);
557
+ return lines.join("\n");
558
+ }
559
+
522
560
  function formatTelegramMembersMarkup(project) {
523
561
  if (!project?.enabled) {
524
562
  return "This project is not shared.";
@@ -1005,6 +1043,15 @@ class TelegramGateway {
1005
1043
  if (!value) {
1006
1044
  throw new Error("member target is required");
1007
1045
  }
1046
+ const replied = this.describeTelegramUser(message?.reply_to_message?.from || {});
1047
+ const normalizedPronoun = value.toLowerCase().replace(/[?.!]+$/g, "").trim();
1048
+ if (["him", "her", "them", "this person", "this user", "that person", "that user"].includes(normalizedPronoun)) {
1049
+ if (replied.userId) {
1050
+ const knownReply = this.listKnownChatPeople(message).find((person) => person.userId === replied.userId) || replied;
1051
+ return { userId: replied.userId, displayName: knownReply.displayName || replied.displayName || replied.userId };
1052
+ }
1053
+ throw new Error("Reply to the person's message first, or use /people to choose them by name or id.");
1054
+ }
1008
1055
  if (/^\d+$/.test(value) || value.startsWith("@")) {
1009
1056
  const match = this.resolveInviteTarget(message, value);
1010
1057
  return { userId: match.userId, displayName: match.displayName };
@@ -1016,6 +1063,17 @@ class TelegramGateway {
1016
1063
  if (exact) {
1017
1064
  return { userId: exact.userId, displayName: exact.displayName || exact.userId };
1018
1065
  }
1066
+ const byFirstName = known.filter((person) => {
1067
+ const display = String(person.displayName || "").trim().toLowerCase();
1068
+ const first = display.split(/\s+/).filter(Boolean)[0] || "";
1069
+ return first === normalized;
1070
+ });
1071
+ if (byFirstName.length === 1) {
1072
+ return { userId: byFirstName[0].userId, displayName: byFirstName[0].displayName || byFirstName[0].userId };
1073
+ }
1074
+ if (byFirstName.length > 1) {
1075
+ throw new Error(`Multiple Telegram users matched first name ${value}. Use /people and choose by id or full name.`);
1076
+ }
1019
1077
  const partial = known.filter((person) => {
1020
1078
  const display = String(person.displayName || "").trim().toLowerCase();
1021
1079
  const handle = String(person.usernameHandle || "").trim().toLowerCase();
@@ -1050,7 +1108,16 @@ class TelegramGateway {
1050
1108
  return {
1051
1109
  kind: "member",
1052
1110
  project: nextProject,
1053
- markup: `Updated <code>${escapeTelegramHtml(target.displayName || target.userId)}</code> to <code>${escapeTelegramHtml(intent.role)}</code> in this shared project.`
1111
+ markup: formatOwnerActionMarkup({
1112
+ title: "Roundtable member updated",
1113
+ projectName: nextProject.projectName || path.basename(session.cwd || this.cwd),
1114
+ cwd: session.cwd || this.cwd,
1115
+ paired: true,
1116
+ member: true,
1117
+ role: intent.role,
1118
+ targetName: target.displayName || target.userId,
1119
+ targetId: target.userId
1120
+ })
1054
1121
  };
1055
1122
  }
1056
1123
  const known = this.listKnownChatPeople(message);
@@ -1068,7 +1135,16 @@ class TelegramGateway {
1068
1135
  return {
1069
1136
  kind: "member",
1070
1137
  project: nextProject,
1071
- markup: `${pairedNow ? `Paired <code>${escapeTelegramHtml(target.displayName || target.userId)}</code> <i>(${escapeTelegramHtml(target.userId)})</i> and ` : ""}added <code>${escapeTelegramHtml(target.displayName || target.userId)}</code> to this shared project as <code>${escapeTelegramHtml(intent.role)}</code>.`
1138
+ markup: formatOwnerActionMarkup({
1139
+ title: pairedNow ? "Paired and added to Roundtable" : "Added to Roundtable",
1140
+ projectName: nextProject.projectName || path.basename(session.cwd || this.cwd),
1141
+ cwd: session.cwd || this.cwd,
1142
+ paired: true,
1143
+ member: true,
1144
+ role: intent.role,
1145
+ targetName: target.displayName || target.userId,
1146
+ targetId: target.userId
1147
+ })
1072
1148
  };
1073
1149
  }
1074
1150
 
@@ -1078,7 +1154,7 @@ class TelegramGateway {
1078
1154
  const known = this.listKnownChatPeople(message);
1079
1155
 
1080
1156
  if (intent.action === "project-status") {
1081
- return formatTelegramProjectMarkup({
1157
+ return formatTelegramSummaryMarkup({
1082
1158
  cwd,
1083
1159
  project,
1084
1160
  chatId: String(message.chat.id),
@@ -1454,7 +1530,15 @@ class TelegramGateway {
1454
1530
  runtimeProfile: project?.runtimeProfile || this.channel.defaultRuntimeProfile || this.gateway.defaultRuntimeProfile || "",
1455
1531
  hostSessionId: host?.sessionId || ""
1456
1532
  };
1457
- return project?.enabled ? formatTelegramRoomMarkup(project, { executor }) : "<b>Shared room</b>\nThis project is not shared.";
1533
+ return project?.enabled
1534
+ ? `${formatTelegramSummaryMarkup({
1535
+ cwd: project.cwd || this.cwd,
1536
+ project,
1537
+ chatId: String(message.chat.id),
1538
+ title: String(message.chat.title || "").trim(),
1539
+ executor
1540
+ })}\n\n${formatTelegramRoomMarkup(project, { executor })}`
1541
+ : "<b>Shared room</b>\nThis project is not shared.";
1458
1542
  }
1459
1543
 
1460
1544
  async ensureSharedOperator(message, sessionId) {
@@ -1776,9 +1860,18 @@ class TelegramGateway {
1776
1860
  }
1777
1861
  try {
1778
1862
  const result = await this.pairKnownTelegramUser(message, rawTarget);
1863
+ const pairSession = await loadSession(sessionId);
1779
1864
  await this.sendMarkup(
1780
1865
  message.chat.id,
1781
- `Paired <code>${escapeTelegramHtml(result.displayName)}</code> <i>(${escapeTelegramHtml(result.userId)})</i> via <code>${escapeTelegramHtml(result.configPath)}</code>.`,
1866
+ formatOwnerActionMarkup({
1867
+ title: "Telegram user paired",
1868
+ projectName: path.basename(pairSession.cwd || this.cwd),
1869
+ cwd: pairSession.cwd || this.cwd,
1870
+ paired: true,
1871
+ targetName: result.displayName,
1872
+ targetId: result.userId,
1873
+ note: `via ${result.configPath}`
1874
+ }),
1782
1875
  message.message_id
1783
1876
  );
1784
1877
  } catch (error) {
@@ -1789,13 +1882,19 @@ class TelegramGateway {
1789
1882
 
1790
1883
  if (text === "/project") {
1791
1884
  const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1885
+ const host = await this.getLiveBridgeHost();
1792
1886
  await this.sendMarkup(
1793
1887
  message.chat.id,
1794
- formatTelegramProjectMarkup({
1888
+ formatTelegramSummaryMarkup({
1795
1889
  cwd: session.cwd || this.cwd,
1796
1890
  project,
1797
1891
  chatId: String(message.chat.id),
1798
- title: String(message.chat.title || "").trim()
1892
+ title: String(message.chat.title || "").trim(),
1893
+ executor: {
1894
+ surface: host ? "live-tui" : "telegram-fallback",
1895
+ provider: this.runtime.provider,
1896
+ model: this.runtime.model
1897
+ }
1799
1898
  }),
1800
1899
  message.message_id
1801
1900
  );
@@ -1807,7 +1906,7 @@ class TelegramGateway {
1807
1906
  const { session, project } = await this.ensureProjectSharedForChat(message, sessionId);
1808
1907
  await this.sendMarkup(
1809
1908
  message.chat.id,
1810
- `${formatTelegramProjectMarkup({
1909
+ `${formatTelegramSummaryMarkup({
1811
1910
  cwd: session.cwd || this.cwd,
1812
1911
  project,
1813
1912
  chatId: String(message.chat.id),
@@ -1831,7 +1930,7 @@ class TelegramGateway {
1831
1930
  const result = await this.switchPeerProject(message, rawPath);
1832
1931
  await this.sendMarkup(
1833
1932
  message.chat.id,
1834
- `${formatTelegramProjectMarkup({
1933
+ `${formatTelegramSummaryMarkup({
1835
1934
  cwd: result.cwd,
1836
1935
  project: result.project,
1837
1936
  chatId: String(message.chat.id),
@@ -1855,7 +1954,7 @@ class TelegramGateway {
1855
1954
  const result = await this.switchPeerProject(message, projectName, { create: true });
1856
1955
  await this.sendMarkup(
1857
1956
  message.chat.id,
1858
- `${formatTelegramProjectMarkup({
1957
+ `${formatTelegramSummaryMarkup({
1859
1958
  cwd: result.cwd,
1860
1959
  project: result.project,
1861
1960
  chatId: String(message.chat.id),
@@ -2425,7 +2524,15 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
2425
2524
  const result = await this.pairKnownTelegramUser(message, pairingIntent.target);
2426
2525
  await this.sendMarkup(
2427
2526
  message.chat.id,
2428
- `Paired <code>${escapeTelegramHtml(result.displayName)}</code> <i>(${escapeTelegramHtml(result.userId)})</i> via <code>${escapeTelegramHtml(result.configPath)}</code>.`,
2527
+ formatOwnerActionMarkup({
2528
+ title: "Telegram user paired",
2529
+ projectName: sharedBinding.project?.projectName || path.basename(sharedBinding.session.cwd || this.cwd),
2530
+ cwd: sharedBinding.session.cwd || this.cwd,
2531
+ paired: true,
2532
+ targetName: result.displayName,
2533
+ targetId: result.userId,
2534
+ note: `via ${result.configPath}`
2535
+ }),
2429
2536
  message.message_id
2430
2537
  );
2431
2538
  } catch (error) {
@@ -2445,13 +2552,19 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
2445
2552
  }
2446
2553
  const projectIntent = parseTelegramProjectIntent(promptText);
2447
2554
  if (projectIntent?.action === "show-project") {
2555
+ const host = await this.getLiveBridgeHost();
2448
2556
  await this.sendMarkup(
2449
2557
  message.chat.id,
2450
- formatTelegramProjectMarkup({
2558
+ formatTelegramSummaryMarkup({
2451
2559
  cwd: sharedBinding.session.cwd || this.cwd,
2452
2560
  project: sharedBinding.project,
2453
2561
  chatId: String(message.chat.id),
2454
- title: String(message.chat.title || "").trim()
2562
+ title: String(message.chat.title || "").trim(),
2563
+ executor: {
2564
+ surface: host ? "live-tui" : "telegram-fallback",
2565
+ provider: this.runtime.provider,
2566
+ model: this.runtime.model
2567
+ }
2455
2568
  }),
2456
2569
  message.message_id
2457
2570
  );
@@ -2462,7 +2575,7 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
2462
2575
  const { session, project } = await this.ensureProjectSharedForChat(message, sessionId);
2463
2576
  await this.sendMarkup(
2464
2577
  message.chat.id,
2465
- `${formatTelegramProjectMarkup({
2578
+ `${formatTelegramSummaryMarkup({
2466
2579
  cwd: session.cwd || this.cwd,
2467
2580
  project,
2468
2581
  chatId: String(message.chat.id),
@@ -2480,7 +2593,7 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
2480
2593
  const result = await this.switchPeerProject(message, projectIntent.target);
2481
2594
  await this.sendMarkup(
2482
2595
  message.chat.id,
2483
- `${formatTelegramProjectMarkup({
2596
+ `${formatTelegramSummaryMarkup({
2484
2597
  cwd: result.cwd,
2485
2598
  project: result.project,
2486
2599
  chatId: String(message.chat.id),
@@ -2498,7 +2611,7 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
2498
2611
  const result = await this.switchPeerProject(message, projectIntent.name, { create: true, share: projectIntent.share });
2499
2612
  await this.sendMarkup(
2500
2613
  message.chat.id,
2501
- `${formatTelegramProjectMarkup({
2614
+ `${formatTelegramSummaryMarkup({
2502
2615
  cwd: result.cwd,
2503
2616
  project: result.project,
2504
2617
  chatId: String(message.chat.id),