@tritard/waterbrother 0.16.47 → 0.16.49
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 -0
- package/package.json +1 -1
- package/src/gateway.js +202 -0
package/README.md
CHANGED
|
@@ -297,6 +297,8 @@ Current Telegram behavior:
|
|
|
297
297
|
- pending pairings are explicit and expire automatically after 12 hours unless approved
|
|
298
298
|
- paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
|
|
299
299
|
- Telegram now supports remote workspace control with `/project`, `/project use <path|name>`, `/project new <name>`, `/project share`, plus the lower-level `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>` commands
|
|
300
|
+
- Telegram owners can pair visible chat participants conversationally or with `/pair <user-id|@username>`, then continue with shared-room invites without dropping back to the terminal
|
|
301
|
+
- Telegram owners can also use conversational project and member requests like “share this project in this chat”, “switch to TelegramTest”, or “add Austin as editor”, and Waterbrother routes those through the same project and shared-room actions
|
|
300
302
|
- shared projects now support `/room`, `/events`, `/members`, `/invites`, `/whoami`, `/people`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/accept-invite`, `/approve-invite`, `/reject-invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
|
|
301
303
|
- Telegram now supports `/whoami` for Telegram-id and shared-room membership visibility, plus `/events` for recent shared-room activity
|
|
302
304
|
- `/room` now includes pending invite count plus task ownership summaries
|
package/package.json
CHANGED
package/src/gateway.js
CHANGED
|
@@ -7,6 +7,7 @@ import { promisify } from "node:util";
|
|
|
7
7
|
import { createSession, listSessions, loadSession, saveSession } from "./session-store.js";
|
|
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
|
+
import { getConfigPath, loadConfigLayers, saveConfig } from "./config.js";
|
|
10
11
|
import { canonicalizeLoosePath } from "./path-utils.js";
|
|
11
12
|
import { acceptSharedInvite, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, enableSharedProject, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember, upsertSharedMember } from "./shared-project.js";
|
|
12
13
|
import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState, resolveLocalConceptQuestion } from "./self-awareness.js";
|
|
@@ -29,6 +30,7 @@ const TELEGRAM_COMMANDS = [
|
|
|
29
30
|
{ command: "status", description: "Show the linked remote session" },
|
|
30
31
|
{ command: "whoami", description: "Show your Telegram identity and room membership" },
|
|
31
32
|
{ command: "people", description: "Show recently seen Telegram people in this chat" },
|
|
33
|
+
{ command: "pair", description: "Pair a known Telegram user from this chat" },
|
|
32
34
|
{ command: "project", description: "Show or change the linked project" },
|
|
33
35
|
{ command: "cwd", description: "Show the current remote working directory" },
|
|
34
36
|
{ command: "runtime", description: "Show active runtime status" },
|
|
@@ -210,6 +212,7 @@ function buildRemoteHelp() {
|
|
|
210
212
|
"<code>/status</code> show the current linked remote session",
|
|
211
213
|
"<code>/whoami</code> show your Telegram identity and room membership",
|
|
212
214
|
"<code>/people</code> list recent Telegram users in this chat for easier invites",
|
|
215
|
+
"<code>/pair <user-id|@username></code> pair a known Telegram user from this chat",
|
|
213
216
|
"<code>/project</code> show the linked project and sharing state",
|
|
214
217
|
"<code>/project use <path|name></code> switch to another project or directory",
|
|
215
218
|
"<code>/project new <name></code> create a Desktop project and switch into it",
|
|
@@ -328,6 +331,54 @@ function parseTelegramProjectIntent(text = "") {
|
|
|
328
331
|
return null;
|
|
329
332
|
}
|
|
330
333
|
|
|
334
|
+
function parseTelegramMemberIntent(text = "") {
|
|
335
|
+
const value = normalizeTelegramProjectIntentText(text);
|
|
336
|
+
const lowered = value.toLowerCase();
|
|
337
|
+
if (!value) return null;
|
|
338
|
+
if (/^(how|what|why|when|where|who)\b/.test(lowered)) return null;
|
|
339
|
+
|
|
340
|
+
const roleMatch = lowered.match(/\b(owner|editor|observer)s?\b/);
|
|
341
|
+
const role = roleMatch?.[1] || "editor";
|
|
342
|
+
const explicitUsername = value.match(/@([a-zA-Z0-9_]+)/);
|
|
343
|
+
|
|
344
|
+
if (explicitUsername && /\b(add|invite|make|set|promote)\b/.test(lowered)) {
|
|
345
|
+
return { action: "member-upsert", target: `@${explicitUsername[1]}`, role };
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
const patterns = [
|
|
349
|
+
/^(?:add|invite)\s+(.+?)\s+as\s+(owner|editor|observer)\s*$/i,
|
|
350
|
+
/^(?:add|invite)\s+(.+?)\s*$/i,
|
|
351
|
+
/^(?:make|set|promote)\s+(.+?)\s+(?:an?\s+)?(owner|editor|observer)\s*$/i
|
|
352
|
+
];
|
|
353
|
+
for (const pattern of patterns) {
|
|
354
|
+
const match = value.match(pattern);
|
|
355
|
+
if (!match) continue;
|
|
356
|
+
const target = String(match[1] || "").trim();
|
|
357
|
+
const nextRole = String(match[2] || role || "editor").trim().toLowerCase();
|
|
358
|
+
if (target) {
|
|
359
|
+
return { action: "member-upsert", target, role: nextRole };
|
|
360
|
+
}
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
return null;
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
function parseTelegramPairingIntent(text = "") {
|
|
367
|
+
const value = normalizeTelegramProjectIntentText(text);
|
|
368
|
+
const lowered = value.toLowerCase();
|
|
369
|
+
if (!value) return null;
|
|
370
|
+
if (/^(how|what|why|when|where|who)\b/.test(lowered)) return null;
|
|
371
|
+
const explicitUsername = value.match(/@([a-zA-Z0-9_]+)/);
|
|
372
|
+
if (explicitUsername && /\b(pair|approve|allow)\b/.test(lowered)) {
|
|
373
|
+
return { action: "pair", target: `@${explicitUsername[1]}` };
|
|
374
|
+
}
|
|
375
|
+
const match = value.match(/^(?:pair|approve|allow)\s+(.+?)\s*$/i);
|
|
376
|
+
if (match?.[1]) {
|
|
377
|
+
return { action: "pair", target: match[1].trim() };
|
|
378
|
+
}
|
|
379
|
+
return null;
|
|
380
|
+
}
|
|
381
|
+
|
|
331
382
|
async function buildTelegramSelfAwarenessMarkup({ cwd, runtime, currentSession, kind = "about" }) {
|
|
332
383
|
const manifest = await buildSelfAwarenessManifest({ cwd, runtime, currentSession });
|
|
333
384
|
if (kind === "state") {
|
|
@@ -918,6 +969,114 @@ class TelegramGateway {
|
|
|
918
969
|
throw new Error(`Unknown Telegram user reference: ${value}. Use /people to list recent users in this chat.`);
|
|
919
970
|
}
|
|
920
971
|
|
|
972
|
+
resolveKnownPerson(message, target) {
|
|
973
|
+
const value = String(target || "").trim();
|
|
974
|
+
if (!value) {
|
|
975
|
+
throw new Error("member target is required");
|
|
976
|
+
}
|
|
977
|
+
if (/^\d+$/.test(value) || value.startsWith("@")) {
|
|
978
|
+
const match = this.resolveInviteTarget(message, value);
|
|
979
|
+
return { userId: match.userId, displayName: match.displayName };
|
|
980
|
+
}
|
|
981
|
+
|
|
982
|
+
const normalized = value.toLowerCase();
|
|
983
|
+
const known = this.listKnownChatPeople(message);
|
|
984
|
+
const exact = known.find((person) => String(person.displayName || "").trim().toLowerCase() === normalized);
|
|
985
|
+
if (exact) {
|
|
986
|
+
return { userId: exact.userId, displayName: exact.displayName || exact.userId };
|
|
987
|
+
}
|
|
988
|
+
const partial = known.filter((person) => {
|
|
989
|
+
const display = String(person.displayName || "").trim().toLowerCase();
|
|
990
|
+
const handle = String(person.usernameHandle || "").trim().toLowerCase();
|
|
991
|
+
return display.includes(normalized) || handle === normalized.replace(/^@/, "");
|
|
992
|
+
});
|
|
993
|
+
if (partial.length === 1) {
|
|
994
|
+
return { userId: partial[0].userId, displayName: partial[0].displayName || partial[0].userId };
|
|
995
|
+
}
|
|
996
|
+
if (partial.length > 1) {
|
|
997
|
+
throw new Error(`Multiple Telegram users matched ${value}. Use /people and invite by id or @username.`);
|
|
998
|
+
}
|
|
999
|
+
throw new Error(`Unknown Telegram person: ${value}. Use /people to see recent users in this chat.`);
|
|
1000
|
+
}
|
|
1001
|
+
|
|
1002
|
+
async handleConversationalMemberIntent(message, sessionId, intent, peer) {
|
|
1003
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1004
|
+
if (!project?.enabled) {
|
|
1005
|
+
throw new Error("This project is not shared yet. Use /project share first.");
|
|
1006
|
+
}
|
|
1007
|
+
const target = this.resolveKnownPerson(message, intent.target);
|
|
1008
|
+
const actorId = String(message?.from?.id || "").trim();
|
|
1009
|
+
const actorName = peer?.username || [message?.from?.first_name, message?.from?.last_name].filter(Boolean).join(" ").trim() || actorId;
|
|
1010
|
+
const existingMember = Array.isArray(project.members)
|
|
1011
|
+
? project.members.find((entry) => String(entry?.id || "").trim() === target.userId) || null
|
|
1012
|
+
: null;
|
|
1013
|
+
if (existingMember) {
|
|
1014
|
+
const nextProject = await upsertSharedMember(
|
|
1015
|
+
session.cwd || this.cwd,
|
|
1016
|
+
{ id: target.userId, name: target.displayName || target.userId, role: intent.role, paired: true },
|
|
1017
|
+
{ actorId, actorName }
|
|
1018
|
+
);
|
|
1019
|
+
return {
|
|
1020
|
+
kind: "member",
|
|
1021
|
+
project: nextProject,
|
|
1022
|
+
markup: `Updated <code>${escapeTelegramHtml(target.displayName || target.userId)}</code> to <code>${escapeTelegramHtml(intent.role)}</code> in this shared project.`
|
|
1023
|
+
};
|
|
1024
|
+
}
|
|
1025
|
+
const result = await createSharedInvite(
|
|
1026
|
+
session.cwd || this.cwd,
|
|
1027
|
+
{ id: target.userId, role: intent.role, name: target.displayName || target.userId, paired: true },
|
|
1028
|
+
{ actorId, actorName }
|
|
1029
|
+
);
|
|
1030
|
+
return {
|
|
1031
|
+
kind: "invite",
|
|
1032
|
+
project: result.project,
|
|
1033
|
+
markup: `Created pending invite <code>${escapeTelegramHtml(result.invite.id)}</code> for <code>${escapeTelegramHtml(target.displayName || target.userId)}</code> as <code>${escapeTelegramHtml(result.invite.role)}</code>.\nAsk them to run <code>/accept-invite ${escapeTelegramHtml(result.invite.id)}</code>.`
|
|
1034
|
+
};
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
async pairKnownTelegramUser(message, target) {
|
|
1038
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, await this.ensurePeerSession(message));
|
|
1039
|
+
if (project?.enabled) {
|
|
1040
|
+
const owner = Array.isArray(project.members)
|
|
1041
|
+
? project.members.find((entry) => String(entry?.id || "").trim() === String(message?.from?.id || "").trim())
|
|
1042
|
+
: null;
|
|
1043
|
+
if (!owner || String(owner.role || "").trim() !== "owner") {
|
|
1044
|
+
throw new Error("Only a shared-project owner can pair new Telegram users from this chat.");
|
|
1045
|
+
}
|
|
1046
|
+
}
|
|
1047
|
+
|
|
1048
|
+
const person = this.resolveKnownPerson(message, target);
|
|
1049
|
+
const { userConfig, projectConfig } = await loadConfigLayers(session.cwd || this.cwd);
|
|
1050
|
+
const targetScope = projectConfig?.channels?.telegram ? "project" : "user";
|
|
1051
|
+
const targetConfig = targetScope === "project" ? { ...projectConfig } : { ...userConfig };
|
|
1052
|
+
const channels = targetConfig.channels && typeof targetConfig.channels === "object" ? { ...targetConfig.channels } : {};
|
|
1053
|
+
const telegram = channels.telegram && typeof channels.telegram === "object" ? { ...channels.telegram } : {};
|
|
1054
|
+
const allowed = new Set(Array.isArray(telegram.allowedUserIds) ? telegram.allowedUserIds.map((value) => String(value || "").trim()).filter(Boolean) : []);
|
|
1055
|
+
allowed.add(person.userId);
|
|
1056
|
+
telegram.enabled = telegram.enabled !== false;
|
|
1057
|
+
if (!telegram.pairingMode) telegram.pairingMode = "manual";
|
|
1058
|
+
telegram.allowedUserIds = [...allowed];
|
|
1059
|
+
channels.telegram = telegram;
|
|
1060
|
+
targetConfig.channels = channels;
|
|
1061
|
+
await saveConfig(targetConfig, { scope: targetScope, cwd: session.cwd || this.cwd });
|
|
1062
|
+
|
|
1063
|
+
this.channel.allowedUserIds = telegram.allowedUserIds;
|
|
1064
|
+
if (this.runtime.channels?.telegram) {
|
|
1065
|
+
this.runtime.channels.telegram.allowedUserIds = telegram.allowedUserIds;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
if (this.state.pendingPairings?.[person.userId]) {
|
|
1069
|
+
delete this.state.pendingPairings[person.userId];
|
|
1070
|
+
}
|
|
1071
|
+
await this.persistState();
|
|
1072
|
+
|
|
1073
|
+
return {
|
|
1074
|
+
userId: person.userId,
|
|
1075
|
+
displayName: person.displayName || person.userId,
|
|
1076
|
+
configPath: getConfigPath({ scope: targetScope, cwd: session.cwd || this.cwd })
|
|
1077
|
+
};
|
|
1078
|
+
}
|
|
1079
|
+
|
|
921
1080
|
async addPendingPairing(message) {
|
|
922
1081
|
const { userId, usernameHandle, displayName } = this.describeTelegramUser(message?.from || {});
|
|
923
1082
|
this.state.pendingPairings[userId] = {
|
|
@@ -1507,6 +1666,25 @@ class TelegramGateway {
|
|
|
1507
1666
|
return;
|
|
1508
1667
|
}
|
|
1509
1668
|
|
|
1669
|
+
if (text.startsWith("/pair")) {
|
|
1670
|
+
const rawTarget = text.replace("/pair", "").trim();
|
|
1671
|
+
if (!rawTarget) {
|
|
1672
|
+
await this.sendMarkup(message.chat.id, "Usage: <code>/pair <user-id|@username></code>", message.message_id);
|
|
1673
|
+
return;
|
|
1674
|
+
}
|
|
1675
|
+
try {
|
|
1676
|
+
const result = await this.pairKnownTelegramUser(message, rawTarget);
|
|
1677
|
+
await this.sendMarkup(
|
|
1678
|
+
message.chat.id,
|
|
1679
|
+
`Paired <code>${escapeTelegramHtml(result.displayName)}</code> <i>(${escapeTelegramHtml(result.userId)})</i> via <code>${escapeTelegramHtml(result.configPath)}</code>.`,
|
|
1680
|
+
message.message_id
|
|
1681
|
+
);
|
|
1682
|
+
} catch (error) {
|
|
1683
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
1684
|
+
}
|
|
1685
|
+
return;
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1510
1688
|
if (text === "/project") {
|
|
1511
1689
|
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1512
1690
|
await this.sendMarkup(
|
|
@@ -2126,6 +2304,30 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
|
|
|
2126
2304
|
? (isExplicitRun ? { kind: "execution", reason: "explicit /run" } : classifyTelegramGroupIntent(promptText))
|
|
2127
2305
|
: { kind: "execution", reason: "private chat" };
|
|
2128
2306
|
const sharedBinding = await this.bindSharedRoomForMessage(message, sessionId);
|
|
2307
|
+
const pairingIntent = parseTelegramPairingIntent(promptText);
|
|
2308
|
+
if (pairingIntent) {
|
|
2309
|
+
try {
|
|
2310
|
+
const result = await this.pairKnownTelegramUser(message, pairingIntent.target);
|
|
2311
|
+
await this.sendMarkup(
|
|
2312
|
+
message.chat.id,
|
|
2313
|
+
`Paired <code>${escapeTelegramHtml(result.displayName)}</code> <i>(${escapeTelegramHtml(result.userId)})</i> via <code>${escapeTelegramHtml(result.configPath)}</code>.`,
|
|
2314
|
+
message.message_id
|
|
2315
|
+
);
|
|
2316
|
+
} catch (error) {
|
|
2317
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
2318
|
+
}
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
const memberIntent = parseTelegramMemberIntent(promptText);
|
|
2322
|
+
if (memberIntent) {
|
|
2323
|
+
try {
|
|
2324
|
+
const result = await this.handleConversationalMemberIntent(message, sessionId, memberIntent, peer);
|
|
2325
|
+
await this.sendMarkup(message.chat.id, result.markup, message.message_id);
|
|
2326
|
+
} catch (error) {
|
|
2327
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
2328
|
+
}
|
|
2329
|
+
return;
|
|
2330
|
+
}
|
|
2129
2331
|
const projectIntent = parseTelegramProjectIntent(promptText);
|
|
2130
2332
|
if (projectIntent?.action === "show-project") {
|
|
2131
2333
|
await this.sendMarkup(
|