esque-bridge 0.6.0 → 0.6.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.
Files changed (2) hide show
  1. package/index.js +44 -4
  2. package/package.json +2 -2
package/index.js CHANGED
@@ -778,6 +778,43 @@ function confirmWorkdir(dir) {
778
778
  });
779
779
  }
780
780
 
781
+ // Bind the local HTTP server to the first free port at/after `startPort`.
782
+ // EADDRINUSE on the default port almost always means a previous esque-bridge
783
+ // is still running (or another app grabbed 3030). Rather than crash with a raw
784
+ // Node stack trace, we step to the next port and tell the user. The pairing
785
+ // URL travels over the tunnel, so the exact local port doesn't matter to the
786
+ // app. Returns { server, port } for the port we actually bound.
787
+ function listenOnFreePort(app, startPort, maxTries = 25) {
788
+ return new Promise((resolve, reject) => {
789
+ let attempts = 0;
790
+ const attempt = (port) => {
791
+ const server = app.listen(port);
792
+ const onError = (err) => {
793
+ server.removeListener('listening', onListening);
794
+ if (err && err.code === 'EADDRINUSE' && attempts < maxTries) {
795
+ if (attempts === 0) {
796
+ console.error(
797
+ `\n Port ${startPort} is busy — another Esque bridge may already be running.`,
798
+ );
799
+ console.error(' Stepping to the next free port…');
800
+ }
801
+ attempts += 1;
802
+ attempt(port + 1);
803
+ } else {
804
+ reject(err);
805
+ }
806
+ };
807
+ const onListening = () => {
808
+ server.removeListener('error', onError);
809
+ resolve({ server, port });
810
+ };
811
+ server.once('error', onError);
812
+ server.once('listening', onListening);
813
+ };
814
+ attempt(startPort);
815
+ });
816
+ }
817
+
781
818
  async function main() {
782
819
  if (!fs.existsSync(WORKDIR) || !fs.statSync(WORKDIR).isDirectory()) {
783
820
  console.error(`workdir does not exist: ${WORKDIR}`);
@@ -812,15 +849,18 @@ async function main() {
812
849
  process.exit(1);
813
850
  }
814
851
 
815
- await new Promise((resolve) => app.listen(PORT, resolve));
852
+ const { port: boundPort } = await listenOnFreePort(app, PORT);
853
+ if (boundPort !== PORT) {
854
+ console.log(` ✓ Using port ${boundPort} instead (${PORT} was taken).`);
855
+ }
816
856
 
817
857
  let tunnel;
818
858
  try {
819
- tunnel = await localtunnel({ port: PORT, subdomain: LT_SUBDOMAIN });
859
+ tunnel = await localtunnel({ port: boundPort, subdomain: LT_SUBDOMAIN });
820
860
  } catch (err) {
821
861
  console.error('Failed to open localtunnel:', err.message);
822
862
  console.error('If localtunnel.me is blocked, try cloudflared:');
823
- console.error(` cloudflared tunnel --url http://localhost:${PORT}`);
863
+ console.error(` cloudflared tunnel --url http://localhost:${boundPort}`);
824
864
  process.exit(1);
825
865
  }
826
866
 
@@ -836,7 +876,7 @@ async function main() {
836
876
  qrcode.generate(pairUrl, { small: true });
837
877
  console.log('');
838
878
  console.log(` Agent ${adapter.label} (${AGENT_TYPE})`);
839
- console.log(` Local http://localhost:${PORT}`);
879
+ console.log(` Local http://localhost:${boundPort}`);
840
880
  console.log(` Tunnel ${tunnel.url}`);
841
881
  console.log(` Workdir ${WORKDIR}`);
842
882
  console.log(` Binary ${AGENT_BIN ?? '(custom)'}`);
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "esque-bridge",
3
- "version": "0.6.0",
4
- "description": "Desktop-side receiver for the Esque Agent mobile app. Pairs your phone with a local coding-agent CLI (Claude Code, Aider, or any custom command) via a tunnel + QR code, so prompts run through your subscription instead of per-token API billing.",
3
+ "version": "0.6.1",
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"
7
7
  },