@ctliz/agent-intercom-codex 0.12.0-connect.6 → 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 CHANGED
@@ -18,7 +18,35 @@
18
18
  | AGY | [`agent-intercom-agy`](https://github.com/ctliz/agent-intercom-agy) |
19
19
  | Fleet lifecycle | [`agent-intercom-orchestrator`](https://github.com/ctliz/agent-intercom-orchestrator) |
20
20
 
21
- Grok Build and AGY use lightweight npm-packaged MCP launchers backed by the Claude MCP runtime. They retain inbound messages for `intercom_pending` polling but do not provide wake-on-message.
21
+ ## Grok Build and AGY support
22
+
23
+ Grok Build and AGY are supported as first-class protocol peers through two dedicated npm packages:
24
+
25
+ | Host | npm package | Installed MCP launcher |
26
+ |---|---|---|
27
+ | Grok Build | [`@ctliz/agent-intercom-grok`](https://www.npmjs.com/package/@ctliz/agent-intercom-grok) | `agent-intercom-grok-mcp` |
28
+ | AGY | [`@ctliz/agent-intercom-agy`](https://www.npmjs.com/package/@ctliz/agent-intercom-agy) | `agent-intercom-agy-mcp` |
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 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
+
32
+ Install the host adapter before installing its plugin:
33
+
34
+ ```bash
35
+ npm install -g @ctliz/agent-intercom-grok
36
+ npm install -g @ctliz/agent-intercom-agy
37
+ ```
38
+
39
+ For multi-pane or Auto-Team-style use, the supervisor must give every MCP child a unique literal identity and the same scope as its intended peers:
40
+
41
+ ```text
42
+ AGENT_INTERCOM_SESSION_ID=<unique-pane-or-worker-id>
43
+ AGENT_INTERCOM_SESSION_NAME=<human-readable-name>
44
+ AGENT_INTERCOM_SCOPE_ID=<shared-team-or-workspace-scope>
45
+ ```
46
+
47
+ The plugin manifests intentionally do not contain static session IDs, because sharing one ID across concurrent panes would create identity collisions. `CLAUDE_INTERCOM_SESSION_ID` and `CLAUDE_INTERCOM_NAME` remain higher-priority compatibility aliases.
48
+
49
+ Grok Build and AGY currently provide polling-only MCP integrations. Messages are durably retained, but there is no host-specific wake bridge to inject a new turn. Each active agent should call `intercom_pending` at startup and at natural work boundaries. They can join an existing team and exchange messages with every other adapter, but Agent Intercom Orchestrator does not currently spawn or lifecycle-manage Grok or AGY workers.
22
50
 
23
51
  ## Maintenance & Upstream Provenance
24
52
 
@@ -103,7 +131,7 @@ For normal use, install the package so the command-line entry points are on
103
131
  ```bash
104
132
  npm install -g @ctliz/agent-intercom-codex@connect
105
133
  # or by exact prerelease version
106
- npm install -g @ctliz/agent-intercom-codex@0.12.0-connect.6
134
+ npm install -g @ctliz/agent-intercom-codex@0.12.0-connect.8
107
135
  ```
108
136
 
109
137
  This provides:
@@ -135,7 +163,7 @@ stable names or IDs.
135
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:
136
164
 
137
165
  ```bash
138
- pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.7
166
+ pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.9
139
167
  pi install git:github.com/ctliz/agent-intercom-orchestrator@v0.12.0-connect.5
140
168
  ```
141
169
 
@@ -159,6 +187,7 @@ intercom tools.
159
187
 
160
188
  - `intercom_whoami`: show this session's intercom ID, name, cwd, and model.
161
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).
162
191
  - `intercom_status`: show connection status and pending message counts.
163
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).
164
193
  - `intercom_set_summary`: publish a short discoverable status.
@@ -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
- const initialScopeId = intercomScopeIdFromEnv();
223
- this.clientFactory = options.clientFactory ?? (() => new IntercomClient(initialScopeId ? { scopeId: initialScopeId } : {}));
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) {
@@ -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.6 target=bridge-daemon sourceSha256=2f84f8a3fd8f97cb77cd03bc8a036c0ec90bfe1c67d3750a707f8814dcaf6c21\n");
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.6 target=broker sourceSha256=2f84f8a3fd8f97cb77cd03bc8a036c0ec90bfe1c67d3750a707f8814dcaf6c21\n");
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";
@@ -1,8 +1,8 @@
1
1
  {
2
2
  "schemaVersion": 1,
3
3
  "package": "@ctliz/agent-intercom-codex",
4
- "version": "0.12.0-connect.6",
5
- "sourceSha256": "2f84f8a3fd8f97cb77cd03bc8a036c0ec90bfe1c67d3750a707f8814dcaf6c21",
4
+ "version": "0.12.0-connect.8",
5
+ "sourceSha256": "7f8bf59e220410dd1faaca8f35430c6c3685f504b0fb8f4387b29ac53a3baa86",
6
6
  "targets": [
7
7
  "codex-server",
8
8
  "broker",
@@ -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.6 target=codex-server sourceSha256=2f84f8a3fd8f97cb77cd03bc8a036c0ec90bfe1c67d3750a707f8814dcaf6c21\n");
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
- const initialScopeId = intercomScopeIdFromEnv();
2505
- this.clientFactory = options.clientFactory ?? (() => new IntercomClient(initialScopeId ? { scopeId: initialScopeId } : {}));
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.6 target=coi sourceSha256=2f84f8a3fd8f97cb77cd03bc8a036c0ec90bfe1c67d3750a707f8814dcaf6c21\n");
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,7 +1,7 @@
1
1
  {
2
2
  "name": "@ctliz/agent-intercom-codex",
3
- "version": "0.12.0-connect.6",
4
- "description": "Codex MCP plugin for direct local messaging with Pi and Codex coding-agent sessions.",
3
+ "version": "0.12.0-connect.8",
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",
7
7
  "engines": {
@@ -60,7 +60,12 @@
60
60
  "mcp",
61
61
  "intercom",
62
62
  "multi-agent",
63
- "pi"
63
+ "pi",
64
+ "claude",
65
+ "opencode",
66
+ "grok",
67
+ "grok-build",
68
+ "agy"
64
69
  ],
65
70
  "peerDependencies": {
66
71
  "@ctliz/agent-intercom-core": "0.2.0"
@@ -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