agent-inbox 0.0.1 → 0.1.1

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.
Files changed (126) hide show
  1. package/CLAUDE.md +113 -0
  2. package/README.md +195 -1
  3. package/dist/federation/address.d.ts +24 -0
  4. package/dist/federation/address.d.ts.map +1 -0
  5. package/dist/federation/address.js +54 -0
  6. package/dist/federation/address.js.map +1 -0
  7. package/dist/federation/connection-manager.d.ts +118 -0
  8. package/dist/federation/connection-manager.d.ts.map +1 -0
  9. package/dist/federation/connection-manager.js +369 -0
  10. package/dist/federation/connection-manager.js.map +1 -0
  11. package/dist/federation/delivery-queue.d.ts +66 -0
  12. package/dist/federation/delivery-queue.d.ts.map +1 -0
  13. package/dist/federation/delivery-queue.js +199 -0
  14. package/dist/federation/delivery-queue.js.map +1 -0
  15. package/dist/federation/index.d.ts +7 -0
  16. package/dist/federation/index.d.ts.map +1 -0
  17. package/dist/federation/index.js +6 -0
  18. package/dist/federation/index.js.map +1 -0
  19. package/dist/federation/routing-engine.d.ts +74 -0
  20. package/dist/federation/routing-engine.d.ts.map +1 -0
  21. package/dist/federation/routing-engine.js +158 -0
  22. package/dist/federation/routing-engine.js.map +1 -0
  23. package/dist/federation/trust.d.ts +39 -0
  24. package/dist/federation/trust.d.ts.map +1 -0
  25. package/dist/federation/trust.js +64 -0
  26. package/dist/federation/trust.js.map +1 -0
  27. package/dist/index.d.ts +60 -2
  28. package/dist/index.d.ts.map +1 -0
  29. package/dist/index.js +217 -18
  30. package/dist/index.js.map +1 -1
  31. package/dist/ipc/ipc-server.d.ts +20 -0
  32. package/dist/ipc/ipc-server.d.ts.map +1 -0
  33. package/dist/ipc/ipc-server.js +152 -0
  34. package/dist/ipc/ipc-server.js.map +1 -0
  35. package/dist/jsonrpc/mail-server.d.ts +45 -0
  36. package/dist/jsonrpc/mail-server.d.ts.map +1 -0
  37. package/dist/jsonrpc/mail-server.js +284 -0
  38. package/dist/jsonrpc/mail-server.js.map +1 -0
  39. package/dist/map/map-client.d.ts +91 -0
  40. package/dist/map/map-client.d.ts.map +1 -0
  41. package/dist/map/map-client.js +202 -0
  42. package/dist/map/map-client.js.map +1 -0
  43. package/dist/mcp/mcp-server.d.ts +23 -0
  44. package/dist/mcp/mcp-server.d.ts.map +1 -0
  45. package/dist/mcp/mcp-server.js +226 -0
  46. package/dist/mcp/mcp-server.js.map +1 -0
  47. package/dist/push/notifier.d.ts +49 -0
  48. package/dist/push/notifier.d.ts.map +1 -0
  49. package/dist/push/notifier.js +150 -0
  50. package/dist/push/notifier.js.map +1 -0
  51. package/dist/registry/warm-registry.d.ts +63 -0
  52. package/dist/registry/warm-registry.d.ts.map +1 -0
  53. package/dist/registry/warm-registry.js +173 -0
  54. package/dist/registry/warm-registry.js.map +1 -0
  55. package/dist/router/message-router.d.ts +44 -0
  56. package/dist/router/message-router.d.ts.map +1 -0
  57. package/dist/router/message-router.js +137 -0
  58. package/dist/router/message-router.js.map +1 -0
  59. package/dist/storage/interface.d.ts +31 -0
  60. package/dist/storage/interface.d.ts.map +1 -0
  61. package/dist/storage/interface.js +2 -0
  62. package/dist/storage/interface.js.map +1 -0
  63. package/dist/storage/memory.d.ts +28 -0
  64. package/dist/storage/memory.d.ts.map +1 -0
  65. package/dist/storage/memory.js +118 -0
  66. package/dist/storage/memory.js.map +1 -0
  67. package/dist/storage/sqlite.d.ts +35 -0
  68. package/dist/storage/sqlite.d.ts.map +1 -0
  69. package/dist/storage/sqlite.js +445 -0
  70. package/dist/storage/sqlite.js.map +1 -0
  71. package/dist/traceability/traceability.d.ts +29 -0
  72. package/dist/traceability/traceability.d.ts.map +1 -0
  73. package/dist/traceability/traceability.js +150 -0
  74. package/dist/traceability/traceability.js.map +1 -0
  75. package/dist/types.d.ts +253 -0
  76. package/dist/types.d.ts.map +1 -0
  77. package/dist/types.js +3 -0
  78. package/dist/types.js.map +1 -0
  79. package/docs/DESIGN.md +1156 -0
  80. package/docs/PLAN.md +545 -0
  81. package/hooks/inbox-hook.mjs +119 -0
  82. package/hooks/register-hook.mjs +69 -0
  83. package/package.json +33 -25
  84. package/rules/agent-inbox.md +78 -0
  85. package/src/federation/address.ts +61 -0
  86. package/src/federation/connection-manager.ts +458 -0
  87. package/src/federation/delivery-queue.ts +222 -0
  88. package/src/federation/index.ts +6 -0
  89. package/src/federation/routing-engine.ts +188 -0
  90. package/src/federation/trust.ts +71 -0
  91. package/src/index.ts +299 -0
  92. package/src/ipc/ipc-server.ts +180 -0
  93. package/src/jsonrpc/mail-server.ts +356 -0
  94. package/src/map/map-client.ts +260 -0
  95. package/src/mcp/mcp-server.ts +272 -0
  96. package/src/push/notifier.ts +192 -0
  97. package/src/registry/warm-registry.ts +210 -0
  98. package/src/router/message-router.ts +175 -0
  99. package/src/storage/interface.ts +48 -0
  100. package/src/storage/memory.ts +145 -0
  101. package/src/storage/sqlite.ts +645 -0
  102. package/src/traceability/traceability.ts +183 -0
  103. package/src/types.ts +287 -0
  104. package/test/federation/address.test.ts +101 -0
  105. package/test/federation/connection-manager.test.ts +546 -0
  106. package/test/federation/delivery-queue.test.ts +159 -0
  107. package/test/federation/integration.test.ts +823 -0
  108. package/test/federation/routing-engine.test.ts +117 -0
  109. package/test/federation/sdk-integration.test.ts +748 -0
  110. package/test/federation/trust.test.ts +89 -0
  111. package/test/ipc-jsonrpc.test.ts +113 -0
  112. package/test/ipc-server.test.ts +138 -0
  113. package/test/mail-server.test.ts +208 -0
  114. package/test/map-client.test.ts +408 -0
  115. package/test/message-router.test.ts +184 -0
  116. package/test/push-notifier.test.ts +139 -0
  117. package/test/registry/warm-registry.test.ts +171 -0
  118. package/test/sqlite-storage.test.ts +243 -0
  119. package/test/storage.test.ts +196 -0
  120. package/test/traceability.test.ts +123 -0
  121. package/tsconfig.json +20 -0
  122. package/tsup.config.ts +10 -0
  123. package/vitest.config.ts +8 -0
  124. package/dist/index.d.mts +0 -2
  125. package/dist/index.mjs +0 -1
  126. package/dist/index.mjs.map +0 -1
