@tritard/waterbrother 0.16.40 → 0.16.42

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
@@ -266,6 +266,7 @@ Shared project foundation is now live:
266
266
  - inspect it with `waterbrother room status`
267
267
  - control conversation vs execution with `waterbrother room mode chat|plan|execute`
268
268
  - manage collaborators with `waterbrother room members`, `waterbrother room invites`, `waterbrother room invite`, `waterbrother room invite accept`, and `waterbrother room remove`
269
+ - inspect recent collaboration activity with `waterbrother room events`
269
270
  - manage the shared backlog with `waterbrother room tasks`, `waterbrother room task add`, and `waterbrother room task move`
270
271
  - assign, claim, and discuss shared work with `waterbrother room task assign`, `waterbrother room task claim`, `waterbrother room task comment`, and `waterbrother room task history`
271
272
  - choose a shared execution preset with `waterbrother room runtime <profile>`
@@ -296,6 +297,7 @@ Current Telegram behavior:
296
297
  - paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
297
298
  - Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
298
299
  - shared projects now support `/room`, `/members`, `/invites`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/accept-invite`, `/approve-invite`, `/reject-invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
300
+ - Telegram now supports `/whoami` for Telegram-id and shared-room membership visibility, plus `/events` for recent shared-room activity
299
301
  - `/room` now includes pending invite count plus task ownership summaries
300
302
  - local `/status` now includes `sharedRoom` with pending invites, task ownership summary, and recent shared-room event activity
301
303
  - the TUI now prints a small Roundtable event feed when new shared-room activity lands
@@ -304,6 +306,10 @@ Current Telegram behavior:
304
306
  - room administration is owner-only, and only owners/editors can hold the operator lock
305
307
  - `/room` status now shows the active executor surface plus provider/model/runtime identity
306
308
  - repo-first concept resolution now covers Waterbrother itself, Roundtable, Telegram, shared rooms, the gateway, runtime profiles, approvals, and sessions
309
+ - collaboration how-to questions now answer from local state too, including:
310
+ - how to invite a partner
311
+ - how to share the current project
312
+ - how to make a new project
307
313
  - in Telegram groups, Waterbrother only responds when directly targeted: slash commands, `@botname` mentions, or replies to a bot message
308
314
  - in Telegram groups, directly targeted messages are now classified as chat, planning, or execution; explicit execution should use `/run <prompt>`
