@tritard/waterbrother 0.16.95 → 0.16.97

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@tritard/waterbrother",
3
- "version": "0.16.95",
3
+ "version": "0.16.97",
4
4
  "description": "Waterbrother: bring-your-own-model coding CLI with local tools, sessions, operator modes, and approval controls",
5
5
  "type": "module",
6
6
  "bin": {
package/src/gateway.js CHANGED
@@ -385,7 +385,27 @@ function listProjectParticipants(project) {
385
385
  }
386
386
 
387
387
  function listProjectAgents(project) {
388
- return Array.isArray(project?.agents) ? project.agents : [];
388
+ const agents = Array.isArray(project?.agents) ? project.agents : [];
389
+ const seen = new Set();
390
+ return agents.filter((agent) => {
391
+ const ownerId = String(agent?.ownerId || "").trim();
392
+ const ownerName = String(agent?.ownerName || "").trim();
393
+ const label = String(agent?.label || "").trim();
394
+ const runtime = agent?.provider && agent?.model ? `${agent.provider}/${agent.model}` : "";
395
+ if (!String(agent?.id || "").trim()) return false;
396
+ if (!ownerId && !ownerName && (!label || /\bundefined\b/i.test(label))) return false;
397
+ const key = [
398
+ ownerId,
399
+ ownerName.toLowerCase(),
400
+ label.replace(/\bundefined\b/ig, "").trim().toLowerCase(),
401
+ String(agent?.role || "").trim().toLowerCase(),
402
+ String(agent?.surface || "").trim().toLowerCase(),
403
+ runtime.toLowerCase()
404
+ ].join("|");
405
+ if (seen.has(key)) return false;
406
+ seen.add(key);
407
+ return true;
408
+ });
389
409
  }
390
410
 
391
411
  function findProjectParticipant(project, memberId = "") {
@@ -486,7 +506,9 @@ function formatBridgeHostLabel(host = {}) {
486
506
  const owner = String(host?.ownerName || host?.ownerId || "").trim();
487
507
  const label = String(host?.label || "").trim();
488
508
  const runtime = host?.provider && host?.model ? `${host.provider}/${host.model}` : "";
489
- return [owner || label, label && label !== owner ? `(${label})` : "", runtime ? `[${runtime}]` : ""].filter(Boolean).join(" ").trim();
509
+ const safeLabel = label && !/\bundefined\b/i.test(label) ? label : "";
510
+ const primary = owner || safeLabel || "live terminal";
511
+ return [primary, safeLabel && safeLabel !== owner ? `(${safeLabel})` : "", runtime ? `[${runtime}]` : ""].filter(Boolean).join(" ").trim();
490
512
  }
491
513
 
492
514
  function findLiveHostForAgent(hosts = [], agent = {}) {
@@ -800,7 +822,7 @@ function parseTelegramStateIntent(text = "") {
800
822
  if (/\bwhat project\b/.test(lower) || /\bwhich project\b/.test(lower) || /\bproject is this chat\b/.test(lower) || /\bchat bound to\b/.test(lower)) {
801
823
  return { action: "project-status" };
802
824
  }
803
- if (/\bwho is in the room\b/.test(lower) || /\bwho(?:'s| is) in the project\b/.test(lower) || /\bwho are the members\b/.test(lower) || /\bshow members\b/.test(lower)) {
825
+ if (/\bwho(?:'s| is)? in (?:the )?room\b/.test(lower) || /\bwho(?:'s| is)? in (?:the )?project\b/.test(lower) || /\bwho are the members\b/.test(lower) || /\bshow members\b/.test(lower)) {
804
826
  return { action: "room-members" };
805
827
  }
806
828
  if (/\bwhat mode\b/.test(lower) || /\broom mode\b/.test(lower) || /\bmode are we in\b/.test(lower)) {
@@ -812,7 +834,7 @@ function parseTelegramStateIntent(text = "") {
812
834
  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)) {
813
835
  return { action: "room-guidance" };
814
836
  }
815
- 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)) {
837
+ if (/\bwho are the (?:bots|boys)\b/.test(lower) || /\bwho are the agents\b/.test(lower) || /\bwhat (?:bots|boys) are here\b/.test(lower) || /\bwhat agents are here\b/.test(lower)) {
816
838
  return { action: "agent-list" };
817
839
  }
818
840
  if (/\bwhich terminals are live\b/.test(lower) || /\bwhich bots are live\b/.test(lower) || /\bwho is live\b/.test(lower) || /\bwhat terminals are live\b/.test(lower)) {
@@ -54,11 +54,13 @@ function normalizeParticipant(participant = {}) {
54
54
  }
55
55
 
56
56
  function normalizeAgent(agent = {}) {
57
+ const rawLabel = String(agent.label || agent.name || "").trim();
58
+ const cleanLabel = rawLabel.replace(/\bundefined\b/ig, "").replace(/\s+/g, " ").trim();
57
59
  return {
58
60
  id: String(agent.id || "").trim(),
59
61
  ownerId: String(agent.ownerId || "").trim(),
60
62
  ownerName: String(agent.ownerName || "").trim(),
61
- label: String(agent.label || agent.name || "").trim(),
63
+ label: cleanLabel,
62
64
  surface: String(agent.surface || "").trim(),
63
65
  role: AGENT_ROLES.includes(String(agent.role || "").trim()) ? String(agent.role).trim() : "standby",
64
66
  provider: String(agent.provider || "").trim(),
@@ -71,6 +73,42 @@ function normalizeAgent(agent = {}) {
71
73
  };
72
74
  }
73
75
 
76
+ function isBrokenAgent(agent = {}) {
77
+ const ownerId = String(agent.ownerId || "").trim();
78
+ const ownerName = String(agent.ownerName || "").trim();
79
+ const label = String(agent.label || "").trim();
80
+ return !ownerId && !ownerName && !label;
81
+ }
82
+
83
+ function dedupeAgents(agents = []) {
84
+ const bySemanticKey = new Map();
85
+ for (const agent of agents) {
86
+ const normalized = normalizeAgent(agent);
87
+ if (!normalized.id || isBrokenAgent(normalized)) continue;
88
+ const semanticKey = [
89
+ String(normalized.ownerId || "").trim(),
90
+ String(normalized.ownerName || "").trim().toLowerCase(),
91
+ String(normalized.label || "").trim().toLowerCase(),
92
+ String(normalized.surface || "").trim().toLowerCase(),
93
+ String(normalized.role || "").trim().toLowerCase(),
94
+ String(normalized.provider || "").trim().toLowerCase(),
95
+ String(normalized.model || "").trim().toLowerCase(),
96
+ String(normalized.chatId || "").trim()
97
+ ].join("|");
98
+ const prior = bySemanticKey.get(semanticKey);
99
+ if (!prior) {
100
+ bySemanticKey.set(semanticKey, normalized);
101
+ continue;
102
+ }
103
+ const priorUpdated = Date.parse(String(prior.updatedAt || "").trim()) || 0;
104
+ const nextUpdated = Date.parse(String(normalized.updatedAt || "").trim()) || 0;
105
+ if (nextUpdated >= priorUpdated) {
106
+ bySemanticKey.set(semanticKey, normalized);
107
+ }
108
+ }
109
+ return [...bySemanticKey.values()];
110
+ }
111
+
74
112
  function areAgentsEquivalent(left = {}, right = {}) {
75
113
  return [
76
114
  "ownerId",
@@ -119,12 +157,8 @@ function buildProjectParticipants(project = {}) {
119
157
  }
120
158
 
121
159
  function buildProjectAgents(project = {}) {
122
- const existing = new Map(
123
- (Array.isArray(project.agents) ? project.agents : [])
124
- .map((agent) => normalizeAgent(agent))
125
- .filter((agent) => agent.id)
126
- .map((agent) => [agent.id, agent])
127
- );
160
+ const existingAgents = dedupeAgents(Array.isArray(project.agents) ? project.agents : []);
161
+ const existing = new Map(existingAgents.map((agent) => [agent.id, agent]));
128
162
  const next = [];
129
163
  const activeOperatorId = String(project.activeOperator?.id || "").trim();
130
164
  if (activeOperatorId) {
@@ -145,7 +179,7 @@ function buildProjectAgents(project = {}) {
145
179
  if (next.some((item) => item.id === agent.id)) continue;
146
180
  next.push(normalizeAgent(agent));
147
181
  }
148
- return next;
182
+ return dedupeAgents(next);
149
183
  }
150
184
 
151
185
  function memberRoleWeight(role = "") {
@@ -433,7 +467,12 @@ async function recordSharedProjectEvent(cwd, project, text, { type = "note", act
433
467
  export async function loadSharedProject(cwd) {
434
468
  try {
435
469
  const raw = await fs.readFile(sharedFilePath(cwd), "utf8");
436
- return normalizeSharedProject(JSON.parse(raw), cwd);
470
+ const parsed = JSON.parse(raw);
471
+ const normalized = normalizeSharedProject(parsed, cwd);
472
+ if (JSON.stringify(parsed) !== JSON.stringify(normalized)) {
473
+ await writeJsonAtomically(sharedFilePath(cwd), normalized);
474
+ }
475
+ return normalized;
437
476
  } catch (error) {
438
477
  if (error?.code === "ENOENT") return null;
439
478
  throw error;