@@ -0,0 +1,188 @@
1
+ import { EventEmitter } from "node:events";
2
+ import type {
3
+ FederatedAddress,
4
+ FederationRoutingConfig,
5
+ RoutingEntry,
6
+ WarmAgentStatus,
7
+ } from "../types.js";
8
+
9
+ const DEFAULT_CONFIG: FederationRoutingConfig = {
10
+ strategy: "table",
11
+ tableTTL: 300_000, // 5 minutes
12
+ refreshOnMiss: true,
13
+ broadcastTimeout: 5_000,
14
+ };
15
+
16
+ /**
17
+ * Federation routing engine with configurable strategies.
18
+ *
19
+ * - table: In-memory routing table populated from federation exposure data
20
+ * - broadcast: Forward to all peers, first responder wins
21
+ * - hierarchical: Local table first, then delegate to upstream hubs
22
+ */
23
+ export class RoutingEngine {
24
+ private table = new Map<string, RoutingEntry>();
25
+ private config: FederationRoutingConfig;
26
+
27
+ constructor(
28
+ private events: EventEmitter,
29
+ config?: Partial<FederationRoutingConfig>
30
+ ) {
31
+ this.config = { ...DEFAULT_CONFIG, ...config };
32
+ }
33
+
34
+ /**
35
+ * Resolve a federated address to a peer system ID.
36
+ * Returns null if no route found.
37
+ */
38
+ resolveRoute(addr: FederatedAddress): string | null {
39
+ // If the address specifies a system directly, use it
40
+ if (addr.system) {
41
+ return addr.system;
42
+ }
43
+
44
+ // Otherwise, look up the agent in the routing table
45
+ if (addr.agent) {
46
+ return this.lookupAgent(addr.agent);
47
+ }
48
+
49
+ return null;
50
+ }
51
+
52
+ /**
53
+ * Look up an agent in the routing table.
54
+ * Respects TTL — expired entries are not returned.
55
+ */
56
+ lookupAgent(agentId: string): string | null {
57
+ const entry = this.table.get(agentId);
58
+ if (!entry) return null;
59
+
60
+ // Check TTL
61
+ const age = Date.now() - new Date(entry.lastSeen).getTime();
62
+ if (age > (this.config.tableTTL ?? DEFAULT_CONFIG.tableTTL!)) {
63
+ this.table.delete(agentId);
64
+ return null;
65
+ }
66
+
67
+ // Only route to active or away agents
68
+ if (entry.status === "expired") return null;
69
+
70
+ return entry.peerId;
71
+ }
72
+
73
+ /**
74
+ * Update the routing table with exposure data from a peer.
75
+ * Called when federation/connect completes or peer sends updates.
76
+ */
77
+ updateFromExposure(
78
+ peerId: string,
79
+ agents: Array<{ agentId: string; status?: WarmAgentStatus }>
80
+ ): void {
81
+ const now = new Date().toISOString();
82
+ for (const agent of agents) {
83
+ this.table.set(agent.agentId, {
84
+ agentId: agent.agentId,
85
+ peerId,
86
+ lastSeen: now,
87
+ status: agent.status ?? "active",
88
+ });
89
+ }
90
+ this.events.emit("routing.updated", { peerId, agentCount: agents.length });
91
+ }
92
+
93
+ /**
94
+ * Update status for a specific agent in the routing table.
95
+ */
96
+ updateAgentStatus(agentId: string, status: WarmAgentStatus): void {
97
+ const entry = this.table.get(agentId);
98
+ if (entry) {
99
+ entry.status = status;
100
+ entry.lastSeen = new Date().toISOString();
101
+ }
102
+ }
103
+
104
+ /**
105
+ * Remove all routing entries for a peer (e.g., on disconnect).
106
+ */
107
+ removePeer(peerId: string): number {
108
+ let removed = 0;
109
+ for (const [agentId, entry] of this.table.entries()) {
110
+ if (entry.peerId === peerId) {
111
+ this.table.delete(agentId);
112
+ removed++;
113
+ }
114
+ }
115
+ return removed;
116
+ }
117
+
118
+ /**
119
+ * Get all known peers from the routing table.
120
+ */
121
+ knownPeers(): string[] {
122
+ const peers = new Set<string>();
123
+ for (const entry of this.table.values()) {
124
+ peers.add(entry.peerId);
125
+ }
126
+ return Array.from(peers);
127
+ }
128
+
129
+ /**
130
+ * Get all routing entries (for debugging/inspection).
131
+ */
132
+ getTable(): RoutingEntry[] {
133
+ return Array.from(this.table.values());
134
+ }
135
+
136
+ /**
137
+ * Get the routing strategy in use.
138
+ */
139
+ getStrategy(): FederationRoutingConfig["strategy"] {
140
+ return this.config.strategy;
141
+ }
142
+
143
+ /**
144
+ * Get configured upstream hubs (for hierarchical strategy).
145
+ */
146
+ getUpstream(): string[] {
147
+ return this.config.upstream ?? [];
148
+ }
149
+
150
+ /**
151
+ * Whether to query peers on cache miss (for table strategy).
152
+ */
153
+ shouldRefreshOnMiss(): boolean {
154
+ return this.config.refreshOnMiss ?? true;
155
+ }
156
+
157
+ /**
158
+ * Get broadcast timeout (for broadcast strategy).
159
+ */
160
+ getBroadcastTimeout(): number {
161
+ return this.config.broadcastTimeout ?? 5_000;
162
+ }
163
+
164
+ /**
165
+ * Clean expired entries from the routing table.
166
+ */
167
+ cleanup(): number {
168
+ const ttl = this.config.tableTTL ?? DEFAULT_CONFIG.tableTTL!;
169
+ const now = Date.now();
170
+ let removed = 0;
171
+
172
+ for (const [agentId, entry] of this.table.entries()) {
173
+ const age = now - new Date(entry.lastSeen).getTime();
174
+ if (age > ttl) {
175
+ this.table.delete(agentId);
176
+ removed++;
177
+ }
178
+ }
179
+ return removed;
180
+ }
181
+
182
+ /**
183
+ * Clear all state.
184
+ */
185
+ destroy(): void {
186
+ this.table.clear();
187
+ }
188
+ }
@@ -0,0 +1,71 @@
1
+ import type { FederationTrustPolicy } from "../types.js";
2
+
3
+ const DEFAULT_POLICY: FederationTrustPolicy = {
4
+ allowedServers: [],
5
+ scopePermissions: {},
6
+ requireAuth: false,
7
+ };
8
+
9
+ /**
10
+ * Federation trust enforcement.
11
+ *
12
+ * Layer 1 (implemented): Server allow-list
13
+ * Layer 2 (stub): Scope-based permissions
14
+ * Layer 3 (stub): Token-based auth
15
+ */
16
+ export class TrustManager {
17
+ private policy: FederationTrustPolicy;
18
+
19
+ constructor(policy?: Partial<FederationTrustPolicy>) {
20
+ this.policy = { ...DEFAULT_POLICY, ...policy };
21
+ }
22
+
23
+ /**
24
+ * Check if a system is allowed to connect (Layer 1).
25
+ * If allowedServers is empty, all servers are allowed (open federation).
26
+ */
27
+ canConnect(systemId: string): boolean {
28
+ if (this.policy.allowedServers.length === 0) return true;
29
+ return this.policy.allowedServers.includes(systemId);
30
+ }
31
+
32
+ /**
33
+ * Check if routing from a remote system to a local scope is allowed (Layer 2 — stub).
34
+ * Currently always returns true.
35
+ */
36
+ canRoute(fromSystem: string, toScope: string): boolean {
37
+ // Stub: check scope permissions when implemented
38
+ const allowed = this.policy.scopePermissions[toScope];
39
+ if (!allowed) return true; // No restrictions defined = allow all
40
+ return allowed.includes(fromSystem);
41
+ }
42
+
43
+ /**
44
+ * Validate authentication credentials for a system (Layer 3 — stub).
45
+ * Currently always returns true unless requireAuth is explicitly set.
46
+ */
47
+ validateAuth(
48
+ systemId: string,
49
+ _credentials?: { method?: string; token?: string }
50
+ ): boolean {
51
+ if (!this.policy.requireAuth) return true;
52
+ // Stub: when implemented, validate credentials against authTokens
53
+ const expectedToken = this.policy.authTokens?.[systemId];
54
+ if (!expectedToken) return true; // No token configured = allow
55
+ return _credentials?.token === expectedToken;
56
+ }
57
+
58
+ /**
59
+ * Update the trust policy at runtime.
60
+ */
61
+ updatePolicy(updates: Partial<FederationTrustPolicy>): void {
62
+ Object.assign(this.policy, updates);
63
+ }
64
+
65
+ /**
66
+ * Get current policy (for inspection/debugging).
67
+ */
68
+ getPolicy(): Readonly<FederationTrustPolicy> {
69
+ return this.policy;
70
+ }
71
+ }
package/src/index.ts ADDED
@@ -0,0 +1,299 @@
1
+ import { EventEmitter } from "node:events";
2
+ import * as path from "node:path";
3
+ import * as os from "node:os";
4
+ import { InMemoryStorage } from "./storage/memory.js";
5
+ import { SqliteStorage } from "./storage/sqlite.js";
6
+ import { MessageRouter } from "./router/message-router.js";
7
+ import { TraceabilityLayer } from "./traceability/traceability.js";
8
+ import { IpcServer } from "./ipc/ipc-server.js";
9
+ import { MapClient } from "./map/map-client.js";
10
+ import { InboxMcpServer } from "./mcp/mcp-server.js";
11
+ import { MailJsonRpcServer } from "./jsonrpc/mail-server.js";
12
+ import { PushNotifier } from "./push/notifier.js";
13
+ import { ConnectionManager } from "./federation/connection-manager.js";
14
+ import { WarmRegistry } from "./registry/warm-registry.js";
15
+ import type { InboxConfig } from "./types.js";
16
+ import type { Storage } from "./storage/interface.js";
17
+
18
+ export { InMemoryStorage } from "./storage/memory.js";
19
+ export { SqliteStorage } from "./storage/sqlite.js";
20
+ export { MessageRouter, normalizeContent } from "./router/message-router.js";
21
+ export { TraceabilityLayer } from "./traceability/traceability.js";
22
+ export { IpcServer } from "./ipc/ipc-server.js";
23
+ export { MapClient } from "./map/map-client.js";
24
+ export { InboxMcpServer } from "./mcp/mcp-server.js";
25
+ export { MailJsonRpcServer } from "./jsonrpc/mail-server.js";
26
+ export { PushNotifier, formatInboxMarkdown } from "./push/notifier.js";
27
+ export { ConnectionManager } from "./federation/connection-manager.js";
28
+ export type { DeliveryResult, IncomingMessageHandler } from "./federation/connection-manager.js";
29
+ export type { MapConnection, MapAgentConnectionClass, IncomingMapMessage } from "./map/map-client.js";
30
+ export { WarmRegistry } from "./registry/warm-registry.js";
31
+ export { RoutingEngine } from "./federation/routing-engine.js";
32
+ export { DeliveryQueue } from "./federation/delivery-queue.js";
33
+ export { TrustManager } from "./federation/trust.js";
34
+ export {
35
+ parseAddress,
36
+ formatAddress,
37
+ isRemoteAddress,
38
+ isBroadcastAddress,
39
+ } from "./federation/address.js";
40
+ export type * from "./types.js";
41
+ export type { Storage, InboxQuery, ThreadQuery } from "./storage/interface.js";
42
+
43
+ function defaultSocketPath(): string {
44
+ const home = os.homedir();
45
+ return path.join(home, ".claude", "agent-inbox", "inbox.sock");
46
+ }
47
+
48
+ function defaultInboxJsonlPath(): string {
49
+ const home = os.homedir();
50
+ return path.join(home, ".claude", "agent-inbox", "inbox.jsonl");
51
+ }
52
+
53
+ function defaultInboxDir(): string {
54
+ const home = os.homedir();
55
+ return path.join(home, ".claude", "agent-inbox", "inboxes");
56
+ }
57
+
58
+ function defaultSqlitePath(): string {
59
+ const home = os.homedir();
60
+ return path.join(home, ".claude", "agent-inbox", "inbox.db");
61
+ }
62
+
63
+ function loadConfig(): InboxConfig {
64
+ return {
65
+ socketPath: process.env.INBOX_SOCKET_PATH ?? defaultSocketPath(),
66
+ scope: process.env.INBOX_SCOPE ?? "default",
67
+ map: {
68
+ enabled: process.env.INBOX_MAP_ENABLED === "true",
69
+ server: process.env.INBOX_MAP_SERVER,
70
+ scope: process.env.INBOX_MAP_SCOPE ?? process.env.INBOX_SCOPE ?? "default",
71
+ systemId: process.env.INBOX_MAP_SYSTEM_ID ?? "agent-inbox",
72
+ },
73
+ };
74
+ }
75
+
76
+ export interface AgentInbox {
77
+ storage: Storage;
78
+ router: MessageRouter;
79
+ traceability: TraceabilityLayer;
80
+ ipcServer: IpcServer;
81
+ mapClient: MapClient;
82
+ jsonRpc: MailJsonRpcServer;
83
+ notifier: PushNotifier;
84
+ federation: ConnectionManager | null;
85
+ registry: WarmRegistry | null;
86
+ events: EventEmitter;
87
+ stop(): Promise<void>;
88
+ }
89
+
90
+ export interface CreateOptions {
91
+ config?: Partial<InboxConfig>;
92
+ /** Use SQLite storage at this path (":memory:" for in-memory SQLite) */
93
+ sqlitePath?: string;
94
+ /** HTTP port for JSON-RPC endpoint (0 = disabled) */
95
+ httpPort?: number;
96
+ /** Webhook URLs for push notifications */
97
+ webhooks?: string[];
98
+ /** Enable federation with peer systems */
99
+ enableFederation?: boolean;
100
+ /** Use an externally-managed MAP connection instead of creating one.
101
+ * When provided, Agent Inbox borrows this connection for messaging
102
+ * but does not own its lifecycle (won't disconnect it on stop()). */
103
+ connection?: import("./map/map-client.js").MapConnection;
104
+ }
105
+
106
+ export async function createAgentInbox(
107
+ opts: CreateOptions = {}
108
+ ): Promise<AgentInbox> {
109
+ const config = { ...loadConfig(), ...opts.config };
110
+
111
+ // 1. Storage — SQLite if path provided, otherwise in-memory
112
+ const storage: Storage = opts.sqlitePath
113
+ ? new SqliteStorage({ path: opts.sqlitePath })
114
+ : new InMemoryStorage();
115
+
116
+ // 2. Event bus (internal)
117
+ const events = new EventEmitter();
118
+
119
+ // 3. Message router
120
+ const router = new MessageRouter(storage, events, config.scope);
121
+
122
+ // 4. Traceability layer (subscribes to message.created events)
123
+ const traceability = new TraceabilityLayer(storage, events);
124
+
125
+ // 5. Push notifier (per-agent inbox files + webhooks)
126
+ const notifier = new PushNotifier(
127
+ { inboxDir: defaultInboxDir(), webhooks: opts.webhooks },
128
+ storage,
129
+ events
130
+ );
131
+
132
+ // 6. MAP client (connects first to obtain SDK class for federation)
133
+ const mapClient = new MapClient(
134
+ storage,
135
+ router,
136
+ events,
137
+ defaultInboxJsonlPath()
138
+ );
139
+ if (opts.connection) {
140
+ // External connection — borrow it, don't manage its lifecycle
141
+ mapClient.useConnection(opts.connection);
142
+ } else if (config.map?.enabled) {
143
+ const connected = await mapClient.connect(config.map);
144
+ if (connected) {
145
+ // Replay missed messages from MAP server
146
+ const replayed = await mapClient.replayMissed();
147
+ if (replayed > 0) {
148
+ console.error(`Replayed ${replayed} missed messages from MAP server`);
149
+ }
150
+ }
151
+ }
152
+
153
+ // 7. Federation (optional) — inject SDK class from MapClient if available
154
+ let federation: ConnectionManager | null = null;
155
+ let registry: WarmRegistry | null = null;
156
+
157
+ if (opts.enableFederation || config.federation?.peers?.length) {
158
+ registry = new WarmRegistry(storage, events, config.federation?.registry);
159
+
160
+ const sdkClass = mapClient.getAgentConnectionClass() ?? undefined;
161
+
162
+ federation = new ConnectionManager(events, config.federation, {
163
+ sdkClass,
164
+ onIncomingMessage: ({ from, peerId, payload, meta }) => {
165
+ // Delegate incoming federation messages to the router.
166
+ // The targetAgent in meta tells us which local agent this is for.
167
+ const targetAgent = (meta?.targetAgent as string) ?? from;
168
+ router.routeMessage({
169
+ from: `${from}@${peerId}`,
170
+ to: targetAgent,
171
+ payload,
172
+ scope: meta?.scope as string | undefined,
173
+ subject: meta?.subject as string | undefined,
174
+ importance: meta?.importance as "high" | "normal" | undefined,
175
+ threadTag: meta?.threadTag as string | undefined,
176
+ inReplyTo: meta?.inReplyTo as string | undefined,
177
+ metadata: meta,
178
+ }).catch((err) => {
179
+ console.error(
180
+ `Failed to route incoming federation message from ${from}@${peerId}: ${err instanceof Error ? err.message : err}`
181
+ );
182
+ });
183
+ },
184
+ });
185
+ router.setFederation(federation);
186
+
187
+ // Tier 2 system ID resolution: update from MAP systemInfo if available
188
+ const mapSystemName = mapClient.getSystemName();
189
+ if (mapSystemName) {
190
+ federation.updateSystemIdFromMap(mapSystemName);
191
+ }
192
+
193
+ // Connect to configured federation peers
194
+ if (config.federation?.peers) {
195
+ for (const peer of config.federation.peers) {
196
+ try {
197
+ await federation.federate(peer);
198
+ console.error(`Federated with peer: ${peer.systemId} (${peer.url})`);
199
+ } catch (err) {
200
+ console.error(
201
+ `Failed to federate with ${peer.systemId}: ${err instanceof Error ? err.message : err}`
202
+ );
203
+ }
204
+ }
205
+ }
206
+
207
+ // Start delivery queue tick timer
208
+ federation.queue.startTicking();
209
+ }
210
+
211
+ // 8. JSON-RPC server (mail/* methods)
212
+ const jsonRpc = new MailJsonRpcServer(storage, router, events);
213
+ if (opts.httpPort && opts.httpPort > 0) {
214
+ await jsonRpc.startHttp(opts.httpPort);
215
+ console.error(`JSON-RPC HTTP server listening on port ${opts.httpPort}`);
216
+ }
217
+
218
+ // 9. IPC server (UNIX socket — handles both NDJSON commands and JSON-RPC inline)
219
+ const socketPath = config.socketPath ?? defaultSocketPath();
220
+ const ipcServer = new IpcServer(socketPath, router, storage, jsonRpc);
221
+ await ipcServer.start();
222
+
223
+ const stop = async () => {
224
+ await ipcServer.stop();
225
+ await jsonRpc.stopHttp();
226
+ await mapClient.disconnect();
227
+ if (federation) await federation.destroy();
228
+ if (registry) registry.destroy();
229
+ if ("close" in storage && typeof storage.close === "function") {
230
+ (storage as SqliteStorage).close();
231
+ }
232
+ };
233
+
234
+ return {
235
+ storage,
236
+ router,
237
+ traceability,
238
+ ipcServer,
239
+ mapClient,
240
+ jsonRpc,
241
+ notifier,
242
+ federation,
243
+ registry,
244
+ events,
245
+ stop,
246
+ };
247
+ }
248
+
249
+ // CLI entry point
250
+ const isMainModule =
251
+ process.argv[1] &&
252
+ (process.argv[1].endsWith("/index.js") ||
253
+ process.argv[1].endsWith("/index.ts"));
254
+
255
+ if (isMainModule) {
256
+ const mode = process.argv[2] ?? "ipc";
257
+
258
+ if (mode === "mcp") {
259
+ // MCP-only mode (stdio transport for Claude Code integration)
260
+ const config = loadConfig();
261
+ const sqlitePath = process.env.INBOX_SQLITE_PATH;
262
+ const storage: Storage = sqlitePath
263
+ ? new SqliteStorage({ path: sqlitePath })
264
+ : new InMemoryStorage();
265
+ const events = new EventEmitter();
266
+ const router = new MessageRouter(storage, events, config.scope);
267
+ const _traceability = new TraceabilityLayer(storage, events);
268
+ const mcpServer = new InboxMcpServer(router, storage, config.scope);
269
+ mcpServer.start().catch((err) => {
270
+ console.error("MCP server failed:", err);
271
+ process.exit(1);
272
+ });
273
+ } else {
274
+ // IPC server mode (default)
275
+ const httpPort = process.env.INBOX_HTTP_PORT
276
+ ? parseInt(process.env.INBOX_HTTP_PORT, 10)
277
+ : 0;
278
+ const sqlitePath = process.env.INBOX_SQLITE_PATH;
279
+
280
+ createAgentInbox({ httpPort, sqlitePath })
281
+ .then((inbox) => {
282
+ console.error(
283
+ `Agent Inbox IPC server listening on ${process.env.INBOX_SOCKET_PATH ?? defaultSocketPath()}`
284
+ );
285
+ process.on("SIGINT", async () => {
286
+ await inbox.stop();
287
+ process.exit(0);
288
+ });
289
+ process.on("SIGTERM", async () => {
290
+ await inbox.stop();
291
+ process.exit(0);
292
+ });
293
+ })
294
+ .catch((err) => {
295
+ console.error("Failed to start Agent Inbox:", err);
296
+ process.exit(1);
297
+ });
298
+ }
299
+ }