@tritard/waterbrother 0.16.49 → 0.16.51

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/README.md CHANGED
@@ -297,7 +297,8 @@ Current Telegram behavior:
297
297
  - pending pairings are explicit and expire automatically after 12 hours unless approved
298
298
  - paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
299
299
  - Telegram now supports remote workspace control with `/project`, `/project use <path|name>`, `/project new <name>`, `/project share`, plus the lower-level `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>` commands
300
- - Telegram owners can pair visible chat participants conversationally or with `/pair <user-id|@username>`, then continue with shared-room invites without dropping back to the terminal
300
+ - Telegram owners can pair visible chat participants conversationally or with `/pair <user-id|@username>`, and owner requests like “add Austin as editor” now pair the person if needed and add them to the shared project directly
301
+ - Telegram also answers live state questions from room state now, including prompts like “is Austin paired?”, “is Austin in the project?”, “who is in the room?”, and “what project is this chat bound to?”
301
302
  - Telegram owners can also use conversational project and member requests like “share this project in this chat”, “switch to TelegramTest”, or “add Austin as editor”, and Waterbrother routes those through the same project and shared-room actions
302
303
  - shared projects now support `/room`, `/events`, `/members`, `/invites`, `/whoami`, `/people`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/accept-invite`, `/approve-invite`, `/reject-invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
303
304
  - Telegram now supports `/whoami` for Telegram-id and shared-room membership visibility, plus `/events` for recent shared-room activity
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.49",
3
+ "version": "0.16.51",
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
@@ -379,6 +379,37 @@ function parseTelegramPairingIntent(text = "") {
379
379
  return null;
380
380
  }
381
381
 
