agent-comms 8.15.9 → 8.15.11
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/.claude-plugin/marketplace.json +1 -1
- package/.claude-plugin/plugin.json +1 -1
- package/README.md +1 -1
- package/dist/bridges/user/web/server.d.ts.map +1 -1
- package/dist/bridges/user/web/server.js +44 -24
- package/dist/bridges/user/web/server.js.map +1 -1
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts +5 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js +90 -0
- package/dist/bridges/user/web/test/web-assets.unit.test.js.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts +2 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.d.ts.map +1 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js +75 -0
- package/dist/bridges/user/web/test/web-server-unbuilt-frontend.integration.test.js.map +1 -0
- package/dist/bridges/user/web/web-assets.d.ts +28 -0
- package/dist/bridges/user/web/web-assets.d.ts.map +1 -0
- package/dist/bridges/user/web/web-assets.js +67 -0
- package/dist/bridges/user/web/web-assets.js.map +1 -0
- package/dist/test/pi-manifest-packed.integration.test.d.ts +2 -0
- package/dist/test/pi-manifest-packed.integration.test.d.ts.map +1 -0
- package/dist/test/pi-manifest-packed.integration.test.js +68 -0
- package/dist/test/pi-manifest-packed.integration.test.js.map +1 -0
- package/package.json +8 -2
- package/src/account-cli.ts +195 -0
- package/src/account-join-cli.ts +191 -0
- package/src/bridges/cc-peer/approval-commands.ts +82 -0
- package/src/bridges/cc-peer/bridge.ts +138 -0
- package/src/bridges/cc-peer/default-front.ts +58 -0
- package/src/bridges/cc-peer/front-controller.ts +160 -0
- package/src/bridges/cc-peer/front-relay.ts +191 -0
- package/src/bridges/cc-peer/front-runtime.ts +167 -0
- package/src/bridges/cc-peer/front.ts +59 -0
- package/src/bridges/cc-peer/reply-aliases.ts +90 -0
- package/src/bridges/cc-peer/run.ts +100 -0
- package/src/bridges/cc-peer/shared-peer.ts +28 -0
- package/src/bridges/claude-code/channel.ts +318 -0
- package/src/bridges/codex/tool.ts +132 -0
- package/src/bridges/mcp/server.ts +137 -0
- package/src/bridges/opencode/plugin.ts +130 -0
- package/src/bridges/pi/extension.ts +330 -0
- package/src/bridges/registry.ts +17 -0
- package/src/bridges/user/cli.ts +87 -0
- package/src/bridges/user/controller.ts +345 -0
- package/src/bridges/user/tui.ts +300 -0
- package/src/bridges/user/web/actions.ts +173 -0
- package/src/bridges/user/web/contract.ts +222 -0
- package/src/bridges/user/web/dist/bundle.js +49 -0
- package/src/bridges/user/web/dist/icons/icon-192x192.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-512x512.svg +9 -0
- package/src/bridges/user/web/dist/icons/icon-96x96.svg +9 -0
- package/src/bridges/user/web/dist/index.html +21 -0
- package/src/bridges/user/web/dist/manifest.json +26 -0
- package/src/bridges/user/web/dist/mesh-worker.js +64 -0
- package/src/bridges/user/web/dist/sw.js +1 -0
- package/src/bridges/user/web/event-publisher.ts +114 -0
- package/src/bridges/user/web/port-discovery.ts +49 -0
- package/src/bridges/user/web/reads.ts +35 -0
- package/src/bridges/user/web/router.ts +158 -0
- package/src/bridges/user/web/server.ts +782 -0
- package/src/bridges/user/web/vite.config.ts +71 -0
- package/src/bridges/user/web/web-access.ts +404 -0
- package/src/bridges/user/web/web-assets.ts +90 -0
- package/src/bridges/user/web/web-beacon.ts +69 -0
- package/src/bridges/user/web/web-console-static.ts +90 -0
- package/src/cli-main.ts +635 -0
- package/src/cli.ts +13 -0
- package/src/core/account-bundle.ts +264 -0
- package/src/core/account-join.ts +289 -0
- package/src/core/account-ledger-crypto.ts +154 -0
- package/src/core/account-ledger-store.ts +66 -0
- package/src/core/account-ledger.ts +385 -0
- package/src/core/agent-details.ts +216 -0
- package/src/core/agent-list-format.ts +97 -0
- package/src/core/agent-registry.ts +282 -0
- package/src/core/agent-table.ts +58 -0
- package/src/core/atomic-file.ts +46 -0
- package/src/core/bind-retry.ts +38 -0
- package/src/core/bridge-mesh.ts +176 -0
- package/src/core/bridge.ts +718 -0
- package/src/core/capability-ask.ts +162 -0
- package/src/core/capability-bubble-up.ts +237 -0
- package/src/core/comms-store.ts +130 -0
- package/src/core/connection-approval.ts +134 -0
- package/src/core/connection-code-tool.ts +92 -0
- package/src/core/connection-code.ts +296 -0
- package/src/core/coordinator-role.ts +224 -0
- package/src/core/data-directory.ts +32 -0
- package/src/core/data-frame-routing.ts +309 -0
- package/src/core/default-command.ts +12 -0
- package/src/core/delegation-policy.ts +41 -0
- package/src/core/delivery-engine.ts +649 -0
- package/src/core/device-membership-verification.ts +125 -0
- package/src/core/device-membership.ts +130 -0
- package/src/core/directory-admission.ts +155 -0
- package/src/core/discovery-mdns.ts +247 -0
- package/src/core/discovery-tailscale.ts +224 -0
- package/src/core/discovery.ts +303 -0
- package/src/core/display-name.ts +73 -0
- package/src/core/dm-grant-actions.ts +72 -0
- package/src/core/dm-send-delegation.ts +73 -0
- package/src/core/dm-token-verification.ts +76 -0
- package/src/core/election-sessions.ts +135 -0
- package/src/core/error-reporting.ts +85 -0
- package/src/core/first-contact.ts +242 -0
- package/src/core/gateway-trust-actions.ts +184 -0
- package/src/core/gateway-trust.ts +305 -0
- package/src/core/gossip-directory.ts +276 -0
- package/src/core/gossip-extensions.ts +123 -0
- package/src/core/group-proofs.ts +205 -0
- package/src/core/handshake.ts +354 -0
- package/src/core/hub-forwarding.ts +24 -0
- package/src/core/hub-link.ts +176 -0
- package/src/core/hub-session.ts +374 -0
- package/src/core/identity-store.ts +715 -0
- package/src/core/identity.ts +489 -0
- package/src/core/index.ts +65 -0
- package/src/core/issuer-grouping.ts +165 -0
- package/src/core/issuer-identity-file.ts +187 -0
- package/src/core/lan-web-actions.ts +34 -0
- package/src/core/lan-web-uis.ts +55 -0
- package/src/core/ledger-lock.ts +81 -0
- package/src/core/listener-registry.ts +133 -0
- package/src/core/loopback-address.ts +22 -0
- package/src/core/machine-identity.ts +77 -0
- package/src/core/machine-list-groups.ts +53 -0
- package/src/core/mcp-registry-retry.ts +69 -0
- package/src/core/mcp-registry-version.ts +18 -0
- package/src/core/membership-proof.ts +139 -0
- package/src/core/membership-proofs.ts +149 -0
- package/src/core/mesh-graph-trace-tool.ts +81 -0
- package/src/core/mesh-graph.ts +89 -0
- package/src/core/mesh-network-actions.ts +183 -0
- package/src/core/mesh-store-options.ts +23 -0
- package/src/core/mesh-store-shared.ts +134 -0
- package/src/core/mesh-store.ts +1114 -0
- package/src/core/name-claim.ts +145 -0
- package/src/core/naming-actions.ts +90 -0
- package/src/core/naming.ts +138 -0
- package/src/core/nanoid.ts +20 -0
- package/src/core/network-interfaces.ts +24 -0
- package/src/core/package-version.ts +34 -0
- package/src/core/peer-lifecycle.ts +377 -0
- package/src/core/peer-link-error.ts +41 -0
- package/src/core/peer-versions.ts +98 -0
- package/src/core/pending-connection.ts +53 -0
- package/src/core/petnames.ts +121 -0
- package/src/core/pgp-keyserver.ts +32 -0
- package/src/core/pid-lock-file.ts +122 -0
- package/src/core/push-manager.ts +76 -0
- package/src/core/random-id.ts +9 -0
- package/src/core/receiver-set.ts +42 -0
- package/src/core/relay-role.ts +182 -0
- package/src/core/relay-selection.ts +120 -0
- package/src/core/relay-server.ts +198 -0
- package/src/core/relay-uplink.ts +78 -0
- package/src/core/request-timeouts.ts +37 -0
- package/src/core/room-discovery.ts +48 -0
- package/src/core/room-lifecycle.ts +838 -0
- package/src/core/room-lookup.ts +41 -0
- package/src/core/room-messaging.ts +247 -0
- package/src/core/room-notice-log.ts +61 -0
- package/src/core/room-path.ts +84 -0
- package/src/core/room-protocol.ts +893 -0
- package/src/core/room-router.ts +180 -0
- package/src/core/room-token-verification.ts +85 -0
- package/src/core/room-wire-extensions.ts +60 -0
- package/src/core/self-name-claim.ts +67 -0
- package/src/core/send-outcome.ts +69 -0
- package/src/core/shutdown-signals.ts +57 -0
- package/src/core/stale-agent-checker.ts +108 -0
- package/src/core/store.ts +704 -0
- package/src/core/token-text.ts +40 -0
- package/src/core/tool.ts +853 -0
- package/src/core/transport-events.ts +73 -0
- package/src/core/transport-queries.ts +54 -0
- package/src/core/transport.ts +367 -0
- package/src/core/types.ts +550 -0
- package/src/core/user-identity.ts +290 -0
- package/src/core/vapid.ts +81 -0
- package/src/core/version-check.ts +147 -0
- package/src/core/version-report-actions.ts +79 -0
- package/src/core/web-beacon.ts +31 -0
- package/src/core/web-push.ts +322 -0
- package/src/core/wire-mesh-core-version.ts +25 -0
- package/src/core/wire-mesh-identity.ts +36 -0
- package/src/core/wire-mesh-transport-options.ts +82 -0
- package/src/core/wire-mesh-transport.ts +1007 -0
- package/src/core/wire-protocol.ts +153 -0
- package/src/core/ws-dial.ts +191 -0
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Comms — Claude Code channel bridge.
|
|
3
|
+
*
|
|
4
|
+
* MCP channel server that provides the "agent_comms" tool and pushes
|
|
5
|
+
* incoming messages into Claude's context via <channel> events and hooks.
|
|
6
|
+
* Uses TCP mesh for real-time delivery — no filesystem polling.
|
|
7
|
+
*
|
|
8
|
+
* Run via: npx agent-comms bridge claude-code
|
|
9
|
+
*
|
|
10
|
+
* Pending events are persisted to ~/.agents/bus/pending/claude-code--<slug>.jsonl
|
|
11
|
+
* so the asyncRewake hook scripts (hooks/drain.sh) can drain them out-of-process
|
|
12
|
+
* and wake idle Claude via exit code 2.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
import { execFileSync } from "node:child_process";
|
|
16
|
+
import * as fs from "node:fs";
|
|
17
|
+
import * as os from "node:os";
|
|
18
|
+
import * as path from "node:path";
|
|
19
|
+
|
|
20
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
21
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
22
|
+
|
|
23
|
+
import {
|
|
24
|
+
createBridgeMesh,
|
|
25
|
+
createMeshErrorReporter,
|
|
26
|
+
buildAction,
|
|
27
|
+
ensureRegistered,
|
|
28
|
+
extractStreamingBehavior,
|
|
29
|
+
formatDeliveryEvent,
|
|
30
|
+
installShutdownSignalHandlers,
|
|
31
|
+
isActionableEvent,
|
|
32
|
+
MCP_TOOL_PARAMS,
|
|
33
|
+
type BridgeRunOptions,
|
|
34
|
+
} from "../../core/index.js";
|
|
35
|
+
import {
|
|
36
|
+
releaseIdentityLock,
|
|
37
|
+
type IdentitySlot,
|
|
38
|
+
} from "../../core/identity-store.js";
|
|
39
|
+
import { wireDefaultCcPeerFront } from "../cc-peer/default-front.js";
|
|
40
|
+
import {
|
|
41
|
+
tryStartBridgeWebServer,
|
|
42
|
+
getWebUrlStatus,
|
|
43
|
+
} from "../user/web/server.js";
|
|
44
|
+
import { nanoid } from "../../core/nanoid.js";
|
|
45
|
+
|
|
46
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
47
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function isErrnoException(err: unknown): err is NodeJS.ErrnoException {
|
|
51
|
+
return typeof err === "object" && err !== null && "code" in err;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function cwdSlug(cwd: string): string {
|
|
55
|
+
return cwd.replace(/[^a-zA-Z0-9]/g, "_");
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Maximum number of process-tree ancestors to walk when hunting for the Claude Code CLI PID. */
|
|
59
|
+
const MAX_PID_WALK_HOPS = 10;
|
|
60
|
+
|
|
61
|
+
/** Length of the random suffix appended to an auto-generated agent display name. */
|
|
62
|
+
const AUTO_NAME_SUFFIX_LENGTH = 4;
|
|
63
|
+
|
|
64
|
+
function pendingDir(): string {
|
|
65
|
+
return path.join(os.homedir(), ".agents", "bus", "pending");
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Walk up the process tree to find the Claude Code CLI PID.
|
|
70
|
+
*
|
|
71
|
+
* The bridge runs ~3 hops below claude (tsx wrapper → tsx loader → bridge),
|
|
72
|
+
* so process.ppid alone isn't enough. We walk until we find an ancestor
|
|
73
|
+
* whose command basename is "claude".
|
|
74
|
+
*
|
|
75
|
+
* Two Claude Code instances in the same cwd would otherwise share one
|
|
76
|
+
* pending file and consume each other's messages. Keying by Claude PID
|
|
77
|
+
* isolates them. Returns undefined if no claude ancestor can be located
|
|
78
|
+
* within 10 hops — caller falls back to cwd-only path.
|
|
79
|
+
*/
|
|
80
|
+
function findClaudeCodePid(): number | undefined {
|
|
81
|
+
let pid = process.ppid;
|
|
82
|
+
for (let i = 0; i < MAX_PID_WALK_HOPS && pid > 1; i++) {
|
|
83
|
+
let out: string;
|
|
84
|
+
try {
|
|
85
|
+
out = execFileSync("ps", ["-o", "ppid=,comm=", "-p", String(pid)], {
|
|
86
|
+
encoding: "utf-8",
|
|
87
|
+
});
|
|
88
|
+
} catch {
|
|
89
|
+
return undefined;
|
|
90
|
+
}
|
|
91
|
+
const trimmed = out.trim();
|
|
92
|
+
const match = /^(\d+)\s+(.+)$/.exec(trimmed);
|
|
93
|
+
if (!match) return undefined;
|
|
94
|
+
const parentPid = Number.parseInt(match[1] ?? "", 10);
|
|
95
|
+
const comm = (match[2] ?? "").trim();
|
|
96
|
+
if (/(^|\/)claude$/.test(comm)) return pid;
|
|
97
|
+
if (!Number.isFinite(parentPid)) return undefined;
|
|
98
|
+
pid = parentPid;
|
|
99
|
+
}
|
|
100
|
+
return undefined;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function pendingFilePath(cwd: string, claudePid: number | undefined): string {
|
|
104
|
+
const slug = cwdSlug(cwd);
|
|
105
|
+
const name =
|
|
106
|
+
claudePid !== undefined
|
|
107
|
+
? `claude-code--${slug}--${String(claudePid)}.jsonl`
|
|
108
|
+
: `claude-code--${slug}.jsonl`;
|
|
109
|
+
return path.join(pendingDir(), name);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* Drop pending files in this cwd whose owning Claude Code PID is no longer
|
|
114
|
+
* alive. Prevents accumulation when sessions exit without graceful shutdown.
|
|
115
|
+
*/
|
|
116
|
+
function cleanupStalePendingFiles(cwd: string): void {
|
|
117
|
+
const dir = pendingDir();
|
|
118
|
+
const slug = cwdSlug(cwd);
|
|
119
|
+
const prefix = `claude-code--${slug}--`;
|
|
120
|
+
let entries: string[];
|
|
121
|
+
try {
|
|
122
|
+
entries = fs.readdirSync(dir);
|
|
123
|
+
} catch (err) {
|
|
124
|
+
if (isErrnoException(err) && err.code === "ENOENT") return;
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
for (const entry of entries) {
|
|
128
|
+
if (!entry.startsWith(prefix) || !entry.endsWith(".jsonl")) continue;
|
|
129
|
+
const pidStr = entry.slice(prefix.length, -".jsonl".length);
|
|
130
|
+
const pid = Number.parseInt(pidStr, 10);
|
|
131
|
+
if (!Number.isFinite(pid) || pid <= 1) continue;
|
|
132
|
+
try {
|
|
133
|
+
process.kill(pid, 0);
|
|
134
|
+
} catch (err) {
|
|
135
|
+
if (isErrnoException(err) && err.code === "ESRCH") {
|
|
136
|
+
try {
|
|
137
|
+
fs.unlinkSync(path.join(dir, entry));
|
|
138
|
+
} catch {
|
|
139
|
+
// best-effort cleanup, ignore failures
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
function appendPending(filePath: string, line: string): void {
|
|
147
|
+
fs.mkdirSync(path.dirname(filePath), { recursive: true });
|
|
148
|
+
fs.appendFileSync(filePath, line + "\n");
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function drainPending(filePath: string): string[] {
|
|
152
|
+
const drainPath = `${filePath}.draining-${String(process.pid)}-${String(Date.now())}`;
|
|
153
|
+
try {
|
|
154
|
+
fs.renameSync(filePath, drainPath);
|
|
155
|
+
} catch (err) {
|
|
156
|
+
if (isErrnoException(err) && err.code === "ENOENT") return [];
|
|
157
|
+
throw err;
|
|
158
|
+
}
|
|
159
|
+
const content = fs.readFileSync(drainPath, "utf-8");
|
|
160
|
+
fs.unlinkSync(drainPath);
|
|
161
|
+
return content
|
|
162
|
+
.split("\n")
|
|
163
|
+
.map((l) => l.trim())
|
|
164
|
+
.filter((l) => l.length > 0);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export async function run(options?: Readonly<BridgeRunOptions>): Promise<void> {
|
|
168
|
+
// Persistent identity for this slot: a stable device-id means the agent ID survives restarts, so peers can keep targeting us
|
|
169
|
+
const identitySlot: IdentitySlot = options?.slot ?? {
|
|
170
|
+
harness: "claude-code",
|
|
171
|
+
cwd: process.cwd(),
|
|
172
|
+
};
|
|
173
|
+
const { store, tool } = await createBridgeMesh(identitySlot, options?.mesh);
|
|
174
|
+
store.onError = createMeshErrorReporter();
|
|
175
|
+
store.onCoordinatorRoleChanged = wireDefaultCcPeerFront(store);
|
|
176
|
+
let agentId: string | undefined;
|
|
177
|
+
|
|
178
|
+
const claudeCodePid = findClaudeCodePid();
|
|
179
|
+
if (claudeCodePid === undefined) {
|
|
180
|
+
process.stderr.write(
|
|
181
|
+
"agent-comms bridge: could not locate Claude Code PID; using shared cwd pending file (concurrent sessions in the same cwd will share messages)\n",
|
|
182
|
+
);
|
|
183
|
+
}
|
|
184
|
+
cleanupStalePendingFiles(process.cwd());
|
|
185
|
+
const pendingFile = pendingFilePath(process.cwd(), claudeCodePid);
|
|
186
|
+
|
|
187
|
+
const mcp = new McpServer(
|
|
188
|
+
{ name: "agent-comms", version: "0.2.0" },
|
|
189
|
+
{
|
|
190
|
+
capabilities: {
|
|
191
|
+
experimental: { "claude/channel": {} },
|
|
192
|
+
},
|
|
193
|
+
},
|
|
194
|
+
);
|
|
195
|
+
|
|
196
|
+
// All events are written to the pending file so the out-of-process hook
|
|
197
|
+
// drain script can surface them to idle Claude via asyncRewake exit 2.
|
|
198
|
+
// Actionable events also get an eager channel push for mid-turn delivery
|
|
199
|
+
// when channels are enabled (--dangerously-load-development-channels).
|
|
200
|
+
store.onDelivery = async (_targetId: string, event) => {
|
|
201
|
+
const line = formatDeliveryEvent(event);
|
|
202
|
+
const hint = extractStreamingBehavior(event);
|
|
203
|
+
const shouldPush = isActionableEvent(event) && hint !== "info";
|
|
204
|
+
|
|
205
|
+
appendPending(pendingFile, line);
|
|
206
|
+
|
|
207
|
+
if (shouldPush) {
|
|
208
|
+
await mcp.server.notification({
|
|
209
|
+
method: "notifications/claude/channel",
|
|
210
|
+
params: {
|
|
211
|
+
content: line,
|
|
212
|
+
meta: { streamingBehavior: hint ?? "steer" },
|
|
213
|
+
},
|
|
214
|
+
});
|
|
215
|
+
}
|
|
216
|
+
};
|
|
217
|
+
|
|
218
|
+
// -----------------------------------------------------------------------
|
|
219
|
+
// Tool registration
|
|
220
|
+
// -----------------------------------------------------------------------
|
|
221
|
+
|
|
222
|
+
mcp.registerTool(
|
|
223
|
+
"agent_comms",
|
|
224
|
+
{
|
|
225
|
+
description: [
|
|
226
|
+
"Cross-harness agent communication mesh. Actions:",
|
|
227
|
+
"register, update, whoami, web_url, create_room, list_rooms, join_room, leave_room,",
|
|
228
|
+
"send, dm, list_agents, read_room, invite, decline_invite, kick, destroy_room.",
|
|
229
|
+
'Incoming messages appear as <channel source="agent-comms"> events or [comms] Pending prefix.',
|
|
230
|
+
"Use streamingBehavior on send/dm: steer (act now), followUp (act when idle), info (whenever, default).",
|
|
231
|
+
].join(" "),
|
|
232
|
+
inputSchema: MCP_TOOL_PARAMS,
|
|
233
|
+
},
|
|
234
|
+
async (rawParams: unknown) => {
|
|
235
|
+
const params = isRecord(rawParams) ? rawParams : {};
|
|
236
|
+
const actionParam = params.action;
|
|
237
|
+
if (agentId === undefined) {
|
|
238
|
+
const name =
|
|
239
|
+
actionParam === "register" && typeof params.name === "string"
|
|
240
|
+
? params.name
|
|
241
|
+
: `claude-code-${nanoid(AUTO_NAME_SUFFIX_LENGTH)}`;
|
|
242
|
+
const reg = await ensureRegistered({
|
|
243
|
+
cwd: process.cwd(),
|
|
244
|
+
store,
|
|
245
|
+
harness: "claude-code",
|
|
246
|
+
defaultName: name,
|
|
247
|
+
});
|
|
248
|
+
agentId = reg.agentId;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
const action = buildAction(params);
|
|
252
|
+
const result = await tool.handle(
|
|
253
|
+
{
|
|
254
|
+
agentId,
|
|
255
|
+
harness: "claude-code",
|
|
256
|
+
cwd: process.cwd(),
|
|
257
|
+
pid: process.pid,
|
|
258
|
+
},
|
|
259
|
+
action,
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
const pending = drainPending(pendingFile);
|
|
263
|
+
const prefix =
|
|
264
|
+
pending.length > 0
|
|
265
|
+
? `[comms] Pending:\n${pending.map((l) => ` 📬 ${l}`).join("\n")}\n\n`
|
|
266
|
+
: "";
|
|
267
|
+
|
|
268
|
+
return {
|
|
269
|
+
content: [{ type: "text", text: `${prefix}${result.content}` }],
|
|
270
|
+
isError: result.isError,
|
|
271
|
+
};
|
|
272
|
+
},
|
|
273
|
+
);
|
|
274
|
+
|
|
275
|
+
// -----------------------------------------------------------------------
|
|
276
|
+
// Startup
|
|
277
|
+
// -----------------------------------------------------------------------
|
|
278
|
+
|
|
279
|
+
await store.init();
|
|
280
|
+
const webHandle = await tryStartBridgeWebServer(store, "claude-code");
|
|
281
|
+
tool.getWebUrlStatus = () => getWebUrlStatus(webHandle);
|
|
282
|
+
await mcp.connect(new StdioServerTransport());
|
|
283
|
+
|
|
284
|
+
const reg = await ensureRegistered({
|
|
285
|
+
cwd: process.cwd(),
|
|
286
|
+
store,
|
|
287
|
+
harness: "claude-code",
|
|
288
|
+
defaultName: `claude-code-${nanoid(AUTO_NAME_SUFFIX_LENGTH)}`,
|
|
289
|
+
});
|
|
290
|
+
agentId = reg.agentId;
|
|
291
|
+
|
|
292
|
+
// -----------------------------------------------------------------------
|
|
293
|
+
// Shutdown — clean up mesh state when Claude Code exits
|
|
294
|
+
// -----------------------------------------------------------------------
|
|
295
|
+
|
|
296
|
+
async function shutdown(): Promise<void> {
|
|
297
|
+
try {
|
|
298
|
+
if (agentId !== undefined) {
|
|
299
|
+
await store.setAgentOffline(agentId);
|
|
300
|
+
}
|
|
301
|
+
await store.shutdown();
|
|
302
|
+
} catch {
|
|
303
|
+
// best-effort — the process is exiting anyway
|
|
304
|
+
} finally {
|
|
305
|
+
releaseIdentityLock(identitySlot);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
installShutdownSignalHandlers({ shutdown, disposition: "exit" });
|
|
310
|
+
|
|
311
|
+
process.on("exit", () => {
|
|
312
|
+
// Synchronous fallback if async shutdown didn't complete in time.
|
|
313
|
+
// store.shutdown() is async but the coordinator socket unref() in
|
|
314
|
+
// MeshStore.init() means the process won't hang on the way out.
|
|
315
|
+
// Lock release is synchronous and pid-guarded, so it is safe to repeat.
|
|
316
|
+
releaseIdentityLock(identitySlot);
|
|
317
|
+
});
|
|
318
|
+
}
|
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Comms — Codex MCP tool server.
|
|
3
|
+
*
|
|
4
|
+
* Provides the "agent_comms" tool for Codex to call.
|
|
5
|
+
* Uses TCP mesh for state sync. Pending messages are drained and
|
|
6
|
+
* appended to every tool response so Codex sees them mid-turn.
|
|
7
|
+
*
|
|
8
|
+
* Run via: npx agent-comms bridge codex
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
createBridgeMesh,
|
|
16
|
+
createMeshErrorReporter,
|
|
17
|
+
buildAction,
|
|
18
|
+
ensureRegistered,
|
|
19
|
+
drainAndFormat,
|
|
20
|
+
installShutdownSignalHandlers,
|
|
21
|
+
MCP_TOOL_PARAMS,
|
|
22
|
+
} from "../../core/index.js";
|
|
23
|
+
import type { IdentitySlot } from "../../core/identity-store.js";
|
|
24
|
+
import { releaseIdentityLock } from "../../core/identity-store.js";
|
|
25
|
+
import { wireDefaultCcPeerFront } from "../cc-peer/default-front.js";
|
|
26
|
+
import {
|
|
27
|
+
tryStartBridgeWebServer,
|
|
28
|
+
getWebUrlStatus,
|
|
29
|
+
} from "../user/web/server.js";
|
|
30
|
+
import { nanoid } from "../../core/nanoid.js";
|
|
31
|
+
|
|
32
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
33
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Length of the random suffix appended to an auto-generated `codex-<suffix>` agent name. */
|
|
37
|
+
const CODEX_AGENT_NAME_SUFFIX_LENGTH = 4;
|
|
38
|
+
|
|
39
|
+
export async function run(): Promise<void> {
|
|
40
|
+
// Persistent identity for this slot: a stable device-id means the agent ID survives restarts, so peers can keep targeting us.
|
|
41
|
+
const identitySlot: IdentitySlot = { harness: "codex", cwd: process.cwd() };
|
|
42
|
+
const { store, tool } = await createBridgeMesh(identitySlot);
|
|
43
|
+
store.onError = createMeshErrorReporter();
|
|
44
|
+
store.onCoordinatorRoleChanged = wireDefaultCcPeerFront(store);
|
|
45
|
+
let agentId: string | undefined;
|
|
46
|
+
|
|
47
|
+
const mcp = new McpServer(
|
|
48
|
+
{ name: "agent-comms", version: "0.2.0" },
|
|
49
|
+
{ capabilities: {} },
|
|
50
|
+
);
|
|
51
|
+
|
|
52
|
+
// -----------------------------------------------------------------------
|
|
53
|
+
// Tool registration
|
|
54
|
+
// -----------------------------------------------------------------------
|
|
55
|
+
|
|
56
|
+
mcp.registerTool(
|
|
57
|
+
"agent_comms",
|
|
58
|
+
{
|
|
59
|
+
description: [
|
|
60
|
+
"Cross-harness agent communication mesh. Actions:",
|
|
61
|
+
"register, update, whoami, web_url, create_room, list_rooms, join_room, leave_room,",
|
|
62
|
+
"send, dm, list_agents, read_room, invite, decline_invite, kick, destroy_room.",
|
|
63
|
+
"Pending incoming messages are included in every response.",
|
|
64
|
+
].join(" "),
|
|
65
|
+
inputSchema: MCP_TOOL_PARAMS,
|
|
66
|
+
},
|
|
67
|
+
async (rawParams: unknown) => {
|
|
68
|
+
const params = isRecord(rawParams) ? rawParams : {};
|
|
69
|
+
const actionParam = params.action;
|
|
70
|
+
if (agentId === undefined) {
|
|
71
|
+
const name =
|
|
72
|
+
actionParam === "register" && typeof params.name === "string"
|
|
73
|
+
? params.name
|
|
74
|
+
: `codex-${nanoid(CODEX_AGENT_NAME_SUFFIX_LENGTH)}`;
|
|
75
|
+
const reg = await ensureRegistered({
|
|
76
|
+
cwd: process.cwd(),
|
|
77
|
+
store,
|
|
78
|
+
harness: "codex",
|
|
79
|
+
defaultName: name,
|
|
80
|
+
});
|
|
81
|
+
agentId = reg.agentId;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const action = buildAction(params);
|
|
85
|
+
const result = await tool.handle(
|
|
86
|
+
{ agentId, harness: "codex", cwd: process.cwd(), pid: process.pid },
|
|
87
|
+
action,
|
|
88
|
+
);
|
|
89
|
+
|
|
90
|
+
// Drain pending delivery messages and append to response
|
|
91
|
+
const deliveryLines = await drainAndFormat(store, agentId);
|
|
92
|
+
const content: { type: "text"; text: string }[] = [
|
|
93
|
+
{ type: "text", text: result.content },
|
|
94
|
+
];
|
|
95
|
+
|
|
96
|
+
if (deliveryLines.length > 0) {
|
|
97
|
+
content.push({
|
|
98
|
+
type: "text",
|
|
99
|
+
text: "📬 Incoming messages:\n" + deliveryLines.join("\n"),
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { content, isError: result.isError };
|
|
104
|
+
},
|
|
105
|
+
);
|
|
106
|
+
|
|
107
|
+
// -----------------------------------------------------------------------
|
|
108
|
+
// Startup
|
|
109
|
+
// -----------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
await store.init();
|
|
112
|
+
const webHandle = await tryStartBridgeWebServer(store, "codex");
|
|
113
|
+
tool.getWebUrlStatus = () => getWebUrlStatus(webHandle);
|
|
114
|
+
await mcp.connect(new StdioServerTransport());
|
|
115
|
+
|
|
116
|
+
const reg = await ensureRegistered({
|
|
117
|
+
cwd: process.cwd(),
|
|
118
|
+
store,
|
|
119
|
+
harness: "codex",
|
|
120
|
+
defaultName: `codex-${nanoid(CODEX_AGENT_NAME_SUFFIX_LENGTH)}`,
|
|
121
|
+
});
|
|
122
|
+
agentId = reg.agentId;
|
|
123
|
+
|
|
124
|
+
installShutdownSignalHandlers({
|
|
125
|
+
shutdown: async () => {
|
|
126
|
+
if (agentId !== undefined) await store.setAgentOffline(agentId);
|
|
127
|
+
await store.shutdown();
|
|
128
|
+
releaseIdentityLock(identitySlot);
|
|
129
|
+
},
|
|
130
|
+
disposition: "exit",
|
|
131
|
+
});
|
|
132
|
+
}
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Comms — generic MCP tool server.
|
|
3
|
+
*
|
|
4
|
+
* Standard MCP server that works with any MCP-compatible harness.
|
|
5
|
+
* Uses TCP mesh for state sync. Pending messages are drained and
|
|
6
|
+
* appended to every tool response so the agent sees them.
|
|
7
|
+
*
|
|
8
|
+
* Run via: npx agent-comms bridge mcp
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import { McpServer } from "@modelcontextprotocol/sdk/server/mcp.js";
|
|
12
|
+
import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js";
|
|
13
|
+
|
|
14
|
+
import {
|
|
15
|
+
createBridgeMesh,
|
|
16
|
+
createMeshErrorReporter,
|
|
17
|
+
buildAction,
|
|
18
|
+
ensureRegistered,
|
|
19
|
+
drainAndFormat,
|
|
20
|
+
installShutdownSignalHandlers,
|
|
21
|
+
MCP_TOOL_PARAMS,
|
|
22
|
+
type BridgeRunOptions,
|
|
23
|
+
} from "../../core/index.js";
|
|
24
|
+
import type { IdentitySlot } from "../../core/identity-store.js";
|
|
25
|
+
import { releaseIdentityLock } from "../../core/identity-store.js";
|
|
26
|
+
import { wireDefaultCcPeerFront } from "../cc-peer/default-front.js";
|
|
27
|
+
import {
|
|
28
|
+
tryStartBridgeWebServer,
|
|
29
|
+
getWebUrlStatus,
|
|
30
|
+
} from "../user/web/server.js";
|
|
31
|
+
import { nanoid } from "../../core/nanoid.js";
|
|
32
|
+
|
|
33
|
+
/** Length of the random suffix appended to a default `mcp-<suffix>` agent name when no explicit name is registered. */
|
|
34
|
+
const DEFAULT_AGENT_NAME_SUFFIX_LENGTH = 4;
|
|
35
|
+
|
|
36
|
+
function isRecord(value: unknown): value is Record<string, unknown> {
|
|
37
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export async function run(options?: Readonly<BridgeRunOptions>): Promise<void> {
|
|
41
|
+
// Persistent identity for this slot: a stable device-id means the agent ID survives restarts, so peers can keep targeting us.
|
|
42
|
+
const identitySlot: IdentitySlot = options?.slot ?? {
|
|
43
|
+
harness: "mcp",
|
|
44
|
+
cwd: process.cwd(),
|
|
45
|
+
};
|
|
46
|
+
const { store, tool } = await createBridgeMesh(identitySlot, options?.mesh);
|
|
47
|
+
store.onError = createMeshErrorReporter();
|
|
48
|
+
store.onCoordinatorRoleChanged = wireDefaultCcPeerFront(store);
|
|
49
|
+
let agentId: string | undefined;
|
|
50
|
+
|
|
51
|
+
const mcp = new McpServer(
|
|
52
|
+
{ name: "agent-comms", version: "1.1.0" },
|
|
53
|
+
{ capabilities: {} },
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
// -----------------------------------------------------------------------
|
|
57
|
+
// Tool registration
|
|
58
|
+
// -----------------------------------------------------------------------
|
|
59
|
+
|
|
60
|
+
mcp.registerTool(
|
|
61
|
+
"agent_comms",
|
|
62
|
+
{
|
|
63
|
+
description: [
|
|
64
|
+
"Cross-harness agent communication mesh. Actions:",
|
|
65
|
+
"register, update, whoami, web_url, create_room, list_rooms, join_room, leave_room,",
|
|
66
|
+
"send, dm, list_agents, read_room, invite, decline_invite, kick, destroy_room.",
|
|
67
|
+
"Pending incoming messages are included in every response.",
|
|
68
|
+
].join(" "),
|
|
69
|
+
inputSchema: MCP_TOOL_PARAMS,
|
|
70
|
+
},
|
|
71
|
+
async (rawParams: unknown) => {
|
|
72
|
+
const params = isRecord(rawParams) ? rawParams : {};
|
|
73
|
+
const actionParam = params.action;
|
|
74
|
+
|
|
75
|
+
if (agentId === undefined) {
|
|
76
|
+
const name =
|
|
77
|
+
actionParam === "register" && typeof params.name === "string"
|
|
78
|
+
? params.name
|
|
79
|
+
: `mcp-${nanoid(DEFAULT_AGENT_NAME_SUFFIX_LENGTH)}`;
|
|
80
|
+
const reg = await ensureRegistered({
|
|
81
|
+
cwd: process.cwd(),
|
|
82
|
+
store,
|
|
83
|
+
harness: "mcp",
|
|
84
|
+
defaultName: name,
|
|
85
|
+
});
|
|
86
|
+
agentId = reg.agentId;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const action = buildAction(params);
|
|
90
|
+
const result = await tool.handle(
|
|
91
|
+
{ agentId, harness: "mcp", cwd: process.cwd(), pid: process.pid },
|
|
92
|
+
action,
|
|
93
|
+
);
|
|
94
|
+
|
|
95
|
+
// Drain pending delivery messages and append to response
|
|
96
|
+
const deliveryLines = await drainAndFormat(store, agentId);
|
|
97
|
+
const content: { type: "text"; text: string }[] = [
|
|
98
|
+
{ type: "text", text: result.content },
|
|
99
|
+
];
|
|
100
|
+
|
|
101
|
+
if (deliveryLines.length > 0) {
|
|
102
|
+
content.push({
|
|
103
|
+
type: "text",
|
|
104
|
+
text: "📬 Incoming messages:\n" + deliveryLines.join("\n"),
|
|
105
|
+
});
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
return { content, isError: result.isError };
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
|
|
112
|
+
// -----------------------------------------------------------------------
|
|
113
|
+
// Startup
|
|
114
|
+
// -----------------------------------------------------------------------
|
|
115
|
+
|
|
116
|
+
await store.init();
|
|
117
|
+
const webHandle = await tryStartBridgeWebServer(store, "mcp");
|
|
118
|
+
tool.getWebUrlStatus = () => getWebUrlStatus(webHandle);
|
|
119
|
+
await mcp.connect(new StdioServerTransport());
|
|
120
|
+
|
|
121
|
+
const reg = await ensureRegistered({
|
|
122
|
+
cwd: process.cwd(),
|
|
123
|
+
store,
|
|
124
|
+
harness: "mcp",
|
|
125
|
+
defaultName: `mcp-${nanoid(DEFAULT_AGENT_NAME_SUFFIX_LENGTH)}`,
|
|
126
|
+
});
|
|
127
|
+
agentId = reg.agentId;
|
|
128
|
+
|
|
129
|
+
installShutdownSignalHandlers({
|
|
130
|
+
shutdown: async () => {
|
|
131
|
+
if (agentId !== undefined) await store.setAgentOffline(agentId);
|
|
132
|
+
await store.shutdown();
|
|
133
|
+
releaseIdentityLock(identitySlot);
|
|
134
|
+
},
|
|
135
|
+
disposition: "exit",
|
|
136
|
+
});
|
|
137
|
+
}
|