esque-bridge 0.1.0 → 0.2.1
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 +237 -94
- package/package.json +2 -2
package/index.js
CHANGED
|
@@ -2,20 +2,19 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* esque-bridge — the desktop-side receiver for the Esque Agent mobile app.
|
|
4
4
|
*
|
|
5
|
-
* Pairs your phone with
|
|
6
|
-
*
|
|
7
|
-
* per-token API billing.
|
|
5
|
+
* Pairs your phone with a local coding-agent CLI (Claude Code by default,
|
|
6
|
+
* Aider, or any custom command) so prompts you send from the Esque app
|
|
7
|
+
* run through your subscription rather than per-token API billing.
|
|
8
8
|
*
|
|
9
9
|
* 1. Boots an Express server on localhost
|
|
10
|
-
* 2. Opens a public localtunnel URL
|
|
11
|
-
* 3. Prints a QR code embedding `esque://pair?url=…&secret=…`
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
* and runs the prompt through `claude --print`, returning stdout
|
|
10
|
+
* 2. Opens a public localtunnel URL
|
|
11
|
+
* 3. Prints a QR code embedding `esque://pair?url=…&secret=…&agent=…`
|
|
12
|
+
* 4. Accepts POST /execute (pairing-secret-gated) and runs the prompt
|
|
13
|
+
* through the configured agent adapter, returning stdout
|
|
15
14
|
*
|
|
16
|
-
*
|
|
17
|
-
* npx esque-bridge
|
|
18
|
-
* esque-bridge --
|
|
15
|
+
* npx esque-bridge # default: claude
|
|
16
|
+
* npx esque-bridge --agent aider # Aider against the current repo
|
|
17
|
+
* npx esque-bridge --agent custom --cmd 'mycli --prompt {prompt}'
|
|
19
18
|
*/
|
|
20
19
|
|
|
21
20
|
const express = require('express');
|
|
@@ -34,21 +33,31 @@ const argv = parseArgs(process.argv.slice(2));
|
|
|
34
33
|
if (argv.help || argv.h) {
|
|
35
34
|
console.log(
|
|
36
35
|
`
|
|
37
|
-
Esque Bridge — pair your phone with
|
|
36
|
+
Esque Bridge — pair your phone with a local coding-agent CLI.
|
|
38
37
|
|
|
39
38
|
USAGE
|
|
40
|
-
esque-bridge [--
|
|
39
|
+
esque-bridge [--agent claude|aider|custom] [--port 3030] [--workdir .]
|
|
40
|
+
[--cmd 'tool --prompt {prompt}'] [--bin <binary>]
|
|
41
|
+
[--timeout 300000]
|
|
42
|
+
|
|
43
|
+
AGENT ADAPTERS
|
|
44
|
+
claude (default) — uses Claude Code CLI (\`claude --print --output-format json\`).
|
|
45
|
+
Persists session ids so each conversation continues via --resume.
|
|
46
|
+
aider — uses Aider CLI (\`aider --message ... --yes-always --no-stream\`).
|
|
47
|
+
Conversation continuity is handled by aider's own .aider.chat.history.md.
|
|
48
|
+
custom — runs an arbitrary command. Pass --cmd 'tool --prompt {prompt}'
|
|
49
|
+
with literal {prompt} as a placeholder.
|
|
41
50
|
|
|
42
51
|
OPTIONS
|
|
43
52
|
--port Local HTTP port to listen on (default: 3030)
|
|
44
|
-
--workdir Working directory for
|
|
45
|
-
--bin
|
|
53
|
+
--workdir Working directory for the agent (default: current dir)
|
|
54
|
+
--bin Override the agent's default binary
|
|
46
55
|
--timeout Max ms per prompt before SIGTERM (default: 300000 = 5 min)
|
|
47
56
|
--subdomain Request a stable localtunnel subdomain (optional)
|
|
48
57
|
|
|
49
58
|
PREREQS
|
|
50
|
-
npm install -g @anthropic-ai/claude-code
|
|
51
|
-
|
|
59
|
+
Claude: npm install -g @anthropic-ai/claude-code && claude /login
|
|
60
|
+
Aider: python -m pip install aider-chat
|
|
52
61
|
|
|
53
62
|
OUTPUT
|
|
54
63
|
Prints a QR code in your terminal. Scan it from the Esque iOS app to
|
|
@@ -63,7 +72,9 @@ const WORKDIR = path.resolve(
|
|
|
63
72
|
argv.workdir || process.env.CLAUDE_WORKDIR || process.cwd(),
|
|
64
73
|
);
|
|
65
74
|
const TIMEOUT_MS = Number(argv.timeout || 5 * 60 * 1000);
|
|
66
|
-
const
|
|
75
|
+
const AGENT_TYPE = String(argv.agent || process.env.ESQUE_AGENT || 'claude').toLowerCase();
|
|
76
|
+
const CUSTOM_CMD = argv.cmd || process.env.ESQUE_CMD || null;
|
|
77
|
+
const BIN_OVERRIDE = argv.bin || null;
|
|
67
78
|
const LT_SUBDOMAIN = argv.subdomain || process.env.LT_SUBDOMAIN || undefined;
|
|
68
79
|
const PAIRING_SECRET = crypto.randomBytes(16).toString('hex');
|
|
69
80
|
const SESSIONS_FILE = path.join(os.homedir(), '.esque-bridge-sessions.json');
|
|
@@ -86,8 +97,8 @@ function parseArgs(args) {
|
|
|
86
97
|
}
|
|
87
98
|
|
|
88
99
|
// --- Session persistence -------------------------------------------------
|
|
89
|
-
//
|
|
90
|
-
//
|
|
100
|
+
// Per-agent map of {esqueSessionId → cliSessionId}. Each adapter that
|
|
101
|
+
// supports --resume reads/writes its own slot. Survives bridge restarts.
|
|
91
102
|
|
|
92
103
|
let sessionMap = {};
|
|
93
104
|
try {
|
|
@@ -102,23 +113,158 @@ function saveSessions() {
|
|
|
102
113
|
console.warn('[bridge] could not persist session map:', err.message);
|
|
103
114
|
}
|
|
104
115
|
}
|
|
116
|
+
function getCliSessionId(agent, esqueSessionId) {
|
|
117
|
+
if (!esqueSessionId) return null;
|
|
118
|
+
return sessionMap[agent]?.[esqueSessionId] ?? null;
|
|
119
|
+
}
|
|
120
|
+
function setCliSessionId(agent, esqueSessionId, cliId) {
|
|
121
|
+
if (!cliId || !esqueSessionId) return;
|
|
122
|
+
if (!sessionMap[agent]) sessionMap[agent] = {};
|
|
123
|
+
sessionMap[agent][esqueSessionId] = cliId;
|
|
124
|
+
saveSessions();
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// --- Adapters -------------------------------------------------------------
|
|
128
|
+
// Each adapter describes how to invoke a CLI for a single prompt. The
|
|
129
|
+
// runner is identical across adapters; only argv-building and stdout
|
|
130
|
+
// parsing differ.
|
|
131
|
+
|
|
132
|
+
const ADAPTERS = {
|
|
133
|
+
claude: {
|
|
134
|
+
label: 'Claude Code',
|
|
135
|
+
defaultBin: 'claude',
|
|
136
|
+
install:
|
|
137
|
+
"npm install -g @anthropic-ai/claude-code, then \`claude /login\` to authenticate.",
|
|
138
|
+
// `--output-format json` returns a single JSON object with
|
|
139
|
+
// {result, session_id, is_error, ...} — easy to parse, exact.
|
|
140
|
+
buildArgs(_prompt, prevSessionId) {
|
|
141
|
+
// `--dangerously-skip-permissions` lets Claude actually use its
|
|
142
|
+
// Write / Edit / Bash tools without an interactive approval prompt.
|
|
143
|
+
// In headless `--print` mode there's no way to say "yes" to a
|
|
144
|
+
// permission request, so without this every file write is silently
|
|
145
|
+
// blocked — the agent looks busy but can't touch the disk. This is
|
|
146
|
+
// the autonomous-bridge use case the flag exists for; access is
|
|
147
|
+
// already gated by the per-session pairing secret. (Mirrors Aider's
|
|
148
|
+
// `--yes-always` below.)
|
|
149
|
+
const args = [
|
|
150
|
+
'--print',
|
|
151
|
+
'--output-format',
|
|
152
|
+
'json',
|
|
153
|
+
'--dangerously-skip-permissions',
|
|
154
|
+
];
|
|
155
|
+
if (prevSessionId) args.push('--resume', prevSessionId);
|
|
156
|
+
return args;
|
|
157
|
+
},
|
|
158
|
+
parseOutput(stdout) {
|
|
159
|
+
try {
|
|
160
|
+
const r = JSON.parse(stdout);
|
|
161
|
+
return {
|
|
162
|
+
text: r.result || r.text || '(claude returned no text)',
|
|
163
|
+
cliSessionId: r.session_id || null,
|
|
164
|
+
isError: !!r.is_error,
|
|
165
|
+
};
|
|
166
|
+
} catch {
|
|
167
|
+
return { text: stdout.trim() || '(no output)', cliSessionId: null, isError: false };
|
|
168
|
+
}
|
|
169
|
+
},
|
|
170
|
+
},
|
|
171
|
+
|
|
172
|
+
aider: {
|
|
173
|
+
label: 'Aider',
|
|
174
|
+
defaultBin: 'aider',
|
|
175
|
+
install: 'python -m pip install aider-chat',
|
|
176
|
+
// Aider keeps its own .aider.chat.history.md per-directory, so we
|
|
177
|
+
// don't need to track a session id — every invocation against the
|
|
178
|
+
// same workdir picks up where the last one left off.
|
|
179
|
+
buildArgs(_prompt) {
|
|
180
|
+
return [
|
|
181
|
+
'--message', '-', // read message from stdin (we'll pipe it)
|
|
182
|
+
'--no-stream',
|
|
183
|
+
'--yes-always', // skip the "apply edit? y/n" prompts
|
|
184
|
+
'--no-pretty', // ANSI-free output for parsing
|
|
185
|
+
'--no-show-model-warnings',
|
|
186
|
+
];
|
|
187
|
+
},
|
|
188
|
+
parseOutput(stdout) {
|
|
189
|
+
// Aider streams its assistant turn interleaved with diff blocks.
|
|
190
|
+
// Best-effort: return everything stdout produced.
|
|
191
|
+
return {
|
|
192
|
+
text: stdout.trim() || '(aider returned no text)',
|
|
193
|
+
cliSessionId: null,
|
|
194
|
+
isError: false,
|
|
195
|
+
};
|
|
196
|
+
},
|
|
197
|
+
},
|
|
198
|
+
|
|
199
|
+
custom: {
|
|
200
|
+
label: 'Custom',
|
|
201
|
+
defaultBin: null,
|
|
202
|
+
install:
|
|
203
|
+
"Pass --cmd 'your-cli --prompt {prompt}' (with literal {prompt} as placeholder).",
|
|
204
|
+
buildArgs(_prompt) {
|
|
205
|
+
if (!CUSTOM_CMD) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
"--agent custom requires --cmd '<command-template>' with {prompt} placeholder.",
|
|
208
|
+
);
|
|
209
|
+
}
|
|
210
|
+
// Split the template on whitespace but preserve {prompt} as its own
|
|
211
|
+
// argv slot (so the runner can substitute the prompt body in).
|
|
212
|
+
return CUSTOM_CMD.split(/\s+/).filter(Boolean);
|
|
213
|
+
},
|
|
214
|
+
parseOutput(stdout) {
|
|
215
|
+
return {
|
|
216
|
+
text: stdout.trim() || '(no output)',
|
|
217
|
+
cliSessionId: null,
|
|
218
|
+
isError: false,
|
|
219
|
+
};
|
|
220
|
+
},
|
|
221
|
+
},
|
|
222
|
+
};
|
|
223
|
+
|
|
224
|
+
if (!ADAPTERS[AGENT_TYPE]) {
|
|
225
|
+
console.error(
|
|
226
|
+
`Unknown --agent: ${AGENT_TYPE}. Use one of: ${Object.keys(ADAPTERS).join(', ')}.`,
|
|
227
|
+
);
|
|
228
|
+
process.exit(1);
|
|
229
|
+
}
|
|
105
230
|
|
|
106
|
-
|
|
231
|
+
const adapter = ADAPTERS[AGENT_TYPE];
|
|
232
|
+
const AGENT_BIN = BIN_OVERRIDE || adapter.defaultBin;
|
|
107
233
|
|
|
108
|
-
|
|
234
|
+
if (AGENT_TYPE === 'custom') {
|
|
235
|
+
if (!CUSTOM_CMD) {
|
|
236
|
+
console.error(
|
|
237
|
+
"--agent custom requires --cmd '<your-cli> {prompt}' (literal {prompt} substitution).",
|
|
238
|
+
);
|
|
239
|
+
process.exit(1);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// --- Runner ---------------------------------------------------------------
|
|
244
|
+
|
|
245
|
+
function runAgent(prompt, esqueSessionId) {
|
|
109
246
|
return new Promise((resolve, reject) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
// prompt
|
|
121
|
-
|
|
247
|
+
const prevId = getCliSessionId(AGENT_TYPE, esqueSessionId);
|
|
248
|
+
let argv;
|
|
249
|
+
try {
|
|
250
|
+
argv = adapter.buildArgs(prompt, prevId);
|
|
251
|
+
} catch (err) {
|
|
252
|
+
reject(err);
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// For 'custom', substitute the {prompt} placeholder in argv. For
|
|
257
|
+
// built-in adapters, the prompt always rides via stdin (avoids
|
|
258
|
+
// ARG_MAX on long blueprint payloads).
|
|
259
|
+
let bin = AGENT_BIN;
|
|
260
|
+
let usesStdin = true;
|
|
261
|
+
if (AGENT_TYPE === 'custom') {
|
|
262
|
+
bin = argv.shift();
|
|
263
|
+
argv = argv.map((a) => a.replace('{prompt}', prompt));
|
|
264
|
+
usesStdin = !argv.some((a) => a.includes(prompt));
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
const child = spawn(bin, argv, {
|
|
122
268
|
cwd: WORKDIR,
|
|
123
269
|
env: process.env,
|
|
124
270
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -138,7 +284,7 @@ function runClaude(prompt, claudeSessionId) {
|
|
|
138
284
|
const killTimer = setTimeout(() => {
|
|
139
285
|
child.kill('SIGTERM');
|
|
140
286
|
rejectOnce(
|
|
141
|
-
new Error(
|
|
287
|
+
new Error(`${adapter.label} timed out after ${Math.round(TIMEOUT_MS / 1000)}s`),
|
|
142
288
|
);
|
|
143
289
|
}, TIMEOUT_MS);
|
|
144
290
|
|
|
@@ -148,9 +294,7 @@ function runClaude(prompt, claudeSessionId) {
|
|
|
148
294
|
clearTimeout(killTimer);
|
|
149
295
|
if (err.code === 'ENOENT') {
|
|
150
296
|
rejectOnce(
|
|
151
|
-
new Error(
|
|
152
|
-
`'${CLAUDE_BIN}' not found in PATH. Install: npm install -g @anthropic-ai/claude-code, then \`claude /login\`.`,
|
|
153
|
-
),
|
|
297
|
+
new Error(`'${bin}' not found in PATH. Install: ${adapter.install}`),
|
|
154
298
|
);
|
|
155
299
|
return;
|
|
156
300
|
}
|
|
@@ -161,31 +305,25 @@ function runClaude(prompt, claudeSessionId) {
|
|
|
161
305
|
if (code !== 0) {
|
|
162
306
|
rejectOnce(
|
|
163
307
|
new Error(
|
|
164
|
-
|
|
308
|
+
`${adapter.label} exited ${code}${stderr ? `: ${stderr.trim().slice(0, 500)}` : ''}`,
|
|
165
309
|
),
|
|
166
310
|
);
|
|
167
311
|
return;
|
|
168
312
|
}
|
|
169
313
|
try {
|
|
170
|
-
const
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
} catch {
|
|
178
|
-
// Claude returned non-JSON for some reason — surface raw stdout so
|
|
179
|
-
// the user at least sees what happened.
|
|
180
|
-
resolveOnce({
|
|
181
|
-
text: stdout.trim() || '(no output)',
|
|
182
|
-
sessionId: null,
|
|
183
|
-
isError: false,
|
|
184
|
-
});
|
|
314
|
+
const parsed = adapter.parseOutput(stdout);
|
|
315
|
+
if (parsed.cliSessionId) {
|
|
316
|
+
setCliSessionId(AGENT_TYPE, esqueSessionId, parsed.cliSessionId);
|
|
317
|
+
}
|
|
318
|
+
resolveOnce(parsed);
|
|
319
|
+
} catch (err) {
|
|
320
|
+
rejectOnce(err);
|
|
185
321
|
}
|
|
186
322
|
});
|
|
187
323
|
|
|
188
|
-
|
|
324
|
+
if (usesStdin) {
|
|
325
|
+
child.stdin.write(prompt);
|
|
326
|
+
}
|
|
189
327
|
child.stdin.end();
|
|
190
328
|
});
|
|
191
329
|
}
|
|
@@ -194,34 +332,28 @@ function runClaude(prompt, claudeSessionId) {
|
|
|
194
332
|
|
|
195
333
|
const app = express();
|
|
196
334
|
app.use(express.json({ limit: '5mb' }));
|
|
197
|
-
|
|
198
|
-
// Disable Express's default x-powered-by header and any tunnel-side
|
|
199
|
-
// caching surprises.
|
|
200
335
|
app.disable('x-powered-by');
|
|
201
336
|
app.use((req, res, next) => {
|
|
202
337
|
res.setHeader('Cache-Control', 'no-store');
|
|
203
338
|
next();
|
|
204
339
|
});
|
|
205
340
|
|
|
206
|
-
// Public health probe.
|
|
207
|
-
// the bridge alive" answer. The Esque app's connection-test uses this for
|
|
208
|
-
// reachability before it sends a real prompt.
|
|
341
|
+
// Public health probe.
|
|
209
342
|
app.get('/', (_req, res) => {
|
|
210
343
|
res.json({
|
|
211
344
|
ok: true,
|
|
212
345
|
service: 'esque-bridge',
|
|
346
|
+
agent: AGENT_TYPE,
|
|
347
|
+
agentLabel: adapter.label,
|
|
213
348
|
workdir: WORKDIR,
|
|
214
|
-
sessions: Object.keys(sessionMap).length,
|
|
349
|
+
sessions: Object.keys(sessionMap[AGENT_TYPE] ?? {}).length,
|
|
215
350
|
});
|
|
216
351
|
});
|
|
217
352
|
|
|
218
|
-
//
|
|
219
|
-
// POST with `_probe: true`. Reply OK without invoking claude, and without
|
|
220
|
-
// requiring the secret (because the very first thing a freshly-paired app
|
|
221
|
-
// will do is run this check).
|
|
353
|
+
// Connection-test probe — no auth.
|
|
222
354
|
app.post('/', (req, res, next) => {
|
|
223
355
|
if (req.body && req.body._probe === true) {
|
|
224
|
-
return res.json({ ok: true, service: 'esque-bridge' });
|
|
356
|
+
return res.json({ ok: true, service: 'esque-bridge', agent: AGENT_TYPE });
|
|
225
357
|
}
|
|
226
358
|
return next();
|
|
227
359
|
});
|
|
@@ -238,8 +370,6 @@ function requireAuth(req, res, next) {
|
|
|
238
370
|
next();
|
|
239
371
|
}
|
|
240
372
|
|
|
241
|
-
// The execute endpoint. Body: { prompt, sessionId?, pushToken? }.
|
|
242
|
-
// Returns: { text, status: 'finished' | 'blocked' }.
|
|
243
373
|
async function executeHandler(req, res) {
|
|
244
374
|
const body = req.body || {};
|
|
245
375
|
const prompt = String(body.prompt || '');
|
|
@@ -248,37 +378,53 @@ async function executeHandler(req, res) {
|
|
|
248
378
|
return res.status(400).json({ text: 'Empty prompt.', status: 'blocked' });
|
|
249
379
|
}
|
|
250
380
|
|
|
251
|
-
const
|
|
381
|
+
const prev = getCliSessionId(AGENT_TYPE, esqueSessionId);
|
|
252
382
|
const preview = prompt.slice(0, 80).replace(/\s+/g, ' ');
|
|
253
383
|
console.log(
|
|
254
|
-
`[bridge] POST ${preview}… esque=${esqueSessionId ?? '-'}
|
|
384
|
+
`[bridge] POST ${preview}… esque=${esqueSessionId ?? '-'} ${AGENT_TYPE}=${prev ?? 'new'}`,
|
|
255
385
|
);
|
|
256
386
|
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
387
|
+
// Stream a keep-alive heartbeat while the agent thinks. `claude --print`
|
|
388
|
+
// emits nothing until it finishes (often 30–120s on a cold start with a big
|
|
389
|
+
// prompt), and localtunnel / intervening proxies close *idle* connections —
|
|
390
|
+
// which surfaced on the phone as "fetch request has been canceled". A space
|
|
391
|
+
// every few seconds keeps the connection active. The phone trims the body
|
|
392
|
+
// before JSON.parse, so leading whitespace is harmless.
|
|
393
|
+
res.status(200);
|
|
394
|
+
res.setHeader('Content-Type', 'application/json; charset=utf-8');
|
|
395
|
+
res.setHeader('Cache-Control', 'no-cache, no-transform');
|
|
396
|
+
const heartbeat = setInterval(() => {
|
|
397
|
+
try {
|
|
398
|
+
res.write(' ');
|
|
399
|
+
} catch {
|
|
400
|
+
/* socket already closed */
|
|
262
401
|
}
|
|
263
|
-
|
|
402
|
+
}, 5000);
|
|
403
|
+
const finish = (payload) => {
|
|
404
|
+
clearInterval(heartbeat);
|
|
405
|
+
try {
|
|
406
|
+
res.end(JSON.stringify(payload));
|
|
407
|
+
} catch {
|
|
408
|
+
/* socket already closed */
|
|
409
|
+
}
|
|
410
|
+
};
|
|
411
|
+
|
|
412
|
+
try {
|
|
413
|
+
const result = await runAgent(prompt, esqueSessionId);
|
|
414
|
+
finish({
|
|
264
415
|
text: result.text,
|
|
265
416
|
status: result.isError ? 'blocked' : 'finished',
|
|
266
417
|
});
|
|
267
418
|
} catch (err) {
|
|
268
419
|
console.error('[bridge] error:', err.message);
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
420
|
+
// Headers are already sent (heartbeat), so the error rides in the body
|
|
421
|
+
// with status 'blocked' rather than an HTTP 500.
|
|
422
|
+
finish({ text: `${adapter.label} failed: ${err.message}`, status: 'blocked' });
|
|
272
423
|
}
|
|
273
424
|
}
|
|
274
425
|
|
|
275
|
-
// Two routes for the same handler: `/` (what the Esque app POSTs to today)
|
|
276
|
-
// and `/execute` (the cleaner name the directive spec specified — kept so
|
|
277
|
-
// future iOS versions can switch without breaking the bridge).
|
|
278
426
|
app.post('/execute', requireAuth, executeHandler);
|
|
279
427
|
app.post('/', requireAuth, executeHandler);
|
|
280
|
-
|
|
281
|
-
// Anything else: 405.
|
|
282
428
|
app.use((_req, res) => res.status(405).json({ error: 'POST only' }));
|
|
283
429
|
|
|
284
430
|
// --- Boot -----------------------------------------------------------------
|
|
@@ -296,30 +442,27 @@ async function main() {
|
|
|
296
442
|
tunnel = await localtunnel({ port: PORT, subdomain: LT_SUBDOMAIN });
|
|
297
443
|
} catch (err) {
|
|
298
444
|
console.error('Failed to open localtunnel:', err.message);
|
|
299
|
-
console.error(
|
|
300
|
-
'If localtunnel.me is blocked on your network, try cloudflared:',
|
|
301
|
-
);
|
|
445
|
+
console.error('If localtunnel.me is blocked, try cloudflared:');
|
|
302
446
|
console.error(` cloudflared tunnel --url http://localhost:${PORT}`);
|
|
303
447
|
process.exit(1);
|
|
304
448
|
}
|
|
305
449
|
|
|
306
|
-
const pairUrl = `esque://pair?url=${encodeURIComponent(tunnel.url)}&secret=${PAIRING_SECRET}`;
|
|
450
|
+
const pairUrl = `esque://pair?url=${encodeURIComponent(tunnel.url)}&secret=${PAIRING_SECRET}&agent=${AGENT_TYPE}`;
|
|
307
451
|
|
|
308
452
|
console.log('');
|
|
309
453
|
console.log('━'.repeat(68));
|
|
310
|
-
console.log(
|
|
454
|
+
console.log(` Esque Bridge Active · ${adapter.label}`);
|
|
311
455
|
console.log('━'.repeat(68));
|
|
312
456
|
console.log('');
|
|
313
|
-
console.log(
|
|
314
|
-
' Scan this QR code with the Esque Agent mobile app to pair your device.',
|
|
315
|
-
);
|
|
457
|
+
console.log(' Scan this QR code with the Esque Agent mobile app to pair.');
|
|
316
458
|
console.log('');
|
|
317
459
|
qrcode.generate(pairUrl, { small: true });
|
|
318
460
|
console.log('');
|
|
461
|
+
console.log(` Agent ${adapter.label} (${AGENT_TYPE})`);
|
|
319
462
|
console.log(` Local http://localhost:${PORT}`);
|
|
320
463
|
console.log(` Tunnel ${tunnel.url}`);
|
|
321
464
|
console.log(` Workdir ${WORKDIR}`);
|
|
322
|
-
console.log(`
|
|
465
|
+
console.log(` Binary ${AGENT_BIN ?? '(custom)'}`);
|
|
323
466
|
console.log(
|
|
324
467
|
` Pair secret ${PAIRING_SECRET.slice(0, 8)}… (rotates on restart — don't share)`,
|
|
325
468
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esque-bridge",
|
|
3
|
-
"version": "0.1
|
|
4
|
-
"description": "Desktop-side receiver for the Esque Agent mobile app. Pairs your phone with
|
|
3
|
+
"version": "0.2.1",
|
|
4
|
+
"description": "Desktop-side receiver for the Esque Agent mobile app. Pairs your phone with a local coding-agent CLI (Claude Code, 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"
|
|
7
7
|
},
|