@tianshu-ai/local-bridge 0.4.1 → 0.6.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 +72 -8
- package/app/TianshuBridge.swift +311 -0
- package/dist/connection.js +10 -1
- package/dist/connection.js.map +1 -1
- package/dist/index.js +85 -15
- package/dist/index.js.map +1 -1
- package/dist/install-app.js +87 -0
- package/dist/install-app.js.map +1 -0
- package/dist/local-tools.js +402 -0
- package/dist/local-tools.js.map +1 -0
- package/dist/mcp-child.js +27 -1
- package/dist/mcp-child.js.map +1 -1
- package/dist/update.js +51 -67
- package/dist/update.js.map +1 -1
- package/package.json +2 -1
- package/dist/browser.js +0 -170
- package/dist/browser.js.map +0 -1
package/README.md
CHANGED
|
@@ -38,6 +38,25 @@ npx @tianshu-ai/local-bridge --server wss://your-tianshu.example.com/ws --token
|
|
|
38
38
|
The **Local Bridge** panel in tianshu shows a ready-to-copy command with
|
|
39
39
|
your server URL + token filled in.
|
|
40
40
|
|
|
41
|
+
### macOS menu-bar app
|
|
42
|
+
|
|
43
|
+
Prefer a UI? Build a tiny native menu-bar app (no Electron) that wraps
|
|
44
|
+
the CLI — a menu-bar icon with Start/Stop + Settings (server / token /
|
|
45
|
+
browser engine / headless):
|
|
46
|
+
|
|
47
|
+
```bash
|
|
48
|
+
npm i -g @tianshu-ai/local-bridge
|
|
49
|
+
tsbridge install-app --run
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
That installs `Tianshu Bridge.app` to `~/Applications` (menu-bar only, no
|
|
53
|
+
Dock icon) and launches it. Click the bolt icon → Settings to configure,
|
|
54
|
+
then Start. Requires Xcode command line tools (`xcode-select --install`)
|
|
55
|
+
for the one-time `swiftc` build. First launch: right-click → Open
|
|
56
|
+
(unsigned local build).
|
|
57
|
+
|
|
58
|
+
(`app/install-app.sh` is also available for a clone-based build.)
|
|
59
|
+
|
|
41
60
|
For a local dev server with auth off, the token is optional:
|
|
42
61
|
|
|
43
62
|
```bash
|
|
@@ -49,11 +68,15 @@ tsbridge --server ws://localhost:3110/ws
|
|
|
49
68
|
### Keeping it updated
|
|
50
69
|
|
|
51
70
|
```bash
|
|
52
|
-
tsbridge update
|
|
53
|
-
tsbridge update --
|
|
54
|
-
tsbridge
|
|
71
|
+
tsbridge update # update to the latest version if newer (default)
|
|
72
|
+
tsbridge update --check # just check; exit 1 if an update is available
|
|
73
|
+
tsbridge update --dry-run # print the npm command without running it
|
|
74
|
+
tsbridge update --tag next # target a non-`latest` dist-tag
|
|
75
|
+
tsbridge version # print the installed version
|
|
55
76
|
```
|
|
56
77
|
|
|
78
|
+
(Same semantics as `tianshu update`.)
|
|
79
|
+
|
|
57
80
|
### Flags
|
|
58
81
|
|
|
59
82
|
| flag | meaning |
|
|
@@ -63,6 +86,8 @@ tsbridge version # print the installed version
|
|
|
63
86
|
| `--device <id>` | stable device id (default: hostname) |
|
|
64
87
|
| `--label <name>` | human label shown in the panel |
|
|
65
88
|
| `--no-browser` | don't expose browser tools |
|
|
89
|
+
| `--no-shell` | don't expose the shell exec + file sync tools |
|
|
90
|
+
| `--shell-root <dir>` | fixed root dir that jails `exec` + `sync_up`/`sync_down` (default: `~/.tianshu_shell`) |
|
|
66
91
|
| `--headless` | run the browser without a window (default: headful) |
|
|
67
92
|
| `--cdp <url>` | connect to an already-running Chrome's CDP endpoint (default probe `http://127.0.0.1:9222`; `off` to skip) |
|
|
68
93
|
| `--chrome-channel <ch>` | which installed browser to launch: `chrome` (default), `msedge`, … |
|
|
@@ -85,11 +110,50 @@ To expose your everyday browser, start Chrome with a debugging port:
|
|
|
85
110
|
|
|
86
111
|
## Tools
|
|
87
112
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
113
|
+
Both engines expose the **full Playwright-MCP toolset** (navigate,
|
|
114
|
+
snapshot, click, type, fill_form, evaluate, tabs, screenshot, …). Only
|
|
115
|
+
the underlying browser differs:
|
|
116
|
+
|
|
117
|
+
- **own** — `@playwright/mcp` pointed at *your* Chrome: connects over CDP
|
|
118
|
+
to a Chrome already running with `--remote-debugging-port`, else
|
|
119
|
+
launches your system Chrome (`--browser chrome`). Real cookies +
|
|
120
|
+
fingerprint, no Chromium download.
|
|
121
|
+
- **stealth** — `cloakbrowser-mcp`: the same toolset on CloakBrowser
|
|
122
|
+
stealth Chromium (source-level anti-bot-detection patches).
|
|
123
|
+
|
|
124
|
+
### Shell + file sync
|
|
125
|
+
|
|
126
|
+
Unless `--no-shell` is passed, the bridge also exposes three **native**
|
|
127
|
+
tools that run directly on your machine. Their names, parameters and
|
|
128
|
+
result shapes mirror tianshu's server-side `openshell` plugin, so an
|
|
129
|
+
agent written against openshell works unchanged against a bridge device.
|
|
130
|
+
|
|
131
|
+
**All three are jailed to a single fixed root directory**
|
|
132
|
+
(`~/.tianshu_shell` by default, override with `--shell-root`). `exec`'s
|
|
133
|
+
cwd + `workdir`, `sync_up`'s read base, and `sync_down`'s write dest all
|
|
134
|
+
resolve *inside* that root; any path that escapes it is rejected.
|
|
135
|
+
|
|
136
|
+
- **`exec`** — run a shell command via `bash -c`, cwd inside the root.
|
|
137
|
+
Params: `command`, optional `workdir` (relative to the root, defaults
|
|
138
|
+
to the root itself), optional `timeout_ms` (default 300s, cap 1800s).
|
|
139
|
+
Output is truncated at 200 lines / 8000 bytes per stream. Returns
|
|
140
|
+
`{ ok, exit_code, stdout, stderr, truncated, duration_ms, timed_out }`.
|
|
141
|
+
- **`sync_up`** — read files/dirs under the root and return their bytes
|
|
142
|
+
(base64) *up* to the agent. Params: `paths[]` (relative to the root,
|
|
143
|
+
directories walked recursively). Per-file cap 8 MiB, ≤500 files/call.
|
|
144
|
+
- **`sync_down`** — receive files (base64) from the agent and write them
|
|
145
|
+
*down* under the root. Params: `files: [{ path, base64 }]`. Parent
|
|
146
|
+
dirs are created; existing files overwritten.
|
|
147
|
+
|
|
148
|
+
Transport is base64-in-args over the existing reverse-MCP `tools/call`
|
|
149
|
+
channel — no protocol changes.
|
|
150
|
+
|
|
151
|
+
> **Security:** `exec` runs commands with your user's permissions, but
|
|
152
|
+
> is confined to the `--shell-root` directory (default `~/.tianshu_shell`)
|
|
153
|
+
> — the agent can't `cd` or read/write outside it via these tools. It is
|
|
154
|
+
> not a kernel sandbox (a determined command could still reach the wider
|
|
155
|
+
> fs), so only connect a bridge to a tianshu server you trust; pass
|
|
156
|
+
> `--no-shell` to expose the browser only.
|
|
93
157
|
|
|
94
158
|
## Protocol
|
|
95
159
|
|
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
// Tianshu Local Bridge — macOS menu-bar app.
|
|
2
|
+
//
|
|
3
|
+
// A thin GUI wrapper around the `tsbridge` CLI: lives only in the menu
|
|
4
|
+
// bar (LSUIElement), lets you set the server URL / token / browser
|
|
5
|
+
// engine / headless, and Start/Stop the bridge. It spawns `tsbridge`
|
|
6
|
+
// with the chosen flags and shows connection status. Config persists to
|
|
7
|
+
// ~/.tianshu-bridge/config.json.
|
|
8
|
+
//
|
|
9
|
+
// Built on demand by install-app.sh via `swiftc` — no external deps.
|
|
10
|
+
|
|
11
|
+
import AppKit
|
|
12
|
+
import Foundation
|
|
13
|
+
|
|
14
|
+
// ─── config ─────────────────────────────────────────────────────────
|
|
15
|
+
|
|
16
|
+
struct Config: Codable {
|
|
17
|
+
var server: String = "ws://localhost:3110/ws"
|
|
18
|
+
var token: String = ""
|
|
19
|
+
var engine: String = "own" // "own" | "stealth"
|
|
20
|
+
var headless: Bool = false
|
|
21
|
+
var device: String = "" // empty → hostname
|
|
22
|
+
|
|
23
|
+
static var path: URL {
|
|
24
|
+
let dir = FileManager.default.homeDirectoryForCurrentUser
|
|
25
|
+
.appendingPathComponent(".tianshu-bridge", isDirectory: true)
|
|
26
|
+
try? FileManager.default.createDirectory(at: dir, withIntermediateDirectories: true)
|
|
27
|
+
return dir.appendingPathComponent("config.json")
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
static func load() -> Config {
|
|
31
|
+
guard let data = try? Data(contentsOf: path),
|
|
32
|
+
let cfg = try? JSONDecoder().decode(Config.self, from: data) else {
|
|
33
|
+
return Config()
|
|
34
|
+
}
|
|
35
|
+
return cfg
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
func save() {
|
|
39
|
+
if let data = try? JSONEncoder().encode(self) {
|
|
40
|
+
try? data.write(to: Config.path)
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ─── locating tsbridge / node ────────────────────────────────────────
|
|
46
|
+
|
|
47
|
+
// GUI apps don't inherit the shell PATH, and tsbridge often lives under
|
|
48
|
+
// a Node version manager (nvm/fnm). Resolve a usable command + PATH.
|
|
49
|
+
func resolveTsbridge() -> (cmd: String, env: [String: String])? {
|
|
50
|
+
let fm = FileManager.default
|
|
51
|
+
let home = fm.homeDirectoryForCurrentUser.path
|
|
52
|
+
var candidates: [String] = [
|
|
53
|
+
"/usr/local/bin/tsbridge",
|
|
54
|
+
"/opt/homebrew/bin/tsbridge",
|
|
55
|
+
]
|
|
56
|
+
// nvm: newest version dir that has tsbridge
|
|
57
|
+
let nvm = "\(home)/.nvm/versions/node"
|
|
58
|
+
if let vers = try? fm.contentsOfDirectory(atPath: nvm) {
|
|
59
|
+
for v in vers.sorted(by: >) {
|
|
60
|
+
candidates.append("\(nvm)/\(v)/bin/tsbridge")
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
for c in candidates where fm.isExecutableFile(atPath: c) {
|
|
64
|
+
// Prepend the bin dir to PATH so the spawned tsbridge finds
|
|
65
|
+
// `node` and `npx` (needed for @playwright/mcp / cloakbrowser).
|
|
66
|
+
let bin = (c as NSString).deletingLastPathComponent
|
|
67
|
+
var env = ProcessInfo.processInfo.environment
|
|
68
|
+
env["PATH"] = "\(bin):/usr/local/bin:/opt/homebrew/bin:/usr/bin:/bin"
|
|
69
|
+
// GUI-launched processes can start with an empty/missing HOME;
|
|
70
|
+
// @playwright/mcp & cloakbrowser use it for their output dir
|
|
71
|
+
// (else they try to mkdir /.playwright-mcp and crash). Force it.
|
|
72
|
+
if (env["HOME"] ?? "").isEmpty { env["HOME"] = NSHomeDirectory() }
|
|
73
|
+
return (c, env)
|
|
74
|
+
}
|
|
75
|
+
return nil
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── the bridge process ──────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
final class Bridge {
|
|
81
|
+
private var proc: Process?
|
|
82
|
+
var isRunning: Bool { proc?.isRunning ?? false }
|
|
83
|
+
var onStateChange: (() -> Void)?
|
|
84
|
+
|
|
85
|
+
func start(_ cfg: Config) {
|
|
86
|
+
stop()
|
|
87
|
+
guard let (cmd, env) = resolveTsbridge() else {
|
|
88
|
+
notify("tsbridge not found", "Install it: npm i -g @tianshu-ai/local-bridge")
|
|
89
|
+
return
|
|
90
|
+
}
|
|
91
|
+
var args = ["--server", cfg.server]
|
|
92
|
+
if !cfg.token.isEmpty { args += ["--token", cfg.token] }
|
|
93
|
+
if cfg.engine == "stealth" { args += ["--browser-engine", "stealth"] }
|
|
94
|
+
if cfg.headless { args += ["--headless"] }
|
|
95
|
+
if !cfg.device.isEmpty { args += ["--device", cfg.device] }
|
|
96
|
+
|
|
97
|
+
let p = Process()
|
|
98
|
+
p.executableURL = URL(fileURLWithPath: cmd)
|
|
99
|
+
p.arguments = args
|
|
100
|
+
p.environment = env
|
|
101
|
+
// Log to ~/.tianshu-bridge/bridge.log for troubleshooting.
|
|
102
|
+
let logURL = Config.path.deletingLastPathComponent().appendingPathComponent("bridge.log")
|
|
103
|
+
FileManager.default.createFile(atPath: logURL.path, contents: nil)
|
|
104
|
+
if let fh = try? FileHandle(forWritingTo: logURL) {
|
|
105
|
+
p.standardOutput = fh
|
|
106
|
+
p.standardError = fh
|
|
107
|
+
}
|
|
108
|
+
p.terminationHandler = { [weak self] _ in
|
|
109
|
+
DispatchQueue.main.async { self?.onStateChange?() }
|
|
110
|
+
}
|
|
111
|
+
do {
|
|
112
|
+
try p.run()
|
|
113
|
+
proc = p
|
|
114
|
+
} catch {
|
|
115
|
+
notify("Failed to start bridge", error.localizedDescription)
|
|
116
|
+
}
|
|
117
|
+
onStateChange?()
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
func stop() {
|
|
121
|
+
if let p = proc, p.isRunning { p.terminate() }
|
|
122
|
+
proc = nil
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
private func notify(_ title: String, _ body: String) {
|
|
126
|
+
let a = NSAlert()
|
|
127
|
+
a.messageText = title
|
|
128
|
+
a.informativeText = body
|
|
129
|
+
a.runModal()
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// ─── menu-bar controller ─────────────────────────────────────────────
|
|
134
|
+
|
|
135
|
+
final class AppController: NSObject, NSApplicationDelegate {
|
|
136
|
+
// Created in applicationDidFinishLaunching (not at init) so the app
|
|
137
|
+
// is fully up before we claim a status-bar slot.
|
|
138
|
+
var statusItem: NSStatusItem!
|
|
139
|
+
let bridge = Bridge()
|
|
140
|
+
var cfg = Config.load()
|
|
141
|
+
var configWindow: NSWindow?
|
|
142
|
+
|
|
143
|
+
func applicationDidFinishLaunching(_ notification: Notification) {
|
|
144
|
+
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
|
|
145
|
+
updateIcon()
|
|
146
|
+
bridge.onStateChange = { [weak self] in
|
|
147
|
+
self?.updateIcon()
|
|
148
|
+
self?.rebuildMenu()
|
|
149
|
+
}
|
|
150
|
+
rebuildMenu()
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
func updateIcon() {
|
|
154
|
+
guard let btn = statusItem.button else { return }
|
|
155
|
+
let on = bridge.isRunning
|
|
156
|
+
let name = on ? "bolt.horizontal.circle.fill" : "bolt.horizontal.circle"
|
|
157
|
+
if let img = NSImage(systemSymbolName: name, accessibilityDescription: "Tianshu Bridge") {
|
|
158
|
+
img.isTemplate = true
|
|
159
|
+
btn.image = img
|
|
160
|
+
btn.title = ""
|
|
161
|
+
} else {
|
|
162
|
+
// SF Symbol unavailable → always-visible text fallback.
|
|
163
|
+
btn.image = nil
|
|
164
|
+
btn.title = on ? "⚡●" : "⚡"
|
|
165
|
+
}
|
|
166
|
+
btn.toolTip = on ? "Tianshu Bridge: connected" : "Tianshu Bridge: stopped"
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
func rebuildMenu() {
|
|
170
|
+
let menu = NSMenu()
|
|
171
|
+
let status = bridge.isRunning ? "● Connected" : "○ Stopped"
|
|
172
|
+
let statusMI = NSMenuItem(title: status, action: nil, keyEquivalent: "")
|
|
173
|
+
statusMI.isEnabled = false
|
|
174
|
+
menu.addItem(statusMI)
|
|
175
|
+
menu.addItem(NSMenuItem(title: "Server: \(cfg.server)", action: nil, keyEquivalent: ""))
|
|
176
|
+
let eng = cfg.engine == "stealth" ? "stealth" : "own"
|
|
177
|
+
menu.addItem(NSMenuItem(title: "Browser: \(eng)\(cfg.headless ? " (headless)" : "")",
|
|
178
|
+
action: nil, keyEquivalent: ""))
|
|
179
|
+
menu.addItem(.separator())
|
|
180
|
+
|
|
181
|
+
if bridge.isRunning {
|
|
182
|
+
menu.addItem(withTitle: "Stop", action: #selector(stop), keyEquivalent: "s").target = self
|
|
183
|
+
} else {
|
|
184
|
+
menu.addItem(withTitle: "Start", action: #selector(start), keyEquivalent: "s").target = self
|
|
185
|
+
}
|
|
186
|
+
menu.addItem(withTitle: "Settings…", action: #selector(openSettings), keyEquivalent: ",").target = self
|
|
187
|
+
menu.addItem(withTitle: "Open Log", action: #selector(openLog), keyEquivalent: "").target = self
|
|
188
|
+
menu.addItem(.separator())
|
|
189
|
+
menu.addItem(withTitle: "Quit", action: #selector(quit), keyEquivalent: "q").target = self
|
|
190
|
+
statusItem.menu = menu
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
@objc func start() { bridge.start(cfg) }
|
|
194
|
+
@objc func stop() { bridge.stop(); updateIcon(); rebuildMenu() }
|
|
195
|
+
@objc func openLog() {
|
|
196
|
+
let log = Config.path.deletingLastPathComponent().appendingPathComponent("bridge.log")
|
|
197
|
+
NSWorkspace.shared.open(log)
|
|
198
|
+
}
|
|
199
|
+
@objc func quit() { bridge.stop(); NSApp.terminate(nil) }
|
|
200
|
+
|
|
201
|
+
// ── settings window (simple form) ──
|
|
202
|
+
@objc func openSettings() {
|
|
203
|
+
if configWindow == nil {
|
|
204
|
+
let w = NSWindow(contentRect: NSRect(x: 0, y: 0, width: 380, height: 300),
|
|
205
|
+
styleMask: [.titled, .closable], backing: .buffered, defer: false)
|
|
206
|
+
w.title = "Tianshu Bridge Settings"
|
|
207
|
+
w.center()
|
|
208
|
+
w.contentView = makeSettingsView()
|
|
209
|
+
w.isReleasedWhenClosed = false
|
|
210
|
+
configWindow = w
|
|
211
|
+
}
|
|
212
|
+
NSApp.activate(ignoringOtherApps: true)
|
|
213
|
+
configWindow?.makeKeyAndOrderFront(nil)
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
private var serverField: NSTextField!
|
|
217
|
+
private var tokenField: NSTextField!
|
|
218
|
+
private var enginePopup: NSPopUpButton!
|
|
219
|
+
private var headlessCheck: NSButton!
|
|
220
|
+
|
|
221
|
+
private func makeSettingsView() -> NSView {
|
|
222
|
+
let v = NSView(frame: NSRect(x: 0, y: 0, width: 380, height: 300))
|
|
223
|
+
func label(_ s: String, _ y: CGFloat) -> NSTextField {
|
|
224
|
+
let l = NSTextField(labelWithString: s); l.frame = NSRect(x: 16, y: y, width: 90, height: 20); return l
|
|
225
|
+
}
|
|
226
|
+
// Paste-config row: grab the config string copied from the
|
|
227
|
+
// tianshu Local Bridge panel and fill every field at once.
|
|
228
|
+
let paste = NSButton(title: "Paste from tianshu", target: self, action: #selector(pasteConfig))
|
|
229
|
+
paste.frame = NSRect(x: 16, y: 258, width: 200, height: 28)
|
|
230
|
+
paste.bezelStyle = .rounded
|
|
231
|
+
v.addSubview(paste)
|
|
232
|
+
let hint = NSTextField(labelWithString: "(copies server + token + browser settings)")
|
|
233
|
+
hint.frame = NSRect(x: 16, y: 238, width: 348, height: 16)
|
|
234
|
+
hint.font = NSFont.systemFont(ofSize: 10)
|
|
235
|
+
hint.textColor = .secondaryLabelColor
|
|
236
|
+
v.addSubview(hint)
|
|
237
|
+
|
|
238
|
+
v.addSubview(label("Server", 210))
|
|
239
|
+
serverField = NSTextField(frame: NSRect(x: 110, y: 208, width: 250, height: 24))
|
|
240
|
+
serverField.stringValue = cfg.server; v.addSubview(serverField)
|
|
241
|
+
|
|
242
|
+
v.addSubview(label("Token", 172))
|
|
243
|
+
tokenField = NSTextField(frame: NSRect(x: 110, y: 170, width: 250, height: 24))
|
|
244
|
+
tokenField.stringValue = cfg.token; v.addSubview(tokenField)
|
|
245
|
+
|
|
246
|
+
v.addSubview(label("Browser", 134))
|
|
247
|
+
enginePopup = NSPopUpButton(frame: NSRect(x: 110, y: 130, width: 250, height: 26))
|
|
248
|
+
enginePopup.addItems(withTitles: ["own (your Chrome)", "stealth (CloakBrowser)"])
|
|
249
|
+
enginePopup.selectItem(at: cfg.engine == "stealth" ? 1 : 0)
|
|
250
|
+
v.addSubview(enginePopup)
|
|
251
|
+
|
|
252
|
+
headlessCheck = NSButton(checkboxWithTitle: "Headless (no window)", target: nil, action: nil)
|
|
253
|
+
headlessCheck.frame = NSRect(x: 110, y: 98, width: 250, height: 20)
|
|
254
|
+
headlessCheck.state = cfg.headless ? .on : .off
|
|
255
|
+
v.addSubview(headlessCheck)
|
|
256
|
+
|
|
257
|
+
let save = NSButton(title: "Save & Restart", target: self, action: #selector(saveSettings))
|
|
258
|
+
save.frame = NSRect(x: 200, y: 20, width: 160, height: 32)
|
|
259
|
+
save.bezelStyle = .rounded
|
|
260
|
+
v.addSubview(save)
|
|
261
|
+
return v
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
@objc func saveSettings() {
|
|
265
|
+
cfg.server = serverField.stringValue.trimmingCharacters(in: .whitespaces)
|
|
266
|
+
cfg.token = tokenField.stringValue.trimmingCharacters(in: .whitespaces)
|
|
267
|
+
cfg.engine = enginePopup.indexOfSelectedItem == 1 ? "stealth" : "own"
|
|
268
|
+
cfg.headless = headlessCheck.state == .on
|
|
269
|
+
cfg.save()
|
|
270
|
+
configWindow?.close()
|
|
271
|
+
rebuildMenu()
|
|
272
|
+
if bridge.isRunning { bridge.start(cfg) } // restart with new config
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
// Parse the config string copied from the tianshu panel and fill the
|
|
276
|
+
// form. Accepts a `tsbridge://configure?server=…&token=…&engine=…&
|
|
277
|
+
// headless=…` URL, or a bare query string of the same params.
|
|
278
|
+
@objc func pasteConfig() {
|
|
279
|
+
let raw = (NSPasteboard.general.string(forType: .string) ?? "").trimmingCharacters(in: .whitespacesAndNewlines)
|
|
280
|
+
guard !raw.isEmpty else { return alert("Clipboard is empty", "Copy the config from the tianshu Local Bridge panel first.") }
|
|
281
|
+
// Extract the query part whether it's a full URL or bare query.
|
|
282
|
+
var query = raw
|
|
283
|
+
if let q = raw.range(of: "?") { query = String(raw[raw.index(after: q.lowerBound)...]) }
|
|
284
|
+
var params: [String: String] = [:]
|
|
285
|
+
for pair in query.split(separator: "&") {
|
|
286
|
+
let kv = pair.split(separator: "=", maxSplits: 1).map(String.init)
|
|
287
|
+
if kv.count == 2 {
|
|
288
|
+
params[kv[0]] = kv[1].removingPercentEncoding ?? kv[1]
|
|
289
|
+
}
|
|
290
|
+
}
|
|
291
|
+
guard let server = params["server"], !server.isEmpty else {
|
|
292
|
+
return alert("Couldn't read config", "Expected a tsbridge://configure?server=… string copied from the tianshu panel.")
|
|
293
|
+
}
|
|
294
|
+
serverField.stringValue = server
|
|
295
|
+
if let t = params["token"] { tokenField.stringValue = t }
|
|
296
|
+
if let e = params["engine"] { enginePopup.selectItem(at: e == "stealth" ? 1 : 0) }
|
|
297
|
+
if let h = params["headless"] { headlessCheck.state = (h == "1" || h == "true") ? .on : .off }
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
private func alert(_ title: String, _ body: String) {
|
|
301
|
+
let a = NSAlert(); a.messageText = title; a.informativeText = body; a.runModal()
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
// ─── main ────────────────────────────────────────────────────────────
|
|
306
|
+
|
|
307
|
+
let app = NSApplication.shared
|
|
308
|
+
app.setActivationPolicy(.accessory) // menu-bar only, no Dock icon
|
|
309
|
+
let controller = AppController()
|
|
310
|
+
app.delegate = controller
|
|
311
|
+
app.run()
|
package/dist/connection.js
CHANGED
|
@@ -90,7 +90,16 @@ export class BridgeConnection {
|
|
|
90
90
|
}, delay);
|
|
91
91
|
});
|
|
92
92
|
ws.on("error", (err) => {
|
|
93
|
-
|
|
93
|
+
const e = err;
|
|
94
|
+
const detail = [e.code, e.message].filter(Boolean).join(" ") ||
|
|
95
|
+
(err ? String(err) : "unknown (connection refused / handshake rejected?)");
|
|
96
|
+
this.log(`socket error connecting to ${this.opts.server}: ${detail}`);
|
|
97
|
+
});
|
|
98
|
+
// Surface non-101 handshake responses (auth/path problems), which
|
|
99
|
+
// otherwise arrive as an opaque error.
|
|
100
|
+
ws.on("unexpected-response", (_req, res) => {
|
|
101
|
+
this.log(`server rejected the WebSocket upgrade: HTTP ${res.statusCode} ${res.statusMessage ?? ""} ` +
|
|
102
|
+
`(check --server URL/path and --token). Expected /ws to accept an upgrade.`);
|
|
94
103
|
});
|
|
95
104
|
}
|
|
96
105
|
send(msg) {
|
package/dist/connection.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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
|
|
1
|
+
{"version":3,"file":"connection.js","sourceRoot":"","sources":["../src/connection.ts"],"names":[],"mappings":"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"}
|
package/dist/index.js
CHANGED
|
@@ -14,22 +14,44 @@
|
|
|
14
14
|
// --device <id> stable device id (default: hostname)
|
|
15
15
|
// --label <name> human label shown in the panel (default: device id)
|
|
16
16
|
// --no-browser don't expose browser tools
|
|
17
|
+
// --no-shell don't expose the shell exec + file sync tools
|
|
18
|
+
// --shell-root <dir> fixed root dir that jails exec + sync
|
|
19
|
+
// (default: ~/.tianshu_shell)
|
|
17
20
|
// --browser-engine own | stealth (default: own)
|
|
18
21
|
// own = your system/running Chrome (no download,
|
|
19
22
|
// real cookies+fingerprint)
|
|
20
23
|
// stealth = cloakbrowser-mcp (CloakBrowser stealth
|
|
21
24
|
// Chromium, full Playwright-MCP toolset,
|
|
22
25
|
// passes bot detection; ~200MB first run)
|
|
23
|
-
// --headless run the browser without a window (own
|
|
26
|
+
// --headless run the browser without a window (own + stealth;
|
|
27
|
+
// default is headful — a window you can watch)
|
|
24
28
|
// --cdp <url> own: connect to a running Chrome's CDP endpoint
|
|
25
29
|
// (default probe http://127.0.0.1:9222; "off" to skip)
|
|
26
30
|
// --chrome-channel own: which installed browser to launch (chrome, msedge, …)
|
|
27
31
|
// --user-data-dir own: reuse a Chrome profile dir so logins carry over
|
|
32
|
+
//
|
|
33
|
+
// Subcommands:
|
|
34
|
+
// tsbridge update [--check|--dry-run|--tag <t>] self-update
|
|
35
|
+
// tsbridge install-app [--run] build + install the
|
|
36
|
+
// macOS menu-bar app
|
|
37
|
+
// tsbridge version print installed version
|
|
28
38
|
import os from "node:os";
|
|
29
39
|
import { BridgeConnection } from "./connection.js";
|
|
30
|
-
import {
|
|
31
|
-
import {
|
|
40
|
+
import { connectMcpChild, bridgeOutputDir } from "./mcp-child.js";
|
|
41
|
+
import { localTools, defaultShellRoot } from "./local-tools.js";
|
|
32
42
|
import { runUpdate, installedVersion } from "./update.js";
|
|
43
|
+
import { installApp } from "./install-app.js";
|
|
44
|
+
/** Is a Chrome already listening on this CDP endpoint? */
|
|
45
|
+
async function probeCdp(cdpUrl) {
|
|
46
|
+
try {
|
|
47
|
+
const base = cdpUrl.replace(/\/$/, "");
|
|
48
|
+
const res = await fetch(`${base}/json/version`, { signal: AbortSignal.timeout(2500) });
|
|
49
|
+
return res.ok;
|
|
50
|
+
}
|
|
51
|
+
catch {
|
|
52
|
+
return false;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
33
55
|
function parseArgs(argv) {
|
|
34
56
|
const out = {};
|
|
35
57
|
for (let i = 0; i < argv.length; i++) {
|
|
@@ -54,18 +76,30 @@ async function main() {
|
|
|
54
76
|
const sub = argv.find((a) => !a.startsWith("--"));
|
|
55
77
|
const args = parseArgs(argv);
|
|
56
78
|
if (sub === "update") {
|
|
57
|
-
process.exit(await runUpdate(
|
|
79
|
+
process.exit(await runUpdate({
|
|
80
|
+
check: args.check === true,
|
|
81
|
+
dryRun: args["dry-run"] === true,
|
|
82
|
+
tag: typeof args.tag === "string" ? args.tag : undefined,
|
|
83
|
+
}));
|
|
58
84
|
}
|
|
59
85
|
if (sub === "version" || args.version === true || args.v === true) {
|
|
60
86
|
process.stdout.write(`@tianshu-ai/local-bridge v${installedVersion()}\n`);
|
|
61
87
|
process.exit(0);
|
|
62
88
|
}
|
|
89
|
+
if (sub === "install-app") {
|
|
90
|
+
process.exit(installApp({
|
|
91
|
+
run: args.run === true,
|
|
92
|
+
dest: typeof args.dest === "string" ? args.dest : undefined,
|
|
93
|
+
}));
|
|
94
|
+
}
|
|
63
95
|
const server = typeof args.server === "string" ? args.server : "";
|
|
64
96
|
if (!server) {
|
|
65
97
|
console.error("error: --server <wss://host/ws> is required");
|
|
66
98
|
console.error("example: tsbridge --server wss://tianshu.example.com/ws --token ***");
|
|
67
99
|
console.error("install: npm i -g @tianshu-ai/local-bridge (then run `tsbridge`)");
|
|
68
|
-
console.error("commands: tsbridge update [--
|
|
100
|
+
console.error("commands: tsbridge update [--check|--dry-run|--tag <t>]");
|
|
101
|
+
console.error(" tsbridge install-app [--run] (macOS menu-bar app)");
|
|
102
|
+
console.error(" tsbridge version");
|
|
69
103
|
process.exit(2);
|
|
70
104
|
}
|
|
71
105
|
const token = typeof args.token === "string" ? args.token : undefined;
|
|
@@ -78,17 +112,22 @@ async function main() {
|
|
|
78
112
|
const channel = typeof args["chrome-channel"] === "string" ? args["chrome-channel"] : "chrome";
|
|
79
113
|
const userDataDir = typeof args["user-data-dir"] === "string" ? args["user-data-dir"] : "";
|
|
80
114
|
const browserOn = args["no-browser"] !== true;
|
|
115
|
+
const shellOn = args["no-shell"] !== true;
|
|
81
116
|
const engine = args["browser-engine"] === "stealth" ? "stealth" : "own";
|
|
117
|
+
const shellRoot = typeof args["shell-root"] === "string" ? args["shell-root"] : defaultShellRoot();
|
|
82
118
|
const tools = [];
|
|
83
119
|
if (browserOn) {
|
|
84
120
|
if (engine === "stealth") {
|
|
85
121
|
// Full Playwright-MCP toolset pointed at CloakBrowser stealth
|
|
86
122
|
// Chromium. Spawned as a child MCP server over stdio.
|
|
87
123
|
try {
|
|
124
|
+
// cloakbrowser-mcp reads PLAYWRIGHT_MCP_HEADLESS (default true =
|
|
125
|
+
// headless). Set it explicitly from our headful/--headless flag
|
|
126
|
+
// so the window shows when the user wants it.
|
|
88
127
|
const stealthTools = await connectMcpChild({
|
|
89
128
|
command: "npx",
|
|
90
129
|
args: ["-y", "cloakbrowser-mcp@latest"],
|
|
91
|
-
env: headful ?
|
|
130
|
+
env: { PLAYWRIGHT_MCP_HEADLESS: headful ? "false" : "true" },
|
|
92
131
|
log: (m) => console.log(`[local-bridge] ${m}`),
|
|
93
132
|
});
|
|
94
133
|
tools.push(...stealthTools);
|
|
@@ -99,17 +138,48 @@ async function main() {
|
|
|
99
138
|
}
|
|
100
139
|
}
|
|
101
140
|
else {
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
141
|
+
// own engine: the full @playwright/mcp toolset pointed at the
|
|
142
|
+
// user's real Chrome — connect to a running Chrome over CDP if one
|
|
143
|
+
// is up, else launch the system Chrome (channel). Same tool
|
|
144
|
+
// surface as stealth; only the underlying browser differs.
|
|
145
|
+
const log = (m) => console.log(`[local-bridge] ${m}`);
|
|
146
|
+
// Pin output dir explicitly so it never resolves against a bad cwd.
|
|
147
|
+
const mcpArgs = ["-y", "@playwright/mcp@latest", "--output-dir", bridgeOutputDir()];
|
|
148
|
+
const cdpUp = cdpUrl ? await probeCdp(cdpUrl) : false;
|
|
149
|
+
if (cdpUp) {
|
|
150
|
+
mcpArgs.push("--cdp-endpoint", cdpUrl);
|
|
151
|
+
log(`own: connecting to your running Chrome at ${cdpUrl}`);
|
|
152
|
+
}
|
|
153
|
+
else {
|
|
154
|
+
// @playwright/mcp: --browser takes a browser OR chrome channel
|
|
155
|
+
// ("chrome", "msedge", …). Point it at the system Chrome.
|
|
156
|
+
mcpArgs.push("--browser", channel);
|
|
157
|
+
if (userDataDir)
|
|
158
|
+
mcpArgs.push("--user-data-dir", userDataDir);
|
|
159
|
+
if (!headful)
|
|
160
|
+
mcpArgs.push("--headless");
|
|
161
|
+
log(`own: launching system ${channel}${headful ? "" : " (headless)"}`);
|
|
162
|
+
}
|
|
163
|
+
try {
|
|
164
|
+
const ownTools = await connectMcpChild({ command: "npx", args: mcpArgs, log });
|
|
165
|
+
tools.push(...ownTools);
|
|
166
|
+
}
|
|
167
|
+
catch (err) {
|
|
168
|
+
console.error(`[local-bridge] failed to start browser (@playwright/mcp): ${err instanceof Error ? err.message : String(err)}`);
|
|
169
|
+
process.exit(1);
|
|
170
|
+
}
|
|
109
171
|
}
|
|
110
172
|
}
|
|
173
|
+
// Native local tools: shell exec + file sync. Run directly on this
|
|
174
|
+
// machine (no child MCP), mirroring the openshell plugin's
|
|
175
|
+
// exec / sync_up / sync_down surface.
|
|
176
|
+
if (shellOn) {
|
|
177
|
+
const log = (m) => console.log(`[local-bridge] ${m}`);
|
|
178
|
+
tools.push(...localTools({ root: shellRoot, log }));
|
|
179
|
+
log(`shell: exec + sync_up/sync_down enabled, jailed to ${shellRoot}`);
|
|
180
|
+
}
|
|
111
181
|
if (tools.length === 0) {
|
|
112
|
-
console.error("[local-bridge] no tools enabled — nothing to expose. Remove --no-browser (or enable a capability) and try again.");
|
|
182
|
+
console.error("[local-bridge] no tools enabled — nothing to expose. Remove --no-browser / --no-shell (or enable a capability) and try again.");
|
|
113
183
|
process.exit(2);
|
|
114
184
|
}
|
|
115
185
|
const conn = new BridgeConnection({
|
|
@@ -127,7 +197,7 @@ async function main() {
|
|
|
127
197
|
: cdpUrl
|
|
128
198
|
? `own: connect(${cdpUrl}) or ${channel}`
|
|
129
199
|
: `own: ${channel}`;
|
|
130
|
-
console.log(`[local-bridge] starting — device="${deviceId}", ${tools.length} tools, browser=${browserDesc}`);
|
|
200
|
+
console.log(`[local-bridge] starting — device="${deviceId}", ${tools.length} tools, browser=${browserDesc}, shell=${shellOn ? "on" : "off"}`);
|
|
131
201
|
conn.start();
|
|
132
202
|
const shutdown = () => {
|
|
133
203
|
console.log("\n[local-bridge] shutting down…");
|