@ctliz/agent-intercom-claude 0.14.1 → 0.15.1

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.
@@ -635,14 +635,15 @@ function isMessage(value) {
635
635
  }
636
636
  if (message.replyTo !== void 0 || message.expectsReply !== void 0) return false;
637
637
  }
638
- if (!hasExactDataKeys(message.content, ["text"], ["attachments"])) {
638
+ if (!hasExactDataKeys(message.content, ["text"], ["attachments", "team"])) {
639
639
  return false;
640
640
  }
641
641
  const content = message.content;
642
642
  if (typeof content.text !== "string") {
643
643
  return false;
644
644
  }
645
- if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0)) return false;
645
+ if (content.team !== void 0 && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
646
+ if (message.control !== void 0 && (content.text !== "" || content.attachments !== void 0 || content.team !== void 0)) return false;
646
647
  return content.attachments === void 0 || isDenseArrayOf(content.attachments, isAttachment);
647
648
  }
648
649
  function isSessionInfo(value) {
@@ -1265,6 +1266,7 @@ var IntercomClient = class extends EventEmitter {
1265
1266
  ...options.control === void 0 ? {} : { control: options.control },
1266
1267
  content: {
1267
1268
  text: options.text,
1269
+ ...options.team === void 0 ? {} : { team: options.team },
1268
1270
  ...options.attachments === void 0 ? {} : { attachments: options.attachments }
1269
1271
  }
1270
1272
  };
@@ -1859,6 +1861,7 @@ function toInboxEntry(from, message, now = Date.now()) {
1859
1861
  fromId: from.id,
1860
1862
  ...from.name ? { fromName: from.name } : {},
1861
1863
  messageId: message.id,
1864
+ ...message.content.team ? { team: message.content.team } : {},
1862
1865
  ...message.replyTo ? { replyTo: message.replyTo } : {},
1863
1866
  expectsReply: Boolean(message.expectsReply),
1864
1867
  text: message.content.text
@@ -2188,19 +2191,17 @@ function parseStoredTeam(value) {
2188
2191
  if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt <= 0) {
2189
2192
  return void 0;
2190
2193
  }
2194
+ if (value.memberSessionIds !== void 0 && (!Array.isArray(value.memberSessionIds) || !value.memberSessionIds.every((id) => typeof id === "string" && id.trim() === id && id.length > 0 && !/[\u0000-\u001f\u007f]/.test(id)) || new Set(value.memberSessionIds).size !== value.memberSessionIds.length || !value.memberSessionIds.includes(value.managerSessionId))) return void 0;
2195
+ if (value.work !== void 0 && (typeof value.work !== "string" || !value.work.trim() || value.work.length > 2e3)) return void 0;
2191
2196
  return {
2192
2197
  name: value.name,
2193
2198
  scopeId: value.scopeId,
2194
2199
  managerSessionId: value.managerSessionId,
2195
- createdAt: value.createdAt
2200
+ createdAt: value.createdAt,
2201
+ ...value.memberSessionIds === void 0 ? {} : { memberSessionIds: value.memberSessionIds },
2202
+ ...value.work === void 0 ? {} : { work: value.work }
2196
2203
  };
2197
2204
  }
2198
- function rejectManagedJoin(env = process.env) {
2199
- if (env.AGENT_INTERCOM_WORKER_ID?.trim() || env.AGENT_INTERCOM_OWNED === "1" || env.AGENT_INTERCOM_TEAM_MANIFEST?.trim()) {
2200
- return "This session is already a managed member and cannot join another team.";
2201
- }
2202
- return void 0;
2203
- }
2204
2205
  function parseTeamName(raw) {
2205
2206
  const name = raw.trim();
2206
2207
  if (!name || name.includes(" ") || !NAMED_TEAM_NAME_PATTERN.test(name)) {
@@ -2245,35 +2246,6 @@ function listNamedTeams(agentDir) {
2245
2246
  }
2246
2247
  return teams;
2247
2248
  }
2248
- function findNamedTeam(name, agentDir) {
2249
- return listNamedTeams(agentDir).find((team) => team.name === name);
2250
- }
2251
- function createNamedTeam(input) {
2252
- const name = parseTeamName(input.name);
2253
- const managerSessionId = input.managerSessionId.trim();
2254
- if (!managerSessionId || managerSessionId !== input.managerSessionId || /[\u0000-\u001f\u007f]/.test(managerSessionId)) {
2255
- throw genericWriteError();
2256
- }
2257
- const existing = listNamedTeams(input.agentDir);
2258
- if (existing.some((team2) => team2.name === name)) {
2259
- throw new Error(`A named team called ${name} already exists.`);
2260
- }
2261
- const scopeId = (input.generateScope ?? (() => generateNamedTeamScope(existing.map((team2) => team2.scopeId))))();
2262
- if (!isNamedTeamScope(scopeId) || existing.some((team2) => team2.scopeId === scopeId)) {
2263
- throw genericWriteError();
2264
- }
2265
- const team = {
2266
- name,
2267
- scopeId,
2268
- managerSessionId,
2269
- createdAt: input.now ?? Date.now()
2270
- };
2271
- const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
2272
- ensureIntercomRuntimeDir(dir);
2273
- const payload = { version: NAMED_TEAMS_VERSION, teams: [...existing, team] };
2274
- writeDurableJson(teamsFilePath(input.agentDir), payload);
2275
- return team;
2276
- }
2277
2249
  function formatJoinableNamedTeamList(teams) {
2278
2250
  if (teams.length === 0) {
2279
2251
  return 'No joinable named teams found.\nCreate one with intercom_join({ name: "billing", create: true }).';
@@ -2293,6 +2265,147 @@ Role: teammate
2293
2265
  Display name: ${input.name}`;
2294
2266
  }
2295
2267
 
2268
+ // claude/named-team-membership.ts
2269
+ import { mkdirSync as mkdirSync4, rmSync } from "node:fs";
2270
+ import { join as join8 } from "node:path";
2271
+ import { setTimeout as delay } from "node:timers/promises";
2272
+ function namedTeamMemberIds(team) {
2273
+ return team.memberSessionIds ?? [team.managerSessionId];
2274
+ }
2275
+ function sessionNamedTeams(sessionId, agentDir) {
2276
+ return listNamedTeams(agentDir).filter((team) => namedTeamMemberIds(team).includes(sessionId));
2277
+ }
2278
+ async function appendNamedTeamMembership(input) {
2279
+ const name = parseTeamName(input.name);
2280
+ const memberIds = [.../* @__PURE__ */ new Set([input.selfId, ...input.members ?? []])];
2281
+ if (memberIds.some((id) => !id || id.trim() !== id || /[\u0000-\u001f\u007f]/.test(id))) {
2282
+ throw new Error("Invalid team member session ID");
2283
+ }
2284
+ if (input.work !== void 0 && (!input.work.trim() || input.work.length > 2e3)) {
2285
+ throw new Error("Work must be a non-empty task description (max 2000 characters)");
2286
+ }
2287
+ const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
2288
+ ensureIntercomRuntimeDir(dir);
2289
+ const lock = join8(dir, "named-teams.lock");
2290
+ const deadline = Date.now() + 5e3;
2291
+ for (; ; ) {
2292
+ try {
2293
+ mkdirSync4(lock, { mode: 448 });
2294
+ break;
2295
+ } catch (error2) {
2296
+ if (error2.code !== "EEXIST") throw error2;
2297
+ if (Date.now() >= deadline) throw new Error("Team registry is busy; retry. If a writer crashed, remove the stale named-teams.lock directory after verifying no writer is active.");
2298
+ await delay(25);
2299
+ }
2300
+ }
2301
+ try {
2302
+ const teams = listNamedTeams(input.agentDir);
2303
+ let team = teams.find((entry) => entry.name === name);
2304
+ if (input.create) {
2305
+ if (team) throw new Error(`A named team called ${name} already exists; join it instead.`);
2306
+ team = { name, scopeId: generateNamedTeamScope(teams.map((entry) => entry.scopeId)), managerSessionId: input.selfId, createdAt: Date.now() };
2307
+ teams.push(team);
2308
+ }
2309
+ if (!team) throw new Error(`Team "${name}" does not exist; create it explicitly.`);
2310
+ if (memberIds.some((id) => id !== input.selfId) && team.managerSessionId !== input.selfId) {
2311
+ throw new Error(`Only the manager of team "${name}" may add other sessions`);
2312
+ }
2313
+ if (input.work !== void 0 && !input.create) {
2314
+ throw new Error("Work is set when creating a team; use a new team for a different task");
2315
+ }
2316
+ const updated = {
2317
+ ...team,
2318
+ memberSessionIds: [.../* @__PURE__ */ new Set([...namedTeamMemberIds(team), ...memberIds])],
2319
+ ...input.work === void 0 ? {} : { work: input.work.trim() }
2320
+ };
2321
+ writeDurableJson(join8(dir, NAMED_TEAMS_FILE), {
2322
+ version: NAMED_TEAMS_VERSION,
2323
+ teams: teams.map((entry) => entry.name === name ? updated : entry)
2324
+ });
2325
+ return updated;
2326
+ } finally {
2327
+ rmSync(lock, { recursive: true });
2328
+ }
2329
+ }
2330
+ function requireNamedTeamMembers(name, selfId, peerId, agentDir) {
2331
+ const team = listNamedTeams(agentDir).find((entry) => entry.name === name);
2332
+ if (!team) throw new Error(`Unknown team "${name}"`);
2333
+ const members = namedTeamMemberIds(team);
2334
+ if (!members.includes(selfId) || !members.includes(peerId)) {
2335
+ throw new Error(`Both sessions must belong to team "${name}" before messaging`);
2336
+ }
2337
+ return team;
2338
+ }
2339
+ function resolveNamedMessageTeam(selfId, peerId, requested, agentDir) {
2340
+ if (requested !== void 0) return requireNamedTeamMembers(requested, selfId, peerId, agentDir).name;
2341
+ const mine = sessionNamedTeams(selfId, agentDir);
2342
+ const shared = mine.filter((team) => namedTeamMemberIds(team).includes(peerId));
2343
+ if (shared.length === 1) return shared[0].name;
2344
+ if (shared.length > 1) throw new Error("Multiple shared teams; specify `team` for this task");
2345
+ return void 0;
2346
+ }
2347
+ function namedTeamRoster(team, selfId, sessions) {
2348
+ return {
2349
+ name: team.name,
2350
+ ...team.work ? { work: team.work } : {},
2351
+ self: { id: selfId, isManager: selfId === team.managerSessionId },
2352
+ manager: { target: team.managerSessionId, connected: sessions.some((entry) => entry.id === team.managerSessionId) },
2353
+ members: namedTeamMemberIds(team).map((id) => ({
2354
+ id,
2355
+ target: id,
2356
+ name: sessions.find((entry) => entry.id === id)?.name,
2357
+ role: id === team.managerSessionId ? "manager" : "member",
2358
+ connected: sessions.some((entry) => entry.id === id)
2359
+ }))
2360
+ };
2361
+ }
2362
+ function formatNamedTeamRoster(team) {
2363
+ return [
2364
+ `Team: ${team.name}`,
2365
+ ...team.work ? [`Work: ${team.work}`] : [],
2366
+ `You: ${team.self.id}${team.self.isManager ? " [manager]" : ""}`,
2367
+ `Manager: ${team.manager.target}${team.manager.connected ? "" : " [offline]"}`,
2368
+ "Members:",
2369
+ ...team.members.map((member) => `- ${member.name || member.id} (${member.id}) [${member.role}]${member.connected ? "" : " [offline]"}`)
2370
+ ].join("\n");
2371
+ }
2372
+
2373
+ // claude/reply-context.ts
2374
+ function contextId(entry) {
2375
+ return `ctx-${entry.message.id}`;
2376
+ }
2377
+ function askId(entry) {
2378
+ return `ask-${entry.message.id}`;
2379
+ }
2380
+ function replyHint(entry) {
2381
+ const team = entry.message.content.team;
2382
+ return `${team ? ` [Team: ${team}]` : ""} [contextId: ${contextId(entry)}]${entry.message.expectsReply ? ` [askId: ${askId(entry)}]` : ""}`;
2383
+ }
2384
+ function selectReplyContext(entries, asks, input) {
2385
+ if (input.askId && input.contextId) throw new Error("Specify askId or contextId, not both");
2386
+ let target;
2387
+ if (input.askId || input.contextId) {
2388
+ const selected = input.askId ? asks.find((entry) => askId(entry) === input.askId) : entries.find((entry) => contextId(entry) === input.contextId);
2389
+ if (!selected || selected.message.expectsReply && !asks.includes(selected)) throw new Error("Unknown or resolved reply selector; call intercom_pending");
2390
+ if (input.to && selected.from.id !== input.to && selected.from.name?.toLowerCase() !== input.to.toLowerCase()) {
2391
+ throw new Error("Reply sender does not match the selected context");
2392
+ }
2393
+ if (input.which) throw new Error("Do not combine an exact reply selector with which");
2394
+ target = selected;
2395
+ } else {
2396
+ const to = input.to;
2397
+ const candidates = to ? asks.filter((entry) => entry.from.id === to || entry.from.name?.toLowerCase() === to.toLowerCase() || entry.from.id.startsWith(to)) : asks;
2398
+ if (!input.which && new Set(candidates.map((entry) => entry.message.content.team)).size > 1) {
2399
+ throw new Error("Multiple team contexts; specify askId or contextId");
2400
+ }
2401
+ target = selectPendingAsk(asks, input.to, input.which);
2402
+ }
2403
+ if (input.team !== void 0 && input.team !== target.message.content.team) {
2404
+ throw new Error("Reply team must match the original message; omit team to inherit it");
2405
+ }
2406
+ return target;
2407
+ }
2408
+
2296
2409
  // claude/runtime.ts
2297
2410
  function matchesPendingSender(entry, to) {
2298
2411
  return entry.from.id === to || entry.from.name?.toLowerCase() === to.toLowerCase() || entry.from.id.startsWith(to);
@@ -2330,6 +2443,9 @@ function publicPendingEntry(entry, selector) {
2330
2443
  ...entry.from.parentSessionId ? { parent_session_id: entry.from.parentSessionId } : {},
2331
2444
  ...entry.from.generation ? { generation: entry.from.generation } : {}
2332
2445
  },
2446
+ contextId: contextId(entry),
2447
+ ...entry.message.expectsReply ? { askId: askId(entry) } : {},
2448
+ ...entry.message.content.team ? { team: entry.message.content.team } : {},
2333
2449
  received_at: entry.receivedAt,
2334
2450
  read: entry.read,
2335
2451
  text: entry.message.content.text,
@@ -2545,7 +2661,7 @@ var ClaudeIntercomRuntime = class {
2545
2661
  }
2546
2662
  scheduleReconnect() {
2547
2663
  if (!this.reconnectEnabled || this.reconnectTimer) return;
2548
- const delay2 = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
2664
+ const delay3 = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
2549
2665
  this.reconnectTimer = setTimeout(() => {
2550
2666
  this.reconnectTimer = null;
2551
2667
  void this.connect().then((client) => {
@@ -2557,7 +2673,7 @@ var ClaudeIntercomRuntime = class {
2557
2673
  this.reconnectAttempt += 1;
2558
2674
  this.scheduleReconnect();
2559
2675
  });
2560
- }, delay2);
2676
+ }, delay3);
2561
2677
  this.reconnectTimer.unref?.();
2562
2678
  }
2563
2679
  clearReconnectTimer() {
@@ -2609,25 +2725,24 @@ var ClaudeIntercomRuntime = class {
2609
2725
  this.reconnectEnabled = true;
2610
2726
  await this.connect();
2611
2727
  }
2612
- async join(name, create = false) {
2613
- const blocked = rejectManagedJoin();
2614
- if (blocked) return textResult(blocked, { ok: false }, true);
2728
+ async join(name, create = false, members, work) {
2615
2729
  try {
2616
- if (create) {
2617
- if (typeof name !== "string" || !name.trim()) {
2618
- return textResult("Creating a team requires a name.", { ok: false }, true);
2619
- }
2620
- const team2 = createNamedTeam({ name: parseTeamName(name), managerSessionId: this.identity.sessionId });
2621
- await this.switchRuntimeScope(team2.scopeId, team2.managerSessionId);
2622
- return textResult(formatCreateSuccess({ team: team2.name, name: this.identity.name }), { ok: true, team: team2.name, role: "manager" });
2623
- }
2624
2730
  if (!name?.trim()) {
2731
+ if (create || members?.length || work !== void 0) throw new Error("Creating or extending a team requires a name");
2625
2732
  return textResult(formatJoinableNamedTeamList(listNamedTeams()));
2626
2733
  }
2627
- const team = findNamedTeam(parseTeamName(name));
2628
- if (!team) return textResult("Could not join that team.", { ok: false }, true);
2629
- await this.switchRuntimeScope(team.scopeId, team.managerSessionId);
2630
- return textResult(formatNamedJoinSuccess({ team: team.name, name: this.identity.name }), { ok: true, team: team.name, role: "teammate" });
2734
+ const client = await this.connect();
2735
+ const sessions = await client.listSessions();
2736
+ const memberIds = (members ?? []).map((member) => {
2737
+ const id = resolveSessionTarget(sessions, member);
2738
+ if (!id) throw new Error(`Session "${member}" is not connected`);
2739
+ return id;
2740
+ });
2741
+ const team = await appendNamedTeamMembership({ name: parseTeamName(name), selfId: this.identity.sessionId, members: memberIds, create, work });
2742
+ const roster = namedTeamRoster(team, this.identity.sessionId, sessions);
2743
+ const notice = create ? formatCreateSuccess({ team: team.name, name: this.identity.name }) : formatNamedJoinSuccess({ team: team.name, name: this.identity.name });
2744
+ return textResult(`${notice}
2745
+ Other team memberships are unchanged.`, { ok: true, team: team.name, role: roster.self.isManager ? "manager" : "member", roster });
2631
2746
  } catch (error2) {
2632
2747
  return textResult(error2 instanceof Error ? error2.message : String(error2), { ok: false }, true);
2633
2748
  }
@@ -2637,7 +2752,7 @@ var ClaudeIntercomRuntime = class {
2637
2752
  if (waiter) {
2638
2753
  const senderTarget = from.name || from.id;
2639
2754
  const fromMatches = senderTarget.toLowerCase() === waiter.from.toLowerCase() || from.id === waiter.from;
2640
- if (fromMatches) {
2755
+ if (fromMatches && message.content.team === waiter.team) {
2641
2756
  this.replyWaiters.delete(waiter.replyTo);
2642
2757
  clearTimeout(waiter.timeout);
2643
2758
  waiter.cleanup?.();
@@ -2655,7 +2770,7 @@ var ClaudeIntercomRuntime = class {
2655
2770
  } catch {
2656
2771
  }
2657
2772
  }
2658
- waitForReply(from, replyTo, timeoutMs = getAskTimeoutMs(), signal) {
2773
+ waitForReply(from, replyTo, timeoutMs = getAskTimeoutMs(), signal, team) {
2659
2774
  return new Promise((resolve3, reject) => {
2660
2775
  if (signal?.aborted) {
2661
2776
  reject(new Error("intercom_ask cancelled"));
@@ -2679,7 +2794,7 @@ var ClaudeIntercomRuntime = class {
2679
2794
  reject(new Error(`No reply from "${from}" within ${Math.round(timeoutMs / 1e3)} seconds`));
2680
2795
  }, timeoutMs);
2681
2796
  signal?.addEventListener("abort", onAbort, { once: true });
2682
- this.replyWaiters.set(replyTo, { from, replyTo, resolve: resolve3, reject, timeout, cleanup });
2797
+ this.replyWaiters.set(replyTo, { from, replyTo, team, resolve: resolve3, reject, timeout, cleanup });
2683
2798
  });
2684
2799
  }
2685
2800
  async resolveTarget(to) {
@@ -2697,9 +2812,16 @@ cwd: ${this.identity.cwd}`,
2697
2812
  { session_id: sessionId, name: this.identity.name, cwd: this.identity.cwd, model: this.identity.model }
2698
2813
  );
2699
2814
  }
2700
- async team() {
2815
+ async team(name) {
2701
2816
  const client = await this.connect();
2702
2817
  const sessions = await client.listSessions();
2818
+ const mine = sessionNamedTeams(this.identity.sessionId);
2819
+ const selected = name ? mine.filter((entry) => entry.name === name) : mine;
2820
+ if (name && !selected.length) return textResult(`You do not belong to team "${name}"`, { ok: false }, true);
2821
+ if (selected.length) {
2822
+ const teams = selected.map((entry) => namedTeamRoster(entry, this.identity.sessionId, sessions));
2823
+ return textResult(teams.map(formatNamedTeamRoster).join("\n\n"), { teams });
2824
+ }
2703
2825
  const team = await resolveIntercomTeam({ selfId: client.sessionId ?? this.identity.sessionId, sessions });
2704
2826
  return textResult(formatIntercomTeam(team), team);
2705
2827
  }
@@ -2751,28 +2873,34 @@ Pending asks: ${this.unresolvedAsks.size}`,
2751
2873
  client.updatePresence({ status: summary.trim() || "idle" });
2752
2874
  return textResult("Summary updated.", { ok: true, summary });
2753
2875
  }
2754
- async send(to, message, attachments, replyTo) {
2876
+ async send(to, message, attachments, replyTo, requestedTeam) {
2755
2877
  const client = await this.connect();
2756
2878
  const sendTo = await this.resolveTarget(to);
2757
- const result = await client.send(sendTo, { text: message, attachments, replyTo });
2879
+ const source = replyTo ? this.unread.find((entry) => entry.message.id === replyTo && entry.from.id === sendTo) : void 0;
2880
+ if (replyTo && !source) throw new Error("Unknown inbound reply context");
2881
+ if (source && requestedTeam !== void 0 && requestedTeam !== source.message.content.team) throw new Error("Reply team must match the original message");
2882
+ const team = source ? source.message.content.team : resolveNamedMessageTeam(this.identity.sessionId, sendTo, requestedTeam);
2883
+ const result = await client.send(sendTo, { text: message, attachments, replyTo: source?.message.expectsReply ? replyTo : void 0, team });
2758
2884
  if (!result.delivered) {
2759
2885
  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);
2760
2886
  }
2761
2887
  if (replyTo) this.unresolvedAsks.delete(replyTo);
2762
2888
  return textResult(`Message sent to ${to}.`, { ok: true, message_id: result.id, to });
2763
2889
  }
2764
- async ask(to, message, attachments, timeoutMs = getAskTimeoutMs(), signal) {
2890
+ async ask(to, message, attachments, timeoutMs = getAskTimeoutMs(), signal, requestedTeam) {
2765
2891
  const client = await this.connect();
2766
2892
  const sendTo = await this.resolveTarget(to);
2893
+ const team = resolveNamedMessageTeam(this.identity.sessionId, sendTo, requestedTeam);
2767
2894
  const questionId = randomUUID4();
2768
- const replyPromise = this.waitForReply(sendTo, questionId, timeoutMs, signal);
2895
+ const replyPromise = this.waitForReply(sendTo, questionId, timeoutMs, signal, team);
2769
2896
  void replyPromise.catch(() => void 0);
2770
2897
  try {
2771
2898
  const result = await client.send(sendTo, {
2772
2899
  messageId: questionId,
2773
2900
  text: message,
2774
2901
  attachments,
2775
- expectsReply: true
2902
+ expectsReply: true,
2903
+ team
2776
2904
  });
2777
2905
  if (!result.delivered) {
2778
2906
  this.replyWaiters.get(questionId)?.reject(new Error(result.reason ?? "Session may not exist or has disconnected."));
@@ -2796,12 +2924,12 @@ ${replyText}`, { ok: true, message_id: result.id, reply });
2796
2924
  }
2797
2925
  const pendingAsks = Array.from(this.unresolvedAsks.values()).sort((a, b) => a.receivedAt - b.receivedAt);
2798
2926
  const lines = [
2799
- unreadMessages.length ? unreadMessages.map((entry) => `- ${formatSessionDisplay(entry.from)}: ${entry.message.content.text}${formatAttachments(entry.message.content.attachments)}`).join("\n") : "No unread messages.",
2927
+ unreadMessages.length ? unreadMessages.map((entry) => `- ${formatSessionDisplay(entry.from)}${replyHint(entry)}: ${entry.message.content.text}${formatAttachments(entry.message.content.attachments)}`).join("\n") : "No unread messages.",
2800
2928
  pendingAsks.length ? `
2801
2929
  Pending asks:
2802
2930
  ${pendingAsks.map((entry) => {
2803
2931
  const selector = pendingSelector(pendingAsks, entry);
2804
- return `- ${formatSessionDisplay(entry.from)}${selector ? ` [${selector}]` : ""}: ${entry.message.content.text}`;
2932
+ return `- ${formatSessionDisplay(entry.from)}${replyHint(entry)}${selector ? ` [${selector}]` : ""}: ${entry.message.content.text}`;
2805
2933
  }).join("\n")}` : ""
2806
2934
  ].filter(Boolean);
2807
2935
  return textResult(lines.join("\n"), {
@@ -2809,10 +2937,10 @@ ${pendingAsks.map((entry) => {
2809
2937
  pending_asks: pendingAsks.map((entry) => publicPendingEntry(entry, pendingSelector(pendingAsks, entry)))
2810
2938
  });
2811
2939
  }
2812
- async reply(message, to, which) {
2940
+ async reply(message, to, which, askId2, contextId2, team) {
2813
2941
  let target;
2814
2942
  try {
2815
- target = selectPendingAsk(Array.from(this.unresolvedAsks.values()), to, which);
2943
+ target = selectReplyContext(this.unread, Array.from(this.unresolvedAsks.values()), { to, which, askId: askId2, contextId: contextId2, team });
2816
2944
  } catch (error2) {
2817
2945
  return textResult(error2 instanceof Error ? error2.message : String(error2), { ok: false }, true);
2818
2946
  }
@@ -2824,6 +2952,21 @@ ${pendingAsks.map((entry) => {
2824
2952
  }
2825
2953
  };
2826
2954
 
2955
+ // claude/team-guidance.ts
2956
+ var TASK_TEAM_GUIDANCE = `Intercom shortest calling path:
2957
+ - For a known recipient, call intercom_send directly. Do not run intercom_whoami, intercom_team, intercom_list, or intercom_status as routine preflight checks.
2958
+ - Discover peers only when the target is unknown, missing, or ambiguous: intercom_team for managed or approved teammates, otherwise intercom_list. Use intercom_status only for connection errors; intercom_set_summary only for role discovery or meaningful status changes.
2959
+ - If the host requires tool discovery, discover only the selected tool and reuse its definition while it remains in context. Do not browse tool-definition directories or read unrelated definitions.
2960
+ - A standalone successful send needs no extra identity/status/pending checks or resending. Delivery is not task completion; diagnose uncertain delivery before retrying. Check intercom_pending at natural work boundaries when expecting a response or handling inbound messages, never in a tight loop.
2961
+
2962
+ Intercom task-team rules:
2963
+ - 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.
2964
+ - 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.
2965
+ - 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.
2966
+ - 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.
2967
+ - 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.
2968
+ - 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.`;
2969
+
2827
2970
  // claude/mcp-protocol.ts
2828
2971
  var inflightToolCalls = /* @__PURE__ */ new Map();
2829
2972
  function asString(value, name) {
@@ -2832,6 +2975,14 @@ function asString(value, name) {
2832
2975
  }
2833
2976
  return value;
2834
2977
  }
2978
+ function optionalString(value, name) {
2979
+ return value === void 0 ? void 0 : asString(value, name);
2980
+ }
2981
+ function memberArray(value) {
2982
+ if (value === void 0) return void 0;
2983
+ if (!Array.isArray(value)) throw new Error("members must be an array of session names or IDs");
2984
+ return Array.from(value, (member) => asString(member, "member"));
2985
+ }
2835
2986
  function asBoolean(value, defaultValue) {
2836
2987
  return typeof value === "boolean" ? value : defaultValue;
2837
2988
  }
@@ -2882,26 +3033,30 @@ function buildToolDefinitions(runtime) {
2882
3033
  },
2883
3034
  {
2884
3035
  name: "intercom_team",
2885
- description: "Show your current manager and the live coworkers owned by that manager. No arguments are required.",
2886
- inputSchema: { type: "object", properties: {}, additionalProperties: false },
3036
+ description: "Show all your named task teams, or inspect one by name. Falls back to managed-team discovery.",
3037
+ inputSchema: { type: "object", properties: { team: { type: "string" } }, additionalProperties: false },
2887
3038
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2888
- handler: async () => runtime.team()
3039
+ handler: async (args) => runtime.team(optionalString(args.team, "team"))
2889
3040
  },
2890
3041
  {
2891
3042
  name: "intercom_join",
2892
- description: "List, join, or create a named intercom team without tmux. Omit name to list joinable teams. Set create=true to create a team and join as manager.",
3043
+ description: "After user approval, create or join a task team. Membership is additive; members lets the manager add connected peers in one call. Omit name to list teams.",
2893
3044
  inputSchema: {
2894
3045
  type: "object",
2895
3046
  properties: {
2896
3047
  name: { type: "string", description: "Team name to join. Omit to list joinable teams." },
2897
- create: { type: "boolean", description: "Create this named team and join as manager. Requires name." }
3048
+ create: { type: "boolean", description: "Create this named team and join as manager. Requires name." },
3049
+ members: { type: "array", items: { type: "string" }, description: "Connected session names or IDs to add; manager only." },
3050
+ work: { type: "string", maxLength: 2e3, description: "Task description when creating a team." }
2898
3051
  },
2899
3052
  additionalProperties: false
2900
3053
  },
2901
3054
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2902
3055
  handler: async (args) => runtime.join(
2903
- typeof args.name === "string" ? args.name : void 0,
2904
- args.create === true
3056
+ optionalString(args.name, "name"),
3057
+ args.create === true,
3058
+ memberArray(args.members),
3059
+ optionalString(args.work, "work")
2905
3060
  )
2906
3061
  },
2907
3062
  {
@@ -2948,13 +3103,14 @@ function buildToolDefinitions(runtime) {
2948
3103
  properties: {
2949
3104
  to: { type: "string" },
2950
3105
  message: { type: "string" },
2951
- attachments: attachmentsSchema
3106
+ attachments: attachmentsSchema,
3107
+ team: { type: "string", description: "Task team; required when multiple teams are shared. Omit for ungrouped contact." }
2952
3108
  },
2953
3109
  required: ["to", "message"],
2954
3110
  additionalProperties: false
2955
3111
  },
2956
3112
  annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2957
- handler: async (args) => runtime.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments))
3113
+ handler: async (args) => runtime.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments), void 0, optionalString(args.team, "team"))
2958
3114
  },
2959
3115
  {
2960
3116
  name: "intercom_ask",
@@ -2965,6 +3121,7 @@ function buildToolDefinitions(runtime) {
2965
3121
  to: { type: "string" },
2966
3122
  message: { type: "string" },
2967
3123
  attachments: attachmentsSchema,
3124
+ team: { type: "string", description: "Task team; required when multiple teams are shared." },
2968
3125
  timeout_ms: { type: "integer", minimum: 1, maximum: 12e4, description: "Maximum time to wait for a reply before returning an error. Use intercom_send plus intercom_pending for longer work." }
2969
3126
  },
2970
3127
  required: ["to", "message"],
@@ -2976,7 +3133,8 @@ function buildToolDefinitions(runtime) {
2976
3133
  asString(args.message, "message"),
2977
3134
  asAttachmentArray(args.attachments),
2978
3135
  asOptionalPositiveInteger(args.timeout_ms, "timeout_ms"),
2979
- signal
3136
+ signal,
3137
+ optionalString(args.team, "team")
2980
3138
  )
2981
3139
  },
2982
3140
  {
@@ -2992,19 +3150,22 @@ function buildToolDefinitions(runtime) {
2992
3150
  },
2993
3151
  {
2994
3152
  name: "intercom_reply",
2995
- description: "Reply to a pending inbound ask. Use to plus which=oldest/latest when one sender has multiple unresolved asks.",
3153
+ description: "Reply to an inbound ask or ordinary message using askId or contextId from intercom_pending. Inherits the original team; team cannot override it.",
2996
3154
  inputSchema: {
2997
3155
  type: "object",
2998
3156
  properties: {
2999
3157
  message: { type: "string" },
3000
3158
  to: { type: "string", description: "Optional sender/session selector; never a message or thread ID." },
3159
+ askId: { type: "string", description: "Stable receiver-local pending ask selector." },
3160
+ contextId: { type: "string", description: "Exact inbound context selector, including ordinary messages." },
3161
+ team: { type: "string", description: "Must match the original message; omit to inherit." },
3001
3162
  which: { type: "string", enum: ["oldest", "latest"], description: "Select the oldest or latest ask from the chosen sender." }
3002
3163
  },
3003
3164
  required: ["message"],
3004
3165
  additionalProperties: false
3005
3166
  },
3006
3167
  annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
3007
- handler: async (args) => runtime.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : void 0, args.which === "oldest" || args.which === "latest" ? args.which : void 0)
3168
+ handler: async (args) => runtime.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : void 0, args.which === "oldest" || args.which === "latest" ? args.which : void 0, optionalString(args.askId, "askId"), optionalString(args.contextId, "contextId"), optionalString(args.team, "team"))
3008
3169
  }
3009
3170
  ];
