ghost-bridge 0.5.1 → 0.5.2

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 CHANGED
@@ -139,7 +139,7 @@ ghost-bridge init
139
139
 
140
140
  | Setting | Default | Description |
141
141
  |---------|---------|-------------|
142
- | **Base Port** | `33333` | WS port. Auto-increments if occupied. |
142
+ | **Base Port** | `33333` | Fixed WS port. If occupied, Ghost Bridge fails fast and asks you to free it or set `GHOST_BRIDGE_PORT`. |
143
143
  | **Token** | *Monthly UUID* | Local WS auth token, auto-rotates on the 1st of every month. |
144
144
  | **Auto Detach** | `false` | Keeps debugger attached to actively buffer invisible exceptions and network calls. |
145
145
 
package/dist/server.js CHANGED
@@ -28536,7 +28536,6 @@ var GHOST_BRIDGE_VERSION = packageJson.version;
28536
28536
 
28537
28537
  // src/server.js
28538
28538
  var BASE_PORT = Number(process.env.GHOST_BRIDGE_PORT || 33333);
28539
- var MAX_PORT_RETRIES = 10;
28540
28539
  function getMonthlyToken() {
28541
28540
  const now = /* @__PURE__ */ new Date();
28542
28541
  const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0);
@@ -28567,6 +28566,10 @@ function getExistingService() {
28567
28566
  if (!fs2.existsSync(PORT_INFO_FILE)) return null;
28568
28567
  const info = JSON.parse(fs2.readFileSync(PORT_INFO_FILE, "utf-8"));
28569
28568
  if (!info.pid || !info.port) return null;
28569
+ if (info.port !== BASE_PORT) {
28570
+ fs2.unlinkSync(PORT_INFO_FILE);
28571
+ return null;
28572
+ }
28570
28573
  if (!isProcessRunning(info.pid)) {
28571
28574
  log(`\u65E7\u670D\u52A1 PID ${info.pid} \u5DF2\u4E0D\u5B58\u5728\uFF0C\u6E05\u7406\u65E7\u4FE1\u606F`);
28572
28575
  fs2.unlinkSync(PORT_INFO_FILE);
@@ -28618,22 +28621,14 @@ function isPortAvailable(port) {
28618
28621
  });
28619
28622
  }
28620
28623
  async function startWebSocketServer() {
28621
- for (let i = 0; i < MAX_PORT_RETRIES; i++) {
28622
- const port = BASE_PORT + i;
28623
- const available = await isPortAvailable(port);
28624
- if (available) {
28625
- actualPort = port;
28626
- const wss2 = new import_websocket_server.default({ port });
28627
- if (port !== BASE_PORT) {
28628
- log(`\u26A0\uFE0F \u7AEF\u53E3 ${BASE_PORT} \u88AB\u5360\u7528\uFF0C\u5DF2\u5207\u6362\u5230\u7AEF\u53E3 ${port}`);
28629
- }
28630
- log(`\u{1F680} WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 ${port}${WS_TOKEN ? "\uFF08\u542F\u7528 token \u6821\u9A8C\uFF09" : ""}`);
28631
- return wss2;
28632
- } else {
28633
- log(`\u7AEF\u53E3 ${port} \u88AB\u5360\u7528\uFF0C\u5C1D\u8BD5\u4E0B\u4E00\u4E2A...`);
28634
- }
28624
+ const available = await isPortAvailable(BASE_PORT);
28625
+ if (!available) {
28626
+ throw new Error(`\u56FA\u5B9A\u7AEF\u53E3 ${BASE_PORT} \u4E0D\u53EF\u7528\uFF0C\u8BF7\u91CA\u653E\u8BE5\u7AEF\u53E3\u6216\u901A\u8FC7 GHOST_BRIDGE_PORT \u6307\u5B9A\u5176\u4ED6\u7AEF\u53E3`);
28635
28627
  }
28636
- throw new Error(`\u65E0\u6CD5\u627E\u5230\u53EF\u7528\u7AEF\u53E3\uFF08\u5DF2\u5C1D\u8BD5 ${BASE_PORT} - ${BASE_PORT + MAX_PORT_RETRIES - 1}\uFF09`);
28628
+ actualPort = BASE_PORT;
28629
+ const wss2 = new import_websocket_server.default({ port: BASE_PORT });
28630
+ log(`\u{1F680} WebSocket \u670D\u52A1\u5DF2\u542F\u52A8\uFF0C\u7AEF\u53E3 ${BASE_PORT}${WS_TOKEN ? "\uFF08\u542F\u7528 token \u6821\u9A8C\uFF09" : ""}`);
28631
+ return wss2;
28637
28632
  }
28638
28633
  async function initWebSocketService() {
28639
28634
  const existing = getExistingService();
@@ -28653,6 +28648,16 @@ async function initWebSocketService() {
28653
28648
  }
28654
28649
  }
28655
28650
  }
