@tritard/waterbrother 0.16.69 → 0.16.71

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 +161 -4
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.69",
3
+ "version": "0.16.71",
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
@@ -272,6 +272,68 @@ function buildRemoteHelp() {
272
272
  ].join("\n");
273
273
  }
274
274
 
275
+ function buildTelegramDmOnboardingMarkup({ cwd = "", project = null, peer = null } = {}) {
276
+ const activeProject = project?.projectName || (cwd ? path.basename(cwd) : "");
277
+ return [
278
+ "<b>Waterbrother on Telegram</b>",
279
+ "You can talk to me here much like the TUI. Ask questions, make changes, refine work, or share a project into a room.",
280
+ activeProject ? `current project: <code>${escapeTelegramHtml(activeProject)}</code>` : "",
281
+ cwd ? `cwd: <code>${escapeTelegramHtml(cwd)}</code>` : "",
282
+ peer?.sessionId ? `linked session: <code>${escapeTelegramHtml(peer.sessionId)}</code>` : "",
283
+ "",
284
+ "<b>Good first moves</b>",
285
+ "• ask <code>what project are you in?</code>",
286
+ "• say <code>make a test page in this project</code>",
287
+ "• say <code>share this project in my Telegram room</code>",
288
+ "",
289
+ "<b>Need the full command list?</b>",
290
+ "Use <code>/help</code>."
291
+ ].filter(Boolean).join("\n");
292
+ }
293
+
294
+ function buildTelegramRoomOnboardingMarkup({ project = null, actorName = "", executor = {} } = {}) {
295
+ const participants = listProjectParticipants(project);
296
+ const agents = listProjectAgents(project);
297
+ return [
298
+ "<b>Roundtable room</b>",
299
+ actorName ? `${escapeTelegramHtml(actorName)} is talking to Waterbrother in the shared room for <code>${escapeTelegramHtml(project?.projectName || "this project")}</code>.` : "",
300
+ `room mode: <code>${escapeTelegramHtml(project?.roomMode || "chat")}</code>`,
301
+ `participants: <code>${escapeTelegramHtml(String(participants.length || (project?.members || []).length || 0))}</code>`,
302
+ `agents: <code>${escapeTelegramHtml(String(agents.length || 0))}</code>`,
303
+ executor?.provider && executor?.model ? `active runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>` : "",
304
+ "",
305
+ "<b>What you can do here</b>",
306
+ "• ask <code>what project is this chat bound to?</code>",
307
+ "• ask <code>who is in the room?</code>",
308
+ "• say <code>add Austin as editor</code>",
309
+ "• in execute mode, address Waterbrother directly to build or change code",
310
+ "",
311
+ "Use <code>/help</code> for the full command list."
312
+ ].filter(Boolean).join("\n");
313
+ }
314
+
315
+ function buildTelegramRoomGuidanceMarkup({ project = null, member = null, executor = {} } = {}) {
316
+ const role = String(member?.role || "observer").trim() || "observer";
317
+ const roleGuidance = role === "owner"
318
+ ? "As owner, you can add people, change room mode, assign terminal roles, and run work."
319
+ : role === "editor"
320
+ ? "As editor, you can collaborate on tasks, discuss plans, and execute work when the room is in execute mode and you hold the floor."
321
+ : "As observer, you can ask questions, discuss plans, and review work here.";
322
+ return [
323
+ "<b>What you can do here</b>",
324
+ `project: <code>${escapeTelegramHtml(project?.projectName || "unknown")}</code>`,
325
+ `your role: <code>${escapeTelegramHtml(role)}</code>`,
326
+ `room mode: <code>${escapeTelegramHtml(project?.roomMode || "chat")}</code>`,
327
+ executor?.provider && executor?.model ? `active runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>` : "",
328
+ roleGuidance,
329
+ "Try asking:",
330
+ "• <code>what project is this chat bound to?</code>",
331
+ "• <code>who is in the room?</code>",
332
+ "• <code>who are the bots?</code>",
333
+ role === "owner" ? "• <code>add Austin as editor</code>" : "• <code>what mode are we in?</code>"
334
+ ].filter(Boolean).join("\n");
335
+ }
336
+
275
337
  function formatGatewaySessionStatus({ sessionId, userId, username, cwd, runtimeProfile, provider, model }) {
276
338
  const bits = [
277
339
  "<b>Telegram remote session</b>",
@@ -501,6 +563,34 @@ function parseTelegramAgentIntent(text = "") {
501
563
  const lowered = value.toLowerCase();
502
564
  if (!value) return null;
503
565
  if (/^(how|what|why|when|where|who)\b/.test(lowered)) return null;
566
+ const reviewPatterns = [
567
+ /^(?:have|ask|use)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:to\s+)?review(?:\s+this)?\s*$/i,
568
+ /^(?:have|ask|use)\s+(.+?)\s+(?:bot|terminal)\s+(?:to\s+)?review(?:\s+this)?\s*$/i
569
+ ];
570
+ for (const pattern of reviewPatterns) {
571
+ const match = value.match(pattern);
572
+ if (!match) continue;
573
+ const target = String(match[1] || "").trim();
574
+ if (target) {
575
+ return { action: "agent-review", target, role: "reviewer" };
576
+ }
577
+ }
578
+
579
+ const executePatterns = [
580
+ /^(?:have|ask|use)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:to\s+)?(?:take execution|execute|handle execution|take this)\s*$/i,
581
+ /^(?:have|ask|use)\s+(.+?)\s+(?:bot|terminal)\s+(?:to\s+)?(?:take execution|execute|handle execution|take this)\s*$/i,
582
+ /^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:take execution|execute|handle execution)\s*$/i,
583
+ /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:take execution|execute|handle execution)\s*$/i
584
+ ];
585
+ for (const pattern of executePatterns) {
586
+ const match = value.match(pattern);
587
+ if (!match) continue;
588
+ const target = String(match[1] || "").trim();
589
+ if (target) {
590
+ return { action: "agent-execute", target, role: "executor" };
591
+ }
592
+ }
593
+
504
594
  const roleMatch = lowered.match(/\b(executor|reviewer|standby)\b/);
