@tritard/waterbrother 0.16.41 → 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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.41",
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) {
@@ -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);