28651
+ if (!await isPortAvailable(BASE_PORT)) {
28652
+ const valid = await verifyExistingService(BASE_PORT);
28653
+ if (valid) {
28654
+ actualPort = BASE_PORT;
28655
+ isMainInstance = false;
28656
+ log(`\u2705 \u590D\u7528\u56FA\u5B9A\u7AEF\u53E3\u4E0A\u7684\u73B0\u6709\u670D\u52A1\uFF0C\u7AEF\u53E3 ${actualPort}`);
28657
+ return null;
28658
+ }
28659
+ throw new Error(`\u56FA\u5B9A\u7AEF\u53E3 ${BASE_PORT} \u5DF2\u88AB\u5176\u4ED6\u8FDB\u7A0B\u5360\u7528\uFF0C\u8BF7\u91CA\u653E\u8BE5\u7AEF\u53E3\u6216\u901A\u8FC7 GHOST_BRIDGE_PORT \u6307\u5B9A\u5176\u4ED6\u7AEF\u53E3`);
28660
+ }
28656
28661
  const wss2 = await startWebSocketServer();
28657
28662
  isMainInstance = true;
28658
28663
  fs2.writeFileSync(
@@ -7,7 +7,6 @@ function getMonthlyToken() {
7
7
 
8
8
  const CONFIG = {
9
9
  basePort: 33333,
10
- maxPortRetries: 10,
11
10
  token: getMonthlyToken(),
12
11
  autoDetach: false,
13
12
  maxErrors: 100,
@@ -23,7 +22,7 @@ let lastErrors = []
23
22
  let lastErrorLocation = null
24
23
  let requestMap = new Map()
25
24
  let networkRequests = []
26
- let state = { enabled: false, connected: false, port: null, currentPort: null, scanRound: 0 }
25
+ let state = { enabled: false, connected: false, port: null, currentPort: null, connectionStatus: 'disconnected', connectionError: '' }
27
26
 
28
27
  // 待处理的请求(等待 offscreen 响应)
29
28
  const pendingRequests = new Map()
@@ -1396,10 +1395,8 @@ function broadcastStatus() {
1396
1395
  status = 'disconnected'
1397
1396
  } else if (state.connected) {
1398
1397
  status = 'connected'
1399
- } else if (state.scanRound >= 4) {
1400
- status = 'not_found'
1401
1398
  } else {
1402
- status = 'scanning'
1399
+ status = state.connectionStatus || 'connecting'
1403
1400
  }
1404
1401
 
1405
1402
  let tabUrl = ''
@@ -1425,7 +1422,7 @@ function broadcastStatus() {
1425
1422
  port: state.port,
1426
1423
  currentPort: state.currentPort,
1427
1424
  basePort: CONFIG.basePort,
1428
- scanRound: state.scanRound,
1425
+ connectionError: state.connectionError,
1429
1426
  errorCount: actualErrors.length,
1430
1427
  recentErrors: actualErrors.slice(0, 5),
1431
1428
  tabTitle,
@@ -1474,18 +1471,32 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1474
1471
  if (message.status === 'connected') {
1475
1472
  state.connected = true
1476
1473
  state.port = message.port
1474
+ state.currentPort = message.port
1475
+ state.connectionStatus = 'connected'
1476
+ state.connectionError = ''
1477
1477
  setBadgeState('on')
1478
1478
  log(`✅ 已连接到 ghost-bridge 服务 (端口 ${message.port})`)
1479
1479
  ensureAttached().catch((e) => log(`attach 失败:${e.message}`))
1480
1480
  } else if (message.status === 'disconnected') {
1481
1481
  state.connected = false
1482
1482
  state.port = null
1483
+ state.connectionStatus = 'connecting'
1484
+ state.connectionError = ''
1483
1485
  if (state.enabled) setBadgeState('connecting')
1484
- } else if (message.status === 'scanning') {
1486
+ } else if (message.status === 'connecting') {
1485
1487
  state.currentPort = message.currentPort
1488
+ state.connectionStatus = 'connecting'
1489
+ state.connectionError = ''
1486
1490
  setBadgeState('connecting')
1491
+ } else if (message.status === 'error') {
1492
+ state.currentPort = message.currentPort
1493
+ state.connectionStatus = 'error'
1494
+ state.connectionError = message.errorMessage || ''
1495
+ setBadgeState('err')
1487
1496
  } else if (message.status === 'not_found') {
1488
- state.scanRound++
1497
+ state.currentPort = message.currentPort
1498
+ state.connectionStatus = 'not_found'
1499
+ state.connectionError = ''
1489
1500
  setBadgeState('connecting')
1490
1501
  }
1491
1502
  broadcastStatus() // 状态变化时主动推送
@@ -1516,10 +1527,8 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1516
1527
  status = 'disconnected'
1517
1528
  } else if (state.connected) {
1518
1529
  status = 'connected'
1519
- } else if (state.scanRound >= 4) {
1520
- status = 'not_found'
1521
1530
  } else {
1522
- status = 'scanning'
1531
+ status = state.connectionStatus || 'connecting'
1523
1532
  }
1524
1533
 
1525
1534
  let tabUrl = ''
@@ -1544,7 +1553,7 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1544
1553
  port: state.port,
1545
1554
  currentPort: state.currentPort,
1546
1555
  basePort: CONFIG.basePort,
1547
- scanRound: state.scanRound,
1556
+ connectionError: state.connectionError,
1548
1557
  errorCount: actualErrors.length,
1549
1558
  recentErrors: actualErrors.slice(0, 5),
1550
1559
  tabTitle,
@@ -1561,7 +1570,11 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1561
1570
  chrome.storage.local.set({ basePort: message.port })
1562
1571
  }
1563
1572
  state.enabled = true
1564
- state.scanRound = 0
1573
+ state.connected = false
1574
+ state.port = null
1575
+ state.currentPort = CONFIG.basePort
1576
+ state.connectionStatus = 'connecting'
1577
+ state.connectionError = ''
1565
1578
  setBadgeState('connecting')
1566
1579
 
1567
1580
  // 启动 offscreen 并开始连接
@@ -1570,7 +1583,6 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1570
1583
  type: 'connect',
1571
1584
  basePort: CONFIG.basePort,
1572
1585
  token: CONFIG.token,
1573
- maxPortRetries: CONFIG.maxPortRetries,
1574
1586
  }).catch(() => {})
