@openagents-org/agent-launcher 0.2.142 → 0.2.145

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/src/agent-rows.js CHANGED
@@ -40,7 +40,16 @@ function loadAgentRows(connector) {
40
40
  let health = null;
41
41
  try {
42
42
  health = connector.healthCheck(agent.type || 'openclaw');
43
- if (health && !health.ready) notReadyMsg = health.message || 'Not configured';
43
+ if (health && !health.ready) {
44
+ // "Not installed" only when the executable is genuinely missing; an
45
+ // installed-but-signed-out agent shows its login/config message, never
46
+ // "Not installed". Mirrors the launcher's formatHealthLabel rule.
47
+ const notInstalled =
48
+ health.reason === 'not_installed' || health.installed === false;
49
+ notReadyMsg = notInstalled
50
+ ? 'Not installed'
51
+ : health.message || 'Not configured';
52
+ }
44
53
  } catch {}
45
54
 
46
55
  return {
@@ -55,6 +64,9 @@ function loadAgentRows(connector) {
55
64
  path: agent.path || '',
56
65
  network: agent.network || '',
57
66
  lastError: info.last_error || '',
67
+ // Structured daemon failure reason (spawn_failed / workspace_join_failed /
68
+ // heartbeat_failed / runtime_missing / …) paired with lastError.
69
+ errorReason: info.error_reason || '',
58
70
  notReadyMsg,
59
71
  health,
60
72
  configured: true,
@@ -0,0 +1,98 @@
1
+ 'use strict';
2
+
3
+ /**
4
+ * Pure, content-free helpers that turn an agent's registry `check_ready` plus a
5
+ * health-check result into human-readable authentication guidance.
6
+ *
7
+ * Used by the connector TUI (`showConfigureScreen`) and CLI (`agn env`) so an
8
+ * agent whose sign-in is an OAuth/credential FILE (e.g. Gemini's
9
+ * ~/.gemini/oauth_creds.json) is guided to authenticate instead of being
10
+ * mislabeled "No configuration required for this agent type".
11
+ *
12
+ * These helpers NEVER read, parse, copy, or print credential CONTENTS. They use
13
+ * only the agent's DECLARED metadata — the login command and env-var NAMES from
14
+ * `check_ready`, and the already-computed (non-sensitive) `message` / status
15
+ * fields the installer's health check returns. No token, key, email, or file
16
+ * path is ever emitted.
17
+ */
18
+
19
+ /**
20
+ * True when an agent declares any credential / sign-in capability in
21
+ * `check_ready` — an OAuth/credential file, API-key env vars, a service-account
22
+ * path env, or a login command. This is the OPT-IN gate: agents WITHOUT any of
23
+ * these are left exactly as before (the caller keeps showing "No configuration
24
+ * required"), so other agents are never polluted by Gemini's guidance.
25
+ *
26
+ * @param {object|null|undefined} checkReady
27
+ * @returns {boolean}
28
+ */
29
+ function hasCredentialMetadata(checkReady) {
30
+ if (!checkReady || typeof checkReady !== 'object') return false;
31
+ return !!(
32
+ checkReady.creds_file ||
33
+ (Array.isArray(checkReady.env_vars) && checkReady.env_vars.length) ||
34
+ (Array.isArray(checkReady.creds_path_env) && checkReady.creds_path_env.length) ||
35
+ checkReady.login_command
36
+ );
37
+ }
38
+
39
+ /**
40
+ * A short, NON-sensitive label for a ready auth_mode. Registry-overridable via
41
+ * `check_ready.auth_detected_labels` (e.g. Gemini maps cli_login → "Google
42
+ * account sign-in detected"). Falls back to a generic, agent-agnostic phrase so
43
+ * any future credential-file agent reads sensibly without extra config.
44
+ *
45
+ * @param {object} checkReady
46
+ * @param {string|null|undefined} authMode 'cli_login' | 'api_key' | other
47
+ * @returns {string}
48
+ */
49
+ function readyLabel(checkReady, authMode) {
50
+ const labels = (checkReady && checkReady.auth_detected_labels) || {};
51
+ if (authMode === 'cli_login') return labels.cli_login || 'CLI sign-in detected';
52
+ if (authMode === 'api_key') return labels.api_key || 'API key detected';
53
+ return labels[authMode] || 'authenticated';
54
+ }
55
+
56
+ /**
57
+ * Build display lines (plain text, no markup, no secrets) describing an agent's
58
+ * authentication state and the concrete next step. The caller is responsible
59
+ * for any terminal styling.
60
+ *
61
+ * @param {object} entry Registry catalog entry (has .label/.name/.check_ready)
62
+ * @param {object|null} health Result of installer.healthCheck(type)
63
+ * @returns {{ ready: boolean, status: string, lines: string[] }}
64
+ */
65
+ function formatAuthGuidance(entry, health) {
66
+ const checkReady = (entry && entry.check_ready) || {};
67
+ const label = (entry && (entry.label || entry.name)) || 'This agent';
68
+ const h = health || {};
69
+ const lines = [];
70
+
71
+ if (h.installed === false) {
72
+ lines.push(`${label} is not installed yet.`);
73
+ return { ready: false, status: 'not_installed', lines };
74
+ }
75
+
76
+ if (h.ready) {
77
+ lines.push(`Ready — ${readyLabel(checkReady, h.auth_mode)}.`);
78
+ return { ready: true, status: h.auth_status || 'ready', lines };
79
+ }
80
+
81
+ // Not ready: lead with the registry's own (non-sensitive) reason, then the
82
+ // concrete next steps derived from the DECLARED login command + env-var names.
83
+ if (h.message) lines.push(h.message);
84
+ const loginCmd = checkReady.login_command;
85
+ if (loginCmd) {
86
+ lines.push(
87
+ `Run \`${loginCmd}\` in your terminal and complete sign-in, then return here and refresh the agent status.`,
88
+ );
89
+ }
90
+ const envVars = Array.isArray(checkReady.env_vars) ? checkReady.env_vars : [];
91
+ if (envVars.length) {
92
+ lines.push(`Alternatively, configure ${envVars.join(' or ')}.`);
93
+ }
94
+ if (!lines.length) lines.push(`${label} requires authentication before use.`);
95
+ return { ready: false, status: h.auth_status || 'no_credentials', lines };
96
+ }
97
+
98
+ module.exports = { hasCredentialMetadata, readyLabel, formatAuthGuidance };
package/src/cli.js CHANGED
@@ -1,6 +1,7 @@
1
1
  'use strict';
2
2
 
3
3
  const { AgentConnector, Daemon } = require('./index');
4
+ const { hasCredentialMetadata, formatAuthGuidance } = require('./auth-guidance');
4
5
 
5
6
  // ---------------------------------------------------------------------------
6
7
  // Arg parsing
@@ -41,6 +42,9 @@ function parseArgs(argv) {
41
42
  function getConnector(flags) {
42
43
  const opts = {};
43
44
  if (flags.config) opts.configDir = flags.config;
45
+ if (flags.endpoint || process.env.OPENAGENTS_ENDPOINT) {
46
+ opts.workspaceEndpoint = flags.endpoint || process.env.OPENAGENTS_ENDPOINT;
47
+ }
44
48
  return new AgentConnector(opts);
45
49
  }
46
50
 
@@ -358,6 +362,8 @@ async function cmdConnect(connector, flags, positional) {
358
362
  if (pid) {
359
363
  connector.sendDaemonCommand(`restart:${name}`);
360
364
  print('Daemon notified');
365
+ } else {
366
+ print('Daemon is not running. Run `agn up` to bring this agent online.');
361
367
  }
362
368
  } catch (e) {
363
369
  print(`Error: ${e.message}`);
@@ -478,14 +484,30 @@ async function cmdEnv(connector, flags, positional) {
478
484
  const display = field.password && val ? '***' : (val || '(not set)');
479
485
  print(` ${field.name}: ${display} ${field.required ? '(required)' : ''}`);
480
486
  }
487
+ return;
488
+ }
489
+
490
+ // No env_config, but the agent may still require a sign-in/credential (e.g.
491
+ // Gemini's OAuth login). Surface readiness + login guidance from the registry
492
+ // check_ready rather than a misleading "No env vars configured". Gated on
493
+ // credential metadata so plain no-config agents keep their existing output.
494
+ const entry = connector.registry.getEntry(type);
495
+ if (hasCredentialMetadata(entry && entry.check_ready)) {
496
+ let health = null;
497
+ try { health = connector.healthCheck(type); } catch {}
498
+ const g = formatAuthGuidance(entry, health);
499
+ print(`${entry.label || type} authentication status: ${g.ready ? 'Ready' : 'Not ready'}`);
500
+ print('');
501
+ for (const line of g.lines) print(line);
502
+ return;
503
+ }
504
+
505
+ const entries = Object.entries(env);
506
+ if (entries.length === 0) {
507
+ print(`No env vars configured for ${type}`);
481
508
  } else {
482
- const entries = Object.entries(env);
483
- if (entries.length === 0) {
484
- print(`No env vars configured for ${type}`);
485
- } else {
486
- for (const [k, v] of entries) {
487
- print(` ${k}: ${v}`);
488
- }
509
+ for (const [k, v] of entries) {
510
+ print(` ${k}: ${v}`);
489
511
  }
490
512
  }
491
513
  }
package/src/config.js CHANGED
@@ -426,8 +426,14 @@ function parseYamlValue(val) {
426
426
  if (val === 'true') return true;
427
427
  if (val === 'false') return false;
428
428
  if (/^\d+$/.test(val)) return parseInt(val, 10);
429
- if ((val.startsWith("'") && val.endsWith("'")) ||
430
- (val.startsWith('"') && val.endsWith('"'))) {
429
+ if (val.startsWith('"') && val.endsWith('"')) {
430
+ // Reverse serializeYamlValue's escaping (\\ → \, \" → "). Without this a
431
+ // double-quoted Windows path like "D:\重要资料" (quoted because of the drive
432
+ // colon) round-trips to D:\\重要资料 and accumulates an extra backslash on
433
+ // every save/load cycle, corrupting the agent's working dir.
434
+ return val.slice(1, -1).replace(/\\(["\\])/g, '$1');
435
+ }
436
+ if (val.startsWith("'") && val.endsWith("'")) {
431
437
  return val.slice(1, -1);
432
438
  }
433
439
  if (val.startsWith('{') || val.startsWith('[')) {
package/src/daemon.js CHANGED
@@ -210,6 +210,7 @@ class Daemon {
210
210
  restarts: info.restarts,
211
211
  started_at: info.startedAt || null,
212
212
  last_error: info.lastError || null,
213
+ error_reason: info.errorReason || null,
213
214
  };
214
215
  }
215
216
  return result;
@@ -327,7 +328,17 @@ class Daemon {
327
328
  * Read daemon PID, returning null if not running.
328
329
  */
329
330
  static readDaemonPid(configDir) {
330
- return Daemon._readPid(path.join(configDir, 'daemon.pid'));
331
+ const pidFile = path.join(configDir, 'daemon.pid');
332
+ const statusFile = path.join(configDir, 'daemon.status.json');
333
+ const pid = Daemon._readPid(pidFile);
334
+ if (!pid) return null;
335
+
336
+ if (!Daemon._isAlive(pid)) {
337
+ Daemon._cleanupStaleDaemonFiles(pidFile, statusFile);
338
+ return null;
339
+ }
340
+
341
+ return pid;
331
342
  }
332
343
 
333
344
  // ---------------------------------------------------------------------------
@@ -353,6 +364,9 @@ class Daemon {
353
364
  restarts: 0,
354
365
  startedAt: null,
355
366
  lastError: null,
367
+ // Structured failure reason (health-status REASON) paired with lastError,
368
+ // so the UI/TUI can classify "why" without parsing the message.
369
+ errorReason: null,
356
370
  proc: null,
357
371
  _backoff: 2,
358
372
  };
@@ -462,9 +476,33 @@ class Daemon {
462
476
  // Internal — adapter loop (workspace-connected agents)
463
477
  // ---------------------------------------------------------------------------
464
478
 
479
+ /**
480
+ * Apply a live status update reported by a running adapter (join/heartbeat
481
+ * health). A genuine failure reason → state 'error' + redacted last_error so
482
+ * the Agents list / TUI show the real cause; a null reason → recovered, clear
483
+ * the error. Ignored once the agent is stopping (a user stop must win).
484
+ */
485
+ _applyAdapterStatus(name, info, update) {
486
+ if (!update || this._shuttingDown || this._stoppedAgents.has(name)) return;
487
+ const { isErrorReason, redactDiagnostic } = require('./adapters/health-status');
488
+ const reason = update.reason || null;
489
+ if (reason && isErrorReason(reason)) {
490
+ info.state = 'error';
491
+ info.errorReason = reason;
492
+ info.lastError = redactDiagnostic(update.message || reason);
493
+ } else {
494
+ // Recovered / healthy again — clear any prior connectivity error.
495
+ if (info.state === 'error') info.state = 'running';
496
+ info.errorReason = null;
497
+ info.lastError = null;
498
+ }
499
+ this._writeStatus();
500
+ }
501
+
465
502
  async _adapterLoop(name, agentCfg, info, network) {
466
503
  const { createAdapter } = require('./adapters');
467
504
  const { skillsToDisabledModules } = require('./skill-catalog');
505
+ const { redactDiagnostic } = require('./adapters/health-status');
468
506
  const agentType = agentCfg.type || 'openclaw';
469
507
  const endpoint = network.endpoint || 'https://workspace-endpoint.openagents.org';
470
508
 
@@ -491,20 +529,46 @@ class Daemon {
491
529
  // .claude/skills there failed with EPERM. Root it under ~/.openagents.
492
530
  workingDir: agentCfg.path || defaultAgentWorkdir(name),
493
531
  toolMode: agentCfg.tool_mode || 'skills',
532
+ // Live runtime/connectivity status → daemon.status.json (Agents list/TUI).
533
+ onStatus: (update) => this._applyAdapterStatus(name, info, update),
494
534
  });
495
535
  } catch (e) {
496
- this._log(`${name} failed to create ${agentType} adapter: ${e.message}`);
536
+ // A construction failure (e.g. unknown agent type in this core) is a hard
537
+ // error with a real cause — surface it (redacted), do not pretend stopped.
497
538
  info.state = 'error';
498
- info.lastError = e.message;
539
+ info.errorReason = 'adapter_crashed';
540
+ info.lastError = redactDiagnostic(e.message || String(e));
541
+ this._log(`${name} failed to create ${agentType} adapter: ${info.lastError}`);
499
542
  this._writeStatus();
500
543
  return;
501
544
  }
502
545
 
546
+ // Preflight: when the agent's runtime binary is genuinely missing, surface a
547
+ // precise reason ('runtime_missing') and DO NOT join the workspace — there is
548
+ // no point spinning a join/poll loop that can never run the CLI. Adapters
549
+ // that don't override preflight() always pass.
550
+ try {
551
+ const pf =
552
+ typeof adapter.preflight === 'function' ? adapter.preflight() : null;
553
+ if (pf && pf.ok === false) {
554
+ info.state = 'error';
555
+ info.errorReason = pf.reason || 'runtime_missing';
556
+ info.lastError = redactDiagnostic(pf.message || 'runtime not available');
557
+ this._writeStatus();
558
+ this._log(`${name} preflight failed (${info.errorReason}): ${info.lastError}`);
559
+ return;
560
+ }
561
+ } catch (e) {
562
+ this._log(`${name} preflight error (ignored, continuing): ${e.message}`);
563
+ }
564
+
503
565
  // Store adapter reference for stop and duplicate detection
504
566
  this._adapters[name] = adapter;
505
567
 
506
568
  info.state = 'running';
507
569
  info.startedAt = new Date().toISOString();
570
+ info.lastError = null;
571
+ info.errorReason = null;
508
572
  this._writeStatus();
509
573
  this._log(`${name} adapter online → ${network.slug} (type: ${agentType})`);
510
574
 
@@ -521,14 +585,41 @@ class Daemon {
521
585
  await adapter.run();
522
586
  clearInterval(checkStop);
523
587
  } catch (e) {
524
- info.lastError = (e.message || String(e)).slice(0, 200);
588
+ info.errorReason = info.errorReason || 'adapter_crashed';
589
+ info.lastError = redactDiagnostic((e.message || String(e)).slice(0, 200));
525
590
  this._log(`${name} adapter error: ${info.lastError}`);
526
591
  }
527
592
 
528
593
  delete this._adapters[name];
529
- info.state = 'stopped';
594
+
595
+ // Final state: a clean stop (user stop / daemon shutdown) is NEVER an error.
596
+ // A terminal failure recorded by the adapter (join/heartbeat/session-revoked)
597
+ // or a crash surfaces as 'error' with its classified, redacted reason.
598
+ const userStopped =
599
+ this._shuttingDown ||
600
+ this._stoppedAgents.has(name) ||
601
+ (typeof adapter.wasStopRequested === 'function' && adapter.wasStopRequested());
602
+ if (userStopped) {
603
+ info.state = 'stopped';
604
+ info.lastError = null;
605
+ info.errorReason = null;
606
+ } else {
607
+ const exit =
608
+ (typeof adapter.getExitInfo === 'function' && adapter.getExitInfo()) ||
609
+ null;
610
+ if (exit && exit.reason) {
611
+ info.state = 'error';
612
+ info.errorReason = exit.reason;
613
+ info.lastError = redactDiagnostic(exit.message || exit.reason);
614
+ } else if (info.errorReason) {
615
+ // A crash, or a live error report that never recovered — keep it visible.
616
+ info.state = 'error';
617
+ } else {
618
+ info.state = 'stopped';
619
+ }
620
+ }
530
621
  this._writeStatus();
531
- this._log(`${name} adapter stopped`);
622
+ this._log(`${name} adapter stopped (state: ${info.state})`);
532
623
  }
533
624
 
534
625
  // NOTE: Adapter-specific message handling (openclaw, claude, codex)
@@ -873,6 +964,17 @@ class Daemon {
873
964
  }
874
965
  }
875
966
 
967
+ static _cleanupStaleDaemonFiles(pidFile, statusFile) {
968
+ Daemon._unlinkIfExists(pidFile);
969
+ Daemon._unlinkIfExists(statusFile);
970
+ }
971
+
972
+ static _unlinkIfExists(filePath) {
973
+ if (fs.existsSync(filePath)) {
974
+ fs.unlinkSync(filePath);
975
+ }
976
+ }
977
+
876
978
  static _isAlive(pid) {
877
979
  try {
878
980
  process.kill(pid, 0);