@tritard/waterbrother 0.16.42 → 0.16.44
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 +2 -1
- package/package.json +1 -1
- package/src/cli.js +27 -3
- package/src/gateway.js +161 -22
- package/src/self-awareness.js +309 -118
package/README.md
CHANGED
|
@@ -204,6 +204,7 @@ waterbrother "Search the web for the latest model provider docs about tool calli
|
|
|
204
204
|
/runtime-profiles
|
|
205
205
|
/model anthropic/claude-sonnet-4-20250514
|
|
206
206
|
/models
|
|
207
|
+
/models all
|
|
207
208
|
|
|
208
209
|
# config
|
|
209
210
|
waterbrother config set provider anthropic --scope project
|
|
@@ -296,7 +297,7 @@ Current Telegram behavior:
|
|
|
296
297
|
- pending pairings are explicit and expire automatically after 12 hours unless approved
|
|
297
298
|
- paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
|
|
298
299
|
- Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
|
|
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
|
+
- 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
301
|
- Telegram now supports `/whoami` for Telegram-id and shared-room membership visibility, plus `/events` for recent shared-room activity
|
|
301
302
|
- `/room` now includes pending invite count plus task ownership summaries
|
|
302
303
|
- local `/status` now includes `sharedRoom` with pending invites, task ownership summary, and recent shared-room event activity
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -209,8 +209,8 @@ const INTERACTIVE_COMMANDS = [
|
|
|
209
209
|
{ name: "/provider", description: "Show active provider" },
|
|
210
210
|
{ name: "/provider <id>", description: "Switch provider for this session" },
|
|
211
211
|
{ name: "/providers", description: "Select provider from list" },
|
|
212
|
-
{ name: "/model", description: "Show current model" },
|
|
213
|
-
{ name: "/model <id>", description: "Switch model for this session" },
|
|
212
|
+
{ name: "/model", description: "Show current model for this session" },
|
|
213
|
+
{ name: "/model <id>", description: "Switch model directly for this session" },
|
|
214
214
|
{ name: "/read <url>", description: "Read and summarize a public URL" },
|
|
215
215
|
{ name: "/search <query>", description: "Search the web and summarize results" },
|
|
216
216
|
{ name: "/open <url|index>", description: "Open a URL or last search result in browser" },
|
|
@@ -220,7 +220,8 @@ const INTERACTIVE_COMMANDS = [
|
|
|
220
220
|
{ name: "/update", description: "Pull latest code, install deps, and run checks" },
|
|
221
221
|
{ name: "/onboarding", description: "Print onboarding/API key guide" },
|
|
222
222
|
{ name: "/onboarding <telegram|discord|signal>", description: "Print messaging service onboarding" },
|
|
223
|
-
{ name: "/models", description: "Select model from
|
|
223
|
+
{ name: "/models", description: "Select model from active provider" },
|
|
224
|
+
{ name: "/models all", description: "Browse all known models across providers" },
|
|
224
225
|
{ name: "/feedback", description: "Report a bug or share feedback" },
|
|
225
226
|
{ name: "/cost", description: "Show session token usage and cost breakdown" },
|
|
226
227
|
{ name: "/diff", description: "Show git changes in the current repo" },
|
|
@@ -9854,6 +9855,7 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
9854
9855
|
if (line === "/models") {
|
|
9855
9856
|
try {
|
|
9856
9857
|
const providerSpec = getProviderSpec(context.runtime.provider);
|
|
9858
|
+
console.log(dim(`Browsing models for active provider: ${context.runtime.provider}`));
|
|
9857
9859
|
const selectedModel = await chooseRuntimeModelInteractive(agent.getModel(), context.runtime.provider, {
|
|
9858
9860
|
apiKey: context.runtime.apiKey,
|
|
9859
9861
|
baseUrl: context.runtime.baseUrl,
|
|
@@ -9868,12 +9870,34 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
9868
9870
|
model: selectedModel
|
|
9869
9871
|
});
|
|
9870
9872
|
console.log(`model set to ${nextRuntime.model}`);
|
|
9873
|
+
console.log(dim('Use /models all to browse the cross-provider catalog.'));
|
|
9871
9874
|
} catch (error) {
|
|
9872
9875
|
console.log(`model selection canceled: ${error instanceof Error ? error.message : String(error)}`);
|
|
9873
9876
|
}
|
|
9874
9877
|
continue;
|
|
9875
9878
|
}
|
|
9876
9879
|
|
|
9880
|
+
if (line === "/models all") {
|
|
9881
|
+
try {
|
|
9882
|
+
const selectedModel = await chooseAllModelsInteractive(agent.getModel());
|
|
9883
|
+
const { config } = await loadConfigLayers(context.cwd);
|
|
9884
|
+
const nextRuntime = await applyRuntimeSelection({
|
|
9885
|
+
config,
|
|
9886
|
+
context,
|
|
9887
|
+
agent,
|
|
9888
|
+
session: currentSession,
|
|
9889
|
+
model: selectedModel
|
|
9890
|
+
});
|
|
9891
|
+
const keyStatus = runtimeRequiresApiKey(nextRuntime) && (!nextRuntime.apiKey || isLikelyPlaceholderApiKey(nextRuntime.apiKey))
|
|
9892
|
+
? ' (API key missing)'
|
|
9893
|
+
: '';
|
|
9894
|
+
console.log(`model set to ${nextRuntime.model} (${nextRuntime.provider})${keyStatus}`);
|
|
9895
|
+
} catch (error) {
|
|
9896
|
+
console.log(`model catalog selection canceled: ${error instanceof Error ? error.message : String(error)}`);
|
|
9897
|
+
}
|
|
9898
|
+
continue;
|
|
9899
|
+
}
|
|
9900
|
+
|
|
9877
9901
|
if (line === "/providers") {
|
|
9878
9902
|
try {
|
|
9879
9903
|
const selectedProvider = await chooseProviderInteractive(context.runtime.provider);
|
package/src/gateway.js
CHANGED
|
@@ -28,6 +28,7 @@ const TELEGRAM_COMMANDS = [
|
|
|
28
28
|
{ command: "run", description: "Execute an explicit remote prompt" },
|
|
29
29
|
{ command: "status", description: "Show the linked remote session" },
|
|
30
30
|
{ command: "whoami", description: "Show your Telegram identity and room membership" },
|
|
31
|
+
{ command: "people", description: "Show recently seen Telegram people in this chat" },
|
|
31
32
|
{ command: "cwd", description: "Show the current remote working directory" },
|
|
32
33
|
{ command: "runtime", description: "Show active runtime status" },
|
|
33
34
|
{ command: "room", description: "Show shared room status" },
|
|
@@ -207,6 +208,7 @@ function buildRemoteHelp() {
|
|
|
207
208
|
"<code>/run <prompt></code> execute an explicit remote request",
|
|
208
209
|
"<code>/status</code> show the current linked remote session",
|
|
209
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",
|
|
210
212
|
"<code>/cwd</code> show the current remote working directory",
|
|
211
213
|
"<code>/use <path></code> switch the linked session to another directory",
|
|
212
214
|
"<code>/desktop</code> switch the linked session to <code>~/Desktop</code>",
|
|
@@ -224,7 +226,8 @@ function buildRemoteHelp() {
|
|
|
224
226
|
"<code>/task history <id></code> show task history and comments",
|
|
225
227
|
"<code>/room-runtime</code> or <code>/room-runtime <name|clear></code> inspect or set the shared room runtime profile",
|
|
226
228
|
"<code>/invites</code> list pending shared invites",
|
|
227
|
-
"<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",
|
|
228
231
|
"<code>/accept-invite <invite-id></code> accept your own pending invite",
|
|
229
232
|
"<code>/approve-invite <invite-id></code> approve a pending invite",
|
|
230
233
|
"<code>/reject-invite <invite-id></code> reject a pending invite",
|
|
@@ -431,12 +434,40 @@ function formatTelegramWhoamiMarkup({ message, member = null, invites = [], shar
|
|
|
431
434
|
lines.push("<b>Pending invites you can accept</b>");
|
|
432
435
|
lines.push(...pending.map((invite) => `• <code>${escapeTelegramHtml(invite.id)}</code> <i>(${escapeTelegramHtml(invite.role || "editor")})</i>`));
|
|
433
436
|
lines.push("Use <code>/accept-invite <invite-id></code> to join the shared room.");
|
|
434
|
-
} else
|
|
437
|
+
} else {
|
|
435
438
|
lines.push("No pending Waterbrother room invite for this Telegram user id.");
|
|
436
439
|
}
|
|
440
|
+
lines.push("Use <code>/people</code> in a shared chat to see recent Telegram ids and usernames you can invite.");
|
|
437
441
|
return lines.join("\n");
|
|
438
442
|
}
|
|
439
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
|
+
|
|
440
471
|
function formatTelegramTasksMarkup(tasks = []) {
|
|
441
472
|
if (!tasks.length) {
|
|
442
473
|
return "<b>Shared tasks</b>\n• none";
|
|
@@ -533,12 +564,15 @@ function suggestTaskText(text = "") {
|
|
|
533
564
|
|
|
534
565
|
function parseInviteCommand(text) {
|
|
535
566
|
const parts = String(text || "").trim().split(/\s+/).filter(Boolean);
|
|
536
|
-
|
|
567
|
+
let target = String(parts[1] || "").trim();
|
|
537
568
|
let role = "editor";
|
|
538
|
-
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())) {
|
|
539
573
|
role = String(parts[2]).toLowerCase();
|
|
540
574
|
}
|
|
541
|
-
return {
|
|
575
|
+
return { target, role };
|
|
542
576
|
}
|
|
543
577
|
|
|
544
578
|
function extractRetryDelayMs(error, attempt) {
|
|
@@ -723,12 +757,100 @@ class TelegramGateway {
|
|
|
723
757
|
await saveGatewayState("telegram", this.state);
|
|
724
758
|
}
|
|
725
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
|
+
|
|
726
847
|
async addPendingPairing(message) {
|
|
727
|
-
const userId =
|
|
728
|
-
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 || {});
|
|
729
849
|
this.state.pendingPairings[userId] = {
|
|
730
850
|
userId,
|
|
731
|
-
username,
|
|
851
|
+
username: displayName,
|
|
852
|
+
usernameHandle,
|
|
853
|
+
displayName,
|
|
732
854
|
chatId: String(message.chat.id),
|
|
733
855
|
firstSeenAt: this.state.pendingPairings[userId]?.firstSeenAt || new Date().toISOString(),
|
|
734
856
|
lastSeenAt: new Date().toISOString(),
|
|
@@ -763,14 +885,15 @@ class TelegramGateway {
|
|
|
763
885
|
}
|
|
764
886
|
|
|
765
887
|
async ensurePeerSession(message) {
|
|
766
|
-
const userId =
|
|
767
|
-
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 || {});
|
|
768
889
|
const existing = this.getPeerState(userId);
|
|
769
890
|
if (existing?.sessionId) {
|
|
770
891
|
this.state.peers[userId] = {
|
|
771
892
|
...existing,
|
|
772
893
|
chatId: String(message.chat.id),
|
|
773
|
-
username,
|
|
894
|
+
username: displayName,
|
|
895
|
+
usernameHandle,
|
|
896
|
+
displayName,
|
|
774
897
|
sessions: upsertSessionHistory(existing.sessions, existing.sessionId),
|
|
775
898
|
lastSeenAt: new Date().toISOString(),
|
|
776
899
|
lastMessageId: message.message_id
|
|
@@ -793,7 +916,9 @@ class TelegramGateway {
|
|
|
793
916
|
this.state.peers[userId] = {
|
|
794
917
|
sessionId: session.id,
|
|
795
918
|
chatId: String(message.chat.id),
|
|
796
|
-
username,
|
|
919
|
+
username: displayName,
|
|
920
|
+
usernameHandle,
|
|
921
|
+
displayName,
|
|
797
922
|
sessions: upsertSessionHistory([], session.id),
|
|
798
923
|
linkedAt: new Date().toISOString(),
|
|
799
924
|
lastSeenAt: new Date().toISOString(),
|
|
@@ -822,7 +947,9 @@ class TelegramGateway {
|
|
|
822
947
|
...previous,
|
|
823
948
|
sessionId: session.id,
|
|
824
949
|
chatId: String(message.chat.id),
|
|
825
|
-
username:
|
|
950
|
+
username: this.describeTelegramUser(message?.from || {}).displayName,
|
|
951
|
+
usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
|
|
952
|
+
displayName: this.describeTelegramUser(message?.from || {}).displayName,
|
|
826
953
|
sessions: upsertSessionHistory(previous.sessions, session.id),
|
|
827
954
|
linkedAt: new Date().toISOString(),
|
|
828
955
|
lastSeenAt: new Date().toISOString(),
|
|
@@ -853,7 +980,9 @@ class TelegramGateway {
|
|
|
853
980
|
...previous,
|
|
854
981
|
sessionId: session.id,
|
|
855
982
|
chatId: String(message.chat.id),
|
|
856
|
-
username:
|
|
983
|
+
username: this.describeTelegramUser(message?.from || {}).displayName,
|
|
984
|
+
usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
|
|
985
|
+
displayName: this.describeTelegramUser(message?.from || {}).displayName,
|
|
857
986
|
sessions: upsertSessionHistory(previous.sessions, session.id),
|
|
858
987
|
linkedAt: previous.linkedAt || new Date().toISOString(),
|
|
859
988
|
lastSeenAt: new Date().toISOString(),
|
|
@@ -977,7 +1106,9 @@ class TelegramGateway {
|
|
|
977
1106
|
id: requestId,
|
|
978
1107
|
chatId: String(message.chat.id),
|
|
979
1108
|
userId: String(message?.from?.id || ""),
|
|
980
|
-
username:
|
|
1109
|
+
username: this.describeTelegramUser(message?.from || {}).displayName,
|
|
1110
|
+
usernameHandle: this.describeTelegramUser(message?.from || {}).usernameHandle,
|
|
1111
|
+
displayName: this.describeTelegramUser(message?.from || {}).displayName,
|
|
981
1112
|
sessionId: String(sessionId || "").trim(),
|
|
982
1113
|
text: String(promptText || "").trim(),
|
|
983
1114
|
runtimeProfile: String(project?.runtimeProfile || "").trim(),
|
|
@@ -1215,6 +1346,16 @@ class TelegramGateway {
|
|
|
1215
1346
|
return;
|
|
1216
1347
|
}
|
|
1217
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
|
+
|
|
1218
1359
|
if (text === "/status") {
|
|
1219
1360
|
await this.sendMessage(
|
|
1220
1361
|
message.chat.id,
|
|
@@ -1394,20 +1535,18 @@ class TelegramGateway {
|
|
|
1394
1535
|
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
1395
1536
|
return;
|
|
1396
1537
|
}
|
|
1397
|
-
const {
|
|
1398
|
-
if (!nextUserId) {
|
|
1399
|
-
await this.sendMessage(message.chat.id, "Usage: /invite <user-id> [owner|editor|observer]", message.message_id);
|
|
1400
|
-
return;
|
|
1401
|
-
}
|
|
1538
|
+
const { target, role } = parseInviteCommand(text);
|
|
1402
1539
|
try {
|
|
1540
|
+
const inviteTarget = this.resolveInviteTarget(message, target);
|
|
1403
1541
|
const result = await createSharedInvite(
|
|
1404
1542
|
session.cwd || this.cwd,
|
|
1405
|
-
{ id:
|
|
1543
|
+
{ id: inviteTarget.userId, role, name: inviteTarget.displayName || inviteTarget.userId, paired: true },
|
|
1406
1544
|
{ actorId: userId, actorName: peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || userId }
|
|
1407
1545
|
);
|
|
1408
1546
|
await this.sendMessage(
|
|
1409
1547
|
message.chat.id,
|
|
1410
|
-
`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>.`,
|
|
1411
1550
|
message.message_id
|
|
1412
1551
|
);
|
|
1413
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;
|