byterover-cli 1.0.5 → 1.2.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 (204) hide show
  1. package/README.md +19 -13
  2. package/dist/commands/hook-prompt-submit.d.ts +27 -0
  3. package/dist/commands/hook-prompt-submit.js +39 -0
  4. package/dist/commands/mcp.d.ts +13 -0
  5. package/dist/commands/mcp.js +61 -0
  6. package/dist/commands/status.js +8 -3
  7. package/dist/constants.d.ts +1 -1
  8. package/dist/constants.js +1 -1
  9. package/dist/core/domain/cipher/agent-events/types.d.ts +44 -1
  10. package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
  11. package/dist/core/domain/cipher/tools/constants.js +1 -0
  12. package/dist/core/domain/entities/agent.d.ts +16 -0
  13. package/dist/core/domain/entities/agent.js +78 -0
  14. package/dist/core/domain/entities/connector-type.d.ts +10 -0
  15. package/dist/core/domain/entities/connector-type.js +9 -0
  16. package/dist/core/domain/entities/event.d.ts +1 -1
  17. package/dist/core/domain/entities/event.js +2 -0
  18. package/dist/core/domain/errors/task-error.d.ts +4 -0
  19. package/dist/core/domain/errors/task-error.js +7 -0
  20. package/dist/core/domain/transport/schemas.d.ts +40 -0
  21. package/dist/core/domain/transport/schemas.js +28 -0
  22. package/dist/core/interfaces/connectors/connector-types.d.ts +70 -0
  23. package/dist/core/interfaces/connectors/i-connector-manager.d.ts +72 -0
  24. package/dist/core/interfaces/connectors/i-connector-manager.js +1 -0
  25. package/dist/core/interfaces/connectors/i-connector.d.ts +54 -0
  26. package/dist/core/interfaces/connectors/i-connector.js +1 -0
  27. package/dist/core/interfaces/i-file-service.d.ts +7 -0
  28. package/dist/core/interfaces/i-mcp-config-writer.d.ts +40 -0
  29. package/dist/core/interfaces/i-mcp-config-writer.js +1 -0
  30. package/dist/core/interfaces/i-rule-template-service.d.ts +4 -2
  31. package/dist/core/interfaces/transport/i-transport-client.d.ts +7 -0
  32. package/dist/core/interfaces/usecase/i-connectors-use-case.d.ts +3 -0
  33. package/dist/core/interfaces/usecase/i-connectors-use-case.js +1 -0
  34. package/dist/hooks/init/update-notifier.d.ts +1 -0
  35. package/dist/hooks/init/update-notifier.js +10 -1
  36. package/dist/infra/cipher/agent/cipher-agent.d.ts +8 -0
  37. package/dist/infra/cipher/agent/cipher-agent.js +16 -0
  38. package/dist/infra/cipher/file-system/binary-utils.d.ts +7 -12
  39. package/dist/infra/cipher/file-system/binary-utils.js +46 -31
  40. package/dist/infra/cipher/llm/context/context-manager.d.ts +10 -2
  41. package/dist/infra/cipher/llm/context/context-manager.js +39 -2
  42. package/dist/infra/cipher/llm/formatters/gemini-formatter.js +48 -9
  43. package/dist/infra/cipher/llm/internal-llm-service.d.ts +4 -0
  44. package/dist/infra/cipher/llm/internal-llm-service.js +40 -12
  45. package/dist/infra/cipher/session/chat-session.d.ts +3 -0
  46. package/dist/infra/cipher/session/chat-session.js +7 -1
  47. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.d.ts +6 -7
  48. package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +57 -18
  49. package/dist/infra/cipher/tools/implementations/curate-tool.d.ts +1 -8
  50. package/dist/infra/cipher/tools/implementations/curate-tool.js +380 -24
  51. package/dist/infra/cipher/tools/implementations/read-file-tool.js +38 -17
  52. package/dist/infra/cipher/tools/implementations/search-knowledge-tool.d.ts +7 -0
  53. package/dist/infra/cipher/tools/implementations/search-knowledge-tool.js +303 -0
  54. package/dist/infra/cipher/tools/index.d.ts +1 -0
  55. package/dist/infra/cipher/tools/index.js +1 -0
  56. package/dist/infra/cipher/tools/tool-manager.js +1 -0
  57. package/dist/infra/cipher/tools/tool-registry.js +7 -0
  58. package/dist/infra/connectors/connector-manager.d.ts +32 -0
  59. package/dist/infra/connectors/connector-manager.js +158 -0
  60. package/dist/infra/connectors/hook/hook-connector-config.d.ts +52 -0
  61. package/dist/infra/connectors/hook/hook-connector-config.js +41 -0
  62. package/dist/infra/connectors/hook/hook-connector.d.ts +46 -0
  63. package/dist/infra/connectors/hook/hook-connector.js +231 -0
  64. package/dist/infra/connectors/mcp/index.d.ts +4 -0
  65. package/dist/infra/connectors/mcp/index.js +4 -0
  66. package/dist/infra/connectors/mcp/json-mcp-config-writer.d.ts +26 -0
  67. package/dist/infra/connectors/mcp/json-mcp-config-writer.js +71 -0
  68. package/dist/infra/connectors/mcp/mcp-connector-config.d.ts +229 -0
  69. package/dist/infra/connectors/mcp/mcp-connector-config.js +173 -0
  70. package/dist/infra/connectors/mcp/mcp-connector.d.ts +80 -0
  71. package/dist/infra/connectors/mcp/mcp-connector.js +324 -0
  72. package/dist/infra/connectors/mcp/toml-mcp-config-writer.d.ts +45 -0
  73. package/dist/infra/connectors/mcp/toml-mcp-config-writer.js +134 -0
  74. package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.d.ts +2 -2
  75. package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.js +1 -1
  76. package/dist/infra/connectors/rules/rules-connector-config.d.ts +95 -0
  77. package/dist/infra/{rule/agent-rule-config.js → connectors/rules/rules-connector-config.js} +10 -10
  78. package/dist/infra/connectors/rules/rules-connector.d.ts +34 -0
  79. package/dist/infra/connectors/rules/rules-connector.js +139 -0
  80. package/dist/infra/connectors/shared/rule-file-manager.d.ts +72 -0
  81. package/dist/infra/connectors/shared/rule-file-manager.js +119 -0
  82. package/dist/infra/connectors/shared/template-service.d.ts +27 -0
  83. package/dist/infra/connectors/shared/template-service.js +125 -0
  84. package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +5 -2
  85. package/dist/infra/context-tree/file-context-tree-writer-service.js +20 -5
  86. package/dist/infra/core/executors/curate-executor.d.ts +2 -2
  87. package/dist/infra/core/executors/curate-executor.js +7 -7
  88. package/dist/infra/core/executors/query-executor.d.ts +12 -0
  89. package/dist/infra/core/executors/query-executor.js +62 -1
  90. package/dist/infra/file/fs-file-service.d.ts +7 -0
  91. package/dist/infra/file/fs-file-service.js +15 -1
  92. package/dist/infra/mcp/index.d.ts +2 -0
  93. package/dist/infra/mcp/index.js +2 -0
  94. package/dist/infra/mcp/mcp-server.d.ts +58 -0
  95. package/dist/infra/mcp/mcp-server.js +178 -0
  96. package/dist/infra/mcp/tools/brv-curate-tool.d.ts +23 -0
  97. package/dist/infra/mcp/tools/brv-curate-tool.js +68 -0
  98. package/dist/infra/mcp/tools/brv-query-tool.d.ts +17 -0
  99. package/dist/infra/mcp/tools/brv-query-tool.js +68 -0
  100. package/dist/infra/mcp/tools/index.d.ts +3 -0
  101. package/dist/infra/mcp/tools/index.js +3 -0
  102. package/dist/infra/mcp/tools/task-result-waiter.d.ts +30 -0
  103. package/dist/infra/mcp/tools/task-result-waiter.js +56 -0
  104. package/dist/infra/process/agent-worker.d.ts +2 -2
  105. package/dist/infra/process/agent-worker.js +663 -142
  106. package/dist/infra/process/constants.d.ts +1 -1
  107. package/dist/infra/process/constants.js +1 -1
  108. package/dist/infra/process/ipc-types.d.ts +17 -4
  109. package/dist/infra/process/ipc-types.js +3 -3
  110. package/dist/infra/process/parent-heartbeat.d.ts +47 -0
  111. package/dist/infra/process/parent-heartbeat.js +118 -0
  112. package/dist/infra/process/process-manager.d.ts +79 -0
  113. package/dist/infra/process/process-manager.js +277 -3
  114. package/dist/infra/process/task-queue-manager.d.ts +13 -0
  115. package/dist/infra/process/task-queue-manager.js +19 -0
  116. package/dist/infra/process/transport-handlers.d.ts +3 -0
  117. package/dist/infra/process/transport-handlers.js +51 -5
  118. package/dist/infra/process/transport-worker.js +9 -69
  119. package/dist/infra/repl/commands/connectors-command.d.ts +8 -0
  120. package/dist/infra/repl/commands/{gen-rules-command.js → connectors-command.js} +21 -10
  121. package/dist/infra/repl/commands/curate-command.js +2 -2
  122. package/dist/infra/repl/commands/index.js +3 -2
  123. package/dist/infra/repl/commands/init-command.js +11 -7
  124. package/dist/infra/repl/commands/query-command.js +22 -2
  125. package/dist/infra/repl/commands/reset-command.js +1 -1
  126. package/dist/infra/transport/socket-io-transport-client.d.ts +75 -0
  127. package/dist/infra/transport/socket-io-transport-client.js +308 -7
  128. package/dist/infra/transport/socket-io-transport-server.js +4 -0
  129. package/dist/infra/usecase/connectors-use-case.d.ts +63 -0
  130. package/dist/infra/usecase/connectors-use-case.js +222 -0
  131. package/dist/infra/usecase/init-use-case.d.ts +8 -43
  132. package/dist/infra/usecase/init-use-case.js +27 -252
  133. package/dist/infra/usecase/logout-use-case.js +1 -1
  134. package/dist/infra/usecase/pull-use-case.js +5 -5
  135. package/dist/infra/usecase/push-use-case.js +4 -4
  136. package/dist/infra/usecase/reset-use-case.js +3 -4
  137. package/dist/infra/usecase/space-list-use-case.js +3 -3
  138. package/dist/infra/usecase/space-switch-use-case.js +3 -3
  139. package/dist/infra/usecase/status-use-case.d.ts +10 -0
  140. package/dist/infra/usecase/status-use-case.js +53 -0
  141. package/dist/resources/prompts/curate.yml +114 -4
  142. package/dist/resources/prompts/explore.yml +34 -0
  143. package/dist/resources/prompts/query-orchestrator.yml +112 -0
  144. package/dist/resources/prompts/system-prompt.yml +12 -2
  145. package/dist/resources/tools/search_knowledge.txt +32 -0
  146. package/dist/templates/mcp-base.md +1 -0
  147. package/dist/templates/sections/brv-instructions.md +98 -0
  148. package/dist/templates/sections/mcp-workflow.md +13 -0
  149. package/dist/tui/app.js +4 -1
  150. package/dist/tui/components/command-details.js +1 -1
  151. package/dist/tui/components/execution/execution-changes.d.ts +2 -0
  152. package/dist/tui/components/execution/execution-changes.js +5 -1
  153. package/dist/tui/components/execution/execution-content.d.ts +2 -0
  154. package/dist/tui/components/execution/execution-content.js +8 -18
  155. package/dist/tui/components/execution/execution-input.d.ts +2 -0
  156. package/dist/tui/components/execution/execution-input.js +6 -4
  157. package/dist/tui/components/execution/execution-progress.d.ts +2 -0
  158. package/dist/tui/components/execution/execution-progress.js +6 -2
  159. package/dist/tui/components/execution/expanded-log-view.d.ts +20 -0
  160. package/dist/tui/components/execution/expanded-log-view.js +75 -0
  161. package/dist/tui/components/execution/expanded-message-view.d.ts +24 -0
  162. package/dist/tui/components/execution/expanded-message-view.js +68 -0
  163. package/dist/tui/components/execution/index.d.ts +2 -0
  164. package/dist/tui/components/execution/index.js +2 -0
  165. package/dist/tui/components/execution/log-item.d.ts +4 -0
  166. package/dist/tui/components/execution/log-item.js +2 -2
  167. package/dist/tui/components/footer.js +1 -1
  168. package/dist/tui/components/index.d.ts +2 -1
  169. package/dist/tui/components/index.js +2 -1
  170. package/dist/tui/components/init.js +2 -9
  171. package/dist/tui/components/logo.js +4 -3
  172. package/dist/tui/components/markdown.d.ts +13 -0
  173. package/dist/tui/components/markdown.js +88 -0
  174. package/dist/tui/components/message-item.js +1 -1
  175. package/dist/tui/components/onboarding/onboarding-flow.js +14 -11
  176. package/dist/tui/components/onboarding/welcome-box.js +1 -1
  177. package/dist/tui/components/suggestions.js +3 -3
  178. package/dist/tui/contexts/mode-context.js +6 -2
  179. package/dist/tui/contexts/onboarding-context.d.ts +4 -0
  180. package/dist/tui/contexts/onboarding-context.js +14 -2
  181. package/dist/tui/hooks/index.d.ts +1 -0
  182. package/dist/tui/hooks/index.js +1 -0
  183. package/dist/tui/hooks/use-is-latest-version.d.ts +6 -0
  184. package/dist/tui/hooks/use-is-latest-version.js +22 -0
  185. package/dist/tui/views/command-view.d.ts +1 -1
  186. package/dist/tui/views/command-view.js +87 -98
  187. package/dist/tui/views/logs-view.d.ts +8 -0
  188. package/dist/tui/views/logs-view.js +55 -27
  189. package/dist/utils/file-validator.d.ts +1 -1
  190. package/dist/utils/file-validator.js +25 -28
  191. package/dist/utils/type-guards.d.ts +5 -0
  192. package/dist/utils/type-guards.js +7 -0
  193. package/oclif.manifest.json +55 -4
  194. package/package.json +12 -1
  195. package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +0 -3
  196. package/dist/infra/repl/commands/gen-rules-command.d.ts +0 -7
  197. package/dist/infra/rule/agent-rule-config.d.ts +0 -19
  198. package/dist/infra/rule/rule-template-service.d.ts +0 -18
  199. package/dist/infra/rule/rule-template-service.js +0 -88
  200. package/dist/infra/usecase/generate-rules-use-case.d.ts +0 -61
  201. package/dist/infra/usecase/generate-rules-use-case.js +0 -285
  202. /package/dist/core/interfaces/{usecase/i-generate-rules-use-case.js → connectors/connector-types.js} +0 -0
  203. /package/dist/infra/{rule → connectors/shared}/constants.d.ts +0 -0
  204. /package/dist/infra/{rule → connectors/shared}/constants.js +0 -0
