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,90 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Lazy per-correspondent reply aliases for the default cc-peer front (agent-comms#158) — pure alias-name derivation and the in-memory correspondent↔alias directory the front consults on both the mesh-to-session direction (materialise an alias so the session can address a reply to it) and the session-to-mesh direction (translate a reply arriving on an alias back into the correspondent it stands for and where that reply should go).
|
|
3
|
+
*
|
|
4
|
+
* Deliberately not persisted: aliases are ephemeral by design, lost on restart and re-materialised by the next inbound message from that correspondent (see the issue body's "Rejected alternatives" for why a directory of every mesh agent is not minted upfront). Kept free of any real cc-peer/AliasPool construction, exactly like front.ts's pure decision logic — front-relay.ts and front-controller.ts consult this directory, and front-runtime.ts supplies the real AliasPool the alias names get materialised against.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
import type { DeliveryEvent } from "../../core/types.js";
|
|
8
|
+
|
|
9
|
+
/** How many leading hex characters of a correspondent's device-id go into its alias name -- 48 bits of a SHA-256 hash, chosen purely for a short, readable cc-peer peer name; cc-peer's own registry name field has no length or format restriction beyond non-empty (see AliasStartCommandSchema in cc-peer's alias-ipc schema), so this is a readability choice, not a protocol requirement. */
|
|
10
|
+
const ALIAS_ID_LENGTH = 12;
|
|
11
|
+
|
|
12
|
+
/** cc-peer peer names materialised for reply aliases are prefixed so they read unambiguously as a mesh correspondent, not a real local Claude Code session, in `roster()`/UI listings that show every locally known peer name side by side. */
|
|
13
|
+
const ALIAS_NAME_PREFIX = "mesh-";
|
|
14
|
+
|
|
15
|
+
/** Derives the cc-peer peer name a correspondent's reply alias registers under. Deterministic and side-effect-free: the same correspondent id always derives the same name, so ReplyAliasDirectory only needs to remember the mapping, not invent a fresh name per call. */
|
|
16
|
+
export function deriveAliasName(correspondentId: string): string {
|
|
17
|
+
return `${ALIAS_NAME_PREFIX}${correspondentId.slice(0, ALIAS_ID_LENGTH)}`;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Where a reply arriving on a correspondent's alias should go back to: a DM straight to that correspondent, or a post back into the room the message that produced the alias was itself sent in. A room's own reply-aliased message always names the room it came from, so a reply lands in the same room rather than always falling back to a DM (agent-comms#289). */
|
|
21
|
+
export type ReplyContext =
|
|
22
|
+
{ readonly kind: "dm" } | { readonly kind: "room"; readonly room: string };
|
|
23
|
+
|
|
24
|
+
/** The mesh correspondent and reply context a DeliveryEvent carries, for the event types with a single, repliable-to sender — undefined for every other event type (room membership/status/capability events describe something happening, not a message from one correspondent worth aliasing). */
|
|
25
|
+
export interface ReplyTarget {
|
|
26
|
+
readonly correspondentId: string;
|
|
27
|
+
readonly context: ReplyContext;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Resolves a DeliveryEvent's own reply target, or undefined when the event has no single originating correspondent. A dm event's reply target is always another dm; a room_message event's reply target is the room the message was posted in, since that is what a reply to it should return to, not the sender's own dm channel (agent-comms#289). */
|
|
31
|
+
export function replyTargetForEvent(
|
|
32
|
+
event: Readonly<DeliveryEvent>,
|
|
33
|
+
): ReplyTarget | undefined {
|
|
34
|
+
switch (event.type) {
|
|
35
|
+
case "dm":
|
|
36
|
+
return { correspondentId: event.message.from, context: { kind: "dm" } };
|
|
37
|
+
case "room_message":
|
|
38
|
+
return {
|
|
39
|
+
correspondentId: event.message.from,
|
|
40
|
+
context: { kind: "room", room: event.message.room },
|
|
41
|
+
};
|
|
42
|
+
case "room_invite":
|
|
43
|
+
case "member_joined":
|
|
44
|
+
case "member_left":
|
|
45
|
+
case "room_members":
|
|
46
|
+
case "member_status":
|
|
47
|
+
case "delivery_status":
|
|
48
|
+
case "invite_declined":
|
|
49
|
+
case "name_changed":
|
|
50
|
+
case "connection_request":
|
|
51
|
+
case "capability_request":
|
|
52
|
+
case "room_join_request":
|
|
53
|
+
return undefined;
|
|
54
|
+
default:
|
|
55
|
+
return event satisfies never;
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Bounded, in-memory, bijective map between the cc-peer peer names materialised for a front's reply aliases and the mesh correspondent id each one stands for, plus the reply context (dm vs. room) each alias currently carries. Bounded by real correspondents, per the issue's own "Rejected alternatives": a name is only ever minted by ensure(), which the front calls solely on genuine inbound contact from that correspondent — nothing pre-populates this directory with the wider mesh roster.
|
|
61
|
+
*
|
|
62
|
+
* A single correspondent id can reach a fronted session two different ways — a direct dm, and a message posted in a room they share with the session — and both share the same alias, since the alias scheme is keyed on the correspondent alone (agent-comms#158). ensure() therefore always records the context of the *most recent* event sent from that alias: a session's reply always continues whatever conversation it was last shown on that alias, so the context that matters is the one behind the last message it actually saw, not whichever context first minted the name.
|
|
63
|
+
*/
|
|
64
|
+
export class ReplyAliasDirectory {
|
|
65
|
+
private readonly nameToCorrespondent = new Map<string, string>();
|
|
66
|
+
private readonly correspondentToName = new Map<string, string>();
|
|
67
|
+
private readonly nameToContext = new Map<string, ReplyContext>();
|
|
68
|
+
|
|
69
|
+
/** Returns the alias name for a correspondent, deriving and recording it the first time this correspondent is seen, and records `context` as that alias's current reply target regardless of whether the alias was just minted or already existed. Idempotent on the name itself: a correspondent already known always gets back its existing name, so a session's reply target stays stable across repeated inbound contact within the same front lifetime; the context, however, is refreshed on every call. */
|
|
70
|
+
ensure(correspondentId: string, context: ReplyContext): string {
|
|
71
|
+
const existing = this.correspondentToName.get(correspondentId);
|
|
72
|
+
const name = existing ?? deriveAliasName(correspondentId);
|
|
73
|
+
if (existing === undefined) {
|
|
74
|
+
this.correspondentToName.set(correspondentId, name);
|
|
75
|
+
this.nameToCorrespondent.set(name, correspondentId);
|
|
76
|
+
}
|
|
77
|
+
this.nameToContext.set(name, context);
|
|
78
|
+
return name;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The correspondent a previously-minted alias name stands for, or undefined for a name this directory never minted (or no longer remembers, e.g. after a front restart cleared it) -- the front's own signal to treat an inbound reply as addressed to a stale alias rather than a known correspondent. */
|
|
82
|
+
correspondentFor(aliasName: string): string | undefined {
|
|
83
|
+
return this.nameToCorrespondent.get(aliasName);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** The reply context a previously-minted alias name currently carries (dm or a named room), or undefined for a name this directory never minted. Always defined whenever correspondentFor(aliasName) is, since both are set together by ensure(). */
|
|
87
|
+
contextFor(aliasName: string): ReplyContext | undefined {
|
|
88
|
+
return this.nameToContext.get(aliasName);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* cc-peer bridge — CLI entry point.
|
|
3
|
+
*
|
|
4
|
+
* Run via: npx agent-comms bridge cc-peer <target-name> npx agent-comms bridge cc-peer --pid=<pid>
|
|
5
|
+
*
|
|
6
|
+
* Constructs the real CcPeer and MeshStore, then hands both to wireCcPeerBridge for the actual relay wiring (see bridge.ts).
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import { CcPeer, CC_PEER_VERSION } from "cc-peer";
|
|
10
|
+
import {
|
|
11
|
+
createBridgeMesh,
|
|
12
|
+
createMeshErrorReporter,
|
|
13
|
+
ensureRegistered,
|
|
14
|
+
ensureProjectRoom,
|
|
15
|
+
installShutdownSignalHandlers,
|
|
16
|
+
} from "../../core/index.js";
|
|
17
|
+
import type { IdentitySlot } from "../../core/identity-store.js";
|
|
18
|
+
import { releaseIdentityLock } from "../../core/identity-store.js";
|
|
19
|
+
import {
|
|
20
|
+
createTargetSenderMatcher,
|
|
21
|
+
targetMatchesEntry,
|
|
22
|
+
wireCcPeerBridge,
|
|
23
|
+
type CcPeerRef,
|
|
24
|
+
} from "./bridge.js";
|
|
25
|
+
import { wireDefaultCcPeerFront } from "./default-front.js";
|
|
26
|
+
|
|
27
|
+
const PID_FLAG_PREFIX = "--pid=";
|
|
28
|
+
/** 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. */
|
|
29
|
+
const BRIDGE_ARGS_START_INDEX = 4;
|
|
30
|
+
/** The name this command registers its one cc-peer peer under, which the target session is told to message to answer a join request. */
|
|
31
|
+
const BRIDGE_PEER_NAME = "agent-comms-bridge";
|
|
32
|
+
|
|
33
|
+
function parseTarget(argv: readonly string[]): CcPeerRef {
|
|
34
|
+
const arg = argv[0];
|
|
35
|
+
if (arg === undefined || arg === "") {
|
|
36
|
+
console.error(
|
|
37
|
+
"Usage: agent-comms bridge cc-peer <target-name> | --pid=<pid>",
|
|
38
|
+
);
|
|
39
|
+
process.exit(1);
|
|
40
|
+
}
|
|
41
|
+
if (arg.startsWith(PID_FLAG_PREFIX)) {
|
|
42
|
+
const pid = Number(arg.slice(PID_FLAG_PREFIX.length));
|
|
43
|
+
if (!Number.isInteger(pid) || pid <= 0) {
|
|
44
|
+
console.error(`Invalid --pid value: ${arg}`);
|
|
45
|
+
process.exit(1);
|
|
46
|
+
}
|
|
47
|
+
return { pid };
|
|
48
|
+
}
|
|
49
|
+
return { name: arg };
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export async function run(): Promise<void> {
|
|
53
|
+
const target = parseTarget(process.argv.slice(BRIDGE_ARGS_START_INDEX));
|
|
54
|
+
|
|
55
|
+
const identitySlot: IdentitySlot = { harness: "cc-peer", cwd: process.cwd() };
|
|
56
|
+
// cc-peer allows one peer per process, so this is the only one: the default front borrows it below rather than creating its own, and the front leaves this bridge's own target alone since the relay below already covers it.
|
|
57
|
+
const peer = await CcPeer.create({ name: BRIDGE_PEER_NAME });
|
|
58
|
+
const { store, tool } = await createBridgeMesh(identitySlot);
|
|
59
|
+
store.onError = createMeshErrorReporter();
|
|
60
|
+
store.getCcPeerVersion = () => CC_PEER_VERSION;
|
|
61
|
+
store.onCoordinatorRoleChanged = wireDefaultCcPeerFront(store, {
|
|
62
|
+
peer,
|
|
63
|
+
peerName: BRIDGE_PEER_NAME,
|
|
64
|
+
excludeSession: (entry) => targetMatchesEntry(target, entry),
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
const reg = await ensureRegistered({
|
|
68
|
+
store,
|
|
69
|
+
cwd: process.cwd(),
|
|
70
|
+
harness: "cc-peer",
|
|
71
|
+
defaultName: "cc-peer-bridge",
|
|
72
|
+
});
|
|
73
|
+
const roomId = await ensureProjectRoom(store, reg.agentId, process.cwd());
|
|
74
|
+
|
|
75
|
+
wireCcPeerBridge({
|
|
76
|
+
store,
|
|
77
|
+
tool,
|
|
78
|
+
peer,
|
|
79
|
+
agentId: reg.agentId,
|
|
80
|
+
roomId,
|
|
81
|
+
target,
|
|
82
|
+
cwd: process.cwd(),
|
|
83
|
+
peerName: BRIDGE_PEER_NAME,
|
|
84
|
+
isFromTarget: createTargetSenderMatcher(target, async () => peer.roster()),
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
await store.init();
|
|
88
|
+
|
|
89
|
+
installShutdownSignalHandlers({
|
|
90
|
+
shutdown: async () => {
|
|
91
|
+
await peer.stop();
|
|
92
|
+
await store.setAgentOffline(reg.agentId);
|
|
93
|
+
// After store.shutdown(), not before: the mesh handover this bridge owes its peers rides the sessions shutdown() closes, and the lock is what stops a replacement claiming this slot's identity while that is still in flight.
|
|
94
|
+
await store.shutdown();
|
|
95
|
+
releaseIdentityLock(identitySlot);
|
|
96
|
+
},
|
|
97
|
+
disposition: "exit",
|
|
98
|
+
onError: createMeshErrorReporter(),
|
|
99
|
+
});
|
|
100
|
+
}
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SharedPeer -- a lazily-created, process-wide holder of one cc-peer peer.
|
|
3
|
+
*
|
|
4
|
+
* cc-peer binds one socket per process (`<socket dir>/<pid>.sock`) and registers one session entry per pid, so a second CcPeer.create in the same process fails with EADDRINUSE. Every role in a process that talks through cc-peer (the default front, the one-shot `bridge cc-peer` command) therefore has to go through a single instance.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
/** Holds at most one peer of type P, created by the injected factory on first use. */
|
|
8
|
+
export class SharedPeer<P> {
|
|
9
|
+
private promise: Promise<P> | undefined;
|
|
10
|
+
|
|
11
|
+
constructor(private readonly create: () => Promise<P>) {}
|
|
12
|
+
|
|
13
|
+
/** The shared peer, creating it on the first call. Concurrent callers share one creation. A failed creation is forgotten, so the next call retries rather than replaying the same rejection forever. */
|
|
14
|
+
async get(): Promise<P> {
|
|
15
|
+
this.promise ??= this.create().catch((error: unknown) => {
|
|
16
|
+
this.promise = undefined;
|
|
17
|
+
throw error;
|
|
18
|
+
});
|
|
19
|
+
return this.promise;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/** Forgets the shared peer and returns it so the caller can stop it, or undefined if none was ever successfully created. The next get() creates a fresh one. */
|
|
23
|
+
async release(): Promise<P | undefined> {
|
|
24
|
+
const pending = this.promise;
|
|
25
|
+
this.promise = undefined;
|
|
26
|
+
return pending?.catch(() => undefined);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
@@ -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
|
+
}
|