@openagents-org/agent-launcher 0.2.181 → 0.2.183

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.
@@ -34,7 +34,11 @@ const crypto = require('crypto');
34
34
  const fs = require('fs');
35
35
  const os = require('os');
36
36
  const path = require('path');
37
- const { execFileSync, execSync, spawn } = require('child_process');
37
+ const { execFileSync, execSync } = require('child_process');
38
+ // spawn() here is the WSL bridge from ../wsl: same signature as
39
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
40
+ // lives on the other side of the Windows/WSL boundary.
41
+ const { spawn } = require('../wsl');
38
42
 
39
43
  const BaseAdapter = require('./base');
40
44
  const { formatAttachmentsForPrompt, SESSION_DEFAULT_RE, generateSessionTitle } = require('./utils');
@@ -1391,6 +1395,7 @@ class PiAdapter extends BaseAdapter {
1391
1395
  endpoint: this.endpoint,
1392
1396
  token: this.token,
1393
1397
  mode: this._mode,
1398
+ model: this.modelLabel(),
1394
1399
  disabledModules: this.disabledModules,
1395
1400
  browserEnabled,
1396
1401
  });
@@ -121,7 +121,7 @@ function workspaceSkillName(agentName) {
121
121
  * - `'skills'` → points at the agent's workspace skill (Bash + curl),
122
122
  * since no MCP server is spawned and that tool would not exist.
123
123
  */
124
- function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'execute', toolMode = 'mcp') {
124
+ function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'execute', toolMode = 'mcp', model = null) {
125
125
  const priorContext = toolMode === 'skills'
126
126
  ? (
127
127
  `When you need prior context, use the ${workspaceSkillName(agentName)} skill to ` +
@@ -135,6 +135,15 @@ function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'exe
135
135
  'channel argument the tool falls back to a default channel that may ' +
136
136
  'be different from where you are.\n'
137
137
  );
138
+ // The model is the one fact about itself an agent cannot look up. Asked
139
+ // "what model are you?", a CLI-driven agent answers from whatever its
140
+ // weights remember about their own training — which is how an agent
141
+ // configured with deepseek came to tell a user it was Claude. The launcher
142
+ // knows the answer exactly (it is what was passed to the CLI), so state it
143
+ // rather than leaving the agent to guess.
144
+ const modelLine = String(model || '').trim()
145
+ ? `- Model: ${String(model).trim()} (this is the model you are running on — do not guess otherwise)\n`
146
+ : '';
138
147
  return (
139
148
  `You are agent '${agentName}' connected to an OpenAgents workspace.\n` +
140
149
  'Your text responses are automatically posted to the workspace chat ' +
@@ -142,7 +151,9 @@ function buildWorkspaceIdentity(agentName, workspaceId, channelName, mode = 'exe
142
151
  '## Workspace Context\n' +
143
152
  `- Workspace ID: ${workspaceId}\n` +
144
153
  `- Channel: ${channelName} (this is the channel you are currently speaking in)\n` +
145
- `- Mode: ${mode}\n\n` +
154
+ `- Mode: ${mode}\n` +
155
+ modelLine +
156
+ '\n' +
146
157
  priorContext
147
158
  );
148
159
  }
@@ -931,10 +942,10 @@ function buildPinnedSections({ toolMode = 'mcp', channelName, mode = 'execute',
931
942
  * writeAccess? }. Both default to off — other adapters that reuse this
932
943
  * builder (e.g. Gemini) are unaffected unless they pass them.
933
944
  */
934
- function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = 'execute', browserEnabled = false, toolMode = 'mcp', decisionLog = null, glossary = null }) {
945
+ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = 'execute', browserEnabled = false, toolMode = 'mcp', decisionLog = null, glossary = null, model = null }) {
935
946
  const skillName = workspaceSkillName(agentName);
936
947
  const parts = [];
937
- parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, toolMode));
948
+ parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, toolMode, model));
938
949
  parts.push(toolMode === 'skills' ? buildClaudeSkillsToolBlock(skillName) : buildClaudeMcpToolBlock());
939
950
  parts.push(buildBrowserDirective(browserEnabled));
940
951
  parts.push(buildCollaborationPrompt(toolMode, skillName));
