ghost-bridge 0.8.0 → 0.9.1
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 +27 -5
- package/dist/cli.js +59 -0
- package/dist/server.js +315 -122
- package/extension/background.js +529 -216
- package/extension/manifest.json +3 -2
- package/extension/offscreen.js +149 -10
- package/package.json +4 -4
package/README.md
CHANGED
|
@@ -16,6 +16,7 @@ Most browser-capable AI tools start a separate browser. Ghost Bridge connects AI
|
|
|
16
16
|
- Inspect page structure, text, screenshots, errors, and network traffic
|
|
17
17
|
- Search and extract script sources, even in production bundles
|
|
18
18
|
- Click, type, scroll, and submit forms on the current page
|
|
19
|
+
- Bind multiple Chrome tabs as named targets and operate them independently
|
|
19
20
|
- Share one Chrome transport across multiple MCP clients
|
|
20
21
|
|
|
21
22
|
## Quick Start
|
|
@@ -94,6 +95,9 @@ Typical prompts:
|
|
|
94
95
|
| `get_page_content` | Text, HTML, and structured DOM extraction |
|
|
95
96
|
| `get_interactive_snapshot` | Find clickable and editable elements |
|
|
96
97
|
| `dispatch_action` | Click, fill, press, scroll, hover, select |
|
|
98
|
+
| `bind_tab` | Bind a Chrome tab as a named target such as `cases` or `app` |
|
|
99
|
+
| `unbind_tab` | Remove a named target binding |
|
|
100
|
+
| `list_targets` | Show named targets and their per-tab session status |
|
|
97
101
|
| `pin_current_tab` | Keep Ghost Bridge attached to the current tab while you browse elsewhere |
|
|
98
102
|
| `pin_tab` | Pin a target tab by tab ID, URL fragment, or title fragment |
|
|
99
103
|
| `unpin_tab` | Return to following the focused tab |
|
|
@@ -117,9 +121,19 @@ Recommended flow:
|
|
|
117
121
|
|
|
118
122
|
Notes:
|
|
119
123
|
|
|
124
|
+
- Use `bind_tab` when a workflow spans multiple pages. For example, bind a checklist page as `cases` and a business page as `app`, then call tools with `target: "cases"` or `target: "app"`.
|
|
125
|
+
- All browser tools accept an optional `target` parameter. When named targets are bound, `dispatch_action` requires `target` so refs from one page are not accidentally used on another page.
|
|
120
126
|
- Use `pin_current_tab` when you are debugging a page and need to switch to other tabs without changing the AI target. Use `unpin_tab` to restore the original follow-focused-tab behavior.
|
|
121
127
|
- `list_network_requests` and `get_network_detail` automatically summarize `data:` URLs and very long URLs so inline images or oversized query strings do not overwhelm model context
|
|
122
128
|
|
|
129
|
+
Multi-page example:
|
|
130
|
+
|
|
131
|
+
```text
|
|
132
|
+
Bind the current checklist tab as cases.
|
|
133
|
+
Bind the tab whose title contains "Orders" as app.
|
|
134
|
+
Read the next case from target cases, operate target app, then mark the case passed or failed back on target cases.
|
|
135
|
+
```
|
|
136
|
+
|
|
123
137
|
## Configuration
|
|
124
138
|
|
|
125
139
|
| Setting | Default | Notes |
|
|
@@ -133,18 +147,26 @@ Notes:
|
|
|
133
147
|
```mermaid
|
|
134
148
|
flowchart LR
|
|
135
149
|
A["AI Client<br/>Claude / Codex / Cursor"]
|
|
136
|
-
|
|
150
|
+
S["Session Process<br/>dist/server.js (stdio)"]
|
|
151
|
+
B["Ghost Bridge Daemon<br/>resident WebSocket service"]
|
|
137
152
|
C["Chrome Extension<br/>background.js"]
|
|
138
|
-
D["Browser
|
|
153
|
+
D["Browser Tabs<br/>Target Sessions"]
|
|
139
154
|
|
|
140
|
-
A <-->|"stdio"|
|
|
141
|
-
|
|
155
|
+
A <-->|"stdio"| S
|
|
156
|
+
S <-->|"WebSocket (mcp-client)"| B
|
|
157
|
+
C <-->|"WebSocket"| B
|
|
142
158
|
C <-->|"CDP"| D
|
|
143
159
|
```
|
|
144
160
|
|
|
161
|
+
The WebSocket service runs as a detached daemon, independent of any MCP session:
|
|
162
|
+
|
|
163
|
+
- The first session process spawns the daemon automatically; it keeps running after that session exits
|
|
164
|
+
- If the daemon crashes, any live session detects it and respawns it automatically
|
|
165
|
+
- Stop it manually with `ghost-bridge stop` (close live MCP sessions first, otherwise they will bring it back on their next reconnect)
|
|
166
|
+
|
|
145
167
|
## Troubleshooting
|
|
146
168
|
|
|
147
|
-
If the popup shows `No Bridge` / `Not Found`, it means the Chrome extension could not find a Ghost Bridge WebSocket service on the configured port.
|
|
169
|
+
If the popup shows `No Bridge` / `Not Found`, it means the Chrome extension could not find a Ghost Bridge WebSocket service on the configured port. With the resident daemon this normally only happens before the first MCP session of the day starts, or after `ghost-bridge stop`. Starting any MCP session (or reconnecting the extension) brings the service back within seconds.
|
|
148
170
|
|
|
149
171
|
Run `ghost-bridge status` and check:
|
|
150
172
|
|
package/dist/cli.js
CHANGED
|
@@ -6010,6 +6010,7 @@ init_source();
|
|
|
6010
6010
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
6011
6011
|
import path5 from "path";
|
|
6012
6012
|
import fs5 from "fs";
|
|
6013
|
+
import os7 from "os";
|
|
6013
6014
|
var __filename = fileURLToPath2(import.meta.url);
|
|
6014
6015
|
var __dirname = path5.dirname(__filename);
|
|
6015
6016
|
var packageJsonParams = JSON.parse(
|
|
@@ -6062,4 +6063,62 @@ program2.command("status").description("Check Ghost Bridge configuration status"
|
|
|
6062
6063
|
console.error(source_default.red("Error checking status:"), error);
|
|
6063
6064
|
}
|
|
6064
6065
|
});
|
|
6066
|
+
var sleepMs = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
6067
|
+
function isProcessAlive(pid) {
|
|
6068
|
+
try {
|
|
6069
|
+
process.kill(pid, 0);
|
|
6070
|
+
return true;
|
|
6071
|
+
} catch (e) {
|
|
6072
|
+
return e.code === "EPERM";
|
|
6073
|
+
}
|
|
6074
|
+
}
|
|
6075
|
+
function readPortInfoFile() {
|
|
6076
|
+
const portInfoFile = process.env.GHOST_BRIDGE_PORT_INFO || path5.join(os7.tmpdir(), "ghost-bridge-port.json");
|
|
6077
|
+
if (!fs5.existsSync(portInfoFile)) return null;
|
|
6078
|
+
try {
|
|
6079
|
+
const info = JSON.parse(fs5.readFileSync(portInfoFile, "utf-8"));
|
|
6080
|
+
return info && info.pid ? { file: portInfoFile, ...info } : null;
|
|
6081
|
+
} catch {
|
|
6082
|
+
return null;
|
|
6083
|
+
}
|
|
6084
|
+
}
|
|
6085
|
+
function removePortInfoFile(entry) {
|
|
6086
|
+
try {
|
|
6087
|
+
const current = JSON.parse(fs5.readFileSync(entry.file, "utf-8"));
|
|
6088
|
+
if (current.pid === entry.pid) fs5.unlinkSync(entry.file);
|
|
6089
|
+
} catch {
|
|
6090
|
+
}
|
|
6091
|
+
}
|
|
6092
|
+
program2.command("stop").description("Stop the resident ghost-bridge daemon").action(async () => {
|
|
6093
|
+
try {
|
|
6094
|
+
const entry = readPortInfoFile();
|
|
6095
|
+
if (!entry) {
|
|
6096
|
+
console.log(source_default.yellow("\u672A\u53D1\u73B0\u8FD0\u884C\u4E2D\u7684 ghost-bridge \u5E38\u9A7B\u670D\u52A1"));
|
|
6097
|
+
return;
|
|
6098
|
+
}
|
|
6099
|
+
if (!isProcessAlive(entry.pid)) {
|
|
6100
|
+
removePortInfoFile(entry);
|
|
6101
|
+
console.log(source_default.yellow(`\u7AEF\u53E3\u4FE1\u606F\u5DF2\u8FC7\u671F\uFF08PID ${entry.pid} \u4E0D\u5B58\u5728\uFF09\uFF0C\u5DF2\u6E05\u7406`));
|
|
6102
|
+
return;
|
|
6103
|
+
}
|
|
6104
|
+
console.log(source_default.blue(`\u6B63\u5728\u505C\u6B62 ghost-bridge \u5E38\u9A7B\u670D\u52A1 (PID: ${entry.pid})...`));
|
|
6105
|
+
process.kill(entry.pid, "SIGTERM");
|
|
6106
|
+
const deadline = Date.now() + 5e3;
|
|
6107
|
+
while (Date.now() < deadline && isProcessAlive(entry.pid)) {
|
|
6108
|
+
await sleepMs(200);
|
|
6109
|
+
}
|
|
6110
|
+
if (isProcessAlive(entry.pid)) {
|
|
6111
|
+
console.error(source_default.red("\u670D\u52A1\u672A\u80FD\u5728\u9884\u671F\u65F6\u95F4\u5185\u9000\u51FA\uFF0C\u8BF7\u624B\u52A8\u68C0\u67E5\u8BE5\u8FDB\u7A0B"));
|
|
6112
|
+
process.exit(1);
|
|
6113
|
+
}
|
|
6114
|
+
removePortInfoFile(entry);
|
|
6115
|
+
console.log(source_default.green("\u2705 ghost-bridge \u5E38\u9A7B\u670D\u52A1\u5DF2\u505C\u6B62"));
|
|
6116
|
+
console.log(
|
|
6117
|
+
source_default.dim("\u6CE8\u610F\uFF1A\u82E5\u4ECD\u6709\u6D3B\u8DC3\u7684 MCP \u4F1A\u8BDD\uFF0C\u4F1A\u8BDD\u4F1A\u5728\u4E0B\u6B21\u91CD\u8FDE\u65F6\u81EA\u52A8\u91CD\u65B0\u62C9\u8D77\u670D\u52A1\uFF1B\u5982\u9700\u5F7B\u5E95\u505C\u6B62\uFF0C\u8BF7\u5148\u5173\u95ED\u4F7F\u7528\u4E2D\u7684\u4F1A\u8BDD\u3002")
|
|
6118
|
+
);
|
|
6119
|
+
} catch (error) {
|
|
6120
|
+
console.error(source_default.red("Error stopping ghost-bridge:"), error);
|
|
6121
|
+
process.exit(1);
|
|
6122
|
+
}
|
|
6123
|
+
});
|
|
6065
6124
|
program2.parse();
|