@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-plugin/plugin.json +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +14 -3
- package/broker/broker.ts +3 -2
- package/broker/client.ts +5 -2
- package/claude/inbox.ts +3 -1
- package/claude/mcp-protocol.ts +31 -9
- package/claude/named-team-membership.ts +124 -0
- package/claude/named-teams.ts +11 -0
- package/claude/native-bridge.ts +13 -4
- package/claude/reply-context.ts +49 -0
- package/claude/runtime.ts +45 -31
- package/claude/team-guidance.ts +7 -0
- package/claude/worker-daemon.ts +4 -0
- package/commands/intercom-create.md +4 -2
- package/commands/intercom-join.md +1 -1
- package/dist/broker.mjs +3 -2
- package/dist/cci.mjs +23 -8
- package/dist/ccim.mjs +23 -8
- package/dist/claude-server.mjs +241 -85
- package/dist/inbox-monitor.mjs +2 -1
- package/dist/worker-daemon.mjs +11 -5
- package/package.json +1 -1
- package/skills/claude-intercom/SKILL.md +5 -5
- package/types.ts +1 -0
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
|
-
//
|
|
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
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
|
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 =
|
|
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.`;
|
package/claude/worker-daemon.ts
CHANGED
|
@@ -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
|
|
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
|
-
|
|
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
|
|
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 (
|
|
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 (
|
|
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
|
|
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 (
|
|
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
|
|
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" });
|