@pinet/slack-bridge 0.1.0

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 (179) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +570 -0
  3. package/dist/activity-log.d.ts +62 -0
  4. package/dist/activity-log.js +293 -0
  5. package/dist/agent-completion-runtime.d.ts +17 -0
  6. package/dist/agent-completion-runtime.js +26 -0
  7. package/dist/agent-event-runtime.d.ts +13 -0
  8. package/dist/agent-event-runtime.js +27 -0
  9. package/dist/agent-prompt-guidance.d.ts +22 -0
  10. package/dist/agent-prompt-guidance.js +46 -0
  11. package/dist/broker/adapters/slack.d.ts +74 -0
  12. package/dist/broker/adapters/slack.js +563 -0
  13. package/dist/broker/adapters/types.d.ts +1 -0
  14. package/dist/broker/adapters/types.js +1 -0
  15. package/dist/broker/agent-messaging.d.ts +1 -0
  16. package/dist/broker/agent-messaging.js +1 -0
  17. package/dist/broker/auth.d.ts +1 -0
  18. package/dist/broker/auth.js +1 -0
  19. package/dist/broker/client.d.ts +167 -0
  20. package/dist/broker/client.js +624 -0
  21. package/dist/broker/control-plane-dashboard.d.ts +98 -0
  22. package/dist/broker/control-plane-dashboard.js +213 -0
  23. package/dist/broker/ghost-reaper.d.ts +49 -0
  24. package/dist/broker/ghost-reaper.js +209 -0
  25. package/dist/broker/index.d.ts +36 -0
  26. package/dist/broker/index.js +90 -0
  27. package/dist/broker/leader.d.ts +1 -0
  28. package/dist/broker/leader.js +1 -0
  29. package/dist/broker/maintenance.d.ts +1 -0
  30. package/dist/broker/maintenance.js +1 -0
  31. package/dist/broker/message-send.d.ts +1 -0
  32. package/dist/broker/message-send.js +1 -0
  33. package/dist/broker/paths.d.ts +1 -0
  34. package/dist/broker/paths.js +1 -0
  35. package/dist/broker/raw-tcp-loopback.d.ts +1 -0
  36. package/dist/broker/raw-tcp-loopback.js +1 -0
  37. package/dist/broker/router.d.ts +1 -0
  38. package/dist/broker/router.js +1 -0
  39. package/dist/broker/schema.d.ts +9 -0
  40. package/dist/broker/schema.js +63 -0
  41. package/dist/broker/socket-server.d.ts +120 -0
  42. package/dist/broker/socket-server.js +1087 -0
  43. package/dist/broker/types.d.ts +1 -0
  44. package/dist/broker/types.js +1 -0
  45. package/dist/broker-delivery.d.ts +10 -0
  46. package/dist/broker-delivery.js +26 -0
  47. package/dist/broker-inbound-persistence.d.ts +11 -0
  48. package/dist/broker-inbound-persistence.js +22 -0
  49. package/dist/broker-prompt-loader.d.ts +34 -0
  50. package/dist/broker-prompt-loader.js +185 -0
  51. package/dist/broker-runtime-access.d.ts +14 -0
  52. package/dist/broker-runtime-access.js +20 -0
  53. package/dist/broker-runtime.d.ts +100 -0
  54. package/dist/broker-runtime.js +533 -0
  55. package/dist/broker-thread-owner-hints.d.ts +10 -0
  56. package/dist/broker-thread-owner-hints.js +14 -0
  57. package/dist/canvases.d.ts +80 -0
  58. package/dist/canvases.js +221 -0
  59. package/dist/command-registration-runtime.d.ts +9 -0
  60. package/dist/command-registration-runtime.js +9 -0
  61. package/dist/core-tool-guardrails.d.ts +24 -0
  62. package/dist/core-tool-guardrails.js +66 -0
  63. package/dist/deploy-manifest.d.ts +27 -0
  64. package/dist/deploy-manifest.js +197 -0
  65. package/dist/follower-delivery.d.ts +16 -0
  66. package/dist/follower-delivery.js +70 -0
  67. package/dist/follower-runtime.d.ts +61 -0
  68. package/dist/follower-runtime.js +350 -0
  69. package/dist/git-metadata.d.ts +35 -0
  70. package/dist/git-metadata.js +88 -0
  71. package/dist/guardrails.d.ts +69 -0
  72. package/dist/guardrails.js +283 -0
  73. package/dist/helpers.d.ts +550 -0
  74. package/dist/helpers.js +2711 -0
  75. package/dist/home-tab.d.ts +27 -0
  76. package/dist/home-tab.js +242 -0
  77. package/dist/imessage-tools.d.ts +33 -0
  78. package/dist/imessage-tools.js +96 -0
  79. package/dist/inbox-drain-runtime.d.ts +30 -0
  80. package/dist/inbox-drain-runtime.js +71 -0
  81. package/dist/index.d.ts +2 -0
  82. package/dist/index.js +1330 -0
  83. package/dist/persisted-runtime-state.d.ts +52 -0
  84. package/dist/persisted-runtime-state.js +98 -0
  85. package/dist/pinet-activity-formatting.d.ts +20 -0
  86. package/dist/pinet-activity-formatting.js +43 -0
  87. package/dist/pinet-agent-status.d.ts +36 -0
  88. package/dist/pinet-agent-status.js +48 -0
  89. package/dist/pinet-commands.d.ts +70 -0
  90. package/dist/pinet-commands.js +402 -0
  91. package/dist/pinet-confirmation-replies.d.ts +7 -0
  92. package/dist/pinet-confirmation-replies.js +30 -0
  93. package/dist/pinet-control-plane-dashboard.d.ts +34 -0
  94. package/dist/pinet-control-plane-dashboard.js +86 -0
  95. package/dist/pinet-home-tabs.d.ts +36 -0
  96. package/dist/pinet-home-tabs.js +101 -0
  97. package/dist/pinet-maintenance-delivery.d.ts +22 -0
  98. package/dist/pinet-maintenance-delivery.js +45 -0
  99. package/dist/pinet-mesh-ops.d.ts +97 -0
  100. package/dist/pinet-mesh-ops.js +266 -0
  101. package/dist/pinet-registration-gate.d.ts +14 -0
  102. package/dist/pinet-registration-gate.js +42 -0
  103. package/dist/pinet-remote-control-acks.d.ts +16 -0
  104. package/dist/pinet-remote-control-acks.js +46 -0
  105. package/dist/pinet-remote-control.d.ts +17 -0
  106. package/dist/pinet-remote-control.js +74 -0
  107. package/dist/pinet-runtime-composition.d.ts +27 -0
  108. package/dist/pinet-runtime-composition.js +20 -0
  109. package/dist/pinet-tools.d.ts +67 -0
  110. package/dist/pinet-tools.js +1211 -0
  111. package/dist/prompts/broker/default.md +41 -0
  112. package/dist/prompts/broker/tmux.md +49 -0
  113. package/dist/ralph-loop.d.ts +76 -0
  114. package/dist/ralph-loop.js +524 -0
  115. package/dist/reaction-triggers.d.ts +25 -0
  116. package/dist/reaction-triggers.js +171 -0
  117. package/dist/repo-tool-guardrails.d.ts +20 -0
  118. package/dist/repo-tool-guardrails.js +81 -0
  119. package/dist/runtime-agent-context.d.ts +93 -0
  120. package/dist/runtime-agent-context.js +302 -0
  121. package/dist/runtime-mode.d.ts +10 -0
  122. package/dist/runtime-mode.js +39 -0
  123. package/dist/scheduled-wakeups.d.ts +1 -0
  124. package/dist/scheduled-wakeups.js +1 -0
  125. package/dist/session-ui-runtime.d.ts +20 -0
  126. package/dist/session-ui-runtime.js +163 -0
  127. package/dist/single-player-runtime.d.ts +71 -0
  128. package/dist/single-player-runtime.js +436 -0
  129. package/dist/skins/cosmere.json +1571 -0
  130. package/dist/skins/foundation.json +1304 -0
  131. package/dist/slack-access.d.ts +186 -0
  132. package/dist/slack-access.js +462 -0
  133. package/dist/slack-api.d.ts +2 -0
  134. package/dist/slack-api.js +1 -0
  135. package/dist/slack-block-kit.d.ts +36 -0
  136. package/dist/slack-block-kit.js +242 -0
  137. package/dist/slack-export.d.ts +31 -0
  138. package/dist/slack-export.js +206 -0
  139. package/dist/slack-message-context.d.ts +15 -0
  140. package/dist/slack-message-context.js +207 -0
  141. package/dist/slack-modals.d.ts +13 -0
  142. package/dist/slack-modals.js +79 -0
  143. package/dist/slack-pinet-runtime-adapter.d.ts +16 -0
  144. package/dist/slack-pinet-runtime-adapter.js +72 -0
  145. package/dist/slack-presence.d.ts +36 -0
  146. package/dist/slack-presence.js +89 -0
  147. package/dist/slack-request-runtime.d.ts +8 -0
  148. package/dist/slack-request-runtime.js +19 -0
  149. package/dist/slack-runtime-access.d.ts +28 -0
  150. package/dist/slack-runtime-access.js +107 -0
  151. package/dist/slack-scope-diagnostics.d.ts +31 -0
  152. package/dist/slack-scope-diagnostics.js +235 -0
  153. package/dist/slack-socket-dedup.d.ts +7 -0
  154. package/dist/slack-socket-dedup.js +135 -0
  155. package/dist/slack-thread-status.d.ts +41 -0
  156. package/dist/slack-thread-status.js +156 -0
  157. package/dist/slack-tool-policy-runtime.d.ts +36 -0
  158. package/dist/slack-tool-policy-runtime.js +96 -0
  159. package/dist/slack-tools.d.ts +52 -0
  160. package/dist/slack-tools.js +2688 -0
  161. package/dist/slack-turn-guardrails.d.ts +19 -0
  162. package/dist/slack-turn-guardrails.js +36 -0
  163. package/dist/slack-upload.d.ts +49 -0
  164. package/dist/slack-upload.js +224 -0
  165. package/dist/task-assignments.d.ts +45 -0
  166. package/dist/task-assignments.js +527 -0
  167. package/dist/thread-confirmations.d.ts +22 -0
  168. package/dist/thread-confirmations.js +139 -0
  169. package/dist/tool-registration-runtime.d.ts +13 -0
  170. package/dist/tool-registration-runtime.js +13 -0
  171. package/dist/ttl-cache.d.ts +47 -0
  172. package/dist/ttl-cache.js +105 -0
  173. package/manifest.yaml +57 -0
  174. package/package.json +60 -0
  175. package/skills/pinet-skin-creator/SKILL.md +109 -0
  176. package/skills/pinet-skin-creator/references/descriptor-format.md +99 -0
  177. package/skills/pinet-skin-creator/references/safety-checklist.md +56 -0
  178. package/skills/pinet-skin-creator/templates/pinet-skin-descriptor.json +77 -0
  179. package/skills/slack-bridge/SKILL.md +360 -0
