@tritard/waterbrother 0.16.65 → 0.16.67

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.65",
3
+ "version": "0.16.67",
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/cli.js CHANGED
@@ -92,6 +92,7 @@ import {
92
92
  removeSharedMember,
93
93
  setSharedRuntimeProfile,
94
94
  setSharedRoomMode,
95
+ upsertSharedAgent,
95
96
  upsertSharedMember
96
97
  } from "./shared-project.js";
97
98
  import { findRelevantRegressions, buildRegressionWarningBlock } from "./regressions.js";
@@ -3004,47 +3005,89 @@ async function stopTrackedGateway(service = "telegram", { io = console } = {}) {
3004
3005
  return { stopped, pid };
3005
3006
  }
3006
3007
 
3007
- function createTelegramBridgeHostRecord({ sessionId, cwd }) {
3008
+ function createTelegramBridgeHostRecord({ sessionId, cwd, label = "", surface = "live-tui", ownerId = "", ownerName = "", provider = "", model = "", runtimeProfile = "" }) {
3008
3009
  const now = new Date().toISOString();
3009
3010
  return {
3010
3011
  pid: process.pid,
3011
3012
  sessionId: String(sessionId || "").trim(),
3012
3013
  cwd: String(cwd || "").trim(),
3014
+ label: String(label || "").trim(),
3015
+ surface: String(surface || "live-tui").trim() || "live-tui",
3016
+ ownerId: String(ownerId || "").trim(),
3017
+ ownerName: String(ownerName || "").trim(),
3018
+ provider: String(provider || "").trim(),
3019
+ model: String(model || "").trim(),
3020
+ runtimeProfile: String(runtimeProfile || "").trim(),
3013
3021
  startedAt: now,
3014
3022
  updatedAt: now
3015
3023
  };
3016
3024
  }
3017
3025
 
3018
- async function registerTelegramBridgeHost({ sessionId, cwd }) {
3026
+ async function syncSharedTelegramBridgeAgent({ cwd, host = {}, actor = {} }) {
3027
+ try {
3028
+ const project = await loadSharedProject(cwd);
3029
+ if (!project?.enabled) return null;
3030
+ return await upsertSharedAgent(cwd, {
3031
+ id: `agent:telegram-bridge:${String(host.sessionId || process.pid).trim()}`,
3032
+ ownerId: String(actor.id || "").trim() || String(host.ownerId || "").trim(),
3033
+ ownerName: String(actor.name || "").trim() || String(host.ownerName || "").trim(),
3034
+ label: String(host.label || "").trim() || "Waterbrother live TUI",
3035
+ surface: String(host.surface || "").trim() || "live-tui",
3036
+ role: "executor",
3037
+ provider: String(host.provider || "").trim(),
3038
+ model: String(host.model || "").trim(),
3039
+ runtimeProfile: String(host.runtimeProfile || "").trim(),
3040
+ sessionId: String(host.sessionId || "").trim(),
3041
+ cwd: String(cwd || "").trim(),
3042
+ chatId: String(project.room?.chatId || "").trim()
3043
+ }, {
3044
+ actorId: String(actor.id || "").trim(),
3045
+ actorName: String(actor.name || "").trim()
3046
+ });
3047
+ } catch {
3048
+ return null;
3049
+ }
3050
+ }
3051
+
3052
+ async function registerTelegramBridgeHost({ sessionId, cwd, label = "", surface = "live-tui", ownerId = "", ownerName = "", provider = "", model = "", runtimeProfile = "", actor = null }) {
3019
3053
  const bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3020
- bridge.activeHost = createTelegramBridgeHostRecord({ sessionId, cwd });
3054
+ bridge.activeHost = createTelegramBridgeHostRecord({ sessionId, cwd, label, surface, ownerId, ownerName, provider, model, runtimeProfile });
3021
3055
  bridge.deliveredReplies = Array.isArray(bridge.deliveredReplies) ? bridge.deliveredReplies.slice(-50) : [];
3022
3056
  bridge.pendingRequests = Array.isArray(bridge.pendingRequests) ? bridge.pendingRequests : [];
3023
3057
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3058
+ await syncSharedTelegramBridgeAgent({ cwd, host: bridge.activeHost, actor: actor || { id: ownerId, name: ownerName } });
3024
3059
  return bridge.activeHost;
3025
3060
  }
3026
3061
 
3027
- async function touchTelegramBridgeHost({ sessionId, cwd }) {
3062
+ async function touchTelegramBridgeHost({ sessionId, cwd, label = "", surface = "live-tui", ownerId = "", ownerName = "", provider = "", model = "", runtimeProfile = "", actor = null }) {
3028
3063
  const bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3029
3064
  const activeHost = bridge.activeHost || {};
3030
3065
  if (Number(activeHost.pid || 0) !== process.pid) {
3031
- bridge.activeHost = createTelegramBridgeHostRecord({ sessionId, cwd });
3066
+ bridge.activeHost = createTelegramBridgeHostRecord({ sessionId, cwd, label, surface, ownerId, ownerName, provider, model, runtimeProfile });
3032
3067
  } else {
3033
3068
  bridge.activeHost = {
3034
3069
  ...activeHost,
3035
3070
  sessionId: String(sessionId || "").trim(),
3036
3071
  cwd: String(cwd || "").trim(),
3072
+ label: String(label || activeHost.label || "").trim(),
3073
+ surface: String(surface || activeHost.surface || "live-tui").trim() || "live-tui",
3074
+ ownerId: String(ownerId || activeHost.ownerId || "").trim(),
3075
+ ownerName: String(ownerName || activeHost.ownerName || "").trim(),
3076
+ provider: String(provider || activeHost.provider || "").trim(),
3077
+ model: String(model || activeHost.model || "").trim(),
3078
+ runtimeProfile: String(runtimeProfile || activeHost.runtimeProfile || "").trim(),
3037
3079
  updatedAt: new Date().toISOString()
3038
3080
  };
3039
3081
  }
3040
3082
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3083
+ await syncSharedTelegramBridgeAgent({ cwd, host: bridge.activeHost, actor: actor || { id: ownerId, name: ownerName } });
3041
3084
  return bridge.activeHost;
3042
3085
  }
3043
3086
 
3044
3087
  async function clearTelegramBridgeHost() {
3045
3088
  const bridge = await loadGatewayBridge(TELEGRAM_BRIDGE_SERVICE);
3046
3089
  if (Number(bridge.activeHost?.pid || 0) === process.pid) {
3047
- bridge.activeHost = { pid: 0, sessionId: "", cwd: "", startedAt: "", updatedAt: "" };
3090
+ bridge.activeHost = { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" };
3048
3091
  await saveGatewayBridge(TELEGRAM_BRIDGE_SERVICE, bridge);
3049
3092
  }
3050
3093
  }
@@ -7458,7 +7501,23 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7458
7501
 
7459
7502
  const gatewayHandle = await maybeAutostartGateway(context.runtime, { cwd: context.cwd });
7460
7503
  let gatewayCleanupDone = false;
7461
- await registerTelegramBridgeHost({ sessionId: currentSession.id, cwd: context.cwd });
7504
+ const telegramBridgeOperator = buildOperatorIdentity({
7505
+ mode: agent.getExperienceMode(),
7506
+ autonomy: agent.getAutonomyMode(),
7507
+ profile: agent.getProfile()
7508
+ });
7509
+ await registerTelegramBridgeHost({
7510
+ sessionId: currentSession.id,
7511
+ cwd: context.cwd,
7512
+ label: `${telegramBridgeOperator.name} terminal`,
7513
+ surface: "live-tui",
7514
+ ownerId: telegramBridgeOperator.id,
7515
+ ownerName: telegramBridgeOperator.name,
7516
+ provider: context.runtime.provider,
7517
+ model: context.runtime.model,
7518
+ runtimeProfile: currentSession.runtimeProfile || "",
7519
+ actor: { id: telegramBridgeOperator.id, name: telegramBridgeOperator.name }
7520
+ });
7462
7521
  const cleanupManagedGateway = async () => {
7463
7522
  if (gatewayCleanupDone) return;
7464
7523
  gatewayCleanupDone = true;
@@ -7494,7 +7553,18 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7494
7553
  if (sharedFeed.latestEventId) {
7495
7554
  lastSeenSharedRoomEventId = sharedFeed.latestEventId;
7496
7555
  }
7497
- await touchTelegramBridgeHost({ sessionId: currentSession.id, cwd: context.cwd });
7556
+ await touchTelegramBridgeHost({
7557
+ sessionId: currentSession.id,
7558
+ cwd: context.cwd,
7559
+ label: `${telegramBridgeOperator.name} terminal`,
7560
+ surface: "live-tui",
7561
+ ownerId: telegramBridgeOperator.id,
7562
+ ownerName: telegramBridgeOperator.name,
7563
+ provider: context.runtime.provider,
7564
+ model: context.runtime.model,
7565
+ runtimeProfile: currentSession.runtimeProfile || "",
7566
+ actor: { id: telegramBridgeOperator.id, name: telegramBridgeOperator.name }
7567
+ });
7498
7568
  const nextInput = await readInteractiveLine({
7499
7569
  getFooterText(inputBuffer) {
7500
7570
  const footer = buildInteractiveFooter({
@@ -7574,7 +7644,18 @@ Be concrete about surfaces — name actual pages/flows. Choose the best stack fo
7574
7644
  }
7575
7645
 
7576
7646
  remoteSessionId = currentSession.id;
7577
- await touchTelegramBridgeHost({ sessionId: currentSession.id, cwd: context.cwd });
7647
+ await touchTelegramBridgeHost({
7648
+ sessionId: currentSession.id,
7649
+ cwd: context.cwd,
7650
+ label: `${telegramBridgeOperator.name} terminal`,
7651
+ surface: "live-tui",
7652
+ ownerId: telegramBridgeOperator.id,
7653
+ ownerName: telegramBridgeOperator.name,
7654
+ provider: context.runtime.provider,
7655
+ model: context.runtime.model,
7656
+ runtimeProfile: currentSession.runtimeProfile || "",
7657
+ actor: { id: telegramBridgeOperator.id, name: telegramBridgeOperator.name }
7658
+ });
7578
7659
  if (remoteRequest.runtimeProfile && remoteRequest.runtimeProfile !== currentSession.runtimeProfile) {
7579
7660
  const { config } = await loadConfigLayers(context.cwd);
7580
7661
  const loaded = await loadNamedRuntimeProfile(remoteRequest.runtimeProfile, {
@@ -83,10 +83,17 @@ function normalizeGatewayBridge(parsed = {}) {
83
83
  pid: Number.isFinite(Number(parsed.activeHost.pid)) ? Math.floor(Number(parsed.activeHost.pid)) : 0,
84
84
  sessionId: String(parsed.activeHost.sessionId || "").trim(),
85
85
  cwd: String(parsed.activeHost.cwd || "").trim(),
86
+ label: String(parsed.activeHost.label || "").trim(),
87
+ surface: String(parsed.activeHost.surface || "").trim(),
88
+ ownerId: String(parsed.activeHost.ownerId || "").trim(),
89
+ ownerName: String(parsed.activeHost.ownerName || "").trim(),
90
+ provider: String(parsed.activeHost.provider || "").trim(),
91
+ model: String(parsed.activeHost.model || "").trim(),
92
+ runtimeProfile: String(parsed.activeHost.runtimeProfile || "").trim(),
86
93
  startedAt: String(parsed.activeHost.startedAt || "").trim(),
87
94
  updatedAt: String(parsed.activeHost.updatedAt || "").trim()
88
95
  }
89
- : { pid: 0, sessionId: "", cwd: "", startedAt: "", updatedAt: "" },
96
+ : { pid: 0, sessionId: "", cwd: "", label: "", surface: "", ownerId: "", ownerName: "", provider: "", model: "", runtimeProfile: "", startedAt: "", updatedAt: "" },
90
97
  pendingRequests,
91
98
  deliveredReplies
92
99
  };
package/src/gateway.js CHANGED
@@ -285,6 +285,32 @@ function formatGatewaySessionStatus({ sessionId, userId, username, cwd, runtimeP
285
285
  return bits.join("\n");
286
286
  }
287
287
 
288
+ function listProjectParticipants(project) {
289
+ return Array.isArray(project?.participants) ? project.participants : [];
290
+ }
291
+
292
+ function listProjectAgents(project) {
293
+ return Array.isArray(project?.agents) ? project.agents : [];
294
+ }
295
+
296
+ function findProjectParticipant(project, memberId = "") {
297
+ const normalizedId = String(memberId || "").trim();
298
+ if (!normalizedId) return null;
299
+ return listProjectParticipants(project).find((participant) => String(participant?.memberId || participant?.id || "").trim() === normalizedId) || null;
300
+ }
301
+
302
+ function formatAgentLabel(agent = {}) {
303
+ const label = String(agent?.label || agent?.name || "").trim();
304
+ const role = String(agent?.role || "").trim();
305
+ const provider = String(agent?.provider || "").trim();
306
+ const model = String(agent?.model || "").trim();
307
+ const bits = [];
308
+ if (label) bits.push(label);
309
+ if (role) bits.push(`(${role})`);
310
+ if (provider && model) bits.push(`[${provider}/${model}]`);
311
+ return bits.join(" ").trim();
312
+ }
313
+
288
314
  function formatTelegramSummaryMarkup({ cwd, project, chatId = "", title = "", executor = {} }) {
289
315
  const roomLabel = project?.room?.chatId
290
316
  ? `${project.room.provider || "telegram"} ${project.room.chatId}${project.room.title ? ` (${project.room.title})` : ""}`
@@ -299,10 +325,15 @@ function formatTelegramSummaryMarkup({ cwd, project, chatId = "", title = "", ex
299
325
  `shared: <code>${project?.enabled ? "yes" : "no"}</code>`
300
326
  ];
301
327
  if (project?.enabled) {
328
+ const participants = listProjectParticipants(project);
329
+ const agents = listProjectAgents(project);
302
330
  lines.push(`room mode: <code>${escapeTelegramHtml(project.roomMode || "chat")}</code>`);
303
331
  lines.push(`bound chat: <code>${escapeTelegramHtml(roomLabel)}</code>`);
304
332
  lines.push(`active operator: <code>${escapeTelegramHtml(activeOperator)}</code>`);
305
- lines.push(`members: <code>${escapeTelegramHtml(String((project.members || []).length))}</code>`);
333
+ lines.push(`participants: <code>${escapeTelegramHtml(String(participants.length || (project.members || []).length))}</code>`);
334
+ if (agents.length) {
335
+ lines.push(`agents: <code>${escapeTelegramHtml(String(agents.length))}</code>`);
336
+ }
306
337
  if (executor?.surface) lines.push(`executor: <code>${escapeTelegramHtml(executor.surface)}</code>`);
307
338
  if (executor?.provider && executor?.model) {
308
339
  lines.push(`runtime: <code>${escapeTelegramHtml(`${executor.provider}/${executor.model}`)}</code>`);
@@ -489,6 +520,8 @@ function formatTelegramRoomMarkup(project, options = {}) {
489
520
  return "<b>Shared room</b>\nThis project is not shared.";
490
521
  }
491
522
  const members = Array.isArray(project.members) ? project.members : [];
523
+ const participants = listProjectParticipants(project);
524
+ const agents = listProjectAgents(project);
492
525
  const active = project.activeOperator?.id
493
526
  ? `${project.activeOperator.name || project.activeOperator.id} (${project.activeOperator.id})`
494
527
  : "none";
@@ -513,6 +546,20 @@ function formatTelegramRoomMarkup(project, options = {}) {
513
546
  .sort((a, b) => b[1] - a[1])
514
547
  .slice(0, 4)
515
548
  .map(([assignee, count]) => `• <code>${escapeTelegramHtml(assignee)}</code> ${count}`);
549
+ const participantLines = participants.length
550
+ ? participants.map((participant) => {
551
+ const channelBits = [];
552
+ if (participant?.channels?.telegram?.userId) channelBits.push(`tg:${participant.channels.telegram.userId}`);
553
+ if (participant?.channels?.local?.username) channelBits.push(`local:${participant.channels.local.username}`);
554
+ const label = participant.displayName || participant.memberId || participant.id || "unknown";
555
+ return `• ${escapeTelegramHtml(label)} <i>(${escapeTelegramHtml(participant.role || "editor")})</i>${channelBits.length ? ` <code>${escapeTelegramHtml(channelBits.join(" "))}</code>` : ""}`;
556
+ })
557
+ : members.length
558
+ ? members.map((member) => `• ${escapeTelegramHtml(member.name || member.id)} <i>(${escapeTelegramHtml(member.role || "editor")})</i>`)
559
+ : ["• none"];
560
+ const agentLines = agents.length
561
+ ? agents.map((agent) => `• ${escapeTelegramHtml(formatAgentLabel(agent) || agent.id || "agent")} <code>${escapeTelegramHtml(agent.surface || "unknown")}</code>`)
562
+ : ["• none"];
516
563
  const executorBits = [
517
564
  `surface: <code>${escapeTelegramHtml(executor.surface || "telegram")}</code>`,
518
565
  `provider: <code>${escapeTelegramHtml(executor.provider || "unknown")}</code>`,
@@ -542,8 +589,10 @@ function formatTelegramRoomMarkup(project, options = {}) {
542
589
  ...[...byState.entries()].map(([state, count]) => `${escapeTelegramHtml(state)}: <code>${count}</code>`),
543
590
  "<b>Ownership</b>",
544
591
  ...(ownershipLines.length ? ownershipLines : ["• none"]),
545
- "<b>Members</b>",
546
- memberLines
592
+ "<b>Participants</b>",
593
+ ...participantLines,
594
+ "<b>Agents</b>",
595
+ ...agentLines
547
596
  ].join("\n");
548
597
  }
549
598
 
@@ -576,13 +625,24 @@ function formatTelegramMembersMarkup(project) {
576
625
  if (!project?.enabled) {
577
626
  return "This project is not shared.";
578
627
  }
579
- const members = Array.isArray(project.members) ? project.members : [];
580
- if (!members.length) {
581
- return "<b>Shared members</b>\n• none";
628
+ const participants = listProjectParticipants(project);
629
+ if (!participants.length) {
630
+ const members = Array.isArray(project.members) ? project.members : [];
631
+ if (!members.length) {
632
+ return "<b>Shared participants</b>\n• none";
633
+ }
634
+ return [
635
+ "<b>Shared participants</b>",
636
+ ...members.map((member) => `• ${escapeTelegramHtml(member.name || member.id)} <i>(${escapeTelegramHtml(member.role || "editor")})</i> <code>${escapeTelegramHtml(member.id || "")}</code>`)
637
+ ].join("\n");
582
638
  }
583
639
  return [
584
- "<b>Shared members</b>",
585
- ...members.map((member) => `• ${escapeTelegramHtml(member.name || member.id)} <i>(${escapeTelegramHtml(member.role || "editor")})</i> <code>${escapeTelegramHtml(member.id || "")}</code>`)
640
+ "<b>Shared participants</b>",
641
+ ...participants.map((participant) => {
642
+ const id = participant.memberId || participant.id || "";
643
+ const telegramId = participant?.channels?.telegram?.userId ? ` tg:${participant.channels.telegram.userId}` : "";
644
+ return `• ${escapeTelegramHtml(participant.displayName || id)} <i>(${escapeTelegramHtml(participant.role || "editor")})</i> <code>${escapeTelegramHtml(`${id}${telegramId}`.trim())}</code>`;
645
+ })
586
646
  ].join("\n");
587
647
  }
588
648
 
@@ -656,11 +716,13 @@ function formatTelegramPeopleMarkup({ people = [], project = null }) {
656
716
  "<b>Recent Telegram people</b>",
657
717
  ...people.map((person) => {
658
718
  const member = members.find((entry) => String(entry?.id || "").trim() === person.userId);
719
+ const participant = findProjectParticipant(project, person.userId);
659
720
  const pendingInvite = pendingInvites.find((entry) => String(entry?.memberId || "").trim() === person.userId);
660
721
  const bits = [`<code>${escapeTelegramHtml(person.userId)}</code>`];
661
722
  if (person.usernameHandle) bits.push(`@${escapeTelegramHtml(person.usernameHandle)}`);
662
723
  bits.push(escapeTelegramHtml(person.displayName || person.userId));
663
- if (member?.role) bits.push(`<i>member:${escapeTelegramHtml(member.role)}</i>`);
724
+ if (participant?.role) bits.push(`<i>participant:${escapeTelegramHtml(participant.role)}</i>`);
725
+ else if (member?.role) bits.push(`<i>member:${escapeTelegramHtml(member.role)}</i>`);
664
726
  else if (pendingInvite?.id) bits.push(`<i>pending:${escapeTelegramHtml(pendingInvite.id)}</i>`);
665
727
  else if (person.paired) bits.push("<i>paired</i>");
666
728
  return `• ${bits.join(" ")}`;
@@ -1116,6 +1178,9 @@ class TelegramGateway {
1116
1178
  }
1117
1179
 
1118
1180
  buildTrustedRoomIntroMarkup({ project, actor }) {
1181
+ const participants = listProjectParticipants(project);
1182
+ const agents = listProjectAgents(project);
1183
+ const executorAgent = agents.find((agent) => String(agent?.role || "").trim() === "executor") || null;
1119
1184
  return [
1120
1185
  "<b>Roundtable room</b>",
1121
1186
  `${escapeTelegramHtml(actor.displayName || actor.userId)} is now in <code>${escapeTelegramHtml(project.projectName || "this project")}</code> as <code>observer</code>.`,
@@ -1123,10 +1188,12 @@ class TelegramGateway {
1123
1188
  project.activeOperator?.id
1124
1189
  ? `active operator: <code>${escapeTelegramHtml(project.activeOperator.name || project.activeOperator.id)}</code>`
1125
1190
  : "active operator: <code>none</code>",
1191
+ `participants: <code>${escapeTelegramHtml(String(participants.length || (project.members || []).length))}</code>`,
1192
+ executorAgent ? `active terminal: <code>${escapeTelegramHtml(formatAgentLabel(executorAgent) || executorAgent.id)}</code>` : "",
1126
1193
  "As observer, you can ask questions, discuss plans, and review work here.",
1127
1194
  "An owner can promote you to editor or owner conversationally.",
1128
1195
  "Examples: <code>what project is this chat bound to?</code>, <code>who is in the room?</code>, <code>what can I do here?</code>"
1129
- ].join("\n");
1196
+ ].filter(Boolean).join("\n");
1130
1197
  }
1131
1198
 
1132
1199
  describeTelegramUser(from = {}) {
@@ -1744,10 +1811,10 @@ class TelegramGateway {
1744
1811
  const { project } = await this.bindSharedRoomForMessage(message, sessionId);
1745
1812
  const host = await this.getLiveBridgeHost();
1746
1813
  const executor = {
1747
- surface: host ? "live-tui" : "telegram-fallback",
1748
- provider: this.runtime.provider,
1749
- model: this.runtime.model,
1750
- runtimeProfile: project?.runtimeProfile || this.channel.defaultRuntimeProfile || this.gateway.defaultRuntimeProfile || "",
1814
+ surface: host?.surface || (host ? "live-tui" : "telegram-fallback"),
1815
+ provider: host?.provider || this.runtime.provider,
1816
+ model: host?.model || this.runtime.model,
1817
+ runtimeProfile: host?.runtimeProfile || project?.runtimeProfile || this.channel.defaultRuntimeProfile || this.gateway.defaultRuntimeProfile || "",
1751
1818
  hostSessionId: host?.sessionId || ""
1752
1819
  };
1753
1820
  return project?.enabled
@@ -57,6 +57,7 @@ function normalizeAgent(agent = {}) {
57
57
  return {
58
58
  id: String(agent.id || "").trim(),
59
59
  ownerId: String(agent.ownerId || "").trim(),
60
+ ownerName: String(agent.ownerName || "").trim(),
60
61
  label: String(agent.label || agent.name || "").trim(),
61
62
  surface: String(agent.surface || "").trim(),
62
63
  role: AGENT_ROLES.includes(String(agent.role || "").trim()) ? String(agent.role).trim() : "standby",
@@ -64,6 +65,8 @@ function normalizeAgent(agent = {}) {
64
65
  model: String(agent.model || "").trim(),
65
66
  runtimeProfile: String(agent.runtimeProfile || "").trim(),
66
67
  sessionId: String(agent.sessionId || "").trim(),
68
+ cwd: String(agent.cwd || "").trim(),
69
+ chatId: String(agent.chatId || "").trim(),
67
70
  updatedAt: String(agent.updatedAt || new Date().toISOString()).trim()
68
71
  };
69
72
  }
@@ -602,6 +605,38 @@ export async function upsertSharedMember(cwd, member = {}, options = {}) {
602
605
  })).project;
603
606
  }
604
607
 
608
+ export async function upsertSharedAgent(cwd, agent = {}, options = {}) {
609
+ const existing = await loadSharedProject(cwd);
610
+ requireSharedProject(existing);
611
+ const nextAgent = normalizeAgent(agent);
612
+ if (!nextAgent.id) throw new Error("agent id is required");
613
+ if (options.actorId) {
614
+ requireMember(existing, options.actorId);
615
+ }
616
+ const agents = Array.isArray(existing.agents) ? [...existing.agents] : [];
617
+ const index = agents.findIndex((item) => item.id === nextAgent.id);
618
+ if (index >= 0) {
619
+ agents[index] = normalizeAgent({ ...agents[index], ...nextAgent });
620
+ } else {
621
+ agents.push(nextAgent);
622
+ }
623
+ const next = await saveSharedProject(cwd, {
624
+ ...existing,
625
+ agents
626
+ });
627
+ return (await recordSharedProjectEvent(cwd, next, `agent ${nextAgent.label || nextAgent.id} registered as ${nextAgent.role}`, {
628
+ type: "agent-upsert",
629
+ actorId: String(options.actorId || "").trim(),
630
+ actorName: String(options.actorName || "").trim(),
631
+ meta: {
632
+ agentId: nextAgent.id,
633
+ ownerId: nextAgent.ownerId,
634
+ role: nextAgent.role,
635
+ surface: nextAgent.surface
636
+ }
637
+ })).project;
638
+ }
639
+
605
640
  export async function createSharedInvite(cwd, member = {}, options = {}) {
606
641
  const existing = await loadSharedProject(cwd);
607
642
  requireOwner(existing, options.actorId);