@tritard/waterbrother 0.16.47 → 0.16.48

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,6 +297,7 @@ 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 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
300
301
  - 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
301
302
  - Telegram now supports `/whoami` for Telegram-id and shared-room membership visibility, plus `/events` for recent shared-room activity
302
303
  - `/room` now includes pending invite count plus task ownership summaries
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.47",
3
+ "version": "0.16.48",
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
@@ -328,6 +328,38 @@ function parseTelegramProjectIntent(text = "") {
328
328
  return null;
329
329
  }
330
330
 
331
+ function parseTelegramMemberIntent(text = "") {
332
+ const value = normalizeTelegramProjectIntentText(text);
333
+ const lowered = value.toLowerCase();
334
+ if (!value) return null;
335
+ if (/^(how|what|why|when|where|who)\b/.test(lowered)) return null;
336
+
337
+ const roleMatch = lowered.match(/\b(owner|editor|observer)s?\b/);
338
+ const role = roleMatch?.[1] || "editor";
339
+ const explicitUsername = value.match(/@([a-zA-Z0-9_]+)/);
340
+
341
+ if (explicitUsername && /\b(add|invite|make|set|promote)\b/.test(lowered)) {
342
+ return { action: "member-upsert", target: `@${explicitUsername[1]}`, role };
343
+ }
344
+
345
+ const patterns = [
346
+ /^(?:add|invite)\s+(.+?)\s+as\s+(owner|editor|observer)\s*$/i,
347
+ /^(?:add|invite)\s+(.+?)\s*$/i,
348
+ /^(?:make|set|promote)\s+(.+?)\s+(?:an?\s+)?(owner|editor|observer)\s*$/i
349
+ ];
350
+ for (const pattern of patterns) {
351
+ const match = value.match(pattern);
352
+ if (!match) continue;
353
+ const target = String(match[1] || "").trim();
354
+ const nextRole = String(match[2] || role || "editor").trim().toLowerCase();
355
+ if (target) {
356
+ return { action: "member-upsert", target, role: nextRole };
357
+ }
358
+ }
359
+
360
+ return null;
361
+ }
362
+
331
363
  async function buildTelegramSelfAwarenessMarkup({ cwd, runtime, currentSession, kind = "about" }) {
332
364
  const manifest = await buildSelfAwarenessManifest({ cwd, runtime, currentSession });
333
365
  if (kind === "state") {
@@ -918,6 +950,71 @@ class TelegramGateway {
918
950
  throw new Error(`Unknown Telegram user reference: ${value}. Use /people to list recent users in this chat.`);
919
951
  }
920
952
 
953
+ resolveKnownPerson(message, target) {
954
+ const value = String(target || "").trim();
955
+ if (!value) {
956
+ throw new Error("member target is required");
957
+ }
958
+ if (/^\d+$/.test(value) || value.startsWith("@")) {
959
+ const match = this.resolveInviteTarget(message, value);
960
+ return { userId: match.userId, displayName: match.displayName };
961
+ }
962
+
963
+ const normalized = value.toLowerCase();
964
+ const known = this.listKnownChatPeople(message);
965
+ const exact = known.find((person) => String(person.displayName || "").trim().toLowerCase() === normalized);
966
+ if (exact) {
967
+ return { userId: exact.userId, displayName: exact.displayName || exact.userId };
968
+ }
969
+ const partial = known.filter((person) => {
970
+ const display = String(person.displayName || "").trim().toLowerCase();
971
+ const handle = String(person.usernameHandle || "").trim().toLowerCase();
972
+ return display.includes(normalized) || handle === normalized.replace(/^@/, "");
973
+ });
974
+ if (partial.length === 1) {
975
+ return { userId: partial[0].userId, displayName: partial[0].displayName || partial[0].userId };
976
+ }
977
+ if (partial.length > 1) {
978
+ throw new Error(`Multiple Telegram users matched ${value}. Use /people and invite by id or @username.`);
979
+ }
980
+ throw new Error(`Unknown Telegram person: ${value}. Use /people to see recent users in this chat.`);
981
+ }
982
+
983
+ async handleConversationalMemberIntent(message, sessionId, intent, peer) {
984
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
985
+ if (!project?.enabled) {
986
+ throw new Error("This project is not shared yet. Use /project share first.");
987
+ }
988
+ const target = this.resolveKnownPerson(message, intent.target);
989
+ const actorId = String(message?.from?.id || "").trim();
990
+ const actorName = peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || actorId;
991
+ const existingMember = Array.isArray(project.members)
992
+ ? project.members.find((entry) => String(entry?.id || "").trim() === target.userId) || null
993
+ : null;
994
+ if (existingMember) {
995
+ const nextProject = await upsertSharedMember(
996
+ session.cwd || this.cwd,
997
+ { id: target.userId, name: target.displayName || target.userId, role: intent.role, paired: true },
998
+ { actorId, actorName }
999
+ );
1000
+ return {
1001
+ kind: "member",
1002
+ project: nextProject,
1003
+ markup: `Updated <code>${escapeTelegramHtml(target.displayName || target.userId)}</code> to <code>${escapeTelegramHtml(intent.role)}</code> in this shared project.`
1004
+ };
1005
+ }
1006
+ const result = await createSharedInvite(
1007
+ session.cwd || this.cwd,
1008
+ { id: target.userId, role: intent.role, name: target.displayName || target.userId, paired: true },
1009
+ { actorId, actorName }
1010
+ );
1011
+ return {
1012
+ kind: "invite",
1013
+ project: result.project,
1014
+ 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>.`
1015
+ };
1016
+ }
1017
+
921
1018
  async addPendingPairing(message) {
922
1019
  const { userId, usernameHandle, displayName } = this.describeTelegramUser(message?.from || {});
923
1020
  this.state.pendingPairings[userId] = {
@@ -2126,6 +2223,16 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
2126
2223
  ? (isExplicitRun ? { kind: "execution", reason: "explicit /run" } : classifyTelegramGroupIntent(promptText))
2127
2224
  : { kind: "execution", reason: "private chat" };
2128
2225
  const sharedBinding = await this.bindSharedRoomForMessage(message, sessionId);
2226
+ const memberIntent = parseTelegramMemberIntent(promptText);
2227
+ if (memberIntent) {
2228
+ try {
2229
+ const result = await this.handleConversationalMemberIntent(message, sessionId, memberIntent, peer);
2230
+ await this.sendMarkup(message.chat.id, result.markup, message.message_id);
2231
+ } catch (error) {
2232
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
2233
+ }
2234
+ return;
2235
+ }
2129
2236
  const projectIntent = parseTelegramProjectIntent(promptText);
2130
2237
  if (projectIntent?.action === "show-project") {
2131
2238
  await this.sendMarkup(