@ctliz/agent-intercom-codex 0.12.0-connect.7 → 0.12.0-connect.8
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/README.md +4 -3
- package/codex/mcp-protocol.ts +17 -0
- package/codex/named-teams.ts +178 -0
- package/codex/runtime.ts +59 -2
- package/dist/bridge-daemon.mjs +1 -1
- package/dist/broker.mjs +1 -1
- package/dist/build-info.json +2 -2
- package/dist/codex-server.mjs +210 -3
- package/dist/coi.mjs +1 -1
- package/package.json +1 -1
- package/skills/codex-intercom/SKILL.md +2 -1
package/README.md
CHANGED
|
@@ -27,7 +27,7 @@ Grok Build and AGY are supported as first-class protocol peers through two dedic
|
|
|
27
27
|
| Grok Build | [`@ctliz/agent-intercom-grok`](https://www.npmjs.com/package/@ctliz/agent-intercom-grok) | `agent-intercom-grok-mcp` |
|
|
28
28
|
| AGY | [`@ctliz/agent-intercom-agy`](https://www.npmjs.com/package/@ctliz/agent-intercom-agy) | `agent-intercom-agy-mcp` |
|
|
29
29
|
|
|
30
|
-
The host packages depend on `@ctliz/agent-intercom-claude` and load its MCP runtime internally. Users do **not** need to install or place `claude-intercom-mcp` on `PATH` separately. Once connected, Grok and AGY sessions share the same local broker and protocol as Pi, Codex, Claude Code, and OpenCode, and expose
|
|
30
|
+
The host packages depend on `@ctliz/agent-intercom-claude` and load its MCP runtime internally. Users do **not** need to install or place `claude-intercom-mcp` on `PATH` separately. Once connected, Grok and AGY sessions share the same local broker and protocol as Pi, Codex, Claude Code, and OpenCode, and expose the MCP operations: `intercom_whoami`, `intercom_list`, `intercom_send`, `intercom_ask`, `intercom_reply`, `intercom_pending`, `intercom_status`, `intercom_team`, `intercom_join`, and `intercom_set_summary`.
|
|
31
31
|
|
|
32
32
|
Install the host adapter before installing its plugin:
|
|
33
33
|
|
|
@@ -131,7 +131,7 @@ For normal use, install the package so the command-line entry points are on
|
|
|
131
131
|
```bash
|
|
132
132
|
npm install -g @ctliz/agent-intercom-codex@connect
|
|
133
133
|
# or by exact prerelease version
|
|
134
|
-
npm install -g @ctliz/agent-intercom-codex@0.12.0-connect.
|
|
134
|
+
npm install -g @ctliz/agent-intercom-codex@0.12.0-connect.8
|
|
135
135
|
```
|
|
136
136
|
|
|
137
137
|
This provides:
|
|
@@ -163,7 +163,7 @@ stable names or IDs.
|
|
|
163
163
|
To let a Pi manager create Codex workers with owned systemd cgroups, leases, model/effort selection, logs, and verified cleanup, install the companion Pi packages:
|
|
164
164
|
|
|
165
165
|
```bash
|
|
166
|
-
pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.
|
|
166
|
+
pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.9
|
|
167
167
|
pi install git:github.com/ctliz/agent-intercom-orchestrator@v0.12.0-connect.5
|
|
168
168
|
```
|
|
169
169
|
|
|
@@ -187,6 +187,7 @@ intercom tools.
|
|
|
187
187
|
|
|
188
188
|
- `intercom_whoami`: show this session's intercom ID, name, cwd, and model.
|
|
189
189
|
- `intercom_team`: show the current manager and live coworkers owned by that manager.
|
|
190
|
+
- `intercom_join`: list, join, or create a named team without tmux (`create: true` joins as manager).
|
|
190
191
|
- `intercom_status`: show connection status and pending message counts.
|
|
191
192
|
- `intercom_list`: list local Pi, Codex, Claude Code, and OpenCode sessions in your scope (protocol v4 is same-scope; cross-scope contact requires an exact full session ID).
|
|
192
193
|
- `intercom_set_summary`: publish a short discoverable status.
|
package/codex/mcp-protocol.ts
CHANGED
|
@@ -90,6 +90,23 @@ export function buildToolDefinitions(runtime: CodexIntercomRuntime): ToolDefinit
|
|
|
90
90
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
91
91
|
handler: async () => runtime.team(),
|
|
92
92
|
},
|
|
93
|
+
{
|
|
94
|
+
name: "intercom_join",
|
|
95
|
+
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.",
|
|
96
|
+
inputSchema: {
|
|
97
|
+
type: "object",
|
|
98
|
+
properties: {
|
|
99
|
+
name: { type: "string", description: "Team name to join. Omit to list joinable teams." },
|
|
100
|
+
create: { type: "boolean", description: "Create this named team and join as manager. Requires name." },
|
|
101
|
+
},
|
|
102
|
+
additionalProperties: false,
|
|
103
|
+
},
|
|
104
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
105
|
+
handler: async (args) => runtime.join(
|
|
106
|
+
typeof args.name === "string" ? args.name : undefined,
|
|
107
|
+
args.create === true,
|
|
108
|
+
),
|
|
109
|
+
},
|
|
93
110
|
{
|
|
94
111
|
name: "intercom_status",
|
|
95
112
|
description: "Show intercom connection status, active sessions, unread messages, and pending asks.",
|
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
import { randomBytes } from "node:crypto";
|
|
2
|
+
import { readFileSync } from "node:fs";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import { ensureIntercomRuntimeDir, getAgentDirPath, getIntercomDirPath } from "../broker/paths.ts";
|
|
5
|
+
import { writeDurableJson } from "../durable-json.ts";
|
|
6
|
+
|
|
7
|
+
export const NAMED_TEAMS_FILE = "named-teams.json";
|
|
8
|
+
export const NAMED_TEAMS_VERSION = 1;
|
|
9
|
+
export const NAMED_TEAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
10
|
+
|
|
11
|
+
export interface NamedTeam {
|
|
12
|
+
name: string;
|
|
13
|
+
scopeId: string;
|
|
14
|
+
managerSessionId: string;
|
|
15
|
+
createdAt: number;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
interface NamedTeamsFile {
|
|
19
|
+
version: number;
|
|
20
|
+
teams: NamedTeam[];
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function isNamedTeamScope(value: string): boolean {
|
|
24
|
+
if (value.length !== 48) return false;
|
|
25
|
+
for (const char of value) {
|
|
26
|
+
if (!((char >= "0" && char <= "9") || (char >= "a" && char <= "f"))) return false;
|
|
27
|
+
}
|
|
28
|
+
return true;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function teamsFilePath(agentDir?: string): string {
|
|
32
|
+
return join(getIntercomDirPath(agentDir ?? getAgentDirPath()), NAMED_TEAMS_FILE);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function genericReadError(): Error {
|
|
36
|
+
return new Error("Could not read the local named-team list.");
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
function genericWriteError(): Error {
|
|
40
|
+
return new Error("Could not create that named team.");
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
44
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function parseStoredTeam(value: unknown): NamedTeam | undefined {
|
|
48
|
+
if (!isPlainObject(value)) return undefined;
|
|
49
|
+
if (typeof value.name !== "string" || !NAMED_TEAM_NAME_PATTERN.test(value.name)) return undefined;
|
|
50
|
+
if (typeof value.scopeId !== "string" || !isNamedTeamScope(value.scopeId)) return undefined;
|
|
51
|
+
if (typeof value.managerSessionId !== "string" || !value.managerSessionId.trim()) return undefined;
|
|
52
|
+
if (value.managerSessionId !== value.managerSessionId.trim() || /[\u0000-\u001f\u007f]/.test(value.managerSessionId)) {
|
|
53
|
+
return undefined;
|
|
54
|
+
}
|
|
55
|
+
if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt <= 0) {
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
return {
|
|
59
|
+
name: value.name,
|
|
60
|
+
scopeId: value.scopeId,
|
|
61
|
+
managerSessionId: value.managerSessionId,
|
|
62
|
+
createdAt: value.createdAt,
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function rejectManagedJoin(env: NodeJS.ProcessEnv = process.env): string | undefined {
|
|
67
|
+
if (env.AGENT_INTERCOM_WORKER_ID?.trim() || env.AGENT_INTERCOM_OWNED === "1" || env.AGENT_INTERCOM_TEAM_MANIFEST?.trim()) {
|
|
68
|
+
return "This session is already a managed member and cannot join another team.";
|
|
69
|
+
}
|
|
70
|
+
return undefined;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function parseTeamName(raw: string): string {
|
|
74
|
+
const name = raw.trim();
|
|
75
|
+
if (!name || name.includes(" ") || !NAMED_TEAM_NAME_PATTERN.test(name)) {
|
|
76
|
+
throw new Error("Team names start with a letter and may include letters, numbers, hyphens, or underscores (max 32).");
|
|
77
|
+
}
|
|
78
|
+
return name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
export function generateNamedTeamScope(existing: Iterable<string> = []): string {
|
|
82
|
+
const taken = new Set(existing);
|
|
83
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
84
|
+
const scopeId = randomBytes(24).toString("hex");
|
|
85
|
+
if (!taken.has(scopeId)) return scopeId;
|
|
86
|
+
}
|
|
87
|
+
throw genericWriteError();
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
export function listNamedTeams(agentDir?: string): NamedTeam[] {
|
|
91
|
+
let raw: string;
|
|
92
|
+
try {
|
|
93
|
+
raw = readFileSync(teamsFilePath(agentDir), "utf8");
|
|
94
|
+
} catch (error) {
|
|
95
|
+
if (error && typeof error === "object" && "code" in error && error.code === "ENOENT") return [];
|
|
96
|
+
throw genericReadError();
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
let parsed: unknown;
|
|
100
|
+
try {
|
|
101
|
+
parsed = JSON.parse(raw);
|
|
102
|
+
} catch {
|
|
103
|
+
throw genericReadError();
|
|
104
|
+
}
|
|
105
|
+
if (!isPlainObject(parsed) || parsed.version !== NAMED_TEAMS_VERSION || !Array.isArray(parsed.teams)) {
|
|
106
|
+
throw genericReadError();
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
const teams: NamedTeam[] = [];
|
|
110
|
+
const names = new Set<string>();
|
|
111
|
+
const scopes = new Set<string>();
|
|
112
|
+
for (const entry of parsed.teams) {
|
|
113
|
+
const team = parseStoredTeam(entry);
|
|
114
|
+
if (!team || names.has(team.name) || scopes.has(team.scopeId)) throw genericReadError();
|
|
115
|
+
names.add(team.name);
|
|
116
|
+
scopes.add(team.scopeId);
|
|
117
|
+
teams.push(team);
|
|
118
|
+
}
|
|
119
|
+
return teams;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function findNamedTeam(name: string, agentDir?: string): NamedTeam | undefined {
|
|
123
|
+
return listNamedTeams(agentDir).find((team) => team.name === name);
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function createNamedTeam(input: {
|
|
127
|
+
name: string;
|
|
128
|
+
managerSessionId: string;
|
|
129
|
+
agentDir?: string;
|
|
130
|
+
now?: number;
|
|
131
|
+
generateScope?: () => string;
|
|
132
|
+
}): NamedTeam {
|
|
133
|
+
const name = parseTeamName(input.name);
|
|
134
|
+
const managerSessionId = input.managerSessionId.trim();
|
|
135
|
+
if (!managerSessionId || managerSessionId !== input.managerSessionId || /[\u0000-\u001f\u007f]/.test(managerSessionId)) {
|
|
136
|
+
throw genericWriteError();
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const existing = listNamedTeams(input.agentDir);
|
|
140
|
+
if (existing.some((team) => team.name === name)) {
|
|
141
|
+
throw new Error(`A named team called ${name} already exists.`);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
const scopeId = (input.generateScope ?? (() => generateNamedTeamScope(existing.map((team) => team.scopeId))))();
|
|
145
|
+
if (!isNamedTeamScope(scopeId) || existing.some((team) => team.scopeId === scopeId)) {
|
|
146
|
+
throw genericWriteError();
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const team: NamedTeam = {
|
|
150
|
+
name,
|
|
151
|
+
scopeId,
|
|
152
|
+
managerSessionId,
|
|
153
|
+
createdAt: input.now ?? Date.now(),
|
|
154
|
+
};
|
|
155
|
+
const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
|
|
156
|
+
ensureIntercomRuntimeDir(dir);
|
|
157
|
+
const payload: NamedTeamsFile = { version: NAMED_TEAMS_VERSION, teams: [...existing, team] };
|
|
158
|
+
writeDurableJson(teamsFilePath(input.agentDir), payload);
|
|
159
|
+
return team;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export function formatJoinableNamedTeamList(teams: Array<Pick<NamedTeam, "name">>): string {
|
|
163
|
+
if (teams.length === 0) {
|
|
164
|
+
return "No joinable named teams found.\nCreate one with intercom_join({ name: \"billing\", create: true }).";
|
|
165
|
+
}
|
|
166
|
+
return [
|
|
167
|
+
"Joinable named teams:",
|
|
168
|
+
...teams.map((team, index) => ` ${index + 1}) ${team.name}`),
|
|
169
|
+
].join("\n");
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export function formatCreateSuccess(input: { team: string; name: string }): string {
|
|
173
|
+
return `Created team ${input.team} and joined as manager.\nDisplay name: ${input.name}`;
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export function formatNamedJoinSuccess(input: { team: string; name: string }): string {
|
|
177
|
+
return `Joined team ${input.team}.\nRole: teammate\nDisplay name: ${input.name}`;
|
|
178
|
+
}
|
package/codex/runtime.ts
CHANGED
|
@@ -8,6 +8,16 @@ import { spawnBrokerIfNeeded } from "../broker/spawn.ts";
|
|
|
8
8
|
import { getAskTimeoutMs, loadConfig } from "../config.ts";
|
|
9
9
|
import type { Attachment, Message, SessionInfo } from "../types.ts";
|
|
10
10
|
import { formatIntercomTeam, resolveIntercomTeam } from "./team.ts";
|
|
11
|
+
import {
|
|
12
|
+
createNamedTeam,
|
|
13
|
+
findNamedTeam,
|
|
14
|
+
formatCreateSuccess,
|
|
15
|
+
formatJoinableNamedTeamList,
|
|
16
|
+
formatNamedJoinSuccess,
|
|
17
|
+
listNamedTeams,
|
|
18
|
+
parseTeamName,
|
|
19
|
+
rejectManagedJoin,
|
|
20
|
+
} from "./named-teams.ts";
|
|
11
21
|
|
|
12
22
|
export interface CodexRuntimeIdentity {
|
|
13
23
|
sessionId: string;
|
|
@@ -214,13 +224,16 @@ export class CodexIntercomRuntime {
|
|
|
214
224
|
private replyWaiters = new Map<string, ReplyWaiter>();
|
|
215
225
|
|
|
216
226
|
private readonly clientFactory: () => IntercomClient;
|
|
227
|
+
private capturedScopeId: string | undefined;
|
|
217
228
|
private readonly prepareConnection: () => Promise<void>;
|
|
218
229
|
private readonly reconnectDelays: number[];
|
|
219
230
|
|
|
220
231
|
constructor(identity: CodexRuntimeIdentity = buildCodexRuntimeIdentity(), options: CodexIntercomRuntimeOptions = {}) {
|
|
221
232
|
this.identity = identity;
|
|
222
|
-
|
|
223
|
-
this.clientFactory = options.clientFactory ?? (() => new IntercomClient(
|
|
233
|
+
this.capturedScopeId = intercomScopeIdFromEnv();
|
|
234
|
+
this.clientFactory = options.clientFactory ?? (() => new IntercomClient(
|
|
235
|
+
this.capturedScopeId ? { scopeId: this.capturedScopeId } : {},
|
|
236
|
+
));
|
|
224
237
|
this.prepareConnection = options.prepareConnection ?? (async () => {
|
|
225
238
|
const config = loadConfig();
|
|
226
239
|
if (!config.enabled) throw new Error("Intercom disabled");
|
|
@@ -316,6 +329,50 @@ export class CodexIntercomRuntime {
|
|
|
316
329
|
if (client) await client.disconnect();
|
|
317
330
|
}
|
|
318
331
|
|
|
332
|
+
async switchRuntimeScope(nextScopeId: string, managerSessionId?: string): Promise<void> {
|
|
333
|
+
if (managerSessionId) process.env.AGENT_INTERCOM_MANAGER_TARGET = managerSessionId;
|
|
334
|
+
else delete process.env.AGENT_INTERCOM_MANAGER_TARGET;
|
|
335
|
+
if (this.capturedScopeId === nextScopeId && process.env.AGENT_INTERCOM_SCOPE_ID === nextScopeId) {
|
|
336
|
+
return;
|
|
337
|
+
}
|
|
338
|
+
this.reconnectEnabled = false;
|
|
339
|
+
this.clearReconnectTimer();
|
|
340
|
+
if (this.connectPromise) {
|
|
341
|
+
try { await this.connectPromise; } catch { /* ignore in-flight connect */ }
|
|
342
|
+
}
|
|
343
|
+
const previous = this.client;
|
|
344
|
+
this.client = null;
|
|
345
|
+
if (previous) await previous.disconnect().catch(() => undefined);
|
|
346
|
+
this.capturedScopeId = nextScopeId;
|
|
347
|
+
process.env.AGENT_INTERCOM_SCOPE_ID = nextScopeId;
|
|
348
|
+
this.reconnectEnabled = true;
|
|
349
|
+
await this.connect();
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
async join(name?: string, create = false): Promise<ToolResult> {
|
|
353
|
+
const blocked = rejectManagedJoin();
|
|
354
|
+
if (blocked) return textResult(blocked, { ok: false }, true);
|
|
355
|
+
try {
|
|
356
|
+
if (create) {
|
|
357
|
+
if (typeof name !== "string" || !name.trim()) {
|
|
358
|
+
return textResult("Creating a team requires a name.", { ok: false }, true);
|
|
359
|
+
}
|
|
360
|
+
const team = createNamedTeam({ name: parseTeamName(name), managerSessionId: this.identity.sessionId });
|
|
361
|
+
await this.switchRuntimeScope(team.scopeId, team.managerSessionId);
|
|
362
|
+
return textResult(formatCreateSuccess({ team: team.name, name: this.identity.name }), { ok: true, team: team.name, role: "manager" });
|
|
363
|
+
}
|
|
364
|
+
if (!name?.trim()) {
|
|
365
|
+
return textResult(formatJoinableNamedTeamList(listNamedTeams()));
|
|
366
|
+
}
|
|
367
|
+
const team = findNamedTeam(parseTeamName(name));
|
|
368
|
+
if (!team) return textResult("Could not join that team.", { ok: false }, true);
|
|
369
|
+
await this.switchRuntimeScope(team.scopeId, team.managerSessionId);
|
|
370
|
+
return textResult(formatNamedJoinSuccess({ team: team.name, name: this.identity.name }), { ok: true, team: team.name, role: "teammate" });
|
|
371
|
+
} catch (error) {
|
|
372
|
+
return textResult(error instanceof Error ? error.message : String(error), { ok: false }, true);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
|
|
319
376
|
private handleIncomingMessage(from: SessionInfo, message: Message): void {
|
|
320
377
|
const waiter = this.replyWaiters.get(message.replyTo ?? "");
|
|
321
378
|
if (waiter) {
|
package/dist/bridge-daemon.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.12.0-connect.
|
|
2
|
+
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.12.0-connect.8 target=bridge-daemon sourceSha256=7f8bf59e220410dd1faaca8f35430c6c3685f504b0fb8f4387b29ac53a3baa86\n");
|
|
3
3
|
|
|
4
4
|
// codex/bridge-daemon.ts
|
|
5
5
|
import { once } from "node:events";
|
package/dist/broker.mjs
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.12.0-connect.
|
|
1
|
+
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.12.0-connect.8 target=broker sourceSha256=7f8bf59e220410dd1faaca8f35430c6c3685f504b0fb8f4387b29ac53a3baa86\n");
|
|
2
2
|
|
|
3
3
|
// broker/broker.ts
|
|
4
4
|
import net from "net";
|
package/dist/build-info.json
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
{
|
|
2
2
|
"schemaVersion": 1,
|
|
3
3
|
"package": "@ctliz/agent-intercom-codex",
|
|
4
|
-
"version": "0.12.0-connect.
|
|
5
|
-
"sourceSha256": "
|
|
4
|
+
"version": "0.12.0-connect.8",
|
|
5
|
+
"sourceSha256": "7f8bf59e220410dd1faaca8f35430c6c3685f504b0fb8f4387b29ac53a3baa86",
|
|
6
6
|
"targets": [
|
|
7
7
|
"codex-server",
|
|
8
8
|
"broker",
|
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.12.0-connect.
|
|
2
|
+
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.12.0-connect.8 target=codex-server sourceSha256=7f8bf59e220410dd1faaca8f35430c6c3685f504b0fb8f4387b29ac53a3baa86\n");
|
|
3
3
|
|
|
4
4
|
// codex/server.ts
|
|
5
5
|
import readline from "node:readline";
|
|
@@ -2343,6 +2343,148 @@ function formatIntercomTeam(team) {
|
|
|
2343
2343
|
return lines.join("\n");
|
|
2344
2344
|
}
|
|
2345
2345
|
|
|
2346
|
+
// codex/named-teams.ts
|
|
2347
|
+
import { randomBytes } from "node:crypto";
|
|
2348
|
+
import { readFileSync as readFileSync7 } from "node:fs";
|
|
2349
|
+
import { join as join7 } from "node:path";
|
|
2350
|
+
var NAMED_TEAMS_FILE = "named-teams.json";
|
|
2351
|
+
var NAMED_TEAMS_VERSION = 1;
|
|
2352
|
+
var NAMED_TEAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
|
|
2353
|
+
function isNamedTeamScope(value) {
|
|
2354
|
+
if (value.length !== 48) return false;
|
|
2355
|
+
for (const char of value) {
|
|
2356
|
+
if (!(char >= "0" && char <= "9" || char >= "a" && char <= "f")) return false;
|
|
2357
|
+
}
|
|
2358
|
+
return true;
|
|
2359
|
+
}
|
|
2360
|
+
function teamsFilePath(agentDir) {
|
|
2361
|
+
return join7(getIntercomDirPath(agentDir ?? getAgentDirPath()), NAMED_TEAMS_FILE);
|
|
2362
|
+
}
|
|
2363
|
+
function genericReadError() {
|
|
2364
|
+
return new Error("Could not read the local named-team list.");
|
|
2365
|
+
}
|
|
2366
|
+
function genericWriteError() {
|
|
2367
|
+
return new Error("Could not create that named team.");
|
|
2368
|
+
}
|
|
2369
|
+
function isPlainObject(value) {
|
|
2370
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
2371
|
+
}
|
|
2372
|
+
function parseStoredTeam(value) {
|
|
2373
|
+
if (!isPlainObject(value)) return void 0;
|
|
2374
|
+
if (typeof value.name !== "string" || !NAMED_TEAM_NAME_PATTERN.test(value.name)) return void 0;
|
|
2375
|
+
if (typeof value.scopeId !== "string" || !isNamedTeamScope(value.scopeId)) return void 0;
|
|
2376
|
+
if (typeof value.managerSessionId !== "string" || !value.managerSessionId.trim()) return void 0;
|
|
2377
|
+
if (value.managerSessionId !== value.managerSessionId.trim() || /[\u0000-\u001f\u007f]/.test(value.managerSessionId)) {
|
|
2378
|
+
return void 0;
|
|
2379
|
+
}
|
|
2380
|
+
if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt <= 0) {
|
|
2381
|
+
return void 0;
|
|
2382
|
+
}
|
|
2383
|
+
return {
|
|
2384
|
+
name: value.name,
|
|
2385
|
+
scopeId: value.scopeId,
|
|
2386
|
+
managerSessionId: value.managerSessionId,
|
|
2387
|
+
createdAt: value.createdAt
|
|
2388
|
+
};
|
|
2389
|
+
}
|
|
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
|
+
function parseTeamName(raw) {
|
|
2397
|
+
const name = raw.trim();
|
|
2398
|
+
if (!name || name.includes(" ") || !NAMED_TEAM_NAME_PATTERN.test(name)) {
|
|
2399
|
+
throw new Error("Team names start with a letter and may include letters, numbers, hyphens, or underscores (max 32).");
|
|
2400
|
+
}
|
|
2401
|
+
return name;
|
|
2402
|
+
}
|
|
2403
|
+
function generateNamedTeamScope(existing = []) {
|
|
2404
|
+
const taken = new Set(existing);
|
|
2405
|
+
for (let attempt = 0; attempt < 8; attempt += 1) {
|
|
2406
|
+
const scopeId = randomBytes(24).toString("hex");
|
|
2407
|
+
if (!taken.has(scopeId)) return scopeId;
|
|
2408
|
+
}
|
|
2409
|
+
throw genericWriteError();
|
|
2410
|
+
}
|
|
2411
|
+
function listNamedTeams(agentDir) {
|
|
2412
|
+
let raw;
|
|
2413
|
+
try {
|
|
2414
|
+
raw = readFileSync7(teamsFilePath(agentDir), "utf8");
|
|
2415
|
+
} catch (error2) {
|
|
2416
|
+
if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") return [];
|
|
2417
|
+
throw genericReadError();
|
|
2418
|
+
}
|
|
2419
|
+
let parsed;
|
|
2420
|
+
try {
|
|
2421
|
+
parsed = JSON.parse(raw);
|
|
2422
|
+
} catch {
|
|
2423
|
+
throw genericReadError();
|
|
2424
|
+
}
|
|
2425
|
+
if (!isPlainObject(parsed) || parsed.version !== NAMED_TEAMS_VERSION || !Array.isArray(parsed.teams)) {
|
|
2426
|
+
throw genericReadError();
|
|
2427
|
+
}
|
|
2428
|
+
const teams = [];
|
|
2429
|
+
const names = /* @__PURE__ */ new Set();
|
|
2430
|
+
const scopes = /* @__PURE__ */ new Set();
|
|
2431
|
+
for (const entry of parsed.teams) {
|
|
2432
|
+
const team = parseStoredTeam(entry);
|
|
2433
|
+
if (!team || names.has(team.name) || scopes.has(team.scopeId)) throw genericReadError();
|
|
2434
|
+
names.add(team.name);
|
|
2435
|
+
scopes.add(team.scopeId);
|
|
2436
|
+
teams.push(team);
|
|
2437
|
+
}
|
|
2438
|
+
return teams;
|
|
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
|
+
function formatJoinableNamedTeamList(teams) {
|
|
2470
|
+
if (teams.length === 0) {
|
|
2471
|
+
return 'No joinable named teams found.\nCreate one with intercom_join({ name: "billing", create: true }).';
|
|
2472
|
+
}
|
|
2473
|
+
return [
|
|
2474
|
+
"Joinable named teams:",
|
|
2475
|
+
...teams.map((team, index) => ` ${index + 1}) ${team.name}`)
|
|
2476
|
+
].join("\n");
|
|
2477
|
+
}
|
|
2478
|
+
function formatCreateSuccess(input) {
|
|
2479
|
+
return `Created team ${input.team} and joined as manager.
|
|
2480
|
+
Display name: ${input.name}`;
|
|
2481
|
+
}
|
|
2482
|
+
function formatNamedJoinSuccess(input) {
|
|
2483
|
+
return `Joined team ${input.team}.
|
|
2484
|
+
Role: teammate
|
|
2485
|
+
Display name: ${input.name}`;
|
|
2486
|
+
}
|
|
2487
|
+
|
|
2346
2488
|
// codex/runtime.ts
|
|
2347
2489
|
function matchesPendingSender(entry, to) {
|
|
2348
2490
|
return entry.from.id === to || entry.from.name?.toLowerCase() === to.toLowerCase() || entry.from.id.startsWith(to);
|
|
@@ -2497,12 +2639,15 @@ var CodexIntercomRuntime = class {
|
|
|
2497
2639
|
unresolvedAsks = /* @__PURE__ */ new Map();
|
|
2498
2640
|
replyWaiters = /* @__PURE__ */ new Map();
|
|
2499
2641
|
clientFactory;
|
|
2642
|
+
capturedScopeId;
|
|
2500
2643
|
prepareConnection;
|
|
2501
2644
|
reconnectDelays;
|
|
2502
2645
|
constructor(identity = buildCodexRuntimeIdentity(), options = {}) {
|
|
2503
2646
|
this.identity = identity;
|
|
2504
|
-
|
|
2505
|
-
this.clientFactory = options.clientFactory ?? (() => new IntercomClient(
|
|
2647
|
+
this.capturedScopeId = intercomScopeIdFromEnv();
|
|
2648
|
+
this.clientFactory = options.clientFactory ?? (() => new IntercomClient(
|
|
2649
|
+
this.capturedScopeId ? { scopeId: this.capturedScopeId } : {}
|
|
2650
|
+
));
|
|
2506
2651
|
this.prepareConnection = options.prepareConnection ?? (async () => {
|
|
2507
2652
|
const config = loadConfig();
|
|
2508
2653
|
if (!config.enabled) throw new Error("Intercom disabled");
|
|
@@ -2590,6 +2735,51 @@ var CodexIntercomRuntime = class {
|
|
|
2590
2735
|
this.client = null;
|
|
2591
2736
|
if (client) await client.disconnect();
|
|
2592
2737
|
}
|
|
2738
|
+
async switchRuntimeScope(nextScopeId, managerSessionId) {
|
|
2739
|
+
if (managerSessionId) process.env.AGENT_INTERCOM_MANAGER_TARGET = managerSessionId;
|
|
2740
|
+
else delete process.env.AGENT_INTERCOM_MANAGER_TARGET;
|
|
2741
|
+
if (this.capturedScopeId === nextScopeId && process.env.AGENT_INTERCOM_SCOPE_ID === nextScopeId) {
|
|
2742
|
+
return;
|
|
2743
|
+
}
|
|
2744
|
+
this.reconnectEnabled = false;
|
|
2745
|
+
this.clearReconnectTimer();
|
|
2746
|
+
if (this.connectPromise) {
|
|
2747
|
+
try {
|
|
2748
|
+
await this.connectPromise;
|
|
2749
|
+
} catch {
|
|
2750
|
+
}
|
|
2751
|
+
}
|
|
2752
|
+
const previous = this.client;
|
|
2753
|
+
this.client = null;
|
|
2754
|
+
if (previous) await previous.disconnect().catch(() => void 0);
|
|
2755
|
+
this.capturedScopeId = nextScopeId;
|
|
2756
|
+
process.env.AGENT_INTERCOM_SCOPE_ID = nextScopeId;
|
|
2757
|
+
this.reconnectEnabled = true;
|
|
2758
|
+
await this.connect();
|
|
2759
|
+
}
|
|
2760
|
+
async join(name, create = false) {
|
|
2761
|
+
const blocked = rejectManagedJoin();
|
|
2762
|
+
if (blocked) return textResult(blocked, { ok: false }, true);
|
|
2763
|
+
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
|
+
if (!name?.trim()) {
|
|
2773
|
+
return textResult(formatJoinableNamedTeamList(listNamedTeams()));
|
|
2774
|
+
}
|
|
2775
|
+
const team = findNamedTeam(parseTeamName(name));
|
|
2776
|
+
if (!team) return textResult("Could not join that team.", { ok: false }, true);
|
|
2777
|
+
await this.switchRuntimeScope(team.scopeId, team.managerSessionId);
|
|
2778
|
+
return textResult(formatNamedJoinSuccess({ team: team.name, name: this.identity.name }), { ok: true, team: team.name, role: "teammate" });
|
|
2779
|
+
} catch (error2) {
|
|
2780
|
+
return textResult(error2 instanceof Error ? error2.message : String(error2), { ok: false }, true);
|
|
2781
|
+
}
|
|
2782
|
+
}
|
|
2593
2783
|
handleIncomingMessage(from, message) {
|
|
2594
2784
|
const waiter = this.replyWaiters.get(message.replyTo ?? "");
|
|
2595
2785
|
if (waiter) {
|
|
@@ -2827,6 +3017,23 @@ function buildToolDefinitions(runtime2) {
|
|
|
2827
3017
|
annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
|
|
2828
3018
|
handler: async () => runtime2.team()
|
|
2829
3019
|
},
|
|
3020
|
+
{
|
|
3021
|
+
name: "intercom_join",
|
|
3022
|
+
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.",
|
|
3023
|
+
inputSchema: {
|
|
3024
|
+
type: "object",
|
|
3025
|
+
properties: {
|
|
3026
|
+
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." }
|
|
3028
|
+
},
|
|
3029
|
+
additionalProperties: false
|
|
3030
|
+
},
|
|
3031
|
+
annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
|
|
3032
|
+
handler: async (args) => runtime2.join(
|
|
3033
|
+
typeof args.name === "string" ? args.name : void 0,
|
|
3034
|
+
args.create === true
|
|
3035
|
+
)
|
|
3036
|
+
},
|
|
2830
3037
|
{
|
|
2831
3038
|
name: "intercom_status",
|
|
2832
3039
|
description: "Show intercom connection status, active sessions, unread messages, and pending asks.",
|
package/dist/coi.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.12.0-connect.
|
|
2
|
+
process.stderr.write("[agent-intercom-build] package=@ctliz/agent-intercom-codex version=0.12.0-connect.8 target=coi sourceSha256=7f8bf59e220410dd1faaca8f35430c6c3685f504b0fb8f4387b29ac53a3baa86\n");
|
|
3
3
|
|
|
4
4
|
// codex/coi.ts
|
|
5
5
|
import { once as once2 } from "node:events";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ctliz/agent-intercom-codex",
|
|
3
|
-
"version": "0.12.0-connect.
|
|
3
|
+
"version": "0.12.0-connect.8",
|
|
4
4
|
"description": "Codex MCP plugin and wakeable worker for local messaging with Pi, Claude Code, OpenCode, Grok Build, and AGY agents.",
|
|
5
5
|
"license": "AGPL-3.0-or-later",
|
|
6
6
|
"type": "module",
|
|
@@ -24,6 +24,7 @@ messages to those workers create or resume app-server threads and start turns.
|
|
|
24
24
|
|
|
25
25
|
- `intercom_whoami`: show this session's intercom ID, name, cwd, and model.
|
|
26
26
|
- `intercom_team`: show the current orchestrator manager and live same-manager coworkers.
|
|
27
|
+
- `intercom_join`: list, join, or create a named team without tmux (`create: true` joins as manager).
|
|
27
28
|
- `intercom_status`: check connection, active session count, unread messages,
|
|
28
29
|
and unresolved inbound asks.
|
|
29
30
|
- `intercom_list`: list connected Pi and Codex sessions.
|
|
@@ -39,7 +40,7 @@ messages to those workers create or resume app-server threads and start turns.
|
|
|
39
40
|
connected.
|
|
40
41
|
2. Call `intercom_set_summary` with a concise status when other sessions need
|
|
41
42
|
to discover your role.
|
|
42
|
-
3. If this is an orchestrator-owned coworker, call `intercom_team` to get the manager and sibling targets without searching globally. Otherwise use `intercom_list`.
|
|
43
|
+
3. If this is an orchestrator-owned coworker, call `intercom_team` to get the manager and sibling targets without searching globally. To form a team without tmux, call `intercom_join({ name: "billing", create: true })` in one session and `intercom_join({ name: "billing" })` in the others. Otherwise use `intercom_list`.
|
|
43
44
|
4. Use `intercom_send` for non-blocking updates and handoffs.
|
|
44
45
|
5. Use `intercom_ask` only when you need the answer before continuing. Assignments, progress/status checkpoints, notifications, and completion reports use `intercom_send`.
|
|
45
46
|
6. Call `intercom_pending` before ending a coordination turn, then answer
|