@openagents-org/agent-launcher 0.2.109 → 0.2.111
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 +17 -6
- package/src/adapters/claude.js +45 -61
- package/src/adapters/codex.js +21 -6
- package/src/cli.js +44 -0
- package/src/installer.js +157 -41
- package/src/paths.js +10 -1
- package/src/tui.js +17 -46
- package/src/update-check.js +203 -0
package/package.json
CHANGED
package/registry.json
CHANGED
|
@@ -136,16 +136,27 @@
|
|
|
136
136
|
{
|
|
137
137
|
"name": "OPENAI_API_KEY",
|
|
138
138
|
"description": "OpenAI API key",
|
|
139
|
-
"required":
|
|
139
|
+
"required": false,
|
|
140
140
|
"password": true
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
"name": "OPENAI_BASE_URL",
|
|
144
|
+
"description": "OpenAI-compatible base URL",
|
|
145
|
+
"required": false
|
|
141
146
|
}
|
|
142
147
|
],
|
|
143
148
|
"check_ready": {
|
|
144
|
-
"
|
|
145
|
-
"OPENAI_API_KEY"
|
|
149
|
+
"env_all": [
|
|
150
|
+
"OPENAI_API_KEY",
|
|
151
|
+
"OPENAI_BASE_URL"
|
|
146
152
|
],
|
|
147
|
-
"
|
|
148
|
-
|
|
153
|
+
"saved_env_all": [
|
|
154
|
+
"OPENAI_API_KEY",
|
|
155
|
+
"OPENAI_BASE_URL"
|
|
156
|
+
],
|
|
157
|
+
"status_command": "codex login status",
|
|
158
|
+
"login_command": "codex login",
|
|
159
|
+
"not_ready_message": "Not configured. Set OPENAI_API_KEY + OPENAI_BASE_URL, or run: codex login"
|
|
149
160
|
},
|
|
150
161
|
"resolve_env": {
|
|
151
162
|
"rules": [
|
|
@@ -525,4 +536,4 @@
|
|
|
525
536
|
"windows": "pip install sweagent"
|
|
526
537
|
}
|
|
527
538
|
}
|
|
528
|
-
]
|
|
539
|
+
]
|
package/src/adapters/claude.js
CHANGED
|
@@ -272,72 +272,56 @@ class ClaudeAdapter extends BaseAdapter {
|
|
|
272
272
|
if (this.disabledModules.has('files')) mcpArgs.push('--disable-files');
|
|
273
273
|
if (this.disabledModules.has('browser')) mcpArgs.push('--disable-browser');
|
|
274
274
|
|
|
275
|
-
//
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
//
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
275
|
+
// Resolve the MCP server entry point. Prefer the sibling bin inside this
|
|
276
|
+
// very package — it's guaranteed to exist whenever claude.js is executing,
|
|
277
|
+
// so it never falls through to a broken PATH lookup. If Claude Code can't
|
|
278
|
+
// spawn the MCP server, it silently hides every workspace tool and the
|
|
279
|
+
// agent reports "workspace_read_file isn't in my tool set".
|
|
280
|
+
let mcpCommand = this._findNodeBin();
|
|
281
|
+
let mcpFinalArgs = mcpArgs;
|
|
282
|
+
const siblingBin = path.resolve(__dirname, '..', '..', 'bin', 'agent-connector.js');
|
|
283
|
+
if (fs.existsSync(siblingBin)) {
|
|
284
|
+
mcpFinalArgs = [siblingBin, ...mcpArgs];
|
|
285
|
+
} else {
|
|
286
|
+
// Fallback: search installed locations (older layouts, global installs)
|
|
287
|
+
let oaBin = null;
|
|
288
|
+
const home3 = os.homedir();
|
|
289
|
+
const oaExt = IS_WINDOWS ? '.cmd' : '';
|
|
290
|
+
const runtimesRoot = path.join(home3, '.openagents', 'runtimes');
|
|
291
|
+
try {
|
|
292
|
+
for (const d of fs.readdirSync(runtimesRoot, { withFileTypes: true })) {
|
|
293
|
+
if (d.isDirectory()) {
|
|
294
|
+
const candidate = path.join(runtimesRoot, d.name, 'node_modules', '.bin', `openagents${oaExt}`);
|
|
295
|
+
if (fs.existsSync(candidate)) { oaBin = candidate; break; }
|
|
296
|
+
}
|
|
286
297
|
}
|
|
298
|
+
} catch {}
|
|
299
|
+
if (!oaBin) {
|
|
300
|
+
const oaPortable = path.join(home3, '.openagents', 'nodejs', 'node_modules', '.bin', `openagents${oaExt}`);
|
|
301
|
+
if (fs.existsSync(oaPortable)) oaBin = oaPortable;
|
|
287
302
|
}
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
303
|
+
if (!oaBin) try {
|
|
304
|
+
if (IS_WINDOWS) {
|
|
305
|
+
oaBin = execSync('where openagents.cmd 2>nul || where openagents.exe 2>nul || where openagents 2>nul', {
|
|
306
|
+
encoding: 'utf-8', timeout: 5000,
|
|
307
|
+
}).split(/\r?\n/)[0].trim();
|
|
308
|
+
} else {
|
|
309
|
+
oaBin = execSync('which openagents', { encoding: 'utf-8', timeout: 5000 }).trim();
|
|
310
|
+
}
|
|
311
|
+
} catch {}
|
|
312
|
+
if (!oaBin) {
|
|
313
|
+
this._log('Could not find openagents binary — MCP tools may not be available');
|
|
314
|
+
mcpCommand = 'openagents';
|
|
300
315
|
} else {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const nearNode2 = path.join(nodeBinDir2, `openagents${oaExt}`);
|
|
309
|
-
if (fs.existsSync(nearNode2)) oaBin = nearNode2;
|
|
310
|
-
}
|
|
311
|
-
// Tier 3: Common locations
|
|
312
|
-
if (!oaBin) {
|
|
313
|
-
const home2 = os.homedir();
|
|
314
|
-
const oaCandidates = IS_WINDOWS ? [
|
|
315
|
-
path.join(process.env.APPDATA || '', 'npm', 'openagents.cmd'),
|
|
316
|
-
] : [
|
|
317
|
-
path.join(home2, '.openagents', 'npm-global', 'bin', 'openagents'),
|
|
318
|
-
path.join(home2, '.local', 'bin', 'openagents'),
|
|
319
|
-
path.join(home2, '.npm-global', 'bin', 'openagents'),
|
|
320
|
-
'/opt/homebrew/bin/openagents',
|
|
321
|
-
'/usr/local/bin/openagents',
|
|
322
|
-
];
|
|
323
|
-
for (const c of oaCandidates) {
|
|
324
|
-
if (fs.existsSync(c)) { oaBin = c; break; }
|
|
316
|
+
const resolved = this._resolveToNodeCmd(oaBin);
|
|
317
|
+
if (resolved) {
|
|
318
|
+
mcpCommand = resolved[0];
|
|
319
|
+
mcpFinalArgs = [resolved[1], ...mcpArgs];
|
|
320
|
+
} else {
|
|
321
|
+
mcpCommand = oaBin;
|
|
322
|
+
}
|
|
325
323
|
}
|
|
326
324
|
}
|
|
327
|
-
if (!oaBin) {
|
|
328
|
-
oaBin = 'openagents';
|
|
329
|
-
this._log('Could not find openagents binary — MCP tools may not be available');
|
|
330
|
-
}
|
|
331
|
-
|
|
332
|
-
// Resolve shim/symlink to node + JS entry point for MCP server
|
|
333
|
-
// (.cmd shims and #!/usr/bin/env node shebangs both fail as MCP commands)
|
|
334
|
-
let mcpCommand = oaBin;
|
|
335
|
-
let mcpFinalArgs = mcpArgs;
|
|
336
|
-
const mcpResolved = this._resolveToNodeCmd(oaBin);
|
|
337
|
-
if (mcpResolved) {
|
|
338
|
-
mcpCommand = mcpResolved[0];
|
|
339
|
-
mcpFinalArgs = [mcpResolved[1], ...mcpArgs];
|
|
340
|
-
}
|
|
341
325
|
|
|
342
326
|
const mcpConfig = {
|
|
343
327
|
mcpServers: {
|
package/src/adapters/codex.js
CHANGED
|
@@ -140,7 +140,18 @@ class CodexAdapter extends BaseAdapter {
|
|
|
140
140
|
const nearNode = path.join(nodeBinDir, `codex${ext}`);
|
|
141
141
|
if (fs.existsSync(nearNode)) return nearNode;
|
|
142
142
|
|
|
143
|
-
// Tier 3:
|
|
143
|
+
// Tier 3: npm global prefix (handles custom npm prefix like D:\node\node_global)
|
|
144
|
+
try {
|
|
145
|
+
const npmPrefix = execSync('npm config get prefix', {
|
|
146
|
+
encoding: 'utf-8', timeout: 5000, windowsHide: true,
|
|
147
|
+
}).trim();
|
|
148
|
+
if (npmPrefix) {
|
|
149
|
+
const prefixCandidate = path.join(npmPrefix, `codex${ext}`);
|
|
150
|
+
if (fs.existsSync(prefixCandidate)) return prefixCandidate;
|
|
151
|
+
}
|
|
152
|
+
} catch {}
|
|
153
|
+
|
|
154
|
+
// Tier 4: Common install locations
|
|
144
155
|
const candidates = IS_WINDOWS ? [
|
|
145
156
|
path.join(process.env.APPDATA || '', 'npm', 'codex.cmd'),
|
|
146
157
|
] : [
|
|
@@ -265,12 +276,10 @@ class CodexAdapter extends BaseAdapter {
|
|
|
265
276
|
cmd.push('-C', this.workingDir);
|
|
266
277
|
}
|
|
267
278
|
|
|
268
|
-
cmd.push(fullPrompt);
|
|
269
|
-
|
|
270
279
|
this._log(`Spawning: codex exec ${threadId && attempt === 0 ? `resume ${threadId} ` : ''}--json --full-auto -m ${this._directModel || 'default'}`);
|
|
271
280
|
|
|
272
281
|
try {
|
|
273
|
-
const result = await this._spawnCodex(cmd, env, msgChannel);
|
|
282
|
+
const result = await this._spawnCodex(cmd, env, msgChannel, fullPrompt);
|
|
274
283
|
|
|
275
284
|
if (result.responseText) {
|
|
276
285
|
await this.sendResponse(msgChannel, result.responseText);
|
|
@@ -293,14 +302,15 @@ class CodexAdapter extends BaseAdapter {
|
|
|
293
302
|
}
|
|
294
303
|
}
|
|
295
304
|
|
|
296
|
-
async _spawnCodex(cmd, env, msgChannel) {
|
|
305
|
+
async _spawnCodex(cmd, env, msgChannel, prompt) {
|
|
297
306
|
return new Promise((resolve, reject) => {
|
|
298
307
|
const proc = spawn(cmd[0], cmd.slice(1), {
|
|
299
|
-
stdio: ['
|
|
308
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
300
309
|
env,
|
|
301
310
|
cwd: this.workingDir,
|
|
302
311
|
detached: !IS_WINDOWS,
|
|
303
312
|
windowsHide: true,
|
|
313
|
+
shell: IS_WINDOWS,
|
|
304
314
|
});
|
|
305
315
|
this._channelProcesses[msgChannel] = proc;
|
|
306
316
|
|
|
@@ -314,6 +324,11 @@ class CodexAdapter extends BaseAdapter {
|
|
|
314
324
|
proc.stderr.on('data', (chunk) => { stderrBuf += chunk.toString('utf-8'); });
|
|
315
325
|
}
|
|
316
326
|
|
|
327
|
+
if (proc.stdin) {
|
|
328
|
+
proc.stdin.write(prompt || '', 'utf-8');
|
|
329
|
+
proc.stdin.end();
|
|
330
|
+
}
|
|
331
|
+
|
|
317
332
|
const processLine = async (line) => {
|
|
318
333
|
line = line.trim();
|
|
319
334
|
if (!line) return;
|
package/src/cli.js
CHANGED
|
@@ -459,6 +459,28 @@ async function cmdVersion() {
|
|
|
459
459
|
print(`${pkg.name} v${pkg.version}`);
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
+
async function cmdUpdate() {
|
|
463
|
+
const { checkForUpdate, runUpdate, currentVersion } = require('./update-check');
|
|
464
|
+
const info = await checkForUpdate();
|
|
465
|
+
if (!info) {
|
|
466
|
+
print('Could not reach the npm registry. Check your network.');
|
|
467
|
+
process.exitCode = 1;
|
|
468
|
+
return;
|
|
469
|
+
}
|
|
470
|
+
if (!info.isNewer) {
|
|
471
|
+
print(`Already on the latest version (${currentVersion()}).`);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
print(`Updating ${info.current} → ${info.latest}...`);
|
|
475
|
+
const ok = runUpdate();
|
|
476
|
+
if (!ok) {
|
|
477
|
+
print('Update failed.');
|
|
478
|
+
process.exitCode = 1;
|
|
479
|
+
return;
|
|
480
|
+
}
|
|
481
|
+
print(`Updated to ${info.latest}.`);
|
|
482
|
+
}
|
|
483
|
+
|
|
462
484
|
async function cmdHelp() {
|
|
463
485
|
print(`Usage: agn <command> [options]
|
|
464
486
|
|
|
@@ -485,6 +507,7 @@ Commands:
|
|
|
485
507
|
workspace join <token> Join workspace with token
|
|
486
508
|
workspace list List configured workspaces
|
|
487
509
|
mcp-server Start MCP server (stdio) for workspace tools
|
|
510
|
+
update Upgrade launcher to the latest npm release
|
|
488
511
|
version Show version
|
|
489
512
|
help Show this help
|
|
490
513
|
|
|
@@ -503,6 +526,26 @@ async function main() {
|
|
|
503
526
|
if (cmd === 'help' || flags.help) { await cmdHelp(); return; }
|
|
504
527
|
if (cmd === 'version' || flags.version) { await cmdVersion(); return; }
|
|
505
528
|
|
|
529
|
+
// Check for a newer launcher version and offer to install it. Skip for:
|
|
530
|
+
// - mcp-server: JSON-RPC subprocess spawned by Claude Code
|
|
531
|
+
// - up --foreground: the backgrounded daemon child
|
|
532
|
+
// - tui / auto-TUI: interactive UI manages its own rendering
|
|
533
|
+
// - update: already updating; avoid recursion
|
|
534
|
+
const skipUpdateCheck =
|
|
535
|
+
cmd === 'mcp-server' ||
|
|
536
|
+
(cmd === 'up' && flags.foreground) ||
|
|
537
|
+
cmd === 'tui' ||
|
|
538
|
+
cmd === 'update' ||
|
|
539
|
+
flags['no-update-check'] ||
|
|
540
|
+
process.env.OPENAGENTS_SKIP_UPDATE_CHECK === '1' ||
|
|
541
|
+
(cmd === 'status' && process.argv.length <= 2 && process.stdin.isTTY);
|
|
542
|
+
if (!skipUpdateCheck) {
|
|
543
|
+
try {
|
|
544
|
+
const { notifyAndMaybeUpdate } = require('./update-check');
|
|
545
|
+
await notifyAndMaybeUpdate();
|
|
546
|
+
} catch {}
|
|
547
|
+
}
|
|
548
|
+
|
|
506
549
|
const connector = getConnector(flags);
|
|
507
550
|
|
|
508
551
|
// Launch TUI if command is 'tui' or no command with interactive terminal
|
|
@@ -542,6 +585,7 @@ async function main() {
|
|
|
542
585
|
workspace: () => cmdWorkspace(connector, flags, positional),
|
|
543
586
|
env: () => cmdEnv(connector, flags, positional),
|
|
544
587
|
'test-llm': () => cmdTestLLM(connector, flags, positional),
|
|
588
|
+
update: () => cmdUpdate(),
|
|
545
589
|
'mcp-server': () => {
|
|
546
590
|
const { runMcpServer } = require('./mcp-server');
|
|
547
591
|
const workspaceId = flags['workspace-id'] || process.env.OPENAGENTS_WORKSPACE_ID;
|
package/src/installer.js
CHANGED
|
@@ -5,6 +5,10 @@ const os = require('os');
|
|
|
5
5
|
const path = require('path');
|
|
6
6
|
const { execSync, exec } = require('child_process');
|
|
7
7
|
const { whichBinary, getEnhancedEnv, getRuntimePrefix } = require('./paths');
|
|
8
|
+
const { EnvManager } = require('./env');
|
|
9
|
+
|
|
10
|
+
const STATUS_CACHE_TTL_MS = 10000;
|
|
11
|
+
const statusCache = new Map();
|
|
8
12
|
|
|
9
13
|
/**
|
|
10
14
|
* Manages installation and uninstallation of agent runtimes.
|
|
@@ -19,6 +23,7 @@ class Installer {
|
|
|
19
23
|
this.configDir = configDir;
|
|
20
24
|
this.markersFile = path.join(configDir, 'installed_agents.json');
|
|
21
25
|
this.markersDir = path.join(configDir, 'installed');
|
|
26
|
+
this.env = new EnvManager(configDir);
|
|
22
27
|
}
|
|
23
28
|
|
|
24
29
|
/**
|
|
@@ -131,7 +136,17 @@ class Installer {
|
|
|
131
136
|
*/
|
|
132
137
|
healthCheck(agentType) {
|
|
133
138
|
const binary = this._whichBinary(agentType);
|
|
134
|
-
if (!binary)
|
|
139
|
+
if (!binary) {
|
|
140
|
+
return {
|
|
141
|
+
installed: false,
|
|
142
|
+
binary: null,
|
|
143
|
+
version: null,
|
|
144
|
+
ready: false,
|
|
145
|
+
auth_mode: null,
|
|
146
|
+
execution_mode: 'unavailable',
|
|
147
|
+
message: 'Not installed',
|
|
148
|
+
};
|
|
149
|
+
}
|
|
135
150
|
|
|
136
151
|
const entry = this.registry.getEntry(agentType);
|
|
137
152
|
const checkCmd = entry && entry.install ? entry.install.check_command : null;
|
|
@@ -150,51 +165,152 @@ class Installer {
|
|
|
150
165
|
version = match ? match[1] : raw.split('\n')[0];
|
|
151
166
|
} catch {}
|
|
152
167
|
|
|
153
|
-
|
|
154
|
-
|
|
168
|
+
const readiness = this._evaluateReadiness(agentType, entry, binary);
|
|
169
|
+
return { installed: true, binary, version, ...readiness };
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
_evaluateReadiness(agentType, entry, binary) {
|
|
155
173
|
const checkReady = entry?.check_ready;
|
|
156
|
-
if (checkReady) {
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
174
|
+
if (!checkReady) {
|
|
175
|
+
return {
|
|
176
|
+
ready: true,
|
|
177
|
+
auth_mode: null,
|
|
178
|
+
execution_mode: 'unavailable',
|
|
179
|
+
message: 'Ready',
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const savedEnv = this.env.getEffective(agentType, this.registry);
|
|
184
|
+
const directEnv = this._hasAllValues(process.env, checkReady.env_all);
|
|
185
|
+
const directSaved = this._hasAllValues(savedEnv, checkReady.saved_env_all || checkReady.env_all);
|
|
186
|
+
const directReady = directEnv || directSaved;
|
|
187
|
+
const envAnyReady = this._hasAnyValue(process.env, checkReady.env_vars);
|
|
188
|
+
const savedAnyReady = !!(checkReady.saved_env_key && savedEnv[checkReady.saved_env_key]);
|
|
189
|
+
const credsReady = this._checkCredsReady(checkReady);
|
|
190
|
+
|
|
191
|
+
let cliReady = false;
|
|
192
|
+
if (checkReady.status_command && binary) {
|
|
193
|
+
cliReady = this._checkStatusCommand(checkReady.status_command);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
if (directReady) {
|
|
197
|
+
return {
|
|
198
|
+
ready: true,
|
|
199
|
+
auth_mode: 'api_key',
|
|
200
|
+
execution_mode: 'direct',
|
|
201
|
+
message: 'Ready',
|
|
202
|
+
};
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
if (cliReady) {
|
|
206
|
+
return {
|
|
207
|
+
ready: true,
|
|
208
|
+
auth_mode: 'cli_login',
|
|
209
|
+
execution_mode: 'subprocess',
|
|
210
|
+
message: 'Ready',
|
|
211
|
+
};
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
if (envAnyReady || savedAnyReady) {
|
|
215
|
+
// Legacy single-key path (e.g. agents that only advertise env_vars / saved_env_key).
|
|
216
|
+
// An API key being present means the agent can be launched with it in the environment,
|
|
217
|
+
// so treat this as a direct-launch configuration.
|
|
218
|
+
return {
|
|
219
|
+
ready: true,
|
|
220
|
+
auth_mode: 'api_key',
|
|
221
|
+
execution_mode: 'direct',
|
|
222
|
+
message: 'Ready',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
if (credsReady) {
|
|
227
|
+
return {
|
|
228
|
+
ready: true,
|
|
229
|
+
auth_mode: 'cli_login',
|
|
230
|
+
execution_mode: 'subprocess',
|
|
231
|
+
message: 'Ready',
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
ready: false,
|
|
237
|
+
auth_mode: null,
|
|
238
|
+
execution_mode: 'unavailable',
|
|
239
|
+
message: checkReady.not_ready_message || 'Not configured',
|
|
240
|
+
};
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
_hasAllValues(source, keys) {
|
|
244
|
+
if (!keys || keys.length === 0) return false;
|
|
245
|
+
return keys.every((key) => !!(source && source[key]));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
_hasAnyValue(source, keys) {
|
|
249
|
+
if (!keys || keys.length === 0) return false;
|
|
250
|
+
return keys.some((key) => !!(source && source[key]));
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
_checkCredsReady(checkReady) {
|
|
254
|
+
if (checkReady.creds_file) {
|
|
255
|
+
try {
|
|
256
|
+
const credsPath = checkReady.creds_file.replace('~', os.homedir());
|
|
257
|
+
if (fs.existsSync(credsPath)) {
|
|
258
|
+
const stat = fs.statSync(credsPath);
|
|
259
|
+
if (stat.isDirectory()) return fs.readdirSync(credsPath).length > 0;
|
|
260
|
+
const creds = JSON.parse(fs.readFileSync(credsPath, 'utf-8'));
|
|
261
|
+
if (checkReady.creds_key) return !!creds[checkReady.creds_key];
|
|
262
|
+
return true;
|
|
162
263
|
}
|
|
264
|
+
} catch {}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
if (checkReady.keychain_service && process.platform === 'darwin') {
|
|
268
|
+
if (this._checkMacKeychain(checkReady.keychain_service, checkReady.creds_key)) {
|
|
269
|
+
return true;
|
|
163
270
|
}
|
|
164
|
-
// Check credentials file or directory
|
|
165
|
-
if (!ready && checkReady.creds_file) {
|
|
166
|
-
try {
|
|
167
|
-
const credsPath = checkReady.creds_file.replace('~', os.homedir());
|
|
168
|
-
if (fs.existsSync(credsPath)) {
|
|
169
|
-
const stat = fs.statSync(credsPath);
|
|
170
|
-
if (stat.isDirectory()) {
|
|
171
|
-
// Directory exists — check if it has files (e.g. session files)
|
|
172
|
-
ready = fs.readdirSync(credsPath).length > 0;
|
|
173
|
-
} else {
|
|
174
|
-
// File — parse JSON and check key
|
|
175
|
-
const creds = JSON.parse(fs.readFileSync(credsPath, 'utf-8'));
|
|
176
|
-
if (checkReady.creds_key) {
|
|
177
|
-
ready = !!creds[checkReady.creds_key];
|
|
178
|
-
} else {
|
|
179
|
-
ready = true;
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
} catch {}
|
|
184
|
-
}
|
|
185
|
-
// Also check OAuth credentials (Claude Code stores tokens in .credentials.json)
|
|
186
|
-
if (!ready) {
|
|
187
|
-
try {
|
|
188
|
-
const oauthFile = path.join(os.homedir(), '.claude', '.credentials.json');
|
|
189
|
-
if (fs.existsSync(oauthFile)) {
|
|
190
|
-
const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf-8'));
|
|
191
|
-
if (creds.claudeAiOauth?.accessToken) ready = true;
|
|
192
|
-
}
|
|
193
|
-
} catch {}
|
|
194
|
-
}
|
|
195
271
|
}
|
|
196
272
|
|
|
197
|
-
return
|
|
273
|
+
return false;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Check a macOS Keychain generic-password entry. If creds_key is provided the
|
|
278
|
+
* stored value is parsed as JSON and required to contain that key; otherwise any
|
|
279
|
+
* non-empty value counts as ready. Returns false on non-macOS or on any error.
|
|
280
|
+
*/
|
|
281
|
+
_checkMacKeychain(service, credsKey) {
|
|
282
|
+
try {
|
|
283
|
+
const stdout = execSync(
|
|
284
|
+
`security find-generic-password -s ${JSON.stringify(service)} -w`,
|
|
285
|
+
{ stdio: ['ignore', 'pipe', 'ignore'], timeout: 5000, encoding: 'utf-8' },
|
|
286
|
+
).trim();
|
|
287
|
+
if (!stdout) return false;
|
|
288
|
+
if (!credsKey) return true;
|
|
289
|
+
const creds = JSON.parse(stdout);
|
|
290
|
+
return !!creds[credsKey];
|
|
291
|
+
} catch {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
_checkStatusCommand(command) {
|
|
297
|
+
const cached = statusCache.get(command);
|
|
298
|
+
if (cached && (Date.now() - cached.ts) < STATUS_CACHE_TTL_MS) {
|
|
299
|
+
return cached.ok;
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
let ok = false;
|
|
303
|
+
try {
|
|
304
|
+
execSync(command, {
|
|
305
|
+
stdio: 'ignore',
|
|
306
|
+
timeout: 5000,
|
|
307
|
+
env: getEnhancedEnv(),
|
|
308
|
+
});
|
|
309
|
+
ok = true;
|
|
310
|
+
} catch {}
|
|
311
|
+
|
|
312
|
+
statusCache.set(command, { ok, ts: Date.now() });
|
|
313
|
+
return ok;
|
|
198
314
|
}
|
|
199
315
|
|
|
200
316
|
/**
|
package/src/paths.js
CHANGED
|
@@ -139,9 +139,18 @@ function _addWindowsPaths(dirs) {
|
|
|
139
139
|
// System32 (cmd.exe, powershell, etc) — Electron may not have it
|
|
140
140
|
_push(dirs, path.join(sysRoot, 'System32'));
|
|
141
141
|
|
|
142
|
-
// npm global bin
|
|
142
|
+
// npm global bin (default location)
|
|
143
143
|
if (appData) _push(dirs, path.join(appData, 'npm'));
|
|
144
144
|
|
|
145
|
+
// npm global bin (custom prefix — e.g. user configured npm prefix to D:\node\node_global)
|
|
146
|
+
try {
|
|
147
|
+
const npmPrefix = execSync('npm config get prefix', {
|
|
148
|
+
encoding: 'utf-8', timeout: 5000, windowsHide: true,
|
|
149
|
+
stdio: ['pipe', 'pipe', 'pipe'],
|
|
150
|
+
}).trim();
|
|
151
|
+
if (npmPrefix) _push(dirs, npmPrefix);
|
|
152
|
+
} catch {}
|
|
153
|
+
|
|
145
154
|
// Portable Node.js installed by OpenAgents Launcher
|
|
146
155
|
_push(dirs, path.join(HOME, '.openagents', 'nodejs'));
|
|
147
156
|
|
package/src/tui.js
CHANGED
|
@@ -85,54 +85,11 @@ function loadAgentRows(connector) {
|
|
|
85
85
|
workspace = agent.network;
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
|
-
// Check if agent type needs configuration (API key etc.)
|
|
89
88
|
let notReadyMsg = '';
|
|
89
|
+
let health = null;
|
|
90
90
|
try {
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
if (entry && entry.check_ready) {
|
|
94
|
-
const cr = entry.check_ready;
|
|
95
|
-
let isReady = false;
|
|
96
|
-
// Check saved env
|
|
97
|
-
if (cr.saved_env_key) {
|
|
98
|
-
const saved = connector.env.load(agentType);
|
|
99
|
-
if (saved[cr.saved_env_key]) isReady = true;
|
|
100
|
-
}
|
|
101
|
-
// Check process env vars
|
|
102
|
-
if (!isReady && cr.env_vars) {
|
|
103
|
-
for (const v of cr.env_vars) {
|
|
104
|
-
if (process.env[v]) { isReady = true; break; }
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
// Check creds file/directory (for claude)
|
|
108
|
-
if (!isReady && cr.creds_file) {
|
|
109
|
-
const credsPath = cr.creds_file.replace('~', process.env.HOME || process.env.USERPROFILE || '');
|
|
110
|
-
try {
|
|
111
|
-
if (fs.existsSync(credsPath)) {
|
|
112
|
-
const stat = fs.statSync(credsPath);
|
|
113
|
-
if (stat.isDirectory()) {
|
|
114
|
-
// Directory (e.g. ~/.claude/sessions) — check if it has files
|
|
115
|
-
isReady = fs.readdirSync(credsPath).length > 0;
|
|
116
|
-
} else {
|
|
117
|
-
const creds = JSON.parse(fs.readFileSync(credsPath, 'utf-8'));
|
|
118
|
-
if (cr.creds_key) isReady = !!creds[cr.creds_key];
|
|
119
|
-
else isReady = true;
|
|
120
|
-
}
|
|
121
|
-
}
|
|
122
|
-
} catch {}
|
|
123
|
-
}
|
|
124
|
-
// Also check OAuth credentials (Claude Code stores tokens in .credentials.json)
|
|
125
|
-
if (!isReady) {
|
|
126
|
-
try {
|
|
127
|
-
const oauthFile = path.join(process.env.HOME || '', '.claude', '.credentials.json');
|
|
128
|
-
if (fs.existsSync(oauthFile)) {
|
|
129
|
-
const creds = JSON.parse(fs.readFileSync(oauthFile, 'utf-8'));
|
|
130
|
-
if (creds.claudeAiOauth && creds.claudeAiOauth.accessToken) isReady = true;
|
|
131
|
-
}
|
|
132
|
-
} catch {}
|
|
133
|
-
}
|
|
134
|
-
if (!isReady) notReadyMsg = cr.not_ready_message || 'Not configured';
|
|
135
|
-
}
|
|
91
|
+
health = connector.healthCheck(agent.type || 'openclaw');
|
|
92
|
+
if (health && !health.ready) notReadyMsg = health.message || 'Not configured';
|
|
136
93
|
} catch {}
|
|
137
94
|
|
|
138
95
|
return {
|
|
@@ -144,11 +101,24 @@ function loadAgentRows(connector) {
|
|
|
144
101
|
network: agent.network || '',
|
|
145
102
|
lastError: info.last_error || '',
|
|
146
103
|
notReadyMsg,
|
|
104
|
+
health,
|
|
147
105
|
configured: true,
|
|
148
106
|
};
|
|
149
107
|
});
|
|
150
108
|
}
|
|
151
109
|
|
|
110
|
+
function describeHealth(health) {
|
|
111
|
+
if (!health) return '';
|
|
112
|
+
if (!health.ready) return health.message || 'Not configured';
|
|
113
|
+
const parts = ['Ready'];
|
|
114
|
+
if (health.auth_mode === 'api_key') parts.push('API key');
|
|
115
|
+
else if (health.auth_mode === 'cli_login') parts.push('CLI login');
|
|
116
|
+
if (health.execution_mode && health.execution_mode !== 'unavailable') {
|
|
117
|
+
parts.push(health.execution_mode);
|
|
118
|
+
}
|
|
119
|
+
return parts.join(' | ');
|
|
120
|
+
}
|
|
121
|
+
|
|
152
122
|
function loadCatalog(connector) {
|
|
153
123
|
const { execSync } = require('child_process');
|
|
154
124
|
const entries = connector.registry.getCatalogSync();
|
|
@@ -372,6 +342,7 @@ function createTUI() {
|
|
|
372
342
|
// Detail row: working dir + config warning
|
|
373
343
|
const details = [];
|
|
374
344
|
details.push(r.path || process.env.HOME || '~');
|
|
345
|
+
if (r.health) details.push(`{cyan-fg}${describeHealth(r.health)}{/cyan-fg}`);
|
|
375
346
|
if (r.notReadyMsg) details.push(`{yellow-fg}⚠ ${r.notReadyMsg}{/yellow-fg}`);
|
|
376
347
|
items.push(` {gray-fg} ${details.join(' | ')}{/gray-fg}`);
|
|
377
348
|
}
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Update check + prompt flow.
|
|
5
|
+
*
|
|
6
|
+
* Queries the npm registry for the latest @openagents-org/agent-launcher
|
|
7
|
+
* version and, if newer than the running version, prints a notification
|
|
8
|
+
* and (for TTY invocations) prompts the user to update in place.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
const https = require('https');
|
|
12
|
+
const fs = require('fs');
|
|
13
|
+
const os = require('os');
|
|
14
|
+
const path = require('path');
|
|
15
|
+
const readline = require('readline');
|
|
16
|
+
const { execSync, spawnSync } = require('child_process');
|
|
17
|
+
|
|
18
|
+
const PKG_NAME = '@openagents-org/agent-launcher';
|
|
19
|
+
const CACHE_FILE = path.join(os.homedir(), '.openagents', '.update-check.json');
|
|
20
|
+
const CACHE_TTL_MS = 60 * 60 * 1000; // 1 hour
|
|
21
|
+
|
|
22
|
+
function currentVersion() {
|
|
23
|
+
return require('../package.json').version;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function compareSemver(a, b) {
|
|
27
|
+
const pa = String(a).split('.').map((n) => parseInt(n, 10) || 0);
|
|
28
|
+
const pb = String(b).split('.').map((n) => parseInt(n, 10) || 0);
|
|
29
|
+
for (let i = 0; i < 3; i++) {
|
|
30
|
+
if ((pa[i] || 0) > (pb[i] || 0)) return 1;
|
|
31
|
+
if ((pa[i] || 0) < (pb[i] || 0)) return -1;
|
|
32
|
+
}
|
|
33
|
+
return 0;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function loadCache() {
|
|
37
|
+
try {
|
|
38
|
+
const data = JSON.parse(fs.readFileSync(CACHE_FILE, 'utf-8'));
|
|
39
|
+
if (data && data.latest && data.checkedAt &&
|
|
40
|
+
Date.now() - data.checkedAt < CACHE_TTL_MS) {
|
|
41
|
+
return data.latest;
|
|
42
|
+
}
|
|
43
|
+
} catch {}
|
|
44
|
+
return null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function saveCache(latest) {
|
|
48
|
+
try {
|
|
49
|
+
fs.mkdirSync(path.dirname(CACHE_FILE), { recursive: true });
|
|
50
|
+
fs.writeFileSync(CACHE_FILE, JSON.stringify({ latest, checkedAt: Date.now() }));
|
|
51
|
+
} catch {}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function fetchLatest(timeoutMs = 2500) {
|
|
55
|
+
return new Promise((resolve) => {
|
|
56
|
+
const req = https.request(
|
|
57
|
+
`https://registry.npmjs.org/${PKG_NAME}/latest`,
|
|
58
|
+
{ method: 'GET', timeout: timeoutMs, headers: { Accept: 'application/json' } },
|
|
59
|
+
(res) => {
|
|
60
|
+
let body = '';
|
|
61
|
+
res.on('data', (c) => { body += c; });
|
|
62
|
+
res.on('end', () => {
|
|
63
|
+
try {
|
|
64
|
+
const parsed = JSON.parse(body);
|
|
65
|
+
resolve(parsed.version || null);
|
|
66
|
+
} catch { resolve(null); }
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
);
|
|
70
|
+
req.on('error', () => resolve(null));
|
|
71
|
+
req.on('timeout', () => { req.destroy(); resolve(null); });
|
|
72
|
+
req.end();
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Return { current, latest, isNewer } or null if the check failed / offline.
|
|
78
|
+
*/
|
|
79
|
+
async function checkForUpdate() {
|
|
80
|
+
const current = currentVersion();
|
|
81
|
+
let latest = loadCache();
|
|
82
|
+
if (!latest) {
|
|
83
|
+
latest = await fetchLatest();
|
|
84
|
+
if (latest) saveCache(latest);
|
|
85
|
+
}
|
|
86
|
+
if (!latest) return null;
|
|
87
|
+
return { current, latest, isNewer: compareSemver(latest, current) > 0 };
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Walk up from __dirname until we find the enclosing node_modules; the
|
|
92
|
+
* directory containing node_modules is the install prefix.
|
|
93
|
+
*/
|
|
94
|
+
function detectInstallPrefix() {
|
|
95
|
+
let dir = __dirname;
|
|
96
|
+
for (let i = 0; i < 10; i++) {
|
|
97
|
+
const parent = path.dirname(dir);
|
|
98
|
+
if (parent === dir) break;
|
|
99
|
+
if (path.basename(parent) === 'node_modules') {
|
|
100
|
+
return path.dirname(parent);
|
|
101
|
+
}
|
|
102
|
+
dir = parent;
|
|
103
|
+
}
|
|
104
|
+
return null;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function findNpmBin() {
|
|
108
|
+
const nodeDir = path.dirname(process.execPath);
|
|
109
|
+
for (const name of ['npm', 'npm.cmd']) {
|
|
110
|
+
const candidate = path.join(nodeDir, name);
|
|
111
|
+
if (fs.existsSync(candidate)) return candidate;
|
|
112
|
+
}
|
|
113
|
+
try {
|
|
114
|
+
return execSync(process.platform === 'win32' ? 'where npm' : 'which npm', {
|
|
115
|
+
encoding: 'utf-8', timeout: 3000,
|
|
116
|
+
}).trim().split(/\r?\n/)[0];
|
|
117
|
+
} catch { return 'npm'; }
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run npm install to update to the latest version. Blocking.
|
|
122
|
+
* Returns true on success.
|
|
123
|
+
*/
|
|
124
|
+
function runUpdate() {
|
|
125
|
+
const prefix = detectInstallPrefix();
|
|
126
|
+
const npmBin = findNpmBin();
|
|
127
|
+
const args = ['install', '--no-save', `${PKG_NAME}@latest`];
|
|
128
|
+
if (prefix) args.push('--prefix', prefix);
|
|
129
|
+
else args.push('-g');
|
|
130
|
+
process.stderr.write(`[launcher] Running: ${npmBin} ${args.join(' ')}\n`);
|
|
131
|
+
const r = spawnSync(npmBin, args, { stdio: 'inherit' });
|
|
132
|
+
return r.status === 0;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/**
|
|
136
|
+
* Prompt for a Y/n keystroke. Defaults to Y on empty input.
|
|
137
|
+
* Times out after `timeoutMs` and returns false.
|
|
138
|
+
*/
|
|
139
|
+
function promptYes(question, timeoutMs = 30000) {
|
|
140
|
+
return new Promise((resolve) => {
|
|
141
|
+
if (!process.stdin.isTTY || !process.stdout.isTTY) return resolve(false);
|
|
142
|
+
process.stderr.write(question);
|
|
143
|
+
let answered = false;
|
|
144
|
+
const rl = readline.createInterface({ input: process.stdin, output: process.stderr });
|
|
145
|
+
const timer = setTimeout(() => {
|
|
146
|
+
if (answered) return;
|
|
147
|
+
answered = true;
|
|
148
|
+
process.stderr.write('\n');
|
|
149
|
+
rl.close();
|
|
150
|
+
resolve(false);
|
|
151
|
+
}, timeoutMs);
|
|
152
|
+
rl.question('', (ans) => {
|
|
153
|
+
if (answered) return;
|
|
154
|
+
answered = true;
|
|
155
|
+
clearTimeout(timer);
|
|
156
|
+
rl.close();
|
|
157
|
+
const a = (ans || '').trim().toLowerCase();
|
|
158
|
+
resolve(a === '' || a === 'y' || a === 'yes');
|
|
159
|
+
});
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Check, notify, and offer to update. Exits the process on successful update.
|
|
165
|
+
* Safe to call at the top of any user-facing CLI command.
|
|
166
|
+
*/
|
|
167
|
+
async function notifyAndMaybeUpdate() {
|
|
168
|
+
let info;
|
|
169
|
+
try { info = await checkForUpdate(); } catch { return; }
|
|
170
|
+
if (!info || !info.isNewer) return;
|
|
171
|
+
|
|
172
|
+
process.stderr.write(
|
|
173
|
+
`\n[launcher] Update available: ${info.current} → ${info.latest}\n`
|
|
174
|
+
);
|
|
175
|
+
|
|
176
|
+
const interactive = process.stdin.isTTY && process.stdout.isTTY;
|
|
177
|
+
if (!interactive) {
|
|
178
|
+
process.stderr.write(
|
|
179
|
+
'[launcher] Run `agn update` (or re-run install.sh) to upgrade.\n\n'
|
|
180
|
+
);
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const accepted = await promptYes('[launcher] Update now? [Y/n] ');
|
|
185
|
+
if (!accepted) {
|
|
186
|
+
process.stderr.write('[launcher] Skipped. Run `agn update` later to upgrade.\n\n');
|
|
187
|
+
return;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
const ok = runUpdate();
|
|
191
|
+
if (ok) {
|
|
192
|
+
process.stderr.write(`[launcher] Updated to ${info.latest}. Re-run your command.\n`);
|
|
193
|
+
process.exit(0);
|
|
194
|
+
}
|
|
195
|
+
process.stderr.write('[launcher] Update failed — continuing with current version.\n\n');
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
module.exports = {
|
|
199
|
+
checkForUpdate,
|
|
200
|
+
notifyAndMaybeUpdate,
|
|
201
|
+
runUpdate,
|
|
202
|
+
currentVersion,
|
|
203
|
+
};
|