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.
Files changed (2) hide show
  1. package/index.js +67 -6
  2. 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. The error tells the user (and the agent,
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 && result.text) result.text = await applyPreview(result.text);
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.2",
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"