@remcp/remcp 0.2.18 → 0.2.20
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/package.json +1 -1
- package/src/cli.mjs +99 -13
package/package.json
CHANGED
package/src/cli.mjs
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import os from 'node:os';
|
|
3
3
|
import path from 'node:path';
|
|
4
4
|
import process from 'node:process';
|
|
5
|
-
import { spawnSync } from 'node:child_process';
|
|
5
|
+
import { spawn, spawnSync } from 'node:child_process';
|
|
6
6
|
import { randomUUID } from 'node:crypto';
|
|
7
7
|
import { localRuntimeEntry, runAgent, supervisorRestart } from './agent.mjs';
|
|
8
8
|
import { npmVersion, resolveNpm } from './npm.mjs';
|
|
@@ -217,6 +217,63 @@ function ensureServiceIfRecorded(config) {
|
|
|
217
217
|
}
|
|
218
218
|
}
|
|
219
219
|
|
|
220
|
+
// Opens the approval page in the person's browser. A machine that nobody is looking at only gets the
|
|
221
|
+
// printed URL, so every failure here is silent and non-fatal.
|
|
222
|
+
function openInBrowser(url) {
|
|
223
|
+
try {
|
|
224
|
+
const command = process.platform === 'darwin' ? 'open' : process.platform === 'win32' ? 'cmd' : 'xdg-open';
|
|
225
|
+
const args = process.platform === 'win32' ? ['/c', 'start', '', url] : [url];
|
|
226
|
+
const child = spawn(command, args, { stdio: 'ignore', detached: true });
|
|
227
|
+
child.on('error', () => {});
|
|
228
|
+
child.unref();
|
|
229
|
+
} catch {}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const sleep = ms => new Promise(resolve => setTimeout(resolve, ms));
|
|
233
|
+
|
|
234
|
+
// Device authorization (RFC 8628): the computer asks for a code, the person approves it in the
|
|
235
|
+
// browser while signed in, and this process collects the credential by polling. The device never
|
|
236
|
+
// sees a browser session or an account password.
|
|
237
|
+
async function pairWithDeviceCode(server, flags) {
|
|
238
|
+
const authorization = await fetch(`${server}/oauth/device_authorization`, {
|
|
239
|
+
method: 'POST',
|
|
240
|
+
headers: { 'content-type': 'application/json' },
|
|
241
|
+
body: JSON.stringify({
|
|
242
|
+
name: String(flags.name || os.hostname()),
|
|
243
|
+
hostname: os.hostname(),
|
|
244
|
+
platform: process.platform,
|
|
245
|
+
arch: process.arch,
|
|
246
|
+
machineId: ensureMachineId(),
|
|
247
|
+
}),
|
|
248
|
+
});
|
|
249
|
+
if (!authorization.ok) throw new Error(`Pairing failed (${authorization.status}): ${await authorization.text()}`);
|
|
250
|
+
const grant = await authorization.json();
|
|
251
|
+
const approvalUrl = grant.verification_uri_complete || grant.verification_uri;
|
|
252
|
+
console.log(`Approve this computer in your browser: ${approvalUrl}`);
|
|
253
|
+
console.log(`Pairing code: ${grant.user_code} (expires in ${Math.max(1, Math.round(Number(grant.expires_in || 600) / 60))} minutes)`);
|
|
254
|
+
openInBrowser(approvalUrl);
|
|
255
|
+
const deadline = Date.now() + (Number(grant.expires_in) || 600) * 1000;
|
|
256
|
+
const intervalMs = Math.max(1, Number(grant.interval) || 5) * 1000;
|
|
257
|
+
while (Date.now() < deadline) {
|
|
258
|
+
await sleep(intervalMs);
|
|
259
|
+
const response = await fetch(`${server}/oauth/token`, {
|
|
260
|
+
method: 'POST',
|
|
261
|
+
headers: { 'content-type': 'application/x-www-form-urlencoded' },
|
|
262
|
+
body: new URLSearchParams({
|
|
263
|
+
grant_type: 'urn:ietf:params:oauth:grant-type:device_code',
|
|
264
|
+
device_code: String(grant.device_code || ''),
|
|
265
|
+
}).toString(),
|
|
266
|
+
});
|
|
267
|
+
const data = await response.json().catch(() => ({}));
|
|
268
|
+
if (response.ok && data.device_token) return data;
|
|
269
|
+
if (data.error === 'authorization_pending' || data.error === 'slow_down') continue;
|
|
270
|
+
if (data.error === 'access_denied') throw new Error('That pairing request was denied in the browser. Run the command again if it was not you.');
|
|
271
|
+
if (data.error === 'expired_token') break;
|
|
272
|
+
throw new Error(`Pairing failed (${response.status}): ${JSON.stringify(data)}`);
|
|
273
|
+
}
|
|
274
|
+
throw new Error('The pairing code expired before it was approved. Run the command again.');
|
|
275
|
+
}
|
|
276
|
+
|
|
220
277
|
function restartPersistentServiceIfInstalled() {
|
|
221
278
|
const platform = servicePlatform();
|
|
222
279
|
if (platform === 'linux' && fs.existsSync(linuxServiceFile)) {
|
|
@@ -345,21 +402,30 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
345
402
|
}
|
|
346
403
|
|
|
347
404
|
if (command === 'connect') {
|
|
348
|
-
|
|
405
|
+
// Like the desktop-app flow this mirrors: with no flags the command talks to the official
|
|
406
|
+
// server, prints a code, opens the browser to approve it, and pairs. `--code` keeps working for
|
|
407
|
+
// the workspace-generated command and for CI, and `--server` for self-hosted deployments.
|
|
408
|
+
const server = String(flags.server || officialOrigin).replace(/\/$/, '');
|
|
349
409
|
const code = String(flags.code || '').replace(/\s+/g, '').toUpperCase();
|
|
350
|
-
if (!server || !code) throw new Error('--server and --code are required');
|
|
351
410
|
assertRuntimeTrust(server, flags);
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
411
|
+
let paired;
|
|
412
|
+
let deviceInitiated = false;
|
|
413
|
+
if (code) {
|
|
414
|
+
const response = await fetch(`${server}/api/pair/claim`, {
|
|
415
|
+
method: 'POST',
|
|
416
|
+
headers: { 'content-type': 'application/json' },
|
|
417
|
+
body: JSON.stringify({ code, machineId: ensureMachineId(), name: String(flags.name || os.hostname()), hostname: os.hostname(), platform: process.platform, arch: process.arch }),
|
|
418
|
+
});
|
|
419
|
+
if (!response.ok) throw new Error(`Pairing failed (${response.status}): ${await response.text()}`);
|
|
420
|
+
paired = await response.json();
|
|
421
|
+
} else {
|
|
422
|
+
deviceInitiated = true;
|
|
423
|
+
paired = await pairWithDeviceCode(server, flags);
|
|
424
|
+
}
|
|
359
425
|
const config = {
|
|
360
426
|
serverUrl: server,
|
|
361
|
-
deviceId: paired.deviceId,
|
|
362
|
-
deviceToken: paired.deviceToken,
|
|
427
|
+
deviceId: paired.deviceId || paired.device_id,
|
|
428
|
+
deviceToken: paired.deviceToken || paired.device_token,
|
|
363
429
|
deviceName: String(flags.name || os.hostname()),
|
|
364
430
|
runtime: normalizeRuntime(paired.runtime),
|
|
365
431
|
machineId: ensureMachineId(),
|
|
@@ -369,7 +435,27 @@ export async function main(argv = process.argv.slice(2)) {
|
|
|
369
435
|
};
|
|
370
436
|
saveConfig(config);
|
|
371
437
|
console.log(`Paired ${os.hostname()} with ${server}`);
|
|
372
|
-
if (flags.install)
|
|
438
|
+
if (flags.install) {
|
|
439
|
+
installPersistentAgent(config);
|
|
440
|
+
return;
|
|
441
|
+
}
|
|
442
|
+
// Only the command that asked for its own code keeps running: someone who ran `remcp connect` on
|
|
443
|
+
// a fresh machine expects the connection to be live when the command finishes. A workspace code
|
|
444
|
+
// keeps its old meaning (pair, then `remcp start` or `--install`).
|
|
445
|
+
if (!deviceInitiated) return;
|
|
446
|
+
// Nobody supervises this machine yet, so the agent runs in this window: Ctrl+C disconnects it,
|
|
447
|
+
// which is the behaviour people expect from a command they just ran themselves.
|
|
448
|
+
console.log('ReMCP is connected. Keep this window open, or run `remcp install` for a background service. Press Ctrl+C to stop.');
|
|
449
|
+
const telemetry = telemetryState();
|
|
450
|
+
await runAgent({
|
|
451
|
+
...config,
|
|
452
|
+
autoUpdate: config.autoUpdate !== false,
|
|
453
|
+
trustRuntime: config.trustRuntime === true,
|
|
454
|
+
telemetryEnabled: telemetry.enabled,
|
|
455
|
+
installReported: telemetry.installReported,
|
|
456
|
+
installSpec: `${PACKAGE_NAME}@${VERSION}`,
|
|
457
|
+
persistState: patch => saveConfig({ ...config, ...patch }),
|
|
458
|
+
});
|
|
373
459
|
return;
|
|
374
460
|
}
|
|
375
461
|
|