esque-bridge 0.6.10 → 0.6.12
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 +61 -1
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -92,6 +92,12 @@ const BIN_OVERRIDE = argv.bin || null;
|
|
|
92
92
|
const LT_SUBDOMAIN = argv.subdomain || process.env.LT_SUBDOMAIN || undefined;
|
|
93
93
|
const SESSIONS_FILE = path.join(os.homedir(), '.esque-bridge-sessions.json');
|
|
94
94
|
const SECRET_FILE = path.join(os.homedir(), '.esque-bridge-secret');
|
|
95
|
+
// Where to announce the current tunnel URL so the phone can rediscover it
|
|
96
|
+
// after a restart rotates the cloudflared URL. Best-effort — QR pairing works
|
|
97
|
+
// without it. Override for self-hosters via --backend / ESQUE_BACKEND_URL.
|
|
98
|
+
const BACKEND_URL = String(
|
|
99
|
+
argv.backend || process.env.ESQUE_BACKEND_URL || 'https://esque-backend-production.up.railway.app',
|
|
100
|
+
).replace(/\/+$/, '');
|
|
95
101
|
|
|
96
102
|
// Pairing secret. Persisted across restarts so a routine bridge restart
|
|
97
103
|
// doesn't silently invalidate the phone's pairing (the #1 real-world
|
|
@@ -469,6 +475,26 @@ function runAgent(prompt, esqueSessionId) {
|
|
|
469
475
|
clearTimers();
|
|
470
476
|
if (truncated) return;
|
|
471
477
|
if (code !== 0) {
|
|
478
|
+
// A non-zero exit usually still carries the REAL reason. `claude
|
|
479
|
+
// --print --output-format json` emits a result object with the error
|
|
480
|
+
// (API overload, usage limit, a refusal) on stdout even when it exits
|
|
481
|
+
// 1 — surfacing "exited 1" instead throws that away. Parse stdout; if
|
|
482
|
+
// there's a real message, return it as an error reply (so the phone
|
|
483
|
+
// shows the cause and the user can just resend) rather than a bare
|
|
484
|
+
// exit code. Fall back to exit-code + stderr only when stdout is empty.
|
|
485
|
+
let parsedErr = null;
|
|
486
|
+
try {
|
|
487
|
+
const p = adapter.parseOutput(stdout);
|
|
488
|
+
if (p && p.text && p.text !== '(no output)' && p.text !== '(claude returned no text)') {
|
|
489
|
+
parsedErr = p;
|
|
490
|
+
}
|
|
491
|
+
} catch {
|
|
492
|
+
/* unparseable stdout — fall through to the generic message */
|
|
493
|
+
}
|
|
494
|
+
if (parsedErr) {
|
|
495
|
+
resolveOnce({ ...parsedErr, isError: true });
|
|
496
|
+
return;
|
|
497
|
+
}
|
|
472
498
|
rejectOnce(
|
|
473
499
|
new Error(
|
|
474
500
|
`${adapter.label} exited ${code}${stderr ? `: ${stderr.trim().slice(0, 500)}` : ''}`,
|
|
@@ -1457,21 +1483,55 @@ async function main() {
|
|
|
1457
1483
|
// after a restart (the app asks GET /preview for the fresh URL).
|
|
1458
1484
|
if (loadPreviewCmd()) revivePreview();
|
|
1459
1485
|
|
|
1486
|
+
// Announce this tunnel URL to the backend, keyed by the persistent secret,
|
|
1487
|
+
// so the phone can rediscover it after a restart rotates the URL — then
|
|
1488
|
+
// refresh on a heartbeat so the mapping never expires while we're alive.
|
|
1489
|
+
// Entirely best-effort: a down/unreachable backend never affects pairing or
|
|
1490
|
+
// prompts (the QR already carries this URL for the current session).
|
|
1491
|
+
const announce = async () => {
|
|
1492
|
+
try {
|
|
1493
|
+
await fetch(`${BACKEND_URL}/v1/bridge/announce`, {
|
|
1494
|
+
method: 'POST',
|
|
1495
|
+
headers: { 'content-type': 'application/json' },
|
|
1496
|
+
body: JSON.stringify({ secret: PAIRING_SECRET, url: tunnel.url, agent: AGENT_TYPE }),
|
|
1497
|
+
signal: AbortSignal.timeout(8000),
|
|
1498
|
+
});
|
|
1499
|
+
} catch {
|
|
1500
|
+
/* best-effort rendezvous */
|
|
1501
|
+
}
|
|
1502
|
+
};
|
|
1503
|
+
announce();
|
|
1504
|
+
const announceTimer = setInterval(announce, 45_000);
|
|
1505
|
+
announceTimer.unref?.();
|
|
1506
|
+
|
|
1460
1507
|
let shuttingDown = false;
|
|
1461
1508
|
const shutdown = (signal) => {
|
|
1462
1509
|
shuttingDown = true;
|
|
1463
1510
|
console.log(`\n Received ${signal} — closing tunnel…`);
|
|
1511
|
+
clearInterval(announceTimer);
|
|
1464
1512
|
// The agent runs detached in its own process group, so the terminal's
|
|
1465
1513
|
// Ctrl-C does NOT reach it — stop it explicitly or it keeps editing
|
|
1466
1514
|
// files with no bridge attached.
|
|
1467
1515
|
try { activeAgentKill && activeAgentKill('SIGTERM'); } catch { /* already gone */ }
|
|
1468
1516
|
killPreview();
|
|
1517
|
+
// Best-effort deregister so the phone learns we're down right away rather
|
|
1518
|
+
// than after the rendezvous TTL. Fire-and-forget — don't delay exit on it.
|
|
1519
|
+
try {
|
|
1520
|
+
fetch(`${BACKEND_URL}/v1/bridge/forget`, {
|
|
1521
|
+
method: 'POST',
|
|
1522
|
+
headers: { 'content-type': 'application/json' },
|
|
1523
|
+
body: JSON.stringify({ secret: PAIRING_SECRET }),
|
|
1524
|
+
signal: AbortSignal.timeout(2000),
|
|
1525
|
+
}).catch(() => undefined);
|
|
1526
|
+
} catch { /* ignore */ }
|
|
1469
1527
|
try {
|
|
1470
1528
|
tunnel.close();
|
|
1471
1529
|
} catch {
|
|
1472
1530
|
/* tunnel may already be closed */
|
|
1473
1531
|
}
|
|
1474
|
-
|
|
1532
|
+
// Give the forget() a brief moment to flush, then exit regardless.
|
|
1533
|
+
setTimeout(() => process.exit(0), 250);
|
|
1534
|
+
return;
|
|
1475
1535
|
};
|
|
1476
1536
|
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
1477
1537
|
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esque-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.12",
|
|
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"
|