1575
1587
  })
1576
1588
 
@@ -1582,7 +1594,10 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
1582
1594
  if (message.type === 'disconnect') {
1583
1595
  state.enabled = false
1584
1596
  state.connected = false
1585
- state.scanRound = 0
1597
+ state.port = null
1598
+ state.currentPort = null
1599
+ state.connectionStatus = 'disconnected'
1600
+ state.connectionError = ''
1586
1601
  setBadgeState('off')
1587
1602
  detachAllTargets().catch(() => {})
1588
1603
 
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "manifest_version": 3,
3
3
  "name": "Ghost Bridge",
4
- "version": "0.5.1",
4
+ "version": "0.5.2",
5
5
  "description": "Zero-restart Chrome debugger bridge for Claude MCP, optimized for no-sourcemap production debugging.",
6
6
  "permissions": [
7
7
  "debugger",
@@ -7,7 +7,6 @@ let manualDisconnect = false // 用户主动断开标志,防止 onclose 触
7
7
  let config = {
8
8
  basePort: 33333,
9
9
  token: '',
10
- maxPortRetries: 10,
11
10
  }
12
11
 
13
12
  function log(msg) {
@@ -23,29 +22,17 @@ function getMonthlyToken() {
23
22
  }
24
23
 
25
24
  // 连接到服务器
26
- function connect(portIndex = 0, isNewRound = false) {
25
+ function connect() {
27
26
  // 如果已手动断开,不再尝试连接
28
27
  if (manualDisconnect) return
29
28
 
30
- if (portIndex >= config.maxPortRetries) {
31
- log(`扫描完毕,未找到服务,2秒后重试...`)
32
- reconnectTimer = setTimeout(() => connect(0, true), 2000)
33
- chrome.runtime.sendMessage({ type: 'status', status: 'not_found' }).catch(() => {})
34
- return
35
- }
36
-
37
- const port = config.basePort + portIndex
29
+ const port = config.basePort
38
30
  const url = new URL(`ws://localhost:${port}`)
39
31
  url.searchParams.set('token', config.token)
40
-
41
- if (portIndex === 0 && isNewRound) {
42
- log(`开始扫描端口 ${config.basePort}-${config.basePort + config.maxPortRetries - 1}`)
43
- }
44
-
45
- log(`尝试连接端口 ${port}...`)
32
+ log(`尝试连接固定端口 ${port}...`)
46
33
  chrome.runtime.sendMessage({
47
34
  type: 'status',
48
- status: 'scanning',
35
+ status: 'connecting',
49
36
  currentPort: port,
50
37
  }).catch(() => {})
51
38
 
@@ -59,8 +46,11 @@ function connect(portIndex = 0, isNewRound = false) {
59
46
  }, 2000) // 增加到 2 秒
60
47
 
61
48
  let identityVerified = false
49
+ let socketOpened = false
50
+ let terminalErrorMessage = ''
62
51
 
63
52
  ws.onopen = () => {
53
+ socketOpened = true
64
54
  clearTimeout(connectionTimeout)
65
55
  log(`WebSocket 已连接端口 ${port},等待身份验证...`)
66
56
  }
@@ -84,9 +74,15 @@ function connect(portIndex = 0, isNewRound = false) {
84
74
  port: port,
85
75
  }).catch(() => {})
86
76
  } else {
87
- log(`身份验证失败,尝试下一个端口...`)
77
+ terminalErrorMessage = `Port ${port} is occupied by a non-matching service, or the token does not match.`
78
+ log('身份验证失败,将在固定端口上重试...')
79
+ chrome.runtime.sendMessage({
80
+ type: 'status',
81
+ status: 'error',
82
+ currentPort: port,
83
+ errorMessage: terminalErrorMessage,
84
+ }).catch(() => {})
88
85
  ws.close()
89
- setTimeout(() => connect(portIndex + 1), 50)
90
86
  }
91
87
  return
92
88
  }
@@ -107,15 +103,28 @@ function connect(portIndex = 0, isNewRound = false) {
107
103
  if (manualDisconnect) return
108
104
 
109
105
  if (!identityVerified) {
110
- // 连接失败,尝试下一个端口
111
- setTimeout(() => connect(portIndex + 1), 50)
106
+ if (terminalErrorMessage || socketOpened) {
107
+ const errorMessage = terminalErrorMessage || `Port ${port} is occupied or responding with a non-ghost-bridge protocol.`
108
+ log(`${errorMessage} 2秒后重试...`)
109
+ chrome.runtime.sendMessage({
110
+ type: 'status',
111
+ status: 'error',
112
+ currentPort: port,
113
+ errorMessage,
114
+ }).catch(() => {})
115
+ reconnectTimer = setTimeout(() => connect(), 2000)
116
+ return
117
+ }
118
+ log(`固定端口 ${port} 未发现可用服务,2秒后重试...`)
119
+ chrome.runtime.sendMessage({ type: 'status', status: 'not_found', currentPort: port }).catch(() => {})
120
+ reconnectTimer = setTimeout(() => connect(), 2000)
112
121
  return
113
122
  }
114
123
 
115
124
  // 连接断开,重试
116
- log('连接断开,尝试重连...')
125
+ log(`端口 ${port} 连接断开,尝试重连...`)
117
126
  chrome.runtime.sendMessage({ type: 'status', status: 'disconnected' }).catch(() => {})
118
- reconnectTimer = setTimeout(() => connect(0, true), 1000)
127
+ reconnectTimer = setTimeout(() => connect(), 1000)
119
128
  }
120
129
 
121
130
  ws.onerror = () => {
@@ -151,10 +160,9 @@ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
151
160
  if (message.type === 'connect') {
152
161
  config.basePort = message.basePort || 33333
153
162
  config.token = message.token || getMonthlyToken()
154
- config.maxPortRetries = message.maxPortRetries || 10
155
163
  disconnect()
156
164
  manualDisconnect = false // 用户重新连接,清除断开标志
157
- connect(0, true)
165
+ connect()
158
166
  sendResponse({ ok: true })
159
167
  return true
160
168
  }
@@ -121,6 +121,15 @@
121
121
  }
122
122
  body.connected-state .bg-ripple-2 { animation-delay: 1.25s; }
123
123
 
124
+ .ghost-error .ghost-body { fill: #ef4444; }
125
+ .ghost-error .ghost-eyes-normal { opacity: 1; }
126
+
127
+ body.error-state .bg-ripple {
128
+ stroke: #ef4444;
129
+ animation: bg-ripple-scan 1.8s infinite cubic-bezier(0, 0, 0.2, 1);
130
+ }
131
+ body.error-state .bg-ripple-2 { animation-delay: 0.9s; }
132
+
124
133
  @keyframes bg-ripple-scan {
125
134
  0% { r: 10%; opacity: 0.8; stroke-width: 3; }
126
135
  100% { r: 80%; opacity: 0; stroke-width: 0; }
@@ -53,7 +53,7 @@ const STATUS_MAP = {
53
53
  }
54
54
 
55
55
  function renderUI(state) {
56
- const { status, port, scanRound, enabled, currentPort, basePort, errorCount, recentErrors, tabTitle } = state
56
+ const { status, port, enabled, currentPort, basePort, connectionError, errorCount, recentErrors, tabTitle } = state
57
57
  const config = STATUS_MAP[status] || STATUS_MAP.disconnected
58
58
 
59
59
  // Update classes for color & animations
@@ -66,6 +66,9 @@ function renderUI(state) {
66
66
  } else if (config.statusClass === 'connecting') {
67
67
  headerGhost.className = 'ghost-wrapper ghost-connecting'
68
68
  document.body.className = 'connecting-state'
69
+ } else if (config.statusClass === 'error') {
70
+ headerGhost.className = 'ghost-wrapper ghost-error'
71
+ document.body.className = 'error-state'
69
72
  } else {
70
73
  headerGhost.className = 'ghost-wrapper ghost-disconnected'
71
74
  document.body.className = 'disconnected-state'
@@ -117,8 +120,7 @@ function renderUI(state) {
117
120
 
118
121
  detailContainer.classList.remove('collapsed')
119
122
  } else if ((status === 'connecting' || status === 'verifying' || status === 'scanning') && currentPort) {
120
- const roundText = scanRound > 0 ? ` [R${scanRound + 1}]` : ''
121
- portVal.textContent = `Scanning: ${currentPort}${roundText}`
123
+ portVal.textContent = `Connecting: ${currentPort}`
122
124
  portVal.className = 'detail-value highlight'
123
125
  tabRow.classList.add('hidden')
124
126
  errorRow.classList.add('hidden')
@@ -131,6 +133,12 @@ function renderUI(state) {
131
133
  tabRow.classList.add('hidden')
132
134
  errorRow.classList.add('hidden')
133
135
  detailContainer.classList.remove('collapsed')
136
+ } else if (status === 'error') {
137
+ portVal.textContent = `Port ${currentPort || basePort || '-'} blocked`
138
+ portVal.className = 'detail-value warning'
139
+ tabRow.classList.add('hidden')
140
+ errorRow.classList.add('hidden')
141
+ detailContainer.classList.remove('collapsed')
134
142
  } else {
135
143
  detailContainer.classList.add('collapsed')
136
144
  }
@@ -145,8 +153,11 @@ function renderUI(state) {
145
153
  }
146
154
 
147
155
  // Scan info text
148
- if ((status === 'connecting' || status === 'scanning') && scanRound > 2) {
149
- scanInfo.textContent = `Round ${scanRound}: Is your MCP Server running?`
156
+ if (status === 'error' && connectionError) {
157
+ scanInfo.textContent = connectionError
158
+ scanInfo.classList.remove('collapsed')
159
+ } else if (status === 'not_found' && basePort) {
160
+ scanInfo.textContent = `Port ${basePort} unavailable. Is your MCP server running on this port?`
150
161
  scanInfo.classList.remove('collapsed')
151
162
  } else {
152
163
  scanInfo.classList.add('collapsed')
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ghost-bridge",
3
- "version": "0.5.1",
3
+ "version": "0.5.2",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "description": "Ghost Bridge: Zero-restart Chrome debugger bridge for Claude MCP. Includes CLI for easy setup.",