esque-bridge 0.6.2 → 0.6.4
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 +67 -6
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -709,22 +709,26 @@ const PREVIEW_RE = /^[ \t>*`-]*ESQUE_PREVIEW:\s*(.+?)\s*@\s*(\d+)\s*`?[ \t]*$/im
|
|
|
709
709
|
|
|
710
710
|
// If the agent's reply declares a preview, start it (bridge-owned) and
|
|
711
711
|
// swap the marker line for the public URL the phone can open.
|
|
712
|
+
// Returns { text, buildError }. `buildError` is the short failure summary when
|
|
713
|
+
// the declared preview started but its web bundle didn't compile (else null) —
|
|
714
|
+
// the caller uses it to drive the auto-fix loop.
|
|
712
715
|
async function applyPreview(text) {
|
|
713
|
-
if (!text) return text;
|
|
716
|
+
if (!text) return { text, buildError: null };
|
|
714
717
|
const m = text.match(PREVIEW_RE);
|
|
715
|
-
if (!m) return text;
|
|
718
|
+
if (!m) return { text, buildError: null };
|
|
716
719
|
const cmd = m[1].trim();
|
|
717
720
|
const port = Number(m[2]);
|
|
718
721
|
let result = null;
|
|
719
722
|
try { result = await startPreview(cmd, port); } catch (e) { console.error('[preview] error:', e.message); }
|
|
720
723
|
let replacement;
|
|
724
|
+
let buildError = null;
|
|
721
725
|
if (!result) {
|
|
722
726
|
replacement = `(Couldn't auto-start the preview — run \`${cmd}\` in ${WORKDIR} yourself.)`;
|
|
723
727
|
} else if (result.buildError) {
|
|
728
|
+
buildError = result.buildError;
|
|
724
729
|
// Surface the failure as plain text WITHOUT the URL — the app turns any
|
|
725
730
|
// URL in a message into a "Preview Build" pill, and we don't want a pill
|
|
726
|
-
// that just opens a blank page.
|
|
727
|
-
// who sees this reply) exactly what to fix.
|
|
731
|
+
// that just opens a blank page.
|
|
728
732
|
replacement =
|
|
729
733
|
`⚠️ Preview build failed — the web bundle didn't compile:\n\n` +
|
|
730
734
|
`${result.buildError}\n\n` +
|
|
@@ -732,12 +736,35 @@ async function applyPreview(text) {
|
|
|
732
736
|
} else {
|
|
733
737
|
replacement = `🔗 Live preview: ${result.url}`;
|
|
734
738
|
}
|
|
735
|
-
return text.replace(PREVIEW_RE, replacement);
|
|
739
|
+
return { text: text.replace(PREVIEW_RE, replacement), buildError };
|
|
736
740
|
}
|
|
737
741
|
|
|
738
742
|
async function runAgentWithPreview(prompt, sessionId) {
|
|
739
743
|
const result = await runAgent(prompt, sessionId);
|
|
740
|
-
if (result
|
|
744
|
+
if (!result || !result.text) return result;
|
|
745
|
+
|
|
746
|
+
const applied = await applyPreview(result.text);
|
|
747
|
+
result.text = applied.text;
|
|
748
|
+
|
|
749
|
+
// Auto-fix (one retry): the agent's own session history never saw our
|
|
750
|
+
// rewritten reply, so if the preview failed to build we hand the error back
|
|
751
|
+
// to the agent — same session (--resume), so it has full context — and let
|
|
752
|
+
// it fix the cause and re-emit the marker. Bounded at a single retry so a
|
|
753
|
+
// stubborn error can't loop.
|
|
754
|
+
if (applied.buildError) {
|
|
755
|
+
console.log('[preview] build failed — handing the error back to the agent (1 retry)');
|
|
756
|
+
const fixPrompt =
|
|
757
|
+
`The live preview you just started failed to build with this error:\n\n` +
|
|
758
|
+
`${applied.buildError}\n\n` +
|
|
759
|
+
`Fix the root cause in the code, then re-emit the ESQUE_PREVIEW marker on ` +
|
|
760
|
+
`its own line. Keep the explanation brief — make the fix and output the marker.`;
|
|
761
|
+
let fix = null;
|
|
762
|
+
try { fix = await runAgent(fixPrompt, sessionId); } catch (e) { console.error('[preview] auto-fix run failed:', e.message); }
|
|
763
|
+
if (fix && fix.text) {
|
|
764
|
+
const fixApplied = await applyPreview(fix.text);
|
|
765
|
+
result.text = `${applied.text}\n\n— Auto-fix attempt —\n${fixApplied.text}`;
|
|
766
|
+
}
|
|
767
|
+
}
|
|
741
768
|
return result;
|
|
742
769
|
}
|
|
743
770
|
|
|
@@ -765,10 +792,35 @@ function gcJobs() {
|
|
|
765
792
|
// never keeps the process alive on its own.
|
|
766
793
|
setInterval(gcJobs, 60_000).unref?.();
|
|
767
794
|
|
|
795
|
+
// Ping the phone via Expo's push service when a job finishes, so the user is
|
|
796
|
+
// notified even with the app backgrounded or closed (iOS suspends the phone's
|
|
797
|
+
// polling loop). The data payload mirrors the app's local-notification schema
|
|
798
|
+
// so a tapped push lands in the right session. No-op for non-Expo tokens.
|
|
799
|
+
async function sendExpoPush(token, kind, sessionId, title, message) {
|
|
800
|
+
if (!token || !String(token).startsWith('ExponentPushToken[')) return;
|
|
801
|
+
try {
|
|
802
|
+
await fetch('https://exp.host/--/api/v2/push/send', {
|
|
803
|
+
method: 'POST',
|
|
804
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
805
|
+
body: JSON.stringify({
|
|
806
|
+
to: token,
|
|
807
|
+
title,
|
|
808
|
+
body: String(message || '').replace(/\s+/g, ' ').trim().slice(0, 178) || 'Tap to review.',
|
|
809
|
+
sound: 'default',
|
|
810
|
+
priority: 'high',
|
|
811
|
+
data: { kind, sessionId },
|
|
812
|
+
}),
|
|
813
|
+
});
|
|
814
|
+
} catch (e) {
|
|
815
|
+
console.error('[push] send failed:', e.message);
|
|
816
|
+
}
|
|
817
|
+
}
|
|
818
|
+
|
|
768
819
|
async function executeHandler(req, res) {
|
|
769
820
|
const body = req.body || {};
|
|
770
821
|
const prompt = String(body.prompt || '');
|
|
771
822
|
const esqueSessionId = body.sessionId ?? null;
|
|
823
|
+
const pushToken = typeof body.pushToken === 'string' ? body.pushToken : null;
|
|
772
824
|
if (!prompt.trim()) {
|
|
773
825
|
return res.status(400).json({ text: 'Empty prompt.', status: 'blocked' });
|
|
774
826
|
}
|
|
@@ -799,6 +851,14 @@ async function executeHandler(req, res) {
|
|
|
799
851
|
console.log(
|
|
800
852
|
`[bridge] done job=${jobId} ${result.isError ? 'blocked' : 'finished'}`,
|
|
801
853
|
);
|
|
854
|
+
const ok = !result.isError;
|
|
855
|
+
sendExpoPush(
|
|
856
|
+
pushToken,
|
|
857
|
+
ok ? 'finished' : 'blocked',
|
|
858
|
+
esqueSessionId,
|
|
859
|
+
ok ? '✅ Step complete' : '⚠️ Agent needs you',
|
|
860
|
+
result.text,
|
|
861
|
+
);
|
|
802
862
|
})
|
|
803
863
|
.catch((err) => {
|
|
804
864
|
jobs.set(jobId, {
|
|
@@ -807,6 +867,7 @@ async function executeHandler(req, res) {
|
|
|
807
867
|
createdAt: Date.now(),
|
|
808
868
|
});
|
|
809
869
|
console.error(`[bridge] error job=${jobId}:`, err.message);
|
|
870
|
+
sendExpoPush(pushToken, 'blocked', esqueSessionId, '⚠️ Agent hit an error', err.message);
|
|
810
871
|
});
|
|
811
872
|
return;
|
|
812
873
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esque-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.4",
|
|
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"
|