@openagents-org/agent-launcher 0.2.187 → 0.2.189

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.187",
3
+ "version": "0.2.189",
4
4
  "description": "OpenAgents Launcher \u2014 install, configure, and run AI coding agents from your terminal",
5
5
  "main": "src/index.js",
6
6
  "bin": {
package/registry.json CHANGED
@@ -333,6 +333,10 @@
333
333
  "env_vars": [
334
334
  "CURSOR_API_KEY"
335
335
  ],
336
+ "creds_file": "~/.cursor/cli-config.json",
337
+ "creds_key": "authInfo",
338
+ "creds_no_parse": true,
339
+ "unverifiable": true,
336
340
  "login_command": "cursor-agent login"
337
341
  },
338
342
  "env_config": [
@@ -42,7 +42,7 @@ const {
42
42
  buildModePrompt,
43
43
  buildPinnedSections,
44
44
  } = require('./workspace-prompt');
45
- const { whichBinary, getEnhancedEnv, defaultAgentWorkdir } = require('../paths');
45
+ const { whichBinary, whereBinary, getEnhancedEnv, defaultAgentWorkdir } = require('../paths');
46
46
 
47
47
  const IS_WINDOWS = process.platform === 'win32';
48
48
 
@@ -105,10 +105,9 @@ function findGooseBinary() {
105
105
  try {
106
106
  const env = getEnhancedEnv();
107
107
  if (IS_WINDOWS) {
108
- const r = execSync('where goose.exe 2>nul || where goose.cmd 2>nul || where goose 2>nul', {
109
- encoding: 'utf-8', timeout: 5000, windowsHide: true, env,
110
- });
111
- const hit = r.split(/\r?\n/)[0].trim();
108
+ // In-process PATH walk, not `where`: its OEM-codepage output mangled
109
+ // non-ASCII profile paths into a hit that could never be spawned.
110
+ const hit = whereBinary(['goose'], env);
112
111
  if (hit) return hit;
113
112
  } else {
114
113
  const hit = execSync('command -v goose', {
@@ -21,6 +21,7 @@ const path = require('path');
21
21
  const { execFile, execFileSync } = require('child_process');
22
22
 
23
23
  const { redactSecrets } = require('./nanoclaw-protocol');
24
+ const { whereAll } = require('../paths');
24
25
 
25
26
  const IS_WINDOWS = process.platform === 'win32';
26
27
 
@@ -176,14 +177,7 @@ function looksLikeNanoclaw(dir) {
176
177
  /** Follow `ncl` on PATH back to the checkout root (bin/ncl → <root>). */
177
178
  function homeFromNclBinary() {
178
179
  try {
179
- const which = IS_WINDOWS ? 'where' : 'which';
180
- const out = execFileSync(which, ['ncl'], {
181
- encoding: 'utf-8',
182
- timeout: 5000,
183
- stdio: ['ignore', 'pipe', 'ignore'],
184
- })
185
- .split(/\r?\n/)[0]
186
- .trim();
180
+ const out = whichSafe('ncl');
187
181
  if (!out) return null;
188
182
  let real = out;
189
183
  try {
@@ -233,15 +227,9 @@ function findNanoclawHome(env = process.env) {
233
227
 
234
228
  function whichSafe(bin) {
235
229
  try {
236
- const which = IS_WINDOWS ? 'where' : 'which';
237
- const out = execFileSync(which, [bin], {
238
- encoding: 'utf-8',
239
- timeout: 5000,
240
- stdio: ['ignore', 'pipe', 'ignore'],
241
- })
242
- .split(/\r?\n/)[0]
243
- .trim();
244
- return out || null;
230
+ // Walks PATH in-process: `where` prints in the OEM codepage, which
231
+ // mangled any non-ASCII profile path when read back as UTF-8.
232
+ return whereAll(bin)[0] || null;
245
233
  } catch {
246
234
  return null;
247
235
  }
@@ -23,7 +23,7 @@ const { spawn } = require('../wsl');
23
23
  const BaseAdapter = require('./base');
24
24
  const { formatAttachmentsForPrompt } = require('./utils');
25
25
  const { buildOpenclawSkillMd, buildOpenclawSystemPrompt, workspaceSkillName } = require('./workspace-prompt');
26
- const { getRuntimePrefix } = require('../paths');
26
+ const { getRuntimePrefix, whereBinary } = require('../paths');
27
27
 
28
28
  const IS_WINDOWS = process.platform === 'win32';
29
29
  const OPENCLAW_STATE_DIR = path.join(
@@ -31,6 +31,41 @@ const OPENCLAW_STATE_DIR = path.join(
31
31
  '.openclaw'
32
32
  );
33
33
 
34
+ /**
35
+ * Archive the legacy auth-profiles.json this adapter used to write.
36
+ *
37
+ * Current OpenClaw keeps credentials in SQLite and refuses to run while a
38
+ * legacy JSON store sits beside an empty one ("requires legacy credential
39
+ * migration; run openclaw doctor --fix"); beside a populated one it silently
40
+ * ignores the file, so a changed key never took effect either. The key now
41
+ * reaches OpenClaw through the env and openclaw.json, so the file only gets
42
+ * in the way.
43
+ *
44
+ * Only a file made entirely of our entries is moved: the `custom` provider
45
+ * exists only because configureNativeAuth created it, and any other entry must
46
+ * carry a key we hold ourselves. A profile the user made (OpenClaw's own
47
+ * paste-token also names them `<provider>:manual`) keeps the file in place,
48
+ * for `openclaw doctor --fix` to migrate. Renamed, never deleted.
49
+ *
50
+ * @param {Array<string|undefined>} knownKeys API keys this agent is configured with
51
+ */
52
+ function retireOwnLegacyAuthProfiles(knownKeys) {
53
+ const file = path.join(OPENCLAW_STATE_DIR, 'agents', 'main', 'agent', 'auth-profiles.json');
54
+ let data;
55
+ try { data = JSON.parse(fs.readFileSync(file, 'utf-8')); } catch { return false; }
56
+ const keys = new Set((knownKeys || []).filter(Boolean));
57
+ const profiles = Object.values((data && data.profiles) || {});
58
+ const ours = profiles.every((p) => p && p.type === 'token'
59
+ && (p.provider === 'custom' || keys.has(p.token)));
60
+ if (!ours) return false;
61
+ try {
62
+ fs.renameSync(file, `${file}.openagents-retired`);
63
+ return true;
64
+ } catch {
65
+ return false;
66
+ }
67
+ }
68
+
34
69
  class OpenClawAdapter extends BaseAdapter {
35
70
  /**
36
71
  * @param {object} opts - BaseAdapter opts plus:
@@ -54,6 +89,14 @@ class OpenClawAdapter extends BaseAdapter {
54
89
  this._log('OpenClaw binary not found — agent will not be able to process messages');
55
90
  }
56
91
 
92
+ // Agents configured before the key stopped going into auth-profiles.json
93
+ // still have that file; clear it before the first run rather than on the
94
+ // next save.
95
+ const env = this.agentEnv || {};
96
+ if (retireOwnLegacyAuthProfiles([env.LLM_API_KEY, env.OPENAI_API_KEY, env.ANTHROPIC_API_KEY])) {
97
+ this._log('Archived the legacy auth-profiles.json this launcher wrote (OpenClaw now stores credentials in SQLite)');
98
+ }
99
+
57
100
  // Install workspace skill
58
101
  this._installWorkspaceSkill();
59
102
  }
@@ -74,31 +117,17 @@ class OpenClawAdapter extends BaseAdapter {
74
117
  const mjs = path.join(portableDir, 'node_modules', 'openclaw', 'openclaw.mjs');
75
118
  if (fs.existsSync(mjs)) return mjs;
76
119
 
77
- // Fallback: check if openclaw is on PATH (system install)
78
- // On Windows, resolve .cmd shim to actual .mjs path to avoid spawn issues
79
- try {
80
- const cmd = IS_WINDOWS ? 'where openclaw.cmd' : 'which openclaw';
81
- const result = execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
82
- .split(/\r?\n/)[0].trim();
83
- if (result) {
84
- const resolved = this._resolveShimToMjs(result);
85
- if (resolved) return resolved;
86
- // On Unix, which returns the actual binary/symlink
87
- if (!IS_WINDOWS) return result;
88
- }
89
- } catch {}
90
- // Windows: also try without .cmd extension (for system installs on PATH)
91
- if (IS_WINDOWS) {
92
- try {
93
- const result = execSync('where openclaw', { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
94
- .split(/\r?\n/)[0].trim();
95
- if (result) {
96
- // Try the .cmd variant of this path
97
- const cmdPath = result.replace(/(?:\.cmd)?$/i, '.cmd');
98
- const resolved = this._resolveShimToMjs(cmdPath);
99
- if (resolved) return resolved;
100
- }
101
- } catch {}
120
+ // Fallback: a copy the user installed themselves (npm -g, nvm, a custom
121
+ // npm prefix). whereBinary searches the directories itself instead of
122
+ // shelling out to `where`, which prints in the OEM codepage and mangled
123
+ // any non-ASCII profile path (C:\Users\王…) past recognition.
124
+ const found = whereBinary('openclaw');
125
+ if (found) {
126
+ // Windows: resolve the .cmd shim to openclaw.mjs to avoid spawn issues.
127
+ // Unix: `which` hands back the binary or its symlink, either runs as is.
128
+ const resolved = this._resolveShimToMjs(found);
129
+ if (resolved) return resolved;
130
+ if (!IS_WINDOWS) return found;
102
131
  }
103
132
  return null;
104
133
  }
@@ -359,13 +388,7 @@ class OpenClawAdapter extends BaseAdapter {
359
388
  // Unified path first (symlink on Unix), then legacy bin/ fallback, then system node
360
389
  const nodeUnified = path.join(portableDir, IS_WINDOWS ? 'node.exe' : 'node');
361
390
  let nodeBin = fs.existsSync(nodeUnified) ? nodeUnified : path.join(portableDir, 'bin', 'node');
362
- if (!fs.existsSync(nodeBin)) {
363
- try {
364
- const cmd = IS_WINDOWS ? 'where node.exe' : 'which node';
365
- nodeBin = execSync(cmd, { encoding: 'utf-8', timeout: 5000, stdio: ['pipe', 'pipe', 'pipe'] })
366
- .split(/\r?\n/)[0].trim();
367
- } catch { nodeBin = 'node'; }
368
- }
391
+ if (!fs.existsSync(nodeBin)) nodeBin = whereBinary('node') || 'node';
369
392
 
370
393
  // binary from _findOpenclawBinary() is already resolved to .mjs when possible
371
394
  let spawnBin, spawnArgs;
@@ -606,9 +629,10 @@ class OpenClawAdapter extends BaseAdapter {
606
629
  * LLM_API_KEY / LLM_BASE_URL / LLM_MODEL values.
607
630
  * Called by the Launcher's saveAgentEnv when type === 'openclaw'.
608
631
  *
609
- * For standard providers (OpenAI, Anthropic), uses auth-profiles.json.
610
- * For custom endpoints, uses models.providers in openclaw.json which
611
- * gives full tool support via the CLI gateway mode.
632
+ * For standard providers (OpenAI, Anthropic), the key travels in the env
633
+ * and only the model is set here. For custom endpoints, uses
634
+ * models.providers in openclaw.json which gives full tool support via the
635
+ * CLI gateway mode.
612
636
  */
613
637
  static configureNativeAuth(env) {
614
638
  const apiKey = env.LLM_API_KEY;
@@ -622,26 +646,19 @@ class OpenClawAdapter extends BaseAdapter {
622
646
  const isAnthropic = baseUrl.includes('api.anthropic.com');
623
647
  const configFile = path.join(OPENCLAW_STATE_DIR, 'openclaw.json');
624
648
 
649
+ // The key itself never goes into auth-profiles.json any more: current
650
+ // OpenClaw keeps credentials in SQLite and refuses to start while a legacy
651
+ // JSON store sits beside an empty one ("requires legacy credential
652
+ // migration; run openclaw doctor --fix"). A standard provider reads the
653
+ // key from the OPENAI_API_KEY / ANTHROPIC_API_KEY the resolve rules put
654
+ // in its env; a custom one from models.providers below.
655
+ retireOwnLegacyAuthProfiles([apiKey]);
656
+
625
657
  if (isOpenAI || isAnthropic) {
626
- // Standard provider — use auth-profiles.json
627
658
  const provider = isAnthropic ? 'anthropic' : 'openai';
628
- const profileId = `${provider}:manual`;
629
- const agentDir = path.join(OPENCLAW_STATE_DIR, 'agents', 'main', 'agent');
630
-
631
- try {
632
- fs.mkdirSync(agentDir, { recursive: true });
633
- const authFile = path.join(agentDir, 'auth-profiles.json');
634
- let authData = { version: 1, profiles: {} };
635
- try { authData = JSON.parse(fs.readFileSync(authFile, 'utf-8')); } catch {}
636
- authData.profiles = authData.profiles || {};
637
- authData.profiles[profileId] = { type: 'token', provider, token: apiKey };
638
- authData.lastGood = authData.lastGood || {};
639
- authData.lastGood[provider] = profileId;
640
- fs.writeFileSync(authFile, JSON.stringify(authData, null, 2), 'utf-8');
641
- } catch {}
642
-
643
659
  // Set model
644
660
  try {
661
+ fs.mkdirSync(OPENCLAW_STATE_DIR, { recursive: true });
645
662
  let config = {};
646
663
  try { config = JSON.parse(fs.readFileSync(configFile, 'utf-8')); } catch {}
647
664
  config.agents = config.agents || {};
@@ -673,22 +690,9 @@ class OpenClawAdapter extends BaseAdapter {
673
690
 
674
691
  fs.writeFileSync(configFile, JSON.stringify(config, null, 2), 'utf-8');
675
692
  } catch {}
676
-
677
- // Also write auth-profiles.json for the custom provider
678
- try {
679
- const agentDir = path.join(OPENCLAW_STATE_DIR, 'agents', 'main', 'agent');
680
- fs.mkdirSync(agentDir, { recursive: true });
681
- const authFile = path.join(agentDir, 'auth-profiles.json');
682
- let authData = { version: 1, profiles: {} };
683
- try { authData = JSON.parse(fs.readFileSync(authFile, 'utf-8')); } catch {}
684
- authData.profiles = authData.profiles || {};
685
- authData.profiles['custom:manual'] = { type: 'token', provider: 'custom', token: apiKey };
686
- authData.lastGood = authData.lastGood || {};
687
- authData.lastGood.custom = 'custom:manual';
688
- fs.writeFileSync(authFile, JSON.stringify(authData, null, 2), 'utf-8');
689
- } catch {}
690
693
  }
691
694
  }
692
695
  }
693
696
 
694
697
  module.exports = OpenClawAdapter;
698
+ module.exports.retireOwnLegacyAuthProfiles = retireOwnLegacyAuthProfiles;
package/src/installer.js CHANGED
@@ -14,6 +14,7 @@ const {
14
14
  resolveManagedNpmBinary,
15
15
  resolveManagedNpmPackageBin,
16
16
  isNodeShebangScript,
17
+ whereAll,
17
18
  } = require('./paths');
18
19
  const { isWslBinary, bridgedCommandString, wslHomeUnc } = require('./wsl');
19
20
  const { canBlock } = require('./probe-mode');
@@ -2087,18 +2088,10 @@ class Installer {
2087
2088
  */
2088
2089
  _hasSystemNode(bundledDir) {
2089
2090
  try {
2090
- const { execFileSync } = require('child_process');
2091
- const cmd = process.platform === 'win32' ? 'where' : 'which';
2092
- const args = process.platform === 'win32' ? ['node'] : ['node'];
2093
- const out = execFileSync(cmd, args, {
2094
- encoding: 'utf-8',
2095
- stdio: ['ignore', 'pipe', 'ignore'],
2096
- timeout: 5000,
2097
- windowsHide: true,
2098
- env: { ...process.env },
2099
- });
2091
+ // In-process PATH walk: `where` prints in the OEM codepage, so on a
2092
+ // non-ASCII profile its UTF-8-decoded lines never matched bundledDir.
2100
2093
  const prefix = (bundledDir || '').toLowerCase();
2101
- for (const line of out.split(/\r?\n/).map(s => s.trim()).filter(Boolean)) {
2094
+ for (const line of whereAll('node')) {
2102
2095
  if (!prefix || !line.toLowerCase().startsWith(prefix)) return true;
2103
2096
  }
2104
2097
  return false;
@@ -2446,7 +2439,10 @@ class Installer {
2446
2439
  const psTokens = /\b(irm|iwr|iex|Invoke-RestMethod|Invoke-WebRequest|Invoke-Expression|Expand-Archive|Get-[A-Z]\w*|Set-[A-Z]\w*)\b/;
2447
2440
  if (!psTokens.test(cmd)) return cmd;
2448
2441
  const escaped = cmd.replace(/"/g, '\\"');
2449
- return `powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "${escaped}"`;
2442
+ // PowerShell writes to a pipe in the OEM codepage (936 on zh-CN) and this
2443
+ // output is read as UTF-8, so a non-ASCII path in an installer's log or
2444
+ // error came back garbled. Switch the console encoding first.
2445
+ return `powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "[Console]::OutputEncoding=[Text.Encoding]::UTF8; ${escaped}"`;
2450
2446
  }
2451
2447
 
2452
2448
  _execShell(cmd, timeoutMs = 300000, envOverride = null) {
package/src/paths.js CHANGED
@@ -403,6 +403,20 @@ function whereBinary(names, env) {
403
403
  return null;
404
404
  }
405
405
 
406
+ /**
407
+ * Every PATH hit for a name, in PATH order — for callers that filter the list
408
+ * (skip the bundled node, find the first one that runs). Like whereBinary it
409
+ * walks PATH in-process: `where` prints in the OEM codepage, and decoding that
410
+ * as UTF-8 mangles any non-ASCII profile path.
411
+ *
412
+ * @param {string} name
413
+ * @param {object} [env] env whose PATH to search (defaults to process.env)
414
+ * @returns {string[]}
415
+ */
416
+ function whereAll(name, env) {
417
+ return _searchPath(name, env || process.env);
418
+ }
419
+
406
420
  /**
407
421
  * Drop the bin-dir snapshots and every lookup answered from them — for when a
408
422
  * background probe has just learned of directories they did not include.
@@ -1304,6 +1318,7 @@ module.exports = {
1304
1318
  getEnhancedEnv,
1305
1319
  whichBinary,
1306
1320
  whereBinary,
1321
+ whereAll,
1307
1322
  resolveManagedNpmBinary,
1308
1323
  resolveManagedNpmPackageBin,
1309
1324
  isNodeShebangScript,
@@ -13,8 +13,9 @@ const fs = require('fs');
13
13
  const os = require('os');
14
14
  const path = require('path');
15
15
  const readline = require('readline');
16
- const { execSync, spawnSync } = require('child_process');
16
+ const { spawnSync } = require('child_process');
17
17
  const { npmUrls } = require('./mirrors');
18
+ const { whereBinary } = require('./paths');
18
19
 
19
20
  const PKG_NAME = '@openagents-org/agent-launcher';
20
21
  const CACHE_FILE = path.join(os.homedir(), '.openagents', '.update-check.json');
@@ -187,10 +188,9 @@ function findNpmBin(opts = {}) {
187
188
  }
188
189
  // PATH fallback (injectable so tests can exercise the hard fallback
189
190
  // deterministically regardless of the host OS).
190
- const lookup = opts.lookup || (() =>
191
- execSync(platform === 'win32' ? 'where npm' : 'which npm', {
192
- encoding: 'utf-8', timeout: 3000,
193
- }).trim().split(/\r?\n/)[0]);
191
+ // Walked in-process rather than via `where`, whose OEM-codepage output
192
+ // mangled non-ASCII profile paths.
193
+ const lookup = opts.lookup || (() => whereBinary('npm'));
194
194
  try {
195
195
  return lookup() || (platform === 'win32' ? 'npm.cmd' : 'npm');
196
196
  } catch { return platform === 'win32' ? 'npm.cmd' : 'npm'; }