382
+ function parseTelegramStateIntent(text = "") {
383
+ const value = normalizeTelegramProjectIntentText(text);
384
+ const lower = value.toLowerCase();
385
+ if (!value) return null;
386
+
387
+ if (/\bwhat project\b/.test(lower) || /\bwhich project\b/.test(lower) || /\bproject is this chat\b/.test(lower) || /\bchat bound to\b/.test(lower)) {
388
+ return { action: "project-status" };
389
+ }
390
+ if (/\bwho is in the room\b/.test(lower) || /\bwho(?:'s| is) in the project\b/.test(lower) || /\bwho are the members\b/.test(lower) || /\bshow members\b/.test(lower)) {
391
+ return { action: "room-members" };
392
+ }
393
+ if (/\bwhat mode\b/.test(lower) || /\broom mode\b/.test(lower) || /\bmode are we in\b/.test(lower)) {
394
+ return { action: "room-mode" };
395
+ }
396
+ 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)) {
397
+ return { action: "active-operator" };
398
+ }
399
+
400
+ const pairedMatch = value.match(/^(?:is|has)\s+(.+?)\s+paired(?:\s+now)?\??$/i);
401
+ if (pairedMatch?.[1]) {
402
+ return { action: "pairing-status", target: pairedMatch[1].trim() };
403
+ }
404
+
405
+ const memberMatch = value.match(/^(?:is|has)\s+(.+?)\s+(?:in the project|in the room|a member)(?:\s+now)?\??$/i);
406
+ if (memberMatch?.[1]) {
407
+ return { action: "member-status", target: memberMatch[1].trim() };
408
+ }
409
+
410
+ return null;
411
+ }
412
+
382
413
  async function buildTelegramSelfAwarenessMarkup({ cwd, runtime, currentSession, kind = "about" }) {
383
414
  const manifest = await buildSelfAwarenessManifest({ cwd, runtime, currentSession });
384
415
  if (kind === "state") {
@@ -1022,21 +1053,92 @@ class TelegramGateway {
1022
1053
  markup: `Updated <code>${escapeTelegramHtml(target.displayName || target.userId)}</code> to <code>${escapeTelegramHtml(intent.role)}</code> in this shared project.`
1023
1054
  };
1024
1055
  }
1025
- const result = await createSharedInvite(
1056
+ const known = this.listKnownChatPeople(message);
1057
+ const knownPerson = known.find((person) => String(person.userId || "").trim() === target.userId) || null;
1058
+ let pairedNow = false;
1059
+ if (!knownPerson?.paired) {
1060
+ await this.pairKnownTelegramUser(message, target.userId, { skipOwnerCheck: true });
1061
+ pairedNow = true;
1062
+ }
1063
+ const nextProject = await upsertSharedMember(
1026
1064
  session.cwd || this.cwd,
1027
- { id: target.userId, role: intent.role, name: target.displayName || target.userId, paired: true },
1065
+ { id: target.userId, name: target.displayName || target.userId, role: intent.role, paired: true },
1028
1066
  { actorId, actorName }
1029
1067
  );
1030
1068
  return {
1031
- kind: "invite",
1032
- project: result.project,
1033
- markup: `Created pending invite <code>${escapeTelegramHtml(result.invite.id)}</code> for <code>${escapeTelegramHtml(target.displayName || target.userId)}</code> as <code>${escapeTelegramHtml(result.invite.role)}</code>.\nAsk them to run <code>/accept-invite ${escapeTelegramHtml(result.invite.id)}</code>.`
1069
+ kind: "member",
1070
+ 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>.`
1034
1072
  };
1035
1073
  }
1036
1074
 
1037
- async pairKnownTelegramUser(message, target) {
1075
+ async handleStateIntent(message, sessionId, intent) {
1076
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1077
+ const cwd = session.cwd || this.cwd;
1078
+ const known = this.listKnownChatPeople(message);
1079
+
1080
+ if (intent.action === "project-status") {
1081
+ return formatTelegramProjectMarkup({
1082
+ cwd,
1083
+ project,
1084
+ chatId: String(message.chat.id),
1085
+ title: String(message.chat.title || "").trim()
1086
+ });
1087
+ }
1088
+
1089
+ if (intent.action === "room-members") {
1090
+ return formatTelegramMembersMarkup(project);
1091
+ }
1092
+
1093
+ if (intent.action === "room-mode") {
1094
+ return project?.enabled
1095
+ ? `<b>Shared room mode</b>\n<code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`
1096
+ : "This project is not shared.";
1097
+ }
1098
+
1099
+ if (intent.action === "active-operator") {
1100
+ if (!project?.enabled) {
1101
+ return "This project is not shared.";
1102
+ }
1103
+ return project.activeOperator?.id
1104
+ ? `<b>Active operator</b>\n<code>${escapeTelegramHtml(project.activeOperator.name || project.activeOperator.id)}</code>`
1105
+ : "<b>Active operator</b>\nnone";
1106
+ }
1107
+
1108
+ if (intent.action === "pairing-status") {
1109
+ const person = this.resolveKnownPerson(message, intent.target);
1110
+ const match = known.find((entry) => String(entry.userId || "").trim() === person.userId) || null;
1111
+ return [
1112
+ `<b>Telegram pairing</b>`,
1113
+ `person: <code>${escapeTelegramHtml(match?.displayName || person.displayName || person.userId)}</code>`,
1114
+ `user id: <code>${escapeTelegramHtml(person.userId)}</code>`,
1115
+ `paired: <code>${escapeTelegramHtml(match?.paired ? "yes" : "no")}</code>`
1116
+ ].join("\n");
1117
+ }
1118
+
1119
+ if (intent.action === "member-status") {
1120
+ if (!project?.enabled) {
1121
+ return "This project is not shared.";
1122
+ }
1123
+ const person = this.resolveKnownPerson(message, intent.target);
1124
+ const member = Array.isArray(project.members)
1125
+ ? project.members.find((entry) => String(entry?.id || "").trim() === person.userId) || null
1126
+ : null;
1127
+ return [
1128
+ `<b>Shared-project membership</b>`,
1129
+ `person: <code>${escapeTelegramHtml(person.displayName || person.userId)}</code>`,
1130
+ `user id: <code>${escapeTelegramHtml(person.userId)}</code>`,
1131
+ `member: <code>${escapeTelegramHtml(member ? "yes" : "no")}</code>`,
1132
+ member?.role ? `role: <code>${escapeTelegramHtml(member.role)}</code>` : ""
1133
+ ].filter(Boolean).join("\n");
1134
+ }
1135
+
1136
+ return null;
1137
+ }
1138
+
1139
+ async pairKnownTelegramUser(message, target, { skipOwnerCheck = false } = {}) {
1038
1140
  const { session, project } = await this.bindSharedRoomForMessage(message, await this.ensurePeerSession(message));
1039
- if (project?.enabled) {
1141
+ if (project?.enabled && !skipOwnerCheck) {
1040
1142
  const owner = Array.isArray(project.members)
1041
1143
  ? project.members.find((entry) => String(entry?.id || "").trim() === String(message?.from?.id || "").trim())
1042
1144
  : null;
@@ -2304,6 +2406,19 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
2304
2406
  ? (isExplicitRun ? { kind: "execution", reason: "explicit /run" } : classifyTelegramGroupIntent(promptText))
2305
2407
  : { kind: "execution", reason: "private chat" };
2306
2408
  const sharedBinding = await this.bindSharedRoomForMessage(message, sessionId);
2409
+ const stateIntent = parseTelegramStateIntent(promptText);
2410
+ if (stateIntent) {
2411
+ try {
2412
+ const markup = await this.handleStateIntent(message, sessionId, stateIntent);
2413
+ if (markup) {
2414
+ await this.sendMarkup(message.chat.id, markup, message.message_id);
2415
+ return;
2416
+ }
2417
+ } catch (error) {
2418
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
2419
+ return;
2420
+ }
2421
+ }
2307
2422
  const pairingIntent = parseTelegramPairingIntent(promptText);
2308
2423
  if (pairingIntent) {
2309
2424
  try {