@openagents-org/agent-launcher 0.2.157 → 0.2.158

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.157",
3
+ "version": "0.2.158",
4
4
  "description": "OpenAgents Launcher \u2014 install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/src/cli.js CHANGED
@@ -311,7 +311,28 @@ async function cmdList(connector) {
311
311
  table(rows, ['NAME', 'TYPE', 'ROLE', 'NETWORK']);
312
312
  }
313
313
 
314
- async function cmdRuntimes(connector) {
314
+ async function cmdRuntimes(connector, flags) {
315
+ // Machine-readable detection for every supported type: installed? logged-in?
316
+ // Used by the daemon to report node runtimes to the workspace (Add-agent
317
+ // gallery). Emits ONLY JSON on stdout.
318
+ if (flags && flags.json) {
319
+ const runtimes = connector.registry.getCatalogSync().map((e) => {
320
+ let h;
321
+ try { h = connector.healthCheck(e.name); } catch { h = {}; }
322
+ return {
323
+ type: e.name,
324
+ installed: !!h.installed,
325
+ ready: !!h.ready,
326
+ version: h.version || null,
327
+ reason: h.reason || null,
328
+ message: h.message || null,
329
+ authStatus: h.auth_status || null,
330
+ };
331
+ });
332
+ process.stdout.write(JSON.stringify(runtimes));
333
+ return;
334
+ }
335
+
315
336
  let catalog;
316
337
  try {
317
338
  catalog = await connector.getCatalog();
@@ -922,7 +943,7 @@ async function main() {
922
943
  install: () => cmdInstall(connector, flags, positional),
923
944
  uninstall: () => cmdUninstall(connector, flags, positional),
924
945
  search: () => cmdSearch(connector, flags, positional),
925
- runtimes: () => cmdRuntimes(connector),
946
+ runtimes: () => cmdRuntimes(connector, flags),
926
947
  connect: () => cmdConnect(connector, flags, positional),
927
948
  node: () => cmdNode(connector, flags, positional),
928
949
  disconnect: () => cmdDisconnect(connector, flags, positional),
package/src/daemon.js CHANGED
@@ -33,6 +33,8 @@ class Daemon {
33
33
  this._nodeHeartbeatInterval = null; // device-level heartbeat (connect-a-node)
34
34
  this._nodeClient = null; // lazily-created WorkspaceClient for the node
35
35
  this._runningCommands = new Set(); // node-command ids currently executing
36
+ this._runtimes = []; // detected agent runtimes for the node view
37
+ this._runtimesInterval = null;
36
38
  this._reloadInFlight = null; // serialize concurrent _reload() calls
37
39
  }
38
40
 
@@ -49,7 +51,7 @@ class Daemon {
49
51
  if (!this._nodeClient) {
50
52
  this._nodeClient = new WorkspaceClient(n.endpoint);
51
53
  }
52
- const info = { ...nodeCfg.gatherDeviceInfo(), agents: this._buildRoster() };
54
+ const info = { ...nodeCfg.gatherDeviceInfo(), agents: this._buildRoster(), runtimes: this._runtimes };
53
55
  const resp = await this._nodeClient.nodeHeartbeat(n.node_id, n.token, info);
54
56
  // The heartbeat response is our push channel: run any queued remote
55
57
  // agent-management commands the workspace enqueued for this node. Fire
@@ -86,6 +88,24 @@ class Daemon {
86
88
  return roster;
87
89
  }
88
90
 
91
+ /**
92
+ * Detect, for every supported agent type, whether its runtime is installed and
93
+ * logged-in/ready on this device. Runs `agn runtimes --json` in a CHILD process
94
+ * so the (synchronous, execSync-heavy) version/login probes never block the
95
+ * daemon's event loop. Result is reported to the workspace on the heartbeat.
96
+ */
97
+ async _refreshRuntimes() {
98
+ try {
99
+ const r = await this._runAgn(['runtimes', '--json']);
100
+ if (r.code === 0 && r.stdout) {
101
+ const parsed = JSON.parse(r.stdout.trim());
102
+ if (Array.isArray(parsed)) this._runtimes = parsed;
103
+ }
104
+ } catch {
105
+ // keep the previous snapshot on failure
106
+ }
107
+ }
108
+
89
109
  /**
90
110
  * Execute one remote command by shelling out to this same launcher's CLI, so
91
111
  * all of create/install/connect/start/stop/remove reuses the exact code path a
@@ -100,7 +120,9 @@ class Daemon {
100
120
  try {
101
121
  if (action === 'create_agent') {
102
122
  const type = (args.type || '').trim();
103
- const r1 = await this._runAgn(['create', name, '--type', type, '--install']);
123
+ const createArgs = ['create', name, '--type', type, '--install'];
124
+ if (args.workingDir) createArgs.push('--path', String(args.workingDir));
125
+ const r1 = await this._runAgn(createArgs);
104
126
  if (r1.code !== 0) throw new Error(r1.stderr || r1.stdout || 'create failed');
105
127
  // Optional credentials for API-key agents (generic → provider mapping
106
128
  // happens in env resolution).
@@ -124,6 +146,13 @@ class Daemon {
124
146
  const r = await this._runAgn(['remove', name]);
125
147
  ok = r.code === 0;
126
148
  message = ok ? `Agent '${name}' removed` : (r.stderr || r.stdout || 'remove failed');
149
+ } else if (action === 'detect_runtimes') {
150
+ await this._refreshRuntimes();
151
+ // Push the fresh detection right away rather than waiting for the next
152
+ // heartbeat, so the workspace's Add-agent gallery updates promptly.
153
+ this._nodeHeartbeat();
154
+ ok = true;
155
+ message = `Detected ${this._runtimes.length} runtime(s)`;
127
156
  } else {
128
157
  message = `Unknown action '${action}'`;
129
158
  }
@@ -226,6 +255,11 @@ class Daemon {
226
255
  this._nodeHeartbeat();
227
256
  this._nodeHeartbeatInterval = setInterval(() => this._nodeHeartbeat(), 10000);
228
257
 
258
+ // Detect installed/logged-in agent runtimes for the Add-agent gallery. Runs
259
+ // in a child process (off the event loop), refreshed periodically.
260
+ this._refreshRuntimes();
261
+ this._runtimesInterval = setInterval(() => this._refreshRuntimes(), 120000);
262
+
229
263
  // Watch config file for hot-reload
230
264
  this._watchConfig();
231
265
 
@@ -252,6 +286,7 @@ class Daemon {
252
286
  if (this._statusInterval) clearInterval(this._statusInterval);
253
287
  if (this._cmdInterval) clearInterval(this._cmdInterval);
254
288
  if (this._nodeHeartbeatInterval) clearInterval(this._nodeHeartbeatInterval);
289
+ if (this._runtimesInterval) clearInterval(this._runtimesInterval);
255
290
  if (this._configWatcher) { try { this._configWatcher.close(); } catch {} }
256
291
 
257
292
  // Kill all child processes
@@ -134,6 +134,7 @@ class WorkspaceClient {
134
134
  if (deviceInfo.os) body.os = deviceInfo.os;
135
135
  if (deviceInfo.launcherVersion) body.launcher_version = deviceInfo.launcherVersion;
136
136
  if (Array.isArray(deviceInfo.agents)) body.agents = deviceInfo.agents;
137
+ if (Array.isArray(deviceInfo.runtimes)) body.runtimes = deviceInfo.runtimes;
137
138
  const data = await this._post('/v1/nodes/heartbeat', body, this._wsHeaders(token));
138
139
  return data.data || data;
139
140
  }