@openagents-org/agent-launcher 0.2.152 → 0.2.154
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.js +71 -0
- package/src/daemon.js +27 -0
- package/src/index.js +4 -0
- package/src/installer.js +59 -2
- package/src/node-config.js +72 -0
- package/src/paths.js +57 -0
- package/src/workspace-client.js +30 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -396,6 +396,76 @@ async function cmdConnect(connector, flags, positional) {
|
|
|
396
396
|
}
|
|
397
397
|
}
|
|
398
398
|
|
|
399
|
+
async function cmdNode(connector, flags, positional) {
|
|
400
|
+
const sub = positional[0] || 'status';
|
|
401
|
+
const nodeCfg = require('./node-config');
|
|
402
|
+
|
|
403
|
+
if (sub === 'connect') {
|
|
404
|
+
const code = positional[1] || flags.code;
|
|
405
|
+
if (!code) {
|
|
406
|
+
print('Usage: agn node connect <pairing-code>');
|
|
407
|
+
print('Get a pairing code from your workspace: Connect a Node.');
|
|
408
|
+
process.exitCode = 1;
|
|
409
|
+
return;
|
|
410
|
+
}
|
|
411
|
+
const info = nodeCfg.gatherDeviceInfo();
|
|
412
|
+
if (flags.type) info.deviceType = flags.type; // server | laptop | desktop
|
|
413
|
+
if (flags.name) info.name = flags.name;
|
|
414
|
+
|
|
415
|
+
print('Connecting this device to your workspace...');
|
|
416
|
+
try {
|
|
417
|
+
const res = await connector.redeemNodePairingCode(code, info);
|
|
418
|
+
nodeCfg.saveNode({
|
|
419
|
+
node_id: res.nodeId,
|
|
420
|
+
node_key: info.nodeKey,
|
|
421
|
+
workspace_id: res.workspaceId,
|
|
422
|
+
workspace_slug: res.workspaceSlug,
|
|
423
|
+
endpoint: connector.workspace.endpoint,
|
|
424
|
+
token: res.token,
|
|
425
|
+
});
|
|
426
|
+
// Register the workspace locally so agents created on this node can use it.
|
|
427
|
+
connector.config.addNetwork({
|
|
428
|
+
id: res.workspaceId,
|
|
429
|
+
slug: res.workspaceSlug,
|
|
430
|
+
name: res.workspaceName,
|
|
431
|
+
endpoint: connector.workspace.endpoint,
|
|
432
|
+
token: res.token,
|
|
433
|
+
});
|
|
434
|
+
print(`Node connected to workspace '${res.workspaceName}' (${res.workspaceSlug})`);
|
|
435
|
+
print(` This device: ${info.hostname} (${info.deviceType})`);
|
|
436
|
+
|
|
437
|
+
// Start the daemon so the node heartbeats and agents can run.
|
|
438
|
+
if (connector.getDaemonPid()) {
|
|
439
|
+
print(' Daemon already running.');
|
|
440
|
+
} else {
|
|
441
|
+
print(' Starting daemon...');
|
|
442
|
+
connector.startDaemon();
|
|
443
|
+
}
|
|
444
|
+
print('');
|
|
445
|
+
print('Next: add an agent from the workspace, or run:');
|
|
446
|
+
print(' agn create <name> --type <type> --install');
|
|
447
|
+
} catch (e) {
|
|
448
|
+
print(`Error: ${e.message}`);
|
|
449
|
+
process.exitCode = 1;
|
|
450
|
+
}
|
|
451
|
+
return;
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
if (sub === 'status') {
|
|
455
|
+
const n = nodeCfg.loadNode();
|
|
456
|
+
if (n && n.node_id) {
|
|
457
|
+
print(`Node connected: ${n.node_id}`);
|
|
458
|
+
print(` Workspace: ${n.workspace_slug || n.workspace_id}`);
|
|
459
|
+
} else {
|
|
460
|
+
print('No node connected. Run: agn node connect <pairing-code>');
|
|
461
|
+
}
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
print('Usage: agn node <connect|status> [pairing-code]');
|
|
466
|
+
process.exitCode = 1;
|
|
467
|
+
}
|
|
468
|
+
|
|
399
469
|
async function cmdDisconnect(connector, _flags, positional) {
|
|
400
470
|
const name = positional[0];
|
|
401
471
|
if (!name) { print('Usage: agn disconnect <agent-name>'); return; }
|
|
@@ -830,6 +900,7 @@ async function main() {
|
|
|
830
900
|
search: () => cmdSearch(connector, flags, positional),
|
|
831
901
|
runtimes: () => cmdRuntimes(connector),
|
|
832
902
|
connect: () => cmdConnect(connector, flags, positional),
|
|
903
|
+
node: () => cmdNode(connector, flags, positional),
|
|
833
904
|
disconnect: () => cmdDisconnect(connector, flags, positional),
|
|
834
905
|
logs: () => cmdLogs(connector, flags, positional),
|
|
835
906
|
autostart: () => cmdAutostart(connector, flags),
|
package/src/daemon.js
CHANGED
|
@@ -30,9 +30,31 @@ class Daemon {
|
|
|
30
30
|
this._shuttingDown = false;
|
|
31
31
|
this._statusInterval = null;
|
|
32
32
|
this._cmdInterval = null;
|
|
33
|
+
this._nodeHeartbeatInterval = null; // device-level heartbeat (connect-a-node)
|
|
34
|
+
this._nodeClient = null; // lazily-created WorkspaceClient for the node
|
|
33
35
|
this._reloadInFlight = null; // serialize concurrent _reload() calls
|
|
34
36
|
}
|
|
35
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Heartbeat this device (node) to its workspace, independent of any agent, so
|
|
40
|
+
* the workspace's "connected devices" view stays live even with zero agents.
|
|
41
|
+
* Best-effort: node liveness is non-critical to agent operation.
|
|
42
|
+
*/
|
|
43
|
+
async _nodeHeartbeat() {
|
|
44
|
+
try {
|
|
45
|
+
const nodeCfg = require('./node-config');
|
|
46
|
+
const n = nodeCfg.loadNode();
|
|
47
|
+
if (!n || !n.node_id || !n.token) return; // no node connected
|
|
48
|
+
if (!this._nodeClient) {
|
|
49
|
+
const { WorkspaceClient } = require('./workspace-client');
|
|
50
|
+
this._nodeClient = new WorkspaceClient(n.endpoint);
|
|
51
|
+
}
|
|
52
|
+
await this._nodeClient.nodeHeartbeat(n.node_id, n.token, nodeCfg.gatherDeviceInfo());
|
|
53
|
+
} catch {
|
|
54
|
+
// ignore — will retry next interval
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
36
58
|
// ---------------------------------------------------------------------------
|
|
37
59
|
// Public API
|
|
38
60
|
// ---------------------------------------------------------------------------
|
|
@@ -91,6 +113,10 @@ class Daemon {
|
|
|
91
113
|
this._processCommands();
|
|
92
114
|
}, 200);
|
|
93
115
|
|
|
116
|
+
// Device-level heartbeat (connect-a-node), independent of agents.
|
|
117
|
+
this._nodeHeartbeat();
|
|
118
|
+
this._nodeHeartbeatInterval = setInterval(() => this._nodeHeartbeat(), 30000);
|
|
119
|
+
|
|
94
120
|
// Watch config file for hot-reload
|
|
95
121
|
this._watchConfig();
|
|
96
122
|
|
|
@@ -116,6 +142,7 @@ class Daemon {
|
|
|
116
142
|
|
|
117
143
|
if (this._statusInterval) clearInterval(this._statusInterval);
|
|
118
144
|
if (this._cmdInterval) clearInterval(this._cmdInterval);
|
|
145
|
+
if (this._nodeHeartbeatInterval) clearInterval(this._nodeHeartbeatInterval);
|
|
119
146
|
if (this._configWatcher) { try { this._configWatcher.close(); } catch {} }
|
|
120
147
|
|
|
121
148
|
// Kill all child processes
|
package/src/index.js
CHANGED
|
@@ -275,6 +275,10 @@ class AgentConnector {
|
|
|
275
275
|
return this.workspace.resolveToken(token);
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
async redeemNodePairingCode(code, deviceInfo) {
|
|
279
|
+
return this.workspace.redeemPairingCode(code, deviceInfo);
|
|
280
|
+
}
|
|
281
|
+
|
|
278
282
|
// -- LLM test --
|
|
279
283
|
|
|
280
284
|
async testLLM(env) {
|
package/src/installer.js
CHANGED
|
@@ -4,7 +4,7 @@ const fs = require('fs');
|
|
|
4
4
|
const os = require('os');
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync, exec } = require('child_process');
|
|
7
|
-
const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs } = require('./paths');
|
|
7
|
+
const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs, resolveBinaryInKnownDirs } = require('./paths');
|
|
8
8
|
const { EnvManager } = require('./env');
|
|
9
9
|
const { readinessReason, REASON } = require('./adapters/health-status');
|
|
10
10
|
|
|
@@ -394,6 +394,44 @@ class Installer {
|
|
|
394
394
|
return found ? { path: found } : null;
|
|
395
395
|
}
|
|
396
396
|
|
|
397
|
+
/**
|
|
398
|
+
* Confirm a real cursor-agent binary landed, for verify-before-mark.
|
|
399
|
+
*
|
|
400
|
+
* Cursor's Windows one-liner (`irm 'https://cursor.com/install?win32=true' |
|
|
401
|
+
* iex`) downloads the CLI with Invoke-WebRequest. When that download fails —
|
|
402
|
+
* an "unexpected EOF or 0 bytes" IOException is what users behind a filtering
|
|
403
|
+
* network get — PowerShell treats it as NON-terminating: the script carries
|
|
404
|
+
* on, prints "Start using Cursor Agent" and "Happy coding!", and exits 0. All
|
|
405
|
+
* that's left on disk is an empty %LOCALAPPDATA%\cursor-agent\versions\.
|
|
406
|
+
*
|
|
407
|
+
* Same defect hermes and amp already guard against here. Without this the
|
|
408
|
+
* marker gets written, the launcher reports Cursor installed, offers a
|
|
409
|
+
* sign-in, and the only way the user finds out is a terminal saying
|
|
410
|
+
* "'cursor-agent' is not recognized".
|
|
411
|
+
*/
|
|
412
|
+
_verifyCursorBinary() {
|
|
413
|
+
try { clearBinaryLookupCache(); } catch {}
|
|
414
|
+
const resolved = this._whichBinary('cursor');
|
|
415
|
+
if (resolved && fs.existsSync(resolved)) return { path: resolved };
|
|
416
|
+
return null;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
_cursorBinaryNotFoundMessage() {
|
|
420
|
+
const localAppData =
|
|
421
|
+
process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
422
|
+
const isWin = process.platform === 'win32';
|
|
423
|
+
const expected = isWin
|
|
424
|
+
? path.join(localAppData, 'cursor-agent')
|
|
425
|
+
: path.join(os.homedir(), '.local', 'bin', 'cursor-agent');
|
|
426
|
+
return (
|
|
427
|
+
'Cursor install command completed, but the cursor-agent binary could not be found\n' +
|
|
428
|
+
"(its installer prints \"Happy coding!\" and exits 0 even when the download failed —\n" +
|
|
429
|
+
'look for "unexpected EOF" or a network error in the log above).\n\n' +
|
|
430
|
+
`Expected under:\n${expected}\n\n` +
|
|
431
|
+
'Retry the install, or check that cursor.com is reachable from this network.'
|
|
432
|
+
);
|
|
433
|
+
}
|
|
434
|
+
|
|
397
435
|
_hermesBinaryNotFoundMessage() {
|
|
398
436
|
const isWin = process.platform === 'win32';
|
|
399
437
|
const localAppData = process.env.LOCALAPPDATA || path.join(os.homedir(), 'AppData', 'Local');
|
|
@@ -1066,6 +1104,18 @@ class Installer {
|
|
|
1066
1104
|
}
|
|
1067
1105
|
if (onData) onData(`\nHermes CLI resolved: ${hermes.path}\n`);
|
|
1068
1106
|
}
|
|
1107
|
+
// Cursor-only: same failure shape as hermes above — its installer
|
|
1108
|
+
// exits 0 after a failed download. See _verifyCursorBinary.
|
|
1109
|
+
if (agentType === 'cursor') {
|
|
1110
|
+
const cursor = this._verifyCursorBinary();
|
|
1111
|
+
if (!cursor) {
|
|
1112
|
+
const msg = this._cursorBinaryNotFoundMessage();
|
|
1113
|
+
if (onData) onData(`\n${msg}\n`);
|
|
1114
|
+
reject(new Error(msg));
|
|
1115
|
+
return;
|
|
1116
|
+
}
|
|
1117
|
+
if (onData) onData(`\nCursor CLI resolved: ${cursor.path}\n`);
|
|
1118
|
+
}
|
|
1069
1119
|
this._markInstalled(agentType);
|
|
1070
1120
|
if (onData) onData(`\nDone! ${agentType} is now installed.\n`);
|
|
1071
1121
|
resolve({ success: true, command: displayCmd });
|
|
@@ -1296,7 +1346,14 @@ class Installer {
|
|
|
1296
1346
|
// runnable. Generic and backward-compatible — only runs after PATH misses.
|
|
1297
1347
|
const pkgBin = this._resolvePackageBin(agentType, entry, binary);
|
|
1298
1348
|
if (pkgBin) return pkgBin;
|
|
1299
|
-
|
|
1349
|
+
// Last: check the known install dirs on disk. A PATH lookup misses a CLI
|
|
1350
|
+
// whose installer only edited the *registry* PATH (Cursor, Amp, Hermes on
|
|
1351
|
+
// Windows) until the process restarts, and the launcher — which resolves
|
|
1352
|
+
// through here — then reports a working install as missing and offers a
|
|
1353
|
+
// terminal running a bare command that cannot resolve either. Each adapter
|
|
1354
|
+
// already does this search; this puts it in the one place the launcher
|
|
1355
|
+
// actually calls.
|
|
1356
|
+
return resolveBinaryInKnownDirs([binary, ...aliases], agentType);
|
|
1300
1357
|
}
|
|
1301
1358
|
|
|
1302
1359
|
/**
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Node identity for "connect a node" (~/.openagents/node.json).
|
|
5
|
+
*
|
|
6
|
+
* A node is this device's registration with a workspace, independent of any
|
|
7
|
+
* agent. Stores the stable device key, the assigned node id, and the workspace
|
|
8
|
+
* binding + token so the daemon can heartbeat the node.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const fs = require('fs');
|
|
12
|
+
const path = require('path');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const crypto = require('crypto');
|
|
15
|
+
|
|
16
|
+
const NODE_DIR = path.join(os.homedir(), '.openagents');
|
|
17
|
+
const NODE_FILE = path.join(NODE_DIR, 'node.json');
|
|
18
|
+
|
|
19
|
+
function loadNode() {
|
|
20
|
+
try {
|
|
21
|
+
if (fs.existsSync(NODE_FILE)) return JSON.parse(fs.readFileSync(NODE_FILE, 'utf-8'));
|
|
22
|
+
} catch {}
|
|
23
|
+
return null;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function saveNode(data) {
|
|
27
|
+
try {
|
|
28
|
+
fs.mkdirSync(NODE_DIR, { recursive: true });
|
|
29
|
+
fs.writeFileSync(NODE_FILE, JSON.stringify(data, null, 2));
|
|
30
|
+
try { fs.chmodSync(NODE_FILE, 0o600); } catch {} // holds the workspace token
|
|
31
|
+
} catch {}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Stable per-device id, generated once and persisted. */
|
|
35
|
+
function getOrCreateNodeKey() {
|
|
36
|
+
const existing = loadNode();
|
|
37
|
+
if (existing && existing.node_key) return existing.node_key;
|
|
38
|
+
const key = (crypto.randomUUID && crypto.randomUUID()) || crypto.randomBytes(16).toString('hex');
|
|
39
|
+
saveNode({ ...(existing || {}), node_key: key });
|
|
40
|
+
return key;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Best-effort device type from the platform (user/UI can override). */
|
|
44
|
+
function inferDeviceType() {
|
|
45
|
+
const p = os.platform();
|
|
46
|
+
if (p === 'darwin') return 'laptop';
|
|
47
|
+
if (p === 'win32') return 'desktop';
|
|
48
|
+
return 'server'; // linux is typically a server/VM in this context
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Device info sent on redeem/heartbeat. */
|
|
52
|
+
function gatherDeviceInfo() {
|
|
53
|
+
let launcherVersion = '';
|
|
54
|
+
try { launcherVersion = require('../package.json').version; } catch {}
|
|
55
|
+
return {
|
|
56
|
+
nodeKey: getOrCreateNodeKey(),
|
|
57
|
+
hostname: os.hostname(),
|
|
58
|
+
os: os.platform(),
|
|
59
|
+
deviceType: inferDeviceType(),
|
|
60
|
+
launcherVersion,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
module.exports = {
|
|
65
|
+
loadNode,
|
|
66
|
+
saveNode,
|
|
67
|
+
getOrCreateNodeKey,
|
|
68
|
+
inferDeviceType,
|
|
69
|
+
gatherDeviceInfo,
|
|
70
|
+
NODE_FILE,
|
|
71
|
+
NODE_DIR,
|
|
72
|
+
};
|
package/src/paths.js
CHANGED
|
@@ -517,12 +517,69 @@ function aiderBinDirs() {
|
|
|
517
517
|
return dirs;
|
|
518
518
|
}
|
|
519
519
|
|
|
520
|
+
/** Executable suffixes to try for a bare binary name, per platform. */
|
|
521
|
+
const BIN_EXTS = IS_WINDOWS ? ['.cmd', '.exe', '.bat', ''] : [''];
|
|
522
|
+
|
|
523
|
+
/**
|
|
524
|
+
* Last-resort binary lookup: walk the directories we already know agent CLIs
|
|
525
|
+
* land in and check the filesystem directly, instead of asking the shell.
|
|
526
|
+
*
|
|
527
|
+
* `where`/`which` is not enough on Windows. The Cursor, Amp and Hermes
|
|
528
|
+
* installers edit the *registry* PATH, which an already-running process never
|
|
529
|
+
* inherits — so a perfectly good install is invisible to a PATH lookup until
|
|
530
|
+
* the machine (or at least the app) is restarted. Every adapter grew its own
|
|
531
|
+
* tiered search to cope; the launcher had none, so it alone reported "can't
|
|
532
|
+
* find it" and then opened a terminal running a bare command that could only
|
|
533
|
+
* fail with "'cursor-agent' is not recognized".
|
|
534
|
+
*
|
|
535
|
+
* Deliberately reuses getExtraBinDirs() rather than a second hardcoded list:
|
|
536
|
+
* that list already IS the curated set, and a copy of it is exactly how the
|
|
537
|
+
* launcher and the adapters drifted apart in the first place.
|
|
538
|
+
*
|
|
539
|
+
* Runs only after a PATH lookup has already missed, so it can add resolutions
|
|
540
|
+
* but never change one that already works.
|
|
541
|
+
*
|
|
542
|
+
* @param {string|string[]} names binary name(s), e.g. ['cursor-agent','agent']
|
|
543
|
+
* @param {string} [agentType] adds that agent's isolated runtime bin dir
|
|
544
|
+
* @returns {string|null} absolute path, or null
|
|
545
|
+
*/
|
|
546
|
+
function resolveBinaryInKnownDirs(names, agentType) {
|
|
547
|
+
const list = (Array.isArray(names) ? names : [names]).filter(Boolean);
|
|
548
|
+
if (!list.length) return null;
|
|
549
|
+
|
|
550
|
+
const dirs = [];
|
|
551
|
+
const push = (d) => { if (d && !dirs.includes(d)) dirs.push(d); };
|
|
552
|
+
|
|
553
|
+
// The agent's own isolated runtime first — it is the copy this launcher
|
|
554
|
+
// installed, and should win over anything the user happens to have globally.
|
|
555
|
+
if (agentType) push(path.join(getRuntimePrefix(agentType), 'node_modules', '.bin'));
|
|
556
|
+
push(path.join(HOME, '.openagents', 'nodejs', 'node_modules', '.bin'));
|
|
557
|
+
try { push(path.dirname(process.execPath)); } catch {}
|
|
558
|
+
for (const d of getExtraBinDirs()) push(d);
|
|
559
|
+
|
|
560
|
+
for (const dir of dirs) {
|
|
561
|
+
for (const name of list) {
|
|
562
|
+
for (const ext of BIN_EXTS) {
|
|
563
|
+
const candidate = path.join(dir, `${name}${ext}`);
|
|
564
|
+
try {
|
|
565
|
+
if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) return candidate;
|
|
566
|
+
} catch {
|
|
567
|
+
/* unreadable path — keep looking */
|
|
568
|
+
}
|
|
569
|
+
}
|
|
570
|
+
}
|
|
571
|
+
}
|
|
572
|
+
return null;
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
|
|
520
576
|
module.exports = {
|
|
521
577
|
getExtraBinDirs,
|
|
522
578
|
getEnhancedPATH,
|
|
523
579
|
getEnhancedEnv,
|
|
524
580
|
whichBinary,
|
|
525
581
|
whereBinary,
|
|
582
|
+
resolveBinaryInKnownDirs,
|
|
526
583
|
clearBinaryLookupCache,
|
|
527
584
|
getRuntimePrefix,
|
|
528
585
|
getCorePrefix,
|
package/src/workspace-client.js
CHANGED
|
@@ -107,6 +107,36 @@ class WorkspaceClient {
|
|
|
107
107
|
return data.data || data;
|
|
108
108
|
}
|
|
109
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Redeem a node pairing code via POST /v1/nodes/redeem. The code is the
|
|
112
|
+
* credential (no auth header). Returns { nodeId, workspaceId, workspaceSlug,
|
|
113
|
+
* workspaceName, token } — the token is the workspace's machine credential.
|
|
114
|
+
*/
|
|
115
|
+
async redeemPairingCode(code, deviceInfo = {}) {
|
|
116
|
+
const body = { code, node_key: deviceInfo.nodeKey };
|
|
117
|
+
if (deviceInfo.name) body.name = deviceInfo.name;
|
|
118
|
+
if (deviceInfo.hostname) body.hostname = deviceInfo.hostname;
|
|
119
|
+
if (deviceInfo.deviceType) body.device_type = deviceInfo.deviceType;
|
|
120
|
+
if (deviceInfo.os) body.os = deviceInfo.os;
|
|
121
|
+
if (deviceInfo.launcherVersion) body.launcher_version = deviceInfo.launcherVersion;
|
|
122
|
+
const data = await this._post('/v1/nodes/redeem', body);
|
|
123
|
+
return data.data || data;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/**
|
|
127
|
+
* Node liveness via POST /v1/nodes/heartbeat (authenticated by the workspace
|
|
128
|
+
* token). Independent of any agent heartbeat.
|
|
129
|
+
*/
|
|
130
|
+
async nodeHeartbeat(nodeId, token, deviceInfo = {}) {
|
|
131
|
+
const body = { node_id: nodeId };
|
|
132
|
+
if (deviceInfo.hostname) body.hostname = deviceInfo.hostname;
|
|
133
|
+
if (deviceInfo.deviceType) body.device_type = deviceInfo.deviceType;
|
|
134
|
+
if (deviceInfo.os) body.os = deviceInfo.os;
|
|
135
|
+
if (deviceInfo.launcherVersion) body.launcher_version = deviceInfo.launcherVersion;
|
|
136
|
+
const data = await this._post('/v1/nodes/heartbeat', body, this._wsHeaders(token));
|
|
137
|
+
return data.data || data;
|
|
138
|
+
}
|
|
139
|
+
|
|
110
140
|
/**
|
|
111
141
|
* Send heartbeat via POST /v1/heartbeat.
|
|
112
142
|
*
|