@tritard/waterbrother 0.16.22 → 0.16.23
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 +39 -2
- package/src/gateway.js +40 -1
- package/src/shared-project.js +20 -0
package/README.md
CHANGED
|
@@ -264,6 +264,7 @@ Rollout order:
|
|
|
264
264
|
Shared project foundation is now live:
|
|
265
265
|
- enable it with `waterbrother project share`
|
|
266
266
|
- inspect it with `waterbrother room status`
|
|
267
|
+
- control conversation vs execution with `waterbrother room mode chat|plan|execute`
|
|
267
268
|
- claim or release the shared operator lock with `waterbrother room claim` and `waterbrother room release`
|
|
268
269
|
- shared project metadata lives in `.waterbrother/shared.json`
|
|
269
270
|
- human collaboration notes live in `ROUNDTABLE.md`
|
|
@@ -276,7 +277,8 @@ Current Telegram behavior:
|
|
|
276
277
|
- pending pairings are explicit and expire automatically after 12 hours unless approved
|
|
277
278
|
- paired Telegram users drive the same live session and permissions as the terminal operator when the TUI bridge is attached
|
|
278
279
|
- Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
|
|
279
|
-
- shared projects now support `/room`, `/claim`, and `/release` from Telegram with a single active-operator lock
|
|
280
|
+
- shared projects now support `/room`, `/mode`, `/claim`, and `/release` from Telegram with a single active-operator lock
|
|
281
|
+
- shared Telegram execution only runs when the shared room is in `execute` mode
|
|
280
282
|
- pairing is now explicit: first DM creates a pending request, then approve locally with `waterbrother gateway pair telegram <user-id>`
|
|
281
283
|
|
|
282
284
|
## Release flow
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -64,7 +64,8 @@ import {
|
|
|
64
64
|
formatSharedProjectStatus,
|
|
65
65
|
getSharedProjectPaths,
|
|
66
66
|
loadSharedProject,
|
|
67
|
-
releaseSharedOperator
|
|
67
|
+
releaseSharedOperator,
|
|
68
|
+
setSharedRoomMode
|
|
68
69
|
} from "./shared-project.js";
|
|
69
70
|
|
|
70
71
|
const execFileAsync = promisify(execFile);
|
|
@@ -147,6 +148,7 @@ const INTERACTIVE_COMMANDS = [
|
|
|
147
148
|
{ name: "/share-project", description: "Enable shared-project mode in the current cwd" },
|
|
148
149
|
{ name: "/unshare-project", description: "Disable shared-project mode in the current cwd" },
|
|
149
150
|
{ name: "/room", description: "Show shared room status for the current project" },
|
|
151
|
+
{ name: "/room mode <chat|plan|execute>", description: "Set collaboration mode for the shared room" },
|
|
150
152
|
{ name: "/room claim", description: "Claim operator control for the shared room" },
|
|
151
153
|
{ name: "/room release", description: "Release operator control for the shared room" },
|
|
152
154
|
{ name: "/cwd", description: "Show current working directory" },
|
|
@@ -270,6 +272,7 @@ Usage:
|
|
|
270
272
|
waterbrother project share
|
|
271
273
|
waterbrother project unshare
|
|
272
274
|
waterbrother room status
|
|
275
|
+
waterbrother room mode <chat|plan|execute>
|
|
273
276
|
waterbrother room claim
|
|
274
277
|
waterbrother room release
|
|
275
278
|
waterbrother mcp list
|
|
@@ -3787,7 +3790,27 @@ async function runRoomCommand(positional, { cwd = process.cwd(), asJson = false
|
|
|
3787
3790
|
return;
|
|
3788
3791
|
}
|
|
3789
3792
|
|
|
3790
|
-
|
|
3793
|
+
if (sub === "mode") {
|
|
3794
|
+
const nextMode = String(positional[2] || "").trim().toLowerCase();
|
|
3795
|
+
if (!nextMode) {
|
|
3796
|
+
const project = await loadSharedProject(cwd);
|
|
3797
|
+
if (asJson) {
|
|
3798
|
+
printData({ ok: true, roomMode: project?.roomMode || null, project }, true);
|
|
3799
|
+
return;
|
|
3800
|
+
}
|
|
3801
|
+
console.log(project?.roomMode || "shared project: off");
|
|
3802
|
+
return;
|
|
3803
|
+
}
|
|
3804
|
+
const project = await setSharedRoomMode(cwd, nextMode);
|
|
3805
|
+
if (asJson) {
|
|
3806
|
+
printData({ ok: true, action: "mode", roomMode: project.roomMode, project }, true);
|
|
3807
|
+
return;
|
|
3808
|
+
}
|
|
3809
|
+
console.log(`Room mode set to ${project.roomMode}`);
|
|
3810
|
+
return;
|
|
3811
|
+
}
|
|
3812
|
+
|
|
3813
|
+
throw new Error("Usage: waterbrother room status|mode <chat|plan|execute>|claim|release");
|
|
3791
3814
|
}
|
|
3792
3815
|
|
|
3793
3816
|
async function runMcpCommand(positional, runtime, { cwd, asJson = false } = {}) {
|
|
@@ -7615,6 +7638,20 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7615
7638
|
continue;
|
|
7616
7639
|
}
|
|
7617
7640
|
|
|
7641
|
+
if (line.startsWith("/room mode ")) {
|
|
7642
|
+
const nextMode = line.replace("/room mode", "").trim().toLowerCase();
|
|
7643
|
+
if (!nextMode) {
|
|
7644
|
+
console.log("Usage: /room mode <chat|plan|execute>");
|
|
7645
|
+
continue;
|
|
7646
|
+
}
|
|
7647
|
+
try {
|
|
7648
|
+
await runRoomCommand(["room", "mode", nextMode], { cwd: context.cwd, asJson: false });
|
|
7649
|
+
} catch (error) {
|
|
7650
|
+
console.log(`room mode failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7651
|
+
}
|
|
7652
|
+
continue;
|
|
7653
|
+
}
|
|
7654
|
+
|
|
7618
7655
|
if (line === "/room claim") {
|
|
7619
7656
|
try {
|
|
7620
7657
|
await runRoomCommand(["room", "claim"], { cwd: context.cwd, asJson: false });
|
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 { claimSharedOperator, loadSharedProject, releaseSharedOperator, setSharedRoom } from "./shared-project.js";
|
|
11
|
+
import { claimSharedOperator, loadSharedProject, releaseSharedOperator, setSharedRoom, setSharedRoomMode } from "./shared-project.js";
|
|
12
12
|
|
|
13
13
|
const execFileAsync = promisify(execFile);
|
|
14
14
|
const PACKAGE_ROOT = path.resolve(path.dirname(fileURLToPath(import.meta.url)), "..");
|
|
@@ -26,6 +26,7 @@ const TELEGRAM_COMMANDS = [
|
|
|
26
26
|
{ command: "cwd", description: "Show the current remote working directory" },
|
|
27
27
|
{ command: "runtime", description: "Show active runtime status" },
|
|
28
28
|
{ command: "room", description: "Show shared room status" },
|
|
29
|
+
{ command: "mode", description: "Show or set shared room mode" },
|
|
29
30
|
{ command: "claim", description: "Claim operator control for a shared project" },
|
|
30
31
|
{ command: "release", description: "Release operator control for a shared project" },
|
|
31
32
|
{ command: "sessions", description: "List recent remote sessions" },
|
|
@@ -198,6 +199,7 @@ function buildRemoteHelp() {
|
|
|
198
199
|
"<code>/new-project <name></code> create a folder on Desktop and switch into it",
|
|
199
200
|
"<code>/runtime</code> show active provider/model/runtime state",
|
|
200
201
|
"<code>/room</code> show shared project room status",
|
|
202
|
+
"<code>/mode</code> or <code>/mode <chat|plan|execute></code> inspect or change shared room mode",
|
|
201
203
|
"<code>/claim</code> claim operator control for a shared project",
|
|
202
204
|
"<code>/release</code> release operator control for a shared project",
|
|
203
205
|
"<code>/sessions</code> list recent linked remote sessions",
|
|
@@ -282,6 +284,7 @@ function formatTelegramRoomMarkup(project) {
|
|
|
282
284
|
"<b>Shared room</b>",
|
|
283
285
|
`project: <code>${escapeTelegramHtml(project.projectName || "")}</code>`,
|
|
284
286
|
`mode: <code>${escapeTelegramHtml(project.mode || "single-operator")}</code>`,
|
|
287
|
+
`room mode: <code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`,
|
|
285
288
|
`room: <code>${escapeTelegramHtml(roomLabel)}</code>`,
|
|
286
289
|
`active operator: <code>${escapeTelegramHtml(active)}</code>`,
|
|
287
290
|
"<b>Members</b>",
|
|
@@ -620,6 +623,14 @@ class TelegramGateway {
|
|
|
620
623
|
const { session, project } = await this.loadSharedProjectForSession(sessionId);
|
|
621
624
|
if (!project?.enabled) return { ok: true, project: null, session };
|
|
622
625
|
const bound = await this.bindSharedRoomForMessage(message, sessionId);
|
|
626
|
+
if (bound.project?.roomMode !== "execute") {
|
|
627
|
+
return {
|
|
628
|
+
ok: false,
|
|
629
|
+
project: bound.project,
|
|
630
|
+
session: bound.session,
|
|
631
|
+
reason: `Shared room is in ${bound.project?.roomMode || "chat"} mode. Switch to /mode execute before running code.`
|
|
632
|
+
};
|
|
633
|
+
}
|
|
623
634
|
const userId = String(message?.from?.id || "").trim();
|
|
624
635
|
if (!bound.project?.activeOperator?.id) {
|
|
625
636
|
return { ok: false, project: bound.project, session: bound.session, reason: "No active operator. Use /claim first." };
|
|
@@ -869,6 +880,34 @@ class TelegramGateway {
|
|
|
869
880
|
return;
|
|
870
881
|
}
|
|
871
882
|
|
|
883
|
+
if (text === "/mode") {
|
|
884
|
+
const { project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
885
|
+
await this.sendMessage(
|
|
886
|
+
message.chat.id,
|
|
887
|
+
project?.enabled
|
|
888
|
+
? `<b>Shared room mode</b>\n<code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`
|
|
889
|
+
: "This project is not shared.",
|
|
890
|
+
message.message_id
|
|
891
|
+
);
|
|
892
|
+
return;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
if (text.startsWith("/mode ")) {
|
|
896
|
+
const requestedMode = text.replace("/mode", "").trim().toLowerCase();
|
|
897
|
+
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
898
|
+
if (!project?.enabled) {
|
|
899
|
+
await this.sendMessage(message.chat.id, "This project is not shared.", message.message_id);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
try {
|
|
903
|
+
const next = await setSharedRoomMode(session.cwd || this.cwd, requestedMode);
|
|
904
|
+
await this.sendMessage(message.chat.id, `Shared room mode set to <code>${escapeTelegramHtml(next.roomMode)}</code>`, message.message_id);
|
|
905
|
+
} catch (error) {
|
|
906
|
+
await this.sendMessage(message.chat.id, escapeTelegramHtml(error instanceof Error ? error.message : String(error)), message.message_id);
|
|
907
|
+
}
|
|
908
|
+
return;
|
|
909
|
+
}
|
|
910
|
+
|
|
872
911
|
if (text === "/claim") {
|
|
873
912
|
const { session, project } = await this.bindSharedRoomForMessage(message, sessionId);
|
|
874
913
|
if (!project?.enabled) {
|
package/src/shared-project.js
CHANGED
|
@@ -37,6 +37,9 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
|
|
|
37
37
|
title: String(project.room?.title || "").trim()
|
|
38
38
|
},
|
|
39
39
|
mode: String(project.mode || "single-operator").trim() || "single-operator",
|
|
40
|
+
roomMode: ["chat", "plan", "execute"].includes(String(project.roomMode || "").trim())
|
|
41
|
+
? String(project.roomMode).trim()
|
|
42
|
+
: "chat",
|
|
40
43
|
members,
|
|
41
44
|
activeOperator: activeOperator?.id ? activeOperator : null,
|
|
42
45
|
approvalPolicy: String(project.approvalPolicy || "owner").trim() || "owner",
|
|
@@ -85,6 +88,7 @@ function defaultRoundtableContent(project) {
|
|
|
85
88
|
"## Project",
|
|
86
89
|
`- Name: ${project.projectName}`,
|
|
87
90
|
`- Mode: ${project.mode}`,
|
|
91
|
+
`- Room mode: ${project.roomMode}`,
|
|
88
92
|
`- Approval policy: ${project.approvalPolicy}`,
|
|
89
93
|
project.room?.provider ? `- Room: ${project.room.provider} ${project.room.chatId || ""}`.trim() : "- Room: not linked",
|
|
90
94
|
"",
|
|
@@ -196,6 +200,21 @@ export async function setSharedRoom(cwd, room = {}) {
|
|
|
196
200
|
return next;
|
|
197
201
|
}
|
|
198
202
|
|
|
203
|
+
export async function setSharedRoomMode(cwd, roomMode = "chat") {
|
|
204
|
+
const existing = await loadSharedProject(cwd);
|
|
205
|
+
if (!existing?.enabled) throw new Error("Project is not shared.");
|
|
206
|
+
const normalized = String(roomMode || "").trim().toLowerCase();
|
|
207
|
+
if (!["chat", "plan", "execute"].includes(normalized)) {
|
|
208
|
+
throw new Error("Invalid room mode. Expected one of chat, plan, execute.");
|
|
209
|
+
}
|
|
210
|
+
const next = await saveSharedProject(cwd, {
|
|
211
|
+
...existing,
|
|
212
|
+
roomMode: normalized
|
|
213
|
+
});
|
|
214
|
+
await appendRoundtableEvent(cwd, `- ${new Date().toISOString()}: room mode set to ${normalized}`);
|
|
215
|
+
return next;
|
|
216
|
+
}
|
|
217
|
+
|
|
199
218
|
export async function claimSharedOperator(cwd, operator = {}) {
|
|
200
219
|
const existing = await loadSharedProject(cwd);
|
|
201
220
|
if (!existing?.enabled) throw new Error("Project is not shared.");
|
|
@@ -245,6 +264,7 @@ export function formatSharedProjectStatus(project) {
|
|
|
245
264
|
cwd: project.cwd,
|
|
246
265
|
room: project.room,
|
|
247
266
|
mode: project.mode,
|
|
267
|
+
roomMode: project.roomMode,
|
|
248
268
|
approvalPolicy: project.approvalPolicy,
|
|
249
269
|
activeOperator: project.activeOperator,
|
|
250
270
|
members: project.members
|