esque-bridge 0.6.7 → 0.6.8

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.
Files changed (2) hide show
  1. package/index.js +56 -8
  2. package/package.json +1 -1
package/index.js CHANGED
@@ -1097,6 +1097,47 @@ function listenOnFreePort(app, startPort, maxTries = 25) {
1097
1097
  });
1098
1098
  }
1099
1099
 
1100
+ // Open the public pairing tunnel. Prefers cloudflared (reliable, no
1101
+ // interstitial), and falls back to localtunnel with a HARD timeout — otherwise
1102
+ // a slow/down localtunnel.me hangs the whole bridge before the QR ever prints
1103
+ // (exactly the "nothing happens after y" failure). Resolves a uniform handle
1104
+ // { url, kind, close(), onClose(cb) }, or null if both paths fail.
1105
+ function openPairTunnel(port) {
1106
+ return new Promise((resolve) => {
1107
+ let settled = false;
1108
+ const done = (val) => { if (!settled) { settled = true; resolve(val); } };
1109
+
1110
+ const tryLocaltunnel = () => {
1111
+ const timer = setTimeout(() => done(null), 20000);
1112
+ localtunnel({ port, subdomain: LT_SUBDOMAIN })
1113
+ .then((t) => {
1114
+ clearTimeout(timer);
1115
+ if (settled) { try { t.close(); } catch { /* already gone */ } return; }
1116
+ done({ url: t.url, kind: 'localtunnel', close: () => { try { t.close(); } catch { /* already gone */ } }, onClose: (cb) => t.on('close', cb) });
1117
+ })
1118
+ .catch(() => { clearTimeout(timer); done(null); });
1119
+ };
1120
+
1121
+ if (!hasCloudflared()) { tryLocaltunnel(); return; }
1122
+
1123
+ const proc = spawn('cloudflared', ['tunnel', '--url', `http://localhost:${port}`], { stdio: ['ignore', 'pipe', 'pipe'] });
1124
+ let gotUrl = false, fellBack = false;
1125
+ const scan = (d) => {
1126
+ const m = String(d).match(/https:\/\/[a-z0-9-]+\.trycloudflare\.com/i);
1127
+ if (m && !gotUrl) {
1128
+ gotUrl = true;
1129
+ done({ url: m[0], kind: 'cloudflared', close: () => { try { proc.kill(); } catch { /* already gone */ } }, onClose: (cb) => proc.on('exit', cb) });
1130
+ }
1131
+ };
1132
+ proc.stdout.on('data', scan);
1133
+ proc.stderr.on('data', scan);
1134
+ // cloudflared died before emitting a URL, or took too long → fall back once.
1135
+ const fallback = () => { if (!gotUrl && !fellBack) { fellBack = true; try { proc.kill(); } catch { /* already gone */ } tryLocaltunnel(); } };
1136
+ proc.on('exit', fallback);
1137
+ setTimeout(fallback, 25000);
1138
+ });
1139
+ }
1140
+
1100
1141
  async function main() {
1101
1142
  if (!fs.existsSync(WORKDIR) || !fs.statSync(WORKDIR).isDirectory()) {
1102
1143
  console.error(`workdir does not exist: ${WORKDIR}`);
@@ -1136,15 +1177,17 @@ async function main() {
1136
1177
  console.log(` ✓ Using port ${boundPort} instead (${PORT} was taken).`);
1137
1178
  }
1138
1179
 
1139
- let tunnel;
1140
- try {
1141
- tunnel = await localtunnel({ port: boundPort, subdomain: LT_SUBDOMAIN });
1142
- } catch (err) {
1143
- console.error('Failed to open localtunnel:', err.message);
1144
- console.error('If localtunnel.me is blocked, try cloudflared:');
1145
- console.error(` cloudflared tunnel --url http://localhost:${boundPort}`);
1180
+ const tunnel = await openPairTunnel(boundPort);
1181
+ if (!tunnel) {
1182
+ console.error('\n ✗ Could not open a public tunnel.');
1183
+ console.error(' cloudflared and localtunnel.me both failed to respond.');
1184
+ console.error(' Check your internet connection and retry. For the reliable path:');
1185
+ console.error(' brew install cloudflared');
1146
1186
  process.exit(1);
1147
1187
  }
1188
+ if (tunnel.kind === 'localtunnel') {
1189
+ console.log(' (Using localtunnel — install cloudflared for a faster, steadier tunnel: brew install cloudflared)');
1190
+ }
1148
1191
 
1149
1192
  const pairUrl = `esque://pair?url=${encodeURIComponent(tunnel.url)}&secret=${PAIRING_SECRET}&agent=${AGENT_TYPE}`;
1150
1193
 
@@ -1169,7 +1212,9 @@ async function main() {
1169
1212
  console.log(' Press Ctrl-C to stop.');
1170
1213
  console.log('━'.repeat(68));
1171
1214
 
1215
+ let shuttingDown = false;
1172
1216
  const shutdown = (signal) => {
1217
+ shuttingDown = true;
1173
1218
  console.log(`\n Received ${signal} — closing tunnel…`);
1174
1219
  killPreview();
1175
1220
  try {
@@ -1182,7 +1227,10 @@ async function main() {
1182
1227
  process.on('SIGINT', () => shutdown('SIGINT'));
1183
1228
  process.on('SIGTERM', () => shutdown('SIGTERM'));
1184
1229
 
1185
- tunnel.on('close', () => {
1230
+ // Detect an unexpected tunnel drop (but stay quiet during our own shutdown,
1231
+ // since closing the tunnel also fires this).
1232
+ tunnel.onClose(() => {
1233
+ if (shuttingDown) return;
1186
1234
  console.error('\nTunnel closed unexpectedly. Restart `esque-bridge`.');
1187
1235
  process.exit(1);
1188
1236
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "esque-bridge",
3
- "version": "0.6.7",
3
+ "version": "0.6.8",
4
4
  "description": "Desktop-side receiver for the Esque Agent mobile app. Pairs your phone with a local coding-agent CLI (Claude Code, Codex, Aider, or any custom command) via a tunnel + QR code, so prompts run through your subscription instead of per-token API billing.",
5
5
  "bin": {
6
6
  "esque-bridge": "index.js"