esque-bridge 0.5.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.
- package/index.js +73 -8
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -27,6 +27,10 @@ const fs = require('fs');
|
|
|
27
27
|
const path = require('path');
|
|
28
28
|
const os = require('os');
|
|
29
29
|
|
|
30
|
+
// Windows has no POSIX process groups or `.cmd`-aware spawn, so several
|
|
31
|
+
// process-management paths below branch on this.
|
|
32
|
+
const isWindows = process.platform === 'win32';
|
|
33
|
+
|
|
30
34
|
// --- Config ---------------------------------------------------------------
|
|
31
35
|
|
|
32
36
|
const argv = parseArgs(process.argv.slice(2));
|
|
@@ -331,14 +335,19 @@ function runAgent(prompt, esqueSessionId) {
|
|
|
331
335
|
usesStdin = !argv.some((a) => a.includes(prompt));
|
|
332
336
|
}
|
|
333
337
|
|
|
334
|
-
//
|
|
335
|
-
// agent
|
|
336
|
-
//
|
|
338
|
+
// POSIX: detach into its own process group so we can kill the WHOLE tree
|
|
339
|
+
// (the agent spawns subprocesses) on timeout instead of orphaning zombies.
|
|
340
|
+
// Windows: no detach (it would pop a console window) — we kill the tree
|
|
341
|
+
// via `taskkill /T` instead; and `shell: true` so spawn can resolve the
|
|
342
|
+
// `.cmd` shims npm installs global bins as (claude.cmd / codex.cmd). Our
|
|
343
|
+
// built-in adapters pass fixed flag args (the prompt rides via stdin), so
|
|
344
|
+
// there's no shell-injection surface here.
|
|
337
345
|
const child = spawn(bin, argv, {
|
|
338
346
|
cwd: WORKDIR,
|
|
339
347
|
env: process.env,
|
|
340
348
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
341
|
-
detached:
|
|
349
|
+
detached: !isWindows,
|
|
350
|
+
shell: isWindows,
|
|
342
351
|
});
|
|
343
352
|
|
|
344
353
|
const MAX_BUF = 16 * 1024 * 1024; // hard cap so a runaway agent can't OOM the bridge
|
|
@@ -356,6 +365,22 @@ function runAgent(prompt, esqueSessionId) {
|
|
|
356
365
|
const rejectOnce = settle(reject);
|
|
357
366
|
|
|
358
367
|
const killTree = (signal) => {
|
|
368
|
+
if (isWindows) {
|
|
369
|
+
// No process groups on Windows; force-kill the whole tree by PID.
|
|
370
|
+
// Signals don't map, so SIGTERM/SIGKILL both become a /F force-kill.
|
|
371
|
+
try {
|
|
372
|
+
spawn('taskkill', ['/pid', String(child.pid), '/T', '/F'], {
|
|
373
|
+
stdio: 'ignore',
|
|
374
|
+
});
|
|
375
|
+
} catch {
|
|
376
|
+
try {
|
|
377
|
+
child.kill();
|
|
378
|
+
} catch {
|
|
379
|
+
/* already gone */
|
|
380
|
+
}
|
|
381
|
+
}
|
|
382
|
+
return;
|
|
383
|
+
}
|
|
359
384
|
try {
|
|
360
385
|
process.kill(-child.pid, signal);
|
|
361
386
|
} catch {
|
|
@@ -753,6 +778,43 @@ function confirmWorkdir(dir) {
|
|
|
753
778
|
});
|
|
754
779
|
}
|
|
755
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
|
+
|
|
756
818
|
async function main() {
|
|
757
819
|
if (!fs.existsSync(WORKDIR) || !fs.statSync(WORKDIR).isDirectory()) {
|
|
758
820
|
console.error(`workdir does not exist: ${WORKDIR}`);
|
|
@@ -787,15 +849,18 @@ async function main() {
|
|
|
787
849
|
process.exit(1);
|
|
788
850
|
}
|
|
789
851
|
|
|
790
|
-
|
|
852
|
+
const { port: boundPort } = await listenOnFreePort(app, PORT);
|
|
853
|
+
if (boundPort !== PORT) {
|
|
854
|
+
console.log(` ✓ Using port ${boundPort} instead (${PORT} was taken).`);
|
|
855
|
+
}
|
|
791
856
|
|
|
792
857
|
let tunnel;
|
|
793
858
|
try {
|
|
794
|
-
tunnel = await localtunnel({ port:
|
|
859
|
+
tunnel = await localtunnel({ port: boundPort, subdomain: LT_SUBDOMAIN });
|
|
795
860
|
} catch (err) {
|
|
796
861
|
console.error('Failed to open localtunnel:', err.message);
|
|
797
862
|
console.error('If localtunnel.me is blocked, try cloudflared:');
|
|
798
|
-
console.error(` cloudflared tunnel --url http://localhost:${
|
|
863
|
+
console.error(` cloudflared tunnel --url http://localhost:${boundPort}`);
|
|
799
864
|
process.exit(1);
|
|
800
865
|
}
|
|
801
866
|
|
|
@@ -811,7 +876,7 @@ async function main() {
|
|
|
811
876
|
qrcode.generate(pairUrl, { small: true });
|
|
812
877
|
console.log('');
|
|
813
878
|
console.log(` Agent ${adapter.label} (${AGENT_TYPE})`);
|
|
814
|
-
console.log(` Local http://localhost:${
|
|
879
|
+
console.log(` Local http://localhost:${boundPort}`);
|
|
815
880
|
console.log(` Tunnel ${tunnel.url}`);
|
|
816
881
|
console.log(` Workdir ${WORKDIR}`);
|
|
817
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.
|
|
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
|
},
|