interceptpilot-mcp 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.
- package/LICENSE +21 -0
- package/README.md +116 -0
- package/package.json +44 -0
- package/src/daemon-client.js +281 -0
- package/src/daemon.js +312 -0
- package/src/import-bundle-schema.js +221 -0
- package/src/index.js +113 -0
- package/src/mcp-server.js +674 -0
- package/src/process-manager.js +36 -0
- package/src/protocol.js +151 -0
- package/src/websocket-bridge.js +228 -0
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Anderson Alpin
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
ADDED
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
# interceptpilot-mcp
|
|
2
|
+
|
|
3
|
+
Local [MCP](https://modelcontextprotocol.io) server for the InterceptPilot Chrome extension. It lets AI agents (Claude Code, Codex, Cursor and any other MCP client) read the extension's sanitized test context, inspect captured requests, rules and logs, and propose or run actions such as enabling a mock, importing a collection or capturing a tab.
|
|
4
|
+
|
|
5
|
+
Everything stays on your machine: the server talks to the extension over a local WebSocket on `127.0.0.1`, protected by a session key that the extension generates.
|
|
6
|
+
|
|
7
|
+
## Setup
|
|
8
|
+
|
|
9
|
+
1. Open the InterceptPilot Full App, go to **AI Bridge** and click **Start**.
|
|
10
|
+
2. Click **Copy configuration**. You get something like:
|
|
11
|
+
|
|
12
|
+
```json
|
|
13
|
+
{
|
|
14
|
+
"mcpServers": {
|
|
15
|
+
"interceptpilot": {
|
|
16
|
+
"command": "npx",
|
|
17
|
+
"args": ["-y", "interceptpilot-mcp", "--port", "37177", "--key", "<SESSION_KEY>"]
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
3. Add it to your MCP client and restart the client:
|
|
24
|
+
- **Claude Code**: `claude mcp add-json interceptpilot '<the "interceptpilot" object>'`, or paste it into `~/.claude.json`.
|
|
25
|
+
- **Cursor**: `.cursor/mcp.json` (project) or `~/.cursor/mcp.json` (global).
|
|
26
|
+
- **Claude Desktop**: `claude_desktop_config.json`.
|
|
27
|
+
- **Codex**: `~/.codex/config.toml`, as `[mcp_servers.interceptpilot]` with the same `command` and `args`.
|
|
28
|
+
4. Back in the extension, the AI Bridge status changes to **Connected**.
|
|
29
|
+
|
|
30
|
+
The key is per session. Regenerating it in the extension invalidates the configuration you copied before. Never paste a real key into issues, logs or documentation.
|
|
31
|
+
|
|
32
|
+
## How it works
|
|
33
|
+
|
|
34
|
+
```txt
|
|
35
|
+
AI client
|
|
36
|
+
-> MCP adapter (this package, stdio)
|
|
37
|
+
-> shared local daemon (one per port)
|
|
38
|
+
-> WebSocket connection from the extension
|
|
39
|
+
-> InterceptPilot
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
The MCP client starts the adapter. The adapter starts the local daemon when none is listening on the configured port; several adapters (several chats or clients) share one daemon and one key. If the extension disconnects, the daemon stays up and the tools return a safe error until the extension reconnects.
|
|
43
|
+
|
|
44
|
+
## Options
|
|
45
|
+
|
|
46
|
+
| Option | Meaning |
|
|
47
|
+
| --- | --- |
|
|
48
|
+
| `--key <KEY>` | Session key shown by the AI Bridge page. Required. |
|
|
49
|
+
| `--port <PORT>` | Local daemon port. Default `37177`. |
|
|
50
|
+
| `--no-daemon-autostart` | Do not start the daemon automatically (debugging). |
|
|
51
|
+
| `daemon` | Run only the daemon: `npx -y interceptpilot-mcp daemon --port <PORT> --key <KEY>`. |
|
|
52
|
+
|
|
53
|
+
## Tools
|
|
54
|
+
|
|
55
|
+
What each tool exposes depends on the permissions you set in the extension. By default query-string values, headers, bodies, cookies, `Authorization` values and tab titles are never sent, and every action that changes state opens a proposal you confirm in the Full App. Session permissions let you skip the confirmation for specific groups of actions.
|
|
56
|
+
|
|
57
|
+
### Reading
|
|
58
|
+
|
|
59
|
+
| Tool | Use it to |
|
|
60
|
+
| --- | --- |
|
|
61
|
+
| `get_current_test_context` | See capture status and mode, the active tab origin and path, the active collection, rule counts and the AI Bridge status. |
|
|
62
|
+
| `list_captured_requests` | List the requests captured in the active context. |
|
|
63
|
+
| `search_captured_requests` | Search captured requests with safe filters and pagination. |
|
|
64
|
+
| `list_collections` | List collections with rule counts. |
|
|
65
|
+
| `get_active_collection` | Get the active collection. |
|
|
66
|
+
| `list_rules` | List rules with sanitized match and action summaries. |
|
|
67
|
+
| `search_rules` | Search rules by metadata, match, action and diagnostics. |
|
|
68
|
+
| `list_sanitized_logs` | Read recent logs from the active context. |
|
|
69
|
+
| `search_sanitized_logs` | Search logs with safe filters and pagination. |
|
|
70
|
+
| `list_recent_rule_results` | See which rules matched recent requests and what was applied. |
|
|
71
|
+
| `search_rule_results` | Search rule results with safe filters and pagination. |
|
|
72
|
+
| `explain_rule_match` | Compare a rule with a captured request and learn why it matched or not. |
|
|
73
|
+
| `get_import_bundle_schema` | Get the import bundle schema. Works without the extension connected. |
|
|
74
|
+
| `get_import_bundle_examples` | Get safe example bundles. Works without the extension connected. |
|
|
75
|
+
|
|
76
|
+
### Actions
|
|
77
|
+
|
|
78
|
+
| Tool | Use it to | Skips confirmation when |
|
|
79
|
+
| --- | --- | --- |
|
|
80
|
+
| `import_bundle` | Propose a rule or collection bundle. | "Apply imports without confirmation" is on. |
|
|
81
|
+
| `request_enable_rule`, `request_disable_rule`, `request_set_only_active_rule`, `request_set_active_collection` | Toggle rules or switch the active collection. | "Rule and collection actions without confirmation" is on. |
|
|
82
|
+
| `request_update_rule`, `request_update_collection` | Edit safe fields of a rule or collection, with a before/after diff. `responseBody` and `responseHeaders` cannot be edited. | "Rule and collection actions without confirmation" is on. |
|
|
83
|
+
| `request_delete_rule`, `request_delete_collection` | Delete a rule or collection. | "Delete rules and collections without confirmation" is on. |
|
|
84
|
+
| `request_reload_captured_tab` | Reload only the captured tab to validate a mock. Never reloads other or internal pages. | "Reload captured tab without confirmation" is on. |
|
|
85
|
+
| `request_start_capture`, `request_restart_capture`, `request_stop_capture`, `request_capture_current_tab`, `request_set_capture_mode` | Control capture on the eligible web tab; modes are `auto`, `full` and `light`. | "Capture control without confirmation" is on. |
|
|
86
|
+
|
|
87
|
+
## Security
|
|
88
|
+
|
|
89
|
+
- Local only: the daemon listens on `127.0.0.1`.
|
|
90
|
+
- The session key is required from both the extension and every adapter, and is never echoed in logs, errors or tool results.
|
|
91
|
+
- Sanitized by default; sensitive data is only sent when the matching permission is on.
|
|
92
|
+
- State changes are proposals confirmed by you unless you enable the matching session permission.
|
|
93
|
+
- The daemon accepts an allowlist of commands. There are no tools to clear logs, drive the debugger or reload arbitrary pages.
|
|
94
|
+
|
|
95
|
+
## Troubleshooting
|
|
96
|
+
|
|
97
|
+
**Extension not connected.** Open the Full App, go to AI Bridge and click Start. Tools return a safe error until the extension connects.
|
|
98
|
+
|
|
99
|
+
**Daemon did not start.** Run it by hand to see the error: `npx -y interceptpilot-mcp daemon --port <PORT> --key <KEY>`.
|
|
100
|
+
|
|
101
|
+
**Port in use.** A healthy daemon on the port is reused by other adapters. If an old process is stuck, stop it or change the port in both the AI Bridge page and the client configuration.
|
|
102
|
+
|
|
103
|
+
**Invalid or regenerated key.** Copy the configuration again from the AI Bridge page.
|
|
104
|
+
|
|
105
|
+
**Client using an old configuration.** Update the client configuration and restart the client. Permission changes happen in the extension and do not require a new key.
|
|
106
|
+
|
|
107
|
+
## Development
|
|
108
|
+
|
|
109
|
+
```bash
|
|
110
|
+
npm install
|
|
111
|
+
npm test
|
|
112
|
+
```
|
|
113
|
+
|
|
114
|
+
## License
|
|
115
|
+
|
|
116
|
+
[MIT](./LICENSE)
|
package/package.json
ADDED
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "interceptpilot-mcp",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Local MCP server for InterceptPilot",
|
|
5
|
+
"license": "MIT",
|
|
6
|
+
"type": "module",
|
|
7
|
+
"bin": {
|
|
8
|
+
"interceptpilot-mcp": "src/index.js"
|
|
9
|
+
},
|
|
10
|
+
"files": [
|
|
11
|
+
"src",
|
|
12
|
+
"README.md",
|
|
13
|
+
"LICENSE"
|
|
14
|
+
],
|
|
15
|
+
"engines": {
|
|
16
|
+
"node": ">=18"
|
|
17
|
+
},
|
|
18
|
+
"keywords": [
|
|
19
|
+
"mcp",
|
|
20
|
+
"model-context-protocol",
|
|
21
|
+
"interceptpilot",
|
|
22
|
+
"chrome-extension",
|
|
23
|
+
"api-mock",
|
|
24
|
+
"network-interception"
|
|
25
|
+
],
|
|
26
|
+
"repository": {
|
|
27
|
+
"type": "git",
|
|
28
|
+
"url": "git+https://github.com/AndersonAlpin/interceptpilot-mcp.git"
|
|
29
|
+
},
|
|
30
|
+
"bugs": {
|
|
31
|
+
"url": "https://github.com/AndersonAlpin/interceptpilot-mcp/issues"
|
|
32
|
+
},
|
|
33
|
+
"homepage": "https://github.com/AndersonAlpin/interceptpilot-mcp#readme",
|
|
34
|
+
"scripts": {
|
|
35
|
+
"start": "node src/index.js",
|
|
36
|
+
"check": "node --check src/index.js src/import-bundle-schema.js src/mcp-server.js src/protocol.js src/websocket-bridge.js src/daemon.js src/daemon-client.js src/process-manager.js",
|
|
37
|
+
"test": "node --test tests/*.test.js"
|
|
38
|
+
},
|
|
39
|
+
"dependencies": {
|
|
40
|
+
"@modelcontextprotocol/sdk": "1.30.0",
|
|
41
|
+
"ws": "8.21.0",
|
|
42
|
+
"zod": "4.4.3"
|
|
43
|
+
}
|
|
44
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
import WebSocket from 'ws'
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
BRIDGE_HOST,
|
|
5
|
+
BRIDGE_PORT,
|
|
6
|
+
BRIDGE_PROTOCOL_VERSION,
|
|
7
|
+
MCP_CLIENT_SOURCE,
|
|
8
|
+
MESSAGE_TYPES,
|
|
9
|
+
isAllowedCommand,
|
|
10
|
+
isClientResponse,
|
|
11
|
+
parseBridgeMessage
|
|
12
|
+
} from './protocol.js'
|
|
13
|
+
import { startDaemonProcess as defaultStartDaemonProcess } from './process-manager.js'
|
|
14
|
+
|
|
15
|
+
const DEFAULT_REQUEST_TIMEOUT_MS = 5000
|
|
16
|
+
const DEFAULT_RETRY_DELAY_MS = 100
|
|
17
|
+
const DEFAULT_MAX_PENDING_REQUESTS = 25
|
|
18
|
+
const DEFAULT_MAX_CONNECT_ATTEMPTS = 30
|
|
19
|
+
const TIMEOUT_MESSAGE = 'InterceptPilot did not respond in time.'
|
|
20
|
+
const DISCONNECTED_MESSAGE = 'InterceptPilot daemon disconnected.'
|
|
21
|
+
const COMMAND_NOT_ALLOWED_MESSAGE = 'Command is not allowed.'
|
|
22
|
+
const TOO_MANY_PENDING_MESSAGE = 'Too many pending InterceptPilot requests.'
|
|
23
|
+
const DAEMON_NOT_RUNNING_MESSAGE = 'InterceptPilot daemon is not running. Start AI Bridge or run without --no-daemon-autostart.'
|
|
24
|
+
const DAEMON_START_FAILED_MESSAGE = 'Unable to start InterceptPilot daemon.'
|
|
25
|
+
|
|
26
|
+
function createDefaultLogger() {
|
|
27
|
+
return {
|
|
28
|
+
info: message => console.error(`[interceptpilot-mcp] ${message}`),
|
|
29
|
+
warn: message => console.error(`[interceptpilot-mcp] ${message}`),
|
|
30
|
+
error: message => console.error(`[interceptpilot-mcp] ${message}`)
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function addSocketListener(socket, event, handler) {
|
|
35
|
+
if (typeof socket.on === 'function') {
|
|
36
|
+
socket.on(event, handler)
|
|
37
|
+
return
|
|
38
|
+
}
|
|
39
|
+
socket[`on${event}`] = handler
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function sendJson(socket, message) {
|
|
43
|
+
socket.send(JSON.stringify(message))
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function createSafeError(message) {
|
|
47
|
+
return new Error(message)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getMessageData(raw) {
|
|
51
|
+
if (raw && typeof raw === 'object' && 'data' in raw) return raw.data
|
|
52
|
+
return raw
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export function createDaemonClientBridge(options = {}) {
|
|
56
|
+
const {
|
|
57
|
+
WebSocketCtor = WebSocket,
|
|
58
|
+
host = BRIDGE_HOST,
|
|
59
|
+
port = BRIDGE_PORT,
|
|
60
|
+
expectedKey = '',
|
|
61
|
+
daemonAutostart = true,
|
|
62
|
+
requestTimeoutMs = DEFAULT_REQUEST_TIMEOUT_MS,
|
|
63
|
+
maxPendingRequests = DEFAULT_MAX_PENDING_REQUESTS,
|
|
64
|
+
maxConnectAttempts = DEFAULT_MAX_CONNECT_ATTEMPTS,
|
|
65
|
+
retryDelayMs = DEFAULT_RETRY_DELAY_MS,
|
|
66
|
+
idGenerator,
|
|
67
|
+
clientId = 'interceptpilot-mcp',
|
|
68
|
+
logger = createDefaultLogger(),
|
|
69
|
+
startDaemonProcess = defaultStartDaemonProcess,
|
|
70
|
+
setTimeoutFn = setTimeout,
|
|
71
|
+
clearTimeoutFn = clearTimeout
|
|
72
|
+
} = options
|
|
73
|
+
|
|
74
|
+
const connectionKey = String(expectedKey || '')
|
|
75
|
+
const pending = new Map()
|
|
76
|
+
let socket = null
|
|
77
|
+
let connected = false
|
|
78
|
+
let requestSeq = 0
|
|
79
|
+
let startPromise = null
|
|
80
|
+
|
|
81
|
+
function nextRequestId() {
|
|
82
|
+
if (idGenerator) return idGenerator()
|
|
83
|
+
requestSeq += 1
|
|
84
|
+
return `client_req_${requestSeq}`
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function isConnected() {
|
|
88
|
+
return Boolean(socket && connected)
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function rejectPending(message) {
|
|
92
|
+
for (const [, request] of pending) {
|
|
93
|
+
clearTimeoutFn(request.timer)
|
|
94
|
+
request.reject(createSafeError(message))
|
|
95
|
+
}
|
|
96
|
+
pending.clear()
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function handleResponse(message) {
|
|
100
|
+
if (!isClientResponse(message)) return
|
|
101
|
+
const request = pending.get(message.id)
|
|
102
|
+
if (!request) return
|
|
103
|
+
pending.delete(message.id)
|
|
104
|
+
clearTimeoutFn(request.timer)
|
|
105
|
+
|
|
106
|
+
if (message.type === MESSAGE_TYPES.clientError || message.ok === false) {
|
|
107
|
+
request.reject(createSafeError(String(message.error?.message || 'InterceptPilot command failed.')))
|
|
108
|
+
return
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
request.resolve(message.result)
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function handleSocketClose(activeSocket) {
|
|
115
|
+
if (socket !== activeSocket) return
|
|
116
|
+
connected = false
|
|
117
|
+
socket = null
|
|
118
|
+
startPromise = null
|
|
119
|
+
rejectPending(DISCONNECTED_MESSAGE)
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
function schedule(fn) {
|
|
123
|
+
if (retryDelayMs <= 0) {
|
|
124
|
+
fn()
|
|
125
|
+
return
|
|
126
|
+
}
|
|
127
|
+
const timer = setTimeoutFn(fn, retryDelayMs)
|
|
128
|
+
timer?.unref?.()
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function connectAttempt(resolve, reject, attempt = 1, autostartTried = false) {
|
|
132
|
+
let settled = false
|
|
133
|
+
const activeSocket = new WebSocketCtor(`ws://${host}:${port}`)
|
|
134
|
+
socket = activeSocket
|
|
135
|
+
|
|
136
|
+
function failStart(message) {
|
|
137
|
+
settled = true
|
|
138
|
+
socket = null
|
|
139
|
+
connected = false
|
|
140
|
+
startPromise = null
|
|
141
|
+
reject(createSafeError(message))
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
function retryAfterAutostart() {
|
|
145
|
+
schedule(() => connectAttempt(resolve, reject, attempt + 1, true))
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
addSocketListener(activeSocket, 'open', () => {
|
|
149
|
+
if (settled) return
|
|
150
|
+
sendJson(activeSocket, {
|
|
151
|
+
type: MESSAGE_TYPES.clientHello,
|
|
152
|
+
source: MCP_CLIENT_SOURCE,
|
|
153
|
+
key: connectionKey,
|
|
154
|
+
version: BRIDGE_PROTOCOL_VERSION,
|
|
155
|
+
clientId
|
|
156
|
+
})
|
|
157
|
+
})
|
|
158
|
+
|
|
159
|
+
addSocketListener(activeSocket, 'message', raw => {
|
|
160
|
+
const message = parseBridgeMessage(getMessageData(raw))
|
|
161
|
+
if (!message) return
|
|
162
|
+
|
|
163
|
+
if (message.type === MESSAGE_TYPES.clientHelloAck && message.ok === true && Number(message.version) === BRIDGE_PROTOCOL_VERSION) {
|
|
164
|
+
settled = true
|
|
165
|
+
connected = true
|
|
166
|
+
resolve()
|
|
167
|
+
return
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
handleResponse(message)
|
|
171
|
+
})
|
|
172
|
+
|
|
173
|
+
addSocketListener(activeSocket, 'error', () => {
|
|
174
|
+
if (connected || settled) return
|
|
175
|
+
settled = true
|
|
176
|
+
socket = null
|
|
177
|
+
|
|
178
|
+
if (!daemonAutostart) {
|
|
179
|
+
failStart(DAEMON_NOT_RUNNING_MESSAGE)
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (!autostartTried) {
|
|
184
|
+
try {
|
|
185
|
+
startDaemonProcess({ port, key: connectionKey, logger })
|
|
186
|
+
} catch {
|
|
187
|
+
failStart(DAEMON_START_FAILED_MESSAGE)
|
|
188
|
+
return
|
|
189
|
+
}
|
|
190
|
+
retryAfterAutostart()
|
|
191
|
+
return
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
if (attempt < maxConnectAttempts) {
|
|
195
|
+
retryAfterAutostart()
|
|
196
|
+
return
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
failStart(DAEMON_START_FAILED_MESSAGE)
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
addSocketListener(activeSocket, 'close', () => {
|
|
203
|
+
if (!connected && !settled) {
|
|
204
|
+
settled = true
|
|
205
|
+
socket = null
|
|
206
|
+
if (!daemonAutostart) {
|
|
207
|
+
failStart(DAEMON_NOT_RUNNING_MESSAGE)
|
|
208
|
+
return
|
|
209
|
+
}
|
|
210
|
+
if (!autostartTried) {
|
|
211
|
+
try {
|
|
212
|
+
startDaemonProcess({ port, key: connectionKey, logger })
|
|
213
|
+
} catch {
|
|
214
|
+
failStart(DAEMON_START_FAILED_MESSAGE)
|
|
215
|
+
return
|
|
216
|
+
}
|
|
217
|
+
retryAfterAutostart()
|
|
218
|
+
return
|
|
219
|
+
}
|
|
220
|
+
failStart(DAEMON_START_FAILED_MESSAGE)
|
|
221
|
+
return
|
|
222
|
+
}
|
|
223
|
+
handleSocketClose(activeSocket)
|
|
224
|
+
})
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
function start() {
|
|
228
|
+
if (isConnected()) return Promise.resolve()
|
|
229
|
+
if (startPromise) return startPromise
|
|
230
|
+
startPromise = new Promise((resolve, reject) => {
|
|
231
|
+
connectAttempt(resolve, reject)
|
|
232
|
+
})
|
|
233
|
+
return startPromise
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
async function close() {
|
|
237
|
+
rejectPending(DISCONNECTED_MESSAGE)
|
|
238
|
+
connected = false
|
|
239
|
+
startPromise = null
|
|
240
|
+
if (socket && typeof socket.close === 'function') socket.close()
|
|
241
|
+
socket = null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
function callCommand(command, payload = {}) {
|
|
245
|
+
if (!isAllowedCommand(command)) {
|
|
246
|
+
return Promise.reject(createSafeError(COMMAND_NOT_ALLOWED_MESSAGE))
|
|
247
|
+
}
|
|
248
|
+
if (!isConnected()) {
|
|
249
|
+
return Promise.reject(createSafeError(DAEMON_NOT_RUNNING_MESSAGE))
|
|
250
|
+
}
|
|
251
|
+
if (pending.size >= maxPendingRequests) {
|
|
252
|
+
return Promise.reject(createSafeError(TOO_MANY_PENDING_MESSAGE))
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const id = nextRequestId()
|
|
256
|
+
const message = {
|
|
257
|
+
id,
|
|
258
|
+
type: MESSAGE_TYPES.clientCommand,
|
|
259
|
+
command,
|
|
260
|
+
payload: payload && typeof payload === 'object' ? payload : {}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return new Promise((resolve, reject) => {
|
|
264
|
+
const timer = setTimeoutFn(() => {
|
|
265
|
+
pending.delete(id)
|
|
266
|
+
reject(createSafeError(TIMEOUT_MESSAGE))
|
|
267
|
+
}, requestTimeoutMs)
|
|
268
|
+
timer?.unref?.()
|
|
269
|
+
pending.set(id, { resolve, reject, timer })
|
|
270
|
+
sendJson(socket, message)
|
|
271
|
+
})
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
return {
|
|
275
|
+
start,
|
|
276
|
+
close,
|
|
277
|
+
callCommand,
|
|
278
|
+
isConnected,
|
|
279
|
+
getPendingCount: () => pending.size
|
|
280
|
+
}
|
|
281
|
+
}
|