@openagents-org/agent-launcher 0.2.90 → 0.2.92

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.90",
3
+ "version": "0.2.92",
4
4
  "description": "OpenAgents Launcher — install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
@@ -130,7 +130,7 @@ class ClaudeAdapter extends BaseAdapter {
130
130
  if (IS_WINDOWS && binPath.toLowerCase().endsWith('.cmd')) {
131
131
  const cmdDir = path.dirname(path.resolve(binPath));
132
132
  const cmdContent = fs.readFileSync(binPath, 'utf-8');
133
- const jsMatch = cmdContent.match(/%dp0%\\([^\s"*?]+\.js)/i);
133
+ const jsMatch = cmdContent.match(/%dp0%\\([^\s"*?]+\.m?js)/i);
134
134
  if (jsMatch) {
135
135
  return [nodeBin, path.resolve(cmdDir, jsMatch[1])];
136
136
  }
@@ -68,12 +68,60 @@ class OpenClawAdapter extends BaseAdapter {
68
68
  if (fs.existsSync(mjs)) return mjs;
69
69
 
70
70
  // Fallback: check if openclaw is on PATH (system install)
71
+ // On Windows, resolve .cmd shim to actual .mjs path to avoid spawn issues
71
72
  try {
72
- const cmd = IS_WINDOWS ? 'where openclaw' : 'which openclaw';
73
+ const cmd = IS_WINDOWS ? 'where openclaw.cmd' : 'which openclaw';
73
74
  const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
74
75
  .split(/\r?\n/)[0].trim();
75
- if (result) return result;
76
+ if (result) {
77
+ const resolved = this._resolveShimToMjs(result);
78
+ if (resolved) return resolved;
79
+ // On Unix, which returns the actual binary/symlink
80
+ if (!IS_WINDOWS) return result;
81
+ }
76
82
  } catch {}
83
+ // Windows: also try without .cmd extension (for system installs on PATH)
84
+ if (IS_WINDOWS) {
85
+ try {
86
+ const result = execSync('where openclaw', { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
87
+ .split(/\r?\n/)[0].trim();
88
+ if (result) {
89
+ // Try the .cmd variant of this path
90
+ const cmdPath = result.replace(/(?:\.cmd)?$/i, '.cmd');
91
+ const resolved = this._resolveShimToMjs(cmdPath);
92
+ if (resolved) return resolved;
93
+ }
94
+ } catch {}
95
+ }
96
+ return null;
97
+ }
98
+
99
+ /**
100
+ * Resolve a .cmd shim or Unix symlink to the actual openclaw.mjs path.
101
+ * On Windows: parses the .cmd shim to extract %dp0%\..\openclaw\openclaw.mjs
102
+ * On Unix: follows symlink to the .mjs file
103
+ */
104
+ _resolveShimToMjs(binPath) {
105
+ if (IS_WINDOWS) {
106
+ try {
107
+ if (!binPath.toLowerCase().endsWith('.cmd')) return null;
108
+ const cmdContent = fs.readFileSync(binPath, 'utf-8');
109
+ const match = cmdContent.match(/%dp0%\\([^\s"*?]+\.mjs)/i)
110
+ || cmdContent.match(/%dp0%\\([^\s"*?]+\.js)/i);
111
+ if (match) {
112
+ const cmdDir = path.dirname(path.resolve(binPath));
113
+ return path.resolve(cmdDir, match[1]);
114
+ }
115
+ } catch {}
116
+ } else {
117
+ try {
118
+ let target = binPath;
119
+ if (fs.lstatSync(binPath).isSymbolicLink()) {
120
+ target = path.resolve(path.dirname(binPath), fs.readlinkSync(binPath));
121
+ }
122
+ if (target.endsWith('.mjs') || target.endsWith('.js')) return target;
123
+ } catch {}
124
+ }
77
125
  return null;
78
126
  }
79
127
 
@@ -246,23 +294,38 @@ class OpenClawAdapter extends BaseAdapter {
246
294
  this._log('Spawn: stderr → ' + stderrFile);
247
295
 
248
296
  // Always spawn node + openclaw.mjs directly (no shims, no cmd.exe, cross-platform)
297
+ // This avoids Windows .cmd shim issues and Unicode path encoding problems.
249
298
  const portableDir = path.join(os.homedir(), '.openagents', 'nodejs');
250
- // Unified path first (symlink on Unix), then legacy bin/ fallback
299
+ // Unified path first (symlink on Unix), then legacy bin/ fallback, then system node
251
300
  const nodeUnified = path.join(portableDir, IS_WINDOWS ? 'node.exe' : 'node');
252
- const nodeBin = fs.existsSync(nodeUnified) ? nodeUnified : path.join(portableDir, 'bin', 'node');
253
- // Check isolated runtime first, then legacy
254
- const runtimeMjs = path.join(getRuntimePrefix('openclaw'), 'node_modules', 'openclaw', 'openclaw.mjs');
255
- const legacyMjs = path.join(portableDir, 'node_modules', 'openclaw', 'openclaw.mjs');
256
- const openclawMjs = fs.existsSync(runtimeMjs) ? runtimeMjs : legacyMjs;
301
+ let nodeBin = fs.existsSync(nodeUnified) ? nodeUnified : path.join(portableDir, 'bin', 'node');
302
+ if (!fs.existsSync(nodeBin)) {
303
+ try {
304
+ const cmd = IS_WINDOWS ? 'where node.exe' : 'which node';
305
+ nodeBin = execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
306
+ .split(/\r?\n/)[0].trim();
307
+ } catch { nodeBin = 'node'; }
308
+ }
257
309
 
310
+ // binary from _findOpenclawBinary() is already resolved to .mjs when possible
258
311
  let spawnBin, spawnArgs;
259
- if (fs.existsSync(nodeBin) && fs.existsSync(openclawMjs)) {
312
+ if (binary && binary.endsWith('.mjs')) {
313
+ // Direct node + .mjs invocation (works for managed, legacy, AND global installs)
260
314
  spawnBin = nodeBin;
261
- spawnArgs = [openclawMjs, ...args];
315
+ spawnArgs = [binary, ...args];
262
316
  } else {
263
- // Fallback: try the binary path directly (system install)
264
- spawnBin = binary;
265
- spawnArgs = args;
317
+ // Check managed locations explicitly
318
+ const runtimeMjs = path.join(getRuntimePrefix('openclaw'), 'node_modules', 'openclaw', 'openclaw.mjs');
319
+ const legacyMjs = path.join(portableDir, 'node_modules', 'openclaw', 'openclaw.mjs');
320
+ const openclawMjs = fs.existsSync(runtimeMjs) ? runtimeMjs : (fs.existsSync(legacyMjs) ? legacyMjs : null);
321
+ if (openclawMjs) {
322
+ spawnBin = nodeBin;
323
+ spawnArgs = [openclawMjs, ...args];
324
+ } else {
325
+ // Last resort: spawn binary directly with shell (handles .cmd on Windows)
326
+ spawnBin = binary;
327
+ spawnArgs = args;
328
+ }
266
329
  }
267
330
  const proc = spawn(spawnBin, spawnArgs, {
268
331
  stdio: ['ignore', 'pipe', stderrFd],
package/src/cli.js CHANGED
@@ -207,10 +207,10 @@ async function cmdSearch(connector, flags, positional) {
207
207
  try {
208
208
  catalog = await connector.getCatalog();
209
209
  } catch {
210
- catalog = connector.registry.getCatalogSync().map((e) => ({
211
- ...e,
212
- installed: connector.isInstalled(e.name),
213
- }));
210
+ catalog = connector.registry.getCatalogSync().map((e) => {
211
+ const info = connector.installer.getInstallInfo(e.name);
212
+ return { ...e, installed: info.installed, managed: info.managed, location: info.location };
213
+ });
214
214
  }
215
215
 
216
216
  if (query) {
@@ -253,9 +253,10 @@ async function cmdRuntimes(connector) {
253
253
  try {
254
254
  catalog = await connector.getCatalog();
255
255
  } catch {
256
- catalog = connector.registry.getCatalogSync().map((e) => ({
257
- ...e, installed: connector.isInstalled(e.name),
258
- }));
256
+ catalog = connector.registry.getCatalogSync().map((e) => {
257
+ const info = connector.installer.getInstallInfo(e.name);
258
+ return { ...e, installed: info.installed, managed: info.managed, location: info.location };
259
+ });
259
260
  }
260
261
 
261
262
  const installed = catalog.filter((e) => e.installed);
package/src/index.js CHANGED
@@ -33,10 +33,10 @@ class AgentConnector {
33
33
  async getCatalog() {
34
34
  const catalog = await this.registry.getCatalog();
35
35
  // Always re-check installed status (don't trust cached value)
36
- return catalog.map((entry) => ({
37
- ...entry,
38
- installed: this.installer.isInstalled(entry.name),
39
- }));
36
+ return catalog.map((entry) => {
37
+ const info = this.installer.getInstallInfo(entry.name);
38
+ return { ...entry, installed: info.installed, managed: info.managed, location: info.location };
39
+ });
40
40
  }
41
41
 
42
42
  /**
package/src/installer.js CHANGED
@@ -35,7 +35,22 @@ class Installer {
35
35
  * Check if an agent type is installed.
36
36
  * Checks binary on PATH first, then marker files.
37
37
  */
38
+ /**
39
+ * Check if an agent type is installed.
40
+ * @returns {boolean} true if installed (any location)
41
+ */
38
42
  isInstalled(agentType) {
43
+ return this.getInstallInfo(agentType).installed;
44
+ }
45
+
46
+ /**
47
+ * Get detailed install info for an agent type.
48
+ * @returns {{ installed: boolean, managed: boolean, location: string|null }}
49
+ * - installed: true if the agent is found anywhere
50
+ * - managed: true if installed inside ~/.openagents/ (can be uninstalled by launcher)
51
+ * - location: 'runtime' | 'legacy' | 'global' | null
52
+ */
53
+ getInstallInfo(agentType) {
39
54
  const entry = this.registry.getEntry(agentType);
40
55
  const npmPkg = entry && entry.install ? entry.install.npm_package : null;
41
56
  const binary = entry && entry.install ? entry.install.binary : agentType;
@@ -49,17 +64,21 @@ class Installer {
49
64
 
50
65
  // Check isolated runtime prefix first (~/.openagents/runtimes/<type>/)
51
66
  const runtimeModules = path.join(getRuntimePrefix(agentType), 'node_modules');
52
- if (fs.existsSync(path.join(runtimeModules, pkgName, 'package.json'))) return true;
67
+ if (fs.existsSync(path.join(runtimeModules, pkgName, 'package.json'))) {
68
+ return { installed: true, managed: true, location: 'runtime' };
69
+ }
53
70
 
54
71
  // Legacy: check shared prefix (~/.openagents/nodejs/node_modules/)
55
72
  const legacyModules = path.join(os.homedir(), '.openagents', 'nodejs', 'node_modules');
56
- if (fs.existsSync(path.join(legacyModules, pkgName, 'package.json'))) return true;
73
+ if (fs.existsSync(path.join(legacyModules, pkgName, 'package.json'))) {
74
+ return { installed: true, managed: true, location: 'legacy' };
75
+ }
57
76
 
58
77
  // Fallback: check if binary exists on PATH (system install)
59
78
  const binaryPath = this._whichBinary(agentType);
60
79
  if (!binaryPath) {
61
80
  try { fs.unlinkSync(path.join(this.markersDir, agentType)); } catch {}
62
- return false;
81
+ return { installed: false, managed: false, location: null };
63
82
  }
64
83
 
65
84
  // Verify it's not a stale shim pointing to a missing package
@@ -72,11 +91,13 @@ class Installer {
72
91
  for (const ext of ['', '.cmd', '.ps1']) {
73
92
  try { const p = path.join(path.dirname(binaryPath), binary + ext); if (fs.existsSync(p)) fs.unlinkSync(p); } catch {}
74
93
  }
75
- return false;
94
+ return { installed: false, managed: false, location: null };
76
95
  }
96
+ return { installed: true, managed: true, location: 'legacy' };
77
97
  }
78
98
 
79
- return true;
99
+ // Binary found outside ~/.openagents/ — global/system install
100
+ return { installed: true, managed: false, location: 'global' };
80
101
  }
81
102
 
82
103
  /**