@ctliz/agent-intercom-claude 0.13.0-connect.7 → 0.13.0-connect.9

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/CHANGELOG.md CHANGED
@@ -1,5 +1,16 @@
1
1
  # Changelog
2
2
 
3
+ ## Unreleased
4
+
5
+ ## 0.13.0-connect.9 - 2026-09-06
6
+
7
+ - Add `intercom_join` so Claude sessions can create or join a named team without tmux. `/intercom-create` and `/intercom-join` call the same tool. Named teams share `~/.pi/agent/intercom/named-teams.json` with Pi, Codex, and OpenCode.
8
+
9
+ ## 0.13.0-connect.8 - 2026-08-24
10
+
11
+ - Document Grok Build and AGY support in detail on the npm package page, including packages, tools, identity, shared scope, Auto-Team participation, and polling-only delivery.
12
+ - Add Grok Build and AGY to npm search metadata and clarify that their dedicated launchers load the Claude MCP runtime without a separate `claude-intercom-mcp` installation.
13
+
3
14
  ## 0.13.0-connect.7 - 2026-08-24
4
15
 
5
16
  - Synchronize companion install pins with the hosted-CI-validated Orchestrator release.
package/README.md CHANGED
@@ -13,7 +13,35 @@
13
13
  | AGY | [`agent-intercom-agy`](https://github.com/ctliz/agent-intercom-agy) |
14
14
  | Fleet lifecycle | [`agent-intercom-orchestrator`](https://github.com/ctliz/agent-intercom-orchestrator) |
15
15
 
16
- Grok Build and AGY use lightweight npm-packaged MCP launchers backed by this Claude MCP runtime. They retain inbound messages for `intercom_pending` polling but do not provide wake-on-message.
16
+ ## Grok Build and AGY support
17
+
18
+ Grok Build and AGY are supported as first-class protocol peers through two dedicated npm packages:
19
+
20
+ | Host | npm package | Installed MCP launcher |
21
+ |---|---|---|
22
+ | Grok Build | [`@ctliz/agent-intercom-grok`](https://www.npmjs.com/package/@ctliz/agent-intercom-grok) | `agent-intercom-grok-mcp` |
23
+ | AGY | [`@ctliz/agent-intercom-agy`](https://www.npmjs.com/package/@ctliz/agent-intercom-agy) | `agent-intercom-agy-mcp` |
24
+
25
+ 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`.
26
+
27
+ Install the host adapter before installing its plugin:
28
+
29
+ ```bash
30
+ npm install -g @ctliz/agent-intercom-grok
31
+ npm install -g @ctliz/agent-intercom-agy
32
+ ```
33
+
34
+ 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:
35
+
36
+ ```text
37
+ AGENT_INTERCOM_SESSION_ID=<unique-pane-or-worker-id>
38
+ AGENT_INTERCOM_SESSION_NAME=<human-readable-name>
39
+ AGENT_INTERCOM_SCOPE_ID=<shared-team-or-workspace-scope>
40
+ ```
41
+
42
+ 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.
43
+
44
+ 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.
17
45
 
18
46
  ## Maintenance & Upstream Provenance
19
47
 
@@ -101,13 +129,13 @@ Install via npm using the `connect` dist-tag:
101
129
  ```bash
102
130
  npm install -g @ctliz/agent-intercom-claude@connect
103
131
  # or by exact prerelease version
104
- npm install -g @ctliz/agent-intercom-claude@0.13.0-connect.7
132
+ npm install -g @ctliz/agent-intercom-claude@0.13.0-connect.9
105
133
  ```
106
134
 
107
135
  Or install from GitHub source at the exact tag so the command-line entry points are on `PATH`:
108
136
 
109
137
  ```bash
110
- git clone --depth 1 --branch v0.13.0-connect.7 https://github.com/ctliz/agent-intercom-claude.git
138
+ git clone --depth 1 --branch v0.13.0-connect.9 https://github.com/ctliz/agent-intercom-claude.git
111
139
  cd agent-intercom-claude && npm ci && npm link
112
140
  ```
113
141
 
@@ -121,7 +149,7 @@ This provides:
121
149
  To let a Pi manager create Claude workers with owned systemd cgroups, leases, model/effort selection, logs, and verified cleanup, install the companion Pi packages:
122
150
 
123
151
  ```bash
124
- pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.7
152
+ pi install git:github.com/ctliz/agent-intercom-pi@v0.12.0-connect.9
125
153
  pi install git:github.com/ctliz/agent-intercom-orchestrator@v0.12.0-connect.5
126
154
  ```
127
155
 
@@ -155,9 +183,9 @@ With `--transport mcp`, `cci` does this automatically for each normal headless w
155
183
 
156
184
  ### Other stdio MCP hosts
157
185
 
158
- The `claude-intercom-mcp` executable is a standard stdio MCP server. Grok Build and AGY can use the dedicated `@ctliz/agent-intercom-grok` and `@ctliz/agent-intercom-agy` packages, which install host-specific launchers backed by this runtime. For a source checkout, configure `node` with the absolute path to `dist/claude-server.mjs`. Node 22.19+ is required.
186
+ The `claude-intercom-mcp` executable is a standard stdio MCP server, but Grok Build and AGY should normally use the dedicated `@ctliz/agent-intercom-grok` and `@ctliz/agent-intercom-agy` packages. Those packages install `agent-intercom-grok-mcp` and `agent-intercom-agy-mcp`, depend on this package, and load its runtime internally. A separate global `claude-intercom-mcp` installation is not required. For a source-only checkout, configure `node` with the absolute path to `dist/claude-server.mjs`. Node 22.19+ is required.
159
187
 
160
- Install the host packages with `npm install -g @ctliz/agent-intercom-grok` or `npm install -g @ctliz/agent-intercom-agy`, then install their GitHub plugin at the matching release tag.
188
+ Install the matching host package globally, then install its GitHub plugin at the matching release tag.
161
189
 
162
190
  Use `AGENT_INTERCOM_SESSION_ID` and `AGENT_INTERCOM_SESSION_NAME` for a host-neutral identity; `CLAUDE_INTERCOM_SESSION_ID` and `CLAUDE_INTERCOM_NAME` remain higher-priority compatibility aliases. Do not share a fixed session ID between concurrent host sessions.
163
191
 
@@ -170,17 +198,16 @@ grok mcp add agent-intercom \
170
198
  -e AGENT_INTERCOM_SESSION_ID=grok-probe-01 \
171
199
  -e AGENT_INTERCOM_SESSION_NAME=grok-probe \
172
200
  -e CLAUDE_INTERCOM_MODEL=grok-build \
173
- -- claude-intercom-mcp
201
+ -- agent-intercom-grok-mcp
174
202
  ```
175
203
 
176
- For AGY, add the equivalent entry to `~/.gemini/config/mcp_config.json` (replace the source-checkout paths when using an installed `claude-intercom-mcp` executable):
204
+ For AGY, add the equivalent entry to `~/.gemini/config/mcp_config.json`:
177
205
 
178
206
  ```json
179
207
  {
180
208
  "mcpServers": {
181
209
  "agent-intercom": {
182
- "command": "/absolute/path/to/node",
183
- "args": ["/absolute/path/to/agent-intercom-claude/dist/claude-server.mjs"],
210
+ "command": "agent-intercom-agy-mcp",
184
211
  "env": {
185
212
  "AGENT_INTERCOM_SESSION_ID": "agy-probe-01",
186
213
  "AGENT_INTERCOM_SESSION_NAME": "agy-probe",
@@ -240,6 +267,7 @@ tools without the skill.
240
267
 
241
268
  - `intercom_whoami`: show this session's intercom ID, name, cwd, and model.
242
269
  - `intercom_team`: show the current manager and live coworkers owned by that manager.
270
+ - `intercom_join`: list, join, or create a named team without tmux (`create: true` joins as manager).
243
271
  - `intercom_status`: show connection status and pending message counts.
244
272
  - `intercom_list`: list local Pi, Codex, and Claude sessions in your scope (protocol v4 is same-scope; cross-scope contact requires an exact full session ID).
245
273
  - `intercom_set_summary`: publish a short discoverable status.
package/claude/cci.ts CHANGED
@@ -283,7 +283,7 @@ export function buildTuiAppendSystemPrompt(
283
283
  "When such an event arrives, treat it as a request from that peer:",
284
284
  "- If it is marked \"[asking — awaiting your reply]\", the sender is BLOCKING on your answer. Do the work if appropriate, then answer with the intercom_reply tool: intercom_reply({ message: \"...\" }).",
285
285
  "- Otherwise, act if needed and use intercom_send to respond or acknowledge.",
286
- "Use intercom_team to find your manager and managed coworkers. You also have intercom_list, intercom_whoami, intercom_status, intercom_pending, and intercom_set_summary. Keep intercom replies concise.",
286
+ "Use intercom_team to find your manager and managed coworkers. Use intercom_join to create or join a named team without tmux. You also have intercom_list, intercom_whoami, intercom_status, intercom_pending, and intercom_set_summary. Keep intercom replies concise.",
287
287
  ].join("\n");
288
288
  }
289
289
 
@@ -94,6 +94,23 @@ export function buildToolDefinitions(runtime: ClaudeIntercomRuntime): ToolDefini
94
94
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
95
95
  handler: async () => runtime.team(),
96
96
  },
97
+ {
98
+ name: "intercom_join",
99
+ 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.",
100
+ inputSchema: {
101
+ type: "object",
102
+ properties: {
103
+ name: { type: "string", description: "Team name to join. Omit to list joinable teams." },
104
+ create: { type: "boolean", description: "Create this named team and join as manager. Requires name." },
105
+ },
106
+ additionalProperties: false,
107
+ },
108
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
109
+ handler: async (args) => runtime.join(
110
+ typeof args.name === "string" ? args.name : undefined,
111
+ args.create === true,
112
+ ),
113
+ },
97
114
  {
98
115
  name: "intercom_status",
99
116
  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/claude/runtime.ts CHANGED
@@ -9,6 +9,16 @@ import { getAskTimeoutMs, loadConfig } from "../config.ts";
9
9
  import { appendInboxMessage } from "./inbox.ts";
10
10
  import type { Attachment, Message, SessionInfo } from "../types.ts";
11
11
  import { formatIntercomTeam, resolveIntercomTeam } from "./team.ts";
12
+ import {
13
+ createNamedTeam,
14
+ findNamedTeam,
15
+ formatCreateSuccess,
16
+ formatJoinableNamedTeamList,
17
+ formatNamedJoinSuccess,
18
+ listNamedTeams,
19
+ parseTeamName,
20
+ rejectManagedJoin,
21
+ } from "./named-teams.ts";
12
22
 
13
23
  export interface ClaudeRuntimeIdentity {
14
24
  sessionId: string;
@@ -215,7 +225,7 @@ export class ClaudeIntercomRuntime {
215
225
  private replyWaiters = new Map<string, ReplyWaiter>();
216
226
 
217
227
  private readonly clientFactory: () => IntercomClient;
218
- private readonly capturedScopeId: string | undefined;
228
+ private capturedScopeId: string | undefined;
219
229
  private readonly prepareConnection: () => Promise<void>;
220
230
  private readonly reconnectDelays: number[];
221
231
 
@@ -224,13 +234,11 @@ export class ClaudeIntercomRuntime {
224
234
  // Capture AGENT_INTERCOM_SCOPE_ID exactly once at runtime construction so that
225
235
  // reconnects reuse the same private scope even if process.env is mutated later.
226
236
  this.capturedScopeId = intercomScopeIdFromEnvForRegistration(process.env);
227
- // Freeze the exact env value the client should see for scope resolution.
228
- // When capturedScopeId is undefined (unscoped), pass an empty env so the client
229
- // does not silently re-read a later-mutated AGENT_INTERCOM_SCOPE_ID.
230
- const scopeSnapshot: NodeJS.ProcessEnv = this.capturedScopeId
231
- ? { AGENT_INTERCOM_SCOPE_ID: this.capturedScopeId }
232
- : {};
233
- this.clientFactory = options.clientFactory ?? (() => new IntercomClient({ env: scopeSnapshot }));
237
+ // Reconnects reuse capturedScopeId even if process.env is mutated later.
238
+ // An explicit join/create updates capturedScopeId, then rebuilds the client.
239
+ this.clientFactory = options.clientFactory ?? (() => new IntercomClient({
240
+ env: this.capturedScopeId ? { AGENT_INTERCOM_SCOPE_ID: this.capturedScopeId } : {},
241
+ }));
234
242
  this.prepareConnection = options.prepareConnection ?? (async () => {
235
243
  const config = loadConfig();
236
244
  if (!config.enabled) throw new Error("Intercom disabled");
@@ -326,6 +334,50 @@ export class ClaudeIntercomRuntime {
326
334
  if (client) await client.disconnect();
327
335
  }
328
336
 
337
+ async switchRuntimeScope(nextScopeId: string, managerSessionId?: string): Promise<void> {
338
+ if (managerSessionId) process.env.AGENT_INTERCOM_MANAGER_TARGET = managerSessionId;
339
+ else delete process.env.AGENT_INTERCOM_MANAGER_TARGET;
340
+ if (this.capturedScopeId === nextScopeId && process.env.AGENT_INTERCOM_SCOPE_ID === nextScopeId) {
341
+ return;
342
+ }
343
+ this.reconnectEnabled = false;
344
+ this.clearReconnectTimer();
345
+ if (this.connectPromise) {
346
+ try { await this.connectPromise; } catch { /* ignore in-flight connect */ }
347
+ }
348
+ const previous = this.client;
349
+ this.client = null;
350
+ if (previous) await previous.disconnect().catch(() => undefined);
351
+ this.capturedScopeId = nextScopeId;
352
+ process.env.AGENT_INTERCOM_SCOPE_ID = nextScopeId;
353
+ this.reconnectEnabled = true;
354
+ await this.connect();
355
+ }
356
+
357
+ async join(name?: string, create = false): Promise<ToolResult> {
358
+ const blocked = rejectManagedJoin();
359
+ if (blocked) return textResult(blocked, { ok: false }, true);
360
+ try {
361
+ if (create) {
362
+ if (typeof name !== "string" || !name.trim()) {
363
+ return textResult("Creating a team requires a name.", { ok: false }, true);
364
+ }
365
+ const team = createNamedTeam({ name: parseTeamName(name), managerSessionId: this.identity.sessionId });
366
+ await this.switchRuntimeScope(team.scopeId, team.managerSessionId);
367
+ return textResult(formatCreateSuccess({ team: team.name, name: this.identity.name }), { ok: true, team: team.name, role: "manager" });
368
+ }
369
+ if (!name?.trim()) {
370
+ return textResult(formatJoinableNamedTeamList(listNamedTeams()));
371
+ }
372
+ const team = findNamedTeam(parseTeamName(name));
373
+ if (!team) return textResult("Could not join that team.", { ok: false }, true);
374
+ await this.switchRuntimeScope(team.scopeId, team.managerSessionId);
375
+ return textResult(formatNamedJoinSuccess({ team: team.name, name: this.identity.name }), { ok: true, team: team.name, role: "teammate" });
376
+ } catch (error) {
377
+ return textResult(error instanceof Error ? error.message : String(error), { ok: false }, true);
378
+ }
379
+ }
380
+
329
381
  private handleIncomingMessage(from: SessionInfo, message: Message): void {
330
382
  const waiter = this.replyWaiters.get(message.replyTo ?? "");
331
383
  if (waiter) {
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Create a named intercom team and join it as manager
3
+ argument-hint: [team-name]
4
+ ---
5
+
6
+ Create a named Intercom team that does not require tmux.
7
+
8
+ If `$ARGUMENTS` is a valid team name, call `intercom_join` with `create: true` and that name. If `$ARGUMENTS` is empty, ask the user for a name first. Do not invent a team name.
9
+
10
+ Example: `intercom_join({ name: "billing", create: true })`
@@ -0,0 +1,10 @@
1
+ ---
2
+ description: Join a named intercom team, or list joinable teams
3
+ argument-hint: [team-name]
4
+ ---
5
+
6
+ Join a named Intercom team without tmux.
7
+
8
+ If `$ARGUMENTS` is empty, call `intercom_join` with no arguments to list joinable named teams. If `$ARGUMENTS` is a team name, call `intercom_join` with that name. Do not guess a team name.
9
+
10
+ Example: `intercom_join({ name: "billing" })`
package/dist/cci.mjs CHANGED
@@ -3403,7 +3403,7 @@ function buildTuiAppendSystemPrompt(name, id, transport = "mcp") {
3403
3403
  "When such an event arrives, treat it as a request from that peer:",
3404
3404
  '- If it is marked "[asking \u2014 awaiting your reply]", the sender is BLOCKING on your answer. Do the work if appropriate, then answer with the intercom_reply tool: intercom_reply({ message: "..." }).',
3405
3405
  "- Otherwise, act if needed and use intercom_send to respond or acknowledge.",
3406
- "Use intercom_team to find your manager and managed coworkers. You also have intercom_list, intercom_whoami, intercom_status, intercom_pending, and intercom_set_summary. Keep intercom replies concise."
3406
+ "Use intercom_team to find your manager and managed coworkers. Use intercom_join to create or join a named team without tmux. You also have intercom_list, intercom_whoami, intercom_status, intercom_pending, and intercom_set_summary. Keep intercom replies concise."
3407
3407
  ].join("\n");
3408
3408
  }
3409
3409
  function nativeClaudeFeatureEnv(transport) {
package/dist/ccim.mjs CHANGED
@@ -3407,7 +3407,7 @@ function buildTuiAppendSystemPrompt(name, id, transport = "mcp") {
3407
3407
  "When such an event arrives, treat it as a request from that peer:",
3408
3408
  '- If it is marked "[asking \u2014 awaiting your reply]", the sender is BLOCKING on your answer. Do the work if appropriate, then answer with the intercom_reply tool: intercom_reply({ message: "..." }).',
3409
3409
  "- Otherwise, act if needed and use intercom_send to respond or acknowledge.",
3410
- "Use intercom_team to find your manager and managed coworkers. You also have intercom_list, intercom_whoami, intercom_status, intercom_pending, and intercom_set_summary. Keep intercom replies concise."
3410
+ "Use intercom_team to find your manager and managed coworkers. Use intercom_join to create or join a named team without tmux. You also have intercom_list, intercom_whoami, intercom_status, intercom_pending, and intercom_set_summary. Keep intercom replies concise."
3411
3411
  ].join("\n");
3412
3412
  }
3413
3413
  function nativeClaudeFeatureEnv(transport) {
@@ -2145,6 +2145,148 @@ function formatIntercomTeam(team) {
2145
2145
  return lines.join("\n");
2146
2146
  }
2147
2147
 
2148
+ // claude/named-teams.ts
2149
+ import { randomBytes } from "node:crypto";
2150
+ import { readFileSync as readFileSync6 } from "node:fs";
2151
+ import { join as join7 } from "node:path";
2152
+ var NAMED_TEAMS_FILE = "named-teams.json";
2153
+ var NAMED_TEAMS_VERSION = 1;
2154
+ var NAMED_TEAM_NAME_PATTERN = /^[A-Za-z][A-Za-z0-9_-]{0,31}$/;
2155
+ function isNamedTeamScope(value) {
2156
+ if (value.length !== 48) return false;
2157
+ for (const char of value) {
2158
+ if (!(char >= "0" && char <= "9" || char >= "a" && char <= "f")) return false;
2159
+ }
2160
+ return true;
2161
+ }
2162
+ function teamsFilePath(agentDir) {
2163
+ return join7(getIntercomDirPath(agentDir ?? getAgentDirPath()), NAMED_TEAMS_FILE);
2164
+ }
2165
+ function genericReadError() {
2166
+ return new Error("Could not read the local named-team list.");
2167
+ }
2168
+ function genericWriteError() {
2169
+ return new Error("Could not create that named team.");
2170
+ }
2171
+ function isPlainObject(value) {
2172
+ return typeof value === "object" && value !== null && !Array.isArray(value);
2173
+ }
2174
+ function parseStoredTeam(value) {
2175
+ if (!isPlainObject(value)) return void 0;
2176
+ if (typeof value.name !== "string" || !NAMED_TEAM_NAME_PATTERN.test(value.name)) return void 0;
2177
+ if (typeof value.scopeId !== "string" || !isNamedTeamScope(value.scopeId)) return void 0;
2178
+ if (typeof value.managerSessionId !== "string" || !value.managerSessionId.trim()) return void 0;
2179
+ if (value.managerSessionId !== value.managerSessionId.trim() || /[\u0000-\u001f\u007f]/.test(value.managerSessionId)) {
2180
+ return void 0;
2181
+ }
2182
+ if (typeof value.createdAt !== "number" || !Number.isSafeInteger(value.createdAt) || value.createdAt <= 0) {
2183
+ return void 0;
2184
+ }
2185
+ return {
2186
+ name: value.name,
2187
+ scopeId: value.scopeId,
2188
+ managerSessionId: value.managerSessionId,
2189
+ createdAt: value.createdAt
2190
+ };
2191
+ }
2192
+ function rejectManagedJoin(env = process.env) {
2193
+ if (env.AGENT_INTERCOM_WORKER_ID?.trim() || env.AGENT_INTERCOM_OWNED === "1" || env.AGENT_INTERCOM_TEAM_MANIFEST?.trim()) {
2194
+ return "This session is already a managed member and cannot join another team.";
2195
+ }
2196
+ return void 0;
2197
+ }
2198
+ function parseTeamName(raw) {
2199
+ const name = raw.trim();
2200
+ if (!name || name.includes(" ") || !NAMED_TEAM_NAME_PATTERN.test(name)) {
2201
+ throw new Error("Team names start with a letter and may include letters, numbers, hyphens, or underscores (max 32).");
2202
+ }
2203
+ return name;
2204
+ }
2205
+ function generateNamedTeamScope(existing = []) {
2206
+ const taken = new Set(existing);
2207
+ for (let attempt = 0; attempt < 8; attempt += 1) {
2208
+ const scopeId = randomBytes(24).toString("hex");
2209
+ if (!taken.has(scopeId)) return scopeId;
2210
+ }
2211
+ throw genericWriteError();
2212
+ }
2213
+ function listNamedTeams(agentDir) {
2214
+ let raw;
2215
+ try {
2216
+ raw = readFileSync6(teamsFilePath(agentDir), "utf8");
2217
+ } catch (error2) {
2218
+ if (error2 && typeof error2 === "object" && "code" in error2 && error2.code === "ENOENT") return [];
2219
+ throw genericReadError();
2220
+ }
2221
+ let parsed;
2222
+ try {
2223
+ parsed = JSON.parse(raw);
2224
+ } catch {
2225
+ throw genericReadError();
2226
+ }
2227
+ if (!isPlainObject(parsed) || parsed.version !== NAMED_TEAMS_VERSION || !Array.isArray(parsed.teams)) {
2228
+ throw genericReadError();
2229
+ }
2230
+ const teams = [];
2231
+ const names = /* @__PURE__ */ new Set();
2232
+ const scopes = /* @__PURE__ */ new Set();
2233
+ for (const entry of parsed.teams) {
2234
+ const team = parseStoredTeam(entry);
2235
+ if (!team || names.has(team.name) || scopes.has(team.scopeId)) throw genericReadError();
2236
+ names.add(team.name);
2237
+ scopes.add(team.scopeId);
2238
+ teams.push(team);
2239
+ }
2240
+ return teams;
2241
+ }
2242
+ function findNamedTeam(name, agentDir) {
2243
+ return listNamedTeams(agentDir).find((team) => team.name === name);
2244
+ }
2245
+ function createNamedTeam(input) {
2246
+ const name = parseTeamName(input.name);
2247
+ const managerSessionId = input.managerSessionId.trim();
2248
+ if (!managerSessionId || managerSessionId !== input.managerSessionId || /[\u0000-\u001f\u007f]/.test(managerSessionId)) {
2249
+ throw genericWriteError();
2250
+ }
2251
+ const existing = listNamedTeams(input.agentDir);
2252
+ if (existing.some((team2) => team2.name === name)) {
2253
+ throw new Error(`A named team called ${name} already exists.`);
2254
+ }
2255
+ const scopeId = (input.generateScope ?? (() => generateNamedTeamScope(existing.map((team2) => team2.scopeId))))();
2256
+ if (!isNamedTeamScope(scopeId) || existing.some((team2) => team2.scopeId === scopeId)) {
2257
+ throw genericWriteError();
2258
+ }
2259
+ const team = {
2260
+ name,
2261
+ scopeId,
2262
+ managerSessionId,
2263
+ createdAt: input.now ?? Date.now()
2264
+ };
2265
+ const dir = getIntercomDirPath(input.agentDir ?? getAgentDirPath());
2266
+ ensureIntercomRuntimeDir(dir);
2267
+ const payload = { version: NAMED_TEAMS_VERSION, teams: [...existing, team] };
2268
+ writeDurableJson(teamsFilePath(input.agentDir), payload);
2269
+ return team;
2270
+ }
2271
+ function formatJoinableNamedTeamList(teams) {
2272
+ if (teams.length === 0) {
2273
+ return 'No joinable named teams found.\nCreate one with intercom_join({ name: "billing", create: true }).';
2274
+ }
2275
+ return [
2276
+ "Joinable named teams:",
2277
+ ...teams.map((team, index) => ` ${index + 1}) ${team.name}`)
2278
+ ].join("\n");
2279
+ }
2280
+ function formatCreateSuccess(input) {
2281
+ return `Created team ${input.team} and joined as manager.
2282
+ Display name: ${input.name}`;
2283
+ }
2284
+ function formatNamedJoinSuccess(input) {
2285
+ return `Joined team ${input.team}.
2286
+ Role: teammate
2287
+ Display name: ${input.name}`;
2288
+ }
2289
+
2148
2290
  // claude/runtime.ts
2149
2291
  function matchesPendingSender(entry, to) {
2150
2292
  return entry.from.id === to || entry.from.name?.toLowerCase() === to.toLowerCase() || entry.from.id.startsWith(to);
@@ -2305,8 +2447,9 @@ var ClaudeIntercomRuntime = class {
2305
2447
  constructor(identity = buildClaudeRuntimeIdentity(), options = {}) {
2306
2448
  this.identity = identity;
2307
2449
  this.capturedScopeId = intercomScopeIdFromEnvForRegistration(process.env);
2308
- const scopeSnapshot = this.capturedScopeId ? { AGENT_INTERCOM_SCOPE_ID: this.capturedScopeId } : {};
2309
- this.clientFactory = options.clientFactory ?? (() => new IntercomClient({ env: scopeSnapshot }));
2450
+ this.clientFactory = options.clientFactory ?? (() => new IntercomClient({
2451
+ env: this.capturedScopeId ? { AGENT_INTERCOM_SCOPE_ID: this.capturedScopeId } : {}
2452
+ }));
2310
2453
  this.prepareConnection = options.prepareConnection ?? (async () => {
2311
2454
  const config = loadConfig();
2312
2455
  if (!config.enabled) throw new Error("Intercom disabled");
@@ -2394,6 +2537,51 @@ var ClaudeIntercomRuntime = class {
2394
2537
  this.client = null;
2395
2538
  if (client) await client.disconnect();
2396
2539
  }
2540
+ async switchRuntimeScope(nextScopeId, managerSessionId) {
2541
+ if (managerSessionId) process.env.AGENT_INTERCOM_MANAGER_TARGET = managerSessionId;
2542
+ else delete process.env.AGENT_INTERCOM_MANAGER_TARGET;
2543
+ if (this.capturedScopeId === nextScopeId && process.env.AGENT_INTERCOM_SCOPE_ID === nextScopeId) {
2544
+ return;
2545
+ }
2546
+ this.reconnectEnabled = false;
2547
+ this.clearReconnectTimer();
2548
+ if (this.connectPromise) {
2549
+ try {
2550
+ await this.connectPromise;
2551
+ } catch {
2552
+ }
2553
+ }
2554
+ const previous = this.client;
2555
+ this.client = null;
2556
+ if (previous) await previous.disconnect().catch(() => void 0);
2557
+ this.capturedScopeId = nextScopeId;
2558
+ process.env.AGENT_INTERCOM_SCOPE_ID = nextScopeId;
2559
+ this.reconnectEnabled = true;
2560
+ await this.connect();
2561
+ }
2562
+ async join(name, create = false) {
2563
+ const blocked = rejectManagedJoin();
2564
+ if (blocked) return textResult(blocked, { ok: false }, true);
2565
+ try {
2566
+ if (create) {
2567
+ if (typeof name !== "string" || !name.trim()) {
2568
+ return textResult("Creating a team requires a name.", { ok: false }, true);
2569
+ }
2570
+ const team2 = createNamedTeam({ name: parseTeamName(name), managerSessionId: this.identity.sessionId });
2571
+ await this.switchRuntimeScope(team2.scopeId, team2.managerSessionId);
2572
+ return textResult(formatCreateSuccess({ team: team2.name, name: this.identity.name }), { ok: true, team: team2.name, role: "manager" });
2573
+ }
2574
+ if (!name?.trim()) {
2575
+ return textResult(formatJoinableNamedTeamList(listNamedTeams()));
2576
+ }
2577
+ const team = findNamedTeam(parseTeamName(name));
2578
+ if (!team) return textResult("Could not join that team.", { ok: false }, true);
2579
+ await this.switchRuntimeScope(team.scopeId, team.managerSessionId);
2580
+ return textResult(formatNamedJoinSuccess({ team: team.name, name: this.identity.name }), { ok: true, team: team.name, role: "teammate" });
2581
+ } catch (error2) {
2582
+ return textResult(error2 instanceof Error ? error2.message : String(error2), { ok: false }, true);
2583
+ }
2584
+ }
2397
2585
  handleIncomingMessage(from, message) {
2398
2586
  const waiter = this.replyWaiters.get(message.replyTo ?? "");
2399
2587
  if (waiter) {
@@ -2641,6 +2829,23 @@ function buildToolDefinitions(runtime2) {
2641
2829
  annotations: { readOnlyHint: true, destructiveHint: false, idempotentHint: true, openWorldHint: true },
2642
2830
  handler: async () => runtime2.team()
2643
2831
  },
2832
+ {
2833
+ name: "intercom_join",
2834
+ 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.",
2835
+ inputSchema: {
2836
+ type: "object",
2837
+ properties: {
2838
+ name: { type: "string", description: "Team name to join. Omit to list joinable teams." },
2839
+ create: { type: "boolean", description: "Create this named team and join as manager. Requires name." }
2840
+ },
2841
+ additionalProperties: false
2842
+ },
2843
+ annotations: { readOnlyHint: false, destructiveHint: false, idempotentHint: false, openWorldHint: true },
2844
+ handler: async (args) => runtime2.join(
2845
+ typeof args.name === "string" ? args.name : void 0,
2846
+ args.create === true
2847
+ )
2848
+ },
2644
2849
  {
2645
2850
  name: "intercom_status",
2646
2851
  description: "Show intercom connection status, active sessions, unread messages, and pending asks.",
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@ctliz/agent-intercom-claude",
3
- "version": "0.13.0-connect.7",
4
- "description": "Claude Code MCP plugin and wakeable worker for direct local messaging with Pi, Codex, and Claude coding-agent sessions.",
3
+ "version": "0.13.0-connect.9",
4
+ "description": "Claude Code MCP plugin and wakeable worker for local messaging with Pi, Codex, OpenCode, Grok Build, and AGY agents.",
5
5
  "license": "AGPL-3.0-or-later",
6
6
  "type": "module",
7
7
  "main": "dist/claude-server.mjs",
@@ -64,7 +64,11 @@
64
64
  "intercom",
65
65
  "multi-agent",
66
66
  "pi",
67
- "codex"
67
+ "codex",
68
+ "opencode",
69
+ "grok",
70
+ "grok-build",
71
+ "agy"
68
72
  ],
69
73
  "peerDependencies": {
70
74
  "@ctliz/agent-intercom-core": "0.2.0"
@@ -120,7 +120,9 @@ try {
120
120
  if (typeof plugin.commands !== "string") throw new Error("plugin.json missing commands declaration");
121
121
  const commandEntry1 = entries.get("package/commands/intercom.md");
122
122
  const commandEntry2 = entries.get("package/commands/intercom-id.md");
123
- if (!commandEntry1 || !commandEntry2) throw new Error("Packed adapter missing commands markdown files");
123
+ const commandEntry3 = entries.get("package/commands/intercom-create.md");
124
+ const commandEntry4 = entries.get("package/commands/intercom-join.md");
125
+ if (!commandEntry1 || !commandEntry2 || !commandEntry3 || !commandEntry4) throw new Error("Packed adapter missing commands markdown files");
124
126
 
125
127
  // 5. Assert all dist bundles exist and adhere to Core externalization
126
128
  for (const bundle of bundles) {
@@ -30,6 +30,7 @@ proxy-backed Claude profiles; `ccim` safe mode intentionally does not.
30
30
 
31
31
  - `intercom_whoami`: show this session's intercom ID, name, cwd, and model.
32
32
  - `intercom_team`: show the current orchestrator manager and live same-manager coworkers.
33
+ - `intercom_join`: list, join, or create a named team without tmux (`create: true` joins as manager).
33
34
  - `intercom_status`: connection, active session count, unread messages, pending asks.
34
35
  - `intercom_list`: list connected Pi, Codex, and Claude sessions.
35
36
  - `intercom_set_summary`: publish a short discoverable status.
@@ -42,7 +43,7 @@ proxy-backed Claude profiles; `ccim` safe mode intentionally does not.
42
43
 
43
44
  1. Call `intercom_status` or `intercom_whoami` to confirm this session is connected.
44
45
  2. Call `intercom_set_summary` with a concise status so peers can discover your role.
45
- 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`.
46
+ 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`.
46
47
  4. Use `intercom_send` for non-blocking updates and handoffs.
47
48
  5. Use `intercom_ask` only when you need the answer before continuing. Assignments, progress/status checkpoints, notifications, and completion reports use `intercom_send`.
48
49
  6. Call `intercom_pending` before ending a coordination turn, then answer blocking asks with `intercom_reply`.