@tritard/waterbrother 0.16.41 → 0.16.43
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 +3 -1
- package/package.json +1 -1
- package/src/cli.js +28 -1
- package/src/gateway.js +239 -22
- package/src/self-awareness.js +309 -118
- package/src/shared-project.js +11 -0
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>`
|
|
@@ -295,7 +296,8 @@ Current Telegram behavior:
|
|
|
295
296
|
- pending pairings are explicit and expire automatically after 12 hours unless approved
|
|
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
|
-
- 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
|
|
299
|
+
- 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
|
|
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
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,12 @@ 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" },
|
|
31
|
+
{ command: "people", description: "Show recently seen Telegram people in this chat" },
|
|
30
32
|
{ command: "cwd", description: "Show the current remote working directory" },
|
|
31
33
|
{ command: "runtime", description: "Show active runtime status" },
|
|
32
34
|
{ command: "room", description: "Show shared room status" },
|
|
35
|
+
{ command: "events", description: "Show recent shared room events" },
|
|
33
36
|
{ command: "members", description: "List shared room members" },
|
|
34
37
|
{ command: "invites", description: "List pending shared room invites" },
|
|
35
38
|
{ command: "tasks", description: "List shared Roundtable tasks" },
|
|
@@ -204,12 +207,15 @@ function buildRemoteHelp() {
|
|
|
204
207
|
"<code>/state</code> show current Waterbrother self-awareness state",
|
|
205
208
|
"<code>/run <prompt></code> execute an explicit remote request",
|
|
206
209
|
"<code>/status</code> show the current linked remote session",
|
|
210
|
+
"<code>/whoami</code> show your Telegram identity and room membership",
|
|
211
|
+
"<code>/people</code> list recent Telegram users in this chat for easier invites",
|
|
207
212
|
"<code>/cwd</code> show the current remote working directory",
|
|
208
213
|
"<code>/use <path></code> switch the linked session to another directory",
|
|
209
214
|
"<code>/desktop</code> switch the linked session to <code>~/Desktop</code>",
|
|
210
215
|
"<code>/new-project <name></code> create a folder on Desktop and switch into it",
|
|
211
216
|
"<code>/runtime</code> show active provider/model/runtime state",
|
|
212
217
|
"<code>/room</code> show shared project room status",
|
|
218
|
+
"<code>/events</code> show recent shared room events",
|
|
213
219
|
"<code>/members</code> list shared project members",
|
|
214
220
|
"<code>/tasks</code> list shared project tasks",
|
|
215
221
|
"<code>/task add <text></code> add a shared Roundtable task",
|
|
@@ -220,7 +226,8 @@ function buildRemoteHelp() {
|
|
|
220
226
|
"<code>/task history <id></code> show task history and comments",
|
|
221
227
|
"<code>/room-runtime</code> or <code>/room-runtime <name|clear></code> inspect or set the shared room runtime profile",
|
|
222
228
|
"<code>/invites</code> list pending shared invites",
|
|
223
|
-
"<code>/invite <user-id> [owner|editor|observer]</code> create a pending shared-project invite",
|
|
229
|
+
"<code>/invite <user-id|@username> [owner|editor|observer]</code> create a pending shared-project invite",
|
|
230
|
+
"reply to a teammate and use <code>/invite [owner|editor|observer]</code> to invite that person directly",
|
|
224
231
|
"<code>/accept-invite <invite-id></code> accept your own pending invite",
|
|
225
232
|
"<code>/approve-invite <invite-id></code> approve a pending invite",
|
|
226
233
|
"<code>/reject-invite <invite-id></code> reject a pending invite",
|
|
@@ -388,6 +395,79 @@ function formatTelegramInvitesMarkup(invites = []) {
|
|
|
388
395
|
].join("\n");
|
|
389
396
|
}
|
|
390
397
|
|
|
398
|
+
function formatTelegramEventsMarkup(events = []) {
|
|
399
|
+
if (!events.length) {
|
|
400
|
+
return "<b>Recent room events</b>\n• none";
|
|
401
|
+
}
|
|
402
|
+
return [
|
|
403
|
+
"<b>Recent room events</b>",
|
|
404
|
+
...events.map((event) => {
|
|
405
|
+
const actor = String(event.actorName || event.actorId || "").trim();
|
|
406
|
+
const when = String(event.createdAt || "").trim();
|
|
407
|
+
return `• ${actor ? `${escapeTelegramHtml(actor)} — ` : ""}${escapeTelegramHtml(event.text || "")}${when ? `\n <code>${escapeTelegramHtml(when)}</code>` : ""}`;
|
|
408
|
+
})
|
|
409
|
+
].join("\n");
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
function formatTelegramWhoamiMarkup({ message, member = null, invites = [], sharedEnabled = false }) {
|
|
413
|
+
const from = message?.from || {};
|
|
414
|
+
const userId = String(from.id || "").trim();
|
|
415
|
+
const username = String(from.username || "").trim();
|
|
416
|
+
const displayName = [from.first_name, from.last_name].filter(Boolean).join(" ").trim() || username || userId;
|
|
417
|
+
const pending = invites.filter((invite) => String(invite.memberId || "").trim() === userId);
|
|
418
|
+
const lines = [
|
|
419
|
+
"<b>Telegram identity</b>",
|
|
420
|
+
`name: <code>${escapeTelegramHtml(displayName)}</code>`,
|
|
421
|
+
`user id: <code>${escapeTelegramHtml(userId)}</code>`,
|
|
422
|
+
`username: <code>${escapeTelegramHtml(username || "none")}</code>`
|
|
423
|
+
];
|
|
424
|
+
if (!sharedEnabled) {
|
|
425
|
+
lines.push("shared room: <code>off</code>");
|
|
426
|
+
lines.push("This project is not shared yet. Start with <code>waterbrother project share</code>.");
|
|
427
|
+
return lines.join("\n");
|
|
428
|
+
}
|
|
429
|
+
lines.push(`shared member: <code>${member ? "yes" : "no"}</code>`);
|
|
430
|
+
if (member) {
|
|
431
|
+
lines.push(`role: <code>${escapeTelegramHtml(member.role || "editor")}</code>`);
|
|
432
|
+
}
|
|
433
|
+
if (pending.length) {
|
|
434
|
+
lines.push("<b>Pending invites you can accept</b>");
|
|
435
|
+
lines.push(...pending.map((invite) => `• <code>${escapeTelegramHtml(invite.id)}</code> <i>(${escapeTelegramHtml(invite.role || "editor")})</i>`));
|
|
436
|
+
lines.push("Use <code>/accept-invite <invite-id></code> to join the shared room.");
|
|
437
|
+
} else {
|
|
438
|
+
lines.push("No pending Waterbrother room invite for this Telegram user id.");
|
|
439
|
+
}
|
|
440
|
+
lines.push("Use <code>/people</code> in a shared chat to see recent Telegram ids and usernames you can invite.");
|
|
441
|
+
return lines.join("\n");
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
function formatTelegramPeopleMarkup({ people = [], project = null }) {
|
|
445
|
+
if (!people.length) {
|
|
446
|
+
return [
|
|
447
|
+
"<b>Recent Telegram people</b>",
|
|
448
|
+
"• none",
|
|
449
|
+
"Talk in the shared chat first, then run <code>/people</code> again."
|
|
450
|
+
].join("\n");
|
|
451
|
+
}
|
|
452
|
+
const members = Array.isArray(project?.members) ? project.members : [];
|
|
453
|
+
const pendingInvites = Array.isArray(project?.pendingInvites) ? project.pendingInvites : [];
|
|
454
|
+
return [
|
|
455
|
+
"<b>Recent Telegram people</b>",
|
|
456
|
+
...people.map((person) => {
|
|
457
|
+
const member = members.find((entry) => String(entry?.id || "").trim() === person.userId);
|
|
458
|
+
const pendingInvite = pendingInvites.find((entry) => String(entry?.memberId || "").trim() === person.userId);
|
|
459
|
+
const bits = [`<code>${escapeTelegramHtml(person.userId)}</code>`];
|
|
460
|
+
if (person.usernameHandle) bits.push(`@${escapeTelegramHtml(person.usernameHandle)}`);
|
|
461
|
+
bits.push(escapeTelegramHtml(person.displayName || person.userId));
|
|
462
|
+
if (member?.role) bits.push(`<i>member:${escapeTelegramHtml(member.role)}</i>`);
|
|
463
|
+
else if (pendingInvite?.id) bits.push(`<i>pending:${escapeTelegramHtml(pendingInvite.id)}</i>`);
|
|
464
|
+
else if (person.paired) bits.push("<i>paired</i>");
|
|
465
|
+
return `• ${bits.join(" ")}`;
|
|
466
|
+
}),
|
|
467
|
+
"Invite by id, by @username, or by replying to a message with <code>/invite [role]</code>."
|
|
468
|
+
].join("\n");
|
|
469
|
+
}
|
|
470
|
+
|
|
391
471
|
function formatTelegramTasksMarkup(tasks = []) {
|
|
392
472
|
if (!tasks.length) {
|
|
393
473
|
return "<b>Shared tasks</b>\n• none";
|
|
@@ -484,12 +564,15 @@ function suggestTaskText(text = "") {
|
|
|
484
564
|
|
|
485
565
|
function parseInviteCommand(text) {
|
|
486
566
|
const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
|
|
487
|
-
|
|
567
|
+
let target = String(parts[1] || "").trim();
|
|
488
568
|
let role = "editor";
|
|
489
|
-
if (
|
|
569
|
+
if (target && ["owner", "editor", "observer"].includes(target.toLowerCase())) {
|
|
570
|
+
role = target.toLowerCase();
|
|
571
|
+
target = "";
|
|
572
|
+
} else if (parts[2] && ["owner", "editor", "observer"].includes(String(parts[2]).toLowerCase())) {
|
|
490
573
|
role = String(parts[2]).toLowerCase();
|
|
491
574
|
}
|
|
492
|
-
return {
|
|
575
|
+
return { target, role };
|
|
493
576
|
}
|
|
494
577
|
|
|
495
578
|
function extractRetryDelayMs(error, attempt) {
|
|
@@ -674,12 +757,100 @@ class TelegramGateway {
|
|
|
674
757
|
await saveGatewayState("telegram", this.state);
|
|
675
758
|
}
|
|
676
759
|
|
|
760
|
+
describeTelegramUser(from = {}) {
|
|
761
|
+
const userId = String(from?.id || "").trim();
|
|
762
|
+
const usernameHandle = String(from?.username || "").trim();
|
|
763
|
+
const displayName = [from?.first_name, from?.last_name].filter(Boolean).join(" ").trim() || usernameHandle || userId;
|
|
764
|
+
return { userId, usernameHandle, displayName };
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
listKnownChatPeople(message) {
|
|
768
|
+
const chatId = String(message?.chat?.id || "").trim();
|
|
769
|
+
const byId = new Map();
|
|
770
|
+
const upsert = (person = {}) => {
|
|
771
|
+
const userId = String(person.userId || person.id || "").trim();
|
|
772
|
+
if (!userId) return;
|
|
773
|
+
const existing = byId.get(userId) || { userId, usernameHandle: "", displayName: userId, paired: false };
|
|
774
|
+
byId.set(userId, {
|
|
775
|
+
...existing,
|
|
776
|
+
...person,
|
|
777
|
+
userId,
|
|
778
|
+
usernameHandle: String(person.usernameHandle || existing.usernameHandle || "").trim(),
|
|
779
|
+
displayName: String(person.displayName || existing.displayName || userId).trim() || userId,
|
|
780
|
+
paired: person.paired === true || existing.paired === true
|
|
781
|
+
});
|
|
782
|
+
};
|
|
783
|
+
|
|
784
|
+
for (const [userId, peer] of Object.entries(this.state.peers || {})) {
|
|
785
|
+
if (String(peer?.chatId || "").trim() !== chatId) continue;
|
|
786
|
+
upsert({
|
|
787
|
+
userId,
|
|
788
|
+
usernameHandle: String(peer?.usernameHandle || "").trim(),
|
|
789
|
+
displayName: String(peer?.displayName || peer?.username || userId).trim(),
|
|
790
|
+
paired: true,
|
|
791
|
+
lastSeenAt: String(peer?.lastSeenAt || "").trim()
|
|
792
|
+
});
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
for (const [userId, pending] of Object.entries(this.state.pendingPairings || {})) {
|
|
796
|
+
if (String(pending?.chatId || "").trim() !== chatId) continue;
|
|
797
|
+
upsert({
|
|
798
|
+
userId,
|
|
799
|
+
usernameHandle: String(pending?.usernameHandle || "").trim(),
|
|
800
|
+
displayName: String(pending?.displayName || pending?.username || userId).trim(),
|
|
801
|
+
paired: false,
|
|
802
|
+
lastSeenAt: String(pending?.lastSeenAt || pending?.firstSeenAt || "").trim()
|
|
803
|
+
});
|
|
804
|
+
}
|
|
805
|
+
|
|
806
|
+
const current = this.describeTelegramUser(message?.from || {});
|
|
807
|
+
if (current.userId) upsert({ ...current, paired: this.isAuthorizedUser(message?.from) === true });
|
|
808
|
+
const replied = this.describeTelegramUser(message?.reply_to_message?.from || {});
|
|
809
|
+
if (replied.userId) upsert({ ...replied, paired: this.isAuthorizedUser(message?.reply_to_message?.from) === true });
|
|
810
|
+
|
|
811
|
+
return [...byId.values()].sort((a, b) => {
|
|
812
|
+
const aSeen = Date.parse(a.lastSeenAt || "") || 0;
|
|
813
|
+
const bSeen = Date.parse(b.lastSeenAt || "") || 0;
|
|
814
|
+
return bSeen - aSeen || a.displayName.localeCompare(b.displayName);
|
|
815
|
+
});
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
resolveInviteTarget(message, target) {
|
|
819
|
+
const value = String(target || "").trim();
|
|
820
|
+
const known = this.listKnownChatPeople(message);
|
|
821
|
+
const replied = this.describeTelegramUser(message?.reply_to_message?.from || {});
|
|
822
|
+
|
|
823
|
+
if (!value) {
|
|
824
|
+
if (replied.userId) {
|
|
825
|
+
const match = known.find((person) => person.userId === replied.userId) || replied;
|
|
826
|
+
return { userId: replied.userId, displayName: match.displayName || replied.displayName };
|
|
827
|
+
}
|
|
828
|
+
throw new Error("Usage: /invite <user-id|@username> [owner|editor|observer], or reply to someone with /invite [role]");
|
|
829
|
+
}
|
|
830
|
+
|
|
831
|
+
if (/^\d+$/.test(value)) {
|
|
832
|
+
const match = known.find((person) => person.userId === value);
|
|
833
|
+
return { userId: value, displayName: match?.displayName || value };
|
|
834
|
+
}
|
|
835
|
+
|
|
836
|
+
const normalized = value.replace(/^@/, "").trim().toLowerCase();
|
|
837
|
+
const byUsername = known.filter((person) => String(person.usernameHandle || "").trim().toLowerCase() === normalized);
|
|
838
|
+
if (byUsername.length === 1) {
|
|
839
|
+
return { userId: byUsername[0].userId, displayName: byUsername[0].displayName || byUsername[0].userId };
|
|
840
|
+
}
|
|
841
|
+
if (byUsername.length > 1) {
|
|
842
|
+
throw new Error(`Multiple Telegram users matched @${normalized}. Use /people and invite by numeric id.`);
|
|
843
|
+
}
|
|
844
|
+
throw new Error(`Unknown Telegram user reference: ${value}. Use /people to list recent users in this chat.`);
|
|
845
|
+
}
|
|
846
|
+
|
|
677
847
|
async addPendingPairing(message) {
|
|
678
|
-
const userId =
|
|
679
|
-
const username = [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim();
|
|
848
|
+
const { userId, usernameHandle, displayName } = this.describeTelegramUser(message?.from || {});
|
|
680
849
|
this.state.pendingPairings[userId] = {
|
|
681
850
|
userId,
|
|
682
|
-
username,
|
|
851
|
+
username: displayName,
|
|
852
|
+
usernameHandle,
|
|
853
|
+
displayName,
|
|
683
854
|
chatId: String(message.chat.id),
|
|
684
855
|
firstSeenAt: this.state.pendingPairings[userId]?.firstSeenAt || new Date().toISOString(),
|
|
685
856
|
lastSeenAt: new Date().toISOString(),
|
|
@@ -714,14 +885,15 @@ class TelegramGateway {
|
|
|
714
885
|
}
|
|
715
886
|
|
|
716
887
|
async ensurePeerSession(message) {
|
|
717
|
-
const userId =
|
|
718
|
-
const username = [message?.from?.username, message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim();
|
|
888
|
+
const { userId, usernameHandle, displayName } = this.describeTelegramUser(message?.from || {});
|
|
719
889
|
const existing = this.getPeerState(userId);
|
|
720
890
|
if (existing?.sessionId) {
|
|
721
891
|
this.state.peers[userId] = {
|
|
722
892
|
...existing,
|
|
723
893
|
chatId: String(message.chat.id),
|
|
724
|
-
username,
|
|
894
|
+
username: displayName,
|
|
895
|
+
usernameHandle,
|
|
896
|
+
displayName,
|
|
725
897
|
sessions: upsertSessionHistory(existing.sessions, existing.sessionId),
|
|
726
898
|
lastSeenAt: new Date().toISOString(),
|
|
727
899
|
lastMessageId: message.message_id
|
|
@@ -744,7 +916,9 @@ class TelegramGateway {
|
|
|
744
916
|
this.state.peers[userId] = {
|
|
745
917
|
sessionId: session.id,
|
|
746
918
|
chatId: String(message.chat.id),
|
|
747
|
-
username,
|
|
919
|
+
username: displayName,
|
|
920
|
+
usernameHandle,
|
|
921
|
+
displayName,
|
|
748
922
|
sessions: upsertSessionHistory([], session.id),
|
|
749
923
|
linkedAt: new Date().toISOString(),
|
|
750
924
|
lastSeenAt: new Date().toISOString(),
|
|
@@ -773,7 +947,9 @@ class TelegramGateway {
|
|
|
773
947
|
...previous,
|
|
774
948
|
sessionId: session.id,
|
|
775
949
|
chatId: String(message.chat.id),
|
|
776
|
-
username:
|
|
950
|
+
username: this.describeTelegramUser(message?.from || {}).displayName,
|
|
951
|
+
usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
|
|
952
|
+
displayName: this.describeTelegramUser(message?.from || {}).displayName,
|
|
777
953
|
sessions: upsertSessionHistory(previous.sessions, session.id),
|
|
778
954
|
linkedAt: new Date().toISOString(),
|
|
779
955
|
lastSeenAt: new Date().toISOString(),
|
|
@@ -804,7 +980,9 @@ class TelegramGateway {
|
|
|
804
980
|
...previous,
|
|
805
981
|
sessionId: session.id,
|
|
806
982
|
chatId: String(message.chat.id),
|
|
807
|
-
username:
|
|
983
|
+
username: this.describeTelegramUser(message?.from || {}).displayName,
|
|
984
|
+
usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
|
|
985
|
+
displayName: this.describeTelegramUser(message?.from || {}).displayName,
|
|
808
986
|
sessions: upsertSessionHistory(previous.sessions, session.id),
|
|
809
987
|
linkedAt: previous.linkedAt || new Date().toISOString(),
|
|
810
988
|
lastSeenAt: new Date().toISOString(),
|
|
@@ -928,7 +1106,9 @@ class TelegramGateway {
|
|
|
928
1106
|
id: requestId,
|
|
929
1107
|
chatId: String(message.chat.id),
|
|
930
1108
|
userId: String(message?.from?.id || ""),
|
|
931
|
-
username:
|
|
1109
|
+
username: this.describeTelegramUser(message?.from || {}).displayName,
|
|
1110
|
+
usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
|
|
1111
|
+
displayName: this.describeTelegramUser(message?.from || {}).displayName,
|
|
932
1112
|
sessionId: String(sessionId || "").trim(),
|
|
933
1113
|
text: String(promptText || "").trim(),
|
|
934
1114
|
runtimeProfile: String(project?.runtimeProfile || "").trim(),
|
|
@@ -1152,6 +1332,30 @@ class TelegramGateway {
|
|
|
1152
1332
|
return;
|
|
1153
1333
|
}
|
|
1154
1334
|
|
|
1335
|
+
if (text === "/whoami") {
|
|
1336
|
+
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1337
|
+
const invites = project?.enabled ? await listSharedInvites(sessionCwd).catch(() => []) : [];
|
|
1338
|
+
const member = Array.isArray(project?.members)
|
|
1339
|
+
? project.members.find((entry) => String(entry?.id || "").trim() === userId) || null
|
|
1340
|
+
: null;
|
|
1341
|
+
await this.sendMessage(
|
|
1342
|
+
message.chat.id,
|
|
1343
|
+
formatTelegramWhoamiMarkup({ message, member, invites, sharedEnabled: project?.enabled === true }),
|
|
1344
|
+
message.message_id
|
|
1345
|
+
);
|
|
1346
|
+
return;
|
|
1347
|
+
}
|
|
1348
|
+
|
|
1349
|
+
if (text === "/people") {
|
|
1350
|
+
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1351
|
+
await this.sendMessage(
|
|
1352
|
+
message.chat.id,
|
|
1353
|
+
formatTelegramPeopleMarkup({ people: this.listKnownChatPeople(message), project }),
|
|
1354
|
+
message.message_id
|
|
1355
|
+
);
|
|
1356
|
+
return;
|
|
1357
|
+
}
|
|
1358
|
+
|
|
1155
1359
|
if (text === "/status") {
|
|
1156
1360
|
await this.sendMessage(
|
|
1157
1361
|
message.chat.id,
|
|
@@ -1195,6 +1399,21 @@ class TelegramGateway {
|
|
|
1195
1399
|
return;
|
|
1196
1400
|
}
|
|
1197
1401
|
|
|
1402
|
+
if (text === "/events") {
|
|
1403
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1404
|
+
if (!project?.enabled) {
|
|
1405
|
+
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
1406
|
+
return;
|
|
1407
|
+
}
|
|
1408
|
+
try {
|
|
1409
|
+
const events = await listSharedEvents(session.cwd || this.cwd, 12);
|
|
1410
|
+
await this.sendMessage(message.chat.id, formatTelegramEventsMarkup(events), message.message_id);
|
|
1411
|
+
} catch (error) {
|
|
1412
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1413
|
+
}
|
|
1414
|
+
return;
|
|
1415
|
+
}
|
|
1416
|
+
|
|
1198
1417
|
if (text === "/tasks") {
|
|
1199
1418
|
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1200
1419
|
if (!project?.enabled) {
|
|
@@ -1316,20 +1535,18 @@ class TelegramGateway {
|
|
|
1316
1535
|
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
1317
1536
|
return;
|
|
1318
1537
|
}
|
|
1319
|
-
const {
|
|
1320
|
-
if (!nextUserId) {
|
|
1321
|
-
await this.sendMessage(message.chat.id, "Usage: /invite <user-id> [owner|editor|observer]", message.message_id);
|
|
1322
|
-
return;
|
|
1323
|
-
}
|
|
1538
|
+
const { target, role } = parseInviteCommand(text);
|
|
1324
1539
|
try {
|
|
1540
|
+
const inviteTarget = this.resolveInviteTarget(message, target);
|
|
1325
1541
|
const result = await createSharedInvite(
|
|
1326
1542
|
session.cwd || this.cwd,
|
|
1327
|
-
{ id:
|
|
1543
|
+
{ id: inviteTarget.userId, role, name: inviteTarget.displayName || inviteTarget.userId, paired: true },
|
|
1328
1544
|
{ actorId: userId, actorName: peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || userId }
|
|
1329
1545
|
);
|
|
1330
1546
|
await this.sendMessage(
|
|
1331
1547
|
message.chat.id,
|
|
1332
|
-
`Created pending invite <code>${escapeTelegramHtml(result.invite.id)}</code> for <code>${escapeTelegramHtml(result.invite.memberId)}</code> <i>(${escapeTelegramHtml(result.invite.role)})</i
|
|
1548
|
+
`Created pending invite <code>${escapeTelegramHtml(result.invite.id)}</code> for <code>${escapeTelegramHtml(result.invite.memberId)}</code> <i>(${escapeTelegramHtml(result.invite.role)})</i>
|
|
1549
|
+
Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTelegramHtml(result.invite.id)}</code>.`,
|
|
1333
1550
|
message.message_id
|
|
1334
1551
|
);
|
|
1335
1552
|
} catch (error) {
|
package/src/self-awareness.js
CHANGED
|
@@ -98,6 +98,223 @@ function buildProjectMapSummary({ entries = [], packageJson = null, sharedProjec
|
|
|
98
98
|
};
|
|
99
99
|
}
|
|
100
100
|
|
|
101
|
+
function collectSources(...groups) {
|
|
102
|
+
return [...new Set(groups.flat().filter(Boolean))];
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function commandEntry(id, summary, commands = [], options = {}) {
|
|
106
|
+
return {
|
|
107
|
+
id,
|
|
108
|
+
summary,
|
|
109
|
+
commands,
|
|
110
|
+
surfaces: options.surfaces || ["shell", "tui", "telegram"],
|
|
111
|
+
keywords: options.keywords || [id],
|
|
112
|
+
sources: options.sources || []
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function capabilityEntry(id, summary, options = {}) {
|
|
117
|
+
return {
|
|
118
|
+
id,
|
|
119
|
+
summary,
|
|
120
|
+
keywords: options.keywords || [id],
|
|
121
|
+
sources: options.sources || []
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function workflowEntry(id, summary, steps = [], options = {}) {
|
|
126
|
+
return {
|
|
127
|
+
id,
|
|
128
|
+
summary,
|
|
129
|
+
steps,
|
|
130
|
+
keywords: options.keywords || [id],
|
|
131
|
+
sources: options.sources || []
|
|
132
|
+
};
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function buildCommandRegistry({ sourceHints = {}, sharedRoom = {} } = {}) {
|
|
136
|
+
return [
|
|
137
|
+
commandEntry("share-project", "Enable shared-project mode in the current repo and create Roundtable state files.", [
|
|
138
|
+
"waterbrother project share",
|
|
139
|
+
"/share-project"
|
|
140
|
+
], { keywords: ["share project", "shared project", "enable roundtable", "enable shared room"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
|
|
141
|
+
commandEntry("room-status", "Inspect the shared room, recent events, members, invites, tasks, and runtime.", [
|
|
142
|
+
"waterbrother room status",
|
|
143
|
+
"waterbrother room events",
|
|
144
|
+
"/room",
|
|
145
|
+
"/events"
|
|
146
|
+
], { keywords: ["roundtable", "shared room", "room status", "events"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
|
|
147
|
+
commandEntry("room-membership", "Manage shared-room invites and membership.", [
|
|
148
|
+
"waterbrother room invite <member-id> [owner|editor|observer]",
|
|
149
|
+
"waterbrother room invite accept <invite-id>",
|
|
150
|
+
"/invite <user-id|@username> [owner|editor|observer]",
|
|
151
|
+
"/accept-invite <invite-id>",
|
|
152
|
+
"/whoami",
|
|
153
|
+
"/people"
|
|
154
|
+
], { keywords: ["invite", "partner", "teammate", "collaborator", "member", "accept invite"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs, sourceHints.sharedState] }),
|
|
155
|
+
commandEntry("room-mode", "Set whether a shared room is chatting, planning, or executing.", [
|
|
156
|
+
"waterbrother room mode <chat|plan|execute>",
|
|
157
|
+
"/mode <chat|plan|execute>",
|
|
158
|
+
"/run <prompt>"
|
|
159
|
+
], { keywords: ["room mode", "execute mode", "plan mode", "chat mode", "group execution"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs] }),
|
|
160
|
+
commandEntry("room-operator", "Claim or release the active operator lock for shared execution.", [
|
|
161
|
+
"waterbrother room claim",
|
|
162
|
+
"waterbrother room release",
|
|
163
|
+
"/claim",
|
|
164
|
+
"/release"
|
|
165
|
+
], { keywords: ["claim", "release", "operator", "lock", "active operator"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
|
|
166
|
+
commandEntry("room-tasks", "Inspect and manage the shared Roundtable task queue.", [
|
|
167
|
+
"waterbrother room tasks",
|
|
168
|
+
"waterbrother room task add <text>",
|
|
169
|
+
"waterbrother room task assign <id> <member-id>",
|
|
170
|
+
"waterbrother room task move <id> <open|active|blocked|done>",
|
|
171
|
+
"/tasks",
|
|
172
|
+
"/task add <text>",
|
|
173
|
+
"/task assign <id> <member-id>",
|
|
174
|
+
"/task claim <id>",
|
|
175
|
+
"/task move <id> <open|active|blocked|done>"
|
|
176
|
+
], { keywords: ["task", "tasks", "backlog", "assign task", "roundtable task"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
|
|
177
|
+
commandEntry("new-project", "Create a new project and switch the live session into it.", [
|
|
178
|
+
"/new-project <name>",
|
|
179
|
+
"/desktop",
|
|
180
|
+
"/use <path>"
|
|
181
|
+
], { keywords: ["new project", "make project", "create project", "desktop project"], sources: [sourceHints.readme, sourceHints.telegramDocs] }),
|
|
182
|
+
commandEntry("runtime-profiles", "Inspect or select named runtime presets for local and shared execution.", [
|
|
183
|
+
"waterbrother runtime-profiles list",
|
|
184
|
+
"waterbrother room runtime [name|clear]",
|
|
185
|
+
"/runtime-profiles",
|
|
186
|
+
"/room-runtime <name|clear>"
|
|
187
|
+
], { keywords: ["runtime profile", "runtime profiles", "provider preset", "shared room runtime"], sources: [sourceHints.readme, sourceHints.roundtableDocs] }),
|
|
188
|
+
commandEntry("telegram-gateway", "Inspect and run the Telegram messaging gateway.", [
|
|
189
|
+
"waterbrother gateway status",
|
|
190
|
+
"waterbrother gateway run telegram",
|
|
191
|
+
"waterbrother gateway stop",
|
|
192
|
+
"/gateway"
|
|
193
|
+
], { keywords: ["telegram", "gateway", "messaging", "telegram adapter"], sources: [sourceHints.telegramDocs] }),
|
|
194
|
+
commandEntry("self-awareness", "Inspect Waterbrother's own repo/runtime understanding.", [
|
|
195
|
+
"waterbrother about",
|
|
196
|
+
"waterbrother capabilities",
|
|
197
|
+
"waterbrother project-map",
|
|
198
|
+
"waterbrother state",
|
|
199
|
+
"/about",
|
|
200
|
+
"/capabilities",
|
|
201
|
+
"/project-map",
|
|
202
|
+
"/state"
|
|
203
|
+
], { keywords: ["about", "capabilities", "state", "project map", "self awareness", "what are you"], sources: [sourceHints.readme, sourceHints.commandsDocs] })
|
|
204
|
+
];
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
function buildCapabilityRegistry({ sourceHints = {}, sharedRoom = {} } = {}) {
|
|
208
|
+
const sharedEnabled = sharedRoom.enabled === true;
|
|
209
|
+
return [
|
|
210
|
+
capabilityEntry("interactive-terminal", "Interactive terminal coding with sessions, approvals, tools, receipts, and local repo awareness.", { keywords: ["terminal", "tui", "interactive", "coding cli"], sources: [sourceHints.readme] }),
|
|
211
|
+
capabilityEntry("telegram-bridge", `Telegram messaging gateway with ${sharedEnabled ? "live TUI bridging and shared-room controls" : "pairing, remote sessions, and shared-room support when enabled"}.`, { keywords: ["telegram", "bot", "bridge", "remote control", "messaging"], sources: [sourceHints.telegramDocs] }),
|
|
212
|
+
capabilityEntry("shared-projects", `${sharedEnabled ? "Shared-project collaboration is enabled in this repo." : "Shared-project collaboration is available and currently disabled in this repo."} It uses one room, explicit roles, room modes, an operator lock, Roundtable tasks, and a shared runtime.`, { keywords: ["roundtable", "shared project", "shared room", "collaboration"], sources: [sourceHints.roundtableDocs, sourceHints.sharedState] }),
|
|
213
|
+
capabilityEntry("runtime-profiles", "Bring-your-own-model runtime switching with named runtime profiles and room-level shared runtime selection.", { keywords: ["runtime", "profiles", "provider", "model", "byom"], sources: [sourceHints.readme, sourceHints.roundtableDocs] }),
|
|
214
|
+
capabilityEntry("session-persistence", "Persisted sessions that store cwd, runtime state, and messages for local and Telegram resumes.", { keywords: ["session", "sessions", "resume", "history"], sources: [sourceHints.readme, sourceHints.sharedState] }),
|
|
215
|
+
capabilityEntry("self-awareness", "Local self-awareness generated from repo state, docs, product metadata, shared-room state, and live runtime context.", { keywords: ["self awareness", "know itself", "what are you", "harness", "code base"], sources: [sourceHints.readme, sourceHints.sharedState, sourceHints.commandsDocs] })
|
|
216
|
+
];
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
function buildWorkflowRegistry({ sourceHints = {} } = {}) {
|
|
220
|
+
return [
|
|
221
|
+
workflowEntry("invite-partner", "Add a partner to a shared Telegram collaboration room.", [
|
|
222
|
+
"Add the human to the Telegram group/chat itself using normal Telegram controls.",
|
|
223
|
+
"Enable shared-project mode with waterbrother project share if the repo is not shared yet.",
|
|
224
|
+
"Use /people or /whoami to discover Telegram ids already seen in the room.",
|
|
225
|
+
"Create a Waterbrother shared-room invite with waterbrother room invite <member-id> [owner|editor|observer] or /invite <user-id|@username> [owner|editor|observer].",
|
|
226
|
+
"Have the invited person accept with waterbrother room invite accept <invite-id> or /accept-invite <invite-id>."
|
|
227
|
+
], { keywords: ["invite partner", "invite teammate", "add collaborator", "join shared room"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs, sourceHints.sharedState] }),
|
|
228
|
+
workflowEntry("make-project", "Create a new project and optionally make it collaborative.", [
|
|
229
|
+
"Use /new-project <name> in the TUI or Telegram to create a Desktop project and switch into it.",
|
|
230
|
+
"If prompted in the TUI, choose whether the new project should be shared.",
|
|
231
|
+
"Use waterbrother project share afterward if you want to enable Roundtable on an existing repo.",
|
|
232
|
+
"Set room mode and claim the operator lock before shared execution."
|
|
233
|
+
], { keywords: ["make new project", "create project", "new project", "start project"], sources: [sourceHints.readme, sourceHints.roundtableDocs, sourceHints.telegramDocs] }),
|
|
234
|
+
workflowEntry("shared-execution", "Run shared work safely in Telegram groups.", [
|
|
235
|
+
"Use /mode execute for the shared room when the group is ready to act.",
|
|
236
|
+
"Owners or editors claim the operator lock with /claim or waterbrother room claim.",
|
|
237
|
+
"In Telegram groups, explicit execution should use /run <prompt>.",
|
|
238
|
+
"Use /events, /room, and /tasks to keep the group synchronized."
|
|
239
|
+
], { keywords: ["shared execution", "group execute", "work together in telegram", "run in group"], sources: [sourceHints.roundtableDocs, sourceHints.telegramDocs, sourceHints.sharedState] })
|
|
240
|
+
];
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
function summarizeRoomState(sharedRoom = {}) {
|
|
244
|
+
if (!sharedRoom.enabled) {
|
|
245
|
+
return "Shared room is off in this repo.";
|
|
246
|
+
}
|
|
247
|
+
const bits = [`Shared room is enabled in ${sharedRoom.roomMode || "chat"} mode.`];
|
|
248
|
+
if (sharedRoom.runtimeProfile) bits.push(`Room runtime profile: ${sharedRoom.runtimeProfile}.`);
|
|
249
|
+
bits.push(`Members: ${sharedRoom.memberCount || 0}, pending invites: ${sharedRoom.pendingInviteCount || 0}, tasks: ${sharedRoom.taskCount || 0}.`);
|
|
250
|
+
if (sharedRoom.activeOperator?.id) bits.push(`Active operator: ${sharedRoom.activeOperator.name || sharedRoom.activeOperator.id}.`);
|
|
251
|
+
return bits.join(" ");
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
function normalizeQuestion(text = "") {
|
|
255
|
+
return String(text || "")
|
|
256
|
+
.toLowerCase()
|
|
257
|
+
.replace(/[^a-z0-9\s/-]+/g, " ")
|
|
258
|
+
.replace(/\s+/g, " ")
|
|
259
|
+
.trim();
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
function scoreEntry(question, entry) {
|
|
263
|
+
const q = normalizeQuestion(question);
|
|
264
|
+
const qTokens = new Set(q.split(" ").filter(Boolean));
|
|
265
|
+
let score = 0;
|
|
266
|
+
for (const keyword of entry.keywords || []) {
|
|
267
|
+
const normalized = normalizeQuestion(keyword);
|
|
268
|
+
if (!normalized) continue;
|
|
269
|
+
if (q.includes(normalized)) score += Math.max(2, normalized.split(" ").length * 2);
|
|
270
|
+
const tokens = normalized.split(" ").filter(Boolean);
|
|
271
|
+
if (tokens.length > 1 && tokens.every((token) => qTokens.has(token))) {
|
|
272
|
+
score += tokens.length + 1;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
if (entry.id && q.includes(normalizeQuestion(entry.id))) score += 2;
|
|
276
|
+
return score;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
function findBestEntries(question, entries = [], minimumScore = 2) {
|
|
280
|
+
return entries
|
|
281
|
+
.map((entry) => ({ entry, score: scoreEntry(question, entry) }))
|
|
282
|
+
.filter((item) => item.score >= minimumScore)
|
|
283
|
+
.sort((a, b) => b.score - a.score)
|
|
284
|
+
.map((item) => item.entry);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function renderWorkflowAnswer(workflow, roomState, capability, commands) {
|
|
288
|
+
const lines = [workflow.summary, ...workflow.steps.map((step, index) => `${index + 1}. ${step}`)];
|
|
289
|
+
if (commands.length) {
|
|
290
|
+
lines.push("Relevant commands:");
|
|
291
|
+
for (const command of commands.slice(0, 2)) {
|
|
292
|
+
lines.push(...command.commands.map((value) => `- ${value}`));
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
if (roomState) lines.push(roomState);
|
|
296
|
+
if (capability) lines.push(`Context: ${capability.summary}`);
|
|
297
|
+
const sources = collectSources(workflow.sources, capability?.sources || [], ...commands.map((entry) => entry.sources || []));
|
|
298
|
+
if (sources.length) lines.push(`sources: ${sources.join(", ")}`);
|
|
299
|
+
return lines.join("\n");
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
function renderConceptAnswer({ subject, capability, commands = [], roomState, extra = [] }) {
|
|
303
|
+
const lines = [subject];
|
|
304
|
+
if (capability) lines.push(capability.summary);
|
|
305
|
+
if (roomState) lines.push(roomState);
|
|
306
|
+
if (commands.length) {
|
|
307
|
+
lines.push("Relevant commands:");
|
|
308
|
+
for (const entry of commands.slice(0, 3)) {
|
|
309
|
+
lines.push(...entry.commands.map((value) => `- ${value}`));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
if (extra.length) lines.push(...extra);
|
|
313
|
+
const sources = collectSources(capability?.sources || [], ...commands.map((entry) => entry.sources || []));
|
|
314
|
+
if (sources.length) lines.push(`sources: ${sources.join(", ")}`);
|
|
315
|
+
return lines.join("\n");
|
|
316
|
+
}
|
|
317
|
+
|
|
101
318
|
export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSession = null } = {}) {
|
|
102
319
|
const [packageJson, readme, memoryText, roundtableText, sharedProject, product, git, entries] = await Promise.all([
|
|
103
320
|
readJson(path.join(cwd, "package.json")),
|
|
@@ -117,6 +334,26 @@ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSes
|
|
|
117
334
|
};
|
|
118
335
|
|
|
119
336
|
const projectMap = buildProjectMapSummary({ entries, packageJson, sharedProject, product });
|
|
337
|
+
const sourceHints = {
|
|
338
|
+
readme: await exists(path.join(cwd, "README.md")) ? "README.md" : "",
|
|
339
|
+
projectMemory: await exists(path.join(cwd, "WATERBROTHER.md")) ? "WATERBROTHER.md" : "",
|
|
340
|
+
roundtable: await exists(path.join(cwd, "ROUNDTABLE.md")) ? "ROUNDTABLE.md" : "",
|
|
341
|
+
sharedState: sharedProject ? ".waterbrother/shared.json" : "",
|
|
342
|
+
telegramDocs: docsPresence.telegram ? "channels/telegram/index.html" : "",
|
|
343
|
+
roundtableDocs: docsPresence.roundtable ? "roundtable/index.html" : "",
|
|
344
|
+
commandsDocs: await exists(path.join(cwd, "commands", "index.html")) ? "commands/index.html" : ""
|
|
345
|
+
};
|
|
346
|
+
const sharedRoom = sharedProject
|
|
347
|
+
? {
|
|
348
|
+
enabled: true,
|
|
349
|
+
roomMode: sharedProject.roomMode || "chat",
|
|
350
|
+
runtimeProfile: sharedProject.runtimeProfile || "",
|
|
351
|
+
memberCount: Array.isArray(sharedProject.members) ? sharedProject.members.length : 0,
|
|
352
|
+
pendingInviteCount: Array.isArray(sharedProject.pendingInvites) ? sharedProject.pendingInvites.length : 0,
|
|
353
|
+
taskCount: Array.isArray(sharedProject.tasks) ? sharedProject.tasks.length : 0,
|
|
354
|
+
activeOperator: sharedProject.activeOperator || null
|
|
355
|
+
}
|
|
356
|
+
: { enabled: false };
|
|
120
357
|
const manifest = {
|
|
121
358
|
version: 1,
|
|
122
359
|
generatedAt: new Date().toISOString(),
|
|
@@ -154,17 +391,7 @@ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSes
|
|
|
154
391
|
roundtableTitle: firstHeading(roundtableText) || "",
|
|
155
392
|
projectMap
|
|
156
393
|
},
|
|
157
|
-
sharedRoom
|
|
158
|
-
? {
|
|
159
|
-
enabled: true,
|
|
160
|
-
roomMode: sharedProject.roomMode || "chat",
|
|
161
|
-
runtimeProfile: sharedProject.runtimeProfile || "",
|
|
162
|
-
memberCount: Array.isArray(sharedProject.members) ? sharedProject.members.length : 0,
|
|
163
|
-
pendingInviteCount: Array.isArray(sharedProject.pendingInvites) ? sharedProject.pendingInvites.length : 0,
|
|
164
|
-
taskCount: Array.isArray(sharedProject.tasks) ? sharedProject.tasks.length : 0,
|
|
165
|
-
activeOperator: sharedProject.activeOperator || null
|
|
166
|
-
}
|
|
167
|
-
: { enabled: false },
|
|
394
|
+
sharedRoom,
|
|
168
395
|
product: product
|
|
169
396
|
? {
|
|
170
397
|
name: product.name || "",
|
|
@@ -172,13 +399,18 @@ export async function buildSelfAwarenessManifest({ cwd, runtime = {}, currentSes
|
|
|
172
399
|
surfaceCount: Array.isArray(product.surfaces) ? product.surfaces.length : 0
|
|
173
400
|
}
|
|
174
401
|
: null,
|
|
175
|
-
sourceHints
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
402
|
+
sourceHints,
|
|
403
|
+
knowledge: {
|
|
404
|
+
harness: {
|
|
405
|
+
surface: "waterbrother",
|
|
406
|
+
runtimeSurface: "local terminal",
|
|
407
|
+
messagingSurfaces: ["telegram"],
|
|
408
|
+
liveBridge: true,
|
|
409
|
+
sharedRoomSummary: summarizeRoomState(sharedRoom)
|
|
410
|
+
},
|
|
411
|
+
commands: buildCommandRegistry({ sourceHints, sharedRoom }),
|
|
412
|
+
capabilities: buildCapabilityRegistry({ sourceHints, sharedRoom }),
|
|
413
|
+
workflows: buildWorkflowRegistry({ sourceHints })
|
|
182
414
|
}
|
|
183
415
|
};
|
|
184
416
|
|
|
@@ -202,7 +434,8 @@ export function buildSelfAwarenessMemoryBlock(manifest = {}) {
|
|
|
202
434
|
`- git branch: ${manifest.identity?.git?.branch || "none"}`,
|
|
203
435
|
`- runtime: ${manifest.runtime?.provider || "unknown"}/${manifest.runtime?.model || "unknown"}`,
|
|
204
436
|
`- shared room: ${manifest.sharedRoom?.enabled ? `${manifest.sharedRoom.roomMode || "chat"} mode` : "off"}`,
|
|
205
|
-
`- capabilities: ${(manifest.features?.capabilities || []).join(", ")}
|
|
437
|
+
`- capabilities: ${(manifest.features?.capabilities || []).join(", ")}`,
|
|
438
|
+
`- harness: ${manifest.knowledge?.harness?.sharedRoomSummary || "local terminal"}`
|
|
206
439
|
];
|
|
207
440
|
return lines.filter(Boolean).join("\n");
|
|
208
441
|
}
|
|
@@ -210,7 +443,8 @@ export function buildSelfAwarenessMemoryBlock(manifest = {}) {
|
|
|
210
443
|
export function formatCapabilitiesSummary(manifest = {}) {
|
|
211
444
|
return JSON.stringify({
|
|
212
445
|
identity: manifest.identity,
|
|
213
|
-
capabilities: manifest.features?.capabilities || [],
|
|
446
|
+
capabilities: manifest.knowledge?.capabilities || manifest.features?.capabilities || [],
|
|
447
|
+
commands: (manifest.knowledge?.commands || []).map((entry) => ({ id: entry.id, commands: entry.commands })),
|
|
214
448
|
channels: manifest.features?.channels || [],
|
|
215
449
|
sharedRoomEnabled: manifest.sharedRoom?.enabled === true
|
|
216
450
|
}, null, 2);
|
|
@@ -239,13 +473,15 @@ export function formatAboutWaterbrother(manifest = {}) {
|
|
|
239
473
|
manifest.sourceHints?.readme,
|
|
240
474
|
manifest.sourceHints?.roundtableDocs,
|
|
241
475
|
manifest.sourceHints?.telegramDocs,
|
|
242
|
-
manifest.sourceHints?.sharedState
|
|
476
|
+
manifest.sourceHints?.sharedState,
|
|
477
|
+
manifest.sourceHints?.commandsDocs
|
|
243
478
|
].filter(Boolean);
|
|
244
479
|
return [
|
|
245
480
|
`Waterbrother is ${manifest.identity?.description || "a local coding CLI."}`,
|
|
246
481
|
`repo: ${manifest.identity?.repoName || "-"}`,
|
|
247
482
|
`cwd: ${manifest.identity?.cwd || "-"}`,
|
|
248
483
|
`capabilities: ${(manifest.features?.capabilities || []).join(", ") || "-"}`,
|
|
484
|
+
`harness: ${manifest.knowledge?.harness?.sharedRoomSummary || "local terminal"}`,
|
|
249
485
|
sources.length ? `sources: ${sources.join(", ")}` : ""
|
|
250
486
|
].filter(Boolean).join("\n");
|
|
251
487
|
}
|
|
@@ -255,121 +491,76 @@ export function resolveLocalConceptQuestion(text = "", manifest = {}) {
|
|
|
255
491
|
const lower = input.toLowerCase();
|
|
256
492
|
if (!lower) return null;
|
|
257
493
|
|
|
258
|
-
const mentionsRoundtable = /\bround[\s-]?table\b/.test(lower);
|
|
259
494
|
const asksWhatIs = /^(what('| i)?s|what is|tell me about|explain)\b/.test(lower) || /\bwhat is\b/.test(lower);
|
|
260
|
-
const
|
|
261
|
-
const
|
|
262
|
-
const
|
|
263
|
-
const
|
|
264
|
-
const
|
|
265
|
-
const
|
|
266
|
-
...lines,
|
|
267
|
-
...(sources.length ? [`sources: ${sources.join(", ")}`] : [])
|
|
268
|
-
].join("\n");
|
|
269
|
-
if (mentionsRoundtable && asksWhatIs) {
|
|
270
|
-
const sources = sourcesFor("roundtableDocs", "sharedState", "roundtable");
|
|
271
|
-
return withSources([
|
|
272
|
-
"Roundtable is Waterbrother’s shared-project collaboration model.",
|
|
273
|
-
"It ties one project to one shared room with explicit room modes, member roles, operator lock, tasks, invites, and shared runtime selection."
|
|
274
|
-
], sources);
|
|
275
|
-
}
|
|
276
|
-
|
|
277
|
-
if (asksHowToUseRoundtable) {
|
|
278
|
-
const sources = sourcesFor("roundtableDocs", "sharedState", "roundtable");
|
|
279
|
-
return withSources([
|
|
280
|
-
"To add to Roundtable, first enable shared-project mode in the repo with `waterbrother project share`.",
|
|
281
|
-
"Then use `waterbrother room invite <member-id> [owner|editor|observer]` to create member invites, `waterbrother room tasks` / `waterbrother room task add <text>` to work the shared backlog, and `waterbrother room mode <chat|plan|execute>` plus `waterbrother room claim` when you want one operator to execute.",
|
|
282
|
-
"In Telegram, the equivalent shared-room commands are `/room`, `/invite`, `/tasks`, `/task add`, `/mode`, `/claim`, and `/accept-invite`."
|
|
283
|
-
], sources);
|
|
284
|
-
}
|
|
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
|
-
}
|
|
495
|
+
const asksHow = /\b(how do i|how to|use|set up|enable|create|share|join|invite|make|start|add)\b/.test(lower);
|
|
496
|
+
const asksCapabilities = /\bwhat can you do\b/.test(lower) || /\bcapabilities\b/.test(lower) || /\bwhat do you support\b/.test(lower);
|
|
497
|
+
const asksProjectMap = /\bwhat is this project\b/.test(lower) || /\bwhat repo is this\b/.test(lower) || /\bwhat codebase is this\b/.test(lower);
|
|
498
|
+
const asksIdentity = /\bwhat is waterbrother\b/.test(lower) || /\bwho are you\b/.test(lower) || /\bwhat are you\b/.test(lower);
|
|
499
|
+
const mentionsRoundtable = /\bround[\s-]?table\b/.test(lower);
|
|
500
|
+
const mentionsSharedRoom = /\bshared room\b/.test(lower) || /\bshared project\b/.test(lower);
|
|
305
501
|
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
}
|
|
502
|
+
const commands = manifest.knowledge?.commands || buildCommandRegistry({ sourceHints: manifest.sourceHints || {}, sharedRoom: manifest.sharedRoom || {} });
|
|
503
|
+
const capabilities = manifest.knowledge?.capabilities || buildCapabilityRegistry({ sourceHints: manifest.sourceHints || {}, sharedRoom: manifest.sharedRoom || {} });
|
|
504
|
+
const workflows = manifest.knowledge?.workflows || buildWorkflowRegistry({ sourceHints: manifest.sourceHints || {} });
|
|
505
|
+
const roomState = manifest.knowledge?.harness?.sharedRoomSummary || summarizeRoomState(manifest.sharedRoom || {});
|
|
506
|
+
const matchingWorkflows = findBestEntries(input, workflows, asksHow ? 2 : 4);
|
|
507
|
+
const matchingCommands = findBestEntries(input, commands, 2);
|
|
508
|
+
const matchingCapabilities = findBestEntries(input, capabilities, 2);
|
|
314
509
|
|
|
315
|
-
if (
|
|
316
|
-
const sources = sourcesFor("sharedState", "roundtableDocs", "roundtable");
|
|
317
|
-
return withSources([
|
|
318
|
-
"A shared project links the current repo to one collaboration room with explicit member roles, room mode, operator lock, tasks, invites, and shared runtime state.",
|
|
319
|
-
`Current room mode: ${manifest.sharedRoom?.roomMode || "chat"}. Shared enabled: ${manifest.sharedRoom?.enabled ? "yes" : "no"}.`
|
|
320
|
-
], sources);
|
|
321
|
-
}
|
|
322
|
-
|
|
323
|
-
if ((/\bwhat can you do\b/.test(lower) || /\bcapabilities\b/.test(lower) || /\bwhat do you support\b/.test(lower))) {
|
|
510
|
+
if (asksCapabilities) {
|
|
324
511
|
return formatCapabilitiesSummary(manifest);
|
|
325
512
|
}
|
|
326
513
|
|
|
327
|
-
if (
|
|
514
|
+
if (asksProjectMap) {
|
|
328
515
|
return formatProjectMap(manifest);
|
|
329
516
|
}
|
|
330
517
|
|
|
331
|
-
if (
|
|
518
|
+
if (asksIdentity) {
|
|
332
519
|
return formatAboutWaterbrother(manifest);
|
|
333
520
|
}
|
|
334
521
|
|
|
335
|
-
if (
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
if ((/\bgateway\b/.test(lower) || /\bmessaging gateway\b/.test(lower)) && asksWhatIs) {
|
|
344
|
-
const sources = sourcesFor("telegramDocs", "sharedState");
|
|
345
|
-
return withSources([
|
|
346
|
-
"The gateway is Waterbrother’s channel runtime for messaging surfaces like Telegram.",
|
|
347
|
-
"It handles pairing, routing, live TUI bridging, remote sessions, shared-room commands, and delivery state."
|
|
348
|
-
], sources);
|
|
522
|
+
if (asksWhatIs && mentionsRoundtable) {
|
|
523
|
+
return renderConceptAnswer({
|
|
524
|
+
subject: "Roundtable is Waterbrother's shared-project collaboration model.",
|
|
525
|
+
capability: matchingCapabilities[0] || capabilities.find((entry) => entry.id === "shared-projects") || null,
|
|
526
|
+
commands: matchingCommands.length ? matchingCommands : commands.filter((entry) => ["room-status", "room-membership", "room-tasks"].includes(entry.id)),
|
|
527
|
+
roomState,
|
|
528
|
+
extra: ["It ties one project to one room with explicit roles, room modes, an operator lock, Roundtable tasks, and shared runtime state."]
|
|
529
|
+
});
|
|
349
530
|
}
|
|
350
531
|
|
|
351
|
-
if (
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
532
|
+
if (asksWhatIs && mentionsSharedRoom) {
|
|
533
|
+
return renderConceptAnswer({
|
|
534
|
+
subject: "A shared project links the current repo to one collaboration room with explicit shared-room state.",
|
|
535
|
+
capability: matchingCapabilities[0] || capabilities.find((entry) => entry.id === "shared-projects") || null,
|
|
536
|
+
commands: matchingCommands.length ? matchingCommands : commands.filter((entry) => ["room-status", "room-mode", "room-operator"].includes(entry.id)),
|
|
537
|
+
roomState,
|
|
538
|
+
extra: ["That shared room carries member roles, room mode, operator lock, tasks, invites, and shared runtime state."]
|
|
539
|
+
});
|
|
357
540
|
}
|
|
358
541
|
|
|
359
|
-
if (
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
542
|
+
if (asksHow && matchingWorkflows.length) {
|
|
543
|
+
return renderWorkflowAnswer(
|
|
544
|
+
matchingWorkflows[0],
|
|
545
|
+
roomState,
|
|
546
|
+
matchingCapabilities[0] || null,
|
|
547
|
+
matchingCommands
|
|
548
|
+
);
|
|
365
549
|
}
|
|
366
550
|
|
|
367
|
-
if (
|
|
368
|
-
const
|
|
369
|
-
|
|
370
|
-
"
|
|
371
|
-
|
|
372
|
-
|
|
551
|
+
if (asksWhatIs || matchingCommands.length || matchingCapabilities.length) {
|
|
552
|
+
const subject = asksWhatIs
|
|
553
|
+
? `Local Waterbrother context for: ${input}`
|
|
554
|
+
: "Local Waterbrother context";
|
|
555
|
+
return renderConceptAnswer({
|
|
556
|
+
subject,
|
|
557
|
+
capability: matchingCapabilities[0] || null,
|
|
558
|
+
commands: matchingCommands,
|
|
559
|
+
roomState,
|
|
560
|
+
extra: manifest.runtime?.provider || manifest.runtime?.model
|
|
561
|
+
? [`Active runtime: ${manifest.runtime.provider || "unknown"}/${manifest.runtime.model || "unknown"}.`]
|
|
562
|
+
: []
|
|
563
|
+
});
|
|
373
564
|
}
|
|
374
565
|
|
|
375
566
|
return null;
|
package/src/shared-project.js
CHANGED
|
@@ -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);
|