@tritard/waterbrother 0.16.68 → 0.16.70
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/package.json +1 -1
- package/src/gateway.js +208 -2
package/package.json
CHANGED
package/src/gateway.js
CHANGED
|
@@ -9,7 +9,7 @@ import { DEFAULT_PENDING_PAIRING_TTL_MINUTES, loadGatewayBridge, loadGatewayStat
|
|
|
9
9
|
import { getGatewayStatus, getChannelSpec } from "./channels.js";
|
|
10
10
|
import { getConfigPath, loadConfigLayers, saveConfig } from "./config.js";
|
|
11
11
|
import { canonicalizeLoosePath } from "./path-utils.js";
|
|
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
|
+
import { acceptSharedInvite, addSharedTask, approveSharedInvite, assignSharedTask, claimSharedOperator, claimSharedTask, commentSharedTask, createSharedInvite, enableSharedProject, getSharedTaskHistory, listSharedEvents, listSharedInvites, listSharedTasks, loadSharedProject, moveSharedTask, rejectSharedInvite, releaseSharedOperator, setSharedRoom, setSharedRoomMode, setSharedRuntimeProfile, removeSharedMember, upsertSharedAgent, upsertSharedMember } from "./shared-project.js";
|
|
13
13
|
import { buildSelfAwarenessManifest, formatAboutWaterbrother, formatSelfState, resolveLocalConceptQuestion } from "./self-awareness.js";
|
|
14
14
|
|
|
15
15
|
const execFileAsync = promisify(execFile);
|
|
@@ -272,6 +272,68 @@ function buildRemoteHelp() {
|
|
|
272
272
|
].join("\n");
|
|
273
273
|
}
|
|
274
274
|
|
|
275
|
+
function buildTelegramDmOnboardingMarkup({ cwd = "", project = null, peer = null } = {}) {
|
|
276
|
+
const activeProject = project?.projectName || (cwd ? path.basename(cwd) : "");
|
|
277
|
+
return [
|
|
278
|
+
"<b>Waterbrother on Telegram</b>",
|
|
279
|
+
"You can talk to me here much like the TUI. Ask questions, make changes, refine work, or share a project into a room.",
|
|
280
|
+
activeProject ? `current project: <code>${escapeTelegramHtml(activeProject)}</code>` : "",
|
|
281
|
+
cwd ? `cwd: <code>${escapeTelegramHtml(cwd)}</code>` : "",
|
|
282
|
+
peer?.sessionId ? `linked session: <code>${escapeTelegramHtml(peer.sessionId)}</code>` : "",
|
|
283
|
+
"",
|
|
284
|
+
"<b>Good first moves</b>",
|
|
285
|
+
"• ask <code>what project are you in?</code>",
|
|
286
|
+
"• say <code>make a test page in this project</code>",
|
|
287
|
+
"• say <code>share this project in my Telegram room</code>",
|
|
288
|
+
"",
|
|
289
|
+
"<b>Need the full command list?</b>",
|
|
290
|
+
"Use <code>/help</code>."
|
|
291
|
+
].filter(Boolean).join("\n");
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
function buildTelegramRoomOnboardingMarkup({ project = null, actorName = "", executor = {} } = {}) {
|
|
295
|
+
const participants = listProjectParticipants(project);
|
|
296
|
+
const agents = listProjectAgents(project);
|
|
297
|
+
return [
|
|
298
|
+
"<b>Roundtable room</b>",
|
|
299
|
+
actorName ? `${escapeTelegramHtml(actorName)} is talking to Waterbrother in the shared room for <code>${escapeTelegramHtml(project?.projectName || "this project")}</code>.` : "",
|
|
300
|
+
`room mode: <code>${escapeTelegramHtml(project?.roomMode || "chat")}</code>`,
|
|
301
|
+
`participants: <code>${escapeTelegramHtml(String(participants.length || (project?.members || []).length || 0))}</code>`,
|
|
302
|
+
`agents: <code>${escapeTelegramHtml(String(agents.length || 0))}</code>`,
|
|
303
|
+
executor?.provider && executor?.model ? `active runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>` : "",
|
|
304
|
+
"",
|
|
305
|
+
"<b>What you can do here</b>",
|
|
306
|
+
"• ask <code>what project is this chat bound to?</code>",
|
|
307
|
+
"• ask <code>who is in the room?</code>",
|
|
308
|
+
"• say <code>add Austin as editor</code>",
|
|
309
|
+
"• in execute mode, address Waterbrother directly to build or change code",
|
|
310
|
+
"",
|
|
311
|
+
"Use <code>/help</code> for the full command list."
|
|
312
|
+
].filter(Boolean).join("\n");
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function buildTelegramRoomGuidanceMarkup({ project = null, member = null, executor = {} } = {}) {
|
|
316
|
+
const role = String(member?.role || "observer").trim() || "observer";
|
|
317
|
+
const roleGuidance = role === "owner"
|
|
318
|
+
? "As owner, you can add people, change room mode, assign terminal roles, and run work."
|
|
319
|
+
: role === "editor"
|
|
320
|
+
? "As editor, you can collaborate on tasks, discuss plans, and execute work when the room is in execute mode and you hold the floor."
|
|
321
|
+
: "As observer, you can ask questions, discuss plans, and review work here.";
|
|
322
|
+
return [
|
|
323
|
+
"<b>What you can do here</b>",
|
|
324
|
+
`project: <code>${escapeTelegramHtml(project?.projectName || "unknown")}</code>`,
|
|
325
|
+
`your role: <code>${escapeTelegramHtml(role)}</code>`,
|
|
326
|
+
`room mode: <code>${escapeTelegramHtml(project?.roomMode || "chat")}</code>`,
|
|
327
|
+
executor?.provider && executor?.model ? `active runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>` : "",
|
|
328
|
+
roleGuidance,
|
|
329
|
+
"Try asking:",
|
|
330
|
+
"• <code>what project is this chat bound to?</code>",
|
|
331
|
+
"• <code>who is in the room?</code>",
|
|
332
|
+
"• <code>who are the bots?</code>",
|
|
333
|
+
role === "owner" ? "• <code>add Austin as editor</code>" : "• <code>what mode are we in?</code>"
|
|
334
|
+
].filter(Boolean).join("\n");
|
|
335
|
+
}
|
|
336
|
+
|
|
275
337
|
function formatGatewaySessionStatus({ sessionId, userId, username, cwd, runtimeProfile, provider, model }) {
|
|
276
338
|
const bits = [
|
|
277
339
|
"<b>Telegram remote session</b>",
|
|
@@ -340,6 +402,23 @@ function chooseExecutorAgent(project, fallbackExecutor = {}) {
|
|
|
340
402
|
return null;
|
|
341
403
|
}
|
|
342
404
|
|
|
405
|
+
function memberRoleWeight(role = "") {
|
|
406
|
+
const normalized = String(role || "").trim().toLowerCase();
|
|
407
|
+
if (normalized === "owner") return 3;
|
|
408
|
+
if (normalized === "editor") return 2;
|
|
409
|
+
if (normalized === "observer") return 1;
|
|
410
|
+
return 0;
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function memberHasAtLeastRole(project, memberId = "", role = "editor") {
|
|
414
|
+
const normalizedId = String(memberId || "").trim();
|
|
415
|
+
if (!normalizedId) return false;
|
|
416
|
+
const member = Array.isArray(project?.members)
|
|
417
|
+
? project.members.find((entry) => String(entry?.id || "").trim() === normalizedId) || null
|
|
418
|
+
: null;
|
|
419
|
+
return memberRoleWeight(member?.role) >= memberRoleWeight(role);
|
|
420
|
+
}
|
|
421
|
+
|
|
343
422
|
function formatAgentLabel(agent = {}) {
|
|
344
423
|
const label = String(agent?.label || agent?.name || "").trim();
|
|
345
424
|
const role = String(agent?.role || "").trim();
|
|
@@ -479,6 +558,33 @@ function parseTelegramPairingIntent(text = "") {
|
|
|
479
558
|
return null;
|
|
480
559
|
}
|
|
481
560
|
|
|
561
|
+
function parseTelegramAgentIntent(text = "") {
|
|
562
|
+
const value = normalizeTelegramProjectIntentText(text);
|
|
563
|
+
const lowered = value.toLowerCase();
|
|
564
|
+
if (!value) return null;
|
|
565
|
+
if (/^(how|what|why|when|where|who)\b/.test(lowered)) return null;
|
|
566
|
+
const roleMatch = lowered.match(/\b(executor|reviewer|standby)\b/);
|
|
567
|
+
const role = roleMatch?.[1] || "";
|
|
568
|
+
if (!role) return null;
|
|
569
|
+
|
|
570
|
+
const patterns = [
|
|
571
|
+
/^(?:have|make|set)\s+(.+?)['’]s\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|standby)\s*$/i,
|
|
572
|
+
/^(?:have|make|set)\s+(.+?)\s+(?:bot|terminal)\s+(?:be|as|to)?\s*(?:the\s+)?(executor|reviewer|standby)\s*$/i,
|
|
573
|
+
/^(.+?)['’]s\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|standby)\s*$/i,
|
|
574
|
+
/^(.+?)\s+(?:bot|terminal)\s+should\s+(?:be\s+)?(?:the\s+)?(executor|reviewer|standby)\s*$/i
|
|
575
|
+
];
|
|
576
|
+
for (const pattern of patterns) {
|
|
577
|
+
const match = value.match(pattern);
|
|
578
|
+
if (!match) continue;
|
|
579
|
+
const target = String(match[1] || "").trim();
|
|
580
|
+
const nextRole = String(match[2] || role).trim().toLowerCase();
|
|
581
|
+
if (target && nextRole) {
|
|
582
|
+
return { action: "agent-role", target, role: nextRole };
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return null;
|
|
586
|
+
}
|
|
587
|
+
|
|
482
588
|
function parseTelegramStateIntent(text = "") {
|
|
483
589
|
const value = normalizeTelegramProjectIntentText(text);
|
|
484
590
|
const lower = value.toLowerCase();
|
|
@@ -496,6 +602,9 @@ function parseTelegramStateIntent(text = "") {
|
|
|
496
602
|
if (/\bwho can act\b/.test(lower) || /\bwho is the active operator\b/.test(lower) || /\bwho has the floor\b/.test(lower) || /\bwho claimed\b/.test(lower)) {
|
|
497
603
|
return { action: "active-operator" };
|
|
498
604
|
}
|
|
605
|
+
if (/\bwhat can i do here\b/.test(lower) || /\bhow do i use this room\b/.test(lower) || /\bhow do i use this chat\b/.test(lower) || /\bwhat do i do here\b/.test(lower)) {
|
|
606
|
+
return { action: "room-guidance" };
|
|
607
|
+
}
|
|
499
608
|
if (/\bwho are the bots\b/.test(lower) || /\bwho are the agents\b/.test(lower) || /\bwhat bots are here\b/.test(lower) || /\bwhat agents are here\b/.test(lower)) {
|
|
500
609
|
return { action: "agent-list" };
|
|
501
610
|
}
|
|
@@ -1490,6 +1599,41 @@ class TelegramGateway {
|
|
|
1490
1599
|
};
|
|
1491
1600
|
}
|
|
1492
1601
|
|
|
1602
|
+
async handleConversationalAgentIntent(message, sessionId, intent) {
|
|
1603
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1604
|
+
if (!project?.enabled) {
|
|
1605
|
+
throw new Error("This project is not shared yet. Use /project share first.");
|
|
1606
|
+
}
|
|
1607
|
+
const actorId = String(message?.from?.id || "").trim();
|
|
1608
|
+
if (!memberHasAtLeastRole(project, actorId, "owner")) {
|
|
1609
|
+
throw new Error("Only a shared-project owner can change terminal roles.");
|
|
1610
|
+
}
|
|
1611
|
+
const actor = this.describeTelegramUser(message?.from || {});
|
|
1612
|
+
const targetAgent = resolveProjectAgent(project, intent.target);
|
|
1613
|
+
if (!targetAgent) {
|
|
1614
|
+
throw new Error(`No terminal found for ${intent.target}. Ask them to connect their Waterbrother bot/terminal first.`);
|
|
1615
|
+
}
|
|
1616
|
+
const nextProject = await upsertSharedAgent(session.cwd || this.cwd, {
|
|
1617
|
+
...targetAgent,
|
|
1618
|
+
role: intent.role
|
|
1619
|
+
}, {
|
|
1620
|
+
actorId,
|
|
1621
|
+
actorName: actor.displayName || actorId
|
|
1622
|
+
});
|
|
1623
|
+
return {
|
|
1624
|
+
kind: "agent",
|
|
1625
|
+
project: nextProject,
|
|
1626
|
+
markup: [
|
|
1627
|
+
"<b>Roundtable terminal updated</b>",
|
|
1628
|
+
`owner: <code>${escapeTelegramHtml(targetAgent.ownerName || targetAgent.ownerId || targetAgent.label || targetAgent.id || "-")}</code>`,
|
|
1629
|
+
`terminal: <code>${escapeTelegramHtml(targetAgent.label || targetAgent.id || "-")}</code>`,
|
|
1630
|
+
`role: <code>${escapeTelegramHtml(intent.role)}</code>`,
|
|
1631
|
+
`runtime: <code>${escapeTelegramHtml(targetAgent.provider && targetAgent.model ? `${targetAgent.provider}/${targetAgent.model}` : "unknown")}</code>`,
|
|
1632
|
+
`project: <code>${escapeTelegramHtml(nextProject.projectName || path.basename(session.cwd || this.cwd))}</code>`
|
|
1633
|
+
].join("\n")
|
|
1634
|
+
};
|
|
1635
|
+
}
|
|
1636
|
+
|
|
1493
1637
|
async handleStateIntent(message, sessionId, intent) {
|
|
1494
1638
|
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1495
1639
|
const cwd = session.cwd || this.cwd;
|
|
@@ -1532,6 +1676,24 @@ class TelegramGateway {
|
|
|
1532
1676
|
: "<b>Active operator</b>\nnone";
|
|
1533
1677
|
}
|
|
1534
1678
|
|
|
1679
|
+
if (intent.action === "room-guidance") {
|
|
1680
|
+
if (!project?.enabled) {
|
|
1681
|
+
return [
|
|
1682
|
+
"<b>What you can do here</b>",
|
|
1683
|
+
"This chat is not bound to a shared project yet.",
|
|
1684
|
+
"Try:",
|
|
1685
|
+
"• <code>share this project in this chat</code>",
|
|
1686
|
+
"• <code>what project are you in?</code>",
|
|
1687
|
+
"• <code>/help</code> for the full command list"
|
|
1688
|
+
].join("\n");
|
|
1689
|
+
}
|
|
1690
|
+
const actorId = String(message?.from?.id || "").trim();
|
|
1691
|
+
const member = Array.isArray(project?.members)
|
|
1692
|
+
? project.members.find((entry) => String(entry?.id || "").trim() === actorId) || null
|
|
1693
|
+
: null;
|
|
1694
|
+
return buildTelegramRoomGuidanceMarkup({ project, member, executor });
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1535
1697
|
if (intent.action === "agent-list") {
|
|
1536
1698
|
if (!project?.enabled) {
|
|
1537
1699
|
return "This project is not shared.";
|
|
@@ -2200,11 +2362,45 @@ class TelegramGateway {
|
|
|
2200
2362
|
this.clearContinuation(message);
|
|
2201
2363
|
const userId = String(message.from.id);
|
|
2202
2364
|
|
|
2203
|
-
if (text === "/help"
|
|
2365
|
+
if (text === "/help") {
|
|
2204
2366
|
await this.sendMessage(message.chat.id, buildRemoteHelp(), message.message_id);
|
|
2205
2367
|
return;
|
|
2206
2368
|
}
|
|
2207
2369
|
|
|
2370
|
+
if (text === "/start") {
|
|
2371
|
+
if (this.isGroupChat(message)) {
|
|
2372
|
+
const sessionId = await this.ensurePeerSession(message);
|
|
2373
|
+
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
2374
|
+
const host = await this.getLiveBridgeHost();
|
|
2375
|
+
await this.sendMarkup(
|
|
2376
|
+
message.chat.id,
|
|
2377
|
+
buildTelegramRoomOnboardingMarkup({
|
|
2378
|
+
project,
|
|
2379
|
+
actorName: this.describeTelegramUser(message?.from || {}).displayName,
|
|
2380
|
+
executor: {
|
|
2381
|
+
provider: host?.provider || this.runtime.provider,
|
|
2382
|
+
model: host?.model || this.runtime.model
|
|
2383
|
+
}
|
|
2384
|
+
}),
|
|
2385
|
+
message.message_id
|
|
2386
|
+
);
|
|
2387
|
+
} else {
|
|
2388
|
+
const peer = this.getPeerState(String(message?.from?.id || "").trim());
|
|
2389
|
+
const session = peer?.sessionId ? await loadSession(peer.sessionId).catch(() => null) : null;
|
|
2390
|
+
const project = session?.cwd ? await loadSharedProject(session.cwd).catch(() => null) : null;
|
|
2391
|
+
await this.sendMarkup(
|
|
2392
|
+
message.chat.id,
|
|
2393
|
+
buildTelegramDmOnboardingMarkup({
|
|
2394
|
+
cwd: session?.cwd || this.cwd,
|
|
2395
|
+
project,
|
|
2396
|
+
peer
|
|
2397
|
+
}),
|
|
2398
|
+
message.message_id
|
|
2399
|
+
);
|
|
2400
|
+
}
|
|
2401
|
+
return;
|
|
2402
|
+
}
|
|
2403
|
+
|
|
2208
2404
|
if (text === "/new") {
|
|
2209
2405
|
const sessionId = await this.startFreshSession(message);
|
|
2210
2406
|
await this.sendMessage(message.chat.id, `Started new remote session: <code>${escapeTelegramHtml(sessionId)}</code>`, message.message_id);
|
|
@@ -2962,6 +3158,16 @@ Ask them to run <code>/whoami</code> and then <code>/accept-invite ${escapeTeleg
|
|
|
2962
3158
|
}
|
|
2963
3159
|
return;
|
|
2964
3160
|
}
|
|
3161
|
+
const agentIntent = parseTelegramAgentIntent(promptText);
|
|
3162
|
+
if (agentIntent) {
|
|
3163
|
+
try {
|
|
3164
|
+
const result = await this.handleConversationalAgentIntent(message, sessionId, agentIntent);
|
|
3165
|
+
await this.sendMarkup(message.chat.id, result.markup, message.message_id);
|
|
3166
|
+
} catch (error) {
|
|
3167
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
3168
|
+
}
|
|
3169
|
+
return;
|
|
3170
|
+
}
|
|
2965
3171
|
const memberIntent = parseTelegramMemberIntent(promptText);
|
|
2966
3172
|
if (memberIntent) {
|
|
2967
3173
|
try {
|