@@ -1,6 +1,33 @@
1
1
  import { io } from 'socket.io-client';
2
2
  import { TRANSPORT_CONNECT_TIMEOUT_MS, TRANSPORT_DEFAULT_TRANSPORTS, TRANSPORT_RECONNECTION_ATTEMPTS, TRANSPORT_RECONNECTION_DELAY_MAX_MS, TRANSPORT_RECONNECTION_DELAY_MS, TRANSPORT_REQUEST_TIMEOUT_MS, TRANSPORT_ROOM_TIMEOUT_MS, } from '../../constants.js';
3
3
  import { TransportConnectionError, TransportNotConnectedError, TransportRequestError, TransportRequestTimeoutError, TransportRoomError, TransportRoomTimeoutError, } from '../../core/domain/errors/transport-error.js';
4
+ import { processLog } from '../../utils/process-logger.js';
5
+ /**
6
+ * Log transport client events for debugging reconnection issues.
7
+ */
8
+ function clientLog(message) {
9
+ processLog(`[TransportClient] ${message}`);
10
+ }
11
+ /**
12
+ * Force reconnect delays after Socket.IO gives up (exponential backoff).
13
+ * Used when all built-in reconnection attempts fail.
14
+ */
15
+ const FORCE_RECONNECT_DELAYS = [5000, 10_000, 20_000, 30_000, 60_000]; // 5s, 10s, 20s, 30s, 60s cap
16
+ /**
17
+ * Maximum number of force reconnect attempts before giving up.
18
+ * After this, ProcessManager's periodic health check will detect the failure and restart.
19
+ */
20
+ const MAX_FORCE_RECONNECT_ATTEMPTS = 10;
21
+ /**
22
+ * Interval for checking time jumps that indicate system wake from sleep.
23
+ * Uses a short interval so we detect wake quickly.
24
+ */
25
+ const WAKE_DETECTION_INTERVAL_MS = 5000;
26
+ /**
27
+ * Time jump threshold to detect wake from sleep/hibernate.
28
+ * If the actual elapsed time exceeds expected by this amount, system likely woke from sleep.
29
+ */
30
+ const WAKE_TIME_JUMP_THRESHOLD_MS = 10_000;
4
31
  /**
5
32
  * Socket.IO implementation of ITransportClient.
6
33
  *
@@ -8,15 +35,29 @@ import { TransportConnectionError, TransportNotConnectedError, TransportRequestE
8
35
  * - Auto-reconnects with exponential backoff
9
36
  * - Request/response uses Socket.IO acknowledgements (callbacks)
10
37
  * - Connection state is tracked and exposed via onStateChange
38
+ * - Force reconnect after all built-in attempts fail (sleep/wake recovery)
39
+ * - Auto-rejoins rooms after reconnect
11
40
  */
