arena-bridge 0.6.5 → 0.6.7
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/CHANGELOG.md +13 -0
- package/README.md +17 -1
- package/package.json +1 -1
- package/src/cli.mjs +18 -2
- package/src/direct.mjs +1 -1
- package/src/hub.mjs +1 -1
- package/src/mcp.mjs +1 -1
- package/src/service.mjs +9 -3
- package/src/tunnel.mjs +41 -22
package/CHANGELOG.md
CHANGED
|
@@ -4,6 +4,19 @@ All notable changes to this project are documented here.
|
|
|
4
4
|
The format follows [Keep a Changelog](https://keepachangelog.com/) and the project uses
|
|
5
5
|
[Semantic Versioning](https://semver.org/).
|
|
6
6
|
|
|
7
|
+
## 0.6.7
|
|
8
|
+
|
|
9
|
+
### Added
|
|
10
|
+
- `arena-bridge service install --bin ngrok --domain <name>` installs the macOS background
|
|
11
|
+
service with a fixed public address; `arena-bridge service status` now shows the current URL.
|
|
12
|
+
|
|
13
|
+
## 0.6.6
|
|
14
|
+
|
|
15
|
+
### Fixed
|
|
16
|
+
- The tunnel is now supervised: when the tunnel binary dies, it is started again automatically
|
|
17
|
+
and the new public URL is printed and saved to the config, instead of leaving a dead tunnel
|
|
18
|
+
behind (the bridge used to keep running while the public address had stopped working).
|
|
19
|
+
|
|
7
20
|
## 0.6.5
|
|
8
21
|
|
|
9
22
|
### Fixed
|
package/README.md
CHANGED
|
@@ -12,7 +12,7 @@
|
|
|
12
12
|
<img src="https://img.shields.io/badge/license-MIT-blue.svg?style=flat-square" alt="license">
|
|
13
13
|
<img src="https://img.shields.io/badge/node-%3E%3D18-green.svg?style=flat-square" alt="node">
|
|
14
14
|
<img src="https://img.shields.io/badge/dependencies-0-brightgreen.svg?style=flat-square" alt="dependencies">
|
|
15
|
-
<img src="https://img.shields.io/badge/tests-
|
|
15
|
+
<img src="https://img.shields.io/badge/tests-115%20passed-brightgreen.svg?style=flat-square" alt="tests">
|
|
16
16
|
</p>
|
|
17
17
|
|
|
18
18
|
<p align="center">
|
|
@@ -53,6 +53,10 @@ arena-bridge setup --root ~/projects --root ~/Documents
|
|
|
53
53
|
|
|
54
54
|
`setup` печатает готовый промпт и публичный URL. Дальше — просто говорите агенту задачу.
|
|
55
55
|
|
|
56
|
+
Туннель под присмотром: если `cloudflared`/`ngrok` упадёт, мост поднимет его заново и сам
|
|
57
|
+
напечатает новый публичный адрес. Постоянный адрес вместо случайного — `arena-bridge tunnel
|
|
58
|
+
--bin ngrok --domain <зарезервированный домен>`.
|
|
59
|
+
|
|
56
60
|
Нужен туннель? Поставьте `cloudflared` (`brew install cloudflared` или бинарник в `/usr/local/bin`).
|
|
57
61
|
Можно и через ngrok — `arena-bridge tunnel --bin ngrok`; с сохранённым токеном и зарезервированным
|
|
58
62
|
доменом публичный адрес остаётся постоянным, и промпт агенту не нужно копировать заново:
|
|
@@ -60,6 +64,18 @@ arena-bridge setup --root ~/projects --root ~/Documents
|
|
|
60
64
|
|
|
61
65
|
### В фоне, чтобы не открывать терминал
|
|
62
66
|
|
|
67
|
+
Мост можно поднять как службу macOS — он стартует вместе с системой, переживает закрытие
|
|
68
|
+
терминала и поднимается заново после сбоя:
|
|
69
|
+
|
|
70
|
+
```bash
|
|
71
|
+
arena-bridge service install # облако, случайный адрес
|
|
72
|
+
arena-bridge service install --bin ngrok --domain mymac.ngrok-free.app # постоянный адрес
|
|
73
|
+
arena-bridge service status # состояние и текущий URL
|
|
74
|
+
arena-bridge url # текущий публичный адрес
|
|
75
|
+
arena-bridge service uninstall
|
|
76
|
+
```
|
|
77
|
+
|
|
78
|
+
|
|
63
79
|
```bash
|
|
64
80
|
arena-bridge service install # launchd: стартует вместе с системой, URL всегда живой
|
|
65
81
|
arena-bridge url # текущий публичный URL
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "arena-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.7",
|
|
4
4
|
"description": "Give a cloud AI agent (Arena.ai Agent Mode, Claude, Cursor, ...) controlled access to your own machine over HTTP/MCP: files, shell, Chrome and the macOS desktop, with an approval gate, trust windows and an audit log.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
package/src/cli.mjs
CHANGED
|
@@ -199,7 +199,7 @@ async function cmdTunnel(args) {
|
|
|
199
199
|
}
|
|
200
200
|
|
|
201
201
|
console.log(`starting ${bin} (protocol=${protocol})…`);
|
|
202
|
-
const { child, urlPromise } = startTunnel(cfg, {
|
|
202
|
+
const { child, urlPromise, onUrl } = startTunnel(cfg, {
|
|
203
203
|
port,
|
|
204
204
|
protocol,
|
|
205
205
|
bin,
|
|
@@ -229,6 +229,15 @@ async function cmdTunnel(args) {
|
|
|
229
229
|
cfg.port = port;
|
|
230
230
|
saveConfig(cfg);
|
|
231
231
|
|
|
232
|
+
onUrl((url) => {
|
|
233
|
+
if (url === publicUrl) return;
|
|
234
|
+
publicUrl = url;
|
|
235
|
+
cfg.publicUrl = url;
|
|
236
|
+
saveConfig(cfg);
|
|
237
|
+
console.log(`\n⚠ tunnel reconnected — new public URL: ${url}`);
|
|
238
|
+
console.log('Update the base URL in the agent chat (arena-bridge prompt prints a fresh copy).');
|
|
239
|
+
});
|
|
240
|
+
|
|
232
241
|
console.log(`\npublic URL: ${publicUrl} (saved as config.publicUrl)\n`);
|
|
233
242
|
console.log('Paste this into the agent chat:');
|
|
234
243
|
console.log('-----8<-----');
|
|
@@ -431,7 +440,12 @@ async function cmdUpdate(args) {
|
|
|
431
440
|
function cmdService(args) {
|
|
432
441
|
const what = args._[1] || 'status';
|
|
433
442
|
if (what === 'install') {
|
|
434
|
-
const res = serviceInstall(
|
|
443
|
+
const res = serviceInstall({
|
|
444
|
+
bin: args.flags.bin,
|
|
445
|
+
domain: args.flags.domain,
|
|
446
|
+
authtoken: args.flags.authtoken,
|
|
447
|
+
port: args.flags.port,
|
|
448
|
+
});
|
|
435
449
|
console.log(`\u2713 service installed: ${res.plist}`);
|
|
436
450
|
console.log(' the bridge now starts with the system and keeps a public URL alive');
|
|
437
451
|
console.log(' current URL: arena-bridge url');
|
|
@@ -445,6 +459,8 @@ function cmdService(args) {
|
|
|
445
459
|
const st = serviceStatus();
|
|
446
460
|
console.log(`service: ${st.installed ? 'installed' : 'not installed'} (${st.running ? 'running' : 'not running'})`);
|
|
447
461
|
console.log(` plist: ${st.plist}`);
|
|
462
|
+
const current = loadConfig().publicUrl;
|
|
463
|
+
if (current) console.log(` current URL: ${current}`);
|
|
448
464
|
if (st.detail) console.log(` ${st.detail.split('\n')[0]}`);
|
|
449
465
|
if (!st.installed) console.log(' install it with: arena-bridge service install');
|
|
450
466
|
}
|
package/src/direct.mjs
CHANGED
package/src/hub.mjs
CHANGED
package/src/mcp.mjs
CHANGED
|
@@ -48,7 +48,7 @@ export async function handleMcp(req, res, ctx) {
|
|
|
48
48
|
mcpResponse(id, {
|
|
49
49
|
protocolVersion: PROTOCOL_VERSION,
|
|
50
50
|
capabilities: { tools: { listChanged: false } },
|
|
51
|
-
serverInfo: { name: 'arena-bridge', version: '0.6.
|
|
51
|
+
serverInfo: { name: 'arena-bridge', version: '0.6.7' },
|
|
52
52
|
instructions:
|
|
53
53
|
'Tools operate the user\'s own machine. Dangerous operations may return ' +
|
|
54
54
|
'PROTECTED and require a local human approval.',
|
package/src/service.mjs
CHANGED
|
@@ -21,9 +21,14 @@ function cliEntry() {
|
|
|
21
21
|
return path.join(here, '..', 'bin', 'arena-bridge.mjs');
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
function plistXml() {
|
|
24
|
+
function plistXml({ bin, domain, authtoken, port } = {}) {
|
|
25
25
|
const cli = cliEntry();
|
|
26
26
|
const logs = logDir();
|
|
27
|
+
const extra = [];
|
|
28
|
+
if (bin) extra.push('--bin', String(bin));
|
|
29
|
+
if (domain) extra.push('--domain', String(domain));
|
|
30
|
+
if (authtoken) extra.push('--authtoken', String(authtoken));
|
|
31
|
+
if (port) extra.push('--port', String(port));
|
|
27
32
|
return `<?xml version="1.0" encoding="UTF-8"?>
|
|
28
33
|
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
|
|
29
34
|
<plist version="1.0">
|
|
@@ -34,6 +39,7 @@ function plistXml() {
|
|
|
34
39
|
<string>${process.execPath}</string>
|
|
35
40
|
<string>${cli}</string>
|
|
36
41
|
<string>tunnel</string>
|
|
42
|
+
${extra.map((a) => ` <string>${a}</string>`).join('\n')}
|
|
37
43
|
</array>
|
|
38
44
|
<key>RunAtLoad</key><true/>
|
|
39
45
|
<key>KeepAlive</key><true/>
|
|
@@ -63,12 +69,12 @@ export function serviceStatus() {
|
|
|
63
69
|
return { installed, running, plist: plistPath(), detail: out.trim().slice(0, 600) };
|
|
64
70
|
}
|
|
65
71
|
|
|
66
|
-
export function serviceInstall() {
|
|
72
|
+
export function serviceInstall(options = {}) {
|
|
67
73
|
ensureDirs();
|
|
68
74
|
fs.mkdirSync(logDir(), { recursive: true });
|
|
69
75
|
const dir = path.dirname(plistPath());
|
|
70
76
|
fs.mkdirSync(dir, { recursive: true });
|
|
71
|
-
fs.writeFileSync(plistPath(), plistXml());
|
|
77
|
+
fs.writeFileSync(plistPath(), plistXml(options));
|
|
72
78
|
launchctl(['bootout', `gui/${process.getuid()}/${LABEL}`], { quiet: true });
|
|
73
79
|
const out = launchctl(['bootstrap', `gui/${process.getuid()}`, plistPath()], { quiet: true });
|
|
74
80
|
launchctl(['enable', `gui/${process.getuid()}/${LABEL}`], { quiet: true });
|
package/src/tunnel.mjs
CHANGED
|
@@ -9,7 +9,10 @@ const NGROK_URL_RE = /https:\/\/[a-z0-9-]+\.ngrok(?:-free)?\.(?:app|io|dev)/i;
|
|
|
9
9
|
* cloudflared: QUIC (UDP/7844) is blocked on many networks, so we default to http2 over TCP/443.
|
|
10
10
|
* ngrok: an authtoken and an optional reserved domain give the same URL on every restart.
|
|
11
11
|
*/
|
|
12
|
-
export function startTunnel(
|
|
12
|
+
export function startTunnel(
|
|
13
|
+
cfg,
|
|
14
|
+
{ port, protocol = 'http2', bin = 'cloudflared', authtoken, domain, restarts = 20, delay = 2000 } = {},
|
|
15
|
+
) {
|
|
13
16
|
const target = `http://127.0.0.1:${port}`;
|
|
14
17
|
const isNgrok = /ngrok/i.test(bin);
|
|
15
18
|
const args = isNgrok
|
|
@@ -24,12 +27,13 @@ export function startTunnel(cfg, { port, protocol = 'http2', bin = 'cloudflared'
|
|
|
24
27
|
: ['tunnel', '--url', target, '--protocol', protocol];
|
|
25
28
|
const urlPattern = isNgrok ? NGROK_URL_RE : URL_RE;
|
|
26
29
|
|
|
27
|
-
|
|
28
|
-
|
|
30
|
+
let child = null;
|
|
29
31
|
let buffer = '';
|
|
30
32
|
let settled = false;
|
|
33
|
+
let attempt = 0;
|
|
31
34
|
let resolveUrl;
|
|
32
35
|
let rejectUrl;
|
|
36
|
+
const listeners = new Set();
|
|
33
37
|
|
|
34
38
|
const urlPromise = new Promise((resolve, reject) => {
|
|
35
39
|
resolveUrl = resolve;
|
|
@@ -37,33 +41,48 @@ export function startTunnel(cfg, { port, protocol = 'http2', bin = 'cloudflared'
|
|
|
37
41
|
});
|
|
38
42
|
|
|
39
43
|
const onChunk = (chunk) => {
|
|
40
|
-
|
|
41
|
-
buffer += text;
|
|
44
|
+
buffer += chunk.toString();
|
|
42
45
|
const match = urlPattern.exec(buffer);
|
|
43
|
-
if (match
|
|
46
|
+
if (!match) return;
|
|
47
|
+
if (!settled) {
|
|
44
48
|
settled = true;
|
|
45
49
|
resolveUrl(match[0]);
|
|
46
50
|
}
|
|
51
|
+
for (const fn of listeners) fn(match[0]);
|
|
47
52
|
};
|
|
48
53
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
54
|
+
const spawnChild = () => {
|
|
55
|
+
child = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
|
|
56
|
+
buffer = '';
|
|
57
|
+
child.stdout.on('data', onChunk);
|
|
58
|
+
child.stderr.on('data', onChunk);
|
|
59
|
+
child.on('error', (e) => {
|
|
60
|
+
if (!settled) {
|
|
61
|
+
settled = true;
|
|
62
|
+
rejectUrl(new Error(`cannot start ${bin}: ${e.message}`));
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
child.on('exit', (code) => {
|
|
66
|
+
if (!settled) {
|
|
67
|
+
settled = true;
|
|
68
|
+
rejectUrl(new Error(`${bin} exited with code ${code} before the tunnel URL appeared`));
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
if (attempt++ < restarts) {
|
|
72
|
+
setTimeout(spawnChild, delay);
|
|
73
|
+
}
|
|
74
|
+
});
|
|
75
|
+
};
|
|
58
76
|
|
|
59
|
-
|
|
60
|
-
if (!settled) {
|
|
61
|
-
settled = true;
|
|
62
|
-
rejectUrl(new Error(`${bin} exited with code ${code} before the tunnel URL appeared`));
|
|
63
|
-
}
|
|
64
|
-
});
|
|
77
|
+
spawnChild();
|
|
65
78
|
|
|
66
|
-
return {
|
|
79
|
+
return {
|
|
80
|
+
child,
|
|
81
|
+
urlPromise,
|
|
82
|
+
args,
|
|
83
|
+
target,
|
|
84
|
+
onUrl: (fn) => listeners.add(fn),
|
|
85
|
+
};
|
|
67
86
|
}
|
|
68
87
|
|
|
69
88
|
export function isPortOpen(port, host = '127.0.0.1', timeout = 700) {
|