@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 +11 -0
- package/README.md +38 -10
- package/claude/cci.ts +1 -1
- package/claude/mcp-protocol.ts +17 -0
- package/claude/named-teams.ts +178 -0
- package/claude/runtime.ts +60 -8
- package/commands/intercom-create.md +10 -0
- package/commands/intercom-join.md +10 -0
- package/dist/cci.mjs +1 -1
- package/dist/ccim.mjs +1 -1
- package/dist/claude-server.mjs +207 -2
- package/package.json +7 -3
- package/scripts/verify-packed-runtime.mjs +3 -1
- package/skills/claude-intercom/SKILL.md +2 -1
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
|
|
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.
|
|
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.
|
|
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.
|
|
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
|
|
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
|
|
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
|
-
--
|
|
201
|
+
-- agent-intercom-grok-mcp
|
|
174
202
|
```
|
|
175
203
|
|
|
176
|
-
For AGY, add the equivalent entry to `~/.gemini/config/mcp_config.json
|
|
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": "
|
|
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
|
|
package/claude/mcp-protocol.ts
CHANGED
|
@@ -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
|
|
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
|
-
//
|
|
228
|
-
//
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
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) {
|
package/dist/claude-server.mjs
CHANGED
|
@@ -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
|
-
|
|
2309
|
-
|
|
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.
|
|
4
|
-
"description": "Claude Code MCP plugin and wakeable worker for
|
|
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
|
-
|
|
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`.
|