@openagents-org/agent-launcher 0.2.143 → 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/package.json +1 -1
- package/registry.json +274 -66
- package/src/adapters/aider.js +634 -0
- package/src/adapters/amp.js +477 -0
- package/src/adapters/base.js +108 -7
- package/src/adapters/claude.js +36 -34
- package/src/adapters/cline-stream.js +632 -0
- package/src/adapters/cline.js +914 -0
- package/src/adapters/codex.js +14 -16
- package/src/adapters/copilot-stream-parser.js +380 -0
- package/src/adapters/copilot.js +796 -0
- package/src/adapters/cursor.js +9 -15
- package/src/adapters/gemini.js +10 -12
- package/src/adapters/goose-stream.js +268 -0
- package/src/adapters/goose.js +649 -0
- package/src/adapters/health-status.js +166 -0
- package/src/adapters/hermes.js +8 -17
- package/src/adapters/index.js +16 -1
- package/src/adapters/nanoclaw-bridge.js +264 -0
- package/src/adapters/nanoclaw-channel-install.js +411 -0
- package/src/adapters/nanoclaw-control.js +491 -0
- package/src/adapters/nanoclaw-delivery-store.js +0 -0
- package/src/adapters/nanoclaw-protocol.js +349 -0
- package/src/adapters/nanoclaw.js +661 -9
- package/src/adapters/opencode.js +541 -75
- package/src/agent-rows.js +13 -1
- package/src/auth-guidance.js +98 -0
- package/src/cli.js +24 -7
- package/src/config.js +8 -2
- package/src/daemon.js +86 -5
- package/src/installer.js +509 -10
- package/src/paths.js +125 -2
- package/src/tui.js +29 -2
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)
|
|
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
|
|
@@ -483,14 +484,30 @@ async function cmdEnv(connector, flags, positional) {
|
|
|
483
484
|
const display = field.password && val ? '***' : (val || '(not set)');
|
|
484
485
|
print(` ${field.name}: ${display} ${field.required ? '(required)' : ''}`);
|
|
485
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}`);
|
|
486
508
|
} else {
|
|
487
|
-
const
|
|
488
|
-
|
|
489
|
-
print(`No env vars configured for ${type}`);
|
|
490
|
-
} else {
|
|
491
|
-
for (const [k, v] of entries) {
|
|
492
|
-
print(` ${k}: ${v}`);
|
|
493
|
-
}
|
|
509
|
+
for (const [k, v] of entries) {
|
|
510
|
+
print(` ${k}: ${v}`);
|
|
494
511
|
}
|
|
495
512
|
}
|
|
496
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 (
|
|
430
|
-
|
|
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;
|
|
@@ -363,6 +364,9 @@ class Daemon {
|
|
|
363
364
|
restarts: 0,
|
|
364
365
|
startedAt: null,
|
|
365
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,
|
|
366
370
|
proc: null,
|
|
367
371
|
_backoff: 2,
|
|
368
372
|
};
|
|
@@ -472,9 +476,33 @@ class Daemon {
|
|
|
472
476
|
// Internal — adapter loop (workspace-connected agents)
|
|
473
477
|
// ---------------------------------------------------------------------------
|
|
474
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
|
+
|
|
475
502
|
async _adapterLoop(name, agentCfg, info, network) {
|
|
476
503
|
const { createAdapter } = require('./adapters');
|
|
477
504
|
const { skillsToDisabledModules } = require('./skill-catalog');
|
|
505
|
+
const { redactDiagnostic } = require('./adapters/health-status');
|
|
478
506
|
const agentType = agentCfg.type || 'openclaw';
|
|
479
507
|
const endpoint = network.endpoint || 'https://workspace-endpoint.openagents.org';
|
|
480
508
|
|
|
@@ -501,20 +529,46 @@ class Daemon {
|
|
|
501
529
|
// .claude/skills there failed with EPERM. Root it under ~/.openagents.
|
|
502
530
|
workingDir: agentCfg.path || defaultAgentWorkdir(name),
|
|
503
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),
|
|
504
534
|
});
|
|
505
535
|
} catch (e) {
|
|
506
|
-
|
|
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.
|
|
507
538
|
info.state = 'error';
|
|
508
|
-
info.
|
|
539
|
+
info.errorReason = 'adapter_crashed';
|
|
540
|
+
info.lastError = redactDiagnostic(e.message || String(e));
|
|
541
|
+
this._log(`${name} failed to create ${agentType} adapter: ${info.lastError}`);
|
|
509
542
|
this._writeStatus();
|
|
510
543
|
return;
|
|
511
544
|
}
|
|
512
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
|
+
|
|
513
565
|
// Store adapter reference for stop and duplicate detection
|
|
514
566
|
this._adapters[name] = adapter;
|
|
515
567
|
|
|
516
568
|
info.state = 'running';
|
|
517
569
|
info.startedAt = new Date().toISOString();
|
|
570
|
+
info.lastError = null;
|
|
571
|
+
info.errorReason = null;
|
|
518
572
|
this._writeStatus();
|
|
519
573
|
this._log(`${name} adapter online → ${network.slug} (type: ${agentType})`);
|
|
520
574
|
|
|
@@ -531,14 +585,41 @@ class Daemon {
|
|
|
531
585
|
await adapter.run();
|
|
532
586
|
clearInterval(checkStop);
|
|
533
587
|
} catch (e) {
|
|
534
|
-
info.
|
|
588
|
+
info.errorReason = info.errorReason || 'adapter_crashed';
|
|
589
|
+
info.lastError = redactDiagnostic((e.message || String(e)).slice(0, 200));
|
|
535
590
|
this._log(`${name} adapter error: ${info.lastError}`);
|
|
536
591
|
}
|
|
537
592
|
|
|
538
593
|
delete this._adapters[name];
|
|
539
|
-
|
|
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
|
+
}
|
|
540
621
|
this._writeStatus();
|
|
541
|
-
this._log(`${name} adapter stopped`);
|
|
622
|
+
this._log(`${name} adapter stopped (state: ${info.state})`);
|
|
542
623
|
}
|
|
543
624
|
|
|
544
625
|
// NOTE: Adapter-specific message handling (openclaw, claude, codex)
|