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.
- package/README.md +19 -13
- package/dist/commands/hook-prompt-submit.d.ts +27 -0
- package/dist/commands/hook-prompt-submit.js +39 -0
- package/dist/commands/mcp.d.ts +13 -0
- package/dist/commands/mcp.js +61 -0
- package/dist/commands/status.js +8 -3
- package/dist/constants.d.ts +1 -1
- package/dist/constants.js +1 -1
- package/dist/core/domain/cipher/agent-events/types.d.ts +44 -1
- package/dist/core/domain/cipher/tools/constants.d.ts +1 -0
- package/dist/core/domain/cipher/tools/constants.js +1 -0
- package/dist/core/domain/entities/agent.d.ts +16 -0
- package/dist/core/domain/entities/agent.js +78 -0
- package/dist/core/domain/entities/connector-type.d.ts +10 -0
- package/dist/core/domain/entities/connector-type.js +9 -0
- package/dist/core/domain/entities/event.d.ts +1 -1
- package/dist/core/domain/entities/event.js +2 -0
- package/dist/core/domain/errors/task-error.d.ts +4 -0
- package/dist/core/domain/errors/task-error.js +7 -0
- package/dist/core/domain/transport/schemas.d.ts +40 -0
- package/dist/core/domain/transport/schemas.js +28 -0
- package/dist/core/interfaces/connectors/connector-types.d.ts +70 -0
- package/dist/core/interfaces/connectors/i-connector-manager.d.ts +72 -0
- package/dist/core/interfaces/connectors/i-connector-manager.js +1 -0
- package/dist/core/interfaces/connectors/i-connector.d.ts +54 -0
- package/dist/core/interfaces/connectors/i-connector.js +1 -0
- package/dist/core/interfaces/i-file-service.d.ts +7 -0
- package/dist/core/interfaces/i-mcp-config-writer.d.ts +40 -0
- package/dist/core/interfaces/i-mcp-config-writer.js +1 -0
- package/dist/core/interfaces/i-rule-template-service.d.ts +4 -2
- package/dist/core/interfaces/transport/i-transport-client.d.ts +7 -0
- package/dist/core/interfaces/usecase/i-connectors-use-case.d.ts +3 -0
- package/dist/core/interfaces/usecase/i-connectors-use-case.js +1 -0
- package/dist/hooks/init/update-notifier.d.ts +1 -0
- package/dist/hooks/init/update-notifier.js +10 -1
- package/dist/infra/cipher/agent/cipher-agent.d.ts +8 -0
- package/dist/infra/cipher/agent/cipher-agent.js +16 -0
- package/dist/infra/cipher/file-system/binary-utils.d.ts +7 -12
- package/dist/infra/cipher/file-system/binary-utils.js +46 -31
- package/dist/infra/cipher/llm/context/context-manager.d.ts +10 -2
- package/dist/infra/cipher/llm/context/context-manager.js +39 -2
- package/dist/infra/cipher/llm/formatters/gemini-formatter.js +48 -9
- package/dist/infra/cipher/llm/internal-llm-service.d.ts +4 -0
- package/dist/infra/cipher/llm/internal-llm-service.js +40 -12
- package/dist/infra/cipher/session/chat-session.d.ts +3 -0
- package/dist/infra/cipher/session/chat-session.js +7 -1
- package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.d.ts +6 -7
- package/dist/infra/cipher/system-prompt/contributors/context-tree-structure-contributor.js +57 -18
- package/dist/infra/cipher/tools/implementations/curate-tool.d.ts +1 -8
- package/dist/infra/cipher/tools/implementations/curate-tool.js +380 -24
- package/dist/infra/cipher/tools/implementations/read-file-tool.js +38 -17
- package/dist/infra/cipher/tools/implementations/search-knowledge-tool.d.ts +7 -0
- package/dist/infra/cipher/tools/implementations/search-knowledge-tool.js +303 -0
- package/dist/infra/cipher/tools/index.d.ts +1 -0
- package/dist/infra/cipher/tools/index.js +1 -0
- package/dist/infra/cipher/tools/tool-manager.js +1 -0
- package/dist/infra/cipher/tools/tool-registry.js +7 -0
- package/dist/infra/connectors/connector-manager.d.ts +32 -0
- package/dist/infra/connectors/connector-manager.js +158 -0
- package/dist/infra/connectors/hook/hook-connector-config.d.ts +52 -0
- package/dist/infra/connectors/hook/hook-connector-config.js +41 -0
- package/dist/infra/connectors/hook/hook-connector.d.ts +46 -0
- package/dist/infra/connectors/hook/hook-connector.js +231 -0
- package/dist/infra/connectors/mcp/index.d.ts +4 -0
- package/dist/infra/connectors/mcp/index.js +4 -0
- package/dist/infra/connectors/mcp/json-mcp-config-writer.d.ts +26 -0
- package/dist/infra/connectors/mcp/json-mcp-config-writer.js +71 -0
- package/dist/infra/connectors/mcp/mcp-connector-config.d.ts +229 -0
- package/dist/infra/connectors/mcp/mcp-connector-config.js +173 -0
- package/dist/infra/connectors/mcp/mcp-connector.d.ts +80 -0
- package/dist/infra/connectors/mcp/mcp-connector.js +324 -0
- package/dist/infra/connectors/mcp/toml-mcp-config-writer.d.ts +45 -0
- package/dist/infra/connectors/mcp/toml-mcp-config-writer.js +134 -0
- package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.d.ts +2 -2
- package/dist/infra/{rule → connectors/rules}/legacy-rule-detector.js +1 -1
- package/dist/infra/connectors/rules/rules-connector-config.d.ts +95 -0
- package/dist/infra/{rule/agent-rule-config.js → connectors/rules/rules-connector-config.js} +10 -10
- package/dist/infra/connectors/rules/rules-connector.d.ts +34 -0
- package/dist/infra/connectors/rules/rules-connector.js +139 -0
- package/dist/infra/connectors/shared/rule-file-manager.d.ts +72 -0
- package/dist/infra/connectors/shared/rule-file-manager.js +119 -0
- package/dist/infra/connectors/shared/template-service.d.ts +27 -0
- package/dist/infra/connectors/shared/template-service.js +125 -0
- package/dist/infra/context-tree/file-context-tree-writer-service.d.ts +5 -2
- package/dist/infra/context-tree/file-context-tree-writer-service.js +20 -5
- package/dist/infra/core/executors/curate-executor.d.ts +2 -2
- package/dist/infra/core/executors/curate-executor.js +7 -7
- package/dist/infra/core/executors/query-executor.d.ts +12 -0
- package/dist/infra/core/executors/query-executor.js +62 -1
- package/dist/infra/file/fs-file-service.d.ts +7 -0
- package/dist/infra/file/fs-file-service.js +15 -1
- package/dist/infra/mcp/index.d.ts +2 -0
- package/dist/infra/mcp/index.js +2 -0
- package/dist/infra/mcp/mcp-server.d.ts +58 -0
- package/dist/infra/mcp/mcp-server.js +178 -0
- package/dist/infra/mcp/tools/brv-curate-tool.d.ts +23 -0
- package/dist/infra/mcp/tools/brv-curate-tool.js +68 -0
- package/dist/infra/mcp/tools/brv-query-tool.d.ts +17 -0
- package/dist/infra/mcp/tools/brv-query-tool.js +68 -0
- package/dist/infra/mcp/tools/index.d.ts +3 -0
- package/dist/infra/mcp/tools/index.js +3 -0
- package/dist/infra/mcp/tools/task-result-waiter.d.ts +30 -0
- package/dist/infra/mcp/tools/task-result-waiter.js +56 -0
- package/dist/infra/process/agent-worker.d.ts +2 -2
- package/dist/infra/process/agent-worker.js +663 -142
- package/dist/infra/process/constants.d.ts +1 -1
- package/dist/infra/process/constants.js +1 -1
- package/dist/infra/process/ipc-types.d.ts +17 -4
- package/dist/infra/process/ipc-types.js +3 -3
- package/dist/infra/process/parent-heartbeat.d.ts +47 -0
- package/dist/infra/process/parent-heartbeat.js +118 -0
- package/dist/infra/process/process-manager.d.ts +79 -0
- package/dist/infra/process/process-manager.js +277 -3
- package/dist/infra/process/task-queue-manager.d.ts +13 -0
- package/dist/infra/process/task-queue-manager.js +19 -0
- package/dist/infra/process/transport-handlers.d.ts +3 -0
- package/dist/infra/process/transport-handlers.js +51 -5
- package/dist/infra/process/transport-worker.js +9 -69
- package/dist/infra/repl/commands/connectors-command.d.ts +8 -0
- package/dist/infra/repl/commands/{gen-rules-command.js → connectors-command.js} +21 -10
- package/dist/infra/repl/commands/curate-command.js +2 -2
- package/dist/infra/repl/commands/index.js +3 -2
- package/dist/infra/repl/commands/init-command.js +11 -7
- package/dist/infra/repl/commands/query-command.js +22 -2
- package/dist/infra/repl/commands/reset-command.js +1 -1
- package/dist/infra/transport/socket-io-transport-client.d.ts +75 -0
- package/dist/infra/transport/socket-io-transport-client.js +308 -7
- package/dist/infra/transport/socket-io-transport-server.js +4 -0
- package/dist/infra/usecase/connectors-use-case.d.ts +63 -0
- package/dist/infra/usecase/connectors-use-case.js +222 -0
- package/dist/infra/usecase/init-use-case.d.ts +8 -43
- package/dist/infra/usecase/init-use-case.js +27 -252
- package/dist/infra/usecase/logout-use-case.js +1 -1
- package/dist/infra/usecase/pull-use-case.js +5 -5
- package/dist/infra/usecase/push-use-case.js +4 -4
- package/dist/infra/usecase/reset-use-case.js +3 -4
- package/dist/infra/usecase/space-list-use-case.js +3 -3
- package/dist/infra/usecase/space-switch-use-case.js +3 -3
- package/dist/infra/usecase/status-use-case.d.ts +10 -0
- package/dist/infra/usecase/status-use-case.js +53 -0
- package/dist/resources/prompts/curate.yml +114 -4
- package/dist/resources/prompts/explore.yml +34 -0
- package/dist/resources/prompts/query-orchestrator.yml +112 -0
- package/dist/resources/prompts/system-prompt.yml +12 -2
- package/dist/resources/tools/search_knowledge.txt +32 -0
- package/dist/templates/mcp-base.md +1 -0
- package/dist/templates/sections/brv-instructions.md +98 -0
- package/dist/templates/sections/mcp-workflow.md +13 -0
- package/dist/tui/app.js +4 -1
- package/dist/tui/components/command-details.js +1 -1
- package/dist/tui/components/execution/execution-changes.d.ts +2 -0
- package/dist/tui/components/execution/execution-changes.js +5 -1
- package/dist/tui/components/execution/execution-content.d.ts +2 -0
- package/dist/tui/components/execution/execution-content.js +8 -18
- package/dist/tui/components/execution/execution-input.d.ts +2 -0
- package/dist/tui/components/execution/execution-input.js +6 -4
- package/dist/tui/components/execution/execution-progress.d.ts +2 -0
- package/dist/tui/components/execution/execution-progress.js +6 -2
- package/dist/tui/components/execution/expanded-log-view.d.ts +20 -0
- package/dist/tui/components/execution/expanded-log-view.js +75 -0
- package/dist/tui/components/execution/expanded-message-view.d.ts +24 -0
- package/dist/tui/components/execution/expanded-message-view.js +68 -0
- package/dist/tui/components/execution/index.d.ts +2 -0
- package/dist/tui/components/execution/index.js +2 -0
- package/dist/tui/components/execution/log-item.d.ts +4 -0
- package/dist/tui/components/execution/log-item.js +2 -2
- package/dist/tui/components/footer.js +1 -1
- package/dist/tui/components/index.d.ts +2 -1
- package/dist/tui/components/index.js +2 -1
- package/dist/tui/components/init.js +2 -9
- package/dist/tui/components/logo.js +4 -3
- package/dist/tui/components/markdown.d.ts +13 -0
- package/dist/tui/components/markdown.js +88 -0
- package/dist/tui/components/message-item.js +1 -1
- package/dist/tui/components/onboarding/onboarding-flow.js +14 -11
- package/dist/tui/components/onboarding/welcome-box.js +1 -1
- package/dist/tui/components/suggestions.js +3 -3
- package/dist/tui/contexts/mode-context.js +6 -2
- package/dist/tui/contexts/onboarding-context.d.ts +4 -0
- package/dist/tui/contexts/onboarding-context.js +14 -2
- package/dist/tui/hooks/index.d.ts +1 -0
- package/dist/tui/hooks/index.js +1 -0
- package/dist/tui/hooks/use-is-latest-version.d.ts +6 -0
- package/dist/tui/hooks/use-is-latest-version.js +22 -0
- package/dist/tui/views/command-view.d.ts +1 -1
- package/dist/tui/views/command-view.js +87 -98
- package/dist/tui/views/logs-view.d.ts +8 -0
- package/dist/tui/views/logs-view.js +55 -27
- package/dist/utils/file-validator.d.ts +1 -1
- package/dist/utils/file-validator.js +25 -28
- package/dist/utils/type-guards.d.ts +5 -0
- package/dist/utils/type-guards.js +7 -0
- package/oclif.manifest.json +55 -4
- package/package.json +12 -1
- package/dist/core/interfaces/usecase/i-generate-rules-use-case.d.ts +0 -3
- package/dist/infra/repl/commands/gen-rules-command.d.ts +0 -7
- package/dist/infra/rule/agent-rule-config.d.ts +0 -19
- package/dist/infra/rule/rule-template-service.d.ts +0 -18
- package/dist/infra/rule/rule-template-service.js +0 -88
- package/dist/infra/usecase/generate-rules-use-case.d.ts +0 -61
- package/dist/infra/usecase/generate-rules-use-case.js +0 -285
- /package/dist/core/interfaces/{usecase/i-generate-rules-use-case.js → connectors/connector-types.js} +0 -0
- /package/dist/infra/{rule → connectors/shared}/constants.d.ts +0 -0
- /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
|
|
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.
|
|
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
|
|
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
|
-
//
|
|
214
|
-
//
|
|
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
|
+
}
|