@tritard/waterbrother 0.16.68 → 0.16.70

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 +208 -2
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.68",
3
+ "version": "0.16.70",
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
@@ -9,7 +9,7 @@ import { DEFAULT_PENDING_PAIRING_TTL_MINUTES, loadGatewayBridge, loadGatewayStat
9
9
  import { getGatewayStatus, getChannelSpec } from "./channels.js";
10
10
  import { getConfigPath, loadConfigLayers, saveConfig } from "./config.js";
11
11
  import { canonicalizeLoosePath } from "./path-utils.js";
12
- import { acceptSharedInvite, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, enableSharedProject, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember, upsertSharedMember } from "./shared-project.js";
12
+ import { acceptSharedInvite, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, enableSharedProject, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember, upsertSharedAgent, upsertSharedMember } from "./shared-project.js";
13
13
  import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState, resolveLocalConceptQuestion } from "./self-awareness.js";
14
14
 
15
15
  const execFileAsync = promisify(execFile);
@@ -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>",
@@ -340,6 +402,23 @@ function chooseExecutorAgent(project, fallbackExecutor = {}) {
340
402
  return null;
341
403
  }
342
404
 
405
+ function memberRoleWeight(role = "") {
406
+ const normalized = String(role || "").trim().toLowerCase();
407
+ if (normalized === "owner") return 3;
408
+ if (normalized === "editor") return 2;
409
+ if (normalized === "observer") return 1;
410
+ return 0;
411
+ }
412
+
413
+ function memberHasAtLeastRole(project, memberId = "", role = "editor") {
414
+ const normalizedId = String(memberId || "").trim();
415
+ if (!normalizedId) return false;
416
+ const member = Array.isArray(project?.members)
417
+ ? project.members.find((entry) => String(entry?.id || "").trim() === normalizedId) || null
418
+ : null;
419
+ return memberRoleWeight(member?.role) >= memberRoleWeight(role);
420
+ }
421
+
343
422
  function formatAgentLabel(agent = {}) {
344
423
  const label = String(agent?.label || agent?.name || "").trim();
345
424
  const role = String(agent?.role || "").trim();
@@ -479,6 +558,33 @@ function parseTelegramPairingIntent(text = "") {
479
558
  return null;
480
559
  }
481
560
 
561
+ function parseTelegramAgentIntent(text = "") {
562
+ const value = normalizeTelegramProjectIntentText(text);
563
+ const lowered = value.toLowerCase();
564
+ if (!value) return null;
565
+ if (/^(how|what|why|when|where|who)\b/.test(lowered)) return null;
566
+ const roleMatch = lowered.match(/\b(executor|reviewer|standby)\b/);
567
+ const role = roleMatch?.[1] || "";
568
+ if (!role) return null;
569
+
570
+ const patterns = [
571
+ /^(?:have|make|set)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|standby)\s*$/i,
572
+ /^(?:have|make|set)\s+(.+?)\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|standby)\s*$/i,
573
+ /^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|standby)\s*$/i,
574
+ /^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|standby)\s*$/i
575
+ ];
576
+ for (const pattern of patterns) {
577
+ const match = value.match(pattern);
578
+ if (!match) continue;
579
+ const target = String(match[1] || "").trim();
580
+ const nextRole = String(match[2] || role).trim().toLowerCase();
581
+ if (target && nextRole) {
582
+ return { action: "agent-role", target, role: nextRole };
583
+ }
584
+ }
585
+ return null;
586
+ }
587
+
482
588
  function parseTelegramStateIntent(text = "") {
483
589
  const value = normalizeTelegramProjectIntentText(text);
484
590
  const lower = value.toLowerCase();
@@ -496,6 +602,9 @@ function parseTelegramStateIntent(text = "") {
496
602
  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)) {
497
603
  return { action: "active-operator" };
498
604
  }
605
+ 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)) {
606
+ return { action: "room-guidance" };
607
+ }
499
608
  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)) {
500
609
  return { action: "agent-list" };
501
610
  }
@@ -1490,6 +1599,41 @@ class TelegramGateway {
1490
1599
  };
1491
1600
  }
1492
1601
 
