@ctliz/agent-intercom-codex 0.11.0-connect.2
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 +39 -0
- package/.mcp.json +10 -0
- package/COPYRIGHT +6 -0
- package/LICENSE +661 -0
- package/LICENSE_TRANSITION.md +25 -0
- package/README.md +572 -0
- package/THIRD_PARTY_NOTICES.md +17 -0
- package/assets/logo-generated.png +0 -0
- package/assets/logo.svg +22 -0
- package/boss-control-outbox.ts +195 -0
- package/broker/access-credential.ts +76 -0
- package/broker/access-registry.ts +464 -0
- package/broker/audit.ts +60 -0
- package/broker/authorization.ts +54 -0
- package/broker/boss-adapter.ts +531 -0
- package/broker/boss-control-ledger.ts +306 -0
- package/broker/broker.ts +2327 -0
- package/broker/client.ts +1111 -0
- package/broker/framing.ts +87 -0
- package/broker/ownership.ts +61 -0
- package/broker/paths.ts +161 -0
- package/broker/spawn.ts +453 -0
- package/codex/app-server-client.ts +562 -0
- package/codex/boss-client.ts +34 -0
- package/codex/bridge-config.ts +282 -0
- package/codex/bridge-daemon.ts +913 -0
- package/codex/clipboard.ts +142 -0
- package/codex/coi.ts +802 -0
- package/codex/contact.ts +54 -0
- package/codex/mcp-protocol.ts +248 -0
- package/codex/runtime.ts +488 -0
- package/codex/server.ts +60 -0
- package/codex/team.ts +249 -0
- package/codex/tui-input.ts +174 -0
- package/config.ts +195 -0
- package/dist/bridge-daemon.mjs +3781 -0
- package/dist/broker.mjs +3012 -0
- package/dist/build-info.json +12 -0
- package/dist/codex-server.mjs +2890 -0
- package/dist/coi.mjs +4686 -0
- package/durable-json.ts +25 -0
- package/licenses/MIT-pi-intercom.txt +21 -0
- package/outbound-outbox.ts +116 -0
- package/package.json +89 -0
- package/protocol-v4/contract.ts +27 -0
- package/provider/protected-service.ts +162 -0
- package/provider/provider.mjs +34 -0
- package/scripts/build-info.mjs +58 -0
- package/scripts/build.mjs +55 -0
- package/skills/codex-intercom/SKILL.md +104 -0
- package/tsconfig.json +16 -0
- package/types.ts +225 -0
package/codex/contact.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import type { SessionInfo } from "../types.ts";
|
|
2
|
+
|
|
3
|
+
export interface IntercomContact {
|
|
4
|
+
target: string;
|
|
5
|
+
id: string;
|
|
6
|
+
name?: string;
|
|
7
|
+
duplicateName: boolean;
|
|
8
|
+
fallback?: boolean;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function duplicateSessionNames(sessions: SessionInfo[]): Set<string> {
|
|
12
|
+
const counts = new Map<string, number>();
|
|
13
|
+
for (const session of sessions) {
|
|
14
|
+
const name = session.name?.trim().toLowerCase();
|
|
15
|
+
if (name) counts.set(name, (counts.get(name) ?? 0) + 1);
|
|
16
|
+
}
|
|
17
|
+
return new Set([...counts].filter(([, count]) => count > 1).map(([name]) => name));
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export function chooseContactTarget(currentSession: SessionInfo, sessions: SessionInfo[]): IntercomContact {
|
|
21
|
+
const duplicates = duplicateSessionNames(sessions);
|
|
22
|
+
const name = currentSession.name?.trim() || undefined;
|
|
23
|
+
const duplicateName = Boolean(name && duplicates.has(name.toLowerCase()));
|
|
24
|
+
return {
|
|
25
|
+
target: name && !duplicateName ? name : currentSession.id,
|
|
26
|
+
id: currentSession.id,
|
|
27
|
+
...(name ? { name } : {}),
|
|
28
|
+
duplicateName,
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function formatContactInstruction(contact: Pick<IntercomContact, "target" | "id">): string {
|
|
33
|
+
// v4 contact-copy: always include the full session ID. A human-readable
|
|
34
|
+
// name/target may accompany it but may not replace it.
|
|
35
|
+
if (contact.target === contact.id) {
|
|
36
|
+
return `Intercom send ID: ${contact.id}`;
|
|
37
|
+
}
|
|
38
|
+
return `Intercom send ID: ${contact.target} (full ID: ${contact.id})`;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
export async function resolveContactTarget(
|
|
42
|
+
id: string,
|
|
43
|
+
name: string | undefined,
|
|
44
|
+
listSessions: () => Promise<SessionInfo[]>,
|
|
45
|
+
): Promise<IntercomContact> {
|
|
46
|
+
try {
|
|
47
|
+
const sessions = await listSessions();
|
|
48
|
+
const currentSession = sessions.find((session) => session.id === id);
|
|
49
|
+
if (currentSession) return chooseContactTarget(currentSession, sessions);
|
|
50
|
+
} catch {
|
|
51
|
+
// The stable ID remains usable when discovery is temporarily unavailable.
|
|
52
|
+
}
|
|
53
|
+
return { target: id, id, ...(name ? { name } : {}), duplicateName: false, fallback: true };
|
|
54
|
+
}
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
import type { CodexIntercomRuntime, ToolResult } from "./runtime.ts";
|
|
2
|
+
import { validateAskTimeoutMs } from "../config.ts";
|
|
3
|
+
import type { Attachment } from "../types.ts";
|
|
4
|
+
|
|
5
|
+
interface JsonRpcRequest {
|
|
6
|
+
jsonrpc?: string;
|
|
7
|
+
id?: string | number | null;
|
|
8
|
+
method?: string;
|
|
9
|
+
params?: Record<string, unknown>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
type RequestId = string | number;
|
|
13
|
+
type ToolHandler = (args: Record<string, unknown>, signal?: AbortSignal) => Promise<ToolResult>;
|
|
14
|
+
|
|
15
|
+
interface ToolDefinition {
|
|
16
|
+
name: string;
|
|
17
|
+
description: string;
|
|
18
|
+
inputSchema: Record<string, unknown>;
|
|
19
|
+
handler: ToolHandler;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const inflightToolCalls = new Map<RequestId, AbortController>();
|
|
23
|
+
|
|
24
|
+
function asString(value: unknown, name: string): string {
|
|
25
|
+
if (typeof value !== "string" || value.trim() === "") {
|
|
26
|
+
throw new Error(`${name} must be a non-empty string`);
|
|
27
|
+
}
|
|
28
|
+
return value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function asBoolean(value: unknown, defaultValue: boolean): boolean {
|
|
32
|
+
return typeof value === "boolean" ? value : defaultValue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function asOptionalPositiveInteger(value: unknown, name: string): number | undefined {
|
|
36
|
+
if (value === undefined) return undefined;
|
|
37
|
+
return validateAskTimeoutMs(value, name);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function asAttachmentArray(value: unknown): Attachment[] | undefined {
|
|
41
|
+
if (value === undefined) return undefined;
|
|
42
|
+
if (!Array.isArray(value)) throw new Error("attachments must be an array");
|
|
43
|
+
return value.map((item, index) => {
|
|
44
|
+
if (!item || typeof item !== "object" || Array.isArray(item)) throw new Error(`attachments[${index}] must be an object`);
|
|
45
|
+
const raw = item as Record<string, unknown>;
|
|
46
|
+
const type = raw.type;
|
|
47
|
+
if (type !== "file" && type !== "snippet" && type !== "context") throw new Error(`attachments[${index}].type must be file, snippet, or context`);
|
|
48
|
+
const name = asString(raw.name, `attachments[${index}].name`);
|
|
49
|
+
const content = asString(raw.content, `attachments[${index}].content`);
|
|
50
|
+
if (raw.language !== undefined && typeof raw.language !== "string") throw new Error(`attachments[${index}].language must be a string`);
|
|
51
|
+
const language = typeof raw.language === "string" ? raw.language : undefined;
|
|
52
|
+
return { type, name, content, ...(language ? { language } : {}) };
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
const attachmentsSchema = {
|
|
57
|
+
type: "array",
|
|
58
|
+
items: {
|
|
59
|
+
type: "object",
|
|
60
|
+
properties: {
|
|
61
|
+
type: { type: "string", enum: ["file", "snippet", "context"] },
|
|
62
|
+
name: { type: "string" },
|
|
63
|
+
content: { type: "string" },
|
|
64
|
+
language: { type: "string" },
|
|
65
|
+
},
|
|
66
|
+
required: ["type", "name", "content"],
|
|
67
|
+
additionalProperties: false,
|
|
68
|
+
},
|
|
69
|
+
};
|
|
70
|
+
|
|
71
|
+
export function buildToolDefinitions(runtime: CodexIntercomRuntime): ToolDefinition[] {
|
|
72
|
+
return [
|
|
73
|
+
{
|
|
74
|
+
name: "intercom_whoami",
|
|
75
|
+
description: "Return this Codex session's intercom identity for reliable targeting.",
|
|
76
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
77
|
+
handler: async () => runtime.whoami(),
|
|
78
|
+
},
|
|
79
|
+
{
|
|
80
|
+
name: "intercom_team",
|
|
81
|
+
description: "Show your current manager and the live coworkers owned by that manager. No arguments are required.",
|
|
82
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
83
|
+
handler: async () => runtime.team(),
|
|
84
|
+
},
|
|
85
|
+
{
|
|
86
|
+
name: "intercom_status",
|
|
87
|
+
description: "Show intercom connection status, active sessions, unread messages, and pending asks.",
|
|
88
|
+
inputSchema: { type: "object", properties: {}, additionalProperties: false },
|
|
89
|
+
handler: async () => runtime.status(),
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
name: "intercom_list",
|
|
93
|
+
description: "List intercom-connected Pi or Codex sessions on this machine.",
|
|
94
|
+
inputSchema: {
|
|
95
|
+
type: "object",
|
|
96
|
+
properties: {
|
|
97
|
+
scope: { type: "string", enum: ["machine", "directory", "repo"], default: "machine" },
|
|
98
|
+
include_self: { type: "boolean", default: false },
|
|
99
|
+
},
|
|
100
|
+
additionalProperties: false,
|
|
101
|
+
},
|
|
102
|
+
handler: async (args) => runtime.list(
|
|
103
|
+
args.scope === "directory" || args.scope === "repo" ? args.scope : "machine",
|
|
104
|
+
asBoolean(args.include_self, false),
|
|
105
|
+
),
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
name: "intercom_set_summary",
|
|
109
|
+
description: "Publish a short status summary so other sessions can discover what this Codex session is doing.",
|
|
110
|
+
inputSchema: {
|
|
111
|
+
type: "object",
|
|
112
|
+
properties: { summary: { type: "string", maxLength: 400 } },
|
|
113
|
+
required: ["summary"],
|
|
114
|
+
additionalProperties: false,
|
|
115
|
+
},
|
|
116
|
+
handler: async (args) => runtime.setSummary(asString(args.summary, "summary")),
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
name: "intercom_send",
|
|
120
|
+
description: "Send a non-blocking direct message to another intercom session by name, full ID, or unique ID prefix.",
|
|
121
|
+
inputSchema: {
|
|
122
|
+
type: "object",
|
|
123
|
+
properties: {
|
|
124
|
+
to: { type: "string" },
|
|
125
|
+
message: { type: "string" },
|
|
126
|
+
attachments: attachmentsSchema,
|
|
127
|
+
},
|
|
128
|
+
required: ["to", "message"],
|
|
129
|
+
additionalProperties: false,
|
|
130
|
+
},
|
|
131
|
+
handler: async (args) => runtime.send(asString(args.to, "to"), asString(args.message, "message"), asAttachmentArray(args.attachments)),
|
|
132
|
+
},
|
|
133
|
+
{
|
|
134
|
+
name: "intercom_ask",
|
|
135
|
+
description: "Ask another intercom session a question only when the next step depends on its reply. Use intercom_send for assignments, progress/status checkpoints, and notifications.",
|
|
136
|
+
inputSchema: {
|
|
137
|
+
type: "object",
|
|
138
|
+
properties: {
|
|
139
|
+
to: { type: "string" },
|
|
140
|
+
message: { type: "string" },
|
|
141
|
+
attachments: attachmentsSchema,
|
|
142
|
+
timeout_ms: { type: "integer", minimum: 1, maximum: 120000, description: "Maximum time to wait for a reply before returning an error. Use intercom_send plus intercom_pending for longer work." },
|
|
143
|
+
},
|
|
144
|
+
required: ["to", "message"],
|
|
145
|
+
additionalProperties: false,
|
|
146
|
+
},
|
|
147
|
+
handler: async (args, signal) => runtime.ask(
|
|
148
|
+
asString(args.to, "to"),
|
|
149
|
+
asString(args.message, "message"),
|
|
150
|
+
asAttachmentArray(args.attachments),
|
|
151
|
+
asOptionalPositiveInteger(args.timeout_ms, "timeout_ms"),
|
|
152
|
+
signal,
|
|
153
|
+
),
|
|
154
|
+
},
|
|
155
|
+
{
|
|
156
|
+
name: "intercom_pending",
|
|
157
|
+
description: "Read unread inbound messages and unresolved asks for this Codex session.",
|
|
158
|
+
inputSchema: {
|
|
159
|
+
type: "object",
|
|
160
|
+
properties: { mark_read: { type: "boolean", default: false } },
|
|
161
|
+
additionalProperties: false,
|
|
162
|
+
},
|
|
163
|
+
handler: async (args) => runtime.pending(asBoolean(args.mark_read, false)),
|
|
164
|
+
},
|
|
165
|
+
{
|
|
166
|
+
name: "intercom_reply",
|
|
167
|
+
description: "Reply to a pending inbound ask. Use to plus which=oldest/latest when one sender has multiple unresolved asks.",
|
|
168
|
+
inputSchema: {
|
|
169
|
+
type: "object",
|
|
170
|
+
properties: {
|
|
171
|
+
message: { type: "string" },
|
|
172
|
+
to: { type: "string", description: "Optional sender/session selector; never a message or thread ID." },
|
|
173
|
+
which: { type: "string", enum: ["oldest", "latest"], description: "Select the oldest or latest ask from the chosen sender." },
|
|
174
|
+
},
|
|
175
|
+
required: ["message"],
|
|
176
|
+
additionalProperties: false,
|
|
177
|
+
},
|
|
178
|
+
handler: async (args) => runtime.reply(asString(args.message, "message"), typeof args.to === "string" ? args.to : undefined, args.which === "oldest" || args.which === "latest" ? args.which : undefined),
|
|
179
|
+
},
|
|
180
|
+
];
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
function ok(id: JsonRpcRequest["id"], result: unknown) {
|
|
184
|
+
return { jsonrpc: "2.0", id, result };
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function error(id: JsonRpcRequest["id"], code: number, message: string) {
|
|
188
|
+
return { jsonrpc: "2.0", id: id ?? null, error: { code, message } };
|
|
189
|
+
}
|
|
190
|
+
|
|
191
|
+
export async function handleMcpRequest(request: JsonRpcRequest, runtime: CodexIntercomRuntime): Promise<Record<string, unknown> | undefined> {
|
|
192
|
+
if (!request.method) {
|
|
193
|
+
return error(request.id, -32600, "Invalid request");
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (request.method === "notifications/cancelled") {
|
|
197
|
+
const requestId = request.params?.requestId;
|
|
198
|
+
if (typeof requestId === "string" || typeof requestId === "number") {
|
|
199
|
+
inflightToolCalls.get(requestId)?.abort();
|
|
200
|
+
}
|
|
201
|
+
return undefined;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (request.id === undefined && request.method.startsWith("notifications/")) {
|
|
205
|
+
return undefined;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const tools = buildToolDefinitions(runtime);
|
|
209
|
+
switch (request.method) {
|
|
210
|
+
case "initialize":
|
|
211
|
+
return ok(request.id, {
|
|
212
|
+
protocolVersion: "2025-06-18",
|
|
213
|
+
capabilities: { tools: {} },
|
|
214
|
+
serverInfo: { name: "codex-intercom", version: "0.1.0" },
|
|
215
|
+
});
|
|
216
|
+
case "ping":
|
|
217
|
+
return ok(request.id, {});
|
|
218
|
+
case "tools/list":
|
|
219
|
+
return ok(request.id, {
|
|
220
|
+
tools: tools.map(({ name, description, inputSchema }) => ({ name, description, inputSchema })),
|
|
221
|
+
});
|
|
222
|
+
case "tools/call": {
|
|
223
|
+
const name = request.params?.name;
|
|
224
|
+
const args = request.params?.arguments;
|
|
225
|
+
if (typeof name !== "string") return error(request.id, -32602, "tools/call requires params.name");
|
|
226
|
+
if (args !== undefined && (!args || typeof args !== "object" || Array.isArray(args))) {
|
|
227
|
+
return error(request.id, -32602, "tools/call params.arguments must be an object");
|
|
228
|
+
}
|
|
229
|
+
const tool = tools.find((candidate) => candidate.name === name);
|
|
230
|
+
if (!tool) return error(request.id, -32602, `Unknown tool: ${name}`);
|
|
231
|
+
const requestId = request.id;
|
|
232
|
+
const abortController = typeof requestId === "string" || typeof requestId === "number"
|
|
233
|
+
? new AbortController()
|
|
234
|
+
: null;
|
|
235
|
+
if (abortController && requestId !== undefined) inflightToolCalls.set(requestId, abortController);
|
|
236
|
+
try {
|
|
237
|
+
return ok(request.id, await tool.handler((args ?? {}) as Record<string, unknown>, abortController?.signal));
|
|
238
|
+
} catch (caught) {
|
|
239
|
+
const message = caught instanceof Error ? caught.message : String(caught);
|
|
240
|
+
return ok(request.id, { content: [{ type: "text", text: message }], isError: true });
|
|
241
|
+
} finally {
|
|
242
|
+
if (abortController && requestId !== undefined) inflightToolCalls.delete(requestId);
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
default:
|
|
246
|
+
return error(request.id, -32601, `Method not found: ${request.method}`);
|
|
247
|
+
}
|
|
248
|
+
}
|