@openagents-org/agent-launcher 0.2.87 → 0.2.88

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@openagents-org/agent-launcher",
3
- "version": "0.2.87",
3
+ "version": "0.2.88",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -152,9 +152,13 @@ class ClaudeAdapter extends BaseAdapter {
152
152
  _findClaudeBinary() {
153
153
  const home = os.homedir();
154
154
 
155
- // Tier 0: Portable install at ~/.openagents/nodejs/node_modules/.bin/
156
- const portableBin = path.join(home, '.openagents', 'nodejs', 'node_modules', '.bin');
155
+ // Tier 0: Isolated runtime prefix (~/.openagents/runtimes/claude/)
157
156
  const ext = IS_WINDOWS ? '.cmd' : '';
157
+ const runtimeCandidate = path.join(home, '.openagents', 'runtimes', 'claude', 'node_modules', '.bin', `claude${ext}`);
158
+ if (fs.existsSync(runtimeCandidate)) return runtimeCandidate;
159
+
160
+ // Tier 0b: Legacy portable install at ~/.openagents/nodejs/node_modules/.bin/
161
+ const portableBin = path.join(home, '.openagents', 'nodejs', 'node_modules', '.bin');
158
162
  const portableCandidate = path.join(portableBin, `claude${ext}`);
159
163
  if (fs.existsSync(portableCandidate)) return portableCandidate;
160
164
 
@@ -271,9 +275,22 @@ class ClaudeAdapter extends BaseAdapter {
271
275
  // Find openagents binary (multi-tier)
272
276
  let oaBin = null;
273
277
  const home3 = os.homedir();
274
- // Tier 0: Portable install at ~/.openagents/nodejs/node_modules/.bin/
275
- const oaPortable = path.join(home3, '.openagents', 'nodejs', 'node_modules', '.bin', `openagents${IS_WINDOWS ? '.cmd' : ''}`);
276
- if (fs.existsSync(oaPortable)) oaBin = oaPortable;
278
+ // Tier 0: Check all isolated runtime prefixes for openagents binary
279
+ const oaExt = IS_WINDOWS ? '.cmd' : '';
280
+ const runtimesRoot = path.join(home3, '.openagents', 'runtimes');
281
+ try {
282
+ for (const d of fs.readdirSync(runtimesRoot, { withFileTypes: true })) {
283
+ if (d.isDirectory()) {
284
+ const candidate = path.join(runtimesRoot, d.name, 'node_modules', '.bin', `openagents${oaExt}`);
285
+ if (fs.existsSync(candidate)) { oaBin = candidate; break; }
286
+ }
287
+ }
288
+ } catch {}
289
+ // Tier 0b: Legacy portable install
290
+ if (!oaBin) {
291
+ const oaPortable = path.join(home3, '.openagents', 'nodejs', 'node_modules', '.bin', `openagents${oaExt}`);
292
+ if (fs.existsSync(oaPortable)) oaBin = oaPortable;
293
+ }
277
294
  // Tier 1: PATH
278
295
  if (!oaBin) try {
279
296
  if (IS_WINDOWS) {
package/src/installer.js CHANGED
@@ -4,7 +4,7 @@ const fs = require('fs');
4
4
  const os = require('os');
5
5
  const path = require('path');
6
6
  const { execSync, exec } = require('child_process');
7
- const { whichBinary, getEnhancedEnv } = require('./paths');
7
+ const { whichBinary, getEnhancedEnv, getRuntimePrefix } = require('./paths');
8
8
 
9
9
  /**
10
10
  * Manages installation and uninstallation of agent runtimes.
@@ -36,13 +36,9 @@ class Installer {
36
36
  * Checks binary on PATH first, then marker files.
37
37
  */
38
38
  isInstalled(agentType) {
39
- // Definitive check: does the package exist in our node_modules?
40
- const portableDir = path.join(os.homedir(), '.openagents', 'nodejs');
41
- const globalModules = path.join(portableDir, 'node_modules');
42
39
  const entry = this.registry.getEntry(agentType);
43
40
  const npmPkg = entry && entry.install ? entry.install.npm_package : null;
44
41
  const binary = entry && entry.install ? entry.install.binary : agentType;
45
- // Extract actual npm package name from install command (e.g. "npm install -g @anthropic-ai/claude-code" → "@anthropic-ai/claude-code")
46
42
  const installCmd = entry && entry.install ? this._getInstallCommand(entry.install) : null;
47
43
  let npmPkgFromCmd = null;
48
44
  if (!npmPkg && installCmd && installCmd.includes('npm install')) {
@@ -50,29 +46,31 @@ class Installer {
50
46
  if (match) npmPkgFromCmd = match[1];
51
47
  }
52
48
  const pkgName = npmPkg || npmPkgFromCmd || binary;
53
- const pkgDir = path.join(globalModules, pkgName);
54
- if (fs.existsSync(path.join(pkgDir, 'package.json'))) return true;
55
49
 
56
- // Marker file alone is not enough — package may have been pruned
57
- // (npm --prefix removes packages not in its dependency tree)
50
+ // Check isolated runtime prefix first (~/.openagents/runtimes/<type>/)
51
+ const runtimeModules = path.join(getRuntimePrefix(agentType), 'node_modules');
52
+ if (fs.existsSync(path.join(runtimeModules, pkgName, 'package.json'))) return true;
53
+
54
+ // Legacy: check shared prefix (~/.openagents/nodejs/node_modules/)
55
+ const legacyModules = path.join(os.homedir(), '.openagents', 'nodejs', 'node_modules');
56
+ if (fs.existsSync(path.join(legacyModules, pkgName, 'package.json'))) return true;
58
57
 
59
58
  // Fallback: check if binary exists on PATH (system install)
60
59
  const binaryPath = this._whichBinary(agentType);
61
60
  if (!binaryPath) {
62
- // Clean stale marker if package is gone
63
61
  try { fs.unlinkSync(path.join(this.markersDir, agentType)); } catch {}
64
62
  return false;
65
63
  }
66
64
 
67
- // Verify it's not a stale shim if binary is in our portable dir,
68
- // check the actual package exists (reuse variables from above)
69
- if (binaryPath.startsWith(portableDir)) {
70
- if (!fs.existsSync(path.join(pkgDir, 'package.json'))) {
71
- // Stale shim clean it up from all possible locations
72
- for (const dir of [portableDir, path.join(globalModules, '.bin')]) {
73
- for (const ext of ['', '.cmd', '.ps1']) {
74
- try { const p = path.join(dir, binary + ext); if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
75
- }
65
+ // Verify it's not a stale shim pointing to a missing package
66
+ const openagentsDir = path.join(os.homedir(), '.openagents');
67
+ if (binaryPath.startsWith(openagentsDir)) {
68
+ const hasRuntime = fs.existsSync(path.join(runtimeModules, pkgName, 'package.json'));
69
+ const hasLegacy = fs.existsSync(path.join(legacyModules, pkgName, 'package.json'));
70
+ if (!hasRuntime && !hasLegacy) {
71
+ // Stale shim clean it up
72
+ for (const ext of ['', '.cmd', '.ps1']) {
73
+ try { const p = path.join(path.dirname(binaryPath), binary + ext); if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
76
74
  }
77
75
  return false;
78
76
  }
@@ -195,7 +193,8 @@ class Installer {
195
193
 
196
194
  // Use bundled node/npm if system npm not available
197
195
  if (cmd.startsWith('npm install')) {
198
- const prefixDir = path.join(os.homedir(), '.openagents', 'nodejs');
196
+ const prefixDir = getRuntimePrefix(agentType);
197
+ fs.mkdirSync(prefixDir, { recursive: true });
199
198
  const args = cmd.replace('npm install', 'install --save').replace(' -g ', ` --prefix "${prefixDir}" `);
200
199
  cmd = this._resolveNpmCommand(args);
201
200
  }
@@ -231,9 +230,10 @@ class Installer {
231
230
  // Resolve npm command
232
231
  let cmd = rawCmd;
233
232
  if (rawCmd.startsWith('npm install')) {
234
- const prefixDir2 = path.join(os.homedir(), '.openagents', 'nodejs');
233
+ const prefixDir = getRuntimePrefix(agentType);
234
+ fs.mkdirSync(prefixDir, { recursive: true });
235
235
  // Use --save so npm tracks the package in package.json (prevents pruning on next install)
236
- const args = rawCmd.replace('npm install', 'install --loglevel=verbose --save').replace(' -g ', ` --prefix "${prefixDir2}" `);
236
+ const args = rawCmd.replace('npm install', 'install --loglevel=verbose --save').replace(' -g ', ` --prefix "${prefixDir}" `);
237
237
  cmd = this._resolveNpmCommand(args);
238
238
  } else if (rawCmd.startsWith('pip install') || rawCmd.startsWith('pipx install')) {
239
239
  cmd = rawCmd; // pip commands stay as-is
@@ -281,14 +281,14 @@ class Installer {
281
281
  }
282
282
 
283
283
  const installCmd = this._getInstallCommand(entry.install);
284
- const uninstallCmd = this._deriveUninstallCommand(installCmd);
284
+ const uninstallCmd = this._deriveUninstallCommand(installCmd, agentType);
285
285
  if (!uninstallCmd) {
286
286
  throw new Error(`Cannot derive uninstall command for ${agentType}`);
287
287
  }
288
288
 
289
289
  const output = await this._execShell(uninstallCmd);
290
290
  this._markUninstalled(agentType);
291
- // Clean stale shims on Windows (npm sometimes leaves .cmd/.ps1 files behind)
291
+ // Clean stale shims (npm sometimes leaves .cmd/.ps1 files behind)
292
292
  this._cleanStaleShims(agentType);
293
293
  return { success: true, output };
294
294
  }
@@ -297,9 +297,15 @@ class Installer {
297
297
  const entry = this.registry.getEntry(agentType);
298
298
  const binary = entry && entry.install ? entry.install.binary : agentType;
299
299
  if (!binary) return;
300
- const portableDir = path.join(os.homedir(), '.openagents', 'nodejs');
301
- const binDir = path.join(portableDir, 'node_modules', '.bin');
302
- for (const dir of [portableDir, binDir]) {
300
+
301
+ // Clean from isolated runtime prefix
302
+ const runtimeBin = path.join(getRuntimePrefix(agentType), 'node_modules', '.bin');
303
+
304
+ // Clean from legacy shared prefix
305
+ const legacyDir = path.join(os.homedir(), '.openagents', 'nodejs');
306
+ const legacyBin = path.join(legacyDir, 'node_modules', '.bin');
307
+
308
+ for (const dir of [runtimeBin, legacyDir, legacyBin]) {
303
309
  for (const ext of ['', '.cmd', '.ps1']) {
304
310
  const shimPath = path.join(dir, binary + ext);
305
311
  try { if (fs.existsSync(shimPath)) fs.unlinkSync(shimPath); } catch {}
@@ -318,7 +324,7 @@ class Installer {
318
324
  }
319
325
 
320
326
  const installCmd = this._getInstallCommand(entry.install);
321
- let rawCmd = this._deriveUninstallCommand(installCmd);
327
+ let rawCmd = this._deriveUninstallCommand(installCmd, agentType);
322
328
  if (!rawCmd) {
323
329
  throw new Error(`Cannot derive uninstall command for ${agentType}`);
324
330
  }
@@ -372,13 +378,15 @@ class Installer {
372
378
 
373
379
  /**
374
380
  * Derive uninstall command from install command.
381
+ * @param {string} installCmd
382
+ * @param {string} [agentType] - used to resolve the isolated runtime prefix
375
383
  */
376
- _deriveUninstallCommand(installCmd) {
384
+ _deriveUninstallCommand(installCmd, agentType) {
377
385
  if (!installCmd) return null;
378
386
 
379
- // npm install -g <pkg> → npm uninstall --prefix <dir> <pkg>
387
+ // npm install -g <pkg> → npm uninstall --prefix <runtimeDir> <pkg>
380
388
  if (installCmd.includes('npm install')) {
381
- const prefixDir = path.join(os.homedir(), '.openagents', 'nodejs');
389
+ const prefixDir = agentType ? getRuntimePrefix(agentType) : path.join(os.homedir(), '.openagents', 'nodejs');
382
390
  return installCmd
383
391
  .replace('npm install -g', `npm uninstall --prefix "${prefixDir}"`)
384
392
  .replace('npm install', 'npm uninstall')
package/src/paths.js CHANGED
@@ -42,11 +42,24 @@ function getExtraBinDirs() {
42
42
  if (nodeDir) _push(dirs, nodeDir);
43
43
  } catch {}
44
44
 
45
- // Add portable Node.js directory (~/.openagents/nodejs/) and agent binaries
45
+ // Add portable Node.js directory (~/.openagents/nodejs/)
46
46
  const portableNode = path.join(HOME, '.openagents', 'nodejs');
47
- _push(dirs, path.join(portableNode, 'node_modules', '.bin'));
48
47
  _push(dirs, portableNode);
49
48
 
49
+ // Core library bin (~/.openagents/core/node_modules/.bin)
50
+ _push(dirs, path.join(HOME, '.openagents', 'core', 'node_modules', '.bin'));
51
+
52
+ // Per-agent runtime bins (~/.openagents/runtimes/<type>/node_modules/.bin)
53
+ const runtimesDir = path.join(HOME, '.openagents', 'runtimes');
54
+ try {
55
+ for (const d of fs.readdirSync(runtimesDir, { withFileTypes: true })) {
56
+ if (d.isDirectory()) _push(dirs, path.join(runtimesDir, d.name, 'node_modules', '.bin'));
57
+ }
58
+ } catch {}
59
+
60
+ // Legacy: shared node_modules/.bin (for backward compat with pre-isolation installs)
61
+ _push(dirs, path.join(portableNode, 'node_modules', '.bin'));
62
+
50
63
  // Filter to existing directories only, deduplicate
51
64
  const seen = new Set();
52
65
  const currentPATH = process.env.PATH || '';
@@ -178,8 +191,7 @@ function _addUnixPaths(dirs) {
178
191
  _push(dirs, '/usr/local/bin');
179
192
  _push(dirs, '/usr/bin');
180
193
 
181
- // npm global all installs use --prefix ~/.openagents/nodejs
182
- // No need for ~/.npm-global or ~/.openagents/npm-global
194
+ // npm agents install to isolated prefixes: ~/.openagents/runtimes/<type>/
183
195
 
184
196
  // nvm
185
197
  const nvmDir = process.env.NVM_DIR || path.join(HOME, '.nvm');
@@ -272,11 +284,28 @@ function _resolveNvmVersion(nvmDir, alias) {
272
284
  return null;
273
285
  }
274
286
 
287
+ /**
288
+ * Get the isolated npm prefix directory for an agent runtime.
289
+ * Each agent type gets its own prefix to prevent cross-agent interference.
290
+ */
291
+ function getRuntimePrefix(agentType) {
292
+ return path.join(HOME, '.openagents', 'runtimes', agentType);
293
+ }
294
+
295
+ /**
296
+ * Get the core library directory (separate from agent runtimes).
297
+ */
298
+ function getCorePrefix() {
299
+ return path.join(HOME, '.openagents', 'core');
300
+ }
301
+
275
302
  module.exports = {
276
303
  getExtraBinDirs,
277
304
  getEnhancedPATH,
278
305
  getEnhancedEnv,
279
306
  whichBinary,
307
+ getRuntimePrefix,
308
+ getCorePrefix,
280
309
  IS_WINDOWS,
281
310
  IS_MACOS,
282
311
  SEP,
package/src/registry.js CHANGED
@@ -127,7 +127,7 @@ class Registry {
127
127
  */
128
128
  async refresh() {
129
129
  const remote = await this._fetchRemote();
130
- if (remote) this._catalog = remote;
130
+ if (remote) this._catalog = this._mergeBundled(remote);
131
131
  return this._catalog || this.getCatalogSync();
132
132
  }
133
133