3010
3171
  return tools;
@@ -3035,7 +3196,8 @@ async function handleMcpRequest(request, runtime) {
3035
3196
  return ok(request.id, {
3036
3197
  protocolVersion: "2025-06-18",
3037
3198
  capabilities: { tools: {} },
3038
- serverInfo: { name: "claude-intercom", version: "0.14.0" }
3199
+ instructions: TASK_TEAM_GUIDANCE,
3200
+ serverInfo: { name: "claude-intercom", version: "0.15.1" }
3039
3201
  });
3040
3202
  case "ping":
3041
3203
  return ok(request.id, {});
@@ -3072,9 +3234,9 @@ async function handleMcpRequest(request, runtime) {
3072
3234
  // claude/session-lifecycle.ts
3073
3235
  import { execFileSync } from "node:child_process";
3074
3236
  import { closeSync as closeSync2, openSync as openSync2, readFileSync as readFileSync7, readSync, renameSync as renameSync3, statSync, writeFileSync as writeFileSync3 } from "node:fs";
3075
- import { basename as basename2, dirname as dirname4, join as join8 } from "node:path";
3237
+ import { basename as basename2, dirname as dirname4, join as join9 } from "node:path";
3076
3238
  import { StringDecoder } from "node:string_decoder";
3077
- import { setTimeout as delay } from "node:timers/promises";
3239
+ import { setTimeout as delay2 } from "node:timers/promises";
3078
3240
  function findClaudeHost(pid = process.ppid) {
3079
3241
  for (let depth = 0; pid > 1 && depth < 16; depth++) {
3080
3242
  let parts;
@@ -3097,7 +3259,7 @@ function findClaudeHost(pid = process.ppid) {
3097
3259
  return void 0;
3098
3260
  }
3099
3261
  function claudeSessionMetadataPath(host) {
3100
- return join8(getIntercomDirPath(), `claude-host-${host.pid}.json`);
3262
+ return join9(getIntercomDirPath(), `claude-host-${host.pid}.json`);
3101
3263
  }
3102
3264
  function readClaudeSessionMetadata(path, host) {
3103
3265
  try {
@@ -3183,7 +3345,7 @@ async function waitForClaudeSession(reader) {
3183
3345
  for (let attempt = 0; attempt < 20; attempt++) {
3184
3346
  const metadata = reader.read();
3185
3347
  if (metadata) return metadata;
3186
- await delay(50);
3348
+ await delay2(50);
3187
3349
  }
3188
3350
  return reader.read();
3189
3351
  }
@@ -33,7 +33,8 @@ function formatInboxLine(entry) {
33
33
  const who = entry.fromName ? `${entry.fromName} (${entry.fromId.slice(0, 8)})` : entry.fromId;
34
34
  const kind = entry.expectsReply ? " [asking \u2014 awaiting your reply]" : "";
35
35
  const oneLine = entry.text.replace(/\s+/g, " ").trim();
36
- return `Intercom message from ${who}${kind}: ${oneLine}`;
36
+ return `Intercom message from ${who}${entry.team ? ` [Team: ${entry.team}]` : ""}${kind}: ${oneLine}
37
+ Reply with intercom_reply({ contextId: "ctx-${entry.messageId}", message: "..." }); inherit the original team.`;
37
38
  }
38
39
  function inboxBatchFrom(content, alreadyEmitted) {
39
40
  const lines = content.split("\n").map((line) => line.trim()).filter(Boolean);