esque-bridge 0.6.13 → 0.6.15
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 +195 -19
- package/package.json +1 -1
package/index.js
CHANGED
|
@@ -87,6 +87,9 @@ const WORKDIR = path.resolve(
|
|
|
87
87
|
// scaffolds) finish. Override with --timeout.
|
|
88
88
|
const TIMEOUT_MS = Number(argv.timeout || 20 * 60 * 1000);
|
|
89
89
|
const AGENT_TYPE = String(argv.agent || process.env.ESQUE_AGENT || 'claude').toLowerCase();
|
|
90
|
+
// Optional bridge-side model override; the phone's per-request model wins over
|
|
91
|
+
// this, and if neither is set the agent CLI uses its own default model.
|
|
92
|
+
const MODEL_OVERRIDE = argv.model || process.env.ESQUE_MODEL || null;
|
|
90
93
|
const CUSTOM_CMD = argv.cmd || process.env.ESQUE_CMD || null;
|
|
91
94
|
const BIN_OVERRIDE = argv.bin || null;
|
|
92
95
|
const LT_SUBDOMAIN = argv.subdomain || process.env.LT_SUBDOMAIN || undefined;
|
|
@@ -191,6 +194,47 @@ function clearCliSessionId(agent, esqueSessionId) {
|
|
|
191
194
|
saveSessions();
|
|
192
195
|
}
|
|
193
196
|
|
|
197
|
+
// --- Handoff log ----------------------------------------------------------
|
|
198
|
+
// A running, on-disk record of each turn (prompt + what the agent did), kept in
|
|
199
|
+
// the project folder. Its whole purpose is to survive a lost CLI session: if
|
|
200
|
+
// the agent's conversation memory is ever GC'd, the fresh session is told to
|
|
201
|
+
// read this file and recover the project's context — so a memory reset doesn't
|
|
202
|
+
// mean starting from zero. Best-effort; bounded so it stays readable.
|
|
203
|
+
const HANDOFF_MAX = 48 * 1024;
|
|
204
|
+
function handoffRel(esqueSessionId) {
|
|
205
|
+
const short = String(esqueSessionId || 'default').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 16) || 'default';
|
|
206
|
+
return `.esque/handoff-${short}.md`;
|
|
207
|
+
}
|
|
208
|
+
function handoffPath(esqueSessionId) {
|
|
209
|
+
return path.join(WORKDIR, handoffRel(esqueSessionId));
|
|
210
|
+
}
|
|
211
|
+
function appendHandoff(esqueSessionId, prompt, replyText) {
|
|
212
|
+
if (!esqueSessionId) return;
|
|
213
|
+
try {
|
|
214
|
+
fs.mkdirSync(path.join(WORKDIR, '.esque'), { recursive: true });
|
|
215
|
+
const file = handoffPath(esqueSessionId);
|
|
216
|
+
const header = fs.existsSync(file)
|
|
217
|
+
? ''
|
|
218
|
+
: "# Esque handoff log\n\n> A running record of this project's work so your AI agent can recover context if its session is ever reset. Safe to delete (or add `.esque/` to .gitignore).\n\n";
|
|
219
|
+
const reply = String(replyText || '').trim().replace(/\n{3,}/g, '\n\n').slice(0, 1200);
|
|
220
|
+
const stamp = new Date().toISOString();
|
|
221
|
+
fs.appendFileSync(
|
|
222
|
+
file,
|
|
223
|
+
`${header}## ${stamp}\n\n**Prompt:** ${String(prompt || '').slice(0, 600)}\n\n**Agent:** ${reply || '(no output)'}\n\n---\n\n`,
|
|
224
|
+
);
|
|
225
|
+
// Keep it bounded: on overflow, retain the most recent entries (trim to a
|
|
226
|
+
// clean entry boundary so the recovered context reads cleanly).
|
|
227
|
+
const buf = fs.readFileSync(file, 'utf8');
|
|
228
|
+
if (buf.length > HANDOFF_MAX) {
|
|
229
|
+
const tail = buf.slice(buf.length - HANDOFF_MAX);
|
|
230
|
+
const cut = tail.indexOf('\n## ');
|
|
231
|
+
fs.writeFileSync(file, '# Esque handoff log (older entries trimmed)\n\n' + (cut >= 0 ? tail.slice(cut + 1) : tail));
|
|
232
|
+
}
|
|
233
|
+
} catch {
|
|
234
|
+
/* best-effort — never let handoff bookkeeping break a turn */
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
|
|
194
238
|
// --- Adapters -------------------------------------------------------------
|
|
195
239
|
// Each adapter describes how to invoke a CLI for a single prompt. The
|
|
196
240
|
// runner is identical across adapters; only argv-building and stdout
|
|
@@ -204,7 +248,7 @@ const ADAPTERS = {
|
|
|
204
248
|
"npm install -g @anthropic-ai/claude-code, then \`claude /login\` to authenticate.",
|
|
205
249
|
// `--output-format json` returns a single JSON object with
|
|
206
250
|
// {result, session_id, is_error, ...} — easy to parse, exact.
|
|
207
|
-
buildArgs(_prompt, prevSessionId) {
|
|
251
|
+
buildArgs(_prompt, prevSessionId, model) {
|
|
208
252
|
// `--dangerously-skip-permissions` lets Claude actually use its
|
|
209
253
|
// Write / Edit / Bash tools without an interactive approval prompt.
|
|
210
254
|
// In headless `--print` mode there's no way to say "yes" to a
|
|
@@ -219,6 +263,9 @@ const ADAPTERS = {
|
|
|
219
263
|
'json',
|
|
220
264
|
'--dangerously-skip-permissions',
|
|
221
265
|
];
|
|
266
|
+
// Optional per-agent model pin from the phone (Settings → Agent models).
|
|
267
|
+
// Claude accepts an alias (opus/sonnet/haiku) or a full model id.
|
|
268
|
+
if (model) args.push('--model', model);
|
|
222
269
|
if (prevSessionId) args.push('--resume', prevSessionId);
|
|
223
270
|
return args;
|
|
224
271
|
},
|
|
@@ -229,9 +276,22 @@ const ADAPTERS = {
|
|
|
229
276
|
text: r.result || r.text || '(claude returned no text)',
|
|
230
277
|
cliSessionId: r.session_id || null,
|
|
231
278
|
isError: !!r.is_error,
|
|
279
|
+
// Claude Code's JSON result already carries per-turn spend + tokens —
|
|
280
|
+
// free to surface (no extra process). Remaining WEEKLY/5h plan quota
|
|
281
|
+
// is NOT available headless (only the interactive `/usage` TUI), so we
|
|
282
|
+
// report cost + tokens per turn and let the phone keep a running total.
|
|
283
|
+
usage: {
|
|
284
|
+
agent: 'claude',
|
|
285
|
+
costUsd: typeof r.total_cost_usd === 'number' ? r.total_cost_usd : null,
|
|
286
|
+
inputTokens: r.usage?.input_tokens ?? null,
|
|
287
|
+
outputTokens: r.usage?.output_tokens ?? null,
|
|
288
|
+
cacheReadTokens: r.usage?.cache_read_input_tokens ?? null,
|
|
289
|
+
numTurns: r.num_turns ?? null,
|
|
290
|
+
durationMs: r.duration_ms ?? null,
|
|
291
|
+
},
|
|
232
292
|
};
|
|
233
293
|
} catch {
|
|
234
|
-
return { text: stdout.trim() || '(no output)', cliSessionId: null, isError: false };
|
|
294
|
+
return { text: stdout.trim() || '(no output)', cliSessionId: null, isError: false, usage: null };
|
|
235
295
|
}
|
|
236
296
|
},
|
|
237
297
|
},
|
|
@@ -248,12 +308,14 @@ const ADAPTERS = {
|
|
|
248
308
|
// looks busy but can't touch the disk. Access is already gated by the
|
|
249
309
|
// pairing secret + the startup workdir confirmation. --skip-git-repo-check
|
|
250
310
|
// lets it run in a brand-new (not-yet-git) project dir for `fresh` builds.
|
|
251
|
-
buildArgs(_prompt, _prevSessionId) {
|
|
252
|
-
|
|
311
|
+
buildArgs(_prompt, _prevSessionId, model) {
|
|
312
|
+
const args = [
|
|
253
313
|
'exec',
|
|
254
314
|
'--dangerously-bypass-approvals-and-sandbox',
|
|
255
315
|
'--skip-git-repo-check',
|
|
256
316
|
];
|
|
317
|
+
if (model) args.push('--model', model); // optional per-agent model pin
|
|
318
|
+
return args;
|
|
257
319
|
},
|
|
258
320
|
parseOutput(stdout) {
|
|
259
321
|
// `codex exec` streams its run to stdout and logs to stderr; the trimmed
|
|
@@ -263,6 +325,13 @@ const ADAPTERS = {
|
|
|
263
325
|
text: stdout.trim() || '(codex returned no output)',
|
|
264
326
|
cliSessionId: null,
|
|
265
327
|
isError: false,
|
|
328
|
+
// Per-turn tokens ARE available, but only by switching `exec` to
|
|
329
|
+
// `--json` (usage rides a `turn.completed` event) + `--output-last-message`
|
|
330
|
+
// to keep the reply text clean — a change to the invocation that needs a
|
|
331
|
+
// real Codex test before shipping, so it's deferred. Remaining rate-limit
|
|
332
|
+
// quota is NOT available in exec mode at all (rate_limits is null;
|
|
333
|
+
// openai/codex#14728). Reports null until the --json migration lands.
|
|
334
|
+
usage: null,
|
|
266
335
|
};
|
|
267
336
|
},
|
|
268
337
|
},
|
|
@@ -280,22 +349,45 @@ const ADAPTERS = {
|
|
|
280
349
|
// "--message - = read stdin" convention; passing '-' would send the
|
|
281
350
|
// agent a literal one-character message.
|
|
282
351
|
promptInArgs: true,
|
|
283
|
-
buildArgs(prompt) {
|
|
284
|
-
|
|
352
|
+
buildArgs(prompt, _prevSessionId, model) {
|
|
353
|
+
const args = [
|
|
285
354
|
'--message', prompt,
|
|
286
355
|
'--no-stream',
|
|
287
356
|
'--yes-always', // skip the "apply edit? y/n" prompts
|
|
288
357
|
'--no-pretty', // ANSI-free output for parsing
|
|
289
358
|
'--no-show-model-warnings',
|
|
290
359
|
];
|
|
360
|
+
if (model) args.push('--model', model); // optional per-agent model pin
|
|
361
|
+
return args;
|
|
291
362
|
},
|
|
292
363
|
parseOutput(stdout) {
|
|
293
364
|
// Aider streams its assistant turn interleaved with diff blocks.
|
|
294
365
|
// Best-effort: return everything stdout produced.
|
|
366
|
+
// Aider auto-prints a cost line after each exchange (we already run
|
|
367
|
+
// --no-pretty so it's ANSI-free): "Tokens: 2.8k sent, 112 received.
|
|
368
|
+
// Cost: $0.01 message, $0.01 session." The session figure accumulates
|
|
369
|
+
// across turns in the same workdir, so it's a real running total.
|
|
370
|
+
const m = stdout.match(
|
|
371
|
+
/Tokens:\s*([\d.]+[kMK]?)\s*sent,\s*([\d.]+[kMK]?)\s*received\.\s*Cost:\s*\$([\d.]+)\s*message,\s*\$([\d.]+)\s*session/i,
|
|
372
|
+
);
|
|
373
|
+
const scale = (s) => {
|
|
374
|
+
const v = parseFloat(s);
|
|
375
|
+
return /m$/i.test(s) ? v * 1e6 : /k$/i.test(s) ? v * 1e3 : v;
|
|
376
|
+
};
|
|
377
|
+
const usage = m
|
|
378
|
+
? {
|
|
379
|
+
agent: 'aider',
|
|
380
|
+
inputTokens: Math.round(scale(m[1])),
|
|
381
|
+
outputTokens: Math.round(scale(m[2])),
|
|
382
|
+
costUsd: parseFloat(m[3]),
|
|
383
|
+
costUsdSession: parseFloat(m[4]),
|
|
384
|
+
}
|
|
385
|
+
: null;
|
|
295
386
|
return {
|
|
296
387
|
text: stdout.trim() || '(aider returned no text)',
|
|
297
388
|
cliSessionId: null,
|
|
298
389
|
isError: false,
|
|
390
|
+
usage,
|
|
299
391
|
};
|
|
300
392
|
},
|
|
301
393
|
},
|
|
@@ -352,12 +444,12 @@ if (AGENT_TYPE === 'custom') {
|
|
|
352
444
|
// editing files after the bridge is gone.
|
|
353
445
|
let activeAgentKill = null;
|
|
354
446
|
|
|
355
|
-
function runAgent(prompt, esqueSessionId) {
|
|
447
|
+
function runAgent(prompt, esqueSessionId, model) {
|
|
356
448
|
return new Promise((resolve, reject) => {
|
|
357
449
|
const prevId = getCliSessionId(AGENT_TYPE, esqueSessionId);
|
|
358
450
|
let argv;
|
|
359
451
|
try {
|
|
360
|
-
argv = adapter.buildArgs(prompt, prevId);
|
|
452
|
+
argv = adapter.buildArgs(prompt, prevId, model);
|
|
361
453
|
} catch (err) {
|
|
362
454
|
reject(err);
|
|
363
455
|
return;
|
|
@@ -534,15 +626,31 @@ function runAgent(prompt, esqueSessionId) {
|
|
|
534
626
|
// bricked forever. Detect that specific failure, drop the mapping, and rerun
|
|
535
627
|
// once as a fresh CLI session.
|
|
536
628
|
const RESUME_FAIL_RE = /no conversation found|session.*not found|unknown session|invalid session/i;
|
|
537
|
-
async function runAgentResilient(prompt, esqueSessionId) {
|
|
629
|
+
async function runAgentResilient(prompt, esqueSessionId, model) {
|
|
538
630
|
const prevId = getCliSessionId(AGENT_TYPE, esqueSessionId);
|
|
539
631
|
try {
|
|
540
|
-
return await runAgent(prompt, esqueSessionId);
|
|
632
|
+
return await runAgent(prompt, esqueSessionId, model);
|
|
541
633
|
} catch (err) {
|
|
542
634
|
if (prevId && RESUME_FAIL_RE.test(String(err && err.message))) {
|
|
543
635
|
console.warn('[bridge] stored CLI session is gone — retrying as a fresh session');
|
|
544
636
|
clearCliSessionId(AGENT_TYPE, esqueSessionId);
|
|
545
|
-
|
|
637
|
+
// The agent lost its memory. If we've been keeping a handoff log, point
|
|
638
|
+
// the fresh session at it FIRST so it recovers the project's context
|
|
639
|
+
// instead of starting from zero.
|
|
640
|
+
let recovered = prompt;
|
|
641
|
+
try {
|
|
642
|
+
if (fs.existsSync(handoffPath(esqueSessionId))) {
|
|
643
|
+
recovered =
|
|
644
|
+
`[Esque session recovery] Your previous conversation in this project was reset and its in-memory context was lost. ` +
|
|
645
|
+
`BEFORE anything else, read the file \`${handoffRel(esqueSessionId)}\` in this folder — a running log of everything done in this project so far — and use it to reconstruct context. Then carry out this request:\n\n${prompt}`;
|
|
646
|
+
}
|
|
647
|
+
} catch {
|
|
648
|
+
/* fall back to the bare prompt */
|
|
649
|
+
}
|
|
650
|
+
const fresh = await runAgent(recovered, esqueSessionId, model);
|
|
651
|
+
// Tell the phone the agent's memory was reset (it ignores the field if
|
|
652
|
+
// unknown). Older bridges never set it.
|
|
653
|
+
return { ...fresh, sessionReset: true };
|
|
546
654
|
}
|
|
547
655
|
throw err;
|
|
548
656
|
}
|
|
@@ -798,6 +906,45 @@ function waitForPort(port, timeoutMs) {
|
|
|
798
906
|
});
|
|
799
907
|
}
|
|
800
908
|
|
|
909
|
+
// Reclaim `port` from any process already listening on it. A stale/foreign dev
|
|
910
|
+
// server squatting on the preview port is the #1 cause of "the preview shows
|
|
911
|
+
// the WRONG project": waitForPort() only checks that SOMETHING is listening, so
|
|
912
|
+
// a leftover Metro/Vite/expo from another repo would get tunneled instead of the
|
|
913
|
+
// server we just started. Esque owns the preview port, so we forcibly clear it
|
|
914
|
+
// before starting ours. Best-effort + synchronous (we're about to rebind).
|
|
915
|
+
// Returns the number of processes killed.
|
|
916
|
+
function freePort(port) {
|
|
917
|
+
try {
|
|
918
|
+
if (isWindows) {
|
|
919
|
+
const out = execSync(`netstat -ano -p tcp | findstr :${port}`, {
|
|
920
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
921
|
+
});
|
|
922
|
+
const pids = new Set();
|
|
923
|
+
out.split('\n').forEach((l) => {
|
|
924
|
+
const m = l.trim().match(/LISTENING\s+(\d+)\s*$/i);
|
|
925
|
+
if (m) pids.add(m[1]);
|
|
926
|
+
});
|
|
927
|
+
pids.forEach((pid) => {
|
|
928
|
+
try { execSync(`taskkill /PID ${pid} /T /F`, { stdio: 'ignore' }); } catch { /* already gone */ }
|
|
929
|
+
});
|
|
930
|
+
return pids.size;
|
|
931
|
+
}
|
|
932
|
+
const out = execSync(`lsof -ti tcp:${port} -sTCP:LISTEN`, {
|
|
933
|
+
encoding: 'utf8', stdio: ['ignore', 'pipe', 'ignore'],
|
|
934
|
+
}).trim();
|
|
935
|
+
if (!out) return 0;
|
|
936
|
+
const pids = out.split('\n').map((s) => s.trim()).filter(Boolean);
|
|
937
|
+
pids.forEach((pid) => {
|
|
938
|
+
try { process.kill(Number(pid), 'SIGKILL'); } catch { /* already gone */ }
|
|
939
|
+
});
|
|
940
|
+
return pids.length;
|
|
941
|
+
} catch {
|
|
942
|
+
// lsof/netstat exit non-zero when nothing is on the port — that's the
|
|
943
|
+
// common (port-is-free) case, not an error.
|
|
944
|
+
return 0;
|
|
945
|
+
}
|
|
946
|
+
}
|
|
947
|
+
|
|
801
948
|
function hasCloudflared() {
|
|
802
949
|
try { execSync(isWindows ? 'where cloudflared' : 'which cloudflared', { stdio: 'ignore' }); return true; }
|
|
803
950
|
catch { return false; }
|
|
@@ -909,6 +1056,15 @@ async function probeBuildError(port) {
|
|
|
909
1056
|
if (!root) return null; // couldn't reach it — don't cry wolf
|
|
910
1057
|
if (root.status >= 500) return summarizeBuildError(root.status, root.body);
|
|
911
1058
|
|
|
1059
|
+
// A NATIVE Expo dev server (`expo start`, without --web) serves a JSON
|
|
1060
|
+
// MANIFEST at the root — not a web page — so the phone's webview shows raw
|
|
1061
|
+
// JSON / a blank screen. The web preview must use `expo start --web` (React
|
|
1062
|
+
// Native Web), which serves HTML. If we see a native manifest, say exactly
|
|
1063
|
+
// what's wrong instead of handing the phone something it can't render.
|
|
1064
|
+
if (/"launchAsset"/.test(root.body) && /AppEntry|expoGo|packagerOpts|debuggerHost/.test(root.body)) {
|
|
1065
|
+
return 'This preview is a NATIVE Expo dev server (expo start), which serves a manifest the phone cannot render — you would see raw JSON or a blank screen. The web preview must run React Native Web: npx expo install react-native-web react-dom @expo/metro-runtime && npx expo start --web';
|
|
1066
|
+
}
|
|
1067
|
+
|
|
912
1068
|
// Expo/Metro: the HTML points at a *.bundle script. Fetching it forces the
|
|
913
1069
|
// compile and surfaces the react-native-web class of error as a 500.
|
|
914
1070
|
const m = root.body.match(/<script[^>]+src="([^"]*\.bundle[^"]*)"/i);
|
|
@@ -1054,6 +1210,16 @@ async function startPreview(cmd, port) {
|
|
|
1054
1210
|
console.error(`[preview] dependency install failed:\n${deps.error}`);
|
|
1055
1211
|
return { url: null, buildError: deps.error };
|
|
1056
1212
|
}
|
|
1213
|
+
// Reclaim the port from any stale/foreign dev server BEFORE starting ours.
|
|
1214
|
+
// Without this, a leftover server from another project (e.g. a manual
|
|
1215
|
+
// `expo start` left running on 8081) would be what waitForPort() latches onto,
|
|
1216
|
+
// and we'd tunnel the WRONG project to the phone. Give the port a moment to
|
|
1217
|
+
// actually release before we try to bind it.
|
|
1218
|
+
const cleared = freePort(port);
|
|
1219
|
+
if (cleared) {
|
|
1220
|
+
console.log(`[preview] cleared ${cleared} stale process(es) on port ${port} before starting`);
|
|
1221
|
+
await new Promise((r) => setTimeout(r, 400));
|
|
1222
|
+
}
|
|
1057
1223
|
console.log(`[preview] starting: ${cmd} (port ${port})`);
|
|
1058
1224
|
// Remember the command now (not on success): a revival attempt after a
|
|
1059
1225
|
// mid-start crash should still know what to run.
|
|
@@ -1154,8 +1320,8 @@ async function applyPreview(text) {
|
|
|
1154
1320
|
return { text: text.replace(PREVIEW_RE, () => replacement), buildError };
|
|
1155
1321
|
}
|
|
1156
1322
|
|
|
1157
|
-
async function runAgentWithPreview(prompt, sessionId) {
|
|
1158
|
-
const result = await runAgentResilient(prompt, sessionId);
|
|
1323
|
+
async function runAgentWithPreview(prompt, sessionId, model) {
|
|
1324
|
+
const result = await runAgentResilient(prompt, sessionId, model);
|
|
1159
1325
|
if (!result || !result.text) return result;
|
|
1160
1326
|
|
|
1161
1327
|
const applied = await applyPreview(result.text);
|
|
@@ -1174,7 +1340,7 @@ async function runAgentWithPreview(prompt, sessionId) {
|
|
|
1174
1340
|
`Fix the root cause in the code, then re-emit the ESQUE_PREVIEW marker on ` +
|
|
1175
1341
|
`its own line. Keep the explanation brief — make the fix and output the marker.`;
|
|
1176
1342
|
let fix = null;
|
|
1177
|
-
try { fix = await runAgentResilient(fixPrompt, sessionId); } catch (e) { console.error('[preview] auto-fix run failed:', e.message); }
|
|
1343
|
+
try { fix = await runAgentResilient(fixPrompt, sessionId, model); } catch (e) { console.error('[preview] auto-fix run failed:', e.message); }
|
|
1178
1344
|
if (fix && fix.text) {
|
|
1179
1345
|
const fixApplied = await applyPreview(fix.text);
|
|
1180
1346
|
result.text = `${applied.text}\n\n— Auto-fix attempt —\n${fixApplied.text}`;
|
|
@@ -1248,8 +1414,8 @@ async function sendExpoPush(token, kind, sessionId, title, message) {
|
|
|
1248
1414
|
// races two --resume's of the same CLI session, and collides in the preview
|
|
1249
1415
|
// pipeline. A simple promise chain keeps arrival order and isolates failures.
|
|
1250
1416
|
let runQueue = Promise.resolve();
|
|
1251
|
-
function enqueueRun(prompt, sessionId) {
|
|
1252
|
-
const run = runQueue.then(() => runAgentWithPreview(prompt, sessionId));
|
|
1417
|
+
function enqueueRun(prompt, sessionId, model) {
|
|
1418
|
+
const run = runQueue.then(() => runAgentWithPreview(prompt, sessionId, model));
|
|
1253
1419
|
runQueue = run.then(() => undefined, () => undefined);
|
|
1254
1420
|
return run;
|
|
1255
1421
|
}
|
|
@@ -1259,6 +1425,10 @@ async function executeHandler(req, res) {
|
|
|
1259
1425
|
const prompt = String(body.prompt || '');
|
|
1260
1426
|
const esqueSessionId = body.sessionId ?? null;
|
|
1261
1427
|
const pushToken = typeof body.pushToken === 'string' ? body.pushToken : null;
|
|
1428
|
+
// Optional per-agent model pin sent by the phone (Settings → Agent models).
|
|
1429
|
+
// Falls back to a bridge-side --model / ESQUE_MODEL override if the phone
|
|
1430
|
+
// sends none, else the agent CLI's own default model.
|
|
1431
|
+
const model = typeof body.model === 'string' && body.model.trim() ? body.model.trim() : MODEL_OVERRIDE;
|
|
1262
1432
|
if (!prompt.trim()) {
|
|
1263
1433
|
return res.status(400).json({ text: 'Empty prompt.', status: 'blocked' });
|
|
1264
1434
|
}
|
|
@@ -1279,16 +1449,19 @@ async function executeHandler(req, res) {
|
|
|
1279
1449
|
const jobId = newJobId();
|
|
1280
1450
|
jobs.set(jobId, { status: 'working', text: '', createdAt: Date.now() });
|
|
1281
1451
|
res.json({ jobId, status: 'working' });
|
|
1282
|
-
enqueueRun(prompt, esqueSessionId)
|
|
1452
|
+
enqueueRun(prompt, esqueSessionId, model)
|
|
1283
1453
|
.then((result) => {
|
|
1284
1454
|
jobs.set(jobId, {
|
|
1285
1455
|
status: result.isError ? 'blocked' : 'finished',
|
|
1286
1456
|
text: result.text,
|
|
1457
|
+
sessionReset: !!result.sessionReset,
|
|
1458
|
+
usage: result.usage ?? null,
|
|
1287
1459
|
createdAt: Date.now(),
|
|
1288
1460
|
});
|
|
1289
1461
|
console.log(
|
|
1290
1462
|
`[bridge] done job=${jobId} ${result.isError ? 'blocked' : 'finished'}`,
|
|
1291
1463
|
);
|
|
1464
|
+
if (!result.isError) appendHandoff(esqueSessionId, prompt, result.text);
|
|
1292
1465
|
const ok = !result.isError;
|
|
1293
1466
|
sendExpoPush(
|
|
1294
1467
|
pushToken,
|
|
@@ -1334,10 +1507,13 @@ async function executeHandler(req, res) {
|
|
|
1334
1507
|
};
|
|
1335
1508
|
|
|
1336
1509
|
try {
|
|
1337
|
-
const result = await enqueueRun(prompt, esqueSessionId);
|
|
1510
|
+
const result = await enqueueRun(prompt, esqueSessionId, model);
|
|
1511
|
+
if (!result.isError) appendHandoff(esqueSessionId, prompt, result.text);
|
|
1338
1512
|
finish({
|
|
1339
1513
|
text: result.text,
|
|
1340
1514
|
status: result.isError ? 'blocked' : 'finished',
|
|
1515
|
+
sessionReset: !!result.sessionReset,
|
|
1516
|
+
usage: result.usage ?? null,
|
|
1341
1517
|
});
|
|
1342
1518
|
} catch (err) {
|
|
1343
1519
|
console.error('[bridge] error:', err.message);
|
|
@@ -1355,7 +1531,7 @@ function resultHandler(req, res) {
|
|
|
1355
1531
|
text: 'That task is no longer available — the bridge may have restarted.',
|
|
1356
1532
|
});
|
|
1357
1533
|
}
|
|
1358
|
-
res.json({ status: job.status, text: job.text });
|
|
1534
|
+
res.json({ status: job.status, text: job.text, sessionReset: !!job.sessionReset, usage: job.usage ?? null });
|
|
1359
1535
|
}
|
|
1360
1536
|
|
|
1361
1537
|
// Single-flight wrapper for reviving the saved preview: GET /preview and the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esque-bridge",
|
|
3
|
-
"version": "0.6.
|
|
3
|
+
"version": "0.6.15",
|
|
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"
|