@openagents-org/agent-launcher 0.2.181 → 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/icons/antigravity.svg +1 -0
- package/icons/mini-swe-agent.svg +7 -0
- package/package.json +3 -2
- package/registry.json +1137 -622
- package/src/adapters/aider.js +10 -3
- package/src/adapters/amp.js +11 -3
- package/src/adapters/antigravity.js +10 -2
- package/src/adapters/base.js +46 -0
- package/src/adapters/claude.js +6 -1
- package/src/adapters/cline.js +5 -1
- package/src/adapters/codebuddy.js +6 -1
- package/src/adapters/codex.js +10 -2
- package/src/adapters/commandcode.js +5 -1
- package/src/adapters/copilot.js +6 -1
- package/src/adapters/cursor.js +5 -1
- package/src/adapters/deepseek.js +5 -1
- package/src/adapters/gemini.js +10 -2
- package/src/adapters/goose.js +6 -2
- package/src/adapters/hermes.js +23 -53
- package/src/adapters/kimi.js +5 -1
- package/src/adapters/llm-direct.js +6 -0
- package/src/adapters/mini.js +10 -3
- package/src/adapters/openclaw.js +5 -1
- package/src/adapters/opencode.js +6 -1
- package/src/adapters/openworker.js +15 -1
- package/src/adapters/pi.js +6 -1
- package/src/adapters/workspace-prompt.js +23 -12
- package/src/daemon.js +5 -1
- package/src/installer.js +57 -16
- package/src/paths.js +98 -34
- package/src/probe.js +4 -1
- package/src/wsl.js +579 -0
package/src/adapters/pi.js
CHANGED
|
@@ -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
|
|
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
|
|
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 {
|
|
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
|
|
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
|
-
/**
|
|
866
|
-
|
|
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
|
-
|
|
869
|
-
if (p
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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');
|