esque-bridge 0.6.3 → 0.6.5
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 +56 -3
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -476,6 +476,24 @@ app.use((req, res, next) => {
|
|
|
476
476
|
next();
|
|
477
477
|
});
|
|
478
478
|
|
|
479
|
+
// Directory identity the app binds a project to. Lets the phone detect when a
|
|
480
|
+
// project is being re-pointed at a DIFFERENT folder, or scaffolded fresh onto
|
|
481
|
+
// a folder that already has files — the two silent-mismatch foot-guns.
|
|
482
|
+
function workdirInfo() {
|
|
483
|
+
let empty = false;
|
|
484
|
+
let git = false;
|
|
485
|
+
let entries = -1;
|
|
486
|
+
try {
|
|
487
|
+
const all = fs.readdirSync(WORKDIR).filter((f) => f !== '.DS_Store');
|
|
488
|
+
entries = all.length;
|
|
489
|
+
empty = entries === 0;
|
|
490
|
+
git = all.includes('.git');
|
|
491
|
+
} catch {
|
|
492
|
+
/* unreadable dir — report unknown via -1 */
|
|
493
|
+
}
|
|
494
|
+
return { workdir: WORKDIR, workdirName: path.basename(WORKDIR), empty, git, entries };
|
|
495
|
+
}
|
|
496
|
+
|
|
479
497
|
// Public health probe.
|
|
480
498
|
app.get('/', (_req, res) => {
|
|
481
499
|
res.json({
|
|
@@ -483,8 +501,8 @@ app.get('/', (_req, res) => {
|
|
|
483
501
|
service: 'esque-bridge',
|
|
484
502
|
agent: AGENT_TYPE,
|
|
485
503
|
agentLabel: adapter.label,
|
|
486
|
-
workdir: WORKDIR,
|
|
487
504
|
sessions: Object.keys(sessionMap[AGENT_TYPE] ?? {}).length,
|
|
505
|
+
...workdirInfo(),
|
|
488
506
|
});
|
|
489
507
|
});
|
|
490
508
|
|
|
@@ -492,12 +510,13 @@ app.get('/', (_req, res) => {
|
|
|
492
510
|
// the phone includes the pair secret we validate it and report back via
|
|
493
511
|
// `paired` so the app can verify the secret BEFORE showing a green
|
|
494
512
|
// "Paired" state. `paired` is true (match), false (wrong/stale secret), or
|
|
495
|
-
// null (no secret sent — older app builds; we can't say either way).
|
|
513
|
+
// null (no secret sent — older app builds; we can't say either way). We also
|
|
514
|
+
// return workdir identity so the app can warn on a folder mismatch.
|
|
496
515
|
app.post('/', (req, res, next) => {
|
|
497
516
|
if (req.body && req.body._probe === true) {
|
|
498
517
|
const provided = req.header('x-esque-pair') || req.body?.pairSecret;
|
|
499
518
|
const paired = provided == null ? null : provided === PAIRING_SECRET;
|
|
500
|
-
return res.json({ ok: true, service: 'esque-bridge', agent: AGENT_TYPE, paired });
|
|
519
|
+
return res.json({ ok: true, service: 'esque-bridge', agent: AGENT_TYPE, paired, ...workdirInfo() });
|
|
501
520
|
}
|
|
502
521
|
return next();
|
|
503
522
|
});
|
|
@@ -792,10 +811,35 @@ function gcJobs() {
|
|
|
792
811
|
// never keeps the process alive on its own.
|
|
793
812
|
setInterval(gcJobs, 60_000).unref?.();
|
|
794
813
|
|
|
814
|
+
// Ping the phone via Expo's push service when a job finishes, so the user is
|
|
815
|
+
// notified even with the app backgrounded or closed (iOS suspends the phone's
|
|
816
|
+
// polling loop). The data payload mirrors the app's local-notification schema
|
|
817
|
+
// so a tapped push lands in the right session. No-op for non-Expo tokens.
|
|
818
|
+
async function sendExpoPush(token, kind, sessionId, title, message) {
|
|
819
|
+
if (!token || !String(token).startsWith('ExponentPushToken[')) return;
|
|
820
|
+
try {
|
|
821
|
+
await fetch('https://exp.host/--/api/v2/push/send', {
|
|
822
|
+
method: 'POST',
|
|
823
|
+
headers: { 'Content-Type': 'application/json', Accept: 'application/json' },
|
|
824
|
+
body: JSON.stringify({
|
|
825
|
+
to: token,
|
|
826
|
+
title,
|
|
827
|
+
body: String(message || '').replace(/\s+/g, ' ').trim().slice(0, 178) || 'Tap to review.',
|
|
828
|
+
sound: 'default',
|
|
829
|
+
priority: 'high',
|
|
830
|
+
data: { kind, sessionId },
|
|
831
|
+
}),
|
|
832
|
+
});
|
|
833
|
+
} catch (e) {
|
|
834
|
+
console.error('[push] send failed:', e.message);
|
|
835
|
+
}
|
|
836
|
+
}
|
|
837
|
+
|
|
795
838
|
async function executeHandler(req, res) {
|
|
796
839
|
const body = req.body || {};
|
|
797
840
|
const prompt = String(body.prompt || '');
|
|
798
841
|
const esqueSessionId = body.sessionId ?? null;
|
|
842
|
+
const pushToken = typeof body.pushToken === 'string' ? body.pushToken : null;
|
|
799
843
|
if (!prompt.trim()) {
|
|
800
844
|
return res.status(400).json({ text: 'Empty prompt.', status: 'blocked' });
|
|
801
845
|
}
|
|
@@ -826,6 +870,14 @@ async function executeHandler(req, res) {
|
|
|
826
870
|
console.log(
|
|
827
871
|
`[bridge] done job=${jobId} ${result.isError ? 'blocked' : 'finished'}`,
|
|
828
872
|
);
|
|
873
|
+
const ok = !result.isError;
|
|
874
|
+
sendExpoPush(
|
|
875
|
+
pushToken,
|
|
876
|
+
ok ? 'finished' : 'blocked',
|
|
877
|
+
esqueSessionId,
|
|
878
|
+
ok ? '✅ Step complete' : '⚠️ Agent needs you',
|
|
879
|
+
result.text,
|
|
880
|
+
);
|
|
829
881
|
})
|
|
830
882
|
.catch((err) => {
|
|
831
883
|
jobs.set(jobId, {
|
|
@@ -834,6 +886,7 @@ async function executeHandler(req, res) {
|
|
|
834
886
|
createdAt: Date.now(),
|
|
835
887
|
});
|
|
836
888
|
console.error(`[bridge] error job=${jobId}:`, err.message);
|
|
889
|
+
sendExpoPush(pushToken, 'blocked', esqueSessionId, '⚠️ Agent hit an error', err.message);
|
|
837
890
|
});
|
|
838
891
|
return;
|
|
839
892
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esque-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.5",
|
|
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"
|