505
595
  const role = roleMatch?.[1] || "";
506
596
  if (!role) return null;
@@ -540,6 +630,9 @@ function parseTelegramStateIntent(text = "") {
540
630
  if (/\bwho can act\b/.test(lower) || /\bwho is the active operator\b/.test(lower) || /\bwho has the floor\b/.test(lower) || /\bwho claimed\b/.test(lower)) {
541
631
  return { action: "active-operator" };
542
632
  }
633
+ if (/\bwhat can i do here\b/.test(lower) || /\bhow do i use this room\b/.test(lower) || /\bhow do i use this chat\b/.test(lower) || /\bwhat do i do here\b/.test(lower)) {
634
+ return { action: "room-guidance" };
635
+ }
543
636
  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)) {
544
637
  return { action: "agent-list" };
545
638
  }
@@ -1555,16 +1648,28 @@ class TelegramGateway {
1555
1648
  actorId,
1556
1649
  actorName: actor.displayName || actorId
1557
1650
  });
1651
+ const runtimeLabel = targetAgent.provider && targetAgent.model ? `${targetAgent.provider}/${targetAgent.model}` : "unknown";
1652
+ const actionTitle = intent.action === "agent-review"
1653
+ ? "Roundtable reviewer assigned"
1654
+ : intent.action === "agent-execute"
1655
+ ? "Roundtable executor assigned"
1656
+ : "Roundtable terminal updated";
1657
+ const note = intent.action === "agent-review"
1658
+ ? "This sets the room reviewer role. It does not automatically run a review pass yet."
1659
+ : intent.action === "agent-execute"
1660
+ ? "This sets the room executor role. Claim/mode rules still control actual execution."
1661
+ : "";
1558
1662
  return {
1559
1663
  kind: "agent",
1560
1664
  project: nextProject,
1561
1665
  markup: [
1562
- "<b>Roundtable terminal updated</b>",
1666
+ `<b>${escapeTelegramHtml(actionTitle)}</b>`,
1563
1667
  `owner: <code>${escapeTelegramHtml(targetAgent.ownerName || targetAgent.ownerId || targetAgent.label || targetAgent.id || "-")}</code>`,
1564
1668
  `terminal: <code>${escapeTelegramHtml(targetAgent.label || targetAgent.id || "-")}</code>`,
1565
1669
  `role: <code>${escapeTelegramHtml(intent.role)}</code>`,
1566
- `runtime: <code>${escapeTelegramHtml(targetAgent.provider && targetAgent.model ? `${targetAgent.provider}/${targetAgent.model}` : "unknown")}</code>`,
1567
- `project: <code>${escapeTelegramHtml(nextProject.projectName || path.basename(session.cwd || this.cwd))}</code>`
1670
+ `runtime: <code>${escapeTelegramHtml(runtimeLabel)}</code>`,
1671
+ `project: <code>${escapeTelegramHtml(nextProject.projectName || path.basename(session.cwd || this.cwd))}</code>`,
1672
+ note
1568
1673
  ].join("\n")
1569
1674
  };
