@tritard/waterbrother 0.16.39 → 0.16.40
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 -2
- package/package.json +1 -1
- package/src/cli.js +108 -13
- package/src/gateway.js +22 -3
- package/src/shared-project.js +129 -57
package/README.md
CHANGED
|
@@ -297,14 +297,15 @@ Current Telegram behavior:
|
|
|
297
297
|
- Telegram now supports remote workspace control with `/cwd`, `/use <path>`, `/desktop`, and `/new-project <name>`
|
|
298
298
|
- shared projects now support `/room`, `/members`, `/invites`, `/tasks`, `/mode`, `/claim`, `/release`, `/invite`, `/accept-invite`, `/approve-invite`, `/reject-invite`, `/remove-member`, `/room-runtime`, and `/task ...` from Telegram
|
|
299
299
|
- `/room` now includes pending invite count plus task ownership summaries
|
|
300
|
-
- local `/status` now includes `sharedRoom` with pending invites, task ownership summary, and recent
|
|
300
|
+
- local `/status` now includes `sharedRoom` with pending invites, task ownership summary, and recent shared-room event activity
|
|
301
|
+
- the TUI now prints a small Roundtable event feed when new shared-room activity lands
|
|
301
302
|
- Telegram now posts a Roundtable ownership notice when shared task ownership changes via assign/claim/move
|
|
302
303
|
- shared Telegram execution only runs when the shared room is in `execute` mode
|
|
303
304
|
- room administration is owner-only, and only owners/editors can hold the operator lock
|
|
304
305
|
- `/room` status now shows the active executor surface plus provider/model/runtime identity
|
|
305
306
|
- repo-first concept resolution now covers Waterbrother itself, Roundtable, Telegram, shared rooms, the gateway, runtime profiles, approvals, and sessions
|
|
306
307
|
- in Telegram groups, Waterbrother only responds when directly targeted: slash commands, `@botname` mentions, or replies to a bot message
|
|
307
|
-
- in Telegram groups, directly targeted messages are now classified as chat, planning, or execution;
|
|
308
|
+
- in Telegram groups, directly targeted messages are now classified as chat, planning, or execution; explicit execution should use `/run <prompt>`
|
|
308
309
|
- in Telegram group `chat` or `plan` flows, targeted Waterbrother/project questions are now answered directly from local repo state instead of returning only a generic planner hint
|
|
309
310
|
- in shared plan-mode groups, targeted `task:` and `todo:` messages are captured directly into the Roundtable task queue
|
|
310
311
|
- pairing is now explicit: first DM creates a pending request, then approve locally with `waterbrother gateway pair telegram <user-id>`
|
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -2682,18 +2682,11 @@ function buildRoomStatusPayload(project, runtime = null, currentSession = null)
|
|
|
2682
2682
|
const assignee = String(task?.assignedTo || "").trim() || "unassigned";
|
|
2683
2683
|
taskSummary.byAssignee[assignee] = (taskSummary.byAssignee[assignee] || 0) + 1;
|
|
2684
2684
|
}
|
|
2685
|
-
const recentActivity =
|
|
2686
|
-
|
|
2687
|
-
|
|
2688
|
-
|
|
2689
|
-
|
|
2690
|
-
text: entry.text,
|
|
2691
|
-
actorId: entry.actorId || "",
|
|
2692
|
-
actorName: entry.actorName || "",
|
|
2693
|
-
createdAt: entry.createdAt || ""
|
|
2694
|
-
})) : []))
|
|
2695
|
-
.sort((a, b) => String(b.createdAt).localeCompare(String(a.createdAt)))
|
|
2696
|
-
.slice(0, 5);
|
|
2685
|
+
const recentActivity = Array.isArray(project.recentEvents)
|
|
2686
|
+
? [...project.recentEvents]
|
|
2687
|
+
.sort((a, b) => String(b.createdAt || "").localeCompare(String(a.createdAt || "")))
|
|
2688
|
+
.slice(0, 8)
|
|
2689
|
+
: [];
|
|
2697
2690
|
return {
|
|
2698
2691
|
...project,
|
|
2699
2692
|
pendingInviteCount: Array.isArray(project.pendingInvites) ? project.pendingInvites.length : 0,
|
|
@@ -2711,6 +2704,39 @@ function buildRoomStatusPayload(project, runtime = null, currentSession = null)
|
|
|
2711
2704
|
};
|
|
2712
2705
|
}
|
|
2713
2706
|
|
|
2707
|
+
function getLatestSharedRoomEventId(project) {
|
|
2708
|
+
const events = Array.isArray(project?.recentEvents) ? project.recentEvents : [];
|
|
2709
|
+
return events.length ? String(events[events.length - 1]?.id || "").trim() : "";
|
|
2710
|
+
}
|
|
2711
|
+
|
|
2712
|
+
function formatSharedRoomEventFeedLine(event = {}) {
|
|
2713
|
+
const timestamp = String(event.createdAt || "").trim();
|
|
2714
|
+
const shortTime = timestamp ? timestamp.replace("T", " ").replace(/\.\d+Z$/, "Z") : "";
|
|
2715
|
+
const actor = String(event.actorName || event.actorId || "").trim();
|
|
2716
|
+
const prefix = actor ? `${actor}: ` : "";
|
|
2717
|
+
return `${styleSystemPrefix()} ${dim("[roundtable]")} ${shortTime ? `${shortTime} ` : ""}${prefix}${event.text || ""}`.trim();
|
|
2718
|
+
}
|
|
2719
|
+
|
|
2720
|
+
async function flushSharedRoomEventFeed({ cwd, lastSeenEventId = "" } = {}) {
|
|
2721
|
+
const project = await loadSharedProject(cwd).catch(() => null);
|
|
2722
|
+
const events = Array.isArray(project?.recentEvents) ? project.recentEvents : [];
|
|
2723
|
+
if (!events.length) {
|
|
2724
|
+
return { latestEventId: "", printed: 0 };
|
|
2725
|
+
}
|
|
2726
|
+
let unseen = events;
|
|
2727
|
+
if (lastSeenEventId) {
|
|
2728
|
+
const index = events.findIndex((event) => String(event.id || "").trim() === lastSeenEventId);
|
|
2729
|
+
unseen = index >= 0 ? events.slice(index + 1) : [];
|
|
2730
|
+
}
|
|
2731
|
+
for (const event of unseen) {
|
|
2732
|
+
console.log(formatSharedRoomEventFeedLine(event));
|
|
2733
|
+
}
|
|
2734
|
+
return {
|
|
2735
|
+
latestEventId: getLatestSharedRoomEventId(project),
|
|
2736
|
+
printed: unseen.length
|
|
2737
|
+
};
|
|
2738
|
+
}
|
|
2739
|
+
|
|
2714
2740
|
async function applyRuntimeSelection({
|
|
2715
2741
|
config,
|
|
2716
2742
|
context,
|
|
@@ -3844,11 +3870,64 @@ function getLocalOperatorIdentity() {
|
|
|
3844
3870
|
};
|
|
3845
3871
|
}
|
|
3846
3872
|
|
|
3873
|
+
function printSharedProjectNextSteps(project) {
|
|
3874
|
+
console.log(dim(`room mode: ${project.roomMode || "chat"}`));
|
|
3875
|
+
console.log(dim(`room runtime: ${project.runtimeProfile || "none"}`));
|
|
3876
|
+
console.log(dim("Next: invite members with `waterbrother room invite <member-id> [owner|editor|observer]`, then use `waterbrother room mode execute` when you want a claimed operator to act."));
|
|
3877
|
+
}
|
|
3878
|
+
|
|
3879
|
+
async function configureSharedProjectInteractive(cwd, operator, initialProject) {
|
|
3880
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
3881
|
+
return initialProject;
|
|
3882
|
+
}
|
|
3883
|
+
let project = initialProject;
|
|
3884
|
+
const currentMode = project?.roomMode || "chat";
|
|
3885
|
+
const enteredMode = (await promptLine(`Shared room mode [${currentMode}] (chat|plan|execute, Enter to keep): `)).trim().toLowerCase();
|
|
3886
|
+
if (enteredMode) {
|
|
3887
|
+
if (!["chat", "plan", "execute"].includes(enteredMode)) {
|
|
3888
|
+
console.log(yellow("Ignoring invalid room mode. Keeping existing mode."));
|
|
3889
|
+
} else if (enteredMode !== currentMode) {
|
|
3890
|
+
project = await setSharedRoomMode(cwd, enteredMode, { actorId: operator.id });
|
|
3891
|
+
}
|
|
3892
|
+
}
|
|
3893
|
+
|
|
3894
|
+
const currentRuntime = project?.runtimeProfile || "";
|
|
3895
|
+
const enteredRuntime = (await promptLine(`Shared room runtime profile [${currentRuntime || "none"}] (Enter to keep, clear to unset): `)).trim();
|
|
3896
|
+
if (enteredRuntime) {
|
|
3897
|
+
const nextRuntime = enteredRuntime.toLowerCase() === "clear" ? "" : enteredRuntime;
|
|
3898
|
+
project = await setSharedRuntimeProfile(cwd, nextRuntime, { actorId: operator.id });
|
|
3899
|
+
}
|
|
3900
|
+
|
|
3901
|
+
const createInvite = await promptYesNo("Create the first shared-project invite now?", {
|
|
3902
|
+
input: process.stdin,
|
|
3903
|
+
output: process.stdout
|
|
3904
|
+
});
|
|
3905
|
+
if (createInvite) {
|
|
3906
|
+
const memberId = (await promptLine("Telegram user id or member id: ")).trim();
|
|
3907
|
+
if (memberId) {
|
|
3908
|
+
const role = (await promptLine("Role [editor] (owner|editor|observer): ")).trim().toLowerCase() || "editor";
|
|
3909
|
+
const name = (await promptLine("Display name (Enter to reuse id): ")).trim() || memberId;
|
|
3910
|
+
const result = await createSharedInvite(
|
|
3911
|
+
cwd,
|
|
3912
|
+
{ id: memberId, role, name, paired: true },
|
|
3913
|
+
{ actorId: operator.id, actorName: operator.name }
|
|
3914
|
+
);
|
|
3915
|
+
project = result.project;
|
|
3916
|
+
console.log(green(`Created shared-project invite [${result.invite.id}] for ${memberId} as ${result.invite.role}`));
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
|
|
3920
|
+
return project;
|
|
3921
|
+
}
|
|
3922
|
+
|
|
3847
3923
|
async function runProjectCommand(positional, { cwd = process.cwd(), asJson = false } = {}) {
|
|
3848
3924
|
const sub = String(positional[1] || "").trim().toLowerCase();
|
|
3849
3925
|
if (sub === "share") {
|
|
3850
3926
|
const operator = getLocalOperatorIdentity();
|
|
3851
|
-
|
|
3927
|
+
let project = await enableSharedProject(cwd, { userId: operator.id, userName: operator.name, role: "owner" });
|
|
3928
|
+
if (!asJson) {
|
|
3929
|
+
project = await configureSharedProjectInteractive(cwd, operator, project);
|
|
3930
|
+
}
|
|
3852
3931
|
const paths = getSharedProjectPaths(cwd);
|
|
3853
3932
|
if (asJson) {
|
|
3854
3933
|
printData({ ok: true, action: "share", project, paths }, true);
|
|
@@ -3857,6 +3936,7 @@ async function runProjectCommand(positional, { cwd = process.cwd(), asJson = fal
|
|
|
3857
3936
|
console.log(`Shared project enabled in ${cwd}`);
|
|
3858
3937
|
console.log(`shared metadata: ${paths.sharedJson}`);
|
|
3859
3938
|
console.log(`roundtable: ${paths.roundtable}`);
|
|
3939
|
+
printSharedProjectNextSteps(project);
|
|
3860
3940
|
return;
|
|
3861
3941
|
}
|
|
3862
3942
|
|
|
@@ -7342,7 +7422,12 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7342
7422
|
console.log(formatBorderRow(clampColumns(process.stdout.columns || 80)));
|
|
7343
7423
|
}
|
|
7344
7424
|
|
|
7425
|
+
let lastSeenSharedRoomEventId = getLatestSharedRoomEventId(await loadSharedProject(context.cwd).catch(() => null));
|
|
7345
7426
|
while (true) {
|
|
7427
|
+
const sharedFeed = await flushSharedRoomEventFeed({ cwd: context.cwd, lastSeenEventId: lastSeenSharedRoomEventId });
|
|
7428
|
+
if (sharedFeed.latestEventId) {
|
|
7429
|
+
lastSeenSharedRoomEventId = sharedFeed.latestEventId;
|
|
7430
|
+
}
|
|
7346
7431
|
await touchTelegramBridgeHost({ sessionId: currentSession.id, cwd: context.cwd });
|
|
7347
7432
|
const nextInput = await readInteractiveLine({
|
|
7348
7433
|
getFooterText(inputBuffer) {
|
|
@@ -7908,6 +7993,16 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
|
|
|
7908
7993
|
context.costTracker = createCostTracker();
|
|
7909
7994
|
console.log(`started new project session: ${currentSession.id}`);
|
|
7910
7995
|
console.log(`cwd set to ${target}`);
|
|
7996
|
+
const shareProject = await promptYesNo("Make this new project shared?", { input: process.stdin, output: process.stdout });
|
|
7997
|
+
if (shareProject) {
|
|
7998
|
+
const operator = getLocalOperatorIdentity();
|
|
7999
|
+
const project = await configureSharedProjectInteractive(
|
|
8000
|
+
target,
|
|
8001
|
+
operator,
|
|
8002
|
+
await enableSharedProject(target, { userId: operator.id, userName: operator.name, role: "owner" })
|
|
8003
|
+
);
|
|
8004
|
+
printSharedProjectNextSteps(project);
|
|
8005
|
+
}
|
|
7911
8006
|
} catch (error) {
|
|
7912
8007
|
console.log(`new project failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
7913
8008
|
}
|
package/src/gateway.js
CHANGED
|
@@ -25,6 +25,7 @@ const TELEGRAM_COMMANDS = [
|
|
|
25
25
|
{ command: "help", description: "Show Telegram control help" },
|
|
26
26
|
{ command: "about", description: "Show local Waterbrother identity and capabilities" },
|
|
27
27
|
{ command: "state", description: "Show current Waterbrother self-awareness state" },
|
|
28
|
+
{ command: "run", description: "Execute an explicit remote prompt" },
|
|
28
29
|
{ command: "status", description: "Show the linked remote session" },
|
|
29
30
|
{ command: "cwd", description: "Show the current remote working directory" },
|
|
30
31
|
{ command: "runtime", description: "Show active runtime status" },
|
|
@@ -201,6 +202,7 @@ function buildRemoteHelp() {
|
|
|
201
202
|
"<code>/help</code> show this help",
|
|
202
203
|
"<code>/about</code> show Waterbrother identity and local capabilities",
|
|
203
204
|
"<code>/state</code> show current Waterbrother self-awareness state",
|
|
205
|
+
"<code>/run <prompt></code> execute an explicit remote request",
|
|
204
206
|
"<code>/status</code> show the current linked remote session",
|
|
205
207
|
"<code>/cwd</code> show the current remote working directory",
|
|
206
208
|
"<code>/use <path></code> switch the linked session to another directory",
|
|
@@ -231,7 +233,8 @@ function buildRemoteHelp() {
|
|
|
231
233
|
"<code>/new</code> start a fresh remote session",
|
|
232
234
|
"<code>/clear</code> clear the current remote conversation history",
|
|
233
235
|
"",
|
|
234
|
-
"
|
|
236
|
+
"In private chats, any other message is sent to the linked Waterbrother session.",
|
|
237
|
+
"In groups, explicit execution should use <code>/run <prompt></code>.",
|
|
235
238
|
"",
|
|
236
239
|
"<b>Current limitation</b>",
|
|
237
240
|
"• fallback remote runs still use <code>approval=never</code> if no live TUI host is attached",
|
|
@@ -1662,8 +1665,16 @@ class TelegramGateway {
|
|
|
1662
1665
|
return;
|
|
1663
1666
|
}
|
|
1664
1667
|
|
|
1665
|
-
|
|
1666
|
-
|
|
1668
|
+
if (text === "/run") {
|
|
1669
|
+
await this.sendMessage(message.chat.id, "Usage: <code>/run <prompt></code>", message.message_id);
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
const isExplicitRun = text.startsWith("/run ");
|
|
1674
|
+
const promptText = this.stripBotMention(isExplicitRun ? text.replace("/run", "").trim() : text);
|
|
1675
|
+
const groupIntent = this.isGroupChat(message)
|
|
1676
|
+
? (isExplicitRun ? { kind: "execution", reason: "explicit /run" } : classifyTelegramGroupIntent(promptText))
|
|
1677
|
+
: { kind: "execution", reason: "private chat" };
|
|
1667
1678
|
const sharedBinding = await this.bindSharedRoomForMessage(message, sessionId);
|
|
1668
1679
|
const manifest = await buildSelfAwarenessManifest({ cwd: sharedBinding.session.cwd || this.cwd, runtime: this.runtime, currentSession: sharedBinding.session });
|
|
1669
1680
|
const localConceptAnswer = resolveLocalConceptQuestion(promptText, manifest);
|
|
@@ -1696,6 +1707,14 @@ class TelegramGateway {
|
|
|
1696
1707
|
await this.sendMessage(message.chat.id, escapeTelegramHtml(planHint), message.message_id, { parseMode: "HTML" });
|
|
1697
1708
|
return;
|
|
1698
1709
|
}
|
|
1710
|
+
if (this.isGroupChat(message) && groupIntent.kind === "execution" && !isExplicitRun) {
|
|
1711
|
+
await this.sendMessage(
|
|
1712
|
+
message.chat.id,
|
|
1713
|
+
"Execution requests in groups must use <code>/run <prompt></code>. Discussion and planning can stay as normal targeted messages.",
|
|
1714
|
+
message.message_id
|
|
1715
|
+
);
|
|
1716
|
+
return;
|
|
1717
|
+
}
|
|
1699
1718
|
if (localConceptAnswer) {
|
|
1700
1719
|
await this.sendMessage(message.chat.id, renderTelegramChunks(localConceptAnswer).join("\n\n"), message.message_id, { parseMode: "HTML" });
|
|
1701
1720
|
return;
|
package/src/shared-project.js
CHANGED
|
@@ -6,6 +6,7 @@ import path from "node:path";
|
|
|
6
6
|
const SHARED_FILE = path.join(".waterbrother", "shared.json");
|
|
7
7
|
const ROUNDTABLE_FILE = "ROUNDTABLE.md";
|
|
8
8
|
const TASK_STATES = ["open", "active", "blocked", "done"];
|
|
9
|
+
const RECENT_EVENT_LIMIT = 24;
|
|
9
10
|
|
|
10
11
|
function makeId(prefix = "id") {
|
|
11
12
|
return `${prefix}_${crypto.randomBytes(3).toString("hex")}`;
|
|
@@ -40,6 +41,12 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
|
|
|
40
41
|
.map((invite) => normalizePendingInvite(invite))
|
|
41
42
|
.filter((invite) => invite.id && invite.memberId && invite.status === "pending")
|
|
42
43
|
: [];
|
|
44
|
+
const recentEvents = Array.isArray(project.recentEvents)
|
|
45
|
+
? project.recentEvents
|
|
46
|
+
.map((event) => normalizeSharedEvent(event))
|
|
47
|
+
.filter((event) => event.id && event.text)
|
|
48
|
+
.slice(-RECENT_EVENT_LIMIT)
|
|
49
|
+
: [];
|
|
43
50
|
const activeOperator = project.activeOperator && typeof project.activeOperator === "object"
|
|
44
51
|
? {
|
|
45
52
|
id: String(project.activeOperator.id || "").trim(),
|
|
@@ -67,6 +74,7 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
|
|
|
67
74
|
members,
|
|
68
75
|
tasks,
|
|
69
76
|
pendingInvites,
|
|
77
|
+
recentEvents,
|
|
70
78
|
activeOperator: activeOperator?.id ? activeOperator : null,
|
|
71
79
|
approvalPolicy: String(project.approvalPolicy || "owner").trim() || "owner",
|
|
72
80
|
createdAt: String(project.createdAt || new Date().toISOString()).trim(),
|
|
@@ -74,6 +82,18 @@ function normalizeSharedProject(project = {}, cwd = process.cwd()) {
|
|
|
74
82
|
};
|
|
75
83
|
}
|
|
76
84
|
|
|
85
|
+
function normalizeSharedEvent(event = {}) {
|
|
86
|
+
return {
|
|
87
|
+
id: String(event.id || makeId("rte")).trim(),
|
|
88
|
+
type: String(event.type || "note").trim(),
|
|
89
|
+
text: String(event.text || "").trim(),
|
|
90
|
+
actorId: String(event.actorId || "").trim(),
|
|
91
|
+
actorName: String(event.actorName || "").trim(),
|
|
92
|
+
createdAt: String(event.createdAt || new Date().toISOString()).trim(),
|
|
93
|
+
meta: event.meta && typeof event.meta === "object" ? { ...event.meta } : {}
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
77
97
|
function normalizeSharedTask(task = {}) {
|
|
78
98
|
const comments = Array.isArray(task.comments)
|
|
79
99
|
? task.comments
|
|
@@ -269,6 +289,16 @@ export async function appendRoundtableEvent(cwd, line) {
|
|
|
269
289
|
return target;
|
|
270
290
|
}
|
|
271
291
|
|
|
292
|
+
async function recordSharedProjectEvent(cwd, project, text, { type = "note", actorId = "", actorName = "", meta = {} } = {}) {
|
|
293
|
+
const event = normalizeSharedEvent({ type, text, actorId, actorName, meta });
|
|
294
|
+
const next = await saveSharedProject(cwd, {
|
|
295
|
+
...project,
|
|
296
|
+
recentEvents: [...(project.recentEvents || []), event].slice(-RECENT_EVENT_LIMIT)
|
|
297
|
+
});
|
|
298
|
+
await appendRoundtableEvent(cwd, `- ${event.createdAt}: ${event.text}`);
|
|
299
|
+
return { project: next, event };
|
|
300
|
+
}
|
|
301
|
+
|
|
272
302
|
export async function loadSharedProject(cwd) {
|
|
273
303
|
try {
|
|
274
304
|
const raw = await fs.readFile(sharedFilePath(cwd), "utf8");
|
|
@@ -311,8 +341,11 @@ export async function enableSharedProject(cwd, options = {}) {
|
|
|
311
341
|
claimedAt: new Date().toISOString()
|
|
312
342
|
}
|
|
313
343
|
});
|
|
314
|
-
await
|
|
315
|
-
|
|
344
|
+
return (await recordSharedProjectEvent(cwd, project, `sharing enabled by ${member.name || member.id}`, {
|
|
345
|
+
type: "sharing-enabled",
|
|
346
|
+
actorId: member.id,
|
|
347
|
+
actorName: member.name
|
|
348
|
+
})).project;
|
|
316
349
|
}
|
|
317
350
|
|
|
318
351
|
export async function disableSharedProject(cwd) {
|
|
@@ -323,8 +356,7 @@ export async function disableSharedProject(cwd) {
|
|
|
323
356
|
enabled: false,
|
|
324
357
|
activeOperator: null
|
|
325
358
|
});
|
|
326
|
-
await
|
|
327
|
-
return next;
|
|
359
|
+
return (await recordSharedProjectEvent(cwd, next, "sharing disabled", { type: "sharing-disabled" })).project;
|
|
328
360
|
}
|
|
329
361
|
|
|
330
362
|
export async function setSharedRoom(cwd, room = {}) {
|
|
@@ -338,7 +370,9 @@ export async function setSharedRoom(cwd, room = {}) {
|
|
|
338
370
|
title: String(room.title || existing.room?.title || "").trim()
|
|
339
371
|
}
|
|
340
372
|
});
|
|
341
|
-
return next
|
|
373
|
+
return (await recordSharedProjectEvent(cwd, next, `room linked to ${next.room.provider || "unknown"} ${next.room.chatId || ""}`.trim(), {
|
|
374
|
+
type: "room-linked"
|
|
375
|
+
})).project;
|
|
342
376
|
}
|
|
343
377
|
|
|
344
378
|
export async function setSharedRoomMode(cwd, roomMode = "chat", options = {}) {
|
|
@@ -352,8 +386,10 @@ export async function setSharedRoomMode(cwd, roomMode = "chat", options = {}) {
|
|
|
352
386
|
...existing,
|
|
353
387
|
roomMode: normalized
|
|
354
388
|
});
|
|
355
|
-
await
|
|
356
|
-
|
|
389
|
+
return (await recordSharedProjectEvent(cwd, next, `room mode set to ${normalized}`, {
|
|
390
|
+
type: "room-mode",
|
|
391
|
+
actorId: String(options.actorId || "").trim()
|
|
392
|
+
})).project;
|
|
357
393
|
}
|
|
358
394
|
|
|
359
395
|
export async function setSharedRuntimeProfile(cwd, runtimeProfile = "", options = {}) {
|
|
@@ -364,11 +400,10 @@ export async function setSharedRuntimeProfile(cwd, runtimeProfile = "", options
|
|
|
364
400
|
...existing,
|
|
365
401
|
runtimeProfile: normalized
|
|
366
402
|
});
|
|
367
|
-
await
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
);
|
|
371
|
-
return next;
|
|
403
|
+
return (await recordSharedProjectEvent(cwd, next, `room runtime profile ${normalized ? `set to ${normalized}` : "cleared"}`, {
|
|
404
|
+
type: "room-runtime",
|
|
405
|
+
actorId: String(options.actorId || "").trim()
|
|
406
|
+
})).project;
|
|
372
407
|
}
|
|
373
408
|
|
|
374
409
|
export function getSharedMember(project, memberId = "") {
|
|
@@ -437,11 +472,11 @@ export async function upsertSharedMember(cwd, member = {}, options = {}) {
|
|
|
437
472
|
...existing,
|
|
438
473
|
members
|
|
439
474
|
});
|
|
440
|
-
await
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
475
|
+
return (await recordSharedProjectEvent(cwd, next, `member ${nextMember.name || nextMember.id} set to role ${nextMember.role}`, {
|
|
476
|
+
type: "member-upsert",
|
|
477
|
+
actorId: String(options.actorId || "").trim(),
|
|
478
|
+
meta: { memberId: nextMember.id, role: nextMember.role }
|
|
479
|
+
})).project;
|
|
445
480
|
}
|
|
446
481
|
|
|
447
482
|
export async function createSharedInvite(cwd, member = {}, options = {}) {
|
|
@@ -468,11 +503,13 @@ export async function createSharedInvite(cwd, member = {}, options = {}) {
|
|
|
468
503
|
...existing,
|
|
469
504
|
pendingInvites
|
|
470
505
|
});
|
|
471
|
-
await
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
506
|
+
const recorded = await recordSharedProjectEvent(cwd, next, `invite created [${invite.id}] for ${invite.memberName || invite.memberId} as ${invite.role}`, {
|
|
507
|
+
type: "invite-created",
|
|
508
|
+
actorId: String(options.actorId || "").trim(),
|
|
509
|
+
actorName: String(options.actorName || "").trim(),
|
|
510
|
+
meta: { inviteId: invite.id, memberId: invite.memberId, role: invite.role }
|
|
511
|
+
});
|
|
512
|
+
return { project: recorded.project, invite };
|
|
476
513
|
}
|
|
477
514
|
|
|
478
515
|
export async function approveSharedInvite(cwd, inviteId = "", options = {}) {
|
|
@@ -499,11 +536,12 @@ export async function approveSharedInvite(cwd, inviteId = "", options = {}) {
|
|
|
499
536
|
members,
|
|
500
537
|
pendingInvites
|
|
501
538
|
});
|
|
502
|
-
await
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
539
|
+
const recorded = await recordSharedProjectEvent(cwd, next, `invite approved [${invite.id}] for ${invite.memberName || invite.memberId}`, {
|
|
540
|
+
type: "invite-approved",
|
|
541
|
+
actorId: String(options.actorId || "").trim(),
|
|
542
|
+
meta: { inviteId: invite.id, memberId: invite.memberId }
|
|
543
|
+
});
|
|
544
|
+
return { project: recorded.project, invite };
|
|
507
545
|
}
|
|
508
546
|
|
|
509
547
|
export async function rejectSharedInvite(cwd, inviteId = "", options = {}) {
|
|
@@ -519,11 +557,12 @@ export async function rejectSharedInvite(cwd, inviteId = "", options = {}) {
|
|
|
519
557
|
...existing,
|
|
520
558
|
pendingInvites
|
|
521
559
|
});
|
|
522
|
-
await
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
560
|
+
const recorded = await recordSharedProjectEvent(cwd, next, `invite rejected [${invite.id}] for ${invite.memberName || invite.memberId}`, {
|
|
561
|
+
type: "invite-rejected",
|
|
562
|
+
actorId: String(options.actorId || "").trim(),
|
|
563
|
+
meta: { inviteId: invite.id, memberId: invite.memberId }
|
|
564
|
+
});
|
|
565
|
+
return { project: recorded.project, invite };
|
|
527
566
|
}
|
|
528
567
|
|
|
529
568
|
export async function acceptSharedInvite(cwd, inviteId = "", options = {}) {
|
|
@@ -556,11 +595,13 @@ export async function acceptSharedInvite(cwd, inviteId = "", options = {}) {
|
|
|
556
595
|
members,
|
|
557
596
|
pendingInvites
|
|
558
597
|
});
|
|
559
|
-
await
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
598
|
+
const recorded = await recordSharedProjectEvent(cwd, next, `invite accepted [${invite.id}] by ${actorName || actorId}`, {
|
|
599
|
+
type: "invite-accepted",
|
|
600
|
+
actorId,
|
|
601
|
+
actorName,
|
|
602
|
+
meta: { inviteId: invite.id, memberId: invite.memberId }
|
|
603
|
+
});
|
|
604
|
+
return { project: recorded.project, invite };
|
|
564
605
|
}
|
|
565
606
|
|
|
566
607
|
export async function removeSharedMember(cwd, memberId = "", options = {}) {
|
|
@@ -579,11 +620,11 @@ export async function removeSharedMember(cwd, memberId = "", options = {}) {
|
|
|
579
620
|
members: (existing.members || []).filter((member) => member.id !== normalizedId),
|
|
580
621
|
activeOperator: existing.activeOperator?.id === normalizedId ? null : existing.activeOperator
|
|
581
622
|
});
|
|
582
|
-
await
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
623
|
+
return (await recordSharedProjectEvent(cwd, next, `member removed ${current.name || current.id}`, {
|
|
624
|
+
type: "member-removed",
|
|
625
|
+
actorId: String(options.actorId || "").trim(),
|
|
626
|
+
meta: { memberId: current.id }
|
|
627
|
+
})).project;
|
|
587
628
|
}
|
|
588
629
|
|
|
589
630
|
export async function listSharedTasks(cwd) {
|
|
@@ -618,8 +659,13 @@ export async function addSharedTask(cwd, text = "", options = {}) {
|
|
|
618
659
|
...existing,
|
|
619
660
|
tasks: [...(existing.tasks || []), task]
|
|
620
661
|
});
|
|
621
|
-
await
|
|
622
|
-
|
|
662
|
+
const recorded = await recordSharedProjectEvent(cwd, next, `task added [${task.id}] ${task.text}`, {
|
|
663
|
+
type: "task-added",
|
|
664
|
+
actorId: String(options.actorId || "").trim(),
|
|
665
|
+
actorName: String(options.actorName || "").trim(),
|
|
666
|
+
meta: { taskId: task.id }
|
|
667
|
+
});
|
|
668
|
+
return { project: recorded.project, task };
|
|
623
669
|
}
|
|
624
670
|
|
|
625
671
|
export async function moveSharedTask(cwd, taskId = "", state = "open", options = {}) {
|
|
@@ -648,8 +694,13 @@ export async function moveSharedTask(cwd, taskId = "", state = "open", options =
|
|
|
648
694
|
...existing,
|
|
649
695
|
tasks
|
|
650
696
|
});
|
|
651
|
-
await
|
|
652
|
-
|
|
697
|
+
const recorded = await recordSharedProjectEvent(cwd, next, `task moved [${tasks[index].id}] -> ${normalizedState}`, {
|
|
698
|
+
type: "task-moved",
|
|
699
|
+
actorId: String(options.actorId || "").trim(),
|
|
700
|
+
actorName: String(options.actorName || "").trim(),
|
|
701
|
+
meta: { taskId: tasks[index].id, state: normalizedState }
|
|
702
|
+
});
|
|
703
|
+
return { project: recorded.project, task: tasks[index] };
|
|
653
704
|
}
|
|
654
705
|
|
|
655
706
|
export async function assignSharedTask(cwd, taskId = "", memberId = "", options = {}) {
|
|
@@ -678,8 +729,13 @@ export async function assignSharedTask(cwd, taskId = "", memberId = "", options
|
|
|
678
729
|
...existing,
|
|
679
730
|
tasks
|
|
680
731
|
});
|
|
681
|
-
await
|
|
682
|
-
|
|
732
|
+
const recorded = await recordSharedProjectEvent(cwd, next, `task assigned [${tasks[index].id}] -> ${normalizedMemberId}`, {
|
|
733
|
+
type: "task-assigned",
|
|
734
|
+
actorId: String(options.actorId || "").trim(),
|
|
735
|
+
actorName: String(options.actorName || "").trim(),
|
|
736
|
+
meta: { taskId: tasks[index].id, memberId: normalizedMemberId }
|
|
737
|
+
});
|
|
738
|
+
return { project: recorded.project, task: tasks[index] };
|
|
683
739
|
}
|
|
684
740
|
|
|
685
741
|
export async function claimSharedTask(cwd, taskId = "", options = {}) {
|
|
@@ -705,8 +761,13 @@ export async function claimSharedTask(cwd, taskId = "", options = {}) {
|
|
|
705
761
|
...existing,
|
|
706
762
|
tasks
|
|
707
763
|
});
|
|
708
|
-
await
|
|
709
|
-
|
|
764
|
+
const recorded = await recordSharedProjectEvent(cwd, next, `task claimed [${tasks[index].id}] by ${actorId}`, {
|
|
765
|
+
type: "task-claimed",
|
|
766
|
+
actorId,
|
|
767
|
+
actorName: String(options.actorName || "").trim(),
|
|
768
|
+
meta: { taskId: tasks[index].id }
|
|
769
|
+
});
|
|
770
|
+
return { project: recorded.project, task: tasks[index] };
|
|
710
771
|
}
|
|
711
772
|
|
|
712
773
|
export async function commentSharedTask(cwd, taskId = "", text = "", options = {}) {
|
|
@@ -735,8 +796,13 @@ export async function commentSharedTask(cwd, taskId = "", text = "", options = {
|
|
|
735
796
|
...existing,
|
|
736
797
|
tasks
|
|
737
798
|
});
|
|
738
|
-
await
|
|
739
|
-
|
|
799
|
+
const recorded = await recordSharedProjectEvent(cwd, next, `task comment [${tasks[index].id}] ${normalizedText}`, {
|
|
800
|
+
type: "task-comment",
|
|
801
|
+
actorId: String(options.actorId || "").trim(),
|
|
802
|
+
actorName: String(options.actorName || "").trim(),
|
|
803
|
+
meta: { taskId: tasks[index].id }
|
|
804
|
+
});
|
|
805
|
+
return { project: recorded.project, task: tasks[index], comment };
|
|
740
806
|
}
|
|
741
807
|
|
|
742
808
|
export async function getSharedTaskHistory(cwd, taskId = "", options = {}) {
|
|
@@ -778,8 +844,11 @@ export async function claimSharedOperator(cwd, operator = {}) {
|
|
|
778
844
|
members,
|
|
779
845
|
activeOperator: nextOperator
|
|
780
846
|
});
|
|
781
|
-
await
|
|
782
|
-
|
|
847
|
+
return (await recordSharedProjectEvent(cwd, next, `operator claimed by ${nextOperator.name || nextOperator.id}`, {
|
|
848
|
+
type: "operator-claimed",
|
|
849
|
+
actorId: nextOperator.id,
|
|
850
|
+
actorName: nextOperator.name
|
|
851
|
+
})).project;
|
|
783
852
|
}
|
|
784
853
|
|
|
785
854
|
export async function releaseSharedOperator(cwd, operatorId = "", options = {}) {
|
|
@@ -799,8 +868,10 @@ export async function releaseSharedOperator(cwd, operatorId = "", options = {})
|
|
|
799
868
|
...existing,
|
|
800
869
|
activeOperator: null
|
|
801
870
|
});
|
|
802
|
-
await
|
|
803
|
-
|
|
871
|
+
return (await recordSharedProjectEvent(cwd, next, "operator released", {
|
|
872
|
+
type: "operator-released",
|
|
873
|
+
actorId
|
|
874
|
+
})).project;
|
|
804
875
|
}
|
|
805
876
|
|
|
806
877
|
export function formatSharedProjectStatus(project) {
|
|
@@ -818,7 +889,8 @@ export function formatSharedProjectStatus(project) {
|
|
|
818
889
|
activeOperator: project.activeOperator,
|
|
819
890
|
members: project.members,
|
|
820
891
|
pendingInvites: project.pendingInvites,
|
|
821
|
-
tasks: project.tasks
|
|
892
|
+
tasks: project.tasks,
|
|
893
|
+
recentEvents: project.recentEvents
|
|
822
894
|
}, null, 2);
|
|
823
895
|
}
|
|
824
896
|
|