esque-bridge 0.1.0 → 0.2.0
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 +196 -91
- 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,145 @@ 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
|
+
const args = ['--print', '--output-format', 'json'];
|
|
142
|
+
if (prevSessionId) args.push('--resume', prevSessionId);
|
|
143
|
+
return args;
|
|
144
|
+
},
|
|
145
|
+
parseOutput(stdout) {
|
|
146
|
+
try {
|
|
147
|
+
const r = JSON.parse(stdout);
|
|
148
|
+
return {
|
|
149
|
+
text: r.result || r.text || '(claude returned no text)',
|
|
150
|
+
cliSessionId: r.session_id || null,
|
|
151
|
+
isError: !!r.is_error,
|
|
152
|
+
};
|
|
153
|
+
} catch {
|
|
154
|
+
return { text: stdout.trim() || '(no output)', cliSessionId: null, isError: false };
|
|
155
|
+
}
|
|
156
|
+
},
|
|
157
|
+
},
|
|
158
|
+
|
|
159
|
+
aider: {
|
|
160
|
+
label: 'Aider',
|
|
161
|
+
defaultBin: 'aider',
|
|
162
|
+
install: 'python -m pip install aider-chat',
|
|
163
|
+
// Aider keeps its own .aider.chat.history.md per-directory, so we
|
|
164
|
+
// don't need to track a session id — every invocation against the
|
|
165
|
+
// same workdir picks up where the last one left off.
|
|
166
|
+
buildArgs(_prompt) {
|
|
167
|
+
return [
|
|
168
|
+
'--message', '-', // read message from stdin (we'll pipe it)
|
|
169
|
+
'--no-stream',
|
|
170
|
+
'--yes-always', // skip the "apply edit? y/n" prompts
|
|
171
|
+
'--no-pretty', // ANSI-free output for parsing
|
|
172
|
+
'--no-show-model-warnings',
|
|
173
|
+
];
|
|
174
|
+
},
|
|
175
|
+
parseOutput(stdout) {
|
|
176
|
+
// Aider streams its assistant turn interleaved with diff blocks.
|
|
177
|
+
// Best-effort: return everything stdout produced.
|
|
178
|
+
return {
|
|
179
|
+
text: stdout.trim() || '(aider returned no text)',
|
|
180
|
+
cliSessionId: null,
|
|
181
|
+
isError: false,
|
|
182
|
+
};
|
|
183
|
+
},
|
|
184
|
+
},
|
|
185
|
+
|
|
186
|
+
custom: {
|
|
187
|
+
label: 'Custom',
|
|
188
|
+
defaultBin: null,
|
|
189
|
+
install:
|
|
190
|
+
"Pass --cmd 'your-cli --prompt {prompt}' (with literal {prompt} as placeholder).",
|
|
191
|
+
buildArgs(_prompt) {
|
|
192
|
+
if (!CUSTOM_CMD) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
"--agent custom requires --cmd '<command-template>' with {prompt} placeholder.",
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
// Split the template on whitespace but preserve {prompt} as its own
|
|
198
|
+
// argv slot (so the runner can substitute the prompt body in).
|
|
199
|
+
return CUSTOM_CMD.split(/\s+/).filter(Boolean);
|
|
200
|
+
},
|
|
201
|
+
parseOutput(stdout) {
|
|
202
|
+
return {
|
|
203
|
+
text: stdout.trim() || '(no output)',
|
|
204
|
+
cliSessionId: null,
|
|
205
|
+
isError: false,
|
|
206
|
+
};
|
|
207
|
+
},
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
if (!ADAPTERS[AGENT_TYPE]) {
|
|
212
|
+
console.error(
|
|
213
|
+
`Unknown --agent: ${AGENT_TYPE}. Use one of: ${Object.keys(ADAPTERS).join(', ')}.`,
|
|
214
|
+
);
|
|
215
|
+
process.exit(1);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
const adapter = ADAPTERS[AGENT_TYPE];
|
|
219
|
+
const AGENT_BIN = BIN_OVERRIDE || adapter.defaultBin;
|
|
220
|
+
|
|
221
|
+
if (AGENT_TYPE === 'custom') {
|
|
222
|
+
if (!CUSTOM_CMD) {
|
|
223
|
+
console.error(
|
|
224
|
+
"--agent custom requires --cmd '<your-cli> {prompt}' (literal {prompt} substitution).",
|
|
225
|
+
);
|
|
226
|
+
process.exit(1);
|
|
227
|
+
}
|
|
228
|
+
}
|
|
105
229
|
|
|
106
|
-
// ---
|
|
230
|
+
// --- Runner ---------------------------------------------------------------
|
|
107
231
|
|
|
108
|
-
function
|
|
232
|
+
function runAgent(prompt, esqueSessionId) {
|
|
109
233
|
return new Promise((resolve, reject) => {
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
//
|
|
120
|
-
// prompt
|
|
121
|
-
|
|
234
|
+
const prevId = getCliSessionId(AGENT_TYPE, esqueSessionId);
|
|
235
|
+
let argv;
|
|
236
|
+
try {
|
|
237
|
+
argv = adapter.buildArgs(prompt, prevId);
|
|
238
|
+
} catch (err) {
|
|
239
|
+
reject(err);
|
|
240
|
+
return;
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
// For 'custom', substitute the {prompt} placeholder in argv. For
|
|
244
|
+
// built-in adapters, the prompt always rides via stdin (avoids
|
|
245
|
+
// ARG_MAX on long blueprint payloads).
|
|
246
|
+
let bin = AGENT_BIN;
|
|
247
|
+
let usesStdin = true;
|
|
248
|
+
if (AGENT_TYPE === 'custom') {
|
|
249
|
+
bin = argv.shift();
|
|
250
|
+
argv = argv.map((a) => a.replace('{prompt}', prompt));
|
|
251
|
+
usesStdin = !argv.some((a) => a.includes(prompt));
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
const child = spawn(bin, argv, {
|
|
122
255
|
cwd: WORKDIR,
|
|
123
256
|
env: process.env,
|
|
124
257
|
stdio: ['pipe', 'pipe', 'pipe'],
|
|
@@ -138,7 +271,7 @@ function runClaude(prompt, claudeSessionId) {
|
|
|
138
271
|
const killTimer = setTimeout(() => {
|
|
139
272
|
child.kill('SIGTERM');
|
|
140
273
|
rejectOnce(
|
|
141
|
-
new Error(
|
|
274
|
+
new Error(`${adapter.label} timed out after ${Math.round(TIMEOUT_MS / 1000)}s`),
|
|
142
275
|
);
|
|
143
276
|
}, TIMEOUT_MS);
|
|
144
277
|
|
|
@@ -148,9 +281,7 @@ function runClaude(prompt, claudeSessionId) {
|
|
|
148
281
|
clearTimeout(killTimer);
|
|
149
282
|
if (err.code === 'ENOENT') {
|
|
150
283
|
rejectOnce(
|
|
151
|
-
new Error(
|
|
152
|
-
`'${CLAUDE_BIN}' not found in PATH. Install: npm install -g @anthropic-ai/claude-code, then \`claude /login\`.`,
|
|
153
|
-
),
|
|
284
|
+
new Error(`'${bin}' not found in PATH. Install: ${adapter.install}`),
|
|
154
285
|
);
|
|
155
286
|
return;
|
|
156
287
|
}
|
|
@@ -161,31 +292,25 @@ function runClaude(prompt, claudeSessionId) {
|
|
|
161
292
|
if (code !== 0) {
|
|
162
293
|
rejectOnce(
|
|
163
294
|
new Error(
|
|
164
|
-
|
|
295
|
+
`${adapter.label} exited ${code}${stderr ? `: ${stderr.trim().slice(0, 500)}` : ''}`,
|
|
165
296
|
),
|
|
166
297
|
);
|
|
167
298
|
return;
|
|
168
299
|
}
|
|
169
300
|
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
|
-
});
|
|
301
|
+
const parsed = adapter.parseOutput(stdout);
|
|
302
|
+
if (parsed.cliSessionId) {
|
|
303
|
+
setCliSessionId(AGENT_TYPE, esqueSessionId, parsed.cliSessionId);
|
|
304
|
+
}
|
|
305
|
+
resolveOnce(parsed);
|
|
306
|
+
} catch (err) {
|
|
307
|
+
rejectOnce(err);
|
|
185
308
|
}
|
|
186
309
|
});
|
|
187
310
|
|
|
188
|
-
|
|
311
|
+
if (usesStdin) {
|
|
312
|
+
child.stdin.write(prompt);
|
|
313
|
+
}
|
|
189
314
|
child.stdin.end();
|
|
190
315
|
});
|
|
191
316
|
}
|
|
@@ -194,34 +319,28 @@ function runClaude(prompt, claudeSessionId) {
|
|
|
194
319
|
|
|
195
320
|
const app = express();
|
|
196
321
|
app.use(express.json({ limit: '5mb' }));
|
|
197
|
-
|
|
198
|
-
// Disable Express's default x-powered-by header and any tunnel-side
|
|
199
|
-
// caching surprises.
|
|
200
322
|
app.disable('x-powered-by');
|
|
201
323
|
app.use((req, res, next) => {
|
|
202
324
|
res.setHeader('Cache-Control', 'no-store');
|
|
203
325
|
next();
|
|
204
326
|
});
|
|
205
327
|
|
|
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.
|
|
328
|
+
// Public health probe.
|
|
209
329
|
app.get('/', (_req, res) => {
|
|
210
330
|
res.json({
|
|
211
331
|
ok: true,
|
|
212
332
|
service: 'esque-bridge',
|
|
333
|
+
agent: AGENT_TYPE,
|
|
334
|
+
agentLabel: adapter.label,
|
|
213
335
|
workdir: WORKDIR,
|
|
214
|
-
sessions: Object.keys(sessionMap).length,
|
|
336
|
+
sessions: Object.keys(sessionMap[AGENT_TYPE] ?? {}).length,
|
|
215
337
|
});
|
|
216
338
|
});
|
|
217
339
|
|
|
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).
|
|
340
|
+
// Connection-test probe — no auth.
|
|
222
341
|
app.post('/', (req, res, next) => {
|
|
223
342
|
if (req.body && req.body._probe === true) {
|
|
224
|
-
return res.json({ ok: true, service: 'esque-bridge' });
|
|
343
|
+
return res.json({ ok: true, service: 'esque-bridge', agent: AGENT_TYPE });
|
|
225
344
|
}
|
|
226
345
|
return next();
|
|
227
346
|
});
|
|
@@ -238,8 +357,6 @@ function requireAuth(req, res, next) {
|
|
|
238
357
|
next();
|
|
239
358
|
}
|
|
240
359
|
|
|
241
|
-
// The execute endpoint. Body: { prompt, sessionId?, pushToken? }.
|
|
242
|
-
// Returns: { text, status: 'finished' | 'blocked' }.
|
|
243
360
|
async function executeHandler(req, res) {
|
|
244
361
|
const body = req.body || {};
|
|
245
362
|
const prompt = String(body.prompt || '');
|
|
@@ -248,18 +365,14 @@ async function executeHandler(req, res) {
|
|
|
248
365
|
return res.status(400).json({ text: 'Empty prompt.', status: 'blocked' });
|
|
249
366
|
}
|
|
250
367
|
|
|
251
|
-
const
|
|
368
|
+
const prev = getCliSessionId(AGENT_TYPE, esqueSessionId);
|
|
252
369
|
const preview = prompt.slice(0, 80).replace(/\s+/g, ' ');
|
|
253
370
|
console.log(
|
|
254
|
-
`[bridge] POST ${preview}… esque=${esqueSessionId ?? '-'}
|
|
371
|
+
`[bridge] POST ${preview}… esque=${esqueSessionId ?? '-'} ${AGENT_TYPE}=${prev ?? 'new'}`,
|
|
255
372
|
);
|
|
256
373
|
|
|
257
374
|
try {
|
|
258
|
-
const result = await
|
|
259
|
-
if (result.sessionId && esqueSessionId) {
|
|
260
|
-
sessionMap[esqueSessionId] = result.sessionId;
|
|
261
|
-
saveSessions();
|
|
262
|
-
}
|
|
375
|
+
const result = await runAgent(prompt, esqueSessionId);
|
|
263
376
|
res.json({
|
|
264
377
|
text: result.text,
|
|
265
378
|
status: result.isError ? 'blocked' : 'finished',
|
|
@@ -268,17 +381,12 @@ async function executeHandler(req, res) {
|
|
|
268
381
|
console.error('[bridge] error:', err.message);
|
|
269
382
|
res
|
|
270
383
|
.status(500)
|
|
271
|
-
.json({ text:
|
|
384
|
+
.json({ text: `${adapter.label} failed: ${err.message}`, status: 'blocked' });
|
|
272
385
|
}
|
|
273
386
|
}
|
|
274
387
|
|
|
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
388
|
app.post('/execute', requireAuth, executeHandler);
|
|
279
389
|
app.post('/', requireAuth, executeHandler);
|
|
280
|
-
|
|
281
|
-
// Anything else: 405.
|
|
282
390
|
app.use((_req, res) => res.status(405).json({ error: 'POST only' }));
|
|
283
391
|
|
|
284
392
|
// --- Boot -----------------------------------------------------------------
|
|
@@ -296,30 +404,27 @@ async function main() {
|
|
|
296
404
|
tunnel = await localtunnel({ port: PORT, subdomain: LT_SUBDOMAIN });
|
|
297
405
|
} catch (err) {
|
|
298
406
|
console.error('Failed to open localtunnel:', err.message);
|
|
299
|
-
console.error(
|
|
300
|
-
'If localtunnel.me is blocked on your network, try cloudflared:',
|
|
301
|
-
);
|
|
407
|
+
console.error('If localtunnel.me is blocked, try cloudflared:');
|
|
302
408
|
console.error(` cloudflared tunnel --url http://localhost:${PORT}`);
|
|
303
409
|
process.exit(1);
|
|
304
410
|
}
|
|
305
411
|
|
|
306
|
-
const pairUrl = `esque://pair?url=${encodeURIComponent(tunnel.url)}&secret=${PAIRING_SECRET}`;
|
|
412
|
+
const pairUrl = `esque://pair?url=${encodeURIComponent(tunnel.url)}&secret=${PAIRING_SECRET}&agent=${AGENT_TYPE}`;
|
|
307
413
|
|
|
308
414
|
console.log('');
|
|
309
415
|
console.log('━'.repeat(68));
|
|
310
|
-
console.log(
|
|
416
|
+
console.log(` Esque Bridge Active · ${adapter.label}`);
|
|
311
417
|
console.log('━'.repeat(68));
|
|
312
418
|
console.log('');
|
|
313
|
-
console.log(
|
|
314
|
-
' Scan this QR code with the Esque Agent mobile app to pair your device.',
|
|
315
|
-
);
|
|
419
|
+
console.log(' Scan this QR code with the Esque Agent mobile app to pair.');
|
|
316
420
|
console.log('');
|
|
317
421
|
qrcode.generate(pairUrl, { small: true });
|
|
318
422
|
console.log('');
|
|
423
|
+
console.log(` Agent ${adapter.label} (${AGENT_TYPE})`);
|
|
319
424
|
console.log(` Local http://localhost:${PORT}`);
|
|
320
425
|
console.log(` Tunnel ${tunnel.url}`);
|
|
321
426
|
console.log(` Workdir ${WORKDIR}`);
|
|
322
|
-
console.log(`
|
|
427
|
+
console.log(` Binary ${AGENT_BIN ?? '(custom)'}`);
|
|
323
428
|
console.log(
|
|
324
429
|
` Pair secret ${PAIRING_SECRET.slice(0, 8)}… (rotates on restart — don't share)`,
|
|
325
430
|
);
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "esque-bridge",
|
|
3
|
-
"version": "0.
|
|
4
|
-
"description": "Desktop-side receiver for the Esque Agent mobile app. Pairs your phone with
|
|
3
|
+
"version": "0.2.0",
|
|
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
|
},
|