esque-bridge 0.6.9 → 0.6.11
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 +93 -0
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -469,6 +469,26 @@ function runAgent(prompt, esqueSessionId) {
|
|
|
469
469
|
clearTimers();
|
|
470
470
|
if (truncated) return;
|
|
471
471
|
if (code !== 0) {
|
|
472
|
+
// A non-zero exit usually still carries the REAL reason. `claude
|
|
473
|
+
// --print --output-format json` emits a result object with the error
|
|
474
|
+
// (API overload, usage limit, a refusal) on stdout even when it exits
|
|
475
|
+
// 1 — surfacing "exited 1" instead throws that away. Parse stdout; if
|
|
476
|
+
// there's a real message, return it as an error reply (so the phone
|
|
477
|
+
// shows the cause and the user can just resend) rather than a bare
|
|
478
|
+
// exit code. Fall back to exit-code + stderr only when stdout is empty.
|
|
479
|
+
let parsedErr = null;
|
|
480
|
+
try {
|
|
481
|
+
const p = adapter.parseOutput(stdout);
|
|
482
|
+
if (p && p.text && p.text !== '(no output)' && p.text !== '(claude returned no text)') {
|
|
483
|
+
parsedErr = p;
|
|
484
|
+
}
|
|
485
|
+
} catch {
|
|
486
|
+
/* unparseable stdout — fall through to the generic message */
|
|
487
|
+
}
|
|
488
|
+
if (parsedErr) {
|
|
489
|
+
resolveOnce({ ...parsedErr, isError: true });
|
|
490
|
+
return;
|
|
491
|
+
}
|
|
472
492
|
rejectOnce(
|
|
473
493
|
new Error(
|
|
474
494
|
`${adapter.label} exited ${code}${stderr ? `: ${stderr.trim().slice(0, 500)}` : ''}`,
|
|
@@ -611,6 +631,32 @@ const { execSync } = require('child_process');
|
|
|
611
631
|
|
|
612
632
|
let preview = null; // { port, proc, kind, url, tunnel, tunnelProc }
|
|
613
633
|
|
|
634
|
+
// ── Preview persistence ───────────────────────────────────────────────
|
|
635
|
+
// Tunnels die with the laptop lid / a bridge restart, and the URL embedded in
|
|
636
|
+
// the phone's chat history can never come back (quick-tunnel URLs are random).
|
|
637
|
+
// Remember each workdir's last preview COMMAND so a restarted bridge can
|
|
638
|
+
// revive the dev server + mint a fresh tunnel — the phone then resolves the
|
|
639
|
+
// current URL via GET /preview instead of trusting the dead one.
|
|
640
|
+
const PREVIEWS_FILE = path.join(os.homedir(), '.esque-bridge-previews.json');
|
|
641
|
+
function savePreviewCmd(cmd, port) {
|
|
642
|
+
try {
|
|
643
|
+
let all = {};
|
|
644
|
+
try { all = JSON.parse(fs.readFileSync(PREVIEWS_FILE, 'utf8')); } catch { /* first run */ }
|
|
645
|
+
all[WORKDIR] = { cmd, port };
|
|
646
|
+
fs.writeFileSync(PREVIEWS_FILE, JSON.stringify(all, null, 2));
|
|
647
|
+
} catch (err) {
|
|
648
|
+
console.warn('[preview] could not persist preview command:', err.message);
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
function loadPreviewCmd() {
|
|
652
|
+
try {
|
|
653
|
+
const all = JSON.parse(fs.readFileSync(PREVIEWS_FILE, 'utf8'));
|
|
654
|
+
const e = all[WORKDIR];
|
|
655
|
+
if (e && typeof e.cmd === 'string' && Number.isInteger(e.port)) return e;
|
|
656
|
+
} catch { /* none saved */ }
|
|
657
|
+
return null;
|
|
658
|
+
}
|
|
659
|
+
|
|
614
660
|
// Kill a spawned command's WHOLE process tree. `proc.kill()` alone signals
|
|
615
661
|
// only the immediate child (`sh`/`npm`) — npm does not reliably forward
|
|
616
662
|
// signals, so the actual dev server survives, keeps the port, and the next
|
|
@@ -902,6 +948,9 @@ async function startPreview(cmd, port) {
|
|
|
902
948
|
return { url: null, buildError: deps.error };
|
|
903
949
|
}
|
|
904
950
|
console.log(`[preview] starting: ${cmd} (port ${port})`);
|
|
951
|
+
// Remember the command now (not on success): a revival attempt after a
|
|
952
|
+
// mid-start crash should still know what to run.
|
|
953
|
+
savePreviewCmd(cmd, port);
|
|
905
954
|
// Own process group (POSIX) so killPreview can take down the WHOLE tree —
|
|
906
955
|
// `npm run dev` does not forward signals to the real dev server. Windows
|
|
907
956
|
// has no `sh`; route through cmd.exe and kill via taskkill /T.
|
|
@@ -1202,6 +1251,45 @@ function resultHandler(req, res) {
|
|
|
1202
1251
|
res.json({ status: job.status, text: job.text });
|
|
1203
1252
|
}
|
|
1204
1253
|
|
|
1254
|
+
// Single-flight wrapper for reviving the saved preview: GET /preview and the
|
|
1255
|
+
// boot-time revival can race; both should await the same start, never two.
|
|
1256
|
+
let previewStartPromise = null;
|
|
1257
|
+
function revivePreview() {
|
|
1258
|
+
if (preview && preview.url) return Promise.resolve({ url: preview.url, buildError: preview.buildError });
|
|
1259
|
+
if (!previewStartPromise) {
|
|
1260
|
+
const saved = loadPreviewCmd();
|
|
1261
|
+
if (!saved) return Promise.resolve(null);
|
|
1262
|
+
console.log('[preview] reviving last preview for this folder…');
|
|
1263
|
+
previewStartPromise = startPreview(saved.cmd, saved.port)
|
|
1264
|
+
.catch((e) => { console.error('[preview] revive failed:', e.message); return null; })
|
|
1265
|
+
.finally(() => { previewStartPromise = null; });
|
|
1266
|
+
}
|
|
1267
|
+
return previewStartPromise;
|
|
1268
|
+
}
|
|
1269
|
+
|
|
1270
|
+
// Current-preview resolver. The phone calls this when a stored preview URL
|
|
1271
|
+
// goes dead (laptop slept → tunnel gone): if the dev server isn't running,
|
|
1272
|
+
// we restart the last one for this folder and answer with the FRESH url.
|
|
1273
|
+
// Response: { ok, url?, port?, building?, buildError? } — 404 when this
|
|
1274
|
+
// folder has never had a preview.
|
|
1275
|
+
app.get('/preview', requireAuth, async (_req, res) => {
|
|
1276
|
+
if (preview && preview.url) {
|
|
1277
|
+
return res.json({ ok: true, url: preview.url, port: preview.port, buildError: preview.buildError ?? null });
|
|
1278
|
+
}
|
|
1279
|
+
if (!loadPreviewCmd() && !previewStartPromise) {
|
|
1280
|
+
return res.status(404).json({ ok: false, error: 'no_preview_for_this_folder' });
|
|
1281
|
+
}
|
|
1282
|
+
const result = await revivePreview();
|
|
1283
|
+
if (result && result.url) {
|
|
1284
|
+
return res.json({ ok: true, url: result.url, port: preview?.port ?? null, buildError: result.buildError ?? null });
|
|
1285
|
+
}
|
|
1286
|
+
return res.status(503).json({
|
|
1287
|
+
ok: false,
|
|
1288
|
+
error: 'preview_failed_to_start',
|
|
1289
|
+
buildError: (result && result.buildError) || null,
|
|
1290
|
+
});
|
|
1291
|
+
});
|
|
1292
|
+
|
|
1205
1293
|
app.post('/execute', requireAuth, executeHandler);
|
|
1206
1294
|
app.post('/', requireAuth, executeHandler);
|
|
1207
1295
|
app.get('/result/:id', requireAuth, resultHandler);
|
|
@@ -1384,6 +1472,11 @@ async function main() {
|
|
|
1384
1472
|
console.log(' Press Ctrl-C to stop.');
|
|
1385
1473
|
console.log('━'.repeat(68));
|
|
1386
1474
|
|
|
1475
|
+
// Revive this folder's last preview in the background, so a "Preview
|
|
1476
|
+
// Build" pill already sitting in the phone's chat resolves again right
|
|
1477
|
+
// after a restart (the app asks GET /preview for the fresh URL).
|
|
1478
|
+
if (loadPreviewCmd()) revivePreview();
|
|
1479
|
+
|
|
1387
1480
|
let shuttingDown = false;
|
|
1388
1481
|
const shutdown = (signal) => {
|
|
1389
1482
|
shuttingDown = true;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esque-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.11",
|
|
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"
|