@@ -0,0 +1,624 @@
1
+ import * as net from "node:net";
2
+ import { readMeshSecret } from "./auth.js";
3
+ import { DEFAULT_SOCKET_PATH as PINET_DEFAULT_SOCKET_PATH } from "./paths.js";
4
+ import { assertLoopbackTcpHost } from "./raw-tcp-loopback.js";
5
+ import { RPC_AGENT_NAME_CONFLICT, RPC_METHOD_NOT_FOUND } from "./types.js";
6
+ // ─── Constants (exported for testing) ────────────────────
7
+ export const DEFAULT_SOCKET_PATH = PINET_DEFAULT_SOCKET_PATH;
8
+ export const REQUEST_TIMEOUT_MS = 5000;
9
+ export const RECONNECT_DELAY_MS = 3000;
10
+ export const INITIAL_RECONNECT_DELAY_MS = 1000;
11
+ export const MAX_RECONNECT_DELAY_MS = 30000;
12
+ export const HEARTBEAT_INTERVAL_MS = 5000;
13
+ export const HEARTBEAT_METADATA_PROVIDER_TIMEOUT_MS = 2500;
14
+ function withTimeout(promise, timeoutMs) {
15
+ return new Promise((resolve, reject) => {
16
+ const timer = setTimeout(() => reject(new Error(`Timed out after ${timeoutMs}ms`)), timeoutMs);
17
+ timer.unref?.();
18
+ promise.then((value) => {
19
+ clearTimeout(timer);
20
+ resolve(value);
21
+ }, (error) => {
22
+ clearTimeout(timer);
23
+ reject(error instanceof Error ? error : new Error(String(error)));
24
+ });
25
+ });
26
+ }
27
+ /** Compute reconnect delay with exponential backoff and jitter. */
28
+ export function computeReconnectDelay(attempt, random = Math.random()) {
29
+ const baseDelay = INITIAL_RECONNECT_DELAY_MS * Math.pow(2, attempt);
30
+ const capped = Math.min(baseDelay, MAX_RECONNECT_DELAY_MS);
31
+ // Add jitter: ±25%
32
+ const jitter = capped * (0.75 + random * 0.5);
33
+ return Math.round(jitter);
34
+ }
35
+ function createRpcRequestError(method, error) {
36
+ const err = new Error(error.message);
37
+ err.name = "BrokerRpcRequestError";
38
+ err.code = error.code;
39
+ err.data = error.data;
40
+ err.method = method;
41
+ return err;
42
+ }
43
+ function isRpcMethodNotFoundError(err, method) {
44
+ if (!(err instanceof Error)) {
45
+ return false;
46
+ }
47
+ const rpcErr = err;
48
+ if (rpcErr.method !== method) {
49
+ return false;
50
+ }
51
+ return rpcErr.code === RPC_METHOD_NOT_FOUND || err.message === `Unknown method: ${method}`;
52
+ }
53
+ function isRpcAgentNameConflictError(err) {
54
+ if (!(err instanceof Error)) {
55
+ return false;
56
+ }
57
+ const rpcErr = err;
58
+ if (rpcErr.code === RPC_AGENT_NAME_CONFLICT) {
59
+ return true;
60
+ }
61
+ if (typeof rpcErr.data !== "object" || rpcErr.data == null) {
62
+ return false;
63
+ }
64
+ return rpcErr.data.code === "AGENT_NAME_CONFLICT";
65
+ }
66
+ function getMeshAuthCompatibilityError() {
67
+ return new Error("Broker does not support Pinet mesh auth (`auth`). Upgrade the broker or disable follower mesh auth when connecting to older/no-auth brokers.");
68
+ }
69
+ function getErrorCode(err) {
70
+ if (typeof err !== "object" || err == null || !("code" in err)) {
71
+ return null;
72
+ }
73
+ const code = err.code;
74
+ return typeof code === "string" ? code : null;
75
+ }
76
+ // ─── BrokerClient ────────────────────────────────────────
77
+ export class BrokerClient {
78
+ connectOpts;
79
+ meshSecret;
80
+ meshSecretPath;
81
+ socket = null;
82
+ connected = false;
83
+ shuttingDown = false;
84
+ reconnectTimer = null;
85
+ heartbeatTimer = null;
86
+ disconnectHandler = null;
87
+ reconnectHandler = null;
88
+ reconnectFailedHandler = null;
89
+ reconnectAttempt = 0;
90
+ registrationSnapshot = null;
91
+ registeredIdentity = null;
92
+ heartbeatMetadataProvider = null;
93
+ heartbeatMetadataInFlight = null;
94
+ nextId = 1;
95
+ pending = new Map();
96
+ buffer = "";
97
+ constructor(opts) {
98
+ if (opts === undefined) {
99
+ this.connectOpts = { path: DEFAULT_SOCKET_PATH };
100
+ this.meshSecret = null;
101
+ this.meshSecretPath = null;
102
+ return;
103
+ }
104
+ if (typeof opts === "string") {
105
+ this.connectOpts = { path: opts };
106
+ this.meshSecret = null;
107
+ this.meshSecretPath = null;
108
+ return;
109
+ }
110
+ if ("path" in opts) {
111
+ this.connectOpts = { path: opts.path };
112
+ }
113
+ else {
114
+ assertLoopbackTcpHost(opts.host, "broker client connect target");
115
+ this.connectOpts = { host: opts.host, port: opts.port };
116
+ }
117
+ const meshSecret = opts.meshSecret?.trim();
118
+ this.meshSecret = meshSecret && meshSecret.length > 0 ? meshSecret : null;
119
+ const meshSecretPath = opts.meshSecretPath?.trim();
120
+ this.meshSecretPath = meshSecretPath && meshSecretPath.length > 0 ? meshSecretPath : null;
121
+ }
122
+ // ─── Connection ──────────────────────────────────────
123
+ async connect() {
124
+ this.shuttingDown = false;
125
+ await this.connectSocket();
126
+ try {
127
+ await this.authenticateIfNeeded();
128
+ }
129
+ catch (err) {
130
+ this.disconnect();
131
+ throw err;
132
+ }
133
+ }
134
+ disconnect() {
135
+ this.shuttingDown = true;
136
+ if (this.reconnectTimer) {
137
+ clearTimeout(this.reconnectTimer);
138
+ this.reconnectTimer = null;
139
+ }
140
+ this.stopHeartbeat();
141
+ this.rejectAllPending(new Error("Client disconnected"));
142
+ try {
143
+ this.socket?.destroy();
144
+ }
145
+ catch {
146
+ /* ignore */
147
+ }
148
+ this.socket = null;
149
+ this.connected = false;
150
+ }
151
+ async disconnectGracefully() {
152
+ this.shuttingDown = true;
153
+ if (this.reconnectTimer) {
154
+ clearTimeout(this.reconnectTimer);
155
+ this.reconnectTimer = null;
156
+ }
157
+ this.stopHeartbeat();
158
+ try {
159
+ await this.unregister();
160
+ }
161
+ finally {
162
+ this.disconnect();
163
+ }
164
+ }
165
+ isConnected() {
166
+ return this.connected;
167
+ }
168
+ resolveMeshSecret() {
169
+ if (this.meshSecret) {
170
+ return this.meshSecret;
171
+ }
172
+ if (this.meshSecretPath) {
173
+ try {
174
+ return readMeshSecret(this.meshSecretPath);
175
+ }
176
+ catch (err) {
177
+ if (getErrorCode(err) === "ENOENT") {
178
+ throw new Error(`Configured Pinet mesh secret file not found: ${this.meshSecretPath}. Set slack-bridge.meshSecretPath to an existing file, provide slack-bridge.meshSecret directly, or leave both unset to disable shared-secret auth.`);
179
+ }
180
+ throw err;
181
+ }
182
+ }
183
+ return null;
184
+ }
185
+ async authenticateIfNeeded() {
186
+ const meshSecret = this.resolveMeshSecret();
187
+ if (!meshSecret) {
188
+ return;
189
+ }
190
+ try {
191
+ await this.request("auth", { secret: meshSecret });
192
+ }
193
+ catch (err) {
194
+ if (isRpcMethodNotFoundError(err, "auth")) {
195
+ throw getMeshAuthCompatibilityError();
196
+ }
197
+ throw err;
198
+ }
199
+ }
200
+ // ─── Registration ────────────────────────────────────
201
+ async register(name, emoji, metadata, stableId) {
202
+ this.registrationSnapshot = {
203
+ name,
204
+ emoji,
205
+ ...(metadata ? { metadata } : {}),
206
+ ...(stableId ? { stableId } : {}),
207
+ ...(name.trim().length === 0 ? { brokerAssignedIdentity: true } : {}),
208
+ };
209
+ return this.performRegister(this.registrationSnapshot);
210
+ }
211
+ async unregister() {
212
+ if (!this.connected)
213
+ return;
214
+ try {
215
+ await this.request("unregister");
216
+ }
217
+ finally {
218
+ this.stopHeartbeat();
219
+ this.registrationSnapshot = null;
220
+ this.registeredIdentity = null;
221
+ }
222
+ }
223
+ async heartbeat(metadata) {
224
+ if (metadata === undefined) {
225
+ await this.request("heartbeat");
226
+ return;
227
+ }
228
+ await this.request("heartbeat", { metadata });
229
+ }
230
+ setHeartbeatMetadataProvider(provider) {
231
+ this.heartbeatMetadataProvider = provider;
232
+ }
233
+ // ─── Messaging ───────────────────────────────────────
234
+ async pollInbox() {
235
+ const result = (await this.request("inbox.poll"));
236
+ return result;
237
+ }
238
+ async readInbox(options = {}) {
239
+ const result = (await this.request("inbox.read", {
240
+ ...(options.threadId ? { threadId: options.threadId } : {}),
241
+ ...(typeof options.limit === "number" ? { limit: options.limit } : {}),
242
+ ...(typeof options.unreadOnly === "boolean" ? { unreadOnly: options.unreadOnly } : {}),
243
+ ...(typeof options.markRead === "boolean" ? { markRead: options.markRead } : {}),
244
+ }));
245
+ return {
246
+ messages: result.messages.map((item) => ({
247
+ inboxId: item.entry.id,
248
+ delivered: item.entry.delivered,
249
+ readAt: item.entry.readAt,
250
+ message: item.message,
251
+ })),
252
+ unreadCountBefore: result.unreadCountBefore,
253
+ unreadCountAfter: result.unreadCountAfter,
254
+ unreadThreads: result.unreadThreads,
255
+ markedReadIds: result.markedReadIds,
256
+ };
257
+ }
258
+ async ackMessages(ids) {
259
+ await this.request("inbox.ack", { ids });
260
+ }
261
+ async send(threadId, text, metadata) {
262
+ await this.request("send", { threadId, body: text, ...(metadata ? { metadata } : {}) });
263
+ }
264
+ async sendMessage(input) {
265
+ const result = (await this.request("message.send", {
266
+ threadId: input.threadId,
267
+ body: input.body,
268
+ ...(input.source ? { source: input.source } : {}),
269
+ ...(input.channel ? { channel: input.channel } : {}),
270
+ ...(input.content ? { content: input.content } : {}),
271
+ ...(input.blocks && input.blocks.length > 0 ? { blocks: input.blocks } : {}),
272
+ ...(input.agentName ? { agentName: input.agentName } : {}),
273
+ ...(input.agentEmoji ? { agentEmoji: input.agentEmoji } : {}),
274
+ ...(input.agentOwnerToken ? { agentOwnerToken: input.agentOwnerToken } : {}),
275
+ ...(input.metadata ? { metadata: input.metadata } : {}),
276
+ }));
277
+ return result;
278
+ }
279
+ // ─── Thread ownership ─────────────────────────────────
280
+ async claimThread(threadId, channel, source) {
281
+ const params = { threadId };
282
+ if (channel)
283
+ params.channel = channel;
284
+ if (source)
285
+ params.source = source;
286
+ const result = (await this.request("thread.claim", params));
287
+ return result;
288
+ }
289
+ async resolveThread(threadTs) {
290
+ const result = (await this.request("resolveThread", { threadTs }));
291
+ return typeof result.channelId === "string" ? result.channelId : null;
292
+ }
293
+ // ─── Status ────────────────────────────────────────────
294
+ async updateStatus(status) {
295
+ await this.request("status.update", { status });
296
+ }
297
+ // ─── Agent-to-agent messaging ─────────────────────────
298
+ async sendAgentMessage(target, body, metadata) {
299
+ const result = (await this.request("agent.message", {
300
+ targetAgent: target,
301
+ body,
302
+ ...(metadata ? { metadata } : {}),
303
+ }));
304
+ return result.messageId;
305
+ }
306
+ async sendAgentBroadcast(channel, body) {
307
+ const result = (await this.request("agent.broadcast", {
308
+ channel,
309
+ body,
310
+ }));
311
+ return {
312
+ channel: result.channel,
313
+ messageIds: result.messageIds,
314
+ recipients: result.recipients,
315
+ };
316
+ }
317
+ async scheduleWakeup(fireAt, body) {
318
+ const result = (await this.request("schedule.create", {
319
+ fireAt,
320
+ body,
321
+ }));
322
+ return {
323
+ id: result.id,
324
+ threadId: result.threadId,
325
+ fireAt: result.fireAt,
326
+ };
327
+ }
328
+ async listLanes(options = {}) {
329
+ return (await this.request("lane.list", {
330
+ ...(options.state ? { state: options.state } : {}),
331
+ ...(options.ownerAgentId ? { ownerAgentId: options.ownerAgentId } : {}),
332
+ ...(typeof options.includeDone === "boolean" ? { includeDone: options.includeDone } : {}),
333
+ }));
334
+ }
335
+ async upsertLane(input) {
336
+ return (await this.request("lane.upsert", input));
337
+ }
338
+ async setLaneParticipant(input) {
339
+ return (await this.request("lane.participant", input));
340
+ }
341
+ async acquirePortLease(input) {
342
+ return (await this.request("portLease.acquire", input));
343
+ }
344
+ async renewPortLease(input) {
345
+ return (await this.request("portLease.renew", input));
346
+ }
347
+ async releasePortLease(input) {
348
+ return (await this.request("portLease.release", input));
349
+ }
350
+ async getPortLease(leaseId) {
351
+ return (await this.request("portLease.status", { leaseId }));
352
+ }
353
+ async listPortLeases(options = {}) {
354
+ return (await this.request("portLease.list", options));
355
+ }
356
+ async expirePortLeases() {
357
+ return (await this.request("portLease.expire"));
358
+ }
359
+ // ─── Queries ─────────────────────────────────────────
360
+ async listThreads() {
361
+ const result = (await this.request("threads.list"));
362
+ return result;
363
+ }
364
+ async listAgents(includeDisconnected = false) {
365
+ const result = (await this.request("agents.list", includeDisconnected ? { includeDisconnected: true } : undefined));
366
+ return result;
367
+ }
368
+ // ─── Adapter capabilities ───────────────────────────
369
+ async invokeAdapterCapability(adapter, capability, params = {}) {
370
+ const result = (await this.request("adapter.capability", {
371
+ adapter,
372
+ capability,
373
+ params,
374
+ }));
375
+ return result;
376
+ }
377
+ /**
378
+ * Compatibility wrapper for callers that still need direct Slack API access.
379
+ * The broker RPC boundary remains adapter-neutral; Slack-specific payloads are
380
+ * interpreted inside the Slack adapter's api.call capability.
381
+ */
382
+ async slackProxy(method, params) {
383
+ try {
384
+ return await this.invokeAdapterCapability("slack", "api.call", { method, params });
385
+ }
386
+ catch (err) {
387
+ if (!isRpcMethodNotFoundError(err, "adapter.capability")) {
388
+ throw err;
389
+ }
390
+ }
391
+ return (await this.request("slack.proxy", { method, params }));
392
+ }
393
+ // ─── Events ──────────────────────────────────────────
394
+ onDisconnect(handler) {
395
+ this.disconnectHandler = handler;
396
+ }
397
+ onReconnect(handler) {
398
+ this.reconnectHandler = handler;
399
+ }
400
+ onReconnectFailed(handler) {
401
+ this.reconnectFailedHandler = handler;
402
+ }
403
+ getReconnectAttempt() {
404
+ return this.reconnectAttempt;
405
+ }
406
+ getRegisteredIdentity() {
407
+ return this.registeredIdentity ? { ...this.registeredIdentity } : null;
408
+ }
409
+ // ─── JSON-RPC transport ──────────────────────────────
410
+ request(method, params) {
411
+ if (!this.connected || !this.socket) {
412
+ return Promise.reject(new Error("Not connected to broker"));
413
+ }
414
+ const id = this.nextId++;
415
+ const msg = {
416
+ jsonrpc: "2.0",
417
+ id,
418
+ method,
419
+ ...(params ? { params } : {}),
420
+ };
421
+ return new Promise((resolve, reject) => {
422
+ const timer = setTimeout(() => {
423
+ this.pending.delete(id);
424
+ reject(new Error(`Request timed out: ${method}`));
425
+ }, REQUEST_TIMEOUT_MS);
426
+ this.pending.set(id, { method, resolve, reject, timer });
427
+ const line = JSON.stringify(msg) + "\n";
428
+ this.socket.write(line, "utf-8", (err) => {
429
+ if (err) {
430
+ clearTimeout(timer);
431
+ this.pending.delete(id);
432
+ reject(err);
433
+ }
434
+ });
435
+ });
436
+ }
437
+ onData(chunk) {
438
+ this.buffer += chunk;
439
+ const lines = this.buffer.split("\n");
440
+ // Keep the last incomplete line in the buffer
441
+ this.buffer = lines.pop() ?? "";
442
+ for (const line of lines) {
443
+ if (!line.trim())
444
+ continue;
445
+ try {
446
+ const msg = JSON.parse(line);
447
+ this.handleResponse(msg);
448
+ }
449
+ catch {
450
+ /* malformed JSON — skip */
451
+ }
452
+ }
453
+ }
454
+ handleResponse(msg) {
455
+ const entry = this.pending.get(msg.id);
456
+ if (!entry)
457
+ return;
458
+ clearTimeout(entry.timer);
459
+ this.pending.delete(msg.id);
460
+ if (msg.error) {
461
+ entry.reject(createRpcRequestError(entry.method, msg.error));
462
+ }
463
+ else {
464
+ entry.resolve(msg.result);
465
+ }
466
+ }
467
+ // ─── Reconnect ──────────────────────────────────────
468
+ scheduleReconnect() {
469
+ if (this.shuttingDown || this.reconnectTimer)
470
+ return;
471
+ const delay = computeReconnectDelay(this.reconnectAttempt);
472
+ this.reconnectAttempt++;
473
+ this.reconnectTimer = setTimeout(() => {
474
+ this.reconnectTimer = null;
475
+ void this.reconnectOnce();
476
+ }, delay);
477
+ this.reconnectTimer.unref?.();
478
+ }
479
+ connectSocket() {
480
+ return new Promise((resolve, reject) => {
481
+ const sock = net.createConnection(this.connectOpts);
482
+ sock.on("connect", () => {
483
+ this.socket = sock;
484
+ this.connected = true;
485
+ this.buffer = "";
486
+ resolve();
487
+ });
488
+ sock.on("data", (chunk) => {
489
+ this.onData(chunk.toString("utf-8"));
490
+ });
491
+ sock.on("close", () => {
492
+ const wasConnected = this.connected;
493
+ this.connected = false;
494
+ this.socket = null;
495
+ this.stopHeartbeat();
496
+ this.rejectAllPending(new Error("Socket closed"));
497
+ if (wasConnected && !this.shuttingDown) {
498
+ this.disconnectHandler?.();
499
+ this.scheduleReconnect();
500
+ }
501
+ });
502
+ sock.on("error", (err) => {
503
+ if (!this.connected) {
504
+ reject(err);
505
+ }
506
+ // If already connected, the close event handles cleanup
507
+ });
508
+ });
509
+ }
510
+ async performRegister(snapshot) {
511
+ const result = (await this.request("register", {
512
+ name: snapshot.name,
513
+ emoji: snapshot.emoji,
514
+ pid: process.pid,
515
+ ...(snapshot.metadata ? { metadata: snapshot.metadata } : {}),
516
+ ...(snapshot.stableId ? { stableId: snapshot.stableId } : {}),
517
+ }));
518
+ this.registrationSnapshot = snapshot.brokerAssignedIdentity
519
+ ? {
520
+ ...snapshot,
521
+ ...(result.metadata ? { metadata: result.metadata } : {}),
522
+ }
523
+ : {
524
+ ...snapshot,
525
+ name: result.name,
526
+ emoji: result.emoji,
527
+ ...(result.metadata ? { metadata: result.metadata } : {}),
528
+ };
529
+ this.registeredIdentity = result;
530
+ this.startHeartbeat();
531
+ return result;
532
+ }
533
+ async reconnectOnce() {
534
+ try {
535
+ await this.connectSocket();
536
+ }
537
+ catch {
538
+ this.scheduleReconnect();
539
+ return;
540
+ }
541
+ try {
542
+ await this.authenticateIfNeeded();
543
+ if (this.registrationSnapshot) {
544
+ await this.performRegister(this.registrationSnapshot);
545
+ }
546
+ this.reconnectAttempt = 0;
547
+ this.reconnectHandler?.();
548
+ }
549
+ catch (err) {
550
+ // Re-registration failed after the socket connected. Clear the connection
551
+ // state immediately instead of waiting for the async "close" event so the
552
+ // client cannot stay in a broken "connected but not registered" state.
553
+ // Then either surface a terminal reconnect failure or schedule the next
554
+ // retry ourselves. (#139)
555
+ const reconnectError = err instanceof Error ? err : new Error(String(err));
556
+ const failedSocket = this.socket;
557
+ this.socket = null;
558
+ this.connected = false;
559
+ this.buffer = "";
560
+ this.stopHeartbeat();
561
+ this.rejectAllPending(new Error("Socket closed"));
562
+ this.registeredIdentity = null;
563
+ try {
564
+ failedSocket?.destroy();
565
+ }
566
+ catch {
567
+ /* ignore */
568
+ }
569
+ if (isRpcAgentNameConflictError(reconnectError)) {
570
+ this.reconnectAttempt = 0;
571
+ this.reconnectFailedHandler?.(reconnectError);
572
+ return;
573
+ }
574
+ this.scheduleReconnect();
575
+ }
576
+ }
577
+ startHeartbeat() {
578
+ this.stopHeartbeat();
579
+ this.heartbeatTimer = setInterval(() => {
580
+ if (!this.connected)
581
+ return;
582
+ void this.runHeartbeatTick().catch(() => {
583
+ /* best effort */
584
+ });
585
+ }, HEARTBEAT_INTERVAL_MS);
586
+ this.heartbeatTimer.unref?.();
587
+ }
588
+ async readHeartbeatMetadata() {
589
+ const provider = this.heartbeatMetadataProvider;
590
+ if (!provider || this.heartbeatMetadataInFlight) {
591
+ return undefined;
592
+ }
593
+ const work = Promise.resolve()
594
+ .then(() => provider())
595
+ .finally(() => {
596
+ if (this.heartbeatMetadataInFlight === work) {
597
+ this.heartbeatMetadataInFlight = null;
598
+ }
599
+ });
600
+ this.heartbeatMetadataInFlight = work;
601
+ try {
602
+ return await withTimeout(work, HEARTBEAT_METADATA_PROVIDER_TIMEOUT_MS);
603
+ }
604
+ catch {
605
+ return undefined;
606
+ }
607
+ }
608
+ async runHeartbeatTick() {
609
+ await this.heartbeat(await this.readHeartbeatMetadata());
610
+ }
611
+ stopHeartbeat() {
612
+ if (!this.heartbeatTimer)
613
+ return;
614
+ clearInterval(this.heartbeatTimer);
615
+ this.heartbeatTimer = null;
616
+ }
617
+ rejectAllPending(err) {
618
+ for (const [id, entry] of this.pending) {
619
+ clearTimeout(entry.timer);
620
+ entry.reject(err);
621
+ this.pending.delete(id);
622
+ }
623
+ }
624
+ }