ghost-bridge 0.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.
Binary file
Binary file
Binary file
Binary file
@@ -0,0 +1,37 @@
1
+ {
2
+ "manifest_version": 3,
3
+ "name": "Ghost Bridge",
4
+ "version": "0.2.0",
5
+ "description": "Zero-restart Chrome debugger bridge for Claude MCP, optimized for no-sourcemap production debugging.",
6
+ "permissions": [
7
+ "debugger",
8
+ "activeTab",
9
+ "scripting",
10
+ "storage",
11
+ "tabs",
12
+ "offscreen"
13
+ ],
14
+ "host_permissions": [
15
+ "ws://localhost/*",
16
+ "ws://127.0.0.1/*"
17
+ ],
18
+ "action": {
19
+ "default_title": "Ghost Bridge",
20
+ "default_popup": "popup.html",
21
+ "default_icon": {
22
+ "16": "icon-16.png",
23
+ "32": "icon-32.png",
24
+ "48": "icon-48.png",
25
+ "128": "icon-128.png"
26
+ }
27
+ },
28
+ "icons": {
29
+ "16": "icon-16.png",
30
+ "32": "icon-32.png",
31
+ "48": "icon-48.png",
32
+ "128": "icon-128.png"
33
+ },
34
+ "background": {
35
+ "service_worker": "background.js"
36
+ }
37
+ }
@@ -0,0 +1,10 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <title>Ghost Bridge Offscreen</title>
6
+ </head>
7
+ <body>
8
+ <script src="offscreen.js"></script>
9
+ </body>
10
+ </html>
@@ -0,0 +1,175 @@
1
+ // Offscreen document 用于维持 WebSocket 长连接
2
+ // 不受 MV3 service worker 暂停的影响
3
+
4
+ let ws = null
5
+ let reconnectTimer = null
6
+ let config = {
7
+ basePort: 33333,
8
+ token: '',
9
+ maxPortRetries: 10,
10
+ }
11
+
12
+ function log(msg) {
13
+ console.log(`[ghost-bridge offscreen] ${msg}`)
14
+ // 转发日志到 service worker
15
+ chrome.runtime.sendMessage({ type: 'log', msg }).catch(() => {})
16
+ }
17
+
18
+ function getMonthlyToken() {
19
+ const now = new Date()
20
+ const firstDayOfMonth = new Date(now.getFullYear(), now.getMonth(), 1, 0, 0, 0, 0)
21
+ return String(firstDayOfMonth.getTime())
22
+ }
23
+
24
+ // 连接到服务器
25
+ function connect(portIndex = 0, isNewRound = false) {
26
+ if (portIndex >= config.maxPortRetries) {
27
+ log(`扫描完毕,未找到服务,2秒后重试...`)
28
+ reconnectTimer = setTimeout(() => connect(0, true), 2000)
29
+ chrome.runtime.sendMessage({ type: 'status', status: 'not_found' }).catch(() => {})
30
+ return
31
+ }
32
+
33
+ const port = config.basePort + portIndex
34
+ const url = new URL(`ws://localhost:${port}`)
35
+ url.searchParams.set('token', config.token)
36
+
37
+ if (portIndex === 0 && isNewRound) {
38
+ log(`开始扫描端口 ${config.basePort}-${config.basePort + config.maxPortRetries - 1}`)
39
+ }
40
+
41
+ log(`尝试连接端口 ${port}...`)
42
+ chrome.runtime.sendMessage({
43
+ type: 'status',
44
+ status: 'scanning',
45
+ currentPort: port,
46
+ }).catch(() => {})
47
+
48
+ ws = new WebSocket(url.toString())
49
+ ws.binaryType = 'blob' // 明确设置
50
+
51
+ const connectionTimeout = setTimeout(() => {
52
+ if (ws && ws.readyState === WebSocket.CONNECTING) {
53
+ ws.close()
54
+ }
55
+ }, 2000) // 增加到 2 秒
56
+
57
+ let identityVerified = false
58
+
59
+ ws.onopen = () => {
60
+ clearTimeout(connectionTimeout)
61
+ log(`WebSocket 已连接端口 ${port},等待身份验证...`)
62
+ }
63
+
64
+ ws.onmessage = async (event) => {
65
+ try {
66
+ // 处理 Blob 类型的消息
67
+ let data = event.data
68
+ if (data instanceof Blob) {
69
+ data = await data.text()
70
+ }
71
+ const msg = JSON.parse(data)
72
+
73
+ if (msg.type === 'identity') {
74
+ if (msg.service === 'ghost-bridge' && msg.token === config.token) {
75
+ identityVerified = true
76
+ log(`✅ 已连接到 ghost-bridge 服务 (端口 ${port})`)
77
+ chrome.runtime.sendMessage({
78
+ type: 'status',
79
+ status: 'connected',
80
+ port: port,
81
+ }).catch(() => {})
82
+ } else {
83
+ log(`身份验证失败,尝试下一个端口...`)
84
+ ws.close()
85
+ setTimeout(() => connect(portIndex + 1), 50)
86
+ }
87
+ return
88
+ }
89
+
90
+ // 转发命令到 service worker
91
+ if (identityVerified && msg.id) {
92
+ chrome.runtime.sendMessage({ type: 'command', data: msg }).catch(() => {})
93
+ }
94
+ } catch (e) {
95
+ log(`解析消息失败:${e.message}`)
96
+ }
97
+ }
98
+
99
+ ws.onclose = (event) => {
100
+ clearTimeout(connectionTimeout)
101
+
102
+ if (!identityVerified) {
103
+ // 连接失败,尝试下一个端口
104
+ setTimeout(() => connect(portIndex + 1), 50)
105
+ return
106
+ }
107
+
108
+ // 连接断开,重试
109
+ log('连接断开,尝试重连...')
110
+ chrome.runtime.sendMessage({ type: 'status', status: 'disconnected' }).catch(() => {})
111
+ reconnectTimer = setTimeout(() => connect(0, true), 1000)
112
+ }
113
+
114
+ ws.onerror = () => {
115
+ clearTimeout(connectionTimeout)
116
+ }
117
+ }
118
+
119
+ // 发送消息到服务器
120
+ function sendToServer(data) {
121
+ if (ws && ws.readyState === WebSocket.OPEN) {
122
+ ws.send(JSON.stringify(data))
123
+ return true
124
+ }
125
+ return false
126
+ }
127
+
128
+ // 断开连接
129
+ function disconnect() {
130
+ if (reconnectTimer) {
131
+ clearTimeout(reconnectTimer)
132
+ reconnectTimer = null
133
+ }
134
+ if (ws) {
135
+ ws.close()
136
+ ws = null
137
+ }
138
+ log('已断开连接')
139
+ }
140
+
141
+ // 监听来自 service worker 的消息
142
+ chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
143
+ if (message.type === 'connect') {
144
+ config.basePort = message.basePort || 33333
145
+ config.token = message.token || getMonthlyToken()
146
+ config.maxPortRetries = message.maxPortRetries || 10
147
+ disconnect()
148
+ connect(0, true)
149
+ sendResponse({ ok: true })
150
+ return true
151
+ }
152
+
153
+ if (message.type === 'disconnect') {
154
+ disconnect()
155
+ sendResponse({ ok: true })
156
+ return true
157
+ }
158
+
159
+ if (message.type === 'send') {
160
+ const ok = sendToServer(message.data)
161
+ sendResponse({ ok })
162
+ return true
163
+ }
164
+
165
+ if (message.type === 'getStatus') {
166
+ sendResponse({
167
+ connected: ws && ws.readyState === WebSocket.OPEN,
168
+ })
169
+ return true
170
+ }
171
+
172
+ return false
173
+ })
174
+
175
+ log('Offscreen document 已加载')
@@ -0,0 +1,160 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <style>
6
+ * {
7
+ margin: 0;
8
+ padding: 0;
9
+ box-sizing: border-box;
10
+ }
11
+ body {
12
+ width: 280px;
13
+ padding: 16px;
14
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
15
+ background: #1a1a2e;
16
+ color: #eee;
17
+ border-radius: 12px;
18
+ overflow: hidden;
19
+ box-shadow: inset 0 0 0 1px rgba(255,255,255,0.1);
20
+ }
21
+ .header {
22
+ display: flex;
23
+ align-items: center;
24
+ gap: 10px;
25
+ margin-bottom: 16px;
26
+ }
27
+ .header img {
28
+ width: 32px;
29
+ height: 32px;
30
+ }
31
+ .header h1 {
32
+ font-size: 16px;
33
+ font-weight: 600;
34
+ }
35
+ .status-card {
36
+ background: #16213e;
37
+ border-radius: 8px;
38
+ padding: 14px;
39
+ margin-bottom: 12px;
40
+ }
41
+ .status-row {
42
+ display: flex;
43
+ align-items: center;
44
+ gap: 10px;
45
+ }
46
+ .status-dot {
47
+ width: 10px;
48
+ height: 10px;
49
+ border-radius: 50%;
50
+ flex-shrink: 0;
51
+ }
52
+ .status-dot.connected { background: #34c759; box-shadow: 0 0 8px #34c759; }
53
+ .status-dot.connecting { background: #ff9f0a; animation: pulse 1s infinite; }
54
+ .status-dot.disconnected { background: #666; }
55
+ .status-dot.error { background: #ff3b30; }
56
+ @keyframes pulse {
57
+ 0%, 100% { opacity: 1; }
58
+ 50% { opacity: 0.4; }
59
+ }
60
+ .status-text {
61
+ font-size: 14px;
62
+ font-weight: 500;
63
+ }
64
+ .status-detail {
65
+ font-size: 12px;
66
+ color: #888;
67
+ margin-top: 8px;
68
+ padding-left: 20px;
69
+ }
70
+ .config-section {
71
+ background: #16213e;
72
+ border-radius: 8px;
73
+ padding: 14px;
74
+ margin-bottom: 12px;
75
+ }
76
+ .config-label {
77
+ font-size: 12px;
78
+ color: #888;
79
+ margin-bottom: 6px;
80
+ }
81
+ .config-input {
82
+ width: 100%;
83
+ padding: 8px 10px;
84
+ border: 1px solid #333;
85
+ border-radius: 6px;
86
+ background: #0f0f23;
87
+ color: #eee;
88
+ font-size: 13px;
89
+ outline: none;
90
+ }
91
+ .config-input:focus {
92
+ border-color: #4a9eff;
93
+ }
94
+
95
+ .btn-row {
96
+ display: flex;
97
+ gap: 8px;
98
+ }
99
+ .btn {
100
+ flex: 1;
101
+ padding: 10px;
102
+ border: none;
103
+ border-radius: 6px;
104
+ font-size: 13px;
105
+ font-weight: 500;
106
+ cursor: pointer;
107
+ transition: all 0.2s;
108
+ }
109
+ .btn-primary {
110
+ background: #4a9eff;
111
+ color: #fff;
112
+ }
113
+ .btn-primary:hover {
114
+ background: #3a8eef;
115
+ }
116
+ .btn-primary:disabled {
117
+ background: #333;
118
+ cursor: not-allowed;
119
+ }
120
+ .btn-secondary {
121
+ background: #333;
122
+ color: #ccc;
123
+ }
124
+ .btn-secondary:hover {
125
+ background: #444;
126
+ }
127
+ .scan-info {
128
+ font-size: 11px;
129
+ color: #666;
130
+ text-align: center;
131
+ margin-top: 8px;
132
+ }
133
+ </style>
134
+ </head>
135
+ <body>
136
+ <div class="header">
137
+ <img src="icon-32.png" alt="Ghost Bridge">
138
+ <h1>Ghost Bridge</h1>
139
+ </div>
140
+
141
+ <div class="status-card">
142
+ <div class="status-row">
143
+ <div class="status-dot disconnected" id="statusDot"></div>
144
+ <span class="status-text" id="statusText">检查连接状态...</span>
145
+ </div>
146
+ <div class="status-detail" id="statusDetail"></div>
147
+ </div>
148
+
149
+
150
+
151
+ <div class="btn-row">
152
+ <button class="btn btn-primary" id="connectBtn">连接</button>
153
+ <button class="btn btn-secondary" id="disconnectBtn">断开</button>
154
+ </div>
155
+
156
+ <div class="scan-info" id="scanInfo"></div>
157
+
158
+ <script src="popup.js"></script>
159
+ </body>
160
+ </html>
@@ -0,0 +1,165 @@
1
+ // popup.js - Ghost Bridge 弹窗逻辑
2
+
3
+ const statusDot = document.getElementById('statusDot')
4
+ const statusText = document.getElementById('statusText')
5
+ const statusDetail = document.getElementById('statusDetail')
6
+ const connectBtn = document.getElementById('connectBtn')
7
+ const disconnectBtn = document.getElementById('disconnectBtn')
8
+ const scanInfo = document.getElementById('scanInfo')
9
+
10
+ // 状态稳定性控制:防止闪烁
11
+ let lastStableStatus = null
12
+ let pendingStatus = null
13
+ let statusChangeTimer = null
14
+ const STATUS_DEBOUNCE_MS = 300 // 状态变化需要持续 300ms 才生效
15
+
16
+ // 状态映射
17
+ const STATUS_MAP = {
18
+ connected: {
19
+ dotClass: 'connected',
20
+ text: '✅ 已连接',
21
+ },
22
+ connecting: {
23
+ dotClass: 'connecting',
24
+ text: '🔍 正在扫描...',
25
+ },
26
+ verifying: {
27
+ dotClass: 'connecting',
28
+ text: '🔐 验证身份...',
29
+ },
30
+ scanning: {
31
+ dotClass: 'connecting',
32
+ text: '📡 搜索服务...',
33
+ },
34
+ not_found: {
35
+ dotClass: 'disconnected',
36
+ text: '🔴 未找到服务',
37
+ },
38
+ disconnected: {
39
+ dotClass: 'disconnected',
40
+ text: '未连接',
41
+ },
42
+ error: {
43
+ dotClass: 'error',
44
+ text: '连接失败',
45
+ },
46
+ }
47
+
48
+ // 实际执行 UI 更新
49
+ function renderUI(state) {
50
+ const { status, port, scanRound, enabled, currentPort, basePort } = state
51
+ const config = STATUS_MAP[status] || STATUS_MAP.disconnected
52
+
53
+ statusDot.className = `status-dot ${config.dotClass}`
54
+ statusText.textContent = config.text
55
+
56
+ // 状态详情
57
+ if (status === 'connected' && port) {
58
+ statusDetail.textContent = `端口 ${port} · WebSocket 已建立`
59
+ } else if ((status === 'connecting' || status === 'verifying' || status === 'scanning') && currentPort) {
60
+ const roundText = scanRound > 0 ? `(第 ${scanRound + 1} 轮)` : ''
61
+ statusDetail.textContent = `正在扫描 ${basePort}-${basePort + 9}${roundText}`
62
+ } else if (status === 'not_found') {
63
+ statusDetail.textContent = '请确保 Claude Code 已启动'
64
+ } else {
65
+ statusDetail.textContent = ''
66
+ }
67
+
68
+ // 按钮状态
69
+ connectBtn.textContent = enabled ? '重新连接' : '连接'
70
+ connectBtn.disabled = false
71
+
72
+ // 扫描轮次提示
73
+ if ((status === 'connecting' || status === 'scanning') && scanRound > 2) {
74
+ scanInfo.textContent = `已扫描 ${scanRound} 轮,请确保 Claude Code 已启动`
75
+ scanInfo.style.color = '#ff9f0a'
76
+ } else {
77
+ scanInfo.textContent = ''
78
+ }
79
+ }
80
+
81
+ // 更新 UI 状态(带防抖,防止闪烁)
82
+ function updateUI(state) {
83
+ const newStatus = state.status
84
+
85
+ // 如果是首次加载或状态相同,直接更新
86
+ if (lastStableStatus === null || newStatus === lastStableStatus) {
87
+ lastStableStatus = newStatus
88
+ pendingStatus = null
89
+ if (statusChangeTimer) {
90
+ clearTimeout(statusChangeTimer)
91
+ statusChangeTimer = null
92
+ }
93
+ renderUI(state)
94
+ return
95
+ }
96
+
97
+ // 状态变化:从 connected 变为其他状态时需要防抖
98
+ // 防止短暂的状态波动导致 UI 闪烁
99
+ if (lastStableStatus === 'connected' && newStatus !== 'connected') {
100
+ // 需要持续一段时间才确认断开
101
+ if (pendingStatus !== newStatus) {
102
+ pendingStatus = newStatus
103
+ if (statusChangeTimer) clearTimeout(statusChangeTimer)
104
+ statusChangeTimer = setTimeout(() => {
105
+ lastStableStatus = pendingStatus
106
+ pendingStatus = null
107
+ statusChangeTimer = null
108
+ renderUI(state)
109
+ }, STATUS_DEBOUNCE_MS)
110
+ }
111
+ // 暂不更新 UI,等待确认
112
+ return
113
+ }
114
+
115
+ // 其他状态变化(如从 scanning 到 connected)立即更新
116
+ lastStableStatus = newStatus
117
+ pendingStatus = null
118
+ if (statusChangeTimer) {
119
+ clearTimeout(statusChangeTimer)
120
+ statusChangeTimer = null
121
+ }
122
+ renderUI(state)
123
+ }
124
+
125
+ // 从 background 获取状态
126
+ async function fetchStatus() {
127
+ try {
128
+ const response = await chrome.runtime.sendMessage({ type: 'getStatus' })
129
+ if (response) {
130
+ updateUI(response)
131
+ }
132
+ } catch (e) {
133
+ console.error('获取状态失败:', e)
134
+ }
135
+ }
136
+
137
+ // 启用连接(自动扫描端口)
138
+ connectBtn.addEventListener('click', async () => {
139
+ try {
140
+ await chrome.runtime.sendMessage({ type: 'connect' })
141
+ setTimeout(fetchStatus, 100)
142
+ } catch (e) {
143
+ console.error('连接失败:', e)
144
+ }
145
+ })
146
+
147
+ // 断开连接
148
+ disconnectBtn.addEventListener('click', async () => {
149
+ try {
150
+ await chrome.runtime.sendMessage({ type: 'disconnect' })
151
+ setTimeout(fetchStatus, 100)
152
+ } catch (e) {
153
+ console.error('断开失败:', e)
154
+ }
155
+ })
156
+
157
+ // 初始加载
158
+ fetchStatus()
159
+
160
+ // 监听 background 主动推送的状态变化
161
+ chrome.runtime.onMessage.addListener((message) => {
162
+ if (message.type === 'statusUpdate') {
163
+ updateUI(message.state)
164
+ }
165
+ })
package/package.json ADDED
@@ -0,0 +1,29 @@
1
+ {
2
+ "name": "ghost-bridge",
3
+ "version": "0.2.0",
4
+ "private": false,
5
+ "type": "module",
6
+ "description": "Ghost Bridge: Zero-restart Chrome debugger bridge for Claude MCP. Includes CLI for easy setup.",
7
+ "bin": {
8
+ "ghost-bridge": "./dist/cli.js"
9
+ },
10
+ "main": "./dist/server.js",
11
+ "files": [
12
+ "dist/",
13
+ "extension/"
14
+ ],
15
+ "scripts": {
16
+ "start": "node dist/server.js",
17
+ "build": "node scripts/build.js",
18
+ "prepublishOnly": "npm run build"
19
+ },
20
+ "devDependencies": {
21
+ "@modelcontextprotocol/sdk": "^1.5.0",
22
+ "chalk": "^5.3.0",
23
+ "commander": "^11.0.0",
24
+ "esbuild": "^0.27.3",
25
+ "fs-extra": "^11.1.1",
26
+ "js-beautify": "^1.14.11",
27
+ "ws": "^8.14.2"
28
+ }
29
+ }