@@ -960,9 +971,9 @@ function buildClaudeSystemPrompt({ agentName, workspaceId, channelName, mode = '
960
971
  * buildClaudeSystemPrompt (with skills-mode tool phrasing, since these
961
972
  * agents reach the knowledge API through curl commands).
962
973
  */
963
- function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, decisionLog = null, glossary = null }) {
974
+ function buildOpenclawSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, decisionLog = null, glossary = null, model = null }) {
964
975
  const parts = [];
965
- parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills'));
976
+ parts.push(buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills', model));
966
977
  parts.push(buildBrowserDirective(browserEnabled));
967
978
  parts.push(buildCollaborationPrompt('skills', workspaceSkillName(agentName)));
968
979
  parts.push(buildModePrompt(mode));
@@ -1009,8 +1020,8 @@ function buildOpenclawSkillMd({ endpoint, workspaceId, token, agentName, channel
1009
1020
  * `decisionLog` / `glossary` opt in to knowledge pinning exactly as in
1010
1021
  * buildOpenclawSystemPrompt.
1011
1022
  */
1012
- function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, decisionLog = null, glossary = null, isWindows = process.platform === 'win32' }) {
1013
- const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
1023
+ function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, decisionLog = null, glossary = null, isWindows = process.platform === 'win32', model = null }) {
1024
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills', model);
1014
1025
  const directive = buildBrowserDirective(browserEnabled);
1015
1026
  const collab = buildCollaborationPrompt('skills', workspaceSkillName(agentName));
1016
1027
  const modePrompt = buildModePrompt(mode) + buildWindowsShellHint(isWindows);
@@ -1031,8 +1042,8 @@ function buildOpenCodeSystemPrompt({ agentName, workspaceId, channelName, endpoi
1031
1042
  * NOTE: this is APPENDED, never passed via `--system-prompt` — that flag would
1032
1043
  * REPLACE Pi's own coding-assistant prompt and strip its tool instructions.
1033
1044
  */
1034
- function buildPiSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
1035
- const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
1045
+ function buildPiSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, model = null }) {
1046
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills', model);
1036
1047
  const directive = buildBrowserDirective(browserEnabled);
1037
1048
  const collab = buildCollaborationPrompt('skills', workspaceSkillName(agentName));
1038
1049
  const modePrompt = buildModePrompt(mode);
@@ -1060,8 +1071,8 @@ function buildPiSystemPrompt({ agentName, workspaceId, channelName, endpoint, to
1060
1071
  * conversation, so it is sent once and still in context on every later turn
1061
1072
  * — which is why it must read as a briefing, not as a system directive.
1062
1073
  */
1063
- function buildOpenWorkerSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false }) {
1064
- const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills');
1074
+ function buildOpenWorkerSystemPrompt({ agentName, workspaceId, channelName, endpoint, token, mode = 'execute', disabledModules, browserEnabled = false, model = null }) {
1075
+ const identity = buildWorkspaceIdentity(agentName, workspaceId, channelName, mode, 'skills', model);
1065
1076
  const directive = buildBrowserDirective(browserEnabled);
1066
1077
  const collab = buildCollaborationPrompt('skills', workspaceSkillName(agentName));
1067
1078
  const modePrompt = buildModePrompt(mode);
package/src/daemon.js CHANGED
@@ -2,7 +2,11 @@
2
2
 
3
3
  const fs = require('fs');
4
4
  const path = require('path');
5
- const { spawn, execSync, execFileSync } = require('child_process');
5
+ const { execSync, execFileSync } = require('child_process');
6
+ // spawn() here is the WSL bridge from ./wsl: same signature as
7
+ // child_process.spawn, and a straight pass-through unless the resolved CLI
8
+ // lives on the other side of the Windows/WSL boundary.
9
+ const { spawn } = require('./wsl');
6
10
  const os = require('os');
7
11
  const { WorkspaceClient } = require('./workspace-client');
8
12
  const { getEnhancedEnv, whichBinary, IS_WINDOWS, defaultAgentWorkdir } = require('./paths');
package/src/installer.js CHANGED
@@ -5,6 +5,7 @@ const os = require('os');
5
5
  const path = require('path');
6
6
  const { execSync, exec } = require('child_process');
7
7
  const { whichBinary, getEnhancedEnv, getRuntimePrefix, clearBinaryLookupCache, aiderBinDirs, resolveBinaryInKnownDirs } = require('./paths');
8
+ const { isWslBinary, bridgedCommandString, wslHomeUnc } = require('./wsl');
8
9
  const { EnvManager } = require('./env');
9
10
  const { nodeDistUrls, installRegistry } = require('./mirrors');
10
11
  const { readinessReason, REASON } = require('./adapters/health-status');
@@ -173,6 +174,13 @@ class Installer {
173
174
  return { installed: false, managed: false, location: null };
174
175
  }
175
176
 
177
+ // Installed inside a WSL distro. Never 'managed': the launcher did not put
178
+ // it there and cannot uninstall it, and the isolated-runtime bookkeeping
179
+ // below is all about Windows paths that an in-distro binary can never match.
180
+ if (isWslBinary(binaryPath)) {
181
+ return { installed: true, managed: false, location: 'wsl' };
182
+ }
183
+
176
184
  // Verify it's not a stale shim pointing to a missing package
177
185
  const openagentsDir = path.join(os.homedir(), '.openagents');
178
186
  if (binaryPath.startsWith(openagentsDir)) {
@@ -580,6 +588,11 @@ class Installer {
580
588
  * shebang), so it's applied on every platform for consistency.
581
589
  */
582
590
  _versionProbeCommand(binary) {
591
+ // A binary inside WSL has to be asked through wsl.exe; running the Linux
592
+ // path as a Windows command yields "not recognized" and the marketplace
593
+ // then shows an installed agent with no version at all.
594
+ const bridged = bridgedCommandString(binary, ['--version']);
595
+ if (bridged) return bridged;
583
596
  if (/\.(mjs|cjs|js)$/i.test(binary)) {
584
597
  return `"${this._nodeBinary()}" "${binary}" --version`;
585
598
  }
@@ -744,14 +757,14 @@ class Installer {
744
757
  // authoritative; the legacy parser treats an empty {} as ready.
745
758
  const credsReady = checkReady.creds_json_has_entries
746
759
  ? false
747
- : this._checkCredsReady(checkReady);
760
+ : this._checkCredsReady(checkReady, agentType);
748
761
  // Content-free credential probes (opt-in via check_ready). Detect agents
749
762
  // whose sign-in is a local OAuth/credential FILE (e.g. Gemini's
750
763
  // ~/.gemini/oauth_creds.json) or a service-account file path, WITHOUT
751
764
  // parsing or logging the token. Additive: agents that don't declare
752
765
  // creds_no_parse / creds_path_env get identical behavior to before.
753
- const credsFile = this._evaluateCredsFile(checkReady);
754
- const credsPathReady = this._checkCredsPathEnv(checkReady, savedEnv);
766
+ const credsFile = this._evaluateCredsFile(checkReady, agentType);
767
+ const credsPathReady = this._checkCredsPathEnv(checkReady, savedEnv, agentType);
755
768
 
756
769
  let cliReady = false;
757
770
  if (checkReady.status_command && binary) {
@@ -839,10 +852,10 @@ class Installer {
839
852
  return keys.some((key) => !!(source && source[key]));
840
853
  }
841
854
 
842
- _checkCredsReady(checkReady) {
855
+ _checkCredsReady(checkReady, agentType) {
843
856
  if (checkReady.creds_file) {
844
857
  try {
845
- const credsPath = checkReady.creds_file.replace('~', os.homedir());
858
+ const credsPath = this._expandHome(checkReady.creds_file, agentType);
846
859
  if (fs.existsSync(credsPath)) {
847
860
  const stat = fs.statSync(credsPath);
848
861
  if (stat.isDirectory()) return fs.readdirSync(credsPath).length > 0;
@@ -862,14 +875,38 @@ class Installer {
862
875
  return false;
863
876
  }
864
877
 
865
- /** Expand a leading "~" to the running user's home directory. */
866
- _expandHome(p) {
878
+ /**
879
+ * Expand a leading "~" to the home directory the AGENT writes its sign-in to.
880
+ *
881
+ * Normally the running user's. For an agent that resolved inside WSL it is
882
+ * the distro's $HOME, reached over the \\wsl.localhost mount — `claude login`
883
+ * run in the distro leaves ~/.claude there, and expanding against the Windows
884
+ * profile instead reports a signed-in agent as "Not logged in".
885
+ */
886
+ _expandHome(p, agentType) {
867
887
  if (typeof p !== 'string' || !p) return p;
868
- if (p === '~') return os.homedir();
869
- if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(os.homedir(), p.slice(2));
888
+ const home = this._homeForAgent(agentType);
889
+ if (p === '~') return home;
890
+ if (p.startsWith('~/') || p.startsWith('~\\')) return path.join(home, p.slice(2));
870
891
  return p;
871
892
  }
872
893
 
894
+ /**
895
+ * Where this agent's dotfiles live, as a path this process can read. Falls
896
+ * back to the running user's home for every native install — and for a WSL
897
+ * one whose distro home could not be resolved, which is no worse than the
898
+ * behaviour before the WSL tier existed.
899
+ */
900
+ _homeForAgent(agentType) {
901
+ if (!agentType) return os.homedir();
902
+ try {
903
+ if (isWslBinary(this._whichBinary(agentType))) {
904
+ return wslHomeUnc() || os.homedir();
905
+ }
906
+ } catch { /* resolution is best-effort; the host home is the safe answer */ }
907
+ return os.homedir();
908
+ }
909
+
873
910
  /**
874
911
  * Existence/readability of a credential file. Returns 'present' |
875
912
  * 'unreadable' | 'absent'. A non-empty readable file (or a non-empty
@@ -881,9 +918,9 @@ class Installer {
881
918
  * against the running user's home (the daemon/launcher user), not the
882
919
  * renderer/browser user.
883
920
  */
884
- _credsFileState(checkReady) {
921
+ _credsFileState(checkReady, agentType) {
885
922
  if (!checkReady || !checkReady.creds_file) return 'absent';
886
- const p = this._expandHome(checkReady.creds_file);
923
+ const p = this._expandHome(checkReady.creds_file, agentType);
887
924
  let exists = false;
888
925
  try { exists = fs.existsSync(p); } catch { return 'unreadable'; }
889
926
  if (!exists) return 'absent';
@@ -922,11 +959,11 @@ class Installer {
922
959
  * parse + match `creds_key` via _checkCredsReady) keep their exact behavior.
923
960
  * Returns { ready, unreadable }.
924
961
  */
925
- _evaluateCredsFile(checkReady) {
962
+ _evaluateCredsFile(checkReady, agentType) {
926
963
  if (!checkReady || !checkReady.creds_file || !checkReady.creds_no_parse) {
927
964
  return { ready: false, unreadable: false };
928
965
  }
929
- const state = this._credsFileState(checkReady);
966
+ const state = this._credsFileState(checkReady, agentType);
930
967
  return { ready: state === 'present', unreadable: state === 'unreadable' };
931
968
  }
932
969
 
@@ -936,14 +973,14 @@ class Installer {
936
973
  * exists and is a readable file — a non-empty value alone is NOT enough.
937
974
  * Checks both the process env and the agent's saved env.
938
975
  */
939
- _checkCredsPathEnv(checkReady, savedEnv) {
976
+ _checkCredsPathEnv(checkReady, savedEnv, agentType) {
940
977
  const names = checkReady && checkReady.creds_path_env;
941
978
  if (!Array.isArray(names) || !names.length) return false;
942
979
  for (const name of names) {
943
980
  const raw = ((process.env[name] || (savedEnv && savedEnv[name]) || '') + '').trim();
944
981
  if (!raw) continue;
945
982
  try {
946
- const p = this._expandHome(raw);
983
+ const p = this._expandHome(raw, agentType);
947
984
  if (!fs.existsSync(p)) continue;
948
985
  fs.accessSync(p, fs.constants.R_OK);
949
986
  if (fs.statSync(p).isFile()) return true;
@@ -1568,8 +1605,12 @@ class Installer {
1568
1605
  const managed = this._resolveManagedBinary(agentType, entry, binary, aliases);
1569
1606
  if (managed) return managed;
1570
1607
 
1608
+ // allowWsl:false — whichBinary would otherwise end its own search inside
1609
+ // the distro, and this method still has two NATIVE tiers to run below. A
1610
+ // Windows copy must win over a WSL one; resolveBinaryInKnownDirs() reaches
1611
+ // the distro at the very end, once everything on this side has missed.
1571
1612
  for (const candidate of [binary, ...aliases]) {
1572
- const found = whichBinary(candidate);
1613
+ const found = whichBinary(candidate, { allowWsl: false });
1573
1614
  if (found) return found;
1574
1615
  }
1575
1616
  // Fallback: resolve the installed package's OWN bin from its package.json
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');