@openagents-org/agent-launcher 0.2.156 → 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.156",
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
@@ -67,13 +69,18 @@ class Daemon {
67
69
  }
68
70
  }
69
71
 
70
- /** Roster of agents this node hosts, for the workspace's node view. */
72
+ /**
73
+ * Roster of agents this node hosts, for the workspace's node view. Sourced
74
+ * from config (the source of truth for what's configured) and augmented with
75
+ * live process state, so a removed agent drops off immediately rather than
76
+ * lingering as a stale 'stopped' process entry.
77
+ */
71
78
  _buildRoster() {
72
79
  const roster = [];
73
80
  try {
74
- const status = this.getStatus();
75
- for (const [name, info] of Object.entries(status)) {
76
- roster.push({ name, type: info.type || 'unknown', status: info.state || 'unknown' });
81
+ for (const a of this.config.getAgents()) {
82
+ const proc = this._processes[a.name];
83
+ roster.push({ name: a.name, type: a.type || 'unknown', status: (proc && proc.state) || 'stopped' });
77
84
  }
78
85
  } catch {
79
86
  // best-effort
@@ -81,6 +88,24 @@ class Daemon {
81
88
  return roster;
82
89
  }
83
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
+
84
109
  /**
85
110
  * Execute one remote command by shelling out to this same launcher's CLI, so
86
111
  * all of create/install/connect/start/stop/remove reuses the exact code path a
@@ -95,7 +120,9 @@ class Daemon {
95
120
  try {
96
121
  if (action === 'create_agent') {
97
122
  const type = (args.type || '').trim();
98
- 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);
99
126
  if (r1.code !== 0) throw new Error(r1.stderr || r1.stdout || 'create failed');
100
127
  // Optional credentials for API-key agents (generic → provider mapping
101
128
  // happens in env resolution).
@@ -119,6 +146,13 @@ class Daemon {
119
146
  const r = await this._runAgn(['remove', name]);
120
147
  ok = r.code === 0;
121
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)`;
122
156
  } else {
123
157
  message = `Unknown action '${action}'`;
124
158
  }
@@ -221,6 +255,11 @@ class Daemon {
221
255
  this._nodeHeartbeat();
222
256
  this._nodeHeartbeatInterval = setInterval(() => this._nodeHeartbeat(), 10000);
223
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
+
224
263
  // Watch config file for hot-reload
225
264
  this._watchConfig();
226
265
 
@@ -247,6 +286,7 @@ class Daemon {
247
286
  if (this._statusInterval) clearInterval(this._statusInterval);
248
287
  if (this._cmdInterval) clearInterval(this._cmdInterval);
249
288
  if (this._nodeHeartbeatInterval) clearInterval(this._nodeHeartbeatInterval);
289
+ if (this._runtimesInterval) clearInterval(this._runtimesInterval);
250
290
  if (this._configWatcher) { try { this._configWatcher.close(); } catch {} }
251
291
 
252
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
  }