@ctliz/agent-intercom-claude 0.14.1 → 0.15.0

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/claude/runtime.ts CHANGED
@@ -10,15 +10,14 @@ import { appendInboxMessage, defaultInboxPath } from "./inbox.ts";
10
10
  import type { Attachment, Message, SessionInfo } from "../types.ts";
11
11
  import { formatIntercomTeam, resolveIntercomTeam } from "./team.ts";
12
12
  import {
13
- createNamedTeam,
14
- findNamedTeam,
15
13
  formatCreateSuccess,
16
14
  formatJoinableNamedTeamList,
17
15
  formatNamedJoinSuccess,
18
16
  listNamedTeams,
19
17
  parseTeamName,
20
- rejectManagedJoin,
21
18
  } from "./named-teams.ts";
19
+ import { appendNamedTeamMembership, sessionNamedTeams, namedTeamRoster, formatNamedTeamRoster, resolveNamedMessageTeam } from "./named-team-membership.ts";
20
+ import { contextId, askId, replyHint, selectReplyContext } from "./reply-context.ts";
22
21
 
23
22
  export interface ClaudeRuntimeIdentity {
24
23
  sessionId: string;
@@ -78,6 +77,9 @@ function publicPendingEntry(entry: PendingInboundMessage, selector?: string): Re
78
77
  ...(entry.from.parentSessionId ? { parent_session_id: entry.from.parentSessionId } : {}),
79
78
  ...(entry.from.generation ? { generation: entry.from.generation } : {}),
80
79
  },
80
+ contextId: contextId(entry),
81
+ ...(entry.message.expectsReply ? { askId: askId(entry) } : {}),
82
+ ...(entry.message.content.team ? { team: entry.message.content.team } : {}),
81
83
  received_at: entry.receivedAt,
82
84
  read: entry.read,
83
85
  text: entry.message.content.text,
