@openagents-org/agent-launcher 0.2.189 → 0.2.191
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/adapters/hermes.js +18 -6
- package/src/adapters/kimi-stream.js +3 -1
- package/src/adapters/workspace-prompt.js +13 -6
- package/src/agent-rows.js +3 -1
- package/src/cli.js +16 -4
- package/src/daemon.js +50 -14
- package/src/env.js +116 -1
- package/src/index.js +2 -2
- package/src/installer.js +23 -9
- package/src/node-config.js +55 -18
- package/src/probe.js +19 -5
- package/src/tui.js +2 -1
package/package.json
CHANGED
package/src/adapters/hermes.js
CHANGED
|
@@ -267,14 +267,23 @@ class HermesAdapter extends BaseAdapter {
|
|
|
267
267
|
// Output parsing
|
|
268
268
|
// ------------------------------------------------------------------
|
|
269
269
|
|
|
270
|
-
_parseHermesOutput(raw) {
|
|
270
|
+
_parseHermesOutput(raw, meta) {
|
|
271
271
|
let sessionId = null;
|
|
272
272
|
let body = raw;
|
|
273
273
|
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
274
|
+
// Hermes -Q mode emits session_id to stderr (meta); the response body is
|
|
275
|
+
// on stdout (raw). Older Hermes printed session_id on stdout, so fall
|
|
276
|
+
// back to extracting it from the body when stderr carried none.
|
|
277
|
+
if (meta) {
|
|
278
|
+
const m = SESSION_ID_RE.exec(meta);
|
|
279
|
+
if (m) sessionId = m[1];
|
|
280
|
+
}
|
|
281
|
+
if (!sessionId) {
|
|
282
|
+
const m = SESSION_ID_RE.exec(body);
|
|
283
|
+
if (m) {
|
|
284
|
+
sessionId = m[1];
|
|
285
|
+
body = body.replace(SESSION_ID_RE, '');
|
|
286
|
+
}
|
|
278
287
|
}
|
|
279
288
|
|
|
280
289
|
const lines = [];
|
|
@@ -328,6 +337,7 @@ class HermesAdapter extends BaseAdapter {
|
|
|
328
337
|
const proc = spawn(this._hermesBin, args, {
|
|
329
338
|
env,
|
|
330
339
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
340
|
+
cwd: this.workingDir,
|
|
331
341
|
// No process group on Windows (can't signal one); windowsHide keeps a
|
|
332
342
|
// console from flashing up.
|
|
333
343
|
detached: !IS_WINDOWS,
|
|
@@ -358,7 +368,9 @@ class HermesAdapter extends BaseAdapter {
|
|
|
358
368
|
throw new Error(`hermes exited with code ${exitCode}: ${detail}`);
|
|
359
369
|
}
|
|
360
370
|
|
|
361
|
-
|
|
371
|
+
// Hermes -Q mode: response body on stdout, session metadata
|
|
372
|
+
// (session_id:, resume banner) on stderr — parse both streams.
|
|
373
|
+
const { text, sessionId } = this._parseHermesOutput(stdout, stderr);
|
|
362
374
|
if (sessionId) {
|
|
363
375
|
this._channelSessions[channelName] = sessionId;
|
|
364
376
|
this._saveSessions();
|
|
@@ -340,7 +340,9 @@ function classifyKimiError(info) {
|
|
|
340
340
|
};
|
|
341
341
|
}
|
|
342
342
|
|
|
343
|
-
|
|
343
|
+
// Bounded: a bare 401 matched request ids, and `invalid.{0,20}token` turned
|
|
344
|
+
// "Invalid value for max_tokens" into a bad key and a `kimi login` hint.
|
|
345
|
+
if (/auth_error|\b401\b|unauthorized|invalid[\s_-]*(api[\s_-]?key|token|authentication)\b|credential/i.test(detail)) {
|
|
344
346
|
return {
|
|
345
347
|
kind: 'auth',
|
|
346
348
|
userMessage:
|
|
@@ -415,28 +415,35 @@ function buildApiSkillsPrompt({ endpoint, workspaceId, token, agentName, channel
|
|
|
415
415
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
416
416
|
` ${baseUrl}/v1/browser/tabs` +
|
|
417
417
|
` -d '{"url":"URL","network":"${workspaceId}",` +
|
|
418
|
-
`"source":"openagents:${agentName}"}'\`\n
|
|
418
|
+
`"source":"openagents:${agentName}"}'\`\n` +
|
|
419
|
+
'Tabs are **temporary** by default (closed after a while idle). Add ' +
|
|
420
|
+
'`"persistent":true` (and optionally `"name":"…"`) to open a **permanent** ' +
|
|
421
|
+
'tab whose login state is saved and reusable — humans see the two kinds ' +
|
|
422
|
+
'separately in their browser. Permanent slots are scarcer: use them only ' +
|
|
423
|
+
'for logins worth keeping.\n\n' +
|
|
419
424
|
'**Navigate:**\n' +
|
|
420
425
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
421
426
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/navigate` +
|
|
422
|
-
` -d '{"url":"URL"}'\`\n\n` +
|
|
427
|
+
` -d '{"url":"URL","source":"openagents:${agentName}"}'\`\n\n` +
|
|
423
428
|
'**Click element:**\n' +
|
|
424
429
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
425
430
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/click` +
|
|
426
|
-
` -d '{"selector":"CSS_SELECTOR"}'\`\n\n` +
|
|
431
|
+
` -d '{"selector":"CSS_SELECTOR","source":"openagents:${agentName}"}'\`\n\n` +
|
|
427
432
|
'**Type text:**\n' +
|
|
428
433
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
429
434
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/type` +
|
|
430
|
-
` -d '{"selector":"CSS_SELECTOR","text":"TEXT"}'\`\n` +
|
|
435
|
+
` -d '{"selector":"CSS_SELECTOR","text":"TEXT","source":"openagents:${agentName}"}'\`\n` +
|
|
431
436
|
'(add `"append":true` to keep existing text instead of replacing it)\n\n' +
|
|
432
437
|
'**Press a key** (e.g. submit a form with Enter):\n' +
|
|
433
438
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
434
439
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/press_key` +
|
|
435
|
-
` -d '{"key":"Enter"}'\`\n\n` +
|
|
440
|
+
` -d '{"key":"Enter","source":"openagents:${agentName}"}'\`\n\n` +
|
|
436
441
|
'**Run JavaScript** in the page (returns the result):\n' +
|
|
437
442
|
`\`${curl} -s -X POST -H "${h}" -H "Content-Type: application/json"` +
|
|
438
443
|
` ${baseUrl}/v1/browser/tabs/{tab_id}/evaluate` +
|
|
439
|
-
` -d '{"expression":"document.title"}'\`\n
|
|
444
|
+
` -d '{"expression":"document.title","source":"openagents:${agentName}"}'\`\n` +
|
|
445
|
+
'The `source` field is how humans watching the shared browser see *who* is ' +
|
|
446
|
+
'acting — include it on every tab action.\n\n' +
|
|
440
447
|
'**Close tab:**\n' +
|
|
441
448
|
`\`${curl} -s -X DELETE -H "${h}" ${baseUrl}/v1/browser/tabs/{tab_id}\`\n\n` +
|
|
442
449
|
'**Clicks/typing use CSS selectors only** (no pixel coordinates). If a ' +
|
package/src/agent-rows.js
CHANGED
|
@@ -7,6 +7,8 @@
|
|
|
7
7
|
|
|
8
8
|
'use strict';
|
|
9
9
|
|
|
10
|
+
const { isCliLogin } = require('./env');
|
|
11
|
+
|
|
10
12
|
/**
|
|
11
13
|
* Build one display row per configured agent from a connector.
|
|
12
14
|
*
|
|
@@ -39,7 +41,7 @@ function loadAgentRows(connector) {
|
|
|
39
41
|
let notReadyMsg = '';
|
|
40
42
|
let health = null;
|
|
41
43
|
try {
|
|
42
|
-
health = connector.healthCheck(agent.type || 'openclaw');
|
|
44
|
+
health = connector.healthCheck(agent.type || 'openclaw', { cliLogin: isCliLogin(agent.env), agentEnv: agent.env || {} });
|
|
43
45
|
if (health && !health.ready) {
|
|
44
46
|
// "Not installed" only when the executable is genuinely missing; an
|
|
45
47
|
// installed-but-signed-out agent shows its login/config message, never
|
package/src/cli.js
CHANGED
|
@@ -582,7 +582,17 @@ async function cmdNode(connector, flags, positional) {
|
|
|
582
582
|
process.exitCode = 1;
|
|
583
583
|
return;
|
|
584
584
|
}
|
|
585
|
-
|
|
585
|
+
let info;
|
|
586
|
+
try {
|
|
587
|
+
info = nodeCfg.gatherDeviceInfo();
|
|
588
|
+
} catch (e) {
|
|
589
|
+
// Pairing on top of an unreadable node.json would replace the pairings
|
|
590
|
+
// it holds; stop instead, so nothing is lost while it is sorted out.
|
|
591
|
+
print(`Could not read ${nodeCfg.NODE_FILE}: ${e.message}`);
|
|
592
|
+
print('Close anything that may have it open and try again.');
|
|
593
|
+
process.exitCode = 1;
|
|
594
|
+
return;
|
|
595
|
+
}
|
|
586
596
|
if (flags.type) info.deviceType = flags.type; // server | laptop | desktop
|
|
587
597
|
if (flags.name) info.name = flags.name;
|
|
588
598
|
|
|
@@ -985,14 +995,16 @@ async function cmdProbe(connector, flags, positional) {
|
|
|
985
995
|
const target = positional[0];
|
|
986
996
|
if (!target) { print('Usage: agn probe <type|agent> [--json]'); return; }
|
|
987
997
|
|
|
988
|
-
// Accept an agent name
|
|
998
|
+
// Accept an agent name — probe its type on that agent's own env, so a
|
|
999
|
+
// signed-in agent is tested on its sign-in rather than the type's key.
|
|
989
1000
|
let type = target;
|
|
1001
|
+
let agentEnv;
|
|
990
1002
|
try {
|
|
991
1003
|
const agent = connector.config.getAgent(target);
|
|
992
|
-
if (agent) type = agent.type || 'openclaw';
|
|
1004
|
+
if (agent) { type = agent.type || 'openclaw'; agentEnv = agent.env || {}; }
|
|
993
1005
|
} catch {}
|
|
994
1006
|
|
|
995
|
-
const result = await connector.probeAgentType(type);
|
|
1007
|
+
const result = await connector.probeAgentType(type, { agentEnv });
|
|
996
1008
|
|
|
997
1009
|
if (flags && flags.json) {
|
|
998
1010
|
// Machine consumers (the daemon's probe_agent node command) read ONLY
|
package/src/daemon.js
CHANGED
|
@@ -10,6 +10,7 @@ const { spawn } = require('./wsl');
|
|
|
10
10
|
const os = require('os');
|
|
11
11
|
const { WorkspaceClient } = require('./workspace-client');
|
|
12
12
|
const { listEndpointModels } = require('./model-list');
|
|
13
|
+
const { AUTH_MODE_KEY, isCliLogin, stripForCliLogin, typeEnvFor } = require('./env');
|
|
13
14
|
const { getEnhancedEnv, whichBinary, IS_WINDOWS, defaultAgentWorkdir } = require('./paths');
|
|
14
15
|
|
|
15
16
|
/**
|
|
@@ -72,6 +73,28 @@ function endpointHost(value) {
|
|
|
72
73
|
* Compatible with the Python SDK's daemon — reads the same config files,
|
|
73
74
|
* writes the same status format, and supports the same command protocol.
|
|
74
75
|
*/
|
|
76
|
+
/**
|
|
77
|
+
* Whether a node command carries a key for the agent. A blank key is no key:
|
|
78
|
+
* saving one would leave the agent not-ready, and clearing the sign-in
|
|
79
|
+
* marker on its account would flip a signed-in agent to key mode with
|
|
80
|
+
* nothing to run on. `useDeviceCredentials` means the device already holds
|
|
81
|
+
* the key — nothing to write.
|
|
82
|
+
*/
|
|
83
|
+
function _sentKey(args) {
|
|
84
|
+
const key = args && args.apiKey;
|
|
85
|
+
const present = typeof key === 'string' ? key.trim().length > 0 : !!key;
|
|
86
|
+
return present && !args.useDeviceCredentials;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Whether a node heartbeat failure is the workspace saying the node is gone:
|
|
91
|
+
* a 404 carrying the backend's own message (routers/nodes.py), not any 404.
|
|
92
|
+
*/
|
|
93
|
+
const NODE_GONE_MESSAGES = new Set(['Node not found', 'Workspace not found']);
|
|
94
|
+
function _nodeGone(err) {
|
|
95
|
+
return !!err && err.status === 404 && NODE_GONE_MESSAGES.has(err.message);
|
|
96
|
+
}
|
|
97
|
+
|
|
75
98
|
class Daemon {
|
|
76
99
|
constructor(config, envManager, registry) {
|
|
77
100
|
this.config = config;
|
|
@@ -139,7 +162,9 @@ class Daemon {
|
|
|
139
162
|
const type = agent.type || 'openclaw';
|
|
140
163
|
this._probeInFlight.add(name);
|
|
141
164
|
try {
|
|
142
|
-
|
|
165
|
+
// By agent name, not type: the probe runs on this agent's own env, so a
|
|
166
|
+
// signed-in agent is tested on its sign-in, not the type's saved key.
|
|
167
|
+
const r = await this._runAgn(['probe', name, '--json']);
|
|
143
168
|
let parsed = null;
|
|
144
169
|
try { parsed = JSON.parse(r.stdout.trim()); } catch {}
|
|
145
170
|
if (!parsed || typeof parsed !== 'object') {
|
|
@@ -190,7 +215,9 @@ class Daemon {
|
|
|
190
215
|
const nodeCfg = require('./node-config');
|
|
191
216
|
const pairings = nodeCfg.listPairings().filter((p) => p && p.node_id && p.token);
|
|
192
217
|
if (!pairings.length) return; // no node connected
|
|
193
|
-
|
|
218
|
+
// deviceInfo, not gatherDeviceInfo: the heartbeat sends no device key, and
|
|
219
|
+
// must never be the thing that writes node.json.
|
|
220
|
+
const info = { ...nodeCfg.deviceInfo(), runtimes: this._runtimes || [], fs: this._buildFs() };
|
|
194
221
|
await Promise.all(pairings.map((p) => this._nodeHeartbeatOne(nodeCfg, p, info)));
|
|
195
222
|
}
|
|
196
223
|
|
|
@@ -248,8 +275,12 @@ class Daemon {
|
|
|
248
275
|
// Without this the launcher kept showing "connected" long after a remote
|
|
249
276
|
// removal, since node.json was only ever reconciled by the UI on demand.
|
|
250
277
|
// Transient failures (timeouts, 5xx, auth blips) say nothing about the
|
|
251
|
-
// row's existence, so those are swallowed and retried next tick.
|
|
252
|
-
|
|
278
|
+
// row's existence, so those are swallowed and retried next tick. So is a
|
|
279
|
+
// 404 the backend did not write: the hosting platform answers every
|
|
280
|
+
// request with its own 404 ("Application not found") while the service
|
|
281
|
+
// is unreachable, and taking that as the workspace's word unpaired every
|
|
282
|
+
// workspace on the device at once.
|
|
283
|
+
if (_nodeGone(err)) {
|
|
253
284
|
try { nodeCfg.clearPairing(n.workspace_id); } catch {}
|
|
254
285
|
this._nodeClients.delete(n.workspace_id);
|
|
255
286
|
this._log(`node ${n.node_id} no longer recognized by workspace ${n.workspace_slug || n.workspace_id} — pairing cleared`);
|
|
@@ -347,7 +378,7 @@ class Daemon {
|
|
|
347
378
|
// ever leaving the device. The endpoint goes out as a hostname, so
|
|
348
379
|
// the workspace can tell a relay from the vendor its model list is for.
|
|
349
380
|
let typeEnv = {};
|
|
350
|
-
try { typeEnv = this.envManager.load(a.type) || {}; } catch {}
|
|
381
|
+
try { typeEnv = typeEnvFor(a.type, this.envManager.load(a.type) || {}, this.registry, a.env); } catch {}
|
|
351
382
|
const model = (a.env && a.env.LLM_MODEL) || typeEnv.LLM_MODEL || null;
|
|
352
383
|
const apiKey = (a.env && a.env.LLM_API_KEY) || typeEnv.LLM_API_KEY || null;
|
|
353
384
|
roster.push({
|
|
@@ -372,9 +403,11 @@ class Daemon {
|
|
|
372
403
|
* builds its env: LLM_BASE_URL or a provider variable (OPENAI_BASE_URL,
|
|
373
404
|
* ANTHROPIC_BASE_URL, …) saved for it or mapped by resolve_env, and failing
|
|
374
405
|
* those, the daemon's own environment for the provider variable this type's
|
|
375
|
-
* LLM_BASE_URL maps to, which the spawned CLI inherits.
|
|
406
|
+
* LLM_BASE_URL maps to, which the spawned CLI inherits. A signed-in agent
|
|
407
|
+
* has none: _buildAgentEnv drops every saved endpoint for it.
|
|
376
408
|
*/
|
|
377
409
|
_configuredBaseUrl(a, typeEnv) {
|
|
410
|
+
if (isCliLogin(a.env)) return null;
|
|
378
411
|
const saved = { ...typeEnv, ...(a.env || {}) };
|
|
379
412
|
let env = saved;
|
|
380
413
|
try { env = { ...saved, ...this.envManager.resolve(a.type, saved, this.registry) }; } catch {}
|
|
@@ -470,7 +503,12 @@ class Daemon {
|
|
|
470
503
|
*/
|
|
471
504
|
_saveNodeAgentEnv(name, type, args) {
|
|
472
505
|
const changes = {};
|
|
473
|
-
if (args
|
|
506
|
+
if (_sentKey(args)) {
|
|
507
|
+
changes.LLM_API_KEY = args.apiKey;
|
|
508
|
+
// A key sent for the agent means it runs on that key: a sign-in marker
|
|
509
|
+
// left from the launcher would strip it again at launch.
|
|
510
|
+
changes[AUTH_MODE_KEY] = '';
|
|
511
|
+
}
|
|
474
512
|
if (args.baseUrl !== undefined) changes.LLM_BASE_URL = args.baseUrl;
|
|
475
513
|
if (args.model !== undefined) {
|
|
476
514
|
const existing = this.config.getAgent(name);
|
|
@@ -585,7 +623,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
585
623
|
// Optional credentials for API-key agents (generic → provider mapping
|
|
586
624
|
// happens in env resolution). `useDeviceCredentials` means the device
|
|
587
625
|
// already holds them — nothing to write.
|
|
588
|
-
if (args
|
|
626
|
+
if (_sentKey(args))
|
|
589
627
|
await this._runAgn(['env', type, '--set', `LLM_API_KEY=${args.apiKey}`]);
|
|
590
628
|
if (args.model) await this._setModelEnv(type, args.model);
|
|
591
629
|
if (args.baseUrl) await this._runAgn(['env', type, '--set', `LLM_BASE_URL=${args.baseUrl}`]);
|
|
@@ -622,7 +660,7 @@ async _runNodeCommand(n, cmd) {
|
|
|
622
660
|
// restart it so the change takes effect. Working-dir changes require a
|
|
623
661
|
// recreate (agn has no in-place path change) at the new path.
|
|
624
662
|
const type = (args.type || '').trim();
|
|
625
|
-
if (args
|
|
663
|
+
if (_sentKey(args))
|
|
626
664
|
await this._runAgn(['env', type, '--set', `LLM_API_KEY=${args.apiKey}`]);
|
|
627
665
|
if (args.model !== undefined) await this._setModelEnv(type, args.model);
|
|
628
666
|
if (args.baseUrl !== undefined) await this._runAgn(['env', type, '--set', `LLM_BASE_URL=${args.baseUrl}`]);
|
|
@@ -1519,11 +1557,9 @@ async _runNodeCommand(n, cmd) {
|
|
|
1519
1557
|
|
|
1520
1558
|
_buildAgentEnv(agentCfg) {
|
|
1521
1559
|
const type = agentCfg.type || 'openclaw';
|
|
1522
|
-
const
|
|
1523
|
-
|
|
1524
|
-
|
|
1525
|
-
const merged = { ...mergedSaved, ...resolved };
|
|
1526
|
-
return { ...process.env, ...merged };
|
|
1560
|
+
const merged = this.envManager.getForAgent(type, this.registry, agentCfg.env);
|
|
1561
|
+
// A signed-in agent drops every key, the launcher's own environment's too.
|
|
1562
|
+
return stripForCliLogin(type, { ...process.env, ...merged }, this.registry, agentCfg.env);
|
|
1527
1563
|
}
|
|
1528
1564
|
|
|
1529
1565
|
_agentConfigFingerprint(agentCfg) {
|
package/src/env.js
CHANGED
|
@@ -112,6 +112,119 @@ class EnvManager {
|
|
|
112
112
|
const resolved = this.resolve(agentType, saved, registry);
|
|
113
113
|
return { ...saved, ...resolved };
|
|
114
114
|
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* The saved env one agent runs on: the part of <type>.env it takes (see
|
|
118
|
+
* typeEnvFor) under its own env, resolved. The daemon launches the agent on
|
|
119
|
+
* this over its own environment, and readiness judges the agent on it too.
|
|
120
|
+
*/
|
|
121
|
+
getForAgent(agentType, registry, agentEnv) {
|
|
122
|
+
const saved = { ...typeEnvFor(agentType, this.load(agentType), registry, agentEnv), ...(agentEnv || {}) };
|
|
123
|
+
const resolved = this.resolve(agentType, saved, registry);
|
|
124
|
+
return { ...saved, ...resolved };
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Set on an agent's own env when it signs in through its CLI (`codex login`,
|
|
130
|
+
* `claude auth login`) instead of a key. Keys stay shared per type in
|
|
131
|
+
* <type>.env and the launcher's own environment, and a CLI prefers a key it
|
|
132
|
+
* is handed over its account session, so without this an agent created on
|
|
133
|
+
* the sign-in tab still ran on whatever key was saved there earlier.
|
|
134
|
+
*/
|
|
135
|
+
const AUTH_MODE_KEY = 'OPENAGENTS_AUTH_MODE';
|
|
136
|
+
const CLI_LOGIN = 'cli_login';
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Whether the agent signs in through its CLI. Read from the agent's own env
|
|
140
|
+
* only: a marker in <type>.env or the launcher's environment would otherwise
|
|
141
|
+
* strip the key of every agent of that type, keyed ones included.
|
|
142
|
+
*/
|
|
143
|
+
function isCliLogin(agentEnv) {
|
|
144
|
+
return !!agentEnv && agentEnv[AUTH_MODE_KEY] === CLI_LOGIN;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/**
|
|
148
|
+
* Keys and endpoints a CLI reads under a name of its own that no registry
|
|
149
|
+
* field or readiness check declares (the launcher's auth-specs.ts offers the
|
|
150
|
+
* endpoints). Named per type rather than by suffix: the env stripped is the
|
|
151
|
+
* daemon's whole environment, where *_BASE_URL also names endpoints that are
|
|
152
|
+
* not the model's. CODEX_API_KEY: the Codex CLI honors it over its ChatGPT
|
|
153
|
+
* sign-in, so one set system-wide would defeat the sign-in just as
|
|
154
|
+
* OPENAI_API_KEY does.
|
|
155
|
+
*/
|
|
156
|
+
const NATIVE_CREDENTIALS = {
|
|
157
|
+
codex: ['CODEX_API_KEY'],
|
|
158
|
+
gemini: ['GOOGLE_GEMINI_BASE_URL'],
|
|
159
|
+
antigravity: ['GOOGLE_GEMINI_BASE_URL'],
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* The variables that carry a key or an endpoint for this agent type: its
|
|
164
|
+
* password and *_BASE_URL fields, what LLM_API_KEY / LLM_BASE_URL resolve to,
|
|
165
|
+
* and the *_API_KEY variables its readiness check accepts (Gemini has no
|
|
166
|
+
* fields at all, and reads GEMINI_API_KEY / GOOGLE_API_KEY). Only the
|
|
167
|
+
* *_API_KEY ones: that list also holds models and tokens other tools share
|
|
168
|
+
* (MSWEA_MODEL_NAME, GH_TOKEN). The model is not among them, it applies to a
|
|
169
|
+
* sign-in as well. So is
|
|
170
|
+
* CLAUDE_CODE_OAUTH_TOKEN: `claude setup-token` makes it from the account
|
|
171
|
+
* sign-in, so it is how a signed-in claude authenticates, not a key to drop.
|
|
172
|
+
*/
|
|
173
|
+
function credentialKeys(agentType, registry) {
|
|
174
|
+
const keys = new Set(['LLM_API_KEY', 'LLM_BASE_URL', ...(NATIVE_CREDENTIALS[agentType] || [])]);
|
|
175
|
+
if (!registry) return keys;
|
|
176
|
+
for (const field of registry.getEnvFields?.(agentType) || []) {
|
|
177
|
+
if (field.password || /BASE_URL$/.test(field.name || '')) keys.add(field.name);
|
|
178
|
+
}
|
|
179
|
+
for (const rule of registry.getResolveRules?.(agentType) || []) {
|
|
180
|
+
if (keys.has(rule.from) && rule.to) keys.add(rule.to);
|
|
181
|
+
}
|
|
182
|
+
const checkReady = registry.getEntry?.(agentType)?.check_ready;
|
|
183
|
+
for (const name of (checkReady && checkReady.env_vars) || []) {
|
|
184
|
+
if (/_API_KEY$/.test(name)) keys.add(name);
|
|
185
|
+
}
|
|
186
|
+
return keys;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* The variables that pick the model: LLM_MODEL, what it resolves to
|
|
191
|
+
* (CODEX_MODEL, ANTHROPIC_MODEL), and a type's own *_MODEL fields.
|
|
192
|
+
*/
|
|
193
|
+
function modelKeys(agentType, registry) {
|
|
194
|
+
const keys = new Set(['LLM_MODEL']);
|
|
195
|
+
if (!registry) return keys;
|
|
196
|
+
for (const field of registry.getEnvFields?.(agentType) || []) {
|
|
197
|
+
if (/_MODEL(_NAME)?$/.test(field.name || '')) keys.add(field.name);
|
|
198
|
+
}
|
|
199
|
+
for (const rule of registry.getResolveRules?.(agentType) || []) {
|
|
200
|
+
if (rule.from === 'LLM_MODEL' && rule.to) keys.add(rule.to);
|
|
201
|
+
}
|
|
202
|
+
return keys;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
/**
|
|
206
|
+
* The part of <type>.env an agent runs on. All of it for an agent on a key.
|
|
207
|
+
* A signed-in agent takes none of its keys, and none of its model either:
|
|
208
|
+
* that model was picked for the key's endpoint (a relay's
|
|
209
|
+
* openai-gpt-oss-20b), which the account behind a sign-in does not serve.
|
|
210
|
+
* Its model is the one in its own env, else the CLI's default.
|
|
211
|
+
*/
|
|
212
|
+
function typeEnvFor(agentType, typeEnv, registry, agentEnv) {
|
|
213
|
+
if (!isCliLogin(agentEnv)) return typeEnv;
|
|
214
|
+
const out = stripForCliLogin(agentType, typeEnv, registry, agentEnv);
|
|
215
|
+
for (const key of modelKeys(agentType, registry)) delete out[key];
|
|
216
|
+
return out;
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* `env` with every credential removed when the agent's own env (`agentEnv`)
|
|
221
|
+
* marks a sign-in, or unchanged for an agent that runs on a key.
|
|
222
|
+
*/
|
|
223
|
+
function stripForCliLogin(agentType, env, registry, agentEnv) {
|
|
224
|
+
if (!isCliLogin(agentEnv)) return env;
|
|
225
|
+
const out = { ...env };
|
|
226
|
+
for (const key of credentialKeys(agentType, registry)) delete out[key];
|
|
227
|
+
return out;
|
|
115
228
|
}
|
|
116
229
|
|
|
117
230
|
/**
|
|
@@ -128,4 +241,6 @@ function normalizeAnthropicBase(url) {
|
|
|
128
241
|
return String(url || '').trim().replace(/\/+$/, '').replace(/\/v1$/i, '');
|
|
129
242
|
}
|
|
130
243
|
|
|
131
|
-
module.exports = {
|
|
244
|
+
module.exports = {
|
|
245
|
+
EnvManager, AUTH_MODE_KEY, CLI_LOGIN, isCliLogin, credentialKeys, modelKeys, stripForCliLogin, typeEnvFor,
|
|
246
|
+
};
|
package/src/index.js
CHANGED
|
@@ -69,8 +69,8 @@ class AgentConnector {
|
|
|
69
69
|
return this.installer.isInstalled(agentType);
|
|
70
70
|
}
|
|
71
71
|
|
|
72
|
-
healthCheck(agentType) {
|
|
73
|
-
return this.installer.healthCheck(agentType);
|
|
72
|
+
healthCheck(agentType, opts) {
|
|
73
|
+
return this.installer.healthCheck(agentType, opts);
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
// -- Agent CRUD --
|
package/src/installer.js
CHANGED
|
@@ -512,7 +512,15 @@ class Installer {
|
|
|
512
512
|
* Health check — binary existence + version.
|
|
513
513
|
* @returns {{ installed: boolean, binary: string|null, version: string|null }}
|
|
514
514
|
*/
|
|
515
|
-
|
|
515
|
+
/**
|
|
516
|
+
* `opts.cliLogin` checks one agent that signs in through its CLI: the core
|
|
517
|
+
* drops every key for it at launch, so a key saved for the type or set in
|
|
518
|
+
* the daemon's environment must not make it ready.
|
|
519
|
+
*
|
|
520
|
+
* `opts.agentEnv` is that agent's own env. With it the agent is judged on
|
|
521
|
+
* the env the daemon launches it on, not on <type>.env alone.
|
|
522
|
+
*/
|
|
523
|
+
healthCheck(agentType, opts = {}) {
|
|
516
524
|
const entry = this.registry.getEntry(agentType);
|
|
517
525
|
if (entry?.install?.api_only) {
|
|
518
526
|
const info = this.getInstallInfo(agentType);
|
|
@@ -532,7 +540,7 @@ class Installer {
|
|
|
532
540
|
};
|
|
533
541
|
}
|
|
534
542
|
|
|
535
|
-
const apiReadiness = this._evaluateReadiness(agentType, entry, null);
|
|
543
|
+
const apiReadiness = this._evaluateReadiness(agentType, entry, null, opts);
|
|
536
544
|
return {
|
|
537
545
|
installed: true,
|
|
538
546
|
binary: null,
|
|
@@ -568,7 +576,7 @@ class Installer {
|
|
|
568
576
|
const versionCmd = checkCmd || this._versionProbeCommand(binary);
|
|
569
577
|
|
|
570
578
|
const version = this._detectVersion(binary, versionCmd);
|
|
571
|
-
const readiness = this._evaluateReadiness(agentType, entry, binary);
|
|
579
|
+
const readiness = this._evaluateReadiness(agentType, entry, binary, opts);
|
|
572
580
|
|
|
573
581
|
// Generic minimum-version gate (no per-agent special-casing). An entry opts
|
|
574
582
|
// in by declaring `install.min_version`. Below the floor we keep
|
|
@@ -776,7 +784,7 @@ class Installer {
|
|
|
776
784
|
return { compatible: true, minVersion, message: null };
|
|
777
785
|
}
|
|
778
786
|
|
|
779
|
-
_evaluateReadiness(agentType, entry, binary) {
|
|
787
|
+
_evaluateReadiness(agentType, entry, binary, opts = {}) {
|
|
780
788
|
const checkReady = entry?.check_ready;
|
|
781
789
|
if (!checkReady) {
|
|
782
790
|
return {
|
|
@@ -788,10 +796,16 @@ class Installer {
|
|
|
788
796
|
};
|
|
789
797
|
}
|
|
790
798
|
|
|
791
|
-
|
|
799
|
+
// A key set on the agent itself (the workspace saves it there, as
|
|
800
|
+
// LLM_API_KEY) never reaches <type>.env, so judged on that file alone a
|
|
801
|
+
// Kimi agent that answered fine read "Not configured — … run: kimi login".
|
|
802
|
+
const savedEnv = opts.agentEnv
|
|
803
|
+
? this.env.getForAgent(agentType, this.registry, opts.agentEnv)
|
|
804
|
+
: this.env.getEffective(agentType, this.registry);
|
|
792
805
|
const directEnv = this._hasAllValues(process.env, checkReady.env_all);
|
|
793
806
|
const directSaved = this._hasAllValues(savedEnv, checkReady.saved_env_all || checkReady.env_all);
|
|
794
|
-
const
|
|
807
|
+
const keyed = !opts.cliLogin;
|
|
808
|
+
const directReady = keyed && (directEnv || directSaved);
|
|
795
809
|
// env_vars must be satisfiable by the SAVED (and resolved) per-agent env,
|
|
796
810
|
// not only the daemon's own process env. The workspace configure flow
|
|
797
811
|
// saves LLM_API_KEY, which resolve rules map to e.g. ANTHROPIC_AUTH_TOKEN
|
|
@@ -799,10 +813,10 @@ class Installer {
|
|
|
799
813
|
// alone made a perfectly working key-configured agent read "Not logged
|
|
800
814
|
// in" (health banner + failed smoke test) while chat answered fine, with
|
|
801
815
|
// guidance telling the user to configure the very key they had saved.
|
|
802
|
-
const envAnyReady =
|
|
816
|
+
const envAnyReady = keyed && (
|
|
803
817
|
this._hasAnyValue(process.env, checkReady.env_vars) ||
|
|
804
|
-
this._hasAnyValue(savedEnv, checkReady.env_vars);
|
|
805
|
-
const savedAnyReady = !!(checkReady.saved_env_key && savedEnv[checkReady.saved_env_key]);
|
|
818
|
+
this._hasAnyValue(savedEnv, checkReady.env_vars));
|
|
819
|
+
const savedAnyReady = keyed && !!(checkReady.saved_env_key && savedEnv[checkReady.saved_env_key]);
|
|
806
820
|
// A registry entry may require a non-empty JSON object rather than mere
|
|
807
821
|
// parseability. In that mode the stricter _evaluateCredsFile path below is
|
|
808
822
|
// authoritative; the legacy parser treats an empty {} as ready.
|
package/src/node-config.js
CHANGED
|
@@ -16,30 +16,64 @@ const crypto = require('crypto');
|
|
|
16
16
|
const NODE_DIR = path.join(os.homedir(), '.openagents');
|
|
17
17
|
const NODE_FILE = path.join(NODE_DIR, 'node.json');
|
|
18
18
|
|
|
19
|
-
|
|
19
|
+
/**
|
|
20
|
+
* The record on disk: null when there is no file, and a throw when there is
|
|
21
|
+
* one that cannot be read or parsed (half-written by another process, or held
|
|
22
|
+
* by an antivirus scan on Windows). Every writer reads through this, so an
|
|
23
|
+
* unreadable file is left alone rather than rebuilt from nothing — which
|
|
24
|
+
* dropped every pairing and minted a new device key.
|
|
25
|
+
*/
|
|
26
|
+
function readNode() {
|
|
27
|
+
let raw;
|
|
20
28
|
try {
|
|
21
|
-
|
|
22
|
-
} catch {
|
|
23
|
-
|
|
29
|
+
raw = fs.readFileSync(NODE_FILE, 'utf-8');
|
|
30
|
+
} catch (err) {
|
|
31
|
+
if (err && err.code === 'ENOENT') return null;
|
|
32
|
+
throw err;
|
|
33
|
+
}
|
|
34
|
+
return JSON.parse(raw);
|
|
24
35
|
}
|
|
25
36
|
|
|
37
|
+
/** Lenient read for display and heartbeat: an unreadable file counts as none. */
|
|
38
|
+
function loadNode() {
|
|
39
|
+
try { return readNode(); } catch { return null; }
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Written through a temp file and a rename, so a reader in the other process
|
|
44
|
+
* (launcher or daemon) sees the old record or the new one, never a torn one.
|
|
45
|
+
*/
|
|
26
46
|
function saveNode(data) {
|
|
27
47
|
try {
|
|
28
48
|
fs.mkdirSync(NODE_DIR, { recursive: true });
|
|
29
|
-
|
|
30
|
-
|
|
49
|
+
const tmp = `${NODE_FILE}.${process.pid}.tmp`;
|
|
50
|
+
fs.writeFileSync(tmp, JSON.stringify(data, null, 2), { mode: 0o600 }); // holds the workspace token
|
|
51
|
+
try {
|
|
52
|
+
fs.renameSync(tmp, NODE_FILE);
|
|
53
|
+
} catch {
|
|
54
|
+
// Windows refuses the rename while another process holds the target.
|
|
55
|
+
try { fs.unlinkSync(tmp); } catch {}
|
|
56
|
+
fs.writeFileSync(NODE_FILE, JSON.stringify(data, null, 2));
|
|
57
|
+
}
|
|
58
|
+
try { fs.chmodSync(NODE_FILE, 0o600); } catch {}
|
|
31
59
|
} catch {}
|
|
32
60
|
}
|
|
33
61
|
|
|
34
|
-
/** Stable per-device id, generated once and persisted. */
|
|
62
|
+
/** Stable per-device id, generated once and persisted. Throws if node.json is unreadable. */
|
|
35
63
|
function getOrCreateNodeKey() {
|
|
36
|
-
const existing =
|
|
64
|
+
const existing = readNode();
|
|
37
65
|
if (existing && existing.node_key) return existing.node_key;
|
|
38
66
|
const key = (crypto.randomUUID && crypto.randomUUID()) || crypto.randomBytes(16).toString('hex');
|
|
39
67
|
saveNode({ ...(existing || {}), node_key: key });
|
|
40
68
|
return key;
|
|
41
69
|
}
|
|
42
70
|
|
|
71
|
+
function pairingsOf(record) {
|
|
72
|
+
if (!record) return [];
|
|
73
|
+
if (record.pairings && record.pairings.length) return record.pairings;
|
|
74
|
+
return record.workspace_id ? [record] : [];
|
|
75
|
+
}
|
|
76
|
+
|
|
43
77
|
/**
|
|
44
78
|
* Every workspace this device is paired to, most recent first.
|
|
45
79
|
*
|
|
@@ -49,10 +83,7 @@ function getOrCreateNodeKey() {
|
|
|
49
83
|
* existed carry a single top-level pairing, which is reconstructed here.
|
|
50
84
|
*/
|
|
51
85
|
function listPairings() {
|
|
52
|
-
|
|
53
|
-
if (!record) return [];
|
|
54
|
-
if (record.pairings && record.pairings.length) return record.pairings;
|
|
55
|
-
return record.workspace_id ? [record] : [];
|
|
86
|
+
return pairingsOf(loadNode());
|
|
56
87
|
}
|
|
57
88
|
|
|
58
89
|
/**
|
|
@@ -67,7 +98,7 @@ function recordPairing(nodeKey, pairing) {
|
|
|
67
98
|
const stamped = { ...pairing, paired_at: new Date().toISOString() };
|
|
68
99
|
const pairings = [
|
|
69
100
|
stamped,
|
|
70
|
-
...
|
|
101
|
+
...pairingsOf(readNode()).filter((p) => p.workspace_id !== pairing.workspace_id),
|
|
71
102
|
];
|
|
72
103
|
saveNode({ node_key: nodeKey, ...stamped, pairings });
|
|
73
104
|
}
|
|
@@ -86,12 +117,12 @@ function recordPairing(nodeKey, pairing) {
|
|
|
86
117
|
* @returns the pairing that was dropped, or null if there was no such pairing.
|
|
87
118
|
*/
|
|
88
119
|
function clearPairing(workspaceId) {
|
|
89
|
-
const record =
|
|
120
|
+
const record = readNode();
|
|
90
121
|
if (!record) return null;
|
|
91
122
|
const target = workspaceId || record.workspace_id;
|
|
92
123
|
if (!target) return null;
|
|
93
124
|
|
|
94
|
-
const pairings =
|
|
125
|
+
const pairings = pairingsOf(record);
|
|
95
126
|
const dropped = pairings.find((p) => p.workspace_id === target);
|
|
96
127
|
if (!dropped) return null;
|
|
97
128
|
|
|
@@ -114,12 +145,11 @@ function inferDeviceType() {
|
|
|
114
145
|
return 'server'; // linux is typically a server/VM in this context
|
|
115
146
|
}
|
|
116
147
|
|
|
117
|
-
/** Device info sent on
|
|
118
|
-
function
|
|
148
|
+
/** Device info sent on every heartbeat. Reads and writes nothing on disk. */
|
|
149
|
+
function deviceInfo() {
|
|
119
150
|
let launcherVersion = '';
|
|
120
151
|
try { launcherVersion = require('../package.json').version; } catch {}
|
|
121
152
|
return {
|
|
122
|
-
nodeKey: getOrCreateNodeKey(),
|
|
123
153
|
hostname: os.hostname(),
|
|
124
154
|
os: os.platform(),
|
|
125
155
|
deviceType: inferDeviceType(),
|
|
@@ -127,7 +157,13 @@ function gatherDeviceInfo() {
|
|
|
127
157
|
};
|
|
128
158
|
}
|
|
129
159
|
|
|
160
|
+
/** Device info sent on redeem, with the device key (minted on first pairing). */
|
|
161
|
+
function gatherDeviceInfo() {
|
|
162
|
+
return { nodeKey: getOrCreateNodeKey(), ...deviceInfo() };
|
|
163
|
+
}
|
|
164
|
+
|
|
130
165
|
module.exports = {
|
|
166
|
+
readNode,
|
|
131
167
|
loadNode,
|
|
132
168
|
saveNode,
|
|
133
169
|
listPairings,
|
|
@@ -135,6 +171,7 @@ module.exports = {
|
|
|
135
171
|
clearPairing,
|
|
136
172
|
getOrCreateNodeKey,
|
|
137
173
|
inferDeviceType,
|
|
174
|
+
deviceInfo,
|
|
138
175
|
gatherDeviceInfo,
|
|
139
176
|
NODE_FILE,
|
|
140
177
|
NODE_DIR,
|
package/src/probe.js
CHANGED
|
@@ -33,6 +33,7 @@ const { spawn } = require('./wsl');
|
|
|
33
33
|
const { getEnhancedEnv } = require('./paths');
|
|
34
34
|
const { shouldUseShellForBinary } = require('./adapters/health-status');
|
|
35
35
|
const { formatAuthGuidance } = require('./auth-guidance');
|
|
36
|
+
const { isCliLogin, stripForCliLogin, typeEnvFor } = require('./env');
|
|
36
37
|
|
|
37
38
|
const PROBE_PROMPT = 'hi';
|
|
38
39
|
const DEFAULT_TIMEOUT_MS = 90_000;
|
|
@@ -98,7 +99,14 @@ function authFlavor(entry, agentEnv) {
|
|
|
98
99
|
const cr = (entry && entry.check_ready) || {};
|
|
99
100
|
const envCfg = (entry && entry.env_config) || [];
|
|
100
101
|
const hasKeyField = envCfg.some((f) => f && /key/i.test(f.name || ''));
|
|
101
|
-
|
|
102
|
+
// The type's own key names count too: a Kimi agent on KIMI_API_KEY whose
|
|
103
|
+
// key was rejected was told its sign-in had expired and to run kimi login.
|
|
104
|
+
const keyNames = [
|
|
105
|
+
'LLM_API_KEY', 'OPENAI_API_KEY', 'ANTHROPIC_API_KEY', cr.saved_env_key,
|
|
106
|
+
...(cr.env_vars || []).filter((k) => /_API_KEY$/.test(k)),
|
|
107
|
+
...envCfg.filter((f) => f && f.password).map((f) => f.name),
|
|
108
|
+
];
|
|
109
|
+
const hasSavedKey = !!agentEnv && keyNames.some((k) => k && agentEnv[k]);
|
|
102
110
|
if (hasSavedKey || (hasKeyField && !cr.login_command)) return 'api_key';
|
|
103
111
|
if (cr.login_command) return 'cli_login';
|
|
104
112
|
return hasKeyField ? 'api_key' : 'cli_login';
|
|
@@ -287,16 +295,22 @@ async function probeAgentType(connector, type, opts = {}) {
|
|
|
287
295
|
return done({ ok: false, method: 'none', code: CODE.UNKNOWN_TYPE, message: `Unknown agent type '${type}'` });
|
|
288
296
|
}
|
|
289
297
|
|
|
298
|
+
// A signed-in agent is ready on its sign-in alone: judged on the type, a
|
|
299
|
+
// key it will never be given made it "pass" with no login at all.
|
|
300
|
+
const cliLogin = isCliLogin(opts.agentEnv);
|
|
290
301
|
let health = {};
|
|
291
|
-
try { health = connector.healthCheck(type) || {}; } catch (e) {
|
|
302
|
+
try { health = connector.healthCheck(type, { cliLogin, agentEnv: opts.agentEnv }) || {}; } catch (e) {
|
|
292
303
|
health = { installed: false, ready: false, message: e.message };
|
|
293
304
|
}
|
|
294
305
|
|
|
306
|
+
// opts.agentEnv is one agent's own env, layered over the type's the way
|
|
307
|
+
// the daemon builds it; a signed-in agent then carries no key from either.
|
|
295
308
|
let agentEnv = {};
|
|
296
309
|
try {
|
|
297
|
-
const
|
|
310
|
+
const typeEnv = typeEnvFor(type, connector.getAgentEnv(type) || {}, connector.registry, opts.agentEnv);
|
|
311
|
+
const saved = { ...typeEnv, ...(opts.agentEnv || {}) };
|
|
298
312
|
const resolved = connector.resolveAgentEnv(type, saved) || {};
|
|
299
|
-
agentEnv = { ...saved, ...resolved };
|
|
313
|
+
agentEnv = stripForCliLogin(type, { ...saved, ...resolved }, connector.registry, opts.agentEnv);
|
|
300
314
|
} catch {}
|
|
301
315
|
|
|
302
316
|
if (!health.installed) {
|
|
@@ -321,7 +335,7 @@ async function probeAgentType(connector, type, opts = {}) {
|
|
|
321
335
|
|
|
322
336
|
const timeoutMs = opts.timeoutMs
|
|
323
337
|
|| (entry.probe && entry.probe.timeout_s ? entry.probe.timeout_s * 1000 : DEFAULT_TIMEOUT_MS);
|
|
324
|
-
const env = { ...getEnhancedEnv(), ...agentEnv };
|
|
338
|
+
const env = stripForCliLogin(type, { ...getEnhancedEnv(), ...agentEnv }, connector.registry, opts.agentEnv);
|
|
325
339
|
|
|
326
340
|
// Hoisted above the CLI tier: a failing CLI probe consults it too (below).
|
|
327
341
|
const hasApiKey = !!(agentEnv.LLM_API_KEY || agentEnv.OPENAI_API_KEY || agentEnv.ANTHROPIC_API_KEY
|
package/src/tui.js
CHANGED
|
@@ -14,6 +14,7 @@ const fs = require('fs');
|
|
|
14
14
|
const { spawn } = require('child_process');
|
|
15
15
|
const { getExtraBinDirs } = require('./paths');
|
|
16
16
|
const { hasCredentialMetadata, formatAuthGuidance } = require('./auth-guidance');
|
|
17
|
+
const { isCliLogin } = require('./env');
|
|
17
18
|
|
|
18
19
|
const IS_WINDOWS = process.platform === 'win32';
|
|
19
20
|
|
|
@@ -774,7 +775,7 @@ function createTUI() {
|
|
|
774
775
|
const checkReady = entry && entry.check_ready;
|
|
775
776
|
if (hasCredentialMetadata(checkReady)) {
|
|
776
777
|
let health = null;
|
|
777
|
-
try { health = connector.healthCheck(agent.type); } catch {}
|
|
778
|
+
try { health = connector.healthCheck(agent.type, { cliLogin: isCliLogin(agent.env), agentEnv: agent.env || {} }); } catch {}
|
|
778
779
|
const g = formatAuthGuidance(entry, health);
|
|
779
780
|
const tag = g.ready ? 'green-fg' : 'yellow-fg';
|
|
780
781
|
log(`{bold}{${tag}}${entry.label || agent.type} authentication: ${g.ready ? 'Ready' : 'Not ready'}{/}{/bold}`);
|