309
315
  - in Telegram group `chat` or `plan` flows, targeted Waterbrother/project questions are now answered directly from local repo state instead of returning only a generic planner hint
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.40",
3
+ "version": "0.16.42",
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/cli.js CHANGED
@@ -81,6 +81,7 @@ import {
81
81
  formatSharedProjectStatus,
82
82
  getSharedTaskHistory,
83
83
  getSharedProjectPaths,
84
+ listSharedEvents,
84
85
  listSharedInvites,
85
86
  listSharedMembers,
86
87
  listSharedTasks,
@@ -178,6 +179,7 @@ const INTERACTIVE_COMMANDS = [
178
179
  { name: "/share-project", description: "Enable shared-project mode in the current cwd" },
179
180
  { name: "/unshare-project", description: "Disable shared-project mode in the current cwd" },
180
181
  { name: "/room", description: "Show shared room status for the current project" },
182
+ { name: "/room events", description: "Show recent Roundtable room events" },
181
183
  { name: "/room members", description: "List shared-project members" },
182
184
  { name: "/room invites", description: "List pending shared-project invites" },
183
185
  { name: "/room add <id> [owner|editor|observer]", description: "Add or update a shared-project member" },
@@ -4047,6 +4049,22 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false,
4047
4049
  return;
4048
4050
  }
4049
4051
 
4052
+ if (sub === "events") {
4053
+ const events = await listSharedEvents(cwd, 12);
4054
+ if (asJson) {
4055
+ printData({ ok: true, events }, true);
4056
+ return;
4057
+ }
4058
+ if (!events.length) {
4059
+ console.log("No recent shared-room events");
4060
+ return;
4061
+ }
4062
+ for (const event of events) {
4063
+ console.log(`${event.createdAt}\t${event.type}\t${event.actorName || event.actorId || "-"}\t${event.text}`);
4064
+ }
4065
+ return;
4066
+ }
4067
+
4050
4068
  if (sub === "tasks") {
4051
4069
  const tasks = await listSharedTasks(cwd);
4052
4070
  if (asJson) {
@@ -4296,7 +4314,7 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false,
4296
4314
  return;
4297
4315
  }
4298
4316
 
4299
- throw new Error("Usage: waterbrother room status|members|invites|add <member-id> [owner|editor|observer] [display name]|invite <member-id> [owner|editor|observer] [display name]|invite approve <invite-id>|invite accept <invite-id>|invite reject <invite-id>|remove <member-id>|tasks|task add <text>|task assign <id> <member-id>|task claim <id>|task move <id> <open|active|blocked|done>|task comment <id> <text>|task history <id>|runtime [<name>|clear]|mode <chat|plan|execute>|claim|release");
4317
+ throw new Error("Usage: waterbrother room status|events|members|invites|add <member-id> [owner|editor|observer] [display name]|invite <member-id> [owner|editor|observer] [display name]|invite approve <invite-id>|invite accept <invite-id>|invite reject <invite-id>|remove <member-id>|tasks|task add <text>|task assign <id> <member-id>|task claim <id>|task move <id> <open|active|blocked|done>|task comment <id> <text>|task history <id>|runtime [<name>|clear]|mode <chat|plan|execute>|claim|release");
4300
4318
  }
4301
4319
 
4302
4320
  async function runMcpCommand(positional, runtime, { cwd, asJson = false } = {}) {
@@ -8241,6 +8259,15 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
8241
8259
  continue;
8242
8260
  }
8243
8261
 
8262
+ if (line === "/room events") {
8263
+ try {
8264
+ await runRoomCommand(["room", "events"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
8265
+ } catch (error) {
8266
+ console.log(`room events failed: ${error instanceof Error ? error.message : String(error)}`);
8267
+ }
8268
+ continue;
8269
+ }
8270
+
8244
8271
  if (line === "/room tasks") {
8245
8272
  try {
8246
8273
  await runRoomCommand(["room", "tasks"], { cwd: context.cwd, asJson: false, runtime: context.runtime, currentSession, agent, context });
package/src/gateway.js CHANGED
@@ -8,7 +8,7 @@ import { createSession, listSessions, loadSession, saveSession } from "./session
8
8
  import { DEFAULT_PENDING_PAIRING_TTL_MINUTES, loadGatewayBridge, loadGatewayState, prunePendingPairings, saveGatewayBridge, saveGatewayState } from "./gateway-state.js";
9
9
  import { getGatewayStatus, getChannelSpec } from "./channels.js";
10
10
  import { canonicalizeLoosePath } from "./path-utils.js";
11
- import { acceptSharedInvite, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, getSharedTaskHistory, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember } from "./shared-project.js";
11
+ import { acceptSharedInvite, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember } from "./shared-project.js";
12
12
  import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState, resolveLocalConceptQuestion } from "./self-awareness.js";
13
13
 
14
14
  const execFileAsync = promisify(execFile);
@@ -27,9 +27,11 @@ const TELEGRAM_COMMANDS = [
27
27
  { command: "state", description: "Show current Waterbrother self-awareness state" },
28
28
  { command: "run", description: "Execute an explicit remote prompt" },
29
29
  { command: "status", description: "Show the linked remote session" },
30
+ { command: "whoami", description: "Show your Telegram identity and room membership" },
30
31
  { command: "cwd", description: "Show the current remote working directory" },
31
32
  { command: "runtime", description: "Show active runtime status" },
32
33
  { command: "room", description: "Show shared room status" },
34
+ { command: "events", description: "Show recent shared room events" },
33
35
  { command: "members", description: "List shared room members" },
34
36
  { command: "invites", description: "List pending shared room invites" },
35
37
  { command: "tasks", description: "List shared Roundtable tasks" },
@@ -204,12 +206,14 @@ function buildRemoteHelp() {
204
206
  "<code>/state</code> show current Waterbrother self-awareness state",
205
207
  "<code>/run &lt;prompt&gt;</code> execute an explicit remote request",
206
208
  "<code>/status</code> show the current linked remote session",
209
+ "<code>/whoami</code> show your Telegram identity and room membership",
207
210
  "<code>/cwd</code> show the current remote working directory",
208
211
  "<code>/use &lt;path&gt;</code> switch the linked session to another directory",
209
212
  "<code>/desktop</code> switch the linked session to <code>~/Desktop</code>",
210
213
  "<code>/new-project &lt;name&gt;</code> create a folder on Desktop and switch into it",
211
214
  "<code>/runtime</code> show active provider/model/runtime state",
212
215
  "<code>/room</code> show shared project room status",
216
+ "<code>/events</code> show recent shared room events",
213
217
  "<code>/members</code> list shared project members",
214
218
  "<code>/tasks</code> list shared project tasks",
215
219
  "<code>/task add &lt;text&gt;</code> add a shared Roundtable task",
@@ -388,6 +392,51 @@ function formatTelegramInvitesMarkup(invites = []) {
388
392
  ].join("\n");
389
393
  }
390
394
 
395
+ function formatTelegramEventsMarkup(events = []) {
396
+ if (!events.length) {
397
+ return "<b>Recent room events</b>\n• none";
398
+ }
399
+ return [
400
+ "<b>Recent room events</b>",
401
+ ...events.map((event) => {
402
+ const actor = String(event.actorName || event.actorId || "").trim();
403
+ const when = String(event.createdAt || "").trim();
404
+ return `• ${actor ? `${escapeTelegramHtml(actor)} — ` : ""}${escapeTelegramHtml(event.text || "")}${when ? `\n <code>${escapeTelegramHtml(when)}</code>` : ""}`;
405
+ })
406
+ ].join("\n");
407
+ }
408
+
409
+ function formatTelegramWhoamiMarkup({ message, member = null, invites = [], sharedEnabled = false }) {
410
+ const from = message?.from || {};
411
+ const userId = String(from.id || "").trim();
412
+ const username = String(from.username || "").trim();
413
+ const displayName = [from.first_name, from.last_name].filter(Boolean).join(" ").trim() || username || userId;
414
+ const pending = invites.filter((invite) => String(invite.memberId || "").trim() === userId);
415
+ const lines = [
416
+ "<b>Telegram identity</b>",
417
+ `name: <code>${escapeTelegramHtml(displayName)}</code>`,
418
+ `user id: <code>${escapeTelegramHtml(userId)}</code>`,
419
+ `username: <code>${escapeTelegramHtml(username || "none")}</code>`
420
+ ];
421
+ if (!sharedEnabled) {
422
+ lines.push("shared room: <code>off</code>");
423
+ lines.push("This project is not shared yet. Start with <code>waterbrother project share</code>.");
424
+ return lines.join("\n");
425
+ }
426
+ lines.push(`shared member: <code>${member ? "yes" : "no"}</code>`);
427
+ if (member) {
428
+ lines.push(`role: <code>${escapeTelegramHtml(member.role || "editor")}</code>`);
429
+ }
430
+ if (pending.length) {
431
+ lines.push("<b>Pending invites you can accept</b>");
432
+ lines.push(...pending.map((invite) => `• <code>${escapeTelegramHtml(invite.id)}</code> <i>(${escapeTelegramHtml(invite.role || "editor")})</i>`));
433
+ lines.push("Use <code>/accept-invite &lt;invite-id&gt;</code> to join the shared room.");
434
+ } else if (!member) {
435
+ lines.push("No pending Waterbrother room invite for this Telegram user id.");
436
+ }
437
+ return lines.join("\n");
438
+ }
439
+
391
440
  function formatTelegramTasksMarkup(tasks = []) {
392
441
  if (!tasks.length) {
393
442
  return "<b>Shared tasks</b>\n• none";
@@ -1152,6 +1201,20 @@ class TelegramGateway {
1152
1201
  return;
1153
1202
  }
1154
1203
 
1204
+ if (text === "/whoami") {
1205
+ const { project } = await this.bindSharedRoomForMessage(message, sessionId);
1206
+ const invites = project?.enabled ? await listSharedInvites(sessionCwd).catch(() => []) : [];
1207
+ const member = Array.isArray(project?.members)
1208
+ ? project.members.find((entry) => String(entry?.id || "").trim() === userId) || null
1209
+ : null;
1210
+ await this.sendMessage(
1211
+ message.chat.id,
1212
+ formatTelegramWhoamiMarkup({ message, member, invites, sharedEnabled: project?.enabled === true }),
1213
+ message.message_id
1214
+ );
1215
+ return;
1216
+ }
1217
+
1155
1218
  if (text === "/status") {
1156
1219
  await this.sendMessage(
1157
1220
  message.chat.id,
@@ -1195,6 +1258,21 @@ class TelegramGateway {
1195
1258
  return;
1196
1259
  }
1197
1260
 
1261
+ if (text === "/events") {
1262
+ const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1263
+ if (!project?.enabled) {
1264
+ await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
1265
+ return;
1266
+ }
1267
+ try {
1268
+ const events = await listSharedEvents(session.cwd || this.cwd, 12);
1269
+ await this.sendMessage(message.chat.id, formatTelegramEventsMarkup(events), message.message_id);
1270
+ } catch (error) {
1271
+ await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
1272
+ }
1273
+ return;
1274
+ }
1275
+
1198
1276
  if (text === "/tasks") {
1199
1277
  const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
1200
1278
  if (!project?.enabled) {
@@ -258,6 +258,9 @@ export function resolveLocalConceptQuestion(text = "", manifest = {}) {
258
258
  const mentionsRoundtable = /\bround[\s-]?table\b/.test(lower);
259
259
  const asksWhatIs = /^(what('| i)?s|what is|tell me about|explain)\b/.test(lower) || /\bwhat is\b/.test(lower);
260
260
  const asksHowToUseRoundtable = mentionsRoundtable && /\b(how do i|how to|add to|join|use|set up|enable|create)\b/.test(lower);
261
+ const asksHowToInvitePartner = /\b(invite|add)\b/.test(lower) && /\b(partner|teammate|teammates|teammember|collaborator|member)\b/.test(lower);
262
+ const asksHowToShareProject = /\b(how do i|how to|share)\b/.test(lower) && /\b(this project|project)\b/.test(lower);
263
+ const asksHowToMakeProject = /\b(how do i|how to|make|create|start)\b/.test(lower) && /\b(new project|project)\b/.test(lower);
261
264
  const sourcesFor = (...keys) => keys.map((key) => manifest.sourceHints?.[key]).filter(Boolean);
262
265
  const withSources = (lines, sources = []) => [
263
266
  ...lines,
@@ -280,6 +283,35 @@ export function resolveLocalConceptQuestion(text = "", manifest = {}) {
280
283
  ], sources);
281
284
  }
282
285
 
286
+ if (asksHowToInvitePartner) {
287
+ const sources = sourcesFor("roundtableDocs", "telegramDocs", "sharedState");
288
+ return withSources([
289
+ "To collaborate with a Telegram partner, there are two separate steps.",
290
+ "First, add the human to the Telegram group/chat itself using normal Telegram controls.",
291
+ "Second, add them to the Waterbrother shared room with `waterbrother room invite <member-id> [owner|editor|observer]` or `/invite <user-id> [owner|editor|observer]`.",
292
+ "Then the invited person accepts with `waterbrother room invite accept <invite-id>` or `/accept-invite <invite-id>`.",
293
+ "If this repo is not shared yet, start with `waterbrother project share`."
294
+ ], sources);
295
+ }
296
+
297
+ if (asksHowToShareProject) {
298
+ const sources = sourcesFor("roundtableDocs", "sharedState");
299
+ return withSources([
300
+ "To share the current project, run `waterbrother project share` in the repo.",
301
+ "In an interactive terminal, Waterbrother will guide room mode, room runtime profile, and the first invite.",
302
+ "After that, use `waterbrother room status`, `waterbrother room invite <member-id> [owner|editor|observer]`, and `waterbrother room mode <chat|plan|execute>` to manage collaboration."
303
+ ], sources);
304
+ }
305
+
306
+ if (asksHowToMakeProject) {
307
+ const sources = sourcesFor("roundtableDocs", "telegramDocs", "readme");
308
+ return withSources([
309
+ "To make a new project from the TUI, use `/new-project <name>`.",
310
+ "That creates a folder on `~/Desktop/<name>`, switches the live session into it, and now asks whether the new project should be shared.",
311
+ "From Telegram, use `/new-project <name>` to create the folder and switch the linked session there. Then use `waterbrother project share` or the TUI sharing prompt if you want collaboration enabled."
312
+ ], sources);
313
+ }
314
+
283
315
  if ((/\bshared project\b/.test(lower) || /\bshared room\b/.test(lower) || /\broom mode\b/.test(lower)) && asksWhatIs) {
284
316
  const sources = sourcesFor("sharedState", "roundtableDocs", "roundtable");
285
317
  return withSources([
@@ -456,6 +456,17 @@ export async function listSharedInvites(cwd) {
456
456
  return project.pendingInvites || [];
457
457
  }
458
458
 
459
+ export async function listSharedEvents(cwd, limit = 12) {
460
+ const project = await loadSharedProject(cwd);
461
+ requireSharedProject(project);
462
+ const max = Number.isFinite(Number(limit)) && Number(limit) > 0 ? Math.max(1, Math.floor(Number(limit))) : 12;
463
+ return Array.isArray(project.recentEvents)
464
+ ? [...project.recentEvents]
465
+ .sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")))
466
+ .slice(0, max)
467
+ : [];
468
+ }
469
+
459
470
  export async function upsertSharedMember(cwd, member = {}, options = {}) {
460
471
  const existing = await loadSharedProject(cwd);
461
472
  requireOwner(existing, options.actorId);