@@ -96,6 +98,7 @@ export interface ToolResult {
96
98
  interface ReplyWaiter {
97
99
  from: string;
98
100
  replyTo: string;
101
+ team?: string;
99
102
  resolve: (message: Message) => void;
100
103
  reject: (error: Error) => void;
101
104
  timeout: NodeJS.Timeout;
@@ -236,7 +239,7 @@ export class ClaudeIntercomRuntime {
236
239
  // reconnects reuse the same private scope even if process.env is mutated later.
237
240
  this.capturedScopeId = intercomScopeIdFromEnvForRegistration(process.env);
238
241
  // Reconnects reuse capturedScopeId even if process.env is mutated later.
239
- // An explicit join/create updates capturedScopeId, then rebuilds the client.
242
+ // Task-team joins update membership only; registration scope stays fixed.
240
243
  this.clientFactory = options.clientFactory ?? (() => new IntercomClient({
241
244
  env: this.capturedScopeId ? { AGENT_INTERCOM_SCOPE_ID: this.capturedScopeId } : {},
242
245
  }));
@@ -401,25 +404,23 @@ export class ClaudeIntercomRuntime {
401
404
  await this.connect();
402
405
  }
403
406
 
404
- async join(name?: string, create = false): Promise<ToolResult> {
405
- const blocked = rejectManagedJoin();
406
- if (blocked) return textResult(blocked, { ok: false }, true);
407
+ async join(name?: string, create = false, members?: string[], work?: string): Promise<ToolResult> {
407
408
  try {
408
- if (create) {
409
- if (typeof name !== "string" || !name.trim()) {
410
- return textResult("Creating a team requires a name.", { ok: false }, true);
411
- }
412
- const team = createNamedTeam({ name: parseTeamName(name), managerSessionId: this.identity.sessionId });
413
- await this.switchRuntimeScope(team.scopeId, team.managerSessionId);
414
- return textResult(formatCreateSuccess({ team: team.name, name: this.identity.name }), { ok: true, team: team.name, role: "manager" });
415
- }
416
409
  if (!name?.trim()) {
410
+ if (create || members?.length || work !== undefined) throw new Error("Creating or extending a team requires a name");
417
411
  return textResult(formatJoinableNamedTeamList(listNamedTeams()));
418
412
  }
419
- const team = findNamedTeam(parseTeamName(name));
420
- if (!team) return textResult("Could not join that team.", { ok: false }, true);
421
- await this.switchRuntimeScope(team.scopeId, team.managerSessionId);
422
- return textResult(formatNamedJoinSuccess({ team: team.name, name: this.identity.name }), { ok: true, team: team.name, role: "teammate" });
413
+ const client = await this.connect();
414
+ const sessions = await client.listSessions();
415
+ const memberIds = (members ?? []).map((member) => {
416
+ const id = resolveSessionTarget(sessions, member);
417
+ if (!id) throw new Error(`Session "${member}" is not connected`);
418
+ return id;
419
+ });
420
+ const team = await appendNamedTeamMembership({ name: parseTeamName(name), selfId: this.identity.sessionId, members: memberIds, create, work });
421
+ const roster = namedTeamRoster(team, this.identity.sessionId, sessions);
422
+ const notice = create ? formatCreateSuccess({ team: team.name, name: this.identity.name }) : formatNamedJoinSuccess({ team: team.name, name: this.identity.name });
423
+ return textResult(`${notice}\nOther team memberships are unchanged.`, { ok: true, team: team.name, role: roster.self.isManager ? "manager" : "member", roster });
423
424
  } catch (error) {
424
425
  return textResult(error instanceof Error ? error.message : String(error), { ok: false }, true);
425
426
  }
@@ -430,7 +431,7 @@ export class ClaudeIntercomRuntime {
430
431
  if (waiter) {
431
432
  const senderTarget = from.name || from.id;
432
433
  const fromMatches = senderTarget.toLowerCase() === waiter.from.toLowerCase() || from.id === waiter.from;
433
- if (fromMatches) {
434
+ if (fromMatches && message.content.team === waiter.team) {
434
435
  this.replyWaiters.delete(waiter.replyTo);
435
436
  clearTimeout(waiter.timeout);
436
437
  waiter.cleanup?.();
@@ -454,7 +455,7 @@ export class ClaudeIntercomRuntime {
454
455
  }
455
456
  }
456
457
 
457
- private waitForReply(from: string, replyTo: string, timeoutMs = getAskTimeoutMs(), signal?: AbortSignal): Promise<Message> {
458
+ private waitForReply(from: string, replyTo: string, timeoutMs = getAskTimeoutMs(), signal?: AbortSignal, team?: string): Promise<Message> {
458
459
  return new Promise((resolve, reject) => {
459
460
  if (signal?.aborted) {
460
461
  reject(new Error("intercom_ask cancelled"));
@@ -478,7 +479,7 @@ export class ClaudeIntercomRuntime {
478
479
  reject(new Error(`No reply from "${from}" within ${Math.round(timeoutMs / 1000)} seconds`));
479
480
  }, timeoutMs);
480
481
  signal?.addEventListener("abort", onAbort, { once: true });
481
- this.replyWaiters.set(replyTo, { from, replyTo, resolve, reject, timeout, cleanup });
482
+ this.replyWaiters.set(replyTo, { from, replyTo, team, resolve, reject, timeout, cleanup });
482
483
  });
483
484
  }
484
485
 
@@ -497,9 +498,16 @@ export class ClaudeIntercomRuntime {
497
498
  );
498
499
  }
499
500
 
500
- async team(): Promise<ToolResult> {
501
+ async team(name?: string): Promise<ToolResult> {
501
502
  const client = await this.connect();
502
503
  const sessions = await client.listSessions();
504
+ const mine = sessionNamedTeams(this.identity.sessionId);
505
+ const selected = name ? mine.filter((entry) => entry.name === name) : mine;
506
+ if (name && !selected.length) return textResult(`You do not belong to team "${name}"`, { ok: false }, true);
507
+ if (selected.length) {
508
+ const teams = selected.map((entry) => namedTeamRoster(entry, this.identity.sessionId, sessions));
509
+ return textResult(teams.map(formatNamedTeamRoster).join("\n\n"), { teams });
510
+ }
503
511
  const team = await resolveIntercomTeam({ selfId: client.sessionId ?? this.identity.sessionId, sessions });
504
512
  return textResult(formatIntercomTeam(team), team as unknown as Record<string, unknown>);
505
513
  }
@@ -549,10 +557,14 @@ export class ClaudeIntercomRuntime {
549
557
  return textResult("Summary updated.", { ok: true, summary });
550
558
  }
551
559
 
552
- async send(to: string, message: string, attachments?: Attachment[], replyTo?: string): Promise<ToolResult> {
560
+ async send(to: string, message: string, attachments?: Attachment[], replyTo?: string, requestedTeam?: string): Promise<ToolResult> {
553
561
  const client = await this.connect();
554
562
  const sendTo = await this.resolveTarget(to);
555
- const result = await client.send(sendTo, { text: message, attachments, replyTo });
563
+ const source = replyTo ? this.unread.find((entry) => entry.message.id === replyTo && entry.from.id === sendTo) : undefined;
564
+ if (replyTo && !source) throw new Error("Unknown inbound reply context");
565
+ if (source && requestedTeam !== undefined && requestedTeam !== source.message.content.team) throw new Error("Reply team must match the original message");
566
+ const team = source ? source.message.content.team : resolveNamedMessageTeam(this.identity.sessionId, sendTo, requestedTeam);
567
+ const result = await client.send(sendTo, { text: message, attachments, replyTo: source?.message.expectsReply ? replyTo : undefined, team });
556
568
  if (!result.delivered) {
557
569
  return textResult(`Message to "${to}" was not delivered: ${result.reason ?? "Session may not exist or has disconnected."}`, { ok: false, message_id: result.id, reason: result.reason }, true);
558
570
  }
@@ -560,11 +572,12 @@ export class ClaudeIntercomRuntime {
560
572
  return textResult(`Message sent to ${to}.`, { ok: true, message_id: result.id, to });
561
573
  }
562
574
 
563
- async ask(to: string, message: string, attachments?: Attachment[], timeoutMs = getAskTimeoutMs(), signal?: AbortSignal): Promise<ToolResult> {
575
+ async ask(to: string, message: string, attachments?: Attachment[], timeoutMs = getAskTimeoutMs(), signal?: AbortSignal, requestedTeam?: string): Promise<ToolResult> {
564
576
  const client = await this.connect();
565
577
  const sendTo = await this.resolveTarget(to);
578
+ const team = resolveNamedMessageTeam(this.identity.sessionId, sendTo, requestedTeam);
566
579
  const questionId = randomUUID();
567
- const replyPromise = this.waitForReply(sendTo, questionId, timeoutMs, signal);
580
+ const replyPromise = this.waitForReply(sendTo, questionId, timeoutMs, signal, team);
568
581
  void replyPromise.catch(() => undefined);
569
582
  try {
570
583
  const result = await client.send(sendTo, {
@@ -572,6 +585,7 @@ export class ClaudeIntercomRuntime {
572
585
  text: message,
573
586
  attachments,
574
587
  expectsReply: true,
588
+ team,
575
589
  });
576
590
  if (!result.delivered) {
577
591
  this.replyWaiters.get(questionId)?.reject(new Error(result.reason ?? "Session may not exist or has disconnected."));
@@ -596,12 +610,12 @@ export class ClaudeIntercomRuntime {
596
610
  const pendingAsks = Array.from(this.unresolvedAsks.values()).sort((a, b) => a.receivedAt - b.receivedAt);
597
611
  const lines = [
598
612
  unreadMessages.length
599
- ? unreadMessages.map((entry) => `- ${formatSessionDisplay(entry.from)}: ${entry.message.content.text}${formatAttachments(entry.message.content.attachments)}`).join("\n")
613
+ ? unreadMessages.map((entry) => `- ${formatSessionDisplay(entry.from)}${replyHint(entry)}: ${entry.message.content.text}${formatAttachments(entry.message.content.attachments)}`).join("\n")
600
614
  : "No unread messages.",
601
615
  pendingAsks.length
602
616
  ? `\nPending asks:\n${pendingAsks.map((entry) => {
603
617
  const selector = pendingSelector(pendingAsks, entry);
604
- return `- ${formatSessionDisplay(entry.from)}${selector ? ` [${selector}]` : ""}: ${entry.message.content.text}`;
618
+ return `- ${formatSessionDisplay(entry.from)}${replyHint(entry)}${selector ? ` [${selector}]` : ""}: ${entry.message.content.text}`;
605
619
  }).join("\n")}`
606
620
  : "",
607
621
  ].filter(Boolean);
@@ -611,10 +625,10 @@ export class ClaudeIntercomRuntime {
611
625
  });
612
626
  }
613
627
 
614
- async reply(message: string, to?: string, which?: ReplyWhich): Promise<ToolResult> {
628
+ async reply(message: string, to?: string, which?: ReplyWhich, askId?: string, contextId?: string, team?: string): Promise<ToolResult> {
615
629
  let target: PendingInboundMessage;
616
630
  try {
617
- target = selectPendingAsk(Array.from(this.unresolvedAsks.values()), to, which);
631
+ target = selectReplyContext(this.unread, Array.from(this.unresolvedAsks.values()), { to, which, askId, contextId, team });
618
632
  } catch (error) {
619
633
  return textResult(error instanceof Error ? error.message : String(error), { ok: false }, true);
620
634
  }
@@ -0,0 +1,7 @@
1
+ export const TASK_TEAM_GUIDANCE = `Intercom task-team rules:
2
+ - When the user delegates to named peers and this task has no approved team, ask once whether to form a team with you and those peers. Wait for approval before creating a team or adding peers. An explicit create/join request is approval; never ask again for an approved task or inbound team message.
3
+ - After approval, discover the connected peers, then intercom_join({ name: "launch", create: true, members: ["front", "writer"], work: "Current task" }) adds everyone in one call. Do not ask the user to join each terminal manually.
4
+ - Membership is additive: joining another team preserves previous teams. Reuse the approved team for the same task; a different task may need a different team.
5
+ - Initial contact without a shared team is allowed: omit team for an ungrouped direct message. Unrelated team memberships must not prevent contact or silently create teams.
6
+ - Include team on task sends/asks, especially when peers share multiple teams. Replies inherit the original message's team via askId or contextId from intercom_pending; never override it using a current team or mix contexts from different tasks.
7
+ - Use intercom_send for assignments, progress/status requests, notifications and follow-ups. Use intercom_ask only when your next step genuinely depends on the answer; keep only one unresolved ask per recipient.`;
@@ -72,6 +72,7 @@ function formatMessage(from: SessionInfo, message: Message, agent: WorkerAgentCo
72
72
  return [
73
73
  `Intercom message for ${agent.name}.`,
74
74
  `From: ${formatSessionDisplay(from)} (${from.id})`,
75
+ ...(message.content.team ? [`[Team: ${message.content.team}]`] : []),
75
76
  `Message id: ${message.id}`,
76
77
  "",
77
78
  message.content.text,
@@ -235,6 +236,7 @@ export class VirtualClaudeAgent {
235
236
  void this.client.send(from.id, {
236
237
  text: "This worker is rate limited (too many wakes this minute). Please try again shortly.",
237
238
  replyTo: message.id,
239
+ ...(message.content.team === undefined ? {} : { team: message.content.team }),
238
240
  }).catch((error) => {
239
241
  process.stderr.write(`worker ${this.agent.id}: rate-limit reply failed: ${error instanceof Error ? error.message : String(error)}\n`);
240
242
  });
@@ -305,6 +307,7 @@ export class VirtualClaudeAgent {
305
307
  await this.client.send(from.id, {
306
308
  text: result.result || "(the worker finished without a final message)",
307
309
  replyTo: message.id,
310
+ ...(message.content.team === undefined ? {} : { team: message.content.team }),
308
311
  });
309
312
  process.stderr.write(`worker ${this.agent.id}: replied to ${from.name || from.id} (session ${this.sessionId ?? "none"})\n`);
310
313
  }
@@ -316,6 +319,7 @@ export class VirtualClaudeAgent {
316
319
  await this.client.send(from.id, {
317
320
  text: `Worker error: ${text}`,
318
321
  replyTo: message.id,
322
+ ...(message.content.team === undefined ? {} : { team: message.content.team }),
319
323
  }).catch((sendError) => {
320
324
  process.stderr.write(`worker ${this.agent.id}: error reply failed: ${sendError instanceof Error ? sendError.message : String(sendError)}\n`);
321
325
  });
@@ -3,8 +3,10 @@ description: Create a named intercom team and join it as manager
3
3
  argument-hint: [team-name]
4
4
  ---
5
5
 
6
- Create a named Intercom team that does not require tmux.
6
+ Create an additive task team without tmux. This explicit command is user approval; do not ask for approval again. Existing memberships and registration scope stay unchanged.
7
7
 
8
8
  If `$ARGUMENTS` is a valid team name, call `intercom_join` with `create: true` and that name. If `$ARGUMENTS` is empty, ask the user for a name first. Do not invent a team name.
9
9
 
10
- Example: `intercom_join({ name: "billing", create: true })`
10
+ If the user also names peers or describes a task, discover those connected sessions and pass `members` and `work` in the same call. Do not require the user to join each terminal manually.
11
+
12
+ Example: `intercom_join({ name: "billing", create: true, members: ["front", "writer"], work: "Billing UI" })`
@@ -3,7 +3,7 @@ description: Join a named intercom team, or list joinable teams
3
3
  argument-hint: [team-name]
4
4
  ---
5
5
 
6
- Join a named Intercom team without tmux.
6
+ Join an additional named task team without tmux. This explicit command is user approval; do not ask for approval again. Previous memberships and registration scope stay unchanged.
7
7
 
8
8
  If `$ARGUMENTS` is empty, call `intercom_join` with no arguments to list joinable named teams. If `$ARGUMENTS` is a team name, call `intercom_join` with that name. Do not guess a team name.
9
9
 
package/dist/broker.mjs CHANGED
@@ -1027,14 +1027,15 @@ function isMessage(value) {
1027
1027
  }
1028
1028
  if (message.replyTo !== void 0 || message.expectsReply !== void 0) return false;
1029
1029
  }
1030
- if (!hasExactDataKeys(message.content, ["text"], ["attachments"])) {
1030
+ if (!hasExactDataKeys(message.content, ["text"], ["attachments", "team"])) {
1031
1031
  return false;
1032
1032
  }
1033
1033
  const content = message.content;
1034
1034
  if (typeof content.text !== "string" || Buffer.byteLength(content.text, "utf-8") > MAX_MESSAGE_TEXT_BYTES) {
1035
1035
  return false;
1036
1036
  }
1037
- if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0)) {
1037
+ if (content.team !== void 0 && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
1038
+ if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0 || content.team !== void 0)) {
1038
1039
  return false;
1039
1040
  }
1040
1041
  return content.attachments === void 0 || isDenseArrayOf(content.attachments, isAttachment) && content.attachments.length <= MAX_ATTACHMENTS;
package/dist/cci.mjs CHANGED
@@ -1199,14 +1199,15 @@ function isMessage(value) {
1199
1199
  }
1200
1200
  if (message.replyTo !== void 0 || message.expectsReply !== void 0) return false;
1201
1201
  }
1202
- if (!hasExactDataKeys(message.content, ["text"], ["attachments"])) {
1202
+ if (!hasExactDataKeys(message.content, ["text"], ["attachments", "team"])) {
1203
1203
  return false;
1204
1204
  }
1205
1205
  const content = message.content;
1206
1206
  if (typeof content.text !== "string") {
1207
1207
  return false;
1208
1208
  }
1209
- if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0)) return false;
1209
+ if (content.team !== void 0 && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
1210
+ if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0 || content.team !== void 0)) return false;
1210
1211
  return content.attachments === void 0 || isDenseArrayOf(content.attachments, isAttachment);
1211
1212
  }
1212
1213
  function isSessionInfo(value) {
@@ -1829,6 +1830,7 @@ var IntercomClient = class extends EventEmitter {
1829
1830
  ...options.control === void 0 ? {} : { control: options.control },
1830
1831
  content: {
1831
1832
  text: options.text,
1833
+ ...options.team === void 0 ? {} : { team: options.team },
1832
1834
  ...options.attachments === void 0 ? {} : { attachments: options.attachments }
1833
1835
  }
1834
1836
  };
@@ -2497,6 +2499,7 @@ ${agent.instructions}` : "";
2497
2499
  return [
2498
2500
  `Intercom message for ${agent.name}.`,
2499
2501
  `From: ${formatSessionDisplay(from)} (${from.id})`,
2502
+ ...message.content.team ? [`[Team: ${message.content.team}]`] : [],
2500
2503
  `Message id: ${message.id}`,
2501
2504
  "",
2502
2505
  message.content.text,
@@ -2649,7 +2652,8 @@ var VirtualClaudeAgent = class {
2649
2652
  if (message.expectsReply) {
2650
2653
  void this.client.send(from.id, {
2651
2654
  text: "This worker is rate limited (too many wakes this minute). Please try again shortly.",
2652
- replyTo: message.id
2655
+ replyTo: message.id,
2656
+ ...message.content.team === void 0 ? {} : { team: message.content.team }
2653
2657
  }).catch((error) => {
2654
2658
  process.stderr.write(`worker ${this.agent.id}: rate-limit reply failed: ${error instanceof Error ? error.message : String(error)}
2655
2659
  `);
@@ -2711,7 +2715,8 @@ var VirtualClaudeAgent = class {
2711
2715
  if (message.expectsReply) {
2712
2716
  await this.client.send(from.id, {
2713
2717
  text: result.result || "(the worker finished without a final message)",
2714
- replyTo: message.id
2718
+ replyTo: message.id,
2719
+ ...message.content.team === void 0 ? {} : { team: message.content.team }
2715
2720
  });
2716
2721
  process.stderr.write(`worker ${this.agent.id}: replied to ${from.name || from.id} (session ${this.sessionId ?? "none"})
2717
2722
  `);
@@ -2723,7 +2728,8 @@ var VirtualClaudeAgent = class {
2723
2728
  if (message.expectsReply) {
2724
2729
  await this.client.send(from.id, {
2725
2730
  text: `Worker error: ${text}`,
2726
- replyTo: message.id
2731
+ replyTo: message.id,
2732
+ ...message.content.team === void 0 ? {} : { team: message.content.team }
2727
2733
  }).catch((sendError) => {
2728
2734
  process.stderr.write(`worker ${this.agent.id}: error reply failed: ${sendError instanceof Error ? sendError.message : String(sendError)}
2729
2735
  `);
@@ -3088,6 +3094,8 @@ function nativePrompt(from, message) {
3088
3094
  const reply = message.expectsReply ? "\n\nThe sender is blocking. After acting, use Claude's built-in SendMessage tool to reply to this same native peer. A normal assistant response stays local and will not unblock the sender." : "\n\nAct if needed. To acknowledge or report a result, use Claude's built-in SendMessage tool to reply to this same native peer; a normal assistant response stays local.";
3089
3095
  return [
3090
3096
  `[Intercom message from ${formatSessionDisplay(from)} (${from.id})]`,
3097
+ ...message.content.team ? [`[Team: ${message.content.team}]`] : [],
3098
+ `When replying via SendMessage, begin the body with [intercom-context:ctx-${message.id}] so this exact task is retained.`,
3091
3099
  message.content.text,
3092
3100
  formatAttachments(message.content.attachments),
3093
3101
  reply
@@ -3179,7 +3187,7 @@ var NativeClaudeBrokerBridge = class {
3179
3187
  const reason = error instanceof Error ? error.message : String(error);
3180
3188
  this.client.updatePresence({ status: `native delivery error: ${reason}` });
3181
3189
  if (message.expectsReply) {
3182
- await this.client.send(from.id, { text: `Native Claude delivery failed: ${reason}`, replyTo: message.id }).catch(() => void 0);
3190
+ await this.client.send(from.id, { text: `Native Claude delivery failed: ${reason}`, replyTo: message.id, ...message.content.team === void 0 ? {} : { team: message.content.team } }).catch(() => void 0);
3183
3191
  }
3184
3192
  }
3185
3193
  }
@@ -3189,7 +3197,13 @@ var NativeClaudeBrokerBridge = class {
3189
3197
  if (typeof message !== "object" || message === null || !("content" in message)) return;
3190
3198
  const envelope = stripNativeEnvelope(message.content);
3191
3199
  if (!envelope.body) return;
3192
- const relay = this.pending.shift();
3200
+ const selector = /^\[intercom-context:ctx-([^\]\r\n]+)\]\s*/.exec(envelope.body);
3201
+ const index = selector ? this.pending.findIndex((entry) => entry.message.id === selector[1]) : this.pending.length === 1 ? 0 : -1;
3202
+ if (index < 0) {
3203
+ this.client.updatePresence({ status: "Reply requires the original intercom-context selector" });
3204
+ return;
3205
+ }
3206
+ const relay = this.pending.splice(index, 1)[0];
3193
3207
  const fromAddress = typeof frame.from === "string" ? frame.from : envelope.from;
3194
3208
  if (fromAddress?.startsWith("uds:")) {
3195
3209
  void this.sendFrame(fromAddress.slice(4), buildNativeReceipt({
@@ -3201,7 +3215,8 @@ var NativeClaudeBrokerBridge = class {
3201
3215
  }
3202
3216
  if (!relay) return;
3203
3217
  void this.client.send(relay.from.id, {
3204
- text: envelope.body,
3218
+ text: selector ? envelope.body.slice(selector[0].length) : envelope.body,
3219
+ ...relay.message.content.team === void 0 ? {} : { team: relay.message.content.team },
3205
3220
  ...relay.message.expectsReply ? { replyTo: relay.message.id } : {}
3206
3221
  }).then(() => {
3207
3222
  this.client.updatePresence({ status: "idle" });
package/dist/ccim.mjs CHANGED
@@ -1203,14 +1203,15 @@ function isMessage(value) {
1203
1203
  }
1204
1204
  if (message.replyTo !== void 0 || message.expectsReply !== void 0) return false;
1205
1205
  }
1206
- if (!hasExactDataKeys(message.content, ["text"], ["attachments"])) {
1206
+ if (!hasExactDataKeys(message.content, ["text"], ["attachments", "team"])) {
1207
1207
  return false;
1208
1208
  }
1209
1209
  const content = message.content;
1210
1210
  if (typeof content.text !== "string") {
1211
1211
  return false;
1212
1212
  }
1213
- if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0)) return false;
1213
+ if (content.team !== void 0 && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
1214
+ if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0 || content.team !== void 0)) return false;
1214
1215
  return content.attachments === void 0 || isDenseArrayOf(content.attachments, isAttachment);
1215
1216
  }
1216
1217
  function isSessionInfo(value) {
@@ -1833,6 +1834,7 @@ var IntercomClient = class extends EventEmitter {
1833
1834
  ...options.control === void 0 ? {} : { control: options.control },
1834
1835
  content: {
1835
1836
  text: options.text,
1837
+ ...options.team === void 0 ? {} : { team: options.team },
1836
1838
  ...options.attachments === void 0 ? {} : { attachments: options.attachments }
1837
1839
  }
1838
1840
  };
@@ -2501,6 +2503,7 @@ ${agent.instructions}` : "";
2501
2503
  return [
2502
2504
  `Intercom message for ${agent.name}.`,
2503
2505
  `From: ${formatSessionDisplay(from)} (${from.id})`,
2506
+ ...message.content.team ? [`[Team: ${message.content.team}]`] : [],
2504
2507
  `Message id: ${message.id}`,
2505
2508
  "",
2506
2509
  message.content.text,
@@ -2653,7 +2656,8 @@ var VirtualClaudeAgent = class {
2653
2656
  if (message.expectsReply) {
2654
2657
  void this.client.send(from.id, {
2655
2658
  text: "This worker is rate limited (too many wakes this minute). Please try again shortly.",
2656
- replyTo: message.id
2659
+ replyTo: message.id,
2660
+ ...message.content.team === void 0 ? {} : { team: message.content.team }
2657
2661
  }).catch((error) => {
2658
2662
  process.stderr.write(`worker ${this.agent.id}: rate-limit reply failed: ${error instanceof Error ? error.message : String(error)}
2659
2663
  `);
@@ -2715,7 +2719,8 @@ var VirtualClaudeAgent = class {
2715
2719
  if (message.expectsReply) {
2716
2720
  await this.client.send(from.id, {
2717
2721
  text: result.result || "(the worker finished without a final message)",
2718
- replyTo: message.id
2722
+ replyTo: message.id,
2723
+ ...message.content.team === void 0 ? {} : { team: message.content.team }
2719
2724
  });
2720
2725
  process.stderr.write(`worker ${this.agent.id}: replied to ${from.name || from.id} (session ${this.sessionId ?? "none"})
2721
2726
  `);
@@ -2727,7 +2732,8 @@ var VirtualClaudeAgent = class {
2727
2732
  if (message.expectsReply) {
2728
2733
  await this.client.send(from.id, {
2729
2734
  text: `Worker error: ${text}`,
2730
- replyTo: message.id
2735
+ replyTo: message.id,
2736
+ ...message.content.team === void 0 ? {} : { team: message.content.team }
2731
2737
  }).catch((sendError) => {
2732
2738
  process.stderr.write(`worker ${this.agent.id}: error reply failed: ${sendError instanceof Error ? sendError.message : String(sendError)}
2733
2739
  `);
@@ -3092,6 +3098,8 @@ function nativePrompt(from, message) {
3092
3098
  const reply = message.expectsReply ? "\n\nThe sender is blocking. After acting, use Claude's built-in SendMessage tool to reply to this same native peer. A normal assistant response stays local and will not unblock the sender." : "\n\nAct if needed. To acknowledge or report a result, use Claude's built-in SendMessage tool to reply to this same native peer; a normal assistant response stays local.";
3093
3099
  return [
3094
3100
  `[Intercom message from ${formatSessionDisplay(from)} (${from.id})]`,
3101
+ ...message.content.team ? [`[Team: ${message.content.team}]`] : [],
3102
+ `When replying via SendMessage, begin the body with [intercom-context:ctx-${message.id}] so this exact task is retained.`,
3095
3103
  message.content.text,
3096
3104
  formatAttachments(message.content.attachments),
3097
3105
  reply
@@ -3183,7 +3191,7 @@ var NativeClaudeBrokerBridge = class {
3183
3191
  const reason = error instanceof Error ? error.message : String(error);
3184
3192
  this.client.updatePresence({ status: `native delivery error: ${reason}` });
3185
3193
  if (message.expectsReply) {
3186
- await this.client.send(from.id, { text: `Native Claude delivery failed: ${reason}`, replyTo: message.id }).catch(() => void 0);
3194
+ await this.client.send(from.id, { text: `Native Claude delivery failed: ${reason}`, replyTo: message.id, ...message.content.team === void 0 ? {} : { team: message.content.team } }).catch(() => void 0);
3187
3195
  }
3188
3196
  }
3189
3197
  }
@@ -3193,7 +3201,13 @@ var NativeClaudeBrokerBridge = class {
3193
3201
  if (typeof message !== "object" || message === null || !("content" in message)) return;
3194
3202
  const envelope = stripNativeEnvelope(message.content);
3195
3203
  if (!envelope.body) return;
3196
- const relay = this.pending.shift();
3204
+ const selector = /^\[intercom-context:ctx-([^\]\r\n]+)\]\s*/.exec(envelope.body);
3205
+ const index = selector ? this.pending.findIndex((entry) => entry.message.id === selector[1]) : this.pending.length === 1 ? 0 : -1;
3206
+ if (index < 0) {
3207
+ this.client.updatePresence({ status: "Reply requires the original intercom-context selector" });
3208
+ return;
3209
+ }
3210
+ const relay = this.pending.splice(index, 1)[0];
3197
3211
  const fromAddress = typeof frame.from === "string" ? frame.from : envelope.from;
3198
3212
  if (fromAddress?.startsWith("uds:")) {
3199
3213
  void this.sendFrame(fromAddress.slice(4), buildNativeReceipt({
@@ -3205,7 +3219,8 @@ var NativeClaudeBrokerBridge = class {
3205
3219
  }
3206
3220
  if (!relay) return;
3207
3221
  void this.client.send(relay.from.id, {
3208
- text: envelope.body,
3222
+ text: selector ? envelope.body.slice(selector[0].length) : envelope.body,
3223
+ ...relay.message.content.team === void 0 ? {} : { team: relay.message.content.team },
3209
3224
  ...relay.message.expectsReply ? { replyTo: relay.message.id } : {}
3210
3225
  }).then(() => {
3211
3226
  this.client.updatePresence({ status: "idle" });