Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -141,6 +141,18 @@ npx agent-comms # auto-detect and configure

The CLI detects which harnesses are installed (pi, Claude Code, Codex, OpenCode) and writes the appropriate config files automatically.

### cc-peer (cross-machine Claude Code relay)

[`cc-peer`](https://github.com/ExaDev/cc-peer) speaks Claude Code's own local cross-session peer protocol directly — a per-session Unix socket, no cross-machine leg of its own. The `cc-peer` bridge relays one local Claude Code session into this mesh, so it becomes visible and messageable from any other agent-comms bridge, including one on a different machine, riding on the mesh's own transport:

```bash
npx agent-comms bridge cc-peer <local-session-name>
# or address the local session by pid instead of its registered name:
npx agent-comms bridge cc-peer --pid=12345
```

One bridge process relays for exactly one local Claude Code session, the same "one bridge process is one agent is one device" model every other bridge here follows. Inbound messages from that session are posted into this bridge's own project room; mesh deliveries addressed to this bridge's agent are relayed back to that same session via `cc-peer`'s own `send()`.

## Adding a new harness

A bridge is two things:
Expand Down
1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,7 @@
"dependencies": {
"@modelcontextprotocol/sdk": "1.30.0",
"cbor2": "2.3.0",
"cc-peer": "1.3.4",
"preact": "10.29.7",
"typebox": "1.3.6",
"wire-mesh-core": "1.30.1",
Expand Down
12 changes: 12 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

77 changes: 77 additions & 0 deletions src/bridges/cc-peer/bridge.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
/**
* cc-peer bridge — relays between one local Claude Code peer (reached via cc-peer's own native cross-session protocol) and this side's agent-comms mesh, giving that local session cross-machine reach through the mesh's own transport (agent-comms#87).
*
* One bridge process relays for exactly one target local Claude Code peer, matching the "one bridge process is one agent is one device" model every other bridge in this codebase already follows -- not a many-to-one fan-out. Inbound cc-peer messages from that target are posted into this agent's own project room (the same auto-created room ensureRegistered/ensureProjectRoom already gives every bridge); mesh deliveries for this agent are relayed back to that same target via CcPeer.send().
*
* The wiring itself (wireCcPeerBridge) is dependency-injected and deliberately separate from the real CcPeer/MeshStore construction in run.ts, so it can be tested against a fake CcPeer without a real local Claude Code session.
*/

import type { CommsTool } from "../../core/tool.js";
import { buildAction } from "../../core/bridge.js";
import { formatDeliveryEvent } from "../../core/bridge.js";
import type { DeliveryEvent } from "../../core/types.js";

/** How cc-peer addresses a target peer -- mirrors cc-peer's own PeerRef type without importing it, so this file has no direct dependency on the cc-peer package (only run.ts, which does the real construction, needs that). */
export type CcPeerRef =
{ pid: number } | { name: string } | { address: string };

/** The one inbound-message shape this bridge reads from CcPeer's own "message" event -- narrowed to the fields it actually uses. */
export interface CcPeerInboundMessage {
from?: string;
fromName?: string;
body: string;
}

/** The narrow slice of CcPeer's own real API this bridge needs -- satisfied by the real class in run.ts, and by a fake in tests. */
export interface CcPeerLike {
on: (
event: "message",
listener: (m: Readonly<CcPeerInboundMessage>) => void,
) => void;
send: (
target: Readonly<CcPeerRef>,
body: string,
) => Promise<{ msgId: string }>;
}

/** The narrow slice of MeshStore this bridge needs -- onDelivery is MeshStore-only (not part of the generic CommsStore interface every bridge otherwise depends on), since only a mesh-backed store can push. */
export interface CcPeerBridgeStore {
onDelivery:
| ((agentId: string, event: DeliveryEvent) => void | Promise<void>)
| undefined;
}

export interface CcPeerBridgeDeps {
store: CcPeerBridgeStore;
tool: Pick<CommsTool, "handle">;
peer: CcPeerLike;
agentId: string;
roomId: string;
target: Readonly<CcPeerRef>;
cwd: string;
}

/** Wires the two directions of the relay. Never awaited by the caller -- both directions are genuinely fire-and-forget from this function's own point of view (a send failure surfaces through cc-peer's own receipt events / agent-comms' own delivery-status events, not a thrown error here). */
export function wireCcPeerBridge(deps: Readonly<CcPeerBridgeDeps>): void {
deps.peer.on("message", (m) => {
const sender = m.fromName ?? m.from ?? "unknown";
const action = buildAction({
action: "send",
room: deps.roomId,
content: `${sender}: ${m.body}`,
});
void deps.tool.handle(
{
agentId: deps.agentId,
harness: "cc-peer",
cwd: deps.cwd,
pid: process.pid,
},
action,
);
});

deps.store.onDelivery = (_targetId, event) => {
void deps.peer.send(deps.target, formatDeliveryEvent(event));
};
}
84 changes: 84 additions & 0 deletions src/bridges/cc-peer/run.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
/**
* cc-peer bridge — CLI entry point.
*
* Run via: npx agent-comms bridge cc-peer <target-name> npx agent-comms bridge cc-peer --pid=<pid>
*
* Constructs the real CcPeer and MeshStore, then hands both to wireCcPeerBridge for the actual relay wiring (see bridge.ts).
*/

import { CcPeer } from "cc-peer";
import {
createBridgeMesh,
ensureRegistered,
ensureProjectRoom,
} from "../../core/index.js";
import type { IdentitySlot } from "../../core/identity-store.js";
import { releaseIdentityLock } from "../../core/identity-store.js";
import { wireCcPeerBridge, type CcPeerRef } from "./bridge.js";

const PID_FLAG_PREFIX = "--pid=";
/** argv layout for `node cli.js bridge cc-peer <target-arg>`: index 0/1 are the node binary and script path, 2 is "bridge", 3 is the bridge id ("cc-peer") itself -- this bridge's own args start one past that. */
const BRIDGE_ARGS_START_INDEX = 4;

function parseTarget(argv: readonly string[]): CcPeerRef {
const arg = argv[0];
if (arg === undefined || arg === "") {
console.error(
"Usage: agent-comms bridge cc-peer <target-name> | --pid=<pid>",
);
process.exit(1);
}
if (arg.startsWith(PID_FLAG_PREFIX)) {
const pid = Number(arg.slice(PID_FLAG_PREFIX.length));
if (!Number.isInteger(pid) || pid <= 0) {
console.error(`Invalid --pid value: ${arg}`);
process.exit(1);
}
return { pid };
}
return { name: arg };
}

export async function run(): Promise<void> {
const target = parseTarget(process.argv.slice(BRIDGE_ARGS_START_INDEX));

const identitySlot: IdentitySlot = { harness: "cc-peer", cwd: process.cwd() };
const { store, tool } = await createBridgeMesh(identitySlot);

const reg = await ensureRegistered({
store,
cwd: process.cwd(),
harness: "cc-peer",
defaultName: "cc-peer-bridge",
});
const roomId = await ensureProjectRoom(store, reg.agentId, process.cwd());

const peer = await CcPeer.create({ name: "agent-comms-bridge" });

wireCcPeerBridge({
store,
tool,
peer,
agentId: reg.agentId,
roomId,
target,
cwd: process.cwd(),
});

await store.init();

process.on("SIGINT", () => {
void shutdown();
});
process.on("SIGTERM", () => {
void shutdown();
});

async function shutdown(): Promise<void> {
await peer.stop();
await store.setAgentOffline(reg.agentId);
releaseIdentityLock(identitySlot);
await store.shutdown();
process.exit(0);
}
}
2 changes: 2 additions & 0 deletions src/bridges/registry.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import * as claudeCode from "./claude-code/channel.js";
import * as codex from "./codex/tool.js";
import * as mcp from "./mcp/server.js";
import * as ccPeer from "./cc-peer/run.js";

export interface Bridge {
run: () => void | Promise<void>;
Expand All @@ -10,4 +11,5 @@ export const bridges: Record<string, Bridge> = {
"claude-code": claudeCode,
codex,
mcp,
"cc-peer": ccPeer,
};
Loading