arena-bridge 0.6.5 → 0.6.6

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 CHANGED
@@ -4,6 +4,13 @@ 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.6
8
+
9
+ ### Fixed
10
+ - The tunnel is now supervised: when the tunnel binary dies, it is started again automatically
11
+ and the new public URL is printed and saved to the config, instead of leaving a dead tunnel
12
+ behind (the bridge used to keep running while the public address had stopped working).
13
+
7
14
  ## 0.6.5
8
15
 
9
16
  ### 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-112%20passed-brightgreen.svg?style=flat-square" alt="tests">
15
+ <img src="https://img.shields.io/badge/tests-114%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
  доменом публичный адрес остаётся постоянным, и промпт агенту не нужно копировать заново:
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "arena-bridge",
3
- "version": "0.6.5",
3
+ "version": "0.6.6",
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<-----');
package/src/direct.mjs CHANGED
@@ -29,7 +29,7 @@ export function createDirectServer(cfg, engine) {
29
29
  ok: true,
30
30
  mode: 'direct',
31
31
  device: cfg.device,
32
- version: '0.6.5',
32
+ version: '0.6.6',
33
33
  uptime_sec: Math.round(process.uptime()),
34
34
  });
35
35
  }
package/src/hub.mjs CHANGED
@@ -90,7 +90,7 @@ export function createHubServer(cfg) {
90
90
  return sendJson(res, 200, {
91
91
  ok: true,
92
92
  mode: 'hub',
93
- version: '0.6.5',
93
+ version: '0.6.6',
94
94
  devices: [...devices.keys()],
95
95
  });
96
96
  }
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.5' },
51
+ serverInfo: { name: 'arena-bridge', version: '0.6.6' },
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/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(cfg, { port, protocol = 'http2', bin = 'cloudflared', authtoken, domain } = {}) {
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
- const child = spawn(bin, args, { stdio: ['ignore', 'pipe', 'pipe'] });
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
- const text = chunk.toString();
41
- buffer += text;
44
+ buffer += chunk.toString();
42
45
  const match = urlPattern.exec(buffer);
43
- if (match && !settled) {
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
- child.stdout.on('data', onChunk);
50
- child.stderr.on('data', onChunk);
51
-
52
- child.on('error', (e) => {
53
- if (!settled) {
54
- settled = true;
55
- rejectUrl(new Error(`cannot start ${bin}: ${e.message}`));
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
- child.on('exit', (code) => {
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 { child, urlPromise, args, target };
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) {