12
41
  export class SocketIOTransportClient {
13
42
  config;
14
43
  eventHandlers = new Map();
44
+ /** Track force reconnect attempt count for backoff calculation */
45
+ forceReconnectAttempt = 0;
46
+ /** Timer for scheduled force reconnect */
47
+ forceReconnectTimer;
48
+ /** Track joined rooms for auto-rejoin after reconnect */
49
+ joinedRooms = new Set();
50
+ /** Last time we checked for wake (for detecting time jumps) */
51
+ lastWakeCheckTime = Date.now();
15
52
  /** Track which events have socket listeners registered (prevents duplicates on reconnect) */
16
53
  registeredSocketEvents = new Set();
54
+ /** Store server URL for force reconnect */
55
+ serverUrl;
17
56
  socket;
18
57
  state = 'disconnected';
19
58
  stateHandlers = new Set();
59
+ /** Timer for wake detection */
60
+ wakeDetectionTimer;
20
61
  constructor(config) {
21
62
  this.config = {
22
63
  connectTimeoutMs: config?.connectTimeoutMs ?? TRANSPORT_CONNECT_TIMEOUT_MS,
@@ -32,6 +73,20 @@ export class SocketIOTransportClient {
32
73
  if (this.socket?.connected) {
33
74
  return;
34
75
  }
76
+ // Cleanup existing socket if present but not connected
77
+ // This prevents resource leaks when connect() is called again after a failed connection
78
+ if (this.socket) {
79
+ this.socket.removeAllListeners();
80
+ this.socket.io.removeAllListeners();
81
+ this.socket.disconnect();
82
+ this.socket = undefined;
83
+ this.registeredSocketEvents.clear();
84
+ }
85
+ // Store URL for force reconnect
86
+ this.serverUrl = url;
87
+ // Clear any pending force reconnect timer (we're connecting now)
88
+ // Do NOT reset counter - connection may still fail, need to preserve backoff
89
+ this.clearForceReconnectTimer();
35
90
  return new Promise((resolve, reject) => {
36
91
  this.setState('connecting');
37
92
  this.socket = io(url, {
@@ -51,6 +106,8 @@ export class SocketIOTransportClient {
51
106
  // This fixes the issue where on() called before connect() would not
52
107
  // actually register handlers on the socket.
53
108
  this.registerPendingEventHandlers();
109
+ // Start wake detection to handle sleep/hibernate recovery
110
+ this.startWakeDetection();
54
111
  resolve();
55
112
  };
56
113
  const onConnectError = (error) => {
@@ -74,7 +131,8 @@ export class SocketIOTransportClient {
74
131
  this.socket.on('connect', onConnect);
75
132
  this.socket.once('connect_error', onConnectError);
76
133
  // Set up persistent event handlers
77
- this.socket.on('disconnect', () => {
134
+ this.socket.on('disconnect', (reason) => {
135
+ clientLog(`Socket disconnected, reason: ${reason}, active: ${this.socket?.active}`);
78
136
  if (this.socket?.active) {
79
137
  // Socket.IO is attempting to reconnect
80
138
  this.setState('reconnecting');
@@ -83,19 +141,31 @@ export class SocketIOTransportClient {
83
141
  this.setState('disconnected');
84
142
  }
85
143
  });
86
- this.socket.io.on('reconnect', () => {
144
+ this.socket.io.on('reconnect', (attemptNumber) => {
145
+ clientLog(`Socket.IO built-in reconnect succeeded after ${attemptNumber} attempts`);
87
146
  this.setState('connected');
88
147
  // Re-register event handlers after reconnect
89
148
  // Clear tracking first since socket listeners were reset during reconnect
90
149
  this.registeredSocketEvents.clear();
91
150
  this.registerPendingEventHandlers();
151
+ // Auto-rejoin rooms after reconnect
152
+ // Use process.nextTick to ensure socket.connected is true
153
+ // (reconnect event fires before socket.connected is updated)
154
+ process.nextTick(() => this.rejoinRooms());
92
155
  });
93
156
  this.socket.io.on('reconnect_failed', () => {
157
+ clientLog('Socket.IO built-in reconnection failed after all attempts, starting force reconnect');
94
158
  this.setState('disconnected');
159
+ // Start force reconnect loop after Socket.IO gives up
160
+ this.scheduleForceReconnect();
95
161
  });
96
162
  });
97
163
  }
98
164
  async disconnect() {
165
+ // Cancel any pending force reconnect
166
+ this.cancelForceReconnect();
167
+ // Stop wake detection
168
+ this.stopWakeDetection();
99
169
  const { socket } = this;
100
170
  if (!socket) {
101
171
  return;
@@ -107,10 +177,12 @@ export class SocketIOTransportClient {
107
177
  socket.disconnect();
108
178
  this.socket = undefined;
109
179
  this.setState('disconnected');
110
- // Clear all tracking state to prevent leaks
180
+ // Clear socket-specific tracking state to prevent leaks
181
+ // Note: stateHandlers are NOT cleared - subscriptions persist across disconnect/connect cycles
182
+ // Users can unsubscribe via the returned function from onStateChange()
111
183
  this.eventHandlers.clear();
112
184
  this.registeredSocketEvents.clear();
113
- this.stateHandlers.clear();
185
+ this.joinedRooms.clear();
114
186
  resolve();
115
187
  });
116
188
  }
@@ -120,18 +192,52 @@ export class SocketIOTransportClient {
120
192
  getState() {
121
193
  return this.state;
122
194
  }
195
+ /**
196
+ * Checks if the socket is actually connected and responsive.
197
+ * Uses Socket.IO's built-in volatile emit with acknowledgement to verify bidirectional communication.
198
+ * @param timeoutMs - Timeout in milliseconds (default: 2000)
199
+ * @returns true if socket is connected and responsive, false otherwise
200
+ */
201
+ async isConnected(timeoutMs = 2000) {
202
+ const { socket } = this;
203
+ // Quick check: if socket doesn't exist or isn't marked as connected, return false immediately
204
+ if (!socket?.connected) {
205
+ return false;
206
+ }
207
+ // Verify actual bidirectional communication with a ping
208
+ return new Promise((resolve) => {
209
+ const timeout = setTimeout(() => {
210
+ resolve(false);
211
+ }, timeoutMs);
212
+ // Use Socket.IO's built-in ping mechanism via volatile emit
213
+ // Volatile means if the message can't be sent, it won't be buffered
214
+ socket.volatile.emit('ping', { timestamp: Date.now() }, () => {
215
+ clearTimeout(timeout);
216
+ resolve(true);
217
+ });
218
+ });
219
+ }
123
220
  async joinRoom(room) {
124
221
  const { socket } = this;
125
222
  if (!socket?.connected) {
126
223
  throw new TransportNotConnectedError('joinRoom');
127
224
  }
128
225
  return new Promise((resolve, reject) => {
226
+ // Flag to prevent callback from executing after timeout
227
+ let resolved = false;
129
228
  const timer = setTimeout(() => {
229
+ resolved = true;
130
230
  reject(new TransportRoomTimeoutError(room, 'join', this.config.roomTimeoutMs));
131
231
  }, this.config.roomTimeoutMs);
132
232
  socket.emit('room:join', room, (response) => {
233
+ // Don't process if timeout already fired
234
+ if (resolved)
235
+ return;
236
+ resolved = true;
133
237
  clearTimeout(timer);
134
238
  if (response.success) {
239
+ // Track joined room for auto-rejoin after reconnect
240
+ this.joinedRooms.add(room);
135
241
  resolve();
136
242
  }
137
243
  else {
@@ -146,12 +252,21 @@ export class SocketIOTransportClient {
146
252
  throw new TransportNotConnectedError('leaveRoom');
147
253
  }
148
254
  return new Promise((resolve, reject) => {
255
+ // Flag to prevent callback from executing after timeout
256
+ let resolved = false;
149
257
  const timer = setTimeout(() => {
258
+ resolved = true;
150
259
  reject(new TransportRoomTimeoutError(room, 'leave', this.config.roomTimeoutMs));
151
260
  }, this.config.roomTimeoutMs);
152
261
  socket.emit('room:leave', room, (response) => {
262
+ // Don't process if timeout already fired
263
+ if (resolved)
264
+ return;
265
+ resolved = true;
153
266
  clearTimeout(timer);
154
267
  if (response.success) {
268
+ // Remove from tracked rooms
269
+ this.joinedRooms.delete(room);
155
270
  resolve();
156
271
  }
157
272
  else {
@@ -167,7 +282,9 @@ export class SocketIOTransportClient {
167
282
  // Register socket listener if connected and not already registered
168
283
  // Use registeredSocketEvents to prevent duplicates across reconnects
169
284
  this.registerSocketEventIfNeeded(event);
170
- // Wrap handler to store without type assertion
285
+ // Wrap handler to match StoredEventHandler signature.
286
+ // BOUNDARY CAST: Socket.IO delivers unknown data; caller specifies T via generic.
287
+ // Type guard not possible for generic T at runtime.
171
288
  const wrappedHandler = (data) => handler(data);
172
289
  const handlers = this.eventHandlers.get(event);
173
290
  handlers?.add(wrappedHandler);
@@ -188,6 +305,14 @@ export class SocketIOTransportClient {
188
305
  }
189
306
  socket.once(event, handler);
190
307
  }
308
+ /**
309
+ * Subscribe to connection state changes.
310
+ *
311
+ * Important: Subscriptions persist across disconnect/connect cycles. Call the returned
312
+ * function to unsubscribe when no longer needed to prevent memory leaks.
313
+ *
314
+ * @returns Unsubscribe function - call to remove the handler
315
+ */
191
316
  onStateChange(handler) {
192
317
  this.stateHandlers.add(handler);
193
318
  return () => {
@@ -210,8 +335,9 @@ export class SocketIOTransportClient {
210
335
  resolve(response.data);
211
336
  }
212
337
  else if (response.success) {
213
- // Response success but data is undefined - resolve with undefined cast
214
- // This is a boundary case where server returns void
338
+ // BOUNDARY CAST: Server returned success without data (void response).
339
+ // Caller must specify TResponse=void for void endpoints.
340
+ // Type guard not applicable - void vs non-void is a type-level distinction.
215
341
  resolve(undefined);
216
342
  }
217
343
  else {
@@ -220,6 +346,88 @@ export class SocketIOTransportClient {
220
346
  });
221
347
  });
222
348
  }
349
+ /**
350
+ * Attempt force reconnect after Socket.IO gives up.
351
+ * Creates a new socket and connects.
352
+ * Gives up after MAX_FORCE_RECONNECT_ATTEMPTS (ProcessManager will detect via periodic health check).
353
+ */
354
+ async attemptForceReconnect() {
355
+ if (!this.serverUrl || this.state === 'connected') {
356
+ return;
357
+ }
358
+ // Check max attempts - give up if exceeded
359
+ // ProcessManager's periodic health check (30s) will detect and restart
360
+ if (this.forceReconnectAttempt >= MAX_FORCE_RECONNECT_ATTEMPTS) {
361
+ clientLog(`Force reconnect gave up after ${MAX_FORCE_RECONNECT_ATTEMPTS} attempts`);
362
+ this.setState('disconnected');
363
+ return;
364
+ }
365
+ clientLog(`Force reconnect attempt ${this.forceReconnectAttempt + 1}/${MAX_FORCE_RECONNECT_ATTEMPTS}`);
366
+ try {
367
+ // Cleanup old socket completely
368
+ if (this.socket) {
369
+ this.socket.removeAllListeners();
370
+ this.socket.io.removeAllListeners();
371
+ this.socket.disconnect();
372
+ this.socket = undefined;
373
+ }
374
+ // Reset tracking (will be re-populated on connect)
375
+ this.registeredSocketEvents.clear();
376
+ // Attempt to connect
377
+ await this.connect(this.serverUrl);
378
+ // Rejoin rooms after force reconnect (same as built-in reconnect)
379
+ // Without this, TUI REPL won't receive events from broadcast-room
380
+ // Use process.nextTick for consistency with built-in reconnect handler
381
+ clientLog(`Force reconnect succeeded, rejoining ${this.joinedRooms.size} rooms`);
382
+ process.nextTick(() => this.rejoinRooms());
383
+ // Success - reset attempt counter
384
+ this.forceReconnectAttempt = 0;
385
+ }
386
+ catch (error) {
387
+ // Failed - schedule next attempt
388
+ const errorMsg = error instanceof Error ? error.message : String(error);
389
+ clientLog(`Force reconnect failed: ${errorMsg}`);
390
+ this.scheduleForceReconnect();
391
+ }
392
+ }
393
+ /**
394
+ * Cancel force reconnect completely (clears timer AND resets counter).
395
+ * Used when connection is stable or on disconnect().
396
+ */
397
+ cancelForceReconnect() {
398
+ this.clearForceReconnectTimer();
399
+ this.forceReconnectAttempt = 0;
400
+ }
401
+ /**
402
+ * Clear force reconnect timer only (does NOT reset counter).
403
+ * Used when initiating new connection attempt - counter preserved for backoff continuity.
404
+ */
405
+ clearForceReconnectTimer() {
406
+ if (this.forceReconnectTimer) {
407
+ clearTimeout(this.forceReconnectTimer);
408
+ this.forceReconnectTimer = undefined;
409
+ }
410
+ }
411
+ /**
412
+ * Handle system wake from sleep/hibernate.
413
+ * Re-triggers reconnection if not connected and force reconnect has given up.
414
+ */
415
+ handleWakeFromSleep() {
416
+ // Only take action if disconnected (force reconnect may have given up before sleep)
417
+ if (this.state === 'disconnected' && this.serverUrl) {
418
+ clientLog('handleWakeFromSleep: state is disconnected, restarting force reconnect');
419
+ // Reset attempt counter to get fresh tries after wake
420
+ this.forceReconnectAttempt = 0;
421
+ this.scheduleForceReconnect();
422
+ }
423
+ else if (this.state === 'connected' && this.socket && !this.socket.connected) {
424
+ // State says connected but socket isn't - stale state after wake
425
+ clientLog('handleWakeFromSleep: state mismatch (connected but socket disconnected), triggering reconnect');
426
+ this.setState('disconnected');
427
+ this.forceReconnectAttempt = 0;
428
+ this.scheduleForceReconnect();
429
+ }
430
+ }
223
431
  /**
224
432
  * Register all pending event handlers on the socket.
225
433
  * Called after successful connection to handle handlers added before connect().
@@ -249,6 +457,52 @@ export class SocketIOTransportClient {
249
457
  });
250
458
  this.registeredSocketEvents.add(event);
251
459
  }
460
+ /**
461
+ * Re-join all tracked rooms after reconnect.
462
+ * Retries failed room joins with exponential backoff.
463
+ */
464
+ rejoinRooms() {
465
+ clientLog(`rejoinRooms: rejoining ${this.joinedRooms.size} rooms: [${[...this.joinedRooms].join(', ')}]`);
466
+ for (const room of this.joinedRooms) {
467
+ this.rejoinRoomWithRetry(room, 0);
468
+ }
469
+ }
470
+ /**
471
+ * Rejoin a single room with retry logic.
472
+ * Uses exponential backoff: 50ms, 100ms, 200ms, 400ms, 800ms (max 5 attempts).
473
+ * Retries BOTH when socket not connected AND when room:join fails.
474
+ */
475
+ rejoinRoomWithRetry(room, attempt) {
476
+ const MAX_REJOIN_ATTEMPTS = 5;
477
+ const REJOIN_BASE_DELAY_MS = 50;
478
+ if (attempt >= MAX_REJOIN_ATTEMPTS) {
479
+ clientLog(`rejoinRoomWithRetry: gave up rejoining '${room}' after ${MAX_REJOIN_ATTEMPTS} attempts`);
480
+ return;
481
+ }
482
+ // If socket not connected yet, retry with backoff (don't silent return!)
483
+ // This handles race condition where reconnect event fires before socket.connected is true
484
+ if (!this.socket?.connected) {
485
+ const delay = REJOIN_BASE_DELAY_MS * 2 ** attempt;
486
+ clientLog(`rejoinRoomWithRetry: socket not connected, retrying '${room}' in ${delay}ms (attempt ${attempt + 1}/${MAX_REJOIN_ATTEMPTS})`);
487
+ setTimeout(() => this.rejoinRoomWithRetry(room, attempt + 1), delay);
488
+ return;
489
+ }
490
+ clientLog(`rejoinRoomWithRetry: attempting to rejoin '${room}' (attempt ${attempt + 1}/${MAX_REJOIN_ATTEMPTS})`);
491
+ this.socket.emit('room:join', room, (response) => {
492
+ if (response?.success) {
493
+ clientLog(`rejoinRoomWithRetry: successfully rejoined '${room}'`);
494
+ }
495
+ else if (attempt < MAX_REJOIN_ATTEMPTS - 1) {
496
+ // Retry with exponential backoff
497
+ const delay = REJOIN_BASE_DELAY_MS * 2 ** attempt;
498
+ clientLog(`rejoinRoomWithRetry: room:join failed for '${room}', retrying in ${delay}ms`);
499
+ setTimeout(() => this.rejoinRoomWithRetry(room, attempt + 1), delay);
500
+ }
501
+ else {
502
+ clientLog(`rejoinRoomWithRetry: failed to rejoin '${room}' on final attempt`);
503
+ }
504
+ });
505
+ }
252
506
  /**
253
507
  * Remove socket listener for an event and clear tracking.
254
508
  */
@@ -259,12 +513,59 @@ export class SocketIOTransportClient {
259
513
  this.registeredSocketEvents.delete(event);
260
514
  }
261
515
  }
516
+ /**
517
+ * Schedule force reconnect with exponential backoff.
518
+ * Called after Socket.IO's built-in reconnection gives up.
519
+ */
520
+ scheduleForceReconnect() {
521
+ if (!this.serverUrl) {
522
+ clientLog('scheduleForceReconnect: no serverUrl, skipping');
523
+ return;
524
+ }
525
+ // Clear existing timer to prevent duplicate timers (fixes concurrent reconnect race)
526
+ // This can happen when both 'connect_error' and 'reconnect_failed' schedule timers
527
+ this.clearForceReconnectTimer();
528
+ const delay = FORCE_RECONNECT_DELAYS[Math.min(this.forceReconnectAttempt, FORCE_RECONNECT_DELAYS.length - 1)];
529
+ clientLog(`scheduleForceReconnect: scheduling attempt ${this.forceReconnectAttempt + 1} in ${delay}ms`);
530
+ this.forceReconnectTimer = setTimeout(() => {
531
+ this.forceReconnectAttempt++;
532
+ this.attemptForceReconnect();
533
+ }, delay);
534
+ }
262
535
  setState(state) {
263
536
  if (this.state !== state) {
537
+ clientLog(`State change: ${this.state} -> ${state}`);
264
538
  this.state = state;
265
539
  for (const handler of this.stateHandlers) {
266
540
  handler(state);
267
541
  }
268
542
  }
269
543
  }
544
+ /**
545
+ * Start wake detection timer.
546
+ * Periodically checks for time jumps that indicate system woke from sleep.
547
+ */
548
+ startWakeDetection() {
549
+ this.stopWakeDetection();
550
+ this.lastWakeCheckTime = Date.now();
551
+ this.wakeDetectionTimer = setInterval(() => {
552
+ const now = Date.now();
553
+ const elapsed = now - this.lastWakeCheckTime;
554
+ this.lastWakeCheckTime = now;
555
+ // If elapsed time is much greater than interval, system likely woke from sleep
556
+ if (elapsed > WAKE_DETECTION_INTERVAL_MS + WAKE_TIME_JUMP_THRESHOLD_MS) {
557
+ clientLog(`Wake detected: time jump of ${elapsed}ms (expected ~${WAKE_DETECTION_INTERVAL_MS}ms)`);
558
+ this.handleWakeFromSleep();
559
+ }
560
+ }, WAKE_DETECTION_INTERVAL_MS);
561
+ }
562
+ /**
563
+ * Stop wake detection timer.
564
+ */
565
+ stopWakeDetection() {
566
+ if (this.wakeDetectionTimer) {
567
+ clearInterval(this.wakeDetectionTimer);
568
+ this.wakeDetectionTimer = undefined;
569
+ }
570
+ }
270
571
  }
@@ -145,6 +145,10 @@ export class SocketIOTransportServer {
145
145
  socket.leave(room);
146
146
  callback?.({ success: true });
147
147
  });
148
+ // Handle ping requests for health checks
149
+ socket.on('ping', (_data, callback) => {
150
+ callback?.({ pong: true, timestamp: Date.now() });
151
+ });
148
152
  });
149
153
  this.httpServer.on('error', (err) => {
150
154
  if (err.code === 'EADDRINUSE') {
@@ -0,0 +1,63 @@
1
+ import type { IConnectorManager } from '../../core/interfaces/connectors/i-connector-manager.js';
2
+ import type { ITerminal } from '../../core/interfaces/i-terminal.js';
3
+ import type { ITrackingService } from '../../core/interfaces/i-tracking-service.js';
4
+ import { IConnectorsUseCase } from '../../core/interfaces/usecase/i-connectors-use-case.js';
5
+ /**
6
+ * Options for constructing ConnectorsUseCase.
7
+ */
8
+ export interface ConnectorsUseCaseOptions {
9
+ connectorManager: IConnectorManager;
10
+ terminal: ITerminal;
11
+ trackingService: ITrackingService;
12
+ }
13
+ /**
14
+ * Use case for managing connectors.
15
+ * Shows list of connected agents and allows managing or adding new connections.
16
+ */
17
+ export declare class ConnectorsUseCase implements IConnectorsUseCase {
18
+ private readonly connectorManager;
19
+ private readonly terminal;
20
+ private readonly trackingService;
21
+ constructor(options: ConnectorsUseCaseOptions);
22
+ run(): Promise<void>;
23
+ /**
24
+ * Display manual setup instructions for MCP configuration.
25
+ */
26
+ private displayManualInstructions;
27
+ /**
28
+ * Gets a description for a connector type.
29
+ */
30
+ private getConnectorDescription;
31
+ /**
32
+ * Gets a human-readable label for a connector type.
33
+ */
34
+ private getConnectorLabel;
35
+ /**
36
+ * Handles the case when connectors are already installed.
37
+ */
38
+ private handleExistingConnectors;
39
+ /**
40
+ * Handles the case when no connectors are installed.
41
+ */
42
+ private handleNoConnectorsInstalled;
43
+ /**
44
+ * Installs the selected connector and displays result.
45
+ */
46
+ private installConnector;
47
+ /**
48
+ * Prompts user to select from connected agents or add new.
49
+ */
50
+ private promptForAgentToManage;
51
+ /**
52
+ * Prompts the user to select a connector type.
53
+ */
54
+ private promptForConnectorType;
55
+ /**
56
+ * Prompts user to select a new agent (excludes already connected agents).
57
+ */
58
+ private promptForNewAgentSelection;
59
+ /**
60
+ * Prompts the user to confirm switching connectors.
61
+ */
62
+ private promptForSwitchConfirmation;
63
+ }