agent-comms 8.15.9 → 8.15.10
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/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/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,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
|
+
}
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Comms — OpenCode plugin bridge.
|
|
3
|
+
*
|
|
4
|
+
* Receives incoming messages via TCP mesh push and injects them
|
|
5
|
+
* into the OpenCode TUI via tui.prompt.append + tui.submitPrompt.
|
|
6
|
+
*
|
|
7
|
+
* Install (project): ln -s ~/Developer/agent-comms/src/bridges/opencode/plugin.ts .opencode/plugins/agent-comms.ts
|
|
8
|
+
* Install (global): ln -s ~/Developer/agent-comms/src/bridges/opencode/plugin.ts ~/.config/opencode/plugins/agent-comms.ts
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
createBridgeMesh,
|
|
13
|
+
createMeshErrorReporter,
|
|
14
|
+
ensureRegistered,
|
|
15
|
+
formatDeliveryEvent,
|
|
16
|
+
installShutdownSignalHandlers,
|
|
17
|
+
} from "../../core/index.js";
|
|
18
|
+
import type { IdentitySlot } from "../../core/identity-store.js";
|
|
19
|
+
import { releaseIdentityLock } from "../../core/identity-store.js";
|
|
20
|
+
import { wireDefaultCcPeerFront } from "../cc-peer/default-front.js";
|
|
21
|
+
import { tryStartBridgeWebServer } from "../user/web/server.js";
|
|
22
|
+
import { nanoid } from "../../core/nanoid.js";
|
|
23
|
+
|
|
24
|
+
// Persistent identity for this slot: a stable device-id means the agent ID survives restarts, so peers can keep targeting us. A plugin unload has no hook here, so an unload that is not a process exit still leaves a stale lock, which self-heals via the pid probe.
|
|
25
|
+
const identitySlot: IdentitySlot = { harness: "opencode", cwd: process.cwd() };
|
|
26
|
+
|
|
27
|
+
// Length (in characters) of the random suffix appended to the default auto-generated agent name.
|
|
28
|
+
const DEFAULT_NAME_SUFFIX_LENGTH = 4;
|
|
29
|
+
|
|
30
|
+
// Minimal interface for the OpenCode SDK client we actually use
|
|
31
|
+
interface OpenCodeClient {
|
|
32
|
+
tui: {
|
|
33
|
+
appendPrompt: (body: Readonly<{ text: string }>) => Promise<unknown>;
|
|
34
|
+
submitPrompt: () => Promise<unknown>;
|
|
35
|
+
};
|
|
36
|
+
session: {
|
|
37
|
+
list: () => Promise<{ data: { id: string }[] }>;
|
|
38
|
+
prompt: (
|
|
39
|
+
opts: Readonly<{
|
|
40
|
+
path: { id: string };
|
|
41
|
+
body: { parts: { type: string; text: string }[] };
|
|
42
|
+
}>,
|
|
43
|
+
) => Promise<unknown>;
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function isOpenCodeClient(value: unknown): value is OpenCodeClient {
|
|
48
|
+
if (typeof value !== "object" || value === null) return false;
|
|
49
|
+
if (!("tui" in value)) return false;
|
|
50
|
+
if (!("session" in value)) return false;
|
|
51
|
+
return true;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export const AgentCommsPlugin = async (opts: {
|
|
55
|
+
project: unknown;
|
|
56
|
+
client: unknown;
|
|
57
|
+
$: unknown;
|
|
58
|
+
directory: string;
|
|
59
|
+
worktree: string;
|
|
60
|
+
}) => {
|
|
61
|
+
if (!isOpenCodeClient(opts.client)) {
|
|
62
|
+
throw new Error("Agent Comms plugin requires a valid OpenCode client");
|
|
63
|
+
}
|
|
64
|
+
const client = opts.client;
|
|
65
|
+
|
|
66
|
+
const { store } = await createBridgeMesh(identitySlot);
|
|
67
|
+
store.onError = createMeshErrorReporter();
|
|
68
|
+
store.onCoordinatorRoleChanged = wireDefaultCcPeerFront(store);
|
|
69
|
+
await store.init();
|
|
70
|
+
await tryStartBridgeWebServer(store, "opencode");
|
|
71
|
+
|
|
72
|
+
const reg = await ensureRegistered({
|
|
73
|
+
cwd: process.cwd(),
|
|
74
|
+
store,
|
|
75
|
+
harness: "opencode",
|
|
76
|
+
defaultName: `opencode-${nanoid(DEFAULT_NAME_SUFFIX_LENGTH)}`,
|
|
77
|
+
});
|
|
78
|
+
const agentId = reg.agentId;
|
|
79
|
+
|
|
80
|
+
// "reraise", not "exit": this plugin runs inside OpenCode's own process, and registering a signal listener at all removes Node's default terminate behaviour, so exiting here would pre-empt the host and doing nothing would swallow its Ctrl-C.
|
|
81
|
+
installShutdownSignalHandlers({
|
|
82
|
+
shutdown: async () => {
|
|
83
|
+
await store.setAgentOffline(agentId);
|
|
84
|
+
await store.shutdown();
|
|
85
|
+
releaseIdentityLock(identitySlot);
|
|
86
|
+
},
|
|
87
|
+
disposition: "reraise",
|
|
88
|
+
onError: createMeshErrorReporter(),
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
// Incoming messages arrive via TCP mesh — push to TUI immediately
|
|
92
|
+
store.onDelivery = async (_targetId: string, event) => {
|
|
93
|
+
const line = formatDeliveryEvent(event);
|
|
94
|
+
const message = `📬 Agent Comms: ${line}`;
|
|
95
|
+
try {
|
|
96
|
+
await client.tui.appendPrompt({ text: message });
|
|
97
|
+
await client.tui.submitPrompt();
|
|
98
|
+
} catch {
|
|
99
|
+
// Fallback: prompt the current session directly
|
|
100
|
+
const sessions = await client.session.list();
|
|
101
|
+
const current = sessions.data[0];
|
|
102
|
+
if (current) {
|
|
103
|
+
await client.session.prompt({
|
|
104
|
+
path: { id: current.id },
|
|
105
|
+
body: {
|
|
106
|
+
parts: [{ type: "text", text: message }],
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return {
|
|
114
|
+
event: async ({ event }: { event: { type: string } }) => {
|
|
115
|
+
if (event.type === "session.idle") {
|
|
116
|
+
// Drain any remaining undelivered messages on idle
|
|
117
|
+
const events = await store.drainDelivery(agentId);
|
|
118
|
+
for (const e of events) {
|
|
119
|
+
const line = formatDeliveryEvent(e);
|
|
120
|
+
try {
|
|
121
|
+
await client.tui.appendPrompt({ text: `📬 ${line}` });
|
|
122
|
+
await client.tui.submitPrompt();
|
|
123
|
+
} catch {
|
|
124
|
+
/* best effort */
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
};
|
|
130
|
+
};
|
|
@@ -0,0 +1,330 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Agent Comms — pi bridge extension.
|
|
3
|
+
*
|
|
4
|
+
* Provides the `agent_comms` tool and receives incoming messages
|
|
5
|
+
* via TCP mesh push. Actionable events (DMs, room messages, invites)
|
|
6
|
+
* wake the model via sendMessage() with a custom type; informational
|
|
7
|
+
* events are buffered and drained on the next tool call.
|
|
8
|
+
*
|
|
9
|
+
* Install: symlink into ~/.pi/agent/extensions/ with pi.extensions
|
|
10
|
+
* pointing to this file.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
import type {
|
|
14
|
+
ExtensionAPI,
|
|
15
|
+
ExtensionUIContext,
|
|
16
|
+
} from "@earendil-works/pi-coding-agent";
|
|
17
|
+
import * as path from "node:path";
|
|
18
|
+
import { Type } from "typebox";
|
|
19
|
+
import { StringEnum } from "@earendil-works/pi-ai";
|
|
20
|
+
|
|
21
|
+
import {
|
|
22
|
+
createBridgeMeshSync,
|
|
23
|
+
createMeshErrorReporter,
|
|
24
|
+
buildAction,
|
|
25
|
+
ensureProjectRoom,
|
|
26
|
+
ensureRegistered,
|
|
27
|
+
extractStreamingBehavior,
|
|
28
|
+
formatDeliveryEvent,
|
|
29
|
+
isActionableEvent,
|
|
30
|
+
} from "../../core/index.js";
|
|
31
|
+
import {
|
|
32
|
+
releaseIdentityLock,
|
|
33
|
+
type IdentitySlot,
|
|
34
|
+
} from "../../core/identity-store.js";
|
|
35
|
+
import { wireDefaultCcPeerFront } from "../cc-peer/default-front.js";
|
|
36
|
+
import {
|
|
37
|
+
tryStartBridgeWebServer,
|
|
38
|
+
getWebPort,
|
|
39
|
+
getWebUrlStatus,
|
|
40
|
+
type WebServerHandle,
|
|
41
|
+
} from "../user/web/server.js";
|
|
42
|
+
import { nanoid } from "../../core/nanoid.js";
|
|
43
|
+
|
|
44
|
+
export default function (pi: ExtensionAPI) {
|
|
45
|
+
// Persistent identity for this slot: a stable device-id means the agent ID survives restarts, so peers can keep targeting us
|
|
46
|
+
const identitySlot: IdentitySlot = { harness: "pi", cwd: process.cwd() };
|
|
47
|
+
const { store, tool, attachIdentity } = createBridgeMeshSync(identitySlot);
|
|
48
|
+
store.onCoordinatorRoleChanged = wireDefaultCcPeerFront(store);
|
|
49
|
+
|
|
50
|
+
let agentId: string | undefined;
|
|
51
|
+
let webHandle: WebServerHandle | undefined;
|
|
52
|
+
tool.getWebUrlStatus = () => getWebUrlStatus(webHandle);
|
|
53
|
+
let uiCtx: ExtensionUIContext | undefined;
|
|
54
|
+
// The extension runs inside pi's own terminal UI, so mesh errors surface as pi notifications rather than raw stderr writes that would corrupt its display.
|
|
55
|
+
store.onError = createMeshErrorReporter((line) => {
|
|
56
|
+
uiCtx?.notify(line.trimEnd(), "error");
|
|
57
|
+
});
|
|
58
|
+
let projectRoom: string | undefined;
|
|
59
|
+
const informationalBuffer: string[] = [];
|
|
60
|
+
|
|
61
|
+
// LRU-style dedup for delivery events
|
|
62
|
+
const recentDeliveries = new Set<string>();
|
|
63
|
+
const MAX_DEDUP_ENTRIES = 100;
|
|
64
|
+
const AUTO_NAME_SUFFIX_LENGTH = 4;
|
|
65
|
+
|
|
66
|
+
function refreshStatus(): void {
|
|
67
|
+
if (!uiCtx) return;
|
|
68
|
+
const parts: string[] = [];
|
|
69
|
+
if (projectRoom !== undefined) parts.push(`💬 ${projectRoom}`);
|
|
70
|
+
if (webHandle) {
|
|
71
|
+
const port = getWebPort(webHandle);
|
|
72
|
+
if (port !== undefined && port !== 0)
|
|
73
|
+
parts.push(`http://127.0.0.1:${String(port)}`);
|
|
74
|
+
}
|
|
75
|
+
if (informationalBuffer.length > 0) {
|
|
76
|
+
parts.push(`📬 ${String(informationalBuffer.length)}`);
|
|
77
|
+
}
|
|
78
|
+
uiCtx.setStatus("comms", parts.length > 0 ? parts.join(" ") : undefined);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
store.onDelivery = (_targetId: string, event) => {
|
|
82
|
+
const key = `${event.type}:${formatDeliveryEvent(event)}`;
|
|
83
|
+
if (recentDeliveries.has(key)) return;
|
|
84
|
+
recentDeliveries.add(key);
|
|
85
|
+
if (recentDeliveries.size > MAX_DEDUP_ENTRIES) {
|
|
86
|
+
// Evict oldest entries (simple approach: clear half)
|
|
87
|
+
const entries = Array.from(recentDeliveries);
|
|
88
|
+
const half = Math.floor(entries.length / 2);
|
|
89
|
+
for (let i = 0; i < half; i++) {
|
|
90
|
+
const entry = entries[i];
|
|
91
|
+
if (entry !== undefined) recentDeliveries.delete(entry);
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
const line = formatDeliveryEvent(event);
|
|
96
|
+
const hint = extractStreamingBehavior(event);
|
|
97
|
+
|
|
98
|
+
// Pick deliverAs from sender hint; fall back to event-type heuristic
|
|
99
|
+
const deliverAs: "steer" | "followUp" | undefined =
|
|
100
|
+
hint === "followUp"
|
|
101
|
+
? "followUp"
|
|
102
|
+
: hint === "steer" || (!hint && isActionableEvent(event))
|
|
103
|
+
? "steer"
|
|
104
|
+
: undefined;
|
|
105
|
+
|
|
106
|
+
if (deliverAs !== undefined) {
|
|
107
|
+
pi.sendMessage(
|
|
108
|
+
{
|
|
109
|
+
content: `📬 ${line}`,
|
|
110
|
+
customType: "comms-delivery",
|
|
111
|
+
display: false,
|
|
112
|
+
},
|
|
113
|
+
{ triggerTurn: true, deliverAs },
|
|
114
|
+
);
|
|
115
|
+
} else {
|
|
116
|
+
// info or non-actionable with no hint — buffer for next tool call
|
|
117
|
+
informationalBuffer.push(line);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
uiCtx?.notify(`📬 ${line}`, "info");
|
|
121
|
+
refreshStatus();
|
|
122
|
+
};
|
|
123
|
+
|
|
124
|
+
// -----------------------------------------------------------------------
|
|
125
|
+
// Lifecycle
|
|
126
|
+
// -----------------------------------------------------------------------
|
|
127
|
+
|
|
128
|
+
let meshReady = false;
|
|
129
|
+
|
|
130
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
131
|
+
uiCtx = ctx.ui;
|
|
132
|
+
|
|
133
|
+
await attachIdentity();
|
|
134
|
+
await store.init();
|
|
135
|
+
meshReady = store.connected;
|
|
136
|
+
|
|
137
|
+
if (!meshReady) {
|
|
138
|
+
ctx.ui.notify(
|
|
139
|
+
"Agent Comms: mesh unavailable (no coordinator). Running without mesh.",
|
|
140
|
+
"warning",
|
|
141
|
+
);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
if (meshReady) {
|
|
145
|
+
const reg = await ensureRegistered({
|
|
146
|
+
store,
|
|
147
|
+
cwd: process.cwd(),
|
|
148
|
+
harness: "pi",
|
|
149
|
+
defaultName: `pi-${nanoid(AUTO_NAME_SUFFIX_LENGTH)}`,
|
|
150
|
+
});
|
|
151
|
+
agentId = reg.agentId;
|
|
152
|
+
|
|
153
|
+
// Auto-create and join a project room for the working directory
|
|
154
|
+
await ensureProjectRoom(store, agentId, process.cwd());
|
|
155
|
+
projectRoom = path.basename(process.cwd());
|
|
156
|
+
|
|
157
|
+
// Start web UI sharing the same mesh peer and agent identity
|
|
158
|
+
webHandle = await tryStartBridgeWebServer(store, "pi");
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
refreshStatus();
|
|
162
|
+
|
|
163
|
+
if (meshReady) {
|
|
164
|
+
ctx.ui.notify("Agent Comms: connected", "info");
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
168
|
+
pi.on("session_shutdown", async () => {
|
|
169
|
+
uiCtx?.setStatus("comms", undefined);
|
|
170
|
+
uiCtx = undefined;
|
|
171
|
+
// Shut down the web server before the bridge store so the web
|
|
172
|
+
// controller's coordinator connection can close cleanly.
|
|
173
|
+
if (webHandle) {
|
|
174
|
+
webHandle.wss.close();
|
|
175
|
+
webHandle.server.close();
|
|
176
|
+
webHandle = undefined;
|
|
177
|
+
}
|
|
178
|
+
if (agentId !== undefined) {
|
|
179
|
+
await store.setAgentOffline(agentId);
|
|
180
|
+
}
|
|
181
|
+
releaseIdentityLock(identitySlot);
|
|
182
|
+
await store.shutdown();
|
|
183
|
+
});
|
|
184
|
+
|
|
185
|
+
// -----------------------------------------------------------------------
|
|
186
|
+
// Commands
|
|
187
|
+
// -----------------------------------------------------------------------
|
|
188
|
+
|
|
189
|
+
pi.registerCommand("comms-url", {
|
|
190
|
+
description: "Show the Agent Comms web UI URL",
|
|
191
|
+
handler: async (_args, ctx) => {
|
|
192
|
+
if (!webHandle) {
|
|
193
|
+
ctx.ui.notify("Web UI is not running.", "error");
|
|
194
|
+
return Promise.resolve();
|
|
195
|
+
}
|
|
196
|
+
const port = getWebPort(webHandle);
|
|
197
|
+
if (port === undefined || port === 0) {
|
|
198
|
+
ctx.ui.notify("Web UI port not yet assigned.", "error");
|
|
199
|
+
return Promise.resolve();
|
|
200
|
+
}
|
|
201
|
+
ctx.ui.notify(`http://127.0.0.1:${String(port)}`, "info");
|
|
202
|
+
return Promise.resolve();
|
|
203
|
+
},
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// -----------------------------------------------------------------------
|
|
207
|
+
// Tool registration
|
|
208
|
+
// -----------------------------------------------------------------------
|
|
209
|
+
|
|
210
|
+
pi.registerTool({
|
|
211
|
+
name: "agent_comms",
|
|
212
|
+
label: "Agent Comms",
|
|
213
|
+
description: [
|
|
214
|
+
"Cross-harness agent communication mesh. Send messages to rooms and DM other agents.",
|
|
215
|
+
"Actions: register, update, whoami, create_room, list_rooms, join_room, leave_room,",
|
|
216
|
+
"send, dm, list_agents, read_room, invite, decline_invite, kick, destroy_room, web_url.",
|
|
217
|
+
"Register first, then join or create rooms to communicate.",
|
|
218
|
+
].join(" "),
|
|
219
|
+
promptSnippet: "Communicate with other LLM agents via rooms and DMs",
|
|
220
|
+
promptGuidelines: [
|
|
221
|
+
"Use agent_comms to coordinate with other running agents. Register on session start, join rooms for collaboration.",
|
|
222
|
+
],
|
|
223
|
+
parameters: Type.Object({
|
|
224
|
+
action: StringEnum(
|
|
225
|
+
[
|
|
226
|
+
"register",
|
|
227
|
+
"update",
|
|
228
|
+
"whoami",
|
|
229
|
+
"create_room",
|
|
230
|
+
"list_rooms",
|
|
231
|
+
"join_room",
|
|
232
|
+
"leave_room",
|
|
233
|
+
"send",
|
|
234
|
+
"dm",
|
|
235
|
+
"list_agents",
|
|
236
|
+
"read_room",
|
|
237
|
+
"invite",
|
|
238
|
+
"decline_invite",
|
|
239
|
+
"kick",
|
|
240
|
+
"destroy_room",
|
|
241
|
+
"web_url",
|
|
242
|
+
],
|
|
243
|
+
{ description: "Action to perform" },
|
|
244
|
+
),
|
|
245
|
+
name: Type.Optional(
|
|
246
|
+
Type.String({
|
|
247
|
+
description: "Agent display name (for register/update)",
|
|
248
|
+
}),
|
|
249
|
+
),
|
|
250
|
+
visibility: Type.Optional(
|
|
251
|
+
StringEnum(["visible", "hidden", "ghost"], {
|
|
252
|
+
description: "Visibility to other agents",
|
|
253
|
+
}),
|
|
254
|
+
),
|
|
255
|
+
tags: Type.Optional(
|
|
256
|
+
Type.Array(Type.String(), { description: "Agent capability tags" }),
|
|
257
|
+
),
|
|
258
|
+
status: Type.Optional(
|
|
259
|
+
StringEnum(["active", "idle", "busy"], {
|
|
260
|
+
description: "Agent status (for update)",
|
|
261
|
+
}),
|
|
262
|
+
),
|
|
263
|
+
room: Type.Optional(Type.String({ description: "Room name/ID" })),
|
|
264
|
+
type: Type.Optional(
|
|
265
|
+
StringEnum(["public", "private", "secret"], {
|
|
266
|
+
description: "Room type (for create_room)",
|
|
267
|
+
}),
|
|
268
|
+
),
|
|
269
|
+
description: Type.Optional(
|
|
270
|
+
Type.String({ description: "Room description (for create_room)" }),
|
|
271
|
+
),
|
|
272
|
+
target: Type.Optional(
|
|
273
|
+
Type.String({ description: "Target room name or agent ID" }),
|
|
274
|
+
),
|
|
275
|
+
content: Type.Optional(Type.String({ description: "Message content" })),
|
|
276
|
+
replyTo: Type.Optional(
|
|
277
|
+
Type.String({ description: "Message ID to reply to" }),
|
|
278
|
+
),
|
|
279
|
+
agent: Type.Optional(
|
|
280
|
+
Type.String({ description: "Target agent ID (for invite/kick)" }),
|
|
281
|
+
),
|
|
282
|
+
since: Type.Optional(
|
|
283
|
+
Type.String({
|
|
284
|
+
description: "ISO timestamp to read messages since",
|
|
285
|
+
}),
|
|
286
|
+
),
|
|
287
|
+
reason: Type.Optional(
|
|
288
|
+
Type.String({
|
|
289
|
+
description: "Reason for declining an invite (for decline_invite)",
|
|
290
|
+
}),
|
|
291
|
+
),
|
|
292
|
+
streamingBehavior: Type.Optional(
|
|
293
|
+
StringEnum(["steer", "followUp", "info"], {
|
|
294
|
+
description:
|
|
295
|
+
"Delivery timing hint: steer (act now), followUp (act when idle), info (whenever convenient)",
|
|
296
|
+
}),
|
|
297
|
+
),
|
|
298
|
+
}),
|
|
299
|
+
|
|
300
|
+
async execute(_toolCallId, params) {
|
|
301
|
+
if (agentId === undefined) {
|
|
302
|
+
return {
|
|
303
|
+
content: [{ type: "text", text: "Error: not registered" }],
|
|
304
|
+
details: { action: params.action },
|
|
305
|
+
isError: true,
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
const action = buildAction(params);
|
|
310
|
+
const result = await tool.handle(
|
|
311
|
+
{ agentId, harness: "pi", cwd: process.cwd(), pid: process.pid },
|
|
312
|
+
action,
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
// Drain buffered informational events and prepend to the response
|
|
316
|
+
const pending = informationalBuffer.splice(0);
|
|
317
|
+
refreshStatus();
|
|
318
|
+
const prefix =
|
|
319
|
+
pending.length > 0
|
|
320
|
+
? `[comms] Pending events:\n${pending.map((l: string) => ` 📬 ${l}`).join("\n")}\n\n`
|
|
321
|
+
: "";
|
|
322
|
+
|
|
323
|
+
return {
|
|
324
|
+
content: [{ type: "text", text: `${prefix}${result.content}` }],
|
|
325
|
+
details: { action: params.action },
|
|
326
|
+
isError: result.isError,
|
|
327
|
+
};
|
|
328
|
+
},
|
|
329
|
+
});
|
|
330
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import * as claudeCode from "./claude-code/channel.js";
|
|
2
|
+
import * as codex from "./codex/tool.js";
|
|
3
|
+
import * as mcp from "./mcp/server.js";
|
|
4
|
+
import * as ccPeer from "./cc-peer/run.js";
|
|
5
|
+
import type { BridgeRunOptions } from "../core/index.js";
|
|
6
|
+
|
|
7
|
+
export interface Bridge {
|
|
8
|
+
/** Starts the bridge. Only a test passes options; a real launch runs it bare. */
|
|
9
|
+
run: (options?: Readonly<BridgeRunOptions>) => void | Promise<void>;
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export const bridges: Record<string, Bridge> = {
|
|
13
|
+
"claude-code": claudeCode,
|
|
14
|
+
codex,
|
|
15
|
+
mcp,
|
|
16
|
+
"cc-peer": ccPeer,
|
|
17
|
+
};
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Non-interactive CLI — one-shot commands that register, execute, print, and exit.
|
|
3
|
+
*
|
|
4
|
+
* Usage:
|
|
5
|
+
* agent-comms send <room> <message>
|
|
6
|
+
* agent-comms dm <agent> <message>
|
|
7
|
+
* agent-comms rooms
|
|
8
|
+
* agent-comms agents
|
|
9
|
+
* agent-comms read <room> [--since <iso>]
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { ChatController } from "./controller.js";
|
|
13
|
+
|
|
14
|
+
export async function runCli(
|
|
15
|
+
command: string,
|
|
16
|
+
args: readonly string[],
|
|
17
|
+
userName: string,
|
|
18
|
+
): Promise<void> {
|
|
19
|
+
const controller = new ChatController(userName);
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
await controller.init();
|
|
23
|
+
|
|
24
|
+
const result = await dispatch(controller, command, args);
|
|
25
|
+
|
|
26
|
+
if (result.isError) {
|
|
27
|
+
console.error(result.content);
|
|
28
|
+
process.exit(1);
|
|
29
|
+
} else {
|
|
30
|
+
console.log(result.content);
|
|
31
|
+
}
|
|
32
|
+
} finally {
|
|
33
|
+
await controller.shutdown();
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
async function dispatch(
|
|
38
|
+
controller: ChatController,
|
|
39
|
+
command: string,
|
|
40
|
+
args: readonly string[],
|
|
41
|
+
): Promise<{ content: string; isError: boolean }> {
|
|
42
|
+
switch (command) {
|
|
43
|
+
case "send": {
|
|
44
|
+
const room = args[0];
|
|
45
|
+
const content = args.slice(1).join(" ");
|
|
46
|
+
if (room === undefined || room === "" || !content) {
|
|
47
|
+
return {
|
|
48
|
+
content: "Usage: agent-comms send <room> <message>",
|
|
49
|
+
isError: true,
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
// Ensure we're in the room
|
|
53
|
+
const joinResult = await controller.switchRoom(room);
|
|
54
|
+
if (joinResult.isError) return joinResult;
|
|
55
|
+
return controller.send(room, content);
|
|
56
|
+
}
|
|
57
|
+
case "dm": {
|
|
58
|
+
const agent = args[0];
|
|
59
|
+
const content = args.slice(1).join(" ");
|
|
60
|
+
if (agent === undefined || agent === "" || !content) {
|
|
61
|
+
return {
|
|
62
|
+
content: "Usage: agent-comms dm <agent-id> <message>",
|
|
63
|
+
isError: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
return controller.dm(agent, content);
|
|
67
|
+
}
|
|
68
|
+
case "rooms":
|
|
69
|
+
return controller.listRooms();
|
|
70
|
+
case "agents":
|
|
71
|
+
return controller.listAgents();
|
|
72
|
+
case "read": {
|
|
73
|
+
const room = args[0];
|
|
74
|
+
if (room === undefined || room === "") {
|
|
75
|
+
return {
|
|
76
|
+
content: "Usage: agent-comms read <room> [--since <iso>]",
|
|
77
|
+
isError: true,
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
const sinceIdx = args.indexOf("--since");
|
|
81
|
+
const since = sinceIdx !== -1 ? args[sinceIdx + 1] : undefined;
|
|
82
|
+
return controller.readRoom({ roomId: room, since });
|
|
83
|
+
}
|
|
84
|
+
default:
|
|
85
|
+
return { content: `Unknown command: ${command}`, isError: true };
|
|
86
|
+
}
|
|
87
|
+
}
|