@openagents-org/agent-launcher 0.2.180 → 0.2.182

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/src/paths.js CHANGED
@@ -12,6 +12,12 @@ const path = require('path');
12
12
  const fs = require('fs');
13
13
  const os = require('os');
14
14
  const { execSync, execFileSync } = require('child_process');
15
+ const {
16
+ isRunningInWsl,
17
+ resolveWslBinary,
18
+ clearWslBinaryCache,
19
+ wslBinDirs,
20
+ } = require('./wsl');
15
21
 
16
22
  const IS_WINDOWS = process.platform === 'win32';
17
23
  const IS_MACOS = process.platform === 'darwin';
@@ -183,24 +189,84 @@ function getEnhancedEnv(baseEnv) {
183
189
 
184
190
  /**
185
191
  * Find a binary by name. Returns full path or null.
192
+ *
193
+ * On Windows the search ends INSIDE WSL when nothing native turned up: an agent
194
+ * CLI the user installed in their distro is a real, usable install, and the
195
+ * Linux path that comes back is what adapters hand to the wsl.js spawn bridge.
196
+ * It is deliberately the last thing tried — a native copy always wins — and
197
+ * `allowWsl: false` opts out for callers (installer.js) that have further
198
+ * native tiers of their own to run first.
199
+ *
200
+ * @param {string} name
201
+ * @param {{allowWsl?: boolean}} [opts]
186
202
  */
187
- function whichBinary(name) {
203
+ function whichBinary(name, { allowWsl = true } = {}) {
188
204
  if (!name) return null;
189
205
  const currentPATH = process.env.PATH || '';
190
- const cacheKey = `${name}\0${currentPATH}`;
206
+ const cacheKey = `${name}\0${currentPATH}\0${allowWsl ? 'wsl' : 'native'}`;
191
207
  const cached = whichBinaryCache.get(cacheKey);
192
208
  if (cached && Date.now() - cached.at < PATH_LOOKUP_CACHE_TTL_MS) {
193
209
  return cached.value;
194
210
  }
195
211
 
196
- // On a non-English Windows the console OUTPUT codepage is OEM (e.g. 936/GBK on
197
- // zh-CN), so `where` prints a path whose non-ASCII bytes don't match the utf-8
198
- // decoding execSync does — a Chinese username comes back mangled (e.g.
199
- // `C:\Users\??.?[\…`) and yields ENOENT downstream. We can't reliably re-encode
200
- // it (and forcing `chcp` is unsafe under windowsHide's console-less cmd), so we
201
- // existence-check every hit and only return one that's real; a mangled path
202
- // fails and the caller falls through to its Node-derived tiers (built from
203
- // process.env / os.homedir, which the OS hands us as correct UTF-16).
212
+ let value = _whichNative(name);
213
+ if (!value && allowWsl) value = resolveWslBinary(name);
214
+ whichBinaryCache.set(cacheKey, { value, at: Date.now() });
215
+ return value;
216
+ }
217
+
218
+ /** The PATH lookup proper — `where`/`which`, this side of any boundary. */
219
+ function _whichNative(name) {
220
+ let hits = _runWhich(name);
221
+
222
+ if (isRunningInWsl()) {
223
+ // Interop puts the Windows PATH on the distro's PATH, so `which claude`
224
+ // happily answers /mnt/c/Users/…/AppData/Roaming/npm/claude — npm's
225
+ // extensionless POSIX shim, a sh script that execs node.exe with Windows
226
+ // paths. Linux cannot run it, and returning it turns "not installed" into
227
+ // the more confusing "installed but every run fails". Only a real PE image
228
+ // crosses back this way, which interop does exec with argv intact.
229
+ hits = hits.filter((h) => !_isWindowsMount(h) || /\.exe$/i.test(h));
230
+ // `which cursor-agent` can't see cursor-agent.exe — the name on disk
231
+ // carries the suffix — so ask for it explicitly before giving up.
232
+ if (!hits.length && !/\.exe$/i.test(name)) hits = _runWhich(`${name}.exe`);
233
+ }
234
+
235
+ let value = hits[0] || null;
236
+ if (IS_WINDOWS && hits.length) {
237
+ // `where` can list npm's extensionless POSIX shim (node_modules/.bin/
238
+ // claude, a sh script) ahead of claude.cmd. Windows cannot execute that
239
+ // file — spawning it fails and the probe misreports an installed CLI as
240
+ // "not installed" while the chat adapters (which prefer .cmd) work fine.
241
+ // Prefer a hit Windows can run; failing that, a runnable sibling of the
242
+ // shim; only then fall back to the raw first hit.
243
+ const RUNNABLE = /\.(cmd|exe|bat|com)$/i;
244
+ const runnable = hits.find((h) => RUNNABLE.test(h));
245
+ if (runnable) {
246
+ value = runnable;
247
+ } else {
248
+ for (const h of hits) {
249
+ const sibling = ['.cmd', '.exe', '.bat'].map((e) => h + e).find((c) => fs.existsSync(c));
250
+ if (sibling) { value = sibling; break; }
251
+ }
252
+ }
253
+ }
254
+ return value;
255
+ }
256
+
257
+ /**
258
+ * Existing paths `where`/`which` reports for a name, in its own order.
259
+ *
260
+ * On a non-English Windows the console OUTPUT codepage is OEM (e.g. 936/GBK on
261
+ * zh-CN), so `where` prints a path whose non-ASCII bytes don't match the utf-8
262
+ * decoding execSync does — a Chinese username comes back mangled (e.g.
263
+ * `C:\Users\??.?[\…`) and yields ENOENT downstream. We can't reliably re-encode
264
+ * it (and forcing `chcp` is unsafe under windowsHide's console-less cmd), so we
265
+ * existence-check every hit and only return ones that are real; a mangled path
266
+ * fails and the caller falls through to its Node-derived tiers (built from
267
+ * process.env / os.homedir, which the OS hands us as correct UTF-16).
268
+ */
269
+ function _runWhich(name) {
204
270
  const cmd = IS_WINDOWS ? `where ${name}` : `which ${name}`;
205
271
  try {
206
272
  const result = execSync(cmd, {
@@ -221,33 +287,21 @@ function whichBinary(name) {
221
287
  const hit = line.trim();
222
288
  if (hit && fs.existsSync(hit)) hits.push(hit);
223
289
  }
224
- let value = hits[0] || null;
225
- if (IS_WINDOWS && hits.length) {
226
- // `where` can list npm's extensionless POSIX shim (node_modules/.bin/
227
- // claude, a sh script) ahead of claude.cmd. Windows cannot execute that
228
- // file — spawning it fails and the probe misreports an installed CLI as
229
- // "not installed" while the chat adapters (which prefer .cmd) work fine.
230
- // Prefer a hit Windows can run; failing that, a runnable sibling of the
231
- // shim; only then fall back to the raw first hit.
232
- const RUNNABLE = /\.(cmd|exe|bat|com)$/i;
233
- const runnable = hits.find((h) => RUNNABLE.test(h));
234
- if (runnable) {
235
- value = runnable;
236
- } else {
237
- for (const h of hits) {
238
- const sibling = ['.cmd', '.exe', '.bat'].map((e) => h + e).find((c) => fs.existsSync(c));
239
- if (sibling) { value = sibling; break; }
240
- }
241
- }
242
- }
243
- whichBinaryCache.set(cacheKey, { value, at: Date.now() });
244
- return value;
290
+ return hits;
245
291
  } catch {
246
- whichBinaryCache.set(cacheKey, { value: null, at: Date.now() });
247
- return null;
292
+ return [];
248
293
  }
249
294
  }
250
295
 
296
+ /**
297
+ * A path on a Windows drive as mounted inside a distro (/mnt/c/…). /mnt is
298
+ * WSL's default automount root and the only one interop puts on PATH itself;
299
+ * a distro that moved it in /etc/wsl.conf simply gets the old behaviour.
300
+ */
301
+ function _isWindowsMount(p) {
302
+ return /^\/mnt\/[a-z]\//i.test(String(p || ''));
303
+ }
304
+
251
305
  /**
252
306
  * Codepage-safe `where`/`which` lookup, shared by every CLI adapter.
253
307
  *
@@ -916,6 +970,7 @@ function clearBinaryLookupCache() {
916
970
  knownBinDirsCache = { value: null, at: 0 };
917
971
  extraBinDirsCache = { value: null, at: 0, path: '' };
918
972
  whichBinaryCache.clear();
973
+ clearWslBinaryCache();
919
974
  // The login-shell / npm-prefix probes are deliberately NOT cleared: they each
920
975
  // cost a process spawn, they answer a question about the machine rather than
921
976
  // about any install, and this is called after every install/uninstall.
@@ -933,6 +988,11 @@ function primeBinaryLookup() {
933
988
  try { loginShellDirs(); } catch {}
934
989
  try { _npmPrefix(); } catch {}
935
990
  try { getKnownBinDirs(); } catch {}
991
+ // The WSL side has the same shape of cost — one `wsl.exe -e bash -ilc` to
992
+ // learn the distro's login PATH — and it is paid by the first lookup that
993
+ // misses natively, which is the marketplace enumerating agents nobody has
994
+ // installed. Warm it here instead.
995
+ try { wslBinDirs(); } catch {}
936
996
  }
937
997
 
938
998
  /**
@@ -1035,7 +1095,11 @@ function resolveBinaryInKnownDirs(names, agentType) {
1035
1095
  }
1036
1096
  }
1037
1097
  }
1038
- return null;
1098
+
1099
+ // Nothing native anywhere: the last place left to look is inside WSL, whose
1100
+ // filesystem none of the dirs above can reach. Last by construction — a
1101
+ // Windows copy of the CLI is always preferred to an in-distro one.
1102
+ return resolveWslBinary(list);
1039
1103
  }
1040
1104
 
1041
1105
 
package/src/probe.js CHANGED
@@ -26,7 +26,10 @@
26
26
  */
27
27
 
28
28
  const os = require('os');
29
- const { spawn } = require('child_process');
29
+ // spawn() here is the WSL bridge from ./wsl: same signature as
30
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
31
+ // lives on the other side of the Windows/WSL boundary.
32
+ const { spawn } = require('./wsl');
30
33
  const { getEnhancedEnv } = require('./paths');
31
34
  const { shouldUseShellForBinary } = require('./adapters/health-status');
32
35
  const { formatAuthGuidance } = require('./auth-guidance');
@@ -84,6 +84,40 @@ class WorkspaceClient {
84
84
  }
85
85
  }
86
86
 
87
+ /**
88
+ * Remove one agent from a workspace via
89
+ * DELETE /v1/workspaces/{workspaceId}/members/{agentName}.
90
+ *
91
+ * Unlike deleteWorkspace above this is NOT best-effort: the caller offered
92
+ * the user a choice ("also remove it from the workspace"), so a failure has
93
+ * to be reported rather than swallowed — silently leaving the member behind
94
+ * is the exact problem this exists to fix.
95
+ */
96
+ async removeMember(workspaceId, token, agentName) {
97
+ await this._delete(
98
+ `/v1/workspaces/${workspaceId}/members/${encodeURIComponent(agentName)}`,
99
+ this._wsHeaders(token),
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Rename one agent's DISPLAY name in a workspace, via
105
+ * PATCH /v1/workspaces/{workspaceId}/members/{agentName}.
106
+ *
107
+ * The agent's identity (`agent_name`) is untouched — this is the label the
108
+ * workspace shows and accepts as an @-mention alias, which is why the server
109
+ * rejects one that clashes with another member. An empty string clears the
110
+ * label there, falling back to `agent_name`, which is exactly what clearing
111
+ * the display name on this side means too.
112
+ */
113
+ async setMemberDisplayName(workspaceId, token, agentName, displayName) {
114
+ return this._patch(
115
+ `/v1/workspaces/${workspaceId}/members/${encodeURIComponent(agentName)}`,
116
+ { display_name: displayName || '' },
117
+ this._wsHeaders(token),
118
+ );
119
+ }
120
+
87
121
  /**
88
122
  * Join a workspace via POST /v1/join.
89
123
  */
@@ -514,6 +548,10 @@ class WorkspaceClient {
514
548
  const agents = (result && result.agents) || [];
515
549
  return agents.map((a) => ({
516
550
  agentName: (a.address || '').replace('openagents:', ''),
551
+ // The workspace's label for this agent, or null when it has none and
552
+ // falls back to agentName. Carried so a rename made in the workspace can
553
+ // be reflected back on the device.
554
+ displayName: a.display_name || null,
517
555
  role: a.role || 'member',
518
556
  status: a.status || 'offline',
519
557
  enabledSkills: a.enabled_skills || null,