@openagents-org/agent-launcher 0.2.153 → 0.2.155
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 +97 -2
- package/src/daemon.js +27 -0
- package/src/index.js +4 -0
- package/src/node-config.js +72 -0
- package/src/workspace-client.js +30 -0
package/package.json
CHANGED
package/src/cli.js
CHANGED
|
@@ -396,6 +396,83 @@ 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
|
+
// (Re)start the daemon so it runs the CURRENT launcher build and loads
|
|
438
|
+
// the new node identity. A daemon started before the connect-a-node
|
|
439
|
+
// feature has no node-heartbeat loop, so reusing it would leave the node
|
|
440
|
+
// showing offline — recycle it rather than reuse it.
|
|
441
|
+
if (connector.getDaemonPid()) {
|
|
442
|
+
print(' Restarting daemon...');
|
|
443
|
+
connector.stopDaemon();
|
|
444
|
+
for (let i = 0; i < 25 && connector.getDaemonPid(); i++) {
|
|
445
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
446
|
+
}
|
|
447
|
+
} else {
|
|
448
|
+
print(' Starting daemon...');
|
|
449
|
+
}
|
|
450
|
+
connector.startDaemon();
|
|
451
|
+
print('');
|
|
452
|
+
print('Next: add an agent from the workspace, or run:');
|
|
453
|
+
print(' agn create <name> --type <type> --install');
|
|
454
|
+
} catch (e) {
|
|
455
|
+
print(`Error: ${e.message}`);
|
|
456
|
+
process.exitCode = 1;
|
|
457
|
+
}
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
if (sub === 'status') {
|
|
462
|
+
const n = nodeCfg.loadNode();
|
|
463
|
+
if (n && n.node_id) {
|
|
464
|
+
print(`Node connected: ${n.node_id}`);
|
|
465
|
+
print(` Workspace: ${n.workspace_slug || n.workspace_id}`);
|
|
466
|
+
} else {
|
|
467
|
+
print('No node connected. Run: agn node connect <pairing-code>');
|
|
468
|
+
}
|
|
469
|
+
return;
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
print('Usage: agn node <connect|status> [pairing-code]');
|
|
473
|
+
process.exitCode = 1;
|
|
474
|
+
}
|
|
475
|
+
|
|
399
476
|
async function cmdDisconnect(connector, _flags, positional) {
|
|
400
477
|
const name = positional[0];
|
|
401
478
|
if (!name) { print('Usage: agn disconnect <agent-name>'); return; }
|
|
@@ -705,7 +782,7 @@ async function cmdVersion() {
|
|
|
705
782
|
print(`${pkg.name} v${pkg.version}`);
|
|
706
783
|
}
|
|
707
784
|
|
|
708
|
-
async function cmdUpdate() {
|
|
785
|
+
async function cmdUpdate(connector) {
|
|
709
786
|
const { checkForUpdate, runUpdate, currentVersion } = require('./update-check');
|
|
710
787
|
const info = await checkForUpdate();
|
|
711
788
|
if (!info) {
|
|
@@ -725,6 +802,23 @@ async function cmdUpdate() {
|
|
|
725
802
|
return;
|
|
726
803
|
}
|
|
727
804
|
print(`Updated to ${info.latest}.`);
|
|
805
|
+
|
|
806
|
+
// Recycle a running daemon so the new build takes effect immediately. Without
|
|
807
|
+
// this, a long-lived daemon keeps executing the OLD in-memory code (and misses
|
|
808
|
+
// new features) until the next manual restart or reboot — the failure mode
|
|
809
|
+
// that left a connected node showing offline after its launcher was upgraded.
|
|
810
|
+
try {
|
|
811
|
+
if (connector && connector.getDaemonPid()) {
|
|
812
|
+
print('Restarting daemon to apply the update...');
|
|
813
|
+
connector.stopDaemon();
|
|
814
|
+
for (let i = 0; i < 25 && connector.getDaemonPid(); i++) {
|
|
815
|
+
await new Promise((resolve) => setTimeout(resolve, 200));
|
|
816
|
+
}
|
|
817
|
+
connector.startDaemon();
|
|
818
|
+
}
|
|
819
|
+
} catch (e) {
|
|
820
|
+
print(`Note: could not restart the daemon automatically (${e.message}). Run 'agn restart'.`);
|
|
821
|
+
}
|
|
728
822
|
}
|
|
729
823
|
|
|
730
824
|
async function cmdHelp() {
|
|
@@ -830,6 +924,7 @@ async function main() {
|
|
|
830
924
|
search: () => cmdSearch(connector, flags, positional),
|
|
831
925
|
runtimes: () => cmdRuntimes(connector),
|
|
832
926
|
connect: () => cmdConnect(connector, flags, positional),
|
|
927
|
+
node: () => cmdNode(connector, flags, positional),
|
|
833
928
|
disconnect: () => cmdDisconnect(connector, flags, positional),
|
|
834
929
|
logs: () => cmdLogs(connector, flags, positional),
|
|
835
930
|
autostart: () => cmdAutostart(connector, flags),
|
|
@@ -838,7 +933,7 @@ async function main() {
|
|
|
838
933
|
skills: () => cmdSkills(connector, flags, positional),
|
|
839
934
|
'tool-mode': () => cmdToolMode(connector, flags, positional),
|
|
840
935
|
'test-llm': () => cmdTestLLM(connector, flags, positional),
|
|
841
|
-
update: () => cmdUpdate(),
|
|
936
|
+
update: () => cmdUpdate(connector),
|
|
842
937
|
'mcp-server': () => {
|
|
843
938
|
const { runMcpServer } = require('./mcp-server');
|
|
844
939
|
const workspaceId = flags['workspace-id'] || process.env.OPENAGENTS_WORKSPACE_ID;
|
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) {
|
|
@@ -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/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
|
*
|