@ctliz/agent-intercom-codex 0.12.2 → 0.13.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/.codex-plugin/plugin.json +1 -1
- package/CHANGELOG.md +6 -0
- package/README.md +14 -3
- package/broker/broker.ts +1 -0
- package/broker/client.ts +3 -0
- package/codex/bridge-daemon.ts +51 -15
- package/codex/mcp-protocol.ts +31 -9
- package/codex/named-team-membership.ts +124 -0
- package/codex/named-teams.ts +11 -0
- package/codex/reply-context.ts +49 -0
- package/codex/runtime.ts +44 -30
- package/codex/team-guidance.ts +7 -0
- package/dist/bridge-daemon.mjs +264 -14
- package/dist/broker.mjs +2 -1
- package/dist/build-info.json +2 -2
- package/dist/codex-server.mjs +235 -80
- package/dist/coi.mjs +275 -25
- package/package.json +1 -1
- package/skills/codex-intercom/SKILL.md +5 -5
- package/types.ts +1 -0
package/dist/codex-server.mjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.
|
|
2
|
+
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.13.0 target=codex-server sourceSha256=04013522326f3042c72f6454a9c79e26d2d1c8de7af57769ed890e50fca514e3\n");
|
|
3
3
|
|
|
4
4
|
// codex/server.ts
|
|
5
5
|
import readline from "node:readline";
|
|
@@ -867,6 +867,7 @@ function isMessage(value) {
|
|
|
867
867
|
if (typeof content.text !== "string") {
|
|
868
868
|
return false;
|
|
869
869
|
}
|
|
870
|
+
if (content.team !== void 0 && (typeof content.team !== "string" || !/^[A-Za-z][A-Za-z0-9_-]{0,31}$/.test(content.team))) return false;
|
|
870
871
|
return content.attachments === void 0 || Array.isArray(content.attachments) && content.attachments.every(isAttachment);
|
|
871
872
|
}
|
|
872
873
|
function isSessionInfo(value) {
|
|
@@ -1457,6 +1458,7 @@ var IntercomClient = class extends EventEmitter {
|
|
|
1457
1458
|
expectsReply: options.expectsReply,
|
|
1458
1459
|
content: {
|
|
1459
1460
|
text: options.text,
|
|
1461
|
+
...options.team === void 0 ? {} : { team: options.team },
|
|
1460
1462
|
attachments: options.attachments
|
|
1461
1463
|
}
|
|
1462
1464
|
};
|
|
@@ -2380,19 +2382,17 @@ function parseStoredTeam(value) {
|
|
|
2380
2382
|
if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt <= 0) {
|
|
2381
2383
|
return void 0;
|
|
2382
2384
|
}
|
|
2385
|
+
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;
|
|
2386
|
+
if (value.work !== void 0 && (typeof value.work !== "string" || !value.work.trim() || value.work.length > 2e3)) return void 0;
|
|
2383
2387
|
return {
|
|
2384
2388
|
name: value.name,
|
|
2385
2389
|
scopeId: value.scopeId,
|
|
2386
2390
|
managerSessionId: value.managerSessionId,
|
|
2387
|
-
createdAt: value.createdAt
|
|
2391
|
+
createdAt: value.createdAt,
|
|
2392
|
+
...value.memberSessionIds === void 0 ? {} : { memberSessionIds: value.memberSessionIds },
|
|
2393
|
+
...value.work === void 0 ? {} : { work: value.work }
|
|
2388
2394
|
};
|
|
2389
2395
|
}
|
|
2390
|
-
function rejectManagedJoin(env = process.env) {
|
|
2391
|
-
if (env.AGENT_INTERCOM_WORKER_ID?.trim() || env.AGENT_INTERCOM_OWNED === "1" || env.AGENT_INTERCOM_TEAM_MANIFEST?.trim()) {
|
|
2392
|
-
return "This session is already a managed member and cannot join another team.";
|
|
2393
|
-
}
|
|
2394
|
-
return void 0;
|
|
2395
|
-
}
|
|
2396
2396
|
function parseTeamName(raw) {
|
|
2397
2397
|
const name = raw.trim();
|
|
2398
2398
|
if (!name || name.includes(" ") || !NAMED_TEAM_NAME_PATTERN.test(name)) {
|
|
@@ -2437,35 +2437,6 @@ function listNamedTeams(agentDir) {
|
|
|
2437
2437
|
}
|
|
2438
2438
|
return teams;
|
|
2439
2439
|
}
|
|
2440
|
-
function findNamedTeam(name, agentDir) {
|
|
2441
|
-
return listNamedTeams(agentDir).find((team) => team.name === name);
|
|
2442
|
-
}
|
|
2443
|
-
function createNamedTeam(input) {
|
|
2444
|
-
const name = parseTeamName(input.name);
|
|
2445
|
-
const managerSessionId = input.managerSessionId.trim();
|
|
2446
|
-
if (!managerSessionId || managerSessionId !== input.managerSessionId || /[\u0000-\u001f\u007f]/.test(managerSessionId)) {
|
|
2447
|
-
throw genericWriteError();
|
|
2448
|
-
}
|
|
2449
|
-
const existing = listNamedTeams(input.agentDir);
|
|
2450
|
-
if (existing.some((team2) => team2.name === name)) {
|
|
2451
|
-
throw new Error(`A named team called ${name} already exists.`);
|
|
2452
|
-
}
|
|
2453
|
-
const scopeId = (input.generateScope ?? (() => generateNamedTeamScope(existing.map((team2) => team2.scopeId))))();
|
|
2454
|
-
if (!isNamedTeamScope(scopeId) || existing.some((team2) => team2.scopeId === scopeId)) {
|
|
2455
|
-
throw genericWriteError();
|
|
2456
|
-
}
|
|
2457
|
-
const team = {
|
|
2458
|
-
name,
|
|
2459
|
-
scopeId,
|
|
2460
|
-
managerSessionId,
|
|
2461
|
-
createdAt: input.now ?? Date.now()
|
|
2462
|
-
};
|
|
2463
|
-
const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
|
|
2464
|
-
ensureIntercomRuntimeDir(dir);
|
|
2465
|
-
const payload = { version: NAMED_TEAMS_VERSION, teams: [...existing, team] };
|
|
2466
|
-
writeDurableJson(teamsFilePath(input.agentDir), payload);
|
|
2467
|
-
return team;
|
|
2468
|
-
}
|
|
2469
2440
|
function formatJoinableNamedTeamList(teams) {
|
|
2470
2441
|
if (teams.length === 0) {
|
|
2471
2442
|
return 'No joinable named teams found.\nCreate one with intercom_join({ name: "billing", create: true }).';
|
|
@@ -2485,6 +2456,147 @@ Role: teammate
|
|
|
2485
2456
|
Display name: ${input.name}`;
|
|
2486
2457
|
}
|
|
2487
2458
|
|
|
2459
|
+
// codex/named-team-membership.ts
|
|
2460
|
+
import { mkdirSync as mkdirSync4, rmSync } from "node:fs";
|
|
2461
|
+
import { join as join8 } from "node:path";
|
|
2462
|
+
import { setTimeout as delay } from "node:timers/promises";
|
|
2463
|
+
function namedTeamMemberIds(team) {
|
|
2464
|
+
return team.memberSessionIds ?? [team.managerSessionId];
|
|
2465
|
+
}
|
|
2466
|
+
function sessionNamedTeams(sessionId, agentDir) {
|
|
2467
|
+
return listNamedTeams(agentDir).filter((team) => namedTeamMemberIds(team).includes(sessionId));
|
|
2468
|
+
}
|
|
2469
|
+
async function appendNamedTeamMembership(input) {
|
|
2470
|
+
const name = parseTeamName(input.name);
|
|
2471
|
+
const memberIds = [.../* @__PURE__ */ new Set([input.selfId, ...input.members ?? []])];
|
|
2472
|
+
if (memberIds.some((id) => !id || id.trim() !== id || /[\u0000-\u001f\u007f]/.test(id))) {
|
|
2473
|
+
throw new Error("Invalid team member session ID");
|
|
2474
|
+
}
|
|
2475
|
+
if (input.work !== void 0 && (!input.work.trim() || input.work.length > 2e3)) {
|
|
2476
|
+
throw new Error("Work must be a non-empty task description (max 2000 characters)");
|
|
2477
|
+
}
|
|
2478
|
+
const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
|
|
2479
|
+
ensureIntercomRuntimeDir(dir);
|
|
2480
|
+
const lock = join8(dir, "named-teams.lock");
|
|
2481
|
+
const deadline = Date.now() + 5e3;
|
|
2482
|
+
for (; ; ) {
|
|
2483
|
+
try {
|
|
2484
|
+
mkdirSync4(lock, { mode: 448 });
|
|
2485
|
+
break;
|
|
2486
|
+
} catch (error2) {
|
|
2487
|
+
if (error2.code !== "EEXIST") throw error2;
|
|
2488
|
+
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.");
|
|
2489
|
+
await delay(25);
|
|
2490
|
+
}
|
|
2491
|
+
}
|
|
2492
|
+
try {
|
|
2493
|
+
const teams = listNamedTeams(input.agentDir);
|
|
2494
|
+
let team = teams.find((entry) => entry.name === name);
|
|
2495
|
+
if (input.create) {
|
|
2496
|
+
if (team) throw new Error(`A named team called ${name} already exists; join it instead.`);
|
|
2497
|
+
team = { name, scopeId: generateNamedTeamScope(teams.map((entry) => entry.scopeId)), managerSessionId: input.selfId, createdAt: Date.now() };
|
|
2498
|
+
teams.push(team);
|
|
2499
|
+
}
|
|
2500
|
+
if (!team) throw new Error(`Team "${name}" does not exist; create it explicitly.`);
|
|
2501
|
+
if (memberIds.some((id) => id !== input.selfId) && team.managerSessionId !== input.selfId) {
|
|
2502
|
+
throw new Error(`Only the manager of team "${name}" may add other sessions`);
|
|
2503
|
+
}
|
|
2504
|
+
if (input.work !== void 0 && !input.create) {
|
|
2505
|
+
throw new Error("Work is set when creating a team; use a new team for a different task");
|
|
2506
|
+
}
|
|
2507
|
+
const updated = {
|
|
2508
|
+
...team,
|
|
2509
|
+
memberSessionIds: [.../* @__PURE__ */ new Set([...namedTeamMemberIds(team), ...memberIds])],
|
|
2510
|
+
...input.work === void 0 ? {} : { work: input.work.trim() }
|
|
2511
|
+
};
|
|
2512
|
+
writeDurableJson(join8(dir, NAMED_TEAMS_FILE), {
|
|
2513
|
+
version: NAMED_TEAMS_VERSION,
|
|
2514
|
+
teams: teams.map((entry) => entry.name === name ? updated : entry)
|
|
2515
|
+
});
|
|
2516
|
+
return updated;
|
|
2517
|
+
} finally {
|
|
2518
|
+
rmSync(lock, { recursive: true });
|
|
2519
|
+
}
|
|
2520
|
+
}
|
|
2521
|
+
function requireNamedTeamMembers(name, selfId, peerId, agentDir) {
|
|
2522
|
+
const team = listNamedTeams(agentDir).find((entry) => entry.name === name);
|
|
2523
|
+
if (!team) throw new Error(`Unknown team "${name}"`);
|
|
2524
|
+
const members = namedTeamMemberIds(team);
|
|
2525
|
+
if (!members.includes(selfId) || !members.includes(peerId)) {
|
|
2526
|
+
throw new Error(`Both sessions must belong to team "${name}" before messaging`);
|
|
2527
|
+
}
|
|
2528
|
+
return team;
|
|
2529
|
+
}
|
|
2530
|
+
function resolveNamedMessageTeam(selfId, peerId, requested, agentDir) {
|
|
2531
|
+
if (requested !== void 0) return requireNamedTeamMembers(requested, selfId, peerId, agentDir).name;
|
|
2532
|
+
const mine = sessionNamedTeams(selfId, agentDir);
|
|
2533
|
+
const shared = mine.filter((team) => namedTeamMemberIds(team).includes(peerId));
|
|
2534
|
+
if (shared.length === 1) return shared[0].name;
|
|
2535
|
+
if (shared.length > 1) throw new Error("Multiple shared teams; specify `team` for this task");
|
|
2536
|
+
return void 0;
|
|
2537
|
+
}
|
|
2538
|
+
function namedTeamRoster(team, selfId, sessions) {
|
|
2539
|
+
return {
|
|
2540
|
+
name: team.name,
|
|
2541
|
+
...team.work ? { work: team.work } : {},
|
|
2542
|
+
self: { id: selfId, isManager: selfId === team.managerSessionId },
|
|
2543
|
+
manager: { target: team.managerSessionId, connected: sessions.some((entry) => entry.id === team.managerSessionId) },
|
|
2544
|
+
members: namedTeamMemberIds(team).map((id) => ({
|
|
2545
|
+
id,
|
|
2546
|
+
target: id,
|
|
2547
|
+
name: sessions.find((entry) => entry.id === id)?.name,
|
|
2548
|
+
role: id === team.managerSessionId ? "manager" : "member",
|
|
2549
|
+
connected: sessions.some((entry) => entry.id === id)
|
|
2550
|
+
}))
|
|
2551
|
+
};
|
|
2552
|
+
}
|
|
2553
|
+
function formatNamedTeamRoster(team) {
|
|
2554
|
+
return [
|
|
2555
|
+
`Team: ${team.name}`,
|
|
2556
|
+
...team.work ? [`Work: ${team.work}`] : [],
|
|
2557
|
+
`You: ${team.self.id}${team.self.isManager ? " [manager]" : ""}`,
|
|
2558
|
+
`Manager: ${team.manager.target}${team.manager.connected ? "" : " [offline]"}`,
|
|
2559
|
+
"Members:",
|
|
2560
|
+
...team.members.map((member) => `- ${member.name || member.id} (${member.id}) [${member.role}]${member.connected ? "" : " [offline]"}`)
|
|
2561
|
+
].join("\n");
|
|
2562
|
+
}
|
|
2563
|
+
|
|
2564
|
+
// codex/reply-context.ts
|
|
2565
|
+
function contextId(entry) {
|
|
2566
|
+
return `ctx-${entry.message.id}`;
|
|
2567
|
+
}
|
|
2568
|
+
function askId(entry) {
|
|
2569
|
+
return `ask-${entry.message.id}`;
|
|
2570
|
+
}
|
|
2571
|
+
function replyHint(entry) {
|
|
2572
|
+
const team = entry.message.content.team;
|
|
2573
|
+
return `${team ? ` [Team: ${team}]` : ""} [contextId: ${contextId(entry)}]${entry.message.expectsReply ? ` [askId: ${askId(entry)}]` : ""}`;
|
|
2574
|
+
}
|
|
2575
|
+
function selectReplyContext(entries, asks, input) {
|
|
2576
|
+
if (input.askId && input.contextId) throw new Error("Specify askId or contextId, not both");
|
|
2577
|
+
let target;
|
|
2578
|
+
if (input.askId || input.contextId) {
|
|
2579
|
+
const selected = input.askId ? asks.find((entry) => askId(entry) === input.askId) : entries.find((entry) => contextId(entry) === input.contextId);
|
|
2580
|
+
if (!selected || selected.message.expectsReply && !asks.includes(selected)) throw new Error("Unknown or resolved reply selector; call intercom_pending");
|
|
2581
|
+
if (input.to && selected.from.id !== input.to && selected.from.name?.toLowerCase() !== input.to.toLowerCase()) {
|
|
2582
|
+
throw new Error("Reply sender does not match the selected context");
|
|
2583
|
+
}
|
|
2584
|
+
if (input.which) throw new Error("Do not combine an exact reply selector with which");
|
|
2585
|
+
target = selected;
|
|
2586
|
+
} else {
|
|
2587
|
+
const to = input.to;
|
|
2588
|
+
const candidates = to ? asks.filter((entry) => entry.from.id === to || entry.from.name?.toLowerCase() === to.toLowerCase() || entry.from.id.startsWith(to)) : asks;
|
|
2589
|
+
if (!input.which && new Set(candidates.map((entry) => entry.message.content.team)).size > 1) {
|
|
2590
|
+
throw new Error("Multiple team contexts; specify askId or contextId");
|
|
2591
|
+
}
|
|
2592
|
+
target = selectPendingAsk(asks, input.to, input.which);
|
|
2593
|
+
}
|
|
2594
|
+
if (input.team !== void 0 && input.team !== target.message.content.team) {
|
|
2595
|
+
throw new Error("Reply team must match the original message; omit team to inherit it");
|
|
2596
|
+
}
|
|
2597
|
+
return target;
|
|
2598
|
+
}
|
|
2599
|
+
|
|
2488
2600
|
// codex/runtime.ts
|
|
2489
2601
|
function matchesPendingSender(entry, to) {
|
|
2490
2602
|
return entry.from.id === to || entry.from.name?.toLowerCase() === to.toLowerCase() || entry.from.id.startsWith(to);
|
|
@@ -2522,6 +2634,9 @@ function publicPendingEntry(entry, selector) {
|
|
|
2522
2634
|
...entry.from.parentSessionId ? { parent_session_id: entry.from.parentSessionId } : {},
|
|
2523
2635
|
...entry.from.generation ? { generation: entry.from.generation } : {}
|
|
2524
2636
|
},
|
|
2637
|
+
contextId: contextId(entry),
|
|
2638
|
+
...entry.message.expectsReply ? { askId: askId(entry) } : {},
|
|
2639
|
+
...entry.message.content.team ? { team: entry.message.content.team } : {},
|
|
2525
2640
|
received_at: entry.receivedAt,
|
|
2526
2641
|
read: entry.read,
|
|
2527
2642
|
text: entry.message.content.text,
|
|
@@ -2702,7 +2817,7 @@ var CodexIntercomRuntime = class {
|
|
|
2702
2817
|
}
|
|
2703
2818
|
scheduleReconnect() {
|
|
2704
2819
|
if (!this.reconnectEnabled || this.reconnectTimer) return;
|
|
2705
|
-
const
|
|
2820
|
+
const delay2 = this.reconnectDelays[Math.min(this.reconnectAttempt, this.reconnectDelays.length - 1)];
|
|
2706
2821
|
this.reconnectTimer = setTimeout(() => {
|
|
2707
2822
|
this.reconnectTimer = null;
|
|
2708
2823
|
void this.connect().then((client) => {
|
|
@@ -2714,7 +2829,7 @@ var CodexIntercomRuntime = class {
|
|
|
2714
2829
|
this.reconnectAttempt += 1;
|
|
2715
2830
|
this.scheduleReconnect();
|
|
2716
2831
|
});
|
|
2717
|
-
},
|
|
2832
|
+
}, delay2);
|
|
2718
2833
|
this.reconnectTimer.unref?.();
|
|
2719
2834
|
}
|
|
2720
2835
|
clearReconnectTimer() {
|
|
@@ -2757,25 +2872,24 @@ var CodexIntercomRuntime = class {
|
|
|
2757
2872
|
this.reconnectEnabled = true;
|
|
2758
2873
|
await this.connect();
|
|
2759
2874
|
}
|
|
2760
|
-
async join(name, create = false) {
|
|
2761
|
-
const blocked = rejectManagedJoin();
|
|
2762
|
-
if (blocked) return textResult(blocked, { ok: false }, true);
|
|
2875
|
+
async join(name, create = false, members, work) {
|
|
2763
2876
|
try {
|
|
2764
|
-
if (create) {
|
|
2765
|
-
if (typeof name !== "string" || !name.trim()) {
|
|
2766
|
-
return textResult("Creating a team requires a name.", { ok: false }, true);
|
|
2767
|
-
}
|
|
2768
|
-
const team2 = createNamedTeam({ name: parseTeamName(name), managerSessionId: this.identity.sessionId });
|
|
2769
|
-
await this.switchRuntimeScope(team2.scopeId, team2.managerSessionId);
|
|
2770
|
-
return textResult(formatCreateSuccess({ team: team2.name, name: this.identity.name }), { ok: true, team: team2.name, role: "manager" });
|
|
2771
|
-
}
|
|
2772
2877
|
if (!name?.trim()) {
|
|
2878
|
+
if (create || members?.length || work !== void 0) throw new Error("Creating or extending a team requires a name");
|
|
2773
2879
|
return textResult(formatJoinableNamedTeamList(listNamedTeams()));
|
|
2774
2880
|
}
|
|
2775
|
-
const
|
|
2776
|
-
|
|
2777
|
-
|
|
2778
|
-
|
|
2881
|
+
const client = await this.connect();
|
|
2882
|
+
const sessions = await client.listSessions();
|
|
2883
|
+
const memberIds = (members ?? []).map((member) => {
|
|
2884
|
+
const id = resolveSessionTarget(sessions, member);
|
|
2885
|
+
if (!id) throw new Error(`Session "${member}" is not connected`);
|
|
2886
|
+
return id;
|
|
2887
|
+
});
|
|
2888
|
+
const team = await appendNamedTeamMembership({ name: parseTeamName(name), selfId: this.identity.sessionId, members: memberIds, create, work });
|
|
2889
|
+
const roster = namedTeamRoster(team, this.identity.sessionId, sessions);
|
|
2890
|
+
const notice = create ? formatCreateSuccess({ team: team.name, name: this.identity.name }) : formatNamedJoinSuccess({ team: team.name, name: this.identity.name });
|
|
2891
|
+
return textResult(`${notice}
|
|
2892
|
+
Other team memberships are unchanged.`, { ok: true, team: team.name, role: roster.self.isManager ? "manager" : "member", roster });
|
|
2779
2893
|
} catch (error2) {
|
|
2780
2894
|
return textResult(error2 instanceof Error ? error2.message : String(error2), { ok: false }, true);
|
|
2781
2895
|
}
|
|
@@ -2785,7 +2899,7 @@ var CodexIntercomRuntime = class {
|
|
|
2785
2899
|
if (waiter) {
|
|
2786
2900
|
const senderTarget = from.name || from.id;
|
|
2787
2901
|
const fromMatches = senderTarget.toLowerCase() === waiter.from.toLowerCase() || from.id === waiter.from;
|
|
2788
|
-
if (fromMatches) {
|
|
2902
|
+
if (fromMatches && message.content.team === waiter.team) {
|
|
2789
2903
|
this.replyWaiters.delete(waiter.replyTo);
|
|
2790
2904
|
clearTimeout(waiter.timeout);
|
|
2791
2905
|
waiter.cleanup?.();
|
|
@@ -2799,7 +2913,7 @@ var CodexIntercomRuntime = class {
|
|
|
2799
2913
|
this.unresolvedAsks.set(message.id, entry);
|
|
2800
2914
|
}
|
|
2801
2915
|
}
|
|
2802
|
-
waitForReply(from, replyTo, timeoutMs = getAskTimeoutMs(), signal) {
|
|
2916
|
+
waitForReply(from, replyTo, timeoutMs = getAskTimeoutMs(), signal, team) {
|
|
2803
2917
|
return new Promise((resolve3, reject) => {
|
|
2804
2918
|
if (signal?.aborted) {
|
|
2805
2919
|
reject(new Error("intercom_ask cancelled"));
|
|
@@ -2823,7 +2937,7 @@ var CodexIntercomRuntime = class {
|
|
|
2823
2937
|
reject(new Error(`No reply from "${from}" within ${Math.round(timeoutMs / 1e3)} seconds`));
|
|
2824
2938
|
}, timeoutMs);
|
|
2825
2939
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
2826
|
-
this.replyWaiters.set(replyTo, { from, replyTo, resolve: resolve3, reject, timeout, cleanup });
|
|
2940
|
+
this.replyWaiters.set(replyTo, { from, replyTo, team, resolve: resolve3, reject, timeout, cleanup });
|
|
2827
2941
|
});
|
|
2828
2942
|
}
|
|
2829
2943
|
async resolveTarget(to) {
|
|
@@ -2841,9 +2955,16 @@ cwd: ${this.identity.cwd}`,
|
|
|
2841
2955
|
{ session_id: sessionId, name: this.identity.name, cwd: this.identity.cwd, model: this.identity.model }
|
|
2842
2956
|
);
|
|
2843
2957
|
}
|
|
2844
|
-
async team() {
|
|
2958
|
+
async team(name) {
|
|
2845
2959
|
const client = await this.connect();
|
|
2846
2960
|
const sessions = await client.listSessions();
|
|
2961
|
+
const mine = sessionNamedTeams(this.identity.sessionId);
|
|
2962
|
+
const selected = name ? mine.filter((entry) => entry.name === name) : mine;
|
|
2963
|
+
if (name && !selected.length) return textResult(`You do not belong to team "${name}"`, { ok: false }, true);
|
|
2964
|
+
if (selected.length) {
|
|
2965
|
+
const teams = selected.map((entry) => namedTeamRoster(entry, this.identity.sessionId, sessions));
|
|
2966
|
+
return textResult(teams.map(formatNamedTeamRoster).join("\n\n"), { teams });
|
|
2967
|
+
}
|
|
2847
2968
|
const team = await resolveIntercomTeam({ selfId: client.sessionId ?? this.identity.sessionId, sessions });
|
|
2848
2969
|
return textResult(formatIntercomTeam(team), team);
|
|
2849
2970
|
}
|
|
@@ -2884,28 +3005,34 @@ Pending asks: ${this.unresolvedAsks.size}`,
|
|
|
2884
3005
|
client.updatePresence({ status: summary.trim() || "idle" });
|
|
2885
3006
|
return textResult("Summary updated.", { ok: true, summary });
|
|
2886
3007
|
}
|
|
2887
|
-
async send(to, message, attachments, replyTo) {
|
|
3008
|
+
async send(to, message, attachments, replyTo, requestedTeam) {
|
|
2888
3009
|
const client = await this.connect();
|
|
2889
3010
|
const sendTo = await this.resolveTarget(to);
|
|
2890
|
-
const
|
|
3011
|
+
const source = replyTo ? this.unread.find((entry) => entry.message.id === replyTo && entry.from.id === sendTo) : void 0;
|
|
3012
|
+
if (replyTo && !source) throw new Error("Unknown inbound reply context");
|
|
3013
|
+
if (source && requestedTeam !== void 0 && requestedTeam !== source.message.content.team) throw new Error("Reply team must match the original message");
|
|
3014
|
+
const team = source ? source.message.content.team : resolveNamedMessageTeam(this.identity.sessionId, sendTo, requestedTeam);
|
|
3015
|
+
const result = await client.send(sendTo, { text: message, attachments, replyTo: source?.message.expectsReply ? replyTo : void 0, team });
|
|
2891
3016
|
if (!result.delivered) {
|
|
2892
3017
|
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);
|
|
2893
3018
|
}
|
|
2894
3019
|
if (replyTo) this.unresolvedAsks.delete(replyTo);
|
|
2895
3020
|
return textResult(`Message sent to ${to}.`, { ok: true, message_id: result.id, to });
|
|
2896
3021
|
}
|
|
2897
|
-
async ask(to, message, attachments, timeoutMs = getAskTimeoutMs(), signal) {
|
|
3022
|
+
async ask(to, message, attachments, timeoutMs = getAskTimeoutMs(), signal, requestedTeam) {
|
|
2898
3023
|
const client = await this.connect();
|
|
2899
3024
|
const sendTo = await this.resolveTarget(to);
|
|
3025
|
+
const team = resolveNamedMessageTeam(this.identity.sessionId, sendTo, requestedTeam);
|
|
2900
3026
|
const questionId = randomUUID4();
|
|
2901
|
-
const replyPromise = this.waitForReply(sendTo, questionId, timeoutMs, signal);
|
|
3027
|
+
const replyPromise = this.waitForReply(sendTo, questionId, timeoutMs, signal, team);
|
|
2902
3028
|
void replyPromise.catch(() => void 0);
|
|
2903
3029
|
try {
|
|
2904
3030
|
const result = await client.send(sendTo, {
|
|
2905
3031
|
messageId: questionId,
|
|
2906
3032
|
text: message,
|
|
2907
3033
|
attachments,
|
|
2908
|
-
expectsReply: true
|
|
3034
|
+
expectsReply: true,
|
|
3035
|
+
team
|
|
2909
3036
|
});
|
|
2910
3037
|
if (!result.delivered) {
|
|
2911
3038
|
this.replyWaiters.get(questionId)?.reject(new Error(result.reason ?? "Session may not exist or has disconnected."));
|
|
@@ -2929,12 +3056,12 @@ ${replyText}`, { ok: true, message_id: result.id, reply });
|
|
|
2929
3056
|
}
|
|
2930
3057
|
const pendingAsks = Array.from(this.unresolvedAsks.values()).sort((a, b) => a.receivedAt - b.receivedAt);
|
|
2931
3058
|
const lines = [
|
|
2932
|
-
unreadMessages.length ? unreadMessages.map((entry) => `- ${formatSessionDisplay(entry.from)}: ${entry.message.content.text}${formatAttachments(entry.message.content.attachments)}`).join("\n") : "No unread messages.",
|
|
3059
|
+
unreadMessages.length ? unreadMessages.map((entry) => `- ${formatSessionDisplay(entry.from)}${replyHint(entry)}: ${entry.message.content.text}${formatAttachments(entry.message.content.attachments)}`).join("\n") : "No unread messages.",
|
|
2933
3060
|
pendingAsks.length ? `
|
|
2934
3061
|
Pending asks:
|
|
2935
3062
|
${pendingAsks.map((entry) => {
|
|
2936
3063
|
const selector = pendingSelector(pendingAsks, entry);
|
|
2937
|
-
return `- ${formatSessionDisplay(entry.from)}${selector ? ` [${selector}]` : ""}: ${entry.message.content.text}`;
|
|
3064
|
+
return `- ${formatSessionDisplay(entry.from)}${replyHint(entry)}${selector ? ` [${selector}]` : ""}: ${entry.message.content.text}`;
|
|
2938
3065
|
}).join("\n")}` : ""
|
|
2939
3066
|
].filter(Boolean);
|
|
2940
3067
|
return textResult(lines.join("\n"), {
|
|
@@ -2942,10 +3069,10 @@ ${pendingAsks.map((entry) => {
|
|
|
2942
3069
|
pending_asks: pendingAsks.map((entry) => publicPendingEntry(entry, pendingSelector(pendingAsks, entry)))
|
|
2943
3070
|
});
|
|
2944
3071
|
}
|
|
2945
|
-
async reply(message, to, which) {
|
|
3072
|
+
async reply(message, to, which, askId2, contextId2, team) {
|
|
2946
3073
|
let target;
|
|
2947
3074
|
try {
|
|
2948
|
-
target =
|
|
3075
|
+
target = selectReplyContext(this.unread, Array.from(this.unresolvedAsks.values()), { to, which, askId: askId2, contextId: contextId2, team });
|
|
2949
3076
|
} catch (error2) {
|
|
2950
3077
|
return textResult(error2 instanceof Error ? error2.message : String(error2), { ok: false }, true);
|
|
2951
3078
|
}
|
|
@@ -2957,6 +3084,15 @@ ${pendingAsks.map((entry) => {
|
|
|
2957
3084
|
}
|
|
2958
3085
|
};
|
|
2959
3086
|
|
|
3087
|
+
// codex/team-guidance.ts
|
|
3088
|
+
var TASK_TEAM_GUIDANCE = `Intercom task-team rules:
|
|
3089
|
+
- 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.
|
|
3090
|
+
- 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.
|
|
3091
|
+
- 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.
|
|
3092
|
+
- 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.
|
|
3093
|
+
- 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.
|
|
3094
|
+
- 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.`;
|
|
3095
|
+
|
|
2960
3096
|
// codex/mcp-protocol.ts
|
|
2961
3097
|
var inflightToolCalls = /* @__PURE__ */ new Map();
|
|
2962
3098
|
function asString(value, name) {
|
|
@@ -2965,6 +3101,14 @@ function asString(value, name) {
|
|
|
2965
3101
|
}
|
|
2966
3102
|
return value;
|
|
2967
3103
|
}
|
|
3104
|
+
function optionalString(value, name) {
|
|
3105
|
+
return value === void 0 ? void 0 : asString(value, name);
|
|
3106
|
+
}
|
|
3107
|
+
function memberArray(value) {
|
|
3108
|
+
if (value === void 0) return void 0;
|
|
3109
|
+
if (!Array.isArray(value)) throw new Error("members must be an array of session names or IDs");
|
|
3110
|
+
return Array.from(value, (member) => asString(member, "member"));
|
|
3111
|
+
}
|
|
2968
3112
|
function asBoolean(value, defaultValue) {
|
|
2969
3113
|
return typeof value === "boolean" ? value : defaultValue;
|
|
2970
3114
|
}
|
|
@@ -3012,26 +3156,30 @@ function buildToolDefinitions(runtime2) {
|
|
|
3012
3156
|
},
|
|
3013
3157
|
{
|
|
3014
3158
|
name: "intercom_team",
|
|
3015
|
-
description: "Show your
|
|
3016
|
-
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
3159
|
+
description: "Show all your named task teams, or inspect one by name. Falls back to managed-team discovery.",
|
|
3160
|
+
inputSchema: { type: "object", properties: { team: { type: "string" } }, additionalProperties: false },
|
|
3017
3161
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
3018
|
-
handler: async () => runtime2.team()
|
|
3162
|
+
handler: async (args) => runtime2.team(optionalString(args.team, "team"))
|
|
3019
3163
|
},
|
|
3020
3164
|
{
|
|
3021
3165
|
name: "intercom_join",
|
|
3022
|
-
description: "
|
|
3166
|
+
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.",
|
|
3023
3167
|
inputSchema: {
|
|
3024
3168
|
type: "object",
|
|
3025
3169
|
properties: {
|
|
3026
3170
|
name: { type: "string", description: "Team name to join. Omit to list joinable teams." },
|
|
3027
|
-
create: { type: "boolean", description: "Create this named team and join as manager. Requires name." }
|
|
3171
|
+
create: { type: "boolean", description: "Create this named team and join as manager. Requires name." },
|
|
3172
|
+
members: { type: "array", items: { type: "string" }, description: "Connected session names or IDs to add; manager only." },
|
|
3173
|
+
work: { type: "string", maxLength: 2e3, description: "Task description when creating a team." }
|
|
3028
3174
|
},
|
|
3029
3175
|
additionalProperties: false
|
|
3030
3176
|
},
|
|
3031
3177
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
3032
3178
|
handler: async (args) => runtime2.join(
|
|
3033
|
-
|
|
3034
|
-
args.create === true
|
|
3179
|
+
optionalString(args.name, "name"),
|
|
3180
|
+
args.create === true,
|
|
3181
|
+
memberArray(args.members),
|
|
3182
|
+
optionalString(args.work, "work")
|
|
3035
3183
|
)
|
|
3036
3184
|
},
|
|
3037
3185
|
{
|
|
@@ -3078,13 +3226,14 @@ function buildToolDefinitions(runtime2) {
|
|
|
3078
3226
|
properties: {
|
|
3079
3227
|
to: { type: "string" },
|
|
3080
3228
|
message: { type: "string" },
|
|
3081
|
-
attachments: attachmentsSchema
|
|
3229
|
+
attachments: attachmentsSchema,
|
|
3230
|
+
team: { type: "string", description: "Task team; required when multiple teams are shared. Omit for ungrouped contact." }
|
|
3082
3231
|
},
|
|
3083
3232
|
required: ["to", "message"],
|
|
3084
3233
|
additionalProperties: false
|
|
3085
3234
|
},
|
|
3086
3235
|
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
3087
|
-
handler: async (args) => runtime2.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments))
|
|
3236
|
+
handler: async (args) => runtime2.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments), void 0, optionalString(args.team, "team"))
|
|
3088
3237
|
},
|
|
3089
3238
|
{
|
|
3090
3239
|
name: "intercom_ask",
|
|
@@ -3095,6 +3244,7 @@ function buildToolDefinitions(runtime2) {
|
|
|
3095
3244
|
to: { type: "string" },
|
|
3096
3245
|
message: { type: "string" },
|
|
3097
3246
|
attachments: attachmentsSchema,
|
|
3247
|
+
team: { type: "string", description: "Task team; required when multiple teams are shared." },
|
|
3098
3248
|
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." }
|
|
3099
3249
|
},
|
|
3100
3250
|
required: ["to", "message"],
|
|
@@ -3106,7 +3256,8 @@ function buildToolDefinitions(runtime2) {
|
|
|
3106
3256
|
asString(args.message, "message"),
|
|
3107
3257
|
asAttachmentArray(args.attachments),
|
|
3108
3258
|
asOptionalPositiveInteger(args.timeout_ms, "timeout_ms"),
|
|
3109
|
-
signal
|
|
3259
|
+
signal,
|
|
3260
|
+
optionalString(args.team, "team")
|
|
3110
3261
|
)
|
|
3111
3262
|
},
|
|
3112
3263
|
{
|
|
@@ -3122,19 +3273,22 @@ function buildToolDefinitions(runtime2) {
|
|
|
3122
3273
|
},
|
|
3123
3274
|
{
|
|
3124
3275
|
name: "intercom_reply",
|
|
3125
|
-
description: "Reply to
|
|
3276
|
+
description: "Reply to an inbound ask or ordinary message using askId or contextId from intercom_pending. Inherits the original team; team cannot override it.",
|
|
3126
3277
|
inputSchema: {
|
|
3127
3278
|
type: "object",
|
|
3128
3279
|
properties: {
|
|
3129
3280
|
message: { type: "string" },
|
|
3130
3281
|
to: { type: "string", description: "Optional sender/session selector; never a message or thread ID." },
|
|
3282
|
+
askId: { type: "string", description: "Stable receiver-local pending ask selector." },
|
|
3283
|
+
contextId: { type: "string", description: "Exact inbound context selector, including ordinary messages." },
|
|
3284
|
+
team: { type: "string", description: "Must match the original message; omit to inherit." },
|
|
3131
3285
|
which: { type: "string", enum: ["oldest", "latest"], description: "Select the oldest or latest ask from the chosen sender." }
|
|
3132
3286
|
},
|
|
3133
3287
|
required: ["message"],
|
|
3134
3288
|
additionalProperties: false
|
|
3135
3289
|
},
|
|
3136
3290
|
annotations: { readOnlyHint: false, destructiveHint: true, idempotentHint: false, openWorldHint: true },
|
|
3137
|
-
handler: async (args) => runtime2.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : void 0, args.which === "oldest" || args.which === "latest" ? args.which : void 0)
|
|
3291
|
+
handler: async (args) => runtime2.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"))
|
|
3138
3292
|
}
|
|
3139
3293
|
];
|
|
3140
3294
|
}
|
|
@@ -3164,7 +3318,8 @@ async function handleMcpRequest(request, runtime2) {
|
|
|
3164
3318
|
return ok(request.id, {
|
|
3165
3319
|
protocolVersion: "2025-06-18",
|
|
3166
3320
|
capabilities: { tools: {} },
|
|
3167
|
-
|
|
3321
|
+
instructions: TASK_TEAM_GUIDANCE,
|
|
3322
|
+
serverInfo: { name: "codex-intercom", version: "0.13.0" }
|
|
3168
3323
|
});
|
|
3169
3324
|
case "ping":
|
|
3170
3325
|
return ok(request.id, {});
|