1570
1675
  }
@@ -1611,6 +1716,24 @@ class TelegramGateway {
1611
1716
  : "<b>Active operator</b>\nnone";
1612
1717
  }
1613
1718
 
1719
+ if (intent.action === "room-guidance") {
1720
+ if (!project?.enabled) {
1721
+ return [
1722
+ "<b>What you can do here</b>",
1723
+ "This chat is not bound to a shared project yet.",
1724
+ "Try:",
1725
+ "• <code>share this project in this chat</code>",
1726
+ "• <code>what project are you in?</code>",
1727
+ "• <code>/help</code> for the full command list"
1728
+ ].join("\n");
1729
+ }
1730
+ const actorId = String(message?.from?.id || "").trim();
1731
+ const member = Array.isArray(project?.members)
1732
+ ? project.members.find((entry) => String(entry?.id || "").trim() === actorId) || null
1733
+ : null;
1734
+ return buildTelegramRoomGuidanceMarkup({ project, member, executor });
1735
+ }
1736
+
1614
1737
  if (intent.action === "agent-list") {
1615
1738
  if (!project?.enabled) {
1616
1739
  return "This project is not shared.";
@@ -2279,11 +2402,45 @@ class TelegramGateway {
2279
2402
  this.clearContinuation(message);
2280
2403
  const userId = String(message.from.id);
2281
2404
 
2282
- if (text === "/help" || text === "/start") {
2405
+ if (text === "/help") {
2283
2406
  await this.sendMessage(message.chat.id, buildRemoteHelp(), message.message_id);
2284
2407
  return;
2285
2408
  }
2286
2409
 
2410
+ if (text === "/start") {
2411
+ if (this.isGroupChat(message)) {
2412
+ const sessionId = await this.ensurePeerSession(message);
2413
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
2414
+ const host = await this.getLiveBridgeHost();
2415
+ await this.sendMarkup(
2416
+ message.chat.id,
2417
+ buildTelegramRoomOnboardingMarkup({
2418
+ project,
2419
+ actorName: this.describeTelegramUser(message?.from || {}).displayName,
2420
+ executor: {
2421
+ provider: host?.provider || this.runtime.provider,
2422
+ model: host?.model || this.runtime.model
2423
+ }
2424
+ }),
2425
+ message.message_id
2426
+ );
2427
+ } else {
2428
+ const peer = this.getPeerState(String(message?.from?.id || "").trim());
2429
+ const session = peer?.sessionId ? await loadSession(peer.sessionId).catch(() => null) : null;
2430
+ const project = session?.cwd ? await loadSharedProject(session.cwd).catch(() => null) : null;
2431
+ await this.sendMarkup(
2432
+ message.chat.id,
2433
+ buildTelegramDmOnboardingMarkup({
2434
+ cwd: session?.cwd || this.cwd,
2435
+ project,
2436
+ peer
2437
+ }),
2438
+ message.message_id
2439
+ );
2440
+ }
2441
+ return;
2442
+ }
2443
+
2287
2444
  if (text === "/new") {
2288
2445
  const sessionId = await this.startFreshSession(message);
2289
2446
  await this.sendMessage(message.chat.id, `Started new remote session: <code>${escapeTelegramHtml(sessionId)}</code>`, message.message_id);