1602
+ async handleConversationalAgentIntent(message, sessionId, intent) {
1603
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1604
+ if (!project?.enabled) {
1605
+ throw new Error("This project is not shared yet. Use /project share first.");
1606
+ }
1607
+ const actorId = String(message?.from?.id || "").trim();
1608
+ if (!memberHasAtLeastRole(project, actorId, "owner")) {
1609
+ throw new Error("Only a shared-project owner can change terminal roles.");
1610
+ }
1611
+ const actor = this.describeTelegramUser(message?.from || {});
1612
+ const targetAgent = resolveProjectAgent(project, intent.target);
1613
+ if (!targetAgent) {
1614
+ throw new Error(`No terminal found for ${intent.target}. Ask them to connect their Waterbrother bot/terminal first.`);
1615
+ }
1616
+ const nextProject = await upsertSharedAgent(session.cwd || this.cwd, {
1617
+ ...targetAgent,
1618
+ role: intent.role
1619
+ }, {
1620
+ actorId,
1621
+ actorName: actor.displayName || actorId
1622
+ });
1623
+ return {
1624
+ kind: "agent",
1625
+ project: nextProject,
1626
+ markup: [
1627
+ "<b>Roundtable terminal updated</b>",
1628
+ `owner: <code>${escapeTelegramHtml(targetAgent.ownerName || targetAgent.ownerId || targetAgent.label || targetAgent.id || "-")}</code>`,
1629
+ `terminal: <code>${escapeTelegramHtml(targetAgent.label || targetAgent.id || "-")}</code>`,
1630
+ `role: <code>${escapeTelegramHtml(intent.role)}</code>`,
1631
+ `runtime: <code>${escapeTelegramHtml(targetAgent.provider && targetAgent.model ? `${targetAgent.provider}/${targetAgent.model}` : "unknown")}</code>`,
1632
+ `project: <code>${escapeTelegramHtml(nextProject.projectName || path.basename(session.cwd || this.cwd))}</code>`
1633
+ ].join("\n")
1634
+ };
1635
+ }
1636
+
1493
1637
  async handleStateIntent(message, sessionId, intent) {
1494
1638
  const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1495
1639
  const cwd = session.cwd || this.cwd;
@@ -1532,6 +1676,24 @@ class TelegramGateway {
1532
1676
  : "<b>Active operator</b>\nnone";
1533
1677
  }
1534
1678
 
1679
+ if (intent.action === "room-guidance") {
1680
+ if (!project?.enabled) {
1681
+ return [
1682
+ "<b>What you can do here</b>",
1683
+ "This chat is not bound to a shared project yet.",
1684
+ "Try:",
1685
+ "• <code>share this project in this chat</code>",
1686
+ "• <code>what project are you in?</code>",
1687
+ "• <code>/help</code> for the full command list"
1688
+ ].join("\n");
1689
+ }
1690
+ const actorId = String(message?.from?.id || "").trim();
1691
+ const member = Array.isArray(project?.members)
1692
+ ? project.members.find((entry) => String(entry?.id || "").trim() === actorId) || null
1693
+ : null;
1694
+ return buildTelegramRoomGuidanceMarkup({ project, member, executor });
1695
+ }
1696
+
1535
1697
  if (intent.action === "agent-list") {
1536
1698
  if (!project?.enabled) {
1537
1699
  return "This project is not shared.";
@@ -2200,11 +2362,45 @@ class TelegramGateway {
2200
2362
  this.clearContinuation(message);
2201
2363
  const userId = String(message.from.id);
2202
2364
 
2203
- if (text === "/help" || text === "/start") {
2365
+ if (text === "/help") {
2204
2366
  await this.sendMessage(message.chat.id, buildRemoteHelp(), message.message_id);
2205
2367
  return;
2206
2368
  }
2207
2369
 
2370
+ if (text === "/start") {
2371
+ if (this.isGroupChat(message)) {
2372
+ const sessionId = await this.ensurePeerSession(message);
2373
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
2374
+ const host = await this.getLiveBridgeHost();
2375
+ await this.sendMarkup(
2376
+ message.chat.id,
2377
+ buildTelegramRoomOnboardingMarkup({
2378
+ project,
2379
+ actorName: this.describeTelegramUser(message?.from || {}).displayName,
2380
+ executor: {
2381
+ provider: host?.provider || this.runtime.provider,
2382
+ model: host?.model || this.runtime.model
2383
+ }
2384
+ }),
2385
+ message.message_id
2386
+ );
2387
+ } else {
2388
+ const peer = this.getPeerState(String(message?.from?.id || "").trim());
2389
+ const session = peer?.sessionId ? await loadSession(peer.sessionId).catch(() => null) : null;
2390
+ const project = session?.cwd ? await loadSharedProject(session.cwd).catch(() => null) : null;
2391
+ await this.sendMarkup(
2392
+ message.chat.id,
2393
+ buildTelegramDmOnboardingMarkup({
2394
+ cwd: session?.cwd || this.cwd,
2395
+ project,
2396
+ peer
2397
+ }),
2398
+ message.message_id
2399
+ );
2400
+ }
2401
+ return;
2402
+ }
2403
+
2208
2404
  if (text === "/new") {
2209
2405
  const sessionId = await this.startFreshSession(message);
2210
2406
  await this.sendMessage(message.chat.id, `Started new remote session: <code>${escapeTelegramHtml(sessionId)}</code>`, message.message_id);
@@ -2962,6 +3158,16 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
2962
3158
  }
2963
3159
  return;
2964
3160
  }
3161
+ const agentIntent = parseTelegramAgentIntent(promptText);
3162
+ if (agentIntent) {
3163
+ try {
3164
+ const result = await this.handleConversationalAgentIntent(message, sessionId, agentIntent);
3165
+ await this.sendMarkup(message.chat.id, result.markup, message.message_id);
3166
+ } catch (error) {
3167
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
3168
+ }
3169
+ return;
3170
+ }
2965
3171
  const memberIntent = parseTelegramMemberIntent(promptText);
2966
3172
  if (memberIntent) {
2967
3173
  try {